@12ui/design 0.2.48 → 0.2.49
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-page-conversion.d.ts +4 -1
- package/dist/branch-page-conversion.d.ts.map +1 -1
- package/dist/branch-page-conversion.js +27 -17
- package/dist/branch-page-conversion.js.map +1 -1
- package/dist/branch-run-record.d.ts +2 -0
- package/dist/branch-run-record.d.ts.map +1 -1
- package/dist/branch-run-record.js.map +1 -1
- package/dist/cli-draft-command.d.ts +11 -5
- package/dist/cli-draft-command.d.ts.map +1 -1
- package/dist/cli-draft-command.js +37 -35
- package/dist/cli-draft-command.js.map +1 -1
- package/dist/cli-help/design.d.ts +10 -0
- package/dist/cli-help/design.d.ts.map +1 -1
- package/dist/cli-help/design.js +29 -0
- package/dist/cli-help/design.js.map +1 -1
- package/dist/cli-help/index.d.ts +11 -0
- package/dist/cli-help/index.d.ts.map +1 -1
- package/dist/cli-help/index.js +18 -2
- package/dist/cli-help/index.js.map +1 -1
- package/dist/cli-next-command.d.ts.map +1 -1
- package/dist/cli-next-command.js +26 -14
- package/dist/cli-next-command.js.map +1 -1
- package/dist/cli-progress.d.ts +2 -0
- package/dist/cli-progress.d.ts.map +1 -1
- package/dist/cli-progress.js +2 -0
- package/dist/cli-progress.js.map +1 -1
- package/dist/cli-redesign-command.d.ts +42 -0
- package/dist/cli-redesign-command.d.ts.map +1 -0
- package/dist/cli-redesign-command.js +520 -0
- package/dist/cli-redesign-command.js.map +1 -0
- package/dist/cli-run.d.ts.map +1 -1
- package/dist/cli-run.js +8 -0
- package/dist/cli-run.js.map +1 -1
- package/dist/cli-usage.d.ts.map +1 -1
- package/dist/cli-usage.js +8 -1
- package/dist/cli-usage.js.map +1 -1
- package/dist/draft-dispatch-attempt.d.ts +0 -7
- package/dist/draft-dispatch-attempt.d.ts.map +1 -1
- package/dist/draft-dispatch-attempt.js +1 -25
- package/dist/draft-dispatch-attempt.js.map +1 -1
- package/dist/draft-identity.d.ts +2 -0
- package/dist/draft-identity.d.ts.map +1 -1
- package/dist/draft-identity.js +1 -0
- package/dist/draft-identity.js.map +1 -1
- package/dist/draft-run.d.ts +2 -0
- package/dist/draft-run.d.ts.map +1 -1
- package/dist/draft-run.js +4 -3
- package/dist/draft-run.js.map +1 -1
- package/dist/package-client.d.ts +11 -0
- package/dist/package-client.d.ts.map +1 -1
- package/dist/package-client.js.map +1 -1
- package/dist/package-submission.d.ts +12 -0
- package/dist/package-submission.d.ts.map +1 -1
- package/dist/package-submission.js +5 -0
- package/dist/package-submission.js.map +1 -1
- package/dist/redesign-brand-asset-inventory.d.ts +71 -0
- package/dist/redesign-brand-asset-inventory.d.ts.map +1 -0
- package/dist/redesign-brand-asset-inventory.js +318 -0
- package/dist/redesign-brand-asset-inventory.js.map +1 -0
- package/dist/redesign-capture.d.ts +57 -0
- package/dist/redesign-capture.d.ts.map +1 -0
- package/dist/redesign-capture.js +181 -0
- package/dist/redesign-capture.js.map +1 -0
- package/dist/redesign-pinned-asset.d.ts +20 -0
- package/dist/redesign-pinned-asset.d.ts.map +1 -0
- package/dist/redesign-pinned-asset.js +35 -0
- package/dist/redesign-pinned-asset.js.map +1 -0
- package/dist/redesign-plan-client.d.ts +49 -0
- package/dist/redesign-plan-client.d.ts.map +1 -0
- package/dist/redesign-plan-client.js +90 -0
- package/dist/redesign-plan-client.js.map +1 -0
- package/dist/redesign-plan-identity.d.ts +8 -0
- package/dist/redesign-plan-identity.d.ts.map +1 -0
- package/dist/redesign-plan-identity.js +16 -0
- package/dist/redesign-plan-identity.js.map +1 -0
- package/dist/redesign-plan.d.ts +30 -0
- package/dist/redesign-plan.d.ts.map +1 -0
- package/dist/redesign-plan.js +99 -0
- package/dist/redesign-plan.js.map +1 -0
- package/dist/redesign-workspace.d.ts +142 -0
- package/dist/redesign-workspace.d.ts.map +1 -0
- package/dist/redesign-workspace.js +82 -0
- package/dist/redesign-workspace.js.map +1 -0
- package/dist/run-journal-commands.d.ts.map +1 -1
- package/dist/run-journal-commands.js +10 -0
- package/dist/run-journal-commands.js.map +1 -1
- package/open-design.json +2 -2
- package/package.json +2 -2
- package/prototype-kit/build.mjs +9 -3
- package/prototype-kit/lib/action-target.mjs +26 -0
- package/prototype-kit/lib/local-document.mjs +16 -0
- package/prototype-kit/lib/runtime-check.mjs +83 -19
- package/prototype-kit/lib/shell-runtime-equivalence.mjs +41 -0
- package/prototype-kit/polish/harm.mjs +27 -5
- package/prototype-kit/polish/measure.mjs +101 -5
- package/prototype-kit/polish/overrides.mjs +32 -3
- package/prototype-kit/polish/polish.mjs +23 -11
- package/prototype-kit/polish/rules/align.mjs +73 -3
- package/prototype-kit/polish/rules/left-rail.mjs +2 -3
- package/prototype-kit/polish/rules/section-breathing.mjs +0 -1
- package/prototype-kit/polish/tests/fixtures/align/expect.json +1 -1
- package/prototype-kit/polish/tests/fixtures/align/fire.html +4 -1
- package/prototype-kit/polish/tests/fixtures/align/silent.html +8 -0
- package/prototype-kit/polish/tests/fixtures/align/zoom-constrained.html +17 -0
- package/prototype-kit/polish/tests/fixtures/harm/left-rail-contained.html +16 -0
- package/prototype-kit/polish/tests/fixtures/harm/left-rail-overflow.html +16 -0
- package/prototype-kit/polish/tests/fixtures/harm/nested-actions.html +6 -0
- package/prototype-kit/polish/tests/fixtures/harm/unidentified-parent.html +3 -0
- package/prototype-kit/polish/tests/fixtures/shell-seam/fire.html +1 -1
- package/prototype-kit/polish/tests/harm-check.mjs +131 -2
- package/prototype-kit/polish/tests/rules-check.mjs +7 -2
- package/prototype-kit/runtime/proto.js +4 -1
- package/prototype-kit/tests/adversarial-runtime.mjs +3 -0
- package/prototype-kit/tests/align-rule-check.mjs +105 -0
- package/prototype-kit/tests/fixtures/interaction-runtime/review-bottom-action.html +8 -0
- package/prototype-kit/tests/offline-check.mjs +17 -1
- package/prototype-kit/tests/rules-check.mjs +67 -1
- package/prototype-kit/tests/shell-stitching-runtime.mjs +15 -8
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.49",
|
|
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.49",
|
|
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.49",
|
|
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",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"start": "node dist/cli.js",
|
|
24
24
|
"test": "vitest",
|
|
25
25
|
"test:run": "vitest run",
|
|
26
|
-
"test:prototype": "node prototype-kit/polish/tests/rules-check.mjs && node prototype-kit/polish/tests/harm-check.mjs && node prototype-kit/polish/tests/breathing-check.mjs && node prototype-kit/polish/tests/report-check.mjs && node prototype-kit/tests/rules-check.mjs && node prototype-kit/tests/offline-check.mjs && node prototype-kit/tests/shell-stitching-runtime.mjs && node prototype-kit/tests/adversarial-runtime.mjs --assert",
|
|
26
|
+
"test:prototype": "node prototype-kit/polish/tests/rules-check.mjs && node prototype-kit/polish/tests/harm-check.mjs && node prototype-kit/polish/tests/breathing-check.mjs && node prototype-kit/polish/tests/report-check.mjs && node prototype-kit/tests/rules-check.mjs && node prototype-kit/tests/offline-check.mjs && node prototype-kit/tests/shell-stitching-runtime.mjs && node prototype-kit/tests/shell-stitching-runtime.mjs --reintroduce-defect && node prototype-kit/tests/adversarial-runtime.mjs --assert",
|
|
27
27
|
"typecheck": "tsc --noEmit && tsc -p tsconfig.type-tests.json",
|
|
28
28
|
"prepublishOnly": "npm run build"
|
|
29
29
|
},
|
package/prototype-kit/build.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import fs from 'node:fs';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { parseHTML } from 'linkedom';
|
|
6
6
|
import { resolveDropdownAction } from './lib/actions.mjs';
|
|
7
|
-
import { restoreActionPointerReachability } from './lib/action-target.mjs';
|
|
7
|
+
import { restoreActionPointerReachability, selectUniqueActionOwners } from './lib/action-target.mjs';
|
|
8
8
|
import {
|
|
9
9
|
HOLDING_SHELL_ATTRIBUTE,
|
|
10
10
|
holdingBlockGeometry,
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
shellPlacementCss,
|
|
14
14
|
} from './lib/holding-composition.mjs';
|
|
15
15
|
import { injectReviewFraming } from './lib/review-framing.mjs';
|
|
16
|
+
import { removeRemoteHeadLinks } from './lib/local-document.mjs';
|
|
16
17
|
import {
|
|
17
18
|
isolateTransplantedShell,
|
|
18
19
|
rulesFor,
|
|
@@ -156,6 +157,7 @@ const report = {
|
|
|
156
157
|
unbound: [],
|
|
157
158
|
invalidDropdowns: [],
|
|
158
159
|
actionDemotions: [],
|
|
160
|
+
duplicateActionOwners: [],
|
|
159
161
|
shellSizeDelta: [],
|
|
160
162
|
shellAmbiguities: model.shellAmbiguities || [],
|
|
161
163
|
suppressedShellOverlaps: [],
|
|
@@ -234,7 +236,9 @@ function actionsFor(s) {
|
|
|
234
236
|
if (a) acts.set(c.id, a);
|
|
235
237
|
else report.counts.none++;
|
|
236
238
|
}
|
|
237
|
-
|
|
239
|
+
const normalized = selectUniqueActionOwners(loadDoc(s.file).document, acts);
|
|
240
|
+
report.duplicateActionOwners.push(...normalized.removed.map((removal) => ({ state: s.stateId, ...removal })));
|
|
241
|
+
return normalized.actions;
|
|
238
242
|
}
|
|
239
243
|
|
|
240
244
|
function bindAction(el, action) {
|
|
@@ -410,6 +414,7 @@ for (const u of units) {
|
|
|
410
414
|
const cfg = { unit: u.id, states: u.states.map((s) => s.stateId), tokens: model.tokens, contentFile: `${u.id}.json`, activeNav: activeNav?.text || null };
|
|
411
415
|
document.body.insertAdjacentHTML('beforeend', `<script id="proto-state" type="application/json">${JSON.stringify(cfg).replace(/</g, '\\u003c')}</script><script src="proto.js"></script>`);
|
|
412
416
|
if (review) injectReviewFraming(document, review);
|
|
417
|
+
removeRemoteHeadLinks(document);
|
|
413
418
|
fs.writeFileSync(path.join(outDir, outputFileOfUnit(u)), '<!doctype html>\n' + document.documentElement.outerHTML);
|
|
414
419
|
fs.writeFileSync(path.join(outDir, 'content', u.id + '.json'), JSON.stringify(content, null, 1));
|
|
415
420
|
report.units.push({
|
|
@@ -612,6 +617,7 @@ for (const u of units) {
|
|
|
612
617
|
const cfg = { unit: hslug, holding: true, holdingBands: bandDeclarations, tokens: model.tokens, contentFile: null, activeNav: navMatch?.text || null };
|
|
613
618
|
document.body.insertAdjacentHTML('beforeend', `<script id="proto-state" type="application/json">${JSON.stringify(cfg)}</script><script src="proto.js"></script>`);
|
|
614
619
|
if (review) injectReviewFraming(document, review);
|
|
620
|
+
removeRemoteHeadLinks(document);
|
|
615
621
|
fs.writeFileSync(path.join(outDir, outputFile), '<!doctype html>\n' + document.documentElement.outerHTML);
|
|
616
622
|
}
|
|
617
623
|
console.error(`holding pages: ${usedHoldings.size}`);
|
|
@@ -623,5 +629,5 @@ fs.writeFileSync(path.join(outDir, 'index.html'), `<!doctype html><meta http-equ
|
|
|
623
629
|
const manifestHoldings = review
|
|
624
630
|
? [...usedHoldings.entries()].map(([holding, title]) => [holding.replace(/\.html$/, ''), title])
|
|
625
631
|
: [...usedHoldings.entries()];
|
|
626
|
-
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, ...(report.actionDemotions.length > 0 ? { actionDemotions: report.actionDemotions } : {}), 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));
|
|
632
|
+
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, ...(report.actionDemotions.length > 0 ? { actionDemotions: report.actionDemotions } : {}), ...(report.duplicateActionOwners.length > 0 ? { duplicateActionOwners: report.duplicateActionOwners } : {}), 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));
|
|
627
633
|
console.error('counts', JSON.stringify(report.counts), 'unbound', report.unbound.length);
|
|
@@ -8,3 +8,29 @@ export function restoreActionPointerReachability(element) {
|
|
|
8
8
|
element.setAttribute('data-proto-pointer-restored', 'true');
|
|
9
9
|
return true;
|
|
10
10
|
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Give one semantic action to one rendered owner. A label response can name
|
|
14
|
+
* both a layout wrapper and one or more controls inside it. When descendants
|
|
15
|
+
* carry the same action kind, only the terminal-most elements are real action
|
|
16
|
+
* targets; retaining the wrapper adds a duplicate listener and an impossible
|
|
17
|
+
* hit-test identity. Different nested action kinds remain independent.
|
|
18
|
+
*/
|
|
19
|
+
export function selectUniqueActionOwners(document, actions) {
|
|
20
|
+
const selected = new Map(actions);
|
|
21
|
+
const removed = [];
|
|
22
|
+
for (const [wrapperId, wrapperAction] of actions) {
|
|
23
|
+
const wrapper = document.getElementById(wrapperId);
|
|
24
|
+
if (!wrapper) continue;
|
|
25
|
+
const descendants = [...wrapper.querySelectorAll('[id]')].filter((candidate) => (
|
|
26
|
+
actions.get(candidate.id)?.action === wrapperAction.action
|
|
27
|
+
));
|
|
28
|
+
if (descendants.length === 0) continue;
|
|
29
|
+
const ownerIds = descendants.filter((candidate) => ![...candidate.querySelectorAll('[id]')].some((nested) => (
|
|
30
|
+
actions.get(nested.id)?.action === wrapperAction.action
|
|
31
|
+
))).map((candidate) => candidate.id);
|
|
32
|
+
selected.delete(wrapperId);
|
|
33
|
+
removed.push({ wrapperId, ownerIds, action: wrapperAction.action });
|
|
34
|
+
}
|
|
35
|
+
return { actions: selected, removed };
|
|
36
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const REMOTE_HREF = /^(?:https?:)?\/\//iu;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Prototype files are owner-review artifacts that must open directly from
|
|
5
|
+
* disk. Remote head links turn an otherwise self-contained page into a noisy
|
|
6
|
+
* runtime failure when the review browser correctly blocks the network.
|
|
7
|
+
*/
|
|
8
|
+
export function removeRemoteHeadLinks(document) {
|
|
9
|
+
let removed = 0;
|
|
10
|
+
for (const link of document.querySelectorAll('head link[href]')) {
|
|
11
|
+
if (!REMOTE_HREF.test(link.getAttribute('href')?.trim() ?? '')) continue;
|
|
12
|
+
link.remove();
|
|
13
|
+
removed += 1;
|
|
14
|
+
}
|
|
15
|
+
return removed;
|
|
16
|
+
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
+
import {
|
|
5
|
+
compareCanonicalShellSamples,
|
|
6
|
+
SHELL_RUNTIME_GEOMETRY_EPSILON_PX,
|
|
7
|
+
} from './shell-runtime-equivalence.mjs';
|
|
8
|
+
|
|
4
9
|
function errorText(error) {
|
|
5
10
|
return error instanceof Error ? error.message : String(error);
|
|
6
11
|
}
|
|
@@ -21,7 +26,23 @@ function configuredWidths(protoDir, file) {
|
|
|
21
26
|
async function smokeInteraction(page, selector, assertion) {
|
|
22
27
|
const locator = page.locator(selector).first();
|
|
23
28
|
if (!await locator.count()) return { present: false, ok: true };
|
|
29
|
+
let reviewClearanceAdded = false;
|
|
24
30
|
try {
|
|
31
|
+
reviewClearanceAdded = await locator.evaluate((element) => {
|
|
32
|
+
const framing = document.getElementById('proto-review-framing');
|
|
33
|
+
if (!framing) return false;
|
|
34
|
+
const box = framing.getBoundingClientRect();
|
|
35
|
+
const target = element.getBoundingClientRect();
|
|
36
|
+
const overlaps = Math.min(box.right, target.right) - Math.max(box.left, target.left) > 0
|
|
37
|
+
&& Math.min(box.bottom, target.bottom) - Math.max(box.top, target.top) > 0;
|
|
38
|
+
if (!overlaps) return false;
|
|
39
|
+
const clearance = document.createElement('div');
|
|
40
|
+
clearance.setAttribute('data-proto-runtime-clearance', 'true');
|
|
41
|
+
clearance.setAttribute('aria-hidden', 'true');
|
|
42
|
+
clearance.style.cssText = `display:block;position:relative;flex:none;width:1px;height:${box.height + 36}px;pointer-events:none`;
|
|
43
|
+
document.body.appendChild(clearance);
|
|
44
|
+
return true;
|
|
45
|
+
});
|
|
25
46
|
await locator.scrollIntoViewIfNeeded();
|
|
26
47
|
// Playwright considers an element "in view" even when a fixed review
|
|
27
48
|
// panel covers every point in its box. Center it before hit-testing so
|
|
@@ -55,6 +76,10 @@ async function smokeInteraction(page, selector, assertion) {
|
|
|
55
76
|
return { present: true, ok, point, opened };
|
|
56
77
|
} catch (error) {
|
|
57
78
|
return { present: true, ok: false, error: errorText(error) };
|
|
79
|
+
} finally {
|
|
80
|
+
if (reviewClearanceAdded) {
|
|
81
|
+
await page.locator('[data-proto-runtime-clearance="true"]').evaluateAll((nodes) => nodes.forEach((node) => node.remove())).catch(() => null);
|
|
82
|
+
}
|
|
58
83
|
}
|
|
59
84
|
}
|
|
60
85
|
|
|
@@ -286,7 +311,12 @@ export async function checkRuntime({ browser, baseUrl, relativeProtoPath, protoD
|
|
|
286
311
|
const host = screen?.querySelector('.ly-anchor-canvas') || screen;
|
|
287
312
|
const hostZoom = host ? parseFloat(getComputedStyle(host).zoom) || 1 : 1;
|
|
288
313
|
const round = (value) => Math.round(value * 10) / 10;
|
|
289
|
-
const
|
|
314
|
+
const digestJson = async (value) => {
|
|
315
|
+
const bytes = new TextEncoder().encode(JSON.stringify(value));
|
|
316
|
+
const digest = await crypto.subtle.digest('SHA-256', bytes);
|
|
317
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
318
|
+
};
|
|
319
|
+
const visualSample = async (shell, declaration) => {
|
|
290
320
|
const rootBox = shell.getBoundingClientRect();
|
|
291
321
|
const markerIds = new Set(declaration.activeMarkerIds || []);
|
|
292
322
|
const visualNodes = [shell, ...shell.querySelectorAll('*')]
|
|
@@ -306,10 +336,10 @@ export async function checkRuntime({ browser, baseUrl, relativeProtoPath, protoD
|
|
|
306
336
|
label: node.getAttribute('alt') || node.getAttribute('aria-label') || '',
|
|
307
337
|
source: node.getAttribute('src') || '',
|
|
308
338
|
box: [
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
339
|
+
(box.left - rootBox.left) / hostZoom,
|
|
340
|
+
(box.top - rootBox.top) / hostZoom,
|
|
341
|
+
box.width / hostZoom,
|
|
342
|
+
box.height / hostZoom,
|
|
313
343
|
],
|
|
314
344
|
style: [
|
|
315
345
|
style.display, node === shell ? 'shell-root' : style.position,
|
|
@@ -319,9 +349,13 @@ export async function checkRuntime({ browser, baseUrl, relativeProtoPath, protoD
|
|
|
319
349
|
],
|
|
320
350
|
};
|
|
321
351
|
}).filter((node) => node.box[2] > 0 && node.box[3] > 0);
|
|
322
|
-
const
|
|
323
|
-
const
|
|
324
|
-
return
|
|
352
|
+
const appearanceNodes = visualNodes.map(({ box, ...appearance }) => appearance);
|
|
353
|
+
const diagnosticNodes = visualNodes.map((node) => ({ ...node, box: node.box.map(round) }));
|
|
354
|
+
return {
|
|
355
|
+
appearanceSignature: await digestJson({ region: declaration.region, visualNodes: appearanceNodes }),
|
|
356
|
+
diagnosticSignature: await digestJson({ region: declaration.region, visualNodes: diagnosticNodes }),
|
|
357
|
+
geometry: visualNodes.map((node) => node.box),
|
|
358
|
+
};
|
|
325
359
|
};
|
|
326
360
|
for (const declaration of declared.regions || []) {
|
|
327
361
|
const shell = document.getElementById(declaration.shellId);
|
|
@@ -329,15 +363,16 @@ export async function checkRuntime({ browser, baseUrl, relativeProtoPath, protoD
|
|
|
329
363
|
failures.push({ kind: 'shell-consistency', region: declaration.region, reason: 'missing-declared-shell' });
|
|
330
364
|
continue;
|
|
331
365
|
}
|
|
332
|
-
const
|
|
366
|
+
const sample = await visualSample(shell, declaration);
|
|
333
367
|
const shellBox = shell.getBoundingClientRect();
|
|
334
368
|
const hostBox = host?.getBoundingClientRect() || { left: 0, top: 0 };
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
369
|
+
const comparisonGeometry = [
|
|
370
|
+
(shellBox.left - hostBox.left) / hostZoom,
|
|
371
|
+
(shellBox.top - hostBox.top) / hostZoom,
|
|
372
|
+
shellBox.width / hostZoom,
|
|
373
|
+
shellBox.height / hostZoom,
|
|
340
374
|
];
|
|
375
|
+
const actualGeometry = comparisonGeometry.map(round);
|
|
341
376
|
let activeMatches = true;
|
|
342
377
|
if (declaration.active?.status === 'linked') {
|
|
343
378
|
const marker = document.getElementById(declaration.active.markerId);
|
|
@@ -364,8 +399,11 @@ export async function checkRuntime({ browser, baseUrl, relativeProtoPath, protoD
|
|
|
364
399
|
regions.push({
|
|
365
400
|
region: declaration.region,
|
|
366
401
|
shellId: declaration.shellId,
|
|
367
|
-
actualSignature,
|
|
402
|
+
actualSignature: sample.diagnosticSignature,
|
|
368
403
|
actualGeometry,
|
|
404
|
+
appearanceSignature: sample.appearanceSignature,
|
|
405
|
+
visualGeometry: sample.geometry,
|
|
406
|
+
comparisonGeometry,
|
|
369
407
|
inventorySampleSignature: declaration.canonicalSignature,
|
|
370
408
|
consistencyStatus: declaration.consistencyStatus || 'canonical',
|
|
371
409
|
activeStatus: declaration.active?.status || 'silent',
|
|
@@ -427,23 +465,48 @@ export async function checkRuntime({ browser, baseUrl, relativeProtoPath, protoD
|
|
|
427
465
|
.map((failure) => ({ file: page.file, width: viewport.width, ...failure })) || []
|
|
428
466
|
)));
|
|
429
467
|
const shellRuntimeGroups = new Map();
|
|
468
|
+
const shellConsistencyTolerances = [];
|
|
430
469
|
for (const page of pages) for (const viewport of page.viewports) {
|
|
431
470
|
if (!unitByFile.has(page.file)) continue;
|
|
432
471
|
for (const region of viewport.shellState?.regions || []) {
|
|
433
472
|
if (region.consistencyStatus !== 'canonical') continue;
|
|
434
473
|
const key = `${viewport.width}:${viewport.shellState.activeBand}:${region.region}`;
|
|
435
474
|
if (!shellRuntimeGroups.has(key)) shellRuntimeGroups.set(key, []);
|
|
436
|
-
shellRuntimeGroups.get(key).push({
|
|
475
|
+
shellRuntimeGroups.get(key).push({
|
|
476
|
+
file: page.file,
|
|
477
|
+
signature: region.actualSignature,
|
|
478
|
+
geometry: region.actualGeometry,
|
|
479
|
+
appearanceSignature: region.appearanceSignature,
|
|
480
|
+
visualGeometry: region.visualGeometry,
|
|
481
|
+
shellGeometry: region.comparisonGeometry,
|
|
482
|
+
});
|
|
437
483
|
}
|
|
438
484
|
}
|
|
439
485
|
for (const [key, entries] of shellRuntimeGroups) {
|
|
440
|
-
const
|
|
441
|
-
|
|
486
|
+
const reference = entries[0];
|
|
487
|
+
const comparisons = entries.slice(1).map((entry) => compareCanonicalShellSamples(reference, entry));
|
|
488
|
+
const equivalent = comparisons.every((comparison) => comparison.equivalent);
|
|
489
|
+
const pagesInGroup = entries.map(({ file, signature, geometry }) => ({ file, signature, geometry }));
|
|
490
|
+
if (!equivalent) shellConsistencyFailures.push({
|
|
442
491
|
kind: 'shell-consistency',
|
|
443
492
|
reason: 'prototype-shell-signature-mismatch',
|
|
444
493
|
group: key,
|
|
445
|
-
pages:
|
|
494
|
+
pages: pagesInGroup,
|
|
446
495
|
});
|
|
496
|
+
else {
|
|
497
|
+
const diagnosticFingerprints = new Set(entries.map((entry) => JSON.stringify([entry.signature, entry.geometry])));
|
|
498
|
+
if (diagnosticFingerprints.size > 1) shellConsistencyTolerances.push({
|
|
499
|
+
group: key,
|
|
500
|
+
epsilonPx: SHELL_RUNTIME_GEOMETRY_EPSILON_PX,
|
|
501
|
+
maxGeometryDelta: Math.max(0, ...comparisons.map((comparison) => comparison.maxGeometryDelta || 0)),
|
|
502
|
+
pages: pagesInGroup,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
for (const page of pages) for (const viewport of page.viewports) for (const region of viewport.shellState?.regions || []) {
|
|
507
|
+
delete region.appearanceSignature;
|
|
508
|
+
delete region.visualGeometry;
|
|
509
|
+
delete region.comparisonGeometry;
|
|
447
510
|
}
|
|
448
511
|
const activeNavigationFailures = pages.flatMap((page) => page.viewports.flatMap((viewport) => (
|
|
449
512
|
viewport.shellState?.failures?.filter((failure) => failure.kind === 'active-navigation')
|
|
@@ -458,6 +521,7 @@ export async function checkRuntime({ browser, baseUrl, relativeProtoPath, protoD
|
|
|
458
521
|
interactionFailures: pages.filter((page) => page.dropdown?.ok === false || page.modal?.ok === false).map((page) => page.file),
|
|
459
522
|
compositionFailures,
|
|
460
523
|
shellConsistencyFailures,
|
|
524
|
+
shellConsistencyTolerances,
|
|
461
525
|
activeNavigationFailures,
|
|
462
526
|
deepLinkBack,
|
|
463
527
|
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const SHELL_RUNTIME_GEOMETRY_EPSILON_PX = 0.5;
|
|
2
|
+
|
|
3
|
+
function geometryDelta(reference, candidate) {
|
|
4
|
+
if (!Array.isArray(reference) || !Array.isArray(candidate) || reference.length !== candidate.length) {
|
|
5
|
+
return Number.POSITIVE_INFINITY;
|
|
6
|
+
}
|
|
7
|
+
let maximum = 0;
|
|
8
|
+
for (let index = 0; index < reference.length; index++) {
|
|
9
|
+
const left = reference[index];
|
|
10
|
+
const right = candidate[index];
|
|
11
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
12
|
+
const nested = geometryDelta(left, right);
|
|
13
|
+
if (!Number.isFinite(nested)) return nested;
|
|
14
|
+
maximum = Math.max(maximum, nested);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (!Number.isFinite(left) || !Number.isFinite(right)) return Number.POSITIVE_INFINITY;
|
|
18
|
+
maximum = Math.max(maximum, Math.abs(left - right));
|
|
19
|
+
}
|
|
20
|
+
return maximum;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Canonical shells must retain identical visible structure and styles. Browser
|
|
25
|
+
* layout geometry may differ by at most half a CSS pixel per node; that absorbs
|
|
26
|
+
* subpixel glyph measurement noise while remaining far below displaced-shell
|
|
27
|
+
* defects.
|
|
28
|
+
*/
|
|
29
|
+
export function compareCanonicalShellSamples(reference, candidate, epsilon = SHELL_RUNTIME_GEOMETRY_EPSILON_PX) {
|
|
30
|
+
if (reference.appearanceSignature !== candidate.appearanceSignature) {
|
|
31
|
+
return { equivalent: false, reason: 'appearance', maxGeometryDelta: null };
|
|
32
|
+
}
|
|
33
|
+
const visualDelta = geometryDelta(reference.visualGeometry, candidate.visualGeometry);
|
|
34
|
+
const shellDelta = geometryDelta(reference.shellGeometry, candidate.shellGeometry);
|
|
35
|
+
const maxGeometryDelta = Math.max(visualDelta, shellDelta);
|
|
36
|
+
return {
|
|
37
|
+
equivalent: maxGeometryDelta <= epsilon,
|
|
38
|
+
reason: maxGeometryDelta <= epsilon ? 'equivalent' : 'geometry',
|
|
39
|
+
maxGeometryDelta,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
// Structure the harm guard protects, computed from two measurements of the same page
|
|
1
|
+
// Structure and interaction state the harm guard protects, computed from two measurements of the same page
|
|
2
|
+
// (before / after a rule set):
|
|
2
3
|
// misaligned — a same-kind sibling pair whose left (top) edges were exactly aligned before and are not after
|
|
3
4
|
// (an icon nudged out of its column, a nav label pulled off its list edge);
|
|
4
5
|
// spacing — a consecutive sibling gap that changed by more than GAP_TOL px, unless one side was itself cut or
|
|
5
6
|
// escaped before (a stranded footer coming home is allowed to change its gap).
|
|
7
|
+
// containment — an element newly leaves (or moves farther beyond) its direct parent;
|
|
8
|
+
// actions — a semantic action that was pointer-reachable before is no longer reachable after.
|
|
6
9
|
// Owner rule (2026-08-22): a repair that moves an icon out of its column or changes sibling spacing is harm.
|
|
10
|
+
// Owner rule (2026-08-25): no moving rule is guard-exempt, and reachable actions may never be traded away.
|
|
7
11
|
export const GAP_TOL = 6;
|
|
12
|
+
export const CONTAINMENT_TOL = 0.5;
|
|
8
13
|
|
|
9
|
-
export function structureHarm(before, after,
|
|
14
|
+
export function structureHarm(before, after, declaredGaps = new Set()) {
|
|
10
15
|
const colKey = (c) => `${c.axis}|${c.a}|${c.b}`;
|
|
11
16
|
const beforeCols = new Set((before.columns || []).map(colKey));
|
|
12
17
|
const afterCols = new Set((after.columns || []).map(colKey));
|
|
@@ -16,8 +21,25 @@ export function structureHarm(before, after, exempt = new Set(), exemptGaps = ne
|
|
|
16
21
|
const spacing = (after.gaps || []).filter((g) => {
|
|
17
22
|
const b = beforeGaps.get(`${g.axis}|${g.a}|${g.b}`);
|
|
18
23
|
// a horizontal gap inside a fitted shell child is the fit's own declared change (name … chevron), not harm
|
|
19
|
-
const declaredGap =
|
|
20
|
-
return b != null && Math.abs(g.gap - b) > GAP_TOL && !broken.has(g.a) && !broken.has(g.b) && !g.fit && !declaredGap
|
|
24
|
+
const declaredGap = declaredGaps.has(`${g.a}|${g.b}`) || declaredGaps.has(`${g.b}|${g.a}`);
|
|
25
|
+
return b != null && Math.abs(g.gap - b) > GAP_TOL && !broken.has(g.a) && !broken.has(g.b) && !g.fit && !declaredGap;
|
|
21
26
|
}).map((g) => ({ ...g, before: beforeGaps.get(`${g.axis}|${g.a}|${g.b}`) }));
|
|
22
|
-
|
|
27
|
+
const beforeContainment = new Map((before.containment || []).map((finding) => [`${finding.parent}|${finding.id}`, finding]));
|
|
28
|
+
const containment = (after.containment || []).filter((finding) => {
|
|
29
|
+
const baseline = beforeContainment.get(`${finding.parent}|${finding.id}`);
|
|
30
|
+
return Object.entries(finding.sides || {}).some(([side, px]) => px > (baseline?.sides?.[side] || 0) + CONTAINMENT_TOL);
|
|
31
|
+
}).map((finding) => ({
|
|
32
|
+
...finding,
|
|
33
|
+
before: beforeContainment.get(`${finding.parent}|${finding.id}`)?.sides || {},
|
|
34
|
+
}));
|
|
35
|
+
const afterActions = new Map((after.actions || []).map((action) => [action.key, action]));
|
|
36
|
+
const unreachableActions = (before.actions || []).filter((action) => (
|
|
37
|
+
action.reachable && afterActions.get(action.key)?.reachable !== true
|
|
38
|
+
)).map((action) => ({
|
|
39
|
+
key: action.key,
|
|
40
|
+
id: action.id,
|
|
41
|
+
kind: action.kind,
|
|
42
|
+
after: afterActions.get(action.key) || null,
|
|
43
|
+
}));
|
|
44
|
+
return { misaligned, spacing, containment, unreachableActions };
|
|
23
45
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// Polish metrics: deterministic
|
|
2
|
-
// One page → { overflow, overlap, alignment, whitespace, missingText, shift }
|
|
1
|
+
// Polish metrics: deterministic geometry + hit-test evidence, computed in the browser on a rendered page.
|
|
2
|
+
// One page → { overflow, containment, actions, overlap, alignment, whitespace, missingText, shift }
|
|
3
|
+
// with per-finding detail.
|
|
3
4
|
// Used before AND after the polish stage; the numbers are instruments, the findings are the evidence.
|
|
4
5
|
|
|
5
6
|
export const NEAR = 6; // px: an edge this close (but not equal) to another is a near-miss
|
|
@@ -37,6 +38,18 @@ export function pageMetricsScript() {
|
|
|
37
38
|
const inShell = (el) => !!el.closest('[data-proto-shell], aside, header, footer, [id*="sidebar"], [id*="-nav"]');
|
|
38
39
|
const scope = sections.length ? sections : [section];
|
|
39
40
|
const qsa = (sel) => scope.flatMap((s) => [...s.querySelectorAll(sel)]);
|
|
41
|
+
const domKey = (el) => {
|
|
42
|
+
if (el.id) return el.id;
|
|
43
|
+
const parts = [];
|
|
44
|
+
let node = el;
|
|
45
|
+
while (node?.parentElement) {
|
|
46
|
+
const parent = node.parentElement;
|
|
47
|
+
parts.unshift(`${node.tagName.toLowerCase()}:nth-child(${[...parent.children].indexOf(node) + 1})`);
|
|
48
|
+
if (parent.id) return `${parent.id}>${parts.join('>')}`;
|
|
49
|
+
node = parent;
|
|
50
|
+
}
|
|
51
|
+
return parts.join('>');
|
|
52
|
+
};
|
|
40
53
|
const leaves = qsa('.text, .paragraph, h1, h2, h3, h4, h5, h6, .icon, img, .image, .badge, .divider, button, .button')
|
|
41
54
|
.filter((el) => visible(el) && !el.closest('#proto-overlay-root') && !el.querySelector('.text, .paragraph') && !(el.matches('button, .button') && el.closest('.proto-notice, .proto-holding-block')));
|
|
42
55
|
const containers = qsa('div[id], button[id], aside[id], header[id], footer[id], nav[id], article[id], form[id], main[id]')
|
|
@@ -56,7 +69,7 @@ export function pageMetricsScript() {
|
|
|
56
69
|
}
|
|
57
70
|
return { r: 255, g: 255, b: 255, a: 1 };
|
|
58
71
|
};
|
|
59
|
-
const out = { overflow: [], overlap: [], alignment: [], whitespace: [], missingText: [], shift: [], counts: {} };
|
|
72
|
+
const out = { overflow: [], containment: [], actions: [], overlap: [], alignment: [], whitespace: [], missingText: [], shift: [], counts: {} };
|
|
60
73
|
|
|
61
74
|
// ---- overflow / clipping / escape
|
|
62
75
|
for (const el of leaves) {
|
|
@@ -88,6 +101,29 @@ export function pageMetricsScript() {
|
|
|
88
101
|
if (cut > 1.5) out.overflow.push({ kind: 'cut', px: +cut.toFixed(1), ...box });
|
|
89
102
|
}
|
|
90
103
|
|
|
104
|
+
// ---- direct parent containment for every positioned block/leaf. Text overflow alone cannot see a full-width
|
|
105
|
+
// card moved beyond its owning stack. Record existing overhangs as baseline evidence; the harm guard rejects only
|
|
106
|
+
// newly introduced or enlarged overhangs, so intentional source geometry remains untouched.
|
|
107
|
+
for (const el of qsa('[id]')) {
|
|
108
|
+
const parent = el.parentElement;
|
|
109
|
+
if (!parent || !visible(el) || !visible(parent) || parent.closest('#proto-overlay-root')) continue;
|
|
110
|
+
const r = el.getBoundingClientRect(); const p = parent.getBoundingClientRect();
|
|
111
|
+
const sides = {
|
|
112
|
+
left: Math.max(0, p.left - r.left),
|
|
113
|
+
top: Math.max(0, p.top - r.top),
|
|
114
|
+
right: Math.max(0, r.right - p.right),
|
|
115
|
+
bottom: Math.max(0, r.bottom - p.bottom),
|
|
116
|
+
};
|
|
117
|
+
const px = Math.max(sides.left, sides.top, sides.right, sides.bottom);
|
|
118
|
+
if (px > 1.5) out.containment.push({
|
|
119
|
+
id: el.id,
|
|
120
|
+
parent: domKey(parent),
|
|
121
|
+
parentId: parent.id || null,
|
|
122
|
+
px: +px.toFixed(2),
|
|
123
|
+
sides: Object.fromEntries(Object.entries(sides).filter(([, value]) => value > 1.5).map(([side, value]) => [side, +value.toFixed(2)])),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
91
127
|
// ---- overlap between unrelated leaves
|
|
92
128
|
const L = leaves.map((el) => ({ el, r: rel(isText(el) ? tightRect(el) : el.getBoundingClientRect(), el) }));
|
|
93
129
|
for (let i = 0; i < L.length; i++) for (let j = i + 1; j < L.length; j++) {
|
|
@@ -299,10 +335,70 @@ export function pageMetricsScript() {
|
|
|
299
335
|
if (contrast < 1.25 && fg.a > 0.05) out.missingText.push({ id, kind: 'invisible', contrast: +contrast.toFixed(2), src: el.textContent.trim().slice(0, 60) });
|
|
300
336
|
}
|
|
301
337
|
|
|
302
|
-
|
|
338
|
+
// ---- semantic action reachability. CSS-only polish must not change which declared controls a real pointer can
|
|
339
|
+
// reach. Probe every action/field at its own scrolled-into-view action points, not merely the first dropdown or
|
|
340
|
+
// modal that the runtime smoke test happens to exercise.
|
|
341
|
+
{
|
|
342
|
+
const DECLARED_ACTION = '[data-proto-action], [data-proto-field], [data-proto-submit-for]';
|
|
343
|
+
const NATIVE_ACTION = 'a[href], button, input:not([type="hidden"]), select, textarea, [role="button"], [role="link"], [tabindex]:not([tabindex="-1"])';
|
|
344
|
+
const ACTION = `${DECLARED_ACTION}, ${NATIVE_ACTION}`;
|
|
345
|
+
const targets = qsa(ACTION).filter((el) => (
|
|
346
|
+
visible(el)
|
|
347
|
+
&& !el.closest('#proto-overlay-root')
|
|
348
|
+
&& !el.hasAttribute('disabled')
|
|
349
|
+
&& el.getAttribute('aria-disabled') !== 'true'
|
|
350
|
+
&& !el.closest('[inert]')
|
|
351
|
+
&& !el.hasAttribute('data-proto-suppressed')
|
|
352
|
+
// A plain native child is the pointer surface of its declared wrapper, not a competing action. A nested
|
|
353
|
+
// DECLARED action remains distinct and correctly owns its own hit points.
|
|
354
|
+
&& (el.matches(DECLARED_ACTION) || !el.closest(DECLARED_ACTION))
|
|
355
|
+
));
|
|
356
|
+
for (let index = 0; index < targets.length; index++) {
|
|
357
|
+
const el = targets[index];
|
|
358
|
+
const kind = el.getAttribute('data-proto-action')
|
|
359
|
+
|| (el.hasAttribute('data-proto-field') ? 'field' : null)
|
|
360
|
+
|| (el.hasAttribute('data-proto-submit-for') ? 'submit' : null)
|
|
361
|
+
|| el.getAttribute('role')
|
|
362
|
+
|| el.tagName.toLowerCase();
|
|
363
|
+
el.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
|
|
364
|
+
const rects = [...el.getClientRects()].filter((r) => r.width > 0 && r.height > 0);
|
|
365
|
+
let point = null; let receiver = null;
|
|
366
|
+
for (const rect of rects) {
|
|
367
|
+
const insetX = Math.min(8, rect.width / 4), insetY = Math.min(8, rect.height / 4);
|
|
368
|
+
const points = [
|
|
369
|
+
[rect.left + rect.width / 2, rect.top + rect.height / 2],
|
|
370
|
+
[rect.left + insetX, rect.top + insetY],
|
|
371
|
+
[rect.right - insetX, rect.top + insetY],
|
|
372
|
+
[rect.left + insetX, rect.bottom - insetY],
|
|
373
|
+
[rect.right - insetX, rect.bottom - insetY],
|
|
374
|
+
];
|
|
375
|
+
for (const [x, y] of points) {
|
|
376
|
+
if (x < 0 || y < 0 || x >= window.innerWidth || y >= window.innerHeight) continue;
|
|
377
|
+
const hit = document.elementFromPoint(x, y);
|
|
378
|
+
const owner = hit?.closest(DECLARED_ACTION) || hit?.closest(NATIVE_ACTION);
|
|
379
|
+
if (owner === el) { point = { x: +x.toFixed(2), y: +y.toFixed(2) }; receiver = hit; break; }
|
|
380
|
+
if (!receiver && hit) receiver = hit;
|
|
381
|
+
}
|
|
382
|
+
if (point) break;
|
|
383
|
+
}
|
|
384
|
+
out.actions.push({
|
|
385
|
+
key: el.id || `${kind}:${index}`,
|
|
386
|
+
id: el.id || null,
|
|
387
|
+
kind,
|
|
388
|
+
reachable: Boolean(point),
|
|
389
|
+
...(point ? { point } : {}),
|
|
390
|
+
...(!point && receiver ? { interceptedBy: receiver.id || receiver.tagName.toLowerCase() } : {}),
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
window.scrollTo(0, 0); // effectiveBg and action probes scroll; leave the page as found
|
|
303
396
|
out.counts = {
|
|
304
397
|
leaves: leaves.length, textLeaves: leaves.filter(isText).length, containers: containers.length,
|
|
305
|
-
overflow: out.overflow.length,
|
|
398
|
+
overflow: out.overflow.length, containment: out.containment.length,
|
|
399
|
+
actions: out.actions.length, reachableActions: out.actions.filter((action) => action.reachable).length,
|
|
400
|
+
unreachableActions: out.actions.filter((action) => !action.reachable).length,
|
|
401
|
+
overlap: out.overlap.length, alignment: out.alignment.length, whitespace: out.whitespace.length,
|
|
306
402
|
columns: out.columns.length, gaps: out.gaps.length, seams: out.seams.length, narrowSeams: out.seams.filter((x) => x.gap < 48).length, documentHeight: document.documentElement.scrollHeight, offcenter: out.offcenter.length, rail: out.rail.length, missingText: out.missingText.length, shifted: out.shift.length, maxShift: out.shift.reduce((m, s) => Math.max(m, s.d), 0),
|
|
307
403
|
sectionW: sb.width, sectionH: sb.height,
|
|
308
404
|
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// Override model shared by every polish rule and the driver.
|
|
2
|
-
// An override is { id | sel, rule, reason, dx?, dy?, w?, h?, css?: {prop: value} }.
|
|
2
|
+
// An override is { id | sel, rule, reason, dx?, dy?, motion?, w?, h?, css?: {prop: value} }.
|
|
3
3
|
// id → targets #id; sel → targets an arbitrary selector (holding pages have class-only nodes)
|
|
4
4
|
// dx/dy → translate (does not disturb siblings; sums across rules)
|
|
5
|
+
// motion → `inset` means dx/dy describe the cap/evidence only; css left/top performs the nudge without a
|
|
6
|
+
// stacking-context-producing transform
|
|
5
7
|
// w/h → width/height in px (last rule wins)
|
|
6
8
|
// css → any other declarations (last rule wins per property)
|
|
7
9
|
|
|
@@ -11,8 +13,10 @@ export function mergeOverrides(list) {
|
|
|
11
13
|
if (!o || !(o.id || o.sel)) continue;
|
|
12
14
|
const key = o.sel || o.id;
|
|
13
15
|
const cur = byId.get(key) || { id: o.id, sel: o.sel, dx: 0, dy: 0, css: {}, rules: [] };
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
if (o.motion !== 'inset') {
|
|
17
|
+
cur.dx += o.dx || 0;
|
|
18
|
+
cur.dy += o.dy || 0;
|
|
19
|
+
}
|
|
16
20
|
if (o.w != null) cur.w = o.w;
|
|
17
21
|
if (o.h != null) cur.h = o.h;
|
|
18
22
|
Object.assign(cur.css, o.css || {});
|
|
@@ -40,6 +44,31 @@ export function overridesToCss(merged) {
|
|
|
40
44
|
return lines.join('\n') + '\n';
|
|
41
45
|
}
|
|
42
46
|
|
|
47
|
+
// Canonical shell regions are a cross-page contract. A page-local rule cannot safely mutate a shell descendant, the
|
|
48
|
+
// shell root, or one of its ancestors: any of those changes can make one page's shell signature diverge from peers.
|
|
49
|
+
// Partition in the browser because selectors and ancestry are facts of the rendered page.
|
|
50
|
+
export function partitionProtectedShellOverridesScript() {
|
|
51
|
+
return ({ overrides, shellIds }) => {
|
|
52
|
+
const shells = (shellIds || []).map((id) => document.getElementById(id)).filter(Boolean);
|
|
53
|
+
if (!shells.length) return { allowed: overrides, protected: [] };
|
|
54
|
+
const allowed = []; const protectedOverrides = [];
|
|
55
|
+
for (const override of overrides) {
|
|
56
|
+
let targets = [];
|
|
57
|
+
try {
|
|
58
|
+
targets = override.id ? [document.getElementById(override.id)].filter(Boolean) : [...document.querySelectorAll(override.sel)];
|
|
59
|
+
} catch {
|
|
60
|
+
protectedOverrides.push(override);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const touchesShell = targets.some((target) => shells.some((shell) => (
|
|
64
|
+
target === shell || target.contains(shell) || shell.contains(target)
|
|
65
|
+
)));
|
|
66
|
+
(touchesShell ? protectedOverrides : allowed).push(override);
|
|
67
|
+
}
|
|
68
|
+
return { allowed, protected: protectedOverrides };
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
43
72
|
function round(n) { return Math.round(n * 100) / 100; }
|
|
44
73
|
|
|
45
74
|
// In-browser: (re)place a <style id="proto-polish"> with the given css text.
|