@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.
- package/LICENSE +178 -0
- package/NOTICE +4 -0
- package/README.md +180 -0
- package/bin/paint.js +266 -0
- package/next-env.d.ts +6 -0
- package/next.config.ts +19 -0
- package/package.json +81 -0
- package/postcss.config.mjs +8 -0
- package/public/dev-editor-inspector.js +1872 -0
- package/src/app/api/claude/analyze/route.ts +319 -0
- package/src/app/api/claude/apply/route.ts +185 -0
- package/src/app/api/claude/pick-folder/route.ts +64 -0
- package/src/app/api/claude/scan/route.ts +221 -0
- package/src/app/api/claude/status/route.ts +55 -0
- package/src/app/api/project/scan/route.ts +634 -0
- package/src/app/api/project-scan/css-variables/route.ts +238 -0
- package/src/app/api/project-scan/route.ts +40 -0
- package/src/app/api/project-scan/tailwind-config/route.ts +172 -0
- package/src/app/api/proxy/[[...path]]/route.ts +2400 -0
- package/src/app/docs/DocsClient.tsx +322 -0
- package/src/app/docs/layout.tsx +7 -0
- package/src/app/docs/page.tsx +855 -0
- package/src/app/globals.css +176 -0
- package/src/app/layout.tsx +19 -0
- package/src/app/page.tsx +46 -0
- package/src/bridge/api-handlers.ts +885 -0
- package/src/bridge/proxy-handler.ts +329 -0
- package/src/bridge/server.ts +113 -0
- package/src/components/BreakpointTabs.tsx +72 -0
- package/src/components/ChangeSummaryModal.tsx +267 -0
- package/src/components/ConnectModal.tsx +994 -0
- package/src/components/Editor.tsx +90 -0
- package/src/components/PageSelector.tsx +208 -0
- package/src/components/PreviewFrame.tsx +299 -0
- package/src/components/ProjectFolderBanner.tsx +91 -0
- package/src/components/ResponsiveToolbar.tsx +222 -0
- package/src/components/TargetSelector.tsx +243 -0
- package/src/components/TopBar.tsx +315 -0
- package/src/components/common/CollapsibleSection.tsx +36 -0
- package/src/components/common/ColorPicker.tsx +920 -0
- package/src/components/common/EditablePre.tsx +136 -0
- package/src/components/common/ErrorBoundary.tsx +65 -0
- package/src/components/common/ResizablePanel.tsx +83 -0
- package/src/components/common/ScanAnimation.tsx +76 -0
- package/src/components/common/ToastContainer.tsx +97 -0
- package/src/components/common/UnitInput.tsx +77 -0
- package/src/components/common/VariableColorPicker.tsx +622 -0
- package/src/components/left-panel/AddElementPanel.tsx +237 -0
- package/src/components/left-panel/ComponentsPanel.tsx +609 -0
- package/src/components/left-panel/IconSidebar.tsx +99 -0
- package/src/components/left-panel/LayerNode.tsx +874 -0
- package/src/components/left-panel/LayerSearch.tsx +23 -0
- package/src/components/left-panel/LayersPanel.tsx +52 -0
- package/src/components/left-panel/LeftPanel.tsx +122 -0
- package/src/components/left-panel/PagesPanel.tsx +114 -0
- package/src/components/left-panel/icons.tsx +162 -0
- package/src/components/left-panel/terminal/ScanOverlay.tsx +66 -0
- package/src/components/left-panel/terminal/TerminalPanel.tsx +217 -0
- package/src/components/right-panel/ElementLogBox.tsx +248 -0
- package/src/components/right-panel/PanelTabs.tsx +83 -0
- package/src/components/right-panel/RightPanel.tsx +41 -0
- package/src/components/right-panel/changes/AiScanResultPanel.tsx +285 -0
- package/src/components/right-panel/changes/ChangeEntry.tsx +59 -0
- package/src/components/right-panel/changes/ChangelogActions.tsx +105 -0
- package/src/components/right-panel/changes/ChangesPanel.tsx +1474 -0
- package/src/components/right-panel/claude/ApplyConfirmModal.tsx +376 -0
- package/src/components/right-panel/claude/ClaudeErrorState.tsx +125 -0
- package/src/components/right-panel/claude/ClaudeIntegrationPanel.tsx +482 -0
- package/src/components/right-panel/claude/ClaudeProgressIndicator.tsx +76 -0
- package/src/components/right-panel/claude/DiffCard.tsx +130 -0
- package/src/components/right-panel/claude/DiffViewer.tsx +54 -0
- package/src/components/right-panel/claude/ProjectRootSelector.tsx +275 -0
- package/src/components/right-panel/claude/ResultsSummary.tsx +119 -0
- package/src/components/right-panel/claude/SetupFlow.tsx +315 -0
- package/src/components/right-panel/console/ConsolePanel.tsx +209 -0
- package/src/components/right-panel/design/AppearanceSection.tsx +703 -0
- package/src/components/right-panel/design/BackgroundSection.tsx +516 -0
- package/src/components/right-panel/design/BorderSection.tsx +161 -0
- package/src/components/right-panel/design/CSSRawView.tsx +412 -0
- package/src/components/right-panel/design/DesignCSSTabToggle.tsx +51 -0
- package/src/components/right-panel/design/DesignPanel.tsx +275 -0
- package/src/components/right-panel/design/ElementBreadcrumb.tsx +51 -0
- package/src/components/right-panel/design/GradientEditor.tsx +726 -0
- package/src/components/right-panel/design/LayoutSection.tsx +1948 -0
- package/src/components/right-panel/design/PositionSection.tsx +865 -0
- package/src/components/right-panel/design/PropertiesSection.tsx +86 -0
- package/src/components/right-panel/design/SVGSection.tsx +361 -0
- package/src/components/right-panel/design/ShadowBlurSection.tsx +227 -0
- package/src/components/right-panel/design/SizeSection.tsx +183 -0
- package/src/components/right-panel/design/TextSection.tsx +719 -0
- package/src/components/right-panel/design/icons.tsx +948 -0
- package/src/components/right-panel/design/inputs/BoxModelPreview.tsx +467 -0
- package/src/components/right-panel/design/inputs/ColorInput.tsx +43 -0
- package/src/components/right-panel/design/inputs/CompactInput.tsx +333 -0
- package/src/components/right-panel/design/inputs/DraggableLabel.tsx +118 -0
- package/src/components/right-panel/design/inputs/IconToggleGroup.tsx +54 -0
- package/src/components/right-panel/design/inputs/LinkedInputPair.tsx +174 -0
- package/src/components/right-panel/design/inputs/SectionHeader.tsx +79 -0
- package/src/components/right-panel/variables/VariablesPanel.tsx +388 -0
- package/src/hooks/useBridge.ts +95 -0
- package/src/hooks/useChangeTracker.ts +563 -0
- package/src/hooks/useClaudeAPI.ts +118 -0
- package/src/hooks/useDOMTree.ts +25 -0
- package/src/hooks/useKeyboardShortcuts.ts +76 -0
- package/src/hooks/usePostMessage.ts +589 -0
- package/src/hooks/useProjectScan.ts +204 -0
- package/src/hooks/useResizable.ts +20 -0
- package/src/hooks/useSelectedElement.ts +51 -0
- package/src/hooks/useTargetUrl.ts +81 -0
- package/src/inspector/DOMTraverser.ts +71 -0
- package/src/inspector/ElementSelector.ts +23 -0
- package/src/inspector/HoverHighlighter.ts +54 -0
- package/src/inspector/SelectionHighlighter.ts +27 -0
- package/src/inspector/StyleExtractor.ts +19 -0
- package/src/inspector/inspector.ts +17 -0
- package/src/inspector/messaging.ts +30 -0
- package/src/lib/apiBase.ts +15 -0
- package/src/lib/classifyElement.ts +430 -0
- package/src/lib/claude-bin.ts +197 -0
- package/src/lib/claude-stream.ts +158 -0
- package/src/lib/clientProjectScanner.ts +344 -0
- package/src/lib/componentMatcher.ts +156 -0
- package/src/lib/constants.ts +573 -0
- package/src/lib/cssVariableUtils.ts +409 -0
- package/src/lib/diffParser.ts +206 -0
- package/src/lib/folderPicker.ts +84 -0
- package/src/lib/gradientParser.ts +160 -0
- package/src/lib/projectScanner.ts +355 -0
- package/src/lib/promptBuilder.ts +402 -0
- package/src/lib/shadowParser.ts +124 -0
- package/src/lib/tailwindClassParser.ts +248 -0
- package/src/lib/textShadowUtils.ts +106 -0
- package/src/lib/utils.ts +299 -0
- package/src/lib/validatePath.ts +40 -0
- package/src/proxy.ts +92 -0
- package/src/server/terminal-server.ts +104 -0
- package/src/store/changeSlice.ts +288 -0
- package/src/store/claudeSlice.ts +222 -0
- package/src/store/componentSlice.ts +90 -0
- package/src/store/consoleSlice.ts +51 -0
- package/src/store/cssVariableSlice.ts +94 -0
- package/src/store/elementSlice.ts +78 -0
- package/src/store/index.ts +35 -0
- package/src/store/terminalSlice.ts +30 -0
- package/src/store/treeSlice.ts +69 -0
- package/src/store/uiSlice.ts +327 -0
- package/src/types/changelog.ts +49 -0
- package/src/types/claude.ts +131 -0
- package/src/types/component.ts +49 -0
- package/src/types/cssVariables.ts +18 -0
- package/src/types/element.ts +21 -0
- package/src/types/file-system-access.d.ts +27 -0
- package/src/types/gradient.ts +12 -0
- package/src/types/messages.ts +392 -0
- package/src/types/shadow.ts +8 -0
- package/src/types/tree.ts +9 -0
- package/tsconfig.json +42 -0
- 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
|
+
"{value}"
|
|
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
|
+
}
|