@newtonedev/editor 0.1.12 → 0.2.1

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 (116) hide show
  1. package/dist/Editor.d.ts.map +1 -1
  2. package/dist/components/CodeBlock.d.ts.map +1 -1
  3. package/dist/components/ConfiguratorPanel.d.ts +6 -3
  4. package/dist/components/ConfiguratorPanel.d.ts.map +1 -1
  5. package/dist/components/EditorHeader.d.ts +3 -2
  6. package/dist/components/EditorHeader.d.ts.map +1 -1
  7. package/dist/components/EditorShell.d.ts.map +1 -1
  8. package/dist/components/PresetSelector.d.ts +3 -2
  9. package/dist/components/PresetSelector.d.ts.map +1 -1
  10. package/dist/components/PreviewWindow.d.ts.map +1 -1
  11. package/dist/components/RightSidebar.d.ts.map +1 -1
  12. package/dist/components/Sidebar.d.ts +8 -1
  13. package/dist/components/Sidebar.d.ts.map +1 -1
  14. package/dist/components/TableOfContents.d.ts.map +1 -1
  15. package/dist/components/sections/ColorsSection.d.ts +6 -3
  16. package/dist/components/sections/ColorsSection.d.ts.map +1 -1
  17. package/dist/components/sections/DynamicRangeSection.d.ts +2 -2
  18. package/dist/components/sections/DynamicRangeSection.d.ts.map +1 -1
  19. package/dist/components/sections/FontsSection.d.ts +2 -2
  20. package/dist/components/sections/FontsSection.d.ts.map +1 -1
  21. package/dist/components/sections/IconsSection.d.ts +2 -2
  22. package/dist/components/sections/IconsSection.d.ts.map +1 -1
  23. package/dist/components/sections/OthersSection.d.ts +2 -2
  24. package/dist/components/sections/OthersSection.d.ts.map +1 -1
  25. package/dist/components/sections/ScalePlots.d.ts +11 -0
  26. package/dist/components/sections/ScalePlots.d.ts.map +1 -0
  27. package/dist/components/sections/index.d.ts +1 -0
  28. package/dist/components/sections/index.d.ts.map +1 -1
  29. package/dist/configurator/bridge/toCSS.d.ts +7 -0
  30. package/dist/configurator/bridge/toCSS.d.ts.map +1 -0
  31. package/dist/configurator/bridge/toJSON.d.ts +15 -0
  32. package/dist/configurator/bridge/toJSON.d.ts.map +1 -0
  33. package/dist/configurator/bridge/toThemeConfig.d.ts +8 -0
  34. package/dist/configurator/bridge/toThemeConfig.d.ts.map +1 -0
  35. package/dist/configurator/constants.d.ts +13 -0
  36. package/dist/configurator/constants.d.ts.map +1 -0
  37. package/dist/configurator/hex-conversion.d.ts +21 -0
  38. package/dist/configurator/hex-conversion.d.ts.map +1 -0
  39. package/dist/configurator/hooks/useConfigurator.d.ts +11 -0
  40. package/dist/configurator/hooks/useConfigurator.d.ts.map +1 -0
  41. package/dist/configurator/hooks/usePreviewColors.d.ts +8 -0
  42. package/dist/configurator/hooks/usePreviewColors.d.ts.map +1 -0
  43. package/dist/configurator/hooks/useWcagValidation.d.ts +20 -0
  44. package/dist/configurator/hooks/useWcagValidation.d.ts.map +1 -0
  45. package/dist/configurator/hue-conversion.d.ts +10 -0
  46. package/dist/configurator/hue-conversion.d.ts.map +1 -0
  47. package/dist/configurator/state/actions.d.ts +107 -0
  48. package/dist/configurator/state/actions.d.ts.map +1 -0
  49. package/dist/configurator/state/defaults.d.ts +7 -0
  50. package/dist/configurator/state/defaults.d.ts.map +1 -0
  51. package/dist/configurator/state/reducer.d.ts +19 -0
  52. package/dist/configurator/state/reducer.d.ts.map +1 -0
  53. package/dist/configurator/types.d.ts +60 -0
  54. package/dist/configurator/types.d.ts.map +1 -0
  55. package/dist/hooks/useEditorState.d.ts +8 -6
  56. package/dist/hooks/useEditorState.d.ts.map +1 -1
  57. package/dist/hooks/usePresets.d.ts +7 -6
  58. package/dist/hooks/usePresets.d.ts.map +1 -1
  59. package/dist/index.cjs +30372 -808
  60. package/dist/index.cjs.map +1 -1
  61. package/dist/index.d.ts +17 -0
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +30351 -799
  64. package/dist/index.js.map +1 -1
  65. package/dist/preview/CategoryView.d.ts.map +1 -1
  66. package/dist/preview/ComponentDetailView.d.ts.map +1 -1
  67. package/dist/preview/ComponentRenderer.d.ts.map +1 -1
  68. package/dist/preview/IconBrowserView.d.ts.map +1 -1
  69. package/dist/preview/OverviewView.d.ts.map +1 -1
  70. package/dist/preview/PaletteScaleView.d.ts +11 -0
  71. package/dist/preview/PaletteScaleView.d.ts.map +1 -0
  72. package/dist/types.d.ts +4 -3
  73. package/dist/types.d.ts.map +1 -1
  74. package/package.json +7 -4
  75. package/src/Editor.tsx +43 -19
  76. package/src/components/CodeBlock.tsx +7 -11
  77. package/src/components/ConfiguratorPanel.tsx +25 -18
  78. package/src/components/EditorHeader.tsx +29 -39
  79. package/src/components/EditorShell.tsx +17 -29
  80. package/src/components/FontPicker.tsx +7 -7
  81. package/src/components/PresetSelector.tsx +211 -129
  82. package/src/components/PreviewWindow.tsx +5 -12
  83. package/src/components/PrimaryNav.tsx +6 -6
  84. package/src/components/RightSidebar.tsx +24 -25
  85. package/src/components/Sidebar.tsx +54 -60
  86. package/src/components/TableOfContents.tsx +4 -5
  87. package/src/components/sections/ColorsSection.tsx +109 -121
  88. package/src/components/sections/DynamicRangeSection.tsx +61 -75
  89. package/src/components/sections/FontsSection.tsx +17 -28
  90. package/src/components/sections/IconsSection.tsx +2 -2
  91. package/src/components/sections/OthersSection.tsx +4 -5
  92. package/src/components/sections/ScalePlots.tsx +221 -0
  93. package/src/components/sections/index.ts +1 -0
  94. package/src/configurator/bridge/toCSS.ts +44 -0
  95. package/src/configurator/bridge/toJSON.ts +24 -0
  96. package/src/configurator/bridge/toThemeConfig.ts +114 -0
  97. package/src/configurator/constants.ts +13 -0
  98. package/src/configurator/hex-conversion.ts +67 -0
  99. package/src/configurator/hooks/useConfigurator.ts +33 -0
  100. package/src/configurator/hooks/usePreviewColors.ts +47 -0
  101. package/src/configurator/hooks/useWcagValidation.ts +133 -0
  102. package/src/configurator/hue-conversion.ts +25 -0
  103. package/src/configurator/state/actions.ts +43 -0
  104. package/src/configurator/state/defaults.ts +107 -0
  105. package/src/configurator/state/reducer.ts +399 -0
  106. package/src/configurator/types.ts +65 -0
  107. package/src/hooks/useEditorState.ts +25 -11
  108. package/src/hooks/usePresets.ts +54 -33
  109. package/src/index.ts +33 -0
  110. package/src/preview/CategoryView.tsx +8 -11
  111. package/src/preview/ComponentDetailView.tsx +24 -54
  112. package/src/preview/ComponentRenderer.tsx +2 -4
  113. package/src/preview/IconBrowserView.tsx +9 -10
  114. package/src/preview/OverviewView.tsx +9 -12
  115. package/src/preview/PaletteScaleView.tsx +122 -0
  116. package/src/types.ts +4 -3
