@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.
Files changed (62) hide show
  1. package/dist/Editor.d.ts +1 -1
  2. package/dist/Editor.d.ts.map +1 -1
  3. package/dist/components/CodeBlock.d.ts.map +1 -1
  4. package/dist/components/ConfiguratorPanel.d.ts +17 -0
  5. package/dist/components/ConfiguratorPanel.d.ts.map +1 -0
  6. package/dist/components/FontPicker.d.ts +4 -2
  7. package/dist/components/FontPicker.d.ts.map +1 -1
  8. package/dist/components/PresetSelector.d.ts.map +1 -1
  9. package/dist/components/PreviewWindow.d.ts +9 -3
  10. package/dist/components/PreviewWindow.d.ts.map +1 -1
  11. package/dist/components/PrimaryNav.d.ts +7 -0
  12. package/dist/components/PrimaryNav.d.ts.map +1 -0
  13. package/dist/components/RightSidebar.d.ts +4 -1
  14. package/dist/components/RightSidebar.d.ts.map +1 -1
  15. package/dist/components/Sidebar.d.ts +1 -10
  16. package/dist/components/Sidebar.d.ts.map +1 -1
  17. package/dist/components/TableOfContents.d.ts +2 -1
  18. package/dist/components/TableOfContents.d.ts.map +1 -1
  19. package/dist/components/sections/DynamicRangeSection.d.ts.map +1 -1
  20. package/dist/components/sections/FontsSection.d.ts +3 -1
  21. package/dist/components/sections/FontsSection.d.ts.map +1 -1
  22. package/dist/hooks/useEditorState.d.ts +4 -1
  23. package/dist/hooks/useEditorState.d.ts.map +1 -1
  24. package/dist/index.cjs +2893 -2248
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.ts +2 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +2895 -2251
  29. package/dist/index.js.map +1 -1
  30. package/dist/preview/ComponentDetailView.d.ts +9 -2
  31. package/dist/preview/ComponentDetailView.d.ts.map +1 -1
  32. package/dist/preview/ComponentRenderer.d.ts +2 -1
  33. package/dist/preview/ComponentRenderer.d.ts.map +1 -1
  34. package/dist/preview/IconBrowserView.d.ts +7 -0
  35. package/dist/preview/IconBrowserView.d.ts.map +1 -0
  36. package/dist/types.d.ts +17 -0
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/utils/lookupFontMetrics.d.ts +19 -0
  39. package/dist/utils/lookupFontMetrics.d.ts.map +1 -0
  40. package/dist/utils/measureFonts.d.ts +18 -0
  41. package/dist/utils/measureFonts.d.ts.map +1 -0
  42. package/package.json +1 -1
  43. package/src/Editor.tsx +57 -11
  44. package/src/components/CodeBlock.tsx +42 -14
  45. package/src/components/ConfiguratorPanel.tsx +77 -0
  46. package/src/components/FontPicker.tsx +38 -29
  47. package/src/components/PresetSelector.tsx +8 -33
  48. package/src/components/PreviewWindow.tsx +20 -4
  49. package/src/components/PrimaryNav.tsx +76 -0
  50. package/src/components/RightSidebar.tsx +103 -40
  51. package/src/components/Sidebar.tsx +4 -211
  52. package/src/components/TableOfContents.tsx +41 -78
  53. package/src/components/sections/DynamicRangeSection.tsx +2 -225
  54. package/src/components/sections/FontsSection.tsx +61 -93
  55. package/src/hooks/useEditorState.ts +68 -9
  56. package/src/index.ts +2 -0
  57. package/src/preview/ComponentDetailView.tsx +576 -73
  58. package/src/preview/ComponentRenderer.tsx +6 -4
  59. package/src/preview/IconBrowserView.tsx +187 -0
  60. package/src/types.ts +15 -0
  61. package/src/utils/lookupFontMetrics.ts +52 -0
  62. package/src/utils/measureFonts.ts +41 -0
@@ -1,13 +1,13 @@
1
1
  import { useState } from "react";
