diff --git a/apps/web/index.html b/apps/web/index.html index dadef17d3bc..b608fd0bbff 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -15,23 +15,255 @@ (() => { const LIGHT_BACKGROUND = "#ffffff"; const DARK_BACKGROUND = "#161616"; - const themeColorMeta = document.querySelector('meta[name="theme-color"]'); + const T3_CHAT_BACKGROUND = "#faf5fa"; + const T3_CHAT_DARK_BACKGROUND = "#180f1b"; + const T3_GROVE_BACKGROUND = "#f4f8f5"; + const T3_GROVE_DARK_BACKGROUND = "#111f19"; + const CUSTOM_THEMES_STORAGE_KEY = "t3code:themes:v1"; + const THEME_FOLLOW_SYSTEM_STORAGE_KEY = "t3code:theme-follow-system"; + const SPLASH_COLORS = { + light: { + background: "#ffffff", + foreground: "#262626", + accent: "#4f46e5", + }, + dark: { + background: "#161616", + foreground: "#f5f5f5", + accent: "#818cf8", + }, + t3Chat: { + background: "#faf5fa", + foreground: "#501854", + accent: "#a84370", + }, + t3ChatDark: { + background: "#180f1b", + foreground: "#faeaf9", + accent: "#f06cab", + }, + t3Grove: { + background: "#f4f8f5", + foreground: "#241523", + accent: "#1e7d52", + }, + t3GroveDark: { + background: "#111f19", + foreground: "#fffaff", + accent: "#5dd58e", + }, + }; + // Built-in themes and the modes they can render. T3 Chat is light-only, + // so a dark OS must not flip its splash; the runtime resolves the same + // way via getThemeColorsForMode. + const BUILT_IN_THEME_MODES = { + "t3-chat": ["light"], + "t3-grove": ["light", "dark"], + }; + const RESERVED_THEME_IDS = [ + "system", + "light", + "dark", + "t3-chat", + "t3-grove", + "t3-chat-dark", + ]; + // Update every theme-color meta (including the media-scoped ones) so + // the browser picks up the resolved color regardless of which element + // its media query matches. + const setThemeColor = (color) => { + for (const meta of document.querySelectorAll('meta[name="theme-color"]')) { + meta.setAttribute("content", color); + } + }; + + const isHexColor = (value) => + typeof value === "string" && + /^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(value); + const isThemePreferenceMode = (value) => + value === "light" || value === "dark" || value === "system"; + const isThemeAppearance = (value) => value === "light" || value === "dark"; + const splitThemePreference = (preference) => { + const separatorIndex = preference.lastIndexOf(":"); + if (separatorIndex <= 0) { + return { id: preference, mode: null, invalidMode: false }; + } + const rawMode = preference.slice(separatorIndex + 1); + return { + id: preference.slice(0, separatorIndex), + mode: isThemePreferenceMode(rawMode) ? rawMode : null, + invalidMode: !isThemePreferenceMode(rawMode), + }; + }; + 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) => + value && + value.id === themeId && + isThemeAppearance(value.appearance) && + value.colors, + ) ?? null + ); + } catch { + return null; + } + }; + try { const storedTheme = window.localStorage.getItem("t3code:theme"); - const theme = - storedTheme === "light" || storedTheme === "dark" || storedTheme === "system" - ? storedTheme - : "system"; const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; - const isDark = theme === "dark" || (theme === "system" && prefersDark); + const rawPreference = splitThemePreference(storedTheme ?? ""); + // Older builds stored the dark T3 Chat palette as its own theme id; + // the runtime resolves it to the light-only T3 Chat theme. + const isLegacyT3ChatDark = + !rawPreference.invalidMode && rawPreference.id === "t3-chat-dark"; + const preference = isLegacyT3ChatDark + ? { + id: "t3-chat", + mode: rawPreference.mode === "system" ? "system" : null, + invalidMode: false, + } + : rawPreference; + const explicitMode = + preference.mode === "light" || preference.mode === "dark" ? preference.mode : null; + const builtInModes = preference.invalidMode + ? null + : (BUILT_IN_THEME_MODES[preference.id] ?? null); + const customTheme = + preference.invalidMode || builtInModes ? null : findCustomTheme(preference.id); + const themeModes = + builtInModes ?? + (customTheme + ? ["light", "dark"].filter( + (mode) => mode === customTheme.appearance || customTheme.variants?.[mode], + ) + : null); + // Mirrors the runtime's isKnownThemePreference: an explicit mode the + // theme cannot render makes the whole preference unknown, and the + // runtime falls back to the system default. + const isKnownTheme = + storedTheme === "light" || + storedTheme === "dark" || + storedTheme === "system" || + (themeModes !== null && (explicitMode === null || themeModes.includes(explicitMode))); + const theme = isKnownTheme && storedTheme ? storedTheme : "system"; + const storedFollowSystem = window.localStorage.getItem(THEME_FOLLOW_SYSTEM_STORAGE_KEY); + const followSystem = + storedFollowSystem === "true" + ? true + : storedFollowSystem === "false" + ? false + : theme === "system" || preference.mode === "system"; + const isThemed = isKnownTheme && themeModes !== null; + const baseAppearance = customTheme ? customTheme.appearance : "light"; + const systemMode = prefersDark ? "dark" : "light"; + // followSystem already accounts for a `:system` suffix when the + // follow-system key is unset; when that key is explicitly "false" + // the runtime ignores the suffix, so the boot script must too. + const themeMode = !isThemed + ? null + : followSystem + ? themeModes.includes(systemMode) + ? systemMode + : baseAppearance + : (explicitMode ?? baseAppearance); + const isDark = isThemed + ? themeMode === "dark" + : followSystem + ? prefersDark + : theme === "dark"; + const customColors = + isThemed && customTheme + ? themeMode === customTheme.appearance + ? customTheme.colors + : (customTheme.variants?.[themeMode] ?? customTheme.colors) + : null; + // The runtime tolerates individual malformed colors, so fall back + // per role rather than dropping the theme. + const customSplash = customColors + ? { + background: isHexColor(customColors.canvas) + ? customColors.canvas + : themeMode === "dark" + ? SPLASH_COLORS.t3ChatDark.background + : SPLASH_COLORS.t3Chat.background, + foreground: isHexColor(customColors.text) + ? customColors.text + : themeMode === "dark" + ? SPLASH_COLORS.t3ChatDark.foreground + : SPLASH_COLORS.t3Chat.foreground, + accent: isHexColor(customColors.accent) + ? customColors.accent + : themeMode === "dark" + ? SPLASH_COLORS.t3ChatDark.accent + : SPLASH_COLORS.t3Chat.accent, + } + : null; + if (isThemed) { + document.documentElement.dataset.themeId = customTheme ? customTheme.id : preference.id; + } else { + delete document.documentElement.dataset.themeId; + } + if (isKnownTheme && storedTheme !== null) { + document.documentElement.dataset.themeSelected = "true"; + } else { + delete document.documentElement.dataset.themeSelected; + } document.documentElement.classList.toggle("dark", isDark); - const chromeColor = isDark ? DARK_BACKGROUND : LIGHT_BACKGROUND; + const chromeColor = customSplash + ? customSplash.background + : isThemed + ? themeMode === "dark" + ? preference.id === "t3-grove" + ? T3_GROVE_DARK_BACKGROUND + : T3_CHAT_DARK_BACKGROUND + : preference.id === "t3-grove" + ? T3_GROVE_BACKGROUND + : T3_CHAT_BACKGROUND + : isDark + ? DARK_BACKGROUND + : LIGHT_BACKGROUND; document.documentElement.style.backgroundColor = chromeColor; - themeColorMeta?.setAttribute("content", chromeColor); + if (isKnownTheme && storedTheme !== null) { + const splashColors = + customSplash ?? + (isThemed + ? preference.id === "t3-grove" + ? themeMode === "dark" + ? SPLASH_COLORS.t3GroveDark + : SPLASH_COLORS.t3Grove + : themeMode === "dark" + ? SPLASH_COLORS.t3ChatDark + : SPLASH_COLORS.t3Chat + : isDark + ? SPLASH_COLORS.dark + : SPLASH_COLORS.light); + for (const [name, value] of Object.entries(splashColors)) { + document.documentElement.style.setProperty(`--boot-${name}`, value); + } + } + setThemeColor(chromeColor); } catch { - document.documentElement.classList.add("dark"); - document.documentElement.style.backgroundColor = DARK_BACKGROUND; - themeColorMeta?.setAttribute("content", DARK_BACKGROUND); + // 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); } })(); @@ -63,14 +295,35 @@ } #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); + } + + html[data-theme-selected="true"] #boot-shell::before { + position: absolute; + inset: -20%; + content: ""; + background: radial-gradient( + circle at 50% 38%, + color-mix(in srgb, var(--boot-accent) 18%, transparent), + transparent 42% + ); } #boot-shell-card { + position: relative; + z-index: 1; display: flex; align-items: center; justify-content: center; diff --git a/apps/web/src/components/CommandPalette.logic.ts b/apps/web/src/components/CommandPalette.logic.ts index eee6ba5886e..c9ded5e7eee 100644 --- a/apps/web/src/components/CommandPalette.logic.ts +++ b/apps/web/src/components/CommandPalette.logic.ts @@ -12,7 +12,7 @@ import { formatRelativeTimeLabel } from "../timestampFormat"; import { type Project, type SidebarThreadSummary, type Thread } from "../types"; export const RECENT_THREAD_LIMIT = 12; -export const ITEM_ICON_CLASS = "size-4 text-muted-foreground/80"; +export const ITEM_ICON_CLASS = "size-4 text-icon-muted"; export const ADDON_ICON_CLASS = "size-4"; /** diff --git a/apps/web/src/components/ComposerPromptEditor.tsx b/apps/web/src/components/ComposerPromptEditor.tsx index 169126788ae..078fc130297 100644 --- a/apps/web/src/components/ComposerPromptEditor.tsx +++ b/apps/web/src/components/ComposerPromptEditor.tsx @@ -1763,7 +1763,7 @@ function ComposerPromptEditorInner({ } placeholder={ terminalContexts.length > 0 ? null : ( -
{selectedPatchError}
+{selectedPatchError}
+
{props.isLoading ? "Searching workspace skills..." : (props.emptyStateText ?? @@ -183,7 +183,7 @@ export const ComposerCommandMenu = memo(function ComposerCommandMenu(props: {
+
{props.isLoading
? "Searching workspace files..."
: (props.emptyStateText ??
@@ -235,26 +235,26 @@ const ComposerCommandMenuItem = memo(function ComposerCommandMenuItem(props: {
/>
) : null}
{props.item.type === "slash-command" ? (
- {activeQuestion.question} Select one or more options. Select one or more options.
+
{annotation.comment.trim()}