agent-flutter 0.1.6 → 0.1.8

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.
@@ -0,0 +1,1997 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { createRequire } from 'node:module'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { parse } from '@babel/parser'
6
+ import traverseModule from '@babel/traverse'
7
+ const traverse = traverseModule.default
8
+ import postcss from 'postcss'
9
+ import safeParser from 'postcss-safe-parser'
10
+
11
+ const __filename = fileURLToPath(import.meta.url)
12
+ const __dirname = path.dirname(__filename)
13
+ const require = createRequire(import.meta.url)
14
+ let sass = null
15
+ try {
16
+ sass = require('sass')
17
+ } catch {}
18
+
19
+ function resolveProjectRoot() {
20
+ const fromCwd = process.cwd()
21
+ if (fs.existsSync(path.resolve(fromCwd, 'pubspec.yaml'))) {
22
+ return fromCwd
23
+ }
24
+ let current = __dirname
25
+ while (true) {
26
+ if (fs.existsSync(path.resolve(current, 'pubspec.yaml'))) {
27
+ return current
28
+ }
29
+ const parent = path.dirname(current)
30
+ if (parent === current) break
31
+ current = parent
32
+ }
33
+ return fromCwd
34
+ }
35
+
36
+ function readPackageName(projectRoot) {
37
+ const pubspecPath = path.resolve(projectRoot, 'pubspec.yaml')
38
+ if (!fs.existsSync(pubspecPath)) return 'app'
39
+ const content = fs.readFileSync(pubspecPath, 'utf8')
40
+ const matched = content.match(/^name:\s*([A-Za-z0-9_]+)/m)
41
+ return matched?.[1] || 'app'
42
+ }
43
+
44
+ const REPO_ROOT = resolveProjectRoot()
45
+ const PROJECT_PACKAGE_NAME = readPackageName(REPO_ROOT)
46
+ const APP_ASSETS_FILE = path.resolve(REPO_ROOT, 'lib/src/utils/app_assets.dart')
47
+ let ICONS_MAP = {}
48
+ try {
49
+ const mapPath = path.resolve(__dirname, 'icons_map.json')
50
+ if (fs.existsSync(mapPath)) ICONS_MAP = JSON.parse(fs.readFileSync(mapPath, 'utf8'))
51
+ } catch {}
52
+
53
+ let ASSET_CONTEXT = null
54
+ const DEFAULT_ASSET_DIR = process.env.JSX2FLUTTER_ASSET_DIR || 'assets/figma'
55
+ const GENERIC_ASSET_TOKENS = new Set([
56
+ 'asset',
57
+ 'auto',
58
+ 'div',
59
+ 'ellipse',
60
+ 'frame',
61
+ 'group',
62
+ 'icon',
63
+ 'image',
64
+ 'img',
65
+ 'line',
66
+ 'path',
67
+ 'rect',
68
+ 'shape',
69
+ 'svg',
70
+ 'vector',
71
+ 'wrapper',
72
+ ])
73
+
74
+ function normalizeAssetPath(p) {
75
+ return p.replace(/\\/g, '/')
76
+ }
77
+
78
+ function sanitizeAssetToken(value) {
79
+ return String(value || '')
80
+ .trim()
81
+ .toLowerCase()
82
+ .replace(/[^a-z0-9]+/g, '_')
83
+ .replace(/^_+|_+$/g, '')
84
+ .replace(/_+/g, '_')
85
+ }
86
+
87
+ function normalizeSemanticAssetToken(value) {
88
+ const base = sanitizeAssetToken(value)
89
+ if (!base) return ''
90
+ const withoutPrefix = base.replace(/^(icon|image|img|ic)_+/, '')
91
+ const trimmed = withoutPrefix
92
+ .replace(/(_?\d+)+$/g, '')
93
+ .replace(/_+/g, '_')
94
+ .replace(/^_+|_+$/g, '')
95
+ return trimmed || withoutPrefix || base
96
+ }
97
+
98
+ function isGenericSemanticToken(token) {
99
+ const normalized = normalizeSemanticAssetToken(token)
100
+ if (!normalized) return true
101
+ if (GENERIC_ASSET_TOKENS.has(normalized)) return true
102
+ if (/^(frame|group|rect|ellipse|vector|path|shape|line|icon|img|image)\d*$/.test(normalized)) return true
103
+ if (/^[a-z0-9]{6,}(?:_[a-z0-9]{5,})+$/.test(normalized) && /\d/.test(normalized)) return true
104
+ if (/^\d+$/.test(normalized)) return true
105
+ return false
106
+ }
107
+
108
+ function pickSemanticAssetToken(absPath, semanticHint, stem) {
109
+ const directHint = normalizeSemanticAssetToken(semanticHint)
110
+ if (directHint && !isGenericSemanticToken(directHint)) return directHint
111
+
112
+ const mappedHint = normalizeSemanticAssetToken(ASSET_CONTEXT?.semanticHintByAbs?.get(absPath))
113
+ if (mappedHint && !isGenericSemanticToken(mappedHint)) return mappedHint
114
+
115
+ const stemHint = normalizeSemanticAssetToken(stem)
116
+ if (stemHint && !isGenericSemanticToken(stemHint)) return stemHint
117
+
118
+ return ''
119
+ }
120
+
121
+ function toAssetAbsPath(src, baseDir) {
122
+ if (!src || !baseDir) return null
123
+ const cleaned = String(src).split('?')[0].split('#')[0]
124
+ if (!cleaned || /^https?:\/\//i.test(cleaned) || /^data:/i.test(cleaned)) return null
125
+ const abs = path.resolve(baseDir, cleaned)
126
+ if (!fs.existsSync(abs)) return null
127
+ return abs
128
+ }
129
+
130
+ function buildAssetOutputName(absPath, semanticHint = '') {
131
+ if (!ASSET_CONTEXT || !ASSET_CONTEXT.renameAssets) {
132
+ return path.basename(absPath)
133
+ }
134
+ const existing = ASSET_CONTEXT.fileNameByAbs.get(absPath)
135
+ if (existing) return existing
136
+
137
+ const extRaw = path.extname(absPath).toLowerCase()
138
+ const ext = extRaw || '.bin'
139
+ const stemRaw = path.basename(absPath, extRaw)
140
+ const stem = sanitizeAssetToken(stemRaw) || 'asset'
141
+ const prefix = sanitizeAssetToken(ASSET_CONTEXT.assetPrefix)
142
+ const kind = ext === '.svg' ? 'ic' : 'img'
143
+ const semanticPart = pickSemanticAssetToken(absPath, semanticHint, stem) || (ext === '.svg' ? 'icon' : 'image')
144
+ const base = [kind, prefix, semanticPart].filter(Boolean).join('_')
145
+
146
+ let candidate = `${base}${ext}`
147
+ let index = 2
148
+ while (ASSET_CONTEXT.usedFileNames.has(candidate)) {
149
+ candidate = `${base}_${index}${ext}`
150
+ index += 1
151
+ }
152
+
153
+ ASSET_CONTEXT.usedFileNames.add(candidate)
154
+ ASSET_CONTEXT.fileNameByAbs.set(absPath, candidate)
155
+ return candidate
156
+ }
157
+
158
+ function registerAsset(absPath, relPath) {
159
+ if (!ASSET_CONTEXT) return
160
+ ASSET_CONTEXT.assets.set(absPath, relPath)
161
+ }
162
+
163
+ function resolveLocalAsset(src, semanticHint = '') {
164
+ if (!src || !ASSET_CONTEXT) return null
165
+ const abs = toAssetAbsPath(src, ASSET_CONTEXT.jsxDir)
166
+ if (!abs) return null
167
+ const fileName = buildAssetOutputName(abs, semanticHint)
168
+ const rel = normalizeAssetPath(path.posix.join(ASSET_CONTEXT.assetDir, fileName))
169
+ const assetRef = resolveAssetReference(rel)
170
+ registerAsset(abs, rel)
171
+ return {
172
+ rel,
173
+ isSvg: fileName.toLowerCase().endsWith('.svg'),
174
+ assetRef,
175
+ }
176
+ }
177
+
178
+ function copyCollectedAssets() {
179
+ if (!ASSET_CONTEXT || !ASSET_CONTEXT.copyAssets) return
180
+ if (!ASSET_CONTEXT.assets.size) return
181
+ for (const [abs, rel] of ASSET_CONTEXT.assets.entries()) {
182
+ const dest = path.resolve(REPO_ROOT, rel)
183
+ ensureDir(path.dirname(dest))
184
+ try {
185
+ fs.copyFileSync(abs, dest)
186
+ } catch {}
187
+ }
188
+ }
189
+
190
+ function ensureDir(dir) {
191
+ fs.mkdirSync(dir, { recursive: true })
192
+ }
193
+
194
+ function semanticTokenFromClassName(className) {
195
+ const token = normalizeSemanticAssetToken(className)
196
+ if (!token || isGenericSemanticToken(token)) return ''
197
+ return token
198
+ }
199
+
200
+ function isBetterSemanticToken(nextToken, currentToken) {
201
+ if (!currentToken) return true
202
+ const currentGeneric = isGenericSemanticToken(currentToken)
203
+ const nextGeneric = isGenericSemanticToken(nextToken)
204
+ if (currentGeneric !== nextGeneric) return !nextGeneric
205
+ if (nextToken.length !== currentToken.length) return nextToken.length < currentToken.length
206
+ return nextToken < currentToken
207
+ }
208
+
209
+ function collectAssetSemanticHints(ast, jsxDir) {
210
+ const semanticHints = new Map()
211
+ traverse(ast, {
212
+ JSXElement(pathNode) {
213
+ const el = pathNode.node
214
+ if (jsxElementName(el).toLowerCase() !== 'img') return
215
+ const src = getAttr(el, 'src') || ''
216
+ const abs = toAssetAbsPath(src, jsxDir)
217
+ if (!abs) return
218
+ const hint = semanticTokenFromClassName(getClassNameAttr(el))
219
+ if (!hint) return
220
+ const existing = semanticHints.get(abs)
221
+ if (!existing || isBetterSemanticToken(hint, existing)) {
222
+ semanticHints.set(abs, hint)
223
+ }
224
+ }
225
+ })
226
+ return semanticHints
227
+ }
228
+
229
+ function toPascalCaseToken(raw) {
230
+ const token = sanitizeAssetToken(raw)
231
+ if (!token) return ''
232
+ return token
233
+ .split('_')
234
+ .filter(Boolean)
235
+ .map(part => part[0].toUpperCase() + part.slice(1))
236
+ .join('')
237
+ }
238
+
239
+ function toWidgetClassName(raw) {
240
+ const normalized = String(raw || '')
241
+ .trim()
242
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
243
+ .replace(/[^A-Za-z0-9]+/g, '_')
244
+ .replace(/^_+|_+$/g, '')
245
+ .toLowerCase()
246
+ if (!normalized) return 'FigmaWidget'
247
+ const className = normalized
248
+ .split('_')
249
+ .filter(Boolean)
250
+ .map(part => part[0].toUpperCase() + part.slice(1))
251
+ .join('')
252
+ if (!className) return 'FigmaWidget'
253
+ return /^[A-Za-z_]/.test(className) ? className : `Figma${className}`
254
+ }
255
+
256
+ function buildAppAssetConstName(assetPath) {
257
+ const normalized = normalizeAssetPath(assetPath).replace(/^assets\//, '')
258
+ const parts = normalized
259
+ .split(/[\/._-]+/)
260
+ .map(part => sanitizeAssetToken(part))
261
+ .filter(Boolean)
262
+ if (!parts.length) return 'assetGenerated'
263
+ const [head, ...tail] = parts
264
+ const headName = /^[a-z]/.test(head) ? head : `asset${toPascalCaseToken(head)}`
265
+ return `${headName}${tail.map(toPascalCaseToken).join('')}`
266
+ }
267
+
268
+ function loadAppAssetsRegistry() {
269
+ if (!fs.existsSync(APP_ASSETS_FILE)) return null
270
+ let content = ''
271
+ try {
272
+ content = fs.readFileSync(APP_ASSETS_FILE, 'utf8')
273
+ } catch {
274
+ return null
275
+ }
276
+ const pathToConst = new Map()
277
+ const constToPath = new Map()
278
+ const regex = /static const String (\w+)\s*=\s*'([^']+)';/g
279
+ let match
280
+ while ((match = regex.exec(content)) !== null) {
281
+ const constName = match[1]
282
+ const assetPath = normalizeAssetPath(match[2])
283
+ constToPath.set(constName, assetPath)
284
+ if (!pathToConst.has(assetPath)) pathToConst.set(assetPath, constName)
285
+ }
286
+ return {
287
+ pathToConst,
288
+ constToPath,
289
+ pendingPathToConst: new Map(),
290
+ pendingConstNames: new Set(),
291
+ }
292
+ }
293
+
294
+ function resolveAppAssetConstName(assetPath) {
295
+ const registry = ASSET_CONTEXT?.appAssetsRegistry
296
+ if (!registry) return null
297
+ const normalized = normalizeAssetPath(assetPath)
298
+ const existing = registry.pathToConst.get(normalized) || registry.pendingPathToConst.get(normalized)
299
+ if (existing) return existing
300
+ const base = buildAppAssetConstName(normalized)
301
+ let candidate = base
302
+ let index = 2
303
+ while (
304
+ registry.constToPath.has(candidate) ||
305
+ registry.pendingConstNames.has(candidate)
306
+ ) {
307
+ candidate = `${base}${index}`
308
+ index += 1
309
+ }
310
+ registry.pathToConst.set(normalized, candidate)
311
+ registry.pendingPathToConst.set(normalized, candidate)
312
+ registry.pendingConstNames.add(candidate)
313
+ return candidate
314
+ }
315
+
316
+ function resolveAssetReference(assetPath) {
317
+ const constName = resolveAppAssetConstName(assetPath)
318
+ if (constName) return `AppAssets.${constName}`
319
+ return toDartLiteral(assetPath)
320
+ }
321
+
322
+ function syncAppAssetsFile() {
323
+ const registry = ASSET_CONTEXT?.appAssetsRegistry
324
+ if (!registry || !registry.pendingPathToConst.size) return
325
+ if (!fs.existsSync(APP_ASSETS_FILE)) return
326
+ let content = ''
327
+ try {
328
+ content = fs.readFileSync(APP_ASSETS_FILE, 'utf8')
329
+ } catch {
330
+ return
331
+ }
332
+ const insertAt = content.lastIndexOf('\n}')
333
+ if (insertAt < 0) return
334
+ const lines = [...registry.pendingPathToConst.entries()]
335
+ .sort((a, b) => a[1].localeCompare(b[1]))
336
+ .map(([assetPath, constName]) => ` static const String ${constName} = '${assetPath}';`)
337
+ if (!lines.length) return
338
+ const block = `\n${lines.join('\n')}\n`
339
+ const updated = `${content.slice(0, insertAt)}${block}${content.slice(insertAt)}`
340
+ fs.writeFileSync(APP_ASSETS_FILE, updated, 'utf8')
341
+ registry.pendingPathToConst.clear()
342
+ registry.pendingConstNames.clear()
343
+ }
344
+
345
+ function readFile(p) {
346
+ return fs.readFileSync(p, 'utf8')
347
+ }
348
+
349
+ function writeFile(p, content) {
350
+ ensureDir(path.dirname(p))
351
+ fs.writeFileSync(p, content, 'utf8')
352
+ }
353
+
354
+ function compileScssIfNeeded(css, cssPath) {
355
+ // If dart-sass is available, use it (most accurate).
356
+ if (!sass) return css
357
+ try {
358
+ const result = sass.compileString(css, {
359
+ style: 'expanded',
360
+ loadPaths: cssPath ? [path.dirname(cssPath)] : []
361
+ })
362
+ return result.css || css
363
+ } catch {
364
+ return css
365
+ }
366
+ }
367
+
368
+ function stripInlineComment(line) {
369
+ // Keep URLs like http://... by only treating '//' as a comment start when it's not in quotes.
370
+ const s = String(line)
371
+ let inSingle = false
372
+ let inDouble = false
373
+ for (let i = 0; i < s.length - 1; i++) {
374
+ const ch = s[i]
375
+ const next = s[i + 1]
376
+ if (ch === "'" && !inDouble) inSingle = !inSingle
377
+ if (ch === '"' && !inSingle) inDouble = !inDouble
378
+ if (!inSingle && !inDouble && ch === '/' && next === '/') {
379
+ return s.slice(0, i)
380
+ }
381
+ }
382
+ return s
383
+ }
384
+
385
+ function splitSelectors(sel) {
386
+ return String(sel)
387
+ .split(',')
388
+ .map(s => s.trim())
389
+ .filter(Boolean)
390
+ }
391
+
392
+ function combineSelectors(parents, current) {
393
+ const cur = splitSelectors(current)
394
+ if (!cur.length) return parents
395
+ const out = []
396
+ for (const p of parents) {
397
+ for (const c of cur) {
398
+ if (!p) {
399
+ out.push(c)
400
+ } else if (c.includes('&')) {
401
+ out.push(c.replace(/&/g, p))
402
+ } else {
403
+ out.push(`${p} ${c}`)
404
+ }
405
+ }
406
+ }
407
+ return out
408
+ }
409
+
410
+ function flattenScssToCss(scss) {
411
+ // Minimal SCSS flattener: handles nested selectors with braces.
412
+ // This is enough for Figma/Trae-generated SCSS modules (no mixins/functions).
413
+ const lines = String(scss).replace(/\r\n/g, '\n').split('\n')
414
+ const stack = [{ selectors: [''], decls: [] }]
415
+ let inBlockComment = false
416
+ const outRules = []
417
+ let pendingDecl = null
418
+
419
+ function flushTop() {
420
+ const top = stack[stack.length - 1]
421
+ if (!top || !top.decls.length) return
422
+ const decls = top.decls.join('\n')
423
+ for (const sel of top.selectors) {
424
+ if (!sel) continue
425
+ outRules.push(`${sel} {\n${decls}\n}`)
426
+ }
427
+ top.decls = []
428
+ }
429
+
430
+ for (let rawLine of lines) {
431
+ let line = String(rawLine)
432
+ // Block comments /* ... */
433
+ if (inBlockComment) {
434
+ const end = line.indexOf('*/')
435
+ if (end >= 0) {
436
+ inBlockComment = false
437
+ line = line.slice(end + 2)
438
+ } else {
439
+ continue
440
+ }
441
+ }
442
+ while (true) {
443
+ const start = line.indexOf('/*')
444
+ if (start < 0) break
445
+ const end = line.indexOf('*/', start + 2)
446
+ if (end >= 0) {
447
+ line = line.slice(0, start) + line.slice(end + 2)
448
+ } else {
449
+ inBlockComment = true
450
+ line = line.slice(0, start)
451
+ break
452
+ }
453
+ }
454
+
455
+ line = stripInlineComment(line).trim()
456
+ if (!line) continue
457
+
458
+ if (pendingDecl) {
459
+ pendingDecl.value = `${pendingDecl.value} ${line}`.trim()
460
+ if (pendingDecl.value.includes(';')) {
461
+ const semicolonIndex = pendingDecl.value.indexOf(';')
462
+ const declValue = pendingDecl.value.slice(0, semicolonIndex + 1).trim()
463
+ stack[stack.length - 1].decls.push(` ${pendingDecl.prop}: ${declValue}`)
464
+ pendingDecl = null
465
+ }
466
+ continue
467
+ }
468
+
469
+ // Handle multiple braces on same line crudely by iterating chars.
470
+ // We avoid full tokenization by treating declarations as whole lines.
471
+ // Common generator output keeps '{' on selector line and '}' on its own line.
472
+ if (line.endsWith('{')) {
473
+ const selector = line.slice(0, -1).trim()
474
+ const parentSelectors = stack[stack.length - 1]?.selectors || ['']
475
+ const selectors = combineSelectors(parentSelectors, selector)
476
+ stack.push({ selectors, decls: [] })
477
+ continue
478
+ }
479
+ if (line === '}') {
480
+ if (pendingDecl) pendingDecl = null
481
+ flushTop()
482
+ stack.pop()
483
+ continue
484
+ }
485
+ // Declaration line (supports multiline values like linear-gradient(...))
486
+ if (line.includes(':')) {
487
+ // Only accept standard declarations (avoid mis-parsing nested selectors)
488
+ const idx = line.indexOf(':')
489
+ const prop = line.slice(0, idx).trim()
490
+ const val = line.slice(idx + 1).trim()
491
+ if (prop && /^[a-zA-Z_-][a-zA-Z0-9_-]*$/.test(prop)) {
492
+ if (val.includes(';')) {
493
+ const semicolonIndex = val.indexOf(';')
494
+ const declValue = val.slice(0, semicolonIndex + 1).trim()
495
+ stack[stack.length - 1].decls.push(` ${prop}: ${declValue}`)
496
+ } else {
497
+ pendingDecl = { prop, value: val }
498
+ }
499
+ }
500
+ continue
501
+ }
502
+ }
503
+
504
+ // Flush any remaining
505
+ while (stack.length > 1) {
506
+ flushTop()
507
+ stack.pop()
508
+ }
509
+ return outRules.join('\n\n')
510
+ }
511
+
512
+ function compileOrFlattenScss(css, cssPath) {
513
+ if (sass) return compileScssIfNeeded(css, cssPath)
514
+ try {
515
+ return flattenScssToCss(css)
516
+ } catch {
517
+ return css
518
+ }
519
+ }
520
+
521
+ function parseCssModules(css, cssPath) {
522
+ const normalizedCss = compileOrFlattenScss(css, cssPath)
523
+ const root = postcss.parse(normalizedCss, { parser: safeParser })
524
+ const map = {}
525
+ root.walkRules(rule => {
526
+ const selector = rule.selector
527
+ if (!selector) return
528
+ const props = {}
529
+ rule.walkDecls(decl => {
530
+ props[decl.prop] = decl.value
531
+ })
532
+ const classes = [...selector.matchAll(/\.([A-Za-z0-9_-]+)/g)].map(m => m[1])
533
+ if (classes.length) {
534
+ const key = classes[classes.length - 1]
535
+ map[key] = { ...(map[key] || {}), ...props }
536
+ }
537
+ })
538
+ return map
539
+ }
540
+
541
+ function cssColorToAppColor(v) {
542
+ const raw = v.trim().toLowerCase()
543
+ // CSS variable tokens mapping -> AppColors
544
+ const varMatch = raw.match(/^var\(\s*--?([a-z0-9\-_]+)\s*(?:,\s*([^)]+))?\)$/i)
545
+ if (varMatch) {
546
+ const token = varMatch[1]
547
+ const tok = token.replace(/[^a-z0-9_]/gi, '').toLowerCase()
548
+ const dict = {
549
+ primary: 'AppColors.primary',
550
+ buttondarkbtndarkcolor: 'AppColors.white',
551
+ text2: 'AppColors.colorB8BCC6',
552
+ tabdisabletext: 'AppColors.colorB7B7B7',
553
+ neutrals200: 'AppColors.colorEAECF0',
554
+ titletext: 'AppColors.color667394',
555
+ bg: 'AppColors.background',
556
+ }
557
+ const mapped = dict[tok]
558
+ if (mapped) return mapped
559
+ const fallback = varMatch[2]
560
+ if (fallback) return cssColorToAppColor(fallback)
561
+ return null
562
+ }
563
+ const hex = raw
564
+ if (/^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(hex)) {
565
+ // Expand short hex (#abc / #rgba) and normalize alpha order
566
+ if (/^#[0-9a-f]{3}$/.test(hex)) {
567
+ const r = hex[1], g = hex[2], b = hex[3]
568
+ return `AppColors.fromHex('#${r}${r}${g}${g}${b}${b}')`
569
+ }
570
+ if (/^#[0-9a-f]{4}$/.test(hex)) {
571
+ const r = hex[1], g = hex[2], b = hex[3], a = hex[4]
572
+ // CSS #RGBA -> Flutter expects #AARRGGBB
573
+ return `AppColors.fromHex('#${a}${a}${r}${r}${g}${g}${b}${b}')`
574
+ }
575
+ if (/^#[0-9a-f]{8}$/.test(hex)) {
576
+ const rr = hex.slice(1, 3)
577
+ const gg = hex.slice(3, 5)
578
+ const bb = hex.slice(5, 7)
579
+ const aa = hex.slice(7, 9)
580
+ return `AppColors.fromHex('#${aa}${rr}${gg}${bb}')`
581
+ }
582
+ return `AppColors.fromHex('${hex}')`
583
+ }
584
+ const rgbMatch = raw.match(/^rgba?\((\s*\d+\s*),(\s*\d+\s*),(\s*\d+\s*)(?:,\s*(\d*\.?\d+)\s*)?\)$/i)
585
+ if (rgbMatch) {
586
+ const r = Math.max(0, Math.min(255, parseInt(rgbMatch[1])))
587
+ const g = Math.max(0, Math.min(255, parseInt(rgbMatch[2])))
588
+ const b = Math.max(0, Math.min(255, parseInt(rgbMatch[3])))
589
+ const a = rgbMatch[4] != null ? Math.max(0, Math.min(1, parseFloat(rgbMatch[4]))) : 1
590
+ const aa = Math.round(a * 255)
591
+ const hexStr = `#${aa.toString(16).padStart(2,'0')}${r.toString(16).padStart(2,'0')}${g.toString(16).padStart(2,'0')}${b.toString(16).padStart(2,'0')}`
592
+ return `AppColors.fromHex('${hexStr}')`
593
+ }
594
+ if (hex === 'white') return 'AppColors.white'
595
+ if (hex === 'black') return 'AppColors.black'
596
+ if (hex === 'transparent') return 'AppColors.transparent'
597
+ return null
598
+ }
599
+
600
+ function extractLinearGradientArgs(str) {
601
+ const source = String(str || '')
602
+ const marker = 'linear-gradient('
603
+ const start = source.toLowerCase().indexOf(marker)
604
+ if (start < 0) return null
605
+
606
+ let depth = 1
607
+ let args = ''
608
+ for (let i = start + marker.length; i < source.length; i += 1) {
609
+ const ch = source[i]
610
+ if (ch === '(') {
611
+ depth += 1
612
+ args += ch
613
+ continue
614
+ }
615
+ if (ch === ')') {
616
+ depth -= 1
617
+ if (depth === 0) break
618
+ args += ch
619
+ continue
620
+ }
621
+ args += ch
622
+ }
623
+ if (depth !== 0) return null
624
+ return args
625
+ }
626
+
627
+ function parseGradientColors(str) {
628
+ const args = extractLinearGradientArgs(str)
629
+ if (!args) return []
630
+ const rawColors = [...args.matchAll(/#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{4}|[0-9a-f]{3})/ig)].map(x => x[0])
631
+ return rawColors.map(c => cssColorToAppColor(c)).filter(Boolean)
632
+ }
633
+
634
+ function parseLinearGradient(str) {
635
+ const args = extractLinearGradientArgs(str)
636
+ if (!args) return null
637
+ const colors = parseGradientColors(str)
638
+ if (!colors.length) return null
639
+ const angleMatch = args.match(/(\d+)\s*deg/i)
640
+ const angle = angleMatch ? parseInt(angleMatch[1]) : 180
641
+ let begin = 'Alignment.topLeft', end = 'Alignment.bottomRight'
642
+ if (angle === 180) { begin = 'Alignment.topCenter'; end = 'Alignment.bottomCenter' }
643
+ if (angle === 90) { begin = 'Alignment.centerLeft'; end = 'Alignment.centerRight' }
644
+ return `LinearGradient(begin: ${begin}, end: ${end}, colors: [${colors.join(', ')}])`
645
+ }
646
+
647
+ function gradientTextFromProps(props) {
648
+ const clip = `${props['background-clip'] || ''} ${props['-webkit-background-clip'] || ''}`.toLowerCase()
649
+ if (!clip.includes('text')) return null
650
+ const gradientSource = props['background-image'] || props['background']
651
+ if (!gradientSource || !/linear-gradient/i.test(String(gradientSource))) return null
652
+ return parseLinearGradient(gradientSource)
653
+ }
654
+
655
+ function cssPxToDouble(v) {
656
+ const m = String(v).match(/([0-9.]+)px/)
657
+ if (m) return parseFloat(m[1])
658
+ const n = parseFloat(v)
659
+ if (!Number.isNaN(n)) return n
660
+ return null
661
+ }
662
+
663
+ function parseBoxValues(v) {
664
+ if (!v) return null
665
+ const cleaned = String(v).split('/')[0]
666
+ const parts = cleaned.trim().split(/\s+/).filter(Boolean)
667
+ const nums = parts.map(cssPxToDouble).filter(n => n != null)
668
+ if (!nums.length) return null
669
+ if (nums.length === 1) return { top: nums[0], right: nums[0], bottom: nums[0], left: nums[0] }
670
+ if (nums.length === 2) return { top: nums[0], right: nums[1], bottom: nums[0], left: nums[1] }
671
+ if (nums.length === 3) return { top: nums[0], right: nums[1], bottom: nums[2], left: nums[1] }
672
+ return { top: nums[0], right: nums[1], bottom: nums[2], left: nums[3] }
673
+ }
674
+
675
+ function fontWeightFromCss(v) {
676
+ const raw = String(v || '').trim().toLowerCase()
677
+ if (raw === 'bold') return 'FontWeight.w700'
678
+ if (raw === 'normal') return 'FontWeight.w400'
679
+ const n = parseInt(raw, 10)
680
+ if (n >= 700) return 'FontWeight.w700'
681
+ if (n >= 600) return 'FontWeight.w600'
682
+ if (n >= 500) return 'FontWeight.w500'
683
+ return 'FontWeight.w400'
684
+ }
685
+
686
+ function fontFamilyFromCss(v) {
687
+ if (!v) return null
688
+ const fontMap = {
689
+ 'zen maru gothic': 'ZenMaruGothic',
690
+ 'noto sans jp': 'ZenMaruGothic',
691
+ 'sf pro text': null,
692
+ 'inter': null,
693
+ }
694
+ const candidates = String(v)
695
+ .split(',')
696
+ .map(s => s.trim().replace(/^['"]|['"]$/g, ''))
697
+ .filter(Boolean)
698
+ for (const name of candidates) {
699
+ const key = name.toLowerCase()
700
+ if (Object.prototype.hasOwnProperty.call(fontMap, key)) {
701
+ const mapped = fontMap[key]
702
+ return mapped ? toDartLiteral(mapped) : null
703
+ }
704
+ if (key.includes('sans-serif') || key.includes('serif') || key.includes('monospace')) continue
705
+ return toDartLiteral(name)
706
+ }
707
+ return null
708
+ }
709
+
710
+ function textStyleFromProps(props) {
711
+ const size = cssPxToDouble(props['font-size'])
712
+ const weight = props['font-weight'] ? fontWeightFromCss(props['font-weight']) : null
713
+ let color = props['color'] ? cssColorToAppColor(props['color']) : null
714
+ if (color === 'AppColors.transparent') {
715
+ // Figma CSS often exports gradient text as:
716
+ // color: transparent; background: linear-gradient(...); background-clip: text.
717
+ // Flutter Text doesn't support CSS background-clip, so fallback to first gradient color.
718
+ const clip = `${props['background-clip'] || ''} ${props['-webkit-background-clip'] || ''}`.toLowerCase()
719
+ if (clip.includes('text')) {
720
+ const gradientSource = props['background-image'] || props['background']
721
+ const gradientColors = parseGradientColors(gradientSource)
722
+ if (gradientColors.length) color = gradientColors[0]
723
+ }
724
+ }
725
+ const lineHeight = cssPxToDouble(props['line-height'])
726
+ const heightRatio = size && lineHeight ? (lineHeight / size) : null
727
+ const fontFamily = fontFamilyFromCss(props['font-family'])
728
+ const letterSpacing = cssPxToDouble(props['letter-spacing'])
729
+ if (!size && !weight && !color && !heightRatio && !fontFamily && letterSpacing == null) return null
730
+ const args = []
731
+ if (fontFamily) args.push(`fontFamily: ${fontFamily}`)
732
+ if (size) args.push(`fontSize: ${size}`)
733
+ if (color) args.push(`color: ${color}`)
734
+ if (weight) args.push(`fontWeight: ${weight}`)
735
+ if (heightRatio) args.push(`height: ${heightRatio}`)
736
+ if (letterSpacing != null) args.push(`letterSpacing: ${letterSpacing}`)
737
+ return `TextStyle(${args.join(', ')})`
738
+ }
739
+
740
+ function textAlignFromProps(props) {
741
+ const align = String(props['text-align'] || '').toLowerCase()
742
+ if (align === 'center') return 'TextAlign.center'
743
+ if (align === 'right' || align === 'end') return 'TextAlign.right'
744
+ if (align === 'left' || align === 'start') return 'TextAlign.left'
745
+ return null
746
+ }
747
+
748
+ function paddingFromProps(props) {
749
+ const p = props['padding']
750
+ const pt = props['padding-top']
751
+ const pr = props['padding-right']
752
+ const pb = props['padding-bottom']
753
+ const pl = props['padding-left']
754
+ if (p) {
755
+ const box = parseBoxValues(p)
756
+ if (box) return `EdgeInsets.fromLTRB(${box.left}, ${box.top}, ${box.right}, ${box.bottom})`
757
+ const v = cssPxToDouble(p)
758
+ if (v !== null) return `EdgeInsets.all(${v})`
759
+ }
760
+ const top = cssPxToDouble(pt) || 0
761
+ const right = cssPxToDouble(pr) || 0
762
+ const bottom = cssPxToDouble(pb) || 0
763
+ const left = cssPxToDouble(pl) || 0
764
+ if (top || right || bottom || left) return `EdgeInsets.fromLTRB(${left}, ${top}, ${right}, ${bottom})`
765
+ return null
766
+ }
767
+
768
+ function borderRadiusFromProps(props) {
769
+ const v = props['border-radius']
770
+ const box = parseBoxValues(v)
771
+ if (box) {
772
+ if (box.top === box.right && box.top === box.bottom && box.top === box.left) {
773
+ return `BorderRadius.circular(${box.top})`
774
+ }
775
+ return `BorderRadius.only(topLeft: Radius.circular(${box.top}), topRight: Radius.circular(${box.right}), bottomRight: Radius.circular(${box.bottom}), bottomLeft: Radius.circular(${box.left}))`
776
+ }
777
+ const n = cssPxToDouble(v)
778
+ if (n !== null) return `BorderRadius.circular(${n})`
779
+ return null
780
+ }
781
+
782
+ function marginFromProps(props) {
783
+ const m = props['margin']
784
+ const mt = props['margin-top']
785
+ const mr = props['margin-right']
786
+ const mb = props['margin-bottom']
787
+ const ml = props['margin-left']
788
+ if (m) {
789
+ const box = parseBoxValues(m)
790
+ if (box) return `EdgeInsets.fromLTRB(${box.left}, ${box.top}, ${box.right}, ${box.bottom})`
791
+ const v = cssPxToDouble(m)
792
+ if (v !== null) return `EdgeInsets.all(${v})`
793
+ }
794
+ const top = cssPxToDouble(mt) || 0
795
+ const right = cssPxToDouble(mr) || 0
796
+ const bottom = cssPxToDouble(mb) || 0
797
+ const left = cssPxToDouble(ml) || 0
798
+ if (top || right || bottom || left) return `EdgeInsets.fromLTRB(${left}, ${top}, ${right}, ${bottom})`
799
+ return null
800
+ }
801
+
802
+ function borderFromProps(props) {
803
+ const border = props['border']
804
+ const borderWidth = props['border-width']
805
+ const borderColor = props['border-color']
806
+ let width = borderWidth ? cssPxToDouble(borderWidth) : null
807
+ let color = borderColor ? cssColorToAppColor(borderColor) : null
808
+ if (border) {
809
+ if (width == null) {
810
+ const m = String(border).match(/(-?\d*\.?\d+)px/)
811
+ if (m) width = parseFloat(m[1])
812
+ }
813
+ if (!color) {
814
+ const colorMatch = String(border).match(/(rgba?\([^)]*\)|#[0-9a-f]{3,8}|var\([^)]*\))/i)
815
+ if (colorMatch) color = cssColorToAppColor(colorMatch[1])
816
+ }
817
+ }
818
+ if (width == null && !color) return null
819
+ if (width == null) width = 1
820
+ if (!color) color = 'AppColors.black'
821
+ return `Border.all(color: ${color}, width: ${width})`
822
+ }
823
+
824
+ function borderColorFromProps(props) {
825
+ if (!props) return null
826
+ const border = props['border']
827
+ const borderColor = props['border-color']
828
+ let color = borderColor ? cssColorToAppColor(borderColor) : null
829
+ if (!color && border) {
830
+ const colorMatch = String(border).match(/(rgba?\([^)]*\)|#[0-9a-f]{3,8}|var\([^)]*\))/i)
831
+ if (colorMatch) color = cssColorToAppColor(colorMatch[1])
832
+ }
833
+ return color
834
+ }
835
+
836
+ function boxShadowFromProps(props) {
837
+ const shadow = props['box-shadow']
838
+ if (!shadow) return null
839
+ const parts = String(shadow).split(/,(?![^(]*\))/).map(s => s.trim()).filter(Boolean)
840
+ const shadows = []
841
+ for (const part of parts) {
842
+ const colorMatch = part.match(/(rgba?\([^)]*\)|#[0-9a-f]{3,8}|var\([^)]*\))/i)
843
+ const color = colorMatch ? cssColorToAppColor(colorMatch[1]) : null
844
+ const numbers = part.replace(colorMatch ? colorMatch[1] : '', '').trim().split(/\s+/).filter(Boolean)
845
+ const ox = cssPxToDouble(numbers[0]) ?? 0
846
+ const oy = cssPxToDouble(numbers[1]) ?? 0
847
+ const blur = cssPxToDouble(numbers[2]) ?? 0
848
+ const spread = cssPxToDouble(numbers[3]) ?? 0
849
+ if (!color) continue
850
+ shadows.push(`BoxShadow(offset: Offset(${ox}, ${oy}), blurRadius: ${blur}, spreadRadius: ${spread}, color: ${color})`)
851
+ }
852
+ if (!shadows.length) return null
853
+ return shadows.join(', ')
854
+ }
855
+
856
+ function boxDecorationFromProps(props, options = {}) {
857
+ const { omitBackgroundColor = false } = options
858
+ const bg = props['background'] || props['background-color']
859
+ const bgImg = props['background-image'] || props['backgroundImage']
860
+ const bgGradientSource = bg && /linear-gradient/i.test(bg) ? bg : null
861
+ const gradientSource = bgImg || bgGradientSource
862
+ const gradient = gradientSource && /linear-gradient/i.test(gradientSource)
863
+ ? (parseLinearGradient(gradientSource) || 'AppColors.primaryBackgroundGradient()')
864
+ : null
865
+ const color = bg && !bgGradientSource ? cssColorToAppColor(bg) : null
866
+ const radius = borderRadiusFromProps(props)
867
+ const border = borderFromProps(props)
868
+ const shadows = boxShadowFromProps(props)
869
+ const parts = []
870
+ if (color && !omitBackgroundColor) parts.push(`color: ${color}`)
871
+ if (gradient) parts.push(`gradient: ${gradient}`)
872
+ if (radius) parts.push(`borderRadius: ${radius}`)
873
+ if (border) parts.push(`border: ${border}`)
874
+ if (shadows) parts.push(`boxShadow: [${shadows}]`)
875
+ return parts.length ? `BoxDecoration(${parts.join(', ')})` : null
876
+ }
877
+
878
+ function usesAppOwnedInputBackground(widget) {
879
+ if (!widget) return false
880
+ const normalized = String(widget).trim()
881
+ return normalized.startsWith('AppInput(') || normalized.startsWith('_GeneratedDateTimeField(')
882
+ }
883
+
884
+ function usesAppOwnedProgressBarBackground(widget) {
885
+ if (!widget) return false
886
+ const normalized = String(widget).trim()
887
+ return normalized.startsWith('AppProgressBar(')
888
+ }
889
+
890
+ function toDartLiteral(s) {
891
+ const t = String(s || '')
892
+ return `'${t.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
893
+ }
894
+
895
+ function isTextNode(node) {
896
+ return node.type === 'JSXText'
897
+ }
898
+
899
+ function jsxElementName(el) {
900
+ const n = el.openingElement.name
901
+ if (n.type === 'JSXIdentifier') return n.name
902
+ return 'div'
903
+ }
904
+
905
+ function getClassNameAttr(el) {
906
+ const attrs = el.openingElement.attributes || []
907
+ for (const a of attrs) {
908
+ if (a.type === 'JSXAttribute' && a.name && a.name.name === 'className') {
909
+ if (a.value && a.value.type === 'StringLiteral') return a.value.value
910
+ if (a.value && a.value.type === 'JSXExpressionContainer') {
911
+ const expr = a.value.expression
912
+ if (expr.type === 'StringLiteral') return expr.value
913
+ if (expr.type === 'MemberExpression') {
914
+ const obj = expr.object
915
+ const prop = expr.property
916
+ if ((obj.type === 'Identifier' && obj.name === 'styles') && prop.type === 'Identifier') {
917
+ return prop.name
918
+ }
919
+ }
920
+ }
921
+ }
922
+ }
923
+ return null
924
+ }
925
+
926
+ function assetForClassName(className) {
927
+ if (!className) return null
928
+ if (ICONS_MAP[className]) return ICONS_MAP[className]
929
+ const n = className.toLowerCase()
930
+ const map = [
931
+ { key: 'calendar', asset: 'AppAssets.iconsCalendarSvg' },
932
+ { key: 'search', asset: 'AppAssets.iconsSearchSvg' },
933
+ { key: 'user', asset: 'AppAssets.iconsUserSvg' },
934
+ { key: 'key', asset: 'AppAssets.iconsUserKeySvg' },
935
+ { key: 'family', asset: 'AppAssets.iconsFamilySvg' },
936
+ { key: 'phone', asset: 'AppAssets.iconsPhoneSvg' },
937
+ { key: 'location', asset: 'AppAssets.iconsLocationSvg' },
938
+ { key: 'battery', asset: 'AppAssets.iconsBatterySvg' },
939
+ { key: 'solar', asset: 'AppAssets.iconsSolarSvg' },
940
+ { key: 'water', asset: 'AppAssets.iconsWaterSystemSvg' },
941
+ { key: 'home', asset: 'AppAssets.iconsHomeSvg' },
942
+ { key: 'notification', asset: 'AppAssets.iconsNotificationSvg' },
943
+ { key: 'percent', asset: 'AppAssets.iconsPercentSvg' },
944
+ { key: 'plus', asset: 'AppAssets.iconsPlusSvg' },
945
+ ]
946
+ for (const m of map) {
947
+ if (n.includes(m.key)) return m.asset
948
+ }
949
+ return null
950
+ }
951
+
952
+ function sizeFromProps(props) {
953
+ const w = cssPxToDouble(props['width'])
954
+ const h = cssPxToDouble(props['height'])
955
+ return { w, h }
956
+ }
957
+
958
+ function positionFromProps(props) {
959
+ const pos = (props['position'] || '').toLowerCase()
960
+ if (pos !== 'absolute') return null
961
+ const top = cssPxToDouble(props['top'])
962
+ const left = cssPxToDouble(props['left'])
963
+ const right = cssPxToDouble(props['right'])
964
+ const bottom = cssPxToDouble(props['bottom'])
965
+ if (top == null && left == null && right == null && bottom == null) return null
966
+ return { top, left, right, bottom }
967
+ }
968
+
969
+ function zIndexFromProps(props) {
970
+ const raw = props['z-index']
971
+ const n = raw == null ? NaN : parseInt(raw, 10)
972
+ return Number.isFinite(n) ? n : 0
973
+ }
974
+
975
+ function flexGrowFromProps(props) {
976
+ const raw = props['flex-grow'] ?? props['flex']
977
+ if (raw == null) return 0
978
+ const m = String(raw).trim().match(/-?\d+(\.\d+)?/)
979
+ if (!m) return 0
980
+ const n = parseFloat(m[0])
981
+ return Number.isFinite(n) ? n : 0
982
+ }
983
+
984
+ function rotationFromProps(props) {
985
+ const raw = props['rotate'] || props['transform']
986
+ if (!raw) return null
987
+ const m = String(raw).match(/(-?\d+(\.\d+)?)deg/i)
988
+ if (!m) return null
989
+ const deg = parseFloat(m[1])
990
+ if (!Number.isFinite(deg)) return null
991
+ return deg * Math.PI / 180
992
+ }
993
+
994
+ function estimateStackSize(absKids) {
995
+ let maxRight = 0
996
+ let maxBottom = 0
997
+ for (const child of absKids) {
998
+ const pos = child.position || {}
999
+ const w = child.size?.w ?? 0
1000
+ const h = child.size?.h ?? 0
1001
+ const left = pos.left ?? 0
1002
+ const top = pos.top ?? 0
1003
+ const rightEdge = (pos.left != null ? left : (pos.right != null ? pos.right : 0)) + w
1004
+ const bottomEdge = (pos.top != null ? top : (pos.bottom != null ? pos.bottom : 0)) + h
1005
+ if (rightEdge > maxRight) maxRight = rightEdge
1006
+ if (bottomEdge > maxBottom) maxBottom = bottomEdge
1007
+ }
1008
+ const width = maxRight > 0 ? maxRight : null
1009
+ const height = maxBottom > 0 ? maxBottom : null
1010
+ if (width == null && height == null) return null
1011
+ return { w: width, h: height }
1012
+ }
1013
+
1014
+ function flexConfigFromProps(props) {
1015
+ if ((props['display'] || '').includes('flex')) {
1016
+ // CSS default is `row` when `flex-direction` is omitted.
1017
+ const dir = (props['flex-direction'] || 'row').toLowerCase()
1018
+ const align = (props['align-items'] || '').toLowerCase()
1019
+ const justify = (props['justify-content'] || '').toLowerCase()
1020
+ const gapCol = cssPxToDouble(props['column-gap'])
1021
+ const gapRow = cssPxToDouble(props['row-gap'])
1022
+ const isRow = dir.includes('row')
1023
+ const main = (() => {
1024
+ switch (justify) {
1025
+ case 'center': return 'MainAxisAlignment.center'
1026
+ case 'space-between': return 'MainAxisAlignment.spaceBetween'
1027
+ case 'space-around': return 'MainAxisAlignment.spaceAround'
1028
+ case 'flex-end': return 'MainAxisAlignment.end'
1029
+ default: return 'MainAxisAlignment.start'
1030
+ }
1031
+ })()
1032
+ const cross = (() => {
1033
+ switch (align) {
1034
+ case 'center': return 'CrossAxisAlignment.center'
1035
+ case 'flex-end': return 'CrossAxisAlignment.end'
1036
+ case 'stretch': return 'CrossAxisAlignment.stretch'
1037
+ default: return 'CrossAxisAlignment.start'
1038
+ }
1039
+ })()
1040
+ return { isRow, main, cross, gap: isRow ? gapCol : gapRow }
1041
+ }
1042
+ return null
1043
+ }
1044
+
1045
+ function getAttr(el, name) {
1046
+ const attrs = el.openingElement.attributes || []
1047
+ for (const a of attrs) {
1048
+ if (a.type === 'JSXAttribute' && a.name && a.name.name === name) {
1049
+ if (a.value && a.value.type === 'StringLiteral') return a.value.value
1050
+ }
1051
+ }
1052
+ return null
1053
+ }
1054
+
1055
+ function normalizeTextValue(raw) {
1056
+ return String(raw || '').replace(/\s+/g, ' ').trim()
1057
+ }
1058
+
1059
+ function extractTextsFromElement(el, out = []) {
1060
+ if (!el || !el.children) return out
1061
+ for (const child of el.children) {
1062
+ if (child.type === 'JSXText') {
1063
+ const text = normalizeTextValue(child.value)
1064
+ if (text) out.push(text)
1065
+ continue
1066
+ }
1067
+ if (child.type === 'JSXExpressionContainer' && child.expression?.type === 'StringLiteral') {
1068
+ const text = normalizeTextValue(child.expression.value)
1069
+ if (text) out.push(text)
1070
+ continue
1071
+ }
1072
+ if (child.type === 'JSXElement') {
1073
+ extractTextsFromElement(child, out)
1074
+ }
1075
+ }
1076
+ return out
1077
+ }
1078
+
1079
+ function uniqueTexts(texts) {
1080
+ const result = []
1081
+ const seen = new Set()
1082
+ for (const t of texts) {
1083
+ const key = normalizeTextValue(t)
1084
+ if (!key || seen.has(key)) continue
1085
+ seen.add(key)
1086
+ result.push(key)
1087
+ }
1088
+ return result
1089
+ }
1090
+
1091
+ function isHintLikeText(text) {
1092
+ const raw = normalizeTextValue(text)
1093
+ if (!raw) return false
1094
+ if (/^\d[\d\s./:-]*$/.test(raw)) return true
1095
+ if (/^(yyyy|yy|mm|dd)/i.test(raw)) return true
1096
+ if (raw.includes('YYYY') || raw.includes('MM') || raw.includes('DD')) return true
1097
+ if (raw.length >= 24) return true
1098
+ return false
1099
+ }
1100
+
1101
+ function deriveInputLabelHint(el) {
1102
+ const texts = uniqueTexts(extractTextsFromElement(el, []))
1103
+ if (!texts.length) return { label: null, hint: null }
1104
+ const labelCandidate = texts.find(t => !isHintLikeText(t)) || texts[0]
1105
+ const hintCandidate = texts.find(t => t !== labelCandidate) || null
1106
+ return { label: labelCandidate || null, hint: hintCandidate || null }
1107
+ }
1108
+
1109
+ function looksLikeDateText(rawValue) {
1110
+ const raw = normalizeTextValue(rawValue)
1111
+ if (!raw) return false
1112
+ const lower = raw.toLowerCase()
1113
+ if (raw.includes('生年')) return true
1114
+ if (raw.includes('生年月日')) return true
1115
+ if (raw.includes('誕生日')) return true
1116
+ if (raw.includes('出生')) return true
1117
+ if (raw.includes('設置年')) return true
1118
+ if (raw.includes('施工年')) return true
1119
+ if (raw.includes('実施年')) return true
1120
+ if (raw.includes('年度')) return true
1121
+ if (/\bdob\b/.test(lower)) return true
1122
+ if (lower.includes('birth')) return true
1123
+ if (/(yyyy|yy).*(mm).*(dd)/i.test(raw)) return true
1124
+ if (/\b\d{4}[\/.-]\d{1,2}[\/.-]\d{1,2}\b/.test(raw)) return true
1125
+ if (/\d{4}年\d{1,2}月\d{1,2}日/.test(raw)) return true
1126
+ if (raw.includes('年月日')) return true
1127
+ if (raw.includes('日付')) return true
1128
+ if (/\bdate\b/.test(lower)) return true
1129
+ if (raw.includes('カレンダー')) return true
1130
+ if (lower.includes('calendar')) return true
1131
+ if (raw.includes('日程')) return true
1132
+ const hasYear = raw.includes('年')
1133
+ const hasMonth = raw.includes('月')
1134
+ const hasDay = raw.includes('日')
1135
+ if (hasYear && hasMonth && hasDay) return true
1136
+ if (hasMonth && hasDay) return true
1137
+ return false
1138
+ }
1139
+
1140
+ function looksLikeDateField({ className, inputType, label, hint }) {
1141
+ if (inputType === 'date' || inputType === 'datetime-local' || inputType === 'month') return true
1142
+ return [className, label, hint].some(value => looksLikeDateText(value))
1143
+ }
1144
+
1145
+ function extractSvgAssetCall(widget) {
1146
+ if (!widget) return null
1147
+ const match = String(widget).match(/SvgPicture\.asset\([^)]*\)/)
1148
+ return match ? match[0] : null
1149
+ }
1150
+
1151
+ function isSmallIconSize(size) {
1152
+ if (!size) return false
1153
+ const hasWidth = size.w != null
1154
+ const hasHeight = size.h != null
1155
+ if (!hasWidth && !hasHeight) return false
1156
+ const widthOk = !hasWidth || size.w <= 128
1157
+ const heightOk = !hasHeight || size.h <= 128
1158
+ return widthOk && heightOk
1159
+ }
1160
+
1161
+ function normalizeGeneratedSize(size) {
1162
+ if (!size) return size
1163
+ if (isSmallIconSize(size)) return size
1164
+ if (size.h != null && size.h > 1200) {
1165
+ return { ...size, h: null }
1166
+ }
1167
+ return size
1168
+ }
1169
+
1170
+ function simplifySvgWidget(widget, size) {
1171
+ const svgCall = extractSvgAssetCall(widget)
1172
+ if (!svgCall) return null
1173
+ if (!size || (size.w == null && size.h == null)) return svgCall
1174
+ const width = size.w != null ? `width: ${size.w}, ` : ''
1175
+ const height = size.h != null ? `height: ${size.h}, ` : ''
1176
+ return `SizedBox(${width}${height}child: ${svgCall})`
1177
+ }
1178
+
1179
+ function shouldStripIconPositioning(size, position, rotate) {
1180
+ return (position != null || rotate != null) && isSmallIconSize(size)
1181
+ }
1182
+
1183
+ function looksLikeInputClass(className) {
1184
+ if (!className) return false
1185
+ const normalized = String(className).toLowerCase()
1186
+ return normalized.includes('textfield')
1187
+ || normalized.includes('input')
1188
+ || normalized.includes('textarea')
1189
+ || normalized.includes('formfield')
1190
+ || normalized.includes('form_field')
1191
+ }
1192
+
1193
+ function looksLikeButtonClass(className) {
1194
+ if (!className) return false
1195
+ const normalized = String(className).toLowerCase()
1196
+ return normalized.includes('nextbutton')
1197
+ || /(^|[-_])button([0-9]|$|[-_])/.test(normalized)
1198
+ || normalized.endsWith('btn')
1199
+ || normalized.includes('btn_')
1200
+ }
1201
+
1202
+ function isCheckboxClassName(className) {
1203
+ if (!className) return false
1204
+ const normalized = String(className).toLowerCase()
1205
+ return normalized.includes('checkbox') || normalized.includes('check_box')
1206
+ }
1207
+
1208
+ function isUncheckedCheckboxClass(className) {
1209
+ if (!className) return false
1210
+ const normalized = String(className).toLowerCase()
1211
+ return normalized.includes('checkbox2')
1212
+ || normalized.includes('unchecked')
1213
+ || normalized.includes('uncheck')
1214
+ }
1215
+
1216
+ function buildCheckboxWidgetFromElement(el, cssMap) {
1217
+ if (!el || !Array.isArray(el.children)) return null
1218
+ const directElements = el.children.filter(c => c.type === 'JSXElement')
1219
+ if (directElements.length < 2 || directElements.length > 3) return null
1220
+ const checkboxNode = directElements.find(child => isCheckboxClassName(getClassNameAttr(child)))
1221
+ if (!checkboxNode) return null
1222
+ const label = uniqueTexts(extractTextsFromElement(el, [])).find(Boolean)
1223
+ if (!label) return null
1224
+ const checkboxClass = getClassNameAttr(checkboxNode)
1225
+ const initialChecked = !isUncheckedCheckboxClass(checkboxClass)
1226
+ const checkboxProps = (checkboxClass && cssMap && cssMap[checkboxClass]) ? cssMap[checkboxClass] : {}
1227
+ const borderColor = borderColorFromProps(checkboxProps)
1228
+ const widgetArgs = [
1229
+ `title: ${toDartLiteral(label)}`,
1230
+ `initialChecked: ${initialChecked ? 'true' : 'false'}`
1231
+ ]
1232
+ if (borderColor) widgetArgs.push(`borderColor: ${borderColor}`)
1233
+ return `_GeneratedCheckbox(${widgetArgs.join(', ')})`
1234
+ }
1235
+
1236
+ function isRadioClassName(className) {
1237
+ if (!className) return false
1238
+ const normalized = String(className).toLowerCase()
1239
+ return /^radio\d*$/.test(normalized)
1240
+ || /(^|[-_])radio\d*($|[-_])/.test(normalized)
1241
+ || normalized.includes('radiobutton')
1242
+ || normalized.includes('radio_button')
1243
+ || normalized.includes('radio-option')
1244
+ || normalized.includes('radiooption')
1245
+ }
1246
+
1247
+ function findRadioClassInTree(el) {
1248
+ if (!el || el.type !== 'JSXElement') return null
1249
+ const cls = getClassNameAttr(el)
1250
+ if (isRadioClassName(cls)) return cls
1251
+ for (const child of el.children || []) {
1252
+ if (child.type !== 'JSXElement') continue
1253
+ const match = findRadioClassInTree(child)
1254
+ if (match) return match
1255
+ }
1256
+ return null
1257
+ }
1258
+
1259
+ function extractRadioOptionFromElement(el, cssMap) {
1260
+ if (!el || el.type !== 'JSXElement') return null
1261
+ const radioClass = findRadioClassInTree(el)
1262
+ if (!radioClass) return null
1263
+ const texts = uniqueTexts(extractTextsFromElement(el, []))
1264
+ const label = texts.find(Boolean)
1265
+ if (!label) return null
1266
+ const radioProps = (radioClass && cssMap && cssMap[radioClass]) ? cssMap[radioClass] : {}
1267
+ const borderColor = borderColorFromProps(radioProps)
1268
+ const normalizedRadio = radioClass.toLowerCase()
1269
+ const selected = normalizedRadio === 'radio2' || normalizedRadio === 'radio3' || normalizedRadio.includes('checked')
1270
+ return { label, selected, borderColor }
1271
+ }
1272
+
1273
+ function buildRadioGroupWidgetFromElement(el, cssMap) {
1274
+ if (!el || !Array.isArray(el.children)) return null
1275
+ const directElements = el.children.filter(c => c.type === 'JSXElement')
1276
+ if (directElements.length < 2) return null
1277
+ const options = []
1278
+ for (const child of directElements) {
1279
+ const option = extractRadioOptionFromElement(child, cssMap)
1280
+ if (!option) return null
1281
+ options.push(option)
1282
+ }
1283
+ if (options.length < 2) return null
1284
+ const optionLiterals = options.map(option => {
1285
+ const valueLiteral = toDartLiteral(option.label)
1286
+ const labelLiteral = toDartLiteral(option.label)
1287
+ return `AppRadioOption(value: ${valueLiteral}, label: ${labelLiteral})`
1288
+ })
1289
+ const selected = options.find(o => o.selected) || options[0]
1290
+ const inactive = options.find(o => !o.selected && o.borderColor)
1291
+ const widgetArgs = [
1292
+ `initialValue: ${toDartLiteral(selected.label)}`,
1293
+ `options: [${optionLiterals.join(', ')}]`
1294
+ ]
1295
+ if (selected.borderColor) widgetArgs.push(`activeColor: ${selected.borderColor}`)
1296
+ if (inactive?.borderColor) widgetArgs.push(`inactiveColor: ${inactive.borderColor}`)
1297
+ return `_GeneratedRadioGroup(${widgetArgs.join(', ')})`
1298
+ }
1299
+
1300
+ function roundNumber(value, digits = 4) {
1301
+ const factor = 10 ** digits
1302
+ return Math.round(value * factor) / factor
1303
+ }
1304
+
1305
+ function parsePaddingValues(props) {
1306
+ const fromPadding = parseBoxValues(props['padding'])
1307
+ if (fromPadding) return fromPadding
1308
+ return {
1309
+ top: cssPxToDouble(props['padding-top']) || 0,
1310
+ right: cssPxToDouble(props['padding-right']) || 0,
1311
+ bottom: cssPxToDouble(props['padding-bottom']) || 0,
1312
+ left: cssPxToDouble(props['padding-left']) || 0,
1313
+ }
1314
+ }
1315
+
1316
+ function solidColorFromBackground(props) {
1317
+ const bg = props['background-color'] || props['background']
1318
+ if (!bg || /linear-gradient/i.test(String(bg))) return null
1319
+ return cssColorToAppColor(bg)
1320
+ }
1321
+
1322
+ function gradientOrSolidFromBackground(props) {
1323
+ const bgSource = props['background-image'] || props['background']
1324
+ if (bgSource && /linear-gradient/i.test(String(bgSource))) {
1325
+ const gradient = parseLinearGradient(bgSource)
1326
+ if (gradient) return gradient
1327
+ }
1328
+ const solid = solidColorFromBackground(props)
1329
+ if (solid) return `LinearGradient(colors: [${solid}, ${solid}])`
1330
+ return null
1331
+ }
1332
+
1333
+ function buildProgressBarWidgetFromElement(el, cssMap) {
1334
+ if (!el || !Array.isArray(el.children)) return null
1335
+ const directElements = el.children.filter(c => c.type === 'JSXElement')
1336
+ if (directElements.length !== 1) return null
1337
+
1338
+ const parentClass = getClassNameAttr(el)
1339
+ const child = directElements[0]
1340
+ const childClass = getClassNameAttr(child)
1341
+ if (!parentClass || !childClass) return null
1342
+
1343
+ const parentProps = (cssMap && cssMap[parentClass]) ? cssMap[parentClass] : {}
1344
+ const childProps = (cssMap && cssMap[childClass]) ? cssMap[childClass] : {}
1345
+
1346
+ const parentSize = normalizeGeneratedSize(sizeFromProps(parentProps))
1347
+ const childSize = normalizeGeneratedSize(sizeFromProps(childProps))
1348
+ if (childSize.w == null || childSize.h == null) return null
1349
+
1350
+ const hasProgressLikeBackground = !!solidColorFromBackground(parentProps)
1351
+ const fillGradient = gradientOrSolidFromBackground(childProps)
1352
+ if (!hasProgressLikeBackground || !fillGradient) return null
1353
+
1354
+ const padding = parsePaddingValues(parentProps)
1355
+ let totalWidth = parentSize.w
1356
+ if (totalWidth == null) {
1357
+ const inferred = childSize.w + (padding.left || 0) + (padding.right || 0)
1358
+ if (inferred > childSize.w) totalWidth = inferred
1359
+ }
1360
+ if (totalWidth == null || totalWidth <= 0) return null
1361
+
1362
+ let value = childSize.w / totalWidth
1363
+ if (!Number.isFinite(value)) return null
1364
+ if (value <= 0 || value > 1.05) return null
1365
+ value = Math.max(0, Math.min(1, value))
1366
+
1367
+ const height = parentSize.h ?? childSize.h
1368
+ if (height == null || height <= 0) return null
1369
+
1370
+ const backgroundColor = solidColorFromBackground(parentProps)
1371
+ const radius = borderRadiusFromProps(parentProps) || borderRadiusFromProps(childProps)
1372
+ const args = [
1373
+ `value: ${roundNumber(value)}`,
1374
+ `height: ${roundNumber(height, 2)}`,
1375
+ `width: ${roundNumber(totalWidth, 2)}`,
1376
+ `progressGradient: ${fillGradient}`,
1377
+ 'padding: 0',
1378
+ 'showDot: false',
1379
+ ]
1380
+ if (backgroundColor) args.push(`backgroundColor: ${backgroundColor}`)
1381
+ if (radius) args.push(`borderRadius: ${radius}`)
1382
+
1383
+ return `AppProgressBar(${args.join(', ')})`
1384
+ }
1385
+
1386
+ function wrapOverflowIfSized(body, size) {
1387
+ if (process.env.JSX2FLUTTER_ENABLE_OVERFLOW !== '1') return body
1388
+ if (!size || (!size.w && !size.h)) return body
1389
+ const trimmed = body.trim()
1390
+ const isColumn = trimmed.startsWith('Column(')
1391
+ const isRow = trimmed.startsWith('Row(')
1392
+ if (!isColumn && !isRow) return body
1393
+ if (/\b(?:Expanded|Flexible|Spacer)\s*\(/.test(trimmed)) return body
1394
+ if (trimmed.includes('SvgPicture.asset(')) return body
1395
+
1396
+ // Only allow overflow on the main axis of the flex container:
1397
+ // - Column: vertical overflow
1398
+ // - Row: horizontal overflow
1399
+ //
1400
+ // To keep constraints valid, only apply overflow when the cross-axis size is known.
1401
+ // Otherwise, OverflowBox defaults can make the cross-axis unbounded and produce NaN during paint.
1402
+ const overflowWidth = isRow && size.w != null && size.h != null
1403
+ const overflowHeight = isColumn && size.h != null && size.w != null
1404
+ if (!overflowWidth && !overflowHeight) return body
1405
+
1406
+ const overflowArgs = [
1407
+ 'alignment: Alignment.topLeft',
1408
+ 'minWidth: 0',
1409
+ 'minHeight: 0',
1410
+ ]
1411
+ overflowArgs.push(`maxWidth: ${overflowWidth ? 'double.infinity' : size.w}`)
1412
+ overflowArgs.push(`maxHeight: ${overflowHeight ? 'double.infinity' : size.h}`)
1413
+ return `OverflowBox(${overflowArgs.join(', ')}, child: ${body})`
1414
+ }
1415
+
1416
+ function buildGeneratedRippleButton(titleExpr) {
1417
+ return `RippleButton(title: ${titleExpr}, backgroundColor: Colors.transparent, minWidth: 0, padding: EdgeInsets.zero, onTap: () {})`
1418
+ }
1419
+
1420
+ function buildWidgetFromElement(el, cssMap, topClassName, skipRootDecoration) {
1421
+ const tag = jsxElementName(el).toLowerCase()
1422
+ const className = getClassNameAttr(el)
1423
+ const props = className && cssMap[className] ? cssMap[className] : {}
1424
+ let size = sizeFromProps(props)
1425
+ size = normalizeGeneratedSize(size)
1426
+ const position = positionFromProps(props)
1427
+ const zIndex = zIndexFromProps(props)
1428
+ const flexGrow = flexGrowFromProps(props)
1429
+ const rotate = rotationFromProps(props)
1430
+ const overflowHidden = (props['overflow'] || '').toLowerCase() === 'hidden'
1431
+ const inputType = (getAttr(el, 'type') || '').toLowerCase()
1432
+ const inputPlaceholder = getAttr(el, 'placeholder')
1433
+ const inputLabel = getAttr(el, 'label')
1434
+ const inputValue = getAttr(el, 'value')
1435
+ let semanticWidget = null
1436
+ if (tag === 'input' || tag === 'textarea' || (tag === 'div' && looksLikeInputClass(className))) {
1437
+ if (inputType === 'radio') {
1438
+ const radioLabel = inputLabel || inputValue || className || 'Option'
1439
+ semanticWidget = `_GeneratedRadioGroup(initialValue: ${toDartLiteral(radioLabel)}, options: [AppRadioOption(value: ${toDartLiteral(radioLabel)}, label: ${toDartLiteral(radioLabel)})])`
1440
+ } else {
1441
+ const derived = deriveInputLabelHint(el)
1442
+ const finalLabel = inputLabel || (tag === 'textarea' ? (derived.label || className || '内容') : derived.label)
1443
+ const finalHint = inputPlaceholder || inputValue || derived.hint
1444
+ const isDateField = tag !== 'textarea' && looksLikeDateField({
1445
+ className,
1446
+ inputType,
1447
+ label: finalLabel,
1448
+ hint: finalHint
1449
+ })
1450
+ const widgetArgs = []
1451
+ if (finalLabel) widgetArgs.push(`label: ${toDartLiteral(finalLabel)}`)
1452
+ if (finalHint) widgetArgs.push(`hint: ${toDartLiteral(finalHint)}`)
1453
+ if (isDateField) {
1454
+ semanticWidget = `_GeneratedDateTimeField(${widgetArgs.join(', ')})`
1455
+ } else {
1456
+ const maxLines = tag === 'textarea' ? 4 : 1
1457
+ if (maxLines > 1) widgetArgs.push(`maxLines: ${maxLines}`)
1458
+ semanticWidget = `AppInput(${widgetArgs.join(', ')})`
1459
+ }
1460
+ }
1461
+ } else if (tag === 'div') {
1462
+ const checkboxWidget = buildCheckboxWidgetFromElement(el, cssMap)
1463
+ if (checkboxWidget) {
1464
+ semanticWidget = checkboxWidget
1465
+ }
1466
+ if (!semanticWidget) {
1467
+ const radioGroupWidget = buildRadioGroupWidgetFromElement(el, cssMap)
1468
+ if (radioGroupWidget) {
1469
+ semanticWidget = radioGroupWidget
1470
+ }
1471
+ }
1472
+ if (!semanticWidget) {
1473
+ const progressBarWidget = buildProgressBarWidgetFromElement(el, cssMap)
1474
+ if (progressBarWidget) {
1475
+ semanticWidget = progressBarWidget
1476
+ }
1477
+ }
1478
+ if (!semanticWidget && looksLikeButtonClass(className)) {
1479
+ const buttonLabel = uniqueTexts(extractTextsFromElement(el, [])).find(Boolean)
1480
+ if (buttonLabel) {
1481
+ semanticWidget = buildGeneratedRippleButton(toDartLiteral(buttonLabel))
1482
+ }
1483
+ }
1484
+ }
1485
+ const children = []
1486
+ if (!semanticWidget) {
1487
+ for (const c of el.children || []) {
1488
+ if (isTextNode(c)) {
1489
+ const text = c.value.trim()
1490
+ if (text) {
1491
+ const style = textStyleFromProps(props)
1492
+ const align = textAlignFromProps(props)
1493
+ const textLiteral = toDartLiteral(text)
1494
+ const gradientText = gradientTextFromProps(props)
1495
+ if (gradientText) {
1496
+ const parts = [`text: ${textLiteral}`]
1497
+ if (style) parts.push(`style: ${style}`)
1498
+ if (align) parts.push(`textAlign: ${align}`)
1499
+ parts.push(`gradient: ${gradientText}`)
1500
+ children.push({ widget: `AppTextGradient(${parts.join(', ')})`, isAbsolute: false, position: null, zIndex: 0, flexGrow: 0, size: { w: null, h: null } })
1501
+ } else {
1502
+ const parts = [textLiteral]
1503
+ if (style) parts.push(`style: ${style}`)
1504
+ if (align) parts.push(`textAlign: ${align}`)
1505
+ children.push({ widget: `Text(${parts.join(', ')})`, isAbsolute: false, position: null, zIndex: 0, flexGrow: 0, size: { w: null, h: null } })
1506
+ }
1507
+ }
1508
+ } else if (c.type === 'JSXElement') {
1509
+ children.push(buildWidgetFromElement(c, cssMap, topClassName, skipRootDecoration))
1510
+ }
1511
+ }
1512
+ }
1513
+ if (!semanticWidget && tag !== 'img' && shouldStripIconPositioning(size, position, rotate)) {
1514
+ const hasDirectText = (el.children || []).some(child => isTextNode(child) && child.value.trim())
1515
+ const flowOnlyChildren = children.filter(child => !child.isAbsolute)
1516
+ const hasAbsoluteChildren = children.some(child => child.isAbsolute)
1517
+ if (!hasDirectText && !hasAbsoluteChildren && flowOnlyChildren.length === 1) {
1518
+ const simplifiedIcon = simplifySvgWidget(flowOnlyChildren[0].widget, size)
1519
+ if (simplifiedIcon) {
1520
+ return { widget: simplifiedIcon, isAbsolute: false, position: null, zIndex, flexGrow, size }
1521
+ }
1522
+ }
1523
+ }
1524
+ if (tag === 'img') {
1525
+ const src = getAttr(el, 'src') || ''
1526
+ if (/^https?:\/\//.test(src)) {
1527
+ const widget = `Image.network(${toDartLiteral(src)}, fit: BoxFit.cover)`
1528
+ return { widget: rotate != null ? `Transform.rotate(angle: ${rotate}, child: ${widget})` : widget, isAbsolute: !!position, position, zIndex, flexGrow, size }
1529
+ }
1530
+ const local = resolveLocalAsset(src, className)
1531
+ if (local) {
1532
+ const widget = local.isSvg
1533
+ ? `SvgPicture.asset(${local.assetRef})`
1534
+ : `Image.asset(${local.assetRef}, fit: BoxFit.contain)`
1535
+ if (local.isSvg) {
1536
+ const simplified = simplifySvgWidget(widget, size) || widget
1537
+ const stripPlacement = shouldStripIconPositioning(size, position, rotate)
1538
+ const finalWidget = (rotate != null && !stripPlacement) ? `Transform.rotate(angle: ${rotate}, child: ${simplified})` : simplified
1539
+ return { widget: finalWidget, isAbsolute: stripPlacement ? false : !!position, position: stripPlacement ? null : position, zIndex, flexGrow, size }
1540
+ }
1541
+ return { widget: rotate != null ? `Transform.rotate(angle: ${rotate}, child: ${widget})` : widget, isAbsolute: !!position, position, zIndex, flexGrow, size }
1542
+ }
1543
+ const asset = assetForClassName(className)
1544
+ if (asset) {
1545
+ const widget = `SvgPicture.asset(${asset})`
1546
+ const simplified = simplifySvgWidget(widget, size) || widget
1547
+ const stripPlacement = shouldStripIconPositioning(size, position, rotate)
1548
+ const finalWidget = (rotate != null && !stripPlacement) ? `Transform.rotate(angle: ${rotate}, child: ${simplified})` : simplified
1549
+ return { widget: finalWidget, isAbsolute: stripPlacement ? false : !!position, position: stripPlacement ? null : position, zIndex, flexGrow, size }
1550
+ }
1551
+ const widget = 'SizedBox.shrink()'
1552
+ return { widget: rotate != null ? `Transform.rotate(angle: ${rotate}, child: ${widget})` : widget, isAbsolute: !!position, position, zIndex, flexGrow, size }
1553
+ }
1554
+ if (tag === 'button') {
1555
+ let labelText = toDartLiteral('Button')
1556
+ if (children.length) {
1557
+ const m = children[0]?.widget?.match(/Text\('([^']*)'/)
1558
+ if (m && m[1]) labelText = `'${m[1]}'`
1559
+ }
1560
+ const widget = buildGeneratedRippleButton(labelText)
1561
+ return { widget: rotate != null ? `Transform.rotate(angle: ${rotate}, child: ${widget})` : widget, isAbsolute: !!position, position, zIndex, flexGrow, size }
1562
+ }
1563
+ if (tag === 'span' || tag === 'p') {
1564
+ if (children.length === 1) {
1565
+ const widget = children[0].widget
1566
+ return { widget: rotate != null ? `Transform.rotate(angle: ${rotate}, child: ${widget})` : widget, isAbsolute: !!position, position, zIndex, flexGrow, size }
1567
+ }
1568
+ const widget = children.length
1569
+ ? `Row(children: [${children.map(c => c.widget).join(', ')}])`
1570
+ : 'SizedBox.shrink()'
1571
+ return { widget: rotate != null ? `Transform.rotate(angle: ${rotate}, child: ${widget})` : widget, isAbsolute: !!position, position, zIndex, flexGrow, size }
1572
+ }
1573
+ const padding = paddingFromProps(props)
1574
+ const margin = marginFromProps(props)
1575
+ const flowKids = semanticWidget ? [] : children.filter(c => !c.isAbsolute)
1576
+ const absKids = semanticWidget ? [] : children.filter(c => c.isAbsolute).sort((a, b) => (a.zIndex || 0) - (b.zIndex || 0))
1577
+ const inputOwnedWidget = usesAppOwnedInputBackground(semanticWidget)
1578
+ const progressBarOwnedWidget = usesAppOwnedProgressBarBackground(semanticWidget)
1579
+ const wrappedInputOwnedWidget = !semanticWidget && flowKids.length === 1 && usesAppOwnedInputBackground(flowKids[0].widget)
1580
+ const wrappedProgressBarOwnedWidget = !semanticWidget && flowKids.length === 1 && usesAppOwnedProgressBarBackground(flowKids[0].widget)
1581
+ const appOwnedInputWidget = inputOwnedWidget || wrappedInputOwnedWidget
1582
+ const appOwnedProgressBarWidget = progressBarOwnedWidget || wrappedProgressBarOwnedWidget
1583
+ const appOwnedStyledWidget = appOwnedInputWidget || appOwnedProgressBarWidget
1584
+ const omitBackgroundColor = appOwnedStyledWidget
1585
+ const decoration = (skipRootDecoration && className === topClassName)
1586
+ ? null
1587
+ : boxDecorationFromProps(props, { omitBackgroundColor })
1588
+ const effectivePadding = appOwnedStyledWidget ? null : padding
1589
+ const effectiveDecoration = appOwnedStyledWidget ? null : decoration
1590
+ const layoutSize = (appOwnedInputWidget && size) ? { ...size, h: null } : size
1591
+ const flex = flexConfigFromProps(props)
1592
+ let flowBody = semanticWidget
1593
+ if (!flowBody && flowKids.length) {
1594
+ if (flex) {
1595
+ const alignSelf = String(props['align-self'] || '').toLowerCase()
1596
+ const hasMainAxisSize = flex.isRow
1597
+ ? (layoutSize.w != null || alignSelf === 'stretch')
1598
+ : (layoutSize.h != null)
1599
+ const gapLiteral = flex.gap ? (flex.isRow ? `${flex.gap}.width` : `${flex.gap}.height`) : null
1600
+ const baseKids = flowKids.map(c => {
1601
+ if (flex && c.flexGrow > 0 && hasMainAxisSize) {
1602
+ const flexValue = Math.max(1, Math.round(c.flexGrow))
1603
+ return `Expanded(flex: ${flexValue}, child: ${c.widget})`
1604
+ }
1605
+ return c.widget
1606
+ })
1607
+ const kids = gapLiteral && baseKids.length > 1
1608
+ ? baseKids.map((c, i) => i < baseKids.length - 1 ? `${c}, ${gapLiteral}` : c).join(', ')
1609
+ : baseKids.join(', ')
1610
+ flowBody = flex.isRow
1611
+ ? `Row(mainAxisAlignment: ${flex.main}, crossAxisAlignment: ${flex.cross}, children: [${kids}])`
1612
+ : `Column(crossAxisAlignment: ${flex.cross}, mainAxisAlignment: ${flex.main}, children: [${kids}])`
1613
+ } else if (flowKids.length === 1 && absKids.length === 0) {
1614
+ flowBody = flowKids[0].widget
1615
+ } else {
1616
+ flowBody = `Column(crossAxisAlignment: CrossAxisAlignment.start, children: [${flowKids.map(c => c.widget).join(', ')}])`
1617
+ }
1618
+ }
1619
+ let body = flowBody || 'SizedBox.shrink()'
1620
+ if (absKids.length) {
1621
+ const stackChildren = []
1622
+ let primaryAbsIndex = -1
1623
+ if (flowBody) {
1624
+ stackChildren.push(flowBody)
1625
+ } else {
1626
+ for (let i = 0; i < absKids.length; i += 1) {
1627
+ const child = absKids[i]
1628
+ const pos = child.position || {}
1629
+ const isAnchoredTopLeft = (pos.top == null || pos.top === 0)
1630
+ && (pos.left == null || pos.left === 0)
1631
+ && pos.right == null
1632
+ && pos.bottom == null
1633
+ if (isAnchoredTopLeft && !isSmallIconSize(child.size)) {
1634
+ primaryAbsIndex = i
1635
+ stackChildren.push(child.widget)
1636
+ break
1637
+ }
1638
+ }
1639
+ if (primaryAbsIndex === -1) {
1640
+ const derived = normalizeGeneratedSize(estimateStackSize(absKids))
1641
+ const derivedHeight = className === topClassName ? null : derived?.h
1642
+ if (derived && (derived.w != null || derivedHeight != null)) {
1643
+ const w = derived.w != null ? `width: ${derived.w}, ` : ''
1644
+ const h = derivedHeight != null ? `height: ${derivedHeight}, ` : ''
1645
+ stackChildren.push(`SizedBox(${w}${h})`)
1646
+ }
1647
+ }
1648
+ }
1649
+ for (let i = 0; i < absKids.length; i += 1) {
1650
+ if (i === primaryAbsIndex) continue
1651
+ const child = absKids[i]
1652
+ if (isSmallIconSize(child.size)) {
1653
+ const simplifiedIcon = simplifySvgWidget(child.widget, child.size)
1654
+ if (simplifiedIcon) {
1655
+ stackChildren.push(simplifiedIcon)
1656
+ continue
1657
+ }
1658
+ }
1659
+ const pos = child.position || {}
1660
+ const args = []
1661
+ if (pos.top != null) args.push(`top: ${pos.top}`)
1662
+ if (pos.left != null) args.push(`left: ${pos.left}`)
1663
+ if (pos.right != null) args.push(`right: ${pos.right}`)
1664
+ if (pos.bottom != null) args.push(`bottom: ${pos.bottom}`)
1665
+ const positioned = args.length
1666
+ ? `Positioned(${args.join(', ')}, child: ${child.widget})`
1667
+ : child.widget
1668
+ stackChildren.push(positioned)
1669
+ }
1670
+ body = `Stack(clipBehavior: Clip.none, children: [${stackChildren.join(', ')}])`
1671
+ }
1672
+ if (effectivePadding || margin || effectiveDecoration) {
1673
+ const padLine = effectivePadding ? `padding: ${effectivePadding},` : ''
1674
+ const marLine = margin ? `margin: ${margin},` : ''
1675
+ const decLine = effectiveDecoration ? `decoration: ${effectiveDecoration},` : ''
1676
+ const sizedChild = (layoutSize.w || layoutSize.h) && className !== topClassName
1677
+ ? `SizedBox(${layoutSize.w ? `width: ${layoutSize.w}, ` : ''}${layoutSize.h ? `height: ${layoutSize.h}, ` : ''}child: ${wrapOverflowIfSized(body, layoutSize)})`
1678
+ : body
1679
+ let container = `Container(${padLine}${marLine}${decLine} child: ${sizedChild})`
1680
+ if (overflowHidden) {
1681
+ const radius = borderRadiusFromProps(props)
1682
+ container = radius
1683
+ ? `ClipRRect(borderRadius: ${radius}, child: ${container})`
1684
+ : `ClipRect(child: ${container})`
1685
+ }
1686
+ const widget = rotate != null ? `Transform.rotate(angle: ${rotate}, child: ${container})` : container
1687
+ return { widget, isAbsolute: !!position, position, zIndex, flexGrow, size: layoutSize }
1688
+ }
1689
+ if ((layoutSize.w || layoutSize.h) && className !== topClassName) {
1690
+ let sized = `SizedBox(${layoutSize.w ? `width: ${layoutSize.w}, ` : ''}${layoutSize.h ? `height: ${layoutSize.h}, ` : ''}child: ${wrapOverflowIfSized(body, layoutSize)})`
1691
+ if (overflowHidden) {
1692
+ const radius = borderRadiusFromProps(props)
1693
+ sized = radius
1694
+ ? `ClipRRect(borderRadius: ${radius}, child: ${sized})`
1695
+ : `ClipRect(child: ${sized})`
1696
+ }
1697
+ const widget = rotate != null ? `Transform.rotate(angle: ${rotate}, child: ${sized})` : sized
1698
+ return { widget, isAbsolute: !!position, position, zIndex, flexGrow, size: layoutSize }
1699
+ }
1700
+ let finalWidget = body
1701
+ if (overflowHidden) {
1702
+ const radius = borderRadiusFromProps(props)
1703
+ finalWidget = radius
1704
+ ? `ClipRRect(borderRadius: ${radius}, child: ${finalWidget})`
1705
+ : `ClipRect(child: ${finalWidget})`
1706
+ }
1707
+ if (rotate != null) finalWidget = `Transform.rotate(angle: ${rotate}, child: ${finalWidget})`
1708
+ return { widget: finalWidget, isAbsolute: !!position, position, zIndex, flexGrow, size: layoutSize }
1709
+ }
1710
+
1711
+ function generateDart(ast, cssMap, outClassName, outPath) {
1712
+ let rootWidget = null
1713
+ let topClassName = null
1714
+ traverse(ast, {
1715
+ JSXElement(pathNode) {
1716
+ if (!rootWidget) {
1717
+ topClassName = getClassNameAttr(pathNode.node)
1718
+ const mode = process.env.JSX2FLUTTER_MODE
1719
+ rootWidget = buildWidgetFromElement(pathNode.node, cssMap, topClassName, mode !== 'classic')
1720
+ }
1721
+ }
1722
+ })
1723
+ // Root wrapper: background + scroll
1724
+ const rootProps = topClassName && cssMap[topClassName] ? cssMap[topClassName] : {}
1725
+ const rootSize = sizeFromProps(rootProps)
1726
+ const rootWidth = rootSize.w
1727
+ const rootDeco = boxDecorationFromProps(rootProps)
1728
+ const mode = process.env.JSX2FLUTTER_MODE
1729
+ let inner = rootWidget?.widget || 'const SizedBox.shrink()'
1730
+ let finalRoot
1731
+ if (mode === 'classic') {
1732
+ finalRoot = inner
1733
+ } else if (mode === 'scaffold') {
1734
+ finalRoot = rootDeco
1735
+ ? `Scaffold(body: Container(width: Get.width, decoration: ${rootDeco}, child: SingleChildScrollView(child: ${inner})))`
1736
+ : `Scaffold(body: Container(width: Get.width, child: SingleChildScrollView(child: ${inner})))`
1737
+ } else {
1738
+ finalRoot = rootDeco
1739
+ ? `DecoratedBox(decoration: ${rootDeco}, child: SingleChildScrollView(child: FittedBox(alignment: Alignment.topLeft, fit: BoxFit.fitWidth, child: ${inner})))`
1740
+ : `SingleChildScrollView(child: FittedBox(alignment: Alignment.topLeft, fit: BoxFit.fitWidth, child: ${inner}))`
1741
+ }
1742
+ if (rootWidth != null) {
1743
+ const numericRootWidth = Number(rootWidth)
1744
+ const escapedRootWidth = String(rootWidth).replace('.', '\\.')
1745
+ const rootWidthPattern = Number.isInteger(numericRootWidth) ? `${escapedRootWidth}(?:\\.0+)?` : escapedRootWidth
1746
+ finalRoot = finalRoot.replace(new RegExp(`width:\\s*${rootWidthPattern}(?=\\s*[,\\)])`, 'g'), 'width: Get.width')
1747
+ finalRoot = finalRoot.replace(new RegExp(`maxWidth:\\s*${rootWidthPattern}(?=\\s*[,\\)])`, 'g'), 'maxWidth: Get.width')
1748
+ }
1749
+ const usesDateTimeField = finalRoot.includes('_GeneratedDateTimeField(')
1750
+ const usesInput = finalRoot.includes('AppInput(')
1751
+ const usesRadio = finalRoot.includes('_GeneratedRadioGroup(') || finalRoot.includes('AppRadioGroup(') || finalRoot.includes('AppRadioOption(')
1752
+ const usesCheckbox = finalRoot.includes('_GeneratedCheckbox(') || finalRoot.includes('AppCheckbox(')
1753
+ const usesRippleButton = finalRoot.includes('RippleButton(')
1754
+ const usesTextGradient = finalRoot.includes('AppTextGradient(')
1755
+ const usesProgressBar = finalRoot.includes('AppProgressBar(')
1756
+ const usesGetWidth = finalRoot.includes('Get.width')
1757
+ const imports = [
1758
+ "import 'package:flutter/material.dart';",
1759
+ `import 'package:${PROJECT_PACKAGE_NAME}/src/utils/app_colors.dart';`,
1760
+ `import 'package:${PROJECT_PACKAGE_NAME}/src/extensions/int_extensions.dart';`,
1761
+ "import 'package:flutter_svg/flutter_svg.dart';",
1762
+ `import 'package:${PROJECT_PACKAGE_NAME}/src/utils/app_assets.dart';`,
1763
+ ]
1764
+ if (usesInput) {
1765
+ imports.push(`import 'package:${PROJECT_PACKAGE_NAME}/src/ui/widgets/app_input.dart';`)
1766
+ }
1767
+ if (usesDateTimeField) {
1768
+ imports.push(`import 'package:${PROJECT_PACKAGE_NAME}/src/ui/widgets/app_input_full_time.dart';`)
1769
+ }
1770
+ if (usesRadio) {
1771
+ imports.push(`import 'package:${PROJECT_PACKAGE_NAME}/src/ui/widgets/app_radio_button.dart';`)
1772
+ }
1773
+ if (usesCheckbox) {
1774
+ imports.push(`import 'package:${PROJECT_PACKAGE_NAME}/src/ui/widgets/base/checkbox/app_checkbox.dart';`)
1775
+ }
1776
+ if (usesRippleButton) {
1777
+ imports.push(`import 'package:${PROJECT_PACKAGE_NAME}/src/ui/widgets/base/ripple_button.dart';`)
1778
+ }
1779
+ if (usesTextGradient) {
1780
+ imports.push(`import 'package:${PROJECT_PACKAGE_NAME}/src/ui/widgets/app_text_gradient.dart';`)
1781
+ }
1782
+ if (usesProgressBar) {
1783
+ imports.push(`import 'package:${PROJECT_PACKAGE_NAME}/src/ui/widgets/app_progressbar.dart';`)
1784
+ }
1785
+ if (usesGetWidth) {
1786
+ imports.push("import 'package:get/get.dart';")
1787
+ }
1788
+ const helperBlocks = []
1789
+ if (usesDateTimeField) {
1790
+ helperBlocks.push(
1791
+ [
1792
+ "class _GeneratedDateTimeField extends StatefulWidget {",
1793
+ " final String? label;",
1794
+ " final String? hint;",
1795
+ " const _GeneratedDateTimeField({this.label, this.hint});",
1796
+ " @override",
1797
+ " State<_GeneratedDateTimeField> createState() => _GeneratedDateTimeFieldState();",
1798
+ "}",
1799
+ "",
1800
+ "class _GeneratedDateTimeFieldState extends State<_GeneratedDateTimeField> {",
1801
+ " late DateTime _selectedDate;",
1802
+ " @override",
1803
+ " void initState() {",
1804
+ " super.initState();",
1805
+ " final parsed = _parseDate(widget.hint);",
1806
+ " _selectedDate = parsed ?? DateTime.now();",
1807
+ " }",
1808
+ " DateTime? _parseDate(String? value) {",
1809
+ " final raw = (value ?? '').trim();",
1810
+ " if (raw.isEmpty) {",
1811
+ " return null;",
1812
+ " }",
1813
+ " final normalized = raw",
1814
+ " .replaceAll('年', '/')",
1815
+ " .replaceAll('月', '/')",
1816
+ " .replaceAll('日', '')",
1817
+ " .replaceAll('.', '/')",
1818
+ " .replaceAll('-', '/');",
1819
+ " final full = RegExp(r'(\\\\d{4})/(\\\\d{1,2})/(\\\\d{1,2})').firstMatch(normalized);",
1820
+ " if (full != null) {",
1821
+ " final year = int.tryParse(full.group(1)!);",
1822
+ " final month = int.tryParse(full.group(2)!);",
1823
+ " final day = int.tryParse(full.group(3)!);",
1824
+ " if (year != null && month != null && day != null) {",
1825
+ " return DateTime(year, month, day);",
1826
+ " }",
1827
+ " }",
1828
+ " final yearMonth = RegExp(r'(\\\\d{4})/(\\\\d{1,2})').firstMatch(normalized);",
1829
+ " if (yearMonth != null) {",
1830
+ " final year = int.tryParse(yearMonth.group(1)!);",
1831
+ " final month = int.tryParse(yearMonth.group(2)!);",
1832
+ " if (year != null && month != null) {",
1833
+ " return DateTime(year, month, 1);",
1834
+ " }",
1835
+ " }",
1836
+ " final yearOnly = RegExp(r'\\\\b(\\\\d{4})\\\\b').firstMatch(normalized);",
1837
+ " if (yearOnly != null) {",
1838
+ " final year = int.tryParse(yearOnly.group(1)!);",
1839
+ " if (year != null) {",
1840
+ " return DateTime(year, 1, 1);",
1841
+ " }",
1842
+ " }",
1843
+ " return null;",
1844
+ " }",
1845
+ " @override",
1846
+ " Widget build(BuildContext context) {",
1847
+ " return AppInputFullTime(",
1848
+ " label: widget.label,",
1849
+ " hint: widget.hint ?? 'YYYY/MM/DD',",
1850
+ " initialTime: _selectedDate,",
1851
+ " onTimeChanged: (next) {",
1852
+ " if (!mounted) {",
1853
+ " return;",
1854
+ " }",
1855
+ " setState(() => _selectedDate = next);",
1856
+ " },",
1857
+ " minimumYear: 1900,",
1858
+ " maximumYear: 2100,",
1859
+ " );",
1860
+ " }",
1861
+ "}",
1862
+ ].join('\n')
1863
+ )
1864
+ }
1865
+ if (usesRadio) {
1866
+ helperBlocks.push(
1867
+ [
1868
+ "class _GeneratedRadioGroup extends StatefulWidget {",
1869
+ " final String initialValue;",
1870
+ " final List<AppRadioOption> options;",
1871
+ " final Color? activeColor;",
1872
+ " final Color? inactiveColor;",
1873
+ " const _GeneratedRadioGroup({required this.initialValue, required this.options, this.activeColor, this.inactiveColor});",
1874
+ " @override",
1875
+ " State<_GeneratedRadioGroup> createState() => _GeneratedRadioGroupState();",
1876
+ "}",
1877
+ "",
1878
+ "class _GeneratedRadioGroupState extends State<_GeneratedRadioGroup> {",
1879
+ " late String _value;",
1880
+ " @override",
1881
+ " void initState() {",
1882
+ " super.initState();",
1883
+ " if (widget.initialValue.isNotEmpty) {",
1884
+ " _value = widget.initialValue;",
1885
+ " } else if (widget.options.isNotEmpty) {",
1886
+ " _value = widget.options.first.value;",
1887
+ " } else {",
1888
+ " _value = '';",
1889
+ " }",
1890
+ " }",
1891
+ " @override",
1892
+ " Widget build(BuildContext context) {",
1893
+ " if (widget.options.isEmpty) {",
1894
+ " return const SizedBox.shrink();",
1895
+ " }",
1896
+ " return AppRadioGroup(",
1897
+ " value: _value,",
1898
+ " options: widget.options,",
1899
+ " activeColor: widget.activeColor,",
1900
+ " inactiveColor: widget.inactiveColor,",
1901
+ " onChanged: (next) {",
1902
+ " if (!mounted) {",
1903
+ " return;",
1904
+ " }",
1905
+ " setState(() => _value = next);",
1906
+ " },",
1907
+ " );",
1908
+ " }",
1909
+ "}",
1910
+ ].join('\n')
1911
+ )
1912
+ }
1913
+ if (usesCheckbox) {
1914
+ helperBlocks.push(
1915
+ [
1916
+ "class _GeneratedCheckbox extends StatefulWidget {",
1917
+ " final String title;",
1918
+ " final bool initialChecked;",
1919
+ " final Color? borderColor;",
1920
+ " const _GeneratedCheckbox({required this.title, required this.initialChecked, this.borderColor});",
1921
+ " @override",
1922
+ " State<_GeneratedCheckbox> createState() => _GeneratedCheckboxState();",
1923
+ "}",
1924
+ "",
1925
+ "class _GeneratedCheckboxState extends State<_GeneratedCheckbox> {",
1926
+ " late bool _checked;",
1927
+ " @override",
1928
+ " void initState() {",
1929
+ " super.initState();",
1930
+ " _checked = widget.initialChecked;",
1931
+ " }",
1932
+ " @override",
1933
+ " Widget build(BuildContext context) {",
1934
+ " return AppCheckbox(",
1935
+ " title: widget.title,",
1936
+ " isChecked: _checked,",
1937
+ " borderColor: widget.borderColor,",
1938
+ " onTap: () {",
1939
+ " if (!mounted) {",
1940
+ " return;",
1941
+ " }",
1942
+ " setState(() => _checked = !_checked);",
1943
+ " },",
1944
+ " );",
1945
+ " }",
1946
+ "}",
1947
+ ].join('\n')
1948
+ )
1949
+ }
1950
+ const content = [
1951
+ ...imports,
1952
+ "",
1953
+ `class ${outClassName} extends StatelessWidget {`,
1954
+ " const " + outClassName + "({super.key});",
1955
+ " @override",
1956
+ " Widget build(BuildContext context) {",
1957
+ " return " + finalRoot + ";",
1958
+ " }",
1959
+ "}",
1960
+ ...(helperBlocks.length ? ["", ...helperBlocks] : []),
1961
+ ""
1962
+ ].join('\n')
1963
+ writeFile(outPath, content)
1964
+ }
1965
+
1966
+ function run(inJsx, inCss, outDart) {
1967
+ const jsx = readFile(inJsx)
1968
+ const css = readFile(inCss)
1969
+ const assetPrefix = process.env.JSX2FLUTTER_ASSET_PREFIX || ''
1970
+ ASSET_CONTEXT = {
1971
+ jsxDir: path.dirname(inJsx),
1972
+ assetDir: DEFAULT_ASSET_DIR,
1973
+ assetPrefix,
1974
+ assets: new Map(),
1975
+ appAssetsRegistry: loadAppAssetsRegistry(),
1976
+ semanticHintByAbs: new Map(),
1977
+ fileNameByAbs: new Map(),
1978
+ usedFileNames: new Set(),
1979
+ copyAssets: process.env.JSX2FLUTTER_COPY_ASSETS !== '0',
1980
+ renameAssets: !!sanitizeAssetToken(assetPrefix),
1981
+ }
1982
+ const cssMap = parseCssModules(css, inCss)
1983
+ const ast = parse(jsx, { sourceType: 'module', plugins: ['jsx'] })
1984
+ ASSET_CONTEXT.semanticHintByAbs = collectAssetSemanticHints(ast, path.dirname(inJsx))
1985
+ const baseName = path.basename(outDart, '.dart')
1986
+ const className = toWidgetClassName(baseName)
1987
+ generateDart(ast, cssMap, className, outDart)
1988
+ copyCollectedAssets()
1989
+ syncAppAssetsFile()
1990
+ }
1991
+
1992
+ if (process.argv.length < 5) {
1993
+ process.stderr.write('Usage: node jsx2flutter.mjs <index.jsx> <index.module.scss> <out.dart>\\n')
1994
+ process.exit(1)
1995
+ }
1996
+
1997
+ run(process.argv[2], process.argv[3], process.argv[4])