| 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> |