agent-flutter 0.1.25 → 0.1.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -74,3 +74,30 @@ Finalize this feature:
|
|
|
74
74
|
- Screenshot evidence: [No UI OR before/after]
|
|
75
75
|
- Paired with: [Solo OR name]
|
|
76
76
|
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## **5. Refactor Converted UI (Post-Convert)**
|
|
81
|
+
|
|
82
|
+
Use this when code comes from convert script and needs production refactor:
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
Refactor converted UI:
|
|
86
|
+
- Source file: [e.g. lib/src/ui/home_demo_figma/home_demo_figma_page.dart]
|
|
87
|
+
- Feature name: [e.g. home_demo]
|
|
88
|
+
- Keep exact Figma visual: [yes/no]
|
|
89
|
+
- Needs API integration now?: [yes/no]
|
|
90
|
+
- Normalize/rename convert assets?: [yes/no]
|
|
91
|
+
- Commit now?: [yes/no]
|
|
92
|
+
- Push now?: [yes/no]
|
|
93
|
+
- Create PR now?: [yes/no]
|
|
94
|
+
- Priority fixes: [naming, localization, App* widgets, token cleanup, asset-rename, overflow]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Expected behavior:
|
|
98
|
+
1. Apply `ui-refactor-convert.md` + `ui.md`.
|
|
99
|
+
2. Rename generated classes/files to semantic names.
|
|
100
|
+
3. Normalize convert-generated asset folder and names, then update `AppAssets` usages.
|
|
101
|
+
4. Replace hardcoded style/string with tokens + localization.
|
|
102
|
+
5. Extract components and pass UI correctness gates.
|
|
103
|
+
6. Ask and execute in order: commit -> push -> create PR.
|
|
@@ -21,7 +21,7 @@ This is the required "done" flow before feature handoff.
|
|
|
21
21
|
6. If answer is `yes`, push branch to remote.
|
|
22
22
|
7. Ask user explicitly: `Do you want me to create PR now? (yes/no)`
|
|
23
23
|
8. If answer is `yes`, create PR with mandatory template sections.
|
|
24
|
-
9. If
|
|
24
|
+
9. If answer is `no` at any step, stop at that step and return current status.
|
|
25
25
|
|
|
26
26
|
If any step is skipped, feature is not considered complete.
|
|
27
27
|
|
|
@@ -48,6 +48,6 @@ PR must follow `.github/pull_request_template.md` and include all sections:
|
|
|
48
48
|
|
|
49
49
|
## 6. Agent Behavior
|
|
50
50
|
- After finishing UI/API tasks, agent must ask in order: commit -> push -> create PR.
|
|
51
|
-
- Agent
|
|
51
|
+
- Agent executes only steps explicitly confirmed by user (`yes`).
|
|
52
52
|
- If user confirms all steps, agent returns PR URL as final output.
|
|
53
|
-
- If user declines at any step, agent stops and returns
|
|
53
|
+
- If user declines at any step, agent stops and returns repository status.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
---
|
|
2
|
+
alwaysApply: false
|
|
3
|
+
---
|
|
4
|
+
# UI Refactor Rule for Convert Output (JSX/Figma -> Dart)
|
|
5
|
+
|
|
6
|
+
## 1. Goal
|
|
7
|
+
Turn generated UI code into production-ready Flutter code that fully complies with `ui.md`.
|
|
8
|
+
|
|
9
|
+
This rule applies after running convert scripts (for example files like `home_demo_figma_page.dart` and generated component trees), especially when convert output creates non-standard asset names/folders.
|
|
10
|
+
|
|
11
|
+
## 1.1 Required References
|
|
12
|
+
Always apply together with:
|
|
13
|
+
- [ui.md](./ui.md)
|
|
14
|
+
- [flutter-ui-widgets](../skills/flutter-ui-widgets/SKILL.md)
|
|
15
|
+
- [flutter-assets-management](../skills/flutter-assets-management/SKILL.md)
|
|
16
|
+
- [getx-localization-standard](../skills/getx-localization-standard/SKILL.md)
|
|
17
|
+
- [dart-best-practices](../skills/dart-best-practices/SKILL.md)
|
|
18
|
+
- [ci-cd-pr.md](./ci-cd-pr.md)
|
|
19
|
+
|
|
20
|
+
If API data is involved, also apply:
|
|
21
|
+
- [integration-api.md](./integration-api.md)
|
|
22
|
+
- [dart-model-reuse](../skills/dart-model-reuse/SKILL.md)
|
|
23
|
+
|
|
24
|
+
## 2. Trigger Conditions
|
|
25
|
+
Use this rule when at least one condition is true:
|
|
26
|
+
- Source file is generated by convert pipeline.
|
|
27
|
+
- Class/file names are non-semantic (for example: `UhomeUdemoFigma`, `Frame123`).
|
|
28
|
+
- Generated UI has hardcoded strings/colors/styles or deep nested widget trees.
|
|
29
|
+
- Converted UI works visually but fails architecture/readability/maintainability standards.
|
|
30
|
+
|
|
31
|
+
## 3. Refactor Workflow
|
|
32
|
+
|
|
33
|
+
### Step 1: Normalize Structure and Naming
|
|
34
|
+
- Keep page architecture:
|
|
35
|
+
- `lib/src/ui/<feature>/{binding,interactor,components}`
|
|
36
|
+
- Rename generated class/file names to semantic names.
|
|
37
|
+
- Keep page entry simple (`<feature>_page.dart` should not contain huge raw trees).
|
|
38
|
+
|
|
39
|
+
### Step 2: Apply Design Tokens and Shared Widgets
|
|
40
|
+
- Replace raw style values with:
|
|
41
|
+
- `AppColors`
|
|
42
|
+
- `AppStyles`
|
|
43
|
+
- `AppDimensions` and extensions (`n.width`, `n.height`)
|
|
44
|
+
- Replace raw controls with existing `App*` widgets where applicable.
|
|
45
|
+
- Keep exact Figma values when overriding tokens (radius/spacing/typography).
|
|
46
|
+
|
|
47
|
+
### Step 3: Remove Convert Artifacts
|
|
48
|
+
- Remove redundant wrappers (`ClipRRect + Container` duplicates, empty containers, unnecessary stacks).
|
|
49
|
+
- Reduce deeply nested UI into clear sub-components under `components/`.
|
|
50
|
+
- Add `const` where possible.
|
|
51
|
+
- Avoid `Get.width` direct usage when local layout constraints are more correct (`LayoutBuilder`, `MediaQuery` scoped use).
|
|
52
|
+
|
|
53
|
+
### Step 4: Localization and Content Cleanup
|
|
54
|
+
- Move all user-facing text to `LocaleKey` + `.tr`.
|
|
55
|
+
- Create/Update feature localization JSON files:
|
|
56
|
+
- `lib/src/locale/json/en/<feature>.json`
|
|
57
|
+
- `lib/src/locale/json/ja/<feature>.json`
|
|
58
|
+
- Create/Update feature key module:
|
|
59
|
+
- `lib/src/locale/keys/<feature>_locale_key.dart`
|
|
60
|
+
- Ensure `lang_en.dart` / `lang_ja.dart` only aggregate feature maps (do not inject random inline feature strings).
|
|
61
|
+
- Ensure EN/JA JSON key parity for the refactored feature.
|
|
62
|
+
- Do not keep demo/static strings inside production widget tree.
|
|
63
|
+
- If temporary mock is needed, source it from `app_demo_data.dart`.
|
|
64
|
+
|
|
65
|
+
### Step 5: State and Behavior Correctness
|
|
66
|
+
- Ensure required states render correctly:
|
|
67
|
+
- `loading`, `success`, `empty`, `error` (plus `unauthorized` if relevant).
|
|
68
|
+
- Ensure UI behavior is not only visual:
|
|
69
|
+
- tap handlers wired,
|
|
70
|
+
- enabled/disabled state clear,
|
|
71
|
+
- no broken interaction overlays.
|
|
72
|
+
|
|
73
|
+
### Step 6: Asset and Icon Hygiene
|
|
74
|
+
- Use `AppAssets` only (no raw asset path in widget code).
|
|
75
|
+
- Keep icon/image naming consistent and avoid duplicates.
|
|
76
|
+
- Verify SVG rendering without unexpected tint/color shift.
|
|
77
|
+
- Normalize convert-generated asset folders and names:
|
|
78
|
+
- Detect non-standard convert folders (for example: `assets/figma/**`).
|
|
79
|
+
- Move/rename assets to feature-scoped locations:
|
|
80
|
+
- `assets/images/<feature>/**`
|
|
81
|
+
- `assets/images/icons/<feature>/**`
|
|
82
|
+
- Rename files to meaningful `snake_case` names based on feature and purpose.
|
|
83
|
+
- Replace convert-style asset constants in `lib/src/utils/app_assets.dart` with feature-scoped naming.
|
|
84
|
+
- Update all usages in Dart code to new `AppAssets` constants.
|
|
85
|
+
- Prevent cross-feature collisions (same filename/constant for different meanings).
|
|
86
|
+
- Keep shared assets explicitly under shared scope only when truly reused.
|
|
87
|
+
- Remove orphan/unused old asset files and constants.
|
|
88
|
+
|
|
89
|
+
### Step 7: Verify and Prepare for PR
|
|
90
|
+
- Run at minimum:
|
|
91
|
+
- `fvm flutter analyze`
|
|
92
|
+
- Validate UI correctness gates from `ui.md`:
|
|
93
|
+
- responsive widths `320/390/430`
|
|
94
|
+
- text scale `1.0/1.3`
|
|
95
|
+
- no overflow warnings in debug logs
|
|
96
|
+
|
|
97
|
+
### Step 8: Commit/Push/PR Confirmation (Mandatory)
|
|
98
|
+
- After refactor verification, ask user in strict order:
|
|
99
|
+
1. `Do you want me to commit now? (yes/no)`
|
|
100
|
+
2. `Do you want me to push now? (yes/no)`
|
|
101
|
+
3. `Do you want me to create PR now? (yes/no)`
|
|
102
|
+
- Execute only steps explicitly confirmed with `yes`.
|
|
103
|
+
- If user answers `no` at any step, stop there and return current status.
|
|
104
|
+
|
|
105
|
+
## 4. Mandatory Output of Refactor Task
|
|
106
|
+
After refactor, output must include:
|
|
107
|
+
- List of renamed files/classes.
|
|
108
|
+
- List of extracted components.
|
|
109
|
+
- List of localized keys added/updated.
|
|
110
|
+
- Localization JSON files created/updated per feature (`en/ja`) and parity status.
|
|
111
|
+
- List of token replacements (`raw -> AppColors/AppStyles/AppDimensions`).
|
|
112
|
+
- Asset rename mapping (`old path/name -> new path/name`) and updated feature-scoped `AppAssets` constants.
|
|
113
|
+
- Feature asset placement report (`feature -> directories/constants`) to prove no cross-feature mixing.
|
|
114
|
+
- Commit/push/PR confirmation answers and execution results.
|
|
115
|
+
- Remaining known gaps (if any) with exact file paths.
|
|
116
|
+
|
|
117
|
+
## 5. Anti-Patterns (Do Not)
|
|
118
|
+
- Do not keep generated class names in final code.
|
|
119
|
+
- Do not leave hardcoded colors/text if a project token/key exists.
|
|
120
|
+
- Do not keep feature localization inline in `lang_en.dart`/`lang_ja.dart` when feature JSON exists.
|
|
121
|
+
- Do not keep unmatched keys between `json/en/<feature>.json` and `json/ja/<feature>.json`.
|
|
122
|
+
- Do not keep convert-only asset naming/folders in final code if they violate project standard.
|
|
123
|
+
- Do not place feature-specific assets in generic folders without feature namespace.
|
|
124
|
+
- Do not reuse ambiguous asset constants across different features.
|
|
125
|
+
- Do not parse API JSON directly in UI widgets.
|
|
126
|
+
- Do not submit refactor that only changes formatting but leaves architectural violations.
|
|
127
|
+
|
|
128
|
+
## 6. Prompt Template
|
|
129
|
+
Use this prompt to trigger this rule:
|
|
130
|
+
|
|
131
|
+
> Refactor converted UI using `ui-refactor-convert.md` + `ui.md`.
|
|
132
|
+
> Source: `<path-to-generated-page-or-component>`.
|
|
133
|
+
> Keep visual fidelity, but enforce architecture/tokens/localization/state correctness.
|
|
134
|
+
> Extract reusable components and remove convert artifacts.
|
|
135
|
+
> Normalize asset folder/naming by feature and update `AppAssets` with feature-scoped constants.
|
|
136
|
+
> Split localization by feature JSON (en/ja) and update LocaleKey feature module.
|
|
137
|
+
> Then ask: commit now? push now? create PR now?
|
|
138
|
+
> Return changed files and verification checklist results.
|
|
@@ -67,18 +67,25 @@ alwaysApply: false
|
|
|
67
67
|
|
|
68
68
|
### **C. Localization (Priority P1)**
|
|
69
69
|
- **No Raw Strings**: All user-facing text MUST be localized.
|
|
70
|
-
- **
|
|
71
|
-
-
|
|
72
|
-
|
|
73
|
-
- `lib/src/locale/
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
70
|
+
- **JSON Per Feature (Required)**:
|
|
71
|
+
- Each feature must have its own JSON files:
|
|
72
|
+
- `lib/src/locale/json/en/<feature>.json`
|
|
73
|
+
- `lib/src/locale/json/ja/<feature>.json`
|
|
74
|
+
- One JSON file equals one function/feature scope. Do not mix multiple features in one JSON.
|
|
75
|
+
- Keep key parity between `en` and `ja` for the same feature.
|
|
76
|
+
- **LocaleKey By Feature (Required)**:
|
|
77
|
+
- Split key declarations by feature:
|
|
78
|
+
- `lib/src/locale/keys/<feature>_locale_key.dart`
|
|
79
|
+
- `lib/src/locale/locale_key.dart` should act as barrel/aggregator entry for all feature key files.
|
|
80
|
+
- Key naming format remains snake_case with feature prefix: `feature_screen_element`.
|
|
81
|
+
- **Aggregator Contract (Required)**:
|
|
82
|
+
- `lang_en.dart` and `lang_ja.dart` are aggregators that merge feature maps from JSON-derived modules.
|
|
83
|
+
- Do not hardcode new feature strings directly inside `lang_en.dart`/`lang_ja.dart` except app-level bootstrap keys.
|
|
84
|
+
- `translation_manager.dart` must keep consuming `enUs` and `jaJp` variables.
|
|
85
|
+
- **Usage**: Use `.tr` extension with `LocaleKey`.
|
|
86
|
+
- **Common Keys**:
|
|
87
|
+
- Maintain `common` localization as separate JSON modules (`common.json`) per language.
|
|
88
|
+
- Reuse common keys (`ok`, `cancel`, `loading`, `error`, ...) instead of duplicating per feature.
|
|
82
89
|
```dart
|
|
83
90
|
// ✅ Correct
|
|
84
91
|
Text(LocaleKey.home_title.tr);
|
|
@@ -119,6 +126,16 @@ alwaysApply: false
|
|
|
119
126
|
- **Rule**: Follow the [Assets Management Skill](../skills/flutter-assets-management/SKILL.md) strictly.
|
|
120
127
|
- **Naming**: MUST match Figma layer names in `snake_case`.
|
|
121
128
|
- **Registry**: All assets MUST be defined in `lib/src/utils/app_assets.dart`.
|
|
129
|
+
- **Feature-Scoped Assets (Required)**:
|
|
130
|
+
- Group assets by feature to avoid collisions/misuse:
|
|
131
|
+
- `assets/images/<feature>/...`
|
|
132
|
+
- `assets/images/icons/<feature>/...`
|
|
133
|
+
- Do not put new feature assets into generic/global folders unless truly shared.
|
|
134
|
+
- Shared cross-feature assets must live in explicit shared folder and use shared-prefixed constants.
|
|
135
|
+
- **Feature-Scoped AppAssets Constants (Required)**:
|
|
136
|
+
- Constant names must include feature prefix (for example: `homeDemoIconBellSvg`, `visitRecordImgHeaderPng`).
|
|
137
|
+
- Prefer splitting asset declarations by feature module, with `app_assets.dart` as aggregator entry.
|
|
138
|
+
- When refactoring, replace old ambiguous constants with feature-scoped names.
|
|
122
139
|
- **Usage**:
|
|
123
140
|
```dart
|
|
124
141
|
// ✅ Correct
|
|
@@ -129,9 +146,10 @@ alwaysApply: false
|
|
|
129
146
|
```
|
|
130
147
|
- **Workflow**:
|
|
131
148
|
1. Rename layer in Figma to `snake_case`.
|
|
132
|
-
2. Export to `assets/images
|
|
133
|
-
3. Register path in `AppAssets`.
|
|
134
|
-
4. Use `AppAssets
|
|
149
|
+
2. Export to feature folder (`assets/images/<feature>/` or `assets/images/icons/<feature>/`).
|
|
150
|
+
3. Register path with feature-scoped constant in `AppAssets`.
|
|
151
|
+
4. Use `AppAssets.<featureScopedName>` in code.
|
|
152
|
+
5. Remove/replace old ambiguous asset names and unused files.
|
|
135
153
|
- **SVG Hygiene (Priority P1)**:
|
|
136
154
|
- Flatten stroke → fill để tint/recolor an toàn.
|
|
137
155
|
- Remove mask/clip/filter phức tạp; đảm bảo `viewBox` 24×24 cho icon.
|
|
@@ -188,8 +206,16 @@ Follow these steps when creating a new UI screen:
|
|
|
188
206
|
|
|
189
207
|
### **Step 5: Localization**
|
|
190
208
|
- **Identify Strings**: List all text in the new UI (See [GetX Localization Skill](../skills/getx-localization-standard/SKILL.md)).
|
|
191
|
-
- **
|
|
192
|
-
-
|
|
209
|
+
- **Create Feature JSON Files**:
|
|
210
|
+
- `lib/src/locale/json/en/<feature>.json`
|
|
211
|
+
- `lib/src/locale/json/ja/<feature>.json`
|
|
212
|
+
- **Update Keys**:
|
|
213
|
+
- Add/update keys in `lib/src/locale/keys/<feature>_locale_key.dart`.
|
|
214
|
+
- Ensure `locale_key.dart` exports/aggregates the feature key module.
|
|
215
|
+
- **Update Aggregators**:
|
|
216
|
+
- Merge new feature translations into `lang_en.dart` and `lang_ja.dart` via feature module maps.
|
|
217
|
+
- Ensure `enUs` and `jaJp` remain valid and synchronized.
|
|
218
|
+
- **Parity Check**: `en` and `ja` JSON for the same feature must have identical key sets.
|
|
193
219
|
|
|
194
220
|
### **Step 6: Main Page Implementation**
|
|
195
221
|
- Create `<page_name>_page.dart` (See [UI Widgets Skill](../skills/flutter-ui-widgets/SKILL.md)).
|
|
@@ -281,6 +307,12 @@ Follow these steps when creating a new UI screen:
|
|
|
281
307
|
## **4. Checklist Before PR**
|
|
282
308
|
- [ ] No hardcoded colors/styles?
|
|
283
309
|
- [ ] No raw strings? (Localization used)
|
|
310
|
+
- [ ] **Localization JSON per feature created?** (`json/en/<feature>.json` + `json/ja/<feature>.json`)
|
|
311
|
+
- [ ] **LocaleKey split by feature?** (`keys/<feature>_locale_key.dart`)
|
|
312
|
+
- [ ] **EN/JA key parity passed?** (same keys between feature JSON files)
|
|
313
|
+
- [ ] **lang_en/lang_ja are aggregator-only?** (no random inline feature strings)
|
|
314
|
+
- [ ] **Assets grouped by feature?** (`assets/images/<feature>` and `assets/images/icons/<feature>`)
|
|
315
|
+
- [ ] **AppAssets constants are feature-scoped?** (no ambiguous generic names)
|
|
284
316
|
- [ ] Used `App*` widgets where possible?
|
|
285
317
|
- [ ] Page broken down into `components/`?
|
|
286
318
|
- [ ] Logic separated into `interactor/`?
|
|
@@ -1,48 +1,20 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { createRequire } from 'node:module'
|
|
4
|
-
import { fileURLToPath } from 'node:url'
|
|
5
4
|
import { parse } from '@babel/parser'
|
|
6
5
|
import traverseModule from '@babel/traverse'
|
|
7
6
|
const traverse = traverseModule.default
|
|
8
7
|
import postcss from 'postcss'
|
|
9
8
|
import safeParser from 'postcss-safe-parser'
|
|
10
9
|
|
|
11
|
-
const
|
|
12
|
-
const __dirname = path.dirname(__filename)
|
|
10
|
+
const __dirname = path.dirname(new URL(import.meta.url).pathname)
|
|
13
11
|
const require = createRequire(import.meta.url)
|
|
14
12
|
let sass = null
|
|
15
13
|
try {
|
|
16
14
|
sass = require('sass')
|
|
17
15
|
} catch {}
|
|
18
16
|
|
|
19
|
-
|
|
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)
|
|
17
|
+
const REPO_ROOT = path.resolve(__dirname, '../..')
|
|
46
18
|
const APP_ASSETS_FILE = path.resolve(REPO_ROOT, 'lib/src/utils/app_assets.dart')
|
|
47
19
|
let ICONS_MAP = {}
|
|
48
20
|
try {
|
|
@@ -745,6 +717,47 @@ function textAlignFromProps(props) {
|
|
|
745
717
|
return null
|
|
746
718
|
}
|
|
747
719
|
|
|
720
|
+
function isInlineTextTag(tag) {
|
|
721
|
+
return tag === 'span' || tag === 'p'
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function mergeTextStyles(baseStyle, ownStyle) {
|
|
725
|
+
if (baseStyle && ownStyle) return `(${baseStyle}).merge(${ownStyle})`
|
|
726
|
+
return ownStyle || baseStyle || null
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function buildInlineTextSpans(el, cssMap, topClassName, skipRootDecoration, inheritedStyle = null) {
|
|
730
|
+
const className = getClassNameAttr(el)
|
|
731
|
+
const props = className && cssMap[className] ? cssMap[className] : {}
|
|
732
|
+
const style = mergeTextStyles(inheritedStyle, textStyleFromProps(props))
|
|
733
|
+
const spans = []
|
|
734
|
+
for (const c of el.children || []) {
|
|
735
|
+
if (isTextNode(c)) {
|
|
736
|
+
const text = c.value.trim()
|
|
737
|
+
if (!text) continue
|
|
738
|
+
const parts = [`text: ${toDartLiteral(text)}`]
|
|
739
|
+
if (style) parts.push(`style: ${style}`)
|
|
740
|
+
spans.push(`TextSpan(${parts.join(', ')})`)
|
|
741
|
+
continue
|
|
742
|
+
}
|
|
743
|
+
if (c.type !== 'JSXElement') continue
|
|
744
|
+
const childTag = jsxElementName(c).toLowerCase()
|
|
745
|
+
if (childTag === 'br') {
|
|
746
|
+
const parts = [`text: '\\n'`]
|
|
747
|
+
if (style) parts.push(`style: ${style}`)
|
|
748
|
+
spans.push(`TextSpan(${parts.join(', ')})`)
|
|
749
|
+
continue
|
|
750
|
+
}
|
|
751
|
+
if (isInlineTextTag(childTag)) {
|
|
752
|
+
spans.push(...buildInlineTextSpans(c, cssMap, topClassName, skipRootDecoration, style))
|
|
753
|
+
continue
|
|
754
|
+
}
|
|
755
|
+
const child = buildWidgetFromElement(c, cssMap, topClassName, skipRootDecoration)
|
|
756
|
+
spans.push(`WidgetSpan(child: ${child.widget})`)
|
|
757
|
+
}
|
|
758
|
+
return spans
|
|
759
|
+
}
|
|
760
|
+
|
|
748
761
|
function paddingFromProps(props) {
|
|
749
762
|
const p = props['padding']
|
|
750
763
|
const pt = props['padding-top']
|
|
@@ -881,15 +894,9 @@ function usesAppOwnedInputBackground(widget) {
|
|
|
881
894
|
return normalized.startsWith('AppInput(') || normalized.startsWith('_GeneratedDateTimeField(')
|
|
882
895
|
}
|
|
883
896
|
|
|
884
|
-
function usesAppOwnedProgressBarBackground(widget) {
|
|
885
|
-
if (!widget) return false
|
|
886
|
-
const normalized = String(widget).trim()
|
|
887
|
-
return normalized.startsWith('AppProgressBar(')
|
|
888
|
-
}
|
|
889
|
-
|
|
890
897
|
function toDartLiteral(s) {
|
|
891
898
|
const t = String(s || '')
|
|
892
|
-
return `'${t.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
|
|
899
|
+
return `'${t.replace(/\\/g, '\\\\').replace(/\$/g, '\\$').replace(/'/g, "\\'")}'`
|
|
893
900
|
}
|
|
894
901
|
|
|
895
902
|
function isTextNode(node) {
|
|
@@ -1176,6 +1183,13 @@ function simplifySvgWidget(widget, size) {
|
|
|
1176
1183
|
return `SizedBox(${width}${height}child: ${svgCall})`
|
|
1177
1184
|
}
|
|
1178
1185
|
|
|
1186
|
+
function applySizeToWidget(widget, size) {
|
|
1187
|
+
if (!size || (size.w == null && size.h == null)) return widget
|
|
1188
|
+
const width = size.w != null ? `width: ${size.w}, ` : ''
|
|
1189
|
+
const height = size.h != null ? `height: ${size.h}, ` : ''
|
|
1190
|
+
return `SizedBox(${width}${height}child: ${widget})`
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1179
1193
|
function shouldStripIconPositioning(size, position, rotate) {
|
|
1180
1194
|
return (position != null || rotate != null) && isSmallIconSize(size)
|
|
1181
1195
|
}
|
|
@@ -1297,92 +1311,6 @@ function buildRadioGroupWidgetFromElement(el, cssMap) {
|
|
|
1297
1311
|
return `_GeneratedRadioGroup(${widgetArgs.join(', ')})`
|
|
1298
1312
|
}
|
|
1299
1313
|
|
|
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
1314
|
function wrapOverflowIfSized(body, size) {
|
|
1387
1315
|
if (process.env.JSX2FLUTTER_ENABLE_OVERFLOW !== '1') return body
|
|
1388
1316
|
if (!size || (!size.w && !size.h)) return body
|
|
@@ -1413,8 +1341,20 @@ function wrapOverflowIfSized(body, size) {
|
|
|
1413
1341
|
return `OverflowBox(${overflowArgs.join(', ')}, child: ${body})`
|
|
1414
1342
|
}
|
|
1415
1343
|
|
|
1344
|
+
function unwrapDirectFlexWidget(widgetExpr) {
|
|
1345
|
+
let current = String(widgetExpr || '').trim()
|
|
1346
|
+
while (/^(?:const\s+)?(?:Expanded|Flexible)\s*\(/.test(current)) {
|
|
1347
|
+
const wrapperMatch = current.match(/^(?:const\s+)?(?:Expanded|Flexible)\s*\(([\s\S]*)\)$/)
|
|
1348
|
+
if (!wrapperMatch) break
|
|
1349
|
+
const childMatch = wrapperMatch[1].match(/\bchild:\s*([\s\S]+)$/)
|
|
1350
|
+
if (!childMatch) break
|
|
1351
|
+
current = childMatch[1].trim()
|
|
1352
|
+
}
|
|
1353
|
+
return current
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1416
1356
|
function buildGeneratedRippleButton(titleExpr) {
|
|
1417
|
-
return `RippleButton(title: ${titleExpr}, backgroundColor: Colors.transparent,
|
|
1357
|
+
return `RippleButton(title: ${titleExpr}, backgroundColor: Colors.transparent,minWidth : 0, padding: EdgeInsets.zero, onTap: () {})`
|
|
1418
1358
|
}
|
|
1419
1359
|
|
|
1420
1360
|
function buildWidgetFromElement(el, cssMap, topClassName, skipRootDecoration) {
|
|
@@ -1469,12 +1409,6 @@ function buildWidgetFromElement(el, cssMap, topClassName, skipRootDecoration) {
|
|
|
1469
1409
|
semanticWidget = radioGroupWidget
|
|
1470
1410
|
}
|
|
1471
1411
|
}
|
|
1472
|
-
if (!semanticWidget) {
|
|
1473
|
-
const progressBarWidget = buildProgressBarWidgetFromElement(el, cssMap)
|
|
1474
|
-
if (progressBarWidget) {
|
|
1475
|
-
semanticWidget = progressBarWidget
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
1412
|
if (!semanticWidget && looksLikeButtonClass(className)) {
|
|
1479
1413
|
const buttonLabel = uniqueTexts(extractTextsFromElement(el, [])).find(Boolean)
|
|
1480
1414
|
if (buttonLabel) {
|
|
@@ -1525,7 +1459,8 @@ function buildWidgetFromElement(el, cssMap, topClassName, skipRootDecoration) {
|
|
|
1525
1459
|
const src = getAttr(el, 'src') || ''
|
|
1526
1460
|
if (/^https?:\/\//.test(src)) {
|
|
1527
1461
|
const widget = `Image.network(${toDartLiteral(src)}, fit: BoxFit.cover)`
|
|
1528
|
-
|
|
1462
|
+
const sized = applySizeToWidget(widget, size)
|
|
1463
|
+
return { widget: rotate != null ? `Transform.rotate(angle: ${rotate}, child: ${sized})` : sized, isAbsolute: !!position, position, zIndex, flexGrow, size }
|
|
1529
1464
|
}
|
|
1530
1465
|
const local = resolveLocalAsset(src, className)
|
|
1531
1466
|
if (local) {
|
|
@@ -1538,7 +1473,8 @@ function buildWidgetFromElement(el, cssMap, topClassName, skipRootDecoration) {
|
|
|
1538
1473
|
const finalWidget = (rotate != null && !stripPlacement) ? `Transform.rotate(angle: ${rotate}, child: ${simplified})` : simplified
|
|
1539
1474
|
return { widget: finalWidget, isAbsolute: stripPlacement ? false : !!position, position: stripPlacement ? null : position, zIndex, flexGrow, size }
|
|
1540
1475
|
}
|
|
1541
|
-
|
|
1476
|
+
const sized = applySizeToWidget(widget, size)
|
|
1477
|
+
return { widget: rotate != null ? `Transform.rotate(angle: ${rotate}, child: ${sized})` : sized, isAbsolute: !!position, position, zIndex, flexGrow, size }
|
|
1542
1478
|
}
|
|
1543
1479
|
const asset = assetForClassName(className)
|
|
1544
1480
|
if (asset) {
|
|
@@ -1561,12 +1497,10 @@ function buildWidgetFromElement(el, cssMap, topClassName, skipRootDecoration) {
|
|
|
1561
1497
|
return { widget: rotate != null ? `Transform.rotate(angle: ${rotate}, child: ${widget})` : widget, isAbsolute: !!position, position, zIndex, flexGrow, size }
|
|
1562
1498
|
}
|
|
1563
1499
|
if (tag === 'span' || tag === 'p') {
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
const widget = children.length
|
|
1569
|
-
? `Row(children: [${children.map(c => c.widget).join(', ')}])`
|
|
1500
|
+
const align = textAlignFromProps(props)
|
|
1501
|
+
const spans = buildInlineTextSpans(el, cssMap, topClassName, skipRootDecoration)
|
|
1502
|
+
const widget = spans.length
|
|
1503
|
+
? `RichText(text: TextSpan(children: [${spans.join(', ')}])${align ? `, textAlign: ${align}` : ''})`
|
|
1570
1504
|
: 'SizedBox.shrink()'
|
|
1571
1505
|
return { widget: rotate != null ? `Transform.rotate(angle: ${rotate}, child: ${widget})` : widget, isAbsolute: !!position, position, zIndex, flexGrow, size }
|
|
1572
1506
|
}
|
|
@@ -1575,27 +1509,24 @@ function buildWidgetFromElement(el, cssMap, topClassName, skipRootDecoration) {
|
|
|
1575
1509
|
const flowKids = semanticWidget ? [] : children.filter(c => !c.isAbsolute)
|
|
1576
1510
|
const absKids = semanticWidget ? [] : children.filter(c => c.isAbsolute).sort((a, b) => (a.zIndex || 0) - (b.zIndex || 0))
|
|
1577
1511
|
const inputOwnedWidget = usesAppOwnedInputBackground(semanticWidget)
|
|
1578
|
-
const progressBarOwnedWidget = usesAppOwnedProgressBarBackground(semanticWidget)
|
|
1579
1512
|
const wrappedInputOwnedWidget = !semanticWidget && flowKids.length === 1 && usesAppOwnedInputBackground(flowKids[0].widget)
|
|
1580
|
-
const wrappedProgressBarOwnedWidget = !semanticWidget && flowKids.length === 1 && usesAppOwnedProgressBarBackground(flowKids[0].widget)
|
|
1581
1513
|
const appOwnedInputWidget = inputOwnedWidget || wrappedInputOwnedWidget
|
|
1582
|
-
const
|
|
1583
|
-
const appOwnedStyledWidget = appOwnedInputWidget || appOwnedProgressBarWidget
|
|
1584
|
-
const omitBackgroundColor = appOwnedStyledWidget
|
|
1514
|
+
const omitBackgroundColor = appOwnedInputWidget
|
|
1585
1515
|
const decoration = (skipRootDecoration && className === topClassName)
|
|
1586
1516
|
? null
|
|
1587
1517
|
: boxDecorationFromProps(props, { omitBackgroundColor })
|
|
1588
|
-
const effectivePadding =
|
|
1589
|
-
const effectiveDecoration =
|
|
1518
|
+
const effectivePadding = appOwnedInputWidget ? null : padding
|
|
1519
|
+
const effectiveDecoration = appOwnedInputWidget ? null : decoration
|
|
1590
1520
|
const layoutSize = (appOwnedInputWidget && size) ? { ...size, h: null } : size
|
|
1591
1521
|
const flex = flexConfigFromProps(props)
|
|
1592
1522
|
let flowBody = semanticWidget
|
|
1593
1523
|
if (!flowBody && flowKids.length) {
|
|
1594
1524
|
if (flex) {
|
|
1525
|
+
const isScrollableRoot = className === topClassName && process.env.JSX2FLUTTER_MODE !== 'classic'
|
|
1595
1526
|
const alignSelf = String(props['align-self'] || '').toLowerCase()
|
|
1596
|
-
const hasMainAxisSize = flex.isRow
|
|
1527
|
+
const hasMainAxisSize = !isScrollableRoot && (flex.isRow
|
|
1597
1528
|
? (layoutSize.w != null || alignSelf === 'stretch')
|
|
1598
|
-
: (layoutSize.h != null)
|
|
1529
|
+
: (layoutSize.h != null))
|
|
1599
1530
|
const gapLiteral = flex.gap ? (flex.isRow ? `${flex.gap}.width` : `${flex.gap}.height`) : null
|
|
1600
1531
|
const baseKids = flowKids.map(c => {
|
|
1601
1532
|
if (flex && c.flexGrow > 0 && hasMainAxisSize) {
|
|
@@ -1727,6 +1658,7 @@ function generateDart(ast, cssMap, outClassName, outPath) {
|
|
|
1727
1658
|
const rootDeco = boxDecorationFromProps(rootProps)
|
|
1728
1659
|
const mode = process.env.JSX2FLUTTER_MODE
|
|
1729
1660
|
let inner = rootWidget?.widget || 'const SizedBox.shrink()'
|
|
1661
|
+
inner = unwrapDirectFlexWidget(inner)
|
|
1730
1662
|
let finalRoot
|
|
1731
1663
|
if (mode === 'classic') {
|
|
1732
1664
|
finalRoot = inner
|
|
@@ -1752,35 +1684,31 @@ function generateDart(ast, cssMap, outClassName, outPath) {
|
|
|
1752
1684
|
const usesCheckbox = finalRoot.includes('_GeneratedCheckbox(') || finalRoot.includes('AppCheckbox(')
|
|
1753
1685
|
const usesRippleButton = finalRoot.includes('RippleButton(')
|
|
1754
1686
|
const usesTextGradient = finalRoot.includes('AppTextGradient(')
|
|
1755
|
-
const usesProgressBar = finalRoot.includes('AppProgressBar(')
|
|
1756
1687
|
const usesGetWidth = finalRoot.includes('Get.width')
|
|
1757
1688
|
const imports = [
|
|
1758
1689
|
"import 'package:flutter/material.dart';",
|
|
1759
|
-
|
|
1760
|
-
|
|
1690
|
+
"import 'package:link_home/src/utils/app_colors.dart';",
|
|
1691
|
+
"import 'package:link_home/src/extensions/int_extensions.dart';",
|
|
1761
1692
|
"import 'package:flutter_svg/flutter_svg.dart';",
|
|
1762
|
-
|
|
1693
|
+
"import 'package:link_home/src/utils/app_assets.dart';",
|
|
1763
1694
|
]
|
|
1764
1695
|
if (usesInput) {
|
|
1765
|
-
imports.push(
|
|
1696
|
+
imports.push("import 'package:link_home/src/ui/widgets/app_input.dart';")
|
|
1766
1697
|
}
|
|
1767
1698
|
if (usesDateTimeField) {
|
|
1768
|
-
imports.push(
|
|
1699
|
+
imports.push("import 'package:link_home/src/ui/widgets/app_input_full_time.dart';")
|
|
1769
1700
|
}
|
|
1770
1701
|
if (usesRadio) {
|
|
1771
|
-
imports.push(
|
|
1702
|
+
imports.push("import 'package:link_home/src/ui/widgets/app_radio_button.dart';")
|
|
1772
1703
|
}
|
|
1773
1704
|
if (usesCheckbox) {
|
|
1774
|
-
imports.push(
|
|
1705
|
+
imports.push("import 'package:link_home/src/ui/widgets/base/checkbox/app_checkbox.dart';")
|
|
1775
1706
|
}
|
|
1776
1707
|
if (usesRippleButton) {
|
|
1777
|
-
imports.push(
|
|
1708
|
+
imports.push("import 'package:link_home/src/ui/widgets/base/ripple_button.dart';")
|
|
1778
1709
|
}
|
|
1779
1710
|
if (usesTextGradient) {
|
|
1780
|
-
imports.push(
|
|
1781
|
-
}
|
|
1782
|
-
if (usesProgressBar) {
|
|
1783
|
-
imports.push(`import 'package:${PROJECT_PACKAGE_NAME}/src/ui/widgets/app_progressbar.dart';`)
|
|
1711
|
+
imports.push("import 'package:link_home/src/ui/widgets/app_text_gradient.dart';")
|
|
1784
1712
|
}
|
|
1785
1713
|
if (usesGetWidth) {
|
|
1786
1714
|
imports.push("import 'package:get/get.dart';")
|