authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-08-28 07:54:02-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-28 07:54:02-07:00
log018d7f2775daabd2ef07898af29586915a0b7f67
treebd856f872c5a262273a9f4a07d64fb4114db4bd9
parent9257bd860a44ce2a324c917ca66a1a81e73607bd
signature Signed by PGP key B5690EEEBB952194

refactor(mobile): compile semantic themes for Uniwind (#7327)

Co-authored-by: codex <codex@users.noreply.github.com>

111 files changed, 3636 insertions(+), 1221 deletions(-)

apps/mobile/generated-uniwind-default-theme-variables.json created+136
......@@ -0,0 +1,136 @@
1{
2 "light": {
3 "--color-screen": "#f2f2f7",
4 "--color-sheet": "rgba(242, 242, 247, 0.98)",
5 "--color-sheet-solid": "#f2f2f7",
6 "--color-card": "#ffffff",
7 "--color-card-alt": "#f5f5f5",
8 "--color-card-translucent": "rgba(255, 255, 255, 0.8)",
9 "--color-foreground": "#262626",
10 "--color-foreground-secondary": "#525252",
11 "--color-foreground-muted": "#737373",
12 "--color-foreground-tertiary": "#8e8e93",
13 "--color-border": "rgba(0, 0, 0, 0.08)",
14 "--color-border-subtle": "rgba(0, 0, 0, 0.06)",
15 "--color-separator": "rgba(0, 0, 0, 0.04)",
16 "--color-subtle": "rgba(0, 0, 0, 0.04)",
17 "--color-subtle-strong": "rgba(0, 0, 0, 0.08)",
18 "--color-inline-skill-background": "rgba(217, 70, 239, 0.12)",
19 "--color-inline-skill-border": "rgba(217, 70, 239, 0.25)",
20 "--color-inline-skill-foreground": "#a21caf",
21 "--color-primary": "#262626",
22 "--color-primary-foreground": "#ffffff",
23 "--color-primary-shadow": "#000000",
24 "--color-secondary": "#ffffff",
25 "--color-secondary-foreground": "#262626",
26 "--color-secondary-border": "rgba(0, 0, 0, 0.08)",
27 "--color-switch-active-track": "#34c759",
28 "--color-switch-active-thumb": "#ffffff",
29 "--color-switch-inactive-track": "rgba(0, 0, 0, 0.08)",
30 "--color-switch-inactive-thumb": "#8e8e93",
31 "--color-danger": "#fef2f2",
32 "--color-danger-border": "rgba(239, 68, 68, 0.12)",
33 "--color-danger-foreground": "#dc2626",
34 "--color-input": "#ffffff",
35 "--color-input-border": "rgba(0, 0, 0, 0.1)",
36 "--color-sidebar-search": "rgba(118, 118, 128, 0.12)",
37 "--color-placeholder": "#737373",
38 "--color-icon": "#262626",
39 "--color-icon-muted": "#525252",
40 "--color-icon-subtle": "#a3a3a3",
41 "--color-header": "rgba(255, 255, 255, 0.97)",
42 "--color-header-border": "rgba(0, 0, 0, 0.06)",
43 "--color-glass-surface": "rgba(255, 255, 255, 0.72)",
44 "--color-glass-tint": "rgba(255, 255, 255, 0.18)",
45 "--color-status-bar": "#f2f2f7",
46 "--color-md-body": "#111111",
47 "--color-md-strong": "#000000",
48 "--color-md-link": "#2563eb",
49 "--color-md-blockquote-border": "rgba(0, 0, 0, 0.08)",
50 "--color-md-blockquote-bg": "rgba(0, 0, 0, 0.02)",
51 "--color-md-code-bg": "rgba(0, 0, 0, 0.04)",
52 "--color-md-code-text": "#262626",
53 "--color-md-user-code-bg": "rgba(255, 255, 255, 0.22)",
54 "--color-md-user-code-text": "#ffffff",
55 "--color-md-user-fence-bg": "rgba(0, 0, 0, 0.16)",
56 "--color-md-user-fence-text": "#ffffff",
57 "--color-md-hr": "rgba(0, 0, 0, 0.08)",
58 "--color-user-bubble": "#007aff",
59 "--color-user-bubble-foreground": "#ffffff",
60 "--color-user-bubble-foreground-muted": "rgba(255, 255, 255, 0.78)",
61 "--color-user-bubble-skill-foreground": "#f0abfc",
62 "--color-backdrop": "rgba(0, 0, 0, 0.22)",
63 "--color-drawer": "rgba(255, 255, 255, 0.99)",
64 "--color-drawer-shadow": "rgba(0, 0, 0, 0.12)",
65 "--color-dot-separator": "rgba(0, 0, 0, 0.2)",
66 "--color-wordmark": "#262626",
67 "--color-chevron": "rgba(0, 0, 0, 0.2)"
68 },
69 "dark": {
70 "--color-screen": "#0a0a0a",
71 "--color-sheet": "rgba(14, 14, 14, 0.98)",
72 "--color-sheet-solid": "#0e0e0e",
73 "--color-card": "#171717",
74 "--color-card-alt": "#1c1c1c",
75 "--color-card-translucent": "rgba(17, 17, 17, 0.8)",
76 "--color-foreground": "#f5f5f5",
77 "--color-foreground-secondary": "#a3a3a3",
78 "--color-foreground-muted": "#8e8e93",
79 "--color-foreground-tertiary": "#636366",
80 "--color-border": "rgba(255, 255, 255, 0.06)",
81 "--color-border-subtle": "rgba(255, 255, 255, 0.04)",
82 "--color-separator": "rgba(255, 255, 255, 0.03)",
83 "--color-subtle": "rgba(255, 255, 255, 0.04)",
84 "--color-subtle-strong": "rgba(255, 255, 255, 0.08)",
85 "--color-inline-skill-background": "rgba(217, 70, 239, 0.12)",
86 "--color-inline-skill-border": "rgba(217, 70, 239, 0.25)",
87 "--color-inline-skill-foreground": "#f0abfc",
88 "--color-primary": "#f5f5f5",
89 "--color-primary-foreground": "#0a0a0a",
90 "--color-primary-shadow": "#000000",
91 "--color-secondary": "rgba(255, 255, 255, 0.04)",
92 "--color-secondary-foreground": "#f5f5f5",
93 "--color-secondary-border": "rgba(255, 255, 255, 0.06)",
94 "--color-switch-active-track": "#30d158",
95 "--color-switch-active-thumb": "#ffffff",
96 "--color-switch-inactive-track": "rgba(255, 255, 255, 0.06)",
97 "--color-switch-inactive-thumb": "#8e8e93",
98 "--color-danger": "rgba(239, 68, 68, 0.14)",
99 "--color-danger-border": "rgba(248, 113, 113, 0.18)",
100 "--color-danger-foreground": "#fca5a5",
101 "--color-input": "#141414",
102 "--color-input-border": "rgba(255, 255, 255, 0.08)",
103 "--color-sidebar-search": "rgba(118, 118, 128, 0.24)",
104 "--color-placeholder": "#8e8e93",
105 "--color-icon": "#f5f5f5",
106 "--color-icon-muted": "#a3a3a3",
107 "--color-icon-subtle": "#8e8e93",
108 "--color-header": "rgba(10, 10, 10, 0.97)",
109 "--color-header-border": "rgba(255, 255, 255, 0.06)",
110 "--color-glass-surface": "rgba(23, 23, 23, 0.78)",
111 "--color-glass-tint": "rgba(23, 23, 23, 0.24)",
112 "--color-status-bar": "#0a0a0a",
113 "--color-md-body": "#e5e5e5",
114 "--color-md-strong": "#f5f5f5",
115 "--color-md-link": "#60a5fa",
116 "--color-md-blockquote-border": "rgba(255, 255, 255, 0.1)",
117 "--color-md-blockquote-bg": "rgba(255, 255, 255, 0.03)",
118 "--color-md-code-bg": "rgba(255, 255, 255, 0.06)",
119 "--color-md-code-text": "#e5e5e5",
120 "--color-md-user-code-bg": "rgba(255, 255, 255, 0.18)",
121 "--color-md-user-code-text": "#ffffff",
122 "--color-md-user-fence-bg": "rgba(0, 0, 0, 0.28)",
123 "--color-md-user-fence-text": "#ffffff",
124 "--color-md-hr": "rgba(255, 255, 255, 0.08)",
125 "--color-user-bubble": "#0a84ff",
126 "--color-user-bubble-foreground": "#ffffff",
127 "--color-user-bubble-foreground-muted": "rgba(255, 255, 255, 0.78)",
128 "--color-user-bubble-skill-foreground": "#f0abfc",
129 "--color-backdrop": "rgba(0, 0, 0, 0.48)",
130 "--color-drawer": "rgba(14, 14, 14, 0.99)",
131 "--color-drawer-shadow": "rgba(0, 0, 0, 0.32)",
132 "--color-dot-separator": "rgba(255, 255, 255, 0.2)",
133 "--color-wordmark": "#f5f5f5",
134 "--color-chevron": "rgba(255, 255, 255, 0.2)"
135 }
136}
apps/mobile/generated-uniwind-theme-names.json created+12
......@@ -0,0 +1,12 @@
1[
2 "t3-chat-light",
3 "t3-chat-dark",
4 "grove-light",
5 "grove-dark",
6 "ocean-light",
7 "ocean-dark",
8 "ember-light",
9 "ember-dark",
10 "iris-light",
11 "iris-dark"
12]
apps/mobile/generated-uniwind-themes.css created+1374
......@@ -0,0 +1,1374 @@
1/* Generated by scripts/generate-uniwind-themes.mts. Do not edit manually. */
2@layer theme {
3 :root {
4 @variant light {
5 --color-adaptive-amber-50-950-a40: oklch(98.7% 0.022 95.277);
6 --color-adaptive-amber-200-900-a60: oklch(92.4% 0.12 95.746);
7 --color-adaptive-amber-500-a12-a16: oklch(76.9% 0.188 70.08 / 12%);
8 --color-adaptive-amber-700-300: oklch(55.5% 0.163 48.998);
9 --color-adaptive-amber-700-400: oklch(55.5% 0.163 48.998);
10 --color-adaptive-amber-800-200: oklch(47.3% 0.137 46.201);
11 --color-adaptive-blue-50-blue-400-a14: oklch(97% 0.014 254.604);
12 --color-adaptive-blue-300-a50-blue-400-a28: oklch(80.9% 0.105 251.813 / 50%);
13 --color-adaptive-blue-500-a20-blue-400-a15: oklch(62.3% 0.214 259.815 / 20%);
14 --color-adaptive-blue-500-400: oklch(62.3% 0.214 259.815);
15 --color-adaptive-blue-600-400: oklch(54.6% 0.245 262.881);
16 --color-adaptive-black-a10-a25: rgb(0 0 0 / 10%);
17 --color-adaptive-black-a15-a35: rgb(0 0 0 / 15%);
18 --color-adaptive-emerald-500-a12-a16: oklch(69.6% 0.17 162.48 / 12%);
19 --color-adaptive-emerald-600-400: oklch(59.6% 0.145 163.225);
20 --color-adaptive-emerald-700-300: oklch(50.8% 0.118 165.612);
21 --color-adaptive-indigo-500-a12-a16: oklch(58.5% 0.233 277.117 / 12%);
22 --color-adaptive-indigo-600-300: oklch(51.1% 0.262 276.966);
23 --color-adaptive-indigo-700-300: oklch(45.7% 0.24 277.023);
24 --color-adaptive-neutral-100-900: oklch(97% 0 0);
25 --color-adaptive-neutral-200-700-a60: oklch(92.2% 0 0);
26 --color-adaptive-neutral-200-800: oklch(92.2% 0 0);
27 --color-adaptive-neutral-200-a70-white-a8: oklch(92.2% 0 0 / 70%);
28 --color-adaptive-neutral-200-white-a6: oklch(92.2% 0 0);
29 --color-adaptive-neutral-200-white-a8: oklch(92.2% 0 0);
30 --color-adaptive-neutral-200-a80-white-a8: oklch(92.2% 0 0 / 80%);
31 --color-adaptive-neutral-300-a60-white-a12: oklch(87% 0 0 / 60%);
32 --color-adaptive-neutral-400-500: oklch(70.8% 0 0);
33 --color-adaptive-neutral-400-a60-500-a60: oklch(70.8% 0 0 / 60%);
34 --color-adaptive-neutral-400-a80-500-a80: oklch(70.8% 0 0 / 80%);
35 --color-adaptive-neutral-500-a10-a16: oklch(55.6% 0 0 / 10%);
36 --color-adaptive-neutral-500-400: oklch(55.6% 0 0);
37 --color-adaptive-neutral-500-500: oklch(55.6% 0 0);
38 --color-adaptive-neutral-600-300: oklch(43.9% 0 0);
39 --color-adaptive-neutral-600-400: oklch(43.9% 0 0);
40 --color-adaptive-neutral-950-50: oklch(14.5% 0 0);
41 --color-adaptive-red-50-950-a80: oklch(97.1% 0.013 17.38);
42 --color-adaptive-red-200-800: oklch(88.5% 0.062 18.334);
43 --color-adaptive-red-600-a80-400-a80: oklch(57.7% 0.245 27.325 / 80%);
44 --color-adaptive-red-700-300: oklch(50.5% 0.213 27.518);
45 --color-adaptive-rose-100-500-a18: oklch(94.1% 0.03 12.58);
46 --color-adaptive-rose-100-a80-500-a12: oklch(94.1% 0.03 12.58 / 80%);
47 --color-adaptive-rose-300-a70-400-a28: oklch(81% 0.117 11.638 / 70%);
48 --color-adaptive-rose-500-a12-a16: oklch(64.5% 0.246 16.439 / 12%);
49 --color-adaptive-rose-500-400: oklch(64.5% 0.246 16.439);
50 --color-adaptive-rose-600-400: oklch(58.6% 0.253 17.585);
51 --color-adaptive-rose-700-300: oklch(51.4% 0.222 16.935);
52 --color-adaptive-sky-500-a12-a16: oklch(68.5% 0.169 237.323 / 12%);
53 --color-adaptive-sky-600-400: oklch(58.8% 0.158 241.966);
54 --color-adaptive-sky-700-300: oklch(50% 0.134 242.749);
55 --color-adaptive-violet-500-a12-a16: oklch(60.6% 0.25 292.717 / 12%);
56 --color-adaptive-violet-600-400: oklch(54.1% 0.281 293.009);
57 --color-adaptive-violet-700-300: oklch(49.1% 0.27 292.581);
58 --color-adaptive-white-neutral-950-a70: #fff;
59 --color-adaptive-zinc-500-a12-a16: oklch(55.2% 0.016 285.938 / 12%);
60 --color-adaptive-zinc-500-400: oklch(55.2% 0.016 285.938);
61 --color-adaptive-zinc-600-300: oklch(44.2% 0.017 285.786);
62 }
63
64 @variant dark {
65 --color-adaptive-amber-50-950-a40: oklch(27.9% 0.077 45.635 / 40%);
66 --color-adaptive-amber-200-900-a60: oklch(41.4% 0.112 45.904 / 60%);
67 --color-adaptive-amber-500-a12-a16: oklch(76.9% 0.188 70.08 / 16%);
68 --color-adaptive-amber-700-300: oklch(87.9% 0.169 91.605);
69 --color-adaptive-amber-700-400: oklch(82.8% 0.189 84.429);
70 --color-adaptive-amber-800-200: oklch(92.4% 0.12 95.746);
71 --color-adaptive-blue-50-blue-400-a14: oklch(70.7% 0.165 254.624 / 14%);
72 --color-adaptive-blue-300-a50-blue-400-a28: oklch(70.7% 0.165 254.624 / 28%);
73 --color-adaptive-blue-500-a20-blue-400-a15: oklch(70.7% 0.165 254.624 / 15%);
74 --color-adaptive-blue-500-400: oklch(70.7% 0.165 254.624);
75 --color-adaptive-blue-600-400: oklch(70.7% 0.165 254.624);
76 --color-adaptive-black-a10-a25: rgb(0 0 0 / 25%);
77 --color-adaptive-black-a15-a35: rgb(0 0 0 / 35%);
78 --color-adaptive-emerald-500-a12-a16: oklch(69.6% 0.17 162.48 / 16%);
79 --color-adaptive-emerald-600-400: oklch(76.5% 0.177 163.223);
80 --color-adaptive-emerald-700-300: oklch(84.5% 0.143 164.978);
81 --color-adaptive-indigo-500-a12-a16: oklch(58.5% 0.233 277.117 / 16%);
82 --color-adaptive-indigo-600-300: oklch(78.5% 0.115 274.713);
83 --color-adaptive-indigo-700-300: oklch(78.5% 0.115 274.713);
84 --color-adaptive-neutral-100-900: oklch(20.5% 0 0);
85 --color-adaptive-neutral-200-700-a60: oklch(37.1% 0 0 / 60%);
86 --color-adaptive-neutral-200-800: oklch(26.9% 0 0);
87 --color-adaptive-neutral-200-a70-white-a8: rgb(255 255 255 / 8%);
88 --color-adaptive-neutral-200-white-a6: rgb(255 255 255 / 6%);
89 --color-adaptive-neutral-200-white-a8: rgb(255 255 255 / 8%);
90 --color-adaptive-neutral-200-a80-white-a8: rgb(255 255 255 / 8%);
91 --color-adaptive-neutral-300-a60-white-a12: rgb(255 255 255 / 12%);
92 --color-adaptive-neutral-400-500: oklch(55.6% 0 0);
93 --color-adaptive-neutral-400-a60-500-a60: oklch(55.6% 0 0 / 60%);
94 --color-adaptive-neutral-400-a80-500-a80: oklch(55.6% 0 0 / 80%);
95 --color-adaptive-neutral-500-a10-a16: oklch(55.6% 0 0 / 16%);
96 --color-adaptive-neutral-500-400: oklch(70.8% 0 0);
97 --color-adaptive-neutral-500-500: oklch(55.6% 0 0);
98 --color-adaptive-neutral-600-300: oklch(87% 0 0);
99 --color-adaptive-neutral-600-400: oklch(70.8% 0 0);
100 --color-adaptive-neutral-950-50: oklch(98.5% 0 0);
101 --color-adaptive-red-50-950-a80: oklch(25.8% 0.092 26.042 / 80%);
102 --color-adaptive-red-200-800: oklch(44.4% 0.177 26.899);
103 --color-adaptive-red-600-a80-400-a80: oklch(70.4% 0.191 22.216 / 80%);
104 --color-adaptive-red-700-300: oklch(80.8% 0.114 19.571);
105 --color-adaptive-rose-100-500-a18: oklch(64.5% 0.246 16.439 / 18%);
106 --color-adaptive-rose-100-a80-500-a12: oklch(64.5% 0.246 16.439 / 12%);
107 --color-adaptive-rose-300-a70-400-a28: oklch(71.2% 0.194 13.428 / 28%);
108 --color-adaptive-rose-500-a12-a16: oklch(64.5% 0.246 16.439 / 16%);
109 --color-adaptive-rose-500-400: oklch(71.2% 0.194 13.428);
110 --color-adaptive-rose-600-400: oklch(71.2% 0.194 13.428);
111 --color-adaptive-rose-700-300: oklch(81% 0.117 11.638);
112 --color-adaptive-sky-500-a12-a16: oklch(68.5% 0.169 237.323 / 16%);
113 --color-adaptive-sky-600-400: oklch(74.6% 0.16 232.661);
114 --color-adaptive-sky-700-300: oklch(82.8% 0.111 230.318);
115 --color-adaptive-violet-500-a12-a16: oklch(60.6% 0.25 292.717 / 16%);
116 --color-adaptive-violet-600-400: oklch(70.2% 0.183 293.541);
117 --color-adaptive-violet-700-300: oklch(81.1% 0.111 293.571);
118 --color-adaptive-white-neutral-950-a70: oklch(14.5% 0 0 / 70%);
119 --color-adaptive-zinc-500-a12-a16: oklch(55.2% 0.016 285.938 / 16%);
120 --color-adaptive-zinc-500-400: oklch(70.5% 0.015 286.067);
121 --color-adaptive-zinc-600-300: oklch(87.1% 0.006 286.286);
122 }
123
124 @variant t3-chat-light {
125 --color-screen: #fdf7fd;
126 --color-sheet: rgba(253, 247, 253, 0.98);
127 --color-sheet-solid: #fdf7fd;
128 --color-card: #fdfafd;
129 --color-card-alt: #faf3fb;
130 --color-card-translucent: rgba(253, 250, 253, 0.8);
131 --color-foreground: #501854;
132 --color-foreground-secondary: #ac1668;
133 --color-foreground-muted: #8d1255;
134 --color-foreground-tertiary: #ac1668;
135 --color-border: #eee1ed;
136 --color-border-subtle: rgba(238, 225, 237, 0.7);
137 --color-separator: rgba(238, 225, 237, 0.55);
138 --color-subtle: #eaa7cb;
139 --color-subtle-strong: #f1c4e6;
140 --color-inline-skill-background: #f3e6f5;
141 --color-inline-skill-border: rgba(219, 39, 119, 0.42);
142 --color-inline-skill-foreground: #454554;
143 --color-primary: #db2777;
144 --color-primary-foreground: #ffffff;
145 --color-primary-shadow: #000000;
146 --color-secondary: #f1c4e6;
147 --color-secondary-foreground: #77347c;
148 --color-secondary-border: #eee1ed;
149 --color-switch-active-track: #db2777;
150 --color-switch-active-thumb: #ffffff;
151 --color-switch-inactive-track: #f1c4e6;
152 --color-switch-inactive-thumb: #8d1255;
153 --color-danger: #fde4f1;
154 --color-danger-border: rgba(247, 8, 108, 0.32);
155 --color-danger-foreground: #9d174d;
156 --color-input: #fdfafd;
157 --color-input-border: #e7c1dc;
158 --color-sidebar-search: #f8f8f7;
159 --color-placeholder: #8b5f90;
160 --color-icon: #501854;
161 --color-icon-muted: #ac1668;
162 --color-icon-subtle: #ac1668;
163 --color-header: rgba(253, 247, 253, 0.97);
164 --color-header-border: #efbdeb;
165 --color-glass-surface: rgba(255, 255, 255, 0.74);
166 --color-glass-tint: rgba(255, 255, 255, 0.22);
167 --color-status-bar: #fdf7fd;
168 --color-md-body: #501854;
169 --color-md-strong: #501854;
170 --color-md-link: #db2777;
171 --color-md-blockquote-border: #eee1ed;
172 --color-md-blockquote-bg: #eaa7cb;
173 --color-md-code-bg: #f5ecf9;
174 --color-md-code-text: #673c8b;
175 --color-md-user-code-bg: rgba(73, 44, 97, 0.18);
176 --color-md-user-code-text: #492c61;
177 --color-md-user-fence-bg: rgba(0, 0, 0, 0.16);
178 --color-md-user-fence-text: #492c61;
179 --color-md-hr: #eee1ed;
180 --color-user-bubble: #f7def2;
181 --color-user-bubble-foreground: #492c61;
182 --color-user-bubble-foreground-muted: rgba(73, 44, 97, 0.78);
183 --color-user-bubble-skill-foreground: #c12269;
184 --color-backdrop: rgba(0, 0, 0, 0.22);
185 --color-drawer: rgba(242, 225, 244, 0.99);
186 --color-drawer-shadow: rgba(0, 0, 0, 0.12);
187 --color-dot-separator: rgba(172, 22, 104, 0.35);
188 --color-wordmark: #501854;
189 --color-chevron: rgba(172, 22, 104, 0.42);
190 --color-adaptive-amber-50-950-a40: oklch(98.7% 0.022 95.277);
191 --color-adaptive-amber-200-900-a60: oklch(92.4% 0.12 95.746);
192 --color-adaptive-amber-500-a12-a16: oklch(76.9% 0.188 70.08 / 12%);
193 --color-adaptive-amber-700-300: oklch(55.5% 0.163 48.998);
194 --color-adaptive-amber-700-400: oklch(55.5% 0.163 48.998);
195 --color-adaptive-amber-800-200: oklch(47.3% 0.137 46.201);
196 --color-adaptive-blue-50-blue-400-a14: oklch(97% 0.014 254.604);
197 --color-adaptive-blue-300-a50-blue-400-a28: oklch(80.9% 0.105 251.813 / 50%);
198 --color-adaptive-blue-500-a20-blue-400-a15: oklch(62.3% 0.214 259.815 / 20%);
199 --color-adaptive-blue-500-400: oklch(62.3% 0.214 259.815);
200 --color-adaptive-blue-600-400: oklch(54.6% 0.245 262.881);
201 --color-adaptive-black-a10-a25: rgb(0 0 0 / 10%);
202 --color-adaptive-black-a15-a35: rgb(0 0 0 / 15%);
203 --color-adaptive-emerald-500-a12-a16: oklch(69.6% 0.17 162.48 / 12%);
204 --color-adaptive-emerald-600-400: oklch(59.6% 0.145 163.225);
205 --color-adaptive-emerald-700-300: oklch(50.8% 0.118 165.612);
206 --color-adaptive-indigo-500-a12-a16: oklch(58.5% 0.233 277.117 / 12%);
207 --color-adaptive-indigo-600-300: oklch(51.1% 0.262 276.966);
208 --color-adaptive-indigo-700-300: oklch(45.7% 0.24 277.023);
209 --color-adaptive-neutral-100-900: oklch(97% 0 0);
210 --color-adaptive-neutral-200-700-a60: oklch(92.2% 0 0);
211 --color-adaptive-neutral-200-800: oklch(92.2% 0 0);
212 --color-adaptive-neutral-200-a70-white-a8: oklch(92.2% 0 0 / 70%);
213 --color-adaptive-neutral-200-white-a6: oklch(92.2% 0 0);
214 --color-adaptive-neutral-200-white-a8: oklch(92.2% 0 0);
215 --color-adaptive-neutral-200-a80-white-a8: oklch(92.2% 0 0 / 80%);
216 --color-adaptive-neutral-300-a60-white-a12: oklch(87% 0 0 / 60%);
217 --color-adaptive-neutral-400-500: oklch(70.8% 0 0);
218 --color-adaptive-neutral-400-a60-500-a60: oklch(70.8% 0 0 / 60%);
219 --color-adaptive-neutral-400-a80-500-a80: oklch(70.8% 0 0 / 80%);
220 --color-adaptive-neutral-500-a10-a16: oklch(55.6% 0 0 / 10%);
221 --color-adaptive-neutral-500-400: oklch(55.6% 0 0);
222 --color-adaptive-neutral-500-500: oklch(55.6% 0 0);
223 --color-adaptive-neutral-600-300: oklch(43.9% 0 0);
224 --color-adaptive-neutral-600-400: oklch(43.9% 0 0);
225 --color-adaptive-neutral-950-50: oklch(14.5% 0 0);
226 --color-adaptive-red-50-950-a80: oklch(97.1% 0.013 17.38);
227 --color-adaptive-red-200-800: oklch(88.5% 0.062 18.334);
228 --color-adaptive-red-600-a80-400-a80: oklch(57.7% 0.245 27.325 / 80%);
229 --color-adaptive-red-700-300: oklch(50.5% 0.213 27.518);
230 --color-adaptive-rose-100-500-a18: oklch(94.1% 0.03 12.58);
231 --color-adaptive-rose-100-a80-500-a12: oklch(94.1% 0.03 12.58 / 80%);
232 --color-adaptive-rose-300-a70-400-a28: oklch(81% 0.117 11.638 / 70%);
233 --color-adaptive-rose-500-a12-a16: oklch(64.5% 0.246 16.439 / 12%);
234 --color-adaptive-rose-500-400: oklch(64.5% 0.246 16.439);
235 --color-adaptive-rose-600-400: oklch(58.6% 0.253 17.585);
236 --color-adaptive-rose-700-300: oklch(51.4% 0.222 16.935);
237 --color-adaptive-sky-500-a12-a16: oklch(68.5% 0.169 237.323 / 12%);
238 --color-adaptive-sky-600-400: oklch(58.8% 0.158 241.966);
239 --color-adaptive-sky-700-300: oklch(50% 0.134 242.749);
240 --color-adaptive-violet-500-a12-a16: oklch(60.6% 0.25 292.717 / 12%);
241 --color-adaptive-violet-600-400: oklch(54.1% 0.281 293.009);
242 --color-adaptive-violet-700-300: oklch(49.1% 0.27 292.581);
243 --color-adaptive-white-neutral-950-a70: #fff;
244 --color-adaptive-zinc-500-a12-a16: oklch(55.2% 0.016 285.938 / 12%);
245 --color-adaptive-zinc-500-400: oklch(55.2% 0.016 285.938);
246 --color-adaptive-zinc-600-300: oklch(44.2% 0.017 285.786);
247 }
248
249 @variant t3-chat-dark {
250 --color-screen: #1f1a24;
251 --color-sheet: rgba(31, 26, 36, 0.98);
252 --color-sheet-solid: #1f1a24;
253 --color-card: #2c2631;
254 --color-card-alt: #29232d;
255 --color-card-translucent: rgba(44, 38, 49, 0.8);
256 --color-foreground: #f9f8fb;
257 --color-foreground-secondary: #e7d0dd;
258 --color-foreground-muted: #e7d0dd;
259 --color-foreground-tertiary: #e7d0dd;
260 --color-border: #27242c;
261 --color-border-subtle: rgba(39, 36, 44, 0.7);
262 --color-separator: rgba(39, 36, 44, 0.55);
263 --color-subtle: #423a45;
264 --color-subtle-strong: #362d3d;
265 --color-inline-skill-background: #463753;
266 --color-inline-skill-border: rgba(163, 0, 76, 0.42);
267 --color-inline-skill-foreground: #f8f1f5;
268 --color-primary: #a3004c;
269 --color-primary-foreground: #fbd0e8;
270 --color-primary-shadow: #000000;
271 --color-secondary: #362d3d;
272 --color-secondary-foreground: #d4c7e1;
273 --color-secondary-border: #27242c;
274 --color-switch-active-track: #a3004c;
275 --color-switch-active-thumb: #fbd0e8;
276 --color-switch-inactive-track: #362d3d;
277 --color-switch-inactive-thumb: #e7d0dd;
278 --color-danger: #331a2b;
279 --color-danger-border: rgba(157, 23, 77, 0.32);
280 --color-danger-foreground: #fbd0e8;
281 --color-input: #2c2631;
282 --color-input-border: #302029;
283 --color-sidebar-search: #261922;
284 --color-placeholder: #968d9f;
285 --color-icon: #f9f8fb;
286 --color-icon-muted: #d4c7e1;
287 --color-icon-subtle: #e7d0dd;
288 --color-header: rgba(31, 26, 36, 0.97);
289 --color-header-border: #27242c;
290 --color-glass-surface: rgba(16, 10, 14, 0.74);
291 --color-glass-tint: rgba(16, 10, 14, 0.22);
292 --color-status-bar: #1f1a24;
293 --color-md-body: #f9f8fb;
294 --color-md-strong: #f9f8fb;
295 --color-md-link: #a3004c;
296 --color-md-blockquote-border: #27242c;
297 --color-md-blockquote-bg: #423a45;
298 --color-md-code-bg: #1f1a24;
299 --color-md-code-text: #d8c3ef;
300 --color-md-user-code-bg: rgba(242, 235, 250, 0.18);
301 --color-md-user-code-text: #f2ebfa;
302 --color-md-user-fence-bg: rgba(0, 0, 0, 0.28);
303 --color-md-user-fence-text: #f2ebfa;
304 --color-md-hr: #27242c;
305 --color-user-bubble: #2b2431;
306 --color-user-bubble-foreground: #f2ebfa;
307 --color-user-bubble-foreground-muted: rgba(242, 235, 250, 0.78);
308 --color-user-bubble-skill-foreground: #cb709a;
309 --color-backdrop: rgba(0, 0, 0, 0.48);
310 --color-drawer: rgba(23, 16, 24, 0.99);
311 --color-drawer-shadow: rgba(0, 0, 0, 0.32);
312 --color-dot-separator: rgba(231, 208, 221, 0.35);
313 --color-wordmark: #f9f8fb;
314 --color-chevron: rgba(231, 208, 221, 0.42);
315 --color-adaptive-amber-50-950-a40: oklch(27.9% 0.077 45.635 / 40%);
316 --color-adaptive-amber-200-900-a60: oklch(41.4% 0.112 45.904 / 60%);
317 --color-adaptive-amber-500-a12-a16: oklch(76.9% 0.188 70.08 / 16%);
318 --color-adaptive-amber-700-300: oklch(87.9% 0.169 91.605);
319 --color-adaptive-amber-700-400: oklch(82.8% 0.189 84.429);
320 --color-adaptive-amber-800-200: oklch(92.4% 0.12 95.746);
321 --color-adaptive-blue-50-blue-400-a14: oklch(70.7% 0.165 254.624 / 14%);
322 --color-adaptive-blue-300-a50-blue-400-a28: oklch(70.7% 0.165 254.624 / 28%);
323 --color-adaptive-blue-500-a20-blue-400-a15: oklch(70.7% 0.165 254.624 / 15%);
324 --color-adaptive-blue-500-400: oklch(70.7% 0.165 254.624);
325 --color-adaptive-blue-600-400: oklch(70.7% 0.165 254.624);
326 --color-adaptive-black-a10-a25: rgb(0 0 0 / 25%);
327 --color-adaptive-black-a15-a35: rgb(0 0 0 / 35%);
328 --color-adaptive-emerald-500-a12-a16: oklch(69.6% 0.17 162.48 / 16%);
329 --color-adaptive-emerald-600-400: oklch(76.5% 0.177 163.223);
330 --color-adaptive-emerald-700-300: oklch(84.5% 0.143 164.978);
331 --color-adaptive-indigo-500-a12-a16: oklch(58.5% 0.233 277.117 / 16%);
332 --color-adaptive-indigo-600-300: oklch(78.5% 0.115 274.713);
333 --color-adaptive-indigo-700-300: oklch(78.5% 0.115 274.713);
334 --color-adaptive-neutral-100-900: oklch(20.5% 0 0);
335 --color-adaptive-neutral-200-700-a60: oklch(37.1% 0 0 / 60%);
336 --color-adaptive-neutral-200-800: oklch(26.9% 0 0);
337 --color-adaptive-neutral-200-a70-white-a8: rgb(255 255 255 / 8%);
338 --color-adaptive-neutral-200-white-a6: rgb(255 255 255 / 6%);
339 --color-adaptive-neutral-200-white-a8: rgb(255 255 255 / 8%);
340 --color-adaptive-neutral-200-a80-white-a8: rgb(255 255 255 / 8%);
341 --color-adaptive-neutral-300-a60-white-a12: rgb(255 255 255 / 12%);
342 --color-adaptive-neutral-400-500: oklch(55.6% 0 0);
343 --color-adaptive-neutral-400-a60-500-a60: oklch(55.6% 0 0 / 60%);
344 --color-adaptive-neutral-400-a80-500-a80: oklch(55.6% 0 0 / 80%);
345 --color-adaptive-neutral-500-a10-a16: oklch(55.6% 0 0 / 16%);
346 --color-adaptive-neutral-500-400: oklch(70.8% 0 0);
347 --color-adaptive-neutral-500-500: oklch(55.6% 0 0);
348 --color-adaptive-neutral-600-300: oklch(87% 0 0);
349 --color-adaptive-neutral-600-400: oklch(70.8% 0 0);
350 --color-adaptive-neutral-950-50: oklch(98.5% 0 0);
351 --color-adaptive-red-50-950-a80: oklch(25.8% 0.092 26.042 / 80%);
352 --color-adaptive-red-200-800: oklch(44.4% 0.177 26.899);
353 --color-adaptive-red-600-a80-400-a80: oklch(70.4% 0.191 22.216 / 80%);
354 --color-adaptive-red-700-300: oklch(80.8% 0.114 19.571);
355 --color-adaptive-rose-100-500-a18: oklch(64.5% 0.246 16.439 / 18%);
356 --color-adaptive-rose-100-a80-500-a12: oklch(64.5% 0.246 16.439 / 12%);
357 --color-adaptive-rose-300-a70-400-a28: oklch(71.2% 0.194 13.428 / 28%);
358 --color-adaptive-rose-500-a12-a16: oklch(64.5% 0.246 16.439 / 16%);
359 --color-adaptive-rose-500-400: oklch(71.2% 0.194 13.428);
360 --color-adaptive-rose-600-400: oklch(71.2% 0.194 13.428);
361 --color-adaptive-rose-700-300: oklch(81% 0.117 11.638);
362 --color-adaptive-sky-500-a12-a16: oklch(68.5% 0.169 237.323 / 16%);
363 --color-adaptive-sky-600-400: oklch(74.6% 0.16 232.661);
364 --color-adaptive-sky-700-300: oklch(82.8% 0.111 230.318);
365 --color-adaptive-violet-500-a12-a16: oklch(60.6% 0.25 292.717 / 16%);
366 --color-adaptive-violet-600-400: oklch(70.2% 0.183 293.541);
367 --color-adaptive-violet-700-300: oklch(81.1% 0.111 293.571);
368 --color-adaptive-white-neutral-950-a70: oklch(14.5% 0 0 / 70%);
369 --color-adaptive-zinc-500-a12-a16: oklch(55.2% 0.016 285.938 / 16%);
370 --color-adaptive-zinc-500-400: oklch(70.5% 0.015 286.067);
371 --color-adaptive-zinc-600-300: oklch(87.1% 0.006 286.286);
372 }
373
374 @variant grove-light {
375 --color-screen: #f3f7f4;
376 --color-sheet: rgba(243, 247, 244, 0.98);
377 --color-sheet-solid: #f3f7f4;
378 --color-card: #ecefed;
379 --color-card-alt: #f3f7f4;
380 --color-card-translucent: rgba(236, 239, 237, 0.8);
381 --color-foreground: #241523;
382 --color-foreground-secondary: #746c73;
383 --color-foreground-muted: #6e696f;
384 --color-foreground-tertiary: #746c73;
385 --color-border: #cbd5d1;
386 --color-border-subtle: rgba(203, 213, 209, 0.7);
387 --color-separator: rgba(203, 213, 209, 0.55);
388 --color-subtle: #e6f0ea;
389 --color-subtle-strong: #e2ede7;
390 --color-inline-skill-background: #d5e6dd;
391 --color-inline-skill-border: rgba(27, 125, 80, 0.42);
392 --color-inline-skill-foreground: #241523;
393 --color-primary: #1b7d50;
394 --color-primary-foreground: #fffaff;
395 --color-primary-shadow: #000000;
396 --color-secondary: #e2ede7;
397 --color-secondary-foreground: #241523;
398 --color-secondary-border: #cbd5d1;
399 --color-switch-active-track: #1b7d50;
400 --color-switch-active-thumb: #fffaff;
401 --color-switch-inactive-track: #e2ede7;
402 --color-switch-inactive-thumb: #6e696f;
403 --color-danger: #f4e7e5;
404 --color-danger-border: rgba(251, 44, 54, 0.32);
405 --color-danger-foreground: #c10007;
406 --color-input: #ecefed;
407 --color-input-border: #becbc5;
408 --color-sidebar-search: #d3dcd8;
409 --color-placeholder: #716971;
410 --color-icon: #241523;
411 --color-icon-muted: #746c73;
412 --color-icon-subtle: #746c73;
413 --color-header: rgba(243, 247, 244, 0.97);
414 --color-header-border: #d5e6dd;
415 --color-glass-surface: rgba(231, 233, 232, 0.74);
416 --color-glass-tint: rgba(231, 233, 232, 0.22);
417 --color-status-bar: #f3f7f4;
418 --color-md-body: #241523;
419 --color-md-strong: #241523;
420 --color-md-link: #1b7d50;
421 --color-md-blockquote-border: #cbd5d1;
422 --color-md-blockquote-bg: #e6f0ea;
423 --color-md-code-bg: #eef1ef;
424 --color-md-code-text: #241523;
425 --color-md-user-code-bg: rgba(36, 21, 35, 0.18);
426 --color-md-user-code-text: #241523;
427 --color-md-user-fence-bg: rgba(0, 0, 0, 0.16);
428 --color-md-user-fence-text: #241523;
429 --color-md-hr: #cbd5d1;
430 --color-user-bubble: #cce1d7;
431 --color-user-bubble-foreground: #241523;
432 --color-user-bubble-foreground-muted: rgba(36, 21, 35, 0.78);
433 --color-user-bubble-skill-foreground: #815a0e;
434 --color-backdrop: rgba(0, 0, 0, 0.22);
435 --color-drawer: rgba(226, 237, 231, 0.99);
436 --color-drawer-shadow: rgba(0, 0, 0, 0.12);
437 --color-dot-separator: rgba(116, 108, 115, 0.35);
438 --color-wordmark: #241523;
439 --color-chevron: rgba(116, 108, 115, 0.42);
440 --color-adaptive-amber-50-950-a40: oklch(98.7% 0.022 95.277);
441 --color-adaptive-amber-200-900-a60: oklch(92.4% 0.12 95.746);
442 --color-adaptive-amber-500-a12-a16: oklch(76.9% 0.188 70.08 / 12%);
443 --color-adaptive-amber-700-300: oklch(55.5% 0.163 48.998);
444 --color-adaptive-amber-700-400: oklch(55.5% 0.163 48.998);
445 --color-adaptive-amber-800-200: oklch(47.3% 0.137 46.201);
446 --color-adaptive-blue-50-blue-400-a14: oklch(97% 0.014 254.604);
447 --color-adaptive-blue-300-a50-blue-400-a28: oklch(80.9% 0.105 251.813 / 50%);
448 --color-adaptive-blue-500-a20-blue-400-a15: oklch(62.3% 0.214 259.815 / 20%);
449 --color-adaptive-blue-500-400: oklch(62.3% 0.214 259.815);
450 --color-adaptive-blue-600-400: oklch(54.6% 0.245 262.881);
451 --color-adaptive-black-a10-a25: rgb(0 0 0 / 10%);
452 --color-adaptive-black-a15-a35: rgb(0 0 0 / 15%);
453 --color-adaptive-emerald-500-a12-a16: oklch(69.6% 0.17 162.48 / 12%);
454 --color-adaptive-emerald-600-400: oklch(59.6% 0.145 163.225);
455 --color-adaptive-emerald-700-300: oklch(50.8% 0.118 165.612);
456 --color-adaptive-indigo-500-a12-a16: oklch(58.5% 0.233 277.117 / 12%);
457 --color-adaptive-indigo-600-300: oklch(51.1% 0.262 276.966);
458 --color-adaptive-indigo-700-300: oklch(45.7% 0.24 277.023);
459 --color-adaptive-neutral-100-900: oklch(97% 0 0);
460 --color-adaptive-neutral-200-700-a60: oklch(92.2% 0 0);
461 --color-adaptive-neutral-200-800: oklch(92.2% 0 0);
462 --color-adaptive-neutral-200-a70-white-a8: oklch(92.2% 0 0 / 70%);
463 --color-adaptive-neutral-200-white-a6: oklch(92.2% 0 0);
464 --color-adaptive-neutral-200-white-a8: oklch(92.2% 0 0);
465 --color-adaptive-neutral-200-a80-white-a8: oklch(92.2% 0 0 / 80%);
466 --color-adaptive-neutral-300-a60-white-a12: oklch(87% 0 0 / 60%);
467 --color-adaptive-neutral-400-500: oklch(70.8% 0 0);
468 --color-adaptive-neutral-400-a60-500-a60: oklch(70.8% 0 0 / 60%);
469 --color-adaptive-neutral-400-a80-500-a80: oklch(70.8% 0 0 / 80%);
470 --color-adaptive-neutral-500-a10-a16: oklch(55.6% 0 0 / 10%);
471 --color-adaptive-neutral-500-400: oklch(55.6% 0 0);
472 --color-adaptive-neutral-500-500: oklch(55.6% 0 0);
473 --color-adaptive-neutral-600-300: oklch(43.9% 0 0);
474 --color-adaptive-neutral-600-400: oklch(43.9% 0 0);
475 --color-adaptive-neutral-950-50: oklch(14.5% 0 0);
476 --color-adaptive-red-50-950-a80: oklch(97.1% 0.013 17.38);
477 --color-adaptive-red-200-800: oklch(88.5% 0.062 18.334);
478 --color-adaptive-red-600-a80-400-a80: oklch(57.7% 0.245 27.325 / 80%);
479 --color-adaptive-red-700-300: oklch(50.5% 0.213 27.518);
480 --color-adaptive-rose-100-500-a18: oklch(94.1% 0.03 12.58);
481 --color-adaptive-rose-100-a80-500-a12: oklch(94.1% 0.03 12.58 / 80%);
482 --color-adaptive-rose-300-a70-400-a28: oklch(81% 0.117 11.638 / 70%);
483 --color-adaptive-rose-500-a12-a16: oklch(64.5% 0.246 16.439 / 12%);
484 --color-adaptive-rose-500-400: oklch(64.5% 0.246 16.439);
485 --color-adaptive-rose-600-400: oklch(58.6% 0.253 17.585);
486 --color-adaptive-rose-700-300: oklch(51.4% 0.222 16.935);
487 --color-adaptive-sky-500-a12-a16: oklch(68.5% 0.169 237.323 / 12%);
488 --color-adaptive-sky-600-400: oklch(58.8% 0.158 241.966);
489 --color-adaptive-sky-700-300: oklch(50% 0.134 242.749);
490 --color-adaptive-violet-500-a12-a16: oklch(60.6% 0.25 292.717 / 12%);
491 --color-adaptive-violet-600-400: oklch(54.1% 0.281 293.009);
492 --color-adaptive-violet-700-300: oklch(49.1% 0.27 292.581);
493 --color-adaptive-white-neutral-950-a70: #fff;
494 --color-adaptive-zinc-500-a12-a16: oklch(55.2% 0.016 285.938 / 12%);
495 --color-adaptive-zinc-500-400: oklch(55.2% 0.016 285.938);
496 --color-adaptive-zinc-600-300: oklch(44.2% 0.017 285.786);
497 }
498
499 @variant grove-dark {
500 --color-screen: #1b2821;
501 --color-sheet: rgba(27, 40, 33, 0.98);
502 --color-sheet-solid: #1b2821;
503 --color-card: #36413c;
504 --color-card-alt: #1b2821;
505 --color-card-translucent: rgba(54, 65, 60, 0.8);
506 --color-foreground: #fffaff;
507 --color-foreground-secondary: #919595;
508 --color-foreground-muted: #9da5a2;
509 --color-foreground-tertiary: #919595;
510 --color-border: #415f4f;
511 --color-border-subtle: rgba(65, 95, 79, 0.7);
512 --color-separator: rgba(65, 95, 79, 0.55);
513 --color-subtle: #253e31;
514 --color-subtle-strong: #2a4b39;
515 --color-inline-skill-background: #325c46;
516 --color-inline-skill-border: rgba(105, 214, 154, 0.42);
517 --color-inline-skill-foreground: #fffaff;
518 --color-primary: #69d69a;
519 --color-primary-foreground: #241523;
520 --color-primary-shadow: #000000;
521 --color-secondary: #2a4b39;
522 --color-secondary-foreground: #fffaff;
523 --color-secondary-border: #415f4f;
524 --color-switch-active-track: #69d69a;
525 --color-switch-active-thumb: #241523;
526 --color-switch-inactive-track: #2a4b39;
527 --color-switch-inactive-thumb: #9da5a2;
528 --color-danger: #3f2c28;
529 --color-danger-border: rgba(251, 65, 74, 0.32);
530 --color-danger-foreground: #ff6668;
531 --color-input: #36413c;
532 --color-input-border: #4f725f;
533 --color-sidebar-search: #45554d;
534 --color-placeholder: #a9abab;
535 --color-icon: #fffaff;
536 --color-icon-muted: #919595;
537 --color-icon-subtle: #919595;
538 --color-header: rgba(27, 40, 33, 0.97);
539 --color-header-border: #36654c;
540 --color-glass-surface: rgba(68, 77, 73, 0.74);
541 --color-glass-tint: rgba(68, 77, 73, 0.22);
542 --color-status-bar: #1b2821;
543 --color-md-body: #fffaff;
544 --color-md-strong: #fffaff;
545 --color-md-link: #69d69a;
546 --color-md-blockquote-border: #415f4f;
547 --color-md-blockquote-bg: #253e31;
548 --color-md-code-bg: #28342e;
549 --color-md-code-text: #fffaff;
550 --color-md-user-code-bg: rgba(255, 250, 255, 0.18);
551 --color-md-user-code-text: #fffaff;
552 --color-md-user-fence-bg: rgba(0, 0, 0, 0.28);
553 --color-md-user-fence-text: #fffaff;
554 --color-md-hr: #415f4f;
555 --color-user-bubble: #37664d;
556 --color-user-bubble-foreground: #fffaff;
557 --color-user-bubble-foreground-muted: rgba(255, 250, 255, 0.78);
558 --color-user-bubble-skill-foreground: #eed295;
559 --color-backdrop: rgba(0, 0, 0, 0.48);
560 --color-drawer: rgba(33, 54, 43, 0.99);
561 --color-drawer-shadow: rgba(0, 0, 0, 0.32);
562 --color-dot-separator: rgba(145, 149, 149, 0.35);
563 --color-wordmark: #fffaff;
564 --color-chevron: rgba(145, 149, 149, 0.42);
565 --color-adaptive-amber-50-950-a40: oklch(27.9% 0.077 45.635 / 40%);
566 --color-adaptive-amber-200-900-a60: oklch(41.4% 0.112 45.904 / 60%);
567 --color-adaptive-amber-500-a12-a16: oklch(76.9% 0.188 70.08 / 16%);
568 --color-adaptive-amber-700-300: oklch(87.9% 0.169 91.605);
569 --color-adaptive-amber-700-400: oklch(82.8% 0.189 84.429);
570 --color-adaptive-amber-800-200: oklch(92.4% 0.12 95.746);
571 --color-adaptive-blue-50-blue-400-a14: oklch(70.7% 0.165 254.624 / 14%);
572 --color-adaptive-blue-300-a50-blue-400-a28: oklch(70.7% 0.165 254.624 / 28%);
573 --color-adaptive-blue-500-a20-blue-400-a15: oklch(70.7% 0.165 254.624 / 15%);
574 --color-adaptive-blue-500-400: oklch(70.7% 0.165 254.624);
575 --color-adaptive-blue-600-400: oklch(70.7% 0.165 254.624);
576 --color-adaptive-black-a10-a25: rgb(0 0 0 / 25%);
577 --color-adaptive-black-a15-a35: rgb(0 0 0 / 35%);
578 --color-adaptive-emerald-500-a12-a16: oklch(69.6% 0.17 162.48 / 16%);
579 --color-adaptive-emerald-600-400: oklch(76.5% 0.177 163.223);
580 --color-adaptive-emerald-700-300: oklch(84.5% 0.143 164.978);
581 --color-adaptive-indigo-500-a12-a16: oklch(58.5% 0.233 277.117 / 16%);
582 --color-adaptive-indigo-600-300: oklch(78.5% 0.115 274.713);
583 --color-adaptive-indigo-700-300: oklch(78.5% 0.115 274.713);
584 --color-adaptive-neutral-100-900: oklch(20.5% 0 0);
585 --color-adaptive-neutral-200-700-a60: oklch(37.1% 0 0 / 60%);
586 --color-adaptive-neutral-200-800: oklch(26.9% 0 0);
587 --color-adaptive-neutral-200-a70-white-a8: rgb(255 255 255 / 8%);
588 --color-adaptive-neutral-200-white-a6: rgb(255 255 255 / 6%);
589 --color-adaptive-neutral-200-white-a8: rgb(255 255 255 / 8%);
590 --color-adaptive-neutral-200-a80-white-a8: rgb(255 255 255 / 8%);
591 --color-adaptive-neutral-300-a60-white-a12: rgb(255 255 255 / 12%);
592 --color-adaptive-neutral-400-500: oklch(55.6% 0 0);
593 --color-adaptive-neutral-400-a60-500-a60: oklch(55.6% 0 0 / 60%);
594 --color-adaptive-neutral-400-a80-500-a80: oklch(55.6% 0 0 / 80%);
595 --color-adaptive-neutral-500-a10-a16: oklch(55.6% 0 0 / 16%);
596 --color-adaptive-neutral-500-400: oklch(70.8% 0 0);
597 --color-adaptive-neutral-500-500: oklch(55.6% 0 0);
598 --color-adaptive-neutral-600-300: oklch(87% 0 0);
599 --color-adaptive-neutral-600-400: oklch(70.8% 0 0);
600 --color-adaptive-neutral-950-50: oklch(98.5% 0 0);
601 --color-adaptive-red-50-950-a80: oklch(25.8% 0.092 26.042 / 80%);
602 --color-adaptive-red-200-800: oklch(44.4% 0.177 26.899);
603 --color-adaptive-red-600-a80-400-a80: oklch(70.4% 0.191 22.216 / 80%);
604 --color-adaptive-red-700-300: oklch(80.8% 0.114 19.571);
605 --color-adaptive-rose-100-500-a18: oklch(64.5% 0.246 16.439 / 18%);
606 --color-adaptive-rose-100-a80-500-a12: oklch(64.5% 0.246 16.439 / 12%);
607 --color-adaptive-rose-300-a70-400-a28: oklch(71.2% 0.194 13.428 / 28%);
608 --color-adaptive-rose-500-a12-a16: oklch(64.5% 0.246 16.439 / 16%);
609 --color-adaptive-rose-500-400: oklch(71.2% 0.194 13.428);
610 --color-adaptive-rose-600-400: oklch(71.2% 0.194 13.428);
611 --color-adaptive-rose-700-300: oklch(81% 0.117 11.638);
612 --color-adaptive-sky-500-a12-a16: oklch(68.5% 0.169 237.323 / 16%);
613 --color-adaptive-sky-600-400: oklch(74.6% 0.16 232.661);
614 --color-adaptive-sky-700-300: oklch(82.8% 0.111 230.318);
615 --color-adaptive-violet-500-a12-a16: oklch(60.6% 0.25 292.717 / 16%);
616 --color-adaptive-violet-600-400: oklch(70.2% 0.183 293.541);
617 --color-adaptive-violet-700-300: oklch(81.1% 0.111 293.571);
618 --color-adaptive-white-neutral-950-a70: oklch(14.5% 0 0 / 70%);
619 --color-adaptive-zinc-500-a12-a16: oklch(55.2% 0.016 285.938 / 16%);
620 --color-adaptive-zinc-500-400: oklch(70.5% 0.015 286.067);
621 --color-adaptive-zinc-600-300: oklch(87.1% 0.006 286.286);
622 }
623
624 @variant ocean-light {
625 --color-screen: #f5f7f8;
626 --color-sheet: rgba(245, 247, 248, 0.98);
627 --color-sheet-solid: #f5f7f8;
628 --color-card: #edeff1;
629 --color-card-alt: #f5f7f8;
630 --color-card-translucent: rgba(237, 239, 241, 0.8);
631 --color-foreground: #241523;
632 --color-foreground-secondary: #746c75;
633 --color-foreground-muted: #6f6873;
634 --color-foreground-tertiary: #746c75;
635 --color-border: #cdd4dc;
636 --color-border-subtle: rgba(205, 212, 220, 0.7);
637 --color-separator: rgba(205, 212, 220, 0.55);
638 --color-subtle: #e8eff4;
639 --color-subtle-strong: #e4ecf2;
640 --color-inline-skill-background: #d8e4ee;
641 --color-inline-skill-border: rgba(38, 114, 175, 0.42);
642 --color-inline-skill-foreground: #241523;
643 --color-primary: #2672af;
644 --color-primary-foreground: #fffaff;
645 --color-primary-shadow: #000000;
646 --color-secondary: #e4ecf2;
647 --color-secondary-foreground: #241523;
648 --color-secondary-border: #cdd4dc;
649 --color-switch-active-track: #2672af;
650 --color-switch-active-thumb: #fffaff;
651 --color-switch-inactive-track: #e4ecf2;
652 --color-switch-inactive-thumb: #6f6873;
653 --color-danger: #f5e6e9;
654 --color-danger-border: rgba(251, 44, 54, 0.32);
655 --color-danger-foreground: #c10007;
656 --color-input: #edeff1;
657 --color-input-border: #c0c9d4;
658 --color-sidebar-search: #d5dbe2;
659 --color-placeholder: #716972;
660 --color-icon: #241523;
661 --color-icon-muted: #746c75;
662 --color-icon-subtle: #746c75;
663 --color-header: rgba(245, 247, 248, 0.97);
664 --color-header-border: #d8e4ee;
665 --color-glass-surface: rgba(232, 233, 235, 0.74);
666 --color-glass-tint: rgba(232, 233, 235, 0.22);
667 --color-status-bar: #f5f7f8;
668 --color-md-body: #241523;
669 --color-md-strong: #241523;
670 --color-md-link: #2672af;
671 --color-md-blockquote-border: #cdd4dc;
672 --color-md-blockquote-bg: #e8eff4;
673 --color-md-code-bg: #f0f1f3;
674 --color-md-code-text: #241523;
675 --color-md-user-code-bg: rgba(36, 21, 35, 0.18);
676 --color-md-user-code-text: #241523;
677 --color-md-user-fence-bg: rgba(0, 0, 0, 0.16);
678 --color-md-user-fence-text: #241523;
679 --color-md-hr: #cdd4dc;
680 --color-user-bubble: #d0dfeb;
681 --color-user-bubble-foreground: #241523;
682 --color-user-bubble-foreground-muted: rgba(36, 21, 35, 0.78);
683 --color-user-bubble-skill-foreground: #0a6c72;
684 --color-backdrop: rgba(0, 0, 0, 0.22);
685 --color-drawer: rgba(228, 236, 242, 0.99);
686 --color-drawer-shadow: rgba(0, 0, 0, 0.12);
687 --color-dot-separator: rgba(116, 108, 117, 0.35);
688 --color-wordmark: #241523;
689 --color-chevron: rgba(116, 108, 117, 0.42);
690 --color-adaptive-amber-50-950-a40: oklch(98.7% 0.022 95.277);
691 --color-adaptive-amber-200-900-a60: oklch(92.4% 0.12 95.746);
692 --color-adaptive-amber-500-a12-a16: oklch(76.9% 0.188 70.08 / 12%);
693 --color-adaptive-amber-700-300: oklch(55.5% 0.163 48.998);
694 --color-adaptive-amber-700-400: oklch(55.5% 0.163 48.998);
695 --color-adaptive-amber-800-200: oklch(47.3% 0.137 46.201);
696 --color-adaptive-blue-50-blue-400-a14: oklch(97% 0.014 254.604);
697 --color-adaptive-blue-300-a50-blue-400-a28: oklch(80.9% 0.105 251.813 / 50%);
698 --color-adaptive-blue-500-a20-blue-400-a15: oklch(62.3% 0.214 259.815 / 20%);
699 --color-adaptive-blue-500-400: oklch(62.3% 0.214 259.815);
700 --color-adaptive-blue-600-400: oklch(54.6% 0.245 262.881);
701 --color-adaptive-black-a10-a25: rgb(0 0 0 / 10%);
702 --color-adaptive-black-a15-a35: rgb(0 0 0 / 15%);
703 --color-adaptive-emerald-500-a12-a16: oklch(69.6% 0.17 162.48 / 12%);
704 --color-adaptive-emerald-600-400: oklch(59.6% 0.145 163.225);
705 --color-adaptive-emerald-700-300: oklch(50.8% 0.118 165.612);
706 --color-adaptive-indigo-500-a12-a16: oklch(58.5% 0.233 277.117 / 12%);
707 --color-adaptive-indigo-600-300: oklch(51.1% 0.262 276.966);
708 --color-adaptive-indigo-700-300: oklch(45.7% 0.24 277.023);
709 --color-adaptive-neutral-100-900: oklch(97% 0 0);
710 --color-adaptive-neutral-200-700-a60: oklch(92.2% 0 0);
711 --color-adaptive-neutral-200-800: oklch(92.2% 0 0);
712 --color-adaptive-neutral-200-a70-white-a8: oklch(92.2% 0 0 / 70%);
713 --color-adaptive-neutral-200-white-a6: oklch(92.2% 0 0);
714 --color-adaptive-neutral-200-white-a8: oklch(92.2% 0 0);
715 --color-adaptive-neutral-200-a80-white-a8: oklch(92.2% 0 0 / 80%);
716 --color-adaptive-neutral-300-a60-white-a12: oklch(87% 0 0 / 60%);
717 --color-adaptive-neutral-400-500: oklch(70.8% 0 0);
718 --color-adaptive-neutral-400-a60-500-a60: oklch(70.8% 0 0 / 60%);
719 --color-adaptive-neutral-400-a80-500-a80: oklch(70.8% 0 0 / 80%);
720 --color-adaptive-neutral-500-a10-a16: oklch(55.6% 0 0 / 10%);
721 --color-adaptive-neutral-500-400: oklch(55.6% 0 0);
722 --color-adaptive-neutral-500-500: oklch(55.6% 0 0);
723 --color-adaptive-neutral-600-300: oklch(43.9% 0 0);
724 --color-adaptive-neutral-600-400: oklch(43.9% 0 0);
725 --color-adaptive-neutral-950-50: oklch(14.5% 0 0);
726 --color-adaptive-red-50-950-a80: oklch(97.1% 0.013 17.38);
727 --color-adaptive-red-200-800: oklch(88.5% 0.062 18.334);
728 --color-adaptive-red-600-a80-400-a80: oklch(57.7% 0.245 27.325 / 80%);
729 --color-adaptive-red-700-300: oklch(50.5% 0.213 27.518);
730 --color-adaptive-rose-100-500-a18: oklch(94.1% 0.03 12.58);
731 --color-adaptive-rose-100-a80-500-a12: oklch(94.1% 0.03 12.58 / 80%);
732 --color-adaptive-rose-300-a70-400-a28: oklch(81% 0.117 11.638 / 70%);
733 --color-adaptive-rose-500-a12-a16: oklch(64.5% 0.246 16.439 / 12%);
734 --color-adaptive-rose-500-400: oklch(64.5% 0.246 16.439);
735 --color-adaptive-rose-600-400: oklch(58.6% 0.253 17.585);
736 --color-adaptive-rose-700-300: oklch(51.4% 0.222 16.935);
737 --color-adaptive-sky-500-a12-a16: oklch(68.5% 0.169 237.323 / 12%);
738 --color-adaptive-sky-600-400: oklch(58.8% 0.158 241.966);
739 --color-adaptive-sky-700-300: oklch(50% 0.134 242.749);
740 --color-adaptive-violet-500-a12-a16: oklch(60.6% 0.25 292.717 / 12%);
741 --color-adaptive-violet-600-400: oklch(54.1% 0.281 293.009);
742 --color-adaptive-violet-700-300: oklch(49.1% 0.27 292.581);
743 --color-adaptive-white-neutral-950-a70: #fff;
744 --color-adaptive-zinc-500-a12-a16: oklch(55.2% 0.016 285.938 / 12%);
745 --color-adaptive-zinc-500-400: oklch(55.2% 0.016 285.938);
746 --color-adaptive-zinc-600-300: oklch(44.2% 0.017 285.786);
747 }
748
749 @variant ocean-dark {
750 --color-screen: #17212b;
751 --color-sheet: rgba(23, 33, 43, 0.98);
752 --color-sheet-solid: #17212b;
753 --color-card: #333b45;
754 --color-card-alt: #17212b;
755 --color-card-translucent: rgba(51, 59, 69, 0.8);
756 --color-foreground: #fffaff;
757 --color-foreground-secondary: #8d8f97;
758 --color-foreground-muted: #969ca6;
759 --color-foreground-tertiary: #8d8f97;
760 --color-border: #405567;
761 --color-border-subtle: rgba(64, 85, 103, 0.7);
762 --color-separator: rgba(64, 85, 103, 0.55);
763 --color-subtle: #233544;
764 --color-subtle-strong: #293f52;
765 --color-inline-skill-background: #324e66;
766 --color-inline-skill-border: rgba(112, 185, 238, 0.42);
767 --color-inline-skill-foreground: #fffaff;
768 --color-primary: #70b9ee;
769 --color-primary-foreground: #241523;
770 --color-primary-shadow: #000000;
771 --color-secondary: #293f52;
772 --color-secondary-foreground: #fffaff;
773 --color-secondary-border: #405567;
774 --color-switch-active-track: #70b9ee;
775 --color-switch-active-thumb: #241523;
776 --color-switch-inactive-track: #293f52;
777 --color-switch-inactive-thumb: #969ca6;
778 --color-danger: #3c2630;
779 --color-danger-border: rgba(251, 65, 74, 0.32);
780 --color-danger-foreground: #ff6467;
781 --color-input: #333b45;
782 --color-input-border: #4f677b;
783 --color-sidebar-search: #424e5a;
784 --color-placeholder: #a4a4ac;
785 --color-icon: #fffaff;
786 --color-icon-muted: #8d8f97;
787 --color-icon-subtle: #8d8f97;
788 --color-header: rgba(23, 33, 43, 0.97);
789 --color-header-border: #36566f;
790 --color-glass-surface: rgba(65, 72, 81, 0.74);
791 --color-glass-tint: rgba(65, 72, 81, 0.22);
792 --color-status-bar: #17212b;
793 --color-md-body: #fffaff;
794 --color-md-strong: #fffaff;
795 --color-md-link: #70b9ee;
796 --color-md-blockquote-border: #405567;
797 --color-md-blockquote-bg: #233544;
798 --color-md-code-bg: #252e38;
799 --color-md-code-text: #fffaff;
800 --color-md-user-code-bg: rgba(255, 250, 255, 0.18);
801 --color-md-user-code-text: #fffaff;
802 --color-md-user-fence-bg: rgba(0, 0, 0, 0.28);
803 --color-md-user-fence-text: #fffaff;
804 --color-md-hr: #405567;
805 --color-user-bubble: #375871;
806 --color-user-bubble-foreground: #fffaff;
807 --color-user-bubble-foreground-muted: rgba(255, 250, 255, 0.78);
808 --color-user-bubble-skill-foreground: #75d8dd;
809 --color-backdrop: rgba(0, 0, 0, 0.48);
810 --color-drawer: rgba(30, 45, 59, 0.99);
811 --color-drawer-shadow: rgba(0, 0, 0, 0.32);
812 --color-dot-separator: rgba(141, 143, 151, 0.35);
813 --color-wordmark: #fffaff;
814 --color-chevron: rgba(141, 143, 151, 0.42);
815 --color-adaptive-amber-50-950-a40: oklch(27.9% 0.077 45.635 / 40%);
816 --color-adaptive-amber-200-900-a60: oklch(41.4% 0.112 45.904 / 60%);
817 --color-adaptive-amber-500-a12-a16: oklch(76.9% 0.188 70.08 / 16%);
818 --color-adaptive-amber-700-300: oklch(87.9% 0.169 91.605);
819 --color-adaptive-amber-700-400: oklch(82.8% 0.189 84.429);
820 --color-adaptive-amber-800-200: oklch(92.4% 0.12 95.746);
821 --color-adaptive-blue-50-blue-400-a14: oklch(70.7% 0.165 254.624 / 14%);
822 --color-adaptive-blue-300-a50-blue-400-a28: oklch(70.7% 0.165 254.624 / 28%);
823 --color-adaptive-blue-500-a20-blue-400-a15: oklch(70.7% 0.165 254.624 / 15%);
824 --color-adaptive-blue-500-400: oklch(70.7% 0.165 254.624);
825 --color-adaptive-blue-600-400: oklch(70.7% 0.165 254.624);
826 --color-adaptive-black-a10-a25: rgb(0 0 0 / 25%);
827 --color-adaptive-black-a15-a35: rgb(0 0 0 / 35%);
828 --color-adaptive-emerald-500-a12-a16: oklch(69.6% 0.17 162.48 / 16%);
829 --color-adaptive-emerald-600-400: oklch(76.5% 0.177 163.223);
830 --color-adaptive-emerald-700-300: oklch(84.5% 0.143 164.978);
831 --color-adaptive-indigo-500-a12-a16: oklch(58.5% 0.233 277.117 / 16%);
832 --color-adaptive-indigo-600-300: oklch(78.5% 0.115 274.713);
833 --color-adaptive-indigo-700-300: oklch(78.5% 0.115 274.713);
834 --color-adaptive-neutral-100-900: oklch(20.5% 0 0);
835 --color-adaptive-neutral-200-700-a60: oklch(37.1% 0 0 / 60%);
836 --color-adaptive-neutral-200-800: oklch(26.9% 0 0);
837 --color-adaptive-neutral-200-a70-white-a8: rgb(255 255 255 / 8%);
838 --color-adaptive-neutral-200-white-a6: rgb(255 255 255 / 6%);
839 --color-adaptive-neutral-200-white-a8: rgb(255 255 255 / 8%);
840 --color-adaptive-neutral-200-a80-white-a8: rgb(255 255 255 / 8%);
841 --color-adaptive-neutral-300-a60-white-a12: rgb(255 255 255 / 12%);
842 --color-adaptive-neutral-400-500: oklch(55.6% 0 0);
843 --color-adaptive-neutral-400-a60-500-a60: oklch(55.6% 0 0 / 60%);
844 --color-adaptive-neutral-400-a80-500-a80: oklch(55.6% 0 0 / 80%);
845 --color-adaptive-neutral-500-a10-a16: oklch(55.6% 0 0 / 16%);
846 --color-adaptive-neutral-500-400: oklch(70.8% 0 0);
847 --color-adaptive-neutral-500-500: oklch(55.6% 0 0);
848 --color-adaptive-neutral-600-300: oklch(87% 0 0);
849 --color-adaptive-neutral-600-400: oklch(70.8% 0 0);
850 --color-adaptive-neutral-950-50: oklch(98.5% 0 0);
851 --color-adaptive-red-50-950-a80: oklch(25.8% 0.092 26.042 / 80%);
852 --color-adaptive-red-200-800: oklch(44.4% 0.177 26.899);
853 --color-adaptive-red-600-a80-400-a80: oklch(70.4% 0.191 22.216 / 80%);
854 --color-adaptive-red-700-300: oklch(80.8% 0.114 19.571);
855 --color-adaptive-rose-100-500-a18: oklch(64.5% 0.246 16.439 / 18%);
856 --color-adaptive-rose-100-a80-500-a12: oklch(64.5% 0.246 16.439 / 12%);
857 --color-adaptive-rose-300-a70-400-a28: oklch(71.2% 0.194 13.428 / 28%);
858 --color-adaptive-rose-500-a12-a16: oklch(64.5% 0.246 16.439 / 16%);
859 --color-adaptive-rose-500-400: oklch(71.2% 0.194 13.428);
860 --color-adaptive-rose-600-400: oklch(71.2% 0.194 13.428);
861 --color-adaptive-rose-700-300: oklch(81% 0.117 11.638);
862 --color-adaptive-sky-500-a12-a16: oklch(68.5% 0.169 237.323 / 16%);
863 --color-adaptive-sky-600-400: oklch(74.6% 0.16 232.661);
864 --color-adaptive-sky-700-300: oklch(82.8% 0.111 230.318);
865 --color-adaptive-violet-500-a12-a16: oklch(60.6% 0.25 292.717 / 16%);
866 --color-adaptive-violet-600-400: oklch(70.2% 0.183 293.541);
867 --color-adaptive-violet-700-300: oklch(81.1% 0.111 293.571);
868 --color-adaptive-white-neutral-950-a70: oklch(14.5% 0 0 / 70%);
869 --color-adaptive-zinc-500-a12-a16: oklch(55.2% 0.016 285.938 / 16%);
870 --color-adaptive-zinc-500-400: oklch(70.5% 0.015 286.067);
871 --color-adaptive-zinc-600-300: oklch(87.1% 0.006 286.286);
872 }
873
874 @variant ember-light {
875 --color-screen: #f9f7f5;
876 --color-sheet: rgba(249, 247, 245, 0.98);
877 --color-sheet-solid: #f9f7f5;
878 --color-card: #f1efee;
879 --color-card-alt: #f9f7f5;
880 --color-card-translucent: rgba(241, 239, 238, 0.8);
881 --color-foreground: #241523;
882 --color-foreground-secondary: #766c74;
883 --color-foreground-muted: #74686f;
884 --color-foreground-tertiary: #766c74;
885 --color-border: #ddd2ce;
886 --color-border-subtle: rgba(221, 210, 206, 0.7);
887 --color-separator: rgba(221, 210, 206, 0.55);
888 --color-subtle: #f4ede9;
889 --color-subtle-strong: #f3eae5;
890 --color-inline-skill-background: #eee0d9;
891 --color-inline-skill-border: rgba(174, 85, 42, 0.42);
892 --color-inline-skill-foreground: #241523;
893 --color-primary: #ae552a;
894 --color-primary-foreground: #fffaff;
895 --color-primary-shadow: #000000;
896 --color-secondary: #f3eae5;
897 --color-secondary-foreground: #241523;
898 --color-secondary-border: #ddd2ce;
899 --color-switch-active-track: #ae552a;
900 --color-switch-active-thumb: #fffaff;
901 --color-switch-inactive-track: #f3eae5;
902 --color-switch-inactive-thumb: #74686f;
903 --color-danger: #f9e7e6;
904 --color-danger-border: rgba(251, 44, 54, 0.32);
905 --color-danger-foreground: #c10007;
906 --color-input: #f1efee;
907 --color-input-border: #d4c6c1;
908 --color-sidebar-search: #e2d9d6;
909 --color-placeholder: #736971;
910 --color-icon: #241523;
911 --color-icon-muted: #766c74;
912 --color-icon-subtle: #766c74;
913 --color-header: rgba(249, 247, 245, 0.97);
914 --color-header-border: #eee0d9;
915 --color-glass-surface: rgba(236, 233, 233, 0.74);
916 --color-glass-tint: rgba(236, 233, 233, 0.22);
917 --color-status-bar: #f9f7f5;
918 --color-md-body: #241523;
919 --color-md-strong: #241523;
920 --color-md-link: #ae552a;
921 --color-md-blockquote-border: #ddd2ce;
922 --color-md-blockquote-bg: #f4ede9;
923 --color-md-code-bg: #f3f1f0;
924 --color-md-code-text: #241523;
925 --color-md-user-code-bg: rgba(36, 21, 35, 0.18);
926 --color-md-user-code-text: #241523;
927 --color-md-user-fence-bg: rgba(0, 0, 0, 0.16);
928 --color-md-user-fence-text: #241523;
929 --color-md-hr: #ddd2ce;
930 --color-user-bubble: #ebdad1;
931 --color-user-bubble-foreground: #241523;
932 --color-user-bubble-foreground-muted: rgba(36, 21, 35, 0.78);
933 --color-user-bubble-skill-foreground: #b13535;
934 --color-backdrop: rgba(0, 0, 0, 0.22);
935 --color-drawer: rgba(243, 234, 229, 0.99);
936 --color-drawer-shadow: rgba(0, 0, 0, 0.12);
937 --color-dot-separator: rgba(118, 108, 116, 0.35);
938 --color-wordmark: #241523;
939 --color-chevron: rgba(118, 108, 116, 0.42);
940 --color-adaptive-amber-50-950-a40: oklch(98.7% 0.022 95.277);
941 --color-adaptive-amber-200-900-a60: oklch(92.4% 0.12 95.746);
942 --color-adaptive-amber-500-a12-a16: oklch(76.9% 0.188 70.08 / 12%);
943 --color-adaptive-amber-700-300: oklch(55.5% 0.163 48.998);
944 --color-adaptive-amber-700-400: oklch(55.5% 0.163 48.998);
945 --color-adaptive-amber-800-200: oklch(47.3% 0.137 46.201);
946 --color-adaptive-blue-50-blue-400-a14: oklch(97% 0.014 254.604);
947 --color-adaptive-blue-300-a50-blue-400-a28: oklch(80.9% 0.105 251.813 / 50%);
948 --color-adaptive-blue-500-a20-blue-400-a15: oklch(62.3% 0.214 259.815 / 20%);
949 --color-adaptive-blue-500-400: oklch(62.3% 0.214 259.815);
950 --color-adaptive-blue-600-400: oklch(54.6% 0.245 262.881);
951 --color-adaptive-black-a10-a25: rgb(0 0 0 / 10%);
952 --color-adaptive-black-a15-a35: rgb(0 0 0 / 15%);
953 --color-adaptive-emerald-500-a12-a16: oklch(69.6% 0.17 162.48 / 12%);
954 --color-adaptive-emerald-600-400: oklch(59.6% 0.145 163.225);
955 --color-adaptive-emerald-700-300: oklch(50.8% 0.118 165.612);
956 --color-adaptive-indigo-500-a12-a16: oklch(58.5% 0.233 277.117 / 12%);
957 --color-adaptive-indigo-600-300: oklch(51.1% 0.262 276.966);
958 --color-adaptive-indigo-700-300: oklch(45.7% 0.24 277.023);
959 --color-adaptive-neutral-100-900: oklch(97% 0 0);
960 --color-adaptive-neutral-200-700-a60: oklch(92.2% 0 0);
961 --color-adaptive-neutral-200-800: oklch(92.2% 0 0);
962 --color-adaptive-neutral-200-a70-white-a8: oklch(92.2% 0 0 / 70%);
963 --color-adaptive-neutral-200-white-a6: oklch(92.2% 0 0);
964 --color-adaptive-neutral-200-white-a8: oklch(92.2% 0 0);
965 --color-adaptive-neutral-200-a80-white-a8: oklch(92.2% 0 0 / 80%);
966 --color-adaptive-neutral-300-a60-white-a12: oklch(87% 0 0 / 60%);
967 --color-adaptive-neutral-400-500: oklch(70.8% 0 0);
968 --color-adaptive-neutral-400-a60-500-a60: oklch(70.8% 0 0 / 60%);
969 --color-adaptive-neutral-400-a80-500-a80: oklch(70.8% 0 0 / 80%);
970 --color-adaptive-neutral-500-a10-a16: oklch(55.6% 0 0 / 10%);
971 --color-adaptive-neutral-500-400: oklch(55.6% 0 0);
972 --color-adaptive-neutral-500-500: oklch(55.6% 0 0);
973 --color-adaptive-neutral-600-300: oklch(43.9% 0 0);
974 --color-adaptive-neutral-600-400: oklch(43.9% 0 0);
975 --color-adaptive-neutral-950-50: oklch(14.5% 0 0);
976 --color-adaptive-red-50-950-a80: oklch(97.1% 0.013 17.38);
977 --color-adaptive-red-200-800: oklch(88.5% 0.062 18.334);
978 --color-adaptive-red-600-a80-400-a80: oklch(57.7% 0.245 27.325 / 80%);
979 --color-adaptive-red-700-300: oklch(50.5% 0.213 27.518);
980 --color-adaptive-rose-100-500-a18: oklch(94.1% 0.03 12.58);
981 --color-adaptive-rose-100-a80-500-a12: oklch(94.1% 0.03 12.58 / 80%);
982 --color-adaptive-rose-300-a70-400-a28: oklch(81% 0.117 11.638 / 70%);
983 --color-adaptive-rose-500-a12-a16: oklch(64.5% 0.246 16.439 / 12%);
984 --color-adaptive-rose-500-400: oklch(64.5% 0.246 16.439);
985 --color-adaptive-rose-600-400: oklch(58.6% 0.253 17.585);
986 --color-adaptive-rose-700-300: oklch(51.4% 0.222 16.935);
987 --color-adaptive-sky-500-a12-a16: oklch(68.5% 0.169 237.323 / 12%);
988 --color-adaptive-sky-600-400: oklch(58.8% 0.158 241.966);
989 --color-adaptive-sky-700-300: oklch(50% 0.134 242.749);
990 --color-adaptive-violet-500-a12-a16: oklch(60.6% 0.25 292.717 / 12%);
991 --color-adaptive-violet-600-400: oklch(54.1% 0.281 293.009);
992 --color-adaptive-violet-700-300: oklch(49.1% 0.27 292.581);
993 --color-adaptive-white-neutral-950-a70: #fff;
994 --color-adaptive-zinc-500-a12-a16: oklch(55.2% 0.016 285.938 / 12%);
995 --color-adaptive-zinc-500-400: oklch(55.2% 0.016 285.938);
996 --color-adaptive-zinc-600-300: oklch(44.2% 0.017 285.786);
997 }
998
999 @variant ember-dark {
1000 --color-screen: #291e1a;
1001 --color-sheet: rgba(41, 30, 26, 0.98);
1002 --color-sheet-solid: #291e1a;
1003 --color-card: #433835;
1004 --color-card-alt: #291e1a;
1005 --color-card-translucent: rgba(67, 56, 53, 0.8);
1006 --color-foreground: #fffaff;
1007 --color-foreground-secondary: #968e8f;
1008 --color-foreground-muted: #a59996;
1009 --color-foreground-tertiary: #968e8f;
1010 --color-border: #664c3f;
1011 --color-border-subtle: rgba(102, 76, 63, 0.7);
1012 --color-separator: rgba(102, 76, 63, 0.55);
1013 --color-subtle: #432e23;
1014 --color-subtle-strong: #513728;
1015 --color-inline-skill-background: #644330;
1016 --color-inline-skill-border: rgba(240, 154, 100, 0.42);
1017 --color-inline-skill-foreground: #fffaff;
1018 --color-primary: #f09a64;
1019 --color-primary-foreground: #241523;
1020 --color-primary-shadow: #000000;
1021 --color-secondary: #513728;
1022 --color-secondary-foreground: #fffaff;
1023 --color-secondary-border: #664c3f;
1024 --color-switch-active-track: #f09a64;
1025 --color-switch-active-thumb: #241523;
1026 --color-switch-inactive-track: #513728;
1027 --color-switch-inactive-thumb: #a59996;
1028 --color-danger: #4a2321;
1029 --color-danger-border: rgba(251, 65, 74, 0.32);
1030 --color-danger-foreground: #ff6467;
1031 --color-input: #433835;
1032 --color-input-border: #7a5d4d;
1033 --color-sidebar-search: #584943;
1034 --color-placeholder: #aba3a5;
1035 --color-icon: #fffaff;
1036 --color-icon-muted: #968e8f;
1037 --color-icon-subtle: #968e8f;
1038 --color-header: rgba(41, 30, 26, 0.97);
1039 --color-header-border: #6e4934;
1040 --color-glass-surface: rgba(79, 69, 67, 0.74);
1041 --color-glass-tint: rgba(79, 69, 67, 0.22);
1042 --color-status-bar: #291e1a;
1043 --color-md-body: #fffaff;
1044 --color-md-strong: #fffaff;
1045 --color-md-link: #f09a64;
1046 --color-md-blockquote-border: #664c3f;
1047 --color-md-blockquote-bg: #432e23;
1048 --color-md-code-bg: #362b27;
1049 --color-md-code-text: #fffaff;
1050 --color-md-user-code-bg: rgba(255, 250, 255, 0.18);
1051 --color-md-user-code-text: #fffaff;
1052 --color-md-user-fence-bg: rgba(0, 0, 0, 0.28);
1053 --color-md-user-fence-text: #fffaff;
1054 --color-md-hr: #664c3f;
1055 --color-user-bubble: #704b34;
1056 --color-user-bubble-foreground: #fffaff;
1057 --color-user-bubble-foreground-muted: rgba(255, 250, 255, 0.78);
1058 --color-user-bubble-skill-foreground: #fab6ad;
1059 --color-backdrop: rgba(0, 0, 0, 0.48);
1060 --color-drawer: rgba(57, 40, 31, 0.99);
1061 --color-drawer-shadow: rgba(0, 0, 0, 0.32);
1062 --color-dot-separator: rgba(150, 142, 143, 0.35);
1063 --color-wordmark: #fffaff;
1064 --color-chevron: rgba(150, 142, 143, 0.42);
1065 --color-adaptive-amber-50-950-a40: oklch(27.9% 0.077 45.635 / 40%);
1066 --color-adaptive-amber-200-900-a60: oklch(41.4% 0.112 45.904 / 60%);
1067 --color-adaptive-amber-500-a12-a16: oklch(76.9% 0.188 70.08 / 16%);
1068 --color-adaptive-amber-700-300: oklch(87.9% 0.169 91.605);
1069 --color-adaptive-amber-700-400: oklch(82.8% 0.189 84.429);
1070 --color-adaptive-amber-800-200: oklch(92.4% 0.12 95.746);
1071 --color-adaptive-blue-50-blue-400-a14: oklch(70.7% 0.165 254.624 / 14%);
1072 --color-adaptive-blue-300-a50-blue-400-a28: oklch(70.7% 0.165 254.624 / 28%);
1073 --color-adaptive-blue-500-a20-blue-400-a15: oklch(70.7% 0.165 254.624 / 15%);
1074 --color-adaptive-blue-500-400: oklch(70.7% 0.165 254.624);
1075 --color-adaptive-blue-600-400: oklch(70.7% 0.165 254.624);
1076 --color-adaptive-black-a10-a25: rgb(0 0 0 / 25%);
1077 --color-adaptive-black-a15-a35: rgb(0 0 0 / 35%);
1078 --color-adaptive-emerald-500-a12-a16: oklch(69.6% 0.17 162.48 / 16%);
1079 --color-adaptive-emerald-600-400: oklch(76.5% 0.177 163.223);
1080 --color-adaptive-emerald-700-300: oklch(84.5% 0.143 164.978);
1081 --color-adaptive-indigo-500-a12-a16: oklch(58.5% 0.233 277.117 / 16%);
1082 --color-adaptive-indigo-600-300: oklch(78.5% 0.115 274.713);
1083 --color-adaptive-indigo-700-300: oklch(78.5% 0.115 274.713);
1084 --color-adaptive-neutral-100-900: oklch(20.5% 0 0);
1085 --color-adaptive-neutral-200-700-a60: oklch(37.1% 0 0 / 60%);
1086 --color-adaptive-neutral-200-800: oklch(26.9% 0 0);
1087 --color-adaptive-neutral-200-a70-white-a8: rgb(255 255 255 / 8%);
1088 --color-adaptive-neutral-200-white-a6: rgb(255 255 255 / 6%);
1089 --color-adaptive-neutral-200-white-a8: rgb(255 255 255 / 8%);
1090 --color-adaptive-neutral-200-a80-white-a8: rgb(255 255 255 / 8%);
1091 --color-adaptive-neutral-300-a60-white-a12: rgb(255 255 255 / 12%);
1092 --color-adaptive-neutral-400-500: oklch(55.6% 0 0);
1093 --color-adaptive-neutral-400-a60-500-a60: oklch(55.6% 0 0 / 60%);
1094 --color-adaptive-neutral-400-a80-500-a80: oklch(55.6% 0 0 / 80%);
1095 --color-adaptive-neutral-500-a10-a16: oklch(55.6% 0 0 / 16%);
1096 --color-adaptive-neutral-500-400: oklch(70.8% 0 0);
1097 --color-adaptive-neutral-500-500: oklch(55.6% 0 0);
1098 --color-adaptive-neutral-600-300: oklch(87% 0 0);
1099 --color-adaptive-neutral-600-400: oklch(70.8% 0 0);
1100 --color-adaptive-neutral-950-50: oklch(98.5% 0 0);
1101 --color-adaptive-red-50-950-a80: oklch(25.8% 0.092 26.042 / 80%);
1102 --color-adaptive-red-200-800: oklch(44.4% 0.177 26.899);
1103 --color-adaptive-red-600-a80-400-a80: oklch(70.4% 0.191 22.216 / 80%);
1104 --color-adaptive-red-700-300: oklch(80.8% 0.114 19.571);
1105 --color-adaptive-rose-100-500-a18: oklch(64.5% 0.246 16.439 / 18%);
1106 --color-adaptive-rose-100-a80-500-a12: oklch(64.5% 0.246 16.439 / 12%);
1107 --color-adaptive-rose-300-a70-400-a28: oklch(71.2% 0.194 13.428 / 28%);
1108 --color-adaptive-rose-500-a12-a16: oklch(64.5% 0.246 16.439 / 16%);
1109 --color-adaptive-rose-500-400: oklch(71.2% 0.194 13.428);
1110 --color-adaptive-rose-600-400: oklch(71.2% 0.194 13.428);
1111 --color-adaptive-rose-700-300: oklch(81% 0.117 11.638);
1112 --color-adaptive-sky-500-a12-a16: oklch(68.5% 0.169 237.323 / 16%);
1113 --color-adaptive-sky-600-400: oklch(74.6% 0.16 232.661);
1114 --color-adaptive-sky-700-300: oklch(82.8% 0.111 230.318);
1115 --color-adaptive-violet-500-a12-a16: oklch(60.6% 0.25 292.717 / 16%);
1116 --color-adaptive-violet-600-400: oklch(70.2% 0.183 293.541);
1117 --color-adaptive-violet-700-300: oklch(81.1% 0.111 293.571);
1118 --color-adaptive-white-neutral-950-a70: oklch(14.5% 0 0 / 70%);
1119 --color-adaptive-zinc-500-a12-a16: oklch(55.2% 0.016 285.938 / 16%);
1120 --color-adaptive-zinc-500-400: oklch(70.5% 0.015 286.067);
1121 --color-adaptive-zinc-600-300: oklch(87.1% 0.006 286.286);
1122 }
1123
1124 @variant iris-light {
1125 --color-screen: #f8f7f9;
1126 --color-sheet: rgba(248, 247, 249, 0.98);
1127 --color-sheet-solid: #f8f7f9;
1128 --color-card: #f0eff2;
1129 --color-card-alt: #f8f7f9;
1130 --color-card-translucent: rgba(240, 239, 242, 0.8);
1131 --color-foreground: #241523;
1132 --color-foreground-secondary: #766c76;
1133 --color-foreground-muted: #726874;
1134 --color-foreground-tertiary: #766c76;
1135 --color-border: #d6d1de;
1136 --color-border-subtle: rgba(214, 209, 222, 0.7);
1137 --color-separator: rgba(214, 209, 222, 0.55);
1138 --color-subtle: #f0edf6;
1139 --color-subtle-strong: #edeaf4;
1140 --color-inline-skill-background: #e5e0f0;
1141 --color-inline-skill-border: rgba(114, 83, 185, 0.42);
1142 --color-inline-skill-foreground: #241523;
1143 --color-primary: #7253b9;
1144 --color-primary-foreground: #fffaff;
1145 --color-primary-shadow: #000000;
1146 --color-secondary: #edeaf4;
1147 --color-secondary-foreground: #241523;
1148 --color-secondary-border: #d6d1de;
1149 --color-switch-active-track: #7253b9;
1150 --color-switch-active-thumb: #fffaff;
1151 --color-switch-inactive-track: #edeaf4;
1152 --color-switch-inactive-thumb: #726874;
1153 --color-danger: #f8e6ea;
1154 --color-danger-border: rgba(251, 44, 54, 0.32);
1155 --color-danger-foreground: #c10007;
1156 --color-input: #f0eff2;
1157 --color-input-border: #ccc5d6;
1158 --color-sidebar-search: #ddd9e3;
1159 --color-placeholder: #736973;
1160 --color-icon: #241523;
1161 --color-icon-muted: #766c76;
1162 --color-icon-subtle: #766c76;
1163 --color-header: rgba(248, 247, 249, 0.97);
1164 --color-header-border: #e5e0f0;
1165 --color-glass-surface: rgba(235, 233, 237, 0.74);
1166 --color-glass-tint: rgba(235, 233, 237, 0.22);
1167 --color-status-bar: #f8f7f9;
1168 --color-md-body: #241523;
1169 --color-md-strong: #241523;
1170 --color-md-link: #7253b9;
1171 --color-md-blockquote-border: #d6d1de;
1172 --color-md-blockquote-bg: #f0edf6;
1173 --color-md-code-bg: #f2f1f4;
1174 --color-md-code-text: #241523;
1175 --color-md-user-code-bg: rgba(36, 21, 35, 0.18);
1176 --color-md-user-code-text: #241523;
1177 --color-md-user-fence-bg: rgba(0, 0, 0, 0.16);
1178 --color-md-user-fence-text: #241523;
1179 --color-md-hr: #d6d1de;
1180 --color-user-bubble: #e0d9ee;
1181 --color-user-bubble-foreground: #241523;
1182 --color-user-bubble-foreground-muted: rgba(36, 21, 35, 0.78);
1183 --color-user-bubble-skill-foreground: #a82c87;
1184 --color-backdrop: rgba(0, 0, 0, 0.22);
1185 --color-drawer: rgba(237, 234, 244, 0.99);
1186 --color-drawer-shadow: rgba(0, 0, 0, 0.12);
1187 --color-dot-separator: rgba(118, 108, 118, 0.35);
1188 --color-wordmark: #241523;
1189 --color-chevron: rgba(118, 108, 118, 0.42);
1190 --color-adaptive-amber-50-950-a40: oklch(98.7% 0.022 95.277);
1191 --color-adaptive-amber-200-900-a60: oklch(92.4% 0.12 95.746);
1192 --color-adaptive-amber-500-a12-a16: oklch(76.9% 0.188 70.08 / 12%);
1193 --color-adaptive-amber-700-300: oklch(55.5% 0.163 48.998);
1194 --color-adaptive-amber-700-400: oklch(55.5% 0.163 48.998);
1195 --color-adaptive-amber-800-200: oklch(47.3% 0.137 46.201);
1196 --color-adaptive-blue-50-blue-400-a14: oklch(97% 0.014 254.604);
1197 --color-adaptive-blue-300-a50-blue-400-a28: oklch(80.9% 0.105 251.813 / 50%);
1198 --color-adaptive-blue-500-a20-blue-400-a15: oklch(62.3% 0.214 259.815 / 20%);
1199 --color-adaptive-blue-500-400: oklch(62.3% 0.214 259.815);
1200 --color-adaptive-blue-600-400: oklch(54.6% 0.245 262.881);
1201 --color-adaptive-black-a10-a25: rgb(0 0 0 / 10%);
1202 --color-adaptive-black-a15-a35: rgb(0 0 0 / 15%);
1203 --color-adaptive-emerald-500-a12-a16: oklch(69.6% 0.17 162.48 / 12%);
1204 --color-adaptive-emerald-600-400: oklch(59.6% 0.145 163.225);
1205 --color-adaptive-emerald-700-300: oklch(50.8% 0.118 165.612);
1206 --color-adaptive-indigo-500-a12-a16: oklch(58.5% 0.233 277.117 / 12%);
1207 --color-adaptive-indigo-600-300: oklch(51.1% 0.262 276.966);
1208 --color-adaptive-indigo-700-300: oklch(45.7% 0.24 277.023);
1209 --color-adaptive-neutral-100-900: oklch(97% 0 0);
1210 --color-adaptive-neutral-200-700-a60: oklch(92.2% 0 0);
1211 --color-adaptive-neutral-200-800: oklch(92.2% 0 0);
1212 --color-adaptive-neutral-200-a70-white-a8: oklch(92.2% 0 0 / 70%);
1213 --color-adaptive-neutral-200-white-a6: oklch(92.2% 0 0);
1214 --color-adaptive-neutral-200-white-a8: oklch(92.2% 0 0);
1215 --color-adaptive-neutral-200-a80-white-a8: oklch(92.2% 0 0 / 80%);
1216 --color-adaptive-neutral-300-a60-white-a12: oklch(87% 0 0 / 60%);
1217 --color-adaptive-neutral-400-500: oklch(70.8% 0 0);
1218 --color-adaptive-neutral-400-a60-500-a60: oklch(70.8% 0 0 / 60%);
1219 --color-adaptive-neutral-400-a80-500-a80: oklch(70.8% 0 0 / 80%);
1220 --color-adaptive-neutral-500-a10-a16: oklch(55.6% 0 0 / 10%);
1221 --color-adaptive-neutral-500-400: oklch(55.6% 0 0);
1222 --color-adaptive-neutral-500-500: oklch(55.6% 0 0);
1223 --color-adaptive-neutral-600-300: oklch(43.9% 0 0);
1224 --color-adaptive-neutral-600-400: oklch(43.9% 0 0);
1225 --color-adaptive-neutral-950-50: oklch(14.5% 0 0);
1226 --color-adaptive-red-50-950-a80: oklch(97.1% 0.013 17.38);
1227 --color-adaptive-red-200-800: oklch(88.5% 0.062 18.334);
1228 --color-adaptive-red-600-a80-400-a80: oklch(57.7% 0.245 27.325 / 80%);
1229 --color-adaptive-red-700-300: oklch(50.5% 0.213 27.518);
1230 --color-adaptive-rose-100-500-a18: oklch(94.1% 0.03 12.58);
1231 --color-adaptive-rose-100-a80-500-a12: oklch(94.1% 0.03 12.58 / 80%);
1232 --color-adaptive-rose-300-a70-400-a28: oklch(81% 0.117 11.638 / 70%);
1233 --color-adaptive-rose-500-a12-a16: oklch(64.5% 0.246 16.439 / 12%);
1234 --color-adaptive-rose-500-400: oklch(64.5% 0.246 16.439);
1235 --color-adaptive-rose-600-400: oklch(58.6% 0.253 17.585);
1236 --color-adaptive-rose-700-300: oklch(51.4% 0.222 16.935);
1237 --color-adaptive-sky-500-a12-a16: oklch(68.5% 0.169 237.323 / 12%);
1238 --color-adaptive-sky-600-400: oklch(58.8% 0.158 241.966);
1239 --color-adaptive-sky-700-300: oklch(50% 0.134 242.749);
1240 --color-adaptive-violet-500-a12-a16: oklch(60.6% 0.25 292.717 / 12%);
1241 --color-adaptive-violet-600-400: oklch(54.1% 0.281 293.009);
1242 --color-adaptive-violet-700-300: oklch(49.1% 0.27 292.581);
1243 --color-adaptive-white-neutral-950-a70: #fff;
1244 --color-adaptive-zinc-500-a12-a16: oklch(55.2% 0.016 285.938 / 12%);
1245 --color-adaptive-zinc-500-400: oklch(55.2% 0.016 285.938);
1246 --color-adaptive-zinc-600-300: oklch(44.2% 0.017 285.786);
1247 }
1248
1249 @variant iris-dark {
1250 --color-screen: #1d1929;
1251 --color-sheet: rgba(29, 25, 41, 0.98);
1252 --color-sheet-solid: #1d1929;
1253 --color-card: #383443;
1254 --color-card-alt: #1d1929;
1255 --color-card-translucent: rgba(56, 52, 67, 0.8);
1256 --color-foreground: #fffaff;
1257 --color-foreground-secondary: #8e8a95;
1258 --color-foreground-muted: #9690a1;
1259 --color-foreground-tertiary: #8e8a95;
1260 --color-border: #4d4366;
1261 --color-border-subtle: rgba(77, 67, 102, 0.7);
1262 --color-separator: rgba(77, 67, 102, 0.55);
1263 --color-subtle: #2d2643;
1264 --color-subtle-strong: #362d51;
1265 --color-inline-skill-background: #433765;
1266 --color-inline-skill-border: rgba(157, 125, 242, 0.42);
1267 --color-inline-skill-foreground: #fffaff;
1268 --color-primary: #9d7df2;
1269 --color-primary-foreground: #241523;
1270 --color-primary-shadow: #000000;
1271 --color-secondary: #362d51;
1272 --color-secondary-foreground: #fffaff;
1273 --color-secondary-border: #4d4366;
1274 --color-switch-active-track: #9d7df2;
1275 --color-switch-active-thumb: #241523;
1276 --color-switch-inactive-track: #362d51;
1277 --color-switch-inactive-thumb: #9690a1;
1278 --color-danger: #40202e;
1279 --color-danger-border: rgba(251, 65, 74, 0.32);
1280 --color-danger-foreground: #ff6467;
1281 --color-input: #383443;
1282 --color-input-border: #5d527b;
1283 --color-sidebar-search: #494459;
1284 --color-placeholder: #a29ea8;
1285 --color-icon: #fffaff;
1286 --color-icon-muted: #8e8a95;
1287 --color-icon-subtle: #8e8a95;
1288 --color-header: rgba(29, 25, 41, 0.97);
1289 --color-header-border: #4a3c70;
1290 --color-glass-surface: rgba(69, 66, 80, 0.74);
1291 --color-glass-tint: rgba(69, 66, 80, 0.22);
1292 --color-status-bar: #1d1929;
1293 --color-md-body: #fffaff;
1294 --color-md-strong: #fffaff;
1295 --color-md-link: #9d7df2;
1296 --color-md-blockquote-border: #4d4366;
1297 --color-md-blockquote-bg: #2d2643;
1298 --color-md-code-bg: #2a2736;
1299 --color-md-code-text: #fffaff;
1300 --color-md-user-code-bg: rgba(255, 250, 255, 0.18);
1301 --color-md-user-code-text: #fffaff;
1302 --color-md-user-fence-bg: rgba(0, 0, 0, 0.28);
1303 --color-md-user-fence-text: #fffaff;
1304 --color-md-hr: #4d4366;
1305 --color-user-bubble: #4b3d72;
1306 --color-user-bubble-foreground: #fffaff;
1307 --color-user-bubble-foreground-muted: rgba(255, 250, 255, 0.78);
1308 --color-user-bubble-skill-foreground: #f099d8;
1309 --color-backdrop: rgba(0, 0, 0, 0.48);
1310 --color-drawer: rgba(39, 33, 57, 0.99);
1311 --color-drawer-shadow: rgba(0, 0, 0, 0.32);
1312 --color-dot-separator: rgba(142, 138, 149, 0.35);
1313 --color-wordmark: #fffaff;
1314 --color-chevron: rgba(142, 138, 149, 0.42);
1315 --color-adaptive-amber-50-950-a40: oklch(27.9% 0.077 45.635 / 40%);
1316 --color-adaptive-amber-200-900-a60: oklch(41.4% 0.112 45.904 / 60%);
1317 --color-adaptive-amber-500-a12-a16: oklch(76.9% 0.188 70.08 / 16%);
1318 --color-adaptive-amber-700-300: oklch(87.9% 0.169 91.605);
1319 --color-adaptive-amber-700-400: oklch(82.8% 0.189 84.429);
1320 --color-adaptive-amber-800-200: oklch(92.4% 0.12 95.746);
1321 --color-adaptive-blue-50-blue-400-a14: oklch(70.7% 0.165 254.624 / 14%);
1322 --color-adaptive-blue-300-a50-blue-400-a28: oklch(70.7% 0.165 254.624 / 28%);
1323 --color-adaptive-blue-500-a20-blue-400-a15: oklch(70.7% 0.165 254.624 / 15%);
1324 --color-adaptive-blue-500-400: oklch(70.7% 0.165 254.624);
1325 --color-adaptive-blue-600-400: oklch(70.7% 0.165 254.624);
1326 --color-adaptive-black-a10-a25: rgb(0 0 0 / 25%);
1327 --color-adaptive-black-a15-a35: rgb(0 0 0 / 35%);
1328 --color-adaptive-emerald-500-a12-a16: oklch(69.6% 0.17 162.48 / 16%);
1329 --color-adaptive-emerald-600-400: oklch(76.5% 0.177 163.223);
1330 --color-adaptive-emerald-700-300: oklch(84.5% 0.143 164.978);
1331 --color-adaptive-indigo-500-a12-a16: oklch(58.5% 0.233 277.117 / 16%);
1332 --color-adaptive-indigo-600-300: oklch(78.5% 0.115 274.713);
1333 --color-adaptive-indigo-700-300: oklch(78.5% 0.115 274.713);
1334 --color-adaptive-neutral-100-900: oklch(20.5% 0 0);
1335 --color-adaptive-neutral-200-700-a60: oklch(37.1% 0 0 / 60%);
1336 --color-adaptive-neutral-200-800: oklch(26.9% 0 0);
1337 --color-adaptive-neutral-200-a70-white-a8: rgb(255 255 255 / 8%);
1338 --color-adaptive-neutral-200-white-a6: rgb(255 255 255 / 6%);
1339 --color-adaptive-neutral-200-white-a8: rgb(255 255 255 / 8%);
1340 --color-adaptive-neutral-200-a80-white-a8: rgb(255 255 255 / 8%);
1341 --color-adaptive-neutral-300-a60-white-a12: rgb(255 255 255 / 12%);
1342 --color-adaptive-neutral-400-500: oklch(55.6% 0 0);
1343 --color-adaptive-neutral-400-a60-500-a60: oklch(55.6% 0 0 / 60%);
1344 --color-adaptive-neutral-400-a80-500-a80: oklch(55.6% 0 0 / 80%);
1345 --color-adaptive-neutral-500-a10-a16: oklch(55.6% 0 0 / 16%);
1346 --color-adaptive-neutral-500-400: oklch(70.8% 0 0);
1347 --color-adaptive-neutral-500-500: oklch(55.6% 0 0);
1348 --color-adaptive-neutral-600-300: oklch(87% 0 0);
1349 --color-adaptive-neutral-600-400: oklch(70.8% 0 0);
1350 --color-adaptive-neutral-950-50: oklch(98.5% 0 0);
1351 --color-adaptive-red-50-950-a80: oklch(25.8% 0.092 26.042 / 80%);
1352 --color-adaptive-red-200-800: oklch(44.4% 0.177 26.899);
1353 --color-adaptive-red-600-a80-400-a80: oklch(70.4% 0.191 22.216 / 80%);
1354 --color-adaptive-red-700-300: oklch(80.8% 0.114 19.571);
1355 --color-adaptive-rose-100-500-a18: oklch(64.5% 0.246 16.439 / 18%);
1356 --color-adaptive-rose-100-a80-500-a12: oklch(64.5% 0.246 16.439 / 12%);
1357 --color-adaptive-rose-300-a70-400-a28: oklch(71.2% 0.194 13.428 / 28%);
1358 --color-adaptive-rose-500-a12-a16: oklch(64.5% 0.246 16.439 / 16%);
1359 --color-adaptive-rose-500-400: oklch(71.2% 0.194 13.428);
1360 --color-adaptive-rose-600-400: oklch(71.2% 0.194 13.428);
1361 --color-adaptive-rose-700-300: oklch(81% 0.117 11.638);
1362 --color-adaptive-sky-500-a12-a16: oklch(68.5% 0.169 237.323 / 16%);
1363 --color-adaptive-sky-600-400: oklch(74.6% 0.16 232.661);
1364 --color-adaptive-sky-700-300: oklch(82.8% 0.111 230.318);
1365 --color-adaptive-violet-500-a12-a16: oklch(60.6% 0.25 292.717 / 16%);
1366 --color-adaptive-violet-600-400: oklch(70.2% 0.183 293.541);
1367 --color-adaptive-violet-700-300: oklch(81.1% 0.111 293.571);
1368 --color-adaptive-white-neutral-950-a70: oklch(14.5% 0 0 / 70%);
1369 --color-adaptive-zinc-500-a12-a16: oklch(55.2% 0.016 285.938 / 16%);
1370 --color-adaptive-zinc-500-400: oklch(70.5% 0.015 286.067);
1371 --color-adaptive-zinc-600-300: oklch(87.1% 0.006 286.286);
1372 }
1373 }
1374}
apps/mobile/global.css+1
......@@ -1,5 +1,6 @@
11@import "tailwindcss";
22@import "uniwind";
3@import "./generated-uniwind-themes.css";
34
45/* ─── Theme tokens ──────────────────────────────────────────────────── */
56@layer theme {
apps/mobile/metro.config.js+2
......@@ -2,6 +2,7 @@ const fs = require("node:fs");
22const path = require("node:path");
33const { getDefaultConfig } = require("expo/metro-config");
44const { withUniwindConfig } = require("uniwind/metro");
5const extraThemes = require("./generated-uniwind-theme-names.json");
56
67/** @type {import("expo/metro-config").MetroConfig} */
78const config = getDefaultConfig(__dirname);
......@@ -50,5 +51,6 @@ config.resolver = {
5051
5152module.exports = withUniwindConfig(config, {
5253 cssEntryFile: "./global.css",
54 extraThemes,
5355 polyfills: { rem: 14 },
5456});
apps/mobile/package.json+1
......@@ -39,6 +39,7 @@
3939 "config:prod": "APP_VARIANT=production expo config",
4040 "profile:android:hermes": "mkdir -p profiles/review && react-native profile-hermes profiles/review",
4141 "sync:pierre-icons": "node modules/t3-markdown-text/scripts/sync-pierre-file-icons.mjs",
42 "generate": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON scripts/generate-uniwind-themes.mts",
4243 "test": "vp test run",
4344 "typecheck": "tsc --noEmit"
4445 },
apps/mobile/scripts/generate-uniwind-themes.mts created+262
......@@ -0,0 +1,262 @@
1#!/usr/bin/env node
2
3import * as NodeFS from "node:fs";
4import * as NodePath from "node:path";
5import tailwindColors from "tailwindcss/colors";
6import { BUILT_IN_THEME_IDS, type BuiltInThemeId } from "@t3tools/shared/themePalettes";
7
8import {
9 getMobileThemeVariables,
10 MOBILE_THEME_VARIABLE_NAMES,
11 type MobileThemeAppearance,
12 type MobileThemeVariables,
13} from "../src/lib/mobileTheme.ts";
14
15const APPEARANCES = ["light", "dark"] as const;
16const GLOBAL_CSS_PATH = NodePath.resolve(import.meta.dirname, "../global.css");
17const GENERATED_CSS_PATH = NodePath.resolve(import.meta.dirname, "../generated-uniwind-themes.css");
18const GENERATED_NAMES_PATH = NodePath.resolve(
19 import.meta.dirname,
20 "../generated-uniwind-theme-names.json",
21);
22const GENERATED_DEFAULT_VARIABLES_PATH = NodePath.resolve(
23 import.meta.dirname,
24 "../generated-uniwind-default-theme-variables.json",
25);
26
27type TailwindColorFamily = keyof typeof tailwindColors;
28type TailwindColorShade = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 950;
29
30const color = (family: TailwindColorFamily, shade?: TailwindColorShade, opacity = 1): string => {
31 const familyColors = tailwindColors[family];
32 const value =
33 typeof familyColors === "string"
34 ? shade === undefined
35 ? familyColors
36 : undefined
37 : shade === undefined
38 ? undefined
39 : familyColors[String(shade) as keyof typeof familyColors];
40 if (value === undefined) {
41 throw new Error(`Unknown Tailwind color ${family}${shade === undefined ? "" : `-${shade}`}.`);
42 }
43 if (opacity === 1) return value;
44
45 const percentage = Number((opacity * 100).toFixed(4));
46 const oklch = /^oklch\((.*)\)$/.exec(value);
47 if (oklch) return `oklch(${oklch[1]} / ${percentage}%)`;
48 if (value === "#fff") return `rgb(255 255 255 / ${percentage}%)`;
49 if (value === "#000") return `rgb(0 0 0 / ${percentage}%)`;
50 return `color-mix(in srgb, ${value} ${percentage}%, transparent)`;
51};
52
53// These replace the remaining dark:* utility pairs. A registered palette theme is
54// neither literally `light` nor `dark`, so appearance-sensitive values must also be
55// represented as semantic variables for custom themes.
56const ADAPTIVE_COLORS = {
57 "--color-adaptive-amber-50-950-a40": [color("amber", 50), color("amber", 950, 0.4)],
58 "--color-adaptive-amber-200-900-a60": [color("amber", 200), color("amber", 900, 0.6)],
59 "--color-adaptive-amber-500-a12-a16": [color("amber", 500, 0.12), color("amber", 500, 0.16)],
60 "--color-adaptive-amber-700-300": [color("amber", 700), color("amber", 300)],
61 "--color-adaptive-amber-700-400": [color("amber", 700), color("amber", 400)],
62 "--color-adaptive-amber-800-200": [color("amber", 800), color("amber", 200)],
63 "--color-adaptive-blue-50-blue-400-a14": [color("blue", 50), color("blue", 400, 0.14)],
64 "--color-adaptive-blue-300-a50-blue-400-a28": [color("blue", 300, 0.5), color("blue", 400, 0.28)],
65 "--color-adaptive-blue-500-a20-blue-400-a15": [color("blue", 500, 0.2), color("blue", 400, 0.15)],
66 "--color-adaptive-blue-500-400": [color("blue", 500), color("blue", 400)],
67 "--color-adaptive-blue-600-400": [color("blue", 600), color("blue", 400)],
68 "--color-adaptive-black-a10-a25": [
69 color("black", undefined, 0.1),
70 color("black", undefined, 0.25),
71 ],
72 "--color-adaptive-black-a15-a35": [
73 color("black", undefined, 0.15),
74 color("black", undefined, 0.35),
75 ],
76 "--color-adaptive-emerald-500-a12-a16": [
77 color("emerald", 500, 0.12),
78 color("emerald", 500, 0.16),
79 ],
80 "--color-adaptive-emerald-600-400": [color("emerald", 600), color("emerald", 400)],
81 "--color-adaptive-emerald-700-300": [color("emerald", 700), color("emerald", 300)],
82 "--color-adaptive-indigo-500-a12-a16": [color("indigo", 500, 0.12), color("indigo", 500, 0.16)],
83 "--color-adaptive-indigo-600-300": [color("indigo", 600), color("indigo", 300)],
84 "--color-adaptive-indigo-700-300": [color("indigo", 700), color("indigo", 300)],
85 "--color-adaptive-neutral-100-900": [color("neutral", 100), color("neutral", 900)],
86 "--color-adaptive-neutral-200-700-a60": [color("neutral", 200), color("neutral", 700, 0.6)],
87 "--color-adaptive-neutral-200-800": [color("neutral", 200), color("neutral", 800)],
88 "--color-adaptive-neutral-200-a70-white-a8": [
89 color("neutral", 200, 0.7),
90 color("white", undefined, 0.08),
91 ],
92 "--color-adaptive-neutral-200-white-a6": [color("neutral", 200), color("white", undefined, 0.06)],
93 "--color-adaptive-neutral-200-white-a8": [color("neutral", 200), color("white", undefined, 0.08)],
94 "--color-adaptive-neutral-200-a80-white-a8": [
95 color("neutral", 200, 0.8),
96 color("white", undefined, 0.08),
97 ],
98 "--color-adaptive-neutral-300-a60-white-a12": [
99 color("neutral", 300, 0.6),
100 color("white", undefined, 0.12),
101 ],
102 "--color-adaptive-neutral-400-500": [color("neutral", 400), color("neutral", 500)],
103 "--color-adaptive-neutral-400-a60-500-a60": [
104 color("neutral", 400, 0.6),
105 color("neutral", 500, 0.6),
106 ],
107 "--color-adaptive-neutral-400-a80-500-a80": [
108 color("neutral", 400, 0.8),
109 color("neutral", 500, 0.8),
110 ],
111 "--color-adaptive-neutral-500-a10-a16": [color("neutral", 500, 0.1), color("neutral", 500, 0.16)],
112 "--color-adaptive-neutral-500-400": [color("neutral", 500), color("neutral", 400)],
113 "--color-adaptive-neutral-500-500": [color("neutral", 500), color("neutral", 500)],
114 "--color-adaptive-neutral-600-300": [color("neutral", 600), color("neutral", 300)],
115 "--color-adaptive-neutral-600-400": [color("neutral", 600), color("neutral", 400)],
116 "--color-adaptive-neutral-950-50": [color("neutral", 950), color("neutral", 50)],
117 "--color-adaptive-red-50-950-a80": [color("red", 50), color("red", 950, 0.8)],
118 "--color-adaptive-red-200-800": [color("red", 200), color("red", 800)],
119 "--color-adaptive-red-600-a80-400-a80": [color("red", 600, 0.8), color("red", 400, 0.8)],
120 "--color-adaptive-red-700-300": [color("red", 700), color("red", 300)],
121 "--color-adaptive-rose-100-500-a18": [color("rose", 100), color("rose", 500, 0.18)],
122 "--color-adaptive-rose-100-a80-500-a12": [color("rose", 100, 0.8), color("rose", 500, 0.12)],
123 "--color-adaptive-rose-300-a70-400-a28": [color("rose", 300, 0.7), color("rose", 400, 0.28)],
124 "--color-adaptive-rose-500-a12-a16": [color("rose", 500, 0.12), color("rose", 500, 0.16)],
125 "--color-adaptive-rose-500-400": [color("rose", 500), color("rose", 400)],
126 "--color-adaptive-rose-600-400": [color("rose", 600), color("rose", 400)],
127 "--color-adaptive-rose-700-300": [color("rose", 700), color("rose", 300)],
128 "--color-adaptive-sky-500-a12-a16": [color("sky", 500, 0.12), color("sky", 500, 0.16)],
129 "--color-adaptive-sky-600-400": [color("sky", 600), color("sky", 400)],
130 "--color-adaptive-sky-700-300": [color("sky", 700), color("sky", 300)],
131 "--color-adaptive-violet-500-a12-a16": [color("violet", 500, 0.12), color("violet", 500, 0.16)],
132 "--color-adaptive-violet-600-400": [color("violet", 600), color("violet", 400)],
133 "--color-adaptive-violet-700-300": [color("violet", 700), color("violet", 300)],
134 "--color-adaptive-white-neutral-950-a70": [color("white"), color("neutral", 950, 0.7)],
135 "--color-adaptive-zinc-500-a12-a16": [color("zinc", 500, 0.12), color("zinc", 500, 0.16)],
136 "--color-adaptive-zinc-500-400": [color("zinc", 500), color("zinc", 400)],
137 "--color-adaptive-zinc-600-300": [color("zinc", 600), color("zinc", 300)],
138};
139
140export const customThemeNames = BUILT_IN_THEME_IDS.flatMap((themeId) =>
141 APPEARANCES.map((appearance) => `${themeId}-${appearance}`),
142);
143
144const adaptiveVariablesFor = (appearance: MobileThemeAppearance) =>
145 Object.fromEntries(
146 Object.entries(ADAPTIVE_COLORS).map(([name, values]) => [
147 name,
148 values[appearance === "light" ? 0 : 1],
149 ]),
150 );
151
152const variablesFor = (themeId: BuiltInThemeId, appearance: MobileThemeAppearance) => ({
153 ...getMobileThemeVariables(themeId, appearance),
154 ...adaptiveVariablesFor(appearance),
155});
156
157const renderVariant = (name: string, variables: Readonly<Record<string, string>>) => {
158 const declarations = Object.entries(variables)
159 .map(([variable, value]) => ` ${variable}: ${value};`)
160 .join("\n");
161 return ` @variant ${name} {\n${declarations}\n }`;
162};
163
164export const renderUniwindThemesCSS = () => {
165 const variants = [
166 renderVariant("light", adaptiveVariablesFor("light")),
167 renderVariant("dark", adaptiveVariablesFor("dark")),
168 ...BUILT_IN_THEME_IDS.flatMap((themeId) =>
169 APPEARANCES.map((appearance) =>
170 renderVariant(`${themeId}-${appearance}`, variablesFor(themeId, appearance)),
171 ),
172 ),
173 ];
174 return [
175 "/* Generated by scripts/generate-uniwind-themes.mts. Do not edit manually. */",
176 "@layer theme {",
177 " :root {",
178 variants.join("\n\n"),
179 " }",
180 "}",
181 "",
182 ].join("\n");
183};
184
185const readVariantBody = (css: string, appearance: MobileThemeAppearance): string => {
186 const marker = `@variant ${appearance} {`;
187 const markerIndex = css.indexOf(marker);
188 if (markerIndex === -1) throw new Error(`Could not find ${marker} in global.css.`);
189
190 const openingBraceIndex = css.indexOf("{", markerIndex);
191 let depth = 0;
192 for (let index = openingBraceIndex; index < css.length; index += 1) {
193 if (css[index] === "{") depth += 1;
194 if (css[index] !== "}") continue;
195 depth -= 1;
196 if (depth === 0) return css.slice(openingBraceIndex + 1, index);
197 }
198 throw new Error(`Could not find the end of ${marker} in global.css.`);
199};
200
201export const readDefaultThemeVariables = (css: string) =>
202 Object.fromEntries(
203 APPEARANCES.map((appearance) => {
204 const body = readVariantBody(css, appearance);
205 const variables = Object.fromEntries(
206 MOBILE_THEME_VARIABLE_NAMES.map((name) => {
207 const match = new RegExp(`^\\s*${name}:\\s*([^;]+);`, "mu").exec(body);
208 if (!match?.[1]) {
209 throw new Error(`Default ${appearance} theme is missing ${name}.`);
210 }
211 return [name, match[1].trim()];
212 }),
213 ) as MobileThemeVariables;
214 return [appearance, variables];
215 }),
216 ) as Readonly<Record<MobileThemeAppearance, MobileThemeVariables>>;
217
218export const renderDefaultThemeVariablesJSON = (css: string) =>
219 `${JSON.stringify(readDefaultThemeVariables(css), null, 2)}\n`;
220
221export const getGeneratedUniwindThemeOutputs = (): ReadonlyArray<
222 readonly [filename: string, contents: string]
223> => [
224 [GENERATED_CSS_PATH, renderUniwindThemesCSS()],
225 [GENERATED_NAMES_PATH, `${JSON.stringify(customThemeNames, null, 2)}\n`],
226 [
227 GENERATED_DEFAULT_VARIABLES_PATH,
228 renderDefaultThemeVariablesJSON(NodeFS.readFileSync(GLOBAL_CSS_PATH, "utf8")),
229 ],
230];
231
232const writeFileAtomically = (filename: string, contents: string) => {
233 const current = NodeFS.existsSync(filename) ? NodeFS.readFileSync(filename, "utf8") : null;
234 if (current === contents) return;
235
236 const temporaryFilename = `${filename}.${process.pid}.tmp`;
237 try {
238 NodeFS.writeFileSync(temporaryFilename, contents);
239 NodeFS.renameSync(temporaryFilename, filename);
240 } finally {
241 if (NodeFS.existsSync(temporaryFilename)) NodeFS.unlinkSync(temporaryFilename);
242 }
243};
244
245if (import.meta.main) {
246 const checkOnly = process.argv.includes("--check");
247 for (const [filename, contents] of getGeneratedUniwindThemeOutputs()) {
248 if (checkOnly) {
249 const current = NodeFS.existsSync(filename) ? NodeFS.readFileSync(filename, "utf8") : null;
250 if (current !== contents) {
251 console.error(
252 `${NodePath.relative(process.cwd(), filename)} is stale. Run vp run --filter @t3tools/mobile generate.`,
253 );
254 process.exitCode = 1;
255 }
256 continue;
257 }
258 // Metro watches the generated CSS. Replacing a complete temporary file keeps
259 // Tailwind from compiling a partially rewritten theme file.
260 writeFileAtomically(filename, contents);
261 }
262}
apps/mobile/scripts/generate-uniwind-themes.test.ts created+55
......@@ -0,0 +1,55 @@
1import * as NodeFS from "node:fs";
2import * as NodePath from "node:path";
3import { describe, expect, it } from "vite-plus/test";
4
5import {
6 customThemeNames,
7 getGeneratedUniwindThemeOutputs,
8 readDefaultThemeVariables,
9 renderUniwindThemesCSS,
10} from "./generate-uniwind-themes.mts";
11
12describe("generate mobile Uniwind themes", () => {
13 it("keeps the committed outputs current", () => {
14 const staleOutputs = getGeneratedUniwindThemeOutputs()
15 .filter(
16 ([filename, contents]) =>
17 !NodeFS.existsSync(filename) || NodeFS.readFileSync(filename, "utf8") !== contents,
18 )
19 .map(([filename]) => NodePath.relative(import.meta.dirname, filename));
20
21 expect(
22 staleOutputs,
23 "Run `vp run --filter @t3tools/mobile generate` and commit the generated outputs.",
24 ).toEqual([]);
25 });
26
27 it("registers every custom palette for both appearances", () => {
28 expect(customThemeNames).toEqual([
29 "t3-chat-light",
30 "t3-chat-dark",
31 "grove-light",
32 "grove-dark",
33 "ocean-light",
34 "ocean-dark",
35 "ember-light",
36 "ember-dark",
37 "iris-light",
38 "iris-dark",
39 ]);
40
41 const stylesheet = renderUniwindThemesCSS();
42 for (const themeName of customThemeNames) {
43 expect(stylesheet.match(new RegExp(`@variant ${themeName} \\{`, "gu"))).toHaveLength(1);
44 }
45 });
46
47 it("generates the default runtime bridge from the authored CSS", () => {
48 const css = NodeFS.readFileSync(NodePath.resolve(import.meta.dirname, "../global.css"), "utf8");
49 const variables = readDefaultThemeVariables(css);
50
51 expect(variables.light["--color-screen"]).toBe("#f2f2f7");
52 expect(variables.dark["--color-screen"]).toBe("#0a0a0a");
53 expect(Object.keys(variables.light)).toEqual(Object.keys(variables.dark));
54 });
55});
apps/mobile/src/App.tsx+1-4
......@@ -21,7 +21,6 @@ import { RootStack } from "./Stack";
2121import { appAtomRegistry } from "./state/atom-registry";
2222import { OverlayPortalHost } from "./components/OverlayPortal";
2323import { appBlurTargetRef } from "./lib/appBlurTarget";
24import { useThemeColor } from "./lib/useThemeColor";
2524import { useMobileNavigationTheme } from "./lib/useMobileNavigationTheme";
2625
2726import "../global.css";
......@@ -72,8 +71,7 @@ export default function App() {
7271
7372function AppContent() {
7473 const { themeAppearance } = useAppearancePreferences();
75 const statusBarBg = useThemeColor("--color-status-bar");
76 const navigationTheme = useMobileNavigationTheme(themeAppearance);
74 const navigationTheme = useMobileNavigationTheme();
7775
7876 return (
7977 <>
......@@ -83,7 +81,6 @@ function AppContent() {
8381 <SafeAreaProvider>
8482 <StatusBar
8583 barStyle={themeAppearance === "dark" ? "light-content" : "dark-content"}
86 backgroundColor={statusBarBg}
8784 translucent
8885 />
8986 {/* The navigation theme drives the NATIVE header appearance: native-stack
apps/mobile/src/components/AndroidAnchoredMenu.tsx+6-11
......@@ -9,7 +9,6 @@ import Animated, { FadeIn } from "react-native-reanimated";
99
1010import { appBlurTargetRef } from "../lib/appBlurTarget";
1111import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
12import { useThemeColor } from "../lib/useThemeColor";
1312import { cn } from "../lib/cn";
1413import { type AppSymbolName, SymbolView } from "./AppSymbol";
1514import { AppText as Text } from "./AppText";
......@@ -84,11 +83,6 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) {
8483 const isDarkMode = themeAppearance === "dark";
8584 const keyboardVisible = useKeyboardState((state) => state.isVisible);
8685 const keyboardHeight = useKeyboardState((state) => state.height);
87 const rippleColor = useThemeColor("--color-subtle");
88 const iconColor = useThemeColor("--color-icon");
89 const iconSubtleColor = useThemeColor("--color-icon-subtle");
90 const dangerColor = useThemeColor("--color-danger-foreground");
91
9286 const close = useCallback(() => {
9387 setAnchor(null);
9488 setPath([]);
......@@ -279,10 +273,9 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) {
279273 return (
280274 <Pressable
281275 key={action.id ?? `${index}-${action.title}`}
282 android_ripple={{ color: rippleColor }}
283276 disabled={disabled}
284277 className={cn(
285 "min-h-11 flex-row items-center gap-2.5 px-3.5 py-2.5",
278 "min-h-11 flex-row items-center gap-2.5 px-3.5 py-2.5 active:bg-subtle",
286279 disabled && "opacity-45",
287280 )}
288281 onPress={() => onPressItem(action)}
......@@ -307,21 +300,23 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) {
307300 <SymbolView
308301 name="chevron.right"
309302 size={13}
310 tintColor={iconSubtleColor}
303 tintColorClassName={"accent-icon-subtle"}
311304 type="monochrome"
312305 />
313306 ) : action.state === "on" ? (
314307 <SymbolView
315308 name="checkmark"
316309 size={15}
317 tintColor={iconColor}
310 tintColorClassName={"accent-icon"}
318311 type="monochrome"
319312 />
320313 ) : action.image ? (
321314 <SymbolView
322315 name={action.image as AppSymbolName}
323316 size={15}
324 tintColor={destructive ? dangerColor : iconColor}
317 tintColorClassName={
318 destructive ? "accent-danger-foreground" : "accent-icon"
319 }
325320 type="monochrome"
326321 />
327322 ) : null}
apps/mobile/src/components/AndroidScreenHeader.tsx+2-7
......@@ -5,7 +5,6 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
55import { SymbolView, type AppSymbolName } from "./AppSymbol";
66import { AppText as Text } from "./AppText";
77import { cn } from "../lib/cn";
8import { useThemeColor } from "../lib/useThemeColor";
98
109export interface AndroidHeaderAction {
1110 readonly accessibilityLabel: string;
......@@ -20,9 +19,6 @@ export function AndroidHeaderIconButton(props: {
2019 readonly onPress?: () => void;
2120 readonly disabled?: boolean;
2221}) {
23 const foregroundColor = useThemeColor("--color-foreground");
24 const disabledColor = useThemeColor("--color-icon-subtle");
25
2622 return (
2723 <Pressable
2824 accessibilityLabel={props.accessibilityLabel}
......@@ -38,7 +34,7 @@ export function AndroidHeaderIconButton(props: {
3834 <SymbolView
3935 name={props.icon}
4036 size={20}
41 tintColor={props.disabled ? disabledColor : foregroundColor}
37 tintColorClassName={props.disabled ? "accent-icon-subtle" : "accent-foreground"}
4238 type="monochrome"
4339 />
4440 </Pressable>
......@@ -54,7 +50,6 @@ export function AndroidScreenHeader(props: {
5450 readonly embedded?: boolean;
5551}) {
5652 const insets = useSafeAreaInsets();
57 const foregroundColor = useThemeColor("--color-foreground");
5853
5954 return (
6055 <View
......@@ -75,7 +70,7 @@ export function AndroidScreenHeader(props: {
7570 <SymbolView
7671 name="chevron.left"
7772 size={24}
78 tintColor={foregroundColor}
73 tintColorClassName={"accent-foreground"}
7974 type="monochrome"
8075 />
8176 </Pressable>
apps/mobile/src/components/AppSymbol.tsx+10-1
......@@ -81,6 +81,7 @@ import {
8181} from "@tabler/icons-react-native";
8282import { Platform } from "react-native";
8383import { SymbolView as ExpoSymbolView, type SFSymbol, type SymbolViewProps } from "expo-symbols";
84import { withUniwind } from "uniwind";
8485
8586const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = {
8687 "arrow.branch": IconGitBranch,
......@@ -190,7 +191,7 @@ const ANDROID_ICON_BY_MATERIAL_NAME: Record<string, Icon> = {
190191export type { SFSymbol } from "expo-symbols";
191192export type AppSymbolName = SymbolViewProps["name"];
192193
193export function SymbolView(props: SymbolViewProps) {
194function AppSymbolView(props: SymbolViewProps) {
194195 if (Platform.OS !== "android") {
195196 return <ExpoSymbolView {...props} />;
196197 }
......@@ -216,3 +217,11 @@ export function SymbolView(props: SymbolViewProps) {
216217 />
217218 );
218219}
220
221/**
222 * expo-symbols and the Android Tabler fallback both expose tint as a native
223 * prop rather than a React Native style. Keep that third-party boundary here
224 * so callers can use Uniwind's `tintColorClassName` instead of subscribing to
225 * theme variables in every parent component.
226 */
227export const SymbolView = withUniwind(AppSymbolView);
apps/mobile/src/components/CompactBrandTitle.tsx+6-32
......@@ -9,7 +9,6 @@ import { AppText as Text } from "./AppText";
99import { T3Wordmark } from "./T3Wordmark";
1010import { IPAD_HOME_TITLE_OFFSET } from "../lib/layoutMetrics";
1111import { resolveMobileStageLabel } from "../lib/mobileBranding";
12import { useThemeColor } from "../lib/useThemeColor";
1312import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../native/native-glass";
1413
1514// Native leading items inherit different UIKit margins than title views.
......@@ -37,9 +36,6 @@ export function CompactBrandTitle(
3736 readonly nativeLeadingItem?: boolean;
3837 } = {},
3938) {
40 const iconColor = useThemeColor("--color-icon");
41 const mutedColor = useThemeColor("--color-foreground-muted");
42 const subtleColor = useThemeColor("--color-subtle");
4339 const stageLabel = resolveMobileStageLabel(Constants.expoConfig?.extra?.appVariant);
4440 const titleOffset = brandTitleOffset(props.nativeLeadingItem === true);
4541
......@@ -49,42 +45,20 @@ export function CompactBrandTitle(
4945 accessibilityLabel="T3 Code, Threads"
5046 accessible
5147 role="heading"
52 style={{
53 alignItems: "center",
54 flexDirection: "row",
55 gap: 6,
56 marginLeft: titleOffset,
57 }}
48 className="flex-row items-center gap-1.5"
49 style={{ marginLeft: titleOffset }}
5850 >
59 <T3Wordmark color={iconColor} height={15} />
51 <T3Wordmark colorClassName="accent-icon" height={15} />
6052 <Text
6153 allowFontScaling={props.allowFontScaling}
62 style={{
63 color: mutedColor,
64 fontFamily: "DMSans-Medium",
65 fontSize: 21,
66 letterSpacing: -0.5,
67 }}
54 className="font-t3-medium text-[21px] tracking-[-0.5px] text-foreground-muted"
6855 >
6956 Code
7057 </Text>
71 <View
72 style={{
73 backgroundColor: subtleColor,
74 borderRadius: 999,
75 paddingHorizontal: 6,
76 paddingVertical: 2,
77 }}
78 >
58 <View className="rounded-full bg-subtle px-1.5 py-0.5">
7959 <Text
8060 allowFontScaling={props.allowFontScaling}
81 style={{
82 color: mutedColor,
83 fontFamily: "DMSans-Bold",
84 fontSize: 9,
85 letterSpacing: 0.9,
86 textTransform: "uppercase",
87 }}
61 className="font-t3-bold text-[9px] tracking-[0.9px] text-foreground-muted uppercase"
8862 >
8963 {stageLabel}
9064 </Text>
apps/mobile/src/components/ComposerAttachmentStrip.tsx+1-3
......@@ -1,6 +1,5 @@
11import { SymbolView } from "../components/AppSymbol";
22import { Image, Pressable, ScrollView, View } from "react-native";
3import { useThemeColor } from "../lib/useThemeColor";
43
54import type { DraftComposerImageAttachment } from "../lib/composerImages";
65
......@@ -24,7 +23,6 @@ export interface ComposerAttachmentStripProps {
2423 * buttons. Used by both the thread composer and the new-task draft screen.
2524 */
2625export function ComposerAttachmentStrip(props: ComposerAttachmentStripProps) {
27 const subtleBg = useThemeColor("--color-subtle");
2826 const size = props.imageSize ?? 72;
2927 const radius = props.imageBorderRadius ?? 16;
3028 const removeButtonPlacement = props.removeButtonPlacement ?? "overlay";
......@@ -56,11 +54,11 @@ export function ComposerAttachmentStrip(props: ComposerAttachmentStripProps) {
5654 >
5755 <Image
5856 source={{ uri: image.previewUri }}
57 className="bg-subtle"
5958 style={{
6059 width: size,
6160 height: size,
6261 borderRadius: radius,
63 backgroundColor: subtleBg,
6462 }}
6563 resizeMode="cover"
6664 />
apps/mobile/src/components/ComposerToolbar.tsx+65-54
......@@ -11,9 +11,6 @@ import {
1111 type ViewStyle,
1212} from "react-native";
1313
14import { useThemeColor } from "../lib/useThemeColor";
15import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
16import { themeColorWithAlpha } from "../lib/mobileTheme";
1714import { cn } from "../lib/cn";
1815import { AppText as Text } from "./AppText";
1916import { SymbolView } from "./AppSymbol";
......@@ -42,10 +39,6 @@ export function ComposerInlineControl(props: {
4239 readonly chevronDirection?: "down" | "right";
4340 readonly showChevron?: boolean;
4441}) {
45 const iconColor = useThemeColor(
46 props.emphasized || props.selected ? "--color-icon" : "--color-icon-muted",
47 );
48
4942 return (
5043 <Pressable
5144 accessibilityLabel={props.accessibilityLabel ?? props.label}
......@@ -62,7 +55,14 @@ export function ComposerInlineControl(props: {
6255 {props.iconNode ? (
6356 <View className="size-4 shrink-0 items-center justify-center">{props.iconNode}</View>
6457 ) : props.icon ? (
65 <SymbolView name={props.icon} size={16} tintColor={iconColor} type="monochrome" />
58 <SymbolView
59 name={props.icon}
60 size={16}
61 tintColorClassName={
62 props.emphasized || props.selected ? "accent-icon" : "accent-icon-muted"
63 }
64 type="monochrome"
65 />
6666 ) : null}
6767 <Text
6868 className={cn(
......@@ -77,7 +77,9 @@ export function ComposerInlineControl(props: {
7777 <SymbolView
7878 name={props.chevronDirection === "right" ? "chevron.right" : "chevron.down"}
7979 size={10}
80 tintColor={iconColor}
80 tintColorClassName={
81 props.emphasized || props.selected ? "accent-icon" : "accent-icon-muted"
82 }
8183 type="monochrome"
8284 />
8385 )}
......@@ -111,8 +113,12 @@ export function ComposerToolbarRow(props: {
111113
112114export function ComposerToolbarScroller(props: {
113115 readonly children: ReactNode;
114 readonly fadeOpaque: string;
115 readonly fadeTransparent: string;
116 /** Only for non-Uniwind surfaces such as the native terminal palette. */
117 readonly fadeOpaque?: string;
118 /** Only for non-Uniwind surfaces such as the native terminal palette. */
119 readonly fadeTransparent?: string;
120 /** Semantic Uniwind surface behind the toolbar. Defaults to card. */
121 readonly fadeSurface?: "card" | "sheet";
116122 readonly contentPaddingRight?: number;
117123}) {
118124 const [metrics, setMetrics] = useState({
......@@ -170,27 +176,37 @@ export function ComposerToolbarScroller(props: {
170176 </ScrollView>
171177 {scrollEdges.showLeftFade ? (
172178 <View
179 className={cn(
180 "absolute inset-y-0 left-0",
181 props.fadeOpaque === undefined && "bg-linear-to-r to-transparent",
182 props.fadeOpaque === undefined &&
183 (props.fadeSurface === "sheet" ? "from-sheet" : "from-card"),
184 )}
173185 pointerEvents="none"
174186 style={{
175 bottom: 0,
176 experimental_backgroundImage: `linear-gradient(to right, ${props.fadeOpaque} 0%, ${props.fadeTransparent} 100%)`,
177 left: 0,
178 position: "absolute",
179 top: 0,
180187 width: COMPOSER_TOOLBAR_FADE_WIDTH,
188 experimental_backgroundImage:
189 props.fadeOpaque === undefined
190 ? undefined
191 : `linear-gradient(to right, ${props.fadeOpaque} 0%, ${props.fadeTransparent ?? "transparent"} 100%)`,
181192 }}
182193 />
183194 ) : null}
184195 {scrollEdges.showRightFade ? (
185196 <View
197 className={cn(
198 "absolute inset-y-0 right-0",
199 props.fadeOpaque === undefined && "bg-linear-to-r from-transparent",
200 props.fadeOpaque === undefined &&
201 (props.fadeSurface === "sheet" ? "to-sheet" : "to-card"),
202 )}
186203 pointerEvents="none"
187204 style={{
188 bottom: 0,
189 experimental_backgroundImage: `linear-gradient(to right, ${props.fadeTransparent} 0%, ${props.fadeOpaque} 100%)`,
190 position: "absolute",
191 right: 0,
192 top: 0,
193205 width: COMPOSER_TOOLBAR_FADE_WIDTH,
206 experimental_backgroundImage:
207 props.fadeOpaque === undefined
208 ? undefined
209 : `linear-gradient(to right, ${props.fadeTransparent ?? "transparent"} 0%, ${props.fadeOpaque} 100%)`,
194210 }}
195211 />
196212 ) : null}
......@@ -214,30 +230,16 @@ export function ComposerToolbarButton(props: {
214230 readonly className?: string;
215231 readonly style?: StyleProp<ViewStyle>;
216232}) {
217 const { themeAppearance } = useAppearancePreferences();
218 const isDarkMode = themeAppearance === "dark";
219 const iconColor = useThemeColor("--color-icon");
220 const iconSubtle = useThemeColor("--color-icon-subtle");
221 const primaryFg = useThemeColor("--color-primary-foreground");
222 const dangerFg = useThemeColor("--color-danger-foreground");
223233 const variant = props.variant ?? "default";
224234 const isCircle = !props.label && props.showChevron === false;
225 const defaultBorderColor = useThemeColor("--color-border-subtle");
226 const activeBorderColor = useThemeColor("--color-border");
227 const filledBorderColor =
228 variant === "danger"
229 ? themeColorWithAlpha(String(dangerFg), 0.14)
230 : props.disabled
231 ? defaultBorderColor
232 : themeColorWithAlpha(String(primaryFg), 0.18);
233 const iconTintColor =
235 const iconTintClassName =
234236 variant === "primary"
235237 ? props.disabled
236 ? iconSubtle
237 : primaryFg
238 ? "accent-icon-subtle"
239 : "accent-primary-foreground"
238240 : variant === "danger"
239 ? dangerFg
240 : iconColor;
241 ? "accent-danger-foreground"
242 : "accent-icon";
241243
242244 return (
243245 <Pressable
......@@ -250,7 +252,7 @@ export function ComposerToolbarButton(props: {
250252 // so callers can lift it with max-w-full — flex-filling pills in the
251253 // thread composer stretch to the row's edge. The numeric maxWidth
252254 // prop still wins via the inline style below.
253 "h-11 max-w-[172px] flex-row items-center justify-center rounded-full active:opacity-70",
255 "h-11 max-w-[172px] flex-row items-center justify-center rounded-full border shadow-lg shadow-adaptive-black-a10-a25 active:opacity-70",
254256 isCircle ? "w-11" : "gap-2 px-3.5",
255257 variant === "primary"
256258 ? props.disabled
......@@ -261,24 +263,23 @@ export function ComposerToolbarButton(props: {
261263 : props.active
262264 ? "bg-subtle-strong"
263265 : "bg-subtle",
266 variant === "default"
267 ? props.active
268 ? "border-border"
269 : "border-border-subtle"
270 : variant === "danger"
271 ? "border-danger-border"
272 : props.disabled
273 ? "border-border-subtle"
274 : "border-primary-foreground/20",
264275 props.className,
265276 )}
266277 style={({ pressed }) => [
267278 {
268 borderColor:
269 variant === "default"
270 ? props.active
271 ? activeBorderColor
272 : defaultBorderColor
273 : filledBorderColor,
274 borderWidth: 1,
275279 maxWidth: props.maxWidth,
276280 minWidth: props.minWidth,
277281 opacity: props.disabled ? 0.55 : pressed ? 0.72 : 1,
278 shadowColor: "#000",
279 shadowOffset: { width: 0, height: isDarkMode ? 3 : 2 },
280 shadowOpacity: props.disabled ? 0 : isDarkMode ? 0.24 : 0.08,
281 shadowRadius: isDarkMode ? 10 : 8,
282 shadowOpacity: props.disabled ? 0 : 1,
282283 },
283284 props.style,
284285 ]}
......@@ -286,7 +287,12 @@ export function ComposerToolbarButton(props: {
286287 {props.iconNode ? (
287288 <View className="h-4 w-4 items-center justify-center">{props.iconNode}</View>
288289 ) : props.icon ? (
289 <SymbolView name={props.icon} size={16} tintColor={iconTintColor} type="monochrome" />
290 <SymbolView
291 name={props.icon}
292 size={16}
293 tintColorClassName={iconTintClassName}
294 type="monochrome"
295 />
290296 ) : null}
291297 {props.label ? (
292298 <Text
......@@ -306,7 +312,12 @@ export function ComposerToolbarButton(props: {
306312 </Text>
307313 ) : null}
308314 {props.showChevron === false ? null : (
309 <SymbolView name="chevron.down" size={11} tintColor={iconTintColor} type="monochrome" />
315 <SymbolView
316 name="chevron.down"
317 size={11}
318 tintColorClassName={iconTintClassName}
319 type="monochrome"
320 />
310321 )}
311322 </Pressable>
312323 );
apps/mobile/src/components/ConfirmDialogHost.tsx+2-7
......@@ -1,7 +1,6 @@
11import { useCallback, useEffect, useState } from "react";
22import { Modal, Pressable, View } from "react-native";
33
4import { useThemeColor } from "../lib/useThemeColor";
54import { cn } from "../lib/cn";
65import { AppText } from "./AppText";
76
......@@ -35,8 +34,6 @@ export function showConfirmDialog(request: ConfirmDialogRequest): void {
3534 */
3635export function ConfirmDialogHost() {
3736 const [request, setRequest] = useState<ConfirmDialogRequest | null>(null);
38 const pressedOverlay = useThemeColor("--color-subtle");
39
4037 useEffect(() => {
4138 presentRequest = setRequest;
4239 return () => {
......@@ -76,8 +73,7 @@ export function ConfirmDialogHost() {
7673 <View className="overflow-hidden rounded-full">
7774 <Pressable
7875 accessibilityRole="button"
79 className="min-h-10 items-center justify-center px-4"
80 android_ripple={{ color: pressedOverlay }}
76 className="min-h-10 items-center justify-center px-4 active:bg-subtle"
8177 onPress={handleCancel}
8278 >
8379 <AppText className="text-base font-t3-medium">
......@@ -88,8 +84,7 @@ export function ConfirmDialogHost() {
8884 <View className="overflow-hidden rounded-full">
8985 <Pressable
9086 accessibilityRole="button"
91 className="min-h-10 items-center justify-center px-4"
92 android_ripple={{ color: pressedOverlay }}
87 className="min-h-10 items-center justify-center px-4 active:bg-subtle"
9388 onPress={handleConfirm}
9489 >
9590 <AppText
apps/mobile/src/components/ControlPill.tsx+11-11
......@@ -9,7 +9,6 @@ import {
99 useRef,
1010} from "react";
1111import { Platform, Pressable, View } from "react-native";
12import { useThemeColor } from "../lib/useThemeColor";
1312import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
1413
1514import { cn } from "../lib/cn";
......@@ -49,18 +48,14 @@ export function ControlPill(props: {
4948 props.onPress?.();
5049 };
5150
52 const iconColor = useThemeColor("--color-icon");
53 const iconSubtle = useThemeColor("--color-icon-subtle");
54 const primaryFg = useThemeColor("--color-primary-foreground");
55 const dangerFg = useThemeColor("--color-danger-foreground");
56 const iconTintColor =
51 const iconTintClassName =
5752 variant === "primary"
5853 ? props.disabled
59 ? iconSubtle
60 : primaryFg
54 ? "accent-icon-subtle"
55 : "accent-primary-foreground"
6156 : variant === "danger"
62 ? dangerFg
63 : iconColor;
57 ? "accent-danger-foreground"
58 : "accent-icon";
6459
6560 const isCircle =
6661 variant === "circle" || variant === "danger" || (variant === "primary" && !props.label);
......@@ -101,7 +96,12 @@ export function ControlPill(props: {
10196 {props.iconNode ? (
10297 <View className="h-4 w-4 items-center justify-center">{props.iconNode}</View>
10398 ) : props.icon ? (
104 <SymbolView name={props.icon} size={16} tintColor={iconTintColor} type="monochrome" />
99 <SymbolView
100 name={props.icon}
101 size={16}
102 tintColorClassName={iconTintClassName}
103 type="monochrome"
104 />
105105 ) : null}
106106 {props.label ? <Text className={labelClassName}>{props.label}</Text> : null}
107107 </Pressable>
apps/mobile/src/components/ErrorBanner.tsx+2-4
......@@ -3,10 +3,8 @@ import { View } from "react-native";
33import { AppText as Text } from "./AppText";
44export function ErrorBanner(props: { readonly message: string }) {
55 return (
6 <View className="rounded-2xl border border-rose-300/70 bg-rose-100/80 px-3.5 py-3 dark:border-rose-400/28 dark:bg-rose-500/12">
7 <Text className="font-t3-medium text-sm text-rose-700 dark:text-rose-300">
8 {props.message}
9 </Text>
6 <View className="rounded-2xl border border-adaptive-rose-300-a70-400-a28 bg-adaptive-rose-100-a80-500-a12 px-3.5 py-3">
7 <Text className="font-t3-medium text-sm text-adaptive-rose-700-300">{props.message}</Text>
108 </View>
119 );
1210}
apps/mobile/src/components/GlassSafeAreaView.tsx+1-12
......@@ -1,7 +1,6 @@
11import type { ReactNode } from "react";
22import { View, type StyleProp, type ViewStyle } from "react-native";
33import { useSafeAreaInsets } from "react-native-safe-area-context";
4import { useThemeColor } from "../lib/useThemeColor";
54
65import { GlassSurface } from "./GlassSurface";
76
......@@ -19,23 +18,13 @@ export function GlassSafeAreaView({
1918 style,
2019}: GlassSafeAreaViewProps) {
2120 const insets = useSafeAreaInsets();
22 const headerColor = useThemeColor("--color-header");
23 const headerBorderColor = useThemeColor("--color-header-border");
24 const glassTint = useThemeColor("--color-glass-tint");
2521 const headerPaddingTop = insets.top + 16;
26 const surfaceStyle = {
27 borderRadius: 0,
28 backgroundColor: headerColor,
29 borderBottomWidth: 1,
30 borderBottomColor: headerBorderColor,
31 } as const;
3222
3323 return (
34 <View style={[surfaceStyle, style]}>
24 <View className="border-b border-header-border bg-header" style={style}>
3525 <GlassSurface
3626 chrome="none"
3727 glassEffectStyle="regular"
38 tintColor={glassTint}
3928 style={{ borderRadius: 0, backgroundColor: "transparent" }}
4029 >
4130 <View
apps/mobile/src/components/GlassSurface.tsx+37-15
......@@ -2,22 +2,29 @@ import { GlassView, isGlassEffectAPIAvailable } from "expo-glass-effect";
22import type { ReactNode } from "react";
33import {
44 Platform,
5 useColorScheme,
56 View,
67 type ColorValue,
78 type StyleProp,
89 type ViewProps,
910 type ViewStyle,
1011} from "react-native";
11import { useThemeColor } from "../lib/useThemeColor";
12import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
12import { withUniwind } from "uniwind";
1313
14interface GlassSurfaceProps extends Omit<ViewProps, "className"> {
14import { cn } from "../lib/cn";
15
16const ThemedGlassView = withUniwind(GlassView);
17
18interface GlassSurfaceProps extends ViewProps {
1519 readonly children: ReactNode;
1620 readonly glassEffectStyle?: "clear" | "regular" | "none";
1721 readonly tintColor?: ColorValue;
22 readonly tintColorClassName?: string;
1823 readonly chrome?: "default" | "none";
1924 /** Styling used only when native Liquid Glass is unavailable. */
2025 readonly fallbackStyle?: StyleProp<ViewStyle>;
26 /** Uniwind styling used only when native Liquid Glass is unavailable. */
27 readonly fallbackClassName?: string;
2128}
2229
2330export function GlassSurface({
......@@ -25,22 +32,18 @@ export function GlassSurface({
2532 glassEffectStyle = "regular",
2633 chrome = "default",
2734 tintColor,
35 tintColorClassName,
2836 fallbackStyle,
37 fallbackClassName,
38 className,
2939 style,
3040 ...props
3141}: GlassSurfaceProps) {
32 const { themeAppearance } = useAppearancePreferences();
33 const isDarkMode = themeAppearance === "dark";
34 const borderColor = useThemeColor("--color-border");
35 const glassSurface = useThemeColor("--color-glass-surface");
36 const glassTint = useThemeColor("--color-glass-tint");
42 const isDarkMode = useColorScheme() === "dark";
3743 const supportsGlass = Platform.OS === "ios" && isGlassEffectAPIAvailable();
3844 const surfaceStyle: ViewStyle = {
3945 borderRadius: 32,
4046 overflow: "hidden",
41 borderWidth: chrome === "none" ? 0 : 1,
42 borderColor: chrome === "none" ? "transparent" : borderColor,
43 backgroundColor: chrome === "none" ? "transparent" : glassSurface,
4447 shadowColor: chrome === "none" ? "transparent" : "#000000",
4548 shadowOpacity: chrome === "none" ? 0 : isDarkMode ? 0.22 : 0.08,
4649 shadowRadius: chrome === "none" ? 0 : 28,
......@@ -59,20 +62,39 @@ export function GlassSurface({
5962
6063 if (supportsGlass) {
6164 return (
62 <GlassView
65 <ThemedGlassView
6366 {...props}
67 className={cn(
68 chrome === "none"
69 ? "border-0 border-transparent bg-transparent"
70 : "border border-border bg-glass-surface",
71 className,
72 )}
6473 glassEffectStyle={glassEffectStyle}
65 tintColor={String(tintColor ?? glassTint)}
74 tintColor={tintColor === undefined ? undefined : String(tintColor)}
75 tintColorClassName={
76 tintColorClassName ?? (tintColor === undefined ? "accent-glass-tint" : undefined)
77 }
6678 colorScheme={isDarkMode ? "dark" : "light"}
6779 style={[surfaceStyle, style]}
6880 >
6981 {children}
70 </GlassView>
82 </ThemedGlassView>
7183 );
7284 }
7385
7486 return (
75 <View {...props} style={[surfaceStyle, fallbackStyle, style]}>
87 <View
88 {...props}
89 className={cn(
90 chrome === "none"
91 ? "border-0 border-transparent bg-transparent"
92 : "border border-border bg-glass-surface",
93 fallbackClassName,
94 className,
95 )}
96 style={[surfaceStyle, fallbackStyle, style]}
97 >
7698 {children}
7799 </View>
78100 );
apps/mobile/src/components/LoadingScreen.tsx+1-7
......@@ -1,6 +1,5 @@
11import { ActivityIndicator, StatusBar, View } from "react-native";
22import { useSafeAreaInsets } from "react-native-safe-area-context";
3import { useThemeColor } from "../lib/useThemeColor";
43import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
54
65import { AppText as Text } from "./AppText";
......@@ -11,17 +10,12 @@ export function LoadingScreen(props: {
1110 readonly messagePlacement?: "above-spinner" | "below-spinner";
1211}) {
1312 const { themeAppearance: colorScheme } = useAppearancePreferences();
14 const screenBg = useThemeColor("--color-screen");
1513 const insets = useSafeAreaInsets();
1614 const messagePlacement = props.messagePlacement ?? "below-spinner";
1715
1816 return (
1917 <View className="flex-1 bg-screen" style={{ paddingTop: insets.top }}>
20 <StatusBar
21 barStyle={colorScheme === "dark" ? "light-content" : "dark-content"}
22 backgroundColor={screenBg as string}
23 translucent
24 />
18 <StatusBar barStyle={colorScheme === "dark" ? "light-content" : "dark-content"} translucent />
2519 <View className="flex-1 items-center justify-center gap-5 px-6">
2620 <BrandMark compact />
2721 {messagePlacement === "above-spinner" ? (
apps/mobile/src/components/PierreEntryIcon.tsx+8-3
......@@ -3,7 +3,6 @@ import { Image, type ImageStyle, type StyleProp } from "react-native";
33
44import { markdownFileIconSource } from "@t3tools/mobile-markdown-text/file-icons";
55import { resolveMarkdownFileIcon } from "@t3tools/mobile-markdown-text/links";
6import { useThemeColor } from "../lib/useThemeColor";
76
87export function PierreEntryIcon(props: {
98 readonly path: string;
......@@ -12,9 +11,15 @@ export function PierreEntryIcon(props: {
1211 readonly style?: StyleProp<ImageStyle>;
1312}) {
1413 const size = props.size ?? 16;
15 const folderColor = useThemeColor("--color-icon-subtle");
1614 if (props.kind === "directory") {
17 return <SymbolView name="folder" size={size} tintColor={folderColor} type="monochrome" />;
15 return (
16 <SymbolView
17 name="folder"
18 size={size}
19 tintColorClassName={"accent-icon-subtle"}
20 type="monochrome"
21 />
22 );
1823 }
1924
2025 return (
apps/mobile/src/components/ProjectFavicon.tsx+1-3
......@@ -7,7 +7,6 @@ import {
77 getProjectFaviconCacheKey,
88 isProjectFaviconFallbackUrl,
99} from "@t3tools/shared/projectFavicon";
10import { useThemeColor } from "../lib/useThemeColor";
1110import { useAssetUrl } from "../state/assets";
1211import {
1312 beginProjectFaviconRequest,
......@@ -62,7 +61,6 @@ function ProjectFaviconImage(props: {
6261 readonly projectTitle: string;
6362 readonly size: number;
6463}) {
65 const iconMuted = useThemeColor("--color-icon-subtle");
6664 const faviconRequest = useMemo(
6765 () => createProjectFaviconRequest(props.cacheKey, props.faviconUrl),
6866 [props.cacheKey, props.faviconUrl],
......@@ -97,7 +95,7 @@ function ProjectFaviconImage(props: {
9795 <SymbolView
9896 name={{ ios: "folder.fill", android: props.open ? "folder_open" : "folder" }}
9997 size={props.size * 0.78}
100 tintColor={iconMuted}
98 tintColorClassName={"accent-icon-subtle"}
10199 type="monochrome"
102100 />
103101 ) : null}
apps/mobile/src/components/SourceControlIcon.tsx+14-3
......@@ -1,4 +1,7 @@
11import Svg, { Defs, LinearGradient, Path, Stop } from "react-native-svg";
2import { withUniwind } from "uniwind";
3
4const ThemedSvg = withUniwind(Svg);
25
36export type SourceControlIconKind = "github" | "gitlab" | "bitbucket" | "azure-devops";
47
......@@ -6,20 +9,28 @@ export function SourceControlIcon(props: {
69 readonly kind: SourceControlIconKind;
710 readonly size?: number;
811 readonly color?: string;
12 readonly colorClassName?: string;
913}) {
1014 const size = props.size ?? 18;
1115
1216 switch (props.kind) {
1317 case "github":
1418 return (
15 <Svg width={size} height={size} viewBox="0 0 16 16" fill="none">
19 <ThemedSvg
20 width={size}
21 height={size}
22 viewBox="0 0 16 16"
23 color={props.color}
24 colorClassName={props.colorClassName}
25 fill="none"
26 >
1627 <Path
1728 fillRule="evenodd"
1829 clipRule="evenodd"
1930 d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82A7.68 7.68 0 0 1 8.02 3.86c.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z"
20 fill={props.color ?? "#24292F"}
31 fill="currentColor"
2132 />
22 </Svg>
33 </ThemedSvg>
2334 );
2435 case "gitlab":
2536 return (
apps/mobile/src/components/T3Wordmark.tsx+12-3
......@@ -1,11 +1,18 @@
11import type { ColorValue } from "react-native";
22import Svg, { Path } from "react-native-svg";
3import { withUniwind } from "uniwind";
4
5const ThemedPath = withUniwind(Path);
36
47/**
58 * The "T3" brand mark, matching the desktop sidebar's T3Wordmark SVG
69 * (apps/web Sidebar.tsx). Width derives from the viewBox aspect ratio.
710 */
8export function T3Wordmark(props: { readonly height: number; readonly color: ColorValue }) {
11export function T3Wordmark(props: {
12 readonly height: number;
13 readonly color?: ColorValue;
14 readonly colorClassName?: string;
15}) {
916 const aspectRatio = 94.3941 / 56.96;
1017 return (
1118 <Svg
......@@ -14,9 +21,11 @@ export function T3Wordmark(props: { readonly height: number; readonly color: Col
1421 width={props.height * aspectRatio}
1522 viewBox="15.5309 37 94.3941 56.96"
1623 >
17 <Path
24 <ThemedPath
1825 d="M33.4509 93V47.56H15.5309V37H64.3309V47.56H46.4109V93H33.4509ZM86.7253 93.96C82.832 93.96 78.9653 93.4533 75.1253 92.44C71.2853 91.3733 68.032 89.88 65.3653 87.96L70.4053 78.04C72.5386 79.5867 75.0186 80.8133 77.8453 81.72C80.672 82.6267 83.5253 83.08 86.4053 83.08C89.6586 83.08 92.2186 82.44 94.0853 81.16C95.952 79.88 96.8853 78.12 96.8853 75.88C96.8853 73.7467 96.0586 72.0667 94.4053 70.84C92.752 69.6133 90.0853 69 86.4053 69H80.4853V60.44L96.0853 42.76L97.5253 47.4H68.1653V37H107.365V45.4L91.8453 63.08L85.2853 59.32H89.0453C95.9253 59.32 101.125 60.8667 104.645 63.96C108.165 67.0533 109.925 71.0267 109.925 75.88C109.925 79.0267 109.099 81.9867 107.445 84.76C105.792 87.48 103.259 89.6933 99.8453 91.4C96.432 93.1067 92.0586 93.96 86.7253 93.96Z"
19 fill={props.color}
26 color={props.color}
27 colorClassName={props.colorClassName}
28 fill="currentColor"
2029 />
2130 </Svg>
2231 );
apps/mobile/src/components/ThemedSwitch.tsx+9-11
......@@ -1,21 +1,19 @@
11import { Platform, Switch, type SwitchProps } from "react-native";
22
3import { useThemeColor } from "../lib/useThemeColor";
4
53export function ThemedSwitch(props: SwitchProps) {
6 const activeTrack = String(useThemeColor("--color-switch-active-track"));
7 const inactiveTrack = String(useThemeColor("--color-switch-inactive-track"));
8 const activeThumb = String(useThemeColor("--color-switch-active-thumb"));
9 const inactiveThumb = String(useThemeColor("--color-switch-inactive-thumb"));
10
114 return (
125 <Switch
136 {...props}
14 ios_backgroundColor={inactiveTrack}
15 thumbColor={
16 Platform.OS === "android" ? (props.value ? activeThumb : inactiveThumb) : undefined
7 ios_backgroundColorClassName="accent-switch-inactive-track"
8 thumbColorClassName={
9 Platform.OS === "android"
10 ? props.value
11 ? "accent-switch-active-thumb"
12 : "accent-switch-inactive-thumb"
13 : undefined
1714 }
18 trackColor={{ false: inactiveTrack, true: activeTrack }}
15 trackColorOffClassName="accent-switch-inactive-track"
16 trackColorOnClassName="accent-switch-active-track"
1917 />
2018 );
2119}
apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx+16-20
......@@ -27,7 +27,7 @@ import { EmptyState } from "../../components/EmptyState";
2727import { ProjectFavicon } from "../../components/ProjectFavicon";
2828import { useSafeAreaInsets } from "react-native-safe-area-context";
2929import { relativeTime } from "../../lib/time";
30import { useThemeColor } from "../../lib/useThemeColor";
30import { useUniwindTheme } from "../../lib/useUniwindTheme";
3131import { ThreadSwipeable } from "../home/thread-swipe-actions";
3232import {
3333 createNativeMailSearchToolbarItem,
......@@ -70,8 +70,6 @@ function ArchivedThreadsHeader(props: {
7070 const navigation = useNavigation();
7171 const insets = useSafeAreaInsets();
7272 const hasCustomFilter = props.selectedEnvironmentId !== null || props.sortOrder !== "newest";
73 const searchIconColor = useThemeColor("--color-icon");
74 const searchTextColor = useThemeColor("--color-foreground");
7573 const usesNativeChrome = Platform.OS === "ios";
7674 const usesCompactMailToolbar =
7775 Platform.OS === "ios" && width < 700 && NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED;
......@@ -154,7 +152,7 @@ function ArchivedThreadsHeader(props: {
154152 <SymbolView
155153 name="chevron.left"
156154 size={24}
157 tintColor={searchTextColor}
155 tintColorClassName={"accent-foreground"}
158156 type="monochrome"
159157 />
160158 </Pressable>
......@@ -162,7 +160,7 @@ function ArchivedThreadsHeader(props: {
162160 <SymbolView
163161 name="magnifyingglass"
164162 size={17}
165 tintColor={searchIconColor}
163 tintColorClassName={"accent-icon"}
166164 type="monochrome"
167165 />
168166 <TextInput
......@@ -192,7 +190,7 @@ function ArchivedThreadsHeader(props: {
192190 : "line.3.horizontal.decrease.circle"
193191 }
194192 size={16}
195 tintColor={searchIconColor}
193 tintColorClassName={"accent-icon"}
196194 type="monochrome"
197195 />
198196 </Pressable>
......@@ -402,9 +400,7 @@ function ArchivedThreadRow(props: {
402400 readonly thread: EnvironmentThreadShell;
403401}) {
404402 const { width: windowWidth } = useWindowDimensions();
405 const cardColor = useThemeColor("--color-card");
406 const iconColor = useThemeColor("--color-icon-subtle");
407 const separatorColor = useThemeColor("--color-separator");
403 const cardColor = useUniwindTheme()["--color-card"];
408404 const timestamp = relativeTime(props.thread.archivedAt ?? props.thread.updatedAt);
409405 const subtitle = [props.environmentLabel, props.thread.branch].filter((part): part is string =>
410406 Boolean(part),
......@@ -436,14 +432,15 @@ function ArchivedThreadRow(props: {
436432 >
437433 {() => (
438434 <View
439 className="flex-row items-center gap-3 bg-card px-4 py-3"
440 style={{
441 borderBottomColor: separatorColor,
442 borderBottomWidth: props.isLast ? 0 : 1,
443 }}
435 className={`flex-row items-center gap-3 bg-card px-4 py-3 ${props.isLast ? "" : "border-b border-separator"}`}
444436 >
445437 <View className="h-[34px] w-[34px] items-center justify-center rounded-[11px] bg-subtle">
446 <SymbolView name="archivebox.fill" size={15} tintColor={iconColor} type="monochrome" />
438 <SymbolView
439 name="archivebox.fill"
440 size={15}
441 tintColorClassName={"accent-icon-subtle"}
442 type="monochrome"
443 />
447444 </View>
448445
449446 <View className="min-w-0 flex-1 gap-1">
......@@ -463,7 +460,7 @@ function ArchivedThreadRow(props: {
463460 <SymbolView
464461 name="arrow.triangle.branch"
465462 size={10}
466 tintColor={iconColor}
463 tintColorClassName={"accent-icon-subtle"}
467464 type="monochrome"
468465 />
469466 <Text
......@@ -513,7 +510,6 @@ export function ArchivedThreadsScreen(props: {
513510 const { onDeleteThread, onUnarchiveThread } = props;
514511 const openSwipeableRef = useRef<SwipeableMethods | null>(null);
515512 const archiveScrollGesture = useMemo(() => Gesture.Native(), []);
516 const refreshTint = useThemeColor("--color-icon");
517513 const environmentLabelsById = useMemo(
518514 () =>
519515 new Map(
......@@ -594,7 +590,7 @@ export function ArchivedThreadsScreen(props: {
594590 if (isInitialLoad) {
595591 return (
596592 <View className="items-center py-16">
597 <ActivityIndicator color={refreshTint} />
593 <ActivityIndicator colorClassName={"accent-icon"} />
598594 <Text className="mt-3 text-sm text-foreground-muted">Loading archive...</Text>
599595 </View>
600596 );
......@@ -610,7 +606,7 @@ export function ArchivedThreadsScreen(props: {
610606 title={isFiltered ? "No matching threads" : "No archived threads"}
611607 />
612608 );
613 }, [isFiltered, isInitialLoad, refreshTint]);
609 }, [isFiltered, isInitialLoad]);
614610
615611 return (
616612 <View className="flex-1 bg-sheet">
......@@ -649,7 +645,7 @@ export function ArchivedThreadsScreen(props: {
649645 <RefreshControl
650646 onRefresh={props.onRefresh}
651647 refreshing={props.isLoading && !isInitialLoad}
652 tintColor={String(refreshTint)}
648 tintColorClassName={String("accent-icon")}
653649 />
654650 }
655651 renderItem={renderListItem}
apps/mobile/src/features/connection/CloudEnvironmentRows.tsx+11-11
......@@ -18,7 +18,6 @@ import { AppText as Text } from "../../components/AppText";
1818import { ThemedSwitch } from "../../components/ThemedSwitch";
1919import { cn } from "../../lib/cn";
2020import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic";
21import { useThemeColor } from "../../lib/useThemeColor";
2221import type { ConnectedEnvironmentSummary } from "../../state/remote-runtime-types";
2322import { availableCloudEnvironmentPresentation } from "../cloud/cloudEnvironmentPresentation";
2423import { hasCloudPublicConfig } from "../cloud/publicConfig";
......@@ -78,7 +77,6 @@ function CloudEnvironmentRowsContent(
7877 props: CloudEnvironmentRowsProps & { readonly discoveryAvailable?: boolean },
7978) {
8079 const controller = useConnectionController();
81 const iconColor = useThemeColor("--color-icon");
8280 const discoveryAvailable = props.discoveryAvailable ?? true;
8381 const availableCloudEnvironments = discoveryAvailable
8482 ? (props.showcaseAvailableEnvironments ?? controller.availableRelayEnvironments)
......@@ -118,12 +116,12 @@ function CloudEnvironmentRowsContent(
118116 className="h-9 w-9 items-center justify-center rounded-full bg-subtle active:opacity-70 disabled:opacity-50"
119117 >
120118 {controller.relayDiscovery.isRefreshing ? (
121 <ActivityIndicator color={iconColor} size="small" />
119 <ActivityIndicator colorClassName={"accent-icon"} size="small" />
122120 ) : (
123121 <SymbolView
124122 name="arrow.clockwise"
125123 size={14}
126 tintColor={iconColor}
124 tintColorClassName={"accent-icon"}
127125 type="monochrome"
128126 />
129127 )}
......@@ -158,7 +156,7 @@ function CloudEnvironmentRowsContent(
158156 </View>
159157 ) : controller.relayDiscovery.isRefreshing ? (
160158 <View collapsable={false} className="items-center gap-3 rounded-[24px] bg-card p-6">
161 <ActivityIndicator color={iconColor} />
159 <ActivityIndicator colorClassName={"accent-icon"} />
162160 <Text className="text-center text-sm leading-normal text-foreground-muted">
163161 Loading linked cloud environments.
164162 </Text>
......@@ -275,7 +273,6 @@ function CloudEnvironmentRowShell(props: {
275273 readonly statusText?: string;
276274 readonly value: boolean;
277275}) {
278 const chevron = useThemeColor("--color-chevron");
279276 const isRetrying =
280277 props.connectionState === "connecting" || props.connectionState === "reconnecting";
281278 const shouldPulse = isRetrying;
......@@ -287,7 +284,7 @@ function CloudEnvironmentRowShell(props: {
287284 traceId: props.connectionErrorTraceId,
288285 });
289286 const statusClassName = props.connectionError
290 ? "text-rose-500 dark:text-rose-400"
287 ? "text-adaptive-rose-500-400"
291288 : "text-foreground-muted";
292289 const [errorMeasurement, setErrorMeasurement] = useState<{
293290 readonly text: string;
......@@ -377,7 +374,7 @@ function CloudEnvironmentRowShell(props: {
377374 <SymbolView
378375 name="chevron.down"
379376 size={10}
380 tintColor={chevron}
377 tintColorClassName={"accent-chevron"}
381378 type="monochrome"
382379 style={{
383380 marginTop: 3,
......@@ -397,8 +394,6 @@ function CloudEnvironmentRowShell(props: {
397394}
398395
399396function CopyTraceIdButton(props: { readonly traceId: string }) {
400 const iconColor = useThemeColor("--color-icon");
401
402397 return (
403398 <Pressable
404399 accessibilityRole="button"
......@@ -407,7 +402,12 @@ function CopyTraceIdButton(props: { readonly traceId: string }) {
407402 }}
408403 className="self-start flex-row items-center gap-1.5 rounded-full bg-subtle px-3 py-2 active:opacity-70"
409404 >
410 <SymbolView name="doc.on.doc" size={12} tintColor={iconColor} type="monochrome" />
405 <SymbolView
406 name="doc.on.doc"
407 size={12}
408 tintColorClassName={"accent-icon"}
409 type="monochrome"
410 />
411411 <Text className="text-xs font-t3-bold text-foreground">Copy trace ID</Text>
412412 </Pressable>
413413 );
apps/mobile/src/features/connection/ConnectionEnvironmentRow.tsx+15-10
......@@ -7,7 +7,6 @@ import { AsyncResult } from "effect/unstable/reactivity";
77import { useCallback, useState } from "react";
88import { Alert, Pressable, View } from "react-native";
99import Animated, { FadeIn, FadeOut, LinearTransition } from "react-native-reanimated";
10import { useThemeColor } from "../../lib/useThemeColor";
1110
1211import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText";
1312import { cn } from "../../lib/cn";
......@@ -36,10 +35,6 @@ export function ConnectionEnvironmentRow(props: {
3635}) {
3736 const [label, setLabel] = useState(props.environment.environmentLabel);
3837 const [url, setUrl] = useState(props.environment.displayUrl);
39
40 const mutedColor = useThemeColor("--color-icon-subtle");
41 const primaryFg = useThemeColor("--color-primary-foreground");
42 const dangerFg = useThemeColor("--color-danger-foreground");
4338 const statusLabel = connectionStatusLabel(props.environment);
4439 const statusTraceId = props.environment.connectionErrorTraceId;
4540 const hasConnectionFailure = props.environment.connectionError !== null;
......@@ -85,7 +80,7 @@ export function ConnectionEnvironmentRow(props: {
8580 <Text
8681 className={cn(
8782 "text-xs",
88 hasConnectionFailure ? "text-rose-500 dark:text-rose-400" : "text-foreground-muted",
83 hasConnectionFailure ? "text-adaptive-rose-500-400" : "text-foreground-muted",
8984 )}
9085 numberOfLines={props.expanded ? undefined : 1}
9186 selectable={props.expanded}
......@@ -117,7 +112,7 @@ export function ConnectionEnvironmentRow(props: {
117112 <SymbolView
118113 name="chevron.down"
119114 size={12}
120 tintColor={mutedColor}
115 tintColorClassName={"accent-icon-subtle"}
121116 type="monochrome"
122117 style={{
123118 transform: [{ rotate: props.expanded ? "180deg" : "0deg" }],
......@@ -174,7 +169,12 @@ export function ConnectionEnvironmentRow(props: {
174169 className="min-h-[42px] flex-1 flex-row items-center justify-center gap-1.5 rounded-[14px] bg-primary px-3.5 py-2.5 active:opacity-70"
175170 onPress={handleSave}
176171 >
177 <SymbolView name="checkmark" size={13} tintColor={primaryFg} type="monochrome" />
172 <SymbolView
173 name="checkmark"
174 size={13}
175 tintColorClassName={"accent-primary-foreground"}
176 type="monochrome"
177 />
178178 <Text className="text-xs font-t3-bold tracking-[0.8px] uppercase text-primary-foreground">
179179 Save
180180 </Text>
......@@ -188,7 +188,7 @@ export function ConnectionEnvironmentRow(props: {
188188 <SymbolView
189189 name="arrow.clockwise"
190190 size={14}
191 tintColor={mutedColor}
191 tintColorClassName={"accent-icon-subtle"}
192192 type="monochrome"
193193 />
194194 </Pressable>
......@@ -197,7 +197,12 @@ export function ConnectionEnvironmentRow(props: {
197197 className="h-[42px] w-[42px] items-center justify-center rounded-[14px] border border-danger-border bg-danger active:opacity-70"
198198 onPress={() => props.onRemove(props.environment.environmentId)}
199199 >
200 <SymbolView name="trash" size={14} tintColor={dangerFg} type="monochrome" />
200 <SymbolView
201 name="trash"
202 size={14}
203 tintColorClassName={"accent-danger-foreground"}
204 type="monochrome"
205 />
201206 </Pressable>
202207 </View>
203208 </Animated.View>
apps/mobile/src/features/connection/ConnectionSheetButton.tsx+7-7
......@@ -1,6 +1,5 @@
11import { SymbolView } from "../../components/AppSymbol";
22import { Platform, Pressable } from "react-native";
3import { useThemeColor } from "../../lib/useThemeColor";
43
54import { AppText as Text } from "../../components/AppText";
65import { cn } from "../../lib/cn";
......@@ -37,11 +36,12 @@ export function ConnectionSheetButton(props: {
3736}) {
3837 const tone = props.tone ?? "secondary";
3938
40 const primaryFg = useThemeColor("--color-primary-foreground");
41 const dangerFg = useThemeColor("--color-danger-foreground");
42 const secondaryFg = useThemeColor("--color-secondary-foreground");
43
44 const textColor = tone === "primary" ? primaryFg : tone === "danger" ? dangerFg : secondaryFg;
39 const textColorClassName =
40 tone === "primary"
41 ? "accent-primary-foreground"
42 : tone === "danger"
43 ? "accent-danger-foreground"
44 : "accent-secondary-foreground";
4545
4646 const primaryShadow =
4747 tone === "primary"
......@@ -79,7 +79,7 @@ export function ConnectionSheetButton(props: {
7979 <SymbolView
8080 name={props.icon}
8181 size={props.compact ? 13 : 14}
82 tintColor={textColor}
82 tintColorClassName={textColorClassName}
8383 type="monochrome"
8484 />
8585 <Text
apps/mobile/src/features/connection/ConnectionsNewRouteScreen.tsx+2-2
......@@ -5,7 +5,7 @@ import { AsyncResult } from "effect/unstable/reactivity";
55import { useCallback, useEffect, useRef, useState } from "react";
66import { Alert, Linking, Platform, ScrollView, View } from "react-native";
77import { useSafeAreaInsets } from "react-native-safe-area-context";
8import { useThemeColor } from "../../lib/useThemeColor";
8import { useUniwindTheme } from "../../lib/useUniwindTheme";
99
1010import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";
1111import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText";
......@@ -47,7 +47,7 @@ export function ConnectionsNewRouteScreen({
4747 const [scannerLocked, setScannerLocked] = useState(false);
4848 const attemptedAutoConnectRef = useRef<string | null>(null);
4949
50 const headerIconColor = useThemeColor("--color-icon");
50 const headerIconColor = useUniwindTheme()["--color-icon"];
5151
5252 const connectDisabled = isSubmitting || hostInput.trim().length === 0;
5353
apps/mobile/src/features/connection/ConnectionsRouteScreen.tsx+1-5
......@@ -5,7 +5,6 @@ import type { EnvironmentId } from "@t3tools/contracts";
55import { useCallback, useState } from "react";
66import { Platform, ScrollView, View } from "react-native";
77import { useSafeAreaInsets } from "react-native-safe-area-context";
8import { useThemeColor } from "../../lib/useThemeColor";
98
109import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";
1110import { AppText as Text } from "../../components/AppText";
......@@ -24,9 +23,6 @@ export function ConnectionsRouteScreen() {
2423 const insets = useSafeAreaInsets();
2524 const hasEnvironments = connectedEnvironments.length > 0;
2625 const [expandedId, setExpandedId] = useState<EnvironmentId | null>(null);
27
28 const accentColor = useThemeColor("--color-icon-muted");
29
3026 const handleToggle = useCallback((environmentId: EnvironmentId) => {
3127 setExpandedId((prev) => (prev === environmentId ? null : environmentId));
3228 }, []);
......@@ -89,7 +85,7 @@ export function ConnectionsRouteScreen() {
8985 <SymbolView
9086 name="point.3.connected.trianglepath.dotted"
9187 size={20}
92 tintColor={accentColor}
88 tintColorClassName={"accent-icon-muted"}
9389 type="monochrome"
9490 />
9591 </View>
apps/mobile/src/features/connection/EnvironmentConnectionNotice.tsx+2-4
......@@ -7,7 +7,6 @@ import { ActivityIndicator, Pressable, View } from "react-native";
77
88import { AppText as Text } from "../../components/AppText";
99import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic";
10import { useThemeColor } from "../../lib/useThemeColor";
1110
1211function noticeTitle(phase: EnvironmentConnectionPhase, environmentLabel: string): string {
1312 switch (phase) {
......@@ -55,7 +54,6 @@ export function EnvironmentConnectionNotice(props: {
5554 readonly resourceName: string;
5655 readonly onRetry: () => void;
5756}) {
58 const iconColor = String(useThemeColor("--color-icon-muted"));
5957 const isRetrying =
6058 props.connection.phase === "connecting" || props.connection.phase === "reconnecting";
6159
......@@ -63,12 +61,12 @@ export function EnvironmentConnectionNotice(props: {
6361 <View className="flex-1 items-center justify-center px-8">
6462 <View className="max-w-[320px] items-center gap-3">
6563 {isRetrying ? (
66 <ActivityIndicator size="small" color={iconColor} />
64 <ActivityIndicator size="small" colorClassName={"accent-icon-muted"} />
6765 ) : (
6866 <SymbolView
6967 name={props.connection.phase === "offline" ? "wifi.slash" : "bolt.horizontal.circle"}
7068 size={24}
71 tintColor={iconColor}
69 tintColorClassName={"accent-icon-muted"}
7270 type="monochrome"
7371 />
7472 )}
apps/mobile/src/features/connection/connectionTone.ts+12-12
......@@ -6,38 +6,38 @@ export function connectionTone(state: RemoteClientConnectionState): StatusTone {
66 case "connected":
77 return {
88 label: "Connected",
9 pillClassName: "bg-emerald-500/12 dark:bg-emerald-500/16",
10 textClassName: "text-emerald-700 dark:text-emerald-300",
9 pillClassName: "bg-adaptive-emerald-500-a12-a16",
10 textClassName: "text-adaptive-emerald-700-300",
1111 };
1212 case "reconnecting":
1313 return {
1414 label: "Reconnecting",
15 pillClassName: "bg-amber-500/12 dark:bg-amber-500/16",
16 textClassName: "text-amber-700 dark:text-amber-300",
15 pillClassName: "bg-adaptive-amber-500-a12-a16",
16 textClassName: "text-adaptive-amber-700-300",
1717 };
1818 case "connecting":
1919 return {
2020 label: "Connecting",
21 pillClassName: "bg-sky-500/12 dark:bg-sky-500/16",
22 textClassName: "text-sky-700 dark:text-sky-300",
21 pillClassName: "bg-adaptive-sky-500-a12-a16",
22 textClassName: "text-adaptive-sky-700-300",
2323 };
2424 case "error":
2525 return {
2626 label: "Connection failed",
27 pillClassName: "bg-rose-500/12 dark:bg-rose-500/16",
28 textClassName: "text-rose-700 dark:text-rose-300",
27 pillClassName: "bg-adaptive-rose-500-a12-a16",
28 textClassName: "text-adaptive-rose-700-300",
2929 };
3030 case "offline":
3131 return {
3232 label: "Offline",
33 pillClassName: "bg-rose-500/12 dark:bg-rose-500/16",
34 textClassName: "text-rose-700 dark:text-rose-300",
33 pillClassName: "bg-adaptive-rose-500-a12-a16",
34 textClassName: "text-adaptive-rose-700-300",
3535 };
3636 case "available":
3737 return {
3838 label: "Available",
39 pillClassName: "bg-neutral-500/10 dark:bg-neutral-500/16",
40 textClassName: "text-neutral-600 dark:text-neutral-300",
39 pillClassName: "bg-adaptive-neutral-500-a10-a16",
40 textClassName: "text-adaptive-neutral-600-300",
4141 };
4242 }
4343}
apps/mobile/src/features/files/FileMarkdownPreview.tsx+10-9
......@@ -13,7 +13,7 @@ import {
1313 resolveMarkdownFontSizes,
1414 resolveNativeMarkdownTypography,
1515} from "../../lib/appearancePreferences";
16import { useThemeColor } from "../../lib/useThemeColor";
16import { useUniwindTheme } from "../../lib/useUniwindTheme";
1717import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
1818import {
1919 hasNativeSelectableMarkdownText,
......@@ -38,14 +38,15 @@ function useMarkdownPreviewStyles(): MarkdownPreviewStyles {
3838 () => resolveNativeMarkdownTypography(appearance.baseFontSize),
3939 [appearance.baseFontSize],
4040 );
41 const body = String(useThemeColor("--color-md-body"));
42 const strong = String(useThemeColor("--color-md-strong"));
43 const link = String(useThemeColor("--color-md-link"));
44 const blockquoteBorder = String(useThemeColor("--color-md-blockquote-border"));
45 const blockquoteBackground = String(useThemeColor("--color-md-blockquote-bg"));
46 const codeBackground = String(useThemeColor("--color-md-code-bg"));
47 const codeText = String(useThemeColor("--color-md-code-text"));
48 const horizontalRule = String(useThemeColor("--color-md-hr"));
41 const theme = useUniwindTheme();
42 const body = theme["--color-md-body"];
43 const strong = theme["--color-md-strong"];
44 const link = theme["--color-md-link"];
45 const blockquoteBorder = theme["--color-md-blockquote-border"];
46 const blockquoteBackground = theme["--color-md-blockquote-bg"];
47 const codeBackground = theme["--color-md-code-bg"];
48 const codeText = theme["--color-md-code-text"];
49 const horizontalRule = theme["--color-md-hr"];
4950 const regularFontFamily = useFontFamily("regular");
5051 const mediumFontFamily = useFontFamily("medium");
5152 const boldFontFamily = useFontFamily("bold");
apps/mobile/src/features/files/FileTreeBrowser.tsx+2-6
......@@ -7,7 +7,6 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
77import { AppText as Text } from "../../components/AppText";
88import { PierreEntryIcon } from "../../components/PierreEntryIcon";
99import { cn } from "../../lib/cn";
10import { useThemeColor } from "../../lib/useThemeColor";
1110import { IOS_NAV_BAR_HEIGHT } from "../../lib/layoutMetrics";
1211import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
1312import {
......@@ -46,7 +45,6 @@ const FileTreeRow = memo(function FileTreeRow(props: {
4645 readonly item: VisibleFileTreeNode;
4746 readonly selected: boolean;
4847 readonly expanded: boolean;
49 readonly iconColor: string;
5048 readonly onPressDirectory: (path: string) => void;
5149 readonly onPreviewFile?: (path: string) => void;
5250 readonly onPressFile: (path: string) => void;
......@@ -79,7 +77,7 @@ const FileTreeRow = memo(function FileTreeRow(props: {
7977 <SymbolView
8078 name={props.expanded ? "chevron.down" : "chevron.right"}
8179 size={12}
82 tintColor={props.iconColor}
80 tintColorClassName="accent-icon-muted"
8381 type="monochrome"
8482 />
8583 ) : (
......@@ -125,7 +123,6 @@ export function FileTreeBrowser(props: {
125123 // Native transparent-header height ≈ safe-area top + nav bar (~44). Matches the
126124 // observed adjustedContentInset bottom (~102) seen in the native trace.
127125 const headerInset = NATIVE_LIQUID_GLASS_SUPPORTED ? insets.top + IOS_NAV_BAR_HEIGHT : 0;
128 const iconColor = String(useThemeColor("--color-icon-muted"));
129126 const { onPreviewFile, onSelectFile, selectedPath: controlledSelectedPath } = props;
130127 const controlledSelectedPathRef = useRef(controlledSelectedPath);
131128 const pendingSelectionTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
......@@ -216,13 +213,12 @@ export function FileTreeBrowser(props: {
216213 item={item}
217214 selected={item.node.kind === "file" && item.node.path === selectedPath}
218215 expanded={expandedPaths.has(item.node.path)}
219 iconColor={iconColor}
220216 onPressDirectory={toggleDirectory}
221217 onPreviewFile={onPreviewFile}
222218 onPressFile={handleSelectFile}
223219 />
224220 ),
225 [expandedPaths, handleSelectFile, iconColor, onPreviewFile, selectedPath, toggleDirectory],
221 [expandedPaths, handleSelectFile, onPreviewFile, selectedPath, toggleDirectory],
226222 );
227223
228224 if (props.error && props.entries.length === 0) {
apps/mobile/src/features/files/SourceFileSurface.tsx+4-2
......@@ -17,6 +17,7 @@ import { cn } from "../../lib/cn";
1717import type { ResolvedMobileCodeSurface } from "../../lib/appearancePreferences";
1818import { useAppearanceCodeSurface } from "../settings/appearance/useAppearanceCodeSurface";
1919import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
20import { useUniwindTheme } from "../../lib/useUniwindTheme";
2021import {
2122 buildNativeSourceTokens,
2223 NATIVE_SOURCE_CONTENT_WIDTH,
......@@ -153,6 +154,7 @@ function NativeSourceFileSurface(
153154 const { NativeView, onRefresh } = props;
154155 const { codeSurface, codeWordBreak, nativeSourceStyle } = useAppearanceCodeSurface();
155156 const { themeAppearance, themeId } = useAppearancePreferences();
157 const appTheme = useUniwindTheme();
156158 const { width: viewportWidth } = useWindowDimensions();
157159 const { rowsJson, status, targetIndex, tokens } = useSourceFileModel(props);
158160 const [isPullRefreshing, setIsPullRefreshing] = useState(false);
......@@ -173,8 +175,8 @@ function NativeSourceFileSurface(
173175 [targetIndex],
174176 );
175177 const themeJson = useMemo(
176 () => JSON.stringify(createNativeReviewDiffTheme(themeAppearance, themeId)),
177 [themeAppearance, themeId],
178 () => JSON.stringify(createNativeReviewDiffTheme(themeAppearance, themeId, appTheme)),
179 [appTheme, themeAppearance, themeId],
178180 );
179181 const styleJson = useMemo(() => JSON.stringify(nativeSourceStyle), [nativeSourceStyle]);
180182 const contentWidth = codeWordBreak
apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx+14-9
......@@ -19,7 +19,7 @@ import { LoadingScreen } from "../../components/LoadingScreen";
1919import { resolveFileSelectionNavigationAction } from "../../lib/adaptive-navigation";
2020import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic";
2121import { tryOpenExternalUrl } from "../../lib/openExternalUrl";
22import { useThemeColor } from "../../lib/useThemeColor";
22import { useUniwindTheme } from "../../lib/useUniwindTheme";
2323import { useThreadSelection } from "../../state/use-thread-selection";
2424import { useSelectedThreadWorktree } from "../../state/use-selected-thread-worktree";
2525import { useEnvironmentQuery } from "../../state/query";
......@@ -135,11 +135,11 @@ function FileContent(props: {
135135 return (
136136 <View className="flex-1 bg-sheet">
137137 {props.truncated ? (
138 <View className="border-b border-amber-200 bg-amber-50 px-4 py-2 dark:border-amber-900/60 dark:bg-amber-950/40">
139 <Text className="text-2xs font-t3-bold uppercase text-amber-700 dark:text-amber-300">
138 <View className="border-b border-adaptive-amber-200-900-a60 bg-adaptive-amber-50-950-a40 px-4 py-2">
139 <Text className="text-2xs font-t3-bold uppercase text-adaptive-amber-700-300">
140140 Partial file
141141 </Text>
142 <Text className="text-xs leading-snug text-amber-800 dark:text-amber-200">
142 <Text className="text-xs leading-snug text-adaptive-amber-800-200">
143143 Preview limited to the first 1 MB of a truncated file.
144144 </Text>
145145 </View>
......@@ -210,7 +210,7 @@ function FilesUnavailable() {
210210}
211211
212212function FilesToolbarBottomFade() {
213 const sheetColor = String(useThemeColor("--color-sheet"));
213 const sheetColor = String(useUniwindTheme()["--color-sheet"]);
214214
215215 if (process.env.EXPO_OS !== "ios") {
216216 return null;
......@@ -245,8 +245,8 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {
245245 const [searchQuery, setSearchQuery] = useState("");
246246 const isAndroid = Platform.OS === "android";
247247 const { themeAppearance: highlightTheme } = useAppearancePreferences();
248 const iconColor = String(useThemeColor("--color-icon-muted"));
249 const sheetSurfaceColor = String(useThemeColor("--color-sheet-solid"));
248 const theme = useUniwindTheme();
249 const sheetSurfaceColor = theme["--color-sheet-solid"];
250250 const { cwd, environmentId, projectName, selectedThread, threadId } = useThreadFilesWorkspace(
251251 props.route.params,
252252 );
......@@ -413,7 +413,12 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {
413413 ]}
414414 />
415415 <View className="flex-row items-center gap-2 border-b border-border px-3 py-2">
416 <SymbolView name="magnifyingglass" size={17} tintColor={iconColor} type="monochrome" />
416 <SymbolView
417 name="magnifyingglass"
418 size={17}
419 tintColorClassName={"accent-icon-muted"}
420 type="monochrome"
421 />
417422 <TextInput
418423 accessibilityLabel="Search files"
419424 autoCapitalize="none"
......@@ -467,7 +472,7 @@ export function ThreadFileScreen(props: ThreadFileRouteScreenProps) {
467472 useAdaptiveWorkspacePaneRole("inspector");
468473 const navigation = useNavigation();
469474 const { fileInspector, panes, toggleAuxiliaryPane } = useAdaptiveWorkspaceLayout();
470 const iconColor = useThemeColor("--color-icon");
475 const iconColor = useUniwindTheme()["--color-icon"];
471476 const params = props.route.params;
472477 const relativePath = normalizeRoutePath(params.path);
473478 const targetLine = normalizeRouteLine(firstRouteParam(params.line));
apps/mobile/src/features/files/thread-file-navigator-pane.tsx+16-6
......@@ -12,7 +12,7 @@ import {
1212
1313import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText";
1414import { nativeHeaderScrollEdgeEffects } from "../../native/StackHeader";
15import { useThemeColor } from "../../lib/useThemeColor";
15import { useUniwindTheme } from "../../lib/useUniwindTheme";
1616import { projectEnvironment } from "../../state/projects";
1717import { useEnvironmentQuery } from "../../state/query";
1818import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
......@@ -29,9 +29,9 @@ export function ThreadFileNavigatorPane(props: {
2929}) {
3030 const [searchQuery, setSearchQuery] = useState("");
3131 const { themeAppearance: highlightTheme } = useAppearancePreferences();
32 const iconColor = String(useThemeColor("--color-icon-muted"));
33 const foregroundColor = String(useThemeColor("--color-foreground"));
34 const sheetColor = String(useThemeColor("--color-sheet"));
32 const theme = useUniwindTheme();
33 const foregroundColor = theme["--color-foreground"];
34 const sheetColor = theme["--color-sheet"];
3535 const headerScrollEdgeEffects = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version);
3636 const entriesQuery = useEnvironmentQuery(
3737 projectEnvironment.listEntries({
......@@ -152,11 +152,21 @@ export function ThreadFileNavigatorPane(props: {
152152 className="h-8 w-8 items-center justify-center rounded-full active:bg-subtle"
153153 onPress={entriesQuery.refresh}
154154 >
155 <SymbolView name="arrow.clockwise" size={14} tintColor={iconColor} type="monochrome" />
155 <SymbolView
156 name="arrow.clockwise"
157 size={14}
158 tintColorClassName={"accent-icon-muted"}
159 type="monochrome"
160 />
156161 </Pressable>
157162 </View>
158163 <View className="flex-row items-center gap-2 border-t border-border px-3 py-2">
159 <SymbolView name="magnifyingglass" size={15} tintColor={iconColor} type="monochrome" />
164 <SymbolView
165 name="magnifyingglass"
166 size={15}
167 tintColorClassName={"accent-icon-muted"}
168 type="monochrome"
169 />
160170 <TextInput
161171 accessibilityLabel="Search files"
162172 autoCapitalize="none"
apps/mobile/src/features/home/AndroidHomeFab.tsx+1-4
......@@ -3,7 +3,6 @@ import { Platform, Pressable, View } from "react-native";
33import { useSafeAreaInsets } from "react-native-safe-area-context";
44
55import { SymbolView } from "../../components/AppSymbol";
6import { useThemeColor } from "../../lib/useThemeColor";
76
87/**
98 * Android-only wrapper that overlays a bottom-right new-task FAB on a thread
......@@ -25,8 +24,6 @@ function AndroidHomeFab(props: {
2524 readonly children: ReactNode;
2625}) {
2726 const insets = useSafeAreaInsets();
28 const primaryForegroundColor = useThemeColor("--color-primary-foreground");
29
3027 return (
3128 <View className="flex-1">
3229 {props.children}
......@@ -42,7 +39,7 @@ function AndroidHomeFab(props: {
4239 <SymbolView
4340 name="square.and.pencil"
4441 size={22}
45 tintColor={primaryForegroundColor}
42 tintColorClassName={"accent-primary-foreground"}
4643 type="monochrome"
4744 />
4845 </Pressable>
apps/mobile/src/features/home/HomeHeader.tsx+17-9
......@@ -12,7 +12,7 @@ import { SymbolView } from "../../components/AppSymbol";
1212import { T3Wordmark } from "../../components/T3Wordmark";
1313import { HOME_HORIZONTAL_INSET } from "../../lib/layoutMetrics";
1414import { resolveMobileStageLabel } from "../../lib/mobileBranding";
15import { useThemeColor } from "../../lib/useThemeColor";
15import { useUniwindTheme } from "../../lib/useUniwindTheme";
1616import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled";
1717import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands";
1818import { withNativeGlassHeaderItem } from "../layout/native-glass-header-items";
......@@ -67,8 +67,6 @@ function checkedMenuState(checked: boolean) {
6767
6868function AndroidHomeHeader(props: HomeHeaderProps) {
6969 const insets = useSafeAreaInsets();
70 const iconColor = useThemeColor("--color-icon");
71 const mutedColor = useThemeColor("--color-foreground-muted");
7270 const stageLabel = resolveMobileStageLabel(Constants.expoConfig?.extra?.appVariant);
7371 // Thread List v2 lays the list out in fixed creation order, so the
7472 // sort/group filter controls would be silently ignored — hide them and
......@@ -218,7 +216,7 @@ function AndroidHomeHeader(props: HomeHeaderProps) {
218216 brand={
219217 <View className="flex-row items-center gap-2">
220218 {/* Mirrors the desktop SidebarBrand: T3 mark + muted "Code". */}
221 <T3Wordmark color={iconColor} height={15} />
219 <T3Wordmark colorClassName="accent-icon" height={15} />
222220 <RNText className="-ml-0.5 text-[21px] font-t3-medium tracking-[-0.5px] text-foreground-muted">
223221 Code
224222 </RNText>
......@@ -248,7 +246,7 @@ function AndroidHomeHeader(props: HomeHeaderProps) {
248246 : "line.3.horizontal.decrease.circle"
249247 }
250248 size={16}
251 tintColor={iconColor}
249 tintColorClassName={"accent-icon"}
252250 type="monochrome"
253251 />
254252 </Pressable>
......@@ -262,12 +260,22 @@ function AndroidHomeHeader(props: HomeHeaderProps) {
262260 onPress={props.onOpenSettings}
263261 className="size-11 items-center justify-center rounded-full bg-subtle"
264262 >
265 <SymbolView name="gearshape" size={18} tintColor={iconColor} type="monochrome" />
263 <SymbolView
264 name="gearshape"
265 size={18}
266 tintColorClassName={"accent-icon"}
267 type="monochrome"
268 />
266269 </Pressable>
267270 </View>
268271
269272 <View className="min-h-12 flex-row items-center gap-2.5 rounded-2xl border border-input-border bg-input px-3.5">
270 <SymbolView name="magnifyingglass" size={17} tintColor={mutedColor} type="monochrome" />
273 <SymbolView
274 name="magnifyingglass"
275 size={17}
276 tintColorClassName={"accent-foreground-muted"}
277 type="monochrome"
278 />
271279 <TextInput
272280 accessibilityLabel="Search threads"
273281 autoCapitalize="none"
......@@ -286,7 +294,7 @@ function AndroidHomeHeader(props: HomeHeaderProps) {
286294 <SymbolView
287295 name="xmark.circle.fill"
288296 size={17}
289 tintColor={mutedColor}
297 tintColorClassName={"accent-foreground-muted"}
290298 type="monochrome"
291299 />
292300 </Pressable>
......@@ -300,7 +308,7 @@ function AndroidHomeHeader(props: HomeHeaderProps) {
300308
301309function IosHomeHeader(props: HomeHeaderProps) {
302310 const searchBarRef = useRef<SearchBarCommands>(null);
303 const iconColor = useThemeColor("--color-icon");
311 const iconColor = useUniwindTheme()["--color-icon"];
304312 // Thread List v2 lays the list out in fixed creation order, so the
305313 // sort/group filter controls would be silently ignored — hide them and
306314 // key the "customized" icon state off the environment filter alone.
apps/mobile/src/features/home/HomeScreen.tsx+1-3
......@@ -23,7 +23,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2323import { ActivityIndicator, FlatList, Platform, Pressable, View } from "react-native";
2424import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable";
2525import { useSafeAreaInsets } from "react-native-safe-area-context";
26import { useThemeColor } from "../../lib/useThemeColor";
2726
2827import { AppText as Text } from "../../components/AppText";
2928import { EmptyState } from "../../components/EmptyState";
......@@ -216,7 +215,6 @@ export function HomeScreen(props: HomeScreenProps) {
216215 const openSwipeableRef = useRef<SwipeableMethods | null>(null);
217216 const listRef = useRef<LegendListRef | null>(null);
218217 const insets = useSafeAreaInsets();
219 const accentColor = useThemeColor("--color-icon-muted");
220218 const iosBottomToolbarClearance =
221219 Platform.OS === "ios" && !NATIVE_LIQUID_GLASS_SUPPORTED
222220 ? PRE_LIQUID_GLASS_BOTTOM_TOOLBAR_HEIGHT
......@@ -1088,7 +1086,7 @@ export function HomeScreen(props: HomeScreenProps) {
10881086 />
10891087 {emptyState.loading ? (
10901088 <View className="mt-4 items-center">
1091 <ActivityIndicator color={accentColor} />
1089 <ActivityIndicator colorClassName={"accent-icon-muted"} />
10921090 </View>
10931091 ) : null}
10941092 </View>
apps/mobile/src/features/home/WorkspaceConnectionTitle.tsx+2-4
......@@ -8,7 +8,6 @@ import { ActivityIndicator, Animated, Platform, Pressable, View } from "react-na
88import { SymbolView } from "../../components/AppSymbol";
99import { AppText as Text } from "../../components/AppText";
1010import { brandTitleOffset, CompactBrandTitle } from "../../components/CompactBrandTitle";
11import { useThemeColor } from "../../lib/useThemeColor";
1211import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
1312import { useWorkspaceState } from "../../state/workspace";
1413import {
......@@ -98,7 +97,6 @@ export function WorkspaceConnectionTitle(props: {
9897 /** Horizontal correction so the status aligns with the brand in native title slots. */
9998 readonly statusOffset?: number;
10099}) {
101 const iconColor = String(useThemeColor("--color-icon-muted"));
102100 const status = useDelayedConnectionStatus();
103101 const size = props.size ?? "navbar";
104102
......@@ -125,12 +123,12 @@ export function WorkspaceConnectionTitle(props: {
125123 style={{ marginLeft: props.statusOffset ?? 0 }}
126124 >
127125 {status.showsProgress ? (
128 <ActivityIndicator color={iconColor} size="small" />
126 <ActivityIndicator colorClassName={"accent-icon-muted"} size="small" />
129127 ) : (
130128 <SymbolView
131129 name="wifi.slash"
132130 size={size === "pageTitle" ? 17 : 15}
133 tintColor={iconColor}
131 tintColorClassName={"accent-icon-muted"}
134132 type="monochrome"
135133 />
136134 )}
apps/mobile/src/features/layout/WorkspaceEmptyDetail.tsx+6-4
......@@ -2,15 +2,17 @@ import { SymbolView } from "../../components/AppSymbol";
22import { Pressable, View } from "react-native";
33
44import { AppText as Text } from "../../components/AppText";
5import { useThemeColor } from "../../lib/useThemeColor";
65
76export function WorkspaceEmptyDetail(props: { readonly onStartNewTask?: () => void }) {
8 const iconColor = useThemeColor("--color-icon-subtle");
9
107 return (
118 <View className="flex-1 items-center justify-center bg-screen px-10">
129 <View className="max-w-[360px] items-center gap-3">
13 <SymbolView name="sidebar.left" size={34} tintColor={iconColor} type="hierarchical" />
10 <SymbolView
11 name="sidebar.left"
12 size={34}
13 tintColorClassName={"accent-icon-subtle"}
14 type="hierarchical"
15 />
1416 <Text className="text-center text-xl font-t3-bold">Select a thread</Text>
1517 <Text className="text-center text-base text-foreground-muted">
1618 Choose a thread from the sidebar or start a new task.
apps/mobile/src/features/layout/workspace-pane-divider.tsx+6-10
......@@ -2,7 +2,7 @@ import { useCallback, useMemo, useRef, useState } from "react";
22import { Pressable, StyleSheet, View, type AccessibilityActionEvent } from "react-native";
33import { Gesture, GestureDetector } from "react-native-gesture-handler";
44import { runOnJS } from "react-native-reanimated";
5import { useThemeColor } from "../../lib/useThemeColor";
5import { cn } from "../../lib/cn";
66
77const ACCESSIBILITY_RESIZE_STEP = 24;
88
......@@ -22,8 +22,6 @@ export function WorkspacePaneDivider(props: WorkspacePaneDividerProps) {
2222 latestProps.current = props;
2323 const [hovered, setHovered] = useState(false);
2424 const [dragging, setDragging] = useState(false);
25 const dividerColor = useThemeColor("--color-border");
26 const activeDividerColor = useThemeColor("--color-primary");
2725 const handleResizeStart = useCallback(() => {
2826 setDragging(true);
2927 latestProps.current.onResizeStart?.();
......@@ -81,11 +79,11 @@ export function WorkspacePaneDivider(props: WorkspacePaneDividerProps) {
8179 onHoverOut={() => setHovered(false)}
8280 >
8381 <View
84 style={[
85 styles.line,
86 { backgroundColor: dividerColor },
87 (hovered || dragging) && [styles.activeLine, { backgroundColor: activeDividerColor }],
88 ]}
82 className={cn(
83 "h-full self-center bg-border opacity-70",
84 hovered || dragging ? "w-0.5 bg-primary opacity-100" : "w-px",
85 )}
86 style={[styles.line, (hovered || dragging) && styles.activeLine]}
8987 />
9088 </Pressable>
9189 </GestureDetector>
......@@ -96,11 +94,9 @@ const styles = StyleSheet.create({
9694 line: {
9795 alignSelf: "center",
9896 height: "100%",
99 opacity: 0.7,
10097 width: StyleSheet.hairlineWidth,
10198 },
10299 activeLine: {
103 opacity: 1,
104100 width: 2,
105101 },
106102});
apps/mobile/src/features/projects/AddProjectScreen.tsx+25-20
......@@ -51,7 +51,6 @@ import { sourceControlEnvironment } from "../../state/sourceControl";
5151import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText";
5252import { ErrorBanner } from "../../components/ErrorBanner";
5353import { SourceControlIcon } from "../../components/SourceControlIcon";
54import { useThemeColor } from "../../lib/useThemeColor";
5554import { uuidv4 } from "../../lib/uuid";
5655import { useAtomCommand } from "../../state/use-atom-command";
5756import { useAtomQueryRunner } from "../../state/use-atom-query-runner";
......@@ -159,8 +158,6 @@ function ListRow(props: {
159158 readonly right?: ReactNode;
160159 readonly onPress?: () => void;
161160}) {
162 const chevronColor = useThemeColor("--color-chevron");
163
164161 return (
165162 <Pressable
166163 disabled={props.disabled}
......@@ -192,7 +189,12 @@ function ListRow(props: {
192189 {"right" in props ? (
193190 props.right
194191 ) : !props.disabled ? (
195 <SymbolView name="chevron.right" size={13} tintColor={chevronColor} type="monochrome" />
192 <SymbolView
193 name="chevron.right"
194 size={13}
195 tintColorClassName={"accent-chevron"}
196 type="monochrome"
197 />
196198 ) : null}
197199 </View>
198200 </Pressable>
......@@ -205,8 +207,6 @@ function PrimaryActionButton(props: {
205207 readonly loading?: boolean;
206208 readonly onPress: () => void;
207209}) {
208 const primaryForeground = useThemeColor("--color-primary-foreground");
209
210210 return (
211211 <Pressable
212212 disabled={props.disabled}
......@@ -214,7 +214,7 @@ function PrimaryActionButton(props: {
214214 className="h-12 items-center justify-center rounded-full bg-primary active:opacity-70 disabled:opacity-45"
215215 >
216216 {props.loading ? (
217 <ActivityIndicator color={String(primaryForeground)} />
217 <ActivityIndicator colorClassName={String("accent-primary-foreground")} />
218218 ) : (
219219 <Text className="text-base font-t3-bold text-primary-foreground">{props.label}</Text>
220220 )}
......@@ -414,7 +414,6 @@ function SourceControlRow(props: {
414414 readonly isFirst: boolean;
415415}) {
416416 const navigation = useNavigation();
417 const iconColor = useThemeColor("--color-icon");
418417 const title =
419418 props.source === "url" ? "Git URL" : `${addProjectRemoteSourceLabel(props.source)} repository`;
420419 const subtitle =
......@@ -423,9 +422,9 @@ function SourceControlRow(props: {
423422 : `Clone ${addProjectRemoteSourceLabel(props.source)} ${props.hint}`;
424423 const icon =
425424 props.source === "url" ? (
426 <SymbolView name="link" size={17} tintColor={iconColor} type="monochrome" />
425 <SymbolView name="link" size={17} tintColorClassName={"accent-icon"} type="monochrome" />
427426 ) : (
428 <SourceControlIcon kind={props.source} size={18} color={String(iconColor)} />
427 <SourceControlIcon kind={props.source} size={18} colorClassName="accent-icon" />
429428 );
430429
431430 if (!props.ready) {
......@@ -454,8 +453,6 @@ function SourceControlRow(props: {
454453
455454export function AddProjectSourceScreen() {
456455 const navigation = useNavigation();
457 const accentColor = useThemeColor("--color-icon-muted");
458 const iconColor = useThemeColor("--color-icon");
459456 const { environmentOptions, selectedEnvironment, setSelectedEnvironmentId } =
460457 useSelectedEnvironment();
461458 const discoveryState = useEnvironmentQuery(
......@@ -496,7 +493,7 @@ export function AddProjectSourceScreen() {
496493 <SymbolView
497494 name="server.rack"
498495 size={17}
499 tintColor={iconColor}
496 tintColorClassName={"accent-icon"}
500497 type="monochrome"
501498 />
502499 }
......@@ -508,7 +505,7 @@ export function AddProjectSourceScreen() {
508505 <SymbolView
509506 name="checkmark"
510507 size={14}
511 tintColor={iconColor}
508 tintColorClassName={"accent-icon"}
512509 type="monochrome"
513510 />
514511 ) : null
......@@ -530,7 +527,7 @@ export function AddProjectSourceScreen() {
530527 <SymbolView
531528 name="folder.badge.plus"
532529 size={17}
533 tintColor={iconColor}
530 tintColorClassName={"accent-icon"}
534531 type="monochrome"
535532 />
536533 }
......@@ -560,7 +557,9 @@ export function AddProjectSourceScreen() {
560557 ),
561558 )}
562559 </ListSection>
563 {discoveryState.isPending ? <ActivityIndicator color={accentColor} /> : null}
560 {discoveryState.isPending ? (
561 <ActivityIndicator colorClassName={"accent-icon-muted"} />
562 ) : null}
564563 </>
565564 ) : null}
566565 </AddProjectShell>
......@@ -745,7 +744,6 @@ function FolderBrowser(props: {
745744 }) => Promise<boolean>;
746745 readonly pinnedDirectoryName?: string;
747746}) {
748 const accentColor = useThemeColor("--color-icon-muted");
749747 const browsePath = useMemo(
750748 () => getFilesystemBrowsePath(props.pathInput, props.environment.platform),
751749 [props.environment.platform, props.pathInput],
......@@ -781,7 +779,7 @@ function FolderBrowser(props: {
781779 <ListSection>
782780 {browseState.isPending && browseState.data === null ? (
783781 <View className="items-center py-5">
784 <ActivityIndicator color={accentColor} />
782 <ActivityIndicator colorClassName={"accent-icon-muted"} />
785783 </View>
786784 ) : null}
787785 {browsePath.canBrowseUp ? (
......@@ -791,7 +789,7 @@ function FolderBrowser(props: {
791789 <SymbolView
792790 name="arrow.turn.left.up"
793791 size={17}
794 tintColor={accentColor}
792 tintColorClassName={"accent-icon-muted"}
795793 type="monochrome"
796794 />
797795 }
......@@ -810,7 +808,14 @@ function FolderBrowser(props: {
810808 <ListRow
811809 key={entry.fullPath}
812810 title={entry.name}
813 icon={<SymbolView name="folder" size={17} tintColor={accentColor} type="monochrome" />}
811 icon={
812 <SymbolView
813 name="folder"
814 size={17}
815 tintColorClassName={"accent-icon-muted"}
816 type="monochrome"
817 />
818 }
814819 isFirst={index === 0 && !browsePath.canBrowseUp}
815820 right={null}
816821 onPress={() => {
apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx+6-3
......@@ -14,7 +14,6 @@ import { ControlPill } from "../../components/ControlPill";
1414import { cn } from "../../lib/cn";
1515import type { DraftComposerImageAttachment } from "../../lib/composerImages";
1616import { convertPastedImagesToAttachments, pickComposerImages } from "../../lib/composerImages";
17import { useThemeColor } from "../../lib/useThemeColor";
1817import { useNativePaste } from "../../lib/useNativePaste";
1918import { setPendingConnectionError } from "../../state/use-remote-environment-registry";
2019import { appendReviewCommentToDraft } from "../../state/use-thread-composer-state";
......@@ -46,7 +45,6 @@ export function ReviewCommentComposerSheet(props: ReviewCommentComposerSheetProp
4645 const insets = useSafeAreaInsets();
4746 const { width } = useWindowDimensions();
4847 const { themeAppearance: selectedTheme } = useAppearancePreferences();
49 const iconTint = String(useThemeColor("--color-icon"));
5048 const target = useReviewCommentTarget();
5149 const { codeSurface } = useAppearanceCodeSurface();
5250 const { environmentId, threadId } = props.route.params;
......@@ -168,7 +166,12 @@ export function ReviewCommentComposerSheet(props: ReviewCommentComposerSheetProp
168166 className="bg-subtle h-12 w-12 items-center justify-center rounded-full"
169167 onPress={dismissComposer}
170168 >
171 <SymbolView name="xmark" size={18} tintColor={iconTint} type="monochrome" />
169 <SymbolView
170 name="xmark"
171 size={18}
172 tintColorClassName={"accent-icon"}
173 type="monochrome"
174 />
172175 </Pressable>
173176
174177 <Text className="text-lg font-t3-bold text-foreground">Add Comment</Text>
apps/mobile/src/features/review/ReviewSheet.tsx+15-12
......@@ -37,7 +37,7 @@ import { ControlPillMenu } from "../../components/ControlPill";
3737import { environmentCatalog } from "../../connection/catalog";
3838import { useEnvironmentPresentation } from "../../state/presentation";
3939import { useAtomCommand } from "../../state/use-atom-command";
40import { useThemeColor } from "../../lib/useThemeColor";
40import { useUniwindTheme } from "../../lib/useUniwindTheme";
4141import { IOS_NAV_BAR_HEIGHT } from "../../lib/layoutMetrics";
4242import { useThreadDraftForThread } from "../../state/use-thread-composer-state";
4343import { EnvironmentConnectionNotice } from "../connection/EnvironmentConnectionNotice";
......@@ -80,13 +80,11 @@ const SHOWCASE_ENABLED = process.env.EXPO_PUBLIC_SHOWCASE === "1";
8080
8181const ReviewNotice = memo(function ReviewNotice(props: { readonly notice: string }) {
8282 return (
83 <View className="border-b border-amber-200 bg-amber-50 px-4 py-3 dark:border-amber-900/60 dark:bg-amber-950/40">
84 <Text className="text-xs font-t3-bold uppercase text-amber-700 dark:text-amber-300">
83 <View className="border-b border-adaptive-amber-200-900-a60 bg-adaptive-amber-50-950-a40 px-4 py-3">
84 <Text className="text-xs font-t3-bold uppercase text-adaptive-amber-700-300">
8585 Partial diff
8686 </Text>
87 <Text className="text-xs leading-normal text-amber-800 dark:text-amber-200">
88 {props.notice}
89 </Text>
87 <Text className="text-xs leading-normal text-adaptive-amber-800-200">{props.notice}</Text>
9088 </View>
9189 );
9290});
......@@ -97,7 +95,6 @@ function ReviewSelectionActionBar(props: {
9795 readonly onOpenComment: (() => void) | null;
9896 readonly onClear: () => void;
9997}) {
100 const foreground = useThemeColor("--color-primary-foreground");
10198 if (!props.title) {
10299 return null;
103100 }
......@@ -107,7 +104,7 @@ function ReviewSelectionActionBar(props: {
107104 <SymbolView
108105 name={props.onOpenComment ? "text.bubble" : "line.3.horizontal.decrease.circle"}
109106 size={16}
110 tintColor={foreground}
107 tintColorClassName={"accent-primary-foreground"}
111108 type="monochrome"
112109 />
113110 <Text className="text-base font-t3-bold text-primary-foreground">{props.title}</Text>
......@@ -144,7 +141,12 @@ function ReviewSelectionActionBar(props: {
144141 className="h-12 w-12 items-center justify-center rounded-full bg-primary"
145142 onPress={props.onClear}
146143 >
147 <SymbolView name="xmark" size={16} tintColor={foreground} type="monochrome" />
144 <SymbolView
145 name="xmark"
146 size={16}
147 tintColorClassName={"accent-primary-foreground"}
148 type="monochrome"
149 />
148150 </Pressable>
149151 </View>
150152 );
......@@ -217,8 +219,9 @@ function ReviewFileNavigator({
217219 ref,
218220}: ReviewFileNavigatorProps) {
219221 const insets = useSafeAreaInsets();
220 const sheetColor = String(useThemeColor("--color-sheet"));
221 const foregroundColor = String(useThemeColor("--color-foreground"));
222 const theme = useUniwindTheme();
223 const sheetColor = theme["--color-sheet"];
224 const foregroundColor = theme["--color-foreground"];
222225 const headerScrollEdgeEffects = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version);
223226 const [fileSelection, setFileSelection] = useState<{
224227 readonly sectionId: string | null;
......@@ -348,7 +351,7 @@ export function ReviewSheet(props: ReviewSheetProps) {
348351 const navigation = useNavigation();
349352 const insets = useSafeAreaInsets();
350353 const { themeAppearance: selectedTheme } = useAppearancePreferences();
351 const headerIcon = String(useThemeColor("--color-icon"));
354 const headerIcon = String(useUniwindTheme()["--color-icon"]);
352355 const { environmentId, threadId } = props.route.params;
353356 const environment = useEnvironmentPresentation(environmentId);
354357 const retryEnvironment = useAtomCommand(environmentCatalog.retryNow, "environment retry");
apps/mobile/src/features/review/nativeReviewDiffAdapter.test.ts+21-4
......@@ -1,5 +1,12 @@
11import { describe, expect, it } from "vite-plus/test";
2import { MOBILE_THEME_IDS } from "../../lib/mobileTheme";
2import {
3 DEFAULT_MOBILE_THEME_ID,
4 getMobileThemeVariables,
5 MOBILE_THEME_IDS,
6 type MobileThemeAppearance,
7 type MobileThemeId,
8} from "../../lib/mobileTheme";
9import { readDefaultMobileThemeVariables } from "../../lib/mobileTheme.test-support";
310
411import {
512 createNativeReviewDiffTheme,
......@@ -39,6 +46,12 @@ function buildInput(comments: BuildNativeReviewDiffDataInput["comments"]) {
3946 return { parsedDiff, comments } satisfies BuildNativeReviewDiffDataInput;
4047}
4148
49function appTheme(themeId: MobileThemeId, appearance: MobileThemeAppearance) {
50 return themeId === DEFAULT_MOBILE_THEME_ID
51 ? readDefaultMobileThemeVariables(appearance)
52 : getMobileThemeVariables(themeId, appearance);
53}
54
4255describe("getCachedNativeReviewDiffData", () => {
4356 it("reuses the row model for equivalent empty comment arrays", () => {
4457 const first = getCachedNativeReviewDiffData(buildInput([]));
......@@ -61,7 +74,11 @@ describe("createNativeReviewDiffTheme", () => {
6174 it("serializes every native color as cross-platform opaque hex", () => {
6275 for (const themeId of MOBILE_THEME_IDS) {
6376 for (const appearance of ["light", "dark"] as const) {
64 const theme = createNativeReviewDiffTheme(appearance, themeId);
77 const theme = createNativeReviewDiffTheme(
78 appearance,
79 themeId,
80 appTheme(themeId, appearance),
81 );
6582 for (const color of Object.values(theme)) {
6683 expect(color, `${themeId}/${appearance}`).toMatch(/^#[\da-f]{6}$/i);
6784 }
......@@ -70,8 +87,8 @@ describe("createNativeReviewDiffTheme", () => {
7087 });
7188
7289 it("uses the selected app palette for native code surfaces", () => {
73 const standard = createNativeReviewDiffTheme("dark", "t3-code");
74 const iris = createNativeReviewDiffTheme("dark", "iris");
90 const standard = createNativeReviewDiffTheme("dark", "t3-code", appTheme("t3-code", "dark"));
91 const iris = createNativeReviewDiffTheme("dark", "iris", appTheme("iris", "dark"));
7592
7693 expect(iris.background).not.toBe(standard.background);
7794 expect(iris.hunkText).not.toBe(standard.hunkText);
apps/mobile/src/features/review/nativeReviewDiffAdapter.ts+3-7
......@@ -8,11 +8,7 @@ import { pipe } from "effect/Function";
88import type { ResolvedMobileCodeSurface } from "../../lib/appearancePreferences";
99import { resolveMobileCodeSurface } from "../../lib/appearancePreferences";
1010import { MOBILE_CODE_SURFACE } from "../../lib/typography";
11import {
12 DEFAULT_MOBILE_THEME_ID,
13 getMobileThemeVariables,
14 type MobileThemeId,
15} from "../../lib/mobileTheme";
11import { type MobileThemeId, type MobileThemeVariables } from "../../lib/mobileTheme";
1612import { getMobileTerminalTheme, type TerminalAppearanceScheme } from "../terminal/terminalTheme";
1713import { computeWordAltDiffRanges } from "./reviewWordDiffs";
1814import {
......@@ -137,10 +133,10 @@ function buildReviewCommentsCacheKey(comments: ReadonlyArray<ReviewInlineComment
137133
138134export function createNativeReviewDiffTheme(
139135 scheme: TerminalAppearanceScheme,
140 themeId: MobileThemeId = DEFAULT_MOBILE_THEME_ID,
136 themeId: MobileThemeId,
137 appTheme: MobileThemeVariables,
141138): NativeReviewDiffTheme {
142139 const terminalTheme = getMobileTerminalTheme(themeId, scheme);
143 const appTheme = getMobileThemeVariables(themeId, scheme);
144140 const [, terminalRed] = terminalTheme.palette;
145141 // Swift expects #RRGGBB/#RRGGBBAA while Android expects #RRGGBB/#AARRGGBB.
146142 // Flatten translucent app tokens onto the code surface so both native
apps/mobile/src/features/review/useNativeReviewDiffBridge.ts+6-1
......@@ -6,6 +6,7 @@ import { useAppearanceCodeSurface } from "../settings/appearance/useAppearanceCo
66import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
77import { useNativeReviewDiffHighlighting } from "./useNativeReviewDiffHighlighting";
88import { buildNativeReviewTokensResetKey } from "./reviewDiffBridgeKeys";
9import { useUniwindTheme } from "../../lib/useUniwindTheme";
910
1011export { buildNativeReviewTokensResetKey, hashReviewDiffKey } from "./reviewDiffBridgeKeys";
1112
......@@ -31,11 +32,15 @@ export function useNativeReviewDiffBridge(input: {
3132 } = input;
3233 const { nativeReviewDiffStyle } = useAppearanceCodeSurface();
3334 const { themeAppearance: scheme, themeId } = useAppearancePreferences();
35 const appTheme = useUniwindTheme();
3436 const [collapsedCommentIds, setCollapsedCommentIds] = useState<ReadonlySet<string>>(
3537 () => new Set(),
3638 );
3739
38 const theme = useMemo(() => createNativeReviewDiffTheme(scheme, themeId), [scheme, themeId]);
40 const theme = useMemo(
41 () => createNativeReviewDiffTheme(scheme, themeId, appTheme),
42 [appTheme, scheme, themeId],
43 );
3944 const rowsJson = useMemo(() => JSON.stringify(data.rows), [data.rows]);
4045 const collapsedFileIdsJson = useMemo(() => JSON.stringify(collapsedFileIds), [collapsedFileIds]);
4146 const viewedFileIdsJson = useMemo(() => JSON.stringify(viewedFileIds), [viewedFileIds]);
apps/mobile/src/features/settings/SettingsClientStorageRouteScreen.tsx+8-10
......@@ -1,12 +1,11 @@
11import { useAtomSet, useAtomValue } from "@effect/atom-react";
22import { AsyncResult } from "effect/unstable/reactivity";
3import { SymbolView } from "expo-symbols";
43import { useMemo } from "react";
54import { ActivityIndicator, Alert, Pressable, ScrollView, View } from "react-native";
65import { useSafeAreaInsets } from "react-native-safe-area-context";
76
87import { AppText as Text } from "../../components/AppText";
9import { useThemeColor } from "../../lib/useThemeColor";
8import { SymbolView } from "../../components/AppSymbol";
109import {
1110 clearClientCacheAtom,
1211 clientCacheSummaryAtom,
......@@ -17,8 +16,6 @@ import { SettingsSection } from "./components/SettingsSection";
1716
1817export function SettingsClientStorageRouteScreen() {
1918 const insets = useSafeAreaInsets();
20 const iconColor = useThemeColor("--color-icon");
21 const dangerForegroundColor = useThemeColor("--color-danger-foreground");
2219 const summaryResult = useAtomValue(clientCacheSummaryAtom);
2320 const clearResult = useAtomValue(clearClientCacheAtom);
2421 const clearCache = useAtomSet(clearClientCacheAtom);
......@@ -84,7 +81,7 @@ export function SettingsClientStorageRouteScreen() {
8481 <SymbolView
8582 name="exclamationmark.triangle"
8683 size={28}
87 tintColor={dangerForegroundColor}
84 tintColorClassName={"accent-danger-foreground"}
8885 type="monochrome"
8986 weight="regular"
9087 />
......@@ -119,7 +116,7 @@ export function SettingsClientStorageRouteScreen() {
119116 <SymbolView
120117 name="checkmark.circle"
121118 size={28}
122 tintColor={iconColor}
119 tintColorClassName={"accent-icon"}
123120 type="monochrome"
124121 weight="regular"
125122 />
......@@ -142,14 +139,16 @@ export function SettingsClientStorageRouteScreen() {
142139 <SymbolView
143140 name="trash"
144141 size={22}
145 tintColor={dangerForegroundColor}
142 tintColorClassName={"accent-danger-foreground"}
146143 type="monochrome"
147144 weight="regular"
148145 />
149146 <Text className="flex-1 text-lg tabular-nums text-danger-foreground">
150147 {summary ? `Clear ${formatBytes(summary.payloadBytes)}` : "Clear caches"}
151148 </Text>
152 {isClearing ? <ActivityIndicator color={dangerForegroundColor} /> : null}
149 {isClearing ? (
150 <ActivityIndicator colorClassName={"accent-danger-foreground"} />
151 ) : null}
153152 </Pressable>
154153 </SettingsSection>
155154 <Text className="px-2 text-sm leading-normal text-foreground-muted">
......@@ -174,7 +173,6 @@ function CacheEnvironmentRow(props: {
174173 readonly first: boolean;
175174 readonly onClear: () => void;
176175}) {
177 const iconColor = useThemeColor("--color-icon");
178176 return (
179177 <View
180178 className={
......@@ -186,7 +184,7 @@ function CacheEnvironmentRow(props: {
186184 <SymbolView
187185 name="desktopcomputer"
188186 size={22}
189 tintColor={iconColor}
187 tintColorClassName={"accent-icon"}
190188 type="monochrome"
191189 weight="regular"
192190 />
apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx+3-4
......@@ -12,7 +12,7 @@ import { CloudEnvironmentRows } from "../connection/CloudEnvironmentRows";
1212import { ConnectionEnvironmentRow } from "../connection/ConnectionEnvironmentRow";
1313import { splitEnvironmentSections } from "../connection/environmentSections";
1414import { cn } from "../../lib/cn";
15import { useThemeColor } from "../../lib/useThemeColor";
15import { useUniwindTheme } from "../../lib/useUniwindTheme";
1616import { useRemoteConnections } from "../../state/use-remote-environment-registry";
1717import {
1818 applyShowcaseLocalEnvironmentDisplayUrls,
......@@ -44,8 +44,7 @@ export function SettingsEnvironmentsRouteScreen() {
4444 : environmentSections.connectedCloudEnvironments;
4545 const hasLocalEnvironments = localEnvironments.length > 0;
4646 const [expandedId, setExpandedId] = useState<EnvironmentId | null>(null);
47 const accentColor = useThemeColor("--color-icon-muted");
48 const headerIconColor = useThemeColor("--color-icon");
47 const headerIconColor = useUniwindTheme()["--color-icon"];
4948
5049 const handleToggle = useCallback((environmentId: EnvironmentId) => {
5150 setExpandedId((prev) => (prev === environmentId ? null : environmentId));
......@@ -148,7 +147,7 @@ export function SettingsEnvironmentsRouteScreen() {
148147 <SymbolView
149148 name="point.3.connected.trianglepath.dotted"
150149 size={20}
151 tintColor={accentColor}
150 tintColorClassName={"accent-icon-muted"}
152151 type="monochrome"
153152 />
154153 </View>
apps/mobile/src/features/settings/SettingsProjectGroupingRouteScreen.tsx+1-3
......@@ -8,7 +8,6 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
88import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";
99import { AppText as Text } from "../../components/AppText";
1010import { SymbolView } from "../../components/AppSymbol";
11import { useThemeColor } from "../../lib/useThemeColor";
1211import { NativeStackScreenOptions } from "../../native/StackHeader";
1312import {
1413 mobileProjectGroupingModePatch,
......@@ -42,7 +41,6 @@ const GROUPING_OPTIONS: ReadonlyArray<{
4241export function SettingsProjectGroupingRouteScreen() {
4342 const navigation = useNavigation();
4443 const insets = useSafeAreaInsets();
45 const checkmarkColor = useThemeColor("--color-icon");
4644 const preferencesResult = useAtomValue(mobilePreferencesAtom);
4745 const savePreferences = useAtomSet(updateMobilePreferencesAtom);
4846 const preferencesReady = AsyncResult.isSuccess(preferencesResult) && !preferencesResult.waiting;
......@@ -92,7 +90,7 @@ export function SettingsProjectGroupingRouteScreen() {
9290 <SymbolView
9391 name="checkmark"
9492 size={18}
95 tintColor={checkmarkColor}
93 tintColorClassName={"accent-icon"}
9694 type="monochrome"
9795 weight="semibold"
9896 />
apps/mobile/src/features/settings/SettingsRouteScreen.tsx+1-3
......@@ -33,7 +33,6 @@ import { hasCloudPublicConfig, resolveRelayClerkTokenOptions } from "../cloud/pu
3333import { withNativeGlassHeaderItem } from "../layout/native-glass-header-items";
3434import { WorkspaceSidebarToolbar } from "../layout/workspace-sidebar-toolbar";
3535import { runtime } from "../../lib/runtime";
36import { useThemeColor } from "../../lib/useThemeColor";
3736import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences";
3837import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled";
3938import {
......@@ -585,7 +584,6 @@ function LegacySettingsSection() {
585584}
586585
587586function AppSettingsSection() {
588 const icon = useThemeColor("--color-icon");
589587 const [updateState, setUpdateState] = useState<AppUpdateCheckState>("idle");
590588 const updateInFlight = useRef(false);
591589 const hiddenUpdateTapCount = useRef(0);
......@@ -655,7 +653,7 @@ function AppSettingsSection() {
655653 <SymbolView
656654 name="info.circle"
657655 size={22}
658 tintColor={icon}
656 tintColorClassName={"accent-icon"}
659657 type="monochrome"
660658 weight="regular"
661659 />
apps/mobile/src/features/settings/appearance/AppearancePreferencesProvider.tsx+118-49
......@@ -1,15 +1,23 @@
1import { createContext, use, useCallback, useLayoutEffect, useMemo, type ReactNode } from "react";
2import { useColorScheme } from "react-native";
1import {
2 createContext,
3 startTransition,
4 use,
5 useCallback,
6 useLayoutEffect,
7 useMemo,
8 useRef,
9 type ReactNode,
10} from "react";
11import { Appearance, useColorScheme } from "react-native";
312
413import { useAtomSet, useAtomValue } from "@effect/atom-react";
514import { AsyncResult } from "effect/unstable/reactivity";
615
7import { Uniwind } from "uniwind";
16import { ScopedTheme, Uniwind } from "uniwind";
817
918import {
1019 resolveAppearance,
1120 resolveAppearancePreferences,
12 resolveTextScaleVariables,
1321 type ResolvedAppearance,
1422} from "../../../lib/appearancePreferences";
1523import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../../state/preferences";
......@@ -17,7 +25,6 @@ import type { Preferences } from "../../../persistence/mobile-preferences";
1725import {
1826 createMobileThemePairPatch,
1927 createMobileThemeSelectionPatch,
20 getMobileThemeVariables,
2128 normalizeMobileThemeMode,
2229 resolveMobileThemeIds,
2330 type MobileThemeAppearance,
......@@ -25,6 +32,11 @@ import {
2532 type MobileThemeIds,
2633 type MobileThemeMode,
2734} from "../../../lib/mobileTheme";
35import {
36 createMobileThemeRuntimeOperations,
37 getMobileUniwindThemeName,
38 type MobileThemeRuntimeState,
39} from "../../../lib/mobileThemeRuntime";
2840import { cacheTerminalFontSize } from "../../terminal/terminalUiState";
2941
3042interface AppearancePreferencesContextValue {
......@@ -51,30 +63,6 @@ interface AppearancePreferencesContextValue {
5163
5264const AppearancePreferencesContext = createContext<AppearancePreferencesContextValue | null>(null);
5365
54/**
55 * Injects palette and text-scale variables into both adaptive stylesheets.
56 * Updating the active sheet last lets the visible app settle in one pass.
57 */
58function applyAppearanceVariables(baseFontSize: number, themeIds: MobileThemeIds) {
59 const textVariables = resolveTextScaleVariables(baseFontSize);
60 const currentTheme = Uniwind.currentTheme;
61 const activeAppearance =
62 currentTheme === "light" || currentTheme === "dark" ? currentTheme : null;
63
64 for (const theme of ["light", "dark"] as const) {
65 const variables = { ...getMobileThemeVariables(themeIds[theme], theme), ...textVariables };
66 if (theme !== activeAppearance) {
67 Uniwind.updateCSSVariables(theme, variables);
68 }
69 }
70 if (activeAppearance !== null) {
71 Uniwind.updateCSSVariables(activeAppearance, {
72 ...getMobileThemeVariables(themeIds[activeAppearance], activeAppearance),
73 ...textVariables,
74 });
75 }
76}
77
7866export function AppearancePreferencesProvider(props: { readonly children: ReactNode }) {
7967 const preferencesResult = useAtomValue(mobilePreferencesAtom);
8068 const savePreferences = useAtomSet(updateMobilePreferencesAtom);
......@@ -88,54 +76,135 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN
8876 );
8977 const themeMode = normalizeMobileThemeMode(storedPreferences?.themeMode);
9078 const themeAppearance = themeMode === "system" ? systemColorScheme : themeMode;
91 const themeIds = useMemo(
92 () => resolveMobileThemeIds(storedPreferences ?? {}),
93 [storedPreferences],
79 const resolvedThemeIds = resolveMobileThemeIds(storedPreferences ?? {});
80 const themeIds = useMemo<MobileThemeIds>(
81 () => ({ light: resolvedThemeIds.light, dark: resolvedThemeIds.dark }),
82 [resolvedThemeIds.dark, resolvedThemeIds.light],
9483 );
9584 const themeId = themeIds[themeAppearance];
96 const isReady = AsyncResult.isSuccess(preferencesResult) && !preferencesResult.waiting;
85 const activeThemeName = getMobileUniwindThemeName(themeId, themeAppearance);
86 const { baseFontSize, codeFontSize, codeWordBreak, terminalFontSize } = preferences;
87 const appearance = useMemo(
88 () => resolveAppearance({ baseFontSize, codeFontSize, codeWordBreak, terminalFontSize }),
89 [baseFontSize, codeFontSize, codeWordBreak, terminalFontSize],
90 );
91 // Preference patches are optimistic. Keep controls interactive while a save is
92 // in flight so rapid theme choices can supersede one another immediately.
93 const isReady = AsyncResult.isSuccess(preferencesResult);
94 const runtimeState = useMemo<MobileThemeRuntimeState>(
95 () => ({
96 baseFontSize,
97 themeAppearance,
98 themeMode,
99 }),
100 [baseFontSize, themeAppearance, themeMode],
101 );
102 const appliedRuntimeStateRef = useRef<MobileThemeRuntimeState | null>(null);
103 const selectedThemeIdsRef = useRef(themeIds);
97104
98 useLayoutEffect(() => {
99 applyAppearanceVariables(preferences.baseFontSize, themeIds);
100 Uniwind.setTheme(themeMode);
101 cacheTerminalFontSize(resolveAppearance(preferences).terminalFontSize);
102 }, [preferences, themeIds, themeMode]);
105 const applyThemeRuntime = useCallback((next: MobileThemeRuntimeState) => {
106 const operations = createMobileThemeRuntimeOperations(appliedRuntimeStateRef.current, next);
107 for (const operation of operations) {
108 if (operation.kind === "update-text-variables") {
109 Uniwind.updateCSSVariables(operation.themeName, operation.variables);
110 continue;
111 }
112 if (operation.kind === "set-appearance-mode") {
113 Appearance.setColorScheme(
114 operation.themeMode === "system" ? "unspecified" : operation.appearance,
115 );
116 }
117 }
118 appliedRuntimeStateRef.current = next;
119 }, []);
120
121 const syncThemeRuntime = useCallback(
122 (next: MobileThemeRuntimeState) => applyThemeRuntime(next),
123 [applyThemeRuntime],
124 );
103125
104126 const updatePreferences = useCallback(
105127 (patch: Partial<Preferences>) => {
128 startTransition(() => savePreferences(patch));
129 },
130 [savePreferences],
131 );
132
133 const updateThemePreferences = useCallback(
134 (patch: Partial<Preferences>) => {
135 // Theme selection owns the visible root ScopedTheme value. Keep its
136 // optimistic atom update urgent so the first frame after a press is the
137 // complete new palette rather than a deferred transition render.
106138 savePreferences(patch);
107139 },
108140 [savePreferences],
109141 );
110142
143 useLayoutEffect(() => {
144 selectedThemeIdsRef.current = themeIds;
145 syncThemeRuntime(runtimeState);
146 cacheTerminalFontSize(appearance.terminalFontSize);
147 }, [appearance.terminalFontSize, runtimeState, syncThemeRuntime, themeIds]);
148
111149 const setThemeIdForAppearance = useCallback(
112150 (appearance: MobileThemeAppearance, value: MobileThemeId) => {
113 updatePreferences(
114 createMobileThemeSelectionPatch(themeIds, themeAppearance, appearance, value),
151 const patch = createMobileThemeSelectionPatch(
152 selectedThemeIdsRef.current,
153 themeAppearance,
154 appearance,
155 value,
115156 );
157 selectedThemeIdsRef.current = resolveMobileThemeIds(patch);
158 updateThemePreferences(patch);
116159 },
117 [themeAppearance, themeIds, updatePreferences],
160 [themeAppearance, updateThemePreferences],
118161 );
119162
120163 const setThemeIdForBothAppearances = useCallback(
121164 (value: MobileThemeId) => {
122 updatePreferences(createMobileThemePairPatch(value));
165 const patch = createMobileThemePairPatch(value);
166 selectedThemeIdsRef.current = resolveMobileThemeIds(patch);
167 updateThemePreferences(patch);
123168 },
124 [updatePreferences],
169 [updateThemePreferences],
125170 );
126171
127172 const setThemeMode = useCallback(
128173 (value: MobileThemeMode) => {
129 updatePreferences({ themeMode: value });
174 const current = appliedRuntimeStateRef.current ?? runtimeState;
175
176 // Clear a forced native appearance before publishing System. The
177 // resulting useColorScheme notification still sees the previous forced
178 // preference, so React batches the actual system palette into the one
179 // urgent preference commit below.
180 if (value === "system") {
181 Appearance.setColorScheme("unspecified");
182 }
183 const nextAppearance =
184 value === "system" ? (Appearance.getColorScheme() === "dark" ? "dark" : "light") : value;
185 const next = {
186 ...current,
187 themeAppearance: nextAppearance,
188 themeMode: value,
189 };
190
191 updateThemePreferences({ themeMode: value });
192 if (value === "system") {
193 appliedRuntimeStateRef.current = next;
194 } else {
195 syncThemeRuntime(next);
196 }
130197 },
131 [updatePreferences],
198 [runtimeState, syncThemeRuntime, updateThemePreferences],
132199 );
133200
134201 const setBaseFontSize = useCallback(
135202 (value: number) => {
203 const current = appliedRuntimeStateRef.current ?? runtimeState;
204 syncThemeRuntime({ ...current, baseFontSize: value });
136205 updatePreferences({ baseFontSize: value });
137206 },
138 [updatePreferences],
207 [runtimeState, syncThemeRuntime, updatePreferences],
139208 );
140209
141210 const setTerminalFontSize = useCallback(
......@@ -161,7 +230,7 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN
161230
162231 const value = useMemo(
163232 (): AppearancePreferencesContextValue => ({
164 appearance: resolveAppearance(preferences),
233 appearance,
165234 themeId,
166235 themeIds,
167236 themeMode,
......@@ -176,7 +245,7 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN
176245 setCodeWordBreak,
177246 }),
178247 [
179 preferences,
248 appearance,
180249 themeId,
181250 themeIds,
182251 themeMode,
......@@ -194,7 +263,7 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN
194263
195264 return (
196265 <AppearancePreferencesContext.Provider value={value}>
197 {props.children}
266 <ScopedTheme theme={activeThemeName}>{props.children}</ScopedTheme>
198267 </AppearancePreferencesContext.Provider>
199268 );
200269}
apps/mobile/src/features/settings/appearance/components/AppearancePreviews.tsx+4-3
......@@ -5,7 +5,7 @@ import {
55 resolveMarkdownFontSizes,
66 resolveMobileCodeSurface,
77} from "../../../../lib/appearancePreferences";
8import { useThemeColor } from "../../../../lib/useThemeColor";
8import { useUniwindTheme } from "../../../../lib/useUniwindTheme";
99import { getMobileTerminalTheme } from "../../../terminal/terminalTheme";
1010import { useAppearancePreferences } from "../AppearancePreferencesProvider";
1111
......@@ -138,8 +138,9 @@ export function CodeAppearancePreview(props: {
138138 readonly wordBreak: boolean;
139139}) {
140140 const surface = resolveMobileCodeSurface(props.fontSize);
141 const lineNumberColor = useThemeColor("--color-icon-subtle");
142 const keywordColor = useThemeColor("--color-md-link");
141 const theme = useUniwindTheme();
142 const lineNumberColor = theme["--color-icon-subtle"];
143 const keywordColor = theme["--color-md-link"];
143144
144145 const lineNumber = (line: CodePreviewLine, index: number) => (
145146 <Text
apps/mobile/src/features/settings/appearance/components/FontSizeSliderRow.tsx+7-8
......@@ -12,7 +12,7 @@ import Animated, {
1212import type { ComponentProps } from "react";
1313
1414import { AppText as Text } from "../../../../components/AppText";
15import { useThemeColor } from "../../../../lib/useThemeColor";
15import { useUniwindTheme } from "../../../../lib/useUniwindTheme";
1616
1717type SymbolName = ComponentProps<typeof SymbolView>["name"];
1818
......@@ -36,10 +36,9 @@ export function FontSizeSliderRow(props: {
3636 readonly value: number;
3737 readonly onChange: (value: number) => void;
3838}) {
39 const icon = useThemeColor("--color-icon");
40 const iconMuted = String(useThemeColor("--color-icon-muted"));
41 const trackColor = String(useThemeColor("--color-secondary-border"));
42 const fillColor = String(useThemeColor("--color-primary"));
39 const theme = useUniwindTheme();
40 const trackColor = theme["--color-secondary-border"];
41 const fillColor = theme["--color-primary"];
4342
4443 const latest = useRef(props);
4544 latest.current = props;
......@@ -141,7 +140,7 @@ export function FontSizeSliderRow(props: {
141140 <SymbolView
142141 name={props.icon}
143142 size={22}
144 tintColor={icon}
143 tintColorClassName={"accent-icon"}
145144 type="monochrome"
146145 weight="regular"
147146 />
......@@ -152,7 +151,7 @@ export function FontSizeSliderRow(props: {
152151 <SymbolView
153152 name="textformat.size.smaller"
154153 size={15}
155 tintColor={iconMuted}
154 tintColorClassName={"accent-icon-muted"}
156155 type="monochrome"
157156 weight="regular"
158157 />
......@@ -204,7 +203,7 @@ export function FontSizeSliderRow(props: {
204203 <SymbolView
205204 name="textformat.size.larger"
206205 size={22}
207 tintColor={iconMuted}
206 tintColorClassName={"accent-icon-muted"}
208207 type="monochrome"
209208 weight="regular"
210209 />
apps/mobile/src/features/settings/appearance/sections/ThemeAppearanceSection.tsx+73-117
......@@ -1,22 +1,22 @@
11import { memo, useId } from "react";
22import { Pressable, View } from "react-native";
33import Svg, { Circle, Defs, RadialGradient, Stop } from "react-native-svg";
4import { ScopedTheme } from "uniwind";
45
56import { mixThemePreviewBase, THEME_PREVIEW_RENDER_SPECS } from "@t3tools/shared/themePreview";
67
78import { SymbolView } from "../../../../components/AppSymbol";
89import { AppText as Text } from "../../../../components/AppText";
910import {
10 getMobileThemeVariables,
1111 getMobileThemePreviewColors,
1212 MOBILE_THEME_OPTIONS,
1313 type MobileThemeAppearance,
1414 type MobileThemeId,
1515 type MobileThemeIds,
1616 type MobileThemeMode,
17 type MobileThemeVariables,
1817} from "../../../../lib/mobileTheme";
19import { useThemeColor } from "../../../../lib/useThemeColor";
18import { getMobileUniwindThemeName } from "../../../../lib/mobileThemeRuntime";
19import { cn } from "../../../../lib/cn";
2020import { useAppearancePreferences } from "../AppearancePreferencesProvider";
2121
2222const APPEARANCE_MODES: ReadonlyArray<{
......@@ -28,6 +28,8 @@ const APPEARANCE_MODES: ReadonlyArray<{
2828 { id: "dark", label: "Dark" },
2929];
3030
31const previewPercentage = (value: number) => `${value * 100}%`;
32
3133const PreviewOrb = memo(function PreviewOrb(props: {
3234 readonly appearance: MobileThemeAppearance;
3335 readonly compact?: boolean;
......@@ -46,9 +48,6 @@ const PreviewOrb = memo(function PreviewOrb(props: {
4648 Math.max(spec.action.center[0], 1 - spec.action.center[0]),
4749 Math.max(spec.action.center[1], 1 - spec.action.center[1]),
4850 );
49 const position = (value: number) => `${value * 100}%`;
50 const radius = (value: number) => `${value * 100}%`;
51
5251 return (
5352 <View
5453 className={`${props.compact ? "size-14" : "size-16"} overflow-hidden rounded-full border`}
......@@ -60,33 +59,33 @@ const PreviewOrb = memo(function PreviewOrb(props: {
6059 <Svg accessibilityElementsHidden height="100%" viewBox="0 0 64 64" width="100%">
6160 <Defs>
6261 <RadialGradient
63 cx={position(spec.accent.center[0])}
64 cy={position(spec.accent.center[1])}
65 fx={position(spec.accent.center[0])}
66 fy={position(spec.accent.center[1])}
62 cx={previewPercentage(spec.accent.center[0])}
63 cy={previewPercentage(spec.accent.center[1])}
64 fx={previewPercentage(spec.accent.center[0])}
65 fy={previewPercentage(spec.accent.center[1])}
6766 id={accentGradientId}
68 r={radius(accentRadius)}
67 r={previewPercentage(accentRadius)}
6968 >
7069 <Stop offset="0%" stopColor={colors.accent} stopOpacity={1} />
7170 <Stop
72 offset={position(spec.accent.middleOffset)}
71 offset={previewPercentage(spec.accent.middleOffset)}
7372 stopColor={colors.accent}
7473 stopOpacity={spec.accent.middleOpacity}
7574 />
7675 <Stop
77 offset={position(spec.accent.endOffset)}
76 offset={previewPercentage(spec.accent.endOffset)}
7877 stopColor={colors.accent}
7978 stopOpacity={0}
8079 />
8180 <Stop offset="100%" stopColor={colors.accent} stopOpacity={0} />
8281 </RadialGradient>
8382 <RadialGradient
84 cx={position(spec.action.center[0])}
85 cy={position(spec.action.center[1])}
86 fx={position(spec.action.center[0])}
87 fy={position(spec.action.center[1])}
83 cx={previewPercentage(spec.action.center[0])}
84 cy={previewPercentage(spec.action.center[1])}
85 fx={previewPercentage(spec.action.center[0])}
86 fy={previewPercentage(spec.action.center[1])}
8887 id={actionGradientId}
89 r={radius(actionRadius)}
88 r={previewPercentage(actionRadius)}
9089 >
9190 <Stop
9291 offset="0%"
......@@ -94,7 +93,7 @@ const PreviewOrb = memo(function PreviewOrb(props: {
9493 stopOpacity={spec.action.startOpacity}
9594 />
9695 <Stop
97 offset={position(spec.action.endOffset)}
96 offset={previewPercentage(spec.action.endOffset)}
9897 stopColor={colors.messageAction}
9998 stopOpacity={0}
10099 />
......@@ -118,32 +117,26 @@ function ThemeCard(props: {
118117 readonly onSelect: (appearance: MobileThemeAppearance) => void;
119118 readonly themeId: MobileThemeId;
120119}) {
121 const badgeBackground = useThemeColor("--color-card");
122 const badgeIcon = useThemeColor("--color-icon");
123
124120 const choice = (appearance: MobileThemeAppearance, selected: boolean) => (
125121 <Pressable
122 accessibilityHint={`Sets the ${appearance} appearance only`}
126123 accessibilityLabel={`${props.label} ${appearance} theme`}
127 accessibilityRole="radio"
128 accessibilityState={{ checked: selected, disabled: props.disabled }}
129 className={
130 selected
131 ? "size-[66px] items-center justify-center rounded-full border-[3px] border-primary"
132 : "size-[66px] items-center justify-center rounded-full border-[3px] border-transparent"
133 }
124 accessibilityRole="button"
125 accessibilityState={{ disabled: props.disabled, selected }}
126 className={cn(
127 "size-[66px] items-center justify-center rounded-full border-[3px] active:scale-[0.94]",
128 selected ? "border-primary" : "border-transparent",
129 )}
134130 disabled={props.disabled}
135131 onPress={() => props.onSelect(appearance)}
136132 >
137133 <PreviewOrb appearance={appearance} compact themeId={props.themeId} />
138134 {selected ? (
139 <View
140 className="absolute -bottom-0.5 -right-0.5 size-5 items-center justify-center rounded-full border border-border"
141 style={{ backgroundColor: badgeBackground }}
142 >
135 <View className="absolute -bottom-0.5 -right-0.5 size-5 items-center justify-center rounded-full border border-border bg-card">
143136 <SymbolView
144137 name={appearance === "light" ? "sun.max" : "moon"}
145138 size={12}
146 tintColor={badgeIcon}
139 tintColorClassName="accent-icon"
147140 type="monochrome"
148141 weight="medium"
149142 />
......@@ -158,7 +151,10 @@ function ThemeCard(props: {
158151 accessibilityHint="Sets both light and dark appearances"
159152 accessibilityLabel={`${props.label} theme`}
160153 accessibilityRole="button"
161 accessibilityState={{ disabled: props.disabled }}
154 accessibilityState={{
155 disabled: props.disabled,
156 selected: props.lightSelected && props.darkSelected,
157 }}
162158 className="absolute inset-0 rounded-[24px] active:bg-subtle"
163159 disabled={props.disabled}
164160 onPress={props.onSelectBoth}
......@@ -167,34 +163,26 @@ function ThemeCard(props: {
167163 {choice("light", props.lightSelected)}
168164 {choice("dark", props.darkSelected)}
169165 </View>
170 <View className="min-h-8 flex-row items-center" pointerEvents="none">
171 <Text className="min-w-0 flex-1 text-lg font-t3-medium" numberOfLines={1}>
172 {props.label}
173 </Text>
174 </View>
166 <Text
167 className="min-w-0 flex-1 px-1 text-lg font-t3-medium"
168 numberOfLines={1}
169 pointerEvents="none"
170 >
171 {props.label}
172 </Text>
175173 </View>
176174 );
177175}
178176
179function PreviewPane(props: { readonly colors: MobileThemeVariables; readonly compact?: boolean }) {
177function PreviewPane(props: { readonly compact?: boolean }) {
180178 return (
181 <View
182 className="flex-1 overflow-hidden"
183 style={{ backgroundColor: props.colors["--color-screen"] }}
184 >
179 <View className="flex-1 overflow-hidden bg-screen">
185180 <View
186 className={props.compact ? "h-[18px] gap-0.5 px-1" : "h-[18px] gap-1 px-1.5"}
187 style={{ backgroundColor: props.colors["--color-card"] }}
181 className={cn("bg-card", props.compact ? "h-[18px] gap-0.5 px-1" : "h-[18px] gap-1 px-1.5")}
188182 >
189183 <View className="mt-2 flex-row items-center gap-1">
190 <View
191 className="size-1.5 rounded-full"
192 style={{ backgroundColor: props.colors["--color-primary"] }}
193 />
194 <View
195 className="h-1 flex-1 rounded-full"
196 style={{ backgroundColor: props.colors["--color-foreground-muted"] }}
197 />
184 <View className="size-1.5 rounded-full bg-primary" />
185 <View className="h-1 flex-1 rounded-full bg-foreground-muted" />
198186 </View>
199187 </View>
200188 <View
......@@ -203,24 +191,12 @@ function PreviewPane(props: { readonly colors: MobileThemeVariables; readonly co
203191 }
204192 >
205193 <View className="gap-1">
206 <View
207 className="h-1.5 w-[72%] rounded-full"
208 style={{ backgroundColor: props.colors["--color-subtle-strong"] }}
209 />
210 <View
211 className="h-1.5 w-[46%] rounded-full"
212 style={{ backgroundColor: props.colors["--color-subtle-strong"] }}
213 />
194 <View className="h-1.5 w-[72%] rounded-full bg-subtle-strong" />
195 <View className="h-1.5 w-[46%] rounded-full bg-subtle-strong" />
214196 </View>
215197 <View className="items-end gap-1 pb-2">
216 <View
217 className="h-3 w-[78%] rounded-full"
218 style={{ backgroundColor: props.colors["--color-user-bubble"] }}
219 />
220 <View
221 className="h-1 w-[38%] rounded-full"
222 style={{ backgroundColor: props.colors["--color-foreground-muted"] }}
223 />
198 <View className="h-3 w-[78%] rounded-full bg-user-bubble" />
199 <View className="h-1 w-[38%] rounded-full bg-foreground-muted" />
224200 </View>
225201 </View>
226202 </View>
......@@ -228,50 +204,31 @@ function PreviewPane(props: { readonly colors: MobileThemeVariables; readonly co
228204}
229205
230206function ModePreview(props: { readonly mode: MobileThemeMode; readonly themeIds: MobileThemeIds }) {
231 const light = getMobileThemeVariables(props.themeIds.light, "light");
232 const dark = getMobileThemeVariables(props.themeIds.dark, "dark");
233 const currentBorder = useThemeColor("--color-border");
234 const currentFrame = useThemeColor("--color-drawer");
235 const currentIndicator = useThemeColor("--color-foreground-muted");
236 const frameColor =
237 props.mode === "light"
238 ? light["--color-border"]
239 : props.mode === "dark"
240 ? dark["--color-border"]
241 : currentBorder;
242 const frameBackground =
243 props.mode === "light"
244 ? light["--color-drawer"]
245 : props.mode === "dark"
246 ? dark["--color-drawer"]
247 : currentFrame;
248 const indicatorColor =
249 props.mode === "light"
250 ? light["--color-foreground-muted"]
251 : props.mode === "dark"
252 ? dark["--color-foreground-muted"]
253 : currentIndicator;
207 if (props.mode === "system") {
208 return (
209 <View className="h-24 w-14 self-center rounded-[16px] border-[1.5px] border-border bg-drawer p-[3px]">
210 <View className="flex-1 flex-row overflow-hidden rounded-[11px]">
211 <ScopedTheme theme={getMobileUniwindThemeName(props.themeIds.light, "light")}>
212 <PreviewPane compact />
213 </ScopedTheme>
214 <ScopedTheme theme={getMobileUniwindThemeName(props.themeIds.dark, "dark")}>
215 <PreviewPane compact />
216 </ScopedTheme>
217 </View>
218 <View className="absolute bottom-[6px] left-1/2 h-1 w-4 -translate-x-1/2 rounded-full bg-foreground-muted" />
219 </View>
220 );
221 }
254222
255223 return (
256 <View
257 className="h-24 w-14 self-center rounded-[16px] p-[3px]"
258 style={{ backgroundColor: frameBackground, borderColor: frameColor, borderWidth: 1.5 }}
259 >
260 <View className="flex-1 flex-row overflow-hidden rounded-[11px]">
261 {props.mode === "system" ? (
262 <>
263 <PreviewPane colors={light} compact />
264 <PreviewPane colors={dark} compact />
265 </>
266 ) : (
267 <PreviewPane colors={props.mode === "light" ? light : dark} />
268 )}
224 <ScopedTheme theme={getMobileUniwindThemeName(props.themeIds[props.mode], props.mode)}>
225 <View className="h-24 w-14 self-center rounded-[16px] border-[1.5px] border-border bg-drawer p-[3px]">
226 <View className="flex-1 flex-row overflow-hidden rounded-[11px]">
227 <PreviewPane />
228 </View>
229 <View className="absolute bottom-[6px] left-1/2 h-1 w-4 -translate-x-1/2 rounded-full bg-foreground-muted" />
269230 </View>
270 <View
271 className="absolute bottom-[6px] left-1/2 h-1 w-4 -translate-x-1/2 rounded-full"
272 style={{ backgroundColor: indicatorColor }}
273 />
274 </View>
231 </ScopedTheme>
275232 );
276233}
277234
......@@ -288,11 +245,10 @@ function ModeCard(props: {
288245 accessibilityLabel={`${props.label} appearance`}
289246 accessibilityRole="radio"
290247 accessibilityState={{ checked: props.selected, disabled: props.disabled }}
291 className={
292 props.selected
293 ? "min-w-0 flex-1 gap-2 rounded-[24px] border-2 border-primary bg-subtle p-2"
294 : "min-w-0 flex-1 gap-2 rounded-[24px] border border-border bg-card p-2"
295 }
248 className={cn(
249 "min-w-0 flex-1 gap-2 rounded-[24px] p-2 active:scale-[0.97]",
250 props.selected ? "border-2 border-primary bg-subtle" : "border border-border bg-card",
251 )}
296252 disabled={props.disabled}
297253 onPress={props.onPress}
298254 >
apps/mobile/src/features/settings/appearance/useScaledTextRole.ts+22-24
......@@ -1,18 +1,12 @@
1import { useCSSVariable } from "uniwind";
1import { useMemo } from "react";
22
3import {
4 DEFAULT_BASE_FONT_SIZE,
5 normalizeBaseFontSize,
6 scaledTypographyLineHeight,
7} from "../../../lib/appearancePreferences";
38import { MOBILE_TYPOGRAPHY } from "../../../lib/typography";
4
5const TEXT_ROLE_VARIABLES = {
6 micro: "--text-3xs",
7 caption: "--text-2xs",
8 label: "--text-xs",
9 footnote: "--text-sm",
10 body: "--text-base",
11 headline: "--text-lg",
12 title: "--text-xl",
13 largeTitle: "--text-2xl",
14 display: "--text-3xl",
15} as const satisfies Record<keyof typeof MOBILE_TYPOGRAPHY, string>;
9import { useAppearancePreferences } from "./AppearancePreferencesProvider";
1610
1711export interface ScaledTextRole {
1812 readonly fontSize: number;
......@@ -20,17 +14,21 @@ export interface ScaledTextRole {
2014}
2115
2216/**
23 * Reads a typography role's current size from the Uniwind `--text-*` CSS
24 * variables (scaled at runtime with the base font size). Use for style-prop
25 * consumers that can't express their size as a `text-*` className. Reactive:
26 * re-renders when the appearance provider re-injects the variables.
17 * Mirrors the values injected into Uniwind for style-prop consumers that
18 * cannot use a `text-*` class. This deliberately does not subscribe to CSS
19 * variables, so palette-only setTheme calls remain native-only.
2720 */
2821export function useScaledTextRole(role: keyof typeof MOBILE_TYPOGRAPHY): ScaledTextRole {
29 const variable = TEXT_ROLE_VARIABLES[role];
30 const [fontSize, lineHeight] = useCSSVariable([variable, `${variable}--line-height`]);
31
32 return {
33 fontSize: typeof fontSize === "number" ? fontSize : MOBILE_TYPOGRAPHY[role].fontSize,
34 lineHeight: typeof lineHeight === "number" ? lineHeight : MOBILE_TYPOGRAPHY[role].lineHeight,
35 };
22 const { appearance } = useAppearancePreferences();
23 return useMemo(() => {
24 const baseFontSize = normalizeBaseFontSize(appearance.baseFontSize);
25 const typography = MOBILE_TYPOGRAPHY[role];
26 return {
27 fontSize: Math.max(
28 8,
29 Math.round(typography.fontSize * (baseFontSize / DEFAULT_BASE_FONT_SIZE)),
30 ),
31 lineHeight: scaledTypographyLineHeight(typography, baseFontSize),
32 };
33 }, [appearance.baseFontSize, role]);
3634}
apps/mobile/src/features/settings/components/SettingsLegalDocumentRouteScreen.tsx+3-7
......@@ -6,12 +6,10 @@ import { WebView } from "react-native-webview";
66import { AppText as Text } from "../../../components/AppText";
77import { LoadingStrip } from "../../../components/LoadingStrip";
88import { SymbolView } from "../../../components/AppSymbol";
9import { useThemeColor } from "../../../lib/useThemeColor";
109import { isLegalDocumentUrl, LEGAL_URL } from "../lib/legal-document-url";
1110
1211export function SettingsLegalDocumentCloseHeaderButton() {
1312 const navigation = useNavigation();
14 const iconColor = useThemeColor("--color-icon");
1513
1614 return (
1715 <Pressable
......@@ -24,7 +22,7 @@ export function SettingsLegalDocumentCloseHeaderButton() {
2422 <SymbolView
2523 name="xmark"
2624 size={18}
27 tintColor={iconColor}
25 tintColorClassName={"accent-icon"}
2826 type="monochrome"
2927 weight="semibold"
3028 />
......@@ -37,7 +35,6 @@ export function SettingsLegalDocumentExternalHeaderButton({
3735}: {
3836 readonly externalUrl?: string;
3937}) {
40 const iconColor = useThemeColor("--color-icon");
4138 const safeExternalUrl = isLegalDocumentUrl(externalUrl) ? externalUrl : LEGAL_URL;
4239
4340 return (
......@@ -51,7 +48,7 @@ export function SettingsLegalDocumentExternalHeaderButton({
5148 <SymbolView
5249 name="safari"
5350 size={19}
54 tintColor={iconColor}
51 tintColorClassName={"accent-icon"}
5552 type="monochrome"
5653 weight="regular"
5754 />
......@@ -69,7 +66,6 @@ export function SettingsLegalDocumentRouteScreen({
6966 documentUrl,
7067}: SettingsLegalDocumentRouteScreenProps) {
7168 const navigation = useNavigation<NavigationProp<ParamListBase>>();
72 const iconColor = useThemeColor("--color-icon");
7369 const [reloadKey, setReloadKey] = useState(0);
7470 const [loadProgress, setLoadProgress] = useState(0);
7571 const [loadError, setLoadError] = useState<string | null>(null);
......@@ -94,7 +90,7 @@ export function SettingsLegalDocumentRouteScreen({
9490 <SymbolView
9591 name="exclamationmark.triangle"
9692 size={32}
97 tintColor={iconColor}
93 tintColorClassName={"accent-icon"}
9894 type="monochrome"
9995 weight="regular"
10096 />
apps/mobile/src/features/settings/components/SettingsRow.tsx+8-5
......@@ -5,7 +5,6 @@ import { Pressable, View } from "react-native";
55import { SymbolView } from "../../../components/AppSymbol";
66
77import { AppText as Text } from "../../../components/AppText";
8import { useThemeColor } from "../../../lib/useThemeColor";
98import type { SettingsLegalDocumentTarget, SettingsSheetTarget } from "./settings-sheet-targets";
109
1110type SymbolName = ComponentProps<typeof SymbolView>["name"];
......@@ -20,8 +19,6 @@ export function SettingsRow(props: {
2019 readonly onPress?: () => void;
2120}) {
2221 const navigation = useNavigation();
23 const icon = useThemeColor("--color-icon");
24 const chevron = useThemeColor("--color-chevron");
2522 const content = (
2623 <View
2724 className={
......@@ -30,7 +27,13 @@ export function SettingsRow(props: {
3027 : "flex-row items-center gap-4 p-4"
3128 }
3229 >
33 <SymbolView name={props.icon} size={22} tintColor={icon} type="monochrome" weight="regular" />
30 <SymbolView
31 name={props.icon}
32 size={22}
33 tintColorClassName={"accent-icon"}
34 type="monochrome"
35 weight="regular"
36 />
3437 <Text className="shrink-0 text-lg text-foreground" numberOfLines={1}>
3538 {props.label}
3639 </Text>
......@@ -48,7 +51,7 @@ export function SettingsRow(props: {
4851 <SymbolView
4952 name="chevron.right"
5053 size={16}
51 tintColor={chevron}
54 tintColorClassName={"accent-chevron"}
5255 type="monochrome"
5356 weight="semibold"
5457 />
apps/mobile/src/features/settings/components/SettingsSwitchRow.tsx+7-4
......@@ -4,7 +4,6 @@ import { View } from "react-native";
44import { SymbolView } from "../../../components/AppSymbol";
55import { AppText as Text } from "../../../components/AppText";
66import { ThemedSwitch } from "../../../components/ThemedSwitch";
7import { useThemeColor } from "../../../lib/useThemeColor";
87
98type SymbolName = ComponentProps<typeof SymbolView>["name"];
109
......@@ -16,8 +15,6 @@ export function SettingsSwitchRow(props: {
1615 readonly value: boolean;
1716 readonly onValueChange: (value: boolean) => void;
1817}) {
19 const icon = useThemeColor("--color-icon");
20
2118 return (
2219 <View
2320 className={
......@@ -26,7 +23,13 @@ export function SettingsSwitchRow(props: {
2623 : "flex-row items-center gap-4 p-4"
2724 }
2825 >
29 <SymbolView name={props.icon} size={22} tintColor={icon} type="monochrome" weight="regular" />
26 <SymbolView
27 name={props.icon}
28 size={22}
29 tintColorClassName={"accent-icon"}
30 type="monochrome"
31 weight="regular"
32 />
3033 <View className="min-w-0 flex-1">
3134 <Text className="text-lg text-foreground">{props.label}</Text>
3235 {props.subtitle ? (
apps/mobile/src/features/terminal/ThreadTerminalPanel.tsx+6-3
......@@ -4,7 +4,6 @@ import { memo, useCallback, useEffect, useMemo, useRef } from "react";
44import { Pressable, View } from "react-native";
55
66import { AppText as Text } from "../../components/AppText";
7import { useThemeColor } from "../../lib/useThemeColor";
87import { terminalEnvironment } from "../../state/terminal";
98import { useAtomCommand } from "../../state/use-atom-command";
109import { useAttachedTerminalSession } from "../../state/use-terminal-session";
......@@ -36,7 +35,6 @@ export const ThreadTerminalPanel = memo(function ThreadTerminalPanel(
3635 const closeTerminal = useAtomCommand(terminalEnvironment.close, "terminal close");
3736 const openTerminal = useAtomCommand(terminalEnvironment.open, "terminal open");
3837 const nativeTerminalAvailable = hasNativeTerminalSurface();
39 const iconColor = useThemeColor("--color-icon");
4038 const terminalId = DEFAULT_TERMINAL_ID;
4139 const lastGridSizeRef = useRef<TerminalGridSize>({
4240 cols: DEFAULT_TERMINAL_COLS,
......@@ -236,7 +234,12 @@ export const ThreadTerminalPanel = memo(function ThreadTerminalPanel(
236234 className="h-8 w-8 items-center justify-center rounded-[8px] bg-subtle"
237235 onPress={props.onClose}
238236 >
239 <SymbolView name="xmark" size={13} tintColor={iconColor} type="monochrome" />
237 <SymbolView
238 name="xmark"
239 size={13}
240 tintColorClassName={"accent-icon"}
241 type="monochrome"
242 />
240243 </Pressable>
241244 </View>
242245 </View>
apps/mobile/src/features/threads/ComposerCommandPopover.tsx+14-17
......@@ -5,13 +5,12 @@ import {
55import type { ServerProviderSkill, ServerProviderSlashCommand } from "@t3tools/contracts";
66import type { ComposerTriggerKind } from "@t3tools/shared/composerTrigger";
77import { memo } from "react";
8import { Pressable, ScrollView, View, type ViewStyle } from "react-native";
8import { Pressable, ScrollView, StyleSheet, View, type ViewStyle } from "react-native";
99
1010import { SymbolView, type AppSymbolName } from "../../components/AppSymbol";
1111import { AppText as Text } from "../../components/AppText";
1212import { GlassSurface } from "../../components/GlassSurface";
1313import { PierreEntryIcon } from "../../components/PierreEntryIcon";
14import { useThemeColor } from "../../lib/useThemeColor";
1514export type ComposerCommandItem =
1615 | {
1716 readonly id: string;
......@@ -51,7 +50,6 @@ interface ComposerCommandPopoverProps {
5150}
5251
5352function PopoverSurface(props: { readonly children: React.ReactNode; readonly style?: ViewStyle }) {
54 const tintColor = useThemeColor("--color-glass-surface");
5553 const baseStyle: ViewStyle = {
5654 borderRadius: 16,
5755 overflow: "hidden",
......@@ -59,7 +57,11 @@ function PopoverSurface(props: { readonly children: React.ReactNode; readonly st
5957 };
6058
6159 return (
62 <GlassSurface glassEffectStyle="clear" tintColor={tintColor} style={baseStyle}>
60 <GlassSurface
61 glassEffectStyle="clear"
62 tintColorClassName="accent-glass-surface"
63 style={baseStyle}
64 >
6365 {props.children}
6466 </GlassSurface>
6567 );
......@@ -122,27 +124,22 @@ const CommandRow = memo(function CommandRow(props: {
122124 readonly isSlashSkill: boolean;
123125}) {
124126 const iconName = itemIcon(props.item);
125 const iconColor = useThemeColor("--color-icon-subtle");
126 const borderColor = useThemeColor("--color-border");
127127
128128 return (
129129 <Pressable
130130 onPress={props.onPress}
131 style={({ pressed }) => ({
132 flexDirection: "row",
133 alignItems: "center",
134 paddingHorizontal: 14,
135 paddingVertical: 10,
136 gap: 10,
137 opacity: pressed ? 0.6 : 1,
138 borderBottomWidth: props.isLast ? 0 : 0.5,
139 borderBottomColor: borderColor,
140 })}
131 className="flex-row items-center gap-2.5 border-border px-3.5 py-2.5 active:opacity-60"
132 style={{ borderBottomWidth: props.isLast ? 0 : StyleSheet.hairlineWidth }}
141133 >
142134 {props.item.type === "path" ? (
143135 <PierreEntryIcon path={props.item.path} kind={props.item.kind} size={16} />
144136 ) : iconName ? (
145 <SymbolView name={iconName} size={14} tintColor={iconColor} type="monochrome" />
137 <SymbolView
138 name={iconName}
139 size={14}
140 tintColorClassName={"accent-icon-subtle"}
141 type="monochrome"
142 />
146143 ) : null}
147144 <Text className="shrink-0 text-base font-t3-medium text-foreground" numberOfLines={1}>
148145 {props.isSlashSkill && props.item.type === "skill" ? (
apps/mobile/src/features/threads/GitActionProgressOverlay.tsx+17-17
......@@ -2,7 +2,7 @@ import * as Haptics from "expo-haptics";
22import { isLiquidGlassSupported, LiquidGlassView } from "@callstack/liquid-glass";
33import { SymbolView } from "../../components/AppSymbol";
44import { useCallback, useEffect, useRef } from "react";
5import { ActivityIndicator, Pressable, StyleSheet, View } from "react-native";
5import { ActivityIndicator, Pressable, StyleSheet, useColorScheme, View } from "react-native";
66import Animated, { FadeIn, FadeOut, LinearTransition } from "react-native-reanimated";
77import { useSafeAreaInsets } from "react-native-safe-area-context";
88
......@@ -10,9 +10,8 @@ import { AppText as Text } from "../../components/AppText";
1010import { APP_BAR_HEIGHT } from "../../lib/layoutMetrics";
1111import { themeColorWithAlpha } from "../../lib/mobileTheme";
1212import { tryOpenExternalUrl } from "../../lib/openExternalUrl";
13import { useThemeColor } from "../../lib/useThemeColor";
13import { useUniwindTheme } from "../../lib/useUniwindTheme";
1414import type { GitActionProgress } from "../../state/use-vcs-action-state";
15import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
1615
1716const OVERLAY_LAYOUT_TRANSITION = LinearTransition.duration(220);
1817const OVERLAY_TOP_GAP = 8;
......@@ -68,17 +67,16 @@ export function GitActionProgressOverlay(props: {
6867
6968function OverlayContent(props: { readonly progress: GitActionProgress }) {
7069 const { progress } = props;
71 const iconColor = useThemeColor("--color-icon");
72 const glassSurface = useThemeColor("--color-glass-surface");
73 const foreground = useThemeColor("--color-foreground");
74 const shadowColor = useThemeColor("--color-primary-shadow");
75 const { themeAppearance } = useAppearancePreferences();
76 const isDarkMode = themeAppearance === "dark";
70 const colors = useUniwindTheme();
71 const glassSurface = colors["--color-glass-surface"];
72 const foreground = colors["--color-foreground"];
73 const shadowColor = colors["--color-primary-shadow"];
74 const isDarkMode = useColorScheme() === "dark";
7775 const glassTint = themeColorWithAlpha(String(glassSurface), isDarkMode ? 0.48 : 0.38);
7876 const glassBorder = themeColorWithAlpha(String(foreground), isDarkMode ? 0.18 : 0.12);
7977 const content = (
8078 <>
81 <OverlayIcon phase={progress.phase} iconColor={iconColor} />
79 <OverlayIcon phase={progress.phase} />
8280
8381 <View className="flex-1 gap-0.5">
8482 {progress.label ? (
......@@ -94,7 +92,12 @@ function OverlayContent(props: { readonly progress: GitActionProgress }) {
9492 </View>
9593
9694 {progress.prUrl ? (
97 <SymbolView name="arrow.up.right" size={13} tintColor={iconColor} type="monochrome" />
95 <SymbolView
96 name="arrow.up.right"
97 size={13}
98 tintColorClassName={"accent-icon"}
99 type="monochrome"
100 />
98101 ) : null}
99102 </>
100103 );
......@@ -140,7 +143,7 @@ function OverlayContent(props: { readonly progress: GitActionProgress }) {
140143
141144 const bgClass =
142145 progress.phase === "error"
143 ? "bg-red-50 dark:bg-red-950/80 border-red-200 dark:border-red-800"
146 ? "border-adaptive-red-200-800 bg-adaptive-red-50-950-a80"
144147 : "bg-card border-border";
145148
146149 return (
......@@ -153,13 +156,10 @@ function OverlayContent(props: { readonly progress: GitActionProgress }) {
153156 );
154157}
155158
156function OverlayIcon(props: {
157 readonly phase: GitActionProgress["phase"];
158 readonly iconColor: ReturnType<typeof useThemeColor>;
159}) {
159function OverlayIcon(props: { readonly phase: GitActionProgress["phase"] }) {
160160 switch (props.phase) {
161161 case "running":
162 return <ActivityIndicator size="small" />;
162 return <ActivityIndicator size="small" colorClassName={"accent-icon"} />;
163163 case "success":
164164 return (
165165 <View className="h-6 w-6 items-center justify-center rounded-full bg-green-500">
apps/mobile/src/features/threads/NewTaskContextPickerScreens.tsx+10-9
......@@ -24,7 +24,7 @@ import { AppText as Text } from "../../components/AppText";
2424import { ThemedSwitch } from "../../components/ThemedSwitch";
2525import { cn } from "../../lib/cn";
2626import { useFontFamily } from "../../lib/useFontFamily";
27import { useThemeColor } from "../../lib/useThemeColor";
27import { useUniwindTheme } from "../../lib/useUniwindTheme";
2828import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader";
2929import { useAtomCommand } from "../../state/use-atom-command";
3030import { vcsEnvironment } from "../../state/vcs";
......@@ -45,9 +45,6 @@ function SelectionRow(props: {
4545 readonly subtitle?: string;
4646 readonly title: string;
4747}) {
48 const iconColor = useThemeColor("--color-icon-muted");
49 const checkmarkColor = useThemeColor("--color-icon");
50
5148 return (
5249 <Pressable
5350 accessibilityLabel={[props.title, props.subtitle].filter(Boolean).join(", ")}
......@@ -62,7 +59,12 @@ function SelectionRow(props: {
6259 style={{ opacity: props.disabled ? 0.45 : 1 }}
6360 >
6461 {props.icon ? (
65 <SymbolView name={props.icon} size={17} tintColor={iconColor} type="monochrome" />
62 <SymbolView
63 name={props.icon}
64 size={17}
65 tintColorClassName={"accent-icon-muted"}
66 type="monochrome"
67 />
6668 ) : null}
6769 <View className="min-w-0 flex-1 gap-0.5">
6870 <Text className="text-base font-t3-medium text-foreground" numberOfLines={1}>
......@@ -78,7 +80,7 @@ function SelectionRow(props: {
7880 <SymbolView
7981 name="checkmark"
8082 size={16}
81 tintColor={checkmarkColor}
83 tintColorClassName={"accent-icon"}
8284 type="monochrome"
8385 weight="semibold"
8486 />
......@@ -191,8 +193,7 @@ export function NewTaskBranchPickerRouteScreen() {
191193 const flow = useNewTaskFlow();
192194 const navigation = useNavigation();
193195 const insets = useSafeAreaInsets();
194 const placeholderColor = useThemeColor("--color-placeholder");
195 const foregroundColor = useThemeColor("--color-foreground");
196 const foregroundColor = useUniwindTheme()["--color-foreground"];
196197 const fontFamily = useFontFamily("regular");
197198 const switchRef = useAtomCommand(vcsEnvironment.switchRef, { reportFailure: false });
198199 const [switchingBranchName, setSwitchingBranchName] = useState<string | null>(null);
......@@ -419,7 +420,7 @@ export function NewTaskBranchPickerRouteScreen() {
419420 className="h-11 rounded-xl bg-card px-4 text-base text-foreground"
420421 onChangeText={flow.setBranchQuery}
421422 placeholder="Find a branch"
422 placeholderTextColor={placeholderColor}
423 placeholderTextColorClassName={"accent-placeholder"}
423424 style={{ color: foregroundColor, fontFamily }}
424425 value={flow.branchQuery}
425426 />
apps/mobile/src/features/threads/NewTaskDraftScreen.tsx+24-26
......@@ -13,8 +13,7 @@ import {
1313 useKeyboardState,
1414} from "react-native-keyboard-controller";
1515import { useSafeAreaInsets } from "react-native-safe-area-context";
16import { useThemeColor } from "../../lib/useThemeColor";
17import { themeColorWithAlpha } from "../../lib/mobileTheme";
16import { useUniwindTheme } from "../../lib/useUniwindTheme";
1817import { useFontFamily } from "../../lib/useFontFamily";
1918
2019import {
......@@ -43,7 +42,6 @@ import {
4342import { makeTurnCommandMetadata } from "../../lib/commandMetadata";
4443import { convertPastedImagesToAttachments, pickComposerImages } from "../../lib/composerImages";
4544import { useScaledTextRole } from "../settings/appearance/useScaledTextRole";
46import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
4745import {
4846 clearComposerDraftContent,
4947 getComposerDraftSnapshot,
......@@ -71,17 +69,32 @@ function NewTaskWorkspaceIcon(props: {
7169 readonly workspaceMode: "local" | "worktree";
7270 readonly worktreePath: string | null;
7371}) {
74 const iconColor = useThemeColor("--color-icon-muted");
75
7672 if (props.workspaceMode === "local" && props.worktreePath === null) {
77 return <SymbolView name="folder" size={16} tintColor={iconColor} type="monochrome" />;
73 return (
74 <SymbolView
75 name="folder"
76 size={16}
77 tintColorClassName={"accent-icon-muted"}
78 type="monochrome"
79 />
80 );
7881 }
7982
8083 return (
8184 <View className="size-4">
82 <SymbolView name="folder" size={16} tintColor={iconColor} type="monochrome" />
85 <SymbolView
86 name="folder"
87 size={16}
88 tintColorClassName={"accent-icon-muted"}
89 type="monochrome"
90 />
8391 <View className="absolute -right-1 -bottom-1">
84 <SymbolView name="arrow.triangle.branch" size={9} tintColor={iconColor} type="monochrome" />
92 <SymbolView
93 name="arrow.triangle.branch"
94 size={9}
95 tintColorClassName={"accent-icon-muted"}
96 type="monochrome"
97 />
8598 </View>
8699 </View>
87100 );
......@@ -109,7 +122,6 @@ export function NewTaskDraftScreen(props: {
109122 reserveShare,
110123 } = useIncomingShare();
111124 const insets = useSafeAreaInsets();
112 const { themeAppearance: colorScheme } = useAppearancePreferences();
113125 const isKeyboardVisible = useKeyboardState((state) => state.isVisible);
114126 const controlsBottomPadding = Math.max(insets.bottom, 10);
115127 const keyboardOpenedOffset = Math.max(0, controlsBottomPadding - 8);
......@@ -291,13 +303,9 @@ export function NewTaskDraftScreen(props: {
291303 };
292304 }, [props.pendingTaskId, cancelEditingPendingTask]);
293305
294 const foregroundColor = useThemeColor("--color-foreground");
295 const sheetColor = String(useThemeColor("--color-sheet"));
296 const projectUnderlineColor = useThemeColor("--color-foreground-muted");
306 const foregroundColor = useUniwindTheme()["--color-foreground"];
297307 const regularFontFamily = useFontFamily("regular");
298308 const bodyText = useScaledTextRole("body");
299 const sheetFadeOpaque = sheetColor;
300 const sheetFadeTransparent = themeColorWithAlpha(sheetColor, 0);
301309
302310 // A new navigation to this mounted screen delivers a fresh initialProjectRef
303311 // reference — treat it as a new request and let it apply again.
......@@ -797,7 +805,6 @@ export function NewTaskDraftScreen(props: {
797805 }
798806
799807 const isAndroid = Platform.OS === "android";
800 const isDarkMode = colorScheme === "dark";
801808 const canStart =
802809 Boolean(flow.selectedProject) &&
803810 Boolean(flow.selectedModel) &&
......@@ -874,11 +881,7 @@ export function NewTaskDraftScreen(props: {
874881 accessibilityRole="button"
875882 disabled={isIncomingShareTransferPending}
876883 onPress={chooseProject}
877 className="min-w-0 max-w-[250px] active:opacity-65"
878 style={{
879 borderBottomColor: projectUnderlineColor,
880 borderBottomWidth: 1,
881 }}
884 className="min-w-0 max-w-[250px] border-b border-foreground-muted active:opacity-65"
882885 >
883886 <Text
884887 className="text-2xl font-t3-medium tracking-tight text-foreground"
......@@ -960,7 +963,6 @@ export function NewTaskDraftScreen(props: {
960963
961964 <ComposerSurface
962965 animateLayout={false}
963 isDarkMode={isDarkMode}
964966 style={{
965967 borderRadius: 26,
966968 minHeight: 140,
......@@ -984,11 +986,7 @@ export function NewTaskDraftScreen(props: {
984986 {promptEditor}
985987
986988 <ComposerToolbarRow paddingBottom={0} paddingHorizontal={0} paddingTop={4}>
987 <ComposerToolbarScroller
988 fadeOpaque={sheetFadeOpaque}
989 fadeTransparent={sheetFadeTransparent}
990 contentPaddingRight={8}
991 >
989 <ComposerToolbarScroller contentPaddingRight={8} fadeSurface="sheet">
992990 <ComposerToolbarButton
993991 accessibilityLabel="Add attachment"
994992 disabled={isIncomingShareTransferPending}
apps/mobile/src/features/threads/NewTaskRouteScreen.tsx+4-5
......@@ -10,7 +10,6 @@ import type { EnvironmentProject } from "@t3tools/client-runtime/state/shell";
1010import { useEffect, useRef } from "react";
1111import { ActivityIndicator, Alert, Platform, Pressable, ScrollView, View } from "react-native";
1212import { useSafeAreaInsets } from "react-native-safe-area-context";
13import { useThemeColor } from "../../lib/useThemeColor";
1413import { cn } from "../../lib/cn";
1514
1615import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";
......@@ -91,8 +90,6 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara
9190 const isFocused = useIsFocused();
9291 const { layout } = useAdaptiveWorkspaceLayout();
9392 const insets = useSafeAreaInsets();
94 const chevronColor = useThemeColor("--color-chevron");
95 const accentColor = useThemeColor("--color-icon-muted");
9693 const { getShare, releaseShareReservation } = useIncomingShare();
9794 const routeShareId = Array.isArray(route.params?.incomingShareId)
9895 ? route.params.incomingShareId[0]
......@@ -242,7 +239,9 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara
242239 >
243240 {projectScopes.length === 0 ? (
244241 <View collapsable={false} className="items-center gap-3 rounded-[24px] bg-card px-6 py-8">
245 {projectEmptyState.loading ? <ActivityIndicator color={accentColor} /> : null}
242 {projectEmptyState.loading ? (
243 <ActivityIndicator colorClassName={"accent-icon-muted"} />
244 ) : null}
246245 <Text className="text-center text-lg font-t3-bold text-foreground">
247246 {projectEmptyState.title}
248247 </Text>
......@@ -308,7 +307,7 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara
308307 <SymbolView
309308 name="chevron.right"
310309 size={14}
311 tintColor={chevronColor}
310 tintColorClassName={"accent-chevron"}
312311 type="monochrome"
313312 />
314313 </Pressable>
apps/mobile/src/features/threads/PendingApprovalCard.tsx+8-8
......@@ -28,15 +28,15 @@ export function PendingApprovalCard(props: PendingApprovalCardProps) {
2828 // Opaque for the same reason as PendingUserInputCard: nothing blurs the feed
2929 // behind this card, so a translucent surface bleeds messages through it.
3030 return (
31 <View className="gap-2.5 rounded-[20px] border border-neutral-200 bg-neutral-100 p-4 dark:border-white/6 dark:bg-neutral-900">
32 <Text className="font-t3-bold text-2xs uppercase tracking-[1.1px] text-sky-700 dark:text-sky-300">
31 <View className="gap-2.5 rounded-[20px] border border-adaptive-neutral-200-white-a6 bg-adaptive-neutral-100-900 p-4">
32 <Text className="font-t3-bold text-2xs uppercase tracking-[1.1px] text-adaptive-sky-700-300">
3333 Approval needed
3434 </Text>
35 <Text className="font-t3-bold text-lg text-neutral-950 dark:text-neutral-50">
35 <Text className="font-t3-bold text-lg text-adaptive-neutral-950-50">
3636 {props.approval.appName ?? props.approval.requestKind}
3737 </Text>
3838 {props.approval.detail ? (
39 <Text className="font-sans text-sm leading-normal text-neutral-600 dark:text-neutral-400">
39 <Text className="font-sans text-sm leading-normal text-adaptive-neutral-600-400">
4040 {props.approval.detail}
4141 </Text>
4242 ) : null}
......@@ -48,8 +48,8 @@ export function PendingApprovalCard(props: PendingApprovalCardProps) {
4848 option.decision === "accept"
4949 ? "bg-blue-500"
5050 : option.decision === "decline"
51 ? "bg-rose-100 dark:bg-rose-500/18"
52 : "bg-neutral-200 dark:bg-neutral-800"
51 ? "bg-adaptive-rose-100-500-a18"
52 : "bg-adaptive-neutral-200-800"
5353 }`}
5454 disabled={props.respondingApprovalId === props.approval.requestId}
5555 onPress={() => void props.onRespond(props.approval.requestId, option.decision)}
......@@ -59,8 +59,8 @@ export function PendingApprovalCard(props: PendingApprovalCardProps) {
5959 option.decision === "accept"
6060 ? "font-t3-extrabold text-white"
6161 : option.decision === "decline"
62 ? "font-t3-bold text-rose-700 dark:text-rose-300"
63 : "font-t3-bold text-neutral-950 dark:text-neutral-50"
62 ? "font-t3-bold text-adaptive-rose-700-300"
63 : "font-t3-bold text-adaptive-neutral-950-50"
6464 }`}
6565 >
6666 {option.label}
apps/mobile/src/features/threads/PendingUserInputCard.tsx+28-20
......@@ -18,7 +18,6 @@ import { SymbolView } from "../../components/AppSymbol";
1818import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText";
1919import { ControlPill } from "../../components/ControlPill";
2020import { cn } from "../../lib/cn";
21import { useThemeColor } from "../../lib/useThemeColor";
2221import {
2322 isPendingUserInputOptionSelected,
2423 type PendingUserInput,
......@@ -87,7 +86,6 @@ const EXPANDED_CARD_IS_OVERLAY = Platform.OS === "ios";
8786const CARD_LAYOUT_TRANSITION = LinearTransition.duration(200);
8887
8988export function PendingUserInputCard(props: PendingUserInputCardProps) {
90 const iconSubtle = useThemeColor("--color-icon-subtle");
9189 const questionCount = props.pendingUserInput.questions.length;
9290
9391 const cardCoverage = props.cardCoverage;
......@@ -163,7 +161,7 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {
163161 pointerEvents={props.collapsed ? "auto" : "none"}
164162 accessibilityElementsHidden={!props.collapsed}
165163 importantForAccessibility={props.collapsed ? "auto" : "no-hide-descendants"}
166 className="flex-row items-center gap-2 rounded-full border border-neutral-200 bg-neutral-100 py-1.5 pl-4 pr-1.5 dark:border-white/6 dark:bg-neutral-900"
164 className="flex-row items-center gap-2 rounded-full border border-adaptive-neutral-200-white-a6 bg-adaptive-neutral-100-900 py-1.5 pl-4 pr-1.5"
167165 >
168166 <Pressable
169167 accessibilityRole="button"
......@@ -173,14 +171,19 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {
173171 onPress={props.onToggleCollapsed}
174172 className="min-h-10 flex-1 flex-row items-center gap-2 active:opacity-70"
175173 >
176 <Text className="font-t3-bold text-2xs uppercase tracking-[1.1px] text-sky-700 dark:text-sky-300">
174 <Text className="font-t3-bold text-2xs uppercase tracking-[1.1px] text-adaptive-sky-700-300">
177175 User input needed
178176 </Text>
179 <Text className="font-sans text-xs text-neutral-500 dark:text-neutral-400">
177 <Text className="font-sans text-xs text-adaptive-neutral-500-400">
180178 {questionCount} question{questionCount === 1 ? "" : "s"}
181179 </Text>
182180 <View className="flex-1" />
183 <SymbolView name="chevron.up" size={12} tintColor={iconSubtle} type="monochrome" />
181 <SymbolView
182 name="chevron.up"
183 size={12}
184 tintColorClassName={"accent-icon-subtle"}
185 type="monochrome"
186 />
184187 </Pressable>
185188 {props.onStopThread ? (
186189 <ControlPill
......@@ -213,7 +216,7 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {
213216 : FadeOutDown.duration(USER_INPUT_TOGGLE_DURATION_MS).easing(Easing.out(Easing.cubic))
214217 }
215218 layout={CARD_LAYOUT_TRANSITION}
216 className="overflow-hidden gap-2.5 rounded-[20px] border border-neutral-200 bg-neutral-100 p-4 dark:border-white/6 dark:bg-neutral-900"
219 className="overflow-hidden gap-2.5 rounded-[20px] border border-adaptive-neutral-200-white-a6 bg-adaptive-neutral-100-900 p-4"
217220 style={
218221 EXPANDED_CARD_IS_OVERLAY
219222 ? [{ maxHeight: props.maxHeight }, cardAnimatedStyle]
......@@ -227,15 +230,20 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {
227230 className="flex-row items-start gap-2"
228231 >
229232 <View className="flex-1 gap-2.5">
230 <Text className="font-t3-bold text-2xs uppercase tracking-[1.1px] text-sky-700 dark:text-sky-300">
233 <Text className="font-t3-bold text-2xs uppercase tracking-[1.1px] text-adaptive-sky-700-300">
231234 User input needed
232235 </Text>
233 <Text className="font-t3-bold text-lg text-neutral-950 dark:text-neutral-50">
236 <Text className="font-t3-bold text-lg text-adaptive-neutral-950-50">
234237 Fill in the pending answers
235238 </Text>
236239 </View>
237 <View className="h-8 w-8 items-center justify-center rounded-full bg-neutral-200/70 dark:bg-white/8">
238 <SymbolView name="chevron.down" size={13} tintColor={iconSubtle} type="monochrome" />
240 <View className="h-8 w-8 items-center justify-center rounded-full bg-adaptive-neutral-200-a70-white-a8">
241 <SymbolView
242 name="chevron.down"
243 size={13}
244 tintColorClassName={"accent-icon-subtle"}
245 type="monochrome"
246 />
239247 </View>
240248 </Pressable>
241249 <ScrollView
......@@ -251,10 +259,10 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {
251259 const draft = props.drafts[question.id];
252260 return (
253261 <View key={question.id} className="gap-2 pt-1">
254 <Text className="font-t3-bold text-xs uppercase tracking-[1px] text-neutral-500 dark:text-neutral-500">
262 <Text className="font-t3-bold text-xs uppercase tracking-[1px] text-neutral-500">
255263 {question.header}
256264 </Text>
257 <Text className="font-sans text-base leading-snug text-neutral-950 dark:text-neutral-50">
265 <Text className="font-sans text-base leading-snug text-adaptive-neutral-950-50">
258266 {question.question}
259267 </Text>
260268 <View className="gap-2">
......@@ -268,8 +276,8 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {
268276 className={cn(
269277 "min-h-12 w-full rounded-2xl border px-3.5 py-3",
270278 selected
271 ? "border-blue-300/50 bg-blue-50 dark:border-blue-400/28 dark:bg-blue-400/14"
272 : "border-neutral-200 bg-white dark:border-white/6 dark:bg-neutral-950/70",
279 ? "border-adaptive-blue-300-a50-blue-400-a28 bg-adaptive-blue-50-blue-400-a14"
280 : "border-adaptive-neutral-200-white-a6 bg-adaptive-white-neutral-950-a70",
273281 )}
274282 onPress={() =>
275283 props.onSelectOption(
......@@ -284,14 +292,14 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {
284292 className={cn(
285293 "font-t3-bold text-sm",
286294 selected
287 ? "text-sky-700 dark:text-sky-300"
288 : "text-neutral-700 dark:text-neutral-200",
295 ? "text-adaptive-sky-700-300"
296 : "text-adaptive-neutral-600-300",
289297 )}
290298 >
291299 {option.label}
292300 </Text>
293301 {description ? (
294 <Text className="font-sans text-sm leading-5 text-neutral-500 dark:text-neutral-400">
302 <Text className="font-sans text-sm leading-5 text-adaptive-neutral-500-400">
295303 {description}
296304 </Text>
297305 ) : null}
......@@ -308,7 +316,7 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {
308316 onFocus={() => props.onInputFocusChange?.(true)}
309317 onBlur={() => props.onInputFocusChange?.(false)}
310318 placeholder="Or type a custom answer"
311 className="min-h-[54px] rounded-2xl border border-neutral-200 bg-white px-3.5 py-3 font-sans text-base text-neutral-950 dark:border-white/8 dark:bg-neutral-950/70 dark:text-neutral-50"
319 className="min-h-[54px] rounded-2xl border border-adaptive-neutral-200-white-a8 bg-adaptive-white-neutral-950-a70 px-3.5 py-3 font-sans text-base text-adaptive-neutral-950-50"
312320 />
313321 </View>
314322 );
......@@ -317,7 +325,7 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {
317325 <Pressable
318326 className={cn(
319327 "items-center justify-center rounded-2xl px-4 py-3.5",
320 props.answers ? "bg-blue-500" : "bg-neutral-200 dark:bg-neutral-700/60",
328 props.answers ? "bg-blue-500" : "bg-adaptive-neutral-200-700-a60",
321329 )}
322330 disabled={
323331 props.answers === null || props.respondingUserInputId === props.pendingUserInput.requestId
apps/mobile/src/features/threads/ThreadComposer.tsx+9-46
......@@ -16,15 +16,7 @@ import {
1616import { StackActions, useFocusEffect, useNavigation } from "@react-navigation/native";
1717import type { ReactNode } from "react";
1818import { memo, useCallback, useEffect, useMemo, useRef, useState, type RefObject } from "react";
19import {
20 ActivityIndicator,
21 Image,
22 Platform,
23 Pressable,
24 StyleSheet,
25 View,
26 type ViewStyle,
27} from "react-native";
19import { ActivityIndicator, Image, Platform, Pressable, View, type ViewStyle } from "react-native";
2820import ImageViewing from "react-native-image-viewing";
2921import Animated, {
3022 FadeIn,
......@@ -33,8 +25,7 @@ import Animated, {
3325 FadeOutDown,
3426 LinearTransition,
3527} from "react-native-reanimated";
36import { useThemeColor } from "../../lib/useThemeColor";
37import { themeColorWithAlpha } from "../../lib/mobileTheme";
28import { useUniwindTheme } from "../../lib/useUniwindTheme";
3829import { armAgentAwarenessLiveActivityForLocalWork } from "../agent-awareness/remoteRegistration";
3930import { scopedThreadKey } from "../../lib/scopedEntities";
4031
......@@ -57,7 +48,6 @@ import { ProviderIcon } from "../../components/ProviderIcon";
5748import type { DraftComposerImageAttachment } from "../../lib/composerImages";
5849import { buildModelOptions, groupByProvider } from "../../lib/modelOptions";
5950import { useScaledTextRole } from "../settings/appearance/useScaledTextRole";
60import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
6151import type { RemoteClientConnectionState } from "../../lib/connection";
6252import {
6353 insertRankedSearchResult,
......@@ -142,19 +132,14 @@ const COMPOSER_LAYOUT_TRANSITION =
142132export function ComposerSurface(props: {
143133 readonly children: ReactNode;
144134 readonly style: ViewStyle;
145 readonly isDarkMode: boolean;
146135 /** Existing thread composers morph between pill and card layouts. */
147136 readonly animateLayout?: boolean;
148137}) {
149 const cardColor = useThemeColor("--color-card-translucent");
150 const borderColor = useThemeColor("--color-border");
151 const shadowColor = useThemeColor("--color-primary-shadow");
152138 // Drop shadow lives on a wrapper: `overflow: "hidden"` on the surface itself
153139 // (needed to clip content to the pill shape) would clip the shadow on iOS.
154140 const shadowStyle: ViewStyle = {
155141 borderRadius: props.style.borderRadius,
156 shadowColor,
157 shadowOpacity: props.isDarkMode ? 0.35 : 0.12,
142 shadowOpacity: 1,
158143 shadowRadius: 14,
159144 shadowOffset: { width: 0, height: 6 },
160145 elevation: 10,
......@@ -162,16 +147,13 @@ export function ComposerSurface(props: {
162147
163148 return (
164149 <Animated.View
150 className="shadow-adaptive-black-a15-a35"
165151 layout={props.animateLayout === false ? undefined : COMPOSER_LAYOUT_TRANSITION}
166152 style={shadowStyle}
167153 >
168154 <GlassSurface
169155 chrome="none"
170 fallbackStyle={{
171 backgroundColor: cardColor,
172 borderWidth: 1,
173 borderColor,
174 }}
156 fallbackClassName="border border-border bg-card-translucent"
175157 glassEffectStyle="regular"
176158 // The composer is a passive material containing interactive controls.
177159 // Expo GlassView defaults to non-interactive and both layouts share it.
......@@ -240,8 +222,6 @@ const ComposerConnectionStatusPill = memo(function ComposerConnectionStatusPill(
240222 readonly status: ComposerStatusPillState;
241223}) {
242224 const isReconnecting = props.status.kind !== "unavailable";
243 const indicatorColor = useThemeColor("--color-icon-muted");
244
245225 return (
246226 <Animated.View
247227 className="absolute inset-x-0 bottom-full items-center pb-2"
......@@ -255,7 +235,7 @@ const ComposerConnectionStatusPill = memo(function ComposerConnectionStatusPill(
255235 className="max-w-full flex-row items-center gap-2 rounded-full bg-card px-3 py-2 shadow-sm active:opacity-70"
256236 >
257237 {isReconnecting ? (
258 <ActivityIndicator size="small" color={indicatorColor} />
238 <ActivityIndicator size="small" colorClassName={"accent-icon-muted"} />
259239 ) : (
260240 <View className="h-2 w-2 rounded-full bg-red-500" />
261241 )}
......@@ -272,9 +252,7 @@ const ComposerConnectionStatusPill = memo(function ComposerConnectionStatusPill(
272252
273253export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposerProps) {
274254 const navigation = useNavigation();
275 const { themeAppearance } = useAppearancePreferences();
276 const isDarkMode = themeAppearance === "dark";
277 const foregroundColor = useThemeColor("--color-foreground");
255 const foregroundColor = useUniwindTheme()["--color-foreground"];
278256 const bodyText = useScaledTextRole("body");
279257 const fallbackInputRef = useRef<ComposerEditorHandle>(null);
280258 const inputRef = props.editorRef ?? fallbackInputRef;
......@@ -342,11 +320,6 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
342320 environmentLabel: props.environmentLabel,
343321 threadSyncPhase: props.threadSyncPhase,
344322 });
345 const toolbarSurface = String(useThemeColor("--color-card"));
346 const backdropSurface = String(useThemeColor("--color-screen"));
347 const toolbarFadeOpaque = themeColorWithAlpha(toolbarSurface, 0.95);
348 const toolbarFadeTransparent = themeColorWithAlpha(toolbarSurface, 0);
349 const backdropGradient = `linear-gradient(to bottom, ${themeColorWithAlpha(backdropSurface, 0)} 0%, ${themeColorWithAlpha(backdropSurface, 0.6)} 55%, ${themeColorWithAlpha(backdropSurface, 0.9)} 100%)`;
350323 const selectedProviderStatus = useMemo(() => {
351324 if (!props.serverConfig) return null;
352325 return (
......@@ -723,13 +696,8 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
723696 silently drops experimental_backgroundImage on Android, which left this
724697 strip fully transparent and the feed text legible through the composer. */}
725698 <View
699 className="absolute inset-0 bg-linear-to-b from-screen/0 via-screen/60 to-screen/90"
726700 pointerEvents="none"
727 style={[
728 StyleSheet.absoluteFill,
729 {
730 experimental_backgroundImage: backdropGradient,
731 },
732 ]}
733701 />
734702 <Animated.View
735703 className="relative w-full self-center"
......@@ -755,7 +723,6 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
755723 ) : null}
756724
757725 <ComposerSurface
758 isDarkMode={isDarkMode}
759726 style={
760727 isExpanded
761728 ? {
......@@ -865,11 +832,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
865832 ) : null}
866833 {isExpanded ? (
867834 <ComposerToolbarRow paddingBottom={0} paddingHorizontal={0} paddingTop={4}>
868 <ComposerToolbarScroller
869 fadeOpaque={toolbarFadeOpaque}
870 fadeTransparent={toolbarFadeTransparent}
871 contentPaddingRight={8}
872 >
835 <ComposerToolbarScroller contentPaddingRight={8}>
873836 <ComposerToolbarButton
874837 accessibilityLabel="Add attachment"
875838 icon="plus"
apps/mobile/src/features/threads/ThreadFeed.tsx+46-56
......@@ -46,7 +46,7 @@ import { TouchableOpacity } from "react-native-gesture-handler";
4646import ImageViewing from "react-native-image-viewing";
4747import { useSafeAreaInsets } from "react-native-safe-area-context";
4848import Animated, { FadeIn, FadeInUp, type SharedValue } from "react-native-reanimated";
49import { useThemeColor } from "../../lib/useThemeColor";
49import { useUniwindTheme } from "../../lib/useUniwindTheme";
5050import { IOS_NAV_BAR_HEIGHT } from "../../lib/layoutMetrics";
5151import { useFontFamily } from "../../lib/useFontFamily";
5252import { scopedThreadKey } from "../../lib/scopedEntities";
......@@ -209,7 +209,6 @@ function ThreadMarkdownImageView(props: {
209209 readonly alt: string | null;
210210 readonly onPressImage: (uri: string) => void;
211211}) {
212 const codeBackground = useThemeColor("--color-md-code-bg");
213212 const [availableWidth, setAvailableWidth] = useState(0);
214213 const [sourceSize, setSourceSize] = useState<{ width: number; height: number } | null>(null);
215214 const [failedUri, setFailedUri] = useState<string | null>(null);
......@@ -242,12 +241,9 @@ function ThreadMarkdownImageView(props: {
242241 >
243242 {props.uri === null || failed ? (
244243 <View
244 className="items-center justify-center rounded-[10px] bg-md-code-bg"
245245 style={{
246246 ...frameStyle,
247 borderRadius: 10,
248 backgroundColor: codeBackground,
249 alignItems: "center",
250 justifyContent: "center",
251247 }}
252248 >
253249 {failed ? (
......@@ -265,13 +261,9 @@ function ThreadMarkdownImageView(props: {
265261 style={{ alignSelf: "flex-start" }}
266262 >
267263 <View
264 className="items-center justify-center overflow-hidden rounded-[10px] bg-md-code-bg"
268265 style={{
269266 ...frameStyle,
270 borderRadius: 10,
271 backgroundColor: codeBackground,
272 alignItems: "center",
273 justifyContent: "center",
274 overflow: "hidden",
275267 }}
276268 >
277269 <ThreadMarkdownImageRequest
......@@ -556,23 +548,18 @@ function MarkdownCodeBlock(props: {
556548}
557549
558550function useReviewCommentColors(): ReviewCommentColors {
559 const background = useThemeColor("--color-card");
560 const border = useThemeColor("--color-border");
561 const mutedBackground = useThemeColor("--color-subtle");
562 const text = useThemeColor("--color-foreground");
563 const mutedText = useThemeColor("--color-foreground-muted");
564 const codeBackground = useThemeColor("--color-md-code-bg");
551 const theme = useUniwindTheme();
565552
566553 return useMemo(
567554 () => ({
568 background,
569 border,
570 mutedBackground,
571 text,
572 mutedText,
573 codeBackground,
555 background: theme["--color-card"],
556 border: theme["--color-border"],
557 mutedBackground: theme["--color-subtle"],
558 text: theme["--color-foreground"],
559 mutedText: theme["--color-foreground-muted"],
560 codeBackground: theme["--color-md-code-bg"],
574561 }),
575 [background, border, codeBackground, mutedBackground, mutedText, text],
562 [theme],
576563 );
577564}
578565
......@@ -590,25 +577,26 @@ function useMarkdownStyles(
590577 [appearance.baseFontSize],
591578 );
592579 const themeMode = themeAppearance;
593 const markdownBodyColor = String(useThemeColor("--color-md-body"));
594 const markdownStrongColor = String(useThemeColor("--color-md-strong"));
595 const markdownLinkColor = String(useThemeColor("--color-md-link"));
596 const markdownBlockquoteBg = String(useThemeColor("--color-md-blockquote-bg"));
597 const markdownBlockquoteBorder = String(useThemeColor("--color-md-blockquote-border"));
598 const markdownCodeBg = String(useThemeColor("--color-md-code-bg"));
599 const markdownCodeText = String(useThemeColor("--color-md-code-text"));
600 const markdownInlineCodeText = String(useThemeColor("--color-foreground-secondary"));
601 const markdownHrColor = String(useThemeColor("--color-md-hr"));
602 const markdownUserBodyColor = String(useThemeColor("--color-user-bubble-foreground"));
603 const markdownUserCodeBg = String(useThemeColor("--color-md-user-code-bg"));
604 const markdownUserCodeText = String(useThemeColor("--color-md-user-code-text"));
605 const markdownUserInlineCodeText = String(useThemeColor("--color-user-bubble-foreground-muted"));
606 const markdownUserFenceBg = String(useThemeColor("--color-md-user-fence-bg"));
607 const markdownUserFenceText = String(useThemeColor("--color-md-user-fence-text"));
608 const iconSubtleColor = String(useThemeColor("--color-icon-subtle"));
609 const inlineSkillForeground = String(useThemeColor("--color-inline-skill-foreground"));
610 const userBubbleSkillForeground = String(useThemeColor("--color-user-bubble-skill-foreground"));
611 const userBubbleForegroundMuted = String(useThemeColor("--color-user-bubble-foreground-muted"));
580 const theme = useUniwindTheme();
581 const markdownBodyColor = theme["--color-md-body"];
582 const markdownStrongColor = theme["--color-md-strong"];
583 const markdownLinkColor = theme["--color-md-link"];
584 const markdownBlockquoteBg = theme["--color-md-blockquote-bg"];
585 const markdownBlockquoteBorder = theme["--color-md-blockquote-border"];
586 const markdownCodeBg = theme["--color-md-code-bg"];
587 const markdownCodeText = theme["--color-md-code-text"];
588 const markdownInlineCodeText = theme["--color-foreground-secondary"];
589 const markdownHrColor = theme["--color-md-hr"];
590 const markdownUserBodyColor = theme["--color-user-bubble-foreground"];
591 const markdownUserCodeBg = theme["--color-md-user-code-bg"];
592 const markdownUserCodeText = theme["--color-md-user-code-text"];
593 const markdownUserInlineCodeText = theme["--color-user-bubble-foreground-muted"];
594 const markdownUserFenceBg = theme["--color-md-user-fence-bg"];
595 const markdownUserFenceText = theme["--color-md-user-fence-text"];
596 const iconSubtleColor = theme["--color-icon-subtle"];
597 const inlineSkillForeground = theme["--color-inline-skill-foreground"];
598 const userBubbleSkillForeground = theme["--color-user-bubble-skill-foreground"];
599 const userBubbleForegroundMuted = theme["--color-user-bubble-foreground-muted"];
612600 const regularFontFamily = useFontFamily("regular");
613601 const boldFontFamily = useFontFamily("bold");
614602
......@@ -1008,7 +996,7 @@ function renderFeedEntry(
1008996 accessibilityState={{ expanded: entry.expanded }}
1009997 onPress={() => props.onToggleTurnFold(entry.turnId)}
1010998 hitSlop={4}
1011 className="mb-3 min-h-11 flex-row items-center gap-2 border-b border-neutral-200/80 px-2 dark:border-white/[0.08]"
999 className="mb-3 min-h-11 flex-row items-center gap-2 border-b border-adaptive-neutral-200-a80-white-a8 px-2"
10121000 >
10131001 <Text className="font-t3-medium text-sm tabular-nums text-foreground-muted">
10141002 {entry.label}
......@@ -1016,7 +1004,7 @@ function renderFeedEntry(
10161004 <SymbolView
10171005 name={entry.expanded ? "chevron.down" : "chevron.right"}
10181006 size={15}
1019 tintColor={iconSubtleColor}
1007 tintColorClassName={"accent-icon-subtle"}
10201008 type="monochrome"
10211009 />
10221010 </Pressable>
......@@ -1102,7 +1090,7 @@ function renderFeedEntry(
11021090 })}
11031091 </View>
11041092 <View className="mt-1 flex-row items-center justify-end gap-1 pr-0.5">
1105 <Text className="font-t3-medium text-xs tabular-nums text-neutral-600 dark:text-neutral-400">
1093 <Text className="font-t3-medium text-xs tabular-nums text-adaptive-neutral-600-400">
11061094 {timestampLabel}
11071095 </Text>
11081096 {message.text.trim().length > 0 ? (
......@@ -1157,7 +1145,7 @@ function renderFeedEntry(
11571145 key={attachment.id}
11581146 environmentId={props.environmentId}
11591147 attachmentId={attachment.id}
1160 className="mt-1.5 aspect-[1.3] w-full rounded-[18px] bg-neutral-200 dark:bg-neutral-800"
1148 className="mt-1.5 aspect-[1.3] w-full rounded-[18px] bg-adaptive-neutral-200-800"
11611149 onPressImage={props.onPressImage}
11621150 />
11631151 );
......@@ -1171,7 +1159,7 @@ function renderFeedEntry(
11711159 buttonSize={28}
11721160 iconSize={13}
11731161 />
1174 <Text className="font-t3-medium text-xs tabular-nums text-neutral-600 dark:text-neutral-400">
1162 <Text className="font-t3-medium text-xs tabular-nums text-adaptive-neutral-600-400">
11751163 {timestampLabel}
11761164 </Text>
11771165 </View>
......@@ -1207,11 +1195,11 @@ const WorkingTimelineRow = memo(function WorkingTimelineRow(props: { readonly st
12071195 return (
12081196 <View className="mb-4 flex-row items-center gap-2 px-1.5 py-1">
12091197 <View className="flex-row items-center gap-1">
1210 <View className="h-1 w-1 rounded-full bg-neutral-400 dark:bg-neutral-500" />
1211 <View className="h-1 w-1 rounded-full bg-neutral-400/80 dark:bg-neutral-500/80" />
1212 <View className="h-1 w-1 rounded-full bg-neutral-400/60 dark:bg-neutral-500/60" />
1198 <View className="h-1 w-1 rounded-full bg-adaptive-neutral-400-500" />
1199 <View className="h-1 w-1 rounded-full bg-adaptive-neutral-400-a80-500-a80" />
1200 <View className="h-1 w-1 rounded-full bg-adaptive-neutral-400-a60-500-a60" />
12131201 </View>
1214 <Text className="font-t3-medium text-xs tabular-nums text-neutral-600 dark:text-neutral-400">
1202 <Text className="font-t3-medium text-xs tabular-nums text-adaptive-neutral-600-400">
12151203 Working for {durationLabel}
12161204 </Text>
12171205 </View>
......@@ -1303,6 +1291,7 @@ const ReviewCommentCard = memo(function ReviewCommentCard(props: {
13031291}) {
13041292 const { codeSurface, nativeReviewDiffStyle } = useAppearanceCodeSurface();
13051293 const { themeAppearance: appearanceScheme, themeId } = useAppearancePreferences();
1294 const appTheme = useUniwindTheme();
13061295 const NativeReviewDiffView = resolveNativeReviewDiffView();
13071296 const patch = useMemo(() => buildReviewCommentPatch(props.comment), [props.comment]);
13081297 const parsedDiff = useMemo(
......@@ -1315,8 +1304,8 @@ const ReviewCommentCard = memo(function ReviewCommentCard(props: {
13151304 [nativeReviewDiffData.rows],
13161305 );
13171306 const nativeReviewDiffTheme = useMemo(
1318 () => createNativeReviewDiffTheme(appearanceScheme, themeId),
1319 [appearanceScheme, themeId],
1307 () => createNativeReviewDiffTheme(appearanceScheme, themeId, appTheme),
1308 [appearanceScheme, appTheme, themeId],
13201309 );
13211310 const nativeRowsJson = useMemo(() => JSON.stringify(compactNativeRows), [compactNativeRows]);
13221311 const nativeThemeJson = useMemo(
......@@ -1578,8 +1567,9 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) {
15781567 ? navigationHeaderHeight || insets.top + IOS_NAV_BAR_HEIGHT
15791568 : topContentInset;
15801569
1581 const iconSubtleColor = useThemeColor("--color-icon-subtle");
1582 const userBubbleColor = useThemeColor("--color-user-bubble");
1570 const theme = useUniwindTheme();
1571 const iconSubtleColor = theme["--color-icon-subtle"];
1572 const userBubbleColor = theme["--color-user-bubble"];
15831573 const onMarkdownLinkPress = useCallback(
15841574 (href: string) => {
15851575 const presentation = resolveMarkdownLinkPresentation(href);
apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx+13-29
......@@ -27,7 +27,6 @@ import { SymbolView } from "../../components/AppSymbol";
2727import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
2828import { NativeStackScreenOptions } from "../../native/StackHeader";
2929import { scopedProjectKey, scopedThreadKey } from "../../lib/scopedEntities";
30import { useThemeColor } from "../../lib/useThemeColor";
3130import { useProjects, useThreadShells } from "../../state/entities";
3231import { mobilePreferencesAtom } from "../../state/preferences";
3332import { useThreadSearch } from "../../state/queries";
......@@ -127,19 +126,11 @@ export function ThreadNavigationSidebar(props: ThreadNavigationSidebarProps) {
127126}
128127
129128function NativeSidebarContainer(props: ThreadNavigationSidebarProps) {
130 const backgroundColor = useThemeColor("--color-drawer");
131 const borderColor = useThemeColor("--color-border");
132
133129 return (
134130 <View
135131 testID="thread-navigation-sidebar"
136 className="flex-1"
137 style={{
138 width: props.width,
139 backgroundColor,
140 borderRightColor: borderColor,
141 borderRightWidth: StyleSheet.hairlineWidth,
142 }}
132 className="flex-1 border-border bg-drawer"
133 style={{ borderRightWidth: StyleSheet.hairlineWidth, width: props.width }}
143134 >
144135 <SidebarNavigationShell>
145136 <ThreadNavigationSidebarPane {...props} nativeChrome />
......@@ -731,10 +722,6 @@ function ThreadNavigationSidebarPane(
731722 ],
732723 );
733724
734 const backgroundColor = useThemeColor("--color-drawer");
735 const borderColor = useThemeColor("--color-border");
736 const mutedColor = useThemeColor("--color-foreground-muted");
737 const placeholderColor = useThemeColor("--color-placeholder");
738725 const [measuredHeaderHeight, setMeasuredHeaderHeight] = useState<number | null>(null);
739726 // The sticky header (title row, search field, optional connection status)
740727 // is measured so the list inset always matches its real height — no
......@@ -1242,13 +1229,8 @@ function ThreadNavigationSidebarPane(
12421229 return (
12431230 <View
12441231 testID="thread-navigation-sidebar"
1245 className="flex-1"
1246 style={{
1247 width: props.width,
1248 backgroundColor,
1249 borderRightColor: borderColor,
1250 borderRightWidth: StyleSheet.hairlineWidth,
1251 }}
1232 className="flex-1 border-r border-border bg-drawer"
1233 style={{ width: props.width }}
12521234 >
12531235 <View className="flex-1" style={{ paddingBottom: insets.bottom }}>
12541236 <SwipeableScrollGateProvider enabled={swipeEnabled}>
......@@ -1286,14 +1268,11 @@ function ThreadNavigationSidebarPane(
12861268 </View>
12871269
12881270 <View
1289 className="absolute inset-x-0 top-0 z-[4]"
1271 className="absolute inset-x-0 top-0 z-[4] bg-drawer"
12901272 collapsable={false}
12911273 onLayout={handleStickyHeaderLayout}
12921274 pointerEvents="auto"
1293 style={{
1294 paddingTop: insets.top,
1295 backgroundColor,
1296 }}
1275 style={{ paddingTop: insets.top }}
12971276 >
12981277 <View className="h-[50px] flex-row items-end gap-0.5 pr-2 pl-5">
12991278 {/* Title slot doubles as the connection status surface: while an
......@@ -1318,7 +1297,12 @@ function ThreadNavigationSidebarPane(
13181297 </View>
13191298
13201299 <View className="mx-4 mt-[9px] h-[38px] flex-row items-center gap-1.5 rounded-xl bg-sidebar-search pr-2.5 pl-[11px]">
1321 <SymbolView name="magnifyingglass" size={15} tintColor={mutedColor} type="monochrome" />
1300 <SymbolView
1301 name="magnifyingglass"
1302 size={15}
1303 tintColorClassName={"accent-foreground-muted"}
1304 type="monochrome"
1305 />
13221306 <TextInput
13231307 ref={searchInputRef}
13241308 accessibilityLabel="Search threads"
......@@ -1327,7 +1311,7 @@ function ThreadNavigationSidebarPane(
13271311 clearButtonMode="while-editing"
13281312 onChangeText={props.onSearchQueryChange}
13291313 placeholder="Search"
1330 placeholderTextColor={placeholderColor}
1314 placeholderTextColorClassName={"accent-placeholder"}
13311315 returnKeyType="search"
13321316 className="h-[34px] flex-1 px-0 py-0 font-sans text-base text-foreground"
13331317 value={props.searchQuery}
apps/mobile/src/features/threads/ThreadSettingsSheet.tsx+13-11
......@@ -40,7 +40,7 @@ import { cn } from "../../lib/cn";
4040import type { ModelOption, ProviderGroup } from "../../lib/modelOptions";
4141import { applyProviderOptionSelection } from "../../lib/providerOptions";
4242import { resolveProviderOptionDescriptors } from "../../lib/providerOptions";
43import { useThemeColor } from "../../lib/useThemeColor";
43import { useUniwindTheme } from "../../lib/useUniwindTheme";
4444import {
4545 NativeHeaderToolbar,
4646 NativeStackScreenOptions,
......@@ -91,7 +91,6 @@ function ModelRow(props: {
9191 readonly isFirst: boolean;
9292 readonly isLast: boolean;
9393}) {
94 const checkmarkColor = useThemeColor("--color-icon");
9594 return (
9695 <Pressable
9796 accessibilityLabel={props.option.label}
......@@ -122,7 +121,7 @@ function ModelRow(props: {
122121 <SymbolView
123122 name="checkmark"
124123 size={16}
125 tintColor={checkmarkColor}
124 tintColorClassName={"accent-icon"}
126125 type="monochrome"
127126 weight="semibold"
128127 />
......@@ -140,7 +139,6 @@ function ProviderHeader(props: {
140139 readonly modelCount: number;
141140 readonly onToggle: () => void;
142141}) {
143 const iconSubtle = useThemeColor("--color-icon-subtle");
144142 const content = (
145143 <>
146144 <ProviderIcon provider={props.driver} size={15} />
......@@ -156,7 +154,7 @@ function ProviderHeader(props: {
156154 <SymbolView
157155 name={props.collapsed ? "chevron.down" : "chevron.up"}
158156 size={12}
159 tintColor={iconSubtle}
157 tintColorClassName={"accent-icon-subtle"}
160158 type="monochrome"
161159 />
162160 </>
......@@ -192,7 +190,6 @@ function DisclosureRow(props: {
192190 readonly onPress: () => void;
193191 readonly isLast?: boolean;
194192}) {
195 const iconSubtle = useThemeColor("--color-icon-subtle");
196193 return (
197194 <Pressable
198195 accessibilityRole="button"
......@@ -209,7 +206,12 @@ function DisclosureRow(props: {
209206 {props.value}
210207 </Text>
211208 ) : null}
212 <SymbolView name="chevron.right" size={12} tintColor={iconSubtle} type="monochrome" />
209 <SymbolView
210 name="chevron.right"
211 size={12}
212 tintColorClassName={"accent-icon-subtle"}
213 type="monochrome"
214 />
213215 </Pressable>
214216 );
215217}
......@@ -222,7 +224,6 @@ function ChoiceRow(props: {
222224 readonly onPress: () => void;
223225 readonly isLast: boolean;
224226}) {
225 const checkmarkColor = useThemeColor("--color-icon");
226227 return (
227228 <Pressable
228229 accessibilityLabel={props.description ? `${props.label}. ${props.description}` : props.label}
......@@ -244,7 +245,7 @@ function ChoiceRow(props: {
244245 <SymbolView
245246 name="checkmark"
246247 size={16}
247 tintColor={checkmarkColor}
248 tintColorClassName={"accent-icon"}
248249 type="monochrome"
249250 weight="semibold"
250251 />
......@@ -1125,8 +1126,9 @@ function ThreadSettingsChoiceScreen() {
11251126}
11261127
11271128function ThreadSettingsPickerNavigator(props: ThreadSettingsPickerPresentation) {
1128 const solidSheetBackground = String(useThemeColor("--color-sheet-solid"));
1129 const foreground = String(useThemeColor("--color-foreground"));
1129 const theme = useUniwindTheme();
1130 const solidSheetBackground = theme["--color-sheet-solid"];
1131 const foreground = theme["--color-foreground"];
11301132 const presentation = useMemo(
11311133 () => ({
11321134 onClose: props.onClose,
apps/mobile/src/features/threads/git/GitCommitSheet.tsx+1-1
......@@ -85,7 +85,7 @@ export function GitCommitSheet(_props: GitCommitSheetProps) {
8585 </Text>
8686 </View>
8787 {isDefaultRef ? (
88 <Text className="text-xs leading-normal text-amber-700 dark:text-amber-400">
88 <Text className="text-xs leading-normal text-adaptive-amber-700-400">
8989 Warning: this is the default branch.
9090 </Text>
9191 ) : null}
apps/mobile/src/features/threads/git/GitOverviewSheet.tsx+5-6
......@@ -17,7 +17,7 @@ import { Alert, Platform, Pressable, RefreshControl, ScrollView, View } from "re
1717
1818import { Screen, ScreenStack, ScreenStackHeaderConfig } from "react-native-screens";
1919import { useSafeAreaInsets } from "react-native-safe-area-context";
20import { useThemeColor } from "../../../lib/useThemeColor";
20import { useUniwindTheme } from "../../../lib/useUniwindTheme";
2121
2222import { AndroidSheetHeader } from "../../../components/AndroidScreenHeader";
2323import { AppText as Text } from "../../../components/AppText";
......@@ -53,10 +53,9 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) {
5353 const { selectedThreadCwd, selectedThreadWorktreePath } = useSelectedThreadWorktree();
5454 const gitState = useSelectedThreadGitState();
5555 const gitActions = useSelectedThreadGitActions();
56
57 const iconColor = useThemeColor("--color-icon");
58 const foregroundColor = String(useThemeColor("--color-foreground"));
59 const sheetColor = String(useThemeColor("--color-sheet"));
56 const theme = useUniwindTheme();
57 const foregroundColor = theme["--color-foreground"];
58 const sheetColor = theme["--color-sheet"];
6059
6160 const gitStatus = useEnvironmentQuery(
6261 selectedThread !== null && selectedThreadCwd !== null
......@@ -385,7 +384,7 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) {
385384 <SymbolView
386385 name="arrow.clockwise"
387386 size={16}
388 tintColor={iconColor}
387 tintColorClassName={"accent-icon"}
389388 type="monochrome"
390389 weight="medium"
391390 />
apps/mobile/src/features/threads/git/gitSheetComponents.tsx+24-12
......@@ -1,7 +1,6 @@
11import { SymbolView } from "../../../components/AppSymbol";
22import type { ComponentProps } from "react";
33import { Pressable, View } from "react-native";
4import { useThemeColor } from "../../../lib/useThemeColor";
54import { AppText as Text } from "../../../components/AppText";
65import { cn } from "../../../lib/cn";
76
......@@ -14,12 +13,13 @@ export function SheetActionButton(props: {
1413 readonly tone?: "primary" | "secondary" | "danger";
1514 readonly onPress: () => void;
1615}) {
17 const primaryFg = useThemeColor("--color-primary-foreground");
18 const dangerFg = useThemeColor("--color-danger-foreground");
19 const secondaryFg = useThemeColor("--color-secondary-foreground");
20
2116 const tone = props.tone ?? "secondary";
22 const textColor = tone === "primary" ? primaryFg : tone === "danger" ? dangerFg : secondaryFg;
17 const textColorClassName =
18 tone === "primary"
19 ? "accent-primary-foreground"
20 : tone === "danger"
21 ? "accent-danger-foreground"
22 : "accent-secondary-foreground";
2323
2424 return (
2525 <Pressable
......@@ -34,7 +34,12 @@ export function SheetActionButton(props: {
3434 disabled={props.disabled}
3535 onPress={props.onPress}
3636 >
37 <SymbolView name={props.icon} size={16} tintColor={textColor} type="monochrome" />
37 <SymbolView
38 name={props.icon}
39 size={16}
40 tintColorClassName={textColorClassName}
41 type="monochrome"
42 />
3843 <Text
3944 className={cn(
4045 "text-xs font-t3-bold tracking-[0.9px] uppercase",
......@@ -71,9 +76,6 @@ export function SheetListRow(props: {
7176 readonly disabled?: boolean;
7277 readonly onPress: () => void;
7378}) {
74 const iconColor = useThemeColor("--color-icon");
75 const iconSubtleColor = useThemeColor("--color-icon-subtle");
76
7779 return (
7880 <Pressable
7981 className="flex-row items-center gap-3 px-1 py-3 disabled:opacity-[0.45]"
......@@ -81,7 +83,12 @@ export function SheetListRow(props: {
8183 onPress={props.onPress}
8284 >
8385 <View className="bg-subtle h-9 w-9 items-center justify-center rounded-full">
84 <SymbolView name={props.icon} size={16} tintColor={iconColor} type="monochrome" />
86 <SymbolView
87 name={props.icon}
88 size={16}
89 tintColorClassName={"accent-icon"}
90 type="monochrome"
91 />
8592 </View>
8693 <View className="flex-1 gap-0.5">
8794 <Text className="text-foreground text-base font-t3-bold">{props.title}</Text>
......@@ -89,7 +96,12 @@ export function SheetListRow(props: {
8996 <Text className="text-foreground-muted text-xs leading-snug">{props.subtitle}</Text>
9097 ) : null}
9198 </View>
92 <SymbolView name="chevron.right" size={13} tintColor={iconSubtleColor} type="monochrome" />
99 <SymbolView
100 name="chevron.right"
101 size={13}
102 tintColorClassName={"accent-icon-subtle"}
103 type="monochrome"
104 />
93105 </Pressable>
94106 );
95107}
apps/mobile/src/features/threads/sidebar-filter-button.tsx+6-5
......@@ -1,8 +1,6 @@
11import { SymbolView } from "../../components/AppSymbol";
22import { Pressable } from "react-native";
33
4import { useThemeColor } from "../../lib/useThemeColor";
5
64export type SidebarFilterButtonIcon =
75 | "line.3.horizontal.decrease.circle"
86 | "line.3.horizontal.decrease.circle.fill";
......@@ -11,8 +9,6 @@ export function SidebarFilterButton(props: {
119 readonly accessibilityLabel: string;
1210 readonly icon: SidebarFilterButtonIcon;
1311}) {
14 const iconColor = useThemeColor("--color-foreground");
15
1612 return (
1713 <Pressable
1814 className="size-11 cursor-pointer items-center justify-center rounded-full bg-subtle active:opacity-70"
......@@ -20,7 +16,12 @@ export function SidebarFilterButton(props: {
2016 accessibilityRole="button"
2117 hitSlop={4}
2218 >
23 <SymbolView name={props.icon} size={16} tintColor={iconColor} type="monochrome" />
19 <SymbolView
20 name={props.icon}
21 size={16}
22 tintColorClassName="accent-foreground"
23 type="monochrome"
24 />
2425 </Pressable>
2526 );
2627}
apps/mobile/src/features/threads/sidebar-header-actions.tsx+6-5
......@@ -1,8 +1,6 @@
11import { SymbolView } from "../../components/AppSymbol";
22import { Pressable, View } from "react-native";
33
4import { useThemeColor } from "../../lib/useThemeColor";
5
64export interface SidebarHeaderActionsProps {
75 readonly onOpenSettings: () => void;
86}
......@@ -12,8 +10,6 @@ function FallbackHeaderButton(props: {
1210 readonly icon: "gearshape" | "square.and.pencil";
1311 readonly onPress: () => void;
1412}) {
15 const iconColor = useThemeColor("--color-foreground");
16
1713 return (
1814 <Pressable
1915 className="size-11 items-center justify-center rounded-full bg-subtle active:opacity-70"
......@@ -22,7 +18,12 @@ function FallbackHeaderButton(props: {
2218 hitSlop={4}
2319 onPress={props.onPress}
2420 >
25 <SymbolView name={props.icon} size={18} tintColor={iconColor} type="monochrome" />
21 <SymbolView
22 name={props.icon}
23 size={18}
24 tintColorClassName="accent-foreground"
25 type="monochrome"
26 />
2627 </Pressable>
2728 );
2829}
apps/mobile/src/features/threads/sidebar-navigation-shell.tsx+1-3
......@@ -10,7 +10,6 @@ import { getCompactBrandHeaderOptions } from "../../components/CompactBrandTitle
1010import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
1111import { nativeHeaderScrollEdgeEffects } from "../../native/StackHeader";
1212import { useMobileNavigationTheme } from "../../lib/useMobileNavigationTheme";
13import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
1413
1514const SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version);
1615
......@@ -53,8 +52,7 @@ const SidebarStack = createNativeStackNavigator();
5352 * navigation hooks used for header configuration inside the pane.
5453 */
5554export function SidebarNavigationShell(props: { readonly children: ReactNode }) {
56 const { themeAppearance } = useAppearancePreferences();
57 const navigationTheme = useMobileNavigationTheme(themeAppearance);
55 const navigationTheme = useMobileNavigationTheme();
5856
5957 return (
6058 <NavigationIndependentTree>
apps/mobile/src/features/threads/thread-list-items.tsx+18-21
......@@ -19,7 +19,7 @@ import { cn } from "../../lib/cn";
1919import { HOME_HORIZONTAL_INSET } from "../../lib/layoutMetrics";
2020import { relativeTime } from "../../lib/time";
2121import { themeColorWithAlpha } from "../../lib/mobileTheme";
22import { useThemeColor } from "../../lib/useThemeColor";
22import { useUniwindTheme } from "../../lib/useUniwindTheme";
2323import type { PendingNewTask } from "../../state/use-pending-new-tasks";
2424import { useThreadPr, type ThreadPr } from "../../state/use-thread-pr";
2525import type { HomeGroupDisplayAction } from "../home/homeListItems";
......@@ -87,7 +87,6 @@ export const ThreadListGroupHeader = memo(function ThreadListGroupHeader(props:
8787 readonly newThreadTarget?: EnvironmentProject | null;
8888 readonly onNewThread?: (project: EnvironmentProject) => void;
8989}) {
90 const iconMutedColor = useThemeColor("--color-icon-muted");
9190 const { groupKey, onGroupAction, onNewThread } = props;
9291 const newThreadTarget = props.newThreadTarget ?? null;
9392 const compact = props.variant === "compact";
......@@ -171,7 +170,7 @@ export const ThreadListGroupHeader = memo(function ThreadListGroupHeader(props:
171170 <SymbolView
172171 name="plus"
173172 size={compact ? 20 : 16}
174 tintColor={iconMutedColor}
173 tintColorClassName={"accent-icon-muted"}
175174 type="monochrome"
176175 weight="medium"
177176 />
......@@ -190,7 +189,6 @@ export const ThreadListShowMoreRow = memo(function ThreadListShowMoreRow(props:
190189 readonly groupKey: string;
191190 readonly onGroupAction: (key: string, action: HomeGroupDisplayAction) => void;
192191}) {
193 const iconSubtleColor = useThemeColor("--color-icon-subtle");
194192 const showsMore = props.hiddenCount > 0;
195193 const compact = props.variant === "compact";
196194 const { groupKey, onGroupAction } = props;
......@@ -221,7 +219,7 @@ export const ThreadListShowMoreRow = memo(function ThreadListShowMoreRow(props:
221219 <SymbolView
222220 name={icon}
223221 size={10}
224 tintColor={iconSubtleColor}
222 tintColorClassName={"accent-icon-subtle"}
225223 type="monochrome"
226224 weight="semibold"
227225 />
......@@ -275,10 +273,9 @@ export const PendingTaskListRow = memo(function PendingTaskListRow(props: {
275273 readonly onDeletePendingTask: (pendingTask: PendingNewTask) => void;
276274}) {
277275 const compact = props.variant === "compact";
278 const separatorColor = useThemeColor("--color-separator");
279 const iconSubtleColor = useThemeColor("--color-icon-subtle");
280 const mutedColor = useThemeColor("--color-foreground-muted");
281 const pressedBackgroundColor = useThemeColor("--color-subtle");
276 const theme = useUniwindTheme();
277 const separatorColor = theme["--color-separator"];
278 const pressedBackgroundColor = theme["--color-subtle"];
282279
283280 const { pendingTask, onSelectPendingTask, onDeletePendingTask } = props;
284281 const timestamp = relativeTime(pendingTask.message.createdAt);
......@@ -294,8 +291,8 @@ export const PendingTaskListRow = memo(function PendingTaskListRow(props: {
294291 );
295292
296293 const statusPill = (
297 <View className="rounded-full bg-zinc-500/12 px-1.5 py-0.5 dark:bg-zinc-500/16">
298 <Text className="text-3xs font-t3-bold text-zinc-600 dark:text-zinc-300">Pending</Text>
294 <View className="rounded-full bg-adaptive-zinc-500-a12-a16 px-1.5 py-0.5">
295 <Text className="text-3xs font-t3-bold text-adaptive-zinc-600-300">Pending</Text>
299296 </View>
300297 );
301298
......@@ -305,7 +302,7 @@ export const PendingTaskListRow = memo(function PendingTaskListRow(props: {
305302 <SymbolView
306303 name="tray.and.arrow.up"
307304 size={10}
308 tintColor={compact ? iconSubtleColor : mutedColor}
305 tintColorClassName={compact ? "accent-icon-subtle" : "accent-foreground-muted"}
309306 type="monochrome"
310307 />
311308 <Text
......@@ -355,7 +352,7 @@ export const PendingTaskListRow = memo(function PendingTaskListRow(props: {
355352 <SymbolView
356353 name="chevron.right"
357354 size={13}
358 tintColor={iconSubtleColor}
355 tintColorClassName={"accent-icon-subtle"}
359356 type="monochrome"
360357 />
361358 </View>
......@@ -446,13 +443,13 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
446443 // thread, so a hover highlight can't leak across rows.
447444 const [hovered, setHovered] = useRecyclingState(false);
448445
449 const separatorColor = useThemeColor("--color-separator");
450 const iconSubtleColor = useThemeColor("--color-icon-subtle");
451 const screenColor = useThemeColor("--color-screen");
452 const drawerColor = useThemeColor("--color-drawer");
453 const pressedBackgroundColor = useThemeColor("--color-subtle");
454 const selectedBackgroundColor = useThemeColor("--color-user-bubble");
455 const selectedForegroundColor = useThemeColor("--color-user-bubble-foreground");
446 const theme = useUniwindTheme();
447 const separatorColor = theme["--color-separator"];
448 const screenColor = theme["--color-screen"];
449 const drawerColor = theme["--color-drawer"];
450 const pressedBackgroundColor = theme["--color-subtle"];
451 const selectedBackgroundColor = theme["--color-user-bubble"];
452 const selectedForegroundColor = theme["--color-user-bubble-foreground"];
456453
457454 const { thread, onSelectThread, onArchiveThread, onDeleteThread, onRegenerateThreadTitle } =
458455 props;
......@@ -600,7 +597,7 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
600597 <SymbolView
601598 name="chevron.right"
602599 size={13}
603 tintColor={iconSubtleColor}
600 tintColorClassName={"accent-icon-subtle"}
604601 type="monochrome"
605602 />
606603 </View>
apps/mobile/src/features/threads/thread-list-v2-items.tsx+25-23
......@@ -16,7 +16,7 @@ import { ProjectFavicon } from "../../components/ProjectFavicon";
1616import { ProviderIcon } from "../../components/ProviderIcon";
1717import { cn } from "../../lib/cn";
1818import { relativeTime } from "../../lib/time";
19import { useThemeColor } from "../../lib/useThemeColor";
19import { useUniwindTheme } from "../../lib/useUniwindTheme";
2020import type { PendingNewTask } from "../../state/use-pending-new-tasks";
2121import { useThreadPr } from "../../state/use-thread-pr";
2222import { ThreadSwipeable } from "../home/thread-swipe-actions";
......@@ -52,10 +52,10 @@ const MONO_FONT = Platform.select({
5252const STATUS_LABEL_BY_STATUS: Partial<
5353 Record<ThreadListV2Status, { label: string; className: string }>
5454> = {
55 approval: { label: "Approval", className: "text-amber-700 dark:text-amber-300" },
56 input: { label: "Input", className: "text-indigo-600 dark:text-indigo-300" },
57 working: { label: "Working", className: "text-sky-600 dark:text-sky-400" },
58 failed: { label: "Failed", className: "text-red-700 dark:text-red-300" },
55 approval: { label: "Approval", className: "text-adaptive-amber-700-300" },
56 input: { label: "Input", className: "text-adaptive-indigo-600-300" },
57 working: { label: "Working", className: "text-adaptive-sky-600-400" },
58 failed: { label: "Failed", className: "text-adaptive-red-700-300" },
5959};
6060
6161function threadTimeLabel(thread: EnvironmentThreadShell): string {
......@@ -93,7 +93,6 @@ export const ThreadListV2SectionDivider = memo(function ThreadListV2SectionDivid
9393 readonly label: string;
9494 readonly pane?: "screen" | "sidebar";
9595}) {
96 const borderColor = useThemeColor("--color-border");
9796 return (
9897 <View
9998 className={cn(
......@@ -102,7 +101,7 @@ export const ThreadListV2SectionDivider = memo(function ThreadListV2SectionDivid
102101 )}
103102 >
104103 <Text className="text-xs font-t3-medium text-foreground-tertiary">{props.label}</Text>
105 <View className="h-px flex-1" style={{ backgroundColor: borderColor }} />
104 <View className="h-px flex-1 bg-border" />
106105 </View>
107106 );
108107});
......@@ -134,10 +133,10 @@ export const ThreadListV2SnoozedShelfHeader = memo(function ThreadListV2SnoozedS
134133 onPress={props.onToggle}
135134 style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1 })}
136135 >
137 <Text className="text-xs font-t3-medium text-blue-600 dark:text-blue-400">
136 <Text className="text-xs font-t3-medium text-adaptive-blue-600-400">
138137 {props.expanded ? "Snoozed" : `Snoozed (${props.count})`}
139138 </Text>
140 <View className="h-px flex-1 bg-blue-500/20 dark:bg-blue-400/15" />
139 <View className="h-px flex-1 bg-adaptive-blue-500-a20-blue-400-a15" />
141140 <SymbolView
142141 name="chevron.down"
143142 size={10}
......@@ -156,7 +155,6 @@ export const ThreadListV2SettledShelfHeader = memo(function ThreadListV2SettledS
156155 readonly onToggle: () => void;
157156 readonly pane?: "screen" | "sidebar";
158157}) {
159 const mutedColor = useThemeColor("--color-foreground-muted");
160158 return (
161159 <Pressable
162160 accessibilityHint={
......@@ -180,7 +178,7 @@ export const ThreadListV2SettledShelfHeader = memo(function ThreadListV2SettledS
180178 <SymbolView
181179 name="chevron.down"
182180 size={10}
183 tintColor={mutedColor}
181 tintColorClassName={"accent-foreground-muted"}
184182 type="monochrome"
185183 style={{ transform: [{ rotate: props.expanded ? "180deg" : "0deg" }] }}
186184 />
......@@ -213,8 +211,9 @@ export const ThreadListV2PendingRow = memo(function ThreadListV2PendingRow(props
213211 readonly onDeletePendingTask: (pendingTask: PendingNewTask) => void;
214212}) {
215213 const { pendingTask, onSelectPendingTask, onDeletePendingTask } = props;
216 const drawerColor = useThemeColor("--color-drawer");
217 const pressedBackgroundColor = useThemeColor("--color-subtle");
214 const theme = useUniwindTheme();
215 const drawerColor = theme["--color-drawer"];
216 const pressedBackgroundColor = theme["--color-subtle"];
218217 const sidebarPane = props.pane === "sidebar";
219218 const projectTitle =
220219 props.projectTitle ?? props.project?.title ?? pendingTask.creation.projectTitle ?? "";
......@@ -418,11 +417,11 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
418417 onChangeRequestState?.(threadKey, changeRequest);
419418 }, [onChangeRequestState, prState, prUpdatedAt, thread.linkedPullRequest, threadKey]);
420419
421 const screenColor = useThemeColor("--color-screen");
422 const drawerColor = useThemeColor("--color-drawer");
423 const pressedBackgroundColor = useThemeColor("--color-subtle");
424 const selectedBackgroundColor = useThemeColor("--color-user-bubble");
425 const pinTintColor = useThemeColor("--color-foreground-muted");
420 const theme = useUniwindTheme();
421 const screenColor = theme["--color-screen"];
422 const drawerColor = theme["--color-drawer"];
423 const pressedBackgroundColor = theme["--color-subtle"];
424 const selectedBackgroundColor = theme["--color-user-bubble"];
426425 const sidebarPane = props.pane === "sidebar";
427426 const selected = props.selected === true;
428427
......@@ -699,7 +698,12 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
699698 {props.projectTitle ?? props.project?.title ?? ""}
700699 </Text>
701700 {pinnedRow ? (
702 <SymbolView name="pin" size={11} tintColor={pinTintColor} type="monochrome" />
701 <SymbolView
702 name="pin"
703 size={11}
704 tintColorClassName={"accent-foreground-muted"}
705 type="monochrome"
706 />
703707 ) : null}
704708 <Text
705709 className={cn(
......@@ -735,9 +739,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
735739 <Text
736740 className={cn(
737741 "flex-1 text-xs",
738 selected
739 ? "text-user-bubble-foreground-muted"
740 : "text-red-600/80 dark:text-red-400/80",
742 selected ? "text-user-bubble-foreground-muted" : "text-adaptive-red-600-a80-400-a80",
741743 )}
742744 numberOfLines={1}
743745 >
......@@ -905,7 +907,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
905907 selected
906908 ? "text-user-bubble-foreground-muted"
907909 : snoozedRow
908 ? "text-blue-600 dark:text-blue-400"
910 ? "text-adaptive-blue-600-400"
909911 : "text-foreground-tertiary",
910912 )}
911913 style={{ fontFamily: MONO_FONT }}
apps/mobile/src/features/threads/thread-search-match.tsx+2-2
......@@ -65,8 +65,8 @@ export function ThreadSearchMatchExcerpt(props: {
6565 props.selected
6666 ? "text-user-bubble-foreground"
6767 : isUser
68 ? "text-blue-500 dark:text-blue-400"
69 : "text-emerald-600 dark:text-emerald-400",
68 ? "text-adaptive-blue-500-400"
69 : "text-adaptive-emerald-600-400",
7070 )}
7171 >
7272 {isUser ? "You:" : "Agent:"}{" "}
apps/mobile/src/features/threads/thread-work-log.tsx+5-14
......@@ -7,7 +7,6 @@ import { scaledTypographyLineHeight } from "../../lib/appearancePreferences";
77import { cn } from "../../lib/cn";
88import type { ThreadFeedActivity } from "../../lib/threadActivity";
99import { MOBILE_TYPOGRAPHY } from "../../lib/typography";
10import { useThemeColor } from "../../lib/useThemeColor";
1110import Animated, { FadeIn } from "react-native-reanimated";
1211
1312const WORK_LOG_LAYOUT_ANIMATION = {
......@@ -128,7 +127,6 @@ export function ThreadWorkLog(props: {
128127 readonly onCopyRow: (rowId: string, value: string) => void;
129128 readonly onToggleRow: (rowId: string) => void;
130129}) {
131 const pressedBackground = useThemeColor("--color-subtle");
132130 const rows = visibleWorkLogActivities(props.activities).map((activity) => ({
133131 ...activity,
134132 detail: compactActivityDetail(activity.detail),
......@@ -178,10 +176,7 @@ export function ThreadWorkLog(props: {
178176 }
179177 }}
180178 onLongPress={() => props.onCopyRow(row.id, row.getCopyText())}
181 style={({ pressed }) => ({
182 backgroundColor: pressed ? pressedBackground : "transparent",
183 })}
184 className="rounded-md px-0.5 py-0"
179 className="rounded-md px-0.5 py-0 active:bg-subtle"
185180 >
186181 <View className="min-h-8 flex-row items-center gap-1.5">
187182 <View className="h-[18px] w-5 shrink-0 items-center justify-center">
......@@ -198,7 +193,7 @@ export function ThreadWorkLog(props: {
198193 <Text
199194 className={cn(
200195 "font-t3-medium text-foreground",
201 iconIsDestructive && "text-rose-600 dark:text-rose-400",
196 iconIsDestructive && "text-adaptive-rose-600-400",
202197 )}
203198 >
204199 {row.summary}
......@@ -210,7 +205,7 @@ export function ThreadWorkLog(props: {
210205
211206 <View className="shrink-0 flex-row items-center gap-px">
212207 {props.copiedRowId === row.id ? (
213 <Text className="pr-1 font-t3-medium text-3xs text-emerald-600 dark:text-emerald-400">
208 <Text className="pr-1 font-t3-medium text-3xs text-adaptive-emerald-600-400">
214209 Copied
215210 </Text>
216211 ) : null}
......@@ -249,7 +244,7 @@ export function ThreadWorkLog(props: {
249244 </Pressable>
250245
251246 {fullDetail ? (
252 <View className="ml-7 border-l border-neutral-300/60 pb-1 pl-3 pt-0.5 dark:border-white/[0.12]">
247 <View className="ml-7 border-l border-adaptive-neutral-300-a60-white-a12 pb-1 pl-3 pt-0.5">
253248 <ScrollView
254249 nestedScrollEnabled
255250 directionalLockEnabled
......@@ -281,7 +276,6 @@ export function ThreadWorkGroupToggle(props: {
281276 readonly onlyToolActivities: boolean;
282277 readonly onToggle: () => void;
283278}) {
284 const pressedBackground = useThemeColor("--color-subtle");
285279 const noun = props.onlyToolActivities
286280 ? props.hiddenCount === 1
287281 ? "tool call"
......@@ -305,10 +299,7 @@ export function ThreadWorkGroupToggle(props: {
305299 void Haptics.selectionAsync();
306300 props.onToggle();
307301 }}
308 style={({ pressed }) => ({
309 backgroundColor: pressed ? pressedBackground : "transparent",
310 })}
311 className="min-h-8 flex-row items-center gap-1.5 rounded-md px-0.5 py-0"
302 className="min-h-8 flex-row items-center gap-1.5 rounded-md px-0.5 py-0 active:bg-subtle"
312303 >
313304 <View className="h-[18px] w-5 items-center justify-center">
314305 <SymbolView
apps/mobile/src/features/threads/threadPresentation.ts+12-12
......@@ -53,8 +53,8 @@ export function resolveThreadStatus(
5353 return {
5454 kind: "pending-approval",
5555 label: "Needs Approval",
56 pillClassName: "bg-amber-500/12 dark:bg-amber-500/16",
57 textClassName: "text-amber-700 dark:text-amber-300",
56 pillClassName: "bg-adaptive-amber-500-a12-a16",
57 textClassName: "text-adaptive-amber-700-300",
5858 iconColor: "#ff9f0a",
5959 iconBackground: "rgba(255,159,10,0.22)",
6060 pulse: false,
......@@ -65,8 +65,8 @@ export function resolveThreadStatus(
6565 return {
6666 kind: "awaiting-input",
6767 label: "Awaiting Input",
68 pillClassName: "bg-indigo-500/12 dark:bg-indigo-500/16",
69 textClassName: "text-indigo-700 dark:text-indigo-300",
68 pillClassName: "bg-adaptive-indigo-500-a12-a16",
69 textClassName: "text-adaptive-indigo-700-300",
7070 iconColor: "#5e5ce6",
7171 iconBackground: "rgba(94,92,230,0.22)",
7272 pulse: false,
......@@ -77,8 +77,8 @@ export function resolveThreadStatus(
7777 return {
7878 kind: "working",
7979 label: "Working",
80 pillClassName: "bg-sky-500/12 dark:bg-sky-500/16",
81 textClassName: "text-sky-700 dark:text-sky-300",
80 pillClassName: "bg-adaptive-sky-500-a12-a16",
81 textClassName: "text-adaptive-sky-700-300",
8282 iconColor: "#0a84ff",
8383 iconBackground: "rgba(10,132,255,0.22)",
8484 pulse: true,
......@@ -89,8 +89,8 @@ export function resolveThreadStatus(
8989 return {
9090 kind: "connecting",
9191 label: "Connecting",
92 pillClassName: "bg-sky-500/12 dark:bg-sky-500/16",
93 textClassName: "text-sky-700 dark:text-sky-300",
92 pillClassName: "bg-adaptive-sky-500-a12-a16",
93 textClassName: "text-adaptive-sky-700-300",
9494 iconColor: "#0a84ff",
9595 iconBackground: "rgba(10,132,255,0.22)",
9696 pulse: true,
......@@ -101,8 +101,8 @@ export function resolveThreadStatus(
101101 return {
102102 kind: "error",
103103 label: "Error",
104 pillClassName: "bg-rose-500/12 dark:bg-rose-500/16",
105 textClassName: "text-rose-700 dark:text-rose-300",
104 pillClassName: "bg-adaptive-rose-500-a12-a16",
105 textClassName: "text-adaptive-rose-700-300",
106106 iconColor: "#ff453a",
107107 iconBackground: "rgba(255,69,58,0.22)",
108108 pulse: false,
......@@ -117,8 +117,8 @@ export function resolveThreadStatus(
117117 return {
118118 kind: "plan-ready",
119119 label: "Plan Ready",
120 pillClassName: "bg-violet-500/12 dark:bg-violet-500/16",
121 textClassName: "text-violet-700 dark:text-violet-300",
120 pillClassName: "bg-adaptive-violet-500-a12-a16",
121 textClassName: "text-adaptive-violet-700-300",
122122 iconColor: "#bf5af2",
123123 iconBackground: "rgba(191,90,242,0.22)",
124124 pulse: false,
apps/mobile/src/lib/mobileDefaultTheme.ts deleted-139
......@@ -1,139 +0,0 @@
1import type { MobileThemeVariables } from "./mobileTheme";
2
3/** The existing T3 Code mobile palette, retained as the upgrade-safe default. */
4export const DEFAULT_MOBILE_THEME_VARIABLES = {
5 light: {
6 "--color-screen": "#f2f2f7",
7 "--color-sheet": "rgba(242, 242, 247, 0.98)",
8 "--color-sheet-solid": "#f2f2f7",
9 "--color-card": "#ffffff",
10 "--color-card-alt": "#f5f5f5",
11 "--color-card-translucent": "rgba(255, 255, 255, 0.8)",
12 "--color-foreground": "#262626",
13 "--color-foreground-secondary": "#525252",
14 "--color-foreground-muted": "#737373",
15 "--color-foreground-tertiary": "#8e8e93",
16 "--color-border": "rgba(0, 0, 0, 0.08)",
17 "--color-border-subtle": "rgba(0, 0, 0, 0.06)",
18 "--color-separator": "rgba(0, 0, 0, 0.04)",
19 "--color-subtle": "rgba(0, 0, 0, 0.04)",
20 "--color-subtle-strong": "rgba(0, 0, 0, 0.08)",
21 "--color-inline-skill-background": "rgba(217, 70, 239, 0.12)",
22 "--color-inline-skill-border": "rgba(217, 70, 239, 0.25)",
23 "--color-inline-skill-foreground": "#a21caf",
24 "--color-primary": "#262626",
25 "--color-primary-foreground": "#ffffff",
26 "--color-primary-shadow": "#000000",
27 "--color-secondary": "#ffffff",
28 "--color-secondary-foreground": "#262626",
29 "--color-secondary-border": "rgba(0, 0, 0, 0.08)",
30 "--color-switch-active-track": "#34c759",
31 "--color-switch-active-thumb": "#ffffff",
32 "--color-switch-inactive-track": "rgba(0, 0, 0, 0.08)",
33 "--color-switch-inactive-thumb": "#8e8e93",
34 "--color-danger": "#fef2f2",
35 "--color-danger-border": "rgba(239, 68, 68, 0.12)",
36 "--color-danger-foreground": "#dc2626",
37 "--color-input": "#ffffff",
38 "--color-input-border": "rgba(0, 0, 0, 0.1)",
39 "--color-sidebar-search": "rgba(118, 118, 128, 0.12)",
40 "--color-placeholder": "#737373",
41 "--color-icon": "#262626",
42 "--color-icon-muted": "#525252",
43 "--color-icon-subtle": "#a3a3a3",
44 "--color-header": "rgba(255, 255, 255, 0.97)",
45 "--color-header-border": "rgba(0, 0, 0, 0.06)",
46 "--color-glass-surface": "rgba(255, 255, 255, 0.72)",
47 "--color-glass-tint": "rgba(255, 255, 255, 0.18)",
48 "--color-status-bar": "#f2f2f7",
49 "--color-md-body": "#111111",
50 "--color-md-strong": "#000000",
51 "--color-md-link": "#2563eb",
52 "--color-md-blockquote-border": "rgba(0, 0, 0, 0.08)",
53 "--color-md-blockquote-bg": "rgba(0, 0, 0, 0.02)",
54 "--color-md-code-bg": "rgba(0, 0, 0, 0.04)",
55 "--color-md-code-text": "#262626",
56 "--color-md-user-code-bg": "rgba(255, 255, 255, 0.22)",
57 "--color-md-user-code-text": "#ffffff",
58 "--color-md-user-fence-bg": "rgba(0, 0, 0, 0.16)",
59 "--color-md-user-fence-text": "#ffffff",
60 "--color-md-hr": "rgba(0, 0, 0, 0.08)",
61 "--color-user-bubble": "#007aff",
62 "--color-user-bubble-foreground": "#ffffff",
63 "--color-user-bubble-foreground-muted": "rgba(255, 255, 255, 0.78)",
64 "--color-user-bubble-skill-foreground": "#f0abfc",
65 "--color-backdrop": "rgba(0, 0, 0, 0.22)",
66 "--color-drawer": "rgba(255, 255, 255, 0.99)",
67 "--color-drawer-shadow": "rgba(0, 0, 0, 0.12)",
68 "--color-dot-separator": "rgba(0, 0, 0, 0.2)",
69 "--color-wordmark": "#262626",
70 "--color-chevron": "rgba(0, 0, 0, 0.2)",
71 },
72 dark: {
73 "--color-screen": "#0a0a0a",
74 "--color-sheet": "rgba(14, 14, 14, 0.98)",
75 "--color-sheet-solid": "#0e0e0e",
76 "--color-card": "#171717",
77 "--color-card-alt": "#1c1c1c",
78 "--color-card-translucent": "rgba(17, 17, 17, 0.8)",
79 "--color-foreground": "#f5f5f5",
80 "--color-foreground-secondary": "#a3a3a3",
81 "--color-foreground-muted": "#8e8e93",
82 "--color-foreground-tertiary": "#636366",
83 "--color-border": "rgba(255, 255, 255, 0.06)",
84 "--color-border-subtle": "rgba(255, 255, 255, 0.04)",
85 "--color-separator": "rgba(255, 255, 255, 0.03)",
86 "--color-subtle": "rgba(255, 255, 255, 0.04)",
87 "--color-subtle-strong": "rgba(255, 255, 255, 0.08)",
88 "--color-inline-skill-background": "rgba(217, 70, 239, 0.12)",
89 "--color-inline-skill-border": "rgba(217, 70, 239, 0.25)",
90 "--color-inline-skill-foreground": "#f0abfc",
91 "--color-primary": "#f5f5f5",
92 "--color-primary-foreground": "#0a0a0a",
93 "--color-primary-shadow": "#000000",
94 "--color-secondary": "rgba(255, 255, 255, 0.04)",
95 "--color-secondary-foreground": "#f5f5f5",
96 "--color-secondary-border": "rgba(255, 255, 255, 0.06)",
97 "--color-switch-active-track": "#30d158",
98 "--color-switch-active-thumb": "#ffffff",
99 "--color-switch-inactive-track": "rgba(255, 255, 255, 0.06)",
100 "--color-switch-inactive-thumb": "#8e8e93",
101 "--color-danger": "rgba(239, 68, 68, 0.14)",
102 "--color-danger-border": "rgba(248, 113, 113, 0.18)",
103 "--color-danger-foreground": "#fca5a5",
104 "--color-input": "#141414",
105 "--color-input-border": "rgba(255, 255, 255, 0.08)",
106 "--color-sidebar-search": "rgba(118, 118, 128, 0.24)",
107 "--color-placeholder": "#8e8e93",
108 "--color-icon": "#f5f5f5",
109 "--color-icon-muted": "#a3a3a3",
110 "--color-icon-subtle": "#8e8e93",
111 "--color-header": "rgba(10, 10, 10, 0.97)",
112 "--color-header-border": "rgba(255, 255, 255, 0.06)",
113 "--color-glass-surface": "rgba(23, 23, 23, 0.78)",
114 "--color-glass-tint": "rgba(23, 23, 23, 0.24)",
115 "--color-status-bar": "#0a0a0a",
116 "--color-md-body": "#e5e5e5",
117 "--color-md-strong": "#f5f5f5",
118 "--color-md-link": "#60a5fa",
119 "--color-md-blockquote-border": "rgba(255, 255, 255, 0.1)",
120 "--color-md-blockquote-bg": "rgba(255, 255, 255, 0.03)",
121 "--color-md-code-bg": "rgba(255, 255, 255, 0.06)",
122 "--color-md-code-text": "#e5e5e5",
123 "--color-md-user-code-bg": "rgba(255, 255, 255, 0.18)",
124 "--color-md-user-code-text": "#ffffff",
125 "--color-md-user-fence-bg": "rgba(0, 0, 0, 0.28)",
126 "--color-md-user-fence-text": "#ffffff",
127 "--color-md-hr": "rgba(255, 255, 255, 0.08)",
128 "--color-user-bubble": "#0a84ff",
129 "--color-user-bubble-foreground": "#ffffff",
130 "--color-user-bubble-foreground-muted": "rgba(255, 255, 255, 0.78)",
131 "--color-user-bubble-skill-foreground": "#f0abfc",
132 "--color-backdrop": "rgba(0, 0, 0, 0.48)",
133 "--color-drawer": "rgba(14, 14, 14, 0.99)",
134 "--color-drawer-shadow": "rgba(0, 0, 0, 0.32)",
135 "--color-dot-separator": "rgba(255, 255, 255, 0.2)",
136 "--color-wordmark": "#f5f5f5",
137 "--color-chevron": "rgba(255, 255, 255, 0.2)",
138 },
139} as const satisfies Readonly<Record<"light" | "dark", MobileThemeVariables>>;
apps/mobile/src/lib/mobileTheme.test-support.ts created+20
......@@ -0,0 +1,20 @@
1import * as NodeFS from "node:fs";
2
3import type { MobileThemeAppearance, MobileThemeVariables } from "./mobileTheme";
4
5export function readDefaultMobileThemeVariables(
6 appearance: MobileThemeAppearance,
7): MobileThemeVariables {
8 const stylesheet = NodeFS.readFileSync(new URL("../../global.css", import.meta.url), "utf8");
9 const variant = new RegExp(`@variant ${appearance} \\{([\\s\\S]*?)\\n \\}`, "u").exec(
10 stylesheet,
11 )?.[1];
12 if (variant === undefined) throw new Error(`Missing default ${appearance} theme in global.css.`);
13
14 return Object.fromEntries(
15 Array.from(variant.matchAll(/(--color-[a-z0-9-]+):\s*([^;]+);/gu), ([, name, value]) => [
16 name,
17 value.trim(),
18 ]),
19 ) as MobileThemeVariables;
20}
apps/mobile/src/lib/mobileTheme.test.ts+13-29
......@@ -1,8 +1,6 @@
11import { describe, expect, it } from "vite-plus/test";
2import * as NodeFS from "node:fs";
3
42import { BUILT_IN_THEME_IDS, BUILT_IN_THEMES } from "@t3tools/shared/themePalettes";
5import { DEFAULT_MOBILE_THEME_VARIABLES } from "./mobileDefaultTheme";
3import { readDefaultMobileThemeVariables } from "./mobileTheme.test-support";
64
75import {
86 createMobileThemePairPatch,
......@@ -11,7 +9,6 @@ import {
119 DEFAULT_MOBILE_THEME_ID,
1210 getMobileThemePreviewColors,
1311 getMobileThemeVariables,
14 MOBILE_THEME_IDS,
1512 normalizeMobileThemeId,
1613 normalizeMobileThemeMode,
1714 resolveMobileThemeIds,
......@@ -52,13 +49,12 @@ function compositeOver(overlay: string, background: string): string {
5249
5350describe("mobile themes", () => {
5451 it("declares every runtime theme variable in the static stylesheet", () => {
55 const stylesheet = NodeFS.readFileSync(new URL("../../global.css", import.meta.url), "utf8");
56 const stylesheetVariables = new Set(
57 Array.from(stylesheet.matchAll(/--color-[a-z0-9-]+/g), ([variable]) => variable),
52 const generatedVariables = createMobileThemeVariables(BUILT_IN_THEMES[0].colors, "light");
53 expect(Object.keys(readDefaultMobileThemeVariables("light")).sort()).toEqual(
54 Object.keys(generatedVariables).sort(),
5855 );
59
60 expect(Array.from(stylesheetVariables).sort()).toEqual(
61 Object.keys(DEFAULT_MOBILE_THEME_VARIABLES.light).sort(),
56 expect(Object.keys(readDefaultMobileThemeVariables("dark")).sort()).toEqual(
57 Object.keys(generatedVariables).sort(),
6258 );
6359 });
6460
......@@ -71,17 +67,11 @@ describe("mobile themes", () => {
7167 });
7268
7369 it("preserves the existing mobile palette as the default", () => {
74 expect(getMobileThemeVariables(DEFAULT_MOBILE_THEME_ID, "light")["--color-screen"]).toBe(
75 "#f2f2f7",
70 expect(readDefaultMobileThemeVariables("light")["--color-screen"]).toBe("#f2f2f7");
71 expect(readDefaultMobileThemeVariables("dark")["--color-screen"]).toBe("#0a0a0a");
72 expect(readDefaultMobileThemeVariables("light")["--color-user-bubble-skill-foreground"]).toBe(
73 "#f0abfc",
7674 );
77 expect(getMobileThemeVariables(DEFAULT_MOBILE_THEME_ID, "dark")["--color-screen"]).toBe(
78 "#0a0a0a",
79 );
80 expect(
81 getMobileThemeVariables(DEFAULT_MOBILE_THEME_ID, "light")[
82 "--color-user-bubble-skill-foreground"
83 ],
84 ).toBe("#f0abfc");
8575 });
8676
8777 it("applies palette overrides on top of the selected built-in theme", () => {
......@@ -172,17 +162,11 @@ describe("mobile themes", () => {
172162 expect(variables["--color-backdrop"]).toBe("rgba(0, 0, 0, 0.22)");
173163 expect(variables["--color-drawer-shadow"]).toBe("rgba(0, 0, 0, 0.12)");
174164 expect(variables["--color-user-bubble-foreground"]).toMatch(/^#/);
175 expect(Object.keys(DEFAULT_MOBILE_THEME_VARIABLES.light).sort()).toEqual(
176 Object.keys(variables).sort(),
177 );
178 expect(Object.keys(DEFAULT_MOBILE_THEME_VARIABLES.dark).sort()).toEqual(
179 Object.keys(variables).sort(),
180 );
181165 });
182166
183167 it("keeps every built-in shadow and backdrop black-based in dark mode", () => {
184 for (const theme of BUILT_IN_THEMES) {
185 const variables = getMobileThemeVariables(normalizeMobileThemeId(theme.id), "dark");
168 for (const themeId of BUILT_IN_THEME_IDS) {
169 const variables = getMobileThemeVariables(themeId, "dark");
186170 expect(variables["--color-primary-shadow"]).toBe("#000000");
187171 expect(variables["--color-backdrop"]).toBe("rgba(0, 0, 0, 0.48)");
188172 expect(variables["--color-drawer-shadow"]).toBe("rgba(0, 0, 0, 0.32)");
......@@ -190,7 +174,7 @@ describe("mobile themes", () => {
190174 });
191175
192176 it("keeps placeholders and selected-row labels readable on their mobile surfaces", () => {
193 for (const themeId of MOBILE_THEME_IDS) {
177 for (const themeId of BUILT_IN_THEME_IDS) {
194178 for (const appearance of ["light", "dark"] as const) {
195179 const variables = getMobileThemeVariables(themeId, appearance);
196180 expect(
apps/mobile/src/lib/mobileTheme.ts+10-10
......@@ -3,6 +3,7 @@ import {
33 getThemeColorsForAppearance,
44 MOBILE_DEFAULT_THEME_ID,
55 MOBILE_THEME_IDS as SHARED_MOBILE_THEME_IDS,
6 type BuiltInThemeId,
67 type MobileThemeId as SharedMobileThemeId,
78 type ThemeAppearance,
89 type ThemeColors,
......@@ -11,7 +12,6 @@ import {
1112 STANDARD_THEME_PREVIEW_COLORS,
1213 type ThemePreviewColors,
1314} from "@t3tools/shared/themePreview";
14import { DEFAULT_MOBILE_THEME_VARIABLES } from "./mobileDefaultTheme";
1515
1616export const DEFAULT_MOBILE_THEME_ID = MOBILE_DEFAULT_THEME_ID;
1717export const MOBILE_THEME_IDS = SHARED_MOBILE_THEME_IDS;
......@@ -28,7 +28,7 @@ export const MOBILE_THEME_OPTIONS: ReadonlyArray<{
2828 ...BUILT_IN_THEMES.map((theme) => ({ id: theme.id as MobileThemeId, label: theme.label })),
2929];
3030
31type MobileThemeVariable = `--color-${string}`;
31export type MobileThemeVariable = `--color-${string}`;
3232export type MobileThemeVariables = Readonly<Record<MobileThemeVariable, string>>;
3333
3434export function normalizeMobileThemeId(value: unknown): MobileThemeId {
......@@ -282,18 +282,18 @@ export function createMobileThemeVariables(
282282 };
283283}
284284
285export const MOBILE_THEME_VARIABLE_NAMES = Object.keys(
286 createMobileThemeVariables(BUILT_IN_THEMES[0].colors, "light"),
287) as ReadonlyArray<MobileThemeVariable>;
288
285289export function getMobileThemeVariables(
286 themeId: MobileThemeId,
290 themeId: BuiltInThemeId,
287291 appearance: MobileThemeAppearance,
288292 overrides: Partial<MobileThemeVariables> | null = null,
289293): MobileThemeVariables {
290 const baseVariables = (() => {
291 if (themeId === DEFAULT_MOBILE_THEME_ID) return DEFAULT_MOBILE_THEME_VARIABLES[appearance];
292 const theme =
293 BUILT_IN_THEMES.find((candidate) => candidate.id === themeId) ?? BUILT_IN_THEMES[0];
294 const colors = getThemeColorsForAppearance(theme, appearance) ?? theme.colors;
295 return createMobileThemeVariables(colors, appearance);
296 })();
294 const theme = BUILT_IN_THEMES.find((candidate) => candidate.id === themeId) ?? BUILT_IN_THEMES[0];
295 const colors = getThemeColorsForAppearance(theme, appearance) ?? theme.colors;
296 const baseVariables = createMobileThemeVariables(colors, appearance);
297297
298298 // The complete base record guarantees that optional overrides cannot leave a token undefined.
299299 return overrides ? ({ ...baseVariables, ...overrides } as MobileThemeVariables) : baseVariables;
apps/mobile/src/lib/mobileThemeRuntime.test.ts created+82
......@@ -0,0 +1,82 @@
1import { describe, expect, it } from "vite-plus/test";
2
3import {
4 createMobileThemeRuntimeOperations,
5 getMobileUniwindThemeName,
6 type MobileThemeRuntimeState,
7} from "./mobileThemeRuntime";
8
9const initialState: MobileThemeRuntimeState = {
10 baseFontSize: 16,
11 themeAppearance: "light",
12 themeMode: "system",
13};
14
15describe("mobileThemeRuntime", () => {
16 it("keeps the default palette on Uniwind's built-in appearance themes", () => {
17 expect(getMobileUniwindThemeName("t3-code", "light")).toBe("light");
18 expect(getMobileUniwindThemeName("t3-code", "dark")).toBe("dark");
19 });
20
21 it("maps custom palettes and appearances to registered themes", () => {
22 expect(getMobileUniwindThemeName("t3-chat", "dark")).toBe("t3-chat-dark");
23 });
24
25 it("hydrates text variables and clears the native appearance override", () => {
26 const operations = createMobileThemeRuntimeOperations(null, initialState);
27 const variableOperations = operations.filter(
28 (operation) => operation.kind === "update-text-variables",
29 );
30
31 expect(variableOperations).toHaveLength(12);
32 expect(variableOperations.at(-1)?.themeName).toBe("iris-dark");
33 expect(operations.at(-1)).toEqual({
34 kind: "set-appearance-mode",
35 appearance: "light",
36 themeMode: "system",
37 });
38 });
39
40 it("lets system appearance changes flow through the root ScopedTheme only", () => {
41 const operations = createMobileThemeRuntimeOperations(initialState, {
42 ...initialState,
43 themeAppearance: "dark",
44 });
45
46 expect(operations).toEqual([]);
47 });
48
49 it("updates native appearance once when the selected mode changes", () => {
50 const operations = createMobileThemeRuntimeOperations(initialState, {
51 ...initialState,
52 themeAppearance: "dark",
53 themeMode: "dark",
54 });
55
56 expect(operations).toEqual([
57 {
58 kind: "set-appearance-mode",
59 appearance: "dark",
60 themeMode: "dark",
61 },
62 ]);
63 });
64
65 it("updates text variables for every theme without switching palettes", () => {
66 const operations = createMobileThemeRuntimeOperations(initialState, {
67 ...initialState,
68 baseFontSize: 18,
69 });
70
71 expect(operations).toHaveLength(12);
72 expect(operations.every((operation) => operation.kind === "update-text-variables")).toBe(true);
73 expect(operations.at(-1)).toMatchObject({
74 kind: "update-text-variables",
75 themeName: "iris-dark",
76 });
77 });
78
79 it("does no native work when persistence echoes an already-applied state", () => {
80 expect(createMobileThemeRuntimeOperations(initialState, initialState)).toEqual([]);
81 });
82});
apps/mobile/src/lib/mobileThemeRuntime.ts created+75
......@@ -0,0 +1,75 @@
1import { resolveTextScaleVariables } from "./appearancePreferences";
2import { BUILT_IN_THEME_IDS, type BuiltInThemeId } from "@t3tools/shared/themePalettes";
3import {
4 DEFAULT_MOBILE_THEME_ID,
5 type MobileThemeAppearance,
6 type MobileThemeId,
7 type MobileThemeMode,
8} from "./mobileTheme";
9
10export type MobileUniwindThemeName =
11 | MobileThemeAppearance
12 | `${BuiltInThemeId}-${MobileThemeAppearance}`;
13
14export interface MobileThemeRuntimeState {
15 readonly baseFontSize: number;
16 readonly themeAppearance: MobileThemeAppearance;
17 readonly themeMode: MobileThemeMode;
18}
19
20export type MobileThemeRuntimeOperation =
21 | {
22 readonly kind: "update-text-variables";
23 readonly themeName: "light" | "dark" | MobileUniwindThemeName;
24 readonly variables: Readonly<Record<string, number>>;
25 }
26 | {
27 readonly kind: "set-appearance-mode";
28 readonly appearance: MobileThemeAppearance;
29 readonly themeMode: MobileThemeMode;
30 };
31
32const UNIWIND_THEME_NAMES: ReadonlyArray<"light" | "dark" | MobileUniwindThemeName> = [
33 "light",
34 "dark",
35 ...BUILT_IN_THEME_IDS.flatMap((themeId) => [
36 `${themeId}-light` as const,
37 `${themeId}-dark` as const,
38 ]),
39];
40
41export function getMobileUniwindThemeName(
42 themeId: MobileThemeId,
43 appearance: MobileThemeAppearance,
44): MobileUniwindThemeName {
45 return themeId === DEFAULT_MOBILE_THEME_ID ? appearance : `${themeId}-${appearance}`;
46}
47
48/**
49 * Plans imperative runtime work separately from theme selection. Palette
50 * changes are handled by one root ScopedTheme render; only typography and the
51 * native appearance override need imperative Uniwind/React Native updates.
52 */
53export function createMobileThemeRuntimeOperations(
54 previous: MobileThemeRuntimeState | null,
55 next: MobileThemeRuntimeState,
56): ReadonlyArray<MobileThemeRuntimeOperation> {
57 const operations: MobileThemeRuntimeOperation[] = [];
58
59 if (previous === null || previous.baseFontSize !== next.baseFontSize) {
60 const variables = resolveTextScaleVariables(next.baseFontSize);
61 for (const themeName of UNIWIND_THEME_NAMES) {
62 operations.push({ kind: "update-text-variables", themeName, variables });
63 }
64 }
65
66 if (previous === null || previous.themeMode !== next.themeMode) {
67 operations.push({
68 kind: "set-appearance-mode",
69 appearance: next.themeAppearance,
70 themeMode: next.themeMode,
71 });
72 }
73
74 return operations;
75}
apps/mobile/src/lib/mobileThemeVariables.test.ts created+25
......@@ -0,0 +1,25 @@
1import { describe, expect, it } from "vite-plus/test";
2
3import { readDefaultMobileThemeVariables } from "./mobileTheme.test-support";
4import { getMobileThemeVariables } from "./mobileTheme";
5import { getMobileThemeRuntimeVariables } from "./mobileThemeVariables";
6
7describe("mobile theme runtime variables", () => {
8 it("derives the standard runtime palette from global.css", () => {
9 expect(getMobileThemeRuntimeVariables("t3-code", "light")).toEqual(
10 readDefaultMobileThemeVariables("light"),
11 );
12 expect(getMobileThemeRuntimeVariables("t3-code", "dark")).toEqual(
13 readDefaultMobileThemeVariables("dark"),
14 );
15 });
16
17 it("uses the same shared palette source as generated custom themes", () => {
18 expect(getMobileThemeRuntimeVariables("ocean", "light")).toEqual(
19 getMobileThemeVariables("ocean", "light"),
20 );
21 expect(getMobileThemeRuntimeVariables("iris", "dark")).toEqual(
22 getMobileThemeVariables("iris", "dark"),
23 );
24 });
25});
apps/mobile/src/lib/mobileThemeVariables.ts created+27
......@@ -0,0 +1,27 @@
1import defaultThemeVariables from "../../generated-uniwind-default-theme-variables.json";
2
3import {
4 DEFAULT_MOBILE_THEME_ID,
5 getMobileThemeVariables,
6 type MobileThemeAppearance,
7 type MobileThemeId,
8 type MobileThemeVariables,
9} from "./mobileTheme";
10
11const defaults = defaultThemeVariables as Readonly<
12 Record<MobileThemeAppearance, MobileThemeVariables>
13>;
14
15/**
16 * Complete palette for native and third-party APIs that cannot consume a
17 * Uniwind className. The standard palette is generated from global.css; custom
18 * palettes share the same source that generates their registered CSS themes.
19 */
20export function getMobileThemeRuntimeVariables(
21 themeId: MobileThemeId,
22 appearance: MobileThemeAppearance,
23): MobileThemeVariables {
24 return themeId === DEFAULT_MOBILE_THEME_ID
25 ? defaults[appearance]
26 : getMobileThemeVariables(themeId, appearance);
27}
apps/mobile/src/lib/storage.test.ts+6
......@@ -196,6 +196,12 @@ describe("mobile connection storage", () => {
196196 });
197197 });
198198
199 it("drops the removed theme transition preference", async () => {
200 mocks.setPreferencesJson(JSON.stringify({ themeTransition: "circle-bottom-left" }), 10);
201
202 await expect(loadPreferences()).resolves.toEqual({});
203 });
204
199205 it("falls back to secure storage when SQLite cannot save preferences", async () => {
200206 mocks.setDatabaseFailures(true, true);
201207 await expect(savePreferencesPatch({ baseFontSize: 19 })).resolves.toEqual({ baseFontSize: 19 });
apps/mobile/src/lib/useFontFamily.ts+6-8
......@@ -1,15 +1,13 @@
1import { useCSSVariable } from "uniwind";
2
3const FONT_FAMILY_VARIABLES = {
4 regular: "--font-sans",
5 medium: "--font-medium",
6 bold: "--font-bold",
1const FONT_FAMILIES = {
2 regular: "DMSans-Regular",
3 medium: "DMSans-Medium",
4 bold: "DMSans-Bold",
75} as const;
86
97/**
108 * Resolves a font family for APIs that require a style object or native prop.
119 * Prefer Uniwind font classes when the target component accepts `className`.
1210 */
13export function useFontFamily(weight: keyof typeof FONT_FAMILY_VARIABLES): string {
14 return useCSSVariable(FONT_FAMILY_VARIABLES[weight]) as string;
11export function useFontFamily(weight: keyof typeof FONT_FAMILIES): string {
12 return FONT_FAMILIES[weight];
1513}
apps/mobile/src/lib/useMobileNavigationTheme.ts+21-12
......@@ -1,22 +1,31 @@
11import { DarkTheme, DefaultTheme, type Theme } from "@react-navigation/native";
22import { useMemo } from "react";
33
4import type { MobileThemeAppearance } from "./mobileTheme";
5import { useThemeColor } from "./useThemeColor";
6
7export function useMobileNavigationTheme(appearance: MobileThemeAppearance): Theme {
8 const primary = String(useThemeColor("--color-primary"));
9 const background = String(useThemeColor("--color-screen"));
10 const card = String(useThemeColor("--color-sheet-solid"));
11 const text = String(useThemeColor("--color-foreground"));
12 const border = String(useThemeColor("--color-header-border"));
13 const notification = String(useThemeColor("--color-danger-foreground"));
4import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
5import { useUniwindTheme } from "./useUniwindTheme";
146
7/**
8 * React Navigation requires a JS theme object. Derive it from the same palette
9 * source as Uniwind instead of subscribing the app root to CSS variables. The
10 * preferences provider applies the registered Uniwind theme first, then
11 * publishes this matching navigation palette through React.
12 */
13export function useMobileNavigationTheme(): Theme {
14 const { themeAppearance: appearance } = useAppearancePreferences();
15 const variables = useUniwindTheme();
1516 return useMemo(() => {
1617 const base = appearance === "dark" ? DarkTheme : DefaultTheme;
1718 return {
1819 ...base,
19 colors: { ...base.colors, primary, background, card, text, border, notification },
20 colors: {
21 ...base.colors,
22 primary: variables["--color-primary"],
23 background: variables["--color-screen"],
24 card: variables["--color-sheet-solid"],
25 text: variables["--color-foreground"],
26 border: variables["--color-header-border"],
27 notification: variables["--color-danger-foreground"],
28 },
2029 };
21 }, [appearance, background, border, card, notification, primary, text]);
30 }, [appearance, variables]);
2231}
apps/mobile/src/lib/useThemeColor.ts deleted-12
......@@ -1,12 +0,0 @@
1import type { ColorValue } from "react-native";
2import { useCSSVariable } from "uniwind";
3
4/**
5 * Typed wrapper around `useCSSVariable` that returns a `ColorValue` for use
6 * in React Native style props (backgroundColor, tintColor, etc.).
7 *
8 * Usage: `const color = useThemeColor("--color-icon");`
9 */
10export function useThemeColor(variable: `--color-${string}`): ColorValue {
11 return useCSSVariable(variable) as string as ColorValue;
12}
apps/mobile/src/lib/useUniwindTheme.ts created+21
......@@ -0,0 +1,21 @@
1import { useMemo } from "react";
2
3import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
4import type { MobileThemeVariables } from "./mobileTheme";
5import { getMobileThemeRuntimeVariables } from "./mobileThemeVariables";
6
7/**
8 * Complete JS palette for native and third-party APIs that cannot consume a
9 * Uniwind className (React Navigation, native editors, Markdown, SVG gradients,
10 * Reanimated worklets). Ordinary React Native rendering must use className.
11 *
12 * This bridge follows the same single React theme commit as the root
13 * ScopedTheme instead of subscribing every consumer to CSS-variable updates.
14 */
15export function useUniwindTheme(): MobileThemeVariables {
16 const { themeAppearance, themeId } = useAppearancePreferences();
17 return useMemo(
18 () => getMobileThemeRuntimeVariables(themeId, themeAppearance),
19 [themeAppearance, themeId],
20 );
21}
apps/mobile/src/native/T3ComposerEditor.ios.tsx+11-19
......@@ -14,7 +14,7 @@ import { Image, StyleSheet } from "react-native";
1414
1515import { markdownFileIconSource } from "@t3tools/mobile-markdown-text/file-icons";
1616import { resolveMarkdownFileIcon } from "@t3tools/mobile-markdown-text/links";
17import { useThemeColor } from "../lib/useThemeColor";
17import { useUniwindTheme } from "../lib/useUniwindTheme";
1818import { useFontFamily } from "../lib/useFontFamily";
1919import { useScaledTextRole } from "../features/settings/appearance/useScaledTextRole";
2020import {
......@@ -110,15 +110,7 @@ export function ComposerEditor({
110110 const nativeEventSnapshotsRef = useRef<ComposerNativeEventSnapshot[]>([]);
111111 const confirmedTokensRef = useRef(collectComposerInlineTokens(props.value));
112112 const bodyText = useScaledTextRole("body");
113 const textColor = useThemeColor("--color-foreground");
114 const placeholderColor = useThemeColor("--color-placeholder");
115 const chipBackground = useThemeColor("--color-subtle");
116 const chipBorder = useThemeColor("--color-border");
117 const chipText = useThemeColor("--color-foreground");
118 const skillBackground = useThemeColor("--color-inline-skill-background");
119 const skillBorder = useThemeColor("--color-inline-skill-border");
120 const skillText = useThemeColor("--color-inline-skill-foreground");
121 const fileTint = useThemeColor("--color-icon-muted");
113 const theme = useUniwindTheme();
122114 const fontFamily = useFontFamily("regular");
123115
124116 useImperativeHandle(
......@@ -219,15 +211,15 @@ export function ComposerEditor({
219211 [],
220212 );
221213 const themeJson = JSON.stringify({
222 text: String(textColor),
223 placeholder: String(placeholderColor),
224 chipBackground: String(chipBackground),
225 chipBorder: String(chipBorder),
226 chipText: String(chipText),
227 skillBackground: String(skillBackground),
228 skillBorder: String(skillBorder),
229 skillText: String(skillText),
230 fileTint: String(fileTint),
214 text: theme["--color-foreground"],
215 placeholder: theme["--color-placeholder"],
216 chipBackground: theme["--color-subtle"],
217 chipBorder: theme["--color-border"],
218 chipText: theme["--color-foreground"],
219 skillBackground: theme["--color-inline-skill-background"],
220 skillBorder: theme["--color-inline-skill-border"],
221 skillText: theme["--color-inline-skill-foreground"],
222 fileTint: theme["--color-icon-muted"],
231223 });
232224 const resolvedTextStyle = StyleSheet.flatten(textStyle) ?? {};
233225 return (
apps/mobile/src/native/T3ComposerEditor.native.tsx+11-19
......@@ -18,7 +18,7 @@ import { resolveMarkdownFileIcon } from "@t3tools/mobile-markdown-text/links";
1818import { MOBILE_TYPOGRAPHY } from "../lib/typography";
1919import { useNativePaste } from "../lib/useNativePaste";
2020import { useFontFamily } from "../lib/useFontFamily";
21import { useThemeColor } from "../lib/useThemeColor";
21import { useUniwindTheme } from "../lib/useUniwindTheme";
2222import {
2323 acknowledgeComposerNativeEvent,
2424 assumeComposerControlledState,
......@@ -111,15 +111,7 @@ export function ComposerEditor({
111111 const nativeEventSnapshotsRef = useRef<ComposerNativeEventSnapshot[]>([]);
112112 const [initialConfirmedTokens] = useState(() => collectComposerInlineTokens(props.value));
113113 const confirmedTokensRef = useRef(initialConfirmedTokens);
114 const textColor = useThemeColor("--color-foreground");
115 const placeholderColor = useThemeColor("--color-placeholder");
116 const chipBackground = useThemeColor("--color-subtle");
117 const chipBorder = useThemeColor("--color-border");
118 const chipText = useThemeColor("--color-foreground");
119 const skillBackground = useThemeColor("--color-inline-skill-background");
120 const skillBorder = useThemeColor("--color-inline-skill-border");
121 const skillText = useThemeColor("--color-inline-skill-foreground");
122 const fileTint = useThemeColor("--color-icon-muted");
114 const theme = useUniwindTheme();
123115 const handlePaste = useNativePaste((uris) => onPasteImages?.(uris));
124116
125117 useImperativeHandle(
......@@ -220,15 +212,15 @@ export function ComposerEditor({
220212 [],
221213 );
222214 const themeJson = JSON.stringify({
223 text: String(textColor),
224 placeholder: String(placeholderColor),
225 chipBackground: String(chipBackground),
226 chipBorder: String(chipBorder),
227 chipText: String(chipText),
228 skillBackground: String(skillBackground),
229 skillBorder: String(skillBorder),
230 skillText: String(skillText),
231 fileTint: String(fileTint),
215 text: theme["--color-foreground"],
216 placeholder: theme["--color-placeholder"],
217 chipBackground: theme["--color-subtle"],
218 chipBorder: theme["--color-border"],
219 chipText: theme["--color-foreground"],
220 skillBackground: theme["--color-inline-skill-background"],
221 skillBorder: theme["--color-inline-skill-border"],
222 skillText: theme["--color-inline-skill-foreground"],
223 fileTint: theme["--color-icon-muted"],
232224 });
233225 const resolvedTextStyle = StyleSheet.flatten(textStyle) ?? {};
234226 const regularFontFamily = useFontFamily("regular");
apps/mobile/src/native/T3ComposerEditor.tsx+2-5
......@@ -2,7 +2,6 @@ import { TextInputWrapper } from "expo-paste-input";
22import { useImperativeHandle, useRef } from "react";
33import { TextInput, type TextInput as RNTextInput } from "react-native";
44
5import { useThemeColor } from "../lib/useThemeColor";
65import { useFontFamily } from "../lib/useFontFamily";
76import { useScaledTextRole } from "../features/settings/appearance/useScaledTextRole";
87import { useNativePaste } from "../lib/useNativePaste";
......@@ -21,8 +20,6 @@ export function ComposerEditor({
2120}: ComposerEditorProps) {
2221 const inputRef = useRef<RNTextInput>(null);
2322 const bodyText = useScaledTextRole("body");
24 const foregroundColor = useThemeColor("--color-foreground");
25 const placeholderColor = useThemeColor("--color-placeholder");
2623 const fontFamily = useFontFamily("regular");
2724 const handlePaste = useNativePaste((uris) => onPasteImages?.(uris));
2825
......@@ -45,12 +42,12 @@ export function ComposerEditor({
4542 selection={selection}
4643 onSelectionChange={(event) => props.onSelectionChange?.(event.nativeEvent.selection)}
4744 multiline={props.multiline ?? true}
48 placeholderTextColor={placeholderColor}
45 placeholderTextColorClassName={"accent-placeholder"}
46 className="text-foreground"
4947 style={[
5048 {
5149 flex: 1,
5250 minHeight: 0,
53 color: foregroundColor,
5451 fontFamily,
5552 ...bodyText,
5653 paddingVertical: contentInsetVertical,
apps/mobile/src/state/thread-pr-presentation.ts+3-3
......@@ -17,9 +17,9 @@ export interface ThreadPrPresentation {
1717}
1818
1919const PR_STATE_TEXT_CLASS: Record<ThreadPr["state"], string> = {
20 open: "text-emerald-600 dark:text-emerald-400",
21 merged: "text-violet-600 dark:text-violet-400",
22 closed: "text-zinc-500 dark:text-zinc-400",
20 open: "text-adaptive-emerald-600-400",
21 merged: "text-adaptive-violet-600-400",
22 closed: "text-adaptive-zinc-500-400",
2323};
2424
2525export function presentThreadPr(
apps/mobile/src/state/use-thread-pr.test.ts+1-1
......@@ -17,7 +17,7 @@ describe("presentThreadPr", () => {
1717 expect(presentThreadPr(pullRequest, undefined)).toMatchObject({
1818 label: "3774",
1919 accessibilityLabel: "#3774 pull request merged",
20 textClassName: "text-violet-600 dark:text-violet-400",
20 textClassName: "text-adaptive-violet-600-400",
2121 });
2222 });
2323
apps/mobile/uniwind-types.d.ts+1-1
......@@ -3,7 +3,7 @@
33
44declare module 'uniwind' {
55 export interface UniwindConfig {
6 themes: readonly ['light', 'dark']
6 themes: readonly ['light', 'dark', 't3-chat-light', 't3-chat-dark', 'grove-light', 'grove-dark', 'ocean-light', 'ocean-dark', 'ember-light', 'ember-dark', 'iris-light', 'iris-dark']
77 }
88}
99
docs/user/mobile-appearance.md+3
......@@ -11,5 +11,8 @@ To change themes:
11113. Choose a theme.
12124. Select **System**, **Light**, or **Dark**.
1313
14Tap a theme card to use it for both light and dark appearance. To mix themes, tap the light or dark
15preview circle inside a card to change only that appearance.
16
1417**System** follows the device appearance automatically. Theme, text, code, and terminal appearance
1518preferences are stored on the device.
oxlint-plugin-t3code/index.ts+2
......@@ -4,6 +4,7 @@ import namespaceNodeImports from "./rules/namespace-node-imports.ts";
44import noGlobalProcessRuntime from "./rules/no-global-process-runtime.ts";
55import noInlineSchemaCompile from "./rules/no-inline-schema-compile.ts";
66import noManualEffectRuntimeInTests from "./rules/no-manual-effect-runtime-in-tests.ts";
7import noMobileUniwindThemeEscapeHatches from "./rules/no-mobile-uniwind-theme-escape-hatches.ts";
78import noNativeTitleTooltip from "./rules/no-native-title-tooltip.ts";
89
910export default definePlugin({
......@@ -15,6 +16,7 @@ export default definePlugin({
1516 "no-global-process-runtime": noGlobalProcessRuntime,
1617 "no-inline-schema-compile": noInlineSchemaCompile,
1718 "no-manual-effect-runtime-in-tests": noManualEffectRuntimeInTests,
19 "no-mobile-uniwind-theme-escape-hatches": noMobileUniwindThemeEscapeHatches,
1820 "no-native-title-tooltip": noNativeTitleTooltip,
1921 },
2022});
oxlint-plugin-t3code/rules/no-mobile-uniwind-theme-escape-hatches.test.ts created+264
......@@ -0,0 +1,264 @@
1import { assert, describe, expect } from "@effect/vitest";
2
3import { createOxlintRuleHarness } from "../test/utils.ts";
4
5const guardedMobileFile = createOxlintRuleHarness("t3code/no-mobile-uniwind-theme-escape-hatches", {
6 filename: "apps/mobile/src/features/settings/NewThemeSurface.tsx",
7});
8const reviewedInteropFile = createOxlintRuleHarness(
9 "t3code/no-mobile-uniwind-theme-escape-hatches",
10 { filename: "apps/mobile/src/features/home/HomeHeader.tsx" },
11);
12const gitOverlayInteropFile = createOxlintRuleHarness(
13 "t3code/no-mobile-uniwind-theme-escape-hatches",
14 { filename: "apps/mobile/src/features/threads/GitActionProgressOverlay.tsx" },
15);
16const webFile = createOxlintRuleHarness("t3code/no-mobile-uniwind-theme-escape-hatches", {
17 filename: "apps/web/src/ThemeSurface.tsx",
18});
19
20describe("t3code/no-mobile-uniwind-theme-escape-hatches", () => {
21 guardedMobileFile.valid(
22 "allows semantic mobile theme classes",
23 `const surface = <View className="bg-surface text-foreground" />;`,
24 );
25
26 guardedMobileFile.valid(
27 "allows unrelated functions with legacy hook names",
28 `
29 const useCSSVariable = () => "local value";
30 const useThemeColor = () => "local color";
31
32 export const values = [useCSSVariable(), useThemeColor()];
33 `,
34 );
35
36 guardedMobileFile.valid(
37 "allows unrelated imports with legacy hook names",
38 `
39 import { useThemeColor } from "./unrelated-library";
40
41 export const foreground = useThemeColor();
42 `,
43 );
44
45 guardedMobileFile.valid(
46 "allows erased type-only theme imports",
47 `
48 import type { useCSSVariable } from "uniwind";
49 import { type useCSSVariable as CSSVariableHook } from "uniwind";
50 import type { useThemeColor } from "../../../hooks/useThemeColor";
51 import type { useUniwindTheme } from "../../../lib/useUniwindTheme";
52
53 export type ThemeHooks = [
54 typeof useCSSVariable,
55 typeof CSSVariableHook,
56 typeof useThemeColor,
57 typeof useUniwindTheme,
58 ];
59 `,
60 );
61
62 guardedMobileFile.valid(
63 "allows appearance words in ordinary copy",
64 `
65 const message = "Set dark mode manually";
66 const detail = "For dark: high contrast mode is available";
67 const label = <Text accessibilityLabel="Light theme">Theme</Text>;
68 `,
69 );
70
71 guardedMobileFile.valid(
72 "does not conflate shadowed Uniwind namespaces",
73 `
74 import * as Uniwind from "uniwind";
75
76 export function readDirect(Uniwind: { useCSSVariable: () => string }) {
77 return Uniwind.useCSSVariable();
78 }
79
80 export function readDestructured(Uniwind: { useCSSVariable: () => string }) {
81 const { useCSSVariable } = Uniwind;
82 return useCSSVariable();
83 }
84 `,
85 );
86
87 reviewedInteropFile.valid(
88 "allows reviewed native interop boundaries",
89 `
90 import { useUniwindTheme } from "../../lib/useUniwindTheme";
91
92 export const foreground = useUniwindTheme().colors.foreground;
93 `,
94 );
95
96 gitOverlayInteropFile.valid(
97 "allows the native liquid-glass theme boundary",
98 `
99 import { useUniwindTheme } from "../../lib/useUniwindTheme";
100
101 export const tint = useUniwindTheme()["--color-glass-surface"];
102 `,
103 );
104
105 webFile.valid(
106 "does not impose the mobile custom-theme policy on web code",
107 `const surface = <div className="bg-white dark:bg-black" />;`,
108 );
109
110 guardedMobileFile.invalid(
111 "reports new React theme subscriptions",
112 `
113 import { useCSSVariable } from "uniwind";
114
115 export const foreground = useCSSVariable("--color-foreground");
116 `,
117 (output) => {
118 assert.match(output, /semantic className/);
119 },
120 );
121
122 guardedMobileFile.invalid(
123 "reports the retired theme color hook",
124 `
125 import { useThemeColor } from "../../../hooks/useThemeColor";
126
127 export const foreground = useThemeColor({}, "foreground");
128 `,
129 (output) => {
130 assert.match(output, /replaced by semantic Uniwind classes/);
131 },
132 );
133
134 guardedMobileFile.invalid(
135 "reports guarded hooks imported with TypeScript extensions",
136 `
137 import { useThemeColor } from "../../../hooks/useThemeColor.ts";
138 import { useUniwindTheme } from "../../../lib/useUniwindTheme.ts";
139
140 export const foreground = [
141 useThemeColor({}, "foreground"),
142 useUniwindTheme().colors.foreground,
143 ];
144 `,
145 (output) => {
146 assert.match(output, /replaced by semantic Uniwind classes/);
147 assert.match(output, /native\/third-party interop boundary/);
148 },
149 );
150
151 guardedMobileFile.invalid(
152 "reports unreviewed native interop subscriptions",
153 `
154 import { useUniwindTheme } from "../../../lib/useUniwindTheme";
155
156 export const foreground = useUniwindTheme().colors.foreground;
157 `,
158 (output) => {
159 assert.match(output, /native\/third-party interop boundary/);
160 },
161 );
162
163 guardedMobileFile.invalid(
164 "reports appearance variants in string literals",
165 `const surface = <View className="bg-white dark:bg-black" />;`,
166 (output) => {
167 assert.match(output, /registered custom themes/);
168 },
169 );
170
171 guardedMobileFile.invalid(
172 "reports appearance variants in template literals",
173 "const className = `bg-black light:bg-white`;",
174 );
175
176 guardedMobileFile.invalid(
177 "reports escaped appearance variants in template literals",
178 "const className = `dark\\u003abg-black`;",
179 );
180
181 guardedMobileFile.invalid(
182 "reports appearance variants through nested class-map indirection",
183 `
184 const styles = {
185 variants: { root: "bg-white dark:bg-black" },
186 };
187
188 const root = styles.variants.root;
189 export const surface = <View className={root} />;
190 `,
191 (output) => {
192 expect(output.match(/registered custom themes/g)).toHaveLength(1);
193 },
194 );
195
196 guardedMobileFile.invalid(
197 "reports appearance variants passed to class builders",
198 `const surface = cn("bg-white", enabled && "dark:bg-black");`,
199 );
200
201 guardedMobileFile.invalid(
202 "reports negative and important appearance variants",
203 `const className = "dark:-mt-2 light:!bg-white";`,
204 );
205
206 guardedMobileFile.invalid(
207 "reports namespace CSS variable subscriptions",
208 `
209 import * as Uniwind from "uniwind";
210
211 export const foreground = Uniwind.useCSSVariable("--color-foreground");
212 `,
213 );
214
215 guardedMobileFile.invalid(
216 "reports destructured namespace CSS variable subscriptions",
217 `
218 import * as Uniwind from "uniwind";
219
220 const { useCSSVariable: resolveVariable } = Uniwind;
221 export const foreground = resolveVariable("--color-foreground");
222 `,
223 );
224
225 guardedMobileFile.invalid(
226 "reports aliased namespace CSS variable subscriptions",
227 `
228 import * as Uniwind from "uniwind";
229
230 const Theme = Uniwind;
231 const NestedTheme = Theme;
232 export const foreground = NestedTheme.useCSSVariable("--color-foreground");
233 `,
234 );
235
236 guardedMobileFile.invalid(
237 "reports object-rest namespace CSS variable subscriptions",
238 `
239 import * as Uniwind from "uniwind";
240
241 const { ...Theme } = Uniwind;
242 export const foreground = Theme.useCSSVariable("--color-foreground");
243 `,
244 );
245
246 guardedMobileFile.invalid(
247 "reports namespace access to the retired theme hook",
248 `
249 import * as ThemeColor from "../../../hooks/useThemeColor";
250
251 export const foreground = ThemeColor.useThemeColor({}, "foreground");
252 `,
253 );
254
255 guardedMobileFile.invalid(
256 "reports destructured namespace access to the retired theme hook",
257 `
258 import * as ThemeColor from "../../../hooks/useThemeColor.ts";
259
260 const { useThemeColor: resolveThemeColor } = ThemeColor;
261 export const foreground = resolveThemeColor({}, "foreground");
262 `,
263 );
264});
oxlint-plugin-t3code/rules/no-mobile-uniwind-theme-escape-hatches.ts created+225
......@@ -0,0 +1,225 @@
1import { defineRule, type Variable } from "@oxlint/plugins";
2import * as Option from "effect/Option";
3
4import { getPropertyName, unwrapExpression } from "../utils.ts";
5
6const MOBILE_SOURCE_MARKER = "/apps/mobile/src/";
7const APPEARANCE_VARIANT_PATTERN = /\b(?:dark|light):(?=\S)/u;
8const APPEARANCE_VARIANT_MESSAGE =
9 "dark:/light: utilities do not follow registered custom themes; use an adaptive semantic token.";
10const THEME_INTEROP_ALLOWLIST = new Set([
11 "features/archive/ArchivedThreadsScreen.tsx",
12 "features/connection/ConnectionsNewRouteScreen.tsx",
13 "features/files/FileMarkdownPreview.tsx",
14 "features/files/SourceFileSurface.tsx",
15 "features/files/ThreadFilesRouteScreen.tsx",
16 "features/files/thread-file-navigator-pane.tsx",
17 "features/home/HomeHeader.tsx",
18 "features/review/ReviewSheet.tsx",
19 "features/review/useNativeReviewDiffBridge.ts",
20 "features/settings/SettingsEnvironmentsRouteScreen.tsx",
21 "features/settings/appearance/components/AppearancePreviews.tsx",
22 "features/settings/appearance/components/FontSizeSliderRow.tsx",
23 "features/threads/GitActionProgressOverlay.tsx",
24 "features/threads/NewTaskContextPickerScreens.tsx",
25 "features/threads/NewTaskDraftScreen.tsx",
26 "features/threads/ThreadComposer.tsx",
27 "features/threads/ThreadFeed.tsx",
28 "features/threads/ThreadSettingsSheet.tsx",
29 "features/threads/git/GitOverviewSheet.tsx",
30 "features/threads/thread-list-items.tsx",
31 "features/threads/thread-list-v2-items.tsx",
32 "lib/useMobileNavigationTheme.ts",
33 "native/T3ComposerEditor.ios.tsx",
34 "native/T3ComposerEditor.native.tsx",
35]);
36
37const mobileSourcePath = (filename: string): string | undefined => {
38 const normalized = `/${filename.replaceAll("\\", "/")}`;
39 const markerIndex = normalized.lastIndexOf(MOBILE_SOURCE_MARKER);
40 return markerIndex === -1
41 ? undefined
42 : normalized.slice(markerIndex + MOBILE_SOURCE_MARKER.length);
43};
44
45const literalStringValue = (node: unknown): Option.Option<string> => {
46 if (typeof node !== "object" || node === null) return Option.none();
47 if (!("type" in node) || node.type !== "Literal") return Option.none();
48 if (!("value" in node) || typeof node.value !== "string") return Option.none();
49 return Option.some(node.value);
50};
51
52const reportsAppearanceVariant = (value: string) => APPEARANCE_VARIANT_PATTERN.test(value);
53
54const importsModule = (source: string, modulePath: string): boolean =>
55 source.replace(/\.[cm]?[jt]sx?$/u, "").endsWith(modulePath);
56
57export default defineRule({
58 meta: {
59 type: "problem",
60 docs: {
61 description:
62 "Keep mobile theme styling on semantic Uniwind classes and reviewed native interop boundaries.",
63 },
64 },
65 create(context) {
66 const sourcePath = mobileSourcePath(context.filename);
67 if (sourcePath === undefined) return {};
68
69 const uniwindNamespaces = new Set<Variable>();
70
71 const resolveVariable = (node: unknown): Variable | undefined => {
72 const identifier = unwrapExpression(node);
73 if (Option.isNone(identifier) || identifier.value.type !== "Identifier") return undefined;
74
75 let scope = context.sourceCode.getScope(identifier.value);
76 while (true) {
77 const variable = scope.set.get(identifier.value.name);
78 if (variable !== undefined || scope.upper === null) return variable;
79 scope = scope.upper;
80 }
81 };
82
83 return {
84 ImportDeclaration(node) {
85 const source = literalStringValue(node.source);
86 if (Option.isNone(source)) return;
87 const declaredVariables = context.sourceCode.getDeclaredVariables(node);
88
89 for (const specifier of node.specifiers) {
90 const local = unwrapExpression(specifier.local);
91 const importedName =
92 specifier.type === "ImportSpecifier"
93 ? getPropertyName(specifier.imported)
94 : Option.none();
95 const isTypeOnly =
96 node.importKind === "type" ||
97 (specifier.type === "ImportSpecifier" && specifier.importKind === "type");
98
99 if (
100 !isTypeOnly &&
101 specifier.type === "ImportNamespaceSpecifier" &&
102 Option.isSome(local) &&
103 local.value.type === "Identifier"
104 ) {
105 const localName = local.value.name;
106 const variable = declaredVariables.find((candidate) => candidate.name === localName);
107 if (source.value === "uniwind" && variable !== undefined) {
108 uniwindNamespaces.add(variable);
109 }
110 }
111
112 if (
113 !isTypeOnly &&
114 source.value === "uniwind" &&
115 Option.isSome(importedName) &&
116 importedName.value === "useCSSVariable"
117 ) {
118 context.report({
119 node: specifier,
120 message:
121 "Use a semantic className instead of useCSSVariable; it adds a React theme subscription.",
122 });
123 }
124
125 if (!isTypeOnly && importsModule(source.value, "/useThemeColor")) {
126 context.report({
127 node: specifier,
128 message: "useThemeColor was replaced by semantic Uniwind classes.",
129 });
130 }
131
132 if (
133 !isTypeOnly &&
134 importsModule(source.value, "/useUniwindTheme") &&
135 !THEME_INTEROP_ALLOWLIST.has(sourcePath)
136 ) {
137 context.report({
138 node: specifier,
139 message:
140 "Use className for theme styling, or review and add this native/third-party interop boundary to the lint allowlist.",
141 });
142 }
143 }
144 },
145 MemberExpression(node) {
146 const object = unwrapExpression(node.object);
147 if (Option.isNone(object) || object.value.type !== "Identifier") return;
148
149 const property = getPropertyName(node.property);
150 if (Option.isNone(property)) return;
151
152 const namespace = resolveVariable(object.value);
153 if (
154 namespace !== undefined &&
155 uniwindNamespaces.has(namespace) &&
156 property.value === "useCSSVariable"
157 ) {
158 context.report({
159 node,
160 message:
161 "Use a semantic className instead of useCSSVariable; it adds a React theme subscription.",
162 });
163 }
164 },
165 VariableDeclarator(node) {
166 const initializer = unwrapExpression(node.init);
167 const binding = unwrapExpression(node.id);
168 if (
169 Option.isNone(initializer) ||
170 initializer.value.type !== "Identifier" ||
171 Option.isNone(binding)
172 ) {
173 return;
174 }
175
176 const namespace = resolveVariable(initializer.value);
177 if (namespace === undefined || !uniwindNamespaces.has(namespace)) return;
178
179 if (binding.value.type === "Identifier") {
180 const bindingName = binding.value.name;
181 const variable = context.sourceCode
182 .getDeclaredVariables(node)
183 .find((candidate) => candidate.name === bindingName);
184 if (variable !== undefined) uniwindNamespaces.add(variable);
185 return;
186 }
187
188 if (binding.value.type !== "ObjectPattern") return;
189
190 const declaredVariables = context.sourceCode.getDeclaredVariables(node);
191 for (const propertyNode of binding.value.properties) {
192 if (propertyNode.type === "RestElement") {
193 const restBinding = unwrapExpression(propertyNode.argument);
194 if (Option.isNone(restBinding) || restBinding.value.type !== "Identifier") continue;
195
196 const restName = restBinding.value.name;
197 const variable = declaredVariables.find((candidate) => candidate.name === restName);
198 if (variable !== undefined) uniwindNamespaces.add(variable);
199 continue;
200 }
201
202 if (propertyNode.type !== "Property") continue;
203 const property = getPropertyName(propertyNode.key);
204 if (Option.isNone(property)) continue;
205
206 if (property.value === "useCSSVariable") {
207 context.report({
208 node: propertyNode,
209 message:
210 "Use a semantic className instead of useCSSVariable; it adds a React theme subscription.",
211 });
212 }
213 }
214 },
215 Literal(node) {
216 if (typeof node.value !== "string" || !reportsAppearanceVariant(node.value)) return;
217 context.report({ node, message: APPEARANCE_VARIANT_MESSAGE });
218 },
219 TemplateElement(node) {
220 if (!reportsAppearanceVariant(node.value.cooked ?? "")) return;
221 context.report({ node, message: APPEARANCE_VARIANT_MESSAGE });
222 },
223 };
224 },
225});
oxlint-plugin-t3code/test/utils.ts+1
......@@ -114,6 +114,7 @@ export const createOxlintRuleHarness = (
114114 rules: { [ruleName]: "error" },
115115 }),
116116 );
117 yield* fs.makeDirectory(path.dirname(sourcePath), { recursive: true });
117118 yield* fs.writeFileString(sourcePath, source);
118119
119120 // Run through the current Node binary: oxlint's bin is an extensionless
vite.config.ts+1
......@@ -120,6 +120,7 @@ export default defineConfig({
120120 "t3code/no-global-process-runtime": "error",
121121 "t3code/no-inline-schema-compile": "warn",
122122 "t3code/no-manual-effect-runtime-in-tests": "error",
123 "t3code/no-mobile-uniwind-theme-escape-hatches": "error",
123124 "t3code/no-native-title-tooltip": "error",
124125 "t3code/namespace-node-imports": "error",
125126 },