@zixcel/dom-renderer 0.10.4 → 0.10.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/package.json +7 -5
- package/src/book-reader.d.mts +9 -1
- package/src/book-reader.mjs +54 -33
- package/src/dom-elements.d.mts +11 -0
- package/src/dom-elements.mjs +48 -0
- package/src/frame-layout.d.mts +9 -3
- package/src/frame-layout.mjs +22 -22
- package/src/frame.d.mts +22 -10
- package/src/frame.mjs +93 -45
- package/src/geometry-policy.d.mts +3 -0
- package/src/geometry-policy.mjs +17 -0
- package/src/graph-documents.d.mts +48 -0
- package/src/graph-documents.mjs +40 -0
- package/src/graphics-disposal.d.mts +2 -0
- package/src/graphics-disposal.mjs +14 -0
- package/src/index.d.mts +11 -57
- package/src/index.mjs +9 -205
- package/src/input-form.d.mts +5 -0
- package/src/input-form.mjs +103 -0
- package/src/inspection-overview.d.mts +27 -1
- package/src/inspection-overview.mjs +50 -33
- package/src/npc-motion.d.mts +1 -1
- package/src/npc-motion.mjs +5 -5
- package/src/object-presentation.d.mts +22 -26
- package/src/object-presentation.mjs +20 -17
- package/src/open-world-projection.d.mts +19 -10
- package/src/open-world-projection.mjs +31 -22
- package/src/reconcile.d.mts +1 -1
- package/src/reconcile.mjs +31 -19
- package/src/scene-items.d.mts +9 -0
- package/src/scene-items.mjs +29 -0
- package/src/scene-renderer.d.mts +37 -0
- package/src/scene-renderer.mjs +170 -0
- package/src/spatial-patterns.d.mts +22 -27
- package/src/spatial-patterns.mjs +63 -41
- package/src/spatial.d.mts +17 -6
- package/src/spatial.mjs +39 -25
- package/src/structure-graph.d.mts +5 -13
- package/src/structure-graph.mjs +167 -96
- package/src/symbol.d.mts +1 -1
- package/src/symbol.mjs +21 -16
- package/src/world-api.d.mts +56 -0
- package/src/world-api.mjs +8 -0
- package/src/world-camera.d.mts +14 -0
- package/src/world-camera.mjs +74 -0
- package/src/world-clock.d.mts +19 -0
- package/src/world-clock.mjs +30 -0
- package/src/world-commands.d.mts +39 -0
- package/src/world-commands.mjs +1 -0
- package/src/world-context.d.mts +132 -0
- package/src/world-context.mjs +1 -0
- package/src/world-controller.d.mts +43 -0
- package/src/world-controller.mjs +62 -0
- package/src/world-conversation.d.mts +3 -0
- package/src/world-conversation.mjs +46 -0
- package/src/world-events.d.mts +2 -0
- package/src/world-events.mjs +120 -0
- package/src/world-gpu.d.mts +3 -15
- package/src/world-gpu.mjs +159 -74
- package/src/world-input.d.mts +2 -2
- package/src/world-input.mjs +28 -18
- package/src/world-lifecycle.d.mts +12 -0
- package/src/world-lifecycle.mjs +172 -0
- package/src/world-model.d.mts +29 -40
- package/src/world-model.mjs +103 -74
- package/src/world-projection.d.mts +5 -0
- package/src/world-projection.mjs +169 -0
- package/src/world-render-loop.d.mts +9 -0
- package/src/world-render-loop.mjs +110 -0
- package/src/world-resources.d.mts +34 -0
- package/src/world-resources.mjs +8 -0
- package/src/world-runtime.d.mts +2 -36
- package/src/world-runtime.mjs +6 -311
- package/src/world-setup.d.mts +2 -0
- package/src/world-setup.mjs +179 -0
- package/src/world-surface.d.mts +2 -7
- package/src/world-surface.mjs +20 -12
- package/src/world-types.d.mts +74 -0
- package/src/world-types.mjs +1 -0
package/README.md
CHANGED
|
@@ -36,4 +36,10 @@ pnpm test
|
|
|
36
36
|
|
|
37
37
|
## Release provenance
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
Current releases use GitHub Actions OIDC trusted publishing from `zixcel/data-presentation` (`publish-npm.yml`), without a stored npm publishing token. The workflow verifies reviewed archive hashes and requires npm provenance metadata. Check the published version on npm and verify registry signatures and provenance with `npm audit signatures` in an installed consumer project. Package runtime behavior and API contracts are unchanged by this release.
|
|
40
|
+
|
|
41
|
+
## Node.js security baseline
|
|
42
|
+
|
|
43
|
+
Use a supported Node.js security release for Node-based build, delivery and consumer processes: at least 22.23.4, 24.22.1 or 26.12.1. The renderer runs in the browser and does not bundle Node. Updating an npm library does not update the caller's Node executable. The October 21, 2026 security fixes include TLS, DNS, HTTP and Brotli handling; operator-managed runtimes must be updated independently. GitHub Actions pins select the shell/build runtime, not the GitHub-managed runtime of JavaScript Actions.
|
|
44
|
+
|
|
45
|
+
Official advisory: https://nodejs.org/en/blog/vulnerability/october-2026-security-releases
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zixcel/dom-renderer",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"description": "Trusted native DOM rendering of PP RenderPlans and Zixcel owner inputs",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"three": "0.186.1",
|
|
24
24
|
"@zixcel/interaction": "0.10.1",
|
|
25
|
-
"@zixcel/projection-client": "0.10.
|
|
25
|
+
"@zixcel/projection-client": "0.10.5"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@playwright/test": "1.64.1",
|
|
@@ -38,10 +38,12 @@
|
|
|
38
38
|
"dom",
|
|
39
39
|
"data-presentation",
|
|
40
40
|
"rendering",
|
|
41
|
-
"
|
|
41
|
+
"typescript"
|
|
42
42
|
],
|
|
43
43
|
"scripts": {
|
|
44
|
-
"test": "node --test test/*.test.
|
|
45
|
-
"test:browser": "node test/world-browser.
|
|
44
|
+
"test": "node --test --test-concurrency=1 test/*.test.mts",
|
|
45
|
+
"test:browser": "node test/world-browser.mts",
|
|
46
|
+
"build": "tsc -p tsconfig.build.json && node ../../scripts/emit-dom-declarations.mts",
|
|
47
|
+
"typecheck": "tsc -p tsconfig.build.json --noEmit"
|
|
46
48
|
}
|
|
47
49
|
}
|
package/src/book-reader.d.mts
CHANGED
|
@@ -1 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
import type { RenderPlan } from '@zixcel/projection-client';
|
|
2
|
+
export interface BookPosition {
|
|
3
|
+
id?: string;
|
|
4
|
+
revision?: string;
|
|
5
|
+
page: number;
|
|
6
|
+
}
|
|
7
|
+
type DisplayItem = RenderPlan['regions'][number]['items'][number];
|
|
8
|
+
export declare function bookReader(document: Document, item: DisplayItem, revision: string, close: HTMLElement, signal: AbortSignal, previous: BookPosition | null): HTMLElement;
|
|
9
|
+
export {};
|
package/src/book-reader.mjs
CHANGED
|
@@ -1,36 +1,57 @@
|
|
|
1
1
|
// A world-local inspection view over declared scalar fields. Page position is
|
|
2
2
|
// presentation state only; neither the object nor its evidence is copied.
|
|
3
|
-
import {symbol} from './symbol.mjs'
|
|
4
|
-
export function bookReader(document,item,revision,close,signal,previous){
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
root
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
3
|
+
import { symbol } from './symbol.mjs';
|
|
4
|
+
export function bookReader(document, item, revision, close, signal, previous) {
|
|
5
|
+
const make = (tag, text, className) => { const node = document.createElement(tag); if (text !== undefined)
|
|
6
|
+
node.textContent = text; if (className)
|
|
7
|
+
node.className = className; return node; };
|
|
8
|
+
const root = make('article', undefined, 'entity-inspector game-book'), header = make('header'), title = make('h2', item.display?.title ?? 'Display not configured');
|
|
9
|
+
root.dataset.bookId = item.id;
|
|
10
|
+
root.dataset.bookRevision = revision;
|
|
11
|
+
root.setAttribute('aria-label', 'Inspect ' + title.textContent);
|
|
12
|
+
const heading = make('div', undefined, 'game-book-heading');
|
|
13
|
+
heading.append(make('small', 'INSPECT'), title);
|
|
14
|
+
if (item.display?.summary)
|
|
15
|
+
heading.append(make('p', item.display.summary));
|
|
16
|
+
header.append(heading, close);
|
|
17
|
+
const object = make('div', undefined, 'game-book-object');
|
|
18
|
+
object.setAttribute('aria-hidden', 'true');
|
|
19
|
+
object.append(symbol(document, 'book'));
|
|
20
|
+
const pageView = make('section', undefined, 'game-book-page'), body = make('div', undefined, 'game-book-body'), footer = make('nav'), counter = make('output');
|
|
21
|
+
pageView.setAttribute('aria-live', 'polite');
|
|
22
|
+
body.append(object, pageView);
|
|
23
|
+
footer.setAttribute('aria-label', 'Book pages');
|
|
24
|
+
const fields = item.display?.fields ?? [], pages = Math.max(1, Math.ceil(fields.length / 2));
|
|
25
|
+
let page = previous?.id === item.id && previous.revision === revision && Number.isInteger(previous.page) ? Math.max(0, Math.min(pages - 1, previous.page)) : 0;
|
|
26
|
+
const button = (text, delta) => { const node = make('button', text); node.type = 'button'; node.addEventListener('click', () => { page = Math.max(0, Math.min(pages - 1, page + delta)); draw(); }, { signal }); return node; };
|
|
27
|
+
const back = button('Previous pages', -1), next = button('Next pages', 1);
|
|
28
|
+
footer.append(back, counter, next);
|
|
29
|
+
root.append(header, body, footer);
|
|
30
|
+
function draw() {
|
|
31
|
+
root.dataset.bookPage = String(page);
|
|
32
|
+
pageView.setAttribute('aria-label', 'Page ' + (page + 1) + ' of ' + pages);
|
|
33
|
+
const list = make('dl', undefined, 'object-fields');
|
|
34
|
+
for (const field of fields.slice(page * 2, page * 2 + 2))
|
|
35
|
+
list.append(make('dt', field.label), make('dd', field.value === null ? 'Not set' : String(field.value)));
|
|
36
|
+
pageView.replaceChildren(make('small', 'PAGE ' + String(page + 1).padStart(2, '0') + ' / ' + String(pages).padStart(2, '0')), list);
|
|
37
|
+
if (!list.children.length)
|
|
38
|
+
pageView.append(make('p', 'No display fields have been declared.'));
|
|
39
|
+
const focused = document.activeElement, refocus = focused === back && page === 0 ? next : focused === next && page === pages - 1 ? back : null;
|
|
40
|
+
counter.textContent = (page + 1) + ' / ' + pages;
|
|
41
|
+
back.disabled = page === 0;
|
|
42
|
+
next.disabled = page === pages - 1;
|
|
43
|
+
refocus?.focus({ preventScroll: true });
|
|
44
|
+
}
|
|
45
|
+
document.addEventListener('keydown', event => {
|
|
46
|
+
if (!['ArrowLeft', 'ArrowRight'].includes(event.key) || event.altKey || event.ctrlKey || event.metaKey)
|
|
47
|
+
return;
|
|
48
|
+
const panel = root.closest('.frame-panel'), focused = document.activeElement;
|
|
49
|
+
if (!panel?.contains(focused) || focused?.matches('input,select,textarea,[contenteditable]'))
|
|
50
|
+
return;
|
|
51
|
+
event.preventDefault();
|
|
52
|
+
page = Math.max(0, Math.min(pages - 1, page + (event.key === 'ArrowRight' ? 1 : -1)));
|
|
53
|
+
draw();
|
|
54
|
+
}, { signal });
|
|
55
|
+
draw();
|
|
56
|
+
return root;
|
|
36
57
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const renderBounds: Readonly<{
|
|
2
|
+
nodes: 8192;
|
|
3
|
+
depth: 20;
|
|
4
|
+
textBytes: 524288;
|
|
5
|
+
}>;
|
|
6
|
+
export declare function fail(code: string): never;
|
|
7
|
+
export declare function element<K extends keyof HTMLElementTagNameMap>(document: Document, tag: K, text?: unknown, attrs?: Record<string, unknown>): HTMLElementTagNameMap[K];
|
|
8
|
+
export declare function valueView(document: Document, value: unknown, budget?: {
|
|
9
|
+
nodes: number;
|
|
10
|
+
bytes: number;
|
|
11
|
+
}): HTMLElement;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Trusted DOM primitives: external values are rendered as text, never HTML.
|
|
2
|
+
export const renderBounds = Object.freeze({ nodes: 8192, depth: 20, textBytes: 524288 });
|
|
3
|
+
export function fail(code) { throw Object.assign(Error(code), { code }); }
|
|
4
|
+
export function element(document, tag, text = null, attrs = {}) {
|
|
5
|
+
const node = document.createElement(tag);
|
|
6
|
+
if (text !== null)
|
|
7
|
+
node.textContent = String(text);
|
|
8
|
+
for (const [key, value] of Object.entries(attrs))
|
|
9
|
+
if (value !== null)
|
|
10
|
+
node.setAttribute(key, String(value));
|
|
11
|
+
return node;
|
|
12
|
+
}
|
|
13
|
+
export function valueView(document, value, budget = { nodes: 0, bytes: 0 }) {
|
|
14
|
+
function walk(value, depth) {
|
|
15
|
+
if (++budget.nodes > renderBounds.nodes || depth > renderBounds.depth)
|
|
16
|
+
fail('RenderLimitExceeded');
|
|
17
|
+
if (value === null || typeof value !== 'object') {
|
|
18
|
+
if (typeof value === 'string' && value.length > renderBounds.textBytes)
|
|
19
|
+
fail('RenderLimitExceeded');
|
|
20
|
+
const text = JSON.stringify(value);
|
|
21
|
+
if (typeof text !== 'string')
|
|
22
|
+
fail('InvalidRenderValue');
|
|
23
|
+
budget.bytes += new TextEncoder().encode(text).length;
|
|
24
|
+
if (budget.bytes > renderBounds.textBytes)
|
|
25
|
+
fail('RenderLimitExceeded');
|
|
26
|
+
return element(document, 'span', text, { 'data-projection-scalar': text });
|
|
27
|
+
}
|
|
28
|
+
if (Object.keys(value).length > 4096)
|
|
29
|
+
fail('RenderLimitExceeded');
|
|
30
|
+
if (!Object.keys(value).length)
|
|
31
|
+
return element(document, 'span', Array.isArray(value) ? '[]' : '{}', { 'data-empty-value': Array.isArray(value) ? 'array' : 'object' });
|
|
32
|
+
const list = element(document, Array.isArray(value) ? 'ol' : 'dl');
|
|
33
|
+
for (const [key, child] of Object.entries(value)) {
|
|
34
|
+
if (Array.isArray(value)) {
|
|
35
|
+
const item = element(document, 'li');
|
|
36
|
+
item.append(walk(child, depth + 1));
|
|
37
|
+
list.append(item);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
const name = element(document, 'dt', key), item = element(document, 'dd');
|
|
41
|
+
item.append(walk(child, depth + 1));
|
|
42
|
+
list.append(name, item);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return list;
|
|
46
|
+
}
|
|
47
|
+
return walk(value, 0);
|
|
48
|
+
}
|
package/src/frame-layout.d.mts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface AnchorBounds {
|
|
2
|
+
left: number;
|
|
3
|
+
right: number;
|
|
4
|
+
top: number;
|
|
5
|
+
bottom: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function frameLayout(width: number, height: number, kind: string, anchor?: AnchorBounds | null): {
|
|
2
8
|
mode: string;
|
|
3
9
|
world: {
|
|
4
|
-
width:
|
|
5
|
-
height:
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
6
12
|
};
|
|
7
13
|
panel: {
|
|
8
14
|
x: number;
|
package/src/frame-layout.mjs
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
// Pure presentation policy: never resize the world or refit its camera for a panel.
|
|
2
2
|
// The exact object's projected bounds determine an adjacent, bounded bubble.
|
|
3
|
-
export function frameLayout(width,height,kind,anchor=null){
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
3
|
+
export function frameLayout(width, height, kind, anchor = null) {
|
|
4
|
+
const gap = 12, left = 8, top = 56, right = width - 8, bottom = height - 64;
|
|
5
|
+
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
|
|
6
|
+
const w = Math.min(kind === 'actions' ? 320 : ['details', 'structure'].includes(kind) ? 640 : 432, right - left);
|
|
7
|
+
const h = Math.min(kind === 'actions' ? 360 : kind === 'details' ? height * .6 : 480, bottom - top);
|
|
8
|
+
let panel = { x: right - w, y: top, width: w, height: h }, mode = 'overlay';
|
|
9
|
+
if (anchor) {
|
|
10
|
+
const areas = [
|
|
11
|
+
{ side: 'right', x: Math.max(left, anchor.right + gap), y: top, right, bottom },
|
|
12
|
+
{ side: 'left', x: left, y: top, right: Math.min(right, anchor.left - gap), bottom },
|
|
13
|
+
{ side: 'above', x: left, y: top, right, bottom: Math.min(bottom, anchor.top - gap) },
|
|
14
|
+
{ side: 'below', x: left, y: Math.max(top, anchor.bottom + gap), right, bottom }
|
|
15
|
+
].map(a => ({ ...a, width: Math.min(w, Math.max(0, a.right - a.x)), height: Math.min(h, Math.max(0, a.bottom - a.y)) }));
|
|
16
|
+
const best = areas.filter(a => a.width >= Math.min(240, w) && a.height >= 80).sort((a, b) => b.width * b.height - a.width * a.height)[0];
|
|
17
|
+
if (best) {
|
|
18
|
+
const vertical = ['above', 'below'].includes(best.side);
|
|
19
|
+
panel = { x: vertical ? clamp((anchor.left + anchor.right - best.width) / 2, best.x, best.right - best.width) : best.side === 'left' ? best.right - best.width : best.x,
|
|
20
|
+
y: vertical ? (best.side === 'above' ? best.bottom - best.height : best.y) : clamp(anchor.top, best.y, best.bottom - best.height), width: best.width, height: best.height };
|
|
21
|
+
mode = best.side;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return { mode, world: { width, height }, panel };
|
|
25
25
|
}
|
package/src/frame.d.mts
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
export interface FramePanel {
|
|
2
|
+
id: string;
|
|
3
|
+
label: string;
|
|
4
|
+
symbol: string;
|
|
5
|
+
nodes?: readonly Node[];
|
|
6
|
+
activate?: () => void;
|
|
7
|
+
dock?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface FrameOptions {
|
|
10
|
+
stage: Node;
|
|
11
|
+
title: Node;
|
|
12
|
+
state: HTMLElement;
|
|
13
|
+
panels: FramePanel[];
|
|
14
|
+
}
|
|
15
|
+
export declare function windowFrame(root: HTMLElement, { stage, title, state, panels }: FrameOptions): {
|
|
16
|
+
open: (id: string, objectId?: string | null) => void;
|
|
8
17
|
close: (restoreFocus?: boolean) => void;
|
|
9
|
-
speaker(value:
|
|
10
|
-
|
|
11
|
-
|
|
18
|
+
speaker(value: {
|
|
19
|
+
id: string;
|
|
20
|
+
title?: string;
|
|
21
|
+
} | null): void;
|
|
22
|
+
label(id: string, value: string): void;
|
|
23
|
+
notify(count: number): void;
|
|
12
24
|
dispose(): void;
|
|
13
25
|
};
|
package/src/frame.mjs
CHANGED
|
@@ -1,47 +1,95 @@
|
|
|
1
|
+
import { eventDetail, record } from './world-api.mjs';
|
|
1
2
|
// Presentation-only window frame: one active panel, stable content and bounded listeners.
|
|
2
|
-
import {symbol} from './symbol.mjs'
|
|
3
|
-
import {frameLayout} from './frame-layout.mjs'
|
|
4
|
-
export function windowFrame(root,{stage,title,state,panels}){
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
root.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
3
|
+
import { symbol } from './symbol.mjs';
|
|
4
|
+
import { frameLayout } from './frame-layout.mjs';
|
|
5
|
+
export function windowFrame(root, { stage, title, state, panels }) {
|
|
6
|
+
const document = root.ownerDocument, browser = document.defaultView;
|
|
7
|
+
if (!browser)
|
|
8
|
+
throw Error('FrameDocumentUnavailable');
|
|
9
|
+
const window = browser, abort = new AbortController(), buttons = new Map(), bodies = new Map();
|
|
10
|
+
const make = (tag, attrs = {}) => { const node = document.createElement(tag); for (const [k, v] of Object.entries(attrs))
|
|
11
|
+
node.setAttribute(k, v); return node; };
|
|
12
|
+
const dock = make('nav', { class: 'frame-dock', 'aria-label': 'Window tools' }), bar = make('header', { class: 'frame-bar' });
|
|
13
|
+
const panel = make('aside', { class: 'frame-panel', role: 'dialog', 'aria-modal': 'false', tabindex: '-1' }), head = make('header'), heading = make('h2'), closeButton = make('button', { type: 'button', 'aria-label': 'Close panel', title: 'Close panel' });
|
|
14
|
+
closeButton.textContent = '×';
|
|
15
|
+
head.append(heading, closeButton);
|
|
16
|
+
panel.append(head);
|
|
17
|
+
panel.hidden = true;
|
|
18
|
+
let active = null, speaker = null, target = null, anchor = null;
|
|
19
|
+
const requestAnchor = () => root.dispatchEvent(new window.CustomEvent('world-anchor-request', { detail: { id: target } }));
|
|
20
|
+
const position = () => {
|
|
21
|
+
if (!active) {
|
|
22
|
+
delete root.dataset.panelLayout;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const layout = frameLayout(root.clientWidth, root.clientHeight, active, anchor), rect = layout.panel;
|
|
26
|
+
root.dataset.panelLayout = layout.mode;
|
|
27
|
+
panel.dataset.anchorId = target ?? '';
|
|
28
|
+
if (anchor)
|
|
29
|
+
panel.dataset.anchorBounds = JSON.stringify(anchor);
|
|
30
|
+
else
|
|
31
|
+
delete panel.dataset.anchorBounds;
|
|
32
|
+
Object.assign(panel.style, { left: rect.x + 'px', top: rect.y + 'px', right: 'auto', bottom: 'auto', width: rect.width + 'px', height: rect.height + 'px', maxHeight: rect.height + 'px' });
|
|
33
|
+
};
|
|
34
|
+
const describe = () => { const talking = Boolean(speaker && active === 'actions'); panel.dataset.book = String(active === 'details'); panel.dataset.conversation = String(talking); if (talking && speaker)
|
|
35
|
+
heading.textContent = speaker.title ?? 'Information'; position(); };
|
|
36
|
+
root.addEventListener('world-anchor', event => { const detail = eventDetail(event); if (detail.id !== target)
|
|
37
|
+
return; const b = record(detail.bounds); anchor = ['left', 'right', 'top', 'bottom'].every(k => typeof b[k] === 'number' && Number.isFinite(b[k])) ? { left: Number(b.left), right: Number(b.right), top: Number(b.top), bottom: Number(b.bottom) } : null; position(); }, { signal: abort.signal });
|
|
38
|
+
const resize = new window.ResizeObserver(() => { requestAnchor(); position(); });
|
|
39
|
+
resize.observe(root);
|
|
40
|
+
function close(restoreFocus = true) { const previous = active ? buttons.get(active) : undefined; active = null; target = null; anchor = null; requestAnchor(); panel.hidden = true; delete root.dataset.panel; position(); for (const b of buttons.values())
|
|
41
|
+
b.setAttribute('aria-expanded', 'false'); if (restoreFocus)
|
|
42
|
+
(previous?.isConnected ? previous : dock.querySelector('button'))?.focus({ preventScroll: true }); }
|
|
43
|
+
function open(id, objectId = null) {
|
|
44
|
+
if (!bodies.has(id))
|
|
45
|
+
return;
|
|
46
|
+
active = id;
|
|
47
|
+
target = objectId ?? (id === 'actions' ? speaker?.id ?? null : null);
|
|
48
|
+
anchor = null;
|
|
49
|
+
panel.hidden = false;
|
|
50
|
+
heading.textContent = panels.find(p => p.id === id)?.label ?? '';
|
|
51
|
+
panel.setAttribute('aria-label', heading.textContent);
|
|
52
|
+
for (const [key, body] of bodies)
|
|
53
|
+
body.hidden = key !== id;
|
|
54
|
+
for (const [key, button] of buttons)
|
|
55
|
+
button.setAttribute('aria-expanded', String(key === id));
|
|
56
|
+
root.dataset.panel = id;
|
|
57
|
+
root.dispatchEvent(new window.CustomEvent('frame-panel-open'));
|
|
58
|
+
describe();
|
|
59
|
+
requestAnchor();
|
|
60
|
+
panel.focus({ preventScroll: true });
|
|
61
|
+
}
|
|
62
|
+
for (const item of panels) {
|
|
63
|
+
if (item.activate) {
|
|
64
|
+
const button = make('button', { type: 'button', 'aria-label': item.label, title: item.label, 'data-panel': item.id });
|
|
65
|
+
button.append(symbol(document, item.symbol), Object.assign(document.createElement('span'), { textContent: item.label }));
|
|
66
|
+
button.addEventListener('click', () => { close(); item.activate?.(); }, { signal: abort.signal });
|
|
67
|
+
dock.append(button);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const id = 'panel-' + crypto.randomUUID(), button = make('button', { type: 'button', 'aria-label': item.label, title: item.label, 'aria-expanded': 'false', 'aria-controls': id, 'data-panel': item.id });
|
|
71
|
+
button.append(symbol(document, item.symbol), Object.assign(document.createElement('span'), { textContent: item.label }));
|
|
72
|
+
button.addEventListener('click', () => active === item.id ? close() : open(item.id), { signal: abort.signal });
|
|
73
|
+
const body = make('section', { id, class: 'frame-panel-body' });
|
|
74
|
+
body.hidden = true;
|
|
75
|
+
body.append(...item.nodes ?? []);
|
|
76
|
+
panel.append(body);
|
|
77
|
+
if (item.dock !== false)
|
|
78
|
+
dock.append(button);
|
|
79
|
+
buttons.set(item.id, button);
|
|
80
|
+
bodies.set(item.id, body);
|
|
81
|
+
}
|
|
82
|
+
closeButton.addEventListener('click', () => close(), { signal: abort.signal });
|
|
83
|
+
document.addEventListener('keydown', event => { if (event.key === 'Escape' && active !== null) {
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
close();
|
|
86
|
+
} }, { signal: abort.signal });
|
|
87
|
+
bar.append(title, state);
|
|
88
|
+
root.replaceChildren(stage, bar, dock, panel);
|
|
89
|
+
return { open, close, speaker(value) { speaker = value; describe(); }, label(id, value) { const item = panels.find(p => p.id === id), button = buttons.get(id); if (!item || !button)
|
|
90
|
+
return; item.label = value; button.setAttribute('aria-label', value); button.title = value; const label = button.querySelector('span:last-child'); if (label)
|
|
91
|
+
label.textContent = value; if (active === id) {
|
|
92
|
+
heading.textContent = value;
|
|
93
|
+
panel.setAttribute('aria-label', value);
|
|
94
|
+
} }, notify(count) { state.setAttribute('data-attention', String(count > 0)); state.title = count ? count + ' notifications' : 'Connection status'; }, dispose() { resize.disconnect(); abort.abort(); buttons.clear(); bodies.clear(); root.replaceChildren(); } };
|
|
47
95
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { worldLimits } from './world-model.mjs';
|
|
2
|
+
/** Validate the complete upload before allocating or updating GPU state. */
|
|
3
|
+
export function admitGeometry(data, ranges) {
|
|
4
|
+
const fail = () => { throw Error('WorldGeometryLimitExceeded'); };
|
|
5
|
+
if (!(data instanceof Float32Array) || data.length % 9 || data.length / 9 >= worldLimits.instances || ranges.length > worldLimits.objects)
|
|
6
|
+
fail();
|
|
7
|
+
for (const value of data)
|
|
8
|
+
if (!Number.isFinite(value))
|
|
9
|
+
fail();
|
|
10
|
+
const ids = new Set(), count = data.length / 9;
|
|
11
|
+
for (const range of ranges) {
|
|
12
|
+
if (!range || typeof range.id !== 'string' || !range.id || range.id.length > 512 || ids.has(range.id) ||
|
|
13
|
+
!Number.isSafeInteger(range.start) || !Number.isSafeInteger(range.end) || range.start < 0 || range.end <= range.start || range.end > count)
|
|
14
|
+
fail();
|
|
15
|
+
ids.add(range.id);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { InspectionOverview } from './inspection-overview.mjs';
|
|
2
|
+
export interface GraphDocument {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
value: unknown;
|
|
6
|
+
source?: {
|
|
7
|
+
method?: string;
|
|
8
|
+
semanticRevision?: string;
|
|
9
|
+
memoryRevision?: string;
|
|
10
|
+
truncated?: boolean;
|
|
11
|
+
roleRef?: {
|
|
12
|
+
id?: string;
|
|
13
|
+
revision?: string;
|
|
14
|
+
digest_sha256?: string;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export interface GraphNode {
|
|
19
|
+
id: string;
|
|
20
|
+
label: string;
|
|
21
|
+
path: string;
|
|
22
|
+
level: number;
|
|
23
|
+
value: unknown;
|
|
24
|
+
document: GraphDocument;
|
|
25
|
+
}
|
|
26
|
+
export interface GraphEdge {
|
|
27
|
+
from: string;
|
|
28
|
+
to: string;
|
|
29
|
+
label: string;
|
|
30
|
+
}
|
|
31
|
+
export interface GraphView {
|
|
32
|
+
roles: readonly {
|
|
33
|
+
id: string;
|
|
34
|
+
}[];
|
|
35
|
+
selected?: string | null;
|
|
36
|
+
documents: GraphDocument[];
|
|
37
|
+
overview: InspectionOverview | null;
|
|
38
|
+
}
|
|
39
|
+
export declare function graphDocuments(documents: GraphDocument[], { component, query, depth, maximum }?: {
|
|
40
|
+
component?: string | undefined;
|
|
41
|
+
depth?: number | undefined;
|
|
42
|
+
maximum?: number | undefined;
|
|
43
|
+
query?: string | undefined;
|
|
44
|
+
}): {
|
|
45
|
+
nodes: GraphNode[];
|
|
46
|
+
edges: GraphEdge[];
|
|
47
|
+
truncated: boolean;
|
|
48
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Hatter 2026: bounded document-containment graph. No meaning inference or writes.
|
|
2
|
+
export function graphDocuments(documents, { component = '', query = '', depth = 3, maximum = 256 } = {}) {
|
|
3
|
+
if (!Array.isArray(documents) || documents.length > 32 || !Number.isInteger(depth) || depth < 1 || depth > 6 || !Number.isSafeInteger(maximum) || maximum < 1 || maximum > 256)
|
|
4
|
+
throw Error('GraphInputInvalid');
|
|
5
|
+
const nodes = [], edges = [];
|
|
6
|
+
let truncated = false;
|
|
7
|
+
const add = (value, label, path, level, parent, document) => {
|
|
8
|
+
if (nodes.length >= maximum) {
|
|
9
|
+
truncated = true;
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const id = document.id + ':' + path, node = { id, label, path, level, value, document };
|
|
13
|
+
nodes.push(node);
|
|
14
|
+
if (parent)
|
|
15
|
+
edges.push({ from: parent, to: id, label });
|
|
16
|
+
if (value && typeof value === 'object') {
|
|
17
|
+
const entries = Object.entries(value);
|
|
18
|
+
if (level >= depth) {
|
|
19
|
+
if (entries.length)
|
|
20
|
+
truncated = true;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
for (const [key, child] of entries) {
|
|
24
|
+
if (nodes.length >= maximum) {
|
|
25
|
+
truncated = true;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
add(child, key, path + '/' + key.replaceAll('~', '~0').replaceAll('/', '~1'), level + 1, id, document);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
for (const doc of documents)
|
|
33
|
+
if (!component || doc.id === component)
|
|
34
|
+
add(doc.value, doc.label, '', 0, null, doc);
|
|
35
|
+
if (query) {
|
|
36
|
+
const matches = new Set(nodes.filter(n => (n.label + ' ' + n.path + ' ' + (n.value === null || typeof n.value !== 'object' ? String(n.value) : '')).toLowerCase().includes(query.toLowerCase())).map(n => n.id));
|
|
37
|
+
return { nodes: nodes.filter(n => matches.has(n.id)), edges: edges.filter(e => matches.has(e.from) && matches.has(e.to)), truncated };
|
|
38
|
+
}
|
|
39
|
+
return { nodes, edges, truncated };
|
|
40
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Drain each independently owned resource; preserve falsy failures too. */
|
|
2
|
+
export function disposeGraphics(operations) {
|
|
3
|
+
const failures = [];
|
|
4
|
+
for (const operation of operations) {
|
|
5
|
+
try {
|
|
6
|
+
operation();
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
failures.push(error);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
if (failures.length)
|
|
13
|
+
throw new AggregateError(failures, 'Graphics cleanup failed');
|
|
14
|
+
}
|