@newtonedev/editor 0.1.6 → 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 (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 +2464 -2046
  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 +2466 -2049
  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 +54 -6
  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
package/dist/index.cjs CHANGED
@@ -193,6 +193,53 @@ function usePresets({
193
193
  revertActivePreset
194
194
  };
195
195
  }
196
+ async function measureFontCalibrations(fonts) {
197
+ if (!fonts || typeof document === "undefined") return {};
198
+ const calibrations = {};
199
+ const seen = /* @__PURE__ */ new Set();
200
+ for (const slot of Object.values(fonts)) {
201
+ const { family, fallback } = slot.config;
202
+ if (seen.has(family)) continue;
203
+ seen.add(family);
204
+ const ratio = await components.measureAvgCharWidth(
205
+ family,
206
+ slot.weights.regular,
207
+ fallback
208
+ );
209
+ calibrations[family] = ratio;
210
+ }
211
+ return calibrations;
212
+ }
213
+
214
+ // src/utils/lookupFontMetrics.ts
215
+ async function lookupFontMetrics(fonts, manifestUrl) {
216
+ if (!fonts || !manifestUrl) return {};
217
+ try {
218
+ const res = await fetch(manifestUrl);
219
+ if (!res.ok) return {};
220
+ const manifest = await res.json();
221
+ const result = {};
222
+ const seen = /* @__PURE__ */ new Set();
223
+ for (const slot of Object.values(fonts)) {
224
+ const family = slot.config.family;
225
+ if (seen.has(family)) continue;
226
+ seen.add(family);
227
+ const entry = manifest.families?.[family];
228
+ if (entry?.metrics) {
229
+ result[family] = {
230
+ naturalLineHeightRatio: entry.metrics.naturalLineHeightRatio,
231
+ verticalCenterOffset: entry.metrics.verticalCenterOffset,
232
+ features: entry.metrics.features ?? []
233
+ };
234
+ }
235
+ }
236
+ return result;
237
+ } catch {
238
+ return {};
239
+ }
240
+ }
241
+
242
+ // src/hooks/useEditorState.ts
196
243
  function useEditorState({
197
244
  initialState,
198
245
  initialIsPublished,
@@ -202,7 +249,8 @@ function useEditorState({
202
249
  defaultState,
203
250
  persistence,
204
251
  onNavigate,
205
- initialPreviewView
252
+ initialPreviewView,
253
+ manifestUrl
206
254
  }) {
207
255
  const {
208
256
  state: configuratorState,
@@ -219,6 +267,9 @@ function useEditorState({
219
267
  const [previewView, setPreviewView] = react.useState(
220
268
  initialPreviewView ?? { kind: "overview" }
221
269
  );
270
+ const [activeSectionId, setActiveSectionId] = react.useState(
271
+ components.CATEGORIES[0]?.id ?? "colors"
272
+ );
222
273
  const [sidebarSelection, setSidebarSelection] = react.useState(null);
223
274
  const [propOverrides, setPropOverrides] = react.useState(
224
275
  {}
@@ -300,10 +351,11 @@ function useEditorState({
300
351
  setPreviewView(view);
301
352
  onNavigate?.(view);
302
353
  if (view.kind === "component") {
303
- setSidebarSelection({
304
- scope: "component",
305
- componentId: view.componentId
306
- });
354
+ const comp = components.getComponent(view.componentId);
355
+ const firstVariantId = comp?.variants[0]?.id;
356
+ setSidebarSelection(
357
+ firstVariantId ? { scope: "variant", componentId: view.componentId, variantId: firstVariantId } : { scope: "component", componentId: view.componentId }
358
+ );
307
359
  initOverridesFromVariant(view.componentId);
308
360
  } else {
309
361
  setSidebarSelection(null);
@@ -312,6 +364,30 @@ function useEditorState({
312
364
  },
313
365
  [onNavigate, initOverridesFromVariant]
314
366
  );
367
+ const handleSectionChange = react.useCallback(
368
+ (sectionId) => {
369
+ setActiveSectionId(sectionId);
370
+ const sectionComponents = components.getComponentsByCategory(sectionId);
371
+ if (sectionComponents.length === 1) {
372
+ const comp = sectionComponents[0];
373
+ const view = { kind: "component", componentId: comp.id };
374
+ setPreviewView(view);
375
+ onNavigate?.(view);
376
+ const firstVariantId = comp.variants[0]?.id;
377
+ setSidebarSelection(
378
+ firstVariantId ? { scope: "variant", componentId: comp.id, variantId: firstVariantId } : { scope: "component", componentId: comp.id }
379
+ );
380
+ initOverridesFromVariant(comp.id);
381
+ } else {
382
+ const view = { kind: "category", categoryId: sectionId };
383
+ setPreviewView(view);
384
+ onNavigate?.(view);
385
+ setSidebarSelection(null);
386
+ setPropOverrides({});
387
+ }
388
+ },
389
+ [onNavigate, initOverridesFromVariant]
390
+ );
315
391
  const handleSelectVariant = react.useCallback(
316
392
  (variantId) => {
317
393
  if (previewView.kind === "component") {
@@ -399,17 +475,23 @@ function useEditorState({
399
475
  setPublishing(true);
400
476
  const currentState = latestStateRef.current;
401
477
  const updatedPresets = publishActivePreset(currentState);
478
+ const [calibrations, fontMetrics] = await Promise.all([
479
+ measureFontCalibrations(currentState.typography?.fonts),
480
+ lookupFontMetrics(currentState.typography?.fonts, manifestUrl)
481
+ ]);
402
482
  const { error } = await persistence.onPublish({
403
483
  state: currentState,
404
484
  presets: updatedPresets,
405
- activePresetId
485
+ activePresetId,
486
+ calibrations,
487
+ fontMetrics
406
488
  });
407
489
  if (!error) {
408
490
  setSaveStatus("saved");
409
491
  setIsPublished(true);
410
492
  }
411
493
  setPublishing(false);
412
- }, [activePresetId, publishActivePreset, persistence]);
494
+ }, [activePresetId, publishActivePreset, persistence, manifestUrl]);
413
495
  react.useEffect(() => {
414
496
  const handleBeforeUnload = (e) => {
415
497
  if (saveStatus === "unsaved" || saveStatus === "saving") {
@@ -448,7 +530,9 @@ function useEditorState({
448
530
  // Preview
449
531
  previewView,
450
532
  colorMode,
533
+ activeSectionId,
451
534
  handlePreviewNavigate,
535
+ handleSectionChange,
452
536
  handleSelectVariant,
453
537
  handleColorModeChange,
454
538
  // Sidebar
@@ -540,2076 +624,1994 @@ function EditorShell({
540
624
  }
541
625
  );
542
626
  }
543
- var STRENGTH_OPTIONS = [
544
- { label: "None", value: "none" },
545
- { label: "Low", value: "low" },
546
- { label: "Medium", value: "medium" },
547
- { label: "Hard", value: "hard" }
548
- ];
549
- function getHexAtNv(previewColors, nv) {
550
- const idx = Math.round((1 - nv) * (previewColors.length - 1));
551
- const clamped = Math.max(0, Math.min(previewColors.length - 1, idx));
552
- return newtone.srgbToHex(previewColors[clamped].srgb);
553
- }
554
- function ColorsSection({
555
- state,
556
- dispatch,
557
- previewColors,
558
- colorMode,
559
- onColorModeChange
627
+ function PresetSelector({
628
+ presets,
629
+ activePresetId,
630
+ publishedPresetId,
631
+ onSwitchPreset,
632
+ onCreatePreset,
633
+ onRenamePreset,
634
+ onDeletePreset,
635
+ onDuplicatePreset
560
636
  }) {
561
637
  const tokens = components.useTokens();
562
- const [activePaletteIndex, setActivePaletteIndex] = react.useState(0);
563
- const [modeToggleHovered, setModeToggleHovered] = react.useState(false);
564
- const palette = state.palettes[activePaletteIndex];
565
- const hueRange = configurator.SEMANTIC_HUE_RANGES[activePaletteIndex];
566
- const isNeutral = activePaletteIndex === 0;
567
- const activeColor = newtone.srgbToHex(tokens.accent.fill.srgb);
638
+ const [isOpen, setIsOpen] = react.useState(false);
639
+ const [renamingId, setRenamingId] = react.useState(null);
640
+ const [renameValue, setRenameValue] = react.useState("");
641
+ const [menuOpenId, setMenuOpenId] = react.useState(null);
642
+ const [hoveredId, setHoveredId] = react.useState(null);
643
+ const [hoveredAction, setHoveredAction] = react.useState(null);
644
+ const dropdownRef = react.useRef(null);
645
+ const renameInputRef = react.useRef(null);
646
+ const activePreset = presets.find((p) => p.id === activePresetId);
568
647
  const borderColor = newtone.srgbToHex(tokens.border.srgb);
569
- const effectiveKeyColor = colorMode === "dark" ? palette.keyColorDark : palette.keyColor;
570
- const setKeyColorAction = colorMode === "dark" ? "SET_PALETTE_KEY_COLOR_DARK" : "SET_PALETTE_KEY_COLOR";
571
- const clearKeyColorAction = colorMode === "dark" ? "CLEAR_PALETTE_KEY_COLOR_DARK" : "CLEAR_PALETTE_KEY_COLOR";
572
- const hexAction = colorMode === "dark" ? "SET_PALETTE_FROM_HEX_DARK" : "SET_PALETTE_FROM_HEX";
573
- const wcag = configurator.useWcagValidation(state, activePaletteIndex);
574
- const [hexText, setHexText] = react.useState("");
575
- const [hexError, setHexError] = react.useState("");
576
- const [isEditingHex, setIsEditingHex] = react.useState(false);
577
- const [isHexUserSet, setIsHexUserSet] = react.useState(false);
578
- react.useEffect(() => {
579
- setHexText("");
580
- setHexError("");
581
- setIsEditingHex(false);
582
- setIsHexUserSet(false);
583
- }, [colorMode]);
584
- const currentPreview = previewColors[activePaletteIndex];
585
- const displayedHex = react.useMemo(() => {
586
- if (!currentPreview || currentPreview.length === 0) return "";
587
- const nv = effectiveKeyColor ?? wcag.autoNormalizedValue;
588
- return getHexAtNv(currentPreview, nv);
589
- }, [currentPreview, effectiveKeyColor, wcag.autoNormalizedValue]);
648
+ const bgColor = newtone.srgbToHex(tokens.background.srgb);
649
+ const textPrimary = newtone.srgbToHex(tokens.textPrimary.srgb);
650
+ const textSecondary = newtone.srgbToHex(tokens.textSecondary.srgb);
651
+ const interactiveColor = newtone.srgbToHex(tokens.accent.fill.srgb);
652
+ const warningColor = newtone.srgbToHex(tokens.warning.fill.srgb);
653
+ const errorColor = newtone.srgbToHex(tokens.error.fill.srgb);
654
+ const hoverBg = `${borderColor}18`;
655
+ const activeBg = `${interactiveColor}14`;
590
656
  react.useEffect(() => {
591
- if (!isEditingHex && !isHexUserSet) {
592
- setHexText(displayedHex);
593
- }
594
- }, [displayedHex, isEditingHex, isHexUserSet]);
595
- const dynamicRange = react.useMemo(() => {
596
- const light = state.globalHueGrading.light.strength !== "none" ? {
597
- hue: configurator.traditionalHueToOklch(state.globalHueGrading.light.hue),
598
- strength: state.globalHueGrading.light.strength
599
- } : void 0;
600
- const dark = state.globalHueGrading.dark.strength !== "none" ? {
601
- hue: configurator.traditionalHueToOklch(state.globalHueGrading.dark.hue),
602
- strength: state.globalHueGrading.dark.strength
603
- } : void 0;
604
- const hueGrading = light || dark ? { light, dark } : void 0;
605
- return {
606
- lightest: state.dynamicRange.lightest,
607
- darkest: state.dynamicRange.darkest,
608
- ...hueGrading ? { hueGrading } : {}
657
+ if (!isOpen) return;
658
+ const handleClickOutside = (e) => {
659
+ if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
660
+ setIsOpen(false);
661
+ setMenuOpenId(null);
662
+ setRenamingId(null);
663
+ }
609
664
  };
610
- }, [state.dynamicRange, state.globalHueGrading]);
611
- const handleHexSubmit = react.useCallback(() => {
612
- setIsEditingHex(false);
613
- const trimmed = hexText.trim();
614
- if (!trimmed) {
615
- setHexError("");
616
- return;
617
- }
618
- const hex = trimmed.startsWith("#") ? trimmed : `#${trimmed}`;
619
- const params = configurator.hexToPaletteParams(hex, dynamicRange);
620
- if (!params) {
621
- setHexError("Invalid hex color");
622
- return;
665
+ document.addEventListener("mousedown", handleClickOutside);
666
+ return () => document.removeEventListener("mousedown", handleClickOutside);
667
+ }, [isOpen]);
668
+ react.useEffect(() => {
669
+ if (renamingId && renameInputRef.current) {
670
+ renameInputRef.current.focus();
671
+ renameInputRef.current.select();
623
672
  }
624
- setHexError("");
625
- setIsHexUserSet(true);
626
- dispatch({
627
- type: hexAction,
628
- index: activePaletteIndex,
629
- hue: params.hue,
630
- saturation: params.saturation,
631
- keyColor: params.normalizedValue
632
- });
633
- }, [hexText, dynamicRange, dispatch, activePaletteIndex, hexAction]);
634
- const handleClearKeyColor = react.useCallback(() => {
635
- dispatch({ type: clearKeyColorAction, index: activePaletteIndex });
636
- setHexError("");
637
- setIsHexUserSet(false);
638
- }, [dispatch, activePaletteIndex, clearKeyColorAction]);
639
- const wcagWarning = react.useMemo(() => {
640
- if (effectiveKeyColor === void 0 || wcag.keyColorContrast === null)
641
- return void 0;
642
- if (wcag.passesAA) return void 0;
643
- const ratio = wcag.keyColorContrast.toFixed(1);
644
- if (wcag.passesAALargeText) {
645
- return `Contrast ${ratio}:1 \u2014 passes large text (AA) but fails normal text (requires 4.5:1)`;
673
+ }, [renamingId]);
674
+ const handleCreate = react.useCallback(async () => {
675
+ const name = `Preset ${presets.length + 1}`;
676
+ const newId = await onCreatePreset(name);
677
+ onSwitchPreset(newId);
678
+ setIsOpen(false);
679
+ }, [presets.length, onCreatePreset, onSwitchPreset]);
680
+ const handleStartRename = react.useCallback(
681
+ (presetId, currentName) => {
682
+ setRenamingId(presetId);
683
+ setRenameValue(currentName);
684
+ setMenuOpenId(null);
685
+ },
686
+ []
687
+ );
688
+ const handleCommitRename = react.useCallback(() => {
689
+ if (renamingId && renameValue.trim()) {
690
+ onRenamePreset(renamingId, renameValue.trim());
646
691
  }
647
- return `Contrast ${ratio}:1 \u2014 fails WCAG AA (requires 4.5:1 for normal text, 3:1 for large text)`;
648
- }, [effectiveKeyColor, wcag]);
649
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 16 }, children: [
650
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
651
- state.palettes.map((_p, index) => {
652
- const isActive = index === activePaletteIndex;
653
- const colors = previewColors[index];
654
- const isNeutralCircle = index === 0;
655
- const paletteKeyColor = colorMode === "dark" ? _p.keyColorDark : _p.keyColor;
656
- const circleColor = !isNeutralCircle && colors ? getHexAtNv(
657
- colors,
658
- paletteKeyColor ?? wcag.autoNormalizedValue
659
- ) : void 0;
660
- const ringStyle = isActive ? `0 0 0 2px ${newtone.srgbToHex(tokens.background.srgb)}, 0 0 0 4px ${activeColor}` : "none";
661
- return /* @__PURE__ */ jsxRuntime.jsx(
662
- "button",
663
- {
664
- onClick: () => setActivePaletteIndex(index),
665
- "aria-label": _p.name,
666
- "aria-pressed": isActive,
667
- style: {
668
- width: 32,
669
- height: 32,
670
- borderRadius: "50%",
671
- border: "none",
672
- cursor: "pointer",
673
- flexShrink: 0,
674
- boxShadow: ringStyle,
675
- transition: "box-shadow 150ms ease",
676
- padding: 0,
677
- overflow: "hidden",
678
- ...isNeutralCircle ? {
679
- background: colors ? `linear-gradient(to right, ${newtone.srgbToHex(colors[0].srgb)} 50%, ${newtone.srgbToHex(colors[colors.length - 1].srgb)} 50%)` : `linear-gradient(to right, #ffffff 50%, #000000 50%)`
680
- } : { backgroundColor: circleColor ?? borderColor }
681
- }
682
- },
683
- index
684
- );
685
- }),
686
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1 } }),
687
- /* @__PURE__ */ jsxRuntime.jsxs(
688
- "button",
689
- {
690
- onClick: () => onColorModeChange(colorMode === "light" ? "dark" : "light"),
691
- onMouseEnter: () => setModeToggleHovered(true),
692
- onMouseLeave: () => setModeToggleHovered(false),
693
- "aria-label": colorMode === "light" ? "Switch to dark mode" : "Switch to light mode",
694
- style: {
695
- display: "flex",
696
- alignItems: "center",
697
- gap: 6,
698
- padding: "4px 10px",
699
- borderRadius: 6,
700
- border: `1px solid ${borderColor}`,
701
- background: modeToggleHovered ? `${borderColor}20` : "none",
702
- cursor: "pointer",
703
- fontSize: 12,
704
- color: newtone.srgbToHex(tokens.textPrimary.srgb),
705
- transition: "background-color 150ms ease"
706
- },
707
- children: [
708
- colorMode === "light" ? "\u2600" : "\u263E",
709
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: colorMode === "light" ? "Light" : "Dark" })
710
- ]
711
- }
712
- )
713
- ] }),
714
- currentPreview && (isNeutral ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", gap: 1 }, children: currentPreview.map((color, i) => /* @__PURE__ */ jsxRuntime.jsx(
715
- "div",
716
- {
717
- style: {
718
- flex: 1,
719
- height: 64,
720
- borderRadius: 2,
721
- backgroundColor: newtone.srgbToHex(color.srgb)
722
- }
723
- },
724
- i
725
- )) }) : /* @__PURE__ */ jsxRuntime.jsxs(
726
- "div",
692
+ setRenamingId(null);
693
+ }, [renamingId, renameValue, onRenamePreset]);
694
+ const handleDelete = react.useCallback(
695
+ async (presetId) => {
696
+ if (presets.length <= 1) return;
697
+ if (window.confirm("Delete this preset? This cannot be undone.")) {
698
+ await onDeletePreset(presetId);
699
+ }
700
+ setMenuOpenId(null);
701
+ },
702
+ [presets.length, onDeletePreset]
703
+ );
704
+ const handleDuplicate = react.useCallback(
705
+ async (presetId, sourceName) => {
706
+ const newId = await onDuplicatePreset(presetId, `${sourceName} (copy)`);
707
+ onSwitchPreset(newId);
708
+ setMenuOpenId(null);
709
+ setIsOpen(false);
710
+ },
711
+ [onDuplicatePreset, onSwitchPreset]
712
+ );
713
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: dropdownRef, style: { position: "relative" }, children: [
714
+ /* @__PURE__ */ jsxRuntime.jsxs(
715
+ "button",
727
716
  {
728
- style: { display: "flex", flexDirection: "column", gap: 8 },
717
+ onClick: () => setIsOpen(!isOpen),
718
+ style: {
719
+ display: "flex",
720
+ alignItems: "center",
721
+ gap: 6,
722
+ padding: "4px 10px",
723
+ borderRadius: 6,
724
+ border: `1px solid ${borderColor}`,
725
+ backgroundColor: "transparent",
726
+ color: textPrimary,
727
+ fontSize: 12,
728
+ fontWeight: 500,
729
+ cursor: "pointer",
730
+ maxWidth: 160
731
+ },
729
732
  children: [
730
733
  /* @__PURE__ */ jsxRuntime.jsx(
731
- components.ColorScaleSlider,
732
- {
733
- colors: currentPreview,
734
- value: effectiveKeyColor ?? wcag.autoNormalizedValue,
735
- onValueChange: (nv) => {
736
- setIsHexUserSet(false);
737
- dispatch({
738
- type: setKeyColorAction,
739
- index: activePaletteIndex,
740
- normalizedValue: nv
741
- });
742
- },
743
- trimEnds: true,
744
- snap: true,
745
- label: "Key Color",
746
- warning: wcagWarning,
747
- animateValue: true
748
- }
749
- ),
750
- /* @__PURE__ */ jsxRuntime.jsxs(
751
- "div",
734
+ "span",
752
735
  {
753
736
  style: {
754
- display: "flex",
755
- gap: 8,
756
- alignItems: "flex-end"
737
+ overflow: "hidden",
738
+ textOverflow: "ellipsis",
739
+ whiteSpace: "nowrap"
757
740
  },
758
- children: [
759
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(
760
- components.TextInput,
761
- {
762
- label: "Hex",
763
- value: hexText,
764
- onChangeText: (text) => {
765
- setIsEditingHex(true);
766
- setHexText(text);
767
- setHexError("");
768
- },
769
- onBlur: handleHexSubmit,
770
- onSubmitEditing: handleHexSubmit,
771
- placeholder: "#000000"
772
- }
773
- ) }),
774
- effectiveKeyColor !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(
775
- "button",
776
- {
777
- onClick: handleClearKeyColor,
778
- style: {
779
- background: "none",
780
- border: "none",
781
- cursor: "pointer",
782
- padding: "0 0 6px",
783
- fontSize: 13,
784
- fontWeight: 600,
785
- color: activeColor
786
- },
787
- children: "Auto"
788
- }
789
- )
790
- ]
741
+ children: activePreset?.name ?? "Default"
791
742
  }
792
743
  ),
793
- hexError && /* @__PURE__ */ jsxRuntime.jsx(
794
- "div",
744
+ /* @__PURE__ */ jsxRuntime.jsx(
745
+ components.Icon,
795
746
  {
747
+ name: "expand_more",
748
+ size: 14,
796
749
  style: {
797
- fontSize: 12,
798
- fontWeight: 500,
799
- color: newtone.srgbToHex(tokens.error.fill.srgb)
800
- },
801
- children: hexError
750
+ transform: isOpen ? "rotate(180deg)" : "none",
751
+ transition: "transform 150ms ease",
752
+ flexShrink: 0
753
+ }
802
754
  }
803
755
  )
804
756
  ]
805
757
  }
806
- )),
807
- /* @__PURE__ */ jsxRuntime.jsx(
758
+ ),
759
+ isOpen && /* @__PURE__ */ jsxRuntime.jsxs(
808
760
  "div",
809
761
  {
810
762
  style: {
811
- height: 1,
812
- backgroundColor: borderColor,
813
- margin: "4px 0"
814
- }
815
- }
816
- ),
817
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: [
818
- /* @__PURE__ */ jsxRuntime.jsx(
819
- components.HueSlider,
820
- {
821
- value: palette.hue,
822
- onValueChange: (hue) => dispatch({
823
- type: "SET_PALETTE_HUE",
824
- index: activePaletteIndex,
825
- hue
826
- }),
827
- label: "Hue",
828
- editableValue: true,
829
- ...hueRange ? { min: hueRange.min, max: hueRange.max } : {}
830
- }
831
- ),
832
- /* @__PURE__ */ jsxRuntime.jsx(
833
- components.Slider,
834
- {
835
- value: palette.saturation,
836
- onValueChange: (saturation) => dispatch({
837
- type: "SET_PALETTE_SATURATION",
838
- index: activePaletteIndex,
839
- saturation
840
- }),
841
- min: 0,
842
- max: 100,
843
- label: "Saturation",
844
- editableValue: true
845
- }
846
- ),
847
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 12, alignItems: "flex-end" }, children: [
848
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(
849
- components.Select,
850
- {
851
- options: STRENGTH_OPTIONS,
852
- value: palette.desaturationStrength,
853
- onValueChange: (strength) => dispatch({
854
- type: "SET_PALETTE_DESAT_STRENGTH",
855
- index: activePaletteIndex,
856
- strength
857
- }),
858
- label: "Desaturation"
859
- }
860
- ) }),
861
- palette.desaturationStrength !== "none" && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { paddingBottom: 2 }, children: /* @__PURE__ */ jsxRuntime.jsx(
862
- components.Toggle,
863
- {
864
- value: palette.desaturationDirection === "dark",
865
- onValueChange: (v) => dispatch({
866
- type: "SET_PALETTE_DESAT_DIRECTION",
867
- index: activePaletteIndex,
868
- direction: v ? "dark" : "light"
869
- }),
870
- label: "Invert"
871
- }
872
- ) })
873
- ] }),
874
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 12, alignItems: "flex-end" }, children: [
875
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(
876
- components.Select,
877
- {
878
- options: STRENGTH_OPTIONS,
879
- value: palette.hueGradeStrength,
880
- onValueChange: (strength) => dispatch({
881
- type: "SET_PALETTE_HUE_GRADE_STRENGTH",
882
- index: activePaletteIndex,
883
- strength
884
- }),
885
- label: "Hue Grading"
886
- }
887
- ) }),
888
- palette.hueGradeStrength !== "none" && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { paddingBottom: 2 }, children: /* @__PURE__ */ jsxRuntime.jsx(
889
- components.Toggle,
890
- {
891
- value: palette.hueGradeDirection === "dark",
892
- onValueChange: (v) => dispatch({
893
- type: "SET_PALETTE_HUE_GRADE_DIRECTION",
894
- index: activePaletteIndex,
895
- direction: v ? "dark" : "light"
896
- }),
897
- label: "Invert"
898
- }
899
- ) })
900
- ] }),
901
- palette.hueGradeStrength !== "none" && /* @__PURE__ */ jsxRuntime.jsx(
902
- components.HueSlider,
903
- {
904
- value: palette.hueGradeHue,
905
- onValueChange: (hue) => dispatch({
906
- type: "SET_PALETTE_HUE_GRADE_HUE",
907
- index: activePaletteIndex,
908
- hue
909
- }),
910
- label: "Grade Target",
911
- editableValue: true
912
- }
913
- )
914
- ] })
915
- ] });
916
- }
917
- var STRENGTH_OPTIONS2 = [
918
- { label: "None", value: "none" },
919
- { label: "Low", value: "low" },
920
- { label: "Medium", value: "medium" },
921
- { label: "Hard", value: "hard" }
922
- ];
923
- var TRACK_HEIGHT = 8;
924
- var THUMB_SIZE = 18;
925
- var ZONE_FRAC = 1 / 3;
926
- function clamp(v, min, max) {
927
- return Math.min(max, Math.max(min, v));
928
- }
929
- function internalToDisplay(internal) {
930
- return clamp(Math.round(internal * 10), 0, 10);
931
- }
932
- function displayToInternal(display) {
933
- return clamp(display, 0, 10) / 10;
934
- }
935
- function posToWhitesDisplay(pos) {
936
- const ratio = clamp(pos / ZONE_FRAC, 0, 1);
937
- return Math.round(10 * (1 - ratio));
938
- }
939
- function posToBlacksDisplay(pos) {
940
- const ratio = clamp((pos - (1 - ZONE_FRAC)) / ZONE_FRAC, 0, 1);
941
- return Math.round(ratio * 10);
942
- }
943
- function whitesDisplayToPos(display) {
944
- return (10 - display) / 10 * ZONE_FRAC;
945
- }
946
- function blacksDisplayToPos(display) {
947
- return 1 - ZONE_FRAC + display / 10 * ZONE_FRAC;
948
- }
949
- function DualRangeSlider({
950
- whitesValue,
951
- blacksValue,
952
- onWhitesChange,
953
- onBlacksChange
954
- }) {
955
- const tokens = components.useTokens();
956
- const trackRef = react.useRef(null);
957
- const [activeThumb, setActiveThumb] = react.useState(null);
958
- const interactiveColor = newtone.srgbToHex(tokens.accent.fill.srgb);
959
- const borderColor = newtone.srgbToHex(tokens.border.srgb);
960
- const wDisplay = internalToDisplay(whitesValue);
961
- const bDisplay = internalToDisplay(blacksValue);
962
- const wPos = whitesDisplayToPos(wDisplay);
963
- const bPos = blacksDisplayToPos(bDisplay);
964
- const getPosRatio = react.useCallback((clientX) => {
965
- if (!trackRef.current) return 0;
966
- const rect = trackRef.current.getBoundingClientRect();
967
- return clamp((clientX - rect.left) / rect.width, 0, 1);
968
- }, []);
969
- const handlePointerDown = react.useCallback(
970
- (e) => {
971
- e.preventDefault();
972
- const pos = getPosRatio(e.clientX);
973
- if (pos <= ZONE_FRAC) {
974
- setActiveThumb("whites");
975
- onWhitesChange(displayToInternal(posToWhitesDisplay(pos)));
976
- } else if (pos >= 1 - ZONE_FRAC) {
977
- setActiveThumb("blacks");
978
- onBlacksChange(displayToInternal(posToBlacksDisplay(pos)));
979
- } else {
980
- return;
981
- }
982
- e.currentTarget.setPointerCapture(e.pointerId);
983
- },
984
- [getPosRatio, onWhitesChange, onBlacksChange]
985
- );
986
- const handlePointerMove = react.useCallback(
987
- (e) => {
988
- if (!activeThumb) return;
989
- const pos = getPosRatio(e.clientX);
990
- if (activeThumb === "whites") {
991
- onWhitesChange(displayToInternal(posToWhitesDisplay(pos)));
992
- } else {
993
- onBlacksChange(displayToInternal(posToBlacksDisplay(pos)));
994
- }
995
- },
996
- [activeThumb, getPosRatio, onWhitesChange, onBlacksChange]
997
- );
998
- const handlePointerUp = react.useCallback(() => {
999
- setActiveThumb(null);
1000
- }, []);
1001
- const trackTop = (THUMB_SIZE - TRACK_HEIGHT) / 2;
1002
- return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: `0 ${THUMB_SIZE / 2}px` }, children: /* @__PURE__ */ jsxRuntime.jsxs(
1003
- "div",
1004
- {
1005
- ref: trackRef,
1006
- onPointerDown: handlePointerDown,
1007
- onPointerMove: handlePointerMove,
1008
- onPointerUp: handlePointerUp,
1009
- onPointerCancel: handlePointerUp,
1010
- style: {
1011
- position: "relative",
1012
- height: THUMB_SIZE,
1013
- cursor: activeThumb ? "grabbing" : "pointer",
1014
- touchAction: "none",
1015
- userSelect: "none"
1016
- },
1017
- children: [
1018
- /* @__PURE__ */ jsxRuntime.jsx(
1019
- "div",
1020
- {
1021
- style: {
1022
- position: "absolute",
1023
- left: 0,
1024
- right: 0,
1025
- top: trackTop,
1026
- height: TRACK_HEIGHT,
1027
- borderRadius: TRACK_HEIGHT / 2,
1028
- background: "linear-gradient(to right, white, black)",
1029
- border: `1px solid ${borderColor}`,
1030
- boxSizing: "border-box"
1031
- }
1032
- }
1033
- ),
1034
- /* @__PURE__ */ jsxRuntime.jsx(
1035
- "div",
1036
- {
1037
- style: {
1038
- position: "absolute",
1039
- left: `${wPos * 100}%`,
1040
- width: `${(bPos - wPos) * 100}%`,
1041
- top: trackTop,
1042
- height: TRACK_HEIGHT,
1043
- backgroundColor: interactiveColor
1044
- }
1045
- }
1046
- ),
1047
- /* @__PURE__ */ jsxRuntime.jsx(
1048
- "div",
1049
- {
1050
- style: {
1051
- position: "absolute",
1052
- left: `calc(${wPos * 100}% - ${THUMB_SIZE / 2}px)`,
1053
- top: 0,
1054
- width: THUMB_SIZE,
1055
- height: THUMB_SIZE,
1056
- borderRadius: THUMB_SIZE / 2,
1057
- backgroundColor: interactiveColor,
1058
- pointerEvents: "none",
1059
- zIndex: activeThumb === "whites" ? 2 : 1
1060
- }
1061
- }
1062
- ),
1063
- /* @__PURE__ */ jsxRuntime.jsx(
1064
- "div",
1065
- {
1066
- style: {
1067
- position: "absolute",
1068
- left: `calc(${bPos * 100}% - ${THUMB_SIZE / 2}px)`,
1069
- top: 0,
1070
- width: THUMB_SIZE,
1071
- height: THUMB_SIZE,
1072
- borderRadius: THUMB_SIZE / 2,
1073
- backgroundColor: interactiveColor,
1074
- pointerEvents: "none",
1075
- zIndex: activeThumb === "blacks" ? 2 : 1
1076
- }
1077
- }
1078
- )
1079
- ]
1080
- }
1081
- ) });
1082
- }
1083
- function RangeInput({ display, onCommit, toInternal }) {
1084
- const tokens = components.useTokens();
1085
- const [text, setText] = react.useState(String(display));
1086
- const [isEditing, setIsEditing] = react.useState(false);
1087
- const displayText = isEditing ? text : String(display);
1088
- const commit = () => {
1089
- setIsEditing(false);
1090
- const parsed = parseInt(text, 10);
1091
- if (isNaN(parsed)) {
1092
- setText(String(display));
1093
- return;
1094
- }
1095
- const clamped = clamp(Math.round(parsed), 0, 10);
1096
- onCommit(toInternal(clamped));
1097
- setText(String(clamped));
1098
- };
1099
- return /* @__PURE__ */ jsxRuntime.jsx(
1100
- "input",
1101
- {
1102
- type: "text",
1103
- inputMode: "numeric",
1104
- value: displayText,
1105
- onChange: (e) => {
1106
- setIsEditing(true);
1107
- setText(e.target.value);
1108
- },
1109
- onBlur: commit,
1110
- onKeyDown: (e) => {
1111
- if (e.key === "Enter") commit();
1112
- },
1113
- style: {
1114
- width: 40,
1115
- padding: "2px 6px",
1116
- border: `1px solid ${newtone.srgbToHex(tokens.border.srgb)}`,
1117
- borderRadius: 4,
1118
- backgroundColor: "transparent",
1119
- color: newtone.srgbToHex(tokens.textPrimary.srgb),
1120
- fontFamily: "inherit",
1121
- fontSize: 12,
1122
- fontWeight: 500,
1123
- textAlign: "center",
1124
- outline: "none"
1125
- }
1126
- }
1127
- );
1128
- }
1129
- var GRAPH_HEIGHT = 80;
1130
- var GRAPH_COLS = 256;
1131
- var GRAPH_ROWS = 64;
1132
- function strengthToFactor(strength) {
1133
- switch (strength) {
1134
- case "none":
1135
- return 0;
1136
- case "low":
1137
- return newtone.HUE_GRADING_STRENGTH_LOW;
1138
- case "medium":
1139
- return newtone.HUE_GRADING_STRENGTH_MEDIUM;
1140
- case "hard":
1141
- return newtone.HUE_GRADING_STRENGTH_HARD;
1142
- }
1143
- }
1144
- function blendHues(lightHue, darkHue, wLight, wDark) {
1145
- const totalW = wLight + wDark;
1146
- if (totalW === 0) return 0;
1147
- const delta = ((darkHue - lightHue + 180) % 360 + 360) % 360 - 180;
1148
- const t = wDark / totalW;
1149
- const result = lightHue + delta * t;
1150
- return (result % 360 + 360) % 360;
1151
- }
1152
- function computeGraphData(state) {
1153
- const { dynamicRange, globalHueGrading } = state;
1154
- const lightActive = globalHueGrading.light.strength !== "none";
1155
- const darkActive = globalHueGrading.dark.strength !== "none";
1156
- const lightOklchHue = configurator.traditionalHueToOklch(globalHueGrading.light.hue);
1157
- const darkOklchHue = configurator.traditionalHueToOklch(globalHueGrading.dark.hue);
1158
- const lightFactor = strengthToFactor(globalHueGrading.light.strength);
1159
- const darkFactor = strengthToFactor(globalHueGrading.dark.strength);
1160
- const buffer = new Uint8ClampedArray(GRAPH_COLS * GRAPH_ROWS * 4);
1161
- for (let col = 0; col < GRAPH_COLS; col++) {
1162
- const nv = 1 - col / (GRAPH_COLS - 1);
1163
- const L = newtone.resolveLightness(dynamicRange, nv);
1164
- const wLight = lightActive ? Math.pow(nv, newtone.HUE_GRADING_EASING_POWER) : 0;
1165
- const wDark = darkActive ? Math.pow(1 - nv, newtone.HUE_GRADING_EASING_POWER) : 0;
1166
- const totalW = wLight + wDark;
1167
- let topHue;
1168
- let topChroma;
1169
- if (totalW === 0) {
1170
- topHue = 0;
1171
- topChroma = 0;
1172
- } else {
1173
- if (!lightActive) {
1174
- topHue = darkOklchHue;
1175
- } else if (!darkActive) {
1176
- topHue = lightOklchHue;
1177
- } else {
1178
- topHue = blendHues(lightOklchHue, darkOklchHue, wLight, wDark);
1179
- }
1180
- topChroma = newtone.findMaxChromaInGamut(L, topHue) * Math.min(totalW, 1);
1181
- }
1182
- for (let row = 0; row < GRAPH_ROWS; row++) {
1183
- const gradingIntensity = row / (GRAPH_ROWS - 1);
1184
- const C = topChroma * gradingIntensity;
1185
- const srgb = newtone.clampSrgb(newtone.oklchToSrgb({ L, C, h: topHue }));
1186
- const canvasY = GRAPH_ROWS - 1 - row;
1187
- const idx = (canvasY * GRAPH_COLS + col) * 4;
1188
- buffer[idx] = Math.round(srgb.r * 255);
1189
- buffer[idx + 1] = Math.round(srgb.g * 255);
1190
- buffer[idx + 2] = Math.round(srgb.b * 255);
1191
- buffer[idx + 3] = 255;
1192
- }
1193
- }
1194
- const curvePoints = [];
1195
- for (let i = 0; i < 26; i++) {
1196
- const nv = 1 - i / 25;
1197
- const x = i / 25 * (GRAPH_COLS - 1);
1198
- const lightContrib = Math.pow(nv, newtone.HUE_GRADING_EASING_POWER) * (lightFactor / newtone.HUE_GRADING_STRENGTH_HARD);
1199
- const darkContrib = Math.pow(1 - nv, newtone.HUE_GRADING_EASING_POWER) * (darkFactor / newtone.HUE_GRADING_STRENGTH_HARD);
1200
- const y = clamp(lightContrib + darkContrib, 0, 1);
1201
- curvePoints.push({ x, y });
1202
- }
1203
- return { buffer, curvePoints };
1204
- }
1205
- function DynamicRangeGraph({ state }) {
1206
- const tokens = components.useTokens();
1207
- const canvasRef = react.useRef(null);
1208
- const graphData = react.useMemo(
1209
- () => computeGraphData(state),
1210
- [
1211
- state.dynamicRange.lightest,
1212
- state.dynamicRange.darkest,
1213
- state.globalHueGrading.light.strength,
1214
- state.globalHueGrading.light.hue,
1215
- state.globalHueGrading.dark.strength,
1216
- state.globalHueGrading.dark.hue
1217
- ]
1218
- );
1219
- react.useEffect(() => {
1220
- const canvas = canvasRef.current;
1221
- if (!canvas) return;
1222
- canvas.width = GRAPH_COLS;
1223
- canvas.height = GRAPH_ROWS;
1224
- const ctx = canvas.getContext("2d");
1225
- if (!ctx) return;
1226
- const imageData = ctx.createImageData(GRAPH_COLS, GRAPH_ROWS);
1227
- imageData.data.set(graphData.buffer);
1228
- ctx.putImageData(imageData, 0, 0);
1229
- const curveColor = newtone.srgbToHex(tokens.accent.fill.srgb);
1230
- const { curvePoints } = graphData;
1231
- if (curvePoints.length < 2) return;
1232
- const mapped = curvePoints.map((p) => ({
1233
- cx: p.x,
1234
- cy: (1 - p.y) * (GRAPH_ROWS - 1)
1235
- }));
1236
- ctx.beginPath();
1237
- ctx.strokeStyle = curveColor;
1238
- ctx.lineWidth = 1.5;
1239
- ctx.lineJoin = "round";
1240
- ctx.lineCap = "round";
1241
- ctx.moveTo(mapped[0].cx, mapped[0].cy);
1242
- for (let i = 0; i < mapped.length - 1; i++) {
1243
- const p0 = mapped[Math.max(0, i - 1)];
1244
- const p1 = mapped[i];
1245
- const p2 = mapped[i + 1];
1246
- const p3 = mapped[Math.min(mapped.length - 1, i + 2)];
1247
- const cp1x = p1.cx + (p2.cx - p0.cx) / 6;
1248
- const cp1y = p1.cy + (p2.cy - p0.cy) / 6;
1249
- const cp2x = p2.cx - (p3.cx - p1.cx) / 6;
1250
- const cp2y = p2.cy - (p3.cy - p1.cy) / 6;
1251
- ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, p2.cx, p2.cy);
1252
- }
1253
- ctx.stroke();
1254
- ctx.fillStyle = curveColor;
1255
- for (const p of mapped) {
1256
- ctx.beginPath();
1257
- ctx.arc(p.cx, p.cy, 2, 0, Math.PI * 2);
1258
- ctx.fill();
1259
- }
1260
- }, [graphData, tokens]);
1261
- const borderColor = newtone.srgbToHex(tokens.border.srgb);
1262
- return /* @__PURE__ */ jsxRuntime.jsx(
1263
- "canvas",
1264
- {
1265
- ref: canvasRef,
1266
- style: {
1267
- width: "100%",
1268
- height: GRAPH_HEIGHT,
1269
- borderRadius: 6,
1270
- border: `1px solid ${borderColor}`,
1271
- display: "block",
1272
- overflow: "hidden"
1273
- }
1274
- }
1275
- );
1276
- }
1277
- function DynamicRangeSection({
1278
- state,
1279
- dispatch
1280
- }) {
1281
- const tokens = components.useTokens();
1282
- const labelColor = newtone.srgbToHex(tokens.textSecondary.srgb);
1283
- const labelStyle = {
1284
- fontSize: 11,
1285
- fontWeight: 600,
1286
- color: labelColor,
1287
- textTransform: "uppercase",
1288
- letterSpacing: 0.5
1289
- };
1290
- const wDisplay = internalToDisplay(state.dynamicRange.lightest);
1291
- const bDisplay = internalToDisplay(state.dynamicRange.darkest);
1292
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: [
1293
- /* @__PURE__ */ jsxRuntime.jsx(DynamicRangeGraph, { state }),
1294
- /* @__PURE__ */ jsxRuntime.jsxs(
1295
- "div",
1296
- {
1297
- style: {
1298
- display: "flex",
1299
- justifyContent: "space-between",
1300
- alignItems: "center"
763
+ position: "absolute",
764
+ top: "calc(100% + 4px)",
765
+ left: 0,
766
+ width: 260,
767
+ backgroundColor: bgColor,
768
+ border: `1px solid ${borderColor}`,
769
+ borderRadius: 8,
770
+ boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
771
+ zIndex: 100,
772
+ overflow: "hidden"
1301
773
  },
1302
774
  children: [
1303
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: labelStyle, children: "Whites" }),
1304
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: labelStyle, children: "Blacks" })
1305
- ]
1306
- }
1307
- ),
1308
- /* @__PURE__ */ jsxRuntime.jsx(
1309
- DualRangeSlider,
1310
- {
1311
- whitesValue: state.dynamicRange.lightest,
1312
- blacksValue: state.dynamicRange.darkest,
1313
- onWhitesChange: (v) => dispatch({ type: "SET_LIGHTEST", value: v }),
1314
- onBlacksChange: (v) => dispatch({ type: "SET_DARKEST", value: v })
1315
- }
1316
- ),
1317
- /* @__PURE__ */ jsxRuntime.jsxs(
1318
- "div",
1319
- {
1320
- style: {
1321
- display: "flex",
1322
- justifyContent: "space-between",
1323
- alignItems: "center"
1324
- },
1325
- children: [
1326
- /* @__PURE__ */ jsxRuntime.jsx(
1327
- RangeInput,
1328
- {
1329
- display: wDisplay,
1330
- onCommit: (v) => dispatch({ type: "SET_LIGHTEST", value: v }),
1331
- toInternal: displayToInternal
1332
- }
1333
- ),
1334
- /* @__PURE__ */ jsxRuntime.jsx(
1335
- RangeInput,
1336
- {
1337
- display: bDisplay,
1338
- onCommit: (v) => dispatch({ type: "SET_DARKEST", value: v }),
1339
- toInternal: displayToInternal
1340
- }
1341
- )
1342
- ]
1343
- }
1344
- ),
1345
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { ...labelStyle, marginTop: 4 }, children: "Global Hue Grading \u2014 Light" }),
1346
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 12 }, children: [
1347
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(
1348
- components.Select,
1349
- {
1350
- options: STRENGTH_OPTIONS2,
1351
- value: state.globalHueGrading.light.strength,
1352
- onValueChange: (s) => dispatch({
1353
- type: "SET_GLOBAL_GRADE_LIGHT_STRENGTH",
1354
- strength: s
1355
- }),
1356
- label: "Strength"
1357
- }
1358
- ) }),
1359
- state.globalHueGrading.light.strength !== "none" && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(
1360
- components.HueSlider,
1361
- {
1362
- value: state.globalHueGrading.light.hue,
1363
- onValueChange: (hue) => dispatch({ type: "SET_GLOBAL_GRADE_LIGHT_HUE", hue }),
1364
- label: "Target Hue",
1365
- showValue: true
1366
- }
1367
- ) })
1368
- ] }),
1369
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { ...labelStyle, marginTop: 4 }, children: "Global Hue Grading \u2014 Dark" }),
1370
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 12 }, children: [
1371
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(
1372
- components.Select,
1373
- {
1374
- options: STRENGTH_OPTIONS2,
1375
- value: state.globalHueGrading.dark.strength,
1376
- onValueChange: (s) => dispatch({
1377
- type: "SET_GLOBAL_GRADE_DARK_STRENGTH",
1378
- strength: s
1379
- }),
1380
- label: "Strength"
1381
- }
1382
- ) }),
1383
- state.globalHueGrading.dark.strength !== "none" && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(
1384
- components.HueSlider,
1385
- {
1386
- value: state.globalHueGrading.dark.hue,
1387
- onValueChange: (hue) => dispatch({ type: "SET_GLOBAL_GRADE_DARK_HUE", hue }),
1388
- label: "Target Hue",
1389
- showValue: true
1390
- }
1391
- ) })
1392
- ] })
1393
- ] });
1394
- }
1395
- var ICON_VARIANT_OPTIONS = [
1396
- { label: "Outlined", value: "outlined" },
1397
- { label: "Rounded", value: "rounded" },
1398
- { label: "Sharp", value: "sharp" }
1399
- ];
1400
- var ICON_WEIGHT_OPTIONS = [
1401
- { label: "100", value: "100" },
1402
- { label: "200", value: "200" },
1403
- { label: "300", value: "300" },
1404
- { label: "400", value: "400" },
1405
- { label: "500", value: "500" },
1406
- { label: "600", value: "600" },
1407
- { label: "700", value: "700" }
1408
- ];
1409
- function IconsSection({ state, dispatch }) {
1410
- const variant = state.icons?.variant ?? "rounded";
1411
- const weight = state.icons?.weight ?? 400;
1412
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 12 }, children: [
1413
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(
1414
- components.Select,
1415
- {
1416
- options: ICON_VARIANT_OPTIONS,
1417
- value: variant,
1418
- onValueChange: (v) => dispatch({
1419
- type: "SET_ICON_VARIANT",
1420
- variant: v
1421
- }),
1422
- label: "Variant"
1423
- }
1424
- ) }),
1425
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(
1426
- components.Select,
1427
- {
1428
- options: ICON_WEIGHT_OPTIONS,
1429
- value: weight.toString(),
1430
- onValueChange: (v) => dispatch({
1431
- type: "SET_ICON_WEIGHT",
1432
- weight: parseInt(v)
1433
- }),
1434
- label: "Weight"
1435
- }
1436
- ) })
1437
- ] });
1438
- }
1439
- var previewLoaded = false;
1440
- function preloadFontsForPreview() {
1441
- if (previewLoaded || typeof document === "undefined") return;
1442
- previewLoaded = true;
1443
- const families = components.GOOGLE_FONTS.map(
1444
- (f) => `family=${f.family.replace(/ /g, "+")}:wght@400`
1445
- ).join("&");
1446
- const url = `https://fonts.googleapis.com/css2?${families}&display=swap`;
1447
- const link = document.createElement("link");
1448
- link.rel = "stylesheet";
1449
- link.href = url;
1450
- document.head.appendChild(link);
1451
- }
1452
- function googleFontToConfig(entry) {
1453
- return {
1454
- type: "google",
1455
- family: entry.family,
1456
- fallback: entry.fallback
1457
- };
1458
- }
1459
- function systemFontToConfig(entry) {
1460
- return {
1461
- type: "system",
1462
- family: entry.family,
1463
- fallback: entry.fallback
1464
- };
1465
- }
1466
- var CATEGORY_LABELS = {
1467
- "sans-serif": "Sans Serif",
1468
- serif: "Serif",
1469
- monospace: "Monospace",
1470
- display: "Display"
1471
- };
1472
- var CATEGORY_ORDER = [
1473
- "sans-serif",
1474
- "serif",
1475
- "monospace",
1476
- "display"
1477
- ];
1478
- var MONO_CATEGORY_ORDER = [
1479
- "monospace",
1480
- "sans-serif",
1481
- "serif",
1482
- "display"
1483
- ];
1484
- function FontPicker({
1485
- label,
1486
- slot,
1487
- currentFont,
1488
- onSelect
1489
- }) {
1490
- const tokens = components.useTokens();
1491
- const [isOpen, setIsOpen] = react.useState(false);
1492
- const [search, setSearch] = react.useState("");
1493
- const containerRef = react.useRef(null);
1494
- const searchInputRef = react.useRef(null);
1495
- const labelColor = newtone.srgbToHex(tokens.textSecondary.srgb);
1496
- const textColor = newtone.srgbToHex(tokens.textPrimary.srgb);
1497
- const bgColor = newtone.srgbToHex(tokens.backgroundElevated.srgb);
1498
- const borderColor = newtone.srgbToHex(tokens.border.srgb);
1499
- const hoverColor = newtone.srgbToHex(tokens.backgroundSunken.srgb);
1500
- const interactiveColor = newtone.srgbToHex(tokens.accent.fill.srgb);
1501
- react.useEffect(() => {
1502
- if (!isOpen) return;
1503
- function handleMouseDown(e) {
1504
- if (containerRef.current && !containerRef.current.contains(e.target)) {
1505
- setIsOpen(false);
1506
- setSearch("");
1507
- }
1508
- }
1509
- document.addEventListener("mousedown", handleMouseDown);
1510
- return () => document.removeEventListener("mousedown", handleMouseDown);
1511
- }, [isOpen]);
1512
- react.useEffect(() => {
1513
- if (isOpen) {
1514
- preloadFontsForPreview();
1515
- requestAnimationFrame(() => searchInputRef.current?.focus());
1516
- }
1517
- }, [isOpen]);
1518
- const categoryOrder = slot === "mono" ? MONO_CATEGORY_ORDER : CATEGORY_ORDER;
1519
- const filteredGoogleFonts = react.useMemo(() => {
1520
- const query = search.toLowerCase().trim();
1521
- const fonts = query ? components.GOOGLE_FONTS.filter((f) => f.family.toLowerCase().includes(query)) : components.GOOGLE_FONTS;
1522
- const grouped = {};
1523
- for (const cat of categoryOrder) {
1524
- const inCategory = fonts.filter((f) => f.category === cat);
1525
- if (inCategory.length > 0) {
1526
- grouped[cat] = inCategory;
1527
- }
1528
- }
1529
- return grouped;
1530
- }, [search, categoryOrder]);
1531
- const filteredSystemFonts = react.useMemo(() => {
1532
- const query = search.toLowerCase().trim();
1533
- return query ? components.SYSTEM_FONTS.filter((f) => f.family.toLowerCase().includes(query)) : [...components.SYSTEM_FONTS];
1534
- }, [search]);
1535
- const handleSelect = react.useCallback(
1536
- (font) => {
1537
- onSelect(font);
1538
- setIsOpen(false);
1539
- setSearch("");
1540
- },
1541
- [onSelect]
1542
- );
1543
- const fontFamily = currentFont.family.includes(" ") ? `"${currentFont.family}"` : currentFont.family;
1544
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: containerRef, style: { position: "relative" }, children: [
1545
- /* @__PURE__ */ jsxRuntime.jsxs(
1546
- "button",
1547
- {
1548
- type: "button",
1549
- onClick: () => setIsOpen(!isOpen),
1550
- style: {
1551
- width: "100%",
1552
- display: "flex",
1553
- justifyContent: "space-between",
1554
- alignItems: "center",
1555
- padding: "6px 10px",
1556
- borderRadius: 6,
1557
- border: `1px solid ${isOpen ? interactiveColor : borderColor}`,
1558
- background: "transparent",
1559
- cursor: "pointer",
1560
- outline: "none"
1561
- },
1562
- children: [
1563
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 12, color: labelColor }, children: label }),
1564
- /* @__PURE__ */ jsxRuntime.jsx(
1565
- "span",
775
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { maxHeight: 240, overflowY: "auto", padding: "4px 0" }, children: presets.map((preset) => {
776
+ const isActive = preset.id === activePresetId;
777
+ const isPublishedPreset = preset.id === publishedPresetId;
778
+ const hasChanges = presetHasUnpublishedChanges(preset);
779
+ const isHovered = hoveredId === preset.id;
780
+ const isRenaming = renamingId === preset.id;
781
+ const isMenuShown = menuOpenId === preset.id;
782
+ return /* @__PURE__ */ jsxRuntime.jsxs(
783
+ "div",
784
+ {
785
+ onMouseEnter: () => setHoveredId(preset.id),
786
+ onMouseLeave: () => setHoveredId(null),
787
+ style: {
788
+ display: "flex",
789
+ alignItems: "center",
790
+ padding: "6px 12px",
791
+ backgroundColor: isActive ? activeBg : isHovered ? hoverBg : "transparent",
792
+ cursor: isRenaming ? "default" : "pointer",
793
+ transition: "background-color 100ms ease",
794
+ position: "relative"
795
+ },
796
+ children: [
797
+ isRenaming ? /* @__PURE__ */ jsxRuntime.jsx(
798
+ "input",
799
+ {
800
+ ref: renameInputRef,
801
+ value: renameValue,
802
+ onChange: (e) => setRenameValue(e.target.value),
803
+ onBlur: handleCommitRename,
804
+ onKeyDown: (e) => {
805
+ if (e.key === "Enter") handleCommitRename();
806
+ if (e.key === "Escape") setRenamingId(null);
807
+ },
808
+ style: {
809
+ flex: 1,
810
+ fontSize: 13,
811
+ padding: "2px 6px",
812
+ border: `1px solid ${interactiveColor}`,
813
+ borderRadius: 4,
814
+ backgroundColor: bgColor,
815
+ color: textPrimary,
816
+ outline: "none"
817
+ }
818
+ }
819
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
820
+ /* @__PURE__ */ jsxRuntime.jsxs(
821
+ "div",
822
+ {
823
+ onClick: () => {
824
+ onSwitchPreset(preset.id);
825
+ setIsOpen(false);
826
+ },
827
+ style: {
828
+ flex: 1,
829
+ display: "flex",
830
+ alignItems: "center",
831
+ gap: 6,
832
+ minWidth: 0
833
+ },
834
+ children: [
835
+ /* @__PURE__ */ jsxRuntime.jsx(
836
+ "span",
837
+ {
838
+ style: {
839
+ fontSize: 13,
840
+ fontWeight: isActive ? 600 : 400,
841
+ color: textPrimary,
842
+ overflow: "hidden",
843
+ textOverflow: "ellipsis",
844
+ whiteSpace: "nowrap"
845
+ },
846
+ children: preset.name
847
+ }
848
+ ),
849
+ hasChanges && /* @__PURE__ */ jsxRuntime.jsx(
850
+ "span",
851
+ {
852
+ title: "Unpublished changes",
853
+ style: {
854
+ width: 6,
855
+ height: 6,
856
+ borderRadius: "50%",
857
+ backgroundColor: warningColor,
858
+ flexShrink: 0
859
+ }
860
+ }
861
+ ),
862
+ isPublishedPreset && /* @__PURE__ */ jsxRuntime.jsx(
863
+ "span",
864
+ {
865
+ style: {
866
+ fontSize: 10,
867
+ fontWeight: 600,
868
+ color: interactiveColor,
869
+ padding: "1px 4px",
870
+ borderRadius: 3,
871
+ border: `1px solid ${interactiveColor}`,
872
+ flexShrink: 0,
873
+ lineHeight: "14px"
874
+ },
875
+ children: "API"
876
+ }
877
+ )
878
+ ]
879
+ }
880
+ ),
881
+ (isHovered || isMenuShown) && /* @__PURE__ */ jsxRuntime.jsx(
882
+ "button",
883
+ {
884
+ onClick: (e) => {
885
+ e.stopPropagation();
886
+ setMenuOpenId(isMenuShown ? null : preset.id);
887
+ },
888
+ style: {
889
+ display: "flex",
890
+ alignItems: "center",
891
+ justifyContent: "center",
892
+ width: 24,
893
+ height: 24,
894
+ border: "none",
895
+ background: "none",
896
+ color: textSecondary,
897
+ cursor: "pointer",
898
+ borderRadius: 4,
899
+ flexShrink: 0
900
+ },
901
+ children: /* @__PURE__ */ jsxRuntime.jsx(components.Icon, { name: "more_vert", size: 14, color: textSecondary })
902
+ }
903
+ )
904
+ ] }),
905
+ isMenuShown && !isRenaming && /* @__PURE__ */ jsxRuntime.jsxs(
906
+ "div",
907
+ {
908
+ style: {
909
+ position: "absolute",
910
+ top: 0,
911
+ right: -140,
912
+ width: 130,
913
+ backgroundColor: bgColor,
914
+ border: `1px solid ${borderColor}`,
915
+ borderRadius: 6,
916
+ boxShadow: "0 2px 8px rgba(0,0,0,0.12)",
917
+ zIndex: 101,
918
+ overflow: "hidden"
919
+ },
920
+ children: [
921
+ /* @__PURE__ */ jsxRuntime.jsx(
922
+ "button",
923
+ {
924
+ onClick: (e) => {
925
+ e.stopPropagation();
926
+ handleStartRename(preset.id, preset.name);
927
+ },
928
+ onMouseEnter: () => setHoveredAction("rename"),
929
+ onMouseLeave: () => setHoveredAction(null),
930
+ style: {
931
+ display: "block",
932
+ width: "100%",
933
+ padding: "8px 12px",
934
+ border: "none",
935
+ backgroundColor: hoveredAction === "rename" ? hoverBg : "transparent",
936
+ color: textPrimary,
937
+ fontSize: 12,
938
+ textAlign: "left",
939
+ cursor: "pointer"
940
+ },
941
+ children: "Rename"
942
+ }
943
+ ),
944
+ /* @__PURE__ */ jsxRuntime.jsx(
945
+ "button",
946
+ {
947
+ onClick: (e) => {
948
+ e.stopPropagation();
949
+ handleDuplicate(preset.id, preset.name);
950
+ },
951
+ onMouseEnter: () => setHoveredAction("duplicate"),
952
+ onMouseLeave: () => setHoveredAction(null),
953
+ style: {
954
+ display: "block",
955
+ width: "100%",
956
+ padding: "8px 12px",
957
+ border: "none",
958
+ backgroundColor: hoveredAction === "duplicate" ? hoverBg : "transparent",
959
+ color: textPrimary,
960
+ fontSize: 12,
961
+ textAlign: "left",
962
+ cursor: "pointer"
963
+ },
964
+ children: "Duplicate"
965
+ }
966
+ ),
967
+ /* @__PURE__ */ jsxRuntime.jsx(
968
+ "button",
969
+ {
970
+ onClick: (e) => {
971
+ e.stopPropagation();
972
+ handleDelete(preset.id);
973
+ },
974
+ onMouseEnter: () => setHoveredAction("delete"),
975
+ onMouseLeave: () => setHoveredAction(null),
976
+ disabled: presets.length <= 1,
977
+ style: {
978
+ display: "block",
979
+ width: "100%",
980
+ padding: "8px 12px",
981
+ border: "none",
982
+ backgroundColor: hoveredAction === "delete" ? hoverBg : "transparent",
983
+ color: presets.length <= 1 ? textSecondary : errorColor,
984
+ fontSize: 12,
985
+ textAlign: "left",
986
+ cursor: presets.length <= 1 ? "not-allowed" : "pointer",
987
+ opacity: presets.length <= 1 ? 0.5 : 1
988
+ },
989
+ children: "Delete"
990
+ }
991
+ )
992
+ ]
993
+ }
994
+ )
995
+ ]
996
+ },
997
+ preset.id
998
+ );
999
+ }) }),
1000
+ /* @__PURE__ */ jsxRuntime.jsxs(
1001
+ "button",
1566
1002
  {
1003
+ onClick: handleCreate,
1004
+ onMouseEnter: () => setHoveredAction("create"),
1005
+ onMouseLeave: () => setHoveredAction(null),
1567
1006
  style: {
1568
- fontSize: 12,
1569
- color: textColor,
1570
- fontFamily: `${fontFamily}, ${currentFont.fallback}`,
1571
- maxWidth: 140,
1572
- overflow: "hidden",
1573
- textOverflow: "ellipsis",
1574
- whiteSpace: "nowrap"
1007
+ display: "flex",
1008
+ alignItems: "center",
1009
+ gap: 8,
1010
+ width: "100%",
1011
+ padding: "10px 12px",
1012
+ border: "none",
1013
+ borderTop: `1px solid ${borderColor}`,
1014
+ backgroundColor: hoveredAction === "create" ? hoverBg : "transparent",
1015
+ color: textSecondary,
1016
+ fontSize: 13,
1017
+ cursor: "pointer"
1575
1018
  },
1576
- children: currentFont.family
1019
+ children: [
1020
+ /* @__PURE__ */ jsxRuntime.jsx(components.Icon, { name: "add", size: 14, color: textSecondary }),
1021
+ "New preset"
1022
+ ]
1577
1023
  }
1578
1024
  )
