@antigenic-oss/paint 0.1.0

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 (158) hide show
  1. package/LICENSE +178 -0
  2. package/NOTICE +4 -0
  3. package/README.md +180 -0
  4. package/bin/paint.js +266 -0
  5. package/next-env.d.ts +6 -0
  6. package/next.config.ts +19 -0
  7. package/package.json +81 -0
  8. package/postcss.config.mjs +8 -0
  9. package/public/dev-editor-inspector.js +1872 -0
  10. package/src/app/api/claude/analyze/route.ts +319 -0
  11. package/src/app/api/claude/apply/route.ts +185 -0
  12. package/src/app/api/claude/pick-folder/route.ts +64 -0
  13. package/src/app/api/claude/scan/route.ts +221 -0
  14. package/src/app/api/claude/status/route.ts +55 -0
  15. package/src/app/api/project/scan/route.ts +634 -0
  16. package/src/app/api/project-scan/css-variables/route.ts +238 -0
  17. package/src/app/api/project-scan/route.ts +40 -0
  18. package/src/app/api/project-scan/tailwind-config/route.ts +172 -0
  19. package/src/app/api/proxy/[[...path]]/route.ts +2400 -0
  20. package/src/app/docs/DocsClient.tsx +322 -0
  21. package/src/app/docs/layout.tsx +7 -0
  22. package/src/app/docs/page.tsx +855 -0
  23. package/src/app/globals.css +176 -0
  24. package/src/app/layout.tsx +19 -0
  25. package/src/app/page.tsx +46 -0
  26. package/src/bridge/api-handlers.ts +885 -0
  27. package/src/bridge/proxy-handler.ts +329 -0
  28. package/src/bridge/server.ts +113 -0
  29. package/src/components/BreakpointTabs.tsx +72 -0
  30. package/src/components/ChangeSummaryModal.tsx +267 -0
  31. package/src/components/ConnectModal.tsx +994 -0
  32. package/src/components/Editor.tsx +90 -0
  33. package/src/components/PageSelector.tsx +208 -0
  34. package/src/components/PreviewFrame.tsx +299 -0
  35. package/src/components/ProjectFolderBanner.tsx +91 -0
  36. package/src/components/ResponsiveToolbar.tsx +222 -0
  37. package/src/components/TargetSelector.tsx +243 -0
  38. package/src/components/TopBar.tsx +315 -0
  39. package/src/components/common/CollapsibleSection.tsx +36 -0
  40. package/src/components/common/ColorPicker.tsx +920 -0
  41. package/src/components/common/EditablePre.tsx +136 -0
  42. package/src/components/common/ErrorBoundary.tsx +65 -0
  43. package/src/components/common/ResizablePanel.tsx +83 -0
  44. package/src/components/common/ScanAnimation.tsx +76 -0
  45. package/src/components/common/ToastContainer.tsx +97 -0
  46. package/src/components/common/UnitInput.tsx +77 -0
  47. package/src/components/common/VariableColorPicker.tsx +622 -0
  48. package/src/components/left-panel/AddElementPanel.tsx +237 -0
  49. package/src/components/left-panel/ComponentsPanel.tsx +609 -0
  50. package/src/components/left-panel/IconSidebar.tsx +99 -0
  51. package/src/components/left-panel/LayerNode.tsx +874 -0
  52. package/src/components/left-panel/LayerSearch.tsx +23 -0
  53. package/src/components/left-panel/LayersPanel.tsx +52 -0
  54. package/src/components/left-panel/LeftPanel.tsx +122 -0
  55. package/src/components/left-panel/PagesPanel.tsx +114 -0
  56. package/src/components/left-panel/icons.tsx +162 -0
  57. package/src/components/left-panel/terminal/ScanOverlay.tsx +66 -0
  58. package/src/components/left-panel/terminal/TerminalPanel.tsx +217 -0
  59. package/src/components/right-panel/ElementLogBox.tsx +248 -0
  60. package/src/components/right-panel/PanelTabs.tsx +83 -0
  61. package/src/components/right-panel/RightPanel.tsx +41 -0
  62. package/src/components/right-panel/changes/AiScanResultPanel.tsx +285 -0
  63. package/src/components/right-panel/changes/ChangeEntry.tsx +59 -0
  64. package/src/components/right-panel/changes/ChangelogActions.tsx +105 -0
  65. package/src/components/right-panel/changes/ChangesPanel.tsx +1474 -0
  66. package/src/components/right-panel/claude/ApplyConfirmModal.tsx +376 -0
  67. package/src/components/right-panel/claude/ClaudeErrorState.tsx +125 -0
  68. package/src/components/right-panel/claude/ClaudeIntegrationPanel.tsx +482 -0
  69. package/src/components/right-panel/claude/ClaudeProgressIndicator.tsx +76 -0
  70. package/src/components/right-panel/claude/DiffCard.tsx +130 -0
  71. package/src/components/right-panel/claude/DiffViewer.tsx +54 -0
  72. package/src/components/right-panel/claude/ProjectRootSelector.tsx +275 -0
  73. package/src/components/right-panel/claude/ResultsSummary.tsx +119 -0
  74. package/src/components/right-panel/claude/SetupFlow.tsx +315 -0
  75. package/src/components/right-panel/console/ConsolePanel.tsx +209 -0
  76. package/src/components/right-panel/design/AppearanceSection.tsx +703 -0
  77. package/src/components/right-panel/design/BackgroundSection.tsx +516 -0
  78. package/src/components/right-panel/design/BorderSection.tsx +161 -0
  79. package/src/components/right-panel/design/CSSRawView.tsx +412 -0
  80. package/src/components/right-panel/design/DesignCSSTabToggle.tsx +51 -0
  81. package/src/components/right-panel/design/DesignPanel.tsx +275 -0
  82. package/src/components/right-panel/design/ElementBreadcrumb.tsx +51 -0
  83. package/src/components/right-panel/design/GradientEditor.tsx +726 -0
  84. package/src/components/right-panel/design/LayoutSection.tsx +1948 -0
  85. package/src/components/right-panel/design/PositionSection.tsx +865 -0
  86. package/src/components/right-panel/design/PropertiesSection.tsx +86 -0
  87. package/src/components/right-panel/design/SVGSection.tsx +361 -0
  88. package/src/components/right-panel/design/ShadowBlurSection.tsx +227 -0
  89. package/src/components/right-panel/design/SizeSection.tsx +183 -0
  90. package/src/components/right-panel/design/TextSection.tsx +719 -0
  91. package/src/components/right-panel/design/icons.tsx +948 -0
  92. package/src/components/right-panel/design/inputs/BoxModelPreview.tsx +467 -0
  93. package/src/components/right-panel/design/inputs/ColorInput.tsx +43 -0
  94. package/src/components/right-panel/design/inputs/CompactInput.tsx +333 -0
  95. package/src/components/right-panel/design/inputs/DraggableLabel.tsx +118 -0
  96. package/src/components/right-panel/design/inputs/IconToggleGroup.tsx +54 -0
  97. package/src/components/right-panel/design/inputs/LinkedInputPair.tsx +174 -0
  98. package/src/components/right-panel/design/inputs/SectionHeader.tsx +79 -0
  99. package/src/components/right-panel/variables/VariablesPanel.tsx +388 -0
  100. package/src/hooks/useBridge.ts +95 -0
  101. package/src/hooks/useChangeTracker.ts +563 -0
  102. package/src/hooks/useClaudeAPI.ts +118 -0
  103. package/src/hooks/useDOMTree.ts +25 -0
  104. package/src/hooks/useKeyboardShortcuts.ts +76 -0
  105. package/src/hooks/usePostMessage.ts +589 -0
  106. package/src/hooks/useProjectScan.ts +204 -0
  107. package/src/hooks/useResizable.ts +20 -0
  108. package/src/hooks/useSelectedElement.ts +51 -0
  109. package/src/hooks/useTargetUrl.ts +81 -0
  110. package/src/inspector/DOMTraverser.ts +71 -0
  111. package/src/inspector/ElementSelector.ts +23 -0
  112. package/src/inspector/HoverHighlighter.ts +54 -0
  113. package/src/inspector/SelectionHighlighter.ts +27 -0
  114. package/src/inspector/StyleExtractor.ts +19 -0
  115. package/src/inspector/inspector.ts +17 -0
  116. package/src/inspector/messaging.ts +30 -0
  117. package/src/lib/apiBase.ts +15 -0
  118. package/src/lib/classifyElement.ts +430 -0
  119. package/src/lib/claude-bin.ts +197 -0
  120. package/src/lib/claude-stream.ts +158 -0
  121. package/src/lib/clientProjectScanner.ts +344 -0
  122. package/src/lib/componentMatcher.ts +156 -0
  123. package/src/lib/constants.ts +573 -0
  124. package/src/lib/cssVariableUtils.ts +409 -0
  125. package/src/lib/diffParser.ts +206 -0
  126. package/src/lib/folderPicker.ts +84 -0
  127. package/src/lib/gradientParser.ts +160 -0
  128. package/src/lib/projectScanner.ts +355 -0
  129. package/src/lib/promptBuilder.ts +402 -0
  130. package/src/lib/shadowParser.ts +124 -0
  131. package/src/lib/tailwindClassParser.ts +248 -0
  132. package/src/lib/textShadowUtils.ts +106 -0
  133. package/src/lib/utils.ts +299 -0
  134. package/src/lib/validatePath.ts +40 -0
  135. package/src/proxy.ts +92 -0
  136. package/src/server/terminal-server.ts +104 -0
  137. package/src/store/changeSlice.ts +288 -0
  138. package/src/store/claudeSlice.ts +222 -0
  139. package/src/store/componentSlice.ts +90 -0
  140. package/src/store/consoleSlice.ts +51 -0
  141. package/src/store/cssVariableSlice.ts +94 -0
  142. package/src/store/elementSlice.ts +78 -0
  143. package/src/store/index.ts +35 -0
  144. package/src/store/terminalSlice.ts +30 -0
  145. package/src/store/treeSlice.ts +69 -0
  146. package/src/store/uiSlice.ts +327 -0
  147. package/src/types/changelog.ts +49 -0
  148. package/src/types/claude.ts +131 -0
  149. package/src/types/component.ts +49 -0
  150. package/src/types/cssVariables.ts +18 -0
  151. package/src/types/element.ts +21 -0
  152. package/src/types/file-system-access.d.ts +27 -0
  153. package/src/types/gradient.ts +12 -0
  154. package/src/types/messages.ts +392 -0
  155. package/src/types/shadow.ts +8 -0
  156. package/src/types/tree.ts +9 -0
  157. package/tsconfig.json +42 -0
  158. package/tsconfig.server.json +12 -0
