| ... | ... | @@ -20,8 +20,10 @@ import { |
| 20 | 20 | parseThemeFile, |
| 21 | 21 | removeCustomTheme, |
| 22 | 22 | themeIdFromName, |
| 23 | updateThemeColorFamily, |
| 23 | 24 | updateCustomTheme, |
| 24 | 25 | type ThemeAppearance, |
| 26 | type ThemeColors, |
| 25 | 27 | type ThemeColorRole, |
| 26 | 28 | type ThemeDefinition, |
| 27 | 29 | } from "../../themePalette"; |
| ... | ... | @@ -42,62 +44,189 @@ import { |
| 42 | 44 | type ThemeElementInspection, |
| 43 | 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 | 47 | const THEME_EDITOR_SIMPLE_ROLES: ReadonlyArray<ThemeColorRole> = ["canvas", "accent"]; |
| 61 | 48 | |
| 62 | | const THEME_EDITOR_STATUS_ROLES: ReadonlyArray<ThemeColorRole> = [ |
| 63 | | "error", |
| 64 | | "errorForeground", |
| 65 | | "errorSurface", |
| 66 | | "warning", |
| 67 | | "warningForeground", |
| 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 | | ); |
| 49 | type ThemeEditorColorFamily = Readonly<{ |
| 50 | id: string; |
| 51 | label: string; |
| 52 | role: ThemeColorRole; |
| 53 | roles: ReadonlyArray<ThemeColorRole>; |
| 54 | }>; |
| 77 | 55 | |
| 78 | 56 | const THEME_EDITOR_ROLE_GROUPS: ReadonlyArray<{ |
| 79 | 57 | id: string; |
| 80 | 58 | title: string; |
| 81 | | roles: ReadonlyArray<ThemeColorRole>; |
| 59 | families: ReadonlyArray<ThemeEditorColorFamily>; |
| 82 | 60 | }> = [ |
| 83 | 61 | { |
| 84 | | id: "main", |
| 85 | | title: "Main colors", |
| 86 | | roles: THEME_EDITOR_PRIMARY_ROLES, |
| 62 | id: "foundation", |
| 63 | title: "Foundation", |
| 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", |
| 90 | | title: "Status colors", |
| 91 | | roles: THEME_EDITOR_STATUS_ROLES, |
| 113 | id: "brand-content", |
| 114 | title: "Brand & content", |
| 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", |
| 95 | | title: "Other colors", |
| 96 | | roles: THEME_EDITOR_ADVANCED_ROLES, |
| 163 | id: "context", |
| 164 | title: "Context", |
| 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 | 230 | type ThemeEditorColorsByAppearance = Record<ThemeAppearance, ThemeEditorColors>; |
| 102 | 231 | |
| 103 | 232 | // A draft with no source theme starts as the standard T3 Code look — the |
| ... | ... | @@ -348,9 +477,11 @@ export function ThemeEditorPanel({ |
| 348 | 477 | |
| 349 | 478 | return { |
| 350 | 479 | ...current, |
| 351 | | [activeAppearance]: shouldManageColors |
| 352 | | ? getManagedEditorColors(activeAppearance, nextColors) |
| 353 | | : nextColors, |
| 480 | [activeAppearance]: isAdvanced |
| 481 | ? updateThemeColorFamily(activeAppearance, current[activeAppearance], role, value) |
| 482 | : shouldManageColors |
| 483 | ? getManagedEditorColors(activeAppearance, nextColors) |
| 484 | : nextColors, |
| 354 | 485 | }; |
| 355 | 486 | }); |
| 356 | 487 | if (!isAdvanced && THEME_EDITOR_SIMPLE_ROLES.includes(role) && isThemeEditorColor(value)) { |
| ... | ... | @@ -364,8 +495,9 @@ export function ThemeEditorPanel({ |
| 364 | 495 | ); |
| 365 | 496 | |
| 366 | 497 | const selectThemeRole = useCallback((role: ThemeColorRole, reveal = false) => { |
| 367 | | setSelectedRole(role); |
| 368 | | if (!THEME_EDITOR_SIMPLE_ROLES.includes(role)) { |
| 498 | const visibleRole = getThemeEditorColorFamily(role)?.role ?? role; |
| 499 | setSelectedRole(visibleRole); |
| 500 | if (!THEME_EDITOR_SIMPLE_ROLES.includes(visibleRole)) { |
| 369 | 501 | setIsAdvanced(true); |
| 370 | 502 | setRoleQuery(""); |
| 371 | 503 | } |
| ... | ... | @@ -373,7 +505,7 @@ export function ThemeEditorPanel({ |
| 373 | 505 | |
| 374 | 506 | requestAnimationFrame(() => { |
| 375 | 507 | panelRef.current |
| 376 | | ?.querySelector(`[data-theme-color-role="${role}"]`) |
| 508 | ?.querySelector(`[data-theme-color-role="${visibleRole}"]`) |
| 377 | 509 | ?.scrollIntoView({ behavior: "smooth", block: "nearest" }); |
| 378 | 510 | }); |
| 379 | 511 | }, []); |
| ... | ... | @@ -389,13 +521,15 @@ export function ThemeEditorPanel({ |
| 389 | 521 | }, []); |
| 390 | 522 | |
| 391 | 523 | const selectedHighlightRoles = selectedRole |
| 392 | | ? !isAdvanced && THEME_EDITOR_SIMPLE_ROLES.includes(selectedRole) |
| 393 | | ? THEME_COLOR_ROLES.filter( |
| 394 | | (role) => |
| 395 | | colorsByAppearance[activeAppearance][role].trim().toLowerCase() === |
| 396 | | colorsByAppearance[activeAppearance][selectedRole].trim().toLowerCase(), |
| 397 | | ) |
| 398 | | : [selectedRole] |
| 524 | ? isAdvanced |
| 525 | ? (getThemeEditorColorFamily(selectedRole)?.roles ?? [selectedRole]) |
| 526 | : THEME_EDITOR_SIMPLE_ROLES.includes(selectedRole) |
| 527 | ? THEME_COLOR_ROLES.filter( |
| 528 | (role) => |
| 529 | colorsByAppearance[activeAppearance][role].trim().toLowerCase() === |
| 530 | colorsByAppearance[activeAppearance][selectedRole].trim().toLowerCase(), |
| 531 | ) |
| 532 | : [selectedRole] |
| 399 | 533 | : []; |
| 400 | 534 | const selectedHighlightRolesKey = selectedHighlightRoles.join(","); |
| 401 | 535 | |
| ... | ... | @@ -490,7 +624,10 @@ export function ThemeEditorPanel({ |
| 490 | 624 | }; |
| 491 | 625 | const showInspection = (inspection: ThemeElementInspection) => { |
| 492 | 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 | 632 | const handlePointerOver = (event: PointerEvent) => { |
| 496 | 633 | const target = event.target; |
| ... | ... | @@ -553,7 +690,11 @@ export function ThemeEditorPanel({ |
| 553 | 690 | hoverFrame ??= requestAnimationFrame(() => { |
| 554 | 691 | hoverFrame = null; |
| 555 | 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 | 995 | ); |
| 855 | 996 | |
| 856 | 997 | const renderRoleFields = ( |
| 857 | | roles: ReadonlyArray<ThemeColorRole>, |
| 998 | families: ReadonlyArray<ThemeEditorColorFamily>, |
| 858 | 999 | gridClassName = "grid gap-2 sm:grid-cols-2", |
| 859 | 1000 | ) => ( |
| 860 | 1001 | <div className={gridClassName}> |
| 861 | | {roles.map((role) => ( |
| 1002 | {families.map((family) => ( |
| 862 | 1003 | <ThemeColorField |
| 863 | | key={role} |
| 1004 | key={family.id} |
| 1005 | label={family.label} |
| 864 | 1006 | onChange={updateColor} |
| 865 | 1007 | onSelect={selectThemeRole} |
| 866 | 1008 | onToggleSelected={toggleThemeRole} |
| 867 | | role={role} |
| 868 | | selected={selectedRole === role} |
| 869 | | value={colorsByAppearance[activeAppearance][role]} |
| 1009 | role={family.role} |
| 1010 | selected={selectedRole === family.role} |
| 1011 | value={colorsByAppearance[activeAppearance][family.role]} |
| 870 | 1012 | /> |
| 871 | 1013 | ))} |
| 872 | 1014 | </div> |
| ... | ... | @@ -876,16 +1018,21 @@ export function ThemeEditorPanel({ |
| 876 | 1018 | const query = roleQuery.trim().toLowerCase(); |
| 877 | 1019 | const groups = THEME_EDITOR_ROLE_GROUPS.map((group) => ({ |
| 878 | 1020 | ...group, |
| 879 | | roles: group.roles.filter( |
| 880 | | (role) => !query || getThemeRoleLabel(role).toLowerCase().includes(query), |
| 1021 | families: group.families.filter( |
| 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 | 1030 | return isAdvanced ? ( |
| 884 | 1031 | <div className="space-y-5"> |
| 885 | 1032 | {groups.map((group) => ( |
| 886 | 1033 | <section className="space-y-2" key={group.id}> |
| 887 | 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 | 1036 | </section> |
| 890 | 1037 | ))} |
| 891 | 1038 | {groups.length === 0 ? <p className="text-xs text-muted-foreground">No matches.</p> : null} |
| ... | ... | @@ -1018,7 +1165,7 @@ export function ThemeEditorPanel({ |
| 1018 | 1165 | {isInspecting |
| 1019 | 1166 | ? "Select an element · Esc to cancel" |
| 1020 | 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 | 1169 | : "Select a color below"} |
| 1023 | 1170 | </p> |
| 1024 | 1171 | )} |