@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,467 @@
1
+ 'use client'
2
+
3
+ import { useState, useRef, useCallback } from 'react'
4
+ import { parseCSSValue, formatCSSValue } from '@/lib/utils'
5
+ import { useEditorStore } from '@/store'
6
+
7
+ interface BoxModelPreviewProps {
8
+ margin: { top: string; right: string; bottom: string; left: string }
9
+ border: { top: string; right: string; bottom: string; left: string }
10
+ padding: { top: string; right: string; bottom: string; left: string }
11
+ width: string
12
+ height: string
13
+ onChange: (property: string, value: string) => void
14
+ onReset?: (property: string) => void
15
+ }
16
+
17
+ // All box-model properties for "Reset All"
18
+ const BOX_MODEL_PROPERTIES = [
19
+ 'marginTop',
20
+ 'marginRight',
21
+ 'marginBottom',
22
+ 'marginLeft',
23
+ 'borderTopWidth',
24
+ 'borderRightWidth',
25
+ 'borderBottomWidth',
26
+ 'borderLeftWidth',
27
+ 'paddingTop',
28
+ 'paddingRight',
29
+ 'paddingBottom',
30
+ 'paddingLeft',
31
+ ]
32
+
33
+ // Editable value label with drag-to-scrub + double-click to edit
34
+ function ScrubValue({
35
+ value,
36
+ property,
37
+ color,
38
+ onChange,
39
+ }: {
40
+ value: string
41
+ property: string
42
+ color: string
43
+ onChange: (property: string, value: string) => void
44
+ onReset?: (property: string) => void
45
+ }) {
46
+ const spanRef = useRef<HTMLSpanElement>(null)
47
+ const inputRef = useRef<HTMLInputElement>(null)
48
+ const isDragging = useRef(false)
49
+ const hasDragged = useRef(false)
50
+ const dragStartX = useRef(0)
51
+ const dragStartVal = useRef(0)
52
+ const [isEditing, setIsEditing] = useState(false)
53
+ const [editValue, setEditValue] = useState('')
54
+
55
+ const parsed = parseCSSValue(value)
56
+ const num = parsed.number
57
+ const unit = parsed.unit || 'px'
58
+
59
+ const hasChange = useEditorStore((s) => {
60
+ const sp = s.selectorPath
61
+ return sp
62
+ ? s.styleChanges.some(
63
+ (c) => c.elementSelector === sp && c.property === property,
64
+ )
65
+ : false
66
+ })
67
+
68
+ const handlePointerDown = useCallback(
69
+ (e: React.PointerEvent) => {
70
+ if (isEditing) return
71
+ e.preventDefault()
72
+ isDragging.current = true
73
+ hasDragged.current = false
74
+ dragStartX.current = e.clientX
75
+ dragStartVal.current = num
76
+ spanRef.current?.setPointerCapture(e.pointerId)
77
+ document.body.style.cursor = 'ew-resize'
78
+ document.body.style.userSelect = 'none'
79
+ },
80
+ [num, isEditing],
81
+ )
82
+
83
+ const handlePointerMove = useCallback(
84
+ (e: React.PointerEvent) => {
85
+ if (!isDragging.current) return
86
+ hasDragged.current = true
87
+ const delta = e.clientX - dragStartX.current
88
+ // Base: 2 per pixel. Shift = 10x, Alt/Option = 0.1x
89
+ const multiplier = e.shiftKey ? 10 : e.altKey ? 0.1 : 1
90
+ const next = Math.max(
91
+ 0,
92
+ Math.round((dragStartVal.current + delta * 2 * multiplier) * 100) / 100,
93
+ )
94
+ onChange(property, formatCSSValue(next, unit))
95
+ },
96
+ [onChange, property, unit],
97
+ )
98
+
99
+ const handlePointerUp = useCallback((e: React.PointerEvent) => {
100
+ if (!isDragging.current) return
101
+ isDragging.current = false
102
+ spanRef.current?.releasePointerCapture(e.pointerId)
103
+ document.body.style.cursor = ''
104
+ document.body.style.userSelect = ''
105
+ }, [])
106
+
107
+ // Double-click enters inline edit mode
108
+ const handleDoubleClick = useCallback(() => {
109
+ setEditValue(String(num))
110
+ setIsEditing(true)
111
+ // Focus the input after React renders it
112
+ requestAnimationFrame(() => {
113
+ inputRef.current?.focus()
114
+ inputRef.current?.select()
115
+ })
116
+ }, [num])
117
+
118
+ const commitEdit = useCallback(() => {
119
+ setIsEditing(false)
120
+ const n = parseFloat(editValue)
121
+ if (!isNaN(n)) {
122
+ onChange(property, formatCSSValue(Math.max(0, n), unit))
123
+ }
124
+ }, [editValue, onChange, property, unit])
125
+
126
+ const handleEditKeyDown = useCallback(
127
+ (e: React.KeyboardEvent) => {
128
+ if (e.key === 'Enter') {
129
+ commitEdit()
130
+ } else if (e.key === 'Escape') {
131
+ setIsEditing(false)
132
+ }
133
+ },
134
+ [commitEdit],
135
+ )
136
+
137
+ if (isEditing) {
138
+ return (
139
+ <input
140
+ ref={inputRef}
141
+ type="text"
142
+ inputMode="numeric"
143
+ value={editValue}
144
+ onChange={(e) => setEditValue(e.target.value)}
145
+ onBlur={commitEdit}
146
+ onKeyDown={handleEditKeyDown}
147
+ className="text-[10px] leading-none tabular-nums bg-transparent border-none outline-none text-center"
148
+ style={{
149
+ color,
150
+ width: '30px',
151
+ padding: '1px 2px',
152
+ borderRadius: '2px',
153
+ background: 'rgba(255,255,255,0.08)',
154
+ }}
155
+ />
156
+ )
157
+ }
158
+
159
+ return (
160
+ <span
161
+ ref={spanRef}
162
+ onPointerDown={handlePointerDown}
163
+ onPointerMove={handlePointerMove}
164
+ onPointerUp={handlePointerUp}
165
+ onDoubleClick={handleDoubleClick}
166
+ className="text-[10px] leading-none select-none tabular-nums"
167
+ style={{
168
+ color: hasChange ? color : `${color}99`,
169
+ cursor: 'ew-resize',
170
+ minWidth: '14px',
171
+ textAlign: 'center',
172
+ fontWeight: hasChange ? 600 : 400,
173
+ }}
174
+ title="Drag to scrub, double-click to edit"
175
+ >
176
+ {num}
177
+ </span>
178
+ )
179
+ }
180
+
181
+ export function BoxModelPreview({
182
+ margin,
183
+ border,
184
+ padding,
185
+ width,
186
+ height,
187
+ onChange,
188
+ onReset,
189
+ }: BoxModelPreviewProps) {
190
+ const contentW = parseCSSValue(width).number
191
+ const contentH = parseCSSValue(height).number
192
+
193
+ // Check if any box-model property has a tracked change
194
+ const hasAnyChange = useEditorStore((s) => {
195
+ const sp = s.selectorPath
196
+ if (!sp) return false
197
+ return s.styleChanges.some(
198
+ (c) =>
199
+ c.elementSelector === sp && BOX_MODEL_PROPERTIES.includes(c.property),
200
+ )
201
+ })
202
+
203
+ const handleResetAll = useCallback(() => {
204
+ if (!onReset) return
205
+ for (const prop of BOX_MODEL_PROPERTIES) {
206
+ onReset(prop)
207
+ }
208
+ }, [onReset])
209
+
210
+ // Dim-display helper for the content dimension text
211
+ const dimText =
212
+ (contentW ? `${contentW}` : '–') + ' × ' + (contentH ? `${contentH}` : '–')
213
+
214
+ return (
215
+ <div className="pt-1.5" style={{ borderTop: '1px solid var(--border)' }}>
216
+ <div className="flex items-center justify-between mb-1">
217
+ <span
218
+ className="text-[10px] font-medium"
219
+ style={{ color: 'var(--text-muted)' }}
220
+ >
221
+ Box Model
222
+ </span>
223
+ {hasAnyChange && (
224
+ <button
225
+ type="button"
226
+ onClick={handleResetAll}
227
+ className="text-[9px] px-1.5 py-0.5 rounded hover:opacity-80"
228
+ style={{
229
+ color: 'var(--accent)',
230
+ background: 'rgba(74, 158, 255, 0.10)',
231
+ border: 'none',
232
+ cursor: 'pointer',
233
+ }}
234
+ >
235
+ Reset
236
+ </button>
237
+ )}
238
+ </div>
239
+
240
+ {/* Margin layer */}
241
+ <div
242
+ className="relative flex flex-col items-center"
243
+ style={{
244
+ background: 'rgba(251, 191, 36, 0.06)',
245
+ border: '1px solid rgba(251, 191, 36, 0.20)',
246
+ borderRadius: '4px',
247
+ padding: '6px 8px',
248
+ }}
249
+ >
250
+ {/* Margin label */}
251
+ <span
252
+ className="absolute top-0.5 left-1.5 text-[8px] uppercase tracking-wider"
253
+ style={{ color: 'rgba(251, 191, 36, 0.50)' }}
254
+ >
255
+ margin
256
+ </span>
257
+
258
+ {/* Margin top */}
259
+ <div className="flex justify-center py-0.5">
260
+ <ScrubValue
261
+ value={margin.top}
262
+ property="marginTop"
263
+ color="#fbbf24"
264
+ onChange={onChange}
265
+ onReset={onReset}
266
+ />
267
+ </div>
268
+
269
+ {/* Margin left + border layer + margin right */}
270
+ <div className="flex items-center w-full">
271
+ <div
272
+ className="flex justify-center px-1"
273
+ style={{ minWidth: '20px' }}
274
+ >
275
+ <ScrubValue
276
+ value={margin.left}
277
+ property="marginLeft"
278
+ color="#fbbf24"
279
+ onChange={onChange}
280
+ onReset={onReset}
281
+ />
282
+ </div>
283
+
284
+ {/* Border layer */}
285
+ <div
286
+ className="relative flex-1 flex flex-col items-center"
287
+ style={{
288
+ background: 'rgba(163, 163, 163, 0.08)',
289
+ border: '1px solid rgba(163, 163, 163, 0.25)',
290
+ borderRadius: '3px',
291
+ padding: '5px 6px',
292
+ }}
293
+ >
294
+ <span
295
+ className="absolute top-0.5 left-1 text-[8px] uppercase tracking-wider"
296
+ style={{ color: 'rgba(163, 163, 163, 0.50)' }}
297
+ >
298
+ border
299
+ </span>
300
+
301
+ {/* Border top */}
302
+ <div className="flex justify-center py-0.5">
303
+ <ScrubValue
304
+ value={border.top}
305
+ property="borderTopWidth"
306
+ color="#a3a3a3"
307
+ onChange={onChange}
308
+ onReset={onReset}
309
+ />
310
+ </div>
311
+
312
+ {/* Border left + padding layer + border right */}
313
+ <div className="flex items-center w-full">
314
+ <div
315
+ className="flex justify-center px-1"
316
+ style={{ minWidth: '16px' }}
317
+ >
318
+ <ScrubValue
319
+ value={border.left}
320
+ property="borderLeftWidth"
321
+ color="#a3a3a3"
322
+ onChange={onChange}
323
+ onReset={onReset}
324
+ />
325
+ </div>
326
+
327
+ {/* Padding layer */}
328
+ <div
329
+ className="relative flex-1 flex flex-col items-center"
330
+ style={{
331
+ background: 'rgba(74, 222, 128, 0.06)',
332
+ border: '1px solid rgba(74, 222, 128, 0.20)',
333
+ borderRadius: '2px',
334
+ padding: '4px 4px',
335
+ }}
336
+ >
337
+ <span
338
+ className="absolute top-0 left-1 text-[8px] uppercase tracking-wider"
339
+ style={{ color: 'rgba(74, 222, 128, 0.50)' }}
340
+ >
341
+ padding
342
+ </span>
343
+
344
+ {/* Padding top */}
345
+ <div className="flex justify-center py-0.5">
346
+ <ScrubValue
347
+ value={padding.top}
348
+ property="paddingTop"
349
+ color="#4ade80"
350
+ onChange={onChange}
351
+ onReset={onReset}
352
+ />
353
+ </div>
354
+
355
+ {/* Padding left + content + padding right */}
356
+ <div className="flex items-center w-full">
357
+ <div
358
+ className="flex justify-center px-0.5"
359
+ style={{ minWidth: '14px' }}
360
+ >
361
+ <ScrubValue
362
+ value={padding.left}
363
+ property="paddingLeft"
364
+ color="#4ade80"
365
+ onChange={onChange}
366
+ onReset={onReset}
367
+ />
368
+ </div>
369
+
370
+ {/* Content box */}
371
+ <div
372
+ className="flex-1 flex items-center justify-center py-2"
373
+ style={{
374
+ background: 'rgba(74, 158, 255, 0.08)',
375
+ border: '1px solid rgba(74, 158, 255, 0.25)',
376
+ borderRadius: '2px',
377
+ minHeight: '24px',
378
+ }}
379
+ >
380
+ <span
381
+ className="text-[9px] tabular-nums whitespace-nowrap"
382
+ style={{ color: 'rgba(74, 158, 255, 0.7)' }}
383
+ >
384
+ {dimText}
385
+ </span>
386
+ </div>
387
+
388
+ <div
389
+ className="flex justify-center px-0.5"
390
+ style={{ minWidth: '14px' }}
391
+ >
392
+ <ScrubValue
393
+ value={padding.right}
394
+ property="paddingRight"
395
+ color="#4ade80"
396
+ onChange={onChange}
397
+ onReset={onReset}
398
+ />
399
+ </div>
400
+ </div>
401
+
402
+ {/* Padding bottom */}
403
+ <div className="flex justify-center py-0.5">
404
+ <ScrubValue
405
+ value={padding.bottom}
406
+ property="paddingBottom"
407
+ color="#4ade80"
408
+ onChange={onChange}
409
+ onReset={onReset}
410
+ />
411
+ </div>
412
+ </div>
413
+
414
+ <div
415
+ className="flex justify-center px-1"
416
+ style={{ minWidth: '16px' }}
417
+ >
418
+ <ScrubValue
419
+ value={border.right}
420
+ property="borderRightWidth"
421
+ color="#a3a3a3"
422
+ onChange={onChange}
423
+ onReset={onReset}
424
+ />
425
+ </div>
426
+ </div>
427
+
428
+ {/* Border bottom */}
429
+ <div className="flex justify-center py-0.5">
430
+ <ScrubValue
431
+ value={border.bottom}
432
+ property="borderBottomWidth"
433
+ color="#a3a3a3"
434
+ onChange={onChange}
435
+ onReset={onReset}
436
+ />
437
+ </div>
438
+ </div>
439
+
440
+ <div
441
+ className="flex justify-center px-1"
442
+ style={{ minWidth: '20px' }}
443
+ >
444
+ <ScrubValue
445
+ value={margin.right}
446
+ property="marginRight"
447
+ color="#fbbf24"
448
+ onChange={onChange}
449
+ onReset={onReset}
450
+ />
451
+ </div>
452
+ </div>
453
+
454
+ {/* Margin bottom */}
455
+ <div className="flex justify-center py-0.5">
456
+ <ScrubValue
457
+ value={margin.bottom}
458
+ property="marginBottom"
459
+ color="#fbbf24"
460
+ onChange={onChange}
461
+ onReset={onReset}
462
+ />
463
+ </div>
464
+ </div>
465
+ </div>
466
+ )
467
+ }
@@ -0,0 +1,43 @@
1
+ 'use client'
2
+
3
+ import { useEditorStore } from '@/store'
4
+ import { VariableColorPicker } from '@/components/common/VariableColorPicker'
5
+
6
+ interface ColorInputProps {
7
+ value: string
8
+ property: string
9
+ onChange: (property: string, value: string) => void
10
+ varExpression?: string
11
+ label?: string
12
+ }
13
+
14
+ export function ColorInput({
15
+ value,
16
+ property,
17
+ onChange,
18
+ varExpression,
19
+ label,
20
+ }: ColorInputProps) {
21
+ const selectorPath = useEditorStore((s) => s.selectorPath)
22
+ const detachProperty = useEditorStore((s) => s.detachProperty)
23
+ const reattachProperty = useEditorStore((s) => s.reattachProperty)
24
+ const tailwindEntry = useEditorStore((s) => s.tailwindClassMap[property])
25
+
26
+ return (
27
+ <VariableColorPicker
28
+ label={label || ''}
29
+ property={property}
30
+ value={value}
31
+ varExpression={varExpression}
32
+ tailwindClassName={tailwindEntry?.className}
33
+ onChange={onChange}
34
+ onDetach={() => selectorPath && detachProperty(selectorPath, property)}
35
+ onReattach={(expr) => {
36
+ if (selectorPath) {
37
+ reattachProperty(selectorPath, property)
38
+ onChange(property, expr)
39
+ }
40
+ }}
41
+ />
42
+ )
43
+ }