| author | |
| committer | |
| log | f0fb406acf419c1ab3d37d5f42093c07921484b2 |
| tree | 03ad6198026af6472aae44c8979793dea36ebe26 |
| parent | 82406bce99c2cf46d1d8fbad26bfefc2ab3169ce |
| signature | Signed by PGP key B5690EEEBB952194 |
5 files changed, 185 insertions(+), 6 deletions(-)
apps/web/src/components/settings/ThemeEditorPanel.tsx+39-2| ... | ... | @@ -165,6 +165,7 @@ export function ThemeEditorPanel({ |
| 165 | 165 | const isEditing = editingTheme !== null; |
| 166 | 166 | const [name, setName] = useState(""); |
| 167 | 167 | const [activeAppearance, setActiveAppearance] = useState<ThemeAppearance>(initialAppearance); |
| 168 | const [sidebarArtwork, setSidebarArtwork] = useState(false); | |
| 168 | 169 | const [isAdvanced, setIsAdvanced] = useState(false); |
| 169 | 170 | const [colorsByAppearance, setColorsByAppearance] = useState<ThemeEditorColorsByAppearance>(() => |
| 170 | 171 | getThemeEditorColorsByAppearance(), |
| ... | ... | @@ -178,6 +179,7 @@ export function ThemeEditorPanel({ |
| 178 | 179 | const [isInspecting, setIsInspecting] = useState(false); |
| 179 | 180 | const [selectedRole, setSelectedRole] = useState<ThemeColorRole | null>(null); |
| 180 | 181 | const [usageCount, setUsageCount] = useState<number | null>(null); |
| 182 | const previousMergeTargetIdRef = useRef<string | null>(null); | |
| 181 | 183 | // Null parks the panel at its default corner; a value is a dragged spot, |
| 182 | 184 | // kept clamped so the header can always be grabbed again. |
| 183 | 185 | const [position, setPosition] = useState<{ x: number; y: number } | null>(null); |
| ... | ... | @@ -259,6 +261,9 @@ export function ThemeEditorPanel({ |
| 259 | 261 | |
| 260 | 262 | setName(editingTheme?.label ?? seedName ?? ""); |
| 261 | 263 | setActiveAppearance(nextAppearance); |
| 264 | // Artwork is opt-in for new themes, including duplicates. Editing keeps | |
| 265 | // the theme's existing choice. | |
| 266 | setSidebarArtwork(editingTheme?.sidebarArtwork === true); | |
| 262 | 267 | // Themes saved by the guided editor carry the managed flag; anything |
| 263 | 268 | // else (imports, hand-edited files, older saves) opens in advanced mode |
| 264 | 269 | // so guided regeneration cannot silently discard hand-tuned colors. A |
| ... | ... | @@ -315,6 +320,18 @@ export function ThemeEditorPanel({ |
| 315 | 320 | // an explanation instead. |
| 316 | 321 | const mergeTargetId = mergeTarget?.id ?? null; |
| 317 | 322 | const takenAppearancesKey = takenAppearances.join(","); |
| 323 | useEffect(() => { | |
| 324 | if (previousMergeTargetIdRef.current === mergeTargetId) return; | |
| 325 | previousMergeTargetIdRef.current = mergeTargetId; | |
| 326 | // A matching name makes that existing theme the surviving merge target. | |
| 327 | // Seed theme-level options from it so adding a palette or renaming onto it | |
| 328 | // does not silently reset them. Leaving the merge restores the edited | |
| 329 | // theme's option (or the off-by-default choice for a new theme). | |
| 330 | setSidebarArtwork( | |
| 331 | mergeTarget ? mergeTarget.sidebarArtwork === true : editingTheme?.sidebarArtwork === true, | |
| 332 | ); | |
| 333 | }, [editingTheme, mergeTarget, mergeTargetId]); | |
| 334 | ||
| 318 | 335 | useEffect(() => { |
| 319 | 336 | if (isEditing || mergeTargetId === null) return; |
| 320 | 337 | const taken = takenAppearancesKey.split(",").filter(Boolean) as ThemeAppearance[]; |
| ... | ... | @@ -330,8 +347,8 @@ export function ThemeEditorPanel({ |
| 330 | 347 | // comes back when the editor closes, including on cancel. |
| 331 | 348 | useEffect(() => { |
| 332 | 349 | if (!open || !isDraftSeeded) return; |
| 333 | applyThemeColorPreview(colorsByAppearance[activeAppearance], activeAppearance); | |
| 334 | }, [activeAppearance, colorsByAppearance, isDraftSeeded, open]); | |
| 350 | applyThemeColorPreview(colorsByAppearance[activeAppearance], activeAppearance, sidebarArtwork); | |
| 351 | }, [activeAppearance, colorsByAppearance, isDraftSeeded, open, sidebarArtwork]); | |
| 335 | 352 | |
| 336 | 353 | useEffect(() => { |
| 337 | 354 | if (!open) return; |
| ... | ... | @@ -657,6 +674,7 @@ export function ThemeEditorPanel({ |
| 657 | 674 | ...mergeTarget.variants, |
| 658 | 675 | ...Object.fromEntries(editedModes.map((mode) => [mode, colorsForSave[mode]])), |
| 659 | 676 | }, |
| 677 | ...(sidebarArtwork ? { sidebarArtwork: true } : {}), | |
| 660 | 678 | ...(mergeTarget.managed === true && !isAdvanced ? { managed: true } : {}), |
| 661 | 679 | }), |
| 662 | 680 | ); |
| ... | ... | @@ -687,6 +705,7 @@ export function ThemeEditorPanel({ |
| 687 | 705 | ...(getThemeModes(editingTheme).length > 1 |
| 688 | 706 | ? { variants: { [variantAppearance]: colorsForSave[variantAppearance] } } |
| 689 | 707 | : {}), |
| 708 | ...(sidebarArtwork ? { sidebarArtwork: true } : {}), | |
| 690 | 709 | ...(isAdvanced ? {} : { managed: true }), |
| 691 | 710 | }), |
| 692 | 711 | ); |
| ... | ... | @@ -713,6 +732,7 @@ export function ThemeEditorPanel({ |
| 713 | 732 | ...mergeTarget.variants, |
| 714 | 733 | [activeAppearance]: colorsForSave[activeAppearance], |
| 715 | 734 | }, |
| 735 | ...(sidebarArtwork ? { sidebarArtwork: true } : {}), | |
| 716 | 736 | ...(mergeTarget.managed === true && !isAdvanced ? { managed: true } : {}), |
| 717 | 737 | }), |
| 718 | 738 | ); |
| ... | ... | @@ -723,6 +743,7 @@ export function ThemeEditorPanel({ |
| 723 | 743 | name, |
| 724 | 744 | appearance: activeAppearance, |
| 725 | 745 | colors: colorsForSave[activeAppearance], |
| 746 | ...(sidebarArtwork ? { sidebarArtwork: true } : {}), | |
| 726 | 747 | ...(isAdvanced ? {} : { managed: true }), |
| 727 | 748 | }), |
| 728 | 749 | ); |
| ... | ... | @@ -773,6 +794,7 @@ export function ThemeEditorPanel({ |
| 773 | 794 | name, |
| 774 | 795 | onOpenChange, |
| 775 | 796 | onSaved, |
| 797 | sidebarArtwork, | |
| 776 | 798 | simpleColorsDirtyByAppearance, |
| 777 | 799 | takenAppearances, |
| 778 | 800 | ]); |
| ... | ... | @@ -832,6 +854,20 @@ export function ThemeEditorPanel({ |
| 832 | 854 | </div> |
| 833 | 855 | ); |
| 834 | 856 | |
| 857 | const renderSidebarArtworkToggle = () => ( | |
| 858 | <label className="grid cursor-pointer grid-cols-[minmax(0,1fr)_minmax(0,2fr)] items-center gap-3"> | |
| 859 | <span className="text-sm font-medium">Sidebar artwork</span> | |
| 860 | <span className="flex items-center justify-between gap-3 text-xs text-muted-foreground"> | |
| 861 | <span>Show T3 Code environment artwork</span> | |
| 862 | <Switch | |
| 863 | aria-label="Allow sidebar artwork with this theme" | |
| 864 | checked={sidebarArtwork} | |
| 865 | onCheckedChange={(checked) => setSidebarArtwork(Boolean(checked))} | |
| 866 | /> | |
| 867 | </span> | |
| 868 | </label> | |
| 869 | ); | |
| 870 | ||
| 835 | 871 | const renderColorsHeader = () => ( |
| 836 | 872 | <div className="grid grid-cols-[minmax(0,1fr)_minmax(0,2fr)] items-start gap-3"> |
| 837 | 873 | <div> |
| ... | ... | @@ -1088,6 +1124,7 @@ export function ThemeEditorPanel({ |
| 1088 | 1124 | </p> |
| 1089 | 1125 | ) : null} |
| 1090 | 1126 | {renderAppearanceButtons()} |
| 1127 | {renderSidebarArtworkToggle()} | |
| 1091 | 1128 | <div className="space-y-3"> |
| 1092 | 1129 | {renderColorsHeader()} |
| 1093 | 1130 | {renderColorFields()} |
apps/web/src/hooks/useSettings.test.ts+31| ... | ... | @@ -17,6 +17,37 @@ describe("resolveEnvironmentIdentificationMode", () => { |
| 17 | 17 | "pill", |
| 18 | 18 | ); |
| 19 | 19 | }); |
| 20 | ||
| 21 | it("uses a pill instead of artwork with a palette theme", () => { | |
| 22 | expect( | |
| 23 | resolveEnvironmentIdentificationMode({ | |
| 24 | mode: "artwork", | |
| 25 | settingsHydrated: true, | |
| 26 | paletteThemeActive: true, | |
| 27 | }), | |
| 28 | ).toBe("pill"); | |
| 29 | }); | |
| 30 | ||
| 31 | it("respects none with a palette theme", () => { | |
| 32 | expect( | |
| 33 | resolveEnvironmentIdentificationMode({ | |
| 34 | mode: "none", | |
| 35 | settingsHydrated: true, | |
| 36 | paletteThemeActive: true, | |
| 37 | }), | |
| 38 | ).toBe("none"); | |
| 39 | }); | |
| 40 | ||
| 41 | it("keeps artwork when the palette theme opts into it", () => { | |
| 42 | expect( | |
| 43 | resolveEnvironmentIdentificationMode({ | |
| 44 | mode: "artwork", | |
| 45 | settingsHydrated: true, | |
| 46 | paletteThemeActive: true, | |
| 47 | paletteThemeAllowsArtwork: true, | |
| 48 | }), | |
| 49 | ).toBe("artwork"); | |
| 50 | }); | |
| 20 | 51 | }); |
| 21 | 52 | |
| 22 | 53 | describe("mergeEnvironmentSettings", () => { |
apps/web/src/hooks/useSettings.ts+30-2| ... | ... | @@ -28,10 +28,17 @@ import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors"; |
| 28 | 28 | import { APP_STAGE_LABEL } from "~/branding"; |
| 29 | 29 | import { resolveSidebarV2Enabled } from "~/branding.logic"; |
| 30 | 30 | import { ensureLocalApi } from "~/localApi"; |
| 31 | import { | |
| 32 | getThemeDefinition, | |
| 33 | getThemePreviewSidebarArtwork, | |
| 34 | resolveThemeHalf, | |
| 35 | subscribeToThemePreview, | |
| 36 | } from "~/themePalette"; | |
| 31 | 37 | import * as Struct from "effect/Struct"; |
| 32 | 38 | import { primaryServerSettingsAtom, serverEnvironment } from "~/state/server"; |
| 33 | 39 | import { usePrimaryEnvironment } from "~/state/environments"; |
| 34 | 40 | import { useAtomCommand } from "~/state/use-atom-command"; |
| 41 | import { useTheme } from "./useTheme"; | |
| 35 | 42 | |
| 36 | 43 | const CLIENT_SETTINGS_PERSISTENCE_ERROR_SCOPE = "[CLIENT_SETTINGS]"; |
| 37 | 44 | |
| ... | ... | @@ -226,15 +233,36 @@ export function useClientSettings<T = ClientSettings>( |
| 226 | 233 | export function resolveEnvironmentIdentificationMode(input: { |
| 227 | 234 | mode: EnvironmentIdentificationMode; |
| 228 | 235 | settingsHydrated: boolean; |
| 236 | paletteThemeActive?: boolean; | |
| 237 | paletteThemeAllowsArtwork?: boolean; | |
| 229 | 238 | }): EnvironmentIdentificationMode { |
| 230 | 239 | // Avoid briefly rendering the default artwork before a persisted pill/none choice loads. |
| 231 | return input.settingsHydrated ? input.mode : "none"; | |
| 240 | if (!input.settingsHydrated) return "none"; | |
| 241 | // Stage artwork has fixed colors that can clash with palette themes. Keep an | |
| 242 | // explicit "none", but use the theme-aware pill in place of artwork. | |
| 243 | return input.paletteThemeActive && !input.paletteThemeAllowsArtwork && input.mode === "artwork" | |
| 244 | ? "pill" | |
| 245 | : input.mode; | |
| 232 | 246 | } |
| 233 | 247 | |
| 234 | 248 | export function useEnvironmentIdentificationMode(): EnvironmentIdentificationMode { |
| 235 | 249 | const settingsHydrated = useClientSettingsHydrated(); |
| 236 | 250 | const mode = useClientSettingsValue().environmentIdentificationMode; |
| 237 | return resolveEnvironmentIdentificationMode({ mode, settingsHydrated }); | |
| 251 | const { resolvedTheme, theme, themeHalves } = useTheme(); | |
| 252 | const previewSidebarArtwork = useSyncExternalStore( | |
| 253 | subscribeToThemePreview, | |
| 254 | getThemePreviewSidebarArtwork, | |
| 255 | () => null, | |
| 256 | ); | |
| 257 | const activeTheme = resolveThemeHalf(theme, themeHalves, resolvedTheme); | |
| 258 | const activeThemeDefinition = getThemeDefinition(activeTheme); | |
| 259 | return resolveEnvironmentIdentificationMode({ | |
| 260 | mode, | |
| 261 | settingsHydrated, | |
| 262 | paletteThemeActive: previewSidebarArtwork !== null || activeThemeDefinition !== null, | |
| 263 | paletteThemeAllowsArtwork: | |
| 264 | previewSidebarArtwork ?? activeThemeDefinition?.sidebarArtwork === true, | |
| 265 | }); | |
| 238 | 266 | } |
| 239 | 267 | |
| 240 | 268 | /** |
apps/web/src/themePalette.test.ts+55-1| ... | ... | @@ -1,9 +1,12 @@ |
| 1 | 1 | import { describe, expect, it, vi } from "vite-plus/test"; |
| 2 | 2 | |
| 3 | 3 | import { |
| 4 | applyThemeColorPreview, | |
| 5 | applyThemePalette, | |
| 4 | 6 | getThemeColorsForMode, |
| 5 | 7 | getThemeDefinition, |
| 6 | 8 | getThemeModes, |
| 9 | getThemePreviewSidebarArtwork, | |
| 7 | 10 | getThemePreferenceMode, |
| 8 | 11 | isKnownThemePreference, |
| 9 | 12 | getCustomThemes, |
| ... | ... | @@ -15,6 +18,7 @@ import { |
| 15 | 18 | resolveDesktopTheme, |
| 16 | 19 | resolveThemeAppearance, |
| 17 | 20 | serializeThemeFile, |
| 21 | subscribeToThemePreview, | |
| 18 | 22 | subscribeToCustomThemes, |
| 19 | 23 | T3_CHAT_THEME, |
| 20 | 24 | EMBER_THEME, |
| ... | ... | @@ -200,6 +204,49 @@ describe("theme files", () => { |
| 200 | 204 | }); |
| 201 | 205 | }); |
| 202 | 206 | |
| 207 | it("keeps sidebar artwork opt-in through theme files", () => { | |
| 208 | const withoutArtwork = parseThemeFile({ | |
| 209 | version: THEME_FILE_VERSION, | |
| 210 | name: "Plain sidebar", | |
| 211 | appearance: "light", | |
| 212 | colors: { accent: "#5b6cff" }, | |
| 213 | }); | |
| 214 | const withArtwork = parseThemeFile({ | |
| 215 | version: THEME_FILE_VERSION, | |
| 216 | name: "Art sidebar", | |
| 217 | appearance: "light", | |
| 218 | colors: { accent: "#5b6cff" }, | |
| 219 | sidebarArtwork: true, | |
| 220 | }); | |
| 221 | ||
| 222 | expect(withoutArtwork.sidebarArtwork).toBeUndefined(); | |
| 223 | expect(withArtwork.sidebarArtwork).toBe(true); | |
| 224 | expect(JSON.parse(serializeThemeFile(withArtwork)).sidebarArtwork).toBe(true); | |
| 225 | }); | |
| 226 | ||
| 227 | it("publishes sidebar artwork changes from the live theme preview", () => { | |
| 228 | const listener = vi.fn(); | |
| 229 | const unsubscribe = subscribeToThemePreview(listener); | |
| 230 | vi.stubGlobal("document", { | |
| 231 | documentElement: { | |
| 232 | classList: { toggle: vi.fn() }, | |
| 233 | dataset: {}, | |
| 234 | style: { removeProperty: vi.fn(), setProperty: vi.fn() }, | |
| 235 | }, | |
| 236 | }); | |
| 237 | ||
| 238 | applyThemeColorPreview(T3_CHAT_THEME.colors, "light", true); | |
| 239 | expect(getThemePreviewSidebarArtwork()).toBe(true); | |
| 240 | expect(listener).toHaveBeenCalledTimes(1); | |
| 241 | ||
| 242 | applyThemePalette("system"); | |
| 243 | expect(getThemePreviewSidebarArtwork()).toBeNull(); | |
| 244 | expect(listener).toHaveBeenCalledTimes(2); | |
| 245 | ||
| 246 | unsubscribe(); | |
| 247 | vi.unstubAllGlobals(); | |
| 248 | }); | |
| 249 | ||
| 203 | 250 | it("keeps optional light and dark palettes under one theme id", () => { |
| 204 | 251 | const theme = parseThemeFile({ |
| 205 | 252 | version: THEME_FILE_VERSION, |
| ... | ... | @@ -387,6 +434,7 @@ describe("theme files", () => { |
| 387 | 434 | name: "Aurora", |
| 388 | 435 | appearance: "light", |
| 389 | 436 | colors: { canvas: "#f8fbff", accent: "#5b6cff" }, |
| 437 | sidebarArtwork: true, | |
| 390 | 438 | }), |
| 391 | 439 | ); |
| 392 | 440 | const updatedTheme = updateCustomTheme({ |
| ... | ... | @@ -395,11 +443,17 @@ describe("theme files", () => { |
| 395 | 443 | colors: { ...createdTheme.colors, accent: "#7c3aed" }, |
| 396 | 444 | }); |
| 397 | 445 | |
| 398 | expect(updatedTheme).toMatchObject({ id: "aurora", label: "Aurora Night" }); | |
| 446 | expect(updatedTheme).toMatchObject({ | |
| 447 | id: "aurora", | |
| 448 | label: "Aurora Night", | |
| 449 | sidebarArtwork: true, | |
| 450 | }); | |
| 451 | invalidateCustomThemes(); | |
| 399 | 452 | expect(getCustomThemes()).toEqual([updatedTheme]); |
| 400 | 453 | expect(JSON.parse(stored.get(CUSTOM_THEMES_STORAGE_KEY) ?? "[]")[0]).toMatchObject({ |
| 401 | 454 | id: "aurora", |
| 402 | 455 | label: "Aurora Night", |
| 456 | sidebarArtwork: true, | |
| 403 | 457 | }); |
| 404 | 458 | |
| 405 | 459 | vi.unstubAllGlobals(); |
apps/web/src/themePalette.ts+30-1| ... | ... | @@ -96,6 +96,8 @@ export type ThemeDefinition = Readonly<{ |
| 96 | 96 | appearance: ThemeAppearance; |
| 97 | 97 | colors: ThemeColors; |
| 98 | 98 | variants?: ThemeVariants; |
| 99 | /** Allows fixed Dev/Nightly artwork to render over this theme's sidebar. */ | |
| 100 | sidebarArtwork?: boolean; | |
| 99 | 101 | /** True when the palette was generated by the guided editor from its |
| 100 | 102 | * canvas and accent; such themes reopen in guided mode. */ |
| 101 | 103 | managed?: boolean; |
| ... | ... | @@ -107,6 +109,7 @@ export type ThemeFile = Readonly<{ |
| 107 | 109 | appearance: ThemeAppearance; |
| 108 | 110 | colors: ThemeColorOverrides; |
| 109 | 111 | variants?: ThemeVariantOverrides; |
| 112 | sidebarArtwork?: boolean; | |
| 110 | 113 | managed?: boolean; |
| 111 | 114 | }>; |
| 112 | 115 | |
| ... | ... | @@ -128,6 +131,23 @@ const RESERVED_THEME_IDS = new Set([ |
| 128 | 131 | |
| 129 | 132 | const customThemeListeners = new Set<() => void>(); |
| 130 | 133 | let customThemesSnapshot: ReadonlyArray<ThemeDefinition> | null = null; |
| 134 | const themePreviewListeners = new Set<() => void>(); | |
| 135 | let themePreviewSidebarArtwork: boolean | null = null; | |
| 136 | ||
| 137 | export function getThemePreviewSidebarArtwork(): boolean | null { | |
| 138 | return themePreviewSidebarArtwork; | |
| 139 | } | |
| 140 | ||
| 141 | export function subscribeToThemePreview(listener: () => void): () => void { | |
| 142 | themePreviewListeners.add(listener); | |
| 143 | return () => themePreviewListeners.delete(listener); | |
| 144 | } | |
| 145 | ||
| 146 | function setThemePreviewSidebarArtwork(next: boolean | null): void { | |
| 147 | if (themePreviewSidebarArtwork === next) return; | |
| 148 | themePreviewSidebarArtwork = next; | |
| 149 | for (const listener of themePreviewListeners) listener(); | |
| 150 | } | |
| 131 | 151 | |
| 132 | 152 | function isRecord(value: unknown): value is Record<string, unknown> { |
| 133 | 153 | return typeof value === "object" && value !== null && !Array.isArray(value); |
| ... | ... | @@ -203,6 +223,7 @@ function parseStoredTheme(value: unknown): ThemeDefinition | null { |
| 203 | 223 | appearance: value.appearance, |
| 204 | 224 | colors, |
| 205 | 225 | ...(variants ? { variants } : {}), |
| 226 | ...(value.sidebarArtwork === true ? { sidebarArtwork: true } : {}), | |
| 206 | 227 | ...(value.managed === true ? { managed: true } : {}), |
| 207 | 228 | }; |
| 208 | 229 | } |
| ... | ... | @@ -1541,6 +1562,7 @@ export function parseThemeFile(value: unknown): ThemeDefinition { |
| 1541 | 1562 | appearance, |
| 1542 | 1563 | colors: { ...fallback, ...overrides }, |
| 1543 | 1564 | ...(Object.keys(variants).length > 0 ? { variants } : {}), |
| 1565 | ...(value.sidebarArtwork === true ? { sidebarArtwork: true } : {}), | |
| 1544 | 1566 | ...(value.managed === true ? { managed: true } : {}), |
| 1545 | 1567 | }; |
| 1546 | 1568 | } |
| ... | ... | @@ -1553,6 +1575,7 @@ export function serializeThemeFile(theme: ThemeDefinition): string { |
| 1553 | 1575 | appearance: theme.appearance, |
| 1554 | 1576 | colors: theme.colors, |
| 1555 | 1577 | ...(theme.variants ? { variants: theme.variants } : {}), |
| 1578 | ...(theme.sidebarArtwork ? { sidebarArtwork: true } : {}), | |
| 1556 | 1579 | ...(theme.managed ? { managed: true } : {}), |
| 1557 | 1580 | }; |
| 1558 | 1581 | return `${JSON.stringify(file, null, 2)}\n`; |
| ... | ... | @@ -1630,11 +1653,16 @@ export const THEME_PREVIEW_ID = "__preview"; |
| 1630 | 1653 | * can be judged against the real interface instead of a miniature. Callers |
| 1631 | 1654 | * restore the stored theme (refreshTheme) when the draft goes away. |
| 1632 | 1655 | */ |
| 1633 | export function applyThemeColorPreview(colors: ThemeColors, appearance: ThemeAppearance): void { | |
| 1656 | export function applyThemeColorPreview( | |
| 1657 | colors: ThemeColors, | |
| 1658 | appearance: ThemeAppearance, | |
| 1659 | sidebarArtwork = false, | |
| 1660 | ): void { | |
| 1634 | 1661 | if (typeof document === "undefined") return; |
| 1635 | 1662 | const root = document.documentElement; |
| 1636 | 1663 | if (!root?.style) return; |
| 1637 | 1664 | |
| 1665 | setThemePreviewSidebarArtwork(sidebarArtwork); | |
| 1638 | 1666 | root.dataset.themeId = THEME_PREVIEW_ID; |
| 1639 | 1667 | root.classList.toggle("dark", appearance === "dark"); |
| 1640 | 1668 | for (const [role, value] of Object.entries(colors) as Array<[ThemeColorRole, string]>) { |
| ... | ... | @@ -1649,6 +1677,7 @@ export function applyThemePalette(theme: ThemePreference, appearance?: ThemeAppe |
| 1649 | 1677 | const root = document.documentElement; |
| 1650 | 1678 | if (!root?.style) return; |
| 1651 | 1679 | |
| 1680 | setThemePreviewSidebarArtwork(null); | |
| 1652 | 1681 | const palette = getThemeDefinition(theme); |
| 1653 | 1682 | |
| 1654 | 1683 | if (palette) { |