@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,719 @@
1
+ 'use client'
2
+
3
+ import { useState, 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 { DraggableLabel } from '@/components/right-panel/design/inputs/DraggableLabel'
8
+ import { IconToggleGroup } from '@/components/right-panel/design/inputs/IconToggleGroup'
9
+ import { ColorInput } from '@/components/right-panel/design/inputs/ColorInput'
10
+ import { ColorPicker } from '@/components/common/ColorPicker'
11
+ import {
12
+ AlignLeftIcon,
13
+ AlignCenterIcon,
14
+ AlignRightIcon,
15
+ AlignJustifyIcon,
16
+ DecoNoneIcon,
17
+ StrikethroughIcon,
18
+ OverlineIcon,
19
+ UnderlineIcon,
20
+ ItalicIcon,
21
+ DirectionLTRIcon,
22
+ DirectionRTLIcon,
23
+ PlusIcon,
24
+ TrashIcon,
25
+ } from '@/components/right-panel/design/icons'
26
+ import { useChangeTracker } from '@/hooks/useChangeTracker'
27
+ import { parseTextShadow, serializeTextShadow } from '@/lib/textShadowUtils'
28
+ import type { TextShadowData } from '@/lib/textShadowUtils'
29
+
30
+ // --- Option definitions ---
31
+
32
+ const TEXT_ALIGN_OPTIONS = [
33
+ { value: 'left', icon: <AlignLeftIcon />, tooltip: 'Left' },
34
+ { value: 'center', icon: <AlignCenterIcon />, tooltip: 'Center' },
35
+ { value: 'right', icon: <AlignRightIcon />, tooltip: 'Right' },
36
+ { value: 'justify', icon: <AlignJustifyIcon />, tooltip: 'Justify' },
37
+ ]
38
+
39
+ const TEXT_DECORATION_OPTIONS = [
40
+ { value: 'none', icon: <DecoNoneIcon />, tooltip: 'None' },
41
+ {
42
+ value: 'line-through',
43
+ icon: <StrikethroughIcon />,
44
+ tooltip: 'Strikethrough',
45
+ },
46
+ { value: 'overline', icon: <OverlineIcon />, tooltip: 'Overline' },
47
+ { value: 'underline', icon: <UnderlineIcon />, tooltip: 'Underline' },
48
+ ]
49
+
50
+ const FONT_STYLE_OPTIONS = [
51
+ {
52
+ value: 'normal',
53
+ icon: <span className="text-[11px] font-medium">N</span>,
54
+ tooltip: 'Normal',
55
+ },
56
+ { value: 'italic', icon: <ItalicIcon />, tooltip: 'Italic' },
57
+ ]
58
+
59
+ const TEXT_TRANSFORM_OPTIONS = [
60
+ {
61
+ value: 'uppercase',
62
+ icon: <span className="text-[10px] font-bold">AA</span>,
63
+ tooltip: 'Uppercase',
64
+ },
65
+ {
66
+ value: 'capitalize',
67
+ icon: <span className="text-[10px] font-bold">Aa</span>,
68
+ tooltip: 'Capitalize',
69
+ },
70
+ {
71
+ value: 'lowercase',
72
+ icon: <span className="text-[10px] font-bold">aa</span>,
73
+ tooltip: 'Lowercase',
74
+ },
75
+ { value: 'none', icon: <DecoNoneIcon />, tooltip: 'None' },
76
+ ]
77
+
78
+ const DIRECTION_OPTIONS = [
79
+ { value: 'ltr', icon: <DirectionLTRIcon />, tooltip: 'Left to Right' },
80
+ { value: 'rtl', icon: <DirectionRTLIcon />, tooltip: 'Right to Left' },
81
+ ]
82
+
83
+ const WEIGHT_OPTIONS = [
84
+ { value: '100', label: '100 - Thin' },
85
+ { value: '200', label: '200 - Extra Light' },
86
+ { value: '300', label: '300 - Light' },
87
+ { value: '400', label: '400 - Normal' },
88
+ { value: '500', label: '500 - Medium' },
89
+ { value: '600', label: '600 - Semi Bold' },
90
+ { value: '700', label: '700 - Bold' },
91
+ { value: '800', label: '800 - Extra Bold' },
92
+ { value: '900', label: '900 - Black' },
93
+ ]
94
+
95
+ // --- Shared styles ---
96
+
97
+ const selectStyle = {
98
+ background: 'var(--bg-tertiary)',
99
+ border: '1px solid var(--border)',
100
+ color: 'var(--text-primary)',
101
+ } as const
102
+
103
+ const labelStyle = {
104
+ color: 'var(--text-muted)',
105
+ } as const
106
+
107
+ // --- Helper: parse font-family stack into individual font names ---
108
+
109
+ function parseFontStack(value: string): string[] {
110
+ if (!value || value === 'inherit') return []
111
+ // Split by commas, respecting quoted font names
112
+ const fonts: string[] = []
113
+ let current = ''
114
+ let inQuote = false
115
+ let quoteChar = ''
116
+ for (let i = 0; i < value.length; i++) {
117
+ const ch = value[i]
118
+ if (!inQuote && (ch === '"' || ch === "'")) {
119
+ inQuote = true
120
+ quoteChar = ch
121
+ } else if (inQuote && ch === quoteChar) {
122
+ inQuote = false
123
+ } else if (ch === ',' && !inQuote) {
124
+ const trimmed = current.trim().replace(/^["']|["']$/g, '')
125
+ if (trimmed) fonts.push(trimmed)
126
+ current = ''
127
+ continue
128
+ }
129
+ current += ch
130
+ }
131
+ const last = current.trim().replace(/^["']|["']$/g, '')
132
+ if (last) fonts.push(last)
133
+ return [...new Set(fonts)]
134
+ }
135
+
136
+ // --- Helper: extract first keyword from compound text-decoration ---
137
+
138
+ function extractDecorationKeyword(value: string): string {
139
+ if (!value || value === 'none') return 'none'
140
+ const first = value.split(/\s+/)[0]
141
+ if (['underline', 'overline', 'line-through', 'none'].includes(first))
142
+ return first
143
+ return 'none'
144
+ }
145
+
146
+ // --- Component ---
147
+
148
+ const TYPOGRAPHY_PROPERTIES = [
149
+ 'fontFamily',
150
+ 'fontWeight',
151
+ 'fontSize',
152
+ 'lineHeight',
153
+ 'color',
154
+ 'textAlign',
155
+ 'textDecoration',
156
+ 'letterSpacing',
157
+ 'textIndent',
158
+ 'columnCount',
159
+ 'fontStyle',
160
+ 'textTransform',
161
+ 'direction',
162
+ 'wordBreak',
163
+ 'lineBreak',
164
+ 'whiteSpace',
165
+ 'textOverflow',
166
+ 'webkitTextStrokeWidth',
167
+ 'webkitTextStrokeColor',
168
+ 'textShadow',
169
+ ]
170
+
171
+ export function TextSection() {
172
+ const computedStyles = useEditorStore((state) => state.computedStyles)
173
+ const cssVariableUsages = useEditorStore((state) => state.cssVariableUsages)
174
+ const { applyChange, resetProperty } = useChangeTracker()
175
+
176
+ const hasChanges = useEditorStore((s) => {
177
+ const sp = s.selectorPath
178
+ if (!sp) return false
179
+ return s.styleChanges.some(
180
+ (c) =>
181
+ c.elementSelector === sp && TYPOGRAPHY_PROPERTIES.includes(c.property),
182
+ )
183
+ })
184
+
185
+ const handleResetAll = () => {
186
+ const { selectorPath, styleChanges } = useEditorStore.getState()
187
+ if (!selectorPath) return
188
+ const matching = styleChanges.filter(
189
+ (c) =>
190
+ c.elementSelector === selectorPath &&
191
+ TYPOGRAPHY_PROPERTIES.includes(c.property),
192
+ )
193
+ for (const c of matching) resetProperty(c.property)
194
+ }
195
+
196
+ const handleChange = (property: string, value: string) => {
197
+ applyChange(property, value)
198
+ }
199
+
200
+ // --- Core typography values ---
201
+ const fontFamily = computedStyles.fontFamily || 'inherit'
202
+ const fontWeight = computedStyles.fontWeight || '400'
203
+ const fontSize = computedStyles.fontSize || '16px'
204
+ const lineHeight = computedStyles.lineHeight || 'normal'
205
+ const color = computedStyles.color || '#000000'
206
+ const textAlign = computedStyles.textAlign || 'left'
207
+ const textDecoration = computedStyles.textDecoration || 'none'
208
+
209
+ // --- Advanced values ---
210
+ const letterSpacing = computedStyles.letterSpacing || 'normal'
211
+ const textIndent = computedStyles.textIndent || '0px'
212
+ const columnCount = computedStyles.columnCount || 'auto'
213
+ const fontStyle = computedStyles.fontStyle || 'normal'
214
+ const textTransform = computedStyles.textTransform || 'none'
215
+ const direction = computedStyles.direction || 'ltr'
216
+ const wordBreak = computedStyles.wordBreak || 'normal'
217
+ const lineBreak = computedStyles.lineBreak || 'normal'
218
+ const whiteSpace = computedStyles.whiteSpace || 'normal'
219
+ const textOverflow = computedStyles.textOverflow || 'clip'
220
+ const webkitTextStrokeWidth = computedStyles.webkitTextStrokeWidth || '0px'
221
+ const webkitTextStrokeColor =
222
+ computedStyles.webkitTextStrokeColor || 'currentcolor'
223
+
224
+ // --- Text shadow ---
225
+ const textShadowRaw = computedStyles.textShadow || 'none'
226
+ const shadows = useMemo(() => parseTextShadow(textShadowRaw), [textShadowRaw])
227
+
228
+ const addShadow = () => {
229
+ const next: TextShadowData[] = [
230
+ ...shadows,
231
+ { x: 0, y: 0, blur: 0, color: 'rgba(0,0,0,0.25)' },
232
+ ]
233
+ applyChange('textShadow', serializeTextShadow(next))
234
+ }
235
+
236
+ const removeShadow = (index: number) => {
237
+ const next = shadows.filter((_, i) => i !== index)
238
+ applyChange('textShadow', serializeTextShadow(next))
239
+ }
240
+
241
+ const updateShadow = (index: number, updates: Partial<TextShadowData>) => {
242
+ const next = shadows.map((s, i) => (i === index ? { ...s, ...updates } : s))
243
+ applyChange('textShadow', serializeTextShadow(next))
244
+ }
245
+
246
+ // --- More type options toggle ---
247
+ const [moreOpen, setMoreOpen] = useState(false)
248
+
249
+ const decoValue = extractDecorationKeyword(textDecoration)
250
+
251
+ // Parse font stack into individual font options
252
+ const fontOptions = useMemo(() => parseFontStack(fontFamily), [fontFamily])
253
+
254
+ // Row label width
255
+ const LW = 'w-[52px]'
256
+
257
+ return (
258
+ <SectionHeader
259
+ title="Typography"
260
+ defaultOpen={true}
261
+ hasChanges={hasChanges}
262
+ onReset={handleResetAll}
263
+ >
264
+ {/* ===== Core Typography — design tool row layout ===== */}
265
+
266
+ {/* Font */}
267
+ <div className="flex items-center gap-2">
268
+ <span
269
+ className={`${LW} flex-shrink-0 text-[11px]`}
270
+ style={{ color: 'var(--accent)' }}
271
+ >
272
+ Font
273
+ </span>
274
+ <select
275
+ value={fontFamily}
276
+ onChange={(e) => handleChange('fontFamily', e.target.value)}
277
+ className="flex-1 h-7 rounded text-[11px] px-2 cursor-pointer outline-none"
278
+ style={selectStyle}
279
+ >
280
+ <option value={fontFamily}>{fontOptions[0] || fontFamily}</option>
281
+ {fontOptions.map((font) => (
282
+ <option key={font} value={font}>
283
+ {font}
284
+ </option>
285
+ ))}
286
+ </select>
287
+ </div>
288
+
289
+ {/* Weight */}
290
+ <div className="flex items-center gap-2">
291
+ <span
292
+ className={`${LW} flex-shrink-0 text-[11px]`}
293
+ style={{ color: 'var(--accent)' }}
294
+ >
295
+ Weight
296
+ </span>
297
+ <select
298
+ value={fontWeight}
299
+ onChange={(e) => handleChange('fontWeight', e.target.value)}
300
+ className="flex-1 h-7 rounded text-[11px] px-2 cursor-pointer outline-none"
301
+ style={selectStyle}
302
+ >
303
+ {WEIGHT_OPTIONS.map((opt) => (
304
+ <option key={opt.value} value={opt.value}>
305
+ {opt.label}
306
+ </option>
307
+ ))}
308
+ </select>
309
+ </div>
310
+
311
+ {/* Size + Height + Spacing */}
312
+ <div className="grid grid-cols-[52px_1fr_auto_1fr] items-center gap-x-2 gap-y-2">
313
+ <DraggableLabel
314
+ value={fontSize}
315
+ property="fontSize"
316
+ onChange={handleChange}
317
+ min={0}
318
+ className="text-[11px] whitespace-nowrap"
319
+ style={{ color: 'var(--accent)' }}
320
+ >
321
+ Size
322
+ </DraggableLabel>
323
+ <CompactInput
324
+ value={fontSize}
325
+ property="fontSize"
326
+ onChange={handleChange}
327
+ units={['px', 'em', 'rem', '%', 'vw']}
328
+ min={0}
329
+ className="min-w-0"
330
+ />
331
+ <DraggableLabel
332
+ value={lineHeight}
333
+ property="lineHeight"
334
+ onChange={handleChange}
335
+ className="text-[11px] whitespace-nowrap"
336
+ style={{ color: 'var(--text-muted)' }}
337
+ >
338
+ Height
339
+ </DraggableLabel>
340
+ <CompactInput
341
+ value={lineHeight}
342
+ property="lineHeight"
343
+ onChange={handleChange}
344
+ units={['px', 'em', 'rem', '']}
345
+ className="min-w-0"
346
+ />
347
+ <DraggableLabel
348
+ value={letterSpacing}
349
+ property="letterSpacing"
350
+ onChange={handleChange}
351
+ className="text-[11px] whitespace-nowrap"
352
+ style={{ color: 'var(--text-muted)' }}
353
+ >
354
+ Spacing
355
+ </DraggableLabel>
356
+ <CompactInput
357
+ value={letterSpacing}
358
+ property="letterSpacing"
359
+ onChange={handleChange}
360
+ units={['px', 'em', 'rem']}
361
+ className="min-w-0"
362
+ />
363
+ <DraggableLabel
364
+ value={textIndent}
365
+ property="textIndent"
366
+ onChange={handleChange}
367
+ className="text-[11px] whitespace-nowrap"
368
+ style={{ color: 'var(--text-muted)' }}
369
+ >
370
+ Indent
371
+ </DraggableLabel>
372
+ <CompactInput
373
+ value={textIndent}
374
+ property="textIndent"
375
+ onChange={handleChange}
376
+ units={['px', 'em', 'rem', '%']}
377
+ className="min-w-0"
378
+ />
379
+ </div>
380
+
381
+ {/* Color */}
382
+ <div className="flex items-center gap-2">
383
+ <span
384
+ className={`${LW} flex-shrink-0 text-[11px]`}
385
+ style={{ color: 'var(--accent)' }}
386
+ >
387
+ Color
388
+ </span>
389
+ <div className="flex-1">
390
+ <ColorInput
391
+ value={color}
392
+ property="color"
393
+ onChange={handleChange}
394
+ varExpression={cssVariableUsages['color']}
395
+ />
396
+ </div>
397
+ </div>
398
+
399
+ {/* Align */}
400
+ <div className="flex items-center gap-2">
401
+ <span
402
+ className={`${LW} flex-shrink-0 text-[11px]`}
403
+ style={{ color: 'var(--text-secondary)' }}
404
+ >
405
+ Align
406
+ </span>
407
+ <IconToggleGroup
408
+ options={TEXT_ALIGN_OPTIONS}
409
+ value={textAlign}
410
+ onChange={(val) => handleChange('textAlign', val)}
411
+ />
412
+ </div>
413
+
414
+ {/* Decoration */}
415
+ <div className="flex items-center gap-2">
416
+ <span
417
+ className={`${LW} flex-shrink-0 text-[11px]`}
418
+ style={{ color: 'var(--text-secondary)' }}
419
+ >
420
+ Decor
421
+ </span>
422
+ <IconToggleGroup
423
+ options={TEXT_DECORATION_OPTIONS}
424
+ value={decoValue}
425
+ onChange={(val) => handleChange('textDecoration', val)}
426
+ />
427
+ <button
428
+ type="button"
429
+ onClick={() => setMoreOpen(!moreOpen)}
430
+ className="flex items-center justify-center w-6 h-6 rounded transition-colors"
431
+ style={{
432
+ color: moreOpen ? 'var(--accent)' : 'var(--text-muted)',
433
+ background: moreOpen ? 'rgba(74,158,255,0.10)' : 'transparent',
434
+ }}
435
+ title="More type options"
436
+ >
437
+ <svg width={14} height={14} viewBox="0 0 14 14" fill="none">
438
+ <circle cx={3} cy={7} r={1.2} fill="currentColor" />
439
+ <circle cx={7} cy={7} r={1.2} fill="currentColor" />
440
+ <circle cx={11} cy={7} r={1.2} fill="currentColor" />
441
+ </svg>
442
+ </button>
443
+ </div>
444
+
445
+ {/* ===== More Type Options (collapsible) ===== */}
446
+
447
+ {moreOpen && (
448
+ <div
449
+ className="space-y-2 pt-1"
450
+ style={{ borderTop: '1px solid var(--border)' }}
451
+ >
452
+ {/* Columns */}
453
+ <div className="flex items-center gap-2">
454
+ <span
455
+ className={`${LW} flex-shrink-0 text-[11px]`}
456
+ style={{ color: 'var(--text-secondary)' }}
457
+ >
458
+ Columns
459
+ </span>
460
+ <div className="flex-1">
461
+ <CompactInput
462
+ value={columnCount}
463
+ property="columnCount"
464
+ onChange={handleChange}
465
+ units={['', 'auto']}
466
+ />
467
+ </div>
468
+ </div>
469
+
470
+ {/* Style / Transform / Direction */}
471
+ <div className="flex items-center gap-2">
472
+ <span
473
+ className={`${LW} flex-shrink-0 text-[11px]`}
474
+ style={{ color: 'var(--text-secondary)' }}
475
+ >
476
+ Style
477
+ </span>
478
+ <IconToggleGroup
479
+ options={FONT_STYLE_OPTIONS}
480
+ value={fontStyle}
481
+ onChange={(val) => handleChange('fontStyle', val)}
482
+ />
483
+ <IconToggleGroup
484
+ options={TEXT_TRANSFORM_OPTIONS}
485
+ value={textTransform}
486
+ onChange={(val) => handleChange('textTransform', val)}
487
+ />
488
+ <IconToggleGroup
489
+ options={DIRECTION_OPTIONS}
490
+ value={direction}
491
+ onChange={(val) => handleChange('direction', val)}
492
+ />
493
+ </div>
494
+
495
+ {/* Breaking */}
496
+ <div className="flex items-center gap-2">
497
+ <span
498
+ className={`${LW} flex-shrink-0 text-[11px]`}
499
+ style={{ color: 'var(--text-secondary)' }}
500
+ >
501
+ Break
502
+ </span>
503
+ <div className="flex-1">
504
+ <select
505
+ value={wordBreak}
506
+ onChange={(e) => handleChange('wordBreak', e.target.value)}
507
+ className="w-full h-7 rounded text-[11px] px-1.5 cursor-pointer outline-none"
508
+ style={selectStyle}
509
+ >
510
+ <option value="normal">normal</option>
511
+ <option value="break-all">break-all</option>
512
+ <option value="keep-all">keep-all</option>
513
+ <option value="break-word">break-word</option>
514
+ </select>
515
+ </div>
516
+ <div className="flex-1">
517
+ <select
518
+ value={lineBreak}
519
+ onChange={(e) => handleChange('lineBreak', e.target.value)}
520
+ className="w-full h-7 rounded text-[11px] px-1.5 cursor-pointer outline-none"
521
+ style={selectStyle}
522
+ >
523
+ <option value="normal">normal</option>
524
+ <option value="loose">loose</option>
525
+ <option value="strict">strict</option>
526
+ <option value="anywhere">anywhere</option>
527
+ </select>
528
+ </div>
529
+ </div>
530
+
531
+ {/* Wrap */}
532
+ <div className="flex items-center gap-2">
533
+ <span
534
+ className={`${LW} flex-shrink-0 text-[11px]`}
535
+ style={{ color: 'var(--text-secondary)' }}
536
+ >
537
+ Wrap
538
+ </span>
539
+ <div className="flex-1">
540
+ <select
541
+ value={whiteSpace}
542
+ onChange={(e) => handleChange('whiteSpace', e.target.value)}
543
+ className="w-full h-7 rounded text-[11px] px-1.5 cursor-pointer outline-none"
544
+ style={selectStyle}
545
+ >
546
+ <option value="normal">normal</option>
547
+ <option value="nowrap">nowrap</option>
548
+ <option value="pre">pre</option>
549
+ <option value="pre-wrap">pre-wrap</option>
550
+ <option value="pre-line">pre-line</option>
551
+ <option value="break-spaces">break-spaces</option>
552
+ </select>
553
+ </div>
554
+ </div>
555
+
556
+ {/* Truncate */}
557
+ <div className="flex items-center gap-2">
558
+ <span
559
+ className={`${LW} flex-shrink-0 text-[11px]`}
560
+ style={{ color: 'var(--text-secondary)' }}
561
+ >
562
+ Truncate
563
+ </span>
564
+ <div
565
+ className="inline-flex rounded"
566
+ style={{
567
+ background: 'var(--bg-tertiary)',
568
+ border: '1px solid var(--border)',
569
+ }}
570
+ >
571
+ {(['clip', 'ellipsis'] as const).map((opt) => {
572
+ const isActive = textOverflow === opt
573
+ return (
574
+ <button
575
+ key={opt}
576
+ type="button"
577
+ onClick={() => handleChange('textOverflow', opt)}
578
+ className="flex items-center justify-center px-3 text-[11px] transition-colors"
579
+ style={{
580
+ height: 24,
581
+ background: isActive
582
+ ? 'var(--accent-bg, rgba(74,158,255,0.15))'
583
+ : 'transparent',
584
+ color: isActive ? 'var(--accent)' : 'var(--text-muted)',
585
+ }}
586
+ >
587
+ {opt.charAt(0).toUpperCase() + opt.slice(1)}
588
+ </button>
589
+ )
590
+ })}
591
+ </div>
592
+ </div>
593
+
594
+ {/* Stroke */}
595
+ <div className="flex items-center gap-2">
596
+ <span
597
+ className={`${LW} flex-shrink-0 text-[11px]`}
598
+ style={{ color: 'var(--text-secondary)' }}
599
+ >
600
+ Stroke
601
+ </span>
602
+ <div className="flex-1">
603
+ <CompactInput
604
+ label="W"
605
+ value={webkitTextStrokeWidth}
606
+ property="webkitTextStrokeWidth"
607
+ onChange={handleChange}
608
+ units={['px']}
609
+ min={0}
610
+ />
611
+ </div>
612
+ <div className="flex-1">
613
+ <ColorInput
614
+ value={webkitTextStrokeColor}
615
+ property="webkitTextStrokeColor"
616
+ onChange={handleChange}
617
+ varExpression={cssVariableUsages['webkit-text-stroke-color']}
618
+ />
619
+ </div>
620
+ </div>
621
+ </div>
622
+ )}
623
+
624
+ {/* ===== US4: Text Shadows ===== */}
625
+
626
+ <SectionHeader
627
+ title="Text shadows"
628
+ defaultOpen={false}
629
+ actions={
630
+ <button
631
+ type="button"
632
+ onClick={addShadow}
633
+ className="flex items-center justify-center w-5 h-5 rounded hover:opacity-80"
634
+ style={{ color: 'var(--text-muted)' }}
635
+ title="Add text shadow"
636
+ >
637
+ <PlusIcon />
638
+ </button>
639
+ }
640
+ >
641
+ {shadows.map((shadow, i) => (
642
+ <div
643
+ key={i}
644
+ className="space-y-1 pb-2"
645
+ style={{ borderBottom: '1px solid var(--border)' }}
646
+ >
647
+ <div className="flex items-center justify-between">
648
+ <span
649
+ className="text-[10px] font-medium"
650
+ style={{ color: 'var(--text-muted)' }}
651
+ >
652
+ Shadow {shadows.length > 1 ? i + 1 : ''}
653
+ </span>
654
+ <button
655
+ type="button"
656
+ onClick={() => removeShadow(i)}
657
+ className="flex items-center justify-center w-5 h-5 rounded hover:opacity-80"
658
+ style={{ color: 'var(--text-muted)' }}
659
+ title="Remove"
660
+ >
661
+ <TrashIcon />
662
+ </button>
663
+ </div>
664
+ <div className="grid grid-cols-2 gap-1">
665
+ <CompactInput
666
+ label="X"
667
+ value={`${shadow.x}px`}
668
+ property={`textShadow-${i}-x`}
669
+ onChange={(_p, v) => {
670
+ const num = parseFloat(v)
671
+ if (!isNaN(num)) updateShadow(i, { x: num })
672
+ }}
673
+ units={['px']}
674
+ />
675
+ <CompactInput
676
+ label="Y"
677
+ value={`${shadow.y}px`}
678
+ property={`textShadow-${i}-y`}
679
+ onChange={(_p, v) => {
680
+ const num = parseFloat(v)
681
+ if (!isNaN(num)) updateShadow(i, { y: num })
682
+ }}
683
+ units={['px']}
684
+ />
685
+ <CompactInput
686
+ label="B"
687
+ value={`${shadow.blur}px`}
688
+ property={`textShadow-${i}-blur`}
689
+ onChange={(_p, v) => {
690
+ const num = parseFloat(v)
691
+ if (!isNaN(num)) updateShadow(i, { blur: Math.max(0, num) })
692
+ }}
693
+ units={['px']}
694
+ min={0}
695
+ />
696
+ </div>
697
+ <ColorPicker
698
+ label="Color"
699
+ value={shadow.color}
700
+ onChange={(c) => updateShadow(i, { color: c })}
701
+ onSelectVariable={(varExpr) =>
702
+ updateShadow(i, { color: varExpr })
703
+ }
704
+ />
705
+ </div>
706
+ ))}
707
+
708
+ {shadows.length === 0 && (
709
+ <div
710
+ className="text-[11px] py-1"
711
+ style={{ color: 'var(--text-muted)' }}
712
+ >
713
+ No text shadows
714
+ </div>
715
+ )}
716
+ </SectionHeader>
717
+ </SectionHeader>
718
+ )
719
+ }