@newtonedev/editor 0.1.12 → 0.2.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 (116) hide show
  1. package/dist/Editor.d.ts.map +1 -1
  2. package/dist/components/CodeBlock.d.ts.map +1 -1
  3. package/dist/components/ConfiguratorPanel.d.ts +6 -3
  4. package/dist/components/ConfiguratorPanel.d.ts.map +1 -1
  5. package/dist/components/EditorHeader.d.ts +3 -2
  6. package/dist/components/EditorHeader.d.ts.map +1 -1
  7. package/dist/components/EditorShell.d.ts.map +1 -1
  8. package/dist/components/PresetSelector.d.ts +3 -2
  9. package/dist/components/PresetSelector.d.ts.map +1 -1
  10. package/dist/components/PreviewWindow.d.ts.map +1 -1
  11. package/dist/components/RightSidebar.d.ts.map +1 -1
  12. package/dist/components/Sidebar.d.ts +8 -1
  13. package/dist/components/Sidebar.d.ts.map +1 -1
  14. package/dist/components/TableOfContents.d.ts.map +1 -1
  15. package/dist/components/sections/ColorsSection.d.ts +6 -3
  16. package/dist/components/sections/ColorsSection.d.ts.map +1 -1
  17. package/dist/components/sections/DynamicRangeSection.d.ts +2 -2
  18. package/dist/components/sections/DynamicRangeSection.d.ts.map +1 -1
  19. package/dist/components/sections/FontsSection.d.ts +2 -2
  20. package/dist/components/sections/FontsSection.d.ts.map +1 -1
  21. package/dist/components/sections/IconsSection.d.ts +2 -2
  22. package/dist/components/sections/IconsSection.d.ts.map +1 -1
  23. package/dist/components/sections/OthersSection.d.ts +2 -2
  24. package/dist/components/sections/OthersSection.d.ts.map +1 -1
  25. package/dist/components/sections/ScalePlots.d.ts +11 -0
  26. package/dist/components/sections/ScalePlots.d.ts.map +1 -0
  27. package/dist/components/sections/index.d.ts +1 -0
  28. package/dist/components/sections/index.d.ts.map +1 -1
  29. package/dist/configurator/bridge/toCSS.d.ts +7 -0
  30. package/dist/configurator/bridge/toCSS.d.ts.map +1 -0
  31. package/dist/configurator/bridge/toJSON.d.ts +15 -0
  32. package/dist/configurator/bridge/toJSON.d.ts.map +1 -0
  33. package/dist/configurator/bridge/toThemeConfig.d.ts +8 -0
  34. package/dist/configurator/bridge/toThemeConfig.d.ts.map +1 -0
  35. package/dist/configurator/constants.d.ts +13 -0
  36. package/dist/configurator/constants.d.ts.map +1 -0
  37. package/dist/configurator/hex-conversion.d.ts +21 -0
  38. package/dist/configurator/hex-conversion.d.ts.map +1 -0
  39. package/dist/configurator/hooks/useConfigurator.d.ts +11 -0
  40. package/dist/configurator/hooks/useConfigurator.d.ts.map +1 -0
  41. package/dist/configurator/hooks/usePreviewColors.d.ts +8 -0
  42. package/dist/configurator/hooks/usePreviewColors.d.ts.map +1 -0
  43. package/dist/configurator/hooks/useWcagValidation.d.ts +20 -0
  44. package/dist/configurator/hooks/useWcagValidation.d.ts.map +1 -0
  45. package/dist/configurator/hue-conversion.d.ts +10 -0
  46. package/dist/configurator/hue-conversion.d.ts.map +1 -0
  47. package/dist/configurator/state/actions.d.ts +107 -0
  48. package/dist/configurator/state/actions.d.ts.map +1 -0
  49. package/dist/configurator/state/defaults.d.ts +7 -0
  50. package/dist/configurator/state/defaults.d.ts.map +1 -0
  51. package/dist/configurator/state/reducer.d.ts +19 -0
  52. package/dist/configurator/state/reducer.d.ts.map +1 -0
  53. package/dist/configurator/types.d.ts +60 -0
  54. package/dist/configurator/types.d.ts.map +1 -0
  55. package/dist/hooks/useEditorState.d.ts +8 -6
  56. package/dist/hooks/useEditorState.d.ts.map +1 -1
  57. package/dist/hooks/usePresets.d.ts +7 -6
  58. package/dist/hooks/usePresets.d.ts.map +1 -1
  59. package/dist/index.cjs +30372 -808
  60. package/dist/index.cjs.map +1 -1
  61. package/dist/index.d.ts +17 -0
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +30351 -799
  64. package/dist/index.js.map +1 -1
  65. package/dist/preview/CategoryView.d.ts.map +1 -1
  66. package/dist/preview/ComponentDetailView.d.ts.map +1 -1
  67. package/dist/preview/ComponentRenderer.d.ts.map +1 -1
  68. package/dist/preview/IconBrowserView.d.ts.map +1 -1
  69. package/dist/preview/OverviewView.d.ts.map +1 -1
  70. package/dist/preview/PaletteScaleView.d.ts +11 -0
  71. package/dist/preview/PaletteScaleView.d.ts.map +1 -0
  72. package/dist/types.d.ts +4 -3
  73. package/dist/types.d.ts.map +1 -1
  74. package/package.json +7 -4
  75. package/src/Editor.tsx +43 -19
  76. package/src/components/CodeBlock.tsx +7 -11
  77. package/src/components/ConfiguratorPanel.tsx +25 -18
  78. package/src/components/EditorHeader.tsx +29 -39
  79. package/src/components/EditorShell.tsx +17 -29
  80. package/src/components/FontPicker.tsx +7 -7
  81. package/src/components/PresetSelector.tsx +211 -129
  82. package/src/components/PreviewWindow.tsx +5 -12
  83. package/src/components/PrimaryNav.tsx +6 -6
  84. package/src/components/RightSidebar.tsx +24 -25
  85. package/src/components/Sidebar.tsx +54 -60
  86. package/src/components/TableOfContents.tsx +4 -5
  87. package/src/components/sections/ColorsSection.tsx +109 -121
  88. package/src/components/sections/DynamicRangeSection.tsx +61 -75
  89. package/src/components/sections/FontsSection.tsx +17 -28
  90. package/src/components/sections/IconsSection.tsx +2 -2
  91. package/src/components/sections/OthersSection.tsx +4 -5
  92. package/src/components/sections/ScalePlots.tsx +221 -0
  93. package/src/components/sections/index.ts +1 -0
  94. package/src/configurator/bridge/toCSS.ts +44 -0
  95. package/src/configurator/bridge/toJSON.ts +24 -0
  96. package/src/configurator/bridge/toThemeConfig.ts +114 -0
  97. package/src/configurator/constants.ts +13 -0
  98. package/src/configurator/hex-conversion.ts +67 -0
  99. package/src/configurator/hooks/useConfigurator.ts +33 -0
  100. package/src/configurator/hooks/usePreviewColors.ts +47 -0
  101. package/src/configurator/hooks/useWcagValidation.ts +133 -0
  102. package/src/configurator/hue-conversion.ts +25 -0
  103. package/src/configurator/state/actions.ts +43 -0
  104. package/src/configurator/state/defaults.ts +107 -0
  105. package/src/configurator/state/reducer.ts +399 -0
  106. package/src/configurator/types.ts +65 -0
  107. package/src/hooks/useEditorState.ts +25 -11
  108. package/src/hooks/usePresets.ts +54 -33
  109. package/src/index.ts +33 -0
  110. package/src/preview/CategoryView.tsx +8 -11
  111. package/src/preview/ComponentDetailView.tsx +24 -54
  112. package/src/preview/ComponentRenderer.tsx +2 -4
  113. package/src/preview/IconBrowserView.tsx +9 -10
  114. package/src/preview/OverviewView.tsx +9 -12
  115. package/src/preview/PaletteScaleView.tsx +122 -0
  116. package/src/types.ts +4 -3
