@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,9 +1,23 @@
1
- import { useState } from "react";
2
- import { useTokens } from "@newtonedev/components";
1
+ import { useState, useCallback, useMemo, useRef, useEffect } from "react";
2
+ import { useTokens, useNewtoneTheme, NewtoneProvider } from "@newtonedev/components";
3
3
  import { srgbToHex } from "newtone";
4
4
  import { getComponent } from "@newtonedev/components";
5
+ import type { TextRole, BreakpointKey, TextSize, RoleScales } from "@newtonedev/fonts";
6
+ import { ROLE_DEFAULT_WEIGHTS, BREAKPOINT_ROLE_SCALE, scaleRoleStep, resolveResponsiveSize } from "@newtonedev/fonts";
5
7
  import { ComponentRenderer } from "./ComponentRenderer";
6
8
  import { IconBrowserView } from "./IconBrowserView";
9
+ import type { EditorFontEntry } from "../types";
10
+
11
+ /** Variant IDs that map to unique text roles and get inline weight controls. */
12
+ const ROLE_VARIANT_IDS = new Set([
13
+ "body",
14
+ "headline",
15
+ "title",
16
+ "heading",
17
+ "subheading",
18
+ "label",
19
+ "caption",
20
+ ]);
7
21
 
8
22
  interface ComponentDetailViewProps {
9
23
  readonly componentId: string;
@@ -11,6 +25,263 @@ interface ComponentDetailViewProps {
11
25
  readonly onSelectVariant: (variantId: string) => void;
12
26
  readonly propOverrides?: Record<string, unknown>;
13
27
  readonly onPropOverride?: (name: string, value: unknown) => void;
28
+ readonly roleWeights?: Partial<Record<TextRole, number>>;
29
+ readonly onRoleWeightChange?: (role: TextRole, weight: number) => void;
30
+ readonly fontCatalog?: readonly EditorFontEntry[];
31
+ readonly scopeFontMap?: Record<string, string>;
32
+ }
33
+
34
+ /**
35
+ * Determine what kind of weight control to show for a font.
36
+ * Returns 'slider' for variable fonts (smooth) and fixed-weight fonts (with stops),
37
+ * or 'none' for single-weight fonts.
38
+ */
39
+ function getWeightControlType(
40
+ family: string | undefined,
41
+ fontCatalog: readonly EditorFontEntry[] | undefined,
42
+ ): { type: "slider"; min: number; max: number; stops?: readonly number[] } | { type: "none" } {
43
+ if (!family) return { type: "none" };
44
+
45
+ const entry = fontCatalog?.find((f) => f.family === family);
46
+
47
+ if (entry) {
48
+ if (entry.isVariable) {
49
+ return {
50
+ type: "slider",
51
+ min: entry.weightAxisRange?.min ?? 100,
52
+ max: entry.weightAxisRange?.max ?? 900,
53
+ };
54
+ }
55
+ if (entry.availableWeights && entry.availableWeights.length > 1) {
56
+ const sorted = [...entry.availableWeights].sort((a, b) => a - b);
57
+ return { type: "slider", min: sorted[0], max: sorted[sorted.length - 1], stops: sorted };
58
+ }
59
+ return { type: "none" };
60
+ }
61
+
62
+ // Not in catalog — system font or unknown → treat as variable
63
+ return { type: "slider", min: 100, max: 900 };
64
+ }
65
+
66
+ /**
67
+ * Unified weight slider. Smooth for variable fonts, snaps to stops for fixed-weight fonts.
68
+ * Renders tick marks when stops are provided.
69
+ */
70
+ function WeightSlider({
71
+ value,
72
+ min,
73
+ max,
74
+ stops,
75
+ onChange,
76
+ textColor,
77
+ accentColor,
78
+ }: {
79
+ readonly value: number;
80
+ readonly min: number;
81
+ readonly max: number;
82
+ readonly stops?: readonly number[];
83
+ readonly onChange: (v: number) => void;
84
+ readonly textColor: string;
85
+ readonly accentColor: string;
86
+ }) {
87
+ const snap = useCallback(
88
+ (v: number) => {
89
+ if (!stops) return v;
90
+ return stops.reduce((prev, curr) =>
91
+ Math.abs(curr - v) < Math.abs(prev - v) ? curr : prev,
92
+ );
93
+ },
94
+ [stops],
95
+ );
96
+
97
+ const handleChange = useCallback(
98
+ (e: React.ChangeEvent<HTMLInputElement>) => {
99
+ onChange(snap(Number(e.target.value)));
100
+ },
101
+ [onChange, snap],
102
+ );
103
+
104
+ const range = max - min;
105
+
106
+ return (
107
+ <div
108
+ style={{
109
+ display: "flex",
110
+ alignItems: "center",
111
+ gap: 8,
112
+ flexShrink: 0,
113
+ }}
114
+ onClick={(e) => e.stopPropagation()}
115
+ >
116
+ <div style={{ position: "relative", width: 80 }}>
117
+ <input
118
+ type="range"
119
+ min={min}
120
+ max={max}
121
+ step={1}
122
+ value={value}
123
+ onChange={handleChange}
124
+ style={{
125
+ width: 80,
126
+ accentColor,
127
+ cursor: "pointer",
128
+ display: "block",
129
+ }}
130
+ />
131
+ {stops && range > 0 && (
132
+ <div
133
+ style={{
134
+ position: "relative",
135
+ width: 80,
136
+ height: 4,
137
+ marginTop: -2,
138
+ pointerEvents: "none",
139
+ }}
140
+ >
141
+ {stops.map((s) => (
142
+ <div
143
+ key={s}
144
+ style={{
145
+ position: "absolute",
146
+ left: `${((s - min) / range) * 100}%`,
147
+ width: 2,
148
+ height: 4,
149
+ backgroundColor: s === value ? accentColor : textColor,
150
+ opacity: s === value ? 1 : 0.3,
151
+ borderRadius: 1,
152
+ transform: "translateX(-50%)",
153
+ }}
154
+ />
155
+ ))}
156
+ </div>
157
+ )}
158
+ </div>
159
+ <span
160
+ style={{
161
+ fontSize: 11,
162
+ fontWeight: 500,
163
+ color: textColor,
164
+ width: 28,
165
+ textAlign: "right",
166
+ fontVariantNumeric: "tabular-nums",
167
+ }}
168
+ >
169
+ {value}
170
+ </span>
171
+ </div>
172
+ );
173
+ }
174
+
175
+ const PREVIEW_TEXT = "The quick brown fox";
176
+
177
+ /**
178
+ * Measures its container width via ResizeObserver and displays the live
179
+ * resolved fontSize/lineHeight from resolveResponsiveSize.
180
+ */
181
+ function TextAnnotation({
182
+ role,
183
+ roleScales,
184
+ fontFamily,
185
+ calibrations,
186
+ weight,
187
+ textColor,
188
+ accentColor,
189
+ previewText = PREVIEW_TEXT,
190
+ }: {
191
+ readonly role: TextRole;
192
+ readonly roleScales: RoleScales;
193
+ readonly fontFamily?: string;
194
+ readonly calibrations?: Record<string, number>;
195
+ readonly weight: number;
196
+ readonly textColor: string;
197
+ readonly accentColor: string;
198
+ readonly previewText?: string;
199
+ }) {
200
+ const containerRef = useRef<HTMLDivElement>(null);
201
+ const [containerWidth, setContainerWidth] = useState<number | null>(null);
202
+
203
+ useEffect(() => {
204
+ const el = containerRef.current?.parentElement;
205
+ if (!el) return;
206
+ const observer = new ResizeObserver((entries) => {
207
+ const w = entries[0]?.contentRect.width;
208
+ if (w && w > 0) setContainerWidth(w);
209
+ });
210
+ observer.observe(el);
211
+ return () => observer.disconnect();
212
+ }, []);
213
+
214
+ const step = roleScales[role].md;
215
+ const minFs = Math.max(8, Math.round(step.fontSize * 0.7));
216
+ const maxFs = step.fontSize;
217
+
218
+ const resolved = useMemo(() => {
219
+ if (containerWidth == null) return { fontSize: maxFs, lineHeight: step.lineHeight };
220
+ return resolveResponsiveSize(
221
+ {
222
+ role,
223
+ size: "md" as TextSize,
224
+ responsive: true,
225
+ fontFamily,
226
+ maxFontSize: maxFs,
227
+ minFontSize: minFs,
228
+ },
229
+ roleScales,
230
+ { containerWidth, characterCount: previewText.length },
231
+ calibrations,
232
+ );
233
+ }, [role, step, roleScales, fontFamily, calibrations, containerWidth, minFs, maxFs]);
234
+
235
+ const range = maxFs - minFs;
236
+ const position = range > 0 ? ((resolved.fontSize - minFs) / range) * 100 : 100;
237
+
238
+ return (
239
+ <div
240
+ ref={containerRef}
241
+ style={{
242
+ marginTop: 4,
243
+ fontSize: 10,
244
+ color: textColor,
245
+ fontVariantNumeric: "tabular-nums",
246
+ letterSpacing: 0.2,
247
+ }}
248
+ >
249
+ <span>{resolved.fontSize}/{resolved.lineHeight} · {weight}</span>
250
+ <div
251
+ style={{
252
+ display: "flex",
253
+ alignItems: "center",
254
+ gap: 4,
255
+ marginTop: 3,
256
+ }}
257
+ >
258
+ <span style={{ width: 16, textAlign: "right", flexShrink: 0 }}>{minFs}</span>
259
+ <div
260
+ style={{
261
+ flex: 1,
262
+ height: 1,
263
+ backgroundColor: textColor,
264
+ opacity: 0.3,
265
+ position: "relative",
266
+ }}
267
+ >
268
+ <div
269
+ style={{
270
+ position: "absolute",
271
+ left: `${position}%`,
272
+ top: "50%",
273
+ width: 6,
274
+ height: 6,
275
+ borderRadius: "50%",
276
+ backgroundColor: accentColor,
277
+ transform: "translate(-50%, -50%)",
278
+ }}
279
+ />
280
+ </div>
281
+ <span style={{ width: 16, flexShrink: 0 }}>{maxFs}</span>
282
+ </div>
283
+ </div>
284
+ );
14
285
  }
15
286
 
16
287
  export function ComponentDetailView({
@@ -19,10 +290,33 @@ export function ComponentDetailView({
19
290
  onSelectVariant,
20
291
  propOverrides,
21
292
  onPropOverride,
293
+ roleWeights,
294
+ onRoleWeightChange,
295
+ fontCatalog,
296
+ scopeFontMap,
22
297
  }: ComponentDetailViewProps) {
23
298
  const tokens = useTokens();
299
+ const { config } = useNewtoneTheme();
24
300
  const component = getComponent(componentId);
25
301
  const [hoveredId, setHoveredId] = useState<string | null>(null);
302
+ const [previewBreakpoint, setPreviewBreakpoint] = useState<BreakpointKey>("lg");
303
+ const [previewText, setPreviewText] = useState(PREVIEW_TEXT);
304
+
305
+ const scaledConfig = useMemo(() => {
306
+ if (previewBreakpoint === "lg") return config;
307
+ const scales = BREAKPOINT_ROLE_SCALE[previewBreakpoint];
308
+ const baseRoles = config.typography.roles;
309
+ const scaledRoles = {} as Record<string, Record<string, { fontSize: number; lineHeight: number }>>;
310
+ for (const role of Object.keys(baseRoles) as TextRole[]) {
311
+ const scale = scales[role];
312
+ scaledRoles[role] = {} as Record<string, { fontSize: number; lineHeight: number }>;
313
+ for (const size of ["sm", "md", "lg"] as TextSize[]) {
314
+ const step = baseRoles[role][size];
315
+ scaledRoles[role][size] = scale === 1.0 ? step : scaleRoleStep(step, scale);
316
+ }
317
+ }
318
+ return { ...config, typography: { ...config.typography, roles: scaledRoles as unknown as typeof config.typography.roles } };
319
+ }, [config, previewBreakpoint]);
26
320
 
27
321
  if (!component) return null;
28
322
 
@@ -67,6 +361,7 @@ export function ComponentDetailView({
67
361
  }
68
362
 
69
363
  const interactiveColor = srgbToHex(tokens.accent.fill.srgb);
364
+ const isTextComponent = componentId === "text";
70
365
 
71
366
  return (
72
367
  <div style={{ padding: 32 }}>
@@ -86,80 +381,249 @@ export function ComponentDetailView({
86
381
  fontSize: 14,
87
382
  color: srgbToHex(tokens.textSecondary.srgb),
88
383
  margin: 0,
89
- marginBottom: 32,
384
+ marginBottom: isTextComponent ? 16 : 32,
90
385
  }}
91
386
  >
92
387
  {component.description}
93
388
  </p>
94
389
 
95
- <div
96
- style={{
97
- display: "grid",
98
- gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))",
99
- gap: 16,
100
- }}
101
- >
102
- {component.variants.map((variant) => {
103
- const isSelected = selectedVariantId === variant.id;
104
- const isHovered = hoveredId === variant.id;
105
-
106
- const borderColor = isSelected
107
- ? interactiveColor
108
- : isHovered
109
- ? `${interactiveColor}66`
110
- : srgbToHex(tokens.border.srgb);
111
-
112
- return (
113
- <button
114
- key={variant.id}
115
- onClick={() => onSelectVariant(variant.id)}
116
- onMouseEnter={() => setHoveredId(variant.id)}
117
- onMouseLeave={() => setHoveredId(null)}
118
- style={{
119
- display: "flex",
120
- flexDirection: "column",
121
- alignItems: "stretch",
122
- padding: 16,
123
- borderRadius: 12,
124
- border: `2px solid ${borderColor}`,
125
- backgroundColor: srgbToHex(tokens.backgroundElevated.srgb),
126
- cursor: "pointer",
127
- textAlign: "left",
128
- transition: "border-color 150ms ease",
129
- }}
130
- >
131
- <div
390
+ {isTextComponent && (
391
+ <div style={{ display: "flex", flexDirection: "column", gap: 12, marginBottom: 16 }}>
392
+ <input
393
+ type="text"
394
+ value={previewText}
395
+ onChange={(e) => setPreviewText(e.target.value)}
396
+ placeholder={PREVIEW_TEXT}
397
+ style={{
398
+ width: "100%",
399
+ padding: "8px 12px",
400
+ fontSize: 14,
401
+ fontFamily: "'SF Mono', 'Fira Code', Menlo, monospace",
402
+ color: srgbToHex(tokens.textPrimary.srgb),
403
+ backgroundColor: srgbToHex(tokens.backgroundSunken.srgb),
404
+ border: `1px solid ${srgbToHex(tokens.border.srgb)}`,
405
+ borderRadius: 8,
406
+ boxSizing: "border-box",
407
+ outline: "none",
408
+ }}
409
+ />
410
+ <div style={{ display: "flex", gap: 2 }}>
411
+ {(["sm", "md", "lg"] as const).map((bp) => {
412
+ const isActive = previewBreakpoint === bp;
413
+ return (
414
+ <button
415
+ key={bp}
416
+ onClick={() => setPreviewBreakpoint(bp)}
417
+ style={{
418
+ padding: "4px 10px",
419
+ fontSize: 11,
420
+ fontWeight: isActive ? 600 : 400,
421
+ color: isActive ? interactiveColor : srgbToHex(tokens.textSecondary.srgb),
422
+ backgroundColor: isActive ? `${interactiveColor}18` : "transparent",
423
+ border: `1px solid ${isActive ? interactiveColor : srgbToHex(tokens.border.srgb)}`,
424
+ borderRadius: 4,
425
+ cursor: "pointer",
426
+ textTransform: "uppercase",
427
+ letterSpacing: 0.5,
428
+ }}
429
+ >
430
+ {bp}
431
+ </button>
432
+ );
433
+ })}
434
+ </div>
435
+ </div>
436
+ )}
437
+
438
+ {component.previewLayout === "list" ? (
439
+ <NewtoneProvider config={scaledConfig}>
440
+ <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
441
+ {component.variants.map((variant) => {
442
+ const isSelected = selectedVariantId === variant.id;
443
+ const isHovered = hoveredId === variant.id;
444
+
445
+ const borderColor = isSelected
446
+ ? interactiveColor
447
+ : isHovered
448
+ ? `${interactiveColor}66`
449
+ : srgbToHex(tokens.border.srgb);
450
+
451
+ // Weight control for text roles
452
+ const showWeightControl =
453
+ isTextComponent &&
454
+ ROLE_VARIANT_IDS.has(variant.id) &&
455
+ onRoleWeightChange;
456
+
457
+ const role = variant.props.role as string | undefined;
458
+ const scope = (variant.props.scope as string) ?? "main";
459
+
460
+ let weightControl: React.ReactNode = null;
461
+ if (showWeightControl && role) {
462
+ const family = scopeFontMap?.[scope];
463
+ const controlInfo = getWeightControlType(family, fontCatalog);
464
+ const currentWeight =
465
+ roleWeights?.[role as TextRole] ??
466
+ ROLE_DEFAULT_WEIGHTS[role as TextRole] ??
467
+ 400;
468
+
469
+ if (controlInfo.type === "slider") {
470
+ // Snap to closest stop for fixed-weight fonts
471
+ const displayWeight = controlInfo.stops
472
+ ? controlInfo.stops.reduce((prev, curr) =>
473
+ Math.abs(curr - currentWeight) < Math.abs(prev - currentWeight) ? curr : prev,
474
+ )
475
+ : currentWeight;
476
+
477
+ weightControl = (
478
+ <WeightSlider
479
+ value={displayWeight}
480
+ min={controlInfo.min}
481
+ max={controlInfo.max}
482
+ stops={controlInfo.stops}
483
+ onChange={(w) =>
484
+ onRoleWeightChange(role as TextRole, w)
485
+ }
486
+ textColor={srgbToHex(tokens.textSecondary.srgb)}
487
+ accentColor={interactiveColor}
488
+ />
489
+ );
490
+ }
491
+ }
492
+
493
+ return (
494
+ <button
495
+ key={variant.id}
496
+ onClick={() => onSelectVariant(variant.id)}
497
+ onMouseEnter={() => setHoveredId(variant.id)}
498
+ onMouseLeave={() => setHoveredId(null)}
499
+ style={{
500
+ display: "flex",
501
+ flexDirection: "row",
502
+ alignItems: "center",
503
+ gap: 16,
504
+ padding: "12px 16px",
505
+ borderRadius: 12,
506
+ border: `2px solid ${borderColor}`,
507
+ backgroundColor: srgbToHex(tokens.backgroundElevated.srgb),
508
+ cursor: "pointer",
509
+ textAlign: "left",
510
+ transition: "border-color 150ms ease",
511
+ }}
512
+ >
513
+ <span
514
+ style={{
515
+ fontSize: 11,
516
+ fontWeight: 500,
517
+ color: isSelected
518
+ ? interactiveColor
519
+ : srgbToHex(tokens.textSecondary.srgb),
520
+ width: 88,
521
+ flexShrink: 0,
522
+ textTransform: "uppercase",
523
+ letterSpacing: 0.5,
524
+ }}
525
+ >
526
+ {variant.label}
527
+ </span>
528
+ <div style={{ flex: 1, minWidth: 0 }}>
529
+ <ComponentRenderer
530
+ componentId={componentId}
531
+ props={variant.props}
532
+ previewText={previewText}
533
+ />
534
+ {isTextComponent && role && scaledConfig.typography.roles[role as TextRole] && (
535
+ <TextAnnotation
536
+ role={role as TextRole}
537
+ roleScales={scaledConfig.typography.roles}
538
+ fontFamily={scopeFontMap?.[scope]}
539
+ calibrations={scaledConfig.typography.calibrations}
540
+ weight={
541
+ roleWeights?.[role as TextRole] ??
542
+ ROLE_DEFAULT_WEIGHTS[role as TextRole] ??
543
+ 400
544
+ }
545
+ textColor={srgbToHex(tokens.textTertiary.srgb)}
546
+ accentColor={interactiveColor}
547
+ previewText={previewText}
548
+ />
549
+ )}
550
+ </div>
551
+ {weightControl && <>{weightControl}</>}
552
+ </button>
553
+ );
554
+ })}
555
+ </div>
556
+ </NewtoneProvider>
557
+ ) : (
558
+ <div
559
+ style={{
560
+ display: "grid",
561
+ gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))",
562
+ gap: 16,
563
+ }}
564
+ >
565
+ {component.variants.map((variant) => {
566
+ const isSelected = selectedVariantId === variant.id;
567
+ const isHovered = hoveredId === variant.id;
568
+
569
+ const borderColor = isSelected
570
+ ? interactiveColor
571
+ : isHovered
572
+ ? `${interactiveColor}66`
573
+ : srgbToHex(tokens.border.srgb);
574
+
575
+ return (
576
+ <button
577
+ key={variant.id}
578
+ onClick={() => onSelectVariant(variant.id)}
579
+ onMouseEnter={() => setHoveredId(variant.id)}
580
+ onMouseLeave={() => setHoveredId(null)}
132
581
  style={{
133
582
  display: "flex",
134
- alignItems: "center",
135
- justifyContent: "center",
136
- padding: 20,
137
- marginBottom: 12,
138
- borderRadius: 8,
139
- backgroundColor: srgbToHex(tokens.background.srgb),
140
- minHeight: 56,
583
+ flexDirection: "column",
584
+ alignItems: "stretch",
585
+ padding: 16,
586
+ borderRadius: 12,
587
+ border: `2px solid ${borderColor}`,
588
+ backgroundColor: srgbToHex(tokens.backgroundElevated.srgb),
589
+ cursor: "pointer",
590
+ textAlign: "left",
591
+ transition: "border-color 150ms ease",
141
592
  }}
142
593
  >
143
- <ComponentRenderer
144
- componentId={componentId}
145
- props={variant.props}
146
- />
147
- </div>
148
- <span
149
- style={{
150
- fontSize: 13,
151
- fontWeight: isSelected ? 600 : 500,
152
- color: isSelected
153
- ? interactiveColor
154
- : srgbToHex(tokens.textPrimary.srgb),
155
- }}
156
- >
157
- {variant.label}
158
- </span>
159
- </button>
160
- );
161
- })}
162
- </div>
594
+ <div
595
+ style={{
596
+ display: "flex",
597
+ alignItems: "center",
598
+ justifyContent: "center",
599
+ padding: 20,
600
+ marginBottom: 12,
601
+ borderRadius: 8,
602
+ backgroundColor: srgbToHex(tokens.background.srgb),
603
+ minHeight: 56,
604
+ }}
605
+ >
606
+ <ComponentRenderer
607
+ componentId={componentId}
608
+ props={variant.props}
609
+ />
610
+ </div>
611
+ <span
612
+ style={{
613
+ fontSize: 13,
614
+ fontWeight: isSelected ? 600 : 500,
615
+ color: isSelected
616
+ ? interactiveColor
617
+ : srgbToHex(tokens.textPrimary.srgb),
618
+ }}
619
+ >
620
+ {variant.label}
621
+ </span>
622
+ </button>
623
+ );
624
+ })}
625
+ </div>
626
+ )}
163
627
  </div>
164
628
  );
165
629
  }
