Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
281 changes: 214 additions & 67 deletions apps/web/src/components/settings/ThemeEditorPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ import {
parseThemeFile,
removeCustomTheme,
themeIdFromName,
updateThemeColorFamily,
updateCustomTheme,
type ThemeAppearance,
type ThemeColors,
type ThemeColorRole,
type ThemeDefinition,
} from "../../themePalette";
Expand All @@ -42,62 +44,189 @@ import {
type ThemeElementInspection,
} from "./themeInspector";

const THEME_EDITOR_PRIMARY_ROLES: ReadonlyArray<ThemeColorRole> = [
"canvas",
"chrome",
"sidebar",
"surface",
"text",
"textMuted",
"placeholder",
"secondaryLabel",
"iconMuted",
"accent",
"messageSurface",
"messageAction",
];

const THEME_EDITOR_SIMPLE_ROLES: ReadonlyArray<ThemeColorRole> = ["canvas", "accent"];

const THEME_EDITOR_STATUS_ROLES: ReadonlyArray<ThemeColorRole> = [
"error",
"errorForeground",
"errorSurface",
"warning",
"warningForeground",
"warningSurface",
"update",
"updateForeground",
"updateSurface",
];

const THEME_EDITOR_ADVANCED_ROLES = THEME_COLOR_ROLES.filter(
(role) => !THEME_EDITOR_PRIMARY_ROLES.includes(role) && !THEME_EDITOR_STATUS_ROLES.includes(role),
);
type ThemeEditorColorFamily = Readonly<{
id: string;
label: string;
role: ThemeColorRole;
roles: ReadonlyArray<ThemeColorRole>;
}>;

const THEME_EDITOR_ROLE_GROUPS: ReadonlyArray<{
Comment thread
StiensWout marked this conversation as resolved.
id: string;
title: string;
roles: ReadonlyArray<ThemeColorRole>;
families: ReadonlyArray<ThemeEditorColorFamily>;
}> = [
{
id: "main",
title: "Main colors",
roles: THEME_EDITOR_PRIMARY_ROLES,
id: "foundation",
title: "Foundation",
families: [
{
id: "background",
label: "Background",
role: "canvas",
roles: ["canvas", "chrome", "toolbar"],
},
{ id: "surface", label: "Surface", role: "surface", roles: ["surface"] },
{
id: "raised-surface",
label: "Raised surface",
role: "surfaceRaised",
roles: ["surfaceRaised"],
},
{
id: "overlay",
label: "Overlay",
role: "surfaceOverlay",
roles: ["surfaceOverlay"],
},
{
id: "text",
label: "Text",
role: "text",
roles: ["text", "toolbarForeground", "toolbarControlForeground"],
},
{
id: "muted-text",
label: "Muted text",
role: "mutedForeground",
roles: [
"textMuted",
"mutedForeground",
"placeholder",
"secondaryLabel",
"iconMuted",
"sidebarMutedForeground",
],
},
{
id: "border",
label: "Border",
role: "border",
roles: ["border", "toolbarBorder", "sidebarBorder"],
},
{ id: "input", label: "Input", role: "input", roles: ["input"] },
],
},
{
id: "status",
title: "Status colors",
roles: THEME_EDITOR_STATUS_ROLES,
id: "brand-content",
title: "Brand & content",
families: [
{
id: "subtle-surface",
label: "Subtle surface",
role: "secondary",
roles: ["secondary", "secondaryForeground", "muted", "toolbarControl"],
},
{
id: "highlight-surface",
label: "Highlight surface",
role: "accentSurface",
roles: ["accentSurface", "accentSurfaceForeground", "toolbarControlHover"],
},
{
id: "accent",
label: "Accent",
role: "accent",
roles: [
"accent",
"accentForeground",
"focus",
"update",
"updateForeground",
"updateSurface",
"terminalCursor",
],
},
{
id: "action",
label: "Action",
role: "messageAction",
roles: ["messageAction", "messageActionForeground", "messageActionHover"],
},
{
id: "message-surface",
label: "Message surface",
role: "messageSurface",
roles: ["messageSurface", "messageForeground"],
},
{
id: "code-surface",
label: "Code surface",
role: "codeBackground",
roles: ["codeBackground", "codeForeground"],
},
],
},
{
id: "additional",
title: "Other colors",
roles: THEME_EDITOR_ADVANCED_ROLES,
id: "context",
title: "Context",
families: [
{
id: "sidebar-background",
label: "Sidebar background",
role: "sidebar",
roles: ["sidebar", "sidebarForeground"],
},
{
id: "sidebar-controls",
label: "Sidebar controls",
role: "sidebarControlSurface",
roles: ["sidebarControlSurface"],
},
{
id: "sidebar-selection",
label: "Sidebar selection",
role: "sidebarRowSelected",
roles: ["sidebarRowHover", "sidebarRowActive", "sidebarRowSelected"],
},
{
id: "terminal-background",
label: "Terminal background",
role: "terminalBackground",
roles: [
"terminalBackground",
"terminalForeground",
"terminalSelection",
"terminalScrollbar",
"terminalScrollbarHover",
],
},
],
},
{
id: "status",
title: "Status",
families: [
{
id: "error",
label: "Error",
role: "error",
roles: ["error", "errorForeground", "errorSurface"],
},
{
id: "warning",
label: "Warning",
role: "warning",
roles: ["warning", "warningForeground", "warningSurface"],
},
],
},
];