2
2
  import {
3
3
  useTokens,
4
- CATEGORIES,
5
4
  getComponentsByCategory,
6
5
  } from "@newtonedev/components";
7
6
  import { srgbToHex } from "newtone";
8
7
  import type { PreviewView } from "../types";
9
8
 
10
9
  interface TableOfContentsProps {
10
+ readonly activeSectionId: string;
11
11
  readonly activeView: PreviewView;
12
12
  readonly selectedComponentId: string | null;
13
13
  readonly onNavigate: (view: PreviewView) => void;
@@ -16,6 +16,7 @@ interface TableOfContentsProps {
16
16
  const TOC_WIDTH = 220;
17
17
 
18
18
  export function TableOfContents({
19
+ activeSectionId,
19
20
  activeView,
20
21
  selectedComponentId,
21
22
  onNavigate,
@@ -26,10 +27,13 @@ export function TableOfContents({
26
27
  const borderColor = srgbToHex(tokens.border.srgb);
27
28
  const activeColor = srgbToHex(tokens.accent.fill.srgb);
28
29
  const textPrimary = srgbToHex(tokens.textPrimary.srgb);
29
- const textSecondary = srgbToHex(tokens.textSecondary.srgb);
30
30
  const hoverBg = `${borderColor}20`;
31
31
 
32
- const isOverviewActive = activeView.kind === "overview";
32
+ const components = getComponentsByCategory(activeSectionId);
33
+
34
+ const isOverviewActive =
35
+ activeView.kind === "overview" ||
36
+ (activeView.kind === "category" && activeView.categoryId === activeSectionId);
33
37
 
34
38
  return (
35
39
  <nav
@@ -44,7 +48,7 @@ export function TableOfContents({
44
48
  }}
45
49
  >
46
50
  <button
47
- onClick={() => onNavigate({ kind: "overview" })}
51
+ onClick={() => onNavigate({ kind: "category", categoryId: activeSectionId })}
48
52
  onMouseEnter={() => setHoveredId("overview")}
49
53
  onMouseLeave={() => setHoveredId(null)}
50
54
  aria-current={isOverviewActive ? "page" : undefined}
@@ -69,82 +73,41 @@ export function TableOfContents({
69
73
  Overview
70
74
  </button>
71
75
 
72
- {CATEGORIES.map((category) => {
73
- const components = getComponentsByCategory(category.id);
74
- const isCategoryActive =
75
- activeView.kind === "category" &&
76
- activeView.categoryId === category.id;
76
+ {components.map((comp) => {
77
+ const isComponentActive =
78
+ (activeView.kind === "component" &&
79
+ activeView.componentId === comp.id) ||
80
+ selectedComponentId === comp.id;
77
81
 
78
82
  return (
79
- <div key={category.id} style={{ marginTop: 16 }}>
80
- <button
81
- onClick={() =>
82
- onNavigate({ kind: "category", categoryId: category.id })
83
- }
84
- onMouseEnter={() => setHoveredId(`cat-${category.id}`)}
85
- onMouseLeave={() => setHoveredId(null)}
86
- aria-current={isCategoryActive ? "page" : undefined}
87
- style={{
88
- display: "block",
89
- width: "100%",
90
- padding: "6px 20px",
91
- border: "none",
92
- background: isCategoryActive
93
- ? `${activeColor}14`
94
- : hoveredId === `cat-${category.id}`
95
- ? hoverBg
96
- : "none",
97
- cursor: "pointer",
98
- textAlign: "left",
99
- fontSize: 11,
100
- fontWeight: 600,
101
- color: isCategoryActive ? activeColor : textSecondary,
102
- textTransform: "uppercase",
103
- letterSpacing: 0.5,
104
- transition: "background-color 100ms ease",
105
- }}
106
- >
107
- {category.name}
108
- </button>
109
-
110
- {components.map((comp) => {
111
- const isComponentActive =
112
- (activeView.kind === "component" &&
113
- activeView.componentId === comp.id) ||
114
- selectedComponentId === comp.id;
115
-
116
- return (
117
- <button
118
- key={comp.id}
119
- onClick={() =>
120
- onNavigate({ kind: "component", componentId: comp.id })
121
- }
122
- onMouseEnter={() => setHoveredId(comp.id)}
123
- onMouseLeave={() => setHoveredId(null)}
124
- aria-current={isComponentActive ? "page" : undefined}
125
- style={{
126
- display: "block",
127
- width: "100%",
128
- padding: "4px 20px 4px 32px",
129
- border: "none",
130
- background: isComponentActive
131
- ? `${activeColor}14`
132
- : hoveredId === comp.id
133
- ? hoverBg
134
- : "none",
135
- cursor: "pointer",
136
- textAlign: "left",
137
- fontSize: 13,
138
- fontWeight: isComponentActive ? 600 : 400,
139
- color: isComponentActive ? activeColor : textPrimary,
140
- transition: "background-color 100ms ease",
141
- }}
142
- >
143
- {comp.name}
144
- </button>
145
- );
146
- })}
147
- </div>
83
+ <button
84
+ key={comp.id}
85
+ onClick={() =>
86
+ onNavigate({ kind: "component", componentId: comp.id })
87
+ }
88
+ onMouseEnter={() => setHoveredId(comp.id)}
89
+ onMouseLeave={() => setHoveredId(null)}
90
+ aria-current={isComponentActive ? "page" : undefined}
91
+ style={{
92
+ display: "block",
93
+ width: "100%",
94
+ padding: "4px 20px",
95
+ border: "none",
96
+ background: isComponentActive
97
+ ? `${activeColor}14`
98
+ : hoveredId === comp.id
99
+ ? hoverBg
100
+ : "none",
101
+ cursor: "pointer",
102
+ textAlign: "left",
103
+ fontSize: 13,
104
+ fontWeight: isComponentActive ? 600 : 400,
105
+ color: isComponentActive ? activeColor : textPrimary,
106
+ transition: "background-color 100ms ease",
107
+ }}
108
+ >
109
+ {comp.name}
110
+ </button>
148
111
  );
149
112
  })}
150
113
  </nav>
@@ -1,20 +1,9 @@
1
- import { useState, useRef, useCallback, useMemo, useEffect } from "react";
1
+ import { useState, useRef, useCallback } from "react";
2
2
  import { HueSlider, Select, useTokens } from "@newtonedev/components";
3
- import {
4
- srgbToHex,
5
- resolveLightness,
6
- findMaxChromaInGamut,
7
- oklchToSrgb,
8
- clampSrgb,
9
- HUE_GRADING_STRENGTH_LOW,
10
- HUE_GRADING_STRENGTH_MEDIUM,
11
- HUE_GRADING_STRENGTH_HARD,
12
- HUE_GRADING_EASING_POWER,
13
- } from "newtone";
3
+ import { srgbToHex } from "newtone";
14
4
  import type { HueGradingStrength } from "newtone";
15
5
  import type { ConfiguratorState } from "@newtonedev/configurator";
16
6
  import type { ConfiguratorAction } from "@newtonedev/configurator";
17
- import { traditionalHueToOklch } from "@newtonedev/configurator";
18
7
 
19
8
  const STRENGTH_OPTIONS = [
20
9
  { label: "None", value: "none" },
@@ -275,215 +264,6 @@ function RangeInput({ display, onCommit, toInternal }: RangeInputProps) {
275
264
  );
276
265
  }
277
266
 
278
- // --- Dynamic Range Graph ---
279
-
280
- const GRAPH_HEIGHT = 80;
281
- const GRAPH_COLS = 256;
282
- const GRAPH_ROWS = 64;
283
-
284
- function strengthToFactor(strength: HueGradingStrength): number {
285
- switch (strength) {
286
- case "none":
287
- return 0;
288
- case "low":
289
- return HUE_GRADING_STRENGTH_LOW;
290
- case "medium":
291
- return HUE_GRADING_STRENGTH_MEDIUM;
292
- case "hard":
293
- return HUE_GRADING_STRENGTH_HARD;
294
- }
295
- }
296
-
297
- function blendHues(
298
- lightHue: number,
299
- darkHue: number,
300
- wLight: number,
301
- wDark: number,
302
- ): number {
303
- const totalW = wLight + wDark;
304
- if (totalW === 0) return 0;
305
- const delta = (((darkHue - lightHue + 180) % 360) + 360) % 360 - 180;
306
- const t = wDark / totalW;
307
- const result = lightHue + delta * t;
308
- return ((result % 360) + 360) % 360;
309
- }
310
-
311
- interface GraphData {
312
- readonly buffer: Uint8ClampedArray;
313
- readonly curvePoints: readonly { readonly x: number; readonly y: number }[];
314
- }
315
-
316
- function computeGraphData(state: ConfiguratorState): GraphData {
317
- const { dynamicRange, globalHueGrading } = state;
318
-
319
- const lightActive = globalHueGrading.light.strength !== "none";
320
- const darkActive = globalHueGrading.dark.strength !== "none";
321
- const lightOklchHue = traditionalHueToOklch(globalHueGrading.light.hue);
322
- const darkOklchHue = traditionalHueToOklch(globalHueGrading.dark.hue);
323
- const lightFactor = strengthToFactor(globalHueGrading.light.strength);
324
- const darkFactor = strengthToFactor(globalHueGrading.dark.strength);
325
-
326
- const buffer = new Uint8ClampedArray(GRAPH_COLS * GRAPH_ROWS * 4);
327
-
328
- for (let col = 0; col < GRAPH_COLS; col++) {
329
- const nv = 1 - col / (GRAPH_COLS - 1);
330
- const L = resolveLightness(dynamicRange, nv);
331
-
332
- // Easing weights for hue blend at top row (assumes hard strength)
333
- const wLight = lightActive ? Math.pow(nv, HUE_GRADING_EASING_POWER) : 0;
334
- const wDark = darkActive
335
- ? Math.pow(1 - nv, HUE_GRADING_EASING_POWER)
336
- : 0;
337
- const totalW = wLight + wDark;
338
-
339
- let topHue: number;
340
- let topChroma: number;
341
-
342
- if (totalW === 0) {
343
- topHue = 0;
344
- topChroma = 0;
345
- } else {
346
- if (!lightActive) {
347
- topHue = darkOklchHue;
348
- } else if (!darkActive) {
349
- topHue = lightOklchHue;
350
- } else {
351
- topHue = blendHues(lightOklchHue, darkOklchHue, wLight, wDark);
352
- }
353
- topChroma =
354
- findMaxChromaInGamut(L, topHue) * Math.min(totalW, 1);
355
- }
356
-
357
- for (let row = 0; row < GRAPH_ROWS; row++) {
358
- const gradingIntensity = row / (GRAPH_ROWS - 1);
359
- const C = topChroma * gradingIntensity;
360
- const srgb = clampSrgb(oklchToSrgb({ L, C, h: topHue }));
361
-
362
- // Canvas Y=0 is top; row=0 is bottom of our graph
363
- const canvasY = GRAPH_ROWS - 1 - row;
364
- const idx = (canvasY * GRAPH_COLS + col) * 4;
365
- buffer[idx] = Math.round(srgb.r * 255);
366
- buffer[idx + 1] = Math.round(srgb.g * 255);
367
- buffer[idx + 2] = Math.round(srgb.b * 255);
368
- buffer[idx + 3] = 255;
369
- }
370
- }
371
-
372
- // 26-step curve points
373
- const curvePoints: { x: number; y: number }[] = [];
374
- for (let i = 0; i < 26; i++) {
375
- const nv = 1 - i / 25;
376
- const x = (i / 25) * (GRAPH_COLS - 1);
377
-
378
- const lightContrib =
379
- Math.pow(nv, HUE_GRADING_EASING_POWER) *
380
- (lightFactor / HUE_GRADING_STRENGTH_HARD);
381
- const darkContrib =
382
- Math.pow(1 - nv, HUE_GRADING_EASING_POWER) *
383
- (darkFactor / HUE_GRADING_STRENGTH_HARD);
384
- const y = clamp(lightContrib + darkContrib, 0, 1);
385
-
386
- curvePoints.push({ x, y });
387
- }
388
-
389
- return { buffer, curvePoints };
390
- }
391
-
392
- interface DynamicRangeGraphProps {
393
- readonly state: ConfiguratorState;
394
- }
395
-
396
- function DynamicRangeGraph({ state }: DynamicRangeGraphProps) {
397
- const tokens = useTokens();
398
- const canvasRef = useRef<HTMLCanvasElement>(null);
399
-
400
- const graphData = useMemo(
401
- () => computeGraphData(state),
402
- [
403
- state.dynamicRange.lightest,
404
- state.dynamicRange.darkest,
405
- state.globalHueGrading.light.strength,
406
- state.globalHueGrading.light.hue,
407
- state.globalHueGrading.dark.strength,
408
- state.globalHueGrading.dark.hue,
409
- ],
410
- );
411
-
412
- useEffect(() => {
413
- const canvas = canvasRef.current;
414
- if (!canvas) return;
415
-
416
- canvas.width = GRAPH_COLS;
417
- canvas.height = GRAPH_ROWS;
418
-
419
- const ctx = canvas.getContext("2d");
420
- if (!ctx) return;
421
-
422
- // Draw gradient from pre-computed buffer
423
- const imageData = ctx.createImageData(GRAPH_COLS, GRAPH_ROWS);
424
- imageData.data.set(graphData.buffer);
425
- ctx.putImageData(imageData, 0, 0);
426
-
427
- // Draw 26-step curve overlay
428
- const curveColor = srgbToHex(tokens.accent.fill.srgb);
429
- const { curvePoints } = graphData;
430
-
431
- if (curvePoints.length < 2) return;
432
-
433
- const mapped = curvePoints.map((p) => ({
434
- cx: p.x,
435
- cy: (1 - p.y) * (GRAPH_ROWS - 1),
436
- }));
437
-
438
- // Smooth Catmull-Rom spline
439
- ctx.beginPath();
440
- ctx.strokeStyle = curveColor;
441
- ctx.lineWidth = 1.5;
442
- ctx.lineJoin = "round";
443
- ctx.lineCap = "round";
444
-
445
- ctx.moveTo(mapped[0].cx, mapped[0].cy);
446
- for (let i = 0; i < mapped.length - 1; i++) {
447
- const p0 = mapped[Math.max(0, i - 1)];
448
- const p1 = mapped[i];
449
- const p2 = mapped[i + 1];
450
- const p3 = mapped[Math.min(mapped.length - 1, i + 2)];
451
-
452
- const cp1x = p1.cx + (p2.cx - p0.cx) / 6;
453
- const cp1y = p1.cy + (p2.cy - p0.cy) / 6;
454
- const cp2x = p2.cx - (p3.cx - p1.cx) / 6;
455
- const cp2y = p2.cy - (p3.cy - p1.cy) / 6;
456
-
457
- ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, p2.cx, p2.cy);
458
- }
459
- ctx.stroke();
460
-
461
- // Draw dots at each of the 26 steps
462
- ctx.fillStyle = curveColor;
463
- for (const p of mapped) {
464
- ctx.beginPath();
465
- ctx.arc(p.cx, p.cy, 2, 0, Math.PI * 2);
466
- ctx.fill();
467
- }
468
- }, [graphData, tokens]);
469
-
470
- const borderColor = srgbToHex(tokens.border.srgb);
471
-
472
- return (
473
- <canvas
474
- ref={canvasRef}
475
- style={{
476
- width: "100%",
477
- height: GRAPH_HEIGHT,
478
- borderRadius: 6,
479
- border: `1px solid ${borderColor}`,
480
- display: "block",
481
- overflow: "hidden",
482
- }}
483
- />
484
- );
485
- }
486
-
487
267
  // --- Section ---
488
268
 
489
269
  interface DynamicRangeSectionProps {
@@ -511,9 +291,6 @@ export function DynamicRangeSection({
511
291
 
512
292
  return (
513
293
  <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
514
- {/* Dynamic range graph */}
515
- <DynamicRangeGraph state={state} />
516
-
517
294
  {/* Labels above slider */}
518
295
  <div
519
296
  style={{
@@ -1,18 +1,13 @@
1
- import { Slider, useTokens } from "@newtonedev/components";
1
+ import { Slider } from "@newtonedev/components";
2
+ import { useTokens } from "@newtonedev/components";
2
3
  import type { FontConfig } from "@newtonedev/components";
3
4
  import { srgbToHex } from "newtone";
4
- import type { ConfiguratorState } from "@newtonedev/configurator";
5
+ import type { ConfiguratorState, FontScope, FontSlotConfig } from "@newtonedev/configurator";
5
6
  import type { ConfiguratorAction } from "@newtonedev/configurator";
7
+ import type { GoogleFontEntry } from "@newtonedev/fonts";
6
8
  import { FontPicker } from "../FontPicker";
7
9
 
8
- const DEFAULT_FONT_DEFAULT: FontConfig = {
9
- type: "system",
10
- family: "system-ui",
11
- fallback:
12
- '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
13
- };
14
-
15
- const DEFAULT_FONT_DISPLAY: FontConfig = {
10
+ const DEFAULT_FONT_SYSTEM: FontConfig = {
16
11
  type: "system",
17
12
  family: "system-ui",
18
13
  fallback:
@@ -25,107 +20,80 @@ const DEFAULT_FONT_MONO: FontConfig = {
25
20
  fallback: "SFMono-Regular, Menlo, Monaco, Consolas, monospace",
26
21
  };
27
22
 
23
+ function getDefaultFontConfig(scope: FontScope): FontConfig {
24
+ return scope === "mono" ? DEFAULT_FONT_MONO : DEFAULT_FONT_SYSTEM;
25
+ }
26
+
27
+ function getCurrentFontConfig(
28
+ state: ConfiguratorState,
29
+ scope: FontScope,
30
+ ): FontConfig {
31
+ return state.typography?.fonts[scope]?.config ?? getDefaultFontConfig(scope);
32
+ }
33
+
28
34
  interface FontsSectionProps {
29
35
  readonly state: ConfiguratorState;
30
36
  readonly dispatch: (action: ConfiguratorAction) => void;
37
+ readonly fontCatalog?: readonly GoogleFontEntry[];
31
38
  }
32
39
 
33
- export function FontsSection({ state, dispatch }: FontsSectionProps) {
34
- const tokens = useTokens();
40
+ const FONT_SCOPES: readonly { scope: FontScope; label: string; slot: "default" | "display" | "mono" | "currency" }[] = [
41
+ { scope: "main", label: "Main", slot: "default" },
42
+ { scope: "display", label: "Display", slot: "display" },
43
+ { scope: "mono", label: "Mono", slot: "mono" },
44
+ { scope: "currency", label: "Currency", slot: "currency" },
45
+ ];
35
46
 
36
- const baseSize = state.typography?.scale.baseSize ?? 16;
37
- const ratio = state.typography?.scale.ratio ?? 1.25;
47
+ export function FontsSection({ state, dispatch, fontCatalog }: FontsSectionProps) {
48
+ const tokens = useTokens();
38
49
 
39
50
  const labelColor = srgbToHex(tokens.textSecondary.srgb);
40
51
 
41
- const handleFontChange = (
42
- slot: "default" | "display" | "mono",
43
- font: FontConfig,
44
- ) => {
45
- const actionType = {
46
- default: "SET_FONT_DEFAULT",
47
- display: "SET_FONT_DISPLAY",
48
- mono: "SET_FONT_MONO",
49
- }[slot] as "SET_FONT_DEFAULT" | "SET_FONT_DISPLAY" | "SET_FONT_MONO";
50
- dispatch({ type: actionType, font });
52
+ const handleFontChange = (scope: FontScope, font: FontConfig) => {
53
+ // Preserve existing weight slots when switching font family
54
+ const weights = state.typography?.fonts[scope]?.weights ?? { regular: 400, medium: 500, bold: 700 };
55
+ const slotConfig: FontSlotConfig = { config: font, weights };
56
+ dispatch({ type: "SET_FONT", scope, font: slotConfig });
57
+ };
58
+
59
+ const sectionLabelStyle = {
60
+ fontSize: 11,
61
+ fontWeight: 600 as const,
62
+ color: labelColor,
63
+ textTransform: "uppercase" as const,
64
+ letterSpacing: 0.5,
65
+ marginBottom: 8,
51
66
  };
52
67
 
53
68
  return (
54
69
  <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
55
70
  <div>
56
- <div
57
- style={{
58
- fontSize: 11,
59
- fontWeight: 600,
60
- color: labelColor,
61
- textTransform: "uppercase",
62
- letterSpacing: 0.5,
63
- marginBottom: 8,
64
- }}
65
- >
66
- Scale
67
- </div>
71
+ <div style={sectionLabelStyle}>Fonts</div>
68
72
  <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
69
- <Slider
70
- value={baseSize}
71
- onValueChange={(v) =>
72
- dispatch({ type: "SET_TYPOGRAPHY_BASE_SIZE", baseSize: v })
73
- }
74
- min={12}
75
- max={24}
76
- step={1}
77
- label="Base Size"
78
- showValue
79
- />
80
- <Slider
81
- value={Math.round(ratio * 100)}
82
- onValueChange={(v) =>
83
- dispatch({ type: "SET_TYPOGRAPHY_RATIO", ratio: v / 100 })
84
- }
85
- min={110}
86
- max={150}
87
- step={5}
88
- label="Scale Ratio"
89
- showValue
90
- />
73
+ {FONT_SCOPES.map(({ scope, label, slot }) => (
74
+ <FontPicker
75
+ key={scope}
76
+ label={label}
77
+ slot={slot}
78
+ currentFont={getCurrentFontConfig(state, scope)}
79
+ onSelect={(font) => handleFontChange(scope, font)}
80
+ fontCatalog={fontCatalog}
81
+ />
82
+ ))}
91
83
  </div>
92
84
  </div>
93
-
94
85
  <div>
95
- <div
96
- style={{
97
- fontSize: 11,
98
- fontWeight: 600,
99
- color: labelColor,
100
- textTransform: "uppercase",
101
- letterSpacing: 0.5,
102
- marginBottom: 8,
103
- }}
104
- >
105
- Fonts
106
- </div>
107
- <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
108
- <FontPicker
109
- label="Default"
110
- slot="default"
111
- currentFont={state.typography?.fonts.default ?? DEFAULT_FONT_DEFAULT}
112
- onSelect={(font) => handleFontChange("default", font)}
113
- />
114
- <FontPicker
115
- label="Display"
116
- slot="display"
117
- currentFont={
118
- state.typography?.fonts.display ?? DEFAULT_FONT_DISPLAY
119
- }
120
- onSelect={(font) => handleFontChange("display", font)}
121
- />
122
- <FontPicker
123
- label="Mono"
124
- slot="mono"
125
- currentFont={state.typography?.fonts.mono ?? DEFAULT_FONT_MONO}
126
- onSelect={(font) => handleFontChange("mono", font)}
127
- />
128
- </div>
86
+ <div style={sectionLabelStyle}>Type Scale</div>
87
+ <Slider
88
+ value={Math.round((state.typography?.typeScaleOffset ?? 0.5) * 100)}
89
+ onValueChange={(v) =>
90
+ dispatch({ type: "SET_TYPE_SCALE_OFFSET", offset: v / 100 })
91
+ }
92
+ min={0}
93
+ max={100}
94
+ label="Scale"
95
+ showValue
96
+ />
129
97
  </div>
130
98
  </div>
131
99
  );