@bovinphang/frontend-craft 2.5.0 → 2.7.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.
Files changed (156) hide show
  1. package/.claude-plugin/marketplace.json +7 -3
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +77 -1
  4. package/CHANGELOG.zh-CN.md +77 -1
  5. package/CONTRIBUTING.md +15 -10
  6. package/CONTRIBUTING.zh-CN.md +15 -10
  7. package/README.md +69 -43
  8. package/README.openclaw.md +11 -9
  9. package/README.openclaw.zh-CN.md +11 -9
  10. package/README.zh-CN.md +69 -43
  11. package/agents/fec-typescript-reviewer.md +1 -1
  12. package/commands/fec-doc-sync.md +7 -6
  13. package/dist/bin/frontend-craft.js +30 -27
  14. package/docs/example-prompts.md +82 -69
  15. package/docs/ja-JP/README.md +110 -84
  16. package/docs/ko-KR/README.md +100 -74
  17. package/docs/project-structure.md +8 -4
  18. package/docs/runtimes/antigravity.md +1 -1
  19. package/docs/runtimes/augment.md +1 -1
  20. package/docs/runtimes/claude.md +16 -10
  21. package/docs/runtimes/claude.zh-CN.md +16 -10
  22. package/docs/runtimes/cline.md +1 -1
  23. package/docs/runtimes/codebuddy.md +1 -1
  24. package/docs/runtimes/codex.md +1 -1
  25. package/docs/runtimes/copilot.md +1 -1
  26. package/docs/runtimes/cursor.md +1 -1
  27. package/docs/runtimes/gemini.md +1 -1
  28. package/docs/runtimes/kilo.md +1 -1
  29. package/docs/runtimes/openclaw.md +1 -1
  30. package/docs/runtimes/opencode.md +1 -1
  31. package/docs/runtimes/qoder.md +1 -1
  32. package/docs/runtimes/trae.md +1 -1
  33. package/docs/runtimes/windsurf.md +1 -1
  34. package/docs/zh-CN/example-prompts.md +82 -69
  35. package/docs/zh-CN/project-structure.md +8 -4
  36. package/docs/zh-TW/README.md +88 -62
  37. package/openclaw.plugin.json +1 -1
  38. package/package.json +3 -3
  39. package/skills/eval_queries.json +179 -56
  40. package/skills/fec-accessibility-check/SKILL.md +1 -1
  41. package/skills/fec-alchemy/SKILL.md +74 -0
  42. package/skills/fec-alchemy/assets/absorption-plan-template.md +47 -0
  43. package/skills/fec-alchemy/assets/intake-template.md +55 -0
  44. package/skills/fec-alchemy/assets/review-checklist.md +31 -0
  45. package/skills/fec-alchemy/references/methodology.md +94 -0
  46. package/skills/fec-alchemy/references/originality-and-licensing.md +57 -0
  47. package/skills/fec-backend-requirements-handoff/SKILL.md +45 -0
  48. package/skills/fec-browser-storage/SKILL.md +1 -1
  49. package/skills/fec-canvas-threejs/SKILL.md +1 -1
  50. package/skills/fec-code-review/SKILL.md +11 -3
  51. package/skills/fec-data-fetching/SKILL.md +15 -15
  52. package/skills/fec-data-fetching/references/query-patterns.md +27 -1
  53. package/skills/fec-debug-framework/SKILL.md +14 -19
  54. package/skills/fec-debug-framework/references/report-template.md +58 -0
  55. package/skills/fec-doc-sync/SKILL.md +19 -17
  56. package/skills/fec-drawio-studio/SKILL.md +69 -0
  57. package/skills/fec-drawio-studio/data/THIRD_PARTY_NOTICES.md +10 -0
  58. package/skills/fec-drawio-studio/data/brand-icons.json +878 -0
  59. package/skills/fec-drawio-studio/data/shape-index.json.gz +0 -0
  60. package/skills/fec-drawio-studio/references/data-residency.md +27 -0
  61. package/skills/fec-drawio-studio/references/diagram-patterns.md +41 -0
  62. package/skills/fec-drawio-studio/references/layout-and-export.md +57 -0
  63. package/skills/fec-drawio-studio/references/troubleshooting.md +29 -0
  64. package/skills/fec-drawio-studio/references/xml-and-mermaid.md +54 -0
  65. package/skills/fec-drawio-studio/scripts/brand-symbols.mjs +99 -0
  66. package/skills/fec-drawio-studio/scripts/diagram-lint.mjs +143 -0
  67. package/skills/fec-drawio-studio/scripts/diagram-url.mjs +102 -0
  68. package/skills/fec-drawio-studio/scripts/layout-graph.mjs +107 -0
  69. package/skills/fec-drawio-studio/scripts/png-embed-fix.mjs +26 -0
  70. package/skills/fec-drawio-studio/scripts/scan-go-packages.mjs +36 -0
  71. package/skills/fec-drawio-studio/scripts/scan-js-modules.mjs +52 -0
  72. package/skills/fec-drawio-studio/scripts/scan-python-classes.mjs +30 -0
  73. package/skills/fec-drawio-studio/scripts/scan-python-modules.mjs +55 -0
  74. package/skills/fec-drawio-studio/scripts/scan-rust-modules.mjs +35 -0
  75. package/skills/fec-drawio-studio/scripts/scan-ts-modules.mjs +7 -0
  76. package/skills/fec-drawio-studio/scripts/shape-query.mjs +81 -0
  77. package/skills/fec-drawio-studio/scripts/studio-core.mjs +139 -0
  78. package/skills/fec-drawio-studio/styles/built-in/corporate.json +49 -0
  79. package/skills/fec-drawio-studio/styles/built-in/default.json +49 -0
  80. package/skills/fec-drawio-studio/styles/built-in/handdrawn.json +49 -0
  81. package/skills/fec-drawio-studio/styles/schema.json +112 -0
  82. package/skills/fec-e2e-testing/SKILL.md +5 -3
  83. package/skills/fec-e2e-testing/references/e2e-ci-reporting.md +62 -0
  84. package/skills/fec-e2e-testing/references/e2e-special-scenarios.md +52 -0
  85. package/skills/fec-e2e-testing/references/e2e-visual-regression.md +60 -0
  86. package/skills/fec-form-handling/SKILL.md +11 -11
  87. package/skills/fec-form-handling/references/advanced-form-patterns.md +34 -5
  88. package/skills/fec-image-generation/SKILL.md +56 -0
  89. package/skills/fec-image-generation/references/artifact-routing.md +80 -0
  90. package/skills/fec-image-generation/references/diagram-workflows.md +43 -0
  91. package/skills/fec-image-generation/references/png-qa-autofix.md +30 -0
  92. package/skills/fec-image-generation/scripts/png-qa.mjs +431 -0
  93. package/skills/fec-implement-from-design/SKILL.md +9 -2
  94. package/skills/fec-legacy-to-modern-migration/SKILL.md +29 -163
  95. package/skills/fec-legacy-to-modern-migration/references/migration-execution-checklist.md +110 -0
  96. package/skills/fec-legacy-to-modern-migration/references/migration-strategy-and-mapping.md +48 -0
  97. package/skills/fec-legacy-web-standard/SKILL.md +1 -1
  98. package/skills/fec-list-virtualization/SKILL.md +3 -3
  99. package/skills/fec-list-virtualization/references/virtualization-patterns.md +32 -4
  100. package/skills/fec-motion-interaction/SKILL.md +7 -0
  101. package/skills/fec-nextjs-project-standard/SKILL.md +7 -1
  102. package/skills/fec-performance-optimization/SKILL.md +9 -1
  103. package/skills/fec-performance-optimization/references/framework-performance-patterns.md +38 -0
  104. package/skills/fec-performance-optimization/references/report-template.md +59 -0
  105. package/skills/fec-pwa-implementation/SKILL.md +3 -4
  106. package/skills/fec-react-project-standard/SKILL.md +12 -4
  107. package/skills/fec-react-project-standard/references/react-performance-patterns.md +35 -0
  108. package/skills/fec-react-project-standard/references/react-project-structure.md +197 -0
  109. package/skills/fec-react-project-standard/references/react-quality-patterns.md +131 -0
  110. package/skills/fec-react-project-standard/references/react-runtime-patterns.md +293 -0
  111. package/skills/fec-refactor-clean/SKILL.md +4 -1
  112. package/skills/fec-refactor-clean/references/report-template.md +42 -0
  113. package/skills/fec-responsive-layout/references/responsive-layout-patterns.md +11 -0
  114. package/skills/fec-security-review/SKILL.md +3 -3
  115. package/skills/fec-source-driven-development/SKILL.md +26 -0
  116. package/skills/fec-state-management/SKILL.md +1 -1
  117. package/skills/fec-storybook-component-doc/SKILL.md +2 -2
  118. package/skills/fec-tailwind-design-system/references/tailwind-system-patterns.md +5 -1
  119. package/skills/fec-typescript-project-standard/SKILL.md +54 -0
  120. package/skills/{fec-typescript-type-safety → fec-typescript-project-standard}/references/type-safety-patterns.md +1 -1
  121. package/skills/{fec-typescript-type-safety/SKILL.md → fec-typescript-project-standard/references/type-safety.md} +1 -10
  122. package/skills/fec-ui-design/SKILL.md +26 -8
  123. package/skills/fec-ui-design/data/style-archetypes.json +40 -0
  124. package/skills/fec-ui-design/data/ux-quality-rules.json +35 -0
  125. package/skills/fec-ui-design/references/design-intelligence.md +66 -6
  126. package/skills/fec-ui-design/references/pre-delivery-checklist.md +6 -0
  127. package/skills/fec-ui-design/scripts/design-system.mjs +119 -1
  128. package/skills/fec-validation-fix/SKILL.md +5 -2
  129. package/skills/fec-validation-fix/references/report-template.md +43 -0
  130. package/skills/fec-vue3-project-standard/SKILL.md +12 -3
  131. package/skills/fec-vue3-project-standard/references/vue3-composables-routing.md +163 -0
  132. package/skills/fec-vue3-project-standard/references/vue3-performance-patterns.md +13 -0
  133. package/skills/fec-vue3-project-standard/references/vue3-project-structure.md +177 -0
  134. package/skills/fec-vue3-project-standard/references/vue3-state-api-quality.md +216 -0
  135. package/skills/fec-web-video-presentation/SKILL.md +61 -0
  136. package/skills/fec-web-video-presentation/data/starter-themes.json +59 -0
  137. package/skills/fec-web-video-presentation/references/chapter-craft.md +37 -0
  138. package/skills/fec-web-video-presentation/references/content-plan.md +38 -0
  139. package/skills/fec-web-video-presentation/references/recording-and-audio.md +33 -0
  140. package/skills/fec-web-video-presentation/references/theme-system.md +36 -0
  141. package/skills/fec-web-video-presentation/scripts/create-video-presentation.mjs +430 -0
  142. package/skills/fec-web-workers/SKILL.md +3 -4
  143. package/skills/metadata.json +381 -168
  144. package/skills/relations.json +136 -25
  145. package/templates/shared/rules/fec-ci-cd.md +20 -108
  146. package/templates/shared/rules/fec-design-system.md +47 -223
  147. package/templates/shared/rules/fec-error-handling.md +35 -179
  148. package/templates/shared/rules/fec-i18n.md +43 -91
  149. package/templates/shared/rules/fec-refactoring.md +4 -0
  150. package/templates/shared/rules/fec-rendering-patterns.md +28 -171
  151. package/templates/shared/rules/fec-responsive-design.md +34 -211
  152. package/templates/shared/rules/fec-state-management.md +41 -144
  153. package/templates/shared/rules/fec-typescript.md +47 -290
  154. package/skills/fec-e2e-testing/references/e2e-advanced.md +0 -176
  155. package/skills/fec-react-project-standard/references/react-project-details.md +0 -642
  156. package/skills/fec-vue3-project-standard/references/vue3-project-details.md +0 -547
