umlimator 0.13.0 → 0.14.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.
package/README.md CHANGED
@@ -38,8 +38,9 @@ drill-down, a data pipeline, an AI agent's skills, and host pages for embedding,
38
38
  - **The page's pieces, embedded**: the standalone page's views list, details panel, legend and hint line, each one a
39
39
  `render()` option, around the map in the host's own page.
40
40
  - **Editing** (opt-in): gestures emit `change` events; the host applies them and calls `update()`.
41
- - **Export**: any view as a stand-alone SVG or PNG, in the browser or from the CLI; the whole map as one PDF, each view
42
- with its text, then the legend and the rules (`umlimator.py pdf`).
41
+ - **Export**: any view as a stand-alone SVG or PNG, in the browser or from the CLI; the whole map as one PDF to hand in
42
+ for a review: a cover with the contents, each view with its legend and tables of its cards, lines and boxes, then the
43
+ rules (`umlimator.py pdf`).
43
44
  - **In docs**: `<umlimator-map src="model.json">` in any page, and an `umlimator` fenced block in VitePress, Docusaurus,
44
45
  MkDocs or any Markdown that allows HTML.
45
46
  - **A layout check**: crossings, shared lines, detours, labels on lines, text that overflows its card.
@@ -59,7 +60,7 @@ drill-down, a data pipeline, an AI agent's skills, and host pages for embedding,
59
60
  npm install umlimator
60
61
  ```
61
62
 
62
- Or from GitHub, at a tag or any commit: `"umlimator": "github:zintaen/umlimator#v0.13.0"`. npm 12 refuses git
63
+ Or from GitHub, at a tag or any commit: `"umlimator": "github:zintaen/umlimator#v0.14.0"`. npm 12 refuses git
63
64
  dependencies unless the project opts in, so that also needs `allow-git=root` in the `.npmrc` next to `package.json`.
64
65
 
65
66
  ## Embed
@@ -141,7 +142,8 @@ the map is drawn, and stay where scripts don't run ([`examples/element.html`](ex
141
142
 
142
143
  - **The model**: `src`, a URL resolved as an `<img>`'s is (a `data:` URL too), or the model in a child
143
144
  `<script type="application/json">`.
144
- - **Attributes**: `view`, `minimap`, `controls="false"`, `height` (a number is pixels; 480 by default, or size it in CSS)
145
+ - **Attributes**: `view`, `minimap`, `controls="false"`, `wheel="ctrl"` (the wheel zooms only with Ctrl or ⌘, and scrolls
146
+ the page otherwise, as a map in a document should), `height` (a number is pixels; 480 by default, or size it in CSS)
145
147
  and `theme` (`light` or `dark`: theme.css's palette for that map, whatever the page's). Changing one draws the map
146
148
  again, and removing the element destroys it.
147
149
  - **Events**: `umlimator:select`, `umlimator:open` (cancel it to stay) and `umlimator:viewchange`, the map's event as
@@ -223,7 +225,7 @@ python3 umlimator.py record bike-share.html --story ride --out ride.png # bike
223
225
  python3 umlimator.py import workspace.dsl --out model.json # a diagram you have already, as a model
224
226
  docker compose config --format json > compose.json && python3 umlimator.py import compose.json --out model.json
225
227
  kubectl get deploy,sts,svc,ing -A -o json > cluster.json && python3 umlimator.py import cluster.json --out model.json
226
- python3 umlimator.py pdf page.html --out map.pdf # every view in one PDF, with its text, the legend, the rules
228
+ python3 umlimator.py pdf page.html --out map.pdf # every view in one PDF: a cover, each view with its tables, the rules
227
229
  python3 umlimator.py trace model.json trace.json --view ride-end --match match.json --out model.json # an OTLP trace kept as a numbered view
228
230
  python3 umlimator.py mcp model.json # an MCP server over stdio, for an AI assistant (below)
229
231
  ```
