@polymorphism-tech/morph-spec 4.8.12 → 4.8.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +379 -379
  2. package/bin/morph-spec.js +23 -2
  3. package/bin/{task-manager.cjs → task-manager.js} +249 -172
  4. package/claude-plugin.json +14 -14
  5. package/docs/CHEATSHEET.md +203 -203
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/agents.json +224 -140
  8. package/framework/hooks/README.md +202 -202
  9. package/framework/hooks/claude-code/post-tool-use/dispatch.js +48 -2
  10. package/framework/hooks/claude-code/post-tool-use/validator-feedback.js +151 -0
  11. package/framework/hooks/claude-code/pre-tool-use/enforce-phase-writes.js +12 -0
  12. package/framework/hooks/claude-code/pre-tool-use/protect-spec-files.js +6 -0
  13. package/framework/hooks/claude-code/session-start/inject-morph-context.js +34 -0
  14. package/framework/hooks/claude-code/statusline.py +6 -0
  15. package/framework/hooks/claude-code/stop/validate-completion.js +38 -4
  16. package/framework/hooks/claude-code/teammate-idle/teammate-idle.js +87 -0
  17. package/framework/hooks/claude-code/user-prompt/set-terminal-title.js +58 -0
  18. package/framework/hooks/shared/phase-utils.js +4 -1
  19. package/framework/hooks/shared/state-reader.js +1 -0
  20. package/framework/skills/README.md +1 -0
  21. package/framework/skills/level-0-meta/brainstorming/SKILL.md +2 -0
  22. package/framework/skills/level-0-meta/code-review/SKILL.md +16 -0
  23. package/framework/skills/level-0-meta/code-review/references/review-guidelines.md +100 -0
  24. package/framework/skills/level-0-meta/code-review/scripts/scan-csharp.mjs +36 -6
  25. package/framework/skills/level-0-meta/code-review-nextjs/SKILL.md +16 -0
  26. package/framework/skills/level-0-meta/code-review-nextjs/scripts/scan-nextjs.mjs +189 -0
  27. package/framework/skills/level-0-meta/frontend-review/SKILL.md +359 -0
  28. package/framework/skills/level-0-meta/frontend-review/scripts/scan-accessibility.mjs +376 -0
  29. package/framework/skills/level-0-meta/morph-checklist/SKILL.md +1 -1
  30. package/framework/skills/level-0-meta/morph-replicate/SKILL.md +10 -8
  31. package/framework/skills/level-0-meta/morph-replicate/references/blazor-html-mapping.md +70 -0
  32. package/framework/skills/level-0-meta/post-implementation/SKILL.md +315 -0
  33. package/framework/skills/level-0-meta/post-implementation/scripts/detect-dev-server.mjs +153 -0
  34. package/framework/skills/level-0-meta/post-implementation/scripts/detect-stack.mjs +234 -0
  35. package/framework/skills/level-0-meta/terminal-title/SKILL.md +61 -0
  36. package/framework/skills/level-0-meta/terminal-title/scripts/set_title.sh +65 -0
  37. package/framework/skills/level-0-meta/tool-usage-guide/SKILL.md +50 -188
  38. package/framework/skills/level-0-meta/tool-usage-guide/references/tools-per-phase.md +213 -0
  39. package/framework/skills/level-0-meta/verification-before-completion/SKILL.md +2 -0
  40. package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +4 -7
  41. package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +1 -1
  42. package/framework/skills/level-1-workflows/phase-design/SKILL.md +71 -109
  43. package/framework/skills/level-1-workflows/phase-design/references/architecture-analysis-guide.md +89 -0
  44. package/framework/skills/level-1-workflows/phase-design/references/spec-authoring-guide.md +55 -0
  45. package/framework/skills/level-1-workflows/phase-implement/SKILL.md +171 -114
  46. package/framework/skills/level-1-workflows/phase-implement/references/vsa-implementation-guide.md +92 -0
  47. package/framework/skills/level-1-workflows/phase-setup/SKILL.md +1 -2
  48. package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +35 -159
  49. package/framework/skills/level-1-workflows/phase-tasks/references/task-planning-patterns.md +172 -0
  50. package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +42 -3
  51. package/framework/squad-templates/backend-only.json +14 -1
  52. package/framework/squad-templates/frontend-only.json +14 -1
  53. package/framework/squad-templates/full-stack.json +25 -8
  54. package/framework/standards/STANDARDS.json +631 -86
  55. package/framework/standards/frontend/design-system/aesthetic-direction.md +213 -0
  56. package/framework/templates/project/validate.js +122 -0
  57. package/framework/workflows/configs/zero-touch.json +7 -0
  58. package/package.json +87 -87
  59. package/src/commands/agents/dispatch-agents.js +53 -10
  60. package/src/commands/state/advance-phase.js +88 -13
  61. package/src/commands/state/index.js +2 -1
  62. package/src/commands/state/phase-runner.js +215 -0
  63. package/src/commands/tasks/task.js +25 -4
  64. package/src/core/paths/output-schema.js +2 -1
  65. package/src/lib/detectors/design-system-detector.js +5 -4
  66. package/src/lib/generators/recap-generator.js +16 -0
  67. package/src/lib/orchestration/team-orchestrator.js +171 -89
  68. package/src/lib/phase-chain/eligibility-checker.js +243 -0
  69. package/src/lib/standards/digest-builder.js +231 -0
  70. package/src/lib/tasks/task-parser.js +94 -0
  71. package/src/lib/validators/blazor/blazor-concurrency-analyzer.js +39 -0
  72. package/src/lib/validators/content/content-validator.js +34 -106
  73. package/src/lib/validators/nextjs/next-component-validator.js +2 -0
  74. package/src/lib/validators/validation-runner.js +2 -2
  75. package/src/utils/file-copier.js +1 -0
  76. package/src/utils/hooks-installer.js +31 -7
