@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
@@ -2,7 +2,6 @@ import { useState, useMemo, useCallback, useEffect } from "react";
2
2
  import {
3
3
  HueSlider,
4
4
  Slider,
5
- Select,
6
5
  Toggle,
7
6
  ColorScaleSlider,
8
7
  TextInput,
@@ -10,40 +9,42 @@ import {
10
9
  } from "@newtonedev/components";
11
10
  import type { ColorMode } from "@newtonedev/components";
12
11
  import { srgbToHex } from "newtone";
13
- import type { ColorResult, DynamicRange } from "newtone";
14
- import type { HueGradingStrength } from "newtone";
15
- import type { ConfiguratorState } from "@newtonedev/configurator";
16
- import type { ConfiguratorAction } from "@newtonedev/configurator";
17
- import {
18
- SEMANTIC_HUE_RANGES,
19
- useWcagValidation,
20
- hexToPaletteParams,
21
- traditionalHueToOklch,
22
- } from "@newtonedev/configurator";
23
-
24
- const STRENGTH_OPTIONS = [
25
- { label: "None", value: "none" },
26
- { label: "Low", value: "low" },
27
- { label: "Medium", value: "medium" },
28
- { label: "Hard", value: "hard" },
29
- ];
30
-
12
+ import type { ColorResult } from "newtone";
13
+ import { oklchToP3 } from "@newtonedev/colors";
14
+ import type { ConfiguratorState } from "../../configurator/types";
15
+ import type { ConfiguratorAction } from "../../configurator/state/actions";
16
+ import { SEMANTIC_HUE_RANGES } from "../../configurator/constants";
17
+ import { useWcagValidation } from "../../configurator/hooks/useWcagValidation";
18
+ import { hexToPaletteParams } from "../../configurator/hex-conversion";
31
19
  interface ColorsSectionProps {
32
20
  readonly state: ConfiguratorState;
33
21
  readonly dispatch: (action: ConfiguratorAction) => void;
34
22
  readonly previewColors: readonly (readonly ColorResult[])[];
35
23
  readonly colorMode: ColorMode;
36
24
  readonly onColorModeChange: (mode: ColorMode) => void;
25
+ readonly activePaletteIndex: number;
26
+ readonly onActivePaletteChange: (index: number) => void;
27
+ readonly useP3: boolean;
28
+ }
29
+
30
+ /** Format a ColorResult as a CSS color string in the appropriate gamut. */
31
+ function colorToCss(color: ColorResult, useP3: boolean): string {
32
+ if (!useP3) return srgbToHex(color.srgb);
33
+ const p3 = oklchToP3(color.oklch);
34
+ const r = Math.max(0, Math.min(1, p3.r));
35
+ const g = Math.max(0, Math.min(1, p3.g));
36
+ const b = Math.max(0, Math.min(1, p3.b));
37
+ return `color(display-p3 ${r.toFixed(5)} ${g.toFixed(5)} ${b.toFixed(5)})`;
37
38
  }
38
39
 
39
- /** Get the hex color at a normalizedValue from the preview colors array */
40
- function getHexAtNv(
41
- previewColors: readonly ColorResult[],
42
- nv: number,
43
- ): string {
44
- const idx = Math.round((1 - nv) * (previewColors.length - 1));
45
- const clamped = Math.max(0, Math.min(previewColors.length - 1, idx));
46
- return srgbToHex(previewColors[clamped].srgb);
40
+ /** Convert step index [0=lightest, 25=darkest] to NV [0=darkest, 1=lightest] for ColorScaleSlider */
41
+ function stepToNv(step: number): number {
42
+ return 1 - Math.max(0, Math.min(25, step)) / 25;
43
+ }
44
+
45
+ /** Convert NV [0=darkest, 1=lightest] to step index [0=lightest, 25=darkest] */
46
+ function nvToStep(nv: number): number {
47
+ return Math.round((1 - nv) * 25);
47
48
  }
48
49
 
49
50
  export function ColorsSection({
@@ -52,31 +53,33 @@ export function ColorsSection({
52
53
  previewColors,
53
54
  colorMode,
54
55
  onColorModeChange,
56
+ activePaletteIndex,
57
+ onActivePaletteChange,
58
+ useP3,
55
59
  }: ColorsSectionProps) {
56
60
  const tokens = useTokens();
57
- const [activePaletteIndex, setActivePaletteIndex] = useState(0);
58
61
  const [modeToggleHovered, setModeToggleHovered] = useState(false);
59
62
 
60
63
  const palette = state.palettes[activePaletteIndex];
61
64
  const hueRange = SEMANTIC_HUE_RANGES[activePaletteIndex];
62
- const isNeutral = activePaletteIndex === 0;
65
+ const isPrimary = activePaletteIndex === 0;
63
66
 
64
- const activeColor = srgbToHex(tokens.accent.fill.srgb);
65
- const borderColor = srgbToHex(tokens.border.srgb);
67
+ const activeColor = tokens.colors.secondary.emphasis.fontPrimary;
68
+ const borderColor = tokens.colors.primary.main.fontDisabled;
66
69
 
67
- // Resolve effective key color for current mode
68
- const effectiveKeyColor =
69
- colorMode === "dark" ? palette.keyColorDark : palette.keyColor;
70
+ // Resolve effective key color step for current mode
71
+ const effectiveKeyStep =
72
+ colorMode === "dark" ? palette.keyColorStepDark : palette.keyColorStep;
70
73
 
71
74
  // Mode-aware action types
72
75
  const setKeyColorAction =
73
76
  colorMode === "dark"
74
- ? ("SET_PALETTE_KEY_COLOR_DARK" as const)
75
- : ("SET_PALETTE_KEY_COLOR" as const);
77
+ ? ("SET_PALETTE_KEY_COLOR_STEP_DARK" as const)
78
+ : ("SET_PALETTE_KEY_COLOR_STEP" as const);
76
79
  const clearKeyColorAction =
77
80
  colorMode === "dark"
78
- ? ("CLEAR_PALETTE_KEY_COLOR_DARK" as const)
79
- : ("CLEAR_PALETTE_KEY_COLOR" as const);
81
+ ? ("CLEAR_PALETTE_KEY_COLOR_STEP_DARK" as const)
82
+ : ("CLEAR_PALETTE_KEY_COLOR_STEP" as const);
80
83
  const hexAction =
81
84
  colorMode === "dark"
82
85
  ? ("SET_PALETTE_FROM_HEX_DARK" as const)
@@ -99,13 +102,15 @@ export function ColorsSection({
99
102
  setIsHexUserSet(false);
100
103
  }, [colorMode]);
101
104
 
102
- // Compute displayed hex from current key color position
105
+ // Compute displayed hex from current key color step (always sRGB for text input)
103
106
  const currentPreview = previewColors[activePaletteIndex];
104
107
  const displayedHex = useMemo(() => {
105
108
  if (!currentPreview || currentPreview.length === 0) return "";
106
- const nv = effectiveKeyColor ?? wcag.autoNormalizedValue;
107
- return getHexAtNv(currentPreview, nv);
108
- }, [currentPreview, effectiveKeyColor, wcag.autoNormalizedValue]);
109
+ const step = effectiveKeyStep ?? wcag.autoStep;
110
+ // step 0=lightest (array start), step 25=darkest (array end)
111
+ const idx = Math.max(0, Math.min(currentPreview.length - 1, step));
112
+ return colorToCss(currentPreview[idx], false);
113
+ }, [currentPreview, effectiveKeyStep, wcag.autoStep]);
109
114
 
110
115
  // Sync hex text when not actively editing and not user-submitted
111
116
  useEffect(() => {
@@ -114,30 +119,6 @@ export function ColorsSection({
114
119
  }
115
120
  }, [displayedHex, isEditingHex, isHexUserSet]);
116
121
 
117
- // Build dynamic range for hex conversion
118
- const dynamicRange = useMemo((): DynamicRange => {
119
- const light =
120
- state.globalHueGrading.light.strength !== "none"
121
- ? {
122
- hue: traditionalHueToOklch(state.globalHueGrading.light.hue),
123
- strength: state.globalHueGrading.light.strength,
124
- }
125
- : undefined;
126
- const dark =
127
- state.globalHueGrading.dark.strength !== "none"
128
- ? {
129
- hue: traditionalHueToOklch(state.globalHueGrading.dark.hue),
130
- strength: state.globalHueGrading.dark.strength,
131
- }
132
- : undefined;
133
- const hueGrading = light || dark ? { light, dark } : undefined;
134
- return {
135
- lightest: state.dynamicRange.lightest,
136
- darkest: state.dynamicRange.darkest,
137
- ...(hueGrading ? { hueGrading } : {}),
138
- };
139
- }, [state.dynamicRange, state.globalHueGrading]);
140
-
141
122
  const handleHexSubmit = useCallback(() => {
142
123
  setIsEditingHex(false);
143
124
  const trimmed = hexText.trim();
@@ -147,7 +128,7 @@ export function ColorsSection({
147
128
  }
148
129
 
149
130
  const hex = trimmed.startsWith("#") ? trimmed : `#${trimmed}`;
150
- const params = hexToPaletteParams(hex, dynamicRange);
131
+ const params = hexToPaletteParams(hex, state.dynamicRange);
151
132
 
152
133
  if (!params) {
153
134
  setHexError("Invalid hex color");
@@ -160,10 +141,10 @@ export function ColorsSection({
160
141
  type: hexAction,
161
142
  index: activePaletteIndex,
162
143
  hue: params.hue,
163
- saturation: params.saturation,
164
- keyColor: params.normalizedValue,
144
+ chromaRatio: params.chromaRatio,
145
+ keyColorStep: params.stepIndex,
165
146
  });
166
- }, [hexText, dynamicRange, dispatch, activePaletteIndex, hexAction]);
147
+ }, [hexText, state.dynamicRange, dispatch, activePaletteIndex, hexAction]);
167
148
 
168
149
  const handleClearKeyColor = useCallback(() => {
169
150
  dispatch({ type: clearKeyColorAction, index: activePaletteIndex });
@@ -173,7 +154,7 @@ export function ColorsSection({
173
154
 
174
155
  // Build WCAG warning message
175
156
  const wcagWarning = useMemo(() => {
176
- if (effectiveKeyColor === undefined || wcag.keyColorContrast === null)
157
+ if (effectiveKeyStep === undefined || wcag.keyColorContrast === null)
177
158
  return undefined;
178
159
  if (wcag.passesAA) return undefined;
179
160
  const ratio = wcag.keyColorContrast.toFixed(1);
@@ -181,7 +162,9 @@ export function ColorsSection({
181
162
  return `Contrast ${ratio}:1 — passes large text (AA) but fails normal text (requires 4.5:1)`;
182
163
  }
183
164
  return `Contrast ${ratio}:1 — fails WCAG AA (requires 4.5:1 for normal text, 3:1 for large text)`;
184
- }, [effectiveKeyColor, wcag]);
165
+ }, [effectiveKeyStep, wcag]);
166
+
167
+ const localIntensity = palette.localHueGrade?.intensity ?? 0;
185
168
 
186
169
  return (
187
170
  <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
@@ -190,27 +173,28 @@ export function ColorsSection({
190
173
  {state.palettes.map((_p, index) => {
191
174
  const isActive = index === activePaletteIndex;
192
175
  const colors = previewColors[index];
193
- const isNeutralCircle = index === 0;
194
-
195
- // Per-mode key color for this palette's circle
196
- const paletteKeyColor =
197
- colorMode === "dark" ? _p.keyColorDark : _p.keyColor;
176
+ const isPrimaryCircle = index === 0;
177
+
178
+ // Per-mode key color step for this palette's circle
179
+ const paletteKeyStep =
180
+ colorMode === "dark" ? _p.keyColorStepDark : _p.keyColorStep;
181
+ const circleStep = paletteKeyStep ?? wcag.autoStep;
182
+ const circleIdx = !isPrimaryCircle && colors
183
+ ? Math.max(0, Math.min(colors.length - 1, circleStep))
184
+ : -1;
198
185
  const circleColor =
199
- !isNeutralCircle && colors
200
- ? getHexAtNv(
201
- colors,
202
- paletteKeyColor ?? wcag.autoNormalizedValue,
203
- )
186
+ !isPrimaryCircle && colors && circleIdx >= 0
187
+ ? colorToCss(colors[circleIdx], useP3)
204
188
  : undefined;
205
189
 
206
190
  const ringStyle = isActive
207
- ? `0 0 0 2px ${srgbToHex(tokens.background.srgb)}, 0 0 0 4px ${activeColor}`
191
+ ? `0 0 0 2px ${tokens.colors.primary.main.background}, 0 0 0 4px ${activeColor}`
208
192
  : "none";
209
193
 
210
194
  return (
211
195
  <button
212
196
  key={index}
213
- onClick={() => setActivePaletteIndex(index)}
197
+ onClick={() => onActivePaletteChange(index)}
214
198
  aria-label={_p.name}
215
199
  aria-pressed={isActive}
216
200
  style={{
@@ -224,10 +208,10 @@ export function ColorsSection({
224
208
  transition: "box-shadow 150ms ease",
225
209
  padding: 0,
226
210
  overflow: "hidden",
227
- ...(isNeutralCircle
211
+ ...(isPrimaryCircle
228
212
  ? {
229
213
  background: colors
230
- ? `linear-gradient(to right, ${srgbToHex(colors[0].srgb)} 50%, ${srgbToHex(colors[colors.length - 1].srgb)} 50%)`
214
+ ? `linear-gradient(to right, ${colorToCss(colors[0], useP3)} 50%, ${colorToCss(colors[colors.length - 1], useP3)} 50%)`
231
215
  : `linear-gradient(to right, #ffffff 50%, #000000 50%)`,
232
216
  }
233
217
  : { backgroundColor: circleColor ?? borderColor }),
@@ -261,18 +245,19 @@ export function ColorsSection({
261
245
  background: modeToggleHovered ? `${borderColor}20` : "none",
262
246
  cursor: "pointer",
263
247
  fontSize: 12,
264
- color: srgbToHex(tokens.textPrimary.srgb),
248
+ color: tokens.colors.primary.main.fontPrimary,
265
249
  transition: "background-color 150ms ease",
266
250
  }}
267
251
  >
268
252
  {colorMode === "light" ? "\u2600" : "\u263E"}
269
253
  <span>{colorMode === "light" ? "Light" : "Dark"}</span>
270
254
  </button>
255
+
271
256
  </div>
272
257
 
273
258
  {/* ─── Key Color (mode-specific) ─── */}
274
259
  {currentPreview &&
275
- (isNeutral ? (
260
+ (isPrimary ? (
276
261
  <div style={{ display: "flex", gap: 1 }}>
277
262
  {currentPreview.map((color, i) => (
278
263
  <div
@@ -281,7 +266,7 @@ export function ColorsSection({
281
266
  flex: 1,
282
267
  height: 64,
283
268
  borderRadius: 2,
284
- backgroundColor: srgbToHex(color.srgb),
269
+ backgroundColor: colorToCss(color, useP3),
285
270
  }}
286
271
  />
287
272
  ))}
@@ -292,13 +277,13 @@ export function ColorsSection({
292
277
  >
293
278
  <ColorScaleSlider
294
279
  colors={currentPreview}
295
- value={effectiveKeyColor ?? wcag.autoNormalizedValue}
280
+ value={stepToNv(effectiveKeyStep ?? wcag.autoStep)}
296
281
  onValueChange={(nv) => {
297
282
  setIsHexUserSet(false);
298
283
  dispatch({
299
284
  type: setKeyColorAction,
300
285
  index: activePaletteIndex,
301
- normalizedValue: nv,
286
+ step: nvToStep(nv),
302
287
  });
303
288
  }}
304
289
  trimEnds
@@ -328,7 +313,7 @@ export function ColorsSection({
328
313
  placeholder="#000000"
329
314
  />
330
315
  </div>
331
- {effectiveKeyColor !== undefined && (
316
+ {effectiveKeyStep !== undefined && (
332
317
  <button
333
318
  onClick={handleClearKeyColor}
334
319
  style={{
@@ -350,7 +335,7 @@ export function ColorsSection({
350
335
  style={{
351
336
  fontSize: 12,
352
337
  fontWeight: 500,
353
- color: srgbToHex(tokens.error.fill.srgb),
338
+ color: tokens.colors.error.emphasis.fontPrimary,
354
339
  }}
355
340
  >
356
341
  {hexError}
@@ -385,84 +370,87 @@ export function ColorsSection({
385
370
  />
386
371
 
387
372
  <Slider
388
- value={palette.saturation}
389
- onValueChange={(saturation) =>
373
+ value={Math.round(palette.chromaRatio * 100)}
374
+ onValueChange={(v) =>
390
375
  dispatch({
391
- type: "SET_PALETTE_SATURATION",
376
+ type: "SET_PALETTE_CHROMA_RATIO",
392
377
  index: activePaletteIndex,
393
- saturation,
378
+ chromaRatio: v / 100,
394
379
  })
395
380
  }
396
381
  min={0}
397
382
  max={100}
398
- label="Saturation"
383
+ label="Chroma"
399
384
  editableValue
400
385
  />
401
386
 
402
- {/* Desaturation */}
403
387
  <Slider
404
- value={Math.round((palette.desaturation ?? 0) * 100)}
388
+ value={Math.round(palette.chromaPeak * 100)}
405
389
  onValueChange={(v) =>
406
390
  dispatch({
407
- type: "SET_PALETTE_DESATURATION",
391
+ type: "SET_PALETTE_CHROMA_PEAK",
408
392
  index: activePaletteIndex,
409
- desaturation: v / 100,
393
+ chromaPeak: v / 100,
410
394
  })
411
395
  }
412
- min={-100}
396
+ min={0}
413
397
  max={100}
414
- label="Desaturation"
398
+ label="Balance"
415
399
  editableValue
400
+ disabled={palette.chromaRatio <= 0 || palette.chromaRatio >= 1}
416
401
  />
417
402
 
418
- {/* Hue Grading */}
403
+ {/* Per-palette hue grading */}
419
404
  <div style={{ display: "flex", gap: 12, alignItems: "flex-end" }}>
420
405
  <div style={{ flex: 1 }}>
421
- <Select
422
- options={STRENGTH_OPTIONS}
423
- value={palette.hueGradeStrength}
424
- onValueChange={(strength) =>
406
+ <Slider
407
+ value={Math.round(localIntensity * 100)}
408
+ onValueChange={(v) =>
425
409
  dispatch({
426
- type: "SET_PALETTE_HUE_GRADE_STRENGTH",
410
+ type: "SET_PALETTE_LOCAL_HUE_GRADE_INTENSITY",
427
411
  index: activePaletteIndex,
428
- strength: strength as HueGradingStrength,
412
+ intensity: v / 100,
429
413
  })
430
414
  }
431
- label="Hue Grading"
415
+ min={0}
416
+ max={50}
417
+ label="Shift"
418
+ editableValue
432
419
  />
433
420
  </div>
434
- {palette.hueGradeStrength !== "none" && (
421
+ {localIntensity > 0 && (
435
422
  <div style={{ paddingBottom: 2 }}>
436
423
  <Toggle
437
- value={palette.hueGradeDirection === "dark"}
424
+ value={palette.localHueGrade?.side === "dark"}
438
425
  onValueChange={(v) =>
439
426
  dispatch({
440
- type: "SET_PALETTE_HUE_GRADE_DIRECTION",
427
+ type: "SET_PALETTE_LOCAL_HUE_GRADE_SIDE",
441
428
  index: activePaletteIndex,
442
- direction: v ? "dark" : "light",
429
+ side: v ? "dark" : "light",
443
430
  })
444
431
  }
445
- label="Invert"
432
+ label="Dark"
446
433
  />
447
434
  </div>
448
435
  )}
449
436
  </div>
450
437
 
451
- {palette.hueGradeStrength !== "none" && (
438
+ {localIntensity > 0 && (
452
439
  <HueSlider
453
- value={palette.hueGradeHue}
440
+ value={palette.localHueGrade?.hue ?? palette.hue}
454
441
  onValueChange={(hue) =>
455
442
  dispatch({
456
- type: "SET_PALETTE_HUE_GRADE_HUE",
443
+ type: "SET_PALETTE_LOCAL_HUE_GRADE_HUE",
457
444
  index: activePaletteIndex,
458
445
  hue,
459
446
  })
460
447
  }
461
- label="Grade Target"
448
+ label="Shift Hue"
462
449
  editableValue
463
450
  />
464
451
  )}
465
452
  </div>
453
+
466
454
  </div>
467
455
  );
468
456
  }
@@ -1,16 +1,7 @@
1
1
  import { useState, useRef, useCallback } from "react";
2
- import { HueSlider, Select, useTokens } from "@newtonedev/components";
3
- import { srgbToHex } from "newtone";
4
- import type { HueGradingStrength } from "newtone";
5
- import type { ConfiguratorState } from "@newtonedev/configurator";
6
- import type { ConfiguratorAction } from "@newtonedev/configurator";
7
-
8
- const STRENGTH_OPTIONS = [
9
- { label: "None", value: "none" },
10
- { label: "Low", value: "low" },
11
- { label: "Medium", value: "medium" },
12
- { label: "Hard", value: "hard" },
13
- ];
2
+ import { HueSlider, Slider, useTokens } from "@newtonedev/components";
3
+ import type { ConfiguratorState } from "../../configurator/types";
4
+ import type { ConfiguratorAction } from "../../configurator/state/actions";
14
5
 
15
6
  // --- Dual Range Slider ---
16
7
 
@@ -76,8 +67,8 @@ function DualRangeSlider({
76
67
  "whites" | "blacks" | null
77
68
  >(null);
78
69
 
79
- const interactiveColor = srgbToHex(tokens.accent.fill.srgb);
80
- const borderColor = srgbToHex(tokens.border.srgb);
70
+ const interactiveColor = tokens.colors.secondary.emphasis.fontPrimary;
71
+ const borderColor = tokens.colors.primary.main.fontDisabled;
81
72
 
82
73
  const wDisplay = internalToDisplay(whitesValue);
83
74
  const bDisplay = internalToDisplay(blacksValue);
@@ -250,10 +241,10 @@ function RangeInput({ display, onCommit, toInternal }: RangeInputProps) {
250
241
  style={{
251
242
  width: 40,
252
243
  padding: "2px 6px",
253
- border: `1px solid ${srgbToHex(tokens.border.srgb)}`,
244
+ border: `1px solid ${tokens.colors.primary.main.fontDisabled}`,
254
245
  borderRadius: 4,
255
246
  backgroundColor: "transparent",
256
- color: srgbToHex(tokens.textPrimary.srgb),
247
+ color: tokens.colors.primary.main.fontPrimary,
257
248
  fontFamily: "inherit",
258
249
  fontSize: 12,
259
250
  fontWeight: 500,
@@ -276,7 +267,7 @@ export function DynamicRangeSection({
276
267
  dispatch,
277
268
  }: DynamicRangeSectionProps) {
278
269
  const tokens = useTokens();
279
- const labelColor = srgbToHex(tokens.textSecondary.srgb);
270
+ const labelColor = tokens.colors.primary.main.fontTertiary;
280
271
 
281
272
  const labelStyle = {
282
273
  fontSize: 11,
@@ -289,6 +280,9 @@ export function DynamicRangeSection({
289
280
  const wDisplay = internalToDisplay(state.dynamicRange.lightest);
290
281
  const bDisplay = internalToDisplay(state.dynamicRange.darkest);
291
282
 
283
+ const lightIntensity = state.globalHueGrading.lightIntensity;
284
+ const darkIntensity = state.globalHueGrading.darkIntensity;
285
+
292
286
  return (
293
287
  <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
294
288
  {/* Labels above slider */}
@@ -331,69 +325,61 @@ export function DynamicRangeSection({
331
325
  />
332
326
  </div>
333
327
 
334
- {/* Global Hue Grading — Light End */}
328
+ {/* Global Grading — Light End */}
335
329
  <div style={{ ...labelStyle, marginTop: 4 }}>
336
- Global Hue Grading — Light
337
- </div>
338
- <div style={{ display: "flex", gap: 12 }}>
339
- <div style={{ flex: 1 }}>
340
- <Select
341
- options={STRENGTH_OPTIONS}
342
- value={state.globalHueGrading.light.strength}
343
- onValueChange={(s) =>
344
- dispatch({
345
- type: "SET_GLOBAL_GRADE_LIGHT_STRENGTH",
346
- strength: s as HueGradingStrength,
347
- })
348
- }
349
- label="Strength"
350
- />
351
- </div>
352
- {state.globalHueGrading.light.strength !== "none" && (
353
- <div style={{ flex: 1 }}>
354
- <HueSlider
355
- value={state.globalHueGrading.light.hue}
356
- onValueChange={(hue) =>
357
- dispatch({ type: "SET_GLOBAL_GRADE_LIGHT_HUE", hue })
358
- }
359
- label="Target Hue"
360
- showValue
361
- />
362
- </div>
363
- )}
330
+ Grading — Light
364
331
  </div>
332
+ <Slider
333
+ value={Math.round(lightIntensity * 100)}
334
+ onValueChange={(v) =>
335
+ dispatch({
336
+ type: "SET_GLOBAL_GRADE_LIGHT_INTENSITY",
337
+ intensity: v / 100,
338
+ })
339
+ }
340
+ min={0}
341
+ max={25}
342
+ label="Intensity"
343
+ editableValue
344
+ />
345
+ {lightIntensity > 0 && (
346
+ <HueSlider
347
+ value={state.globalHueGrading.lightHue}
348
+ onValueChange={(hue) =>
349
+ dispatch({ type: "SET_GLOBAL_GRADE_LIGHT_HUE", hue })
350
+ }
351
+ label="Target Hue"
352
+ showValue
353
+ />
354
+ )}
365
355
 
366
- {/* Global Hue Grading — Dark End */}
356
+ {/* Global Grading — Dark End */}
367
357
  <div style={{ ...labelStyle, marginTop: 4 }}>
368
- Global Hue Grading — Dark
369
- </div>
370
- <div style={{ display: "flex", gap: 12 }}>
371
- <div style={{ flex: 1 }}>
372
- <Select
373
- options={STRENGTH_OPTIONS}
374
- value={state.globalHueGrading.dark.strength}
375
- onValueChange={(s) =>
376
- dispatch({
377
- type: "SET_GLOBAL_GRADE_DARK_STRENGTH",
378
- strength: s as HueGradingStrength,
379
- })
380
- }
381
- label="Strength"
382
- />
383
- </div>
384
- {state.globalHueGrading.dark.strength !== "none" && (
385
- <div style={{ flex: 1 }}>
386
- <HueSlider
387
- value={state.globalHueGrading.dark.hue}
388
- onValueChange={(hue) =>
389
- dispatch({ type: "SET_GLOBAL_GRADE_DARK_HUE", hue })
390
- }
391
- label="Target Hue"
392
- showValue
393
- />
394
- </div>
395
- )}
358
+ Grading — Dark
396
359
  </div>
360
+ <Slider
361
+ value={Math.round(darkIntensity * 100)}
362
+ onValueChange={(v) =>
363
+ dispatch({
364
+ type: "SET_GLOBAL_GRADE_DARK_INTENSITY",
365
+ intensity: v / 100,
366
+ })
367
+ }
368
+ min={0}
369
+ max={25}
370
+ label="Intensity"
371
+ editableValue
372
+ />
373
+ {darkIntensity > 0 && (
374
+ <HueSlider
375
+ value={state.globalHueGrading.darkHue}
376
+ onValueChange={(hue) =>
377
+ dispatch({ type: "SET_GLOBAL_GRADE_DARK_HUE", hue })
378
+ }
379
+ label="Target Hue"
380
+ showValue
381
+ />
382
+ )}
397
383
  </div>
398
384
  );
399
385
  }