authorgravatar for 71498452+StiensWout@users.noreply.github.comWout Stiens <71498452+StiensWout@users.noreply.github.com> 2026-08-15 20:59:13+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-15 14:59:13-04:00
log2f486ab80c748b4d8e3d3b17e49b5a327cb93335
tree457cc46729ba0c4793a4472e2634569625cbf839
parente58cbb9e75e97448f88d0fe2eccc8a45cd980394
signature Signed by PGP key B5690EEEBB952194

refactor(web): simplify advanced theme controls (#7107)


3 files changed, 409 insertions(+), 68 deletions(-)

apps/web/src/components/settings/ThemeEditorPanel.tsx+214-67
...@@ -20,8 +20,10 @@ import {...@@ -20,8 +20,10 @@ import {
20 parseThemeFile,20 parseThemeFile,
21 removeCustomTheme,21 removeCustomTheme,
22 themeIdFromName,22 themeIdFromName,
23 updateThemeColorFamily,
23 updateCustomTheme,24 updateCustomTheme,
24 type ThemeAppearance,25 type ThemeAppearance,
26 type ThemeColors,
25 type ThemeColorRole,27 type ThemeColorRole,
26 type ThemeDefinition,28 type ThemeDefinition,
27} from "../../themePalette";29} from "../../themePalette";
...@@ -42,62 +44,189 @@ import {...@@ -42,62 +44,189 @@ import {
42 type ThemeElementInspection,44 type ThemeElementInspection,
43} from "./themeInspector";45} from "./themeInspector";
4446
45const THEME_EDITOR_PRIMARY_ROLES: ReadonlyArray<ThemeColorRole> = [
46 "canvas",
47 "chrome",
48 "sidebar",
49 "surface",
50 "text",
51 "textMuted",
52 "placeholder",
53 "secondaryLabel",
54 "iconMuted",
55 "accent",
56 "messageSurface",
57 "messageAction",
58];
59
60const THEME_EDITOR_SIMPLE_ROLES: ReadonlyArray<ThemeColorRole> = ["canvas", "accent"];47const THEME_EDITOR_SIMPLE_ROLES: ReadonlyArray<ThemeColorRole> = ["canvas", "accent"];
6148
62const THEME_EDITOR_STATUS_ROLES: ReadonlyArray<ThemeColorRole> = [49type ThemeEditorColorFamily = Readonly<{
63 "error",50 id: string;
64 "errorForeground",51 label: string;
65 "errorSurface",52 role: ThemeColorRole;
66 "warning",53 roles: ReadonlyArray<ThemeColorRole>;
67 "warningForeground",54}>;
68 "warningSurface",
69 "update",
70 "updateForeground",
71 "updateSurface",
72];
73
74const THEME_EDITOR_ADVANCED_ROLES = THEME_COLOR_ROLES.filter(
75 (role) => !THEME_EDITOR_PRIMARY_ROLES.includes(role) && !THEME_EDITOR_STATUS_ROLES.includes(role),
76);
7755
78const THEME_EDITOR_ROLE_GROUPS: ReadonlyArray<{56const THEME_EDITOR_ROLE_GROUPS: ReadonlyArray<{
79 id: string;57 id: string;
80 title: string;58 title: string;
81 roles: ReadonlyArray<ThemeColorRole>;59 families: ReadonlyArray<ThemeEditorColorFamily>;
82}> = [60}> = [
83 {61 {
84 id: "main",62 id: "foundation",
85 title: "Main colors",63 title: "Foundation",
86 roles: THEME_EDITOR_PRIMARY_ROLES,64 families: [
65 {
66 id: "background",
67 label: "Background",
68 role: "canvas",
69 roles: ["canvas", "chrome", "toolbar"],
70 },
71 { id: "surface", label: "Surface", role: "surface", roles: ["surface"] },
72 {
73 id: "raised-surface",
74 label: "Raised surface",
75 role: "surfaceRaised",
76 roles: ["surfaceRaised"],
77 },
78 {
79 id: "overlay",
80 label: "Overlay",
81 role: "surfaceOverlay",
82 roles: ["surfaceOverlay"],
83 },
84 {
85 id: "text",
86 label: "Text",
87 role: "text",
88 roles: ["text", "toolbarForeground", "toolbarControlForeground"],
89 },
90 {
91 id: "muted-text",
92 label: "Muted text",
93 role: "mutedForeground",
94 roles: [
95 "textMuted",
96 "mutedForeground",
97 "placeholder",
98 "secondaryLabel",
99 "iconMuted",
100 "sidebarMutedForeground",
101 ],
102 },
103 {
104 id: "border",
105 label: "Border",
106 role: "border",
107 roles: ["border", "toolbarBorder", "sidebarBorder"],
108 },
109 { id: "input", label: "Input", role: "input", roles: ["input"] },
110 ],
87 },111 },
88 {112 {
89 id: "status",113 id: "brand-content",
90 title: "Status colors",114 title: "Brand & content",
91 roles: THEME_EDITOR_STATUS_ROLES,115 families: [
116 {
117 id: "subtle-surface",
118 label: "Subtle surface",
119 role: "secondary",
120 roles: ["secondary", "secondaryForeground", "muted", "toolbarControl"],
121 },
122 {
123 id: "highlight-surface",
124 label: "Highlight surface",
125 role: "accentSurface",
126 roles: ["accentSurface", "accentSurfaceForeground", "toolbarControlHover"],
127 },
128 {
129 id: "accent",
130 label: "Accent",
131 role: "accent",
132 roles: [
133 "accent",
134 "accentForeground",
135 "focus",
136 "update",
137 "updateForeground",
138 "updateSurface",
139 "terminalCursor",
140 ],
141 },
142 {
143 id: "action",
144 label: "Action",
145 role: "messageAction",
146 roles: ["messageAction", "messageActionForeground", "messageActionHover"],
147 },
148 {
149 id: "message-surface",
150 label: "Message surface",
151 role: "messageSurface",
152 roles: ["messageSurface", "messageForeground"],
153 },
154 {
155 id: "code-surface",
156 label: "Code surface",
157 role: "codeBackground",
158 roles: ["codeBackground", "codeForeground"],
159 },
160 ],
92 },161 },
93 {162 {
94 id: "additional",163 id: "context",
95 title: "Other colors",164 title: "Context",
96 roles: THEME_EDITOR_ADVANCED_ROLES,165 families: [
166 {
167 id: "sidebar-background",
168 label: "Sidebar background",
169 role: "sidebar",
170 roles: ["sidebar", "sidebarForeground"],
171 },
172 {
173 id: "sidebar-controls",
174 label: "Sidebar controls",
175 role: "sidebarControlSurface",
176 roles: ["sidebarControlSurface"],
177 },
178 {
179 id: "sidebar-selection",
180 label: "Sidebar selection",
181 role: "sidebarRowSelected",
182 roles: ["sidebarRowHover", "sidebarRowActive", "sidebarRowSelected"],
183 },
184 {
185 id: "terminal-background",
186 label: "Terminal background",
187 role: "terminalBackground",
188 roles: [
189 "terminalBackground",
190 "terminalForeground",
191 "terminalSelection",
192 "terminalScrollbar",
193 "terminalScrollbarHover",
194 ],
195 },
196 ],
197 },
198 {
199 id: "status",
200 title: "Status",
201 families: [
202 {
203 id: "error",
204 label: "Error",
205 role: "error",
206 roles: ["error", "errorForeground", "errorSurface"],
207 },
208 {
209 id: "warning",
210 label: "Warning",
211 role: "warning",
212 roles: ["warning", "warningForeground", "warningSurface"],
213 },
214 ],
97 },215 },
98];216];
99217
100type ThemeEditorColors = Record<ThemeColorRole, string>;218const THEME_EDITOR_COLOR_FAMILIES = THEME_EDITOR_ROLE_GROUPS.flatMap((group) => group.families);
219const THEME_EDITOR_COLOR_FAMILY_BY_ROLE = new Map(
220 THEME_EDITOR_COLOR_FAMILIES.flatMap((family) =>
221 family.roles.map((role) => [role, family] as const),
222 ),
223);
224
225function getThemeEditorColorFamily(role: ThemeColorRole): ThemeEditorColorFamily | null {
226 return THEME_EDITOR_COLOR_FAMILY_BY_ROLE.get(role) ?? null;
227}
228
229type ThemeEditorColors = ThemeColors;
101type ThemeEditorColorsByAppearance = Record<ThemeAppearance, ThemeEditorColors>;230type ThemeEditorColorsByAppearance = Record<ThemeAppearance, ThemeEditorColors>;
102231
103// A draft with no source theme starts as the standard T3 Code look — the232// A draft with no source theme starts as the standard T3 Code look — the
...@@ -348,9 +477,11 @@ export function ThemeEditorPanel({...@@ -348,9 +477,11 @@ export function ThemeEditorPanel({
348477
349 return {478 return {
350 ...current,479 ...current,
351 [activeAppearance]: shouldManageColors480 [activeAppearance]: isAdvanced
352 ? getManagedEditorColors(activeAppearance, nextColors)481 ? updateThemeColorFamily(activeAppearance, current[activeAppearance], role, value)
353 : nextColors,482 : shouldManageColors
483 ? getManagedEditorColors(activeAppearance, nextColors)
484 : nextColors,
354 };485 };
355 });486 });
356 if (!isAdvanced && THEME_EDITOR_SIMPLE_ROLES.includes(role) && isThemeEditorColor(value)) {487 if (!isAdvanced && THEME_EDITOR_SIMPLE_ROLES.includes(role) && isThemeEditorColor(value)) {
...@@ -364,8 +495,9 @@ export function ThemeEditorPanel({...@@ -364,8 +495,9 @@ export function ThemeEditorPanel({
364 );495 );
365496
366 const selectThemeRole = useCallback((role: ThemeColorRole, reveal = false) => {497 const selectThemeRole = useCallback((role: ThemeColorRole, reveal = false) => {
367 setSelectedRole(role);498 const visibleRole = getThemeEditorColorFamily(role)?.role ?? role;
368 if (!THEME_EDITOR_SIMPLE_ROLES.includes(role)) {499 setSelectedRole(visibleRole);
500 if (!THEME_EDITOR_SIMPLE_ROLES.includes(visibleRole)) {
369 setIsAdvanced(true);501 setIsAdvanced(true);
370 setRoleQuery("");502 setRoleQuery("");
371 }503 }
...@@ -373,7 +505,7 @@ export function ThemeEditorPanel({...@@ -373,7 +505,7 @@ export function ThemeEditorPanel({
373505
374 requestAnimationFrame(() => {506 requestAnimationFrame(() => {
375 panelRef.current507 panelRef.current
376 ?.querySelector(`[data-theme-color-role="${role}"]`)508 ?.querySelector(`[data-theme-color-role="${visibleRole}"]`)
377 ?.scrollIntoView({ behavior: "smooth", block: "nearest" });509 ?.scrollIntoView({ behavior: "smooth", block: "nearest" });
378 });510 });
379 }, []);511 }, []);
...@@ -389,13 +521,15 @@ export function ThemeEditorPanel({...@@ -389,13 +521,15 @@ export function ThemeEditorPanel({
389 }, []);521 }, []);
390522
391 const selectedHighlightRoles = selectedRole523 const selectedHighlightRoles = selectedRole
392 ? !isAdvanced && THEME_EDITOR_SIMPLE_ROLES.includes(selectedRole)524 ? isAdvanced
393 ? THEME_COLOR_ROLES.filter(525 ? (getThemeEditorColorFamily(selectedRole)?.roles ?? [selectedRole])
394 (role) =>526 : THEME_EDITOR_SIMPLE_ROLES.includes(selectedRole)
395 colorsByAppearance[activeAppearance][role].trim().toLowerCase() ===527 ? THEME_COLOR_ROLES.filter(
396 colorsByAppearance[activeAppearance][selectedRole].trim().toLowerCase(),528 (role) =>
397 )529 colorsByAppearance[activeAppearance][role].trim().toLowerCase() ===
398 : [selectedRole]530 colorsByAppearance[activeAppearance][selectedRole].trim().toLowerCase(),
531 )
532 : [selectedRole]
399 : [];533 : [];
400 const selectedHighlightRolesKey = selectedHighlightRoles.join(",");534 const selectedHighlightRolesKey = selectedHighlightRoles.join(",");
401535
...@@ -490,7 +624,10 @@ export function ThemeEditorPanel({...@@ -490,7 +624,10 @@ export function ThemeEditorPanel({
490 };624 };
491 const showInspection = (inspection: ThemeElementInspection) => {625 const showInspection = (inspection: ThemeElementInspection) => {
492 hoverInspection = inspection;626 hoverInspection = inspection;
493 showThemeInspectorHover(inspection, getThemeRoleLabel(inspection.role));627 showThemeInspectorHover(
628 inspection,
629 getThemeEditorColorFamily(inspection.role)?.label ?? getThemeRoleLabel(inspection.role),
630 );
494 };631 };
495 const handlePointerOver = (event: PointerEvent) => {632 const handlePointerOver = (event: PointerEvent) => {
496 const target = event.target;633 const target = event.target;
...@@ -553,7 +690,11 @@ export function ThemeEditorPanel({...@@ -553,7 +690,11 @@ export function ThemeEditorPanel({
553 hoverFrame ??= requestAnimationFrame(() => {690 hoverFrame ??= requestAnimationFrame(() => {
554 hoverFrame = null;691 hoverFrame = null;
555 if (hoverInspection) {692 if (hoverInspection) {
556 showThemeInspectorHover(hoverInspection, getThemeRoleLabel(hoverInspection.role));693 showThemeInspectorHover(
694 hoverInspection,
695 getThemeEditorColorFamily(hoverInspection.role)?.label ??
696 getThemeRoleLabel(hoverInspection.role),
697 );
557 }698 }
558 });699 });
559 };700 };
...@@ -854,19 +995,20 @@ export function ThemeEditorPanel({...@@ -854,19 +995,20 @@ export function ThemeEditorPanel({
854 );995 );
855996
856 const renderRoleFields = (997 const renderRoleFields = (
857 roles: ReadonlyArray<ThemeColorRole>,998 families: ReadonlyArray<ThemeEditorColorFamily>,
858 gridClassName = "grid gap-2 sm:grid-cols-2",999 gridClassName = "grid gap-2 sm:grid-cols-2",
859 ) => (1000 ) => (
860 <div className={gridClassName}>1001 <div className={gridClassName}>
861 {roles.map((role) => (1002 {families.map((family) => (
862 <ThemeColorField1003 <ThemeColorField
863 key={role}1004 key={family.id}
1005 label={family.label}
864 onChange={updateColor}1006 onChange={updateColor}
865 onSelect={selectThemeRole}1007 onSelect={selectThemeRole}
866 onToggleSelected={toggleThemeRole}1008 onToggleSelected={toggleThemeRole}
867 role={role}1009 role={family.role}
868 selected={selectedRole === role}1010 selected={selectedRole === family.role}
869 value={colorsByAppearance[activeAppearance][role]}1011 value={colorsByAppearance[activeAppearance][family.role]}
870 />1012 />
871 ))}1013 ))}
872 </div>1014 </div>
...@@ -876,16 +1018,21 @@ export function ThemeEditorPanel({...@@ -876,16 +1018,21 @@ export function ThemeEditorPanel({
876 const query = roleQuery.trim().toLowerCase();1018 const query = roleQuery.trim().toLowerCase();
877 const groups = THEME_EDITOR_ROLE_GROUPS.map((group) => ({1019 const groups = THEME_EDITOR_ROLE_GROUPS.map((group) => ({
878 ...group,1020 ...group,
879 roles: group.roles.filter(1021 families: group.families.filter(
880 (role) => !query || getThemeRoleLabel(role).toLowerCase().includes(query),1022 (family) =>
1023 !query ||
1024 [family.label, ...family.roles.map((role) => getThemeRoleLabel(role))]
1025 .join(" ")
1026 .toLowerCase()
1027 .includes(query),
881 ),1028 ),
882 })).filter((group) => group.roles.length > 0);1029 })).filter((group) => group.families.length > 0);
883 return isAdvanced ? (1030 return isAdvanced ? (
884 <div className="space-y-5">1031 <div className="space-y-5">
885 {groups.map((group) => (1032 {groups.map((group) => (
886 <section className="space-y-2" key={group.id}>1033 <section className="space-y-2" key={group.id}>
887 <h4 className="text-sm font-medium text-foreground">{group.title}</h4>1034 <h4 className="text-sm font-medium text-foreground">{group.title}</h4>
888 {renderRoleFields(group.roles, "grid gap-1")}1035 {renderRoleFields(group.families, "grid gap-1")}
889 </section>1036 </section>
890 ))}1037 ))}
891 {groups.length === 0 ? <p className="text-xs text-muted-foreground">No matches.</p> : null}1038 {groups.length === 0 ? <p className="text-xs text-muted-foreground">No matches.</p> : null}
...@@ -1018,7 +1165,7 @@ export function ThemeEditorPanel({...@@ -1018,7 +1165,7 @@ export function ThemeEditorPanel({
1018 {isInspecting1165 {isInspecting
1019 ? "Select an element · Esc to cancel"1166 ? "Select an element · Esc to cancel"
1020 : selectedRole1167 : selectedRole
1021 ? `${getThemeRoleLabel(selectedRole)} · ${usageCount ?? 0} ${usageCount === 1 ? "use" : "uses"}`1168 ? `${isAdvanced ? (getThemeEditorColorFamily(selectedRole)?.label ?? getThemeRoleLabel(selectedRole)) : getThemeRoleLabel(selectedRole)} · ${usageCount ?? 0} ${usageCount === 1 ? "use" : "uses"}`
1022 : "Select a color below"}1169 : "Select a color below"}
1023 </p>1170 </p>
1024 )}1171 )}
apps/web/src/themePalette.ts+192
...@@ -1468,6 +1468,198 @@ function themeActionColors(...@@ -1468,6 +1468,198 @@ function themeActionColors(
1468 };1468 };
1469}1469}
14701470
1471/**
1472 * Update one Advanced-editor color family without normalizing the rest of an
1473 * imported or hand-tuned palette. The editor exposes a representative role
1474 * for each family; paired foregrounds and nearby states are derived only when
1475 * that representative is changed.
1476 */
1477export function updateThemeColorFamily(
1478 appearance: ThemeAppearance,
1479 colors: ThemeColors,
1480 role: ThemeColorRole,
1481 value: string,
1482): ThemeColors {
1483 const parsedSelected = parseThemeColor(value);
1484 if (!parsedSelected) return { ...colors, [role]: value };
1485 const normalized = formatOklchThemeColor(parsedSelected.color, parsedSelected.alpha);
1486
1487 const canvas = parseThemeRgbColor(
1488 colors.canvas,
1489 appearance === "dark" ? { r: 24, g: 15, b: 27 } : { r: 250, g: 245, b: 250 },
1490 );
1491 const selected = themeOklchToRgb(parsedSelected.color);
1492 const selectedOn = (background: ThemeRgbColor) =>
1493 mixThemeRgbColors(background, selected, parsedSelected.alpha);
1494 const selectedOnCanvas = selectedOn(canvas);
1495 const accent = parseThemeRgbColor(colors.accent, { r: 168, g: 67, b: 112 });
1496 const canvasIsDark = themeRelativeLuminance(canvas) < 0.179;
1497 const terminalIsDark = themeRelativeLuminance(selectedOnCanvas) < 0.179;
1498 const colorOf = (color: ThemeRgbColor) => themeRgbToThemeColor(color);
1499 const foregroundOn = (background: ThemeRgbColor) => colorOf(readableThemeForeground(background));
1500 const selectedToneOn = (background: ThemeRgbColor) =>
1501 themeOklchToThemeColor(
1502 solveOklchLightness(
1503 parsedSelected.color,
1504 background,
1505 4.6,
1506 themeRelativeLuminance(background) < 0.179 ? "lighter" : "darker",
1507 ),
1508 );
1509 const statusColors = () => {
1510 const surface = mixThemeRgbColors(canvas, selectedOnCanvas, canvasIsDark ? 0.16 : 0.08);
1511 return {
1512 foreground: selectedToneOn(surface),
1513 surface: colorOf(surface),
1514 };
1515 };
1516
1517 switch (role) {
1518 case "canvas":
1519 return { ...colors, canvas: normalized, chrome: normalized, toolbar: normalized };
1520 case "surface":
1521 case "surfaceRaised":
1522 case "surfaceOverlay":
1523 case "input":
1524 case "sidebarControlSurface":
1525 return { ...colors, [role]: normalized };
1526 case "text":
1527 return {
1528 ...colors,
1529 text: normalized,
1530 toolbarForeground: normalized,
1531 toolbarControlForeground: normalized,
1532 };
1533 case "mutedForeground":
1534 return {
1535 ...colors,
1536 textMuted: normalized,
1537 mutedForeground: normalized,
1538 placeholder: normalized,
1539 secondaryLabel: normalized,
1540 iconMuted: normalized,
1541 sidebarMutedForeground: normalized,
1542 };
1543 case "border":
1544 return {
1545 ...colors,
1546 border: normalized,
1547 toolbarBorder: normalized,
1548 sidebarBorder: normalized,
1549 };
1550 case "secondary":
1551 return {
1552 ...colors,
1553 secondary: normalized,
1554 secondaryForeground: foregroundOn(selectedOnCanvas),
1555 muted: normalized,
1556 toolbarControl: normalized,
1557 };
1558 case "accentSurface":
1559 return {
1560 ...colors,
1561 accentSurface: normalized,
1562 accentSurfaceForeground: foregroundOn(selectedOnCanvas),
1563 toolbarControlHover: normalized,
1564 };
1565 case "accent": {
1566 const updateSurface = mixThemeRgbColors(canvas, selectedOnCanvas, canvasIsDark ? 0.32 : 0.16);
1567 return {
1568 ...colors,
1569 accent: normalized,
1570 accentForeground: foregroundOn(selectedOnCanvas),
1571 focus: normalized,
1572 update: normalized,
1573 updateForeground: selectedToneOn(updateSurface),
1574 updateSurface: colorOf(updateSurface),
1575 terminalCursor: normalized,
1576 };
1577 }
1578 case "messageAction": {
1579 const actionForeground = readableThemeForeground(selectedOnCanvas);
1580 const towardOpposite =
1581 actionForeground === THEME_LIGHT_FOREGROUND || actionForeground === THEME_WHITE_FOREGROUND
1582 ? THEME_BLACK_FOREGROUND
1583 : THEME_WHITE_FOREGROUND;
1584 const actionHover = mixThemeRgbColors(selected, towardOpposite, 0.12);
1585 return {
1586 ...colors,
1587 messageAction: normalized,
1588 messageActionForeground: colorOf(actionForeground),
1589 messageActionHover: formatOklchThemeColor(
1590 themeRgbToOklch(actionHover),
1591 parsedSelected.alpha,
1592 ),
1593 };
1594 }
1595 case "messageSurface":
1596 return {
1597 ...colors,
1598 messageSurface: normalized,
1599 messageForeground: foregroundOn(selectedOnCanvas),
1600 };
1601 case "codeBackground":
1602 return {
1603 ...colors,
1604 codeBackground: normalized,
1605 codeForeground: foregroundOn(selectedOnCanvas),
1606 };
1607 case "sidebar":
1608 return {
1609 ...colors,
1610 sidebar: normalized,
1611 sidebarForeground: foregroundOn(selectedOnCanvas),
1612 };
1613 case "sidebarRowSelected": {
1614 const sidebar = parseThemeRgbColor(colors.sidebar, canvas);
1615 const selectedOnSidebar = selectedOn(sidebar);
1616 return {
1617 ...colors,
1618 sidebarRowHover: colorOf(mixThemeRgbColors(sidebar, selectedOnSidebar, 0.5)),
1619 sidebarRowActive: colorOf(mixThemeRgbColors(sidebar, selectedOnSidebar, 0.8)),
1620 sidebarRowSelected: normalized,
1621 };
1622 }
1623 case "terminalBackground": {
1624 const terminalForeground = readableThemeForeground(selectedOnCanvas);
1625 return {
1626 ...colors,
1627 terminalBackground: normalized,
1628 terminalForeground: colorOf(terminalForeground),
1629 terminalSelection: colorOf(
1630 mixThemeRgbColors(selectedOnCanvas, accent, terminalIsDark ? 0.35 : 0.18),
1631 ),
1632 terminalScrollbar: colorOf(
1633 mixThemeRgbColors(selectedOnCanvas, terminalForeground, terminalIsDark ? 0.42 : 0.22),
1634 ),
1635 terminalScrollbarHover: colorOf(
1636 mixThemeRgbColors(selectedOnCanvas, terminalForeground, terminalIsDark ? 0.55 : 0.32),
1637 ),
1638 };
1639 }
1640 case "error": {
1641 const status = statusColors();
1642 return {
1643 ...colors,
1644 error: normalized,
1645 errorForeground: status.foreground,
1646 errorSurface: status.surface,
1647 };
1648 }
1649 case "warning": {
1650 const status = statusColors();
1651 return {
1652 ...colors,
1653 warning: normalized,
1654 warningForeground: status.foreground,
1655 warningSurface: status.surface,
1656 };
1657 }
1658 default:
1659 return { ...colors, [role]: normalized };
1660 }
1661}
1662
1471export const GROVE_THEME: ThemeDefinition = {1663export const GROVE_THEME: ThemeDefinition = {
1472 id: GROVE_THEME_ID,1664 id: GROVE_THEME_ID,
1473 label: GROVE_THEME_LABEL,1665 label: GROVE_THEME_LABEL,
docs/user/keybindings.md+3-1
...@@ -43,8 +43,10 @@ Repeating either shortcut closes that search, and switching shortcuts replaces t...@@ -43,8 +43,10 @@ Repeating either shortcut closes that search, and switching shortcuts replaces t
43`themeEditor.toggle` opens or closes the floating theme editor and defaults to43`themeEditor.toggle` opens or closes the floating theme editor and defaults to
44`mod+alt+shift+t`. Select a color label to spotlight the elements that use it; select the label44`mod+alt+shift+t`. Select a color label to spotlight the elements that use it; select the label
45again to clear the spotlight. The swatch and hex field keep that color selected while you edit it.45again to clear the spotlight. The swatch and hex field keep that color selected while you edit it.
46Advanced mode groups related app tokens into a smaller set of color families. Changing a family
47updates its paired text and interaction states while leaving every unrelated imported color intact.
46Use **Inspect** to pick an element in the app and reveal its color token. Inspect disarms after one48Use **Inspect** to pick an element in the app and reveal its color token. Inspect disarms after one
47successful pick; its hover glow and badge preview the element and token that click will select.49successful pick; its hover glow and badge preview the element and color family that click will select.
48**Cancel** or `Escape` exits Inspect and clears its selection and spotlight.50**Cancel** or `Escape` exits Inspect and clears its selection and spotlight.
4951
50`rightPanel.toggleMaximized` maximizes or restores the open right panel. It has no default shortcut,52`rightPanel.toggleMaximized` maximizes or restores the open right panel. It has no default shortcut,