import * as Schema from "effect/Schema";import "culori/css";import { converter, parse } from "culori/fn"; export const T3_CHAT_THEME_ID = "t3-chat" as const;export const T3_CHAT_THEME_LABEL = "T3 Chat";export const GROVE_THEME_ID = "grove" as const;export const GROVE_THEME_LABEL = "Grove";export const OCEAN_THEME_ID = "ocean" as const;export const OCEAN_THEME_LABEL = "Ocean";export const EMBER_THEME_ID = "ember" as const;export const EMBER_THEME_LABEL = "Ember";export const IRIS_THEME_ID = "iris" as const;export const IRIS_THEME_LABEL = "Iris";export const THEME_FILE_VERSION = 1 as const;export const CUSTOM_THEMES_STORAGE_KEY = "t3code:themes:v1";export const THEME_FOLLOW_SYSTEM_STORAGE_KEY = "t3code:theme-follow-system";export const THEME_APPEARANCE_MODE_STORAGE_KEY = "t3code:theme-appearance-mode";export const THEME_HALVES_STORAGE_KEY = "t3code:theme-halves:v1"; const LEGACY_T3_CHAT_DARK_THEME_ID = "t3-chat-dark"; export const ThemePreference = Schema.String;export type ThemePreference = typeof ThemePreference.Type; export const THEME_COLOR_ROLES = [ "canvas", "chrome", "toolbar", "toolbarForeground", "toolbarBorder", "toolbarControl", "toolbarControlForeground", "toolbarControlHover", "surface", "surfaceRaised", "surfaceOverlay", "text", "textMuted", "border", "input", "focus", "accent", "accentForeground", "secondary", "secondaryForeground", "muted", "mutedForeground", "placeholder", "secondaryLabel", "iconMuted", "error", "errorForeground", "errorSurface", "warning", "warningForeground", "warningSurface", "update", "updateForeground", "updateSurface", "accentSurface", "accentSurfaceForeground", "messageSurface", "messageForeground", "messageAction", "messageActionForeground", "messageActionHover", "codeBackground", "codeForeground", "sidebar", "sidebarForeground", "sidebarMutedForeground", "sidebarControlSurface", "sidebarRowHover", "sidebarRowActive", "sidebarRowSelected", "sidebarBorder", "terminalBackground", "terminalForeground", "terminalCursor", "terminalSelection", "terminalScrollbar", "terminalScrollbarHover",] as const; export type ThemeColorRole = (typeof THEME_COLOR_ROLES)[number];const THEME_COLOR_ROLE_SET: ReadonlySet<string> = new Set(THEME_COLOR_ROLES);export type ThemeAppearance = "light" | "dark"; export type ThemeColors = Readonly<Record<ThemeColorRole, string>>;export type ThemeColorOverrides = Readonly<Partial<Record<ThemeColorRole, string>>>;export type ThemeVariants = Readonly<Partial<Record<ThemeAppearance, ThemeColors>>>;export type ThemeVariantOverrides = Readonly<Partial<Record<ThemeAppearance, ThemeColorOverrides>>>;export type ThemePreferenceMode = ThemeAppearance | "system";export type ThemeCollection = Readonly<{ id: string; label: string }>;export type ThemeDefinition = Readonly<{ id: string; label: string; appearance: ThemeAppearance; colors: ThemeColors; variants?: ThemeVariants; /** Groups related imported variants into one library card. */ collection?: ThemeCollection; /** Allows Dev/Nightly artwork to render over a maintainer-controlled sidebar. */ sidebarArtwork?: boolean; /** True when the palette was generated by the guided editor from its * canvas and accent; such themes reopen in guided mode. */ managed?: boolean;}>;export type ThemeFile = Readonly<{ version: typeof THEME_FILE_VERSION; id: string; name: string; appearance: ThemeAppearance; colors: ThemeColorOverrides; variants?: ThemeVariantOverrides; collection?: ThemeCollection; managed?: boolean;}>; const RESERVED_THEME_IDS = new Set([ "system", "light", "dark", T3_CHAT_THEME_ID, GROVE_THEME_ID, OCEAN_THEME_ID, EMBER_THEME_ID, IRIS_THEME_ID, LEGACY_T3_CHAT_DARK_THEME_ID, "t3-grove", "t3-ocean", "t3-ember", "t3-iris",]); const customThemeListeners = new Set<() => void>();type CustomThemeLibrarySnapshot = | Readonly<{ status: "ready"; storedThemes: ReadonlyArray<unknown>; themes: ReadonlyArray<ThemeDefinition>; }> | Readonly<{ status: "unavailable"; reason: "malformed" }> | Readonly<{ status: "unavailable"; reason: "storage-unavailable"; cause: unknown }>; let customThemeLibrarySnapshot: CustomThemeLibrarySnapshot | null = null;const themePreviewListeners = new Set<() => void>();let themePreviewSidebarArtwork: boolean | null = null; export function getThemePreviewSidebarArtwork(): boolean | null { return themePreviewSidebarArtwork;} export function subscribeToThemePreview(listener: () => void): () => void { themePreviewListeners.add(listener); return () => themePreviewListeners.delete(listener);} function setThemePreviewSidebarArtwork(next: boolean | null): void { if (themePreviewSidebarArtwork === next) return; themePreviewSidebarArtwork = next; for (const listener of themePreviewListeners) listener();} function isRecord(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value);} function isThemeAppearance(value: unknown): value is ThemeAppearance { return value === "light" || value === "dark";} export function isThemeColor(value: unknown): value is string { return typeof value === "string" && toCanonicalThemeColor(value) !== null;} function isThemeId(value: unknown): value is string { return typeof value === "string" && /^[a-z0-9](?:[a-z0-9-]{0,47})$/.test(value);} function isThemeLabel(value: unknown): value is string { return typeof value === "string" && value.trim().length > 0 && value.trim().length <= 48;} function parseThemeCollection(value: unknown): ThemeCollection | undefined { return isRecord(value) && typeof value.id === "string" && /^[a-z0-9][a-z0-9.:-]{0,127}$/i.test(value.id) && isThemeLabel(value.label) ? { id: value.id, label: value.label.trim() } : undefined;} function parseStoredThemeColors(value: unknown, appearance: ThemeAppearance): ThemeColors | null { if (!isRecord(value)) return null; const colors: Partial<Record<ThemeColorRole, string>> = { ...getDefaultThemeColors(appearance), }; // Tolerate unknown roles and malformed values so themes saved by other // builds (for example one that adds a new role) keep their remaining colors. for (const [role, color] of Object.entries(value)) { const normalized = toCanonicalThemeColor(color); if (THEME_COLOR_ROLE_SET.has(role) && normalized) { colors[role as ThemeColorRole] = normalized; } } return colors as ThemeColors;} function parseStoredThemeVariants( value: unknown, baseAppearance: ThemeAppearance,): ThemeVariants | null | undefined { if (value === undefined) return undefined; if (!isRecord(value)) return null; const variants: Partial<Record<ThemeAppearance, ThemeColors>> = {}; for (const [appearance, colors] of Object.entries(value)) { if (!isThemeAppearance(appearance)) return null; // A variant matching the base appearance would be shadowed by the base // colors; drop it so the theme round-trips through parseThemeFile. if (appearance === baseAppearance) continue; const parsedColors = parseStoredThemeColors(colors, appearance); if (!parsedColors) return null; variants[appearance] = parsedColors; } return Object.keys(variants).length > 0 ? variants : undefined;} function parseStoredTheme(value: unknown): ThemeDefinition | null { if (!isRecord(value)) return null; if (!isThemeId(value.id) || RESERVED_THEME_IDS.has(value.id)) return null; if (!isThemeLabel(value.label) || !isThemeAppearance(value.appearance)) return null; const colors = parseStoredThemeColors(value.colors, value.appearance); if (!colors) return null; const variants = parseStoredThemeVariants(value.variants, value.appearance); if (value.variants !== undefined && variants === null) return null; const collection = parseThemeCollection(value.collection); return { id: value.id, label: value.label.trim(), appearance: value.appearance, colors, ...(variants ? { variants } : {}), ...(collection ? { collection } : {}), ...(value.managed === true ? { managed: true } : {}), };} function parseStoredThemes(storedThemes: ReadonlyArray<unknown>): ReadonlyArray<ThemeDefinition> { const themes: ThemeDefinition[] = []; for (const value of storedThemes) { const theme = parseStoredTheme(value); if (theme && !themes.some((existing) => existing.id === theme.id)) { themes.push(theme); } } return themes;} function readCustomThemeLibrarySnapshot(): CustomThemeLibrarySnapshot { if (typeof window === "undefined") { return { status: "ready", storedThemes: [], themes: [] }; } let raw: string | null; try { raw = window.localStorage.getItem(CUSTOM_THEMES_STORAGE_KEY); } catch (cause) { return { status: "unavailable", reason: "storage-unavailable", cause }; } if (!raw) return { status: "ready", storedThemes: [], themes: [] }; let parsed: unknown; try { parsed = JSON.parse(raw); } catch { return { status: "unavailable", reason: "malformed" }; } if (!Array.isArray(parsed)) return { status: "unavailable", reason: "malformed" }; return { status: "ready", storedThemes: parsed, themes: parseStoredThemes(parsed) };} function getCustomThemeLibrarySnapshot(): CustomThemeLibrarySnapshot { if (customThemeLibrarySnapshot === null) { customThemeLibrarySnapshot = readCustomThemeLibrarySnapshot(); } return customThemeLibrarySnapshot;} function notifyCustomThemeListeners() { for (const listener of customThemeListeners) listener();} export function invalidateCustomThemes() { customThemeLibrarySnapshot = null; notifyCustomThemeListeners();} export function getCustomThemes(): ReadonlyArray<ThemeDefinition> { const snapshot = getCustomThemeLibrarySnapshot(); return snapshot.status === "ready" ? snapshot.themes : [];} export function getStoredCustomThemeCollection( collectionId: string,): ReadonlyArray<ThemeDefinition> { return readWritableCustomThemeLibrary().themes.filter( (theme) => theme.collection?.id === collectionId, );} export function subscribeToCustomThemes(listener: () => void): () => void { customThemeListeners.add(listener); if (typeof window === "undefined") { return () => customThemeListeners.delete(listener); } const handleStorage = (event: StorageEvent) => { if (event.key === CUSTOM_THEMES_STORAGE_KEY || event.key === null) { invalidateCustomThemes(); } }; window.addEventListener("storage", handleStorage); return () => { customThemeListeners.delete(listener); window.removeEventListener("storage", handleStorage); };} // Earlier builds shipped every maintainer theme under a t3- prefix; only the// genuinely T3-branded palette keeps it. Stored preferences and mixes with the// old ids stay readable through this alias table.const LEGACY_THEME_ID_ALIASES: Readonly<Record<string, string>> = { [LEGACY_T3_CHAT_DARK_THEME_ID]: T3_CHAT_THEME_ID, "t3-grove": GROVE_THEME_ID, "t3-ocean": OCEAN_THEME_ID, "t3-ember": EMBER_THEME_ID, "t3-iris": IRIS_THEME_ID,}; function normalizeThemeId(themeId: string): string { return LEGACY_THEME_ID_ALIASES[themeId] ?? themeId;} /** * Map a stored preference onto the id the runtime applies, so selection state * matches the theme cards. The legacy dark-variant id stays as-is because it * still carries the appearance hint getThemePreferenceMode reads. */export function canonicalThemePreference(theme: string): string { return theme === LEGACY_T3_CHAT_DARK_THEME_ID ? theme : normalizeThemeId(theme);} function themeIdFromPreference(theme: ThemePreference): string { return normalizeThemeId(theme);} // Older builds stored the dark T3 Chat palette as a separate theme. Keep// those preferences readable while mapping them to the dark variant.function legacyThemeMode(theme: ThemePreference): ThemeAppearance | null { return theme === LEGACY_T3_CHAT_DARK_THEME_ID ? "dark" : null;} /** * Maintainer palettes use product color roles rather than Tailwind or component * names so the same definitions can feed other clients and native surfaces. */// Measured from the live t3.chat default theme. Translucent chat surfaces are// flattened over --chat-background so this opaque palette reproduces the// pixels users see after T3 Chat's blur and noise layers are composited.// Foreground pairs deviate where necessary to keep normal text at WCAG AA.const T3_CHAT_LIGHT_COLORS: ThemeColors = { canvas: "#fdf7fd", // T3 Code's workspace header belongs to the chat panel, so keep it seamless // with the light chat canvas rather than mapping it to T3 Chat's outer shell. chrome: "#fdf7fd", toolbar: "#fdf7fd", toolbarForeground: "#501854", toolbarBorder: "#efbdeb", // T3 Chat's light chrome controls sit on its pale gradient-noise surface, // not the substantially darker solid accent token. toolbarControl: "#f3e6f5", toolbarControlForeground: "#501854", toolbarControlHover: "#eccfe3", surface: "#faf3fb", surfaceRaised: "#fdfafd", surfaceOverlay: "#ffffff", text: "#501854", textMuted: "#ac1668", border: "#eee1ed", input: "#e7c1dc", focus: "#db2777", accent: "#db2777", accentForeground: "#ffffff", secondary: "#f1c4e6", secondaryForeground: "#77347c", muted: "#eaa7cb", mutedForeground: "#8d1255", placeholder: "#8b5f90", secondaryLabel: "#ac1668", iconMuted: "#ac1668", error: "#f7086c", errorForeground: "#9d174d", errorSurface: "#fde4f1", warning: "#f59e0b", warningForeground: "#b05109", warningSurface: "#fcf0ea", update: "#db2777", updateForeground: "#ac1668", updateSurface: "#fadfef", accentSurface: "#f3e6f5", accentSurfaceForeground: "#454554", messageSurface: "#f7def2", messageForeground: "#492c61", messageAction: "#db2777", messageActionForeground: "#ffffff", messageActionHover: "#c12269", // T3 Chat uses a light lavender code surface in light mode. Keeping the // dark plum pair here also leaked the dark palette into T3 Code's diffs. codeBackground: "#f5ecf9", codeForeground: "#673c8b", // The live sidebar is transparent over T3 Chat's outer shell. Use that // rendered shell color rather than its unused, darker sidebar token. sidebar: "#f2e1f4", sidebarForeground: "#454554", sidebarMutedForeground: "#ac1668", sidebarControlSurface: "#f8f8f7", sidebarRowHover: "#f8f8f7", sidebarRowActive: "#f8f8f7", sidebarRowSelected: "#f8f8f7", sidebarBorder: "#eceae9", terminalBackground: "#fdf7fd", terminalForeground: "#501854", terminalCursor: "#db2777", terminalSelection: "#f1c4e6", terminalScrollbar: "#e7c1dc", terminalScrollbarHover: "#eaa7cb",}; const T3_CHAT_DARK_COLORS: ThemeColors = { canvas: "#1f1a24", // T3 Code's workspace header belongs to the chat panel, so keep it seamless // with the canvas rather than mapping it to T3 Chat's outer shell. chrome: "#1f1a24", toolbar: "#1f1a24", toolbarForeground: "#f9f8fb", toolbarBorder: "#27242c", toolbarControl: "#362d3d", toolbarControlForeground: "#d4c7e1", toolbarControlHover: "#463753", // Cards and panels stay in T3 Chat's plum surface family. Near-black here // made the right-panel surface picker look unrelated to the chat canvas. surface: "#29232d", // Pre-composited for the composer's 80% glass layer; this resolves to the // measured #29232d input fill over the canvas. surfaceRaised: "#2c2631", surfaceOverlay: "#100a0e", text: "#f9f8fb", textMuted: "#e7d0dd", border: "#27242c", input: "#302029", focus: "#db2777", accent: "#a3004c", accentForeground: "#fbd0e8", secondary: "#362d3d", secondaryForeground: "#d4c7e1", muted: "#423a45", mutedForeground: "#e7d0dd", placeholder: "#968d9f", secondaryLabel: "#e7d0dd", iconMuted: "#d4c7e1", error: "#9d174d", errorForeground: "#fbd0e8", errorSurface: "#331a2b", warning: "#f59e0b", warningForeground: "#fbbf24", warningSurface: "#412f20", update: "#a3004c", updateForeground: "#fbd0e8", updateSurface: "#37152b", accentSurface: "#463753", accentSurfaceForeground: "#f8f1f5", messageSurface: "#2b2431", messageForeground: "#f2ebfa", messageAction: "#a3004c", messageActionForeground: "#fbd0e8", messageActionHover: "#a2004c", // Diffs and file previews are full workspace surfaces in T3 Code. Keep them // continuous with the themed canvas instead of dropping to near-black. codeBackground: "#1f1a24", codeForeground: "#d8c3ef", // The live sidebar starts from #131314, then gains its hue from a pink // gradient/noise stack. This pre-grain base lands on the same #1a131a // visible shell color after our surface-grain layer is composited. sidebar: "#171018",Lines 1–500 of 2097. Load more
import * as Schema from "effect/Schema"; import "culori/css"; import { converter, parse } from "culori/fn"; export const T3_CHAT_THEME_ID = "t3-chat" as const; export const T3_CHAT_THEME_LABEL = "T3 Chat"; export const GROVE_THEME_ID = "grove" as const; export const GROVE_THEME_LABEL = "Grove"; export const OCEAN_THEME_ID = "ocean" as const; export const OCEAN_THEME_LABEL = "Ocean"; export const EMBER_THEME_ID = "ember" as const; export const EMBER_THEME_LABEL = "Ember"; export const IRIS_THEME_ID = "iris" as const; export const IRIS_THEME_LABEL = "Iris"; export const THEME_FILE_VERSION = 1 as const; export const CUSTOM_THEMES_STORAGE_KEY = "t3code:themes:v1"; export const THEME_FOLLOW_SYSTEM_STORAGE_KEY = "t3code:theme-follow-system"; export const THEME_APPEARANCE_MODE_STORAGE_KEY = "t3code:theme-appearance-mode"; export const THEME_HALVES_STORAGE_KEY = "t3code:theme-halves:v1"; const LEGACY_T3_CHAT_DARK_THEME_ID = "t3-chat-dark"; export const ThemePreference = Schema.String; export type ThemePreference = typeof ThemePreference.Type; export const THEME_COLOR_ROLES = [ "canvas", "chrome", "toolbar", "toolbarForeground", "toolbarBorder", "toolbarControl", "toolbarControlForeground", "toolbarControlHover", "surface", "surfaceRaised", "surfaceOverlay", "text", "textMuted", "border", "input", "focus", "accent", "accentForeground", "secondary", "secondaryForeground", "muted", "mutedForeground", "placeholder", "secondaryLabel", "iconMuted", "error", "errorForeground", "errorSurface", "warning", "warningForeground", "warningSurface", "update", "updateForeground", "updateSurface", "accentSurface", "accentSurfaceForeground", "messageSurface", "messageForeground", "messageAction", "messageActionForeground", "messageActionHover", "codeBackground", "codeForeground", "sidebar", "sidebarForeground", "sidebarMutedForeground", "sidebarControlSurface", "sidebarRowHover", "sidebarRowActive", "sidebarRowSelected", "sidebarBorder", "terminalBackground", "terminalForeground", "terminalCursor", "terminalSelection", "terminalScrollbar", "terminalScrollbarHover", ] as const; export type ThemeColorRole = (typeof THEME_COLOR_ROLES)[number]; const THEME_COLOR_ROLE_SET: ReadonlySet<string> = new Set(THEME_COLOR_ROLES); export type ThemeAppearance = "light" | "dark"; export type ThemeColors = Readonly<Record<ThemeColorRole, string>>; export type ThemeColorOverrides = Readonly<Partial<Record<ThemeColorRole, string>>>; export type ThemeVariants = Readonly<Partial<Record<ThemeAppearance, ThemeColors>>>; export type ThemeVariantOverrides = Readonly<Partial<Record<ThemeAppearance, ThemeColorOverrides>>>; export type ThemePreferenceMode = ThemeAppearance | "system"; export type ThemeCollection = Readonly<{ id: string; label: string }>; export type ThemeDefinition = Readonly<{ id: string; label: string; appearance: ThemeAppearance; colors: ThemeColors; variants?: ThemeVariants; /** Groups related imported variants into one library card. */ collection?: ThemeCollection; /** Allows Dev/Nightly artwork to render over a maintainer-controlled sidebar. */ sidebarArtwork?: boolean; /** True when the palette was generated by the guided editor from its * canvas and accent; such themes reopen in guided mode. */ managed?: boolean; }>; export type ThemeFile = Readonly<{ version: typeof THEME_FILE_VERSION; id: string; name: string; appearance: ThemeAppearance; colors: ThemeColorOverrides; variants?: ThemeVariantOverrides; collection?: ThemeCollection; managed?: boolean; }>; const RESERVED_THEME_IDS = new Set([ "system", "light", "dark", T3_CHAT_THEME_ID, GROVE_THEME_ID, OCEAN_THEME_ID, EMBER_THEME_ID, IRIS_THEME_ID, LEGACY_T3_CHAT_DARK_THEME_ID, "t3-grove", "t3-ocean", "t3-ember", "t3-iris", ]); const customThemeListeners = new Set<() => void>(); type CustomThemeLibrarySnapshot = | Readonly<{ status: "ready"; storedThemes: ReadonlyArray<unknown>; themes: ReadonlyArray<ThemeDefinition>; }> | Readonly<{ status: "unavailable"; reason: "malformed" }> | Readonly<{ status: "unavailable"; reason: "storage-unavailable"; cause: unknown }>; let customThemeLibrarySnapshot: CustomThemeLibrarySnapshot | null = null; const themePreviewListeners = new Set<() => void>(); let themePreviewSidebarArtwork: boolean | null = null; export function getThemePreviewSidebarArtwork(): boolean | null { return themePreviewSidebarArtwork; } export function subscribeToThemePreview(listener: () => void): () => void { themePreviewListeners.add(listener); return () => themePreviewListeners.delete(listener); } function setThemePreviewSidebarArtwork(next: boolean | null): void { if (themePreviewSidebarArtwork === next) return; themePreviewSidebarArtwork = next; for (const listener of themePreviewListeners) listener(); } function isRecord(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value); } function isThemeAppearance(value: unknown): value is ThemeAppearance { return value === "light" || value === "dark"; } export function isThemeColor(value: unknown): value is string { return typeof value === "string" && toCanonicalThemeColor(value) !== null; } function isThemeId(value: unknown): value is string { return typeof value === "string" && /^[a-z0-9](?:[a-z0-9-]{0,47})$/.test(value); } function isThemeLabel(value: unknown): value is string { return typeof value === "string" && value.trim().length > 0 && value.trim().length <= 48; } function parseThemeCollection(value: unknown): ThemeCollection | undefined { return isRecord(value) && typeof value.id === "string" && /^[a-z0-9][a-z0-9.:-]{0,127}$/i.test(value.id) && isThemeLabel(value.label) ? { id: value.id, label: value.label.trim() } : undefined; } function parseStoredThemeColors(value: unknown, appearance: ThemeAppearance): ThemeColors | null { if (!isRecord(value)) return null; const colors: Partial<Record<ThemeColorRole, string>> = { ...getDefaultThemeColors(appearance), }; // Tolerate unknown roles and malformed values so themes saved by other // builds (for example one that adds a new role) keep their remaining colors. for (const [role, color] of Object.entries(value)) { const normalized = toCanonicalThemeColor(color); if (THEME_COLOR_ROLE_SET.has(role) && normalized) { colors[role as ThemeColorRole] = normalized; } } return colors as ThemeColors; } function parseStoredThemeVariants( value: unknown, baseAppearance: ThemeAppearance, ): ThemeVariants | null | undefined { if (value === undefined) return undefined; if (!isRecord(value)) return null; const variants: Partial<Record<ThemeAppearance, ThemeColors>> = {}; for (const [appearance, colors] of Object.entries(value)) { if (!isThemeAppearance(appearance)) return null; // A variant matching the base appearance would be shadowed by the base // colors; drop it so the theme round-trips through parseThemeFile. if (appearance === baseAppearance) continue; const parsedColors = parseStoredThemeColors(colors, appearance); if (!parsedColors) return null; variants[appearance] = parsedColors; } return Object.keys(variants).length > 0 ? variants : undefined; } function parseStoredTheme(value: unknown): ThemeDefinition | null { if (!isRecord(value)) return null; if (!isThemeId(value.id) || RESERVED_THEME_IDS.has(value.id)) return null; if (!isThemeLabel(value.label) || !isThemeAppearance(value.appearance)) return null; const colors = parseStoredThemeColors(value.colors, value.appearance); if (!colors) return null; const variants = parseStoredThemeVariants(value.variants, value.appearance); if (value.variants !== undefined && variants === null) return null; const collection = parseThemeCollection(value.collection); return { id: value.id, label: value.label.trim(), appearance: value.appearance, colors, ...(variants ? { variants } : {}), ...(collection ? { collection } : {}), ...(value.managed === true ? { managed: true } : {}), }; } function parseStoredThemes(storedThemes: ReadonlyArray<unknown>): ReadonlyArray<ThemeDefinition> { const themes: ThemeDefinition[] = []; for (const value of storedThemes) { const theme = parseStoredTheme(value); if (theme && !themes.some((existing) => existing.id === theme.id)) { themes.push(theme); } } return themes; } function readCustomThemeLibrarySnapshot(): CustomThemeLibrarySnapshot { if (typeof window === "undefined") { return { status: "ready", storedThemes: [], themes: [] }; } let raw: string | null; try { raw = window.localStorage.getItem(CUSTOM_THEMES_STORAGE_KEY); } catch (cause) { return { status: "unavailable", reason: "storage-unavailable", cause }; } if (!raw) return { status: "ready", storedThemes: [], themes: [] }; let parsed: unknown; try { parsed = JSON.parse(raw); } catch { return { status: "unavailable", reason: "malformed" }; } if (!Array.isArray(parsed)) return { status: "unavailable", reason: "malformed" }; return { status: "ready", storedThemes: parsed, themes: parseStoredThemes(parsed) }; } function getCustomThemeLibrarySnapshot(): CustomThemeLibrarySnapshot { if (customThemeLibrarySnapshot === null) { customThemeLibrarySnapshot = readCustomThemeLibrarySnapshot(); } return customThemeLibrarySnapshot; } function notifyCustomThemeListeners() { for (const listener of customThemeListeners) listener(); } export function invalidateCustomThemes() { customThemeLibrarySnapshot = null; notifyCustomThemeListeners(); } export function getCustomThemes(): ReadonlyArray<ThemeDefinition> { const snapshot = getCustomThemeLibrarySnapshot(); return snapshot.status === "ready" ? snapshot.themes : []; } export function getStoredCustomThemeCollection( collectionId: string, ): ReadonlyArray<ThemeDefinition> { return readWritableCustomThemeLibrary().themes.filter( (theme) => theme.collection?.id === collectionId, ); } export function subscribeToCustomThemes(listener: () => void): () => void { customThemeListeners.add(listener); if (typeof window === "undefined") { return () => customThemeListeners.delete(listener); } const handleStorage = (event: StorageEvent) => { if (event.key === CUSTOM_THEMES_STORAGE_KEY || event.key === null) { invalidateCustomThemes(); } }; window.addEventListener("storage", handleStorage); return () => { customThemeListeners.delete(listener); window.removeEventListener("storage", handleStorage); }; } // Earlier builds shipped every maintainer theme under a t3- prefix; only the // genuinely T3-branded palette keeps it. Stored preferences and mixes with the // old ids stay readable through this alias table. const LEGACY_THEME_ID_ALIASES: Readonly<Record<string, string>> = { [LEGACY_T3_CHAT_DARK_THEME_ID]: T3_CHAT_THEME_ID, "t3-grove": GROVE_THEME_ID, "t3-ocean": OCEAN_THEME_ID, "t3-ember": EMBER_THEME_ID, "t3-iris": IRIS_THEME_ID, }; function normalizeThemeId(themeId: string): string { return LEGACY_THEME_ID_ALIASES[themeId] ?? themeId; } /** * Map a stored preference onto the id the runtime applies, so selection state * matches the theme cards. The legacy dark-variant id stays as-is because it * still carries the appearance hint getThemePreferenceMode reads. */ export function canonicalThemePreference(theme: string): string { return theme === LEGACY_T3_CHAT_DARK_THEME_ID ? theme : normalizeThemeId(theme); } function themeIdFromPreference(theme: ThemePreference): string { return normalizeThemeId(theme); } // Older builds stored the dark T3 Chat palette as a separate theme. Keep // those preferences readable while mapping them to the dark variant. function legacyThemeMode(theme: ThemePreference): ThemeAppearance | null { return theme === LEGACY_T3_CHAT_DARK_THEME_ID ? "dark" : null; } /** * Maintainer palettes use product color roles rather than Tailwind or component * names so the same definitions can feed other clients and native surfaces. */ // Measured from the live t3.chat default theme. Translucent chat surfaces are // flattened over --chat-background so this opaque palette reproduces the // pixels users see after T3 Chat's blur and noise layers are composited. // Foreground pairs deviate where necessary to keep normal text at WCAG AA. const T3_CHAT_LIGHT_COLORS: ThemeColors = { canvas: "#fdf7fd", // T3 Code's workspace header belongs to the chat panel, so keep it seamless // with the light chat canvas rather than mapping it to T3 Chat's outer shell. chrome: "#fdf7fd", toolbar: "#fdf7fd", toolbarForeground: "#501854", toolbarBorder: "#efbdeb", // T3 Chat's light chrome controls sit on its pale gradient-noise surface, // not the substantially darker solid accent token. toolbarControl: "#f3e6f5", toolbarControlForeground: "#501854", toolbarControlHover: "#eccfe3", surface: "#faf3fb", surfaceRaised: "#fdfafd", surfaceOverlay: "#ffffff", text: "#501854", textMuted: "#ac1668", border: "#eee1ed", input: "#e7c1dc", focus: "#db2777", accent: "#db2777", accentForeground: "#ffffff", secondary: "#f1c4e6", secondaryForeground: "#77347c", muted: "#eaa7cb", mutedForeground: "#8d1255", placeholder: "#8b5f90", secondaryLabel: "#ac1668", iconMuted: "#ac1668", error: "#f7086c", errorForeground: "#9d174d", errorSurface: "#fde4f1", warning: "#f59e0b", warningForeground: "#b05109", warningSurface: "#fcf0ea", update: "#db2777", updateForeground: "#ac1668", updateSurface: "#fadfef", accentSurface: "#f3e6f5", accentSurfaceForeground: "#454554", messageSurface: "#f7def2", messageForeground: "#492c61", messageAction: "#db2777", messageActionForeground: "#ffffff", messageActionHover: "#c12269", // T3 Chat uses a light lavender code surface in light mode. Keeping the // dark plum pair here also leaked the dark palette into T3 Code's diffs. codeBackground: "#f5ecf9", codeForeground: "#673c8b", // The live sidebar is transparent over T3 Chat's outer shell. Use that // rendered shell color rather than its unused, darker sidebar token. sidebar: "#f2e1f4", sidebarForeground: "#454554", sidebarMutedForeground: "#ac1668", sidebarControlSurface: "#f8f8f7", sidebarRowHover: "#f8f8f7", sidebarRowActive: "#f8f8f7", sidebarRowSelected: "#f8f8f7", sidebarBorder: "#eceae9", terminalBackground: "#fdf7fd", terminalForeground: "#501854", terminalCursor: "#db2777", terminalSelection: "#f1c4e6", terminalScrollbar: "#e7c1dc", terminalScrollbarHover: "#eaa7cb", }; const T3_CHAT_DARK_COLORS: ThemeColors = { canvas: "#1f1a24", // T3 Code's workspace header belongs to the chat panel, so keep it seamless // with the canvas rather than mapping it to T3 Chat's outer shell. chrome: "#1f1a24", toolbar: "#1f1a24", toolbarForeground: "#f9f8fb", toolbarBorder: "#27242c", toolbarControl: "#362d3d", toolbarControlForeground: "#d4c7e1", toolbarControlHover: "#463753", // Cards and panels stay in T3 Chat's plum surface family. Near-black here // made the right-panel surface picker look unrelated to the chat canvas. surface: "#29232d", // Pre-composited for the composer's 80% glass layer; this resolves to the // measured #29232d input fill over the canvas. surfaceRaised: "#2c2631", surfaceOverlay: "#100a0e", text: "#f9f8fb", textMuted: "#e7d0dd", border: "#27242c", input: "#302029", focus: "#db2777", accent: "#a3004c", accentForeground: "#fbd0e8", secondary: "#362d3d", secondaryForeground: "#d4c7e1", muted: "#423a45", mutedForeground: "#e7d0dd", placeholder: "#968d9f", secondaryLabel: "#e7d0dd", iconMuted: "#d4c7e1", error: "#9d174d", errorForeground: "#fbd0e8", errorSurface: "#331a2b", warning: "#f59e0b", warningForeground: "#fbbf24", warningSurface: "#412f20", update: "#a3004c", updateForeground: "#fbd0e8", updateSurface: "#37152b", accentSurface: "#463753", accentSurfaceForeground: "#f8f1f5", messageSurface: "#2b2431", messageForeground: "#f2ebfa", messageAction: "#a3004c", messageActionForeground: "#fbd0e8", messageActionHover: "#a2004c", // Diffs and file previews are full workspace surfaces in T3 Code. Keep them // continuous with the themed canvas instead of dropping to near-black. codeBackground: "#1f1a24", codeForeground: "#d8c3ef", // The live sidebar starts from #131314, then gains its hue from a pink // gradient/noise stack. This pre-grain base lands on the same #1a131a // visible shell color after our surface-grain layer is composited. sidebar: "#171018",