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 @@...@@ -1,5 +1,6 @@
1@import "tailwindcss";1@import "tailwindcss";
2@import "uniwind";2@import "uniwind";
3@import "./generated-uniwind-themes.css";
34
4/* ─── Theme tokens ──────────────────────────────────────────────────── */5/* ─── Theme tokens ──────────────────────────────────────────────────── */
5@layer theme {6@layer theme {
apps/mobile/metro.config.js+2
...@@ -2,6 +2,7 @@ const fs = require("node:fs");...@@ -2,6 +2,7 @@ const fs = require("node:fs");
2const path = require("node:path");2const path = require("node:path");
3const { getDefaultConfig } = require("expo/metro-config");3const { getDefaultConfig } = require("expo/metro-config");
4const { withUniwindConfig } = require("uniwind/metro");4const { withUniwindConfig } = require("uniwind/metro");
5const extraThemes = require("./generated-uniwind-theme-names.json");
56
6/** @type {import("expo/metro-config").MetroConfig} */7/** @type {import("expo/metro-config").MetroConfig} */
7const config = getDefaultConfig(__dirname);8const config = getDefaultConfig(__dirname);
...@@ -50,5 +51,6 @@ config.resolver = {...@@ -50,5 +51,6 @@ config.resolver = {
5051
51module.exports = withUniwindConfig(config, {52module.exports = withUniwindConfig(config, {
52 cssEntryFile: "./global.css",53 cssEntryFile: "./global.css",
54 extraThemes,
53 polyfills: { rem: 14 },55 polyfills: { rem: 14 },
54});56});
apps/mobile/package.json+1
...@@ -39,6 +39,7 @@...@@ -39,6 +39,7 @@
39 "config:prod": "APP_VARIANT=production expo config",39 "config:prod": "APP_VARIANT=production expo config",
40 "profile:android:hermes": "mkdir -p profiles/review && react-native profile-hermes profiles/review",40 "profile:android:hermes": "mkdir -p profiles/review && react-native profile-hermes profiles/review",
41 "sync:pierre-icons": "node modules/t3-markdown-text/scripts/sync-pierre-file-icons.mjs",41 "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",
42 "test": "vp test run",43 "test": "vp test run",
43 "typecheck": "tsc --noEmit"44 "typecheck": "tsc --noEmit"
44 },45 },
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";...@@ -21,7 +21,6 @@ import { RootStack } from "./Stack";
21import { appAtomRegistry } from "./state/atom-registry";21import { appAtomRegistry } from "./state/atom-registry";
22import { OverlayPortalHost } from "./components/OverlayPortal";22import { OverlayPortalHost } from "./components/OverlayPortal";
23import { appBlurTargetRef } from "./lib/appBlurTarget";23import { appBlurTargetRef } from "./lib/appBlurTarget";
24import { useThemeColor } from "./lib/useThemeColor";
25import { useMobileNavigationTheme } from "./lib/useMobileNavigationTheme";24import { useMobileNavigationTheme } from "./lib/useMobileNavigationTheme";
2625
27import "../global.css";26import "../global.css";
...@@ -72,8 +71,7 @@ export default function App() {...@@ -72,8 +71,7 @@ export default function App() {
7271
73function AppContent() {72function AppContent() {
74 const { themeAppearance } = useAppearancePreferences();73 const { themeAppearance } = useAppearancePreferences();
75 const statusBarBg = useThemeColor("--color-status-bar");74 const navigationTheme = useMobileNavigationTheme();
76 const navigationTheme = useMobileNavigationTheme(themeAppearance);
7775
78 return (76 return (
79 <>77 <>
...@@ -83,7 +81,6 @@ function AppContent() {...@@ -83,7 +81,6 @@ function AppContent() {
83 <SafeAreaProvider>81 <SafeAreaProvider>
84 <StatusBar82 <StatusBar
85 barStyle={themeAppearance === "dark" ? "light-content" : "dark-content"}83 barStyle={themeAppearance === "dark" ? "light-content" : "dark-content"}
86 backgroundColor={statusBarBg}
87 translucent84 translucent
88 />85 />
89 {/* The navigation theme drives the NATIVE header appearance: native-stack86 {/* 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";...@@ -9,7 +9,6 @@ import Animated, { FadeIn } from "react-native-reanimated";
99
10import { appBlurTargetRef } from "../lib/appBlurTarget";10import { appBlurTargetRef } from "../lib/appBlurTarget";
11import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";11import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
12import { useThemeColor } from "../lib/useThemeColor";
13import { cn } from "../lib/cn";12import { cn } from "../lib/cn";
14import { type AppSymbolName, SymbolView } from "./AppSymbol";13import { type AppSymbolName, SymbolView } from "./AppSymbol";
15import { AppText as Text } from "./AppText";14import { AppText as Text } from "./AppText";
...@@ -84,11 +83,6 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) {...@@ -84,11 +83,6 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) {
84 const isDarkMode = themeAppearance === "dark";83 const isDarkMode = themeAppearance === "dark";
85 const keyboardVisible = useKeyboardState((state) => state.isVisible);84 const keyboardVisible = useKeyboardState((state) => state.isVisible);
86 const keyboardHeight = useKeyboardState((state) => state.height);85 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
92 const close = useCallback(() => {86 const close = useCallback(() => {
93 setAnchor(null);87 setAnchor(null);
94 setPath([]);88 setPath([]);
...@@ -279,10 +273,9 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) {...@@ -279,10 +273,9 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) {
279 return (273 return (
280 <Pressable274 <Pressable
281 key={action.id ?? `${index}-${action.title}`}275 key={action.id ?? `${index}-${action.title}`}
282 android_ripple={{ color: rippleColor }}
283 disabled={disabled}276 disabled={disabled}
284 className={cn(277 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",
286 disabled && "opacity-45",279 disabled && "opacity-45",
287 )}280 )}
288 onPress={() => onPressItem(action)}281 onPress={() => onPressItem(action)}
...@@ -307,21 +300,23 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) {...@@ -307,21 +300,23 @@ export function AndroidAnchoredMenu(props: AndroidAnchoredMenuProps) {
307 <SymbolView300 <SymbolView
308 name="chevron.right"301 name="chevron.right"
309 size={13}302 size={13}
310 tintColor={iconSubtleColor}303 tintColorClassName={"accent-icon-subtle"}
311 type="monochrome"304 type="monochrome"
312 />305 />
313 ) : action.state === "on" ? (306 ) : action.state === "on" ? (
314 <SymbolView307 <SymbolView
315 name="checkmark"308 name="checkmark"
316 size={15}309 size={15}
317 tintColor={iconColor}310 tintColorClassName={"accent-icon"}
318 type="monochrome"311 type="monochrome"
319 />312 />
320 ) : action.image ? (313 ) : action.image ? (
321 <SymbolView314 <SymbolView
322 name={action.image as AppSymbolName}315 name={action.image as AppSymbolName}
323 size={15}316 size={15}
324 tintColor={destructive ? dangerColor : iconColor}317 tintColorClassName={
318 destructive ? "accent-danger-foreground" : "accent-icon"
319 }
325 type="monochrome"320 type="monochrome"
326 />321 />
327 ) : null}322 ) : null}
apps/mobile/src/components/AndroidScreenHeader.tsx+2-7
...@@ -5,7 +5,6 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";...@@ -5,7 +5,6 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
5import { SymbolView, type AppSymbolName } from "./AppSymbol";5import { SymbolView, type AppSymbolName } from "./AppSymbol";
6import { AppText as Text } from "./AppText";6import { AppText as Text } from "./AppText";
7import { cn } from "../lib/cn";7import { cn } from "../lib/cn";
8import { useThemeColor } from "../lib/useThemeColor";
98
10export interface AndroidHeaderAction {9export interface AndroidHeaderAction {
11 readonly accessibilityLabel: string;10 readonly accessibilityLabel: string;
...@@ -20,9 +19,6 @@ export function AndroidHeaderIconButton(props: {...@@ -20,9 +19,6 @@ export function AndroidHeaderIconButton(props: {
20 readonly onPress?: () => void;19 readonly onPress?: () => void;
21 readonly disabled?: boolean;20 readonly disabled?: boolean;
22}) {21}) {
23 const foregroundColor = useThemeColor("--color-foreground");
24 const disabledColor = useThemeColor("--color-icon-subtle");
25
26 return (22 return (
27 <Pressable23 <Pressable
28 accessibilityLabel={props.accessibilityLabel}24 accessibilityLabel={props.accessibilityLabel}
...@@ -38,7 +34,7 @@ export function AndroidHeaderIconButton(props: {...@@ -38,7 +34,7 @@ export function AndroidHeaderIconButton(props: {
38 <SymbolView34 <SymbolView
39 name={props.icon}35 name={props.icon}
40 size={20}36 size={20}
41 tintColor={props.disabled ? disabledColor : foregroundColor}37 tintColorClassName={props.disabled ? "accent-icon-subtle" : "accent-foreground"}
42 type="monochrome"38 type="monochrome"
43 />39 />
44 </Pressable>40 </Pressable>
...@@ -54,7 +50,6 @@ export function AndroidScreenHeader(props: {...@@ -54,7 +50,6 @@ export function AndroidScreenHeader(props: {
54 readonly embedded?: boolean;50 readonly embedded?: boolean;
55}) {51}) {
56 const insets = useSafeAreaInsets();52 const insets = useSafeAreaInsets();
57 const foregroundColor = useThemeColor("--color-foreground");
5853
59 return (54 return (
60 <View55 <View
...@@ -75,7 +70,7 @@ export function AndroidScreenHeader(props: {...@@ -75,7 +70,7 @@ export function AndroidScreenHeader(props: {
75 <SymbolView70 <SymbolView
76 name="chevron.left"71 name="chevron.left"
77 size={24}72 size={24}
78 tintColor={foregroundColor}73 tintColorClassName={"accent-foreground"}
79 type="monochrome"74 type="monochrome"
80 />75 />
81 </Pressable>76 </Pressable>
apps/mobile/src/components/AppSymbol.tsx+10-1
...@@ -81,6 +81,7 @@ import {...@@ -81,6 +81,7 @@ import {
81} from "@tabler/icons-react-native";81} from "@tabler/icons-react-native";
82import { Platform } from "react-native";82import { Platform } from "react-native";
83import { SymbolView as ExpoSymbolView, type SFSymbol, type SymbolViewProps } from "expo-symbols";83import { SymbolView as ExpoSymbolView, type SFSymbol, type SymbolViewProps } from "expo-symbols";
84import { withUniwind } from "uniwind";
8485
85const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = {86const ANDROID_ICON_BY_SF_SYMBOL: Partial<Record<SFSymbol, Icon>> = {
86 "arrow.branch": IconGitBranch,87 "arrow.branch": IconGitBranch,
...@@ -190,7 +191,7 @@ const ANDROID_ICON_BY_MATERIAL_NAME: Record<string, Icon> = {...@@ -190,7 +191,7 @@ const ANDROID_ICON_BY_MATERIAL_NAME: Record<string, Icon> = {
190export type { SFSymbol } from "expo-symbols";191export type { SFSymbol } from "expo-symbols";
191export type AppSymbolName = SymbolViewProps["name"];192export type AppSymbolName = SymbolViewProps["name"];
192193
193export function SymbolView(props: SymbolViewProps) {194function AppSymbolView(props: SymbolViewProps) {
194 if (Platform.OS !== "android") {195 if (Platform.OS !== "android") {
195 return <ExpoSymbolView {...props} />;196 return <ExpoSymbolView {...props} />;
196 }197 }
...@@ -216,3 +217,11 @@ export function SymbolView(props: SymbolViewProps) {...@@ -216,3 +217,11 @@ export function SymbolView(props: SymbolViewProps) {
216 />217 />
217 );218 );
218}219}
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";...@@ -9,7 +9,6 @@ import { AppText as Text } from "./AppText";
9import { T3Wordmark } from "./T3Wordmark";9import { T3Wordmark } from "./T3Wordmark";
10import { IPAD_HOME_TITLE_OFFSET } from "../lib/layoutMetrics";10import { IPAD_HOME_TITLE_OFFSET } from "../lib/layoutMetrics";
11import { resolveMobileStageLabel } from "../lib/mobileBranding";11import { resolveMobileStageLabel } from "../lib/mobileBranding";
12import { useThemeColor } from "../lib/useThemeColor";
13import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../native/native-glass";12import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../native/native-glass";
1413
15// Native leading items inherit different UIKit margins than title views.14// Native leading items inherit different UIKit margins than title views.
...@@ -37,9 +36,6 @@ export function CompactBrandTitle(...@@ -37,9 +36,6 @@ export function CompactBrandTitle(
37 readonly nativeLeadingItem?: boolean;36 readonly nativeLeadingItem?: boolean;
38 } = {},37 } = {},
39) {38) {
40 const iconColor = useThemeColor("--color-icon");
41 const mutedColor = useThemeColor("--color-foreground-muted");
42 const subtleColor = useThemeColor("--color-subtle");
43 const stageLabel = resolveMobileStageLabel(Constants.expoConfig?.extra?.appVariant);39 const stageLabel = resolveMobileStageLabel(Constants.expoConfig?.extra?.appVariant);
44 const titleOffset = brandTitleOffset(props.nativeLeadingItem === true);40 const titleOffset = brandTitleOffset(props.nativeLeadingItem === true);
4541
...@@ -49,42 +45,20 @@ export function CompactBrandTitle(...@@ -49,42 +45,20 @@ export function CompactBrandTitle(
49 accessibilityLabel="T3 Code, Threads"45 accessibilityLabel="T3 Code, Threads"
50 accessible46 accessible
51 role="heading"47 role="heading"
52 style={{48 className="flex-row items-center gap-1.5"
53 alignItems: "center",49 style={{ marginLeft: titleOffset }}
54 flexDirection: "row",
55 gap: 6,
56 marginLeft: titleOffset,
57 }}
58 >50 >
59 <T3Wordmark color={iconColor} height={15} />51 <T3Wordmark colorClassName="accent-icon" height={15} />
60 <Text52 <Text
61 allowFontScaling={props.allowFontScaling}53 allowFontScaling={props.allowFontScaling}
62 style={{54 className="font-t3-medium text-[21px] tracking-[-0.5px] text-foreground-muted"
63 color: mutedColor,
64 fontFamily: "DMSans-Medium",
65 fontSize: 21,
66 letterSpacing: -0.5,
67 }}
68 >55 >
69 Code56 Code
70 </Text>57 </Text>
71 <View58 <View className="rounded-full bg-subtle px-1.5 py-0.5">
72 style={{
73 backgroundColor: subtleColor,
74 borderRadius: 999,
75 paddingHorizontal: 6,
76 paddingVertical: 2,
77 }}
78 >
79 <Text59 <Text
80 allowFontScaling={props.allowFontScaling}60 allowFontScaling={props.allowFontScaling}
81 style={{61 className="font-t3-bold text-[9px] tracking-[0.9px] text-foreground-muted uppercase"
82 color: mutedColor,
83 fontFamily: "DMSans-Bold",
84 fontSize: 9,
85 letterSpacing: 0.9,
86 textTransform: "uppercase",
87 }}
88 >62 >
89 {stageLabel}63 {stageLabel}
90 </Text>64 </Text>
apps/mobile/src/components/ComposerAttachmentStrip.tsx+1-3
...@@ -1,6 +1,5 @@...@@ -1,6 +1,5 @@
1import { SymbolView } from "../components/AppSymbol";1import { SymbolView } from "../components/AppSymbol";
2import { Image, Pressable, ScrollView, View } from "react-native";2import { Image, Pressable, ScrollView, View } from "react-native";
3import { useThemeColor } from "../lib/useThemeColor";
43
5import type { DraftComposerImageAttachment } from "../lib/composerImages";4import type { DraftComposerImageAttachment } from "../lib/composerImages";
65
...@@ -24,7 +23,6 @@ export interface ComposerAttachmentStripProps {...@@ -24,7 +23,6 @@ export interface ComposerAttachmentStripProps {
24 * buttons. Used by both the thread composer and the new-task draft screen.23 * buttons. Used by both the thread composer and the new-task draft screen.
25 */24 */
26export function ComposerAttachmentStrip(props: ComposerAttachmentStripProps) {25export function ComposerAttachmentStrip(props: ComposerAttachmentStripProps) {
27 const subtleBg = useThemeColor("--color-subtle");
28 const size = props.imageSize ?? 72;26 const size = props.imageSize ?? 72;
29 const radius = props.imageBorderRadius ?? 16;27 const radius = props.imageBorderRadius ?? 16;
30 const removeButtonPlacement = props.removeButtonPlacement ?? "overlay";28 const removeButtonPlacement = props.removeButtonPlacement ?? "overlay";
...@@ -56,11 +54,11 @@ export function ComposerAttachmentStrip(props: ComposerAttachmentStripProps) {...@@ -56,11 +54,11 @@ export function ComposerAttachmentStrip(props: ComposerAttachmentStripProps) {
56 >54 >
57 <Image55 <Image
58 source={{ uri: image.previewUri }}56 source={{ uri: image.previewUri }}
57 className="bg-subtle"
59 style={{58 style={{
60 width: size,59 width: size,
61 height: size,60 height: size,
62 borderRadius: radius,61 borderRadius: radius,
63 backgroundColor: subtleBg,
64 }}62 }}
65 resizeMode="cover"63 resizeMode="cover"
66 />64 />
apps/mobile/src/components/ComposerToolbar.tsx+65-54
...@@ -11,9 +11,6 @@ import {...@@ -11,9 +11,6 @@ import {
11 type ViewStyle,11 type ViewStyle,
12} from "react-native";12} from "react-native";
1313
14import { useThemeColor } from "../lib/useThemeColor";
15import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
16import { themeColorWithAlpha } from "../lib/mobileTheme";
17import { cn } from "../lib/cn";14import { cn } from "../lib/cn";
18import { AppText as Text } from "./AppText";15import { AppText as Text } from "./AppText";
19import { SymbolView } from "./AppSymbol";16import { SymbolView } from "./AppSymbol";
...@@ -42,10 +39,6 @@ export function ComposerInlineControl(props: {...@@ -42,10 +39,6 @@ export function ComposerInlineControl(props: {
42 readonly chevronDirection?: "down" | "right";39 readonly chevronDirection?: "down" | "right";
43 readonly showChevron?: boolean;40 readonly showChevron?: boolean;
44}) {41}) {
45 const iconColor = useThemeColor(
46 props.emphasized || props.selected ? "--color-icon" : "--color-icon-muted",
47 );
48
49 return (42 return (
50 <Pressable43 <Pressable
51 accessibilityLabel={props.accessibilityLabel ?? props.label}44 accessibilityLabel={props.accessibilityLabel ?? props.label}
...@@ -62,7 +55,14 @@ export function ComposerInlineControl(props: {...@@ -62,7 +55,14 @@ export function ComposerInlineControl(props: {
62 {props.iconNode ? (55 {props.iconNode ? (
63 <View className="size-4 shrink-0 items-center justify-center">{props.iconNode}</View>56 <View className="size-4 shrink-0 items-center justify-center">{props.iconNode}</View>
64 ) : props.icon ? (57 ) : 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 />
66 ) : null}66 ) : null}
67 <Text67 <Text
68 className={cn(68 className={cn(
...@@ -77,7 +77,9 @@ export function ComposerInlineControl(props: {...@@ -77,7 +77,9 @@ export function ComposerInlineControl(props: {
77 <SymbolView77 <SymbolView
78 name={props.chevronDirection === "right" ? "chevron.right" : "chevron.down"}78 name={props.chevronDirection === "right" ? "chevron.right" : "chevron.down"}
79 size={10}79 size={10}
80 tintColor={iconColor}80 tintColorClassName={
81 props.emphasized || props.selected ? "accent-icon" : "accent-icon-muted"
82 }
81 type="monochrome"83 type="monochrome"
82 />84 />
83 )}85 )}
...@@ -111,8 +113,12 @@ export function ComposerToolbarRow(props: {...@@ -111,8 +113,12 @@ export function ComposerToolbarRow(props: {
111113
112export function ComposerToolbarScroller(props: {114export function ComposerToolbarScroller(props: {
113 readonly children: ReactNode;115 readonly children: ReactNode;
114 readonly fadeOpaque: string;116 /** Only for non-Uniwind surfaces such as the native terminal palette. */
115 readonly fadeTransparent: string;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";
116 readonly contentPaddingRight?: number;122 readonly contentPaddingRight?: number;
117}) {123}) {
118 const [metrics, setMetrics] = useState({124 const [metrics, setMetrics] = useState({
...@@ -170,27 +176,37 @@ export function ComposerToolbarScroller(props: {...@@ -170,27 +176,37 @@ export function ComposerToolbarScroller(props: {
170 </ScrollView>176 </ScrollView>
171 {scrollEdges.showLeftFade ? (177 {scrollEdges.showLeftFade ? (
172 <View178 <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 )}
173 pointerEvents="none"185 pointerEvents="none"
174 style={{186 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,
180 width: COMPOSER_TOOLBAR_FADE_WIDTH,187 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%)`,
181 }}192 }}
182 />193 />
183 ) : null}194 ) : null}
184 {scrollEdges.showRightFade ? (195 {scrollEdges.showRightFade ? (
185 <View196 <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 )}
186 pointerEvents="none"203 pointerEvents="none"
187 style={{204 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,
193 width: COMPOSER_TOOLBAR_FADE_WIDTH,205 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%)`,
194 }}210 }}
195 />211 />
196 ) : null}212 ) : null}
...@@ -214,30 +230,16 @@ export function ComposerToolbarButton(props: {...@@ -214,30 +230,16 @@ export function ComposerToolbarButton(props: {
214 readonly className?: string;230 readonly className?: string;
215 readonly style?: StyleProp<ViewStyle>;231 readonly style?: StyleProp<ViewStyle>;
216}) {232}) {
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");
223 const variant = props.variant ?? "default";233 const variant = props.variant ?? "default";
224 const isCircle = !props.label && props.showChevron === false;234 const isCircle = !props.label && props.showChevron === false;
225 const defaultBorderColor = useThemeColor("--color-border-subtle");235 const iconTintClassName =
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 =
234 variant === "primary"236 variant === "primary"
235 ? props.disabled237 ? props.disabled
236 ? iconSubtle238 ? "accent-icon-subtle"
237 : primaryFg239 : "accent-primary-foreground"
238 : variant === "danger"240 : variant === "danger"
239 ? dangerFg241 ? "accent-danger-foreground"
240 : iconColor;242 : "accent-icon";
241243
242 return (244 return (
243 <Pressable245 <Pressable
...@@ -250,7 +252,7 @@ export function ComposerToolbarButton(props: {...@@ -250,7 +252,7 @@ export function ComposerToolbarButton(props: {
250 // so callers can lift it with max-w-full — flex-filling pills in the252 // so callers can lift it with max-w-full — flex-filling pills in the
251 // thread composer stretch to the row's edge. The numeric maxWidth253 // thread composer stretch to the row's edge. The numeric maxWidth
252 // prop still wins via the inline style below.254 // 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",
254 isCircle ? "w-11" : "gap-2 px-3.5",256 isCircle ? "w-11" : "gap-2 px-3.5",
255 variant === "primary"257 variant === "primary"
256 ? props.disabled258 ? props.disabled
...@@ -261,24 +263,23 @@ export function ComposerToolbarButton(props: {...@@ -261,24 +263,23 @@ export function ComposerToolbarButton(props: {
261 : props.active263 : props.active
262 ? "bg-subtle-strong"264 ? "bg-subtle-strong"
263 : "bg-subtle",265 : "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",
264 props.className,275 props.className,
265 )}276 )}
266 style={({ pressed }) => [277 style={({ pressed }) => [
267 {278 {
268 borderColor:
269 variant === "default"
270 ? props.active
271 ? activeBorderColor
272 : defaultBorderColor
273 : filledBorderColor,
274 borderWidth: 1,
275 maxWidth: props.maxWidth,279 maxWidth: props.maxWidth,
276 minWidth: props.minWidth,280 minWidth: props.minWidth,
277 opacity: props.disabled ? 0.55 : pressed ? 0.72 : 1,281 opacity: props.disabled ? 0.55 : pressed ? 0.72 : 1,
278 shadowColor: "#000",282 shadowOpacity: props.disabled ? 0 : 1,
279 shadowOffset: { width: 0, height: isDarkMode ? 3 : 2 },
280 shadowOpacity: props.disabled ? 0 : isDarkMode ? 0.24 : 0.08,
281 shadowRadius: isDarkMode ? 10 : 8,
282 },283 },
283 props.style,284 props.style,
284 ]}285 ]}
...@@ -286,7 +287,12 @@ export function ComposerToolbarButton(props: {...@@ -286,7 +287,12 @@ export function ComposerToolbarButton(props: {
286 {props.iconNode ? (287 {props.iconNode ? (
287 <View className="h-4 w-4 items-center justify-center">{props.iconNode}</View>288 <View className="h-4 w-4 items-center justify-center">{props.iconNode}</View>
288 ) : props.icon ? (289 ) : 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 />
290 ) : null}296 ) : null}
291 {props.label ? (297 {props.label ? (
292 <Text298 <Text
...@@ -306,7 +312,12 @@ export function ComposerToolbarButton(props: {...@@ -306,7 +312,12 @@ export function ComposerToolbarButton(props: {
306 </Text>312 </Text>
307 ) : null}313 ) : null}
308 {props.showChevron === false ? null : (314 {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 />
310 )}321 )}
311 </Pressable>322 </Pressable>
312 );323 );
apps/mobile/src/components/ConfirmDialogHost.tsx+2-7
...@@ -1,7 +1,6 @@...@@ -1,7 +1,6 @@
1import { useCallback, useEffect, useState } from "react";1import { useCallback, useEffect, useState } from "react";
2import { Modal, Pressable, View } from "react-native";2import { Modal, Pressable, View } from "react-native";
33
4import { useThemeColor } from "../lib/useThemeColor";
5import { cn } from "../lib/cn";4import { cn } from "../lib/cn";
6import { AppText } from "./AppText";5import { AppText } from "./AppText";
76
...@@ -35,8 +34,6 @@ export function showConfirmDialog(request: ConfirmDialogRequest): void {...@@ -35,8 +34,6 @@ export function showConfirmDialog(request: ConfirmDialogRequest): void {
35 */34 */
36export function ConfirmDialogHost() {35export function ConfirmDialogHost() {
37 const [request, setRequest] = useState<ConfirmDialogRequest | null>(null);36 const [request, setRequest] = useState<ConfirmDialogRequest | null>(null);
38 const pressedOverlay = useThemeColor("--color-subtle");
39
40 useEffect(() => {37 useEffect(() => {
41 presentRequest = setRequest;38 presentRequest = setRequest;
42 return () => {39 return () => {
...@@ -76,8 +73,7 @@ export function ConfirmDialogHost() {...@@ -76,8 +73,7 @@ export function ConfirmDialogHost() {
76 <View className="overflow-hidden rounded-full">73 <View className="overflow-hidden rounded-full">
77 <Pressable74 <Pressable
78 accessibilityRole="button"75 accessibilityRole="button"
79 className="min-h-10 items-center justify-center px-4"76 className="min-h-10 items-center justify-center px-4 active:bg-subtle"
80 android_ripple={{ color: pressedOverlay }}
81 onPress={handleCancel}77 onPress={handleCancel}
82 >78 >
83 <AppText className="text-base font-t3-medium">79 <AppText className="text-base font-t3-medium">
...@@ -88,8 +84,7 @@ export function ConfirmDialogHost() {...@@ -88,8 +84,7 @@ export function ConfirmDialogHost() {
88 <View className="overflow-hidden rounded-full">84 <View className="overflow-hidden rounded-full">
89 <Pressable85 <Pressable
90 accessibilityRole="button"86 accessibilityRole="button"
91 className="min-h-10 items-center justify-center px-4"87 className="min-h-10 items-center justify-center px-4 active:bg-subtle"
92 android_ripple={{ color: pressedOverlay }}
93 onPress={handleConfirm}88 onPress={handleConfirm}
94 >89 >
95 <AppText90 <AppText
apps/mobile/src/components/ControlPill.tsx+11-11
...@@ -9,7 +9,6 @@ import {...@@ -9,7 +9,6 @@ import {
9 useRef,9 useRef,
10} from "react";10} from "react";
11import { Platform, Pressable, View } from "react-native";11import { Platform, Pressable, View } from "react-native";
12import { useThemeColor } from "../lib/useThemeColor";
13import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";12import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
1413
15import { cn } from "../lib/cn";14import { cn } from "../lib/cn";
...@@ -49,18 +48,14 @@ export function ControlPill(props: {...@@ -49,18 +48,14 @@ export function ControlPill(props: {
49 props.onPress?.();48 props.onPress?.();
50 };49 };
5150
52 const iconColor = useThemeColor("--color-icon");51 const iconTintClassName =
53 const iconSubtle = useThemeColor("--color-icon-subtle");
54 const primaryFg = useThemeColor("--color-primary-foreground");
55 const dangerFg = useThemeColor("--color-danger-foreground");
56 const iconTintColor =
57 variant === "primary"52 variant === "primary"
58 ? props.disabled53 ? props.disabled
59 ? iconSubtle54 ? "accent-icon-subtle"
60 : primaryFg55 : "accent-primary-foreground"
61 : variant === "danger"56 : variant === "danger"
62 ? dangerFg57 ? "accent-danger-foreground"
63 : iconColor;58 : "accent-icon";
6459
65 const isCircle =60 const isCircle =
66 variant === "circle" || variant === "danger" || (variant === "primary" && !props.label);61 variant === "circle" || variant === "danger" || (variant === "primary" && !props.label);
...@@ -101,7 +96,12 @@ export function ControlPill(props: {...@@ -101,7 +96,12 @@ export function ControlPill(props: {
101 {props.iconNode ? (96 {props.iconNode ? (
102 <View className="h-4 w-4 items-center justify-center">{props.iconNode}</View>97 <View className="h-4 w-4 items-center justify-center">{props.iconNode}</View>
103 ) : props.icon ? (98 ) : 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 />
105 ) : null}105 ) : null}
106 {props.label ? <Text className={labelClassName}>{props.label}</Text> : null}106 {props.label ? <Text className={labelClassName}>{props.label}</Text> : null}
107 </Pressable>107 </Pressable>
apps/mobile/src/components/ErrorBanner.tsx+2-4
...@@ -3,10 +3,8 @@ import { View } from "react-native";...@@ -3,10 +3,8 @@ import { View } from "react-native";
3import { AppText as Text } from "./AppText";3import { AppText as Text } from "./AppText";
4export function ErrorBanner(props: { readonly message: string }) {4export function ErrorBanner(props: { readonly message: string }) {
5 return (5 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">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-rose-700 dark:text-rose-300">7 <Text className="font-t3-medium text-sm text-adaptive-rose-700-300">{props.message}</Text>
8 {props.message}
9 </Text>
10 </View>8 </View>
11 );9 );
12}10}
apps/mobile/src/components/GlassSafeAreaView.tsx+1-12
...@@ -1,7 +1,6 @@...@@ -1,7 +1,6 @@
1import type { ReactNode } from "react";1import type { ReactNode } from "react";
2import { View, type StyleProp, type ViewStyle } from "react-native";2import { View, type StyleProp, type ViewStyle } from "react-native";
3import { useSafeAreaInsets } from "react-native-safe-area-context";3import { useSafeAreaInsets } from "react-native-safe-area-context";
4import { useThemeColor } from "../lib/useThemeColor";
54
6import { GlassSurface } from "./GlassSurface";5import { GlassSurface } from "./GlassSurface";
76
...@@ -19,23 +18,13 @@ export function GlassSafeAreaView({...@@ -19,23 +18,13 @@ export function GlassSafeAreaView({
19 style,18 style,
20}: GlassSafeAreaViewProps) {19}: GlassSafeAreaViewProps) {
21 const insets = useSafeAreaInsets();20 const insets = useSafeAreaInsets();
22 const headerColor = useThemeColor("--color-header");
23 const headerBorderColor = useThemeColor("--color-header-border");
24 const glassTint = useThemeColor("--color-glass-tint");
25 const headerPaddingTop = insets.top + 16;21 const headerPaddingTop = insets.top + 16;
26 const surfaceStyle = {
27 borderRadius: 0,
28 backgroundColor: headerColor,
29 borderBottomWidth: 1,
30 borderBottomColor: headerBorderColor,
31 } as const;
3222
33 return (23 return (
34 <View style={[surfaceStyle, style]}>24 <View className="border-b border-header-border bg-header" style={style}>
35 <GlassSurface25 <GlassSurface
36 chrome="none"26 chrome="none"
37 glassEffectStyle="regular"27 glassEffectStyle="regular"
38 tintColor={glassTint}
39 style={{ borderRadius: 0, backgroundColor: "transparent" }}28 style={{ borderRadius: 0, backgroundColor: "transparent" }}
40 >29 >
41 <View30 <View
apps/mobile/src/components/GlassSurface.tsx+37-15
...@@ -2,22 +2,29 @@ import { GlassView, isGlassEffectAPIAvailable } from "expo-glass-effect";...@@ -2,22 +2,29 @@ import { GlassView, isGlassEffectAPIAvailable } from "expo-glass-effect";
2import type { ReactNode } from "react";2import type { ReactNode } from "react";
3import {3import {
4 Platform,4 Platform,
5 useColorScheme,
5 View,6 View,
6 type ColorValue,7 type ColorValue,
7 type StyleProp,8 type StyleProp,
8 type ViewProps,9 type ViewProps,
9 type ViewStyle,10 type ViewStyle,
10} from "react-native";11} from "react-native";
11import { useThemeColor } from "../lib/useThemeColor";12import { withUniwind } from "uniwind";
12import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
1313
14interface GlassSurfaceProps extends Omit<ViewProps, "className"> {14import { cn } from "../lib/cn";
15
16const ThemedGlassView = withUniwind(GlassView);
17
18interface GlassSurfaceProps extends ViewProps {
15 readonly children: ReactNode;19 readonly children: ReactNode;
16 readonly glassEffectStyle?: "clear" | "regular" | "none";20 readonly glassEffectStyle?: "clear" | "regular" | "none";
17 readonly tintColor?: ColorValue;21 readonly tintColor?: ColorValue;
22 readonly tintColorClassName?: string;
18 readonly chrome?: "default" | "none";23 readonly chrome?: "default" | "none";
19 /** Styling used only when native Liquid Glass is unavailable. */24 /** Styling used only when native Liquid Glass is unavailable. */
20 readonly fallbackStyle?: StyleProp<ViewStyle>;25 readonly fallbackStyle?: StyleProp<ViewStyle>;
26 /** Uniwind styling used only when native Liquid Glass is unavailable. */
27 readonly fallbackClassName?: string;
21}28}
2229
23export function GlassSurface({30export function GlassSurface({
...@@ -25,22 +32,18 @@ export function GlassSurface({...@@ -25,22 +32,18 @@ export function GlassSurface({
25 glassEffectStyle = "regular",32 glassEffectStyle = "regular",
26 chrome = "default",33 chrome = "default",
27 tintColor,34 tintColor,
35 tintColorClassName,
28 fallbackStyle,36 fallbackStyle,
37 fallbackClassName,
38 className,
29 style,39 style,
30 ...props40 ...props
31}: GlassSurfaceProps) {41}: GlassSurfaceProps) {
32 const { themeAppearance } = useAppearancePreferences();42 const isDarkMode = useColorScheme() === "dark";
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");
37 const supportsGlass = Platform.OS === "ios" && isGlassEffectAPIAvailable();43 const supportsGlass = Platform.OS === "ios" && isGlassEffectAPIAvailable();
38 const surfaceStyle: ViewStyle = {44 const surfaceStyle: ViewStyle = {
39 borderRadius: 32,45 borderRadius: 32,
40 overflow: "hidden",46 overflow: "hidden",
41 borderWidth: chrome === "none" ? 0 : 1,
42 borderColor: chrome === "none" ? "transparent" : borderColor,
43 backgroundColor: chrome === "none" ? "transparent" : glassSurface,
44 shadowColor: chrome === "none" ? "transparent" : "#000000",47 shadowColor: chrome === "none" ? "transparent" : "#000000",
45 shadowOpacity: chrome === "none" ? 0 : isDarkMode ? 0.22 : 0.08,48 shadowOpacity: chrome === "none" ? 0 : isDarkMode ? 0.22 : 0.08,
46 shadowRadius: chrome === "none" ? 0 : 28,49 shadowRadius: chrome === "none" ? 0 : 28,
...@@ -59,20 +62,39 @@ export function GlassSurface({...@@ -59,20 +62,39 @@ export function GlassSurface({
5962
60 if (supportsGlass) {63 if (supportsGlass) {
61 return (64 return (
62 <GlassView65 <ThemedGlassView
63 {...props}66 {...props}
67 className={cn(
68 chrome === "none"
69 ? "border-0 border-transparent bg-transparent"
70 : "border border-border bg-glass-surface",
71 className,
72 )}
64 glassEffectStyle={glassEffectStyle}73 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 }
66 colorScheme={isDarkMode ? "dark" : "light"}78 colorScheme={isDarkMode ? "dark" : "light"}
67 style={[surfaceStyle, style]}79 style={[surfaceStyle, style]}
68 >80 >
69 {children}81 {children}
70 </GlassView>82 </ThemedGlassView>
71 );83 );
72 }84 }
7385
74 return (86 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 >
76 {children}98 {children}
77 </View>99 </View>
78 );100 );
apps/mobile/src/components/LoadingScreen.tsx+1-7
...@@ -1,6 +1,5 @@...@@ -1,6 +1,5 @@
1import { ActivityIndicator, StatusBar, View } from "react-native";1import { ActivityIndicator, StatusBar, View } from "react-native";
2import { useSafeAreaInsets } from "react-native-safe-area-context";2import { useSafeAreaInsets } from "react-native-safe-area-context";
3import { useThemeColor } from "../lib/useThemeColor";
4import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";3import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
54
6import { AppText as Text } from "./AppText";5import { AppText as Text } from "./AppText";
...@@ -11,17 +10,12 @@ export function LoadingScreen(props: {...@@ -11,17 +10,12 @@ export function LoadingScreen(props: {
11 readonly messagePlacement?: "above-spinner" | "below-spinner";10 readonly messagePlacement?: "above-spinner" | "below-spinner";
12}) {11}) {
13 const { themeAppearance: colorScheme } = useAppearancePreferences();12 const { themeAppearance: colorScheme } = useAppearancePreferences();
14 const screenBg = useThemeColor("--color-screen");
15 const insets = useSafeAreaInsets();13 const insets = useSafeAreaInsets();
16 const messagePlacement = props.messagePlacement ?? "below-spinner";14 const messagePlacement = props.messagePlacement ?? "below-spinner";
1715
18 return (16 return (
19 <View className="flex-1 bg-screen" style={{ paddingTop: insets.top }}>17 <View className="flex-1 bg-screen" style={{ paddingTop: insets.top }}>
20 <StatusBar18 <StatusBar barStyle={colorScheme === "dark" ? "light-content" : "dark-content"} translucent />
21 barStyle={colorScheme === "dark" ? "light-content" : "dark-content"}
22 backgroundColor={screenBg as string}
23 translucent
24 />
25 <View className="flex-1 items-center justify-center gap-5 px-6">19 <View className="flex-1 items-center justify-center gap-5 px-6">
26 <BrandMark compact />20 <BrandMark compact />
27 {messagePlacement === "above-spinner" ? (21 {messagePlacement === "above-spinner" ? (
apps/mobile/src/components/PierreEntryIcon.tsx+8-3
...@@ -3,7 +3,6 @@ import { Image, type ImageStyle, type StyleProp } from "react-native";...@@ -3,7 +3,6 @@ import { Image, type ImageStyle, type StyleProp } from "react-native";
33
4import { markdownFileIconSource } from "@t3tools/mobile-markdown-text/file-icons";4import { markdownFileIconSource } from "@t3tools/mobile-markdown-text/file-icons";
5import { resolveMarkdownFileIcon } from "@t3tools/mobile-markdown-text/links";5import { resolveMarkdownFileIcon } from "@t3tools/mobile-markdown-text/links";
6import { useThemeColor } from "../lib/useThemeColor";
76
8export function PierreEntryIcon(props: {7export function PierreEntryIcon(props: {
9 readonly path: string;8 readonly path: string;
...@@ -12,9 +11,15 @@ export function PierreEntryIcon(props: {...@@ -12,9 +11,15 @@ export function PierreEntryIcon(props: {
12 readonly style?: StyleProp<ImageStyle>;11 readonly style?: StyleProp<ImageStyle>;
13}) {12}) {
14 const size = props.size ?? 16;13 const size = props.size ?? 16;
15 const folderColor = useThemeColor("--color-icon-subtle");
16 if (props.kind === "directory") {14 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 );
18 }23 }
1924
20 return (25 return (
apps/mobile/src/components/ProjectFavicon.tsx+1-3
...@@ -7,7 +7,6 @@ import {...@@ -7,7 +7,6 @@ import {
7 getProjectFaviconCacheKey,7 getProjectFaviconCacheKey,
8 isProjectFaviconFallbackUrl,8 isProjectFaviconFallbackUrl,
9} from "@t3tools/shared/projectFavicon";9} from "@t3tools/shared/projectFavicon";
10import { useThemeColor } from "../lib/useThemeColor";
11import { useAssetUrl } from "../state/assets";10import { useAssetUrl } from "../state/assets";
12import {11import {
13 beginProjectFaviconRequest,12 beginProjectFaviconRequest,
...@@ -62,7 +61,6 @@ function ProjectFaviconImage(props: {...@@ -62,7 +61,6 @@ function ProjectFaviconImage(props: {
62 readonly projectTitle: string;61 readonly projectTitle: string;
63 readonly size: number;62 readonly size: number;
64}) {63}) {
65 const iconMuted = useThemeColor("--color-icon-subtle");
66 const faviconRequest = useMemo(64 const faviconRequest = useMemo(
67 () => createProjectFaviconRequest(props.cacheKey, props.faviconUrl),65 () => createProjectFaviconRequest(props.cacheKey, props.faviconUrl),
68 [props.cacheKey, props.faviconUrl],66 [props.cacheKey, props.faviconUrl],
...@@ -97,7 +95,7 @@ function ProjectFaviconImage(props: {...@@ -97,7 +95,7 @@ function ProjectFaviconImage(props: {
97 <SymbolView95 <SymbolView
98 name={{ ios: "folder.fill", android: props.open ? "folder_open" : "folder" }}96 name={{ ios: "folder.fill", android: props.open ? "folder_open" : "folder" }}
99 size={props.size * 0.78}97 size={props.size * 0.78}
100 tintColor={iconMuted}98 tintColorClassName={"accent-icon-subtle"}
101 type="monochrome"99 type="monochrome"
102 />100 />
103 ) : null}101 ) : null}
apps/mobile/src/components/SourceControlIcon.tsx+14-3
...@@ -1,4 +1,7 @@...@@ -1,4 +1,7 @@
1import Svg, { Defs, LinearGradient, Path, Stop } from "react-native-svg";1import Svg, { Defs, LinearGradient, Path, Stop } from "react-native-svg";
2import { withUniwind } from "uniwind";
3
4const ThemedSvg = withUniwind(Svg);
25
3export type SourceControlIconKind = "github" | "gitlab" | "bitbucket" | "azure-devops";6export type SourceControlIconKind = "github" | "gitlab" | "bitbucket" | "azure-devops";
47
...@@ -6,20 +9,28 @@ export function SourceControlIcon(props: {...@@ -6,20 +9,28 @@ export function SourceControlIcon(props: {
6 readonly kind: SourceControlIconKind;9 readonly kind: SourceControlIconKind;
7 readonly size?: number;10 readonly size?: number;
8 readonly color?: string;11 readonly color?: string;
12 readonly colorClassName?: string;
9}) {13}) {
10 const size = props.size ?? 18;14 const size = props.size ?? 18;
1115
12 switch (props.kind) {16 switch (props.kind) {
13 case "github":17 case "github":
14 return (18 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 >
16 <Path27 <Path
17 fillRule="evenodd"28 fillRule="evenodd"
18 clipRule="evenodd"29 clipRule="evenodd"
19 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"30 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"
21 />32 />
22 </Svg>33 </ThemedSvg>
23 );34 );
24 case "gitlab":35 case "gitlab":
25 return (36 return (
apps/mobile/src/components/T3Wordmark.tsx+12-3
...@@ -1,11 +1,18 @@...@@ -1,11 +1,18 @@
1import type { ColorValue } from "react-native";1import type { ColorValue } from "react-native";
2import Svg, { Path } from "react-native-svg";2import Svg, { Path } from "react-native-svg";
3import { withUniwind } from "uniwind";
4
5const ThemedPath = withUniwind(Path);
36
4/**7/**
5 * The "T3" brand mark, matching the desktop sidebar's T3Wordmark SVG8 * The "T3" brand mark, matching the desktop sidebar's T3Wordmark SVG
6 * (apps/web Sidebar.tsx). Width derives from the viewBox aspect ratio.9 * (apps/web Sidebar.tsx). Width derives from the viewBox aspect ratio.
7 */10 */
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}) {
9 const aspectRatio = 94.3941 / 56.96;16 const aspectRatio = 94.3941 / 56.96;
10 return (17 return (
11 <Svg18 <Svg
...@@ -14,9 +21,11 @@ export function T3Wordmark(props: { readonly height: number; readonly color: Col...@@ -14,9 +21,11 @@ export function T3Wordmark(props: { readonly height: number; readonly color: Col
14 width={props.height * aspectRatio}21 width={props.height * aspectRatio}
15 viewBox="15.5309 37 94.3941 56.96"22 viewBox="15.5309 37 94.3941 56.96"
16 >23 >
17 <Path24 <ThemedPath
18 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"25 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"
20 />29 />
21 </Svg>30 </Svg>
22 );31 );
apps/mobile/src/components/ThemedSwitch.tsx+9-11
...@@ -1,21 +1,19 @@...@@ -1,21 +1,19 @@
1import { Platform, Switch, type SwitchProps } from "react-native";1import { Platform, Switch, type SwitchProps } from "react-native";
22
3import { useThemeColor } from "../lib/useThemeColor";
4
5export function ThemedSwitch(props: SwitchProps) {3export 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
11 return (4 return (
12 <Switch5 <Switch
13 {...props}6 {...props}
14 ios_backgroundColor={inactiveTrack}7 ios_backgroundColorClassName="accent-switch-inactive-track"
15 thumbColor={8 thumbColorClassName={
16 Platform.OS === "android" ? (props.value ? activeThumb : inactiveThumb) : undefined9 Platform.OS === "android"
10 ? props.value
11 ? "accent-switch-active-thumb"
12 : "accent-switch-inactive-thumb"
13 : undefined
17 }14 }
18 trackColor={{ false: inactiveTrack, true: activeTrack }}15 trackColorOffClassName="accent-switch-inactive-track"
16 trackColorOnClassName="accent-switch-active-track"
19 />17 />
20 );18 );
21}19}
apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx+16-20
...@@ -27,7 +27,7 @@ import { EmptyState } from "../../components/EmptyState";...@@ -27,7 +27,7 @@ import { EmptyState } from "../../components/EmptyState";
27import { ProjectFavicon } from "../../components/ProjectFavicon";27import { ProjectFavicon } from "../../components/ProjectFavicon";
28import { useSafeAreaInsets } from "react-native-safe-area-context";28import { useSafeAreaInsets } from "react-native-safe-area-context";
29import { relativeTime } from "../../lib/time";29import { relativeTime } from "../../lib/time";
30import { useThemeColor } from "../../lib/useThemeColor";30import { useUniwindTheme } from "../../lib/useUniwindTheme";
31import { ThreadSwipeable } from "../home/thread-swipe-actions";31import { ThreadSwipeable } from "../home/thread-swipe-actions";
32import {32import {
33 createNativeMailSearchToolbarItem,33 createNativeMailSearchToolbarItem,
...@@ -70,8 +70,6 @@ function ArchivedThreadsHeader(props: {...@@ -70,8 +70,6 @@ function ArchivedThreadsHeader(props: {
70 const navigation = useNavigation();70 const navigation = useNavigation();
71 const insets = useSafeAreaInsets();71 const insets = useSafeAreaInsets();
72 const hasCustomFilter = props.selectedEnvironmentId !== null || props.sortOrder !== "newest";72 const hasCustomFilter = props.selectedEnvironmentId !== null || props.sortOrder !== "newest";
73 const searchIconColor = useThemeColor("--color-icon");
74 const searchTextColor = useThemeColor("--color-foreground");
75 const usesNativeChrome = Platform.OS === "ios";73 const usesNativeChrome = Platform.OS === "ios";
76 const usesCompactMailToolbar =74 const usesCompactMailToolbar =
77 Platform.OS === "ios" && width < 700 && NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED;75 Platform.OS === "ios" && width < 700 && NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED;
...@@ -154,7 +152,7 @@ function ArchivedThreadsHeader(props: {...@@ -154,7 +152,7 @@ function ArchivedThreadsHeader(props: {
154 <SymbolView152 <SymbolView
155 name="chevron.left"153 name="chevron.left"
156 size={24}154 size={24}
157 tintColor={searchTextColor}155 tintColorClassName={"accent-foreground"}
158 type="monochrome"156 type="monochrome"
159 />157 />
160 </Pressable>158 </Pressable>
...@@ -162,7 +160,7 @@ function ArchivedThreadsHeader(props: {...@@ -162,7 +160,7 @@ function ArchivedThreadsHeader(props: {
162 <SymbolView160 <SymbolView
163 name="magnifyingglass"161 name="magnifyingglass"
164 size={17}162 size={17}
165 tintColor={searchIconColor}163 tintColorClassName={"accent-icon"}
166 type="monochrome"164 type="monochrome"
167 />165 />
168 <TextInput166 <TextInput
...@@ -192,7 +190,7 @@ function ArchivedThreadsHeader(props: {...@@ -192,7 +190,7 @@ function ArchivedThreadsHeader(props: {
192 : "line.3.horizontal.decrease.circle"190 : "line.3.horizontal.decrease.circle"
193 }191 }
194 size={16}192 size={16}
195 tintColor={searchIconColor}193 tintColorClassName={"accent-icon"}
196 type="monochrome"194 type="monochrome"
197 />195 />
198 </Pressable>196 </Pressable>
...@@ -402,9 +400,7 @@ function ArchivedThreadRow(props: {...@@ -402,9 +400,7 @@ function ArchivedThreadRow(props: {
402 readonly thread: EnvironmentThreadShell;400 readonly thread: EnvironmentThreadShell;
403}) {401}) {
404 const { width: windowWidth } = useWindowDimensions();402 const { width: windowWidth } = useWindowDimensions();
405 const cardColor = useThemeColor("--color-card");403 const cardColor = useUniwindTheme()["--color-card"];
406 const iconColor = useThemeColor("--color-icon-subtle");
407 const separatorColor = useThemeColor("--color-separator");
408 const timestamp = relativeTime(props.thread.archivedAt ?? props.thread.updatedAt);404 const timestamp = relativeTime(props.thread.archivedAt ?? props.thread.updatedAt);
409 const subtitle = [props.environmentLabel, props.thread.branch].filter((part): part is string =>405 const subtitle = [props.environmentLabel, props.thread.branch].filter((part): part is string =>
410 Boolean(part),406 Boolean(part),
...@@ -436,14 +432,15 @@ function ArchivedThreadRow(props: {...@@ -436,14 +432,15 @@ function ArchivedThreadRow(props: {
436 >432 >
437 {() => (433 {() => (
438 <View434 <View
439 className="flex-row items-center gap-3 bg-card px-4 py-3"435 className={`flex-row items-center gap-3 bg-card px-4 py-3 ${props.isLast ? "" : "border-b border-separator"}`}
440 style={{
441 borderBottomColor: separatorColor,
442 borderBottomWidth: props.isLast ? 0 : 1,
443 }}
444 >436 >
445 <View className="h-[34px] w-[34px] items-center justify-center rounded-[11px] bg-subtle">437 <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 />
447 </View>444 </View>
448445
449 <View className="min-w-0 flex-1 gap-1">446 <View className="min-w-0 flex-1 gap-1">
...@@ -463,7 +460,7 @@ function ArchivedThreadRow(props: {...@@ -463,7 +460,7 @@ function ArchivedThreadRow(props: {
463 <SymbolView460 <SymbolView
464 name="arrow.triangle.branch"461 name="arrow.triangle.branch"
465 size={10}462 size={10}
466 tintColor={iconColor}463 tintColorClassName={"accent-icon-subtle"}
467 type="monochrome"464 type="monochrome"
468 />465 />
469 <Text466 <Text
...@@ -513,7 +510,6 @@ export function ArchivedThreadsScreen(props: {...@@ -513,7 +510,6 @@ export function ArchivedThreadsScreen(props: {
513 const { onDeleteThread, onUnarchiveThread } = props;510 const { onDeleteThread, onUnarchiveThread } = props;
514 const openSwipeableRef = useRef<SwipeableMethods | null>(null);511 const openSwipeableRef = useRef<SwipeableMethods | null>(null);
515 const archiveScrollGesture = useMemo(() => Gesture.Native(), []);512 const archiveScrollGesture = useMemo(() => Gesture.Native(), []);
516 const refreshTint = useThemeColor("--color-icon");
517 const environmentLabelsById = useMemo(513 const environmentLabelsById = useMemo(
518 () =>514 () =>
519 new Map(515 new Map(
...@@ -594,7 +590,7 @@ export function ArchivedThreadsScreen(props: {...@@ -594,7 +590,7 @@ export function ArchivedThreadsScreen(props: {
594 if (isInitialLoad) {590 if (isInitialLoad) {
595 return (591 return (
596 <View className="items-center py-16">592 <View className="items-center py-16">
597 <ActivityIndicator color={refreshTint} />593 <ActivityIndicator colorClassName={"accent-icon"} />
598 <Text className="mt-3 text-sm text-foreground-muted">Loading archive...</Text>594 <Text className="mt-3 text-sm text-foreground-muted">Loading archive...</Text>
599 </View>595 </View>
600 );596 );
...@@ -610,7 +606,7 @@ export function ArchivedThreadsScreen(props: {...@@ -610,7 +606,7 @@ export function ArchivedThreadsScreen(props: {
610 title={isFiltered ? "No matching threads" : "No archived threads"}606 title={isFiltered ? "No matching threads" : "No archived threads"}
611 />607 />
612 );608 );
613 }, [isFiltered, isInitialLoad, refreshTint]);609 }, [isFiltered, isInitialLoad]);
614610
615 return (611 return (
616 <View className="flex-1 bg-sheet">612 <View className="flex-1 bg-sheet">
...@@ -649,7 +645,7 @@ export function ArchivedThreadsScreen(props: {...@@ -649,7 +645,7 @@ export function ArchivedThreadsScreen(props: {
649 <RefreshControl645 <RefreshControl
650 onRefresh={props.onRefresh}646 onRefresh={props.onRefresh}
651 refreshing={props.isLoading && !isInitialLoad}647 refreshing={props.isLoading && !isInitialLoad}
652 tintColor={String(refreshTint)}648 tintColorClassName={String("accent-icon")}
653 />649 />
654 }650 }
655 renderItem={renderListItem}651 renderItem={renderListItem}
apps/mobile/src/features/connection/CloudEnvironmentRows.tsx+11-11
...@@ -18,7 +18,6 @@ import { AppText as Text } from "../../components/AppText";...@@ -18,7 +18,6 @@ import { AppText as Text } from "../../components/AppText";
18import { ThemedSwitch } from "../../components/ThemedSwitch";18import { ThemedSwitch } from "../../components/ThemedSwitch";
19import { cn } from "../../lib/cn";19import { cn } from "../../lib/cn";
20import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic";20import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic";
21import { useThemeColor } from "../../lib/useThemeColor";
22import type { ConnectedEnvironmentSummary } from "../../state/remote-runtime-types";21import type { ConnectedEnvironmentSummary } from "../../state/remote-runtime-types";
23import { availableCloudEnvironmentPresentation } from "../cloud/cloudEnvironmentPresentation";22import { availableCloudEnvironmentPresentation } from "../cloud/cloudEnvironmentPresentation";
24import { hasCloudPublicConfig } from "../cloud/publicConfig";23import { hasCloudPublicConfig } from "../cloud/publicConfig";
...@@ -78,7 +77,6 @@ function CloudEnvironmentRowsContent(...@@ -78,7 +77,6 @@ function CloudEnvironmentRowsContent(
78 props: CloudEnvironmentRowsProps & { readonly discoveryAvailable?: boolean },77 props: CloudEnvironmentRowsProps & { readonly discoveryAvailable?: boolean },
79) {78) {
80 const controller = useConnectionController();79 const controller = useConnectionController();
81 const iconColor = useThemeColor("--color-icon");
82 const discoveryAvailable = props.discoveryAvailable ?? true;80 const discoveryAvailable = props.discoveryAvailable ?? true;
83 const availableCloudEnvironments = discoveryAvailable81 const availableCloudEnvironments = discoveryAvailable
84 ? (props.showcaseAvailableEnvironments ?? controller.availableRelayEnvironments)82 ? (props.showcaseAvailableEnvironments ?? controller.availableRelayEnvironments)
...@@ -118,12 +116,12 @@ function CloudEnvironmentRowsContent(...@@ -118,12 +116,12 @@ function CloudEnvironmentRowsContent(
118 className="h-9 w-9 items-center justify-center rounded-full bg-subtle active:opacity-70 disabled:opacity-50"116 className="h-9 w-9 items-center justify-center rounded-full bg-subtle active:opacity-70 disabled:opacity-50"
119 >117 >
120 {controller.relayDiscovery.isRefreshing ? (118 {controller.relayDiscovery.isRefreshing ? (
121 <ActivityIndicator color={iconColor} size="small" />119 <ActivityIndicator colorClassName={"accent-icon"} size="small" />
122 ) : (120 ) : (
123 <SymbolView121 <SymbolView
124 name="arrow.clockwise"122 name="arrow.clockwise"
125 size={14}123 size={14}
126 tintColor={iconColor}124 tintColorClassName={"accent-icon"}
127 type="monochrome"125 type="monochrome"
128 />126 />
129 )}127 )}
...@@ -158,7 +156,7 @@ function CloudEnvironmentRowsContent(...@@ -158,7 +156,7 @@ function CloudEnvironmentRowsContent(
158 </View>156 </View>
159 ) : controller.relayDiscovery.isRefreshing ? (157 ) : controller.relayDiscovery.isRefreshing ? (
160 <View collapsable={false} className="items-center gap-3 rounded-[24px] bg-card p-6">158 <View collapsable={false} className="items-center gap-3 rounded-[24px] bg-card p-6">
161 <ActivityIndicator color={iconColor} />159 <ActivityIndicator colorClassName={"accent-icon"} />
162 <Text className="text-center text-sm leading-normal text-foreground-muted">160 <Text className="text-center text-sm leading-normal text-foreground-muted">
163 Loading linked cloud environments.161 Loading linked cloud environments.
164 </Text>162 </Text>
...@@ -275,7 +273,6 @@ function CloudEnvironmentRowShell(props: {...@@ -275,7 +273,6 @@ function CloudEnvironmentRowShell(props: {
275 readonly statusText?: string;273 readonly statusText?: string;
276 readonly value: boolean;274 readonly value: boolean;
277}) {275}) {
278 const chevron = useThemeColor("--color-chevron");
279 const isRetrying =276 const isRetrying =
280 props.connectionState === "connecting" || props.connectionState === "reconnecting";277 props.connectionState === "connecting" || props.connectionState === "reconnecting";
281 const shouldPulse = isRetrying;278 const shouldPulse = isRetrying;
...@@ -287,7 +284,7 @@ function CloudEnvironmentRowShell(props: {...@@ -287,7 +284,7 @@ function CloudEnvironmentRowShell(props: {
287 traceId: props.connectionErrorTraceId,284 traceId: props.connectionErrorTraceId,
288 });285 });
289 const statusClassName = props.connectionError286 const statusClassName = props.connectionError
290 ? "text-rose-500 dark:text-rose-400"287 ? "text-adaptive-rose-500-400"
291 : "text-foreground-muted";288 : "text-foreground-muted";
292 const [errorMeasurement, setErrorMeasurement] = useState<{289 const [errorMeasurement, setErrorMeasurement] = useState<{
293 readonly text: string;290 readonly text: string;
...@@ -377,7 +374,7 @@ function CloudEnvironmentRowShell(props: {...@@ -377,7 +374,7 @@ function CloudEnvironmentRowShell(props: {
377 <SymbolView374 <SymbolView
378 name="chevron.down"375 name="chevron.down"
379 size={10}376 size={10}
380 tintColor={chevron}377 tintColorClassName={"accent-chevron"}
381 type="monochrome"378 type="monochrome"
382 style={{379 style={{
383 marginTop: 3,380 marginTop: 3,
...@@ -397,8 +394,6 @@ function CloudEnvironmentRowShell(props: {...@@ -397,8 +394,6 @@ function CloudEnvironmentRowShell(props: {
397}394}
398395
399function CopyTraceIdButton(props: { readonly traceId: string }) {396function CopyTraceIdButton(props: { readonly traceId: string }) {
400 const iconColor = useThemeColor("--color-icon");
401
402 return (397 return (
403 <Pressable398 <Pressable
404 accessibilityRole="button"399 accessibilityRole="button"
...@@ -407,7 +402,12 @@ function CopyTraceIdButton(props: { readonly traceId: string }) {...@@ -407,7 +402,12 @@ function CopyTraceIdButton(props: { readonly traceId: string }) {
407 }}402 }}
408 className="self-start flex-row items-center gap-1.5 rounded-full bg-subtle px-3 py-2 active:opacity-70"403 className="self-start flex-row items-center gap-1.5 rounded-full bg-subtle px-3 py-2 active:opacity-70"
409 >404 >
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 />
411 <Text className="text-xs font-t3-bold text-foreground">Copy trace ID</Text>411 <Text className="text-xs font-t3-bold text-foreground">Copy trace ID</Text>
412 </Pressable>412 </Pressable>
413 );413 );
apps/mobile/src/features/connection/ConnectionEnvironmentRow.tsx+15-10
...@@ -7,7 +7,6 @@ import { AsyncResult } from "effect/unstable/reactivity";...@@ -7,7 +7,6 @@ import { AsyncResult } from "effect/unstable/reactivity";
7import { useCallback, useState } from "react";7import { useCallback, useState } from "react";
8import { Alert, Pressable, View } from "react-native";8import { Alert, Pressable, View } from "react-native";
9import Animated, { FadeIn, FadeOut, LinearTransition } from "react-native-reanimated";9import Animated, { FadeIn, FadeOut, LinearTransition } from "react-native-reanimated";
10import { useThemeColor } from "../../lib/useThemeColor";
1110
12import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText";11import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText";
13import { cn } from "../../lib/cn";12import { cn } from "../../lib/cn";
...@@ -36,10 +35,6 @@ export function ConnectionEnvironmentRow(props: {...@@ -36,10 +35,6 @@ export function ConnectionEnvironmentRow(props: {
36}) {35}) {
37 const [label, setLabel] = useState(props.environment.environmentLabel);36 const [label, setLabel] = useState(props.environment.environmentLabel);
38 const [url, setUrl] = useState(props.environment.displayUrl);37 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");
43 const statusLabel = connectionStatusLabel(props.environment);38 const statusLabel = connectionStatusLabel(props.environment);
44 const statusTraceId = props.environment.connectionErrorTraceId;39 const statusTraceId = props.environment.connectionErrorTraceId;
45 const hasConnectionFailure = props.environment.connectionError !== null;40 const hasConnectionFailure = props.environment.connectionError !== null;
...@@ -85,7 +80,7 @@ export function ConnectionEnvironmentRow(props: {...@@ -85,7 +80,7 @@ export function ConnectionEnvironmentRow(props: {
85 <Text80 <Text
86 className={cn(81 className={cn(
87 "text-xs",82 "text-xs",
88 hasConnectionFailure ? "text-rose-500 dark:text-rose-400" : "text-foreground-muted",83 hasConnectionFailure ? "text-adaptive-rose-500-400" : "text-foreground-muted",
89 )}84 )}
90 numberOfLines={props.expanded ? undefined : 1}85 numberOfLines={props.expanded ? undefined : 1}
91 selectable={props.expanded}86 selectable={props.expanded}
...@@ -117,7 +112,7 @@ export function ConnectionEnvironmentRow(props: {...@@ -117,7 +112,7 @@ export function ConnectionEnvironmentRow(props: {
117 <SymbolView112 <SymbolView
118 name="chevron.down"113 name="chevron.down"
119 size={12}114 size={12}
120 tintColor={mutedColor}115 tintColorClassName={"accent-icon-subtle"}
121 type="monochrome"116 type="monochrome"
122 style={{117 style={{
123 transform: [{ rotate: props.expanded ? "180deg" : "0deg" }],118 transform: [{ rotate: props.expanded ? "180deg" : "0deg" }],
...@@ -174,7 +169,12 @@ export function ConnectionEnvironmentRow(props: {...@@ -174,7 +169,12 @@ export function ConnectionEnvironmentRow(props: {
174 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"169 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"
175 onPress={handleSave}170 onPress={handleSave}
176 >171 >
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 />
178 <Text className="text-xs font-t3-bold tracking-[0.8px] uppercase text-primary-foreground">178 <Text className="text-xs font-t3-bold tracking-[0.8px] uppercase text-primary-foreground">
179 Save179 Save
180 </Text>180 </Text>
...@@ -188,7 +188,7 @@ export function ConnectionEnvironmentRow(props: {...@@ -188,7 +188,7 @@ export function ConnectionEnvironmentRow(props: {
188 <SymbolView188 <SymbolView
189 name="arrow.clockwise"189 name="arrow.clockwise"
190 size={14}190 size={14}
191 tintColor={mutedColor}191 tintColorClassName={"accent-icon-subtle"}
192 type="monochrome"192 type="monochrome"
193 />193 />
194 </Pressable>194 </Pressable>
...@@ -197,7 +197,12 @@ export function ConnectionEnvironmentRow(props: {...@@ -197,7 +197,12 @@ export function ConnectionEnvironmentRow(props: {
197 className="h-[42px] w-[42px] items-center justify-center rounded-[14px] border border-danger-border bg-danger active:opacity-70"197 className="h-[42px] w-[42px] items-center justify-center rounded-[14px] border border-danger-border bg-danger active:opacity-70"
198 onPress={() => props.onRemove(props.environment.environmentId)}198 onPress={() => props.onRemove(props.environment.environmentId)}
199 >199 >
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 />
201 </Pressable>206 </Pressable>
202 </View>207 </View>
203 </Animated.View>208 </Animated.View>
apps/mobile/src/features/connection/ConnectionSheetButton.tsx+7-7
...@@ -1,6 +1,5 @@...@@ -1,6 +1,5 @@
1import { SymbolView } from "../../components/AppSymbol";1import { SymbolView } from "../../components/AppSymbol";
2import { Platform, Pressable } from "react-native";2import { Platform, Pressable } from "react-native";
3import { useThemeColor } from "../../lib/useThemeColor";
43
5import { AppText as Text } from "../../components/AppText";4import { AppText as Text } from "../../components/AppText";
6import { cn } from "../../lib/cn";5import { cn } from "../../lib/cn";
...@@ -37,11 +36,12 @@ export function ConnectionSheetButton(props: {...@@ -37,11 +36,12 @@ export function ConnectionSheetButton(props: {
37}) {36}) {
38 const tone = props.tone ?? "secondary";37 const tone = props.tone ?? "secondary";
3938
40 const primaryFg = useThemeColor("--color-primary-foreground");39 const textColorClassName =
41 const dangerFg = useThemeColor("--color-danger-foreground");40 tone === "primary"
42 const secondaryFg = useThemeColor("--color-secondary-foreground");41 ? "accent-primary-foreground"
4342 : tone === "danger"
44 const textColor = tone === "primary" ? primaryFg : tone === "danger" ? dangerFg : secondaryFg;43 ? "accent-danger-foreground"
44 : "accent-secondary-foreground";
4545
46 const primaryShadow =46 const primaryShadow =
47 tone === "primary"47 tone === "primary"
...@@ -79,7 +79,7 @@ export function ConnectionSheetButton(props: {...@@ -79,7 +79,7 @@ export function ConnectionSheetButton(props: {
79 <SymbolView79 <SymbolView
80 name={props.icon}80 name={props.icon}
81 size={props.compact ? 13 : 14}81 size={props.compact ? 13 : 14}
82 tintColor={textColor}82 tintColorClassName={textColorClassName}
83 type="monochrome"83 type="monochrome"
84 />84 />
85 <Text85 <Text
apps/mobile/src/features/connection/ConnectionsNewRouteScreen.tsx+2-2
...@@ -5,7 +5,7 @@ import { AsyncResult } from "effect/unstable/reactivity";...@@ -5,7 +5,7 @@ import { AsyncResult } from "effect/unstable/reactivity";
5import { useCallback, useEffect, useRef, useState } from "react";5import { useCallback, useEffect, useRef, useState } from "react";
6import { Alert, Linking, Platform, ScrollView, View } from "react-native";6import { Alert, Linking, Platform, ScrollView, View } from "react-native";
7import { useSafeAreaInsets } from "react-native-safe-area-context";7import { useSafeAreaInsets } from "react-native-safe-area-context";
8import { useThemeColor } from "../../lib/useThemeColor";8import { useUniwindTheme } from "../../lib/useUniwindTheme";
99
10import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";10import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";
11import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText";11import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText";
...@@ -47,7 +47,7 @@ export function ConnectionsNewRouteScreen({...@@ -47,7 +47,7 @@ export function ConnectionsNewRouteScreen({
47 const [scannerLocked, setScannerLocked] = useState(false);47 const [scannerLocked, setScannerLocked] = useState(false);
48 const attemptedAutoConnectRef = useRef<string | null>(null);48 const attemptedAutoConnectRef = useRef<string | null>(null);
4949
50 const headerIconColor = useThemeColor("--color-icon");50 const headerIconColor = useUniwindTheme()["--color-icon"];
5151
52 const connectDisabled = isSubmitting || hostInput.trim().length === 0;52 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";...@@ -5,7 +5,6 @@ import type { EnvironmentId } from "@t3tools/contracts";
5import { useCallback, useState } from "react";5import { useCallback, useState } from "react";
6import { Platform, ScrollView, View } from "react-native";6import { Platform, ScrollView, View } from "react-native";
7import { useSafeAreaInsets } from "react-native-safe-area-context";7import { useSafeAreaInsets } from "react-native-safe-area-context";
8import { useThemeColor } from "../../lib/useThemeColor";
98
10import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";9import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";
11import { AppText as Text } from "../../components/AppText";10import { AppText as Text } from "../../components/AppText";
...@@ -24,9 +23,6 @@ export function ConnectionsRouteScreen() {...@@ -24,9 +23,6 @@ export function ConnectionsRouteScreen() {
24 const insets = useSafeAreaInsets();23 const insets = useSafeAreaInsets();
25 const hasEnvironments = connectedEnvironments.length > 0;24 const hasEnvironments = connectedEnvironments.length > 0;
26 const [expandedId, setExpandedId] = useState<EnvironmentId | null>(null);25 const [expandedId, setExpandedId] = useState<EnvironmentId | null>(null);
27
28 const accentColor = useThemeColor("--color-icon-muted");
29
30 const handleToggle = useCallback((environmentId: EnvironmentId) => {26 const handleToggle = useCallback((environmentId: EnvironmentId) => {
31 setExpandedId((prev) => (prev === environmentId ? null : environmentId));27 setExpandedId((prev) => (prev === environmentId ? null : environmentId));
32 }, []);28 }, []);
...@@ -89,7 +85,7 @@ export function ConnectionsRouteScreen() {...@@ -89,7 +85,7 @@ export function ConnectionsRouteScreen() {
89 <SymbolView85 <SymbolView
90 name="point.3.connected.trianglepath.dotted"86 name="point.3.connected.trianglepath.dotted"
91 size={20}87 size={20}
92 tintColor={accentColor}88 tintColorClassName={"accent-icon-muted"}
93 type="monochrome"89 type="monochrome"
94 />90 />
95 </View>91 </View>
apps/mobile/src/features/connection/EnvironmentConnectionNotice.tsx+2-4
...@@ -7,7 +7,6 @@ import { ActivityIndicator, Pressable, View } from "react-native";...@@ -7,7 +7,6 @@ import { ActivityIndicator, Pressable, View } from "react-native";
77
8import { AppText as Text } from "../../components/AppText";8import { AppText as Text } from "../../components/AppText";
9import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic";9import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic";
10import { useThemeColor } from "../../lib/useThemeColor";
1110
12function noticeTitle(phase: EnvironmentConnectionPhase, environmentLabel: string): string {11function noticeTitle(phase: EnvironmentConnectionPhase, environmentLabel: string): string {
13 switch (phase) {12 switch (phase) {
...@@ -55,7 +54,6 @@ export function EnvironmentConnectionNotice(props: {...@@ -55,7 +54,6 @@ export function EnvironmentConnectionNotice(props: {
55 readonly resourceName: string;54 readonly resourceName: string;
56 readonly onRetry: () => void;55 readonly onRetry: () => void;
57}) {56}) {
58 const iconColor = String(useThemeColor("--color-icon-muted"));
59 const isRetrying =57 const isRetrying =
60 props.connection.phase === "connecting" || props.connection.phase === "reconnecting";58 props.connection.phase === "connecting" || props.connection.phase === "reconnecting";
6159
...@@ -63,12 +61,12 @@ export function EnvironmentConnectionNotice(props: {...@@ -63,12 +61,12 @@ export function EnvironmentConnectionNotice(props: {
63 <View className="flex-1 items-center justify-center px-8">61 <View className="flex-1 items-center justify-center px-8">
64 <View className="max-w-[320px] items-center gap-3">62 <View className="max-w-[320px] items-center gap-3">
65 {isRetrying ? (63 {isRetrying ? (
66 <ActivityIndicator size="small" color={iconColor} />64 <ActivityIndicator size="small" colorClassName={"accent-icon-muted"} />
67 ) : (65 ) : (
68 <SymbolView66 <SymbolView
69 name={props.connection.phase === "offline" ? "wifi.slash" : "bolt.horizontal.circle"}67 name={props.connection.phase === "offline" ? "wifi.slash" : "bolt.horizontal.circle"}
70 size={24}68 size={24}
71 tintColor={iconColor}69 tintColorClassName={"accent-icon-muted"}
72 type="monochrome"70 type="monochrome"
73 />71 />
74 )}72 )}
apps/mobile/src/features/connection/connectionTone.ts+12-12
...@@ -6,38 +6,38 @@ export function connectionTone(state: RemoteClientConnectionState): StatusTone {...@@ -6,38 +6,38 @@ export function connectionTone(state: RemoteClientConnectionState): StatusTone {
6 case "connected":6 case "connected":
7 return {7 return {
8 label: "Connected",8 label: "Connected",
9 pillClassName: "bg-emerald-500/12 dark:bg-emerald-500/16",9 pillClassName: "bg-adaptive-emerald-500-a12-a16",
10 textClassName: "text-emerald-700 dark:text-emerald-300",10 textClassName: "text-adaptive-emerald-700-300",
11 };11 };
12 case "reconnecting":12 case "reconnecting":
13 return {13 return {
14 label: "Reconnecting",14 label: "Reconnecting",
15 pillClassName: "bg-amber-500/12 dark:bg-amber-500/16",15 pillClassName: "bg-adaptive-amber-500-a12-a16",
16 textClassName: "text-amber-700 dark:text-amber-300",16 textClassName: "text-adaptive-amber-700-300",
17 };17 };
18 case "connecting":18 case "connecting":
19 return {19 return {
20 label: "Connecting",20 label: "Connecting",
21 pillClassName: "bg-sky-500/12 dark:bg-sky-500/16",21 pillClassName: "bg-adaptive-sky-500-a12-a16",
22 textClassName: "text-sky-700 dark:text-sky-300",22 textClassName: "text-adaptive-sky-700-300",
23 };23 };
24 case "error":24 case "error":
25 return {25 return {
26 label: "Connection failed",26 label: "Connection failed",
27 pillClassName: "bg-rose-500/12 dark:bg-rose-500/16",27 pillClassName: "bg-adaptive-rose-500-a12-a16",
28 textClassName: "text-rose-700 dark:text-rose-300",28 textClassName: "text-adaptive-rose-700-300",
29 };29 };
30 case "offline":30 case "offline":
31 return {31 return {
32 label: "Offline",32 label: "Offline",
33 pillClassName: "bg-rose-500/12 dark:bg-rose-500/16",33 pillClassName: "bg-adaptive-rose-500-a12-a16",
34 textClassName: "text-rose-700 dark:text-rose-300",34 textClassName: "text-adaptive-rose-700-300",
35 };35 };
36 case "available":36 case "available":
37 return {37 return {
38 label: "Available",38 label: "Available",
39 pillClassName: "bg-neutral-500/10 dark:bg-neutral-500/16",39 pillClassName: "bg-adaptive-neutral-500-a10-a16",
40 textClassName: "text-neutral-600 dark:text-neutral-300",40 textClassName: "text-adaptive-neutral-600-300",
41 };41 };
42 }42 }
43}43}
apps/mobile/src/features/files/FileMarkdownPreview.tsx+10-9
...@@ -13,7 +13,7 @@ import {...@@ -13,7 +13,7 @@ import {
13 resolveMarkdownFontSizes,13 resolveMarkdownFontSizes,
14 resolveNativeMarkdownTypography,14 resolveNativeMarkdownTypography,
15} from "../../lib/appearancePreferences";15} from "../../lib/appearancePreferences";
16import { useThemeColor } from "../../lib/useThemeColor";16import { useUniwindTheme } from "../../lib/useUniwindTheme";
17import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";17import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
18import {18import {
19 hasNativeSelectableMarkdownText,19 hasNativeSelectableMarkdownText,
...@@ -38,14 +38,15 @@ function useMarkdownPreviewStyles(): MarkdownPreviewStyles {...@@ -38,14 +38,15 @@ function useMarkdownPreviewStyles(): MarkdownPreviewStyles {
38 () => resolveNativeMarkdownTypography(appearance.baseFontSize),38 () => resolveNativeMarkdownTypography(appearance.baseFontSize),
39 [appearance.baseFontSize],39 [appearance.baseFontSize],
40 );40 );
41 const body = String(useThemeColor("--color-md-body"));41 const theme = useUniwindTheme();
42 const strong = String(useThemeColor("--color-md-strong"));42 const body = theme["--color-md-body"];
43 const link = String(useThemeColor("--color-md-link"));43 const strong = theme["--color-md-strong"];
44 const blockquoteBorder = String(useThemeColor("--color-md-blockquote-border"));44 const link = theme["--color-md-link"];
45 const blockquoteBackground = String(useThemeColor("--color-md-blockquote-bg"));45 const blockquoteBorder = theme["--color-md-blockquote-border"];
46 const codeBackground = String(useThemeColor("--color-md-code-bg"));46 const blockquoteBackground = theme["--color-md-blockquote-bg"];
47 const codeText = String(useThemeColor("--color-md-code-text"));47 const codeBackground = theme["--color-md-code-bg"];
48 const horizontalRule = String(useThemeColor("--color-md-hr"));48 const codeText = theme["--color-md-code-text"];
49 const horizontalRule = theme["--color-md-hr"];
49 const regularFontFamily = useFontFamily("regular");50 const regularFontFamily = useFontFamily("regular");
50 const mediumFontFamily = useFontFamily("medium");51 const mediumFontFamily = useFontFamily("medium");
51 const boldFontFamily = useFontFamily("bold");52 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";...@@ -7,7 +7,6 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
7import { AppText as Text } from "../../components/AppText";7import { AppText as Text } from "../../components/AppText";
8import { PierreEntryIcon } from "../../components/PierreEntryIcon";8import { PierreEntryIcon } from "../../components/PierreEntryIcon";
9import { cn } from "../../lib/cn";9import { cn } from "../../lib/cn";
10import { useThemeColor } from "../../lib/useThemeColor";
11import { IOS_NAV_BAR_HEIGHT } from "../../lib/layoutMetrics";10import { IOS_NAV_BAR_HEIGHT } from "../../lib/layoutMetrics";
12import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";11import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
13import {12import {
...@@ -46,7 +45,6 @@ const FileTreeRow = memo(function FileTreeRow(props: {...@@ -46,7 +45,6 @@ const FileTreeRow = memo(function FileTreeRow(props: {
46 readonly item: VisibleFileTreeNode;45 readonly item: VisibleFileTreeNode;
47 readonly selected: boolean;46 readonly selected: boolean;
48 readonly expanded: boolean;47 readonly expanded: boolean;
49 readonly iconColor: string;
50 readonly onPressDirectory: (path: string) => void;48 readonly onPressDirectory: (path: string) => void;
51 readonly onPreviewFile?: (path: string) => void;49 readonly onPreviewFile?: (path: string) => void;
52 readonly onPressFile: (path: string) => void;50 readonly onPressFile: (path: string) => void;
...@@ -79,7 +77,7 @@ const FileTreeRow = memo(function FileTreeRow(props: {...@@ -79,7 +77,7 @@ const FileTreeRow = memo(function FileTreeRow(props: {
79 <SymbolView77 <SymbolView
80 name={props.expanded ? "chevron.down" : "chevron.right"}78 name={props.expanded ? "chevron.down" : "chevron.right"}
81 size={12}79 size={12}
82 tintColor={props.iconColor}80 tintColorClassName="accent-icon-muted"
83 type="monochrome"81 type="monochrome"
84 />82 />
85 ) : (83 ) : (
...@@ -125,7 +123,6 @@ export function FileTreeBrowser(props: {...@@ -125,7 +123,6 @@ export function FileTreeBrowser(props: {
125 // Native transparent-header height ≈ safe-area top + nav bar (~44). Matches the123 // Native transparent-header height ≈ safe-area top + nav bar (~44). Matches the
126 // observed adjustedContentInset bottom (~102) seen in the native trace.124 // observed adjustedContentInset bottom (~102) seen in the native trace.
127 const headerInset = NATIVE_LIQUID_GLASS_SUPPORTED ? insets.top + IOS_NAV_BAR_HEIGHT : 0;125 const headerInset = NATIVE_LIQUID_GLASS_SUPPORTED ? insets.top + IOS_NAV_BAR_HEIGHT : 0;
128 const iconColor = String(useThemeColor("--color-icon-muted"));
129 const { onPreviewFile, onSelectFile, selectedPath: controlledSelectedPath } = props;126 const { onPreviewFile, onSelectFile, selectedPath: controlledSelectedPath } = props;
130 const controlledSelectedPathRef = useRef(controlledSelectedPath);127 const controlledSelectedPathRef = useRef(controlledSelectedPath);
131 const pendingSelectionTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);128 const pendingSelectionTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
...@@ -216,13 +213,12 @@ export function FileTreeBrowser(props: {...@@ -216,13 +213,12 @@ export function FileTreeBrowser(props: {
216 item={item}213 item={item}
217 selected={item.node.kind === "file" && item.node.path === selectedPath}214 selected={item.node.kind === "file" && item.node.path === selectedPath}
218 expanded={expandedPaths.has(item.node.path)}215 expanded={expandedPaths.has(item.node.path)}
219 iconColor={iconColor}
220 onPressDirectory={toggleDirectory}216 onPressDirectory={toggleDirectory}
221 onPreviewFile={onPreviewFile}217 onPreviewFile={onPreviewFile}
222 onPressFile={handleSelectFile}218 onPressFile={handleSelectFile}
223 />219 />
224 ),220 ),
225 [expandedPaths, handleSelectFile, iconColor, onPreviewFile, selectedPath, toggleDirectory],221 [expandedPaths, handleSelectFile, onPreviewFile, selectedPath, toggleDirectory],
226 );222 );
227223
228 if (props.error && props.entries.length === 0) {224 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";...@@ -17,6 +17,7 @@ import { cn } from "../../lib/cn";
17import type { ResolvedMobileCodeSurface } from "../../lib/appearancePreferences";17import type { ResolvedMobileCodeSurface } from "../../lib/appearancePreferences";
18import { useAppearanceCodeSurface } from "../settings/appearance/useAppearanceCodeSurface";18import { useAppearanceCodeSurface } from "../settings/appearance/useAppearanceCodeSurface";
19import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";19import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
20import { useUniwindTheme } from "../../lib/useUniwindTheme";
20import {21import {
21 buildNativeSourceTokens,22 buildNativeSourceTokens,
22 NATIVE_SOURCE_CONTENT_WIDTH,23 NATIVE_SOURCE_CONTENT_WIDTH,
...@@ -153,6 +154,7 @@ function NativeSourceFileSurface(...@@ -153,6 +154,7 @@ function NativeSourceFileSurface(
153 const { NativeView, onRefresh } = props;154 const { NativeView, onRefresh } = props;
154 const { codeSurface, codeWordBreak, nativeSourceStyle } = useAppearanceCodeSurface();155 const { codeSurface, codeWordBreak, nativeSourceStyle } = useAppearanceCodeSurface();
155 const { themeAppearance, themeId } = useAppearancePreferences();156 const { themeAppearance, themeId } = useAppearancePreferences();
157 const appTheme = useUniwindTheme();
156 const { width: viewportWidth } = useWindowDimensions();158 const { width: viewportWidth } = useWindowDimensions();
157 const { rowsJson, status, targetIndex, tokens } = useSourceFileModel(props);159 const { rowsJson, status, targetIndex, tokens } = useSourceFileModel(props);
158 const [isPullRefreshing, setIsPullRefreshing] = useState(false);160 const [isPullRefreshing, setIsPullRefreshing] = useState(false);
...@@ -173,8 +175,8 @@ function NativeSourceFileSurface(...@@ -173,8 +175,8 @@ function NativeSourceFileSurface(
173 [targetIndex],175 [targetIndex],
174 );176 );
175 const themeJson = useMemo(177 const themeJson = useMemo(
176 () => JSON.stringify(createNativeReviewDiffTheme(themeAppearance, themeId)),178 () => JSON.stringify(createNativeReviewDiffTheme(themeAppearance, themeId, appTheme)),
177 [themeAppearance, themeId],179 [appTheme, themeAppearance, themeId],
178 );180 );
179 const styleJson = useMemo(() => JSON.stringify(nativeSourceStyle), [nativeSourceStyle]);181 const styleJson = useMemo(() => JSON.stringify(nativeSourceStyle), [nativeSourceStyle]);
180 const contentWidth = codeWordBreak182 const contentWidth = codeWordBreak
apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx+14-9
...@@ -19,7 +19,7 @@ import { LoadingScreen } from "../../components/LoadingScreen";...@@ -19,7 +19,7 @@ import { LoadingScreen } from "../../components/LoadingScreen";
19import { resolveFileSelectionNavigationAction } from "../../lib/adaptive-navigation";19import { resolveFileSelectionNavigationAction } from "../../lib/adaptive-navigation";
20import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic";20import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic";
21import { tryOpenExternalUrl } from "../../lib/openExternalUrl";21import { tryOpenExternalUrl } from "../../lib/openExternalUrl";
22import { useThemeColor } from "../../lib/useThemeColor";22import { useUniwindTheme } from "../../lib/useUniwindTheme";
23import { useThreadSelection } from "../../state/use-thread-selection";23import { useThreadSelection } from "../../state/use-thread-selection";
24import { useSelectedThreadWorktree } from "../../state/use-selected-thread-worktree";24import { useSelectedThreadWorktree } from "../../state/use-selected-thread-worktree";
25import { useEnvironmentQuery } from "../../state/query";25import { useEnvironmentQuery } from "../../state/query";
...@@ -135,11 +135,11 @@ function FileContent(props: {...@@ -135,11 +135,11 @@ function FileContent(props: {
135 return (135 return (
136 <View className="flex-1 bg-sheet">136 <View className="flex-1 bg-sheet">
137 {props.truncated ? (137 {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">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-amber-700 dark:text-amber-300">139 <Text className="text-2xs font-t3-bold uppercase text-adaptive-amber-700-300">
140 Partial file140 Partial file
141 </Text>141 </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">
143 Preview limited to the first 1 MB of a truncated file.143 Preview limited to the first 1 MB of a truncated file.
144 </Text>144 </Text>
145 </View>145 </View>
...@@ -210,7 +210,7 @@ function FilesUnavailable() {...@@ -210,7 +210,7 @@ function FilesUnavailable() {
210}210}
211211
212function FilesToolbarBottomFade() {212function FilesToolbarBottomFade() {
213 const sheetColor = String(useThemeColor("--color-sheet"));213 const sheetColor = String(useUniwindTheme()["--color-sheet"]);
214214
215 if (process.env.EXPO_OS !== "ios") {215 if (process.env.EXPO_OS !== "ios") {
216 return null;216 return null;
...@@ -245,8 +245,8 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {...@@ -245,8 +245,8 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {
245 const [searchQuery, setSearchQuery] = useState("");245 const [searchQuery, setSearchQuery] = useState("");
246 const isAndroid = Platform.OS === "android";246 const isAndroid = Platform.OS === "android";
247 const { themeAppearance: highlightTheme } = useAppearancePreferences();247 const { themeAppearance: highlightTheme } = useAppearancePreferences();
248 const iconColor = String(useThemeColor("--color-icon-muted"));248 const theme = useUniwindTheme();
249 const sheetSurfaceColor = String(useThemeColor("--color-sheet-solid"));249 const sheetSurfaceColor = theme["--color-sheet-solid"];
250 const { cwd, environmentId, projectName, selectedThread, threadId } = useThreadFilesWorkspace(250 const { cwd, environmentId, projectName, selectedThread, threadId } = useThreadFilesWorkspace(
251 props.route.params,251 props.route.params,
252 );252 );
...@@ -413,7 +413,12 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {...@@ -413,7 +413,12 @@ export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) {
413 ]}413 ]}
414 />414 />
415 <View className="flex-row items-center gap-2 border-b border-border px-3 py-2">415 <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 />
417 <TextInput422 <TextInput
418 accessibilityLabel="Search files"423 accessibilityLabel="Search files"
419 autoCapitalize="none"424 autoCapitalize="none"
...@@ -467,7 +472,7 @@ export function ThreadFileScreen(props: ThreadFileRouteScreenProps) {...@@ -467,7 +472,7 @@ export function ThreadFileScreen(props: ThreadFileRouteScreenProps) {
467 useAdaptiveWorkspacePaneRole("inspector");472 useAdaptiveWorkspacePaneRole("inspector");
468 const navigation = useNavigation();473 const navigation = useNavigation();
469 const { fileInspector, panes, toggleAuxiliaryPane } = useAdaptiveWorkspaceLayout();474 const { fileInspector, panes, toggleAuxiliaryPane } = useAdaptiveWorkspaceLayout();
470 const iconColor = useThemeColor("--color-icon");475 const iconColor = useUniwindTheme()["--color-icon"];
471 const params = props.route.params;476 const params = props.route.params;
472 const relativePath = normalizeRoutePath(params.path);477 const relativePath = normalizeRoutePath(params.path);
473 const targetLine = normalizeRouteLine(firstRouteParam(params.line));478 const targetLine = normalizeRouteLine(firstRouteParam(params.line));
apps/mobile/src/features/files/thread-file-navigator-pane.tsx+16-6
...@@ -12,7 +12,7 @@ import {...@@ -12,7 +12,7 @@ import {
1212
13import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText";13import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText";
14import { nativeHeaderScrollEdgeEffects } from "../../native/StackHeader";14import { nativeHeaderScrollEdgeEffects } from "../../native/StackHeader";
15import { useThemeColor } from "../../lib/useThemeColor";15import { useUniwindTheme } from "../../lib/useUniwindTheme";
16import { projectEnvironment } from "../../state/projects";16import { projectEnvironment } from "../../state/projects";
17import { useEnvironmentQuery } from "../../state/query";17import { useEnvironmentQuery } from "../../state/query";
18import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";18import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
...@@ -29,9 +29,9 @@ export function ThreadFileNavigatorPane(props: {...@@ -29,9 +29,9 @@ export function ThreadFileNavigatorPane(props: {
29}) {29}) {
30 const [searchQuery, setSearchQuery] = useState("");30 const [searchQuery, setSearchQuery] = useState("");
31 const { themeAppearance: highlightTheme } = useAppearancePreferences();31 const { themeAppearance: highlightTheme } = useAppearancePreferences();
32 const iconColor = String(useThemeColor("--color-icon-muted"));32 const theme = useUniwindTheme();
33 const foregroundColor = String(useThemeColor("--color-foreground"));33 const foregroundColor = theme["--color-foreground"];
34 const sheetColor = String(useThemeColor("--color-sheet"));34 const sheetColor = theme["--color-sheet"];
35 const headerScrollEdgeEffects = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version);35 const headerScrollEdgeEffects = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version);
36 const entriesQuery = useEnvironmentQuery(36 const entriesQuery = useEnvironmentQuery(
37 projectEnvironment.listEntries({37 projectEnvironment.listEntries({
...@@ -152,11 +152,21 @@ export function ThreadFileNavigatorPane(props: {...@@ -152,11 +152,21 @@ export function ThreadFileNavigatorPane(props: {
152 className="h-8 w-8 items-center justify-center rounded-full active:bg-subtle"152 className="h-8 w-8 items-center justify-center rounded-full active:bg-subtle"
153 onPress={entriesQuery.refresh}153 onPress={entriesQuery.refresh}
154 >154 >
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 />
156 </Pressable>161 </Pressable>
157 </View>162 </View>
158 <View className="flex-row items-center gap-2 border-t border-border px-3 py-2">163 <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 />
160 <TextInput170 <TextInput
161 accessibilityLabel="Search files"171 accessibilityLabel="Search files"
162 autoCapitalize="none"172 autoCapitalize="none"
apps/mobile/src/features/home/AndroidHomeFab.tsx+1-4
...@@ -3,7 +3,6 @@ import { Platform, Pressable, View } from "react-native";...@@ -3,7 +3,6 @@ import { Platform, Pressable, View } from "react-native";
3import { useSafeAreaInsets } from "react-native-safe-area-context";3import { useSafeAreaInsets } from "react-native-safe-area-context";
44
5import { SymbolView } from "../../components/AppSymbol";5import { SymbolView } from "../../components/AppSymbol";
6import { useThemeColor } from "../../lib/useThemeColor";
76
8/**7/**
9 * Android-only wrapper that overlays a bottom-right new-task FAB on a thread8 * Android-only wrapper that overlays a bottom-right new-task FAB on a thread
...@@ -25,8 +24,6 @@ function AndroidHomeFab(props: {...@@ -25,8 +24,6 @@ function AndroidHomeFab(props: {
25 readonly children: ReactNode;24 readonly children: ReactNode;
26}) {25}) {
27 const insets = useSafeAreaInsets();26 const insets = useSafeAreaInsets();
28 const primaryForegroundColor = useThemeColor("--color-primary-foreground");
29
30 return (27 return (
31 <View className="flex-1">28 <View className="flex-1">
32 {props.children}29 {props.children}
...@@ -42,7 +39,7 @@ function AndroidHomeFab(props: {...@@ -42,7 +39,7 @@ function AndroidHomeFab(props: {
42 <SymbolView39 <SymbolView
43 name="square.and.pencil"40 name="square.and.pencil"
44 size={22}41 size={22}
45 tintColor={primaryForegroundColor}42 tintColorClassName={"accent-primary-foreground"}
46 type="monochrome"43 type="monochrome"
47 />44 />
48 </Pressable>45 </Pressable>
apps/mobile/src/features/home/HomeHeader.tsx+17-9
...@@ -12,7 +12,7 @@ import { SymbolView } from "../../components/AppSymbol";...@@ -12,7 +12,7 @@ import { SymbolView } from "../../components/AppSymbol";
12import { T3Wordmark } from "../../components/T3Wordmark";12import { T3Wordmark } from "../../components/T3Wordmark";
13import { HOME_HORIZONTAL_INSET } from "../../lib/layoutMetrics";13import { HOME_HORIZONTAL_INSET } from "../../lib/layoutMetrics";
14import { resolveMobileStageLabel } from "../../lib/mobileBranding";14import { resolveMobileStageLabel } from "../../lib/mobileBranding";
15import { useThemeColor } from "../../lib/useThemeColor";15import { useUniwindTheme } from "../../lib/useUniwindTheme";
16import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled";16import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled";
17import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands";17import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands";
18import { withNativeGlassHeaderItem } from "../layout/native-glass-header-items";18import { withNativeGlassHeaderItem } from "../layout/native-glass-header-items";
...@@ -67,8 +67,6 @@ function checkedMenuState(checked: boolean) {...@@ -67,8 +67,6 @@ function checkedMenuState(checked: boolean) {
6767
68function AndroidHomeHeader(props: HomeHeaderProps) {68function AndroidHomeHeader(props: HomeHeaderProps) {
69 const insets = useSafeAreaInsets();69 const insets = useSafeAreaInsets();
70 const iconColor = useThemeColor("--color-icon");
71 const mutedColor = useThemeColor("--color-foreground-muted");
72 const stageLabel = resolveMobileStageLabel(Constants.expoConfig?.extra?.appVariant);70 const stageLabel = resolveMobileStageLabel(Constants.expoConfig?.extra?.appVariant);
73 // Thread List v2 lays the list out in fixed creation order, so the71 // Thread List v2 lays the list out in fixed creation order, so the
74 // sort/group filter controls would be silently ignored — hide them and72 // sort/group filter controls would be silently ignored — hide them and
...@@ -218,7 +216,7 @@ function AndroidHomeHeader(props: HomeHeaderProps) {...@@ -218,7 +216,7 @@ function AndroidHomeHeader(props: HomeHeaderProps) {
218 brand={216 brand={
219 <View className="flex-row items-center gap-2">217 <View className="flex-row items-center gap-2">
220 {/* Mirrors the desktop SidebarBrand: T3 mark + muted "Code". */}218 {/* Mirrors the desktop SidebarBrand: T3 mark + muted "Code". */}
221 <T3Wordmark color={iconColor} height={15} />219 <T3Wordmark colorClassName="accent-icon" height={15} />
222 <RNText className="-ml-0.5 text-[21px] font-t3-medium tracking-[-0.5px] text-foreground-muted">220 <RNText className="-ml-0.5 text-[21px] font-t3-medium tracking-[-0.5px] text-foreground-muted">
223 Code221 Code
224 </RNText>222 </RNText>
...@@ -248,7 +246,7 @@ function AndroidHomeHeader(props: HomeHeaderProps) {...@@ -248,7 +246,7 @@ function AndroidHomeHeader(props: HomeHeaderProps) {
248 : "line.3.horizontal.decrease.circle"246 : "line.3.horizontal.decrease.circle"
249 }247 }
250 size={16}248 size={16}
251 tintColor={iconColor}249 tintColorClassName={"accent-icon"}
252 type="monochrome"250 type="monochrome"
253 />251 />
254 </Pressable>252 </Pressable>
...@@ -262,12 +260,22 @@ function AndroidHomeHeader(props: HomeHeaderProps) {...@@ -262,12 +260,22 @@ function AndroidHomeHeader(props: HomeHeaderProps) {
262 onPress={props.onOpenSettings}260 onPress={props.onOpenSettings}
263 className="size-11 items-center justify-center rounded-full bg-subtle"261 className="size-11 items-center justify-center rounded-full bg-subtle"
264 >262 >
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 />
266 </Pressable>269 </Pressable>
267 </View>270 </View>
268271
269 <View className="min-h-12 flex-row items-center gap-2.5 rounded-2xl border border-input-border bg-input px-3.5">272 <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 />
271 <TextInput279 <TextInput
272 accessibilityLabel="Search threads"280 accessibilityLabel="Search threads"
273 autoCapitalize="none"281 autoCapitalize="none"
...@@ -286,7 +294,7 @@ function AndroidHomeHeader(props: HomeHeaderProps) {...@@ -286,7 +294,7 @@ function AndroidHomeHeader(props: HomeHeaderProps) {
286 <SymbolView294 <SymbolView
287 name="xmark.circle.fill"295 name="xmark.circle.fill"
288 size={17}296 size={17}
289 tintColor={mutedColor}297 tintColorClassName={"accent-foreground-muted"}
290 type="monochrome"298 type="monochrome"
291 />299 />
292 </Pressable>300 </Pressable>
...@@ -300,7 +308,7 @@ function AndroidHomeHeader(props: HomeHeaderProps) {...@@ -300,7 +308,7 @@ function AndroidHomeHeader(props: HomeHeaderProps) {
300308
301function IosHomeHeader(props: HomeHeaderProps) {309function IosHomeHeader(props: HomeHeaderProps) {
302 const searchBarRef = useRef<SearchBarCommands>(null);310 const searchBarRef = useRef<SearchBarCommands>(null);
303 const iconColor = useThemeColor("--color-icon");311 const iconColor = useUniwindTheme()["--color-icon"];
304 // Thread List v2 lays the list out in fixed creation order, so the312 // Thread List v2 lays the list out in fixed creation order, so the
305 // sort/group filter controls would be silently ignored — hide them and313 // sort/group filter controls would be silently ignored — hide them and
306 // key the "customized" icon state off the environment filter alone.314 // 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";...@@ -23,7 +23,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
23import { ActivityIndicator, FlatList, Platform, Pressable, View } from "react-native";23import { ActivityIndicator, FlatList, Platform, Pressable, View } from "react-native";
24import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable";24import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable";
25import { useSafeAreaInsets } from "react-native-safe-area-context";25import { useSafeAreaInsets } from "react-native-safe-area-context";
26import { useThemeColor } from "../../lib/useThemeColor";
2726
28import { AppText as Text } from "../../components/AppText";27import { AppText as Text } from "../../components/AppText";
29import { EmptyState } from "../../components/EmptyState";28import { EmptyState } from "../../components/EmptyState";
...@@ -216,7 +215,6 @@ export function HomeScreen(props: HomeScreenProps) {...@@ -216,7 +215,6 @@ export function HomeScreen(props: HomeScreenProps) {
216 const openSwipeableRef = useRef<SwipeableMethods | null>(null);215 const openSwipeableRef = useRef<SwipeableMethods | null>(null);
217 const listRef = useRef<LegendListRef | null>(null);216 const listRef = useRef<LegendListRef | null>(null);
218 const insets = useSafeAreaInsets();217 const insets = useSafeAreaInsets();
219 const accentColor = useThemeColor("--color-icon-muted");
220 const iosBottomToolbarClearance =218 const iosBottomToolbarClearance =
221 Platform.OS === "ios" && !NATIVE_LIQUID_GLASS_SUPPORTED219 Platform.OS === "ios" && !NATIVE_LIQUID_GLASS_SUPPORTED
222 ? PRE_LIQUID_GLASS_BOTTOM_TOOLBAR_HEIGHT220 ? PRE_LIQUID_GLASS_BOTTOM_TOOLBAR_HEIGHT
...@@ -1088,7 +1086,7 @@ export function HomeScreen(props: HomeScreenProps) {...@@ -1088,7 +1086,7 @@ export function HomeScreen(props: HomeScreenProps) {
1088 />1086 />
1089 {emptyState.loading ? (1087 {emptyState.loading ? (
1090 <View className="mt-4 items-center">1088 <View className="mt-4 items-center">
1091 <ActivityIndicator color={accentColor} />1089 <ActivityIndicator colorClassName={"accent-icon-muted"} />
1092 </View>1090 </View>
1093 ) : null}1091 ) : null}
1094 </View>1092 </View>
apps/mobile/src/features/home/WorkspaceConnectionTitle.tsx+2-4
...@@ -8,7 +8,6 @@ import { ActivityIndicator, Animated, Platform, Pressable, View } from "react-na...@@ -8,7 +8,6 @@ import { ActivityIndicator, Animated, Platform, Pressable, View } from "react-na
8import { SymbolView } from "../../components/AppSymbol";8import { SymbolView } from "../../components/AppSymbol";
9import { AppText as Text } from "../../components/AppText";9import { AppText as Text } from "../../components/AppText";
10import { brandTitleOffset, CompactBrandTitle } from "../../components/CompactBrandTitle";10import { brandTitleOffset, CompactBrandTitle } from "../../components/CompactBrandTitle";
11import { useThemeColor } from "../../lib/useThemeColor";
12import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";11import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
13import { useWorkspaceState } from "../../state/workspace";12import { useWorkspaceState } from "../../state/workspace";
14import {13import {
...@@ -98,7 +97,6 @@ export function WorkspaceConnectionTitle(props: {...@@ -98,7 +97,6 @@ export function WorkspaceConnectionTitle(props: {
98 /** Horizontal correction so the status aligns with the brand in native title slots. */97 /** Horizontal correction so the status aligns with the brand in native title slots. */
99 readonly statusOffset?: number;98 readonly statusOffset?: number;
100}) {99}) {
101 const iconColor = String(useThemeColor("--color-icon-muted"));
102 const status = useDelayedConnectionStatus();100 const status = useDelayedConnectionStatus();
103 const size = props.size ?? "navbar";101 const size = props.size ?? "navbar";
104102
...@@ -125,12 +123,12 @@ export function WorkspaceConnectionTitle(props: {...@@ -125,12 +123,12 @@ export function WorkspaceConnectionTitle(props: {
125 style={{ marginLeft: props.statusOffset ?? 0 }}123 style={{ marginLeft: props.statusOffset ?? 0 }}
126 >124 >
127 {status.showsProgress ? (125 {status.showsProgress ? (
128 <ActivityIndicator color={iconColor} size="small" />126 <ActivityIndicator colorClassName={"accent-icon-muted"} size="small" />
129 ) : (127 ) : (
130 <SymbolView128 <SymbolView
131 name="wifi.slash"129 name="wifi.slash"
132 size={size === "pageTitle" ? 17 : 15}130 size={size === "pageTitle" ? 17 : 15}
133 tintColor={iconColor}131 tintColorClassName={"accent-icon-muted"}
134 type="monochrome"132 type="monochrome"
135 />133 />
136 )}134 )}
apps/mobile/src/features/layout/WorkspaceEmptyDetail.tsx+6-4
...@@ -2,15 +2,17 @@ import { SymbolView } from "../../components/AppSymbol";...@@ -2,15 +2,17 @@ import { SymbolView } from "../../components/AppSymbol";
2import { Pressable, View } from "react-native";2import { Pressable, View } from "react-native";
33
4import { AppText as Text } from "../../components/AppText";4import { AppText as Text } from "../../components/AppText";
5import { useThemeColor } from "../../lib/useThemeColor";
65
7export function WorkspaceEmptyDetail(props: { readonly onStartNewTask?: () => void }) {6export function WorkspaceEmptyDetail(props: { readonly onStartNewTask?: () => void }) {
8 const iconColor = useThemeColor("--color-icon-subtle");
9
10 return (7 return (
11 <View className="flex-1 items-center justify-center bg-screen px-10">8 <View className="flex-1 items-center justify-center bg-screen px-10">
12 <View className="max-w-[360px] items-center gap-3">9 <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 />
14 <Text className="text-center text-xl font-t3-bold">Select a thread</Text>16 <Text className="text-center text-xl font-t3-bold">Select a thread</Text>
15 <Text className="text-center text-base text-foreground-muted">17 <Text className="text-center text-base text-foreground-muted">
16 Choose a thread from the sidebar or start a new task.18 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";...@@ -2,7 +2,7 @@ import { useCallback, useMemo, useRef, useState } from "react";
2import { Pressable, StyleSheet, View, type AccessibilityActionEvent } from "react-native";2import { Pressable, StyleSheet, View, type AccessibilityActionEvent } from "react-native";
3import { Gesture, GestureDetector } from "react-native-gesture-handler";3import { Gesture, GestureDetector } from "react-native-gesture-handler";
4import { runOnJS } from "react-native-reanimated";4import { runOnJS } from "react-native-reanimated";
5import { useThemeColor } from "../../lib/useThemeColor";5import { cn } from "../../lib/cn";
66
7const ACCESSIBILITY_RESIZE_STEP = 24;7const ACCESSIBILITY_RESIZE_STEP = 24;
88
...@@ -22,8 +22,6 @@ export function WorkspacePaneDivider(props: WorkspacePaneDividerProps) {...@@ -22,8 +22,6 @@ export function WorkspacePaneDivider(props: WorkspacePaneDividerProps) {
22 latestProps.current = props;22 latestProps.current = props;
23 const [hovered, setHovered] = useState(false);23 const [hovered, setHovered] = useState(false);
24 const [dragging, setDragging] = useState(false);24 const [dragging, setDragging] = useState(false);
25 const dividerColor = useThemeColor("--color-border");
26 const activeDividerColor = useThemeColor("--color-primary");
27 const handleResizeStart = useCallback(() => {25 const handleResizeStart = useCallback(() => {
28 setDragging(true);26 setDragging(true);
29 latestProps.current.onResizeStart?.();27 latestProps.current.onResizeStart?.();
...@@ -81,11 +79,11 @@ export function WorkspacePaneDivider(props: WorkspacePaneDividerProps) {...@@ -81,11 +79,11 @@ export function WorkspacePaneDivider(props: WorkspacePaneDividerProps) {
81 onHoverOut={() => setHovered(false)}79 onHoverOut={() => setHovered(false)}
82 >80 >
83 <View81 <View
84 style={[82 className={cn(
85 styles.line,83 "h-full self-center bg-border opacity-70",
86 { backgroundColor: dividerColor },84 hovered || dragging ? "w-0.5 bg-primary opacity-100" : "w-px",
87 (hovered || dragging) && [styles.activeLine, { backgroundColor: activeDividerColor }],85 )}
88 ]}86 style={[styles.line, (hovered || dragging) && styles.activeLine]}
89 />87 />
90 </Pressable>88 </Pressable>
91 </GestureDetector>89 </GestureDetector>
...@@ -96,11 +94,9 @@ const styles = StyleSheet.create({...@@ -96,11 +94,9 @@ const styles = StyleSheet.create({
96 line: {94 line: {
97 alignSelf: "center",95 alignSelf: "center",
98 height: "100%",96 height: "100%",
99 opacity: 0.7,
100 width: StyleSheet.hairlineWidth,97 width: StyleSheet.hairlineWidth,
101 },98 },
102 activeLine: {99 activeLine: {
103 opacity: 1,
104 width: 2,100 width: 2,
105 },101 },
106});102});
apps/mobile/src/features/projects/AddProjectScreen.tsx+25-20
...@@ -51,7 +51,6 @@ import { sourceControlEnvironment } from "../../state/sourceControl";...@@ -51,7 +51,6 @@ import { sourceControlEnvironment } from "../../state/sourceControl";
51import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText";51import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText";
52import { ErrorBanner } from "../../components/ErrorBanner";52import { ErrorBanner } from "../../components/ErrorBanner";
53import { SourceControlIcon } from "../../components/SourceControlIcon";53import { SourceControlIcon } from "../../components/SourceControlIcon";
54import { useThemeColor } from "../../lib/useThemeColor";
55import { uuidv4 } from "../../lib/uuid";54import { uuidv4 } from "../../lib/uuid";
56import { useAtomCommand } from "../../state/use-atom-command";55import { useAtomCommand } from "../../state/use-atom-command";
57import { useAtomQueryRunner } from "../../state/use-atom-query-runner";56import { useAtomQueryRunner } from "../../state/use-atom-query-runner";
...@@ -159,8 +158,6 @@ function ListRow(props: {...@@ -159,8 +158,6 @@ function ListRow(props: {
159 readonly right?: ReactNode;158 readonly right?: ReactNode;
160 readonly onPress?: () => void;159 readonly onPress?: () => void;
161}) {160}) {
162 const chevronColor = useThemeColor("--color-chevron");
163
164 return (161 return (
165 <Pressable162 <Pressable
166 disabled={props.disabled}163 disabled={props.disabled}
...@@ -192,7 +189,12 @@ function ListRow(props: {...@@ -192,7 +189,12 @@ function ListRow(props: {
192 {"right" in props ? (189 {"right" in props ? (
193 props.right190 props.right
194 ) : !props.disabled ? (191 ) : !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 />
196 ) : null}198 ) : null}
197 </View>199 </View>
198 </Pressable>200 </Pressable>
...@@ -205,8 +207,6 @@ function PrimaryActionButton(props: {...@@ -205,8 +207,6 @@ function PrimaryActionButton(props: {
205 readonly loading?: boolean;207 readonly loading?: boolean;
206 readonly onPress: () => void;208 readonly onPress: () => void;
207}) {209}) {
208 const primaryForeground = useThemeColor("--color-primary-foreground");
209
210 return (210 return (
211 <Pressable211 <Pressable
212 disabled={props.disabled}212 disabled={props.disabled}
...@@ -214,7 +214,7 @@ function PrimaryActionButton(props: {...@@ -214,7 +214,7 @@ function PrimaryActionButton(props: {
214 className="h-12 items-center justify-center rounded-full bg-primary active:opacity-70 disabled:opacity-45"214 className="h-12 items-center justify-center rounded-full bg-primary active:opacity-70 disabled:opacity-45"
215 >215 >
216 {props.loading ? (216 {props.loading ? (
217 <ActivityIndicator color={String(primaryForeground)} />217 <ActivityIndicator colorClassName={String("accent-primary-foreground")} />
218 ) : (218 ) : (
219 <Text className="text-base font-t3-bold text-primary-foreground">{props.label}</Text>219 <Text className="text-base font-t3-bold text-primary-foreground">{props.label}</Text>
220 )}220 )}
...@@ -414,7 +414,6 @@ function SourceControlRow(props: {...@@ -414,7 +414,6 @@ function SourceControlRow(props: {
414 readonly isFirst: boolean;414 readonly isFirst: boolean;
415}) {415}) {
416 const navigation = useNavigation();416 const navigation = useNavigation();
417 const iconColor = useThemeColor("--color-icon");
418 const title =417 const title =
419 props.source === "url" ? "Git URL" : `${addProjectRemoteSourceLabel(props.source)} repository`;418 props.source === "url" ? "Git URL" : `${addProjectRemoteSourceLabel(props.source)} repository`;
420 const subtitle =419 const subtitle =
...@@ -423,9 +422,9 @@ function SourceControlRow(props: {...@@ -423,9 +422,9 @@ function SourceControlRow(props: {
423 : `Clone ${addProjectRemoteSourceLabel(props.source)} ${props.hint}`;422 : `Clone ${addProjectRemoteSourceLabel(props.source)} ${props.hint}`;
424 const icon =423 const icon =
425 props.source === "url" ? (424 props.source === "url" ? (
426 <SymbolView name="link" size={17} tintColor={iconColor} type="monochrome" />425 <SymbolView name="link" size={17} tintColorClassName={"accent-icon"} type="monochrome" />
427 ) : (426 ) : (
428 <SourceControlIcon kind={props.source} size={18} color={String(iconColor)} />427 <SourceControlIcon kind={props.source} size={18} colorClassName="accent-icon" />
429 );428 );
430429
431 if (!props.ready) {430 if (!props.ready) {
...@@ -454,8 +453,6 @@ function SourceControlRow(props: {...@@ -454,8 +453,6 @@ function SourceControlRow(props: {
454453
455export function AddProjectSourceScreen() {454export function AddProjectSourceScreen() {
456 const navigation = useNavigation();455 const navigation = useNavigation();
457 const accentColor = useThemeColor("--color-icon-muted");
458 const iconColor = useThemeColor("--color-icon");
459 const { environmentOptions, selectedEnvironment, setSelectedEnvironmentId } =456 const { environmentOptions, selectedEnvironment, setSelectedEnvironmentId } =
460 useSelectedEnvironment();457 useSelectedEnvironment();
461 const discoveryState = useEnvironmentQuery(458 const discoveryState = useEnvironmentQuery(
...@@ -496,7 +493,7 @@ export function AddProjectSourceScreen() {...@@ -496,7 +493,7 @@ export function AddProjectSourceScreen() {
496 <SymbolView493 <SymbolView
497 name="server.rack"494 name="server.rack"
498 size={17}495 size={17}
499 tintColor={iconColor}496 tintColorClassName={"accent-icon"}
500 type="monochrome"497 type="monochrome"
501 />498 />
502 }499 }
...@@ -508,7 +505,7 @@ export function AddProjectSourceScreen() {...@@ -508,7 +505,7 @@ export function AddProjectSourceScreen() {
508 <SymbolView505 <SymbolView
509 name="checkmark"506 name="checkmark"
510 size={14}507 size={14}
511 tintColor={iconColor}508 tintColorClassName={"accent-icon"}
512 type="monochrome"509 type="monochrome"
513 />510 />
514 ) : null511 ) : null
...@@ -530,7 +527,7 @@ export function AddProjectSourceScreen() {...@@ -530,7 +527,7 @@ export function AddProjectSourceScreen() {
530 <SymbolView527 <SymbolView
531 name="folder.badge.plus"528 name="folder.badge.plus"
532 size={17}529 size={17}
533 tintColor={iconColor}530 tintColorClassName={"accent-icon"}
534 type="monochrome"531 type="monochrome"
535 />532 />
536 }533 }
...@@ -560,7 +557,9 @@ export function AddProjectSourceScreen() {...@@ -560,7 +557,9 @@ export function AddProjectSourceScreen() {
560 ),557 ),
561 )}558 )}
562 </ListSection>559 </ListSection>
563 {discoveryState.isPending ? <ActivityIndicator color={accentColor} /> : null}560 {discoveryState.isPending ? (
561 <ActivityIndicator colorClassName={"accent-icon-muted"} />
562 ) : null}
564 </>563 </>
565 ) : null}564 ) : null}
566 </AddProjectShell>565 </AddProjectShell>
...@@ -745,7 +744,6 @@ function FolderBrowser(props: {...@@ -745,7 +744,6 @@ function FolderBrowser(props: {
745 }) => Promise<boolean>;744 }) => Promise<boolean>;
746 readonly pinnedDirectoryName?: string;745 readonly pinnedDirectoryName?: string;
747}) {746}) {
748 const accentColor = useThemeColor("--color-icon-muted");
749 const browsePath = useMemo(747 const browsePath = useMemo(
750 () => getFilesystemBrowsePath(props.pathInput, props.environment.platform),748 () => getFilesystemBrowsePath(props.pathInput, props.environment.platform),
751 [props.environment.platform, props.pathInput],749 [props.environment.platform, props.pathInput],
...@@ -781,7 +779,7 @@ function FolderBrowser(props: {...@@ -781,7 +779,7 @@ function FolderBrowser(props: {
781 <ListSection>779 <ListSection>
782 {browseState.isPending && browseState.data === null ? (780 {browseState.isPending && browseState.data === null ? (
783 <View className="items-center py-5">781 <View className="items-center py-5">
784 <ActivityIndicator color={accentColor} />782 <ActivityIndicator colorClassName={"accent-icon-muted"} />
785 </View>783 </View>
786 ) : null}784 ) : null}
787 {browsePath.canBrowseUp ? (785 {browsePath.canBrowseUp ? (
...@@ -791,7 +789,7 @@ function FolderBrowser(props: {...@@ -791,7 +789,7 @@ function FolderBrowser(props: {
791 <SymbolView789 <SymbolView
792 name="arrow.turn.left.up"790 name="arrow.turn.left.up"
793 size={17}791 size={17}
794 tintColor={accentColor}792 tintColorClassName={"accent-icon-muted"}
795 type="monochrome"793 type="monochrome"
796 />794 />
797 }795 }
...@@ -810,7 +808,14 @@ function FolderBrowser(props: {...@@ -810,7 +808,14 @@ function FolderBrowser(props: {
810 <ListRow808 <ListRow
811 key={entry.fullPath}809 key={entry.fullPath}
812 title={entry.name}810 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 }
814 isFirst={index === 0 && !browsePath.canBrowseUp}819 isFirst={index === 0 && !browsePath.canBrowseUp}
815 right={null}820 right={null}
816 onPress={() => {821 onPress={() => {
apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx+6-3
...@@ -14,7 +14,6 @@ import { ControlPill } from "../../components/ControlPill";...@@ -14,7 +14,6 @@ import { ControlPill } from "../../components/ControlPill";
14import { cn } from "../../lib/cn";14import { cn } from "../../lib/cn";
15import type { DraftComposerImageAttachment } from "../../lib/composerImages";15import type { DraftComposerImageAttachment } from "../../lib/composerImages";
16import { convertPastedImagesToAttachments, pickComposerImages } from "../../lib/composerImages";16import { convertPastedImagesToAttachments, pickComposerImages } from "../../lib/composerImages";
17import { useThemeColor } from "../../lib/useThemeColor";
18import { useNativePaste } from "../../lib/useNativePaste";17import { useNativePaste } from "../../lib/useNativePaste";
19import { setPendingConnectionError } from "../../state/use-remote-environment-registry";18import { setPendingConnectionError } from "../../state/use-remote-environment-registry";
20import { appendReviewCommentToDraft } from "../../state/use-thread-composer-state";19import { appendReviewCommentToDraft } from "../../state/use-thread-composer-state";
...@@ -46,7 +45,6 @@ export function ReviewCommentComposerSheet(props: ReviewCommentComposerSheetProp...@@ -46,7 +45,6 @@ export function ReviewCommentComposerSheet(props: ReviewCommentComposerSheetProp
46 const insets = useSafeAreaInsets();45 const insets = useSafeAreaInsets();
47 const { width } = useWindowDimensions();46 const { width } = useWindowDimensions();
48 const { themeAppearance: selectedTheme } = useAppearancePreferences();47 const { themeAppearance: selectedTheme } = useAppearancePreferences();
49 const iconTint = String(useThemeColor("--color-icon"));
50 const target = useReviewCommentTarget();48 const target = useReviewCommentTarget();
51 const { codeSurface } = useAppearanceCodeSurface();49 const { codeSurface } = useAppearanceCodeSurface();
52 const { environmentId, threadId } = props.route.params;50 const { environmentId, threadId } = props.route.params;
...@@ -168,7 +166,12 @@ export function ReviewCommentComposerSheet(props: ReviewCommentComposerSheetProp...@@ -168,7 +166,12 @@ export function ReviewCommentComposerSheet(props: ReviewCommentComposerSheetProp
168 className="bg-subtle h-12 w-12 items-center justify-center rounded-full"166 className="bg-subtle h-12 w-12 items-center justify-center rounded-full"
169 onPress={dismissComposer}167 onPress={dismissComposer}
170 >168 >
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 />
172 </Pressable>175 </Pressable>
173176
174 <Text className="text-lg font-t3-bold text-foreground">Add Comment</Text>177 <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";...@@ -37,7 +37,7 @@ import { ControlPillMenu } from "../../components/ControlPill";
37import { environmentCatalog } from "../../connection/catalog";37import { environmentCatalog } from "../../connection/catalog";
38import { useEnvironmentPresentation } from "../../state/presentation";38import { useEnvironmentPresentation } from "../../state/presentation";
39import { useAtomCommand } from "../../state/use-atom-command";39import { useAtomCommand } from "../../state/use-atom-command";
40import { useThemeColor } from "../../lib/useThemeColor";40import { useUniwindTheme } from "../../lib/useUniwindTheme";
41import { IOS_NAV_BAR_HEIGHT } from "../../lib/layoutMetrics";41import { IOS_NAV_BAR_HEIGHT } from "../../lib/layoutMetrics";
42import { useThreadDraftForThread } from "../../state/use-thread-composer-state";42import { useThreadDraftForThread } from "../../state/use-thread-composer-state";
43import { EnvironmentConnectionNotice } from "../connection/EnvironmentConnectionNotice";43import { EnvironmentConnectionNotice } from "../connection/EnvironmentConnectionNotice";
...@@ -80,13 +80,11 @@ const SHOWCASE_ENABLED = process.env.EXPO_PUBLIC_SHOWCASE === "1";...@@ -80,13 +80,11 @@ const SHOWCASE_ENABLED = process.env.EXPO_PUBLIC_SHOWCASE === "1";
8080
81const ReviewNotice = memo(function ReviewNotice(props: { readonly notice: string }) {81const ReviewNotice = memo(function ReviewNotice(props: { readonly notice: string }) {
82 return (82 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">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-amber-700 dark:text-amber-300">84 <Text className="text-xs font-t3-bold uppercase text-adaptive-amber-700-300">
85 Partial diff85 Partial diff
86 </Text>86 </Text>
87 <Text className="text-xs leading-normal text-amber-800 dark:text-amber-200">87 <Text className="text-xs leading-normal text-adaptive-amber-800-200">{props.notice}</Text>
88 {props.notice}
89 </Text>
90 </View>88 </View>
91 );89 );
92});90});
...@@ -97,7 +95,6 @@ function ReviewSelectionActionBar(props: {...@@ -97,7 +95,6 @@ function ReviewSelectionActionBar(props: {
97 readonly onOpenComment: (() => void) | null;95 readonly onOpenComment: (() => void) | null;
98 readonly onClear: () => void;96 readonly onClear: () => void;
99}) {97}) {
100 const foreground = useThemeColor("--color-primary-foreground");
101 if (!props.title) {98 if (!props.title) {
102 return null;99 return null;
103 }100 }
...@@ -107,7 +104,7 @@ function ReviewSelectionActionBar(props: {...@@ -107,7 +104,7 @@ function ReviewSelectionActionBar(props: {
107 <SymbolView104 <SymbolView
108 name={props.onOpenComment ? "text.bubble" : "line.3.horizontal.decrease.circle"}105 name={props.onOpenComment ? "text.bubble" : "line.3.horizontal.decrease.circle"}
109 size={16}106 size={16}
110 tintColor={foreground}107 tintColorClassName={"accent-primary-foreground"}
111 type="monochrome"108 type="monochrome"
112 />109 />
113 <Text className="text-base font-t3-bold text-primary-foreground">{props.title}</Text>110 <Text className="text-base font-t3-bold text-primary-foreground">{props.title}</Text>
...@@ -144,7 +141,12 @@ function ReviewSelectionActionBar(props: {...@@ -144,7 +141,12 @@ function ReviewSelectionActionBar(props: {
144 className="h-12 w-12 items-center justify-center rounded-full bg-primary"141 className="h-12 w-12 items-center justify-center rounded-full bg-primary"
145 onPress={props.onClear}142 onPress={props.onClear}
146 >143 >
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 />
148 </Pressable>150 </Pressable>
149 </View>151 </View>
150 );152 );
...@@ -217,8 +219,9 @@ function ReviewFileNavigator({...@@ -217,8 +219,9 @@ function ReviewFileNavigator({
217 ref,219 ref,
218}: ReviewFileNavigatorProps) {220}: ReviewFileNavigatorProps) {
219 const insets = useSafeAreaInsets();221 const insets = useSafeAreaInsets();
220 const sheetColor = String(useThemeColor("--color-sheet"));222 const theme = useUniwindTheme();
221 const foregroundColor = String(useThemeColor("--color-foreground"));223 const sheetColor = theme["--color-sheet"];
224 const foregroundColor = theme["--color-foreground"];
222 const headerScrollEdgeEffects = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version);225 const headerScrollEdgeEffects = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version);
223 const [fileSelection, setFileSelection] = useState<{226 const [fileSelection, setFileSelection] = useState<{
224 readonly sectionId: string | null;227 readonly sectionId: string | null;
...@@ -348,7 +351,7 @@ export function ReviewSheet(props: ReviewSheetProps) {...@@ -348,7 +351,7 @@ export function ReviewSheet(props: ReviewSheetProps) {
348 const navigation = useNavigation();351 const navigation = useNavigation();
349 const insets = useSafeAreaInsets();352 const insets = useSafeAreaInsets();
350 const { themeAppearance: selectedTheme } = useAppearancePreferences();353 const { themeAppearance: selectedTheme } = useAppearancePreferences();
351 const headerIcon = String(useThemeColor("--color-icon"));354 const headerIcon = String(useUniwindTheme()["--color-icon"]);
352 const { environmentId, threadId } = props.route.params;355 const { environmentId, threadId } = props.route.params;
353 const environment = useEnvironmentPresentation(environmentId);356 const environment = useEnvironmentPresentation(environmentId);
354 const retryEnvironment = useAtomCommand(environmentCatalog.retryNow, "environment retry");357 const retryEnvironment = useAtomCommand(environmentCatalog.retryNow, "environment retry");
apps/mobile/src/features/review/nativeReviewDiffAdapter.test.ts+21-4
...@@ -1,5 +1,12 @@...@@ -1,5 +1,12 @@
1import { describe, expect, it } from "vite-plus/test";1import { 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
4import {11import {
5 createNativeReviewDiffTheme,12 createNativeReviewDiffTheme,
...@@ -39,6 +46,12 @@ function buildInput(comments: BuildNativeReviewDiffDataInput["comments"]) {...@@ -39,6 +46,12 @@ function buildInput(comments: BuildNativeReviewDiffDataInput["comments"]) {
39 return { parsedDiff, comments } satisfies BuildNativeReviewDiffDataInput;46 return { parsedDiff, comments } satisfies BuildNativeReviewDiffDataInput;
40}47}
4148
49function appTheme(themeId: MobileThemeId, appearance: MobileThemeAppearance) {
50 return themeId === DEFAULT_MOBILE_THEME_ID
51 ? readDefaultMobileThemeVariables(appearance)
52 : getMobileThemeVariables(themeId, appearance);
53}
54
42describe("getCachedNativeReviewDiffData", () => {55describe("getCachedNativeReviewDiffData", () => {
43 it("reuses the row model for equivalent empty comment arrays", () => {56 it("reuses the row model for equivalent empty comment arrays", () => {
44 const first = getCachedNativeReviewDiffData(buildInput([]));57 const first = getCachedNativeReviewDiffData(buildInput([]));
...@@ -61,7 +74,11 @@ describe("createNativeReviewDiffTheme", () => {...@@ -61,7 +74,11 @@ describe("createNativeReviewDiffTheme", () => {
61 it("serializes every native color as cross-platform opaque hex", () => {74 it("serializes every native color as cross-platform opaque hex", () => {
62 for (const themeId of MOBILE_THEME_IDS) {75 for (const themeId of MOBILE_THEME_IDS) {
63 for (const appearance of ["light", "dark"] as const) {76 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 );
65 for (const color of Object.values(theme)) {82 for (const color of Object.values(theme)) {
66 expect(color, `${themeId}/${appearance}`).toMatch(/^#[\da-f]{6}$/i);83 expect(color, `${themeId}/${appearance}`).toMatch(/^#[\da-f]{6}$/i);
67 }84 }
...@@ -70,8 +87,8 @@ describe("createNativeReviewDiffTheme", () => {...@@ -70,8 +87,8 @@ describe("createNativeReviewDiffTheme", () => {
70 });87 });
7188
72 it("uses the selected app palette for native code surfaces", () => {89 it("uses the selected app palette for native code surfaces", () => {
73 const standard = createNativeReviewDiffTheme("dark", "t3-code");90 const standard = createNativeReviewDiffTheme("dark", "t3-code", appTheme("t3-code", "dark"));
74 const iris = createNativeReviewDiffTheme("dark", "iris");91 const iris = createNativeReviewDiffTheme("dark", "iris", appTheme("iris", "dark"));
7592
76 expect(iris.background).not.toBe(standard.background);93 expect(iris.background).not.toBe(standard.background);
77 expect(iris.hunkText).not.toBe(standard.hunkText);94 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";...@@ -8,11 +8,7 @@ import { pipe } from "effect/Function";
8import type { ResolvedMobileCodeSurface } from "../../lib/appearancePreferences";8import type { ResolvedMobileCodeSurface } from "../../lib/appearancePreferences";
9import { resolveMobileCodeSurface } from "../../lib/appearancePreferences";9import { resolveMobileCodeSurface } from "../../lib/appearancePreferences";
10import { MOBILE_CODE_SURFACE } from "../../lib/typography";10import { MOBILE_CODE_SURFACE } from "../../lib/typography";
11import {11import { type MobileThemeId, type MobileThemeVariables } from "../../lib/mobileTheme";
12 DEFAULT_MOBILE_THEME_ID,
13 getMobileThemeVariables,
14 type MobileThemeId,
15} from "../../lib/mobileTheme";
16import { getMobileTerminalTheme, type TerminalAppearanceScheme } from "../terminal/terminalTheme";12import { getMobileTerminalTheme, type TerminalAppearanceScheme } from "../terminal/terminalTheme";
17import { computeWordAltDiffRanges } from "./reviewWordDiffs";13import { computeWordAltDiffRanges } from "./reviewWordDiffs";
18import {14import {
...@@ -137,10 +133,10 @@ function buildReviewCommentsCacheKey(comments: ReadonlyArray<ReviewInlineComment...@@ -137,10 +133,10 @@ function buildReviewCommentsCacheKey(comments: ReadonlyArray<ReviewInlineComment
137133
138export function createNativeReviewDiffTheme(134export function createNativeReviewDiffTheme(
139 scheme: TerminalAppearanceScheme,135 scheme: TerminalAppearanceScheme,
140 themeId: MobileThemeId = DEFAULT_MOBILE_THEME_ID,136 themeId: MobileThemeId,
137 appTheme: MobileThemeVariables,
141): NativeReviewDiffTheme {138): NativeReviewDiffTheme {
142 const terminalTheme = getMobileTerminalTheme(themeId, scheme);139 const terminalTheme = getMobileTerminalTheme(themeId, scheme);
143 const appTheme = getMobileThemeVariables(themeId, scheme);
144 const [, terminalRed] = terminalTheme.palette;140 const [, terminalRed] = terminalTheme.palette;
145 // Swift expects #RRGGBB/#RRGGBBAA while Android expects #RRGGBB/#AARRGGBB.141 // Swift expects #RRGGBB/#RRGGBBAA while Android expects #RRGGBB/#AARRGGBB.
146 // Flatten translucent app tokens onto the code surface so both native142 // 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...@@ -6,6 +6,7 @@ import { useAppearanceCodeSurface } from "../settings/appearance/useAppearanceCo
6import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";6import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
7import { useNativeReviewDiffHighlighting } from "./useNativeReviewDiffHighlighting";7import { useNativeReviewDiffHighlighting } from "./useNativeReviewDiffHighlighting";
8import { buildNativeReviewTokensResetKey } from "./reviewDiffBridgeKeys";8import { buildNativeReviewTokensResetKey } from "./reviewDiffBridgeKeys";
9import { useUniwindTheme } from "../../lib/useUniwindTheme";
910
10export { buildNativeReviewTokensResetKey, hashReviewDiffKey } from "./reviewDiffBridgeKeys";11export { buildNativeReviewTokensResetKey, hashReviewDiffKey } from "./reviewDiffBridgeKeys";
1112
...@@ -31,11 +32,15 @@ export function useNativeReviewDiffBridge(input: {...@@ -31,11 +32,15 @@ export function useNativeReviewDiffBridge(input: {
31 } = input;32 } = input;
32 const { nativeReviewDiffStyle } = useAppearanceCodeSurface();33 const { nativeReviewDiffStyle } = useAppearanceCodeSurface();
33 const { themeAppearance: scheme, themeId } = useAppearancePreferences();34 const { themeAppearance: scheme, themeId } = useAppearancePreferences();
35 const appTheme = useUniwindTheme();
34 const [collapsedCommentIds, setCollapsedCommentIds] = useState<ReadonlySet<string>>(36 const [collapsedCommentIds, setCollapsedCommentIds] = useState<ReadonlySet<string>>(
35 () => new Set(),37 () => new Set(),
36 );38 );
3739
38 const theme = useMemo(() => createNativeReviewDiffTheme(scheme, themeId), [scheme, themeId]);40 const theme = useMemo(
41 () => createNativeReviewDiffTheme(scheme, themeId, appTheme),
42 [appTheme, scheme, themeId],
43 );
39 const rowsJson = useMemo(() => JSON.stringify(data.rows), [data.rows]);44 const rowsJson = useMemo(() => JSON.stringify(data.rows), [data.rows]);
40 const collapsedFileIdsJson = useMemo(() => JSON.stringify(collapsedFileIds), [collapsedFileIds]);45 const collapsedFileIdsJson = useMemo(() => JSON.stringify(collapsedFileIds), [collapsedFileIds]);
41 const viewedFileIdsJson = useMemo(() => JSON.stringify(viewedFileIds), [viewedFileIds]);46 const viewedFileIdsJson = useMemo(() => JSON.stringify(viewedFileIds), [viewedFileIds]);
apps/mobile/src/features/settings/SettingsClientStorageRouteScreen.tsx+8-10
...@@ -1,12 +1,11 @@...@@ -1,12 +1,11 @@
1import { useAtomSet, useAtomValue } from "@effect/atom-react";1import { useAtomSet, useAtomValue } from "@effect/atom-react";
2import { AsyncResult } from "effect/unstable/reactivity";2import { AsyncResult } from "effect/unstable/reactivity";
3import { SymbolView } from "expo-symbols";
4import { useMemo } from "react";3import { useMemo } from "react";
5import { ActivityIndicator, Alert, Pressable, ScrollView, View } from "react-native";4import { ActivityIndicator, Alert, Pressable, ScrollView, View } from "react-native";
6import { useSafeAreaInsets } from "react-native-safe-area-context";5import { useSafeAreaInsets } from "react-native-safe-area-context";
76
8import { AppText as Text } from "../../components/AppText";7import { AppText as Text } from "../../components/AppText";
9import { useThemeColor } from "../../lib/useThemeColor";8import { SymbolView } from "../../components/AppSymbol";
10import {9import {
11 clearClientCacheAtom,10 clearClientCacheAtom,
12 clientCacheSummaryAtom,11 clientCacheSummaryAtom,
...@@ -17,8 +16,6 @@ import { SettingsSection } from "./components/SettingsSection";...@@ -17,8 +16,6 @@ import { SettingsSection } from "./components/SettingsSection";
1716
18export function SettingsClientStorageRouteScreen() {17export function SettingsClientStorageRouteScreen() {
19 const insets = useSafeAreaInsets();18 const insets = useSafeAreaInsets();
20 const iconColor = useThemeColor("--color-icon");
21 const dangerForegroundColor = useThemeColor("--color-danger-foreground");
22 const summaryResult = useAtomValue(clientCacheSummaryAtom);19 const summaryResult = useAtomValue(clientCacheSummaryAtom);
23 const clearResult = useAtomValue(clearClientCacheAtom);20 const clearResult = useAtomValue(clearClientCacheAtom);
24 const clearCache = useAtomSet(clearClientCacheAtom);21 const clearCache = useAtomSet(clearClientCacheAtom);
...@@ -84,7 +81,7 @@ export function SettingsClientStorageRouteScreen() {...@@ -84,7 +81,7 @@ export function SettingsClientStorageRouteScreen() {
84 <SymbolView81 <SymbolView
85 name="exclamationmark.triangle"82 name="exclamationmark.triangle"
86 size={28}83 size={28}
87 tintColor={dangerForegroundColor}84 tintColorClassName={"accent-danger-foreground"}
88 type="monochrome"85 type="monochrome"
89 weight="regular"86 weight="regular"
90 />87 />
...@@ -119,7 +116,7 @@ export function SettingsClientStorageRouteScreen() {...@@ -119,7 +116,7 @@ export function SettingsClientStorageRouteScreen() {
119 <SymbolView116 <SymbolView
120 name="checkmark.circle"117 name="checkmark.circle"
121 size={28}118 size={28}
122 tintColor={iconColor}119 tintColorClassName={"accent-icon"}
123 type="monochrome"120 type="monochrome"
124 weight="regular"121 weight="regular"
125 />122 />
...@@ -142,14 +139,16 @@ export function SettingsClientStorageRouteScreen() {...@@ -142,14 +139,16 @@ export function SettingsClientStorageRouteScreen() {
142 <SymbolView139 <SymbolView
143 name="trash"140 name="trash"
144 size={22}141 size={22}
145 tintColor={dangerForegroundColor}142 tintColorClassName={"accent-danger-foreground"}
146 type="monochrome"143 type="monochrome"
147 weight="regular"144 weight="regular"
148 />145 />
149 <Text className="flex-1 text-lg tabular-nums text-danger-foreground">146 <Text className="flex-1 text-lg tabular-nums text-danger-foreground">
150 {summary ? `Clear ${formatBytes(summary.payloadBytes)}` : "Clear caches"}147 {summary ? `Clear ${formatBytes(summary.payloadBytes)}` : "Clear caches"}
151 </Text>148 </Text>
152 {isClearing ? <ActivityIndicator color={dangerForegroundColor} /> : null}149 {isClearing ? (
150 <ActivityIndicator colorClassName={"accent-danger-foreground"} />
151 ) : null}
153 </Pressable>152 </Pressable>
154 </SettingsSection>153 </SettingsSection>
155 <Text className="px-2 text-sm leading-normal text-foreground-muted">154 <Text className="px-2 text-sm leading-normal text-foreground-muted">
...@@ -174,7 +173,6 @@ function CacheEnvironmentRow(props: {...@@ -174,7 +173,6 @@ function CacheEnvironmentRow(props: {
174 readonly first: boolean;173 readonly first: boolean;
175 readonly onClear: () => void;174 readonly onClear: () => void;
176}) {175}) {
177 const iconColor = useThemeColor("--color-icon");
178 return (176 return (
179 <View177 <View
180 className={178 className={
...@@ -186,7 +184,7 @@ function CacheEnvironmentRow(props: {...@@ -186,7 +184,7 @@ function CacheEnvironmentRow(props: {
186 <SymbolView184 <SymbolView
187 name="desktopcomputer"185 name="desktopcomputer"
188 size={22}186 size={22}
189 tintColor={iconColor}187 tintColorClassName={"accent-icon"}
190 type="monochrome"188 type="monochrome"
191 weight="regular"189 weight="regular"
192 />190 />
apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx+3-4
...@@ -12,7 +12,7 @@ import { CloudEnvironmentRows } from "../connection/CloudEnvironmentRows";...@@ -12,7 +12,7 @@ import { CloudEnvironmentRows } from "../connection/CloudEnvironmentRows";
12import { ConnectionEnvironmentRow } from "../connection/ConnectionEnvironmentRow";12import { ConnectionEnvironmentRow } from "../connection/ConnectionEnvironmentRow";
13import { splitEnvironmentSections } from "../connection/environmentSections";13import { splitEnvironmentSections } from "../connection/environmentSections";
14import { cn } from "../../lib/cn";14import { cn } from "../../lib/cn";
15import { useThemeColor } from "../../lib/useThemeColor";15import { useUniwindTheme } from "../../lib/useUniwindTheme";
16import { useRemoteConnections } from "../../state/use-remote-environment-registry";16import { useRemoteConnections } from "../../state/use-remote-environment-registry";
17import {17import {
18 applyShowcaseLocalEnvironmentDisplayUrls,18 applyShowcaseLocalEnvironmentDisplayUrls,
...@@ -44,8 +44,7 @@ export function SettingsEnvironmentsRouteScreen() {...@@ -44,8 +44,7 @@ export function SettingsEnvironmentsRouteScreen() {
44 : environmentSections.connectedCloudEnvironments;44 : environmentSections.connectedCloudEnvironments;
45 const hasLocalEnvironments = localEnvironments.length > 0;45 const hasLocalEnvironments = localEnvironments.length > 0;
46 const [expandedId, setExpandedId] = useState<EnvironmentId | null>(null);46 const [expandedId, setExpandedId] = useState<EnvironmentId | null>(null);
47 const accentColor = useThemeColor("--color-icon-muted");47 const headerIconColor = useUniwindTheme()["--color-icon"];
48 const headerIconColor = useThemeColor("--color-icon");
4948
50 const handleToggle = useCallback((environmentId: EnvironmentId) => {49 const handleToggle = useCallback((environmentId: EnvironmentId) => {
51 setExpandedId((prev) => (prev === environmentId ? null : environmentId));50 setExpandedId((prev) => (prev === environmentId ? null : environmentId));
...@@ -148,7 +147,7 @@ export function SettingsEnvironmentsRouteScreen() {...@@ -148,7 +147,7 @@ export function SettingsEnvironmentsRouteScreen() {
148 <SymbolView147 <SymbolView
149 name="point.3.connected.trianglepath.dotted"148 name="point.3.connected.trianglepath.dotted"
150 size={20}149 size={20}
151 tintColor={accentColor}150 tintColorClassName={"accent-icon-muted"}
152 type="monochrome"151 type="monochrome"
153 />152 />
154 </View>153 </View>
apps/mobile/src/features/settings/SettingsProjectGroupingRouteScreen.tsx+1-3
...@@ -8,7 +8,6 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";...@@ -8,7 +8,6 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
8import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";8import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";
9import { AppText as Text } from "../../components/AppText";9import { AppText as Text } from "../../components/AppText";
10import { SymbolView } from "../../components/AppSymbol";10import { SymbolView } from "../../components/AppSymbol";
11import { useThemeColor } from "../../lib/useThemeColor";
12import { NativeStackScreenOptions } from "../../native/StackHeader";11import { NativeStackScreenOptions } from "../../native/StackHeader";
13import {12import {
14 mobileProjectGroupingModePatch,13 mobileProjectGroupingModePatch,
...@@ -42,7 +41,6 @@ const GROUPING_OPTIONS: ReadonlyArray<{...@@ -42,7 +41,6 @@ const GROUPING_OPTIONS: ReadonlyArray<{
42export function SettingsProjectGroupingRouteScreen() {41export function SettingsProjectGroupingRouteScreen() {
43 const navigation = useNavigation();42 const navigation = useNavigation();
44 const insets = useSafeAreaInsets();43 const insets = useSafeAreaInsets();
45 const checkmarkColor = useThemeColor("--color-icon");
46 const preferencesResult = useAtomValue(mobilePreferencesAtom);44 const preferencesResult = useAtomValue(mobilePreferencesAtom);
47 const savePreferences = useAtomSet(updateMobilePreferencesAtom);45 const savePreferences = useAtomSet(updateMobilePreferencesAtom);
48 const preferencesReady = AsyncResult.isSuccess(preferencesResult) && !preferencesResult.waiting;46 const preferencesReady = AsyncResult.isSuccess(preferencesResult) && !preferencesResult.waiting;
...@@ -92,7 +90,7 @@ export function SettingsProjectGroupingRouteScreen() {...@@ -92,7 +90,7 @@ export function SettingsProjectGroupingRouteScreen() {
92 <SymbolView90 <SymbolView
93 name="checkmark"91 name="checkmark"
94 size={18}92 size={18}
95 tintColor={checkmarkColor}93 tintColorClassName={"accent-icon"}
96 type="monochrome"94 type="monochrome"
97 weight="semibold"95 weight="semibold"
98 />96 />
apps/mobile/src/features/settings/SettingsRouteScreen.tsx+1-3
...@@ -33,7 +33,6 @@ import { hasCloudPublicConfig, resolveRelayClerkTokenOptions } from "../cloud/pu...@@ -33,7 +33,6 @@ import { hasCloudPublicConfig, resolveRelayClerkTokenOptions } from "../cloud/pu
33import { withNativeGlassHeaderItem } from "../layout/native-glass-header-items";33import { withNativeGlassHeaderItem } from "../layout/native-glass-header-items";
34import { WorkspaceSidebarToolbar } from "../layout/workspace-sidebar-toolbar";34import { WorkspaceSidebarToolbar } from "../layout/workspace-sidebar-toolbar";
35import { runtime } from "../../lib/runtime";35import { runtime } from "../../lib/runtime";
36import { useThemeColor } from "../../lib/useThemeColor";
37import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences";36import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences";
38import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled";37import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled";
39import {38import {
...@@ -585,7 +584,6 @@ function LegacySettingsSection() {...@@ -585,7 +584,6 @@ function LegacySettingsSection() {
585}584}
586585
587function AppSettingsSection() {586function AppSettingsSection() {
588 const icon = useThemeColor("--color-icon");
589 const [updateState, setUpdateState] = useState<AppUpdateCheckState>("idle");587 const [updateState, setUpdateState] = useState<AppUpdateCheckState>("idle");
590 const updateInFlight = useRef(false);588 const updateInFlight = useRef(false);
591 const hiddenUpdateTapCount = useRef(0);589 const hiddenUpdateTapCount = useRef(0);
...@@ -655,7 +653,7 @@ function AppSettingsSection() {...@@ -655,7 +653,7 @@ function AppSettingsSection() {
655 <SymbolView653 <SymbolView
656 name="info.circle"654 name="info.circle"
657 size={22}655 size={22}
658 tintColor={icon}656 tintColorClassName={"accent-icon"}
659 type="monochrome"657 type="monochrome"
660 weight="regular"658 weight="regular"
661 />659 />
apps/mobile/src/features/settings/appearance/AppearancePreferencesProvider.tsx+118-49
...@@ -1,15 +1,23 @@...@@ -1,15 +1,23 @@
1import { createContext, use, useCallback, useLayoutEffect, useMemo, type ReactNode } from "react";1import {
2import { useColorScheme } from "react-native";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
4import { useAtomSet, useAtomValue } from "@effect/atom-react";13import { useAtomSet, useAtomValue } from "@effect/atom-react";
5import { AsyncResult } from "effect/unstable/reactivity";14import { AsyncResult } from "effect/unstable/reactivity";
615
7import { Uniwind } from "uniwind";16import { ScopedTheme, Uniwind } from "uniwind";
817
9import {18import {
10 resolveAppearance,19 resolveAppearance,
11 resolveAppearancePreferences,20 resolveAppearancePreferences,
12 resolveTextScaleVariables,
13 type ResolvedAppearance,21 type ResolvedAppearance,
14} from "../../../lib/appearancePreferences";22} from "../../../lib/appearancePreferences";
15import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../../state/preferences";23import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../../state/preferences";
...@@ -17,7 +25,6 @@ import type { Preferences } from "../../../persistence/mobile-preferences";...@@ -17,7 +25,6 @@ import type { Preferences } from "../../../persistence/mobile-preferences";
17import {25import {
18 createMobileThemePairPatch,26 createMobileThemePairPatch,
19 createMobileThemeSelectionPatch,27 createMobileThemeSelectionPatch,
20 getMobileThemeVariables,
21 normalizeMobileThemeMode,28 normalizeMobileThemeMode,
22 resolveMobileThemeIds,29 resolveMobileThemeIds,
23 type MobileThemeAppearance,30 type MobileThemeAppearance,
...@@ -25,6 +32,11 @@ import {...@@ -25,6 +32,11 @@ import {
25 type MobileThemeIds,32 type MobileThemeIds,
26 type MobileThemeMode,33 type MobileThemeMode,
27} from "../../../lib/mobileTheme";34} from "../../../lib/mobileTheme";
35import {
36 createMobileThemeRuntimeOperations,
37 getMobileUniwindThemeName,
38 type MobileThemeRuntimeState,
39} from "../../../lib/mobileThemeRuntime";
28import { cacheTerminalFontSize } from "../../terminal/terminalUiState";40import { cacheTerminalFontSize } from "../../terminal/terminalUiState";
2941
30interface AppearancePreferencesContextValue {42interface AppearancePreferencesContextValue {
...@@ -51,30 +63,6 @@ interface AppearancePreferencesContextValue {...@@ -51,30 +63,6 @@ interface AppearancePreferencesContextValue {
5163
52const AppearancePreferencesContext = createContext<AppearancePreferencesContextValue | null>(null);64const 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
78export function AppearancePreferencesProvider(props: { readonly children: ReactNode }) {66export function AppearancePreferencesProvider(props: { readonly children: ReactNode }) {
79 const preferencesResult = useAtomValue(mobilePreferencesAtom);67 const preferencesResult = useAtomValue(mobilePreferencesAtom);
80 const savePreferences = useAtomSet(updateMobilePreferencesAtom);68 const savePreferences = useAtomSet(updateMobilePreferencesAtom);
...@@ -88,54 +76,135 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN...@@ -88,54 +76,135 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN
88 );76 );
89 const themeMode = normalizeMobileThemeMode(storedPreferences?.themeMode);77 const themeMode = normalizeMobileThemeMode(storedPreferences?.themeMode);
90 const themeAppearance = themeMode === "system" ? systemColorScheme : themeMode;78 const themeAppearance = themeMode === "system" ? systemColorScheme : themeMode;
91 const themeIds = useMemo(79 const resolvedThemeIds = resolveMobileThemeIds(storedPreferences ?? {});
92 () => resolveMobileThemeIds(storedPreferences ?? {}),80 const themeIds = useMemo<MobileThemeIds>(
93 [storedPreferences],81 () => ({ light: resolvedThemeIds.light, dark: resolvedThemeIds.dark }),
82 [resolvedThemeIds.dark, resolvedThemeIds.light],
94 );83 );
95 const themeId = themeIds[themeAppearance];84 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(() => {105 const applyThemeRuntime = useCallback((next: MobileThemeRuntimeState) => {
99 applyAppearanceVariables(preferences.baseFontSize, themeIds);106 const operations = createMobileThemeRuntimeOperations(appliedRuntimeStateRef.current, next);
100 Uniwind.setTheme(themeMode);107 for (const operation of operations) {
101 cacheTerminalFontSize(resolveAppearance(preferences).terminalFontSize);108 if (operation.kind === "update-text-variables") {
102 }, [preferences, themeIds, themeMode]);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
104 const updatePreferences = useCallback(126 const updatePreferences = useCallback(
105 (patch: Partial<Preferences>) => {127 (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.
106 savePreferences(patch);138 savePreferences(patch);
107 },139 },
108 [savePreferences],140 [savePreferences],
109 );141 );
110142
143 useLayoutEffect(() => {
144 selectedThemeIdsRef.current = themeIds;
145 syncThemeRuntime(runtimeState);
146 cacheTerminalFontSize(appearance.terminalFontSize);
147 }, [appearance.terminalFontSize, runtimeState, syncThemeRuntime, themeIds]);
148
111 const setThemeIdForAppearance = useCallback(149 const setThemeIdForAppearance = useCallback(
112 (appearance: MobileThemeAppearance, value: MobileThemeId) => {150 (appearance: MobileThemeAppearance, value: MobileThemeId) => {
113 updatePreferences(151 const patch = createMobileThemeSelectionPatch(
114 createMobileThemeSelectionPatch(themeIds, themeAppearance, appearance, value),152 selectedThemeIdsRef.current,
153 themeAppearance,
154 appearance,
155 value,
115 );156 );
157 selectedThemeIdsRef.current = resolveMobileThemeIds(patch);
158 updateThemePreferences(patch);
116 },159 },
117 [themeAppearance, themeIds, updatePreferences],160 [themeAppearance, updateThemePreferences],
118 );161 );
119162
120 const setThemeIdForBothAppearances = useCallback(163 const setThemeIdForBothAppearances = useCallback(
121 (value: MobileThemeId) => {164 (value: MobileThemeId) => {
122 updatePreferences(createMobileThemePairPatch(value));165 const patch = createMobileThemePairPatch(value);
166 selectedThemeIdsRef.current = resolveMobileThemeIds(patch);
167 updateThemePreferences(patch);
123 },168 },
124 [updatePreferences],169 [updateThemePreferences],
125 );170 );
126171
127 const setThemeMode = useCallback(172 const setThemeMode = useCallback(
128 (value: MobileThemeMode) => {173 (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 }
130 },197 },
131 [updatePreferences],198 [runtimeState, syncThemeRuntime, updateThemePreferences],
132 );199 );
133200
134 const setBaseFontSize = useCallback(201 const setBaseFontSize = useCallback(
135 (value: number) => {202 (value: number) => {
203 const current = appliedRuntimeStateRef.current ?? runtimeState;
204 syncThemeRuntime({ ...current, baseFontSize: value });
136 updatePreferences({ baseFontSize: value });205 updatePreferences({ baseFontSize: value });
137 },206 },
138 [updatePreferences],207 [runtimeState, syncThemeRuntime, updatePreferences],
139 );208 );
140209
141 const setTerminalFontSize = useCallback(210 const setTerminalFontSize = useCallback(
...@@ -161,7 +230,7 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN...@@ -161,7 +230,7 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN
161230
162 const value = useMemo(231 const value = useMemo(
163 (): AppearancePreferencesContextValue => ({232 (): AppearancePreferencesContextValue => ({
164 appearance: resolveAppearance(preferences),233 appearance,
165 themeId,234 themeId,
166 themeIds,235 themeIds,
167 themeMode,236 themeMode,
...@@ -176,7 +245,7 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN...@@ -176,7 +245,7 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN
176 setCodeWordBreak,245 setCodeWordBreak,
177 }),246 }),
178 [247 [
179 preferences,248 appearance,
180 themeId,249 themeId,
181 themeIds,250 themeIds,
182 themeMode,251 themeMode,
...@@ -194,7 +263,7 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN...@@ -194,7 +263,7 @@ export function AppearancePreferencesProvider(props: { readonly children: ReactN
194263
195 return (264 return (
196 <AppearancePreferencesContext.Provider value={value}>265 <AppearancePreferencesContext.Provider value={value}>
197 {props.children}266 <ScopedTheme theme={activeThemeName}>{props.children}</ScopedTheme>
198 </AppearancePreferencesContext.Provider>267 </AppearancePreferencesContext.Provider>
199 );268 );
200}269}
apps/mobile/src/features/settings/appearance/components/AppearancePreviews.tsx+4-3
...@@ -5,7 +5,7 @@ import {...@@ -5,7 +5,7 @@ import {
5 resolveMarkdownFontSizes,5 resolveMarkdownFontSizes,
6 resolveMobileCodeSurface,6 resolveMobileCodeSurface,
7} from "../../../../lib/appearancePreferences";7} from "../../../../lib/appearancePreferences";
8import { useThemeColor } from "../../../../lib/useThemeColor";8import { useUniwindTheme } from "../../../../lib/useUniwindTheme";
9import { getMobileTerminalTheme } from "../../../terminal/terminalTheme";9import { getMobileTerminalTheme } from "../../../terminal/terminalTheme";
10import { useAppearancePreferences } from "../AppearancePreferencesProvider";10import { useAppearancePreferences } from "../AppearancePreferencesProvider";
1111
...@@ -138,8 +138,9 @@ export function CodeAppearancePreview(props: {...@@ -138,8 +138,9 @@ export function CodeAppearancePreview(props: {
138 readonly wordBreak: boolean;138 readonly wordBreak: boolean;
139}) {139}) {
140 const surface = resolveMobileCodeSurface(props.fontSize);140 const surface = resolveMobileCodeSurface(props.fontSize);
141 const lineNumberColor = useThemeColor("--color-icon-subtle");141 const theme = useUniwindTheme();
142 const keywordColor = useThemeColor("--color-md-link");142 const lineNumberColor = theme["--color-icon-subtle"];
143 const keywordColor = theme["--color-md-link"];
143144
144 const lineNumber = (line: CodePreviewLine, index: number) => (145 const lineNumber = (line: CodePreviewLine, index: number) => (
145 <Text146 <Text
apps/mobile/src/features/settings/appearance/components/FontSizeSliderRow.tsx+7-8
...@@ -12,7 +12,7 @@ import Animated, {...@@ -12,7 +12,7 @@ import Animated, {
12import type { ComponentProps } from "react";12import type { ComponentProps } from "react";
1313
14import { AppText as Text } from "../../../../components/AppText";14import { AppText as Text } from "../../../../components/AppText";
15import { useThemeColor } from "../../../../lib/useThemeColor";15import { useUniwindTheme } from "../../../../lib/useUniwindTheme";
1616
17type SymbolName = ComponentProps<typeof SymbolView>["name"];17type SymbolName = ComponentProps<typeof SymbolView>["name"];
1818
...@@ -36,10 +36,9 @@ export function FontSizeSliderRow(props: {...@@ -36,10 +36,9 @@ export function FontSizeSliderRow(props: {
36 readonly value: number;36 readonly value: number;
37 readonly onChange: (value: number) => void;37 readonly onChange: (value: number) => void;
38}) {38}) {
39 const icon = useThemeColor("--color-icon");39 const theme = useUniwindTheme();
40 const iconMuted = String(useThemeColor("--color-icon-muted"));40 const trackColor = theme["--color-secondary-border"];
41 const trackColor = String(useThemeColor("--color-secondary-border"));41 const fillColor = theme["--color-primary"];
42 const fillColor = String(useThemeColor("--color-primary"));
4342
44 const latest = useRef(props);43 const latest = useRef(props);
45 latest.current = props;44 latest.current = props;
...@@ -141,7 +140,7 @@ export function FontSizeSliderRow(props: {...@@ -141,7 +140,7 @@ export function FontSizeSliderRow(props: {
141 <SymbolView140 <SymbolView
142 name={props.icon}141 name={props.icon}
143 size={22}142 size={22}
144 tintColor={icon}143 tintColorClassName={"accent-icon"}
145 type="monochrome"144 type="monochrome"
146 weight="regular"145 weight="regular"
147 />146 />
...@@ -152,7 +151,7 @@ export function FontSizeSliderRow(props: {...@@ -152,7 +151,7 @@ export function FontSizeSliderRow(props: {
152 <SymbolView151 <SymbolView
153 name="textformat.size.smaller"152 name="textformat.size.smaller"
154 size={15}153 size={15}
155 tintColor={iconMuted}154 tintColorClassName={"accent-icon-muted"}
156 type="monochrome"155 type="monochrome"
157 weight="regular"156 weight="regular"
158 />157 />
...@@ -204,7 +203,7 @@ export function FontSizeSliderRow(props: {...@@ -204,7 +203,7 @@ export function FontSizeSliderRow(props: {
204 <SymbolView203 <SymbolView
205 name="textformat.size.larger"204 name="textformat.size.larger"
206 size={22}205 size={22}
207 tintColor={iconMuted}206 tintColorClassName={"accent-icon-muted"}
208 type="monochrome"207 type="monochrome"
209 weight="regular"208 weight="regular"
210 />209 />
apps/mobile/src/features/settings/appearance/sections/ThemeAppearanceSection.tsx+73-117
...@@ -1,22 +1,22 @@...@@ -1,22 +1,22 @@
1import { memo, useId } from "react";1import { memo, useId } from "react";
2import { Pressable, View } from "react-native";2import { Pressable, View } from "react-native";
3import Svg, { Circle, Defs, RadialGradient, Stop } from "react-native-svg";3import Svg, { Circle, Defs, RadialGradient, Stop } from "react-native-svg";
4import { ScopedTheme } from "uniwind";
45
5import { mixThemePreviewBase, THEME_PREVIEW_RENDER_SPECS } from "@t3tools/shared/themePreview";6import { mixThemePreviewBase, THEME_PREVIEW_RENDER_SPECS } from "@t3tools/shared/themePreview";
67
7import { SymbolView } from "../../../../components/AppSymbol";8import { SymbolView } from "../../../../components/AppSymbol";
8import { AppText as Text } from "../../../../components/AppText";9import { AppText as Text } from "../../../../components/AppText";
9import {10import {
10 getMobileThemeVariables,
11 getMobileThemePreviewColors,11 getMobileThemePreviewColors,
12 MOBILE_THEME_OPTIONS,12 MOBILE_THEME_OPTIONS,
13 type MobileThemeAppearance,13 type MobileThemeAppearance,
14 type MobileThemeId,14 type MobileThemeId,
15 type MobileThemeIds,15 type MobileThemeIds,
16 type MobileThemeMode,16 type MobileThemeMode,
17 type MobileThemeVariables,
18} from "../../../../lib/mobileTheme";17} from "../../../../lib/mobileTheme";
19import { useThemeColor } from "../../../../lib/useThemeColor";18import { getMobileUniwindThemeName } from "../../../../lib/mobileThemeRuntime";
19import { cn } from "../../../../lib/cn";
20import { useAppearancePreferences } from "../AppearancePreferencesProvider";20import { useAppearancePreferences } from "../AppearancePreferencesProvider";
2121
22const APPEARANCE_MODES: ReadonlyArray<{22const APPEARANCE_MODES: ReadonlyArray<{
...@@ -28,6 +28,8 @@ const APPEARANCE_MODES: ReadonlyArray<{...@@ -28,6 +28,8 @@ const APPEARANCE_MODES: ReadonlyArray<{
28 { id: "dark", label: "Dark" },28 { id: "dark", label: "Dark" },
29];29];
3030
31const previewPercentage = (value: number) => `${value * 100}%`;
32
31const PreviewOrb = memo(function PreviewOrb(props: {33const PreviewOrb = memo(function PreviewOrb(props: {
32 readonly appearance: MobileThemeAppearance;34 readonly appearance: MobileThemeAppearance;
33 readonly compact?: boolean;35 readonly compact?: boolean;
...@@ -46,9 +48,6 @@ const PreviewOrb = memo(function PreviewOrb(props: {...@@ -46,9 +48,6 @@ const PreviewOrb = memo(function PreviewOrb(props: {
46 Math.max(spec.action.center[0], 1 - spec.action.center[0]),48 Math.max(spec.action.center[0], 1 - spec.action.center[0]),
47 Math.max(spec.action.center[1], 1 - spec.action.center[1]),49 Math.max(spec.action.center[1], 1 - spec.action.center[1]),
48 );50 );
49 const position = (value: number) => `${value * 100}%`;
50 const radius = (value: number) => `${value * 100}%`;
51
52 return (51 return (
53 <View52 <View
54 className={`${props.compact ? "size-14" : "size-16"} overflow-hidden rounded-full border`}53 className={`${props.compact ? "size-14" : "size-16"} overflow-hidden rounded-full border`}
...@@ -60,33 +59,33 @@ const PreviewOrb = memo(function PreviewOrb(props: {...@@ -60,33 +59,33 @@ const PreviewOrb = memo(function PreviewOrb(props: {
60 <Svg accessibilityElementsHidden height="100%" viewBox="0 0 64 64" width="100%">59 <Svg accessibilityElementsHidden height="100%" viewBox="0 0 64 64" width="100%">
61 <Defs>60 <Defs>
62 <RadialGradient61 <RadialGradient
63 cx={position(spec.accent.center[0])}62 cx={previewPercentage(spec.accent.center[0])}
64 cy={position(spec.accent.center[1])}63 cy={previewPercentage(spec.accent.center[1])}
65 fx={position(spec.accent.center[0])}64 fx={previewPercentage(spec.accent.center[0])}
66 fy={position(spec.accent.center[1])}65 fy={previewPercentage(spec.accent.center[1])}
67 id={accentGradientId}66 id={accentGradientId}
68 r={radius(accentRadius)}67 r={previewPercentage(accentRadius)}
69 >68 >
70 <Stop offset="0%" stopColor={colors.accent} stopOpacity={1} />69 <Stop offset="0%" stopColor={colors.accent} stopOpacity={1} />
71 <Stop70 <Stop
72 offset={position(spec.accent.middleOffset)}71 offset={previewPercentage(spec.accent.middleOffset)}
73 stopColor={colors.accent}72 stopColor={colors.accent}
74 stopOpacity={spec.accent.middleOpacity}73 stopOpacity={spec.accent.middleOpacity}
75 />74 />
76 <Stop75 <Stop
77 offset={position(spec.accent.endOffset)}76 offset={previewPercentage(spec.accent.endOffset)}
78 stopColor={colors.accent}77 stopColor={colors.accent}
79 stopOpacity={0}78 stopOpacity={0}
80 />79 />
81 <Stop offset="100%" stopColor={colors.accent} stopOpacity={0} />80 <Stop offset="100%" stopColor={colors.accent} stopOpacity={0} />
82 </RadialGradient>81 </RadialGradient>
83 <RadialGradient82 <RadialGradient
84 cx={position(spec.action.center[0])}83 cx={previewPercentage(spec.action.center[0])}
85 cy={position(spec.action.center[1])}84 cy={previewPercentage(spec.action.center[1])}
86 fx={position(spec.action.center[0])}85 fx={previewPercentage(spec.action.center[0])}
87 fy={position(spec.action.center[1])}86 fy={previewPercentage(spec.action.center[1])}
88 id={actionGradientId}87 id={actionGradientId}
89 r={radius(actionRadius)}88 r={previewPercentage(actionRadius)}
90 >89 >
91 <Stop90 <Stop
92 offset="0%"91 offset="0%"
...@@ -94,7 +93,7 @@ const PreviewOrb = memo(function PreviewOrb(props: {...@@ -94,7 +93,7 @@ const PreviewOrb = memo(function PreviewOrb(props: {
94 stopOpacity={spec.action.startOpacity}93 stopOpacity={spec.action.startOpacity}
95 />94 />
96 <Stop95 <Stop
97 offset={position(spec.action.endOffset)}96 offset={previewPercentage(spec.action.endOffset)}
98 stopColor={colors.messageAction}97 stopColor={colors.messageAction}
99 stopOpacity={0}98 stopOpacity={0}
100 />99 />
...@@ -118,32 +117,26 @@ function ThemeCard(props: {...@@ -118,32 +117,26 @@ function ThemeCard(props: {
118 readonly onSelect: (appearance: MobileThemeAppearance) => void;117 readonly onSelect: (appearance: MobileThemeAppearance) => void;
119 readonly themeId: MobileThemeId;118 readonly themeId: MobileThemeId;
120}) {119}) {
121 const badgeBackground = useThemeColor("--color-card");
122 const badgeIcon = useThemeColor("--color-icon");
123
124 const choice = (appearance: MobileThemeAppearance, selected: boolean) => (120 const choice = (appearance: MobileThemeAppearance, selected: boolean) => (
125 <Pressable121 <Pressable
122 accessibilityHint={`Sets the ${appearance} appearance only`}
126 accessibilityLabel={`${props.label} ${appearance} theme`}123 accessibilityLabel={`${props.label} ${appearance} theme`}
127 accessibilityRole="radio"124 accessibilityRole="button"
128 accessibilityState={{ checked: selected, disabled: props.disabled }}125 accessibilityState={{ disabled: props.disabled, selected }}
129 className={126 className={cn(
130 selected127 "size-[66px] items-center justify-center rounded-full border-[3px] active:scale-[0.94]",
131 ? "size-[66px] items-center justify-center rounded-full border-[3px] border-primary"128 selected ? "border-primary" : "border-transparent",
132 : "size-[66px] items-center justify-center rounded-full border-[3px] border-transparent"129 )}
133 }
134 disabled={props.disabled}130 disabled={props.disabled}
135 onPress={() => props.onSelect(appearance)}131 onPress={() => props.onSelect(appearance)}
136 >132 >
137 <PreviewOrb appearance={appearance} compact themeId={props.themeId} />133 <PreviewOrb appearance={appearance} compact themeId={props.themeId} />
138 {selected ? (134 {selected ? (
139 <View135 <View className="absolute -bottom-0.5 -right-0.5 size-5 items-center justify-center rounded-full border border-border bg-card">
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 >
143 <SymbolView136 <SymbolView
144 name={appearance === "light" ? "sun.max" : "moon"}137 name={appearance === "light" ? "sun.max" : "moon"}
145 size={12}138 size={12}
146 tintColor={badgeIcon}139 tintColorClassName="accent-icon"
147 type="monochrome"140 type="monochrome"
148 weight="medium"141 weight="medium"
149 />142 />
...@@ -158,7 +151,10 @@ function ThemeCard(props: {...@@ -158,7 +151,10 @@ function ThemeCard(props: {
158 accessibilityHint="Sets both light and dark appearances"151 accessibilityHint="Sets both light and dark appearances"
159 accessibilityLabel={`${props.label} theme`}152 accessibilityLabel={`${props.label} theme`}
160 accessibilityRole="button"153 accessibilityRole="button"
161 accessibilityState={{ disabled: props.disabled }}154 accessibilityState={{
155 disabled: props.disabled,
156 selected: props.lightSelected && props.darkSelected,
157 }}
162 className="absolute inset-0 rounded-[24px] active:bg-subtle"158 className="absolute inset-0 rounded-[24px] active:bg-subtle"
163 disabled={props.disabled}159 disabled={props.disabled}
164 onPress={props.onSelectBoth}160 onPress={props.onSelectBoth}
...@@ -167,34 +163,26 @@ function ThemeCard(props: {...@@ -167,34 +163,26 @@ function ThemeCard(props: {
167 {choice("light", props.lightSelected)}163 {choice("light", props.lightSelected)}
168 {choice("dark", props.darkSelected)}164 {choice("dark", props.darkSelected)}
169 </View>165 </View>
170 <View className="min-h-8 flex-row items-center" pointerEvents="none">166 <Text
171 <Text className="min-w-0 flex-1 text-lg font-t3-medium" numberOfLines={1}>167 className="min-w-0 flex-1 px-1 text-lg font-t3-medium"
172 {props.label}168 numberOfLines={1}
173 </Text>169 pointerEvents="none"
174 </View>170 >
171 {props.label}
172 </Text>
175 </View>173 </View>
176 );174 );
177}175}
178176
179function PreviewPane(props: { readonly colors: MobileThemeVariables; readonly compact?: boolean }) {177function PreviewPane(props: { readonly compact?: boolean }) {
180 return (178 return (
181 <View179 <View className="flex-1 overflow-hidden bg-screen">
182 className="flex-1 overflow-hidden"
183 style={{ backgroundColor: props.colors["--color-screen"] }}
184 >
185 <View180 <View
186 className={props.compact ? "h-[18px] gap-0.5 px-1" : "h-[18px] gap-1 px-1.5"}181 className={cn("bg-card", props.compact ? "h-[18px] gap-0.5 px-1" : "h-[18px] gap-1 px-1.5")}
187 style={{ backgroundColor: props.colors["--color-card"] }}
188 >182 >
189 <View className="mt-2 flex-row items-center gap-1">183 <View className="mt-2 flex-row items-center gap-1">
190 <View184 <View className="size-1.5 rounded-full bg-primary" />
191 className="size-1.5 rounded-full"185 <View className="h-1 flex-1 rounded-full bg-foreground-muted" />
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 />
198 </View>186 </View>
199 </View>187 </View>
200 <View188 <View
...@@ -203,24 +191,12 @@ function PreviewPane(props: { readonly colors: MobileThemeVariables; readonly co...@@ -203,24 +191,12 @@ function PreviewPane(props: { readonly colors: MobileThemeVariables; readonly co
203 }191 }
204 >192 >
205 <View className="gap-1">193 <View className="gap-1">
206 <View194 <View className="h-1.5 w-[72%] rounded-full bg-subtle-strong" />
207 className="h-1.5 w-[72%] rounded-full"195 <View className="h-1.5 w-[46%] rounded-full bg-subtle-strong" />
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 />
214 </View>196 </View>
215 <View className="items-end gap-1 pb-2">197 <View className="items-end gap-1 pb-2">
216 <View198 <View className="h-3 w-[78%] rounded-full bg-user-bubble" />
217 className="h-3 w-[78%] rounded-full"199 <View className="h-1 w-[38%] rounded-full bg-foreground-muted" />
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 />
224 </View>200 </View>
225 </View>201 </View>
226 </View>202 </View>
...@@ -228,50 +204,31 @@ function PreviewPane(props: { readonly colors: MobileThemeVariables; readonly co...@@ -228,50 +204,31 @@ function PreviewPane(props: { readonly colors: MobileThemeVariables; readonly co
228}204}
229205
230function ModePreview(props: { readonly mode: MobileThemeMode; readonly themeIds: MobileThemeIds }) {206function ModePreview(props: { readonly mode: MobileThemeMode; readonly themeIds: MobileThemeIds }) {
231 const light = getMobileThemeVariables(props.themeIds.light, "light");207 if (props.mode === "system") {
232 const dark = getMobileThemeVariables(props.themeIds.dark, "dark");208 return (
233 const currentBorder = useThemeColor("--color-border");209 <View className="h-24 w-14 self-center rounded-[16px] border-[1.5px] border-border bg-drawer p-[3px]">
234 const currentFrame = useThemeColor("--color-drawer");210 <View className="flex-1 flex-row overflow-hidden rounded-[11px]">
235 const currentIndicator = useThemeColor("--color-foreground-muted");211 <ScopedTheme theme={getMobileUniwindThemeName(props.themeIds.light, "light")}>
236 const frameColor =212 <PreviewPane compact />
237 props.mode === "light"213 </ScopedTheme>
238 ? light["--color-border"]214 <ScopedTheme theme={getMobileUniwindThemeName(props.themeIds.dark, "dark")}>
239 : props.mode === "dark"215 <PreviewPane compact />
240 ? dark["--color-border"]216 </ScopedTheme>
241 : currentBorder;217 </View>
242 const frameBackground =218 <View className="absolute bottom-[6px] left-1/2 h-1 w-4 -translate-x-1/2 rounded-full bg-foreground-muted" />
243 props.mode === "light"219 </View>
244 ? light["--color-drawer"]220 );
245 : props.mode === "dark"221 }
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;
254222
255 return (223 return (
256 <View224 <ScopedTheme theme={getMobileUniwindThemeName(props.themeIds[props.mode], props.mode)}>
257 className="h-24 w-14 self-center rounded-[16px] p-[3px]"225 <View className="h-24 w-14 self-center rounded-[16px] border-[1.5px] border-border bg-drawer p-[3px]">
258 style={{ backgroundColor: frameBackground, borderColor: frameColor, borderWidth: 1.5 }}226 <View className="flex-1 flex-row overflow-hidden rounded-[11px]">
259 >227 <PreviewPane />
260 <View className="flex-1 flex-row overflow-hidden rounded-[11px]">228 </View>
261 {props.mode === "system" ? (229 <View className="absolute bottom-[6px] left-1/2 h-1 w-4 -translate-x-1/2 rounded-full bg-foreground-muted" />
262 <>
263 <PreviewPane colors={light} compact />
264 <PreviewPane colors={dark} compact />
265 </>
266 ) : (
267 <PreviewPane colors={props.mode === "light" ? light : dark} />
268 )}
269 </View>230 </View>
270 <View231 </ScopedTheme>
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>
275 );232 );
276}233}
277234
...@@ -288,11 +245,10 @@ function ModeCard(props: {...@@ -288,11 +245,10 @@ function ModeCard(props: {
288 accessibilityLabel={`${props.label} appearance`}245 accessibilityLabel={`${props.label} appearance`}
289 accessibilityRole="radio"246 accessibilityRole="radio"
290 accessibilityState={{ checked: props.selected, disabled: props.disabled }}247 accessibilityState={{ checked: props.selected, disabled: props.disabled }}
291 className={248 className={cn(
292 props.selected249 "min-w-0 flex-1 gap-2 rounded-[24px] p-2 active:scale-[0.97]",
293 ? "min-w-0 flex-1 gap-2 rounded-[24px] border-2 border-primary bg-subtle p-2"250 props.selected ? "border-2 border-primary bg-subtle" : "border border-border bg-card",
294 : "min-w-0 flex-1 gap-2 rounded-[24px] border border-border bg-card p-2"251 )}
295 }
296 disabled={props.disabled}252 disabled={props.disabled}
297 onPress={props.onPress}253 onPress={props.onPress}
298 >254 >
apps/mobile/src/features/settings/appearance/useScaledTextRole.ts+22-24
...@@ -1,18 +1,12 @@...@@ -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";
3import { MOBILE_TYPOGRAPHY } from "../../../lib/typography";8import { MOBILE_TYPOGRAPHY } from "../../../lib/typography";
49import { useAppearancePreferences } from "./AppearancePreferencesProvider";
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>;
1610
17export interface ScaledTextRole {11export interface ScaledTextRole {
18 readonly fontSize: number;12 readonly fontSize: number;
...@@ -20,17 +14,21 @@ export interface ScaledTextRole {...@@ -20,17 +14,21 @@ export interface ScaledTextRole {
20}14}
2115
22/**16/**
23 * Reads a typography role's current size from the Uniwind `--text-*` CSS17 * Mirrors the values injected into Uniwind for style-prop consumers that
24 * variables (scaled at runtime with the base font size). Use for style-prop18 * cannot use a `text-*` class. This deliberately does not subscribe to CSS
25 * consumers that can't express their size as a `text-*` className. Reactive:19 * variables, so palette-only setTheme calls remain native-only.
26 * re-renders when the appearance provider re-injects the variables.
27 */20 */
28export function useScaledTextRole(role: keyof typeof MOBILE_TYPOGRAPHY): ScaledTextRole {21export function useScaledTextRole(role: keyof typeof MOBILE_TYPOGRAPHY): ScaledTextRole {
29 const variable = TEXT_ROLE_VARIABLES[role];22 const { appearance } = useAppearancePreferences();
30 const [fontSize, lineHeight] = useCSSVariable([variable, `${variable}--line-height`]);23 return useMemo(() => {
3124 const baseFontSize = normalizeBaseFontSize(appearance.baseFontSize);
32 return {25 const typography = MOBILE_TYPOGRAPHY[role];
33 fontSize: typeof fontSize === "number" ? fontSize : MOBILE_TYPOGRAPHY[role].fontSize,26 return {
34 lineHeight: typeof lineHeight === "number" ? lineHeight : MOBILE_TYPOGRAPHY[role].lineHeight,27 fontSize: Math.max(
35 };28 8,
29 Math.round(typography.fontSize * (baseFontSize / DEFAULT_BASE_FONT_SIZE)),
30 ),
31 lineHeight: scaledTypographyLineHeight(typography, baseFontSize),
32 };
33 }, [appearance.baseFontSize, role]);
36}34}
apps/mobile/src/features/settings/components/SettingsLegalDocumentRouteScreen.tsx+3-7
...@@ -6,12 +6,10 @@ import { WebView } from "react-native-webview";...@@ -6,12 +6,10 @@ import { WebView } from "react-native-webview";
6import { AppText as Text } from "../../../components/AppText";6import { AppText as Text } from "../../../components/AppText";
7import { LoadingStrip } from "../../../components/LoadingStrip";7import { LoadingStrip } from "../../../components/LoadingStrip";
8import { SymbolView } from "../../../components/AppSymbol";8import { SymbolView } from "../../../components/AppSymbol";
9import { useThemeColor } from "../../../lib/useThemeColor";
10import { isLegalDocumentUrl, LEGAL_URL } from "../lib/legal-document-url";9import { isLegalDocumentUrl, LEGAL_URL } from "../lib/legal-document-url";
1110
12export function SettingsLegalDocumentCloseHeaderButton() {11export function SettingsLegalDocumentCloseHeaderButton() {
13 const navigation = useNavigation();12 const navigation = useNavigation();
14 const iconColor = useThemeColor("--color-icon");
1513
16 return (14 return (
17 <Pressable15 <Pressable
...@@ -24,7 +22,7 @@ export function SettingsLegalDocumentCloseHeaderButton() {...@@ -24,7 +22,7 @@ export function SettingsLegalDocumentCloseHeaderButton() {
24 <SymbolView22 <SymbolView
25 name="xmark"23 name="xmark"
26 size={18}24 size={18}
27 tintColor={iconColor}25 tintColorClassName={"accent-icon"}
28 type="monochrome"26 type="monochrome"
29 weight="semibold"27 weight="semibold"
30 />28 />
...@@ -37,7 +35,6 @@ export function SettingsLegalDocumentExternalHeaderButton({...@@ -37,7 +35,6 @@ export function SettingsLegalDocumentExternalHeaderButton({
37}: {35}: {
38 readonly externalUrl?: string;36 readonly externalUrl?: string;
39}) {37}) {
40 const iconColor = useThemeColor("--color-icon");
41 const safeExternalUrl = isLegalDocumentUrl(externalUrl) ? externalUrl : LEGAL_URL;38 const safeExternalUrl = isLegalDocumentUrl(externalUrl) ? externalUrl : LEGAL_URL;
4239
43 return (40 return (
...@@ -51,7 +48,7 @@ export function SettingsLegalDocumentExternalHeaderButton({...@@ -51,7 +48,7 @@ export function SettingsLegalDocumentExternalHeaderButton({
51 <SymbolView48 <SymbolView
52 name="safari"49 name="safari"
53 size={19}50 size={19}
54 tintColor={iconColor}51 tintColorClassName={"accent-icon"}
55 type="monochrome"52 type="monochrome"
56 weight="regular"53 weight="regular"
57 />54 />
...@@ -69,7 +66,6 @@ export function SettingsLegalDocumentRouteScreen({...@@ -69,7 +66,6 @@ export function SettingsLegalDocumentRouteScreen({
69 documentUrl,66 documentUrl,
70}: SettingsLegalDocumentRouteScreenProps) {67}: SettingsLegalDocumentRouteScreenProps) {
71 const navigation = useNavigation<NavigationProp<ParamListBase>>();68 const navigation = useNavigation<NavigationProp<ParamListBase>>();
72 const iconColor = useThemeColor("--color-icon");
73 const [reloadKey, setReloadKey] = useState(0);69 const [reloadKey, setReloadKey] = useState(0);
74 const [loadProgress, setLoadProgress] = useState(0);70 const [loadProgress, setLoadProgress] = useState(0);
75 const [loadError, setLoadError] = useState<string | null>(null);71 const [loadError, setLoadError] = useState<string | null>(null);
...@@ -94,7 +90,7 @@ export function SettingsLegalDocumentRouteScreen({...@@ -94,7 +90,7 @@ export function SettingsLegalDocumentRouteScreen({
94 <SymbolView90 <SymbolView
95 name="exclamationmark.triangle"91 name="exclamationmark.triangle"
96 size={32}92 size={32}
97 tintColor={iconColor}93 tintColorClassName={"accent-icon"}
98 type="monochrome"94 type="monochrome"
99 weight="regular"95 weight="regular"
100 />96 />
apps/mobile/src/features/settings/components/SettingsRow.tsx+8-5
...@@ -5,7 +5,6 @@ import { Pressable, View } from "react-native";...@@ -5,7 +5,6 @@ import { Pressable, View } from "react-native";
5import { SymbolView } from "../../../components/AppSymbol";5import { SymbolView } from "../../../components/AppSymbol";
66
7import { AppText as Text } from "../../../components/AppText";7import { AppText as Text } from "../../../components/AppText";
8import { useThemeColor } from "../../../lib/useThemeColor";
9import type { SettingsLegalDocumentTarget, SettingsSheetTarget } from "./settings-sheet-targets";8import type { SettingsLegalDocumentTarget, SettingsSheetTarget } from "./settings-sheet-targets";
109
11type SymbolName = ComponentProps<typeof SymbolView>["name"];10type SymbolName = ComponentProps<typeof SymbolView>["name"];
...@@ -20,8 +19,6 @@ export function SettingsRow(props: {...@@ -20,8 +19,6 @@ export function SettingsRow(props: {
20 readonly onPress?: () => void;19 readonly onPress?: () => void;
21}) {20}) {
22 const navigation = useNavigation();21 const navigation = useNavigation();
23 const icon = useThemeColor("--color-icon");
24 const chevron = useThemeColor("--color-chevron");
25 const content = (22 const content = (
26 <View23 <View
27 className={24 className={
...@@ -30,7 +27,13 @@ export function SettingsRow(props: {...@@ -30,7 +27,13 @@ export function SettingsRow(props: {
30 : "flex-row items-center gap-4 p-4"27 : "flex-row items-center gap-4 p-4"
31 }28 }
32 >29 >
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 />
34 <Text className="shrink-0 text-lg text-foreground" numberOfLines={1}>37 <Text className="shrink-0 text-lg text-foreground" numberOfLines={1}>
35 {props.label}38 {props.label}
36 </Text>39 </Text>
...@@ -48,7 +51,7 @@ export function SettingsRow(props: {...@@ -48,7 +51,7 @@ export function SettingsRow(props: {
48 <SymbolView51 <SymbolView
49 name="chevron.right"52 name="chevron.right"
50 size={16}53 size={16}
51 tintColor={chevron}54 tintColorClassName={"accent-chevron"}
52 type="monochrome"55 type="monochrome"
53 weight="semibold"56 weight="semibold"
54 />57 />
apps/mobile/src/features/settings/components/SettingsSwitchRow.tsx+7-4
...@@ -4,7 +4,6 @@ import { View } from "react-native";...@@ -4,7 +4,6 @@ import { View } from "react-native";
4import { SymbolView } from "../../../components/AppSymbol";4import { SymbolView } from "../../../components/AppSymbol";
5import { AppText as Text } from "../../../components/AppText";5import { AppText as Text } from "../../../components/AppText";
6import { ThemedSwitch } from "../../../components/ThemedSwitch";6import { ThemedSwitch } from "../../../components/ThemedSwitch";
7import { useThemeColor } from "../../../lib/useThemeColor";
87
9type SymbolName = ComponentProps<typeof SymbolView>["name"];8type SymbolName = ComponentProps<typeof SymbolView>["name"];
109
...@@ -16,8 +15,6 @@ export function SettingsSwitchRow(props: {...@@ -16,8 +15,6 @@ export function SettingsSwitchRow(props: {
16 readonly value: boolean;15 readonly value: boolean;
17 readonly onValueChange: (value: boolean) => void;16 readonly onValueChange: (value: boolean) => void;
18}) {17}) {
19 const icon = useThemeColor("--color-icon");
20
21 return (18 return (
22 <View19 <View
23 className={20 className={
...@@ -26,7 +23,13 @@ export function SettingsSwitchRow(props: {...@@ -26,7 +23,13 @@ export function SettingsSwitchRow(props: {
26 : "flex-row items-center gap-4 p-4"23 : "flex-row items-center gap-4 p-4"
27 }24 }
28 >25 >
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 />
30 <View className="min-w-0 flex-1">33 <View className="min-w-0 flex-1">
31 <Text className="text-lg text-foreground">{props.label}</Text>34 <Text className="text-lg text-foreground">{props.label}</Text>
32 {props.subtitle ? (35 {props.subtitle ? (
apps/mobile/src/features/terminal/ThreadTerminalPanel.tsx+6-3
...@@ -4,7 +4,6 @@ import { memo, useCallback, useEffect, useMemo, useRef } from "react";...@@ -4,7 +4,6 @@ import { memo, useCallback, useEffect, useMemo, useRef } from "react";
4import { Pressable, View } from "react-native";4import { Pressable, View } from "react-native";
55
6import { AppText as Text } from "../../components/AppText";6import { AppText as Text } from "../../components/AppText";
7import { useThemeColor } from "../../lib/useThemeColor";
8import { terminalEnvironment } from "../../state/terminal";7import { terminalEnvironment } from "../../state/terminal";
9import { useAtomCommand } from "../../state/use-atom-command";8import { useAtomCommand } from "../../state/use-atom-command";
10import { useAttachedTerminalSession } from "../../state/use-terminal-session";9import { useAttachedTerminalSession } from "../../state/use-terminal-session";
...@@ -36,7 +35,6 @@ export const ThreadTerminalPanel = memo(function ThreadTerminalPanel(...@@ -36,7 +35,6 @@ export const ThreadTerminalPanel = memo(function ThreadTerminalPanel(
36 const closeTerminal = useAtomCommand(terminalEnvironment.close, "terminal close");35 const closeTerminal = useAtomCommand(terminalEnvironment.close, "terminal close");
37 const openTerminal = useAtomCommand(terminalEnvironment.open, "terminal open");36 const openTerminal = useAtomCommand(terminalEnvironment.open, "terminal open");
38 const nativeTerminalAvailable = hasNativeTerminalSurface();37 const nativeTerminalAvailable = hasNativeTerminalSurface();
39 const iconColor = useThemeColor("--color-icon");
40 const terminalId = DEFAULT_TERMINAL_ID;38 const terminalId = DEFAULT_TERMINAL_ID;
41 const lastGridSizeRef = useRef<TerminalGridSize>({39 const lastGridSizeRef = useRef<TerminalGridSize>({
42 cols: DEFAULT_TERMINAL_COLS,40 cols: DEFAULT_TERMINAL_COLS,
...@@ -236,7 +234,12 @@ export const ThreadTerminalPanel = memo(function ThreadTerminalPanel(...@@ -236,7 +234,12 @@ export const ThreadTerminalPanel = memo(function ThreadTerminalPanel(
236 className="h-8 w-8 items-center justify-center rounded-[8px] bg-subtle"234 className="h-8 w-8 items-center justify-center rounded-[8px] bg-subtle"
237 onPress={props.onClose}235 onPress={props.onClose}
238 >236 >
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 />
240 </Pressable>243 </Pressable>
241 </View>244 </View>
242 </View>245 </View>
apps/mobile/src/features/threads/ComposerCommandPopover.tsx+14-17
...@@ -5,13 +5,12 @@ import {...@@ -5,13 +5,12 @@ import {
5import type { ServerProviderSkill, ServerProviderSlashCommand } from "@t3tools/contracts";5import type { ServerProviderSkill, ServerProviderSlashCommand } from "@t3tools/contracts";
6import type { ComposerTriggerKind } from "@t3tools/shared/composerTrigger";6import type { ComposerTriggerKind } from "@t3tools/shared/composerTrigger";
7import { memo } from "react";7import { memo } from "react";
8import { Pressable, ScrollView, View, type ViewStyle } from "react-native";8import { Pressable, ScrollView, StyleSheet, View, type ViewStyle } from "react-native";
99
10import { SymbolView, type AppSymbolName } from "../../components/AppSymbol";10import { SymbolView, type AppSymbolName } from "../../components/AppSymbol";
11import { AppText as Text } from "../../components/AppText";11import { AppText as Text } from "../../components/AppText";
12import { GlassSurface } from "../../components/GlassSurface";12import { GlassSurface } from "../../components/GlassSurface";
13import { PierreEntryIcon } from "../../components/PierreEntryIcon";13import { PierreEntryIcon } from "../../components/PierreEntryIcon";
14import { useThemeColor } from "../../lib/useThemeColor";
15export type ComposerCommandItem =14export type ComposerCommandItem =
16 | {15 | {
17 readonly id: string;16 readonly id: string;
...@@ -51,7 +50,6 @@ interface ComposerCommandPopoverProps {...@@ -51,7 +50,6 @@ interface ComposerCommandPopoverProps {
51}50}
5251
53function PopoverSurface(props: { readonly children: React.ReactNode; readonly style?: ViewStyle }) {52function PopoverSurface(props: { readonly children: React.ReactNode; readonly style?: ViewStyle }) {
54 const tintColor = useThemeColor("--color-glass-surface");
55 const baseStyle: ViewStyle = {53 const baseStyle: ViewStyle = {
56 borderRadius: 16,54 borderRadius: 16,
57 overflow: "hidden",55 overflow: "hidden",
...@@ -59,7 +57,11 @@ function PopoverSurface(props: { readonly children: React.ReactNode; readonly st...@@ -59,7 +57,11 @@ function PopoverSurface(props: { readonly children: React.ReactNode; readonly st
59 };57 };
6058
61 return (59 return (
62 <GlassSurface glassEffectStyle="clear" tintColor={tintColor} style={baseStyle}>60 <GlassSurface
61 glassEffectStyle="clear"
62 tintColorClassName="accent-glass-surface"
63 style={baseStyle}
64 >
63 {props.children}65 {props.children}
64 </GlassSurface>66 </GlassSurface>
65 );67 );
...@@ -122,27 +124,22 @@ const CommandRow = memo(function CommandRow(props: {...@@ -122,27 +124,22 @@ const CommandRow = memo(function CommandRow(props: {
122 readonly isSlashSkill: boolean;124 readonly isSlashSkill: boolean;
123}) {125}) {
124 const iconName = itemIcon(props.item);126 const iconName = itemIcon(props.item);
125 const iconColor = useThemeColor("--color-icon-subtle");
126 const borderColor = useThemeColor("--color-border");
127127
128 return (128 return (
129 <Pressable129 <Pressable
130 onPress={props.onPress}130 onPress={props.onPress}
131 style={({ pressed }) => ({131 className="flex-row items-center gap-2.5 border-border px-3.5 py-2.5 active:opacity-60"
132 flexDirection: "row",132 style={{ borderBottomWidth: props.isLast ? 0 : StyleSheet.hairlineWidth }}
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 })}
141 >133 >
142 {props.item.type === "path" ? (134 {props.item.type === "path" ? (
143 <PierreEntryIcon path={props.item.path} kind={props.item.kind} size={16} />135 <PierreEntryIcon path={props.item.path} kind={props.item.kind} size={16} />
144 ) : iconName ? (136 ) : 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 />
146 ) : null}143 ) : null}
147 <Text className="shrink-0 text-base font-t3-medium text-foreground" numberOfLines={1}>144 <Text className="shrink-0 text-base font-t3-medium text-foreground" numberOfLines={1}>
148 {props.isSlashSkill && props.item.type === "skill" ? (145 {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";...@@ -2,7 +2,7 @@ import * as Haptics from "expo-haptics";
2import { isLiquidGlassSupported, LiquidGlassView } from "@callstack/liquid-glass";2import { isLiquidGlassSupported, LiquidGlassView } from "@callstack/liquid-glass";
3import { SymbolView } from "../../components/AppSymbol";3import { SymbolView } from "../../components/AppSymbol";
4import { useCallback, useEffect, useRef } from "react";4import { useCallback, useEffect, useRef } from "react";
5import { ActivityIndicator, Pressable, StyleSheet, View } from "react-native";5import { ActivityIndicator, Pressable, StyleSheet, useColorScheme, View } from "react-native";
6import Animated, { FadeIn, FadeOut, LinearTransition } from "react-native-reanimated";6import Animated, { FadeIn, FadeOut, LinearTransition } from "react-native-reanimated";
7import { useSafeAreaInsets } from "react-native-safe-area-context";7import { useSafeAreaInsets } from "react-native-safe-area-context";
88
...@@ -10,9 +10,8 @@ import { AppText as Text } from "../../components/AppText";...@@ -10,9 +10,8 @@ import { AppText as Text } from "../../components/AppText";
10import { APP_BAR_HEIGHT } from "../../lib/layoutMetrics";10import { APP_BAR_HEIGHT } from "../../lib/layoutMetrics";
11import { themeColorWithAlpha } from "../../lib/mobileTheme";11import { themeColorWithAlpha } from "../../lib/mobileTheme";
12import { tryOpenExternalUrl } from "../../lib/openExternalUrl";12import { tryOpenExternalUrl } from "../../lib/openExternalUrl";
13import { useThemeColor } from "../../lib/useThemeColor";13import { useUniwindTheme } from "../../lib/useUniwindTheme";
14import type { GitActionProgress } from "../../state/use-vcs-action-state";14import type { GitActionProgress } from "../../state/use-vcs-action-state";
15import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
1615
17const OVERLAY_LAYOUT_TRANSITION = LinearTransition.duration(220);16const OVERLAY_LAYOUT_TRANSITION = LinearTransition.duration(220);
18const OVERLAY_TOP_GAP = 8;17const OVERLAY_TOP_GAP = 8;
...@@ -68,17 +67,16 @@ export function GitActionProgressOverlay(props: {...@@ -68,17 +67,16 @@ export function GitActionProgressOverlay(props: {
6867
69function OverlayContent(props: { readonly progress: GitActionProgress }) {68function OverlayContent(props: { readonly progress: GitActionProgress }) {
70 const { progress } = props;69 const { progress } = props;
71 const iconColor = useThemeColor("--color-icon");70 const colors = useUniwindTheme();
72 const glassSurface = useThemeColor("--color-glass-surface");71 const glassSurface = colors["--color-glass-surface"];
73 const foreground = useThemeColor("--color-foreground");72 const foreground = colors["--color-foreground"];
74 const shadowColor = useThemeColor("--color-primary-shadow");73 const shadowColor = colors["--color-primary-shadow"];
75 const { themeAppearance } = useAppearancePreferences();74 const isDarkMode = useColorScheme() === "dark";
76 const isDarkMode = themeAppearance === "dark";
77 const glassTint = themeColorWithAlpha(String(glassSurface), isDarkMode ? 0.48 : 0.38);75 const glassTint = themeColorWithAlpha(String(glassSurface), isDarkMode ? 0.48 : 0.38);
78 const glassBorder = themeColorWithAlpha(String(foreground), isDarkMode ? 0.18 : 0.12);76 const glassBorder = themeColorWithAlpha(String(foreground), isDarkMode ? 0.18 : 0.12);
79 const content = (77 const content = (
80 <>78 <>
81 <OverlayIcon phase={progress.phase} iconColor={iconColor} />79 <OverlayIcon phase={progress.phase} />
8280
83 <View className="flex-1 gap-0.5">81 <View className="flex-1 gap-0.5">
84 {progress.label ? (82 {progress.label ? (
...@@ -94,7 +92,12 @@ function OverlayContent(props: { readonly progress: GitActionProgress }) {...@@ -94,7 +92,12 @@ function OverlayContent(props: { readonly progress: GitActionProgress }) {
94 </View>92 </View>
9593
96 {progress.prUrl ? (94 {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 />
98 ) : null}101 ) : null}
99 </>102 </>
100 );103 );
...@@ -140,7 +143,7 @@ function OverlayContent(props: { readonly progress: GitActionProgress }) {...@@ -140,7 +143,7 @@ function OverlayContent(props: { readonly progress: GitActionProgress }) {
140143
141 const bgClass =144 const bgClass =
142 progress.phase === "error"145 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"
144 : "bg-card border-border";147 : "bg-card border-border";
145148
146 return (149 return (
...@@ -153,13 +156,10 @@ function OverlayContent(props: { readonly progress: GitActionProgress }) {...@@ -153,13 +156,10 @@ function OverlayContent(props: { readonly progress: GitActionProgress }) {
153 );156 );
154}157}
155158
156function OverlayIcon(props: {159function OverlayIcon(props: { readonly phase: GitActionProgress["phase"] }) {
157 readonly phase: GitActionProgress["phase"];
158 readonly iconColor: ReturnType<typeof useThemeColor>;
159}) {
160 switch (props.phase) {160 switch (props.phase) {
161 case "running":161 case "running":
162 return <ActivityIndicator size="small" />;162 return <ActivityIndicator size="small" colorClassName={"accent-icon"} />;
163 case "success":163 case "success":
164 return (164 return (
165 <View className="h-6 w-6 items-center justify-center rounded-full bg-green-500">165 <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";...@@ -24,7 +24,7 @@ import { AppText as Text } from "../../components/AppText";
24import { ThemedSwitch } from "../../components/ThemedSwitch";24import { ThemedSwitch } from "../../components/ThemedSwitch";
25import { cn } from "../../lib/cn";25import { cn } from "../../lib/cn";
26import { useFontFamily } from "../../lib/useFontFamily";26import { useFontFamily } from "../../lib/useFontFamily";
27import { useThemeColor } from "../../lib/useThemeColor";27import { useUniwindTheme } from "../../lib/useUniwindTheme";
28import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader";28import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader";
29import { useAtomCommand } from "../../state/use-atom-command";29import { useAtomCommand } from "../../state/use-atom-command";
30import { vcsEnvironment } from "../../state/vcs";30import { vcsEnvironment } from "../../state/vcs";
...@@ -45,9 +45,6 @@ function SelectionRow(props: {...@@ -45,9 +45,6 @@ function SelectionRow(props: {
45 readonly subtitle?: string;45 readonly subtitle?: string;
46 readonly title: string;46 readonly title: string;
47}) {47}) {
48 const iconColor = useThemeColor("--color-icon-muted");
49 const checkmarkColor = useThemeColor("--color-icon");
50
51 return (48 return (
52 <Pressable49 <Pressable
53 accessibilityLabel={[props.title, props.subtitle].filter(Boolean).join(", ")}50 accessibilityLabel={[props.title, props.subtitle].filter(Boolean).join(", ")}
...@@ -62,7 +59,12 @@ function SelectionRow(props: {...@@ -62,7 +59,12 @@ function SelectionRow(props: {
62 style={{ opacity: props.disabled ? 0.45 : 1 }}59 style={{ opacity: props.disabled ? 0.45 : 1 }}
63 >60 >
64 {props.icon ? (61 {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 />
66 ) : null}68 ) : null}
67 <View className="min-w-0 flex-1 gap-0.5">69 <View className="min-w-0 flex-1 gap-0.5">
68 <Text className="text-base font-t3-medium text-foreground" numberOfLines={1}>70 <Text className="text-base font-t3-medium text-foreground" numberOfLines={1}>
...@@ -78,7 +80,7 @@ function SelectionRow(props: {...@@ -78,7 +80,7 @@ function SelectionRow(props: {
78 <SymbolView80 <SymbolView
79 name="checkmark"81 name="checkmark"
80 size={16}82 size={16}
81 tintColor={checkmarkColor}83 tintColorClassName={"accent-icon"}
82 type="monochrome"84 type="monochrome"
83 weight="semibold"85 weight="semibold"
84 />86 />
...@@ -191,8 +193,7 @@ export function NewTaskBranchPickerRouteScreen() {...@@ -191,8 +193,7 @@ export function NewTaskBranchPickerRouteScreen() {
191 const flow = useNewTaskFlow();193 const flow = useNewTaskFlow();
192 const navigation = useNavigation();194 const navigation = useNavigation();
193 const insets = useSafeAreaInsets();195 const insets = useSafeAreaInsets();
194 const placeholderColor = useThemeColor("--color-placeholder");196 const foregroundColor = useUniwindTheme()["--color-foreground"];
195 const foregroundColor = useThemeColor("--color-foreground");
196 const fontFamily = useFontFamily("regular");197 const fontFamily = useFontFamily("regular");
197 const switchRef = useAtomCommand(vcsEnvironment.switchRef, { reportFailure: false });198 const switchRef = useAtomCommand(vcsEnvironment.switchRef, { reportFailure: false });
198 const [switchingBranchName, setSwitchingBranchName] = useState<string | null>(null);199 const [switchingBranchName, setSwitchingBranchName] = useState<string | null>(null);
...@@ -419,7 +420,7 @@ export function NewTaskBranchPickerRouteScreen() {...@@ -419,7 +420,7 @@ export function NewTaskBranchPickerRouteScreen() {
419 className="h-11 rounded-xl bg-card px-4 text-base text-foreground"420 className="h-11 rounded-xl bg-card px-4 text-base text-foreground"
420 onChangeText={flow.setBranchQuery}421 onChangeText={flow.setBranchQuery}
421 placeholder="Find a branch"422 placeholder="Find a branch"
422 placeholderTextColor={placeholderColor}423 placeholderTextColorClassName={"accent-placeholder"}
423 style={{ color: foregroundColor, fontFamily }}424 style={{ color: foregroundColor, fontFamily }}
424 value={flow.branchQuery}425 value={flow.branchQuery}
425 />426 />
apps/mobile/src/features/threads/NewTaskDraftScreen.tsx+24-26
...@@ -13,8 +13,7 @@ import {...@@ -13,8 +13,7 @@ import {
13 useKeyboardState,13 useKeyboardState,
14} from "react-native-keyboard-controller";14} from "react-native-keyboard-controller";
15import { useSafeAreaInsets } from "react-native-safe-area-context";15import { useSafeAreaInsets } from "react-native-safe-area-context";
16import { useThemeColor } from "../../lib/useThemeColor";16import { useUniwindTheme } from "../../lib/useUniwindTheme";
17import { themeColorWithAlpha } from "../../lib/mobileTheme";
18import { useFontFamily } from "../../lib/useFontFamily";17import { useFontFamily } from "../../lib/useFontFamily";
1918
20import {19import {
...@@ -43,7 +42,6 @@ import {...@@ -43,7 +42,6 @@ import {
43import { makeTurnCommandMetadata } from "../../lib/commandMetadata";42import { makeTurnCommandMetadata } from "../../lib/commandMetadata";
44import { convertPastedImagesToAttachments, pickComposerImages } from "../../lib/composerImages";43import { convertPastedImagesToAttachments, pickComposerImages } from "../../lib/composerImages";
45import { useScaledTextRole } from "../settings/appearance/useScaledTextRole";44import { useScaledTextRole } from "../settings/appearance/useScaledTextRole";
46import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
47import {45import {
48 clearComposerDraftContent,46 clearComposerDraftContent,
49 getComposerDraftSnapshot,47 getComposerDraftSnapshot,
...@@ -71,17 +69,32 @@ function NewTaskWorkspaceIcon(props: {...@@ -71,17 +69,32 @@ function NewTaskWorkspaceIcon(props: {
71 readonly workspaceMode: "local" | "worktree";69 readonly workspaceMode: "local" | "worktree";
72 readonly worktreePath: string | null;70 readonly worktreePath: string | null;
73}) {71}) {
74 const iconColor = useThemeColor("--color-icon-muted");
75
76 if (props.workspaceMode === "local" && props.worktreePath === null) {72 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 );
78 }81 }
7982
80 return (83 return (
81 <View className="size-4">84 <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 />
83 <View className="absolute -right-1 -bottom-1">91 <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 />
85 </View>98 </View>
86 </View>99 </View>
87 );100 );
...@@ -109,7 +122,6 @@ export function NewTaskDraftScreen(props: {...@@ -109,7 +122,6 @@ export function NewTaskDraftScreen(props: {
109 reserveShare,122 reserveShare,
110 } = useIncomingShare();123 } = useIncomingShare();
111 const insets = useSafeAreaInsets();124 const insets = useSafeAreaInsets();
112 const { themeAppearance: colorScheme } = useAppearancePreferences();
113 const isKeyboardVisible = useKeyboardState((state) => state.isVisible);125 const isKeyboardVisible = useKeyboardState((state) => state.isVisible);
114 const controlsBottomPadding = Math.max(insets.bottom, 10);126 const controlsBottomPadding = Math.max(insets.bottom, 10);
115 const keyboardOpenedOffset = Math.max(0, controlsBottomPadding - 8);127 const keyboardOpenedOffset = Math.max(0, controlsBottomPadding - 8);
...@@ -291,13 +303,9 @@ export function NewTaskDraftScreen(props: {...@@ -291,13 +303,9 @@ export function NewTaskDraftScreen(props: {
291 };303 };
292 }, [props.pendingTaskId, cancelEditingPendingTask]);304 }, [props.pendingTaskId, cancelEditingPendingTask]);
293305
294 const foregroundColor = useThemeColor("--color-foreground");306 const foregroundColor = useUniwindTheme()["--color-foreground"];
295 const sheetColor = String(useThemeColor("--color-sheet"));
296 const projectUnderlineColor = useThemeColor("--color-foreground-muted");
297 const regularFontFamily = useFontFamily("regular");307 const regularFontFamily = useFontFamily("regular");
298 const bodyText = useScaledTextRole("body");308 const bodyText = useScaledTextRole("body");
299 const sheetFadeOpaque = sheetColor;
300 const sheetFadeTransparent = themeColorWithAlpha(sheetColor, 0);
301309
302 // A new navigation to this mounted screen delivers a fresh initialProjectRef310 // A new navigation to this mounted screen delivers a fresh initialProjectRef
303 // reference — treat it as a new request and let it apply again.311 // reference — treat it as a new request and let it apply again.
...@@ -797,7 +805,6 @@ export function NewTaskDraftScreen(props: {...@@ -797,7 +805,6 @@ export function NewTaskDraftScreen(props: {
797 }805 }
798806
799 const isAndroid = Platform.OS === "android";807 const isAndroid = Platform.OS === "android";
800 const isDarkMode = colorScheme === "dark";
801 const canStart =808 const canStart =
802 Boolean(flow.selectedProject) &&809 Boolean(flow.selectedProject) &&
803 Boolean(flow.selectedModel) &&810 Boolean(flow.selectedModel) &&
...@@ -874,11 +881,7 @@ export function NewTaskDraftScreen(props: {...@@ -874,11 +881,7 @@ export function NewTaskDraftScreen(props: {
874 accessibilityRole="button"881 accessibilityRole="button"
875 disabled={isIncomingShareTransferPending}882 disabled={isIncomingShareTransferPending}
876 onPress={chooseProject}883 onPress={chooseProject}
877 className="min-w-0 max-w-[250px] active:opacity-65"884 className="min-w-0 max-w-[250px] border-b border-foreground-muted active:opacity-65"
878 style={{
879 borderBottomColor: projectUnderlineColor,
880 borderBottomWidth: 1,
881 }}
882 >885 >
883 <Text886 <Text
884 className="text-2xl font-t3-medium tracking-tight text-foreground"887 className="text-2xl font-t3-medium tracking-tight text-foreground"
...@@ -960,7 +963,6 @@ export function NewTaskDraftScreen(props: {...@@ -960,7 +963,6 @@ export function NewTaskDraftScreen(props: {
960963
961 <ComposerSurface964 <ComposerSurface
962 animateLayout={false}965 animateLayout={false}
963 isDarkMode={isDarkMode}
964 style={{966 style={{
965 borderRadius: 26,967 borderRadius: 26,
966 minHeight: 140,968 minHeight: 140,
...@@ -984,11 +986,7 @@ export function NewTaskDraftScreen(props: {...@@ -984,11 +986,7 @@ export function NewTaskDraftScreen(props: {
984 {promptEditor}986 {promptEditor}
985987
986 <ComposerToolbarRow paddingBottom={0} paddingHorizontal={0} paddingTop={4}>988 <ComposerToolbarRow paddingBottom={0} paddingHorizontal={0} paddingTop={4}>
987 <ComposerToolbarScroller989 <ComposerToolbarScroller contentPaddingRight={8} fadeSurface="sheet">
988 fadeOpaque={sheetFadeOpaque}
989 fadeTransparent={sheetFadeTransparent}
990 contentPaddingRight={8}
991 >
992 <ComposerToolbarButton990 <ComposerToolbarButton
993 accessibilityLabel="Add attachment"991 accessibilityLabel="Add attachment"
994 disabled={isIncomingShareTransferPending}992 disabled={isIncomingShareTransferPending}
apps/mobile/src/features/threads/NewTaskRouteScreen.tsx+4-5
...@@ -10,7 +10,6 @@ import type { EnvironmentProject } from "@t3tools/client-runtime/state/shell";...@@ -10,7 +10,6 @@ import type { EnvironmentProject } from "@t3tools/client-runtime/state/shell";
10import { useEffect, useRef } from "react";10import { useEffect, useRef } from "react";
11import { ActivityIndicator, Alert, Platform, Pressable, ScrollView, View } from "react-native";11import { ActivityIndicator, Alert, Platform, Pressable, ScrollView, View } from "react-native";
12import { useSafeAreaInsets } from "react-native-safe-area-context";12import { useSafeAreaInsets } from "react-native-safe-area-context";
13import { useThemeColor } from "../../lib/useThemeColor";
14import { cn } from "../../lib/cn";13import { cn } from "../../lib/cn";
1514
16import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";15import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";
...@@ -91,8 +90,6 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara...@@ -91,8 +90,6 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara
91 const isFocused = useIsFocused();90 const isFocused = useIsFocused();
92 const { layout } = useAdaptiveWorkspaceLayout();91 const { layout } = useAdaptiveWorkspaceLayout();
93 const insets = useSafeAreaInsets();92 const insets = useSafeAreaInsets();
94 const chevronColor = useThemeColor("--color-chevron");
95 const accentColor = useThemeColor("--color-icon-muted");
96 const { getShare, releaseShareReservation } = useIncomingShare();93 const { getShare, releaseShareReservation } = useIncomingShare();
97 const routeShareId = Array.isArray(route.params?.incomingShareId)94 const routeShareId = Array.isArray(route.params?.incomingShareId)
98 ? route.params.incomingShareId[0]95 ? route.params.incomingShareId[0]
...@@ -242,7 +239,9 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara...@@ -242,7 +239,9 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara
242 >239 >
243 {projectScopes.length === 0 ? (240 {projectScopes.length === 0 ? (
244 <View collapsable={false} className="items-center gap-3 rounded-[24px] bg-card px-6 py-8">241 <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}
246 <Text className="text-center text-lg font-t3-bold text-foreground">245 <Text className="text-center text-lg font-t3-bold text-foreground">
247 {projectEmptyState.title}246 {projectEmptyState.title}
248 </Text>247 </Text>
...@@ -308,7 +307,7 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara...@@ -308,7 +307,7 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara
308 <SymbolView307 <SymbolView
309 name="chevron.right"308 name="chevron.right"
310 size={14}309 size={14}
311 tintColor={chevronColor}310 tintColorClassName={"accent-chevron"}
312 type="monochrome"311 type="monochrome"
313 />312 />
314 </Pressable>313 </Pressable>
apps/mobile/src/features/threads/PendingApprovalCard.tsx+8-8
...@@ -28,15 +28,15 @@ export function PendingApprovalCard(props: PendingApprovalCardProps) {...@@ -28,15 +28,15 @@ export function PendingApprovalCard(props: PendingApprovalCardProps) {
28 // Opaque for the same reason as PendingUserInputCard: nothing blurs the feed28 // Opaque for the same reason as PendingUserInputCard: nothing blurs the feed
29 // behind this card, so a translucent surface bleeds messages through it.29 // behind this card, so a translucent surface bleeds messages through it.
30 return (30 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">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-sky-700 dark:text-sky-300">32 <Text className="font-t3-bold text-2xs uppercase tracking-[1.1px] text-adaptive-sky-700-300">
33 Approval needed33 Approval needed
34 </Text>34 </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">
36 {props.approval.appName ?? props.approval.requestKind}36 {props.approval.appName ?? props.approval.requestKind}
37 </Text>37 </Text>
38 {props.approval.detail ? (38 {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">
40 {props.approval.detail}40 {props.approval.detail}
41 </Text>41 </Text>
42 ) : null}42 ) : null}
...@@ -48,8 +48,8 @@ export function PendingApprovalCard(props: PendingApprovalCardProps) {...@@ -48,8 +48,8 @@ export function PendingApprovalCard(props: PendingApprovalCardProps) {
48 option.decision === "accept"48 option.decision === "accept"
49 ? "bg-blue-500"49 ? "bg-blue-500"
50 : option.decision === "decline"50 : option.decision === "decline"
51 ? "bg-rose-100 dark:bg-rose-500/18"51 ? "bg-adaptive-rose-100-500-a18"
52 : "bg-neutral-200 dark:bg-neutral-800"52 : "bg-adaptive-neutral-200-800"
53 }`}53 }`}
54 disabled={props.respondingApprovalId === props.approval.requestId}54 disabled={props.respondingApprovalId === props.approval.requestId}
55 onPress={() => void props.onRespond(props.approval.requestId, option.decision)}55 onPress={() => void props.onRespond(props.approval.requestId, option.decision)}
...@@ -59,8 +59,8 @@ export function PendingApprovalCard(props: PendingApprovalCardProps) {...@@ -59,8 +59,8 @@ export function PendingApprovalCard(props: PendingApprovalCardProps) {
59 option.decision === "accept"59 option.decision === "accept"
60 ? "font-t3-extrabold text-white"60 ? "font-t3-extrabold text-white"
61 : option.decision === "decline"61 : option.decision === "decline"
62 ? "font-t3-bold text-rose-700 dark:text-rose-300"62 ? "font-t3-bold text-adaptive-rose-700-300"
63 : "font-t3-bold text-neutral-950 dark:text-neutral-50"63 : "font-t3-bold text-adaptive-neutral-950-50"
64 }`}64 }`}
65 >65 >
66 {option.label}66 {option.label}
apps/mobile/src/features/threads/PendingUserInputCard.tsx+28-20
...@@ -18,7 +18,6 @@ import { SymbolView } from "../../components/AppSymbol";...@@ -18,7 +18,6 @@ import { SymbolView } from "../../components/AppSymbol";
18import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText";18import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText";
19import { ControlPill } from "../../components/ControlPill";19import { ControlPill } from "../../components/ControlPill";
20import { cn } from "../../lib/cn";20import { cn } from "../../lib/cn";
21import { useThemeColor } from "../../lib/useThemeColor";
22import {21import {
23 isPendingUserInputOptionSelected,22 isPendingUserInputOptionSelected,
24 type PendingUserInput,23 type PendingUserInput,
...@@ -87,7 +86,6 @@ const EXPANDED_CARD_IS_OVERLAY = Platform.OS === "ios";...@@ -87,7 +86,6 @@ const EXPANDED_CARD_IS_OVERLAY = Platform.OS === "ios";
87const CARD_LAYOUT_TRANSITION = LinearTransition.duration(200);86const CARD_LAYOUT_TRANSITION = LinearTransition.duration(200);
8887
89export function PendingUserInputCard(props: PendingUserInputCardProps) {88export function PendingUserInputCard(props: PendingUserInputCardProps) {
90 const iconSubtle = useThemeColor("--color-icon-subtle");
91 const questionCount = props.pendingUserInput.questions.length;89 const questionCount = props.pendingUserInput.questions.length;
9290
93 const cardCoverage = props.cardCoverage;91 const cardCoverage = props.cardCoverage;
...@@ -163,7 +161,7 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {...@@ -163,7 +161,7 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {
163 pointerEvents={props.collapsed ? "auto" : "none"}161 pointerEvents={props.collapsed ? "auto" : "none"}
164 accessibilityElementsHidden={!props.collapsed}162 accessibilityElementsHidden={!props.collapsed}
165 importantForAccessibility={props.collapsed ? "auto" : "no-hide-descendants"}163 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"
167 >165 >
168 <Pressable166 <Pressable
169 accessibilityRole="button"167 accessibilityRole="button"
...@@ -173,14 +171,19 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {...@@ -173,14 +171,19 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {
173 onPress={props.onToggleCollapsed}171 onPress={props.onToggleCollapsed}
174 className="min-h-10 flex-1 flex-row items-center gap-2 active:opacity-70"172 className="min-h-10 flex-1 flex-row items-center gap-2 active:opacity-70"
175 >173 >
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">
177 User input needed175 User input needed
178 </Text>176 </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">
180 {questionCount} question{questionCount === 1 ? "" : "s"}178 {questionCount} question{questionCount === 1 ? "" : "s"}
181 </Text>179 </Text>
182 <View className="flex-1" />180 <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 />
184 </Pressable>187 </Pressable>
185 {props.onStopThread ? (188 {props.onStopThread ? (
186 <ControlPill189 <ControlPill
...@@ -213,7 +216,7 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {...@@ -213,7 +216,7 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {
213 : FadeOutDown.duration(USER_INPUT_TOGGLE_DURATION_MS).easing(Easing.out(Easing.cubic))216 : FadeOutDown.duration(USER_INPUT_TOGGLE_DURATION_MS).easing(Easing.out(Easing.cubic))
214 }217 }
215 layout={CARD_LAYOUT_TRANSITION}218 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"
217 style={220 style={
218 EXPANDED_CARD_IS_OVERLAY221 EXPANDED_CARD_IS_OVERLAY
219 ? [{ maxHeight: props.maxHeight }, cardAnimatedStyle]222 ? [{ maxHeight: props.maxHeight }, cardAnimatedStyle]
...@@ -227,15 +230,20 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {...@@ -227,15 +230,20 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {
227 className="flex-row items-start gap-2"230 className="flex-row items-start gap-2"
228 >231 >
229 <View className="flex-1 gap-2.5">232 <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">
231 User input needed234 User input needed
232 </Text>235 </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">
234 Fill in the pending answers237 Fill in the pending answers
235 </Text>238 </Text>
236 </View>239 </View>
237 <View className="h-8 w-8 items-center justify-center rounded-full bg-neutral-200/70 dark:bg-white/8">240 <View className="h-8 w-8 items-center justify-center rounded-full bg-adaptive-neutral-200-a70-white-a8">
238 <SymbolView name="chevron.down" size={13} tintColor={iconSubtle} type="monochrome" />241 <SymbolView
242 name="chevron.down"
243 size={13}
244 tintColorClassName={"accent-icon-subtle"}
245 type="monochrome"
246 />
239 </View>247 </View>
240 </Pressable>248 </Pressable>
241 <ScrollView249 <ScrollView
...@@ -251,10 +259,10 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {...@@ -251,10 +259,10 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {
251 const draft = props.drafts[question.id];259 const draft = props.drafts[question.id];
252 return (260 return (
253 <View key={question.id} className="gap-2 pt-1">261 <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">
255 {question.header}263 {question.header}
256 </Text>264 </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">
258 {question.question}266 {question.question}
259 </Text>267 </Text>
260 <View className="gap-2">268 <View className="gap-2">
...@@ -268,8 +276,8 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {...@@ -268,8 +276,8 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {
268 className={cn(276 className={cn(
269 "min-h-12 w-full rounded-2xl border px-3.5 py-3",277 "min-h-12 w-full rounded-2xl border px-3.5 py-3",
270 selected278 selected
271 ? "border-blue-300/50 bg-blue-50 dark:border-blue-400/28 dark:bg-blue-400/14"279 ? "border-adaptive-blue-300-a50-blue-400-a28 bg-adaptive-blue-50-blue-400-a14"
272 : "border-neutral-200 bg-white dark:border-white/6 dark:bg-neutral-950/70",280 : "border-adaptive-neutral-200-white-a6 bg-adaptive-white-neutral-950-a70",
273 )}281 )}
274 onPress={() =>282 onPress={() =>
275 props.onSelectOption(283 props.onSelectOption(
...@@ -284,14 +292,14 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {...@@ -284,14 +292,14 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {
284 className={cn(292 className={cn(
285 "font-t3-bold text-sm",293 "font-t3-bold text-sm",
286 selected294 selected
287 ? "text-sky-700 dark:text-sky-300"295 ? "text-adaptive-sky-700-300"
288 : "text-neutral-700 dark:text-neutral-200",296 : "text-adaptive-neutral-600-300",
289 )}297 )}
290 >298 >
291 {option.label}299 {option.label}
292 </Text>300 </Text>
293 {description ? (301 {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">
295 {description}303 {description}
296 </Text>304 </Text>
297 ) : null}305 ) : null}
...@@ -308,7 +316,7 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {...@@ -308,7 +316,7 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {
308 onFocus={() => props.onInputFocusChange?.(true)}316 onFocus={() => props.onInputFocusChange?.(true)}
309 onBlur={() => props.onInputFocusChange?.(false)}317 onBlur={() => props.onInputFocusChange?.(false)}
310 placeholder="Or type a custom answer"318 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"
312 />320 />
313 </View>321 </View>
314 );322 );
...@@ -317,7 +325,7 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {...@@ -317,7 +325,7 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) {
317 <Pressable325 <Pressable
318 className={cn(326 className={cn(
319 "items-center justify-center rounded-2xl px-4 py-3.5",327 "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",
321 )}329 )}
322 disabled={330 disabled={
323 props.answers === null || props.respondingUserInputId === props.pendingUserInput.requestId331 props.answers === null || props.respondingUserInputId === props.pendingUserInput.requestId
apps/mobile/src/features/threads/ThreadComposer.tsx+9-46
...@@ -16,15 +16,7 @@ import {...@@ -16,15 +16,7 @@ import {
16import { StackActions, useFocusEffect, useNavigation } from "@react-navigation/native";16import { StackActions, useFocusEffect, useNavigation } from "@react-navigation/native";
17import type { ReactNode } from "react";17import type { ReactNode } from "react";
18import { memo, useCallback, useEffect, useMemo, useRef, useState, type RefObject } from "react";18import { memo, useCallback, useEffect, useMemo, useRef, useState, type RefObject } from "react";
19import {19import { ActivityIndicator, Image, Platform, Pressable, View, type ViewStyle } from "react-native";
20 ActivityIndicator,
21 Image,
22 Platform,
23 Pressable,
24 StyleSheet,
25 View,
26 type ViewStyle,
27} from "react-native";
28import ImageViewing from "react-native-image-viewing";20import ImageViewing from "react-native-image-viewing";
29import Animated, {21import Animated, {
30 FadeIn,22 FadeIn,
...@@ -33,8 +25,7 @@ import Animated, {...@@ -33,8 +25,7 @@ import Animated, {
33 FadeOutDown,25 FadeOutDown,
34 LinearTransition,26 LinearTransition,
35} from "react-native-reanimated";27} from "react-native-reanimated";
36import { useThemeColor } from "../../lib/useThemeColor";28import { useUniwindTheme } from "../../lib/useUniwindTheme";
37import { themeColorWithAlpha } from "../../lib/mobileTheme";
38import { armAgentAwarenessLiveActivityForLocalWork } from "../agent-awareness/remoteRegistration";29import { armAgentAwarenessLiveActivityForLocalWork } from "../agent-awareness/remoteRegistration";
39import { scopedThreadKey } from "../../lib/scopedEntities";30import { scopedThreadKey } from "../../lib/scopedEntities";
4031
...@@ -57,7 +48,6 @@ import { ProviderIcon } from "../../components/ProviderIcon";...@@ -57,7 +48,6 @@ import { ProviderIcon } from "../../components/ProviderIcon";
57import type { DraftComposerImageAttachment } from "../../lib/composerImages";48import type { DraftComposerImageAttachment } from "../../lib/composerImages";
58import { buildModelOptions, groupByProvider } from "../../lib/modelOptions";49import { buildModelOptions, groupByProvider } from "../../lib/modelOptions";
59import { useScaledTextRole } from "../settings/appearance/useScaledTextRole";50import { useScaledTextRole } from "../settings/appearance/useScaledTextRole";
60import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
61import type { RemoteClientConnectionState } from "../../lib/connection";51import type { RemoteClientConnectionState } from "../../lib/connection";
62import {52import {
63 insertRankedSearchResult,53 insertRankedSearchResult,
...@@ -142,19 +132,14 @@ const COMPOSER_LAYOUT_TRANSITION =...@@ -142,19 +132,14 @@ const COMPOSER_LAYOUT_TRANSITION =
142export function ComposerSurface(props: {132export function ComposerSurface(props: {
143 readonly children: ReactNode;133 readonly children: ReactNode;
144 readonly style: ViewStyle;134 readonly style: ViewStyle;
145 readonly isDarkMode: boolean;
146 /** Existing thread composers morph between pill and card layouts. */135 /** Existing thread composers morph between pill and card layouts. */
147 readonly animateLayout?: boolean;136 readonly animateLayout?: boolean;
148}) {137}) {
149 const cardColor = useThemeColor("--color-card-translucent");
150 const borderColor = useThemeColor("--color-border");
151 const shadowColor = useThemeColor("--color-primary-shadow");
152 // Drop shadow lives on a wrapper: `overflow: "hidden"` on the surface itself138 // Drop shadow lives on a wrapper: `overflow: "hidden"` on the surface itself
153 // (needed to clip content to the pill shape) would clip the shadow on iOS.139 // (needed to clip content to the pill shape) would clip the shadow on iOS.
154 const shadowStyle: ViewStyle = {140 const shadowStyle: ViewStyle = {
155 borderRadius: props.style.borderRadius,141 borderRadius: props.style.borderRadius,
156 shadowColor,142 shadowOpacity: 1,
157 shadowOpacity: props.isDarkMode ? 0.35 : 0.12,
158 shadowRadius: 14,143 shadowRadius: 14,
159 shadowOffset: { width: 0, height: 6 },144 shadowOffset: { width: 0, height: 6 },
160 elevation: 10,145 elevation: 10,
...@@ -162,16 +147,13 @@ export function ComposerSurface(props: {...@@ -162,16 +147,13 @@ export function ComposerSurface(props: {
162147
163 return (148 return (
164 <Animated.View149 <Animated.View
150 className="shadow-adaptive-black-a15-a35"
165 layout={props.animateLayout === false ? undefined : COMPOSER_LAYOUT_TRANSITION}151 layout={props.animateLayout === false ? undefined : COMPOSER_LAYOUT_TRANSITION}
166 style={shadowStyle}152 style={shadowStyle}
167 >153 >
168 <GlassSurface154 <GlassSurface
169 chrome="none"155 chrome="none"
170 fallbackStyle={{156 fallbackClassName="border border-border bg-card-translucent"
171 backgroundColor: cardColor,
172 borderWidth: 1,
173 borderColor,
174 }}
175 glassEffectStyle="regular"157 glassEffectStyle="regular"
176 // The composer is a passive material containing interactive controls.158 // The composer is a passive material containing interactive controls.
177 // Expo GlassView defaults to non-interactive and both layouts share it.159 // Expo GlassView defaults to non-interactive and both layouts share it.
...@@ -240,8 +222,6 @@ const ComposerConnectionStatusPill = memo(function ComposerConnectionStatusPill(...@@ -240,8 +222,6 @@ const ComposerConnectionStatusPill = memo(function ComposerConnectionStatusPill(
240 readonly status: ComposerStatusPillState;222 readonly status: ComposerStatusPillState;
241}) {223}) {
242 const isReconnecting = props.status.kind !== "unavailable";224 const isReconnecting = props.status.kind !== "unavailable";
243 const indicatorColor = useThemeColor("--color-icon-muted");
244
245 return (225 return (
246 <Animated.View226 <Animated.View
247 className="absolute inset-x-0 bottom-full items-center pb-2"227 className="absolute inset-x-0 bottom-full items-center pb-2"
...@@ -255,7 +235,7 @@ const ComposerConnectionStatusPill = memo(function ComposerConnectionStatusPill(...@@ -255,7 +235,7 @@ const ComposerConnectionStatusPill = memo(function ComposerConnectionStatusPill(
255 className="max-w-full flex-row items-center gap-2 rounded-full bg-card px-3 py-2 shadow-sm active:opacity-70"235 className="max-w-full flex-row items-center gap-2 rounded-full bg-card px-3 py-2 shadow-sm active:opacity-70"
256 >236 >
257 {isReconnecting ? (237 {isReconnecting ? (
258 <ActivityIndicator size="small" color={indicatorColor} />238 <ActivityIndicator size="small" colorClassName={"accent-icon-muted"} />
259 ) : (239 ) : (
260 <View className="h-2 w-2 rounded-full bg-red-500" />240 <View className="h-2 w-2 rounded-full bg-red-500" />
261 )}241 )}
...@@ -272,9 +252,7 @@ const ComposerConnectionStatusPill = memo(function ComposerConnectionStatusPill(...@@ -272,9 +252,7 @@ const ComposerConnectionStatusPill = memo(function ComposerConnectionStatusPill(
272252
273export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposerProps) {253export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposerProps) {
274 const navigation = useNavigation();254 const navigation = useNavigation();
275 const { themeAppearance } = useAppearancePreferences();255 const foregroundColor = useUniwindTheme()["--color-foreground"];
276 const isDarkMode = themeAppearance === "dark";
277 const foregroundColor = useThemeColor("--color-foreground");
278 const bodyText = useScaledTextRole("body");256 const bodyText = useScaledTextRole("body");
279 const fallbackInputRef = useRef<ComposerEditorHandle>(null);257 const fallbackInputRef = useRef<ComposerEditorHandle>(null);
280 const inputRef = props.editorRef ?? fallbackInputRef;258 const inputRef = props.editorRef ?? fallbackInputRef;
...@@ -342,11 +320,6 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer...@@ -342,11 +320,6 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
342 environmentLabel: props.environmentLabel,320 environmentLabel: props.environmentLabel,
343 threadSyncPhase: props.threadSyncPhase,321 threadSyncPhase: props.threadSyncPhase,
344 });322 });
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%)`;
350 const selectedProviderStatus = useMemo(() => {323 const selectedProviderStatus = useMemo(() => {
351 if (!props.serverConfig) return null;324 if (!props.serverConfig) return null;
352 return (325 return (
...@@ -723,13 +696,8 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer...@@ -723,13 +696,8 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
723 silently drops experimental_backgroundImage on Android, which left this696 silently drops experimental_backgroundImage on Android, which left this
724 strip fully transparent and the feed text legible through the composer. */}697 strip fully transparent and the feed text legible through the composer. */}
725 <View698 <View
699 className="absolute inset-0 bg-linear-to-b from-screen/0 via-screen/60 to-screen/90"
726 pointerEvents="none"700 pointerEvents="none"
727 style={[
728 StyleSheet.absoluteFill,
729 {
730 experimental_backgroundImage: backdropGradient,
731 },
732 ]}
733 />701 />
734 <Animated.View702 <Animated.View
735 className="relative w-full self-center"703 className="relative w-full self-center"
...@@ -755,7 +723,6 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer...@@ -755,7 +723,6 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
755 ) : null}723 ) : null}
756724
757 <ComposerSurface725 <ComposerSurface
758 isDarkMode={isDarkMode}
759 style={726 style={
760 isExpanded727 isExpanded
761 ? {728 ? {
...@@ -865,11 +832,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer...@@ -865,11 +832,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
865 ) : null}832 ) : null}
866 {isExpanded ? (833 {isExpanded ? (
867 <ComposerToolbarRow paddingBottom={0} paddingHorizontal={0} paddingTop={4}>834 <ComposerToolbarRow paddingBottom={0} paddingHorizontal={0} paddingTop={4}>
868 <ComposerToolbarScroller835 <ComposerToolbarScroller contentPaddingRight={8}>
869 fadeOpaque={toolbarFadeOpaque}
870 fadeTransparent={toolbarFadeTransparent}
871 contentPaddingRight={8}
872 >
873 <ComposerToolbarButton836 <ComposerToolbarButton
874 accessibilityLabel="Add attachment"837 accessibilityLabel="Add attachment"
875 icon="plus"838 icon="plus"
apps/mobile/src/features/threads/ThreadFeed.tsx+46-56
...@@ -46,7 +46,7 @@ import { TouchableOpacity } from "react-native-gesture-handler";...@@ -46,7 +46,7 @@ import { TouchableOpacity } from "react-native-gesture-handler";
46import ImageViewing from "react-native-image-viewing";46import ImageViewing from "react-native-image-viewing";
47import { useSafeAreaInsets } from "react-native-safe-area-context";47import { useSafeAreaInsets } from "react-native-safe-area-context";
48import Animated, { FadeIn, FadeInUp, type SharedValue } from "react-native-reanimated";48import Animated, { FadeIn, FadeInUp, type SharedValue } from "react-native-reanimated";
49import { useThemeColor } from "../../lib/useThemeColor";49import { useUniwindTheme } from "../../lib/useUniwindTheme";
50import { IOS_NAV_BAR_HEIGHT } from "../../lib/layoutMetrics";50import { IOS_NAV_BAR_HEIGHT } from "../../lib/layoutMetrics";
51import { useFontFamily } from "../../lib/useFontFamily";51import { useFontFamily } from "../../lib/useFontFamily";
52import { scopedThreadKey } from "../../lib/scopedEntities";52import { scopedThreadKey } from "../../lib/scopedEntities";
...@@ -209,7 +209,6 @@ function ThreadMarkdownImageView(props: {...@@ -209,7 +209,6 @@ function ThreadMarkdownImageView(props: {
209 readonly alt: string | null;209 readonly alt: string | null;
210 readonly onPressImage: (uri: string) => void;210 readonly onPressImage: (uri: string) => void;
211}) {211}) {
212 const codeBackground = useThemeColor("--color-md-code-bg");
213 const [availableWidth, setAvailableWidth] = useState(0);212 const [availableWidth, setAvailableWidth] = useState(0);
214 const [sourceSize, setSourceSize] = useState<{ width: number; height: number } | null>(null);213 const [sourceSize, setSourceSize] = useState<{ width: number; height: number } | null>(null);
215 const [failedUri, setFailedUri] = useState<string | null>(null);214 const [failedUri, setFailedUri] = useState<string | null>(null);
...@@ -242,12 +241,9 @@ function ThreadMarkdownImageView(props: {...@@ -242,12 +241,9 @@ function ThreadMarkdownImageView(props: {
242 >241 >
243 {props.uri === null || failed ? (242 {props.uri === null || failed ? (
244 <View243 <View
244 className="items-center justify-center rounded-[10px] bg-md-code-bg"
245 style={{245 style={{
246 ...frameStyle,246 ...frameStyle,
247 borderRadius: 10,
248 backgroundColor: codeBackground,
249 alignItems: "center",
250 justifyContent: "center",
251 }}247 }}
252 >248 >
253 {failed ? (249 {failed ? (
...@@ -265,13 +261,9 @@ function ThreadMarkdownImageView(props: {...@@ -265,13 +261,9 @@ function ThreadMarkdownImageView(props: {
265 style={{ alignSelf: "flex-start" }}261 style={{ alignSelf: "flex-start" }}
266 >262 >
267 <View263 <View
264 className="items-center justify-center overflow-hidden rounded-[10px] bg-md-code-bg"
268 style={{265 style={{
269 ...frameStyle,266 ...frameStyle,
270 borderRadius: 10,
271 backgroundColor: codeBackground,
272 alignItems: "center",
273 justifyContent: "center",
274 overflow: "hidden",
275 }}267 }}
276 >268 >
277 <ThreadMarkdownImageRequest269 <ThreadMarkdownImageRequest
...@@ -556,23 +548,18 @@ function MarkdownCodeBlock(props: {...@@ -556,23 +548,18 @@ function MarkdownCodeBlock(props: {
556}548}
557549
558function useReviewCommentColors(): ReviewCommentColors {550function useReviewCommentColors(): ReviewCommentColors {
559 const background = useThemeColor("--color-card");551 const theme = useUniwindTheme();
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");
565552
566 return useMemo(553 return useMemo(
567 () => ({554 () => ({
568 background,555 background: theme["--color-card"],
569 border,556 border: theme["--color-border"],
570 mutedBackground,557 mutedBackground: theme["--color-subtle"],
571 text,558 text: theme["--color-foreground"],
572 mutedText,559 mutedText: theme["--color-foreground-muted"],
573 codeBackground,560 codeBackground: theme["--color-md-code-bg"],
574 }),561 }),
575 [background, border, codeBackground, mutedBackground, mutedText, text],562 [theme],
576 );563 );
577}564}
578565
...@@ -590,25 +577,26 @@ function useMarkdownStyles(...@@ -590,25 +577,26 @@ function useMarkdownStyles(
590 [appearance.baseFontSize],577 [appearance.baseFontSize],
591 );578 );
592 const themeMode = themeAppearance;579 const themeMode = themeAppearance;
593 const markdownBodyColor = String(useThemeColor("--color-md-body"));580 const theme = useUniwindTheme();
594 const markdownStrongColor = String(useThemeColor("--color-md-strong"));581 const markdownBodyColor = theme["--color-md-body"];
595 const markdownLinkColor = String(useThemeColor("--color-md-link"));582 const markdownStrongColor = theme["--color-md-strong"];
596 const markdownBlockquoteBg = String(useThemeColor("--color-md-blockquote-bg"));583 const markdownLinkColor = theme["--color-md-link"];
597 const markdownBlockquoteBorder = String(useThemeColor("--color-md-blockquote-border"));584 const markdownBlockquoteBg = theme["--color-md-blockquote-bg"];
598 const markdownCodeBg = String(useThemeColor("--color-md-code-bg"));585 const markdownBlockquoteBorder = theme["--color-md-blockquote-border"];
599 const markdownCodeText = String(useThemeColor("--color-md-code-text"));586 const markdownCodeBg = theme["--color-md-code-bg"];
600 const markdownInlineCodeText = String(useThemeColor("--color-foreground-secondary"));587 const markdownCodeText = theme["--color-md-code-text"];
601 const markdownHrColor = String(useThemeColor("--color-md-hr"));588 const markdownInlineCodeText = theme["--color-foreground-secondary"];
602 const markdownUserBodyColor = String(useThemeColor("--color-user-bubble-foreground"));589 const markdownHrColor = theme["--color-md-hr"];
603 const markdownUserCodeBg = String(useThemeColor("--color-md-user-code-bg"));590 const markdownUserBodyColor = theme["--color-user-bubble-foreground"];
604 const markdownUserCodeText = String(useThemeColor("--color-md-user-code-text"));591 const markdownUserCodeBg = theme["--color-md-user-code-bg"];
605 const markdownUserInlineCodeText = String(useThemeColor("--color-user-bubble-foreground-muted"));592 const markdownUserCodeText = theme["--color-md-user-code-text"];
606 const markdownUserFenceBg = String(useThemeColor("--color-md-user-fence-bg"));593 const markdownUserInlineCodeText = theme["--color-user-bubble-foreground-muted"];
607 const markdownUserFenceText = String(useThemeColor("--color-md-user-fence-text"));594 const markdownUserFenceBg = theme["--color-md-user-fence-bg"];
608 const iconSubtleColor = String(useThemeColor("--color-icon-subtle"));595 const markdownUserFenceText = theme["--color-md-user-fence-text"];
609 const inlineSkillForeground = String(useThemeColor("--color-inline-skill-foreground"));596 const iconSubtleColor = theme["--color-icon-subtle"];
610 const userBubbleSkillForeground = String(useThemeColor("--color-user-bubble-skill-foreground"));597 const inlineSkillForeground = theme["--color-inline-skill-foreground"];
611 const userBubbleForegroundMuted = String(useThemeColor("--color-user-bubble-foreground-muted"));598 const userBubbleSkillForeground = theme["--color-user-bubble-skill-foreground"];
599 const userBubbleForegroundMuted = theme["--color-user-bubble-foreground-muted"];
612 const regularFontFamily = useFontFamily("regular");600 const regularFontFamily = useFontFamily("regular");
613 const boldFontFamily = useFontFamily("bold");601 const boldFontFamily = useFontFamily("bold");
614602
...@@ -1008,7 +996,7 @@ function renderFeedEntry(...@@ -1008,7 +996,7 @@ function renderFeedEntry(
1008 accessibilityState={{ expanded: entry.expanded }}996 accessibilityState={{ expanded: entry.expanded }}
1009 onPress={() => props.onToggleTurnFold(entry.turnId)}997 onPress={() => props.onToggleTurnFold(entry.turnId)}
1010 hitSlop={4}998 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"
1012 >1000 >
1013 <Text className="font-t3-medium text-sm tabular-nums text-foreground-muted">1001 <Text className="font-t3-medium text-sm tabular-nums text-foreground-muted">
1014 {entry.label}1002 {entry.label}
...@@ -1016,7 +1004,7 @@ function renderFeedEntry(...@@ -1016,7 +1004,7 @@ function renderFeedEntry(
1016 <SymbolView1004 <SymbolView
1017 name={entry.expanded ? "chevron.down" : "chevron.right"}1005 name={entry.expanded ? "chevron.down" : "chevron.right"}
1018 size={15}1006 size={15}
1019 tintColor={iconSubtleColor}1007 tintColorClassName={"accent-icon-subtle"}
1020 type="monochrome"1008 type="monochrome"
1021 />1009 />
1022 </Pressable>1010 </Pressable>
...@@ -1102,7 +1090,7 @@ function renderFeedEntry(...@@ -1102,7 +1090,7 @@ function renderFeedEntry(
1102 })}1090 })}
1103 </View>1091 </View>
1104 <View className="mt-1 flex-row items-center justify-end gap-1 pr-0.5">1092 <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">
1106 {timestampLabel}1094 {timestampLabel}
1107 </Text>1095 </Text>
1108 {message.text.trim().length > 0 ? (1096 {message.text.trim().length > 0 ? (
...@@ -1157,7 +1145,7 @@ function renderFeedEntry(...@@ -1157,7 +1145,7 @@ function renderFeedEntry(
1157 key={attachment.id}1145 key={attachment.id}
1158 environmentId={props.environmentId}1146 environmentId={props.environmentId}
1159 attachmentId={attachment.id}1147 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"
1161 onPressImage={props.onPressImage}1149 onPressImage={props.onPressImage}
1162 />1150 />
1163 );1151 );
...@@ -1171,7 +1159,7 @@ function renderFeedEntry(...@@ -1171,7 +1159,7 @@ function renderFeedEntry(
1171 buttonSize={28}1159 buttonSize={28}
1172 iconSize={13}1160 iconSize={13}
1173 />1161 />
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">
1175 {timestampLabel}1163 {timestampLabel}
1176 </Text>1164 </Text>
1177 </View>1165 </View>
...@@ -1207,11 +1195,11 @@ const WorkingTimelineRow = memo(function WorkingTimelineRow(props: { readonly st...@@ -1207,11 +1195,11 @@ const WorkingTimelineRow = memo(function WorkingTimelineRow(props: { readonly st
1207 return (1195 return (
1208 <View className="mb-4 flex-row items-center gap-2 px-1.5 py-1">1196 <View className="mb-4 flex-row items-center gap-2 px-1.5 py-1">
1209 <View className="flex-row items-center gap-1">1197 <View className="flex-row items-center gap-1">
1210 <View className="h-1 w-1 rounded-full bg-neutral-400 dark:bg-neutral-500" />1198 <View className="h-1 w-1 rounded-full bg-adaptive-neutral-400-500" />
1211 <View className="h-1 w-1 rounded-full bg-neutral-400/80 dark:bg-neutral-500/80" />1199 <View className="h-1 w-1 rounded-full bg-adaptive-neutral-400-a80-500-a80" />
1212 <View className="h-1 w-1 rounded-full bg-neutral-400/60 dark:bg-neutral-500/60" />1200 <View className="h-1 w-1 rounded-full bg-adaptive-neutral-400-a60-500-a60" />
1213 </View>1201 </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">
1215 Working for {durationLabel}1203 Working for {durationLabel}
1216 </Text>1204 </Text>
1217 </View>1205 </View>
...@@ -1303,6 +1291,7 @@ const ReviewCommentCard = memo(function ReviewCommentCard(props: {...@@ -1303,6 +1291,7 @@ const ReviewCommentCard = memo(function ReviewCommentCard(props: {
1303}) {1291}) {
1304 const { codeSurface, nativeReviewDiffStyle } = useAppearanceCodeSurface();1292 const { codeSurface, nativeReviewDiffStyle } = useAppearanceCodeSurface();
1305 const { themeAppearance: appearanceScheme, themeId } = useAppearancePreferences();1293 const { themeAppearance: appearanceScheme, themeId } = useAppearancePreferences();
1294 const appTheme = useUniwindTheme();
1306 const NativeReviewDiffView = resolveNativeReviewDiffView();1295 const NativeReviewDiffView = resolveNativeReviewDiffView();
1307 const patch = useMemo(() => buildReviewCommentPatch(props.comment), [props.comment]);1296 const patch = useMemo(() => buildReviewCommentPatch(props.comment), [props.comment]);
1308 const parsedDiff = useMemo(1297 const parsedDiff = useMemo(
...@@ -1315,8 +1304,8 @@ const ReviewCommentCard = memo(function ReviewCommentCard(props: {...@@ -1315,8 +1304,8 @@ const ReviewCommentCard = memo(function ReviewCommentCard(props: {
1315 [nativeReviewDiffData.rows],1304 [nativeReviewDiffData.rows],
1316 );1305 );
1317 const nativeReviewDiffTheme = useMemo(1306 const nativeReviewDiffTheme = useMemo(
1318 () => createNativeReviewDiffTheme(appearanceScheme, themeId),1307 () => createNativeReviewDiffTheme(appearanceScheme, themeId, appTheme),
1319 [appearanceScheme, themeId],1308 [appearanceScheme, appTheme, themeId],
1320 );1309 );
1321 const nativeRowsJson = useMemo(() => JSON.stringify(compactNativeRows), [compactNativeRows]);1310 const nativeRowsJson = useMemo(() => JSON.stringify(compactNativeRows), [compactNativeRows]);
1322 const nativeThemeJson = useMemo(1311 const nativeThemeJson = useMemo(
...@@ -1578,8 +1567,9 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) {...@@ -1578,8 +1567,9 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) {
1578 ? navigationHeaderHeight || insets.top + IOS_NAV_BAR_HEIGHT1567 ? navigationHeaderHeight || insets.top + IOS_NAV_BAR_HEIGHT
1579 : topContentInset;1568 : topContentInset;
15801569
1581 const iconSubtleColor = useThemeColor("--color-icon-subtle");1570 const theme = useUniwindTheme();
1582 const userBubbleColor = useThemeColor("--color-user-bubble");1571 const iconSubtleColor = theme["--color-icon-subtle"];
1572 const userBubbleColor = theme["--color-user-bubble"];
1583 const onMarkdownLinkPress = useCallback(1573 const onMarkdownLinkPress = useCallback(
1584 (href: string) => {1574 (href: string) => {
1585 const presentation = resolveMarkdownLinkPresentation(href);1575 const presentation = resolveMarkdownLinkPresentation(href);
apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx+13-29
...@@ -27,7 +27,6 @@ import { SymbolView } from "../../components/AppSymbol";...@@ -27,7 +27,6 @@ import { SymbolView } from "../../components/AppSymbol";
27import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";27import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
28import { NativeStackScreenOptions } from "../../native/StackHeader";28import { NativeStackScreenOptions } from "../../native/StackHeader";
29import { scopedProjectKey, scopedThreadKey } from "../../lib/scopedEntities";29import { scopedProjectKey, scopedThreadKey } from "../../lib/scopedEntities";
30import { useThemeColor } from "../../lib/useThemeColor";
31import { useProjects, useThreadShells } from "../../state/entities";30import { useProjects, useThreadShells } from "../../state/entities";
32import { mobilePreferencesAtom } from "../../state/preferences";31import { mobilePreferencesAtom } from "../../state/preferences";
33import { useThreadSearch } from "../../state/queries";32import { useThreadSearch } from "../../state/queries";
...@@ -127,19 +126,11 @@ export function ThreadNavigationSidebar(props: ThreadNavigationSidebarProps) {...@@ -127,19 +126,11 @@ export function ThreadNavigationSidebar(props: ThreadNavigationSidebarProps) {
127}126}
128127
129function NativeSidebarContainer(props: ThreadNavigationSidebarProps) {128function NativeSidebarContainer(props: ThreadNavigationSidebarProps) {
130 const backgroundColor = useThemeColor("--color-drawer");
131 const borderColor = useThemeColor("--color-border");
132
133 return (129 return (
134 <View130 <View
135 testID="thread-navigation-sidebar"131 testID="thread-navigation-sidebar"
136 className="flex-1"132 className="flex-1 border-border bg-drawer"
137 style={{133 style={{ borderRightWidth: StyleSheet.hairlineWidth, width: props.width }}
138 width: props.width,
139 backgroundColor,
140 borderRightColor: borderColor,
141 borderRightWidth: StyleSheet.hairlineWidth,
142 }}
143 >134 >
144 <SidebarNavigationShell>135 <SidebarNavigationShell>
145 <ThreadNavigationSidebarPane {...props} nativeChrome />136 <ThreadNavigationSidebarPane {...props} nativeChrome />
...@@ -731,10 +722,6 @@ function ThreadNavigationSidebarPane(...@@ -731,10 +722,6 @@ function ThreadNavigationSidebarPane(
731 ],722 ],
732 );723 );
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");
738 const [measuredHeaderHeight, setMeasuredHeaderHeight] = useState<number | null>(null);725 const [measuredHeaderHeight, setMeasuredHeaderHeight] = useState<number | null>(null);
739 // The sticky header (title row, search field, optional connection status)726 // The sticky header (title row, search field, optional connection status)
740 // is measured so the list inset always matches its real height — no727 // is measured so the list inset always matches its real height — no
...@@ -1242,13 +1229,8 @@ function ThreadNavigationSidebarPane(...@@ -1242,13 +1229,8 @@ function ThreadNavigationSidebarPane(
1242 return (1229 return (
1243 <View1230 <View
1244 testID="thread-navigation-sidebar"1231 testID="thread-navigation-sidebar"
1245 className="flex-1"1232 className="flex-1 border-r border-border bg-drawer"
1246 style={{1233 style={{ width: props.width }}
1247 width: props.width,
1248 backgroundColor,
1249 borderRightColor: borderColor,
1250 borderRightWidth: StyleSheet.hairlineWidth,
1251 }}
1252 >1234 >
1253 <View className="flex-1" style={{ paddingBottom: insets.bottom }}>1235 <View className="flex-1" style={{ paddingBottom: insets.bottom }}>
1254 <SwipeableScrollGateProvider enabled={swipeEnabled}>1236 <SwipeableScrollGateProvider enabled={swipeEnabled}>
...@@ -1286,14 +1268,11 @@ function ThreadNavigationSidebarPane(...@@ -1286,14 +1268,11 @@ function ThreadNavigationSidebarPane(
1286 </View>1268 </View>
12871269
1288 <View1270 <View
1289 className="absolute inset-x-0 top-0 z-[4]"1271 className="absolute inset-x-0 top-0 z-[4] bg-drawer"
1290 collapsable={false}1272 collapsable={false}
1291 onLayout={handleStickyHeaderLayout}1273 onLayout={handleStickyHeaderLayout}
1292 pointerEvents="auto"1274 pointerEvents="auto"
1293 style={{1275 style={{ paddingTop: insets.top }}
1294 paddingTop: insets.top,
1295 backgroundColor,
1296 }}
1297 >1276 >
1298 <View className="h-[50px] flex-row items-end gap-0.5 pr-2 pl-5">1277 <View className="h-[50px] flex-row items-end gap-0.5 pr-2 pl-5">
1299 {/* Title slot doubles as the connection status surface: while an1278 {/* Title slot doubles as the connection status surface: while an
...@@ -1318,7 +1297,12 @@ function ThreadNavigationSidebarPane(...@@ -1318,7 +1297,12 @@ function ThreadNavigationSidebarPane(
1318 </View>1297 </View>
13191298
1320 <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]">1299 <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 />
1322 <TextInput1306 <TextInput
1323 ref={searchInputRef}1307 ref={searchInputRef}
1324 accessibilityLabel="Search threads"1308 accessibilityLabel="Search threads"
...@@ -1327,7 +1311,7 @@ function ThreadNavigationSidebarPane(...@@ -1327,7 +1311,7 @@ function ThreadNavigationSidebarPane(
1327 clearButtonMode="while-editing"1311 clearButtonMode="while-editing"
1328 onChangeText={props.onSearchQueryChange}1312 onChangeText={props.onSearchQueryChange}
1329 placeholder="Search"1313 placeholder="Search"
1330 placeholderTextColor={placeholderColor}1314 placeholderTextColorClassName={"accent-placeholder"}
1331 returnKeyType="search"1315 returnKeyType="search"
1332 className="h-[34px] flex-1 px-0 py-0 font-sans text-base text-foreground"1316 className="h-[34px] flex-1 px-0 py-0 font-sans text-base text-foreground"
1333 value={props.searchQuery}1317 value={props.searchQuery}
apps/mobile/src/features/threads/ThreadSettingsSheet.tsx+13-11
...@@ -40,7 +40,7 @@ import { cn } from "../../lib/cn";...@@ -40,7 +40,7 @@ import { cn } from "../../lib/cn";
40import type { ModelOption, ProviderGroup } from "../../lib/modelOptions";40import type { ModelOption, ProviderGroup } from "../../lib/modelOptions";
41import { applyProviderOptionSelection } from "../../lib/providerOptions";41import { applyProviderOptionSelection } from "../../lib/providerOptions";
42import { resolveProviderOptionDescriptors } from "../../lib/providerOptions";42import { resolveProviderOptionDescriptors } from "../../lib/providerOptions";
43import { useThemeColor } from "../../lib/useThemeColor";43import { useUniwindTheme } from "../../lib/useUniwindTheme";
44import {44import {
45 NativeHeaderToolbar,45 NativeHeaderToolbar,
46 NativeStackScreenOptions,46 NativeStackScreenOptions,
...@@ -91,7 +91,6 @@ function ModelRow(props: {...@@ -91,7 +91,6 @@ function ModelRow(props: {
91 readonly isFirst: boolean;91 readonly isFirst: boolean;
92 readonly isLast: boolean;92 readonly isLast: boolean;
93}) {93}) {
94 const checkmarkColor = useThemeColor("--color-icon");
95 return (94 return (
96 <Pressable95 <Pressable
97 accessibilityLabel={props.option.label}96 accessibilityLabel={props.option.label}
...@@ -122,7 +121,7 @@ function ModelRow(props: {...@@ -122,7 +121,7 @@ function ModelRow(props: {
122 <SymbolView121 <SymbolView
123 name="checkmark"122 name="checkmark"
124 size={16}123 size={16}
125 tintColor={checkmarkColor}124 tintColorClassName={"accent-icon"}
126 type="monochrome"125 type="monochrome"
127 weight="semibold"126 weight="semibold"
128 />127 />
...@@ -140,7 +139,6 @@ function ProviderHeader(props: {...@@ -140,7 +139,6 @@ function ProviderHeader(props: {
140 readonly modelCount: number;139 readonly modelCount: number;
141 readonly onToggle: () => void;140 readonly onToggle: () => void;
142}) {141}) {
143 const iconSubtle = useThemeColor("--color-icon-subtle");
144 const content = (142 const content = (
145 <>143 <>
146 <ProviderIcon provider={props.driver} size={15} />144 <ProviderIcon provider={props.driver} size={15} />
...@@ -156,7 +154,7 @@ function ProviderHeader(props: {...@@ -156,7 +154,7 @@ function ProviderHeader(props: {
156 <SymbolView154 <SymbolView
157 name={props.collapsed ? "chevron.down" : "chevron.up"}155 name={props.collapsed ? "chevron.down" : "chevron.up"}
158 size={12}156 size={12}
159 tintColor={iconSubtle}157 tintColorClassName={"accent-icon-subtle"}
160 type="monochrome"158 type="monochrome"
161 />159 />
162 </>160 </>
...@@ -192,7 +190,6 @@ function DisclosureRow(props: {...@@ -192,7 +190,6 @@ function DisclosureRow(props: {
192 readonly onPress: () => void;190 readonly onPress: () => void;
193 readonly isLast?: boolean;191 readonly isLast?: boolean;
194}) {192}) {
195 const iconSubtle = useThemeColor("--color-icon-subtle");
196 return (193 return (
197 <Pressable194 <Pressable
198 accessibilityRole="button"195 accessibilityRole="button"
...@@ -209,7 +206,12 @@ function DisclosureRow(props: {...@@ -209,7 +206,12 @@ function DisclosureRow(props: {
209 {props.value}206 {props.value}
210 </Text>207 </Text>
211 ) : null}208 ) : 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 />
213 </Pressable>215 </Pressable>
214 );216 );
215}217}
...@@ -222,7 +224,6 @@ function ChoiceRow(props: {...@@ -222,7 +224,6 @@ function ChoiceRow(props: {
222 readonly onPress: () => void;224 readonly onPress: () => void;
223 readonly isLast: boolean;225 readonly isLast: boolean;
224}) {226}) {
225 const checkmarkColor = useThemeColor("--color-icon");
226 return (227 return (
227 <Pressable228 <Pressable
228 accessibilityLabel={props.description ? `${props.label}. ${props.description}` : props.label}229 accessibilityLabel={props.description ? `${props.label}. ${props.description}` : props.label}
...@@ -244,7 +245,7 @@ function ChoiceRow(props: {...@@ -244,7 +245,7 @@ function ChoiceRow(props: {
244 <SymbolView245 <SymbolView
245 name="checkmark"246 name="checkmark"
246 size={16}247 size={16}
247 tintColor={checkmarkColor}248 tintColorClassName={"accent-icon"}
248 type="monochrome"249 type="monochrome"
249 weight="semibold"250 weight="semibold"
250 />251 />
...@@ -1125,8 +1126,9 @@ function ThreadSettingsChoiceScreen() {...@@ -1125,8 +1126,9 @@ function ThreadSettingsChoiceScreen() {
1125}1126}
11261127
1127function ThreadSettingsPickerNavigator(props: ThreadSettingsPickerPresentation) {1128function ThreadSettingsPickerNavigator(props: ThreadSettingsPickerPresentation) {
1128 const solidSheetBackground = String(useThemeColor("--color-sheet-solid"));1129 const theme = useUniwindTheme();
1129 const foreground = String(useThemeColor("--color-foreground"));1130 const solidSheetBackground = theme["--color-sheet-solid"];
1131 const foreground = theme["--color-foreground"];
1130 const presentation = useMemo(1132 const presentation = useMemo(
1131 () => ({1133 () => ({
1132 onClose: props.onClose,1134 onClose: props.onClose,
apps/mobile/src/features/threads/git/GitCommitSheet.tsx+1-1
...@@ -85,7 +85,7 @@ export function GitCommitSheet(_props: GitCommitSheetProps) {...@@ -85,7 +85,7 @@ export function GitCommitSheet(_props: GitCommitSheetProps) {
85 </Text>85 </Text>
86 </View>86 </View>
87 {isDefaultRef ? (87 {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">
89 Warning: this is the default branch.89 Warning: this is the default branch.
90 </Text>90 </Text>
91 ) : null}91 ) : null}
apps/mobile/src/features/threads/git/GitOverviewSheet.tsx+5-6
...@@ -17,7 +17,7 @@ import { Alert, Platform, Pressable, RefreshControl, ScrollView, View } from "re...@@ -17,7 +17,7 @@ import { Alert, Platform, Pressable, RefreshControl, ScrollView, View } from "re
1717
18import { Screen, ScreenStack, ScreenStackHeaderConfig } from "react-native-screens";18import { Screen, ScreenStack, ScreenStackHeaderConfig } from "react-native-screens";
19import { useSafeAreaInsets } from "react-native-safe-area-context";19import { useSafeAreaInsets } from "react-native-safe-area-context";
20import { useThemeColor } from "../../../lib/useThemeColor";20import { useUniwindTheme } from "../../../lib/useUniwindTheme";
2121
22import { AndroidSheetHeader } from "../../../components/AndroidScreenHeader";22import { AndroidSheetHeader } from "../../../components/AndroidScreenHeader";
23import { AppText as Text } from "../../../components/AppText";23import { AppText as Text } from "../../../components/AppText";
...@@ -53,10 +53,9 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) {...@@ -53,10 +53,9 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) {
53 const { selectedThreadCwd, selectedThreadWorktreePath } = useSelectedThreadWorktree();53 const { selectedThreadCwd, selectedThreadWorktreePath } = useSelectedThreadWorktree();
54 const gitState = useSelectedThreadGitState();54 const gitState = useSelectedThreadGitState();
55 const gitActions = useSelectedThreadGitActions();55 const gitActions = useSelectedThreadGitActions();
5656 const theme = useUniwindTheme();
57 const iconColor = useThemeColor("--color-icon");57 const foregroundColor = theme["--color-foreground"];
58 const foregroundColor = String(useThemeColor("--color-foreground"));58 const sheetColor = theme["--color-sheet"];
59 const sheetColor = String(useThemeColor("--color-sheet"));
6059
61 const gitStatus = useEnvironmentQuery(60 const gitStatus = useEnvironmentQuery(
62 selectedThread !== null && selectedThreadCwd !== null61 selectedThread !== null && selectedThreadCwd !== null
...@@ -385,7 +384,7 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) {...@@ -385,7 +384,7 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) {
385 <SymbolView384 <SymbolView
386 name="arrow.clockwise"385 name="arrow.clockwise"
387 size={16}386 size={16}
388 tintColor={iconColor}387 tintColorClassName={"accent-icon"}
389 type="monochrome"388 type="monochrome"
390 weight="medium"389 weight="medium"
391 />390 />
apps/mobile/src/features/threads/git/gitSheetComponents.tsx+24-12
...@@ -1,7 +1,6 @@...@@ -1,7 +1,6 @@
1import { SymbolView } from "../../../components/AppSymbol";1import { SymbolView } from "../../../components/AppSymbol";
2import type { ComponentProps } from "react";2import type { ComponentProps } from "react";
3import { Pressable, View } from "react-native";3import { Pressable, View } from "react-native";
4import { useThemeColor } from "../../../lib/useThemeColor";
5import { AppText as Text } from "../../../components/AppText";4import { AppText as Text } from "../../../components/AppText";
6import { cn } from "../../../lib/cn";5import { cn } from "../../../lib/cn";
76
...@@ -14,12 +13,13 @@ export function SheetActionButton(props: {...@@ -14,12 +13,13 @@ export function SheetActionButton(props: {
14 readonly tone?: "primary" | "secondary" | "danger";13 readonly tone?: "primary" | "secondary" | "danger";
15 readonly onPress: () => void;14 readonly onPress: () => void;
16}) {15}) {
17 const primaryFg = useThemeColor("--color-primary-foreground");
18 const dangerFg = useThemeColor("--color-danger-foreground");
19 const secondaryFg = useThemeColor("--color-secondary-foreground");
20
21 const tone = props.tone ?? "secondary";16 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
24 return (24 return (
25 <Pressable25 <Pressable
...@@ -34,7 +34,12 @@ export function SheetActionButton(props: {...@@ -34,7 +34,12 @@ export function SheetActionButton(props: {
34 disabled={props.disabled}34 disabled={props.disabled}
35 onPress={props.onPress}35 onPress={props.onPress}
36 >36 >
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 />
38 <Text43 <Text
39 className={cn(44 className={cn(
40 "text-xs font-t3-bold tracking-[0.9px] uppercase",45 "text-xs font-t3-bold tracking-[0.9px] uppercase",
...@@ -71,9 +76,6 @@ export function SheetListRow(props: {...@@ -71,9 +76,6 @@ export function SheetListRow(props: {
71 readonly disabled?: boolean;76 readonly disabled?: boolean;
72 readonly onPress: () => void;77 readonly onPress: () => void;
73}) {78}) {
74 const iconColor = useThemeColor("--color-icon");
75 const iconSubtleColor = useThemeColor("--color-icon-subtle");
76
77 return (79 return (
78 <Pressable80 <Pressable
79 className="flex-row items-center gap-3 px-1 py-3 disabled:opacity-[0.45]"81 className="flex-row items-center gap-3 px-1 py-3 disabled:opacity-[0.45]"
...@@ -81,7 +83,12 @@ export function SheetListRow(props: {...@@ -81,7 +83,12 @@ export function SheetListRow(props: {
81 onPress={props.onPress}83 onPress={props.onPress}
82 >84 >
83 <View className="bg-subtle h-9 w-9 items-center justify-center rounded-full">85 <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 />
85 </View>92 </View>
86 <View className="flex-1 gap-0.5">93 <View className="flex-1 gap-0.5">
87 <Text className="text-foreground text-base font-t3-bold">{props.title}</Text>94 <Text className="text-foreground text-base font-t3-bold">{props.title}</Text>
...@@ -89,7 +96,12 @@ export function SheetListRow(props: {...@@ -89,7 +96,12 @@ export function SheetListRow(props: {
89 <Text className="text-foreground-muted text-xs leading-snug">{props.subtitle}</Text>96 <Text className="text-foreground-muted text-xs leading-snug">{props.subtitle}</Text>
90 ) : null}97 ) : null}
91 </View>98 </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 />
93 </Pressable>105 </Pressable>
94 );106 );
95}107}
apps/mobile/src/features/threads/sidebar-filter-button.tsx+6-5
...@@ -1,8 +1,6 @@...@@ -1,8 +1,6 @@
1import { SymbolView } from "../../components/AppSymbol";1import { SymbolView } from "../../components/AppSymbol";
2import { Pressable } from "react-native";2import { Pressable } from "react-native";
33
4import { useThemeColor } from "../../lib/useThemeColor";
5
6export type SidebarFilterButtonIcon =4export type SidebarFilterButtonIcon =
7 | "line.3.horizontal.decrease.circle"5 | "line.3.horizontal.decrease.circle"
8 | "line.3.horizontal.decrease.circle.fill";6 | "line.3.horizontal.decrease.circle.fill";
...@@ -11,8 +9,6 @@ export function SidebarFilterButton(props: {...@@ -11,8 +9,6 @@ export function SidebarFilterButton(props: {
11 readonly accessibilityLabel: string;9 readonly accessibilityLabel: string;
12 readonly icon: SidebarFilterButtonIcon;10 readonly icon: SidebarFilterButtonIcon;
13}) {11}) {
14 const iconColor = useThemeColor("--color-foreground");
15
16 return (12 return (
17 <Pressable13 <Pressable
18 className="size-11 cursor-pointer items-center justify-center rounded-full bg-subtle active:opacity-70"14 className="size-11 cursor-pointer items-center justify-center rounded-full bg-subtle active:opacity-70"
...@@ -20,7 +16,12 @@ export function SidebarFilterButton(props: {...@@ -20,7 +16,12 @@ export function SidebarFilterButton(props: {
20 accessibilityRole="button"16 accessibilityRole="button"
21 hitSlop={4}17 hitSlop={4}
22 >18 >
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 />
24 </Pressable>25 </Pressable>
25 );26 );
26}27}
apps/mobile/src/features/threads/sidebar-header-actions.tsx+6-5
...@@ -1,8 +1,6 @@...@@ -1,8 +1,6 @@
1import { SymbolView } from "../../components/AppSymbol";1import { SymbolView } from "../../components/AppSymbol";
2import { Pressable, View } from "react-native";2import { Pressable, View } from "react-native";
33
4import { useThemeColor } from "../../lib/useThemeColor";
5
6export interface SidebarHeaderActionsProps {4export interface SidebarHeaderActionsProps {
7 readonly onOpenSettings: () => void;5 readonly onOpenSettings: () => void;
8}6}
...@@ -12,8 +10,6 @@ function FallbackHeaderButton(props: {...@@ -12,8 +10,6 @@ function FallbackHeaderButton(props: {
12 readonly icon: "gearshape" | "square.and.pencil";10 readonly icon: "gearshape" | "square.and.pencil";
13 readonly onPress: () => void;11 readonly onPress: () => void;
14}) {12}) {
15 const iconColor = useThemeColor("--color-foreground");
16
17 return (13 return (
18 <Pressable14 <Pressable
19 className="size-11 items-center justify-center rounded-full bg-subtle active:opacity-70"15 className="size-11 items-center justify-center rounded-full bg-subtle active:opacity-70"
...@@ -22,7 +18,12 @@ function FallbackHeaderButton(props: {...@@ -22,7 +18,12 @@ function FallbackHeaderButton(props: {
22 hitSlop={4}18 hitSlop={4}
23 onPress={props.onPress}19 onPress={props.onPress}
24 >20 >
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 />
26 </Pressable>27 </Pressable>
27 );28 );
28}29}
apps/mobile/src/features/threads/sidebar-navigation-shell.tsx+1-3
...@@ -10,7 +10,6 @@ import { getCompactBrandHeaderOptions } from "../../components/CompactBrandTitle...@@ -10,7 +10,6 @@ import { getCompactBrandHeaderOptions } from "../../components/CompactBrandTitle
10import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";10import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
11import { nativeHeaderScrollEdgeEffects } from "../../native/StackHeader";11import { nativeHeaderScrollEdgeEffects } from "../../native/StackHeader";
12import { useMobileNavigationTheme } from "../../lib/useMobileNavigationTheme";12import { useMobileNavigationTheme } from "../../lib/useMobileNavigationTheme";
13import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
1413
15const SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version);14const SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version);
1615
...@@ -53,8 +52,7 @@ const SidebarStack = createNativeStackNavigator();...@@ -53,8 +52,7 @@ const SidebarStack = createNativeStackNavigator();
53 * navigation hooks used for header configuration inside the pane.52 * navigation hooks used for header configuration inside the pane.
54 */53 */
55export function SidebarNavigationShell(props: { readonly children: ReactNode }) {54export function SidebarNavigationShell(props: { readonly children: ReactNode }) {
56 const { themeAppearance } = useAppearancePreferences();55 const navigationTheme = useMobileNavigationTheme();
57 const navigationTheme = useMobileNavigationTheme(themeAppearance);
5856
59 return (57 return (
60 <NavigationIndependentTree>58 <NavigationIndependentTree>
apps/mobile/src/features/threads/thread-list-items.tsx+18-21
...@@ -19,7 +19,7 @@ import { cn } from "../../lib/cn";...@@ -19,7 +19,7 @@ import { cn } from "../../lib/cn";
19import { HOME_HORIZONTAL_INSET } from "../../lib/layoutMetrics";19import { HOME_HORIZONTAL_INSET } from "../../lib/layoutMetrics";
20import { relativeTime } from "../../lib/time";20import { relativeTime } from "../../lib/time";
21import { themeColorWithAlpha } from "../../lib/mobileTheme";21import { themeColorWithAlpha } from "../../lib/mobileTheme";
22import { useThemeColor } from "../../lib/useThemeColor";22import { useUniwindTheme } from "../../lib/useUniwindTheme";
23import type { PendingNewTask } from "../../state/use-pending-new-tasks";23import type { PendingNewTask } from "../../state/use-pending-new-tasks";
24import { useThreadPr, type ThreadPr } from "../../state/use-thread-pr";24import { useThreadPr, type ThreadPr } from "../../state/use-thread-pr";
25import type { HomeGroupDisplayAction } from "../home/homeListItems";25import type { HomeGroupDisplayAction } from "../home/homeListItems";
...@@ -87,7 +87,6 @@ export const ThreadListGroupHeader = memo(function ThreadListGroupHeader(props:...@@ -87,7 +87,6 @@ export const ThreadListGroupHeader = memo(function ThreadListGroupHeader(props:
87 readonly newThreadTarget?: EnvironmentProject | null;87 readonly newThreadTarget?: EnvironmentProject | null;
88 readonly onNewThread?: (project: EnvironmentProject) => void;88 readonly onNewThread?: (project: EnvironmentProject) => void;
89}) {89}) {
90 const iconMutedColor = useThemeColor("--color-icon-muted");
91 const { groupKey, onGroupAction, onNewThread } = props;90 const { groupKey, onGroupAction, onNewThread } = props;
92 const newThreadTarget = props.newThreadTarget ?? null;91 const newThreadTarget = props.newThreadTarget ?? null;
93 const compact = props.variant === "compact";92 const compact = props.variant === "compact";
...@@ -171,7 +170,7 @@ export const ThreadListGroupHeader = memo(function ThreadListGroupHeader(props:...@@ -171,7 +170,7 @@ export const ThreadListGroupHeader = memo(function ThreadListGroupHeader(props:
171 <SymbolView170 <SymbolView
172 name="plus"171 name="plus"
173 size={compact ? 20 : 16}172 size={compact ? 20 : 16}
174 tintColor={iconMutedColor}173 tintColorClassName={"accent-icon-muted"}
175 type="monochrome"174 type="monochrome"
176 weight="medium"175 weight="medium"
177 />176 />
...@@ -190,7 +189,6 @@ export const ThreadListShowMoreRow = memo(function ThreadListShowMoreRow(props:...@@ -190,7 +189,6 @@ export const ThreadListShowMoreRow = memo(function ThreadListShowMoreRow(props:
190 readonly groupKey: string;189 readonly groupKey: string;
191 readonly onGroupAction: (key: string, action: HomeGroupDisplayAction) => void;190 readonly onGroupAction: (key: string, action: HomeGroupDisplayAction) => void;
192}) {191}) {
193 const iconSubtleColor = useThemeColor("--color-icon-subtle");
194 const showsMore = props.hiddenCount > 0;192 const showsMore = props.hiddenCount > 0;
195 const compact = props.variant === "compact";193 const compact = props.variant === "compact";
196 const { groupKey, onGroupAction } = props;194 const { groupKey, onGroupAction } = props;
...@@ -221,7 +219,7 @@ export const ThreadListShowMoreRow = memo(function ThreadListShowMoreRow(props:...@@ -221,7 +219,7 @@ export const ThreadListShowMoreRow = memo(function ThreadListShowMoreRow(props:
221 <SymbolView219 <SymbolView
222 name={icon}220 name={icon}
223 size={10}221 size={10}
224 tintColor={iconSubtleColor}222 tintColorClassName={"accent-icon-subtle"}
225 type="monochrome"223 type="monochrome"
226 weight="semibold"224 weight="semibold"
227 />225 />
...@@ -275,10 +273,9 @@ export const PendingTaskListRow = memo(function PendingTaskListRow(props: {...@@ -275,10 +273,9 @@ export const PendingTaskListRow = memo(function PendingTaskListRow(props: {
275 readonly onDeletePendingTask: (pendingTask: PendingNewTask) => void;273 readonly onDeletePendingTask: (pendingTask: PendingNewTask) => void;
276}) {274}) {
277 const compact = props.variant === "compact";275 const compact = props.variant === "compact";
278 const separatorColor = useThemeColor("--color-separator");276 const theme = useUniwindTheme();
279 const iconSubtleColor = useThemeColor("--color-icon-subtle");277 const separatorColor = theme["--color-separator"];
280 const mutedColor = useThemeColor("--color-foreground-muted");278 const pressedBackgroundColor = theme["--color-subtle"];
281 const pressedBackgroundColor = useThemeColor("--color-subtle");
282279
283 const { pendingTask, onSelectPendingTask, onDeletePendingTask } = props;280 const { pendingTask, onSelectPendingTask, onDeletePendingTask } = props;
284 const timestamp = relativeTime(pendingTask.message.createdAt);281 const timestamp = relativeTime(pendingTask.message.createdAt);
...@@ -294,8 +291,8 @@ export const PendingTaskListRow = memo(function PendingTaskListRow(props: {...@@ -294,8 +291,8 @@ export const PendingTaskListRow = memo(function PendingTaskListRow(props: {
294 );291 );
295292
296 const statusPill = (293 const statusPill = (
297 <View className="rounded-full bg-zinc-500/12 px-1.5 py-0.5 dark:bg-zinc-500/16">294 <View className="rounded-full bg-adaptive-zinc-500-a12-a16 px-1.5 py-0.5">
298 <Text className="text-3xs font-t3-bold text-zinc-600 dark:text-zinc-300">Pending</Text>295 <Text className="text-3xs font-t3-bold text-adaptive-zinc-600-300">Pending</Text>
299 </View>296 </View>
300 );297 );
301298
...@@ -305,7 +302,7 @@ export const PendingTaskListRow = memo(function PendingTaskListRow(props: {...@@ -305,7 +302,7 @@ export const PendingTaskListRow = memo(function PendingTaskListRow(props: {
305 <SymbolView302 <SymbolView
306 name="tray.and.arrow.up"303 name="tray.and.arrow.up"
307 size={10}304 size={10}
308 tintColor={compact ? iconSubtleColor : mutedColor}305 tintColorClassName={compact ? "accent-icon-subtle" : "accent-foreground-muted"}
309 type="monochrome"306 type="monochrome"
310 />307 />
311 <Text308 <Text
...@@ -355,7 +352,7 @@ export const PendingTaskListRow = memo(function PendingTaskListRow(props: {...@@ -355,7 +352,7 @@ export const PendingTaskListRow = memo(function PendingTaskListRow(props: {
355 <SymbolView352 <SymbolView
356 name="chevron.right"353 name="chevron.right"
357 size={13}354 size={13}
358 tintColor={iconSubtleColor}355 tintColorClassName={"accent-icon-subtle"}
359 type="monochrome"356 type="monochrome"
360 />357 />
361 </View>358 </View>
...@@ -446,13 +443,13 @@ export const ThreadListRow = memo(function ThreadListRow(props: {...@@ -446,13 +443,13 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
446 // thread, so a hover highlight can't leak across rows.443 // thread, so a hover highlight can't leak across rows.
447 const [hovered, setHovered] = useRecyclingState(false);444 const [hovered, setHovered] = useRecyclingState(false);
448445
449 const separatorColor = useThemeColor("--color-separator");446 const theme = useUniwindTheme();
450 const iconSubtleColor = useThemeColor("--color-icon-subtle");447 const separatorColor = theme["--color-separator"];
451 const screenColor = useThemeColor("--color-screen");448 const screenColor = theme["--color-screen"];
452 const drawerColor = useThemeColor("--color-drawer");449 const drawerColor = theme["--color-drawer"];
453 const pressedBackgroundColor = useThemeColor("--color-subtle");450 const pressedBackgroundColor = theme["--color-subtle"];
454 const selectedBackgroundColor = useThemeColor("--color-user-bubble");451 const selectedBackgroundColor = theme["--color-user-bubble"];
455 const selectedForegroundColor = useThemeColor("--color-user-bubble-foreground");452 const selectedForegroundColor = theme["--color-user-bubble-foreground"];
456453
457 const { thread, onSelectThread, onArchiveThread, onDeleteThread, onRegenerateThreadTitle } =454 const { thread, onSelectThread, onArchiveThread, onDeleteThread, onRegenerateThreadTitle } =
458 props;455 props;
...@@ -600,7 +597,7 @@ export const ThreadListRow = memo(function ThreadListRow(props: {...@@ -600,7 +597,7 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
600 <SymbolView597 <SymbolView
601 name="chevron.right"598 name="chevron.right"
602 size={13}599 size={13}
603 tintColor={iconSubtleColor}600 tintColorClassName={"accent-icon-subtle"}
604 type="monochrome"601 type="monochrome"
605 />602 />
606 </View>603 </View>
apps/mobile/src/features/threads/thread-list-v2-items.tsx+25-23
...@@ -16,7 +16,7 @@ import { ProjectFavicon } from "../../components/ProjectFavicon";...@@ -16,7 +16,7 @@ import { ProjectFavicon } from "../../components/ProjectFavicon";
16import { ProviderIcon } from "../../components/ProviderIcon";16import { ProviderIcon } from "../../components/ProviderIcon";
17import { cn } from "../../lib/cn";17import { cn } from "../../lib/cn";
18import { relativeTime } from "../../lib/time";18import { relativeTime } from "../../lib/time";
19import { useThemeColor } from "../../lib/useThemeColor";19import { useUniwindTheme } from "../../lib/useUniwindTheme";
20import type { PendingNewTask } from "../../state/use-pending-new-tasks";20import type { PendingNewTask } from "../../state/use-pending-new-tasks";
21import { useThreadPr } from "../../state/use-thread-pr";21import { useThreadPr } from "../../state/use-thread-pr";
22import { ThreadSwipeable } from "../home/thread-swipe-actions";22import { ThreadSwipeable } from "../home/thread-swipe-actions";
...@@ -52,10 +52,10 @@ const MONO_FONT = Platform.select({...@@ -52,10 +52,10 @@ const MONO_FONT = Platform.select({
52const STATUS_LABEL_BY_STATUS: Partial<52const STATUS_LABEL_BY_STATUS: Partial<
53 Record<ThreadListV2Status, { label: string; className: string }>53 Record<ThreadListV2Status, { label: string; className: string }>
54> = {54> = {
55 approval: { label: "Approval", className: "text-amber-700 dark:text-amber-300" },55 approval: { label: "Approval", className: "text-adaptive-amber-700-300" },
56 input: { label: "Input", className: "text-indigo-600 dark:text-indigo-300" },56 input: { label: "Input", className: "text-adaptive-indigo-600-300" },
57 working: { label: "Working", className: "text-sky-600 dark:text-sky-400" },57 working: { label: "Working", className: "text-adaptive-sky-600-400" },
58 failed: { label: "Failed", className: "text-red-700 dark:text-red-300" },58 failed: { label: "Failed", className: "text-adaptive-red-700-300" },
59};59};
6060
61function threadTimeLabel(thread: EnvironmentThreadShell): string {61function threadTimeLabel(thread: EnvironmentThreadShell): string {
...@@ -93,7 +93,6 @@ export const ThreadListV2SectionDivider = memo(function ThreadListV2SectionDivid...@@ -93,7 +93,6 @@ export const ThreadListV2SectionDivider = memo(function ThreadListV2SectionDivid
93 readonly label: string;93 readonly label: string;
94 readonly pane?: "screen" | "sidebar";94 readonly pane?: "screen" | "sidebar";
95}) {95}) {
96 const borderColor = useThemeColor("--color-border");
97 return (96 return (
98 <View97 <View
99 className={cn(98 className={cn(
...@@ -102,7 +101,7 @@ export const ThreadListV2SectionDivider = memo(function ThreadListV2SectionDivid...@@ -102,7 +101,7 @@ export const ThreadListV2SectionDivider = memo(function ThreadListV2SectionDivid
102 )}101 )}
103 >102 >
104 <Text className="text-xs font-t3-medium text-foreground-tertiary">{props.label}</Text>103 <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" />
106 </View>105 </View>
107 );106 );
108});107});
...@@ -134,10 +133,10 @@ export const ThreadListV2SnoozedShelfHeader = memo(function ThreadListV2SnoozedS...@@ -134,10 +133,10 @@ export const ThreadListV2SnoozedShelfHeader = memo(function ThreadListV2SnoozedS
134 onPress={props.onToggle}133 onPress={props.onToggle}
135 style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1 })}134 style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1 })}
136 >135 >
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">
138 {props.expanded ? "Snoozed" : `Snoozed (${props.count})`}137 {props.expanded ? "Snoozed" : `Snoozed (${props.count})`}
139 </Text>138 </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" />
141 <SymbolView140 <SymbolView
142 name="chevron.down"141 name="chevron.down"
143 size={10}142 size={10}
...@@ -156,7 +155,6 @@ export const ThreadListV2SettledShelfHeader = memo(function ThreadListV2SettledS...@@ -156,7 +155,6 @@ export const ThreadListV2SettledShelfHeader = memo(function ThreadListV2SettledS
156 readonly onToggle: () => void;155 readonly onToggle: () => void;
157 readonly pane?: "screen" | "sidebar";156 readonly pane?: "screen" | "sidebar";
158}) {157}) {
159 const mutedColor = useThemeColor("--color-foreground-muted");
160 return (158 return (
161 <Pressable159 <Pressable
162 accessibilityHint={160 accessibilityHint={
...@@ -180,7 +178,7 @@ export const ThreadListV2SettledShelfHeader = memo(function ThreadListV2SettledS...@@ -180,7 +178,7 @@ export const ThreadListV2SettledShelfHeader = memo(function ThreadListV2SettledS
180 <SymbolView178 <SymbolView
181 name="chevron.down"179 name="chevron.down"
182 size={10}180 size={10}
183 tintColor={mutedColor}181 tintColorClassName={"accent-foreground-muted"}
184 type="monochrome"182 type="monochrome"
185 style={{ transform: [{ rotate: props.expanded ? "180deg" : "0deg" }] }}183 style={{ transform: [{ rotate: props.expanded ? "180deg" : "0deg" }] }}
186 />184 />
...@@ -213,8 +211,9 @@ export const ThreadListV2PendingRow = memo(function ThreadListV2PendingRow(props...@@ -213,8 +211,9 @@ export const ThreadListV2PendingRow = memo(function ThreadListV2PendingRow(props
213 readonly onDeletePendingTask: (pendingTask: PendingNewTask) => void;211 readonly onDeletePendingTask: (pendingTask: PendingNewTask) => void;
214}) {212}) {
215 const { pendingTask, onSelectPendingTask, onDeletePendingTask } = props;213 const { pendingTask, onSelectPendingTask, onDeletePendingTask } = props;
216 const drawerColor = useThemeColor("--color-drawer");214 const theme = useUniwindTheme();
217 const pressedBackgroundColor = useThemeColor("--color-subtle");215 const drawerColor = theme["--color-drawer"];
216 const pressedBackgroundColor = theme["--color-subtle"];
218 const sidebarPane = props.pane === "sidebar";217 const sidebarPane = props.pane === "sidebar";
219 const projectTitle =218 const projectTitle =
220 props.projectTitle ?? props.project?.title ?? pendingTask.creation.projectTitle ?? "";219 props.projectTitle ?? props.project?.title ?? pendingTask.creation.projectTitle ?? "";
...@@ -418,11 +417,11 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {...@@ -418,11 +417,11 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
418 onChangeRequestState?.(threadKey, changeRequest);417 onChangeRequestState?.(threadKey, changeRequest);
419 }, [onChangeRequestState, prState, prUpdatedAt, thread.linkedPullRequest, threadKey]);418 }, [onChangeRequestState, prState, prUpdatedAt, thread.linkedPullRequest, threadKey]);
420419
421 const screenColor = useThemeColor("--color-screen");420 const theme = useUniwindTheme();
422 const drawerColor = useThemeColor("--color-drawer");421 const screenColor = theme["--color-screen"];
423 const pressedBackgroundColor = useThemeColor("--color-subtle");422 const drawerColor = theme["--color-drawer"];
424 const selectedBackgroundColor = useThemeColor("--color-user-bubble");423 const pressedBackgroundColor = theme["--color-subtle"];
425 const pinTintColor = useThemeColor("--color-foreground-muted");424 const selectedBackgroundColor = theme["--color-user-bubble"];
426 const sidebarPane = props.pane === "sidebar";425 const sidebarPane = props.pane === "sidebar";
427 const selected = props.selected === true;426 const selected = props.selected === true;
428427
...@@ -699,7 +698,12 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {...@@ -699,7 +698,12 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
699 {props.projectTitle ?? props.project?.title ?? ""}698 {props.projectTitle ?? props.project?.title ?? ""}
700 </Text>699 </Text>
701 {pinnedRow ? (700 {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 />
703 ) : null}707 ) : null}
704 <Text708 <Text
705 className={cn(709 className={cn(
...@@ -735,9 +739,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {...@@ -735,9 +739,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
735 <Text739 <Text
736 className={cn(740 className={cn(
737 "flex-1 text-xs",741 "flex-1 text-xs",
738 selected742 selected ? "text-user-bubble-foreground-muted" : "text-adaptive-red-600-a80-400-a80",
739 ? "text-user-bubble-foreground-muted"
740 : "text-red-600/80 dark:text-red-400/80",
741 )}743 )}
742 numberOfLines={1}744 numberOfLines={1}
743 >745 >
...@@ -905,7 +907,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {...@@ -905,7 +907,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
905 selected907 selected
906 ? "text-user-bubble-foreground-muted"908 ? "text-user-bubble-foreground-muted"
907 : snoozedRow909 : snoozedRow
908 ? "text-blue-600 dark:text-blue-400"910 ? "text-adaptive-blue-600-400"
909 : "text-foreground-tertiary",911 : "text-foreground-tertiary",
910 )}912 )}
911 style={{ fontFamily: MONO_FONT }}913 style={{ fontFamily: MONO_FONT }}
apps/mobile/src/features/threads/thread-search-match.tsx+2-2
...@@ -65,8 +65,8 @@ export function ThreadSearchMatchExcerpt(props: {...@@ -65,8 +65,8 @@ export function ThreadSearchMatchExcerpt(props: {
65 props.selected65 props.selected
66 ? "text-user-bubble-foreground"66 ? "text-user-bubble-foreground"
67 : isUser67 : isUser
68 ? "text-blue-500 dark:text-blue-400"68 ? "text-adaptive-blue-500-400"
69 : "text-emerald-600 dark:text-emerald-400",69 : "text-adaptive-emerald-600-400",
70 )}70 )}
71 >71 >
72 {isUser ? "You:" : "Agent:"}{" "}72 {isUser ? "You:" : "Agent:"}{" "}
apps/mobile/src/features/threads/thread-work-log.tsx+5-14
...@@ -7,7 +7,6 @@ import { scaledTypographyLineHeight } from "../../lib/appearancePreferences";...@@ -7,7 +7,6 @@ import { scaledTypographyLineHeight } from "../../lib/appearancePreferences";
7import { cn } from "../../lib/cn";7import { cn } from "../../lib/cn";
8import type { ThreadFeedActivity } from "../../lib/threadActivity";8import type { ThreadFeedActivity } from "../../lib/threadActivity";
9import { MOBILE_TYPOGRAPHY } from "../../lib/typography";9import { MOBILE_TYPOGRAPHY } from "../../lib/typography";
10import { useThemeColor } from "../../lib/useThemeColor";
11import Animated, { FadeIn } from "react-native-reanimated";10import Animated, { FadeIn } from "react-native-reanimated";
1211
13const WORK_LOG_LAYOUT_ANIMATION = {12const WORK_LOG_LAYOUT_ANIMATION = {
...@@ -128,7 +127,6 @@ export function ThreadWorkLog(props: {...@@ -128,7 +127,6 @@ export function ThreadWorkLog(props: {
128 readonly onCopyRow: (rowId: string, value: string) => void;127 readonly onCopyRow: (rowId: string, value: string) => void;
129 readonly onToggleRow: (rowId: string) => void;128 readonly onToggleRow: (rowId: string) => void;
130}) {129}) {
131 const pressedBackground = useThemeColor("--color-subtle");
132 const rows = visibleWorkLogActivities(props.activities).map((activity) => ({130 const rows = visibleWorkLogActivities(props.activities).map((activity) => ({
133 ...activity,131 ...activity,
134 detail: compactActivityDetail(activity.detail),132 detail: compactActivityDetail(activity.detail),
...@@ -178,10 +176,7 @@ export function ThreadWorkLog(props: {...@@ -178,10 +176,7 @@ export function ThreadWorkLog(props: {
178 }176 }
179 }}177 }}
180 onLongPress={() => props.onCopyRow(row.id, row.getCopyText())}178 onLongPress={() => props.onCopyRow(row.id, row.getCopyText())}
181 style={({ pressed }) => ({179 className="rounded-md px-0.5 py-0 active:bg-subtle"
182 backgroundColor: pressed ? pressedBackground : "transparent",
183 })}
184 className="rounded-md px-0.5 py-0"
185 >180 >
186 <View className="min-h-8 flex-row items-center gap-1.5">181 <View className="min-h-8 flex-row items-center gap-1.5">
187 <View className="h-[18px] w-5 shrink-0 items-center justify-center">182 <View className="h-[18px] w-5 shrink-0 items-center justify-center">
...@@ -198,7 +193,7 @@ export function ThreadWorkLog(props: {...@@ -198,7 +193,7 @@ export function ThreadWorkLog(props: {
198 <Text193 <Text
199 className={cn(194 className={cn(
200 "font-t3-medium text-foreground",195 "font-t3-medium text-foreground",
201 iconIsDestructive && "text-rose-600 dark:text-rose-400",196 iconIsDestructive && "text-adaptive-rose-600-400",
202 )}197 )}
203 >198 >
204 {row.summary}199 {row.summary}
...@@ -210,7 +205,7 @@ export function ThreadWorkLog(props: {...@@ -210,7 +205,7 @@ export function ThreadWorkLog(props: {
210205
211 <View className="shrink-0 flex-row items-center gap-px">206 <View className="shrink-0 flex-row items-center gap-px">
212 {props.copiedRowId === row.id ? (207 {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">
214 Copied209 Copied
215 </Text>210 </Text>
216 ) : null}211 ) : null}
...@@ -249,7 +244,7 @@ export function ThreadWorkLog(props: {...@@ -249,7 +244,7 @@ export function ThreadWorkLog(props: {
249 </Pressable>244 </Pressable>
250245
251 {fullDetail ? (246 {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">
253 <ScrollView248 <ScrollView
254 nestedScrollEnabled249 nestedScrollEnabled
255 directionalLockEnabled250 directionalLockEnabled
...@@ -281,7 +276,6 @@ export function ThreadWorkGroupToggle(props: {...@@ -281,7 +276,6 @@ export function ThreadWorkGroupToggle(props: {
281 readonly onlyToolActivities: boolean;276 readonly onlyToolActivities: boolean;
282 readonly onToggle: () => void;277 readonly onToggle: () => void;
283}) {278}) {
284 const pressedBackground = useThemeColor("--color-subtle");
285 const noun = props.onlyToolActivities279 const noun = props.onlyToolActivities
286 ? props.hiddenCount === 1280 ? props.hiddenCount === 1
287 ? "tool call"281 ? "tool call"
...@@ -305,10 +299,7 @@ export function ThreadWorkGroupToggle(props: {...@@ -305,10 +299,7 @@ export function ThreadWorkGroupToggle(props: {
305 void Haptics.selectionAsync();299 void Haptics.selectionAsync();
306 props.onToggle();300 props.onToggle();
307 }}301 }}
308 style={({ pressed }) => ({302 className="min-h-8 flex-row items-center gap-1.5 rounded-md px-0.5 py-0 active:bg-subtle"
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"
312 >303 >
313 <View className="h-[18px] w-5 items-center justify-center">304 <View className="h-[18px] w-5 items-center justify-center">
314 <SymbolView305 <SymbolView
apps/mobile/src/features/threads/threadPresentation.ts+12-12
...@@ -53,8 +53,8 @@ export function resolveThreadStatus(...@@ -53,8 +53,8 @@ export function resolveThreadStatus(
53 return {53 return {
54 kind: "pending-approval",54 kind: "pending-approval",
55 label: "Needs Approval",55 label: "Needs Approval",
56 pillClassName: "bg-amber-500/12 dark:bg-amber-500/16",56 pillClassName: "bg-adaptive-amber-500-a12-a16",
57 textClassName: "text-amber-700 dark:text-amber-300",57 textClassName: "text-adaptive-amber-700-300",
58 iconColor: "#ff9f0a",58 iconColor: "#ff9f0a",
59 iconBackground: "rgba(255,159,10,0.22)",59 iconBackground: "rgba(255,159,10,0.22)",
60 pulse: false,60 pulse: false,
...@@ -65,8 +65,8 @@ export function resolveThreadStatus(...@@ -65,8 +65,8 @@ export function resolveThreadStatus(
65 return {65 return {
66 kind: "awaiting-input",66 kind: "awaiting-input",
67 label: "Awaiting Input",67 label: "Awaiting Input",
68 pillClassName: "bg-indigo-500/12 dark:bg-indigo-500/16",68 pillClassName: "bg-adaptive-indigo-500-a12-a16",
69 textClassName: "text-indigo-700 dark:text-indigo-300",69 textClassName: "text-adaptive-indigo-700-300",
70 iconColor: "#5e5ce6",70 iconColor: "#5e5ce6",
71 iconBackground: "rgba(94,92,230,0.22)",71 iconBackground: "rgba(94,92,230,0.22)",
72 pulse: false,72 pulse: false,
...@@ -77,8 +77,8 @@ export function resolveThreadStatus(...@@ -77,8 +77,8 @@ export function resolveThreadStatus(
77 return {77 return {
78 kind: "working",78 kind: "working",
79 label: "Working",79 label: "Working",
80 pillClassName: "bg-sky-500/12 dark:bg-sky-500/16",80 pillClassName: "bg-adaptive-sky-500-a12-a16",
81 textClassName: "text-sky-700 dark:text-sky-300",81 textClassName: "text-adaptive-sky-700-300",
82 iconColor: "#0a84ff",82 iconColor: "#0a84ff",
83 iconBackground: "rgba(10,132,255,0.22)",83 iconBackground: "rgba(10,132,255,0.22)",
84 pulse: true,84 pulse: true,
...@@ -89,8 +89,8 @@ export function resolveThreadStatus(...@@ -89,8 +89,8 @@ export function resolveThreadStatus(
89 return {89 return {
90 kind: "connecting",90 kind: "connecting",
91 label: "Connecting",91 label: "Connecting",
92 pillClassName: "bg-sky-500/12 dark:bg-sky-500/16",92 pillClassName: "bg-adaptive-sky-500-a12-a16",
93 textClassName: "text-sky-700 dark:text-sky-300",93 textClassName: "text-adaptive-sky-700-300",
94 iconColor: "#0a84ff",94 iconColor: "#0a84ff",
95 iconBackground: "rgba(10,132,255,0.22)",95 iconBackground: "rgba(10,132,255,0.22)",
96 pulse: true,96 pulse: true,
...@@ -101,8 +101,8 @@ export function resolveThreadStatus(...@@ -101,8 +101,8 @@ export function resolveThreadStatus(
101 return {101 return {
102 kind: "error",102 kind: "error",
103 label: "Error",103 label: "Error",
104 pillClassName: "bg-rose-500/12 dark:bg-rose-500/16",104 pillClassName: "bg-adaptive-rose-500-a12-a16",
105 textClassName: "text-rose-700 dark:text-rose-300",105 textClassName: "text-adaptive-rose-700-300",
106 iconColor: "#ff453a",106 iconColor: "#ff453a",
107 iconBackground: "rgba(255,69,58,0.22)",107 iconBackground: "rgba(255,69,58,0.22)",
108 pulse: false,108 pulse: false,
...@@ -117,8 +117,8 @@ export function resolveThreadStatus(...@@ -117,8 +117,8 @@ export function resolveThreadStatus(
117 return {117 return {
118 kind: "plan-ready",118 kind: "plan-ready",
119 label: "Plan Ready",119 label: "Plan Ready",
120 pillClassName: "bg-violet-500/12 dark:bg-violet-500/16",120 pillClassName: "bg-adaptive-violet-500-a12-a16",
121 textClassName: "text-violet-700 dark:text-violet-300",121 textClassName: "text-adaptive-violet-700-300",
122 iconColor: "#bf5af2",122 iconColor: "#bf5af2",
123 iconBackground: "rgba(191,90,242,0.22)",123 iconBackground: "rgba(191,90,242,0.22)",
124 pulse: false,124 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 @@...@@ -1,8 +1,6 @@
1import { describe, expect, it } from "vite-plus/test";1import { describe, expect, it } from "vite-plus/test";
2import * as NodeFS from "node:fs";
3
4import { BUILT_IN_THEME_IDS, BUILT_IN_THEMES } from "@t3tools/shared/themePalettes";2import { 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
7import {5import {
8 createMobileThemePairPatch,6 createMobileThemePairPatch,
...@@ -11,7 +9,6 @@ import {...@@ -11,7 +9,6 @@ import {
11 DEFAULT_MOBILE_THEME_ID,9 DEFAULT_MOBILE_THEME_ID,
12 getMobileThemePreviewColors,10 getMobileThemePreviewColors,
13 getMobileThemeVariables,11 getMobileThemeVariables,
14 MOBILE_THEME_IDS,
15 normalizeMobileThemeId,12 normalizeMobileThemeId,
16 normalizeMobileThemeMode,13 normalizeMobileThemeMode,
17 resolveMobileThemeIds,14 resolveMobileThemeIds,
...@@ -52,13 +49,12 @@ function compositeOver(overlay: string, background: string): string {...@@ -52,13 +49,12 @@ function compositeOver(overlay: string, background: string): string {
5249
53describe("mobile themes", () => {50describe("mobile themes", () => {
54 it("declares every runtime theme variable in the static stylesheet", () => {51 it("declares every runtime theme variable in the static stylesheet", () => {
55 const stylesheet = NodeFS.readFileSync(new URL("../../global.css", import.meta.url), "utf8");52 const generatedVariables = createMobileThemeVariables(BUILT_IN_THEMES[0].colors, "light");
56 const stylesheetVariables = new Set(53 expect(Object.keys(readDefaultMobileThemeVariables("light")).sort()).toEqual(
57 Array.from(stylesheet.matchAll(/--color-[a-z0-9-]+/g), ([variable]) => variable),54 Object.keys(generatedVariables).sort(),
58 );55 );
5956 expect(Object.keys(readDefaultMobileThemeVariables("dark")).sort()).toEqual(
60 expect(Array.from(stylesheetVariables).sort()).toEqual(57 Object.keys(generatedVariables).sort(),
61 Object.keys(DEFAULT_MOBILE_THEME_VARIABLES.light).sort(),
62 );58 );
63 });59 });
6460
...@@ -71,17 +67,11 @@ describe("mobile themes", () => {...@@ -71,17 +67,11 @@ describe("mobile themes", () => {
71 });67 });
7268
73 it("preserves the existing mobile palette as the default", () => {69 it("preserves the existing mobile palette as the default", () => {
74 expect(getMobileThemeVariables(DEFAULT_MOBILE_THEME_ID, "light")["--color-screen"]).toBe(70 expect(readDefaultMobileThemeVariables("light")["--color-screen"]).toBe("#f2f2f7");
75 "#f2f2f7",71 expect(readDefaultMobileThemeVariables("dark")["--color-screen"]).toBe("#0a0a0a");
72 expect(readDefaultMobileThemeVariables("light")["--color-user-bubble-skill-foreground"]).toBe(
73 "#f0abfc",
76 );74 );
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");
85 });75 });
8676
87 it("applies palette overrides on top of the selected built-in theme", () => {77 it("applies palette overrides on top of the selected built-in theme", () => {
...@@ -172,17 +162,11 @@ describe("mobile themes", () => {...@@ -172,17 +162,11 @@ describe("mobile themes", () => {
172 expect(variables["--color-backdrop"]).toBe("rgba(0, 0, 0, 0.22)");162 expect(variables["--color-backdrop"]).toBe("rgba(0, 0, 0, 0.22)");
173 expect(variables["--color-drawer-shadow"]).toBe("rgba(0, 0, 0, 0.12)");163 expect(variables["--color-drawer-shadow"]).toBe("rgba(0, 0, 0, 0.12)");
174 expect(variables["--color-user-bubble-foreground"]).toMatch(/^#/);164 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 );
181 });165 });
182166
183 it("keeps every built-in shadow and backdrop black-based in dark mode", () => {167 it("keeps every built-in shadow and backdrop black-based in dark mode", () => {
184 for (const theme of BUILT_IN_THEMES) {168 for (const themeId of BUILT_IN_THEME_IDS) {
185 const variables = getMobileThemeVariables(normalizeMobileThemeId(theme.id), "dark");169 const variables = getMobileThemeVariables(themeId, "dark");
186 expect(variables["--color-primary-shadow"]).toBe("#000000");170 expect(variables["--color-primary-shadow"]).toBe("#000000");
187 expect(variables["--color-backdrop"]).toBe("rgba(0, 0, 0, 0.48)");171 expect(variables["--color-backdrop"]).toBe("rgba(0, 0, 0, 0.48)");
188 expect(variables["--color-drawer-shadow"]).toBe("rgba(0, 0, 0, 0.32)");172 expect(variables["--color-drawer-shadow"]).toBe("rgba(0, 0, 0, 0.32)");
...@@ -190,7 +174,7 @@ describe("mobile themes", () => {...@@ -190,7 +174,7 @@ describe("mobile themes", () => {
190 });174 });
191175
192 it("keeps placeholders and selected-row labels readable on their mobile surfaces", () => {176 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) {
194 for (const appearance of ["light", "dark"] as const) {178 for (const appearance of ["light", "dark"] as const) {
195 const variables = getMobileThemeVariables(themeId, appearance);179 const variables = getMobileThemeVariables(themeId, appearance);
196 expect(180 expect(
apps/mobile/src/lib/mobileTheme.ts+10-10
...@@ -3,6 +3,7 @@ import {...@@ -3,6 +3,7 @@ import {
3 getThemeColorsForAppearance,3 getThemeColorsForAppearance,
4 MOBILE_DEFAULT_THEME_ID,4 MOBILE_DEFAULT_THEME_ID,
5 MOBILE_THEME_IDS as SHARED_MOBILE_THEME_IDS,5 MOBILE_THEME_IDS as SHARED_MOBILE_THEME_IDS,
6 type BuiltInThemeId,
6 type MobileThemeId as SharedMobileThemeId,7 type MobileThemeId as SharedMobileThemeId,
7 type ThemeAppearance,8 type ThemeAppearance,
8 type ThemeColors,9 type ThemeColors,
...@@ -11,7 +12,6 @@ import {...@@ -11,7 +12,6 @@ import {
11 STANDARD_THEME_PREVIEW_COLORS,12 STANDARD_THEME_PREVIEW_COLORS,
12 type ThemePreviewColors,13 type ThemePreviewColors,
13} from "@t3tools/shared/themePreview";14} from "@t3tools/shared/themePreview";
14import { DEFAULT_MOBILE_THEME_VARIABLES } from "./mobileDefaultTheme";
1515
16export const DEFAULT_MOBILE_THEME_ID = MOBILE_DEFAULT_THEME_ID;16export const DEFAULT_MOBILE_THEME_ID = MOBILE_DEFAULT_THEME_ID;
17export const MOBILE_THEME_IDS = SHARED_MOBILE_THEME_IDS;17export const MOBILE_THEME_IDS = SHARED_MOBILE_THEME_IDS;
...@@ -28,7 +28,7 @@ export const MOBILE_THEME_OPTIONS: ReadonlyArray<{...@@ -28,7 +28,7 @@ export const MOBILE_THEME_OPTIONS: ReadonlyArray<{
28 ...BUILT_IN_THEMES.map((theme) => ({ id: theme.id as MobileThemeId, label: theme.label })),28 ...BUILT_IN_THEMES.map((theme) => ({ id: theme.id as MobileThemeId, label: theme.label })),
29];29];
3030
31type MobileThemeVariable = `--color-${string}`;31export type MobileThemeVariable = `--color-${string}`;
32export type MobileThemeVariables = Readonly<Record<MobileThemeVariable, string>>;32export type MobileThemeVariables = Readonly<Record<MobileThemeVariable, string>>;
3333
34export function normalizeMobileThemeId(value: unknown): MobileThemeId {34export function normalizeMobileThemeId(value: unknown): MobileThemeId {
...@@ -282,18 +282,18 @@ export function createMobileThemeVariables(...@@ -282,18 +282,18 @@ export function createMobileThemeVariables(
282 };282 };
283}283}
284284
285export const MOBILE_THEME_VARIABLE_NAMES = Object.keys(
286 createMobileThemeVariables(BUILT_IN_THEMES[0].colors, "light"),
287) as ReadonlyArray<MobileThemeVariable>;
288
285export function getMobileThemeVariables(289export function getMobileThemeVariables(
286 themeId: MobileThemeId,290 themeId: BuiltInThemeId,
287 appearance: MobileThemeAppearance,291 appearance: MobileThemeAppearance,
288 overrides: Partial<MobileThemeVariables> | null = null,292 overrides: Partial<MobileThemeVariables> | null = null,
289): MobileThemeVariables {293): MobileThemeVariables {
290 const baseVariables = (() => {294 const theme = BUILT_IN_THEMES.find((candidate) => candidate.id === themeId) ?? BUILT_IN_THEMES[0];
291 if (themeId === DEFAULT_MOBILE_THEME_ID) return DEFAULT_MOBILE_THEME_VARIABLES[appearance];295 const colors = getThemeColorsForAppearance(theme, appearance) ?? theme.colors;
292 const theme =296 const baseVariables = createMobileThemeVariables(colors, appearance);
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 })();
297297
298 // The complete base record guarantees that optional overrides cannot leave a token undefined.298 // The complete base record guarantees that optional overrides cannot leave a token undefined.
299 return overrides ? ({ ...baseVariables, ...overrides } as MobileThemeVariables) : baseVariables;299 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", () => {...@@ -196,6 +196,12 @@ describe("mobile connection storage", () => {
196 });196 });
197 });197 });
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
199 it("falls back to secure storage when SQLite cannot save preferences", async () => {205 it("falls back to secure storage when SQLite cannot save preferences", async () => {
200 mocks.setDatabaseFailures(true, true);206 mocks.setDatabaseFailures(true, true);
201 await expect(savePreferencesPatch({ baseFontSize: 19 })).resolves.toEqual({ baseFontSize: 19 });207 await expect(savePreferencesPatch({ baseFontSize: 19 })).resolves.toEqual({ baseFontSize: 19 });
apps/mobile/src/lib/useFontFamily.ts+6-8
...@@ -1,15 +1,13 @@...@@ -1,15 +1,13 @@
1import { useCSSVariable } from "uniwind";1const FONT_FAMILIES = {
22 regular: "DMSans-Regular",
3const FONT_FAMILY_VARIABLES = {3 medium: "DMSans-Medium",
4 regular: "--font-sans",4 bold: "DMSans-Bold",
5 medium: "--font-medium",
6 bold: "--font-bold",
7} as const;5} as const;
86
9/**7/**
10 * Resolves a font family for APIs that require a style object or native prop.8 * Resolves a font family for APIs that require a style object or native prop.
11 * Prefer Uniwind font classes when the target component accepts `className`.9 * Prefer Uniwind font classes when the target component accepts `className`.
12 */10 */
13export function useFontFamily(weight: keyof typeof FONT_FAMILY_VARIABLES): string {11export function useFontFamily(weight: keyof typeof FONT_FAMILIES): string {
14 return useCSSVariable(FONT_FAMILY_VARIABLES[weight]) as string;12 return FONT_FAMILIES[weight];
15}13}
apps/mobile/src/lib/useMobileNavigationTheme.ts+21-12
...@@ -1,22 +1,31 @@...@@ -1,22 +1,31 @@
1import { DarkTheme, DefaultTheme, type Theme } from "@react-navigation/native";1import { DarkTheme, DefaultTheme, type Theme } from "@react-navigation/native";
2import { useMemo } from "react";2import { useMemo } from "react";
33
4import type { MobileThemeAppearance } from "./mobileTheme";4import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
5import { useThemeColor } from "./useThemeColor";5import { useUniwindTheme } from "./useUniwindTheme";
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"));
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();
15 return useMemo(() => {16 return useMemo(() => {
16 const base = appearance === "dark" ? DarkTheme : DefaultTheme;17 const base = appearance === "dark" ? DarkTheme : DefaultTheme;
17 return {18 return {
18 ...base,19 ...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 },
20 };29 };
21 }, [appearance, background, border, card, notification, primary, text]);30 }, [appearance, variables]);
22}31}
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";...@@ -14,7 +14,7 @@ import { Image, StyleSheet } from "react-native";
1414
15import { markdownFileIconSource } from "@t3tools/mobile-markdown-text/file-icons";15import { markdownFileIconSource } from "@t3tools/mobile-markdown-text/file-icons";
16import { resolveMarkdownFileIcon } from "@t3tools/mobile-markdown-text/links";16import { resolveMarkdownFileIcon } from "@t3tools/mobile-markdown-text/links";
17import { useThemeColor } from "../lib/useThemeColor";17import { useUniwindTheme } from "../lib/useUniwindTheme";
18import { useFontFamily } from "../lib/useFontFamily";18import { useFontFamily } from "../lib/useFontFamily";
19import { useScaledTextRole } from "../features/settings/appearance/useScaledTextRole";19import { useScaledTextRole } from "../features/settings/appearance/useScaledTextRole";
20import {20import {
...@@ -110,15 +110,7 @@ export function ComposerEditor({...@@ -110,15 +110,7 @@ export function ComposerEditor({
110 const nativeEventSnapshotsRef = useRef<ComposerNativeEventSnapshot[]>([]);110 const nativeEventSnapshotsRef = useRef<ComposerNativeEventSnapshot[]>([]);
111 const confirmedTokensRef = useRef(collectComposerInlineTokens(props.value));111 const confirmedTokensRef = useRef(collectComposerInlineTokens(props.value));
112 const bodyText = useScaledTextRole("body");112 const bodyText = useScaledTextRole("body");
113 const textColor = useThemeColor("--color-foreground");113 const theme = useUniwindTheme();
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");
122 const fontFamily = useFontFamily("regular");114 const fontFamily = useFontFamily("regular");
123115
124 useImperativeHandle(116 useImperativeHandle(
...@@ -219,15 +211,15 @@ export function ComposerEditor({...@@ -219,15 +211,15 @@ export function ComposerEditor({
219 [],211 [],
220 );212 );
221 const themeJson = JSON.stringify({213 const themeJson = JSON.stringify({
222 text: String(textColor),214 text: theme["--color-foreground"],
223 placeholder: String(placeholderColor),215 placeholder: theme["--color-placeholder"],
224 chipBackground: String(chipBackground),216 chipBackground: theme["--color-subtle"],
225 chipBorder: String(chipBorder),217 chipBorder: theme["--color-border"],
226 chipText: String(chipText),218 chipText: theme["--color-foreground"],
227 skillBackground: String(skillBackground),219 skillBackground: theme["--color-inline-skill-background"],
228 skillBorder: String(skillBorder),220 skillBorder: theme["--color-inline-skill-border"],
229 skillText: String(skillText),221 skillText: theme["--color-inline-skill-foreground"],
230 fileTint: String(fileTint),222 fileTint: theme["--color-icon-muted"],
231 });223 });
232 const resolvedTextStyle = StyleSheet.flatten(textStyle) ?? {};224 const resolvedTextStyle = StyleSheet.flatten(textStyle) ?? {};
233 return (225 return (
apps/mobile/src/native/T3ComposerEditor.native.tsx+11-19
...@@ -18,7 +18,7 @@ import { resolveMarkdownFileIcon } from "@t3tools/mobile-markdown-text/links";...@@ -18,7 +18,7 @@ import { resolveMarkdownFileIcon } from "@t3tools/mobile-markdown-text/links";
18import { MOBILE_TYPOGRAPHY } from "../lib/typography";18import { MOBILE_TYPOGRAPHY } from "../lib/typography";
19import { useNativePaste } from "../lib/useNativePaste";19import { useNativePaste } from "../lib/useNativePaste";
20import { useFontFamily } from "../lib/useFontFamily";20import { useFontFamily } from "../lib/useFontFamily";
21import { useThemeColor } from "../lib/useThemeColor";21import { useUniwindTheme } from "../lib/useUniwindTheme";
22import {22import {
23 acknowledgeComposerNativeEvent,23 acknowledgeComposerNativeEvent,
24 assumeComposerControlledState,24 assumeComposerControlledState,
...@@ -111,15 +111,7 @@ export function ComposerEditor({...@@ -111,15 +111,7 @@ export function ComposerEditor({
111 const nativeEventSnapshotsRef = useRef<ComposerNativeEventSnapshot[]>([]);111 const nativeEventSnapshotsRef = useRef<ComposerNativeEventSnapshot[]>([]);
112 const [initialConfirmedTokens] = useState(() => collectComposerInlineTokens(props.value));112 const [initialConfirmedTokens] = useState(() => collectComposerInlineTokens(props.value));
113 const confirmedTokensRef = useRef(initialConfirmedTokens);113 const confirmedTokensRef = useRef(initialConfirmedTokens);
114 const textColor = useThemeColor("--color-foreground");114 const theme = useUniwindTheme();
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");
123 const handlePaste = useNativePaste((uris) => onPasteImages?.(uris));115 const handlePaste = useNativePaste((uris) => onPasteImages?.(uris));
124116
125 useImperativeHandle(117 useImperativeHandle(
...@@ -220,15 +212,15 @@ export function ComposerEditor({...@@ -220,15 +212,15 @@ export function ComposerEditor({
220 [],212 [],
221 );213 );
222 const themeJson = JSON.stringify({214 const themeJson = JSON.stringify({
223 text: String(textColor),215 text: theme["--color-foreground"],
224 placeholder: String(placeholderColor),216 placeholder: theme["--color-placeholder"],
225 chipBackground: String(chipBackground),217 chipBackground: theme["--color-subtle"],
226 chipBorder: String(chipBorder),218 chipBorder: theme["--color-border"],
227 chipText: String(chipText),219 chipText: theme["--color-foreground"],
228 skillBackground: String(skillBackground),220 skillBackground: theme["--color-inline-skill-background"],
229 skillBorder: String(skillBorder),221 skillBorder: theme["--color-inline-skill-border"],
230 skillText: String(skillText),222 skillText: theme["--color-inline-skill-foreground"],
231 fileTint: String(fileTint),223 fileTint: theme["--color-icon-muted"],
232 });224 });
233 const resolvedTextStyle = StyleSheet.flatten(textStyle) ?? {};225 const resolvedTextStyle = StyleSheet.flatten(textStyle) ?? {};
234 const regularFontFamily = useFontFamily("regular");226 const regularFontFamily = useFontFamily("regular");
apps/mobile/src/native/T3ComposerEditor.tsx+2-5
...@@ -2,7 +2,6 @@ import { TextInputWrapper } from "expo-paste-input";...@@ -2,7 +2,6 @@ import { TextInputWrapper } from "expo-paste-input";
2import { useImperativeHandle, useRef } from "react";2import { useImperativeHandle, useRef } from "react";
3import { TextInput, type TextInput as RNTextInput } from "react-native";3import { TextInput, type TextInput as RNTextInput } from "react-native";
44
5import { useThemeColor } from "../lib/useThemeColor";
6import { useFontFamily } from "../lib/useFontFamily";5import { useFontFamily } from "../lib/useFontFamily";
7import { useScaledTextRole } from "../features/settings/appearance/useScaledTextRole";6import { useScaledTextRole } from "../features/settings/appearance/useScaledTextRole";
8import { useNativePaste } from "../lib/useNativePaste";7import { useNativePaste } from "../lib/useNativePaste";
...@@ -21,8 +20,6 @@ export function ComposerEditor({...@@ -21,8 +20,6 @@ export function ComposerEditor({
21}: ComposerEditorProps) {20}: ComposerEditorProps) {
22 const inputRef = useRef<RNTextInput>(null);21 const inputRef = useRef<RNTextInput>(null);
23 const bodyText = useScaledTextRole("body");22 const bodyText = useScaledTextRole("body");
24 const foregroundColor = useThemeColor("--color-foreground");
25 const placeholderColor = useThemeColor("--color-placeholder");
26 const fontFamily = useFontFamily("regular");23 const fontFamily = useFontFamily("regular");
27 const handlePaste = useNativePaste((uris) => onPasteImages?.(uris));24 const handlePaste = useNativePaste((uris) => onPasteImages?.(uris));
2825
...@@ -45,12 +42,12 @@ export function ComposerEditor({...@@ -45,12 +42,12 @@ export function ComposerEditor({
45 selection={selection}42 selection={selection}
46 onSelectionChange={(event) => props.onSelectionChange?.(event.nativeEvent.selection)}43 onSelectionChange={(event) => props.onSelectionChange?.(event.nativeEvent.selection)}
47 multiline={props.multiline ?? true}44 multiline={props.multiline ?? true}
48 placeholderTextColor={placeholderColor}45 placeholderTextColorClassName={"accent-placeholder"}
46 className="text-foreground"
49 style={[47 style={[
50 {48 {
51 flex: 1,49 flex: 1,
52 minHeight: 0,50 minHeight: 0,
53 color: foregroundColor,
54 fontFamily,51 fontFamily,
55 ...bodyText,52 ...bodyText,
56 paddingVertical: contentInsetVertical,53 paddingVertical: contentInsetVertical,
apps/mobile/src/state/thread-pr-presentation.ts+3-3
...@@ -17,9 +17,9 @@ export interface ThreadPrPresentation {...@@ -17,9 +17,9 @@ export interface ThreadPrPresentation {
17}17}
1818
19const PR_STATE_TEXT_CLASS: Record<ThreadPr["state"], string> = {19const PR_STATE_TEXT_CLASS: Record<ThreadPr["state"], string> = {
20 open: "text-emerald-600 dark:text-emerald-400",20 open: "text-adaptive-emerald-600-400",
21 merged: "text-violet-600 dark:text-violet-400",21 merged: "text-adaptive-violet-600-400",
22 closed: "text-zinc-500 dark:text-zinc-400",22 closed: "text-adaptive-zinc-500-400",
23};23};
2424
25export function presentThreadPr(25export function presentThreadPr(
apps/mobile/src/state/use-thread-pr.test.ts+1-1
...@@ -17,7 +17,7 @@ describe("presentThreadPr", () => {...@@ -17,7 +17,7 @@ describe("presentThreadPr", () => {
17 expect(presentThreadPr(pullRequest, undefined)).toMatchObject({17 expect(presentThreadPr(pullRequest, undefined)).toMatchObject({
18 label: "3774",18 label: "3774",
19 accessibilityLabel: "#3774 pull request merged",19 accessibilityLabel: "#3774 pull request merged",
20 textClassName: "text-violet-600 dark:text-violet-400",20 textClassName: "text-adaptive-violet-600-400",
21 });21 });
22 });22 });
2323
apps/mobile/uniwind-types.d.ts+1-1
...@@ -3,7 +3,7 @@...@@ -3,7 +3,7 @@
33
4declare module 'uniwind' {4declare module 'uniwind' {
5 export interface UniwindConfig {5 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']
7 }7 }
8}8}
99
docs/user/mobile-appearance.md+3
...@@ -11,5 +11,8 @@ To change themes:...@@ -11,5 +11,8 @@ To change themes:
113. Choose a theme.113. Choose a theme.
124. Select **System**, **Light**, or **Dark**.124. 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
14**System** follows the device appearance automatically. Theme, text, code, and terminal appearance17**System** follows the device appearance automatically. Theme, text, code, and terminal appearance
15preferences are stored on the device.18preferences are stored on the device.
oxlint-plugin-t3code/index.ts+2
...@@ -4,6 +4,7 @@ import namespaceNodeImports from "./rules/namespace-node-imports.ts";...@@ -4,6 +4,7 @@ import namespaceNodeImports from "./rules/namespace-node-imports.ts";
4import noGlobalProcessRuntime from "./rules/no-global-process-runtime.ts";4import noGlobalProcessRuntime from "./rules/no-global-process-runtime.ts";
5import noInlineSchemaCompile from "./rules/no-inline-schema-compile.ts";5import noInlineSchemaCompile from "./rules/no-inline-schema-compile.ts";
6import noManualEffectRuntimeInTests from "./rules/no-manual-effect-runtime-in-tests.ts";6import noManualEffectRuntimeInTests from "./rules/no-manual-effect-runtime-in-tests.ts";
7import noMobileUniwindThemeEscapeHatches from "./rules/no-mobile-uniwind-theme-escape-hatches.ts";
7import noNativeTitleTooltip from "./rules/no-native-title-tooltip.ts";8import noNativeTitleTooltip from "./rules/no-native-title-tooltip.ts";
89
9export default definePlugin({10export default definePlugin({
...@@ -15,6 +16,7 @@ export default definePlugin({...@@ -15,6 +16,7 @@ export default definePlugin({
15 "no-global-process-runtime": noGlobalProcessRuntime,16 "no-global-process-runtime": noGlobalProcessRuntime,
16 "no-inline-schema-compile": noInlineSchemaCompile,17 "no-inline-schema-compile": noInlineSchemaCompile,
17 "no-manual-effect-runtime-in-tests": noManualEffectRuntimeInTests,18 "no-manual-effect-runtime-in-tests": noManualEffectRuntimeInTests,
19 "no-mobile-uniwind-theme-escape-hatches": noMobileUniwindThemeEscapeHatches,
18 "no-native-title-tooltip": noNativeTitleTooltip,20 "no-native-title-tooltip": noNativeTitleTooltip,
19 },21 },
20});22});
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 = (...@@ -114,6 +114,7 @@ export const createOxlintRuleHarness = (
114 rules: { [ruleName]: "error" },114 rules: { [ruleName]: "error" },
115 }),115 }),
116 );116 );
117 yield* fs.makeDirectory(path.dirname(sourcePath), { recursive: true });
117 yield* fs.writeFileString(sourcePath, source);118 yield* fs.writeFileString(sourcePath, source);
118119
119 // Run through the current Node binary: oxlint's bin is an extensionless120 // Run through the current Node binary: oxlint's bin is an extensionless
vite.config.ts+1
...@@ -120,6 +120,7 @@ export default defineConfig({...@@ -120,6 +120,7 @@ export default defineConfig({
120 "t3code/no-global-process-runtime": "error",120 "t3code/no-global-process-runtime": "error",
121 "t3code/no-inline-schema-compile": "warn",121 "t3code/no-inline-schema-compile": "warn",
122 "t3code/no-manual-effect-runtime-in-tests": "error",122 "t3code/no-manual-effect-runtime-in-tests": "error",
123 "t3code/no-mobile-uniwind-theme-escape-hatches": "error",
123 "t3code/no-native-title-tooltip": "error",124 "t3code/no-native-title-tooltip": "error",
124 "t3code/namespace-node-imports": "error",125 "t3code/namespace-node-imports": "error",
125 },126 },