@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,516 @@
1
+ 'use client'
2
+
3
+ import { useState, useMemo, useEffect, useCallback } from 'react'
4
+ import { useEditorStore } from '@/store'
5
+ import { SectionHeader } from '@/components/right-panel/design/inputs/SectionHeader'
6
+ import { ColorInput } from '@/components/right-panel/design/inputs/ColorInput'
7
+ import { GradientEditor } from './GradientEditor'
8
+ import { PlusIcon } from '@/components/right-panel/design/icons'
9
+ import { ColorPicker } from '@/components/common/ColorPicker'
10
+ import { parseGradient, serializeGradient } from '@/lib/gradientParser'
11
+ import { useChangeTracker } from '@/hooks/useChangeTracker'
12
+ import type { GradientData } from '@/types/gradient'
13
+
14
+ // ─── Constants ───────────────────────────────────────────────────
15
+
16
+ const BACKGROUND_PROPERTIES = [
17
+ 'backgroundColor',
18
+ 'backgroundImage',
19
+ 'backgroundSize',
20
+ 'backgroundPosition',
21
+ 'backgroundRepeat',
22
+ 'backgroundAttachment',
23
+ 'backgroundClip',
24
+ ]
25
+
26
+ const DEFAULT_GRADIENT: GradientData = {
27
+ type: 'linear',
28
+ angle: 180,
29
+ stops: [
30
+ { color: '#000000', position: 0, opacity: 1 },
31
+ { color: '#ffffff', position: 100, opacity: 1 },
32
+ ],
33
+ }
34
+
35
+ const CLIP_OPTIONS: { value: string; label: string }[] = [
36
+ { value: 'border-box', label: 'None' },
37
+ { value: 'padding-box', label: 'Clip to padding' },
38
+ { value: 'content-box', label: 'Clip to content' },
39
+ { value: 'text', label: 'Clip to text' },
40
+ ]
41
+
42
+ type BgLayerType = 'linear' | 'radial' | 'overlay'
43
+
44
+ // ─── Helpers ─────────────────────────────────────────────────────
45
+
46
+ function detectLayerType(bgImage: string): BgLayerType | null {
47
+ if (!bgImage || bgImage === 'none') return null
48
+ if (bgImage.includes('radial-gradient')) return 'radial'
49
+ if (bgImage.includes('linear-gradient') || bgImage.includes('conic-gradient'))
50
+ return 'linear'
51
+ return null
52
+ }
53
+
54
+ // ─── Layer Type Icons ────────────────────────────────────────────
55
+
56
+ function LinearTypeIcon(props: React.SVGProps<SVGSVGElement>) {
57
+ return (
58
+ <svg width={16} height={16} viewBox="0 0 16 16" fill="none" {...props}>
59
+ <rect
60
+ x={2}
61
+ y={2}
62
+ width={12}
63
+ height={12}
64
+ rx={2}
65
+ stroke="currentColor"
66
+ strokeWidth={1.2}
67
+ />
68
+ <line
69
+ x1={4}
70
+ y1={12}
71
+ x2={12}
72
+ y2={4}
73
+ stroke="currentColor"
74
+ strokeWidth={1.2}
75
+ strokeLinecap="round"
76
+ />
77
+ </svg>
78
+ )
79
+ }
80
+
81
+ function RadialTypeIcon(props: React.SVGProps<SVGSVGElement>) {
82
+ return (
83
+ <svg width={16} height={16} viewBox="0 0 16 16" fill="none" {...props}>
84
+ <circle cx={8} cy={8} r={3} stroke="currentColor" strokeWidth={1.2} />
85
+ <circle
86
+ cx={8}
87
+ cy={8}
88
+ r={6}
89
+ stroke="currentColor"
90
+ strokeWidth={1.2}
91
+ opacity={0.4}
92
+ />
93
+ </svg>
94
+ )
95
+ }
96
+
97
+ function OverlayTypeIcon(props: React.SVGProps<SVGSVGElement>) {
98
+ return (
99
+ <svg width={16} height={16} viewBox="0 0 16 16" fill="none" {...props}>
100
+ <rect
101
+ x={2}
102
+ y={4}
103
+ width={8}
104
+ height={8}
105
+ rx={1}
106
+ stroke="currentColor"
107
+ strokeWidth={1.2}
108
+ />
109
+ <rect
110
+ x={6}
111
+ y={2}
112
+ width={8}
113
+ height={8}
114
+ rx={1}
115
+ stroke="currentColor"
116
+ strokeWidth={1.2}
117
+ opacity={0.5}
118
+ />
119
+ </svg>
120
+ )
121
+ }
122
+
123
+ // ─── Trash icon ──────────────────────────────────────────────────
124
+
125
+ function TrashSmallIcon() {
126
+ return (
127
+ <svg width={12} height={12} viewBox="0 0 14 14" fill="none">
128
+ <path
129
+ d="M3 4h8M5.5 4V3a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1v1M4 4v7a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V4"
130
+ stroke="currentColor"
131
+ strokeWidth={1.2}
132
+ strokeLinecap="round"
133
+ strokeLinejoin="round"
134
+ />
135
+ </svg>
136
+ )
137
+ }
138
+
139
+ // ─── Sub-Components ──────────────────────────────────────────────
140
+
141
+ const LAYER_TYPES: {
142
+ type: BgLayerType
143
+ Icon: React.FC<React.SVGProps<SVGSVGElement>>
144
+ title: string
145
+ }[] = [
146
+ { type: 'linear', Icon: LinearTypeIcon, title: 'Linear' },
147
+ { type: 'radial', Icon: RadialTypeIcon, title: 'Radial' },
148
+ { type: 'overlay', Icon: OverlayTypeIcon, title: 'Overlay' },
149
+ ]
150
+
151
+ function TypeSelector({
152
+ value,
153
+ onChange,
154
+ }: {
155
+ value: BgLayerType
156
+ onChange: (type: BgLayerType) => void
157
+ }) {
158
+ return (
159
+ <div className="flex items-center gap-2">
160
+ <span
161
+ className="text-[11px] w-12 shrink-0"
162
+ style={{ color: 'var(--text-muted)' }}
163
+ >
164
+ Type
165
+ </span>
166
+ <div
167
+ className="flex gap-px rounded overflow-hidden"
168
+ style={{ border: '1px solid var(--border)' }}
169
+ >
170
+ {LAYER_TYPES.map(({ type, Icon, title }) => {
171
+ const active = value === type
172
+ return (
173
+ <button
174
+ key={type}
175
+ type="button"
176
+ className="flex items-center justify-center w-7 h-7"
177
+ style={{
178
+ background: active
179
+ ? 'rgba(74,158,255,0.15)'
180
+ : 'var(--bg-tertiary)',
181
+ color: active ? 'var(--accent)' : 'var(--text-secondary)',
182
+ border: 'none',
183
+ cursor: 'pointer',
184
+ }}
185
+ title={title}
186
+ onClick={() => onChange(type)}
187
+ >
188
+ <Icon />
189
+ </button>
190
+ )
191
+ })}
192
+ </div>
193
+ </div>
194
+ )
195
+ }
196
+
197
+ function OverlayPanel({
198
+ color,
199
+ onChange,
200
+ }: {
201
+ color: string
202
+ onChange: (color: string) => void
203
+ }) {
204
+ return (
205
+ <div className="flex items-center gap-2">
206
+ <span
207
+ className="text-[11px] w-12 shrink-0"
208
+ style={{ color: 'var(--text-muted)' }}
209
+ >
210
+ Color
211
+ </span>
212
+ <ColorPicker
213
+ value={color}
214
+ onChange={onChange}
215
+ onSelectVariable={onChange}
216
+ />
217
+ </div>
218
+ )
219
+ }
220
+
221
+ function ClipDropdown({
222
+ value,
223
+ onChange,
224
+ }: {
225
+ value: string
226
+ onChange: (val: string) => void
227
+ }) {
228
+ return (
229
+ <select
230
+ value={value}
231
+ onChange={(e) => onChange(e.target.value)}
232
+ className="w-full h-6 rounded text-[11px] px-1.5 cursor-pointer outline-none"
233
+ style={{
234
+ background: 'var(--bg-tertiary)',
235
+ border: '1px solid var(--border)',
236
+ color: 'var(--text-primary)',
237
+ }}
238
+ >
239
+ {CLIP_OPTIONS.map((opt) => (
240
+ <option key={opt.value} value={opt.value}>
241
+ {opt.label}
242
+ </option>
243
+ ))}
244
+ </select>
245
+ )
246
+ }
247
+
248
+ // ─── Main Component ──────────────────────────────────────────────
249
+
250
+ export function BackgroundSection() {
251
+ const computedStyles = useEditorStore((state) => state.computedStyles)
252
+ const cssVariableUsages = useEditorStore((state) => state.cssVariableUsages)
253
+ const selectorPath = useEditorStore((state) => state.selectorPath)
254
+ const { applyChange, resetProperty } = useChangeTracker()
255
+
256
+ const hasChanges = useEditorStore((s) => {
257
+ const sp = s.selectorPath
258
+ if (!sp) return false
259
+ return s.styleChanges.some(
260
+ (c) =>
261
+ c.elementSelector === sp && BACKGROUND_PROPERTIES.includes(c.property),
262
+ )
263
+ })
264
+
265
+ const handleResetAll = () => {
266
+ const { selectorPath: sp, styleChanges } = useEditorStore.getState()
267
+ if (!sp) return
268
+ const matching = styleChanges.filter(
269
+ (c) =>
270
+ c.elementSelector === sp && BACKGROUND_PROPERTIES.includes(c.property),
271
+ )
272
+ for (const c of matching) resetProperty(c.property)
273
+ }
274
+
275
+ // --- Read computed values ---
276
+ const bgImage = computedStyles.backgroundImage || 'none'
277
+ const rawBgColor = computedStyles.backgroundColor || ''
278
+ const bgColor =
279
+ !rawBgColor ||
280
+ rawBgColor === 'rgba(0, 0, 0, 0)' ||
281
+ rawBgColor === 'transparent'
282
+ ? 'transparent'
283
+ : rawBgColor
284
+ const bgClip = computedStyles.backgroundClip || 'border-box'
285
+
286
+ // --- Layer detection ---
287
+ const detectedType = useMemo(() => detectLayerType(bgImage), [bgImage])
288
+ const hasLayer = detectedType !== null
289
+
290
+ // --- Layer type state ---
291
+ const [layerType, setLayerType] = useState<BgLayerType>(
292
+ () => detectedType || 'linear',
293
+ )
294
+
295
+ // --- Gradient state ---
296
+ const parsedGradient = useMemo(() => parseGradient(bgImage), [bgImage])
297
+ const [gradientData, setGradientData] = useState<GradientData>(
298
+ () => parsedGradient || DEFAULT_GRADIENT,
299
+ )
300
+
301
+ // --- Overlay state ---
302
+ const [overlayColor, setOverlayColor] = useState('rgba(0, 0, 0, 0.50)')
303
+
304
+ // Sync when element changes
305
+ useEffect(() => {
306
+ const detected = detectLayerType(bgImage)
307
+ if (detected) {
308
+ setLayerType(detected)
309
+ }
310
+ if (parsedGradient) {
311
+ setGradientData(parsedGradient)
312
+ }
313
+ }, [selectorPath, bgImage, parsedGradient])
314
+
315
+ // --- Layer preview swatch ---
316
+ const layerSwatchBg = useMemo(() => {
317
+ if (layerType === 'linear' || layerType === 'radial') {
318
+ return serializeGradient(gradientData)
319
+ }
320
+ if (layerType === 'overlay') return overlayColor
321
+ return 'var(--bg-tertiary)'
322
+ }, [layerType, gradientData, overlayColor])
323
+
324
+ const layerLabel = useMemo(() => {
325
+ switch (layerType) {
326
+ case 'linear':
327
+ return 'Linear gradient'
328
+ case 'radial':
329
+ return 'Radial gradient'
330
+ case 'overlay':
331
+ return 'Overlay'
332
+ }
333
+ }, [layerType])
334
+
335
+ // --- Handlers ---
336
+ const handleColorChange = useCallback(
337
+ (property: string, value: string) => applyChange(property, value),
338
+ [applyChange],
339
+ )
340
+
341
+ const handleAddLayer = useCallback(() => {
342
+ setLayerType('linear')
343
+ setGradientData(DEFAULT_GRADIENT)
344
+ applyChange('backgroundImage', serializeGradient(DEFAULT_GRADIENT))
345
+ }, [applyChange])
346
+
347
+ const handleRemoveLayer = useCallback(() => {
348
+ applyChange('backgroundImage', 'none')
349
+ }, [applyChange])
350
+
351
+ const handleTypeChange = useCallback(
352
+ (newType: BgLayerType) => {
353
+ setLayerType(newType)
354
+ switch (newType) {
355
+ case 'linear': {
356
+ const data = { ...gradientData, type: 'linear' as const }
357
+ setGradientData(data)
358
+ applyChange('backgroundImage', serializeGradient(data))
359
+ break
360
+ }
361
+ case 'radial': {
362
+ const data = { ...gradientData, type: 'radial' as const }
363
+ setGradientData(data)
364
+ applyChange('backgroundImage', serializeGradient(data))
365
+ break
366
+ }
367
+ case 'overlay':
368
+ applyChange(
369
+ 'backgroundImage',
370
+ `linear-gradient(${overlayColor}, ${overlayColor})`,
371
+ )
372
+ break
373
+ }
374
+ },
375
+ [applyChange, gradientData, overlayColor],
376
+ )
377
+
378
+ const handleGradientChange = useCallback(
379
+ (data: GradientData) => {
380
+ setGradientData(data)
381
+ applyChange('backgroundImage', serializeGradient(data))
382
+ },
383
+ [applyChange],
384
+ )
385
+
386
+ const handleOverlayColorChange = useCallback(
387
+ (color: string) => {
388
+ setOverlayColor(color)
389
+ applyChange('backgroundImage', `linear-gradient(${color}, ${color})`)
390
+ },
391
+ [applyChange],
392
+ )
393
+
394
+ const handleClipChange = useCallback(
395
+ (val: string) => applyChange('backgroundClip', val),
396
+ [applyChange],
397
+ )
398
+
399
+ return (
400
+ <SectionHeader
401
+ title="Backgrounds"
402
+ defaultOpen={true}
403
+ hasChanges={hasChanges}
404
+ onReset={handleResetAll}
405
+ actions={
406
+ <button
407
+ type="button"
408
+ className="flex items-center justify-center w-5 h-5 rounded hover:opacity-80"
409
+ style={{ color: 'var(--text-muted)' }}
410
+ title="Add gradient"
411
+ onClick={handleAddLayer}
412
+ >
413
+ <PlusIcon />
414
+ </button>
415
+ }
416
+ >
417
+ <div className="space-y-2.5">
418
+ {/* ── Gradient header ── */}
419
+ <div className="space-y-1.5">
420
+ <div className="flex items-center justify-between">
421
+ <span
422
+ className="text-[10px] uppercase tracking-wider"
423
+ style={{ color: 'var(--text-muted)' }}
424
+ >
425
+ Gradient
426
+ </span>
427
+ {!hasLayer && (
428
+ <button
429
+ type="button"
430
+ className="flex items-center justify-center w-4 h-4 rounded hover:opacity-80"
431
+ style={{ color: 'var(--text-muted)' }}
432
+ title="Add gradient"
433
+ onClick={handleAddLayer}
434
+ >
435
+ <PlusIcon />
436
+ </button>
437
+ )}
438
+ </div>
439
+
440
+ {/* ── Layer panel ── */}
441
+ {hasLayer && (
442
+ <div
443
+ className="space-y-2.5 rounded p-2"
444
+ style={{
445
+ background: 'var(--bg-tertiary)',
446
+ border: '1px solid var(--border)',
447
+ }}
448
+ >
449
+ {/* Preview swatch + label + remove */}
450
+ <div className="flex items-center gap-2">
451
+ <div
452
+ className="w-6 h-6 rounded shrink-0"
453
+ style={{
454
+ background: `${layerSwatchBg}, repeating-conic-gradient(#333 0% 25%, #444 0% 50%) 0 0 / 6px 6px`,
455
+ border: '1px solid var(--border)',
456
+ }}
457
+ />
458
+ <span
459
+ className="text-[11px] flex-1 truncate"
460
+ style={{ color: 'var(--text-secondary)' }}
461
+ >
462
+ {layerLabel}
463
+ </span>
464
+ <button
465
+ type="button"
466
+ className="flex items-center justify-center w-5 h-5 rounded hover:opacity-80"
467
+ style={{ color: 'var(--text-muted)' }}
468
+ title="Remove layer"
469
+ onClick={handleRemoveLayer}
470
+ >
471
+ <TrashSmallIcon />
472
+ </button>
473
+ </div>
474
+
475
+ {/* Type selector */}
476
+ <TypeSelector value={layerType} onChange={handleTypeChange} />
477
+
478
+ {/* ── Type-specific content ── */}
479
+ {(layerType === 'linear' || layerType === 'radial') && (
480
+ <GradientEditor
481
+ value={gradientData}
482
+ onChange={handleGradientChange}
483
+ showTypeSelector={false}
484
+ />
485
+ )}
486
+
487
+ {layerType === 'overlay' && (
488
+ <OverlayPanel
489
+ color={overlayColor}
490
+ onChange={handleOverlayColorChange}
491
+ />
492
+ )}
493
+ </div>
494
+ )}
495
+ </div>
496
+
497
+ {/* ── Color row (always visible) ── */}
498
+ <ColorInput
499
+ label="Color"
500
+ value={bgColor}
501
+ property="backgroundColor"
502
+ onChange={handleColorChange}
503
+ varExpression={cssVariableUsages['background-color']}
504
+ />
505
+
506
+ {/* ── Clipping ── */}
507
+ <div className="space-y-1">
508
+ <span className="text-[10px]" style={{ color: 'var(--text-muted)' }}>
509
+ Clipping
510
+ </span>
511
+ <ClipDropdown value={bgClip} onChange={handleClipChange} />
512
+ </div>
513
+ </div>
514
+ </SectionHeader>
515
+ )
516
+ }
@@ -0,0 +1,161 @@
1
+ 'use client'
2
+
3
+ import { useEditorStore } from '@/store'
4
+ import { SectionHeader } from '@/components/right-panel/design/inputs/SectionHeader'
5
+ import { CompactInput } from '@/components/right-panel/design/inputs/CompactInput'
6
+ import { ColorInput } from '@/components/right-panel/design/inputs/ColorInput'
7
+ import { useChangeTracker } from '@/hooks/useChangeTracker'
8
+
9
+ const BORDER_STYLES = [
10
+ 'none',
11
+ 'solid',
12
+ 'dashed',
13
+ 'dotted',
14
+ 'double',
15
+ 'groove',
16
+ 'ridge',
17
+ 'inset',
18
+ 'outset',
19
+ ]
20
+
21
+ const BORDER_PROPERTIES = [
22
+ 'borderWidth',
23
+ 'borderStyle',
24
+ 'borderColor',
25
+ 'borderTopWidth',
26
+ 'borderRightWidth',
27
+ 'borderBottomWidth',
28
+ 'borderLeftWidth',
29
+ ]
30
+
31
+ export function BorderSection() {
32
+ const computedStyles = useEditorStore((state) => state.computedStyles)
33
+ const cssVariableUsages = useEditorStore((state) => state.cssVariableUsages)
34
+ const { applyChange, resetProperty } = useChangeTracker()
35
+
36
+ const hasChanges = useEditorStore((s) => {
37
+ const sp = s.selectorPath
38
+ if (!sp) return false
39
+ return s.styleChanges.some(
40
+ (c) => c.elementSelector === sp && BORDER_PROPERTIES.includes(c.property),
41
+ )
42
+ })
43
+
44
+ const handleResetAll = () => {
45
+ const { selectorPath, styleChanges } = useEditorStore.getState()
46
+ if (!selectorPath) return
47
+ const matching = styleChanges.filter(
48
+ (c) =>
49
+ c.elementSelector === selectorPath &&
50
+ BORDER_PROPERTIES.includes(c.property),
51
+ )
52
+ for (const c of matching) resetProperty(c.property)
53
+ }
54
+
55
+ const handleChange = (property: string, value: string) => {
56
+ applyChange(property, value)
57
+ }
58
+
59
+ const borderWidth = computedStyles.borderWidth || '0px'
60
+ const borderStyle = computedStyles.borderStyle || 'solid'
61
+ const borderColor = computedStyles.borderColor || '#000000'
62
+
63
+ const borderTopWidth = computedStyles.borderTopWidth || borderWidth
64
+ const borderRightWidth = computedStyles.borderRightWidth || borderWidth
65
+ const borderBottomWidth = computedStyles.borderBottomWidth || borderWidth
66
+ const borderLeftWidth = computedStyles.borderLeftWidth || borderWidth
67
+
68
+ return (
69
+ <SectionHeader
70
+ title="Border"
71
+ defaultOpen={false}
72
+ hasChanges={hasChanges}
73
+ onReset={handleResetAll}
74
+ >
75
+ {/* General Border */}
76
+ <div
77
+ className="space-y-1.5 pb-2"
78
+ style={{ borderBottom: '1px solid var(--border)' }}
79
+ >
80
+ <div className="grid grid-cols-2 gap-1.5">
81
+ <CompactInput
82
+ label="W"
83
+ value={borderWidth}
84
+ property="borderWidth"
85
+ onChange={handleChange}
86
+ units={['px', 'em', 'rem']}
87
+ min={0}
88
+ />
89
+ <select
90
+ value={borderStyle}
91
+ onChange={(e) => handleChange('borderStyle', e.target.value)}
92
+ className="h-6 rounded text-[11px] px-1.5 cursor-pointer outline-none"
93
+ style={{
94
+ background: 'var(--bg-tertiary)',
95
+ border: '1px solid var(--border)',
96
+ color: 'var(--text-primary)',
97
+ }}
98
+ >
99
+ {BORDER_STYLES.map((s) => (
100
+ <option key={s} value={s}>
101
+ {s}
102
+ </option>
103
+ ))}
104
+ </select>
105
+ </div>
106
+
107
+ <ColorInput
108
+ label="Color"
109
+ value={borderColor}
110
+ property="borderColor"
111
+ onChange={handleChange}
112
+ varExpression={cssVariableUsages['border-color']}
113
+ />
114
+ </div>
115
+
116
+ {/* Individual Widths */}
117
+ <div className="pt-1.5">
118
+ <div
119
+ className="text-[10px] font-medium mb-1.5"
120
+ style={{ color: 'var(--text-muted)' }}
121
+ >
122
+ Individual Widths
123
+ </div>
124
+ <div className="grid grid-cols-2 gap-1.5">
125
+ <CompactInput
126
+ label="T"
127
+ value={borderTopWidth}
128
+ property="borderTopWidth"
129
+ onChange={handleChange}
130
+ units={['px', 'em', 'rem']}
131
+ min={0}
132
+ />
133
+ <CompactInput
134
+ label="R"
135
+ value={borderRightWidth}
136
+ property="borderRightWidth"
137
+ onChange={handleChange}
138
+ units={['px', 'em', 'rem']}
139
+ min={0}
140
+ />
141
+ <CompactInput
142
+ label="B"
143
+ value={borderBottomWidth}
144
+ property="borderBottomWidth"
145
+ onChange={handleChange}
146
+ units={['px', 'em', 'rem']}
147
+ min={0}
148
+ />
149
+ <CompactInput
150
+ label="L"
151
+ value={borderLeftWidth}
152
+ property="borderLeftWidth"
153
+ onChange={handleChange}
154
+ units={['px', 'em', 'rem']}
155
+ min={0}
156
+ />
157
+ </div>
158
+ </div>
159
+ </SectionHeader>
160
+ )
161
+ }