@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,86 @@
1
+ 'use client'
2
+
3
+ import { useEditorStore } from '@/store'
4
+ import { SectionHeader } from '@/components/right-panel/design/inputs/SectionHeader'
5
+
6
+ export function PropertiesSection() {
7
+ const tagName = useEditorStore((s) => s.tagName)
8
+ const elementId = useEditorStore((s) => s.elementId)
9
+ const className = useEditorStore((s) => s.className)
10
+ const attributes = useEditorStore((s) => s.attributes)
11
+
12
+ const classes = className ? className.split(/\s+/).filter(Boolean) : []
13
+ const attrEntries = Object.entries(attributes).filter(
14
+ ([key]) => key !== 'class' && key !== 'id' && key !== 'style',
15
+ )
16
+
17
+ return (
18
+ <SectionHeader title="Properties" defaultOpen={false}>
19
+ <div className="space-y-2 font-mono text-[11px]">
20
+ {/* Tag name */}
21
+ <div className="flex items-center gap-2">
22
+ <span style={{ color: 'var(--text-secondary)' }}>Tag</span>
23
+ <span style={{ color: 'var(--text-primary)' }}>{tagName || '—'}</span>
24
+ </div>
25
+
26
+ {/* Element ID */}
27
+ {elementId && (
28
+ <div className="flex items-center gap-2">
29
+ <span style={{ color: 'var(--text-secondary)' }}>ID</span>
30
+ <span style={{ color: 'var(--accent)' }}>#{elementId}</span>
31
+ </div>
32
+ )}
33
+
34
+ {/* Classes */}
35
+ {classes.length > 0 && (
36
+ <div>
37
+ <span
38
+ className="block mb-1"
39
+ style={{ color: 'var(--text-secondary)' }}
40
+ >
41
+ Classes
42
+ </span>
43
+ <div className="flex flex-wrap gap-1">
44
+ {classes.map((cls, i) => (
45
+ <span
46
+ key={i}
47
+ className="px-1.5 py-0.5 rounded text-[10px]"
48
+ style={{
49
+ background: 'var(--bg-tertiary)',
50
+ color: 'var(--text-primary)',
51
+ border: '1px solid var(--border)',
52
+ }}
53
+ >
54
+ .{cls}
55
+ </span>
56
+ ))}
57
+ </div>
58
+ </div>
59
+ )}
60
+
61
+ {/* Attributes */}
62
+ {attrEntries.length > 0 && (
63
+ <div>
64
+ <span
65
+ className="block mb-1"
66
+ style={{ color: 'var(--text-secondary)' }}
67
+ >
68
+ Attributes
69
+ </span>
70
+ <div className="space-y-0.5">
71
+ {attrEntries.map(([key, value]) => (
72
+ <div key={key}>
73
+ <span style={{ color: 'var(--accent)' }}>{key}</span>
74
+ <span style={{ color: 'var(--text-muted)' }}>=</span>
75
+ <span style={{ color: 'var(--text-primary)' }}>
76
+ &quot;{value}&quot;
77
+ </span>
78
+ </div>
79
+ ))}
80
+ </div>
81
+ </div>
82
+ )}
83
+ </div>
84
+ </SectionHeader>
85
+ )
86
+ }
@@ -0,0 +1,361 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useState } 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 { useChangeTracker } from '@/hooks/useChangeTracker'
8
+ import { sendViaIframe } from '@/hooks/usePostMessage'
9
+ import { generateId } from '@/lib/utils'
10
+
11
+ const SVG_PROPERTIES = ['fill', 'stroke']
12
+
13
+ // ─── Save as Variable Row ──────────────────────────────────────
14
+
15
+ function SaveAsVariableRow({
16
+ property,
17
+ onSave,
18
+ existingVarName,
19
+ onRemove,
20
+ }: {
21
+ property: string
22
+ onSave: (varName: string) => void
23
+ existingVarName: string | null
24
+ onRemove: () => void
25
+ }) {
26
+ const [editing, setEditing] = useState(false)
27
+ const [varName, setVarName] = useState(`--svg-${property}`)
28
+
29
+ const handleSave = () => {
30
+ const name = varName.trim()
31
+ if (!name) return
32
+ // Ensure it starts with --
33
+ const finalName = name.startsWith('--') ? name : `--${name}`
34
+ onSave(finalName)
35
+ setEditing(false)
36
+ }
37
+
38
+ const handleKeyDown = (e: React.KeyboardEvent) => {
39
+ if (e.key === 'Enter') handleSave()
40
+ if (e.key === 'Escape') setEditing(false)
41
+ }
42
+
43
+ if (existingVarName) {
44
+ return (
45
+ <div className="flex items-center gap-1.5 pl-1">
46
+ <span
47
+ className="text-[10px] truncate flex-1"
48
+ style={{ color: 'var(--accent)' }}
49
+ >
50
+ {existingVarName}
51
+ </span>
52
+ <button
53
+ type="button"
54
+ onClick={onRemove}
55
+ className="text-[10px] px-1.5 py-0.5 rounded shrink-0"
56
+ style={{
57
+ color: 'var(--text-muted)',
58
+ background: 'var(--bg-tertiary)',
59
+ }}
60
+ title="Remove variable"
61
+ >
62
+ Remove
63
+ </button>
64
+ </div>
65
+ )
66
+ }
67
+
68
+ if (editing) {
69
+ return (
70
+ <div className="flex items-center gap-1 pl-1">
71
+ <input
72
+ autoFocus
73
+ value={varName}
74
+ onChange={(e) => setVarName(e.target.value)}
75
+ onKeyDown={handleKeyDown}
76
+ className="flex-1 h-6 rounded text-[11px] px-1.5 outline-none min-w-0"
77
+ style={{
78
+ background: 'var(--bg-tertiary)',
79
+ border: '1px solid var(--accent)',
80
+ color: 'var(--text-primary)',
81
+ }}
82
+ placeholder="--variable-name"
83
+ />
84
+ <button
85
+ type="button"
86
+ onClick={handleSave}
87
+ className="text-[10px] px-1.5 py-0.5 rounded shrink-0"
88
+ style={{ color: '#fff', background: 'var(--accent)' }}
89
+ >
90
+ Save
91
+ </button>
92
+ <button
93
+ type="button"
94
+ onClick={() => setEditing(false)}
95
+ className="text-[10px] px-1.5 py-0.5 rounded shrink-0"
96
+ style={{
97
+ color: 'var(--text-muted)',
98
+ background: 'var(--bg-tertiary)',
99
+ }}
100
+ >
101
+ Cancel
102
+ </button>
103
+ </div>
104
+ )
105
+ }
106
+
107
+ return (
108
+ <button
109
+ type="button"
110
+ onClick={() => setEditing(true)}
111
+ className="flex items-center gap-1 pl-1 text-[10px] transition-colors hover:opacity-80"
112
+ style={{
113
+ color: 'var(--text-muted)',
114
+ background: 'none',
115
+ border: 'none',
116
+ cursor: 'pointer',
117
+ }}
118
+ >
119
+ <svg width={10} height={10} viewBox="0 0 12 12" fill="none">
120
+ <path
121
+ d="M2 2h3v8H2zM7 2h3v8H7z"
122
+ stroke="currentColor"
123
+ strokeWidth={1}
124
+ strokeLinecap="round"
125
+ />
126
+ <path
127
+ d="M5 6h2"
128
+ stroke="currentColor"
129
+ strokeWidth={1}
130
+ strokeLinecap="round"
131
+ />
132
+ </svg>
133
+ Save as variable
134
+ </button>
135
+ )
136
+ }
137
+
138
+ // ─── Helpers ────────────────────────────────────────────────────
139
+
140
+ /** Extract variable name from a var() expression */
141
+ function extractVarName(value: string): string | null {
142
+ const match = value.match(/^var\((--[^,)]+)\)/)
143
+ return match ? match[1] : null
144
+ }
145
+
146
+ /** Resolve a var() value by looking up the :root change in the store */
147
+ function resolveVarValue(value: string): string | null {
148
+ const varName = extractVarName(value)
149
+ if (!varName) return null
150
+ const { styleChanges } = useEditorStore.getState()
151
+ const rootChange = styleChanges.find(
152
+ (c) => c.elementSelector === ':root' && c.property === varName,
153
+ )
154
+ return rootChange ? rootChange.newValue : null
155
+ }
156
+
157
+ // ─── Main Component ─────────────────────────────────────────────
158
+
159
+ export function SVGSection() {
160
+ const computedStyles = useEditorStore((state) => state.computedStyles)
161
+ const cssVariableUsages = useEditorStore((state) => state.cssVariableUsages)
162
+ const addStyleChange = useEditorStore((s) => s.addStyleChange)
163
+ const removeStyleChange = useEditorStore((s) => s.removeStyleChange)
164
+ const { applyChange, resetProperty } = useChangeTracker()
165
+
166
+ // Track which properties have been saved as variables
167
+ const fillVarName = useEditorStore((s) => {
168
+ const sp = s.selectorPath
169
+ if (!sp) return null
170
+ const fillChange = s.styleChanges.find(
171
+ (c) => c.elementSelector === sp && c.property === 'fill',
172
+ )
173
+ if (fillChange) return extractVarName(fillChange.newValue)
174
+ return null
175
+ })
176
+
177
+ const strokeVarName = useEditorStore((s) => {
178
+ const sp = s.selectorPath
179
+ if (!sp) return null
180
+ const strokeChange = s.styleChanges.find(
181
+ (c) => c.elementSelector === sp && c.property === 'stroke',
182
+ )
183
+ if (strokeChange) return extractVarName(strokeChange.newValue)
184
+ return null
185
+ })
186
+
187
+ const hasChanges = useEditorStore((s) => {
188
+ const sp = s.selectorPath
189
+ if (!sp) return false
190
+ return s.styleChanges.some(
191
+ (c) => c.elementSelector === sp && SVG_PROPERTIES.includes(c.property),
192
+ )
193
+ })
194
+
195
+ const handleResetAll = () => {
196
+ const { selectorPath, styleChanges } = useEditorStore.getState()
197
+ if (!selectorPath) return
198
+ const matching = styleChanges.filter(
199
+ (c) =>
200
+ c.elementSelector === selectorPath &&
201
+ SVG_PROPERTIES.includes(c.property),
202
+ )
203
+ for (const c of matching) {
204
+ // Also remove associated :root variable definitions
205
+ const varName = extractVarName(c.newValue)
206
+ if (varName) {
207
+ const rootChange = styleChanges.find(
208
+ (rc) => rc.elementSelector === ':root' && rc.property === varName,
209
+ )
210
+ if (rootChange) removeStyleChange(rootChange.id)
211
+ // Revert the :root inline style in the iframe
212
+ sendViaIframe({
213
+ type: 'REVERT_CHANGE',
214
+ payload: { selectorPath: ':root', property: varName },
215
+ })
216
+ }
217
+ resetProperty(c.property)
218
+ }
219
+ }
220
+
221
+ const handleColorChange = useCallback(
222
+ (property: string, value: string) => applyChange(property, value),
223
+ [applyChange],
224
+ )
225
+
226
+ const handleSaveAsVariable = useCallback(
227
+ (property: 'fill' | 'stroke', varName: string) => {
228
+ const { selectorPath, styleChanges, activeBreakpoint, changeScope } =
229
+ useEditorStore.getState()
230
+ if (!selectorPath) return
231
+
232
+ // Get the current color value (from existing change or computed styles)
233
+ const existingChange = styleChanges.find(
234
+ (c) => c.elementSelector === selectorPath && c.property === property,
235
+ )
236
+ const colorValue = existingChange
237
+ ? existingChange.newValue
238
+ : useEditorStore.getState().computedStyles[property] || ''
239
+
240
+ if (!colorValue) return
241
+
242
+ // 1. Add :root variable definition change
243
+ addStyleChange({
244
+ id: generateId(),
245
+ elementSelector: ':root',
246
+ property: varName,
247
+ originalValue: '',
248
+ newValue: colorValue,
249
+ breakpoint: activeBreakpoint,
250
+ timestamp: Date.now(),
251
+ changeScope,
252
+ })
253
+
254
+ // 2. Apply the variable on :root in the iframe
255
+ sendViaIframe({
256
+ type: 'PREVIEW_CHANGE',
257
+ payload: {
258
+ selectorPath: ':root',
259
+ property: varName,
260
+ value: colorValue,
261
+ },
262
+ })
263
+
264
+ // 3. Update the fill/stroke to use var() reference
265
+ applyChange(property, `var(${varName})`)
266
+ },
267
+ [applyChange, addStyleChange],
268
+ )
269
+
270
+ const handleRemoveVariable = useCallback(
271
+ (property: 'fill' | 'stroke') => {
272
+ const { selectorPath, styleChanges } = useEditorStore.getState()
273
+ if (!selectorPath) return
274
+
275
+ // Find the fill/stroke change with var() reference
276
+ const propChange = styleChanges.find(
277
+ (c) => c.elementSelector === selectorPath && c.property === property,
278
+ )
279
+ if (!propChange) return
280
+
281
+ const varName = extractVarName(propChange.newValue)
282
+ if (!varName) return
283
+
284
+ // Resolve the color value from the :root change
285
+ const resolved = resolveVarValue(propChange.newValue)
286
+
287
+ // Remove the :root variable definition
288
+ const rootChange = styleChanges.find(
289
+ (c) => c.elementSelector === ':root' && c.property === varName,
290
+ )
291
+ if (rootChange) removeStyleChange(rootChange.id)
292
+
293
+ // Revert :root inline style in iframe
294
+ sendViaIframe({
295
+ type: 'REVERT_CHANGE',
296
+ payload: { selectorPath: ':root', property: varName },
297
+ })
298
+
299
+ // Restore fill/stroke to the resolved color value
300
+ if (resolved) {
301
+ applyChange(property, resolved)
302
+ }
303
+ },
304
+ [applyChange, removeStyleChange],
305
+ )
306
+
307
+ // Resolve display values: if the value is var(), show the resolved color
308
+ const rawFill = computedStyles.fill || ''
309
+ const rawStroke = computedStyles.stroke || ''
310
+ const fillDisplay = extractVarName(rawFill)
311
+ ? resolveVarValue(rawFill) || rawFill
312
+ : rawFill
313
+ const strokeDisplay = extractVarName(rawStroke)
314
+ ? resolveVarValue(rawStroke) || rawStroke
315
+ : rawStroke
316
+
317
+ return (
318
+ <SectionHeader
319
+ title="SVG"
320
+ defaultOpen={true}
321
+ hasChanges={hasChanges}
322
+ onReset={handleResetAll}
323
+ >
324
+ <div className="space-y-2.5">
325
+ {/* Fill */}
326
+ <div className="space-y-1">
327
+ <ColorInput
328
+ label="Fill"
329
+ value={fillDisplay}
330
+ property="fill"
331
+ onChange={handleColorChange}
332
+ varExpression={cssVariableUsages['fill']}
333
+ />
334
+ <SaveAsVariableRow
335
+ property="fill"
336
+ existingVarName={fillVarName}
337
+ onSave={(name) => handleSaveAsVariable('fill', name)}
338
+ onRemove={() => handleRemoveVariable('fill')}
339
+ />
340
+ </div>
341
+
342
+ {/* Stroke */}
343
+ <div className="space-y-1">
344
+ <ColorInput
345
+ label="Stroke"
346
+ value={strokeDisplay}
347
+ property="stroke"
348
+ onChange={handleColorChange}
349
+ varExpression={cssVariableUsages['stroke']}
350
+ />
351
+ <SaveAsVariableRow
352
+ property="stroke"
353
+ existingVarName={strokeVarName}
354
+ onSave={(name) => handleSaveAsVariable('stroke', name)}
355
+ onRemove={() => handleRemoveVariable('stroke')}
356
+ />
357
+ </div>
358
+ </div>
359
+ </SectionHeader>
360
+ )
361
+ }
@@ -0,0 +1,227 @@
1
+ 'use client'
2
+
3
+ import { useMemo } from 'react'
4
+ import { useEditorStore } from '@/store'
5
+ import { SectionHeader } from '@/components/right-panel/design/inputs/SectionHeader'
6
+ import { CompactInput } from '@/components/right-panel/design/inputs/CompactInput'
7
+ import { ColorPicker } from '@/components/common/ColorPicker'
8
+ import {
9
+ PlusIcon,
10
+ TrashIcon,
11
+ InsetIcon,
12
+ } from '@/components/right-panel/design/icons'
13
+ import { parseShadow, serializeShadow } from '@/lib/shadowParser'
14
+ import { useChangeTracker } from '@/hooks/useChangeTracker'
15
+ import type { ShadowData } from '@/types/shadow'
16
+
17
+ const SHADOW_PROPERTIES = ['boxShadow', 'filter']
18
+
19
+ const DEFAULT_SHADOW: ShadowData = {
20
+ x: 0,
21
+ y: 0,
22
+ blur: 4,
23
+ spread: 0,
24
+ color: 'rgba(0,0,0,0.25)',
25
+ inset: false,
26
+ }
27
+
28
+ export function ShadowBlurSection() {
29
+ const computedStyles = useEditorStore((state) => state.computedStyles)
30
+ const { applyChange, resetProperty } = useChangeTracker()
31
+
32
+ const hasChanges = useEditorStore((s) => {
33
+ const sp = s.selectorPath
34
+ if (!sp) return false
35
+ return s.styleChanges.some(
36
+ (c) => c.elementSelector === sp && SHADOW_PROPERTIES.includes(c.property),
37
+ )
38
+ })
39
+
40
+ const handleResetAll = () => {
41
+ const { selectorPath, styleChanges } = useEditorStore.getState()
42
+ if (!selectorPath) return
43
+ const matching = styleChanges.filter(
44
+ (c) =>
45
+ c.elementSelector === selectorPath &&
46
+ SHADOW_PROPERTIES.includes(c.property),
47
+ )
48
+ for (const c of matching) resetProperty(c.property)
49
+ }
50
+
51
+ const boxShadow = computedStyles.boxShadow || 'none'
52
+ const filter = computedStyles.filter || 'none'
53
+
54
+ const shadows = useMemo(() => parseShadow(boxShadow), [boxShadow])
55
+
56
+ // Extract blur value from filter
57
+ const filterBlurMatch = filter.match(/blur\((\d+(?:\.\d+)?)px\)/)
58
+ const filterBlurValue = filterBlurMatch ? filterBlurMatch[1] : '0'
59
+
60
+ const updateShadows = (newShadows: ShadowData[]) => {
61
+ applyChange('boxShadow', serializeShadow(newShadows))
62
+ }
63
+
64
+ const updateShadow = (index: number, updates: Partial<ShadowData>) => {
65
+ const newShadows = shadows.map((s, i) =>
66
+ i === index ? { ...s, ...updates } : s,
67
+ )
68
+ updateShadows(newShadows)
69
+ }
70
+
71
+ const addShadow = () => {
72
+ updateShadows([...shadows, { ...DEFAULT_SHADOW }])
73
+ }
74
+
75
+ const removeShadow = (index: number) => {
76
+ updateShadows(shadows.filter((_, i) => i !== index))
77
+ }
78
+
79
+ const handleFilterBlurChange = (_property: string, value: string) => {
80
+ const num = parseFloat(value)
81
+ if (!isNaN(num) && num > 0) {
82
+ applyChange('filter', `blur(${num}px)`)
83
+ } else {
84
+ applyChange('filter', 'none')
85
+ }
86
+ }
87
+
88
+ return (
89
+ <SectionHeader
90
+ title="Shadow & Blur"
91
+ defaultOpen={false}
92
+ hasChanges={hasChanges}
93
+ onReset={handleResetAll}
94
+ actions={
95
+ <button
96
+ type="button"
97
+ onClick={addShadow}
98
+ className="flex items-center justify-center w-5 h-5 rounded hover:opacity-80"
99
+ style={{ color: 'var(--text-muted)' }}
100
+ title="Add shadow"
101
+ >
102
+ <PlusIcon />
103
+ </button>
104
+ }
105
+ >
106
+ {/* Shadow layers */}
107
+ {shadows.map((shadow, i) => (
108
+ <div
109
+ key={i}
110
+ className="space-y-1 pb-2"
111
+ style={{ borderBottom: '1px solid var(--border)' }}
112
+ >
113
+ <div className="flex items-center justify-between">
114
+ <span
115
+ className="text-[10px] font-medium"
116
+ style={{ color: 'var(--text-muted)' }}
117
+ >
118
+ Shadow {shadows.length > 1 ? i + 1 : ''}
119
+ </span>
120
+ <div className="flex items-center gap-0.5">
121
+ <button
122
+ type="button"
123
+ onClick={() => updateShadow(i, { inset: !shadow.inset })}
124
+ className="flex items-center justify-center w-5 h-5 rounded hover:opacity-80"
125
+ style={{
126
+ color: shadow.inset ? 'var(--accent)' : 'var(--text-muted)',
127
+ background: shadow.inset
128
+ ? 'var(--accent-bg, rgba(74,158,255,0.15))'
129
+ : 'transparent',
130
+ }}
131
+ title="Inset"
132
+ >
133
+ <InsetIcon />
134
+ </button>
135
+ <button
136
+ type="button"
137
+ onClick={() => removeShadow(i)}
138
+ className="flex items-center justify-center w-5 h-5 rounded hover:opacity-80"
139
+ style={{ color: 'var(--text-muted)' }}
140
+ title="Remove"
141
+ >
142
+ <TrashIcon />
143
+ </button>
144
+ </div>
145
+ </div>
146
+ <div className="grid grid-cols-2 gap-1">
147
+ <CompactInput
148
+ label="X"
149
+ value={`${shadow.x}px`}
150
+ property={`shadow-${i}-x`}
151
+ onChange={(_p, v) => {
152
+ const num = parseFloat(v)
153
+ if (!isNaN(num)) updateShadow(i, { x: num })
154
+ }}
155
+ units={['px']}
156
+ />
157
+ <CompactInput
158
+ label="Y"
159
+ value={`${shadow.y}px`}
160
+ property={`shadow-${i}-y`}
161
+ onChange={(_p, v) => {
162
+ const num = parseFloat(v)
163
+ if (!isNaN(num)) updateShadow(i, { y: num })
164
+ }}
165
+ units={['px']}
166
+ />
167
+ <CompactInput
168
+ label="B"
169
+ value={`${shadow.blur}px`}
170
+ property={`shadow-${i}-blur`}
171
+ onChange={(_p, v) => {
172
+ const num = parseFloat(v)
173
+ if (!isNaN(num)) updateShadow(i, { blur: Math.max(0, num) })
174
+ }}
175
+ units={['px']}
176
+ min={0}
177
+ />
178
+ <CompactInput
179
+ label="S"
180
+ value={`${shadow.spread}px`}
181
+ property={`shadow-${i}-spread`}
182
+ onChange={(_p, v) => {
183
+ const num = parseFloat(v)
184
+ if (!isNaN(num)) updateShadow(i, { spread: num })
185
+ }}
186
+ units={['px']}
187
+ />
188
+ </div>
189
+ <ColorPicker
190
+ label="Color"
191
+ value={shadow.color}
192
+ onChange={(c) => updateShadow(i, { color: c })}
193
+ onSelectVariable={(varExpr) => updateShadow(i, { color: varExpr })}
194
+ />
195
+ </div>
196
+ ))}
197
+
198
+ {shadows.length === 0 && (
199
+ <div
200
+ className="text-[11px] py-1"
201
+ style={{ color: 'var(--text-muted)' }}
202
+ >
203
+ No shadows. Click + to add one.
204
+ </div>
205
+ )}
206
+
207
+ {/* Filter blur */}
208
+ <div className="pt-1">
209
+ <div
210
+ className="text-[10px] font-medium mb-1"
211
+ style={{ color: 'var(--text-muted)' }}
212
+ >
213
+ Filter Blur
214
+ </div>
215
+ <CompactInput
216
+ label="B"
217
+ value={`${filterBlurValue}px`}
218
+ property="filter"
219
+ onChange={handleFilterBlurChange}
220
+ units={['px']}
221
+ min={0}
222
+ className="w-1/2"
223
+ />
224
+ </div>
225
+ </SectionHeader>
226
+ )
227
+ }