@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.
- package/README.md +6 -0
- package/package.json +6 -4
- 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/src/spatial-patterns.mjs
CHANGED
|
@@ -1,43 +1,65 @@
|
|
|
1
|
-
import {layoutWorld} from './world-model.mjs'
|
|
2
|
-
|
|
1
|
+
import { layoutWorld } from './world-model.mjs';
|
|
3
2
|
// View-local partitions, never entities, inferred meanings, relationships or commands.
|
|
4
|
-
export const spatialPatterns=Object.freeze([['garden','Garden'],['ground','Ground · people and places'],['constellation','Constellation · information']])
|
|
5
|
-
export const spatialGroupings=Object.freeze([['declared','Declared structure'],['meaning','Meaning reference'],['source','Source owner']])
|
|
6
|
-
const pageSize=24
|
|
7
|
-
export function spatialPattern(objects,relations=[],options={}){
|
|
8
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
3
|
+
export const spatialPatterns = Object.freeze([['garden', 'Garden'], ['ground', 'Ground · people and places'], ['constellation', 'Constellation · information']]);
|
|
4
|
+
export const spatialGroupings = Object.freeze([['declared', 'Declared structure'], ['meaning', 'Meaning reference'], ['source', 'Source owner']]);
|
|
5
|
+
const pageSize = 24;
|
|
6
|
+
export function spatialPattern(objects, relations = [], options = {}) {
|
|
7
|
+
layoutWorld(objects, relations); // Validate even when a collapsed view would hide bad input.
|
|
8
|
+
const pattern = options.pattern ?? 'ground', groupBy = options.groupBy ?? 'declared';
|
|
9
|
+
if (!spatialPatterns.some(([id]) => id === pattern))
|
|
10
|
+
throw Error('SpatialPatternInvalid');
|
|
11
|
+
if (!spatialGroupings.some(([id]) => id === groupBy))
|
|
12
|
+
throw Error('SpatialGroupingInvalid');
|
|
13
|
+
const state = { pattern, groupBy, group: null, page: 0 };
|
|
14
|
+
if (pattern === 'ground')
|
|
15
|
+
return { objects, relations, groups: [], state, total: objects.length, pages: 1, label: 'All objects' };
|
|
16
|
+
const actors = objects.filter(i => i.kind === 'character'), records = objects.filter(i => i.kind !== 'character'), partitions = new Map(), ids = new Set(objects.map(i => i.id));
|
|
17
|
+
for (const item of records) {
|
|
18
|
+
let key = null, title = 'Unclassified';
|
|
19
|
+
if (groupBy === 'declared' && item.group?.kind && item.group?.ref) {
|
|
20
|
+
key = [item.group.kind, item.group.ref];
|
|
21
|
+
title = item.groupLabel ?? item.group.ref;
|
|
22
|
+
}
|
|
23
|
+
if (groupBy === 'meaning' && typeof item.semanticRef === 'string') {
|
|
24
|
+
key = item.semanticRef;
|
|
25
|
+
title = 'Meaning · ' + key;
|
|
26
|
+
}
|
|
27
|
+
if (groupBy === 'source') {
|
|
28
|
+
const owners = [...new Set((item.sourceRefs ?? []).map(s => s.owner).filter(v => typeof v === 'string'))].sort();
|
|
29
|
+
if (owners.length) {
|
|
30
|
+
key = owners;
|
|
31
|
+
title = owners.join(' + ');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const identity = JSON.stringify([groupBy, key]);
|
|
35
|
+
let group = partitions.get(identity);
|
|
36
|
+
if (!group) {
|
|
37
|
+
let id = 'view:group:' + identity;
|
|
38
|
+
while (ids.has(id))
|
|
39
|
+
id = 'view:' + id;
|
|
40
|
+
ids.add(id);
|
|
41
|
+
group = { id, kind: 'group', title, memberIds: [], members: [] };
|
|
42
|
+
partitions.set(identity, group);
|
|
43
|
+
}
|
|
44
|
+
group.memberIds.push(item.id);
|
|
45
|
+
group.members.push(item);
|
|
46
|
+
}
|
|
47
|
+
const groups = [...partitions.values()], selected = groups.find(g => g.id === options.group);
|
|
48
|
+
state.group = selected?.id ?? null;
|
|
49
|
+
if (pattern === 'garden') {
|
|
50
|
+
if (actors.length > 32)
|
|
51
|
+
throw Error('WorldCapacityExceeded');
|
|
52
|
+
// Records remain in the exact focused projection. A shelf is a local index
|
|
53
|
+
// for that person's information, not an entity standing beside the person.
|
|
54
|
+
state.group = null;
|
|
55
|
+
state.page = 0;
|
|
56
|
+
const actorIds = new Set(actors.map(i => i.id));
|
|
57
|
+
return { objects: actors, relations: relations.filter(r => actorIds.has(r.from) && actorIds.has(r.to)), groups: groups.map(({ members, ...g }) => g), state, total: actors.length, pages: 1, label: 'People' };
|
|
58
|
+
}
|
|
59
|
+
const candidates = selected ? selected.members : groups.map(({ members, ...g }) => ({ ...g, summary: g.memberIds.length + ' records · Browse shelf' }));
|
|
60
|
+
// Actors share the same paging budget; no silent truncation or unbounded group expansion.
|
|
61
|
+
const all = [...actors, ...candidates], pages = Math.max(1, Math.ceil(all.length / pageSize));
|
|
62
|
+
state.page = state.group !== options.group && options.group ? 0 : Math.min(pages - 1, Math.max(0, typeof options.page === 'number' && Number.isSafeInteger(options.page) ? options.page : 0));
|
|
63
|
+
const visible = all.slice(state.page * pageSize, (state.page + 1) * pageSize), visibleIds = new Set(visible.filter(i => i.kind !== 'group').map(i => i.id));
|
|
64
|
+
return { objects: visible, relations: relations.filter(r => visibleIds.has(r.from) && visibleIds.has(r.to)), groups: groups.map(({ members, ...g }) => g), state, total: all.length, pages, label: selected?.title ?? 'All groups' };
|
|
43
65
|
}
|
package/src/spatial.d.mts
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import type { Camera } from '@zixcel/projection-client';
|
|
2
|
+
import type { WorldObject, WorldRelation } from './world-types.mjs';
|
|
3
|
+
import type { WorldSnapshot, WorldSurface, WorldOptions } from './world-api.mjs';
|
|
4
|
+
export interface SpatialPresentation {
|
|
5
|
+
items?: Map<string, Partial<WorldObject>>;
|
|
6
|
+
surface?: WorldSurface;
|
|
7
|
+
activity?: unknown;
|
|
8
|
+
target?: string | null;
|
|
9
|
+
pattern?: string;
|
|
10
|
+
context?: string | null;
|
|
11
|
+
}
|
|
12
|
+
export declare function spatialView(viewport: HTMLElement, board: HTMLElement, camera: Camera, relations: readonly WorldRelation[], signal: AbortSignal, pose: WorldSnapshot | null, conversation?: WorldOptions['conversation'], presentation?: SpatialPresentation): {
|
|
13
|
+
change: (action: string) => void;
|
|
14
|
+
apply: (value: Camera) => void;
|
|
15
|
+
snapshot: () => WorldSnapshot;
|
|
16
|
+
pause: (value: boolean) => void;
|
|
17
|
+
activity: (value: unknown) => void;
|
|
7
18
|
close: () => void;
|
|
8
19
|
};
|
package/src/spatial.mjs
CHANGED
|
@@ -1,27 +1,41 @@
|
|
|
1
|
-
import {createWorld} from './world-runtime.mjs'
|
|
1
|
+
import { createWorld } from './world-runtime.mjs';
|
|
2
2
|
// Installed RenderPlan/role controls remain the only operation authority.
|
|
3
|
-
export function spatialView(viewport,board,camera,relations,signal,pose,conversation=null,presentation={}){
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
3
|
+
export function spatialView(viewport, board, camera, relations, signal, pose, conversation = null, presentation = {}) {
|
|
4
|
+
if (signal.aborted)
|
|
5
|
+
throw Error('WorldViewCancelled');
|
|
6
|
+
const declaredRelations = [...relations];
|
|
7
|
+
const nodes = () => [...board.querySelectorAll('[data-item-id],[data-world-object]')];
|
|
8
|
+
const declarations = () => nodes().filter(node => !node.hidden).map(node => ({
|
|
9
|
+
id: node.dataset.itemId ?? node.dataset.worldObject ?? '',
|
|
10
|
+
...presentation.items?.get(node.dataset.itemId ?? ''),
|
|
11
|
+
kind: node.hasAttribute('data-world-object') ? 'character' : 'record',
|
|
12
|
+
title: node.querySelector('strong,[data-kind=select]')?.textContent ?? 'Information',
|
|
13
|
+
summary: node.querySelector('[data-item-summary]')?.textContent?.slice(0, 80) ?? '',
|
|
14
|
+
observationLabel: node.dataset.observationLabel ?? '',
|
|
15
|
+
disabled: node instanceof viewport.ownerDocument.defaultView.HTMLButtonElement && node.disabled === true, selected: node.dataset.selected === 'true'
|
|
16
|
+
}));
|
|
17
|
+
const options = { objects: declarations(), relations: declaredRelations, pose, conversation, activity: presentation.activity, target: presentation.target, pattern: presentation.pattern, context: presentation.context, onActivate: (id) => {
|
|
18
|
+
const node = nodes().find(node => (node.dataset.itemId ?? node.dataset.worldObject) === id);
|
|
19
|
+
if (!node || node.hidden || node instanceof viewport.ownerDocument.defaultView.HTMLButtonElement && node.disabled)
|
|
20
|
+
return;
|
|
21
|
+
(node.hasAttribute('data-world-object') ? node : node.querySelector('[data-kind=inspect]'))?.click();
|
|
22
|
+
} };
|
|
23
|
+
const lease = presentation.surface?.attach(viewport, options), world = lease?.world ?? createWorld(viewport, options);
|
|
24
|
+
board.hidden = true;
|
|
25
|
+
world.apply(camera);
|
|
26
|
+
const observer = new viewport.ownerDocument.defaultView.MutationObserver(() => world.update(declarations(), declaredRelations));
|
|
27
|
+
observer.observe(board, { subtree: true, childList: true, characterData: true, attributes: true, attributeFilter: ['hidden', 'data-selected', 'disabled'] });
|
|
28
|
+
let closed = false;
|
|
29
|
+
const close = () => { if (closed)
|
|
30
|
+
return; closed = true; observer.disconnect(); try {
|
|
31
|
+
if (lease)
|
|
32
|
+
lease.release();
|
|
33
|
+
else
|
|
34
|
+
world.close();
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
board.hidden = false;
|
|
38
|
+
} };
|
|
39
|
+
signal.addEventListener('abort', close, { once: true });
|
|
40
|
+
return { change: world.change, apply: world.apply, snapshot: world.snapshot, pause: world.pause, activity: world.activity, close };
|
|
27
41
|
}
|
|
@@ -1,16 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
maximum?: number;
|
|
1
|
+
import type { GraphView } from './graph-documents.mjs';
|
|
2
|
+
export { graphDocuments } from './graph-documents.mjs';
|
|
3
|
+
export declare function structureGraph(document: Document, host: HTMLElement, view: GraphView, onRole: (id: string) => void, onRefresh: () => unknown, { embedded }?: {
|
|
4
|
+
embedded?: boolean | undefined;
|
|
6
5
|
}): {
|
|
7
|
-
|
|
8
|
-
edges: any[];
|
|
9
|
-
truncated: boolean;
|
|
10
|
-
};
|
|
11
|
-
export function structureGraph(document: any, host: any, view: any, onRole: any, onRefresh: any, { embedded }?: {
|
|
12
|
-
embedded?: boolean;
|
|
13
|
-
}): {
|
|
14
|
-
update(next: any): void;
|
|
6
|
+
update(next: GraphView): void;
|
|
15
7
|
close(): void;
|
|
16
8
|
};
|
package/src/structure-graph.mjs
CHANGED
|
@@ -1,97 +1,168 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
1
|
+
import { element } from './dom-elements.mjs';
|
|
2
|
+
import { inspectionOverview } from './inspection-overview.mjs';
|
|
3
|
+
import { graphDocuments } from './graph-documents.mjs';
|
|
4
|
+
export { graphDocuments } from './graph-documents.mjs';
|
|
5
|
+
export function structureGraph(document, host, view, onRole, onRefresh, { embedded = false } = {}) {
|
|
6
|
+
const window = document.defaultView;
|
|
7
|
+
if (!window)
|
|
8
|
+
throw Error('GraphDocumentUnavailable');
|
|
9
|
+
const controller = new AbortController(), signal = controller.signal;
|
|
10
|
+
let drawing = new AbortController();
|
|
11
|
+
let component = '', query = '', depth = 1, zoom = 1, pan = { x: 0, y: 0 }, mode = 'overview';
|
|
12
|
+
let drag = null, selected = null;
|
|
13
|
+
const wrap = element(document, 'section', null, { 'aria-label': 'Semantic structure graph' });
|
|
14
|
+
Object.assign(wrap.style, { position: 'absolute', inset: '110px 12px 12px 110px', display: 'grid', gridTemplateRows: 'auto minmax(0,1fr) auto', background: 'Canvas', border: '1px solid GrayText', borderRadius: '12px', overflow: 'hidden' });
|
|
15
|
+
const media = window.matchMedia('(max-width:600px)'), resize = () => { wrap.style.inset = embedded ? 'auto' : media.matches ? '105px 8px 84px' : '110px 12px 12px 110px'; };
|
|
16
|
+
resize();
|
|
17
|
+
media.addEventListener('change', resize, { signal });
|
|
18
|
+
if (embedded)
|
|
19
|
+
Object.assign(wrap.style, { position: 'relative', height: '65dvh', minHeight: '300px', width: '100%' });
|
|
20
|
+
const toolbar = element(document, 'div'), footer = element(document, 'div'), detail = element(document, 'details'), summary = element(document, 'summary', 'Select a node for exact source details'), body = element(document, 'dl');
|
|
21
|
+
Object.assign(toolbar.style, { display: 'flex', gap: '6px', padding: '8px', flexWrap: 'wrap' });
|
|
22
|
+
Object.assign(footer.style, { padding: '8px', maxHeight: '35vh', overflow: 'auto' });
|
|
23
|
+
const control = (label, values, current, change) => { const select = element(document, 'select', null, { 'aria-label': label }); for (const [value, text] of values)
|
|
24
|
+
select.append(element(document, 'option', text, { value })); select.value = current; select.addEventListener('change', () => change(select.value), { signal }); toolbar.append(select); return select; };
|
|
25
|
+
const roleSelect = control('Role', view.roles.map(r => [r.id, r.id]), view.selected ?? '', onRole);
|
|
26
|
+
const tabs = element(document, 'div', null, { role: 'tablist', 'aria-label': 'Inspection view' }), tabButtons = [];
|
|
27
|
+
toolbar.append(tabs);
|
|
28
|
+
const graphControls = element(document, 'div', null, { class: 'inspection-graph-controls' });
|
|
29
|
+
Object.assign(graphControls.style, { display: 'flex', gap: '6px', flexWrap: 'wrap' });
|
|
30
|
+
Object.assign(tabs.style, { display: 'flex', gap: '6px' });
|
|
31
|
+
const componentSelect = control('Component', [['', 'All components'], ...view.documents.map(d => [d.id, d.label])], '', value => { component = value; selected = null; draw(); });
|
|
32
|
+
control('Structure depth', [1, 2, 3, 4, 5, 6].map(n => [String(n), 'Depth ' + n]), '1', value => { depth = Number(value); draw(); });
|
|
33
|
+
const search = element(document, 'input', null, { type: 'search', 'aria-label': 'Filter graph', placeholder: 'Filter nodes' });
|
|
34
|
+
search.style.width = '140px';
|
|
35
|
+
search.addEventListener('input', () => { query = search.value; draw(); }, { signal });
|
|
36
|
+
toolbar.append(search);
|
|
37
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'), group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
38
|
+
svg.setAttribute('aria-label', 'Document containment graph');
|
|
39
|
+
svg.setAttribute('role', 'group');
|
|
40
|
+
Object.assign(svg.style, { width: '100%', height: '100%', minHeight: '0', touchAction: 'none', background: 'color-mix(in srgb,Canvas 96%,#5368df)' });
|
|
41
|
+
svg.append(group);
|
|
42
|
+
const status = element(document, 'p', null, { role: 'status' });
|
|
43
|
+
status.style.margin = '0';
|
|
44
|
+
const button = (label, action) => { const b = element(document, 'button', label, { type: 'button' }); b.addEventListener('click', action, { signal }); toolbar.append(b); return b; };
|
|
45
|
+
const apply = () => group.setAttribute('transform', `translate(${pan.x} ${pan.y}) scale(${zoom})`);
|
|
46
|
+
button('−', () => { zoom = Math.max(.5, zoom / 1.2); apply(); });
|
|
47
|
+
button('+', () => { zoom = Math.min(8, zoom * 1.2); apply(); });
|
|
48
|
+
button('Fit', () => { zoom = 1; pan = { x: 0, y: 0 }; apply(); });
|
|
49
|
+
const refresh = button('Refresh', () => void onRefresh());
|
|
50
|
+
svg.addEventListener('wheel', e => { if (e.ctrlKey || e.metaKey)
|
|
51
|
+
return; e.preventDefault(); zoom = Math.max(.5, Math.min(8, zoom * (e.deltaY < 0 ? 1.1 : 1 / 1.1))); apply(); }, { signal, passive: false });
|
|
52
|
+
svg.addEventListener('pointerdown', e => { if (e.button || (e.target instanceof window.Element && e.target.closest('[data-graph-node]')))
|
|
53
|
+
return; drag = { id: e.pointerId, x: e.clientX, y: e.clientY, pan: { ...pan } }; svg.setPointerCapture(e.pointerId); }, { signal });
|
|
54
|
+
svg.addEventListener('pointermove', e => { if (!drag || drag.id !== e.pointerId)
|
|
55
|
+
return; const matrix = svg.getScreenCTM(); if (matrix) {
|
|
56
|
+
pan = { x: Math.max(-5000, Math.min(5000, drag.pan.x + (e.clientX - drag.x) / matrix.a)), y: Math.max(-5000, Math.min(5000, drag.pan.y + (e.clientY - drag.y) / matrix.d)) };
|
|
57
|
+
apply();
|
|
58
|
+
} }, { signal });
|
|
59
|
+
for (const type of ['pointerup', 'pointercancel', 'lostpointercapture'])
|
|
60
|
+
svg.addEventListener(type, () => drag = null, { signal });
|
|
61
|
+
function inspect(node) {
|
|
62
|
+
selected = node.id;
|
|
63
|
+
summary.textContent = node.document.label + ' · ' + (node.path || '/');
|
|
64
|
+
detail.open = true;
|
|
65
|
+
body.replaceChildren();
|
|
66
|
+
const source = node.document.source ?? {}, fields = [['path', node.path || '/'], ['source', source.method ?? node.document.id], ['semantic revision', source.semanticRevision ?? ''], ['role', source.roleRef?.id ?? ''], ['role revision', source.roleRef?.revision ?? ''], ['role digest', source.roleRef?.digest_sha256 ?? ''], ['memory revision', source.memoryRevision ?? ''], ...Object.entries((node.value && typeof node.value === 'object') ? node.value : { value: node.value }).slice(0, 64).map(([k, v]) => ['field: ' + k, v])];
|
|
67
|
+
for (const [key, value] of fields)
|
|
68
|
+
body.append(element(document, 'dt', key), element(document, 'dd', value !== null && typeof value === 'object' ? (Array.isArray(value) ? 'Array' : 'Object') + ' (' + Object.keys(value).length + ')' : String(value)));
|
|
69
|
+
highlight();
|
|
70
|
+
}
|
|
71
|
+
function highlight() { for (const n of group.querySelectorAll('[data-graph-node]'))
|
|
72
|
+
n.querySelector('rect')?.setAttribute('stroke', n.dataset.graphNode === selected ? '#d97706' : '#5368df'); for (const line of group.querySelectorAll('line'))
|
|
73
|
+
line.setAttribute('stroke', line.dataset.from === selected || line.dataset.to === selected ? '#d97706' : '#a5adc6'); }
|
|
74
|
+
function draw() {
|
|
75
|
+
drawing.abort();
|
|
76
|
+
drawing = new AbortController();
|
|
77
|
+
selected = null;
|
|
78
|
+
detail.open = false;
|
|
79
|
+
body.replaceChildren();
|
|
80
|
+
summary.textContent = 'Select a node for exact source details';
|
|
81
|
+
const graph = graphDocuments(view.documents, { component, query, depth }), levels = new Map(), positions = new Map();
|
|
82
|
+
for (const node of graph.nodes) {
|
|
83
|
+
const list = levels.get(node.level) ?? [];
|
|
84
|
+
list.push(node);
|
|
85
|
+
levels.set(node.level, list);
|
|
86
|
+
}
|
|
87
|
+
const height = Math.max(240, ...[...levels.values()].map(ns => ns.length * 64 + 30)), width = (Math.max(0, ...levels.keys()) + 1) * 220 + 20;
|
|
88
|
+
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
|
89
|
+
group.replaceChildren();
|
|
90
|
+
const make = (tag, attrs) => { const n = document.createElementNS('http://www.w3.org/2000/svg', tag); for (const [k, v] of Object.entries(attrs))
|
|
91
|
+
n.setAttribute(k, String(v)); return n; };
|
|
92
|
+
for (const [level, nodes] of levels)
|
|
93
|
+
nodes.forEach((n, i) => positions.set(n.id, { x: level * 220 + 10, y: (i + .5) * height / nodes.length }));
|
|
94
|
+
for (const edge of graph.edges) {
|
|
95
|
+
const a = positions.get(edge.from), b = positions.get(edge.to);
|
|
96
|
+
group.append(make('line', { x1: a.x + 170, y1: a.y, x2: b.x, y2: b.y, 'data-from': edge.from, 'data-to': edge.to, stroke: '#a5adc6', 'stroke-width': 2 }));
|
|
97
|
+
}
|
|
98
|
+
for (const node of graph.nodes) {
|
|
99
|
+
const p = positions.get(node.id), g = make('g', { transform: `translate(${p.x} ${p.y - 20})`, 'data-graph-node': node.id, role: 'button', tabindex: 0, 'aria-label': node.document.label + ' ' + (node.path || '/') });
|
|
100
|
+
g.append(make('rect', { width: 170, height: 40, rx: 8, fill: 'Canvas', stroke: '#5368df', 'stroke-width': 2 }));
|
|
101
|
+
const title = make('title', {});
|
|
102
|
+
title.textContent = node.document.label + ' ' + (node.path || '/');
|
|
103
|
+
const text = make('text', { x: 8, y: 25, fill: 'CanvasText', 'font-size': 13 });
|
|
104
|
+
text.textContent = node.label.length > 21 ? node.label.slice(0, 20) + '…' : node.label;
|
|
105
|
+
g.append(title, text);
|
|
106
|
+
g.addEventListener('click', () => inspect(node), { signal: drawing.signal });
|
|
107
|
+
g.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') {
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
inspect(node);
|
|
110
|
+
} }, { signal: drawing.signal });
|
|
111
|
+
group.append(g);
|
|
112
|
+
}
|
|
113
|
+
status.textContent = graph.nodes.length + ' nodes · ' + graph.edges.length + ' containment links' + (graph.truncated ? ' · Depth or node limit: increase depth or select a component.' : '') + (view.documents.some(d => d.source?.truncated) ? ' · Source incomplete.' : '');
|
|
114
|
+
if (!view.documents.length)
|
|
115
|
+
status.textContent = 'No adopted semantic structure available. Inspect owner readiness in Notifications.';
|
|
116
|
+
apply();
|
|
117
|
+
highlight();
|
|
118
|
+
}
|
|
119
|
+
// One inspector, two views. Switching changes presentation only, not reads,
|
|
120
|
+
// selected role, semantic state or the world/camera behind the inspector.
|
|
121
|
+
for (const child of [...toolbar.children])
|
|
122
|
+
if (child !== roleSelect && child !== tabs && child !== refresh)
|
|
123
|
+
graphControls.append(child);
|
|
124
|
+
toolbar.append(graphControls);
|
|
125
|
+
const stage = element(document, 'div', null, { class: 'inspection-stage' }), overview = element(document, 'div', null, { class: 'inspection-overview', role: 'tabpanel', 'aria-label': 'Responsibility overview' }), graphPanel = element(document, 'div', null, { class: 'inspection-graph-panel', role: 'tabpanel', 'aria-label': 'Structure view' });
|
|
126
|
+
Object.assign(stage.style, { minHeight: '0', overflow: 'hidden' });
|
|
127
|
+
Object.assign(overview.style, { height: '100%', overflow: 'auto', padding: '1rem', overflowWrap: 'anywhere' });
|
|
128
|
+
graphPanel.style.height = '100%';
|
|
129
|
+
graphPanel.append(svg);
|
|
130
|
+
stage.append(overview, graphPanel);
|
|
131
|
+
function display() {
|
|
132
|
+
overview.hidden = mode !== 'overview';
|
|
133
|
+
graphPanel.hidden = mode !== 'structure';
|
|
134
|
+
graphControls.hidden = mode !== 'structure';
|
|
135
|
+
footer.hidden = mode !== 'structure';
|
|
136
|
+
for (const b of tabButtons) {
|
|
137
|
+
const active = b.dataset.view === mode;
|
|
138
|
+
b.setAttribute('aria-selected', String(active));
|
|
139
|
+
b.tabIndex = active ? 0 : -1;
|
|
140
|
+
b.style.borderColor = active ? 'Highlight' : 'GrayText';
|
|
141
|
+
b.style.fontWeight = active ? 'bold' : 'normal';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
for (const [value, label] of [['overview', 'Overview'], ['structure', 'Structure']]) {
|
|
145
|
+
const tab = element(document, 'button', label, { type: 'button', role: 'tab', 'data-view': value });
|
|
146
|
+
tab.style.minHeight = '44px';
|
|
147
|
+
tab.addEventListener('click', () => { mode = value; display(); }, { signal });
|
|
148
|
+
tab.addEventListener('keydown', event => { if (['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) {
|
|
149
|
+
event.preventDefault();
|
|
150
|
+
mode = event.key === 'Home' ? 'overview' : event.key === 'End' ? 'structure' : mode === 'overview' ? 'structure' : 'overview';
|
|
151
|
+
display();
|
|
152
|
+
tabButtons.find(b => b.dataset.view === mode)?.focus();
|
|
153
|
+
} }, { signal });
|
|
154
|
+
tabs.append(tab);
|
|
155
|
+
tabButtons.push(tab);
|
|
156
|
+
}
|
|
157
|
+
let overviewEvents = new AbortController();
|
|
158
|
+
function summaryView() { overviewEvents.abort(); overviewEvents = new AbortController(); inspectionOverview(document, overview, view.overview, id => { component = id; componentSelect.value = id; mode = 'structure'; draw(); display(); }, overviewEvents.signal); }
|
|
159
|
+
detail.append(summary, body);
|
|
160
|
+
footer.append(status, detail);
|
|
161
|
+
wrap.append(toolbar, stage, footer);
|
|
162
|
+
host.replaceChildren(wrap);
|
|
163
|
+
draw();
|
|
164
|
+
summaryView();
|
|
165
|
+
display();
|
|
166
|
+
return { update(next) { view = next; roleSelect.replaceChildren(...view.roles.map(r => element(document, 'option', r.id, { value: r.id }))); roleSelect.value = view.selected ?? ''; componentSelect.replaceChildren(element(document, 'option', 'All components', { value: '' }), ...view.documents.map(d => element(document, 'option', d.label, { value: d.id }))); if (!view.documents.some(d => d.id === component))
|
|
167
|
+
component = ''; componentSelect.value = component; draw(); summaryView(); display(); }, close() { overviewEvents.abort(); drawing.abort(); controller.abort(); wrap.remove(); } };
|
|
97
168
|
}
|
package/src/symbol.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** @param {Document} document @param {string} [name] */
|
|
2
|
-
export function symbol(document: Document, name?: string): SVGSVGElement;
|
|
2
|
+
export declare function symbol(document: Document, name?: string): SVGSVGElement;
|
package/src/symbol.mjs
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
// Installed vector primitives; labels/routes come from the site, never SVG/HTML input.
|
|
2
2
|
/** @param {Document} document @param {string} [name] */
|
|
3
|
-
export function symbol(document,name='layers'){
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
3
|
+
export function symbol(document, name = 'layers') {
|
|
4
|
+
const paths = { home: ['m3 11 9-8 9 8', 'M5 9v12h14V9', 'M9 21v-8h6v8'], plus: ['M12 4v16M4 12h16'], settings: ['M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8Z', 'M12 2v3m0 14v3M2 12h3m14 0h3M5 5l2 2m10 10 2 2M5 19l2-2M17 7l2-2'], clock: ['M22 12a10 10 0 1 1-20 0 10 10 0 0 1 20 0Z', 'M12 6v6l4 2'], layers: ['M12 3 2 8l10 5 10-5-10-5Z', 'm2 12 10 5 10-5', 'm2 17 10 5 10-5'],
|
|
5
|
+
menu: ['M4 6h16M4 12h16M4 18h16'],
|
|
6
|
+
person: ['M16 7a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z', 'M4 22v-3a8 8 0 0 1 16 0v3'],
|
|
7
|
+
nodes: ['M9 5H5v4h4V5Z', 'M19 15h-4v4h4v-4Z', 'M9 15H5v4h4v-4Z', 'M7 9v6m2-8h8v8M9 17h6'],
|
|
8
|
+
package: ['m3 7 9-5 9 5v10l-9 5-9-5V7Z', 'm3 7 9 5 9-5M12 12v10M7 4l10 6'],
|
|
9
|
+
book: ['M4 3h14a2 2 0 0 1 2 2v15H6a2 2 0 0 1-2-2V3Z', 'M4 18a2 2 0 0 1 2-2h14', 'M8 7h8m-8 4h6'],
|
|
10
|
+
chip: ['M6 6h12v12H6V6Z', 'M9 9h6v6H9V9Z', 'M9 2v4m6-4v4M9 18v4m6-4v4M2 9h4m-4 6h4m12-6h4m-4 6h4'],
|
|
11
|
+
info: ['M22 12a10 10 0 1 1-20 0 10 10 0 0 1 20 0Z', 'M12 11v6m0-10v1'],
|
|
12
|
+
search: ['M17 10a7 7 0 1 1-14 0 7 7 0 0 1 14 0Z', 'm15 15 6 6'],
|
|
13
|
+
play: ['m8 4 12 8-12 8V4Z'],
|
|
14
|
+
activity: ['M2 12h4l3-8 6 16 3-8h4'], check: ['m5 12 4 4L19 6'], warning: ['m12 3 10 18H2L12 3Z', 'M12 9v5m0 3v1'] };
|
|
15
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
16
|
+
for (const [key, value] of Object.entries({ viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', 'stroke-width': '1.6', 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'aria-hidden': 'true', focusable: 'false', class: 'ui-symbol' }))
|
|
17
|
+
svg.setAttribute(key, value);
|
|
18
|
+
for (const d of paths[name] ?? paths.layers) {
|
|
19
|
+
const p = document.createElementNS(svg.namespaceURI, 'path');
|
|
20
|
+
p.setAttribute('d', d);
|
|
21
|
+
svg.append(p);
|
|
22
|
+
}
|
|
23
|
+
return svg;
|
|
19
24
|
}
|