@atlashub/smartstack-cli 4.18.0 → 4.20.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/package.json +1 -1
- package/templates/agents/ba-reader.md +86 -80
- package/templates/agents/ba-writer.md +318 -415
- package/templates/agents/docs-context-reader.md +3 -3
- package/templates/mcp-scaffolding/frontend/nav-routes.ts.hbs +133 -0
- package/templates/mcp-scaffolding/frontend/routes.tsx.hbs +126 -0
- package/templates/skills/apex/SKILL.md +29 -16
- package/templates/skills/apex/_shared.md +62 -9
- package/templates/skills/apex/references/analysis-methods.md +8 -6
- package/templates/skills/apex/references/challenge-questions.md +5 -5
- package/templates/skills/apex/references/core-seed-data.md +68 -45
- package/templates/skills/apex/references/frontend-route-wiring-app-tsx.md +26 -21
- package/templates/skills/apex/references/parallel-execution.md +156 -0
- package/templates/skills/apex/references/person-extension-pattern.md +12 -12
- package/templates/skills/apex/references/post-checks.md +1748 -1726
- package/templates/skills/apex/references/smartstack-api.md +63 -57
- package/templates/skills/apex/references/smartstack-frontend-compliance.md +594 -0
- package/templates/skills/apex/references/smartstack-frontend.md +1246 -1842
- package/templates/skills/apex/references/smartstack-layers.md +98 -145
- package/templates/skills/apex/steps/step-00-init.md +30 -6
- package/templates/skills/apex/steps/step-01-analyze.md +27 -23
- package/templates/skills/apex/steps/step-02-plan.md +12 -12
- package/templates/skills/apex/steps/step-03-execute.md +198 -143
- package/templates/skills/apex/steps/step-04-examine.md +24 -93
- package/templates/skills/apex/steps/step-05-deep-review.md +16 -16
- package/templates/skills/apex/steps/step-06-resolve.md +9 -9
- package/templates/skills/apex/steps/step-07-tests.md +3 -1
- package/templates/skills/apex/steps/step-08-run-tests.md +1 -1
- package/templates/skills/business-analyse/SKILL.md +182 -301
- package/templates/skills/business-analyse/_shared.md +119 -336
- package/templates/skills/business-analyse/html/ba-interactive.html +706 -85
- package/templates/skills/business-analyse/html/build-html.js +41 -3
- package/templates/skills/business-analyse/html/src/partials/cadrage-context.html +34 -0
- package/templates/skills/business-analyse/html/src/partials/cadrage-risks.html +48 -0
- package/templates/skills/business-analyse/html/src/partials/cadrage-scope.html +49 -0
- package/templates/skills/business-analyse/html/src/partials/cadrage-stakeholders.html +55 -0
- package/templates/skills/business-analyse/html/src/partials/cadrage-success.html +34 -0
- package/templates/skills/business-analyse/html/src/partials/consol-datamodel.html +8 -0
- package/templates/skills/business-analyse/html/src/partials/consol-flows.html +29 -0
- package/templates/skills/business-analyse/html/src/partials/consol-interactions.html +8 -0
- package/templates/skills/business-analyse/html/src/partials/consol-permissions.html +8 -0
- package/templates/skills/business-analyse/html/src/partials/decomp-dependencies.html +38 -0
- package/templates/skills/business-analyse/html/src/partials/decomp-modules.html +51 -0
- package/templates/skills/business-analyse/html/src/partials/handoff-summary.html +24 -0
- package/templates/skills/business-analyse/html/src/partials/module-spec-container.html +4 -0
- package/templates/skills/business-analyse/html/src/scripts/01-data-init.js +17 -1
- package/templates/skills/business-analyse/html/src/scripts/02-navigation.js +32 -6
- package/templates/skills/business-analyse/html/src/scripts/05-render-specs.js +100 -63
- package/templates/skills/business-analyse/html/src/scripts/06-render-mockups.js +372 -0
- package/templates/skills/business-analyse/html/src/scripts/07-render-handoff.js +1 -1
- package/templates/skills/business-analyse/html/src/scripts/10-comments.js +41 -13
- package/templates/skills/business-analyse/html/src/styles/09-mockups-html.css +136 -0
- package/templates/skills/business-analyse/html/src/template.html +1 -1
- package/templates/skills/business-analyse/patterns/suggestion-catalog.md +7 -5
- package/templates/skills/business-analyse/questionnaire/01-context.md +11 -157
- package/templates/skills/business-analyse/questionnaire/02-stakeholders-scope.md +101 -0
- package/templates/skills/business-analyse/questionnaire/03-data-ui.md +92 -0
- package/templates/skills/business-analyse/questionnaire/04-risks-metrics.md +6 -0
- package/templates/skills/business-analyse/questionnaire/05-cross-module.md +69 -0
- package/templates/skills/business-analyse/questionnaire.md +22 -280
- package/templates/skills/business-analyse/react/application-viewer.md +2 -2
- package/templates/skills/business-analyse/react/components.md +4 -4
- package/templates/skills/business-analyse/react/i18n-template.md +1 -1
- package/templates/skills/business-analyse/react/schema.md +14 -14
- package/templates/skills/business-analyse/references/acceptance-criteria.md +21 -21
- package/templates/skills/business-analyse/references/analysis-semantic-checks.md +3 -3
- package/templates/skills/business-analyse/references/compilation-structure-cards.md +1 -1
- package/templates/skills/business-analyse/references/consolidation-structural-checks.md +5 -5
- package/templates/skills/business-analyse/references/deploy-data-build.md +12 -11
- package/templates/skills/business-analyse/references/deploy-modes.md +10 -10
- package/templates/skills/business-analyse/references/detection-strategies.md +6 -6
- package/templates/skills/business-analyse/references/html-data-mapping.md +15 -15
- package/templates/skills/business-analyse/references/naming-conventions.md +4 -4
- package/templates/skills/business-analyse/references/review-data-mapping.md +29 -29
- package/templates/skills/business-analyse/references/robustness-checks.md +36 -36
- package/templates/skills/business-analyse/references/spec-auto-inference.md +2 -2
- package/templates/skills/business-analyse/references/ui-dashboard-spec.md +1 -1
- package/templates/skills/business-analyse/references/ui-resource-cards.md +1 -1
- package/templates/skills/business-analyse/references/validation-checklist.md +3 -3
- package/templates/skills/business-analyse/references/wireframe-svg-style-guide.md +2 -2
- package/templates/skills/business-analyse/schemas/application-schema.json +8 -8
- package/templates/skills/business-analyse/schemas/feature-schema.json +3 -3
- package/templates/skills/business-analyse/schemas/index-schema.json +47 -0
- package/templates/skills/business-analyse/schemas/project-schema.json +6 -6
- package/templates/skills/business-analyse/schemas/sections/analysis-schema.json +1 -1
- package/templates/skills/business-analyse/schemas/sections/handoff-schema.json +5 -3
- package/templates/skills/business-analyse/schemas/sections/metadata-schema.json +4 -4
- package/templates/skills/business-analyse/schemas/sections/specification-schema.json +1 -1
- package/templates/skills/business-analyse/schemas/shared/common-defs.json +4 -4
- package/templates/skills/business-analyse/steps/step-00-init.md +68 -77
- package/templates/skills/business-analyse/steps/step-01-cadrage.md +50 -216
- package/templates/skills/business-analyse/steps/step-02-structure.md +175 -0
- package/templates/skills/business-analyse/steps/step-03-specify.md +198 -0
- package/templates/skills/business-analyse/steps/step-04-consolidate.md +478 -0
- package/templates/skills/business-analyse/steps/step-05-deploy.md +220 -0
- package/templates/skills/business-analyse/steps/step-06-review.md +51 -69
- package/templates/skills/business-analyse/templates/tpl-frd.md +1 -1
- package/templates/skills/business-analyse/templates/tpl-handoff.md +20 -17
- package/templates/skills/business-analyse/templates/tpl-launch-displays.md +2 -2
- package/templates/skills/business-analyse/templates-react.md +2 -2
- package/templates/skills/derive-prd/SKILL.md +92 -0
- package/templates/skills/derive-prd/references/acceptance-criteria.md +169 -0
- package/templates/skills/derive-prd/references/entity-domain-mapping.md +115 -0
- package/templates/skills/{business-analyse → derive-prd}/references/handoff-file-templates.md +131 -120
- package/templates/skills/{business-analyse → derive-prd}/references/handoff-mappings.md +95 -95
- package/templates/skills/{business-analyse → derive-prd}/references/handoff-seeddata-generation.md +312 -312
- package/templates/skills/{business-analyse → derive-prd}/references/prd-generation.md +262 -263
- package/templates/skills/derive-prd/references/readiness-scoring.md +104 -0
- package/templates/skills/derive-prd/schemas/handoff-schema.json +95 -0
- package/templates/skills/derive-prd/steps/step-00-validate.md +130 -0
- package/templates/skills/derive-prd/steps/step-01-transform.md +206 -0
- package/templates/skills/derive-prd/steps/step-02-export.md +181 -0
- package/templates/skills/{business-analyse → derive-prd}/templates/tpl-progress.md +172 -172
- package/templates/skills/documentation/SKILL.md +7 -0
- package/templates/skills/ralph-loop/SKILL.md +2 -1
- package/templates/skills/ralph-loop/references/init-resume-recovery.md +1 -1
- package/templates/skills/ralph-loop/steps/step-01-task.md +2 -2
- package/templates/skills/apex/references/agent-teams-protocol.md +0 -203
- package/templates/skills/business-analyse/_architecture.md +0 -124
- package/templates/skills/business-analyse/_elicitation.md +0 -206
- package/templates/skills/business-analyse/_module-loop.md +0 -115
- package/templates/skills/business-analyse/_rules.md +0 -142
- package/templates/skills/business-analyse/_suggestions.md +0 -34
- package/templates/skills/business-analyse/questionnaire/00-application.md +0 -160
- package/templates/skills/business-analyse/questionnaire/00b-project.md +0 -85
- package/templates/skills/business-analyse/questionnaire/02-stakeholders.md +0 -189
- package/templates/skills/business-analyse/questionnaire/03-scope.md +0 -164
- package/templates/skills/business-analyse/questionnaire/04-data.md +0 -88
- package/templates/skills/business-analyse/questionnaire/05-integrations.md +0 -58
- package/templates/skills/business-analyse/questionnaire/06-security.md +0 -68
- package/templates/skills/business-analyse/questionnaire/07-ui.md +0 -76
- package/templates/skills/business-analyse/questionnaire/08-performance.md +0 -42
- package/templates/skills/business-analyse/questionnaire/09-constraints.md +0 -45
- package/templates/skills/business-analyse/questionnaire/10-documentation.md +0 -58
- package/templates/skills/business-analyse/questionnaire/11-data-lifecycle.md +0 -59
- package/templates/skills/business-analyse/questionnaire/12-migration.md +0 -58
- package/templates/skills/business-analyse/questionnaire/13-cross-module.md +0 -69
- package/templates/skills/business-analyse/questionnaire/14-risk-assumptions.md +0 -135
- package/templates/skills/business-analyse/questionnaire/15-success-metrics.md +0 -136
- package/templates/skills/business-analyse/references/agent-module-prompt.md +0 -366
- package/templates/skills/business-analyse/references/agent-pooling-best-practices.md +0 -557
- package/templates/skills/business-analyse/references/cache-warming-strategy.md +0 -566
- package/templates/skills/business-analyse/references/cadrage-challenge-patterns.md +0 -41
- package/templates/skills/business-analyse/references/cadrage-coverage-matrix.md +0 -74
- package/templates/skills/business-analyse/references/cadrage-pre-analysis.md +0 -115
- package/templates/skills/business-analyse/references/cadrage-shared-modules.md +0 -68
- package/templates/skills/business-analyse/references/cadrage-structure-cards.md +0 -85
- package/templates/skills/business-analyse/references/team-orchestration.md +0 -1093
- package/templates/skills/business-analyse/references/validate-incremental-html.md +0 -121
- package/templates/skills/business-analyse/steps/step-01b-applications.md +0 -419
- package/templates/skills/business-analyse/steps/step-02-decomposition.md +0 -387
- package/templates/skills/business-analyse/steps/step-03a-data.md +0 -16
- package/templates/skills/business-analyse/steps/step-03a1-setup.md +0 -486
- package/templates/skills/business-analyse/steps/step-03a2-analysis.md +0 -300
- package/templates/skills/business-analyse/steps/step-03b-ui.md +0 -405
- package/templates/skills/business-analyse/steps/step-03c-compile.md +0 -516
- package/templates/skills/business-analyse/steps/step-03d-validate.md +0 -691
- package/templates/skills/business-analyse/steps/step-04-consolidation.md +0 -17
- package/templates/skills/business-analyse/steps/step-04a-collect.md +0 -415
- package/templates/skills/business-analyse/steps/step-04b-analyze.md +0 -163
- package/templates/skills/business-analyse/steps/step-04c-decide.md +0 -186
- package/templates/skills/business-analyse/steps/step-05a-handoff.md +0 -937
- package/templates/skills/business-analyse/steps/step-05b-deploy.md +0 -522
- package/templates/skills/business-analyse/steps/step-05c-ralph-readiness.md +0 -703
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
# SmartStack Frontend Compliance — Documentation, Testing & Gates
|
|
2
|
+
|
|
3
|
+
> **Loaded by:** step-03 (documentation generation), step-04 (compliance validation)
|
|
4
|
+
> **Purpose:** Documentation integration, form testing templates, and compliance gates.
|
|
5
|
+
> **Split from:** `smartstack-frontend.md` (sections 7-9) for context reduction.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 7. Documentation Panel Integration (DocToggleButton)
|
|
10
|
+
|
|
11
|
+
> **EVERY list/detail page MUST include a `DocToggleButton` in its header.**
|
|
12
|
+
> This button opens the right-side documentation panel showing the module's user documentation.
|
|
13
|
+
|
|
14
|
+
### Component Import
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
import { DocToggleButton } from '@/components/docs/DocToggleButton';
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Placement — Always in the page header actions area (top right)
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
{/* Header with DocToggleButton */}
|
|
24
|
+
<div className="flex items-center justify-between">
|
|
25
|
+
<h1 className="text-2xl font-bold text-[var(--text-primary)]">
|
|
26
|
+
{t('{module}:title', 'Module Title')}
|
|
27
|
+
</h1>
|
|
28
|
+
<div className="flex items-center gap-2">
|
|
29
|
+
<DocToggleButton />
|
|
30
|
+
<button onClick={() => navigate('create')} className="...">
|
|
31
|
+
{t('{module}:actions.create', 'Create')}
|
|
32
|
+
</button>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### How it Works
|
|
38
|
+
|
|
39
|
+
1. `DocToggleButton` uses `useDocPanel()` context (provided by the Layout)
|
|
40
|
+
2. On click → opens the `DocPanel` on the right side of the screen
|
|
41
|
+
3. The panel loads the module's documentation via iframe (`?embedded=true`)
|
|
42
|
+
4. Route → doc mapping is in `DocPanelContext.tsx` — maps current pathname to doc URL
|
|
43
|
+
5. Panel is resizable (20-60% width), size persists in localStorage
|
|
44
|
+
|
|
45
|
+
### Documentation Generation
|
|
46
|
+
|
|
47
|
+
After frontend pages are created, invoke the `/documentation` skill to generate:
|
|
48
|
+
|
|
49
|
+
| File | Content |
|
|
50
|
+
|------|---------|
|
|
51
|
+
| `src/pages/docs/business/{app}/{module}/doc-data.ts` | Data-driven documentation (~50-80 lines) |
|
|
52
|
+
| `src/pages/docs/business/{app}/{module}/index.tsx` | Page wrapper (~10 lines) using `DocRenderer` |
|
|
53
|
+
| `src/i18n/locales/fr/docs-{app}-{module}.json` | French doc translations (source language) |
|
|
54
|
+
|
|
55
|
+
The `DocRenderer` shared component renders all 8 documentation sections (overview, use cases, benefits, features, steps, FAQ, business rules, permissions, API endpoints) from the `doc-data.ts` file.
|
|
56
|
+
|
|
57
|
+
### Custom Doc URL (optional)
|
|
58
|
+
|
|
59
|
+
If the automatic route mapping doesn't work for your module, pass a custom URL:
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
<DocToggleButton customDocUrl="/docs/human-resources/employees" />
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Rules
|
|
66
|
+
|
|
67
|
+
- Include `DocToggleButton` in header actions of every list page
|
|
68
|
+
- Include `DocToggleButton` in header actions of every detail page
|
|
69
|
+
- Create/Edit form pages do NOT need DocToggleButton (users don't read docs while filling forms)
|
|
70
|
+
- DocToggleButton is imported from `@/components/docs/DocToggleButton` (shared component)
|
|
71
|
+
- The Layout already provides `DocPanelProvider` — no additional wrapping needed
|
|
72
|
+
- Documentation content is generated by the `/documentation` skill AFTER frontend pages exist
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 7b. Checklist for /apex Frontend Execution
|
|
77
|
+
|
|
78
|
+
Before marking frontend tasks as complete, verify:
|
|
79
|
+
|
|
80
|
+
- [ ] All page imports use `React.lazy()` with named export wrapping
|
|
81
|
+
- [ ] `<Suspense fallback={<PageLoader />}>` wraps all lazy components in routes
|
|
82
|
+
- [ ] Translation files exist for **all 4 languages** (fr, en, it, de) in `src/i18n/locales/`
|
|
83
|
+
- [ ] All `t()` calls include namespace prefix AND fallback value
|
|
84
|
+
- [ ] No hardcoded strings in JSX — all text goes through `t()`
|
|
85
|
+
- [ ] CSS uses variables only — no hardcoded Tailwind colors (POST-CHECK C9)
|
|
86
|
+
- [ ] Pages follow loading → error → content pattern
|
|
87
|
+
- [ ] Pages use `src/pages/{App}/{Module}/` hierarchy
|
|
88
|
+
- [ ] API calls use generated hooks or `apiClient` (never raw axios)
|
|
89
|
+
- [ ] Components use DataTable/EntityCard (never raw HTML `<table>`)
|
|
90
|
+
- [ ] FK fields use `EntityLookup` — see `smartstack-frontend.md` section 6
|
|
91
|
+
- [ ] Create/Edit forms are full pages with own routes — see `smartstack-frontend.md` section 3b
|
|
92
|
+
- [ ] `DocToggleButton` present in header of every list/detail page (see section 7)
|
|
93
|
+
- [ ] `/documentation` skill invoked to generate module doc-data.ts
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 7c. Cross-Tenant Entity UI Patterns
|
|
98
|
+
|
|
99
|
+
> **For optional and scoped tenant entities, the frontend MUST provide UI controls to set the scope/visibility.**
|
|
100
|
+
|
|
101
|
+
### Scope Types
|
|
102
|
+
|
|
103
|
+
| Type | Behavior | Use case |
|
|
104
|
+
|------|----------|----------|
|
|
105
|
+
| **Optional** | Entity can be tenant-specific OR shared (binary choice) | Data that can belong to one org or all orgs |
|
|
106
|
+
| **Scoped** | Entity has explicit scope enum: Tenant / Shared / Platform | Data with multiple visibility levels |
|
|
107
|
+
|
|
108
|
+
### Scope Selector in Create Forms (Optional Entities)
|
|
109
|
+
|
|
110
|
+
For `optional` tenant entities, add a toggle in the create form allowing the user to decide:
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
import { useState } from 'react';
|
|
114
|
+
import { useTranslation } from 'react-i18next';
|
|
115
|
+
|
|
116
|
+
export function EntityCreatePage() {
|
|
117
|
+
const { t } = useTranslation(['{module}']);
|
|
118
|
+
const [formData, setFormData] = useState({
|
|
119
|
+
name: '',
|
|
120
|
+
isShared: false, // User decision: tenant-specific (false) or shared (true)
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const handleScopeChange = (value: string) => {
|
|
124
|
+
setFormData({ ...formData, isShared: value === 'shared' });
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div className="space-y-6">
|
|
129
|
+
{/* ... form header ... */}
|
|
130
|
+
|
|
131
|
+
<form onSubmit={handleSubmit} className="bg-[var(--bg-card)] border border-[var(--border-color)] rounded-[var(--radius-card)] p-6 space-y-4">
|
|
132
|
+
{/* Name field */}
|
|
133
|
+
<div>
|
|
134
|
+
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
|
135
|
+
{t('{module}:form.name', 'Name')}
|
|
136
|
+
</label>
|
|
137
|
+
<input
|
|
138
|
+
type="text"
|
|
139
|
+
value={formData.name}
|
|
140
|
+
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
|
141
|
+
className="w-full px-3 py-2 border border-[var(--border-color)] bg-[var(--bg-card)] text-[var(--text-primary)] rounded-[var(--radius-input)] focus:outline-none focus:ring-2 focus:ring-[var(--color-accent-500)]"
|
|
142
|
+
required
|
|
143
|
+
/>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
{/* Scope selector — binary toggle for optional entities */}
|
|
147
|
+
<div className="space-y-2">
|
|
148
|
+
<label className="block text-sm font-medium text-[var(--text-primary)]">
|
|
149
|
+
{t('common:scope', 'Scope')}
|
|
150
|
+
</label>
|
|
151
|
+
<select
|
|
152
|
+
value={formData.isShared ? 'shared' : 'tenant'}
|
|
153
|
+
onChange={(e) => handleScopeChange(e.target.value)}
|
|
154
|
+
className="w-full px-3 py-2 border border-[var(--border-color)] rounded-[var(--radius-input)] bg-[var(--bg-card)] text-[var(--text-primary)]"
|
|
155
|
+
>
|
|
156
|
+
<option value="tenant">
|
|
157
|
+
{t('common:scope.tenant', 'My Organization')}
|
|
158
|
+
</option>
|
|
159
|
+
<option value="shared">
|
|
160
|
+
{t('common:scope.shared', 'Shared (All Organizations)')}
|
|
161
|
+
</option>
|
|
162
|
+
</select>
|
|
163
|
+
<p className="text-xs text-[var(--text-secondary)]">
|
|
164
|
+
{formData.isShared
|
|
165
|
+
? t('common:scope.shared.hint', 'This data will be accessible to all organizations')
|
|
166
|
+
: t('common:scope.tenant.hint', 'This data will only be visible to your organization')}
|
|
167
|
+
</p>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
{/* Actions */}
|
|
171
|
+
<div className="flex justify-end gap-3 pt-4 border-t border-[var(--border-color)]">
|
|
172
|
+
<button type="button" onClick={() => navigate(-1)} className="px-4 py-2 text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] rounded-[var(--radius-button)]">
|
|
173
|
+
{t('common:actions.cancel', 'Cancel')}
|
|
174
|
+
</button>
|
|
175
|
+
<button type="submit" disabled={submitting} className="px-4 py-2 bg-[var(--color-accent-500)] text-white rounded-[var(--radius-button)] disabled:opacity-50">
|
|
176
|
+
{submitting ? t('common:actions.saving', 'Saving...') : t('common:actions.save', 'Save')}
|
|
177
|
+
</button>
|
|
178
|
+
</div>
|
|
179
|
+
</form>
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Scope Selector in Create Forms (Scoped Entities)
|
|
186
|
+
|
|
187
|
+
For `scoped` entities with explicit enum values (Tenant, Shared, Platform), use a dropdown with all scope options:
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
export function EntityCreatePage() {
|
|
191
|
+
const { t } = useTranslation(['{module}']);
|
|
192
|
+
const [formData, setFormData] = useState({
|
|
193
|
+
name: '',
|
|
194
|
+
scope: 'Tenant', // Enum: 'Tenant' | 'Shared' | 'Platform'
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<form onSubmit={handleSubmit} className="bg-[var(--bg-card)] border border-[var(--border-color)] rounded-[var(--radius-card)] p-6 space-y-4">
|
|
199
|
+
{/* Name field */}
|
|
200
|
+
<div>
|
|
201
|
+
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
|
202
|
+
{t('{module}:form.name', 'Name')}
|
|
203
|
+
</label>
|
|
204
|
+
<input
|
|
205
|
+
type="text"
|
|
206
|
+
value={formData.name}
|
|
207
|
+
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
|
208
|
+
className="w-full px-3 py-2 border border-[var(--border-color)] bg-[var(--bg-card)] text-[var(--text-primary)] rounded-[var(--radius-input)] focus:outline-none focus:ring-2 focus:ring-[var(--color-accent-500)]"
|
|
209
|
+
required
|
|
210
|
+
/>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
{/* Scope selector — enum values for scoped entities */}
|
|
214
|
+
<div>
|
|
215
|
+
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1">
|
|
216
|
+
{t('common:scope', 'Scope')}
|
|
217
|
+
</label>
|
|
218
|
+
<select
|
|
219
|
+
value={formData.scope}
|
|
220
|
+
onChange={(e) => setFormData(prev => ({ ...prev, scope: e.target.value }))}
|
|
221
|
+
className="w-full px-3 py-2 border border-[var(--border-color)] rounded-[var(--radius-input)] bg-[var(--bg-card)] text-[var(--text-primary)]"
|
|
222
|
+
required
|
|
223
|
+
>
|
|
224
|
+
<option value="Tenant">{t('common:scope.tenant', 'My Organization')}</option>
|
|
225
|
+
<option value="Shared">{t('common:scope.shared', 'Shared')}</option>
|
|
226
|
+
<option value="Platform">{t('common:scope.platform', 'Platform (Admin Only)')}</option>
|
|
227
|
+
</select>
|
|
228
|
+
<p className="text-xs text-[var(--text-secondary)] mt-1">
|
|
229
|
+
{t('common:scope.help', 'Select the visibility level for this data')}
|
|
230
|
+
</p>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
{/* Actions */}
|
|
234
|
+
<div className="flex justify-end gap-3 pt-4 border-t border-[var(--border-color)]">
|
|
235
|
+
<button type="button" onClick={() => navigate(-1)} className="px-4 py-2 text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] rounded-[var(--radius-button)]">
|
|
236
|
+
{t('common:actions.cancel', 'Cancel')}
|
|
237
|
+
</button>
|
|
238
|
+
<button type="submit" disabled={submitting} className="px-4 py-2 bg-[var(--color-accent-500)] text-white rounded-[var(--radius-button)] disabled:opacity-50">
|
|
239
|
+
{submitting ? t('common:actions.saving', 'Saving...') : t('common:actions.save', 'Save')}
|
|
240
|
+
</button>
|
|
241
|
+
</div>
|
|
242
|
+
</form>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Scope Indicator in List Views
|
|
248
|
+
|
|
249
|
+
Display a visual indicator/badge on each row showing the entity scope:
|
|
250
|
+
|
|
251
|
+
```tsx
|
|
252
|
+
import { useTranslation } from 'react-i18next';
|
|
253
|
+
|
|
254
|
+
// ScopeBadge component for reuse
|
|
255
|
+
interface ScopeBadgeProps {
|
|
256
|
+
tenantId?: string | null; // For optional entities: null = shared, value = tenant-specific
|
|
257
|
+
scope?: string; // For scoped entities: 'Tenant' | 'Shared' | 'Platform'
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function ScopeBadge({ tenantId, scope }: ScopeBadgeProps) {
|
|
261
|
+
const { t } = useTranslation(['common']);
|
|
262
|
+
|
|
263
|
+
// Optional entity scope
|
|
264
|
+
if (tenantId !== undefined) {
|
|
265
|
+
const isTenant = Boolean(tenantId);
|
|
266
|
+
return (
|
|
267
|
+
<span
|
|
268
|
+
className={`px-2 py-1 rounded-full text-xs font-semibold ${
|
|
269
|
+
isTenant
|
|
270
|
+
? 'bg-[var(--bg-accent-light)] text-[var(--color-accent-600)]'
|
|
271
|
+
: 'bg-[var(--bg-secondary)] text-[var(--text-secondary)]'
|
|
272
|
+
}`}
|
|
273
|
+
>
|
|
274
|
+
{isTenant
|
|
275
|
+
? t('common:scope.tenant', 'Tenant')
|
|
276
|
+
: t('common:scope.shared', 'Shared')}
|
|
277
|
+
</span>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Scoped entity scope
|
|
282
|
+
if (scope) {
|
|
283
|
+
const scopeStyles: Record<string, { bg: string; text: string }> = {
|
|
284
|
+
Tenant: {
|
|
285
|
+
bg: 'bg-[var(--bg-accent-light)]',
|
|
286
|
+
text: 'text-[var(--color-accent-600)]',
|
|
287
|
+
},
|
|
288
|
+
Shared: {
|
|
289
|
+
bg: 'bg-[var(--bg-secondary)]',
|
|
290
|
+
text: 'text-[var(--text-secondary)]',
|
|
291
|
+
},
|
|
292
|
+
Platform: {
|
|
293
|
+
bg: 'bg-[var(--bg-warning-light)]',
|
|
294
|
+
text: 'text-[var(--color-warning-600)]',
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const style = scopeStyles[scope] || scopeStyles.Tenant;
|
|
299
|
+
const scopeLabel = {
|
|
300
|
+
Tenant: t('common:scope.tenant', 'Organization'),
|
|
301
|
+
Shared: t('common:scope.shared', 'Shared'),
|
|
302
|
+
Platform: t('common:scope.platform', 'Platform'),
|
|
303
|
+
}[scope] || scope;
|
|
304
|
+
|
|
305
|
+
return (
|
|
306
|
+
<span className={`px-2 py-1 rounded-full text-xs font-semibold ${style.bg} ${style.text}`}>
|
|
307
|
+
{scopeLabel}
|
|
308
|
+
</span>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Using ScopeBadge in DataTable Columns
|
|
317
|
+
|
|
318
|
+
```tsx
|
|
319
|
+
// In the list page, add a scope column
|
|
320
|
+
const columns = [
|
|
321
|
+
{ key: 'name', label: t('{module}:columns.name', 'Name') },
|
|
322
|
+
{ key: 'code', label: t('{module}:columns.code', 'Code') },
|
|
323
|
+
{
|
|
324
|
+
key: 'scope',
|
|
325
|
+
label: t('common:scope', 'Scope'),
|
|
326
|
+
render: (row) => (
|
|
327
|
+
// For optional entities: show based on tenantId
|
|
328
|
+
<ScopeBadge tenantId={row.tenantId} />
|
|
329
|
+
// OR for scoped entities: show based on scope field
|
|
330
|
+
// <ScopeBadge scope={row.scope} />
|
|
331
|
+
),
|
|
332
|
+
},
|
|
333
|
+
{ key: 'actions', label: t('{module}:columns.actions', 'Actions') },
|
|
334
|
+
];
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
<DataTable
|
|
338
|
+
columns={columns}
|
|
339
|
+
data={data}
|
|
340
|
+
searchable
|
|
341
|
+
pagination={{ pageSize: 10 }}
|
|
342
|
+
onRowClick={(row) => navigate(`${row.id}`)}
|
|
343
|
+
/>
|
|
344
|
+
);
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### I18n Keys for Scope UI
|
|
348
|
+
|
|
349
|
+
Add these keys to `src/i18n/locales/*/common.json`:
|
|
350
|
+
|
|
351
|
+
```json
|
|
352
|
+
{
|
|
353
|
+
"scope": "Scope",
|
|
354
|
+
"scope.tenant": "My Organization",
|
|
355
|
+
"scope.tenant.hint": "This data will only be visible to your organization",
|
|
356
|
+
"scope.shared": "Shared (All Organizations)",
|
|
357
|
+
"scope.shared.hint": "This data will be accessible to all organizations",
|
|
358
|
+
"scope.platform": "Platform (Admin Only)",
|
|
359
|
+
"scope.help": "Select the visibility level for this data"
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
And in the module-specific translation files (e.g., `employees.json`):
|
|
364
|
+
|
|
365
|
+
```json
|
|
366
|
+
{
|
|
367
|
+
"form": {
|
|
368
|
+
"scope": "Scope",
|
|
369
|
+
"scopeHint": "Choose who can see this data"
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### Rules
|
|
375
|
+
|
|
376
|
+
- Provide scope controls in create forms for optional/scoped entities
|
|
377
|
+
- Show scope indicator badges in list views
|
|
378
|
+
- Use `ScopeBadge` component for consistency across modules
|
|
379
|
+
- Do not let users create shared entities without explicit choice
|
|
380
|
+
- Do not hide scope controls — scope is a business-critical property
|
|
381
|
+
- Include scope-related translation keys in i18n files (all 4 languages)
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## 8. Frontend Form Testing
|
|
386
|
+
|
|
387
|
+
> **ALL form pages MUST have tests.** Forms are critical user interaction points and MUST be verified.
|
|
388
|
+
|
|
389
|
+
### Required Test Coverage per Form Page
|
|
390
|
+
|
|
391
|
+
| Test category | What to verify | Tool |
|
|
392
|
+
|---------------|---------------|------|
|
|
393
|
+
| Rendering | Form renders with all expected fields | Vitest + React Testing Library |
|
|
394
|
+
| Validation | Required fields show errors on empty submit | Vitest + React Testing Library |
|
|
395
|
+
| Submission | Successful submit calls API and navigates back | Vitest + MSW (mock API) |
|
|
396
|
+
| Pre-fill (edit) | Edit form loads entity data into fields | Vitest + React Testing Library |
|
|
397
|
+
| Navigation | Back button calls `navigate(-1)` | Vitest + React Testing Library |
|
|
398
|
+
| Error handling | API error displays error message | Vitest + MSW |
|
|
399
|
+
|
|
400
|
+
### Test File Convention
|
|
401
|
+
|
|
402
|
+
```
|
|
403
|
+
src/pages/{App}/{Module}/
|
|
404
|
+
├── EntityCreatePage.tsx
|
|
405
|
+
├── EntityCreatePage.test.tsx ← MANDATORY
|
|
406
|
+
├── EntityEditPage.tsx
|
|
407
|
+
├── EntityEditPage.test.tsx ← MANDATORY
|
|
408
|
+
├── EntityListPage.tsx
|
|
409
|
+
└── EntityDetailPage.tsx
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### Create Page Test Template
|
|
413
|
+
|
|
414
|
+
```tsx
|
|
415
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
416
|
+
import userEvent from '@testing-library/user-event';
|
|
417
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
418
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
419
|
+
import { EntityCreatePage } from './EntityCreatePage';
|
|
420
|
+
|
|
421
|
+
// Mock API
|
|
422
|
+
vi.mock('@/services/api/apiClient');
|
|
423
|
+
const mockNavigate = vi.fn();
|
|
424
|
+
vi.mock('react-router-dom', async () => ({
|
|
425
|
+
...(await vi.importActual('react-router-dom')),
|
|
426
|
+
useNavigate: () => mockNavigate,
|
|
427
|
+
}));
|
|
428
|
+
|
|
429
|
+
describe('EntityCreatePage', () => {
|
|
430
|
+
it('renders the create form with all fields', () => {
|
|
431
|
+
render(<MemoryRouter><EntityCreatePage /></MemoryRouter>);
|
|
432
|
+
expect(screen.getByRole('textbox', { name: /name/i })).toBeInTheDocument();
|
|
433
|
+
// Verify all expected form fields
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('shows validation errors on empty submit', async () => {
|
|
437
|
+
render(<MemoryRouter><EntityCreatePage /></MemoryRouter>);
|
|
438
|
+
await userEvent.click(screen.getByRole('button', { name: /save|create/i }));
|
|
439
|
+
await waitFor(() => {
|
|
440
|
+
expect(screen.getByText(/required/i)).toBeInTheDocument();
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('submits form and navigates back on success', async () => {
|
|
445
|
+
render(<MemoryRouter><EntityCreatePage /></MemoryRouter>);
|
|
446
|
+
await userEvent.type(screen.getByRole('textbox', { name: /name/i }), 'Test');
|
|
447
|
+
await userEvent.click(screen.getByRole('button', { name: /save|create/i }));
|
|
448
|
+
await waitFor(() => {
|
|
449
|
+
expect(mockNavigate).toHaveBeenCalledWith(-1);
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('navigates back on cancel/back button', async () => {
|
|
454
|
+
render(<MemoryRouter><EntityCreatePage /></MemoryRouter>);
|
|
455
|
+
await userEvent.click(screen.getByRole('button', { name: /back|cancel/i }));
|
|
456
|
+
expect(mockNavigate).toHaveBeenCalledWith(-1);
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
### Edit Page Test Template
|
|
462
|
+
|
|
463
|
+
```tsx
|
|
464
|
+
describe('EntityEditPage', () => {
|
|
465
|
+
it('loads entity data and pre-fills the form', async () => {
|
|
466
|
+
render(<MemoryRouter initialEntries={['/entities/123/edit']}><EntityEditPage /></MemoryRouter>);
|
|
467
|
+
await waitFor(() => {
|
|
468
|
+
expect(screen.getByDisplayValue('Existing Name')).toBeInTheDocument();
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('submits updated data and navigates back', async () => {
|
|
473
|
+
render(<MemoryRouter initialEntries={['/entities/123/edit']}><EntityEditPage /></MemoryRouter>);
|
|
474
|
+
await waitFor(() => screen.getByDisplayValue('Existing Name'));
|
|
475
|
+
await userEvent.clear(screen.getByRole('textbox', { name: /name/i }));
|
|
476
|
+
await userEvent.type(screen.getByRole('textbox', { name: /name/i }), 'Updated');
|
|
477
|
+
await userEvent.click(screen.getByRole('button', { name: /save/i }));
|
|
478
|
+
await waitFor(() => {
|
|
479
|
+
expect(mockNavigate).toHaveBeenCalledWith(-1);
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('displays error when API call fails', async () => {
|
|
484
|
+
// Mock API to reject
|
|
485
|
+
render(<MemoryRouter initialEntries={['/entities/123/edit']}><EntityEditPage /></MemoryRouter>);
|
|
486
|
+
// ... trigger submit with mocked failure
|
|
487
|
+
await waitFor(() => {
|
|
488
|
+
expect(screen.getByText(/failed/i)).toBeInTheDocument();
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### Rules
|
|
495
|
+
|
|
496
|
+
- Include a companion `EntityCreatePage.test.tsx` for every `EntityCreatePage.tsx`
|
|
497
|
+
- Include a companion `EntityEditPage.test.tsx` for every `EntityEditPage.tsx`
|
|
498
|
+
- Cover in tests: rendering, validation, submit success, submit error, navigation
|
|
499
|
+
- Use `@testing-library/react` + `@testing-library/user-event` (NEVER enzyme)
|
|
500
|
+
- Mock API with `vi.mock()` or MSW — NEVER make real API calls in tests
|
|
501
|
+
- Test files live next to their component (co-located, NOT in a separate `__tests__/` folder)
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
505
|
+
## 9. Frontend Compliance Gates (5 Mandatory Checks)
|
|
506
|
+
|
|
507
|
+
> **Run these checks before any frontend commit.** All 5 gates MUST pass.
|
|
508
|
+
|
|
509
|
+
### Gate 1: CSS Variables (Theme System)
|
|
510
|
+
|
|
511
|
+
```bash
|
|
512
|
+
ALL_PAGES=$(find src/pages/ src/components/ -name "*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\.")
|
|
513
|
+
if [ -n "$ALL_PAGES" ]; then
|
|
514
|
+
HARDCODED=$(grep -Pn '(bg|text|border)-(?!\[)(red|blue|green|gray|white|black|slate|zinc|neutral|stone)-' $ALL_PAGES 2>/dev/null)
|
|
515
|
+
if [ -n "$HARDCODED" ]; then
|
|
516
|
+
echo "FAIL: Hardcoded Tailwind colors found — must use CSS variables (see smartstack-frontend.md section 4)"
|
|
517
|
+
echo "$HARDCODED"
|
|
518
|
+
else
|
|
519
|
+
echo "PASS: CSS variables"
|
|
520
|
+
fi
|
|
521
|
+
fi
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### Gate 2: Forms as Pages (ZERO Modals/Drawers)
|
|
525
|
+
|
|
526
|
+
```bash
|
|
527
|
+
PAGE_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null)
|
|
528
|
+
if [ -n "$PAGE_FILES" ]; then
|
|
529
|
+
FAIL=false
|
|
530
|
+
MODAL_IMPORTS=$(grep -Pn "import.*(?:Modal|Dialog|Drawer|Popup|Sheet|SlideOver|Overlay)" $PAGE_FILES 2>/dev/null)
|
|
531
|
+
if [ -n "$MODAL_IMPORTS" ]; then
|
|
532
|
+
echo "FAIL: Modal/Dialog/Drawer imports — forms MUST be full pages (see smartstack-frontend.md section 3b)"
|
|
533
|
+
echo "$MODAL_IMPORTS"
|
|
534
|
+
FAIL=true
|
|
535
|
+
fi
|
|
536
|
+
MODAL_STATE=$(grep -Pn "useState.*(?:isOpen|showModal|showDialog|showCreate|showEdit|showForm|isCreating|isEditing|showDrawer|showPanel|showSlideOver|selectedEntity|editingEntity)" $PAGE_FILES 2>/dev/null)
|
|
537
|
+
if [ -n "$MODAL_STATE" ]; then
|
|
538
|
+
echo "FAIL: Inline form state detected — forms MUST be separate pages (see smartstack-frontend.md section 3b)"
|
|
539
|
+
echo "$MODAL_STATE"
|
|
540
|
+
FAIL=true
|
|
541
|
+
fi
|
|
542
|
+
if [ "$FAIL" = false ]; then echo "PASS: No modals/drawers"; fi
|
|
543
|
+
fi
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
### Gate 3: I18n File Structure
|
|
547
|
+
|
|
548
|
+
```bash
|
|
549
|
+
if [ ! -d "src/i18n/locales" ]; then
|
|
550
|
+
echo "FAIL: Missing src/i18n/locales/ directory"
|
|
551
|
+
else
|
|
552
|
+
for LANG in fr en it de; do
|
|
553
|
+
JSON_FILES=$(find "src/i18n/locales/$LANG" -name "*.json" 2>/dev/null | wc -l)
|
|
554
|
+
if [ "$JSON_FILES" -eq 0 ]; then
|
|
555
|
+
echo "FAIL: No JSON files in src/i18n/locales/$LANG/"
|
|
556
|
+
else
|
|
557
|
+
echo "PASS: $LANG ($JSON_FILES files)"
|
|
558
|
+
fi
|
|
559
|
+
done
|
|
560
|
+
fi
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### Gate 4: Lazy Loading
|
|
564
|
+
|
|
565
|
+
```bash
|
|
566
|
+
APP_TSX=$(find src/ -name "App.tsx" -not -path "*/node_modules/*" 2>/dev/null | head -1)
|
|
567
|
+
ROUTE_FILES=$(find src/routes/ -name "*.tsx" -o -name "*.ts" 2>/dev/null)
|
|
568
|
+
if [ -n "$APP_TSX" ]; then
|
|
569
|
+
STATIC_IMPORTS=$(grep -Pn "^import .+ from '@/pages/" "$APP_TSX" $ROUTE_FILES 2>/dev/null)
|
|
570
|
+
if [ -n "$STATIC_IMPORTS" ]; then
|
|
571
|
+
echo "FAIL: Static page imports — MUST use React.lazy() (see smartstack-frontend.md section 1)"
|
|
572
|
+
echo "$STATIC_IMPORTS"
|
|
573
|
+
else
|
|
574
|
+
echo "PASS: Lazy loading"
|
|
575
|
+
fi
|
|
576
|
+
fi
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
### Gate 5: useTranslation in Pages
|
|
580
|
+
|
|
581
|
+
```bash
|
|
582
|
+
PAGE_FILES=$(find src/pages/ -name "*.tsx" 2>/dev/null | grep -v "\.test\." | grep -v node_modules)
|
|
583
|
+
if [ -n "$PAGE_FILES" ]; then
|
|
584
|
+
TOTAL=$(echo "$PAGE_FILES" | wc -l)
|
|
585
|
+
WITH_I18N=$(grep -l "useTranslation" $PAGE_FILES 2>/dev/null | wc -l)
|
|
586
|
+
if [ "$WITH_I18N" -eq 0 ]; then
|
|
587
|
+
echo "FAIL: No pages use useTranslation — all text must be translated (see smartstack-frontend.md section 2)"
|
|
588
|
+
else
|
|
589
|
+
echo "PASS: $WITH_I18N/$TOTAL pages use useTranslation"
|
|
590
|
+
fi
|
|
591
|
+
fi
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
> **ALL 5 gates MUST pass before frontend commit.** When delegating to `/ui-components` skill, include explicit instructions: CSS variables only, forms as full pages, i18n with namespace + fallback.
|