@@ -0,0 +1,431 @@
1
+ #!/usr/bin/env node
2
+ // @ts-nocheck
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import zlib from "node:zlib";
6
+
7
+ const args = parseArgs(process.argv.slice(2));
8
+
9
+ if (!args.png || args.help) {
10
+ printHelp();
11
+ process.exit(args.help ? 0 : 1);
12
+ }
13
+
14
+ try {
15
+ const image = readPng(args.png);
16
+ const manifest = args.manifest ? readManifest(args.manifest) : undefined;
17
+ const report = analyze(image, manifest, Boolean(args.strict));
18
+ if ((args.format ?? "markdown") === "json") {
19
+ console.log(JSON.stringify(report, null, 2));
20
+ } else {
21
+ console.log(renderMarkdown(report));
22
+ }
23
+ process.exit(report.ok ? 0 : 2);
24
+ } catch (error) {
25
+ console.error(error instanceof Error ? error.message : String(error));
26
+ process.exit(1);
27
+ }
28
+
29
+ function parseArgs(values) {
30
+ const parsed = { format: "markdown", strict: false, help: false };
31
+ for (let index = 0; index < values.length; index += 1) {
32
+ const value = values[index];
33
+ if (value === "--help" || value === "-h") parsed.help = true;
34
+ else if (value === "--strict") parsed.strict = true;
35
+ else if (value === "--png") parsed.png = values[++index];
36
+ else if (value === "--manifest") parsed.manifest = values[++index];
37
+ else if (value === "--format") parsed.format = values[++index];
38
+ else throw new Error(`Unknown argument: ${value}`);
39
+ }
40
+ if (parsed.format !== "json" && parsed.format !== "markdown") {
41
+ throw new Error("--format must be json or markdown");
42
+ }
43
+ return parsed;
44
+ }
45
+
46
+ function printHelp() {
47
+ console.log(`Usage: node skills/fec-image-generation/scripts/png-qa.mjs --png <file> [--manifest <layout.json>] [--format json|markdown] [--strict]`);
48
+ }
49
+
50
+ function readManifest(filePath) {
51
+ const manifest = JSON.parse(fs.readFileSync(filePath, "utf8"));
52
+ return {
53
+ canvas: manifest.canvas,
54
+ boxes: Array.isArray(manifest.boxes) ? manifest.boxes : [],
55
+ connectors: Array.isArray(manifest.connectors) ? manifest.connectors : [],
56
+ };
57
+ }
58
+
59
+ function readPng(filePath) {
60
+ const buffer = fs.readFileSync(filePath);
61
+ const signature = "89504e470d0a1a0a";
62
+ if (buffer.subarray(0, 8).toString("hex") !== signature) {
63
+ throw new Error(`Not a PNG file: ${filePath}`);
64
+ }
65
+
66
+ let offset = 8;
67
+ let ihdr;
68
+ const idat = [];
69
+ while (offset < buffer.length) {
70
+ const length = buffer.readUInt32BE(offset);
71
+ const type = buffer.subarray(offset + 4, offset + 8).toString("ascii");
72
+ const data = buffer.subarray(offset + 8, offset + 8 + length);
73
+ offset += 12 + length;
74
+ if (type === "IHDR") {
75
+ ihdr = {
76
+ width: data.readUInt32BE(0),
77
+ height: data.readUInt32BE(4),
78
+ bitDepth: data[8],
79
+ colorType: data[9],
80
+ compression: data[10],
81
+ filter: data[11],
82
+ interlace: data[12],
83
+ };
84
+ } else if (type === "IDAT") {
85
+ idat.push(data);
86
+ } else if (type === "IEND") {
87
+ break;
88
+ }
89
+ }
90
+
91
+ if (!ihdr) throw new Error("PNG is missing IHDR");
92
+ if (ihdr.bitDepth !== 8) throw new Error(`Unsupported PNG bit depth: ${ihdr.bitDepth}`);
93
+ if (ihdr.compression !== 0 || ihdr.filter !== 0 || ihdr.interlace !== 0) {
94
+ throw new Error("Unsupported PNG compression/filter/interlace settings");
95
+ }
96
+
97
+ const channels = channelsForColorType(ihdr.colorType);
98
+ const inflated = zlib.inflateSync(Buffer.concat(idat));
99
+ const pixels = unfilter(inflated, ihdr.width, ihdr.height, channels);
100
+ return {
101
+ path: path.resolve(filePath),
102
+ width: ihdr.width,
103
+ height: ihdr.height,
104
+ colorType: ihdr.colorType,
105
+ channels,
106
+ pixels,
107
+ };
108
+ }
109
+
110
+ function channelsForColorType(colorType) {
111
+ if (colorType === 0) return 1;
112
+ if (colorType === 2) return 3;
113
+ if (colorType === 4) return 2;
114
+ if (colorType === 6) return 4;
115
+ throw new Error(`Unsupported PNG color type: ${colorType}`);
116
+ }
117
+
118
+ function unfilter(data, width, height, channels) {
119
+ const stride = width * channels;
120
+ const output = Buffer.alloc(stride * height);
121
+ let sourceOffset = 0;
122
+
123
+ for (let y = 0; y < height; y += 1) {
124
+ const filterType = data[sourceOffset];
125
+ sourceOffset += 1;
126
+ const rowOffset = y * stride;
127
+ const previousRowOffset = (y - 1) * stride;
128
+
129
+ for (let x = 0; x < stride; x += 1) {
130
+ const raw = data[sourceOffset + x];
131
+ const left = x >= channels ? output[rowOffset + x - channels] : 0;
132
+ const up = y > 0 ? output[previousRowOffset + x] : 0;
133
+ const upLeft = y > 0 && x >= channels ? output[previousRowOffset + x - channels] : 0;
134
+ let value;
135
+ if (filterType === 0) value = raw;
136
+ else if (filterType === 1) value = raw + left;
137
+ else if (filterType === 2) value = raw + up;
138
+ else if (filterType === 3) value = raw + Math.floor((left + up) / 2);
139
+ else if (filterType === 4) value = raw + paeth(left, up, upLeft);
140
+ else throw new Error(`Unsupported PNG filter type: ${filterType}`);
141
+ output[rowOffset + x] = value & 0xff;
142
+ }
143
+ sourceOffset += stride;
144
+ }
145
+
146
+ return output;
147
+ }
148
+
149
+ function paeth(left, up, upLeft) {
150
+ const estimate = left + up - upLeft;
151
+ const leftDistance = Math.abs(estimate - left);
152
+ const upDistance = Math.abs(estimate - up);
153
+ const upLeftDistance = Math.abs(estimate - upLeft);
154
+ if (leftDistance <= upDistance && leftDistance <= upLeftDistance) return left;
155
+ if (upDistance <= upLeftDistance) return up;
156
+ return upLeft;
157
+ }
158
+
159
+ function analyze(image, manifest, strict) {
160
+ const issues = [];
161
+ const nextActions = new Set();
162
+ const ink = measureInk(image, strict ? 10 : 18);
163
+
164
+ if (image.width < 64 || image.height < 64) {
165
+ addIssue(issues, nextActions, "image-too-small", "PNG is too small for reliable visual QA.", "Export a larger PNG before review.");
166
+ }
167
+
168
+ if (ink.coverage < (strict ? 0.001 : 0.0002)) {
169
+ addIssue(issues, nextActions, "blank-image", "PNG appears blank or nearly blank.", "Check the renderer/export target and regenerate the image.");
170
+ }
171
+
172
+ if (ink.bounds) {
173
+ const pad = strict ? 6 : 3;
174
+ if (ink.bounds.minX <= pad || ink.bounds.minY <= pad || image.width - ink.bounds.maxX <= pad || image.height - ink.bounds.maxY <= pad) {
175
+ addIssue(issues, nextActions, "edge-clipping", "Visible content is very close to the PNG edge.", "Increase canvas/viewBox padding or reduce subject scale.");
176
+ }
177
+ }
178
+
179
+ if (manifest) analyzeManifest(image, manifest, issues, nextActions, strict);
180
+
181
+ return {
182
+ ok: issues.length === 0,
183
+ image: {
184
+ path: image.path,
185
+ width: image.width,
186
+ height: image.height,
187
+ colorType: image.colorType,
188
+ inkCoverage: Number(ink.coverage.toFixed(6)),
189
+ inkBounds: ink.bounds,
190
+ },
191
+ issues,
192
+ nextActions: [...nextActions],
193
+ };
194
+ }
195
+
196
+ function measureInk(image, threshold) {
197
+ const first = samplePixel(image, 0, 0);
198
+ let inkPixels = 0;
199
+ let minX = image.width;
200
+ let minY = image.height;
201
+ let maxX = -1;
202
+ let maxY = -1;
203
+
204
+ for (let y = 0; y < image.height; y += 1) {
205
+ for (let x = 0; x < image.width; x += 1) {
206
+ const pixel = samplePixel(image, x, y);
207
+ const differs = colorDistance(pixel, first) > threshold && pixel.a > 8;
208
+ if (differs) {
209
+ inkPixels += 1;
210
+ minX = Math.min(minX, x);
211
+ minY = Math.min(minY, y);
212
+ maxX = Math.max(maxX, x);
213
+ maxY = Math.max(maxY, y);
214
+ }
215
+ }
216
+ }
217
+
218
+ return {
219
+ coverage: inkPixels / (image.width * image.height),
220
+ bounds: inkPixels > 0 ? { minX, minY, maxX, maxY } : null,
221
+ };
222
+ }
223
+
224
+ function samplePixel(image, x, y) {
225
+ const offset = (y * image.width + x) * image.channels;
226
+ if (image.channels === 1) {
227
+ const value = image.pixels[offset];
228
+ return { r: value, g: value, b: value, a: 255 };
229
+ }
230
+ if (image.channels === 2) {
231
+ const value = image.pixels[offset];
232
+ return { r: value, g: value, b: value, a: image.pixels[offset + 1] };
233
+ }
234
+ return {
235
+ r: image.pixels[offset],
236
+ g: image.pixels[offset + 1],
237
+ b: image.pixels[offset + 2],
238
+ a: image.channels === 4 ? image.pixels[offset + 3] : 255,
239
+ };
240
+ }
241
+
242
+ function colorDistance(left, right) {
243
+ return Math.abs(left.r - right.r) + Math.abs(left.g - right.g) + Math.abs(left.b - right.b) + Math.abs(left.a - right.a);
244
+ }
245
+
246
+ function analyzeManifest(image, manifest, issues, nextActions, strict) {
247
+ if (manifest.canvas && (manifest.canvas.width !== image.width || manifest.canvas.height !== image.height)) {
248
+ addIssue(
249
+ issues,
250
+ nextActions,
251
+ "manifest-canvas-mismatch",
252
+ `Manifest canvas ${manifest.canvas.width}x${manifest.canvas.height} does not match PNG ${image.width}x${image.height}.`,
253
+ "Regenerate the manifest from the final export dimensions.",
254
+ );
255
+ }
256
+
257
+ const boxes = manifest.boxes.map(normalizeBox).filter(Boolean);
258
+ for (const box of boxes) {
259
+ if (box.x < 0 || box.y < 0 || box.x + box.width > image.width || box.y + box.height > image.height) {
260
+ addIssue(issues, nextActions, "box-out-of-bounds", `Box "${box.id}" is outside the canvas.`, "Move out-of-bounds nodes inside the canvas or expand the canvas.");
261
+ }
262
+ if (box.label && estimateLabelOverflow(box, strict)) {
263
+ addIssue(issues, nextActions, "label-overflow", `Box "${box.id}" label is likely to overflow.`, "Wrap or shorten labels, widen boxes, or increase export scale.");
264
+ }
265
+ }
266
+
267
+ for (let i = 0; i < boxes.length; i += 1) {
268
+ for (let j = i + 1; j < boxes.length; j += 1) {
269
+ const overlap = overlapArea(boxes[i], boxes[j]);
270
+ if (overlap > 0) {
271
+ addIssue(issues, nextActions, "box-overlap", `Boxes "${boxes[i].id}" and "${boxes[j].id}" overlap by ${Math.round(overlap)}px^2.`, "Increase node spacing or split dense groups.");
272
+ }
273
+ }
274
+ }
275
+
276
+ const boxById = new Map(boxes.map((box) => [box.id, box]));
277
+ const segments = [];
278
+ for (const connector of manifest.connectors) {
279
+ const points = normalizePoints(connector.points);
280
+ for (let index = 0; index < points.length - 1; index += 1) {
281
+ const segment = { connector, a: points[index], b: points[index + 1] };
282
+ segments.push(segment);
283
+ for (const box of boxes) {
284
+ if (box.id === connector.from || box.id === connector.to) continue;
285
+ if (segmentIntersectsBox(segment, box)) {
286
+ addIssue(issues, nextActions, "connector-through-label", `Connector "${connector.id ?? "unnamed"}" crosses box "${box.id}".`, "Add waypoints around labels or use orthogonal routing.");
287
+ }
288
+ }
289
+ }
290
+ if ((connector.from && !boxById.has(connector.from)) || (connector.to && !boxById.has(connector.to))) {
291
+ addIssue(issues, nextActions, "connector-endpoint-missing", `Connector "${connector.id ?? "unnamed"}" references a missing endpoint.`, "Update connector endpoints to match manifest box ids.");
292
+ }
293
+ }
294
+
295
+ for (let i = 0; i < segments.length; i += 1) {
296
+ for (let j = i + 1; j < segments.length; j += 1) {
297
+ if (segments[i].connector === segments[j].connector) continue;
298
+ if (segmentsStack(segments[i], segments[j], strict ? 4 : 2)) {
299
+ addIssue(issues, nextActions, "connector-stacking", "Two connector segments are stacked or nearly stacked.", "Offset parallel connectors or bundle them into a labeled shared route.");
300
+ }
301
+ }
302
+ }
303
+ }
304
+
305
+ function normalizeBox(box) {
306
+ if (!box || typeof box.id !== "string") return null;
307
+ const normalized = {
308
+ id: box.id,
309
+ x: Number(box.x),
310
+ y: Number(box.y),
311
+ width: Number(box.width),
312
+ height: Number(box.height),
313
+ label: typeof box.label === "string" ? box.label : "",
314
+ };
315
+ if ([normalized.x, normalized.y, normalized.width, normalized.height].some((value) => !Number.isFinite(value))) return null;
316
+ return normalized;
317
+ }
318
+
319
+ function normalizePoints(points) {
320
+ if (!Array.isArray(points)) return [];
321
+ return points
322
+ .map((point) => {
323
+ if (Array.isArray(point)) return { x: Number(point[0]), y: Number(point[1]) };
324
+ return { x: Number(point?.x), y: Number(point?.y) };
325
+ })
326
+ .filter((point) => Number.isFinite(point.x) && Number.isFinite(point.y));
327
+ }
328
+
329
+ function estimateLabelOverflow(box, strict) {
330
+ const label = box.label.trim();
331
+ if (!label) return false;
332
+ const lines = label.split(/\r?\n/);
333
+ const maxLine = Math.max(...lines.map((line) => visualLength(line)));
334
+ const estimatedWidth = maxLine * (strict ? 7.5 : 7);
335
+ const estimatedHeight = lines.length * (strict ? 18 : 16);
336
+ return estimatedWidth > Math.max(0, box.width - 12) || estimatedHeight > Math.max(0, box.height - 8);
337
+ }
338
+
339
+ function visualLength(value) {
340
+ return [...value].reduce((total, char) => total + (/[\u4e00-\u9fff]/.test(char) ? 2 : 1), 0);
341
+ }
342
+
343
+ function overlapArea(left, right) {
344
+ const width = Math.max(0, Math.min(left.x + left.width, right.x + right.width) - Math.max(left.x, right.x));
345
+ const height = Math.max(0, Math.min(left.y + left.height, right.y + right.height) - Math.max(left.y, right.y));
346
+ return width * height;
347
+ }
348
+
349
+ function segmentIntersectsBox(segment, box) {
350
+ if (pointInsideBox(segment.a, box) || pointInsideBox(segment.b, box)) return true;
351
+ const edges = [
352
+ [{ x: box.x, y: box.y }, { x: box.x + box.width, y: box.y }],
353
+ [{ x: box.x + box.width, y: box.y }, { x: box.x + box.width, y: box.y + box.height }],
354
+ [{ x: box.x + box.width, y: box.y + box.height }, { x: box.x, y: box.y + box.height }],
355
+ [{ x: box.x, y: box.y + box.height }, { x: box.x, y: box.y }],
356
+ ];
357
+ return edges.some(([a, b]) => segmentsIntersect(segment.a, segment.b, a, b));
358
+ }
359
+
360
+ function pointInsideBox(point, box) {
361
+ return point.x >= box.x && point.x <= box.x + box.width && point.y >= box.y && point.y <= box.y + box.height;
362
+ }
363
+
364
+ function segmentsIntersect(a, b, c, d) {
365
+ const o1 = orientation(a, b, c);
366
+ const o2 = orientation(a, b, d);
367
+ const o3 = orientation(c, d, a);
368
+ const o4 = orientation(c, d, b);
369
+ if (o1 !== o2 && o3 !== o4) return true;
370
+ return false;
371
+ }
372
+
373
+ function orientation(a, b, c) {
374
+ const value = (b.y - a.y) * (c.x - b.x) - (b.x - a.x) * (c.y - b.y);
375
+ if (Math.abs(value) < 0.0001) return 0;
376
+ return value > 0 ? 1 : 2;
377
+ }
378
+
379
+ function segmentsStack(left, right, tolerance) {
380
+ const leftHorizontal = Math.abs(left.a.y - left.b.y) <= tolerance;
381
+ const rightHorizontal = Math.abs(right.a.y - right.b.y) <= tolerance;
382
+ const leftVertical = Math.abs(left.a.x - left.b.x) <= tolerance;
383
+ const rightVertical = Math.abs(right.a.x - right.b.x) <= tolerance;
384
+
385
+ if (leftHorizontal && rightHorizontal && Math.abs(left.a.y - right.a.y) <= tolerance) {
386
+ return rangesOverlap([left.a.x, left.b.x], [right.a.x, right.b.x]) > tolerance;
387
+ }
388
+ if (leftVertical && rightVertical && Math.abs(left.a.x - right.a.x) <= tolerance) {
389
+ return rangesOverlap([left.a.y, left.b.y], [right.a.y, right.b.y]) > tolerance;
390
+ }
391
+ return false;
392
+ }
393
+
394
+ function rangesOverlap(left, right) {
395
+ const a = [Math.min(...left), Math.max(...left)];
396
+ const b = [Math.min(...right), Math.max(...right)];
397
+ return Math.max(0, Math.min(a[1], b[1]) - Math.max(a[0], b[0]));
398
+ }
399
+
400
+ function addIssue(issues, nextActions, code, message, action) {
401
+ issues.push({ code, message, action });
402
+ nextActions.add(action);
403
+ }
404
+
405
+ function renderMarkdown(report) {
406
+ const lines = [
407
+ "# PNG QA Report",
408
+ "",
409
+ `- Status: ${report.ok ? "ok" : "issues-found"}`,
410
+ `- Image: ${report.image.width}x${report.image.height}`,
411
+ `- Ink coverage: ${report.image.inkCoverage}`,
412
+ ];
413
+ if (report.image.inkBounds) {
414
+ lines.push(`- Ink bounds: ${JSON.stringify(report.image.inkBounds)}`);
415
+ }
416
+ lines.push("", "## Issues");
417
+ if (report.issues.length === 0) {
418
+ lines.push("- None");
419
+ } else {
420
+ for (const issue of report.issues) {
421
+ lines.push(`- ${issue.code}: ${issue.message}`);
422
+ }
423
+ }
424
+ lines.push("", "## Next Actions");
425
+ if (report.nextActions.length === 0) {
426
+ lines.push("- None");
427
+ } else {
428
+ for (const action of report.nextActions) lines.push(`- ${action}`);
429
+ }
430
+ return `${lines.join("\n")}\n`;
431
+ }
@@ -28,7 +28,7 @@ description: Use when implementing UI from Figma, Sketch, MasterGo, Pixso, Modao
28
28
  - 保持实现结果可维护、类型明确、可测试且具备可访问性
