@newtonedev/editor 0.1.5 → 0.1.7
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.
- package/dist/Editor.d.ts +1 -1
- package/dist/Editor.d.ts.map +1 -1
- package/dist/components/CodeBlock.d.ts.map +1 -1
- package/dist/components/ConfiguratorPanel.d.ts +17 -0
- package/dist/components/ConfiguratorPanel.d.ts.map +1 -0
- package/dist/components/FontPicker.d.ts +4 -2
- package/dist/components/FontPicker.d.ts.map +1 -1
- package/dist/components/PresetSelector.d.ts.map +1 -1
- package/dist/components/PreviewWindow.d.ts +9 -3
- package/dist/components/PreviewWindow.d.ts.map +1 -1
- package/dist/components/PrimaryNav.d.ts +7 -0
- package/dist/components/PrimaryNav.d.ts.map +1 -0
- package/dist/components/RightSidebar.d.ts +4 -1
- package/dist/components/RightSidebar.d.ts.map +1 -1
- package/dist/components/Sidebar.d.ts +1 -10
- package/dist/components/Sidebar.d.ts.map +1 -1
- package/dist/components/TableOfContents.d.ts +2 -1
- package/dist/components/TableOfContents.d.ts.map +1 -1
- package/dist/components/sections/DynamicRangeSection.d.ts.map +1 -1
- package/dist/components/sections/FontsSection.d.ts +3 -1
- package/dist/components/sections/FontsSection.d.ts.map +1 -1
- package/dist/hooks/useEditorState.d.ts +4 -1
- package/dist/hooks/useEditorState.d.ts.map +1 -1
- package/dist/index.cjs +2893 -2248
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2895 -2251
- package/dist/index.js.map +1 -1
- package/dist/preview/ComponentDetailView.d.ts +9 -2
- package/dist/preview/ComponentDetailView.d.ts.map +1 -1
- package/dist/preview/ComponentRenderer.d.ts +2 -1
- package/dist/preview/ComponentRenderer.d.ts.map +1 -1
- package/dist/preview/IconBrowserView.d.ts +7 -0
- package/dist/preview/IconBrowserView.d.ts.map +1 -0
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/lookupFontMetrics.d.ts +19 -0
- package/dist/utils/lookupFontMetrics.d.ts.map +1 -0
- package/dist/utils/measureFonts.d.ts +18 -0
- package/dist/utils/measureFonts.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/Editor.tsx +57 -11
- package/src/components/CodeBlock.tsx +42 -14
- package/src/components/ConfiguratorPanel.tsx +77 -0
- package/src/components/FontPicker.tsx +38 -29
- package/src/components/PresetSelector.tsx +8 -33
- package/src/components/PreviewWindow.tsx +20 -4
- package/src/components/PrimaryNav.tsx +76 -0
- package/src/components/RightSidebar.tsx +103 -40
- package/src/components/Sidebar.tsx +4 -211
- package/src/components/TableOfContents.tsx +41 -78
- package/src/components/sections/DynamicRangeSection.tsx +2 -225
- package/src/components/sections/FontsSection.tsx +61 -93
- package/src/hooks/useEditorState.ts +68 -9
- package/src/index.ts +2 -0
- package/src/preview/ComponentDetailView.tsx +576 -73
- package/src/preview/ComponentRenderer.tsx +6 -4
- package/src/preview/IconBrowserView.tsx +187 -0
- package/src/types.ts +15 -0
- package/src/utils/lookupFontMetrics.ts +52 -0
- package/src/utils/measureFonts.ts +41 -0
|
@@ -1,29 +1,367 @@
|
|
|
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";
|
|
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
|
+
]);
|
|
6
21
|
|
|
7
22
|
interface ComponentDetailViewProps {
|
|
8
23
|
readonly componentId: string;
|
|
9
24
|
readonly selectedVariantId: string | null;
|
|
10
|
-
readonly propOverrides?: Record<string, unknown>;
|
|
11
25
|
readonly onSelectVariant: (variantId: string) => void;
|
|
26
|
+
readonly propOverrides?: Record<string, unknown>;
|
|
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
|
+
);
|
|
12
285
|
}
|
|
13
286
|
|
|
14
287
|
export function ComponentDetailView({
|
|
15
288
|
componentId,
|
|
16
289
|
selectedVariantId,
|
|
17
|
-
propOverrides,
|
|
18
290
|
onSelectVariant,
|
|
291
|
+
propOverrides,
|
|
292
|
+
onPropOverride,
|
|
293
|
+
roleWeights,
|
|
294
|
+
onRoleWeightChange,
|
|
295
|
+
fontCatalog,
|
|
296
|
+
scopeFontMap,
|
|
19
297
|
}: ComponentDetailViewProps) {
|
|
20
298
|
const tokens = useTokens();
|
|
299
|
+
const { config } = useNewtoneTheme();
|
|
21
300
|
const component = getComponent(componentId);
|
|
22
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]);
|
|
23
320
|
|
|
24
321
|
if (!component) return null;
|
|
25
322
|
|
|
323
|
+
if (componentId === "icon" && propOverrides && onPropOverride) {
|
|
324
|
+
return (
|
|
325
|
+
<div
|
|
326
|
+
style={{
|
|
327
|
+
padding: "32px 0 0",
|
|
328
|
+
height: "100%",
|
|
329
|
+
display: "flex",
|
|
330
|
+
flexDirection: "column",
|
|
331
|
+
}}
|
|
332
|
+
>
|
|
333
|
+
<div style={{ padding: "0 32px", marginBottom: 24 }}>
|
|
334
|
+
<h2
|
|
335
|
+
style={{
|
|
336
|
+
fontSize: 22,
|
|
337
|
+
fontWeight: 700,
|
|
338
|
+
color: srgbToHex(tokens.textPrimary.srgb),
|
|
339
|
+
margin: 0,
|
|
340
|
+
marginBottom: 4,
|
|
341
|
+
}}
|
|
342
|
+
>
|
|
343
|
+
{component.name}
|
|
344
|
+
</h2>
|
|
345
|
+
<p
|
|
346
|
+
style={{
|
|
347
|
+
fontSize: 14,
|
|
348
|
+
color: srgbToHex(tokens.textSecondary.srgb),
|
|
349
|
+
margin: 0,
|
|
350
|
+
}}
|
|
351
|
+
>
|
|
352
|
+
{component.description}
|
|
353
|
+
</p>
|
|
354
|
+
</div>
|
|
355
|
+
<IconBrowserView
|
|
356
|
+
selectedIconName={(propOverrides.name as string) ?? "add"}
|
|
357
|
+
onIconSelect={(name) => onPropOverride("name", name)}
|
|
358
|
+
/>
|
|
359
|
+
</div>
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
26
363
|
const interactiveColor = srgbToHex(tokens.accent.fill.srgb);
|
|
364
|
+
const isTextComponent = componentId === "text";
|
|
27
365
|
|
|
28
366
|
return (
|
|
29
367
|
<div style={{ padding: 32 }}>
|
|
@@ -43,84 +381,249 @@ export function ComponentDetailView({
|
|
|
43
381
|
fontSize: 14,
|
|
44
382
|
color: srgbToHex(tokens.textSecondary.srgb),
|
|
45
383
|
margin: 0,
|
|
46
|
-
marginBottom: 32,
|
|
384
|
+
marginBottom: isTextComponent ? 16 : 32,
|
|
47
385
|
}}
|
|
48
386
|
>
|
|
49
387
|
{component.description}
|
|
50
388
|
</p>
|
|
51
389
|
|
|
52
|
-
|
|
53
|
-
style={{
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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)}
|
|
89
581
|
style={{
|
|
90
582
|
display: "flex",
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
padding:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
backgroundColor: srgbToHex(tokens.
|
|
97
|
-
|
|
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",
|
|
98
592
|
}}
|
|
99
593
|
>
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
+
)}
|
|
124
627
|
</div>
|
|
125
628
|
);
|
|
126
629
|
}
|