feat(a11y): contrast tokens, accent-solid, responsive drawer, keyboard menus
- Contrast: --fg-muted reaches 4.5:1 on every surface in all nine themes (dark default was 2.55:1); status chips and accent text clear 4.5:1 on their own tints. New --accent-solid(/-hover) + tailwind `accent-solid` is the fill behind white text (.btn-primary, Switch on, Checkbox tick); --accent is now a text colour. Switch off / unchecked Checkbox get a >=3:1 edge. - <html lang> follows the chosen language; a global prefers-reduced-motion rule; index.html body uses a token instead of text-gray-200. - Layout: the sidebar starts collapsed below lg and becomes an off-canvas drawer below md (menu button, backdrop, Escape, useDialog). - ContextMenu: focus moves in, arrows/Home/End, Escape/Tab close, focus returns, a keyboard-opened menu anchors to its target. Select is a real listbox button (aria-expanded, activedescendant, type-ahead). PageTabs implements APG tabs with a tabpanel. New .btn-secondary/.btn-danger/ .btn-icon and a .select class for native selects, all with focus rings. - StatusBar announces connection and "N need you" politely; Sidebar's connection modal uses useDialog; EmptyState/Skeleton/AiToolIcon a11y. - api.ts request(): a non-JSON 200 throws a translated EBADRESPONSE instead of "Unexpected token '<'".
This commit is contained in:
+37
-4
@@ -426,6 +426,7 @@ import type {
|
||||
} from "./types";
|
||||
|
||||
import { activeSourcesParam } from "./dataScope";
|
||||
import i18n from "i18next";
|
||||
|
||||
// Root path all endpoint paths are appended to. Kept relative (no host) so the
|
||||
// same client bundle works behind the Vite dev proxy and in same-origin prod.
|
||||
@@ -538,9 +539,22 @@ async function request<T>(path: string, options?: RequestInit): Promise<T> {
|
||||
status: res.status,
|
||||
// EGITAUTH from a lane git action: which host wants a login.
|
||||
auth: body?.error?.auth as GitAuthNeeded | undefined,
|
||||
// ESESSIONLIVE from a workspace start: the pane already holding it.
|
||||
runId: body?.error?.run_id as string | undefined,
|
||||
});
|
||||
}
|
||||
return res.json();
|
||||
// A 200 that is not JSON — the SPA's index.html from a proxy or a dev server
|
||||
// that missed the route — would otherwise surface as "Unexpected token '<'".
|
||||
const type = res.headers?.get?.("content-type");
|
||||
const notJson = () =>
|
||||
Object.assign(new Error(i18n.t("errors:badResponse")), {
|
||||
code: "EBADRESPONSE",
|
||||
status: res.status,
|
||||
});
|
||||
if (type && !/json/i.test(type)) throw notJson();
|
||||
return res.json().catch(() => {
|
||||
throw notJson();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -700,6 +714,11 @@ export const api = {
|
||||
lane?: number;
|
||||
/** `false` = not archived, `true` = archived only; omitted = both. */
|
||||
archived?: boolean;
|
||||
/** Only active sessions waiting on the user (`awaiting=1`). */
|
||||
awaiting?: boolean;
|
||||
/** ISO bounds on `started_at`. */
|
||||
from?: string;
|
||||
to?: string;
|
||||
sort_by?: string;
|
||||
sort_desc?: boolean;
|
||||
limit?: number;
|
||||
@@ -713,6 +732,9 @@ export const api = {
|
||||
if (params?.tool) qs.set("tool", params.tool);
|
||||
if (params?.lane !== undefined) qs.set("lane", String(params.lane));
|
||||
if (params?.archived !== undefined) qs.set("archived", params.archived ? "1" : "0");
|
||||
if (params?.awaiting) qs.set("awaiting", "1");
|
||||
if (params?.from) qs.set("from", params.from);
|
||||
if (params?.to) qs.set("to", params.to);
|
||||
if (params?.sort_by) qs.set("sort_by", params.sort_by);
|
||||
// `!== undefined` (not truthiness) so an explicit `sort_desc: false` is preserved.
|
||||
if (params?.sort_desc !== undefined) qs.set("sort_desc", String(params.sort_desc));
|
||||
@@ -847,10 +869,18 @@ export const api = {
|
||||
* @param params.offset Row offset.
|
||||
* @returns `{ agents }` — the matching agents (note: no `total` here).
|
||||
*/
|
||||
list: (params?: { status?: string; session_id?: string; limit?: number; offset?: number }) => {
|
||||
list: (params?: {
|
||||
status?: string;
|
||||
session_id?: string;
|
||||
/** Every agent of these sessions in one call. */
|
||||
session_ids?: string[];
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}) => {
|
||||
const qs = new URLSearchParams();
|
||||
if (params?.status) qs.set("status", params.status);
|
||||
if (params?.session_id) qs.set("session_id", params.session_id);
|
||||
if (params?.session_ids) qs.set("session_ids", params.session_ids.join(","));
|
||||
if (params?.limit) qs.set("limit", String(params.limit));
|
||||
if (params?.offset) qs.set("offset", String(params.offset));
|
||||
applyScope(qs); // narrow to the active data scope (source machines)
|
||||
@@ -1191,13 +1221,16 @@ export const api = {
|
||||
*
|
||||
* @param params Retention thresholds.
|
||||
* @param params.abandon_hours Idle-hours cutoff after which a session is abandoned.
|
||||
* @param params.purge_days Age-in-days cutoff after which rows are purged.
|
||||
* @param params.purge_days Age-in-days cutoff after which rows are purged (whole number ≥ 1, else 400).
|
||||
* @param params.dry_run Count only; nothing is changed.
|
||||
* @returns `{ ok, abandoned, purged_sessions, purged_events, purged_agents }`
|
||||
* — how many records each part of the sweep affected.
|
||||
*/
|
||||
cleanup: (params: { abandon_hours?: number; purge_days?: number }) =>
|
||||
cleanup: (params: { abandon_hours?: number; purge_days?: number; dry_run?: boolean }) =>
|
||||
request<{
|
||||
ok: boolean;
|
||||
/** Set when `dry_run` was sent: the counts are what WOULD change. */
|
||||
dry_run?: boolean;
|
||||
abandoned: number;
|
||||
purged_sessions: number;
|
||||
purged_events: number;
|
||||
|
||||
Reference in New Issue
Block a user