114 lines · 4.0 KB · TypeScript
RawDownload
import { useCallback } from "react"; import { useTheme } from "../../hooks/useTheme";import { getThemeDefinition, type ThemeAppearance, type ThemeDefinition } from "../../themePalette";import { stackedThreadToast, toastManager } from "../ui/toast";import { ThemeEditorPanel } from "./ThemeEditorPanel";import { useThemeEditorStore } from "./themeEditorStore"; /** * Renders the theme editor above the router. The editor paints its draft on * the live app, so it has to outlive the settings route: the point is to walk * through threads, panels, and pages while the colors are being tuned. */export function ThemeEditorHost() {  const session = useThemeEditorStore((store) => store.session);  const closeThemeEditor = useThemeEditorStore((store) => store.closeThemeEditor);  const { theme, setTheme, themeHalves, refreshTheme } = useTheme();   // The panel reports which path it actually took: a theme removed while its  // editor is open resolves to null there, so the save becomes a create even  // though the session still names it.  const handleSaved = useCallback(    (      savedTheme: ThemeDefinition,      { created, mergedAppearance }: { created: boolean; mergedAppearance?: ThemeAppearance },    ) => {      // A merge completed an existing theme's light/dark pair; activating the      // whole theme shows the new palette right away.      if (mergedAppearance) {        if (!setTheme(savedTheme.id)) {          toastManager.add(            stackedThreadToast({              type: "error",              title: "Could not save your theme",              description: "Browser storage is unavailable, so the change was not kept.",            }),          );          return false;        }        toastManager.add(          stackedThreadToast({            type: "success",            title: `${savedTheme.label} updated`,            description: `Its ${mergedAppearance} palette was added.`,          }),        );        return true;      }      if (!created) {        // The edited theme may be showing through the base preference or either        // half of the mix; the preference itself is untouched (a setTheme here        // would clear the mix), the palette just needs re-applying.        const wasActive =          getThemeDefinition(theme)?.id === savedTheme.id ||          themeHalves?.light === savedTheme.id ||          themeHalves?.dark === savedTheme.id;        if (wasActive) refreshTheme();        toastManager.add(          stackedThreadToast({            type: "success",            title: `${savedTheme.label} saved`,            description: wasActive ? "Your changes are now active." : "Your changes are saved.",          }),        );        return true;      }       if (!setTheme(savedTheme.id)) {        toastManager.add(          stackedThreadToast({            type: "error",            title: "Could not save your theme",            description: "Browser storage is unavailable, so the change was not kept.",          }),        );        return false;      }      toastManager.add(        stackedThreadToast({          type: "success",          title: `${savedTheme.label} created`,          description: "It’s now active.",        }),      );      return true;    },    [refreshTheme, setTheme, theme, themeHalves],  );   if (!session) return null;   // Resolve on every render: an edit or import can change the stored  // definitions while a session is open.  const editingTheme = session.editingThemeId    ? (getThemeDefinition(session.editingThemeId) ?? null)    : null;  const seedTheme = session.seedThemeId ? (getThemeDefinition(session.seedThemeId) ?? null) : null;   return (    <ThemeEditorPanel      editingTheme={editingTheme}      initialAppearance={session.initialAppearance}      key={session.id}      onOpenChange={(open) => {        if (!open) closeThemeEditor();      }}      onSaved={handleSaved}      open      restoreTheme={refreshTheme}      seedName={session.seedName ?? undefined}      seedTheme={seedTheme}    />  );}