diff --git a/admin/inertia/components/Alert.tsx b/admin/inertia/components/Alert.tsx index f0109eae..cde52a9e 100644 --- a/admin/inertia/components/Alert.tsx +++ b/admin/inertia/components/Alert.tsx @@ -115,7 +115,7 @@ export default function Alert({ switch (type) { case 'warning': - return 'text-desert-orange-dark' + return 'text-text-primary' case 'error': return 'text-desert-red-dark' case 'success': @@ -134,7 +134,7 @@ export default function Alert({ switch (type) { case 'warning': - return 'text-desert-orange-dark text-opacity-80' + return 'text-text-primary text-opacity-80' case 'error': return 'text-desert-red-dark text-opacity-80' case 'success': diff --git a/admin/inertia/components/Footer.tsx b/admin/inertia/components/Footer.tsx index 7368e9d7..c6dd862c 100644 --- a/admin/inertia/components/Footer.tsx +++ b/admin/inertia/components/Footer.tsx @@ -18,7 +18,7 @@ export default function Footer() { | setDebugModalOpen(true)} - className="text-sm/6 text-gray-500 hover:text-desert-green flex items-center gap-1 cursor-pointer" + className="text-sm/6 text-text-secondary hover:text-desert-green flex items-center gap-1 cursor-pointer" > Debug Info diff --git a/admin/inertia/components/MarkdocRenderer.tsx b/admin/inertia/components/MarkdocRenderer.tsx index f362239c..dc84665e 100644 --- a/admin/inertia/components/MarkdocRenderer.tsx +++ b/admin/inertia/components/MarkdocRenderer.tsx @@ -26,7 +26,7 @@ const Link = ({ {children} diff --git a/admin/inertia/components/StyledButton.tsx b/admin/inertia/components/StyledButton.tsx index 27dfc2e8..11227ab9 100644 --- a/admin/inertia/components/StyledButton.tsx +++ b/admin/inertia/components/StyledButton.tsx @@ -67,8 +67,8 @@ const StyledButton: React.FC = ({ case 'secondary': return clsx( 'bg-desert-tan text-white', - 'hover:bg-desert-tan-dark hover:shadow-lg', - 'active:bg-desert-tan-dark', + 'hover:bg-btn-tan-hover hover:shadow-lg', + 'active:bg-btn-tan-active', 'disabled:bg-desert-tan-lighter disabled:text-desert-stone-light', baseTransition, baseHover diff --git a/admin/inertia/components/StyledSidebar.tsx b/admin/inertia/components/StyledSidebar.tsx index eba0e10f..695fd887 100644 --- a/admin/inertia/components/StyledSidebar.tsx +++ b/admin/inertia/components/StyledSidebar.tsx @@ -90,7 +90,7 @@ const StyledSidebar: React.FC = ({ title, items }) => { Project NOMAD™ Command Center v{appVersion} setDebugModalOpen(true)} - className="text-gray-500 hover:text-desert-green inline-flex items-center gap-1 cursor-pointer" + className="text-text-secondary hover:text-desert-green inline-flex items-center gap-1 cursor-pointer" > Debug Info diff --git a/admin/inertia/components/drug-reference/InteractionColumn.tsx b/admin/inertia/components/drug-reference/InteractionColumn.tsx index f167fb37..0bea7260 100644 --- a/admin/inertia/components/drug-reference/InteractionColumn.tsx +++ b/admin/inertia/components/drug-reference/InteractionColumn.tsx @@ -35,7 +35,7 @@ export default function InteractionColumn({ entry, onRemove }: Props) { )} {isOtc && ( - + OTC )} diff --git a/admin/inertia/css/app.css b/admin/inertia/css/app.css index 0e6222d2..0c37cdbd 100644 --- a/admin/inertia/css/app.css +++ b/admin/inertia/css/app.css @@ -1,5 +1,8 @@ @import 'tailwindcss'; +/* dark: utilities follow the in-app theme toggle, not the OS color scheme. */ +@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *)); + @theme { --color-desert-white: #f6f6f4; --color-desert-sand: #f7eedc; @@ -48,6 +51,8 @@ /* Button interactive states (green hover/active swap conflicts with text color inversion) */ --color-btn-green-hover: #353518; --color-btn-green-active: #2a2a15; + --color-btn-tan-hover: #6b5d4f; + --color-btn-tan-active: #6b5d4f; } body { @@ -78,11 +83,11 @@ body { --color-desert-orange-dark: #c85815; --color-desert-orange: #c85815; --color-desert-orange-light: #e69556; - --color-desert-orange-lighter: #f0b87a; + --color-desert-orange-lighter: #8a3d0f; - /* Tan: lightened for readability */ + /* Tan: lightened for readability; the base stays dark enough for white button text */ --color-desert-tan-dark: #c9b99f; - --color-desert-tan: #a8927a; + --color-desert-tan: #80694f; --color-desert-tan-light: #8b7355; --color-desert-tan-lighter: #6b5d4f; @@ -110,13 +115,15 @@ body { --color-surface-elevated: #3d3c2a; --color-text-primary: #f7eedc; --color-text-secondary: #afafa5; - --color-text-muted: #8f8f82; + --color-text-muted: #a0a093; --color-border-default: #424420; --color-border-subtle: #353420; /* Button interactive states: darker green for hover/active on dark bg */ --color-btn-green-hover: #474a28; --color-btn-green-active: #3a3c24; + --color-btn-tan-hover: #705c45; + --color-btn-tan-active: #5e5040; color-scheme: dark; } diff --git a/admin/inertia/pages/supply-depot.tsx b/admin/inertia/pages/supply-depot.tsx index 3bf700e6..84fa96f1 100644 --- a/admin/inertia/pages/supply-depot.tsx +++ b/admin/inertia/pages/supply-depot.tsx @@ -68,12 +68,12 @@ const CATEGORIES = [ const CATEGORY_COLORS: Record = { productivity: 'border border-desert-green-light bg-desert-green-lighter text-desert-green-dark', - media: 'border border-desert-tan-light bg-desert-tan-lighter text-desert-tan-dark', - security: 'border border-desert-red-light bg-desert-red-lighter text-desert-red-dark', - networking: 'border border-desert-stone-light bg-desert-stone-lighter text-desert-stone-dark', - utility: 'border border-desert-olive-light bg-desert-olive-lighter text-desert-olive-dark', + media: 'border border-desert-tan-light bg-desert-tan-lighter text-text-primary', + security: 'border border-desert-red-light bg-desert-red-lighter text-text-primary', + networking: 'border border-desert-stone-light bg-desert-stone-lighter text-text-primary', + utility: 'border border-desert-olive-light bg-desert-olive-lighter text-text-primary', ai: 'border border-desert-green bg-desert-green-light text-desert-green-darker', - education: 'border border-desert-orange-light bg-desert-orange-lighter text-desert-orange-dark', + education: 'border border-desert-orange-light bg-desert-orange-lighter text-text-primary', custom: 'border border-border-subtle bg-surface-secondary text-text-secondary', } @@ -978,7 +978,7 @@ function AppCard({ ) : null} {service.is_user_modified && !service.is_custom ? ( modified @@ -986,7 +986,7 @@ function AppCard({ ) : null} {service.is_deprecated ? ( legacy diff --git a/admin/tests/unit/dark_mode_contrast.spec.ts b/admin/tests/unit/dark_mode_contrast.spec.ts index 1abced11..166f5ab4 100644 --- a/admin/tests/unit/dark_mode_contrast.spec.ts +++ b/admin/tests/unit/dark_mode_contrast.spec.ts @@ -7,6 +7,11 @@ const builderTagSelector = await readFile( new URL('../../inertia/components/BuilderTagSelector.tsx', import.meta.url), 'utf8' ) +const alert = await readFile(new URL('../../inertia/components/Alert.tsx', import.meta.url), 'utf8') +const supplyDepot = await readFile( + new URL('../../inertia/pages/supply-depot.tsx', import.meta.url), + 'utf8' +) const darkTheme = css.match(/\[data-theme="dark"\]\s*\{(?[\s\S]*?)\n\}/)?.groups?.tokens @@ -90,3 +95,50 @@ test('Builder Tag controls pair their inverted background with semantic text', ( 'Builder Tag text and background must have at least 4.5:1 contrast' ) }) + +test('dark muted text clears WCAG AA on the page and card surfaces (#1418)', () => { + const muted = token('--color-text-muted') + for (const surface of [ + token('--color-desert-sand'), + token('--color-surface-primary'), + token('--color-surface-secondary'), + ]) { + assert.ok(contrast(muted, surface) >= 4.5, `${muted} on ${surface} must have at least 4.5:1`) + } +}) + +test('dark category chips pair each tinted background with primary text (#1418)', () => { + for (const family of ['tan', 'red', 'stone', 'olive', 'orange']) { + assert.doesNotMatch( + supplyDepot, + new RegExp(`bg-desert-${family}-lighter text-desert-${family}-dark`), + `${family} chips must not pair two tokens that both invert in dark mode` + ) + const background = token(`--color-desert-${family}-lighter`) + assert.ok( + contrast(token('--color-text-primary'), background) >= 4.5, + `chip text on ${background} must have at least 4.5:1` + ) + } +}) + +test('dark warning alerts pair the orange background with primary text (#1418)', () => { + assert.doesNotMatch(alert, /case 'warning':\s*return 'text-desert-orange-dark/) + assert.ok( + contrast(token('--color-text-primary'), token('--color-desert-orange-lighter')) >= 4.5, + 'warning alert text must have at least 4.5:1 contrast' + ) +}) + +test('dark secondary buttons keep white text readable at rest, hover and active (#1418)', () => { + for (const name of ['--color-desert-tan', '--color-btn-tan-hover', '--color-btn-tan-active']) { + assert.ok(contrast('#ffffff', token(name)) >= 4.5, `white on ${name} must have at least 4.5:1`) + } +}) + +test('dark: utilities follow the in-app theme, not the OS color scheme (#1418)', () => { + assert.match( + css, + /@custom-variant dark \(&:where\(\[data-theme="dark"\], \[data-theme="dark"\] \*\)\);/ + ) +})
Project NOMAD™ Command Center v{appVersion}