@newtonedev/editor 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/Editor.d.ts +3 -0
  2. package/dist/Editor.d.ts.map +1 -0
  3. package/dist/components/CodeBlock.d.ts +7 -0
  4. package/dist/components/CodeBlock.d.ts.map +1 -0
  5. package/dist/components/EditorHeader.d.ts +16 -0
  6. package/dist/components/EditorHeader.d.ts.map +1 -0
  7. package/dist/components/EditorShell.d.ts +10 -0
  8. package/dist/components/EditorShell.d.ts.map +1 -0
  9. package/dist/components/FontPicker.d.ts +11 -0
  10. package/dist/components/FontPicker.d.ts.map +1 -0
  11. package/dist/components/PresetSelector.d.ts +14 -0
  12. package/dist/components/PresetSelector.d.ts.map +1 -0
  13. package/dist/components/PreviewWindow.d.ts +11 -0
  14. package/dist/components/PreviewWindow.d.ts.map +1 -0
  15. package/dist/components/RightSidebar.d.ts +12 -0
  16. package/dist/components/RightSidebar.d.ts.map +1 -0
  17. package/dist/components/Sidebar.d.ts +25 -0
  18. package/dist/components/Sidebar.d.ts.map +1 -0
  19. package/dist/components/TableOfContents.d.ts +9 -0
  20. package/dist/components/TableOfContents.d.ts.map +1 -0
  21. package/dist/components/ThemeBar.d.ts +8 -0
  22. package/dist/components/ThemeBar.d.ts.map +1 -0
  23. package/dist/components/sections/ColorsSection.d.ts +14 -0
  24. package/dist/components/sections/ColorsSection.d.ts.map +1 -0
  25. package/dist/components/sections/DynamicRangeSection.d.ts +9 -0
  26. package/dist/components/sections/DynamicRangeSection.d.ts.map +1 -0
  27. package/dist/components/sections/FontsSection.d.ts +9 -0
  28. package/dist/components/sections/FontsSection.d.ts.map +1 -0
  29. package/dist/components/sections/IconsSection.d.ts +9 -0
  30. package/dist/components/sections/IconsSection.d.ts.map +1 -0
  31. package/dist/components/sections/OthersSection.d.ts +9 -0
  32. package/dist/components/sections/OthersSection.d.ts.map +1 -0
  33. package/dist/components/sections/index.d.ts +6 -0
  34. package/dist/components/sections/index.d.ts.map +1 -0
  35. package/dist/hooks/useEditorState.d.ts +53 -0
  36. package/dist/hooks/useEditorState.d.ts.map +1 -0
  37. package/dist/hooks/useHover.d.ts +8 -0
  38. package/dist/hooks/useHover.d.ts.map +1 -0
  39. package/dist/hooks/usePresets.d.ts +33 -0
  40. package/dist/hooks/usePresets.d.ts.map +1 -0
  41. package/dist/index.cjs +3846 -0
  42. package/dist/index.cjs.map +1 -0
  43. package/dist/index.d.ts +22 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +3819 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/preview/CategoryView.d.ts +7 -0
  48. package/dist/preview/CategoryView.d.ts.map +1 -0
  49. package/dist/preview/ComponentDetailView.d.ts +9 -0
  50. package/dist/preview/ComponentDetailView.d.ts.map +1 -0
  51. package/dist/preview/ComponentRenderer.d.ts +7 -0
  52. package/dist/preview/ComponentRenderer.d.ts.map +1 -0
  53. package/dist/preview/OverviewView.d.ts +7 -0
  54. package/dist/preview/OverviewView.d.ts.map +1 -0
  55. package/dist/types.d.ts +69 -0
  56. package/dist/types.d.ts.map +1 -0
  57. package/dist/utils/presets.d.ts +5 -0
  58. package/dist/utils/presets.d.ts.map +1 -0
  59. package/package.json +51 -0
  60. package/src/Editor.tsx +128 -0
  61. package/src/components/CodeBlock.tsx +58 -0
  62. package/src/components/EditorHeader.tsx +86 -0
  63. package/src/components/EditorShell.tsx +67 -0
  64. package/src/components/FontPicker.tsx +351 -0
  65. package/src/components/PresetSelector.tsx +455 -0
  66. package/src/components/PreviewWindow.tsx +69 -0
  67. package/src/components/RightSidebar.tsx +374 -0
  68. package/src/components/Sidebar.tsx +332 -0
  69. package/src/components/TableOfContents.tsx +152 -0
  70. package/src/components/ThemeBar.tsx +76 -0
  71. package/src/components/sections/ColorsSection.tsx +485 -0
  72. package/src/components/sections/DynamicRangeSection.tsx +399 -0
  73. package/src/components/sections/FontsSection.tsx +132 -0
  74. package/src/components/sections/IconsSection.tsx +66 -0
  75. package/src/components/sections/OthersSection.tsx +70 -0
  76. package/src/components/sections/index.ts +5 -0
  77. package/src/hooks/useEditorState.ts +381 -0
  78. package/src/hooks/useHover.ts +8 -0
  79. package/src/hooks/usePresets.ts +254 -0
  80. package/src/index.ts +52 -0
  81. package/src/preview/CategoryView.tsx +134 -0
  82. package/src/preview/ComponentDetailView.tsx +126 -0
  83. package/src/preview/ComponentRenderer.tsx +107 -0
  84. package/src/preview/OverviewView.tsx +177 -0
  85. package/src/types.ts +77 -0
  86. package/src/utils/presets.ts +24 -0
