@newtonedev/editor 0.1.6 → 0.1.8

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