29
29
  - 避免引入重复的基础组件或并行设计系统
30
30
 
31
- ## 必需工作流
31
+ ## Procedure
32
32
 
33
33
  ### 1. 识别设计来源
34
34
 
@@ -41,6 +41,7 @@ description: Use when implementing UI from Figma, Sketch, MasterGo, Pixso, Modao
41
41
  - `modao` — 墨刀原型数据
42
42
  - `sketch` — Sketch 选区截图
43
43
  - 如以上 MCP 均不可用,请求用户提供设计截图或标注(适用于摹客等无 MCP 工具)
44
+ - 如果没有权威设计稿,但任务明确要求视觉型网站、品牌页、作品集或高审美重设计,可把用户提供或本地生成的分区级参考图作为临时视觉事实来源;这种来源必须先分析后实现,并在计划中标明不是权威设计系统。
44
45
 
45
46
  ### 2. 读取设计上下文
46
47
 
@@ -51,6 +52,9 @@ description: Use when implementing UI from Figma, Sketch, MasterGo, Pixso, Modao
51
52
  - 如果 MCP 提供了资源文件或 SVG / 图片源,直接使用
52
53
  - 如果 MCP 已提供真实资源,不要自行造占位资源
53
54
  - 如果用户提供截图而非 MCP 数据,从截图中推断布局、颜色、字体等视觉信息
