authorgravatar for me@pixp.ccAlex <me@pixp.cc> 2026-10-11 20:32:23-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-11 20:32:23-07:00
log52f9046260981270292e1fd9c77c2c3fd942be54
tree6bd12d08325491c5dedccadd5ee55d2af3ee38f7
parentc32ade41390be10783246fbbeed45de7e3add79b
signature Signed by PGP key B5690EEEBB952194

fix(mobile): keep PR surfaces and Android tab selection visible across themes (#18479)


9 files changed, 301 insertions(+), 99 deletions(-)

apps/mobile/generated-uniwind-themes.css+58-58
...@@ -337,7 +337,7 @@...@@ -337,7 +337,7 @@
337 --color-composer-surface: rgba(250, 243, 251, 0.94);337 --color-composer-surface: rgba(250, 243, 251, 0.94);
338 --color-composer-border: rgba(238, 225, 237, 0.54);338 --color-composer-border: rgba(238, 225, 237, 0.54);
339 --color-foreground: #501854;339 --color-foreground: #501854;
340 --color-foreground-secondary: #ac1668;340 --color-foreground-secondary: #ab1668;
341 --color-foreground-muted: #8d1255;341 --color-foreground-muted: #8d1255;
342 --color-foreground-tertiary: #ac1668;342 --color-foreground-tertiary: #ac1668;
343 --color-border: #eee1ed;343 --color-border: #eee1ed;
...@@ -351,7 +351,7 @@...@@ -351,7 +351,7 @@
351 --color-inline-skill-foreground: #454554;351 --color-inline-skill-foreground: #454554;
352 --color-primary: #db2777;352 --color-primary: #db2777;
353 --color-primary-foreground: #ffffff;353 --color-primary-foreground: #ffffff;
354 --color-primary-text: #d32572;354 --color-primary-text: #aa1e5d;
355 --color-primary-shadow: #000000;355 --color-primary-shadow: #000000;
356 --color-secondary: #f1c4e6;356 --color-secondary: #f1c4e6;
357 --color-secondary-foreground: #77347c;357 --color-secondary-foreground: #77347c;
...@@ -371,7 +371,7 @@...@@ -371,7 +371,7 @@
371 --color-input: #faf3fb;371 --color-input: #faf3fb;
372 --color-input-border: #e7c1dc;372 --color-input-border: #e7c1dc;
373 --color-sidebar-search: #f8f8f7;373 --color-sidebar-search: #f8f8f7;
374 --color-placeholder: #8b5f90;374 --color-placeholder: #714e76;
375 --color-icon: #501854;375 --color-icon: #501854;
376 --color-icon-muted: #ac1668;376 --color-icon-muted: #ac1668;
377 --color-icon-subtle: #ac1668;377 --color-icon-subtle: #ac1668;
...@@ -510,7 +510,7 @@...@@ -510,7 +510,7 @@
510 --color-inline-skill-foreground: #f8f1f5;510 --color-inline-skill-foreground: #f8f1f5;
511 --color-primary: #a3004c;511 --color-primary: #a3004c;
512 --color-primary-foreground: #fbd0e8;512 --color-primary-foreground: #fbd0e8;
513 --color-primary-text: #cc729c;513 --color-primary-text: #d07ea4;
514 --color-primary-shadow: #000000;514 --color-primary-shadow: #000000;
515 --color-secondary: #362d3d;515 --color-secondary: #362d3d;
516 --color-secondary-foreground: #d4c7e1;516 --color-secondary-foreground: #d4c7e1;
...@@ -530,7 +530,7 @@...@@ -530,7 +530,7 @@
530 --color-input: #29232d;530 --color-input: #29232d;
531 --color-input-border: #302029;531 --color-input-border: #302029;
532 --color-sidebar-search: #261922;532 --color-sidebar-search: #261922;
533 --color-placeholder: #968d9f;533 --color-placeholder: #9d94a5;
534 --color-icon: #f9f8fb;534 --color-icon: #f9f8fb;
535 --color-icon-muted: #d4c7e1;535 --color-icon-muted: #d4c7e1;
536 --color-icon-subtle: #e7d0dd;536 --color-icon-subtle: #e7d0dd;
...@@ -642,7 +642,7 @@...@@ -642,7 +642,7 @@
642 --color-sheet: rgba(243, 247, 244, 0.98);642 --color-sheet: rgba(243, 247, 244, 0.98);
643 --color-sheet-solid: #f3f7f4;643 --color-sheet-solid: #f3f7f4;
644 --color-card: #f3f7f4;644 --color-card: #f3f7f4;
645 --color-grouped-card: #f3f7f4;645 --color-grouped-card: #ecefed;
646 --color-card-alt: #ecefed;646 --color-card-alt: #ecefed;
647 --color-card-translucent: rgba(243, 247, 244, 0.8);647 --color-card-translucent: rgba(243, 247, 244, 0.8);
648 --color-thread-canvas: #f3f7f4;648 --color-thread-canvas: #f3f7f4;
...@@ -652,11 +652,11 @@...@@ -652,11 +652,11 @@
652 --color-thread-hover: #cae0d5;652 --color-thread-hover: #cae0d5;
653 --color-row-hover: #d5e6dd;653 --color-row-hover: #d5e6dd;
654 --color-composer-panel: rgba(243, 247, 244, 0.88);654 --color-composer-panel: rgba(243, 247, 244, 0.88);
655 --color-composer-surface: rgba(243, 247, 244, 0.94);655 --color-composer-surface: rgba(236, 239, 237, 0.94);
656 --color-composer-border: rgba(203, 213, 209, 0.54);656 --color-composer-border: rgba(203, 213, 209, 0.8);
657 --color-foreground: #241523;657 --color-foreground: #241523;
658 --color-foreground-secondary: #726a71;658 --color-foreground-secondary: #6f686f;
659 --color-foreground-muted: #6e696f;659 --color-foreground-muted: #6d686e;
660 --color-foreground-tertiary: #746c73;660 --color-foreground-tertiary: #746c73;
661 --color-border: #cbd5d1;661 --color-border: #cbd5d1;
662 --color-focus: #1b7d50;662 --color-focus: #1b7d50;
...@@ -669,7 +669,7 @@...@@ -669,7 +669,7 @@
669 --color-inline-skill-foreground: #241523;669 --color-inline-skill-foreground: #241523;
670 --color-primary: #8f6410;670 --color-primary: #8f6410;
671 --color-primary-foreground: #fffaff;671 --color-primary-foreground: #fffaff;
672 --color-primary-text: #8f6410;672 --color-primary-text: #8d6210;
673 --color-primary-shadow: #000000;673 --color-primary-shadow: #000000;
674 --color-secondary: #e2ede7;674 --color-secondary: #e2ede7;
675 --color-secondary-foreground: #241523;675 --color-secondary-foreground: #241523;
...@@ -689,7 +689,7 @@...@@ -689,7 +689,7 @@
689 --color-input: #f3f7f4;689 --color-input: #f3f7f4;
690 --color-input-border: #becbc5;690 --color-input-border: #becbc5;
691 --color-sidebar-search: #d3dcd8;691 --color-sidebar-search: #d3dcd8;
692 --color-placeholder: #716971;692 --color-placeholder: #6f686f;
693 --color-icon: #241523;693 --color-icon: #241523;
694 --color-icon-muted: #746c73;694 --color-icon-muted: #746c73;
695 --color-icon-subtle: #746c73;695 --color-icon-subtle: #746c73;
...@@ -697,7 +697,7 @@...@@ -697,7 +697,7 @@
697 --color-header-foreground: #241523;697 --color-header-foreground: #241523;
698 --color-header-border: #d5e6dd;698 --color-header-border: #d5e6dd;
699 --color-glass-surface: rgba(231, 233, 232, 0.74);699 --color-glass-surface: rgba(231, 233, 232, 0.74);
700 --color-glass-fallback: rgba(243, 247, 244, 0.94);700 --color-glass-fallback: rgba(236, 239, 237, 0.94);
701 --color-glass-tint: rgba(231, 233, 232, 0.22);701 --color-glass-tint: rgba(231, 233, 232, 0.22);
702 --color-status-bar: #f3f7f4;702 --color-status-bar: #f3f7f4;
703 --color-md-body: #241523;703 --color-md-body: #241523;
...@@ -801,7 +801,7 @@...@@ -801,7 +801,7 @@
801 --color-sheet: rgba(27, 40, 33, 0.98);801 --color-sheet: rgba(27, 40, 33, 0.98);
802 --color-sheet-solid: #1b2821;802 --color-sheet-solid: #1b2821;
803 --color-card: #1b2821;803 --color-card: #1b2821;
804 --color-grouped-card: #1b2821;804 --color-grouped-card: #36413c;
805 --color-card-alt: #36413c;805 --color-card-alt: #36413c;
806 --color-card-translucent: rgba(27, 40, 33, 0.8);806 --color-card-translucent: rgba(27, 40, 33, 0.8);
807 --color-thread-canvas: #1b2821;807 --color-thread-canvas: #1b2821;
...@@ -811,11 +811,11 @@...@@ -811,11 +811,11 @@
811 --color-thread-hover: #2a4938;811 --color-thread-hover: #2a4938;
812 --color-row-hover: #325c46;812 --color-row-hover: #325c46;
813 --color-composer-panel: rgba(27, 40, 33, 0.92);813 --color-composer-panel: rgba(27, 40, 33, 0.92);
814 --color-composer-surface: rgba(27, 40, 33, 0.9);814 --color-composer-surface: rgba(54, 65, 60, 0.9);
815 --color-composer-border: rgba(65, 95, 79, 0.46);815 --color-composer-border: rgba(65, 95, 79, 0.8);
816 --color-foreground: #fffaff;816 --color-foreground: #fffaff;
817 --color-foreground-secondary: #a6aaaa;817 --color-foreground-secondary: #afb1b1;
818 --color-foreground-muted: #a3aba8;818 --color-foreground-muted: #acb2b0;
819 --color-foreground-tertiary: #919595;819 --color-foreground-tertiary: #919595;
820 --color-border: #415f4f;820 --color-border: #415f4f;
821 --color-focus: #69d69a;821 --color-focus: #69d69a;
...@@ -848,7 +848,7 @@...@@ -848,7 +848,7 @@
848 --color-input: #1b2821;848 --color-input: #1b2821;
849 --color-input-border: #4f725f;849 --color-input-border: #4f725f;
850 --color-sidebar-search: #45554d;850 --color-sidebar-search: #45554d;
851 --color-placeholder: #a9abab;851 --color-placeholder: #afb1b1;
852 --color-icon: #fffaff;852 --color-icon: #fffaff;
853 --color-icon-muted: #919595;853 --color-icon-muted: #919595;
854 --color-icon-subtle: #919595;854 --color-icon-subtle: #919595;
...@@ -856,7 +856,7 @@...@@ -856,7 +856,7 @@
856 --color-header-foreground: #fffaff;856 --color-header-foreground: #fffaff;
857 --color-header-border: #36654c;857 --color-header-border: #36654c;
858 --color-glass-surface: rgba(68, 77, 73, 0.74);858 --color-glass-surface: rgba(68, 77, 73, 0.74);
859 --color-glass-fallback: rgba(27, 40, 33, 0.9);859 --color-glass-fallback: rgba(54, 65, 60, 0.9);
860 --color-glass-tint: rgba(68, 77, 73, 0.22);860 --color-glass-tint: rgba(68, 77, 73, 0.22);
861 --color-status-bar: #1b2821;861 --color-status-bar: #1b2821;
862 --color-md-body: #fffaff;862 --color-md-body: #fffaff;
...@@ -960,7 +960,7 @@...@@ -960,7 +960,7 @@
960 --color-sheet: rgba(245, 247, 248, 0.98);960 --color-sheet: rgba(245, 247, 248, 0.98);
961 --color-sheet-solid: #f5f7f8;961 --color-sheet-solid: #f5f7f8;
962 --color-card: #f5f7f8;962 --color-card: #f5f7f8;
963 --color-grouped-card: #f5f7f8;963 --color-grouped-card: #edeff1;
964 --color-card-alt: #edeff1;964 --color-card-alt: #edeff1;
965 --color-card-translucent: rgba(245, 247, 248, 0.8);965 --color-card-translucent: rgba(245, 247, 248, 0.8);
966 --color-thread-canvas: #f5f7f8;966 --color-thread-canvas: #f5f7f8;
...@@ -970,10 +970,10 @@...@@ -970,10 +970,10 @@
970 --color-thread-hover: #cdddea;970 --color-thread-hover: #cdddea;
971 --color-row-hover: #d8e4ee;971 --color-row-hover: #d8e4ee;
972 --color-composer-panel: rgba(245, 247, 248, 0.88);972 --color-composer-panel: rgba(245, 247, 248, 0.88);
973 --color-composer-surface: rgba(245, 247, 248, 0.94);973 --color-composer-surface: rgba(237, 239, 241, 0.94);
974 --color-composer-border: rgba(205, 212, 220, 0.54);974 --color-composer-border: rgba(205, 212, 220, 0.8);
975 --color-foreground: #241523;975 --color-foreground: #241523;
976 --color-foreground-secondary: #726a73;976 --color-foreground-secondary: #706870;
977 --color-foreground-muted: #6f6873;977 --color-foreground-muted: #6f6873;
978 --color-foreground-tertiary: #746c75;978 --color-foreground-tertiary: #746c75;
979 --color-border: #cdd4dc;979 --color-border: #cdd4dc;
...@@ -1007,7 +1007,7 @@...@@ -1007,7 +1007,7 @@
1007 --color-input: #f5f7f8;1007 --color-input: #f5f7f8;
1008 --color-input-border: #c0c9d4;1008 --color-input-border: #c0c9d4;
1009 --color-sidebar-search: #d5dbe2;1009 --color-sidebar-search: #d5dbe2;
1010 --color-placeholder: #716972;1010 --color-placeholder: #706870;
1011 --color-icon: #241523;1011 --color-icon: #241523;
1012 --color-icon-muted: #746c75;1012 --color-icon-muted: #746c75;
1013 --color-icon-subtle: #746c75;1013 --color-icon-subtle: #746c75;
...@@ -1015,7 +1015,7 @@...@@ -1015,7 +1015,7 @@
1015 --color-header-foreground: #241523;1015 --color-header-foreground: #241523;
1016 --color-header-border: #d8e4ee;1016 --color-header-border: #d8e4ee;
1017 --color-glass-surface: rgba(232, 233, 235, 0.74);1017 --color-glass-surface: rgba(232, 233, 235, 0.74);
1018 --color-glass-fallback: rgba(245, 247, 248, 0.94);1018 --color-glass-fallback: rgba(237, 239, 241, 0.94);
1019 --color-glass-tint: rgba(232, 233, 235, 0.22);1019 --color-glass-tint: rgba(232, 233, 235, 0.22);
1020 --color-status-bar: #f5f7f8;1020 --color-status-bar: #f5f7f8;
1021 --color-md-body: #241523;1021 --color-md-body: #241523;
...@@ -1119,7 +1119,7 @@...@@ -1119,7 +1119,7 @@
1119 --color-sheet: rgba(23, 33, 43, 0.98);1119 --color-sheet: rgba(23, 33, 43, 0.98);
1120 --color-sheet-solid: #17212b;1120 --color-sheet-solid: #17212b;
1121 --color-card: #17212b;1121 --color-card: #17212b;
1122 --color-grouped-card: #17212b;1122 --color-grouped-card: #333b45;
1123 --color-card-alt: #333b45;1123 --color-card-alt: #333b45;
1124 --color-card-translucent: rgba(23, 33, 43, 0.8);1124 --color-card-translucent: rgba(23, 33, 43, 0.8);
1125 --color-thread-canvas: #17212b;1125 --color-thread-canvas: #17212b;
...@@ -1129,11 +1129,11 @@...@@ -1129,11 +1129,11 @@
1129 --color-thread-hover: #283e50;1129 --color-thread-hover: #283e50;
1130 --color-row-hover: #324e66;1130 --color-row-hover: #324e66;
1131 --color-composer-panel: rgba(23, 33, 43, 0.92);1131 --color-composer-panel: rgba(23, 33, 43, 0.92);
1132 --color-composer-surface: rgba(23, 33, 43, 0.9);1132 --color-composer-surface: rgba(51, 59, 69, 0.9);
1133 --color-composer-border: rgba(64, 85, 103, 0.46);1133 --color-composer-border: rgba(64, 85, 103, 0.8);
1134 --color-foreground: #fffaff;1134 --color-foreground: #fffaff;
1135 --color-foreground-secondary: #a2a3aa;1135 --color-foreground-secondary: #a5a7ad;
1136 --color-foreground-muted: #9ea4ad;1136 --color-foreground-muted: #a2a7b0;
1137 --color-foreground-tertiary: #8d8f97;1137 --color-foreground-tertiary: #8d8f97;
1138 --color-border: #405567;1138 --color-border: #405567;
1139 --color-focus: #70b9ee;1139 --color-focus: #70b9ee;
...@@ -1166,7 +1166,7 @@...@@ -1166,7 +1166,7 @@
1166 --color-input: #17212b;1166 --color-input: #17212b;
1167 --color-input-border: #4f677b;1167 --color-input-border: #4f677b;
1168 --color-sidebar-search: #424e5a;1168 --color-sidebar-search: #424e5a;
1169 --color-placeholder: #a4a4ac;1169 --color-placeholder: #a6a6ad;
1170 --color-icon: #fffaff;1170 --color-icon: #fffaff;
1171 --color-icon-muted: #8d8f97;1171 --color-icon-muted: #8d8f97;
1172 --color-icon-subtle: #8d8f97;1172 --color-icon-subtle: #8d8f97;
...@@ -1174,7 +1174,7 @@...@@ -1174,7 +1174,7 @@
1174 --color-header-foreground: #fffaff;1174 --color-header-foreground: #fffaff;
1175 --color-header-border: #36566f;1175 --color-header-border: #36566f;
1176 --color-glass-surface: rgba(65, 72, 81, 0.74);1176 --color-glass-surface: rgba(65, 72, 81, 0.74);
1177 --color-glass-fallback: rgba(23, 33, 43, 0.9);1177 --color-glass-fallback: rgba(51, 59, 69, 0.9);
1178 --color-glass-tint: rgba(65, 72, 81, 0.22);1178 --color-glass-tint: rgba(65, 72, 81, 0.22);
1179 --color-status-bar: #17212b;1179 --color-status-bar: #17212b;
1180 --color-md-body: #fffaff;1180 --color-md-body: #fffaff;
...@@ -1278,7 +1278,7 @@...@@ -1278,7 +1278,7 @@
1278 --color-sheet: rgba(249, 247, 245, 0.98);1278 --color-sheet: rgba(249, 247, 245, 0.98);
1279 --color-sheet-solid: #f9f7f5;1279 --color-sheet-solid: #f9f7f5;
1280 --color-card: #f9f7f5;1280 --color-card: #f9f7f5;
1281 --color-grouped-card: #f9f7f5;1281 --color-grouped-card: #f1efee;
1282 --color-card-alt: #f1efee;1282 --color-card-alt: #f1efee;
1283 --color-card-translucent: rgba(249, 247, 245, 0.8);1283 --color-card-translucent: rgba(249, 247, 245, 0.8);
1284 --color-thread-canvas: #f9f7f5;1284 --color-thread-canvas: #f9f7f5;
...@@ -1288,11 +1288,11 @@...@@ -1288,11 +1288,11 @@
1288 --color-thread-hover: #ead8cf;1288 --color-thread-hover: #ead8cf;
1289 --color-row-hover: #eee0d9;1289 --color-row-hover: #eee0d9;
1290 --color-composer-panel: rgba(249, 247, 245, 0.88);1290 --color-composer-panel: rgba(249, 247, 245, 0.88);
1291 --color-composer-surface: rgba(249, 247, 245, 0.94);1291 --color-composer-surface: rgba(241, 239, 238, 0.94);
1292 --color-composer-border: rgba(221, 210, 206, 0.54);1292 --color-composer-border: rgba(221, 210, 206, 0.8);
1293 --color-foreground: #241523;1293 --color-foreground: #241523;
1294 --color-foreground-secondary: #746a72;1294 --color-foreground-secondary: #726870;
1295 --color-foreground-muted: #74686f;1295 --color-foreground-muted: #73686e;
1296 --color-foreground-tertiary: #766c74;1296 --color-foreground-tertiary: #766c74;
1297 --color-border: #ddd2ce;1297 --color-border: #ddd2ce;
1298 --color-focus: #ae552a;1298 --color-focus: #ae552a;
...@@ -1325,7 +1325,7 @@...@@ -1325,7 +1325,7 @@
1325 --color-input: #f9f7f5;1325 --color-input: #f9f7f5;
1326 --color-input-border: #d4c6c1;1326 --color-input-border: #d4c6c1;
1327 --color-sidebar-search: #e2d9d6;1327 --color-sidebar-search: #e2d9d6;
1328 --color-placeholder: #736971;1328 --color-placeholder: #726870;
1329 --color-icon: #241523;1329 --color-icon: #241523;
1330 --color-icon-muted: #766c74;1330 --color-icon-muted: #766c74;
1331 --color-icon-subtle: #766c74;1331 --color-icon-subtle: #766c74;
...@@ -1333,7 +1333,7 @@...@@ -1333,7 +1333,7 @@
1333 --color-header-foreground: #241523;1333 --color-header-foreground: #241523;
1334 --color-header-border: #eee0d9;1334 --color-header-border: #eee0d9;
1335 --color-glass-surface: rgba(236, 233, 233, 0.74);1335 --color-glass-surface: rgba(236, 233, 233, 0.74);
1336 --color-glass-fallback: rgba(249, 247, 245, 0.94);1336 --color-glass-fallback: rgba(241, 239, 238, 0.94);
1337 --color-glass-tint: rgba(236, 233, 233, 0.22);1337 --color-glass-tint: rgba(236, 233, 233, 0.22);
1338 --color-status-bar: #f9f7f5;1338 --color-status-bar: #f9f7f5;
1339 --color-md-body: #241523;1339 --color-md-body: #241523;
...@@ -1437,7 +1437,7 @@...@@ -1437,7 +1437,7 @@
1437 --color-sheet: rgba(41, 30, 26, 0.98);1437 --color-sheet: rgba(41, 30, 26, 0.98);
1438 --color-sheet-solid: #291e1a;1438 --color-sheet-solid: #291e1a;
1439 --color-card: #291e1a;1439 --color-card: #291e1a;
1440 --color-grouped-card: #291e1a;1440 --color-grouped-card: #433835;
1441 --color-card-alt: #433835;1441 --color-card-alt: #433835;
1442 --color-card-translucent: rgba(41, 30, 26, 0.8);1442 --color-card-translucent: rgba(41, 30, 26, 0.8);
1443 --color-thread-canvas: #291e1a;1443 --color-thread-canvas: #291e1a;
...@@ -1447,11 +1447,11 @@...@@ -1447,11 +1447,11 @@
1447 --color-thread-hover: #4f3528;1447 --color-thread-hover: #4f3528;
1448 --color-row-hover: #644330;1448 --color-row-hover: #644330;
1449 --color-composer-panel: rgba(41, 30, 26, 0.92);1449 --color-composer-panel: rgba(41, 30, 26, 0.92);
1450 --color-composer-surface: rgba(41, 30, 26, 0.9);1450 --color-composer-surface: rgba(67, 56, 53, 0.9);
1451 --color-composer-border: rgba(102, 76, 63, 0.46);1451 --color-composer-border: rgba(102, 76, 63, 0.8);
1452 --color-foreground: #fffaff;1452 --color-foreground: #fffaff;
1453 --color-foreground-secondary: #a8a2a2;1453 --color-foreground-secondary: #aca5a6;
1454 --color-foreground-muted: #aca19f;1454 --color-foreground-muted: #afa5a2;
1455 --color-foreground-tertiary: #968e8f;1455 --color-foreground-tertiary: #968e8f;
1456 --color-border: #664c3f;1456 --color-border: #664c3f;
1457 --color-focus: #f09a64;1457 --color-focus: #f09a64;
...@@ -1484,7 +1484,7 @@...@@ -1484,7 +1484,7 @@
1484 --color-input: #291e1a;1484 --color-input: #291e1a;
1485 --color-input-border: #7a5d4d;1485 --color-input-border: #7a5d4d;
1486 --color-sidebar-search: #584943;1486 --color-sidebar-search: #584943;
1487 --color-placeholder: #aba3a5;1487 --color-placeholder: #aca5a6;
1488 --color-icon: #fffaff;1488 --color-icon: #fffaff;
1489 --color-icon-muted: #968e8f;1489 --color-icon-muted: #968e8f;
1490 --color-icon-subtle: #968e8f;1490 --color-icon-subtle: #968e8f;
...@@ -1492,7 +1492,7 @@...@@ -1492,7 +1492,7 @@
1492 --color-header-foreground: #fffaff;1492 --color-header-foreground: #fffaff;
1493 --color-header-border: #6e4934;1493 --color-header-border: #6e4934;
1494 --color-glass-surface: rgba(79, 69, 67, 0.74);1494 --color-glass-surface: rgba(79, 69, 67, 0.74);
1495 --color-glass-fallback: rgba(41, 30, 26, 0.9);1495 --color-glass-fallback: rgba(67, 56, 53, 0.9);
1496 --color-glass-tint: rgba(79, 69, 67, 0.22);1496 --color-glass-tint: rgba(79, 69, 67, 0.22);
1497 --color-status-bar: #291e1a;1497 --color-status-bar: #291e1a;
1498 --color-md-body: #fffaff;1498 --color-md-body: #fffaff;
...@@ -1596,7 +1596,7 @@...@@ -1596,7 +1596,7 @@
1596 --color-sheet: rgba(248, 247, 249, 0.98);1596 --color-sheet: rgba(248, 247, 249, 0.98);
1597 --color-sheet-solid: #f8f7f9;1597 --color-sheet-solid: #f8f7f9;
1598 --color-card: #f8f7f9;1598 --color-card: #f8f7f9;
1599 --color-grouped-card: #f8f7f9;1599 --color-grouped-card: #f0eff2;
1600 --color-card-alt: #f0eff2;1600 --color-card-alt: #f0eff2;
1601 --color-card-translucent: rgba(248, 247, 249, 0.8);1601 --color-card-translucent: rgba(248, 247, 249, 0.8);
1602 --color-thread-canvas: #f8f7f9;1602 --color-thread-canvas: #f8f7f9;
...@@ -1606,11 +1606,11 @@...@@ -1606,11 +1606,11 @@
1606 --color-thread-hover: #ded8ed;1606 --color-thread-hover: #ded8ed;
1607 --color-row-hover: #e5e0f0;1607 --color-row-hover: #e5e0f0;
1608 --color-composer-panel: rgba(248, 247, 249, 0.88);1608 --color-composer-panel: rgba(248, 247, 249, 0.88);
1609 --color-composer-surface: rgba(248, 247, 249, 0.94);1609 --color-composer-surface: rgba(240, 239, 242, 0.94);
1610 --color-composer-border: rgba(214, 209, 222, 0.54);1610 --color-composer-border: rgba(214, 209, 222, 0.8);
1611 --color-foreground: #241523;1611 --color-foreground: #241523;
1612 --color-foreground-secondary: #746a74;1612 --color-foreground-secondary: #716871;
1613 --color-foreground-muted: #726874;1613 --color-foreground-muted: #716773;
1614 --color-foreground-tertiary: #766c76;1614 --color-foreground-tertiary: #766c76;
1615 --color-border: #d6d1de;1615 --color-border: #d6d1de;
1616 --color-focus: #7253b9;1616 --color-focus: #7253b9;
...@@ -1643,7 +1643,7 @@...@@ -1643,7 +1643,7 @@
1643 --color-input: #f8f7f9;1643 --color-input: #f8f7f9;
1644 --color-input-border: #ccc5d6;1644 --color-input-border: #ccc5d6;
1645 --color-sidebar-search: #ddd9e3;1645 --color-sidebar-search: #ddd9e3;
1646 --color-placeholder: #736973;1646 --color-placeholder: #716871;
1647 --color-icon: #241523;1647 --color-icon: #241523;
1648 --color-icon-muted: #766c76;1648 --color-icon-muted: #766c76;
1649 --color-icon-subtle: #766c76;1649 --color-icon-subtle: #766c76;
...@@ -1651,7 +1651,7 @@...@@ -1651,7 +1651,7 @@
1651 --color-header-foreground: #241523;1651 --color-header-foreground: #241523;
1652 --color-header-border: #e5e0f0;1652 --color-header-border: #e5e0f0;
1653 --color-glass-surface: rgba(235, 233, 237, 0.74);1653 --color-glass-surface: rgba(235, 233, 237, 0.74);
1654 --color-glass-fallback: rgba(248, 247, 249, 0.94);1654 --color-glass-fallback: rgba(240, 239, 242, 0.94);
1655 --color-glass-tint: rgba(235, 233, 237, 0.22);1655 --color-glass-tint: rgba(235, 233, 237, 0.22);
1656 --color-status-bar: #f8f7f9;1656 --color-status-bar: #f8f7f9;
1657 --color-md-body: #241523;1657 --color-md-body: #241523;
...@@ -1755,7 +1755,7 @@...@@ -1755,7 +1755,7 @@
1755 --color-sheet: rgba(29, 25, 41, 0.98);1755 --color-sheet: rgba(29, 25, 41, 0.98);
1756 --color-sheet-solid: #1d1929;1756 --color-sheet-solid: #1d1929;
1757 --color-card: #1d1929;1757 --color-card: #1d1929;
1758 --color-grouped-card: #1d1929;1758 --color-grouped-card: #383443;
1759 --color-card-alt: #383443;1759 --color-card-alt: #383443;
1760 --color-card-translucent: rgba(29, 25, 41, 0.8);1760 --color-card-translucent: rgba(29, 25, 41, 0.8);
1761 --color-thread-canvas: #1d1929;1761 --color-thread-canvas: #1d1929;
...@@ -1765,8 +1765,8 @@...@@ -1765,8 +1765,8 @@
1765 --color-thread-hover: #352c4f;1765 --color-thread-hover: #352c4f;
1766 --color-row-hover: #433765;1766 --color-row-hover: #433765;
1767 --color-composer-panel: rgba(29, 25, 41, 0.92);1767 --color-composer-panel: rgba(29, 25, 41, 0.92);
1768 --color-composer-surface: rgba(29, 25, 41, 0.9);1768 --color-composer-surface: rgba(56, 52, 67, 0.9);
1769 --color-composer-border: rgba(77, 67, 102, 0.46);1769 --color-composer-border: rgba(77, 67, 102, 0.8);
1770 --color-foreground: #fffaff;1770 --color-foreground: #fffaff;
1771 --color-foreground-secondary: #a09da6;1771 --color-foreground-secondary: #a09da6;
1772 --color-foreground-muted: #a19cab;1772 --color-foreground-muted: #a19cab;
...@@ -1810,7 +1810,7 @@...@@ -1810,7 +1810,7 @@
1810 --color-header-foreground: #fffaff;1810 --color-header-foreground: #fffaff;
1811 --color-header-border: #4a3c70;1811 --color-header-border: #4a3c70;
1812 --color-glass-surface: rgba(69, 66, 80, 0.74);1812 --color-glass-surface: rgba(69, 66, 80, 0.74);
1813 --color-glass-fallback: rgba(29, 25, 41, 0.9);1813 --color-glass-fallback: rgba(56, 52, 67, 0.9);
1814 --color-glass-tint: rgba(69, 66, 80, 0.22);1814 --color-glass-tint: rgba(69, 66, 80, 0.22);
1815 --color-status-bar: #1d1929;1815 --color-status-bar: #1d1929;
1816 --color-md-body: #fffaff;1816 --color-md-body: #fffaff;
apps/mobile/src/features/pull-requests/MaterialFloatingToolbar.android.tsx+1-1
...@@ -15,7 +15,7 @@ export function MaterialFloatingToolbar(props: { readonly children: ReactElement...@@ -15,7 +15,7 @@ export function MaterialFloatingToolbar(props: { readonly children: ReactElement
15 >15 >
16 <HorizontalFloatingToolbar16 <HorizontalFloatingToolbar
17 colors={{17 colors={{
18 toolbarContainerColor: colors["--color-card-alt"],18 toolbarContainerColor: colors["--color-grouped-card"],
19 toolbarContentColor: colors["--color-foreground"],19 toolbarContentColor: colors["--color-foreground"],
20 }}20 }}
21 >21 >
apps/mobile/src/features/pull-requests/PullRequestDescription.tsx+7-6
...@@ -6,7 +6,7 @@ import {...@@ -6,7 +6,7 @@ import {
6} from "./PullRequestMedia";6} from "./PullRequestMedia";
7import { PullRequestSectionHeading } from "./PullRequestSectionHeading";7import { PullRequestSectionHeading } from "./PullRequestSectionHeading";
8import { useContext, useMemo, useState } from "react";8import { useContext, useMemo, useState } from "react";
9import { Pressable, ScrollView, StyleSheet, View } from "react-native";9import { Platform, Pressable, ScrollView, StyleSheet, View } from "react-native";
10import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg";10import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg";
11import { pullRequestImageSource } from "./pullRequestMediaSource";11import { pullRequestImageSource } from "./pullRequestMediaSource";
12import { parseMarkdownDocument } from "../../lib/markdownDocument";12import { parseMarkdownDocument } from "../../lib/markdownDocument";
...@@ -30,6 +30,7 @@ export function PullRequestDescription(props: {...@@ -30,6 +30,7 @@ export function PullRequestDescription(props: {
30 const [bodyHeight, setBodyHeight] = useState(0);30 const [bodyHeight, setBodyHeight] = useState(0);
31 const mediaContext = useContext(PullRequestMediaContext);31 const mediaContext = useContext(PullRequestMediaContext);
32 const { themeVariables: colors } = useAppearancePreferences();32 const { themeVariables: colors } = useAppearancePreferences();
33 const fadeColor = colors[Platform.OS === "android" ? "--color-sheet-solid" : "--color-screen"];
33 const content = useMemo(() => {34 const content = useMemo(() => {
34 const document = parseMarkdownDocument(props.markdown);35 const document = parseMarkdownDocument(props.markdown);
35 const content = pullRequestDescriptionContent(document, splitPullRequestBody(props.markdown));36 const content = pullRequestDescriptionContent(document, splitPullRequestBody(props.markdown));
...@@ -87,10 +88,10 @@ export function PullRequestDescription(props: {...@@ -87,10 +88,10 @@ export function PullRequestDescription(props: {
87 y2="80"88 y2="80"
88 gradientUnits="userSpaceOnUse"89 gradientUnits="userSpaceOnUse"
89 >90 >
90 <Stop offset="0" stopColor={colors["--color-screen"]} stopOpacity={0} />91 <Stop offset="0" stopColor={fadeColor} stopOpacity={0} />
91 <Stop offset="0.5" stopColor={colors["--color-screen"]} stopOpacity={0.94} />92 <Stop offset="0.5" stopColor={fadeColor} stopOpacity={0.94} />
92 <Stop offset="0.72" stopColor={colors["--color-screen"]} stopOpacity={1} />93 <Stop offset="0.72" stopColor={fadeColor} stopOpacity={1} />
93 <Stop offset="1" stopColor={colors["--color-screen"]} stopOpacity={1} />94 <Stop offset="1" stopColor={fadeColor} stopOpacity={1} />
94 </LinearGradient>95 </LinearGradient>
95 </Defs>96 </Defs>
96 <Rect width="100%" height="100%" fill="url(#description-fade)" />97 <Rect width="100%" height="100%" fill="url(#description-fade)" />
...@@ -102,7 +103,7 @@ export function PullRequestDescription(props: {...@@ -102,7 +103,7 @@ export function PullRequestDescription(props: {
102 left: 0,103 left: 0,
103 right: 0,104 right: 0,
104 height: 44,105 height: 44,
105 backgroundColor: colors["--color-screen"],106 backgroundColor: fadeColor,
106 }}107 }}
107 />108 />
108 </View>109 </View>
apps/mobile/src/features/workspace/AndroidWorkspaceNavigation.android.tsx+6-10
...@@ -12,7 +12,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";...@@ -12,7 +12,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
12import { useKeyboardState } from "react-native-keyboard-controller";12import { useKeyboardState } from "react-native-keyboard-controller";
1313
14import { useAndroidControlSizing } from "../../components/useAndroidControlSizing";14import { useAndroidControlSizing } from "../../components/useAndroidControlSizing";
15import { flattenThemeColor, themeColorWithAlpha } from "../../lib/mobileTheme";15import { androidWorkspaceNavigationColors } from "./android-workspace-navigation-colors";
16import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";16import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
17import { useWorkspaceTabs } from "./WorkspaceTabsContext";17import { useWorkspaceTabs } from "./WorkspaceTabsContext";
18import type { WorkspaceTab } from "./workspace-tabs-state";18import type { WorkspaceTab } from "./workspace-tabs-state";
...@@ -49,12 +49,8 @@ export function AndroidWorkspaceNavigation(props: {...@@ -49,12 +49,8 @@ export function AndroidWorkspaceNavigation(props: {
49 const { themeAppearance, themeVariables: colors } = useAppearancePreferences();49 const { themeAppearance, themeVariables: colors } = useAppearancePreferences();
50 const { scale } = useAndroidControlSizing();50 const { scale } = useAndroidControlSizing();
51 const rail = Boolean(props.rail);51 const rail = Boolean(props.rail);
52 const containerColor = rail52 const { containerColor, indicatorColor, selectedColor, unselectedColor } =
53 ? colors["--color-header"]53 androidWorkspaceNavigationColors(colors, rail);
54 : flattenThemeColor(
55 themeColorWithAlpha(colors["--color-header-foreground"], 0.05),
56 flattenThemeColor(colors["--color-header"], colors["--color-screen"]),
57 );
58 if (!rail && keyboardVisible) return null;54 if (!rail && keyboardVisible) return null;
5955
60 return (56 return (
...@@ -77,9 +73,9 @@ export function AndroidWorkspaceNavigation(props: {...@@ -77,9 +73,9 @@ export function AndroidWorkspaceNavigation(props: {
77 dark={themeAppearance === "dark"}73 dark={themeAppearance === "dark"}
78 textScale={scale}74 textScale={scale}
79 containerColor={nativeColor(containerColor)}75 containerColor={nativeColor(containerColor)}
80 selectedColor={nativeColor(colors["--color-secondary-foreground"])}76 selectedColor={nativeColor(selectedColor)}
81 indicatorColor={nativeColor(colors["--color-secondary"])}77 indicatorColor={nativeColor(indicatorColor)}
82 unselectedColor={nativeColor(colors["--color-header-foreground"])}78 unselectedColor={nativeColor(unselectedColor)}
83 onTabSelected={(event) => {79 onTabSelected={(event) => {
84 Keyboard.dismiss();80 Keyboard.dismiss();
85 const tab = event.nativeEvent.tab;81 const tab = event.nativeEvent.tab;
apps/mobile/src/features/workspace/android-workspace-navigation-colors.test.ts created+110
...@@ -0,0 +1,110 @@
1import { describe, expect, it } from "vite-plus/test";
2import { Hct, SchemeTonalSpot, argbFromHex, hexFromArgb } from "@material/material-color-utilities";
3
4import { MOBILE_THEME_IDS } from "../../lib/mobileTheme";
5import { getMobileThemeRuntimeVariables } from "../../lib/mobileThemeVariables";
6import { materialYouPaletteToMobileThemeVariables } from "../../lib/materialYouTheme.android";
7import type { MaterialYouPalette } from "../../lib/materialYouPalette";
8import { androidWorkspaceNavigationColors } from "./android-workspace-navigation-colors";
9
10function luminance(hex: string) {
11 const channels = hex
12 .slice(1, 7)
13 .match(/.{2}/g)!
14 .map((channel) => Number.parseInt(channel, 16) / 255)
15 .map((channel) => (channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4));
16 return 0.2126 * channels[0]! + 0.7152 * channels[1]! + 0.0722 * channels[2]!;
17}
18
19function contrast(first: string, second: string) {
20 const firstLuminance = luminance(first);
21 const secondLuminance = luminance(second);
22 return (
23 (Math.max(firstLuminance, secondLuminance) + 0.05) /
24 (Math.min(firstLuminance, secondLuminance) + 0.05)
25 );
26}
27
28function expectVisibleSelection(colors: ReturnType<typeof androidWorkspaceNavigationColors>) {
29 expect(contrast(colors.indicatorColor, colors.containerColor)).toBeGreaterThanOrEqual(1.5);
30 // Native labels sit below the indicator, while their icons sit within it.
31 for (const surface of [colors.containerColor, colors.indicatorColor]) {
32 expect(contrast(colors.selectedColor, surface)).toBeGreaterThanOrEqual(4.5);
33 }
34 expect(contrast(colors.unselectedColor, colors.containerColor)).toBeGreaterThanOrEqual(4.5);
35}
36
37describe("Android workspace navigation colors", () => {
38 it.each(MOBILE_THEME_IDS)(
39 "keeps %s selection visible in both navigation positions",
40 (themeId) => {
41 for (const appearance of ["light", "dark"] as const) {
42 const variables = getMobileThemeRuntimeVariables(themeId, appearance, "android");
43 const snapshot = { ...variables };
44 for (const rail of [false, true]) {
45 expectVisibleSelection(androidWorkspaceNavigationColors(variables, rail));
46 }
47 expect(variables).toEqual(snapshot);
48 }
49 },
50 );
51
52 it.each(["#6750a4", "#006a6a", "#9c4146"])(
53 "keeps Material You selection visible for system seed %s",
54 (seed) => {
55 for (const appearance of ["light", "dark"] as const) {
56 const scheme = new SchemeTonalSpot(
57 Hct.fromInt(argbFromHex(seed)),
58 appearance === "dark",
59 0,
60 );
61 const roles = [
62 "primary",
63 "onPrimary",
64 "primaryContainer",
65 "onPrimaryContainer",
66 "inversePrimary",
67 "secondaryContainer",
68 "onSecondaryContainer",
69 "tertiary",
70 "tertiaryContainer",
71 "onTertiaryContainer",
72 "surface",
73 "onSurface",
74 "onSurfaceVariant",
75 "surfaceContainer",
76 "surfaceContainerHigh",
77 "surfaceContainerHighest",
78 "surfaceContainerLow",
79 "surfaceContainerLowest",
80 "errorContainer",
81 "onErrorContainer",
82 "outline",
83 "outlineVariant",
84 "scrim",
85 ] as const satisfies ReadonlyArray<keyof MaterialYouPalette>;
86 const palette = Object.fromEntries(
87 roles.map((role) => [role, `${hexFromArgb(scheme[role])}ff`]),
88 ) as Record<(typeof roles)[number], string>;
89 const variables = materialYouPaletteToMobileThemeVariables(
90 palette,
91 appearance,
92 getMobileThemeRuntimeVariables("material-you", appearance, "android"),
93 );
94 for (const rail of [false, true]) {
95 expectVisibleSelection(androidWorkspaceNavigationColors(variables, rail));
96 }
97 }
98 },
99 );
100
101 it("preserves an already visible tonal fill", () => {
102 const variables = getMobileThemeRuntimeVariables("t3-chat", "light", "android");
103 const colors = androidWorkspaceNavigationColors(
104 { ...variables, "--color-secondary": "#e091cf" },
105 true,
106 );
107 expect(colors.indicatorColor).toBe("#e091cf");
108 expectVisibleSelection(colors);
109 });
110});
apps/mobile/src/features/workspace/android-workspace-navigation-colors.ts created+28
...@@ -0,0 +1,28 @@
1import {
2 flattenThemeColor,
3 themeColorWithAlpha,
4 themeColorWithContrast,
5 type MobileThemeVariables,
6} from "../../lib/mobileTheme";
7
8/** Both navigation positions keep a visible tonal selection against their actual surface. */
9export function androidWorkspaceNavigationColors(colors: MobileThemeVariables, rail: boolean) {
10 const header = flattenThemeColor(colors["--color-header"], colors["--color-screen"]);
11 const containerColor = rail
12 ? header
13 : flattenThemeColor(themeColorWithAlpha(colors["--color-header-foreground"], 0.05), header);
14 const indicatorColor = themeColorWithContrast(
15 flattenThemeColor(colors["--color-secondary"], containerColor),
16 containerColor,
17 1.5,
18 );
19 return {
20 containerColor,
21 indicatorColor,
22 selectedColor: themeColorWithContrast(
23 flattenThemeColor(colors["--color-secondary-foreground"], indicatorColor),
24 [indicatorColor, containerColor],
25 ),
26 unselectedColor: flattenThemeColor(colors["--color-header-foreground"], containerColor),
27 };
28}
apps/mobile/src/lib/mobileTheme.test-support.ts+6-2
...@@ -9,14 +9,18 @@ import {...@@ -9,14 +9,18 @@ import {
9export function readDefaultMobileThemeVariables(9export function readDefaultMobileThemeVariables(
10 appearance: MobileThemeAppearance,10 appearance: MobileThemeAppearance,
11): MobileThemeVariables {11): MobileThemeVariables {
12 return readGeneratedMobileThemeVariables(appearance);
13}
14
15export function readGeneratedMobileThemeVariables(themeName: string): MobileThemeVariables {
12 const stylesheet = NodeFS.readFileSync(16 const stylesheet = NodeFS.readFileSync(
13 new URL("../../generated-uniwind-themes.css", import.meta.url),17 new URL("../../generated-uniwind-themes.css", import.meta.url),
14 "utf8",18 "utf8",
15 );19 );
16 const variant = new RegExp(`@variant ${appearance} \\{([\\s\\S]*?)\\n \\}`, "u").exec(20 const variant = new RegExp(`@variant ${themeName} \\{([\\s\\S]*?)\\n \\}`, "u").exec(
17 stylesheet,21 stylesheet,
18 )?.[1];22 )?.[1];
19 if (variant === undefined) throw new Error(`Missing generated default ${appearance} theme.`);23 if (variant === undefined) throw new Error(`Missing generated ${themeName} theme.`);
2024
21 return Object.fromEntries(25 return Object.fromEntries(
22 Array.from(variant.matchAll(/(--color-[a-z0-9-]+):\s*([^;]+);/gu), ([, name, value]) => [26 Array.from(variant.matchAll(/(--color-[a-z0-9-]+):\s*([^;]+);/gu), ([, name, value]) => [
apps/mobile/src/lib/mobileTheme.test.ts+48-7
...@@ -8,7 +8,10 @@ import {...@@ -8,7 +8,10 @@ import {
8 MOBILE_THEME_IDS,8 MOBILE_THEME_IDS,
9 getThemeColorsForAppearance,9 getThemeColorsForAppearance,
10} from "@t3tools/shared/themePalettes";10} from "@t3tools/shared/themePalettes";
11import { readDefaultMobileThemeVariables } from "./mobileTheme.test-support";11import {
12 readDefaultMobileThemeVariables,
13 readGeneratedMobileThemeVariables,
14} from "./mobileTheme.test-support";
12import { getMobileThemeRuntimeVariables } from "./mobileThemeVariables";15import { getMobileThemeRuntimeVariables } from "./mobileThemeVariables";
1316
14import {17import {
...@@ -96,12 +99,7 @@ describe("mobile themes", () => {...@@ -96,12 +99,7 @@ describe("mobile themes", () => {
96 );99 );
97 expect(variables["--color-card"]).toBe(themeColorToNativeColor(colors.surface));100 expect(variables["--color-card"]).toBe(themeColorToNativeColor(colors.surface));
98 expect(variables["--color-composer-surface"]).toBe(101 expect(variables["--color-composer-surface"]).toBe(
99 themeColorWithAlpha(102 themeColorWithAlpha(variables["--color-grouped-card"], appearance === "dark" ? 0.9 : 0.94),
100 themeId === DEFAULT_MOBILE_THEME_ID
101 ? variables["--color-grouped-card"]
102 : themeColorToNativeColor(colors.surface),
103 appearance === "dark" ? 0.9 : 0.94,
104 ),
105 );103 );
106 expect(variables["--color-thread-selected"]).toBe(104 expect(variables["--color-thread-selected"]).toBe(
107 themeColorToNativeColor(colors.sidebarRowActive),105 themeColorToNativeColor(colors.sidebarRowActive),
...@@ -182,6 +180,49 @@ describe("mobile themes", () => {...@@ -182,6 +180,49 @@ describe("mobile themes", () => {
182 expect(variables["--color-screen"]).toMatch(/^#/);180 expect(variables["--color-screen"]).toMatch(/^#/);
183 });181 });
184182
183 it.each(MOBILE_THEME_IDS)("separates %s filled groups from their sheets", (themeId) => {
184 for (const appearance of ["light", "dark"] as const) {
185 const generated = readGeneratedMobileThemeVariables(
186 themeId === DEFAULT_MOBILE_THEME_ID ? appearance : `${themeId}-${appearance}`,
187 );
188 expect(generated).toEqual(getMobileThemeVariables(themeId, appearance));
189 for (const platform of ["ios", "android"]) {
190 const variables = getMobileThemeRuntimeVariables(themeId, appearance, platform);
191 const sheet = variables["--color-sheet-solid"];
192 const group = variables["--color-grouped-card"];
193 expect(
194 contrastRatio(group, sheet),
195 `${platform} ${appearance}: grouped surface on sheet`,
196 ).toBeGreaterThanOrEqual(1.025);
197 for (const role of [
198 "--color-foreground",
199 "--color-foreground-secondary",
200 "--color-foreground-muted",
201 "--color-placeholder",
202 "--color-primary-text",
203 ] as const) {
204 expect(
205 contrastRatio(variables[role], group),
206 `${platform} ${appearance}: ${role} on grouped surface`,
207 ).toBeGreaterThanOrEqual(4.5);
208 }
209 if (platform === "android") {
210 const header = flattenThemeColor(variables["--color-header"], sheet);
211 for (const role of ["--color-header-foreground", "--color-foreground-muted"] as const) {
212 expect(
213 contrastRatio(variables[role], header),
214 `${appearance}: ${role} on Android header`,
215 ).toBeGreaterThanOrEqual(4.5);
216 }
217 }
218 expect(
219 contrastRatio(variables["--color-foreground-muted"], variables["--color-secondary"]),
220 `${platform} ${appearance}: muted label on secondary surface`,
221 ).toBeGreaterThanOrEqual(4.5);
222 }
223 }
224 });
225
185 it.each(["light", "dark"] as const)(226 it.each(["light", "dark"] as const)(
186 "separates default settings groups from their %s background",227 "separates default settings groups from their %s background",
187 (appearance) => {228 (appearance) => {
apps/mobile/src/lib/mobileTheme.ts+37-15
...@@ -191,15 +191,23 @@ function contrastRatio(...@@ -191,15 +191,23 @@ function contrastRatio(
191 );191 );
192}192}
193193
194/** Preserve the color's hue while giving text 4.5:1 contrast on every supplied surface. */194/** Keep the theme tint while adjusting lightness to contrast with every supplied surface. */
195function readableTextColor(accent: string, surface: string | ReadonlyArray<string>): string {195export function themeColorWithContrast(
196 accent: string,
197 surface: string | ReadonlyArray<string>,
198 minimum = 4.5,
199): string {
196 const accentChannels = rgbChannels(accent);200 const accentChannels = rgbChannels(accent);
197 const surfaceChannels = (typeof surface === "string" ? [surface] : surface)201 const surfaceChannels = (typeof surface === "string" ? [surface] : surface)
198 .map(rgbChannels)202 .map(rgbChannels)
199 .filter((channels) => channels !== null);203 .filter((channels) => channels !== null);
200 const minimumContrast = (channels: readonly [number, number, number]) =>204 const minimumContrast = (channels: readonly [number, number, number]) =>
201 Math.min(...surfaceChannels.map((surface) => contrastRatio(channels, surface)));205 Math.min(...surfaceChannels.map((surface) => contrastRatio(channels, surface)));
202 if (!accentChannels || surfaceChannels.length === 0 || minimumContrast(accentChannels) >= 4.5) {206 if (
207 !accentChannels ||
208 surfaceChannels.length === 0 ||
209 minimumContrast(accentChannels) >= minimum
210 ) {
203 return accent;211 return accent;
204 }212 }
205213
...@@ -216,7 +224,7 @@ function readableTextColor(accent: string, surface: string | ReadonlyArray<strin...@@ -216,7 +224,7 @@ function readableTextColor(accent: string, surface: string | ReadonlyArray<strin
216 Math.round(accentChannels[1] + (target[1] - accentChannels[1]) * amount),224 Math.round(accentChannels[1] + (target[1] - accentChannels[1]) * amount),
217 Math.round(accentChannels[2] + (target[2] - accentChannels[2]) * amount),225 Math.round(accentChannels[2] + (target[2] - accentChannels[2]) * amount),
218 ];226 ];
219 if (minimumContrast(candidate) >= 4.5) {227 if (minimumContrast(candidate) >= minimum) {
220 readable = candidate;228 readable = candidate;
221 upperAmount = amount;229 upperAmount = amount;
222 } else {230 } else {
...@@ -242,7 +250,15 @@ export function createMobileThemeVariables(...@@ -242,7 +250,15 @@ export function createMobileThemeVariables(
242) {250) {
243 const c = nativeColors(colors);251 const c = nativeColors(colors);
244 const groupedCard = themeColorToNativeColor(groupedCardColor);252 const groupedCard = themeColorToNativeColor(groupedCardColor);
245 const textSurfaces = [c.canvas, c.surface, c.surfaceRaised, c.chrome, groupedCard];253 const textSurfaces = [
254 c.canvas,
255 c.surface,
256 c.surfaceRaised,
257 c.chrome,
258 c.sidebar,
259 c.secondary,
260 groupedCard,
261 ];
246 return {262 return {
247 "--color-screen": c.canvas,263 "--color-screen": c.canvas,
248 "--color-sheet": withAlpha(c.chrome, 0.98),264 "--color-sheet": withAlpha(c.chrome, 0.98),
...@@ -254,7 +270,7 @@ export function createMobileThemeVariables(...@@ -254,7 +270,7 @@ export function createMobileThemeVariables(
254 "--color-thread-canvas": c.canvas,270 "--color-thread-canvas": c.canvas,
255 "--color-thread-selected": c.sidebarRowActive,271 "--color-thread-selected": c.sidebarRowActive,
256 "--color-thread-selected-foreground": c.sidebarForeground,272 "--color-thread-selected-foreground": c.sidebarForeground,
257 "--color-thread-selected-foreground-muted": readableTextColor(273 "--color-thread-selected-foreground-muted": themeColorWithContrast(
258 c.sidebarMutedForeground,274 c.sidebarMutedForeground,
259 c.sidebarRowActive,275 c.sidebarRowActive,
260 ),276 ),
...@@ -270,8 +286,8 @@ export function createMobileThemeVariables(...@@ -270,8 +286,8 @@ export function createMobileThemeVariables(
270 groupedCard === c.surface ? (appearance === "dark" ? 0.46 : 0.54) : 0.8,286 groupedCard === c.surface ? (appearance === "dark" ? 0.46 : 0.54) : 0.8,
271 ),287 ),
272 "--color-foreground": c.text,288 "--color-foreground": c.text,
273 "--color-foreground-secondary": readableTextColor(c.textMuted, textSurfaces),289 "--color-foreground-secondary": themeColorWithContrast(c.textMuted, textSurfaces),
274 "--color-foreground-muted": readableTextColor(c.mutedForeground, textSurfaces),290 "--color-foreground-muted": themeColorWithContrast(c.mutedForeground, textSurfaces),
275 "--color-foreground-tertiary": c.secondaryLabel,291 "--color-foreground-tertiary": c.secondaryLabel,
276 "--color-border": c.border,292 "--color-border": c.border,
277 "--color-focus": c.focus,293 "--color-focus": c.focus,
...@@ -284,7 +300,7 @@ export function createMobileThemeVariables(...@@ -284,7 +300,7 @@ export function createMobileThemeVariables(
284 "--color-inline-skill-foreground": c.accentSurfaceForeground,300 "--color-inline-skill-foreground": c.accentSurfaceForeground,
285 "--color-primary": c.messageAction,301 "--color-primary": c.messageAction,
286 "--color-primary-foreground": c.messageActionForeground,302 "--color-primary-foreground": c.messageActionForeground,
287 "--color-primary-text": readableTextColor(c.messageAction, textSurfaces),303 "--color-primary-text": themeColorWithContrast(c.messageAction, textSurfaces),
288 "--color-primary-shadow": "#000000",304 "--color-primary-shadow": "#000000",
289 "--color-secondary": c.secondary,305 "--color-secondary": c.secondary,
290 "--color-secondary-foreground": c.secondaryForeground,306 "--color-secondary-foreground": c.secondaryForeground,
...@@ -304,7 +320,7 @@ export function createMobileThemeVariables(...@@ -304,7 +320,7 @@ export function createMobileThemeVariables(
304 "--color-input": c.surface,320 "--color-input": c.surface,
305 "--color-input-border": c.input,321 "--color-input-border": c.input,
306 "--color-sidebar-search": c.sidebarControlSurface,322 "--color-sidebar-search": c.sidebarControlSurface,
307 "--color-placeholder": readableTextColor(c.placeholder, textSurfaces),323 "--color-placeholder": themeColorWithContrast(c.placeholder, textSurfaces),
308 "--color-icon": c.text,324 "--color-icon": c.text,
309 "--color-icon-muted": c.iconMuted,325 "--color-icon-muted": c.iconMuted,
310 "--color-icon-subtle": c.secondaryLabel,326 "--color-icon-subtle": c.secondaryLabel,
...@@ -317,7 +333,7 @@ export function createMobileThemeVariables(...@@ -317,7 +333,7 @@ export function createMobileThemeVariables(
317 "--color-status-bar": c.canvas,333 "--color-status-bar": c.canvas,
318 "--color-md-body": c.text,334 "--color-md-body": c.text,
319 "--color-md-strong": c.text,335 "--color-md-strong": c.text,
320 "--color-md-link": readableTextColor(c.messageAction, c.canvas),336 "--color-md-link": themeColorWithContrast(c.messageAction, c.canvas),
321 "--color-md-blockquote-border": c.border,337 "--color-md-blockquote-border": c.border,
322 "--color-md-blockquote-bg": c.muted,338 "--color-md-blockquote-bg": c.muted,
323 "--color-md-code-bg": c.codeBackground,339 "--color-md-code-bg": c.codeBackground,
...@@ -330,11 +346,14 @@ export function createMobileThemeVariables(...@@ -330,11 +346,14 @@ export function createMobileThemeVariables(
330 "--color-user-bubble": c.messageSurface,346 "--color-user-bubble": c.messageSurface,
331 "--color-user-bubble-foreground": c.messageForeground,347 "--color-user-bubble-foreground": c.messageForeground,
332 "--color-user-bubble-foreground-muted": withAlpha(c.messageForeground, 0.78),348 "--color-user-bubble-foreground-muted": withAlpha(c.messageForeground, 0.78),
333 "--color-user-bubble-skill-foreground": readableTextColor(c.messageAction, c.messageSurface),349 "--color-user-bubble-skill-foreground": themeColorWithContrast(
350 c.messageAction,
351 c.messageSurface,
352 ),
334 "--color-backdrop": withAlpha("#000000", appearance === "dark" ? 0.48 : 0.22),353 "--color-backdrop": withAlpha("#000000", appearance === "dark" ? 0.48 : 0.22),
335 "--color-drawer": c.sidebar,354 "--color-drawer": c.sidebar,
336 "--color-drawer-foreground": c.sidebarForeground,355 "--color-drawer-foreground": c.sidebarForeground,
337 "--color-drawer-foreground-muted": readableTextColor(c.sidebarMutedForeground, [356 "--color-drawer-foreground-muted": themeColorWithContrast(c.sidebarMutedForeground, [
338 c.sidebar,357 c.sidebar,
339 c.sidebarRowHover,358 c.sidebarRowHover,
340 ]),359 ]),
...@@ -368,13 +387,16 @@ export function getMobileThemeVariables(...@@ -368,13 +387,16 @@ export function getMobileThemeVariables(
368): MobileThemeVariables {387): MobileThemeVariables {
369 const colors = getMobileThemeColors(themeId, appearance);388 const colors = getMobileThemeColors(themeId, appearance);
370 // Mobile settings groups and fallback materials use tonal fills where desktop389 // Mobile settings groups and fallback materials use tonal fills where desktop
371 // uses outlined cards. Regular cards retain their shared desktop surface.390 // uses outlined cards. Raise flat desktop surfaces above the sheet so grouped
391 // content keeps a visible boundary. Regular cards retain their desktop surface.
372 const groupedCard =392 const groupedCard =
373 themeId === DEFAULT_MOBILE_THEME_ID393 themeId === DEFAULT_MOBILE_THEME_ID
374 ? appearance === "light"394 ? appearance === "light"
375 ? colors.toolbarControlHover395 ? colors.toolbarControlHover
376 : colors.sidebarRowActive396 : colors.sidebarRowActive
377 : colors.surface;397 : colors.surface === colors.chrome
398 ? colors.surfaceRaised
399 : colors.surface;
378 const mobileColors =400 const mobileColors =
379 themeId === DEFAULT_MOBILE_THEME_ID401 themeId === DEFAULT_MOBILE_THEME_ID
380 ? {402 ? {