@@ -0,0 +1,381 @@
1
+ import { useState, useCallback, useRef, useEffect, useMemo } from "react";
2
+ import { getComponent } from "@newtonedev/components";
3
+ import type { ColorMode } from "@newtonedev/components";
4
+ import type { ConfiguratorState } from "@newtonedev/configurator";
5
+ import { useConfigurator, usePreviewColors } from "@newtonedev/configurator";
6
+ import { usePresets } from "./usePresets";
7
+ import type {
8
+ Preset,
9
+ SaveStatus,
10
+ ThemeName,
11
+ PreviewView,
12
+ SidebarSelection,
13
+ EditorPersistence,
14
+ } from "../types";
15
+
16
+ interface UseEditorStateOptions {
17
+ readonly initialState: ConfiguratorState;
18
+ readonly initialIsPublished: boolean;
19
+ readonly initialPresets: readonly Preset[];
20
+ readonly initialActivePresetId: string;
21
+ readonly initialPublishedPresetId: string | null;
22
+ readonly defaultState: ConfiguratorState;
23
+ readonly persistence: EditorPersistence;
24
+ readonly onNavigate?: (view: PreviewView) => void;
25
+ readonly initialPreviewView?: PreviewView;
26
+ }
27
+
28
+ export function useEditorState({
29
+ initialState,
30
+ initialIsPublished,
31
+ initialPresets,
32
+ initialActivePresetId,
33
+ initialPublishedPresetId,
34
+ defaultState,
35
+ persistence,
36
+ onNavigate,
37
+ initialPreviewView,
38
+ }: UseEditorStateOptions) {
39
+ // --- Configurator state management ---
40
+ const {
41
+ state: configuratorState,
42
+ dispatch,
43
+ themeConfig,
44
+ } = useConfigurator(initialState);
45
+ const previewColors = usePreviewColors(configuratorState);
46
+
47
+ const [saveStatus, setSaveStatus] = useState<SaveStatus>("saved");
48
+ const [isPublished, setIsPublished] = useState(initialIsPublished);
49
+ const [publishing, setPublishing] = useState(false);
50
+ const [colorMode, setColorMode] = useState<ColorMode>(
51
+ initialState.preview.mode,
52
+ );
53
+ const [activeTheme, setActiveTheme] = useState<ThemeName>(
54
+ (initialState.preview.theme as ThemeName) || "neutral",
55
+ );
56
+ const [previewView, setPreviewView] = useState<PreviewView>(
57
+ initialPreviewView ?? { kind: "overview" },
58
+ );
59
+ const [sidebarSelection, setSidebarSelection] =
60
+ useState<SidebarSelection>(null);
61
+ const [propOverrides, setPropOverrides] = useState<Record<string, unknown>>(
62
+ {},
63
+ );
64
+
65
+ const debounceRef = useRef<ReturnType<typeof setTimeout>>(undefined);
66
+ const latestStateRef = useRef<ConfiguratorState>(initialState);
67
+ const isInitialMount = useRef(true);
68
+ const initialStateRef = useRef(initialState);
69
+
70
+ // --- Preset callbacks ---
71
+
72
+ const handlePresetSwitch = useCallback(
73
+ (newState: ConfiguratorState) => {
74
+ dispatch({ type: "LOAD_STATE", state: newState });
75
+ initialStateRef.current = newState;
76
+ isInitialMount.current = true;
77
+ },
78
+ [dispatch],
79
+ );
80
+
81
+ const flushPendingSave = useCallback(async () => {
82
+ if (debounceRef.current) {
83
+ clearTimeout(debounceRef.current);
84
+ debounceRef.current = undefined;
85
+ }
86
+ }, []);
87
+
88
+ // --- Presets ---
89
+
90
+ const {
91
+ presets,
92
+ activePresetId,
93
+ publishedPresetId,
94
+ activePreset,
95
+ switchPreset,
96
+ createPreset,
97
+ renamePreset,
98
+ deletePreset,
99
+ duplicatePreset,
100
+ updateActivePresetDraftState,
101
+ publishActivePreset,
102
+ revertActivePreset,
103
+ } = usePresets({
104
+ initialPresets,
105
+ initialActivePresetId,
106
+ initialPublishedPresetId,
107
+ defaultState,
108
+ onPresetSwitch: handlePresetSwitch,
109
+ getCurrentState: () => latestStateRef.current,
110
+ flushPendingSave,
111
+ persistPresets: persistence.persistPresets,
112
+ });
113
+
114
+ // --- Dirty check for revert ---
115
+
116
+ const isDirty = useMemo(
117
+ () =>
118
+ JSON.stringify(configuratorState) !==
119
+ JSON.stringify(
120
+ activePreset.published_state ?? initialStateRef.current,
121
+ ),
122
+ [configuratorState, activePreset.published_state],
123
+ );
124
+
125
+ const handleRevert = useCallback(() => {
126
+ const publishedState = revertActivePreset();
127
+ const revertTarget = publishedState ?? defaultState;
128
+ if (
129
+ window.confirm(
130
+ "Revert all changes to the last published version of this preset?",
131
+ )
132
+ ) {
133
+ dispatch({ type: "LOAD_STATE", state: revertTarget });
134
+ initialStateRef.current = revertTarget;
135
+ }
136
+ }, [dispatch, revertActivePreset, defaultState]);
137
+
138
+ // --- Prop override helpers ---
139
+
140
+ const initOverridesFromVariant = useCallback(
141
+ (componentId: string, variantId?: string) => {
142
+ const comp = getComponent(componentId);
143
+ if (!comp) return;
144
+ const variant = variantId
145
+ ? comp.variants.find((v) => v.id === variantId)
146
+ : comp.variants[0];
147
+ if (variant) {
148
+ const overrides: Record<string, unknown> = {};
149
+ for (const prop of comp.editableProps) {
150
+ overrides[prop.name] = variant.props[prop.name] ?? prop.defaultValue;
151
+ }
152
+ setPropOverrides(overrides);
153
+ }
154
+ },
155
+ [],
156
+ );
157
+
158
+ // --- Preview navigation + right sidebar coordination ---
159
+
160
+ const handlePreviewNavigate = useCallback(
161
+ (view: PreviewView) => {
162
+ setPreviewView(view);
163
+ onNavigate?.(view);
164
+
165
+ if (view.kind === "component") {
166
+ setSidebarSelection({
167
+ scope: "component",
168
+ componentId: view.componentId,
169
+ });
170
+ initOverridesFromVariant(view.componentId);
171
+ } else {
172
+ setSidebarSelection(null);
173
+ setPropOverrides({});
174
+ }
175
+ },
176
+ [onNavigate, initOverridesFromVariant],
177
+ );
178
+
179
+ const handleSelectVariant = useCallback(
180
+ (variantId: string) => {
181
+ if (previewView.kind === "component") {
182
+ setSidebarSelection({
183
+ scope: "variant",
184
+ componentId: previewView.componentId,
185
+ variantId,
186
+ });
187
+ initOverridesFromVariant(previewView.componentId, variantId);
188
+ }
189
+ },
190
+ [previewView, initOverridesFromVariant],
191
+ );
192
+
193
+ const handleCloseSidebar = useCallback(() => {
194
+ setSidebarSelection(null);
195
+ setPropOverrides({});
196
+ }, []);
197
+
198
+ const handleScopeToComponent = useCallback(() => {
199
+ if (sidebarSelection && sidebarSelection.scope === "variant") {
200
+ setSidebarSelection({
201
+ scope: "component",
202
+ componentId: sidebarSelection.componentId,
203
+ });
204
+ initOverridesFromVariant(sidebarSelection.componentId);
205
+ }
206
+ }, [sidebarSelection, initOverridesFromVariant]);
207
+
208
+ const handlePropOverride = useCallback((propName: string, value: unknown) => {
209
+ setPropOverrides((prev) => ({ ...prev, [propName]: value }));
210
+ }, []);
211
+
212
+ const handleResetOverrides = useCallback(() => {
213
+ if (sidebarSelection?.scope === "variant") {
214
+ initOverridesFromVariant(
215
+ sidebarSelection.componentId,
216
+ sidebarSelection.variantId,
217
+ );
218
+ }
219
+ }, [sidebarSelection, initOverridesFromVariant]);
220
+
221
+ // --- Draft auto-save ---
222
+
223
+ const saveDraft = useCallback(
224
+ async (state: ConfiguratorState) => {
225
+ setSaveStatus("saving");
226
+ const updatedPresets = updateActivePresetDraftState(state);
227
+ const { error } = await persistence.onSaveDraft({
228
+ state,
229
+ presets: updatedPresets,
230
+ });
231
+ setSaveStatus(error ? "error" : "saved");
232
+ },
233
+ [persistence, updateActivePresetDraftState],
234
+ );
235
+
236
+ const scheduleSave = useCallback(() => {
237
+ setSaveStatus("unsaved");
238
+ if (debounceRef.current) clearTimeout(debounceRef.current);
239
+ debounceRef.current = setTimeout(() => {
240
+ saveDraft(latestStateRef.current);
241
+ }, 2000);
242
+ }, [saveDraft]);
243
+
244
+ // --- State change detection ---
245
+
246
+ useEffect(() => {
247
+ if (isInitialMount.current) {
248
+ isInitialMount.current = false;
249
+ return;
250
+ }
251
+ latestStateRef.current = configuratorState;
252
+ setIsPublished(false);
253
+ scheduleSave();
254
+ }, [configuratorState, scheduleSave]);
255
+
256
+ // --- ThemeBar dispatch handlers ---
257
+
258
+ const handleThemeChange = useCallback(
259
+ (theme: ThemeName) => {
260
+ setActiveTheme(theme);
261
+ dispatch({ type: "SET_PREVIEW_THEME", theme });
262
+ },
263
+ [dispatch],
264
+ );
265
+
266
+ const handleColorModeChange = useCallback(
267
+ (mode: ColorMode) => {
268
+ setColorMode(mode);
269
+ dispatch({ type: "SET_PREVIEW_MODE", mode });
270
+ },
271
+ [dispatch],
272
+ );
273
+
274
+ // --- Publish ---
275
+
276
+ const handlePublish = useCallback(async () => {
277
+ if (debounceRef.current) clearTimeout(debounceRef.current);
278
+ setPublishing(true);
279
+
280
+ const currentState = latestStateRef.current;
281
+ const updatedPresets = publishActivePreset(currentState);
282
+
283
+ const { error } = await persistence.onPublish({
284
+ state: currentState,
285
+ presets: updatedPresets,
286
+ activePresetId,
287
+ });
288
+
289
+ if (!error) {
290
+ setSaveStatus("saved");
291
+ setIsPublished(true);
292
+ }
293
+ setPublishing(false);
294
+ }, [activePresetId, publishActivePreset, persistence]);
295
+
296
+ // --- beforeunload warning ---
297
+ useEffect(() => {
298
+ const handleBeforeUnload = (e: BeforeUnloadEvent) => {
299
+ if (saveStatus === "unsaved" || saveStatus === "saving") {
300
+ e.preventDefault();
301
+ }
302
+ };
303
+ window.addEventListener("beforeunload", handleBeforeUnload);
304
+ return () => {
305
+ window.removeEventListener("beforeunload", handleBeforeUnload);
306
+ };
307
+ }, [saveStatus]);
308
+
309
+ // --- Flush pending save on unmount ---
310
+ useEffect(() => {
311
+ return () => {
312
+ if (debounceRef.current) {
313
+ clearTimeout(debounceRef.current);
314
+ const state = latestStateRef.current;
315
+ const flushedPresets = updateActivePresetDraftState(state);
316
+ persistence.onSaveDraft({ state, presets: flushedPresets });
317
+ }
318
+ };
319
+ // eslint-disable-next-line react-hooks/exhaustive-deps
320
+ }, []);
321
+
322
+ const selectedVariantId =
323
+ sidebarSelection?.scope === "variant" ? sidebarSelection.variantId : null;
324
+
325
+ const selectedComponentId =
326
+ sidebarSelection?.scope === "component" ||
327
+ sidebarSelection?.scope === "variant"
328
+ ? sidebarSelection.componentId
329
+ : null;
330
+
331
+ return {
332
+ // Configurator
333
+ configuratorState,
334
+ dispatch,
335
+ themeConfig,
336
+ previewColors,
337
+
338
+ // Save/publish
339
+ saveStatus,
340
+ isPublished,
341
+ publishing,
342
+ handlePublish,
343
+ saveDraft,
344
+
345
+ // Preview
346
+ previewView,
347
+ colorMode,
348
+ activeTheme,
349
+ handlePreviewNavigate,
350
+ handleSelectVariant,
351
+ handleThemeChange,
352
+ handleColorModeChange,
353
+
354
+ // Sidebar
355
+ sidebarSelection,
356
+ selectedComponentId,
357
+ selectedVariantId,
358
+ propOverrides,
359
+ handlePropOverride,
360
+ handleResetOverrides,
361
+ handleCloseSidebar,
362
+ handleScopeToComponent,
363
+
364
+ // Presets
365
+ presets,
366
+ activePresetId,
367
+ publishedPresetId,
368
+ switchPreset,
369
+ createPreset,
370
+ renamePreset,
371
+ deletePreset,
372
+ duplicatePreset,
373
+
374
+ // Revert
375
+ isDirty,
376
+ handleRevert,
377
+
378
+ // For retry button
379
+ latestStateRef,
380
+ } as const;
381
+ }
@@ -0,0 +1,8 @@
1
+ import { useState, useCallback } from "react";
2
+
3
+ export function useHover() {
4
+ const [isHovered, setIsHovered] = useState(false);
5
+ const onMouseEnter = useCallback(() => setIsHovered(true), []);
6
+ const onMouseLeave = useCallback(() => setIsHovered(false), []);
7
+ return { isHovered, hoverProps: { onMouseEnter, onMouseLeave } };
8
+ }
@@ -0,0 +1,254 @@
1
+ import { useState, useCallback, useRef } from "react";
2
+ import type { ConfiguratorState } from "@newtonedev/configurator";
3
+ import type { Preset } from "../types";
4
+
5
+ interface UsePresetsOptions {
6
+ readonly initialPresets: readonly Preset[];
7
+ readonly initialActivePresetId: string;
8
+ readonly initialPublishedPresetId: string | null;
9
+ readonly defaultState: ConfiguratorState;
10
+ readonly onPresetSwitch: (newState: ConfiguratorState) => void;
11
+ readonly getCurrentState: () => ConfiguratorState;
12
+ readonly flushPendingSave: () => Promise<void>;
13
+ readonly persistPresets: (params: {
14
+ readonly presets: readonly Preset[];
15
+ readonly activePresetId: string;
16
+ readonly publishedPresetId: string | null;
17
+ }) => Promise<void>;
18
+ }
19
+
20
+ interface UsePresetsReturn {
21
+ readonly presets: readonly Preset[];
22
+ readonly activePresetId: string;
23
+ readonly publishedPresetId: string | null;
24
+ readonly activePreset: Preset;
25
+ readonly switchPreset: (presetId: string) => Promise<void>;
26
+ readonly createPreset: (name: string) => Promise<string>;
27
+ readonly renamePreset: (presetId: string, name: string) => void;
28
+ readonly deletePreset: (presetId: string) => Promise<void>;
29
+ readonly duplicatePreset: (
30
+ presetId: string,
31
+ newName: string,
32
+ ) => Promise<string>;
33
+ readonly updateActivePresetDraftState: (
34
+ state: ConfiguratorState,
35
+ ) => readonly Preset[];
36
+ readonly publishActivePreset: (
37
+ state: ConfiguratorState,
38
+ ) => readonly Preset[];
39
+ readonly revertActivePreset: () => ConfiguratorState | null;
40
+ }
41
+
42
+ export function usePresets({
43
+ initialPresets,
44
+ initialActivePresetId,
45
+ initialPublishedPresetId,
46
+ defaultState,
47
+ onPresetSwitch,
48
+ getCurrentState,
49
+ flushPendingSave,
50
+ persistPresets,
51
+ }: UsePresetsOptions): UsePresetsReturn {
52
+ const [presets, setPresets] = useState<readonly Preset[]>(initialPresets);
53
+ const [activePresetId, setActivePresetId] = useState(initialActivePresetId);
54
+ const [publishedPresetId, setPublishedPresetId] = useState<string | null>(
55
+ initialPublishedPresetId,
56
+ );
57
+ const presetsRef = useRef(presets);
58
+
59
+ // Keep ref in sync
60
+ presetsRef.current = presets;
61
+
62
+ const activePreset =
63
+ presets.find((p) => p.id === activePresetId) ?? presets[0];
64
+
65
+ // --- Switch active preset ---
66
+ const switchPreset = useCallback(
67
+ async (presetId: string) => {
68
+ if (presetId === activePresetId) return;
69
+
70
+ // Flush any pending auto-save for current preset
71
+ await flushPendingSave();
72
+
73
+ // Save current state into the current preset's draft_state before switching
74
+ const currentState = getCurrentState();
75
+ const updatedPresets = presetsRef.current.map((p) =>
76
+ p.id === activePresetId ? { ...p, draft_state: currentState } : p,
77
+ );
78
+
79
+ // Find target preset
80
+ const target = updatedPresets.find((p) => p.id === presetId);
81
+ if (!target) return;
82
+
83
+ // Update state
84
+ setPresets(updatedPresets);
85
+ presetsRef.current = updatedPresets;
86
+ setActivePresetId(presetId);
87
+
88
+ // Load new preset's draft_state into configurator
89
+ onPresetSwitch(target.draft_state);
90
+
91
+ // Persist
92
+ await persistPresets({
93
+ presets: updatedPresets,
94
+ activePresetId: presetId,
95
+ publishedPresetId,
96
+ });
97
+ },
98
+ [
99
+ activePresetId,
100
+ publishedPresetId,
101
+ flushPendingSave,
102
+ getCurrentState,
103
+ onPresetSwitch,
104
+ persistPresets,
105
+ ],
106
+ );
107
+
108
+ // --- Create new preset ---
109
+ const createPreset = useCallback(
110
+ async (name: string): Promise<string> => {
111
+ const newPreset: Preset = {
112
+ id: crypto.randomUUID(),
113
+ name,
114
+ draft_state: defaultState,
115
+ published_state: null,
116
+ };
117
+ const newPresets = [...presetsRef.current, newPreset];
118
+ setPresets(newPresets);
119
+ presetsRef.current = newPresets;
120
+ await persistPresets({
121
+ presets: newPresets,
122
+ activePresetId,
123
+ publishedPresetId,
124
+ });
125
+ return newPreset.id;
126
+ },
127
+ [defaultState, activePresetId, publishedPresetId, persistPresets],
128
+ );
129
+
130
+ // --- Duplicate preset ---
131
+ const duplicatePreset = useCallback(
132
+ async (presetId: string, newName: string): Promise<string> => {
133
+ const source = presetsRef.current.find((p) => p.id === presetId);
134
+ if (!source) throw new Error("Preset not found");
135
+
136
+ const newPreset: Preset = {
137
+ id: crypto.randomUUID(),
138
+ name: newName,
139
+ draft_state: source.draft_state,
140
+ published_state: null,
141
+ };
142
+ const newPresets = [...presetsRef.current, newPreset];
143
+ setPresets(newPresets);
144
+ presetsRef.current = newPresets;
145
+ await persistPresets({
146
+ presets: newPresets,
147
+ activePresetId,
148
+ publishedPresetId,
149
+ });
150
+ return newPreset.id;
151
+ },
152
+ [activePresetId, publishedPresetId, persistPresets],
153
+ );
154
+
155
+ // --- Rename preset ---
156
+ const renamePreset = useCallback(
157
+ (presetId: string, name: string) => {
158
+ const newPresets = presetsRef.current.map((p) =>
159
+ p.id === presetId ? { ...p, name } : p,
160
+ );
161
+ setPresets(newPresets);
162
+ presetsRef.current = newPresets;
163
+ // fire-and-forget
164
+ persistPresets({
165
+ presets: newPresets,
166
+ activePresetId,
167
+ publishedPresetId,
168
+ });
169
+ },
170
+ [activePresetId, publishedPresetId, persistPresets],
171
+ );
172
+
173
+ // --- Delete preset ---
174
+ const deletePreset = useCallback(
175
+ async (presetId: string) => {
176
+ if (presetsRef.current.length <= 1) return;
177
+
178
+ const newPresets = presetsRef.current.filter((p) => p.id !== presetId);
179
+ let newActiveId = activePresetId;
180
+ let newPublishedId = publishedPresetId;
181
+
182
+ // If deleting active preset, switch to first remaining
183
+ if (presetId === activePresetId) {
184
+ newActiveId = newPresets[0].id;
185
+ onPresetSwitch(newPresets[0].draft_state);
186
+ }
187
+
188
+ // If deleting published preset, clear it
189
+ if (presetId === publishedPresetId) {
190
+ newPublishedId = null;
191
+ }
192
+
193
+ setPresets(newPresets);
194
+ presetsRef.current = newPresets;
195
+ setActivePresetId(newActiveId);
196
+ setPublishedPresetId(newPublishedId);
197
+ await persistPresets({
198
+ presets: newPresets,
199
+ activePresetId: newActiveId,
200
+ publishedPresetId: newPublishedId,
201
+ });
202
+ },
203
+ [activePresetId, publishedPresetId, onPresetSwitch, persistPresets],
204
+ );
205
+
206
+ // --- Update active preset's draft_state (called during auto-save) ---
207
+ const updateActivePresetDraftState = useCallback(
208
+ (state: ConfiguratorState): readonly Preset[] => {
209
+ const newPresets = presetsRef.current.map((p) =>
210
+ p.id === activePresetId ? { ...p, draft_state: state } : p,
211
+ );
212
+ setPresets(newPresets);
213
+ presetsRef.current = newPresets;
214
+ return newPresets;
215
+ },
216
+ [activePresetId],
217
+ );
218
+
219
+ // --- Publish active preset ---
220
+ const publishActivePreset = useCallback(
221
+ (state: ConfiguratorState): readonly Preset[] => {
222
+ const newPresets = presetsRef.current.map((p) =>
223
+ p.id === activePresetId
224
+ ? { ...p, draft_state: state, published_state: state }
225
+ : p,
226
+ );
227
+ setPresets(newPresets);
228
+ presetsRef.current = newPresets;
229
+ setPublishedPresetId(activePresetId);
230
+ return newPresets;
231
+ },
232
+ [activePresetId],
233
+ );
234
+
235
+ // --- Revert active preset to its published_state ---
236
+ const revertActivePreset = useCallback((): ConfiguratorState | null => {
237
+ return activePreset.published_state;
238
+ }, [activePreset]);
239
+
240
+ return {
241
+ presets,
242
+ activePresetId,
243
+ publishedPresetId,
244
+ activePreset,
245
+ switchPreset,
246
+ createPreset,
247
+ renamePreset,
248
+ deletePreset,
249
+ duplicatePreset,
250
+ updateActivePresetDraftState,
251
+ publishActivePreset,
252
+ revertActivePreset,
253
+ };
254
+ }
package/src/index.ts ADDED
@@ -0,0 +1,52 @@
1
+ // Types
2
+ export type {
3
+ Preset,
4
+ SaveStatus,
5
+ ThemeName,
6
+ PreviewView,
7
+ SidebarSelection,
8
+ EditorPersistence,
9
+ EditorHeaderSlots,
10
+ EditorProps,
11
+ } from "./types";
12
+
13
+ // Utilities
14
+ export {
15
+ findPreset,
16
+ updatePresetInArray,
17
+ presetHasUnpublishedChanges,
18
+ } from "./utils/presets";
19
+
20
+ // Hooks
21
+ export { useHover } from "./hooks/useHover";
22
+ export { usePresets } from "./hooks/usePresets";
23
+ export { useEditorState } from "./hooks/useEditorState";
24
+
25
+ // Main component
26
+ export { Editor } from "./Editor";
27
+
28
+ // Presentational components (for advanced composition)
29
+ export { CodeBlock, CopyButton } from "./components/CodeBlock";
30
+ export { EditorHeader } from "./components/EditorHeader";
31
+ export { EditorShell } from "./components/EditorShell";
32
+ export { FontPicker } from "./components/FontPicker";
33
+ export { PresetSelector } from "./components/PresetSelector";
34
+ export { PreviewWindow } from "./components/PreviewWindow";
35
+ export { RightSidebar } from "./components/RightSidebar";
36
+ export { Sidebar } from "./components/Sidebar";
37
+ export { TableOfContents } from "./components/TableOfContents";
38
+ export { ThemeBar } from "./components/ThemeBar";
39
+
40
+ // Sections
41
+ export {
42
+ ColorsSection,
43
+ FontsSection,
44
+ IconsSection,
45
+ OthersSection,
46
+ } from "./components/sections";
47
+
48
+ // Preview
49
+ export { ComponentRenderer } from "./preview/ComponentRenderer";
50
+ export { OverviewView } from "./preview/OverviewView";
51
+ export { CategoryView } from "./preview/CategoryView";
52
+ export { ComponentDetailView } from "./preview/ComponentDetailView";