@12ui/design 0.2.29 → 0.2.31
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 +31 -6
- package/SKILL.md +1 -1
- package/dist/auth-request.d.ts +9 -0
- package/dist/auth-request.d.ts.map +1 -0
- package/dist/auth-request.js +32 -0
- package/dist/auth-request.js.map +1 -0
- package/dist/branch-execution-types.d.ts +2 -0
- package/dist/branch-execution-types.d.ts.map +1 -1
- package/dist/branch-execution.d.ts.map +1 -1
- package/dist/branch-execution.js +3 -0
- package/dist/branch-execution.js.map +1 -1
- package/dist/branch-run-record.d.ts +1 -0
- package/dist/branch-run-record.d.ts.map +1 -1
- package/dist/branch-run-record.js.map +1 -1
- package/dist/cli-arguments.d.ts.map +1 -1
- package/dist/cli-arguments.js +1 -0
- package/dist/cli-arguments.js.map +1 -1
- package/dist/cli-auth-command.d.ts +1 -1
- package/dist/cli-auth-command.d.ts.map +1 -1
- package/dist/cli-auth-command.js +37 -7
- package/dist/cli-auth-command.js.map +1 -1
- package/dist/cli-auth-onboarding.d.ts +15 -1
- package/dist/cli-auth-onboarding.d.ts.map +1 -1
- package/dist/cli-auth-onboarding.js +46 -12
- package/dist/cli-auth-onboarding.js.map +1 -1
- package/dist/cli-branch-command.d.ts +4 -0
- package/dist/cli-branch-command.d.ts.map +1 -1
- package/dist/cli-branch-command.js +30 -3
- package/dist/cli-branch-command.js.map +1 -1
- package/dist/cli-capabilities.d.ts +9 -0
- package/dist/cli-capabilities.d.ts.map +1 -1
- package/dist/cli-capabilities.js +9 -0
- package/dist/cli-capabilities.js.map +1 -1
- package/dist/cli-cli-command.d.ts.map +1 -1
- package/dist/cli-cli-command.js +5 -1
- package/dist/cli-cli-command.js.map +1 -1
- package/dist/cli-help/design.d.ts +8 -1
- package/dist/cli-help/design.d.ts.map +1 -1
- package/dist/cli-help/design.js +12 -0
- package/dist/cli-help/design.js.map +1 -1
- package/dist/cli-help/index.d.ts +16 -2
- package/dist/cli-help/index.d.ts.map +1 -1
- package/dist/cli-help/setup.d.ts +8 -1
- package/dist/cli-help/setup.d.ts.map +1 -1
- package/dist/cli-help/setup.js +11 -0
- package/dist/cli-help/setup.js.map +1 -1
- package/dist/cli-pending-auth.d.ts +2 -0
- package/dist/cli-pending-auth.d.ts.map +1 -1
- package/dist/cli-pending-auth.js +39 -15
- package/dist/cli-pending-auth.js.map +1 -1
- package/dist/cli-persistence-report.d.ts.map +1 -1
- package/dist/cli-persistence-report.js +6 -0
- package/dist/cli-persistence-report.js.map +1 -1
- package/dist/cli-persistence.d.ts +7 -0
- package/dist/cli-persistence.d.ts.map +1 -1
- package/dist/cli-persistence.js +53 -2
- package/dist/cli-persistence.js.map +1 -1
- package/dist/cli-prototype-command.d.ts +12 -0
- package/dist/cli-prototype-command.d.ts.map +1 -0
- package/dist/cli-prototype-command.js +29 -0
- package/dist/cli-prototype-command.js.map +1 -0
- package/dist/cli-review-command.d.ts.map +1 -1
- package/dist/cli-review-command.js +20 -1
- package/dist/cli-review-command.js.map +1 -1
- package/dist/cli-run.d.ts.map +1 -1
- package/dist/cli-run.js +12 -1
- package/dist/cli-run.js.map +1 -1
- package/dist/cli-skill-command.d.ts.map +1 -1
- package/dist/cli-skill-command.js +9 -3
- package/dist/cli-skill-command.js.map +1 -1
- package/dist/cli-usage.d.ts.map +1 -1
- package/dist/cli-usage.js +7 -1
- package/dist/cli-usage.js.map +1 -1
- package/dist/credential-setup.d.ts +4 -1
- package/dist/credential-setup.d.ts.map +1 -1
- package/dist/credential-setup.js +28 -3
- package/dist/credential-setup.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/legacy-skill-catalog.d.ts.map +1 -1
- package/dist/legacy-skill-catalog.js +16 -0
- package/dist/legacy-skill-catalog.js.map +1 -1
- package/dist/login-auth.d.ts +16 -0
- package/dist/login-auth.d.ts.map +1 -1
- package/dist/login-auth.js +107 -25
- package/dist/login-auth.js.map +1 -1
- package/dist/prototype-gap-html.d.ts +10 -0
- package/dist/prototype-gap-html.d.ts.map +1 -0
- package/dist/prototype-gap-html.js +55 -0
- package/dist/prototype-gap-html.js.map +1 -0
- package/dist/prototype-gap-source.d.ts +14 -0
- package/dist/prototype-gap-source.d.ts.map +1 -0
- package/dist/prototype-gap-source.js +87 -0
- package/dist/prototype-gap-source.js.map +1 -0
- package/dist/prototype-gates.d.ts +24 -0
- package/dist/prototype-gates.d.ts.map +1 -0
- package/dist/prototype-gates.js +76 -0
- package/dist/prototype-gates.js.map +1 -0
- package/dist/prototype-label-client.d.ts +51 -0
- package/dist/prototype-label-client.d.ts.map +1 -0
- package/dist/prototype-label-client.js +53 -0
- package/dist/prototype-label-client.js.map +1 -0
- package/dist/prototype-label-score.d.ts +26 -0
- package/dist/prototype-label-score.d.ts.map +1 -0
- package/dist/prototype-label-score.js +51 -0
- package/dist/prototype-label-score.js.map +1 -0
- package/dist/prototype-process.d.ts +13 -0
- package/dist/prototype-process.d.ts.map +1 -0
- package/dist/prototype-process.js +26 -0
- package/dist/prototype-process.js.map +1 -0
- package/dist/prototype-requests.d.ts +29 -0
- package/dist/prototype-requests.d.ts.map +1 -0
- package/dist/prototype-requests.js +34 -0
- package/dist/prototype-requests.js.map +1 -0
- package/dist/prototype-runner.d.ts +22 -0
- package/dist/prototype-runner.d.ts.map +1 -0
- package/dist/prototype-runner.js +159 -0
- package/dist/prototype-runner.js.map +1 -0
- package/dist/prototype-source.d.ts +9 -0
- package/dist/prototype-source.d.ts.map +1 -0
- package/dist/prototype-source.js +48 -0
- package/dist/prototype-source.js.map +1 -0
- package/dist/review-discovery.d.ts +1 -0
- package/dist/review-discovery.d.ts.map +1 -1
- package/dist/review-discovery.js +49 -3
- package/dist/review-discovery.js.map +1 -1
- package/dist/review-page-assets.d.ts +2 -2
- package/dist/review-page-assets.d.ts.map +1 -1
- package/dist/review-page-assets.js +20 -1
- package/dist/review-page-assets.js.map +1 -1
- package/dist/review-page.d.ts.map +1 -1
- package/dist/review-page.js +38 -0
- package/dist/review-page.js.map +1 -1
- package/dist/review-prototype-gap.d.ts +48 -0
- package/dist/review-prototype-gap.d.ts.map +1 -0
- package/dist/review-prototype-gap.js +192 -0
- package/dist/review-prototype-gap.js.map +1 -0
- package/dist/review-server.d.ts +7 -0
- package/dist/review-server.d.ts.map +1 -1
- package/dist/review-server.js +64 -0
- package/dist/review-server.js.map +1 -1
- package/dist/review-types.d.ts +12 -0
- package/dist/review-types.d.ts.map +1 -1
- package/dist/run-journal-commands.d.ts.map +1 -1
- package/dist/run-journal-commands.js +13 -0
- package/dist/run-journal-commands.js.map +1 -1
- package/dist/run-journal.d.ts.map +1 -1
- package/dist/run-journal.js +6 -0
- package/dist/run-journal.js.map +1 -1
- package/open-design.json +2 -2
- package/package.json +7 -1
- package/prototype-kit/analyze.mjs +275 -0
- package/prototype-kit/build.mjs +393 -0
- package/prototype-kit/inventory.mjs +104 -0
- package/prototype-kit/label-briefs.mjs +47 -0
- package/prototype-kit/lib/actions.mjs +15 -0
- package/prototype-kit/lib/navigation.mjs +33 -0
- package/prototype-kit/lib/runtime-check.mjs +126 -0
- package/prototype-kit/lib/shells.mjs +68 -0
- package/prototype-kit/lib/static-server.mjs +40 -0
- package/prototype-kit/lib/text.mjs +20 -0
- package/prototype-kit/runtime/proto.css +46 -0
- package/prototype-kit/runtime/proto.js +173 -0
- package/prototype-kit/score-labels.mjs +42 -0
- package/prototype-kit/tests/README.md +28 -0
- package/prototype-kit/tests/adversarial-runtime.mjs +90 -0
- package/prototype-kit/tests/discover-runs.mjs +29 -0
- package/prototype-kit/tests/fixtures/no-semantic/branch.plan.json +7 -0
- package/prototype-kit/tests/fixtures/no-semantic/pages/legacy.html +1 -0
- package/prototype-kit/tests/fixtures/partial-plan/branch.plan.json +5 -0
- package/prototype-kit/tests/fixtures/partial-plan/pages/orphan.html +4 -0
- package/prototype-kit/tests/fixtures/runtime/double.html +5 -0
- package/prototype-kit/tests/fixtures/runtime/history-destination.html +1 -0
- package/prototype-kit/tests/fixtures/runtime/history.html +6 -0
- package/prototype-kit/tests/fixtures/runtime/hover-stack.html +15 -0
- package/prototype-kit/tests/fixtures/runtime/invalid-dropdown.html +6 -0
- package/prototype-kit/tests/fixtures/runtime/nested.html +7 -0
- package/prototype-kit/tests/fixtures/three-shell/branch.plan.json +30 -0
- package/prototype-kit/tests/fixtures/three-shell/pages/p0-home.html +11 -0
- package/prototype-kit/tests/fixtures/three-shell/pages/p1-reports.html +11 -0
- package/prototype-kit/tests/lib/run-discovery.mjs +87 -0
- package/prototype-kit/tests/lib/run-process.mjs +49 -0
- package/prototype-kit/tests/offline-check.mjs +41 -0
- package/prototype-kit/tests/population-sweep.mjs +94 -0
- package/prototype-kit/tests/rules-check.mjs +50 -0
- package/prototype-kit/verify-runtime.mjs +36 -0
- package/skills/12ui-design/SKILL.md +9 -3
package/open-design.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"specVersion": "1.0.0",
|
|
4
4
|
"name": "12ui-design",
|
|
5
5
|
"title": "12UI Design",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.31",
|
|
7
7
|
"description": "Explore corpus-grounded interface directions, choose one, and convert it into production-ready responsive or native artifacts with 12UI.",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "Just Every",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"command": "npx",
|
|
50
50
|
"args": [
|
|
51
51
|
"-y",
|
|
52
|
-
"@12ui/design@0.2.
|
|
52
|
+
"@12ui/design@0.2.31",
|
|
53
53
|
"mcp"
|
|
54
54
|
]
|
|
55
55
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@12ui/design",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.31",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The canonical 12ui CLI, SDK, MCP server, and visual interface design skills",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"dist/**/*",
|
|
13
|
+
"prototype-kit/**/*",
|
|
13
14
|
"skills/**/*",
|
|
14
15
|
"SKILL.md",
|
|
15
16
|
"open-design.json",
|
|
@@ -22,14 +23,19 @@
|
|
|
22
23
|
"start": "node dist/cli.js",
|
|
23
24
|
"test": "vitest",
|
|
24
25
|
"test:run": "vitest run",
|
|
26
|
+
"test:prototype": "node prototype-kit/tests/rules-check.mjs && node prototype-kit/tests/offline-check.mjs && node prototype-kit/tests/adversarial-runtime.mjs --assert",
|
|
25
27
|
"typecheck": "tsc --noEmit && tsc -p tsconfig.type-tests.json",
|
|
26
28
|
"prepublishOnly": "npm run build"
|
|
27
29
|
},
|
|
28
30
|
"dependencies": {
|
|
29
31
|
"@just-every/ensemble": "0.2.274",
|
|
30
32
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
33
|
+
"linkedom": "^0.18.12",
|
|
31
34
|
"sharp": "^0.35.3"
|
|
32
35
|
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@playwright/test": "1.59.1"
|
|
38
|
+
},
|
|
33
39
|
"devDependencies": {
|
|
34
40
|
"@types/node": "^22.19.17",
|
|
35
41
|
"typescript": "^5.9.3",
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// Deterministic analysis of an inventory: states, shared shell, nav items, dropdowns, rows, buttons, link-like texts.
|
|
2
|
+
// Usage: node analyze.mjs <inventory.json> <model.json>
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { matchPlannedPage } from './lib/navigation.mjs';
|
|
5
|
+
import { compareCanonicalShells, isWithinShell, qualifiesShellCandidate, selectOneShellPerRegion } from './lib/shells.mjs';
|
|
6
|
+
import { normalizeText as norm, slugText as slug } from './lib/text.mjs';
|
|
7
|
+
|
|
8
|
+
const [invFile, outFile] = process.argv.slice(2);
|
|
9
|
+
const inv = JSON.parse(fs.readFileSync(invFile, 'utf8'));
|
|
10
|
+
const W = 1536, H = 1024;
|
|
11
|
+
const planScreens = Array.isArray(inv.plan?.screens) ? inv.plan.screens : [];
|
|
12
|
+
const chevronDown = (l) => /\b(down|caret down|chevron down|expand)\b/.test(norm(l)) && /chevron|caret|arrow|down/.test(norm(l));
|
|
13
|
+
const chevronRight = (l) => /right/.test(norm(l)) && /chevron|arrow|caret/.test(norm(l));
|
|
14
|
+
const linkVerb = /^(view|open|see|manage|clear|add|run|export|download|learn|read|sign|get|start|try|book|browse|explore|edit|approve|defer|review|show|load|invite|create|upgrade|contact|subscribe|join|buy|shop|reserve|rsvp|register|continue|back|cancel|save|send|submit|apply|filter|sort|search|show)\b/;
|
|
15
|
+
|
|
16
|
+
// ---- states
|
|
17
|
+
const states = [];
|
|
18
|
+
for (const p of inv.pages) {
|
|
19
|
+
p.sections.forEach((s, i) => {
|
|
20
|
+
const slotMatch = s.sectionId.match(/^viewport-(\d+)-(\w+)$/);
|
|
21
|
+
const slot = slotMatch ? slotMatch[2] : String(i);
|
|
22
|
+
const screen = planScreens.find((x) => x.slot === slot);
|
|
23
|
+
const ordinal = i + 1;
|
|
24
|
+
const base = p.pageId.replace(/^p\d+-/, '');
|
|
25
|
+
states.push({
|
|
26
|
+
stateId: ordinal === 1 ? base : `${base}-${ordinal}`,
|
|
27
|
+
pageId: p.pageId,
|
|
28
|
+
pageIndex: p.planPage?.index ?? null,
|
|
29
|
+
file: p.file,
|
|
30
|
+
sectionId: s.sectionId,
|
|
31
|
+
slot,
|
|
32
|
+
ordinal,
|
|
33
|
+
title: p.planPage?.title || base.replace(/-/g, ' ').replace(/\b\p{L}/gu, (letter) => letter.toUpperCase()),
|
|
34
|
+
purpose: p.planPage?.purpose || '',
|
|
35
|
+
surfaceType: p.planPage?.surfaceType || '',
|
|
36
|
+
direction: screen?.direction || '',
|
|
37
|
+
content: screen?.content || '',
|
|
38
|
+
dependsOnSlot: screen?.dependsOnSlot || null,
|
|
39
|
+
isSource: !!p.planPage?.startsFromWinner && ordinal === 1,
|
|
40
|
+
items: s.items,
|
|
41
|
+
containers: s.containers,
|
|
42
|
+
height: s.height,
|
|
43
|
+
scriptDirection: s.scriptDirection,
|
|
44
|
+
writingMode: s.writingMode,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
if (!states.length) throw new Error('Prototype input error: no semantic viewport sections found; exports require section ids shaped like viewport-<ordinal>-<slot>');
|
|
49
|
+
const byId = Object.fromEntries(states.map((s) => [s.stateId, s]));
|
|
50
|
+
const slotToState = Object.fromEntries(states.map((s) => [s.slot, s.stateId]));
|
|
51
|
+
|
|
52
|
+
// ---- shell: elements repeated (same text/label, bbox within tol) across >= 60% of states
|
|
53
|
+
const key = (it) => (it.text ? 't:' + norm(it.text) : it.label ? 'l:' + norm(it.label) : null);
|
|
54
|
+
const TOL = 40;
|
|
55
|
+
const minStates = Math.max(2, Math.ceil(states.length * 0.4));
|
|
56
|
+
function repeatedKeys() {
|
|
57
|
+
const map = new Map();
|
|
58
|
+
for (const s of states) {
|
|
59
|
+
const seen = new Set();
|
|
60
|
+
for (const it of s.items) {
|
|
61
|
+
const k = key(it); if (!k || seen.has(k)) continue;
|
|
62
|
+
seen.add(k);
|
|
63
|
+
(map.get(k) || map.set(k, []).get(k)).push({ state: s.stateId, x: it.x, y: it.y, yb: s.height - it.y, w: it.w, h: it.h, id: it.id });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const rep = new Map();
|
|
67
|
+
for (const [k, occ] of map) {
|
|
68
|
+
if (occ.length < minStates) continue;
|
|
69
|
+
// cluster by position: count occurrences within TOL of the median
|
|
70
|
+
const mx = median(occ.map((o) => o.x)), my = median(occ.map((o) => o.y)), myb = median(occ.map((o) => o.yb));
|
|
71
|
+
const near = occ.filter((o) => Math.abs(o.x - mx) <= TOL && (Math.abs(o.y - my) <= TOL || Math.abs(o.yb - myb) <= TOL));
|
|
72
|
+
if (near.length >= minStates) rep.set(k, { x: mx, y: my, occ: near });
|
|
73
|
+
}
|
|
74
|
+
return rep;
|
|
75
|
+
}
|
|
76
|
+
function median(a) { const b = [...a].sort((x, y) => x - y); return b[Math.floor(b.length / 2)]; }
|
|
77
|
+
const repeated = repeatedKeys();
|
|
78
|
+
|
|
79
|
+
// top-level containers per state and their shell share
|
|
80
|
+
function topContainers(s) {
|
|
81
|
+
const pre = s.sectionId + '-';
|
|
82
|
+
return s.containers.filter((c) => c.id.startsWith(pre) && !c.id.slice(pre.length).includes('-') && c.children > 0);
|
|
83
|
+
}
|
|
84
|
+
function regionOf(c) {
|
|
85
|
+
if (c.w <= 0.3 * W && c.h >= 0.7 * H && c.x <= 0.05 * W) return 'sidebar';
|
|
86
|
+
if (c.w >= 0.15 * W && c.h <= 0.2 * H && c.y <= 0.08 * H) return 'header';
|
|
87
|
+
if (c.w >= 0.6 * W && c.h <= 0.45 * H && c.y + c.h >= 0.9 * H) return 'footer';
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const rejectedShellsByState = new Map();
|
|
91
|
+
for (const s of states) {
|
|
92
|
+
s.shell = [];
|
|
93
|
+
const rejected = [];
|
|
94
|
+
for (const c of topContainers(s)) {
|
|
95
|
+
const inside = s.items.filter((it) => it.chain.includes(c.id) || it.id === c.id);
|
|
96
|
+
if (inside.length < 2) continue;
|
|
97
|
+
const shellCount = inside.filter((it) => { const k = key(it); return k && repeated.has(k) && repeated.get(k).occ.some((o) => o.state === s.stateId); }).length;
|
|
98
|
+
const share = shellCount / inside.length;
|
|
99
|
+
const region = regionOf(c);
|
|
100
|
+
const semantic = ['aside', 'header', 'footer', 'nav'].includes(c.tag) || /-(sidebar|nav|navbar|header|footer|topbar|sidenav)$/.test(c.id);
|
|
101
|
+
const shell = { containerId: c.id, region, share: +share.toFixed(2), semantic, items: inside.length, box: [c.x, c.y, c.w, c.h] };
|
|
102
|
+
const hasShellEvidence = region && (share >= 0.5 || (semantic && region === 'sidebar' && inside.length >= 3));
|
|
103
|
+
if (!hasShellEvidence) continue;
|
|
104
|
+
if (qualifiesShellCandidate(shell, W)) s.shell.push(shell);
|
|
105
|
+
else rejected.push(shell);
|
|
106
|
+
}
|
|
107
|
+
rejectedShellsByState.set(s.stateId, rejected);
|
|
108
|
+
}
|
|
109
|
+
const shellAmbiguities = [];
|
|
110
|
+
for (const state of states) {
|
|
111
|
+
const { selected, dropped } = selectOneShellPerRegion(state.shell);
|
|
112
|
+
state.shell = selected;
|
|
113
|
+
for (const shell of [...dropped, ...(rejectedShellsByState.get(state.stateId) || [])]) {
|
|
114
|
+
const kept = selected.find((candidate) => candidate.region === shell.region);
|
|
115
|
+
if (!kept) continue;
|
|
116
|
+
shellAmbiguities.push({
|
|
117
|
+
state: state.stateId,
|
|
118
|
+
region: shell.region,
|
|
119
|
+
dropped: shell.containerId,
|
|
120
|
+
droppedBox: shell.box,
|
|
121
|
+
kept: kept?.containerId || null,
|
|
122
|
+
keptBox: kept?.box || null,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// canonical shell per region: prefer the source state, else the state whose container has the highest share*items
|
|
127
|
+
let regions = [...new Set(states.flatMap((s) => s.shell.map((x) => x.region)))];
|
|
128
|
+
const canonical = {};
|
|
129
|
+
// how many short texts inside a container match a planned page title (used to rank competing shells)
|
|
130
|
+
function pageMatches(s, cid) {
|
|
131
|
+
return s.items.filter((it) => it.text && it.text.length <= 32 && it.chain.includes(cid)).map((it) => matchPage(it.text)).filter(Boolean).length;
|
|
132
|
+
}
|
|
133
|
+
for (const r of regions) {
|
|
134
|
+
const cands = states.flatMap((s) => s.shell.filter((x) => x.region === r).map((x) => ({ ...x, stateId: s.stateId, isSource: s.isSource, matches: pageMatches(s, x.containerId) })));
|
|
135
|
+
if (cands.length < 2) { for (const s of states) s.shell = s.shell.filter((x) => x.region !== r); continue; }
|
|
136
|
+
cands.sort(compareCanonicalShells);
|
|
137
|
+
canonical[r] = { stateId: cands[0].stateId, containerId: cands[0].containerId, box: cands[0].box, presentIn: cands.map((c) => c.stateId), ranking: cands.map((c) => `${c.stateId}:${c.matches}m/${c.items}i/${c.share}`) };
|
|
138
|
+
}
|
|
139
|
+
regions = Object.keys(canonical);
|
|
140
|
+
|
|
141
|
+
// ---- nav items: texts inside a canonical shell container that repeat, font >= 12, excluding long copy
|
|
142
|
+
const nav = [];
|
|
143
|
+
for (const r of regions) {
|
|
144
|
+
const can = canonical[r]; const s = byId[can.stateId];
|
|
145
|
+
const texts = s.items.filter((it) => it.text && it.chain.includes(can.containerId) && it.fontSize >= 12 && it.text.length <= 32 && !it.inButton && !/\d/.test(it.text));
|
|
146
|
+
// a nav list = >= 3 texts sharing the same x (vertical list) or same y (horizontal list)
|
|
147
|
+
const groups = new Map();
|
|
148
|
+
for (const t of texts) {
|
|
149
|
+
const gx = 'x' + Math.round(t.x / 6), gy = 'y' + Math.round(t.y / 6);
|
|
150
|
+
(groups.get(gx) || groups.set(gx, []).get(gx)).push(t);
|
|
151
|
+
(groups.get(gy) || groups.set(gy, []).get(gy)).push(t);
|
|
152
|
+
}
|
|
153
|
+
const best = [...groups.values()].sort((a, b) => b.length - a.length)[0] || [];
|
|
154
|
+
if (best.length >= 3) for (const t of best) nav.push({ region: r, id: t.id, text: t.text, x: t.x, y: t.y, w: t.w, h: t.h, fontSize: t.fontSize, color: t.color, fontWeight: t.fontWeight });
|
|
155
|
+
}
|
|
156
|
+
// match nav text -> page (first state of that page) by token overlap with title / pageId
|
|
157
|
+
function matchPage(text) {
|
|
158
|
+
return matchPlannedPage(text, states.filter((state) => state.ordinal === 1))?.stateId || null;
|
|
159
|
+
}
|
|
160
|
+
for (const n of nav) { n.target = matchPage(n.text); }
|
|
161
|
+
{ const first = nav.find((n) => n.region !== 'footer'); const home = states.find((s) => s.isSource) || states[0];
|
|
162
|
+
if (first && !first.target && /^(home|overview|dashboard|start)$/.test(norm(first.text))) { first.target = home.stateId; first.fallback = 'home'; } }
|
|
163
|
+
for (const n of nav) n.holding = n.target ? null : 'holding-' + slug(n.text);
|
|
164
|
+
|
|
165
|
+
// ---- per-state interactive candidates
|
|
166
|
+
function smallestCommon(s, ids) {
|
|
167
|
+
const chains = ids.map((id) => { const it = s.items.find((x) => x.id === id); return it ? [it.id, ...it.chain] : []; });
|
|
168
|
+
for (const c of chains[0]) if (chains.every((ch) => ch.includes(c))) return c;
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
function containerItems(s, cid) { return s.items.filter((it) => it.id === cid || it.chain.includes(cid)); }
|
|
172
|
+
for (const s of states) {
|
|
173
|
+
const shellIds = new Set(s.shell.map((x) => x.containerId));
|
|
174
|
+
const inShell = (it) => it.chain.some((c) => shellIds.has(c));
|
|
175
|
+
const cands = [];
|
|
176
|
+
// buttons
|
|
177
|
+
const btnMap = new Map();
|
|
178
|
+
for (const it of s.items) if (it.inButton && it.text) btnMap.set(it.buttonId, ((btnMap.get(it.buttonId) || '') + ' ' + it.text).trim());
|
|
179
|
+
for (const [bid, text] of btnMap) {
|
|
180
|
+
const c = s.containers.find((x) => x.id === bid);
|
|
181
|
+
cands.push({ id: bid, kind: 'button', text, box: c ? [c.x, c.y, c.w, c.h] : null, inShell: !!c && [...shellIds].some((sh) => s.items.find((it) => it.buttonId === bid)?.chain.includes(sh)) });
|
|
182
|
+
}
|
|
183
|
+
// dropdown triggers: a down-chevron icon whose smallest container with a text sibling
|
|
184
|
+
for (const ic of s.items.filter((it) => (it.cls.includes('icon') || it.tag === 'img') && chevronDown(it.label))) {
|
|
185
|
+
// find nearest text to the left on the same line
|
|
186
|
+
// prefer an enclosing input/select-like container; else the nearest text to the left on the same line
|
|
187
|
+
const fieldCid = ic.chain.find((cid) => /(input|select|filter|sort|dropdown|picker|menu)(-|$)/.test(cid) && (s.containers.find((x) => x.id === cid)?.w || 0) <= 420);
|
|
188
|
+
let near = fieldCid ? containerItems(s, fieldCid).filter((t) => t.text).sort((a, b) => a.x - b.x)[0] : null;
|
|
189
|
+
if (!near) near = s.items.filter((t) => t.text && Math.abs((t.y + t.h / 2) - (ic.y + ic.h / 2)) <= Math.max(8, t.h) && t.x + t.w <= ic.x + 4 && ic.x - (t.x + t.w) <= 260).sort((a, b) => (ic.x - (a.x + a.w)) - (ic.x - (b.x + b.w)))[0];
|
|
190
|
+
if (!near) continue;
|
|
191
|
+
const cid = fieldCid || smallestCommon(s, [ic.id, near.id]);
|
|
192
|
+
const c = s.containers.find((x) => x.id === cid);
|
|
193
|
+
if (cands.some((x) => x.id === (cid || near.id))) { const ex = cands.find((x) => x.id === (cid || near.id)); ex.kind = 'dropdown'; ex.textId = near.id; continue; }
|
|
194
|
+
cands.push({ id: cid || near.id, kind: 'dropdown', text: near.text, textId: near.id, iconId: ic.id, box: c ? [c.x, c.y, c.w, c.h] : [near.x, near.y, ic.x + ic.w - near.x, Math.max(near.h, ic.h)], inShell: inShell(near) });
|
|
195
|
+
}
|
|
196
|
+
// rows: right-chevron icon -> its row container (smallest container wider than 200px holding the icon and >=1 text)
|
|
197
|
+
for (const ic of s.items.filter((it) => (it.cls.includes('icon') || it.tag === 'img') && chevronRight(it.label))) {
|
|
198
|
+
// the full row = the widest ancestor that is still row-shaped (<= 140px tall)
|
|
199
|
+
let row = null;
|
|
200
|
+
for (const cid of ic.chain) { const c = s.containers.find((x) => x.id === cid); if (c && c.h <= 140 && c.w >= 200 && containerItems(s, cid).some((t) => t.text) && (!row || c.w * c.h >= row.w * row.h)) row = c; }
|
|
201
|
+
if (!row || shellIds.has(row.id) || isWithinShell(ic.chain, shellIds)) continue;
|
|
202
|
+
if (cands.some((x) => x.id === row.id)) continue;
|
|
203
|
+
const texts = containerItems(s, row.id).filter((t) => t.text).sort((a, b) => b.fontSize - a.fontSize);
|
|
204
|
+
cands.push({ id: row.id, kind: 'row', text: texts.slice(0, 2).map((t) => t.text).join(' — '), box: [row.x, row.y, row.w, row.h], inShell: false });
|
|
205
|
+
}
|
|
206
|
+
// link-like texts
|
|
207
|
+
for (const t of s.items) {
|
|
208
|
+
if (!t.text || t.inButton || t.text.length > 40) continue;
|
|
209
|
+
if (cands.some((c) => c.id === t.id || c.textId === t.id)) continue;
|
|
210
|
+
const isNav = inShell(t) && nav.some((n) => norm(n.text) === norm(t.text));
|
|
211
|
+
if (isNav) { cands.push({ id: t.id, kind: 'nav', text: t.text, box: [t.x, t.y, t.w, t.h], inShell: true }); continue; }
|
|
212
|
+
if (linkVerb.test(norm(t.text)) && t.text.split(' ').length <= 5) cands.push({ id: t.id, kind: 'link', text: t.text, box: [t.x, t.y, t.w, t.h], inShell: inShell(t) });
|
|
213
|
+
}
|
|
214
|
+
// card titles: prominent texts inside a sizeable card container (not the page title, not chrome)
|
|
215
|
+
const pageTitleY = Math.min(...s.items.filter((t) => t.text && t.fontSize >= 24 && !inShell(t)).map((t) => t.y), 1e9);
|
|
216
|
+
for (const t of s.items) {
|
|
217
|
+
if (!t.text || t.inButton || inShell(t) || t.fontSize < 17 || t.text.length > 48 || t.y <= pageTitleY + 4) continue;
|
|
218
|
+
if (cands.some((c) => c.id === t.id || c.textId === t.id || t.chain.includes(c.id))) continue;
|
|
219
|
+
const card = t.chain.map((cid) => s.containers.find((x) => x.id === cid)).find((c) => c && c.w >= 240 && c.h >= 80 && c.h <= 600 && c.bg && c.bg !== 'rgba(0, 0, 0, 0)');
|
|
220
|
+
if (!card) continue;
|
|
221
|
+
cands.push({ id: card.id, kind: 'card', text: t.text, textId: t.id, box: [card.x, card.y, card.w, card.h], inShell: false });
|
|
222
|
+
}
|
|
223
|
+
// search / bell / avatar icons in header: utility
|
|
224
|
+
for (const ic of s.items.filter((it) => (it.cls.includes('icon') || it.tag === 'img') && /search|magnif|bell|notification|avatar|profile|menu|hamburger|user/.test(norm(it.label)) && !it.inButton)) {
|
|
225
|
+
if (cands.some((c) => c.id === ic.id)) continue;
|
|
226
|
+
cands.push({ id: ic.id, kind: 'utility', text: ic.label, box: [ic.x, ic.y, ic.w, ic.h], inShell: inShell(ic) });
|
|
227
|
+
}
|
|
228
|
+
s.candidates = cands;
|
|
229
|
+
// form fields: containers whose id mentions input/field/composer/search and have a text child (placeholder)
|
|
230
|
+
s.fields = s.containers.filter((c) => /(input|field|composer|search|textarea|prompt)(-|$)/.test(c.id) && c.w >= 120 && c.h >= 28 && c.h <= 160 && !cands.some((x) => x.id === c.id)).map((c) => {
|
|
231
|
+
const texts = containerItems(s, c.id).filter((t) => t.text);
|
|
232
|
+
return { id: c.id, placeholder: texts[0]?.text || '', box: [c.x, c.y, c.w, c.h] };
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ---- style tokens from the source (or first) state
|
|
237
|
+
const src = states.find((s) => s.isSource) || states[0];
|
|
238
|
+
function mode(arr) { const m = new Map(); for (const a of arr) if (a) m.set(a, (m.get(a) || 0) + 1); return [...m.entries()].sort((a, b) => b[1] - a[1])[0]?.[0]; }
|
|
239
|
+
const bodyTexts = src.items.filter((t) => t.text && t.fontSize >= 12 && t.fontSize <= 16);
|
|
240
|
+
const heading = src.items.filter((t) => t.text).sort((a, b) => b.fontSize - a.fontSize)[0];
|
|
241
|
+
const btnBgs = states.flatMap((s) => s.containers.filter((c) => c.tag === 'button' && c.bg && c.bg !== 'rgba(0, 0, 0, 0)').map((c) => c.bg));
|
|
242
|
+
const accentText = mode(src.items.filter((t) => t.text && t.fontWeight >= 500 && !/rgb\((\d+), \1, \1\)/.test(t.color) && t.color !== 'rgb(0, 0, 0)').map((t) => t.color));
|
|
243
|
+
const tokens = {
|
|
244
|
+
bodyFont: mode(bodyTexts.map((t) => t.fontFamily)),
|
|
245
|
+
bodySize: mode(bodyTexts.map((t) => Math.round(t.fontSize))),
|
|
246
|
+
bodyColor: mode(bodyTexts.map((t) => t.color)),
|
|
247
|
+
mutedColor: mode(src.items.filter((t) => t.text && t.fontSize < 13).map((t) => t.color)),
|
|
248
|
+
headingFont: heading?.fontFamily, headingSize: heading?.fontSize, headingColor: heading?.color, headingWeight: heading?.fontWeight,
|
|
249
|
+
accent: accentText,
|
|
250
|
+
buttonBg: mode(btnBgs),
|
|
251
|
+
cardRadius: mode(src.containers.filter((c) => c.w > 200 && c.h > 80 && c.radius !== '0px').map((c) => c.radius)),
|
|
252
|
+
cardBorder: mode(src.containers.filter((c) => c.w > 200 && c.h > 80 && !c.border.startsWith('0px')).map((c) => c.border)),
|
|
253
|
+
pageBg: src.containers.find((c) => c.x === 0 && c.y === 0 && c.w >= W - 1 && c.h >= H - 1)?.bg,
|
|
254
|
+
surfaceBg: mode(src.containers.filter((c) => c.w > 300 && c.h > 100 && c.bg !== 'rgba(0, 0, 0, 0)').map((c) => c.bg)),
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const model = {
|
|
258
|
+
run: inv.runDir,
|
|
259
|
+
plan: { concept: typeof inv.plan?.concept === 'string' ? inv.plan.concept : '', sitePurpose: typeof inv.plan?.sitePurpose === 'string' ? inv.plan.sitePurpose : '', scope: inv.plan?.scope || null },
|
|
260
|
+
tokens,
|
|
261
|
+
canonicalShell: canonical,
|
|
262
|
+
shellAmbiguities,
|
|
263
|
+
nav,
|
|
264
|
+
states: states.map((s) => ({
|
|
265
|
+
stateId: s.stateId, pageId: s.pageId, file: s.file, sectionId: s.sectionId, slot: s.slot, ordinal: s.ordinal, title: s.title, purpose: s.purpose,
|
|
266
|
+
surfaceType: s.surfaceType, direction: s.direction, content: s.content, dependsOn: s.dependsOnSlot ? slotToState[s.dependsOnSlot] : null, isSource: s.isSource,
|
|
267
|
+
shell: s.shell, candidates: s.candidates, fields: s.fields, scriptDirection: s.scriptDirection, writingMode: s.writingMode,
|
|
268
|
+
texts: s.items.filter((t) => t.text).map((t) => ({ id: t.id, text: t.text })).length,
|
|
269
|
+
})),
|
|
270
|
+
};
|
|
271
|
+
fs.writeFileSync(outFile, JSON.stringify(model, null, 1));
|
|
272
|
+
// summary
|
|
273
|
+
console.error(`states: ${states.length}; shell regions: ${regions.join(',') || 'none'}; nav items: ${nav.length} (${nav.filter((n) => n.target).length} matched to pages)`);
|
|
274
|
+
for (const s of model.states) console.error(` ${s.stateId.padEnd(28)} shell=${s.shell.map((x) => x.region).join('+') || '-'} cands=${s.candidates.length} [${['button', 'dropdown', 'row', 'link', 'nav', 'utility', 'card'].map((k) => k[0] + s.candidates.filter((c) => c.kind === k).length).join(' ')}] fields=${s.fields.length}`);
|
|
275
|
+
console.error('tokens', JSON.stringify(tokens));
|