@newtonedev/editor 0.1.6 → 0.1.8

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 (52) hide show
  1. package/dist/Editor.d.ts +1 -1
  2. package/dist/Editor.d.ts.map +1 -1
  3. package/dist/components/ConfiguratorPanel.d.ts +17 -0
  4. package/dist/components/ConfiguratorPanel.d.ts.map +1 -0
  5. package/dist/components/FontPicker.d.ts +4 -2
  6. package/dist/components/FontPicker.d.ts.map +1 -1
  7. package/dist/components/PreviewWindow.d.ts +7 -2
  8. package/dist/components/PreviewWindow.d.ts.map +1 -1
  9. package/dist/components/PrimaryNav.d.ts +7 -0
  10. package/dist/components/PrimaryNav.d.ts.map +1 -0
  11. package/dist/components/Sidebar.d.ts +1 -10
  12. package/dist/components/Sidebar.d.ts.map +1 -1
  13. package/dist/components/TableOfContents.d.ts +2 -1
  14. package/dist/components/TableOfContents.d.ts.map +1 -1
  15. package/dist/components/sections/DynamicRangeSection.d.ts.map +1 -1
  16. package/dist/components/sections/FontsSection.d.ts +3 -1
  17. package/dist/components/sections/FontsSection.d.ts.map +1 -1
  18. package/dist/hooks/useEditorState.d.ts +4 -1
  19. package/dist/hooks/useEditorState.d.ts.map +1 -1
  20. package/dist/index.cjs +2484 -2052
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.ts +2 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +2486 -2055
  25. package/dist/index.js.map +1 -1
  26. package/dist/preview/ComponentDetailView.d.ts +7 -1
  27. package/dist/preview/ComponentDetailView.d.ts.map +1 -1
  28. package/dist/preview/ComponentRenderer.d.ts +2 -1
  29. package/dist/preview/ComponentRenderer.d.ts.map +1 -1
  30. package/dist/types.d.ts +17 -0
  31. package/dist/types.d.ts.map +1 -1
  32. package/dist/utils/lookupFontMetrics.d.ts +19 -0
  33. package/dist/utils/lookupFontMetrics.d.ts.map +1 -0
  34. package/dist/utils/measureFonts.d.ts +18 -0
  35. package/dist/utils/measureFonts.d.ts.map +1 -0
  36. package/package.json +1 -1
  37. package/src/Editor.tsx +53 -10
  38. package/src/components/ConfiguratorPanel.tsx +77 -0
  39. package/src/components/FontPicker.tsx +38 -29
  40. package/src/components/PreviewWindow.tsx +14 -1
  41. package/src/components/PrimaryNav.tsx +76 -0
  42. package/src/components/Sidebar.tsx +5 -132
  43. package/src/components/TableOfContents.tsx +41 -78
  44. package/src/components/sections/DynamicRangeSection.tsx +2 -225
  45. package/src/components/sections/FontsSection.tsx +61 -93
  46. package/src/hooks/useEditorState.ts +68 -17
  47. package/src/index.ts +2 -0
  48. package/src/preview/ComponentDetailView.tsx +531 -67
  49. package/src/preview/ComponentRenderer.tsx +6 -4
  50. package/src/types.ts +15 -0
  51. package/src/utils/lookupFontMetrics.ts +52 -0
  52. package/src/utils/measureFonts.ts +41 -0
@@ -1,34 +1,11 @@
1
- import { useState } from "react";
2
- import { useTokens, Icon } from "@newtonedev/components";
3
- import type { ColorMode } from "@newtonedev/components";
1
+ import { useTokens } from "@newtonedev/components";
4
2
  import { srgbToHex } from "newtone";
5
- import type { ColorResult } from "newtone";
6
- import type { ConfiguratorState } from "@newtonedev/configurator";
7
- import type { ConfiguratorAction } from "@newtonedev/configurator";
8
- import {
9
- ColorsSection,
10
- DynamicRangeSection,
11
- IconsSection,
12
- FontsSection,
13
- OthersSection,
14
- } from "./sections";
15
3
  import { PresetSelector } from "./PresetSelector";
16
4
  import type { Preset } from "../types";
17
5
 
18
6
  const SIDEBAR_WIDTH = 360;
19
7
 
