mirror of
https://github.com/VibedByKaKi/t3-code-android-nightly.git
synced 2026-10-09 11:51:15 +02:00
492 lines
20 KiB
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>
|