From 1b5b2451799995822e889d3392b70e6f6d86432d Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 25 Jul 2026 09:46:51 +0800 Subject: [PATCH 1/6] chore: implement dark mode audit recommendations --- eslint-rules/no-hardcoded-hex.mjs | 31 +++ eslint.config.mjs | 3 + playwright.config.ts | 3 + src/app/globals.css | 30 +++ src/app/manifest.ts | 2 - .../clinical-dashboard/ClinicalSidebar.tsx | 2 +- .../provider-brand-icons.tsx | 11 +- .../clinical-dashboard/settings-dialog.tsx | 8 +- .../clinical-dashboard/use-theme.ts | 15 +- .../favourites-library-redesign-page.tsx | 196 +++++++++--------- 10 files changed, 189 insertions(+), 112 deletions(-) create mode 100644 eslint-rules/no-hardcoded-hex.mjs diff --git a/eslint-rules/no-hardcoded-hex.mjs b/eslint-rules/no-hardcoded-hex.mjs new file mode 100644 index 000000000..7ccfda7e5 --- /dev/null +++ b/eslint-rules/no-hardcoded-hex.mjs @@ -0,0 +1,31 @@ +export default { + meta: { + type: "problem", + docs: { + description: "Prevent hardcoded hex color utilities in Tailwind classNames", + }, + schema: [], + }, + create(context) { + const hexPattern = /\b(bg|text|border)-\[#[0-9a-fA-F]{3,8}\]/; + + return { + Literal(node) { + if (typeof node.value === "string" && hexPattern.test(node.value)) { + context.report({ + node, + message: "Hardcoded hex color utilities (bg-[#...], text-[#...], border-[#...]) are forbidden. Use semantic CSS variables (e.g. var(--surface)) to support theming.", + }); + } + }, + TemplateElement(node) { + if (node.value && node.value.raw && hexPattern.test(node.value.raw)) { + context.report({ + node, + message: "Hardcoded hex color utilities (bg-[#...], text-[#...], border-[#...]) are forbidden. Use semantic CSS variables (e.g. var(--surface)) to support theming.", + }); + } + }, + }; + }, +}; diff --git a/eslint.config.mjs b/eslint.config.mjs index b85dedc10..beb2d633d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,6 +5,7 @@ import nextTs from "eslint-config-next/typescript"; import requireLucideIconAria from "./eslint-rules/require-lucide-icon-aria.mjs"; import requireButtonWiring from "./eslint-rules/require-button-wiring.mjs"; import restrictSuppressHydrationWarning from "./eslint-rules/restrict-suppress-hydration-warning.mjs"; +import noHardcodedHex from "./eslint-rules/no-hardcoded-hex.mjs"; // Shared `local` plugin object. ESLint flat config requires every config block // that references a plugin namespace to point at the *same* object, so the two @@ -14,6 +15,7 @@ const localRulesPlugin = { "require-lucide-icon-aria": requireLucideIconAria, "require-button-wiring": requireButtonWiring, "restrict-suppress-hydration-warning": restrictSuppressHydrationWarning, + "no-hardcoded-hex": noHardcodedHex, }, }; @@ -66,6 +68,7 @@ const eslintConfig = defineConfig([ plugins: { local: localRulesPlugin }, rules: { "local/require-button-wiring": "error", + "local/no-hardcoded-hex": "error", }, }, { diff --git a/playwright.config.ts b/playwright.config.ts index 897d06694..0228bef59 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,6 +1,9 @@ import { defineConfig, devices } from "playwright/test"; +import { stableProjectPort } from "./src/lib/local-server-utils.mjs"; import { getPlaywrightBaseUrl } from "./scripts/playwright-base-url"; +process.env.PORT = process.env.PORT || String(stableProjectPort(process.cwd())); + const baseURL = getPlaywrightBaseUrl({ allowEnsure: false }); // Sandboxed CI/cloud containers often ship a preinstalled Chromium and block diff --git a/src/app/globals.css b/src/app/globals.css index da1e65847..4ed855414 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2690,3 +2690,33 @@ html[data-motion="reduced"] .source-capsule-hit[aria-expanded="true"]:hover .sou transition: none; } } + +/* Theme switch micro-interaction (Improvement 2.2) */ +html.theme-transitioning, +html.theme-transitioning *, +html.theme-transitioning *:before, +html.theme-transitioning *:after { + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke !important; + transition-duration: 200ms !important; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1) !important; +} + +@media (prefers-reduced-motion: reduce) { + html.theme-transitioning, + html.theme-transitioning *, + html.theme-transitioning *:before, + html.theme-transitioning *:after { + transition: none !important; + } +} + +/* Windows High Contrast / Forced Colors Compatibility (Improvement 2.3) */ +@media (forced-colors: active) { + :root, + .dark { + --border: CanvasText; + --border-lux: CanvasText; + --border-strong: CanvasText; + --focus: Highlight; + } +} diff --git a/src/app/manifest.ts b/src/app/manifest.ts index 2c8b94b61..0c1992a74 100644 --- a/src/app/manifest.ts +++ b/src/app/manifest.ts @@ -22,8 +22,6 @@ export default function manifest(): MetadataRoute.Manifest { // Focus the already-open app window on launch instead of spawning a second // instance; "auto" lets platforms without the capability use their default. launch_handler: { client_mode: ["navigate-existing", "auto"] }, - background_color: APP_THEME_COLORS[DEFAULT_THEME], - theme_color: APP_THEME_COLORS[DEFAULT_THEME], categories: ["medical", "productivity", "utilities"], prefer_related_applications: false, icons: [ diff --git a/src/components/clinical-dashboard/ClinicalSidebar.tsx b/src/components/clinical-dashboard/ClinicalSidebar.tsx index b8b162c0f..b95f8065e 100644 --- a/src/components/clinical-dashboard/ClinicalSidebar.tsx +++ b/src/components/clinical-dashboard/ClinicalSidebar.tsx @@ -311,7 +311,7 @@ export function ClinicalSidebarContent({ className="mt-2 flex w-full items-center gap-3 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface)] px-3 py-2 text-left shadow-[var(--shadow-inset)] transition hover:border-[color:var(--clinical-accent-border)] hover:bg-[color:var(--clinical-accent-soft)]/40 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]" aria-label={accountLabel} > - + {identity.initials} diff --git a/src/components/clinical-dashboard/provider-brand-icons.tsx b/src/components/clinical-dashboard/provider-brand-icons.tsx index aa1c64512..8670a81c3 100644 --- a/src/components/clinical-dashboard/provider-brand-icons.tsx +++ b/src/components/clinical-dashboard/provider-brand-icons.tsx @@ -103,10 +103,10 @@ export function ProviderBrandMark({ provider }: { provider: SsoProvider }) { className="grid h-7 w-7 shrink-0 grid-cols-2 gap-0.5 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface)] p-1 shadow-[var(--shadow-inset)]" aria-hidden="true" > - - - - + + + + ); } @@ -116,8 +116,9 @@ export function ProviderBrandMark({ provider }: { provider: SsoProvider }) { aria-hidden="true" className={cn( "grid h-7 w-7 shrink-0 place-items-center rounded-lg border border-[color:var(--border)] bg-[color:var(--surface)] text-base font-bold leading-none shadow-[var(--shadow-inset)]", - provider === "Apple" ? "text-[color:var(--text-heading)]" : "text-[#4285f4]", + provider === "Apple" ? "text-[color:var(--text-heading)]" : "", )} + style={provider === "Google" ? { color: "#4285f4" } : undefined} > {provider === "Apple" ? "A" : "G"} diff --git a/src/components/clinical-dashboard/settings-dialog.tsx b/src/components/clinical-dashboard/settings-dialog.tsx index 55b006f08..020600b44 100644 --- a/src/components/clinical-dashboard/settings-dialog.tsx +++ b/src/components/clinical-dashboard/settings-dialog.tsx @@ -283,7 +283,7 @@ export function SettingsDialog({ type="button" onClick={onClose} aria-label="Close settings" - className="grid h-9 w-9 shrink-0 place-items-center rounded-full border border-[color:var(--border)] bg-[color:var(--surface)]/70 text-[color:var(--text-muted)] shadow-[var(--shadow-inset)] transition hover:bg-[color:var(--surface)] hover:text-[color:var(--text-heading)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] lg:h-10 lg:w-10 lg:border-transparent lg:bg-transparent lg:shadow-none" + className="grid size-tap shrink-0 place-items-center rounded-full border border-[color:var(--border)] bg-[color:var(--surface)]/70 text-[color:var(--text-muted)] shadow-[var(--shadow-inset)] transition hover:bg-[color:var(--surface)] hover:text-[color:var(--text-heading)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] lg:h-10 lg:w-10 lg:border-transparent lg:bg-transparent lg:shadow-none" >