| 1 | import { create } from "zustand"; |
| 2 | |
| 3 | import { |
| 4 | getThemeDefinition, |
| 5 | type ThemeAppearance, |
| 6 | type ThemeHalves, |
| 7 | type ThemePreference, |
| 8 | } from "../../themePalette"; |
| 9 | |
| 10 | /** |
| 11 | * The theme editor lives above the router so a draft survives navigation: the |
| 12 | * palette is painted on the live app, and judging it means browsing the app |
| 13 | * while the editor stays open. Themes are referenced by id because the stored |
| 14 | * definitions change underneath an open session (an edit, an import). |
| 15 | */ |
| 16 | export type ThemeEditorSessionInput = { |
| 17 | /** Set when editing an installed theme; null creates a new one. */ |
| 18 | editingThemeId: string | null; |
| 19 | /** Theme a new theme starts from: the active one, or a duplicate target. */ |
| 20 | seedThemeId: string | null; |
| 21 | /** Prefilled name, used by duplicate. */ |
| 22 | seedName: string | null; |
| 23 | initialAppearance: ThemeAppearance; |
| 24 | }; |
| 25 | |
| 26 | export type ThemeEditorSession = ThemeEditorSessionInput & { |
| 27 | /** |
| 28 | * Distinguishes two sessions that name the same themes, so asking for a |
| 29 | * fresh draft while the panel is already open still reseeds it. |
| 30 | */ |
| 31 | id: number; |
| 32 | }; |
| 33 | |
| 34 | type ThemeEditorStore = { |
| 35 | session: ThemeEditorSession | null; |
| 36 | openThemeEditor: (session: ThemeEditorSessionInput) => void; |
| 37 | closeThemeEditor: () => void; |
| 38 | }; |
| 39 | |
| 40 | let nextSessionId = 0; |
| 41 | |
| 42 | export const useThemeEditorStore = create<ThemeEditorStore>((set) => ({ |
| 43 | session: null, |
| 44 | openThemeEditor: (session) => set({ session: { ...session, id: ++nextSessionId } }), |
| 45 | closeThemeEditor: () => set({ session: null }), |
| 46 | })); |
| 47 | |
| 48 | export function toggleThemeEditorForTheme(input: { |
| 49 | theme: ThemePreference; |
| 50 | themeHalves: ThemeHalves | null; |
| 51 | initialAppearance: ThemeAppearance; |
| 52 | }): void { |
| 53 | const store = useThemeEditorStore.getState(); |
| 54 | if (store.session) { |
| 55 | store.closeThemeEditor(); |
| 56 | return; |
| 57 | } |
| 58 | |
| 59 | const baseThemeId = getThemeDefinition(input.theme)?.id ?? null; |
| 60 | const activeThemeId = input.themeHalves?.[input.initialAppearance] ?? baseThemeId; |
| 61 | const seedThemeId = activeThemeId ? (getThemeDefinition(activeThemeId)?.id ?? null) : null; |
| 62 | store.openThemeEditor({ |
| 63 | editingThemeId: null, |
| 64 | seedThemeId, |
| 65 | seedName: null, |
| 66 | initialAppearance: input.initialAppearance, |
| 67 | }); |
| 68 | } |