@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,238 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { readdirSync, readFileSync, statSync } from 'node:fs'
3
+ import path from 'node:path'
4
+ import { validateProjectRoot } from '@/lib/validatePath'
5
+ import {
6
+ extractDesignTokensFromSource,
7
+ TOKEN_FILE_NAMES,
8
+ } from '@/lib/cssVariableUtils'
9
+
10
+ const SKIP_DIRS = new Set([
11
+ 'node_modules',
12
+ '.next',
13
+ 'dist',
14
+ 'build',
15
+ '.git',
16
+ '__tests__',
17
+ '__mocks__',
18
+ '.turbo',
19
+ '.vercel',
20
+ 'coverage',
21
+ '.cache',
22
+ '.output',
23
+ ])
24
+
25
+ const CSS_EXTENSIONS = new Set(['.css', '.scss', '.less'])
26
+ const MAX_FILES = 2000
27
+ const MAX_FILE_SIZE = 512 * 1024 // 512KB per file
28
+
29
+ /**
30
+ * Regex to match CSS custom property definitions:
31
+ * --variable-name: value;
32
+ * Captures: group 1 = variable name, group 2 = value (before ;)
33
+ */
34
+ const CSS_VAR_RE = /^\s*(--[\w-]+)\s*:\s*([^;]+);/gm
35
+
36
+ interface ScannedVariable {
37
+ value: string
38
+ resolvedValue: string
39
+ selector: string
40
+ source: string // relative file path
41
+ }
42
+
43
+ function walkForCSS(
44
+ dir: string,
45
+ projectRoot: string,
46
+ results: Map<string, ScannedVariable>,
47
+ counter: { count: number },
48
+ ): void {
49
+ if (counter.count >= MAX_FILES) return
50
+
51
+ let entries: import('node:fs').Dirent[]
52
+ try {
53
+ entries = readdirSync(dir, { withFileTypes: true })
54
+ } catch {
55
+ return
56
+ }
57
+
58
+ for (const entry of entries) {
59
+ if (counter.count >= MAX_FILES) return
60
+ if (entry.name.startsWith('.')) continue
61
+ if (SKIP_DIRS.has(entry.name)) continue
62
+
63
+ const fullPath = path.join(dir, entry.name)
64
+
65
+ if (entry.isDirectory()) {
66
+ walkForCSS(fullPath, projectRoot, results, counter)
67
+ continue
68
+ }
69
+
70
+ if (!entry.isFile()) continue
71
+ counter.count++
72
+
73
+ const ext = path.extname(entry.name)
74
+ if (!CSS_EXTENSIONS.has(ext)) continue
75
+
76
+ let content: string
77
+ try {
78
+ const stat = statSync(fullPath)
79
+ if (stat.size > MAX_FILE_SIZE) continue
80
+ content = readFileSync(fullPath, 'utf-8')
81
+ } catch {
82
+ continue
83
+ }
84
+
85
+ const relativePath = path.relative(projectRoot, fullPath)
86
+ let match: RegExpExecArray | null
87
+ CSS_VAR_RE.lastIndex = 0
88
+
89
+ while ((match = CSS_VAR_RE.exec(content)) !== null) {
90
+ const name = match[1].trim()
91
+ const rawValue = match[2].trim()
92
+
93
+ // Skip framework internal variables
94
+ if (
95
+ name.startsWith('--tw-') ||
96
+ name.startsWith('--next-') ||
97
+ name.startsWith('--radix-') ||
98
+ name.startsWith('--chakra-') ||
99
+ name.startsWith('--mantine-') ||
100
+ name.startsWith('--mui-') ||
101
+ name.startsWith('--framer-') ||
102
+ name.startsWith('--sb-') ||
103
+ name.startsWith('--css-interop-')
104
+ ) {
105
+ continue
106
+ }
107
+
108
+ // Only add first occurrence (keeps the most "root" definition)
109
+ if (!results.has(name)) {
110
+ results.set(name, {
111
+ value: rawValue,
112
+ resolvedValue: rawValue, // static scan — no computed resolution
113
+ selector: ':root',
114
+ source: relativePath,
115
+ })
116
+ }
117
+ }
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Walk the project tree looking for JS/TS/Dart design-token files
123
+ * (e.g. colors.ts, theme.dart, palette.js) and extract exported constants.
124
+ */
125
+ function walkForTokenFiles(
126
+ dir: string,
127
+ projectRoot: string,
128
+ results: Record<
129
+ string,
130
+ { value: string; resolvedValue: string; selector: string }
131
+ >,
132
+ counter: { count: number },
133
+ ): void {
134
+ if (counter.count >= MAX_FILES) return
135
+
136
+ let entries: import('node:fs').Dirent[]
137
+ try {
138
+ entries = readdirSync(dir, { withFileTypes: true })
139
+ } catch {
140
+ return
141
+ }
142
+
143
+ for (const entry of entries) {
144
+ if (counter.count >= MAX_FILES) return
145
+ if (entry.name.startsWith('.')) continue
146
+ if (SKIP_DIRS.has(entry.name)) continue
147
+
148
+ const fullPath = path.join(dir, entry.name)
149
+
150
+ if (entry.isDirectory()) {
151
+ walkForTokenFiles(fullPath, projectRoot, results, counter)
152
+ continue
153
+ }
154
+
155
+ if (!entry.isFile()) continue
156
+ counter.count++
157
+
158
+ if (!TOKEN_FILE_NAMES.has(entry.name)) continue
159
+
160
+ let content: string
161
+ try {
162
+ const stat = statSync(fullPath)
163
+ if (stat.size > MAX_FILE_SIZE) continue
164
+ content = readFileSync(fullPath, 'utf-8')
165
+ } catch {
166
+ continue
167
+ }
168
+
169
+ const relativePath = path.relative(projectRoot, fullPath)
170
+ const tokens = extractDesignTokensFromSource(content, relativePath)
171
+
172
+ for (const [name, def] of Object.entries(tokens)) {
173
+ if (!results[name]) {
174
+ results[name] = def
175
+ }
176
+ }
177
+ }
178
+ }
179
+
180
+ export async function POST(request: Request): Promise<NextResponse> {
181
+ let body: { projectRoot?: string }
182
+ try {
183
+ body = await request.json()
184
+ } catch {
185
+ return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
186
+ }
187
+
188
+ const { projectRoot } = body
189
+ if (!projectRoot || typeof projectRoot !== 'string') {
190
+ return NextResponse.json(
191
+ { error: 'projectRoot is required' },
192
+ { status: 400 },
193
+ )
194
+ }
195
+
196
+ const rootError = validateProjectRoot(projectRoot)
197
+ if (rootError) {
198
+ return NextResponse.json({ error: rootError }, { status: 400 })
199
+ }
200
+
201
+ const resolved = path.resolve(projectRoot)
202
+
203
+ try {
204
+ const cssResults = new Map<string, ScannedVariable>()
205
+ const counter = { count: 0 }
206
+
207
+ walkForCSS(resolved, resolved, cssResults, counter)
208
+
209
+ // Convert CSS results to the format expected by the store
210
+ const definitions: Record<
211
+ string,
212
+ { value: string; resolvedValue: string; selector: string }
213
+ > = {}
214
+ for (const [name, def] of cssResults) {
215
+ definitions[name] = {
216
+ value: def.value,
217
+ resolvedValue: def.resolvedValue,
218
+ selector: def.selector,
219
+ }
220
+ }
221
+
222
+ // Also scan for JS/TS/Dart token files (React Native, Flutter, etc.)
223
+ const tokenCounter = { count: 0 }
224
+ walkForTokenFiles(resolved, resolved, definitions, tokenCounter)
225
+
226
+ return NextResponse.json({
227
+ definitions,
228
+ count: Object.keys(definitions).length,
229
+ filesScanned: counter.count + tokenCounter.count,
230
+ })
231
+ } catch (error) {
232
+ const message = error instanceof Error ? error.message : 'Unknown error'
233
+ return NextResponse.json(
234
+ { error: 'Scan failed', details: message },
235
+ { status: 500 },
236
+ )
237
+ }
238
+ }
@@ -0,0 +1,40 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { existsSync } from 'node:fs'
3
+ import path from 'node:path'
4
+ import { validateProjectRoot } from '@/lib/validatePath'
5
+ import { scanProject } from '@/lib/projectScanner'
6
+
7
+ export async function POST(request: Request): Promise<NextResponse> {
8
+ let body: { projectRoot?: string }
9
+ try {
10
+ body = await request.json()
11
+ } catch {
12
+ return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
13
+ }
14
+
15
+ const { projectRoot } = body
16
+ if (!projectRoot || typeof projectRoot !== 'string') {
17
+ return NextResponse.json(
18
+ { error: 'projectRoot is required' },
19
+ { status: 400 },
20
+ )
21
+ }
22
+
23
+ const rootError = validateProjectRoot(projectRoot)
24
+ if (rootError) {
25
+ return NextResponse.json({ error: rootError }, { status: 400 })
26
+ }
27
+
28
+ const resolved = path.resolve(projectRoot)
29
+
30
+ const pkgPath = path.join(resolved, 'package.json')
31
+ if (!existsSync(pkgPath)) {
32
+ return NextResponse.json(
33
+ { error: 'Not a valid project directory — no package.json found' },
34
+ { status: 400 },
35
+ )
36
+ }
37
+
38
+ const result = scanProject(resolved)
39
+ return NextResponse.json(result)
40
+ }
@@ -0,0 +1,172 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { existsSync, readFileSync } from 'node:fs'
3
+ import path from 'node:path'
4
+ import { validateProjectRoot } from '@/lib/validatePath'
5
+ import type { CSSVariableDefinition } from '@/types/cssVariables'
6
+
7
+ const MAX_FILE_SIZE = 256 * 1024 // 256KB
8
+
9
+ /** Config file names to search for, in priority order */
10
+ const CONFIG_FILES = [
11
+ 'tailwind.config.ts',
12
+ 'tailwind.config.js',
13
+ 'tailwind.config.mjs',
14
+ 'tailwind.config.cjs',
15
+ ]
16
+
17
+ /**
18
+ * Extract color definitions from a Tailwind v3 config file.
19
+ *
20
+ * Parses patterns like:
21
+ * theme: { extend: { colors: { primary: '#4a9eff', secondary: { 50: '#fff', 500: '#333' } } } }
22
+ * theme: { colors: { ... } }
23
+ *
24
+ * Uses regex-based extraction (no JS eval for security).
25
+ */
26
+ function extractTailwindColors(
27
+ source: string,
28
+ filePath: string,
29
+ ): Record<string, CSSVariableDefinition> {
30
+ const results: Record<string, CSSVariableDefinition> = {}
31
+
32
+ // Strip comments
33
+ const cleaned = source
34
+ .replace(/\/\/.*$/gm, '')
35
+ .replace(/\/\*[\s\S]*?\*\//g, '')
36
+
37
+ // Find colors blocks in theme.extend.colors or theme.colors
38
+ // Match: colors: { ... } or colors: { ... },
39
+ const colorsBlockRe = /colors\s*:\s*\{([^]*?)\}(?:\s*,|\s*\})/g
40
+ let blockMatch: RegExpExecArray | null
41
+ colorsBlockRe.lastIndex = 0
42
+
43
+ while ((blockMatch = colorsBlockRe.exec(cleaned)) !== null) {
44
+ const body = blockMatch[1]
45
+ parseColorBlock(body, '', results, filePath)
46
+ }
47
+
48
+ return results
49
+ }
50
+
51
+ /**
52
+ * Recursively parse a color block, handling nested objects.
53
+ */
54
+ function parseColorBlock(
55
+ body: string,
56
+ prefix: string,
57
+ results: Record<string, CSSVariableDefinition>,
58
+ filePath: string,
59
+ ): void {
60
+ // Match nested objects: key: { ... }
61
+ const nestedRe = /(\w[\w-]*)\s*:\s*\{([^}]*)\}/g
62
+ const nestedKeys = new Set<string>()
63
+ let nestedMatch: RegExpExecArray | null
64
+ nestedRe.lastIndex = 0
65
+
66
+ while ((nestedMatch = nestedRe.exec(body)) !== null) {
67
+ const key = nestedMatch[1]
68
+ nestedKeys.add(key)
69
+ const nestedBody = nestedMatch[2]
70
+ const nestedPrefix = prefix ? `${prefix}-${kebab(key)}` : kebab(key)
71
+ parseColorBlock(nestedBody, nestedPrefix, results, filePath)
72
+ }
73
+
74
+ // Match flat entries: key: 'value' or key: "value" or key: value
75
+ const entryRe = /(\w[\w-]*)\s*:\s*(?:'([^']*)'|"([^"]*)")/g
76
+ let entryMatch: RegExpExecArray | null
77
+ entryRe.lastIndex = 0
78
+
79
+ while ((entryMatch = entryRe.exec(body)) !== null) {
80
+ const key = entryMatch[1]
81
+ if (nestedKeys.has(key)) continue
82
+
83
+ const value = entryMatch[2] ?? entryMatch[3] ?? ''
84
+ if (!value) continue
85
+
86
+ // Skip non-color values (functions, references)
87
+ if (
88
+ value.includes('(') &&
89
+ !value.startsWith('rgb') &&
90
+ !value.startsWith('hsl') &&
91
+ !value.startsWith('oklch')
92
+ )
93
+ continue
94
+
95
+ const varName = prefix ? `--${prefix}-${kebab(key)}` : `--${kebab(key)}`
96
+ results[varName] = {
97
+ value,
98
+ resolvedValue: value,
99
+ selector: `tailwind:${filePath}`,
100
+ }
101
+ }
102
+ }
103
+
104
+ /** Convert camelCase to kebab-case */
105
+ function kebab(str: string): string {
106
+ return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
107
+ }
108
+
109
+ export async function POST(request: Request): Promise<NextResponse> {
110
+ let body: { projectRoot?: string }
111
+ try {
112
+ body = await request.json()
113
+ } catch {
114
+ return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 })
115
+ }
116
+
117
+ const { projectRoot } = body
118
+ if (!projectRoot || typeof projectRoot !== 'string') {
119
+ return NextResponse.json(
120
+ { error: 'projectRoot is required' },
121
+ { status: 400 },
122
+ )
123
+ }
124
+
125
+ const rootError = validateProjectRoot(projectRoot)
126
+ if (rootError) {
127
+ return NextResponse.json({ error: rootError }, { status: 400 })
128
+ }
129
+
130
+ const resolved = path.resolve(projectRoot)
131
+
132
+ // Find the Tailwind config file
133
+ let configPath: string | null = null
134
+ for (const name of CONFIG_FILES) {
135
+ const candidate = path.join(resolved, name)
136
+ if (existsSync(candidate)) {
137
+ configPath = candidate
138
+ break
139
+ }
140
+ }
141
+
142
+ if (!configPath) {
143
+ return NextResponse.json({ definitions: {}, count: 0, found: false })
144
+ }
145
+
146
+ try {
147
+ const stat = readFileSync(configPath)
148
+ if (stat.length > MAX_FILE_SIZE) {
149
+ return NextResponse.json(
150
+ { error: 'Config file too large' },
151
+ { status: 400 },
152
+ )
153
+ }
154
+
155
+ const content = stat.toString('utf-8')
156
+ const relativePath = path.relative(resolved, configPath)
157
+ const definitions = extractTailwindColors(content, relativePath)
158
+
159
+ return NextResponse.json({
160
+ definitions,
161
+ count: Object.keys(definitions).length,
162
+ found: true,
163
+ configFile: relativePath,
164
+ })
165
+ } catch (error) {
166
+ const message = error instanceof Error ? error.message : 'Unknown error'
167
+ return NextResponse.json(
168
+ { error: 'Parse failed', details: message },
169
+ { status: 500 },
170
+ )
171
+ }
172
+ }