| ... | @@ -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"; |
| 44 | | 46 | |
| 45 | const 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 | | | |
| 60 | const THEME_EDITOR_SIMPLE_ROLES: ReadonlyArray<ThemeColorRole> = ["canvas", "accent"]; | 47 | const THEME_EDITOR_SIMPLE_ROLES: ReadonlyArray<ThemeColorRole> = ["canvas", "accent"]; |
| 61 | | 48 | |
| 62 | const THEME_EDITOR_STATUS_ROLES: ReadonlyArray<ThemeColorRole> = [ | 49 | type 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 | | | |
| 74 | const THEME_EDITOR_ADVANCED_ROLES = THEME_COLOR_ROLES.filter( | | |
| 75 | (role) => !THEME_EDITOR_PRIMARY_ROLES.includes(role) && !THEME_EDITOR_STATUS_ROLES.includes(role), | | |
| 76 | ); | | |
| 77 | | 55 | |
| 78 | const THEME_EDITOR_ROLE_GROUPS: ReadonlyArray<{ | 56 | const 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 | ]; |
| 99 | | 217 | |
| 100 | type ThemeEditorColors = Record<ThemeColorRole, string>; | 218 | const THEME_EDITOR_COLOR_FAMILIES = THEME_EDITOR_ROLE_GROUPS.flatMap((group) => group.families); |
| | 219 | const 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 | |
| | 225 | function getThemeEditorColorFamily(role: ThemeColorRole): ThemeEditorColorFamily | null { |
| | 226 | return THEME_EDITOR_COLOR_FAMILY_BY_ROLE.get(role) ?? null; |
| | 227 | } |
| | 228 | |
| | 229 | type ThemeEditorColors = ThemeColors; |
| 101 | type ThemeEditorColorsByAppearance = Record<ThemeAppearance, ThemeEditorColors>; | 230 | type ThemeEditorColorsByAppearance = Record<ThemeAppearance, ThemeEditorColors>; |
| 102 | | 231 | |
| 103 | // A draft with no source theme starts as the standard T3 Code look — the | 232 | // 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({ |
| 348 | | 477 | |
| 349 | return { | 478 | return { |
| 350 | ...current, | 479 | ...current, |
| 351 | [activeAppearance]: shouldManageColors | 480 | [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 | ); |
| 365 | | 496 | |
| 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({ |
| 373 | | 505 | |
| 374 | requestAnimationFrame(() => { | 506 | requestAnimationFrame(() => { |
| 375 | panelRef.current | 507 | 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 | }, []); |
| 390 | | 522 | |
| 391 | const selectedHighlightRoles = selectedRole | 523 | 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(","); |
| 401 | | 535 | |
| ... | @@ -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 | ); |
| 855 | | 996 | |
| 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 | <ThemeColorField | 1003 | <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 | {isInspecting | 1165 | {isInspecting |
| 1019 | ? "Select an element · Esc to cancel" | 1166 | ? "Select an element · Esc to cancel" |
| 1020 | : selectedRole | 1167 | : 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 | )} |