78 lines
4.4 KiB
TypeScript
78 lines
4.4 KiB
TypeScript
import { useProjects } from "../api/queries.ts";
|
||
import type { Project } from "../api/types.ts";
|
||
import { MODULES } from "../modules/index.ts";
|
||
import type { HubModule } from "../modules/types.ts";
|
||
import { fmtStamp } from "../ui/format.ts";
|
||
import { IconButton, IconRefresh } from "../ui/index.ts";
|
||
import { useModuleActivity } from "./activity/activity.ts";
|
||
import { agentText } from "./activity/agentText.ts";
|
||
import { eventText } from "./activity/events.ts";
|
||
|
||
type WithStatus = HubModule & Required<Pick<HubModule, "status">>;
|
||
const WITH_STATUS = MODULES.filter((m): m is WithStatus => !!m.status);
|
||
|
||
// строка модуля: название, агент (точка + текст), значок обновления, свежесть, событие пакета.
|
||
// Живой регион role="status" стоит в строке постоянно и меняет только текст (T10.1 #94): вставка вместе с текстом не озвучивается
|
||
function ModuleRow({ m, project, variant }: { m: WithStatus; project: Project; variant: "expanded" | "collapsed" }) {
|
||
const act = useModuleActivity(m.name);
|
||
const { status } = m;
|
||
const fresh = status.freshness(project);
|
||
const label = status.refresh?.label;
|
||
const scope = status.scope === "all" ? "все проекты" : "проект";
|
||
const action = label ? `${label} — ${status.scope === "all" ? scope : `${scope} ${project.label}`}` : null;
|
||
const a = act.agent;
|
||
const av = a ? agentText(a) : null;
|
||
const agentLabel = av?.text ?? null, agentHint = av?.hint;
|
||
const freshText = fresh.error ? `синхронизация не удалась: ${fresh.error}`
|
||
: fresh.at ? `данные на ${fmtStamp(fresh.at)} · ${scope}` : "обновления ещё не было";
|
||
const ev = act.event ? eventText(act.event, label ?? "") : null;
|
||
const online = !!av?.online;
|
||
const collapsed = variant === "collapsed";
|
||
const eventEl = <span className={`data-status__event${ev ? ` data-status__event--${ev.tone}` : ""}${collapsed ? " visually-hidden" : ""}`}
|
||
role="status" title={ev?.text}>{ev?.text}</span>;
|
||
const button = action && (
|
||
<IconButton label={action} title={collapsed ? [m.title, agentLabel, freshText, ev?.text].filter(Boolean).join(" · ") : action}
|
||
disabled={act.busy} aria-busy={act.busy ? "true" : undefined}
|
||
icon={act.busy ? <span className="ui-spinner" aria-hidden="true" /> : <IconRefresh />} onClick={() => void act.start()} />);
|
||
if (collapsed) {
|
||
return (
|
||
<div className="data-status data-status--compact" data-module={m.name}>
|
||
{button}
|
||
{a && <span className={"data-status__dot" + (online ? " is-online" : "")} aria-hidden="true" />}
|
||
{eventEl}
|
||
</div>
|
||
);
|
||
}
|
||
return (
|
||
<div className="data-status" data-module={m.name}>
|
||
<div className="data-status__head">
|
||
<strong>{m.title}</strong>
|
||
{agentLabel && (
|
||
<span className="data-status__agent" title={agentHint}>
|
||
<span className={"data-status__dot" + (online ? " is-online" : "")} aria-hidden="true" />{agentLabel}
|
||
</span>)}
|
||
{button}
|
||
</div>
|
||
<span className={"data-status__sync" + (fresh.error ? " data-status__sync--error" : "")}>{freshText}</span>
|
||
{eventEl}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// блок состояния модулей панели: по строке на модуль, у которого есть данные для проекта (у вики без корня строки нет)
|
||
export function ModuleStatusBlock({ project, variant }: { project: string | null; variant: "expanded" | "collapsed" }) {
|
||
const p = useProjects().data?.find((x) => x.label === project);
|
||
if (!p) return null;
|
||
return <>{WITH_STATUS.filter((m) => m.status.freshness(p).available).map((m) => <ModuleRow key={m.name} m={m} project={p} variant={variant} />)}</>;
|
||
}
|
||
|
||
// узкий экран: события пакетов модулей — строкой под верхней строкой; контейнер role="status" в DOM всегда (T10.1 #94)
|
||
function ModuleEvent({ m }: { m: WithStatus }) {
|
||
const act = useModuleActivity(m.name);
|
||
const ev = act.event ? eventText(act.event, m.status.refresh?.label ?? "") : null;
|
||
return ev ? <div className={`topbar-event data-status__event--${ev.tone}`}>{ev.text}</div> : null;
|
||
}
|
||
export function ModuleEventStrip() {
|
||
return <div className="topbar-events" role="status" data-testid="topbar-events">{WITH_STATUS.map((m) => <ModuleEvent key={m.name} m={m} />)}</div>;
|
||
}
|