1579
1025
  ]
1580
1026
  }
1581
- ),
1582
- isOpen && /* @__PURE__ */ jsxRuntime.jsxs(
1583
- "div",
1584
- {
1585
- style: {
1586
- position: "absolute",
1587
- top: "calc(100% + 4px)",
1588
- left: 0,
1589
- right: 0,
1590
- zIndex: 100,
1591
- background: bgColor,
1592
- border: `1px solid ${borderColor}`,
1593
- borderRadius: 8,
1594
- boxShadow: "0 4px 16px rgba(0,0,0,0.15)",
1595
- maxHeight: 320,
1596
- display: "flex",
1597
- flexDirection: "column",
1598
- overflow: "hidden"
1599
- },
1600
- children: [
1601
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "8px 8px 4px" }, children: /* @__PURE__ */ jsxRuntime.jsx(
1602
- "input",
1603
- {
1604
- ref: searchInputRef,
1605
- type: "text",
1606
- value: search,
1607
- onChange: (e) => setSearch(e.target.value),
1608
- placeholder: "Search fonts...",
1609
- style: {
1610
- width: "100%",
1611
- padding: "6px 8px",
1612
- fontSize: 12,
1613
- borderRadius: 4,
1614
- border: `1px solid ${borderColor}`,
1615
- background: "transparent",
1616
- color: textColor,
1617
- outline: "none",
1618
- boxSizing: "border-box"
1619
- }
1620
- }
1621
- ) }),
1622
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { overflowY: "auto", padding: "4px 0" }, children: [
1623
- filteredSystemFonts.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1027
+ )
1028
+ ] });
1029
+ }
1030
+ var SIDEBAR_WIDTH2 = 360;
1031
+ function Sidebar({
1032
+ isDirty,
1033
+ onRevert,
1034
+ presets,
1035
+ activePresetId,
1036
+ publishedPresetId,
1037
+ onSwitchPreset,
1038
+ onCreatePreset,
1039
+ onRenamePreset,
1040
+ onDeletePreset,
1041
+ onDuplicatePreset
1042
+ }) {
1043
+ const tokens = components.useTokens();
1044
+ const borderColor = newtone.srgbToHex(tokens.border.srgb);
1045
+ const bgColor = newtone.srgbToHex(tokens.background.srgb);
1046
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1047
+ "div",
1048
+ {
1049
+ style: {
1050
+ width: SIDEBAR_WIDTH2,
1051
+ flexShrink: 0,
1052
+ display: "flex",
1053
+ flexDirection: "column",
1054
+ height: "100vh",
1055
+ borderLeft: `1px solid ${borderColor}`,
1056
+ backgroundColor: bgColor
1057
+ },
1058
+ children: [
1059
+ /* @__PURE__ */ jsxRuntime.jsxs(
1060
+ "div",
1061
+ {
1062
+ style: {
1063
+ flexShrink: 0,
1064
+ padding: "16px 20px",
1065
+ borderBottom: `1px solid ${borderColor}`,
1066
+ display: "flex",
1067
+ alignItems: "center",
1068
+ justifyContent: "space-between"
1069
+ },
1070
+ children: [
1624
1071
  /* @__PURE__ */ jsxRuntime.jsx(
1625
- "div",
1072
+ "span",
1626
1073
  {
1627
1074
  style: {
1628
- fontSize: 10,
1629
- fontWeight: 600,
1630
- color: labelColor,
1631
- textTransform: "uppercase",
1632
- letterSpacing: 0.5,
1633
- padding: "6px 12px 2px"
1075
+ fontSize: 16,
1076
+ fontWeight: 700,
1077
+ color: newtone.srgbToHex(tokens.textPrimary.srgb)
1634
1078
  },
1635
- children: "System"
1079
+ children: "newtone"
1636
1080
  }
1637
1081
  ),
1638
- filteredSystemFonts.map((f) => /* @__PURE__ */ jsxRuntime.jsx(
1639
- FontOption,
1640
- {
1641
- family: f.family,
1642
- fallback: f.fallback,
1643
- isSelected: currentFont.family === f.family && currentFont.type === "system",
1644
- textColor,
1645
- hoverColor,
1646
- interactiveColor,
1647
- onSelect: () => handleSelect(systemFontToConfig(f))
1648
- },
1649
- f.family
1650
- ))
1651
- ] }),
1652
- Object.entries(filteredGoogleFonts).map(([category, fonts]) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1653
1082
  /* @__PURE__ */ jsxRuntime.jsx(
1654
- "div",
1083
+ PresetSelector,
1655
1084
  {
1656
- style: {
1657
- fontSize: 10,
1658
- fontWeight: 600,
1659
- color: labelColor,
1660
- textTransform: "uppercase",
1661
- letterSpacing: 0.5,
1662
- padding: "8px 12px 2px"
1663
- },
1664
- children: CATEGORY_LABELS[category] ?? category
1085
+ presets,
1086
+ activePresetId,
1087
+ publishedPresetId,
1088
+ onSwitchPreset,
1089
+ onCreatePreset,
1090
+ onRenamePreset,
1091
+ onDeletePreset,
1092
+ onDuplicatePreset
1665
1093
  }
1666
- ),
1667
- fonts.map((f) => /* @__PURE__ */ jsxRuntime.jsx(
1668
- FontOption,
1669
- {
1670
- family: f.family,
1671
- fallback: f.fallback,
1672
- isSelected: currentFont.family === f.family && currentFont.type === "google",
1673
- textColor,
1674
- hoverColor,
1675
- interactiveColor,
1676
- onSelect: () => handleSelect(googleFontToConfig(f))
1677
- },
1678
- f.family
1679
- ))
1680
- ] }, category)),
1681
- filteredSystemFonts.length === 0 && Object.keys(filteredGoogleFonts).length === 0 && /* @__PURE__ */ jsxRuntime.jsx(
1682
- "div",
1094
+ )
1095
+ ]
1096
+ }
1097
+ ),
1098
+ /* @__PURE__ */ jsxRuntime.jsx(
1099
+ "div",
1100
+ {
1101
+ style: {
1102
+ flex: 1,
1103
+ overflowY: "auto",
1104
+ overflowX: "hidden"
1105
+ }
1106
+ }
1107
+ ),
1108
+ /* @__PURE__ */ jsxRuntime.jsx(
1109
+ "div",
1110
+ {
1111
+ style: {
1112
+ flexShrink: 0,
1113
+ padding: "12px 20px",
1114
+ borderTop: `1px solid ${borderColor}`
1115
+ },
1116
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1117
+ "button",
1683
1118
  {
1119
+ disabled: !isDirty,
1120
+ onClick: onRevert,
1121
+ "aria-label": "Revert all changes to the last saved version",
1684
1122
  style: {
1685
- padding: "12px",
1686
- fontSize: 12,
1687
- color: labelColor,
1688
- textAlign: "center"
1123
+ width: "100%",
1124
+ padding: "8px 16px",
1125
+ borderRadius: 6,
1126
+ border: `1px solid ${borderColor}`,
1127
+ backgroundColor: "transparent",
1128
+ color: isDirty ? newtone.srgbToHex(tokens.textPrimary.srgb) : newtone.srgbToHex(tokens.textSecondary.srgb),
1129
+ fontSize: 13,
1130
+ cursor: isDirty ? "pointer" : "not-allowed",
1131
+ opacity: isDirty ? 1 : 0.5
1689
1132
  },
1690
- children: "No fonts found"
1133
+ children: "Revert Changes"
1691
1134
  }
1692
1135
  )
1693
- ] })
1694
- ]
1695
- }
1696
- )
1697
- ] });
1698
- }
1699
- function FontOption({
1700
- family,
1701
- fallback,
1702
- isSelected,
1703
- textColor,
1704
- hoverColor,
1705
- interactiveColor,
1706
- onSelect
1707
- }) {
1708
- const [hovered, setHovered] = react.useState(false);
1709
- const fontFamily = family.includes(" ") ? `"${family}"` : family;
1710
- return /* @__PURE__ */ jsxRuntime.jsx(
1711
- "button",
1712
- {
1713
- type: "button",
1714
- onClick: onSelect,
1715
- onMouseEnter: () => setHovered(true),
1716
- onMouseLeave: () => setHovered(false),
1717
- style: {
1718
- display: "block",
1719
- width: "100%",
1720
- padding: "5px 12px",
1721
- fontSize: 13,
1722
- fontFamily: `${fontFamily}, ${fallback}`,
1723
- color: isSelected ? interactiveColor : textColor,
1724
- background: hovered ? hoverColor : "transparent",
1725
- border: "none",
1726
- cursor: "pointer",
1727
- textAlign: "left",
1728
- outline: "none",
1729
- fontWeight: isSelected ? 600 : 400
1730
- },
1731
- children: family
1136
+ }
1137
+ )
1138
+ ]
1732
1139
  }