@@ -239,16 +241,22 @@ opening the view below it; `examples/clinic.json` is `examples/clinic.dsl`, impo
239
241
  what is left out: [docs/module-api.md § Import](docs/module-api.md#import-90). Whatever the file holds that `import`
240
242
  doesn't read is listed on stderr with its line (in JSON, its path).
241
243
 
242
- `pdf` prints the map through Chrome's own print, A4 across: a title page, then each view on a page of its own, as a
243
- vector picture with its `describe()` text, then the legend and each rule, holding or broken in the views printed
244
- ([bike-share's](https://zintaen.github.io/umlimator/examples/bike-share.pdf), as CI builds it). It takes `--views` and
245
- `--theme`, as `stills` does.
244
+ `pdf` prints the map through Chrome's own print, as a document to hand in for a review
245
+ ([bike-share's](https://zintaen.github.io/umlimator/examples/bike-share.pdf), as CI builds it). A cover has the title,
246
+ the date, the model's file, a summary line and the contents with each view's page. Each view starts a page of its own,
247
+ under its path ("Bike share › What runs"): its picture in vectors, as large as the page allows, the legend of what it
248
+ draws, and tables of its cards, lines and boxes, which go on over the next pages with their header row when they are
249
+ long. The rules follow as a table, when the model has any, each holding or broken in the views printed. Every page ends in
250
+ the title and "page N of M". It takes `--views` and `--theme`, as `stills` does, and `--paper a4|letter` (A4 is the
251
+ default; both across).
246
252
 
247
253
  The page has breadcrumbs, a details panel per card (with the slowest path from it and what-if), ⌘K search, deep links
248
254
  (`#view=<id>&node=<id>`, `#story=<id>&step=<n>` for a story's step, and `#lens=<id>`), stories, lenses, and export. For
249
255
  scripts and tests, it keeps its map's handle on `window.umlimatorMap`: the `Handle` that `render()` returns (`setView()`,
250
256
  `highlight()`, `tour()`, `play()` ...).
251
- `stills` skips a view whose inputs have not changed since its last export (`--all` exports every view). Needs Python
257
+ `stills` skips a view whose inputs have not changed since its last export (`--all` exports every view), and exits 1,
258
+ saying why, when a view can't be exported. In `stills`, `check` and `pdf`, `--views` takes view ids, and a trailing `*`
259
+ is a prefix; a name that matches no view is an error. A command that fails leaves its previous output as it was. Needs Python
252
260
  3.9+ (standard library only), Google Chrome or Chromium for `stills`, `check`, `record` and `pdf`, and Node 18+ for
253
261
  `diff`, `drift`, `trace` and `mcp`.
254
262
 
@@ -261,17 +269,19 @@ ffmpeg -f apng -i ride.png -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" -pix_fmt yuv4
261
269
  ```
262
270
 
263
271
  On pull requests, the diff Action posts what they change in your maps: one comment with the summary and a sketch, and
264
- the pictures as the run's artifact.
272
+ the pictures as the run's artifact. The comment names the commit it describes, only the run for the pull request's
273
+ latest commit writes it, and it says so when the diff fails.
265
274
 
266
275
  ```yaml
267
276
  on: pull_request
268
277
  permissions: { contents: read, pull-requests: write }
278
+ concurrency: { group: "maps-${{ github.event.pull_request.number }}", cancel-in-progress: true } # one run per pull request
269
279
  jobs:
270
280
  maps:
271
281
  runs-on: ubuntu-latest
272
282
  steps:
273
283
  - uses: actions/checkout@v4
274
- - uses: zintaen/umlimator@v0.13.0
284
+ - uses: zintaen/umlimator@v0.14.0
275
285
  with:
276
286
  models: "docs/*.json" # optional: where the models are (default: every .json that is a map model)
277
287
  ```
@@ -296,12 +306,18 @@ claude mcp add umlimator -- python3 /path/to/umlimator.py mcp /path/to/model.jso
296
306
  | `show_view(view)` | shows a view on the page |
297
307
  | `highlight(refs, view?)` | lights cards and lines (their ids, or the Refs `paths` and `reach` give) and dims the rest |
298
308
  | `tour(cards, view?)` | a dot rides the lines through these cards |
299
- | `play_story(id)`, `clear()` | plays one of the model's stories; takes the highlight, the tour and the story off |
309
+ | `play_story(id)`, `clear()` | plays one of the model's stories; takes the assistant's highlight, tour and story off |
310
+ | `page_state()` | what each open page shows (its map's `state`: view, selection, layers, story, camera), and whether the model renders |
300
311
 
301
312
  - The read tools run the module's own functions in Node on the model file, read again at each call, so edits show up.
302
313
  - The page tools drive the map's page, which the server serves at `http://127.0.0.1:7391` (`--port N`, or another port
303
- when that one is taken). Open it in a browser. Each tool says whether a page is connected, and gives the URL when none
304
- is; a page that connects later shows the latest command, and a page drawn from an older model draws itself again.
314
+ when that one is taken). Open it in a browser.
315
+ - A tool answers "shown" once a page has shown it, or says why none did: the page's own error, or a tab in the
316
+ background that didn't finish in time. With no page open, it gives the URL.
317
+ - A page that opens later shows what the assistant built: the view, with the highlight on it, and a tour or story
318
+ still under way.
319
+ - The assistant's highlight is a layer of its own (`mcp`): off on another view, back on its own.
320
+ - A model that stops rendering keeps the last page that did, and the tools and the page say why.
305
321
  - The server listens on 127.0.0.1 only and fetches nothing, so a logo not in the cache yet keeps its card's sigil.
306
322
 
307
323
  ## Map a repo with Claude Code
@@ -1,11 +1,13 @@
1
1
  // Types for lib/umlimator-element.js, `umlimator/element` (#92). Its header says what each attribute and event does.
2
2
  import type { Events, Handle } from './umlimator.js';
3
3
 
4
- /** <umlimator-map src view minimap controls theme height>: the model from `src` (a URL) or a child
4
+ /** <umlimator-map src view minimap controls wheel theme height>: the model from `src` (a URL) or a child
5
5
  * <script type="application/json">, drawn by render(); its other children show until the map is drawn. */
6
6
  export class UmlimatorMap extends HTMLElement {
7
- /** render()'s handle once the map is drawn (each `load` event), else null. */
7
+ /** render()'s handle once the map is drawn (each `load` event), else null. Its `state` is the map's MapState. */
8
8
  readonly map: Handle | null;
9
+ /** True while a draw waits for its model (0.14.0); `map` is then still the map drawn last. `load` or `error` ends it. */
10
+ readonly loading: boolean;
9
11
  }
10
12
 
11
13
  declare global {
@@ -6,45 +6,54 @@
6
6
 
7
7
  The model comes from `src`, a URL (a data: URL too), or from a child <script type="application/json">, and render()
8
8
  draws it in the element's shadow root. The element's other children (a still, say) show until the map is drawn, and stay
9
- where scripts don't run. Changing src, view, minimap or controls (controls="false" hides them) draws it again, and
10
- removing the element destroys the map. height: a number is pixels (default 480px; CSS may size it instead). theme:
9
+ where scripts don't run. Changing src, view, minimap, controls (controls="false" hides them) or wheel draws it again,
10
+ and removing the element destroys the map. wheel="ctrl": the wheel zooms only with Ctrl or ⌘, and scrolls the page
11
+ otherwise, as a map in a document should. height: a number is pixels (default 480px; CSS may size it instead). theme:
11
12
  "light" or "dark", that map's palette from lib/umlimator-theme.css, whatever the page's; without it the map follows the
12
13
  page, a docs site's dark mode too (VitePress's html.dark, Docusaurus's html[data-theme="dark"]). The colours come from
13
14
  that file or the host's own --um-* variables; the element fetches nothing but its model.
14
15
  Events: the map's select, open and viewchange as umlimator:select, umlimator:open and umlimator:viewchange, the map's
15
- event as `detail` (preventDefault() on umlimator:open keeps the view); load each time the map is drawn, error when it
16
- can't be (its children show again). `el.map` is render()'s handle once drawn (toSVG(), highlight(), setView() ...),
17
- else null. */
16
+ event as `detail` (preventDefault() on umlimator:open keeps the view); load each time the map is drawn, error (an
17
+ ErrorEvent, whose message says why) when it can't be, a model with no view to draw too (its children show again).
18
+ `el.map` is render()'s handle once drawn (toSVG(), highlight(), setView(), state ...), else null; `el.loading` is true
19
+ while a draw waits for its model, when el.map is still the map drawn last. An attribute set to the value it has draws
20
+ nothing again. */
18
21
  import { render } from './umlimator.js';
19
22
 
20
23
  const Base = globalThis.HTMLElement || class {}; // importable where there is no DOM (a docs site's server render)
21
24
 
22
25
  export class UmlimatorMap extends Base {
23
- static observedAttributes = ['src', 'view', 'minimap', 'controls', 'height'];
26
+ static observedAttributes = ['src', 'view', 'minimap', 'controls', 'wheel', 'height'];
24
27
  map = null;
25
- #drawn = 0; #due = false; #src = null; #model = null;
28
+ #drawn = 0; #due = false; #src = null; #model = null; #loading = false;
26
29
 
27
30
  constructor() {
28
31
  super();
29
32
  this.attachShadow({ mode: 'open' }).innerHTML = '<style>:host { display: block; height: 480px; overflow: hidden; } ::slotted(img) { display: block; max-width: 100%; max-height: 100%; }</style>'
30
33
  + '<slot></slot><div hidden style="height: 100%"></div>';
31
34
  }
35
+ get loading() { return this.#loading; } // a draw under way, waiting for its model (0.14.0)
32
36
  connectedCallback() { this.#soon(); }
33
- disconnectedCallback() { this.#drop(); }
37
+ disconnectedCallback() { this.#loading = false; this.#drop(); }
34
38
  attributeChangedCallback(name, was, now) {
39
+ if (was === now) return; // set to the value it has: nothing to draw again (0.14.0)
35
40
  if (name === 'height') this.style.height = now == null ? '' : /^\d+(\.\d+)?$/.test(now) ? now + 'px' : now;
36
41
  else this.#soon();
37
42
  }
38
43
  #soon() { // attributes set together draw once
44
+ this.#loading = true;
39
45
  if (this.#due) return;
40
46
  this.#due = true;
41
- queueMicrotask(() => { this.#due = false; if (this.isConnected) this.#draw(); });
47
+ queueMicrotask(() => { this.#due = false; if (this.isConnected) this.#draw(); else this.#loading = false; });
42
48
  }
43
49
  #drop() {
44
50
  this.map?.destroy(); this.map = null;
45
51
  const [, slot, box] = this.shadowRoot.children; slot.hidden = false; box.hidden = true;
46
52
  }
47
- #fail(err) { this.#drop(); console.warn('<umlimator-map>:', err); this.dispatchEvent(new Event('error')); } // the children show again
53
+ #fail(err) { // the children show again, and the error event says why
54
+ this.#loading = false; this.#drop(); console.warn('<umlimator-map>:', err);
55
+ this.dispatchEvent(new ErrorEvent('error', { message: String((err && err.message) || err), error: err }));
56
+ }
48
57
  async #draw() {
49
58
  const n = ++this.#drawn, src = this.getAttribute('src'), get = a => this.getAttribute(a); // a later draw wins
50
59
  let model = src && src === this.#src ? this.#model : null;
@@ -59,7 +68,8 @@ export class UmlimatorMap extends Base {
59
68
  model = JSON.parse(json.textContent);
60
69
  }
61
70
  } catch (err) { if (n === this.#drawn) this.#fail(err); return; }
62
- if (n !== this.#drawn || !this.isConnected) return;
71
+ if (n !== this.#drawn) return; // a later draw has the element
72
+ if (!this.isConnected) { this.#loading = false; return; }
63
73
  if (src) { this.#src = src; this.#model = model; }
64
74
  this.#drop();
65
75
  const [, slot, box] = this.shadowRoot.children; slot.hidden = true; box.hidden = false;
@@ -68,8 +78,10 @@ export class UmlimatorMap extends Base {
68
78
  if (!this.dispatchEvent(new CustomEvent('umlimator:' + t, { detail: e, bubbles: true, composed: true, cancelable: t === 'open' }))) e.preventDefault();
69
79
  }]));
70
80
  try {
71
- this.map = render(box, model, { view: get('view') || undefined, minimap: this.hasAttribute('minimap') && get('minimap') !== 'false', controls: get('controls') !== 'false', on });
81
+ this.map = render(box, model, { view: get('view') || undefined, minimap: this.hasAttribute('minimap') && get('minimap') !== 'false', controls: get('controls') !== 'false', wheel: get('wheel') === 'ctrl' ? 'ctrl' : 'zoom', on });
72
82
  } catch (err) { return this.#fail(err); }
83
+ if (this.map.view == null) return this.#fail(new Error(`${src || 'the inline model'}: the model has no view to draw`)); // drawn, with nothing to see: an error, not a load (0.14.0)
84
+ this.#loading = false;
73
85
  this.dispatchEvent(new Event('load'));
74
86
  }
75
87
  }
@@ -6,11 +6,11 @@
6
6
  view: api
7
7
  ```
8
8
 
9
- The block's `key: value` lines are the element's attributes (src, view, minimap, controls, theme, height); after them,
10
- or instead of src, it may hold a model in JSON. An inline model goes into src as a data: URL, since VitePress compiles
11
- the page as a Vue template, which drops a <script> child. A block the element can't use fails the build, where it is
12
- seen. */
13
- const KEYS = ['src', 'view', 'minimap', 'controls', 'theme', 'height'];
9
+ The block's `key: value` lines are the element's attributes (src, view, minimap, controls, wheel, theme, height); after
10
+ them, or instead of src, it may hold a model in JSON. An inline model goes into src as a data: URL, since VitePress
11
+ compiles the page as a Vue template, which drops a <script> child. A block the element can't use fails the build,
12
+ where it is seen. */
13
+ const KEYS = ['src', 'view', 'minimap', 'controls', 'wheel', 'theme', 'height'];
14
14
 
15
15
  /** A block's text as the element's attributes. */
16
16
  export function attrs(text) {
@@ -92,14 +92,44 @@ export interface Events {
92
92
  change: Change; // only with `editable`
93
93
  /** E or Alt + click on a stacked card, or the ⤡ tab of the box it unfolded into (#71): before the card unfolds in place
94
94
  * (expanded: true) or folds back (false). view: the card's inner view, which may not be in the model yet; the card
95
- * unfolds once it is (the host loads it and calls update() right here, or later expand()). preventDefault(): no change. */
95
+ * unfolds once it is: the update() that brings it unfolds the card (here or later), or expand(). preventDefault(): no change. */
96
96
  expand: { target: Extract<Ref, { type: 'node' }>; view: string; expanded: boolean; preventDefault(): void };
97
- story: { story: string; step: number | null; steps: number }; // a story shows its step (0-based); step null: it ended
97
+ /** A story changed: it shows a step (0-based), it was paused or resumed (playing), or it ended (step null, on destroy()
98
+ * too). playing (0.14.0): it goes on by itself. */
99
+ story: { story: string; step: number | null; steps: number; playing: boolean };
100
+ /** The map's state changed (0.14.0): at most once a frame, with the keys that changed since the last state event. The
101
+ * camera is the exception: a pan, zoom, glide or drag is one event, once it comes to rest, and until then the event's
102
+ * camera is where it last rested (handle.state reads it live). The last event, on destroy(), has destroyed: true. */
103
+ state: { state: MapState; changed: (keyof MapState)[] };
104
+ /** The lens on the map changed: lens(), or an update() whose model lacks the lens shown (lens: null). */
105
+ lens: { lens: string | null };
106
+ }
107
+
108
+ /** The map's state, for hosts and agents (0.14.0): handle.state, the state event, the standalone page's
109
+ * window.umlimatorMap.state and <umlimator-map>'s el.map.state. Fresh on every read, frozen, and plain JSON. */
110
+ export interface MapState {
111
+ view: string | null; // shown now (null: the model has no views)
112
+ navigating: string | null; // the view a move under way is going to
113
+ selection: Ref[]; // as `selected`
114
+ expanded: string[]; // as `expanded`
115
+ lens: string | null;
116
+ overlays: string[]; // the layers on the map, in stack order ('lens', 'highlight', 'replay', 'story', 'tour', a host's)
117
+ story: { id: string; step: number; steps: number; playing: boolean } | null;
118
+ tour: boolean; // a tour's dot is riding
119
+ replay: { playing: boolean } | null; // a replay under way: playing, or paused
120
+ camera: { x: number; y: number; zoom: number }; // the centre in map units, and the zoom (1: actual size); a state event's: at rest
121
+ flow: boolean;
122
+ motion: 'full' | 'reduced' | 'instant';
123
+ gesture: 'idle' | 'dragging' | 'connecting' | 'naming'; // an edit in progress (editable maps)
124
+ destroyed: boolean;
98
125
  }
99
126
 
100
127
  /** Where the user dropped or placed something: a point in the view and, on a column view, its column and row. */
101
128
  export interface Hint { x: number; y: number; col?: number; row?: number }
102
- /** An edit the user asked for. Nothing changes until the host applies it and calls update(). */
129
+ /** An edit the user asked for. Nothing changes until the host applies it and calls update(). The places it hints (a card
130
+ * moved, reordered or added) are used only if that update's model shows the edit: the card in its new box, a new card
131
+ * with its name, or, for a place alone, the card's `layout` entry written or the model unchanged. Any other update drops
132
+ * them. An update back to the model before an applied move, or after it (undo, redo), puts the cards back as they were. */
103
133
  export type Change = { id: string; view: string; hint?: Hint } & (
104
134
  | { op: 'add'; kind: 'node'; label: string; container?: string }
105
135
  | { op: 'add'; kind: 'edge'; from: string; to: string; label?: string }
@@ -112,6 +142,9 @@ export interface RenderOptions {
112
142
  view?: string; // default: model.root, else the view with no parent
113
143
  select?: Ref | null;
114
144
  controls?: boolean; // up / zoom / fit / 1:1 / flow buttons (default true)
145
+ /** 'zoom' (the default): the wheel zooms the map, and a trackpad pans it. 'ctrl': only Ctrl or ⌘ with the wheel (and a
146
+ * trackpad's pinch) zooms, and the wheel otherwise scrolls the page, as a map in a document should. */
147
+ wheel?: 'zoom' | 'ctrl';
115
148
  minimap?: boolean; // default false
116
149
  flow?: boolean; // animated flow along lines (default: on unless prefers-reduced-motion)
117
150
  /** Text widths: default the host's font (--um-sans, measured on a canvas); false: a font-independent estimate. */
@@ -148,6 +181,9 @@ export interface Handle {
148
181
  * where it was; none of it with the option `ease: false` or under prefers-reduced-motion, and an export at any point
149
182
  * draws where things end up. */
150
183
  update(model: MapModel): void;
184
+ /** Shows a view, zooming in or out as a drill-down does. A request during a move takes the move to its own view (the
185
+ * latest wins). Resolves true once the view shows; false for a view the model lacks (with a console warning), when a
186
+ * later request supersedes it, or on destroy(). */
151
187
  setView(id: string): Promise<void> | void;
152
188
  select(ref: Ref | null): void;
153
189
  fit(): void;
@@ -175,13 +211,28 @@ export interface Handle {
175
211
  readonly view: string | null;
176
212
  readonly selection: Ref | null; // the latest selected
177
213
  readonly selected: Ref[]; // every selected thing (Shift / Cmd / Ctrl + click adds cards), the latest last
214
+ /** The map's state as one snapshot (0.14.0): fresh, frozen and JSON-serializable on every read. */
215
+ readonly state: MapState;
216
+ /** The view a move is going to, while one is under way (setView(), a drill-down, up); else null. */
217
+ readonly navigating: string | null;
218
+ /** True once destroy() has run: calls then do nothing, and every promise has settled. */
219
+ readonly destroyed: boolean;
220
+ /** The animated flow along lines (the ≋ button and F), to read or set. */
221
+ flow: boolean;
222
+ /** How this map moves: 'instant' (drawn without animation, as the page's #check, #still and #shot are), 'reduced' (the
223
+ * reader's setting), else 'full'. */
224
+ readonly motion: 'full' | 'reduced' | 'instant';
225
+ /** The player of the story shown, whoever started it (play(), the standalone page's Stories menu or a link), else null. */
226
+ readonly story: StoryPlayer | null;
178
227
  /** Sets (or, with null, removes) a named layer of marks, drawn over the picture without touching the model. Layers stack
179
228
  * in the order first set: each property comes from the latest layer that sets it, and a card or line is as lit as its
180
229
  * lowest level. They survive update() and setView(); ids the view doesn't show are ignored. toSVG() and toPNG() draw them. */
181
230
  overlay(name: string, layer: Overlay | null): void;
182
231
  /** The 'highlight' layer: these cards and lines lit, the rest dimmed (in place of the selection's dimming), or null to clear.
183
- * A Ref's level (0 to 1), else its distance (as reach() gives it), fades it. A chip's or badge's Ref lights its card. */
184
- highlight(refs: Lit[] | null): void;
232
+ * A Ref's level (0 to 1), else its distance (as reach() gives it), fades it. A chip's or badge's Ref lights its card.
233
+ * layer (0.14.0): draw them as a layer of that name instead, so two owners never clear each other's (the MCP page's
234
+ * assistant uses 'mcp'); null clears that layer only. */
235
+ highlight(refs: Lit[] | null, options?: { layer?: string }): void;
185
236
  /** Puts one of the model's lenses on the map (#94), or none with null: the 'lens' layer lights what it picks and dims the
186
237
  * rest, on every view shown, and the view is not laid out again. With color 'owner', each lit card is ringed in its
187
238
  * owner's tone, and a legend over the controls names them. It stays across update() and setView(); a model without the
@@ -189,8 +240,9 @@ export interface Handle {
189
240
  lens(id: string | null): boolean;
190
241
  /** The tour dot rides the line between each pair of cards in turn (either way), through each card to its next line; a card
191
242
  * it reaches flashes, and a pair with no line between them is skipped. Resolves true at the end, false when cut short (a
192
- * new tour or replay, tour(null), a new picture of the view, destroy()). With reduced motion it resolves at once.
193
- * speed: map units a second (default 240). */
243
+ * new tour or replay, tour(null), Esc on the map, a new picture of the view, destroy()). With reduced motion no dot rides:
244
+ * it resolves at once, and the path is lit at rest (its cards ringed and numbered in turn, its lines glowing: the 'tour'
245
+ * layer) and said, until the next tour or replay, tour(null), Esc or a new picture. speed: map units a second (default 240). */
194
246
  tour(path: string[] | null, options?: { speed?: number }): Promise<boolean>;
195
247
  /** Plays a trace: each call from one card to another (a span and its parent's) runs along their line at the trace's relative
196
248
  * time; the line glows while the call lasts and keeps its duration as a label (the 'replay' layer, until stop() or the next
@@ -200,8 +252,11 @@ export interface Handle {
200
252
  /** Tells one of the model's stories (#69): each step in turn sets the view, camera, selection, highlight and caption
201
253
  * (drawn in a bar on the map and said to screen readers), then plays its tour. auto (default true): the next step
202
254
  * follows once a step's duration is up and its tour has ended. step: where to start (0-based). The bar and the keys
203
- * (← → PageUp PageDown, Esc) step through and stop it. null when the model has no story `id` with steps. */
204
- play(id: string, options?: { step?: number; auto?: boolean }): StoryPlayer | null;
255
+ * (← → PageUp PageDown, Esc) step through and stop it. The reader taking the map (a press, a key, a view or a selection
256
+ * the story didn't make) pauses it, and a pause holds the step's tour too, until resume(). A step folds back the cards
257
+ * unfolded on its view first, and its highlight is its own 'story' layer, which dims alone while the step shows.
258
+ * null when the model has no story `id` with steps, and the story shown, if any, plays on; play(null) stops it. */
259
+ play(id: string | null, options?: { step?: number; auto?: boolean }): StoryPlayer | null;
205
260
  destroy(): void;
206
261
  }
207
262
 
@@ -215,9 +270,11 @@ export interface StoryStep {
215
270
  camera?: 'fit' | { x: number; y: number; zoom?: number } | { card: string; zoom?: number }; duration?: number;
216
271
  }
217
272
  /** A story being told. step: the one shown (0-based). next() past the last step ends it (done: true); stop() closes it
218
- * (done: false). */
273
+ * (done: false). state (0.14.0): entering a step (its view, camera, selection and highlight on their way), playing or paused
274
+ * on it, then ended or stopped. */
219
275
  export interface StoryPlayer {
220
276
  readonly story: string; readonly step: number; readonly steps: number; readonly playing: boolean; readonly done: Promise<boolean>;
277
+ readonly state: 'entering' | 'playing' | 'paused' | 'ended' | 'stopped';
221
278
  next(): void; prev(): void; pause(): void; resume(): void; stop(): void;
222
279
  }
223
280