Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8014c0a
feat(web): add modular theme library
StiensWout Aug 2, 2026
1446d32
fix(web): smooth theme preview gradients
StiensWout Aug 2, 2026
607dd6b
fix(web): finish theme review fixes
StiensWout Aug 2, 2026
fcd1354
feat(web): theme status and update colors
StiensWout Aug 2, 2026
ebcf874
fix(web): address theme review feedback
StiensWout Aug 2, 2026
23d92c4
test(web): reset theme cache before storage test
StiensWout Aug 2, 2026
f8ef5fa
feat(web): simplify theme editor defaults
StiensWout Aug 2, 2026
e09a4cf
fix(web): unify theme card selection
StiensWout Aug 2, 2026
ef62202
fix(web): refine theme color editor
StiensWout Aug 2, 2026
de81120
fix(web): use themed dialog for theme deletion
StiensWout Aug 2, 2026
82f9299
feat(web): edit personal themes
StiensWout Aug 2, 2026
6a0acb1
feat(web): add theme color picker
StiensWout Aug 2, 2026
e4cf49a
fix(web): guide simple theme palettes
StiensWout Aug 3, 2026
cd50639
feat(web): add T3 Grove theme
StiensWout Aug 3, 2026
2650133
fix(web): simplify guided theme copy
StiensWout Aug 3, 2026
9535885
fix(web): theme theme editor actions
StiensWout Aug 3, 2026
89d70e9
fix(web): theme theme removal action
StiensWout Aug 3, 2026
ea199da
fix(web): align theme mode boot behavior
StiensWout Aug 3, 2026
cdd6470
fix(web): use theme tokens for status accents
StiensWout Aug 3, 2026
c326c28
fix(web): harden theme boot parity, editor state, and token regressions
StiensWout Aug 3, 2026
833f9ee
Merge remote-tracking branch 'upstream/main' into t3code/modular-them…
StiensWout Aug 3, 2026
a096aba
test(web): load the boot script via a raw import
StiensWout Aug 3, 2026
e1c653a
fix(web): address Bugbot review on theme storage failure paths
StiensWout Aug 3, 2026
884aafe
fix(web): update every theme-color meta with the resolved chrome color
StiensWout Aug 3, 2026
46827f2
chore: retrigger CI after GitHub 503 during checkout
StiensWout Aug 3, 2026
f52a34c
feat(web): anchor the T3 Chat theme to the real t3.chat palette
StiensWout Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
275 changes: 264 additions & 11 deletions apps/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
Comment thread
cursor[bot] marked this conversation as resolved.
};
// 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);
Comment thread
StiensWout marked this conversation as resolved.
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;
}
Comment thread
cursor[bot] marked this conversation as resolved.
if (isKnownTheme && storedTheme !== null) {
document.documentElement.dataset.themeSelected = "true";
} else {
delete document.documentElement.dataset.themeSelected;
}
Comment thread
cursor[bot] marked this conversation as resolved.
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);
}
})();
</script>
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/CommandPalette.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/ComposerPromptEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1763,7 +1763,7 @@ function ComposerPromptEditorInner({
}
placeholder={
terminalContexts.length > 0 ? null : (
<div className="pointer-events-none absolute inset-0 text-[16px] leading-relaxed text-muted-foreground/35 sm:text-[14px]">
<div className="pointer-events-none absolute inset-0 text-[16px] leading-relaxed text-placeholder sm:text-[14px]">
{placeholder}
</div>
)
Expand Down
15 changes: 8 additions & 7 deletions apps/web/src/components/DiffPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,9 @@ const DIFF_PANEL_UNSAFE_CSS = `
[data-virtualizer-buffer] {
--diffs-header-font-family: var(--font-sans) !important;
--diffs-font-family: var(--font-mono) !important;
--diffs-bg: color-mix(in srgb, var(--card) 90%, var(--background)) !important;
--diffs-light-bg: color-mix(in srgb, var(--card) 90%, var(--background)) !important;
--diffs-dark-bg: color-mix(in srgb, var(--card) 90%, var(--background)) !important;
--diffs-bg: var(--code-background) !important;
--diffs-light-bg: var(--code-background) !important;
--diffs-dark-bg: var(--code-background) !important;
--diffs-token-light-bg: transparent;
--diffs-token-dark-bg: transparent;

Expand All @@ -117,19 +117,20 @@ const DIFF_PANEL_UNSAFE_CSS = `
);

background-color: var(--diffs-bg) !important;
color: var(--code-foreground) !important;
}

[data-file-info] {
background-color: color-mix(in srgb, var(--card) 94%, var(--foreground)) !important;
background-color: color-mix(in srgb, var(--code-background) 94%, var(--code-foreground)) !important;
border-block-color: var(--border) !important;
color: var(--foreground) !important;
color: var(--code-foreground) !important;
}

[data-diffs-header] {
position: sticky !important;
top: 0;
z-index: 4;
background-color: color-mix(in srgb, var(--card) 94%, var(--foreground)) !important;
background-color: color-mix(in srgb, var(--code-background) 94%, var(--code-foreground)) !important;
border-bottom: 1px solid var(--border) !important;
align-items: center !important;
font-family: var(--font-sans) !important;
Expand Down Expand Up @@ -838,7 +839,7 @@ export default function DiffPanel({
)}
{selectedPatchError && !renderablePatch && (
<div className="px-3">
<p className="mb-2 text-[11px] text-red-500/80">{selectedPatchError}</p>
<p className="mb-2 text-[11px] text-error/80">{selectedPatchError}</p>
</div>
)}
{!renderablePatch ? (
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/ProjectFavicon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ function ProjectFaviconFallback({
readonly className?: string | undefined;
readonly icon: ComponentType<{ className?: string }>;
}) {
return <Icon className={cn("size-3.5 shrink-0 text-muted-foreground/50", className)} />;
return <Icon className={cn("size-3.5 shrink-0 text-icon-muted", className)} />;
}

function ProjectFaviconImage({
Expand Down
Loading
Loading