Files
AgentHub/client/src/components/Switch.tsx
T
nntrivi2001 8fb703c6db 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 '<'".
2026-10-05 13:22:00 +07:00

49 lines
1.7 KiB
TypeScript

/**
* @file Switch.tsx
* @description The app's ONE on/off switch (`role="switch"`): an accent track
* with a white knob, the same size everywhere. Settings rows, webhook targets
* and the AI Tools list all draw this — do not hand-roll another one; a
* yes/no choice inside a form or list is a `Checkbox` instead.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
export function Switch({
checked,
onChange,
label,
title,
disabled = false,
}: {
checked: boolean;
onChange: (v: boolean) => void;
/** Accessible name — a switch usually sits beside its visible label. */
label?: string;
title?: string;
disabled?: boolean;
}) {
return (
<button
type="button"
role="switch"
aria-checked={checked}
aria-label={label}
title={title}
disabled={disabled}
onClick={() => onChange(!checked)}
// On: the solid accent (white knob clears 3:1 in every theme). Off: a
// border-light track with an inset fg-muted/80 ring (≥3:1 in every theme), so the control's edge
// is visible on any surface (surface-4 alone was ~1:1 on a light card).
className={`relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1 focus-visible:ring-offset-surface-0 disabled:cursor-not-allowed disabled:opacity-50 ${
checked ? "bg-accent-solid" : "bg-border-light ring-1 ring-inset ring-fg-muted/80"
}`}
>
<span
aria-hidden
className={`inline-block h-4 w-4 rounded-full bg-white shadow transition-transform ${
checked ? "translate-x-[18px]" : "translate-x-0.5"
}`}
/>
</button>
);
}