Improve modern layout and viewport rendering

This commit is contained in:
SGavrl committed 2026-07-30 20:24:57 +00:00
1 parent aff0172987
commit 10b8492116
22 files changed
+5978 -450

No files matched your search

+657 -120
View File
@@ -657,8 +657,66 @@ globalThis.setInterval = (fn, delay = 0, ...args) => {
};
globalThis.clearInterval = (id) => { _intervals.delete(id); _clearedTimers.add(id); };
globalThis.requestAnimationFrame = (fn) => setTimeout(fn, 0);
globalThis.cancelAnimationFrame = globalThis.clearTimeout;
// Animation callbacks are a rendering-phase batch, not zero-delay
// microtasks. In particular, a callback which queues itself must yield to
// timers, networking, and the embedder between frames. The old setTimeout(0)
// alias eventually used Promise.resolve(), so a normal animation loop formed
// an unbounded microtask chain and pinned V8 until the watchdog terminated it.
const _RAF_FRAME_DELAY_MS = 16;
let _rafPending = new Map();
let _rafCurrentBatch = null;
let _rafFrameScheduled = false;
let _rafRunningFrame = false;
function _scheduleAnimationFrame() {
if (_rafFrameScheduled || _rafRunningFrame || _rafPending.size === 0) return;
_rafFrameScheduled = true;
_scheduleAfter(_RAF_FRAME_DELAY_MS, () => {
_rafFrameScheduled = false;
if (_rafPending.size === 0) return;
// Swap before invoking anything. A callback requested while this batch is
// running therefore belongs to the next frame. Every callback in this
// batch receives the same rendering timestamp.
const batch = _rafPending;
_rafPending = new Map();
_rafCurrentBatch = batch;
_rafRunningFrame = true;
const timestamp = performance.now();
try {
for (const [id, callback] of batch) {
// cancelAnimationFrame() may remove a later callback while an earlier
// callback in the same frame is running.
if (!batch.has(id)) continue;
batch.delete(id);
try { callback(timestamp); }
catch (e) { console.error("Animation frame error:", e); }
}
} finally {
_rafRunningFrame = false;
_rafCurrentBatch = null;
_scheduleAnimationFrame();
}
});
}
globalThis.requestAnimationFrame = (fn) => {
if (typeof fn !== "function") {
throw new TypeError(
"Failed to execute 'requestAnimationFrame' on 'Window': parameter 1 is not of type 'Function'."
);
}
const id = ++_tid;
_rafPending.set(id, fn);
_scheduleAnimationFrame();
return id;
};
globalThis.cancelAnimationFrame = (id) => {
_rafPending.delete(id);
if (_rafCurrentBatch) _rafCurrentBatch.delete(id);
};
globalThis.queueMicrotask = globalThis.queueMicrotask || ((fn) => Promise.resolve().then(fn));
class MessageChannel {
@@ -2687,49 +2745,103 @@ class Element extends Node {
// returning 100x20 there made every element appear off-screen and broke .click().
get clientWidth() { return this._isViewportRoot() ? (globalThis.innerWidth || 1280) : 100; }
get clientHeight() { return this._isViewportRoot() ? (globalThis.innerHeight || 720) : 20; }
get scrollWidth() { return this._isViewportRoot() ? (globalThis.innerWidth || 1280) : 100; }
get scrollHeight() { return this._isViewportRoot() ? (globalThis.innerHeight || 720) : 20; }
get scrollWidth() {
if (!this._isViewportRoot()) return 100;
const metrics = this._renderScrollMetrics();
return metrics ? metrics.scrollWidth : (globalThis.innerWidth || 1280);
}
get scrollHeight() {
if (!this._isViewportRoot()) return 20;
const metrics = this._renderScrollMetrics();
return metrics ? metrics.scrollHeight : (globalThis.innerHeight || 720);
}
_isViewportRoot() {
const t = this.tagName;
return t === 'HTML' || t === 'BODY';
}
// No layout engine, so there is no real overflow to scroll and the offset is
// deliberately NOT clamped: without real geometry any synthetic max is a
// guess, and a max derived from a stub scroll box pins scrollTop at 0, which
// deadlocks scroll-driven lazy loaders (no scroll -> no content -> no scroll).
// We track the offset so scrollTop/scrollLeft round-trip, and fire a scroll
// event on direct assignment -- lazy loaders that set `el.scrollTop = N` rely
// on that event, and scrollTo/scrollBy below would otherwise be its only source.
get scrollTop() { return this._scrollTop || 0; }
_renderScrollMetrics() {
if (typeof Deno.core.ops.op_layout_metrics !== 'function') return null;
try {
const raw = Deno.core.ops.op_layout_metrics();
return raw ? JSON.parse(raw) : null;
} catch (_e) {
return null;
}
}
_renderScrollOffset() {
if (typeof Deno.core.ops.op_scroll_offset !== 'function') return null;
try {
const raw = Deno.core.ops.op_scroll_offset();
return raw ? JSON.parse(raw) : null;
} catch (_e) {
return null;
}
}
_setRenderScroll(x, y) {
if (typeof Deno.core.ops.op_scroll_to !== 'function') return null;
try {
const raw = Deno.core.ops.op_scroll_to(+x || 0, +y || 0);
return raw ? JSON.parse(raw) : null;
} catch (_e) {
return null;
}
}
// Render builds clamp the viewport root against measured document overflow.
// Nested element scrollers still use synthetic, deliberately unclamped state:
// their contents do not yet participate in scroll-container layout, and a
// guessed maximum would pin lazy-loading scrollers at zero.
get scrollTop() {
if (this._isViewportRoot()) {
const offset = this._renderScrollOffset();
if (offset) return offset.y || 0;
}
return this._scrollTop || 0;
}
set scrollTop(v) {
v = +v;
const nv = Number.isFinite(v) && v > 0 ? v : 0;
const changed = nv !== (this._scrollTop || 0);
this._scrollTop = nv;
const old = this.scrollTop;
let actual = nv;
if (this._isViewportRoot()) {
const offset = this._renderScrollOffset();
const updated = offset && this._setRenderScroll(offset.x, nv);
if (updated) actual = updated.y || 0;
}
const changed = actual !== old;
this._scrollTop = actual;
if (changed && !this._scrollSuppress) this._fireScroll();
if (changed && this._isViewportRoot() &&
typeof globalThis.__obscura_recompute_intersections === "function") {
globalThis.__obscura_recompute_intersections();
}
}
get scrollLeft() {
if (this._isViewportRoot()) {
const offset = this._renderScrollOffset();
if (offset) return offset.x || 0;
}
return this._scrollLeft || 0;
}
get scrollLeft() { return this._scrollLeft || 0; }
set scrollLeft(v) {
v = +v;
const nv = Number.isFinite(v) && v > 0 ? v : 0;
const changed = nv !== (this._scrollLeft || 0);
this._scrollLeft = nv;
const old = this.scrollLeft;
let actual = nv;
if (this._isViewportRoot()) {
const offset = this._renderScrollOffset();
const updated = offset && this._setRenderScroll(nv, offset.y);
if (updated) actual = updated.x || 0;
}
const changed = actual !== old;
this._scrollLeft = actual;
if (changed && !this._scrollSuppress) this._fireScroll();
if (changed && this._isViewportRoot() &&
typeof globalThis.__obscura_recompute_intersections === "function") {
globalThis.__obscura_recompute_intersections();
}
}
getBoundingClientRect() {
globalThis.__obscura_click_target = this;
// documentElement and body span the full viewport. Without this every
// hit test against them clips down to a 100x20 synthetic cell and
// Document.elementFromPoint can never recurse into their children.
if (this._isViewportRoot()) {
const vw = globalThis.innerWidth || 1280;
const vh = globalThis.innerHeight || 720;
return {
x: 0, y: 0, width: vw, height: vh,
top: 0, right: vw, bottom: vh, left: 0,
toJSON() { return this; },
};
}
// Real layout when the render feature is compiled in: ask the Rust layout
// cache for this element's border box. The op is absent in the default
// build, so probe with typeof and fall through to the synthetic rect below.
@@ -2740,15 +2852,32 @@ class Element extends Node {
var _r = JSON.parse(_geom);
if (_r && typeof _r.width === 'number') {
var _x = _r.x, _y = _r.y, _w = _r.width, _h = _r.height;
return {
var _rect = {
x: _x, y: _y, width: _w, height: _h,
top: _y, right: _x + _w, bottom: _y + _h, left: _x,
toJSON() { return this; },
};
Object.defineProperty(_rect, "__obscuraViewportFixed", {
value: !!_r.viewportFixed,
enumerable: false,
});
return _rect;
}
}
} catch (_e) {}
}
// Default (non-render) builds keep viewport-sized roots. Without this
// synthetic fallback every hit test against them clips down to a 100x20
// cell and Document.elementFromPoint cannot recurse into their children.
if (this._isViewportRoot()) {
const vw = globalThis.innerWidth || 1280;
const vh = globalThis.innerHeight || 720;
return {
x: 0, y: 0, width: vw, height: vh,
top: 0, right: vw, bottom: vh, left: 0,
toJSON() { return this; },
};
}
// No layout engine (default build) or this node has no box: synthesize a
// deterministic position from the node id so Playwright's actionability
// polling still gets a stable, distinct rect for hit-testing (issue #45).
@@ -2790,7 +2919,40 @@ class Element extends Node {
set ariaHidden(v) { if (v == null) this.removeAttribute('aria-hidden'); else this.setAttribute('aria-hidden', String(v)); }
get ariaSelected() { return this.getAttribute('aria-selected'); }
set ariaSelected(v) { if (v == null) this.removeAttribute('aria-selected'); else this.setAttribute('aria-selected', String(v)); }
scrollIntoView() { globalThis.__obscura_click_target = this; }
scrollIntoView(arg) {
globalThis.__obscura_click_target = this;
const rect = this.getBoundingClientRect();
// A viewport-fixed subtree is already expressed in the viewport's
// coordinate space and cannot be brought closer by moving the document.
if (rect.__obscuraViewportFixed) return;
let block = "start", inline = "nearest";
if (arg === false) block = "end";
else if (arg && typeof arg === "object") {
if (["start", "center", "end", "nearest"].includes(arg.block)) block = arg.block;
if (["start", "center", "end", "nearest"].includes(arg.inline)) inline = arg.inline;
}
const currentX = globalThis.scrollX || 0;
const currentY = globalThis.scrollY || 0;
const vw = globalThis.innerWidth || 1280;
const vh = globalThis.innerHeight || 720;
const align = (mode, start, end, size, viewportSize, current) => {
if (mode === "start") return current + start;
if (mode === "center") return current + start - (viewportSize - size) / 2;
if (mode === "end") return current + end - viewportSize;
// CSSOM View's nearest alignment: do nothing when fully visible or when
// the box spans both viewport edges; otherwise move the closer edge in.
if ((start >= 0 && end <= viewportSize) || (start < 0 && end > viewportSize)) {
return current;
}
if (start < 0) return current + start;
if (end > viewportSize) return current + end - viewportSize;
return current;
};
const left = align(inline, rect.left, rect.right, rect.width, vw, currentX);
const top = align(block, rect.top, rect.bottom, rect.height, vh, currentY);
globalThis.scrollTo({ left, top, behavior: arg && arg.behavior });
}
// scrollTo/scrollBy/scroll accept either (x, y) or a ScrollToOptions object.
// The setters fire a scroll event of their own, so suppress the per-axis ones
// here and emit a single event for the whole movement, the way a real browser
@@ -5498,104 +5660,182 @@ globalThis.NodeFilter = {
// ResizeObserver is defined earlier with real per-target firing; the stub
// that previously lived here was a no-op that clobbered the real class.
//
// IntersectionObserver: without a layout engine we can't compute real
// intersection geometry, so every observed target is treated as fully
// in-viewport (`isIntersecting: true`, `intersectionRatio: 1`). Real
// libraries lean on this in three patterns we must support:
//
// 1. Lazy load: observe(img) -> first intersection -> load src -> unobserve.
// One fire is enough — covered by the initial microtask fire.
// 2. Infinite scroll: observe(sentinel) -> on intersection load more ->
// new sentinel mounts -> fire again. Needs re-fires as DOM grows.
// 3. Reveal-on-scroll animations: observe(card) -> isIntersecting flips
// true once and an animation runs. One fire is enough.
//
// To cover (2) without spinning forever, we burst-fire at an exponential
// backoff schedule and ALSO re-fire whenever the DOM mutates (a strong
// signal that the page just rendered something new). Per-observer total
// fire cap stops us from looping on a never-disconnected observer.
// Viewport IntersectionObserver. Render builds provide real, scroll-relative
// target boxes through getBoundingClientRect. Element scroll-container roots
// remain intentionally unsupported until their offsets participate in layout.
globalThis.__intersectionObservers = [];
function _ioRect(x, y, width, height) {
return {
x, y, width, height,
top: y, left: x, right: x + width, bottom: y + height,
toJSON() { return this; },
};
}
function _ioMargins(value) {
const parts = String(value || "0px").trim().split(/\s+/);
if (parts.length < 1 || parts.length > 4) return null;
const parsed = parts.map((part) => {
const match = /^([-+]?(?:\d+(?:\.\d*)?|\.\d+))(px|%)$/.exec(part);
return match ? { value: Number(match[1]), unit: match[2] } : null;
});
if (parsed.some((part) => !part)) return null;
if (parsed.length === 1) return [parsed[0], parsed[0], parsed[0], parsed[0]];
if (parsed.length === 2) return [parsed[0], parsed[1], parsed[0], parsed[1]];
if (parsed.length === 3) return [parsed[0], parsed[1], parsed[2], parsed[1]];
return parsed;
}
function _ioUnsupportedRoot() {
const error = new Error(
"Obscura IntersectionObserver element roots require element scroll-container layout"
);
error.name = "NotSupportedError";
return error;
}
globalThis.IntersectionObserver = class IntersectionObserver {
constructor(callback, options) {
if (typeof callback !== "function") {
throw new TypeError("IntersectionObserver callback must be a function");
}
this._callback = callback;
this._options = options || {};
if (this._options.root != null) throw _ioUnsupportedRoot();
this._margins = _ioMargins(this._options.rootMargin || "0px");
if (!this._margins) throw new SyntaxError("Invalid IntersectionObserver rootMargin");
const raw = this._options.threshold == null
? [0]
: (Array.isArray(this._options.threshold) ? this._options.threshold : [this._options.threshold]);
this._thresholds = [...new Set(raw.map(Number))].sort((a, b) => a - b);
if (!this._thresholds.length) this._thresholds = [0];
if (this._thresholds.some((value) => !Number.isFinite(value) || value < 0 || value > 1)) {
throw new RangeError("IntersectionObserver threshold must be between 0 and 1");
}
this._targets = new Set();
this._previous = new Map();
this._records = [];
this._connected = true;
this._fireCount = 0;
this._deliveryPending = false;
globalThis.__intersectionObservers.push(this);
}
_fireFor(targets) {
if (!this._connected || !targets.length || this._fireCount >= 256) return;
this._fireCount++;
const records = targets.map(target => ({
target,
isIntersecting: true,
intersectionRatio: 1,
boundingClientRect: target.getBoundingClientRect
? target.getBoundingClientRect()
: { x: 0, y: 0, width: 100, height: 20, top: 0, left: 0, right: 100, bottom: 20 },
intersectionRect: target.getBoundingClientRect
? target.getBoundingClientRect()
: { x: 0, y: 0, width: 100, height: 20, top: 0, left: 0, right: 100, bottom: 20 },
rootBounds: { x: 0, y: 0, width: 1280, height: 720, top: 0, left: 0, right: 1280, bottom: 720 },
time: Date.now(),
}));
try { this._callback(records, this); } catch (e) { /* IO callbacks must not propagate */ }
_rootBounds() {
const width = globalThis.innerWidth || 1280;
const height = globalThis.innerHeight || 720;
const resolve = (margin, basis) =>
margin.unit === "%" ? margin.value * basis / 100 : margin.value;
// IntersectionObserver resolves every rootMargin percentage against the
// root rectangle's width, including the block-axis sides.
const top = resolve(this._margins[0], width);
const right = resolve(this._margins[1], width);
const bottom = resolve(this._margins[2], width);
const left = resolve(this._margins[3], width);
return _ioRect(-left, -top, width + left + right, height + top + bottom);
}
observe(el) {
if (!el || !this._connected) return;
if (this._targets.has(el)) return;
this._targets.add(el);
Promise.resolve().then(() => this._fireFor([el]));
// Exponential burst to cover infinite-scroll sentinels that "re-arm"
// after content lands. Without a real scroll/layout signal, we fake the
// re-fire schedule. Beyond ~10s the page has usually settled.
[120, 500, 1500, 3500, 7000].forEach(delay => {
setTimeout(() => {
if (this._connected && this._targets.has(el)) this._fireFor([el]);
}, delay);
_entry(target) {
const rect = target.getBoundingClientRect();
const root = this._rootBounds();
const left = Math.max(rect.left, root.left);
const top = Math.max(rect.top, root.top);
const right = Math.min(rect.right, root.right);
const bottom = Math.min(rect.bottom, root.bottom);
const edgesTouch = right >= left && bottom >= top;
const width = Math.max(0, right - left);
const height = Math.max(0, bottom - top);
const targetArea = Math.max(0, rect.width) * Math.max(0, rect.height);
const area = width * height;
const isIntersecting = edgesTouch;
return {
target,
isIntersecting,
intersectionRatio: targetArea > 0 ? area / targetArea : (isIntersecting ? 1 : 0),
boundingClientRect: _ioRect(rect.x, rect.y, rect.width, rect.height),
intersectionRect: _ioRect(left, top, width, height),
rootBounds: root,
time: performance.now(),
};
}
_thresholdIndex(ratio) {
let index = 0;
while (index < this._thresholds.length && this._thresholds[index] <= ratio) index++;
return index;
}
_queueChanged(target, forceInitial) {
const entry = this._entry(target);
const previous = this._previous.get(target);
const changed = forceInitial || !previous ||
previous.isIntersecting !== entry.isIntersecting ||
this._thresholdIndex(previous.intersectionRatio) !==
this._thresholdIndex(entry.intersectionRatio);
this._previous.set(target, {
isIntersecting: entry.isIntersecting,
intersectionRatio: entry.intersectionRatio,
});
if (changed) this._records.push(entry);
}
_check(targets, forceInitial) {
if (!this._connected) return;
for (const target of targets) {
if (this._targets.has(target)) this._queueChanged(target, !!forceInitial);
}
if (!this._records.length || this._deliveryPending) return;
this._deliveryPending = true;
Promise.resolve().then(() => {
this._deliveryPending = false;
if (!this._connected || !this._records.length) return;
const records = this.takeRecords();
try { this._callback(records, this); } catch (e) {}
});
}
unobserve(el) { this._targets.delete(el); }
observe(el) {
if (!el || !this._connected || this._targets.has(el)) return;
this._targets.add(el);
this._previous.delete(el);
this._check([el], true);
}
unobserve(el) {
this._targets.delete(el);
this._previous.delete(el);
}
disconnect() {
this._connected = false;
this._targets.clear();
const idx = globalThis.__intersectionObservers.indexOf(this);
if (idx >= 0) globalThis.__intersectionObservers.splice(idx, 1);
this._previous.clear();
this._records.length = 0;
const index = globalThis.__intersectionObservers.indexOf(this);
if (index >= 0) globalThis.__intersectionObservers.splice(index, 1);
}
takeRecords() { return []; }
get root() { return this._options.root || null; }
get rootMargin() { return this._options.rootMargin || "0px 0px 0px 0px"; }
get thresholds() {
const t = this._options.threshold;
if (t == null) return [0];
return Array.isArray(t) ? t.slice() : [t];
takeRecords() { return this._records.splice(0); }
get root() { return null; }
get rootMargin() {
return this._margins.map((margin) => `${margin.value}${margin.unit}`).join(" ");
}
get thresholds() { return this._thresholds.slice(); }
};
// When the DOM mutates (e.g. infinite scroll loads a batch of items), re-fire
// every active IntersectionObserver so libraries observing dynamic content
// see a fresh isIntersecting=true event. Uses the same per-observer fire cap
// to prevent runaway loops if the page is mutating in a tight cycle.
(function() {
const reFire = () => {
for (const obs of globalThis.__intersectionObservers) {
if (!obs._connected) continue;
const ts = [...obs._targets];
if (ts.length) obs._fireFor(ts);
}
};
// Lazy-attach a single MutationObserver on document.body once the page is
// ready, debounced via a microtask so a flurry of mutations only triggers
// one IO sweep.
let pending = false;
const recompute = () => {
if (pending) return;
pending = true;
Promise.resolve().then(() => {
pending = false;
for (const observer of globalThis.__intersectionObservers) {
if (observer._connected && observer._targets.size) {
observer._check([...observer._targets], false);
}
}
});
};
globalThis.__obscura_recompute_intersections = recompute;
globalThis.addEventListener("resize", recompute);
const wireUp = () => {
if (!globalThis.document?.body) return;
const mo = new MutationObserver(() => {
if (pending) return;
pending = true;
Promise.resolve().then(() => { pending = false; reFire(); });
});
try { mo.observe(globalThis.document.body, {childList: true, subtree: true}); } catch {}
const observer = new MutationObserver(recompute);
try {
observer.observe(globalThis.document.body, {
childList: true,
subtree: true,
attributes: true,
characterData: true,
});
} catch {}
};
if (globalThis.document?.body) wireUp();
else Promise.resolve().then(wireUp);
@@ -6718,21 +6958,312 @@ globalThis.screenLeft = 0; globalThis.screenTop = 0;
globalThis.pageXOffset = 0; globalThis.pageYOffset = 0;
globalThis.scrollX = 0; globalThis.scrollY = 0;
// Keep the JavaScript capability surface aligned with the declarations the
// renderer actually implements. Reporting an unknown declaration as supported
// is not harmless: Tailwind and other framework sheets use negative probes to
// select legacy-browser fallbacks, which can replace their modern cascade.
const _CSS_SUPPORTED_DECLARATIONS = new Set((
"display width height min-width min-height max-width max-height box-sizing aspect-ratio " +
"margin margin-top margin-right margin-bottom margin-left margin-inline margin-inline-start " +
"margin-inline-end margin-block margin-block-start margin-block-end padding padding-top " +
"padding-right padding-bottom padding-left padding-inline padding-inline-start padding-inline-end " +
"padding-block padding-block-start padding-block-end border-radius border border-width " +
"border-top-width border-right-width border-bottom-width border-left-width border-top border-right " +
"border-bottom border-left background background-color background-image background-size " +
"background-position background-clip -webkit-background-clip mask-image -webkit-mask-image " +
"mask-size -webkit-mask-size mask-repeat -webkit-mask-repeat color -webkit-text-fill-color fill " +
"stroke stroke-width border-color font-size font font-weight font-family font-style text-align " +
"text-transform text-decoration text-decoration-line line-height align-items justify-items " +
"place-items align-self justify-self place-self align-content justify-content place-content " +
"flex-direction flex-wrap flex-grow flex-shrink flex-basis flex order position float object-fit " +
"top right bottom left inset overflow overflow-x overflow-y scrollbar-gutter visibility opacity animation " +
"animation-name animation-fill-mode animation-iteration-count z-index clear vertical-align " +
"list-style list-style-type gap grid-gap row-gap grid-row-gap column-gap grid-column-gap " +
"border-spacing border-collapse grid-template-columns grid-template-rows grid-template-areas " +
"grid-template grid grid-auto-flow grid-area grid-column grid-row grid-column-start " +
"grid-column-end grid-row-start grid-row-end transform filter backdrop-filter " +
"-webkit-backdrop-filter perspective contain will-change content-visibility box-shadow " +
"-webkit-box-shadow"
).split(/\s+/));
const _CSS_SUPPORTED_COLOR_NAMES = new Set((
"transparent white black gray grey silver lightgray lightgrey darkgray darkgrey whitesmoke " +
"gainsboro red green lime blue navy yellow orange purple maroon teal aqua cyan fuchsia magenta " +
"olive darkblue mediumblue royalblue dodgerblue cornflowerblue steelblue deepskyblue skyblue " +
"lightskyblue lightblue powderblue cadetblue slateblue darkslateblue midnightblue indigo " +
"darkgreen forestgreen seagreen mediumseagreen limegreen yellowgreen olivedrab darkolivegreen " +
"greenyellow lightgreen palegreen springgreen mediumaquamarine aquamarine turquoise " +
"mediumturquoise darkcyan crimson firebrick darkred indianred tomato orangered coral salmon " +
"lightsalmon darksalmon hotpink deeppink pink lightpink palevioletred mediumvioletred violet " +
"orchid plum mediumpurple blueviolet darkviolet darkorchid darkmagenta lavender thistle gold " +
"goldenrod darkgoldenrod khaki darkkhaki peachpuff moccasin papayawhip wheat tan burlywood " +
"sandybrown peru chocolate sienna saddlebrown brown rosybrown darkorange lightyellow " +
"lightgoldenrodyellow lemonchiffon beige ivory azure mintcream honeydew snow seashell linen " +
"oldlace floralwhite ghostwhite aliceblue lavenderblush mistyrose cornsilk antiquewhite bisque " +
"blanchedalmond navajowhite dimgray dimgrey slategray slategrey lightslategray lightslategrey " +
"darkslategray darkslategrey"
).split(/\s+/));
function _cssSupportsColor(value) {
const raw = value.trim();
const lower = raw.toLowerCase();
if (_CSS_SUPPORTED_COLOR_NAMES.has(lower)) return true;
if (/^#[0-9a-f]{3,4}(?:[0-9a-f]{2}){0,2}$/i.test(lower)) {
return [4, 5, 7, 9].includes(lower.length);
}
if (lower.startsWith("var(") && lower.endsWith(")")) {
const comma = _cssTopLevelComma(raw.slice(4, -1));
return comma >= 0 && _cssSupportsColor(raw.slice(4 + comma + 1, -1));
}
if (/^rgba?\(/.test(lower) && lower.endsWith(")")) {
// Chromium accepts relative colors and Tailwind uses this exact syntax as
// a browser-family probe. Painting every relative-color expression is a
// separate concern from recognizing its valid conditional syntax.
if (/\bfrom\b/.test(lower)) {
return /^rgba?\(\s*from\s+\S[\s\S]*\)$/i.test(raw);
}
const parts = lower.slice(lower.indexOf("(") + 1, -1)
.split(/[,\s/]+/).filter(Boolean);
return parts.length >= 3 && parts.slice(0, 3)
.every((part) => /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)%?$/.test(part));
}
if (/^hsla?\(/.test(lower) && lower.endsWith(")")) {
const parts = lower.slice(lower.indexOf("(") + 1, -1)
.split(/[,\s/]+/).filter(Boolean);
return parts.length >= 3 && parts.slice(0, 3).every((part) =>
/^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:deg|%)?$/.test(part));
}
if (/^okl(?:ab|ch)\(/.test(lower) && lower.endsWith(")")) {
const parts = lower.slice(lower.indexOf("(") + 1, -1)
.split(/[,\s/]+/).filter(Boolean);
return parts.length >= 3 && parts.slice(0, 3).every((part) =>
/^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:deg|%)?$/.test(part));
}
if (lower.startsWith("light-dark(") && lower.endsWith(")")) {
const parts = _cssSplitTopLevel(
raw.slice("light-dark(".length, -1),
","
);
return !!parts && parts.length === 2 &&
parts.every((part) => part.trim() && _cssSupportsColor(part));
}
if (lower.startsWith("color-mix(") && lower.endsWith(")")) {
const parts = _cssSplitTopLevel(lower.slice("color-mix(".length, -1), ",");
if (!parts || parts.length < 3 || !/^in\s+\S+$/i.test(parts[0].trim())) return false;
const color = (part) => _cssSupportsColor(part.trim().replace(/\s+[-+]?(?:\d+(?:\.\d*)?|\.\d+)%\s*$/, ""));
return color(parts[1]) && color(parts[2]);
}
return false;
}
function _cssTopLevelComma(text) {
let depth = 0, quote = "";
for (let i = 0; i < text.length; i++) {
const character = text[i];
if (quote) {
if (character === "\\") i++;
else if (character === quote) quote = "";
continue;
}
if (character === "'" || character === '"') quote = character;
else if (character === "(") depth++;
else if (character === ")") depth--;
else if (character === "," && depth === 0) return i;
if (depth < 0) return -1;
}
return -1;
}
function _cssSupportsDeclaration(name, value) {
name = name.trim().toLowerCase();
value = value.trim();
if (!value) return false;
if (name.startsWith("--")) return name.length > 2;
if (!_CSS_SUPPORTED_DECLARATIONS.has(name)) return false;
const lower = value.toLowerCase();
if (["initial", "inherit", "unset", "revert", "revert-layer"].includes(lower)) return true;
if (name === "display") {
return ["none", "flex", "inline-flex", "inline", "inline-block", "grid",
"inline-grid", "block", "flow-root", "contents"].includes(lower);
}
if (name === "position") {
return ["static", "relative", "absolute", "fixed", "sticky"].includes(lower);
}
if (name === "box-sizing") return ["content-box", "border-box"].includes(lower);
if (name === "float") return ["none", "left", "right"].includes(lower);
if (name === "object-fit") {
return ["fill", "contain", "cover", "none", "scale-down"].includes(lower);
}
if (name === "visibility") return ["visible", "hidden", "collapse"].includes(lower);
if (name === "scrollbar-gutter") {
return lower === "auto" || lower === "stable" || lower === "stable both-edges";
}
if (["color", "-webkit-text-fill-color", "background-color", "border-color"].includes(name)) {
return _cssSupportsColor(value);
}
return true;
}
// Return the contents only when one pair of parentheses encloses the complete
// expression. Declaration leaves such as `(display:grid)` are then evaluated
// by the same path as the two-argument overload.
function _cssEnclosingGroup(text) {
if (!text.startsWith("(")) return null;
let depth = 0, quote = "";
for (let i = 0; i < text.length; i++) {
const character = text[i];
if (quote) {
if (character === "\\") i++;
else if (character === quote) quote = "";
continue;
}
if (character === "'" || character === '"') quote = character;
else if (character === "(") depth++;
else if (character === ")") {
depth--;
if (depth < 0) return null;
if (depth === 0) return i === text.length - 1 ? text.slice(1, i) : null;
}
}
return null;
}
function _cssSplitTopLevel(text, operator) {
const parts = [];
const isWord = /^[a-z]+$/i.test(operator);
let start = 0, depth = 0, quote = "";
for (let i = 0; i < text.length; i++) {
const character = text[i];
if (quote) {
if (character === "\\") i++;
else if (character === quote) quote = "";
continue;
}
if (character === "'" || character === '"') {
quote = character;
continue;
}
if (character === "(" || character === "[") depth++;
else if (character === ")" || character === "]") {
depth--;
if (depth < 0) return null;
} else if (depth === 0 &&
text.slice(i, i + operator.length).toLowerCase() === operator.toLowerCase() &&
(!isWord || (i > 0 && /\s/.test(text[i - 1]) &&
i + operator.length < text.length && /\s/.test(text[i + operator.length])))) {
const part = text.slice(start, i).trim();
if (!part) return null;
parts.push(part);
i += operator.length - 1;
start = i + 1;
}
}
if (depth !== 0 || quote || !parts.length) return null;
const tail = text.slice(start).trim();
if (!tail) return null;
parts.push(tail);
return parts;
}
function _cssHasTopLevelComma(text) {
let depth = 0, quote = "";
for (let i = 0; i < text.length; i++) {
const character = text[i];
if (quote) {
if (character === "\\") i++;
else if (character === quote) quote = "";
continue;
}
if (character === "\\") { i++; continue; }
if (character === "'" || character === '"') quote = character;
else if (character === "(" || character === "[") depth++;
else if (character === ")" || character === "]") depth--;
else if (character === "," && depth === 0) return true;
if (depth < 0) return false;
}
return false;
}
const _CSS_SUPPORTED_SIMPLE_PSEUDOS = new Set((
"hover active focus focus-visible focus-within enabled disabled checked link any-link visited " +
"first-child last-child only-child root empty scope first-of-type last-of-type only-of-type " +
"before after"
).split(/\s+/));
const _CSS_SUPPORTED_FUNCTIONAL_PSEUDOS = new Set((
"nth-child nth-of-type nth-last-child nth-last-of-type is where has host not"
).split(/\s+/));
function _cssSupportsSelector(selector) {
selector = selector.trim();
if (!selector || /[{};]/.test(selector)) return false;
const split = _cssSplitTopLevel(selector, ",");
if (!split && _cssHasTopLevelComma(selector)) return false;
const selectors = split || [selector];
return selectors.every((part) => {
part = part.trim();
if (!part || /^[>+~]/.test(part) || /[>+~]\s*$/.test(part)) return false;
let parens = 0, brackets = 0, quote = "";
for (let i = 0; i < part.length; i++) {
const character = part[i];
if (quote) {
if (character === "\\") i++;
else if (character === quote) quote = "";
continue;
}
if (character === "\\") { i++; continue; }
if (character === "'" || character === '"') quote = character;
else if (character === "(") parens++;
else if (character === ")") parens--;
else if (character === "[") brackets++;
else if (character === "]") brackets--;
else if (character === ":" && brackets === 0) {
const doubleColon = part[i + 1] === ":";
let end = i + (doubleColon ? 2 : 1);
const start = end;
while (end < part.length && /[a-z0-9_-]/i.test(part[end])) end++;
if (end === start) return false;
const name = part.slice(start, end).toLowerCase();
const functional = part[end] === "(";
if (doubleColon) {
if (functional || !["before", "after"].includes(name)) return false;
} else if (functional) {
if (!_CSS_SUPPORTED_FUNCTIONAL_PSEUDOS.has(name)) return false;
} else if (!_CSS_SUPPORTED_SIMPLE_PSEUDOS.has(name)) {
return false;
}
i = end - 1;
}
if (parens < 0 || brackets < 0) return false;
}
return !quote && parens === 0 && brackets === 0;
});
}
function _cssSupportsCondition(condition) {
condition = condition.trim();
if (!condition) return false;
const grouped = _cssEnclosingGroup(condition);
if (grouped !== null) return _cssSupportsCondition(grouped);
if (/^not\s/i.test(condition)) return !_cssSupportsCondition(condition.slice(3).trim());
const orParts = _cssSplitTopLevel(condition, "or");
if (orParts) return orParts.some(_cssSupportsCondition);
const andParts = _cssSplitTopLevel(condition, "and");
if (andParts) return andParts.every(_cssSupportsCondition);
if (/^selector\(/i.test(condition) && condition.endsWith(")")) {
return _cssSupportsSelector(condition.slice(condition.indexOf("(") + 1, -1));
}
const colon = condition.indexOf(":");
if (colon < 0) return false;
return _cssSupportsDeclaration(condition.slice(0, colon), condition.slice(colon + 1));
}
globalThis.CSS = {
supports(prop, value){
try {
var p, v;
if (arguments.length >= 2) { p = String(prop).trim(); v = String(value).trim(); }
else {
var cond = String(prop).trim().replace(/^\(+|\)+$/g, "").trim();
var idx = cond.indexOf(":");
if (idx === -1) return false;
p = cond.slice(0, idx).trim(); v = cond.slice(idx + 1).trim();
if (arguments.length >= 2) {
return _cssSupportsDeclaration(String(prop), String(value));
}
if (!p || !v) return false;
// The engine renders standard CSS; report it as supported so feature-gated
// SPAs don't bail to /unsupported. (Previous stub always returned false.)
return true;
return _cssSupportsCondition(String(prop));
} catch (e) { return false; }
},
escape(s){ return s; }
@@ -8290,8 +8821,9 @@ URL.revokeObjectURL = function(url) {
//
// The page offset is stored on the scrolling element rather than in separate
// window state, so window.scrollY and document.scrollingElement.scrollTop are
// two views of one value, which is what pages assume. As with #431 there is no
// layout, so the offset still cannot be clamped to a real maximum.
// two views of one value, which is what pages assume. Render builds clamp that
// shared root offset against measured document overflow; non-render builds keep
// the legacy synthetic offset used by automation-only consumers.
function _scrollRoot() {
const doc = globalThis.document;
return (doc && doc.scrollingElement) || null;
@@ -8299,6 +8831,8 @@ function _scrollRoot() {
function _windowScroll(x, y, relative) {
const root = _scrollRoot();
if (!root) return;
const beforeLeft = root.scrollLeft || 0;
const beforeTop = root.scrollTop || 0;
let left, top;
if (x !== null && typeof x === 'object') { left = x.left; top = x.top; }
else { left = x; top = y; }
@@ -8308,6 +8842,9 @@ function _windowScroll(x, y, relative) {
if (top !== undefined) {
root.scrollTop = (relative ? (root.scrollTop || 0) : 0) + (+top || 0);
}
if ((root.scrollLeft || 0) === beforeLeft && (root.scrollTop || 0) === beforeTop) {
return;
}
// Async, matching the element path #431 added. Dispatched at the document
// AND the window: a page scroll event reaches both in Chrome, but
// Document.dispatchEvent here runs only its own listeners and does not