@@ -0,0 +1,399 @@
1
+ import type { ConfiguratorState, PaletteState } from '../types';
2
+ import type { ConfiguratorAction } from './actions';
3
+ import { DEFAULT_CONFIGURATOR_STATE } from './defaults';
4
+ import { traditionalHueToOklch } from '../hue-conversion';
5
+
6
+ function updatePalette(
7
+ palettes: readonly PaletteState[],
8
+ index: number,
9
+ update: Partial<PaletteState>,
10
+ ): readonly PaletteState[] {
11
+ if (index < 0 || index >= palettes.length) return palettes;
12
+ return palettes.map((p, i) => (i === index ? { ...p, ...update } : p));
13
+ }
14
+
15
+ function clamp(value: number, min: number, max: number): number {
16
+ return Math.max(min, Math.min(max, value));
17
+ }
18
+
19
+ function wrapHue(hue: number): number {
20
+ return ((hue % 360) + 360) % 360;
21
+ }
22
+
23
+ function clampStep(step: number): number {
24
+ return Math.round(Math.max(0, Math.min(25, step)));
25
+ }
26
+
27
+ /** Default Tertiary palette inserted during migration of 5-palette states. */
28
+ const DEFAULT_TERTIARY: PaletteState = {
29
+ name: 'Tertiary',
30
+ hue: 195,
31
+ chromaRatio: 0.5,
32
+ chromaPeak: 0.5,
33
+ };
34
+
35
+ /**
36
+ * Ensure 6 palettes exist (Primary, Secondary, Tertiary, Success, Warning, Error).
37
+ * Old states have 5 palettes (no Tertiary). Insert a default Tertiary at index 2
38
+ * and update legacy palette names (Neutral→Primary, Accent→Secondary).
39
+ */
40
+ function migratePaletteCount(palettes: readonly PaletteState[]): readonly PaletteState[] {
41
+ let result = [...palettes];
42
+
43
+ // Rename legacy palette names
44
+ if (result[0]?.name === 'Neutral') result[0] = { ...result[0], name: 'Primary' };
45
+ if (result[1]?.name === 'Accent') result[1] = { ...result[1], name: 'Secondary' };
46
+
47
+ // Insert Tertiary at index 2 if only 5 palettes exist
48
+ if (result.length === 5) {
49
+ result.splice(2, 0, DEFAULT_TERTIARY);
50
+ }
51
+
52
+ return result;
53
+ }
54
+
55
+ /**
56
+ * Migrate persisted palette state from old format to new format.
57
+ * Handles:
58
+ * - Old `saturation` (0-100) → `chromaRatio` (0-1)
59
+ * - Old `desaturation` (-1 to 1) → `chromaPeak` (0-1, 0.5=natural)
60
+ * - Old `hueGradeStrength/hueGradeHue/hueGradeDirection` → `localHueGrade`
61
+ * - Legacy `desaturationStrength/desaturationDirection` era
62
+ * Idempotent: already-migrated state passes through unchanged.
63
+ */
64
+ export function migratePaletteState(p: Record<string, unknown>): PaletteState {
65
+ const result: Record<string, unknown> = { ...p };
66
+
67
+ // Migrate saturation [0-100] → chromaRatio [0-1]
68
+ if (!('chromaRatio' in result) && 'saturation' in result && typeof result.saturation === 'number') {
69
+ result.chromaRatio = result.saturation / 100;
70
+ delete result.saturation;
71
+ }
72
+
73
+ // Ensure chromaRatio exists with fallback
74
+ if (!('chromaRatio' in result)) {
75
+ result.chromaRatio = 0.5;
76
+ }
77
+
78
+ // Migrate desaturation [-1,1] → chromaPeak [0,1]
79
+ if (!('chromaPeak' in result)) {
80
+ if ('desaturation' in result && typeof result.desaturation === 'number') {
81
+ result.chromaPeak = 0.5 - (result.desaturation as number) * 0.3;
82
+ } else {
83
+ result.chromaPeak = 0.5;
84
+ }
85
+ delete result.desaturation;
86
+ }
87
+
88
+ // Migrate legacy desaturationStrength/desaturationDirection era
89
+ if ('desaturationStrength' in result || 'desaturationDirection' in result) {
90
+ const LEGACY_STRENGTH_MAP: Record<string, number> = { none: 0, low: 0.2, medium: 0.4, hard: 0.6 };
91
+ const magnitude = LEGACY_STRENGTH_MAP[result.desaturationStrength as string] ?? 0;
92
+ const sign = result.desaturationDirection === 'dark' ? 1 : -1;
93
+ const desaturation = magnitude === 0 ? 0 : sign * magnitude;
94
+ result.chromaPeak = 0.5 - desaturation * 0.3;
95
+ delete result.desaturationStrength;
96
+ delete result.desaturationDirection;
97
+ }
98
+
99
+ // Migrate keyColor (float NV [0,1]) → keyColorStep (integer [0,25])
100
+ if ('keyColor' in result && typeof result.keyColor === 'number' && !('keyColorStep' in result)) {
101
+ // NV convention: 0=darkest, 1=lightest → step: 0=lightest, 25=darkest
102
+ result.keyColorStep = Math.round((1 - (result.keyColor as number)) * 25);
103
+ delete result.keyColor;
104
+ }
105
+ if ('keyColorDark' in result && typeof result.keyColorDark === 'number' && !('keyColorStepDark' in result)) {
106
+ result.keyColorStepDark = Math.round((1 - (result.keyColorDark as number)) * 25);
107
+ delete result.keyColorDark;
108
+ }
109
+
110
+ // Migrate hueGradeStrength/hueGradeHue/hueGradeDirection → localHueGrade
111
+ if (!('localHueGrade' in result) && 'hueGradeStrength' in result) {
112
+ const strengthMap: Record<string, number> = { none: 0, low: 0.06, medium: 0.13, hard: 0.25 };
113
+ const strength = result.hueGradeStrength as string;
114
+ const intensity = strengthMap[strength] ?? 0;
115
+ if (intensity > 0) {
116
+ result.localHueGrade = {
117
+ hue: typeof result.hueGradeHue === 'number' ? result.hueGradeHue : 0,
118
+ intensity,
119
+ side: result.hueGradeDirection ?? 'light',
120
+ };
121
+ }
122
+ delete result.hueGradeStrength;
123
+ delete result.hueGradeHue;
124
+ delete result.hueGradeDirection;
125
+ }
126
+
127
+ return result as unknown as PaletteState;
128
+ }
129
+
130
+ /**
131
+ * Migrate persisted ConfiguratorState globalHueGrading from old format.
132
+ * Old: { light: { strength, hue }, dark: { strength, hue } }
133
+ * New: { lightHue, lightIntensity, darkHue, darkIntensity }
134
+ */
135
+ function migrateGlobalHueGrading(g: Record<string, unknown>): ConfiguratorState['globalHueGrading'] {
136
+ if ('lightIntensity' in g) {
137
+ return g as unknown as ConfiguratorState['globalHueGrading'];
138
+ }
139
+ const strengthMap: Record<string, number> = { none: 0, low: 0.06, medium: 0.13, hard: 0.25 };
140
+ const light = (g.light ?? {}) as Record<string, unknown>;
141
+ const dark = (g.dark ?? {}) as Record<string, unknown>;
142
+ return {
143
+ lightHue: typeof light.hue === 'number' ? light.hue : 30,
144
+ lightIntensity: strengthMap[light.strength as string] ?? 0,
145
+ darkHue: typeof dark.hue === 'number' ? dark.hue : 220,
146
+ darkIntensity: strengthMap[dark.strength as string] ?? 0,
147
+ };
148
+ }
149
+
150
+ /** Convert a palette's hue fields from traditional HSL to OKLCH. */
151
+ function migratePaletteHuesToOklch(p: PaletteState): PaletteState {
152
+ return {
153
+ ...p,
154
+ hue: traditionalHueToOklch(p.hue),
155
+ ...(p.localHueGrade ? {
156
+ localHueGrade: { ...p.localHueGrade, hue: traditionalHueToOklch(p.localHueGrade.hue) },
157
+ } : {}),
158
+ };
159
+ }
160
+
161
+ /** Convert global grading hues from traditional HSL to OKLCH. */
162
+ function migrateGlobalHuesToOklch(
163
+ g: ConfiguratorState['globalHueGrading'],
164
+ ): ConfiguratorState['globalHueGrading'] {
165
+ return {
166
+ ...g,
167
+ lightHue: traditionalHueToOklch(g.lightHue),
168
+ darkHue: traditionalHueToOklch(g.darkHue),
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Migrate a full ConfiguratorState loaded from DB (old or new format) to the current format.
174
+ * Safe to call on already-migrated state (idempotent).
175
+ */
176
+ export function migrateConfiguratorState(raw: unknown): ConfiguratorState {
177
+ const r = raw as Record<string, unknown>;
178
+ const base = r as unknown as ConfiguratorState;
179
+
180
+ let palettes = Array.isArray(r.palettes)
181
+ ? migratePaletteCount(r.palettes.map((p) => migratePaletteState(p as Record<string, unknown>)))
182
+ : DEFAULT_CONFIGURATOR_STATE.palettes;
183
+
184
+ let globalHueGrading = migrateGlobalHueGrading(
185
+ (r.globalHueGrading ?? {}) as Record<string, unknown>,
186
+ );
187
+
188
+ // Migrate traditional hues to OKLCH if hueSpace is absent
189
+ if (!r.hueSpace) {
190
+ palettes = [...palettes.map(p => migratePaletteHuesToOklch(p))];
191
+ globalHueGrading = migrateGlobalHuesToOklch(globalHueGrading);
192
+ }
193
+
194
+ return {
195
+ ...base,
196
+ hueSpace: 'oklch',
197
+ palettes,
198
+ globalHueGrading,
199
+ };
200
+ }
201
+
202
+ export function configuratorReducer(
203
+ state: ConfiguratorState,
204
+ action: ConfiguratorAction,
205
+ ): ConfiguratorState {
206
+ switch (action.type) {
207
+ case 'SET_PALETTE_HUE':
208
+ return {
209
+ ...state,
210
+ palettes: updatePalette(state.palettes, action.index, { hue: wrapHue(action.hue) }),
211
+ };
212
+
213
+ case 'SET_PALETTE_CHROMA_RATIO':
214
+ return {
215
+ ...state,
216
+ palettes: updatePalette(state.palettes, action.index, { chromaRatio: clamp(action.chromaRatio, 0, 1) }),
217
+ };
218
+
219
+ case 'SET_PALETTE_CHROMA_PEAK':
220
+ return {
221
+ ...state,
222
+ palettes: updatePalette(state.palettes, action.index, { chromaPeak: clamp(action.chromaPeak, 0, 1) }),
223
+ };
224
+
225
+ case 'SET_PALETTE_LOCAL_HUE_GRADE_INTENSITY': {
226
+ const intensity = clamp(action.intensity, 0, 0.5);
227
+ if (intensity === 0) {
228
+ return {
229
+ ...state,
230
+ palettes: updatePalette(state.palettes, action.index, { localHueGrade: undefined }),
231
+ };
232
+ }
233
+ const existing = state.palettes[action.index]?.localHueGrade;
234
+ return {
235
+ ...state,
236
+ palettes: updatePalette(state.palettes, action.index, {
237
+ localHueGrade: { hue: existing?.hue ?? 0, intensity, side: existing?.side ?? 'light' },
238
+ }),
239
+ };
240
+ }
241
+
242
+ case 'SET_PALETTE_LOCAL_HUE_GRADE_HUE': {
243
+ const existing = state.palettes[action.index]?.localHueGrade;
244
+ if (!existing) return state;
245
+ return {
246
+ ...state,
247
+ palettes: updatePalette(state.palettes, action.index, {
248
+ localHueGrade: { ...existing, hue: wrapHue(action.hue) },
249
+ }),
250
+ };
251
+ }
252
+
253
+ case 'SET_PALETTE_LOCAL_HUE_GRADE_SIDE': {
254
+ const existing = state.palettes[action.index]?.localHueGrade;
255
+ if (!existing) return state;
256
+ return {
257
+ ...state,
258
+ palettes: updatePalette(state.palettes, action.index, {
259
+ localHueGrade: { ...existing, side: action.side },
260
+ }),
261
+ };
262
+ }
263
+
264
+ case 'SET_PALETTE_KEY_COLOR_STEP':
265
+ return {
266
+ ...state,
267
+ palettes: updatePalette(state.palettes, action.index, { keyColorStep: clampStep(action.step) }),
268
+ };
269
+
270
+ case 'CLEAR_PALETTE_KEY_COLOR_STEP':
271
+ return {
272
+ ...state,
273
+ palettes: updatePalette(state.palettes, action.index, { keyColorStep: undefined }),
274
+ };
275
+
276
+ case 'SET_PALETTE_FROM_HEX':
277
+ return {
278
+ ...state,
279
+ palettes: updatePalette(state.palettes, action.index, {
280
+ hue: wrapHue(action.hue),
281
+ chromaRatio: clamp(action.chromaRatio, 0, 1),
282
+ keyColorStep: clampStep(action.keyColorStep),
283
+ }),
284
+ };
285
+
286
+ case 'SET_PALETTE_KEY_COLOR_STEP_DARK':
287
+ return {
288
+ ...state,
289
+ palettes: updatePalette(state.palettes, action.index, { keyColorStepDark: clampStep(action.step) }),
290
+ };
291
+
292
+ case 'CLEAR_PALETTE_KEY_COLOR_STEP_DARK':
293
+ return {
294
+ ...state,
295
+ palettes: updatePalette(state.palettes, action.index, { keyColorStepDark: undefined }),
296
+ };
297
+
298
+ case 'SET_PALETTE_FROM_HEX_DARK':
299
+ return {
300
+ ...state,
301
+ palettes: updatePalette(state.palettes, action.index, {
302
+ hue: wrapHue(action.hue),
303
+ chromaRatio: clamp(action.chromaRatio, 0, 1),
304
+ keyColorStepDark: clampStep(action.keyColorStep),
305
+ }),
306
+ };
307
+
308
+ case 'SET_LIGHTEST':
309
+ return { ...state, dynamicRange: { ...state.dynamicRange, lightest: clamp(action.value, 0, 1) } };
310
+
311
+ case 'SET_DARKEST':
312
+ return { ...state, dynamicRange: { ...state.dynamicRange, darkest: clamp(action.value, 0, 1) } };
313
+
314
+ case 'SET_GLOBAL_GRADE_LIGHT_INTENSITY':
315
+ return { ...state, globalHueGrading: { ...state.globalHueGrading, lightIntensity: clamp(action.intensity, 0, 0.25) } };
316
+
317
+ case 'SET_GLOBAL_GRADE_LIGHT_HUE':
318
+ return { ...state, globalHueGrading: { ...state.globalHueGrading, lightHue: wrapHue(action.hue) } };
319
+
320
+ case 'SET_GLOBAL_GRADE_DARK_INTENSITY':
321
+ return { ...state, globalHueGrading: { ...state.globalHueGrading, darkIntensity: clamp(action.intensity, 0, 0.25) } };
322
+
323
+ case 'SET_GLOBAL_GRADE_DARK_HUE':
324
+ return { ...state, globalHueGrading: { ...state.globalHueGrading, darkHue: wrapHue(action.hue) } };
325
+
326
+ case 'SET_SPACING_PRESET':
327
+ return { ...state, spacing: { preset: action.preset } };
328
+
329
+ case 'SET_ROUNDNESS_INTENSITY':
330
+ return { ...state, roundness: { intensity: clamp(action.intensity, 0, 1) } };
331
+
332
+ case 'SET_FONT': {
333
+ const defaultTypography = DEFAULT_CONFIGURATOR_STATE.typography!;
334
+ const currentFonts = state.typography?.fonts ?? defaultTypography.fonts;
335
+ return { ...state, typography: { fonts: { ...currentFonts, [action.scope]: action.font } } };
336
+ }
337
+
338
+ case 'SET_TYPE_SCALE_OFFSET': {
339
+ const defaultTypography = DEFAULT_CONFIGURATOR_STATE.typography!;
340
+ const currentFonts = state.typography?.fonts ?? defaultTypography.fonts;
341
+ return { ...state, typography: { ...state.typography, fonts: currentFonts, typeScaleOffset: clamp(action.offset, 0, 1) } };
342
+ }
343
+
344
+ case 'SET_ROLE_WEIGHT': {
345
+ const defaultTypography = DEFAULT_CONFIGURATOR_STATE.typography!;
346
+ const currentFonts = state.typography?.fonts ?? defaultTypography.fonts;
347
+ return {
348
+ ...state,
349
+ typography: {
350
+ ...state.typography,
351
+ fonts: currentFonts,
352
+ roleWeights: { ...state.typography?.roleWeights, [action.role]: clamp(action.weight, 100, 900) },
353
+ },
354
+ };
355
+ }
356
+
357
+ case 'SET_ICON_VARIANT': {
358
+ const defaultIcons = DEFAULT_CONFIGURATOR_STATE.icons!;
359
+ return { ...state, icons: { variant: action.variant, weight: state.icons?.weight ?? defaultIcons.weight, autoGrade: state.icons?.autoGrade ?? defaultIcons.autoGrade } };
360
+ }
361
+
362
+ case 'SET_ICON_WEIGHT': {
363
+ const defaultIcons = DEFAULT_CONFIGURATOR_STATE.icons!;
364
+ return { ...state, icons: { variant: state.icons?.variant ?? defaultIcons.variant, weight: action.weight, autoGrade: state.icons?.autoGrade ?? defaultIcons.autoGrade } };
365
+ }
366
+
367
+ case 'SET_ICON_AUTO_GRADE': {
368
+ const defaultIcons = DEFAULT_CONFIGURATOR_STATE.icons!;
369
+ return { ...state, icons: { variant: state.icons?.variant ?? defaultIcons.variant, weight: state.icons?.weight ?? defaultIcons.weight, autoGrade: action.autoGrade } };
370
+ }
371
+
372
+ case 'SET_PREVIEW_MODE':
373
+ return { ...state, preview: { ...state.preview, mode: action.mode } };
374
+
375
+ case 'RESET':
376
+ return DEFAULT_CONFIGURATOR_STATE;
377
+
378
+ case 'LOAD_STATE': {
379
+ const raw = action.state as unknown as Record<string, unknown>;
380
+ let palettes = migratePaletteCount(action.state.palettes.map(
381
+ (p) => migratePaletteState(p as unknown as Record<string, unknown>),
382
+ ));
383
+ let globalHueGrading = migrateGlobalHueGrading(
384
+ (raw.globalHueGrading ?? {}) as Record<string, unknown>,
385
+ );
386
+
387
+ // Migrate traditional hues to OKLCH if hueSpace is absent
388
+ if (!raw.hueSpace) {
389
+ palettes = palettes.map(p => migratePaletteHuesToOklch(p));
390
+ globalHueGrading = migrateGlobalHuesToOklch(globalHueGrading);
391
+ }
392
+
393
+ return { ...action.state, hueSpace: 'oklch', palettes, globalHueGrading };
394
+ }
395
+
396
+ default:
397
+ return state;
398
+ }
399
+ }
@@ -0,0 +1,65 @@
1
+ import type { ColorMode } from '@newtonedev/components';
2
+
3
+ // Re-export font types from @newtonedev/fonts (canonical source)
4
+ export type { FontConfig, FontScope } from '@newtonedev/fonts';
5
+ // Backward-compatible alias: configurator historically used this name
6
+ export type { FontSlot as FontSlotConfig } from '@newtonedev/fonts';
7
+ import type { FontSlot, TextRole } from '@newtonedev/fonts';
8
+
9
+ /** Spacing preset options */
10
+ export type SpacingPreset = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
11
+
12
+ /** Per-palette state in the configurator */
13
+ export interface PaletteState {
14
+ readonly name: string;
15
+ readonly hue: number; // OKLCH hue [0, 360)
16
+ readonly chromaRatio: number; // [0, 1] fraction of max in-gamut chroma
17
+ readonly chromaPeak: number; // [0, 1] where chroma peaks; 0.5 = natural
18
+ readonly localHueGrade?: {
19
+ readonly hue: number; // OKLCH hue [0, 360)
20
+ readonly intensity: number; // [0, 0.5]
21
+ readonly side: 'light' | 'dark';
22
+ };
23
+ readonly keyColorStep?: number; // Light mode step index [0, 25] — undefined = auto
24
+ readonly keyColorStepDark?: number; // Dark mode step index [0, 25] — undefined = auto
25
+ }
26
+
27
+ /** Complete configurator state */
28
+ export interface ConfiguratorState {
29
+ readonly hueSpace?: 'oklch'; // Present = OKLCH hues; absent = legacy traditional hues
30
+ readonly palettes: readonly PaletteState[];
31
+ readonly dynamicRange: {
32
+ readonly lightest: number; // [0, 1]
33
+ readonly darkest: number; // [0, 1]
34
+ };
35
+ readonly globalHueGrading: {
36
+ readonly lightHue: number; // OKLCH hue [0, 360)
37
+ readonly lightIntensity: number; // [0, 0.25]
38
+ readonly darkHue: number; // OKLCH hue [0, 360)
39
+ readonly darkIntensity: number; // [0, 0.25]
40
+ };
41
+ readonly preview: {
42
+ readonly mode: ColorMode;
43
+ };
44
+ readonly spacing?: {
45
+ readonly preset: SpacingPreset; // xs=6px, sm=7px, md=8px, lg=9px, xl=10px base
46
+ };
47
+ readonly roundness?: {
48
+ readonly intensity: number; // [0, 1] where 0=rectangle, 1=pill
49
+ };
50
+ readonly typography?: {
51
+ readonly fonts: {
52
+ readonly main: FontSlot; // Body/default font
53
+ readonly display: FontSlot; // Headlines, titles
54
+ readonly mono: FontSlot; // Code, technical content
55
+ readonly currency: FontSlot; // Monetary amounts, financial data
56
+ };
57
+ readonly typeScaleOffset?: number; // [0, 1], 0.5 = identity (default)
58
+ readonly roleWeights?: Partial<Record<TextRole, number>>; // CSS font-weight per role (100-900)
59
+ };
60
+ readonly icons?: {
61
+ readonly variant: 'outlined' | 'rounded' | 'sharp';
62
+ readonly weight: 100 | 200 | 300 | 400 | 500 | 600 | 700;
63
+ readonly autoGrade: boolean; // true = mode-aware grade (light=-25, dark=200)
64
+ };
65
+ }
@@ -1,8 +1,9 @@
1
1
  import { useState, useCallback, useRef, useEffect, useMemo } from "react";
2
2
  import { getComponent, CATEGORIES, getComponentsByCategory } from "@newtonedev/components";
3
3
  import type { ColorMode } from "@newtonedev/components";
4
- import type { ConfiguratorState } from "@newtonedev/configurator";
5
- import { useConfigurator, usePreviewColors } from "@newtonedev/configurator";
4
+ import type { ConfiguratorState } from "../configurator/types";
5
+ import { useConfigurator } from "../configurator/hooks/useConfigurator";
6
+ import { usePreviewColors } from "../configurator/hooks/usePreviewColors";
6
7
  import { usePresets } from "./usePresets";
7
8
  import type {
8
9
  Preset,
@@ -25,6 +26,7 @@ interface UseEditorStateOptions {
25
26
  readonly onNavigate?: (view: PreviewView) => void;
26
27
  readonly initialPreviewView?: PreviewView;
27
28
  readonly manifestUrl?: string;
29
+ readonly useP3?: boolean;
28
30
  }
29
31
 
30
32
  export function useEditorState({
@@ -38,6 +40,7 @@ export function useEditorState({
38
40
  onNavigate,
39
41
  initialPreviewView,
40
42
  manifestUrl,
43
+ useP3,
41
44
  }: UseEditorStateOptions) {
42
45
  // --- Configurator state management ---
43
46
  const {
@@ -45,7 +48,7 @@ export function useEditorState({
45
48
  dispatch,
46
49
  themeConfig,
47
50
  } = useConfigurator(initialState);
48
- const previewColors = usePreviewColors(configuratorState);
51
+ const previewColors = usePreviewColors(configuratorState, useP3);
49
52
 
50
53
  const [saveStatus, setSaveStatus] = useState<SaveStatus>("saved");
51
54
  const [isPublished, setIsPublished] = useState(initialIsPublished);
@@ -78,6 +81,7 @@ export function useEditorState({
78
81
  (newState: ConfiguratorState) => {
79
82
  dispatch({ type: "LOAD_STATE", state: newState });
80
83
  initialStateRef.current = newState;
84
+ latestStateRef.current = newState;
81
85
  isInitialMount.current = true;
82
86
  },
83
87
  [dispatch],
@@ -87,6 +91,8 @@ export function useEditorState({
87
91
  if (debounceRef.current) {
88
92
  clearTimeout(debounceRef.current);
89
93
  debounceRef.current = undefined;
94
+ // Actually save the pending changes before switching
95
+ await saveDraftRef.current(latestStateRef.current);
90
96
  }
91
97
  }, []);
92
98
 
@@ -95,7 +101,7 @@ export function useEditorState({
95
101
  const {
96
102
  presets,
97
103
  activePresetId,
98
- publishedPresetId,
104
+ defaultVariantId,
99
105
  activePreset,
100
106
  switchPreset,
101
107
  createPreset,
@@ -103,12 +109,13 @@ export function useEditorState({
103
109
  deletePreset,
104
110
  duplicatePreset,
105
111
  updateActivePresetDraftState,
106
- publishActivePreset,
112
+ publishAllVariants,
107
113
  revertActivePreset,
114
+ setDefaultVariant,
108
115
  } = usePresets({
109
116
  initialPresets,
110
117
  initialActivePresetId,
111
- initialPublishedPresetId,
118
+ initialDefaultVariantId: initialPublishedPresetId,
112
119
  defaultState,
113
120
  onPresetSwitch: handlePresetSwitch,
114
121
  getCurrentState: () => latestStateRef.current,
@@ -282,13 +289,18 @@ export function useEditorState({
282
289
  [persistence, updateActivePresetDraftState],
283
290
  );
284
291
 
292
+ // Keep a ref so the debounce timer always calls the latest saveDraft
293
+ const saveDraftRef = useRef(saveDraft);
294
+ saveDraftRef.current = saveDraft;
295
+
296
+ // Stable identity — never changes, so the useEffect below won't re-fire on preset switch
285
297
  const scheduleSave = useCallback(() => {
286
298
  setSaveStatus("unsaved");
287
299
  if (debounceRef.current) clearTimeout(debounceRef.current);
288
300
  debounceRef.current = setTimeout(() => {
289
- saveDraft(latestStateRef.current);
301
+ saveDraftRef.current(latestStateRef.current);
290
302
  }, 2000);
291
- }, [saveDraft]);
303
+ }, []);
292
304
 
293
305
  // --- State change detection ---
294
306
 
@@ -324,7 +336,7 @@ export function useEditorState({
324
336
  await Promise.race([
325
337
  (async () => {
326
338
  const currentState = latestStateRef.current;
327
- const updatedPresets = publishActivePreset(currentState);
339
+ const updatedPresets = publishAllVariants(currentState);
328
340
  const [calibrations, fontMetrics] = await Promise.all([
329
341
  measureFontCalibrations(currentState.typography?.fonts),
330
342
  lookupFontMetrics(currentState.typography?.fonts, manifestUrl),
@@ -334,6 +346,7 @@ export function useEditorState({
334
346
  state: currentState,
335
347
  presets: updatedPresets,
336
348
  activePresetId,
349
+ defaultVariantId: defaultVariantId ?? activePresetId,
337
350
  calibrations,
338
351
  fontMetrics,
339
352
  });
@@ -351,7 +364,7 @@ export function useEditorState({
351
364
  } finally {
352
365
  setPublishing(false);
353
366
  }
354
- }, [activePresetId, publishActivePreset, persistence, manifestUrl]);
367
+ }, [activePresetId, defaultVariantId, publishAllVariants, persistence, manifestUrl]);
355
368
 
356
369
  // --- beforeunload warning ---
357
370
  useEffect(() => {
@@ -424,12 +437,13 @@ export function useEditorState({
424
437
  // Presets
425
438
  presets,
426
439
  activePresetId,
427
- publishedPresetId,
440
+ defaultVariantId,
428
441
  switchPreset,
429
442
  createPreset,
430
443
  renamePreset,
431
444
  deletePreset,
432
445
  duplicatePreset,
446
+ setDefaultVariant,
433
447
 
434
448
  // Revert
435
449
  isDirty,