55
+ - 如果使用图像/截图参考,先提取区块结构、视觉层级、主色和功能色、字体气质、间距节奏、媒体位置、交互状态暗示和响应式风险,再写代码。
56
+ - 对多区块页面优先使用清晰的分区级参考图或局部细节图;不要从压缩长图里猜测按钮、字号、间距和组件状态。
57
+ - 视觉参考中的不可读文字、假截图、不可检查素材或冲突状态不能直接照搬;需要转译成项目真实内容、真实状态或明确占位说明。
54
58
 
55
59
  ### 3. 搜索可复用组件
56
60
 
@@ -73,6 +77,7 @@ description: Use when implementing UI from Figma, Sketch, MasterGo, Pixso, Modao
73
77
  - 响应式行为
74
78
  - 复用还是新建的决策
75
79
  - 设计缺口或歧义点
80
+ - 图像/截图来源的可靠性,以及哪些视觉细节会被项目 token、组件约定或可访问性要求覆盖
76
81
 
77
82
  ### 5. 按框架实现
78
83
 
@@ -102,7 +107,7 @@ description: Use when implementing UI from Figma, Sketch, MasterGo, Pixso, Modao
102
107
  - 如有测试,执行测试
103
108
  - 如果缺少测试,说明建议补充的最小测试范围
104
109
 
105
- ## Detailed References
110
+ ## 详细参考
106
111
 
107
112
  撰写实现计划报告时,加载 [references/design-plan-template.md](references/design-plan-template.md)。
108
113
 
@@ -113,6 +118,8 @@ description: Use when implementing UI from Figma, Sketch, MasterGo, Pixso, Modao
113
118
  - 除非有合理理由,不要用硬编码替代已 Token 化的样式
114
119
  - 不要忽略 hover、active、disabled、loading、empty、error 等状态
115
120
  - 摹客等无 MCP 工具场景下,主动向用户索要关键截图和标注信息,不要凭空编造视觉数据
121
+ - 不把生成图像当作可直接复制的代码规范;它只提供视觉证据,最终实现必须服从当前项目组件、token、可访问性和性能边界
122
+ - 不用一张不可读整页图替代分区分析;关键区块、CTA、表单、导航和状态必须可检查
116
123
 
117
124
  ## Expected Output
118
125