8fb703c6db
- 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 '<'".
49 lines
1.7 KiB
TypeScript
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>
|
|
);
|
|
}
|