@12ui/design 0.2.51 → 0.2.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +1 -1
- package/dist/branch-client.d.ts +5 -0
- package/dist/branch-client.d.ts.map +1 -1
- package/dist/branch-client.js.map +1 -1
- package/dist/branch-completion.d.ts.map +1 -1
- package/dist/branch-completion.js +7 -0
- package/dist/branch-completion.js.map +1 -1
- package/dist/branch-identity.d.ts +25 -0
- package/dist/branch-identity.d.ts.map +1 -1
- package/dist/branch-identity.js +28 -15
- package/dist/branch-identity.js.map +1 -1
- package/dist/branch-page-conversion.d.ts.map +1 -1
- package/dist/branch-page-conversion.js +49 -4
- package/dist/branch-page-conversion.js.map +1 -1
- package/dist/branch-plan.d.ts +4 -1
- package/dist/branch-plan.d.ts.map +1 -1
- package/dist/branch-plan.js +7 -0
- package/dist/branch-plan.js.map +1 -1
- package/dist/branch-run-record.d.ts +4 -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.map +1 -1
- package/dist/cli-draft-command.js +17 -1
- package/dist/cli-draft-command.js.map +1 -1
- package/dist/hosted-draft-run.d.ts +25 -1
- package/dist/hosted-draft-run.d.ts.map +1 -1
- package/dist/hosted-draft-run.js +82 -2
- package/dist/hosted-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 +13 -2
- package/dist/package-submission.d.ts.map +1 -1
- package/dist/package-submission.js +34 -1
- package/dist/package-submission.js.map +1 -1
- package/dist/prototype-retention.d.ts +10 -0
- package/dist/prototype-retention.d.ts.map +1 -0
- package/dist/prototype-retention.js +14 -0
- package/dist/prototype-retention.js.map +1 -0
- package/dist/prototype-revision.d.ts +1 -1
- package/dist/prototype-revision.js +1 -1
- package/dist/prototype-runner.d.ts.map +1 -1
- package/dist/prototype-runner.js +39 -15
- package/dist/prototype-runner.js.map +1 -1
- package/dist/review-server.d.ts.map +1 -1
- package/dist/review-server.js +10 -1
- package/dist/review-server.js.map +1 -1
- package/open-design.json +2 -2
- package/package.json +1 -1
- package/prototype-kit/build.mjs +54 -9
- package/prototype-kit/lib/action-target.mjs +34 -8
- package/prototype-kit/lib/runtime-check.mjs +31 -12
- package/prototype-kit/polish/ground-probe.mjs +4 -1
- package/prototype-kit/polish/measure.mjs +6 -6
- package/prototype-kit/polish/rules/section-breathing.mjs +461 -237
- package/prototype-kit/polish/tests/breathing-check.mjs +75 -27
- package/prototype-kit/polish/tests/fixtures/section-breathing/diagram-silent.html +20 -0
- package/prototype-kit/polish/tests/fixtures/section-breathing/fire.html +2 -2
- package/prototype-kit/polish/tests/fixtures/section-breathing/floor.html +3 -3
- package/prototype-kit/polish/tests/fixtures/section-breathing/no-scale-silent.html +18 -0
- package/prototype-kit/polish/tests/fixtures/section-breathing/rhythm-nested.html +21 -0
- package/prototype-kit/polish/tests/fixtures/section-breathing/semantic-fire.html +10 -4
- package/prototype-kit/polish/tests/fixtures/section-breathing/semantic-silent.html +2 -2
- package/prototype-kit/polish/tests/fixtures/section-breathing/silent.html +2 -2
- package/prototype-kit/polish/tests/fixtures/section-breathing/tight.html +4 -4
- package/prototype-kit/runtime/proto.js +52 -2
- package/prototype-kit/tests/adversarial-runtime.mjs +179 -4
- package/prototype-kit/tests/fixtures/blocking-runtime/broken-interaction.html +1 -0
- package/prototype-kit/tests/fixtures/interaction-runtime/PROVENANCE.md +5 -0
- package/prototype-kit/tests/fixtures/interaction-runtime/inert-field.html +23 -0
- package/prototype-kit/tests/fixtures/interaction-runtime/pointer-cascade.html +19 -0
- package/prototype-kit/tests/fixtures/multiple-trigger-runtime/probe-all.html +14 -0
- package/prototype-kit/tests/fixtures/nested-inert/branch.plan.json +5 -0
- package/prototype-kit/tests/fixtures/nested-inert/pages/p0.html +17 -0
- package/prototype-kit/tests/offline-check.mjs +6 -1
- package/prototype-kit/tests/rules-check.mjs +28 -9
- package/prototype-kit/verify-runtime.mjs +10 -5
- package/prototype-kit/polish/tests/fixtures/section-breathing/attachment-fire.html +0 -5
- package/prototype-kit/polish/tests/fixtures/section-breathing/attachment-silent.html +0 -5
- package/prototype-kit/polish/tests/fixtures/section-breathing/shadow-fire.html +0 -5
- package/prototype-kit/polish/tests/fixtures/section-breathing/shadow-silent.html +0 -5
|
@@ -1,291 +1,515 @@
|
|
|
1
|
-
// section-breathing:
|
|
2
|
-
// boundary's proven solid ground. The rule remains page-atomic and the driver verifies its paint with real pixels.
|
|
1
|
+
// section-breathing: open only model-declared semantic boundaries.
|
|
3
2
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// - only explicit `.decoration` leaves whose bounds touch/cross a page seam travel with the section below it;
|
|
8
|
-
// - an explicit `dashboard` block stays silent (dense surfaces keep their deliberate compression).
|
|
3
|
+
// The model names the exact before/after elements, exact ground owner, and
|
|
4
|
+
// whether that owner may continue. This rule validates and executes that fact;
|
|
5
|
+
// no tag, id-pattern, size, pixel sample, or nearest-ground search may mint it.
|
|
9
6
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
7
|
+
// A semantic seam's AMOUNT is the page's own rhythm, not a constant. The page
|
|
8
|
+
// scale T = min(largest structural gap, 2 x median structural gap) is derived
|
|
9
|
+
// from the vertical air the page already contains, and a seam is opened to T
|
|
10
|
+
// and never past it. The gap it is opened FROM is the page's real air line —
|
|
11
|
+
// the bottom of the lowest peer that ends above the seam — not the pairwise
|
|
12
|
+
// distance between the two declared elements, which reads air that is not there
|
|
13
|
+
// when a floating card or intro column sits inside the seam.
|
|
12
14
|
export const name = 'section-breathing';
|
|
13
15
|
export const maxShift = 192;
|
|
14
16
|
export const phase = 'post';
|
|
15
17
|
export const verifyPaint = true;
|
|
16
18
|
export const recordsCensus = true;
|
|
17
19
|
export const params = {
|
|
18
|
-
FLOOR: 32,
|
|
19
|
-
|
|
20
|
-
MIN_TARGET
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
FLOOR: 32, CAP: 64, MIN_TARGET: 48, MAX_TARGET: 192,
|
|
21
|
+
TIGHT_GAP: 32, SEMANTIC_MAX_GAP: 32,
|
|
22
|
+
// Page-rhythm model (semantic seams only; viewport seams keep FLOOR/MIN_TARGET/MAX_TARGET).
|
|
23
|
+
PERCEPTIBILITY_FLOOR: 8,
|
|
24
|
+
SCOPE_WIDTH_FRACTION: 0.4,
|
|
25
|
+
STRUCTURAL_MIN_INK: 2,
|
|
26
|
+
// A group whose stacked children share no common edge and vary wildly in width is a
|
|
27
|
+
// drawing (a metro map, a scatter), not a stack. Air inserted into it pulls the drawing apart.
|
|
28
|
+
DIAGRAM_MIN_CHILDREN: 4,
|
|
29
|
+
DIAGRAM_MAX_ALIGN_FRACTION: 0.5,
|
|
30
|
+
DIAGRAM_MIN_WIDTH_CV: 0.6,
|
|
31
|
+
DIAGRAM_EDGE_TOLERANCE: 4,
|
|
27
32
|
};
|
|
33
|
+
export const SECTION_BOUNDARY_DECLARATION_REVISION = 'section-boundary-v1';
|
|
28
34
|
|
|
29
35
|
export function browserFn() {
|
|
30
36
|
return ({ params }) => {
|
|
37
|
+
const RULE = 'section-breathing';
|
|
38
|
+
const REVISION = 'section-boundary-v1';
|
|
31
39
|
const P = Object.assign({
|
|
32
|
-
FLOOR: 32, CAP: 64, MIN_TARGET: 48, MAX_TARGET: 192,
|
|
33
|
-
TIGHT_GAP: 32, SEMANTIC_MAX_GAP: 32,
|
|
40
|
+
FLOOR: 32, CAP: 64, MIN_TARGET: 48, MAX_TARGET: 192,
|
|
41
|
+
TIGHT_GAP: 32, SEMANTIC_MAX_GAP: 32,
|
|
42
|
+
PERCEPTIBILITY_FLOOR: 8,
|
|
43
|
+
SCOPE_WIDTH_FRACTION: 0.4,
|
|
44
|
+
STRUCTURAL_MIN_INK: 2,
|
|
45
|
+
DIAGRAM_MIN_CHILDREN: 4,
|
|
46
|
+
DIAGRAM_MAX_ALIGN_FRACTION: 0.5,
|
|
47
|
+
DIAGRAM_MIN_WIDTH_CV: 0.6,
|
|
48
|
+
DIAGRAM_EDGE_TOLERANCE: 4,
|
|
34
49
|
}, params || {});
|
|
35
50
|
const out = [];
|
|
36
51
|
const visible = (el) => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
52
|
+
if (!el) return false;
|
|
53
|
+
const style = getComputedStyle(el); const rect = el.getBoundingClientRect();
|
|
54
|
+
return style.display !== 'none' && style.visibility !== 'hidden'
|
|
55
|
+
&& Number.parseFloat(style.opacity) !== 0 && rect.width > 0 && rect.height > 0;
|
|
41
56
|
};
|
|
42
57
|
const alphaOf = (color) => {
|
|
43
|
-
const
|
|
44
|
-
if (!
|
|
45
|
-
const
|
|
46
|
-
return
|
|
58
|
+
const match = color && color.match(/^rgba?\(([^)]+)\)$/);
|
|
59
|
+
if (!match) return color === 'transparent' ? 0 : 1;
|
|
60
|
+
const parts = match[1].split(/[, /]+/).filter(Boolean).map(Number);
|
|
61
|
+
return parts.length > 3 ? parts[3] : 1;
|
|
47
62
|
};
|
|
48
|
-
const groundClass = (style, el) => {
|
|
49
|
-
if (['IMG', 'PICTURE', 'VIDEO'].includes(el.tagName)) return 'image';
|
|
50
|
-
const img = style.backgroundImage;
|
|
51
|
-
if (/repeating-(?:linear|radial|conic)-gradient\(/.test(img)) return 'texture';
|
|
52
|
-
if (/(?:linear|radial|conic)-gradient\(/.test(img)) return 'gradient';
|
|
53
|
-
if (img !== 'none') return style.backgroundRepeat === 'no-repeat' ? 'image' : 'texture';
|
|
54
|
-
return alphaOf(style.backgroundColor) > 0.01 ? 'solid' : 'transparent';
|
|
55
|
-
};
|
|
56
|
-
const INK = '.text, .paragraph, h1, h2, h3, h4, h5, h6, .icon, img, .image, button, .button';
|
|
57
|
-
const median = (v) => {
|
|
58
|
-
const s = [...v].sort((a, b) => a - b); const m = Math.floor(s.length / 2);
|
|
59
|
-
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
|
|
60
|
-
};
|
|
61
|
-
const ceil8 = (v) => Math.ceil(v / 8) * 8;
|
|
62
63
|
const sections = [...document.querySelectorAll('section[id^="viewport-"]')]
|
|
63
|
-
.filter((
|
|
64
|
-
.sort((
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (edge && (edge === 'bottom' ? Math.abs(o.rect.bottom - sr.bottom) : Math.abs(o.rect.top - sr.top)) > 0.75) {
|
|
75
|
-
return { ok: false, reason: 'ground-does-not-touch-edge' };
|
|
76
|
-
}
|
|
77
|
-
const cs = o.cs;
|
|
78
|
-
if (cs.boxShadow !== 'none' || cs.clipPath !== 'none' || cs.maskImage !== 'none') {
|
|
79
|
-
return { ok: false, reason: 'owner-has-shadow-clip-or-mask' };
|
|
80
|
-
}
|
|
81
|
-
if (/var\(/.test(cs.backgroundColor) || alphaOf(cs.backgroundColor) < 0.999) {
|
|
82
|
-
return { ok: false, reason: 'transparent-or-unresolved-solid' };
|
|
83
|
-
}
|
|
84
|
-
if (!o.el.id) return { ok: false, reason: 'owner-has-no-id' };
|
|
85
|
-
return { ok: true, owner: o.el, color: cs.backgroundColor };
|
|
64
|
+
.filter((section) => /^viewport-\d+-[A-Za-z0-9]+$/.test(section.id) && visible(section))
|
|
65
|
+
.sort((left, right) => left.getBoundingClientRect().top - right.getBoundingClientRect().top);
|
|
66
|
+
const sectionIndex = new Map(sections.map((section, index) => [section, index]));
|
|
67
|
+
const declarations = [...document.querySelectorAll('[data-ly-section-boundary="before"]')].filter(visible);
|
|
68
|
+
const INK_SELECTOR = '.text,.paragraph,h1,h2,h3,h4,h5,h6,.icon,img,.image,button,.button';
|
|
69
|
+
const solidGround = (element) => {
|
|
70
|
+
const style = getComputedStyle(element);
|
|
71
|
+
return style.backgroundImage === 'none'
|
|
72
|
+
&& alphaOf(style.backgroundColor) >= 0.999
|
|
73
|
+
&& !/var\(/.test(style.backgroundColor)
|
|
74
|
+
&& style.clipPath === 'none' && style.maskImage === 'none';
|
|
86
75
|
};
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
76
|
+
// Leaf ink: an ink element that contains no ink of its own. Counting leaves rather than
|
|
77
|
+
// nodes is what keeps label-to-title line leading inside a card from reading as page air.
|
|
78
|
+
const leafInk = (element) => [...element.querySelectorAll(INK_SELECTOR)].filter(visible)
|
|
79
|
+
.filter((node) => ![...node.querySelectorAll(INK_SELECTOR)].some(visible)).length
|
|
80
|
+
|| (element.matches(INK_SELECTOR) ? 1 : 0);
|
|
81
|
+
const stackedChildren = (element) => [...element.children]
|
|
82
|
+
.filter((child) => visible(child) && child.id)
|
|
83
|
+
.sort((left, right) => left.getBoundingClientRect().top - right.getBoundingClientRect().top);
|
|
84
|
+
// Consecutive vertical air inside one container. Children that overlap vertically are one
|
|
85
|
+
// run (side-by-side columns), so a gap is page air rather than a column edge.
|
|
86
|
+
const airRuns = (element) => {
|
|
87
|
+
const children = stackedChildren(element);
|
|
88
|
+
const runs = [];
|
|
89
|
+
let bottom = null; let previous = null;
|
|
90
|
+
for (const child of children) {
|
|
91
|
+
const rect = child.getBoundingClientRect();
|
|
92
|
+
if (bottom === null) { bottom = rect.bottom; previous = child; }
|
|
93
|
+
else {
|
|
94
|
+
const gap = rect.top - bottom;
|
|
95
|
+
if (gap > -0.5) { runs.push({ before: previous, after: child, gap, y: bottom }); previous = child; }
|
|
96
|
+
bottom = Math.max(bottom, rect.bottom);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return runs;
|
|
100
100
|
};
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
101
|
+
// The page's own spacing scale, from every structural gap at every nesting depth.
|
|
102
|
+
// Structural = the container spans a real share of the page AND both sides carry ink.
|
|
103
|
+
const pageScaleTarget = () => {
|
|
104
|
+
const pageWidth = Math.max(0, ...sections.map((section) => section.getBoundingClientRect().width));
|
|
105
|
+
const gaps = [];
|
|
106
|
+
const walk = (element, depth) => {
|
|
107
|
+
if (depth > 6) return;
|
|
108
|
+
const rect = element.getBoundingClientRect();
|
|
109
|
+
if (rect.width >= P.SCOPE_WIDTH_FRACTION * pageWidth) {
|
|
110
|
+
for (const run of airRuns(element)) {
|
|
111
|
+
if (leafInk(run.before) >= P.STRUCTURAL_MIN_INK && leafInk(run.after) >= P.STRUCTURAL_MIN_INK) {
|
|
112
|
+
gaps.push(run.gap);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (const child of stackedChildren(element)) walk(child, depth + 1);
|
|
117
|
+
};
|
|
118
|
+
for (const section of sections) walk(section, 1);
|
|
119
|
+
if (!gaps.length) return { target: null, gaps: 0, median: null, largest: null };
|
|
120
|
+
const sorted = [...gaps].sort((left, right) => left - right);
|
|
121
|
+
const median = sorted.length % 2
|
|
122
|
+
? sorted[(sorted.length - 1) / 2]
|
|
123
|
+
: (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2;
|
|
124
|
+
const largest = sorted[sorted.length - 1];
|
|
125
|
+
return { target: Math.min(largest, 2 * median), gaps: gaps.length, median, largest };
|
|
112
126
|
};
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
127
|
+
// The page's real air line at a seam: the bottom of the lowest peer that ends above the cut.
|
|
128
|
+
// The declared `before` is only one candidate; a floating note card or an intro column that
|
|
129
|
+
// outlives it is what the eye actually reads the air from.
|
|
130
|
+
const realAirLine = (group, after) => {
|
|
131
|
+
const cut = after.getBoundingClientRect().top;
|
|
132
|
+
const peers = [...group.children].filter((child) => child !== after && visible(child) && child.id)
|
|
133
|
+
.filter((child) => child.getBoundingClientRect().bottom <= cut + 0.5);
|
|
134
|
+
if (!peers.length) return null;
|
|
135
|
+
const anchor = peers.reduce((lowest, child) => (
|
|
136
|
+
child.getBoundingClientRect().bottom > lowest.getBoundingClientRect().bottom ? child : lowest
|
|
137
|
+
));
|
|
138
|
+
return { anchor, peers, gap: cut - anchor.getBoundingClientRect().bottom };
|
|
118
139
|
};
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
140
|
+
// A group whose stacked children share no common left/right/centre edge and whose widths vary
|
|
141
|
+
// wildly is a drawing, not a stack: its vertical gaps carry meaning and must not be opened.
|
|
142
|
+
const diagramLike = (element) => {
|
|
143
|
+
const runs = airRuns(element);
|
|
144
|
+
if (!runs.length) return false;
|
|
145
|
+
const stacked = [runs[0].before, ...runs.map((run) => run.after)];
|
|
146
|
+
if (stacked.length < P.DIAGRAM_MIN_CHILDREN) return false;
|
|
147
|
+
const boxes = stacked.map((child) => child.getBoundingClientRect());
|
|
148
|
+
const largestCluster = (values) => {
|
|
149
|
+
const buckets = new Map();
|
|
150
|
+
for (const value of values) {
|
|
151
|
+
const key = Math.round(value / P.DIAGRAM_EDGE_TOLERANCE);
|
|
152
|
+
buckets.set(key, (buckets.get(key) || 0) + 1);
|
|
153
|
+
}
|
|
154
|
+
return Math.max(...buckets.values());
|
|
155
|
+
};
|
|
156
|
+
const alignFraction = Math.max(
|
|
157
|
+
largestCluster(boxes.map((box) => box.left)),
|
|
158
|
+
largestCluster(boxes.map((box) => box.right)),
|
|
159
|
+
largestCluster(boxes.map((box) => (box.left + box.right) / 2)),
|
|
160
|
+
) / boxes.length;
|
|
161
|
+
const widths = boxes.map((box) => box.width);
|
|
162
|
+
const mean = widths.reduce((sum, width) => sum + width, 0) / widths.length;
|
|
163
|
+
if (!(mean > 0.5)) return false;
|
|
164
|
+
const widthCv = Math.sqrt(widths.reduce((sum, width) => sum + (width - mean) ** 2, 0) / widths.length) / mean;
|
|
165
|
+
return alignFraction < P.DIAGRAM_MAX_ALIGN_FRACTION && widthCv >= P.DIAGRAM_MIN_WIDTH_CV;
|
|
123
166
|
};
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
167
|
+
// Air inserted strictly inside a container is painted by that container's own background, so a
|
|
168
|
+
// seam below the section's direct children needs no new ownership proof — only a solid enclosing
|
|
169
|
+
// ground that already spans the group and already continues past the cut.
|
|
170
|
+
const enclosingPainter = (group, after) => {
|
|
171
|
+
const groupRect = group.getBoundingClientRect();
|
|
172
|
+
const cut = after.getBoundingClientRect().top;
|
|
173
|
+
for (let node = group; node; node = node.parentElement) {
|
|
174
|
+
if (solidGround(node)) {
|
|
175
|
+
const rect = node.getBoundingClientRect();
|
|
176
|
+
if (rect.left <= groupRect.left + 0.75 && rect.right >= groupRect.right - 0.75
|
|
177
|
+
&& rect.top <= cut - 0.75 && rect.bottom >= cut + 0.75) return node;
|
|
178
|
+
}
|
|
179
|
+
if (node === group.closest('section[id^="viewport-"]')) break;
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
133
182
|
};
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const addition = tightScaled ? P.FLOOR : raw;
|
|
154
|
-
if (addition < P.FLOOR) {
|
|
155
|
-
return { ...s, addition, declined: addition > 0 ? 'below the 32 px perceptibility floor' : 'already at the page rhythm target' };
|
|
183
|
+
const scale = pageScaleTarget();
|
|
184
|
+
const selectorValue = (value) => window.CSS?.escape
|
|
185
|
+
? CSS.escape(value) : value.replace(/(["\\])/g, '\\$1');
|
|
186
|
+
const refusal = (after, reason, extra = {}) => ({
|
|
187
|
+
kind: after.matches('section[id^="viewport-"]') ? 'viewport' : 'semantic',
|
|
188
|
+
section: (after.closest('section[id^="viewport-"]') || after).id || null,
|
|
189
|
+
before: after.dataset.lySectionBefore || null,
|
|
190
|
+
after: after.id || null,
|
|
191
|
+
fact: after.dataset.lySectionFact || null,
|
|
192
|
+
gap: null, addition: 0, declared: true, declined: reason,
|
|
193
|
+
pageScale: scale.target == null ? null : {
|
|
194
|
+
target: +scale.target.toFixed(1), median: +scale.median.toFixed(1),
|
|
195
|
+
largest: +scale.largest.toFixed(1), structuralGaps: scale.gaps,
|
|
196
|
+
},
|
|
197
|
+
ground: { reason }, ...extra,
|
|
198
|
+
});
|
|
199
|
+
const validate = (after) => {
|
|
200
|
+
if (after.dataset.lySectionBoundaryRevision !== REVISION) {
|
|
201
|
+
return { ok: false, census: refusal(after, 'section-boundary-stale') };
|
|
156
202
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
203
|
+
if (!after.id) return { ok: false, census: refusal(after, 'section-boundary-after-unidentified') };
|
|
204
|
+
const beforeId = after.dataset.lySectionBefore || '';
|
|
205
|
+
const ownerKey = after.dataset.lySectionOwner || '';
|
|
206
|
+
const before = beforeId ? document.getElementById(beforeId) : null;
|
|
207
|
+
const owners = ownerKey
|
|
208
|
+
? [...document.querySelectorAll(`[data-ly-section-ground-owner="${selectorValue(ownerKey)}"]`)] : [];
|
|
209
|
+
if (!before) return { ok: false, census: refusal(after, 'section-boundary-before-missing') };
|
|
210
|
+
if (owners.length !== 1) return {
|
|
211
|
+
ok: false,
|
|
212
|
+
census: refusal(after, owners.length ? 'section-boundary-owner-ambiguous' : 'section-boundary-owner-missing'),
|
|
160
213
|
};
|
|
161
|
-
|
|
162
|
-
|
|
214
|
+
if (after.dataset.lySectionOpenability === 'not-openable') {
|
|
215
|
+
return { ok: false, census: refusal(after, 'declared-not-openable', { before: before.id }) };
|
|
216
|
+
}
|
|
217
|
+
if (after.dataset.lySectionOpenability !== 'openable'
|
|
218
|
+
|| after.dataset.lySectionContinuation !== 'owner-fill') {
|
|
219
|
+
return { ok: false, census: refusal(after, 'section-boundary-continuation-invalid', { before: before.id }) };
|
|
220
|
+
}
|
|
221
|
+
const owner = owners[0];
|
|
222
|
+
const afterSection = after.matches('section[id^="viewport-"]') ? after : after.closest('section[id^="viewport-"]');
|
|
223
|
+
const beforeSection = before.matches('section[id^="viewport-"]') ? before : before.closest('section[id^="viewport-"]');
|
|
224
|
+
const kind = after.matches('section[id^="viewport-"]') ? 'viewport' : 'semantic';
|
|
225
|
+
if (!afterSection || !beforeSection) return { ok: false, census: refusal(after, 'section-boundary-outside-viewport') };
|
|
226
|
+
let group = null; let painter = null;
|
|
227
|
+
if (kind === 'viewport') {
|
|
228
|
+
if (sectionIndex.get(afterSection) !== sectionIndex.get(beforeSection) + 1) {
|
|
229
|
+
return { ok: false, census: refusal(after, 'section-boundary-not-adjacent', { before: before.id }) };
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
// A semantic seam is air between two siblings. The pair may sit at any depth inside the
|
|
233
|
+
// section, not only among its direct children, provided the group they share is enclosed
|
|
234
|
+
// by ground that already paints the cut (see enclosingPainter).
|
|
235
|
+
if (afterSection !== beforeSection || after.parentElement !== before.parentElement) {
|
|
236
|
+
return { ok: false, census: refusal(after, 'section-boundary-crosses-owner-section', { before: before.id }) };
|
|
237
|
+
}
|
|
238
|
+
group = after.parentElement;
|
|
239
|
+
if (group !== afterSection && !afterSection.contains(group)) {
|
|
240
|
+
return { ok: false, census: refusal(after, 'section-boundary-outside-viewport', { before: before.id }) };
|
|
241
|
+
}
|
|
242
|
+
if (diagramLike(group)) {
|
|
243
|
+
return { ok: false, census: refusal(after, 'declared-inside-diagram-group', { before: before.id }) };
|
|
244
|
+
}
|
|
245
|
+
if (group !== afterSection) {
|
|
246
|
+
painter = enclosingPainter(group, after);
|
|
247
|
+
if (!painter) {
|
|
248
|
+
return { ok: false, census: refusal(after, 'nested-seam-ground-unpainted', { before: before.id }) };
|
|
249
|
+
}
|
|
250
|
+
// The painter supplies the paint proof; the declared owner still bounds WHERE the model
|
|
251
|
+
// authorised air, so the painter must lie inside that ground — by nesting or by covering.
|
|
252
|
+
const ownerBox = owner.getBoundingClientRect();
|
|
253
|
+
const painterBox = painter.getBoundingClientRect();
|
|
254
|
+
const insideOwner = owner === painter || owner.contains(painter)
|
|
255
|
+
|| (ownerBox.left <= painterBox.left + 0.75 && ownerBox.right >= painterBox.right - 0.75
|
|
256
|
+
&& ownerBox.top <= painterBox.top + 0.75 && ownerBox.bottom >= painterBox.bottom - 0.75);
|
|
257
|
+
if (!insideOwner) {
|
|
258
|
+
return { ok: false, census: refusal(after, 'nested-seam-ground-outside-owner', { before: before.id }) };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (!(owner.contains(afterSection) || afterSection.contains(owner))) {
|
|
263
|
+
return { ok: false, census: refusal(after, 'section-boundary-owner-disconnected', { before: before.id }) };
|
|
264
|
+
}
|
|
265
|
+
// A nested seam's ground is the enclosing painter, already proven solid and already spanning
|
|
266
|
+
// the cut, so the section-scale owner geometry below does not apply to it.
|
|
267
|
+
if (painter) {
|
|
268
|
+
return {
|
|
269
|
+
ok: true, kind, section: afterSection, group, painter, before, after, owner, ownerKey,
|
|
270
|
+
color: getComputedStyle(painter).backgroundColor, fact: after.dataset.lySectionFact || null,
|
|
271
|
+
execution: 'group-shift', ownerChildren: [],
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
const ownerStyle = getComputedStyle(owner);
|
|
275
|
+
const ownerRect = owner.getBoundingClientRect();
|
|
276
|
+
const sectionRect = afterSection.getBoundingClientRect();
|
|
277
|
+
const ownerIsAfter = owner === after;
|
|
278
|
+
const fullWidth = ownerRect.left <= sectionRect.left + 0.75
|
|
279
|
+
&& ownerRect.right >= sectionRect.right - 0.75;
|
|
280
|
+
const coversSection = fullWidth && ownerRect.top <= sectionRect.top + 0.75
|
|
281
|
+
&& ownerRect.bottom >= sectionRect.bottom - 0.75;
|
|
282
|
+
if (!(ownerIsAfter ? fullWidth : coversSection)) {
|
|
283
|
+
return { ok: false, census: refusal(after, ownerIsAfter
|
|
284
|
+
? 'declared-owner-not-full-width' : 'declared-owner-does-not-cover-section', { before: before.id }) };
|
|
285
|
+
}
|
|
286
|
+
const alpha = alphaOf(ownerStyle.backgroundColor);
|
|
287
|
+
const solid = ownerStyle.backgroundImage === 'none'
|
|
288
|
+
&& alpha >= (ownerIsAfter ? 0.01 : 0.999)
|
|
289
|
+
&& !/var\(/.test(ownerStyle.backgroundColor);
|
|
290
|
+
// owner-fill never stretches raster/image/texture owners. Unsupported
|
|
291
|
+
// continuation programs refuse this region explicitly.
|
|
292
|
+
if (!solid) return { ok: false, census: refusal(after, 'declared-owner-fill-unverifiable', { before: before.id }) };
|
|
293
|
+
if (ownerStyle.clipPath !== 'none' || ownerStyle.maskImage !== 'none') {
|
|
294
|
+
return { ok: false, census: refusal(after, 'declared-owner-clipped-or-masked', { before: before.id }) };
|
|
295
|
+
}
|
|
296
|
+
const ownerChildren = ownerIsAfter ? [...owner.children].filter(visible) : [];
|
|
297
|
+
if (ownerIsAfter && (!ownerChildren.length || ownerChildren.some((child) => !child.id))) {
|
|
298
|
+
return { ok: false, census: refusal(after, 'declared-owner-children-unidentified', { before: before.id }) };
|
|
299
|
+
}
|
|
163
300
|
return {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
301
|
+
ok: true, kind, section: afterSection, group: group || afterSection, painter: null,
|
|
302
|
+
before, after, owner, ownerKey,
|
|
303
|
+
color: ownerStyle.backgroundColor, fact: after.dataset.lySectionFact || null,
|
|
304
|
+
execution: ownerIsAfter ? 'owner-leading-air' : 'section-shift', ownerChildren,
|
|
167
305
|
};
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
306
|
+
};
|
|
307
|
+
const inkEdge = (section, edge) => {
|
|
308
|
+
const sectionRect = section.getBoundingClientRect();
|
|
309
|
+
const ink = [...section.querySelectorAll('.text,.paragraph,h1,h2,h3,h4,h5,h6,.icon,img,.image,button,.button')]
|
|
310
|
+
.filter(visible).map((element) => element.getBoundingClientRect());
|
|
311
|
+
if (!ink.length) return null;
|
|
312
|
+
return edge === 'bottom'
|
|
313
|
+
? Math.max(...ink.map((rect) => rect.bottom)) - sectionRect.top
|
|
314
|
+
: Math.min(...ink.map((rect) => rect.top)) - sectionRect.top;
|
|
315
|
+
};
|
|
316
|
+
const valid = []; const census = [];
|
|
317
|
+
for (const after of declarations) {
|
|
318
|
+
const declaration = validate(after);
|
|
319
|
+
if (!declaration.ok) { census.push(declaration.census); continue; }
|
|
320
|
+
let gap;
|
|
321
|
+
if (declaration.kind === 'viewport') {
|
|
322
|
+
const beforeRect = declaration.before.getBoundingClientRect();
|
|
323
|
+
const afterRect = declaration.after.getBoundingClientRect();
|
|
324
|
+
const lastInk = inkEdge(declaration.before, 'bottom');
|
|
325
|
+
const firstInk = inkEdge(declaration.after, 'top');
|
|
326
|
+
if (lastInk == null || firstInk == null) {
|
|
327
|
+
census.push(refusal(after, 'section-boundary-ink-unmeasurable', { before: declaration.before.id }));
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
gap = (beforeRect.height - lastInk) + (afterRect.top - beforeRect.bottom) + firstInk;
|
|
331
|
+
} else {
|
|
332
|
+
// Measure the air the page actually shows at this seam, not the distance between the two
|
|
333
|
+
// declared boxes: a floating card or an intro column that outlives `before` is what the
|
|
334
|
+
// eye reads the air from, and it is what the inserted band will actually sit under.
|
|
335
|
+
const air = realAirLine(declaration.group, declaration.after);
|
|
336
|
+
if (!air) {
|
|
337
|
+
census.push(refusal(after, 'section-boundary-air-unmeasurable', { before: declaration.before.id }));
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
gap = air.gap;
|
|
341
|
+
declaration.airAnchor = air.anchor;
|
|
342
|
+
declaration.airPeers = air.peers;
|
|
343
|
+
}
|
|
344
|
+
if (!Number.isFinite(gap) || gap < 0) {
|
|
345
|
+
census.push(refusal(after, 'section-boundary-gap-invalid', { before: declaration.before.id, gap }));
|
|
346
|
+
continue;
|
|
209
347
|
}
|
|
348
|
+
valid.push({ ...declaration, gap });
|
|
210
349
|
}
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
350
|
+
const viewport = valid.filter((row) => row.kind === 'viewport');
|
|
351
|
+
const viewportGaps = viewport.map((row) => row.gap).sort((a, b) => a - b);
|
|
352
|
+
const medianGap = viewportGaps.length % 2
|
|
353
|
+
? viewportGaps[Math.floor(viewportGaps.length / 2)]
|
|
354
|
+
: viewportGaps.length ? (viewportGaps[viewportGaps.length / 2 - 1] + viewportGaps[viewportGaps.length / 2]) / 2 : null;
|
|
355
|
+
const viewportTarget = viewport.length === 1 ? viewport[0].gap + P.FLOOR
|
|
356
|
+
: viewport.length ? Math.max(P.MIN_TARGET, Math.min(P.MAX_TARGET, Math.ceil((medianGap + P.FLOOR) / 8) * 8)) : null;
|
|
357
|
+
const plans = valid.map((row) => {
|
|
358
|
+
if (row.kind !== 'viewport') {
|
|
359
|
+
// Page-rhythm model: bring an under-scale seam up to what this page already does elsewhere,
|
|
360
|
+
// and never invent an amount the page has not itself used. addition = max(0, T - gap).
|
|
361
|
+
const floor = P.PERCEPTIBILITY_FLOOR;
|
|
362
|
+
const addition = scale.target == null ? 0
|
|
363
|
+
: +Math.min(P.CAP, Math.max(0, scale.target - row.gap)).toFixed(1);
|
|
364
|
+
const capped = scale.target != null && scale.target - row.gap > P.CAP;
|
|
365
|
+
const declined = scale.target == null ? 'page has no structural gap to set a scale from'
|
|
366
|
+
: addition < floor
|
|
367
|
+
? (addition > 0 ? `below the ${floor} px perceptibility floor`
|
|
368
|
+
: 'already at the page rhythm target')
|
|
369
|
+
: null;
|
|
370
|
+
census.push({
|
|
371
|
+
kind: row.kind, section: row.section.id, group: row.group?.id || row.section.id,
|
|
372
|
+
before: row.before.id, airAnchor: row.airAnchor?.id || null, after: row.after.id,
|
|
373
|
+
fact: row.fact, gap: +row.gap.toFixed(1), addition, capped, declared: true, declined,
|
|
374
|
+
pageScale: scale.target == null ? null : {
|
|
375
|
+
target: +scale.target.toFixed(1), median: +scale.median.toFixed(1),
|
|
376
|
+
largest: +scale.largest.toFixed(1), structuralGaps: scale.gaps,
|
|
377
|
+
},
|
|
378
|
+
ground: { owner: row.ownerKey, color: row.color, continuation: 'owner-fill', execution: row.execution },
|
|
379
|
+
});
|
|
380
|
+
return { ...row, addition, capped, declined };
|
|
381
|
+
}
|
|
382
|
+
const raw = Math.round(Math.min(P.CAP, Math.max(0, viewportTarget - row.gap)));
|
|
383
|
+
const tightScaled = raw > 0 && raw < P.FLOOR && row.gap < P.TIGHT_GAP;
|
|
384
|
+
const addition = tightScaled ? P.FLOOR : raw;
|
|
385
|
+
const declined = addition < P.FLOOR
|
|
386
|
+
? (addition > 0 ? 'below the 32 px perceptibility floor'
|
|
387
|
+
: 'already at the page rhythm target')
|
|
388
|
+
: null;
|
|
389
|
+
census.push({
|
|
390
|
+
kind: row.kind, section: row.section.id, before: row.before.id, after: row.after.id,
|
|
391
|
+
fact: row.fact, gap: +row.gap.toFixed(1), addition, tightScaled, declared: true, declined,
|
|
392
|
+
ground: { owner: row.ownerKey, color: row.color, continuation: 'owner-fill', execution: row.execution },
|
|
393
|
+
});
|
|
394
|
+
return { ...row, addition, tightScaled, declined };
|
|
218
395
|
});
|
|
219
|
-
if (P.census) return
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const sectionShift = new Map(sections.map((s) => [s.id, 0]));
|
|
227
|
-
const sectionGrowth = new Map(sections.map((s) => [s.id, 0]));
|
|
396
|
+
if (P.census) return census;
|
|
397
|
+
const admitted = plans.filter((plan) => !plan.declined);
|
|
398
|
+
if (!admitted.length) return out;
|
|
399
|
+
const sectionShift = new Map(sections.map((section) => [section.id, 0]));
|
|
400
|
+
const sectionGrowth = new Map(sections.map((section) => [section.id, 0]));
|
|
401
|
+
const ownerGrowth = new Map();
|
|
402
|
+
const containerGrowth = new Map();
|
|
228
403
|
const childShift = new Map();
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
404
|
+
for (const plan of admitted.filter((row) => row.kind === 'semantic')) {
|
|
405
|
+
sectionGrowth.set(plan.section.id, sectionGrowth.get(plan.section.id) + plan.addition);
|
|
406
|
+
if (plan.owner !== plan.section && plan.section.contains(plan.owner) && plan.owner.id) {
|
|
407
|
+
ownerGrowth.set(plan.owner.id, (ownerGrowth.get(plan.owner.id) || 0) + plan.addition);
|
|
408
|
+
}
|
|
409
|
+
if (plan.execution === 'owner-leading-air') {
|
|
410
|
+
for (const child of plan.ownerChildren) {
|
|
411
|
+
childShift.set(child.id, (childShift.get(child.id) || 0) + plan.addition);
|
|
412
|
+
}
|
|
413
|
+
} else {
|
|
414
|
+
// Everything at or below the cut inside the seam's own group moves down; every container
|
|
415
|
+
// between that group and the section grows by the same amount so nothing is clipped.
|
|
416
|
+
const afterTop = plan.after.getBoundingClientRect().top;
|
|
417
|
+
for (const child of [...plan.group.children].filter(visible)) {
|
|
418
|
+
if (child.id && child.getBoundingClientRect().top >= afterTop - 0.75) {
|
|
419
|
+
childShift.set(child.id, (childShift.get(child.id) || 0) + plan.addition);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
for (let node = plan.group; node && node !== plan.section; node = node.parentElement) {
|
|
423
|
+
if (node.id) containerGrowth.set(node.id, (containerGrowth.get(node.id) || 0) + plan.addition);
|
|
424
|
+
}
|
|
235
425
|
}
|
|
236
426
|
}
|
|
237
|
-
for (const
|
|
238
|
-
for (let
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
for (const p of admittedViewport) {
|
|
242
|
-
const dy = p.ground.side === 'outgoing' ? p.addition : -p.addition;
|
|
243
|
-
const br = p.before.getBoundingClientRect();
|
|
244
|
-
const attachments = p.attachments.map((a) => ({ id: a.el.id, topOffset: a.topOffset, bottomOffset: a.bottomOffset }));
|
|
427
|
+
for (const plan of admitted.filter((row) => row.kind === 'viewport')) {
|
|
428
|
+
for (let cursor = sectionIndex.get(plan.after); cursor < sections.length; cursor += 1) {
|
|
429
|
+
sectionShift.set(sections[cursor].id, sectionShift.get(sections[cursor].id) + plan.addition);
|
|
430
|
+
}
|
|
245
431
|
out.push({
|
|
246
|
-
id:
|
|
247
|
-
reason: `viewport
|
|
248
|
-
css: { 'box-shadow': `0 ${
|
|
432
|
+
id: plan.before.id, rule: RULE,
|
|
433
|
+
reason: `declared viewport boundary ${plan.before.id}→${plan.after.id}: +${plan.addition} px`,
|
|
434
|
+
css: { 'box-shadow': `0 ${plan.addition}px 0 0 ${plan.color}` },
|
|
249
435
|
meta: { seam: {
|
|
250
|
-
kind:
|
|
251
|
-
|
|
252
|
-
|
|
436
|
+
kind: 'viewport', before: plan.before.id, after: plan.after.id,
|
|
437
|
+
gap: +plan.gap.toFixed(1), baseStripHeight: 0, addition: plan.addition,
|
|
438
|
+
color: plan.color, side: 'outgoing', owner: plan.ownerKey, attachments: [],
|
|
439
|
+
seamTop: plan.before.getBoundingClientRect().bottom + window.scrollY,
|
|
253
440
|
} },
|
|
254
441
|
});
|
|
255
|
-
for (const a of p.attachments) {
|
|
256
|
-
const direct = directChildOf(p.before, a.el); const inherited = direct ? (childShift.get(direct.id) || 0) : 0;
|
|
257
|
-
const travel = p.addition + sectionGrowth.get(p.before.id) - inherited;
|
|
258
|
-
if (travel) out.push({ id: a.el.id, rule: 'section-breathing', reason: `declared decoration travels ${travel} px with ${p.after.id}`, dy: travel });
|
|
259
|
-
}
|
|
260
442
|
}
|
|
261
|
-
for (const
|
|
443
|
+
for (const plan of admitted.filter((row) => row.kind === 'semantic')) {
|
|
444
|
+
// The probe strip is anchored to the real air line, so the pixels verified are the pixels the
|
|
445
|
+
// addition actually inserts — not a band that still contains the peer it was measured past.
|
|
446
|
+
const airAnchor = plan.airAnchor || plan.before;
|
|
447
|
+
if (plan.execution === 'owner-leading-air') {
|
|
448
|
+
const firstChild = [...plan.ownerChildren]
|
|
449
|
+
.sort((left, right) => left.getBoundingClientRect().top - right.getBoundingClientRect().top)[0];
|
|
450
|
+
const baseStripHeight = firstChild.getBoundingClientRect().top
|
|
451
|
+
- airAnchor.getBoundingClientRect().bottom;
|
|
452
|
+
out.push({
|
|
453
|
+
id: firstChild.id, rule: RULE,
|
|
454
|
+
reason: `declared ${plan.fact || 'section'} boundary ${plan.before.id}→${plan.after.id}: +${plan.addition} px inside owner`,
|
|
455
|
+
dy: plan.addition,
|
|
456
|
+
meta: { seam: {
|
|
457
|
+
kind: 'semantic', fact: plan.fact, before: airAnchor.id, after: firstChild.id,
|
|
458
|
+
declaredBefore: plan.before.id, declaredAfter: plan.after.id, gap: +plan.gap.toFixed(1),
|
|
459
|
+
baseStripHeight: +baseStripHeight.toFixed(2), addition: plan.addition,
|
|
460
|
+
color: plan.color, side: 'owner-leading', owner: plan.ownerKey, attachments: [],
|
|
461
|
+
} },
|
|
462
|
+
});
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
const beforePeers = [...plan.group.children].filter((child) => child.id
|
|
466
|
+
&& child !== plan.after && child !== plan.owner
|
|
467
|
+
&& child.getBoundingClientRect().bottom <= plan.after.getBoundingClientRect().top + 0.75)
|
|
468
|
+
.map((child) => child.id);
|
|
262
469
|
out.push({
|
|
263
|
-
id:
|
|
264
|
-
reason:
|
|
470
|
+
id: plan.after.id, rule: RULE,
|
|
471
|
+
reason: `declared ${plan.fact || 'section'} boundary ${plan.before.id}→${plan.after.id}: +${plan.addition} px`,
|
|
472
|
+
dy: plan.addition,
|
|
265
473
|
meta: { seam: {
|
|
266
|
-
kind:
|
|
267
|
-
|
|
268
|
-
|
|
474
|
+
kind: 'semantic', fact: plan.fact, before: airAnchor.id, after: plan.after.id,
|
|
475
|
+
declaredBefore: plan.before.id,
|
|
476
|
+
gap: +plan.gap.toFixed(1), baseStripHeight: +plan.gap.toFixed(2), addition: plan.addition,
|
|
477
|
+
color: plan.color, side: 'internal', owner: plan.ownerKey, attachments: [], beforePeers,
|
|
269
478
|
} },
|
|
270
479
|
});
|
|
271
480
|
}
|
|
272
481
|
for (const [id, dy] of childShift) {
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
482
|
+
const own = admitted.filter((plan) => plan.kind === 'semantic'
|
|
483
|
+
&& (plan.after.id === id || (plan.execution === 'owner-leading-air'
|
|
484
|
+
&& [...plan.ownerChildren].sort((left, right) => left.getBoundingClientRect().top
|
|
485
|
+
- right.getBoundingClientRect().top)[0]?.id === id)))
|
|
486
|
+
.reduce((total, plan) => total + plan.addition, 0);
|
|
487
|
+
if (dy - own) out.push({ id, rule: RULE, reason: 'block follows declared semantic air', dy: dy - own });
|
|
276
488
|
}
|
|
277
489
|
for (const section of sections) {
|
|
278
|
-
const growth = sectionGrowth.get(section.id);
|
|
279
|
-
|
|
280
|
-
const or = owner.getBoundingClientRect();
|
|
281
|
-
out.push({ id: owner.id, rule: 'section-breathing', reason: `solid section ground grows ${growth} px`, h: or.height + growth });
|
|
282
|
-
out.push({ id: section.id, rule: 'section-breathing', reason: `section grows ${growth} px for semantic air`, h: sr.height + growth });
|
|
490
|
+
const growth = sectionGrowth.get(section.id);
|
|
491
|
+
if (growth) out.push({ id: section.id, rule: RULE, reason: `section grows ${growth} px for declared semantic air`, h: section.getBoundingClientRect().height + growth });
|
|
283
492
|
}
|
|
284
|
-
for (const [id,
|
|
285
|
-
|
|
493
|
+
for (const [id, growth] of ownerGrowth) {
|
|
494
|
+
const owner = document.getElementById(id);
|
|
495
|
+
if (owner) out.push({ id, rule: RULE, reason: `declared ground owner continues ${growth} px`, h: owner.getBoundingClientRect().height + growth });
|
|
496
|
+
}
|
|
497
|
+
for (const [id, growth] of containerGrowth) {
|
|
498
|
+
if (ownerGrowth.has(id)) continue;
|
|
499
|
+
const container = document.getElementById(id);
|
|
500
|
+
if (container) out.push({ id, rule: RULE, reason: `container grows ${growth} px for nested declared air`, h: container.getBoundingClientRect().height + growth });
|
|
501
|
+
}
|
|
502
|
+
for (const [id, dy] of sectionShift) if (dy) out.push({ id, rule: RULE, reason: `section follows ${dy} px of declared air`, dy });
|
|
503
|
+
const total = admitted.reduce((sum, plan) => sum + plan.addition, 0);
|
|
286
504
|
const canvas = document.querySelector('#page-flow') || document.querySelector('.ly-anchor-canvas');
|
|
287
|
-
const
|
|
288
|
-
|
|
505
|
+
const expectedDocumentHeight = Math.max(
|
|
506
|
+
document.documentElement.scrollHeight,
|
|
507
|
+
canvas ? canvas.getBoundingClientRect().bottom + window.scrollY + total : 0,
|
|
508
|
+
);
|
|
509
|
+
for (const override of out) {
|
|
510
|
+
if (override.meta?.seam) override.meta.seam.expectedDocumentHeight = expectedDocumentHeight;
|
|
511
|
+
}
|
|
512
|
+
if (canvas) out.push({ sel: '#page-flow, .ly-anchor-canvas', rule: RULE, reason: `canvas +${total} px`, css: { height: `${(canvas.getBoundingClientRect().height + total).toFixed(2)}px` } });
|
|
289
513
|
return out;
|
|
290
514
|
};
|
|
291
515
|
}
|