@newtonedev/editor 0.1.4 → 0.1.6

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 (48) 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/PresetSelector.d.ts.map +1 -1
  4. package/dist/components/PreviewWindow.d.ts +3 -2
  5. package/dist/components/PreviewWindow.d.ts.map +1 -1
  6. package/dist/components/RightSidebar.d.ts +4 -1
  7. package/dist/components/RightSidebar.d.ts.map +1 -1
  8. package/dist/components/Sidebar.d.ts.map +1 -1
  9. package/dist/components/sections/DynamicRangeSection.d.ts.map +1 -1
  10. package/dist/hooks/useEditorState.d.ts +1 -3
  11. package/dist/hooks/useEditorState.d.ts.map +1 -1
  12. package/dist/index.cjs +686 -346
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.ts +1 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +689 -348
  17. package/dist/index.js.map +1 -1
  18. package/dist/preview/ComponentDetailView.d.ts +3 -2
  19. package/dist/preview/ComponentDetailView.d.ts.map +1 -1
  20. package/dist/preview/ComponentRenderer.d.ts.map +1 -1
  21. package/dist/preview/IconBrowserView.d.ts +7 -0
  22. package/dist/preview/IconBrowserView.d.ts.map +1 -0
  23. package/dist/types.d.ts +0 -1
  24. package/dist/types.d.ts.map +1 -1
  25. package/package.json +1 -1
  26. package/src/Editor.tsx +19 -9
  27. package/src/components/CodeBlock.tsx +42 -14
  28. package/src/components/EditorHeader.tsx +3 -3
  29. package/src/components/EditorShell.tsx +2 -2
  30. package/src/components/FontPicker.tsx +1 -1
  31. package/src/components/PresetSelector.tsx +11 -36
  32. package/src/components/PreviewWindow.tsx +6 -3
  33. package/src/components/RightSidebar.tsx +105 -42
  34. package/src/components/Sidebar.tsx +12 -92
  35. package/src/components/TableOfContents.tsx +1 -1
  36. package/src/components/sections/ColorsSection.tsx +2 -2
  37. package/src/components/sections/DynamicRangeSection.tsx +226 -3
  38. package/src/hooks/useEditorState.ts +14 -19
  39. package/src/index.ts +0 -2
  40. package/src/preview/CategoryView.tsx +1 -1
  41. package/src/preview/ComponentDetailView.tsx +47 -8
  42. package/src/preview/ComponentRenderer.tsx +51 -0
  43. package/src/preview/IconBrowserView.tsx +187 -0
  44. package/src/preview/OverviewView.tsx +1 -1
  45. package/src/types.ts +0 -2
  46. package/dist/components/ThemeBar.d.ts +0 -8
  47. package/dist/components/ThemeBar.d.ts.map +0 -1
  48. package/src/components/ThemeBar.tsx +0 -76
@@ -1,5 +1,5 @@
1
1
  import { useState } from "react";
2
- import { useTokens } from "@newtonedev/components";
2
+ import { useTokens, Icon } from "@newtonedev/components";
3
3
  import type { ColorMode } from "@newtonedev/components";
4
4
  import { srgbToHex } from "newtone";
5
5
  import type { ColorResult } from "newtone";
@@ -18,87 +18,13 @@ import type { Preset } from "../types";
18
18
  const SIDEBAR_WIDTH = 360;
19
19
 
20
20
  const ACCORDION_SECTIONS = [
21
- { id: "dynamic-range", label: "Dynamic Range" },
22
- { id: "colors", label: "Colors" },
23
- { id: "fonts", label: "Fonts" },
24
- { id: "icons", label: "Icons" },
25
- { id: "others", label: "Others" },
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
26
  ] as const;
27
27
 
