Isolate Eve browser lowering per host
This commit is contained in:
parent
076d2124ed
commit
7f51dfd6ac
6 changed files with 1078 additions and 84 deletions
168
packages/eve-browser-lowering/dist/index.js
vendored
168
packages/eve-browser-lowering/dist/index.js
vendored
|
|
@ -122,18 +122,27 @@ const defaultStyleTokens = {
|
|||
"--font-title": "\"Montserrat\", \"Zen Kaku Gothic New\", \"M PLUS 1\", \"Ubuntu Sans\", system-ui, sans-serif",
|
||||
"--font-mono": "\"Ubuntu Sans Mono\", \"M PLUS 1 Code\", \"Cascadia Mono\", Consolas, monospace",
|
||||
};
|
||||
let activeFontStylesheet;
|
||||
let currentSurfaceStyles = normalizeSurfaceStyles(undefined);
|
||||
let currentSurfaceDocument;
|
||||
let currentOptions = {};
|
||||
const loadedFontStylesheets = new Map();
|
||||
const activeSurfaceBindings = new WeakMap();
|
||||
const eveBrowserRenderContext = Symbol("eve.browser.render-context");
|
||||
function scopeEveBrowserOptions(surface, options) {
|
||||
return {
|
||||
...options,
|
||||
[eveBrowserRenderContext]: {
|
||||
surface,
|
||||
styles: normalizeSurfaceStyles(surface.surface?.styles),
|
||||
elements: new WeakMap(),
|
||||
},
|
||||
};
|
||||
}
|
||||
function renderContext(options) {
|
||||
return options[eveBrowserRenderContext];
|
||||
}
|
||||
export function renderEveSurface(surface, host, options = {}) {
|
||||
activeSurfaceBindings.get(host)?.dispose();
|
||||
currentSurfaceDocument = surface;
|
||||
currentOptions = options;
|
||||
currentSurfaceStyles = normalizeSurfaceStyles(surface.surface?.styles);
|
||||
globalThis.__eveCurrentMesh = surface.mesh;
|
||||
applyEveSurfaceStyles(surface.surface?.styles, options.body ?? document.body);
|
||||
const scopedOptions = scopeEveBrowserOptions(surface, options);
|
||||
applyEveSurfaceStyles(surface.surface?.styles, options.body ?? host);
|
||||
host.dataset.provider = surface.providerId || options.provider?.providerId || "";
|
||||
if (options.body) {
|
||||
options.body.dataset.provider = surface.providerId || options.provider?.providerId || "";
|
||||
}
|
||||
|
|
@ -141,9 +150,9 @@ export function renderEveSurface(surface, host, options = {}) {
|
|||
options.statusElement.textContent = `${surface.title || surface.surface?.title || "surface"}${options.source ? ` (${options.source})` : ""}`;
|
||||
}
|
||||
if (surface.surface?.root) {
|
||||
host.replaceChildren(renderEveComponent(surface.surface.root, options));
|
||||
host.replaceChildren(renderEveComponent(surface.surface.root, scopedOptions));
|
||||
if (options.stateBindingResolver) {
|
||||
const controller = new EveSurfaceBindingController(surface, host, options);
|
||||
const controller = new EveSurfaceBindingController(surface, host, scopedOptions);
|
||||
activeSurfaceBindings.set(host, controller);
|
||||
void controller.start();
|
||||
}
|
||||
|
|
@ -173,6 +182,8 @@ class EveSurfaceBindingController {
|
|||
options;
|
||||
disposed = false;
|
||||
unsubscribers = [];
|
||||
pendingComponents = new Set();
|
||||
projectionQueued = false;
|
||||
constructor(surface, host, options) {
|
||||
this.surface = surface;
|
||||
this.host = host;
|
||||
|
|
@ -183,6 +194,7 @@ class EveSurfaceBindingController {
|
|||
const root = this.surface.surface?.root;
|
||||
if (!resolver || !root)
|
||||
return;
|
||||
const hydratedComponents = new Set();
|
||||
for (const component of walkEveComponents(root)) {
|
||||
for (const binding of component.stateBindings || []) {
|
||||
if (this.disposed)
|
||||
|
|
@ -194,32 +206,102 @@ class EveSurfaceBindingController {
|
|||
if (this.disposed)
|
||||
return;
|
||||
applyEveStateBindingValue(component, binding, value);
|
||||
this.renderCanonicalProjection();
|
||||
this.queueComponentProjection(component);
|
||||
};
|
||||
apply(await handle.latest());
|
||||
applyEveStateBindingValue(component, binding, await handle.latest());
|
||||
hydratedComponents.add(component);
|
||||
if (this.disposed)
|
||||
return;
|
||||
this.unsubscribers.push(handle.watch(apply));
|
||||
}
|
||||
}
|
||||
for (const component of hydratedComponents) {
|
||||
if (this.disposed)
|
||||
return;
|
||||
this.renderComponentProjection(component);
|
||||
}
|
||||
}
|
||||
dispose() {
|
||||
this.disposed = true;
|
||||
this.pendingComponents.clear();
|
||||
for (const unsubscribe of this.unsubscribers.splice(0))
|
||||
unsubscribe();
|
||||
}
|
||||
renderCanonicalProjection() {
|
||||
const root = this.surface.surface?.root;
|
||||
if (root)
|
||||
this.host.replaceChildren(renderEveComponent(root, this.options));
|
||||
queueComponentProjection(component) {
|
||||
this.pendingComponents.add(component);
|
||||
if (this.projectionQueued)
|
||||
return;
|
||||
this.projectionQueued = true;
|
||||
queueMicrotask(() => {
|
||||
this.projectionQueued = false;
|
||||
if (this.disposed)
|
||||
return;
|
||||
for (const pending of this.pendingComponents)
|
||||
this.renderComponentProjection(pending);
|
||||
this.pendingComponents.clear();
|
||||
});
|
||||
}
|
||||
renderComponentProjection(component) {
|
||||
const current = renderContext(this.options)?.elements.get(component);
|
||||
if (!current?.isConnected)
|
||||
return;
|
||||
replaceElementPreservingInteraction(current, renderEveComponent(component, this.options), this.host);
|
||||
}
|
||||
}
|
||||
function replaceElementPreservingInteraction(current, replacement, host) {
|
||||
const active = current.ownerDocument.activeElement;
|
||||
const activeElement = active instanceof HTMLElement && current.contains(active) ? active : undefined;
|
||||
const activePath = activeElement ? elementPath(current, activeElement) : undefined;
|
||||
const selection = activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement
|
||||
? { start: activeElement.selectionStart, end: activeElement.selectionEnd, direction: activeElement.selectionDirection }
|
||||
: undefined;
|
||||
const hostScroll = { left: host.scrollLeft, top: host.scrollTop };
|
||||
current.replaceWith(replacement);
|
||||
host.scrollLeft = hostScroll.left;
|
||||
host.scrollTop = hostScroll.top;
|
||||
if (!activePath)
|
||||
return;
|
||||
const nextActive = elementAtPath(replacement, activePath);
|
||||
nextActive?.focus({ preventScroll: true });
|
||||
if (selection && (nextActive instanceof HTMLInputElement || nextActive instanceof HTMLTextAreaElement)) {
|
||||
nextActive.setSelectionRange(selection.start, selection.end, selection.direction ?? undefined);
|
||||
}
|
||||
}
|
||||
function elementPath(root, target) {
|
||||
if (root === target)
|
||||
return [];
|
||||
const path = [];
|
||||
let current = target;
|
||||
while (current && current !== root) {
|
||||
const parent = current.parentElement;
|
||||
if (!parent)
|
||||
return undefined;
|
||||
path.unshift(Array.prototype.indexOf.call(parent.children, current));
|
||||
current = parent;
|
||||
}
|
||||
return current === root ? path : undefined;
|
||||
}
|
||||
function elementAtPath(root, path) {
|
||||
let current = root;
|
||||
for (const index of path) {
|
||||
const child = current.children.item(index);
|
||||
if (!(child instanceof HTMLElement))
|
||||
return undefined;
|
||||
current = child;
|
||||
}
|
||||
return current instanceof HTMLElement ? current : undefined;
|
||||
}
|
||||
function* walkEveComponents(root) {
|
||||
yield root;
|
||||
for (const child of root.children || [])
|
||||
yield* walkEveComponents(child);
|
||||
}
|
||||
export function renderEveComponent(node, options = currentOptions) {
|
||||
export function renderEveComponent(node, options = {}) {
|
||||
const element = renderEveComponentProjection(node, options);
|
||||
renderContext(options)?.elements.set(node, element);
|
||||
return element;
|
||||
}
|
||||
function renderEveComponentProjection(node, options) {
|
||||
const kind = node.kind || "panel";
|
||||
const props = objectProps(node.props);
|
||||
const layout = objectProps(node.layout);
|
||||
|
|
@ -300,8 +382,8 @@ export function renderEveComponent(node, options = currentOptions) {
|
|||
if (adapter?.renderComponent) {
|
||||
return adapter.renderComponent(node, props, layout, style, options, {
|
||||
applyGeneratedLayout,
|
||||
providerId: currentSurfaceDocument?.providerId || options.provider?.providerId,
|
||||
resolveAssetUrl,
|
||||
providerId: renderContext(options)?.surface.providerId || options.provider?.providerId,
|
||||
resolveAssetUrl: uri => resolveAssetUrl(uri, options),
|
||||
});
|
||||
}
|
||||
if (kind === "world.scene3d" || kind === "world.scene2d") {
|
||||
|
|
@ -309,7 +391,7 @@ export function renderEveComponent(node, options = currentOptions) {
|
|||
}
|
||||
if (kind === "image.background") {
|
||||
const view = el("div", "cultui-background");
|
||||
const src = resolveAssetUrl(firstString(props.src, props.assetUri, props.assetRef, ""));
|
||||
const src = resolveAssetUrl(firstString(props.src, props.assetUri, props.assetRef, ""), options);
|
||||
if (src)
|
||||
view.style.backgroundImage = `url("${src}")`;
|
||||
view.setAttribute("aria-label", stringProp(props.label, "background"));
|
||||
|
|
@ -318,7 +400,7 @@ export function renderEveComponent(node, options = currentOptions) {
|
|||
if (kind === "image.sprite") {
|
||||
const figure = el("figure", `cultui-sprite ${stringProp(props.slot, "center")}`);
|
||||
assignId(figure, node);
|
||||
const src = resolveAssetUrl(firstString(props.src, props.assetUri, props.assetRef, ""));
|
||||
const src = resolveAssetUrl(firstString(props.src, props.assetUri, props.assetRef, ""), options);
|
||||
if (src) {
|
||||
const image = el("img");
|
||||
image.src = src;
|
||||
|
|
@ -330,7 +412,7 @@ export function renderEveComponent(node, options = currentOptions) {
|
|||
if (kind === "avatar") {
|
||||
const card = el("figure", "cultui-avatar-card");
|
||||
assignId(card, node);
|
||||
const src = resolveAssetUrl(firstString(props.src, props.assetUri, props.assetRef, ""));
|
||||
const src = resolveAssetUrl(firstString(props.src, props.assetUri, props.assetRef, ""), options);
|
||||
if (src) {
|
||||
const image = el("img");
|
||||
image.src = src;
|
||||
|
|
@ -529,7 +611,7 @@ export function renderEveComponent(node, options = currentOptions) {
|
|||
const figure = el("figure", "cultui-image-preview");
|
||||
assignId(figure, node);
|
||||
const frame = el("div", "cultui-image-frame");
|
||||
const src = resolveAssetUrl(firstString(props.src, props.assetUri, props.assetRef, ""));
|
||||
const src = resolveAssetUrl(firstString(props.src, props.assetUri, props.assetRef, ""), options);
|
||||
if (src) {
|
||||
const image = el("img");
|
||||
image.src = src;
|
||||
|
|
@ -848,7 +930,7 @@ function renderWorldScene(node, props, layout, style, options) {
|
|||
function emitWorldCommand(command, action, node, options) {
|
||||
void options.commandSink?.(createWorldActionIntent(command, action, options), node);
|
||||
}
|
||||
export function createWorldActionIntent(command, action, options = currentOptions) {
|
||||
export function createWorldActionIntent(command, action, options = {}) {
|
||||
return createEveCommandIntent(command, { action }, options);
|
||||
}
|
||||
function worldDirectionForKey(key) {
|
||||
|
|
@ -1060,7 +1142,7 @@ function renderInventoryItem(node, props, layout, style, options) {
|
|||
function hasInventoryDropCommand(target) {
|
||||
return Object.entries(target).some(([key, value]) => (key === "dropCommand" || key.startsWith("dropCommand.")) && typeof value === "string" && value.length > 0);
|
||||
}
|
||||
export function createInventoryDropIntent(source, target, destinationX, destinationY, options = currentOptions) {
|
||||
export function createInventoryDropIntent(source, target, destinationX, destinationY, options = {}) {
|
||||
const sourceKind = firstString(source.sourceKind, source.source, "");
|
||||
const command = firstString(target[`dropCommand.${sourceKind}`], target.dropCommand, "");
|
||||
if (!sourceKind || !command)
|
||||
|
|
@ -1395,10 +1477,11 @@ export function applyEveSurfaceStyles(styles, body = document.body) {
|
|||
body.dataset.pixelArt = normalized.tokens.pixelArt || normalized.tokens.imageRendering === "pixelated" ? "true" : "false";
|
||||
body.dataset.scanlines = normalized.tokens.scanlineOverlay ? "true" : "false";
|
||||
}
|
||||
export function createEveCommandIntent(commandId, props = {}, options = currentOptions) {
|
||||
export function createEveCommandIntent(commandId, props = {}, options = {}) {
|
||||
const action = objectProps(props.action);
|
||||
const providerId = options.provider?.providerId || currentSurfaceDocument?.providerId || "surface unknown";
|
||||
const surfaceId = options.activeSurfaceId || currentSurfaceDocument?.surface?.id || options.provider?.surfaces?.[0]?.surfaceId || providerId;
|
||||
const surface = renderContext(options)?.surface;
|
||||
const providerId = options.provider?.providerId || surface?.providerId || "surface unknown";
|
||||
const surfaceId = options.activeSurfaceId || surface?.surface?.id || options.provider?.surfaces?.[0]?.surfaceId || providerId;
|
||||
const worldInteraction = resolveAdvertisedWorldInteraction(options, surfaceId);
|
||||
const commandBoundary = firstString(worldInteraction.commandBoundary, props.commandBoundary, action.commandBoundary, action.target);
|
||||
const receiptSchema = firstString(worldInteraction.receiptSchema, props.receiptSchema, action.receiptSchema);
|
||||
|
|
@ -1456,7 +1539,7 @@ function wireCommand(element, node, commandId, props, options) {
|
|||
});
|
||||
}
|
||||
function renderSlider(props, children, options) {
|
||||
const anatomy = resolveSliderAnatomy(props, children);
|
||||
const anatomy = resolveSliderAnatomy(props, children, options);
|
||||
const box = objectProps(anatomy.find(part => part.kind === "control.box")?.props);
|
||||
const parts = anatomy.filter(part => part.kind === "control.part");
|
||||
const min = Number(props.min ?? 0);
|
||||
|
|
@ -1507,9 +1590,11 @@ function renderSlider(props, children, options) {
|
|||
});
|
||||
return slider;
|
||||
}
|
||||
function resolveSliderAnatomy(props, children) {
|
||||
function resolveSliderAnatomy(props, children, options) {
|
||||
const skinName = typeof props.skin === "string" ? props.skin : "";
|
||||
const skinChildren = skinName ? currentSurfaceStyles.controlSkins[skinName]?.children || [] : [];
|
||||
const skinChildren = skinName
|
||||
? renderContext(options)?.styles.controlSkins[skinName]?.children || []
|
||||
: [];
|
||||
const anatomy = [...skinChildren, ...children];
|
||||
if (anatomy.length)
|
||||
return anatomy;
|
||||
|
|
@ -1540,27 +1625,26 @@ function normalizeSurfaceStyles(styles) {
|
|||
controlSkins: objectProps(styles?.controlSkins),
|
||||
};
|
||||
}
|
||||
function resolveAssetUrl(uri) {
|
||||
function resolveAssetUrl(uri, options) {
|
||||
if (!uri)
|
||||
return "";
|
||||
const resolver = currentOptions.assetUrlResolver;
|
||||
const surface = renderContext(options)?.surface;
|
||||
const resolver = options.assetUrlResolver;
|
||||
if (resolver)
|
||||
return resolver(uri, currentSurfaceDocument);
|
||||
return resolver(uri, surface);
|
||||
if (/^(https?:|data:|blob:)/i.test(uri))
|
||||
return uri;
|
||||
if (currentOptions.assetBaseUrl && uri.startsWith("/")) {
|
||||
return `${currentOptions.assetBaseUrl.replace(/\/+$/, "")}${uri}`;
|
||||
if (options.assetBaseUrl && uri.startsWith("/")) {
|
||||
return `${options.assetBaseUrl.replace(/\/+$/, "")}${uri}`;
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
function loadFontStylesheet(href) {
|
||||
const normalized = typeof href === "string" ? href.trim() : "";
|
||||
if (activeFontStylesheet?.dataset.href === normalized)
|
||||
return;
|
||||
activeFontStylesheet?.remove();
|
||||
activeFontStylesheet = undefined;
|
||||
if (!normalized)
|
||||
return;
|
||||
if (loadedFontStylesheets.has(normalized))
|
||||
return;
|
||||
const preconnect = document.createElement("link");
|
||||
preconnect.rel = "preconnect";
|
||||
preconnect.href = "https://fonts.gstatic.com";
|
||||
|
|
@ -1571,7 +1655,7 @@ function loadFontStylesheet(href) {
|
|||
link.href = normalized;
|
||||
link.dataset.href = normalized;
|
||||
document.head.append(link);
|
||||
activeFontStylesheet = link;
|
||||
loadedFontStylesheets.set(normalized, link);
|
||||
}
|
||||
function textClassName(kind, props, node) {
|
||||
const classes = [];
|
||||
|
|
|
|||
58
packages/eve-browser-lowering/docs/host-authority.md
Normal file
58
packages/eve-browser-lowering/docs/host-authority.md
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# Browser Lowering Host Authority
|
||||
|
||||
Owner:
|
||||
|
||||
- one rendered host owns its surface document, lowering options, style tokens,
|
||||
component-to-element index, and state-binding subscriptions;
|
||||
- the provider owns the surface graph and command meaning;
|
||||
- the browser lowerer owns only DOM projection and interaction forwarding.
|
||||
|
||||
Inputs:
|
||||
|
||||
- one `EveSurfaceDocument`;
|
||||
- one host element;
|
||||
- host-scoped lowering options, plugin adapters, asset resolver, and binding
|
||||
resolver;
|
||||
- canonical state-binding values.
|
||||
|
||||
Outputs:
|
||||
|
||||
- DOM beneath that host;
|
||||
- typed command intents carrying that host's provider and surface identity;
|
||||
- host-local style variables and provider diagnostics.
|
||||
|
||||
Derived state:
|
||||
|
||||
- DOM nodes, focus restoration data, normalized styles, and component indexes
|
||||
are projections of the owning surface;
|
||||
- loaded font stylesheets are a document resource cache, not active-surface
|
||||
state.
|
||||
|
||||
Forbidden writers:
|
||||
|
||||
- another host may not replace the active surface, options, styles, assets, or
|
||||
command identity used by this host;
|
||||
- binding updates may not rebuild an unrelated host or mutate provider state;
|
||||
- module globals may not decide which surface a command or asset belongs to.
|
||||
|
||||
Shared paths:
|
||||
|
||||
- initial render, embedded render, plugin render, binding updates, command
|
||||
creation, and asset resolution all receive the same host-scoped context;
|
||||
- initial binding hydration and live binding watches use the same component
|
||||
projection replacement primitive.
|
||||
|
||||
Cut line:
|
||||
|
||||
- delete module-global current surface/options/styles;
|
||||
- remove the global current-mesh side channel;
|
||||
- replace whole-host binding rerenders with indexed component-subtree updates;
|
||||
- preserve focus, selection, and host scroll across a bound component update.
|
||||
|
||||
Verification:
|
||||
|
||||
- two hosts in one document retain distinct provider commands, asset routes,
|
||||
and control skins after either host renders or updates;
|
||||
- a bound leaf update does not replace either host root;
|
||||
- focused input selection and host scroll survive a binding update;
|
||||
- disposing or rerendering one host cannot unsubscribe the other.
|
||||
585
packages/eve-browser-lowering/package-lock.json
generated
585
packages/eve-browser-lowering/package-lock.json
generated
|
|
@ -8,9 +8,512 @@
|
|||
"name": "@gamecult/eve-browser-lowering",
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"@types/jsdom": "^30.0.0",
|
||||
"jsdom": "^30.0.1",
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color": {
|
||||
"version": "6.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.7.tgz",
|
||||
"integrity": "sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/css-calc": "^3.3.0",
|
||||
"@csstools/css-color-parser": "^4.1.10",
|
||||
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||
"@csstools/css-tokenizer": "^4.0.0",
|
||||
"lru-cache": "^11.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.13.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/dom-selector": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.2.tgz",
|
||||
"integrity": "sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bidi-js": "^1.0.3",
|
||||
"css-tree": "^3.2.1",
|
||||
"is-potential-custom-element-name": "^1.0.1",
|
||||
"lru-cache": "^11.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.13.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@bramus/specificity": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
|
||||
"integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-tree": "^3.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"specificity": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/color-helpers": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz",
|
||||
"integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-calc": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz",
|
||||
"integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||
"@csstools/css-tokenizer": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-color-parser": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.0.tgz",
|
||||
"integrity": "sha512-5+5LEmFuY1AjXdYhmgjTJogtQnP1evJ1zrBZGUNZ0thkpwnnmKxcHdAMn/OtFjAb25zA+jKDVYVRl+5G7rjv1A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/color-helpers": "^6.1.1",
|
||||
"@csstools/css-calc": "^3.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||
"@csstools/css-tokenizer": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-parser-algorithms": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
|
||||
"integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-tokenizer": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-syntax-patches-for-csstree": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.8.tgz",
|
||||
"integrity": "sha512-CpMLjAvwQg3BL5S0IeqsZNMH7EQrEWi0kLKOC13ZBF0ZwERiLWlibNPJr8G1kdU3Ms/r2KiNrF81pUh2HwAHdg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT-0",
|
||||
"peerDependencies": {
|
||||
"css-tree": "^3.2.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"css-tree": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-tokenizer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
|
||||
"integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@exodus/bytes": {
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz",
|
||||
"integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@noble/hashes": "^1.8.0 || ^2.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@noble/hashes": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jsdom": {
|
||||
"version": "30.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-30.0.0.tgz",
|
||||
"integrity": "sha512-uAHGxujGE0cDaKGdK28zgDotFtNA7MKq5DXl8LrfdxdCI8VHcg15oJz+amHTChPNI5JpgEPQWc2xFdrw3em/nQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"@types/tough-cookie": "*",
|
||||
"parse5": "^8.0.0",
|
||||
"undici-types": "^8.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "26.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz",
|
||||
"integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~8.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node/node_modules/undici-types": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
|
||||
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/tough-cookie": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
|
||||
"integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bidi-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
|
||||
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"require-from-string": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/css-tree": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
|
||||
"integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdn-data": "2.27.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
|
||||
"integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-mimetype": "^5.0.0",
|
||||
"whatwg-url": "^16.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/data-urls/node_modules/whatwg-url": {
|
||||
"version": "16.0.1",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
|
||||
"integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@exodus/bytes": "^1.11.0",
|
||||
"tr46": "^6.0.0",
|
||||
"webidl-conversions": "^8.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
|
||||
"integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/html-encoding-sniffer": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
|
||||
"integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@exodus/bytes": "^1.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-potential-custom-element-name": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
|
||||
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsdom": {
|
||||
"version": "30.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.0.1.tgz",
|
||||
"integrity": "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/css-color": "^6.0.5",
|
||||
"@asamuzakjp/dom-selector": "^8.3.0",
|
||||
"@bramus/specificity": "^2.4.2",
|
||||
"@csstools/css-syntax-patches-for-csstree": "^1.1.7",
|
||||
"@exodus/bytes": "^1.15.1",
|
||||
"css-tree": "^3.2.1",
|
||||
"data-urls": "^7.0.0",
|
||||
"decimal.js": "^10.6.0",
|
||||
"html-encoding-sniffer": "^6.0.0",
|
||||
"is-potential-custom-element-name": "^1.0.1",
|
||||
"lru-cache": "^11.5.2",
|
||||
"parse5": "^8.0.1",
|
||||
"saxes": "^6.0.0",
|
||||
"symbol-tree": "^3.2.4",
|
||||
"tough-cookie": "^6.0.2",
|
||||
"undici": "^8.9.0",
|
||||
"w3c-xmlserializer": "^5.0.0",
|
||||
"webidl-conversions": "^8.0.1",
|
||||
"whatwg-mimetype": "^5.0.0",
|
||||
"whatwg-url": "^17.1.0",
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.22.2 || ^24.15.0 || >=26.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"canvas": "^3.2.3"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"canvas": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "11.5.2",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
|
||||
"integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.27.1",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
|
||||
"integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
|
||||
"dev": true,
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
|
||||
"integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"entities": "^8.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/saxes": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
|
||||
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"xmlchars": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=v12.22.7"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/symbol-tree": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tldts": {
|
||||
"version": "7.4.10",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.10.tgz",
|
||||
"integrity": "sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tldts-core": "^7.4.10"
|
||||
},
|
||||
"bin": {
|
||||
"tldts": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts-core": {
|
||||
"version": "7.4.10",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.10.tgz",
|
||||
"integrity": "sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz",
|
||||
"integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tldts": "^7.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
|
||||
"integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
|
|
@ -24,6 +527,88 @@
|
|||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "8.10.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz",
|
||||
"integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=22.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "8.10.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.10.0.tgz",
|
||||
"integrity": "sha512-ibvdovq3nCFs8Msrd95BW+zUOq+aOVbT+wpHUoPWhztbHEoPc6oof51iFDB6Es8lTKvNvVW9jNSAB8dwrKTMGg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/w3c-xmlserializer": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
|
||||
"integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-mimetype": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
|
||||
"integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "17.1.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.0.tgz",
|
||||
"integrity": "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@exodus/bytes": "^1.15.1",
|
||||
"tr46": "^6.0.0",
|
||||
"webidl-conversions": "^8.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.14.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xml-name-validator": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlchars": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,12 @@
|
|||
"styles.css"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "C:\\Progra~1\\nodejs\\node.exe ./node_modules/typescript/bin/tsc -p tsconfig.json",
|
||||
"test": "C:\\Progra~1\\nodejs\\node.exe --test test/*.test.mjs"
|
||||
"build": "node ./node_modules/typescript/bin/tsc -p tsconfig.json",
|
||||
"test": "node ./node_modules/typescript/bin/tsc -p tsconfig.json && node --test test/*.test.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jsdom": "^30.0.0",
|
||||
"jsdom": "^30.0.1",
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -297,10 +297,7 @@ const defaultStyleTokens: Record<string, string> = {
|
|||
"--font-mono": "\"Ubuntu Sans Mono\", \"M PLUS 1 Code\", \"Cascadia Mono\", Consolas, monospace",
|
||||
};
|
||||
|
||||
let activeFontStylesheet: HTMLLinkElement | undefined;
|
||||
let currentSurfaceStyles: NormalizedStyles = normalizeSurfaceStyles(undefined);
|
||||
let currentSurfaceDocument: EveSurfaceDocument | undefined;
|
||||
let currentOptions: EveBrowserLoweringOptions = {};
|
||||
const loadedFontStylesheets = new Map<string, HTMLLinkElement>();
|
||||
const activeSurfaceBindings = new WeakMap<HTMLElement, EveSurfaceBindingController>();
|
||||
|
||||
interface NormalizedStyles {
|
||||
|
|
@ -309,17 +306,44 @@ interface NormalizedStyles {
|
|||
controlSkins: Record<string, { children?: EveSurfaceComponent[] }>;
|
||||
}
|
||||
|
||||
interface EveBrowserRenderContext {
|
||||
readonly surface: EveSurfaceDocument;
|
||||
readonly styles: NormalizedStyles;
|
||||
readonly elements: WeakMap<EveSurfaceComponent, HTMLElement>;
|
||||
}
|
||||
|
||||
const eveBrowserRenderContext = Symbol("eve.browser.render-context");
|
||||
type EveBrowserScopedOptions = EveBrowserLoweringOptions & {
|
||||
[eveBrowserRenderContext]?: EveBrowserRenderContext;
|
||||
};
|
||||
|
||||
function scopeEveBrowserOptions(
|
||||
surface: EveSurfaceDocument,
|
||||
options: EveBrowserLoweringOptions,
|
||||
): EveBrowserScopedOptions {
|
||||
return {
|
||||
...options,
|
||||
[eveBrowserRenderContext]: {
|
||||
surface,
|
||||
styles: normalizeSurfaceStyles(surface.surface?.styles),
|
||||
elements: new WeakMap(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function renderContext(options: EveBrowserLoweringOptions): EveBrowserRenderContext | undefined {
|
||||
return (options as EveBrowserScopedOptions)[eveBrowserRenderContext];
|
||||
}
|
||||
|
||||
export function renderEveSurface(
|
||||
surface: EveSurfaceDocument,
|
||||
host: HTMLElement,
|
||||
options: EveBrowserLoweringOptions = {},
|
||||
): HTMLElement {
|
||||
activeSurfaceBindings.get(host)?.dispose();
|
||||
currentSurfaceDocument = surface;
|
||||
currentOptions = options;
|
||||
currentSurfaceStyles = normalizeSurfaceStyles(surface.surface?.styles);
|
||||
(globalThis as { __eveCurrentMesh?: unknown }).__eveCurrentMesh = surface.mesh;
|
||||
applyEveSurfaceStyles(surface.surface?.styles, options.body ?? document.body);
|
||||
const scopedOptions = scopeEveBrowserOptions(surface, options);
|
||||
applyEveSurfaceStyles(surface.surface?.styles, options.body ?? host);
|
||||
host.dataset.provider = surface.providerId || options.provider?.providerId || "";
|
||||
if (options.body) {
|
||||
options.body.dataset.provider = surface.providerId || options.provider?.providerId || "";
|
||||
}
|
||||
|
|
@ -327,9 +351,9 @@ export function renderEveSurface(
|
|||
options.statusElement.textContent = `${surface.title || surface.surface?.title || "surface"}${options.source ? ` (${options.source})` : ""}`;
|
||||
}
|
||||
if (surface.surface?.root) {
|
||||
host.replaceChildren(renderEveComponent(surface.surface.root, options));
|
||||
host.replaceChildren(renderEveComponent(surface.surface.root, scopedOptions));
|
||||
if (options.stateBindingResolver) {
|
||||
const controller = new EveSurfaceBindingController(surface, host, options);
|
||||
const controller = new EveSurfaceBindingController(surface, host, scopedOptions);
|
||||
activeSurfaceBindings.set(host, controller);
|
||||
void controller.start();
|
||||
}
|
||||
|
|
@ -359,6 +383,8 @@ export function applyEveStateBindingValue(
|
|||
class EveSurfaceBindingController {
|
||||
private disposed = false;
|
||||
private readonly unsubscribers: Array<() => void> = [];
|
||||
private readonly pendingComponents = new Set<EveSurfaceComponent>();
|
||||
private projectionQueued = false;
|
||||
|
||||
constructor(
|
||||
private readonly surface: EveSurfaceDocument,
|
||||
|
|
@ -370,6 +396,7 @@ class EveSurfaceBindingController {
|
|||
const resolver = this.options.stateBindingResolver;
|
||||
const root = this.surface.surface?.root;
|
||||
if (!resolver || !root) return;
|
||||
const hydratedComponents = new Set<EveSurfaceComponent>();
|
||||
for (const component of walkEveComponents(root)) {
|
||||
for (const binding of component.stateBindings || []) {
|
||||
if (this.disposed) return;
|
||||
|
|
@ -378,24 +405,93 @@ class EveSurfaceBindingController {
|
|||
const apply = (value: unknown) => {
|
||||
if (this.disposed) return;
|
||||
applyEveStateBindingValue(component, binding, value);
|
||||
this.renderCanonicalProjection();
|
||||
this.queueComponentProjection(component);
|
||||
};
|
||||
apply(await handle.latest());
|
||||
applyEveStateBindingValue(component, binding, await handle.latest());
|
||||
hydratedComponents.add(component);
|
||||
if (this.disposed) return;
|
||||
this.unsubscribers.push(handle.watch(apply));
|
||||
}
|
||||
}
|
||||
for (const component of hydratedComponents) {
|
||||
if (this.disposed) return;
|
||||
this.renderComponentProjection(component);
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disposed = true;
|
||||
this.pendingComponents.clear();
|
||||
for (const unsubscribe of this.unsubscribers.splice(0)) unsubscribe();
|
||||
}
|
||||
|
||||
private renderCanonicalProjection(): void {
|
||||
const root = this.surface.surface?.root;
|
||||
if (root) this.host.replaceChildren(renderEveComponent(root, this.options));
|
||||
private queueComponentProjection(component: EveSurfaceComponent): void {
|
||||
this.pendingComponents.add(component);
|
||||
if (this.projectionQueued) return;
|
||||
this.projectionQueued = true;
|
||||
queueMicrotask(() => {
|
||||
this.projectionQueued = false;
|
||||
if (this.disposed) return;
|
||||
for (const pending of this.pendingComponents) this.renderComponentProjection(pending);
|
||||
this.pendingComponents.clear();
|
||||
});
|
||||
}
|
||||
|
||||
private renderComponentProjection(component: EveSurfaceComponent): void {
|
||||
const current = renderContext(this.options)?.elements.get(component);
|
||||
if (!current?.isConnected) return;
|
||||
replaceElementPreservingInteraction(
|
||||
current,
|
||||
renderEveComponent(component, this.options),
|
||||
this.host,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function replaceElementPreservingInteraction(
|
||||
current: HTMLElement,
|
||||
replacement: HTMLElement,
|
||||
host: HTMLElement,
|
||||
): void {
|
||||
const active = current.ownerDocument.activeElement;
|
||||
const activeElement = active instanceof HTMLElement && current.contains(active) ? active : undefined;
|
||||
const activePath = activeElement ? elementPath(current, activeElement) : undefined;
|
||||
const selection = activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement
|
||||
? { start: activeElement.selectionStart, end: activeElement.selectionEnd, direction: activeElement.selectionDirection }
|
||||
: undefined;
|
||||
const hostScroll = { left: host.scrollLeft, top: host.scrollTop };
|
||||
current.replaceWith(replacement);
|
||||
host.scrollLeft = hostScroll.left;
|
||||
host.scrollTop = hostScroll.top;
|
||||
if (!activePath) return;
|
||||
const nextActive = elementAtPath(replacement, activePath);
|
||||
nextActive?.focus({ preventScroll: true });
|
||||
if (selection && (nextActive instanceof HTMLInputElement || nextActive instanceof HTMLTextAreaElement)) {
|
||||
nextActive.setSelectionRange(selection.start, selection.end, selection.direction ?? undefined);
|
||||
}
|
||||
}
|
||||
|
||||
function elementPath(root: HTMLElement, target: HTMLElement): number[] | undefined {
|
||||
if (root === target) return [];
|
||||
const path: number[] = [];
|
||||
let current: HTMLElement | null = target;
|
||||
while (current && current !== root) {
|
||||
const parent: HTMLElement | null = current.parentElement;
|
||||
if (!parent) return undefined;
|
||||
path.unshift(Array.prototype.indexOf.call(parent.children, current));
|
||||
current = parent;
|
||||
}
|
||||
return current === root ? path : undefined;
|
||||
}
|
||||
|
||||
function elementAtPath(root: HTMLElement, path: readonly number[]): HTMLElement | undefined {
|
||||
let current: Element = root;
|
||||
for (const index of path) {
|
||||
const child = current.children.item(index);
|
||||
if (!(child instanceof HTMLElement)) return undefined;
|
||||
current = child;
|
||||
}
|
||||
return current instanceof HTMLElement ? current : undefined;
|
||||
}
|
||||
|
||||
function* walkEveComponents(root: EveSurfaceComponent): Generator<EveSurfaceComponent> {
|
||||
|
|
@ -405,7 +501,16 @@ function* walkEveComponents(root: EveSurfaceComponent): Generator<EveSurfaceComp
|
|||
|
||||
export function renderEveComponent(
|
||||
node: EveSurfaceComponent,
|
||||
options: EveBrowserLoweringOptions = currentOptions,
|
||||
options: EveBrowserLoweringOptions = {},
|
||||
): HTMLElement {
|
||||
const element = renderEveComponentProjection(node, options);
|
||||
renderContext(options)?.elements.set(node, element);
|
||||
return element;
|
||||
}
|
||||
|
||||
function renderEveComponentProjection(
|
||||
node: EveSurfaceComponent,
|
||||
options: EveBrowserLoweringOptions,
|
||||
): HTMLElement {
|
||||
const kind = node.kind || "panel";
|
||||
const props = objectProps(node.props);
|
||||
|
|
@ -485,8 +590,8 @@ export function renderEveComponent(
|
|||
if (adapter?.renderComponent) {
|
||||
return adapter.renderComponent(node, props, layout, style, options, {
|
||||
applyGeneratedLayout,
|
||||
providerId: currentSurfaceDocument?.providerId || options.provider?.providerId,
|
||||
resolveAssetUrl,
|
||||
providerId: renderContext(options)?.surface.providerId || options.provider?.providerId,
|
||||
resolveAssetUrl: uri => resolveAssetUrl(uri, options),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -496,7 +601,7 @@ export function renderEveComponent(
|
|||
|
||||
if (kind === "image.background") {
|
||||
const view = el("div", "cultui-background");
|
||||
const src = resolveAssetUrl(firstString(props.src, props.assetUri, props.assetRef, ""));
|
||||
const src = resolveAssetUrl(firstString(props.src, props.assetUri, props.assetRef, ""), options);
|
||||
if (src) view.style.backgroundImage = `url("${src}")`;
|
||||
view.setAttribute("aria-label", stringProp(props.label, "background"));
|
||||
return view;
|
||||
|
|
@ -505,7 +610,7 @@ export function renderEveComponent(
|
|||
if (kind === "image.sprite") {
|
||||
const figure = el("figure", `cultui-sprite ${stringProp(props.slot, "center")}`);
|
||||
assignId(figure, node);
|
||||
const src = resolveAssetUrl(firstString(props.src, props.assetUri, props.assetRef, ""));
|
||||
const src = resolveAssetUrl(firstString(props.src, props.assetUri, props.assetRef, ""), options);
|
||||
if (src) {
|
||||
const image = el("img") as HTMLImageElement;
|
||||
image.src = src;
|
||||
|
|
@ -518,7 +623,7 @@ export function renderEveComponent(
|
|||
if (kind === "avatar") {
|
||||
const card = el("figure", "cultui-avatar-card");
|
||||
assignId(card, node);
|
||||
const src = resolveAssetUrl(firstString(props.src, props.assetUri, props.assetRef, ""));
|
||||
const src = resolveAssetUrl(firstString(props.src, props.assetUri, props.assetRef, ""), options);
|
||||
if (src) {
|
||||
const image = el("img") as HTMLImageElement;
|
||||
image.src = src;
|
||||
|
|
@ -707,7 +812,7 @@ export function renderEveComponent(
|
|||
const figure = el("figure", "cultui-image-preview");
|
||||
assignId(figure, node);
|
||||
const frame = el("div", "cultui-image-frame");
|
||||
const src = resolveAssetUrl(firstString(props.src, props.assetUri, props.assetRef, ""));
|
||||
const src = resolveAssetUrl(firstString(props.src, props.assetUri, props.assetRef, ""), options);
|
||||
if (src) {
|
||||
const image = el("img") as HTMLImageElement;
|
||||
image.src = src;
|
||||
|
|
@ -1027,7 +1132,7 @@ function emitWorldCommand(
|
|||
export function createWorldActionIntent(
|
||||
command: string,
|
||||
action: Record<string, unknown>,
|
||||
options: EveBrowserLoweringOptions = currentOptions,
|
||||
options: EveBrowserLoweringOptions = {},
|
||||
): EveCommandIntent {
|
||||
return createEveCommandIntent(command, { action }, options);
|
||||
}
|
||||
|
|
@ -1261,7 +1366,7 @@ export function createInventoryDropIntent(
|
|||
target: Record<string, unknown>,
|
||||
destinationX: number,
|
||||
destinationY: number,
|
||||
options: EveBrowserLoweringOptions = currentOptions,
|
||||
options: EveBrowserLoweringOptions = {},
|
||||
): EveCommandIntent | undefined {
|
||||
const sourceKind = firstString(source.sourceKind, source.source, "");
|
||||
const command = firstString(target[`dropCommand.${sourceKind}`], target.dropCommand, "");
|
||||
|
|
@ -1628,11 +1733,12 @@ export function applyEveSurfaceStyles(styles: EveSurfaceStyles | undefined, body
|
|||
export function createEveCommandIntent(
|
||||
commandId: string,
|
||||
props: Record<string, unknown> = {},
|
||||
options: EveBrowserLoweringOptions = currentOptions,
|
||||
options: EveBrowserLoweringOptions = {},
|
||||
): EveCommandIntent {
|
||||
const action = objectProps(props.action);
|
||||
const providerId = options.provider?.providerId || currentSurfaceDocument?.providerId || "surface unknown";
|
||||
const surfaceId = options.activeSurfaceId || currentSurfaceDocument?.surface?.id || options.provider?.surfaces?.[0]?.surfaceId || providerId;
|
||||
const surface = renderContext(options)?.surface;
|
||||
const providerId = options.provider?.providerId || surface?.providerId || "surface unknown";
|
||||
const surfaceId = options.activeSurfaceId || surface?.surface?.id || options.provider?.surfaces?.[0]?.surfaceId || providerId;
|
||||
const worldInteraction = resolveAdvertisedWorldInteraction(options, surfaceId);
|
||||
const commandBoundary = firstString(worldInteraction.commandBoundary, props.commandBoundary, action.commandBoundary, action.target);
|
||||
const receiptSchema = firstString(worldInteraction.receiptSchema, props.receiptSchema, action.receiptSchema);
|
||||
|
|
@ -1715,7 +1821,7 @@ function renderSlider(
|
|||
children: EveSurfaceComponent[],
|
||||
options: EveBrowserLoweringOptions,
|
||||
): HTMLElement {
|
||||
const anatomy = resolveSliderAnatomy(props, children);
|
||||
const anatomy = resolveSliderAnatomy(props, children, options);
|
||||
const box = objectProps(anatomy.find(part => part.kind === "control.box")?.props);
|
||||
const parts = anatomy.filter(part => part.kind === "control.part");
|
||||
const min = Number(props.min ?? 0);
|
||||
|
|
@ -1764,9 +1870,15 @@ function renderSlider(
|
|||
return slider;
|
||||
}
|
||||
|
||||
function resolveSliderAnatomy(props: Record<string, unknown>, children: EveSurfaceComponent[]): EveSurfaceComponent[] {
|
||||
function resolveSliderAnatomy(
|
||||
props: Record<string, unknown>,
|
||||
children: EveSurfaceComponent[],
|
||||
options: EveBrowserLoweringOptions,
|
||||
): EveSurfaceComponent[] {
|
||||
const skinName = typeof props.skin === "string" ? props.skin : "";
|
||||
const skinChildren = skinName ? currentSurfaceStyles.controlSkins[skinName]?.children || [] : [];
|
||||
const skinChildren = skinName
|
||||
? renderContext(options)?.styles.controlSkins[skinName]?.children || []
|
||||
: [];
|
||||
const anatomy = [...skinChildren, ...children];
|
||||
if (anatomy.length) return anatomy;
|
||||
return [
|
||||
|
|
@ -1795,23 +1907,22 @@ function normalizeSurfaceStyles(styles: EveSurfaceStyles | undefined): Normalize
|
|||
};
|
||||
}
|
||||
|
||||
function resolveAssetUrl(uri: string): string {
|
||||
function resolveAssetUrl(uri: string, options: EveBrowserLoweringOptions): string {
|
||||
if (!uri) return "";
|
||||
const resolver = currentOptions.assetUrlResolver;
|
||||
if (resolver) return resolver(uri, currentSurfaceDocument);
|
||||
const surface = renderContext(options)?.surface;
|
||||
const resolver = options.assetUrlResolver;
|
||||
if (resolver) return resolver(uri, surface);
|
||||
if (/^(https?:|data:|blob:)/i.test(uri)) return uri;
|
||||
if (currentOptions.assetBaseUrl && uri.startsWith("/")) {
|
||||
return `${currentOptions.assetBaseUrl.replace(/\/+$/, "")}${uri}`;
|
||||
if (options.assetBaseUrl && uri.startsWith("/")) {
|
||||
return `${options.assetBaseUrl.replace(/\/+$/, "")}${uri}`;
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
function loadFontStylesheet(href: unknown): void {
|
||||
const normalized = typeof href === "string" ? href.trim() : "";
|
||||
if (activeFontStylesheet?.dataset.href === normalized) return;
|
||||
activeFontStylesheet?.remove();
|
||||
activeFontStylesheet = undefined;
|
||||
if (!normalized) return;
|
||||
if (loadedFontStylesheets.has(normalized)) return;
|
||||
const preconnect = document.createElement("link");
|
||||
preconnect.rel = "preconnect";
|
||||
preconnect.href = "https://fonts.gstatic.com";
|
||||
|
|
@ -1822,7 +1933,7 @@ function loadFontStylesheet(href: unknown): void {
|
|||
link.href = normalized;
|
||||
link.dataset.href = normalized;
|
||||
document.head.append(link);
|
||||
activeFontStylesheet = link;
|
||||
loadedFontStylesheets.set(normalized, link);
|
||||
}
|
||||
|
||||
function textClassName(kind: string, props: Record<string, unknown>, node: EveSurfaceComponent): string {
|
||||
|
|
|
|||
154
packages/eve-browser-lowering/test/host-isolation.test.mjs
Normal file
154
packages/eve-browser-lowering/test/host-isolation.test.mjs
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { JSDOM } from "jsdom";
|
||||
import { renderEveSurface } from "../dist/index.js";
|
||||
|
||||
function installDom() {
|
||||
const dom = new JSDOM("<!doctype html><html><head></head><body><div id='a'></div><div id='b'></div></body></html>", {
|
||||
url: "https://eve.test/",
|
||||
pretendToBeVisual: true,
|
||||
});
|
||||
const names = [
|
||||
"document",
|
||||
"window",
|
||||
"Element",
|
||||
"HTMLElement",
|
||||
"HTMLButtonElement",
|
||||
"HTMLInputElement",
|
||||
"HTMLTextAreaElement",
|
||||
"HTMLImageElement",
|
||||
];
|
||||
const previous = new Map(names.map(name => [name, globalThis[name]]));
|
||||
for (const name of names) globalThis[name] = dom.window[name];
|
||||
return () => {
|
||||
for (const [name, value] of previous) {
|
||||
if (value === undefined) delete globalThis[name];
|
||||
else globalThis[name] = value;
|
||||
}
|
||||
dom.window.close();
|
||||
};
|
||||
}
|
||||
|
||||
function testInputAdapter(onRender = () => {}) {
|
||||
return {
|
||||
pluginId: "test.input",
|
||||
componentKinds: ["test.input"],
|
||||
renderComponent(_node, props) {
|
||||
onRender();
|
||||
const input = document.createElement("input");
|
||||
input.type = "text";
|
||||
input.value = String(props.value ?? "");
|
||||
return input;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function surface(providerId, imagePath, skinPart, inputComponent) {
|
||||
return {
|
||||
providerId,
|
||||
surface: {
|
||||
id: `${providerId}.hangar`,
|
||||
styles: {
|
||||
tokens: { colorAccent: providerId === "alpha" ? "#a00" : "#00a" },
|
||||
controlSkins: {
|
||||
custom: {
|
||||
children: [{ kind: "control.part", props: { name: skinPart } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
root: {
|
||||
id: `${providerId}.root`,
|
||||
kind: "column",
|
||||
children: [
|
||||
{ kind: "image.preview", props: { src: imagePath, label: providerId } },
|
||||
{ kind: "control.slider", props: { skin: "custom", value: 0.5 } },
|
||||
inputComponent,
|
||||
{ kind: "control.button", props: { label: `Launch ${providerId}`, command: `${providerId}.launch` } },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test("browser lowering isolates two hosts and patches only the bound component", async () => {
|
||||
const restoreDom = installDom();
|
||||
try {
|
||||
let applyAlphaBinding;
|
||||
let alphaInputRenders = 0;
|
||||
const alphaCommands = [];
|
||||
const betaCommands = [];
|
||||
const alphaInput = {
|
||||
id: "alpha.input",
|
||||
kind: "test.input",
|
||||
props: { value: "initial" },
|
||||
stateBindings: [{
|
||||
targetProp: "value",
|
||||
pointerId: "alpha.input.value",
|
||||
sourceId: "alpha.state",
|
||||
schemaId: "alpha.state.v1",
|
||||
routeKind: "cultmesh",
|
||||
}],
|
||||
};
|
||||
const alphaSurface = surface("alpha", "/ship.png", "track-alpha", alphaInput);
|
||||
const betaSurface = surface("beta", "/ship.png", "track-beta", {
|
||||
id: "beta.input",
|
||||
kind: "test.input",
|
||||
props: { value: "beta" },
|
||||
});
|
||||
const alphaHost = document.querySelector("#a");
|
||||
const betaHost = document.querySelector("#b");
|
||||
|
||||
renderEveSurface(alphaSurface, alphaHost, {
|
||||
assetUrlResolver: uri => `https://alpha.assets${uri}`,
|
||||
commandSink: intent => alphaCommands.push(intent),
|
||||
pluginAdapters: [testInputAdapter(() => { alphaInputRenders++; })],
|
||||
stateBindingResolver: async () => ({
|
||||
latest: async () => "hydrated",
|
||||
watch: callback => {
|
||||
applyAlphaBinding = callback;
|
||||
return () => { applyAlphaBinding = undefined; };
|
||||
},
|
||||
}),
|
||||
});
|
||||
renderEveSurface(betaSurface, betaHost, {
|
||||
assetUrlResolver: uri => `https://beta.assets${uri}`,
|
||||
commandSink: intent => betaCommands.push(intent),
|
||||
pluginAdapters: [testInputAdapter()],
|
||||
});
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
assert.equal(alphaHost.querySelector("img").src, "https://alpha.assets/ship.png");
|
||||
assert.equal(betaHost.querySelector("img").src, "https://beta.assets/ship.png");
|
||||
assert.ok(alphaHost.querySelector(".track-alpha"));
|
||||
assert.ok(betaHost.querySelector(".track-beta"));
|
||||
|
||||
alphaHost.querySelector("button").click();
|
||||
betaHost.querySelector("button").click();
|
||||
assert.equal(alphaCommands[0].providerId, "alpha");
|
||||
assert.equal(alphaCommands[0].surfaceId, "alpha.hangar");
|
||||
assert.equal(betaCommands[0].providerId, "beta");
|
||||
assert.equal(betaCommands[0].surfaceId, "beta.hangar");
|
||||
|
||||
const alphaRoot = alphaHost.firstElementChild;
|
||||
const betaRoot = betaHost.firstElementChild;
|
||||
const focusedInput = alphaHost.querySelector("input[type='text']");
|
||||
focusedInput.focus();
|
||||
focusedInput.setSelectionRange(1, 4);
|
||||
alphaHost.scrollTop = 37;
|
||||
const rendersBeforeUpdates = alphaInputRenders;
|
||||
for (let index = 0; index < 1000; index++) applyAlphaBinding(`updated-${index}`);
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
const updatedInput = alphaHost.querySelector("input[type='text']");
|
||||
assert.equal(alphaHost.firstElementChild, alphaRoot);
|
||||
assert.equal(betaHost.firstElementChild, betaRoot);
|
||||
assert.equal(updatedInput.value, "updated-999");
|
||||
assert.equal(alphaInputRenders, rendersBeforeUpdates + 1);
|
||||
assert.equal(document.activeElement, updatedInput);
|
||||
assert.equal(updatedInput.selectionStart, 1);
|
||||
assert.equal(updatedInput.selectionEnd, 4);
|
||||
assert.equal(alphaHost.scrollTop, 37);
|
||||
} finally {
|
||||
restoreDom();
|
||||
}
|
||||
});
|
||||
Loading…
Reference in a new issue