t3-code-android-nightly/apps/web/index.html

492 lines
20 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content"
/>
<meta name="theme-color" content="#0a0a0a" />
<link rel="icon" href="/favicon.ico" sizes="48x48" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/manifest.webmanifest" />
<script>
(() => {
const LIGHT_BACKGROUND = "#ffffff";
const DARK_BACKGROUND = "#0a0a0a";
const CUSTOM_THEMES_STORAGE_KEY = "t3code:themes:v1";
const THEME_FOLLOW_SYSTEM_STORAGE_KEY = "t3code:theme-follow-system";
const THEME_APPEARANCE_MODE_STORAGE_KEY = "t3code:theme-appearance-mode";
const THEME_HALVES_STORAGE_KEY = "t3code:theme-halves:v1";
const SPLASH_COLORS = {
light: {
background: "#ffffff",
foreground: "#262626",
accent: "#4f46e5",
},
dark: {
background: "#0a0a0a",
foreground: "#f5f5f5",
accent: "#818cf8",
},
};
// These are the runtime defaults used to complete a custom palette
// when a stored theme omits a role. Keep them separate from the
// unselected splash so a partial custom theme does not flash generic
// app colors before React mounts.
const DEFAULT_THEME_PALETTES = {
light: {
background: "oklch(0.982446 0.010114 325.653)",
foreground: "oklch(0.325698 0.116116 325.037)",
accent: "oklch(0.591646 0.217985 0.584)",
chrome: "oklch(0.982446 0.010114 325.653)",
},
dark: {
background: "oklch(0.22813 0.020366 307.469)",
foreground: "oklch(0.980735 0.004092 301.426)",
accent: "oklch(0.460685 0.185347 4.099)",
chrome: "oklch(0.22813 0.020366 307.469)",
},
};
// Keep this small boot-time copy in sync with the built-in palettes so
// the selected theme is visible before the app has mounted.
const BUILT_IN_THEME_PALETTES = {
"t3-chat": {
light: {
background: "oklch(0.982446 0.010114 325.653)",
foreground: "oklch(0.325698 0.116116 325.037)",
accent: "oklch(0.591646 0.217985 0.584)",
chrome: "oklch(0.982446 0.010114 325.653)",
},
dark: {
background: "oklch(0.22813 0.020366 307.469)",
foreground: "oklch(0.980735 0.004092 301.426)",
accent: "oklch(0.460685 0.185347 4.099)",
chrome: "oklch(0.22813 0.020366 307.469)",
},
},
grove: {
light: {
background: "oklch(0.972369 0.005497 157.15)",
foreground: "oklch(0.222003 0.03479 328.979)",
accent: "oklch(0.523295 0.112292 158.089)",
chrome: "oklch(0.972369 0.005497 157.15)",
},
dark: {
background: "oklch(0.260865 0.02152 162.75)",
foreground: "oklch(0.990339 0.008411 325.64)",
accent: "oklch(0.796228 0.133058 157.319)",
chrome: "oklch(0.260865 0.02152 162.75)",
},
},
ocean: {
light: {
background: "oklch(0.974199 0.002856 241.597)",
foreground: "oklch(0.222003 0.03479 328.979)",
accent: "oklch(0.536684 0.120219 247.01)",
chrome: "oklch(0.974199 0.002856 241.597)",
},
dark: {
background: "oklch(0.242641 0.024125 250.573)",
foreground: "oklch(0.990339 0.008411 325.64)",
accent: "oklch(0.758933 0.105833 241.548)",
chrome: "oklch(0.242641 0.024125 250.573)",
},
},
ember: {
light: {
background: "oklch(0.976527 0.002685 60.725)",
foreground: "oklch(0.222003 0.03479 328.979)",
accent: "oklch(0.552831 0.129438 44.656)",
chrome: "oklch(0.976527 0.002685 60.725)",
},
dark: {
background: "oklch(0.245899 0.019144 42.044)",
foreground: "oklch(0.990339 0.008411 325.64)",
accent: "oklch(0.762174 0.124117 52.082)",
chrome: "oklch(0.245899 0.019144 42.044)",
},
},
iris: {
light: {
background: "oklch(0.976531 0.003855 303.226)",
foreground: "oklch(0.222003 0.03479 328.979)",
accent: "oklch(0.525348 0.15373 294.176)",
chrome: "oklch(0.976531 0.003855 303.226)",
},
dark: {
background: "oklch(0.225975 0.031062 293.741)",
foreground: "oklch(0.990339 0.008411 325.64)",
accent: "oklch(0.671712 0.169136 293.929)",
chrome: "oklch(0.225975 0.031062 293.741)",
},
},
};
// Keep the built-in mode list in sync with the runtime so the selected
// T3 Chat palette is visible before React has mounted.
const BUILT_IN_THEME_MODES = Object.fromEntries(
Object.entries(BUILT_IN_THEME_PALETTES).map(([id, palettes]) => [
id,
Object.keys(palettes),
]),
);
const RESERVED_THEME_IDS = [
"system",
"light",
"dark",
"t3-chat",
"grove",
"ocean",
"ember",
"iris",
"t3-chat-dark",
"t3-grove",
"t3-ocean",
"t3-ember",
"t3-iris",
];
// Update every theme-color meta so any element another layer added
// carries the resolved color too.
const setThemeColor = (color) => {
for (const meta of document.querySelectorAll('meta[name="theme-color"]')) {
meta.setAttribute("content", color);
}
};
// The runtime decodes the same literal CSS formats into OKLCH. Keep
// contextual colors out: they cannot be resolved from stored data.
const SYSTEM_COLOR_KEYWORD =
/^(?: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;
const isThemeColor = (value) => {
if (typeof value !== "string" || typeof CSS === "undefined") return false;
const color = value.trim();
if (
/^(?:currentcolor|inherit|initial|revert|revert-layer|unset)$/i.test(color) ||
SYSTEM_COLOR_KEYWORD.test(color) ||
/\bfrom\b|(?:var|env|attr|calc|min|max|clamp)\(/i.test(color)
) {
return false;
}
const isLiteralColor =
/^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(color) ||
/^[a-z]+$/i.test(color) ||
/^(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\(/i.test(color);
return isLiteralColor && CSS.supports("color", color);
};
const isRecord = (value) =>
typeof value === "object" && value !== null && !Array.isArray(value);
const isThemeId = (value) =>
typeof value === "string" && /^[a-z0-9](?:[a-z0-9-]{0,47})$/.test(value);
const isThemeLabel = (value) =>
typeof value === "string" && value.trim().length > 0 && value.trim().length <= 48;
const isThemePreferenceMode = (value) =>
value === "light" || value === "dark" || value === "system";
const isThemeAppearance = (value) => value === "light" || value === "dark";
const isStoredCustomTheme = (value) => {
if (
!isRecord(value) ||
!isThemeId(value.id) ||
RESERVED_THEME_IDS.includes(value.id) ||
!isThemeLabel(value.label) ||
!isThemeAppearance(value.appearance) ||
!isRecord(value.colors)
) {
return false;
}
if (value.variants === undefined) return true;
if (!isRecord(value.variants)) return false;
return Object.entries(value.variants).every(
([appearance, colors]) =>
isThemeAppearance(appearance) &&
(appearance === value.appearance || isRecord(colors)),
);
};
const findCustomTheme = (themeId) => {
if (RESERVED_THEME_IDS.includes(themeId)) return null;
try {
const parsed = JSON.parse(
window.localStorage.getItem(CUSTOM_THEMES_STORAGE_KEY) ?? "null",
);
if (!Array.isArray(parsed)) return null;
return (
parsed.find((value) => isStoredCustomTheme(value) && value.id === themeId) ?? null
);
} catch {
return null;
}
};
try {
const storedTheme = window.localStorage.getItem("t3code:theme");
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
// Older builds stored the dark T3 Chat palette as its own theme id
// and every maintainer theme under a t3- prefix; keep those
// preferences readable. Mirrors the runtime's legacy alias table.
const LEGACY_THEME_IDS = {
"t3-chat-dark": "t3-chat",
"t3-grove": "grove",
"t3-ocean": "ocean",
"t3-ember": "ember",
"t3-iris": "iris",
};
const isLegacyT3ChatDark = storedTheme === "t3-chat-dark";
const themeId =
storedTheme === null ? "" : (LEGACY_THEME_IDS[storedTheme] ?? storedTheme);
const legacyMode = isLegacyT3ChatDark ? "dark" : null;
const builtInModes = BUILT_IN_THEME_MODES[themeId] ?? null;
const customTheme = builtInModes ? null : findCustomTheme(themeId);
const themeModes =
builtInModes ??
(customTheme
? ["light", "dark"].filter(
(mode) => mode === customTheme.appearance || customTheme.variants?.[mode],
)
: null);
// Mirrors the runtime's isKnownThemePreference.
const isKnownTheme =
storedTheme === "light" ||
storedTheme === "dark" ||
storedTheme === "system" ||
themeModes !== null;
const theme = isKnownTheme && storedTheme ? storedTheme : "system";
const isThemed = isKnownTheme && themeModes !== null;
const baseAppearance = customTheme ? customTheme.appearance : "light";
const systemMode = prefersDark ? "dark" : "light";
const storedAppearanceMode = window.localStorage.getItem(
THEME_APPEARANCE_MODE_STORAGE_KEY,
);
const storedFollowSystem = window.localStorage.getItem(THEME_FOLLOW_SYSTEM_STORAGE_KEY);
// Mirrors the runtime's getThemePreferenceMode fallback: a bare
// "light"/"dark" preference names its own mode.
const preferenceMode =
theme === "light" || theme === "dark" ? theme : (legacyMode ?? baseAppearance);
const appearanceMode = isThemePreferenceMode(storedAppearanceMode)
? storedAppearanceMode
: storedFollowSystem === "true"
? "system"
: storedFollowSystem === "false"
? preferenceMode
: theme === "system"
? "system"
: preferenceMode;
// A valid mix half for the wanted appearance takes over: it decides
// light vs dark even when the base theme cannot render that mode.
// Mirrors the runtime's halves-aware resolveThemeAppearance.
const wantedMode = appearanceMode === "system" ? systemMode : appearanceMode;
let halfThemeId = null;
let halfCustomTheme = null;
try {
const halvesRaw = JSON.parse(
window.localStorage.getItem(THEME_HALVES_STORAGE_KEY) ?? "null",
);
const candidate = isRecord(halvesRaw) ? halvesRaw[wantedMode] : null;
if (typeof candidate === "string") {
const halfId = LEGACY_THEME_IDS[candidate] ?? candidate;
const halfBuiltIn = BUILT_IN_THEME_MODES[halfId] ?? null;
const halfCustom = halfBuiltIn ? null : findCustomTheme(halfId);
const halfModes =
halfBuiltIn ??
(halfCustom
? ["light", "dark"].filter(
(mode) => mode === halfCustom.appearance || halfCustom.variants?.[mode],
)
: null);
if (halfModes && halfModes.includes(wantedMode)) {
halfThemeId = halfId;
halfCustomTheme = halfCustom;
}
}
} catch {
// A malformed mix degrades to the base preference.
}
const themeMode = !isThemed
? null
: appearanceMode === "system"
? themeModes.includes(systemMode)
? systemMode
: baseAppearance
: themeModes.includes(appearanceMode)
? appearanceMode
: baseAppearance;
const isDark =
halfThemeId !== null
? wantedMode === "dark"
: isThemed
? themeMode === "dark"
: appearanceMode === "system"
? prefersDark
: appearanceMode === "dark";
const effectiveIsThemed = halfThemeId !== null || isThemed;
const effectiveThemeId = halfThemeId ?? themeId;
const effectiveCustomTheme = halfThemeId !== null ? halfCustomTheme : customTheme;
const effectiveThemeMode = halfThemeId !== null ? wantedMode : themeMode;
const customColors =
effectiveIsThemed && effectiveCustomTheme
? effectiveThemeMode === effectiveCustomTheme.appearance
? effectiveCustomTheme.colors
: (effectiveCustomTheme.variants?.[effectiveThemeMode] ??
effectiveCustomTheme.colors)
: null;
const customDefaults =
effectiveCustomTheme && effectiveThemeMode
? DEFAULT_THEME_PALETTES[effectiveThemeMode]
: null;
// The runtime tolerates individual malformed colors, so fall back
// per role rather than dropping the theme.
const builtInSplash =
effectiveIsThemed && !effectiveCustomTheme && effectiveThemeMode
? (BUILT_IN_THEME_PALETTES[effectiveThemeId]?.[effectiveThemeMode] ?? null)
: null;
const fallbackSplash = isDark ? SPLASH_COLORS.dark : SPLASH_COLORS.light;
const customSplash = customColors
? {
background: isThemeColor(customColors.canvas)
? customColors.canvas
: (customDefaults?.background ?? fallbackSplash.background),
foreground: isThemeColor(customColors.text)
? customColors.text
: (customDefaults?.foreground ?? fallbackSplash.foreground),
accent: isThemeColor(customColors.accent)
? customColors.accent
: (customDefaults?.accent ?? fallbackSplash.accent),
}
: null;
if (effectiveIsThemed) {
document.documentElement.dataset.themeId = effectiveCustomTheme
? effectiveCustomTheme.id
: effectiveThemeId;
} else {
delete document.documentElement.dataset.themeId;
}
// A resolved half is a selection even when the base theme is gone
// (deleting the base leaves the mix pointing at a real theme).
const hasSelection = halfThemeId !== null || (isKnownTheme && storedTheme !== null);
if (hasSelection) {
document.documentElement.dataset.themeSelected = "true";
} else {
delete document.documentElement.dataset.themeSelected;
}
document.documentElement.classList.toggle("dark", isDark);
const chromeColor = customColors
? isThemeColor(customColors.chrome)
? customColors.chrome
: (customDefaults?.chrome ?? fallbackSplash.background)
: builtInSplash
? builtInSplash.chrome
: isDark
? DARK_BACKGROUND
: LIGHT_BACKGROUND;
document.documentElement.style.backgroundColor = chromeColor;
if (hasSelection) {
const splashColors = customSplash ?? builtInSplash ?? fallbackSplash;
for (const [name, value] of Object.entries(splashColors)) {
document.documentElement.style.setProperty(`--boot-${name}`, value);
}
}
setThemeColor(chromeColor);
} catch {
// Mirror the runtime's storage-failure fallback: follow the OS.
let prefersDark = true;
try {
prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
} catch {
// Keep the dark default when even matchMedia is unavailable.
}
delete document.documentElement.dataset.themeId;
delete document.documentElement.dataset.themeSelected;
document.documentElement.classList.toggle("dark", prefersDark);
const fallbackColor = prefersDark ? DARK_BACKGROUND : LIGHT_BACKGROUND;
document.documentElement.style.backgroundColor = fallbackColor;
setThemeColor(fallbackColor);
}
})();
</script>
<style>
html,
body,
#root {
width: 100%;
height: 100%;
margin: 0;
}
body {
background: #ffffff;
color: #262626;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
}
html.dark body {
background: #0a0a0a;
color: #f5f5f5;
}
#boot-shell {
position: relative;
overflow: hidden;
display: flex;
min-height: 100%;
align-items: center;
justify-content: center;
background: inherit;
color: inherit;
}
html[data-theme-selected="true"] #boot-shell {
background: var(--boot-background);
color: var(--boot-foreground);
}
#boot-shell-card {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
width: 96px;
height: 96px;
}
#boot-shell-logo {
width: 64px;
height: 64px;
object-fit: contain;
}
#boot-error {
display: grid;
justify-items: center;
gap: 16px;
max-width: 600px;
padding: 24px;
text-align: center;
}
#boot-error p {
margin: 0;
overflow-wrap: anywhere;
}
#boot-error button {
padding: 8px 16px;
border: 1px solid currentColor;
border-radius: 4px;
background: transparent;
color: inherit;
font: inherit;
cursor: pointer;
}
</style>
<title>T3 Code (Alpha)</title>
</head>
<body>
<div id="root">
<div id="boot-shell">
<div id="boot-shell-card" aria-label="T3 Code splash screen">
<img id="boot-shell-logo" src="/apple-touch-icon.png" alt="T3 Code" />
</div>
</div>
</div>
<script type="module" src="/src/bootstrap.ts"></script>
</body>
</html>