2097 lines · 76.1 KB · TypeScript
RawDownload
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.