@atlashub/smartstack-cli 4.80.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.documentation/agents.html +124 -585
- package/.documentation/ba-develop.html +852 -0
- package/.documentation/ba-skills.html +465 -0
- package/.documentation/business-analyse.html +385 -1570
- package/.documentation/cli-commands.html +162 -799
- package/.documentation/commands.html +902 -1338
- package/.documentation/css/styles.css +34 -1
- package/.documentation/efcore.html +161 -2599
- package/.documentation/gitflow.html +62 -105
- package/.documentation/hooks.html +94 -343
- package/.documentation/index.html +116 -385
- package/.documentation/init.html +217 -1566
- package/.documentation/installation.html +121 -1470
- package/.documentation/license.html +90 -450
- package/.documentation/ralph-loop.html +105 -602
- package/dist/index.js +9421 -79036
- package/dist/index.js.map +1 -1
- package/package.json +5 -20
- package/scripts/generate-docs/README.md +87 -0
- package/scripts/generate-docs/index.ts +175 -0
- package/scripts/generate-docs/lib/context-builder.ts +81 -0
- package/scripts/generate-docs/lib/handlebars-setup.ts +162 -0
- package/scripts/generate-docs/lib/markdown-parser.ts +86 -0
- package/scripts/generate-docs/lib/sidebar-builder.ts +80 -0
- package/scripts/generate-docs/lib/skill-parser.ts +171 -0
- package/scripts/generate-docs/lib/stats.ts +32 -0
- package/scripts/generate-docs/lib/version.ts +17 -0
- package/scripts/generate-docs/templates/layout.hbs +33 -0
- package/scripts/generate-docs/templates/pages/_generic.hbs +12 -0
- package/scripts/generate-docs/templates/pages/ba-develop.hbs +10 -0
- package/scripts/generate-docs/templates/pages/ba-skills.hbs +8 -0
- package/scripts/generate-docs/templates/pages/business-analyse.hbs +1 -0
- package/scripts/generate-docs/templates/pages/commands.hbs +13 -0
- package/scripts/generate-docs/templates/pages/gitflow.hbs +2164 -0
- package/scripts/generate-docs/templates/pages/index.hbs +5 -0
- package/scripts/generate-docs/templates/partials/breadcrumb.hbs +6 -0
- package/scripts/generate-docs/templates/partials/header.hbs +22 -0
- package/scripts/generate-docs/templates/partials/sidebar.hbs +32 -0
- package/scripts/generate-docs/templates/partials/skill-card.hbs +22 -0
- package/scripts/generate-docs/templates/partials/skill-grid.hbs +5 -0
- package/scripts/generate-docs/templates/partials/skill-table.hbs +18 -0
- package/scripts/generate-docs/templates/partials/stats-bar.hbs +20 -0
- package/scripts/test-migration-program-cs.mts +94 -0
- package/templates/agents/explore-codebase.md +2 -3
- package/templates/agents/explore-docs.md +5 -5
- package/templates/hooks/hooks.json +0 -9
- package/templates/project/Program.cs.template +17 -5
- package/templates/project/appsettings.json.template +208 -195
- package/templates/project/claude-md/api.CLAUDE.md.template +27 -2
- package/templates/project/patch-smartstack-theme.cjs.template +42 -0
- package/templates/scripts/statusline/README.md +47 -0
- package/templates/scripts/statusline/index.js +224 -0
- package/templates/skills/CLAUDE.md +235 -0
- package/templates/skills/ba-develop/SKILL.md +310 -0
- package/templates/skills/ba-develop/cli/compute-page-diff/__tests__/compute-page-diff.test.ts +177 -0
- package/templates/skills/ba-develop/cli/compute-page-diff/compute-diff.ts +51 -0
- package/templates/skills/ba-develop/cli/compute-page-diff/disk-drift.ts +55 -0
- package/templates/skills/ba-develop/cli/compute-page-diff/index.ts +89 -0
- package/templates/skills/ba-develop/cli/compute-page-diff/scan-pagespecs.ts +115 -0
- package/templates/skills/ba-develop/cli/compute-page-diff/types.ts +63 -0
- package/templates/skills/ba-develop/cli/compute-page-diff/validate.ts +20 -0
- package/templates/skills/ba-develop/cli/update-snapshot/__tests__/update-snapshot.test.ts +73 -0
- package/templates/skills/ba-develop/cli/update-snapshot/execute.ts +24 -0
- package/templates/skills/ba-develop/cli/update-snapshot/index.ts +61 -0
- package/templates/skills/ba-develop/cli/update-snapshot/types.ts +40 -0
- package/templates/skills/ba-develop/cli/update-snapshot/validate.ts +17 -0
- package/templates/skills/ba-develop/references/anti-patterns.md +101 -0
- package/templates/skills/ba-develop/references/auto-healing.md +191 -0
- package/templates/skills/ba-develop/references/commit-checkpoints.md +79 -0
- package/templates/skills/ba-develop/references/gates.md +380 -0
- package/templates/skills/ba-develop/references/output-contract.md +95 -0
- package/templates/skills/ba-develop/references/phases-detail.md +592 -0
- package/templates/skills/ba-develop-plan/SKILL.md +239 -0
- package/templates/skills/ba-develop-plan/cli/preflight-develop-plan/__tests__/validate.test.ts +225 -0
- package/templates/skills/ba-develop-plan/cli/preflight-develop-plan/index.ts +102 -0
- package/templates/skills/ba-develop-plan/cli/preflight-develop-plan/types.ts +121 -0
- package/templates/skills/ba-develop-plan/cli/preflight-develop-plan/validate.ts +261 -0
- package/templates/skills/business-analyse/_workflow/README.md +34 -0
- package/templates/skills/business-analyse/_workflow/ba-files.md +174 -0
- package/templates/skills/business-analyse/_workflow/code-discipline.md +104 -0
- package/templates/skills/business-analyse/_workflow/communication.md +63 -0
- package/templates/skills/business-analyse/_workflow/completeAuto-discipline.md +79 -0
- package/templates/skills/business-analyse/_workflow/context-documents.md +45 -0
- package/templates/skills/business-analyse/_workflow/doc-templates.md +318 -0
- package/templates/skills/business-analyse/audit-actors/SKILL.md +97 -0
- package/templates/skills/business-analyse/audit-cross-dimension/SKILL.md +127 -0
- package/templates/skills/business-analyse/audit-cross-ref-code/SKILL.md +119 -0
- package/templates/skills/business-analyse/audit-data-model/SKILL.md +343 -0
- package/templates/skills/business-analyse/audit-menu/SKILL.md +97 -0
- package/templates/skills/business-analyse/audit-prd/SKILL.md +479 -0
- package/templates/skills/business-analyse/audit-pre-dev/SKILL.md +135 -0
- package/templates/skills/business-analyse/audit-rbac/SKILL.md +93 -0
- package/templates/skills/business-analyse/audit-rules/SKILL.md +182 -0
- package/templates/skills/business-analyse/audit-screens/SKILL.md +169 -0
- package/templates/skills/business-analyse/audit-sections/SKILL.md +174 -0
- package/templates/skills/business-analyse/audit-use-cases/SKILL.md +245 -0
- package/templates/skills/business-analyse/create-actors/SKILL.md +129 -0
- package/templates/skills/business-analyse/create-ba-order/SKILL.md +182 -0
- package/templates/skills/business-analyse/create-ba-order/cli/create-ba-order/__tests__/generate.test.ts +151 -0
- package/templates/skills/business-analyse/create-ba-order/cli/create-ba-order/__tests__/graph.test.ts +173 -0
- package/templates/skills/business-analyse/create-ba-order/cli/create-ba-order/generate.ts +273 -0
- package/templates/skills/business-analyse/create-ba-order/cli/create-ba-order/graph.ts +193 -0
- package/templates/skills/business-analyse/create-ba-order/cli/create-ba-order/index.ts +108 -0
- package/templates/skills/business-analyse/create-ba-order/cli/create-ba-order/types.ts +106 -0
- package/templates/skills/business-analyse/create-ba-order/cli/create-ba-order/validate.ts +79 -0
- package/templates/skills/business-analyse/create-business-rules/SKILL.md +302 -0
- package/templates/skills/business-analyse/create-business-rules/levels/access-rules.md +105 -0
- package/templates/skills/business-analyse/create-business-rules/levels/elaborate.md +193 -0
- package/templates/skills/business-analyse/create-business-rules/levels/identify.md +157 -0
- package/templates/skills/business-analyse/create-business-rules/levels/link.md +86 -0
- package/templates/skills/business-analyse/create-data-model/SKILL.md +319 -0
- package/templates/skills/business-analyse/create-data-model/levels/attributes.md +130 -0
- package/templates/skills/business-analyse/create-data-model/levels/identify.md +100 -0
- package/templates/skills/business-analyse/create-data-model/levels/relationships.md +97 -0
- package/templates/skills/business-analyse/create-menu/SKILL.md +191 -0
- package/templates/skills/business-analyse/create-menu/levels/applications.md +85 -0
- package/templates/skills/business-analyse/create-menu/levels/modules.md +81 -0
- package/templates/skills/business-analyse/create-menu/levels/resources.md +75 -0
- package/templates/skills/business-analyse/create-menu/levels/sections.md +82 -0
- package/templates/skills/business-analyse/create-plan-development/SKILL.md +93 -0
- package/templates/skills/business-analyse/create-plan-development/cli/create-plan-development/__tests__/graph.test.ts +271 -0
- package/templates/skills/business-analyse/create-plan-development/cli/create-plan-development/__tests__/parse.test.ts +177 -0
- package/templates/skills/business-analyse/create-plan-development/cli/create-plan-development/generate.ts +317 -0
- package/templates/skills/business-analyse/create-plan-development/cli/create-plan-development/graph.ts +233 -0
- package/templates/skills/business-analyse/create-plan-development/cli/create-plan-development/index.ts +106 -0
- package/templates/skills/business-analyse/create-plan-development/cli/create-plan-development/parse.ts +346 -0
- package/templates/skills/business-analyse/create-plan-development/cli/create-plan-development/types.ts +160 -0
- package/templates/skills/business-analyse/create-plan-development/cli/create-plan-development/validate.ts +118 -0
- package/templates/skills/business-analyse/create-prd/SKILL.md +228 -0
- package/templates/skills/business-analyse/create-rbac/SKILL.md +255 -0
- package/templates/skills/business-analyse/create-rbac/levels/detail.md +86 -0
- package/templates/skills/business-analyse/create-rbac/levels/discovery.md +63 -0
- package/templates/skills/business-analyse/create-rbac/levels/review.md +66 -0
- package/templates/skills/business-analyse/create-screen/SKILL.md +386 -0
- package/templates/skills/business-analyse/create-screen/levels/dashboard-screens.md +94 -0
- package/templates/skills/business-analyse/create-screen/levels/form-screens.md +142 -0
- package/templates/skills/business-analyse/create-screen/levels/home-screens.md +151 -0
- package/templates/skills/business-analyse/create-screen/levels/kanban-screens.md +86 -0
- package/templates/skills/business-analyse/create-screen/levels/list-screens.md +134 -0
- package/templates/skills/business-analyse/create-screen/references/post-check.md +101 -0
- package/templates/skills/business-analyse/create-screen/references/react-templates.md +252 -0
- package/templates/skills/business-analyse/create-screen/references/smartcomponents.md +419 -0
- package/templates/skills/business-analyse/create-screen/references/type-mapping.md +150 -0
- package/templates/skills/business-analyse/create-use-case/SKILL.md +347 -0
- package/templates/skills/business-analyse/create-use-case/levels/detail.md +136 -0
- package/templates/skills/business-analyse/create-use-case/levels/discovery.md +110 -0
- package/templates/skills/business-analyse/loop/SKILL.md +401 -0
- package/templates/skills/business-analyse/modeling-detail/SKILL.md +241 -0
- package/templates/skills/business-analyse/modeling-inventory/SKILL.md +174 -0
- package/templates/skills/business-analyse/reconcile-menu/SKILL.md +180 -0
- package/templates/skills/business-analyse/reconcile-menu/cli/reconcile-menu/__tests__/clean.test.ts +266 -0
- package/templates/skills/business-analyse/reconcile-menu/cli/reconcile-menu/__tests__/detect.test.ts +231 -0
- package/templates/skills/business-analyse/reconcile-menu/cli/reconcile-menu/__tests__/scan.test.ts +154 -0
- package/templates/skills/business-analyse/reconcile-menu/cli/reconcile-menu/clean.ts +319 -0
- package/templates/skills/business-analyse/reconcile-menu/cli/reconcile-menu/detect.ts +256 -0
- package/templates/skills/business-analyse/reconcile-menu/cli/reconcile-menu/index.ts +126 -0
- package/templates/skills/business-analyse/reconcile-menu/cli/reconcile-menu/scan.ts +175 -0
- package/templates/skills/business-analyse/reconcile-menu/cli/reconcile-menu/types.ts +136 -0
- package/templates/skills/business-analyse/reconcile-menu/cli/reconcile-menu/validate.ts +32 -0
- package/templates/skills/check-version/SKILL.md +196 -196
- package/templates/skills/cli-app-sync/SKILL.md +9 -9
- package/templates/skills/cli-app-sync/references/comparison-map.md +4 -4
- package/templates/skills/cli-app-sync/references/diff-entities.md +6 -6
- package/templates/skills/conventions/SKILL.md +64 -0
- package/templates/skills/dev-start/SKILL.md +190 -237
- package/templates/skills/development/SKILL.md +87 -0
- package/templates/skills/development/audit/SKILL.md +156 -0
- package/templates/skills/development/audit/routing-dynamic/SKILL.md +196 -0
- package/templates/skills/development/audit-dev-api/SKILL.md +331 -0
- package/templates/skills/development/audit-dev-api/cli/audit-dev-api/__tests__/end-to-end.test.ts +364 -0
- package/templates/skills/development/audit-dev-api/cli/audit-dev-api/audit.ts +646 -0
- package/templates/skills/development/audit-dev-api/cli/audit-dev-api/index.ts +140 -0
- package/templates/skills/development/audit-dev-api/cli/audit-dev-api/types.ts +158 -0
- package/templates/skills/development/audit-dev-api/cli/audit-dev-api/validate.ts +45 -0
- package/templates/skills/development/audit-dev-core/SKILL.md +182 -0
- package/templates/skills/development/audit-dev-data/SKILL.md +195 -0
- package/templates/skills/development/audit-dev-domain/SKILL.md +184 -0
- package/templates/skills/development/audit-dev-frontend/SKILL.md +530 -0
- package/templates/skills/development/audit-dev-frontend/cli/audit-dev-actions-alignment/__tests__/end-to-end.test.ts +202 -0
- package/templates/skills/development/audit-dev-frontend/cli/audit-dev-actions-alignment/apply.ts +31 -0
- package/templates/skills/development/audit-dev-frontend/cli/audit-dev-actions-alignment/audit.ts +734 -0
- package/templates/skills/development/audit-dev-frontend/cli/audit-dev-actions-alignment/index.ts +125 -0
- package/templates/skills/development/audit-dev-frontend/cli/audit-dev-actions-alignment/types.ts +165 -0
- package/templates/skills/development/audit-dev-frontend/cli/audit-dev-actions-alignment/validate.ts +36 -0
- package/templates/skills/development/audit-dev-frontend/cli/audit-dev-frontend/__tests__/dev-ui-022.test.ts +193 -0
- package/templates/skills/development/audit-dev-frontend/cli/audit-dev-frontend/apply.ts +374 -0
- package/templates/skills/development/audit-dev-frontend/cli/audit-dev-frontend/audit.ts +1126 -0
- package/templates/skills/development/audit-dev-frontend/cli/audit-dev-frontend/index.ts +141 -0
- package/templates/skills/development/audit-dev-frontend/cli/audit-dev-frontend/types.ts +218 -0
- package/templates/skills/development/audit-dev-frontend/cli/audit-dev-frontend/validate.ts +80 -0
- package/templates/skills/development/audit-dev-tests/SKILL.md +82 -0
- package/templates/skills/development/audit-dev-tests/cli/audit-dev-tests/__tests__/audit.test.ts +220 -0
- package/templates/skills/development/audit-dev-tests/cli/audit-dev-tests/audit.ts +185 -0
- package/templates/skills/development/audit-dev-tests/cli/audit-dev-tests/index.ts +84 -0
- package/templates/skills/development/audit-dev-tests/cli/audit-dev-tests/types.ts +48 -0
- package/templates/skills/development/audit-dev-tests/cli/audit-dev-tests/validate.ts +36 -0
- package/templates/skills/development/audit-dev-wire/SKILL.md +144 -0
- package/templates/skills/development/audit-dev-wire/cli/audit-dev-wire/__tests__/audit.test.ts +171 -0
- package/templates/skills/development/audit-dev-wire/cli/audit-dev-wire/audit.ts +307 -0
- package/templates/skills/development/audit-dev-wire/cli/audit-dev-wire/index.ts +139 -0
- package/templates/skills/development/audit-dev-wire/cli/audit-dev-wire/types.ts +110 -0
- package/templates/skills/development/audit-dev-wire/cli/audit-dev-wire/validate.ts +16 -0
- package/templates/skills/development/backend/business-layer/SKILL.md +255 -0
- package/templates/skills/development/backend/business-layer/cli/scaffold-business/__tests__/generate.test.ts +254 -0
- package/templates/skills/development/backend/business-layer/cli/scaffold-business/generate.ts +842 -0
- package/templates/skills/development/backend/business-layer/cli/scaffold-business/index.ts +56 -0
- package/templates/skills/development/backend/business-layer/cli/scaffold-business/types.ts +112 -0
- package/templates/skills/development/backend/business-layer/cli/scaffold-business/validate.ts +24 -0
- package/templates/skills/development/backend/controller/SKILL.md +154 -0
- package/templates/skills/development/backend/controller/cli/scaffold-controller/__tests__/generate.test.ts +345 -0
- package/templates/skills/development/backend/controller/cli/scaffold-controller/generate.ts +280 -0
- package/templates/skills/development/backend/controller/cli/scaffold-controller/index.ts +49 -0
- package/templates/skills/development/backend/controller/cli/scaffold-controller/types.ts +72 -0
- package/templates/skills/development/backend/controller/cli/scaffold-controller/validate.ts +14 -0
- package/templates/skills/development/backend/core-seed/SKILL.md +177 -0
- package/templates/skills/development/backend/core-seed/cli/scaffold-core-seed/__tests__/build-spec.test.ts +163 -0
- package/templates/skills/development/backend/core-seed/cli/scaffold-core-seed/__tests__/generate.test.ts +330 -0
- package/templates/skills/development/backend/core-seed/cli/scaffold-core-seed/__tests__/validate.test.ts +126 -0
- package/templates/skills/development/backend/core-seed/cli/scaffold-core-seed/build-spec.ts +287 -0
- package/templates/skills/development/backend/core-seed/cli/scaffold-core-seed/generate.ts +826 -0
- package/templates/skills/development/backend/core-seed/cli/scaffold-core-seed/index.ts +188 -0
- package/templates/skills/development/backend/core-seed/cli/scaffold-core-seed/types.ts +163 -0
- package/templates/skills/development/backend/core-seed/cli/scaffold-core-seed/validate.ts +129 -0
- package/templates/skills/development/backend/data-layer/SKILL.md +163 -0
- package/templates/skills/development/backend/data-layer/cli/scaffold-entity/__tests__/generate.test.ts +155 -0
- package/templates/skills/development/backend/data-layer/cli/scaffold-entity/generate.ts +232 -0
- package/templates/skills/development/backend/data-layer/cli/scaffold-entity/index.ts +34 -0
- package/templates/skills/development/backend/data-layer/cli/scaffold-entity/types.ts +60 -0
- package/templates/skills/development/backend/data-layer/cli/scaffold-entity/validate.ts +42 -0
- package/templates/skills/development/backend/data-layer/cli/scaffold-migration/execute.ts +13 -0
- package/templates/skills/development/backend/data-layer/cli/scaffold-migration/index.ts +25 -0
- package/templates/skills/development/backend/data-layer/cli/scaffold-migration/types.ts +11 -0
- package/templates/skills/development/backend/data-layer/cli/scaffold-migration/validate.ts +9 -0
- package/templates/skills/development/backend/screen-controller/SKILL.md +169 -0
- package/templates/skills/development/backend/screen-controller/cli/scaffold-screen-controller/__tests__/generate.test.ts +329 -0
- package/templates/skills/development/backend/screen-controller/cli/scaffold-screen-controller/__tests__/hub-views.test.ts +105 -0
- package/templates/skills/development/backend/screen-controller/cli/scaffold-screen-controller/__tests__/parse-pagespec.test.ts +137 -0
- package/templates/skills/development/backend/screen-controller/cli/scaffold-screen-controller/generate.ts +437 -0
- package/templates/skills/development/backend/screen-controller/cli/scaffold-screen-controller/index.ts +108 -0
- package/templates/skills/development/backend/screen-controller/cli/scaffold-screen-controller/parse-pagespec.ts +104 -0
- package/templates/skills/development/backend/screen-controller/cli/scaffold-screen-controller/types.ts +101 -0
- package/templates/skills/development/backend/screen-controller/cli/scaffold-screen-controller/validate.ts +26 -0
- package/templates/skills/development/backend/seed-data/SKILL.md +91 -0
- package/templates/skills/development/backend/seed-data/cli/scaffold-seed/generate.ts +471 -0
- package/templates/skills/development/backend/seed-data/cli/scaffold-seed/index.ts +74 -0
- package/templates/skills/development/backend/seed-data/cli/scaffold-seed/types.ts +104 -0
- package/templates/skills/development/backend/seed-data/cli/scaffold-seed/validate.ts +63 -0
- package/templates/skills/development/backend/structure/SKILL.md +47 -0
- package/templates/skills/development/debug/SKILL.md +62 -0
- package/templates/skills/development/debug/audit-bug/SKILL.md +185 -0
- package/templates/skills/development/debug/backend/SKILL.md +114 -0
- package/templates/skills/development/debug/discuss-bug/SKILL.md +193 -0
- package/templates/skills/development/debug/fix-bug/SKILL.md +172 -0
- package/templates/skills/development/debug/frontend/SKILL.md +215 -0
- package/templates/skills/development/frontend/api-client/SKILL.md +158 -0
- package/templates/skills/development/frontend/api-client/cli/scaffold-api-client/__tests__/generate.test.ts +1180 -0
- package/templates/skills/development/frontend/api-client/cli/scaffold-api-client/__tests__/screen-strata-contract.test.ts +261 -0
- package/templates/skills/development/frontend/api-client/cli/scaffold-api-client/__tests__/url-parity.test.ts +201 -0
- package/templates/skills/development/frontend/api-client/cli/scaffold-api-client/generate.ts +875 -0
- package/templates/skills/development/frontend/api-client/cli/scaffold-api-client/index.ts +36 -0
- package/templates/skills/development/frontend/api-client/cli/scaffold-api-client/types.ts +251 -0
- package/templates/skills/development/frontend/api-client/cli/scaffold-api-client/validate.ts +10 -0
- package/templates/skills/development/frontend/auth/SKILL.md +77 -0
- package/templates/skills/development/frontend/auth/cli/scaffold-frontend-auth/generate.ts +306 -0
- package/templates/skills/development/frontend/auth/cli/scaffold-frontend-auth/index.ts +179 -0
- package/templates/skills/development/frontend/auth/cli/scaffold-frontend-auth/types.ts +22 -0
- package/templates/skills/development/frontend/auth/cli/scaffold-frontend-auth/validate.ts +37 -0
- package/templates/skills/development/frontend/component/SKILL.md +347 -0
- package/templates/skills/development/frontend/component/cli/scaffold-component/__tests__/generate.test.ts +1237 -0
- package/templates/skills/development/frontend/component/cli/scaffold-component/generate.ts +1923 -0
- package/templates/skills/development/frontend/component/cli/scaffold-component/index.ts +155 -0
- package/templates/skills/development/frontend/component/cli/scaffold-component/types.ts +290 -0
- package/templates/skills/development/frontend/component/cli/scaffold-component/validate.ts +16 -0
- package/templates/skills/development/frontend/component/cli/validate-page/__tests__/execute.test.ts +231 -0
- package/templates/skills/development/frontend/component/cli/validate-page/execute.ts +598 -0
- package/templates/skills/development/frontend/component/cli/validate-page/index.ts +88 -0
- package/templates/skills/development/frontend/component/cli/validate-page/types.ts +58 -0
- package/templates/skills/development/frontend/component/cli/validate-page/validate.ts +28 -0
- package/templates/skills/development/frontend/component/patterns/README.md +42 -0
- package/templates/skills/development/frontend/component/patterns/detail-page.md +133 -0
- package/templates/skills/development/frontend/component/patterns/entity-card.md +148 -0
- package/templates/skills/development/frontend/component/patterns/form-page.md +191 -0
- package/templates/skills/development/frontend/component/patterns/kanban-board.md +195 -0
- package/templates/skills/development/frontend/component/patterns/list-page.md +175 -0
- package/templates/skills/development/frontend/extension-config/SKILL.md +108 -0
- package/templates/skills/development/frontend/extension-config/cli/scaffold-extension-config/generate.ts +64 -0
- package/templates/skills/development/frontend/extension-config/cli/scaffold-extension-config/index.ts +70 -0
- package/templates/skills/development/frontend/extension-config/cli/scaffold-extension-config/types.ts +27 -0
- package/templates/skills/development/frontend/extension-config/cli/scaffold-extension-config/validate.ts +16 -0
- package/templates/skills/development/frontend/layout/SKILL.md +52 -0
- package/templates/skills/development/frontend/layout/cli/scaffold-layout/__tests__/generate.test.ts +66 -0
- package/templates/skills/development/frontend/layout/cli/scaffold-layout/generate.ts +175 -0
- package/templates/skills/development/frontend/layout/cli/scaffold-layout/index.ts +104 -0
- package/templates/skills/development/frontend/layout/cli/scaffold-layout/types.ts +17 -0
- package/templates/skills/development/frontend/layout/cli/scaffold-layout/validate.ts +37 -0
- package/templates/skills/development/frontend/routes/SKILL.md +152 -0
- package/templates/skills/development/frontend/routes/cli/aggregate-component-registry/generate.ts +216 -0
- package/templates/skills/development/frontend/routes/cli/aggregate-component-registry/index.ts +121 -0
- package/templates/skills/development/frontend/routes/cli/aggregate-component-registry/types.ts +69 -0
- package/templates/skills/development/frontend/routes/cli/aggregate-component-registry/validate.ts +23 -0
- package/templates/skills/development/frontend/routes/cli/scaffold-routes/__tests__/generate.test.ts +292 -0
- package/templates/skills/development/frontend/routes/cli/scaffold-routes/generate.ts +270 -0
- package/templates/skills/development/frontend/routes/cli/scaffold-routes/index.ts +42 -0
- package/templates/skills/development/frontend/routes/cli/scaffold-routes/types.ts +114 -0
- package/templates/skills/development/frontend/routes/cli/scaffold-routes/validate.ts +68 -0
- package/templates/skills/development/frontend/structure/SKILL.md +119 -0
- package/templates/skills/development/frontend/theme/SKILL.md +48 -0
- package/templates/skills/development/frontend/theme/cli/scaffold-theme/__tests__/generate.test.ts +53 -0
- package/templates/skills/development/frontend/theme/cli/scaffold-theme/generate.ts +129 -0
- package/templates/skills/development/frontend/theme/cli/scaffold-theme/index.ts +102 -0
- package/templates/skills/development/frontend/theme/cli/scaffold-theme/types.ts +48 -0
- package/templates/skills/development/frontend/theme/cli/scaffold-theme/validate.ts +37 -0
- package/templates/skills/development/frontend/ui-polish/SKILL.md +192 -0
- package/templates/skills/development/frontend/ui-polish/cli/ui-polish/__tests__/r18.test.ts +153 -0
- package/templates/skills/development/frontend/ui-polish/cli/ui-polish/__tests__/r19.test.ts +307 -0
- package/templates/skills/development/frontend/ui-polish/cli/ui-polish/__tests__/r20.test.ts +167 -0
- package/templates/skills/development/frontend/ui-polish/cli/ui-polish/__tests__/shared-scan.test.ts +262 -0
- package/templates/skills/development/frontend/ui-polish/cli/ui-polish/apply.ts +580 -0
- package/templates/skills/development/frontend/ui-polish/cli/ui-polish/audit.ts +825 -0
- package/templates/skills/development/frontend/ui-polish/cli/ui-polish/index.ts +133 -0
- package/templates/skills/development/frontend/ui-polish/cli/ui-polish/types.ts +121 -0
- package/templates/skills/development/frontend/ui-polish/cli/ui-polish/validate.ts +73 -0
- package/templates/skills/development/frontend/ui-polish/tokens.json +292 -0
- package/templates/skills/development/frontend/ui-primitives/SKILL.md +88 -0
- package/templates/skills/development/frontend/ui-primitives/cli/scaffold-ui-primitives/__tests__/generate.test.ts +158 -0
- package/templates/skills/development/frontend/ui-primitives/cli/scaffold-ui-primitives/generate.ts +345 -0
- package/templates/skills/development/frontend/ui-primitives/cli/scaffold-ui-primitives/index.ts +142 -0
- package/templates/skills/development/frontend/ui-primitives/cli/scaffold-ui-primitives/types.ts +27 -0
- package/templates/skills/development/frontend/ui-primitives/cli/scaffold-ui-primitives/validate.ts +37 -0
- package/templates/skills/development/run/SKILL.md +61 -0
- package/templates/skills/development/run/backend/SKILL.md +106 -0
- package/templates/skills/development/run/frontend/SKILL.md +116 -0
- package/templates/skills/development/smoke-test/SKILL.md +99 -0
- package/templates/skills/development/smoke-test/cli/run-smoke/execute.ts +424 -0
- package/templates/skills/development/smoke-test/cli/run-smoke/index.ts +75 -0
- package/templates/skills/development/smoke-test/cli/run-smoke/types.ts +100 -0
- package/templates/skills/development/testing/SKILL.md +148 -0
- package/templates/skills/development/testing/cli/scaffold-tests/generate.ts +530 -0
- package/templates/skills/development/testing/cli/scaffold-tests/index.ts +83 -0
- package/templates/skills/development/testing/cli/scaffold-tests/types.ts +51 -0
- package/templates/skills/development/testing/cli/scaffold-tests/validate.ts +33 -0
- package/templates/skills/development/testing/cli/scaffold-tests-from-ac/__tests__/generate.test.ts +188 -0
- package/templates/skills/development/testing/cli/scaffold-tests-from-ac/__tests__/parse-ac.test.ts +191 -0
- package/templates/skills/development/testing/cli/scaffold-tests-from-ac/generate.ts +190 -0
- package/templates/skills/development/testing/cli/scaffold-tests-from-ac/index.ts +138 -0
- package/templates/skills/development/testing/cli/scaffold-tests-from-ac/parse-ac.ts +172 -0
- package/templates/skills/development/testing/cli/scaffold-tests-from-ac/types.ts +104 -0
- package/templates/skills/development/testing/cli/scaffold-tests-from-ac/validate.ts +57 -0
- package/templates/skills/development/testing/cli/test-report/execute.ts +140 -0
- package/templates/skills/development/testing/cli/test-report/index.ts +96 -0
- package/templates/skills/development/testing/cli/test-report/types.ts +52 -0
- package/templates/skills/development/testing/cli/test-report/validate.ts +26 -0
- package/templates/skills/development/testing/fix-build/SKILL.md +81 -0
- package/templates/skills/development/testing/smoke-http/SKILL.md +123 -0
- package/templates/skills/development/testing/smoke-http/cli/smoke-http/execute.ts +129 -0
- package/templates/skills/development/testing/smoke-http/cli/smoke-http/index.ts +113 -0
- package/templates/skills/development/testing/smoke-http/cli/smoke-http/types.ts +62 -0
- package/templates/skills/development/testing/smoke-http/cli/smoke-http/validate.ts +36 -0
- package/templates/skills/development/testing/ui-test/SKILL.md +128 -0
- package/templates/skills/development/testing/ui-test/cli/build-manifest/generate.ts +129 -0
- package/templates/skills/development/testing/ui-test/cli/build-manifest/index.ts +80 -0
- package/templates/skills/development/testing/ui-test/cli/build-manifest/types.ts +72 -0
- package/templates/skills/development/testing/ui-test/cli/build-manifest/validate.ts +44 -0
- package/templates/skills/development/testing/ui-test/cli/run-ui-test/execute.ts +136 -0
- package/templates/skills/development/testing/ui-test/cli/run-ui-test/index.ts +75 -0
- package/templates/skills/development/testing/ui-test/cli/run-ui-test/lib/dev-browser-driver.ts +250 -0
- package/templates/skills/development/testing/ui-test/cli/run-ui-test/templates/delete.js.hbs +83 -0
- package/templates/skills/development/testing/ui-test/cli/run-ui-test/templates/detail.js.hbs +87 -0
- package/templates/skills/development/testing/ui-test/cli/run-ui-test/templates/edit.js.hbs +91 -0
- package/templates/skills/development/testing/ui-test/cli/run-ui-test/templates/form-submit.js.hbs +82 -0
- package/templates/skills/development/testing/ui-test/cli/run-ui-test/templates/list.js.hbs +125 -0
- package/templates/skills/development/testing/ui-test/cli/run-ui-test/templates/permission-negative.js.hbs +65 -0
- package/templates/skills/development/testing/ui-test/cli/run-ui-test/types.ts +57 -0
- package/templates/skills/development/testing/ui-test/cli/run-ui-test/validate.ts +56 -0
- package/templates/skills/documentation/SKILL.md +168 -139
- package/templates/skills/documentation/cli/extract-doc/__tests__/forbidden.test.ts +136 -0
- package/templates/skills/documentation/cli/extract-doc/__tests__/overflow.test.ts +76 -0
- package/templates/skills/documentation/cli/extract-doc/__tests__/required.test.ts +147 -0
- package/templates/skills/documentation/cli/extract-doc/extract.ts +657 -0
- package/templates/skills/documentation/cli/extract-doc/index.ts +102 -0
- package/templates/skills/documentation/cli/extract-doc/types.ts +133 -0
- package/templates/skills/documentation/cli/extract-doc/validate.ts +35 -0
- package/templates/skills/documentation/cli/scaffold-doc/generate.ts +198 -0
- package/templates/skills/documentation/cli/scaffold-doc/index.ts +61 -0
- package/templates/skills/documentation/cli/scaffold-doc/types.ts +72 -0
- package/templates/skills/documentation/cli/scaffold-doc/validate.ts +33 -0
- package/templates/skills/documentation/data-schema.md +18 -38
- package/templates/skills/documentation/steps/step-01-scan.md +59 -113
- package/templates/skills/documentation/steps/step-02-generate.md +158 -231
- package/templates/skills/documentation/steps/step-03-validate.md +101 -280
- package/templates/skills/documentation/templates.md +403 -92
- package/templates/skills/efcore/SKILL.md +88 -308
- package/templates/skills/efcore/_shared.md +140 -0
- package/templates/skills/efcore/agents/create.md +69 -0
- package/templates/skills/efcore/agents/db-update.md +58 -0
- package/templates/skills/efcore/agents/list.md +35 -0
- package/templates/skills/efcore/agents/rebase-snapshot.md +50 -0
- package/templates/skills/efcore/agents/recreate-db.md +78 -0
- package/templates/skills/efcore/agents/squash.md +78 -0
- package/templates/skills/efcore/agents/status.md +35 -0
- package/templates/skills/efcore/cli/create/execute.ts +164 -0
- package/templates/skills/efcore/cli/create/index.ts +51 -0
- package/templates/skills/efcore/cli/create/types.ts +35 -0
- package/templates/skills/efcore/cli/create/validate.ts +29 -0
- package/templates/skills/efcore/cli/lib/detect-dbcontexts.ts +195 -0
- package/templates/skills/efcore/cli/lib/ef-runner.ts +144 -0
- package/templates/skills/efcore/cli/lib/migration-name.ts +56 -0
- package/templates/skills/efcore/cli/lib/parse-csproj-version.ts +45 -0
- package/templates/skills/efcore/cli/list/execute.ts +83 -0
- package/templates/skills/efcore/cli/list/index.ts +60 -0
- package/templates/skills/efcore/cli/list/types.ts +46 -0
- package/templates/skills/efcore/cli/list/validate.ts +20 -0
- package/templates/skills/efcore/cli/rebase-snapshot/execute.ts +21 -0
- package/templates/skills/efcore/cli/rebase-snapshot/index.ts +53 -0
- package/templates/skills/efcore/cli/rebase-snapshot/types.ts +20 -0
- package/templates/skills/efcore/cli/rebase-snapshot/validate.ts +25 -0
- package/templates/skills/efcore/cli/squash/execute.ts +298 -0
- package/templates/skills/efcore/cli/squash/index.ts +57 -0
- package/templates/skills/efcore/cli/squash/types.ts +38 -0
- package/templates/skills/efcore/cli/squash/validate.ts +30 -0
- package/templates/skills/efcore/cli/status/execute.ts +152 -0
- package/templates/skills/efcore/cli/status/index.ts +45 -0
- package/templates/skills/efcore/cli/status/types.ts +32 -0
- package/templates/skills/efcore/cli/status/validate.ts +20 -0
- package/templates/skills/external/context7/SKILL.md +121 -0
- package/templates/skills/external/dev-browser/SKILL.md +134 -0
- package/templates/skills/gitflow/SKILL.md +139 -392
- package/templates/skills/gitflow/_shared.md +24 -620
- package/templates/skills/gitflow/agents/abort.md +47 -0
- package/templates/skills/gitflow/agents/cleanup.md +50 -0
- package/templates/skills/gitflow/agents/commit.md +41 -0
- package/templates/skills/gitflow/agents/finish.md +47 -0
- package/templates/skills/gitflow/agents/init.md +41 -0
- package/templates/skills/gitflow/agents/merge.md +44 -0
- package/templates/skills/gitflow/agents/pr.md +39 -0
- package/templates/skills/gitflow/agents/start.md +42 -0
- package/templates/skills/gitflow/agents/status.md +39 -0
- package/templates/skills/gitflow/agents/sync.md +38 -0
- package/templates/skills/gitflow/cli/abort/execute.ts +61 -0
- package/templates/skills/gitflow/cli/abort/index.ts +116 -0
- package/templates/skills/gitflow/cli/abort/types.ts +21 -0
- package/templates/skills/gitflow/cli/abort/validate.ts +38 -0
- package/templates/skills/gitflow/cli/cleanup/execute.ts +107 -0
- package/templates/skills/gitflow/cli/cleanup/index.ts +122 -0
- package/templates/skills/gitflow/cli/cleanup/types.ts +26 -0
- package/templates/skills/gitflow/cli/cleanup/validate.ts +33 -0
- package/templates/skills/gitflow/cli/commit/execute.ts +146 -0
- package/templates/skills/gitflow/cli/commit/index.ts +77 -0
- package/templates/skills/gitflow/cli/commit/types.ts +36 -0
- package/templates/skills/gitflow/cli/commit/validate.ts +38 -0
- package/templates/skills/gitflow/cli/finish/execute.ts +127 -0
- package/templates/skills/gitflow/cli/finish/index.ts +106 -0
- package/templates/skills/gitflow/cli/finish/types.ts +25 -0
- package/templates/skills/gitflow/cli/finish/validate.ts +44 -0
- package/templates/skills/gitflow/cli/generate-msg/execute.ts +51 -0
- package/templates/skills/gitflow/cli/generate-msg/index.ts +73 -0
- package/templates/skills/gitflow/cli/generate-msg/types.ts +37 -0
- package/templates/skills/gitflow/cli/generate-msg/validate.ts +42 -0
- package/templates/skills/gitflow/cli/init/execute.ts +186 -0
- package/templates/skills/gitflow/cli/init/index.ts +127 -0
- package/templates/skills/gitflow/cli/init/types.ts +63 -0
- package/templates/skills/gitflow/cli/init/validate.ts +56 -0
- package/templates/skills/gitflow/cli/lib/branch.ts +83 -0
- package/templates/skills/gitflow/cli/lib/config.ts +149 -0
- package/templates/skills/gitflow/cli/lib/efcore.ts +136 -0
- package/templates/skills/gitflow/cli/lib/git.ts +212 -0
- package/templates/skills/gitflow/cli/lib/output.ts +49 -0
- package/templates/skills/gitflow/cli/lib/paths.ts +54 -0
- package/templates/skills/gitflow/cli/lib/platform.ts +44 -0
- package/templates/skills/gitflow/cli/lib/provider.ts +147 -0
- package/templates/skills/gitflow/cli/lib/types.ts +189 -0
- package/templates/skills/gitflow/cli/lib/version.ts +152 -0
- package/templates/skills/gitflow/cli/lib/worktree.ts +170 -0
- package/templates/skills/gitflow/cli/merge/execute.ts +93 -0
- package/templates/skills/gitflow/cli/merge/index.ts +109 -0
- package/templates/skills/gitflow/cli/merge/types.ts +24 -0
- package/templates/skills/gitflow/cli/merge/validate.ts +33 -0
- package/templates/skills/gitflow/cli/pr/execute.ts +131 -0
- package/templates/skills/gitflow/cli/pr/index.ts +115 -0
- package/templates/skills/gitflow/cli/pr/types.ts +27 -0
- package/templates/skills/gitflow/cli/pr/validate.ts +27 -0
- package/templates/skills/gitflow/cli/start/execute.ts +98 -0
- package/templates/skills/gitflow/cli/start/index.ts +75 -0
- package/templates/skills/gitflow/cli/start/types.ts +26 -0
- package/templates/skills/gitflow/cli/start/validate.ts +47 -0
- package/templates/skills/gitflow/cli/status/execute.ts +251 -0
- package/templates/skills/gitflow/cli/status/index.ts +154 -0
- package/templates/skills/gitflow/cli/status/types.ts +75 -0
- package/templates/skills/gitflow/cli/status/validate.ts +38 -0
- package/templates/skills/gitflow/cli/sync/execute.ts +84 -0
- package/templates/skills/gitflow/cli/sync/index.ts +75 -0
- package/templates/skills/gitflow/cli/sync/types.ts +25 -0
- package/templates/skills/gitflow/cli/sync/validate.ts +34 -0
- package/templates/skills/gitflow/commit-message.md +46 -0
- package/templates/skills/init/SKILL.md +54 -0
- package/templates/skills/lib/__tests__/canonical-hash.test.ts +45 -0
- package/templates/skills/lib/__tests__/page-spec-actions.test.ts +232 -0
- package/templates/skills/lib/canonical-hash.ts +40 -0
- package/templates/skills/lib/detector.ts +243 -0
- package/templates/skills/lib/dotnet.ts +238 -0
- package/templates/skills/lib/frontend-fixers.ts +151 -0
- package/templates/skills/lib/fs.ts +238 -0
- package/templates/skills/lib/git.ts +134 -0
- package/templates/skills/lib/graph.ts +138 -0
- package/templates/skills/lib/navroute-parser.ts +51 -0
- package/templates/skills/lib/output.ts +117 -0
- package/templates/skills/lib/page-spec-actions.ts +350 -0
- package/templates/skills/lib/skill-slug.ts +121 -0
- package/templates/skills/lib/string-utils.ts +140 -0
- package/templates/skills/lib/template-loader.ts +115 -0
- package/templates/skills/lib/url-conventions.ts +151 -0
- package/templates/skills/package.json +5 -0
- package/templates/skills/quick-search/SKILL.md +99 -99
- package/templates/skills/review/SKILL.md +56 -0
- package/templates/skills/smoke-generation/SKILL.md +18 -16
- package/templates/skills/ui-components/SKILL.md +136 -457
- package/templates/skills/upgrade/SKILL.md +36 -0
- package/templates/skills/utils/SKILL.md +45 -44
- package/templates/skills/utils/subcommands/test-web-config.md +152 -152
- package/templates/skills/utils/subcommands/test-web.md +123 -123
- package/templates/skills/validate-feature/SKILL.md +102 -101
- package/templates/skills/validate-feature/references/api-smoke-tests.md +140 -140
- package/templates/skills/validate-feature/references/db-validation-checks.md +180 -180
- package/templates/skills/validate-feature/steps/step-00-dependencies.md +121 -121
- package/templates/skills/validate-feature/steps/step-01-compile.md +39 -39
- package/templates/skills/validate-feature/steps/step-02-unit-tests.md +45 -45
- package/templates/skills/validate-feature/steps/step-03-integration-tests.md +53 -53
- package/templates/skills/validate-feature/steps/step-04-api-smoke.md +94 -94
- package/templates/skills/validate-feature/steps/step-05-db-validation.md +149 -149
- package/templates/skills/validation/conventions/SKILL.md +193 -0
- package/templates/skills/validation/conventions/cli/validate-conventions/execute.ts +368 -0
- package/templates/skills/validation/conventions/cli/validate-conventions/index.ts +67 -0
- package/templates/skills/validation/conventions/cli/validate-conventions/types.ts +91 -0
- package/templates/skills/validation/conventions/cli/validate-conventions/validate.ts +36 -0
- package/templates/skills/validation/cross-validate/SKILL.md +83 -0
- package/templates/skills/validation/cross-validate/cli/execute.ts +576 -0
- package/templates/skills/validation/cross-validate/cli/index.ts +87 -0
- package/templates/skills/validation/cross-validate/cli/types.ts +85 -0
- package/templates/skills/validation/cross-validate/cli/validate.ts +54 -0
- package/templates/skills/validation/eslint/SKILL.md +65 -0
- package/templates/skills/validation/eslint/cli/execute.ts +413 -0
- package/templates/skills/validation/eslint/cli/index.ts +102 -0
- package/templates/skills/validation/eslint/cli/types.ts +48 -0
- package/templates/skills/validation/eslint/cli/validate.ts +43 -0
- package/templates/skills/validation/project-inventory/SKILL.md +166 -0
- package/templates/skills/validation/project-inventory/cli/project-inventory/execute.ts +397 -0
- package/templates/skills/validation/project-inventory/cli/project-inventory/index.ts +80 -0
- package/templates/skills/validation/project-inventory/cli/project-inventory/types.ts +89 -0
- package/templates/skills/validation/project-inventory/cli/project-inventory/validate.ts +35 -0
- package/templates/skills/validation/readiness-report/SKILL.md +109 -0
- package/templates/skills/validation/readiness-report/cli/execute.ts +236 -0
- package/templates/skills/validation/readiness-report/cli/index.ts +234 -0
- package/templates/skills/validation/readiness-report/cli/types.ts +61 -0
- package/templates/skills/validation/readiness-report/cli/validate.ts +54 -0
- package/templates/skills/validation/roslyn/SKILL.md +103 -0
- package/templates/skills/validation/roslyn/cli/execute.ts +616 -0
- package/templates/skills/validation/roslyn/cli/index.ts +86 -0
- package/templates/skills/validation/roslyn/cli/types.ts +50 -0
- package/templates/skills/validation/roslyn/cli/validate.ts +43 -0
- package/.documentation/apex.html +0 -649
- package/dist/mcp-entry.mjs +0 -68888
- package/dist/mcp-entry.mjs.map +0 -1
- package/scripts/extract-api-endpoints.ts +0 -325
- package/scripts/extract-business-rules.ts +0 -440
- package/scripts/generate-doc-with-mock-ui.ts +0 -804
- package/templates/agents/ba-reader.md +0 -386
- package/templates/agents/ba-writer.md +0 -810
- package/templates/agents/efcore/migration.md +0 -204
- package/templates/agents/efcore/rebase-snapshot.md +0 -202
- package/templates/agents/efcore/squash.md +0 -269
- package/templates/agents/gitflow/abort.md +0 -45
- package/templates/agents/gitflow/cleanup.md +0 -107
- package/templates/agents/gitflow/commit.md +0 -236
- package/templates/agents/gitflow/exec.md +0 -48
- package/templates/agents/gitflow/finish.md +0 -146
- package/templates/agents/gitflow/init-clone.md +0 -199
- package/templates/agents/gitflow/init-detect.md +0 -137
- package/templates/agents/gitflow/init-validate.md +0 -225
- package/templates/agents/gitflow/init.md +0 -509
- package/templates/agents/gitflow/merge.md +0 -145
- package/templates/agents/gitflow/plan.md +0 -42
- package/templates/agents/gitflow/pr.md +0 -191
- package/templates/agents/gitflow/review.md +0 -49
- package/templates/agents/gitflow/start.md +0 -147
- package/templates/agents/gitflow/status.md +0 -95
- package/templates/agents/mcp-healthcheck.md +0 -163
- package/templates/hooks/docs-drift-check.md +0 -96
- package/templates/hooks/ef-migration-check.md +0 -139
- package/templates/hooks/mcp-check.md +0 -64
- package/templates/hooks/ralph-mcp-logger.sh +0 -46
- package/templates/mcp-scaffolding/component.tsx.hbs +0 -318
- package/templates/mcp-scaffolding/controller.cs.hbs +0 -118
- package/templates/mcp-scaffolding/entity-extension.cs.hbs +0 -239
- package/templates/mcp-scaffolding/frontend/api-client.ts.hbs +0 -117
- package/templates/mcp-scaffolding/frontend/nav-routes.ts.hbs +0 -133
- package/templates/mcp-scaffolding/migrations/seed-roles.cs.hbs +0 -261
- package/templates/mcp-scaffolding/service-extension.cs.hbs +0 -53
- package/templates/mcp-scaffolding/tests/controller.test.cs.hbs +0 -436
- package/templates/mcp-scaffolding/tests/entity.test.cs.hbs +0 -239
- package/templates/mcp-scaffolding/tests/repository.test.cs.hbs +0 -441
- package/templates/mcp-scaffolding/tests/security.test.cs.hbs +0 -442
- package/templates/mcp-scaffolding/tests/service.test.cs.hbs +0 -402
- package/templates/mcp-scaffolding/tests/validator.test.cs.hbs +0 -428
- package/templates/skills/_resources/config-safety.md +0 -61
- package/templates/skills/_resources/context-digest-template.md +0 -53
- package/templates/skills/_resources/doc-context-cache.md +0 -60
- package/templates/skills/_resources/docs-manifest-schema.md +0 -155
- package/templates/skills/_resources/formatting-guide.md +0 -124
- package/templates/skills/_resources/mcp-validate-documentation-spec.md +0 -181
- package/templates/skills/_shared.md +0 -228
- package/templates/skills/admin/SKILL.md +0 -48
- package/templates/skills/ai-prompt/SKILL.md +0 -171
- package/templates/skills/ai-prompt/references/ai-agent-modes.md +0 -89
- package/templates/skills/ai-prompt/references/eval-framework.md +0 -129
- package/templates/skills/ai-prompt/steps/step-00-init.md +0 -47
- package/templates/skills/ai-prompt/steps/step-01-implementation.md +0 -122
- package/templates/skills/apex/SKILL.md +0 -234
- package/templates/skills/apex/_shared.md +0 -197
- package/templates/skills/apex/references/analysis-methods.md +0 -178
- package/templates/skills/apex/references/challenge-questions.md +0 -359
- package/templates/skills/apex/references/checks/architecture-checks.sh +0 -154
- package/templates/skills/apex/references/checks/backend-checks.sh +0 -208
- package/templates/skills/apex/references/checks/frontend-checks.sh +0 -560
- package/templates/skills/apex/references/checks/infrastructure-checks.sh +0 -292
- package/templates/skills/apex/references/checks/security-checks.sh +0 -153
- package/templates/skills/apex/references/checks/seed-checks.sh +0 -610
- package/templates/skills/apex/references/code-generation.md +0 -412
- package/templates/skills/apex/references/core-seed-data.md +0 -1502
- package/templates/skills/apex/references/domain-events-pattern.md +0 -45
- package/templates/skills/apex/references/entity-hooks-pattern.md +0 -68
- package/templates/skills/apex/references/error-classification.md +0 -168
- package/templates/skills/apex/references/frontend-route-wiring-app-tsx.md +0 -91
- package/templates/skills/apex/references/licensing-enforcement.md +0 -52
- package/templates/skills/apex/references/parallel-execution.md +0 -187
- package/templates/skills/apex/references/person-extension-pattern.md +0 -619
- package/templates/skills/apex/references/post-checks.md +0 -162
- package/templates/skills/apex/references/smartstack-api.md +0 -591
- package/templates/skills/apex/references/smartstack-frontend-compliance.md +0 -616
- package/templates/skills/apex/references/smartstack-frontend.md +0 -442
- package/templates/skills/apex/references/smartstack-layers.md +0 -551
- package/templates/skills/apex/steps/step-00-init.md +0 -405
- package/templates/skills/apex/steps/step-01-analyze.md +0 -210
- package/templates/skills/apex/steps/step-02-plan.md +0 -303
- package/templates/skills/apex/steps/step-03-execute.md +0 -194
- package/templates/skills/apex/steps/step-03a-layer0-domain.md +0 -144
- package/templates/skills/apex/steps/step-03b-layer1-seed.md +0 -339
- package/templates/skills/apex/steps/step-03c-layer2-backend.md +0 -401
- package/templates/skills/apex/steps/step-03d-layer3-frontend.md +0 -562
- package/templates/skills/apex/steps/step-03e-layer4-devdata.md +0 -46
- package/templates/skills/apex/steps/step-04-examine.md +0 -404
- package/templates/skills/apex/steps/step-05-deep-review.md +0 -140
- package/templates/skills/apex/steps/step-06-resolve.md +0 -120
- package/templates/skills/apex/steps/step-07-tests.md +0 -271
- package/templates/skills/apex/steps/step-08-run-tests.md +0 -135
- package/templates/skills/apex-verify/SKILL.md +0 -110
- package/templates/skills/apex-verify/references/audit-rules.md +0 -50
- package/templates/skills/apex-verify/steps/step-00-init.md +0 -119
- package/templates/skills/apex-verify/steps/step-01-nav-audit.md +0 -96
- package/templates/skills/apex-verify/steps/step-02-crud-audit.md +0 -127
- package/templates/skills/apex-verify/steps/step-03-perm-audit.md +0 -119
- package/templates/skills/apex-verify/steps/step-04-route-audit.md +0 -98
- package/templates/skills/apex-verify/steps/step-05-report.md +0 -110
- package/templates/skills/application/SKILL.md +0 -241
- package/templates/skills/application/references/application-roles-template.md +0 -228
- package/templates/skills/application/references/backend-controller-hierarchy.md +0 -68
- package/templates/skills/application/references/backend-entity-seeding.md +0 -73
- package/templates/skills/application/references/backend-seeding-and-dto-output.md +0 -83
- package/templates/skills/application/references/backend-table-prefix-mapping.md +0 -80
- package/templates/skills/application/references/backend-verification.md +0 -88
- package/templates/skills/application/references/contexts-cheatsheet.md +0 -86
- package/templates/skills/application/references/extensions-system.md +0 -158
- package/templates/skills/application/references/frontend-i18n-and-output.md +0 -67
- package/templates/skills/application/references/frontend-route-naming.md +0 -123
- package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +0 -91
- package/templates/skills/application/references/frontend-verification.md +0 -158
- package/templates/skills/application/references/init-parameter-detection.md +0 -121
- package/templates/skills/application/references/migration-checklist-troubleshooting.md +0 -88
- package/templates/skills/application/references/nav-fallback-procedure.md +0 -198
- package/templates/skills/application/references/provider-template.md +0 -191
- package/templates/skills/application/references/roles-client-project-handling.md +0 -55
- package/templates/skills/application/references/roles-fallback-procedure.md +0 -144
- package/templates/skills/application/references/smartstack-provider.md +0 -118
- package/templates/skills/application/references/test-coverage-requirements.md +0 -213
- package/templates/skills/application/references/test-frontend.md +0 -73
- package/templates/skills/application/references/test-prerequisites.md +0 -72
- package/templates/skills/application/references/themes-db-driven.md +0 -484
- package/templates/skills/application/steps/step-00-init.md +0 -130
- package/templates/skills/application/steps/step-01-navigation.md +0 -170
- package/templates/skills/application/steps/step-02-permissions.md +0 -196
- package/templates/skills/application/steps/step-03-roles.md +0 -182
- package/templates/skills/application/steps/step-03b-provider.md +0 -134
- package/templates/skills/application/steps/step-04-backend.md +0 -174
- package/templates/skills/application/steps/step-05-frontend.md +0 -189
- package/templates/skills/application/steps/step-06-migration.md +0 -189
- package/templates/skills/application/steps/step-07-tests.md +0 -356
- package/templates/skills/application/steps/step-08-documentation.md +0 -137
- package/templates/skills/application/templates-backend.md +0 -463
- package/templates/skills/application/templates-frontend.md +0 -950
- package/templates/skills/application/templates-i18n.md +0 -520
- package/templates/skills/application/templates-seed.md +0 -1110
- package/templates/skills/audit-route/SKILL.md +0 -107
- package/templates/skills/audit-route/references/routing-pattern.md +0 -131
- package/templates/skills/audit-route/steps/step-00-init.md +0 -128
- package/templates/skills/audit-route/steps/step-01-inventory.md +0 -157
- package/templates/skills/audit-route/steps/step-02-conformity.md +0 -193
- package/templates/skills/audit-route/steps/step-03-report.md +0 -201
- package/templates/skills/business-analyse/SKILL.md +0 -252
- package/templates/skills/business-analyse/_shared.md +0 -276
- package/templates/skills/business-analyse/patterns/suggestion-catalog.md +0 -560
- package/templates/skills/business-analyse/questionnaire/01-context.md +0 -43
- package/templates/skills/business-analyse/questionnaire/02-stakeholders-scope.md +0 -111
- package/templates/skills/business-analyse/questionnaire/03-data-ui.md +0 -125
- package/templates/skills/business-analyse/questionnaire/04-risks-metrics.md +0 -6
- package/templates/skills/business-analyse/questionnaire/05-cross-module.md +0 -69
- package/templates/skills/business-analyse/questionnaire.md +0 -156
- package/templates/skills/business-analyse/react/application-viewer.md +0 -242
- package/templates/skills/business-analyse/react/components.md +0 -532
- package/templates/skills/business-analyse/react/i18n-template.md +0 -306
- package/templates/skills/business-analyse/react/schema.md +0 -831
- package/templates/skills/business-analyse/references/03-json-schemas.md +0 -221
- package/templates/skills/business-analyse/references/03-post-check-validation.md +0 -208
- package/templates/skills/business-analyse/references/03-smartstack-entity-guards.md +0 -32
- package/templates/skills/business-analyse/references/04-cross-module-validation.md +0 -95
- package/templates/skills/business-analyse/references/04-file-allocation.md +0 -162
- package/templates/skills/business-analyse/references/04-naming-audit-checks.md +0 -174
- package/templates/skills/business-analyse/references/04-semantic-validation-matrix.md +0 -118
- package/templates/skills/business-analyse/references/acceptance-criteria.md +0 -164
- package/templates/skills/business-analyse/references/analysis-semantic-checks.md +0 -190
- package/templates/skills/business-analyse/references/canonical-json-formats.md +0 -204
- package/templates/skills/business-analyse/references/compilation-structure-cards.md +0 -297
- package/templates/skills/business-analyse/references/consolidation-structural-checks.md +0 -124
- package/templates/skills/business-analyse/references/detection-strategies.md +0 -424
- package/templates/skills/business-analyse/references/domain-research-playbook.md +0 -234
- package/templates/skills/business-analyse/references/entity-architecture-decision.md +0 -240
- package/templates/skills/business-analyse/references/entity-sourcing-presentation.md +0 -166
- package/templates/skills/business-analyse/references/init-resume-logic.md +0 -70
- package/templates/skills/business-analyse/references/init-schema-deployment.md +0 -65
- package/templates/skills/business-analyse/references/module-completeness-challenge.md +0 -174
- package/templates/skills/business-analyse/references/multi-app-detection.md +0 -149
- package/templates/skills/business-analyse/references/naming-conventions.md +0 -253
- package/templates/skills/business-analyse/references/portal-classification.md +0 -52
- package/templates/skills/business-analyse/references/robustness-checks.md +0 -426
- package/templates/skills/business-analyse/references/ui-dashboard-spec.md +0 -85
- package/templates/skills/business-analyse/references/ui-resource-cards.md +0 -259
- package/templates/skills/business-analyse/references/validation-checklist.md +0 -378
- package/templates/skills/business-analyse/schemas/application-schema.json +0 -481
- package/templates/skills/business-analyse/schemas/feature-schema.json +0 -53
- package/templates/skills/business-analyse/schemas/index-schema.json +0 -47
- package/templates/skills/business-analyse/schemas/project-schema.json +0 -481
- package/templates/skills/business-analyse/schemas/sections/analysis-schema.json +0 -245
- package/templates/skills/business-analyse/schemas/sections/discovery-schema.json +0 -80
- package/templates/skills/business-analyse/schemas/sections/handoff-schema.json +0 -82
- package/templates/skills/business-analyse/schemas/sections/metadata-schema.json +0 -70
- package/templates/skills/business-analyse/schemas/sections/specification-schema.json +0 -567
- package/templates/skills/business-analyse/schemas/sections/validation-schema.json +0 -93
- package/templates/skills/business-analyse/schemas/shared/common-defs.json +0 -227
- package/templates/skills/business-analyse/steps/step-00-init.md +0 -463
- package/templates/skills/business-analyse/steps/step-01-cadrage.md +0 -901
- package/templates/skills/business-analyse/steps/step-02-structure.md +0 -315
- package/templates/skills/business-analyse/steps/step-03-specify.md +0 -986
- package/templates/skills/business-analyse/steps/step-04-consolidate.md +0 -928
- package/templates/skills/business-analyse/templates/tpl-frd.md +0 -168
- package/templates/skills/business-analyse/templates/tpl-handoff.md +0 -189
- package/templates/skills/business-analyse/templates/tpl-launch-displays.md +0 -59
- package/templates/skills/business-analyse/templates-frd.md +0 -476
- package/templates/skills/business-analyse/templates-react.md +0 -574
- package/templates/skills/business-analyse-design/SKILL.md +0 -101
- package/templates/skills/business-analyse-design/references/screens-post-check.md +0 -221
- package/templates/skills/business-analyse-design/references/screens-type-mapping.md +0 -138
- package/templates/skills/business-analyse-design/references/smartcomponents-templates.md +0 -225
- package/templates/skills/business-analyse-design/references/spec-auto-inference.md +0 -117
- package/templates/skills/business-analyse-design/steps/step-01-screens.md +0 -108
- package/templates/skills/business-analyse-design/steps/step-02-wireframes.md +0 -155
- package/templates/skills/business-analyse-design/steps/step-03-navigation.md +0 -189
- package/templates/skills/business-analyse-develop/SKILL.md +0 -250
- package/templates/skills/business-analyse-develop/references/category-completeness.md +0 -326
- package/templates/skills/business-analyse-develop/references/category-rules.md +0 -109
- package/templates/skills/business-analyse-develop/references/compact-loop.md +0 -478
- package/templates/skills/business-analyse-develop/references/handoff-quality-gate.md +0 -132
- package/templates/skills/business-analyse-develop/references/init-resume-recovery.md +0 -183
- package/templates/skills/business-analyse-develop/references/module-transition.md +0 -246
- package/templates/skills/business-analyse-develop/references/multi-module-queue.md +0 -171
- package/templates/skills/business-analyse-develop/references/parallel-execution.md +0 -246
- package/templates/skills/business-analyse-develop/references/prd-v3-transformation.md +0 -326
- package/templates/skills/business-analyse-develop/references/quality-gates.md +0 -160
- package/templates/skills/business-analyse-develop/references/report-reconciliation.md +0 -140
- package/templates/skills/business-analyse-develop/references/report-template.md +0 -142
- package/templates/skills/business-analyse-develop/references/section-splitting.md +0 -439
- package/templates/skills/business-analyse-develop/references/task-transform-legacy.md +0 -256
- package/templates/skills/business-analyse-develop/references/team-orchestration.md +0 -547
- package/templates/skills/business-analyse-develop/steps/step-00-init.md +0 -242
- package/templates/skills/business-analyse-develop/steps/step-01-task.md +0 -182
- package/templates/skills/business-analyse-develop/steps/step-01-v4-execute.md +0 -139
- package/templates/skills/business-analyse-develop/steps/step-02-execute.md +0 -216
- package/templates/skills/business-analyse-develop/steps/step-02-v4-verify.md +0 -176
- package/templates/skills/business-analyse-develop/steps/step-03-commit.md +0 -107
- package/templates/skills/business-analyse-develop/steps/step-04-check.md +0 -419
- package/templates/skills/business-analyse-develop/steps/step-05-report.md +0 -137
- package/templates/skills/business-analyse-handoff/SKILL.md +0 -101
- package/templates/skills/business-analyse-handoff/references/acceptance-criteria.md +0 -318
- package/templates/skills/business-analyse-handoff/references/agent-handoff-transform-prompt.md +0 -211
- package/templates/skills/business-analyse-handoff/references/context-isolation-pattern.md +0 -47
- package/templates/skills/business-analyse-handoff/references/entity-canonicalization.md +0 -158
- package/templates/skills/business-analyse-handoff/references/entity-domain-mapping.md +0 -115
- package/templates/skills/business-analyse-handoff/references/handoff-file-inventory.md +0 -49
- package/templates/skills/business-analyse-handoff/references/handoff-file-templates.md +0 -197
- package/templates/skills/business-analyse-handoff/references/handoff-global-validation.md +0 -142
- package/templates/skills/business-analyse-handoff/references/handoff-mappings.md +0 -108
- package/templates/skills/business-analyse-handoff/references/handoff-seeddata-generation.md +0 -314
- package/templates/skills/business-analyse-handoff/references/prd-generation.md +0 -362
- package/templates/skills/business-analyse-handoff/references/prd-validation-checks.md +0 -125
- package/templates/skills/business-analyse-handoff/references/project-index-update.md +0 -98
- package/templates/skills/business-analyse-handoff/references/readiness-scoring.md +0 -95
- package/templates/skills/business-analyse-handoff/schemas/handoff-schema.json +0 -95
- package/templates/skills/business-analyse-handoff/steps/step-00-validate.md +0 -158
- package/templates/skills/business-analyse-handoff/steps/step-01-transform.md +0 -85
- package/templates/skills/business-analyse-handoff/steps/step-02-export.md +0 -169
- package/templates/skills/business-analyse-handoff/templates/tpl-progress.md +0 -172
- package/templates/skills/business-analyse-html/SKILL.md +0 -89
- package/templates/skills/business-analyse-html/html/ba-interactive.html +0 -6134
- package/templates/skills/business-analyse-html/html/build-html.js +0 -137
- package/templates/skills/business-analyse-html/html/src/partials/cadrage-context.html +0 -34
- package/templates/skills/business-analyse-html/html/src/partials/cadrage-scope.html +0 -27
- package/templates/skills/business-analyse-html/html/src/partials/cadrage-stakeholders.html +0 -55
- package/templates/skills/business-analyse-html/html/src/partials/cadrage-success.html +0 -34
- package/templates/skills/business-analyse-html/html/src/partials/consol-datamodel.html +0 -8
- package/templates/skills/business-analyse-html/html/src/partials/consol-flows.html +0 -29
- package/templates/skills/business-analyse-html/html/src/partials/consol-interactions.html +0 -8
- package/templates/skills/business-analyse-html/html/src/partials/consol-permissions.html +0 -8
- package/templates/skills/business-analyse-html/html/src/partials/decomp-dependencies.html +0 -38
- package/templates/skills/business-analyse-html/html/src/partials/decomp-modules.html +0 -43
- package/templates/skills/business-analyse-html/html/src/partials/handoff-summary.html +0 -24
- package/templates/skills/business-analyse-html/html/src/partials/module-spec-container.html +0 -4
- package/templates/skills/business-analyse-html/html/src/scripts/01-data-init.js +0 -313
- package/templates/skills/business-analyse-html/html/src/scripts/02-navigation.js +0 -316
- package/templates/skills/business-analyse-html/html/src/scripts/03-render-cadrage.js +0 -162
- package/templates/skills/business-analyse-html/html/src/scripts/04-render-modules.js +0 -203
- package/templates/skills/business-analyse-html/html/src/scripts/05-render-specs.js +0 -866
- package/templates/skills/business-analyse-html/html/src/scripts/06-render-consolidation.js +0 -196
- package/templates/skills/business-analyse-html/html/src/scripts/06-render-mockups.js +0 -564
- package/templates/skills/business-analyse-html/html/src/scripts/07-render-handoff.js +0 -108
- package/templates/skills/business-analyse-html/html/src/scripts/08-editing.js +0 -137
- package/templates/skills/business-analyse-html/html/src/scripts/09-export.js +0 -138
- package/templates/skills/business-analyse-html/html/src/scripts/10-comments.js +0 -221
- package/templates/skills/business-analyse-html/html/src/scripts/11-review-panel.js +0 -167
- package/templates/skills/business-analyse-html/html/src/scripts/12-render-diagrams.js +0 -162
- package/templates/skills/business-analyse-html/html/src/styles/01-variables.css +0 -38
- package/templates/skills/business-analyse-html/html/src/styles/02-layout.css +0 -216
- package/templates/skills/business-analyse-html/html/src/styles/03-navigation.css +0 -120
- package/templates/skills/business-analyse-html/html/src/styles/04-cards.css +0 -194
- package/templates/skills/business-analyse-html/html/src/styles/05-modules.css +0 -518
- package/templates/skills/business-analyse-html/html/src/styles/06-wireframes.css +0 -263
- package/templates/skills/business-analyse-html/html/src/styles/07-comments.css +0 -184
- package/templates/skills/business-analyse-html/html/src/styles/08-review-panel.css +0 -241
- package/templates/skills/business-analyse-html/html/src/styles/09-mockups-html.css +0 -220
- package/templates/skills/business-analyse-html/html/src/styles/10-diagrams.css +0 -73
- package/templates/skills/business-analyse-html/html/src/template.html +0 -449
- package/templates/skills/business-analyse-html/references/02-embedded-artifacts-building.md +0 -144
- package/templates/skills/business-analyse-html/references/02-feature-data-building.md +0 -143
- package/templates/skills/business-analyse-html/references/02-mapping-tables.md +0 -442
- package/templates/skills/business-analyse-html/references/02-normalization-helpers.md +0 -139
- package/templates/skills/business-analyse-html/references/02-screen-format-detection.md +0 -283
- package/templates/skills/business-analyse-html/references/02-self-check-validation.md +0 -199
- package/templates/skills/business-analyse-html/references/data-build.md +0 -215
- package/templates/skills/business-analyse-html/references/data-mapping.md +0 -452
- package/templates/skills/business-analyse-html/references/output-modes.md +0 -119
- package/templates/skills/business-analyse-html/references/wireframe-svg-style-guide.md +0 -335
- package/templates/skills/business-analyse-html/steps/step-01-collect.md +0 -140
- package/templates/skills/business-analyse-html/steps/step-02-build-data.md +0 -76
- package/templates/skills/business-analyse-html/steps/step-03-render.md +0 -95
- package/templates/skills/business-analyse-html/steps/step-04-verify.md +0 -224
- package/templates/skills/business-analyse-quick/SKILL.md +0 -807
- package/templates/skills/business-analyse-quick/references/domain-heuristics.md +0 -172
- package/templates/skills/business-analyse-quick/references/prd-schema.md +0 -268
- package/templates/skills/business-analyse-review/SKILL.md +0 -71
- package/templates/skills/business-analyse-review/references/review-data-mapping.md +0 -367
- package/templates/skills/business-analyse-review/steps/step-00-init.md +0 -111
- package/templates/skills/business-analyse-review/steps/step-01-apply.md +0 -304
- package/templates/skills/business-analyse-status/SKILL.md +0 -136
- package/templates/skills/cc-agent/SKILL.md +0 -129
- package/templates/skills/cc-agent/references/agent-behavior-patterns.md +0 -95
- package/templates/skills/cc-agent/references/agent-frontmatter.md +0 -213
- package/templates/skills/cc-agent/references/permission-modes.md +0 -102
- package/templates/skills/cc-agent/references/tools-reference.md +0 -144
- package/templates/skills/cc-agent/steps/step-00-init.md +0 -134
- package/templates/skills/cc-agent/steps/step-01-design.md +0 -186
- package/templates/skills/cc-agent/steps/step-02-generate.md +0 -131
- package/templates/skills/cc-agent/steps/step-03-validate.md +0 -130
- package/templates/skills/cc-agent/templates/agent-categorized.md +0 -67
- package/templates/skills/cc-agent/templates/agent-standalone.md +0 -56
- package/templates/skills/cc-agent/templates/agent-with-skills.md +0 -94
- package/templates/skills/cc-audit/SKILL.md +0 -108
- package/templates/skills/cc-audit/references/agent-checklist.md +0 -91
- package/templates/skills/cc-audit/references/hook-checklist.md +0 -110
- package/templates/skills/cc-audit/references/skill-checklist.md +0 -70
- package/templates/skills/cc-audit/steps/step-00-init.md +0 -98
- package/templates/skills/cc-audit/steps/step-01-scan.md +0 -142
- package/templates/skills/cc-audit/steps/step-02-analyze.md +0 -158
- package/templates/skills/cc-audit/steps/step-03-report.md +0 -142
- package/templates/skills/cc-skill/SKILL.md +0 -134
- package/templates/skills/cc-skill/references/best-practices.md +0 -167
- package/templates/skills/cc-skill/references/frontmatter-reference.md +0 -182
- package/templates/skills/cc-skill/references/skill-patterns.md +0 -199
- package/templates/skills/cc-skill/steps/step-00-init.md +0 -119
- package/templates/skills/cc-skill/steps/step-01-design.md +0 -199
- package/templates/skills/cc-skill/steps/step-02-generate.md +0 -145
- package/templates/skills/cc-skill/steps/step-03-steps.md +0 -151
- package/templates/skills/cc-skill/steps/step-04-validate.md +0 -124
- package/templates/skills/cc-skill/templates/skill-forked.md +0 -85
- package/templates/skills/cc-skill/templates/skill-progressive.md +0 -102
- package/templates/skills/cc-skill/templates/skill-simple.md +0 -75
- package/templates/skills/cc-skill/templates/step-template.md +0 -82
- package/templates/skills/controller/SKILL.md +0 -162
- package/templates/skills/controller/postman-templates.md +0 -614
- package/templates/skills/controller/references/controller-code-templates.md +0 -162
- package/templates/skills/controller/references/mcp-scaffold-workflow.md +0 -237
- package/templates/skills/controller/references/permission-sync-templates.md +0 -149
- package/templates/skills/controller/steps/step-00-init.md +0 -193
- package/templates/skills/controller/steps/step-01-analyze.md +0 -152
- package/templates/skills/controller/steps/step-02-plan.md +0 -176
- package/templates/skills/controller/steps/step-03-generate.md +0 -189
- package/templates/skills/controller/steps/step-04-perms.md +0 -80
- package/templates/skills/controller/steps/step-05-validate.md +0 -107
- package/templates/skills/controller/templates.md +0 -1556
- package/templates/skills/debug/SKILL.md +0 -70
- package/templates/skills/debug/references/team-protocol.md +0 -232
- package/templates/skills/debug/steps/step-00-init.md +0 -57
- package/templates/skills/debug/steps/step-01-analyze.md +0 -219
- package/templates/skills/debug/steps/step-02-resolve.md +0 -85
- package/templates/skills/efcore/references/database-operations.md +0 -66
- package/templates/skills/efcore/references/reset-operations.md +0 -81
- package/templates/skills/efcore/references/seed-methods.md +0 -86
- package/templates/skills/efcore/references/shared-init-functions.md +0 -250
- package/templates/skills/efcore/references/sql-objects-injection.md +0 -19
- package/templates/skills/efcore/references/troubleshooting.md +0 -81
- package/templates/skills/efcore/references/zero-downtime-patterns.md +0 -229
- package/templates/skills/explore/SKILL.md +0 -98
- package/templates/skills/feature-full/SKILL.md +0 -111
- package/templates/skills/feature-full/steps/step-00-init.md +0 -57
- package/templates/skills/feature-full/steps/step-01-implementation.md +0 -133
- package/templates/skills/gitflow/phases/abort.md +0 -189
- package/templates/skills/gitflow/phases/cleanup.md +0 -264
- package/templates/skills/gitflow/phases/status.md +0 -192
- package/templates/skills/gitflow/references/commit-message-generation.md +0 -58
- package/templates/skills/gitflow/references/commit-migration-validation.md +0 -53
- package/templates/skills/gitflow/references/finish-cleanup.md +0 -55
- package/templates/skills/gitflow/references/finish-version-bumping.md +0 -45
- package/templates/skills/gitflow/references/init-config-template.md +0 -143
- package/templates/skills/gitflow/references/init-environment-detection.md +0 -41
- package/templates/skills/gitflow/references/init-name-normalization.md +0 -118
- package/templates/skills/gitflow/references/init-questions.md +0 -193
- package/templates/skills/gitflow/references/init-structure-creation.md +0 -75
- package/templates/skills/gitflow/references/init-version-detection.md +0 -23
- package/templates/skills/gitflow/references/init-workspace-detection.md +0 -43
- package/templates/skills/gitflow/references/merge-ci-status.md +0 -36
- package/templates/skills/gitflow/references/merge-execution.md +0 -62
- package/templates/skills/gitflow/references/merge-pr-context.md +0 -76
- package/templates/skills/gitflow/references/plan-template.md +0 -69
- package/templates/skills/gitflow/references/pr-build-checks.md +0 -60
- package/templates/skills/gitflow/references/pr-generation.md +0 -58
- package/templates/skills/gitflow/references/start-branch-normalization.md +0 -28
- package/templates/skills/gitflow/references/start-efcore-preflight.md +0 -70
- package/templates/skills/gitflow/references/start-local-config.md +0 -113
- package/templates/skills/gitflow/references/start-worktree-creation.md +0 -50
- package/templates/skills/gitflow/references/sync-push-verify.md +0 -44
- package/templates/skills/gitflow/references/sync-rebase-conflicts.md +0 -38
- package/templates/skills/gitflow/steps/step-commit.md +0 -199
- package/templates/skills/gitflow/steps/step-finish.md +0 -147
- package/templates/skills/gitflow/steps/step-init.md +0 -230
- package/templates/skills/gitflow/steps/step-merge.md +0 -85
- package/templates/skills/gitflow/steps/step-plan.md +0 -151
- package/templates/skills/gitflow/steps/step-pr.md +0 -247
- package/templates/skills/gitflow/steps/step-start.md +0 -195
- package/templates/skills/gitflow/steps/step-sync.md +0 -161
- package/templates/skills/gitflow/templates/config.json +0 -72
- package/templates/skills/mcp/SKILL.md +0 -62
- package/templates/skills/mcp/steps/step-01-healthcheck.md +0 -108
- package/templates/skills/mcp/steps/step-02-tools.md +0 -73
- package/templates/skills/migrate/SKILL.md +0 -312
- package/templates/skills/migrate/references/v3.34-to-v3.46.md +0 -289
- package/templates/skills/notification/SKILL.md +0 -173
- package/templates/skills/refactor/SKILL.md +0 -56
- package/templates/skills/refactor/steps/step-01-discover.md +0 -60
- package/templates/skills/refactor/steps/step-02-execute.md +0 -67
- package/templates/skills/review-code/SKILL.md +0 -95
- package/templates/skills/review-code/references/clean-code-principles.md +0 -292
- package/templates/skills/review-code/references/code-quality-metrics.md +0 -174
- package/templates/skills/review-code/references/feedback-patterns.md +0 -149
- package/templates/skills/review-code/references/owasp-api-top10.md +0 -243
- package/templates/skills/review-code/references/security-checklist.md +0 -212
- package/templates/skills/review-code/references/smartstack-conventions.md +0 -568
- package/templates/skills/review-code/steps/step-01-smartstack.md +0 -96
- package/templates/skills/review-code/steps/step-02-detailed-review.md +0 -80
- package/templates/skills/review-code/steps/step-03-react.md +0 -44
- package/templates/skills/sketch/SKILL.md +0 -34
- package/templates/skills/ui-components/accessibility.md +0 -170
- package/templates/skills/ui-components/patterns/dashboard-chart.md +0 -327
- package/templates/skills/ui-components/patterns/data-table.md +0 -175
- package/templates/skills/ui-components/patterns/entity-card.md +0 -77
- package/templates/skills/ui-components/patterns/grid-layout.md +0 -91
- package/templates/skills/ui-components/patterns/kanban.md +0 -43
- package/templates/skills/ui-components/references/component-catalog.md +0 -82
- package/templates/skills/ui-components/responsive-guidelines.md +0 -278
- package/templates/skills/ui-components/style-guide.md +0 -113
- package/templates/skills/validate/SKILL.md +0 -181
- package/templates/skills/workflow/SKILL.md +0 -196
- package/templates/skills/workflow/steps/step-00-init.md +0 -57
- package/templates/skills/workflow/steps/step-01-implementation.md +0 -84
|
@@ -0,0 +1,1923 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli:scaffold-component — generate.ts
|
|
3
|
+
*
|
|
4
|
+
* Emits:
|
|
5
|
+
* - React pages (list, detail, form) under src/features/{module}/{entity}/pages
|
|
6
|
+
* conforming to the SmartStack customisation-ui baseline
|
|
7
|
+
* (`@/components/ui/PageTemplate` + `@/components/ui/DataTable` +
|
|
8
|
+
* token-based Tailwind, lucide-react icons, PermissionGuard + Slot/Fill).
|
|
9
|
+
* - Locale JSON files under web/{appCode}-web/src/i18n/locales/{fr,en,it,de}/{module}.json
|
|
10
|
+
* — one file per MODULE (the entities of a module share it). The CLI writer
|
|
11
|
+
* (index.ts) deep-merges into the existing file, so generating one entity never
|
|
12
|
+
* wipes a sibling entity's keys. Concurrent generation of two entities in the
|
|
13
|
+
* SAME module is therefore unsafe (read-modify-write race) — serialize within a
|
|
14
|
+
* module (see ba-develop/SKILL.md § Phase 3a).
|
|
15
|
+
*
|
|
16
|
+
* Baseline anatomy (derived verbatim from D:\01 - projets\SmartStack.app\features\customisation-ui
|
|
17
|
+
* pages administration/uiConfiguration/{ThemesListPage, PresetsListPage},
|
|
18
|
+
* administration/tenants/OrganisationCreatePage, administration/tenants/TenantDetailPage):
|
|
19
|
+
*
|
|
20
|
+
* - Every page is wrapped in `<PageTemplate title={t('list.title')} icon={<Icon />} actions={...}>`.
|
|
21
|
+
* - List views use `<DataTable data columns searchable pagination>`.
|
|
22
|
+
* - Detail views use card sections with `rounded-[var(--radius-card)] border
|
|
23
|
+
* border-[var(--border-color)] bg-[var(--bg-card)]`.
|
|
24
|
+
* - Form views use `.input` class for fields, primary CTA with
|
|
25
|
+
* `bg-[var(--color-accent-500)] hover:bg-[var(--color-accent-600)] text-white`.
|
|
26
|
+
* - All colors through CSS vars — no Tailwind color palette (`bg-red-500` etc.).
|
|
27
|
+
* - Icons from lucide-react only.
|
|
28
|
+
* - i18n namespace = {module}, keys nested under entity key (lowercased).
|
|
29
|
+
* - PermissionGuard keys: `{module}.{section}.{action}` — no appCode prefix.
|
|
30
|
+
* - Slot/Fill points: {entity}.header.actions, {entity}.table.columns.before/after,
|
|
31
|
+
* {entity}.form.fields.before/after, {entity}.detail.header/sidebar.
|
|
32
|
+
*
|
|
33
|
+
* No legacy patterns from the prior generate.ts are carried forward.
|
|
34
|
+
*/
|
|
35
|
+
import type { ScaffoldComponentInput, GeneratedFile, ComponentField } from './types.js'
|
|
36
|
+
|
|
37
|
+
const LOCALES = ['fr', 'en', 'it', 'de'] as const
|
|
38
|
+
type Locale = (typeof LOCALES)[number]
|
|
39
|
+
|
|
40
|
+
const GENERATED_MARKER = '// @generated-by scaffold-component — DO NOT HAND-EDIT (re-run the CLI to update)\n'
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Convert a flat i18n catalogue (e.g. `{ 'list.title': 'Employés' }`) to the
|
|
44
|
+
* nested form i18next expects in JSON files (`{ list: { title: 'Employés' } }`).
|
|
45
|
+
* Used in enriched mode when `spec.pageSpec.i18nKeys` is provided — the
|
|
46
|
+
* canonical PRD shape stores flat keys (one entry per `t('key')` call) and the
|
|
47
|
+
* generator materialises the nested tree at write time.
|
|
48
|
+
*/
|
|
49
|
+
function flatToNested(flat: Record<string, string>): Record<string, unknown> {
|
|
50
|
+
const root: Record<string, unknown> = {}
|
|
51
|
+
for (const [dottedKey, value] of Object.entries(flat)) {
|
|
52
|
+
const parts = dottedKey.split('.')
|
|
53
|
+
let cursor: Record<string, unknown> = root
|
|
54
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
55
|
+
const part = parts[i]!
|
|
56
|
+
const next = cursor[part]
|
|
57
|
+
if (typeof next === 'object' && next !== null && !Array.isArray(next)) {
|
|
58
|
+
cursor = next as Record<string, unknown>
|
|
59
|
+
} else {
|
|
60
|
+
const fresh: Record<string, unknown> = {}
|
|
61
|
+
cursor[part] = fresh
|
|
62
|
+
cursor = fresh
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
cursor[parts[parts.length - 1]!] = value
|
|
66
|
+
}
|
|
67
|
+
return root
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function translateLabel(label: string, locale: Locale): string {
|
|
71
|
+
const dictionary: Record<string, Record<Locale, string>> = {
|
|
72
|
+
Loading: { fr: 'Chargement…', en: 'Loading…', it: 'Caricamento…', de: 'Wird geladen…' },
|
|
73
|
+
NotFound: { fr: 'Introuvable', en: 'Not found', it: 'Non trovato', de: 'Nicht gefunden' },
|
|
74
|
+
Empty: { fr: 'Aucune donnée', en: 'No data', it: 'Nessun dato', de: 'Keine Daten' },
|
|
75
|
+
Create: { fr: 'Créer', en: 'Create', it: 'Crea', de: 'Erstellen' },
|
|
76
|
+
Edit: { fr: 'Modifier', en: 'Edit', it: 'Modifica', de: 'Bearbeiten' },
|
|
77
|
+
Delete: { fr: 'Supprimer', en: 'Delete', it: 'Elimina', de: 'Löschen' },
|
|
78
|
+
Update: { fr: 'Mettre à jour', en: 'Update', it: 'Aggiorna', de: 'Aktualisieren' },
|
|
79
|
+
Actions: { fr: 'Actions', en: 'Actions', it: 'Azioni', de: 'Aktionen' },
|
|
80
|
+
Save: { fr: 'Enregistrer', en: 'Save', it: 'Salva', de: 'Speichern' },
|
|
81
|
+
Cancel: { fr: 'Annuler', en: 'Cancel', it: 'Annulla', de: 'Abbrechen' },
|
|
82
|
+
Search: { fr: 'Rechercher…', en: 'Search…', it: 'Cerca…', de: 'Suchen…' },
|
|
83
|
+
Subtitle: { fr: 'Gérer les', en: 'Manage', it: 'Gestisci', de: 'Verwalten' },
|
|
84
|
+
Error: { fr: 'Erreur', en: 'Error', it: 'Errore', de: 'Fehler' },
|
|
85
|
+
Dashboard: { fr: 'Tableau de bord', en: 'Dashboard', it: 'Cruscotto', de: 'Dashboard' },
|
|
86
|
+
StartDate: { fr: 'Date de début', en: 'Start date', it: 'Data inizio', de: 'Startdatum' },
|
|
87
|
+
EndDate: { fr: 'Date de fin', en: 'End date', it: 'Data fine', de: 'Enddatum' },
|
|
88
|
+
Alerts: { fr: 'Alertes', en: 'Alerts', it: 'Avvisi', de: 'Warnungen' },
|
|
89
|
+
NoAlerts: { fr: 'Aucune alerte', en: 'No alerts', it: 'Nessun avviso', de: 'Keine Warnungen' },
|
|
90
|
+
SeverityInfo: { fr: 'Info', en: 'Info', it: 'Info', de: 'Info' },
|
|
91
|
+
SeverityWarning: { fr: 'Avertissement', en: 'Warning', it: 'Avviso', de: 'Warnung' },
|
|
92
|
+
SeverityCritical: { fr: 'Critique', en: 'Critical', it: 'Critico', de: 'Kritisch' },
|
|
93
|
+
Kpis: { fr: 'Indicateurs clés', en: 'Key indicators', it: 'Indicatori chiave', de: 'Kennzahlen' },
|
|
94
|
+
Navigation: { fr: 'Navigation', en: 'Navigation', it: 'Navigazione', de: 'Navigation' },
|
|
95
|
+
}
|
|
96
|
+
const entry = dictionary[label]
|
|
97
|
+
if (!entry) throw new Error(`Missing translation for UI label '${label}' — update CLI dictionary`)
|
|
98
|
+
return entry[locale]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Map widget type to a sensible default Lucide icon. */
|
|
102
|
+
const WIDGET_TYPE_ICONS: Record<string, string> = {
|
|
103
|
+
kpi: 'TrendingUp',
|
|
104
|
+
counter: 'Hash',
|
|
105
|
+
'chart-pie': 'PieChart',
|
|
106
|
+
'chart-bar': 'BarChart3',
|
|
107
|
+
'chart-line': 'LineChart',
|
|
108
|
+
list: 'List',
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Convert kebab-case icon name to PascalCase Lucide import (e.g. `file-text` → `FileText`). */
|
|
112
|
+
function kebabToPascal(kebab: string): string {
|
|
113
|
+
return kebab.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join('')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function humanize(name: string): string {
|
|
117
|
+
return name
|
|
118
|
+
.replace(/([A-Z])/g, ' $1')
|
|
119
|
+
.replace(/[_-]+/g, ' ')
|
|
120
|
+
.trim()
|
|
121
|
+
.replace(/^./, c => c.toUpperCase())
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** kebab → camelCase (`bulk-archive` → `bulkArchive`). Used for mutation
|
|
125
|
+
* variable names that match the api-client service member naming. */
|
|
126
|
+
function toCamel(code: string): string {
|
|
127
|
+
const parts = code.split('-')
|
|
128
|
+
return parts[0]! + parts.slice(1).map(p => p.charAt(0).toUpperCase() + p.slice(1)).join('')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function pluralize(singular: string, locale: Locale): string {
|
|
132
|
+
const lowercaseLast = singular.toLowerCase()
|
|
133
|
+
if (locale === 'it') return lowercaseLast.endsWith('a') ? singular.slice(0, -1) + 'e' : singular + 's'
|
|
134
|
+
return singular + 's'
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** PascalCase → camelCase (e.g. `FirstName` → `firstName`). */
|
|
138
|
+
function fieldToCamel(name: string): string {
|
|
139
|
+
if (name.length === 0) return name
|
|
140
|
+
return name.charAt(0).toLowerCase() + name.slice(1)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const I18N_FIELD_PARENTS = new Set(['fields', 'columns', 'filters', 'help'])
|
|
144
|
+
|
|
145
|
+
function normalizeI18nFieldKeys(flat: Record<string, string>): Record<string, string> {
|
|
146
|
+
const result: Record<string, string> = {}
|
|
147
|
+
for (const [key, value] of Object.entries(flat)) {
|
|
148
|
+
const parts = key.split('.')
|
|
149
|
+
if (parts.length >= 3) {
|
|
150
|
+
const parentSegment = parts[parts.length - 2]!
|
|
151
|
+
if (I18N_FIELD_PARENTS.has(parentSegment)) {
|
|
152
|
+
parts[parts.length - 1] = fieldToCamel(parts[parts.length - 1]!)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
result[parts.join('.')] = value
|
|
156
|
+
}
|
|
157
|
+
return result
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Symmetric to normalizeI18nFieldKeys() — applies the same camelCase
|
|
162
|
+
* normalisation rule on a single labelKey BEFORE it lands in the TSX
|
|
163
|
+
* `t('...')` call. Ensures the key the component requests matches the
|
|
164
|
+
* key the JSON catalogue ships (which goes through normalizeI18nFieldKeys
|
|
165
|
+
* at line ~1400). Without this symmetry, a PRD-emitted `list.columns.Code`
|
|
166
|
+
* (PascalCase) becomes `list.columns.code` in JSON but stays
|
|
167
|
+
* `list.columns.Code` in TSX → untranslated raw key on screen.
|
|
168
|
+
*
|
|
169
|
+
* Path is preserved verbatim — only the leaf is camelCased, and only when
|
|
170
|
+
* its parent is in I18N_FIELD_PARENTS. Idempotent: re-camelCasing a
|
|
171
|
+
* camelCase leaf is a no-op, so legacy pagespecs already in camelCase
|
|
172
|
+
* are unaffected. Action labelKeys (e.g. `list.create`, `form.actions.toggle-actif`)
|
|
173
|
+
* stay verbatim because their parent (`list`, `actions`) is not a field-parent —
|
|
174
|
+
* symmetric with normalizeI18nFieldKeys which leaves them alone too.
|
|
175
|
+
*/
|
|
176
|
+
function normalizeI18nKey(key: string): string {
|
|
177
|
+
const parts = key.split('.')
|
|
178
|
+
if (parts.length >= 3) {
|
|
179
|
+
const parentSegment = parts[parts.length - 2]!
|
|
180
|
+
if (I18N_FIELD_PARENTS.has(parentSegment)) {
|
|
181
|
+
parts[parts.length - 1] = fieldToCamel(parts[parts.length - 1]!)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return parts.join('.')
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function buildTranslations(spec: ScaffoldComponentInput, locale: Locale): Record<string, unknown> {
|
|
188
|
+
const fieldLabels: Record<string, string> = {}
|
|
189
|
+
for (const f of spec.fields) {
|
|
190
|
+
fieldLabels[fieldToCamel(f.name)] = f.label ?? humanize(f.name)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const entityName = humanize(spec.entity)
|
|
194
|
+
const pluralName = pluralize(entityName, locale)
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
list: {
|
|
198
|
+
title: pluralName,
|
|
199
|
+
subtitle: `${translateLabel('Subtitle', locale)} ${pluralName.toLowerCase()}`,
|
|
200
|
+
search: translateLabel('Search', locale),
|
|
201
|
+
loading: translateLabel('Loading', locale),
|
|
202
|
+
empty: translateLabel('Empty', locale),
|
|
203
|
+
create: translateLabel('Create', locale),
|
|
204
|
+
edit: translateLabel('Edit', locale),
|
|
205
|
+
delete: translateLabel('Delete', locale),
|
|
206
|
+
actionsColumn: translateLabel('Actions', locale),
|
|
207
|
+
error: translateLabel('Error', locale),
|
|
208
|
+
columns: fieldLabels,
|
|
209
|
+
},
|
|
210
|
+
detail: {
|
|
211
|
+
loading: translateLabel('Loading', locale),
|
|
212
|
+
notFound: translateLabel('NotFound', locale),
|
|
213
|
+
edit: translateLabel('Edit', locale),
|
|
214
|
+
fields: fieldLabels,
|
|
215
|
+
},
|
|
216
|
+
form: {
|
|
217
|
+
createTitle: `${translateLabel('Create', locale)} ${entityName}`,
|
|
218
|
+
editTitle: `${translateLabel('Edit', locale)} ${entityName}`,
|
|
219
|
+
submitCreate: translateLabel('Create', locale),
|
|
220
|
+
submitUpdate: translateLabel('Update', locale),
|
|
221
|
+
save: translateLabel('Save', locale),
|
|
222
|
+
cancel: translateLabel('Cancel', locale),
|
|
223
|
+
fields: fieldLabels,
|
|
224
|
+
},
|
|
225
|
+
dashboard: {
|
|
226
|
+
title: `${translateLabel('Dashboard', locale)} ${pluralName.toLowerCase()}`,
|
|
227
|
+
subtitle: pluralName,
|
|
228
|
+
loading: translateLabel('Loading', locale),
|
|
229
|
+
error: translateLabel('Error', locale),
|
|
230
|
+
startDate: translateLabel('StartDate', locale),
|
|
231
|
+
endDate: translateLabel('EndDate', locale),
|
|
232
|
+
kpi: {} as Record<string, string>,
|
|
233
|
+
alerts: {
|
|
234
|
+
title: translateLabel('Alerts', locale),
|
|
235
|
+
empty: translateLabel('NoAlerts', locale),
|
|
236
|
+
severity: {
|
|
237
|
+
info: translateLabel('SeverityInfo', locale),
|
|
238
|
+
warning: translateLabel('SeverityWarning', locale),
|
|
239
|
+
critical: translateLabel('SeverityCritical', locale),
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
home: {
|
|
244
|
+
title: pluralName,
|
|
245
|
+
subtitle: `${translateLabel('Subtitle', locale)} ${pluralName.toLowerCase()}`,
|
|
246
|
+
kpis: translateLabel('Kpis', locale),
|
|
247
|
+
navigation: translateLabel('Navigation', locale),
|
|
248
|
+
},
|
|
249
|
+
breadcrumb: {
|
|
250
|
+
section: humanize(spec.section),
|
|
251
|
+
},
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function defaultForField(f: ComponentField): string {
|
|
256
|
+
if (f.defaultValue !== undefined && f.defaultValue !== null) {
|
|
257
|
+
if (typeof f.defaultValue === 'string') return `'${f.defaultValue}'`
|
|
258
|
+
return String(f.defaultValue)
|
|
259
|
+
}
|
|
260
|
+
switch (f.type.toLowerCase()) {
|
|
261
|
+
case 'string': return `''`
|
|
262
|
+
case 'number': case 'int': case 'integer': case 'decimal': return `0`
|
|
263
|
+
case 'bool': case 'boolean': return `false`
|
|
264
|
+
case 'datetime': case 'date': return `''`
|
|
265
|
+
default: return `''`
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function formDataTypeLiteral(fields: ComponentField[]): string {
|
|
270
|
+
if (fields.length === 0) return `Record<string, unknown>`
|
|
271
|
+
const lines = fields.map(f => ` ${fieldToCamel(f.name)}${f.required ? '' : '?'}: ${toTsType(f.type)}`)
|
|
272
|
+
return `{\n${lines.join('\n')}\n}`
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function toTsType(type: string): string {
|
|
276
|
+
switch (type.toLowerCase()) {
|
|
277
|
+
case 'string': case 'guid': case 'datetime': case 'date': return 'string'
|
|
278
|
+
case 'number': case 'int': case 'integer': case 'decimal': return 'number'
|
|
279
|
+
case 'bool': case 'boolean': return 'boolean'
|
|
280
|
+
default: return 'string'
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function generate(spec: ScaffoldComponentInput): GeneratedFile[] {
|
|
285
|
+
if (spec.fields.length === 0) {
|
|
286
|
+
throw new Error(`scaffold-component: entity '${spec.entity}' has no fields — scaffolding would produce an empty UI`)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const files: GeneratedFile[] = []
|
|
290
|
+
const e = spec.entity
|
|
291
|
+
const eLower = e.charAt(0).toLowerCase() + e.slice(1)
|
|
292
|
+
const webRoot = spec.webRoot ?? `web/${spec.appCode}-web`
|
|
293
|
+
const plural = e + 's'
|
|
294
|
+
// Pages location. When `pageSpec.filePath` is provided, IT is the source
|
|
295
|
+
// of truth — derive the directory from it. This handles legacy folder
|
|
296
|
+
// naming (e.g. `pages/budgeting/budgets/` instead of the canonical
|
|
297
|
+
// `pages/budgets/budgets/`). Without this override the scaffold would
|
|
298
|
+
// write to a fresh directory and abandon the existing pages, leaving
|
|
299
|
+
// the user with two copies. Falls back to the canonical
|
|
300
|
+
// `src/pages/{appCode}/{module}/{section}` when no pageSpec provided.
|
|
301
|
+
const base = spec.pageSpec?.filePath
|
|
302
|
+
? spec.pageSpec.filePath.replace(/\\/g, '/').replace(/\/[^/]+\.tsx$/i, '')
|
|
303
|
+
: `src/pages/${spec.appCode.toLowerCase()}/${spec.module}/${spec.section}`
|
|
304
|
+
|
|
305
|
+
// When `pageSpec.filePath` matches the current view we want to honour the
|
|
306
|
+
// exact file name from the PRD (e.g. `BudgetsModuleHomePage.tsx`) rather
|
|
307
|
+
// than the entity-derived default. The single-view-per-call invariant
|
|
308
|
+
// (Phase 3a passes `views: [pageSpec.view]`) guarantees the match logic
|
|
309
|
+
// is unambiguous: at most one view in the loop can claim the pageSpec.
|
|
310
|
+
function pathFor(view: string, defaultName: string): string {
|
|
311
|
+
if (spec.pageSpec && spec.pageSpec.view === view && spec.pageSpec.filePath) {
|
|
312
|
+
return spec.pageSpec.filePath.replace(/\\/g, '/')
|
|
313
|
+
}
|
|
314
|
+
return `${base}/${defaultName}`
|
|
315
|
+
}
|
|
316
|
+
const featurePath = `@/features/${spec.module}/${eLower}`
|
|
317
|
+
// Permissions are 4-seg: {app}.{module}.{section}.{action}. The auth
|
|
318
|
+
// service stores them in this shape (ba_permissions.path), and
|
|
319
|
+
// useAuth().hasPermission() does exact-string match. Without the
|
|
320
|
+
// appCode prefix, the match fails → PermissionGuard hides everything →
|
|
321
|
+
// pages render empty in the browser ("aucun changement" symptom).
|
|
322
|
+
// appCode is lowercased to match the DB convention (e.g. TestV2 → testv2).
|
|
323
|
+
const permKey = `${spec.module}.${spec.section}`
|
|
324
|
+
// `spec.section` is kebab-case (URL segment), but `scaffold-routes` emits the
|
|
325
|
+
// `routes` object with camelCase property keys (`typesAffaire`, not `types-affaire`)
|
|
326
|
+
// — so any `routes.<section>` reference in generated TSX must use the same
|
|
327
|
+
// transform. Without it, `routes.types-affaire.detail(item.id)` is parsed by
|
|
328
|
+
// JS as `routes.types - affaire.detail(item.id)` (subtraction), throwing
|
|
329
|
+
// `ReferenceError: affaire is not defined` at the first interaction.
|
|
330
|
+
const sectionCamel = toCamel(spec.section)
|
|
331
|
+
// List file uses the plural entity name (BudgetsListPage.tsx) to match the
|
|
332
|
+
// existing project convention; detail/form/dashboard stay singular.
|
|
333
|
+
const listFileName = `${plural}ListPage.tsx`
|
|
334
|
+
// Form fields exclude computed attributes — they are read-only, projected
|
|
335
|
+
// by the backend, and must NEVER appear as inputs.
|
|
336
|
+
const formFields = spec.fields.filter(f => !f.formula)
|
|
337
|
+
const headerField = spec.fields.find(f => !f.formula) ?? spec.fields[0]
|
|
338
|
+
const initialFormObj = `{\n${formFields.map(f => ` ${fieldToCamel(f.name)}: ${defaultForField(f)},`).join('\n')}\n }`
|
|
339
|
+
|
|
340
|
+
// ─── Shared custom-action infrastructure ─────────────────────────────────
|
|
341
|
+
// Hoisted here so list, detail, AND form blocks can all access it.
|
|
342
|
+
type PageActionMeta = {
|
|
343
|
+
code: string
|
|
344
|
+
kind?: 'api' | 'navigate'
|
|
345
|
+
scope: 'header' | 'row' | 'bulk'
|
|
346
|
+
labelKey: string
|
|
347
|
+
permission: string
|
|
348
|
+
variant?: string
|
|
349
|
+
targetRoute?: string
|
|
350
|
+
targetScreen?: string
|
|
351
|
+
endpoint?: string
|
|
352
|
+
httpMethod?: string
|
|
353
|
+
ucReference?: string
|
|
354
|
+
}
|
|
355
|
+
const STANDARD_CRUD_CODES = new Set(['create', 'edit', 'update', 'delete', 'list', 'detail', 'read'])
|
|
356
|
+
const isNavigate = (a: PageActionMeta): boolean => {
|
|
357
|
+
if (a.kind === 'navigate') return true
|
|
358
|
+
if (a.kind === 'api') return false
|
|
359
|
+
return a.code === 'open' && a.scope === 'row'
|
|
360
|
+
}
|
|
361
|
+
const isApi = (a: PageActionMeta): boolean => !isNavigate(a)
|
|
362
|
+
const hookCodeOf = (a: PageActionMeta): string => a.endpoint ?? a.code
|
|
363
|
+
const toPascal = (code: string): string =>
|
|
364
|
+
code.split('-').map(p => p.charAt(0).toUpperCase() + p.slice(1)).join('')
|
|
365
|
+
|
|
366
|
+
const ICON_FOR_CODE: Record<string, string> = {
|
|
367
|
+
open: 'ExternalLink',
|
|
368
|
+
close: 'XCircle',
|
|
369
|
+
archive: 'Archive',
|
|
370
|
+
restore: 'ArchiveRestore',
|
|
371
|
+
duplicate: 'Copy',
|
|
372
|
+
approve: 'Check',
|
|
373
|
+
reject: 'X',
|
|
374
|
+
validate: 'CheckCircle',
|
|
375
|
+
cancel: 'XCircle',
|
|
376
|
+
submit: 'Send',
|
|
377
|
+
'submit-for-review': 'Send',
|
|
378
|
+
activate: 'Power',
|
|
379
|
+
deactivate: 'PowerOff',
|
|
380
|
+
assign: 'UserPlus',
|
|
381
|
+
unassign: 'UserMinus',
|
|
382
|
+
export: 'Download',
|
|
383
|
+
import: 'Upload',
|
|
384
|
+
print: 'Printer',
|
|
385
|
+
share: 'Share2',
|
|
386
|
+
lock: 'Lock',
|
|
387
|
+
unlock: 'Unlock',
|
|
388
|
+
'toggle-actif': 'ToggleRight',
|
|
389
|
+
'set-availability': 'CalendarCheck',
|
|
390
|
+
'map-to-pce': 'ArrowRightLeft',
|
|
391
|
+
'core-alignment': 'GitMerge',
|
|
392
|
+
'analyze-impact': 'Search',
|
|
393
|
+
'sync-from-pce': 'RefreshCw',
|
|
394
|
+
'sync-from-swisstopo': 'MapPin',
|
|
395
|
+
'open-year': 'CalendarPlus',
|
|
396
|
+
'close-year': 'CalendarMinus',
|
|
397
|
+
'publish-to-pce': 'Upload',
|
|
398
|
+
'detect-anomalies': 'AlertTriangle',
|
|
399
|
+
'bulk-map': 'Layers',
|
|
400
|
+
}
|
|
401
|
+
const iconForCode = (code: string): string => ICON_FOR_CODE[code] ?? 'ChevronRight'
|
|
402
|
+
|
|
403
|
+
const allCustomActions = ((spec.pageSpec?.actions ?? []) as PageActionMeta[])
|
|
404
|
+
.filter(a => !STANDARD_CRUD_CODES.has(a.code.toLowerCase()))
|
|
405
|
+
|
|
406
|
+
/** Render a header action button JSX string (used by list, detail and form). */
|
|
407
|
+
const renderHeaderActionButton = (a: PageActionMeta): string => {
|
|
408
|
+
const Icon = iconForCode(a.code)
|
|
409
|
+
const isPrimary = a.variant === 'primary'
|
|
410
|
+
const className = isPrimary
|
|
411
|
+
? 'flex items-center gap-1.5 px-3 py-2 rounded-[var(--radius-button)] bg-[var(--color-accent-500)] text-white text-sm font-medium hover:bg-[var(--color-accent-600)] transition-colors'
|
|
412
|
+
: 'flex items-center gap-1.5 px-3 py-2 rounded-[var(--radius-button)] bg-[var(--bg-secondary)] text-[var(--text-primary)] text-sm font-medium hover:bg-[var(--bg-hover)] transition-colors'
|
|
413
|
+
return ` <PermissionGuard permission="${a.permission}">
|
|
414
|
+
<button
|
|
415
|
+
type="button"
|
|
416
|
+
onClick={() => { void handle${toPascal(a.code)}() }}
|
|
417
|
+
className="${className}"
|
|
418
|
+
>
|
|
419
|
+
<${Icon} className="w-4 h-4" />
|
|
420
|
+
{t('${eLower}.${a.labelKey}')}
|
|
421
|
+
</button>
|
|
422
|
+
</PermissionGuard>`
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ─── ListPage ────────────────────────────────────────────────────────────
|
|
426
|
+
if (spec.views.includes('list')) {
|
|
427
|
+
const customActions = allCustomActions
|
|
428
|
+
const apiActions = customActions.filter(isApi)
|
|
429
|
+
|
|
430
|
+
// pageSpec.columns is the PRD-curated list (ba_screens.config.columns →
|
|
431
|
+
// create-prd → pageSpec). When present, use it verbatim so the page
|
|
432
|
+
// shows EXACTLY what the BA asked for — no more, no less. The data model
|
|
433
|
+
// can carry legacy columns (e.g. `description` on the Budget entity) that
|
|
434
|
+
// the BA did not declare for this view; using spec.fields would surface
|
|
435
|
+
// them as untranslated `LIST.COLUMNS.DESCRIPTION` headers.
|
|
436
|
+
// Fallback: spec.fields[0..6] for legacy projects without pageSpec.columns
|
|
437
|
+
// (PRD predates the per-page contract).
|
|
438
|
+
const columnSource = spec.pageSpec?.columns?.length
|
|
439
|
+
? spec.pageSpec.columns.map((c) => ({
|
|
440
|
+
name: fieldToCamel(c.key),
|
|
441
|
+
// Fix #9 — normalise leaf to match the JSON i18n catalogue
|
|
442
|
+
// (see normalizeI18nKey JSDoc). PRD may emit PascalCase
|
|
443
|
+
// (`list.columns.Code`); JSON normalises to camelCase; TSX must follow.
|
|
444
|
+
labelKey: normalizeI18nKey(c.labelKey),
|
|
445
|
+
sortable: c.sortable ?? false,
|
|
446
|
+
}))
|
|
447
|
+
: spec.fields.slice(0, 6).map((f) => ({
|
|
448
|
+
name: fieldToCamel(f.name),
|
|
449
|
+
labelKey: `list.columns.${fieldToCamel(f.name)}`,
|
|
450
|
+
sortable: true,
|
|
451
|
+
}))
|
|
452
|
+
const columnEntries = columnSource.map(c =>
|
|
453
|
+
` { key: '${c.name}', label: t('${eLower}.${c.labelKey}'), sortable: ${c.sortable} }`
|
|
454
|
+
).join(',\n')
|
|
455
|
+
|
|
456
|
+
// pageSpec.filters[] generates a FilterBar above the DataTable. Each entry
|
|
457
|
+
// produces ONE controlled input whose value lives in local React state.
|
|
458
|
+
// For text & select controls we apply client-side filtering on data?.items
|
|
459
|
+
// immediately; date-range UI is rendered but its filtering relies on the
|
|
460
|
+
// hook accepting the values (not yet plumbed — left as v2 enhancement).
|
|
461
|
+
const pageSpecFilters = ((spec.pageSpec?.filters ?? []) as Array<{
|
|
462
|
+
field: string
|
|
463
|
+
labelKey: string
|
|
464
|
+
control?: string
|
|
465
|
+
options?: unknown[]
|
|
466
|
+
}>).map(f => ({
|
|
467
|
+
...f,
|
|
468
|
+
// Fix #9 — normalise leaf to match the JSON i18n catalogue. One pass
|
|
469
|
+
// here means renderFilterInput() (6 substitution sites for select/text/
|
|
470
|
+
// date-range/boolean labels + placeholders) does not need to be touched.
|
|
471
|
+
labelKey: normalizeI18nKey(f.labelKey),
|
|
472
|
+
}))
|
|
473
|
+
const hasFilters = pageSpecFilters.length > 0
|
|
474
|
+
const reactImport = hasFilters ? `import { useMemo, useState } from 'react'\n` : ''
|
|
475
|
+
|
|
476
|
+
function renderFilterInput(f: typeof pageSpecFilters[number]): string {
|
|
477
|
+
const ctrl = (f.control ?? 'text').toLowerCase()
|
|
478
|
+
if (ctrl === 'select') {
|
|
479
|
+
const opts = Array.isArray(f.options) ? f.options : []
|
|
480
|
+
// When the PRD declares control="select" but provides no options,
|
|
481
|
+
// fall back to a text input — an empty <select> is non-functional.
|
|
482
|
+
if (opts.length === 0) {
|
|
483
|
+
return ` <div className="flex flex-col gap-1 min-w-[180px]">
|
|
484
|
+
<label className="text-xs font-medium text-[var(--text-muted)]">{t('${eLower}.${f.labelKey}')}</label>
|
|
485
|
+
<input
|
|
486
|
+
type="text"
|
|
487
|
+
value={filters['${f.field}'] ?? ''}
|
|
488
|
+
onChange={(event) => onFilterChange('${f.field}', event.target.value)}
|
|
489
|
+
placeholder={t('${eLower}.${f.labelKey}')}
|
|
490
|
+
className="input"
|
|
491
|
+
/>
|
|
492
|
+
</div>`
|
|
493
|
+
}
|
|
494
|
+
const optionLines = opts.map((opt) => {
|
|
495
|
+
if (typeof opt === 'string') return ` <option value="${opt}">${opt}</option>`
|
|
496
|
+
const o = opt as { value?: string; label?: string }
|
|
497
|
+
const val = String(o.value ?? '')
|
|
498
|
+
const lab = String(o.label ?? o.value ?? '')
|
|
499
|
+
return ` <option value="${val}">${lab}</option>`
|
|
500
|
+
}).join('\n')
|
|
501
|
+
return ` <div className="flex flex-col gap-1 min-w-[180px]">
|
|
502
|
+
<label className="text-xs font-medium text-[var(--text-muted)]">{t('${eLower}.${f.labelKey}')}</label>
|
|
503
|
+
<select
|
|
504
|
+
value={filters['${f.field}'] ?? ''}
|
|
505
|
+
onChange={(event) => onFilterChange('${f.field}', event.target.value)}
|
|
506
|
+
className="input"
|
|
507
|
+
>
|
|
508
|
+
<option value="">{t('${eLower}.list.filters.all')}</option>
|
|
509
|
+
${optionLines}
|
|
510
|
+
</select>
|
|
511
|
+
</div>`
|
|
512
|
+
}
|
|
513
|
+
if (ctrl === 'date-range' || ctrl === 'daterange') {
|
|
514
|
+
return ` <div className="flex flex-col gap-1">
|
|
515
|
+
<label className="text-xs font-medium text-[var(--text-muted)]">{t('${eLower}.${f.labelKey}')}</label>
|
|
516
|
+
<div className="flex items-center gap-1">
|
|
517
|
+
<input
|
|
518
|
+
type="date"
|
|
519
|
+
value={filters['${f.field}From'] ?? ''}
|
|
520
|
+
onChange={(event) => onFilterChange('${f.field}From', event.target.value)}
|
|
521
|
+
className="input"
|
|
522
|
+
/>
|
|
523
|
+
<span className="text-[var(--text-muted)]">→</span>
|
|
524
|
+
<input
|
|
525
|
+
type="date"
|
|
526
|
+
value={filters['${f.field}To'] ?? ''}
|
|
527
|
+
onChange={(event) => onFilterChange('${f.field}To', event.target.value)}
|
|
528
|
+
className="input"
|
|
529
|
+
/>
|
|
530
|
+
</div>
|
|
531
|
+
</div>`
|
|
532
|
+
}
|
|
533
|
+
if (ctrl === 'boolean') {
|
|
534
|
+
return ` <label className="flex items-center gap-1 self-end text-sm text-[var(--text-primary)]">
|
|
535
|
+
<input
|
|
536
|
+
type="checkbox"
|
|
537
|
+
checked={filters['${f.field}'] === 'true'}
|
|
538
|
+
onChange={(event) => onFilterChange('${f.field}', event.target.checked ? 'true' : '')}
|
|
539
|
+
/>
|
|
540
|
+
{t('${eLower}.${f.labelKey}')}
|
|
541
|
+
</label>`
|
|
542
|
+
}
|
|
543
|
+
// text + fallback
|
|
544
|
+
return ` <div className="flex flex-col gap-1 min-w-[180px]">
|
|
545
|
+
<label className="text-xs font-medium text-[var(--text-muted)]">{t('${eLower}.${f.labelKey}')}</label>
|
|
546
|
+
<input
|
|
547
|
+
type="text"
|
|
548
|
+
value={filters['${f.field}'] ?? ''}
|
|
549
|
+
onChange={(event) => onFilterChange('${f.field}', event.target.value)}
|
|
550
|
+
placeholder={t('${eLower}.${f.labelKey}')}
|
|
551
|
+
className="input"
|
|
552
|
+
/>
|
|
553
|
+
</div>`
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const filterStateInitEntries = pageSpecFilters.flatMap(f => {
|
|
557
|
+
const ctrl = (f.control ?? 'text').toLowerCase()
|
|
558
|
+
if (ctrl === 'date-range' || ctrl === 'daterange') {
|
|
559
|
+
return [`'${f.field}From': ''`, `'${f.field}To': ''`]
|
|
560
|
+
}
|
|
561
|
+
return [`'${f.field}': ''`]
|
|
562
|
+
}).join(', ')
|
|
563
|
+
|
|
564
|
+
// Bug 4 fix — DTOs emitted by scaffold-api-client are SEALED interfaces (no
|
|
565
|
+
// index signature), so the single cast `item as Record<string, unknown>` is
|
|
566
|
+
// rejected by TypeScript ("conversion may be a mistake — neither type
|
|
567
|
+
// sufficiently overlaps with the other"). The double-cast `as unknown as
|
|
568
|
+
// Record<string, unknown>` is the canonical TS idiom for crossing this
|
|
569
|
+
// boundary safely. We declare it once at filter-block top so each match
|
|
570
|
+
// expression stays readable.
|
|
571
|
+
const filterMatchersJs = pageSpecFilters.map(f => {
|
|
572
|
+
if (f.field === 'q') {
|
|
573
|
+
return ` if (filters['q']) {
|
|
574
|
+
const q = filters['q'].toLowerCase()
|
|
575
|
+
const match = Object.values(item as unknown as Record<string, unknown>).some(v => String(v ?? '').toLowerCase().includes(q))
|
|
576
|
+
if (!match) return false
|
|
577
|
+
}`
|
|
578
|
+
}
|
|
579
|
+
const ctrl = (f.control ?? 'text').toLowerCase()
|
|
580
|
+
if (ctrl === 'select' || ctrl === 'boolean') {
|
|
581
|
+
return ` if (filters['${f.field}'] && String((item as unknown as Record<string, unknown>)['${f.field}'] ?? '') !== filters['${f.field}']) return false`
|
|
582
|
+
}
|
|
583
|
+
if (ctrl === 'date-range' || ctrl === 'daterange') {
|
|
584
|
+
return ` if (filters['${f.field}From'] || filters['${f.field}To']) {
|
|
585
|
+
const cell = (item as unknown as Record<string, unknown>)['${f.field}']
|
|
586
|
+
const cellTime = cell ? new Date(String(cell)).getTime() : NaN
|
|
587
|
+
if (filters['${f.field}From'] && cellTime < new Date(filters['${f.field}From']).getTime()) return false
|
|
588
|
+
if (filters['${f.field}To'] && cellTime > new Date(filters['${f.field}To']).getTime()) return false
|
|
589
|
+
}`
|
|
590
|
+
}
|
|
591
|
+
// text, default
|
|
592
|
+
return ` if (filters['${f.field}']) {
|
|
593
|
+
const cell = String((item as unknown as Record<string, unknown>)['${f.field}'] ?? '').toLowerCase()
|
|
594
|
+
if (!cell.includes(filters['${f.field}'].toLowerCase())) return false
|
|
595
|
+
}`
|
|
596
|
+
}).join('\n')
|
|
597
|
+
|
|
598
|
+
const filterStateBlock = hasFilters
|
|
599
|
+
? ` const [filters, setFilters] = useState<Record<string, string>>({ ${filterStateInitEntries} })
|
|
600
|
+
const onFilterChange = (key: string, value: string) => setFilters((prev) => ({ ...prev, [key]: value }))
|
|
601
|
+
`
|
|
602
|
+
: ''
|
|
603
|
+
|
|
604
|
+
const customRowActions = customActions.filter(a => a.scope === 'row')
|
|
605
|
+
const customHeaderActions = customActions.filter(a => a.scope === 'header')
|
|
606
|
+
|
|
607
|
+
// Hook imports — ONLY for api-bound actions. Navigate actions do not call
|
|
608
|
+
// any hook (they just `navigate(...)`), so importing one would produce an
|
|
609
|
+
// unused-symbol warning at best, a phantom 405 at worst.
|
|
610
|
+
const customHookImports = apiActions
|
|
611
|
+
.map(a => `, use${toPascal(hookCodeOf(a))}${e}`)
|
|
612
|
+
.join('')
|
|
613
|
+
|
|
614
|
+
// Lucide icon imports — dedup to avoid duplicate identifiers. Both api +
|
|
615
|
+
// navigate actions need their icon, so the union covers every button.
|
|
616
|
+
const usedIcons = new Set(customActions.map(a => iconForCode(a.code)))
|
|
617
|
+
const customIconImports = Array.from(usedIcons).map(name => `, ${name}`).join('')
|
|
618
|
+
|
|
619
|
+
// Mutation declarations — one per api-bound custom action, e.g.
|
|
620
|
+
// const archiveMutation = useArchiveBudget()
|
|
621
|
+
// Navigate actions emit nothing here.
|
|
622
|
+
const customMutationDecls = apiActions
|
|
623
|
+
.map(a => ` const ${toCamel(hookCodeOf(a))}Mutation = use${toPascal(hookCodeOf(a))}${e}()`)
|
|
624
|
+
.join('\n')
|
|
625
|
+
|
|
626
|
+
// Handler functions — bridge button onClick to mutation.mutateAsync (api)
|
|
627
|
+
// or to navigate(targetRoute) (navigate). The two branches keep the same
|
|
628
|
+
// signature shape so the JSX caller is uniform.
|
|
629
|
+
const customHandlerDecls = customActions
|
|
630
|
+
.map(a => {
|
|
631
|
+
const name = `handle${toPascal(a.code)}`
|
|
632
|
+
if (isNavigate(a)) {
|
|
633
|
+
const target = a.targetRoute
|
|
634
|
+
?? (a.scope === 'row'
|
|
635
|
+
? `routes.${sectionCamel}.detail(item.id)`
|
|
636
|
+
: `routes.${sectionCamel}.list()`)
|
|
637
|
+
if (a.scope === 'header') {
|
|
638
|
+
return ` const ${name} = () => {\n navigate(${target})\n }`
|
|
639
|
+
}
|
|
640
|
+
return ` const ${name} = (item: ${e}) => {\n navigate(${target})\n }`
|
|
641
|
+
}
|
|
642
|
+
// kind: 'api' — call the matching React Query mutation.
|
|
643
|
+
const hookVar = `${toCamel(hookCodeOf(a))}Mutation`
|
|
644
|
+
if (a.scope === 'header') {
|
|
645
|
+
return ` const ${name} = async () => {\n await ${hookVar}.mutateAsync()\n }`
|
|
646
|
+
}
|
|
647
|
+
// row scope (bulk omitted from this template — needs DataTable selection support)
|
|
648
|
+
return ` const ${name} = async (item: ${e}) => {\n await ${hookVar}.mutateAsync(item.id)\n }`
|
|
649
|
+
})
|
|
650
|
+
.join('\n\n')
|
|
651
|
+
|
|
652
|
+
const customHeaderActionsJsx = customHeaderActions.map(renderHeaderActionButton).join('\n')
|
|
653
|
+
|
|
654
|
+
// Row action buttons JSX — icon-only, danger variant uses error-bg/text.
|
|
655
|
+
const renderRowActionButton = (a: PageActionMeta): string => {
|
|
656
|
+
const Icon = iconForCode(a.code)
|
|
657
|
+
const isDanger = a.variant === 'danger'
|
|
658
|
+
const hoverBg = isDanger ? 'hover:bg-[var(--error-bg)]' : 'hover:bg-[var(--bg-hover)]'
|
|
659
|
+
const hoverText = isDanger ? 'hover:text-[var(--error-text)]' : 'hover:text-[var(--text-primary)]'
|
|
660
|
+
return ` <PermissionGuard permission="${a.permission}">
|
|
661
|
+
<button
|
|
662
|
+
type="button"
|
|
663
|
+
onClick={(e) => { e.stopPropagation(); void handle${toPascal(a.code)}(item) }}
|
|
664
|
+
className="p-1.5 rounded ${hoverBg} text-[var(--text-muted)] ${hoverText}"
|
|
665
|
+
title={t('${eLower}.${a.labelKey}')}
|
|
666
|
+
>
|
|
667
|
+
<${Icon} className="w-4 h-4" />
|
|
668
|
+
</button>
|
|
669
|
+
</PermissionGuard>`
|
|
670
|
+
}
|
|
671
|
+
const customRowActionsJsx = customRowActions.map(renderRowActionButton).join('\n')
|
|
672
|
+
|
|
673
|
+
const filteredItemsBlock = hasFilters
|
|
674
|
+
? ` const items = data?.items ?? []
|
|
675
|
+
const filtered = useMemo(() => {
|
|
676
|
+
return items.filter((item) => {
|
|
677
|
+
${filterMatchersJs}
|
|
678
|
+
return true
|
|
679
|
+
})
|
|
680
|
+
}, [items, filters])
|
|
681
|
+
`
|
|
682
|
+
: ` const filtered = data?.items ?? []
|
|
683
|
+
`
|
|
684
|
+
|
|
685
|
+
const filterBarJsx = hasFilters
|
|
686
|
+
? ` <div className="mb-4 flex flex-wrap items-end gap-3 p-3 rounded-[var(--radius-card)] border border-[var(--border-color)] bg-[var(--bg-card)]">
|
|
687
|
+
${pageSpecFilters.map(renderFilterInput).join('\n')}
|
|
688
|
+
</div>
|
|
689
|
+
|
|
690
|
+
`
|
|
691
|
+
: ''
|
|
692
|
+
|
|
693
|
+
files.push({
|
|
694
|
+
path: pathFor('list', listFileName),
|
|
695
|
+
content: `${GENERATED_MARKER}${reactImport}import { useNavigate } from 'react-router-dom'
|
|
696
|
+
import { useTranslation } from 'react-i18next'
|
|
697
|
+
import { Loader2, Plus, Pencil, Trash2, List${customIconImports} } from 'lucide-react'
|
|
698
|
+
import { Slot } from '@atlashub/smartstack'
|
|
699
|
+
import { PermissionGuard } from '@/components/auth/PermissionGuard'
|
|
700
|
+
import { PageTemplate } from '@/components/ui/PageTemplate'
|
|
701
|
+
import { DataTable, type DataTableColumn } from '@/components/ui/DataTable'
|
|
702
|
+
import { routes } from '@/extensions/${spec.module}Routes'
|
|
703
|
+
import { use${plural}, useDelete${e}${customHookImports} } from '${featurePath}/hooks/use${e}'
|
|
704
|
+
import type { ${e} } from '${featurePath}/types'
|
|
705
|
+
|
|
706
|
+
export function ${plural}ListPage() {
|
|
707
|
+
const { t } = useTranslation('${spec.module}')
|
|
708
|
+
const navigate = useNavigate()
|
|
709
|
+
const { data, isLoading, error } = use${plural}()
|
|
710
|
+
const deleteMutation = useDelete${e}()
|
|
711
|
+
${customMutationDecls ? customMutationDecls + '\n' : ''}${filterStateBlock}
|
|
712
|
+
const handleDelete = async (item: ${e}) => {
|
|
713
|
+
await deleteMutation.mutateAsync(item.id)
|
|
714
|
+
}
|
|
715
|
+
${customHandlerDecls ? '\n' + customHandlerDecls + '\n' : ''}
|
|
716
|
+
${filteredItemsBlock}
|
|
717
|
+
const columns: DataTableColumn<${e}>[] = [
|
|
718
|
+
${columnEntries},
|
|
719
|
+
{
|
|
720
|
+
key: '__actions',
|
|
721
|
+
label: t('${eLower}.list.actionsColumn'),
|
|
722
|
+
align: 'right',
|
|
723
|
+
render: (item) => (
|
|
724
|
+
<div className="flex items-center justify-end gap-1">
|
|
725
|
+
<PermissionGuard permission="${permKey}.update">
|
|
726
|
+
<button
|
|
727
|
+
type="button"
|
|
728
|
+
onClick={(e) => { e.stopPropagation(); navigate(routes.${sectionCamel}.edit(item.id)) }}
|
|
729
|
+
className="p-1.5 rounded hover:bg-[var(--bg-hover)] text-[var(--text-muted)] hover:text-[var(--text-primary)]"
|
|
730
|
+
title={t('${eLower}.list.edit')}
|
|
731
|
+
>
|
|
732
|
+
<Pencil className="w-4 h-4" />
|
|
733
|
+
</button>
|
|
734
|
+
</PermissionGuard>
|
|
735
|
+
<PermissionGuard permission="${permKey}.delete">
|
|
736
|
+
<button
|
|
737
|
+
type="button"
|
|
738
|
+
onClick={(e) => { e.stopPropagation(); void handleDelete(item) }}
|
|
739
|
+
className="p-1.5 rounded hover:bg-[var(--error-bg)] text-[var(--text-muted)] hover:text-[var(--error-text)]"
|
|
740
|
+
title={t('${eLower}.list.delete')}
|
|
741
|
+
>
|
|
742
|
+
<Trash2 className="w-4 h-4" />
|
|
743
|
+
</button>
|
|
744
|
+
</PermissionGuard>
|
|
745
|
+
${customRowActionsJsx}
|
|
746
|
+
</div>
|
|
747
|
+
),
|
|
748
|
+
},
|
|
749
|
+
]
|
|
750
|
+
|
|
751
|
+
return (
|
|
752
|
+
<PermissionGuard permission="${permKey}.read">
|
|
753
|
+
<PageTemplate
|
|
754
|
+
title={t('${eLower}.list.title')}
|
|
755
|
+
subtitle={t('${eLower}.list.subtitle')}
|
|
756
|
+
icon={<List className="w-6 h-6" />}
|
|
757
|
+
breadcrumbs={[{ label: t('${eLower}.breadcrumb.section') }]}
|
|
758
|
+
actions={
|
|
759
|
+
<div className="flex items-center gap-2">
|
|
760
|
+
<PermissionGuard permission="${permKey}.create">
|
|
761
|
+
<button
|
|
762
|
+
type="button"
|
|
763
|
+
onClick={() => navigate(routes.${sectionCamel}.create())}
|
|
764
|
+
className="flex items-center gap-1.5 px-3 py-2 rounded-[var(--radius-button)] bg-[var(--color-accent-500)] text-white text-sm font-medium hover:bg-[var(--color-accent-600)] transition-colors"
|
|
765
|
+
>
|
|
766
|
+
<Plus className="w-4 h-4" />
|
|
767
|
+
{t('${eLower}.list.create')}
|
|
768
|
+
</button>
|
|
769
|
+
</PermissionGuard>
|
|
770
|
+
${customHeaderActionsJsx}
|
|
771
|
+
</div>
|
|
772
|
+
}
|
|
773
|
+
>
|
|
774
|
+
<Slot name="${eLower}.header.actions" />
|
|
775
|
+
|
|
776
|
+
{error && (
|
|
777
|
+
<div className="mb-4 p-3 rounded-[var(--radius-card)] bg-[var(--error-bg)] border border-[var(--error-border)] text-sm text-[var(--error-text)]">
|
|
778
|
+
{t('${eLower}.list.error')}: {String(error)}
|
|
779
|
+
</div>
|
|
780
|
+
)}
|
|
781
|
+
|
|
782
|
+
${filterBarJsx} <Slot name="${eLower}.table.columns.before" />
|
|
783
|
+
|
|
784
|
+
{isLoading ? (
|
|
785
|
+
<div className="flex items-center justify-center py-12 text-[var(--text-muted)]">
|
|
786
|
+
<Loader2 className="w-5 h-5 animate-spin mr-2" />
|
|
787
|
+
{t('${eLower}.list.loading')}
|
|
788
|
+
</div>
|
|
789
|
+
) : (
|
|
790
|
+
<DataTable<${e}>
|
|
791
|
+
data={filtered}
|
|
792
|
+
columns={columns}
|
|
793
|
+
searchable
|
|
794
|
+
searchPlaceholder={t('${eLower}.list.search')}
|
|
795
|
+
pagination={{ pageSize: 20, showSizeSelector: true }}
|
|
796
|
+
getRowKey={(item) => item.id}
|
|
797
|
+
emptyMessage={t('${eLower}.list.empty')}
|
|
798
|
+
onRowClick={(item) => navigate(routes.${sectionCamel}.detail(item.id))}
|
|
799
|
+
/>
|
|
800
|
+
)}
|
|
801
|
+
|
|
802
|
+
<Slot name="${eLower}.table.columns.after" />
|
|
803
|
+
</PageTemplate>
|
|
804
|
+
</PermissionGuard>
|
|
805
|
+
)
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
export default ${plural}ListPage
|
|
809
|
+
`,
|
|
810
|
+
})
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// ─── DetailPage ──────────────────────────────────────────────────────────
|
|
814
|
+
if (spec.views.includes('detail')) {
|
|
815
|
+
// Tab support: when pageSpec.tabs[] exists, group fields into tab panels
|
|
816
|
+
// with URL-synced state (?tab=key). Otherwise render flat.
|
|
817
|
+
type TabMeta = { key: string; labelKey?: string; label?: string; fields?: string[] }
|
|
818
|
+
const detailTabs = (spec.pageSpec?.tabs ?? []) as TabMeta[]
|
|
819
|
+
const hasDetailTabs = detailTabs.length > 0
|
|
820
|
+
|
|
821
|
+
const detailExtraImport = hasDetailTabs ? `, useSearchParams` : ''
|
|
822
|
+
|
|
823
|
+
function renderFieldDl(fields: typeof spec.fields): string {
|
|
824
|
+
return fields.map(f => ` <div>
|
|
825
|
+
<dt className="text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-1">
|
|
826
|
+
{t('${eLower}.detail.fields.${fieldToCamel(f.name)}')}
|
|
827
|
+
</dt>
|
|
828
|
+
<dd className="text-sm text-[var(--text-primary)]">
|
|
829
|
+
{String(data.${fieldToCamel(f.name)} ?? '')}
|
|
830
|
+
</dd>
|
|
831
|
+
</div>`).join('\n')
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
let detailTabHooks = ''
|
|
835
|
+
let detailBodyJsx: string
|
|
836
|
+
|
|
837
|
+
if (hasDetailTabs) {
|
|
838
|
+
const defaultTab = detailTabs[0].key
|
|
839
|
+
detailTabHooks = `
|
|
840
|
+
const [searchParams, setSearchParams] = useSearchParams()
|
|
841
|
+
const activeTab = searchParams.get('tab') ?? '${defaultTab}'
|
|
842
|
+
const switchTab = (tab: string) => {
|
|
843
|
+
const p = new URLSearchParams(searchParams)
|
|
844
|
+
tab === '${defaultTab}' ? p.delete('tab') : p.set('tab', tab)
|
|
845
|
+
setSearchParams(p, { replace: true })
|
|
846
|
+
}
|
|
847
|
+
`
|
|
848
|
+
const tabBarJsx = detailTabs.map(tab => {
|
|
849
|
+
const label = tab.labelKey ? `t('${eLower}.${tab.labelKey}')` : `'${tab.label ?? tab.key}'`
|
|
850
|
+
return ` <button
|
|
851
|
+
type="button"
|
|
852
|
+
onClick={() => switchTab('${tab.key}')}
|
|
853
|
+
className={\`px-4 py-2 text-sm font-medium border-b-2 transition-colors \${activeTab === '${tab.key}' ? 'border-[var(--color-accent-500)] text-[var(--color-accent-500)]' : 'border-transparent text-[var(--text-muted)] hover:text-[var(--text-primary)]'}\`}
|
|
854
|
+
>
|
|
855
|
+
{${label}}
|
|
856
|
+
</button>`
|
|
857
|
+
}).join('\n')
|
|
858
|
+
|
|
859
|
+
const tabPanelsJsx = detailTabs.map(tab => {
|
|
860
|
+
const tabFieldNames = tab.fields ?? spec.fields.map(f => f.name)
|
|
861
|
+
const tabFields = spec.fields.filter(f => tabFieldNames.includes(f.name))
|
|
862
|
+
return ` {activeTab === '${tab.key}' && (
|
|
863
|
+
<div className="p-6 rounded-[var(--radius-card)] border border-[var(--border-color)] bg-[var(--bg-card)]">
|
|
864
|
+
<dl className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
|
|
865
|
+
${renderFieldDl(tabFields)}
|
|
866
|
+
</dl>
|
|
867
|
+
</div>
|
|
868
|
+
)}`
|
|
869
|
+
}).join('\n\n')
|
|
870
|
+
|
|
871
|
+
detailBodyJsx = ` <div className="flex gap-1 border-b border-[var(--border-color)] mb-6">
|
|
872
|
+
${tabBarJsx}
|
|
873
|
+
</div>
|
|
874
|
+
|
|
875
|
+
${tabPanelsJsx}`
|
|
876
|
+
} else {
|
|
877
|
+
detailBodyJsx = ` <div className="p-6 rounded-[var(--radius-card)] border border-[var(--border-color)] bg-[var(--bg-card)]">
|
|
878
|
+
<dl className="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
|
|
879
|
+
${renderFieldDl(spec.fields)}
|
|
880
|
+
</dl>
|
|
881
|
+
</div>`
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Custom header actions for detail page — rendered alongside the Edit button.
|
|
885
|
+
const detailHeaderActions = allCustomActions.filter(a => a.scope === 'header')
|
|
886
|
+
const detailApiActions = detailHeaderActions.filter(isApi)
|
|
887
|
+
const hasDetailActions = detailHeaderActions.length > 0
|
|
888
|
+
|
|
889
|
+
const detailCustomHookImports = detailApiActions
|
|
890
|
+
.map(a => `, use${toPascal(hookCodeOf(a))}${e}`)
|
|
891
|
+
.join('')
|
|
892
|
+
|
|
893
|
+
const detailCustomIconNames = new Set(detailHeaderActions.map(a => iconForCode(a.code)))
|
|
894
|
+
const detailCustomIconImports = Array.from(detailCustomIconNames).map(name => `, ${name}`).join('')
|
|
895
|
+
|
|
896
|
+
const detailCustomMutationDecls = detailApiActions
|
|
897
|
+
.map(a => ` const ${toCamel(hookCodeOf(a))}Mutation = use${toPascal(hookCodeOf(a))}${e}()`)
|
|
898
|
+
.join('\n')
|
|
899
|
+
|
|
900
|
+
const detailCustomHandlerDecls = detailHeaderActions
|
|
901
|
+
.map(a => {
|
|
902
|
+
const name = `handle${toPascal(a.code)}`
|
|
903
|
+
if (isNavigate(a)) {
|
|
904
|
+
const target = a.targetRoute ?? `routes.${sectionCamel}.list()`
|
|
905
|
+
return ` const ${name} = () => {\n navigate(${target})\n }` // detail actions are always header-scoped
|
|
906
|
+
}
|
|
907
|
+
const hookVar = `${toCamel(hookCodeOf(a))}Mutation`
|
|
908
|
+
return ` const ${name} = async () => {\n if (id) await ${hookVar}.mutateAsync(id)\n }`
|
|
909
|
+
})
|
|
910
|
+
.join('\n\n')
|
|
911
|
+
|
|
912
|
+
const detailCustomActionsJsx = hasDetailActions
|
|
913
|
+
? `\n${detailHeaderActions.map(renderHeaderActionButton).join('\n')}`
|
|
914
|
+
: ''
|
|
915
|
+
|
|
916
|
+
files.push({
|
|
917
|
+
path: pathFor('detail', `${e}DetailPage.tsx`),
|
|
918
|
+
content: `${GENERATED_MARKER}import { useParams, useNavigate, Navigate${detailExtraImport} } from 'react-router-dom'
|
|
919
|
+
import { useTranslation } from 'react-i18next'
|
|
920
|
+
import { Loader2, Pencil, FileText${detailCustomIconImports} } from 'lucide-react'
|
|
921
|
+
import { Slot } from '@atlashub/smartstack'
|
|
922
|
+
import { PermissionGuard } from '@/components/auth/PermissionGuard'
|
|
923
|
+
import { PageTemplate } from '@/components/ui/PageTemplate'
|
|
924
|
+
import { routes } from '@/extensions/${spec.module}Routes'
|
|
925
|
+
import { use${e}${detailCustomHookImports} } from '${featurePath}/hooks/use${e}'
|
|
926
|
+
|
|
927
|
+
export function ${e}DetailPage() {
|
|
928
|
+
const { id } = useParams<{ id: string }>()
|
|
929
|
+
const navigate = useNavigate()
|
|
930
|
+
const { t } = useTranslation('${spec.module}')
|
|
931
|
+
|
|
932
|
+
if (!id) return <Navigate to=".." replace />
|
|
933
|
+
|
|
934
|
+
const { data, isLoading } = use${e}(id)
|
|
935
|
+
${detailCustomMutationDecls ? detailCustomMutationDecls + '\n' : ''}${detailTabHooks}
|
|
936
|
+
if (isLoading) {
|
|
937
|
+
return (
|
|
938
|
+
<div className="flex items-center justify-center py-12 text-[var(--text-muted)]">
|
|
939
|
+
<Loader2 className="w-5 h-5 animate-spin mr-2" />
|
|
940
|
+
{t('${eLower}.detail.loading')}
|
|
941
|
+
</div>
|
|
942
|
+
)
|
|
943
|
+
}
|
|
944
|
+
if (!data) {
|
|
945
|
+
return (
|
|
946
|
+
<div className="p-4 rounded-[var(--radius-card)] bg-[var(--warning-bg)] border border-[var(--warning-border)] text-[var(--warning-text)]">
|
|
947
|
+
{t('${eLower}.detail.notFound')}
|
|
948
|
+
</div>
|
|
949
|
+
)
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const headerTitle = String(data.${fieldToCamel(headerField?.name ?? 'id')} ?? '')
|
|
953
|
+
${detailCustomHandlerDecls ? '\n' + detailCustomHandlerDecls + '\n' : ''}
|
|
954
|
+
return (
|
|
955
|
+
<PermissionGuard permission="${permKey}.read">
|
|
956
|
+
<PageTemplate
|
|
957
|
+
title={headerTitle}
|
|
958
|
+
icon={<FileText className="w-6 h-6" />}
|
|
959
|
+
breadcrumbs={[
|
|
960
|
+
{ label: t('${eLower}.breadcrumb.section'), href: '..' },
|
|
961
|
+
{ label: headerTitle },
|
|
962
|
+
]}
|
|
963
|
+
actions={
|
|
964
|
+
<div className="flex items-center gap-2">
|
|
965
|
+
<PermissionGuard permission="${permKey}.update">
|
|
966
|
+
<button
|
|
967
|
+
type="button"
|
|
968
|
+
onClick={() => id && navigate(routes.${sectionCamel}.edit(id))}
|
|
969
|
+
className="flex items-center gap-1.5 px-3 py-2 rounded-[var(--radius-button)] bg-[var(--color-accent-500)] text-white text-sm font-medium hover:bg-[var(--color-accent-600)] transition-colors"
|
|
970
|
+
>
|
|
971
|
+
<Pencil className="w-4 h-4" />
|
|
972
|
+
{t('${eLower}.detail.edit')}
|
|
973
|
+
</button>
|
|
974
|
+
</PermissionGuard>${detailCustomActionsJsx}
|
|
975
|
+
</div>
|
|
976
|
+
}
|
|
977
|
+
>
|
|
978
|
+
<Slot name="${eLower}.detail.header" context={{ data }} />
|
|
979
|
+
|
|
980
|
+
${detailBodyJsx}
|
|
981
|
+
|
|
982
|
+
<Slot name="${eLower}.detail.sidebar" context={{ data }} />
|
|
983
|
+
</PageTemplate>
|
|
984
|
+
</PermissionGuard>
|
|
985
|
+
)
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
export default ${e}DetailPage
|
|
989
|
+
`,
|
|
990
|
+
})
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// ─── FormPage ────────────────────────────────────────────────────────────
|
|
994
|
+
if (spec.views.includes('form')) {
|
|
995
|
+
// FK fields drive the EntityLookup import + per-target lookup hook imports.
|
|
996
|
+
// Empty array → no extra imports, no <EntityLookup>, audit DEV-UI-022 passes
|
|
997
|
+
// because there's no FK to render in the first place.
|
|
998
|
+
const fkFormFields = formFields.filter(isFkField)
|
|
999
|
+
const fkLookupImport = fkFormFields.length > 0
|
|
1000
|
+
? `\nimport { EntityLookup } from '@/components/ui/EntityLookup'`
|
|
1001
|
+
: ''
|
|
1002
|
+
const fkHookImports = lookupHookImportLines(fkFormFields)
|
|
1003
|
+
.map(line => `\n${line}`).join('')
|
|
1004
|
+
|
|
1005
|
+
// Custom header actions for the form — forms have no rows, only header scope.
|
|
1006
|
+
// Guarded by isEdit: you can't duplicate/archive an entity that doesn't exist yet.
|
|
1007
|
+
const formHeaderActions = allCustomActions.filter(a => a.scope === 'header')
|
|
1008
|
+
const formApiActions = formHeaderActions.filter(isApi)
|
|
1009
|
+
const hasFormActions = formHeaderActions.length > 0
|
|
1010
|
+
|
|
1011
|
+
const formCustomHookImports = formApiActions
|
|
1012
|
+
.map(a => `, use${toPascal(hookCodeOf(a))}${e}`)
|
|
1013
|
+
.join('')
|
|
1014
|
+
|
|
1015
|
+
const formCustomIconNames = new Set(formHeaderActions.map(a => iconForCode(a.code)))
|
|
1016
|
+
const formCustomIconImports = Array.from(formCustomIconNames).map(name => `, ${name}`).join('')
|
|
1017
|
+
|
|
1018
|
+
const formCustomMutationDecls = formApiActions
|
|
1019
|
+
.map(a => ` const ${toCamel(hookCodeOf(a))}Mutation = use${toPascal(hookCodeOf(a))}${e}()`)
|
|
1020
|
+
.join('\n')
|
|
1021
|
+
|
|
1022
|
+
const formCustomHandlerDecls = formHeaderActions
|
|
1023
|
+
.map(a => {
|
|
1024
|
+
const name = `handle${toPascal(a.code)}`
|
|
1025
|
+
if (isNavigate(a)) {
|
|
1026
|
+
const target = a.targetRoute ?? `routes.${sectionCamel}.list()`
|
|
1027
|
+
return ` const ${name} = () => {\n navigate(${target})\n }` // form actions are always header-scoped
|
|
1028
|
+
}
|
|
1029
|
+
const hookVar = `${toCamel(hookCodeOf(a))}Mutation`
|
|
1030
|
+
return ` const ${name} = async () => {\n if (id) await ${hookVar}.mutateAsync(id)\n }`
|
|
1031
|
+
})
|
|
1032
|
+
.join('\n\n')
|
|
1033
|
+
|
|
1034
|
+
const formActionsJsx = hasFormActions
|
|
1035
|
+
? `
|
|
1036
|
+
actions={isEdit ? (
|
|
1037
|
+
<div className="flex items-center gap-2">
|
|
1038
|
+
${formHeaderActions.map(renderHeaderActionButton).join('\n')}
|
|
1039
|
+
</div>
|
|
1040
|
+
) : undefined}`
|
|
1041
|
+
: ''
|
|
1042
|
+
|
|
1043
|
+
files.push({
|
|
1044
|
+
path: pathFor('form', `${e}FormPage.tsx`),
|
|
1045
|
+
content: `${GENERATED_MARKER}import { useState, useEffect } from 'react'
|
|
1046
|
+
import type { FormEvent } from 'react'
|
|
1047
|
+
import { useParams, useNavigate } from 'react-router-dom'
|
|
1048
|
+
import { useTranslation } from 'react-i18next'
|
|
1049
|
+
import { Loader2, AlertTriangle, FilePen${formCustomIconImports} } from 'lucide-react'
|
|
1050
|
+
import { Slot } from '@atlashub/smartstack'
|
|
1051
|
+
import { PermissionGuard } from '@/components/auth/PermissionGuard'
|
|
1052
|
+
import { PageTemplate } from '@/components/ui/PageTemplate'
|
|
1053
|
+
import { use${e}, useCreate${e}, useUpdate${e}${formCustomHookImports} } from '${featurePath}/hooks/use${e}'${fkLookupImport}${fkHookImports}
|
|
1054
|
+
|
|
1055
|
+
type ${e}FormData = ${formDataTypeLiteral(formFields)}
|
|
1056
|
+
|
|
1057
|
+
const initial${e}FormData: ${e}FormData = ${initialFormObj}
|
|
1058
|
+
|
|
1059
|
+
export function ${e}FormPage() {
|
|
1060
|
+
const { id } = useParams<{ id: string }>()
|
|
1061
|
+
const navigate = useNavigate()
|
|
1062
|
+
const { t } = useTranslation('${spec.module}')
|
|
1063
|
+
const isEdit = !!id && id !== 'new'
|
|
1064
|
+
const { data: existing } = use${e}(isEdit ? id! : '')
|
|
1065
|
+
const createMutation = useCreate${e}()
|
|
1066
|
+
const updateMutation = useUpdate${e}()
|
|
1067
|
+
${formCustomMutationDecls ? formCustomMutationDecls + '\n' : ''}
|
|
1068
|
+
const [formData, setFormData] = useState<${e}FormData>(initial${e}FormData)
|
|
1069
|
+
const [error, setError] = useState<string | null>(null)
|
|
1070
|
+
|
|
1071
|
+
useEffect(() => {
|
|
1072
|
+
if (existing) {
|
|
1073
|
+
setFormData(existing as ${e}FormData)
|
|
1074
|
+
}
|
|
1075
|
+
}, [existing])
|
|
1076
|
+
|
|
1077
|
+
const onChange = <K extends keyof ${e}FormData>(field: K, value: ${e}FormData[K]) => {
|
|
1078
|
+
setFormData(prev => ({ ...prev, [field]: value }))
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
const handleSubmit = async (event: FormEvent) => {
|
|
1082
|
+
event.preventDefault()
|
|
1083
|
+
setError(null)
|
|
1084
|
+
try {
|
|
1085
|
+
if (isEdit && id) {
|
|
1086
|
+
await updateMutation.mutateAsync({ id, data: formData })
|
|
1087
|
+
} else {
|
|
1088
|
+
await createMutation.mutateAsync(formData)
|
|
1089
|
+
}
|
|
1090
|
+
navigate(-1)
|
|
1091
|
+
} catch (err) {
|
|
1092
|
+
setError(err instanceof Error ? err.message : String(err))
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
${formCustomHandlerDecls ? '\n' + formCustomHandlerDecls + '\n' : ''}
|
|
1096
|
+
const permission = isEdit ? '${permKey}.update' : '${permKey}.create'
|
|
1097
|
+
const isPending = createMutation.isPending || updateMutation.isPending
|
|
1098
|
+
|
|
1099
|
+
return (
|
|
1100
|
+
<PermissionGuard permission={permission}>
|
|
1101
|
+
<PageTemplate
|
|
1102
|
+
title={isEdit ? t('${eLower}.form.editTitle') : t('${eLower}.form.createTitle')}
|
|
1103
|
+
icon={<FilePen className="w-6 h-6" />}
|
|
1104
|
+
breadcrumbs={[
|
|
1105
|
+
{ label: t('${eLower}.breadcrumb.section'), href: '..' },
|
|
1106
|
+
{ label: isEdit ? t('${eLower}.form.editTitle') : t('${eLower}.form.createTitle') },
|
|
1107
|
+
]}${formActionsJsx}
|
|
1108
|
+
>
|
|
1109
|
+
{error && (
|
|
1110
|
+
<div className="p-4 rounded-[var(--radius-card)] bg-[var(--error-bg)] border border-[var(--error-border)]">
|
|
1111
|
+
<div className="flex items-center gap-2 text-[var(--error-text)]">
|
|
1112
|
+
<AlertTriangle className="w-5 h-5" />
|
|
1113
|
+
<span>{error}</span>
|
|
1114
|
+
</div>
|
|
1115
|
+
</div>
|
|
1116
|
+
)}
|
|
1117
|
+
|
|
1118
|
+
<form onSubmit={handleSubmit} className="space-y-6">
|
|
1119
|
+
<div className="p-6 rounded-[var(--radius-card)] border border-[var(--border-color)] bg-[var(--bg-card)] space-y-4">
|
|
1120
|
+
<Slot name="${eLower}.form.fields.before" context={{ formData, onChange }} />
|
|
1121
|
+
|
|
1122
|
+
${formFields.map(f => isFkField(f)
|
|
1123
|
+
? renderFkLookupField(f, eLower, e)
|
|
1124
|
+
: renderPlainFormField(f, eLower, e)).join('\n')}
|
|
1125
|
+
|
|
1126
|
+
<Slot name="${eLower}.form.fields.after" context={{ formData, onChange }} />
|
|
1127
|
+
</div>
|
|
1128
|
+
|
|
1129
|
+
<div className="flex justify-end gap-2">
|
|
1130
|
+
<button
|
|
1131
|
+
type="button"
|
|
1132
|
+
onClick={() => navigate(-1)}
|
|
1133
|
+
disabled={isPending}
|
|
1134
|
+
className="px-6 py-2 rounded-[var(--radius-button)] bg-[var(--bg-secondary)] text-[var(--text-primary)] hover:bg-[var(--bg-hover)] disabled:opacity-50 transition-colors"
|
|
1135
|
+
>
|
|
1136
|
+
{t('${eLower}.form.cancel')}
|
|
1137
|
+
</button>
|
|
1138
|
+
<button
|
|
1139
|
+
type="submit"
|
|
1140
|
+
disabled={isPending}
|
|
1141
|
+
className="flex items-center gap-2 px-6 py-2 rounded-[var(--radius-button)] bg-[var(--color-accent-500)] text-white hover:bg-[var(--color-accent-600)] disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
1142
|
+
>
|
|
1143
|
+
{isPending && <Loader2 className="w-4 h-4 animate-spin" />}
|
|
1144
|
+
{isEdit ? t('${eLower}.form.submitUpdate') : t('${eLower}.form.submitCreate')}
|
|
1145
|
+
</button>
|
|
1146
|
+
</div>
|
|
1147
|
+
</form>
|
|
1148
|
+
</PageTemplate>
|
|
1149
|
+
</PermissionGuard>
|
|
1150
|
+
)
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
export default ${e}FormPage
|
|
1154
|
+
`,
|
|
1155
|
+
})
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// ─── DashboardPage ──────────────────────────────────────────────────────
|
|
1159
|
+
if (spec.views.includes('dashboard')) {
|
|
1160
|
+
files.push({
|
|
1161
|
+
path: pathFor('dashboard', `${e}DashboardPage.tsx`),
|
|
1162
|
+
content: `${GENERATED_MARKER}import { useState } from 'react'
|
|
1163
|
+
import { useTranslation } from 'react-i18next'
|
|
1164
|
+
import { Loader2, LayoutDashboard, AlertTriangle, BellRing } from 'lucide-react'
|
|
1165
|
+
import { Slot } from '@atlashub/smartstack'
|
|
1166
|
+
import { PermissionGuard } from '@/components/auth/PermissionGuard'
|
|
1167
|
+
import { PageTemplate } from '@/components/ui/PageTemplate'
|
|
1168
|
+
import { useDashboard${e} } from '${featurePath}/hooks/use${e}'
|
|
1169
|
+
|
|
1170
|
+
export function ${e}DashboardPage() {
|
|
1171
|
+
const { t } = useTranslation('${spec.module}')
|
|
1172
|
+
const [period, setPeriod] = useState<{ startDate?: string; endDate?: string }>({})
|
|
1173
|
+
const { consolidated, alerts, isLoading, error } = useDashboard${e}(period)
|
|
1174
|
+
|
|
1175
|
+
return (
|
|
1176
|
+
<PermissionGuard permission="${permKey}.read">
|
|
1177
|
+
<PageTemplate
|
|
1178
|
+
title={t('${eLower}.dashboard.title')}
|
|
1179
|
+
subtitle={t('${eLower}.dashboard.subtitle')}
|
|
1180
|
+
icon={<LayoutDashboard className="w-6 h-6" />}
|
|
1181
|
+
breadcrumbs={[
|
|
1182
|
+
{ label: t('${eLower}.breadcrumb.section'), href: '..' },
|
|
1183
|
+
{ label: t('${eLower}.dashboard.title') },
|
|
1184
|
+
]}
|
|
1185
|
+
>
|
|
1186
|
+
<Slot name="${eLower}.dashboard.header" />
|
|
1187
|
+
|
|
1188
|
+
<div className="mb-4 flex items-center gap-2">
|
|
1189
|
+
<input
|
|
1190
|
+
type="date"
|
|
1191
|
+
value={period.startDate ?? ''}
|
|
1192
|
+
onChange={(event) => setPeriod((p) => ({ ...p, startDate: event.target.value || undefined }))}
|
|
1193
|
+
className="input"
|
|
1194
|
+
aria-label={t('${eLower}.dashboard.startDate')}
|
|
1195
|
+
/>
|
|
1196
|
+
<input
|
|
1197
|
+
type="date"
|
|
1198
|
+
value={period.endDate ?? ''}
|
|
1199
|
+
onChange={(event) => setPeriod((p) => ({ ...p, endDate: event.target.value || undefined }))}
|
|
1200
|
+
className="input"
|
|
1201
|
+
aria-label={t('${eLower}.dashboard.endDate')}
|
|
1202
|
+
/>
|
|
1203
|
+
</div>
|
|
1204
|
+
|
|
1205
|
+
{error && (
|
|
1206
|
+
<div className="mb-4 p-3 rounded-[var(--radius-card)] bg-[var(--error-bg)] border border-[var(--error-border)] text-sm text-[var(--error-text)]">
|
|
1207
|
+
<div className="flex items-center gap-2">
|
|
1208
|
+
<AlertTriangle className="w-4 h-4" />
|
|
1209
|
+
<span>{t('${eLower}.dashboard.error')}: {String(error)}</span>
|
|
1210
|
+
</div>
|
|
1211
|
+
</div>
|
|
1212
|
+
)}
|
|
1213
|
+
|
|
1214
|
+
{isLoading ? (
|
|
1215
|
+
<div className="flex items-center justify-center py-12 text-[var(--text-muted)]">
|
|
1216
|
+
<Loader2 className="w-5 h-5 animate-spin mr-2" />
|
|
1217
|
+
{t('${eLower}.dashboard.loading')}
|
|
1218
|
+
</div>
|
|
1219
|
+
) : (
|
|
1220
|
+
<>
|
|
1221
|
+
<Slot name="${eLower}.dashboard.kpis.before" context={{ consolidated }} />
|
|
1222
|
+
|
|
1223
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
|
1224
|
+
{Object.entries(consolidated?.metrics ?? {}).map(([key, value]) => (
|
|
1225
|
+
<div
|
|
1226
|
+
key={key}
|
|
1227
|
+
className="p-4 rounded-[var(--radius-card)] border border-[var(--border-color)] bg-[var(--bg-card)]"
|
|
1228
|
+
>
|
|
1229
|
+
<div className="text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)]">
|
|
1230
|
+
{t(\`${eLower}.dashboard.kpi.\${key}\`)}
|
|
1231
|
+
</div>
|
|
1232
|
+
<div className="text-2xl font-semibold text-[var(--text-primary)] mt-1">
|
|
1233
|
+
{String(value)}
|
|
1234
|
+
</div>
|
|
1235
|
+
</div>
|
|
1236
|
+
))}
|
|
1237
|
+
</div>
|
|
1238
|
+
|
|
1239
|
+
<Slot name="${eLower}.dashboard.kpis.after" context={{ consolidated }} />
|
|
1240
|
+
|
|
1241
|
+
<section>
|
|
1242
|
+
<h2 className="flex items-center gap-2 text-sm font-semibold text-[var(--text-primary)] mb-2">
|
|
1243
|
+
<BellRing className="w-4 h-4" />
|
|
1244
|
+
{t('${eLower}.dashboard.alerts.title')}
|
|
1245
|
+
</h2>
|
|
1246
|
+
{(alerts ?? []).length === 0 ? (
|
|
1247
|
+
<div className="p-3 rounded-[var(--radius-card)] border border-[var(--border-color)] bg-[var(--bg-card)] text-sm text-[var(--text-muted)]">
|
|
1248
|
+
{t('${eLower}.dashboard.alerts.empty')}
|
|
1249
|
+
</div>
|
|
1250
|
+
) : (
|
|
1251
|
+
<ul className="space-y-2">
|
|
1252
|
+
{(alerts ?? []).map((alert) => (
|
|
1253
|
+
<li
|
|
1254
|
+
key={alert.id}
|
|
1255
|
+
className="p-3 rounded-[var(--radius-card)] border border-[var(--border-color)] bg-[var(--bg-card)] flex items-start justify-between gap-3"
|
|
1256
|
+
>
|
|
1257
|
+
<div>
|
|
1258
|
+
<div
|
|
1259
|
+
className="text-xs font-semibold uppercase tracking-wider"
|
|
1260
|
+
style={{
|
|
1261
|
+
color:
|
|
1262
|
+
alert.severity === 'critical'
|
|
1263
|
+
? 'var(--error-text)'
|
|
1264
|
+
: alert.severity === 'warning'
|
|
1265
|
+
? 'var(--warning-text)'
|
|
1266
|
+
: 'var(--text-muted)',
|
|
1267
|
+
}}
|
|
1268
|
+
aria-label={t(\`${eLower}.dashboard.alerts.severity.\${alert.severity}\`)}
|
|
1269
|
+
>
|
|
1270
|
+
{alert.severity}
|
|
1271
|
+
</div>
|
|
1272
|
+
<div className="text-sm text-[var(--text-primary)]">{alert.message}</div>
|
|
1273
|
+
</div>
|
|
1274
|
+
<time className="text-xs text-[var(--text-muted)] whitespace-nowrap">{alert.createdAt}</time>
|
|
1275
|
+
</li>
|
|
1276
|
+
))}
|
|
1277
|
+
</ul>
|
|
1278
|
+
)}
|
|
1279
|
+
</section>
|
|
1280
|
+
</>
|
|
1281
|
+
)}
|
|
1282
|
+
|
|
1283
|
+
<Slot name="${eLower}.dashboard.footer" />
|
|
1284
|
+
</PageTemplate>
|
|
1285
|
+
</PermissionGuard>
|
|
1286
|
+
)
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
export default ${e}DashboardPage
|
|
1290
|
+
`,
|
|
1291
|
+
})
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// ─── KanbanPage (Bug 7) ──────────────────────────────────────────────────
|
|
1295
|
+
// Workflow-driven view: groups entities by a status-like field into fixed
|
|
1296
|
+
// columns. Cards link to the detail page. Drag-and-drop is opt-in via
|
|
1297
|
+
// `kanbanConfig.dnd` and requires a `move` customAction on the api-client
|
|
1298
|
+
// side (otherwise scaffold-api-client never emits the `useMoveDemande` hook).
|
|
1299
|
+
if (spec.views.includes('kanban')) {
|
|
1300
|
+
const cfg = spec.kanbanConfig
|
|
1301
|
+
if (!cfg) {
|
|
1302
|
+
throw new Error(
|
|
1303
|
+
`scaffold-component: entity '${spec.entity}' lists 'kanban' in views but kanbanConfig is missing — declare it at the spec level so the generator knows which field defines the columns.`,
|
|
1304
|
+
)
|
|
1305
|
+
}
|
|
1306
|
+
const groupByCamel = fieldToCamel(cfg.groupBy)
|
|
1307
|
+
// titleField defaults to the first non-formula field that exists on the entity.
|
|
1308
|
+
const fallbackTitle = spec.fields.find(f => !f.formula)?.name ?? 'id'
|
|
1309
|
+
const titleCamel = fieldToCamel(cfg.titleField ?? fallbackTitle)
|
|
1310
|
+
const hasSubtitle = Boolean(cfg.subtitleField)
|
|
1311
|
+
const subtitleCamel = hasSubtitle ? fieldToCamel(cfg.subtitleField!) : ''
|
|
1312
|
+
const dnd = cfg.dnd === true
|
|
1313
|
+
// Column metadata baked into the page — keeps the order deterministic
|
|
1314
|
+
// (BA controls it, NOT the alphabetical order of grouped keys).
|
|
1315
|
+
const kanbanColumns = cfg.columns.map(c => ` { key: '${c.key}', labelKey: '${c.labelKey}' }`).join(',\n')
|
|
1316
|
+
const moveImport = dnd ? `, useMove${e}` : ''
|
|
1317
|
+
const moveHookDecl = dnd ? ` const moveMutation = useMove${e}()\n` : ''
|
|
1318
|
+
const dropHandler = dnd
|
|
1319
|
+
? `
|
|
1320
|
+
const handleDrop = async (id: string, newStatus: string) => {
|
|
1321
|
+
await moveMutation.mutateAsync({ id, payload: { ${groupByCamel}: newStatus } as never })
|
|
1322
|
+
}`
|
|
1323
|
+
: ''
|
|
1324
|
+
const cardDragProps = dnd
|
|
1325
|
+
? `
|
|
1326
|
+
draggable
|
|
1327
|
+
onDragStart={(event) => event.dataTransfer.setData('text/plain', item.id)}`
|
|
1328
|
+
: ''
|
|
1329
|
+
const columnDropProps = dnd
|
|
1330
|
+
? `
|
|
1331
|
+
onDragOver={(event) => event.preventDefault()}
|
|
1332
|
+
onDrop={(event) => { void handleDrop(event.dataTransfer.getData('text/plain'), col.key) }}`
|
|
1333
|
+
: ''
|
|
1334
|
+
files.push({
|
|
1335
|
+
path: pathFor('kanban', `${e}KanbanPage.tsx`),
|
|
1336
|
+
content: `${GENERATED_MARKER}import { useMemo } from 'react'
|
|
1337
|
+
import { useNavigate } from 'react-router-dom'
|
|
1338
|
+
import { useTranslation } from 'react-i18next'
|
|
1339
|
+
import { Loader2, Trello } from 'lucide-react'
|
|
1340
|
+
import { Slot } from '@atlashub/smartstack'
|
|
1341
|
+
import { PermissionGuard } from '@/components/auth/PermissionGuard'
|
|
1342
|
+
import { PageTemplate } from '@/components/ui/PageTemplate'
|
|
1343
|
+
import { routes } from '@/extensions/${spec.module}Routes'
|
|
1344
|
+
import { use${plural}${moveImport} } from '${featurePath}/hooks/use${e}'
|
|
1345
|
+
import type { ${e} } from '${featurePath}/types'
|
|
1346
|
+
|
|
1347
|
+
const KANBAN_COLUMNS = [
|
|
1348
|
+
${kanbanColumns}
|
|
1349
|
+
] as const
|
|
1350
|
+
|
|
1351
|
+
export function ${e}KanbanPage() {
|
|
1352
|
+
const { t } = useTranslation('${spec.module}')
|
|
1353
|
+
const navigate = useNavigate()
|
|
1354
|
+
const { data, isLoading, error } = use${plural}()
|
|
1355
|
+
${moveHookDecl}
|
|
1356
|
+
// Group items into buckets keyed by KANBAN_COLUMNS[i].key. Items whose
|
|
1357
|
+
// ${groupByCamel} does not match any declared column land in the "unassigned"
|
|
1358
|
+
// bucket displayed at the end. Keeps schema drift visible instead of silently
|
|
1359
|
+
// dropping cards.
|
|
1360
|
+
const buckets = useMemo(() => {
|
|
1361
|
+
const items = data?.items ?? []
|
|
1362
|
+
const map = new Map<string, ${e}[]>()
|
|
1363
|
+
for (const col of KANBAN_COLUMNS) map.set(col.key, [])
|
|
1364
|
+
const unassigned: ${e}[] = []
|
|
1365
|
+
for (const item of items) {
|
|
1366
|
+
const key = String((item as unknown as Record<string, unknown>)['${groupByCamel}'] ?? '')
|
|
1367
|
+
const target = map.get(key)
|
|
1368
|
+
if (target) target.push(item)
|
|
1369
|
+
else unassigned.push(item)
|
|
1370
|
+
}
|
|
1371
|
+
return { map, unassigned }
|
|
1372
|
+
}, [data])
|
|
1373
|
+
${dropHandler}
|
|
1374
|
+
|
|
1375
|
+
return (
|
|
1376
|
+
<PermissionGuard permission="${permKey}.read">
|
|
1377
|
+
<PageTemplate
|
|
1378
|
+
title={t('${eLower}.kanban.title')}
|
|
1379
|
+
subtitle={t('${eLower}.kanban.subtitle')}
|
|
1380
|
+
icon={<Trello className="w-6 h-6" />}
|
|
1381
|
+
breadcrumbs={[
|
|
1382
|
+
{ label: t('${eLower}.breadcrumb.section'), href: '..' },
|
|
1383
|
+
{ label: t('${eLower}.kanban.title') },
|
|
1384
|
+
]}
|
|
1385
|
+
>
|
|
1386
|
+
<Slot name="${eLower}.kanban.header" />
|
|
1387
|
+
|
|
1388
|
+
{error && (
|
|
1389
|
+
<div className="mb-4 p-3 rounded-[var(--radius-card)] bg-[var(--error-bg)] border border-[var(--error-border)] text-sm text-[var(--error-text)]">
|
|
1390
|
+
{t('${eLower}.kanban.error')}: {String(error)}
|
|
1391
|
+
</div>
|
|
1392
|
+
)}
|
|
1393
|
+
|
|
1394
|
+
{isLoading ? (
|
|
1395
|
+
<div className="flex items-center justify-center py-12 text-[var(--text-muted)]">
|
|
1396
|
+
<Loader2 className="w-5 h-5 animate-spin mr-2" />
|
|
1397
|
+
{t('${eLower}.kanban.loading')}
|
|
1398
|
+
</div>
|
|
1399
|
+
) : (
|
|
1400
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
1401
|
+
{KANBAN_COLUMNS.map((col) => (
|
|
1402
|
+
<div
|
|
1403
|
+
key={col.key}
|
|
1404
|
+
className="p-3 rounded-[var(--radius-card)] border border-[var(--border-color)] bg-[var(--bg-card)] min-h-[200px]"${columnDropProps}
|
|
1405
|
+
>
|
|
1406
|
+
<h3 className="text-sm font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-3">
|
|
1407
|
+
{t(\`${eLower}.\${col.labelKey}\`)}
|
|
1408
|
+
<span className="ml-2 text-xs text-[var(--text-muted)]">
|
|
1409
|
+
({buckets.map.get(col.key)?.length ?? 0})
|
|
1410
|
+
</span>
|
|
1411
|
+
</h3>
|
|
1412
|
+
<div className="space-y-2">
|
|
1413
|
+
{(buckets.map.get(col.key) ?? []).map((item) => (
|
|
1414
|
+
<button
|
|
1415
|
+
key={item.id}
|
|
1416
|
+
type="button"
|
|
1417
|
+
onClick={() => navigate(routes.${sectionCamel}.detail(item.id))}
|
|
1418
|
+
className="w-full text-left p-3 rounded-[var(--radius-card)] border border-[var(--border-color)] bg-[var(--bg-elevated)] hover:border-[var(--color-accent-500)] transition-colors"${cardDragProps}
|
|
1419
|
+
>
|
|
1420
|
+
<div className="text-sm font-medium text-[var(--text-primary)]">
|
|
1421
|
+
{String(item.${titleCamel} ?? '')}
|
|
1422
|
+
</div>${hasSubtitle ? `
|
|
1423
|
+
<div className="text-xs text-[var(--text-muted)] mt-1">
|
|
1424
|
+
{String(item.${subtitleCamel} ?? '')}
|
|
1425
|
+
</div>` : ''}
|
|
1426
|
+
</button>
|
|
1427
|
+
))}
|
|
1428
|
+
</div>
|
|
1429
|
+
</div>
|
|
1430
|
+
))}
|
|
1431
|
+
{buckets.unassigned.length > 0 && (
|
|
1432
|
+
<div className="p-3 rounded-[var(--radius-card)] border border-dashed border-[var(--warning-border)] bg-[var(--warning-bg)] min-h-[200px]">
|
|
1433
|
+
<h3 className="text-sm font-semibold uppercase tracking-wider text-[var(--warning-text)] mb-3">
|
|
1434
|
+
{t('${eLower}.kanban.unassigned')}
|
|
1435
|
+
<span className="ml-2 text-xs">({buckets.unassigned.length})</span>
|
|
1436
|
+
</h3>
|
|
1437
|
+
<div className="space-y-2">
|
|
1438
|
+
{buckets.unassigned.map((item) => (
|
|
1439
|
+
<button
|
|
1440
|
+
key={item.id}
|
|
1441
|
+
type="button"
|
|
1442
|
+
onClick={() => navigate(routes.${sectionCamel}.detail(item.id))}
|
|
1443
|
+
className="w-full text-left p-3 rounded-[var(--radius-card)] border border-[var(--border-color)] bg-[var(--bg-elevated)] hover:border-[var(--color-accent-500)] transition-colors"
|
|
1444
|
+
>
|
|
1445
|
+
<div className="text-sm font-medium text-[var(--text-primary)]">
|
|
1446
|
+
{String(item.${titleCamel} ?? '')}
|
|
1447
|
+
</div>
|
|
1448
|
+
</button>
|
|
1449
|
+
))}
|
|
1450
|
+
</div>
|
|
1451
|
+
</div>
|
|
1452
|
+
)}
|
|
1453
|
+
</div>
|
|
1454
|
+
)}
|
|
1455
|
+
|
|
1456
|
+
<Slot name="${eLower}.kanban.footer" />
|
|
1457
|
+
</PageTemplate>
|
|
1458
|
+
</PermissionGuard>
|
|
1459
|
+
)
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
export default ${e}KanbanPage
|
|
1463
|
+
`,
|
|
1464
|
+
})
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// ─── ReconductionPage (Bug 7) ────────────────────────────────────────────
|
|
1468
|
+
// Bulk-renewal pattern: shows eligible entities in a multi-select table with
|
|
1469
|
+
// a single "Reconduct selected" header action. Optional twin action "Refuse"
|
|
1470
|
+
// when reconductionConfig.withRefuse is true. The bulk hooks must exist in
|
|
1471
|
+
// api-client output (declare customActions `reconduct` and `refuse-reconduction`
|
|
1472
|
+
// with scope:'bulk' to wire the buttons).
|
|
1473
|
+
if (spec.views.includes('reconduction')) {
|
|
1474
|
+
const rcfg = spec.reconductionConfig ?? { actionLabelKey: 'reconduction.action.reconduct', withRefuse: false }
|
|
1475
|
+
const refuseImport = rcfg.withRefuse ? `, useRefuseReconduction${plural}` : ''
|
|
1476
|
+
const refuseHookDecl = rcfg.withRefuse
|
|
1477
|
+
? ` const refuseMutation = useRefuseReconduction${plural}()\n`
|
|
1478
|
+
: ''
|
|
1479
|
+
const refuseHandler = rcfg.withRefuse
|
|
1480
|
+
? `
|
|
1481
|
+
const handleRefuse = async () => {
|
|
1482
|
+
if (selectedIds.length === 0) return
|
|
1483
|
+
await refuseMutation.mutateAsync(selectedIds)
|
|
1484
|
+
setSelectedIds([])
|
|
1485
|
+
}`
|
|
1486
|
+
: ''
|
|
1487
|
+
const refuseButton = rcfg.withRefuse
|
|
1488
|
+
? `
|
|
1489
|
+
<button
|
|
1490
|
+
type="button"
|
|
1491
|
+
onClick={() => void handleRefuse()}
|
|
1492
|
+
disabled={selectedIds.length === 0 || refuseMutation.isPending}
|
|
1493
|
+
className="px-3 py-2 rounded-[var(--radius-button)] border border-[var(--border-color)] text-sm font-medium text-[var(--text-primary)] hover:bg-[var(--bg-hover)] disabled:opacity-50"
|
|
1494
|
+
>
|
|
1495
|
+
{t('${eLower}.reconduction.action.refuse')}
|
|
1496
|
+
</button>`
|
|
1497
|
+
: ''
|
|
1498
|
+
files.push({
|
|
1499
|
+
path: pathFor('reconduction', `${e}ReconductionPage.tsx`),
|
|
1500
|
+
content: `${GENERATED_MARKER}import { useState } from 'react'
|
|
1501
|
+
import { useTranslation } from 'react-i18next'
|
|
1502
|
+
import { Loader2, RefreshCw } from 'lucide-react'
|
|
1503
|
+
import { Slot } from '@atlashub/smartstack'
|
|
1504
|
+
import { PermissionGuard } from '@/components/auth/PermissionGuard'
|
|
1505
|
+
import { PageTemplate } from '@/components/ui/PageTemplate'
|
|
1506
|
+
import { use${plural}, useReconduct${plural}${refuseImport} } from '${featurePath}/hooks/use${e}'
|
|
1507
|
+
import type { ${e} } from '${featurePath}/types'
|
|
1508
|
+
|
|
1509
|
+
export function ${e}ReconductionPage() {
|
|
1510
|
+
const { t } = useTranslation('${spec.module}')
|
|
1511
|
+
const { data, isLoading, error } = use${plural}()
|
|
1512
|
+
const reconductMutation = useReconduct${plural}()
|
|
1513
|
+
${refuseHookDecl} const [selectedIds, setSelectedIds] = useState<string[]>([])
|
|
1514
|
+
|
|
1515
|
+
const toggle = (id: string) =>
|
|
1516
|
+
setSelectedIds((prev) =>
|
|
1517
|
+
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id],
|
|
1518
|
+
)
|
|
1519
|
+
|
|
1520
|
+
const handleReconduct = async () => {
|
|
1521
|
+
if (selectedIds.length === 0) return
|
|
1522
|
+
await reconductMutation.mutateAsync(selectedIds)
|
|
1523
|
+
setSelectedIds([])
|
|
1524
|
+
}${refuseHandler}
|
|
1525
|
+
|
|
1526
|
+
const items: ${e}[] = data?.items ?? []
|
|
1527
|
+
|
|
1528
|
+
return (
|
|
1529
|
+
<PermissionGuard permission="${permKey}.update">
|
|
1530
|
+
<PageTemplate
|
|
1531
|
+
title={t('${eLower}.reconduction.title')}
|
|
1532
|
+
subtitle={t('${eLower}.reconduction.subtitle')}
|
|
1533
|
+
icon={<RefreshCw className="w-6 h-6" />}
|
|
1534
|
+
breadcrumbs={[
|
|
1535
|
+
{ label: t('${eLower}.breadcrumb.section'), href: '..' },
|
|
1536
|
+
{ label: t('${eLower}.reconduction.title') },
|
|
1537
|
+
]}
|
|
1538
|
+
actions={
|
|
1539
|
+
<div className="flex items-center gap-2">
|
|
1540
|
+
<button
|
|
1541
|
+
type="button"
|
|
1542
|
+
onClick={() => void handleReconduct()}
|
|
1543
|
+
disabled={selectedIds.length === 0 || reconductMutation.isPending}
|
|
1544
|
+
className="flex items-center gap-1.5 px-3 py-2 rounded-[var(--radius-button)] bg-[var(--color-accent-500)] text-white text-sm font-medium hover:bg-[var(--color-accent-600)] disabled:opacity-50"
|
|
1545
|
+
>
|
|
1546
|
+
<RefreshCw className="w-4 h-4" />
|
|
1547
|
+
{t('${eLower}.${rcfg.actionLabelKey}')}
|
|
1548
|
+
{selectedIds.length > 0 && (
|
|
1549
|
+
<span className="ml-1 text-xs opacity-80">({selectedIds.length})</span>
|
|
1550
|
+
)}
|
|
1551
|
+
</button>${refuseButton}
|
|
1552
|
+
</div>
|
|
1553
|
+
}
|
|
1554
|
+
>
|
|
1555
|
+
<Slot name="${eLower}.reconduction.header" />
|
|
1556
|
+
|
|
1557
|
+
{error && (
|
|
1558
|
+
<div className="mb-4 p-3 rounded-[var(--radius-card)] bg-[var(--error-bg)] border border-[var(--error-border)] text-sm text-[var(--error-text)]">
|
|
1559
|
+
{t('${eLower}.reconduction.error')}: {String(error)}
|
|
1560
|
+
</div>
|
|
1561
|
+
)}
|
|
1562
|
+
|
|
1563
|
+
{isLoading ? (
|
|
1564
|
+
<div className="flex items-center justify-center py-12 text-[var(--text-muted)]">
|
|
1565
|
+
<Loader2 className="w-5 h-5 animate-spin mr-2" />
|
|
1566
|
+
{t('${eLower}.reconduction.loading')}
|
|
1567
|
+
</div>
|
|
1568
|
+
) : items.length === 0 ? (
|
|
1569
|
+
<div className="p-4 rounded-[var(--radius-card)] border border-[var(--border-color)] bg-[var(--bg-card)] text-sm text-[var(--text-muted)]">
|
|
1570
|
+
{t('${eLower}.reconduction.empty')}
|
|
1571
|
+
</div>
|
|
1572
|
+
) : (
|
|
1573
|
+
<div className="overflow-x-auto rounded-[var(--radius-card)] border border-[var(--border-color)]">
|
|
1574
|
+
<table className="w-full text-sm">
|
|
1575
|
+
<thead className="bg-[var(--bg-elevated)] text-[var(--text-muted)]">
|
|
1576
|
+
<tr>
|
|
1577
|
+
<th className="p-3 text-left w-12">
|
|
1578
|
+
<input
|
|
1579
|
+
type="checkbox"
|
|
1580
|
+
checked={selectedIds.length === items.length && items.length > 0}
|
|
1581
|
+
onChange={() =>
|
|
1582
|
+
setSelectedIds(selectedIds.length === items.length ? [] : items.map((x) => x.id))
|
|
1583
|
+
}
|
|
1584
|
+
aria-label={t('${eLower}.reconduction.selectAll')}
|
|
1585
|
+
/>
|
|
1586
|
+
</th>
|
|
1587
|
+
<th className="p-3 text-left font-semibold uppercase text-xs tracking-wider">
|
|
1588
|
+
{t('${eLower}.reconduction.columns.identifier')}
|
|
1589
|
+
</th>
|
|
1590
|
+
</tr>
|
|
1591
|
+
</thead>
|
|
1592
|
+
<tbody>
|
|
1593
|
+
{items.map((item) => (
|
|
1594
|
+
<tr
|
|
1595
|
+
key={item.id}
|
|
1596
|
+
className="border-t border-[var(--border-color)] hover:bg-[var(--bg-hover)]"
|
|
1597
|
+
>
|
|
1598
|
+
<td className="p-3">
|
|
1599
|
+
<input
|
|
1600
|
+
type="checkbox"
|
|
1601
|
+
checked={selectedIds.includes(item.id)}
|
|
1602
|
+
onChange={() => toggle(item.id)}
|
|
1603
|
+
aria-label={item.id}
|
|
1604
|
+
/>
|
|
1605
|
+
</td>
|
|
1606
|
+
<td className="p-3 text-[var(--text-primary)]">{item.id}</td>
|
|
1607
|
+
</tr>
|
|
1608
|
+
))}
|
|
1609
|
+
</tbody>
|
|
1610
|
+
</table>
|
|
1611
|
+
</div>
|
|
1612
|
+
)}
|
|
1613
|
+
|
|
1614
|
+
<Slot name="${eLower}.reconduction.footer" />
|
|
1615
|
+
</PageTemplate>
|
|
1616
|
+
</PermissionGuard>
|
|
1617
|
+
)
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
export default ${e}ReconductionPage
|
|
1621
|
+
`,
|
|
1622
|
+
})
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
// ─── Hub pages (AppHome / ModuleHome / SectionHome) ────────────────────
|
|
1626
|
+
// Landing pages with concrete KPI cards + navigation links read from
|
|
1627
|
+
// pageSpec.widgets[] / pageSpec.quickLinks[] (enriched mode). When no
|
|
1628
|
+
// pageSpec data is available (legacy mode), Slot placeholders are emitted.
|
|
1629
|
+
// Slots are kept as EXTENSION POINTS (before/after) in both modes.
|
|
1630
|
+
type HubViewKind = 'app-home' | 'module-home' | 'section-home'
|
|
1631
|
+
type HubViewSuffix = 'AppHome' | 'ModuleHome' | 'SectionHome'
|
|
1632
|
+
const hubViews: Array<[HubViewKind, HubViewSuffix]> = [
|
|
1633
|
+
['app-home', 'AppHome'],
|
|
1634
|
+
['module-home', 'ModuleHome'],
|
|
1635
|
+
['section-home', 'SectionHome'],
|
|
1636
|
+
]
|
|
1637
|
+
for (const [view, suffix] of hubViews) {
|
|
1638
|
+
if (!spec.views.includes(view)) continue
|
|
1639
|
+
const componentName = `${plural}${suffix}Page`
|
|
1640
|
+
|
|
1641
|
+
// Extract widgets + quickLinks from pageSpec (enriched mode)
|
|
1642
|
+
const widgets = spec.pageSpec?.widgets ?? []
|
|
1643
|
+
const quickLinks = spec.pageSpec?.quickLinks ?? []
|
|
1644
|
+
const hasWidgets = widgets.length > 0
|
|
1645
|
+
const hasQuickLinks = quickLinks.length > 0
|
|
1646
|
+
|
|
1647
|
+
// Collect Lucide icon imports from the data
|
|
1648
|
+
const iconSet = new Set(['LayoutGrid', 'ChevronRight'])
|
|
1649
|
+
for (const w of widgets) {
|
|
1650
|
+
iconSet.add(w.icon ? kebabToPascal(w.icon) : (WIDGET_TYPE_ICONS[w.type ?? 'kpi'] ?? 'TrendingUp'))
|
|
1651
|
+
}
|
|
1652
|
+
for (const ql of quickLinks) {
|
|
1653
|
+
if (ql.icon) iconSet.add(kebabToPascal(ql.icon))
|
|
1654
|
+
}
|
|
1655
|
+
const iconImports = Array.from(iconSet).sort().join(', ')
|
|
1656
|
+
|
|
1657
|
+
// Build KPI card markup — concrete StatCard divs or fallback Slot
|
|
1658
|
+
let kpiContent: string
|
|
1659
|
+
if (hasWidgets) {
|
|
1660
|
+
const cards = widgets.map(w => {
|
|
1661
|
+
const icon = w.icon ? kebabToPascal(w.icon) : (WIDGET_TYPE_ICONS[w.type ?? 'kpi'] ?? 'TrendingUp')
|
|
1662
|
+
const label = w.labelKey ? `t('${eLower}.${w.labelKey}')` : `'${w.label ?? w.key}'`
|
|
1663
|
+
return ` <div className="p-4 rounded-[var(--radius-card)] border border-[var(--border-color)] bg-[var(--bg-card)]">
|
|
1664
|
+
<div className="flex items-center justify-between">
|
|
1665
|
+
<span className="text-sm font-medium text-[var(--text-muted)]">{${label}}</span>
|
|
1666
|
+
<${icon} className="h-5 w-5 text-[var(--color-accent-500)]" />
|
|
1667
|
+
</div>
|
|
1668
|
+
<div className="mt-2 text-2xl font-bold text-[var(--text-primary)]">—</div>
|
|
1669
|
+
</div>`
|
|
1670
|
+
})
|
|
1671
|
+
// Keep the Slot after concrete cards for additive extensions
|
|
1672
|
+
cards.push(` <Slot name="${eLower}.home.kpis" />`)
|
|
1673
|
+
kpiContent = cards.join('\n')
|
|
1674
|
+
} else {
|
|
1675
|
+
kpiContent = ` <Slot name="${eLower}.home.kpis" />`
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
// Build QuickLink card markup — concrete nav buttons or fallback Slot
|
|
1679
|
+
let navContent: string
|
|
1680
|
+
if (hasQuickLinks) {
|
|
1681
|
+
const cards = quickLinks.map(ql => {
|
|
1682
|
+
const icon = ql.icon ? kebabToPascal(ql.icon) : 'ChevronRight'
|
|
1683
|
+
const label = ql.labelKey ? `t('${eLower}.${ql.labelKey}')` : `'${ql.label ?? ql.key}'`
|
|
1684
|
+
return ` <button
|
|
1685
|
+
type="button"
|
|
1686
|
+
onClick={() => navigate('#')}
|
|
1687
|
+
className="flex items-center gap-3 p-4 rounded-[var(--radius-card)] border border-[var(--border-color)] bg-[var(--bg-card)] hover:border-[var(--color-accent-500)] transition-colors text-left w-full"
|
|
1688
|
+
>
|
|
1689
|
+
<${icon} className="h-5 w-5 text-[var(--color-accent-500)] shrink-0" />
|
|
1690
|
+
<span className="text-sm font-medium text-[var(--text-primary)] flex-1">{${label}}</span>
|
|
1691
|
+
<ChevronRight className="h-4 w-4 text-[var(--text-muted)]" />
|
|
1692
|
+
</button>`
|
|
1693
|
+
})
|
|
1694
|
+
// Keep the Slot after concrete cards for additive extensions
|
|
1695
|
+
cards.push(` <Slot name="${eLower}.home.navigation" />`)
|
|
1696
|
+
navContent = cards.join('\n')
|
|
1697
|
+
} else {
|
|
1698
|
+
navContent = ` <Slot name="${eLower}.home.navigation" />`
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
files.push({
|
|
1702
|
+
path: pathFor(view, `${componentName}.tsx`),
|
|
1703
|
+
content: `${GENERATED_MARKER}import { useNavigate } from 'react-router-dom'
|
|
1704
|
+
import { useTranslation } from 'react-i18next'
|
|
1705
|
+
import { ${iconImports} } from 'lucide-react'
|
|
1706
|
+
import { Slot } from '@atlashub/smartstack'
|
|
1707
|
+
import { PermissionGuard } from '@/components/auth/PermissionGuard'
|
|
1708
|
+
import { PageTemplate } from '@/components/ui/PageTemplate'
|
|
1709
|
+
|
|
1710
|
+
export function ${componentName}() {
|
|
1711
|
+
const { t } = useTranslation('${spec.module}')
|
|
1712
|
+
const navigate = useNavigate()
|
|
1713
|
+
|
|
1714
|
+
return (
|
|
1715
|
+
<PermissionGuard permission="${permKey}.read">
|
|
1716
|
+
<PageTemplate
|
|
1717
|
+
title={t('${eLower}.home.title')}
|
|
1718
|
+
subtitle={t('${eLower}.home.subtitle')}
|
|
1719
|
+
icon={<LayoutGrid className="w-6 h-6" />}
|
|
1720
|
+
>
|
|
1721
|
+
<Slot name="${eLower}.home.header" />
|
|
1722
|
+
|
|
1723
|
+
<section className="mb-6">
|
|
1724
|
+
<h2 className="text-sm font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-3">
|
|
1725
|
+
{t('${eLower}.home.kpis')}
|
|
1726
|
+
</h2>
|
|
1727
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
1728
|
+
${kpiContent}
|
|
1729
|
+
</div>
|
|
1730
|
+
</section>
|
|
1731
|
+
|
|
1732
|
+
<Slot name="${eLower}.home.kpis.after" />
|
|
1733
|
+
|
|
1734
|
+
<section>
|
|
1735
|
+
<h2 className="text-sm font-semibold uppercase tracking-wider text-[var(--text-muted)] mb-3">
|
|
1736
|
+
{t('${eLower}.home.navigation')}
|
|
1737
|
+
</h2>
|
|
1738
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
1739
|
+
${navContent}
|
|
1740
|
+
</div>
|
|
1741
|
+
</section>
|
|
1742
|
+
|
|
1743
|
+
<Slot name="${eLower}.home.footer" />
|
|
1744
|
+
</PageTemplate>
|
|
1745
|
+
</PermissionGuard>
|
|
1746
|
+
)
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
export default ${componentName}
|
|
1750
|
+
`,
|
|
1751
|
+
})
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
// ─── i18n JSON per locale ────────────────────────────────────────────────
|
|
1755
|
+
// Enriched mode: pageSpec.i18nKeys is the source of truth (parallel key sets
|
|
1756
|
+
// across the 4 locales, validated upstream by persist-prd). Flat keys get
|
|
1757
|
+
// materialised to the nested form i18next consumes.
|
|
1758
|
+
// Legacy mode: buildTranslations() infers from the field dictionary.
|
|
1759
|
+
// i18n catalogues land at `src/i18n/locales/{locale}/{module}.json` —
|
|
1760
|
+
// module-level (one JSON per module, NOT per entity), matching the
|
|
1761
|
+
// SmartStack project layout where i18n config.ts loads namespaces by
|
|
1762
|
+
// module. The CLI writer (index.ts) merges with the existing JSON if
|
|
1763
|
+
// any so the 132 keys already present (custom common/list/form/...)
|
|
1764
|
+
// are preserved.
|
|
1765
|
+
//
|
|
1766
|
+
// For hub views (app-home/module-home/section-home), the template always
|
|
1767
|
+
// references `home.title`, `home.subtitle`, `home.kpis`, `home.navigation`.
|
|
1768
|
+
// PRD pagespecs may omit these — inject them as fallbacks so the page
|
|
1769
|
+
// never shows raw i18n keys.
|
|
1770
|
+
const isHubView = spec.views.some(v => v === 'app-home' || v === 'module-home' || v === 'section-home')
|
|
1771
|
+
for (const locale of LOCALES) {
|
|
1772
|
+
let i18nFlat: Record<string, string>
|
|
1773
|
+
if (spec.pageSpec) {
|
|
1774
|
+
i18nFlat = normalizeI18nFieldKeys({ ...(spec.pageSpec.i18nKeys[locale as keyof typeof spec.pageSpec.i18nKeys] ?? {}) })
|
|
1775
|
+
// Inject missing home keys from the legacy builder
|
|
1776
|
+
if (isHubView) {
|
|
1777
|
+
const legacyHome = buildTranslations(spec, locale as Locale).home as Record<string, string> | undefined
|
|
1778
|
+
if (legacyHome) {
|
|
1779
|
+
for (const [k, v] of Object.entries(legacyHome)) {
|
|
1780
|
+
const flatKey = `home.${k}`
|
|
1781
|
+
if (!i18nFlat[flatKey]) i18nFlat[flatKey] = v
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
} else {
|
|
1786
|
+
i18nFlat = {}
|
|
1787
|
+
}
|
|
1788
|
+
const innerPayload = spec.pageSpec
|
|
1789
|
+
? flatToNested(i18nFlat)
|
|
1790
|
+
: buildTranslations(spec, locale as Locale)
|
|
1791
|
+
// Wrap under entity key so multiple entities in the same module
|
|
1792
|
+
// coexist without collision: { contact: { list: {...} }, company: { list: {...} } }
|
|
1793
|
+
const payload = { [eLower]: innerPayload }
|
|
1794
|
+
files.push({
|
|
1795
|
+
path: `${webRoot}/src/i18n/locales/${locale}/${spec.module}.json`,
|
|
1796
|
+
content: JSON.stringify(payload, null, 2) + '\n',
|
|
1797
|
+
})
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
return files
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
function toHtmlInputType(type: string): string {
|
|
1804
|
+
switch (type.toLowerCase()) {
|
|
1805
|
+
case 'number': case 'int': case 'integer': case 'decimal': return 'number'
|
|
1806
|
+
case 'bool': case 'boolean': return 'checkbox'
|
|
1807
|
+
case 'datetime': return 'datetime-local'
|
|
1808
|
+
case 'date': return 'date'
|
|
1809
|
+
default: return 'text'
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
function fromInputValue(type: string, expr: string): string {
|
|
1814
|
+
switch (type.toLowerCase()) {
|
|
1815
|
+
case 'number': case 'int': case 'integer': case 'decimal': return `Number(${expr})`
|
|
1816
|
+
case 'bool': case 'boolean': return `(event.target as HTMLInputElement).checked`
|
|
1817
|
+
default: return expr
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
// ─── FK lookup helpers — power <EntityLookup> emission on Form pages ──────
|
|
1822
|
+
|
|
1823
|
+
/**
|
|
1824
|
+
* A field is a FK when the orchestrator (ba-develop Phase 3a) has resolved
|
|
1825
|
+
* `fkTo` for it. The shape check is conservative: even if scaffold-component
|
|
1826
|
+
* is called legacy-mode without `fkTo` populated, the field still falls back
|
|
1827
|
+
* to a plain `<input>` and audit DEV-UI-022 catches the regression.
|
|
1828
|
+
*/
|
|
1829
|
+
function isFkField(f: ComponentField): boolean {
|
|
1830
|
+
return Boolean(f.fkTo)
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
/** PascalCase → kebab-case plural: `Department` → `departments`. */
|
|
1834
|
+
function lookupPluralKebab(entity: string): string {
|
|
1835
|
+
const plural = entity.endsWith('s') ? entity : `${entity}s`
|
|
1836
|
+
return plural
|
|
1837
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
1838
|
+
.replace(/_+/g, '-')
|
|
1839
|
+
.toLowerCase()
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
/** Default endpoint when `fkTo.apiEndpoint` isn't overridden by the caller. */
|
|
1843
|
+
function defaultLookupEndpoint(fkTo: NonNullable<ComponentField['fkTo']>): string {
|
|
1844
|
+
if (fkTo.apiEndpoint) return fkTo.apiEndpoint
|
|
1845
|
+
return `/api/${fkTo.module}/${lookupPluralKebab(fkTo.entity)}/lookup`
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
/**
|
|
1849
|
+
* Build the conditional `import { useXxxLookup } from '@/features/.../...'`
|
|
1850
|
+
* lines for the Form/Detail pages. One line per unique target entity.
|
|
1851
|
+
* Empty array → emit nothing.
|
|
1852
|
+
*/
|
|
1853
|
+
function lookupHookImportLines(fkFields: ComponentField[]): string[] {
|
|
1854
|
+
const seen = new Map<string, string>()
|
|
1855
|
+
for (const f of fkFields) {
|
|
1856
|
+
if (!f.fkTo) continue
|
|
1857
|
+
const key = `${f.fkTo.module}/${f.fkTo.entity}`
|
|
1858
|
+
if (seen.has(key)) continue
|
|
1859
|
+
const targetLower = f.fkTo.entity.charAt(0).toLowerCase() + f.fkTo.entity.slice(1)
|
|
1860
|
+
seen.set(
|
|
1861
|
+
key,
|
|
1862
|
+
`import { use${f.fkTo.entity}Lookup } from '@/features/${f.fkTo.module}/${targetLower}/hooks/use${f.fkTo.entity}'`,
|
|
1863
|
+
)
|
|
1864
|
+
}
|
|
1865
|
+
return [...seen.values()]
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
/**
|
|
1869
|
+
* Emit the JSX for ONE form field when it carries `fkTo`. Wraps
|
|
1870
|
+
* <EntityLookup> — no `<input>`, no `<select>`, no datalist. The FK value is
|
|
1871
|
+
* persisted as a string (Guid) in formData; the onChange contract matches the
|
|
1872
|
+
* generated `<E>FormData` field type (string | null).
|
|
1873
|
+
*/
|
|
1874
|
+
function renderFkLookupField(f: ComponentField, eLower: string, e: string): string {
|
|
1875
|
+
if (!f.fkTo) throw new Error(`renderFkLookupField called on non-FK field: ${f.name}`)
|
|
1876
|
+
const camelName = fieldToCamel(f.name)
|
|
1877
|
+
const endpoint = defaultLookupEndpoint(f.fkTo)
|
|
1878
|
+
return ` <EntityLookup
|
|
1879
|
+
apiEndpoint="${endpoint}"
|
|
1880
|
+
value={(formData.${camelName} as string | null) ?? null}
|
|
1881
|
+
onChange={(id) => onChange('${camelName}', (id ?? '') as ${e}FormData['${camelName}'])}
|
|
1882
|
+
label={t('${eLower}.form.fields.${camelName}')}
|
|
1883
|
+
${f.required ? 'required' : ''}
|
|
1884
|
+
disabled={isPending}
|
|
1885
|
+
/>`
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
/**
|
|
1889
|
+
* Emit the JSX for ONE non-FK form field — the legacy `<input>` path. Mirrors
|
|
1890
|
+
* the historical inline template verbatim so the diff stays minimal when
|
|
1891
|
+
* pages mix FK and non-FK fields.
|
|
1892
|
+
*/
|
|
1893
|
+
function renderPlainFormField(f: ComponentField, eLower: string, e: string): string {
|
|
1894
|
+
const camelName = fieldToCamel(f.name)
|
|
1895
|
+
const isBool = /^bool(ean)?$/i.test(f.type)
|
|
1896
|
+
if (isBool) {
|
|
1897
|
+
return ` <label htmlFor="${camelName}" className="flex items-center gap-2 text-sm font-medium text-[var(--text-primary)]">
|
|
1898
|
+
<input
|
|
1899
|
+
id="${camelName}"
|
|
1900
|
+
type="checkbox"
|
|
1901
|
+
checked={!!formData.${camelName}}
|
|
1902
|
+
onChange={event => onChange('${camelName}', (event.target as HTMLInputElement).checked as ${e}FormData['${camelName}'])}
|
|
1903
|
+
disabled={isPending}
|
|
1904
|
+
className="rounded border-[var(--border-color)]"
|
|
1905
|
+
/>
|
|
1906
|
+
{t('${eLower}.form.fields.${camelName}')}
|
|
1907
|
+
</label>`
|
|
1908
|
+
}
|
|
1909
|
+
return ` <div>
|
|
1910
|
+
<label htmlFor="${camelName}" className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
|
1911
|
+
{t('${eLower}.form.fields.${camelName}')}${f.required ? ` <span className="text-[var(--error-text)]">*</span>` : ''}
|
|
1912
|
+
</label>
|
|
1913
|
+
<input
|
|
1914
|
+
id="${camelName}"
|
|
1915
|
+
type="${toHtmlInputType(f.type)}"
|
|
1916
|
+
value={String(formData.${camelName} ?? '')}
|
|
1917
|
+
onChange={event => onChange('${camelName}', ${fromInputValue(f.type, `event.target.value`)} as ${e}FormData['${camelName}'])}
|
|
1918
|
+
${f.required ? 'required' : ''}
|
|
1919
|
+
disabled={isPending}
|
|
1920
|
+
className="input w-full disabled:opacity-50"
|
|
1921
|
+
/>
|
|
1922
|
+
</div>`
|
|
1923
|
+
}
|