type ThemeEditorColors = Record<ThemeColorRole, string>;
const THEME_EDITOR_COLOR_FAMILIES = THEME_EDITOR_ROLE_GROUPS.flatMap((group) => group.families);
const THEME_EDITOR_COLOR_FAMILY_BY_ROLE = new Map(
THEME_EDITOR_COLOR_FAMILIES.flatMap((family) =>
family.roles.map((role) => [role, family] as const),
),
);

function getThemeEditorColorFamily(role: ThemeColorRole): ThemeEditorColorFamily | null {
return THEME_EDITOR_COLOR_FAMILY_BY_ROLE.get(role) ?? null;
}

type ThemeEditorColors = ThemeColors;
type ThemeEditorColorsByAppearance = Record<ThemeAppearance, ThemeEditorColors>;

// A draft with no source theme starts as the standard T3 Code look — the
Expand Down Expand Up @@ -348,9 +477,11 @@ export function ThemeEditorPanel({

return {
...current,
[activeAppearance]: shouldManageColors
? getManagedEditorColors(activeAppearance, nextColors)
: nextColors,
[activeAppearance]: isAdvanced
? updateThemeColorFamily(activeAppearance, current[activeAppearance], role, value)
: shouldManageColors
? getManagedEditorColors(activeAppearance, nextColors)
: nextColors,
};
});
if (!isAdvanced && THEME_EDITOR_SIMPLE_ROLES.includes(role) && isThemeEditorColor(value)) {
Expand All @@ -364,16 +495,17 @@ export function ThemeEditorPanel({
);

const selectThemeRole = useCallback((role: ThemeColorRole, reveal = false) => {
setSelectedRole(role);
if (!THEME_EDITOR_SIMPLE_ROLES.includes(role)) {
const visibleRole = getThemeEditorColorFamily(role)?.role ?? role;
setSelectedRole(visibleRole);
if (!THEME_EDITOR_SIMPLE_ROLES.includes(visibleRole)) {
setIsAdvanced(true);
setRoleQuery("");
}
if (!reveal) return;

requestAnimationFrame(() => {
panelRef.current
?.querySelector(`[data-theme-color-role="${role}"]`)
?.querySelector(`[data-theme-color-role="${visibleRole}"]`)
?.scrollIntoView({ behavior: "smooth", block: "nearest" });
});
}, []);
Expand All @@ -389,13 +521,15 @@ export function ThemeEditorPanel({
}, []);

const selectedHighlightRoles = selectedRole
? !isAdvanced && THEME_EDITOR_SIMPLE_ROLES.includes(selectedRole)
? THEME_COLOR_ROLES.filter(
(role) =>
colorsByAppearance[activeAppearance][role].trim().toLowerCase() ===
colorsByAppearance[activeAppearance][selectedRole].trim().toLowerCase(),
)
: [selectedRole]
? isAdvanced
? (getThemeEditorColorFamily(selectedRole)?.roles ?? [selectedRole])
: THEME_EDITOR_SIMPLE_ROLES.includes(selectedRole)
? THEME_COLOR_ROLES.filter(
(role) =>
colorsByAppearance[activeAppearance][role].trim().toLowerCase() ===
colorsByAppearance[activeAppearance][selectedRole].trim().toLowerCase(),
)
: [selectedRole]
: [];
const selectedHighlightRolesKey = selectedHighlightRoles.join(",");

Expand Down Expand Up @@ -490,7 +624,10 @@ export function ThemeEditorPanel({
};
const showInspection = (inspection: ThemeElementInspection) => {
hoverInspection = inspection;
showThemeInspectorHover(inspection, getThemeRoleLabel(inspection.role));
showThemeInspectorHover(
inspection,
getThemeEditorColorFamily(inspection.role)?.label ?? getThemeRoleLabel(inspection.role),
);
};
const handlePointerOver = (event: PointerEvent) => {
const target = event.target;
Expand Down Expand Up @@ -553,7 +690,11 @@ export function ThemeEditorPanel({
hoverFrame ??= requestAnimationFrame(() => {
hoverFrame = null;
if (hoverInspection) {
showThemeInspectorHover(hoverInspection, getThemeRoleLabel(hoverInspection.role));
showThemeInspectorHover(
hoverInspection,
getThemeEditorColorFamily(hoverInspection.role)?.label ??
getThemeRoleLabel(hoverInspection.role),
);
}
});
};
Expand Down Expand Up @@ -854,19 +995,20 @@ export function ThemeEditorPanel({
);

const renderRoleFields = (
roles: ReadonlyArray<ThemeColorRole>,
families: ReadonlyArray<ThemeEditorColorFamily>,
gridClassName = "grid gap-2 sm:grid-cols-2",
) => (
<div className={gridClassName}>
{roles.map((role) => (
{families.map((family) => (
<ThemeColorField
key={role}
key={family.id}
label={family.label}
onChange={updateColor}
onSelect={selectThemeRole}
onToggleSelected={toggleThemeRole}
role={role}
selected={selectedRole === role}
value={colorsByAppearance[activeAppearance][role]}
role={family.role}
selected={selectedRole === family.role}
value={colorsByAppearance[activeAppearance][family.role]}
/>
))}
</div>
Expand All @@ -876,16 +1018,21 @@ export function ThemeEditorPanel({
const query = roleQuery.trim().toLowerCase();
const groups = THEME_EDITOR_ROLE_GROUPS.map((group) => ({
...group,
roles: group.roles.filter(
(role) => !query || getThemeRoleLabel(role).toLowerCase().includes(query),
families: group.families.filter(
(family) =>
!query ||
[family.label, ...family.roles.map((role) => getThemeRoleLabel(role))]
.join(" ")
.toLowerCase()
.includes(query),
),
})).filter((group) => group.roles.length > 0);
})).filter((group) => group.families.length > 0);
return isAdvanced ? (
<div className="space-y-5">
{groups.map((group) => (
<section className="space-y-2" key={group.id}>
<h4 className="text-sm font-medium text-foreground">{group.title}</h4>
{renderRoleFields(group.roles, "grid gap-1")}
{renderRoleFields(group.families, "grid gap-1")}
</section>
))}
{groups.length === 0 ? <p className="text-xs text-muted-foreground">No matches.</p> : null}
Expand Down Expand Up @@ -1018,7 +1165,7 @@ export function ThemeEditorPanel({
{isInspecting
? "Select an element · Esc to cancel"
: selectedRole
? `${getThemeRoleLabel(selectedRole)} · ${usageCount ?? 0} ${usageCount === 1 ? "use" : "uses"}`
? `${isAdvanced ? (getThemeEditorColorFamily(selectedRole)?.label ?? getThemeRoleLabel(selectedRole)) : getThemeRoleLabel(selectedRole)} · ${usageCount ?? 0} ${usageCount === 1 ? "use" : "uses"}`
: "Select a color below"}
</p>
)}
Expand Down
Loading
Loading