@@ -0,0 +1,376 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * scan-accessibility.mjs
4
+ *
5
+ * Scans .tsx/.ts (Next.js) and .razor (Blazor) files for static accessibility violations.
6
+ * Executed locally by Claude via Bash — output returned to Claude.
7
+ *
8
+ * Usage:
9
+ * node scan-accessibility.mjs [path] # scan directory or file (default: '.')
10
+ * node scan-accessibility.mjs src/ # scan src/ recursively
11
+ * node scan-accessibility.mjs --summary # counts only, no details
12
+ *
13
+ * Checks (7 total):
14
+ * IMG_MISSING_ALT CRITICAL .tsx/.razor <img without alt= attribute
15
+ * INPUT_MISSING_LABEL HIGH .tsx/.razor <input without associated label or aria-label
16
+ * <InputText/<FluentTextField without Label=
17
+ * HEADING_HIERARCHY HIGH .tsx/.razor Heading level skip (H1→H3 skipping H2)
18
+ * LINK_NO_TEXT HIGH .tsx/.razor <a> empty or icon-only without aria-label
19
+ * BUTTON_NO_TEXT HIGH .tsx/.razor <button>/<FluentButton> without accessible text
20
+ * AUTOPLAY_MEDIA MEDIUM .tsx/.razor <video>/<audio> with autoPlay without muted
21
+ * FLUENT_CHECKBOX_NO_LABEL MEDIUM .razor <FluentCheckbox without Label=
22
+ *
23
+ * Output JSON: { summary: { scanned, findings, bySeverity, clean }, findings: [...] }
24
+ * Exit code: 1 if CRITICAL findings, 0 otherwise
25
+ */
26
+
27
+ import { readdirSync, readFileSync, statSync, existsSync } from 'fs';
28
+ import { join, extname, relative } from 'path';
29
+
30
+ const targetPath = process.argv[2] ?? '.';
31
+ const summaryOnly = process.argv.includes('--summary');
32
+
33
+ // ─── File collectors ────────────────────────────────────────────────────────
34
+
35
+ const IGNORED_DIRS = ['node_modules', '.git', '.next', 'dist', 'build', '.turbo', 'bin', 'obj'];
36
+
37
+ function collectFrontendFiles(dir) {
38
+ if (!existsSync(dir)) return [];
39
+ const stat = statSync(dir);
40
+ if (stat.isFile()) {
41
+ const ext = extname(dir);
42
+ return ['.tsx', '.ts', '.razor'].includes(ext) ? [dir] : [];
43
+ }
44
+ const results = [];
45
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
46
+ if (entry.isDirectory() && !IGNORED_DIRS.includes(entry.name)) {
47
+ results.push(...collectFrontendFiles(join(dir, entry.name)));
48
+ } else if (entry.isFile()) {
49
+ const ext = extname(entry.name);
50
+ if (['.tsx', '.ts', '.razor'].includes(ext)) {
51
+ results.push(join(dir, entry.name));
52
+ }
53
+ }
54
+ }
55
+ return results;
56
+ }
57
+
58
+ // ─── Checks ─────────────────────────────────────────────────────────────────
59
+
60
+ /**
61
+ * Check: IMG_MISSING_ALT — CRITICAL
62
+ * Detect <img without alt= in .tsx and .razor files.
63
+ * Matches both JSX <img ... /> and HTML <img ...> forms.
64
+ */
65
+ function checkImgMissingAlt(content, lines, relFile, findings) {
66
+ // Match <img tags that do NOT contain alt=
67
+ const imgRegex = /<img\b([^>]*?)(?:\/>|>)/g;
68
+ let match;
69
+ while ((match = imgRegex.exec(content)) !== null) {
70
+ const attrs = match[1];
71
+ if (!/\balt\s*=/i.test(attrs)) {
72
+ const lineNum = content.slice(0, match.index).split('\n').length;
73
+ findings.push({
74
+ severity: 'CRITICAL',
75
+ id: 'IMG_MISSING_ALT',
76
+ file: relFile,
77
+ line: lineNum,
78
+ message: '<img> element missing alt attribute — required for screen readers',
79
+ snippet: lines[lineNum - 1]?.trim().slice(0, 100),
80
+ });
81
+ }
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Check: INPUT_MISSING_LABEL — HIGH
87
+ * Detect <input>, <InputText>, <FluentTextField without accessible label.
88
+ * Accepts: aria-label=, aria-labelledby=, or associated <label for=
89
+ * For Blazor: Label= attribute on <InputText> and <FluentTextField>
90
+ */
91
+ function checkInputMissingLabel(content, lines, relFile, findings) {
92
+ const isRazor = relFile.endsWith('.razor');
93
+
94
+ // --- Standard <input> (both tsx and razor HTML sections) ---
95
+ const inputRegex = /<input\b([^>]*?)(?:\/>|>)/g;
96
+ let match;
97
+ while ((match = inputRegex.exec(content)) !== null) {
98
+ const attrs = match[1];
99
+ // Skip hidden inputs — they don't need labels
100
+ if (/type\s*=\s*['"]hidden['"]/i.test(attrs)) continue;
101
+ const hasLabel =
102
+ /\baria-label\s*=/i.test(attrs) ||
103
+ /\baria-labelledby\s*=/i.test(attrs) ||
104
+ /\bid\s*=\s*['"]([^'"]+)['"]/i.test(attrs); // id present → could be associated by <label for>
105
+ if (!hasLabel) {
106
+ const lineNum = content.slice(0, match.index).split('\n').length;
107
+ findings.push({
108
+ severity: 'HIGH',
109
+ id: 'INPUT_MISSING_LABEL',
110
+ file: relFile,
111
+ line: lineNum,
112
+ message: '<input> element missing accessible label (aria-label, aria-labelledby, or <label for=)',
113
+ snippet: lines[lineNum - 1]?.trim().slice(0, 100),
114
+ });
115
+ }
116
+ }
117
+
118
+ if (isRazor) {
119
+ // --- Blazor <InputText> without Label= ---
120
+ const inputTextRegex = /<InputText\b([^>]*?)(?:\/>|>)/g;
121
+ while ((match = inputTextRegex.exec(content)) !== null) {
122
+ const attrs = match[1];
123
+ if (!/\bLabel\s*=/i.test(attrs) && !/\baria-label\s*=/i.test(attrs)) {
124
+ const lineNum = content.slice(0, match.index).split('\n').length;
125
+ findings.push({
126
+ severity: 'HIGH',
127
+ id: 'INPUT_MISSING_LABEL',
128
+ file: relFile,
129
+ line: lineNum,
130
+ message: '<InputText> component missing Label= attribute (or aria-label)',
131
+ snippet: lines[lineNum - 1]?.trim().slice(0, 100),
132
+ });
133
+ }
134
+ }
135
+
136
+ // --- Fluent UI <FluentTextField> without Label= ---
137
+ const fluentTextRegex = /<FluentTextField\b([^>]*?)(?:\/>|>)/g;
138
+ while ((match = fluentTextRegex.exec(content)) !== null) {
139
+ const attrs = match[1];
140
+ if (!/\bLabel\s*=/i.test(attrs) && !/\baria-label\s*=/i.test(attrs)) {
141
+ const lineNum = content.slice(0, match.index).split('\n').length;
142
+ findings.push({
143
+ severity: 'HIGH',
144
+ id: 'INPUT_MISSING_LABEL',
145
+ file: relFile,
146
+ line: lineNum,
147
+ message: '<FluentTextField> component missing Label= attribute (or aria-label)',
148
+ snippet: lines[lineNum - 1]?.trim().slice(0, 100),
149
+ });
150
+ }
151
+ }
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Check: HEADING_HIERARCHY — HIGH
157
+ * Detect heading level skips (e.g. H1 → H3 without H2) within a file.
158
+ * Scans for h1–h6 / H1–H6 tags in order and reports any skip > 1 level.
159
+ */
160
+ function checkHeadingHierarchy(content, lines, relFile, findings) {
161
+ // Collect all headings in document order
162
+ const headingRegex = /<[Hh]([1-6])\b/g;
163
+ const headings = [];
164
+ let match;
165
+ while ((match = headingRegex.exec(content)) !== null) {
166
+ headings.push({
167
+ level: parseInt(match[1], 10),
168
+ lineNum: content.slice(0, match.index).split('\n').length,
169
+ });
170
+ }
171
+
172
+ // Check for level skips
173
+ for (let i = 1; i < headings.length; i++) {
174
+ const prev = headings[i - 1];
175
+ const curr = headings[i];
176
+ // Only flag increases that skip a level (e.g. h1 → h3)
177
+ if (curr.level > prev.level + 1) {
178
+ findings.push({
179
+ severity: 'HIGH',
180
+ id: 'HEADING_HIERARCHY',
181
+ file: relFile,
182
+ line: curr.lineNum,
183
+ message: `Heading level skip: H${prev.level} (line ${prev.lineNum}) → H${curr.level} (line ${curr.lineNum}) — missing H${prev.level + 1}`,
184
+ snippet: lines[curr.lineNum - 1]?.trim().slice(0, 100),
185
+ });
186
+ }
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Check: LINK_NO_TEXT — HIGH
192
+ * Detect <a> elements that are empty or contain only an icon without aria-label.
193
+ * Empty: <a href="..."></a> or <a .../>
194
+ * Icon-only: <a href="..."><SomeIcon /></a> or <a href="..."><i class="..."></i></a>
195
+ */
196
+ function checkLinkNoText(content, lines, relFile, findings) {
197
+ // Match opening <a ...> tag and capture attributes, then check for aria-label
198
+ // We use a simplified approach: find <a> tags and look for aria-label in attrs
199
+ const linkRegex = /<a\b([^>]*?)>([\s\S]*?)<\/a>/g;
200
+ let match;
201
+ while ((match = linkRegex.exec(content)) !== null) {
202
+ const attrs = match[1];
203
+ const innerContent = match[2].trim();
204
+
205
+ // Has aria-label → OK
206
+ if (/\baria-label\s*=/i.test(attrs) || /\baria-labelledby\s*=/i.test(attrs)) continue;
207
+
208
+ // Check if inner content has visible text (not just whitespace, tags, or expressions)
209
+ // Strip JSX expressions {}, HTML tags, and whitespace
210
+ const textOnly = innerContent
211
+ .replace(/\{[^}]*\}/g, '') // strip {expressions}
212
+ .replace(/<[^>]+>/g, '') // strip HTML/JSX tags
213
+ .trim();
214
+
215
+ if (!textOnly) {
216
+ const lineNum = content.slice(0, match.index).split('\n').length;
217
+ findings.push({
218
+ severity: 'HIGH',
219
+ id: 'LINK_NO_TEXT',
220
+ file: relFile,
221
+ line: lineNum,
222
+ message: '<a> element has no accessible text — add aria-label or visible text content',
223
+ snippet: lines[lineNum - 1]?.trim().slice(0, 100),
224
+ });
225
+ }
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Check: BUTTON_NO_TEXT — HIGH
231
+ * Detect <button> and <FluentButton> elements without accessible text or aria-label.
232
+ */
233
+ function checkButtonNoText(content, lines, relFile, findings) {
234
+ // <button> elements
235
+ const buttonRegex = /<button\b([^>]*?)>([\s\S]*?)<\/button>/g;
236
+ let match;
237
+ while ((match = buttonRegex.exec(content)) !== null) {
238
+ const attrs = match[1];
239
+ const innerContent = match[2].trim();
240
+
241
+ if (/\baria-label\s*=/i.test(attrs) || /\baria-labelledby\s*=/i.test(attrs)) continue;
242
+
243
+ const textOnly = innerContent
244
+ .replace(/\{[^}]*\}/g, '')
245
+ .replace(/<[^>]+>/g, '')
246
+ .trim();
247
+
248
+ if (!textOnly) {
249
+ const lineNum = content.slice(0, match.index).split('\n').length;
250
+ findings.push({
251
+ severity: 'HIGH',
252
+ id: 'BUTTON_NO_TEXT',
253
+ file: relFile,
254
+ line: lineNum,
255
+ message: '<button> element has no accessible text — add aria-label or visible text content',
256
+ snippet: lines[lineNum - 1]?.trim().slice(0, 100),
257
+ });
258
+ }
259
+ }
260
+
261
+ // <FluentButton> (Blazor Fluent UI)
262
+ const fluentBtnRegex = /<FluentButton\b([^>]*?)>([\s\S]*?)<\/FluentButton>/g;
263
+ while ((match = fluentBtnRegex.exec(content)) !== null) {
264
+ const attrs = match[1];
265
+ const innerContent = match[2].trim();
266
+
267
+ if (/\baria-label\s*=/i.test(attrs) || /\bTitle\s*=/i.test(attrs)) continue;
268
+
269
+ const textOnly = innerContent
270
+ .replace(/\{[^}]*\}/g, '')
271
+ .replace(/<[^>]+>/g, '')
272
+ .trim();
273
+
274
+ if (!textOnly) {
275
+ const lineNum = content.slice(0, match.index).split('\n').length;
276
+ findings.push({
277
+ severity: 'HIGH',
278
+ id: 'BUTTON_NO_TEXT',
279
+ file: relFile,
280
+ line: lineNum,
281
+ message: '<FluentButton> has no accessible text — add aria-label, Title=, or visible text',
282
+ snippet: lines[lineNum - 1]?.trim().slice(0, 100),
283
+ });
284
+ }
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Check: AUTOPLAY_MEDIA — MEDIUM
290
+ * Detect <video> or <audio> with autoPlay attribute but without muted.
291
+ * Autoplay without muted violates WCAG 1.4.2 (Audio Control).
292
+ */
293
+ function checkAutoplayMedia(content, lines, relFile, findings) {
294
+ const mediaRegex = /<(video|audio)\b([^>]*?)(?:\/>|>)/gi;
295
+ let match;
296
+ while ((match = mediaRegex.exec(content)) !== null) {
297
+ const attrs = match[2];
298
+ // JSX: autoPlay (camelCase), HTML: autoplay
299
+ if (/\bautoPlay\b|\bautoplay\b/i.test(attrs) && !/\bmuted\b/i.test(attrs)) {
300
+ const lineNum = content.slice(0, match.index).split('\n').length;
301
+ findings.push({
302
+ severity: 'MEDIUM',
303
+ id: 'AUTOPLAY_MEDIA',
304
+ file: relFile,
305
+ line: lineNum,
306
+ message: `<${match[1]}> has autoPlay without muted — violates WCAG 1.4.2 (Audio Control)`,
307
+ snippet: lines[lineNum - 1]?.trim().slice(0, 100),
308
+ });
309
+ }
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Check: FLUENT_CHECKBOX_NO_LABEL — MEDIUM
315
+ * Detect <FluentCheckbox without Label= in .razor files.
316
+ */
317
+ function checkFluentCheckboxNoLabel(content, lines, relFile, findings) {
318
+ if (!relFile.endsWith('.razor')) return;
319
+
320
+ const checkboxRegex = /<FluentCheckbox\b([^>]*?)(?:\/>|>)/g;
321
+ let match;
322
+ while ((match = checkboxRegex.exec(content)) !== null) {
323
+ const attrs = match[1];
324
+ if (!/\bLabel\s*=/i.test(attrs) && !/\baria-label\s*=/i.test(attrs)) {
325
+ const lineNum = content.slice(0, match.index).split('\n').length;
326
+ findings.push({
327
+ severity: 'MEDIUM',
328
+ id: 'FLUENT_CHECKBOX_NO_LABEL',
329
+ file: relFile,
330
+ line: lineNum,
331
+ message: '<FluentCheckbox> missing Label= attribute — screen readers cannot identify this checkbox',
332
+ snippet: lines[lineNum - 1]?.trim().slice(0, 100),
333
+ });
334
+ }
335
+ }
336
+ }
337
+
338
+ // ─── Main ────────────────────────────────────────────────────────────────────
339
+
340
+ const files = collectFrontendFiles(targetPath);
341
+ const findings = [];
342
+
343
+ for (const file of files) {
344
+ const content = readFileSync(file, 'utf-8');
345
+ const lines = content.split('\n');
346
+ const relFile = relative(process.cwd(), file).replace(/\\/g, '/');
347
+
348
+ checkImgMissingAlt(content, lines, relFile, findings);
349
+ checkInputMissingLabel(content, lines, relFile, findings);
350
+ checkHeadingHierarchy(content, lines, relFile, findings);
351
+ checkLinkNoText(content, lines, relFile, findings);
352
+ checkButtonNoText(content, lines, relFile, findings);
353
+ checkAutoplayMedia(content, lines, relFile, findings);
354
+ checkFluentCheckboxNoLabel(content, lines, relFile, findings);
355
+ }
356
+
357
+ const bySeverity = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
358
+ for (const f of findings) {
359
+ bySeverity[f.severity] = (bySeverity[f.severity] ?? 0) + 1;
360
+ }
361
+
362
+ const summary = {
363
+ scanned: files.length,
364
+ findings: findings.length,
365
+ bySeverity,
366
+ clean: findings.length === 0,
367
+ };
368
+
369
+ if (summaryOnly) {
370
+ console.log(JSON.stringify(summary, null, 2));
371
+ } else {
372
+ console.log(JSON.stringify({ summary, findings }, null, 2));
373
+ }
374
+
375
+ // Exit 1 if any CRITICAL findings
376
+ process.exit(findings.some(f => f.severity === 'CRITICAL') ? 1 : 0);
@@ -7,7 +7,7 @@ allowed-tools: Read, Write, Edit, Bash, Glob, Grep
7
7
 
8
8
  # MORPH Checklists
9
9
 
10
- Types: `deploy`, `security`, `seo`, `performance`, `accessibility`, `legal-brazil`, `simulation` (see [simulation-checklist.md](simulation-checklist.md))
10
+ Types: `deploy`, `security`, `seo`, `performance`, `accessibility`, `legal-brazil`, `simulation` (ver skill: `simulation-checklist`)
11
11
 
12
12
  ---
13
13
 
@@ -1,11 +1,12 @@
1
1
  ---
2
2
  name: morph-replicate
3
3
  description: Simplified workflow for converting HTML prototypes into functional Blazor components, extracting design patterns and mapping HTML elements to Fluent UI/MudBlazor equivalents. Use when an approved HTML prototype exists and needs conversion to Blazor without a full MORPH-SPEC spec pipeline.
4
- user-invocable: false
4
+ user-invocable: true
5
+ argument-hint: "[feature-name] [prototype-path]"
5
6
  allowed-tools: Read, Write, Edit, Bash, Glob, Grep
6
7
  ---
7
8
 
8
- # Skill: /morph-replicate
9
+ # MORPH Replicate — HTML to Blazor Conversion
9
10
 
10
11
  > Workflow simplificado para replicar prototipos HTML em Blazor.
11
12
  > Use quando tiver um prototipo HTML pronto e precisar converter para codigo Blazor funcional.
@@ -44,12 +45,13 @@ allowed-tools: Read, Write, Edit, Bash, Glob, Grep
44
45
 
45
46
  3. **Gerar mapeamento HTML → Blazor:**
46
47
 
47
- | HTML Pattern | Blazor Equivalente |
48
- |--------------|-------------------|
49
- | `<button class="btn-primary">` | `<FluentButton Appearance="Appearance.Accent">` |
50
- | `<div class="card">` | `<FluentCard>` ou HTML customizado |
51
- | `<input type="text">` | `<FluentTextField>` |
52
- | `<div class="modal">` | Modal customizado (se design especifico) |
48
+ > Para tabela completa de mapeamentos, veja `references/blazor-html-mapping.md`
49
+
50
+ Documente os mapeamentos relevantes para este prototipo especifico:
51
+ ```markdown
52
+ ## Mapeamento HTML Blazor (este prototipo)
53
+ - <elemento HTML> <ComponenteBlazor>
54
+ ```
53
55
 
54
56
  4. **Gerar lista de classes CSS a criar:**
55
57
  ```markdown
@@ -0,0 +1,70 @@
1
+ # HTML → Blazor Component Mapping Reference
2
+
3
+ > Reference for converting HTML prototype elements to Fluent UI / MudBlazor equivalents.
4
+ > Used during FASE 1 of the `/morph-replicate` workflow.
5
+
6
+ ## Common Component Mappings
7
+
8
+ | HTML Pattern | Fluent UI Equivalent | MudBlazor Equivalent |
9
+ |---|---|---|
10
+ | `<button class="btn-primary">` | `<FluentButton Appearance="Appearance.Accent">` | `<MudButton Variant="Variant.Filled" Color="Color.Primary">` |
11
+ | `<button class="btn-secondary">` | `<FluentButton Appearance="Appearance.Outline">` | `<MudButton Variant="Variant.Outlined">` |
12
+ | `<div class="card">` | `<FluentCard>` | `<MudCard>` |
13
+ | `<input type="text">` | `<FluentTextField>` | `<MudTextField>` |
14
+ | `<input type="search">` | `<FluentSearch>` | `<MudTextField Adornment="Adornment.Start" AdornmentIcon="Icons.Material.Filled.Search">` |
15
+ | `<select>` | `<FluentSelect>` | `<MudSelect>` |
16
+ | `<input type="checkbox">` | `<FluentCheckbox>` | `<MudCheckBox>` |
17
+ | `<input type="radio">` | `<FluentRadio>` | `<MudRadio>` |
18
+ | `<div class="modal">` | `<FluentDialog>` | `<MudDialog>` |
19
+ | `<nav>` / `<ul class="nav">` | `<FluentNavMenu>` | `<MudNavMenu>` |
20
+ | `<table>` | `<FluentDataGrid>` | `<MudTable>` |
21
+ | `<div class="spinner">` | `<FluentProgressRing>` | `<MudProgressCircular>` |
22
+ | `<div class="progress-bar">` | `<FluentProgress>` | `<MudProgressLinear>` |
23
+ | `<span class="badge">` | `<FluentBadge>` | `<MudChip>` |
24
+ | `<div class="alert">` | `<FluentMessageBar>` | `<MudAlert>` |
25
+ | `<form>` | `<EditForm>` | `<EditForm>` |
26
+ | `<div class="tooltip">` | `<FluentTooltip>` | `<MudTooltip>` |
27
+ | `<img>` | `<FluentIcon>` (for icons) / `<img>` | `<MudImage>` / `<img>` |
28
+ | `<div class="tabs">` | `<FluentTabs>` | `<MudTabs>` |
29
+ | `<details>/<summary>` | `<FluentAccordion>` | `<MudExpansionPanel>` |
30
+
31
+ ## Layout Mappings
32
+
33
+ | HTML Layout Pattern | Blazor/CSS Approach |
34
+ |---|---|
35
+ | `display: grid` | Keep as CSS grid (no Fluent/MudBlazor equivalent) |
36
+ | `display: flex` | Keep as CSS flexbox |
37
+ | `class="container"` | Keep as CSS class in design-system.css |
38
+ | `class="row col-*"` | Keep as CSS or use CSS grid |
39
+ | `class="sidebar"` | `<FluentBodyContent>` or custom CSS layout |
40
+
41
+ ## Form Patterns
42
+
43
+ | HTML Form Pattern | Blazor Equivalent |
44
+ |---|---|
45
+ | `<input required>` | FluentValidation rule + `<FluentValidationMessage>` |
46
+ | `<input pattern="...">` | FluentValidation `Matches()` rule |
47
+ | `<input min/max>` | FluentValidation `InclusiveBetween()` rule |
48
+ | Form submission | `<EditForm OnValidSubmit="HandleSubmit">` |
49
+ | Error display | `<FluentValidationSummary>` or `<DataAnnotationsValidator>` |
50
+
51
+ ## Animation Mappings
52
+
53
+ | CSS Animation Class | Blazor Approach |
54
+ |---|---|
55
+ | `animate-fadeIn` | CSS class in design-system.css (keep as-is) |
56
+ | `animate-slideInUp` | CSS class in design-system.css (keep as-is) |
57
+ | `animate-pulse` | CSS class or `<FluentProgressRing>` |
58
+
59
+ > For animation standards, see: `framework/standards/frontend/design-system/animations.md`
60
+
61
+ ## Decision Guide
62
+
63
+ When a direct component mapping doesn't exist:
64
+ 1. **Check Fluent UI docs** — it may have a less obvious equivalent
65
+ 2. **Use HTML wrapper** — wrap plain HTML in a `<div>` with design-system CSS classes
66
+ 3. **Create a custom Razor component** — when the design is project-specific and reused across pages
67
+ 4. **Keep as HTML** — for one-off layout elements that don't need a component
68
+
69
+ > For full Fluent UI reference: `framework/standards/frontend/blazor/fluent-ui.md`
70
+ > For MudBlazor reference: `framework/standards/frontend/blazor/html-conversion.md`