/** * Style Customizer v2 — state store. A tiny external store shared by React (via * useSyncExternalStore) and the preview bridge; every mutation goes through a guarded method * so the preview + dirty state stay in sync. `affected` tells the bridge which keys changed * (`null` = re-apply everything). */ import { useSyncExternalStore } from 'react'; import { clone, deepEqual, mixHex } from './constants'; import { BootstrapSettings, Device, DeviceBag, MigrationInfo, ScalarValue, StylePayload, StyleRecord, Token, } from './types'; interface Snapshot { tokens: Record< string, DeviceBag >; palette: string; customCss: string; template: string; applyThemeStyle: boolean; } interface HistoryEntry { label: string; snap: Snapshot; } const MAX_HISTORY = 60; class StyleStore { // Static config (from the REST payload) — never mutated. settings: BootstrapSettings; schema: Token[]; byKey: Record< string, Token > = {}; sections: StylePayload[ 'sections' ]; palettes: StylePayload[ 'palettes' ]; templates: StylePayload[ 'templates' ]; userTemplates: StylePayload[ 'templates' ]; paletteMap: Record< string, string[] >; breakpoints: Record< string, number >; schemaVersion: number; proActive: boolean; googleFonts: string[]; /** Drives the migration notice; never mutated post-init. */ migration: MigrationInfo; // Editable state. tokens: Record< string, DeviceBag > = {}; device: Device = 'desktop'; palette = ''; customCss = ''; template = ''; applyThemeStyle = true; /** True once {@see setApplyThemeStyle} has been called this session — gates whether `save()` * includes `apply_theme_style` in its POST body at all (see App.tsx's `save`). */ applyThemeStyleTouched = false; baseUpdatedAt = 0; /** Set from outside React by the (legacy jQuery) Fields tab — drives the unsaved-fields notice. */ hasUnsavedFieldChanges = false; /** Bumped on every click inside the preview iframe — lets the AI assistant panel close itself * before a native