From 7f3591524836ffbed9080152e0379f937335e9b3 Mon Sep 17 00:00:00 2001 From: Willow C Reed Date: Tue, 20 Oct 2026 16:23:01 -0600 Subject: [PATCH 1/3] feat: high contrast interface theme --- .../dialogs/user-settings-dialog.svelte | 22 +++- .../lib/components/ui/button/button.svelte | 13 +- .../lib/components/ui/switch/switch.svelte | 4 +- .../[projectid]/colocation/[id]/+page.svelte | 4 +- .../[projectid]/servers/[id]/+page.svelte | 8 +- apps/dashboard/src/routes/layout.css | 119 ++++++++++++++++++ apps/dashboard/tests/accessibility.test.ts | 23 ++-- 7 files changed, 174 insertions(+), 19 deletions(-) diff --git a/apps/dashboard/src/lib/components/dialogs/user-settings-dialog.svelte b/apps/dashboard/src/lib/components/dialogs/user-settings-dialog.svelte index 918a9e1c..7db4422d 100644 --- a/apps/dashboard/src/lib/components/dialogs/user-settings-dialog.svelte +++ b/apps/dashboard/src/lib/components/dialogs/user-settings-dialog.svelte @@ -1,6 +1,6 @@ %sveltekit.head% diff --git a/apps/dashboard/src/lib/components/dialogs/user-settings-dialog.svelte b/apps/dashboard/src/lib/components/dialogs/user-settings-dialog.svelte index 7db4422d..6ab1f0b7 100644 --- a/apps/dashboard/src/lib/components/dialogs/user-settings-dialog.svelte +++ b/apps/dashboard/src/lib/components/dialogs/user-settings-dialog.svelte @@ -1,6 +1,6 @@ @@ -21,6 +26,8 @@ + contrast.syncFromStorage(event)} /> + diff --git a/apps/dashboard/src/routes/layout.css b/apps/dashboard/src/routes/layout.css index b9c148e8..6b3a3baf 100644 --- a/apps/dashboard/src/routes/layout.css +++ b/apps/dashboard/src/routes/layout.css @@ -5,7 +5,7 @@ @import "@fontsource-variable/inter"; @custom-variant dark (&:is(.dark *)); -@custom-variant high-contrast (&:is([data-theme="high-contrast"] *)); +@custom-variant high-contrast (&:is([data-contrast="more"] *)); :root { --gray-50: #fafafa; @@ -71,11 +71,11 @@ } /* - * High contrast mode. Applied via mode-watcher's `data-theme` attribute, so it - * stacks on top of light/dark. The red scale is remapped too, because many + * High contrast mode. Applied via `data-contrast="more"` on (see + * #lib/state/contrast.svelte.ts), so it stacks on top of light/dark. The red scale is remapped too, because many * components use raw `red-*` utilities rather than semantic tokens. */ -:root[data-theme="high-contrast"] { +:root[data-contrast="more"] { --red-200: #7a0d0d; --red-300: #7a0d0d; --red-400: #8f1010; @@ -106,7 +106,7 @@ --ring: #000000; } -.dark[data-theme="high-contrast"] { +.dark[data-contrast="more"] { --red-200: #ffe5e5; --red-300: #ffcccc; --red-400: #ffa3a3; @@ -201,7 +201,7 @@ background-color: var(--background); @apply text-foreground antialiased; } - [data-theme="high-contrast"] :focus-visible { + [data-contrast="more"] :focus-visible { outline: 2px solid var(--ring) !important; outline-offset: 2px; } @@ -216,57 +216,57 @@ * `hover:border-ring` still take precedence. */ @layer utilities { - :where([data-theme="high-contrast"]) [class~="border-border/20"], - :where([data-theme="high-contrast"]) [class~="border-border/30"], - :where([data-theme="high-contrast"]) [class~="border-border/40"], - :where([data-theme="high-contrast"]) [class~="border-border/50"], - :where([data-theme="high-contrast"]) [class~="border-border/60"], - :where([data-theme="high-contrast"]) [class~="border-border/80"], - :where([data-theme="high-contrast"]) [class~="border-input/30"] { + :where([data-contrast="more"]) [class~="border-border/20"], + :where([data-contrast="more"]) [class~="border-border/30"], + :where([data-contrast="more"]) [class~="border-border/40"], + :where([data-contrast="more"]) [class~="border-border/50"], + :where([data-contrast="more"]) [class~="border-border/60"], + :where([data-contrast="more"]) [class~="border-border/80"], + :where([data-contrast="more"]) [class~="border-input/30"] { border-color: var(--border); } - :where([data-theme="high-contrast"]) [class~="divide-border/20"] > :where(:not(:last-child)), - :where([data-theme="high-contrast"]) [class~="divide-border/30"] > :where(:not(:last-child)), - :where([data-theme="high-contrast"]) [class~="divide-border/40"] > :where(:not(:last-child)), - :where([data-theme="high-contrast"]) [class~="divide-border/50"] > :where(:not(:last-child)) { + :where([data-contrast="more"]) [class~="divide-border/20"] > :where(:not(:last-child)), + :where([data-contrast="more"]) [class~="divide-border/30"] > :where(:not(:last-child)), + :where([data-contrast="more"]) [class~="divide-border/40"] > :where(:not(:last-child)), + :where([data-contrast="more"]) [class~="divide-border/50"] > :where(:not(:last-child)) { border-color: var(--border); } - :where([data-theme="high-contrast"]) [class~="ring-foreground/10"] { + :where([data-contrast="more"]) [class~="ring-foreground/10"] { --tw-ring-color: var(--border); } - :where([data-theme="high-contrast"]) [class~="border-red-400/60"], - :where([data-theme="high-contrast"]) [class~="border-red-500/10"], - :where([data-theme="high-contrast"]) [class~="border-red-500/20"], - :where([data-theme="high-contrast"]) [class~="border-red-500/50"], - :where([data-theme="high-contrast"]) [class~="border-red-900/50"], - :where(.dark[data-theme="high-contrast"]) + :where([data-contrast="more"]) [class~="border-red-400/60"], + :where([data-contrast="more"]) [class~="border-red-500/10"], + :where([data-contrast="more"]) [class~="border-red-500/20"], + :where([data-contrast="more"]) [class~="border-red-500/50"], + :where([data-contrast="more"]) [class~="border-red-900/50"], + :where(.dark[data-contrast="more"]) [class~="dark:border-red-800/50"][class~="dark:border-red-800/50"], - :where(.dark[data-theme="high-contrast"]) + :where(.dark[data-contrast="more"]) [class~="dark:border-red-900/20"][class~="dark:border-red-900/20"], - :where(.dark[data-theme="high-contrast"]) + :where(.dark[data-contrast="more"]) [class~="dark:border-red-900/30"][class~="dark:border-red-900/30"], - :where(.dark[data-theme="high-contrast"]) + :where(.dark[data-contrast="more"]) [class~="dark:border-red-900/40"][class~="dark:border-red-900/40"], - :where(.dark[data-theme="high-contrast"]) + :where(.dark[data-contrast="more"]) [class~="dark:border-red-900/50"][class~="dark:border-red-900/50"] { border-color: var(--red-500); } - :where([data-theme="high-contrast"]) [class~="text-foreground/60"] { + :where([data-contrast="more"]) [class~="text-foreground/60"] { color: var(--foreground); } - :where([data-theme="high-contrast"]) [class~="text-muted-foreground/60"] { + :where([data-contrast="more"]) [class~="text-muted-foreground/60"] { color: var(--muted-foreground); } - :where([data-theme="high-contrast"]) [class~="text-red-200/80"], - :where([data-theme="high-contrast"]) [class~="text-red-300/70"], - :where([data-theme="high-contrast"]) [class~="text-red-400/60"], - :where([data-theme="high-contrast"]) [class~="text-red-400/80"] { + :where([data-contrast="more"]) [class~="text-red-200/80"], + :where([data-contrast="more"]) [class~="text-red-300/70"], + :where([data-contrast="more"]) [class~="text-red-400/60"], + :where([data-contrast="more"]) [class~="text-red-400/80"] { color: var(--red-400); } } diff --git a/apps/dashboard/tests/accessibility.test.ts b/apps/dashboard/tests/accessibility.test.ts index 8ce962ac..cd095753 100644 --- a/apps/dashboard/tests/accessibility.test.ts +++ b/apps/dashboard/tests/accessibility.test.ts @@ -18,24 +18,24 @@ function formatImpact(impact: string | null | undefined) { } const modes = [ - { label: 'dark', mode: 'dark', theme: '' }, - { label: 'light', mode: 'light', theme: '' }, - { label: 'dark high contrast', mode: 'dark', theme: 'high-contrast' }, - { label: 'light high contrast', mode: 'light', theme: 'high-contrast' } + { label: 'dark', mode: 'dark', contrast: 'off' }, + { label: 'light', mode: 'light', contrast: 'off' }, + { label: 'dark high contrast', mode: 'dark', contrast: 'on' }, + { label: 'light high contrast', mode: 'light', contrast: 'on' } ] as const; -for (const { label: modeLabel, mode, theme } of modes) { +for (const { label: modeLabel, mode, contrast } of modes) { for (const { label, path } of pages) { test(`Testing for accessibility violations on ${label} in ${modeLabel} mode.`, async ({ page, makeAxeBuilder }) => { await page.addInitScript( - ([selectedMode, selectedTheme]) => { + ([selectedMode, selectedContrast]) => { window.localStorage.setItem('mode-watcher-mode', selectedMode); - window.localStorage.setItem('mode-watcher-theme', selectedTheme); + window.localStorage.setItem('contrast-preference', selectedContrast); }, - [mode, theme] as const + [mode, contrast] as const ); await page.goto(`${localURL}${path}`, { waitUntil: 'networkidle' });