@su-record/vibe 2.9.38 → 2.10.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/CLAUDE.md +19 -6
- package/README.md +31 -24
- package/agents/{teams/figma → figma}/figma-analyst.md +2 -2
- package/agents/research/{best-practices-agent.md → best-practices.md} +1 -1
- package/agents/research/{codebase-patterns-agent.md → codebase-patterns.md} +1 -1
- package/agents/research/{framework-docs-agent.md → framework-docs.md} +1 -1
- package/agents/research/{security-advisory-agent.md → security-advisory.md} +1 -1
- package/agents/teams/research-team.md +4 -4
- package/agents/teams/review-debate-team.md +2 -2
- package/agents/teams/security-team.md +4 -4
- package/dist/cli/commands/init.js +2 -2
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/postinstall/claude-agents.d.ts +3 -1
- package/dist/cli/postinstall/claude-agents.d.ts.map +1 -1
- package/dist/cli/postinstall/claude-agents.js +47 -9
- package/dist/cli/postinstall/claude-agents.js.map +1 -1
- package/dist/cli/postinstall/constants.d.ts +5 -0
- package/dist/cli/postinstall/constants.d.ts.map +1 -1
- package/dist/cli/postinstall/constants.js +165 -23
- package/dist/cli/postinstall/constants.js.map +1 -1
- package/dist/cli/postinstall/cursor-skills.js +2 -2
- package/dist/cli/postinstall/main.d.ts.map +1 -1
- package/dist/cli/postinstall/main.js +19 -10
- package/dist/cli/postinstall/main.js.map +1 -1
- package/dist/infra/lib/OrchestrateWorkflow.js +1 -1
- package/dist/infra/lib/OrchestrateWorkflow.js.map +1 -1
- package/dist/infra/lib/telemetry/SkillTelemetry.test.js +4 -4
- package/dist/infra/lib/telemetry/SkillTelemetry.test.js.map +1 -1
- package/dist/infra/orchestrator/parallelResearch.js +4 -4
- package/dist/infra/orchestrator/parallelResearch.js.map +1 -1
- package/hooks/scripts/__tests__/curation-index.test.js +157 -0
- package/hooks/scripts/__tests__/recipe-extractor.test.js +244 -0
- package/hooks/scripts/__tests__/step-counter.test.js +358 -0
- package/hooks/scripts/clone-extract.js +712 -0
- package/hooks/scripts/clone-refine.js +510 -0
- package/hooks/scripts/clone-to-scss.js +275 -0
- package/hooks/scripts/clone-validate.js +280 -0
- package/hooks/scripts/lib/curation-index.js +101 -0
- package/hooks/scripts/recipe-extractor.js +249 -0
- package/hooks/scripts/session-start.js +19 -0
- package/hooks/scripts/step-counter.js +230 -21
- package/package.json +2 -1
- package/skills/agents-md/SKILL.md +2 -0
- package/skills/arch-guard/SKILL.md +2 -0
- package/skills/brand-assets/SKILL.md +1 -0
- package/skills/capability-loop/SKILL.md +2 -0
- package/skills/characterization-test/SKILL.md +2 -0
- package/skills/chub-usage/SKILL.md +1 -0
- package/skills/claude-md-guide/SKILL.md +2 -0
- package/skills/clone/SKILL.md +361 -0
- package/skills/commerce-patterns/SKILL.md +1 -0
- package/skills/commit-push-pr/SKILL.md +1 -0
- package/skills/context7-usage/SKILL.md +1 -0
- package/skills/{vibe-contract → contract}/SKILL.md +7 -8
- package/skills/create-prd/SKILL.md +1 -0
- package/skills/design-audit/SKILL.md +1 -0
- package/skills/design-critique/SKILL.md +1 -0
- package/skills/design-distill/SKILL.md +1 -0
- package/skills/design-normalize/SKILL.md +1 -0
- package/skills/design-polish/SKILL.md +1 -0
- package/skills/design-teach/SKILL.md +2 -0
- package/skills/devlog/SKILL.md +1 -0
- package/skills/{vibe-docs → docs}/SKILL.md +5 -5
- package/skills/e2e-commerce/SKILL.md +1 -0
- package/skills/event-comms/SKILL.md +1 -0
- package/skills/event-ops/SKILL.md +1 -0
- package/skills/event-planning/SKILL.md +1 -0
- package/skills/exec-plan/SKILL.md +2 -0
- package/skills/{vibe-figma → figma}/SKILL.md +4 -3
- package/skills/{vibe-figma-convert → figma-convert}/SKILL.md +4 -3
- package/skills/{vibe-figma-extract → figma-extract}/SKILL.md +4 -3
- package/skills/git-worktree/SKILL.md +1 -0
- package/skills/handoff/SKILL.md +2 -0
- package/skills/{vibe-interview → interview}/SKILL.md +16 -16
- package/skills/parallel-research/SKILL.md +2 -0
- package/skills/{vibe-plan → plan}/SKILL.md +9 -9
- package/skills/prioritization-frameworks/SKILL.md +1 -0
- package/skills/priority-todos/SKILL.md +2 -0
- package/skills/{vibe-regress → regress}/SKILL.md +5 -6
- package/skills/rob-pike/SKILL.md +2 -0
- package/skills/seo-checklist/SKILL.md +1 -0
- package/skills/{vibe-spec → spec}/SKILL.md +14 -14
- package/skills/{vibe-spec-review → spec-review}/SKILL.md +8 -9
- package/skills/systematic-debugging/SKILL.md +2 -0
- package/skills/techdebt/SKILL.md +2 -0
- package/skills/{vibe-test → test}/SKILL.md +12 -12
- package/skills/tool-fallback/SKILL.md +1 -0
- package/skills/typescript-advanced-types/SKILL.md +1 -0
- package/skills/ui-ux-pro-max/SKILL.md +1 -0
- package/skills/user-personas/SKILL.md +1 -0
- package/skills/vercel-react-best-practices/SKILL.md +1 -0
- package/skills/vibe/SKILL.md +266 -0
- package/{commands/vibe.analyze.md → skills/vibe.analyze/SKILL.md} +2 -0
- package/skills/vibe.clone/SKILL.md +117 -0
- package/{commands/vibe.contract.md → skills/vibe.contract/SKILL.md} +3 -1
- package/{commands/vibe.docs.md → skills/vibe.docs/SKILL.md} +3 -1
- package/{commands/vibe.event.md → skills/vibe.event/SKILL.md} +2 -0
- package/{commands/vibe.figma.md → skills/vibe.figma/SKILL.md} +25 -23
- package/{commands/vibe.harness.md → skills/vibe.harness/SKILL.md} +2 -0
- package/{commands/vibe.reason.md → skills/vibe.reason/SKILL.md} +2 -0
- package/{commands/vibe.regress.md → skills/vibe.regress/SKILL.md} +5 -3
- package/{commands/vibe.review.md → skills/vibe.review/SKILL.md} +2 -0
- package/{commands/vibe.run.md → skills/vibe.run/SKILL.md} +3 -1
- package/{commands/vibe.scaffold.md → skills/vibe.scaffold/SKILL.md} +2 -0
- package/{commands/vibe.spec.md → skills/vibe.spec/SKILL.md} +36 -34
- package/{commands/vibe.test.md → skills/vibe.test/SKILL.md} +4 -2
- package/{commands/vibe.trace.md → skills/vibe.trace/SKILL.md} +7 -0
- package/{commands/vibe.utils.md → skills/vibe.utils/SKILL.md} +2 -0
- package/{commands/vibe.verify.md → skills/vibe.verify/SKILL.md} +10 -2
- package/skills/video-production/SKILL.md +1 -0
- /package/agents/{teams/figma → figma}/figma-architect.md +0 -0
- /package/agents/{teams/figma → figma}/figma-auditor.md +0 -0
- /package/agents/{teams/figma → figma}/figma-builder.md +0 -0
- /package/skills/{vibe-docs → docs}/templates/architecture.md +0 -0
- /package/skills/{vibe-docs → docs}/templates/behavioral-principles.md +0 -0
- /package/skills/{vibe-docs → docs}/templates/readme.md +0 -0
- /package/skills/{vibe-docs → docs}/templates/release-notes.md +0 -0
- /package/skills/{vibe-figma → figma}/rubrics/extraction-checklist.md +0 -0
- /package/skills/{vibe-figma → figma}/templates/component-index.md +0 -0
- /package/skills/{vibe-figma → figma}/templates/component-spec.md +0 -0
- /package/skills/{vibe-figma → figma}/templates/figma-handoff.md +0 -0
- /package/skills/{vibe-figma → figma}/templates/remapped-tree.md +0 -0
- /package/skills/{vibe-figma-convert → figma-convert}/rubrics/conversion-rules.md +0 -0
- /package/skills/{vibe-figma-convert → figma-convert}/templates/component.md +0 -0
- /package/skills/{vibe-figma-extract → figma-extract}/rubrics/image-rules.md +0 -0
- /package/skills/{vibe-interview → interview}/checklists/api.md +0 -0
- /package/skills/{vibe-interview → interview}/checklists/feature.md +0 -0
- /package/skills/{vibe-interview → interview}/checklists/library.md +0 -0
- /package/skills/{vibe-interview → interview}/checklists/mobile.md +0 -0
- /package/skills/{vibe-interview → interview}/checklists/webapp.md +0 -0
- /package/skills/{vibe-interview → interview}/checklists/website.md +0 -0
- /package/skills/{vibe-regress → regress}/templates/bug.md +0 -0
- /package/skills/{vibe-regress → regress}/templates/test-jest.md +0 -0
- /package/skills/{vibe-regress → regress}/templates/test-vitest.md +0 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* clone-refine.js — computed.json → sections.json
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node clone-refine.js <rendered.html> <computed.json> --out=<sections.json> --bp=mo|pc
|
|
8
|
+
* [--stylesheets=<stylesheets.json>] [--asset-map=<asset-map.json>]
|
|
9
|
+
*
|
|
10
|
+
* Refinement steps:
|
|
11
|
+
* 1. Tree reconstruction (flat node list → tree)
|
|
12
|
+
* 2. Section detection (semantic landmarks + visual heuristics)
|
|
13
|
+
* 3. Repeated pattern detection → component candidates (structural hash, ≥3 siblings)
|
|
14
|
+
* 4. Design token extraction with bucketing (colors / typography / spacing / radius / shadow)
|
|
15
|
+
* 5. Token-aware CSS values (e.g. color "#0070f3" → "var(--color-blue-500)")
|
|
16
|
+
*
|
|
17
|
+
* Output schema: see SKILL.md.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import fs from 'fs';
|
|
21
|
+
import path from 'path';
|
|
22
|
+
|
|
23
|
+
// ─── CLI ────────────────────────────────────────────────────────────
|
|
24
|
+
function parseArgs(argv) {
|
|
25
|
+
const [, , htmlPath, computedPath, ...rest] = argv;
|
|
26
|
+
const opts = {};
|
|
27
|
+
for (const a of rest) {
|
|
28
|
+
if (a.startsWith('--out=')) opts.out = a.slice(6);
|
|
29
|
+
else if (a.startsWith('--bp=')) opts.bp = a.slice(5);
|
|
30
|
+
else if (a.startsWith('--stylesheets=')) opts.stylesheets = a.slice(14);
|
|
31
|
+
else if (a.startsWith('--asset-map=')) opts.assetMap = a.slice(12);
|
|
32
|
+
}
|
|
33
|
+
return { htmlPath, computedPath, opts };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { htmlPath, computedPath, opts } = parseArgs(process.argv);
|
|
37
|
+
if (!htmlPath || !computedPath || !opts.out) {
|
|
38
|
+
console.error('Usage: node clone-refine.js <rendered.html> <computed.json> --out=<sections.json> --bp=mo|pc');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── Tree reconstruction ────────────────────────────────────────────
|
|
43
|
+
function buildTree(nodes) {
|
|
44
|
+
const byId = new Map();
|
|
45
|
+
for (const n of nodes) byId.set(n.id, { ...n, children: [] });
|
|
46
|
+
const roots = [];
|
|
47
|
+
for (const n of byId.values()) {
|
|
48
|
+
if (n.parent && byId.has(n.parent)) byId.get(n.parent).children.push(n);
|
|
49
|
+
else roots.push(n);
|
|
50
|
+
}
|
|
51
|
+
return { roots, byId };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── CSS value parsers ──────────────────────────────────────────────
|
|
55
|
+
function parsePx(val) {
|
|
56
|
+
if (typeof val !== 'string') return null;
|
|
57
|
+
const m = /^(-?\d+(?:\.\d+)?)px$/.exec(val.trim());
|
|
58
|
+
return m ? Number(m[1]) : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseColor(val) {
|
|
62
|
+
if (typeof val !== 'string') return null;
|
|
63
|
+
const v = val.trim().toLowerCase();
|
|
64
|
+
if (!v || v === 'transparent' || v === 'currentcolor' || v === 'inherit') return null;
|
|
65
|
+
// rgb(a) / hsl(a)
|
|
66
|
+
let m = /^rgba?\(([^)]+)\)$/.exec(v);
|
|
67
|
+
if (m) {
|
|
68
|
+
const parts = m[1].split(/[,/\s]+/).filter(Boolean).map(Number);
|
|
69
|
+
if (parts.length >= 3 && parts.every((x) => !isNaN(x))) {
|
|
70
|
+
const [r, g, b, a] = parts;
|
|
71
|
+
if (a !== undefined && a < 1) return `rgba(${r}, ${g}, ${b}, ${a})`;
|
|
72
|
+
const hex = '#' + [r, g, b].map((x) => Math.max(0, Math.min(255, Math.round(x))).toString(16).padStart(2, '0')).join('');
|
|
73
|
+
return hex;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
m = /^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/.exec(v);
|
|
77
|
+
if (m) {
|
|
78
|
+
let h = m[1];
|
|
79
|
+
if (h.length === 3) h = h.split('').map((c) => c + c).join('');
|
|
80
|
+
if (h.length === 4) h = h.split('').map((c) => c + c).join('');
|
|
81
|
+
return '#' + h;
|
|
82
|
+
}
|
|
83
|
+
return v;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Snap spacing values to the closest grid step (4px → 8px → 16px)
|
|
87
|
+
function snapSpacing(px) {
|
|
88
|
+
if (px === null || px === undefined) return null;
|
|
89
|
+
if (px === 0) return 0;
|
|
90
|
+
const abs = Math.abs(px);
|
|
91
|
+
if (abs < 8) return Math.round(px); // sub-grid keep
|
|
92
|
+
if (abs < 32) return Math.round(px / 4) * 4;
|
|
93
|
+
if (abs < 96) return Math.round(px / 8) * 8;
|
|
94
|
+
return Math.round(px / 16) * 16;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─── Token extraction ───────────────────────────────────────────────
|
|
98
|
+
function extractTokens(nodes) {
|
|
99
|
+
const colorFreq = new Map();
|
|
100
|
+
const typoFreq = new Map();
|
|
101
|
+
const spacingFreq = new Map();
|
|
102
|
+
const radiusFreq = new Map();
|
|
103
|
+
const shadowFreq = new Map();
|
|
104
|
+
|
|
105
|
+
const addColor = (val) => {
|
|
106
|
+
const c = parseColor(val);
|
|
107
|
+
if (c) colorFreq.set(c, (colorFreq.get(c) || 0) + 1);
|
|
108
|
+
};
|
|
109
|
+
const addSpacing = (px) => {
|
|
110
|
+
if (px === null || px === undefined) return;
|
|
111
|
+
const snapped = snapSpacing(px);
|
|
112
|
+
if (snapped === null) return;
|
|
113
|
+
spacingFreq.set(snapped, (spacingFreq.get(snapped) || 0) + 1);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
for (const n of nodes) {
|
|
117
|
+
if (!n.css) continue;
|
|
118
|
+
addColor(n.css['color']);
|
|
119
|
+
addColor(n.css['background-color']);
|
|
120
|
+
for (const side of ['top', 'right', 'bottom', 'left']) {
|
|
121
|
+
addColor(n.css[`border-${side}-color`]);
|
|
122
|
+
}
|
|
123
|
+
// gradient stops
|
|
124
|
+
const bg = n.css['background-image'];
|
|
125
|
+
if (bg && /gradient/i.test(bg)) {
|
|
126
|
+
const re = /(?:#[0-9a-f]{3,8}|rgba?\([^)]+\)|hsla?\([^)]+\))/gi;
|
|
127
|
+
let m;
|
|
128
|
+
while ((m = re.exec(bg)) !== null) addColor(m[0]);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// typography
|
|
132
|
+
const ff = n.css['font-family'];
|
|
133
|
+
const fs = n.css['font-size'];
|
|
134
|
+
const fw = n.css['font-weight'];
|
|
135
|
+
const lh = n.css['line-height'];
|
|
136
|
+
const ls = n.css['letter-spacing'];
|
|
137
|
+
if (ff && fs) {
|
|
138
|
+
const key = JSON.stringify({ ff, fs, fw: fw || '400', lh: lh || 'normal', ls: ls || 'normal' });
|
|
139
|
+
typoFreq.set(key, (typoFreq.get(key) || 0) + 1);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// spacing
|
|
143
|
+
for (const side of ['top', 'right', 'bottom', 'left']) {
|
|
144
|
+
addSpacing(parsePx(n.css[`margin-${side}`]));
|
|
145
|
+
addSpacing(parsePx(n.css[`padding-${side}`]));
|
|
146
|
+
}
|
|
147
|
+
addSpacing(parsePx(n.css['gap']));
|
|
148
|
+
addSpacing(parsePx(n.css['row-gap']));
|
|
149
|
+
addSpacing(parsePx(n.css['column-gap']));
|
|
150
|
+
|
|
151
|
+
// radius
|
|
152
|
+
for (const corner of ['top-left', 'top-right', 'bottom-left', 'bottom-right']) {
|
|
153
|
+
const r = parsePx(n.css[`border-${corner}-radius`]);
|
|
154
|
+
if (r !== null && r > 0) radiusFreq.set(r, (radiusFreq.get(r) || 0) + 1);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// shadow
|
|
158
|
+
const sh = n.css['box-shadow'];
|
|
159
|
+
if (sh && sh !== 'none') shadowFreq.set(sh, (shadowFreq.get(sh) || 0) + 1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const sortedTop = (m, limit) => Array.from(m.entries())
|
|
163
|
+
.sort((a, b) => b[1] - a[1])
|
|
164
|
+
.slice(0, limit)
|
|
165
|
+
.map(([k, count]) => ({ value: k, count }));
|
|
166
|
+
|
|
167
|
+
// Name tokens
|
|
168
|
+
const colors = sortedTop(colorFreq, 20).map((t, i) => ({
|
|
169
|
+
name: nameColor(t.value, i),
|
|
170
|
+
value: t.value,
|
|
171
|
+
count: t.count,
|
|
172
|
+
}));
|
|
173
|
+
|
|
174
|
+
const typography = sortedTop(typoFreq, 12).map((t, i) => {
|
|
175
|
+
const parsed = JSON.parse(t.value);
|
|
176
|
+
return {
|
|
177
|
+
name: `text-${i + 1}`,
|
|
178
|
+
family: parsed.ff,
|
|
179
|
+
size: parsed.fs,
|
|
180
|
+
weight: parsed.fw,
|
|
181
|
+
lineHeight: parsed.lh,
|
|
182
|
+
letterSpacing: parsed.ls,
|
|
183
|
+
count: t.count,
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const spacing = sortedTop(spacingFreq, 16).map((t) => ({
|
|
188
|
+
name: `space-${t.value}`,
|
|
189
|
+
value: `${t.value}px`,
|
|
190
|
+
count: t.count,
|
|
191
|
+
}));
|
|
192
|
+
|
|
193
|
+
const radius = sortedTop(radiusFreq, 8).map((t) => ({
|
|
194
|
+
name: `radius-${t.value}`,
|
|
195
|
+
value: `${t.value}px`,
|
|
196
|
+
count: t.count,
|
|
197
|
+
}));
|
|
198
|
+
|
|
199
|
+
const shadow = sortedTop(shadowFreq, 6).map((t, i) => ({
|
|
200
|
+
name: `shadow-${i + 1}`,
|
|
201
|
+
value: t.value,
|
|
202
|
+
count: t.count,
|
|
203
|
+
}));
|
|
204
|
+
|
|
205
|
+
return { colors, typography, spacing, radius, shadow };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function nameColor(hex, idx) {
|
|
209
|
+
// Heuristic naming: hex → semantic label by brightness/saturation
|
|
210
|
+
if (hex === '#ffffff' || hex === '#fff') return 'color-white';
|
|
211
|
+
if (hex === '#000000' || hex === '#000') return 'color-black';
|
|
212
|
+
if (hex.startsWith('rgba')) return `color-overlay-${idx + 1}`;
|
|
213
|
+
// Compute HSL roughly for naming
|
|
214
|
+
const m = /^#([0-9a-f]{6})$/.exec(hex);
|
|
215
|
+
if (m) {
|
|
216
|
+
const r = parseInt(m[1].slice(0, 2), 16) / 255;
|
|
217
|
+
const g = parseInt(m[1].slice(2, 4), 16) / 255;
|
|
218
|
+
const b = parseInt(m[1].slice(4, 6), 16) / 255;
|
|
219
|
+
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
|
220
|
+
const l = (max + min) / 2;
|
|
221
|
+
const d = max - min;
|
|
222
|
+
if (d < 0.06) return `color-gray-${Math.round(l * 100)}`;
|
|
223
|
+
let h = 0;
|
|
224
|
+
if (max === r) h = ((g - b) / d) % 6;
|
|
225
|
+
else if (max === g) h = (b - r) / d + 2;
|
|
226
|
+
else h = (r - g) / d + 4;
|
|
227
|
+
h = Math.round(h * 60);
|
|
228
|
+
if (h < 0) h += 360;
|
|
229
|
+
const hue =
|
|
230
|
+
h < 15 ? 'red' :
|
|
231
|
+
h < 45 ? 'orange' :
|
|
232
|
+
h < 70 ? 'yellow' :
|
|
233
|
+
h < 165 ? 'green' :
|
|
234
|
+
h < 200 ? 'teal' :
|
|
235
|
+
h < 250 ? 'blue' :
|
|
236
|
+
h < 290 ? 'purple' :
|
|
237
|
+
h < 330 ? 'pink' : 'red';
|
|
238
|
+
return `color-${hue}-${Math.round(l * 100)}`;
|
|
239
|
+
}
|
|
240
|
+
return `color-${idx + 1}`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ─── Section detection ──────────────────────────────────────────────
|
|
244
|
+
const SEMANTIC_SECTION_TAGS = new Set(['header', 'nav', 'main', 'section', 'article', 'aside', 'footer']);
|
|
245
|
+
|
|
246
|
+
function findSections(roots) {
|
|
247
|
+
// 1. Find <body>
|
|
248
|
+
const html = roots.find((r) => r.tag === 'html');
|
|
249
|
+
if (!html) return [];
|
|
250
|
+
const body = html.children.find((c) => c.tag === 'body');
|
|
251
|
+
if (!body) return [];
|
|
252
|
+
|
|
253
|
+
// 2. Collect candidate landmarks from body subtree (BFS, prefer top-level)
|
|
254
|
+
const sections = [];
|
|
255
|
+
const seenIds = new Set();
|
|
256
|
+
|
|
257
|
+
const tryAdd = (node, label) => {
|
|
258
|
+
if (seenIds.has(node.id)) return;
|
|
259
|
+
if (!node.box || node.box.h < 40) return;
|
|
260
|
+
seenIds.add(node.id);
|
|
261
|
+
sections.push({ node, label });
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// First pass: explicit semantic tags within body
|
|
265
|
+
const queue = [...body.children];
|
|
266
|
+
while (queue.length) {
|
|
267
|
+
const n = queue.shift();
|
|
268
|
+
if (SEMANTIC_SECTION_TAGS.has(n.tag)) {
|
|
269
|
+
tryAdd(n, sectionLabelFor(n));
|
|
270
|
+
// Don't descend — keep section as atomic unit
|
|
271
|
+
} else {
|
|
272
|
+
queue.push(...n.children);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Second pass: if no semantic sections found, fall back to top-level body children with significant height
|
|
277
|
+
if (sections.length === 0) {
|
|
278
|
+
for (const child of body.children) {
|
|
279
|
+
if (child.box && child.box.h >= 100) tryAdd(child, sectionLabelFor(child));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Sort by visual order (y position)
|
|
284
|
+
sections.sort((a, b) => a.node.box.y - b.node.box.y);
|
|
285
|
+
|
|
286
|
+
// Disambiguate labels (Hero → Hero, Hero-2 if duplicate)
|
|
287
|
+
const labelCounts = new Map();
|
|
288
|
+
for (const s of sections) {
|
|
289
|
+
const base = s.label;
|
|
290
|
+
const count = (labelCounts.get(base) || 0) + 1;
|
|
291
|
+
labelCounts.set(base, count);
|
|
292
|
+
s.label = count > 1 ? `${base}-${count}` : base;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return sections;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function sectionLabelFor(node) {
|
|
299
|
+
// Prefer explicit class/id hints
|
|
300
|
+
const cls = (node.classes || '').toLowerCase();
|
|
301
|
+
const hints = [
|
|
302
|
+
['hero', 'Hero'],
|
|
303
|
+
['banner', 'Banner'],
|
|
304
|
+
['header', 'Header'],
|
|
305
|
+
['nav', 'Nav'],
|
|
306
|
+
['footer', 'Footer'],
|
|
307
|
+
['feature', 'Features'],
|
|
308
|
+
['testimonial', 'Testimonials'],
|
|
309
|
+
['pricing', 'Pricing'],
|
|
310
|
+
['cta', 'CTA'],
|
|
311
|
+
['faq', 'FAQ'],
|
|
312
|
+
['gallery', 'Gallery'],
|
|
313
|
+
['contact', 'Contact'],
|
|
314
|
+
];
|
|
315
|
+
for (const [pat, name] of hints) if (cls.includes(pat) || (node.attrs && (node.attrs.role || '').includes(pat))) return name;
|
|
316
|
+
// Fall back to semantic tag
|
|
317
|
+
const tagLabels = { header: 'Header', footer: 'Footer', nav: 'Nav', main: 'Main', article: 'Article', aside: 'Aside' };
|
|
318
|
+
if (tagLabels[node.tag]) return tagLabels[node.tag];
|
|
319
|
+
return 'Section';
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ─── Component pattern detection ────────────────────────────────────
|
|
323
|
+
// Structural hash: tag + class signature + first-level child tag list + leaf count tier
|
|
324
|
+
function structuralHash(node, depth = 0) {
|
|
325
|
+
if (depth > 3) return '';
|
|
326
|
+
const classSig = (node.classes || '').split(/\s+/).filter(Boolean).sort().slice(0, 4).join('.');
|
|
327
|
+
const childTags = (node.children || []).map((c) => c.tag).slice(0, 8).join(',');
|
|
328
|
+
const childCount = (node.children || []).length;
|
|
329
|
+
const sizeBucket = node.box ? `${Math.round(node.box.w / 40)}x${Math.round(node.box.h / 40)}` : '?';
|
|
330
|
+
return `${node.tag}|${classSig}|${childTags}|${childCount}|${sizeBucket}`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function detectComponents(sectionRoot) {
|
|
334
|
+
// Group siblings at each depth by structural hash
|
|
335
|
+
const candidates = [];
|
|
336
|
+
const walk = (parent) => {
|
|
337
|
+
if (!parent.children || parent.children.length < 3) {
|
|
338
|
+
for (const c of (parent.children || [])) walk(c);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const groups = new Map();
|
|
342
|
+
for (const child of parent.children) {
|
|
343
|
+
const hash = structuralHash(child);
|
|
344
|
+
if (!groups.has(hash)) groups.set(hash, []);
|
|
345
|
+
groups.get(hash).push(child);
|
|
346
|
+
}
|
|
347
|
+
for (const [hash, members] of groups) {
|
|
348
|
+
if (members.length >= 3) {
|
|
349
|
+
candidates.push({
|
|
350
|
+
hash,
|
|
351
|
+
count: members.length,
|
|
352
|
+
parentId: parent.id,
|
|
353
|
+
memberIds: members.map((m) => m.id),
|
|
354
|
+
exemplarTag: members[0].tag,
|
|
355
|
+
exemplarClasses: members[0].classes,
|
|
356
|
+
exemplarBox: members[0].box,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
for (const c of parent.children) walk(c);
|
|
361
|
+
};
|
|
362
|
+
walk(sectionRoot);
|
|
363
|
+
return candidates;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ─── BG image classification ────────────────────────────────────────
|
|
367
|
+
function classifyImages(node) {
|
|
368
|
+
const out = { bg: [], content: [] };
|
|
369
|
+
const walk = (n) => {
|
|
370
|
+
if (n.css) {
|
|
371
|
+
const bg = n.css['background-image'];
|
|
372
|
+
if (bg && bg !== 'none' && /url\(/.test(bg)) {
|
|
373
|
+
const re = /url\(['"]?([^'")]+)['"]?\)/g;
|
|
374
|
+
let m;
|
|
375
|
+
while ((m = re.exec(bg)) !== null) out.bg.push(m[1]);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (n.tag === 'img') {
|
|
379
|
+
const src = (n.attrs && (n.attrs.currentSrc || n.attrs.src));
|
|
380
|
+
if (src) out.content.push(src);
|
|
381
|
+
}
|
|
382
|
+
for (const c of (n.children || [])) walk(c);
|
|
383
|
+
};
|
|
384
|
+
walk(node);
|
|
385
|
+
return out;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ─── Trim children to essential layout/text/image nodes ─────────────
|
|
389
|
+
function trimSubtree(node) {
|
|
390
|
+
if (!node) return null;
|
|
391
|
+
const trimmed = {
|
|
392
|
+
id: node.id,
|
|
393
|
+
tag: node.tag,
|
|
394
|
+
classes: node.classes || '',
|
|
395
|
+
pseudo: node.pseudo || false,
|
|
396
|
+
isSvg: node.isSvg || false,
|
|
397
|
+
box: node.box,
|
|
398
|
+
css: node.css || {},
|
|
399
|
+
};
|
|
400
|
+
if (node.attrs) {
|
|
401
|
+
const a = node.attrs;
|
|
402
|
+
const keep = {};
|
|
403
|
+
for (const k of ['src', 'currentSrc', 'href', 'alt', 'title', 'role', 'ariaLabel', 'type', 'name', 'placeholder']) {
|
|
404
|
+
if (a[k]) keep[k] = a[k];
|
|
405
|
+
}
|
|
406
|
+
if (Object.keys(keep).length) trimmed.attrs = keep;
|
|
407
|
+
}
|
|
408
|
+
if (node.text) trimmed.text = node.text;
|
|
409
|
+
if (node.svgMarkup) trimmed.svgMarkup = node.svgMarkup;
|
|
410
|
+
if (node.children && node.children.length) {
|
|
411
|
+
trimmed.children = node.children.map(trimSubtree);
|
|
412
|
+
}
|
|
413
|
+
return trimmed;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ─── Token application: replace literal values with token refs ──────
|
|
417
|
+
function applyTokens(nodes, tokens) {
|
|
418
|
+
const colorByValue = new Map(tokens.colors.map((t) => [t.value, t.name]));
|
|
419
|
+
const spacingByValue = new Map(tokens.spacing.map((t) => [t.value, t.name]));
|
|
420
|
+
const radiusByValue = new Map(tokens.radius.map((t) => [t.value, t.name]));
|
|
421
|
+
const shadowByValue = new Map(tokens.shadow.map((t) => [t.value, t.name]));
|
|
422
|
+
|
|
423
|
+
const sub = (val, map) => {
|
|
424
|
+
if (!val) return val;
|
|
425
|
+
const normalized = parseColor(val) || val;
|
|
426
|
+
if (map.has(normalized)) return `var(--${map.get(normalized)})`;
|
|
427
|
+
if (map.has(val)) return `var(--${map.get(val)})`;
|
|
428
|
+
return val;
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const walkAndApply = (n) => {
|
|
432
|
+
if (n.css) {
|
|
433
|
+
for (const prop of Object.keys(n.css)) {
|
|
434
|
+
if (prop === 'color' || prop === 'background-color' || prop.endsWith('-color')) {
|
|
435
|
+
n.css[prop] = sub(n.css[prop], colorByValue);
|
|
436
|
+
} else if (prop.startsWith('margin-') || prop.startsWith('padding-') || prop === 'gap' || prop === 'row-gap' || prop === 'column-gap') {
|
|
437
|
+
const px = parsePx(n.css[prop]);
|
|
438
|
+
if (px !== null) {
|
|
439
|
+
const snapped = snapSpacing(px);
|
|
440
|
+
const key = `${snapped}px`;
|
|
441
|
+
if (spacingByValue.has(key)) n.css[prop] = `var(--${spacingByValue.get(key)})`;
|
|
442
|
+
}
|
|
443
|
+
} else if (prop.endsWith('-radius')) {
|
|
444
|
+
if (radiusByValue.has(n.css[prop])) n.css[prop] = `var(--${radiusByValue.get(n.css[prop])})`;
|
|
445
|
+
} else if (prop === 'box-shadow') {
|
|
446
|
+
if (shadowByValue.has(n.css[prop])) n.css[prop] = `var(--${shadowByValue.get(n.css[prop])})`;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
for (const c of (n.children || [])) walkAndApply(c);
|
|
451
|
+
};
|
|
452
|
+
for (const n of nodes) walkAndApply(n);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ─── Main ───────────────────────────────────────────────────────────
|
|
456
|
+
function main() {
|
|
457
|
+
const computed = JSON.parse(fs.readFileSync(computedPath, 'utf8'));
|
|
458
|
+
const { roots, byId } = buildTree(computed.nodes);
|
|
459
|
+
|
|
460
|
+
// Token extraction first (across whole document)
|
|
461
|
+
const tokens = extractTokens(computed.nodes);
|
|
462
|
+
|
|
463
|
+
// Section detection
|
|
464
|
+
const sectionEntries = findSections(roots);
|
|
465
|
+
console.log(`[clone-refine] detected ${sectionEntries.length} sections, ${tokens.colors.length} colors, ${tokens.typography.length} typo, ${tokens.spacing.length} spacings`);
|
|
466
|
+
|
|
467
|
+
// Build refined sections
|
|
468
|
+
const sections = sectionEntries.map(({ node, label }) => {
|
|
469
|
+
const subtree = trimSubtree(node);
|
|
470
|
+
const components = detectComponents(node);
|
|
471
|
+
const images = classifyImages(node);
|
|
472
|
+
return {
|
|
473
|
+
name: label,
|
|
474
|
+
nodeRef: node.id,
|
|
475
|
+
tag: node.tag,
|
|
476
|
+
classes: node.classes,
|
|
477
|
+
box: node.box,
|
|
478
|
+
css: node.css || {},
|
|
479
|
+
components,
|
|
480
|
+
images,
|
|
481
|
+
children: (subtree.children || []),
|
|
482
|
+
};
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// Apply token substitution last (so original values are preserved through detection)
|
|
486
|
+
applyTokens(sections, tokens);
|
|
487
|
+
|
|
488
|
+
const out = {
|
|
489
|
+
meta: {
|
|
490
|
+
feature: path.basename(path.dirname(opts.out)) || 'feature',
|
|
491
|
+
url: computed.meta.url,
|
|
492
|
+
viewport: computed.meta.viewport,
|
|
493
|
+
bp: opts.bp || computed.meta.bp,
|
|
494
|
+
generatedAt: new Date().toISOString(),
|
|
495
|
+
},
|
|
496
|
+
tokens,
|
|
497
|
+
sections,
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
fs.mkdirSync(path.dirname(opts.out), { recursive: true });
|
|
501
|
+
fs.writeFileSync(opts.out, JSON.stringify(out, null, 2));
|
|
502
|
+
console.log(`[clone-refine] done → ${opts.out}`);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
try { main(); }
|
|
506
|
+
catch (e) {
|
|
507
|
+
console.error(`[clone-refine] FAIL: ${e.message}`);
|
|
508
|
+
if (process.env.DEBUG) console.error(e.stack);
|
|
509
|
+
process.exit(1);
|
|
510
|
+
}
|