@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.
- package/README.md +379 -379
- package/bin/morph-spec.js +23 -2
- package/bin/{task-manager.cjs → task-manager.js} +249 -172
- package/claude-plugin.json +14 -14
- package/docs/CHEATSHEET.md +203 -203
- package/docs/QUICKSTART.md +1 -1
- package/framework/agents.json +224 -140
- package/framework/hooks/README.md +202 -202
- package/framework/hooks/claude-code/post-tool-use/dispatch.js +48 -2
- package/framework/hooks/claude-code/post-tool-use/validator-feedback.js +151 -0
- package/framework/hooks/claude-code/pre-tool-use/enforce-phase-writes.js +12 -0
- package/framework/hooks/claude-code/pre-tool-use/protect-spec-files.js +6 -0
- package/framework/hooks/claude-code/session-start/inject-morph-context.js +34 -0
- package/framework/hooks/claude-code/statusline.py +6 -0
- package/framework/hooks/claude-code/stop/validate-completion.js +38 -4
- package/framework/hooks/claude-code/teammate-idle/teammate-idle.js +87 -0
- package/framework/hooks/claude-code/user-prompt/set-terminal-title.js +58 -0
- package/framework/hooks/shared/phase-utils.js +4 -1
- package/framework/hooks/shared/state-reader.js +1 -0
- package/framework/skills/README.md +1 -0
- package/framework/skills/level-0-meta/brainstorming/SKILL.md +2 -0
- package/framework/skills/level-0-meta/code-review/SKILL.md +16 -0
- package/framework/skills/level-0-meta/code-review/references/review-guidelines.md +100 -0
- package/framework/skills/level-0-meta/code-review/scripts/scan-csharp.mjs +36 -6
- package/framework/skills/level-0-meta/code-review-nextjs/SKILL.md +16 -0
- package/framework/skills/level-0-meta/code-review-nextjs/scripts/scan-nextjs.mjs +189 -0
- package/framework/skills/level-0-meta/frontend-review/SKILL.md +359 -0
- package/framework/skills/level-0-meta/frontend-review/scripts/scan-accessibility.mjs +376 -0
- package/framework/skills/level-0-meta/morph-checklist/SKILL.md +1 -1
- package/framework/skills/level-0-meta/morph-replicate/SKILL.md +10 -8
- package/framework/skills/level-0-meta/morph-replicate/references/blazor-html-mapping.md +70 -0
- package/framework/skills/level-0-meta/post-implementation/SKILL.md +315 -0
- package/framework/skills/level-0-meta/post-implementation/scripts/detect-dev-server.mjs +153 -0
- package/framework/skills/level-0-meta/post-implementation/scripts/detect-stack.mjs +234 -0
- package/framework/skills/level-0-meta/terminal-title/SKILL.md +61 -0
- package/framework/skills/level-0-meta/terminal-title/scripts/set_title.sh +65 -0
- package/framework/skills/level-0-meta/tool-usage-guide/SKILL.md +50 -188
- package/framework/skills/level-0-meta/tool-usage-guide/references/tools-per-phase.md +213 -0
- package/framework/skills/level-0-meta/verification-before-completion/SKILL.md +2 -0
- package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +4 -7
- package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/phase-design/SKILL.md +71 -109
- package/framework/skills/level-1-workflows/phase-design/references/architecture-analysis-guide.md +89 -0
- package/framework/skills/level-1-workflows/phase-design/references/spec-authoring-guide.md +55 -0
- package/framework/skills/level-1-workflows/phase-implement/SKILL.md +171 -114
- package/framework/skills/level-1-workflows/phase-implement/references/vsa-implementation-guide.md +92 -0
- package/framework/skills/level-1-workflows/phase-setup/SKILL.md +1 -2
- package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +35 -159
- package/framework/skills/level-1-workflows/phase-tasks/references/task-planning-patterns.md +172 -0
- package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +42 -3
- package/framework/squad-templates/backend-only.json +14 -1
- package/framework/squad-templates/frontend-only.json +14 -1
- package/framework/squad-templates/full-stack.json +25 -8
- package/framework/standards/STANDARDS.json +631 -86
- package/framework/standards/frontend/design-system/aesthetic-direction.md +213 -0
- package/framework/templates/project/validate.js +122 -0
- package/framework/workflows/configs/zero-touch.json +7 -0
- package/package.json +87 -87
- package/src/commands/agents/dispatch-agents.js +53 -10
- package/src/commands/state/advance-phase.js +88 -13
- package/src/commands/state/index.js +2 -1
- package/src/commands/state/phase-runner.js +215 -0
- package/src/commands/tasks/task.js +25 -4
- package/src/core/paths/output-schema.js +2 -1
- package/src/lib/detectors/design-system-detector.js +5 -4
- package/src/lib/generators/recap-generator.js +16 -0
- package/src/lib/orchestration/team-orchestrator.js +171 -89
- package/src/lib/phase-chain/eligibility-checker.js +243 -0
- package/src/lib/standards/digest-builder.js +231 -0
- package/src/lib/tasks/task-parser.js +94 -0
- package/src/lib/validators/blazor/blazor-concurrency-analyzer.js +39 -0
- package/src/lib/validators/content/content-validator.js +34 -106
- package/src/lib/validators/nextjs/next-component-validator.js +2 -0
- package/src/lib/validators/validation-runner.js +2 -2
- package/src/utils/file-copier.js +1 -0
- 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` (
|
|
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:
|
|
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
|
-
#
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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`
|