@@ -18,6 +18,7 @@ import { srgbToHex } from "newtone";
18
18
  interface ComponentRendererProps {
19
19
  readonly componentId: string;
20
20
  readonly props: Record<string, unknown>;
21
+ readonly previewText?: string;
21
22
  }
22
23
 
23
24
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -93,7 +94,7 @@ function WrapperPreview(props: AnyProps) {
93
94
  );
94
95
  }
95
96
 
96
- export function ComponentRenderer({ componentId, props }: ComponentRendererProps) {
97
+ export function ComponentRenderer({ componentId, props, previewText }: ComponentRendererProps) {
97
98
  const noop = useCallback(() => {}, []);
98
99
 
99
100
  switch (componentId) {
@@ -128,12 +129,13 @@ export function ComponentRenderer({ componentId, props }: ComponentRendererProps
128
129
  case "text":
129
130
  return (
130
131
  <Text
132
+ scope={props.scope as AnyProps}
133
+ role={props.role as AnyProps}
131
134
  size={props.size as AnyProps}
132
- weight={props.weight as AnyProps}
133
135
  color={props.color as AnyProps}
134
- font={props.font as AnyProps}
136
+ responsive
135
137
  >
136
- The quick brown fox
138
+ {previewText || "The quick brown fox"}
137
139
  </Text>
138
140
  );
139
141
  case "icon":