@newtonedev/editor 0.1.6 → 0.1.7

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