@newtonedev/editor 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/Editor.d.ts +1 -1
  2. package/dist/Editor.d.ts.map +1 -1
  3. package/dist/components/CodeBlock.d.ts.map +1 -1
  4. package/dist/components/ConfiguratorPanel.d.ts +17 -0
  5. package/dist/components/ConfiguratorPanel.d.ts.map +1 -0
  6. package/dist/components/FontPicker.d.ts +4 -2
  7. package/dist/components/FontPicker.d.ts.map +1 -1
  8. package/dist/components/PresetSelector.d.ts.map +1 -1
  9. package/dist/components/PreviewWindow.d.ts +9 -3
  10. package/dist/components/PreviewWindow.d.ts.map +1 -1
  11. package/dist/components/PrimaryNav.d.ts +7 -0
  12. package/dist/components/PrimaryNav.d.ts.map +1 -0
  13. package/dist/components/RightSidebar.d.ts +4 -1
  14. package/dist/components/RightSidebar.d.ts.map +1 -1
  15. package/dist/components/Sidebar.d.ts +1 -10
  16. package/dist/components/Sidebar.d.ts.map +1 -1
  17. package/dist/components/TableOfContents.d.ts +2 -1
  18. package/dist/components/TableOfContents.d.ts.map +1 -1
  19. package/dist/components/sections/DynamicRangeSection.d.ts.map +1 -1
  20. package/dist/components/sections/FontsSection.d.ts +3 -1
  21. package/dist/components/sections/FontsSection.d.ts.map +1 -1
  22. package/dist/hooks/useEditorState.d.ts +4 -1
  23. package/dist/hooks/useEditorState.d.ts.map +1 -1
  24. package/dist/index.cjs +2893 -2248
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.ts +2 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +2895 -2251
  29. package/dist/index.js.map +1 -1
  30. package/dist/preview/ComponentDetailView.d.ts +9 -2
  31. package/dist/preview/ComponentDetailView.d.ts.map +1 -1
  32. package/dist/preview/ComponentRenderer.d.ts +2 -1
  33. package/dist/preview/ComponentRenderer.d.ts.map +1 -1
  34. package/dist/preview/IconBrowserView.d.ts +7 -0
  35. package/dist/preview/IconBrowserView.d.ts.map +1 -0
  36. package/dist/types.d.ts +17 -0
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/utils/lookupFontMetrics.d.ts +19 -0
  39. package/dist/utils/lookupFontMetrics.d.ts.map +1 -0
  40. package/dist/utils/measureFonts.d.ts +18 -0
  41. package/dist/utils/measureFonts.d.ts.map +1 -0
  42. package/package.json +1 -1
  43. package/src/Editor.tsx +57 -11
  44. package/src/components/CodeBlock.tsx +42 -14
  45. package/src/components/ConfiguratorPanel.tsx +77 -0
  46. package/src/components/FontPicker.tsx +38 -29
  47. package/src/components/PresetSelector.tsx +8 -33
  48. package/src/components/PreviewWindow.tsx +20 -4
  49. package/src/components/PrimaryNav.tsx +76 -0
  50. package/src/components/RightSidebar.tsx +103 -40
  51. package/src/components/Sidebar.tsx +4 -211
  52. package/src/components/TableOfContents.tsx +41 -78
  53. package/src/components/sections/DynamicRangeSection.tsx +2 -225
  54. package/src/components/sections/FontsSection.tsx +61 -93
  55. package/src/hooks/useEditorState.ts +68 -9
  56. package/src/index.ts +2 -0
  57. package/src/preview/ComponentDetailView.tsx +576 -73
  58. package/src/preview/ComponentRenderer.tsx +6 -4
  59. package/src/preview/IconBrowserView.tsx +187 -0
  60. package/src/types.ts +15 -0
  61. package/src/utils/lookupFontMetrics.ts +52 -0
  62. package/src/utils/measureFonts.ts +41 -0
@@ -1,25 +1,36 @@
1
1
  import { useCallback } from "react";
2
2
  import { useTokens } from "@newtonedev/components";
3
3
  import { srgbToHex } from "newtone";
4
+ import type { TextRole } from "@newtonedev/fonts";
4
5
  import { OverviewView } from "../preview/OverviewView";
5
6
  import { CategoryView } from "../preview/CategoryView";
6
7
  import { ComponentDetailView } from "../preview/ComponentDetailView";
7
- import type { PreviewView } from "../types";
8
+ import type { PreviewView, EditorFontEntry } from "../types";
8
9
 
