@12ui/design 0.2.36 → 0.2.37
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/SKILL.md +1 -1
- package/dist/branch-client.d.ts +12 -0
- package/dist/branch-client.d.ts.map +1 -1
- package/dist/branch-client.js.map +1 -1
- package/dist/branch-completion.d.ts.map +1 -1
- package/dist/branch-completion.js +1 -0
- package/dist/branch-completion.js.map +1 -1
- package/dist/branch-execution-types.d.ts +3 -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 +13 -0
- package/dist/branch-execution.js.map +1 -1
- package/dist/branch-identity.d.ts +1 -1
- package/dist/branch-identity.js +1 -1
- package/dist/branch-plan.d.ts +3 -1
- package/dist/branch-plan.d.ts.map +1 -1
- package/dist/branch-plan.js +2 -0
- package/dist/branch-plan.js.map +1 -1
- package/dist/branch-run-record.d.ts +18 -0
- package/dist/branch-run-record.d.ts.map +1 -1
- package/dist/branch-run-record.js.map +1 -1
- package/dist/branch-winner.d.ts.map +1 -1
- package/dist/branch-winner.js +7 -0
- package/dist/branch-winner.js.map +1 -1
- package/dist/cli-branch-command.d.ts.map +1 -1
- package/dist/cli-branch-command.js +61 -7
- package/dist/cli-branch-command.js.map +1 -1
- package/dist/cli-help/design.d.ts +1 -1
- package/dist/cli-help/design.d.ts.map +1 -1
- package/dist/cli-help/design.js +1 -0
- package/dist/cli-help/design.js.map +1 -1
- package/dist/cli-help/index.d.ts +1 -1
- package/dist/cli-prototype-command.d.ts +3 -2
- package/dist/cli-prototype-command.d.ts.map +1 -1
- package/dist/cli-prototype-command.js +9 -3
- package/dist/cli-prototype-command.js.map +1 -1
- package/dist/cli-review-command.js +2 -2
- package/dist/cli-review-command.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/prototype-branch-contract.d.ts +67 -0
- package/dist/prototype-branch-contract.d.ts.map +1 -0
- package/dist/prototype-branch-contract.js +403 -0
- package/dist/prototype-branch-contract.js.map +1 -0
- package/dist/prototype-poststep.d.ts +27 -0
- package/dist/prototype-poststep.d.ts.map +1 -0
- package/dist/prototype-poststep.js +97 -0
- package/dist/prototype-poststep.js.map +1 -0
- package/dist/prototype-run-record.d.ts +92 -0
- package/dist/prototype-run-record.d.ts.map +1 -0
- package/dist/prototype-run-record.js +236 -0
- package/dist/prototype-run-record.js.map +1 -0
- package/dist/prototype-runner.d.ts +13 -1
- package/dist/prototype-runner.d.ts.map +1 -1
- package/dist/prototype-runner.js +79 -20
- package/dist/prototype-runner.js.map +1 -1
- package/open-design.json +2 -2
- package/package.json +1 -1
- package/prototype-kit/analyze.mjs +48 -6
- package/prototype-kit/build.mjs +249 -65
- package/prototype-kit/inventory.mjs +290 -2
- package/prototype-kit/lib/holding-composition.mjs +67 -0
- package/prototype-kit/lib/nav-active-state.mjs +67 -0
- package/prototype-kit/lib/review-framing.mjs +72 -0
- package/prototype-kit/lib/runtime-check.mjs +361 -28
- package/prototype-kit/lib/shell-consistency.mjs +68 -0
- package/prototype-kit/lib/shell-stitching.mjs +221 -0
- package/prototype-kit/runtime/proto.css +8 -3
- package/prototype-kit/tests/README.md +4 -1
- package/prototype-kit/tests/adversarial-runtime.mjs +85 -2
- package/prototype-kit/tests/fixtures/holding-runtime/PROVENANCE.md +22 -0
- package/prototype-kit/tests/fixtures/holding-runtime/band-ownership-fire.html +10 -0
- package/prototype-kit/tests/fixtures/holding-runtime/band-ownership-silent.html +10 -0
- package/prototype-kit/tests/fixtures/holding-runtime/declared-width-fire.html +11 -0
- package/prototype-kit/tests/fixtures/holding-runtime/declared-width-silent.html +11 -0
- package/prototype-kit/tests/fixtures/holding-runtime/ground-registration-mismatch-fire.html +5 -0
- package/prototype-kit/tests/fixtures/holding-runtime/ground-registration-mismatch-silent.html +5 -0
- package/prototype-kit/tests/fixtures/holding-runtime/holding-contrast-fire.html +5 -0
- package/prototype-kit/tests/fixtures/holding-runtime/holding-contrast-silent.html +5 -0
- package/prototype-kit/tests/fixtures/holding-runtime/positioned-ground-fire.html +5 -0
- package/prototype-kit/tests/fixtures/holding-runtime/positioned-ground-silent.html +5 -0
- package/prototype-kit/tests/fixtures/holding-runtime/shell-geometry-fire.html +5 -0
- package/prototype-kit/tests/fixtures/holding-runtime/shell-geometry-silent.html +5 -0
- package/prototype-kit/tests/fixtures/holding-runtime/shell-intersection-fire.html +5 -0
- package/prototype-kit/tests/fixtures/holding-runtime/shell-intersection-silent.html +5 -0
- package/prototype-kit/tests/fixtures/runtime/history.html +1 -1
- package/prototype-kit/tests/fixtures/shell-nav-runtime/PROVENANCE.md +6 -0
- package/prototype-kit/tests/fixtures/shell-nav-runtime/active-fire.html +3 -0
- package/prototype-kit/tests/fixtures/shell-nav-runtime/proto-manifest.json +20 -0
- package/prototype-kit/tests/fixtures/shell-nav-runtime/shell-fire.html +3 -0
- package/prototype-kit/tests/fixtures/shell-nav-runtime/shell-silent.html +3 -0
- package/prototype-kit/tests/fixtures/shell-nav-runtime/unlinked-silent.html +3 -0
- package/prototype-kit/tests/fixtures/three-shell/pages/p0-home.html +3 -3
- package/prototype-kit/tests/fixtures/three-shell/pages/p1-reports.html +3 -3
- package/prototype-kit/tests/lib/run-process.mjs +8 -2
- package/prototype-kit/tests/offline-check.mjs +143 -0
- package/prototype-kit/tests/population-sweep.mjs +26 -7
- package/prototype-kit/tests/rules-check.mjs +80 -0
- package/prototype-kit/verify-runtime.mjs +5 -1
package/prototype-kit/build.mjs
CHANGED
|
@@ -4,12 +4,22 @@ import fs from 'node:fs';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { parseHTML } from 'linkedom';
|
|
6
6
|
import { usableDropdownOptions } from './lib/actions.mjs';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
HOLDING_SHELL_ATTRIBUTE,
|
|
9
|
+
holdingBlockGeometry,
|
|
10
|
+
holdingBlockMarkup,
|
|
11
|
+
holdingContentArea,
|
|
12
|
+
shellPlacementCss,
|
|
13
|
+
} from './lib/holding-composition.mjs';
|
|
14
|
+
import { injectReviewFraming } from './lib/review-framing.mjs';
|
|
15
|
+
import { stitchUnitShells } from './lib/shell-stitching.mjs';
|
|
16
|
+
import { shouldSuppressDroppedShell } from './lib/shells.mjs';
|
|
8
17
|
import { normalizeText as norm, slugText as slug } from './lib/text.mjs';
|
|
9
18
|
|
|
10
19
|
const [runDir, invFile, modelFile, labelsDir, outDir] = process.argv.slice(2);
|
|
11
20
|
const inv = JSON.parse(fs.readFileSync(invFile, 'utf8'));
|
|
12
21
|
const model = JSON.parse(fs.readFileSync(modelFile, 'utf8'));
|
|
22
|
+
const review = model.review || null;
|
|
13
23
|
const here = path.dirname(new URL(import.meta.url).pathname);
|
|
14
24
|
fs.mkdirSync(path.join(outDir, 'content'), { recursive: true });
|
|
15
25
|
fs.copyFileSync(path.join(here, 'runtime/proto.js'), path.join(outDir, 'proto.js'));
|
|
@@ -31,8 +41,21 @@ for (const s of states) {
|
|
|
31
41
|
u.states.push(s);
|
|
32
42
|
} else units.push({ id: s.stateId, pageId: s.pageId, file: s.file, states: [s], title: s.title, isSource: s.isSource });
|
|
33
43
|
}
|
|
44
|
+
const reviewRealRoutes = review && Array.isArray(review.realRoutes) ? review.realRoutes : [];
|
|
45
|
+
const reviewMissingRoutes = review && Array.isArray(review.missingRoutes) ? review.missingRoutes : [];
|
|
46
|
+
const routeKey = (value) => norm(String(value || '').replace(/^p\d+-/, '').replace(/-/g, ' '));
|
|
47
|
+
const reviewRealRouteForUnit = (unit) => reviewRealRoutes.find((route) => {
|
|
48
|
+
const keys = new Set([
|
|
49
|
+
unit.id,
|
|
50
|
+
unit.pageId,
|
|
51
|
+
unit.title,
|
|
52
|
+
...unit.states.flatMap((state) => [state.stateId, state.pageId, state.title]),
|
|
53
|
+
].map(routeKey));
|
|
54
|
+
return keys.has(routeKey(route.id)) || keys.has(routeKey(route.title));
|
|
55
|
+
});
|
|
56
|
+
const outputFileOfUnit = (unit) => reviewRealRouteForUnit(unit)?.file || `${unit.id}.html`;
|
|
34
57
|
const unitOf = (stateId) => units.find((u) => u.states.some((s) => s.stateId === stateId));
|
|
35
|
-
const fileOf = (stateId) => unitOf(stateId) ? unitOf(stateId)
|
|
58
|
+
const fileOf = (stateId) => unitOf(stateId) ? outputFileOfUnit(unitOf(stateId)) : null;
|
|
36
59
|
|
|
37
60
|
// ---- labels (Luna) per state
|
|
38
61
|
const labels = {};
|
|
@@ -46,7 +69,23 @@ const holdings = new Map(); // slug -> title
|
|
|
46
69
|
for (const n of model.nav) if (n.holding) holdings.set(n.holding, n.text);
|
|
47
70
|
for (const acts of Object.values(labels)) for (const a of acts) if (a.action === 'navigate' && /^holding:/.test(a.target || '')) { const t = a.target.slice(8).trim(); holdings.set('holding-' + slug(t), t); }
|
|
48
71
|
const usedHoldings = new Map(); // slug -> title, only those actually referenced by a bound action or nav item
|
|
49
|
-
const
|
|
72
|
+
const reviewMissingByHolding = new Map(reviewMissingRoutes.map((route) => [route.holding, route]));
|
|
73
|
+
const reviewMissingForTitle = (title) => reviewMissingRoutes.find((route) => (
|
|
74
|
+
norm(route.title) === norm(title) || routeKey(route.id) === routeKey(title)
|
|
75
|
+
));
|
|
76
|
+
for (const route of reviewMissingRoutes) {
|
|
77
|
+
holdings.set(route.holding, route.title);
|
|
78
|
+
usedHoldings.set(route.holding, route.title);
|
|
79
|
+
}
|
|
80
|
+
const holdingFile = (title) => {
|
|
81
|
+
const reviewRoute = reviewMissingForTitle(title);
|
|
82
|
+
if (reviewRoute) {
|
|
83
|
+
usedHoldings.set(reviewRoute.holding, reviewRoute.title);
|
|
84
|
+
return reviewRoute.file;
|
|
85
|
+
}
|
|
86
|
+
usedHoldings.set('holding-' + slug(title), holdings.get('holding-' + slug(title)) || title);
|
|
87
|
+
return 'holding-' + slug(title) + '.html';
|
|
88
|
+
};
|
|
50
89
|
|
|
51
90
|
// ---- page docs cache
|
|
52
91
|
const docCache = new Map();
|
|
@@ -110,7 +149,21 @@ function findPill(region) {
|
|
|
110
149
|
const pills = Object.fromEntries(Object.keys(shell).map((r) => [r, findPill(r)]));
|
|
111
150
|
|
|
112
151
|
// ---- action resolution for one state
|
|
113
|
-
const report = {
|
|
152
|
+
const report = {
|
|
153
|
+
units: [],
|
|
154
|
+
unbound: [],
|
|
155
|
+
invalidDropdowns: [],
|
|
156
|
+
shellSizeDelta: [],
|
|
157
|
+
shellAmbiguities: model.shellAmbiguities || [],
|
|
158
|
+
suppressedShellOverlaps: [],
|
|
159
|
+
skippedShellTransplants: [],
|
|
160
|
+
replacedShellActions: 0,
|
|
161
|
+
shellActionRemaps: 0,
|
|
162
|
+
canonicalShellPage: model.canonicalShellPage || null,
|
|
163
|
+
shellConsistency: { changes: 0, decisions: [] },
|
|
164
|
+
holdings: [...holdings.values()],
|
|
165
|
+
counts: { navigate: 0, dropdown: 0, modal: 0, field: 0, none: 0, lunaOnly: 0, detOnly: 0, agree: 0, disagree: 0 },
|
|
166
|
+
};
|
|
114
167
|
function resolveTarget(target, currentStateId) {
|
|
115
168
|
if (!target) return null;
|
|
116
169
|
if (/^holding:/.test(target)) return holdingFile(target.slice(8).trim());
|
|
@@ -119,7 +172,10 @@ function resolveTarget(target, currentStateId) {
|
|
|
119
172
|
if (unitOf(target) === unitOf(currentStateId)) return fileOf(target) + '#' + byState[target].sectionId;
|
|
120
173
|
return fileOf(target);
|
|
121
174
|
}
|
|
122
|
-
if (holdings.has(target))
|
|
175
|
+
if (holdings.has(target)) {
|
|
176
|
+
const reviewRoute = reviewMissingByHolding.get(target) || reviewMissingForTitle(holdings.get(target));
|
|
177
|
+
return reviewRoute?.file || 'holding-' + target.replace(/^holding-/, '') + '.html';
|
|
178
|
+
}
|
|
123
179
|
// an invented id like "reports" -> a holding page titled "Reports"
|
|
124
180
|
if (/^[a-z][a-z0-9-]{2,40}$/.test(target)) return holdingFile(target.split('-').map((w) => w[0].toUpperCase() + w.slice(1)).join(' '));
|
|
125
181
|
return null;
|
|
@@ -193,7 +249,34 @@ for (const u of units) {
|
|
|
193
249
|
const allActions = {};
|
|
194
250
|
const content = {};
|
|
195
251
|
let fieldCount = 0;
|
|
196
|
-
const
|
|
252
|
+
const recipientPage = inv.pages.find((page) => page.pageId === u.pageId);
|
|
253
|
+
const donorPage = inv.pages.find((page) => page.pageId === model.canonicalShellPage?.pageId);
|
|
254
|
+
const donorState = states.find((state) => state.pageId === donorPage?.pageId);
|
|
255
|
+
if (!recipientPage) throw new Error(`Prototype shell consistency cannot resolve recipient page for ${u.id}.`);
|
|
256
|
+
const stitched = donorPage && donorState
|
|
257
|
+
? stitchUnitShells({
|
|
258
|
+
document,
|
|
259
|
+
donorDocument: loadDoc(donorState.file).document,
|
|
260
|
+
donorPage,
|
|
261
|
+
recipientPage,
|
|
262
|
+
unit: u,
|
|
263
|
+
canonicalShell: model.canonicalShell,
|
|
264
|
+
navRows,
|
|
265
|
+
rulesFor,
|
|
266
|
+
styleText,
|
|
267
|
+
})
|
|
268
|
+
: { extraCss: [], bands: [], decisions: [], changes: 0 };
|
|
269
|
+
extraCss.push(...stitched.extraCss);
|
|
270
|
+
report.shellConsistency.changes += stitched.changes;
|
|
271
|
+
report.shellConsistency.decisions.push(...stitched.decisions.map((decision) => ({ unit: u.id, ...decision })));
|
|
272
|
+
for (const decision of stitched.decisions.filter((candidate) => candidate.reason.startsWith('incompatible-') || candidate.reason.startsWith('missing-'))) {
|
|
273
|
+
report.skippedShellTransplants.push({ unit: u.id, region: decision.region, reason: decision.reason });
|
|
274
|
+
}
|
|
275
|
+
const activeRegionFact = stitched.bands.flatMap((band) => band.regions)
|
|
276
|
+
.find((region) => region.active?.status === 'linked') || null;
|
|
277
|
+
const activeNav = activeRegionFact
|
|
278
|
+
? navRows.find((row) => row.region === activeRegionFact.region && norm(row.text) === norm(activeRegionFact.active.navText))
|
|
279
|
+
: navRows.find((row) => row.target && unitOf(row.target)?.id === u.id);
|
|
197
280
|
|
|
198
281
|
for (const s of u.states) {
|
|
199
282
|
const section = document.getElementById(s.sectionId);
|
|
@@ -205,23 +288,6 @@ for (const u of units) {
|
|
|
205
288
|
report.suppressedShellOverlaps.push({ state: s.stateId, region: ambiguity.region, dropped: ambiguity.dropped, kept: ambiguity.kept });
|
|
206
289
|
}
|
|
207
290
|
}
|
|
208
|
-
// shell transplant
|
|
209
|
-
for (const sh of s.shell) {
|
|
210
|
-
const can = shell[sh.region]; if (!can) continue;
|
|
211
|
-
const own = document.getElementById(sh.containerId);
|
|
212
|
-
if (!own) continue;
|
|
213
|
-
if (can.containerId === sh.containerId) continue; // this is the canonical page
|
|
214
|
-
if (!shouldTransplantShell(sh.box, can.box)) {
|
|
215
|
-
report.skippedShellTransplants.push({ state: s.stateId, region: sh.region, own: sh.box.slice(2), canonical: can.box.slice(2) });
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
// keep this page's own layout box (it may be in normal flow); swap in the canonical children + background
|
|
219
|
-
own.innerHTML = can.inner;
|
|
220
|
-
own.setAttribute('data-proto-shell', can.containerId);
|
|
221
|
-
extraCss.push(can.css);
|
|
222
|
-
extraCss.push(`#${sh.containerId}{${can.background ? `background:${can.background}!important;` : ''}overflow:visible!important;z-index:5}`);
|
|
223
|
-
if (Math.abs(sh.box[2] - can.box[2]) > 2 || Math.abs(sh.box[3] - can.box[3]) > 2) report.shellSizeDelta.push({ state: s.stateId, region: sh.region, own: sh.box.slice(2), canonical: can.box.slice(2) });
|
|
224
|
-
}
|
|
225
291
|
// actions
|
|
226
292
|
const acts = stateActions.get(s.stateId);
|
|
227
293
|
for (const [id, a] of acts) {
|
|
@@ -308,52 +374,174 @@ for (const u of units) {
|
|
|
308
374
|
head.insertAdjacentHTML('beforeend', `<style id="proto-extra">${extraCss.join('\n')}</style><link rel="stylesheet" href="proto.css">`);
|
|
309
375
|
const cfg = { unit: u.id, states: u.states.map((s) => s.stateId), tokens: model.tokens, contentFile: `${u.id}.json`, activeNav: activeNav?.text || null };
|
|
310
376
|
document.body.insertAdjacentHTML('beforeend', `<script id="proto-state" type="application/json">${JSON.stringify(cfg).replace(/</g, '\\u003c')}</script><script src="proto.js"></script>`);
|
|
311
|
-
|
|
377
|
+
if (review) injectReviewFraming(document, review);
|
|
378
|
+
fs.writeFileSync(path.join(outDir, outputFileOfUnit(u)), '<!doctype html>\n' + document.documentElement.outerHTML);
|
|
312
379
|
fs.writeFileSync(path.join(outDir, 'content', u.id + '.json'), JSON.stringify(content, null, 1));
|
|
313
|
-
report.units.push({
|
|
380
|
+
report.units.push({
|
|
381
|
+
id: u.id,
|
|
382
|
+
states: u.states.map((s) => s.stateId),
|
|
383
|
+
actions: Object.keys(allActions).length,
|
|
384
|
+
fields: fieldCount,
|
|
385
|
+
texts: Object.keys(content).length,
|
|
386
|
+
activeNav: activeNav?.text || null,
|
|
387
|
+
shellBands: stitched.bands,
|
|
388
|
+
shellDecisions: stitched.decisions,
|
|
389
|
+
});
|
|
314
390
|
console.error(`${u.id}.html actions=${Object.keys(allActions).length} fields=${fieldCount} texts=${Object.keys(content).length} active=${activeNav?.text || '-'}`);
|
|
315
391
|
}
|
|
316
392
|
|
|
317
|
-
// ---- holding pages:
|
|
393
|
+
// ---- holding pages: fresh per-band composition from measured shells + neutral surface + holding block
|
|
318
394
|
{
|
|
319
395
|
const primaryRegion = model.canonicalShell.sidebar ? 'sidebar' : model.canonicalShell.header ? 'header' : Object.keys(model.canonicalShell)[0];
|
|
320
396
|
const can = primaryRegion ? shell[primaryRegion] : null;
|
|
321
397
|
const base = can ? byState[can.stateId] : (states.find((s) => s.isSource) || states[0]);
|
|
322
398
|
const baseSec = invSection(base);
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const [x, y, w, h] = sh.box;
|
|
327
|
-
if (sh.region === 'sidebar') { area.x = Math.max(area.x, x + w); }
|
|
328
|
-
if (sh.region === 'header') { area.y = Math.max(area.y, y + h); }
|
|
329
|
-
if (sh.region === 'footer') { area.h = Math.min(area.h, y); }
|
|
399
|
+
const basePage = inv.pages.find((page) => page.pageId === base.pageId);
|
|
400
|
+
if (!basePage?.bands?.length) {
|
|
401
|
+
throw new Error(`Holding composition requires measured band inventory for ${base.pageId}.`);
|
|
330
402
|
}
|
|
331
|
-
|
|
332
|
-
// anchor the block where the page's own title sits: the top-most large text in the content area
|
|
333
|
-
const heading = baseSec.items.filter((t) => t.text && t.x >= area.x && t.fontSize >= 20 && t.y < area.y + 200).sort((a, b) => a.y - b.y || a.x - b.x)[0]
|
|
334
|
-
|| baseSec.items.filter((t) => t.text && t.x >= area.x).sort((a, b) => b.fontSize - a.fontSize)[0];
|
|
335
|
-
const pad = Math.min(160, Math.max(32, Math.round((heading?.x || area.x + 40) - area.x)));
|
|
336
|
-
const top = Math.min(200, Math.max(40, Math.round(heading?.y || 60)));
|
|
403
|
+
const sourceState = states.find((state) => state.isSource) || states[0];
|
|
337
404
|
for (const n of navRows) if (n.holding) holdingFile(n.text);
|
|
338
405
|
for (const [hslug, htitle] of usedHoldings) {
|
|
339
406
|
const { document } = loadDoc(base.file);
|
|
340
|
-
for (const sec of [...document.querySelectorAll('section[id^="viewport-"]')]) if (/^viewport-\d+-[A-Za-z0-9]+$/.test(sec.id) && sec.id !== base.sectionId) sec.remove();
|
|
341
407
|
for (const sc of [...document.querySelectorAll('script')]) sc.remove();
|
|
342
|
-
const section = document.getElementById(base.sectionId);
|
|
343
|
-
const keepIds = new Set(base.shell.map((x) => x.containerId));
|
|
344
|
-
const bgIds = new Set(baseSec.containers.filter((c) => c.x === 0 && c.y === 0 && c.w >= 1535 && c.children === 0).map((c) => c.id));
|
|
345
|
-
for (const child of [...section.children]) if (!keepIds.has(child.id) && !bgIds.has(child.id)) child.remove();
|
|
346
408
|
const navMatch = navRows.find((n) => n.holding === hslug);
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
409
|
+
const reviewRoute = reviewMissingByHolding.get(hslug);
|
|
410
|
+
const outputFile = reviewRoute?.file || hslug + '.html';
|
|
411
|
+
const reviewBackRoute = review?.entry?.file === outputFile
|
|
412
|
+
? reviewRealRoutes[0]
|
|
413
|
+
: review?.entry;
|
|
414
|
+
const backTarget = reviewBackRoute?.file || fileOf(sourceState.stateId);
|
|
415
|
+
const backTitle = reviewBackRoute?.title || sourceState.title;
|
|
416
|
+
const extraCss = [];
|
|
417
|
+
const bandDeclarations = [];
|
|
418
|
+
const bandRoots = [...document.querySelectorAll('.ly-band[data-ly-band]')];
|
|
419
|
+
const declared = bandRoots.length > 0
|
|
420
|
+
? bandRoots.map((root) => ({ key: root.getAttribute('data-ly-band'), root }))
|
|
421
|
+
: [{ key: 'native', root: document.body }];
|
|
422
|
+
|
|
423
|
+
for (const { key, root } of declared) {
|
|
424
|
+
const screen = root.querySelector('.generated-screen') || (root === document.body ? root : null);
|
|
425
|
+
if (!screen) throw new Error(`Holding composition requires a screen root in band ${key}.`);
|
|
426
|
+
const band = basePage.bands.find((candidate) => candidate.key === key);
|
|
427
|
+
if (!band) {
|
|
428
|
+
throw new Error(`Holding composition has no measured inventory for declared band ${key} on ${base.pageId}.`);
|
|
429
|
+
}
|
|
430
|
+
if (!Array.isArray(band.hostOrigins) || band.hostOrigins.length === 0) {
|
|
431
|
+
throw new Error(`Holding composition has no measured viewport host origins for declared band ${key} on ${base.pageId}.`);
|
|
432
|
+
}
|
|
433
|
+
const host = screen.querySelector('.ly-anchor-canvas') || screen;
|
|
434
|
+
if (!host) throw new Error(`Holding composition requires a declared composition host in band ${key}.`);
|
|
435
|
+
|
|
436
|
+
const regions = new Set((band.shells || []).map((candidate) => candidate.region));
|
|
437
|
+
const canonicalBands = new Map();
|
|
438
|
+
for (const [region, canonical] of Object.entries(shell)) {
|
|
439
|
+
const canonicalState = byState[canonical.stateId];
|
|
440
|
+
const canonicalPage = inv.pages.find((page) => page.pageId === canonicalState.pageId);
|
|
441
|
+
const canonicalBand = canonicalPage?.bands?.find((candidate) => candidate.key === key) || null;
|
|
442
|
+
canonicalBands.set(region, { canonicalState, canonicalBand });
|
|
443
|
+
if (canonicalBand?.shells?.some((candidate) => candidate.region === region)) regions.add(region);
|
|
444
|
+
}
|
|
445
|
+
const placedShells = [...regions].map((region) => {
|
|
446
|
+
const canonical = canonicalBands.get(region);
|
|
447
|
+
const canonicalCandidate = canonical?.canonicalBand?.shells?.find((candidate) => candidate.region === region);
|
|
448
|
+
const baseCandidate = band.shells.find((candidate) => candidate.region === region);
|
|
449
|
+
const candidate = canonicalCandidate || baseCandidate;
|
|
450
|
+
const donorState = canonicalCandidate ? canonical.canonicalState : base;
|
|
451
|
+
if (!candidate || !donorState) {
|
|
452
|
+
throw new Error(`Holding composition cannot resolve measured ${region} shell geometry in band ${key}.`);
|
|
453
|
+
}
|
|
454
|
+
const { document: donorDocument } = loadDoc(donorState.file);
|
|
455
|
+
const donorRoots = [...donorDocument.querySelectorAll('.ly-band[data-ly-band]')];
|
|
456
|
+
const donorRoot = donorRoots.length > 0
|
|
457
|
+
? donorRoots.find((candidateRoot) => candidateRoot.getAttribute('data-ly-band') === key)
|
|
458
|
+
: donorDocument.body;
|
|
459
|
+
const element = donorDocument.getElementById(candidate.containerId);
|
|
460
|
+
if (!donorRoot || !element || !donorRoot.contains(element)) {
|
|
461
|
+
throw new Error(`Holding composition cannot find declared ${region} shell ${candidate.containerId} in band ${key}.`);
|
|
462
|
+
}
|
|
463
|
+
const ancestorIds = [];
|
|
464
|
+
for (let ancestor = element.parentElement; ancestor && ancestor !== donorRoot; ancestor = ancestor.parentElement) {
|
|
465
|
+
if (ancestor.id) ancestorIds.push(ancestor.id);
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
...candidate,
|
|
469
|
+
region,
|
|
470
|
+
html: element.outerHTML,
|
|
471
|
+
css: rulesFor(styleText(donorDocument), candidate.containerId),
|
|
472
|
+
ancestorIds,
|
|
473
|
+
};
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
if (host !== screen) {
|
|
477
|
+
for (const child of [...screen.children]) if (child !== host) child.remove();
|
|
478
|
+
}
|
|
479
|
+
for (const child of [...host.children]) child.remove();
|
|
480
|
+
const parentShell = (placed) => placedShells.find((candidate) => (
|
|
481
|
+
candidate !== placed && placed.ancestorIds.includes(candidate.containerId)
|
|
482
|
+
));
|
|
483
|
+
for (const placed of placedShells.filter((candidate) => !parentShell(candidate))) {
|
|
484
|
+
host.insertAdjacentHTML('beforeend', placed.html);
|
|
485
|
+
}
|
|
486
|
+
for (const placed of placedShells) {
|
|
487
|
+
const element = document.getElementById(placed.containerId);
|
|
488
|
+
if (!element || !host.contains(element)) {
|
|
489
|
+
throw new Error(`Holding composition failed to retain ${placed.region} shell ${placed.containerId} in band ${key}.`);
|
|
490
|
+
}
|
|
491
|
+
element.setAttribute(HOLDING_SHELL_ATTRIBUTE, placed.region);
|
|
492
|
+
if (placed.css) extraCss.push(placed.css);
|
|
493
|
+
const parent = parentShell(placed);
|
|
494
|
+
const box = parent
|
|
495
|
+
? [placed.box[0] - parent.box[0], placed.box[1] - parent.box[1], placed.box[2], placed.box[3]]
|
|
496
|
+
: placed.box;
|
|
497
|
+
extraCss.push(shellPlacementCss({ ...placed, box }));
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const area = holdingContentArea({ width: band.width, height: band.height, shells: placedShells });
|
|
501
|
+
const heading = band.heading || baseSec.items
|
|
502
|
+
.filter((item) => item.text && item.x >= area.left && item.fontSize >= 20 && item.y < area.top + 200)
|
|
503
|
+
.sort((left, right) => left.y - right.y || left.x - right.x)[0]
|
|
504
|
+
|| baseSec.items.filter((item) => item.text && item.x >= area.left)
|
|
505
|
+
.sort((left, right) => right.fontSize - left.fontSize)[0];
|
|
506
|
+
const geometry = holdingBlockGeometry({ area, heading, viewportWidth: band.viewportWidth });
|
|
507
|
+
const purpose = `${htitle} is planned, but it is not included in this prototype yet.`;
|
|
508
|
+
host.insertAdjacentHTML('beforeend', holdingBlockMarkup({
|
|
509
|
+
title: htitle,
|
|
510
|
+
purpose,
|
|
511
|
+
backTarget,
|
|
512
|
+
backTitle,
|
|
513
|
+
geometry,
|
|
514
|
+
heading,
|
|
515
|
+
tokens: model.tokens,
|
|
516
|
+
textColor: band.readableTextColor,
|
|
517
|
+
mobile: band.viewportWidth <= 600,
|
|
518
|
+
}));
|
|
519
|
+
const holdingSurfaces = root.querySelectorAll('[data-proto-holding-surface="true"]');
|
|
520
|
+
if (holdingSurfaces.length !== 1) {
|
|
521
|
+
throw new Error(`Holding composition requires exactly one surface in band ${key}; found ${holdingSurfaces.length}.`);
|
|
522
|
+
}
|
|
523
|
+
const hostId = host.id || `proto-holding-${key}-host`;
|
|
524
|
+
if (!host.id) host.id = hostId;
|
|
525
|
+
host.setAttribute('data-proto-holding-band', key);
|
|
526
|
+
if (band.groundRegistration === 'positioned-scene') {
|
|
527
|
+
const selector = bandRoots.length > 0
|
|
528
|
+
? `.ly-band[data-ly-band="${key}"]>.generated-screen`
|
|
529
|
+
: document.querySelector('.generated-screen') ? '.generated-screen' : 'body';
|
|
530
|
+
extraCss.push(`${selector}{background:${band.neutralSurface}!important;background-image:none!important}`);
|
|
531
|
+
extraCss.push(`${selector}::before,${selector}::after{content:none!important;display:none!important;background:none!important;border:0!important}`);
|
|
532
|
+
screen.setAttribute('data-proto-ground-neutralized', 'positioned-scene');
|
|
533
|
+
}
|
|
534
|
+
bandDeclarations.push({
|
|
535
|
+
key,
|
|
536
|
+
viewportWidth: band.viewportWidth,
|
|
537
|
+
hostOrigins: band.hostOrigins,
|
|
538
|
+
shellIds: placedShells.map((placed) => placed.containerId),
|
|
539
|
+
shellBoxes: placedShells.map((placed) => ({ id: placed.containerId, box: placed.box })),
|
|
540
|
+
groundRegistration: band.groundRegistration,
|
|
541
|
+
neutralSurface: band.neutralSurface,
|
|
542
|
+
});
|
|
356
543
|
}
|
|
544
|
+
|
|
357
545
|
if (navMatch) {
|
|
358
546
|
const p = pills[navMatch.region];
|
|
359
547
|
if (p && p.own.id !== navMatch.id) {
|
|
@@ -363,14 +551,6 @@ for (const u of units) {
|
|
|
363
551
|
extraCss.push(`#${p.own.id}{color:${curText.color}!important;font-weight:${curText.fontWeight}!important}`);
|
|
364
552
|
}
|
|
365
553
|
}
|
|
366
|
-
const purpose = navMatch ? `${htitle} is part of the planned product but was not one of the screens in this branch run.` : `${htitle} opens here in the full product. It was not one of the screens in this branch run.`;
|
|
367
|
-
const block = `<div class="proto-holding-block" style="left:${area.x + pad}px;top:${top}px;width:${Math.min(area.w - area.x - pad * 2, 1100)}px">
|
|
368
|
-
<h1 class="proto-holding-title" style="font-family:'${heading?.fontFamily || model.tokens.headingFont}',sans-serif;font-size:${heading?.fontSize || model.tokens.headingSize || 28}px;font-weight:${heading?.fontWeight || 600};color:${heading?.color || model.tokens.headingColor}">${esc(htitle)}</h1>
|
|
369
|
-
<p class="proto-holding-sub">${esc(purpose)}</p>
|
|
370
|
-
<div class="proto-holding-cards">${[1, 2, 3].map((i) => `<div class="proto-holding-card"><p class="proto-holding-card-title">${esc(htitle)} ${i}</p><div class="proto-holding-card-line"></div><div class="proto-holding-card-line short"></div><div class="proto-holding-card-line"></div></div>`).join('')}</div>
|
|
371
|
-
<p class="proto-holding-note">Prototype holding page · <span class="proto-clickable" data-proto-action="navigate" data-proto-target="${fileOf((states.find((s) => s.isSource) || states[0]).stateId)}">Back to ${esc((states.find((s) => s.isSource) || states[0]).title)}</span></p>
|
|
372
|
-
</div>`;
|
|
373
|
-
section.insertAdjacentHTML('beforeend', block);
|
|
374
554
|
for (const n of navRows) {
|
|
375
555
|
const el = document.getElementById(n.rowId) || document.getElementById(n.id);
|
|
376
556
|
if (!el) continue;
|
|
@@ -379,15 +559,19 @@ for (const u of units) {
|
|
|
379
559
|
}
|
|
380
560
|
document.title = htitle;
|
|
381
561
|
document.querySelector('head').insertAdjacentHTML('beforeend', `<style id="proto-extra">${extraCss.join('\n')}</style><link rel="stylesheet" href="proto.css">`);
|
|
382
|
-
const cfg = { unit: hslug, holding: true, tokens: model.tokens, contentFile: null, activeNav: navMatch?.text || null };
|
|
562
|
+
const cfg = { unit: hslug, holding: true, holdingBands: bandDeclarations, tokens: model.tokens, contentFile: null, activeNav: navMatch?.text || null };
|
|
383
563
|
document.body.insertAdjacentHTML('beforeend', `<script id="proto-state" type="application/json">${JSON.stringify(cfg)}</script><script src="proto.js"></script>`);
|
|
384
|
-
|
|
564
|
+
if (review) injectReviewFraming(document, review);
|
|
565
|
+
fs.writeFileSync(path.join(outDir, outputFile), '<!doctype html>\n' + document.documentElement.outerHTML);
|
|
385
566
|
}
|
|
386
567
|
console.error(`holding pages: ${usedHoldings.size}`);
|
|
387
568
|
}
|
|
388
569
|
|
|
389
570
|
// index -> source
|
|
390
|
-
const home = fileOf((states.find((s) => s.isSource) || states[0]).stateId);
|
|
571
|
+
const home = review?.entry?.file || fileOf((states.find((s) => s.isSource) || states[0]).stateId);
|
|
391
572
|
fs.writeFileSync(path.join(outDir, 'index.html'), `<!doctype html><meta http-equiv="refresh" content="0;url=${home}"><a href="${home}">${home}</a>`);
|
|
392
|
-
|
|
573
|
+
const manifestHoldings = review
|
|
574
|
+
? [...usedHoldings.entries()].map(([holding, title]) => [holding.replace(/\.html$/, ''), title])
|
|
575
|
+
: [...usedHoldings.entries()];
|
|
576
|
+
fs.writeFileSync(path.join(outDir, 'proto-manifest.json'), JSON.stringify({ units: report.units, nav: navRows.map((n) => ({ text: n.text, target: n.target, holding: n.holding, rowId: n.rowId })), holdings: manifestHoldings, counts: report.counts, unbound: report.unbound, invalidDropdowns: report.invalidDropdowns, shellSizeDelta: report.shellSizeDelta, shellAmbiguities: report.shellAmbiguities, suppressedShellOverlaps: report.suppressedShellOverlaps, skippedShellTransplants: report.skippedShellTransplants, replacedShellActions: report.replacedShellActions, shellActionRemaps: report.shellActionRemaps, scriptDirections: states.map(({ stateId, scriptDirection, writingMode }) => ({ stateId, scriptDirection, writingMode })), canonicalShell: model.canonicalShell, canonicalShellPage: report.canonicalShellPage, shellConsistency: report.shellConsistency, ...(review ? { review } : {}) }, null, 1));
|
|
393
577
|
console.error('counts', JSON.stringify(report.counts), 'unbound', report.unbound.length);
|