@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,402 @@
1
+ /**
2
+ * Prompt construction utilities for Claude CLI integration.
3
+ *
4
+ * buildAnalysisPrompt — read-only analysis session (--allowedTools Read)
5
+ * buildApplyPrompt — resume session to apply edits (--allowedTools Read,Edit)
6
+ */
7
+
8
+ import type { ProjectScanResult } from '@/types/claude'
9
+
10
+ /**
11
+ * Build framework-specific styling instructions for Claude analysis prompts.
12
+ */
13
+ function buildFrameworkInstructions(scan?: ProjectScanResult | null): string {
14
+ const framework = scan?.framework ?? null
15
+ const usesTailwind = scan?.cssStrategy?.includes('tailwind') ?? false
16
+
17
+ if (framework === 'flutter') {
18
+ return [
19
+ '**Flutter / Dart**: This is a Flutter project. Styles are applied via widget',
20
+ ' properties, NOT CSS. Map CSS changes to Dart equivalents:',
21
+ ' - padding/margin → EdgeInsets (EdgeInsets.all(n), EdgeInsets.symmetric(...))',
22
+ ' - font-size → TextStyle(fontSize: n)',
23
+ ' - font-weight → TextStyle(fontWeight: FontWeight.w500)',
24
+ ' - color → TextStyle(color: Color(0xFF______))',
25
+ ' - background-color → Container(color: ...) or ColoredBox',
26
+ ' - display: flex → Row (horizontal) or Column (vertical)',
27
+ ' - justify-content → MainAxisAlignment.center/spaceBetween/etc.',
28
+ ' - align-items → CrossAxisAlignment.center/start/etc.',
29
+ ' - border-radius → BoxDecoration(borderRadius: BorderRadius.circular(n))',
30
+ ' - width/height → SizedBox(width: n, height: n)',
31
+ ' - opacity → Opacity(opacity: n, child: ...)',
32
+ ].join('\n')
33
+ }
34
+
35
+ if (framework === 'react-native') {
36
+ return [
37
+ '**React Native**: This is a React Native project. Styles use StyleSheet objects,',
38
+ ' NOT CSS classes or Tailwind. Map CSS changes to RN style properties:',
39
+ ' - Styles applied via `style` prop: `<View style={styles.container} />`',
40
+ ' - Defined in `StyleSheet.create({ ... })`',
41
+ ' - padding/margin → padding, paddingHorizontal, margin, marginVertical, etc. (number, no units)',
42
+ ' - font-size → fontSize: n',
43
+ ' - font-weight → fontWeight: "600" (string values)',
44
+ ' - color → color: "#hex"',
45
+ ' - background-color → backgroundColor: "#hex"',
46
+ ' - display: flex is default; flexDirection defaults to "column" (opposite of web)',
47
+ ' - justify-content → justifyContent: "center"',
48
+ ' - align-items → alignItems: "center"',
49
+ ' - border-radius → borderRadius: n (number only)',
50
+ ' - box-shadow → iOS: shadowColor/shadowOffset/shadowOpacity; Android: elevation',
51
+ ].join('\n')
52
+ }
53
+
54
+ if (usesTailwind) {
55
+ return [
56
+ '**Tailwind CSS (detected)**: Styles are applied via `className` on JSX elements.',
57
+ ' Find the component rendering the element and update its Tailwind utility classes.',
58
+ ' NEVER add inline styles when the project uses Tailwind.',
59
+ ' - Replace old Tailwind classes with new ones (e.g., `p-2` → `p-4`,',
60
+ ' `text-sm` → `text-lg`, `bg-gray-100` → `bg-blue-500`).',
61
+ ' - Use the Tailwind spacing scale: 4px = 1 unit (p-1=4px, p-2=8px,',
62
+ ' p-4=16px, p-6=24px, p-8=32px).',
63
+ ' - For values not on the scale, use arbitrary syntax: `p-[13px]`,',
64
+ ' `text-[15px]`, `bg-[#1e1e1e]`.',
65
+ ' - For responsive changes, use mobile-first prefixes:',
66
+ ' - Changes marked [mobile] → base classes (no prefix)',
67
+ ' - Changes marked [tablet] → `md:` prefix (768px+)',
68
+ ' - Changes marked [desktop] → `lg:` or `xl:` prefix (1024px+ / 1280px+)',
69
+ ' - Example: `className="text-sm md:text-base lg:text-lg"`',
70
+ ' - When using clsx/cn helper, preserve the conditional structure:',
71
+ ' `cn("base-classes", condition && "conditional-classes")`.',
72
+ ].join('\n')
73
+ }
74
+
75
+ // Generic web — unknown CSS approach
76
+ return [
77
+ ' Determine the CSS approach used (Tailwind classes, CSS modules,',
78
+ ' styled-components, inline styles, global stylesheets, etc.) and',
79
+ ' apply changes using that same approach. For Tailwind, update className',
80
+ ' utility classes. For CSS files, edit the declarations directly.',
81
+ ].join('\n')
82
+ }
83
+
84
+ /**
85
+ * Build a "## Project Context" section from scan results.
86
+ */
87
+ function buildProjectContextSection(scan: ProjectScanResult): string {
88
+ const lines: string[] = ['', '## Project Context', '']
89
+ if (scan.framework) {
90
+ lines.push(`- Framework: ${scan.framework}`)
91
+ }
92
+ if (scan.cssStrategy.length > 0) {
93
+ lines.push(`- CSS: ${scan.cssStrategy.join(', ')}`)
94
+ }
95
+ if (scan.cssFiles.length > 0) {
96
+ lines.push(`- Key CSS files: ${scan.cssFiles.join(', ')}`)
97
+ }
98
+ if (scan.srcDirs.length > 0) {
99
+ lines.push(`- Source directories: ${scan.srcDirs.join(', ')}`)
100
+ }
101
+ if (scan.packageName) {
102
+ lines.push(`- Package: ${scan.packageName}`)
103
+ }
104
+
105
+ // File map summary
106
+ if (scan.fileMap) {
107
+ const { routes, components } = scan.fileMap
108
+
109
+ if (routes.length > 0) {
110
+ const pageRoutes = routes.filter((r) => r.type === 'page')
111
+ const layoutRoutes = routes.filter((r) => r.type === 'layout')
112
+ lines.push('')
113
+ lines.push('### Route Files')
114
+ for (const r of pageRoutes) {
115
+ lines.push(`- ${r.urlPattern} → ${r.filePath}`)
116
+ }
117
+ for (const r of layoutRoutes) {
118
+ lines.push(`- ${r.urlPattern} (layout) → ${r.filePath}`)
119
+ }
120
+ }
121
+
122
+ if (components.length > 0) {
123
+ // Group by category and list directory with counts
124
+ const dirCounts = new Map<string, { count: number; category: string }>()
125
+ for (const comp of components) {
126
+ const dir =
127
+ comp.filePath.substring(0, comp.filePath.lastIndexOf('/')) || '.'
128
+ const existing = dirCounts.get(dir)
129
+ if (existing) {
130
+ existing.count++
131
+ } else {
132
+ dirCounts.set(dir, { count: 1, category: comp.category })
133
+ }
134
+ }
135
+ lines.push('')
136
+ lines.push('### Source Files')
137
+ for (const [dir, { count, category }] of dirCounts) {
138
+ lines.push(`- ${dir}/ (${count} ${category}${count !== 1 ? 's' : ''})`)
139
+ }
140
+ }
141
+ }
142
+
143
+ return lines.join('\n')
144
+ }
145
+
146
+ /**
147
+ * Construct an analysis prompt from a changelog and project root.
148
+ *
149
+ * The resulting prompt instructs Claude to:
150
+ * 1. Read the relevant source files in the project.
151
+ * 2. Analyse the visual changelog entries.
152
+ * 3. Produce unified diffs that implement every change.
153
+ *
154
+ * If a ProjectScanResult is provided, a "Project Context" section is appended
155
+ * so Claude has immediate knowledge of the project's framework and CSS approach.
156
+ */
157
+ export function buildAnalysisPrompt(
158
+ changelog: string,
159
+ projectRoot: string,
160
+ scan?: ProjectScanResult | null,
161
+ ): string {
162
+ const prompt = [
163
+ 'You are a front-end code assistant. A user has made visual edits to their',
164
+ 'web application via a design editor. Your job is to translate those visual',
165
+ 'changes into concrete source-code modifications.',
166
+ '',
167
+ `The project is located at: ${projectRoot}`,
168
+ '',
169
+ '## Changelog',
170
+ '',
171
+ 'Below is the changelog exported from the visual editor. Each entry',
172
+ 'describes a CSS property change applied to a specific DOM element',
173
+ '(identified by its CSS selector path), the original value, and the new',
174
+ 'value.',
175
+ '',
176
+ '```',
177
+ changelog.trim(),
178
+ '```',
179
+ '',
180
+ '## Instructions',
181
+ '',
182
+ '1. Read the relevant source files in the project to understand the',
183
+ ' current code.',
184
+ '2. For every change listed in the changelog, determine which source',
185
+ ' file(s) need to be modified and how.',
186
+ buildFrameworkInstructions(scan),
187
+ '3. Output unified diffs for every file that needs to change.',
188
+ '',
189
+ '## Output format',
190
+ '',
191
+ 'Return ONLY unified diffs — no commentary before or after.',
192
+ 'Use the standard unified diff format:',
193
+ '',
194
+ '```',
195
+ '--- a/<relative-path-to-file>',
196
+ '+++ b/<relative-path-to-file>',
197
+ '@@ -<old-start>,<old-count> +<new-start>,<new-count> @@',
198
+ ' context line',
199
+ '-removed line',
200
+ '+added line',
201
+ '```',
202
+ '',
203
+ 'Include enough context lines (3 is ideal) around each change so the',
204
+ 'diff can be applied unambiguously. If multiple hunks affect the same',
205
+ 'file, combine them under a single --- / +++ header.',
206
+ ].join('\n')
207
+
208
+ if (scan) {
209
+ return prompt + buildProjectContextSection(scan)
210
+ }
211
+ return prompt
212
+ }
213
+
214
+ /**
215
+ * Construct the follow-up prompt used when resuming the Claude session
216
+ * to apply the diffs that were previously analysed.
217
+ *
218
+ * This prompt is used with `--allowedTools Read,Edit` so that Claude
219
+ * can read and then edit the source files directly.
220
+ */
221
+ /**
222
+ * Construct a scan prompt that asks Claude to analyse the changelog and
223
+ * produce a structured, context-aware "smart prompt" another Claude Code
224
+ * instance can use to apply the changes.
225
+ *
226
+ * Uses `--allowedTools Read` so Claude can inspect source files for
227
+ * file-routing accuracy and CSS-approach detection.
228
+ */
229
+ export function buildScanPrompt(
230
+ changelog: string,
231
+ projectRoot: string,
232
+ scan?: ProjectScanResult | null,
233
+ ): string {
234
+ const prompt = [
235
+ 'You are a front-end design system analyst. A user has made visual changes',
236
+ 'to their web application using a design editor. Your job is to analyse',
237
+ 'those changes and produce a smart, context-aware prompt that another',
238
+ 'Claude Code instance will use to apply the changes to source code.',
239
+ '',
240
+ `The project is located at: ${projectRoot}`,
241
+ '',
242
+ '## Your Task',
243
+ '',
244
+ '1. Read the changelog below carefully.',
245
+ '2. Use the Read tool to examine the relevant source files in the project.',
246
+ '3. Analyse the pattern and intent behind the changes.',
247
+ '4. Produce a structured JSON result wrapped in markers.',
248
+ '',
249
+ '## Analysis Steps',
250
+ '',
251
+ '### Intent Detection',
252
+ 'Look at the combination of properties changed across all elements:',
253
+ '- Mostly font-size, font-weight, line-height, letter-spacing, color on text => "Typography redesign"',
254
+ '- Mostly margin, padding, gap => "Spacing normalization"',
255
+ '- Mostly background-color, color, border-color => "Color palette update"',
256
+ '- Mostly display, flex-direction, justify-content, align-items, grid-* => "Layout restructuring"',
257
+ '- Mostly border-radius, box-shadow, opacity => "Visual refinement"',
258
+ '- Mixed => describe the primary intent based on the dominant pattern',
259
+ '',
260
+ '### Logical Grouping',
261
+ 'Group the changes into these categories:',
262
+ '- typography: font-*, text-*, letter-spacing, line-height, color (on text)',
263
+ '- spacing: margin-*, padding-*, gap, row-gap, column-gap',
264
+ '- colors: background-color, color, border-color, fill, stroke',
265
+ '- layout: display, flex-*, grid-*, justify-*, align-*, order',
266
+ '- borders: border-width, border-style, border-radius',
267
+ '- background: background-image, background-size, background-position',
268
+ '- effects: box-shadow, opacity, filter, transform',
269
+ '',
270
+ '### File Routing',
271
+ 'For each CSS selector in the changelog:',
272
+ '1. Read the project source files to find where the matching element is defined.',
273
+ '2. Determine the CSS approach:',
274
+ ' - **Tailwind CSS (React/Next.js)**: Styles live in `className` on JSX elements.',
275
+ ' Look for the component file (.tsx/.jsx) that renders the element.',
276
+ ' Changes must modify the Tailwind utility classes in className.',
277
+ ' - **CSS Modules**: Look for `*.module.css` imports and matching class names.',
278
+ ' - **Global CSS**: Check `globals.css`, `app.css`, or similar.',
279
+ ' - **Inline styles**: `style={{ }}` prop on JSX elements.',
280
+ '3. Record the file path where changes should be applied.',
281
+ '',
282
+ '### Scope Expansion',
283
+ 'Look for patterns suggesting broader changes:',
284
+ '- Same color changed on multiple elements => suggest a CSS variable or design token.',
285
+ '- Spacing follows a consistent system (multiples of 4px/8px) => note the grid.',
286
+ '- Font-size changes map to a Tailwind scale => suggest using the scale consistently.',
287
+ '',
288
+ '### Conflict Detection',
289
+ 'Flag potential issues:',
290
+ '- Breakpoint-specific changes that might conflict with other breakpoints',
291
+ "- Values that don't match the project's existing design system",
292
+ '- Accessibility concerns (font-size below 12px, insufficient contrast)',
293
+ '',
294
+ '## Changelog',
295
+ '',
296
+ '```',
297
+ changelog.trim(),
298
+ '```',
299
+ '',
300
+ '## Output Format',
301
+ '',
302
+ 'Output a JSON object wrapped in these exact markers:',
303
+ '',
304
+ '--- SCAN RESULT JSON ---',
305
+ '{',
306
+ ' "intent": "Brief description of the overall design intent",',
307
+ ' "smartPrompt": "A detailed, actionable prompt that another Claude Code',
308
+ ' instance can use to apply these changes. Include specific file paths,',
309
+ ' the CSS approach used (Tailwind, CSS modules, etc.), grouped instructions,',
310
+ ' and any design system considerations. Write as clear instructions, not',
311
+ ' as a changelog. The recipient will not have the original changelog.",',
312
+ ' "groups": [',
313
+ ' {',
314
+ ' "label": "Human-readable group name",',
315
+ ' "category": "typography|spacing|colors|layout|borders|background|effects|mixed",',
316
+ ' "changeCount": 3,',
317
+ ' "suggestedFiles": ["src/components/Header.tsx"]',
318
+ ' }',
319
+ ' ],',
320
+ ' "warnings": ["Any potential conflicts, accessibility issues, or concerns"]',
321
+ '}',
322
+ '--- END SCAN RESULT ---',
323
+ '',
324
+ 'IMPORTANT: The smartPrompt field must be a complete, self-contained prompt.',
325
+ 'It should read naturally as instructions — do not reference "the changelog"',
326
+ 'since the recipient will not have it. Include actual values, selectors, and',
327
+ 'file paths inline.',
328
+ '',
329
+ 'PERFORMANCE: Minimise the number of Read calls. If Project Context is',
330
+ 'provided below, use it to infer file paths instead of reading the filesystem.',
331
+ 'Only read 1-3 key files to confirm the CSS approach (Tailwind, CSS modules, etc.).',
332
+ 'Do not read every component file — focus on the most relevant ones.',
333
+ ].join('\n')
334
+
335
+ if (scan) {
336
+ return prompt + buildProjectContextSection(scan)
337
+ }
338
+ return prompt
339
+ }
340
+
341
+ /**
342
+ * Wrap an AI-Scan smart prompt with diff-output instructions so the
343
+ * analyze route can produce unified diffs from it.
344
+ */
345
+ export function buildSmartAnalysisPrompt(
346
+ smartPrompt: string,
347
+ projectRoot: string,
348
+ ): string {
349
+ return [
350
+ 'You are a front-end code assistant. Below are detailed instructions',
351
+ "describing visual changes to apply to a web application's source code.",
352
+ '',
353
+ `The project is located at: ${projectRoot}`,
354
+ '',
355
+ '## Change Instructions',
356
+ '',
357
+ smartPrompt.trim(),
358
+ '',
359
+ '## Your Task',
360
+ '',
361
+ '1. Read the relevant source files mentioned in the instructions above.',
362
+ '2. Apply every change described — use the CSS approach already in place.',
363
+ ' For Tailwind CSS projects: modify className utility classes on JSX elements.',
364
+ ' Never add inline styles if the project uses Tailwind.',
365
+ ' Use arbitrary value syntax [value] when no Tailwind scale matches.',
366
+ ' Use responsive prefixes (md:, lg:, xl:) for breakpoint-specific changes.',
367
+ '3. Output unified diffs for every file that needs to change.',
368
+ '',
369
+ '## Output format',
370
+ '',
371
+ 'Return ONLY unified diffs — no commentary before or after.',
372
+ 'Use the standard unified diff format:',
373
+ '',
374
+ '```',
375
+ '--- a/<relative-path-to-file>',
376
+ '+++ b/<relative-path-to-file>',
377
+ '@@ -<old-start>,<old-count> +<new-start>,<new-count> @@',
378
+ ' context line',
379
+ '-removed line',
380
+ '+added line',
381
+ '```',
382
+ '',
383
+ 'Include enough context lines (3 is ideal) around each change so the',
384
+ 'diff can be applied unambiguously. If multiple hunks affect the same',
385
+ 'file, combine them under a single --- / +++ header.',
386
+ ].join('\n')
387
+ }
388
+
389
+ export function buildApplyPrompt(): string {
390
+ return [
391
+ 'Apply the changes you previously analysed.',
392
+ '',
393
+ 'Use the Edit tool to modify each file exactly as described in the',
394
+ 'unified diffs you generated. Follow these rules:',
395
+ '',
396
+ '1. Read each file before editing to confirm current contents.',
397
+ '2. Apply changes in the order they were listed.',
398
+ '3. Do NOT make any changes beyond what was specified in the diffs.',
399
+ '4. After all edits are applied, output a brief summary listing each',
400
+ ' file modified and the number of hunks applied.',
401
+ ].join('\n')
402
+ }
@@ -0,0 +1,124 @@
1
+ import type { ShadowData } from '@/types/shadow'
2
+
3
+ /**
4
+ * Split a box-shadow CSS string by commas, respecting nested parentheses.
5
+ */
6
+ function splitShadows(css: string): string[] {
7
+ const parts: string[] = []
8
+ let depth = 0
9
+ let current = ''
10
+ for (let i = 0; i < css.length; i++) {
11
+ const ch = css[i]
12
+ if (ch === '(') depth++
13
+ else if (ch === ')') depth--
14
+ if (ch === ',' && depth === 0) {
15
+ parts.push(current.trim())
16
+ current = ''
17
+ } else {
18
+ current += ch
19
+ }
20
+ }
21
+ if (current.trim()) parts.push(current.trim())
22
+ return parts
23
+ }
24
+
25
+ /**
26
+ * Parse a single shadow string like "2px 4px 6px 1px rgba(0,0,0,0.3)" or "inset 0 2px 4px #000"
27
+ */
28
+ function parseSingleShadow(str: string): ShadowData | null {
29
+ const trimmed = str.trim()
30
+ if (!trimmed || trimmed === 'none') return null
31
+
32
+ let inset = false
33
+ let working = trimmed
34
+
35
+ // Check for inset keyword
36
+ if (working.startsWith('inset ')) {
37
+ inset = true
38
+ working = working.slice(6).trim()
39
+ } else if (working.endsWith(' inset')) {
40
+ inset = true
41
+ working = working.slice(0, -6).trim()
42
+ }
43
+
44
+ // Extract color — could be at start or end, could be rgb/rgba/hsl/hsla/hex/named
45
+ // Strategy: try to find a color function or hex at the end first, then at the start
46
+ let color = 'rgba(0,0,0,0.25)'
47
+ let numericPart = working
48
+
49
+ // Try color at end: rgb(...), rgba(...), hsl(...), hsla(...), #hex, or named color
50
+ const colorFuncEnd = working.match(/((?:rgba?|hsla?)\([^)]+\))$/)
51
+ if (colorFuncEnd) {
52
+ color = colorFuncEnd[1]
53
+ numericPart = working
54
+ .slice(0, working.length - colorFuncEnd[0].length)
55
+ .trim()
56
+ } else {
57
+ const hexEnd = working.match(/(#[0-9a-fA-F]{3,8})$/)
58
+ if (hexEnd) {
59
+ color = hexEnd[1]
60
+ numericPart = working.slice(0, working.length - hexEnd[0].length).trim()
61
+ } else {
62
+ // Try color at start
63
+ const colorFuncStart = working.match(/^((?:rgba?|hsla?)\([^)]+\))\s+/)
64
+ if (colorFuncStart) {
65
+ color = colorFuncStart[1]
66
+ numericPart = working.slice(colorFuncStart[0].length).trim()
67
+ } else {
68
+ // Try named color at the end (last token)
69
+ const tokens = working.split(/\s+/)
70
+ if (tokens.length >= 3) {
71
+ const lastToken = tokens[tokens.length - 1]
72
+ if (!/^-?\d/.test(lastToken) && !lastToken.includes('px')) {
73
+ color = lastToken
74
+ numericPart = tokens.slice(0, -1).join(' ')
75
+ }
76
+ }
77
+ }
78
+ }
79
+ }
80
+
81
+ // Parse numeric values: x y blur spread
82
+ const numTokens = numericPart.split(/\s+/).map((t) => parseFloat(t))
83
+ const x = numTokens[0] || 0
84
+ const y = numTokens[1] || 0
85
+ const blur = numTokens[2] || 0
86
+ const spread = numTokens[3] || 0
87
+
88
+ return { x, y, blur, spread, color, inset }
89
+ }
90
+
91
+ /**
92
+ * Parse a box-shadow CSS string into an array of ShadowData objects.
93
+ * Returns [] for "none" or unparseable.
94
+ */
95
+ export function parseShadow(css: string): ShadowData[] {
96
+ if (!css || css === 'none') return []
97
+ const parts = splitShadows(css)
98
+ const shadows: ShadowData[] = []
99
+ for (const part of parts) {
100
+ const shadow = parseSingleShadow(part)
101
+ if (shadow) shadows.push(shadow)
102
+ }
103
+ return shadows
104
+ }
105
+
106
+ /**
107
+ * Serialize ShadowData array back to a valid CSS box-shadow string.
108
+ * Returns "none" for empty array.
109
+ */
110
+ export function serializeShadow(shadows: ShadowData[]): string {
111
+ if (shadows.length === 0) return 'none'
112
+ return shadows
113
+ .map((s) => {
114
+ const parts: string[] = []
115
+ if (s.inset) parts.push('inset')
116
+ parts.push(`${s.x}px`)
117
+ parts.push(`${s.y}px`)
118
+ parts.push(`${s.blur}px`)
119
+ parts.push(`${s.spread}px`)
120
+ parts.push(s.color)
121
+ return parts.join(' ')
122
+ })
123
+ .join(', ')
124
+ }