Emission geometry wave: occupying off-screen items, height/width chain trust, subgrid, track-relative percentages, banded override precedence
- Off-viewport in-flow items (carousel slides beyond the right edge) keep their per-viewport geometry deltas - they occupy layout and set track cross-sizes even when not visible - Height chains: single-viewport probe verdicts accepted for nodes that only paint at one band; un-probed picture/object-fit:cover children filling the parent count as height-deriving; a node whose own probe says authored-explicit keeps its height at emission (twin of the flow guard) - Symmetric authored-width circular guard in the walker (height-only before), harvesting shadow-root adopted/style sheets for custom elements - Auto margins only when width is genuinely content-constrained; gutter- inset full-width blocks keep literal margins - grid-template-rows: subgrid never dropped; variant-only subgrid intent re-enters via partial-coverage escape; grid-item percentage widths bail to auto (they resolve against the track, not the container) - Banded raw-prop detection counts non-raw band overrides so a banded bg-none reset isn't beaten by an inlined gradient 399 tests pass (13 new), typecheck clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
144e7ab16b
commit
1d68f16f96
@@ -410,8 +410,22 @@ export function collectPage(opts?: { maxNodes?: number } | void): PageSnapshot {
|
||||
hAuto = false;
|
||||
hFill = false;
|
||||
}
|
||||
let wAuto = Math.abs(wa - r.width) <= 0.5;
|
||||
let wFill = Math.abs(wf - r.width) <= 0.5;
|
||||
// Circular-WIDTH guard (the mirror of the height guard above): an authored `width:24px` inside a
|
||||
// SHRINK-TO-FIT parent (a color-swatch link in a span that shrink-wraps to it) makes BOTH
|
||||
// `width:auto` and `width:100%` reproduce the box — the parent's width still holds while the
|
||||
// child re-measures — so the raw verdict reads wAuto/wFill (drop) and the clone collapses the
|
||||
// child to 0 content width (and the shrink-wrap span to its borders). When this element's own
|
||||
// cascade/inline style declares an explicit definite width, trust it: the width is authored, so
|
||||
// it is neither content-sized (wAuto) nor a parent fill (wFill). Only overrides when a probe
|
||||
// actually reproduced — a genuine explicit width that auto already shrinks stays wAuto:false.
|
||||
if ((wAuto || wFill) && r.width > 2 && authorsExplicitWidth(el, sw)) {
|
||||
wAuto = false;
|
||||
wFill = false;
|
||||
}
|
||||
sizing = {
|
||||
wAuto: Math.abs(wa - r.width) <= 0.5, wFill: Math.abs(wf - r.width) <= 0.5, hAuto,
|
||||
wAuto, wFill, hAuto,
|
||||
hFill,
|
||||
wMin: Math.round(wmin * 100) / 100, wMax: Math.round(wmax * 100) / 100,
|
||||
};
|
||||
@@ -579,6 +593,12 @@ export function collectPage(opts?: { maxNodes?: number } | void): PageSnapshot {
|
||||
// authored dimension gets dropped downstream. A declared explicit length is ground truth that
|
||||
// the height is load-bearing, so we trust the declaration over the reflow verdict.
|
||||
const explicitHeightSelectors: string[] = [];
|
||||
// The WIDTH twin of explicitHeightSelectors: selectors that author an explicit definite `width`/
|
||||
// `min-width` (px/rem/em/…, not auto/%/keyword). Consulted by the sizing probe to break a CIRCULAR
|
||||
// width verdict — an authored `width:24px` inside a shrink-to-fit parent reproduces at both auto and
|
||||
// 100% (the parent's width still holds), so the probe alone reads wAuto and the authored width gets
|
||||
// dropped, collapsing the box in the clone.
|
||||
const explicitWidthSelectors: string[] = [];
|
||||
|
||||
const absUrl = (u: string): string => {
|
||||
try { return new URL(u, document.baseURI).href; } catch { return u; }
|
||||
@@ -627,6 +647,22 @@ export function collectPage(opts?: { maxNodes?: number } | void): PageSnapshot {
|
||||
return false;
|
||||
};
|
||||
|
||||
// The WIDTH twin of authorsExplicitHeight: does this element author an explicit definite width via
|
||||
// its own inline style (`inlineWidth`, already read by the probe) or any matched harvested rule?
|
||||
// `isExplicitHeight` is unit-agnostic (it only rejects auto/%/keywords and matches definite lengths),
|
||||
// so it doubles as the width predicate.
|
||||
const authorsExplicitWidth = (el: Element, inlineWidth: string): boolean => {
|
||||
if (isExplicitHeight(inlineWidth)) return true;
|
||||
try {
|
||||
const inlineMin = (el as HTMLElement).style?.getPropertyValue("min-width") || "";
|
||||
if (isExplicitHeight(inlineMin)) return true;
|
||||
} catch { /* ignore */ }
|
||||
for (const sel of explicitWidthSelectors) {
|
||||
try { if (el.matches(sel)) return true; } catch { /* invalid/unsupported selector */ }
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const readRules = (rules: CSSRuleList): void => {
|
||||
for (const rule of Array.from(rules)) {
|
||||
const type = rule.constructor.name;
|
||||
@@ -657,6 +693,11 @@ export function collectPage(opts?: { maxNodes?: number } | void): PageSnapshot {
|
||||
isExplicitHeight(r.style.getPropertyValue("min-height")))) {
|
||||
explicitHeightSelectors.push(r.selectorText);
|
||||
}
|
||||
if (r.selectorText && r.style &&
|
||||
(isExplicitHeight(r.style.getPropertyValue("width")) ||
|
||||
isExplicitHeight(r.style.getPropertyValue("min-width")))) {
|
||||
explicitWidthSelectors.push(r.selectorText);
|
||||
}
|
||||
} else if (type === "CSSMediaRule" || type === "CSSSupportsRule") {
|
||||
try { readRules((rule as CSSGroupingRule).cssRules); } catch { /* ignore */ }
|
||||
} else if (type === "CSSImportRule") {
|
||||
@@ -676,6 +717,39 @@ export function collectPage(opts?: { maxNodes?: number } | void): PageSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
// Custom elements (web components) carry their styles INSIDE their open shadow root — via
|
||||
// `adoptedStyleSheets` (constructable sheets) and/or `<style>`/`<link>` in the shadow tree
|
||||
// (`shadowRoot.styleSheets`). Those sheets never appear in `document.styleSheets`, so an authored
|
||||
// rule like `color-swatch a { width: 24px }` is invisible to the harvest above and the sizing probe
|
||||
// can't see the explicit width. Walk every open shadow root (composed-tree sweep, matching the
|
||||
// serializer) and read its sheets too — same `readRules` (font-faces, url()s, explicit width/height
|
||||
// selectors). Closed roots return null and are skipped, exactly as elsewhere.
|
||||
const shadowRoots: ShadowRoot[] = [];
|
||||
const collectShadowRoots = (): void => {
|
||||
const stack: Element[] = [];
|
||||
const pushChildren = (parent: Element | Document | ShadowRoot): void => {
|
||||
let c = parent.firstElementChild;
|
||||
while (c) { stack.push(c); c = c.nextElementSibling; }
|
||||
};
|
||||
pushChildren(document);
|
||||
// Depth-first over the composed element tree, descending into every open shadow root.
|
||||
while (stack.length) {
|
||||
const node = stack.pop()!;
|
||||
const sr = (node as HTMLElement).shadowRoot;
|
||||
if (sr) { shadowRoots.push(sr); pushChildren(sr); }
|
||||
pushChildren(node);
|
||||
}
|
||||
};
|
||||
try { collectShadowRoots(); } catch { /* ignore */ }
|
||||
for (const sr of shadowRoots) {
|
||||
const sheets: CSSStyleSheet[] = [];
|
||||
try { sheets.push(...(sr.adoptedStyleSheets || [])); } catch { /* ignore */ }
|
||||
try { sheets.push(...Array.from(sr.styleSheets || [])); } catch { /* ignore */ }
|
||||
for (const sheet of sheets) {
|
||||
try { readRules(sheet.cssRules); } catch { if ((sheet as CSSStyleSheet).href) cssUrlSet.add(absUrl((sheet as CSSStyleSheet).href!)); }
|
||||
}
|
||||
}
|
||||
|
||||
// CSS custom properties declared on :root.
|
||||
try {
|
||||
const rootCs = window.getComputedStyle(document.documentElement);
|
||||
|
||||
Reference in New Issue
Block a user