@newtonedev/editor 0.1.11 → 0.2.0
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.map +1 -1
- package/dist/components/CodeBlock.d.ts.map +1 -1
- package/dist/components/ConfiguratorPanel.d.ts +6 -3
- package/dist/components/ConfiguratorPanel.d.ts.map +1 -1
- package/dist/components/EditorHeader.d.ts +3 -2
- package/dist/components/EditorHeader.d.ts.map +1 -1
- package/dist/components/EditorShell.d.ts.map +1 -1
- package/dist/components/PresetSelector.d.ts +3 -2
- package/dist/components/PresetSelector.d.ts.map +1 -1
- package/dist/components/PreviewWindow.d.ts.map +1 -1
- package/dist/components/RightSidebar.d.ts.map +1 -1
- package/dist/components/Sidebar.d.ts +8 -1
- package/dist/components/Sidebar.d.ts.map +1 -1
- package/dist/components/TableOfContents.d.ts.map +1 -1
- package/dist/components/sections/ColorsSection.d.ts +6 -3
- package/dist/components/sections/ColorsSection.d.ts.map +1 -1
- package/dist/components/sections/DynamicRangeSection.d.ts +2 -2
- package/dist/components/sections/DynamicRangeSection.d.ts.map +1 -1
- package/dist/components/sections/FontsSection.d.ts +2 -2
- package/dist/components/sections/FontsSection.d.ts.map +1 -1
- package/dist/components/sections/IconsSection.d.ts +2 -2
- package/dist/components/sections/IconsSection.d.ts.map +1 -1
- package/dist/components/sections/OthersSection.d.ts +2 -2
- package/dist/components/sections/OthersSection.d.ts.map +1 -1
- package/dist/components/sections/ScalePlots.d.ts +11 -0
- package/dist/components/sections/ScalePlots.d.ts.map +1 -0
- package/dist/components/sections/index.d.ts +1 -0
- package/dist/components/sections/index.d.ts.map +1 -1
- package/dist/configurator/bridge/toCSS.d.ts +7 -0
- package/dist/configurator/bridge/toCSS.d.ts.map +1 -0
- package/dist/configurator/bridge/toJSON.d.ts +15 -0
- package/dist/configurator/bridge/toJSON.d.ts.map +1 -0
- package/dist/configurator/bridge/toThemeConfig.d.ts +8 -0
- package/dist/configurator/bridge/toThemeConfig.d.ts.map +1 -0
- package/dist/configurator/constants.d.ts +13 -0
- package/dist/configurator/constants.d.ts.map +1 -0
- package/dist/configurator/hex-conversion.d.ts +21 -0
- package/dist/configurator/hex-conversion.d.ts.map +1 -0
- package/dist/configurator/hooks/useConfigurator.d.ts +11 -0
- package/dist/configurator/hooks/useConfigurator.d.ts.map +1 -0
- package/dist/configurator/hooks/usePreviewColors.d.ts +8 -0
- package/dist/configurator/hooks/usePreviewColors.d.ts.map +1 -0
- package/dist/configurator/hooks/useWcagValidation.d.ts +20 -0
- package/dist/configurator/hooks/useWcagValidation.d.ts.map +1 -0
- package/dist/configurator/hue-conversion.d.ts +10 -0
- package/dist/configurator/hue-conversion.d.ts.map +1 -0
- package/dist/configurator/state/actions.d.ts +107 -0
- package/dist/configurator/state/actions.d.ts.map +1 -0
- package/dist/configurator/state/defaults.d.ts +7 -0
- package/dist/configurator/state/defaults.d.ts.map +1 -0
- package/dist/configurator/state/reducer.d.ts +19 -0
- package/dist/configurator/state/reducer.d.ts.map +1 -0
- package/dist/configurator/types.d.ts +60 -0
- package/dist/configurator/types.d.ts.map +1 -0
- package/dist/hooks/useEditorState.d.ts +8 -6
- package/dist/hooks/useEditorState.d.ts.map +1 -1
- package/dist/hooks/usePresets.d.ts +7 -6
- package/dist/hooks/usePresets.d.ts.map +1 -1
- package/dist/index.cjs +30380 -828
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30359 -819
- package/dist/index.js.map +1 -1
- package/dist/preview/CategoryView.d.ts.map +1 -1
- package/dist/preview/ComponentDetailView.d.ts.map +1 -1
- package/dist/preview/ComponentRenderer.d.ts.map +1 -1
- package/dist/preview/IconBrowserView.d.ts.map +1 -1
- package/dist/preview/OverviewView.d.ts.map +1 -1
- package/dist/preview/PaletteScaleView.d.ts +11 -0
- package/dist/preview/PaletteScaleView.d.ts.map +1 -0
- package/dist/types.d.ts +4 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -4
- package/src/Editor.tsx +43 -19
- package/src/components/CodeBlock.tsx +7 -11
- package/src/components/ConfiguratorPanel.tsx +25 -18
- package/src/components/EditorHeader.tsx +29 -39
- package/src/components/EditorShell.tsx +17 -29
- package/src/components/FontPicker.tsx +7 -7
- package/src/components/PresetSelector.tsx +211 -129
- package/src/components/PreviewWindow.tsx +5 -12
- package/src/components/PrimaryNav.tsx +6 -6
- package/src/components/RightSidebar.tsx +24 -25
- package/src/components/Sidebar.tsx +54 -60
- package/src/components/TableOfContents.tsx +4 -5
- package/src/components/sections/ColorsSection.tsx +118 -147
- package/src/components/sections/DynamicRangeSection.tsx +61 -75
- package/src/components/sections/FontsSection.tsx +17 -28
- package/src/components/sections/IconsSection.tsx +2 -2
- package/src/components/sections/OthersSection.tsx +4 -5
- package/src/components/sections/ScalePlots.tsx +221 -0
- package/src/components/sections/index.ts +1 -0
- package/src/configurator/bridge/toCSS.ts +44 -0
- package/src/configurator/bridge/toJSON.ts +24 -0
- package/src/configurator/bridge/toThemeConfig.ts +114 -0
- package/src/configurator/constants.ts +13 -0
- package/src/configurator/hex-conversion.ts +67 -0
- package/src/configurator/hooks/useConfigurator.ts +33 -0
- package/src/configurator/hooks/usePreviewColors.ts +47 -0
- package/src/configurator/hooks/useWcagValidation.ts +133 -0
- package/src/configurator/hue-conversion.ts +25 -0
- package/src/configurator/state/actions.ts +43 -0
- package/src/configurator/state/defaults.ts +107 -0
- package/src/configurator/state/reducer.ts +399 -0
- package/src/configurator/types.ts +65 -0
- package/src/hooks/useEditorState.ts +25 -11
- package/src/hooks/usePresets.ts +54 -33
- package/src/index.ts +33 -0
- package/src/preview/CategoryView.tsx +8 -11
- package/src/preview/ComponentDetailView.tsx +24 -54
- package/src/preview/ComponentRenderer.tsx +2 -4
- package/src/preview/IconBrowserView.tsx +9 -10
- package/src/preview/OverviewView.tsx +9 -12
- package/src/preview/PaletteScaleView.tsx +122 -0
- 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 "
|
|
5
|
-
import { useConfigurator
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
301
|
+
saveDraftRef.current(latestStateRef.current);
|
|
290
302
|
}, 2000);
|
|
291
|
-
}, [
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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,
|