@zixcel/dom-renderer 0.10.4 → 0.10.5

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.
Files changed (80) hide show
  1. package/README.md +6 -0
  2. package/package.json +6 -4
  3. package/src/book-reader.d.mts +9 -1
  4. package/src/book-reader.mjs +54 -33
  5. package/src/dom-elements.d.mts +11 -0
  6. package/src/dom-elements.mjs +48 -0
  7. package/src/frame-layout.d.mts +9 -3
  8. package/src/frame-layout.mjs +22 -22
  9. package/src/frame.d.mts +22 -10
  10. package/src/frame.mjs +93 -45
  11. package/src/geometry-policy.d.mts +3 -0
  12. package/src/geometry-policy.mjs +17 -0
  13. package/src/graph-documents.d.mts +48 -0
  14. package/src/graph-documents.mjs +40 -0
  15. package/src/graphics-disposal.d.mts +2 -0
  16. package/src/graphics-disposal.mjs +14 -0
  17. package/src/index.d.mts +11 -57
  18. package/src/index.mjs +9 -205
  19. package/src/input-form.d.mts +5 -0
  20. package/src/input-form.mjs +103 -0
  21. package/src/inspection-overview.d.mts +27 -1
  22. package/src/inspection-overview.mjs +50 -33
  23. package/src/npc-motion.d.mts +1 -1
  24. package/src/npc-motion.mjs +5 -5
  25. package/src/object-presentation.d.mts +22 -26
  26. package/src/object-presentation.mjs +20 -17
  27. package/src/open-world-projection.d.mts +19 -10
  28. package/src/open-world-projection.mjs +31 -22
  29. package/src/reconcile.d.mts +1 -1
  30. package/src/reconcile.mjs +31 -19
  31. package/src/scene-items.d.mts +9 -0
  32. package/src/scene-items.mjs +29 -0
  33. package/src/scene-renderer.d.mts +37 -0
  34. package/src/scene-renderer.mjs +170 -0
  35. package/src/spatial-patterns.d.mts +22 -27
  36. package/src/spatial-patterns.mjs +63 -41
  37. package/src/spatial.d.mts +17 -6
  38. package/src/spatial.mjs +39 -25
  39. package/src/structure-graph.d.mts +5 -13
  40. package/src/structure-graph.mjs +167 -96
  41. package/src/symbol.d.mts +1 -1
  42. package/src/symbol.mjs +21 -16
  43. package/src/world-api.d.mts +56 -0
  44. package/src/world-api.mjs +8 -0
  45. package/src/world-camera.d.mts +14 -0
  46. package/src/world-camera.mjs +74 -0
  47. package/src/world-clock.d.mts +19 -0
  48. package/src/world-clock.mjs +30 -0
  49. package/src/world-commands.d.mts +39 -0
  50. package/src/world-commands.mjs +1 -0
  51. package/src/world-context.d.mts +132 -0
  52. package/src/world-context.mjs +1 -0
  53. package/src/world-controller.d.mts +43 -0
  54. package/src/world-controller.mjs +62 -0
  55. package/src/world-conversation.d.mts +3 -0
  56. package/src/world-conversation.mjs +46 -0
  57. package/src/world-events.d.mts +2 -0
  58. package/src/world-events.mjs +120 -0
  59. package/src/world-gpu.d.mts +3 -15
  60. package/src/world-gpu.mjs +159 -74
  61. package/src/world-input.d.mts +2 -2
  62. package/src/world-input.mjs +28 -18
  63. package/src/world-lifecycle.d.mts +12 -0
  64. package/src/world-lifecycle.mjs +172 -0
  65. package/src/world-model.d.mts +29 -40
  66. package/src/world-model.mjs +103 -74
  67. package/src/world-projection.d.mts +5 -0
  68. package/src/world-projection.mjs +169 -0
  69. package/src/world-render-loop.d.mts +9 -0
  70. package/src/world-render-loop.mjs +110 -0
  71. package/src/world-resources.d.mts +34 -0
  72. package/src/world-resources.mjs +8 -0
  73. package/src/world-runtime.d.mts +2 -36
  74. package/src/world-runtime.mjs +6 -311
  75. package/src/world-setup.d.mts +2 -0
  76. package/src/world-setup.mjs +179 -0
  77. package/src/world-surface.d.mts +2 -7
  78. package/src/world-surface.mjs +20 -12
  79. package/src/world-types.d.mts +74 -0
  80. package/src/world-types.mjs +1 -0
package/README.md CHANGED
@@ -37,3 +37,9 @@ pnpm test
37
37
  ## Release provenance
