@newtonedev/editor 0.1.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 (86) hide show
  1. package/dist/Editor.d.ts +3 -0
  2. package/dist/Editor.d.ts.map +1 -0
  3. package/dist/components/CodeBlock.d.ts +7 -0
  4. package/dist/components/CodeBlock.d.ts.map +1 -0
  5. package/dist/components/EditorHeader.d.ts +16 -0
  6. package/dist/components/EditorHeader.d.ts.map +1 -0
  7. package/dist/components/EditorShell.d.ts +10 -0
  8. package/dist/components/EditorShell.d.ts.map +1 -0
  9. package/dist/components/FontPicker.d.ts +11 -0
  10. package/dist/components/FontPicker.d.ts.map +1 -0
  11. package/dist/components/PresetSelector.d.ts +14 -0
  12. package/dist/components/PresetSelector.d.ts.map +1 -0
  13. package/dist/components/PreviewWindow.d.ts +11 -0
  14. package/dist/components/PreviewWindow.d.ts.map +1 -0
  15. package/dist/components/RightSidebar.d.ts +12 -0
  16. package/dist/components/RightSidebar.d.ts.map +1 -0
  17. package/dist/components/Sidebar.d.ts +25 -0
  18. package/dist/components/Sidebar.d.ts.map +1 -0
  19. package/dist/components/TableOfContents.d.ts +9 -0
  20. package/dist/components/TableOfContents.d.ts.map +1 -0
  21. package/dist/components/ThemeBar.d.ts +8 -0
  22. package/dist/components/ThemeBar.d.ts.map +1 -0
  23. package/dist/components/sections/ColorsSection.d.ts +14 -0
  24. package/dist/components/sections/ColorsSection.d.ts.map +1 -0
  25. package/dist/components/sections/DynamicRangeSection.d.ts +9 -0
  26. package/dist/components/sections/DynamicRangeSection.d.ts.map +1 -0
  27. package/dist/components/sections/FontsSection.d.ts +9 -0
  28. package/dist/components/sections/FontsSection.d.ts.map +1 -0
  29. package/dist/components/sections/IconsSection.d.ts +9 -0
  30. package/dist/components/sections/IconsSection.d.ts.map +1 -0
  31. package/dist/components/sections/OthersSection.d.ts +9 -0
  32. package/dist/components/sections/OthersSection.d.ts.map +1 -0
  33. package/dist/components/sections/index.d.ts +6 -0
  34. package/dist/components/sections/index.d.ts.map +1 -0
  35. package/dist/hooks/useEditorState.d.ts +53 -0
  36. package/dist/hooks/useEditorState.d.ts.map +1 -0
  37. package/dist/hooks/useHover.d.ts +8 -0
  38. package/dist/hooks/useHover.d.ts.map +1 -0
  39. package/dist/hooks/usePresets.d.ts +33 -0
  40. package/dist/hooks/usePresets.d.ts.map +1 -0
  41. package/dist/index.cjs +3846 -0
  42. package/dist/index.cjs.map +1 -0
  43. package/dist/index.d.ts +22 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +3819 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/preview/CategoryView.d.ts +7 -0
  48. package/dist/preview/CategoryView.d.ts.map +1 -0
  49. package/dist/preview/ComponentDetailView.d.ts +9 -0
  50. package/dist/preview/ComponentDetailView.d.ts.map +1 -0
  51. package/dist/preview/ComponentRenderer.d.ts +7 -0
  52. package/dist/preview/ComponentRenderer.d.ts.map +1 -0
  53. package/dist/preview/OverviewView.d.ts +7 -0
  54. package/dist/preview/OverviewView.d.ts.map +1 -0
  55. package/dist/types.d.ts +69 -0
  56. package/dist/types.d.ts.map +1 -0
  57. package/dist/utils/presets.d.ts +5 -0
  58. package/dist/utils/presets.d.ts.map +1 -0
  59. package/package.json +51 -0
  60. package/src/Editor.tsx +128 -0
  61. package/src/components/CodeBlock.tsx +58 -0
  62. package/src/components/EditorHeader.tsx +86 -0
  63. package/src/components/EditorShell.tsx +67 -0
  64. package/src/components/FontPicker.tsx +351 -0
  65. package/src/components/PresetSelector.tsx +455 -0
  66. package/src/components/PreviewWindow.tsx +69 -0
  67. package/src/components/RightSidebar.tsx +374 -0
  68. package/src/components/Sidebar.tsx +332 -0
  69. package/src/components/TableOfContents.tsx +152 -0
  70. package/src/components/ThemeBar.tsx +76 -0
  71. package/src/components/sections/ColorsSection.tsx +485 -0
  72. package/src/components/sections/DynamicRangeSection.tsx +399 -0
  73. package/src/components/sections/FontsSection.tsx +132 -0
  74. package/src/components/sections/IconsSection.tsx +66 -0
  75. package/src/components/sections/OthersSection.tsx +70 -0
  76. package/src/components/sections/index.ts +5 -0
  77. package/src/hooks/useEditorState.ts +381 -0
  78. package/src/hooks/useHover.ts +8 -0
  79. package/src/hooks/usePresets.ts +254 -0
  80. package/src/index.ts +52 -0
  81. package/src/preview/CategoryView.tsx +134 -0
  82. package/src/preview/ComponentDetailView.tsx +126 -0
  83. package/src/preview/ComponentRenderer.tsx +107 -0
  84. package/src/preview/OverviewView.tsx +177 -0
  85. package/src/types.ts +77 -0
  86. package/src/utils/presets.ts +24 -0