@@ -0,0 +1,1948 @@
1
+ 'use client'
2
+
3
+ import { useState, useRef, useEffect, useCallback } from 'react'
4
+ import { useEditorStore } from '@/store'
5
+ import { SectionHeader } from '@/components/right-panel/design/inputs/SectionHeader'
6
+ import { LinkedInputPair } from '@/components/right-panel/design/inputs/LinkedInputPair'
7
+ import { BoxModelPreview } from '@/components/right-panel/design/inputs/BoxModelPreview'
8
+ import { useChangeTracker } from '@/hooks/useChangeTracker'
9
+ import { parseCSSValue, formatCSSValue } from '@/lib/utils'
10
+
11
+ // ─── Types ─────────────────────────────────────────────────────────
12
+
13
+ type DisplayMode =
14
+ | 'block'
15
+ | 'flex'
16
+ | 'grid'
17
+ | 'none'
18
+ | 'inline-block'
19
+ | 'inline-flex'
20
+ | 'inline-grid'
21
+ | 'inline'
22
+
23
+ interface DropdownOption {
24
+ value: string
25
+ label: string
26
+ icon?: React.ReactNode
27
+ desc?: string
28
+ }
29
+
30
+ // ─── Constants ─────────────────────────────────────────────────────
31
+
32
+ const DISPLAY_LABELS: Record<DisplayMode, string> = {
33
+ block: 'Block',
34
+ flex: 'Flex',
35
+ grid: 'Grid',
36
+ none: 'None',
37
+ 'inline-block': 'In-blk',
38
+ 'inline-flex': 'In-flex',
39
+ 'inline-grid': 'In-grid',
40
+ inline: 'Inline',
41
+ }
42
+
43
+ const DROPDOWN_DISPLAYS: {
44
+ value: DisplayMode
45
+ label: string
46
+ icon: React.ReactNode
47
+ }[] = [
48
+ { value: 'inline-block', label: 'Inline-block', icon: <InlineBlockIcon /> },
49
+ { value: 'inline-flex', label: 'Inline-flex', icon: <InlineFlexIcon /> },
50
+ { value: 'inline-grid', label: 'Inline-grid', icon: <InlineGridIcon /> },
51
+ { value: 'inline', label: 'Inline', icon: <InlineTextIcon /> },
52
+ { value: 'none', label: 'None', icon: <NoneDisplayIcon /> },
53
+ ]
54
+
55
+ const FLEX_JUSTIFY: DropdownOption[] = [
56
+ { value: 'flex-start', label: 'Left' },
57
+ { value: 'center', label: 'Center' },
58
+ { value: 'flex-end', label: 'Right' },
59
+ { value: 'space-between', label: 'Space Between' },
60
+ { value: 'space-around', label: 'Space Around' },
61
+ { value: 'space-evenly', label: 'Space Evenly' },
62
+ ]
63
+
64
+ const FLEX_ALIGN: DropdownOption[] = [
65
+ { value: 'stretch', label: 'Stretch' },
66
+ { value: 'flex-start', label: 'Start' },
67
+ { value: 'center', label: 'Center' },
68
+ { value: 'flex-end', label: 'End' },
69
+ { value: 'baseline', label: 'Baseline' },
70
+ ]
71
+
72
+ const GRID_ALIGN: DropdownOption[] = [
73
+ { value: 'stretch', label: 'Stretch' },
74
+ { value: 'start', label: 'Start' },
75
+ { value: 'center', label: 'Center' },
76
+ { value: 'end', label: 'End' },
77
+ ]
78
+
79
+ const VERTICAL_ALIGN_OPTIONS: DropdownOption[] = [
80
+ {
81
+ value: 'baseline',
82
+ label: 'Baseline',
83
+ desc: "Aligns the baseline with the parent's baseline",
84
+ },
85
+ { value: 'sub', label: 'Sub', desc: 'Aligns as subscript' },
86
+ { value: 'super', label: 'Super', desc: 'Aligns as superscript' },
87
+ {
88
+ value: 'top',
89
+ label: 'Top',
90
+ desc: 'Aligns with the tallest element on the line',
91
+ },
92
+ {
93
+ value: 'text-top',
94
+ label: 'Text-top',
95
+ desc: "Aligns with the parent's font top",
96
+ },
97
+ {
98
+ value: 'middle',
99
+ label: 'Middle',
100
+ desc: 'Centers vertically in the parent',
101
+ },
102
+ {
103
+ value: 'bottom',
104
+ label: 'Bottom',
105
+ desc: 'Aligns with the lowest element on the line',
106
+ },
107
+ {
108
+ value: 'text-bottom',
109
+ label: 'Text-bottom',
110
+ desc: "Aligns with the parent's font bottom",
111
+ },
112
+ ]
113
+
114
+ const DIRECTION_BUTTONS = [
115
+ { value: 'row', icon: <ArrowRightIcon /> },
116
+ { value: 'column', icon: <ArrowDownIcon /> },
117
+ { value: 'row-reverse', icon: <ArrowLeftIcon /> },
118
+ { value: 'column-reverse', icon: <ArrowUpIcon /> },
119
+ ]
120
+
121
+ // ─── Utilities ─────────────────────────────────────────────────────
122
+
123
+ function parseGridCount(template: string): number {
124
+ if (!template || template === 'none' || template === 'auto') return 1
125
+ const repeatMatch = template.match(/repeat\((\d+)/)
126
+ if (repeatMatch) return parseInt(repeatMatch[1], 10)
127
+ return template.split(/\s+/).filter(Boolean).length || 1
128
+ }
129
+
130
+ function toGridTemplate(count: number): string {
131
+ if (count <= 0) return 'none'
132
+ return `repeat(${count}, 1fr)`
133
+ }
134
+
135
+ function parseGapValues(gap: string): { row: string; col: string } {
136
+ if (!gap || gap === 'normal') return { row: '0px', col: '0px' }
137
+ const parts = gap.trim().split(/\s+/)
138
+ if (parts.length >= 2) return { row: parts[0], col: parts[1] }
139
+ return { row: parts[0], col: parts[0] }
140
+ }
141
+
142
+ function resolveDisplay(display: string): DisplayMode {
143
+ const valid: DisplayMode[] = [
144
+ 'block',
145
+ 'flex',
146
+ 'grid',
147
+ 'none',
148
+ 'inline-block',
149
+ 'inline-flex',
150
+ 'inline-grid',
151
+ 'inline',
152
+ ]
153
+ return valid.includes(display as DisplayMode)
154
+ ? (display as DisplayMode)
155
+ : 'block'
156
+ }
157
+
158
+ // ─── Icons ─────────────────────────────────────────────────────────
159
+
160
+ function ChevronIcon() {
161
+ return (
162
+ <svg
163
+ width={8}
164
+ height={8}
165
+ viewBox="0 0 8 8"
166
+ fill="currentColor"
167
+ style={{ opacity: 0.7 }}
168
+ >
169
+ <path d="M2 3l2 2.5L6 3" />
170
+ </svg>
171
+ )
172
+ }
173
+
174
+ function CheckMarkIcon() {
175
+ return (
176
+ <svg
177
+ width={12}
178
+ height={12}
179
+ viewBox="0 0 12 12"
180
+ fill="none"
181
+ stroke="currentColor"
182
+ strokeWidth={1.8}
183
+ strokeLinecap="round"
184
+ strokeLinejoin="round"
185
+ >
186
+ <polyline points="2.5 6 5 8.5 9.5 3.5" />
187
+ </svg>
188
+ )
189
+ }
190
+
191
+ function LockIcon({ locked }: { locked: boolean }) {
192
+ return (
193
+ <svg
194
+ width={12}
195
+ height={12}
196
+ viewBox="0 0 14 14"
197
+ fill="none"
198
+ stroke="currentColor"
199
+ strokeWidth={1.3}
200
+ strokeLinecap="round"
201
+ strokeLinejoin="round"
202
+ >
203
+ <rect x={3} y={7} width={8} height={5.5} rx={1} />
204
+ {locked ? (
205
+ <path d="M5 7V5a2 2 0 0 1 4 0v2" />
206
+ ) : (
207
+ <path d="M9 7V5a2 2 0 0 0-4 0" />
208
+ )}
209
+ </svg>
210
+ )
211
+ }
212
+
213
+ function ArrowRightIcon() {
214
+ return (
215
+ <svg
216
+ width={14}
217
+ height={14}
218
+ viewBox="0 0 14 14"
219
+ fill="none"
220
+ stroke="currentColor"
221
+ strokeWidth={1.5}
222
+ strokeLinecap="round"
223
+ strokeLinejoin="round"
224
+ >
225
+ <path d="M2.5 7h9M8.5 4l3 3-3 3" />
226
+ </svg>
227
+ )
228
+ }
229
+ function ArrowDownIcon() {
230
+ return (
231
+ <svg
232
+ width={14}
233
+ height={14}
234
+ viewBox="0 0 14 14"
235
+ fill="none"
236
+ stroke="currentColor"
237
+ strokeWidth={1.5}
238
+ strokeLinecap="round"
239
+ strokeLinejoin="round"
240
+ >
241
+ <path d="M7 2.5v9M4 8.5l3 3 3-3" />
242
+ </svg>
243
+ )
244
+ }
245
+ function ArrowLeftIcon() {
246
+ return (
247
+ <svg
248
+ width={14}
249
+ height={14}
250
+ viewBox="0 0 14 14"
251
+ fill="none"
252
+ stroke="currentColor"
253
+ strokeWidth={1.5}
254
+ strokeLinecap="round"
255
+ strokeLinejoin="round"
256
+ >
257
+ <path d="M11.5 7h-9M5.5 4l-3 3 3 3" />
258
+ </svg>
259
+ )
260
+ }
261
+ function ArrowUpIcon() {
262
+ return (
263
+ <svg
264
+ width={14}
265
+ height={14}
266
+ viewBox="0 0 14 14"
267
+ fill="none"
268
+ stroke="currentColor"
269
+ strokeWidth={1.5}
270
+ strokeLinecap="round"
271
+ strokeLinejoin="round"
272
+ >
273
+ <path d="M7 11.5v-9M4 5.5l3-3 3 3" />
274
+ </svg>
275
+ )
276
+ }
277
+
278
+ function InlineBlockIcon() {
279
+ return (
280
+ <svg
281
+ width={14}
282
+ height={14}
283
+ viewBox="0 0 14 14"
284
+ fill="none"
285
+ stroke="currentColor"
286
+ strokeWidth={1.2}
287
+ >
288
+ <rect x={2} y={3} width={10} height={8} rx={1} />
289
+ <line x1={7} y1={3} x2={7} y2={11} />
290
+ </svg>
291
+ )
292
+ }
293
+ function InlineFlexIcon() {
294
+ return (
295
+ <svg
296
+ width={14}
297
+ height={14}
298
+ viewBox="0 0 14 14"
299
+ fill="none"
300
+ stroke="currentColor"
301
+ strokeWidth={1.2}
302
+ >
303
+ <rect x={2} y={3} width={10} height={8} rx={1} />
304
+ <path d="M5 7h4M7.5 5.5l1.5 1.5-1.5 1.5" />
305
+ </svg>
306
+ )
307
+ }
308
+ function InlineGridIcon() {
309
+ return (
310
+ <svg
311
+ width={14}
312
+ height={14}
313
+ viewBox="0 0 14 14"
314
+ fill="none"
315
+ stroke="currentColor"
316
+ strokeWidth={1.1}
317
+ >
318
+ <rect x={2.5} y={3} width={4} height={3.5} rx={0.5} />
319
+ <rect x={7.5} y={3} width={4} height={3.5} rx={0.5} />
320
+ <rect x={2.5} y={7.5} width={4} height={3.5} rx={0.5} />
321
+ <rect x={7.5} y={7.5} width={4} height={3.5} rx={0.5} />
322
+ </svg>
323
+ )
324
+ }
325
+ function InlineTextIcon() {
326
+ return (
327
+ <svg width={14} height={14} viewBox="0 0 14 14" fill="none">
328
+ <text
329
+ x={1}
330
+ y={11}
331
+ fontSize={9.5}
332
+ fontWeight="bold"
333
+ fontFamily="system-ui"
334
+ fill="currentColor"
335
+ >
336
+ AA
337
+ </text>
338
+ </svg>
339
+ )
340
+ }
341
+ function NoneDisplayIcon() {
342
+ return (
343
+ <svg
344
+ width={14}
345
+ height={14}
346
+ viewBox="0 0 14 14"
347
+ fill="none"
348
+ stroke="currentColor"
349
+ strokeWidth={1.2}
350
+ strokeLinecap="round"
351
+ >
352
+ <circle cx={7} cy={7} r={4.5} />
353
+ <line x1={3.8} y1={10.2} x2={10.2} y2={3.8} />
354
+ </svg>
355
+ )
356
+ }
357
+
358
+ function GridRowFlowIcon() {
359
+ return (
360
+ <svg
361
+ width={14}
362
+ height={14}
363
+ viewBox="0 0 14 14"
364
+ fill="none"
365
+ stroke="currentColor"
366
+ strokeWidth={1.3}
367
+ strokeLinecap="round"
368
+ >
369
+ <path d="M2 4h10M2 7h7M2 10h10" />
370
+ </svg>
371
+ )
372
+ }
373
+ function GridColFlowIcon() {
374
+ return (
375
+ <svg
376
+ width={14}
377
+ height={14}
378
+ viewBox="0 0 14 14"
379
+ fill="none"
380
+ stroke="currentColor"
381
+ strokeWidth={1.3}
382
+ strokeLinecap="round"
383
+ >
384
+ <path d="M4 2v10M7 2v7M10 2v10" />
385
+ </svg>
386
+ )
387
+ }
388
+ function GridDenseRowIcon() {
389
+ return (
390
+ <svg
391
+ width={14}
392
+ height={14}
393
+ viewBox="0 0 14 14"
394
+ fill="none"
395
+ stroke="currentColor"
396
+ strokeWidth={1.2}
397
+ strokeLinecap="round"
398
+ >
399
+ <rect x={2} y={2} width={4.5} height={4.5} rx={0.5} />
400
+ <rect x={7.5} y={2} width={4.5} height={4.5} rx={0.5} />
401
+ <rect x={2} y={7.5} width={10} height={4.5} rx={0.5} />
402
+ </svg>
403
+ )
404
+ }
405
+ function GridDenseColIcon() {
406
+ return (
407
+ <svg
408
+ width={14}
409
+ height={14}
410
+ viewBox="0 0 14 14"
411
+ fill="none"
412
+ stroke="currentColor"
413
+ strokeWidth={1.2}
414
+ strokeLinecap="round"
415
+ >
416
+ <rect x={2} y={2} width={4.5} height={10} rx={0.5} />
417
+ <rect x={7.5} y={2} width={4.5} height={4.5} rx={0.5} />
418
+ <rect x={7.5} y={7.5} width={4.5} height={4.5} rx={0.5} />
419
+ </svg>
420
+ )
421
+ }
422
+
423
+ // Grid alignment icons
424
+ function ColAlignStartIcon() {
425
+ return (
426
+ <svg
427
+ width={14}
428
+ height={14}
429
+ viewBox="0 0 14 14"
430
+ fill="none"
431
+ stroke="currentColor"
432
+ strokeWidth={1.3}
433
+ strokeLinecap="round"
434
+ >
435
+ <line x1={2} y1={2} x2={2} y2={12} />
436
+ <rect
437
+ x={4}
438
+ y={3}
439
+ width={3}
440
+ height={3}
441
+ rx={0.5}
442
+ fill="currentColor"
443
+ stroke="none"
444
+ />
445
+ <rect
446
+ x={4}
447
+ y={8}
448
+ width={3}
449
+ height={3}
450
+ rx={0.5}
451
+ fill="currentColor"
452
+ stroke="none"
453
+ />
454
+ </svg>
455
+ )
456
+ }
457
+ function ColAlignCenterIcon() {
458
+ return (
459
+ <svg
460
+ width={14}
461
+ height={14}
462
+ viewBox="0 0 14 14"
463
+ fill="none"
464
+ stroke="currentColor"
465
+ strokeWidth={1.3}
466
+ strokeLinecap="round"
467
+ >
468
+ <line x1={7} y1={2} x2={7} y2={12} strokeDasharray="1.5 1.5" />
469
+ <rect
470
+ x={5}
471
+ y={3}
472
+ width={4}
473
+ height={3}
474
+ rx={0.5}
475
+ fill="currentColor"
476
+ stroke="none"
477
+ />
478
+ <rect
479
+ x={5}
480
+ y={8}
481
+ width={4}
482
+ height={3}
483
+ rx={0.5}
484
+ fill="currentColor"
485
+ stroke="none"
486
+ />
487
+ </svg>
488
+ )
489
+ }
490
+ function ColAlignEndIcon() {
491
+ return (
492
+ <svg
493
+ width={14}
494
+ height={14}
495
+ viewBox="0 0 14 14"
496
+ fill="none"
497
+ stroke="currentColor"
498
+ strokeWidth={1.3}
499
+ strokeLinecap="round"
500
+ >
501
+ <line x1={12} y1={2} x2={12} y2={12} />
502
+ <rect
503
+ x={7}
504
+ y={3}
505
+ width={3}
506
+ height={3}
507
+ rx={0.5}
508
+ fill="currentColor"
509
+ stroke="none"
510
+ />
511
+ <rect
512
+ x={7}
513
+ y={8}
514
+ width={3}
515
+ height={3}
516
+ rx={0.5}
517
+ fill="currentColor"
518
+ stroke="none"
519
+ />
520
+ </svg>
521
+ )
522
+ }
523
+ function ColAlignStretchIcon() {
524
+ return (
525
+ <svg
526
+ width={14}
527
+ height={14}
528
+ viewBox="0 0 14 14"
529
+ fill="none"
530
+ stroke="currentColor"
531
+ strokeWidth={1.3}
532
+ strokeLinecap="round"
533
+ >
534
+ <line x1={2} y1={2} x2={2} y2={12} />
535
+ <line x1={12} y1={2} x2={12} y2={12} />
536
+ <rect
537
+ x={4}
538
+ y={3}
539
+ width={6}
540
+ height={3}
541
+ rx={0.5}
542
+ fill="currentColor"
543
+ stroke="none"
544
+ />
545
+ <rect
546
+ x={4}
547
+ y={8}
548
+ width={6}
549
+ height={3}
550
+ rx={0.5}
551
+ fill="currentColor"
552
+ stroke="none"
553
+ />
554
+ </svg>
555
+ )
556
+ }
557
+ function RowAlignStartIcon() {
558
+ return (
559
+ <svg
560
+ width={14}
561
+ height={14}
562
+ viewBox="0 0 14 14"
563
+ fill="none"
564
+ stroke="currentColor"
565
+ strokeWidth={1.3}
566
+ strokeLinecap="round"
567
+ >
568
+ <line x1={2} y1={2} x2={12} y2={2} />
569
+ <rect
570
+ x={3}
571
+ y={4}
572
+ width={3}
573
+ height={3}
574
+ rx={0.5}
575
+ fill="currentColor"
576
+ stroke="none"
577
+ />
578
+ <rect
579
+ x={8}
580
+ y={4}
581
+ width={3}
582
+ height={3}
583
+ rx={0.5}
584
+ fill="currentColor"
585
+ stroke="none"
586
+ />
587
+ </svg>
588
+ )
589
+ }
590
+ function RowAlignCenterIcon() {
591
+ return (
592
+ <svg
593
+ width={14}
594
+ height={14}
595
+ viewBox="0 0 14 14"
596
+ fill="none"
597
+ stroke="currentColor"
598
+ strokeWidth={1.3}
599
+ strokeLinecap="round"
600
+ >
601
+ <line x1={2} y1={7} x2={12} y2={7} strokeDasharray="1.5 1.5" />
602
+ <rect
603
+ x={3}
604
+ y={5}
605
+ width={3}
606
+ height={4}
607
+ rx={0.5}
608
+ fill="currentColor"
609
+ stroke="none"
610
+ />
611
+ <rect
612
+ x={8}
613
+ y={5}
614
+ width={3}
615
+ height={4}
616
+ rx={0.5}
617
+ fill="currentColor"
618
+ stroke="none"
619
+ />
620
+ </svg>
621
+ )
622
+ }
623
+ function RowAlignEndIcon() {
624
+ return (
625
+ <svg
626
+ width={14}
627
+ height={14}
628
+ viewBox="0 0 14 14"
629
+ fill="none"
630
+ stroke="currentColor"
631
+ strokeWidth={1.3}
632
+ strokeLinecap="round"
633
+ >
634
+ <line x1={2} y1={12} x2={12} y2={12} />
635
+ <rect
636
+ x={3}
637
+ y={7}
638
+ width={3}
639
+ height={3}
640
+ rx={0.5}
641
+ fill="currentColor"
642
+ stroke="none"
643
+ />
644
+ <rect
645
+ x={8}
646
+ y={7}
647
+ width={3}
648
+ height={3}
649
+ rx={0.5}
650
+ fill="currentColor"
651
+ stroke="none"
652
+ />
653
+ </svg>
654
+ )
655
+ }
656
+ function RowAlignStretchIcon() {
657
+ return (
658
+ <svg
659
+ width={14}
660
+ height={14}
661
+ viewBox="0 0 14 14"
662
+ fill="none"
663
+ stroke="currentColor"
664
+ strokeWidth={1.3}
665
+ strokeLinecap="round"
666
+ >
667
+ <line x1={2} y1={2} x2={12} y2={2} />
668
+ <line x1={2} y1={12} x2={12} y2={12} />
669
+ <rect
670
+ x={3}
671
+ y={4}
672
+ width={3}
673
+ height={6}
674
+ rx={0.5}
675
+ fill="currentColor"
676
+ stroke="none"
677
+ />
678
+ <rect
679
+ x={8}
680
+ y={4}
681
+ width={3}
682
+ height={6}
683
+ rx={0.5}
684
+ fill="currentColor"
685
+ stroke="none"
686
+ />
687
+ </svg>
688
+ )
689
+ }
690
+
691
+ // ─── Dropdown Component ────────────────────────────────────────────
692
+
693
+ function LayoutDropdown({
694
+ value,
695
+ options,
696
+ onChange,
697
+ showDescription = false,
698
+ width = 150,
699
+ }: {
700
+ value: string
701
+ options: DropdownOption[]
702
+ onChange: (value: string) => void
703
+ showDescription?: boolean
704
+ width?: number
705
+ }) {
706
+ const [open, setOpen] = useState(false)
707
+ const ref = useRef<HTMLDivElement>(null)
708
+ const [hoveredDesc, setHoveredDesc] = useState<string | null>(null)
709
+
710
+ useEffect(() => {
711
+ if (!open) return
712
+ const handler = (e: MouseEvent) => {
713
+ if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
714
+ }
715
+ document.addEventListener('mousedown', handler)
716
+ return () => document.removeEventListener('mousedown', handler)
717
+ }, [open])
718
+
719
+ const selected = options.find((o) => o.value === value)
720
+
721
+ return (
722
+ <div ref={ref} className="relative flex-1">
723
+ <button
724
+ type="button"
725
+ onClick={() => setOpen(!open)}
726
+ className="flex items-center justify-between w-full h-6 px-2 rounded text-[11px] transition-colors"
727
+ style={{
728
+ background: 'var(--bg-tertiary)',
729
+ border: '1px solid var(--border)',
730
+ color: 'var(--text-primary)',
731
+ }}
732
+ >
733
+ <span className="truncate">{selected?.label ?? value}</span>
734
+ <ChevronIcon />
735
+ </button>
736
+ {open && (
737
+ <div
738
+ className="absolute top-full left-0 mt-1 rounded shadow-lg z-50 py-1 overflow-auto"
739
+ style={{
740
+ width,
741
+ maxHeight: 280,
742
+ background: '#252526',
743
+ border: '1px solid var(--border)',
744
+ }}
745
+ >
746
+ {options.map((opt) => (
747
+ <button
748
+ key={opt.value}
749
+ type="button"
750
+ onClick={() => {
751
+ onChange(opt.value)
752
+ setOpen(false)
753
+ }}
754
+ onMouseEnter={() => opt.desc && setHoveredDesc(opt.desc)}
755
+ onMouseLeave={() => setHoveredDesc(null)}
756
+ className="flex items-center gap-2 w-full px-2.5 py-1.5 text-[11px] transition-colors"
757
+ style={{
758
+ color:
759
+ opt.value === value
760
+ ? 'var(--text-primary)'
761
+ : 'var(--text-secondary)',
762
+ background:
763
+ opt.value === value
764
+ ? 'rgba(74, 158, 255, 0.08)'
765
+ : 'transparent',
766
+ }}
767
+ onMouseOver={(e) => {
768
+ if (opt.value !== value)
769
+ e.currentTarget.style.background = 'var(--bg-hover)'
770
+ }}
771
+ onMouseOut={(e) => {
772
+ e.currentTarget.style.background =
773
+ opt.value === value
774
+ ? 'rgba(74, 158, 255, 0.08)'
775
+ : 'transparent'
776
+ }}
777
+ >
778
+ <span className="w-3 flex-shrink-0">
779
+ {opt.value === value && <CheckMarkIcon />}
780
+ </span>
781
+ <span className="flex-1 text-left">{opt.label}</span>
782
+ </button>
783
+ ))}
784
+ {showDescription && hoveredDesc && (
785
+ <div
786
+ className="px-3 py-2 text-[10px] leading-relaxed"
787
+ style={{
788
+ color: 'var(--text-muted)',
789
+ borderTop: '1px solid var(--border)',
790
+ }}
791
+ >
792
+ {hoveredDesc}
793
+ </div>
794
+ )}
795
+ </div>
796
+ )}
797
+ </div>
798
+ )
799
+ }
800
+
801
+ // ─── Display Toggle ────────────────────────────────────────────────
802
+
803
+ function DisplayToggle({
804
+ display,
805
+ onChange,
806
+ }: {
807
+ display: DisplayMode
808
+ onChange: (mode: DisplayMode) => void
809
+ }) {
810
+ const [dropdownOpen, setDropdownOpen] = useState(false)
811
+ const dropdownRef = useRef<HTMLDivElement>(null)
812
+
813
+ useEffect(() => {
814
+ if (!dropdownOpen) return
815
+ const handler = (e: MouseEvent) => {
816
+ if (
817
+ dropdownRef.current &&
818
+ !dropdownRef.current.contains(e.target as Node)
819
+ )
820
+ setDropdownOpen(false)
821
+ }
822
+ document.addEventListener('mousedown', handler)
823
+ return () => document.removeEventListener('mousedown', handler)
824
+ }, [dropdownOpen])
825
+
826
+ const isMainDisplay = ['block', 'flex', 'grid'].includes(display)
827
+ const fourthLabel = isMainDisplay ? 'None' : DISPLAY_LABELS[display] || 'None'
828
+ const fourthActive = !isMainDisplay
829
+
830
+ return (
831
+ <div className="flex items-center gap-2">
832
+ <span
833
+ className="text-[11px] flex-shrink-0"
834
+ style={{ color: 'var(--accent)' }}
835
+ >
836
+ Display
837
+ </span>
838
+ <div
839
+ className="flex rounded overflow-hidden flex-1"
840
+ style={{ border: '1px solid var(--border)' }}
841
+ >
842
+ {/* Block, Flex, Grid buttons */}
843
+ {(['block', 'flex', 'grid'] as DisplayMode[]).map((mode) => (
844
+ <button
845
+ key={mode}
846
+ type="button"
847
+ onClick={() => onChange(mode)}
848
+ className="flex-1 h-[26px] text-[11px] transition-colors"
849
+ style={{
850
+ background: display === mode ? '#3a3a3a' : 'var(--bg-tertiary)',
851
+ color:
852
+ display === mode ? 'var(--text-primary)' : 'var(--text-muted)',
853
+ borderRight: '1px solid var(--border)',
854
+ }}
855
+ >
856
+ {DISPLAY_LABELS[mode]}
857
+ </button>
858
+ ))}
859
+
860
+ {/* 4th position — dropdown trigger */}
861
+ <div ref={dropdownRef} className="relative">
862
+ <button
863
+ type="button"
864
+ onClick={() => setDropdownOpen(!dropdownOpen)}
865
+ className="flex items-center gap-0.5 h-[26px] px-2 text-[11px] transition-colors"
866
+ style={{
867
+ background: fourthActive ? '#3a3a3a' : 'var(--bg-tertiary)',
868
+ color: fourthActive ? 'var(--text-primary)' : 'var(--text-muted)',
869
+ whiteSpace: 'nowrap',
870
+ }}
871
+ >
872
+ {fourthLabel}
873
+ <ChevronIcon />
874
+ </button>
875
+
876
+ {dropdownOpen && (
877
+ <div
878
+ className="absolute top-full right-0 mt-1 rounded shadow-lg z-50 py-1"
879
+ style={{
880
+ width: 160,
881
+ background: '#252526',
882
+ border: '1px solid var(--border)',
883
+ }}
884
+ >
885
+ {DROPDOWN_DISPLAYS.map((opt) => (
886
+ <button
887
+ key={opt.value}
888
+ type="button"
889
+ onClick={() => {
890
+ onChange(opt.value)
891
+ setDropdownOpen(false)
892
+ }}
893
+ className="flex items-center gap-2 w-full px-2.5 py-1.5 text-[11px] transition-colors"
894
+ style={{
895
+ color:
896
+ display === opt.value
897
+ ? 'var(--text-primary)'
898
+ : 'var(--text-secondary)',
899
+ background:
900
+ display === opt.value
901
+ ? 'rgba(74, 158, 255, 0.08)'
902
+ : 'transparent',
903
+ }}
904
+ onMouseOver={(e) => {
905
+ if (display !== opt.value)
906
+ e.currentTarget.style.background = 'var(--bg-hover)'
907
+ }}
908
+ onMouseOut={(e) => {
909
+ e.currentTarget.style.background =
910
+ display === opt.value
911
+ ? 'rgba(74, 158, 255, 0.08)'
912
+ : 'transparent'
913
+ }}
914
+ >
915
+ <span style={{ color: 'var(--text-muted)' }}>{opt.icon}</span>
916
+ <span className="flex-1 text-left">{opt.label}</span>
917
+ </button>
918
+ ))}
919
+ </div>
920
+ )}
921
+ </div>
922
+ </div>
923
+ </div>
924
+ )
925
+ }
926
+
927
+ // ─── Alignment Visual Box ──────────────────────────────────────────
928
+
929
+ function FlexAlignVisual({
930
+ direction,
931
+ justify,
932
+ align,
933
+ }: {
934
+ direction: string
935
+ justify: string
936
+ align: string
937
+ }) {
938
+ const isRow = direction === 'row' || direction === 'row-reverse'
939
+ const size = 44
940
+ const barThick = 3
941
+ const barCount = 3
942
+ const barLengths = [14, 20, 17] // varied for visual interest
943
+ const gap = 3
944
+ const totalSpan = barCount * barThick + (barCount - 1) * gap
945
+
946
+ // Compute main-axis offset (justify)
947
+ let mainOffset: number
948
+ switch (justify) {
949
+ case 'center':
950
+ mainOffset = (size - totalSpan) / 2
951
+ break
952
+ case 'flex-end':
953
+ case 'end':
954
+ mainOffset = size - totalSpan - 3
955
+ break
956
+ case 'space-between':
957
+ mainOffset = 3
958
+ break
959
+ default:
960
+ mainOffset = 3 // flex-start
961
+ }
962
+
963
+ const spaceBetween = justify === 'space-between'
964
+ const spaceAround = justify === 'space-around'
965
+ const spaceEvenly = justify === 'space-evenly'
966
+
967
+ function getBarPositions(): Array<{
968
+ x: number
969
+ y: number
970
+ w: number
971
+ h: number
972
+ }> {
973
+ const bars: Array<{ x: number; y: number; w: number; h: number }> = []
974
+
975
+ for (let i = 0; i < barCount; i++) {
976
+ const len = barLengths[i]
977
+
978
+ // Cross-axis position and size
979
+ let crossPos: number
980
+ let crossSize: number
981
+ const isStretch = align === 'stretch'
982
+ const actualLen = isStretch ? size - 6 : len
983
+
984
+ switch (align) {
985
+ case 'center':
986
+ crossPos = (size - actualLen) / 2
987
+ break
988
+ case 'flex-end':
989
+ case 'end':
990
+ crossPos = size - actualLen - 3
991
+ break
992
+ default:
993
+ crossPos = 3 // flex-start, stretch, baseline
994
+ }
995
+ crossSize = actualLen
996
+
997
+ // Main-axis position
998
+ let mainPos: number
999
+ if (spaceBetween) {
1000
+ mainPos =
1001
+ i === 0
1002
+ ? 3
1003
+ : i === barCount - 1
1004
+ ? size - barThick - 3
1005
+ : size / 2 - barThick / 2
1006
+ } else if (spaceAround) {
1007
+ const totalSpace = size - barCount * barThick
1008
+ const spacing = totalSpace / barCount
1009
+ mainPos = spacing / 2 + i * (barThick + spacing)
1010
+ } else if (spaceEvenly) {
1011
+ const spacing = (size - barCount * barThick) / (barCount + 1)
1012
+ mainPos = spacing + i * (barThick + spacing)
1013
+ } else {
1014
+ mainPos = mainOffset + i * (barThick + gap)
1015
+ }
1016
+
1017
+ if (isRow) {
1018
+ bars.push({ x: mainPos, y: crossPos, w: barThick, h: crossSize })
1019
+ } else {
1020
+ bars.push({ x: crossPos, y: mainPos, w: crossSize, h: barThick })
1021
+ }
1022
+ }
1023
+ return bars
1024
+ }
1025
+
1026
+ const bars = getBarPositions()
1027
+
1028
+ return (
1029
+ <svg
1030
+ width={size}
1031
+ height={size}
1032
+ viewBox={`0 0 ${size} ${size}`}
1033
+ className="flex-shrink-0 rounded"
1034
+ style={{ background: '#1a1a1a', border: '1px solid var(--border)' }}
1035
+ >
1036
+ {bars.map((bar, i) => (
1037
+ <rect
1038
+ key={i}
1039
+ x={bar.x}
1040
+ y={bar.y}
1041
+ width={bar.w}
1042
+ height={bar.h}
1043
+ rx={1}
1044
+ fill="#e0e0e0"
1045
+ opacity={0.8}
1046
+ />
1047
+ ))}
1048
+ </svg>
1049
+ )
1050
+ }
1051
+
1052
+ function GridAlignVisual() {
1053
+ return (
1054
+ <svg
1055
+ width={44}
1056
+ height={44}
1057
+ viewBox="0 0 44 44"
1058
+ className="flex-shrink-0 rounded"
1059
+ style={{ background: '#1a1a1a', border: '1px solid var(--border)' }}
1060
+ >
1061
+ {/* Cross arrows */}
1062
+ <line
1063
+ x1={22}
1064
+ y1={8}
1065
+ x2={22}
1066
+ y2={36}
1067
+ stroke="#e0e0e0"
1068
+ strokeWidth={1.2}
1069
+ opacity={0.6}
1070
+ />
1071
+ <line
1072
+ x1={8}
1073
+ y1={22}
1074
+ x2={36}
1075
+ y2={22}
1076
+ stroke="#e0e0e0"
1077
+ strokeWidth={1.2}
1078
+ opacity={0.6}
1079
+ />
1080
+ {/* Arrow tips */}
1081
+ <polyline
1082
+ points="19,11 22,8 25,11"
1083
+ fill="none"
1084
+ stroke="#e0e0e0"
1085
+ strokeWidth={1.2}
1086
+ opacity={0.6}
1087
+ />
1088
+ <polyline
1089
+ points="19,33 22,36 25,33"
1090
+ fill="none"
1091
+ stroke="#e0e0e0"
1092
+ strokeWidth={1.2}
1093
+ opacity={0.6}
1094
+ />
1095
+ <polyline
1096
+ points="11,19 8,22 11,25"
1097
+ fill="none"
1098
+ stroke="#e0e0e0"
1099
+ strokeWidth={1.2}
1100
+ opacity={0.6}
1101
+ />
1102
+ <polyline
1103
+ points="33,19 36,22 33,25"
1104
+ fill="none"
1105
+ stroke="#e0e0e0"
1106
+ strokeWidth={1.2}
1107
+ opacity={0.6}
1108
+ />
1109
+ </svg>
1110
+ )
1111
+ }
1112
+
1113
+ // ─── Number Stepper ────────────────────────────────────────────────
1114
+
1115
+ function NumberStepper({
1116
+ value,
1117
+ onChange,
1118
+ min = 1,
1119
+ max = 12,
1120
+ }: {
1121
+ value: number
1122
+ onChange: (n: number) => void
1123
+ min?: number
1124
+ max?: number
1125
+ }) {
1126
+ return (
1127
+ <div
1128
+ className="flex items-center h-6 rounded overflow-hidden"
1129
+ style={{
1130
+ background: 'var(--bg-tertiary)',
1131
+ border: '1px solid var(--border)',
1132
+ }}
1133
+ >
1134
+ <input
1135
+ type="text"
1136
+ inputMode="numeric"
1137
+ value={value}
1138
+ onChange={(e) => {
1139
+ const n = parseInt(e.target.value, 10)
1140
+ if (!isNaN(n) && n >= min && n <= max) onChange(n)
1141
+ }}
1142
+ className="w-8 h-full text-center text-[11px] bg-transparent border-none outline-none"
1143
+ style={{ color: 'var(--text-primary)' }}
1144
+ />
1145
+ <div
1146
+ className="flex flex-col h-full"
1147
+ style={{ borderLeft: '1px solid var(--border)' }}
1148
+ >
1149
+ <button
1150
+ type="button"
1151
+ onClick={() => value < max && onChange(value + 1)}
1152
+ className="flex items-center justify-center flex-1 px-1 transition-colors hover:bg-[var(--bg-hover)]"
1153
+ style={{ color: 'var(--text-muted)', fontSize: 7 }}
1154
+ >
1155
+
1156
+ </button>
1157
+ <button
1158
+ type="button"
1159
+ onClick={() => value > min && onChange(value - 1)}
1160
+ className="flex items-center justify-center flex-1 px-1 transition-colors hover:bg-[var(--bg-hover)]"
1161
+ style={{
1162
+ color: 'var(--text-muted)',
1163
+ fontSize: 7,
1164
+ borderTop: '1px solid var(--border)',
1165
+ }}
1166
+ >
1167
+
1168
+ </button>
1169
+ </div>
1170
+ </div>
1171
+ )
1172
+ }
1173
+
1174
+ // ─── Gap Input ─────────────────────────────────────────────────────
1175
+
1176
+ function GapInput({
1177
+ value,
1178
+ property,
1179
+ onChange,
1180
+ }: {
1181
+ value: string
1182
+ property: string
1183
+ onChange: (prop: string, val: string) => void
1184
+ }) {
1185
+ const parsed = parseCSSValue(value)
1186
+ const [localVal, setLocalVal] = useState(String(parsed.number))
1187
+ const [unit, setUnit] = useState(parsed.unit || 'px')
1188
+ const containerRef = useRef<HTMLDivElement>(null)
1189
+ const isDragging = useRef(false)
1190
+ const dragStartX = useRef(0)
1191
+ const dragStartValue = useRef(0)
1192
+
1193
+ useEffect(() => {
1194
+ const p = parseCSSValue(value)
1195
+ setLocalVal(String(p.number))
1196
+ setUnit(p.unit || 'px')
1197
+ }, [value])
1198
+
1199
+ const commit = useCallback(
1200
+ (num: string, u: string) => {
1201
+ const n = parseFloat(num)
1202
+ if (!isNaN(n)) onChange(property, formatCSSValue(Math.max(0, n), u))
1203
+ },
1204
+ [onChange, property],
1205
+ )
1206
+
1207
+ // --- Drag-to-scrub (2px per pixel of movement) ---
1208
+ const handlePointerDown = useCallback(
1209
+ (e: React.PointerEvent) => {
1210
+ // Don't hijack if user clicked on the unit button
1211
+ if ((e.target as HTMLElement).tagName === 'BUTTON') return
1212
+ e.preventDefault()
1213
+ isDragging.current = true
1214
+ dragStartX.current = e.clientX
1215
+ dragStartValue.current = parseFloat(localVal || '0')
1216
+ containerRef.current?.setPointerCapture(e.pointerId)
1217
+ document.body.style.cursor = 'ew-resize'
1218
+ document.body.style.userSelect = 'none'
1219
+ },
1220
+ [localVal],
1221
+ )
1222
+
1223
+ const handlePointerMove = useCallback(
1224
+ (e: React.PointerEvent) => {
1225
+ if (!isDragging.current) return
1226
+ const delta = e.clientX - dragStartX.current
1227
+ const next = Math.max(0, Math.round(dragStartValue.current + delta * 2))
1228
+ const nextStr = String(next)
1229
+ setLocalVal(nextStr)
1230
+ onChange(property, formatCSSValue(next, unit))
1231
+ },
1232
+ [onChange, property, unit],
1233
+ )
1234
+
1235
+ const handlePointerUp = useCallback((e: React.PointerEvent) => {
1236
+ if (!isDragging.current) return
1237
+ isDragging.current = false
1238
+ containerRef.current?.releasePointerCapture(e.pointerId)
1239
+ document.body.style.cursor = ''
1240
+ document.body.style.userSelect = ''
1241
+ }, [])
1242
+
1243
+ const units = ['px', 'rem', 'em', '%']
1244
+ const cycleUnit = useCallback(() => {
1245
+ const idx = units.indexOf(unit)
1246
+ const next = units[(idx + 1) % units.length]
1247
+ setUnit(next)
1248
+ const n = parseFloat(localVal || '0')
1249
+ if (!isNaN(n)) onChange(property, formatCSSValue(Math.max(0, n), next))
1250
+ }, [units, unit, localVal, onChange, property])
1251
+
1252
+ return (
1253
+ <div
1254
+ ref={containerRef}
1255
+ onPointerDown={handlePointerDown}
1256
+ onPointerMove={handlePointerMove}
1257
+ onPointerUp={handlePointerUp}
1258
+ className="flex items-center h-6 rounded overflow-hidden"
1259
+ style={{
1260
+ background: 'var(--bg-tertiary)',
1261
+ border: '1px solid var(--border)',
1262
+ cursor: 'ew-resize',
1263
+ }}
1264
+ >
1265
+ <input
1266
+ type="text"
1267
+ inputMode="numeric"
1268
+ value={localVal}
1269
+ onChange={(e) => setLocalVal(e.target.value)}
1270
+ onBlur={() => commit(localVal, unit)}
1271
+ onKeyDown={(e) => {
1272
+ if (e.key === 'Enter') {
1273
+ commit(localVal, unit)
1274
+ ;(e.target as HTMLInputElement).blur()
1275
+ } else if (e.key === 'ArrowUp') {
1276
+ e.preventDefault()
1277
+ const n = Math.max(
1278
+ 0,
1279
+ parseFloat(localVal || '0') + (e.shiftKey ? 10 : 1),
1280
+ )
1281
+ setLocalVal(String(n))
1282
+ commit(String(n), unit)
1283
+ } else if (e.key === 'ArrowDown') {
1284
+ e.preventDefault()
1285
+ const n = Math.max(
1286
+ 0,
1287
+ parseFloat(localVal || '0') - (e.shiftKey ? 10 : 1),
1288
+ )
1289
+ setLocalVal(String(n))
1290
+ commit(String(n), unit)
1291
+ }
1292
+ }}
1293
+ className="flex-1 min-w-0 h-full px-1.5 text-[11px] bg-transparent border-none outline-none"
1294
+ style={{ color: 'var(--text-primary)', cursor: 'ew-resize' }}
1295
+ />
1296
+ <button
1297
+ type="button"
1298
+ onClick={cycleUnit}
1299
+ className="flex-shrink-0 h-full px-1.5 text-[10px] uppercase cursor-pointer hover:opacity-80 bg-transparent border-none outline-none"
1300
+ style={{
1301
+ color: 'var(--text-muted)',
1302
+ borderLeft: '1px solid var(--border)',
1303
+ }}
1304
+ >
1305
+ {unit}
1306
+ </button>
1307
+ </div>
1308
+ )
1309
+ }
1310
+
1311
+ // ─── Icon Toggle Bar ───────────────────────────────────────────────
1312
+
1313
+ function IconBar({
1314
+ options,
1315
+ value,
1316
+ onChange,
1317
+ }: {
1318
+ options: { value: string; icon: React.ReactNode }[]
1319
+ value: string
1320
+ onChange: (v: string) => void
1321
+ }) {
1322
+ return (
1323
+ <div
1324
+ className="inline-flex rounded overflow-hidden"
1325
+ style={{ border: '1px solid var(--border)' }}
1326
+ >
1327
+ {options.map((opt) => (
1328
+ <button
1329
+ key={opt.value}
1330
+ type="button"
1331
+ onClick={() => onChange(opt.value)}
1332
+ className="flex items-center justify-center w-[26px] h-[26px] transition-colors"
1333
+ style={{
1334
+ background: opt.value === value ? '#3a3a3a' : 'var(--bg-tertiary)',
1335
+ color:
1336
+ opt.value === value ? 'var(--text-primary)' : 'var(--text-muted)',
1337
+ }}
1338
+ >
1339
+ {opt.icon}
1340
+ </button>
1341
+ ))}
1342
+ </div>
1343
+ )
1344
+ }
1345
+
1346
+ // ─── Flex Controls ─────────────────────────────────────────────────
1347
+
1348
+ function FlexControls({
1349
+ direction,
1350
+ justifyContent,
1351
+ alignItems,
1352
+ gap,
1353
+ onChange,
1354
+ }: {
1355
+ direction: string
1356
+ justifyContent: string
1357
+ alignItems: string
1358
+ gap: string
1359
+ onChange: (prop: string, val: string) => void
1360
+ }) {
1361
+ return (
1362
+ <div
1363
+ className="space-y-2.5 pt-2"
1364
+ style={{ borderTop: '1px solid var(--border)' }}
1365
+ >
1366
+ {/* Direction */}
1367
+ <div className="flex items-center gap-2">
1368
+ <span
1369
+ className="text-[11px] w-[58px] flex-shrink-0"
1370
+ style={{ color: 'var(--text-secondary)' }}
1371
+ >
1372
+ Direction
1373
+ </span>
1374
+ <IconBar
1375
+ options={DIRECTION_BUTTONS}
1376
+ value={direction}
1377
+ onChange={(v) => onChange('flexDirection', v)}
1378
+ />
1379
+ </div>
1380
+
1381
+ {/* Align */}
1382
+ <div className="flex items-center gap-2">
1383
+ <span
1384
+ className="text-[11px] w-[58px] flex-shrink-0"
1385
+ style={{ color: 'var(--text-secondary)' }}
1386
+ >
1387
+ Align
1388
+ </span>
1389
+ <FlexAlignVisual
1390
+ direction={direction}
1391
+ justify={justifyContent}
1392
+ align={alignItems}
1393
+ />
1394
+ <div className="flex-1 flex flex-col gap-1.5">
1395
+ <div className="flex items-center gap-1.5">
1396
+ <span
1397
+ className="text-[10px] w-3"
1398
+ style={{ color: 'var(--text-muted)' }}
1399
+ >
1400
+ X
1401
+ </span>
1402
+ <LayoutDropdown
1403
+ value={justifyContent}
1404
+ options={FLEX_JUSTIFY}
1405
+ onChange={(v) => onChange('justifyContent', v)}
1406
+ />
1407
+ </div>
1408
+ <div className="flex items-center gap-1.5">
1409
+ <span
1410
+ className="text-[10px] w-3"
1411
+ style={{ color: 'var(--text-muted)' }}
1412
+ >
1413
+ Y
1414
+ </span>
1415
+ <LayoutDropdown
1416
+ value={alignItems}
1417
+ options={FLEX_ALIGN}
1418
+ onChange={(v) => onChange('alignItems', v)}
1419
+ />
1420
+ </div>
1421
+ </div>
1422
+ </div>
1423
+
1424
+ {/* Gap */}
1425
+ <div className="flex items-center gap-2">
1426
+ <span
1427
+ className="text-[11px] w-[58px] flex-shrink-0"
1428
+ style={{ color: 'var(--text-secondary)' }}
1429
+ >
1430
+ Gap
1431
+ </span>
1432
+ <div className="flex-1">
1433
+ <GapInput value={gap} property="gap" onChange={onChange} />
1434
+ </div>
1435
+ </div>
1436
+ </div>
1437
+ )
1438
+ }
1439
+
1440
+ // ─── Grid Controls ─────────────────────────────────────────────────
1441
+
1442
+ function GridControls({
1443
+ columns,
1444
+ rows,
1445
+ justifyItems,
1446
+ alignItems,
1447
+ columnGap,
1448
+ rowGap,
1449
+ autoFlow,
1450
+ onChange,
1451
+ }: {
1452
+ columns: string
1453
+ rows: string
1454
+ justifyItems: string
1455
+ alignItems: string
1456
+ columnGap: string
1457
+ rowGap: string
1458
+ autoFlow: string
1459
+ onChange: (prop: string, val: string) => void
1460
+ }) {
1461
+ const [gapLinked, setGapLinked] = useState(columnGap === rowGap)
1462
+ const [moreAlign, setMoreAlign] = useState(false)
1463
+
1464
+ const colCount = parseGridCount(columns)
1465
+ const rowCount = parseGridCount(rows)
1466
+
1467
+ const handleGapChange = useCallback(
1468
+ (prop: string, val: string) => {
1469
+ if (gapLinked) {
1470
+ onChange('columnGap', val)
1471
+ onChange('rowGap', val)
1472
+ } else {
1473
+ onChange(prop, val)
1474
+ }
1475
+ },
1476
+ [gapLinked, onChange],
1477
+ )
1478
+
1479
+ const GRID_FLOW = [
1480
+ { value: 'row', icon: <GridRowFlowIcon /> },
1481
+ { value: 'column', icon: <GridColFlowIcon /> },
1482
+ { value: 'row dense', icon: <GridDenseRowIcon /> },
1483
+ { value: 'column dense', icon: <GridDenseColIcon /> },
1484
+ ]
1485
+
1486
+ const COL_ALIGN = [
1487
+ { value: 'start', icon: <ColAlignStartIcon /> },
1488
+ { value: 'center', icon: <ColAlignCenterIcon /> },
1489
+ { value: 'end', icon: <ColAlignEndIcon /> },
1490
+ { value: 'stretch', icon: <ColAlignStretchIcon /> },
1491
+ ]
1492
+
1493
+ const ROW_ALIGN = [
1494
+ { value: 'start', icon: <RowAlignStartIcon /> },
1495
+ { value: 'center', icon: <RowAlignCenterIcon /> },
1496
+ { value: 'end', icon: <RowAlignEndIcon /> },
1497
+ { value: 'stretch', icon: <RowAlignStretchIcon /> },
1498
+ ]
1499
+
1500
+ return (
1501
+ <div
1502
+ className="space-y-2.5 pt-2"
1503
+ style={{ borderTop: '1px solid var(--border)' }}
1504
+ >
1505
+ {/* Grid dimensions */}
1506
+ <div className="flex items-center gap-2">
1507
+ <span
1508
+ className="text-[11px] w-[58px] flex-shrink-0"
1509
+ style={{ color: 'var(--text-secondary)' }}
1510
+ >
1511
+ Grid
1512
+ </span>
1513
+ <div className="flex-1 flex gap-2">
1514
+ <div className="flex-1">
1515
+ <NumberStepper
1516
+ value={colCount}
1517
+ onChange={(n) =>
1518
+ onChange('gridTemplateColumns', toGridTemplate(n))
1519
+ }
1520
+ />
1521
+ <div
1522
+ className="text-[9px] text-center mt-0.5"
1523
+ style={{ color: 'var(--text-muted)' }}
1524
+ >
1525
+ Columns
1526
+ </div>
1527
+ </div>
1528
+ <div className="flex-1">
1529
+ <NumberStepper
1530
+ value={rowCount}
1531
+ onChange={(n) => onChange('gridTemplateRows', toGridTemplate(n))}
1532
+ />
1533
+ <div
1534
+ className="text-[9px] text-center mt-0.5"
1535
+ style={{ color: 'var(--text-muted)' }}
1536
+ >
1537
+ Rows
1538
+ </div>
1539
+ </div>
1540
+ </div>
1541
+ </div>
1542
+
1543
+ {/* Direction */}
1544
+ <div className="flex items-center gap-2">
1545
+ <span
1546
+ className="text-[11px] w-[58px] flex-shrink-0"
1547
+ style={{ color: 'var(--text-secondary)' }}
1548
+ >
1549
+ Direction
1550
+ </span>
1551
+ <IconBar
1552
+ options={GRID_FLOW}
1553
+ value={autoFlow}
1554
+ onChange={(v) => onChange('gridAutoFlow', v)}
1555
+ />
1556
+ </div>
1557
+
1558
+ {/* Align */}
1559
+ <div className="flex items-center gap-2">
1560
+ <span
1561
+ className="text-[11px] w-[58px] flex-shrink-0"
1562
+ style={{ color: 'var(--text-secondary)' }}
1563
+ >
1564
+ Align
1565
+ </span>
1566
+ <GridAlignVisual />
1567
+ <div className="flex-1 flex flex-col gap-1.5">
1568
+ <div className="flex items-center gap-1.5">
1569
+ <span
1570
+ className="text-[10px] w-3"
1571
+ style={{ color: 'var(--text-muted)' }}
1572
+ >
1573
+ X
1574
+ </span>
1575
+ <LayoutDropdown
1576
+ value={justifyItems}
1577
+ options={GRID_ALIGN}
1578
+ onChange={(v) => onChange('justifyItems', v)}
1579
+ />
1580
+ </div>
1581
+ <div className="flex items-center gap-1.5">
1582
+ <span
1583
+ className="text-[10px] w-3"
1584
+ style={{ color: 'var(--text-muted)' }}
1585
+ >
1586
+ Y
1587
+ </span>
1588
+ <LayoutDropdown
1589
+ value={alignItems}
1590
+ options={GRID_ALIGN}
1591
+ onChange={(v) => onChange('alignItems', v)}
1592
+ />
1593
+ </div>
1594
+ </div>
1595
+ </div>
1596
+
1597
+ {/* Gap */}
1598
+ <div className="flex items-center gap-2">
1599
+ <span
1600
+ className="text-[11px] w-[58px] flex-shrink-0 flex items-center gap-1"
1601
+ style={{ color: 'var(--text-secondary)' }}
1602
+ >
1603
+ Gap
1604
+ <button
1605
+ type="button"
1606
+ onClick={() => setGapLinked(!gapLinked)}
1607
+ className="flex items-center justify-center w-4 h-4 rounded transition-colors"
1608
+ style={{ color: gapLinked ? 'var(--accent)' : 'var(--text-muted)' }}
1609
+ title={gapLinked ? 'Unlink column/row gap' : 'Link column/row gap'}
1610
+ >
1611
+ <LockIcon locked={gapLinked} />
1612
+ </button>
1613
+ </span>
1614
+ <div className="grid grid-cols-2 gap-2 flex-1">
1615
+ <div>
1616
+ <GapInput
1617
+ value={columnGap}
1618
+ property="columnGap"
1619
+ onChange={handleGapChange}
1620
+ />
1621
+ <div
1622
+ className="text-[9px] text-center mt-0.5"
1623
+ style={{ color: 'var(--text-muted)' }}
1624
+ >
1625
+ Columns
1626
+ </div>
1627
+ </div>
1628
+ <div>
1629
+ <GapInput
1630
+ value={rowGap}
1631
+ property="rowGap"
1632
+ onChange={handleGapChange}
1633
+ />
1634
+ <div
1635
+ className="text-[9px] text-center mt-0.5"
1636
+ style={{ color: 'var(--text-muted)' }}
1637
+ >
1638
+ Rows
1639
+ </div>
1640
+ </div>
1641
+ </div>
1642
+ </div>
1643
+
1644
+ {/* More alignment options */}
1645
+ <button
1646
+ type="button"
1647
+ onClick={() => setMoreAlign(!moreAlign)}
1648
+ className="flex items-center gap-1 text-[10px] transition-colors w-full justify-center py-1"
1649
+ style={{ color: 'var(--text-muted)' }}
1650
+ >
1651
+ <span
1652
+ style={{
1653
+ transform: moreAlign ? 'rotate(90deg)' : 'rotate(0deg)',
1654
+ transition: 'transform 0.15s',
1655
+ display: 'inline-block',
1656
+ fontSize: 8,
1657
+ }}
1658
+ >
1659
+
1660
+ </span>
1661
+ More alignment options
1662
+ </button>
1663
+
1664
+ {moreAlign && (
1665
+ <div className="space-y-2">
1666
+ <div className="flex items-center gap-2">
1667
+ <span
1668
+ className="text-[11px] w-[58px] flex-shrink-0"
1669
+ style={{ color: 'var(--text-secondary)' }}
1670
+ >
1671
+ Columns
1672
+ </span>
1673
+ <IconBar
1674
+ options={COL_ALIGN}
1675
+ value={justifyItems}
1676
+ onChange={(v) => onChange('justifyItems', v)}
1677
+ />
1678
+ </div>
1679
+ <div className="flex items-center gap-2">
1680
+ <span
1681
+ className="text-[11px] w-[58px] flex-shrink-0"
1682
+ style={{ color: 'var(--text-secondary)' }}
1683
+ >
1684
+ Rows
1685
+ </span>
1686
+ <IconBar
1687
+ options={ROW_ALIGN}
1688
+ value={alignItems}
1689
+ onChange={(v) => onChange('alignItems', v)}
1690
+ />
1691
+ </div>
1692
+ </div>
1693
+ )}
1694
+ </div>
1695
+ )
1696
+ }
1697
+
1698
+ // ─── Inline Controls ───────────────────────────────────────────────
1699
+
1700
+ function InlineControls({
1701
+ verticalAlign,
1702
+ onChange,
1703
+ }: {
1704
+ verticalAlign: string
1705
+ onChange: (prop: string, val: string) => void
1706
+ }) {
1707
+ return (
1708
+ <div
1709
+ className="space-y-2.5 pt-2"
1710
+ style={{ borderTop: '1px solid var(--border)' }}
1711
+ >
1712
+ <div className="flex items-center gap-2">
1713
+ <span
1714
+ className="text-[11px] w-[58px] flex-shrink-0"
1715
+ style={{ color: 'var(--text-secondary)' }}
1716
+ >
1717
+ Align
1718
+ </span>
1719
+ <LayoutDropdown
1720
+ value={verticalAlign}
1721
+ options={VERTICAL_ALIGN_OPTIONS}
1722
+ onChange={(v) => onChange('verticalAlign', v)}
1723
+ showDescription
1724
+ width={200}
1725
+ />
1726
+ </div>
1727
+ </div>
1728
+ )
1729
+ }
1730
+
1731
+ // ─── Main Component ────────────────────────────────────────────────
1732
+
1733
+ const LAYOUT_PROPERTIES = [
1734
+ 'display',
1735
+ 'flexDirection',
1736
+ 'justifyContent',
1737
+ 'alignItems',
1738
+ 'gap',
1739
+ 'columnGap',
1740
+ 'rowGap',
1741
+ 'gridTemplateColumns',
1742
+ 'gridTemplateRows',
1743
+ 'gridAutoFlow',
1744
+ 'justifyItems',
1745
+ 'verticalAlign',
1746
+ 'width',
1747
+ 'height',
1748
+ 'paddingTop',
1749
+ 'paddingRight',
1750
+ 'paddingBottom',
1751
+ 'paddingLeft',
1752
+ 'marginTop',
1753
+ 'marginRight',
1754
+ 'marginBottom',
1755
+ 'marginLeft',
1756
+ ]
1757
+
1758
+ export function LayoutSection() {
1759
+ const computedStyles = useEditorStore((state) => state.computedStyles)
1760
+ const { applyChange, resetProperty } = useChangeTracker()
1761
+
1762
+ const hasChanges = useEditorStore((s) => {
1763
+ const sp = s.selectorPath
1764
+ if (!sp) return false
1765
+ return s.styleChanges.some(
1766
+ (c) => c.elementSelector === sp && LAYOUT_PROPERTIES.includes(c.property),
1767
+ )
1768
+ })
1769
+
1770
+ const handleResetAll = useCallback(() => {
1771
+ const { selectorPath, styleChanges } = useEditorStore.getState()
1772
+ if (!selectorPath) return
1773
+ const matching = styleChanges.filter(
1774
+ (c) =>
1775
+ c.elementSelector === selectorPath &&
1776
+ LAYOUT_PROPERTIES.includes(c.property),
1777
+ )
1778
+ for (const c of matching) resetProperty(c.property)
1779
+ }, [resetProperty])
1780
+
1781
+ const handleChange = useCallback(
1782
+ (property: string, value: string) => {
1783
+ applyChange(property, value)
1784
+ },
1785
+ [applyChange],
1786
+ )
1787
+
1788
+ const handleReset = useCallback(
1789
+ (property: string) => {
1790
+ resetProperty(property)
1791
+ },
1792
+ [resetProperty],
1793
+ )
1794
+
1795
+ const rawDisplay = computedStyles.display || 'block'
1796
+ const display = resolveDisplay(rawDisplay)
1797
+ const flexDirection = computedStyles.flexDirection || 'row'
1798
+ const justifyContent = computedStyles.justifyContent || 'flex-start'
1799
+ const alignItems = computedStyles.alignItems || 'stretch'
1800
+ const gap = computedStyles.gap || '0px'
1801
+ const columnGap = computedStyles.columnGap || gap
1802
+ const rowGap = computedStyles.rowGap || gap
1803
+ const gridTemplateColumns = computedStyles.gridTemplateColumns || 'none'
1804
+ const gridTemplateRows = computedStyles.gridTemplateRows || 'none'
1805
+ const gridAutoFlow = computedStyles.gridAutoFlow || 'row'
1806
+ const justifyItems = computedStyles.justifyItems || 'stretch'
1807
+ const verticalAlign = computedStyles.verticalAlign || 'baseline'
1808
+
1809
+ const isFlex = display === 'flex' || display === 'inline-flex'
1810
+ const isGrid = display === 'grid' || display === 'inline-grid'
1811
+ const isInline = display === 'inline' || display === 'inline-block'
1812
+ const isNone = display === 'none'
1813
+
1814
+ const handleDisplayChange = useCallback(
1815
+ (mode: DisplayMode) => {
1816
+ handleChange('display', mode)
1817
+ // Set sensible defaults when switching modes
1818
+ if (mode === 'flex' || mode === 'inline-flex') {
1819
+ if (
1820
+ flexDirection !== 'row' &&
1821
+ flexDirection !== 'column' &&
1822
+ flexDirection !== 'row-reverse' &&
1823
+ flexDirection !== 'column-reverse'
1824
+ ) {
1825
+ handleChange('flexDirection', 'row')
1826
+ }
1827
+ }
1828
+ },
1829
+ [handleChange, flexDirection],
1830
+ )
1831
+
1832
+ return (
1833
+ <SectionHeader
1834
+ title="Layout"
1835
+ defaultOpen={true}
1836
+ hasChanges={hasChanges}
1837
+ onReset={handleResetAll}
1838
+ >
1839
+ {/* Display toggle */}
1840
+ <DisplayToggle display={display} onChange={handleDisplayChange} />
1841
+
1842
+ {/* Flex controls */}
1843
+ {isFlex && (
1844
+ <FlexControls
1845
+ direction={flexDirection}
1846
+ justifyContent={justifyContent}
1847
+ alignItems={alignItems}
1848
+ gap={gap}
1849
+ onChange={handleChange}
1850
+ />
1851
+ )}
1852
+
1853
+ {/* Grid controls */}
1854
+ {isGrid && (
1855
+ <GridControls
1856
+ columns={gridTemplateColumns}
1857
+ rows={gridTemplateRows}
1858
+ justifyItems={justifyItems}
1859
+ alignItems={alignItems}
1860
+ columnGap={columnGap}
1861
+ rowGap={rowGap}
1862
+ autoFlow={gridAutoFlow}
1863
+ onChange={handleChange}
1864
+ />
1865
+ )}
1866
+
1867
+ {/* Inline controls */}
1868
+ {isInline && (
1869
+ <InlineControls verticalAlign={verticalAlign} onChange={handleChange} />
1870
+ )}
1871
+
1872
+ {/* Padding */}
1873
+ {!isNone && (
1874
+ <div
1875
+ className="pt-1.5"
1876
+ style={{ borderTop: '1px solid var(--border)' }}
1877
+ >
1878
+ <LinkedInputPair
1879
+ label="Padding"
1880
+ values={{
1881
+ top: computedStyles.paddingTop || '0px',
1882
+ right: computedStyles.paddingRight || '0px',
1883
+ bottom: computedStyles.paddingBottom || '0px',
1884
+ left: computedStyles.paddingLeft || '0px',
1885
+ }}
1886
+ properties={{
1887
+ top: 'paddingTop',
1888
+ right: 'paddingRight',
1889
+ bottom: 'paddingBottom',
1890
+ left: 'paddingLeft',
1891
+ }}
1892
+ onChange={handleChange}
1893
+ onReset={handleReset}
1894
+ />
1895
+ </div>
1896
+ )}
1897
+
1898
+ {/* Margin */}
1899
+ {!isNone && (
1900
+ <LinkedInputPair
1901
+ label="Margin"
1902
+ values={{
1903
+ top: computedStyles.marginTop || '0px',
1904
+ right: computedStyles.marginRight || '0px',
1905
+ bottom: computedStyles.marginBottom || '0px',
1906
+ left: computedStyles.marginLeft || '0px',
1907
+ }}
1908
+ properties={{
1909
+ top: 'marginTop',
1910
+ right: 'marginRight',
1911
+ bottom: 'marginBottom',
1912
+ left: 'marginLeft',
1913
+ }}
1914
+ onChange={handleChange}
1915
+ onReset={handleReset}
1916
+ />
1917
+ )}
1918
+
1919
+ {/* Box Model Preview */}
1920
+ {!isNone && (
1921
+ <BoxModelPreview
1922
+ margin={{
1923
+ top: computedStyles.marginTop || '0px',
1924
+ right: computedStyles.marginRight || '0px',
1925
+ bottom: computedStyles.marginBottom || '0px',
1926
+ left: computedStyles.marginLeft || '0px',
1927
+ }}
1928
+ border={{
1929
+ top: computedStyles.borderTopWidth || '0px',
1930
+ right: computedStyles.borderRightWidth || '0px',
1931
+ bottom: computedStyles.borderBottomWidth || '0px',
1932
+ left: computedStyles.borderLeftWidth || '0px',
1933
+ }}
1934
+ padding={{
1935
+ top: computedStyles.paddingTop || '0px',
1936
+ right: computedStyles.paddingRight || '0px',
1937
+ bottom: computedStyles.paddingBottom || '0px',
1938
+ left: computedStyles.paddingLeft || '0px',
1939
+ }}
1940
+ width={computedStyles.width || 'auto'}
1941
+ height={computedStyles.height || 'auto'}
1942
+ onChange={handleChange}
1943
+ onReset={handleReset}
1944
+ />
1945
+ )}
1946
+ </SectionHeader>
1947
+ )
1948
+ }