@newtonedev/editor 0.1.12 → 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.
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
@@ -1,9 +1,7 @@
1
- import { Slider } from "@newtonedev/components";
2
- import { useTokens } from "@newtonedev/components";
1
+ import { Slider, Wrapper, Text } from "@newtonedev/components";
3
2
  import type { FontConfig } from "@newtonedev/components";
4
- import { srgbToHex } from "newtone";
5
- import type { ConfiguratorState, FontScope, FontSlotConfig } from "@newtonedev/configurator";
6
- import type { ConfiguratorAction } from "@newtonedev/configurator";
3
+ import type { ConfiguratorState, FontScope, FontSlotConfig } from "../../configurator/types";
4
+ import type { ConfiguratorAction } from "../../configurator/state/actions";
7
5
  import type { GoogleFontEntry } from "@newtonedev/fonts";
8
6
  import { FontPicker } from "../FontPicker";
9
7
 
@@ -45,10 +43,6 @@ const FONT_SCOPES: readonly { scope: FontScope; label: string; slot: "default" |
45
43
  ];
46
44
 
47
45
  export function FontsSection({ state, dispatch, fontCatalog }: FontsSectionProps) {
48
- const tokens = useTokens();
49
-
50
- const labelColor = srgbToHex(tokens.textSecondary.srgb);
51
-
52
46
  const handleFontChange = (scope: FontScope, font: FontConfig) => {
53
47
  // Preserve existing weight slots when switching font family
54
48
  const weights = state.typography?.fonts[scope]?.weights ?? { regular: 400, medium: 500, bold: 700 };
@@ -56,20 +50,13 @@ export function FontsSection({ state, dispatch, fontCatalog }: FontsSectionProps
56
50
  dispatch({ type: "SET_FONT", scope, font: slotConfig });
57
51
  };
58
52
 
59
- const sectionLabelStyle = {
60
- fontSize: 11,
61
- fontWeight: 600 as const,
62
- color: labelColor,
63
- textTransform: "uppercase" as const,
64
- letterSpacing: 0.5,
65
- marginBottom: 8,
66
- };
67
-
68
53
  return (
69
- <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
70
- <div>
71
- <div style={sectionLabelStyle}>Fonts</div>
72
- <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
54
+ <Wrapper direction="vertical" gap="16">
55
+ <Wrapper direction="vertical">
56
+ <Text role="caption" color="tertiary" style={{ textTransform: "uppercase", letterSpacing: 0.5, marginBottom: 8 }}>
57
+ Fonts
58
+ </Text>
59
+ <Wrapper direction="vertical" gap="12">
73
60
  {FONT_SCOPES.map(({ scope, label, slot }) => (
74
61
  <FontPicker
75
62
  key={scope}
@@ -80,10 +67,12 @@ export function FontsSection({ state, dispatch, fontCatalog }: FontsSectionProps
80
67
  fontCatalog={fontCatalog}
81
68
  />
82
69
  ))}
83
- </div>
84
- </div>
85
- <div>
86
- <div style={sectionLabelStyle}>Type Scale</div>
70
+ </Wrapper>
71
+ </Wrapper>
72
+ <Wrapper direction="vertical">
73
+ <Text role="caption" color="tertiary" style={{ textTransform: "uppercase", letterSpacing: 0.5, marginBottom: 8 }}>
74
+ Type Scale
75
+ </Text>
87
76
  <Slider
88
77
  value={Math.round((state.typography?.typeScaleOffset ?? 0.5) * 100)}
89
78
  onValueChange={(v) =>
@@ -94,7 +83,7 @@ export function FontsSection({ state, dispatch, fontCatalog }: FontsSectionProps
94
83
  label="Scale"
95
84
  showValue
96
85
  />
97
- </div>
98
- </div>
86
+ </Wrapper>
87
+ </Wrapper>
99
88
  );
100
89
  }
@@ -1,6 +1,6 @@
1
1
  import { Select } from "@newtonedev/components";
2
- import type { ConfiguratorState } from "@newtonedev/configurator";
3
- import type { ConfiguratorAction } from "@newtonedev/configurator";
2
+ import type { ConfiguratorState } from "../../configurator/types";
3
+ import type { ConfiguratorAction } from "../../configurator/state/actions";
4
4
 
5
5
  const ICON_VARIANT_OPTIONS = [
6
6
  { label: "Outlined", value: "outlined" },
@@ -1,7 +1,6 @@
1
1
  import { Slider, Select, useTokens } from "@newtonedev/components";
2
- import { srgbToHex } from "newtone";
3
- import type { ConfiguratorState, SpacingPreset } from "@newtonedev/configurator";
4
- import type { ConfiguratorAction } from "@newtonedev/configurator";
2
+ import type { ConfiguratorState, SpacingPreset } from "../../configurator/types";
3
+ import type { ConfiguratorAction } from "../../configurator/state/actions";
5
4
 
6
5
  interface OthersSectionProps {
7
6
  readonly state: ConfiguratorState;
@@ -29,7 +28,7 @@ export function OthersSection({ state, dispatch }: OthersSectionProps) {
29
28
  style={{
30
29
  fontSize: 11,
31
30
  fontWeight: 600,
32
- color: srgbToHex(tokens.textSecondary.srgb),
31
+ color: tokens.colors.primary.main.fontTertiary,
33
32
  textTransform: "uppercase",
34
33
  letterSpacing: 0.5,
35
34
  marginBottom: 8,
@@ -52,7 +51,7 @@ export function OthersSection({ state, dispatch }: OthersSectionProps) {
52
51
  style={{
53
52
  fontSize: 11,
54
53
  fontWeight: 600,
55
- color: srgbToHex(tokens.textSecondary.srgb),
54
+ color: tokens.colors.primary.main.fontTertiary,
56
55
  textTransform: "uppercase",
57
56
  letterSpacing: 0.5,
58
57
  marginBottom: 8,
@@ -0,0 +1,221 @@
1
+ import { maxChroma, resolveGradedHue, buildOneSidedGrade, resolveLightest, resolveDarkest } from "@newtonedev/colors";
2
+ import type { Grading } from "@newtonedev/colors";
3
+ import type { ColorResult } from "newtone";
4
+ import type { ConfiguratorState } from "../../configurator/types";
5
+
6
+ const SVG_W = 200;
7
+ const SVG_H = 140;
8
+ const PAD = { top: 8, right: 8, bottom: 20, left: 28 };
9
+
10
+ interface ScalePlotsProps {
11
+ readonly state: ConfiguratorState;
12
+ readonly scale: readonly ColorResult[];
13
+ readonly activePaletteIndex: number;
14
+ readonly useP3: boolean;
15
+ }
16
+
17
+ /** CSS oklch color for a given OKLCH hue — used for plot line/segment colors. */
18
+ function hueColor(h: number): string {
19
+ return `oklch(65% 0.2 ${h.toFixed(1)}deg)`;
20
+ }
21
+
22
+ export function ScalePlots({ state, scale, activePaletteIndex, useP3 }: ScalePlotsProps) {
23
+ if (!scale || scale.length < 2) return null;
24
+
25
+ const palette = state.palettes[activePaletteIndex];
26
+ const pw = SVG_W - PAD.left - PAD.right;
27
+ const ph = SVG_H - PAD.top - PAD.bottom;
28
+
29
+ const lightestL = resolveLightest(state.dynamicRange.lightest);
30
+ const darkestL = resolveDarkest(state.dynamicRange.darkest);
31
+ const lRange = lightestL - darkestL || 1;
32
+
33
+ function toX(L: number): number {
34
+ return PAD.left + ((lightestL - L) / lRange) * pw;
35
+ }
36
+
37
+ // ── Hue Plot ──
38
+
39
+ const baseY = PAD.top + ph - (palette.hue / 360) * ph;
40
+
41
+ const hueSegments = scale.slice(1).map((step, i) => {
42
+ const prev = scale[i];
43
+ const midH = (prev.oklch.h + step.oklch.h) / 2;
44
+ return (
45
+ <line
46
+ key={i}
47
+ x1={toX(prev.oklch.L).toFixed(1)}
48
+ y1={(PAD.top + ph - (prev.oklch.h / 360) * ph).toFixed(1)}
49
+ x2={toX(step.oklch.L).toFixed(1)}
50
+ y2={(PAD.top + ph - (step.oklch.h / 360) * ph).toFixed(1)}
51
+ stroke={hueColor(midH)}
52
+ strokeWidth="1.5"
53
+ strokeLinecap="round"
54
+ />
55
+ );
56
+ });
57
+
58
+ const g = state.globalHueGrading;
59
+ const gradeRefLines: React.ReactNode[] = [];
60
+ if (g.lightIntensity > 0) {
61
+ const h = g.lightHue;
62
+ const y = (PAD.top + ph - (h / 360) * ph).toFixed(1);
63
+ gradeRefLines.push(
64
+ <line key="gl" x1={PAD.left} y1={y} x2={PAD.left + pw} y2={y}
65
+ stroke={hueColor(h)} strokeWidth="0.75" strokeDasharray="3 2" opacity="0.7" />
66
+ );
67
+ }
68
+ if (g.darkIntensity > 0) {
69
+ const h = g.darkHue;
70
+ const y = (PAD.top + ph - (h / 360) * ph).toFixed(1);
71
+ gradeRefLines.push(
72
+ <line key="gd" x1={PAD.left} y1={y} x2={PAD.left + pw} y2={y}
73
+ stroke={hueColor(h)} strokeWidth="0.75" strokeDasharray="3 2" opacity="0.7" />
74
+ );
75
+ }
76
+ const lg = palette.localHueGrade;
77
+ if (lg && lg.intensity > 0) {
78
+ const h = lg.hue;
79
+ const y = (PAD.top + ph - (h / 360) * ph).toFixed(1);
80
+ gradeRefLines.push(
81
+ <line key="ll" x1={PAD.left} y1={y} x2={PAD.left + pw} y2={y}
82
+ stroke={hueColor(h)} strokeWidth="0.75" strokeDasharray="2 3" opacity="0.6" />
83
+ );
84
+ }
85
+
86
+ // ── Chroma Plot ──
87
+
88
+ // Build grading objects for boundary hue resolution
89
+ const globalGrade: Grading | undefined =
90
+ (g.lightIntensity > 0 || g.darkIntensity > 0)
91
+ ? {
92
+ ...(g.lightIntensity > 0 ? { light: { hue: g.lightHue, amount: g.lightIntensity } } : {}),
93
+ ...(g.darkIntensity > 0 ? { dark: { hue: g.darkHue, amount: g.darkIntensity } } : {}),
94
+ }
95
+ : undefined;
96
+
97
+ const localGrade = lg && lg.intensity > 0
98
+ ? buildOneSidedGrade(lg.hue, lg.intensity, lg.side === "light")
99
+ : undefined;
100
+
101
+ // Dense-sample both gamut boundaries (60 points)
102
+ const BOUNDARY_SAMPLES = 60;
103
+ let maxC = 0;
104
+ const srgbBoundary: { x: number; y: number }[] = [];
105
+ const p3Boundary: { x: number; y: number }[] = [];
106
+ for (let i = 0; i <= BOUNDARY_SAMPLES; i++) {
107
+ const t = i / BOUNDARY_SAMPLES;
108
+ const L = lightestL - t * lRange;
109
+ const h = resolveGradedHue(palette.hue, t, globalGrade, localGrade);
110
+ const sC = maxChroma(L, h, "srgb");
111
+ const pC = maxChroma(L, h, "display-p3");
112
+ if (pC > maxC) maxC = pC;
113
+ const x = toX(L);
114
+ srgbBoundary.push({ x, y: sC });
115
+ p3Boundary.push({ x, y: pC });
116
+ }
117
+ if (maxC < 0.01) maxC = 0.01;
118
+
119
+ function toBoundaryPath(pts: { x: number; y: number }[]): string {
120
+ return pts.map((pt, i) => {
121
+ const y = (PAD.top + ph - (pt.y / maxC) * ph).toFixed(1);
122
+ return `${i === 0 ? "M" : "L"}${pt.x.toFixed(1)},${y}`;
123
+ }).join(" ");
124
+ }
125
+
126
+ const srgbPath = toBoundaryPath(srgbBoundary);
127
+ const p3Path = toBoundaryPath(p3Boundary);
128
+
129
+ const chromaSegments = scale.slice(1).map((step, i) => {
130
+ const prev = scale[i];
131
+ const midH = (prev.oklch.h + step.oklch.h) / 2;
132
+ return (
133
+ <line
134
+ key={i}
135
+ x1={toX(prev.oklch.L).toFixed(1)}
136
+ y1={(PAD.top + ph - (prev.oklch.C / maxC) * ph).toFixed(1)}
137
+ x2={toX(step.oklch.L).toFixed(1)}
138
+ y2={(PAD.top + ph - (step.oklch.C / maxC) * ph).toFixed(1)}
139
+ stroke={hueColor(midH)}
140
+ strokeWidth="1.5"
141
+ strokeLinecap="round"
142
+ />
143
+ );
144
+ });
145
+
146
+ const labelStyle: React.CSSProperties = {
147
+ fontSize: 11,
148
+ fontWeight: 600,
149
+ color: "currentColor",
150
+ opacity: 0.5,
151
+ letterSpacing: 0.3,
152
+ textTransform: "uppercase" as const,
153
+ marginBottom: 4,
154
+ };
155
+
156
+ const axisStyle = { stroke: "currentColor", strokeWidth: 0.75, opacity: 0.25 };
157
+ const textStyle: React.SVGProps<SVGTextElement> = {
158
+ fontSize: 8,
159
+ fill: "currentColor",
160
+ opacity: 0.5,
161
+ };
162
+
163
+ return (
164
+ <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
165
+ {/* Hue plot */}
166
+ <div>
167
+ <div style={labelStyle}>Hue</div>
168
+ <svg
169
+ viewBox={`0 0 ${SVG_W} ${SVG_H}`}
170
+ style={{ width: "100%", display: "block" }}
171
+ preserveAspectRatio="xMidYMid meet"
172
+ >
173
+ {/* Axes */}
174
+ <line x1={PAD.left} y1={PAD.top} x2={PAD.left} y2={PAD.top + ph} {...axisStyle} />
175
+ <line x1={PAD.left} y1={PAD.top + ph} x2={PAD.left + pw} y2={PAD.top + ph} {...axisStyle} />
176
+ {/* Base hue reference */}
177
+ <line
178
+ x1={PAD.left} y1={baseY.toFixed(1)}
179
+ x2={PAD.left + pw} y2={baseY.toFixed(1)}
180
+ stroke={hueColor(palette.hue)} strokeWidth="0.75" opacity="0.5"
181
+ />
182
+ {gradeRefLines}
183
+ {hueSegments}
184
+ {/* Labels */}
185
+ <text {...textStyle} x={PAD.left + pw / 2} y={SVG_H - 2} textAnchor="middle">L</text>
186
+ <text {...textStyle} x={3} y={PAD.top + ph / 2} textAnchor="middle"
187
+ transform={`rotate(-90, 3, ${PAD.top + ph / 2})`}>H</text>
188
+ <text {...textStyle} x={PAD.left - 3} y={PAD.top + 3} textAnchor="end">360°</text>
189
+ <text {...textStyle} x={PAD.left - 3} y={PAD.top + ph + 3} textAnchor="end">0°</text>
190
+ </svg>
191
+ </div>
192
+
193
+ {/* Chroma plot */}
194
+ <div>
195
+ <div style={labelStyle}>Chroma</div>
196
+ <svg
197
+ viewBox={`0 0 ${SVG_W} ${SVG_H}`}
198
+ style={{ width: "100%", display: "block" }}
199
+ preserveAspectRatio="xMidYMid meet"
200
+ >
201
+ {/* Axes */}
202
+ <line x1={PAD.left} y1={PAD.top} x2={PAD.left} y2={PAD.top + ph} {...axisStyle} />
203
+ <line x1={PAD.left} y1={PAD.top + ph} x2={PAD.left + pw} y2={PAD.top + ph} {...axisStyle} />
204
+ {/* P3 gamut boundary */}
205
+ <path d={p3Path} fill="none" stroke="currentColor" strokeWidth="0.75"
206
+ opacity={useP3 ? 0.2 : 0.1} strokeDasharray={useP3 ? undefined : "3 2"} />
207
+ {/* sRGB gamut boundary */}
208
+ <path d={srgbPath} fill="none" stroke="currentColor" strokeWidth="0.75"
209
+ opacity={useP3 ? 0.1 : 0.2} strokeDasharray={useP3 ? "3 2" : undefined} />
210
+ {chromaSegments}
211
+ {/* Labels */}
212
+ <text {...textStyle} x={PAD.left + pw / 2} y={SVG_H - 2} textAnchor="middle">L</text>
213
+ <text {...textStyle} x={3} y={PAD.top + ph / 2} textAnchor="middle"
214
+ transform={`rotate(-90, 3, ${PAD.top + ph / 2})`}>C</text>
215
+ <text {...textStyle} x={PAD.left - 3} y={PAD.top + 3} textAnchor="end">{maxC.toFixed(2)}</text>
216
+ <text {...textStyle} x={PAD.left - 3} y={PAD.top + ph + 3} textAnchor="end">0</text>
217
+ </svg>
218
+ </div>
219
+ </div>
220
+ );
221
+ }
@@ -1,4 +1,5 @@
1
1
  export { ColorsSection } from "./ColorsSection";
2
+ export { ScalePlots } from "./ScalePlots";
2
3
  export { DynamicRangeSection } from "./DynamicRangeSection";
3
4
  export { IconsSection } from "./IconsSection";
4
5
  export { FontsSection } from "./FontsSection";
@@ -0,0 +1,44 @@
1
+ import type { ConfiguratorState } from '../types';
2
+ import type { ColorMode } from '@newtonedev/components';
3
+ import { computeTokens } from '@newtonedev/components';
4
+ import { toThemeConfig } from './toThemeConfig';
5
+
6
+ /**
7
+ * Generate CSS custom properties for both light and dark modes.
8
+ * Computes tokens for the primary theme at grounded elevation in sRGB gamut.
9
+ */
10
+ export function toCSS(state: ConfiguratorState): string {
11
+ const config = toThemeConfig(state);
12
+ const modes: readonly ColorMode[] = ['light', 'dark'];
13
+
14
+ let css = '';
15
+ for (const mode of modes) {
16
+ const tokens = computeTokens(
17
+ config.colorSystem,
18
+ mode,
19
+ 'srgb',
20
+ 'grounded',
21
+ config.spacing,
22
+ config.radius,
23
+ config.typography,
24
+ config.icons,
25
+ );
26
+
27
+ const c = tokens.colors;
28
+ const selector = mode === 'light' ? ':root' : '[data-theme="dark"]';
29
+ css += `${selector} {\n`;
30
+ css += ` --newtone-background: ${c.primary.main.background};\n`;
31
+ css += ` --newtone-text-primary: ${c.primary.main.fontPrimary};\n`;
32
+ css += ` --newtone-text-secondary: ${c.primary.main.fontTertiary};\n`;
33
+ css += ` --newtone-border: ${c.primary.main.fontDisabled};\n`;
34
+ css += ` --newtone-accent: ${c.secondary.emphasis.fontPrimary};\n`;
35
+ css += ` --newtone-accent-hover: ${c.secondary.emphasis.fontSecondary};\n`;
36
+ css += ` --newtone-accent-active: ${c.secondary.emphasis.fontTertiary};\n`;
37
+ css += ` --newtone-success: ${c.success.emphasis.fontPrimary};\n`;
38
+ css += ` --newtone-warning: ${c.warning.emphasis.fontPrimary};\n`;
39
+ css += ` --newtone-error: ${c.error.emphasis.fontPrimary};\n`;
40
+ css += `}\n\n`;
41
+ }
42
+
43
+ return css;
44
+ }
@@ -0,0 +1,24 @@
1
+ import type { ConfiguratorState } from '../types';
2
+ import type { NewtoneThemeConfig } from '@newtonedev/components';
3
+ import { toThemeConfig } from './toThemeConfig';
4
+
5
+ /** Serializable configurator export format */
6
+ export interface ConfiguratorExport {
7
+ readonly version: '1.0';
8
+ readonly configuratorState: ConfiguratorState;
9
+ readonly themeConfig: NewtoneThemeConfig;
10
+ }
11
+
12
+ /**
13
+ * Export the configurator state as a JSON string.
14
+ * Includes both the human-readable state (for round-tripping)
15
+ * and the engine-ready theme config.
16
+ */
17
+ export function toJSON(state: ConfiguratorState): string {
18
+ const output: ConfiguratorExport = {
19
+ version: '1.0',
20
+ configuratorState: state,
21
+ themeConfig: toThemeConfig(state),
22
+ };
23
+ return JSON.stringify(output, null, 2);
24
+ }
@@ -0,0 +1,114 @@
1
+ import type { ConfiguratorState, SpacingPreset } from '../types';
2
+ import type { NewtoneThemeConfig } from '@newtonedev/components';
3
+ import { DEFAULT_FONT_SIZES, DEFAULT_LINE_HEIGHTS, DEFAULT_ROLE_SCALES, DEFAULT_FONT_SLOTS, ROLE_DEFAULT_WEIGHTS, migrateToFontSlot, computeBreakpointRoleScales, applyTypeScaleOffset } from '@newtonedev/fonts';
4
+ import type { DynamicRange } from 'newtone';
5
+ import type { Grading } from '@newtonedev/colors';
6
+
7
+ const SPACING_PRESET_TO_BASE: Record<SpacingPreset, number> = {
8
+ xs: 6,
9
+ sm: 7,
10
+ md: 8,
11
+ lg: 9,
12
+ xl: 10,
13
+ };
14
+
15
+ function roundnessToMultiplier(intensity: number): number {
16
+ return intensity * 2.0;
17
+ }
18
+
19
+ const DEFAULT_ICONS = {
20
+ variant: 'rounded' as const,
21
+ weight: 400 as const,
22
+ autoGrade: true,
23
+ };
24
+
25
+ function buildGlobalGrading(g: ConfiguratorState['globalHueGrading']): Grading | undefined {
26
+ if (g.lightIntensity === 0 && g.darkIntensity === 0) return undefined;
27
+ return {
28
+ light: { hue: g.lightHue, amount: g.lightIntensity },
29
+ dark: { hue: g.darkHue, amount: g.darkIntensity },
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Convert configurator state to a NewtoneThemeConfig (engine-ready format).
35
+ * Hues are already in OKLCH space — passed through directly.
36
+ */
37
+ export function toThemeConfig(state: ConfiguratorState): NewtoneThemeConfig {
38
+ const grading = buildGlobalGrading(state.globalHueGrading);
39
+
40
+ const palettes = state.palettes.map(p => {
41
+ const shift = p.localHueGrade && p.localHueGrade.intensity > 0
42
+ ? {
43
+ hue: p.localHueGrade.hue,
44
+ amount: p.localHueGrade.intensity,
45
+ ...(p.localHueGrade.side === 'light' ? { light: true as const } : {}),
46
+ }
47
+ : undefined;
48
+
49
+ return {
50
+ hue: p.hue,
51
+ chroma: {
52
+ amount: p.chromaRatio,
53
+ ...(p.chromaPeak !== 0.5 ? { balance: p.chromaPeak } : {}),
54
+ },
55
+ ...(shift ? { shift } : {}),
56
+ ...(p.keyColorStep !== undefined ? { keyStep: p.keyColorStep } : {}),
57
+ ...(p.keyColorStepDark !== undefined ? { keyStepDark: p.keyColorStepDark } : {}),
58
+ };
59
+ });
60
+
61
+ const dynamicRange: DynamicRange = {
62
+ lightest: state.dynamicRange.lightest,
63
+ darkest: state.dynamicRange.darkest,
64
+ };
65
+
66
+ const spacingBase = SPACING_PRESET_TO_BASE[state.spacing?.preset ?? 'md'];
67
+ const radiusMultiplier = roundnessToMultiplier(state.roundness?.intensity ?? 0.5);
68
+
69
+ return {
70
+ colorSystem: { dynamicRange, grading, palettes },
71
+ spacing: {
72
+ '00': Math.round(spacingBase * 0),
73
+ '02': Math.round(spacingBase * 0.25),
74
+ '04': Math.round(spacingBase * 0.5),
75
+ '06': Math.round(spacingBase * 0.75),
76
+ '08': Math.round(spacingBase * 1),
77
+ '10': Math.round(spacingBase * 1.25),
78
+ '12': Math.round(spacingBase * 1.5),
79
+ '16': Math.round(spacingBase * 2),
80
+ '20': Math.round(spacingBase * 2.5),
81
+ '24': Math.round(spacingBase * 3),
82
+ '32': Math.round(spacingBase * 4),
83
+ '40': Math.round(spacingBase * 5),
84
+ '48': Math.round(spacingBase * 6),
85
+ },
86
+ radius: {
87
+ none: 0,
88
+ sm: Math.round(4 * radiusMultiplier),
89
+ md: Math.round(6 * radiusMultiplier),
90
+ lg: Math.round(8 * radiusMultiplier),
91
+ xl: Math.round(12 * radiusMultiplier),
92
+ pill: 999,
93
+ },
94
+ typography: (() => {
95
+ const roles = state.typography?.typeScaleOffset != null
96
+ ? applyTypeScaleOffset(DEFAULT_ROLE_SCALES, state.typography.typeScaleOffset)
97
+ : DEFAULT_ROLE_SCALES;
98
+ return {
99
+ fonts: {
100
+ main: migrateToFontSlot(state.typography?.fonts.main ?? (state.typography?.fonts as any)?.default, DEFAULT_FONT_SLOTS.main),
101
+ display: migrateToFontSlot(state.typography?.fonts.display, DEFAULT_FONT_SLOTS.display),
102
+ mono: migrateToFontSlot(state.typography?.fonts.mono, DEFAULT_FONT_SLOTS.mono),
103
+ currency: migrateToFontSlot(state.typography?.fonts.currency, DEFAULT_FONT_SLOTS.currency),
104
+ },
105
+ fontSizes: DEFAULT_FONT_SIZES,
106
+ lineHeights: DEFAULT_LINE_HEIGHTS,
107
+ roles,
108
+ breakpointRoles: computeBreakpointRoleScales(roles),
109
+ roleWeights: { ...ROLE_DEFAULT_WEIGHTS, ...state.typography?.roleWeights },
110
+ };
111
+ })(),
112
+ icons: state.icons ?? DEFAULT_ICONS,
113
+ };
114
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Hue ranges for semantic palettes (OKLCH hues).
3
+ * Indices 0 (Primary), 1 (Secondary), and 2 (Tertiary) are unconstrained.
4
+ *
5
+ * Gaps between ranges prevent adjacent semantics from overlapping:
6
+ * - ~34–45: gap between Error (reds) and Warning (yellows)
7
+ * - ~103–130: gap between Warning and Success (greens)
8
+ */
9
+ export const SEMANTIC_HUE_RANGES: Readonly<Record<number, { readonly min: number; readonly max: number }>> = {
10
+ 3: { min: 129.86, max: 160.14 }, // Success (greens)
11
+ 4: { min: 44.85, max: 102.80 }, // Warning (yellow/orange)
12
+ 5: { min: 20.91, max: 34.48 }, // Error (reds)
13
+ };
@@ -0,0 +1,67 @@
1
+ import {
2
+ hexToSrgb,
3
+ srgbToOklch,
4
+ findMaxChromaInGamut,
5
+ resolveLightest,
6
+ resolveDarkest,
7
+ } from 'newtone';
8
+ import type { DynamicRange } from 'newtone';
9
+
10
+ /** Result of decomposing a hex color into palette parameters */
11
+ export interface HexPaletteParams {
12
+ /** OKLCH hue [0, 360) */
13
+ readonly hue: number;
14
+ /** Chroma as fraction of max in-gamut chroma [0, 1] */
15
+ readonly chromaRatio: number;
16
+ /** Step index in the 26-step scale [0, 25] (0 = lightest, 25 = darkest) */
17
+ readonly stepIndex: number;
18
+ }
19
+
20
+ // Achromatic threshold — below this chroma, the color has no meaningful hue
21
+ const ACHROMATIC_THRESHOLD = 0.005;
22
+
23
+ /**
24
+ * Map an OKLCH lightness value back to an engineNv [0, 1] within the dynamic range.
25
+ * engineNv: 0=darkest, 1=lightest.
26
+ */
27
+ function lightnessToNormalizedValue(dynamicRange: DynamicRange, L: number): number {
28
+ const lightestL = resolveLightest(dynamicRange.lightest);
29
+ const darkestL = resolveDarkest(dynamicRange.darkest);
30
+ if (lightestL <= darkestL) return 0.5;
31
+ return Math.max(0, Math.min(1, (L - darkestL) / (lightestL - darkestL)));
32
+ }
33
+
34
+ /**
35
+ * Decompose a hex color string into palette parameters (hue, chromaRatio, normalizedValue).
36
+ *
37
+ * Returns null if the hex string is invalid.
38
+ * Hue is returned in OKLCH space [0, 360).
39
+ *
40
+ * @param hex - Hex color string (e.g., "#FF0000", "#f00", "FF0000")
41
+ * @param dynamicRange - Current dynamic range for normalizedValue mapping
42
+ */
43
+ export function hexToPaletteParams(
44
+ hex: string,
45
+ dynamicRange: DynamicRange,
46
+ ): HexPaletteParams | null {
47
+ const cleaned = hex.startsWith('#') ? hex.slice(1) : hex;
48
+ if (!/^[0-9a-fA-F]{3}$/.test(cleaned) && !/^[0-9a-fA-F]{6}$/.test(cleaned)) {
49
+ return null;
50
+ }
51
+
52
+ const srgb = hexToSrgb(hex);
53
+ const oklch = srgbToOklch(srgb);
54
+ const normalizedValue = lightnessToNormalizedValue(dynamicRange, oklch.L);
55
+ // NV: 0=darkest, 1=lightest → step: 0=lightest, 25=darkest
56
+ const stepIndex = Math.round((1 - normalizedValue) * 25);
57
+
58
+ if (oklch.C < ACHROMATIC_THRESHOLD) {
59
+ return { hue: 0, chromaRatio: 0, stepIndex };
60
+ }
61
+
62
+ const hue = ((oklch.h % 360) + 360) % 360;
63
+ const maxChroma = findMaxChromaInGamut(oklch.L, oklch.h);
64
+ const chromaRatio = maxChroma > 0 ? Math.min(1, oklch.C / maxChroma) : 0;
65
+
66
+ return { hue, chromaRatio, stepIndex };
67
+ }
@@ -0,0 +1,33 @@
1
+ import { useReducer, useCallback, useMemo } from 'react';
2
+ import type { ConfiguratorState } from '../types';
3
+ import type { ConfiguratorAction } from '../state/actions';
4
+ import type { NewtoneThemeConfig } from '@newtonedev/components';
5
+ import { configuratorReducer, migrateConfiguratorState } from '../state/reducer';
6
+ import { DEFAULT_CONFIGURATOR_STATE } from '../state/defaults';
7
+ import { toThemeConfig } from '../bridge/toThemeConfig';
8
+
9
+ export interface UseConfiguratorResult {
10
+ readonly state: ConfiguratorState;
11
+ readonly dispatch: (action: ConfiguratorAction) => void;
12
+ readonly themeConfig: NewtoneThemeConfig;
13
+ readonly reset: () => void;
14
+ }
15
+
16
+ export function useConfigurator(
17
+ initialState?: Partial<ConfiguratorState>,
18
+ ): UseConfiguratorResult {
19
+ // Merge with defaults, then run full migration (palette format + count)
20
+ const merged = initialState
21
+ ? { ...DEFAULT_CONFIGURATOR_STATE, ...initialState }
22
+ : DEFAULT_CONFIGURATOR_STATE;
23
+
24
+ const mergedInitial = migrateConfiguratorState(merged);
25
+
26
+ const [state, dispatch] = useReducer(configuratorReducer, mergedInitial);
27
+
28
+ const themeConfig = useMemo(() => toThemeConfig(state), [state]);
29
+
30
+ const reset = useCallback(() => dispatch({ type: 'RESET' }), []);
31
+
32
+ return { state, dispatch, themeConfig, reset };
33
+ }