@@ -0,0 +1,455 @@
1
+ import { useState, useRef, useEffect, useCallback } from "react";
2
+ import { useTokens } from "@newtonedev/components";
3
+ import { srgbToHex } from "newtone";
4
+ import type { Preset } from "../types";
5
+ import { presetHasUnpublishedChanges } from "../utils/presets";
6
+
7
+ interface PresetSelectorProps {
8
+ readonly presets: readonly Preset[];
9
+ readonly activePresetId: string;
10
+ readonly publishedPresetId: string | null;
11
+ readonly onSwitchPreset: (presetId: string) => void;
12
+ readonly onCreatePreset: (name: string) => Promise<string>;
13
+ readonly onRenamePreset: (presetId: string, name: string) => void;
14
+ readonly onDeletePreset: (presetId: string) => Promise<void>;
15
+ readonly onDuplicatePreset: (
16
+ presetId: string,
17
+ name: string,
18
+ ) => Promise<string>;
19
+ }
20
+
21
+ export function PresetSelector({
22
+ presets,
23
+ activePresetId,
24
+ publishedPresetId,
25
+ onSwitchPreset,
26
+ onCreatePreset,
27
+ onRenamePreset,
28
+ onDeletePreset,
29
+ onDuplicatePreset,
30
+ }: PresetSelectorProps) {
31
+ const tokens = useTokens();
32
+ const [isOpen, setIsOpen] = useState(false);
33
+ const [renamingId, setRenamingId] = useState<string | null>(null);
34
+ const [renameValue, setRenameValue] = useState("");
35
+ const [menuOpenId, setMenuOpenId] = useState<string | null>(null);
36
+ const [hoveredId, setHoveredId] = useState<string | null>(null);
37
+ const [hoveredAction, setHoveredAction] = useState<string | null>(null);
38
+ const dropdownRef = useRef<HTMLDivElement>(null);
39
+ const renameInputRef = useRef<HTMLInputElement>(null);
40
+
41
+ const activePreset = presets.find((p) => p.id === activePresetId);
42
+
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.interactive.srgb);
48
+ const warningColor = srgbToHex(tokens.warning.srgb);
49
+ const errorColor = srgbToHex(tokens.error.srgb);
50
+ const hoverBg = `${borderColor}18`;
51
+ const activeBg = `${interactiveColor}14`;
52
+
53
+ useEffect(() => {
54
+ if (!isOpen) return;
55
+ const handleClickOutside = (e: MouseEvent) => {
56
+ if (
57
+ dropdownRef.current &&
58
+ !dropdownRef.current.contains(e.target as Node)
59
+ ) {
60
+ setIsOpen(false);
61
+ setMenuOpenId(null);
62
+ setRenamingId(null);
63
+ }
64
+ };
65
+ document.addEventListener("mousedown", handleClickOutside);
66
+ return () => document.removeEventListener("mousedown", handleClickOutside);
67
+ }, [isOpen]);
68
+
69
+ useEffect(() => {
70
+ if (renamingId && renameInputRef.current) {
71
+ renameInputRef.current.focus();
72
+ renameInputRef.current.select();
73
+ }
74
+ }, [renamingId]);
75
+
76
+ const handleCreate = useCallback(async () => {
77
+ const name = `Preset ${presets.length + 1}`;
78
+ const newId = await onCreatePreset(name);
79
+ onSwitchPreset(newId);
80
+ setIsOpen(false);
81
+ }, [presets.length, onCreatePreset, onSwitchPreset]);
82
+
83
+ const handleStartRename = useCallback(
84
+ (presetId: string, currentName: string) => {
85
+ setRenamingId(presetId);
86
+ setRenameValue(currentName);
87
+ setMenuOpenId(null);
88
+ },
89
+ [],
90
+ );
91
+
92
+ const handleCommitRename = useCallback(() => {
93
+ if (renamingId && renameValue.trim()) {
94
+ onRenamePreset(renamingId, renameValue.trim());
95
+ }
96
+ setRenamingId(null);
97
+ }, [renamingId, renameValue, onRenamePreset]);
98
+
99
+ const handleDelete = useCallback(
100
+ async (presetId: string) => {
101
+ if (presets.length <= 1) return;
102
+ if (window.confirm("Delete this preset? This cannot be undone.")) {
103
+ await onDeletePreset(presetId);
104
+ }
105
+ setMenuOpenId(null);
106
+ },
107
+ [presets.length, onDeletePreset],
108
+ );
109
+
110
+ const handleDuplicate = useCallback(
111
+ async (presetId: string, sourceName: string) => {
112
+ const newId = await onDuplicatePreset(presetId, `${sourceName} (copy)`);
113
+ onSwitchPreset(newId);
114
+ setMenuOpenId(null);
115
+ setIsOpen(false);
116
+ },
117
+ [onDuplicatePreset, onSwitchPreset],
118
+ );
119
+
120
+ return (
121
+ <div ref={dropdownRef} style={{ position: "relative" }}>
122
+ <button
123
+ onClick={() => setIsOpen(!isOpen)}
124
+ style={{
125
+ display: "flex",
126
+ alignItems: "center",
127
+ gap: 6,
128
+ padding: "4px 10px",
129
+ borderRadius: 6,
130
+ border: `1px solid ${borderColor}`,
131
+ backgroundColor: "transparent",
132
+ color: textPrimary,
133
+ fontSize: 12,
134
+ fontWeight: 500,
135
+ cursor: "pointer",
136
+ maxWidth: 160,
137
+ }}
138
+ >
139
+ <span
140
+ style={{
141
+ overflow: "hidden",
142
+ textOverflow: "ellipsis",
143
+ whiteSpace: "nowrap",
144
+ }}
145
+ >
146
+ {activePreset?.name ?? "Default"}
147
+ </span>
148
+ <svg
149
+ width={10}
150
+ height={10}
151
+ viewBox="0 0 24 24"
152
+ fill="none"
153
+ stroke="currentColor"
154
+ strokeWidth={2}
155
+ style={{
156
+ transform: isOpen ? "rotate(180deg)" : "none",
157
+ transition: "transform 150ms ease",
158
+ flexShrink: 0,
159
+ }}
160
+ >
161
+ <polyline points="6 9 12 15 18 9" />
162
+ </svg>
163
+ </button>
164
+
165
+ {isOpen && (
166
+ <div
167
+ style={{
168
+ position: "absolute",
169
+ top: "calc(100% + 4px)",
170
+ left: 0,
171
+ width: 260,
172
+ backgroundColor: bgColor,
173
+ border: `1px solid ${borderColor}`,
174
+ borderRadius: 8,
175
+ boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
176
+ zIndex: 100,
177
+ overflow: "hidden",
178
+ }}
179
+ >
180
+ <div style={{ maxHeight: 240, overflowY: "auto", padding: "4px 0" }}>
181
+ {presets.map((preset) => {
182
+ const isActive = preset.id === activePresetId;
183
+ const isPublishedPreset = preset.id === publishedPresetId;
184
+ const hasChanges = presetHasUnpublishedChanges(preset);
185
+ const isHovered = hoveredId === preset.id;
186
+ const isRenaming = renamingId === preset.id;
187
+ const isMenuShown = menuOpenId === preset.id;
188
+
189
+ return (
190
+ <div
191
+ key={preset.id}
192
+ onMouseEnter={() => setHoveredId(preset.id)}
193
+ onMouseLeave={() => setHoveredId(null)}
194
+ style={{
195
+ display: "flex",
196
+ alignItems: "center",
197
+ padding: "6px 12px",
198
+ backgroundColor: isActive
199
+ ? activeBg
200
+ : isHovered
201
+ ? hoverBg
202
+ : "transparent",
203
+ cursor: isRenaming ? "default" : "pointer",
204
+ transition: "background-color 100ms ease",
205
+ position: "relative",
206
+ }}
207
+ >
208
+ {isRenaming ? (
209
+ <input
210
+ ref={renameInputRef}
211
+ value={renameValue}
212
+ onChange={(e) => setRenameValue(e.target.value)}
213
+ onBlur={handleCommitRename}
214
+ onKeyDown={(e) => {
215
+ if (e.key === "Enter") handleCommitRename();
216
+ if (e.key === "Escape") setRenamingId(null);
217
+ }}
218
+ style={{
219
+ flex: 1,
220
+ fontSize: 13,
221
+ padding: "2px 6px",
222
+ border: `1px solid ${interactiveColor}`,
223
+ borderRadius: 4,
224
+ backgroundColor: bgColor,
225
+ color: textPrimary,
226
+ outline: "none",
227
+ }}
228
+ />
229
+ ) : (
230
+ <>
231
+ <div
232
+ onClick={() => {
233
+ onSwitchPreset(preset.id);
234
+ setIsOpen(false);
235
+ }}
236
+ style={{
237
+ flex: 1,
238
+ display: "flex",
239
+ alignItems: "center",
240
+ gap: 6,
241
+ minWidth: 0,
242
+ }}
243
+ >
244
+ <span
245
+ style={{
246
+ fontSize: 13,
247
+ fontWeight: isActive ? 600 : 400,
248
+ color: textPrimary,
249
+ overflow: "hidden",
250
+ textOverflow: "ellipsis",
251
+ whiteSpace: "nowrap",
252
+ }}
253
+ >
254
+ {preset.name}
255
+ </span>
256
+ {hasChanges && (
257
+ <span
258
+ title="Unpublished changes"
259
+ style={{
260
+ width: 6,
261
+ height: 6,
262
+ borderRadius: "50%",
263
+ backgroundColor: warningColor,
264
+ flexShrink: 0,
265
+ }}
266
+ />
267
+ )}
268
+ {isPublishedPreset && (
269
+ <span
270
+ style={{
271
+ fontSize: 10,
272
+ fontWeight: 600,
273
+ color: interactiveColor,
274
+ padding: "1px 4px",
275
+ borderRadius: 3,
276
+ border: `1px solid ${interactiveColor}`,
277
+ flexShrink: 0,
278
+ lineHeight: "14px",
279
+ }}
280
+ >
281
+ API
282
+ </span>
283
+ )}
284
+ </div>
285
+
286
+ {(isHovered || isMenuShown) && (
287
+ <button
288
+ onClick={(e) => {
289
+ e.stopPropagation();
290
+ setMenuOpenId(isMenuShown ? null : preset.id);
291
+ }}
292
+ style={{
293
+ display: "flex",
294
+ alignItems: "center",
295
+ justifyContent: "center",
296
+ width: 24,
297
+ height: 24,
298
+ border: "none",
299
+ background: "none",
300
+ color: textSecondary,
301
+ cursor: "pointer",
302
+ borderRadius: 4,
303
+ flexShrink: 0,
304
+ }}
305
+ >
306
+ <svg
307
+ width={14}
308
+ height={14}
309
+ viewBox="0 0 24 24"
310
+ fill="currentColor"
311
+ >
312
+ <circle cx={12} cy={5} r={2} />
313
+ <circle cx={12} cy={12} r={2} />
314
+ <circle cx={12} cy={19} r={2} />
315
+ </svg>
316
+ </button>
317
+ )}
318
+ </>
319
+ )}
320
+
321
+ {isMenuShown && !isRenaming && (
322
+ <div
323
+ style={{
324
+ position: "absolute",
325
+ top: 0,
326
+ right: -140,
327
+ width: 130,
328
+ backgroundColor: bgColor,
329
+ border: `1px solid ${borderColor}`,
330
+ borderRadius: 6,
331
+ boxShadow: "0 2px 8px rgba(0,0,0,0.12)",
332
+ zIndex: 101,
333
+ overflow: "hidden",
334
+ }}
335
+ >
336
+ <button
337
+ onClick={(e) => {
338
+ e.stopPropagation();
339
+ handleStartRename(preset.id, preset.name);
340
+ }}
341
+ onMouseEnter={() => setHoveredAction("rename")}
342
+ onMouseLeave={() => setHoveredAction(null)}
343
+ style={{
344
+ display: "block",
345
+ width: "100%",
346
+ padding: "8px 12px",
347
+ border: "none",
348
+ backgroundColor:
349
+ hoveredAction === "rename"
350
+ ? hoverBg
351
+ : "transparent",
352
+ color: textPrimary,
353
+ fontSize: 12,
354
+ textAlign: "left",
355
+ cursor: "pointer",
356
+ }}
357
+ >
358
+ Rename
359
+ </button>
360
+ <button
361
+ onClick={(e) => {
362
+ e.stopPropagation();
363
+ handleDuplicate(preset.id, preset.name);
364
+ }}
365
+ onMouseEnter={() => setHoveredAction("duplicate")}
366
+ onMouseLeave={() => setHoveredAction(null)}
367
+ style={{
368
+ display: "block",
369
+ width: "100%",
370
+ padding: "8px 12px",
371
+ border: "none",
372
+ backgroundColor:
373
+ hoveredAction === "duplicate"
374
+ ? hoverBg
375
+ : "transparent",
376
+ color: textPrimary,
377
+ fontSize: 12,
378
+ textAlign: "left",
379
+ cursor: "pointer",
380
+ }}
381
+ >
382
+ Duplicate
383
+ </button>
384
+ <button
385
+ onClick={(e) => {
386
+ e.stopPropagation();
387
+ handleDelete(preset.id);
388
+ }}
389
+ onMouseEnter={() => setHoveredAction("delete")}
390
+ onMouseLeave={() => setHoveredAction(null)}
391
+ disabled={presets.length <= 1}
392
+ style={{
393
+ display: "block",
394
+ width: "100%",
395
+ padding: "8px 12px",
396
+ border: "none",
397
+ backgroundColor:
398
+ hoveredAction === "delete"
399
+ ? hoverBg
400
+ : "transparent",
401
+ color:
402
+ presets.length <= 1 ? textSecondary : errorColor,
403
+ fontSize: 12,
404
+ textAlign: "left",
405
+ cursor:
406
+ presets.length <= 1 ? "not-allowed" : "pointer",
407
+ opacity: presets.length <= 1 ? 0.5 : 1,
408
+ }}
409
+ >
410
+ Delete
411
+ </button>
412
+ </div>
413
+ )}
414
+ </div>
415
+ );
416
+ })}
417
+ </div>
418
+
419
+ <button
420
+ onClick={handleCreate}
421
+ onMouseEnter={() => setHoveredAction("create")}
422
+ onMouseLeave={() => setHoveredAction(null)}
423
+ style={{
424
+ display: "flex",
425
+ alignItems: "center",
426
+ gap: 8,
427
+ width: "100%",
428
+ padding: "10px 12px",
429
+ border: "none",
430
+ borderTop: `1px solid ${borderColor}`,
431
+ backgroundColor:
432
+ hoveredAction === "create" ? hoverBg : "transparent",
433
+ color: textSecondary,
434
+ fontSize: 13,
435
+ cursor: "pointer",
436
+ }}
437
+ >
438
+ <svg
439
+ width={14}
440
+ height={14}
441
+ viewBox="0 0 24 24"
442
+ fill="none"
443
+ stroke="currentColor"
444
+ strokeWidth={2}
445
+ >
446
+ <line x1={12} y1={5} x2={12} y2={19} />
447
+ <line x1={5} y1={12} x2={19} y2={12} />
448
+ </svg>
449
+ New preset
450
+ </button>
451
+ </div>
452
+ )}
453
+ </div>
454
+ );
455
+ }
@@ -0,0 +1,69 @@
1
+ import { useCallback } from "react";
2
+ import { useTokens } from "@newtonedev/components";
3
+ import { srgbToHex } from "newtone";
4
+ import { OverviewView } from "../preview/OverviewView";
5
+ import { CategoryView } from "../preview/CategoryView";
6
+ import { ComponentDetailView } from "../preview/ComponentDetailView";
7
+ import type { PreviewView } from "../types";
8
+
9
+ interface PreviewWindowProps {
10
+ readonly view: PreviewView;
11
+ readonly selectedVariantId: string | null;
12
+ readonly propOverrides?: Record<string, unknown>;
13
+ readonly onNavigate: (view: PreviewView) => void;
14
+ readonly onSelectVariant: (variantId: string) => void;
15
+ }
16
+
17
+ export function PreviewWindow({
18
+ view,
19
+ selectedVariantId,
20
+ propOverrides,
21
+ onNavigate,
22
+ onSelectVariant,
23
+ }: PreviewWindowProps) {
24
+ const tokens = useTokens();
25
+
26
+ const handleNavigateToCategory = useCallback(
27
+ (categoryId: string) => onNavigate({ kind: "category", categoryId }),
28
+ [onNavigate],
29
+ );
30
+
31
+ const handleNavigateToComponent = useCallback(
32
+ (componentId: string) => onNavigate({ kind: "component", componentId }),
33
+ [onNavigate],
34
+ );
35
+
36
+ return (
37
+ <div
38
+ style={{
39
+ display: "flex",
40
+ flexDirection: "column",
41
+ height: "100%",
42
+ backgroundColor: srgbToHex(tokens.background.srgb),
43
+ }}
44
+ >
45
+ <div style={{ flex: 1, overflowY: "auto" }}>
46
+ {view.kind === "overview" && (
47
+ <OverviewView
48
+ onNavigateToCategory={handleNavigateToCategory}
49
+ onNavigateToComponent={handleNavigateToComponent}
50
+ />
51
+ )}
52
+ {view.kind === "category" && (
53
+ <CategoryView
54
+ categoryId={view.categoryId}
55
+ onNavigateToComponent={handleNavigateToComponent}
56
+ />
57
+ )}
58
+ {view.kind === "component" && (
59
+ <ComponentDetailView
60
+ componentId={view.componentId}
61
+ selectedVariantId={selectedVariantId}
62
+ propOverrides={propOverrides}
63
+ onSelectVariant={onSelectVariant}
64
+ />
65
+ )}
66
+ </div>
67
+ </div>
68
+ );
69
+ }