38
38
 
39
39
  Starting with 0.10.1, releases use GitHub Actions OIDC trusted publishing from `zixcel/projection-runtime` (`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.4",
3
+ "version": "0.10.5",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "description": "Trusted native DOM rendering of PP RenderPlans and Zixcel owner inputs",
@@ -38,10 +38,12 @@
38
38
  "dom",
39
39
  "data-presentation",
40
40
  "rendering",
41
- "javascript"
41
+ "typescript"
42
42
  ],
43
43
  "scripts": {
44
- "test": "node --test test/*.test.mjs",
45
- "test:browser": "node test/world-browser.mjs"
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
  }
@@ -1 +1,9 @@
1
- export function bookReader(document: any, item: any, revision: any, close: any, signal: any, previous: any): any;
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 {};
@@ -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
- const make=(tag,text,className)=>{const node=document.createElement(tag);if(text!==undefined)node.textContent=text;if(className)node.className=className;return node}
6
- const root=make('article',undefined,'entity-inspector game-book'),header=make('header'),title=make('h2',item.display?.title??'Display not configured')
7
- root.dataset.bookId=item.id;root.dataset.bookRevision=revision
8
- root.setAttribute('aria-label','Inspect '+title.textContent)
9
- const heading=make('div',undefined,'game-book-heading');heading.append(make('small','INSPECT'),title)
10
- if(item.display?.summary)heading.append(make('p',item.display.summary))
11
- header.append(heading,close)
12
- const object=make('div',undefined,'game-book-object');object.setAttribute('aria-hidden','true');object.append(symbol(document,'book'))
13
- const pageView=make('section',undefined,'game-book-page'),body=make('div',undefined,'game-book-body'),footer=make('nav'),counter=make('output')
14
- pageView.setAttribute('aria-live','polite');body.append(object,pageView)
15
- footer.setAttribute('aria-label','Book pages')
16
- const fields=item.display?.fields??[],pages=Math.max(1,Math.ceil(fields.length/2))
17
- let page=previous?.id===item.id&&previous.revision===revision&&Number.isInteger(previous.page)?Math.max(0,Math.min(pages-1,previous.page)):0
18
- 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}
19
- const back=button('Previous pages',-1),next=button('Next pages',1);footer.append(back,counter,next);root.append(header,body,footer)
20
- function draw(){
21
- root.dataset.bookPage=String(page);pageView.setAttribute('aria-label','Page '+(page+1)+' of '+pages)
22
- const list=make('dl',undefined,'object-fields')
23
- for(const field of fields.slice(page*2,page*2+2))list.append(make('dt',field.label),make('dd',field.value===null?'Not set':String(field.value)))
24
- pageView.replaceChildren(make('small','PAGE '+String(page+1).padStart(2,'0')+' / '+String(pages).padStart(2,'0')),list)
25
- if(!list.children.length)pageView.append(make('p','No display fields have been declared.'))
26
- const focused=document.activeElement,refocus=focused===back&&page===0?next:focused===next&&page===pages-1?back:null
27
- counter.textContent=(page+1)+' / '+pages;back.disabled=page===0;next.disabled=page===pages-1;refocus?.focus({preventScroll:true})
28
- }
29
- document.addEventListener('keydown',event=>{
30
- if(!['ArrowLeft','ArrowRight'].includes(event.key)||event.altKey||event.ctrlKey||event.metaKey)return
31
- const panel=root.closest('.frame-panel'),focused=document.activeElement
32
- if(!panel?.contains(focused)||focused?.matches('input,select,textarea,[contenteditable]'))return
33
- event.preventDefault();page=Math.max(0,Math.min(pages-1,page+(event.key==='ArrowRight'?1:-1)));draw()
34
- },{signal})
35
- draw();return root
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
+ }
@@ -1,8 +1,14 @@
1
- export function frameLayout(width: any, height: any, kind: any, anchor?: any): {
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: any;
5
- height: any;
10
+ width: number;
11
+ height: number;
6
12
  };
7
13
  panel: {
8
14
  x: number;
@@ -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
- 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}
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 function windowFrame(root: any, { stage, title, state, panels }: {
2
- stage: any;
3
- title: any;
4
- state: any;
5
- panels: any;
6
- }): {
7
- open: (id: any, objectId?: any) => void;
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: any): void;
10
- label(id: any, value: any): void;
11
- notify(count: any): void;
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
- const document=root.ownerDocument,abort=new AbortController(),buttons=new Map(),bodies=new Map()
6
- const make=(tag,attrs={})=>{const node=document.createElement(tag);for(const [k,v]of Object.entries(attrs))node.setAttribute(k,v);return node}
7
- const dock=make('nav',{class:'frame-dock','aria-label':'Window tools'}),bar=make('header',{class:'frame-bar'})
8
- 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'})
9
- closeButton.textContent='×';head.append(heading,closeButton);panel.append(head);panel.hidden=true
10
- let active=null,speaker=null,target=null,anchor=null
11
- const requestAnchor=()=>root.dispatchEvent(new document.defaultView.CustomEvent('world-anchor-request',{detail:{id:target}}))
12
- const position=()=>{
13
- if(!active){delete root.dataset.panelLayout;return}
14
- const layout=frameLayout(root.clientWidth,root.clientHeight,active,anchor),rect=layout.panel
15
- root.dataset.panelLayout=layout.mode
16
- panel.dataset.anchorId=target??''
17
- if(anchor)panel.dataset.anchorBounds=JSON.stringify(anchor);else delete panel.dataset.anchorBounds
18
- 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'})
19
- }
20
- const describe=()=>{const talking=Boolean(speaker&&active==='actions');panel.dataset.book=String(active==='details');panel.dataset.conversation=String(talking);if(talking)heading.textContent=speaker.title;position()}
21
- root.addEventListener('world-anchor',event=>{if(event.detail.id!==target)return;anchor=event.detail.bounds;position()},{signal:abort.signal})
22
- const resize=new root.ownerDocument.defaultView.ResizeObserver(()=>{requestAnchor();position()});resize.observe(root)
23
- function close(restoreFocus=true){const previous=active;active=null;target=null;anchor=null;requestAnchor();panel.hidden=true;delete root.dataset.panel;position();for(const b of buttons.values())b.setAttribute('aria-expanded','false');if(restoreFocus)(buttons.get(previous)?.isConnected?buttons.get(previous):dock.querySelector('button'))?.focus({preventScroll:true})}
24
- function open(id,objectId=null){if(!bodies.has(id))return;active=id;target=objectId??(id==='actions'?speaker?.id:null);anchor=null;panel.hidden=false;heading.textContent=panels.find(p=>p.id===id).label;panel.setAttribute('aria-label',heading.textContent)
25
- for(const [key,body]of bodies)body.hidden=key!==id
26
- for(const [key,button]of buttons)button.setAttribute('aria-expanded',String(key===id))
27
- root.dataset.panel=id
28
- root.dispatchEvent(new document.defaultView.CustomEvent('frame-panel-open'))
29
- describe()
30
- requestAnchor()
31
- panel.focus({preventScroll:true})
32
- }
33
- for(const item of panels){
34
- if(item.activate){
35
- const button=make('button',{type:'button','aria-label':item.label,title:item.label,'data-panel':item.id})
36
- button.append(symbol(document,item.symbol),Object.assign(document.createElement('span'),{textContent:item.label}))
37
- button.addEventListener('click',()=>{close();item.activate()},{signal:abort.signal});dock.append(button);continue
38
- }
39
- 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})
40
- button.append(symbol(document,item.symbol),Object.assign(document.createElement('span'),{textContent:item.label}));button.addEventListener('click',()=>active===item.id?close():open(item.id),{signal:abort.signal})
41
- const body=make('section',{id,class:'frame-panel-body'});body.hidden=true;body.append(...item.nodes);panel.append(body);if(item.dock!==false)dock.append(button);buttons.set(item.id,button);bodies.set(item.id,body)
42
- }
43
- closeButton.addEventListener('click',close,{signal:abort.signal})
44
- document.addEventListener('keydown',event=>{if(event.key==='Escape'&&active!==null){event.preventDefault();close()}},{signal:abort.signal})
45
- bar.append(title,state);root.replaceChildren(stage,bar,dock,panel)
46
- 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)return;item.label=value;button.setAttribute('aria-label',value);button.title=value;button.querySelector('span:last-child').textContent=value;if(active===id){heading.textContent=value;panel.setAttribute('aria-label',value)}},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()}}
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,3 @@
1
+ import type { ActorRange } from './world-types.mjs';
2
+ /** Validate the complete upload before allocating or updating GPU state. */
3
+ export declare function admitGeometry(data: Float32Array, ranges: readonly ActorRange[]): void;
@@ -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,2 @@
1
+ /** Drain each independently owned resource; preserve falsy failures too. */
2
+ export declare function disposeGraphics(operations: readonly (() => void)[]): void;
@@ -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
+ }