1733
1140
  );
1734
1141
  }
1735
- var DEFAULT_FONT_DEFAULT = {
1736
- type: "system",
1737
- family: "system-ui",
1738
- fallback: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
1739
- };
1740
- var DEFAULT_FONT_DISPLAY = {
1741
- type: "system",
1742
- family: "system-ui",
1743
- fallback: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
1744
- };
1745
- var DEFAULT_FONT_MONO = {
1746
- type: "system",
1747
- family: "ui-monospace",
1748
- fallback: "SFMono-Regular, Menlo, Monaco, Consolas, monospace"
1142
+ var STATUS_LABEL = {
1143
+ saved: "Saved",
1144
+ saving: "Saving...",
1145
+ unsaved: "Unsaved changes",
1146
+ error: "Save failed"
1749
1147
  };
1750
- function FontsSection({ state, dispatch }) {
1148
+ function EditorHeader({
1149
+ saveStatus,
1150
+ isPublished,
1151
+ publishing,
1152
+ onPublish,
1153
+ onRetry,
1154
+ headerSlots
1155
+ }) {
1751
1156
  const tokens = components.useTokens();
1752
- const baseSize = state.typography?.scale.baseSize ?? 16;
1753
- const ratio = state.typography?.scale.ratio ?? 1.25;
1754
- const labelColor = newtone.srgbToHex(tokens.textSecondary.srgb);
1755
- const handleFontChange = (slot, font) => {
1756
- const actionType = {
1757
- default: "SET_FONT_DEFAULT",
1758
- display: "SET_FONT_DISPLAY",
1759
- mono: "SET_FONT_MONO"
1760
- }[slot];
1761
- dispatch({ type: actionType, font });
1157
+ const borderColor = newtone.srgbToHex(tokens.border.srgb);
1158
+ const statusColor = {
1159
+ saved: newtone.srgbToHex(tokens.success.fill.srgb),
1160
+ saving: newtone.srgbToHex(tokens.warning.fill.srgb),
1161
+ unsaved: newtone.srgbToHex(tokens.textSecondary.srgb),
1162
+ error: newtone.srgbToHex(tokens.error.fill.srgb)
1762
1163
  };
1763
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 16 }, children: [
1764
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1765
- /* @__PURE__ */ jsxRuntime.jsx(
1766
- "div",
1767
- {
1768
- style: {
1769
- fontSize: 11,
1770
- fontWeight: 600,
1771
- color: labelColor,
1772
- textTransform: "uppercase",
1773
- letterSpacing: 0.5,
1774
- marginBottom: 8
1775
- },
1776
- children: "Scale"
1777
- }
1778
- ),
1779
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: [
1780
- /* @__PURE__ */ jsxRuntime.jsx(
1781
- components.Slider,
1782
- {
1783
- value: baseSize,
1784
- onValueChange: (v) => dispatch({ type: "SET_TYPOGRAPHY_BASE_SIZE", baseSize: v }),
1785
- min: 12,
1786
- max: 24,
1787
- step: 1,
1788
- label: "Base Size",
1789
- showValue: true
1790
- }
1791
- ),
1792
- /* @__PURE__ */ jsxRuntime.jsx(
1793
- components.Slider,
1794
- {
1795
- value: Math.round(ratio * 100),
1796
- onValueChange: (v) => dispatch({ type: "SET_TYPOGRAPHY_RATIO", ratio: v / 100 }),
1797
- min: 110,
1798
- max: 150,
1799
- step: 5,
1800
- label: "Scale Ratio",
1801
- showValue: true
1802
- }
1803
- )
1804
- ] })
1805
- ] }),
1806
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1807
- /* @__PURE__ */ jsxRuntime.jsx(
1808
- "div",
1809
- {
1810
- style: {
1811
- fontSize: 11,
1812
- fontWeight: 600,
1813
- color: labelColor,
1814
- textTransform: "uppercase",
1815
- letterSpacing: 0.5,
1816
- marginBottom: 8
1817
- },
1818
- children: "Fonts"
1819
- }
1820
- ),
1821
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [
1822
- /* @__PURE__ */ jsxRuntime.jsx(
1823
- FontPicker,
1824
- {
1825
- label: "Default",
1826
- slot: "default",
1827
- currentFont: state.typography?.fonts.default ?? DEFAULT_FONT_DEFAULT,
1828
- onSelect: (font) => handleFontChange("default", font)
1829
- }
1830
- ),
1831
- /* @__PURE__ */ jsxRuntime.jsx(
1832
- FontPicker,
1833
- {
1834
- label: "Display",
1835
- slot: "display",
1836
- currentFont: state.typography?.fonts.display ?? DEFAULT_FONT_DISPLAY,
1837
- onSelect: (font) => handleFontChange("display", font)
1838
- }
1839
- ),
1840
- /* @__PURE__ */ jsxRuntime.jsx(
1841
- FontPicker,
1842
- {
1843
- label: "Mono",
1844
- slot: "mono",
1845
- currentFont: state.typography?.fonts.mono ?? DEFAULT_FONT_MONO,
1846
- onSelect: (font) => handleFontChange("mono", font)
1847
- }
1848
- )
1849
- ] })
1850
- ] })
1851
- ] });
1852
- }
1853
- function OthersSection({ state, dispatch }) {
1854
- const tokens = components.useTokens();
1855
- const spacingPreset = state.spacing?.preset ?? "md";
1856
- const intensity = state.roundness?.intensity ?? 0.5;
1857
- const spacingOptions = [
1858
- { value: "xs", label: "Extra Small" },
1859
- { value: "sm", label: "Small" },
1860
- { value: "md", label: "Medium" },
1861
- { value: "lg", label: "Large" },
1862
- { value: "xl", label: "Extra Large" }
1863
- ];
1864
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 16 }, children: [
1865
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1866
- /* @__PURE__ */ jsxRuntime.jsx(
1867
- "div",
1868
- {
1869
- style: {
1870
- fontSize: 11,
1871
- fontWeight: 600,
1872
- color: newtone.srgbToHex(tokens.textSecondary.srgb),
1873
- textTransform: "uppercase",
1874
- letterSpacing: 0.5,
1875
- marginBottom: 8
1876
- },
1877
- children: "Spacing"
1878
- }
1879
- ),
1880
- /* @__PURE__ */ jsxRuntime.jsx(
1881
- components.Select,
1882
- {
1883
- value: spacingPreset,
1884
- onValueChange: (preset) => dispatch({ type: "SET_SPACING_PRESET", preset }),
1885
- options: spacingOptions,
1886
- label: "Preset"
1887
- }
1888
- )
1889
- ] }),
1890
- /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1891
- /* @__PURE__ */ jsxRuntime.jsx(
1892
- "div",
1893
- {
1894
- style: {
1895
- fontSize: 11,
1896
- fontWeight: 600,
1897
- color: newtone.srgbToHex(tokens.textSecondary.srgb),
1898
- textTransform: "uppercase",
1899
- letterSpacing: 0.5,
1900
- marginBottom: 8
1901
- },
1902
- children: "Roundness"
1903
- }
1904
- ),
1905
- /* @__PURE__ */ jsxRuntime.jsx(
1906
- components.Slider,
1907
- {
1908
- value: Math.round(intensity * 100),
1909
- onValueChange: (v) => dispatch({ type: "SET_ROUNDNESS_INTENSITY", intensity: v / 100 }),
1910
- min: 0,
1911
- max: 100,
1912
- label: "Intensity",
1913
- showValue: true
1914
- }
1915
- )
1916
- ] })
1917
- ] });
1918
- }
1919
- function PresetSelector({
1920
- presets,
1921
- activePresetId,
1922
- publishedPresetId,
1923
- onSwitchPreset,
1924
- onCreatePreset,
1925
- onRenamePreset,
1926
- onDeletePreset,
1927
- onDuplicatePreset
1928
- }) {
1929
- const tokens = components.useTokens();
1930
- const [isOpen, setIsOpen] = react.useState(false);
1931
- const [renamingId, setRenamingId] = react.useState(null);
1932
- const [renameValue, setRenameValue] = react.useState("");
1933
- const [menuOpenId, setMenuOpenId] = react.useState(null);
1934
- const [hoveredId, setHoveredId] = react.useState(null);
1935
- const [hoveredAction, setHoveredAction] = react.useState(null);
1936
- const dropdownRef = react.useRef(null);
1937
- const renameInputRef = react.useRef(null);
1938
- const activePreset = presets.find((p) => p.id === activePresetId);
1939
- const borderColor = newtone.srgbToHex(tokens.border.srgb);
1940
- const bgColor = newtone.srgbToHex(tokens.background.srgb);
1941
- const textPrimary = newtone.srgbToHex(tokens.textPrimary.srgb);
1942
- const textSecondary = newtone.srgbToHex(tokens.textSecondary.srgb);
1943
- const interactiveColor = newtone.srgbToHex(tokens.accent.fill.srgb);
1944
- const warningColor = newtone.srgbToHex(tokens.warning.fill.srgb);
1945
- const errorColor = newtone.srgbToHex(tokens.error.fill.srgb);
1946
- const hoverBg = `${borderColor}18`;
1947
- const activeBg = `${interactiveColor}14`;
1948
- react.useEffect(() => {
1949
- if (!isOpen) return;
1950
- const handleClickOutside = (e) => {
1951
- if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
1952
- setIsOpen(false);
1953
- setMenuOpenId(null);
1954
- setRenamingId(null);
1955
- }
1956
- };
1957
- document.addEventListener("mousedown", handleClickOutside);
1958
- return () => document.removeEventListener("mousedown", handleClickOutside);
1959
- }, [isOpen]);
1960
- react.useEffect(() => {
1961
- if (renamingId && renameInputRef.current) {
1962
- renameInputRef.current.focus();
1963
- renameInputRef.current.select();
1964
- }
1965
- }, [renamingId]);
1966
- const handleCreate = react.useCallback(async () => {
1967
- const name = `Preset ${presets.length + 1}`;
1968
- const newId = await onCreatePreset(name);
1969
- onSwitchPreset(newId);
1970
- setIsOpen(false);
1971
- }, [presets.length, onCreatePreset, onSwitchPreset]);
1972
- const handleStartRename = react.useCallback(
1973
- (presetId, currentName) => {
1974
- setRenamingId(presetId);
1975
- setRenameValue(currentName);
1976
- setMenuOpenId(null);
1977
- },
1978
- []
1979
- );
1980
- const handleCommitRename = react.useCallback(() => {
1981
- if (renamingId && renameValue.trim()) {
1982
- onRenamePreset(renamingId, renameValue.trim());
1983
- }
1984
- setRenamingId(null);
1985
- }, [renamingId, renameValue, onRenamePreset]);
1986
- const handleDelete = react.useCallback(
1987
- async (presetId) => {
1988
- if (presets.length <= 1) return;
1989
- if (window.confirm("Delete this preset? This cannot be undone.")) {
1990
- await onDeletePreset(presetId);
1991
- }
1992
- setMenuOpenId(null);
1993
- },
1994
- [presets.length, onDeletePreset]
1995
- );
1996
- const handleDuplicate = react.useCallback(
1997
- async (presetId, sourceName) => {
1998
- const newId = await onDuplicatePreset(presetId, `${sourceName} (copy)`);
1999
- onSwitchPreset(newId);
2000
- setMenuOpenId(null);
2001
- setIsOpen(false);
2002
- },
2003
- [onDuplicatePreset, onSwitchPreset]
2004
- );
2005
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: dropdownRef, style: { position: "relative" }, children: [
2006
- /* @__PURE__ */ jsxRuntime.jsxs(
2007
- "button",
2008
- {
2009
- onClick: () => setIsOpen(!isOpen),
2010
- style: {
2011
- display: "flex",
2012
- alignItems: "center",
2013
- gap: 6,
2014
- padding: "4px 10px",
2015
- borderRadius: 6,
2016
- border: `1px solid ${borderColor}`,
2017
- backgroundColor: "transparent",
2018
- color: textPrimary,
2019
- fontSize: 12,
2020
- fontWeight: 500,
2021
- cursor: "pointer",
2022
- maxWidth: 160
2023
- },
2024
- children: [
1164
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1165
+ "div",
1166
+ {
1167
+ style: {
1168
+ display: "flex",
1169
+ alignItems: "center",
1170
+ justifyContent: "space-between",
1171
+ padding: "12px 24px",
1172
+ borderBottom: `1px solid ${borderColor}`,
1173
+ backgroundColor: newtone.srgbToHex(tokens.background.srgb),
1174
+ flexShrink: 0
1175
+ },
1176
+ children: [
1177
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", alignItems: "center", gap: 16 }, children: headerSlots?.left }),
1178
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", gap: 12 }, children: [
2025
1179
  /* @__PURE__ */ jsxRuntime.jsx(
2026
1180
  "span",
2027
1181
  {
2028
1182
  style: {
2029
- overflow: "hidden",
2030
- textOverflow: "ellipsis",
2031
- whiteSpace: "nowrap"
1183
+ fontSize: 12,
1184
+ color: statusColor[saveStatus],
1185
+ fontWeight: 500
2032
1186
  },
2033
- children: activePreset?.name ?? "Default"
1187
+ children: STATUS_LABEL[saveStatus]
2034
1188
  }
2035
1189
  ),
1190
+ saveStatus === "error" && /* @__PURE__ */ jsxRuntime.jsx(components.Button, { variant: "tertiary", semantic: "neutral", size: "sm", icon: "refresh", onPress: onRetry, children: "Retry" }),
2036
1191
  /* @__PURE__ */ jsxRuntime.jsx(
2037
- components.Icon,
1192
+ components.Button,
2038
1193
  {
2039
- name: "expand_more",
2040
- size: 14,
2041
- style: {
2042
- transform: isOpen ? "rotate(180deg)" : "none",
2043
- transition: "transform 150ms ease",
2044
- flexShrink: 0
2045
- }
2046
- }
2047
- )
2048
- ]
2049
- }
2050
- ),
2051
- isOpen && /* @__PURE__ */ jsxRuntime.jsxs(
1194
+ variant: "primary",
1195
+ size: "sm",
1196
+ icon: "publish",
1197
+ onPress: onPublish,
1198
+ disabled: isPublished || publishing,
1199
+ children: publishing ? "Publishing..." : isPublished ? "Published" : "Publish"
1200
+ }
1201
+ ),
1202
+ headerSlots?.right
1203
+ ] })
1204
+ ]
1205
+ }
1206
+ );
1207
+ }
1208
+ var NAV_WIDTH = 60;
1209
+ function PrimaryNav({ activeSectionId, onSelectSection }) {
1210
+ const tokens = components.useTokens();
1211
+ const [hoveredId, setHoveredId] = react.useState(null);
1212
+ const bg = newtone.srgbToHex(tokens.background.srgb);
1213
+ const borderColor = newtone.srgbToHex(tokens.border.srgb);
1214
+ const activeBg = newtone.srgbToHex(tokens.backgroundInteractive.srgb);
1215
+ const iconColor = newtone.srgbToHex(tokens.textSecondary.srgb);
1216
+ const activeIconColor = newtone.srgbToHex(tokens.textPrimary.srgb);
1217
+ return /* @__PURE__ */ jsxRuntime.jsx(
1218
+ "nav",
1219
+ {
1220
+ "aria-label": "Section navigation",
1221
+ style: {
1222
+ width: NAV_WIDTH,
1223
+ flexShrink: 0,
1224
+ display: "flex",
1225
+ flexDirection: "column",
1226
+ alignItems: "center",
1227
+ paddingTop: 12,
1228
+ gap: 4,
1229
+ backgroundColor: bg,
1230
+ borderRight: `1px solid ${borderColor}`
1231
+ },
1232
+ children: components.CATEGORIES.map((category) => {
1233
+ const isActive = activeSectionId === category.id;
1234
+ const isHovered = hoveredId === category.id;
1235
+ return /* @__PURE__ */ jsxRuntime.jsx(
1236
+ "button",
1237
+ {
1238
+ onClick: () => onSelectSection(category.id),
1239
+ onMouseEnter: () => setHoveredId(category.id),
1240
+ onMouseLeave: () => setHoveredId(null),
1241
+ title: category.name,
1242
+ "aria-current": isActive ? "page" : void 0,
1243
+ style: {
1244
+ display: "flex",
1245
+ alignItems: "center",
1246
+ justifyContent: "center",
1247
+ width: 44,
1248
+ height: 44,
1249
+ borderRadius: 12,
1250
+ border: "none",
1251
+ backgroundColor: isActive ? activeBg : isHovered ? `${borderColor}20` : "transparent",
1252
+ cursor: "pointer",
1253
+ transition: "background-color 150ms"
1254
+ },
1255
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1256
+ components.Icon,
1257
+ {
1258
+ name: category.icon ?? "circle",
1259
+ size: 24,
1260
+ color: isActive ? activeIconColor : iconColor
1261
+ }
1262
+ )
1263
+ },
1264
+ category.id
1265
+ );
1266
+ })
1267
+ }
1268
+ );
1269
+ }
1270
+ var STRENGTH_OPTIONS = [
1271
+ { label: "None", value: "none" },
1272
+ { label: "Low", value: "low" },
1273
+ { label: "Medium", value: "medium" },
1274
+ { label: "Hard", value: "hard" }
1275
+ ];
1276
+ function getHexAtNv(previewColors, nv) {
1277
+ const idx = Math.round((1 - nv) * (previewColors.length - 1));
1278
+ const clamped = Math.max(0, Math.min(previewColors.length - 1, idx));
1279
+ return newtone.srgbToHex(previewColors[clamped].srgb);
1280
+ }
1281
+ function ColorsSection({
1282
+ state,
1283
+ dispatch,
1284
+ previewColors,
1285
+ colorMode,
1286
+ onColorModeChange
1287
+ }) {
1288
+ const tokens = components.useTokens();
1289
+ const [activePaletteIndex, setActivePaletteIndex] = react.useState(0);
1290
+ const [modeToggleHovered, setModeToggleHovered] = react.useState(false);
1291
+ const palette = state.palettes[activePaletteIndex];
1292
+ const hueRange = configurator.SEMANTIC_HUE_RANGES[activePaletteIndex];
1293
+ const isNeutral = activePaletteIndex === 0;
1294
+ const activeColor = newtone.srgbToHex(tokens.accent.fill.srgb);
1295
+ const borderColor = newtone.srgbToHex(tokens.border.srgb);
1296
+ const effectiveKeyColor = colorMode === "dark" ? palette.keyColorDark : palette.keyColor;
1297
+ const setKeyColorAction = colorMode === "dark" ? "SET_PALETTE_KEY_COLOR_DARK" : "SET_PALETTE_KEY_COLOR";
1298
+ const clearKeyColorAction = colorMode === "dark" ? "CLEAR_PALETTE_KEY_COLOR_DARK" : "CLEAR_PALETTE_KEY_COLOR";
1299
+ const hexAction = colorMode === "dark" ? "SET_PALETTE_FROM_HEX_DARK" : "SET_PALETTE_FROM_HEX";
1300
+ const wcag = configurator.useWcagValidation(state, activePaletteIndex);
1301
+ const [hexText, setHexText] = react.useState("");
1302
+ const [hexError, setHexError] = react.useState("");
1303
+ const [isEditingHex, setIsEditingHex] = react.useState(false);
1304
+ const [isHexUserSet, setIsHexUserSet] = react.useState(false);
1305
+ react.useEffect(() => {
1306
+ setHexText("");
1307
+ setHexError("");
1308
+ setIsEditingHex(false);
1309
+ setIsHexUserSet(false);
1310
+ }, [colorMode]);
1311
+ const currentPreview = previewColors[activePaletteIndex];
1312
+ const displayedHex = react.useMemo(() => {
1313
+ if (!currentPreview || currentPreview.length === 0) return "";
1314
+ const nv = effectiveKeyColor ?? wcag.autoNormalizedValue;
1315
+ return getHexAtNv(currentPreview, nv);
1316
+ }, [currentPreview, effectiveKeyColor, wcag.autoNormalizedValue]);
1317
+ react.useEffect(() => {
1318
+ if (!isEditingHex && !isHexUserSet) {
1319
+ setHexText(displayedHex);
1320
+ }
1321
+ }, [displayedHex, isEditingHex, isHexUserSet]);
1322
+ const dynamicRange = react.useMemo(() => {
1323
+ const light = state.globalHueGrading.light.strength !== "none" ? {
1324
+ hue: configurator.traditionalHueToOklch(state.globalHueGrading.light.hue),
1325
+ strength: state.globalHueGrading.light.strength
1326
+ } : void 0;
1327
+ const dark = state.globalHueGrading.dark.strength !== "none" ? {
1328
+ hue: configurator.traditionalHueToOklch(state.globalHueGrading.dark.hue),
1329
+ strength: state.globalHueGrading.dark.strength
1330
+ } : void 0;
1331
+ const hueGrading = light || dark ? { light, dark } : void 0;
1332
+ return {
1333
+ lightest: state.dynamicRange.lightest,
1334
+ darkest: state.dynamicRange.darkest,
1335
+ ...hueGrading ? { hueGrading } : {}
1336
+ };
1337
+ }, [state.dynamicRange, state.globalHueGrading]);
1338
+ const handleHexSubmit = react.useCallback(() => {
1339
+ setIsEditingHex(false);
1340
+ const trimmed = hexText.trim();
1341
+ if (!trimmed) {
1342
+ setHexError("");
1343
+ return;
1344
+ }
1345
+ const hex = trimmed.startsWith("#") ? trimmed : `#${trimmed}`;
1346
+ const params = configurator.hexToPaletteParams(hex, dynamicRange);
1347
+ if (!params) {
1348
+ setHexError("Invalid hex color");
1349
+ return;
1350
+ }
1351
+ setHexError("");
1352
+ setIsHexUserSet(true);
1353
+ dispatch({
1354
+ type: hexAction,
1355
+ index: activePaletteIndex,
1356
+ hue: params.hue,
1357
+ saturation: params.saturation,
1358
+ keyColor: params.normalizedValue
1359
+ });
1360
+ }, [hexText, dynamicRange, dispatch, activePaletteIndex, hexAction]);
1361
+ const handleClearKeyColor = react.useCallback(() => {
1362
+ dispatch({ type: clearKeyColorAction, index: activePaletteIndex });
1363
+ setHexError("");
1364
+ setIsHexUserSet(false);
1365
+ }, [dispatch, activePaletteIndex, clearKeyColorAction]);
1366
+ const wcagWarning = react.useMemo(() => {
1367
+ if (effectiveKeyColor === void 0 || wcag.keyColorContrast === null)
1368
+ return void 0;
1369
+ if (wcag.passesAA) return void 0;
1370
+ const ratio = wcag.keyColorContrast.toFixed(1);
1371
+ if (wcag.passesAALargeText) {
1372
+ return `Contrast ${ratio}:1 \u2014 passes large text (AA) but fails normal text (requires 4.5:1)`;
1373
+ }
1374
+ return `Contrast ${ratio}:1 \u2014 fails WCAG AA (requires 4.5:1 for normal text, 3:1 for large text)`;
1375
+ }, [effectiveKeyColor, wcag]);
1376
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 16 }, children: [
1377
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center" }, children: [
1378
+ state.palettes.map((_p, index) => {
1379
+ const isActive = index === activePaletteIndex;
1380
+ const colors = previewColors[index];
1381
+ const isNeutralCircle = index === 0;
1382
+ const paletteKeyColor = colorMode === "dark" ? _p.keyColorDark : _p.keyColor;
1383
+ const circleColor = !isNeutralCircle && colors ? getHexAtNv(
1384
+ colors,
1385
+ paletteKeyColor ?? wcag.autoNormalizedValue
1386
+ ) : void 0;
1387
+ const ringStyle = isActive ? `0 0 0 2px ${newtone.srgbToHex(tokens.background.srgb)}, 0 0 0 4px ${activeColor}` : "none";
1388
+ return /* @__PURE__ */ jsxRuntime.jsx(
1389
+ "button",
1390
+ {
1391
+ onClick: () => setActivePaletteIndex(index),
1392
+ "aria-label": _p.name,
1393
+ "aria-pressed": isActive,
1394
+ style: {
1395
+ width: 32,
1396
+ height: 32,
1397
+ borderRadius: "50%",
1398
+ border: "none",
1399
+ cursor: "pointer",
1400
+ flexShrink: 0,
1401
+ boxShadow: ringStyle,
1402
+ transition: "box-shadow 150ms ease",
1403
+ padding: 0,
1404
+ overflow: "hidden",
1405
+ ...isNeutralCircle ? {
1406
+ background: colors ? `linear-gradient(to right, ${newtone.srgbToHex(colors[0].srgb)} 50%, ${newtone.srgbToHex(colors[colors.length - 1].srgb)} 50%)` : `linear-gradient(to right, #ffffff 50%, #000000 50%)`
1407
+ } : { backgroundColor: circleColor ?? borderColor }
1408
+ }
1409
+ },
1410
+ index
1411
+ );
1412
+ }),
1413
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1 } }),
1414
+ /* @__PURE__ */ jsxRuntime.jsxs(
1415
+ "button",
1416
+ {
1417
+ onClick: () => onColorModeChange(colorMode === "light" ? "dark" : "light"),
1418
+ onMouseEnter: () => setModeToggleHovered(true),
1419
+ onMouseLeave: () => setModeToggleHovered(false),
1420
+ "aria-label": colorMode === "light" ? "Switch to dark mode" : "Switch to light mode",
1421
+ style: {
1422
+ display: "flex",
1423
+ alignItems: "center",
1424
+ gap: 6,
1425
+ padding: "4px 10px",
1426
+ borderRadius: 6,
1427
+ border: `1px solid ${borderColor}`,
1428
+ background: modeToggleHovered ? `${borderColor}20` : "none",
1429
+ cursor: "pointer",
1430
+ fontSize: 12,
1431
+ color: newtone.srgbToHex(tokens.textPrimary.srgb),
1432
+ transition: "background-color 150ms ease"
1433
+ },
1434
+ children: [
1435
+ colorMode === "light" ? "\u2600" : "\u263E",
1436
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: colorMode === "light" ? "Light" : "Dark" })
1437
+ ]
1438
+ }
1439
+ )
1440
+ ] }),
1441
+ currentPreview && (isNeutral ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", gap: 1 }, children: currentPreview.map((color, i) => /* @__PURE__ */ jsxRuntime.jsx(
2052
1442
  "div",
2053
1443
  {
2054
1444
  style: {
2055
- position: "absolute",
2056
- top: "calc(100% + 4px)",
2057
- left: 0,
2058
- width: 260,
2059
- backgroundColor: bgColor,
2060
- border: `1px solid ${borderColor}`,
2061
- borderRadius: 8,
2062
- boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
2063
- zIndex: 100,
2064
- overflow: "hidden"
2065
- },
1445
+ flex: 1,
1446
+ height: 64,
1447
+ borderRadius: 2,
1448
+ backgroundColor: newtone.srgbToHex(color.srgb)
1449
+ }
1450
+ },
1451
+ i
1452
+ )) }) : /* @__PURE__ */ jsxRuntime.jsxs(
1453
+ "div",
1454
+ {
1455
+ style: { display: "flex", flexDirection: "column", gap: 8 },
2066
1456
  children: [
2067
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { maxHeight: 240, overflowY: "auto", padding: "4px 0" }, children: presets.map((preset) => {
2068
- const isActive = preset.id === activePresetId;
2069
- const isPublishedPreset = preset.id === publishedPresetId;
2070
- const hasChanges = presetHasUnpublishedChanges(preset);
2071
- const isHovered = hoveredId === preset.id;
2072
- const isRenaming = renamingId === preset.id;
2073
- const isMenuShown = menuOpenId === preset.id;
2074
- return /* @__PURE__ */ jsxRuntime.jsxs(
2075
- "div",
2076
- {
2077
- onMouseEnter: () => setHoveredId(preset.id),
2078
- onMouseLeave: () => setHoveredId(null),
2079
- style: {
2080
- display: "flex",
2081
- alignItems: "center",
2082
- padding: "6px 12px",
2083
- backgroundColor: isActive ? activeBg : isHovered ? hoverBg : "transparent",
2084
- cursor: isRenaming ? "default" : "pointer",
2085
- transition: "background-color 100ms ease",
2086
- position: "relative"
2087
- },
2088
- children: [
2089
- isRenaming ? /* @__PURE__ */ jsxRuntime.jsx(
2090
- "input",
2091
- {
2092
- ref: renameInputRef,
2093
- value: renameValue,
2094
- onChange: (e) => setRenameValue(e.target.value),
2095
- onBlur: handleCommitRename,
2096
- onKeyDown: (e) => {
2097
- if (e.key === "Enter") handleCommitRename();
2098
- if (e.key === "Escape") setRenamingId(null);
2099
- },
2100
- style: {
2101
- flex: 1,
2102
- fontSize: 13,
2103
- padding: "2px 6px",
2104
- border: `1px solid ${interactiveColor}`,
2105
- borderRadius: 4,
2106
- backgroundColor: bgColor,
2107
- color: textPrimary,
2108
- outline: "none"
2109
- }
2110
- }
2111
- ) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2112
- /* @__PURE__ */ jsxRuntime.jsxs(
2113
- "div",
2114
- {
2115
- onClick: () => {
2116
- onSwitchPreset(preset.id);
2117
- setIsOpen(false);
2118
- },
2119
- style: {
2120
- flex: 1,
2121
- display: "flex",
2122
- alignItems: "center",
2123
- gap: 6,
2124
- minWidth: 0
2125
- },
2126
- children: [
2127
- /* @__PURE__ */ jsxRuntime.jsx(
2128
- "span",
2129
- {
2130
- style: {
2131
- fontSize: 13,
2132
- fontWeight: isActive ? 600 : 400,
2133
- color: textPrimary,
2134
- overflow: "hidden",
2135
- textOverflow: "ellipsis",
2136
- whiteSpace: "nowrap"
2137
- },
2138
- children: preset.name
2139
- }
2140
- ),
2141
- hasChanges && /* @__PURE__ */ jsxRuntime.jsx(
2142
- "span",
2143
- {
2144
- title: "Unpublished changes",
2145
- style: {
2146
- width: 6,
2147
- height: 6,
2148
- borderRadius: "50%",
2149
- backgroundColor: warningColor,
2150
- flexShrink: 0
2151
- }
2152
- }
2153
- ),
2154
- isPublishedPreset && /* @__PURE__ */ jsxRuntime.jsx(
2155
- "span",
2156
- {
2157
- style: {
2158
- fontSize: 10,
2159
- fontWeight: 600,
2160
- color: interactiveColor,
2161
- padding: "1px 4px",
2162
- borderRadius: 3,
2163
- border: `1px solid ${interactiveColor}`,
2164
- flexShrink: 0,
2165
- lineHeight: "14px"
2166
- },
2167
- children: "API"
2168
- }
2169
- )
2170
- ]
2171
- }
2172
- ),
2173
- (isHovered || isMenuShown) && /* @__PURE__ */ jsxRuntime.jsx(
2174
- "button",
2175
- {
2176
- onClick: (e) => {
2177
- e.stopPropagation();
2178
- setMenuOpenId(isMenuShown ? null : preset.id);
2179
- },
2180
- style: {
2181
- display: "flex",
2182
- alignItems: "center",
2183
- justifyContent: "center",
2184
- width: 24,
2185
- height: 24,
2186
- border: "none",
2187
- background: "none",
2188
- color: textSecondary,
2189
- cursor: "pointer",
2190
- borderRadius: 4,
2191
- flexShrink: 0
2192
- },
2193
- children: /* @__PURE__ */ jsxRuntime.jsx(components.Icon, { name: "more_vert", size: 14, color: textSecondary })
2194
- }
2195
- )
2196
- ] }),
2197
- isMenuShown && !isRenaming && /* @__PURE__ */ jsxRuntime.jsxs(
2198
- "div",
2199
- {
2200
- style: {
2201
- position: "absolute",
2202
- top: 0,
2203
- right: -140,
2204
- width: 130,
2205
- backgroundColor: bgColor,
2206
- border: `1px solid ${borderColor}`,
2207
- borderRadius: 6,
2208
- boxShadow: "0 2px 8px rgba(0,0,0,0.12)",
2209
- zIndex: 101,
2210
- overflow: "hidden"
2211
- },
2212
- children: [
2213
- /* @__PURE__ */ jsxRuntime.jsx(
2214
- "button",
2215
- {
2216
- onClick: (e) => {
2217
- e.stopPropagation();
2218
- handleStartRename(preset.id, preset.name);
2219
- },
2220
- onMouseEnter: () => setHoveredAction("rename"),
2221
- onMouseLeave: () => setHoveredAction(null),
2222
- style: {
2223
- display: "block",
2224
- width: "100%",
2225
- padding: "8px 12px",
2226
- border: "none",
2227
- backgroundColor: hoveredAction === "rename" ? hoverBg : "transparent",
2228
- color: textPrimary,
2229
- fontSize: 12,
2230
- textAlign: "left",
2231
- cursor: "pointer"
2232
- },
2233
- children: "Rename"
2234
- }
2235
- ),
2236
- /* @__PURE__ */ jsxRuntime.jsx(
2237
- "button",
2238
- {
2239
- onClick: (e) => {
2240
- e.stopPropagation();
2241
- handleDuplicate(preset.id, preset.name);
2242
- },
2243
- onMouseEnter: () => setHoveredAction("duplicate"),
2244
- onMouseLeave: () => setHoveredAction(null),
2245
- style: {
2246
- display: "block",
2247
- width: "100%",
2248
- padding: "8px 12px",
2249
- border: "none",
2250
- backgroundColor: hoveredAction === "duplicate" ? hoverBg : "transparent",
2251
- color: textPrimary,
2252
- fontSize: 12,
2253
- textAlign: "left",
2254
- cursor: "pointer"
2255
- },
2256
- children: "Duplicate"
2257
- }
2258
- ),
2259
- /* @__PURE__ */ jsxRuntime.jsx(
2260
- "button",
2261
- {
2262
- onClick: (e) => {
2263
- e.stopPropagation();
2264
- handleDelete(preset.id);
2265
- },
2266
- onMouseEnter: () => setHoveredAction("delete"),
2267
- onMouseLeave: () => setHoveredAction(null),
2268
- disabled: presets.length <= 1,
2269
- style: {
2270
- display: "block",
2271
- width: "100%",
2272
- padding: "8px 12px",
2273
- border: "none",
2274
- backgroundColor: hoveredAction === "delete" ? hoverBg : "transparent",
2275
- color: presets.length <= 1 ? textSecondary : errorColor,
2276
- fontSize: 12,
2277
- textAlign: "left",
2278
- cursor: presets.length <= 1 ? "not-allowed" : "pointer",
2279
- opacity: presets.length <= 1 ? 0.5 : 1
2280
- },
2281
- children: "Delete"
2282
- }
2283
- )
2284
- ]
2285
- }
2286
- )
2287
- ]
1457
+ /* @__PURE__ */ jsxRuntime.jsx(
1458
+ components.ColorScaleSlider,
1459
+ {
1460
+ colors: currentPreview,
1461
+ value: effectiveKeyColor ?? wcag.autoNormalizedValue,
1462
+ onValueChange: (nv) => {
1463
+ setIsHexUserSet(false);
1464
+ dispatch({
1465
+ type: setKeyColorAction,
1466
+ index: activePaletteIndex,
1467
+ normalizedValue: nv
1468
+ });
2288
1469
  },
2289
- preset.id
2290
- );
2291
- }) }),
1470
+ trimEnds: true,
1471
+ snap: true,
1472
+ label: "Key Color",
1473
+ warning: wcagWarning,
1474
+ animateValue: true
1475
+ }
1476
+ ),
2292
1477
  /* @__PURE__ */ jsxRuntime.jsxs(
2293
- "button",
1478
+ "div",
1479
+ {
1480
+ style: {
1481
+ display: "flex",
1482
+ gap: 8,
1483
+ alignItems: "flex-end"
1484
+ },
1485
+ children: [
1486
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(
1487
+ components.TextInput,
1488
+ {
1489
+ label: "Hex",
1490
+ value: hexText,
1491
+ onChangeText: (text) => {
1492
+ setIsEditingHex(true);
1493
+ setHexText(text);
1494
+ setHexError("");
1495
+ },
1496
+ onBlur: handleHexSubmit,
1497
+ onSubmitEditing: handleHexSubmit,
1498
+ placeholder: "#000000"
1499
+ }
1500
+ ) }),
1501
+ effectiveKeyColor !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(
1502
+ "button",
1503
+ {
1504
+ onClick: handleClearKeyColor,
1505
+ style: {
1506
+ background: "none",
1507
+ border: "none",
1508
+ cursor: "pointer",
1509
+ padding: "0 0 6px",
1510
+ fontSize: 13,
1511
+ fontWeight: 600,
1512
+ color: activeColor
1513
+ },
1514
+ children: "Auto"
1515
+ }
1516
+ )
1517
+ ]
1518
+ }
1519
+ ),
1520
+ hexError && /* @__PURE__ */ jsxRuntime.jsx(
1521
+ "div",
1522
+ {
1523
+ style: {
1524
+ fontSize: 12,
1525
+ fontWeight: 500,
1526
+ color: newtone.srgbToHex(tokens.error.fill.srgb)
1527
+ },
1528
+ children: hexError
1529
+ }
1530
+ )
1531
+ ]
1532
+ }
1533
+ )),
1534
+ /* @__PURE__ */ jsxRuntime.jsx(
1535
+ "div",
1536
+ {
1537
+ style: {
1538
+ height: 1,
1539
+ backgroundColor: borderColor,
1540
+ margin: "4px 0"
1541
+ }
1542
+ }
1543
+ ),
1544
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: [
1545
+ /* @__PURE__ */ jsxRuntime.jsx(
1546
+ components.HueSlider,
1547
+ {
1548
+ value: palette.hue,
1549
+ onValueChange: (hue) => dispatch({
1550
+ type: "SET_PALETTE_HUE",
1551
+ index: activePaletteIndex,
1552
+ hue
1553
+ }),
1554
+ label: "Hue",
1555
+ editableValue: true,
1556
+ ...hueRange ? { min: hueRange.min, max: hueRange.max } : {}
1557
+ }
1558
+ ),
1559
+ /* @__PURE__ */ jsxRuntime.jsx(
1560
+ components.Slider,
1561
+ {
1562
+ value: palette.saturation,
1563
+ onValueChange: (saturation) => dispatch({
1564
+ type: "SET_PALETTE_SATURATION",
1565
+ index: activePaletteIndex,
1566
+ saturation
1567
+ }),
1568
+ min: 0,
1569
+ max: 100,
1570
+ label: "Saturation",
1571
+ editableValue: true
1572
+ }
1573
+ ),
1574
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 12, alignItems: "flex-end" }, children: [
1575
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(
1576
+ components.Select,
1577
+ {
1578
+ options: STRENGTH_OPTIONS,
1579
+ value: palette.desaturationStrength,
1580
+ onValueChange: (strength) => dispatch({
1581
+ type: "SET_PALETTE_DESAT_STRENGTH",
1582
+ index: activePaletteIndex,
1583
+ strength
1584
+ }),
1585
+ label: "Desaturation"
1586
+ }
1587
+ ) }),
1588
+ palette.desaturationStrength !== "none" && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { paddingBottom: 2 }, children: /* @__PURE__ */ jsxRuntime.jsx(
1589
+ components.Toggle,
1590
+ {
1591
+ value: palette.desaturationDirection === "dark",
1592
+ onValueChange: (v) => dispatch({
1593
+ type: "SET_PALETTE_DESAT_DIRECTION",
1594
+ index: activePaletteIndex,
1595
+ direction: v ? "dark" : "light"
1596
+ }),
1597
+ label: "Invert"
1598
+ }
1599
+ ) })
1600
+ ] }),
1601
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 12, alignItems: "flex-end" }, children: [
1602
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(
1603
+ components.Select,
1604
+ {
1605
+ options: STRENGTH_OPTIONS,
1606
+ value: palette.hueGradeStrength,
1607
+ onValueChange: (strength) => dispatch({
1608
+ type: "SET_PALETTE_HUE_GRADE_STRENGTH",
1609
+ index: activePaletteIndex,
1610
+ strength
1611
+ }),
1612
+ label: "Hue Grading"
1613
+ }
1614
+ ) }),
1615
+ palette.hueGradeStrength !== "none" && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { paddingBottom: 2 }, children: /* @__PURE__ */ jsxRuntime.jsx(
1616
+ components.Toggle,
1617
+ {
1618
+ value: palette.hueGradeDirection === "dark",
1619
+ onValueChange: (v) => dispatch({
1620
+ type: "SET_PALETTE_HUE_GRADE_DIRECTION",
1621
+ index: activePaletteIndex,
1622
+ direction: v ? "dark" : "light"
1623
+ }),
1624
+ label: "Invert"
1625
+ }
1626
+ ) })
1627
+ ] }),
1628
+ palette.hueGradeStrength !== "none" && /* @__PURE__ */ jsxRuntime.jsx(
1629
+ components.HueSlider,
1630
+ {
1631
+ value: palette.hueGradeHue,
1632
+ onValueChange: (hue) => dispatch({
1633
+ type: "SET_PALETTE_HUE_GRADE_HUE",
1634
+ index: activePaletteIndex,
1635
+ hue
1636
+ }),
1637
+ label: "Grade Target",
1638
+ editableValue: true
1639
+ }
1640
+ )
1641
+ ] })
1642
+ ] });
1643
+ }
1644
+ var STRENGTH_OPTIONS2 = [
1645
+ { label: "None", value: "none" },
1646
+ { label: "Low", value: "low" },
1647
+ { label: "Medium", value: "medium" },
1648
+ { label: "Hard", value: "hard" }
1649
+ ];
1650
+ var TRACK_HEIGHT = 8;
1651
+ var THUMB_SIZE = 18;
1652
+ var ZONE_FRAC = 1 / 3;
1653
+ function clamp(v, min, max) {
1654
+ return Math.min(max, Math.max(min, v));
1655
+ }
1656
+ function internalToDisplay(internal) {
1657
+ return clamp(Math.round(internal * 10), 0, 10);
1658
+ }
1659
+ function displayToInternal(display) {
1660
+ return clamp(display, 0, 10) / 10;
1661
+ }
1662
+ function posToWhitesDisplay(pos) {
1663
+ const ratio = clamp(pos / ZONE_FRAC, 0, 1);
1664
+ return Math.round(10 * (1 - ratio));
1665
+ }
1666
+ function posToBlacksDisplay(pos) {
1667
+ const ratio = clamp((pos - (1 - ZONE_FRAC)) / ZONE_FRAC, 0, 1);
1668
+ return Math.round(ratio * 10);
1669
+ }
1670
+ function whitesDisplayToPos(display) {
1671
+ return (10 - display) / 10 * ZONE_FRAC;
1672
+ }
1673
+ function blacksDisplayToPos(display) {
1674
+ return 1 - ZONE_FRAC + display / 10 * ZONE_FRAC;
1675
+ }
1676
+ function DualRangeSlider({
1677
+ whitesValue,
1678
+ blacksValue,
1679
+ onWhitesChange,
1680
+ onBlacksChange
1681
+ }) {
1682
+ const tokens = components.useTokens();
1683
+ const trackRef = react.useRef(null);
1684
+ const [activeThumb, setActiveThumb] = react.useState(null);
1685
+ const interactiveColor = newtone.srgbToHex(tokens.accent.fill.srgb);
1686
+ const borderColor = newtone.srgbToHex(tokens.border.srgb);
1687
+ const wDisplay = internalToDisplay(whitesValue);
1688
+ const bDisplay = internalToDisplay(blacksValue);
1689
+ const wPos = whitesDisplayToPos(wDisplay);
1690
+ const bPos = blacksDisplayToPos(bDisplay);
1691
+ const getPosRatio = react.useCallback((clientX) => {
1692
+ if (!trackRef.current) return 0;
1693
+ const rect = trackRef.current.getBoundingClientRect();
1694
+ return clamp((clientX - rect.left) / rect.width, 0, 1);
1695
+ }, []);
1696
+ const handlePointerDown = react.useCallback(
1697
+ (e) => {
1698
+ e.preventDefault();
1699
+ const pos = getPosRatio(e.clientX);
1700
+ if (pos <= ZONE_FRAC) {
1701
+ setActiveThumb("whites");
1702
+ onWhitesChange(displayToInternal(posToWhitesDisplay(pos)));
1703
+ } else if (pos >= 1 - ZONE_FRAC) {
1704
+ setActiveThumb("blacks");
1705
+ onBlacksChange(displayToInternal(posToBlacksDisplay(pos)));
1706
+ } else {
1707
+ return;
1708
+ }
1709
+ e.currentTarget.setPointerCapture(e.pointerId);
1710
+ },
1711
+ [getPosRatio, onWhitesChange, onBlacksChange]
1712
+ );
1713
+ const handlePointerMove = react.useCallback(
1714
+ (e) => {
1715
+ if (!activeThumb) return;
1716
+ const pos = getPosRatio(e.clientX);
1717
+ if (activeThumb === "whites") {
1718
+ onWhitesChange(displayToInternal(posToWhitesDisplay(pos)));
1719
+ } else {
1720
+ onBlacksChange(displayToInternal(posToBlacksDisplay(pos)));
1721
+ }
1722
+ },
1723
+ [activeThumb, getPosRatio, onWhitesChange, onBlacksChange]
1724
+ );
1725
+ const handlePointerUp = react.useCallback(() => {
1726
+ setActiveThumb(null);
1727
+ }, []);
1728
+ const trackTop = (THUMB_SIZE - TRACK_HEIGHT) / 2;
1729
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: `0 ${THUMB_SIZE / 2}px` }, children: /* @__PURE__ */ jsxRuntime.jsxs(
1730
+ "div",
1731
+ {
1732
+ ref: trackRef,
1733
+ onPointerDown: handlePointerDown,
1734
+ onPointerMove: handlePointerMove,
1735
+ onPointerUp: handlePointerUp,
1736
+ onPointerCancel: handlePointerUp,
1737
+ style: {
1738
+ position: "relative",
1739
+ height: THUMB_SIZE,
1740
+ cursor: activeThumb ? "grabbing" : "pointer",
1741
+ touchAction: "none",
1742
+ userSelect: "none"
1743
+ },
1744
+ children: [
1745
+ /* @__PURE__ */ jsxRuntime.jsx(
1746
+ "div",
1747
+ {
1748
+ style: {
1749
+ position: "absolute",
1750
+ left: 0,
1751
+ right: 0,
1752
+ top: trackTop,
1753
+ height: TRACK_HEIGHT,
1754
+ borderRadius: TRACK_HEIGHT / 2,
1755
+ background: "linear-gradient(to right, white, black)",
1756
+ border: `1px solid ${borderColor}`,
1757
+ boxSizing: "border-box"
1758
+ }
1759
+ }
1760
+ ),
1761
+ /* @__PURE__ */ jsxRuntime.jsx(
1762
+ "div",
1763
+ {
1764
+ style: {
1765
+ position: "absolute",
1766
+ left: `${wPos * 100}%`,
1767
+ width: `${(bPos - wPos) * 100}%`,
1768
+ top: trackTop,
1769
+ height: TRACK_HEIGHT,
1770
+ backgroundColor: interactiveColor
1771
+ }
1772
+ }
1773
+ ),
1774
+ /* @__PURE__ */ jsxRuntime.jsx(
1775
+ "div",
1776
+ {
1777
+ style: {
1778
+ position: "absolute",
1779
+ left: `calc(${wPos * 100}% - ${THUMB_SIZE / 2}px)`,
1780
+ top: 0,
1781
+ width: THUMB_SIZE,
1782
+ height: THUMB_SIZE,
1783
+ borderRadius: THUMB_SIZE / 2,
1784
+ backgroundColor: interactiveColor,
1785
+ pointerEvents: "none",
1786
+ zIndex: activeThumb === "whites" ? 2 : 1
1787
+ }
1788
+ }
1789
+ ),
1790
+ /* @__PURE__ */ jsxRuntime.jsx(
1791
+ "div",
1792
+ {
1793
+ style: {
1794
+ position: "absolute",
1795
+ left: `calc(${bPos * 100}% - ${THUMB_SIZE / 2}px)`,
1796
+ top: 0,
1797
+ width: THUMB_SIZE,
1798
+ height: THUMB_SIZE,
1799
+ borderRadius: THUMB_SIZE / 2,
1800
+ backgroundColor: interactiveColor,
1801
+ pointerEvents: "none",
1802
+ zIndex: activeThumb === "blacks" ? 2 : 1
1803
+ }
1804
+ }
1805
+ )
1806
+ ]
1807
+ }
1808
+ ) });
1809
+ }
1810
+ function RangeInput({ display, onCommit, toInternal }) {
1811
+ const tokens = components.useTokens();
1812
+ const [text, setText] = react.useState(String(display));
1813
+ const [isEditing, setIsEditing] = react.useState(false);
1814
+ const displayText = isEditing ? text : String(display);
1815
+ const commit = () => {
1816
+ setIsEditing(false);
1817
+ const parsed = parseInt(text, 10);
1818
+ if (isNaN(parsed)) {
1819
+ setText(String(display));
1820
+ return;
1821
+ }
1822
+ const clamped = clamp(Math.round(parsed), 0, 10);
1823
+ onCommit(toInternal(clamped));
1824
+ setText(String(clamped));
1825
+ };
1826
+ return /* @__PURE__ */ jsxRuntime.jsx(
1827
+ "input",
1828
+ {
1829
+ type: "text",
1830
+ inputMode: "numeric",
1831
+ value: displayText,
1832
+ onChange: (e) => {
1833
+ setIsEditing(true);
1834
+ setText(e.target.value);
1835
+ },
1836
+ onBlur: commit,
1837
+ onKeyDown: (e) => {
1838
+ if (e.key === "Enter") commit();
1839
+ },
1840
+ style: {
1841
+ width: 40,
1842
+ padding: "2px 6px",
1843
+ border: `1px solid ${newtone.srgbToHex(tokens.border.srgb)}`,
1844
+ borderRadius: 4,
1845
+ backgroundColor: "transparent",
1846
+ color: newtone.srgbToHex(tokens.textPrimary.srgb),
1847
+ fontFamily: "inherit",
1848
+ fontSize: 12,
1849
+ fontWeight: 500,
1850
+ textAlign: "center",
1851
+ outline: "none"
1852
+ }
1853
+ }
1854
+ );
1855
+ }
1856
+ function DynamicRangeSection({
1857
+ state,
1858
+ dispatch
1859
+ }) {
1860
+ const tokens = components.useTokens();
1861
+ const labelColor = newtone.srgbToHex(tokens.textSecondary.srgb);
1862
+ const labelStyle = {
1863
+ fontSize: 11,
1864
+ fontWeight: 600,
1865
+ color: labelColor,
1866
+ textTransform: "uppercase",
1867
+ letterSpacing: 0.5
1868
+ };
1869
+ const wDisplay = internalToDisplay(state.dynamicRange.lightest);
1870
+ const bDisplay = internalToDisplay(state.dynamicRange.darkest);
1871
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: [
1872
+ /* @__PURE__ */ jsxRuntime.jsxs(
1873
+ "div",
1874
+ {
1875
+ style: {
1876
+ display: "flex",
1877
+ justifyContent: "space-between",
1878
+ alignItems: "center"
1879
+ },
1880
+ children: [
1881
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: labelStyle, children: "Whites" }),
1882
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: labelStyle, children: "Blacks" })
1883
+ ]
1884
+ }
1885
+ ),
1886
+ /* @__PURE__ */ jsxRuntime.jsx(
1887
+ DualRangeSlider,
1888
+ {
1889
+ whitesValue: state.dynamicRange.lightest,
1890
+ blacksValue: state.dynamicRange.darkest,
1891
+ onWhitesChange: (v) => dispatch({ type: "SET_LIGHTEST", value: v }),
1892
+ onBlacksChange: (v) => dispatch({ type: "SET_DARKEST", value: v })
1893
+ }
1894
+ ),
1895
+ /* @__PURE__ */ jsxRuntime.jsxs(
1896
+ "div",
1897
+ {
1898
+ style: {
1899
+ display: "flex",
1900
+ justifyContent: "space-between",
1901
+ alignItems: "center"
1902
+ },
1903
+ children: [
1904
+ /* @__PURE__ */ jsxRuntime.jsx(
1905
+ RangeInput,
1906
+ {
1907
+ display: wDisplay,
1908
+ onCommit: (v) => dispatch({ type: "SET_LIGHTEST", value: v }),
1909
+ toInternal: displayToInternal
1910
+ }
1911
+ ),
1912
+ /* @__PURE__ */ jsxRuntime.jsx(
1913
+ RangeInput,
1914
+ {
1915
+ display: bDisplay,
1916
+ onCommit: (v) => dispatch({ type: "SET_DARKEST", value: v }),
1917
+ toInternal: displayToInternal
1918
+ }
1919
+ )
1920
+ ]
1921
+ }
1922
+ ),
1923
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { ...labelStyle, marginTop: 4 }, children: "Global Hue Grading \u2014 Light" }),
1924
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 12 }, children: [
1925
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(
1926
+ components.Select,
1927
+ {
1928
+ options: STRENGTH_OPTIONS2,
1929
+ value: state.globalHueGrading.light.strength,
1930
+ onValueChange: (s) => dispatch({
1931
+ type: "SET_GLOBAL_GRADE_LIGHT_STRENGTH",
1932
+ strength: s
1933
+ }),
1934
+ label: "Strength"
1935
+ }
1936
+ ) }),
1937
+ state.globalHueGrading.light.strength !== "none" && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(
1938
+ components.HueSlider,
1939
+ {
1940
+ value: state.globalHueGrading.light.hue,
1941
+ onValueChange: (hue) => dispatch({ type: "SET_GLOBAL_GRADE_LIGHT_HUE", hue }),
1942
+ label: "Target Hue",
1943
+ showValue: true
1944
+ }
1945
+ ) })
1946
+ ] }),
1947
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { ...labelStyle, marginTop: 4 }, children: "Global Hue Grading \u2014 Dark" }),
1948
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 12 }, children: [
1949
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(
1950
+ components.Select,
1951
+ {
1952
+ options: STRENGTH_OPTIONS2,
1953
+ value: state.globalHueGrading.dark.strength,
1954
+ onValueChange: (s) => dispatch({
1955
+ type: "SET_GLOBAL_GRADE_DARK_STRENGTH",
1956
+ strength: s
1957
+ }),
1958
+ label: "Strength"
1959
+ }
1960
+ ) }),
1961
+ state.globalHueGrading.dark.strength !== "none" && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(
1962
+ components.HueSlider,
1963
+ {
1964
+ value: state.globalHueGrading.dark.hue,
1965
+ onValueChange: (hue) => dispatch({ type: "SET_GLOBAL_GRADE_DARK_HUE", hue }),
1966
+ label: "Target Hue",
1967
+ showValue: true
1968
+ }
1969
+ ) })
1970
+ ] })
1971
+ ] });
1972
+ }
1973
+ var ICON_VARIANT_OPTIONS = [
1974
+ { label: "Outlined", value: "outlined" },
1975
+ { label: "Rounded", value: "rounded" },
1976
+ { label: "Sharp", value: "sharp" }
1977
+ ];
1978
+ var ICON_WEIGHT_OPTIONS = [
1979
+ { label: "100", value: "100" },
1980
+ { label: "200", value: "200" },
1981
+ { label: "300", value: "300" },
1982
+ { label: "400", value: "400" },
1983
+ { label: "500", value: "500" },
1984
+ { label: "600", value: "600" },
1985
+ { label: "700", value: "700" }
1986
+ ];
1987
+ function IconsSection({ state, dispatch }) {
1988
+ const variant = state.icons?.variant ?? "rounded";
1989
+ const weight = state.icons?.weight ?? 400;
1990
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 12 }, children: [
1991
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(
1992
+ components.Select,
1993
+ {
1994
+ options: ICON_VARIANT_OPTIONS,
1995
+ value: variant,
1996
+ onValueChange: (v) => dispatch({
1997
+ type: "SET_ICON_VARIANT",
1998
+ variant: v
1999
+ }),
2000
+ label: "Variant"
2001
+ }
2002
+ ) }),
2003
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(
2004
+ components.Select,
2005
+ {
2006
+ options: ICON_WEIGHT_OPTIONS,
2007
+ value: weight.toString(),
2008
+ onValueChange: (v) => dispatch({
2009
+ type: "SET_ICON_WEIGHT",
2010
+ weight: parseInt(v)
2011
+ }),
2012
+ label: "Weight"
2013
+ }
2014
+ ) })
2015
+ ] });
2016
+ }
2017
+
2018
+ // ../../../newtone-fonts/dist/index.js
2019
+ var ROLE_DEFAULT_WEIGHTS = {
2020
+ headline: 700,
2021
+ title: 700,
2022
+ heading: 500,
2023
+ subheading: 500,
2024
+ body: 400,
2025
+ label: 500,
2026
+ caption: 400
2027
+ };
2028
+ var BREAKPOINT_ROLE_SCALE = {
2029
+ sm: { headline: 0.67, title: 0.75, heading: 0.82, subheading: 0.9, body: 0.94, label: 0.96, caption: 1 },
2030
+ md: { headline: 0.83, title: 0.88, heading: 0.92, subheading: 0.95, body: 0.97, label: 0.98, caption: 1 },
2031
+ lg: { headline: 1, title: 1, heading: 1, subheading: 1, body: 1, label: 1, caption: 1 }
2032
+ };
2033
+ function scaleRoleStep(step, scale) {
2034
+ return {
2035
+ fontSize: Math.round(step.fontSize * scale),
2036
+ lineHeight: Math.round(step.lineHeight * scale / 4) * 4
2037
+ };
2038
+ }
2039
+ var SYSTEM_FONTS = [
2040
+ { family: "system-ui", category: "sans-serif", fallback: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif' },
2041
+ { family: "ui-serif", category: "serif", fallback: '"Iowan Old Style", "Apple Garamond", Baskerville, Georgia, serif' },
2042
+ { family: "ui-monospace", category: "monospace", fallback: "SFMono-Regular, Menlo, Monaco, Consolas, monospace" }
2043
+ ];
2044
+ function scoreLineBreaks(lineWidths, containerWidth) {
2045
+ if (lineWidths.length <= 1) return 1;
2046
+ if (containerWidth <= 0) return 0;
2047
+ const lineCount = lineWidths.length;
2048
+ const lastLineWidth = lineWidths[lineCount - 1];
2049
+ const lastLineRatio = Math.max(0, Math.min(1, lastLineWidth / containerWidth));
2050
+ let widowScore;
2051
+ if (lastLineRatio >= 0.4 && lastLineRatio <= 0.8) {
2052
+ widowScore = 1;
2053
+ } else if (lastLineRatio < 0.4) {
2054
+ widowScore = lastLineRatio < 0.15 ? lastLineRatio / 0.15 * 0.3 : 0.3 + (lastLineRatio - 0.15) / 0.25 * 0.7;
2055
+ } else {
2056
+ widowScore = 1 - (lastLineRatio - 0.8) / 0.2 * 0.3;
2057
+ }
2058
+ let ragScore;
2059
+ if (lineCount <= 2) {
2060
+ ragScore = 0.8;
2061
+ } else {
2062
+ const bodyLines = lineWidths.slice(0, lineCount - 1);
2063
+ const mean = bodyLines.reduce((sum, w) => sum + w, 0) / bodyLines.length;
2064
+ if (mean <= 0) {
2065
+ ragScore = 0;
2066
+ } else {
2067
+ const variance = bodyLines.reduce((sum, w) => sum + (w - mean) ** 2, 0) / bodyLines.length;
2068
+ const stdDev = Math.sqrt(variance);
2069
+ const cv = stdDev / mean;
2070
+ ragScore = Math.max(0, 1 - cv / 0.15);
2071
+ }
2072
+ }
2073
+ const WIDOW_WEIGHT = 0.65;
2074
+ const RAG_WEIGHT = 0.35;
2075
+ return WIDOW_WEIGHT * widowScore + RAG_WEIGHT * ragScore;
2076
+ }
2077
+ function estimateLineWidths(characterCount, containerWidth, fontSize, avgCharWidthRatio = 0.55) {
2078
+ const avgCharWidth = fontSize * avgCharWidthRatio;
2079
+ const totalWidth = characterCount * avgCharWidth;
2080
+ if (totalWidth <= containerWidth) {
2081
+ return [totalWidth];
2082
+ }
2083
+ const charsPerLine = Math.max(1, Math.floor(containerWidth / avgCharWidth));
2084
+ const lines = [];
2085
+ let remaining = characterCount;
2086
+ while (remaining > 0) {
2087
+ const charsInLine = Math.min(remaining, charsPerLine);
2088
+ lines.push(charsInLine * avgCharWidth);
2089
+ remaining -= charsInLine;
2090
+ }
2091
+ return lines;
2092
+ }
2093
+ function resolveResponsiveSize(config, roleScales, measurement, calibrations) {
2094
+ const step = roleScales[config.role][config.size];
2095
+ if (!measurement) {
2096
+ return { fontSize: step.fontSize, lineHeight: step.lineHeight, wasScaled: false };
2097
+ }
2098
+ const ratio = config.fontFamily ? calibrations?.[config.fontFamily] ?? 0.55 : 0.55;
2099
+ const maxFs = config.maxFontSize ?? step.fontSize;
2100
+ const minFs = config.minFontSize ?? Math.max(8, Math.round(step.fontSize * 0.7));
2101
+ const lineHeightRatio = step.lineHeight / step.fontSize;
2102
+ const singleLineWidths = estimateLineWidths(measurement.characterCount, measurement.containerWidth, maxFs, ratio);
2103
+ if (singleLineWidths.length <= 1) {
2104
+ return {
2105
+ fontSize: maxFs,
2106
+ lineHeight: Math.round(maxFs * lineHeightRatio / 4) * 4,
2107
+ wasScaled: false
2108
+ };
2109
+ }
2110
+ let bestFontSize = minFs;
2111
+ let bestScore = -1;
2112
+ for (let fs = maxFs; fs >= minFs; fs--) {
2113
+ const lineWidths = estimateLineWidths(measurement.characterCount, measurement.containerWidth, fs, ratio);
2114
+ const score = scoreLineBreaks(lineWidths, measurement.containerWidth);
2115
+ if (score > bestScore) {
2116
+ bestScore = score;
2117
+ bestFontSize = fs;
2118
+ }
2119
+ if (score >= 0.95) break;
2120
+ }
2121
+ return {
2122
+ fontSize: bestFontSize,
2123
+ lineHeight: Math.round(bestFontSize * lineHeightRatio / 4) * 4,
2124
+ wasScaled: bestFontSize < maxFs
2125
+ };
2126
+ }
2127
+ var previewLoadedKey = "";
2128
+ function preloadFontsForPreview(catalog) {
2129
+ if (catalog.length === 0 || typeof document === "undefined") return;
2130
+ const key = catalog.map((f) => f.family).join(",");
2131
+ if (key === previewLoadedKey) return;
2132
+ previewLoadedKey = key;
2133
+ const families = catalog.map(
2134
+ (f) => `family=${f.family.replace(/ /g, "+")}:wght@400`
2135
+ ).join("&");
2136
+ const url = `https://fonts.googleapis.com/css2?${families}&display=swap`;
2137
+ const link = document.createElement("link");
2138
+ link.rel = "stylesheet";
2139
+ link.href = url;
2140
+ document.head.appendChild(link);
2141
+ }
2142
+ function googleFontToConfig(entry) {
2143
+ return {
2144
+ type: "google",
2145
+ family: entry.family,
2146
+ fallback: entry.fallback
2147
+ };
2148
+ }
2149
+ function systemFontToConfig(entry) {
2150
+ return {
2151
+ type: "system",
2152
+ family: entry.family,
2153
+ fallback: entry.fallback
2154
+ };
2155
+ }
2156
+ var CATEGORY_LABELS = {
2157
+ "sans-serif": "Sans Serif",
2158
+ serif: "Serif",
2159
+ monospace: "Monospace",
2160
+ display: "Display"
2161
+ };
2162
+ var CATEGORY_ORDER = [
2163
+ "sans-serif",
2164
+ "serif",
2165
+ "monospace",
2166
+ "display"
2167
+ ];
2168
+ function FontPicker({
2169
+ label,
2170
+ slot,
2171
+ currentFont,
2172
+ onSelect,
2173
+ fontCatalog = []
2174
+ }) {
2175
+ const tokens = components.useTokens();
2176
+ const [isOpen, setIsOpen] = react.useState(false);
2177
+ const [search, setSearch] = react.useState("");
2178
+ const containerRef = react.useRef(null);
2179
+ const searchInputRef = react.useRef(null);
2180
+ const labelColor = newtone.srgbToHex(tokens.textSecondary.srgb);
2181
+ const textColor = newtone.srgbToHex(tokens.textPrimary.srgb);
2182
+ const bgColor = newtone.srgbToHex(tokens.backgroundElevated.srgb);
2183
+ const borderColor = newtone.srgbToHex(tokens.border.srgb);
2184
+ const hoverColor = newtone.srgbToHex(tokens.backgroundSunken.srgb);
2185
+ const interactiveColor = newtone.srgbToHex(tokens.accent.fill.srgb);
2186
+ react.useEffect(() => {
2187
+ if (!isOpen) return;
2188
+ function handleMouseDown(e) {
2189
+ if (containerRef.current && !containerRef.current.contains(e.target)) {
2190
+ setIsOpen(false);
2191
+ setSearch("");
2192
+ }
2193
+ }
2194
+ document.addEventListener("mousedown", handleMouseDown);
2195
+ return () => document.removeEventListener("mousedown", handleMouseDown);
2196
+ }, [isOpen]);
2197
+ react.useEffect(() => {
2198
+ if (isOpen) {
2199
+ preloadFontsForPreview(fontCatalog);
2200
+ requestAnimationFrame(() => searchInputRef.current?.focus());
2201
+ }
2202
+ }, [isOpen, fontCatalog]);
2203
+ const filteredGoogleFonts = react.useMemo(() => {
2204
+ const query = search.toLowerCase().trim();
2205
+ let fonts = query ? fontCatalog.filter((f) => f.family.toLowerCase().includes(query)) : fontCatalog;
2206
+ if (slot === "mono") {
2207
+ fonts = fonts.filter((f) => f.category === "monospace");
2208
+ } else if (slot === "currency") {
2209
+ fonts = fonts.filter((f) => f.category === "monospace" || f.category === "sans-serif");
2210
+ }
2211
+ const grouped = {};
2212
+ for (const cat of CATEGORY_ORDER) {
2213
+ const inCategory = fonts.filter((f) => f.category === cat);
2214
+ if (inCategory.length > 0) {
2215
+ grouped[cat] = inCategory;
2216
+ }
2217
+ }
2218
+ return grouped;
2219
+ }, [search, slot, fontCatalog]);
2220
+ const filteredSystemFonts = react.useMemo(() => {
2221
+ const query = search.toLowerCase().trim();
2222
+ let fonts = query ? SYSTEM_FONTS.filter((f) => f.family.toLowerCase().includes(query)) : [...SYSTEM_FONTS];
2223
+ if (slot === "mono") {
2224
+ fonts = fonts.filter((f) => f.category === "monospace");
2225
+ } else if (slot === "currency") {
2226
+ fonts = fonts.filter((f) => f.category !== "serif");
2227
+ }
2228
+ return fonts;
2229
+ }, [search, slot]);
2230
+ const handleSelect = react.useCallback(
2231
+ (font) => {
2232
+ onSelect(font);
2233
+ setIsOpen(false);
2234
+ setSearch("");
2235
+ },
2236
+ [onSelect]
2237
+ );
2238
+ const fontFamily = currentFont.family.includes(" ") ? `"${currentFont.family}"` : currentFont.family;
2239
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: containerRef, style: { position: "relative" }, children: [
2240
+ /* @__PURE__ */ jsxRuntime.jsxs(
2241
+ "button",
2242
+ {
2243
+ type: "button",
2244
+ onClick: () => setIsOpen(!isOpen),
2245
+ style: {
2246
+ width: "100%",
2247
+ display: "flex",
2248
+ justifyContent: "space-between",
2249
+ alignItems: "center",
2250
+ padding: "6px 10px",
2251
+ borderRadius: 6,
2252
+ border: `1px solid ${isOpen ? interactiveColor : borderColor}`,
2253
+ background: "transparent",
2254
+ cursor: "pointer",
2255
+ outline: "none"
2256
+ },
2257
+ children: [
2258
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontSize: 12, color: labelColor }, children: label }),
2259
+ /* @__PURE__ */ jsxRuntime.jsx(
2260
+ "span",
2294
2261
  {
2295
- onClick: handleCreate,
2296
- onMouseEnter: () => setHoveredAction("create"),
2297
- onMouseLeave: () => setHoveredAction(null),
2298
2262
  style: {
2299
- display: "flex",
2300
- alignItems: "center",
2301
- gap: 8,
2302
- width: "100%",
2303
- padding: "10px 12px",
2304
- border: "none",
2305
- borderTop: `1px solid ${borderColor}`,
2306
- backgroundColor: hoveredAction === "create" ? hoverBg : "transparent",
2307
- color: textSecondary,
2308
- fontSize: 13,
2309
- cursor: "pointer"
2263
+ fontSize: 12,
2264
+ color: textColor,
2265
+ fontFamily: `${fontFamily}, ${currentFont.fallback}`,
2266
+ maxWidth: 140,
2267
+ overflow: "hidden",
2268
+ textOverflow: "ellipsis",
2269
+ whiteSpace: "nowrap"
2310
2270
  },
2311
- children: [
2312
- /* @__PURE__ */ jsxRuntime.jsx(components.Icon, { name: "add", size: 14, color: textSecondary }),
2313
- "New preset"
2314
- ]
2271
+ children: currentFont.family
2315
2272
  }
2316
2273
  )
2317
2274
  ]
2318
2275
  }
2319
- )
2320
- ] });
2321
- }
2322
- var SIDEBAR_WIDTH2 = 360;
2323
- var ACCORDION_SECTIONS = [
2324
- { id: "dynamic-range", label: "Dynamic Range", icon: "contrast" },
2325
- { id: "colors", label: "Colors", icon: "palette" },
2326
- { id: "fonts", label: "Fonts", icon: "text_fields" },
2327
- { id: "icons", label: "Icons", icon: "grid_view" },
2328
- { id: "others", label: "Others", icon: "tune" }
2329
- ];
2330
- function Sidebar({
2331
- state,
2332
- dispatch,
2333
- previewColors,
2334
- isDirty,
2335
- onRevert,
2336
- presets,
2337
- activePresetId,
2338
- publishedPresetId,
2339
- onSwitchPreset,
2340
- onCreatePreset,
2341
- onRenamePreset,
2342
- onDeletePreset,
2343
- onDuplicatePreset,
2344
- colorMode,
2345
- onColorModeChange
2346
- }) {
2347
- const tokens = components.useTokens();
2348
- const [openSections, setOpenSections] = react.useState(
2349
- /* @__PURE__ */ new Set(["dynamic-range", "colors"])
2350
- );
2351
- const [hoveredSectionId, setHoveredSectionId] = react.useState(null);
2352
- const borderColor = newtone.srgbToHex(tokens.border.srgb);
2353
- const bgColor = newtone.srgbToHex(tokens.background.srgb);
2354
- const hoverBg = `${borderColor}10`;
2355
- const toggleSection = (id) => {
2356
- setOpenSections((prev) => {
2357
- const next = new Set(prev);
2358
- if (next.has(id)) next.delete(id);
2359
- else next.add(id);
2360
- return next;
2361
- });
2362
- };
2363
- const renderSectionContent = (sectionId) => {
2364
- switch (sectionId) {
2365
- case "dynamic-range":
2366
- return /* @__PURE__ */ jsxRuntime.jsx(DynamicRangeSection, { state, dispatch });
2367
- case "colors":
2368
- return /* @__PURE__ */ jsxRuntime.jsx(
2369
- ColorsSection,
2370
- {
2371
- state,
2372
- dispatch,
2373
- previewColors,
2374
- colorMode,
2375
- onColorModeChange
2376
- }
2377
- );
2378
- case "icons":
2379
- return /* @__PURE__ */ jsxRuntime.jsx(IconsSection, { state, dispatch });
2380
- case "fonts":
2381
- return /* @__PURE__ */ jsxRuntime.jsx(FontsSection, { state, dispatch });
2382
- case "others":
2383
- return /* @__PURE__ */ jsxRuntime.jsx(OthersSection, { state, dispatch });
2384
- default:
2385
- return null;
2386
- }
2387
- };
2388
- return /* @__PURE__ */ jsxRuntime.jsxs(
2389
- "div",
2390
- {
2391
- style: {
2392
- width: SIDEBAR_WIDTH2,
2393
- flexShrink: 0,
2394
- display: "flex",
2395
- flexDirection: "column",
2396
- height: "100vh",
2397
- borderLeft: `1px solid ${borderColor}`,
2398
- backgroundColor: bgColor
2399
- },
2400
- children: [
2401
- /* @__PURE__ */ jsxRuntime.jsxs(
2402
- "div",
2403
- {
2404
- style: {
2405
- flexShrink: 0,
2406
- padding: "16px 20px",
2407
- borderBottom: `1px solid ${borderColor}`,
2408
- display: "flex",
2409
- alignItems: "center",
2410
- justifyContent: "space-between"
2411
- },
2412
- children: [
2276
+ ),
2277
+ isOpen && /* @__PURE__ */ jsxRuntime.jsxs(
2278
+ "div",
2279
+ {
2280
+ style: {
2281
+ position: "absolute",
2282
+ top: "calc(100% + 4px)",
2283
+ left: 0,
2284
+ right: 0,
2285
+ zIndex: 100,
2286
+ background: bgColor,
2287
+ border: `1px solid ${borderColor}`,
2288
+ borderRadius: 8,
2289
+ boxShadow: "0 4px 16px rgba(0,0,0,0.15)",
2290
+ maxHeight: 320,
2291
+ display: "flex",
2292
+ flexDirection: "column",
2293
+ overflow: "hidden"
2294
+ },
2295
+ children: [
2296
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "8px 8px 4px" }, children: /* @__PURE__ */ jsxRuntime.jsx(
2297
+ "input",
2298
+ {
2299
+ ref: searchInputRef,
2300
+ type: "text",
2301
+ value: search,
2302
+ onChange: (e) => setSearch(e.target.value),
2303
+ placeholder: "Search fonts...",
2304
+ style: {
2305
+ width: "100%",
2306
+ padding: "6px 8px",
2307
+ fontSize: 12,
2308
+ borderRadius: 4,
2309
+ border: `1px solid ${borderColor}`,
2310
+ background: "transparent",
2311
+ color: textColor,
2312
+ outline: "none",
2313
+ boxSizing: "border-box"
2314
+ }
2315
+ }
2316
+ ) }),
2317
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { overflowY: "auto", padding: "4px 0" }, children: [
2318
+ filteredSystemFonts.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2319
+ /* @__PURE__ */ jsxRuntime.jsx(
2320
+ "div",
2321
+ {
2322
+ style: {
2323
+ fontSize: 10,
2324
+ fontWeight: 600,
2325
+ color: labelColor,
2326
+ textTransform: "uppercase",
2327
+ letterSpacing: 0.5,
2328
+ padding: "6px 12px 2px"
2329
+ },
2330
+ children: "System"
2331
+ }
2332
+ ),
2333
+ filteredSystemFonts.map((f) => /* @__PURE__ */ jsxRuntime.jsx(
2334
+ FontOption,
2335
+ {
2336
+ family: f.family,
2337
+ fallback: f.fallback,
2338
+ isSelected: currentFont.family === f.family && currentFont.type === "system",
2339
+ textColor,
2340
+ hoverColor,
2341
+ interactiveColor,
2342
+ onSelect: () => handleSelect(systemFontToConfig(f))
2343
+ },
2344
+ f.family
2345
+ ))
2346
+ ] }),
2347
+ Object.entries(filteredGoogleFonts).map(([category, fonts]) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2413
2348
  /* @__PURE__ */ jsxRuntime.jsx(
2414
- "span",
2349
+ "div",
2415
2350
  {
2416
2351
  style: {
2417
- fontSize: 16,
2418
- fontWeight: 700,
2419
- color: newtone.srgbToHex(tokens.textPrimary.srgb)
2352
+ fontSize: 10,
2353
+ fontWeight: 600,
2354
+ color: labelColor,
2355
+ textTransform: "uppercase",
2356
+ letterSpacing: 0.5,
2357
+ padding: "8px 12px 2px"
2420
2358
  },
2421
- children: "newtone"
2359
+ children: CATEGORY_LABELS[category] ?? category
2422
2360
  }
2423
2361
  ),
2424
- /* @__PURE__ */ jsxRuntime.jsx(
2425
- PresetSelector,
2362
+ fonts.map((f) => /* @__PURE__ */ jsxRuntime.jsx(
2363
+ FontOption,
2426
2364
  {
2427
- presets,
2428
- activePresetId,
2429
- publishedPresetId,
2430
- onSwitchPreset,
2431
- onCreatePreset,
2432
- onRenamePreset,
2433
- onDeletePreset,
2434
- onDuplicatePreset
2435
- }
2436
- )
2437
- ]
2438
- }
2439
- ),
2440
- /* @__PURE__ */ jsxRuntime.jsx(
2441
- "div",
2442
- {
2443
- style: {
2444
- flex: 1,
2445
- overflowY: "auto",
2446
- overflowX: "hidden"
2447
- },
2448
- children: ACCORDION_SECTIONS.map((section) => {
2449
- const isOpen = openSections.has(section.id);
2450
- const isHovered = hoveredSectionId === section.id;
2451
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2452
- /* @__PURE__ */ jsxRuntime.jsxs(
2453
- "button",
2454
- {
2455
- onClick: () => toggleSection(section.id),
2456
- onMouseEnter: () => setHoveredSectionId(section.id),
2457
- onMouseLeave: () => setHoveredSectionId(null),
2458
- "aria-expanded": isOpen,
2459
- "aria-controls": `section-${section.id}`,
2460
- style: {
2461
- display: "flex",
2462
- alignItems: "center",
2463
- justifyContent: "space-between",
2464
- width: "100%",
2465
- padding: "12px 20px",
2466
- border: "none",
2467
- borderBottom: `1px solid ${borderColor}`,
2468
- background: isHovered ? hoverBg : "none",
2469
- cursor: "pointer",
2470
- fontSize: 14,
2471
- fontWeight: 500,
2472
- color: newtone.srgbToHex(tokens.textPrimary.srgb),
2473
- transition: "background-color 100ms ease"
2474
- },
2475
- children: [
2476
- /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [
2477
- /* @__PURE__ */ jsxRuntime.jsx(components.Icon, { name: section.icon, size: 16 }),
2478
- section.label
2479
- ] }),
2480
- /* @__PURE__ */ jsxRuntime.jsx(
2481
- components.Icon,
2482
- {
2483
- name: "expand_more",
2484
- size: 16,
2485
- style: {
2486
- transform: isOpen ? "rotate(180deg)" : "none",
2487
- transition: "transform 150ms ease"
2488
- }
2489
- }
2490
- )
2491
- ]
2492
- }
2493
- ),
2494
- isOpen && /* @__PURE__ */ jsxRuntime.jsx(
2495
- "div",
2496
- {
2497
- id: `section-${section.id}`,
2498
- role: "region",
2499
- "aria-label": section.label,
2500
- style: {
2501
- padding: "16px 20px",
2502
- borderBottom: `1px solid ${borderColor}`
2503
- },
2504
- children: renderSectionContent(section.id)
2505
- }
2506
- )
2507
- ] }, section.id);
2508
- })
2509
- }
2510
- ),
2511
- /* @__PURE__ */ jsxRuntime.jsx(
2512
- "div",
2513
- {
2514
- style: {
2515
- flexShrink: 0,
2516
- padding: "12px 20px",
2517
- borderTop: `1px solid ${borderColor}`
2518
- },
2519
- children: /* @__PURE__ */ jsxRuntime.jsx(
2520
- "button",
2365
+ family: f.family,
2366
+ fallback: f.fallback,
2367
+ isSelected: currentFont.family === f.family && currentFont.type === "google",
2368
+ textColor,
2369
+ hoverColor,
2370
+ interactiveColor,
2371
+ onSelect: () => handleSelect(googleFontToConfig(f))
2372
+ },
2373
+ f.family
2374
+ ))
2375
+ ] }, category)),
2376
+ filteredSystemFonts.length === 0 && Object.keys(filteredGoogleFonts).length === 0 && /* @__PURE__ */ jsxRuntime.jsx(
2377
+ "div",
2521
2378
  {
2522
- disabled: !isDirty,
2523
- onClick: onRevert,
2524
- "aria-label": "Revert all changes to the last saved version",
2525
2379
  style: {
2526
- width: "100%",
2527
- padding: "8px 16px",
2528
- borderRadius: 6,
2529
- border: `1px solid ${borderColor}`,
2530
- backgroundColor: "transparent",
2531
- color: isDirty ? newtone.srgbToHex(tokens.textPrimary.srgb) : newtone.srgbToHex(tokens.textSecondary.srgb),
2532
- fontSize: 13,
2533
- cursor: isDirty ? "pointer" : "not-allowed",
2534
- opacity: isDirty ? 1 : 0.5
2380
+ padding: "12px",
2381
+ fontSize: 12,
2382
+ color: labelColor,
2383
+ textAlign: "center"
2535
2384
  },
2536
- children: "Revert Changes"
2385
+ children: "No fonts found"
2537
2386
  }
2538
2387
  )
