@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.
- package/dist/Editor.d.ts +3 -0
- package/dist/Editor.d.ts.map +1 -0
- package/dist/components/CodeBlock.d.ts +7 -0
- package/dist/components/CodeBlock.d.ts.map +1 -0
- package/dist/components/EditorHeader.d.ts +16 -0
- package/dist/components/EditorHeader.d.ts.map +1 -0
- package/dist/components/EditorShell.d.ts +10 -0
- package/dist/components/EditorShell.d.ts.map +1 -0
- package/dist/components/FontPicker.d.ts +11 -0
- package/dist/components/FontPicker.d.ts.map +1 -0
- package/dist/components/PresetSelector.d.ts +14 -0
- package/dist/components/PresetSelector.d.ts.map +1 -0
- package/dist/components/PreviewWindow.d.ts +11 -0
- package/dist/components/PreviewWindow.d.ts.map +1 -0
- package/dist/components/RightSidebar.d.ts +12 -0
- package/dist/components/RightSidebar.d.ts.map +1 -0
- package/dist/components/Sidebar.d.ts +25 -0
- package/dist/components/Sidebar.d.ts.map +1 -0
- package/dist/components/TableOfContents.d.ts +9 -0
- package/dist/components/TableOfContents.d.ts.map +1 -0
- package/dist/components/ThemeBar.d.ts +8 -0
- package/dist/components/ThemeBar.d.ts.map +1 -0
- package/dist/components/sections/ColorsSection.d.ts +14 -0
- package/dist/components/sections/ColorsSection.d.ts.map +1 -0
- package/dist/components/sections/DynamicRangeSection.d.ts +9 -0
- package/dist/components/sections/DynamicRangeSection.d.ts.map +1 -0
- package/dist/components/sections/FontsSection.d.ts +9 -0
- package/dist/components/sections/FontsSection.d.ts.map +1 -0
- package/dist/components/sections/IconsSection.d.ts +9 -0
- package/dist/components/sections/IconsSection.d.ts.map +1 -0
- package/dist/components/sections/OthersSection.d.ts +9 -0
- package/dist/components/sections/OthersSection.d.ts.map +1 -0
- package/dist/components/sections/index.d.ts +6 -0
- package/dist/components/sections/index.d.ts.map +1 -0
- package/dist/hooks/useEditorState.d.ts +53 -0
- package/dist/hooks/useEditorState.d.ts.map +1 -0
- package/dist/hooks/useHover.d.ts +8 -0
- package/dist/hooks/useHover.d.ts.map +1 -0
- package/dist/hooks/usePresets.d.ts +33 -0
- package/dist/hooks/usePresets.d.ts.map +1 -0
- package/dist/index.cjs +3846 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3819 -0
- package/dist/index.js.map +1 -0
- package/dist/preview/CategoryView.d.ts +7 -0
- package/dist/preview/CategoryView.d.ts.map +1 -0
- package/dist/preview/ComponentDetailView.d.ts +9 -0
- package/dist/preview/ComponentDetailView.d.ts.map +1 -0
- package/dist/preview/ComponentRenderer.d.ts +7 -0
- package/dist/preview/ComponentRenderer.d.ts.map +1 -0
- package/dist/preview/OverviewView.d.ts +7 -0
- package/dist/preview/OverviewView.d.ts.map +1 -0
- package/dist/types.d.ts +69 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/presets.d.ts +5 -0
- package/dist/utils/presets.d.ts.map +1 -0
- package/package.json +51 -0
- package/src/Editor.tsx +128 -0
- package/src/components/CodeBlock.tsx +58 -0
- package/src/components/EditorHeader.tsx +86 -0
- package/src/components/EditorShell.tsx +67 -0
- package/src/components/FontPicker.tsx +351 -0
- package/src/components/PresetSelector.tsx +455 -0
- package/src/components/PreviewWindow.tsx +69 -0
- package/src/components/RightSidebar.tsx +374 -0
- package/src/components/Sidebar.tsx +332 -0
- package/src/components/TableOfContents.tsx +152 -0
- package/src/components/ThemeBar.tsx +76 -0
- package/src/components/sections/ColorsSection.tsx +485 -0
- package/src/components/sections/DynamicRangeSection.tsx +399 -0
- package/src/components/sections/FontsSection.tsx +132 -0
- package/src/components/sections/IconsSection.tsx +66 -0
- package/src/components/sections/OthersSection.tsx +70 -0
- package/src/components/sections/index.ts +5 -0
- package/src/hooks/useEditorState.ts +381 -0
- package/src/hooks/useHover.ts +8 -0
- package/src/hooks/usePresets.ts +254 -0
- package/src/index.ts +52 -0
- package/src/preview/CategoryView.tsx +134 -0
- package/src/preview/ComponentDetailView.tsx +126 -0
- package/src/preview/ComponentRenderer.tsx +107 -0
- package/src/preview/OverviewView.tsx +177 -0
- package/src/types.ts +77 -0
- 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";
|