9
10
  interface PreviewWindowProps {
10
11
  readonly view: PreviewView;
11
12
  readonly selectedVariantId: string | null;
12
- readonly propOverrides?: Record<string, unknown>;
13
13
  readonly onNavigate: (view: PreviewView) => void;
14
14
  readonly onSelectVariant: (variantId: string) => void;
15
+ readonly propOverrides?: Record<string, unknown>;
16
+ readonly onPropOverride?: (name: string, value: unknown) => void;
17
+ readonly roleWeights?: Partial<Record<TextRole, number>>;
18
+ readonly onRoleWeightChange?: (role: TextRole, weight: number) => void;
19
+ readonly fontCatalog?: readonly EditorFontEntry[];
20
+ readonly scopeFontMap?: Record<string, string>;
15
21
  }
16
22
 
17
23
  export function PreviewWindow({
18
24
  view,
19
25
  selectedVariantId,
20
- propOverrides,
21
26
  onNavigate,
22
27
  onSelectVariant,
28
+ propOverrides,
29
+ onPropOverride,
30
+ roleWeights,
31
+ onRoleWeightChange,
32
+ fontCatalog,
33
+ scopeFontMap,
23
34
  }: PreviewWindowProps) {
24
35
  const tokens = useTokens();
25
36
 
@@ -59,8 +70,13 @@ export function PreviewWindow({
59
70
  <ComponentDetailView
60
71
  componentId={view.componentId}
61
72
  selectedVariantId={selectedVariantId}
62
- propOverrides={propOverrides}
63
73
  onSelectVariant={onSelectVariant}
74
+ propOverrides={propOverrides}
75
+ onPropOverride={onPropOverride}
76
+ roleWeights={roleWeights}
77
+ onRoleWeightChange={onRoleWeightChange}
78
+ fontCatalog={fontCatalog}
79
+ scopeFontMap={scopeFontMap}
64
80
  />
65
81
  )}
66
82
  </div>
@@ -0,0 +1,76 @@
1
+ import { useState } from "react";
2
+ import { useTokens, Icon, CATEGORIES } from "@newtonedev/components";
3
+ import { srgbToHex } from "newtone";
4
+
5
+ interface PrimaryNavProps {
6
+ readonly activeSectionId: string | null;
7
+ readonly onSelectSection: (sectionId: string) => void;
8
+ }
9
+
10
+ const NAV_WIDTH = 60;
11
+
12
+ export function PrimaryNav({ activeSectionId, onSelectSection }: PrimaryNavProps) {
13
+ const tokens = useTokens();
14
+ const [hoveredId, setHoveredId] = useState<string | null>(null);
15
+
16
+ const bg = srgbToHex(tokens.background.srgb);
17
+ const borderColor = srgbToHex(tokens.border.srgb);
18
+ const activeBg = srgbToHex(tokens.backgroundInteractive.srgb);
19
+ const iconColor = srgbToHex(tokens.textSecondary.srgb);
20
+ const activeIconColor = srgbToHex(tokens.textPrimary.srgb);
21
+
22
+ return (
23
+ <nav
24
+ aria-label="Section navigation"
25
+ style={{
26
+ width: NAV_WIDTH,
27
+ flexShrink: 0,
28
+ display: "flex",
29
+ flexDirection: "column",
30
+ alignItems: "center",
31
+ paddingTop: 12,
32
+ gap: 4,
33
+ backgroundColor: bg,
34
+ borderRight: `1px solid ${borderColor}`,
35
+ }}
36
+ >
37
+ {CATEGORIES.map((category) => {
38
+ const isActive = activeSectionId === category.id;
39
+ const isHovered = hoveredId === category.id;
40
+
41
+ return (
42
+ <button
43
+ key={category.id}
44
+ onClick={() => onSelectSection(category.id)}
45
+ onMouseEnter={() => setHoveredId(category.id)}
46
+ onMouseLeave={() => setHoveredId(null)}
47
+ title={category.name}
48
+ aria-current={isActive ? "page" : undefined}
49
+ style={{
50
+ display: "flex",
51
+ alignItems: "center",
52
+ justifyContent: "center",
53
+ width: 44,
54
+ height: 44,
55
+ borderRadius: 12,
56
+ border: "none",
57
+ backgroundColor: isActive
58
+ ? activeBg
59
+ : isHovered
60
+ ? `${borderColor}20`
61
+ : "transparent",
62
+ cursor: "pointer",
63
+ transition: "background-color 150ms",
64
+ }}
65
+ >
66
+ <Icon
67
+ name={category.icon ?? "circle"}
68
+ size={24}
69
+ color={isActive ? activeIconColor : iconColor}
70
+ />
71
+ </button>
72
+ );
73
+ })}
74
+ </nav>
75
+ );
76
+ }
@@ -1,8 +1,9 @@
1
1
  import { useCallback, type KeyboardEvent } from "react";
2
- import { useTokens, getComponent, generateComponentCode, Select } from "@newtonedev/components";
3
- import type { EditableProp } from "@newtonedev/components";
2
+ import { useTokens, getComponent, generateComponentCode, Select, NewtoneProvider, Icon } from "@newtonedev/components";
3
+ import type { EditableProp, NewtoneThemeConfig, ColorMode } from "@newtonedev/components";
4
4
  import { srgbToHex } from "newtone";
5
5
  import { CodeBlock } from "./CodeBlock";
6
+ import { ComponentRenderer } from "../preview/ComponentRenderer";
6
7
  import type { SidebarSelection } from "../types";
7
8
 
8
9
  interface RightSidebarProps {
@@ -12,6 +13,8 @@ interface RightSidebarProps {
12
13
  readonly onResetOverrides: () => void;
13
14
  readonly onClose: () => void;
14
15
  readonly onScopeToComponent: () => void;
16
+ readonly previewConfig: NewtoneThemeConfig;
17
+ readonly colorMode: ColorMode;
15
18
  }
16
19
 
17
20
  export function RightSidebar({
@@ -21,6 +24,8 @@ export function RightSidebar({
21
24
  onResetOverrides,
22
25
  onClose,
23
26
  onScopeToComponent,
27
+ previewConfig,
28
+ colorMode,
24
29
  }: RightSidebarProps) {
25
30
  const tokens = useTokens();
26
31
  const visible = selection !== null;
@@ -79,19 +84,7 @@ export function RightSidebar({
79
84
  alignItems: "center",
80
85
  }}
81
86
  >
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>
87
+ <Icon name="arrow_back" size={16} color={srgbToHex(tokens.textSecondary.srgb)} />
95
88
  </button>
96
89
  {selection.scope === "variant" && variant ? (
97
90
  <>
@@ -154,6 +147,27 @@ export function RightSidebar({
154
147
  padding: 16,
155
148
  }}
156
149
  >
150
+ {/* Live Preview */}
151
+ <div
152
+ style={{
153
+ marginBottom: 20,
154
+ borderRadius: 8,
155
+ border: `1px solid ${srgbToHex(tokens.border.srgb)}`,
156
+ overflow: "hidden",
157
+ }}
158
+ >
159
+ <NewtoneProvider
160
+ config={previewConfig}
161
+ initialMode={colorMode}
162
+ key={colorMode}
163
+ >
164
+ <PreviewSurface
165
+ componentId={selection.componentId}
166
+ propOverrides={propOverrides}
167
+ />
168
+ </NewtoneProvider>
169
+ </div>
170
+
157
171
  <h3
158
172
  style={{
159
173
  fontSize: 13,
@@ -222,6 +236,31 @@ export function RightSidebar({
222
236
  );
223
237
  }
224
238
 
239
+ function PreviewSurface({
240
+ componentId,
241
+ propOverrides,
242
+ }: {
243
+ readonly componentId: string;
244
+ readonly propOverrides: Record<string, unknown>;
245
+ }) {
246
+ const previewTokens = useTokens();
247
+
248
+ return (
249
+ <div
250
+ style={{
251
+ display: "flex",
252
+ alignItems: "center",
253
+ justifyContent: "center",
254
+ padding: 24,
255
+ height: 120,
256
+ backgroundColor: srgbToHex(previewTokens.backgroundElevated.srgb),
257
+ }}
258
+ >
259
+ <ComponentRenderer componentId={componentId} props={propOverrides} />
260
+ </div>
261
+ );
262
+ }
263
+
225
264
  function PropControl({
226
265
  prop,
227
266
  value,
@@ -257,14 +296,7 @@ function PropControl({
257
296
 
258
297
  return (
259
298
  <div>
260
- <div
261
- style={{
262
- display: "flex",
263
- alignItems: "center",
264
- justifyContent: "space-between",
265
- marginBottom: 4,
266
- }}
267
- >
299
+ <div style={{ marginBottom: 4 }}>
268
300
  <span
269
301
  style={{
270
302
  fontSize: 12,
@@ -274,15 +306,6 @@ function PropControl({
274
306
  >
275
307
  {prop.label}
276
308
  </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
309
  </div>
287
310
 
288
311
  {prop.control === "select" && prop.options && (
@@ -318,6 +341,54 @@ function PropControl({
318
341
  />
319
342
  )}
320
343
 
344
+ {prop.control === "discrete-slider" && prop.options && (() => {
345
+ const options = prop.options!;
346
+ const currentIndex = options.findIndex((o) => o.value === value);
347
+ const idx = currentIndex >= 0 ? currentIndex : 0;
348
+
349
+ return (
350
+ <div>
351
+ <input
352
+ type="range"
353
+ min={0}
354
+ max={options.length - 1}
355
+ step={1}
356
+ value={idx}
357
+ onChange={(e) => onChange(options[Number(e.target.value)].value)}
358
+ aria-label={prop.label}
359
+ style={{
360
+ width: "100%",
361
+ accentColor: srgbToHex(tokens.accent.fill.srgb),
362
+ cursor: "pointer",
363
+ }}
364
+ />
365
+ <div
366
+ style={{
367
+ display: "flex",
368
+ justifyContent: "space-between",
369
+ marginTop: 2,
370
+ }}
371
+ >
372
+ {options.map((o) => (
373
+ <span
374
+ key={String(o.value)}
375
+ style={{
376
+ fontSize: 11,
377
+ fontFamily: "'SF Mono', 'Fira Code', Menlo, monospace",
378
+ color: o.value === value
379
+ ? srgbToHex(tokens.textPrimary.srgb)
380
+ : srgbToHex(tokens.textTertiary.srgb),
381
+ fontWeight: o.value === value ? 600 : 400,
382
+ }}
383
+ >
384
+ {o.label}
385
+ </span>
386
+ ))}
387
+ </div>
388
+ </div>
389
+ );
390
+ })()}
391
+
321
392
  {prop.control === "toggle" && (
322
393
  <div
323
394
  role="switch"
@@ -359,14 +430,6 @@ function PropControl({
359
430
  }}
360
431
  />
361
432
  </div>
362
- <span
363
- style={{
364
- fontSize: 12,
365
- color: srgbToHex(tokens.textSecondary.srgb),
366
- }}
367
- >
368
- {value ? "true" : "false"}
369
- </span>
370
433
  </div>
371
434
  )}
372
435
  </div>
@@ -1,108 +1,11 @@
1
- import { useState } from "react";
2
1
  import { useTokens } from "@newtonedev/components";
3
- import type { ColorMode } from "@newtonedev/components";
4
2
  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
3
  import { PresetSelector } from "./PresetSelector";
16
4
  import type { Preset } from "../types";
17
5
 
18
6
  const SIDEBAR_WIDTH = 360;
19
7
 
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
8
  interface SidebarProps {
103
- readonly state: ConfiguratorState;
104
- readonly dispatch: (action: ConfiguratorAction) => void;
105
- readonly previewColors: readonly (readonly ColorResult[])[];
106
9
  readonly isDirty: boolean;
107
10
  readonly onRevert: () => void;
108
11
  readonly presets: readonly Preset[];
@@ -116,14 +19,9 @@ interface SidebarProps {
116
19
  presetId: string,
117
20
  name: string,
118
21
  ) => Promise<string>;
119
- readonly colorMode: ColorMode;
120
- readonly onColorModeChange: (mode: ColorMode) => void;
121
22
  }
122
23
 
123
24
  export function Sidebar({
124
- state,
125
- dispatch,
126
- previewColors,
127
25
  isDirty,
128
26
  onRevert,
129
27
  presets,
@@ -134,52 +32,11 @@ export function Sidebar({
134
32
  onRenamePreset,
135
33
  onDeletePreset,
136
34
  onDuplicatePreset,
137
- colorMode,
138
- onColorModeChange,
139
35
  }: SidebarProps) {
140
36
  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
37
 
146
38
  const borderColor = srgbToHex(tokens.border.srgb);
147
39
  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
40
 
184
41
  return (
185
42
  <div
@@ -193,7 +50,7 @@ export function Sidebar({
193
50
  backgroundColor: bgColor,
194
51
  }}
195
52
  >
196
- {/* Sticky Header */}
53
+ {/* Header */}
197
54
  <div
198
55
  style={{
199
56
  flexShrink: 0,
@@ -225,80 +82,16 @@ export function Sidebar({
225
82
  />
226
83
  </div>
227
84
 
228
- {/* Scrollable Accordion Area */}
85
+ {/* Content area (empty for now) */}
229
86
  <div
230
87
  style={{
231
88
  flex: 1,
232
89
  overflowY: "auto",
233
90
  overflowX: "hidden",
234
91
  }}
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>
92
+ />
300
93
 
301
- {/* Sticky Footer */}
94
+ {/* Footer */}
302
95
  <div
303
96
  style={{
304
97
  flexShrink: 0,