1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta
6 name="viewport"
7 content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content"
8 />
9 <meta name="theme-color" content="#0a0a0a" />
10 <link rel="icon" href="/favicon.ico" sizes="48x48" />
11 <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
12 <link rel="manifest" href="/manifest.webmanifest" />
13 <script>
14 (() => {
15 const LIGHT_BACKGROUND = "#ffffff";
16 const DARK_BACKGROUND = "#0a0a0a";
17 const CUSTOM_THEMES_STORAGE_KEY = "t3code:themes:v1";
18 const THEME_FOLLOW_SYSTEM_STORAGE_KEY = "t3code:theme-follow-system";
19 const THEME_APPEARANCE_MODE_STORAGE_KEY = "t3code:theme-appearance-mode";
20 const THEME_HALVES_STORAGE_KEY = "t3code:theme-halves:v1";
21 const SPLASH_COLORS = {
22 light: {
23 background: "#ffffff",
24 foreground: "#262626",
25 accent: "#4f46e5",
26 },
27 dark: {
28 background: "#0a0a0a",
29 foreground: "#f5f5f5",
30 accent: "#818cf8",
31 },
32 };
33 // These are the runtime defaults used to complete a custom palette
34 // when a stored theme omits a role. Keep them separate from the
35 // unselected splash so a partial custom theme does not flash generic
36 // app colors before React mounts.
37 const DEFAULT_THEME_PALETTES = {
38 light: {
39 background: "oklch(0.982446 0.010114 325.653)",
40 foreground: "oklch(0.325698 0.116116 325.037)",
41 accent: "oklch(0.591646 0.217985 0.584)",
42 chrome: "oklch(0.982446 0.010114 325.653)",
43 },
44 dark: {
45 background: "oklch(0.22813 0.020366 307.469)",
46 foreground: "oklch(0.980735 0.004092 301.426)",
47 accent: "oklch(0.460685 0.185347 4.099)",
48 chrome: "oklch(0.22813 0.020366 307.469)",
49 },
50 };
51 // Keep this small boot-time copy in sync with the built-in palettes so
52 // the selected theme is visible before the app has mounted.
53 const BUILT_IN_THEME_PALETTES = {
54 "t3-chat": {
55 light: {
56 background: "oklch(0.982446 0.010114 325.653)",
57 foreground: "oklch(0.325698 0.116116 325.037)",
58 accent: "oklch(0.591646 0.217985 0.584)",
59 chrome: "oklch(0.982446 0.010114 325.653)",
60 },
61 dark: {
62 background: "oklch(0.22813 0.020366 307.469)",
63 foreground: "oklch(0.980735 0.004092 301.426)",
64 accent: "oklch(0.460685 0.185347 4.099)",
65 chrome: "oklch(0.22813 0.020366 307.469)",
66 },
67 },
68 grove: {
69 light: {
70 background: "oklch(0.972369 0.005497 157.15)",
71 foreground: "oklch(0.222003 0.03479 328.979)",
72 accent: "oklch(0.523295 0.112292 158.089)",
73 chrome: "oklch(0.972369 0.005497 157.15)",
74 },
75 dark: {
76 background: "oklch(0.260865 0.02152 162.75)",
77 foreground: "oklch(0.990339 0.008411 325.64)",
78 accent: "oklch(0.796228 0.133058 157.319)",
79 chrome: "oklch(0.260865 0.02152 162.75)",
80 },
81 },
82 ocean: {
83 light: {
84 background: "oklch(0.974199 0.002856 241.597)",
85 foreground: "oklch(0.222003 0.03479 328.979)",
86 accent: "oklch(0.536684 0.120219 247.01)",
87 chrome: "oklch(0.974199 0.002856 241.597)",
88 },
89 dark: {
90 background: "oklch(0.242641 0.024125 250.573)",
91 foreground: "oklch(0.990339 0.008411 325.64)",
92 accent: "oklch(0.758933 0.105833 241.548)",
93 chrome: "oklch(0.242641 0.024125 250.573)",
94 },
95 },
96 ember: {
97 light: {
98 background: "oklch(0.976527 0.002685 60.725)",
99 foreground: "oklch(0.222003 0.03479 328.979)",
100 accent: "oklch(0.552831 0.129438 44.656)",
101 chrome: "oklch(0.976527 0.002685 60.725)",
102 },
103 dark: {
104 background: "oklch(0.245899 0.019144 42.044)",
105 foreground: "oklch(0.990339 0.008411 325.64)",
106 accent: "oklch(0.762174 0.124117 52.082)",
107 chrome: "oklch(0.245899 0.019144 42.044)",
108 },
109 },
110 iris: {
111 light: {
112 background: "oklch(0.976531 0.003855 303.226)",
113 foreground: "oklch(0.222003 0.03479 328.979)",
114 accent: "oklch(0.525348 0.15373 294.176)",
115 chrome: "oklch(0.976531 0.003855 303.226)",
116 },
117 dark: {
118 background: "oklch(0.225975 0.031062 293.741)",
119 foreground: "oklch(0.990339 0.008411 325.64)",
120 accent: "oklch(0.671712 0.169136 293.929)",
121 chrome: "oklch(0.225975 0.031062 293.741)",
122 },
123 },
124 };
125 // Keep the built-in mode list in sync with the runtime so the selected
126 // T3 Chat palette is visible before React has mounted.
127 const BUILT_IN_THEME_MODES = Object.fromEntries(
128 Object.entries(BUILT_IN_THEME_PALETTES).map(([id, palettes]) => [
129 id,
130 Object.keys(palettes),
131 ]),
132 );
133 const RESERVED_THEME_IDS = [
134 "system",
135 "light",
136 "dark",
137 "t3-chat",
138 "grove",
139 "ocean",
140 "ember",
141 "iris",
142 "t3-chat-dark",
143 "t3-grove",
144 "t3-ocean",
145 "t3-ember",
146 "t3-iris",
147 ];
148 // Update every theme-color meta so any element another layer added
149 // carries the resolved color too.
150 const setThemeColor = (color) => {
151 for (const meta of document.querySelectorAll('meta[name="theme-color"]')) {
152 meta.setAttribute("content", color);
153 }
154 };
155
156 // The runtime decodes the same literal CSS formats into OKLCH. Keep
157 // contextual colors out: they cannot be resolved from stored data.
158 const SYSTEM_COLOR_KEYWORD =
159 /^(?:accentcolor(?:text)?|active(?:border|caption|text)|appworkspace|background|button(?:border|face|highlight|shadow|text)|canvas(?:text)?|captiontext|field(?:text)?|graytext|highlight(?:text)?|inactive(?:border|caption(?:text)?)|info(?:background|text)|linktext|mark(?:text)?|menu(?:text)?|scrollbar|selecteditem(?:text)?|threed(?:darkshadow|face|highlight|lightshadow|shadow)|visitedtext|window(?:frame|text)?)$/i;
160 const isThemeColor = (value) => {
161 if (typeof value !== "string" || typeof CSS === "undefined") return false;
162 const color = value.trim();
163 if (
164 /^(?:currentcolor|inherit|initial|revert|revert-layer|unset)$/i.test(color) ||
165 SYSTEM_COLOR_KEYWORD.test(color) ||
166 /\bfrom\b|(?:var|env|attr|calc|min|max|clamp)\(/i.test(color)
167 ) {
168 return false;
169 }
170 const isLiteralColor =
171 /^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(color) ||
172 /^[a-z]+$/i.test(color) ||
173 /^(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\(/i.test(color);
174 return isLiteralColor && CSS.supports("color", color);
175 };
176 const isRecord = (value) =>
177 typeof value === "object" && value !== null && !Array.isArray(value);
178 const isThemeId = (value) =>
179 typeof value === "string" && /^[a-z0-9](?:[a-z0-9-]{0,47})$/.test(value);
180 const isThemeLabel = (value) =>
181 typeof value === "string" && value.trim().length > 0 && value.trim().length <= 48;
182 const isThemePreferenceMode = (value) =>
183 value === "light" || value === "dark" || value === "system";
184 const isThemeAppearance = (value) => value === "light" || value === "dark";
185 const isStoredCustomTheme = (value) => {
186 if (
187 !isRecord(value) ||
188 !isThemeId(value.id) ||
189 RESERVED_THEME_IDS.includes(value.id) ||
190 !isThemeLabel(value.label) ||
191 !isThemeAppearance(value.appearance) ||
192 !isRecord(value.colors)
193 ) {
194 return false;
195 }
196 if (value.variants === undefined) return true;
197 if (!isRecord(value.variants)) return false;
198 return Object.entries(value.variants).every(
199 ([appearance, colors]) =>
200 isThemeAppearance(appearance) &&
201 (appearance === value.appearance || isRecord(colors)),
202 );
203 };
204 const findCustomTheme = (themeId) => {
205 if (RESERVED_THEME_IDS.includes(themeId)) return null;
206 try {
207 const parsed = JSON.parse(
208 window.localStorage.getItem(CUSTOM_THEMES_STORAGE_KEY) ?? "null",
209 );
210 if (!Array.isArray(parsed)) return null;
211 return (
212 parsed.find((value) => isStoredCustomTheme(value) && value.id === themeId) ?? null
213 );
214 } catch {
215 return null;
216 }
217 };
218
219 try {
220 const storedTheme = window.localStorage.getItem("t3code:theme");
221 const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
222 // Older builds stored the dark T3 Chat palette as its own theme id
223 // and every maintainer theme under a t3- prefix; keep those
224 // preferences readable. Mirrors the runtime's legacy alias table.
225 const LEGACY_THEME_IDS = {
226 "t3-chat-dark": "t3-chat",
227 "t3-grove": "grove",
228 "t3-ocean": "ocean",
229 "t3-ember": "ember",
230 "t3-iris": "iris",
231 };
232 const isLegacyT3ChatDark = storedTheme === "t3-chat-dark";
233 const themeId =
234 storedTheme === null ? "" : (LEGACY_THEME_IDS[storedTheme] ?? storedTheme);
235 const legacyMode = isLegacyT3ChatDark ? "dark" : null;
236 const builtInModes = BUILT_IN_THEME_MODES[themeId] ?? null;
237 const customTheme = builtInModes ? null : findCustomTheme(themeId);
238 const themeModes =
239 builtInModes ??
240 (customTheme
241 ? ["light", "dark"].filter(
242 (mode) => mode === customTheme.appearance || customTheme.variants?.[mode],
243 )
244 : null);
245 // Mirrors the runtime's isKnownThemePreference.
246 const isKnownTheme =
247 storedTheme === "light" ||
248 storedTheme === "dark" ||
249 storedTheme === "system" ||
250 themeModes !== null;
251 const theme = isKnownTheme && storedTheme ? storedTheme : "system";
252 const isThemed = isKnownTheme && themeModes !== null;
253 const baseAppearance = customTheme ? customTheme.appearance : "light";
254 const systemMode = prefersDark ? "dark" : "light";
255 const storedAppearanceMode = window.localStorage.getItem(
256 THEME_APPEARANCE_MODE_STORAGE_KEY,
257 );
258 const storedFollowSystem = window.localStorage.getItem(THEME_FOLLOW_SYSTEM_STORAGE_KEY);
259 // Mirrors the runtime's getThemePreferenceMode fallback: a bare
260 // "light"/"dark" preference names its own mode.
261 const preferenceMode =
262 theme === "light" || theme === "dark" ? theme : (legacyMode ?? baseAppearance);
263 const appearanceMode = isThemePreferenceMode(storedAppearanceMode)
264 ? storedAppearanceMode
265 : storedFollowSystem === "true"
266 ? "system"
267 : storedFollowSystem === "false"
268 ? preferenceMode
269 : theme === "system"
270 ? "system"
271 : preferenceMode;
272 // A valid mix half for the wanted appearance takes over: it decides
273 // light vs dark even when the base theme cannot render that mode.
274 // Mirrors the runtime's halves-aware resolveThemeAppearance.
275 const wantedMode = appearanceMode === "system" ? systemMode : appearanceMode;
276 let halfThemeId = null;
277 let halfCustomTheme = null;
278 try {
279 const halvesRaw = JSON.parse(
280 window.localStorage.getItem(THEME_HALVES_STORAGE_KEY) ?? "null",
281 );
282 const candidate = isRecord(halvesRaw) ? halvesRaw[wantedMode] : null;
283 if (typeof candidate === "string") {
284 const halfId = LEGACY_THEME_IDS[candidate] ?? candidate;
285 const halfBuiltIn = BUILT_IN_THEME_MODES[halfId] ?? null;
286 const halfCustom = halfBuiltIn ? null : findCustomTheme(halfId);
287 const halfModes =
288 halfBuiltIn ??
289 (halfCustom
290 ? ["light", "dark"].filter(
291 (mode) => mode === halfCustom.appearance || halfCustom.variants?.[mode],
292 )
293 : null);
294 if (halfModes && halfModes.includes(wantedMode)) {
295 halfThemeId = halfId;
296 halfCustomTheme = halfCustom;
297 }
298 }
299 } catch {
300 // A malformed mix degrades to the base preference.
301 }
302 const themeMode = !isThemed
303 ? null
304 : appearanceMode === "system"
305 ? themeModes.includes(systemMode)
306 ? systemMode
307 : baseAppearance
308 : themeModes.includes(appearanceMode)
309 ? appearanceMode
310 : baseAppearance;
311 const isDark =
312 halfThemeId !== null
313 ? wantedMode === "dark"
314 : isThemed
315 ? themeMode === "dark"
316 : appearanceMode === "system"
317 ? prefersDark
318 : appearanceMode === "dark";
319 const effectiveIsThemed = halfThemeId !== null || isThemed;
320 const effectiveThemeId = halfThemeId ?? themeId;
321 const effectiveCustomTheme = halfThemeId !== null ? halfCustomTheme : customTheme;
322 const effectiveThemeMode = halfThemeId !== null ? wantedMode : themeMode;
323 const customColors =
324 effectiveIsThemed && effectiveCustomTheme
325 ? effectiveThemeMode === effectiveCustomTheme.appearance
326 ? effectiveCustomTheme.colors
327 : (effectiveCustomTheme.variants?.[effectiveThemeMode] ??
328 effectiveCustomTheme.colors)
329 : null;
330 const customDefaults =
331 effectiveCustomTheme && effectiveThemeMode
332 ? DEFAULT_THEME_PALETTES[effectiveThemeMode]
333 : null;
334 // The runtime tolerates individual malformed colors, so fall back
335 // per role rather than dropping the theme.
336 const builtInSplash =
337 effectiveIsThemed && !effectiveCustomTheme && effectiveThemeMode
338 ? (BUILT_IN_THEME_PALETTES[effectiveThemeId]?.[effectiveThemeMode] ?? null)
339 : null;
340 const fallbackSplash = isDark ? SPLASH_COLORS.dark : SPLASH_COLORS.light;
341 const customSplash = customColors
342 ? {
343 background: isThemeColor(customColors.canvas)
344 ? customColors.canvas
345 : (customDefaults?.background ?? fallbackSplash.background),
346 foreground: isThemeColor(customColors.text)
347 ? customColors.text
348 : (customDefaults?.foreground ?? fallbackSplash.foreground),
349 accent: isThemeColor(customColors.accent)
350 ? customColors.accent
351 : (customDefaults?.accent ?? fallbackSplash.accent),
352 }
353 : null;
354 if (effectiveIsThemed) {
355 document.documentElement.dataset.themeId = effectiveCustomTheme
356 ? effectiveCustomTheme.id
357 : effectiveThemeId;
358 } else {
359 delete document.documentElement.dataset.themeId;
360 }
361 // A resolved half is a selection even when the base theme is gone
362 // (deleting the base leaves the mix pointing at a real theme).
363 const hasSelection = halfThemeId !== null || (isKnownTheme && storedTheme !== null);
364 if (hasSelection) {
365 document.documentElement.dataset.themeSelected = "true";
366 } else {
367 delete document.documentElement.dataset.themeSelected;
368 }
369 document.documentElement.classList.toggle("dark", isDark);
370 const chromeColor = customColors
371 ? isThemeColor(customColors.chrome)
372 ? customColors.chrome
373 : (customDefaults?.chrome ?? fallbackSplash.background)
374 : builtInSplash
375 ? builtInSplash.chrome
376 : isDark
377 ? DARK_BACKGROUND
378 : LIGHT_BACKGROUND;
379 document.documentElement.style.backgroundColor = chromeColor;
380 if (hasSelection) {
381 const splashColors = customSplash ?? builtInSplash ?? fallbackSplash;
382 for (const [name, value] of Object.entries(splashColors)) {
383 document.documentElement.style.setProperty(`--boot-${name}`, value);
384 }
385 }
386 setThemeColor(chromeColor);
387 } catch {
388 // Mirror the runtime's storage-failure fallback: follow the OS.
389 let prefersDark = true;
390 try {
391 prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
392 } catch {
393 // Keep the dark default when even matchMedia is unavailable.
394 }
395 delete document.documentElement.dataset.themeId;
396 delete document.documentElement.dataset.themeSelected;
397 document.documentElement.classList.toggle("dark", prefersDark);
398 const fallbackColor = prefersDark ? DARK_BACKGROUND : LIGHT_BACKGROUND;
399 document.documentElement.style.backgroundColor = fallbackColor;
400 setThemeColor(fallbackColor);
401 }
402 })();
403 </script>
404 <style>
405 html,
406 body,
407 #root {
408 width: 100%;
409 height: 100%;
410 margin: 0;
411 }
412
413 body {
414 background: #ffffff;
415 color: #262626;
416 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
417 }
418
419 html.dark body {
420 background: #0a0a0a;
421 color: #f5f5f5;
422 }
423
424 #boot-shell {
425 position: relative;
426 overflow: hidden;
427 display: flex;
428 min-height: 100%;
429 align-items: center;
430 justify-content: center;
431 background: inherit;
432 color: inherit;
433 }
434
435 html[data-theme-selected="true"] #boot-shell {
436 background: var(--boot-background);
437 color: var(--boot-foreground);
438 }
439
440 #boot-shell-card {
441 position: relative;
442 z-index: 1;
443 display: flex;
444 align-items: center;
445 justify-content: center;
446 width: 96px;
447 height: 96px;
448 }
449
450 #boot-shell-logo {
451 width: 64px;
452 height: 64px;
453 object-fit: contain;
454 }
455
456 #boot-error {
457 display: grid;
458 justify-items: center;
459 gap: 16px;
460 max-width: 600px;
461 padding: 24px;
462 text-align: center;
463 }
464
465 #boot-error p {
466 margin: 0;
467 overflow-wrap: anywhere;
468 }
469
470 #boot-error button {
471 padding: 8px 16px;
472 border: 1px solid currentColor;
473 border-radius: 4px;
474 background: transparent;
475 color: inherit;
476 font: inherit;
477 cursor: pointer;
478 }
479 </style>
480 <title>The Knife</title>
481 </head>
482 <body>
483 <div id="root">
484 <div id="boot-shell">
485 <div id="boot-shell-card" aria-label="The Knife splash screen">
486 <img id="boot-shell-logo" src="/apple-touch-icon.png" alt="The Knife" />
487 </div>
488 </div>
489 </div>
490 <script type="module" src="/src/bootstrap.ts"></script>
491 </body>
492</html>