28
- function SectionIcon({ id }: { readonly id: string }) {
29
- const props = {
30
- width: 16,
31
- height: 16,
32
- viewBox: "0 0 24 24",
33
- fill: "none",
34
- stroke: "currentColor",
35
- strokeWidth: 2,
36
- strokeLinecap: "round" as const,
37
- strokeLinejoin: "round" as const,
38
- };
39
-
40
- switch (id) {
41
- case "dynamic-range":
42
- // Sun/contrast icon
43
- return (
44
- <svg {...props}>
45
- <circle cx="12" cy="12" r="5" />
46
- <line x1="12" y1="1" x2="12" y2="3" />
47
- <line x1="12" y1="21" x2="12" y2="23" />
48
- <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
49
- <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
50
- <line x1="1" y1="12" x2="3" y2="12" />
51
- <line x1="21" y1="12" x2="23" y2="12" />
52
- <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
53
- <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
54
- </svg>
55
- );
56
- case "colors":
57
- // Palette/droplet icon
58
- return (
59
- <svg {...props}>
60
- <path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z" />
61
- </svg>
62
- );
63
- case "fonts":
64
- // Type/text icon
65
- return (
66
- <svg {...props}>
67
- <polyline points="4 7 4 4 20 4 20 7" />
68
- <line x1="9" y1="20" x2="15" y2="20" />
69
- <line x1="12" y1="4" x2="12" y2="20" />
70
- </svg>
71
- );
72
- case "icons":
73
- // Grid icon
74
- return (
75
- <svg {...props}>
76
- <rect x="3" y="3" width="7" height="7" />
77
- <rect x="14" y="3" width="7" height="7" />
78
- <rect x="3" y="14" width="7" height="7" />
79
- <rect x="14" y="14" width="7" height="7" />
80
- </svg>
81
- );
82
- case "others":
83
- // Sliders icon
84
- return (
85
- <svg {...props}>
86
- <line x1="4" y1="21" x2="4" y2="14" />
87
- <line x1="4" y1="10" x2="4" y2="3" />
88
- <line x1="12" y1="21" x2="12" y2="12" />
89
- <line x1="12" y1="8" x2="12" y2="3" />
90
- <line x1="20" y1="21" x2="20" y2="16" />
91
- <line x1="20" y1="12" x2="20" y2="3" />
92
- <line x1="1" y1="14" x2="7" y2="14" />
93
- <line x1="9" y1="8" x2="15" y2="8" />
94
- <line x1="17" y1="16" x2="23" y2="16" />
95
- </svg>
96
- );
97
- default:
98
- return null;
99
- }
100
- }
101
-
102
28
  interface SidebarProps {
103
29
  readonly state: ConfiguratorState;
104
30
  readonly dispatch: (action: ConfiguratorAction) => void;
@@ -262,23 +188,17 @@ export function Sidebar({
262
188
  }}
263
189
  >
264
190
  <span style={{ display: "flex", alignItems: "center", gap: 8 }}>
265
- <SectionIcon id={section.id} />
191
+ <Icon name={section.icon} size={16} />
266
192
  {section.label}
267
193
  </span>
268
- <svg
269
- width={12}
270
- height={12}
271
- viewBox="0 0 24 24"
272
- fill="none"
273
- stroke="currentColor"
274
- strokeWidth={2}
194
+ <Icon
195
+ name="expand_more"
196
+ size={16}
275
197
  style={{
276
198
  transform: isOpen ? "rotate(180deg)" : "none",
277
199
  transition: "transform 150ms ease",
278
- }}
279
- >
280
- <polyline points="6 9 12 15 18 9" />
281
- </svg>
200
+ } as any}
201
+ />
282
202
  </button>
