@issuegraph/viewer 0.1.0

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 (67) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +156 -0
  3. package/dist/clusters.d.ts +34 -0
  4. package/dist/clusters.d.ts.map +1 -0
  5. package/dist/clusters.js +195 -0
  6. package/dist/clusters.js.map +1 -0
  7. package/dist/document.d.ts +190 -0
  8. package/dist/document.d.ts.map +1 -0
  9. package/dist/document.js +308 -0
  10. package/dist/document.js.map +1 -0
  11. package/dist/element.d.ts +95 -0
  12. package/dist/element.d.ts.map +1 -0
  13. package/dist/element.js +174 -0
  14. package/dist/element.js.map +1 -0
  15. package/dist/index.d.ts +40 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +33 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/layout.d.ts +122 -0
  20. package/dist/layout.d.ts.map +1 -0
  21. package/dist/layout.js +297 -0
  22. package/dist/layout.js.map +1 -0
  23. package/dist/mount.d.ts +64 -0
  24. package/dist/mount.d.ts.map +1 -0
  25. package/dist/mount.js +402 -0
  26. package/dist/mount.js.map +1 -0
  27. package/dist/navigation.d.ts +56 -0
  28. package/dist/navigation.d.ts.map +1 -0
  29. package/dist/navigation.js +121 -0
  30. package/dist/navigation.js.map +1 -0
  31. package/dist/parts.d.ts +89 -0
  32. package/dist/parts.d.ts.map +1 -0
  33. package/dist/parts.js +214 -0
  34. package/dist/parts.js.map +1 -0
  35. package/dist/projections/graph.d.ts +30 -0
  36. package/dist/projections/graph.d.ts.map +1 -0
  37. package/dist/projections/graph.js +644 -0
  38. package/dist/projections/graph.js.map +1 -0
  39. package/dist/projections/linear.d.ts +35 -0
  40. package/dist/projections/linear.d.ts.map +1 -0
  41. package/dist/projections/linear.js +157 -0
  42. package/dist/projections/linear.js.map +1 -0
  43. package/dist/projections/tree.d.ts +17 -0
  44. package/dist/projections/tree.d.ts.map +1 -0
  45. package/dist/projections/tree.js +209 -0
  46. package/dist/projections/tree.js.map +1 -0
  47. package/dist/render.d.ts +35 -0
  48. package/dist/render.d.ts.map +1 -0
  49. package/dist/render.js +45 -0
  50. package/dist/render.js.map +1 -0
  51. package/dist/scene.d.ts +94 -0
  52. package/dist/scene.d.ts.map +1 -0
  53. package/dist/scene.js +48 -0
  54. package/dist/scene.js.map +1 -0
  55. package/dist/styles.d.ts +17 -0
  56. package/dist/styles.d.ts.map +1 -0
  57. package/dist/styles.js +389 -0
  58. package/dist/styles.js.map +1 -0
  59. package/dist/theme.d.ts +71 -0
  60. package/dist/theme.d.ts.map +1 -0
  61. package/dist/theme.js +169 -0
  62. package/dist/theme.js.map +1 -0
  63. package/dist/vocabulary.d.ts +107 -0
  64. package/dist/vocabulary.d.ts.map +1 -0
  65. package/dist/vocabulary.js +98 -0
  66. package/dist/vocabulary.js.map +1 -0
  67. package/package.json +56 -0
