@newtonedev/editor 0.1.6 → 0.1.8

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