283
203
  {isOpen && (
284
204
  <div
@@ -24,7 +24,7 @@ export function TableOfContents({
24
24
  const [hoveredId, setHoveredId] = useState<string | null>(null);
25
25
 
26
26
  const borderColor = srgbToHex(tokens.border.srgb);
27
- const activeColor = srgbToHex(tokens.interactive.srgb);
27
+ const activeColor = srgbToHex(tokens.accent.fill.srgb);
28
28
  const textPrimary = srgbToHex(tokens.textPrimary.srgb);
29
29
  const textSecondary = srgbToHex(tokens.textSecondary.srgb);
30
30
  const hoverBg = `${borderColor}20`;
@@ -61,7 +61,7 @@ export function ColorsSection({
61
61
  const hueRange = SEMANTIC_HUE_RANGES[activePaletteIndex];
62
62
  const isNeutral = activePaletteIndex === 0;
63
63
 
64
- const activeColor = srgbToHex(tokens.interactive.srgb);
64
+ const activeColor = srgbToHex(tokens.accent.fill.srgb);
65
65
  const borderColor = srgbToHex(tokens.border.srgb);
66
66
 
67
67
  // Resolve effective key color for current mode
@@ -350,7 +350,7 @@ export function ColorsSection({
350
350
  style={{
351
351
  fontSize: 12,
352
352
  fontWeight: 500,
353
- color: srgbToHex(tokens.error.srgb),
353
+ color: srgbToHex(tokens.error.fill.srgb),
354
354
  }}
355
355
  >
356
356
  {hexError}
@@ -1,9 +1,20 @@
1
- import { useState, useRef, useCallback } from "react";
1
+ import { useState, useRef, useCallback, useMemo, useEffect } from "react";
2
2
  import { HueSlider, Select, useTokens } from "@newtonedev/components";
3
- import { srgbToHex } from "newtone";
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";
4
14
  import type { HueGradingStrength } from "newtone";
5
15
  import type { ConfiguratorState } from "@newtonedev/configurator";
6
16
  import type { ConfiguratorAction } from "@newtonedev/configurator";
17
+ import { traditionalHueToOklch } from "@newtonedev/configurator";
7
18
 
8
19
  const STRENGTH_OPTIONS = [
9
20
  { label: "None", value: "none" },
@@ -76,7 +87,7 @@ function DualRangeSlider({
76
87
  "whites" | "blacks" | null
77
88
  >(null);
78
89
 
79
- const interactiveColor = srgbToHex(tokens.interactive.srgb);
90
+ const interactiveColor = srgbToHex(tokens.accent.fill.srgb);
80
91
  const borderColor = srgbToHex(tokens.border.srgb);
81
92
 
82
93
  const wDisplay = internalToDisplay(whitesValue);
@@ -264,6 +275,215 @@ function RangeInput({ display, onCommit, toInternal }: RangeInputProps) {
264
275
  );
265
276
  }
266
277
 
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
+
267
487
  // --- Section ---
268
488
 
269
489
  interface DynamicRangeSectionProps {
@@ -291,6 +511,9 @@ export function DynamicRangeSection({
291
511
 
292
512
  return (
293
513
  <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
514
+ {/* Dynamic range graph */}
515
+ <DynamicRangeGraph state={state} />
516
+
294
517
  {/* Labels above slider */}
295
518
  <div
296
519
  style={{
@@ -7,7 +7,6 @@ import { usePresets } from "./usePresets";
7
7
  import type {
8
8
  Preset,
9
9
  SaveStatus,
10
- ThemeName,
11
10
  PreviewView,
12
11
  SidebarSelection,
13
12
  EditorPersistence,
@@ -50,9 +49,6 @@ export function useEditorState({
50
49
  const [colorMode, setColorMode] = useState<ColorMode>(
51
50
  initialState.preview.mode,
52
51
  );
53
- const [activeTheme, setActiveTheme] = useState<ThemeName>(
54
- (initialState.preview.theme as ThemeName) || "neutral",
55
- );
56
52
  const [previewView, setPreviewView] = useState<PreviewView>(
57
53
  initialPreviewView ?? { kind: "overview" },
58
54
  );
@@ -205,9 +201,20 @@ export function useEditorState({
205
201
  }
206
202
  }, [sidebarSelection, initOverridesFromVariant]);
207
203
 
208
- const handlePropOverride = useCallback((propName: string, value: unknown) => {
209
- setPropOverrides((prev) => ({ ...prev, [propName]: value }));
210
- }, []);
204
+ const handlePropOverride = useCallback(
205
+ (propName: string, value: unknown) => {
206
+ setPropOverrides((prev) => ({ ...prev, [propName]: value }));
207
+ // Re-open sidebar if closed while viewing a component (e.g. icon browser click)
208
+ setSidebarSelection((prev) => {
209
+ if (prev !== null) return prev;
210
+ if (previewView.kind === "component") {
211
+ return { scope: "component", componentId: previewView.componentId };
212
+ }
213
+ return prev;
214
+ });
215
+ },
216
+ [previewView],
217
+ );
211
218
 
212
219
  const handleResetOverrides = useCallback(() => {
213
220
  if (sidebarSelection?.scope === "variant") {
@@ -253,16 +260,6 @@ export function useEditorState({
253
260
  scheduleSave();
254
261
  }, [configuratorState, scheduleSave]);
255
262
 
256
- // --- ThemeBar dispatch handlers ---
257
-
258
- const handleThemeChange = useCallback(
259
- (theme: ThemeName) => {
260
- setActiveTheme(theme);
261
- dispatch({ type: "SET_PREVIEW_THEME", theme });
262
- },
263
- [dispatch],
264
- );
265
-
266
263
  const handleColorModeChange = useCallback(
267
264
  (mode: ColorMode) => {
268
265
  setColorMode(mode);
@@ -345,10 +342,8 @@ export function useEditorState({
345
342
  // Preview
346
343
  previewView,
347
344
  colorMode,
348
- activeTheme,
349
345
  handlePreviewNavigate,
350
346
  handleSelectVariant,
351
- handleThemeChange,
352
347
  handleColorModeChange,
353
348
 
354
349
  // Sidebar
package/src/index.ts CHANGED
@@ -2,7 +2,6 @@
2
2
  export type {
3
3
  Preset,
4
4
  SaveStatus,
5
- ThemeName,
6
5
  PreviewView,
7
6
  SidebarSelection,
8
7
  EditorPersistence,
@@ -35,7 +34,6 @@ export { PreviewWindow } from "./components/PreviewWindow";
35
34
  export { RightSidebar } from "./components/RightSidebar";
36
35
  export { Sidebar } from "./components/Sidebar";
37
36
  export { TableOfContents } from "./components/TableOfContents";
38
- export { ThemeBar } from "./components/ThemeBar";
39
37
 
40
38
  // Sections
41
39
  export {
@@ -66,7 +66,7 @@ export function CategoryView({
66
66
  padding: 24,
67
67
  borderRadius: 12,
68
68
  border: `1px solid ${srgbToHex(
69
- isHovered ? tokens.interactive.srgb : tokens.border.srgb,
69
+ isHovered ? tokens.accent.fill.srgb : tokens.border.srgb,
70
70
  )}`,
71
71
  backgroundColor: srgbToHex(tokens.backgroundElevated.srgb),
72
72
  cursor: "pointer",
@@ -3,19 +3,22 @@ import { useTokens } from "@newtonedev/components";
3
3
  import { srgbToHex } from "newtone";
4
4
  import { getComponent } from "@newtonedev/components";
5
5
  import { ComponentRenderer } from "./ComponentRenderer";
6
+ import { IconBrowserView } from "./IconBrowserView";
6
7
 
7
8
  interface ComponentDetailViewProps {
8
9
  readonly componentId: string;
9
10
  readonly selectedVariantId: string | null;
10
- readonly propOverrides?: Record<string, unknown>;
11
11
  readonly onSelectVariant: (variantId: string) => void;
12
+ readonly propOverrides?: Record<string, unknown>;
13
+ readonly onPropOverride?: (name: string, value: unknown) => void;
12
14
  }
13
15
 
14
16
  export function ComponentDetailView({
15
17
  componentId,
16
18
  selectedVariantId,
17
- propOverrides,
18
19
  onSelectVariant,
20
+ propOverrides,
21
+ onPropOverride,
19
22
  }: ComponentDetailViewProps) {
20
23
  const tokens = useTokens();
21
24
  const component = getComponent(componentId);
@@ -23,7 +26,47 @@ export function ComponentDetailView({
23
26
 
24
27
  if (!component) return null;
25
28
 
26
- const interactiveColor = srgbToHex(tokens.interactive.srgb);
29
+ if (componentId === "icon" && propOverrides && onPropOverride) {
30
+ return (
31
+ <div
32
+ style={{
33
+ padding: "32px 0 0",
34
+ height: "100%",
35
+ display: "flex",
36
+ flexDirection: "column",
37
+ }}
38
+ >
39
+ <div style={{ padding: "0 32px", marginBottom: 24 }}>
40
+ <h2
41
+ style={{
42
+ fontSize: 22,
43
+ fontWeight: 700,
44
+ color: srgbToHex(tokens.textPrimary.srgb),
45
+ margin: 0,
46
+ marginBottom: 4,
47
+ }}
48
+ >
49
+ {component.name}
50
+ </h2>
51
+ <p
52
+ style={{
53
+ fontSize: 14,
54
+ color: srgbToHex(tokens.textSecondary.srgb),
55
+ margin: 0,
56
+ }}
57
+ >
58
+ {component.description}
59
+ </p>
60
+ </div>
61
+ <IconBrowserView
62
+ selectedIconName={(propOverrides.name as string) ?? "add"}
63
+ onIconSelect={(name) => onPropOverride("name", name)}
64
+ />
65
+ </div>
66
+ );
67
+ }
68
+
69
+ const interactiveColor = srgbToHex(tokens.accent.fill.srgb);
27
70
 
28
71
  return (
29
72
  <div style={{ padding: 32 }}>
@@ -99,11 +142,7 @@ export function ComponentDetailView({
99
142
  >
100
143
  <ComponentRenderer
101
144
  componentId={componentId}
102
- props={
103
- isSelected && propOverrides
104
- ? { ...variant.props, ...propOverrides }
105
- : variant.props
106
- }
145
+ props={variant.props}
107
146
  />
108
147
  </div>
109
148
  <span
@@ -7,6 +7,10 @@ import {
7
7
  Toggle,
8
8
  Slider,
9
9
  HueSlider,
10
+ Frame,
11
+ Text,
12
+ Icon,
13
+ Wrapper,
10
14
  useTokens,
11
15
  } from "@newtonedev/components";
12
16
  import { srgbToHex } from "newtone";
@@ -71,6 +75,24 @@ function CardPreview(props: AnyProps) {
71
75
  );
72
76
  }
73
77
 
78
+ function FramePreview(props: AnyProps) {
79
+ return (
80
+ <Frame {...props} style={{ minWidth: 200, minHeight: 60 }}>
81
+ <Text size="sm">Frame content</Text>
82
+ </Frame>
83
+ );
84
+ }
85
+
86
+ function WrapperPreview(props: AnyProps) {
87
+ return (
88
+ <Wrapper {...props} style={{ minWidth: 200 }}>
89
+ <Text size="sm">Item 1</Text>
90
+ <Text size="sm">Item 2</Text>
91
+ <Text size="sm">Item 3</Text>
92
+ </Wrapper>
93
+ );
94
+ }
95
+
74
96
  export function ComponentRenderer({ componentId, props }: ComponentRendererProps) {
75
97
  const noop = useCallback(() => {}, []);
76
98
 
@@ -80,9 +102,11 @@ export function ComponentRenderer({ componentId, props }: ComponentRendererProps
80
102
  return (
81
103
  <Button
82
104
  variant={props.variant as AnyProps}
105
+ semantic={props.semantic as AnyProps}
83
106
  size={props.size as AnyProps}
84
107
  icon={icon}
85
108
  iconPosition={props.iconPosition as AnyProps}
109
+ disabled={props.disabled as AnyProps}
86
110
  onPress={noop}
87
111
  >
88
112
  Button
@@ -101,6 +125,33 @@ export function ComponentRenderer({ componentId, props }: ComponentRendererProps
101
125
  return <StatefulHueSlider {...props} />;
102
126
  case "card":
103
127
  return <CardPreview {...props} />;
128
+ case "text":
129
+ return (
130
+ <Text
131
+ size={props.size as AnyProps}
132
+ weight={props.weight as AnyProps}
133
+ color={props.color as AnyProps}
134
+ font={props.font as AnyProps}
135
+ >
136
+ The quick brown fox
137
+ </Text>
138
+ );
139
+ case "icon":
140
+ return (
141
+ <Icon
142
+ name={(props.name as string) ?? "home"}
143
+ size={props.size as AnyProps}
144
+ fill={props.fill as AnyProps}
145
+ />
146
+ );
147
+ case "frame":
148
+ return (
149
+ <FramePreview {...props} />
150
+ );
151
+ case "wrapper":
152
+ return (
153
+ <WrapperPreview {...props} />
154
+ );
104
155
  default:
105
156
  return null;
106
157
  }