2539
- }
2540
- )
2541
- ]
2388
+ ] })
2389
+ ]
2390
+ }
2391
+ )
2392
+ ] });
2393
+ }
2394
+ function FontOption({
2395
+ family,
2396
+ fallback,
2397
+ isSelected,
2398
+ textColor,
2399
+ hoverColor,
2400
+ interactiveColor,
2401
+ onSelect
2402
+ }) {
2403
+ const [hovered, setHovered] = react.useState(false);
2404
+ const fontFamily = family.includes(" ") ? `"${family}"` : family;
2405
+ return /* @__PURE__ */ jsxRuntime.jsx(
2406
+ "button",
2407
+ {
2408
+ type: "button",
2409
+ onClick: onSelect,
2410
+ onMouseEnter: () => setHovered(true),
2411
+ onMouseLeave: () => setHovered(false),
2412
+ style: {
2413
+ display: "block",
2414
+ width: "100%",
2415
+ padding: "5px 12px",
2416
+ fontSize: 13,
2417
+ fontFamily: `${fontFamily}, ${fallback}`,
2418
+ color: isSelected ? interactiveColor : textColor,
2419
+ background: hovered ? hoverColor : "transparent",
2420
+ border: "none",
2421
+ cursor: "pointer",
2422
+ textAlign: "left",
2423
+ outline: "none",
2424
+ fontWeight: isSelected ? 600 : 400
2425
+ },
2426
+ children: family
2542
2427
  }
2543
2428
  );
2544
2429
  }
