@12ui/design 0.2.47 → 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-execution.js +2 -2
- package/dist/branch-execution.js.map +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-branch-command.d.ts +2 -0
- package/dist/cli-branch-command.d.ts.map +1 -1
- package/dist/cli-branch-command.js +3 -0
- package/dist/cli-branch-command.js.map +1 -1
- package/dist/cli-convert-command.js +2 -2
- package/dist/cli-convert-command.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 +21 -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-help/meta.d.ts +10 -0
- package/dist/cli-help/meta.d.ts.map +1 -1
- package/dist/cli-help/meta.js +20 -0
- package/dist/cli-help/meta.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 +16 -0
- package/dist/cli-run.js.map +1 -1
- package/dist/cli-spend-command.d.ts +20 -0
- package/dist/cli-spend-command.d.ts.map +1 -0
- package/dist/cli-spend-command.js +46 -0
- package/dist/cli-spend-command.js.map +1 -0
- package/dist/cli-usage.d.ts.map +1 -1
- package/dist/cli-usage.js +9 -1
- package/dist/cli-usage.js.map +1 -1
- package/dist/conversion-receipt-store.d.ts +2 -2
- package/dist/conversion-receipt-store.d.ts.map +1 -1
- package/dist/conversion-receipt-store.js +11 -5
- package/dist/conversion-receipt-store.js.map +1 -1
- package/dist/conversion-receipt.d.ts +2 -3
- package/dist/conversion-receipt.d.ts.map +1 -1
- package/dist/conversion-receipt.js +17 -12
- package/dist/conversion-receipt.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/prototype-label-artifact.d.ts +29 -0
- package/dist/prototype-label-artifact.d.ts.map +1 -0
- package/dist/prototype-label-artifact.js +104 -0
- package/dist/prototype-label-artifact.js.map +1 -0
- package/dist/prototype-runner.d.ts +1 -0
- package/dist/prototype-runner.d.ts.map +1 -1
- package/dist/prototype-runner.js +18 -2
- package/dist/prototype-runner.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 +16 -0
- package/dist/run-journal-commands.js.map +1 -1
- package/dist/spend-client.d.ts +97 -0
- package/dist/spend-client.d.ts.map +1 -0
- package/dist/spend-client.js +31 -0
- package/dist/spend-client.js.map +1 -0
- package/dist/spend-report-lines.d.ts +4 -0
- package/dist/spend-report-lines.d.ts.map +1 -0
- package/dist/spend-report-lines.js +131 -0
- package/dist/spend-report-lines.js.map +1 -0
- 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
|
@@ -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.
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// Polish stage: deterministic visual repair of a built prototype, one page at a time (pages are independent → parallel).
|
|
3
3
|
// Usage: node polish/polish.mjs <protoDir> --out <outDir> [--inventory inventory.json --model model.json] [--rules a,b] [--concurrency 4] [--report polish-report.json]
|
|
4
4
|
//
|
|
5
|
-
// For every real page: measure → run rules
|
|
6
|
-
// harm guard (reject any rule that adds
|
|
5
|
+
// For every real page: measure → run rules in-browser → apply overrides live → re-measure →
|
|
6
|
+
// harm guard (reject any rule that adds geometry/containment harm, loses a reachable action, or exceeds its cap) →
|
|
7
7
|
// write polish/<unit>.css + <link> into the page copy. Nothing is redrawn; only per-id CSS overrides are emitted.
|
|
8
8
|
import fs from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
@@ -11,7 +11,7 @@ import { chromium } from '@playwright/test';
|
|
|
11
11
|
import { startStaticServer } from '../lib/static-server.mjs';
|
|
12
12
|
import { measurePage, sourceItemsFor, settleFonts } from './measure.mjs';
|
|
13
13
|
import { loadRules } from './rules/index.mjs';
|
|
14
|
-
import { applyOverridesScript, overridesToCss, mergeOverrides } from './overrides.mjs';
|
|
14
|
+
import { applyOverridesScript, overridesToCss, mergeOverrides, partitionProtectedShellOverridesScript } from './overrides.mjs';
|
|
15
15
|
import { structureHarm } from './harm.mjs';
|
|
16
16
|
import { probeSeams } from './ground-probe.mjs';
|
|
17
17
|
import { pageRuleRecords, summarizePolishReport } from './report.mjs';
|
|
@@ -59,6 +59,9 @@ export async function polishPrototype({
|
|
|
59
59
|
try {
|
|
60
60
|
const sectionIds = model ? unit.states.map((s) => model.states.find((st) => st.stateId === s)?.sectionId).filter(Boolean) : [];
|
|
61
61
|
const sourceItems = inventory && sectionIds.length ? sourceItemsFor(inventory, sectionIds) : null;
|
|
62
|
+
const protectedShellIds = [...new Set((unit.shellBands || []).flatMap((band) => (
|
|
63
|
+
(band.regions || []).filter((region) => (region.consistencyStatus || 'canonical') === 'canonical').map((region) => region.shellId)
|
|
64
|
+
)).filter(Boolean))];
|
|
62
65
|
await page.goto(`${base}/${unit.id}.html`, { waitUntil: 'networkidle', timeout: 30_000 });
|
|
63
66
|
await settleFonts(page);
|
|
64
67
|
const before = await measurePage(page, { sourceItems });
|
|
@@ -69,15 +72,15 @@ export async function polishPrototype({
|
|
|
69
72
|
const harmed = (after, ruleSet) => {
|
|
70
73
|
const newOf = (kind) => (after.overflow.filter((o) => o.kind === kind).length) - (before.overflow.filter((o) => o.kind === kind).length);
|
|
71
74
|
const cap = Math.max(0, ...ruleSet.map((p) => p.rule.maxShift || 0));
|
|
72
|
-
// rules that declare block moves (left-rail) change the horizontal gaps beside the rows they move by design
|
|
73
|
-
const exempt = new Set(ruleSet.filter((p) => p.rule.movesBlocks).flatMap((p) => p.overrides.map((o) => o.id)));
|
|
74
75
|
// A spacing rule may change only the exact before->after gaps it declares in seam metadata. Every other
|
|
75
76
|
// vertical sibling gap remains protected by the ordinary harm guard.
|
|
76
|
-
const
|
|
77
|
-
const st = structureHarm(before, after,
|
|
77
|
+
const declaredGaps = new Set(ruleSet.filter((p) => p.rule.name === 'section-breathing').flatMap((p) => p.overrides.map((o) => o.meta?.seam).filter(Boolean).flatMap((s) => (s.beforePeers || [s.before]).map((beforeId) => `${beforeId}|${s.after}`))));
|
|
78
|
+
const st = structureHarm(before, after, declaredGaps);
|
|
78
79
|
return {
|
|
79
80
|
misaligned: st.misaligned.length > 0,
|
|
80
81
|
spacing: st.spacing.length > 0,
|
|
82
|
+
containment: st.containment.length > 0,
|
|
83
|
+
'reachable-actions': st.unreachableActions.length > 0,
|
|
81
84
|
overlap: after.counts.overlap - before.counts.overlap > HARM.newOverlap,
|
|
82
85
|
escape: newOf('escape') > HARM.newEscape,
|
|
83
86
|
cut: newOf('cut') > HARM.newCut,
|
|
@@ -92,9 +95,17 @@ export async function polishPrototype({
|
|
|
92
95
|
const proposals = [];
|
|
93
96
|
for (const rule of ruleSet) {
|
|
94
97
|
const proposed = await page.evaluate(rule.browserFn(), { metrics, params: rule.params || {} });
|
|
98
|
+
const partition = protectedShellIds.length
|
|
99
|
+
? await page.evaluate(partitionProtectedShellOverridesScript(), { overrides: proposed, shellIds: protectedShellIds })
|
|
100
|
+
: { allowed: proposed, protected: [] };
|
|
95
101
|
const cap = rule.maxShift ?? 0; // each override is held to ITS rule's declared cap
|
|
96
|
-
const found =
|
|
97
|
-
row.rules[rule.name] = {
|
|
102
|
+
const found = partition.allowed.filter((o) => Math.abs(o.dx || 0) <= cap + 0.01 && Math.abs(o.dy || 0) <= cap + 0.01);
|
|
103
|
+
row.rules[rule.name] = {
|
|
104
|
+
proposed: proposed.length,
|
|
105
|
+
overCap: partition.allowed.length - found.length,
|
|
106
|
+
...(partition.protected.length ? { protectedShell: partition.protected.length } : {}),
|
|
107
|
+
};
|
|
108
|
+
if (partition.protected.length) row.rejected.push({ rule: rule.name, harm: ['canonical-shell'] });
|
|
98
109
|
if (rule.recordsCensus) {
|
|
99
110
|
row.rules[rule.name].census = await page.evaluate(rule.browserFn(), {
|
|
100
111
|
metrics,
|
|
@@ -159,8 +170,9 @@ export async function polishPrototype({
|
|
|
159
170
|
row.ground = verdict;
|
|
160
171
|
if (!verdict.ok) {
|
|
161
172
|
row.rejected.push({ rule: rule.name, harm: verdict.failures.map((f) => 'ground-' + f.kind) });
|
|
162
|
-
|
|
163
|
-
|
|
173
|
+
const final = await accept(active.filter((a) => a !== p));
|
|
174
|
+
active = final.active;
|
|
175
|
+
result = final.result;
|
|
164
176
|
}
|
|
165
177
|
}
|
|
166
178
|
if (result) {
|
|
@@ -1,14 +1,46 @@
|
|
|
1
1
|
// align: snap near-miss edges between same-kind siblings (≤ NEAR px apart) onto the group's modal edge.
|
|
2
2
|
// Fires on: three cards whose tops are 0, 1.02 and 2.05 px → the two strays move to 0.
|
|
3
|
-
// Silent on: edges that are equal,
|
|
3
|
+
// Silent on: edges that are equal, further apart than NEAR (deliberate offsets), different kinds, or elements whose
|
|
4
|
+
// before geometry declares vertical centering inside their immediate parent / containing control.
|
|
4
5
|
export const name = 'align';
|
|
5
6
|
export const maxShift = 6;
|
|
6
7
|
|
|
7
8
|
export function browserFn() {
|
|
8
9
|
return ({ metrics }) => {
|
|
10
|
+
// Population evidence (75 pages, 158 accepted align rule-parts; owner 2026-08-26): among the 14 vertical moves
|
|
11
|
+
// measurable inside declared buttons/circles/badges/chips, the centered cluster's inset imbalance is 0–1.024 px
|
|
12
|
+
// and the next observed value is 2.048 px. Keep the threshold between those clusters.
|
|
13
|
+
const CENTERED_INSET_TOLERANCE = 1.1;
|
|
14
|
+
const CENTERED_CONTAINER_SELECTOR = 'button, .button, .badge, .chip, .circle, [role="button"], [data-proto-action]';
|
|
9
15
|
const groups = new Map(); // parent|axis → [{id, value}]
|
|
10
16
|
const axisOf = { left: 'x', right: 'r', top: 'y', bottom: 'b', center: 'cy' };
|
|
11
17
|
const rectOf = (el) => { const r = el.getBoundingClientRect(); const s = el.closest('section[id^="viewport-"]') || document.body; const b = s.getBoundingClientRect(); return { x: r.left - b.left, y: r.top - b.top, r: r.right - b.left, b: r.bottom - b.top, cy: r.top - b.top + r.height / 2 }; };
|
|
18
|
+
const verticalCenterContainer = (el) => {
|
|
19
|
+
const r = el.getBoundingClientRect();
|
|
20
|
+
const parent = el.parentElement;
|
|
21
|
+
if (!parent || !r.width || !r.height) return null;
|
|
22
|
+
const contains = (c) => c.left <= r.left + 0.25 && c.right >= r.right - 0.25
|
|
23
|
+
&& c.top <= r.top + 0.25 && c.bottom >= r.bottom - 0.25;
|
|
24
|
+
// The relationship needs a declared control/container fact. A generic flex parent or painted surface is not
|
|
25
|
+
// enough: both occur around legitimate edge rows in the population and would turn this guard into suppression.
|
|
26
|
+
const ancestor = parent.closest(CENTERED_CONTAINER_SELECTOR);
|
|
27
|
+
const candidates = [...parent.children]
|
|
28
|
+
.filter((candidate) => candidate !== el && candidate.matches(CENTERED_CONTAINER_SELECTOR))
|
|
29
|
+
.map((candidate) => ({ el: candidate, rect: candidate.getBoundingClientRect() }))
|
|
30
|
+
.concat(ancestor ? [{ el: ancestor, rect: ancestor.getBoundingClientRect() }] : [])
|
|
31
|
+
.filter((candidate) => candidate.el !== el && contains(candidate.rect))
|
|
32
|
+
.sort((a, b) => a.rect.width * a.rect.height - b.rect.width * b.rect.height);
|
|
33
|
+
const container = candidates[0]?.el;
|
|
34
|
+
if (!container) return null;
|
|
35
|
+
const c = container.getBoundingClientRect();
|
|
36
|
+
if (!contains(c)) return null;
|
|
37
|
+
const topInset = r.top - c.top;
|
|
38
|
+
const bottomInset = c.bottom - r.bottom;
|
|
39
|
+
return topInset >= -0.5 && bottomInset >= -0.5
|
|
40
|
+
&& Math.abs(topInset - bottomInset) <= CENTERED_INSET_TOLERANCE
|
|
41
|
+
? container
|
|
42
|
+
: null;
|
|
43
|
+
};
|
|
12
44
|
// an element that already belongs to a left (top) column is left-aligned by design: never snap its RIGHT (bottom)
|
|
13
45
|
// edge — that is what moved the Lands "Proposals" nav item 5 px out of its icon column (owner, 2026-08-22)
|
|
14
46
|
const leftAligned = new Set(), topAligned = new Set();
|
|
@@ -41,6 +73,38 @@ export function browserFn() {
|
|
|
41
73
|
}
|
|
42
74
|
return false;
|
|
43
75
|
};
|
|
76
|
+
// Unlike transform, positioned insets do not create a stacking context around a container's independently
|
|
77
|
+
// z-indexed descendants. Preserve the target's positioning mode and existing inset, and stay silent when a static
|
|
78
|
+
// target cannot be nudged without changing layout semantics.
|
|
79
|
+
const insetNudge = (el, horizontal, visualDelta) => {
|
|
80
|
+
const style = getComputedStyle(el);
|
|
81
|
+
if (style.position === 'static') return null;
|
|
82
|
+
const primary = horizontal ? 'left' : 'top';
|
|
83
|
+
const opposite = horizontal ? 'right' : 'bottom';
|
|
84
|
+
const dimension = horizontal ? 'width' : 'height';
|
|
85
|
+
const rect = el.getBoundingClientRect();
|
|
86
|
+
const layoutSpan = Number.parseFloat(style[dimension]);
|
|
87
|
+
const visualSpan = horizontal ? rect.width : rect.height;
|
|
88
|
+
const scale = layoutSpan > 0 && visualSpan > 0 ? visualSpan / layoutSpan : 1;
|
|
89
|
+
const d = visualDelta / scale;
|
|
90
|
+
const current = Number.parseFloat(style[primary]);
|
|
91
|
+
const anchored = Number.parseFloat(style[opposite]);
|
|
92
|
+
// With both insets resolved, moving only one inset resizes the box. Width preservation has an explicit
|
|
93
|
+
// zoom/constrained fixture. For vertical movement, shift both insets in opposite directions instead: that keeps
|
|
94
|
+
// the declared height relationship without publishing an unproven height:...!important lock.
|
|
95
|
+
const constrainedPosition = style.position === 'absolute' || style.position === 'fixed';
|
|
96
|
+
const doublyConstrained = constrainedPosition && Number.isFinite(current) && Number.isFinite(anchored) && Number.isFinite(layoutSpan);
|
|
97
|
+
const preserveSize = horizontal && doublyConstrained
|
|
98
|
+
? { [dimension]: `${+layoutSpan.toFixed(2)}px` }
|
|
99
|
+
: {};
|
|
100
|
+
if (!horizontal && doublyConstrained) return {
|
|
101
|
+
[primary]: `${+(current + d).toFixed(2)}px`,
|
|
102
|
+
[opposite]: `${+(anchored - d).toFixed(2)}px`,
|
|
103
|
+
};
|
|
104
|
+
if (Number.isFinite(current)) return { ...preserveSize, [primary]: `${+(current + d).toFixed(2)}px` };
|
|
105
|
+
if (Number.isFinite(anchored)) return { ...preserveSize, [opposite]: `${+(anchored - d).toFixed(2)}px` };
|
|
106
|
+
return { [primary]: `${+d.toFixed(2)}px` };
|
|
107
|
+
};
|
|
44
108
|
const out = [];
|
|
45
109
|
for (const [key, g] of groups) {
|
|
46
110
|
const axis = key.split('|')[1];
|
|
@@ -57,9 +121,15 @@ export function browserFn() {
|
|
|
57
121
|
for (const [id, v] of cluster) {
|
|
58
122
|
const d = anchor - v;
|
|
59
123
|
if (Math.abs(d) < 0.5 || Math.abs(d) > 6) continue;
|
|
60
|
-
|
|
124
|
+
const target = document.getElementById(id);
|
|
125
|
+
if (breaksColumn(target, axis)) continue;
|
|
61
126
|
const horizontal = axis === 'left' || axis === 'right';
|
|
62
|
-
|
|
127
|
+
// Top/bottom/center edge repair cannot trade away a declared centre relationship. Aligning the containing
|
|
128
|
+
// control itself remains available; nudging only its centered child does not.
|
|
129
|
+
if (!horizontal && verticalCenterContainer(target)) continue;
|
|
130
|
+
const css = insetNudge(target, horizontal, d);
|
|
131
|
+
if (!css) continue;
|
|
132
|
+
out.push({ id, rule: 'align', reason: `${axis} ${v.toFixed(1)}→${anchor}`, dx: horizontal ? d : 0, dy: horizontal ? 0 : d, motion: 'inset', css });
|
|
63
133
|
}
|
|
64
134
|
}
|
|
65
135
|
i = j + 1;
|
|
@@ -14,11 +14,10 @@
|
|
|
14
14
|
// rows far from the rail (> 48 px: a second column, a grid), indented sub-items (a row ≥ 12 px right of its
|
|
15
15
|
// own block's edge while that block already sits on the rail), and pages with fewer than 3 ink rows.
|
|
16
16
|
// maxShift 48 is the declared magnitude; the harm guard still rejects the rule on any page where a snap adds an
|
|
17
|
-
// overlap, cut, escape, broken column or off-center fit.
|
|
18
|
-
//
|
|
17
|
+
// overlap, cut, escape, parent-containment breach, broken column, changed sibling gap, or off-center fit. No moving
|
|
18
|
+
// rule is exempt from the shared guard.
|
|
19
19
|
export const name = 'left-rail';
|
|
20
20
|
export const maxShift = 48;
|
|
21
|
-
export const movesBlocks = true;
|
|
22
21
|
export const params = { SNAP_MAX: 48, SNAP_MIN: 2, CLUSTER: 4, INDENT: 12, CENTER_TOL: 24 };
|
|
23
22
|
|
|
24
23
|
export function browserFn() {
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
// owner, then move the semantic block and later direct children. Gradient/image/texture grounds still abstain.
|
|
12
12
|
export const name = 'section-breathing';
|
|
13
13
|
export const maxShift = 192;
|
|
14
|
-
export const movesBlocks = true;
|
|
15
14
|
export const phase = 'post';
|
|
16
15
|
export const verifyPaint = true;
|
|
17
16
|
export const recordsCensus = true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
[{"id":"viewport-1-a-cards-card-2","dy"
|
|
1
|
+
[{"id":"viewport-1-a-cards-card-2","dy":0,"css":{"top":"-1.02px"}},{"id":"viewport-1-a-cards-card-3","dy":0,"css":{"top":"-2.05px"}}]
|
|
@@ -4,11 +4,14 @@ section{position:relative;width:1536px;height:1024px;background:#fff}
|
|
|
4
4
|
.row{position:relative;display:flex;gap:24px;margin:100px 0 0 100px}
|
|
5
5
|
.card{position:relative;width:300px;height:200px;background:#f3f3f3;border:1px solid #ddd;border-radius:12px}
|
|
6
6
|
.text{position:relative;display:block;font-size:16px;line-height:20px;margin:20px 0 0 20px}
|
|
7
|
+
.action{position:absolute;right:16px;bottom:16px;z-index:100;width:96px;height:32px}
|
|
8
|
+
.canvas-sibling{position:absolute;left:100px;top:100px;z-index:50;width:948px;height:204px;background:rgba(255,255,255,.001)}
|
|
7
9
|
</style>
|
|
8
10
|
<section id="viewport-1-a">
|
|
9
11
|
<div id="viewport-1-a-cards" class="row">
|
|
10
12
|
<div id="viewport-1-a-cards-card-1" class="card"><span id="viewport-1-a-cards-card-1-text" class="text">Alpha</span></div>
|
|
11
|
-
<div id="viewport-1-a-cards-card-2" class="card" style="margin-top:1.02px"><span id="viewport-1-a-cards-card-2-text" class="text">Beta</span></div>
|
|
13
|
+
<div id="viewport-1-a-cards-card-2" class="card" style="margin-top:1.02px"><span id="viewport-1-a-cards-card-2-text" class="text">Beta</span><button id="viewport-1-a-cards-card-2-action" class="action" data-proto-action="modal">Open</button></div>
|
|
12
14
|
<div id="viewport-1-a-cards-card-3" class="card" style="margin-top:2.05px"><span id="viewport-1-a-cards-card-3-text" class="text">Gamma</span></div>
|
|
13
15
|
</div>
|
|
16
|
+
<div class="canvas-sibling"></div>
|
|
14
17
|
</section>
|
|
@@ -25,4 +25,12 @@ section{position:relative;width:1536px;height:1024px;background:#fff}
|
|
|
25
25
|
<div id="viewport-1-a-list-row-2" style="position:absolute;left:11.1px;top:40px;width:200px;height:24px"><span id="viewport-1-a-list-row-2-icon" class="icon" style="position:absolute;left:-1.1px;top:4px;width:16px;height:16px;background:#888"></span><span id="viewport-1-a-list-row-2-text" class="text" style="position:absolute;left:26.9px;top:2px;margin:0;width:100px">Beta</span></div>
|
|
26
26
|
<div id="viewport-1-a-list-row-3" style="position:absolute;left:10px;top:80px;width:200px;height:24px"><span id="viewport-1-a-list-row-3-icon" class="icon" style="position:absolute;left:0;top:4px;width:16px;height:16px;background:#888"></span><span id="viewport-1-a-list-row-3-text" class="text" style="position:absolute;left:28px;top:2px;margin:0;width:100px">Gamma</span></div>
|
|
27
27
|
</div>
|
|
28
|
+
<!-- Owner-found must-stay-silent pair: the icons' top edges differ by 2 px, but each icon is vertically centered
|
|
29
|
+
inside its containing button. Align must preserve that stronger relation instead of snapping their top edges. -->
|
|
30
|
+
<div id="viewport-1-a-centered-controls" style="position:absolute;left:760px;top:400px;width:116px;height:44px">
|
|
31
|
+
<button id="viewport-1-a-centered-controls-button-1" class="button" style="position:absolute;left:0;top:0;width:40px;height:40px;border:0;border-radius:20px;background:#dfeacc"></button>
|
|
32
|
+
<span id="viewport-1-a-centered-controls-icon-1" class="icon" style="position:absolute;left:12px;top:12px;width:16px;height:16px;background:#56714a"></span>
|
|
33
|
+
<div id="viewport-1-a-centered-controls-circle-2" class="circle" style="position:absolute;left:72px;top:0;width:44px;height:44px;border-radius:22px;background:#dfeacc"></div>
|
|
34
|
+
<span id="viewport-1-a-centered-controls-icon-2" class="icon" style="position:absolute;left:86px;top:14px;width:16px;height:16px;background:#56714a"></span>
|
|
35
|
+
</div>
|
|
28
36
|
</section>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!doctype html><meta charset="utf-8"><style>
|
|
2
|
+
*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui}.zoom{zoom:.75}.card,.box{background:#f3f3f3;border:1px solid #ddd}.text{position:absolute;left:12px;top:12px}.row{position:relative;display:flex;gap:20px;margin:80px}.card{position:relative;width:240px;height:100px}.stack{position:relative;width:1536px;height:500px}.box{position:absolute;right:100px;height:60px}
|
|
3
|
+
</style>
|
|
4
|
+
<div class="zoom"><section id="viewport-1-a" style="position:relative;width:1536px;height:300px"><div id="viewport-1-a-row" class="row">
|
|
5
|
+
<div id="viewport-1-a-card-1" class="card"><span id="viewport-1-a-card-1-text" class="text">One</span></div>
|
|
6
|
+
<div id="viewport-1-a-card-2" class="card" style="margin-top:2px"><span id="viewport-1-a-card-2-text" class="text">Two</span></div>
|
|
7
|
+
<div id="viewport-1-a-card-3" class="card" style="margin-top:4px"><span id="viewport-1-a-card-3-text" class="text">Three</span></div>
|
|
8
|
+
</div></section></div>
|
|
9
|
+
<section id="viewport-2-a" class="stack">
|
|
10
|
+
<div id="viewport-2-a-box-1" class="box" style="left:100px;top:80px"><span id="viewport-2-a-box-1-text" class="text">One</span></div>
|
|
11
|
+
<div id="viewport-2-a-box-2" class="box" style="left:102px;top:180px"><span id="viewport-2-a-box-2-text" class="text">Two</span></div>
|
|
12
|
+
<div id="viewport-2-a-box-3" class="box" style="left:104px;top:280px"><span id="viewport-2-a-box-3-text" class="text">Three</span></div>
|
|
13
|
+
</section>
|
|
14
|
+
<section id="viewport-3-a" style="position:relative;width:600px;height:120px">
|
|
15
|
+
<div id="viewport-3-a-box-1" class="box" style="position:absolute;left:80px;top:20px;bottom:20px;width:100px;height:auto"><span id="viewport-3-a-box-1-text" class="text">Anchor</span></div>
|
|
16
|
+
<div id="viewport-3-a-box-2" class="box" style="position:absolute;left:220px;top:22px;bottom:18px;width:100px;height:auto"><span id="viewport-3-a-box-2-text" class="text">Stray</span></div>
|
|
17
|
+
</section>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!doctype html><meta charset="utf-8"><style>
|
|
2
|
+
*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui}section{position:relative;width:1536px;height:1024px}.reference,.inbox{position:absolute;left:0;width:1536px}.text{position:absolute;white-space:nowrap}.inbox{left:64px;width:440px;height:490px}.inbox .text{left:0;width:400px;height:44px;padding:12px;background:#f5f5f2;border:1px solid #ddd}
|
|
3
|
+
</style><section id="viewport-1-a">
|
|
4
|
+
<div id="viewport-1-a-reference-1" class="reference" style="top:20px;height:40px"><p id="viewport-1-a-reference-1-text" class="text" style="left:80px;top:0">Inbox</p></div>
|
|
5
|
+
<div id="viewport-1-a-reference-2" class="reference" style="top:70px;height:40px"><p id="viewport-1-a-reference-2-text" class="text" style="left:80px;top:0">Assigned</p></div>
|
|
6
|
+
<div id="viewport-1-a-reference-3" class="reference" style="top:120px;height:40px"><p id="viewport-1-a-reference-3-text" class="text" style="left:80px;top:0">Resolved</p></div>
|
|
7
|
+
<div id="viewport-1-a-inbox" class="inbox" style="top:190px">
|
|
8
|
+
<button id="viewport-1-a-inbox-row-1" class="text" data-proto-action="navigate" style="top:0">Action one</button>
|
|
9
|
+
<button id="viewport-1-a-inbox-row-2" class="text" data-proto-action="navigate" style="top:60px">Action two</button>
|
|
10
|
+
<button id="viewport-1-a-inbox-row-3" class="text" data-proto-action="navigate" style="top:120px">Action three</button>
|
|
11
|
+
<button id="viewport-1-a-inbox-row-4" class="text" data-proto-action="navigate" style="top:180px">Action four</button>
|
|
12
|
+
<button id="viewport-1-a-inbox-row-5" class="text" data-proto-action="navigate" style="top:240px">Action five</button>
|
|
13
|
+
<button id="viewport-1-a-inbox-row-6" class="text" data-proto-action="navigate" style="top:300px">Action six</button>
|
|
14
|
+
<button id="viewport-1-a-inbox-row-7" class="text" data-proto-action="navigate" style="top:360px">Action seven</button>
|
|
15
|
+
</div>
|
|
16
|
+
</section>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!doctype html><meta charset="utf-8"><style>
|
|
2
|
+
*{margin:0;padding:0;box-sizing:border-box}body{font-family:system-ui}section{position:relative;width:1536px;height:1024px}.reference,.inbox{position:absolute;left:0;width:1536px}.text{position:absolute;white-space:nowrap}.inbox{left:64px;width:400px;height:490px}.inbox .text{left:0;width:400px;height:44px;padding:12px;background:#f5f5f2;border:1px solid #ddd}
|
|
3
|
+
</style><section id="viewport-1-a">
|
|
4
|
+
<div id="viewport-1-a-reference-1" class="reference" style="top:20px;height:40px"><p id="viewport-1-a-reference-1-text" class="text" style="left:80px;top:0">Inbox</p></div>
|
|
5
|
+
<div id="viewport-1-a-reference-2" class="reference" style="top:70px;height:40px"><p id="viewport-1-a-reference-2-text" class="text" style="left:80px;top:0">Assigned</p></div>
|
|
6
|
+
<div id="viewport-1-a-reference-3" class="reference" style="top:120px;height:40px"><p id="viewport-1-a-reference-3-text" class="text" style="left:80px;top:0">Resolved</p></div>
|
|
7
|
+
<div id="viewport-1-a-inbox" class="inbox" style="top:190px">
|
|
8
|
+
<button id="viewport-1-a-inbox-row-1" class="text" data-proto-action="navigate" style="top:0">Action one</button>
|
|
9
|
+
<button id="viewport-1-a-inbox-row-2" class="text" data-proto-action="navigate" style="top:60px">Action two</button>
|
|
10
|
+
<button id="viewport-1-a-inbox-row-3" class="text" data-proto-action="navigate" style="top:120px">Action three</button>
|
|
11
|
+
<button id="viewport-1-a-inbox-row-4" class="text" data-proto-action="navigate" style="top:180px">Action four</button>
|
|
12
|
+
<button id="viewport-1-a-inbox-row-5" class="text" data-proto-action="navigate" style="top:240px">Action five</button>
|
|
13
|
+
<button id="viewport-1-a-inbox-row-6" class="text" data-proto-action="navigate" style="top:300px">Action six</button>
|
|
14
|
+
<button id="viewport-1-a-inbox-row-7" class="text" data-proto-action="navigate" style="top:360px">Action seven</button>
|
|
15
|
+
</div>
|
|
16
|
+
</section>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<!doctype html><meta charset="utf-8"><style>
|
|
2
|
+
*{box-sizing:border-box}section{position:relative;width:1536px;height:1024px}#viewport-1-a-outer,#viewport-1-a-stacked{position:absolute;left:100px;width:240px;height:80px;background:#ddd}#viewport-1-a-outer{top:100px}#viewport-1-a-inner{position:absolute;inset:0;width:100%;height:100%;z-index:2}#viewport-1-a-stacked{top:220px}#viewport-1-a-stacked-button{width:100%;height:100%}
|
|
3
|
+
</style><section id="viewport-1-a">
|
|
4
|
+
<div id="viewport-1-a-outer" role="button" data-proto-action="modal" tabindex="0"><button id="viewport-1-a-inner" data-proto-action="dropdown">Inner action</button></div>
|
|
5
|
+
<div id="viewport-1-a-stacked" data-proto-action="modal"><button id="viewport-1-a-stacked-button">Plain pointer surface</button></div>
|
|
6
|
+
</section>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<!doctype html><meta charset="utf-8"><style>
|
|
2
|
+
*{margin:0;padding:0;box-sizing:border-box}section{position:relative;width:1536px;height:1024px}.wrapper{position:absolute;left:100px;top:100px;width:400px;height:100px}.row{position:absolute;left:0;top:0;width:400px;height:60px;background:#eee}
|
|
3
|
+
</style><section id="viewport-1-a"><div class="wrapper"><div id="viewport-1-a-row" class="row">Row</div></div></section>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
section{position:relative;width:1536px;height:1024px;background:#fff}
|
|
4
4
|
.rail{position:absolute;left:0;top:0;width:220px;height:1024px;background:#f6f4f0;padding:24px 0 0 16px}
|
|
5
5
|
.btn{position:relative;width:214px;height:43px;background:#245;border-radius:8px;margin-top:40px}
|
|
6
|
-
.btn .text{position:absolute;left:0;top:12px;width:
|
|
6
|
+
.btn .text{position:absolute;left:0;top:12px;width:100%;text-align:center;color:#fff;font-size:15px}
|
|
7
7
|
.nav{position:relative;margin-top:24px;font-size:15px;width:99px;height:18px}
|
|
8
8
|
.foot{position:absolute;left:16px;top:1040px;width:120px;height:14px;font-size:11px;color:#666}
|
|
9
9
|
.content{position:absolute;left:270px;top:40px;width:1200px;height:900px;background:#fff;border:1px solid #ddd}
|