@zixcel/dom-renderer 0.10.3 → 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 +19 -11
  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
@@ -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
+ }
package/src/index.d.mts CHANGED
@@ -1,57 +1,11 @@
1
- /** @template {keyof HTMLElementTagNameMap} K
2
- * @param {Document} document
3
- * @param {K} tag
4
- * @param {unknown} [text]
5
- * @param {Record<string,unknown>} [attrs]
6
- * @returns {HTMLElementTagNameMap[K]} */
7
- export function element<K extends keyof HTMLElementTagNameMap>(document: Document, tag: K, text?: unknown, attrs?: Record<string, unknown>): HTMLElementTagNameMap[K];
8
- /** @param {Document} document
9
- * @param {unknown} value
10
- * @param {{nodes:number,bytes:number}} [budget]
11
- * @returns {HTMLElement} */
12
- export function valueView(document: Document, value: unknown, budget?: {
13
- nodes: number;
14
- bytes: number;
15
- }): HTMLElement;
16
- /** @param {Document} document
17
- * @param {import('@zixcel/interaction/input').InputDeclaration} declaration
18
- * @param {(values:Record<string,string|boolean|number>)=>unknown|Promise<unknown>} onSubmit */
19
- export function inputForm(document: Document, declaration: import("@zixcel/interaction/input").InputDeclaration, onSubmit: (values: Record<string, string | boolean | number>) => unknown | Promise<unknown>): {
20
- form: HTMLFormElement;
21
- close(): void;
22
- };
23
- export { createWorldSurface } from "./world-surface.mjs";
24
- export { symbol } from "./symbol.mjs";
25
- export { windowFrame } from "./frame.mjs";
26
- export { worldControlGuide } from "./world-input.mjs";
27
- export { spatialView } from "./spatial.mjs";
28
- export { structureGraph } from "./structure-graph.mjs";
29
- export const renderBounds: Readonly<{
30
- nodes: 8192;
31
- depth: 20;
32
- textBytes: 524288;
33
- }>;
34
- export class SceneRenderer {
35
- /** @param {HTMLElement} root
36
- * @param {(kind:string,value:string|import('@zixcel/projection-client').ActionHandle|undefined)=>void} onEvent
37
- * @param {{tools?:HTMLElement,details?:HTMLElement,actions?:HTMLElement}} [slots]
38
- * @param {ReturnType<typeof createWorldSurface>|null} [surface] */
39
- constructor(root: HTMLElement, onEvent: (kind: string, value: string | import("@zixcel/projection-client").ActionHandle | undefined) => void, slots?: {
40
- tools?: HTMLElement;
41
- details?: HTMLElement;
42
- actions?: HTMLElement;
43
- }, surface?: ReturnType<typeof createWorldSurface> | null);
44
- /** @param {import('@zixcel/projection-client').RenderPlan} plan */
45
- render(plan: import("@zixcel/projection-client").RenderPlan): void;
46
- /** @param {import('@zixcel/projection-client').Camera} value */
47
- camera(value: import("@zixcel/projection-client").Camera): void;
48
- speaker(value: any): void;
49
- garden(value: any): void;
50
- activity(value: any): void;
51
- /** @param {'in'|'out'|'up'|'down'|'left'|'right'|'reset'} action */
52
- changeCamera(action: "in" | "out" | "up" | "down" | "left" | "right" | "reset"): void;
53
- close(): void;
54
- dispose(): void;
55
- #private;
56
- }
57
- import { createWorldSurface } from './world-surface.mjs';
1
+ export { element, valueView, renderBounds } from './dom-elements.mjs';
2
+ export { inputForm } from './input-form.mjs';
3
+ export { SceneRenderer } from './scene-renderer.mjs';
4
+ export type { SceneActor, SceneGarden, SceneSlots, SceneEvents } from './scene-renderer.mjs';
5
+ export { createWorldSurface } from './world-surface.mjs';
6
+ export type { World, WorldSurface, WorldOptions, WorldSnapshot, ActivityPresentation } from './world-api.mjs';
7
+ export { symbol } from './symbol.mjs';
8
+ export { windowFrame } from './frame.mjs';
9
+ export { worldControlGuide } from './world-input.mjs';
10
+ export { spatialView } from './spatial.mjs';
11
+ export { structureGraph } from './structure-graph.mjs';
package/src/index.mjs CHANGED
@@ -1,205 +1,9 @@
1
- // Trusted installed DOM primitives. All external values are text, never HTML.
2
- // Hatter downstream 2026: one bounded DOM tree for spatial and structured Scenes.
3
- import {validateInputDeclaration,validateInputValues} from '@zixcel/interaction/input'
4
- import {spatialView} from './spatial.mjs'
5
- import {symbol} from './symbol.mjs'
6
- import {reconcileChildren} from './reconcile.mjs'
7
- import {createWorldSurface} from './world-surface.mjs'
8
- import {bookReader} from './book-reader.mjs'
9
- export {createWorldSurface} from './world-surface.mjs'
10
- export {symbol} from './symbol.mjs'
11
- export {windowFrame} from './frame.mjs'
12
- export {worldControlGuide} from './world-input.mjs'
13
- export {spatialView} from './spatial.mjs'
14
- export {structureGraph} from './structure-graph.mjs'
15
- export const renderBounds=Object.freeze({nodes:8192,depth:20,textBytes:524288})
16
- const fail=code=>{throw Object.assign(Error(code),{code})}
17
- /** @template {keyof HTMLElementTagNameMap} K
18
- * @param {Document} document
19
- * @param {K} tag
20
- * @param {unknown} [text]
21
- * @param {Record<string,unknown>} [attrs]
22
- * @returns {HTMLElementTagNameMap[K]} */
23
- export function element(document,tag,text=null,attrs={}){
24
- const node=document.createElement(tag);if(text!==null)node.textContent=String(text)
25
- for(const [key,value]of Object.entries(attrs))if(value!==null)node.setAttribute(key,String(value))
26
- return node
27
- }
28
- /** @param {Document} document
29
- * @param {unknown} value
30
- * @param {{nodes:number,bytes:number}} [budget]
31
- * @returns {HTMLElement} */
32
- export function valueView(document,value,budget={nodes:0,bytes:0}){
33
- const walk=(value,depth)=>{
34
- if(++budget.nodes>renderBounds.nodes||depth>renderBounds.depth)fail('RenderLimitExceeded')
35
- if(value===null||typeof value!=='object'){
36
- const text=JSON.stringify(value);if(typeof text!=='string')fail('InvalidRenderValue')
37
- budget.bytes+=new TextEncoder().encode(text).length;if(budget.bytes>renderBounds.textBytes)fail('RenderLimitExceeded')
38
- return element(document,'span',text,{'data-projection-scalar':text})
39
- }
40
- if(Object.keys(value).length>4096)fail('RenderLimitExceeded')
41
- if(!Object.keys(value).length)return element(document,'span',Array.isArray(value)?'[]':'{}',{'data-empty-value':Array.isArray(value)?'array':'object'})
42
- const list=element(document,Array.isArray(value)?'ol':'dl')
43
- for(const [key,child]of Object.entries(value)){
44
- if(Array.isArray(value)){const item=element(document,'li');item.append(walk(child,depth+1));list.append(item)}
45
- else{const name=element(document,'dt',key),item=element(document,'dd');item.append(walk(child,depth+1));list.append(name,item)}
46
- }
47
- return list
48
- }
49
- return walk(value,0)
50
- }
51
- export class SceneRenderer{
52
- #root;#events;#abort=null;#actionHandles=new Map();#spatial=null;#slots;#pose=null;#poseKey=null;#speaker=null;#surface;#ownsSurface;#garden=null
53
- /** @param {HTMLElement} root
54
- * @param {(kind:string,value:string|import('@zixcel/projection-client').ActionHandle|undefined)=>void} onEvent
55
- * @param {{tools?:HTMLElement,details?:HTMLElement,actions?:HTMLElement}} [slots]
56
- * @param {ReturnType<typeof createWorldSurface>|null} [surface] */
57
- constructor(root,onEvent,slots={},surface=null){this.#root=root;this.#events=onEvent;this.#slots=slots;this.#surface=surface??createWorldSurface();this.#ownsSurface=!surface}
58
- /** @param {import('@zixcel/projection-client').RenderPlan} plan */
59
- render(plan){
60
- const root=this.#root,document=root.ownerDocument,focused=document.activeElement?.dataset.control
61
- const previous=root.querySelector('[data-render-key]'),oldViewport=root.querySelector('.scene-viewport')
62
- const scroll=previous?.dataset.renderKey===plan.projectionKey&&previous.dataset.viewMode===plan.viewMode&&oldViewport
63
- ?{left:oldViewport.scrollLeft,top:oldViewport.scrollTop}:null
64
- if(this.#poseKey===plan.projectionKey)this.#pose=this.#spatial?.snapshot()??this.#pose
65
- else this.#pose=null
66
- this.#poseKey=plan.projectionKey
67
- this.#spatial?.close();this.#spatial=null;this.#abort?.abort();this.#abort=new AbortController();this.#actionHandles.clear()
68
- if(!['spatial','structured'].includes(plan.viewMode)||!Array.isArray(plan.relations)||plan.relations.length>512)fail('InvalidRenderPlan')
69
- const section=element(document,'section',null,{'data-render-revision':plan.projectionRevision,'data-render-key':plan.projectionKey,'data-view-mode':plan.viewMode,'aria-label':'Scene content',tabindex:'-1',class:'semantic-scene'})
70
- const button=(label,kind,id,attrs={})=>element(document,'button',label,{type:'button','data-kind':kind,'data-control':JSON.stringify([kind,id]),'data-target':id,...attrs})
71
- const views=element(document,'div',null,{role:'group','aria-label':'Scene view',class:'scene-view-controls'})
72
- for(const [mode,label]of [['spatial','Spatial view'],['structured','List view']])views.append(button(label,'view',mode,{'aria-pressed':plan.viewMode===mode}))
73
- const tools=this.#slots.tools??section;tools.replaceChildren();tools.append(views)
74
- const viewport=element(document,'div',null,{class:'scene-viewport',tabindex:'0','aria-label':'Explore information in this space'}),board=element(document,'div',null,{class:'spatial-board'})
75
- for(const person of this.#garden?.actors??(this.#speaker?[this.#speaker]:[])){const actor=button('',this.#garden?'actor':'conversation',person.id,{'data-world-object':person.id,'data-selected':person.id===this.#speaker?.id,'data-observation-label':person.preparation?.label??''});actor.append(element(document,'strong',person.title));actor.disabled=person.status!==undefined&&person.status!=='Available';board.append(actor)}
76
- const camera=element(document,'div',null,{role:'group','aria-label':'View position',class:'scene-camera'})
77
- for(const [id,label,icon]of [['left','Move left','←'],['right','Move right','→'],['in','Zoom in','+'],['out','Zoom out','−'],['reset','View all','⛶']])camera.append(button(icon,'camera',id,{'aria-label':label,title:label}))
78
- if(plan.viewMode==='spatial')tools.append(camera)
79
- viewport.append(board);(plan.viewMode==='structured'?tools:section).append(viewport)
80
- if(plan.empty)section.append(element(document,'p','No projected information is available.',{role:'status'}))
81
- if(plan.truncated)section.append(element(document,'p','The source reports incomplete information.',{role:'status'}))
82
- const groups=[...plan.regions,...(plan.unplaced.length?[{id:'unplaced',role:'Context',items:plan.unplaced}]:[])]
83
- if(groups.length>17||plan.actions.length>64)fail('RenderLimitExceeded')
84
- for(const group of groups){
85
- const region=element(document,'section',null,{'data-region':group.id}),list=element(document,'ul',null,{class:'scene-items'})
86
- region.append(element(document,'h2',group.role),list)
87
- for(const item of group.items){
88
- const row=element(document,'li',null,{'data-item-id':item.id,'data-selected':item.selected,'data-related':item.related,'data-focused':item.focused}),toolbar=element(document,'div',null,{class:'item-tools'})
89
- const itemButton=(label,kind,attrs={})=>button(label,kind,item.id,{'data-control':JSON.stringify([group.id,kind,item.id]),...attrs})
90
- toolbar.append(itemButton(item.display?.title??item.content.reason??'Display not configured','select',{'aria-pressed':item.selected}),itemButton('View details','inspect',{'aria-expanded':plan.inspector?.id===item.id,'aria-haspopup':'dialog'}))
91
- if(item.content.reason)toolbar.append(itemButton('Review','resolve'))
92
- const solid=element(document,'span',null,{class:'object-solid','aria-hidden':'true'})
93
- solid.append(symbol(document,item.display?.symbol??'nodes'));row.append(solid,toolbar)
94
- if(item.display?.summary)row.append(element(document,'p',item.display.summary,{'data-item-summary':item.id}))
95
- const fields=element(document,'dl',null,{class:'object-fields'})
96
- for(const field of item.display?.fields??[])fields.append(element(document,'dt',field.label),element(document,'dd',field.value===null?'Not set':String(field.value),{'data-projection-scalar':JSON.stringify(field.value)}))
97
- row.append(fields)
98
- if(!item.display)row.append(element(document,'p',item.content.reason?'This information is not yet resolved. Review the available choices or the missing requirements.':'Display information is not configured. Inspect this object in System.'))
99
- list.append(row)
100
- }
101
- board.append(region)
102
- }
103
- const time=element(document,'section',null,{'aria-label':'Scene time',class:'scene-time'})
104
- time.append(element(document,'h2',plan.temporal?.label??'Time'))
105
- if(plan.temporal?.items.length){
106
- time.append(button('All times','time','',{'aria-pressed':plan.temporal.cursor===null}))
107
- for(const item of plan.temporal.items){const control=button(item.at+' · '+item.itemId,'time',item.itemId,{'aria-pressed':plan.temporal.cursor===item.itemId});time.append(control)}
108
- }else time.append(element(document,'p','No event time has been declared by this source.',{role:'status'}))
109
- tools.append(time)
110
- if(plan.relations.length){
111
- const relations=element(document,'details',null,{'aria-label':'Relationships'}),list=element(document,'ul',null,{class:'scene-relations'})
112
- relations.append(element(document,'summary','Relationships ('+plan.relations.length+')'),list)
113
- for(const relation of plan.relations){
114
- const row=element(document,'li',null,{'data-relation-id':relation.id,'data-active':relation.active})
115
- row.append(button(relation.from,'select',relation.from,{'data-control':JSON.stringify(['relation',relation.id,'from']),'aria-label':'Select related '+relation.from}),element(document,'span','→ '+relation.relationRef+' →'),button(relation.to,'select',relation.to,{'data-control':JSON.stringify(['relation',relation.id,'to']),'aria-label':'Select related '+relation.to}))
116
- list.append(row)
117
- }
118
- tools.append(relations)
119
- }
120
- const actions=this.#slots.actions??tools;if(this.#slots.actions)actions.replaceChildren()
121
- // Unresolved owner observations are conversation material too, including
122
- // when no executable choice exists. This does not manufacture an action.
123
- const pending=groups.flatMap(group=>group.items).filter(item=>item.content.reason)
124
- if(pending.length){
125
- const questions=element(document,'section',null,{'aria-label':'Information needing clarification'})
126
- questions.append(element(document,'h3','Why this information needs review'))
127
- for(const item of pending){
128
- const row=element(document,'div',null,{'data-unresolved-ref':item.id})
129
- const explanation=this.#slots.describeUnresolved?.(item.content.reason)??{label:String(item.content.reason),detail:'The source owner reported this unresolved reason.'}
130
- row.append(button(item.display?.title??explanation.label,'resolve',item.id),element(document,'small',explanation.detail))
131
- questions.append(row)
132
- }
133
- actions.append(questions)
134
- }
135
- for(const action of plan.actions){this.#actionHandles.set(action.handle.actionId,action.handle);actions.append(button(action.descriptor.label,'action',action.handle.actionId))}
136
- const lastBook=(this.#slots.details??root).querySelector('[data-book-id]'),bookPosition=lastBook?{id:lastBook.dataset.bookId,revision:lastBook.dataset.bookRevision,page:Number(lastBook.dataset.bookPage)}:null
137
- this.#slots.details?.replaceChildren()
138
- if(plan.inspector){
139
- const chosen=groups.flatMap(r=>r.items).find(i=>i.id===plan.inspector.id)
140
- if(chosen)(this.#slots.details??section).append(bookReader(document,chosen,plan.projectionRevision,button('Close book','dismiss',plan.inspector.id),this.#abort.signal,bookPosition))
141
- }
142
- reconcileChildren(root,[section])
143
- const activeViewport=plan.viewMode==='spatial'?root.querySelector('.scene-viewport'):viewport,activeBoard=activeViewport.querySelector('.spatial-board')
144
- if(plan.viewMode==='spatial'||this.#garden)this.#spatial=spatialView(this.#garden?.host??activeViewport,activeBoard,plan.camera,plan.relations,this.#abort.signal,this.#pose,null,{
145
- surface:this.#surface,activity:this.#garden?.activity,target:this.#garden?'garden':plan.projectionKey,context:this.#garden?plan.projectionKey:null,pattern:this.#garden?'garden':'constellation',items:new Map(groups.flatMap(region=>region.items.map(item=>[item.id,{
146
- anchor:this.#speaker?.id,group:item.content.group,semanticRef:item.content.semanticRef,sourceRefs:item.content.sourceRefs,scalars:(item.display?.fields??[]).map(f=>f.value),unresolved:Boolean(item.content.reason),changeKey:JSON.stringify(item.display?.fields??[])
147
- }])))
148
- })
149
- if(this.#garden&&plan.viewMode==='structured')activeBoard.hidden=false
150
- for(const host of new Set([root,this.#slots.tools,this.#slots.details,this.#slots.actions].filter(Boolean)))host.addEventListener('click',event=>{
151
- const target=event.target.closest('button[data-kind]');if(!target||!host.contains(target))return
152
- if(target.dataset.kind==='camera'){this.#spatial?.change(target.dataset.target);return}
153
- this.#events(target.dataset.kind,target.dataset.kind==='action'?this.#actionHandles.get(target.dataset.target):target.dataset.target)
154
- },{signal:this.#abort.signal})
155
- if(focused)[...root.querySelectorAll('[data-control]')].find(n=>n.dataset.control===focused)?.focus({preventScroll:true})
156
- if(scroll){viewport.scrollLeft=scroll.left;viewport.scrollTop=scroll.top}
157
- }
158
- /** @param {import('@zixcel/projection-client').Camera} value */
159
- camera(value){this.#spatial?.apply(value)}
160
- speaker(value){this.#speaker=value}
161
- garden(value){this.#garden=value}
162
- activity(value){this.#spatial?.activity(value)}
163
- /** @param {'in'|'out'|'up'|'down'|'left'|'right'|'reset'} action */
164
- changeCamera(action){this.#spatial?.change(action)}
165
- close(){this.#spatial?.close();this.#spatial=null;this.#abort?.abort();this.#actionHandles.clear();this.#root.replaceChildren();for(const slot of new Set([this.#slots.tools,this.#slots.details,this.#slots.actions].filter(Boolean)))slot.replaceChildren();if(this.#ownsSurface){this.#surface.close();this.#surface=createWorldSurface()}}
166
- dispose(){this.close();if(this.#ownsSurface)this.#surface.close()}
167
- }
168
- /** @param {Document} document
169
- * @param {import('@zixcel/interaction/input').InputDeclaration} declaration
170
- * @param {(values:Record<string,string|boolean|number>)=>unknown|Promise<unknown>} onSubmit */
171
- export function inputForm(document,declaration,onSubmit){
172
- if(validateInputDeclaration(declaration).length)fail('InvalidInputDeclaration')
173
- const form=element(document,'form'),fields=new Map(),abort=new AbortController(),prefix='input-'+crypto.randomUUID()
174
- const fieldIds=[...new Set([...declaration.action.input_schema.required,...Object.keys(declaration.fields)])]
175
- for(const id of fieldIds){
176
- const field=declaration.fields[id]
177
- const schema=declaration.action.input_schema.fields[id],label=element(document,'label'),name=element(document,'span',field.label)
178
- let input
179
- if(field.choices){input=element(document,'select');input.append(element(document,'option','Choose…',{value:''}));for(const choice of field.choices)input.append(element(document,'option',choice.label,{value:choice.value}))}
180
- else if(schema.type==='string'&&!field.sensitive&&schema.max_length>255){input=element(document,'textarea');input.rows=4;input.maxLength=schema.max_length}
181
- else{input=element(document,'input');input.type=schema.type==='boolean'?'checkbox':['integer','number'].includes(schema.type)?'number':field.sensitive?'password':'text'
182
- if(schema.type==='string')input.maxLength=schema.max_length??4096
183
- if(schema.type==='integer')input.step='1'
184
- if(schema.type==='number')input.step='any'
185
- if(schema.minimum!==undefined)input.min=String(schema.minimum)
186
- if(schema.maximum!==undefined)input.max=String(schema.maximum)
187
- }
188
- input.id=prefix+'-'+fields.size;input.name=id;input.autocomplete='off';label.htmlFor=input.id;label.append(name);form.append(label,input);fields.set(id,{input,schema})
189
- }
190
- const submit=element(document,'button','Submit',{type:'submit'}),status=element(document,'div','',{role:'status'});form.append(submit,status)
191
- form.addEventListener('submit',async event=>{
192
- event.preventDefault();if(submit.disabled)return
193
- const values={};for(const [id,{input,schema}]of fields){
194
- // An absent optional value is not zero, null, false or an empty string.
195
- if(input.value===''&&schema.type!=='boolean'&&!declaration.action.input_schema.required.includes(id))continue
196
- values[id]=schema.type==='boolean'?input.checked:['number','integer'].includes(schema.type)?(input.value===''?null:Number(input.value)):input.value
197
- }
198
- if(validateInputValues(declaration,values).length){status.textContent='Check the declared input constraints.';return}
199
- submit.disabled=true;form.setAttribute('aria-busy','true')
200
- try{await onSubmit(values);if(!abort.signal.aborted)status.textContent='Submitted.'}
201
- catch(error){if(!abort.signal.aborted){status.setAttribute('role','alert');status.replaceChildren(valueView(document,{code:error.code??'OperationUnavailable',...(error.ownerFailure?{ownerFailure:error.ownerFailure}:{})}))}}
202
- finally{for(const {input}of fields.values())if(input.type==='password')input.value='';submit.disabled=false;form.setAttribute('aria-busy','false')}
203
- },{signal:abort.signal})
204
- return {form,close(){abort.abort();for(const {input}of fields.values())input.value='';form.remove()}}
205
- }
1
+ export { element, valueView, renderBounds } from './dom-elements.mjs';
2
+ export { inputForm } from './input-form.mjs';
3
+ export { SceneRenderer } from './scene-renderer.mjs';
4
+ export { createWorldSurface } from './world-surface.mjs';
5
+ export { symbol } from './symbol.mjs';
6
+ export { windowFrame } from './frame.mjs';
7
+ export { worldControlGuide } from './world-input.mjs';
8
+ export { spatialView } from './spatial.mjs';
9
+ export { structureGraph } from './structure-graph.mjs';
@@ -0,0 +1,5 @@
1
+ import type { InputDeclaration } from '@zixcel/interaction/input';
2
+ export declare function inputForm(document: Document, declaration: InputDeclaration, onSubmit: (values: Record<string, string | boolean | number>) => unknown | Promise<unknown>): {
3
+ form: HTMLFormElement;
4
+ close(): void;
5
+ };
@@ -0,0 +1,103 @@
1
+ import { validateInputDeclaration, validateInputValues } from '@zixcel/interaction/input';
2
+ import { element, valueView, fail } from './dom-elements.mjs';
3
+ import { record } from './world-api.mjs';
4
+ function scalar(value) {
5
+ if (value.type === 'boolean')
6
+ return { type: 'boolean' };
7
+ if (value.type === 'string' || value.type === 'number' || value.type === 'integer')
8
+ return value;
9
+ return fail('InvalidInputDeclaration');
10
+ }
11
+ export function inputForm(document, declaration, onSubmit) {
12
+ if (validateInputDeclaration(declaration).length || declaration.action.input_schema.type !== 'object')
13
+ fail('InvalidInputDeclaration');
14
+ const contract = declaration.action.input_schema, required = contract.required ?? [];
15
+ const form = element(document, 'form'), fields = new Map(), abort = new AbortController(), prefix = 'input-' + crypto.randomUUID();
16
+ const fieldIds = [...new Set([...required, ...Object.keys(declaration.fields)])];
17
+ for (const id of fieldIds) {
18
+ const field = declaration.fields[id], schema = scalar(contract.fields[id]), label = element(document, 'label'), name = element(document, 'span', field.label);
19
+ let input;
20
+ if (field.choices) {
21
+ input = element(document, 'select');
22
+ input.append(element(document, 'option', 'Choose…', { value: '' }));
23
+ for (const choice of field.choices)
24
+ input.append(element(document, 'option', choice.label, { value: choice.value }));
25
+ }
26
+ else if (schema.type === 'string' && (schema.max_length ?? 0) > 255 && !field.sensitive) {
27
+ input = element(document, 'textarea');
28
+ input.rows = 4;
29
+ input.maxLength = schema.max_length ?? 4096;
30
+ }
31
+ else {
32
+ input = element(document, 'input');
33
+ input.type = schema.type === 'boolean' ? 'checkbox' : schema.type === 'integer' || schema.type === 'number' ? 'number' : field.sensitive ? 'password' : 'text';
34
+ if (schema.type === 'string')
35
+ input.maxLength = schema.max_length ?? 4096;
36
+ if (schema.type === 'integer')
37
+ input.step = '1';
38
+ if (schema.type === 'number')
39
+ input.step = 'any';
40
+ if (schema.type === 'integer' || schema.type === 'number') {
41
+ input.min = String(schema.minimum);
42
+ input.max = String(schema.maximum);
43
+ }
44
+ }
45
+ input.id = prefix + '-' + fields.size;
46
+ input.name = id;
47
+ input.autocomplete = 'off';
48
+ label.htmlFor = input.id;
49
+ label.append(name);
50
+ form.append(label, input);
51
+ fields.set(id, { input, schema });
52
+ }
53
+ const submit = element(document, 'button', 'Submit', { type: 'submit' }), status = element(document, 'div', '', { role: 'status' });
54
+ form.append(submit, status);
55
+ form.addEventListener('submit', async (event) => {
56
+ event.preventDefault();
57
+ if (submit.disabled)
58
+ return;
59
+ const values = {};
60
+ for (const [id, { input, schema }] of fields) {
61
+ if (input.value === '' && schema.type !== 'boolean' && !required.includes(id))
62
+ continue;
63
+ if (schema.type === 'boolean')
64
+ values[id] = 'checked' in input ? input.checked : false;
65
+ else if (schema.type === 'integer' || schema.type === 'number') {
66
+ if (input.value === '') {
67
+ status.textContent = 'Check the declared input constraints.';
68
+ return;
69
+ }
70
+ values[id] = Number(input.value);
71
+ }
72
+ else
73
+ values[id] = input.value;
74
+ }
75
+ if (validateInputValues(declaration, values).length) {
76
+ status.textContent = 'Check the declared input constraints.';
77
+ return;
78
+ }
79
+ submit.disabled = true;
80
+ form.setAttribute('aria-busy', 'true');
81
+ try {
82
+ await onSubmit(values);
83
+ if (!abort.signal.aborted)
84
+ status.textContent = 'Submitted.';
85
+ }
86
+ catch (error) {
87
+ const value = record(error);
88
+ if (!abort.signal.aborted) {
89
+ status.setAttribute('role', 'alert');
90
+ status.replaceChildren(valueView(document, { code: value.code ?? 'OperationUnavailable', ...(value.ownerFailure ? { ownerFailure: value.ownerFailure } : {}) }));
91
+ }
92
+ }
93
+ finally {
94
+ for (const { input } of fields.values())
95
+ if (input.type === 'password')
96
+ input.value = '';
97
+ submit.disabled = false;
98
+ form.setAttribute('aria-busy', 'false');
99
+ }
100
+ }, { signal: abort.signal });
101
+ return { form, close() { abort.abort(); for (const { input } of fields.values())
102
+ input.value = ''; form.remove(); } };
103
+ }
@@ -1 +1,27 @@
1
- export function inspectionOverview(document: any, host: any, view: any, onSource: any, signal: any): void;
1
+ export interface InspectionOverview {
2
+ readonly title: string;
3
+ readonly note: string;
4
+ readonly modulesAvailable: boolean;
5
+ readonly modulesPartial?: boolean;
6
+ readonly groups: readonly {
7
+ readonly id: string;
8
+ readonly title: string;
9
+ readonly note: string;
10
+ readonly state: string;
11
+ readonly measures: readonly {
12
+ readonly unit: string;
13
+ readonly value: number | null;
14
+ readonly partial?: boolean;
15
+ readonly documentId?: string;
16
+ }[];
17
+ readonly values?: readonly string[];
18
+ }[];
19
+ readonly modules: readonly {
20
+ readonly id: string;
21
+ readonly version: string;
22
+ readonly terms: number;
23
+ readonly expressions: number;
24
+ readonly dependencies: readonly string[];
25
+ }[];
26
+ }
27
+ export declare function inspectionOverview(document: Document, host: HTMLElement, view: InspectionOverview | null, onSource: (id: string) => void, signal: AbortSignal): void;
@@ -1,35 +1,52 @@
1
1
  // Owner-projected facts only. This renderer does not classify semantic modules.
2
- import {element} from './index.mjs'
3
- export function inspectionOverview(document,host,view,onSource,signal){
4
- host.replaceChildren()
5
- if(!view){host.append(element(document,'p','Responsibility overview is unavailable.'));return}
6
- host.append(element(document,'h2',view.title),element(document,'p',view.note,{class:'inspection-note'}))
7
- const groups=element(document,'ul',null,{class:'scene-items'})
8
- groups.style.gridTemplateColumns='repeat(auto-fit,minmax(min(250px,100%),1fr))'
9
- for(const group of view.groups){
10
- const section=element(document,'li',null,{'aria-label':group.title,'data-responsibility':group.id})
11
- const info=element(document,'details'),description=element(document,'summary','About these observations')
12
- info.append(description,element(document,'p',group.note))
13
- section.append(element(document,'h3',group.title),element(document,'p',group.state,{class:'inspection-state'}))
14
- for(const measure of group.measures){
15
- const row=element(document,'div',null,{class:'item-tools','data-measure':measure.unit})
16
- row.append(element(document,'strong',measure.value===null?'Unavailable':String(measure.value)+(measure.partial?' +':'')),element(document,'span',measure.unit))
17
- if(measure.documentId){const button=element(document,'button','Inspect',{type:'button','aria-label':'Inspect '+measure.unit});button.addEventListener('click',()=>onSource(measure.documentId),{signal});row.append(button)}
18
- section.append(row)
19
- }
20
- if(group.values?.length)section.append(element(document,'p',group.values.join(' · ')))
21
- section.append(info);groups.append(section)
22
- }
23
- host.append(groups,element(document,'h3','Adopted definition modules'))
24
- if(!view.modulesAvailable){host.append(element(document,'p','Module data unavailable.'));return}
25
- if(!view.modules.length){host.append(element(document,'p','No definition modules in the observed role.'));return}
26
- const list=element(document,'ul',null,{class:'inspection-modules'})
27
- for(const module of view.modules){
28
- const item=element(document,'li'),button=element(document,'button',module.id,{type:'button'})
29
- button.addEventListener('click',()=>onSource('definitions'),{signal})
30
- item.append(button,element(document,'p',`${module.version} · ${module.terms} terms · ${module.expressions} expressions`),element(document,'p',module.dependencies.length?'Imports: '+module.dependencies.join(', '):'No declared imports'))
31
- list.append(item)
32
- }
33
- host.append(list)
34
- if(view.modulesPartial)host.append(element(document,'p','Module inventory is incomplete.'))
2
+ import { element } from './dom-elements.mjs';
3
+ export function inspectionOverview(document, host, view, onSource, signal) {
4
+ host.replaceChildren();
5
+ if (!view) {
6
+ host.append(element(document, 'p', 'Responsibility overview is unavailable.'));
7
+ return;
8
+ }
9
+ host.append(element(document, 'h2', view.title), element(document, 'p', view.note, { class: 'inspection-note' }));
10
+ const groups = element(document, 'ul', null, { class: 'scene-items' });
11
+ groups.style.gridTemplateColumns = 'repeat(auto-fit,minmax(min(250px,100%),1fr))';
12
+ for (const group of view.groups) {
13
+ const section = element(document, 'li', null, { 'aria-label': group.title, 'data-responsibility': group.id });
14
+ const info = element(document, 'details'), description = element(document, 'summary', 'About these observations');
15
+ info.append(description, element(document, 'p', group.note));
16
+ section.append(element(document, 'h3', group.title), element(document, 'p', group.state, { class: 'inspection-state' }));
17
+ for (const measure of group.measures) {
18
+ const row = element(document, 'div', null, { class: 'item-tools', 'data-measure': measure.unit });
19
+ row.append(element(document, 'strong', measure.value === null ? 'Unavailable' : String(measure.value) + (measure.partial ? ' +' : '')), element(document, 'span', measure.unit));
20
+ if (measure.documentId) {
21
+ const documentId = measure.documentId;
22
+ const button = element(document, 'button', 'Inspect', { type: 'button', 'aria-label': 'Inspect ' + measure.unit });
23
+ button.addEventListener('click', () => onSource(documentId), { signal });
24
+ row.append(button);
25
+ }
26
+ section.append(row);
27
+ }
28
+ if (group.values?.length)
29
+ section.append(element(document, 'p', group.values.join(' · ')));
30
+ section.append(info);
31
+ groups.append(section);
32
+ }
33
+ host.append(groups, element(document, 'h3', 'Adopted definition modules'));
34
+ if (!view.modulesAvailable) {
35
+ host.append(element(document, 'p', 'Module data unavailable.'));
36
+ return;
37
+ }
38
+ if (!view.modules.length) {
39
+ host.append(element(document, 'p', 'No definition modules in the observed role.'));
40
+ return;
41
+ }
42
+ const list = element(document, 'ul', null, { class: 'inspection-modules' });
43
+ for (const module of view.modules) {
44
+ const item = element(document, 'li'), button = element(document, 'button', module.id, { type: 'button' });
45
+ button.addEventListener('click', () => onSource('definitions'), { signal });
46
+ item.append(button, element(document, 'p', `${module.version} · ${module.terms} terms · ${module.expressions} expressions`), element(document, 'p', module.dependencies.length ? 'Imports: ' + module.dependencies.join(', ') : 'No declared imports'));
47
+ list.append(item);
48
+ }
49
+ host.append(list);
50
+ if (view.modulesPartial)
51
+ host.append(element(document, 'p', 'Module inventory is incomplete.'));
35
52
  }
@@ -1,4 +1,4 @@
1
- export function npcMotion(id: any, milliseconds: any, state?: string): {
1
+ export declare function npcMotion(id: string, milliseconds: number, state?: string): {
2
2
  x: number;
3
3
  y: number;
4
4
  swing: number;
@@ -1,7 +1,7 @@
1
1
  // Ambient presentation only. This never changes semantic position or Work.
2
- export function npcMotion(id,milliseconds,state='Idle'){
3
- const seed=[...id].reduce((n,c)=>(Math.imul(n,31)+c.charCodeAt(0))|0,0)>>>0
4
- const active=['Runnable','Running','AwaitingResult'].includes(state)
5
- const phase=milliseconds/1000*(active?2.2:1.1)*(0.85+(seed%7)/20)+(seed%97)*.31
6
- return {x:Math.sin(phase)*(active?.2:.08),y:Math.max(0,Math.sin(phase*1.7))*(active?.12:.04),swing:Math.sin(phase*1.4)*(active?.16:.08)}
2
+ export function npcMotion(id, milliseconds, state = 'Idle') {
3
+ const seed = [...id].reduce((n, c) => (Math.imul(n, 31) + c.charCodeAt(0)) | 0, 0) >>> 0;
4
+ const active = ['Runnable', 'Running', 'AwaitingResult'].includes(state);
5
+ const phase = milliseconds / 1000 * (active ? 2.2 : 1.1) * (0.85 + (seed % 7) / 20) + (seed % 97) * .31;
6
+ return { x: Math.sin(phase) * (active ? .2 : .08), y: Math.max(0, Math.sin(phase * 1.7)) * (active ? .12 : .04), swing: Math.sin(phase * 1.4) * (active ? .16 : .08) };
7
7
  }
@@ -1,13 +1,20 @@
1
- export function objectPresentation(item: any): {
2
- form: any;
3
- dataType?: undefined;
4
- needsReview?: undefined;
5
- } | {
6
- form: string;
7
- dataType: any;
8
- needsReview: boolean;
9
- };
10
- export const spacingPresets: Readonly<{
1
+ export type ObjectForm = 'person' | 'organization' | 'system' | 'shelf' | 'book';
2
+ export type ObjectTint = 'dark' | 'accent' | 'light' | 'face' | 'warning';
3
+ export type ObjectPart = readonly [number, number, number, number, number, number, ObjectTint];
4
+ export interface PresentationInput {
5
+ id?: string;
6
+ title?: string;
7
+ form?: string;
8
+ kind?: string;
9
+ scalars?: readonly unknown[];
10
+ unresolved?: unknown;
11
+ }
12
+ export interface ObjectPresentation {
13
+ form: ObjectForm;
14
+ dataType?: 'text' | 'number' | 'boolean' | 'record';
15
+ needsReview?: boolean;
16
+ }
17
+ export declare const spacingPresets: Readonly<{
11
18
  compact: {
12
19
  person: number;
13
20
  group: number;
@@ -24,14 +31,14 @@ export const spacingPresets: Readonly<{
24
31
  member: number;
25
32
  };
26
33
  }>;
27
- export const objectForms: Readonly<{
34
+ export declare const objectForms: Readonly<{
28
35
  person: "Person";
29
36
  organization: "Organization";
30
37
  system: "System";
31
38
  shelf: "Bookshelf";
32
39
  book: "Book";
33
40
  }>;
34
- export const libraryLayout: Readonly<{
41
+ export declare const libraryLayout: Readonly<{
35
42
  columns: 6;
36
43
  rows: 4;
37
44
  slotWidth: 0.46;
@@ -39,17 +46,6 @@ export const libraryLayout: Readonly<{
39
46
  baseY: 0.32;
40
47
  frontZ: 0.32;
41
48
  }>;
42
- export const objectBounds: Readonly<{
43
- person: number[];
44
- organization: number[];
45
- system: number[];
46
- shelf: number[];
47
- book: number[];
48
- }>;
49
- export const objectTemplates: Readonly<{
50
- person: (string | number)[][];
51
- organization: (string | number)[][];
52
- system: (string | number)[][];
53
- shelf: (string | number)[][];
54
- book: (string | number)[][];
55
- }>;
49
+ export declare const objectBounds: Readonly<Record<ObjectForm, readonly [number, number, number]>>;
50
+ export declare const objectTemplates: Readonly<Record<ObjectForm, readonly ObjectPart[]>>;
51
+ export declare function objectPresentation(item: PresentationInput): ObjectPresentation;