@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,374 @@
1
+ import { useCallback, type KeyboardEvent } from "react";
2
+ import { useTokens, getComponent, generateComponentCode, Select } from "@newtonedev/components";
3
+ import type { EditableProp } from "@newtonedev/components";
4
+ import { srgbToHex } from "newtone";
5
+ import { CodeBlock } from "./CodeBlock";
6
+ import type { SidebarSelection } from "../types";
7
+
8
+ interface RightSidebarProps {
9
+ readonly selection: SidebarSelection;
10
+ readonly propOverrides: Record<string, unknown>;
11
+ readonly onPropOverride: (name: string, value: unknown) => void;
12
+ readonly onResetOverrides: () => void;
13
+ readonly onClose: () => void;
14
+ readonly onScopeToComponent: () => void;
15
+ }
16
+
17
+ export function RightSidebar({
18
+ selection,
19
+ propOverrides,
20
+ onPropOverride,
21
+ onResetOverrides,
22
+ onClose,
23
+ onScopeToComponent,
24
+ }: RightSidebarProps) {
25
+ const tokens = useTokens();
26
+ const visible = selection !== null;
27
+
28
+ const component = selection ? getComponent(selection.componentId) : null;
29
+ const variant =
30
+ selection?.scope === "variant"
31
+ ? component?.variants.find((v) => v.id === selection.variantId)
32
+ : null;
33
+
34
+ const code = component ? generateComponentCode(component, propOverrides) : "";
35
+
36
+ return (
37
+ <aside
38
+ aria-label="Component inspector"
39
+ style={{
40
+ position: "absolute",
41
+ inset: 0,
42
+ zIndex: 1,
43
+ transform: visible ? "translateX(0)" : "translateX(100%)",
44
+ opacity: visible ? 1 : 0,
45
+ transition: "transform 200ms ease, opacity 150ms ease",
46
+ display: "flex",
47
+ flexDirection: "column",
48
+ backgroundColor: srgbToHex(tokens.background.srgb),
49
+ borderLeft: `1px solid ${srgbToHex(tokens.border.srgb)}`,
50
+ }}
51
+ >
52
+ {selection && component && (
53
+ <>
54
+ {/* Header */}
55
+ <div
56
+ style={{
57
+ display: "flex",
58
+ alignItems: "center",
59
+ justifyContent: "space-between",
60
+ padding: "12px 16px",
61
+ borderBottom: `1px solid ${srgbToHex(tokens.border.srgb)}`,
62
+ flexShrink: 0,
63
+ }}
64
+ >
65
+ <div style={{ display: "flex", alignItems: "center", gap: 8, minWidth: 0 }}>
66
+ <button
67
+ onClick={onClose}
68
+ aria-label="Back to controls"
69
+ style={{
70
+ background: "none",
71
+ border: "none",
72
+ cursor: "pointer",
73
+ padding: 4,
74
+ color: srgbToHex(tokens.textSecondary.srgb),
75
+ fontSize: 16,
76
+ lineHeight: 1,
77
+ flexShrink: 0,
78
+ display: "flex",
79
+ alignItems: "center",
80
+ }}
81
+ >
82
+ <svg
83
+ width={16}
84
+ height={16}
85
+ viewBox="0 0 24 24"
86
+ fill="none"
87
+ stroke="currentColor"
88
+ strokeWidth={2}
89
+ strokeLinecap="round"
90
+ strokeLinejoin="round"
91
+ >
92
+ <line x1="19" y1="12" x2="5" y2="12" />
93
+ <polyline points="12 19 5 12 12 5" />
94
+ </svg>
95
+ </button>
96
+ {selection.scope === "variant" && variant ? (
97
+ <>
98
+ <button
99
+ onClick={onScopeToComponent}
100
+ aria-label={`Back to ${component.name} overview`}
101
+ style={{
102
+ background: "none",
103
+ border: "none",
104
+ cursor: "pointer",
105
+ padding: 0,
106
+ fontSize: 14,
107
+ fontWeight: 500,
108
+ color: srgbToHex(tokens.interactive.srgb),
109
+ whiteSpace: "nowrap",
110
+ }}
111
+ >
112
+ {component.name}
113
+ </button>
114
+ <span
115
+ style={{
116
+ fontSize: 13,
117
+ color: srgbToHex(tokens.textSecondary.srgb),
118
+ }}
119
+ >
120
+ /
121
+ </span>
122
+ <span
123
+ style={{
124
+ fontSize: 14,
125
+ fontWeight: 600,
126
+ color: srgbToHex(tokens.textPrimary.srgb),
127
+ whiteSpace: "nowrap",
128
+ overflow: "hidden",
129
+ textOverflow: "ellipsis",
130
+ }}
131
+ >
132
+ {variant.label}
133
+ </span>
134
+ </>
135
+ ) : (
136
+ <span
137
+ style={{
138
+ fontSize: 14,
139
+ fontWeight: 600,
140
+ color: srgbToHex(tokens.textPrimary.srgb),
141
+ }}
142
+ >
143
+ {component.name}
144
+ </span>
145
+ )}
146
+ </div>
147
+ </div>
148
+
149
+ {/* Body */}
150
+ <div
151
+ style={{
152
+ flex: 1,
153
+ overflowY: "auto",
154
+ padding: 16,
155
+ }}
156
+ >
157
+ <h3
158
+ style={{
159
+ fontSize: 13,
160
+ fontWeight: 600,
161
+ color: srgbToHex(tokens.textSecondary.srgb),
162
+ textTransform: "uppercase",
163
+ letterSpacing: 0.5,
164
+ margin: 0,
165
+ marginBottom: 16,
166
+ }}
167
+ >
168
+ Properties
169
+ </h3>
170
+
171
+ <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
172
+ {component.editableProps.map((prop) => (
173
+ <PropControl
174
+ key={prop.name}
175
+ prop={prop}
176
+ value={propOverrides[prop.name] ?? prop.defaultValue}
177
+ onChange={(value) => onPropOverride(prop.name, value)}
178
+ />
179
+ ))}
180
+ </div>
181
+
182
+ {selection.scope === "variant" && (
183
+ <button
184
+ onClick={onResetOverrides}
185
+ aria-label="Reset properties to variant defaults"
186
+ style={{
187
+ marginTop: 20,
188
+ width: "100%",
189
+ padding: "8px 12px",
190
+ borderRadius: 6,
191
+ border: `1px solid ${srgbToHex(tokens.border.srgb)}`,
192
+ backgroundColor: "transparent",
193
+ color: srgbToHex(tokens.textSecondary.srgb),
194
+ fontSize: 13,
195
+ cursor: "pointer",
196
+ }}
197
+ >
198
+ Reset to variant defaults
199
+ </button>
200
+ )}
201
+
202
+ <h3
203
+ style={{
204
+ fontSize: 13,
205
+ fontWeight: 600,
206
+ color: srgbToHex(tokens.textSecondary.srgb),
207
+ textTransform: "uppercase",
208
+ letterSpacing: 0.5,
209
+ margin: 0,
210
+ marginTop: 24,
211
+ marginBottom: 12,
212
+ }}
213
+ >
214
+ Code
215
+ </h3>
216
+
217
+ <CodeBlock code={code} />
218
+ </div>
219
+ </>
220
+ )}
221
+ </aside>
222
+ );
223
+ }
224
+
225
+ function PropControl({
226
+ prop,
227
+ value,
228
+ onChange,
229
+ }: {
230
+ readonly prop: EditableProp;
231
+ readonly value: unknown;
232
+ readonly onChange: (value: unknown) => void;
233
+ }) {
234
+ const tokens = useTokens();
235
+
236
+ const handleToggleKeyDown = useCallback(
237
+ (e: KeyboardEvent) => {
238
+ if (e.key === " " || e.key === "Enter") {
239
+ e.preventDefault();
240
+ onChange(!value);
241
+ }
242
+ },
243
+ [onChange, value],
244
+ );
245
+
246
+ const inputStyle = {
247
+ width: "100%",
248
+ padding: "6px 10px",
249
+ borderRadius: 6,
250
+ border: `1px solid ${srgbToHex(tokens.border.srgb)}`,
251
+ backgroundColor: srgbToHex(tokens.backgroundSunken.srgb),
252
+ color: srgbToHex(tokens.textPrimary.srgb),
253
+ fontSize: 13,
254
+ fontFamily: "'SF Mono', 'Fira Code', Menlo, monospace",
255
+ boxSizing: "border-box" as const,
256
+ };
257
+
258
+ return (
259
+ <div>
260
+ <div
261
+ style={{
262
+ display: "flex",
263
+ alignItems: "center",
264
+ justifyContent: "space-between",
265
+ marginBottom: 4,
266
+ }}
267
+ >
268
+ <span
269
+ style={{
270
+ fontSize: 12,
271
+ fontWeight: 500,
272
+ color: srgbToHex(tokens.textPrimary.srgb),
273
+ }}
274
+ >
275
+ {prop.label}
276
+ </span>
277
+ <span
278
+ style={{
279
+ fontSize: 11,
280
+ color: srgbToHex(tokens.textSecondary.srgb),
281
+ fontFamily: "'SF Mono', 'Fira Code', Menlo, monospace",
282
+ }}
283
+ >
284
+ {prop.control}
285
+ </span>
286
+ </div>
287
+
288
+ {prop.control === "select" && prop.options && (
289
+ <Select
290
+ options={prop.options.map((o) => ({
291
+ label: o.label,
292
+ value: String(o.value),
293
+ }))}
294
+ value={String(value)}
295
+ onValueChange={(v) => {
296
+ const option = prop.options!.find((o) => String(o.value) === v);
297
+ if (option) onChange(option.value);
298
+ }}
299
+ size="sm"
300
+ />
301
+ )}
302
+
303
+ {prop.control === "text" && (
304
+ <input
305
+ type="text"
306
+ value={String(value)}
307
+ onChange={(e) => onChange(e.target.value)}
308
+ style={inputStyle}
309
+ />
310
+ )}
311
+
312
+ {prop.control === "number" && (
313
+ <input
314
+ type="number"
315
+ value={Number(value)}
316
+ onChange={(e) => onChange(Number(e.target.value))}
317
+ style={inputStyle}
318
+ />
319
+ )}
320
+
321
+ {prop.control === "toggle" && (
322
+ <div
323
+ role="switch"
324
+ aria-checked={!!value}
325
+ aria-label={prop.label}
326
+ tabIndex={0}
327
+ onClick={() => onChange(!value)}
328
+ onKeyDown={handleToggleKeyDown}
329
+ style={{
330
+ display: "inline-flex",
331
+ alignItems: "center",
332
+ gap: 8,
333
+ cursor: "pointer",
334
+ }}
335
+ >
336
+ <div
337
+ style={{
338
+ width: 36,
339
+ height: 20,
340
+ borderRadius: 10,
341
+ backgroundColor: value
342
+ ? srgbToHex(tokens.interactive.srgb)
343
+ : srgbToHex(tokens.border.srgb),
344
+ position: "relative",
345
+ transition: "background-color 150ms ease",
346
+ flexShrink: 0,
347
+ }}
348
+ >
349
+ <div
350
+ style={{
351
+ width: 16,
352
+ height: 16,
353
+ borderRadius: 8,
354
+ backgroundColor: "#fff",
355
+ position: "absolute",
356
+ top: 2,
357
+ left: value ? 18 : 2,
358
+ transition: "left 150ms ease",
359
+ }}
360
+ />
361
+ </div>
362
+ <span
363
+ style={{
364
+ fontSize: 12,
365
+ color: srgbToHex(tokens.textSecondary.srgb),
366
+ }}
367
+ >
368
+ {value ? "true" : "false"}
369
+ </span>
370
+ </div>
371
+ )}
372
+ </div>
373
+ );
374
+ }
@@ -0,0 +1,332 @@
1
+ import { useState } from "react";
2
+ import { useTokens } from "@newtonedev/components";
3
+ import type { ColorMode } from "@newtonedev/components";
4
+ import { srgbToHex } from "newtone";
5
+ import type { ColorResult } from "newtone";
6
+ import type { ConfiguratorState } from "@newtonedev/configurator";
7
+ import type { ConfiguratorAction } from "@newtonedev/configurator";
8
+ import {
9
+ ColorsSection,
10
+ DynamicRangeSection,
11
+ IconsSection,
12
+ FontsSection,
13
+ OthersSection,
14
+ } from "./sections";
15
+ import { PresetSelector } from "./PresetSelector";
16
+ import type { Preset } from "../types";
17
+
18
+ const SIDEBAR_WIDTH = 360;
19
+
20
+ const ACCORDION_SECTIONS = [
21
+ { id: "dynamic-range", label: "Dynamic Range" },
22
+ { id: "colors", label: "Colors" },
23
+ { id: "fonts", label: "Fonts" },
24
+ { id: "icons", label: "Icons" },
25
+ { id: "others", label: "Others" },
26
+ ] as const;
27
+
28
+ function SectionIcon({ id }: { readonly id: string }) {
29
+ const props = {
30
+ width: 16,
31
+ height: 16,
32
+ viewBox: "0 0 24 24",
33
+ fill: "none",
34
+ stroke: "currentColor",
35
+ strokeWidth: 2,
36
+ strokeLinecap: "round" as const,
37
+ strokeLinejoin: "round" as const,
38
+ };
39
+
40
+ switch (id) {
41
+ case "dynamic-range":
42
+ // Sun/contrast icon
43
+ return (
44
+ <svg {...props}>
45
+ <circle cx="12" cy="12" r="5" />
46
+ <line x1="12" y1="1" x2="12" y2="3" />
47
+ <line x1="12" y1="21" x2="12" y2="23" />
48
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
49
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
50
+ <line x1="1" y1="12" x2="3" y2="12" />
51
+ <line x1="21" y1="12" x2="23" y2="12" />
52
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
53
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
54
+ </svg>
55
+ );
56
+ case "colors":
57
+ // Palette/droplet icon
58
+ return (
59
+ <svg {...props}>
60
+ <path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z" />
61
+ </svg>
62
+ );
63
+ case "fonts":
64
+ // Type/text icon
65
+ return (
66
+ <svg {...props}>
67
+ <polyline points="4 7 4 4 20 4 20 7" />
68
+ <line x1="9" y1="20" x2="15" y2="20" />
69
+ <line x1="12" y1="4" x2="12" y2="20" />
70
+ </svg>
71
+ );
72
+ case "icons":
73
+ // Grid icon
74
+ return (
75
+ <svg {...props}>
76
+ <rect x="3" y="3" width="7" height="7" />
77
+ <rect x="14" y="3" width="7" height="7" />
78
+ <rect x="3" y="14" width="7" height="7" />
79
+ <rect x="14" y="14" width="7" height="7" />
80
+ </svg>
81
+ );
82
+ case "others":
83
+ // Sliders icon
84
+ return (
85
+ <svg {...props}>
86
+ <line x1="4" y1="21" x2="4" y2="14" />
87
+ <line x1="4" y1="10" x2="4" y2="3" />
88
+ <line x1="12" y1="21" x2="12" y2="12" />
89
+ <line x1="12" y1="8" x2="12" y2="3" />
90
+ <line x1="20" y1="21" x2="20" y2="16" />
91
+ <line x1="20" y1="12" x2="20" y2="3" />
92
+ <line x1="1" y1="14" x2="7" y2="14" />
93
+ <line x1="9" y1="8" x2="15" y2="8" />
94
+ <line x1="17" y1="16" x2="23" y2="16" />
95
+ </svg>
96
+ );
97
+ default:
98
+ return null;
99
+ }
100
+ }
101
+
102
+ interface SidebarProps {
103
+ readonly state: ConfiguratorState;
104
+ readonly dispatch: (action: ConfiguratorAction) => void;
105
+ readonly previewColors: readonly (readonly ColorResult[])[];
106
+ readonly isDirty: boolean;
107
+ readonly onRevert: () => void;
108
+ readonly presets: readonly Preset[];
109
+ readonly activePresetId: string;
110
+ readonly publishedPresetId: string | null;
111
+ readonly onSwitchPreset: (presetId: string) => void;
112
+ readonly onCreatePreset: (name: string) => Promise<string>;
113
+ readonly onRenamePreset: (presetId: string, name: string) => void;
114
+ readonly onDeletePreset: (presetId: string) => Promise<void>;
115
+ readonly onDuplicatePreset: (
116
+ presetId: string,
117
+ name: string,
118
+ ) => Promise<string>;
119
+ readonly colorMode: ColorMode;
120
+ readonly onColorModeChange: (mode: ColorMode) => void;
121
+ }
122
+
123
+ export function Sidebar({
124
+ state,
125
+ dispatch,
126
+ previewColors,
127
+ isDirty,
128
+ onRevert,
129
+ presets,
130
+ activePresetId,
131
+ publishedPresetId,
132
+ onSwitchPreset,
133
+ onCreatePreset,
134
+ onRenamePreset,
135
+ onDeletePreset,
136
+ onDuplicatePreset,
137
+ colorMode,
138
+ onColorModeChange,
139
+ }: SidebarProps) {
140
+ const tokens = useTokens();
141
+ const [openSections, setOpenSections] = useState<Set<string>>(
142
+ new Set(["dynamic-range", "colors"]),
143
+ );
144
+ const [hoveredSectionId, setHoveredSectionId] = useState<string | null>(null);
145
+
146
+ const borderColor = srgbToHex(tokens.border.srgb);
147
+ const bgColor = srgbToHex(tokens.background.srgb);
148
+ const hoverBg = `${borderColor}10`;
149
+
150
+ const toggleSection = (id: string) => {
151
+ setOpenSections((prev) => {
152
+ const next = new Set(prev);
153
+ if (next.has(id)) next.delete(id);
154
+ else next.add(id);
155
+ return next;
156
+ });
157
+ };
158
+
159
+ const renderSectionContent = (sectionId: string) => {
160
+ switch (sectionId) {
161
+ case "dynamic-range":
162
+ return <DynamicRangeSection state={state} dispatch={dispatch} />;
163
+ case "colors":
164
+ return (
165
+ <ColorsSection
166
+ state={state}
167
+ dispatch={dispatch}
168
+ previewColors={previewColors}
169
+ colorMode={colorMode}
170
+ onColorModeChange={onColorModeChange}
171
+ />
172
+ );
173
+ case "icons":
174
+ return <IconsSection state={state} dispatch={dispatch} />;
175
+ case "fonts":
176
+ return <FontsSection state={state} dispatch={dispatch} />;
177
+ case "others":
178
+ return <OthersSection state={state} dispatch={dispatch} />;
179
+ default:
180
+ return null;
181
+ }
182
+ };
183
+
184
+ return (
185
+ <div
186
+ style={{
187
+ width: SIDEBAR_WIDTH,
188
+ flexShrink: 0,
189
+ display: "flex",
190
+ flexDirection: "column",
191
+ height: "100vh",
192
+ borderLeft: `1px solid ${borderColor}`,
193
+ backgroundColor: bgColor,
194
+ }}
195
+ >
196
+ {/* Sticky Header */}
197
+ <div
198
+ style={{
199
+ flexShrink: 0,
200
+ padding: "16px 20px",
201
+ borderBottom: `1px solid ${borderColor}`,
202
+ display: "flex",
203
+ alignItems: "center",
204
+ justifyContent: "space-between",
205
+ }}
206
+ >
207
+ <span
208
+ style={{
209
+ fontSize: 16,
210
+ fontWeight: 700,
211
+ color: srgbToHex(tokens.textPrimary.srgb),
212
+ }}
213
+ >
214
+ newtone
215
+ </span>
216
+ <PresetSelector
217
+ presets={presets}
218
+ activePresetId={activePresetId}
219
+ publishedPresetId={publishedPresetId}
220
+ onSwitchPreset={onSwitchPreset}
221
+ onCreatePreset={onCreatePreset}
222
+ onRenamePreset={onRenamePreset}
223
+ onDeletePreset={onDeletePreset}
224
+ onDuplicatePreset={onDuplicatePreset}
225
+ />
226
+ </div>
227
+
228
+ {/* Scrollable Accordion Area */}
229
+ <div
230
+ style={{
231
+ flex: 1,
232
+ overflowY: "auto",
233
+ overflowX: "hidden",
234
+ }}
235
+ >
236
+ {ACCORDION_SECTIONS.map((section) => {
237
+ const isOpen = openSections.has(section.id);
238
+ const isHovered = hoveredSectionId === section.id;
239
+
240
+ return (
241
+ <div key={section.id}>
242
+ <button
243
+ onClick={() => toggleSection(section.id)}
244
+ onMouseEnter={() => setHoveredSectionId(section.id)}
245
+ onMouseLeave={() => setHoveredSectionId(null)}
246
+ aria-expanded={isOpen}
247
+ aria-controls={`section-${section.id}`}
248
+ style={{
249
+ display: "flex",
250
+ alignItems: "center",
251
+ justifyContent: "space-between",
252
+ width: "100%",
253
+ padding: "12px 20px",
254
+ border: "none",
255
+ borderBottom: `1px solid ${borderColor}`,
256
+ background: isHovered ? hoverBg : "none",
257
+ cursor: "pointer",
258
+ fontSize: 14,
259
+ fontWeight: 500,
260
+ color: srgbToHex(tokens.textPrimary.srgb),
261
+ transition: "background-color 100ms ease",
262
+ }}
263
+ >
264
+ <span style={{ display: "flex", alignItems: "center", gap: 8 }}>
265
+ <SectionIcon id={section.id} />
266
+ {section.label}
267
+ </span>
268
+ <svg
269
+ width={12}
270
+ height={12}
271
+ viewBox="0 0 24 24"
272
+ fill="none"
273
+ stroke="currentColor"
274
+ strokeWidth={2}
275
+ style={{
276
+ transform: isOpen ? "rotate(180deg)" : "none",
277
+ transition: "transform 150ms ease",
278
+ }}
279
+ >
280
+ <polyline points="6 9 12 15 18 9" />
281
+ </svg>
282
+ </button>
283
+ {isOpen && (
284
+ <div
285
+ id={`section-${section.id}`}
286
+ role="region"
287
+ aria-label={section.label}
288
+ style={{
289
+ padding: "16px 20px",
290
+ borderBottom: `1px solid ${borderColor}`,
291
+ }}
292
+ >
293
+ {renderSectionContent(section.id)}
294
+ </div>
295
+ )}
296
+ </div>
297
+ );
298
+ })}
299
+ </div>
300
+
301
+ {/* Sticky Footer */}
302
+ <div
303
+ style={{
304
+ flexShrink: 0,
305
+ padding: "12px 20px",
306
+ borderTop: `1px solid ${borderColor}`,
307
+ }}
308
+ >
309
+ <button
310
+ disabled={!isDirty}
311
+ onClick={onRevert}
312
+ aria-label="Revert all changes to the last saved version"
313
+ style={{
314
+ width: "100%",
315
+ padding: "8px 16px",
316
+ borderRadius: 6,
317
+ border: `1px solid ${borderColor}`,
318
+ backgroundColor: "transparent",
319
+ color: isDirty
320
+ ? srgbToHex(tokens.textPrimary.srgb)
321
+ : srgbToHex(tokens.textSecondary.srgb),
322
+ fontSize: 13,
323
+ cursor: isDirty ? "pointer" : "not-allowed",
324
+ opacity: isDirty ? 1 : 0.5,
325
+ }}
326
+ >
327
+ Revert Changes
328
+ </button>
329
+ </div>
330
+ </div>
331
+ );
332
+ }