package/dist/mount.js ADDED
@@ -0,0 +1,402 @@
1
+ /**
2
+ * The DOM shell — the only module in this package that touches nodes.
3
+ *
4
+ * It is deliberately thin. Everything that could be wrong about WHAT is drawn
5
+ * lives in the projections, everything that could be wrong about WHERE
6
+ * navigation goes lives in `navigate`, and what is left here is three
7
+ * listeners, an index from key to element, and a teardown. A shell that grew
8
+ * decisions would be a second implementation of the parts above it, testable
9
+ * only through a DOM.
10
+ *
11
+ * IT REACHES NO GLOBAL. The document comes from the container the caller
12
+ * passed, which is what keeps this module importable on a runtime that has no
13
+ * DOM — the property the workspace smoke test measures against the published
14
+ * entry.
15
+ */
16
+ import { normalizeDocument } from "./document.js";
17
+ import { materialize } from "./element.js";
18
+ import { initialNavigationState, navigate, reconcile, } from "./navigation.js";
19
+ import { sceneFor } from "./render.js";
20
+ import { GROUP_ATTRIBUTE, KEY_ATTRIBUTE } from "./scene.js";
21
+ import { defaultTheme } from "./theme.js";
22
+ /**
23
+ * Elements that own their own keyboard activation.
24
+ *
25
+ * Every row deliberately carries a deep-link chip, and `keydown` BUBBLES — so
26
+ * the viewer's own Enter/Space handling ran while focus was on the anchor,
27
+ * called `preventDefault()`, and suppressed the link. A projection that exposes
28
+ * a link a keyboard cannot follow has not exposed it.
29
+ */
30
+ const SELF_ACTIVATING_TAGS = new Set([
31
+ 'A',
32
+ 'BUTTON',
33
+ 'INPUT',
34
+ 'SELECT',
35
+ 'TEXTAREA',
36
+ ]);
37
+ function ownsItsOwnActivation(target, container) {
38
+ let cursor = target ?? null;
39
+ while (cursor !== null && cursor !== container) {
40
+ const tag = cursor.tagName;
41
+ if (tag !== undefined && SELF_ACTIVATING_TAGS.has(tag.toUpperCase()))
42
+ return true;
43
+ cursor = cursor.parentElement;
44
+ }
45
+ return false;
46
+ }
47
+ /**
48
+ * Walk up from an event target to the nearest element carrying a key.
49
+ *
50
+ * BOUNDED AT THE CONTAINER. An unbounded walk keeps climbing into the host's
51
+ * own page, so an ancestor that happens to carry `data-ig-key` — a host
52
+ * rendering two viewers, or reusing the attribute — would answer for a click
53
+ * that landed on nothing of ours. Stopping at the mount point makes the answer
54
+ * a fact about this viewer.
55
+ *
56
+ * DECORATION ANSWERS TOO, through {@link GROUP_ATTRIBUTE}. The enclosure and
57
+ * its connector are deliberately outside the focus index — one element per key
58
+ * or `focus()` lands on the wrong one — but they are still visible marks a
59
+ * reader can click, and the design states the connector IS a click target.
60
+ * Reading both attributes here separates "what may take focus" from "what may
61
+ * be pointed at" rather than making one attribute answer both.
62
+ */
63
+ function keyAt(target, container,
64
+ // WHICH IDENTITY IS BEING ASKED FOR. Pointing and focusing are different
65
+ // questions — see the note above — so the caller names the attributes rather
66
+ // than a second near-identical walk being written for the narrower one, which
67
+ // is how two walkers drift apart.
68
+ attributes = [KEY_ATTRIBUTE, GROUP_ATTRIBUTE]) {
69
+ let cursor = target ?? null;
70
+ while (cursor !== null) {
71
+ for (const attribute of attributes) {
72
+ const key = cursor.getAttribute(attribute);
73
+ if (key !== null && key !== '')
74
+ return key;
75
+ }
76
+ if (cursor === container)
77
+ return null;
78
+ cursor = cursor.parentElement;
79
+ }
80
+ return null;
81
+ }
82
+ /**
83
+ * Mount a viewer into a container.
84
+ *
85
+ * Returns a handle rather than nothing, because a host that cannot tear this
86
+ * down leaks a listener on every re-render — the shell's one genuine hazard.
87
+ */
88
+ export function mountViewer(container, input, options = {}) {
89
+ const doc = container.ownerDocument;
90
+ let currentOptions = options;
91
+ let projection = options.projection ?? 'linear';
92
+ let normalized = normalizeDocument(input).document;
93
+ // SEEDED FROM THE OPTIONS, not from nothing. `MountOptions` carries
94
+ // `selected` and `focused` because it extends the render options, so a caller
95
+ // restoring a view — after a projection toggle, a route change, a reload —
96
+ // passes them here. Starting empty silently discarded them and dropped the
97
+ // caller straight back to the top of the order.
98
+ let state = {
99
+ focused: options.focused ?? initialNavigationState.focused,
100
+ selected: options.selected ?? initialNavigationState.selected,
101
+ };
102
+ let root = null;
103
+ let keyed = new Map();
104
+ let hovered = null;
105
+ let destroyed = false;
106
+ // Set by the first `draw()` below, which runs before any listener can fire.
107
+ let currentScene = null;
108
+ const theme = options.theme ?? defaultTheme;
109
+ // ONE PLACE THAT CLEARS A HOVER, because forgetting a site is exactly how this
110
+ // kept going wrong: `pointerleave` had it, then a redraw needed it, and now
111
+ // teardown does too. Three call sites open-coding two lines is a fourth site
112
+ // waiting to omit them, so the rule lives here and the sites ask for it.
113
+ const clearHover = () => {
114
+ if (hovered === null)
115
+ return;
116
+ hovered = null;
117
+ currentOptions.onHover?.(null);
118
+ };
119
+ // `announceSelection` is false ONLY for the redraw `emitSelect` runs, which
120
+ // reports the settled selection itself — see there. Every other caller wants
121
+ // the notice, because a reconciliation that moves the selection has moved it
122
+ // out from under whoever set it.
123
+ const draw = (announceSelection = true) => {
124
+ // A DOCUMENT UPDATE CAN REMOVE THE SELECTED ISSUE, and `reconcile` carries
125
+ // the selection through whole — deliberately, because a PROJECTION switch
126
+ // changes representation rather than subject and dropping it there would
127
+ // lose the thing the reader is looking at. A document REPLACEMENT is the
128
+ // other case: the subject can genuinely be gone, and the handle went on
129
+ // reporting a key this document does not carry, against `update`'s own
130
+ // documented promise that a still-present selection survives.
131
+ // THE TEST IS DOCUMENT MEMBERSHIP, WHICH IS WHY IT COSTS THE PROJECTION
132
+ // SWITCH NOTHING. `setProjection` redraws against the SAME `normalized`, so
133
+ // `byKey` is unchanged and a still-present selection passes untouched —
134
+ // preserved across a projection-only change and reconciled on replacement,
135
+ // from one rule rather than from two entry points that can disagree.
136
+ // THE SAME SHAPE AND THE SAME TEST AS THE HOVER RECONCILIATION at the foot
137
+ // of this function, sitting apart from it only because the selection has to
138
+ // be settled BEFORE the scene is built: `sceneFor` is handed
139
+ // `state.selected`, so clearing afterwards would draw one frame marking a
140
+ // selection that no longer exists.
141
+ // WHAT THE SELECTION WAS BEFORE THIS DRAW TOUCHED IT. Two things can move
142
+ // it — the document-membership drop just below, and `reconcile`'s
143
+ // canonicalization to the station this projection draws it under — and the
144
+ // host has to hear about BOTH. Keying the notice on a single `dropped` flag
145
+ // covered only the first, so `setProjection` renamed the selection and told
146
+ // nobody: the handle read the station while the host still held the member.
147
+ const selectedBefore = state.selected;
148
+ if (state.selected !== null && !normalized.byKey.has(state.selected)) {
149
+ state = { ...state, selected: null };
150
+ }
151
+ if (root !== null)
152
+ container.removeChild(root);
153
+ const scene = sceneFor(normalized, projection, {
154
+ ...currentOptions,
155
+ theme: currentOptions.theme ?? theme,
156
+ selected: state.selected,
157
+ focused: state.focused,
158
+ });
159
+ state = reconcile(scene, state);
160
+ keyed = new Map();
161
+ // EVERY POINTER IDENTITY THIS SCENE ACTUALLY DREW, which is a WIDER set than
162
+ // `keyed` and a NARROWER one than the document. `keyed` holds focus
163
+ // identities only; decoration announces itself through `GROUP_ATTRIBUTE` and
164
+ // deliberately never enters the focus index, so an enclosure or a connector
165
+ // is absent from `keyed` on every redraw. The hover reconciliation below
166
+ // needs both, and needs them from the SCENE rather than from `byKey`.
167
+ const drawnHoverable = new Set();
168
+ // WHICH KEYS ARE INDEXED BY SOMETHING THAT CAN ACTUALLY TAKE FOCUS. `keyed`
169
+ // is a FOCUS index — its only readers call `focus()` on what it holds — so
170
+ // holding an element a browser ignores makes it silently useless.
171
+ // FIRST-WINS WAS THE DEFECT, and the shape is specific: a RAILED lead is
172
+ // emitted twice with `data-ig-key` — the canvas `<g>`, which carries no
173
+ // `tabindex` because the rail row owns the tab stop, and then the row. The
174
+ // canvas comes first, so every ranked row's `focus()` landed on the group:
175
+ // arrow navigation moved nothing, and focus dropped out of the viewer after
176
+ // a selection redraw. It passed 258 tests because the test double counted
177
+ // every `focus()` call regardless of whether the element could take one.
178
+ // A LATER FOCUSABLE ELEMENT REPLACES A STORED UNFOCUSABLE ONE, rather than
179
+ // indexing only focusable elements: a key whose every element is unfocusable
180
+ // still belongs in the map, so `focus?.()` on it is a no-op rather than a
181
+ // lookup miss, and the pointer identity keeps working exactly as before.
182
+ // TABINDEX **OR** A NATURALLY FOCUSABLE TAG, the same pair the test double
183
+ // now applies — two notions of focusable that can disagree is how this class
184
+ // hides, and it just did.
185
+ const focusableKeys = new Set();
186
+ root = materialize(doc, scene.root, {
187
+ onElement: (spec, node) => {
188
+ const key = spec.attrs?.[KEY_ATTRIBUTE];
189
+ if (typeof key === 'string' && key !== '') {
190
+ const tabindex = spec.attrs?.tabindex;
191
+ const canFocus = (tabindex !== undefined && tabindex !== null) || spec.tag === 'a' || spec.tag === 'button';
192
+ if (!keyed.has(key) || (canFocus && !focusableKeys.has(key))) {
193
+ keyed.set(key, node);
194
+ if (canFocus)
195
+ focusableKeys.add(key);
196
+ }
197
+ drawnHoverable.add(key);
198
+ }
199
+ const group = spec.attrs?.[GROUP_ATTRIBUTE];
200
+ if (typeof group === 'string' && group !== '')
201
+ drawnHoverable.add(group);
202
+ },
203
+ });
204
+ container.appendChild(root);
205
+ currentScene = scene;
206
+ // A REDRAW CAN REMOVE THE HOVERED ISSUE, and `pointerleave` does not fire
207
+ // when it does: the element under the pointer is destroyed while the
208
+ // pointer stays inside the container, so the handler that clears `hovered`
209
+ // never runs. The host was then left holding a key for an issue this
210
+ // document no longer carries, and never received the documented
211
+ // `onHover(null)` until the pointer left the whole viewer.
212
+ // IN `draw()` RATHER THAN IN `update()`, because the removal is a property
213
+ // of REDRAWING, not of one entry point — `setProjection` and a selection
214
+ // redraw can drop the key just as `update` can.
215
+ // ASK WHAT THIS SCENE DREW, NOT WHAT THE DOCUMENT CARRIES. Both tests reject
216
+ // `keyed` alone, and for the reason that still stands: decoration announces
217
+ // itself through `GROUP_ATTRIBUTE` and never enters the focus index, so
218
+ // clearing on `keyed` would fire constantly on a hover that is perfectly
219
+ // live. The earlier answer — document membership — was the wrong correction,
220
+ // because the document is not what the pointer is over.
221
+ // A PROJECTION SWITCH IS WHERE THE TWO COME APART. An off-order edge endpoint
222
+ // is drawn on the graph canvas and has no row in the linear projection, so
223
+ // switching destroys the element while its issue stays in `byKey` — the
224
+ // document test then preserves a hover on something that is no longer on
225
+ // screen, and the host never receives the documented `onHover(null)` until
226
+ // the pointer leaves the whole viewer. `drawnHoverable` carries both
227
+ // attributes from the scene just materialized, so it answers for decoration
228
+ // and for issues, on every redraw, from the thing the pointer can touch.
229
+ if (hovered !== null && !drawnHoverable.has(hovered))
230
+ clearHover();
231
+ // TOLD, NOT JUST DROPPED. The host was told a selection began, so leaving it
232
+ // to infer the ending from a document it happens to have replaced is the
233
+ // same divergence `clearHover` exists to prevent one line up. Fired AFTER
234
+ // the DOM is in place, exactly as the hover clear is, so a host that redraws
235
+ // from this callback finds the viewer already consistent.
236
+ if (announceSelection && state.selected !== selectedBefore) {
237
+ currentOptions.onSelect?.(state.selected);
238
+ }
239
+ };
240
+ const emitSelect = (key) => {
241
+ // A KEY THIS DOCUMENT DOES NOT CARRY IS A SELECTION OF NOTHING, and it has
242
+ // to be resolved HERE, before the state is written. `select()` is documented
243
+ // to fire `onSelect` exactly as a click does — and a click can only ever
244
+ // land on a key the canvas drew, so an unknown key has no click to be
245
+ // equivalent to. A host holding a stale key from its own list is the
246
+ // ordinary way one arrives.
247
+ // WITHOUT THIS, THE TWO NOTIFICATIONS CONTRADICTED EACH OTHER. The state
248
+ // took the unknown key, `draw()` reconciled it away against `byKey` and
249
+ // reported `onSelect(null)`, and then this function reported `onSelect(key)`
250
+ // — so the host was told twice, ending on the key that does not exist,
251
+ // while the handle read `selected: null`. Resolving first means the state
252
+ // and the one notification agree, and the reconciliation in `draw()` finds
253
+ // nothing left to undo.
254
+ const resolved = key !== null && normalized.byKey.has(key) ? key : null;
255
+ state = { ...state, selected: resolved, focused: resolved ?? state.focused };
256
+ // SILENCED, because this function reports the result itself once the draw
257
+ // has settled it. Letting the draw announce too would fire `onSelect` twice
258
+ // for one selection whenever reconciliation renamed it — and a caller
259
+ // cannot tell a doubled notice from a second selection.
260
+ draw(false);
261
+ // FOCUS FOLLOWS THE SUBJECT — the movement branch of `onKeyDown` already
262
+ // does this, and selection needs it for the same reason: `draw()` destroys
263
+ // the subtree holding focus and mounts a replacement. Without it a reader
264
+ // who pressed Enter dropped focus out of the viewer entirely, so no later
265
+ // arrow key reached the container and keyboard navigation was over.
266
+ // HERE AND NOT IN `draw()`, which is the tempting generalization and the
267
+ // wrong one: `update()` redraws too, and a host that refreshes the document
268
+ // while the reader is somewhere else on the page would have focus YANKED
269
+ // into the viewer. Restoring only where the viewer itself moved the subject
270
+ // keeps that impossible. Narrowing `draw()` correctly instead would need to
271
+ // know whether focus was already inside — an `activeElement` this module
272
+ // deliberately does not ask its host for.
273
+ // IT COVERS THE CLICK AND `select()` PATHS TOO, which is what the handle
274
+ // documents: selection "fires onSelect exactly as a click does", and a
275
+ // click moves focus. `:focus-visible` is what keeps the ring off a pointer
276
+ // user, so this costs a mouse reader nothing.
277
+ if (state.focused !== null)
278
+ keyed.get(state.focused)?.focus?.();
279
+ // WHAT THE STATE ACTUALLY HOLDS, not what was asked for. `draw()` reconciles
280
+ // the selection against the scene, which canonicalizes a together unit's
281
+ // partner to the station that represents it — so reporting `resolved` here
282
+ // would tell the host a key the handle does not hold, which is the very
283
+ // disagreement between the two notifications this function was written to
284
+ // end. `select()` stays equivalent to the click it documents itself as.
285
+ currentOptions.onSelect?.(state.selected);
286
+ };
287
+ const onClick = (event) => {
288
+ // AN ACTIVATION ON A CONTROL THAT ACTIVATES ITSELF IS THAT CONTROL'S — the
289
+ // same invariant `onKeyDown` already applies, and the click path needs its
290
+ // own copy because returning from the keydown handler does NOT suppress the
291
+ // click the browser synthesizes afterwards. Following a row's deep link was
292
+ // therefore also selecting the row and firing `onSelect` at the host, for an
293
+ // activation the anchor owned.
294
+ // UNCONDITIONAL HERE, CONDITIONAL THERE, and the asymmetry is the point: a
295
+ // click IS an activation, whereas a keydown may be a MOVEMENT key, which
296
+ // stays the viewer's even while focus rests on a link.
297
+ if (ownsItsOwnActivation(event.target, container))
298
+ return;
299
+ const key = keyAt(event.target, container);
300
+ if (key === null)
301
+ return;
302
+ emitSelect(key);
303
+ };
304
+ const onPointerOver = (event) => {
305
+ const key = keyAt(event.target, container);
306
+ if (key === hovered)
307
+ return;
308
+ hovered = key;
309
+ currentOptions.onHover?.(key);
310
+ };
311
+ const onPointerLeave = () => {
312
+ clearHover();
313
+ };
314
+ const onKeyDown = (event) => {
315
+ if (event.key === undefined || currentScene === null)
316
+ return;
317
+ // An activation key on a control that activates itself is that control's.
318
+ // Movement keys are still the viewer's — a reader on a link must be able to
319
+ // arrow away from it.
320
+ if ((event.key === 'Enter' || event.key === ' ') &&
321
+ ownsItsOwnActivation(event.target, container)) {
322
+ return;
323
+ }
324
+ // SYNC FOCUS FROM THE DOM BEFORE MOVING. `state.focused` tracks the viewer's
325
+ // OWN moves — its arrow keys and its clicks — and native Tab is neither. A
326
+ // reader who tabs into the deep link inside the third row leaves `focused`
327
+ // pointing wherever it was, so ArrowDown moved relative to the OLD row and
328
+ // sent focus BACKWARDS past the row they were standing in.
329
+ // FOCUS IDENTITY ONLY, hence the explicit attribute list: `GROUP_ATTRIBUTE`
330
+ // marks decoration that is deliberately outside the focus index, so adopting
331
+ // one would set `focused` to a key `navigate` cannot find — which resolves
332
+ // to -1 and throws the reader back to the top of the order. That is the same
333
+ // bug this fixes, arriving through the fix.
334
+ // `navigable`, NOT `focusOrder`: a gutter node reachable only sideways is a
335
+ // legitimate place to be standing, and `focusOrder` does not contain it.
336
+ const standingOn = keyAt(event.target, container, [KEY_ATTRIBUTE]);
337
+ if (standingOn !== null && standingOn !== state.focused && currentScene.navigable.includes(standingOn)) {
338
+ state = { ...state, focused: standingOn };
339
+ }
340
+ const result = navigate(currentScene, state, event.key);
341
+ if (result.command.kind === 'none')
342
+ return;
343
+ event.preventDefault?.();
344
+ if (result.command.kind === 'select') {
345
+ emitSelect(result.command.key);
346
+ return;
347
+ }
348
+ state = result.state;
349
+ draw();
350
+ keyed.get(result.command.key)?.focus?.();
351
+ };
352
+ container.addEventListener('click', onClick);
353
+ container.addEventListener('pointerover', onPointerOver);
354
+ container.addEventListener('pointerleave', onPointerLeave);
355
+ container.addEventListener('keydown', onKeyDown);
356
+ draw();
357
+ return {
358
+ update(next, nextOptions) {
359
+ if (destroyed)
360
+ return;
361
+ if (nextOptions !== undefined)
362
+ currentOptions = nextOptions;
363
+ if (nextOptions?.projection !== undefined)
364
+ projection = nextOptions.projection;
365
+ normalized = normalizeDocument(next).document;
366
+ draw();
367
+ },
368
+ setProjection(next) {
369
+ if (destroyed)
370
+ return;
371
+ projection = next;
372
+ draw();
373
+ },
374
+ select(key) {
375
+ if (destroyed)
376
+ return;
377
+ emitSelect(key);
378
+ },
379
+ get state() {
380
+ return state;
381
+ },
382
+ destroy() {
383
+ if (destroyed)
384
+ return;
385
+ destroyed = true;
386
+ // BEFORE THE LISTENERS COME OFF, because after that no `pointerleave` can
387
+ // fire and the host is left holding a key for an element about to be
388
+ // removed — a tooltip pinned to nothing. Destruction bypasses `draw()`
389
+ // entirely, so the redraw reconciliation above never sees this path.
390
+ clearHover();
391
+ container.removeEventListener('click', onClick);
392
+ container.removeEventListener('pointerover', onPointerOver);
393
+ container.removeEventListener('pointerleave', onPointerLeave);
394
+ container.removeEventListener('keydown', onKeyDown);
395
+ if (root !== null)
396
+ container.removeChild(root);
397
+ root = null;
398
+ keyed = new Map();
399
+ },
400
+ };
401
+ }
402
+ //# sourceMappingURL=mount.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mount.js","sourceRoot":"","sources":["../src/mount.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Keyboard navigation, as a pure reducer.
3
+ *
4
+ * It takes a scene, the current state and a key name, and returns the next
5
+ * state plus the one thing the shell should do. No DOM, no events, no
6
+ * side-effects — which is what lets the whole key map be tested exhaustively on
7
+ * a runtime with no DOM at all, and what leaves `mount.ts` with nothing to get
8
+ * wrong except wiring.
9
+ *
10
+ * THE TRAVERSAL IS THE SCENE'S PUBLISHED ORDER, NEVER THE PICTURE. On the spine
11
+ * that order is rank order, so `Tab` and the vertical keys walk the work order
12
+ * even where the drawing places boxes elsewhere. Recovering an order from
13
+ * coordinates is precisely what the design forbids, so this module cannot see
14
+ * a coordinate.
15
+ */
16
+ import { type Scene } from './scene.ts';
17
+ export interface NavigationState {
18
+ /** The roving tab stop. `null` before anything has been focused. */
19
+ readonly focused: string | null;
20
+ /** The selected key, which survives a projection change. */
21
+ readonly selected: string | null;
22
+ }
23
+ /** What the shell should do. `none` means the host keeps its own shortcut. */
24
+ export type NavigationCommand = {
25
+ readonly kind: 'none';
26
+ } | {
27
+ readonly kind: 'focus';
28
+ readonly key: string;
29
+ } | {
30
+ readonly kind: 'select';
31
+ readonly key: string;
32
+ };
33
+ export interface NavigationResult {
34
+ readonly state: NavigationState;
35
+ readonly command: NavigationCommand;
36
+ }
37
+ export declare const initialNavigationState: NavigationState;
38
+ /**
39
+ * Re-resolve state against a scene.
40
+ *
41
+ * Switching projection changes the REPRESENTATION, never the subject, so a
42
+ * selection carries across. Focus follows it when it can; when the new
43
+ * projection does not draw the focused key at all, focus falls to the first
44
+ * item rather than nowhere.
45
+ */
46
+ export declare function reconcile(scene: Scene, state: NavigationState): NavigationState;
47
+ /**
48
+ * Apply one key.
49
+ *
50
+ * Movement never wraps: the ends of the order are the ends of the work, and a
51
+ * list that silently teleports to the top reads as a jump rather than as a
52
+ * boundary. An unhandled key returns the state untouched and `none`, so a host
53
+ * keeps every shortcut this package does not claim.
54
+ */
55
+ export declare function navigate(scene: Scene, state: NavigationState, key: string): NavigationResult;
56
+ //# sourceMappingURL=navigation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../src/navigation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,KAAK,KAAK,EAAmB,MAAM,YAAY,CAAC;AAEzD,MAAM,WAAW,eAAe;IAC9B,oEAAoE;IACpE,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,8EAA8E;AAC9E,MAAM,MAAM,iBAAiB,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAChD;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;CACrC;AAED,eAAO,MAAM,sBAAsB,EAAE,eAGnC,CAAC;AAeH;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,eAAe,GAAG,eAAe,CA6B/E;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,eAAe,EAAE,GAAG,EAAE,MAAM,GAAG,gBAAgB,CAyC5F"}
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Keyboard navigation, as a pure reducer.
3
+ *
4
+ * It takes a scene, the current state and a key name, and returns the next
5
+ * state plus the one thing the shell should do. No DOM, no events, no
6
+ * side-effects — which is what lets the whole key map be tested exhaustively on
7
+ * a runtime with no DOM at all, and what leaves `mount.ts` with nothing to get
8
+ * wrong except wiring.
9
+ *
10
+ * THE TRAVERSAL IS THE SCENE'S PUBLISHED ORDER, NEVER THE PICTURE. On the spine
11
+ * that order is rank order, so `Tab` and the vertical keys walk the work order
12
+ * even where the drawing places boxes elsewhere. Recovering an order from
13
+ * coordinates is precisely what the design forbids, so this module cannot see
14
+ * a coordinate.
15
+ */
16
+ import { resolveFocusKey } from "./scene.js";
17
+ export const initialNavigationState = Object.freeze({
18
+ focused: null,
19
+ selected: null,
20
+ });
21
+ function indexOfFocus(scene, state) {
22
+ if (state.focused === null)
23
+ return -1;
24
+ return scene.focusOrder.indexOf(state.focused);
25
+ }
26
+ function stay(state) {
27
+ return { state, command: { kind: 'none' } };
28
+ }
29
+ function moveTo(state, key) {
30
+ return { state: { ...state, focused: key }, command: { kind: 'focus', key } };
31
+ }
32
+ /**
33
+ * Re-resolve state against a scene.
34
+ *
35
+ * Switching projection changes the REPRESENTATION, never the subject, so a
36
+ * selection carries across. Focus follows it when it can; when the new
37
+ * projection does not draw the focused key at all, focus falls to the first
38
+ * item rather than nowhere.
39
+ */
40
+ export function reconcile(scene, state) {
41
+ // Selection is carried WHOLE, even when the new projection does not draw it.
42
+ // It is the subject, and a switch changes representation rather than subject;
43
+ // dropping it here would make the toggle lose the thing the reader is looking
44
+ // at, which is the one property the design fixes about switching.
45
+ //
46
+ // Focus goes through the SHARED rule, so what this reports and what the
47
+ // projection rendered a tab stop on cannot diverge.
48
+ //
49
+ // CANONICALIZED THROUGH THE SCENE'S OWN STATIONS FIRST, and that is what makes
50
+ // "carried whole" true rather than merely stated. A key this projection draws
51
+ // under another key's station — a together unit's partner — is not a subject
52
+ // this projection can hold: `navigable` does not list it, so focus could not
53
+ // follow it and fell to the FIRST entry in the order, an unrelated issue.
54
+ // Reporting `selected` as the station keeps the subject and lets focus reach
55
+ // it; the tree publishes no stations, so a partner selected there stays
56
+ // itself, which is correct because the tree gives it its own row.
57
+ // HERE RATHER THAN AT THE ENTRY POINTS, because there are four of them —
58
+ // pointer, `handle.select`, a projection switch, a document update — and only
59
+ // this one sees the scene that decides the answer. Canonicalizing per entry
60
+ // point is what left `handle.select` and `setProjection` behaving differently
61
+ // from the click they are documented to be equivalent to.
62
+ const station = (key) => key === null || key === undefined ? null : (scene.stationOf.get(key) ?? key);
63
+ const selected = station(state.selected);
64
+ return {
65
+ focused: resolveFocusKey(scene.navigable, station(state.focused), selected),
66
+ selected,
67
+ };
68
+ }
69
+ /**
70
+ * Apply one key.
71
+ *
72
+ * Movement never wraps: the ends of the order are the ends of the work, and a
73
+ * list that silently teleports to the top reads as a jump rather than as a
74
+ * boundary. An unhandled key returns the state untouched and `none`, so a host
75
+ * keeps every shortcut this package does not claim.
76
+ */
77
+ export function navigate(scene, state, key) {
78
+ const order = scene.focusOrder;
79
+ if (order.length === 0)
80
+ return stay(state);
81
+ const current = indexOfFocus(scene, state);
82
+ const first = order[0];
83
+ const last = order[order.length - 1];
84
+ switch (key) {
85
+ case 'ArrowDown': {
86
+ if (current === -1)
87
+ return moveTo(state, first);
88
+ const next = order[Math.min(current + 1, order.length - 1)];
89
+ return next === state.focused ? stay(state) : moveTo(state, next);
90
+ }
91
+ case 'ArrowUp': {
92
+ if (current === -1)
93
+ return moveTo(state, first);
94
+ const next = order[Math.max(current - 1, 0)];
95
+ return next === state.focused ? stay(state) : moveTo(state, next);
96
+ }
97
+ case 'ArrowLeft':
98
+ case 'ArrowRight': {
99
+ if (state.focused === null)
100
+ return moveTo(state, first);
101
+ const neighbours = scene.lateral.get(state.focused);
102
+ const target = key === 'ArrowLeft' ? neighbours?.left : neighbours?.right;
103
+ return target === undefined ? stay(state) : moveTo(state, target);
104
+ }
105
+ case 'Home':
106
+ return state.focused === first ? stay(state) : moveTo(state, first);
107
+ case 'End':
108
+ return state.focused === last ? stay(state) : moveTo(state, last);
109
+ case 'Enter':
110
+ case ' ': {
111
+ const target = state.focused ?? first;
112
+ return {
113
+ state: { focused: target, selected: target },
114
+ command: { kind: 'select', key: target },
115
+ };
116
+ }
117
+ default:
118
+ return stay(state);
119
+ }
120
+ }
121
+ //# sourceMappingURL=navigation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"navigation.js","sourceRoot":"","sources":["../src/navigation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAc,eAAe,EAAE,MAAM,YAAY,CAAC;AAoBzD,MAAM,CAAC,MAAM,sBAAsB,GAAoB,MAAM,CAAC,MAAM,CAAC;IACnE,OAAO,EAAE,IAAI;IACb,QAAQ,EAAE,IAAI;CACf,CAAC,CAAC;AAEH,SAAS,YAAY,CAAC,KAAY,EAAE,KAAsB;IACxD,IAAI,KAAK,CAAC,OAAO,KAAK,IAAI;QAAE,OAAO,CAAC,CAAC,CAAC;IACtC,OAAO,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;AACjD,CAAC;AAED,SAAS,IAAI,CAAC,KAAsB;IAClC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC;AAC9C,CAAC;AAED,SAAS,MAAM,CAAC,KAAsB,EAAE,GAAW;IACjD,OAAO,EAAE,KAAK,EAAE,EAAE,GAAG,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE,CAAC;AAChF,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,SAAS,CAAC,KAAY,EAAE,KAAsB;IAC5D,6EAA6E;IAC7E,8EAA8E;IAC9E,8EAA8E;IAC9E,kEAAkE;IAClE,EAAE;IACF,wEAAwE;IACxE,oDAAoD;IACpD,EAAE;IACF,+EAA+E;IAC/E,8EAA8E;IAC9E,6EAA6E;IAC7E,6EAA6E;IAC7E,0EAA0E;IAC1E,6EAA6E;IAC7E,wEAAwE;IACxE,kEAAkE;IAClE,yEAAyE;IACzE,8EAA8E;IAC9E,4EAA4E;IAC5E,8EAA8E;IAC9E,0DAA0D;IAC1D,MAAM,OAAO,GAAG,CAAC,GAA8B,EAAiB,EAAE,CAChE,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC;IAC/E,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IACzC,OAAO;QACL,OAAO,EAAE,eAAe,CAAC,KAAK,CAAC,SAAS,EAAE,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC;QAC3E,QAAQ;KACT,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAY,EAAE,KAAsB,EAAE,GAAW;IACxE,MAAM,KAAK,GAAG,KAAK,CAAC,UAAU,CAAC;IAC/B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC;IAE3C,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IAC3C,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAW,CAAC;IACjC,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAW,CAAC;IAE/C,QAAQ,GAAG,EAAE,CAAC;QACZ,KAAK,WAAW,CAAC,CAAC,CAAC;YACjB,IAAI,OAAO,KAAK,CAAC,CAAC;gBAAE,OAAO,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YAChD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,GAAG,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAW,CAAC;YACtE,OAAO,IAAI,KAAK,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACpE,CAAC;QACD,KAAK,SAAS,CAAC,CAAC,CAAC;YACf,IAAI,OAAO,KAAK,CAAC,CAAC;gBAAE,OAAO,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YAChD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,GAAG,CAAC,EAAE,CAAC,CAAC,CAAW,CAAC;YACvD,OAAO,IAAI,KAAK,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACpE,CAAC;QACD,KAAK,WAAW,CAAC;QACjB,KAAK,YAAY,CAAC,CAAC,CAAC;YAClB,IAAI,KAAK,CAAC,OAAO,KAAK,IAAI;gBAAE,OAAO,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YACxD,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YACpD,MAAM,MAAM,GAAG,GAAG,KAAK,WAAW,CAAC,CAAC,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,CAAC,UAAU,EAAE,KAAK,CAAC;YAC1E,OAAO,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QACpE,CAAC;QACD,KAAK,MAAM;YACT,OAAO,KAAK,CAAC,OAAO,KAAK,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QACtE,KAAK,KAAK;YACR,OAAO,KAAK,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACpE,KAAK,OAAO,CAAC;QACb,KAAK,GAAG,CAAC,CAAC,CAAC;YACT,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC;YACtC,OAAO;gBACL,KAAK,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE;gBAC5C,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE;aACzC,CAAC;QACJ,CAAC;QACD;YACE,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC;IACvB,CAAC;AACH,CAAC"}