20
- const ACCORDION_SECTIONS = [
21
- { id: "dynamic-range", label: "Dynamic Range", icon: "contrast" },
22
- { id: "colors", label: "Colors", icon: "palette" },
23
- { id: "fonts", label: "Fonts", icon: "text_fields" },
24
- { id: "icons", label: "Icons", icon: "grid_view" },
25
- { id: "others", label: "Others", icon: "tune" },
26
- ] as const;
27
-
28
8
  interface SidebarProps {
29
- readonly state: ConfiguratorState;
30
- readonly dispatch: (action: ConfiguratorAction) => void;
31
- readonly previewColors: readonly (readonly ColorResult[])[];
32
9
  readonly isDirty: boolean;
33
10
  readonly onRevert: () => void;
34
11
  readonly presets: readonly Preset[];
@@ -42,14 +19,9 @@ interface SidebarProps {
42
19
  presetId: string,
43
20
  name: string,
44
21
  ) => Promise<string>;
45
- readonly colorMode: ColorMode;
46
- readonly onColorModeChange: (mode: ColorMode) => void;
47
22
  }
48
23
 
49
24
  export function Sidebar({
50
- state,
51
- dispatch,
52
- previewColors,
53
25
  isDirty,
54
26
  onRevert,
55
27
  presets,
@@ -60,52 +32,11 @@ export function Sidebar({
60
32
  onRenamePreset,
61
33
  onDeletePreset,
62
34
  onDuplicatePreset,
63
- colorMode,
64
- onColorModeChange,
65
35
  }: SidebarProps) {
66
36
  const tokens = useTokens();
67
- const [openSections, setOpenSections] = useState<Set<string>>(
68
- new Set(["dynamic-range", "colors"]),
69
- );
70
- const [hoveredSectionId, setHoveredSectionId] = useState<string | null>(null);
71
37
 
72
38
  const borderColor = srgbToHex(tokens.border.srgb);
73
39
  const bgColor = srgbToHex(tokens.background.srgb);
74
- const hoverBg = `${borderColor}10`;
75
-
76
- const toggleSection = (id: string) => {
77
- setOpenSections((prev) => {
78
- const next = new Set(prev);
79
- if (next.has(id)) next.delete(id);
80
- else next.add(id);
81
- return next;
82
- });
83
- };
84
-
85
- const renderSectionContent = (sectionId: string) => {
86
- switch (sectionId) {
87
- case "dynamic-range":
88
- return <DynamicRangeSection state={state} dispatch={dispatch} />;
89
- case "colors":
90
- return (
91
- <ColorsSection
92
- state={state}
93
- dispatch={dispatch}
94
- previewColors={previewColors}
95
- colorMode={colorMode}
96
- onColorModeChange={onColorModeChange}
97
- />
98
- );
99
- case "icons":
100
- return <IconsSection state={state} dispatch={dispatch} />;
101
- case "fonts":
102
- return <FontsSection state={state} dispatch={dispatch} />;
103
- case "others":
104
- return <OthersSection state={state} dispatch={dispatch} />;
105
- default:
106
- return null;
107
- }
108
- };
109
40
 
110
41
  return (
111
42
  <div
@@ -119,7 +50,7 @@ export function Sidebar({
119
50
  backgroundColor: bgColor,
120
51
  }}
121
52
  >
122
- {/* Sticky Header */}
53
+ {/* Header */}
123
54
  <div
124
55
  style={{
125
56
  flexShrink: 0,
@@ -151,74 +82,16 @@ export function Sidebar({
151
82
  />
152
83
  </div>
153
84
 
154
- {/* Scrollable Accordion Area */}
85
+ {/* Content area (empty for now) */}
155
86
  <div
156
87
  style={{
157
88
  flex: 1,
158
89
  overflowY: "auto",
159
90
  overflowX: "hidden",
160
91
  }}
161
- >
162
- {ACCORDION_SECTIONS.map((section) => {
163
- const isOpen = openSections.has(section.id);
164
- const isHovered = hoveredSectionId === section.id;
165
-
166
- return (
167
- <div key={section.id}>
168
- <button
169
- onClick={() => toggleSection(section.id)}
170
- onMouseEnter={() => setHoveredSectionId(section.id)}
171
- onMouseLeave={() => setHoveredSectionId(null)}
172
- aria-expanded={isOpen}
173
- aria-controls={`section-${section.id}`}
174
- style={{
175
- display: "flex",
176
- alignItems: "center",
177
- justifyContent: "space-between",
178
- width: "100%",
179
- padding: "12px 20px",
180
- border: "none",
181
- borderBottom: `1px solid ${borderColor}`,
182
- background: isHovered ? hoverBg : "none",
183
- cursor: "pointer",
184
- fontSize: 14,
185
- fontWeight: 500,
186
- color: srgbToHex(tokens.textPrimary.srgb),
187
- transition: "background-color 100ms ease",
188
- }}
189
- >
190
- <span style={{ display: "flex", alignItems: "center", gap: 8 }}>
191
- <Icon name={section.icon} size={16} />
192
- {section.label}
193
- </span>
194
- <Icon
195
- name="expand_more"
196
- size={16}
197
- style={{
198
- transform: isOpen ? "rotate(180deg)" : "none",
199
- transition: "transform 150ms ease",
200
- } as any}
201
- />
202
- </button>
203
- {isOpen && (
204
- <div
205
- id={`section-${section.id}`}
206
- role="region"
207
- aria-label={section.label}
208
- style={{
209
- padding: "16px 20px",
210
- borderBottom: `1px solid ${borderColor}`,
211
- }}
212
- >
213
- {renderSectionContent(section.id)}
214
- </div>
215
- )}
216
- </div>
217
- );
218
- })}
219
- </div>
92
+ />
220
93
 
221
- {/* Sticky Footer */}
94
+ {/* Footer */}
222
95
  <div
223
96
  style={{
224
97
  flexShrink: 0,
@@ -1,13 +1,13 @@
1
1
  import { useState } from "react";
2
2
  import {
3
3
  useTokens,
4
- CATEGORIES,
5
4
  getComponentsByCategory,
6
5
  } from "@newtonedev/components";
7
6
  import { srgbToHex } from "newtone";
8
7
  import type { PreviewView } from "../types";
9
8
 
10
9
  interface TableOfContentsProps {
10
+ readonly activeSectionId: string;
11
11
  readonly activeView: PreviewView;
12
12
  readonly selectedComponentId: string | null;
13
13
  readonly onNavigate: (view: PreviewView) => void;
@@ -16,6 +16,7 @@ interface TableOfContentsProps {
16
16
  const TOC_WIDTH = 220;
17
17
 
18
18
  export function TableOfContents({
19
+ activeSectionId,
19
20
  activeView,
20
21
  selectedComponentId,
21
22
  onNavigate,
@@ -26,10 +27,13 @@ export function TableOfContents({
26
27
  const borderColor = srgbToHex(tokens.border.srgb);
27
28
  const activeColor = srgbToHex(tokens.accent.fill.srgb);
28
29
  const textPrimary = srgbToHex(tokens.textPrimary.srgb);
29
- const textSecondary = srgbToHex(tokens.textSecondary.srgb);
30
30
  const hoverBg = `${borderColor}20`;
31
31
 
32
- const isOverviewActive = activeView.kind === "overview";
32
+ const components = getComponentsByCategory(activeSectionId);
33
+
34
+ const isOverviewActive =
35
+ activeView.kind === "overview" ||
36
+ (activeView.kind === "category" && activeView.categoryId === activeSectionId);
33
37
 
34
38
  return (
35
39
  <nav
@@ -44,7 +48,7 @@ export function TableOfContents({
44
48
  }}
45
49
  >
46
50
  <button
47
- onClick={() => onNavigate({ kind: "overview" })}
51
+ onClick={() => onNavigate({ kind: "category", categoryId: activeSectionId })}
48
52
  onMouseEnter={() => setHoveredId("overview")}
49
53
  onMouseLeave={() => setHoveredId(null)}
50
54
  aria-current={isOverviewActive ? "page" : undefined}
@@ -69,82 +73,41 @@ export function TableOfContents({
69
73
  Overview
70
74
  </button>
71
75
 
72
- {CATEGORIES.map((category) => {
73
- const components = getComponentsByCategory(category.id);
74
- const isCategoryActive =
75
- activeView.kind === "category" &&
76
- activeView.categoryId === category.id;
76
+ {components.map((comp) => {
77
+ const isComponentActive =
78
+ (activeView.kind === "component" &&
79
+ activeView.componentId === comp.id) ||
80
+ selectedComponentId === comp.id;
77
81
 
78
82
  return (
79
- <div key={category.id} style={{ marginTop: 16 }}>
80
- <button
81
- onClick={() =>
82
- onNavigate({ kind: "category", categoryId: category.id })
83
- }
84
- onMouseEnter={() => setHoveredId(`cat-${category.id}`)}
85
- onMouseLeave={() => setHoveredId(null)}
86
- aria-current={isCategoryActive ? "page" : undefined}
87
- style={{
88
- display: "block",
89
- width: "100%",
90
- padding: "6px 20px",
91
- border: "none",
92
- background: isCategoryActive
93
- ? `${activeColor}14`
94
- : hoveredId === `cat-${category.id}`
95
- ? hoverBg
96
- : "none",
97
- cursor: "pointer",
98
- textAlign: "left",
99
- fontSize: 11,
100
- fontWeight: 600,
101
- color: isCategoryActive ? activeColor : textSecondary,
102
- textTransform: "uppercase",
103
- letterSpacing: 0.5,
104
- transition: "background-color 100ms ease",
105
- }}
106
- >
107
- {category.name}
108
- </button>
109
-
110
- {components.map((comp) => {
111
- const isComponentActive =
112
- (activeView.kind === "component" &&
113
- activeView.componentId === comp.id) ||
114
- selectedComponentId === comp.id;
115
-
116
- return (
117
- <button
118
- key={comp.id}
119
- onClick={() =>
120
- onNavigate({ kind: "component", componentId: comp.id })
121
- }
122
- onMouseEnter={() => setHoveredId(comp.id)}
123
- onMouseLeave={() => setHoveredId(null)}
124
- aria-current={isComponentActive ? "page" : undefined}
125
- style={{
126
- display: "block",
127
- width: "100%",
128
- padding: "4px 20px 4px 32px",
129
- border: "none",
130
- background: isComponentActive
131
- ? `${activeColor}14`
132
- : hoveredId === comp.id
133
- ? hoverBg
134
- : "none",
135
- cursor: "pointer",
136
- textAlign: "left",
137
- fontSize: 13,
138
- fontWeight: isComponentActive ? 600 : 400,
139
- color: isComponentActive ? activeColor : textPrimary,
140
- transition: "background-color 100ms ease",
141
- }}
142
- >
143
- {comp.name}
144
- </button>
145
- );
146
- })}
147
- </div>
83
+ <button
84
+ key={comp.id}
85
+ onClick={() =>
86
+ onNavigate({ kind: "component", componentId: comp.id })
87
+ }
88
+ onMouseEnter={() => setHoveredId(comp.id)}
89
+ onMouseLeave={() => setHoveredId(null)}
90
+ aria-current={isComponentActive ? "page" : undefined}
91
+ style={{
92
+ display: "block",
93
+ width: "100%",
94
+ padding: "4px 20px",
95
+ border: "none",
96
+ background: isComponentActive
97
+ ? `${activeColor}14`
98
+ : hoveredId === comp.id
99
+ ? hoverBg
100
+ : "none",
101
+ cursor: "pointer",
102
+ textAlign: "left",
103
+ fontSize: 13,
104
+ fontWeight: isComponentActive ? 600 : 400,
105
+ color: isComponentActive ? activeColor : textPrimary,
106
+ transition: "background-color 100ms ease",
107
+ }}
108
+ >
109
+ {comp.name}
110
+ </button>
148
111
  );
149
112
  })}
150
113
  </nav>
@@ -1,20 +1,9 @@
1
- import { useState, useRef, useCallback, useMemo, useEffect } from "react";
1
+ import { useState, useRef, useCallback } from "react";
2
2
  import { HueSlider, Select, useTokens } from "@newtonedev/components";
3
- import {
4
- srgbToHex,
5
- resolveLightness,
6
- findMaxChromaInGamut,
7
- oklchToSrgb,
8
- clampSrgb,
9
- HUE_GRADING_STRENGTH_LOW,
10
- HUE_GRADING_STRENGTH_MEDIUM,
11
- HUE_GRADING_STRENGTH_HARD,
12
- HUE_GRADING_EASING_POWER,
13
- } from "newtone";
3
+ import { srgbToHex } from "newtone";
14
4
  import type { HueGradingStrength } from "newtone";
15
5
  import type { ConfiguratorState } from "@newtonedev/configurator";
16
6
  import type { ConfiguratorAction } from "@newtonedev/configurator";
17
- import { traditionalHueToOklch } from "@newtonedev/configurator";
18
7
 
19
8
  const STRENGTH_OPTIONS = [
20
9
  { label: "None", value: "none" },
@@ -275,215 +264,6 @@ function RangeInput({ display, onCommit, toInternal }: RangeInputProps) {
275
264
  );
276
265
  }
277
266
 
278
- // --- Dynamic Range Graph ---
279
-
280
- const GRAPH_HEIGHT = 80;
281
- const GRAPH_COLS = 256;
282
- const GRAPH_ROWS = 64;
283
-
284
- function strengthToFactor(strength: HueGradingStrength): number {
285
- switch (strength) {
286
- case "none":
287
- return 0;
288
- case "low":
289
- return HUE_GRADING_STRENGTH_LOW;
290
- case "medium":
291
- return HUE_GRADING_STRENGTH_MEDIUM;
292
- case "hard":
293
- return HUE_GRADING_STRENGTH_HARD;
294
- }
295
- }
296
-
297
- function blendHues(
298
- lightHue: number,
299
- darkHue: number,
300
- wLight: number,
301
- wDark: number,
302
- ): number {
303
- const totalW = wLight + wDark;
304
- if (totalW === 0) return 0;
305
- const delta = (((darkHue - lightHue + 180) % 360) + 360) % 360 - 180;
306
- const t = wDark / totalW;
307
- const result = lightHue + delta * t;
308
- return ((result % 360) + 360) % 360;
309
- }
310
-
311
- interface GraphData {
312
- readonly buffer: Uint8ClampedArray;
313
- readonly curvePoints: readonly { readonly x: number; readonly y: number }[];
314
- }
315
-
316
- function computeGraphData(state: ConfiguratorState): GraphData {
317
- const { dynamicRange, globalHueGrading } = state;
318
-
319
- const lightActive = globalHueGrading.light.strength !== "none";
320
- const darkActive = globalHueGrading.dark.strength !== "none";
321
- const lightOklchHue = traditionalHueToOklch(globalHueGrading.light.hue);
322
- const darkOklchHue = traditionalHueToOklch(globalHueGrading.dark.hue);
323
- const lightFactor = strengthToFactor(globalHueGrading.light.strength);
324
- const darkFactor = strengthToFactor(globalHueGrading.dark.strength);
325
-
326
- const buffer = new Uint8ClampedArray(GRAPH_COLS * GRAPH_ROWS * 4);
327
-
328
- for (let col = 0; col < GRAPH_COLS; col++) {
329
- const nv = 1 - col / (GRAPH_COLS - 1);
330
- const L = resolveLightness(dynamicRange, nv);
331
-
332
- // Easing weights for hue blend at top row (assumes hard strength)
333
- const wLight = lightActive ? Math.pow(nv, HUE_GRADING_EASING_POWER) : 0;
334
- const wDark = darkActive
335
- ? Math.pow(1 - nv, HUE_GRADING_EASING_POWER)
336
- : 0;
337
- const totalW = wLight + wDark;
338
-
339
- let topHue: number;
340
- let topChroma: number;
341
-
342
- if (totalW === 0) {
343
- topHue = 0;
344
- topChroma = 0;
345
- } else {
346
- if (!lightActive) {
347
- topHue = darkOklchHue;
348
- } else if (!darkActive) {
349
- topHue = lightOklchHue;
350
- } else {
351
- topHue = blendHues(lightOklchHue, darkOklchHue, wLight, wDark);
352
- }
353
- topChroma =
354
- findMaxChromaInGamut(L, topHue) * Math.min(totalW, 1);
355
- }
356
-
357
- for (let row = 0; row < GRAPH_ROWS; row++) {
358
- const gradingIntensity = row / (GRAPH_ROWS - 1);
359
- const C = topChroma * gradingIntensity;
360
- const srgb = clampSrgb(oklchToSrgb({ L, C, h: topHue }));
361
-
362
- // Canvas Y=0 is top; row=0 is bottom of our graph
363
- const canvasY = GRAPH_ROWS - 1 - row;
364
- const idx = (canvasY * GRAPH_COLS + col) * 4;
365
- buffer[idx] = Math.round(srgb.r * 255);
366
- buffer[idx + 1] = Math.round(srgb.g * 255);
367
- buffer[idx + 2] = Math.round(srgb.b * 255);
368
- buffer[idx + 3] = 255;
369
- }
370
- }
371
-
372
- // 26-step curve points
373
- const curvePoints: { x: number; y: number }[] = [];
374
- for (let i = 0; i < 26; i++) {
375
- const nv = 1 - i / 25;
376
- const x = (i / 25) * (GRAPH_COLS - 1);
377
-
378
- const lightContrib =
379
- Math.pow(nv, HUE_GRADING_EASING_POWER) *
380
- (lightFactor / HUE_GRADING_STRENGTH_HARD);
381
- const darkContrib =
382
- Math.pow(1 - nv, HUE_GRADING_EASING_POWER) *
383
- (darkFactor / HUE_GRADING_STRENGTH_HARD);
384
- const y = clamp(lightContrib + darkContrib, 0, 1);
385
-
386
- curvePoints.push({ x, y });
387
- }
388
-
389
- return { buffer, curvePoints };
390
- }
391
-
392
- interface DynamicRangeGraphProps {
393
- readonly state: ConfiguratorState;
394
- }
395
-
396
- function DynamicRangeGraph({ state }: DynamicRangeGraphProps) {
397
- const tokens = useTokens();
398
- const canvasRef = useRef<HTMLCanvasElement>(null);
399
-
400
- const graphData = useMemo(
401
- () => computeGraphData(state),
402
- [
403
- state.dynamicRange.lightest,
404
- state.dynamicRange.darkest,
405
- state.globalHueGrading.light.strength,
406
- state.globalHueGrading.light.hue,
407
- state.globalHueGrading.dark.strength,
408
- state.globalHueGrading.dark.hue,
409
- ],
410
- );
411
-
412
- useEffect(() => {
413
- const canvas = canvasRef.current;
414
- if (!canvas) return;
415
-
416
- canvas.width = GRAPH_COLS;
417
- canvas.height = GRAPH_ROWS;
418
-
419
- const ctx = canvas.getContext("2d");
420
- if (!ctx) return;
421
-
422
- // Draw gradient from pre-computed buffer
423
- const imageData = ctx.createImageData(GRAPH_COLS, GRAPH_ROWS);
424
- imageData.data.set(graphData.buffer);
425
- ctx.putImageData(imageData, 0, 0);
426
-
427
- // Draw 26-step curve overlay
428
- const curveColor = srgbToHex(tokens.accent.fill.srgb);
429
- const { curvePoints } = graphData;
430
-
431
- if (curvePoints.length < 2) return;
432
-
433
- const mapped = curvePoints.map((p) => ({
434
- cx: p.x,
435
- cy: (1 - p.y) * (GRAPH_ROWS - 1),
436
- }));
437
-
438
- // Smooth Catmull-Rom spline
439
- ctx.beginPath();
440
- ctx.strokeStyle = curveColor;
441
- ctx.lineWidth = 1.5;
442
- ctx.lineJoin = "round";
443
- ctx.lineCap = "round";
444
-
445
- ctx.moveTo(mapped[0].cx, mapped[0].cy);
446
- for (let i = 0; i < mapped.length - 1; i++) {
447
- const p0 = mapped[Math.max(0, i - 1)];
448
- const p1 = mapped[i];
449
- const p2 = mapped[i + 1];
450
- const p3 = mapped[Math.min(mapped.length - 1, i + 2)];
451
-
452
- const cp1x = p1.cx + (p2.cx - p0.cx) / 6;
453
- const cp1y = p1.cy + (p2.cy - p0.cy) / 6;
454
- const cp2x = p2.cx - (p3.cx - p1.cx) / 6;
455
- const cp2y = p2.cy - (p3.cy - p1.cy) / 6;
456
-
457
- ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, p2.cx, p2.cy);
458
- }
459
- ctx.stroke();
460
-
461
- // Draw dots at each of the 26 steps
462
- ctx.fillStyle = curveColor;
463
- for (const p of mapped) {
464
- ctx.beginPath();
465
- ctx.arc(p.cx, p.cy, 2, 0, Math.PI * 2);
466
- ctx.fill();
467
- }
468
- }, [graphData, tokens]);
469
-
470
- const borderColor = srgbToHex(tokens.border.srgb);
471
-
472
- return (
473
- <canvas
474
- ref={canvasRef}
475
- style={{
476
- width: "100%",
477
- height: GRAPH_HEIGHT,
478
- borderRadius: 6,
479
- border: `1px solid ${borderColor}`,
480
- display: "block",
481
- overflow: "hidden",
482
- }}
483
- />
484
- );
485
- }
486
-
487
267
  // --- Section ---
488
268
 
489
269
  interface DynamicRangeSectionProps {
@@ -511,9 +291,6 @@ export function DynamicRangeSection({
511
291
 
512
292
  return (
513
293
  <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
514
- {/* Dynamic range graph */}
515
- <DynamicRangeGraph state={state} />
516
-
517
294
  {/* Labels above slider */}
518
295
  <div
519
296
  style={{