@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.
- package/dist/Editor.d.ts +1 -1
- package/dist/Editor.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/PreviewWindow.d.ts +7 -2
- 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/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 +2484 -2052
- 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 +2486 -2055
- package/dist/index.js.map +1 -1
- package/dist/preview/ComponentDetailView.d.ts +7 -1
- 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/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 +53 -10
- package/src/components/ConfiguratorPanel.tsx +77 -0
- package/src/components/FontPicker.tsx +38 -29
- package/src/components/PreviewWindow.tsx +14 -1
- package/src/components/PrimaryNav.tsx +76 -0
- package/src/components/Sidebar.tsx +5 -132
- 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 -17
- package/src/index.ts +2 -0
- package/src/preview/ComponentDetailView.tsx +531 -67
- package/src/preview/ComponentRenderer.tsx +6 -4
- package/src/types.ts +15 -0
- package/src/utils/lookupFontMetrics.ts +52 -0
- 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
|
-
|
|
96
|
-
style={{
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
padding:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
backgroundColor: srgbToHex(tokens.
|
|
140
|
-
|
|
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
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
136
|
+
responsive
|
|
135
137
|
>
|
|
136
|
-
The quick brown fox
|
|
138
|
+
{previewText || "The quick brown fox"}
|
|
137
139
|
</Text>
|
|
138
140
|
);
|
|
139
141
|
case "icon":
|