2545
- var STATUS_LABEL = {
2546
- saved: "Saved",
2547
- saving: "Saving...",
2548
- unsaved: "Unsaved changes",
2549
- error: "Save failed"
2550
- };
2551
- function EditorHeader({
2552
- saveStatus,
2553
- isPublished,
2554
- publishing,
2555
- onPublish,
2556
- onRetry,
2557
- headerSlots
2430
+ var DEFAULT_FONT_SYSTEM = {
2431
+ type: "system",
2432
+ family: "system-ui",
2433
+ fallback: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
2434
+ };
2435
+ var DEFAULT_FONT_MONO = {
2436
+ type: "system",
2437
+ family: "ui-monospace",
2438
+ fallback: "SFMono-Regular, Menlo, Monaco, Consolas, monospace"
2439
+ };
2440
+ function getDefaultFontConfig(scope) {
2441
+ return scope === "mono" ? DEFAULT_FONT_MONO : DEFAULT_FONT_SYSTEM;
2442
+ }
2443
+ function getCurrentFontConfig(state, scope) {
2444
+ return state.typography?.fonts[scope]?.config ?? getDefaultFontConfig(scope);
2445
+ }
2446
+ var FONT_SCOPES = [
2447
+ { scope: "main", label: "Main", slot: "default" },
2448
+ { scope: "display", label: "Display", slot: "display" },
2449
+ { scope: "mono", label: "Mono", slot: "mono" },
2450
+ { scope: "currency", label: "Currency", slot: "currency" }
2451
+ ];
2452
+ function FontsSection({ state, dispatch, fontCatalog }) {
2453
+ const tokens = components.useTokens();
2454
+ const labelColor = newtone.srgbToHex(tokens.textSecondary.srgb);
2455
+ const handleFontChange = (scope, font) => {
2456
+ const weights = state.typography?.fonts[scope]?.weights ?? { regular: 400, medium: 500, bold: 700 };
2457
+ const slotConfig = { config: font, weights };
2458
+ dispatch({ type: "SET_FONT", scope, font: slotConfig });
2459
+ };
2460
+ const sectionLabelStyle = {
2461
+ fontSize: 11,
2462
+ fontWeight: 600,
2463
+ color: labelColor,
2464
+ textTransform: "uppercase",
2465
+ letterSpacing: 0.5,
2466
+ marginBottom: 8
2467
+ };
2468
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 16 }, children: [
2469
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2470
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: sectionLabelStyle, children: "Fonts" }),
2471
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: FONT_SCOPES.map(({ scope, label, slot }) => /* @__PURE__ */ jsxRuntime.jsx(
2472
+ FontPicker,
2473
+ {
2474
+ label,
2475
+ slot,
2476
+ currentFont: getCurrentFontConfig(state, scope),
2477
+ onSelect: (font) => handleFontChange(scope, font),
2478
+ fontCatalog
2479
+ },
2480
+ scope
2481
+ )) })
2482
+ ] }),
2483
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2484
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: sectionLabelStyle, children: "Type Scale" }),
2485
+ /* @__PURE__ */ jsxRuntime.jsx(
2486
+ components.Slider,
2487
+ {
2488
+ value: Math.round((state.typography?.typeScaleOffset ?? 0.5) * 100),
2489
+ onValueChange: (v) => dispatch({ type: "SET_TYPE_SCALE_OFFSET", offset: v / 100 }),
2490
+ min: 0,
2491
+ max: 100,
2492
+ label: "Scale",
2493
+ showValue: true
2494
+ }
2495
+ )
2496
+ ] })
2497
+ ] });
2498
+ }
2499
+ function OthersSection({ state, dispatch }) {
2500
+ const tokens = components.useTokens();
2501
+ const spacingPreset = state.spacing?.preset ?? "md";
2502
+ const intensity = state.roundness?.intensity ?? 0.5;
2503
+ const spacingOptions = [
2504
+ { value: "xs", label: "Extra Small" },
2505
+ { value: "sm", label: "Small" },
2506
+ { value: "md", label: "Medium" },
2507
+ { value: "lg", label: "Large" },
2508
+ { value: "xl", label: "Extra Large" }
2509
+ ];
2510
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 16 }, children: [
2511
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2512
+ /* @__PURE__ */ jsxRuntime.jsx(
2513
+ "div",
2514
+ {
2515
+ style: {
2516
+ fontSize: 11,
2517
+ fontWeight: 600,
2518
+ color: newtone.srgbToHex(tokens.textSecondary.srgb),
2519
+ textTransform: "uppercase",
2520
+ letterSpacing: 0.5,
2521
+ marginBottom: 8
2522
+ },
2523
+ children: "Spacing"
2524
+ }
2525
+ ),
2526
+ /* @__PURE__ */ jsxRuntime.jsx(
2527
+ components.Select,
2528
+ {
2529
+ value: spacingPreset,
2530
+ onValueChange: (preset) => dispatch({ type: "SET_SPACING_PRESET", preset }),
2531
+ options: spacingOptions,
2532
+ label: "Preset"
2533
+ }
2534
+ )
2535
+ ] }),
2536
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2537
+ /* @__PURE__ */ jsxRuntime.jsx(
2538
+ "div",
2539
+ {
2540
+ style: {
2541
+ fontSize: 11,
2542
+ fontWeight: 600,
2543
+ color: newtone.srgbToHex(tokens.textSecondary.srgb),
2544
+ textTransform: "uppercase",
2545
+ letterSpacing: 0.5,
2546
+ marginBottom: 8
2547
+ },
2548
+ children: "Roundness"
2549
+ }
2550
+ ),
2551
+ /* @__PURE__ */ jsxRuntime.jsx(
2552
+ components.Slider,
2553
+ {
2554
+ value: Math.round(intensity * 100),
2555
+ onValueChange: (v) => dispatch({ type: "SET_ROUNDNESS_INTENSITY", intensity: v / 100 }),
2556
+ min: 0,
2557
+ max: 100,
2558
+ label: "Intensity",
2559
+ showValue: true
2560
+ }
2561
+ )
2562
+ ] })
2563
+ ] });
2564
+ }
2565
+ var PANEL_WIDTH = 280;
2566
+ function ConfiguratorPanel({
2567
+ activeSectionId,
2568
+ state,
2569
+ dispatch,
2570
+ previewColors,
2571
+ colorMode,
2572
+ onColorModeChange,
2573
+ fontCatalog
2558
2574
  }) {
2559
2575
  const tokens = components.useTokens();
2560
2576
  const borderColor = newtone.srgbToHex(tokens.border.srgb);
2561
- const statusColor = {
2562
- saved: newtone.srgbToHex(tokens.success.fill.srgb),
2563
- saving: newtone.srgbToHex(tokens.warning.fill.srgb),
2564
- unsaved: newtone.srgbToHex(tokens.textSecondary.srgb),
2565
- error: newtone.srgbToHex(tokens.error.fill.srgb)
2566
- };
2567
2577
  return /* @__PURE__ */ jsxRuntime.jsxs(
2568
2578
  "div",
2569
2579
  {
2570
2580
  style: {
2571
- display: "flex",
2572
- alignItems: "center",
2573
- justifyContent: "space-between",
2574
- padding: "12px 24px",
2575
- borderBottom: `1px solid ${borderColor}`,
2581
+ width: PANEL_WIDTH,
2582
+ flexShrink: 0,
2583
+ overflowY: "auto",
2584
+ borderRight: `1px solid ${borderColor}`,
2585
+ padding: 20,
2576
2586
  backgroundColor: newtone.srgbToHex(tokens.background.srgb),
2577
- flexShrink: 0
2587
+ display: "flex",
2588
+ flexDirection: "column",
2589
+ gap: 24
2578
2590
  },
2579
2591
  children: [
2580
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", alignItems: "center", gap: 16 }, children: headerSlots?.left }),
2581
- /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", gap: 12 }, children: [
2582
- /* @__PURE__ */ jsxRuntime.jsx(
2583
- "span",
2584
- {
2585
- style: {
2586
- fontSize: 12,
2587
- color: statusColor[saveStatus],
2588
- fontWeight: 500
2589
- },
2590
- children: STATUS_LABEL[saveStatus]
2591
- }
2592
- ),
2593
- saveStatus === "error" && /* @__PURE__ */ jsxRuntime.jsx(components.Button, { variant: "tertiary", semantic: "neutral", size: "sm", icon: "refresh", onPress: onRetry, children: "Retry" }),
2592
+ activeSectionId === "colors" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2593
+ /* @__PURE__ */ jsxRuntime.jsx(DynamicRangeSection, { state, dispatch }),
2594
2594
  /* @__PURE__ */ jsxRuntime.jsx(
2595
- components.Button,
2595
+ ColorsSection,
2596
2596
  {
2597
- variant: "primary",
2598
- size: "sm",
2599
- icon: "publish",
2600
- onPress: onPublish,
2601
- disabled: isPublished || publishing,
2602
- children: publishing ? "Publishing..." : isPublished ? "Published" : "Publish"
2597
+ state,
2598
+ dispatch,
2599
+ previewColors,
2600
+ colorMode,
2601
+ onColorModeChange
2603
2602
  }
2604
- ),
2605
- headerSlots?.right
2606
- ] })
2603
+ )
2604
+ ] }),
2605
+ activeSectionId === "typography" && /* @__PURE__ */ jsxRuntime.jsx(FontsSection, { state, dispatch, fontCatalog }),
2606
+ activeSectionId === "symbols" && /* @__PURE__ */ jsxRuntime.jsx(IconsSection, { state, dispatch }),
2607
+ activeSectionId === "layout" && /* @__PURE__ */ jsxRuntime.jsx(OthersSection, { state, dispatch })
2607
2608
  ]