@@ -1,13 +1,13 @@
1
1
  import { useState, useRef, useEffect, useCallback } from "react";
2
+ import { createPortal } from "react-dom";
2
3
  import { useTokens, Icon } from "@newtonedev/components";
3
- import { srgbToHex } from "newtone";
4
4
  import type { Preset } from "../types";
5
5
  import { presetHasUnpublishedChanges } from "../utils/presets";
6
6
 
7
7
  interface PresetSelectorProps {
8
8
  readonly presets: readonly Preset[];
9
9
  readonly activePresetId: string;
10
- readonly publishedPresetId: string | null;
10
+ readonly defaultVariantId: string | null;
11
11
  readonly onSwitchPreset: (presetId: string) => void;
12
12
  readonly onCreatePreset: (name: string) => Promise<string>;
13
13
  readonly onRenamePreset: (presetId: string, name: string) => void;
@@ -16,49 +16,64 @@ interface PresetSelectorProps {
16
16
  presetId: string,
17
17
  name: string,
18
18
  ) => Promise<string>;
19
+ readonly onSetDefaultVariant: (presetId: string) => void;
19
20
  }
20
21
 
21
22
  export function PresetSelector({
22
23
  presets,
23
24
  activePresetId,
24
- publishedPresetId,
25
+ defaultVariantId,
25
26
  onSwitchPreset,
26
27
  onCreatePreset,
27
28
  onRenamePreset,
28
29
  onDeletePreset,
29
30
  onDuplicatePreset,
31
+ onSetDefaultVariant,
30
32
  }: PresetSelectorProps) {
31
33
  const tokens = useTokens();
32
34
  const [isOpen, setIsOpen] = useState(false);
33
35
  const [renamingId, setRenamingId] = useState<string | null>(null);
34
36
  const [renameValue, setRenameValue] = useState("");
35
37
  const [menuOpenId, setMenuOpenId] = useState<string | null>(null);
38
+ const [menuPos, setMenuPos] = useState<{ top: number; left: number } | null>(
39
+ null,
40
+ );
41
+ const [dropdownPos, setDropdownPos] = useState<{ top: number; left: number } | null>(null);
36
42
  const [hoveredId, setHoveredId] = useState<string | null>(null);
37
43
  const [hoveredAction, setHoveredAction] = useState<string | null>(null);
44
+ const triggerAreaRef = useRef<HTMLDivElement>(null);
38
45
  const dropdownRef = useRef<HTMLDivElement>(null);
46
+ const triggerRef = useRef<HTMLButtonElement>(null);
47
+ const menuRef = useRef<HTMLDivElement>(null);
39
48
  const renameInputRef = useRef<HTMLInputElement>(null);
40
49
 
41
50
  const activePreset = presets.find((p) => p.id === activePresetId);
42
51
 
43
- const borderColor = srgbToHex(tokens.border.srgb);
44
- const bgColor = srgbToHex(tokens.background.srgb);
45
- const textPrimary = srgbToHex(tokens.textPrimary.srgb);
46
- const textSecondary = srgbToHex(tokens.textSecondary.srgb);
47
- const interactiveColor = srgbToHex(tokens.accent.fill.srgb);
48
- const warningColor = srgbToHex(tokens.warning.fill.srgb);
49
- const errorColor = srgbToHex(tokens.error.fill.srgb);
52
+ const borderColor = tokens.colors.primary.main.fontDisabled;
53
+ const bgColor = tokens.colors.primary.main.background;
54
+ const textPrimary = tokens.colors.primary.main.fontPrimary;
55
+ const textSecondary = tokens.colors.primary.main.fontTertiary;
56
+ const interactiveColor = tokens.colors.secondary.emphasis.fontPrimary;
57
+ const warningColor = tokens.colors.warning.emphasis.fontPrimary;
58
+ const errorColor = tokens.colors.error.emphasis.fontPrimary;
50
59
  const hoverBg = `${borderColor}18`;
51
60
  const activeBg = `${interactiveColor}14`;
52
61
 
62
+ // Close dropdown + context menu on outside click
53
63
  useEffect(() => {
54
64
  if (!isOpen) return;
55
65
  const handleClickOutside = (e: MouseEvent) => {
56
- if (
57
- dropdownRef.current &&
58
- !dropdownRef.current.contains(e.target as Node)
59
- ) {
66
+ const target = e.target as Node;
67
+ const inTrigger =
68
+ triggerAreaRef.current && triggerAreaRef.current.contains(target);
69
+ const inDropdown =
70
+ dropdownRef.current && dropdownRef.current.contains(target);
71
+ const inMenu = menuRef.current && menuRef.current.contains(target);
72
+ if (!inTrigger && !inDropdown && !inMenu) {
60
73
  setIsOpen(false);
74
+ setDropdownPos(null);
61
75
  setMenuOpenId(null);
76
+ setMenuPos(null);
62
77
  setRenamingId(null);
63
78
  }
64
79
  };
@@ -73,20 +88,26 @@ export function PresetSelector({
73
88
  }
74
89
  }, [renamingId]);
75
90
 
91
+ const closeMenu = useCallback(() => {
92
+ setMenuOpenId(null);
93
+ setMenuPos(null);
94
+ }, []);
95
+
76
96
  const handleCreate = useCallback(async () => {
77
- const name = `Preset ${presets.length + 1}`;
97
+ const name = `Variant ${presets.length + 1}`;
78
98
  const newId = await onCreatePreset(name);
79
99
  onSwitchPreset(newId);
80
100
  setIsOpen(false);
101
+ setDropdownPos(null);
81
102
  }, [presets.length, onCreatePreset, onSwitchPreset]);
82
103
 
83
104
  const handleStartRename = useCallback(
84
105
  (presetId: string, currentName: string) => {
85
106
  setRenamingId(presetId);
86
107
  setRenameValue(currentName);
87
- setMenuOpenId(null);
108
+ closeMenu();
88
109
  },
89
- [],
110
+ [closeMenu],
90
111
  );
91
112
 
92
113
  const handleCommitRename = useCallback(() => {
@@ -102,25 +123,59 @@ export function PresetSelector({
102
123
  if (window.confirm("Delete this preset? This cannot be undone.")) {
103
124
  await onDeletePreset(presetId);
104
125
  }
105
- setMenuOpenId(null);
126
+ closeMenu();
106
127
  },
107
- [presets.length, onDeletePreset],
128
+ [presets.length, onDeletePreset, closeMenu],
108
129
  );
109
130
 
110
131
  const handleDuplicate = useCallback(
111
132
  async (presetId: string, sourceName: string) => {
112
133
  const newId = await onDuplicatePreset(presetId, `${sourceName} (copy)`);
113
134
  onSwitchPreset(newId);
114
- setMenuOpenId(null);
135
+ closeMenu();
115
136
  setIsOpen(false);
137
+ setDropdownPos(null);
116
138
  },
117
- [onDuplicatePreset, onSwitchPreset],
139
+ [onDuplicatePreset, onSwitchPreset, closeMenu],
118
140
  );
119
141
 
142
+ const openMenu = useCallback(
143
+ (presetId: string, button: HTMLElement) => {
144
+ if (menuOpenId === presetId) {
145
+ closeMenu();
146
+ return;
147
+ }
148
+ const rect = button.getBoundingClientRect();
149
+ setMenuPos({ top: rect.bottom + 4, left: rect.right + 4 });
150
+ setMenuOpenId(presetId);
151
+ setHoveredAction(null);
152
+ },
153
+ [menuOpenId, closeMenu],
154
+ );
155
+
156
+ const menuPreset = menuOpenId
157
+ ? presets.find((p) => p.id === menuOpenId)
158
+ : null;
159
+ const menuIsDefault = menuOpenId === defaultVariantId;
160
+
161
+ const toggleDropdown = useCallback(() => {
162
+ if (isOpen) {
163
+ setIsOpen(false);
164
+ setDropdownPos(null);
165
+ } else {
166
+ if (triggerRef.current) {
167
+ const rect = triggerRef.current.getBoundingClientRect();
168
+ setDropdownPos({ top: rect.bottom + 4, left: rect.left });
169
+ }
170
+ setIsOpen(true);
171
+ }
172
+ }, [isOpen]);
173
+
120
174
  return (
121
- <div ref={dropdownRef} style={{ position: "relative" }}>
175
+ <div ref={triggerAreaRef} style={{ position: "relative" }}>
122
176
  <button
123
- onClick={() => setIsOpen(!isOpen)}
177
+ ref={triggerRef}
178
+ onClick={toggleDropdown}
124
179
  style={{
125
180
  display: "flex",
126
181
  alignItems: "center",
@@ -156,25 +211,26 @@ export function PresetSelector({
156
211
  />
157
212
  </button>
158
213
 
159
- {isOpen && (
214
+ {isOpen && dropdownPos && createPortal(
160
215
  <div
216
+ ref={dropdownRef}
161
217
  style={{
162
- position: "absolute",
163
- top: "calc(100% + 4px)",
164
- left: 0,
218
+ position: "fixed",
219
+ top: dropdownPos.top,
220
+ left: dropdownPos.left,
165
221
  width: 260,
166
222
  backgroundColor: bgColor,
167
223
  border: `1px solid ${borderColor}`,
168
224
  borderRadius: 8,
169
225
  boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
170
- zIndex: 100,
226
+ zIndex: 10000,
171
227
  overflow: "hidden",
172
228
  }}
173
229
  >
174
230
  <div style={{ maxHeight: 240, overflowY: "auto", padding: "4px 0" }}>
175
231
  {presets.map((preset) => {
176
232
  const isActive = preset.id === activePresetId;
177
- const isPublishedPreset = preset.id === publishedPresetId;
233
+ const isDefaultVariant = preset.id === defaultVariantId;
178
234
  const hasChanges = presetHasUnpublishedChanges(preset);
179
235
  const isHovered = hoveredId === preset.id;
180
236
  const isRenaming = renamingId === preset.id;
@@ -226,6 +282,7 @@ export function PresetSelector({
226
282
  onClick={() => {
227
283
  onSwitchPreset(preset.id);
228
284
  setIsOpen(false);
285
+ setDropdownPos(null);
229
286
  }}
230
287
  style={{
231
288
  flex: 1,
@@ -259,7 +316,7 @@ export function PresetSelector({
259
316
  }}
260
317
  />
261
318
  )}
262
- {isPublishedPreset && (
319
+ {isDefaultVariant && (
263
320
  <span
264
321
  style={{
265
322
  fontSize: 10,
@@ -272,7 +329,7 @@ export function PresetSelector({
272
329
  lineHeight: "14px",
273
330
  }}
274
331
  >
275
- API
332
+ Default
276
333
  </span>
277
334
  )}
278
335
  </div>
@@ -281,7 +338,7 @@ export function PresetSelector({
281
338
  <button
282
339
  onClick={(e) => {
283
340
  e.stopPropagation();
284
- setMenuOpenId(isMenuShown ? null : preset.id);
341
+ openMenu(preset.id, e.currentTarget);
285
342
  }}
286
343
  style={{
287
344
  display: "flex",
@@ -297,105 +354,15 @@ export function PresetSelector({
297
354
  flexShrink: 0,
298
355
  }}
299
356
  >
300
- <Icon name="more_vert" size={14} color={textSecondary} />
357
+ <Icon
358
+ name="more_vert"
359
+ size={14}
360
+ color={textSecondary}
361
+ />
301
362
  </button>
302
363
  )}
303
364
  </>
304
365
  )}
305
-
306
- {isMenuShown && !isRenaming && (
307
- <div
308
- style={{
309
- position: "absolute",
310
- top: 0,
311
- right: -140,
312
- width: 130,
313
- backgroundColor: bgColor,
314
- border: `1px solid ${borderColor}`,
315
- borderRadius: 6,
316
- boxShadow: "0 2px 8px rgba(0,0,0,0.12)",
317
- zIndex: 101,
318
- overflow: "hidden",
319
- }}
320
- >
321
- <button
322
- onClick={(e) => {
323
- e.stopPropagation();
324
- handleStartRename(preset.id, preset.name);
325
- }}
326
- onMouseEnter={() => setHoveredAction("rename")}
327
- onMouseLeave={() => setHoveredAction(null)}
328
- style={{
329
- display: "block",
330
- width: "100%",
331
- padding: "8px 12px",
332
- border: "none",
333
- backgroundColor:
334
- hoveredAction === "rename"
335
- ? hoverBg
336
- : "transparent",
337
- color: textPrimary,
338
- fontSize: 12,
339
- textAlign: "left",
340
- cursor: "pointer",
341
- }}
342
- >
343
- Rename
344
- </button>
345
- <button
346
- onClick={(e) => {
347
- e.stopPropagation();
348
- handleDuplicate(preset.id, preset.name);
349
- }}
350
- onMouseEnter={() => setHoveredAction("duplicate")}
351
- onMouseLeave={() => setHoveredAction(null)}
352
- style={{
353
- display: "block",
354
- width: "100%",
355
- padding: "8px 12px",
356
- border: "none",
357
- backgroundColor:
358
- hoveredAction === "duplicate"
359
- ? hoverBg
360
- : "transparent",
361
- color: textPrimary,
362
- fontSize: 12,
363
- textAlign: "left",
364
- cursor: "pointer",
365
- }}
366
- >
367
- Duplicate
368
- </button>
369
- <button
370
- onClick={(e) => {
371
- e.stopPropagation();
372
- handleDelete(preset.id);
373
- }}
374
- onMouseEnter={() => setHoveredAction("delete")}
375
- onMouseLeave={() => setHoveredAction(null)}
376
- disabled={presets.length <= 1}
377
- style={{
378
- display: "block",
379
- width: "100%",
380
- padding: "8px 12px",
381
- border: "none",
382
- backgroundColor:
383
- hoveredAction === "delete"
384
- ? hoverBg
385
- : "transparent",
386
- color:
387
- presets.length <= 1 ? textSecondary : errorColor,
388
- fontSize: 12,
389
- textAlign: "left",
390
- cursor:
391
- presets.length <= 1 ? "not-allowed" : "pointer",
392
- opacity: presets.length <= 1 ? 0.5 : 1,
393
- }}
394
- >
395
- Delete
396
- </button>
397
- </div>
398
- )}
399
366
  </div>
400
367
  );
401
368
  })}
@@ -421,9 +388,124 @@ export function PresetSelector({
421
388
  }}
422
389
  >
423
390
  <Icon name="add" size={14} color={textSecondary} />
424
- New preset
391
+ New variant
392
+ </button>
393
+ </div>,
394
+ document.body,
395
+ )}
396
+
397
+ {/* Context menu — rendered outside the overflow:hidden dropdown panel */}
398
+ {menuOpenId && menuPreset && menuPos && createPortal(
399
+ <div
400
+ ref={menuRef}
401
+ style={{
402
+ position: "fixed",
403
+ top: menuPos.top,
404
+ left: menuPos.left,
405
+ width: 140,
406
+ backgroundColor: bgColor,
407
+ border: `1px solid ${borderColor}`,
408
+ borderRadius: 6,
409
+ boxShadow: "0 2px 8px rgba(0,0,0,0.12)",
410
+ zIndex: 10000,
411
+ overflow: "hidden",
412
+ }}
413
+ >
414
+ {!menuIsDefault && (
415
+ <button
416
+ onClick={(e) => {
417
+ e.stopPropagation();
418
+ onSetDefaultVariant(menuOpenId);
419
+ closeMenu();
420
+ }}
421
+ onMouseEnter={() => setHoveredAction("default")}
422
+ onMouseLeave={() => setHoveredAction(null)}
423
+ style={{
424
+ display: "block",
425
+ width: "100%",
426
+ padding: "8px 12px",
427
+ border: "none",
428
+ backgroundColor:
429
+ hoveredAction === "default" ? hoverBg : "transparent",
430
+ color: textPrimary,
431
+ fontSize: 12,
432
+ textAlign: "left",
433
+ cursor: "pointer",
434
+ }}
435
+ >
436
+ Make default
437
+ </button>
438
+ )}
439
+ <button
440
+ onClick={(e) => {
441
+ e.stopPropagation();
442
+ handleStartRename(menuPreset.id, menuPreset.name);
443
+ }}
444
+ onMouseEnter={() => setHoveredAction("rename")}
445
+ onMouseLeave={() => setHoveredAction(null)}
446
+ style={{
447
+ display: "block",
448
+ width: "100%",
449
+ padding: "8px 12px",
450
+ border: "none",
451
+ backgroundColor:
452
+ hoveredAction === "rename" ? hoverBg : "transparent",
453
+ color: textPrimary,
454
+ fontSize: 12,
455
+ textAlign: "left",
456
+ cursor: "pointer",
457
+ }}
458
+ >
459
+ Rename
460
+ </button>
461
+ <button
462
+ onClick={(e) => {
463
+ e.stopPropagation();
464
+ handleDuplicate(menuPreset.id, menuPreset.name);
465
+ }}
466
+ onMouseEnter={() => setHoveredAction("duplicate")}
467
+ onMouseLeave={() => setHoveredAction(null)}
468
+ style={{
469
+ display: "block",
470
+ width: "100%",
471
+ padding: "8px 12px",
472
+ border: "none",
473
+ backgroundColor:
474
+ hoveredAction === "duplicate" ? hoverBg : "transparent",
475
+ color: textPrimary,
476
+ fontSize: 12,
477
+ textAlign: "left",
478
+ cursor: "pointer",
479
+ }}
480
+ >
481
+ Duplicate
482
+ </button>
483
+ <button
484
+ onClick={(e) => {
485
+ e.stopPropagation();
486
+ handleDelete(menuPreset.id);
487
+ }}
488
+ onMouseEnter={() => setHoveredAction("delete")}
489
+ onMouseLeave={() => setHoveredAction(null)}
490
+ disabled={presets.length <= 1}
491
+ style={{
492
+ display: "block",
493
+ width: "100%",
494
+ padding: "8px 12px",
495
+ border: "none",
496
+ backgroundColor:
497
+ hoveredAction === "delete" ? hoverBg : "transparent",
498
+ color: presets.length <= 1 ? textSecondary : errorColor,
499
+ fontSize: 12,
500
+ textAlign: "left",
501
+ cursor: presets.length <= 1 ? "not-allowed" : "pointer",
502
+ opacity: presets.length <= 1 ? 0.5 : 1,
503
+ }}
504
+ >
505
+ Delete
425
506
  </button>
426
- </div>
507
+ </div>,
508
+ document.body,
427
509
  )}
428
510
  </div>
429
511
  );
@@ -1,6 +1,5 @@
1
1
  import { useCallback } from "react";
2
- import { useTokens } from "@newtonedev/components";
3
- import { srgbToHex } from "newtone";
2
+ import { Frame } from "@newtonedev/components";
4
3
  import type { TextRole } from "@newtonedev/fonts";
5
4
  import { OverviewView } from "../preview/OverviewView";
6
5
  import { CategoryView } from "../preview/CategoryView";
@@ -32,8 +31,6 @@ export function PreviewWindow({
32
31
  fontCatalog,
33
32
  scopeFontMap,
34
33
  }: PreviewWindowProps) {
35
- const tokens = useTokens();
36
-
37
34
  const handleNavigateToCategory = useCallback(
38
35
  (categoryId: string) => onNavigate({ kind: "category", categoryId }),
39
36
  [onNavigate],
@@ -45,13 +42,9 @@ export function PreviewWindow({
45
42
  );
46
43
 
47
44
  return (
48
- <div
49
- style={{
50
- display: "flex",
51
- flexDirection: "column",
52
- height: "100%",
53
- backgroundColor: srgbToHex(tokens.background.srgb),
54
- }}
45
+ <Frame
46
+ direction="vertical"
47
+ height="fill"
55
48
  >
56
49
  <div style={{ flex: 1, overflowY: "auto" }}>
57
50
  {view.kind === "overview" && (
@@ -80,6 +73,6 @@ export function PreviewWindow({
80
73
  />
81
74
  )}
82
75
  </div>
83
- </div>
76
+ </Frame>
84
77
  );
85
78
  }
@@ -1,7 +1,7 @@
1
1
  import type { ReactNode } from "react";
2
2
  import { useState } from "react";
3
3
  import { useTokens, Icon, CATEGORIES } from "@newtonedev/components";
4
- import { srgbToHex } from "newtone";
4
+
5
5
 
6
6
  interface PrimaryNavProps {
7
7
  readonly activeSectionId: string | null;
@@ -15,11 +15,11 @@ export function PrimaryNav({ activeSectionId, onSelectSection, footer }: Primary
15
15
  const tokens = useTokens();
16
16
  const [hoveredId, setHoveredId] = useState<string | null>(null);
17
17
 
18
- const bg = srgbToHex(tokens.background.srgb);
19
- const borderColor = srgbToHex(tokens.border.srgb);
20
- const activeBg = srgbToHex(tokens.backgroundInteractive.srgb);
21
- const iconColor = srgbToHex(tokens.textSecondary.srgb);
22
- const activeIconColor = srgbToHex(tokens.textPrimary.srgb);
18
+ const bg = tokens.colors.primary.main.background;
19
+ const borderColor = tokens.colors.primary.main.fontDisabled;
20
+ const activeBg = tokens.colors.primary.main.divider;
21
+ const iconColor = tokens.colors.primary.main.fontTertiary;
22
+ const activeIconColor = tokens.colors.primary.main.fontPrimary;
23
23
 
24
24
  return (
25
25
  <nav