2608
2609
  }
2609
2610
  );
2610
2611
  }
2611
2612
  var TOC_WIDTH = 220;
2612
2613
  function TableOfContents({
2614
+ activeSectionId,
2613
2615
  activeView,
2614
2616
  selectedComponentId,
2615
2617
  onNavigate
@@ -2619,9 +2621,9 @@ function TableOfContents({
2619
2621
  const borderColor = newtone.srgbToHex(tokens.border.srgb);
2620
2622
  const activeColor = newtone.srgbToHex(tokens.accent.fill.srgb);
2621
2623
  const textPrimary = newtone.srgbToHex(tokens.textPrimary.srgb);
2622
- const textSecondary = newtone.srgbToHex(tokens.textSecondary.srgb);
2623
2624
  const hoverBg = `${borderColor}20`;
2624
- const isOverviewActive = activeView.kind === "overview";
2625
+ const components$1 = components.getComponentsByCategory(activeSectionId);
2626
+ const isOverviewActive = activeView.kind === "overview" || activeView.kind === "category" && activeView.categoryId === activeSectionId;
2625
2627
  return /* @__PURE__ */ jsxRuntime.jsxs(
2626
2628
  "nav",
2627
2629
  {
@@ -2638,7 +2640,7 @@ function TableOfContents({
2638
2640
  /* @__PURE__ */ jsxRuntime.jsx(
2639
2641
  "button",
2640
2642
  {
2641
- onClick: () => onNavigate({ kind: "overview" }),
2643
+ onClick: () => onNavigate({ kind: "category", categoryId: activeSectionId }),
2642
2644
  onMouseEnter: () => setHoveredId("overview"),
2643
2645
  onMouseLeave: () => setHoveredId(null),
2644
2646
  "aria-current": isOverviewActive ? "page" : void 0,
@@ -2658,63 +2660,32 @@ function TableOfContents({
2658
2660
  children: "Overview"
2659
2661
  }
2660
2662
  ),
2661
- components.CATEGORIES.map((category) => {
2662
- const components$1 = components.getComponentsByCategory(category.id);
2663
- const isCategoryActive = activeView.kind === "category" && activeView.categoryId === category.id;
2664
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginTop: 16 }, children: [
2665
- /* @__PURE__ */ jsxRuntime.jsx(
2666
- "button",
2667
- {
2668
- onClick: () => onNavigate({ kind: "category", categoryId: category.id }),
2669
- onMouseEnter: () => setHoveredId(`cat-${category.id}`),
2670
- onMouseLeave: () => setHoveredId(null),
2671
- "aria-current": isCategoryActive ? "page" : void 0,
2672
- style: {
2673
- display: "block",
2674
- width: "100%",
2675
- padding: "6px 20px",
2676
- border: "none",
2677
- background: isCategoryActive ? `${activeColor}14` : hoveredId === `cat-${category.id}` ? hoverBg : "none",
2678
- cursor: "pointer",
2679
- textAlign: "left",
2680
- fontSize: 11,
2681
- fontWeight: 600,
2682
- color: isCategoryActive ? activeColor : textSecondary,
2683
- textTransform: "uppercase",
2684
- letterSpacing: 0.5,
2685
- transition: "background-color 100ms ease"
2686
- },
2687
- children: category.name
2688
- }
2689
- ),
2690
- components$1.map((comp) => {
2691
- const isComponentActive = activeView.kind === "component" && activeView.componentId === comp.id || selectedComponentId === comp.id;
2692
- return /* @__PURE__ */ jsxRuntime.jsx(
2693
- "button",
2694
- {
2695
- onClick: () => onNavigate({ kind: "component", componentId: comp.id }),
2696
- onMouseEnter: () => setHoveredId(comp.id),
2697
- onMouseLeave: () => setHoveredId(null),
2698
- "aria-current": isComponentActive ? "page" : void 0,
2699
- style: {
2700
- display: "block",
2701
- width: "100%",
2702
- padding: "4px 20px 4px 32px",
2703
- border: "none",
2704
- background: isComponentActive ? `${activeColor}14` : hoveredId === comp.id ? hoverBg : "none",
2705
- cursor: "pointer",
2706
- textAlign: "left",
2707
- fontSize: 13,
2708
- fontWeight: isComponentActive ? 600 : 400,
2709
- color: isComponentActive ? activeColor : textPrimary,
2710
- transition: "background-color 100ms ease"
2711
- },
2712
- children: comp.name
2713
- },
2714
- comp.id
2715
- );
2716
- })
2717
- ] }, category.id);
2663
+ components$1.map((comp) => {
2664
+ const isComponentActive = activeView.kind === "component" && activeView.componentId === comp.id || selectedComponentId === comp.id;
2665
+ return /* @__PURE__ */ jsxRuntime.jsx(
2666
+ "button",
2667
+ {
2668
+ onClick: () => onNavigate({ kind: "component", componentId: comp.id }),
2669
+ onMouseEnter: () => setHoveredId(comp.id),
2670
+ onMouseLeave: () => setHoveredId(null),
2671
+ "aria-current": isComponentActive ? "page" : void 0,
2672
+ style: {
2673
+ display: "block",
2674
+ width: "100%",
2675
+ padding: "4px 20px",
2676
+ border: "none",
2677
+ background: isComponentActive ? `${activeColor}14` : hoveredId === comp.id ? hoverBg : "none",
2678
+ cursor: "pointer",
2679
+ textAlign: "left",
2680
+ fontSize: 13,
2681
+ fontWeight: isComponentActive ? 600 : 400,
2682
+ color: isComponentActive ? activeColor : textPrimary,
2683
+ transition: "background-color 100ms ease"
2684
+ },
2685
+ children: comp.name
2686
+ },
2687
+ comp.id
2688
+ );
2718
2689
  })
2719
2690
  ]
2720
2691
  }
@@ -2782,7 +2753,7 @@ function WrapperPreview(props) {
2782
2753
  /* @__PURE__ */ jsxRuntime.jsx(components.Text, { size: "sm", children: "Item 3" })
2783
2754
  ] });
2784
2755
  }
2785
- function ComponentRenderer({ componentId, props }) {
2756
+ function ComponentRenderer({ componentId, props, previewText }) {
2786
2757
  const noop = react.useCallback(() => {
2787
2758
  }, []);
2788
2759
  switch (componentId) {
@@ -2818,11 +2789,12 @@ function ComponentRenderer({ componentId, props }) {
2818
2789
  return /* @__PURE__ */ jsxRuntime.jsx(
2819
2790
  components.Text,
2820
2791
  {
2792
+ scope: props.scope,
2793
+ role: props.role,
2821
2794
  size: props.size,
2822
- weight: props.weight,
2823
2795
  color: props.color,
2824
- font: props.font,
2825
- children: "The quick brown fox"
2796
+ responsive: true,
2797
+ children: previewText || "The quick brown fox"
2826
2798
  }
2827
2799
  );
2828
2800
  case "icon":
@@ -3323,16 +3295,275 @@ function IconBrowserView({
3323
3295
  }
3324
3296
  );
3325
3297
  }
3298
+ var ROLE_VARIANT_IDS = /* @__PURE__ */ new Set([
3299
+ "body",
3300
+ "headline",
3301
+ "title",
3302
+ "heading",
3303
+ "subheading",
3304
+ "label",
3305
+ "caption"
3306
+ ]);
3307
+ function getWeightControlType(family, fontCatalog) {
3308
+ if (!family) return { type: "none" };
3309
+ const entry = fontCatalog?.find((f) => f.family === family);
3310
+ if (entry) {
3311
+ if (entry.isVariable) {
3312
+ return {
3313
+ type: "slider",
3314
+ min: entry.weightAxisRange?.min ?? 100,
3315
+ max: entry.weightAxisRange?.max ?? 900
3316
+ };
3317
+ }
3318
+ if (entry.availableWeights && entry.availableWeights.length > 1) {
3319
+ const sorted = [...entry.availableWeights].sort((a, b) => a - b);
3320
+ return { type: "slider", min: sorted[0], max: sorted[sorted.length - 1], stops: sorted };
3321
+ }
3322
+ return { type: "none" };
3323
+ }
3324
+ return { type: "slider", min: 100, max: 900 };
3325
+ }
3326
+ function WeightSlider({
3327
+ value,
3328
+ min,
3329
+ max,
3330
+ stops,
3331
+ onChange,
3332
+ textColor,
3333
+ accentColor
3334
+ }) {
3335
+ const snap = react.useCallback(
3336
+ (v) => {
3337
+ if (!stops) return v;
3338
+ return stops.reduce(
3339
+ (prev, curr) => Math.abs(curr - v) < Math.abs(prev - v) ? curr : prev
3340
+ );
3341
+ },
3342
+ [stops]
3343
+ );
3344
+ const handleChange = react.useCallback(
3345
+ (e) => {
3346
+ onChange(snap(Number(e.target.value)));
3347
+ },
3348
+ [onChange, snap]
3349
+ );
3350
+ const range = max - min;
3351
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3352
+ "div",
3353
+ {
3354
+ style: {
3355
+ display: "flex",
3356
+ alignItems: "center",
3357
+ gap: 8,
3358
+ flexShrink: 0
3359
+ },
3360
+ onClick: (e) => e.stopPropagation(),
3361
+ children: [
3362
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { position: "relative", width: 80 }, children: [
3363
+ /* @__PURE__ */ jsxRuntime.jsx(
3364
+ "input",
3365
+ {
3366
+ type: "range",
3367
+ min,
3368
+ max,
3369
+ step: 1,
3370
+ value,
3371
+ onChange: handleChange,
3372
+ style: {
3373
+ width: 80,
3374
+ accentColor,
3375
+ cursor: "pointer",
3376
+ display: "block"
3377
+ }
3378
+ }
3379
+ ),
3380
+ stops && range > 0 && /* @__PURE__ */ jsxRuntime.jsx(
3381
+ "div",
3382
+ {
3383
+ style: {
3384
+ position: "relative",
3385
+ width: 80,
3386
+ height: 4,
3387
+ marginTop: -2,
3388
+ pointerEvents: "none"
3389
+ },
3390
+ children: stops.map((s) => /* @__PURE__ */ jsxRuntime.jsx(
3391
+ "div",
3392
+ {
3393
+ style: {
3394
+ position: "absolute",
3395
+ left: `${(s - min) / range * 100}%`,
3396
+ width: 2,
3397
+ height: 4,
3398
+ backgroundColor: s === value ? accentColor : textColor,
3399
+ opacity: s === value ? 1 : 0.3,
3400
+ borderRadius: 1,
3401
+ transform: "translateX(-50%)"
3402
+ }
3403
+ },
3404
+ s
3405
+ ))
3406
+ }
3407
+ )
3408
+ ] }),
3409
+ /* @__PURE__ */ jsxRuntime.jsx(
3410
+ "span",
3411
+ {
3412
+ style: {
3413
+ fontSize: 11,
3414
+ fontWeight: 500,
3415
+ color: textColor,
3416
+ width: 28,
3417
+ textAlign: "right",
3418
+ fontVariantNumeric: "tabular-nums"
3419
+ },
3420
+ children: value
3421
+ }
3422
+ )
3423
+ ]
3424
+ }
3425
+ );
3426
+ }
3427
+ var PREVIEW_TEXT = "The quick brown fox";
3428
+ function TextAnnotation({
3429
+ role,
3430
+ roleScales,
3431
+ fontFamily,
3432
+ calibrations,
3433
+ weight,
3434
+ textColor,
3435
+ accentColor,
3436
+ previewText = PREVIEW_TEXT
3437
+ }) {
3438
+ const containerRef = react.useRef(null);
3439
+ const [containerWidth, setContainerWidth] = react.useState(null);
3440
+ react.useEffect(() => {
3441
+ const el = containerRef.current?.parentElement;
3442
+ if (!el) return;
3443
+ const observer = new ResizeObserver((entries) => {
3444
+ const w = entries[0]?.contentRect.width;
3445
+ if (w && w > 0) setContainerWidth(w);
3446
+ });
3447
+ observer.observe(el);
3448
+ return () => observer.disconnect();
3449
+ }, []);
3450
+ const step = roleScales[role].md;
3451
+ const minFs = Math.max(8, Math.round(step.fontSize * 0.7));
3452
+ const maxFs = step.fontSize;
3453
+ const resolved = react.useMemo(() => {
3454
+ if (containerWidth == null) return { fontSize: maxFs, lineHeight: step.lineHeight };
3455
+ return resolveResponsiveSize(
3456
+ {
3457
+ role,
3458
+ size: "md",
3459
+ fontFamily,
3460
+ maxFontSize: maxFs,
3461
+ minFontSize: minFs
3462
+ },
3463
+ roleScales,
3464
+ { containerWidth, characterCount: previewText.length },
3465
+ calibrations
3466
+ );
3467
+ }, [role, step, roleScales, fontFamily, calibrations, containerWidth, minFs, maxFs]);
3468
+ const range = maxFs - minFs;
3469
+ const position = range > 0 ? (resolved.fontSize - minFs) / range * 100 : 100;
3470
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3471
+ "div",
3472
+ {
3473
+ ref: containerRef,
3474
+ style: {
3475
+ marginTop: 4,
3476
+ fontSize: 10,
3477
+ color: textColor,
3478
+ fontVariantNumeric: "tabular-nums",
3479
+ letterSpacing: 0.2
3480
+ },
3481
+ children: [
3482
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
3483
+ resolved.fontSize,
3484
+ "/",
3485
+ resolved.lineHeight,
3486
+ " \xB7 ",
3487
+ weight
3488
+ ] }),
3489
+ /* @__PURE__ */ jsxRuntime.jsxs(
3490
+ "div",
3491
+ {
3492
+ style: {
3493
+ display: "flex",
3494
+ alignItems: "center",
3495
+ gap: 4,
3496
+ marginTop: 3
3497
+ },
3498
+ children: [
3499
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { width: 16, textAlign: "right", flexShrink: 0 }, children: minFs }),
3500
+ /* @__PURE__ */ jsxRuntime.jsx(
3501
+ "div",
3502
+ {
3503
+ style: {
3504
+ flex: 1,
3505
+ height: 1,
3506
+ backgroundColor: textColor,
3507
+ opacity: 0.3,
3508
+ position: "relative"
3509
+ },
3510
+ children: /* @__PURE__ */ jsxRuntime.jsx(
3511
+ "div",
3512
+ {
3513
+ style: {
3514
+ position: "absolute",
3515
+ left: `${position}%`,
3516
+ top: "50%",
3517
+ width: 6,
3518
+ height: 6,
3519
+ borderRadius: "50%",
3520
+ backgroundColor: accentColor,
3521
+ transform: "translate(-50%, -50%)"
3522
+ }
3523
+ }
3524
+ )
3525
+ }
3526
+ ),
3527
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { width: 16, flexShrink: 0 }, children: maxFs })
3528
+ ]
3529
+ }
3530
+ )
3531
+ ]
3532
+ }
3533
+ );
3534
+ }
3326
3535
  function ComponentDetailView({
3327
3536
  componentId,
3328
3537
  selectedVariantId,
3329
3538
  onSelectVariant,
3330
3539
  propOverrides,
3331
- onPropOverride
3540
+ onPropOverride,
3541
+ roleWeights,
3542
+ onRoleWeightChange,
3543
+ fontCatalog,
3544
+ scopeFontMap
3332
3545
  }) {
3333
3546
  const tokens = components.useTokens();
3547
+ const { config } = components.useNewtoneTheme();
3334
3548
  const component = components.getComponent(componentId);
3335
3549
  const [hoveredId, setHoveredId] = react.useState(null);
3550
+ const [previewBreakpoint, setPreviewBreakpoint] = react.useState("lg");
3551
+ const [previewText, setPreviewText] = react.useState(PREVIEW_TEXT);
3552
+ const scaledConfig = react.useMemo(() => {
3553
+ if (previewBreakpoint === "lg") return config;
3554
+ const scales = BREAKPOINT_ROLE_SCALE[previewBreakpoint];
3555
+ const baseRoles = config.typography.roles;
3556
+ const scaledRoles = {};
3557
+ for (const role of Object.keys(baseRoles)) {
3558
+ const scale = scales[role];
3559
+ scaledRoles[role] = {};
3560
+ for (const size of ["sm", "md", "lg"]) {
3561
+ const step = baseRoles[role][size];
3562
+ scaledRoles[role][size] = scale === 1 ? step : scaleRoleStep(step, scale);
3563
+ }
3564
+ }
3565
+ return { ...config, typography: { ...config.typography, roles: scaledRoles } };
3566
+ }, [config, previewBreakpoint]);
3336
3567
  if (!component) return null;
3337
3568
  if (componentId === "icon" && propOverrides && onPropOverride) {
3338
3569
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -3383,6 +3614,7 @@ function ComponentDetailView({
3383
3614
  );
3384
3615
  }
3385
3616
  const interactiveColor = newtone.srgbToHex(tokens.accent.fill.srgb);
3617
+ const isTextComponent = componentId === "text";
3386
3618
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { padding: 32 }, children: [
3387
3619
  /* @__PURE__ */ jsxRuntime.jsx(
3388
3620
  "h2",
@@ -3404,12 +3636,151 @@ function ComponentDetailView({
3404
3636
  fontSize: 14,
3405
3637
  color: newtone.srgbToHex(tokens.textSecondary.srgb),
3406
3638
  margin: 0,
3407
- marginBottom: 32
3639
+ marginBottom: isTextComponent ? 16 : 32
3408
3640
  },
3409
3641
  children: component.description
3410
3642
  }
3411
3643
  ),
3412
- /* @__PURE__ */ jsxRuntime.jsx(
3644
+ isTextComponent && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 12, marginBottom: 16 }, children: [
3645
+ /* @__PURE__ */ jsxRuntime.jsx(
3646
+ "input",
3647
+ {
3648
+ type: "text",
3649
+ value: previewText,
3650
+ onChange: (e) => setPreviewText(e.target.value),
3651
+ placeholder: PREVIEW_TEXT,
3652
+ style: {
3653
+ width: "100%",
3654
+ padding: "8px 12px",
3655
+ fontSize: 14,
3656
+ fontFamily: "'SF Mono', 'Fira Code', Menlo, monospace",
3657
+ color: newtone.srgbToHex(tokens.textPrimary.srgb),
3658
+ backgroundColor: newtone.srgbToHex(tokens.backgroundSunken.srgb),
3659
+ border: `1px solid ${newtone.srgbToHex(tokens.border.srgb)}`,
3660
+ borderRadius: 8,
3661
+ boxSizing: "border-box",
3662
+ outline: "none"
3663
+ }
3664
+ }
3665
+ ),
3666
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", gap: 2 }, children: ["sm", "md", "lg"].map((bp) => {
3667
+ const isActive = previewBreakpoint === bp;
3668
+ return /* @__PURE__ */ jsxRuntime.jsx(
3669
+ "button",
3670
+ {
3671
+ onClick: () => setPreviewBreakpoint(bp),
3672
+ style: {
3673
+ padding: "4px 10px",
3674
+ fontSize: 11,
3675
+ fontWeight: isActive ? 600 : 400,
3676
+ color: isActive ? interactiveColor : newtone.srgbToHex(tokens.textSecondary.srgb),
3677
+ backgroundColor: isActive ? `${interactiveColor}18` : "transparent",
3678
+ border: `1px solid ${isActive ? interactiveColor : newtone.srgbToHex(tokens.border.srgb)}`,
3679
+ borderRadius: 4,
3680
+ cursor: "pointer",
3681
+ textTransform: "uppercase",
3682
+ letterSpacing: 0.5
3683
+ },
3684
+ children: bp
3685
+ },
3686
+ bp
3687
+ );
3688
+ }) })
3689
+ ] }),
3690
+ component.previewLayout === "list" ? /* @__PURE__ */ jsxRuntime.jsx(components.NewtoneProvider, { config: scaledConfig, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { display: "flex", flexDirection: "column", gap: 8 }, children: component.variants.map((variant) => {
3691
+ const isSelected = selectedVariantId === variant.id;
3692
+ const isHovered = hoveredId === variant.id;
3693
+ const borderColor = isSelected ? interactiveColor : isHovered ? `${interactiveColor}66` : newtone.srgbToHex(tokens.border.srgb);
3694
+ const showWeightControl = isTextComponent && ROLE_VARIANT_IDS.has(variant.id) && onRoleWeightChange;
3695
+ const role = variant.props.role;
3696
+ const scope = variant.props.scope ?? "main";
3697
+ let weightControl = null;
3698
+ if (showWeightControl && role) {
3699
+ const family = scopeFontMap?.[scope];
3700
+ const controlInfo = getWeightControlType(family, fontCatalog);
3701
+ const currentWeight = roleWeights?.[role] ?? ROLE_DEFAULT_WEIGHTS[role] ?? 400;
3702
+ if (controlInfo.type === "slider") {
3703
+ const displayWeight = controlInfo.stops ? controlInfo.stops.reduce(
3704
+ (prev, curr) => Math.abs(curr - currentWeight) < Math.abs(prev - currentWeight) ? curr : prev
3705
+ ) : currentWeight;
3706
+ weightControl = /* @__PURE__ */ jsxRuntime.jsx(
3707
+ WeightSlider,
3708
+ {
3709
+ value: displayWeight,
3710
+ min: controlInfo.min,
3711
+ max: controlInfo.max,
3712
+ stops: controlInfo.stops,
3713
+ onChange: (w) => onRoleWeightChange(role, w),
3714
+ textColor: newtone.srgbToHex(tokens.textSecondary.srgb),
3715
+ accentColor: interactiveColor
3716
+ }
3717
+ );
3718
+ }
3719
+ }
3720
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3721
+ "button",
3722
+ {
3723
+ onClick: () => onSelectVariant(variant.id),
3724
+ onMouseEnter: () => setHoveredId(variant.id),
3725
+ onMouseLeave: () => setHoveredId(null),
3726
+ style: {
3727
+ display: "flex",
3728
+ flexDirection: "row",
3729
+ alignItems: "center",
3730
+ gap: 16,
3731
+ padding: "12px 16px",
3732
+ borderRadius: 12,
3733
+ border: `2px solid ${borderColor}`,
3734
+ backgroundColor: newtone.srgbToHex(tokens.backgroundElevated.srgb),
3735
+ cursor: "pointer",
3736
+ textAlign: "left",
3737
+ transition: "border-color 150ms ease"
3738
+ },
3739
+ children: [
3740
+ /* @__PURE__ */ jsxRuntime.jsx(
3741
+ "span",
3742
+ {
3743
+ style: {
3744
+ fontSize: 11,
3745
+ fontWeight: 500,
3746
+ color: isSelected ? interactiveColor : newtone.srgbToHex(tokens.textSecondary.srgb),
3747
+ width: 88,
3748
+ flexShrink: 0,
3749
+ textTransform: "uppercase",
3750
+ letterSpacing: 0.5
3751
+ },
3752
+ children: variant.label
3753
+ }
3754
+ ),
3755
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [
3756
+ /* @__PURE__ */ jsxRuntime.jsx(
3757
+ ComponentRenderer,
3758
+ {
3759
+ componentId,
3760
+ props: variant.props,
3761
+ previewText
3762
+ }
3763
+ ),
3764
+ isTextComponent && role && scaledConfig.typography.roles[role] && /* @__PURE__ */ jsxRuntime.jsx(
3765
+ TextAnnotation,
3766
+ {
3767
+ role,
3768
+ roleScales: scaledConfig.typography.roles,
3769
+ fontFamily: scopeFontMap?.[scope],
3770
+ calibrations: scaledConfig.typography.calibrations,
3771
+ weight: roleWeights?.[role] ?? ROLE_DEFAULT_WEIGHTS[role] ?? 400,
3772
+ textColor: newtone.srgbToHex(tokens.textTertiary.srgb),
3773
+ accentColor: interactiveColor,
3774
+ previewText
3775
+ }
3776
+ )
3777
+ ] }),
3778
+ weightControl && /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: weightControl })
3779
+ ]
3780
+ },
3781
+ variant.id
3782
+ );
3783
+ }) }) }) : /* @__PURE__ */ jsxRuntime.jsx(
3413
3784
  "div",
3414
3785
  {
3415
3786
  style: {
@@ -3488,7 +3859,11 @@ function PreviewWindow({
3488
3859
  onNavigate,
3489
3860
  onSelectVariant,
3490
3861
  propOverrides,
3491
- onPropOverride
3862
+ onPropOverride,
3863
+ roleWeights,
3864
+ onRoleWeightChange,
3865
+ fontCatalog,
3866
+ scopeFontMap
3492
3867
  }) {
3493
3868
  const tokens = components.useTokens();
3494
3869
  const handleNavigateToCategory = react.useCallback(
@@ -3530,7 +3905,11 @@ function PreviewWindow({
3530
3905
  selectedVariantId,
3531
3906
  onSelectVariant,
3532
3907
  propOverrides,
3533
- onPropOverride
3908
+ onPropOverride,
3909
+ roleWeights,
3910
+ onRoleWeightChange,
3911
+ fontCatalog,
3912
+ scopeFontMap
3534
3913
  }
3535
3914
  )
3536
3915
  ] })
@@ -4042,7 +4421,9 @@ function Editor({
4042
4421
  persistence,
4043
4422
  headerSlots,
4044
4423
  onNavigate,
4045
- initialPreviewView
4424
+ initialPreviewView,
4425
+ manifestUrl,
4426
+ fontCatalog
4046
4427
  }) {
4047
4428
  const editor = useEditorState({
4048
4429
  initialState,
@@ -4053,8 +4434,26 @@ function Editor({
4053
4434
  defaultState,
4054
4435
  persistence,
4055
4436
  onNavigate,
4056
- initialPreviewView
4437
+ initialPreviewView,
4438
+ manifestUrl
4057
4439
  });
4440
+ const roleWeights = editor.configuratorState.typography?.roleWeights;
4441
+ const handleRoleWeightChange = react.useCallback(
4442
+ (role, weight) => {
4443
+ editor.dispatch({ type: "SET_ROLE_WEIGHT", role, weight });
4444
+ },
4445
+ [editor.dispatch]
4446
+ );
4447
+ const scopeFontMap = react.useMemo(() => {
4448
+ const fonts = editor.configuratorState.typography?.fonts;
4449
+ if (!fonts) return {};
4450
+ const map = {};
4451
+ if (fonts.main?.config?.family) map.main = fonts.main.config.family;
4452
+ if (fonts.display?.config?.family) map.display = fonts.display.config.family;
4453
+ if (fonts.mono?.config?.family) map.mono = fonts.mono.config.family;
4454
+ if (fonts.currency?.config?.family) map.currency = fonts.currency.config.family;
4455
+ return map;
4456
+ }, [editor.configuratorState.typography?.fonts]);
4058
4457
  const previewConfig = react.useMemo(
4059
4458
  () => chromeThemeConfig.tokenOverrides ? { ...editor.themeConfig, tokenOverrides: chromeThemeConfig.tokenOverrides } : editor.themeConfig,
4060
4459
  [editor.themeConfig, chromeThemeConfig.tokenOverrides]
@@ -4065,9 +4464,6 @@ function Editor({
4065
4464
  sidebar: /* @__PURE__ */ jsxRuntime.jsx(
4066
4465
  Sidebar,
4067
4466
  {
4068
- state: editor.configuratorState,
4069
- dispatch: editor.dispatch,
4070
- previewColors: editor.previewColors,
4071
4467
  isDirty: editor.isDirty,
4072
4468
  onRevert: editor.handleRevert,
4073
4469
  presets: editor.presets,
@@ -4077,9 +4473,7 @@ function Editor({
4077
4473
  onCreatePreset: editor.createPreset,
4078
4474
  onRenamePreset: editor.renamePreset,
4079
4475
  onDeletePreset: editor.deletePreset,
4080
- onDuplicatePreset: editor.duplicatePreset,
4081
- colorMode: editor.colorMode,
4082
- onColorModeChange: editor.handleColorModeChange
4476
+ onDuplicatePreset: editor.duplicatePreset
4083
4477
  }
4084
4478
  ),
4085
4479
  navbar: /* @__PURE__ */ jsxRuntime.jsx(
@@ -4104,12 +4498,31 @@ function Editor({
4104
4498
  },
4105
4499
  children: [
4106
4500
  /* @__PURE__ */ jsxRuntime.jsx(
4501
+ PrimaryNav,
4502
+ {
4503
+ activeSectionId: editor.activeSectionId,
4504
+ onSelectSection: editor.handleSectionChange
4505
+ }
4506
+ ),
4507
+ editor.activeSectionId === "components" ? /* @__PURE__ */ jsxRuntime.jsx(
4107
4508
  TableOfContents,
4108
4509
  {
4510
+ activeSectionId: editor.activeSectionId,
4109
4511
  activeView: editor.previewView,
4110
4512
  selectedComponentId: editor.selectedComponentId,
4111
4513
  onNavigate: editor.handlePreviewNavigate
4112
4514
  }
4515
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
4516
+ ConfiguratorPanel,
4517
+ {
4518
+ activeSectionId: editor.activeSectionId,
4519
+ state: editor.configuratorState,
4520
+ dispatch: editor.dispatch,
4521
+ previewColors: editor.previewColors,
4522
+ colorMode: editor.colorMode,
4523
+ onColorModeChange: editor.handleColorModeChange,
4524
+ fontCatalog
4525
+ }
4113
4526
  ),
4114
4527
  /* @__PURE__ */ jsxRuntime.jsx(
4115
4528
  "div",
@@ -4134,7 +4547,11 @@ function Editor({
4134
4547
  onNavigate: editor.handlePreviewNavigate,
4135
4548
  onSelectVariant: editor.handleSelectVariant,
4136
4549
  propOverrides: editor.propOverrides,
4137
- onPropOverride: editor.handlePropOverride
4550
+ onPropOverride: editor.handlePropOverride,
4551
+ roleWeights,
4552
+ onRoleWeightChange: handleRoleWeightChange,
4553
+ fontCatalog,
4554
+ scopeFontMap
4138
4555
  }
4139
4556
  ) })
4140
4557
  },
@@ -4178,6 +4595,7 @@ exports.OthersSection = OthersSection;
4178
4595
  exports.OverviewView = OverviewView;
4179
4596
  exports.PresetSelector = PresetSelector;
4180
4597
  exports.PreviewWindow = PreviewWindow;
4598
+ exports.PrimaryNav = PrimaryNav;
4181
4599
  exports.RightSidebar = RightSidebar;
4182
4600
  exports.Sidebar = Sidebar;
4183
4601
  exports.TableOfContents = TableOfContents;