umlimator 0.12.0 → 0.13.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
@@ -18,25 +18,40 @@ drill-down, a data pipeline, an AI agent's skills, and host pages for embedding,
18
18
  - **Unfolding in place**: a stacked card can also open into a box on the map it is on (E, or Alt + click), its inner
19
19
  view's cards inside and the lines to it moved onto them, the rest of the picture in its order.
20
20
  - **Layout and routing**: columns in reading order, each ordered so the lines cross as little as they can, those that
21
- skip columns included; each card level with the cards its lines come from, so lines run straight; orthogonal lines
22
- that never run through a card, room for every label, containers kept apart, and cards with no lines in a row of their
23
- own, under the view or at the bottom of their box. The same model gives the same picture.
21
+ skip columns included; each card level with the cards its lines come from, in its box or outside it, so lines run
22
+ straight; orthogonal lines that never run through a card, room for every label, containers kept apart, and cards with
23
+ no lines in a row of their own, under the view or at the bottom of their box. The same model gives the same picture.
24
24
  - **Reading**: animated flow, neighbours lit on selection, numbered steps, tours, minimap, light and dark themes.
25
25
  - **Exploring**: every path between two cards, or everything a card reaches, lit on the map; a view in plain sentences
26
26
  (`describe()`); a tour along any path the host names.
27
27
  - **Live data**: marks over the map without touching the model (`overlay()`): flow as fast as the traffic, errors that
28
- pulse, slow hops that glow, labels and tabs; and an OpenTelemetry trace replayed along its lines (`replay()`).
28
+ pulse, slow hops that glow, labels and tabs; and an OpenTelemetry trace replayed along its lines (`replay()`), or kept
29
+ as a numbered view to review in a pull request (`umlimator.py trace`).
30
+ - **Lenses**: saved ways to look at one map, by team or by data flow: what a lens picks lit and the rest dimmed, cards
31
+ ringed by owner with a legend, from the page's Lenses menu or a link. The view is never laid out again.
32
+ - **Budgets**: lines' latency and cost; the slowest or costliest path lit with its total (`critical()`), and what loses
33
+ its way if a card goes down (`whatIf()`).
29
34
  - **Stories**: walk-throughs in the model, step by step (view, camera, selection, highlight, tour and a caption), played
30
35
  on the map (`play()`) or from the page's Stories menu, recorded from the page, and written as an animated PNG.
31
36
  - **Keyboard and screen readers**: cards, then lines, in the Tab order (a card's tabs and chips too, for a host that
32
37
  listens for click); arrows between cards; selections announced.
38
+ - **The page's pieces, embedded**: the standalone page's views list, details panel, legend and hint line, each one a
39
+ `render()` option, around the map in the host's own page.
33
40
  - **Editing** (opt-in): gestures emit `change` events; the host applies them and calls `update()`.
34
- - **Export**: any view as a stand-alone SVG or PNG, in the browser or from the CLI.
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`).
43
+ - **In docs**: `<umlimator-map src="model.json">` in any page, and an `umlimator` fenced block in VitePress, Docusaurus,
44
+ MkDocs or any Markdown that allows HTML.
35
45
  - **A layout check**: crossings, shared lines, detours, labels on lines, text that overflows its card.
36
46
  - **Rules**: what the model says must hold ("apps never call a database directly", "no cycles between teams' boxes"),
37
47
  marked on the map where it breaks; `umlimator.py check` fails on a broken rule, so CI does.
38
48
  - **Change**: what a pull request changes, marked on the map and said in words (with a GitHub Action that posts it);
39
49
  a time-lapse of a model's history, where cards glide, fade in and fade out; and drift, the map against the traffic seen.
50
+ - **Import**: a Structurizr workspace, a C4-PlantUML or Mermaid diagram, or what `docker compose config` and `kubectl get`
51
+ print as JSON, read into a model (`umlimator.py import`), with what it can't read listed line by line.
52
+ - **An AI assistant on the map**: an MCP server (`umlimator.py mcp`) through which an assistant such as Claude Code reads
53
+ the model (a view in words, paths, what a card reaches, broken rules, problems, a diff) and drives its page in your
54
+ browser (a view, a highlight, a tour, a story).
40
55
 
41
56
  ## Install
42
57
 
@@ -44,7 +59,7 @@ drill-down, a data pipeline, an AI agent's skills, and host pages for embedding,
44
59
  npm install umlimator
45
60
  ```
46
61
 
47
- Or from GitHub, at a tag or any commit: `"umlimator": "github:zintaen/umlimator#v0.12.0"`. npm 12 refuses git
62
+ Or from GitHub, at a tag or any commit: `"umlimator": "github:zintaen/umlimator#v0.13.0"`. npm 12 refuses git
48
63
  dependencies unless the project opts in, so that also needs `allow-git=root` in the `.npmrc` next to `package.json`.
49
64
 
50
65
  ## Embed
@@ -85,15 +100,76 @@ function MapView({ model, onSelect }) {
85
100
  rule the model breaks, view by view.
86
101
  - `map.overlay(name, layer)`, `highlight(refs)`, `tour(path)` and `replay(spans)` put the host's marks, a tour and a
87
102
  trace on the map; `paths()`, `reach()` and `describe()` walk a view and put it in words, in Node too.
103
+ - `map.lens(id)` puts one of the model's lenses on the map. `critical(model, view, { from, to, by })` finds the slowest
104
+ or costliest path, `whatIf(model, view, { remove })` what loses its way when cards go down, and
105
+ `sequenceFromTrace(model, spans, { view, match })` keeps a trace as a numbered view; all three in Node too.
88
106
  - `editable: true` turns on editing gestures ([docs/editing.md](docs/editing.md); `examples/edit.html` is a working host).
89
107
  - `diff(before, after)` returns `after` with each card and line marked added, changed or removed, what changed in words,
90
108
  and the views that show it; `drift(model, observed)` marks the lines traffic ran on that nobody drew, and the drawn ones
91
109
  that carried nothing. Both are models to draw, in Node too. `update()` eases into the next model (`examples/timelapse.html`);
92
110
  the option `ease: false` draws it at once.
93
- - `mountPage` is the whole standalone viewer that the CLI writes.
111
+ - `mountPage` is the whole standalone viewer that the CLI writes. `render()` can draw its pieces too, each on its own or
112
+ any mix, inside the map's element; the host keeps its own header:
113
+
114
+ ```js
115
+ render(el, model, { nav: true, details: true, legend: true, hints: true });
116
+ ```
117
+
118
+ - `nav`: the views list left of the map, opened and shut by its ☰ button or O. Picking a view emits `viewchange`.
119
+ - `details`: the selected card's panel under the map, with its tabs (`[` and `]`). Its Open and Unfold here buttons
120
+ emit `open` and `expand`, as the canvas does, so a host can load a view first.
121
+ - `legend` and `hints`: the legend, and the line saying what the pointer does, over the map.
122
+
123
+ They share the page's code and look, take the `--um-*` colours (the theme adds `--um-mark` and `--um-scrim` for them),
124
+ and follow `update()` and `destroy()`. `examples/embed.html` uses them.
94
125
 
95
126
  The API, its types and the decisions behind it: [docs/module-api.md](docs/module-api.md) and `lib/umlimator.d.ts`.
96
127
 
128
+ ### In a page or a docs site, as an element
129
+
130
+ `<umlimator-map>` (`umlimator/element`) loads a model and draws it, with no script of your own. Its children show until
131
+ the map is drawn, and stay where scripts don't run ([`examples/element.html`](examples/element.html)):
132
+
133
+ ```html
134
+ <link rel="stylesheet" href="node_modules/umlimator/lib/umlimator-theme.css">
135
+ <script type="module" src="node_modules/umlimator/lib/umlimator-element.js"></script>
136
+
137
+ <umlimator-map src="bookshop.json" view="api" minimap height="520">
138
+ <img src="stills/api.png" alt="An order, step by step">
139
+ </umlimator-map>
140
+ ```
141
+
142
+ - **The model**: `src`, a URL resolved as an `<img>`'s is (a `data:` URL too), or the model in a child
143
+ `<script type="application/json">`.
144
+ - **Attributes**: `view`, `minimap`, `controls="false"`, `height` (a number is pixels; 480 by default, or size it in CSS)
145
+ and `theme` (`light` or `dark`: theme.css's palette for that map, whatever the page's). Changing one draws the map
146
+ again, and removing the element destroys it.
147
+ - **Events**: `umlimator:select`, `umlimator:open` (cancel it to stay) and `umlimator:viewchange`, the map's event as
148
+ `detail`; `load` once it is drawn, `error` when it can't be. `el.map` is the `render()` handle.
149
+ - **Colours**: theme.css, as above, or your own `--um-*` variables. The element fetches nothing but its model.
150
+ - **Dark mode**: without `theme`, the map follows the page, a docs site's dark mode included: VitePress's
151
+ `<html class="dark">` and Docusaurus's `<html data-theme="dark">`.
152
+
153
+ In a docs site, a fenced block becomes the element. It names the model's `src`, with `view` or any other attribute, or
154
+ holds the model in JSON:
155
+
156
+ ````markdown
157
+ ```umlimator
158
+ src: /maps/bookshop.json
159
+ view: api
160
+ ```
161
+ ````
162
+
163
+ - **VitePress**: in `.vitepress/config`, `markdown: { config: md => md.use(umlimator) }` (`import umlimator from
164
+ 'umlimator/markdown-it'`) and `vue: { template: { compilerOptions: { isCustomElement: t => t === 'umlimator-map' } } }`;
165
+ in `.vitepress/theme/index`, `import 'umlimator/element'` and `import 'umlimator/theme.css'`.
166
+ - **Docusaurus**: `remarkPlugins: [umlimator]` (`import umlimator from 'umlimator/remark'`) in the docs preset's options,
167
+ and a client module (`clientModules: ['./src/umlimator.js']`) that imports `umlimator/element` and
168
+ `umlimator/theme.css`.
169
+ - **MkDocs, or any site that allows HTML**: write the element itself in the Markdown. Copy `lib/umlimator.js`,
170
+ `lib/umlimator-element.js` and `lib/umlimator-theme.css` into `docs/js/`, and load them in `mkdocs.yml`:
171
+ `extra_javascript: [{ path: js/umlimator-element.js, type: module }]` and `extra_css: [js/umlimator-theme.css]`.
172
+
97
173
  ## Model
98
174
 
99
175
  A model has shared `nodes` and `edges`, and `views` that each show a subset, with optional `containers` (boxes),
@@ -111,6 +187,7 @@ kinds such as `public`, `lane`, `script`, `hook` and `board` work too). Fields b
111
187
  | node | `icon` | a logo or line icon from `icons/manifest.json` |
112
188
  | node | `noSub` | the name only; the description stays in the details panel |
113
189
  | node | `opensView`, `opensCount` | the view the card opens, which may not be loaded yet, and the ⤢ number for it |
190
+ | node | `owner` | who owns the card (a team): rules and lenses pick by it, and a lens can ring cards by owner |
114
191
  | node, edge | `edited` | a marker on the card or line |
115
192
  | edge | `kind` | what the line means, drawn as its colour: `calls` (default), `feeds`, `triggers`, `state`, `guard` |
116
193
  | edge | `weight` | 1, 2 or 3: how much the line stands for, drawn as its width |
@@ -119,14 +196,16 @@ kinds such as `public`, `lane`, `script`, `hook` and `board` work too). Fields b
119
196
  | edge | `from`, `to` | may name a container of the view: the line ends on its box |
120
197
  | edge | `style: "faded"` | the line in its kind's colour, faint and with no flow (drift's idle lines) |
121
198
  | edge | `drift` | what `drift()` found: `undrawn` or `idle` |
199
+ | edge | `latency`, `cost` | budgets, in ms and in any unit: `critical()` sums them along a path (not drawn) |
122
200
  | view | `gap`, `rgap` | the space between columns and between rows |
123
201
  | view | `keepPairs` | A→B and B→A drawn as two lines |
124
202
  | view | `repair: false` | no port repair |
125
203
  | view | `subAndChips` | cards 80 px tall that show their sub-line, then their chips |
126
204
  | view | `kindsFromLabels` | a line whose edges name no `kind` takes one guessed from its label |
127
205
  | container | `badges` | tabs on the box's border |
128
- | model | `rules` | what must hold: no line `from` some cards `to` others (picked by id, kind or box), or `noCycles` between boxes |
206
+ | model | `rules` | what must hold: no line `from` some cards `to` others (picked by id, kind, box or owner), or `noCycles` between boxes |
129
207
  | model | `stories` | walk-throughs: steps that each set the view, camera, selection, highlight, tour and caption |
208
+ | model | `lenses` | ways to look at the map: what `pick` picks (by id, kind, box, owner or `line` kind) lit, the rest dimmed; `color: "owner"` rings cards by owner |
130
209
 
131
210
  ## Command line
132
211
 
@@ -140,16 +219,38 @@ python3 umlimator.py diff --base origin/main --out diff/ # every mo
140
219
  python3 umlimator.py drift model.json traffic.json --out drift.html # drawn vs. seen (a Jaeger or Tempo dependency list)
141
220
  python3 umlimator.py timelapse model.json --out history.html # the model's versions in git history, on a slider
142
221
  python3 umlimator.py check page.html --expand # also each view with each stacked card unfolded
143
- python3 umlimator.py record bike-share.html --story ride --out ride.png # bike-share's story as an animated PNG, a frame per step
222
+ python3 umlimator.py record bike-share.html --story ride --out ride.png # bike-share's story as an animated PNG, its tours riding
223
+ python3 umlimator.py import workspace.dsl --out model.json # a diagram you have already, as a model
224
+ docker compose config --format json > compose.json && python3 umlimator.py import compose.json --out model.json
225
+ 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
227
+ 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
+ python3 umlimator.py mcp model.json # an MCP server over stdio, for an AI assistant (below)
144
229
  ```
145
230
 
146
231
  `check` also reports each rule the model breaks, and then exits 1.
147
232
 
148
- The page has breadcrumbs, a details panel per card, ⌘K search, deep links (`#view=<id>&node=<id>`, and
149
- `#story=<id>&step=<n>` for a story's step), stories, and export.
233
+ `import` reads Structurizr DSL (people, systems, containers and components, their relationships, and the workspace's
234
+ views), C4-PlantUML (its element, relationship and boundary macros), Mermaid (`flowchart` or `graph`, and `C4Context`,
235
+ `C4Container` or `C4Component`), and the JSON of `docker compose config` (services, `depends_on`, networks) and of
236
+ `kubectl get` (workloads, Services, Ingresses). It tells the format from the content, then the extension; `--format`
237
+ decides. Groups, boundaries, subgraphs, networks and namespaces become boxes; Structurizr's views stay views, a card
238
+ opening the view below it; `examples/clinic.json` is `examples/clinic.dsl`, imported. What each element becomes, and
239
+ what is left out: [docs/module-api.md § Import](docs/module-api.md#import-90). Whatever the file holds that `import`
240
+ doesn't read is listed on stderr with its line (in JSON, its path).
241
+
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.
246
+
247
+ The page has breadcrumbs, a details panel per card (with the slowest path from it and what-if), ⌘K search, deep links
248
+ (`#view=<id>&node=<id>`, `#story=<id>&step=<n>` for a story's step, and `#lens=<id>`), stories, lenses, and export. For
249
+ scripts and tests, it keeps its map's handle on `window.umlimatorMap`: the `Handle` that `render()` returns (`setView()`,
250
+ `highlight()`, `tour()`, `play()` ...).
150
251
  `stills` skips a view whose inputs have not changed since its last export (`--all` exports every view). Needs Python
151
- 3.9+ (standard library only), Google Chrome or Chromium for `stills`, `check` and `record`, and Node 18+ for `diff`
152
- and `drift`.
252
+ 3.9+ (standard library only), Google Chrome or Chromium for `stills`, `check`, `record` and `pdf`, and Node 18+ for
253
+ `diff`, `drift`, `trace` and `mcp`.
153
254
 
154
255
  `record` writes an animated PNG, which browsers and GitHub show as it is. For a GIF or an MP4 (for chat or slides),
155
256
  convert it with ffmpeg:
@@ -170,11 +271,39 @@ jobs:
170
271
  runs-on: ubuntu-latest
171
272
  steps:
172
273
  - uses: actions/checkout@v4
173
- - uses: zintaen/umlimator@v0.12.0
274
+ - uses: zintaen/umlimator@v0.13.0
174
275
  with:
175
276
  models: "docs/*.json" # optional: where the models are (default: every .json that is a map model)
176
277
  ```
177
278
 
279
+ ## An AI assistant on the map (MCP)
280
+
281
+ `umlimator.py mcp model.json` is a [Model Context Protocol](https://modelcontextprotocol.io) server over stdio: an AI
282
+ assistant reads the model, and shows you what it finds on the map in your browser. Add it to Claude Code:
283
+
284
+ ```bash
285
+ claude mcp add umlimator -- python3 /path/to/umlimator.py mcp /path/to/model.json
286
+ ```
287
+
288
+ | Tool | What it does |
289
+ |---|---|
290
+ | `list_views()` | the views (id, title, parent, number of cards) and the stories |
291
+ | `describe_view(view)` | the view in sentences (`describe()`), then each card's id |
292
+ | `paths(view, from, to)` | every card and line on a path from one card to another (`paths()`), as Refs for `highlight` |
293
+ | `reach(view, card, backward?, depth?)` | what a card leads to, or what leads to it, with the distance of each (`reach()`) |
294
+ | `broken_rules()`, `validate()` | each break of the model's `rules` (`rules()`), and its own problems (`validate()`) |
295
+ | `diff(other_model_path)` | what changed from another model file to this one, in words, and the views that show it (`diff()`) |
296
+ | `show_view(view)` | shows a view on the page |
297
+ | `highlight(refs, view?)` | lights cards and lines (their ids, or the Refs `paths` and `reach` give) and dims the rest |
298
+ | `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 |
300
+
301
+ - 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
+ - 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.
305
+ - 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
+
178
307
  ## Map a repo with Claude Code
179
308
 
180
309
  The `map-repo` skill reads a codebase, writes a C4-style model (context, containers, components), renders it to a page
@@ -203,11 +332,13 @@ python3 -I tools/fetch_icons.py icons "name:lucide/<icon>" # a line icon
203
332
  python3 umlimator.py pin-icons # records each new logo's sha256
204
333
  ```
205
334
 
335
+ With `UMLIMATOR_OFFLINE=1` the CLI fetches nothing: a logo not in the cache keeps its card's sigil (`mcp` sets it).
336
+
206
337
  ## Development
207
338
 
208
339
  ```bash
209
340
  node --test # layout() in Node: the bench views' geometry and the layout rules
210
- python3 tests/embed.py # render() in headless Chrome: events, keyboard, colours, contrast, editing
341
+ python3 tests/embed.py # render() in headless Chrome: events, keyboard, colours, contrast, editing, the element
211
342
  python3 bench/bench.py --check-determinism --expect bench/expected.json --max-ms m50=2000
212
343
  ```
213
344
 
@@ -223,9 +354,11 @@ Release with that section as its notes, and publishes the package to npm (truste
223
354
  CI also publishes the examples to GitHub Pages from `main`.
224
355
 
225
356
  ```
226
- umlimator.py CLI: render, stills, check, record, pin-icons, diff, drift, timelapse
357
+ umlimator.py CLI: render, stills, check, record, pdf, pin-icons, diff, drift, timelapse, import, trace, mcp
358
+ umlimator_import.py import's readers: Structurizr DSL, C4-PlantUML, Mermaid, compose and Kubernetes JSON
359
+ umlimator_mcp.py the MCP server that `umlimator.py mcp` runs
227
360
  action.yml the diff Action: what a pull request changes in its maps, posted on it
228
- lib/ the module (umlimator.js), its types and the default theme
361
+ lib/ the module (umlimator.js), its types, the default theme, <umlimator-map> and the docs plugins
229
362
  engine/ the page templates the CLI inlines the module into; the upstream renderer and schema
230
363
  docs/ the module's API and the editing design
231
364
  tests/, bench/ tests, benchmark models and results
@@ -0,0 +1,19 @@
1
+ // Types for lib/umlimator-element.js, `umlimator/element` (#92). Its header says what each attribute and event does.
2
+ import type { Events, Handle } from './umlimator.js';
3
+
4
+ /** <umlimator-map src view minimap controls theme height>: the model from `src` (a URL) or a child
5
+ * <script type="application/json">, drawn by render(); its other children show until the map is drawn. */
6
+ export class UmlimatorMap extends HTMLElement {
7
+ /** render()'s handle once the map is drawn (each `load` event), else null. */
8
+ readonly map: Handle | null;
9
+ }
10
+
11
+ declare global {
12
+ interface HTMLElementTagNameMap { 'umlimator-map': UmlimatorMap }
13
+ /** The map's events, re-dispatched on the element (they bubble); preventDefault() on umlimator:open keeps the view. */
14
+ interface HTMLElementEventMap {
15
+ 'umlimator:select': CustomEvent<Events['select']>;
16
+ 'umlimator:open': CustomEvent<Events['open']>;
17
+ 'umlimator:viewchange': CustomEvent<Events['viewchange']>;
18
+ }
19
+ }
@@ -0,0 +1,77 @@
1
+ /* <umlimator-map> (#92): a map in any page from HTML alone, as `umlimator/element`.
2
+
3
+ <umlimator-map src="model.json" view="system" minimap height="560">
4
+ <img src="system.png" alt="The system view">
5
+ </umlimator-map>
6
+
7
+ The model comes from `src`, a URL (a data: URL too), or from a child <script type="application/json">, and render()
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:
11
+ "light" or "dark", that map's palette from lib/umlimator-theme.css, whatever the page's; without it the map follows the
12
+ page, a docs site's dark mode too (VitePress's html.dark, Docusaurus's html[data-theme="dark"]). The colours come from
13
+ that file or the host's own --um-* variables; the element fetches nothing but its model.
14
+ 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. */
18
+ import { render } from './umlimator.js';
19
+
20
+ const Base = globalThis.HTMLElement || class {}; // importable where there is no DOM (a docs site's server render)
21
+
22
+ export class UmlimatorMap extends Base {
23
+ static observedAttributes = ['src', 'view', 'minimap', 'controls', 'height'];
24
+ map = null;
25
+ #drawn = 0; #due = false; #src = null; #model = null;
26
+
27
+ constructor() {
28
+ super();
29
+ 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
+ + '<slot></slot><div hidden style="height: 100%"></div>';
31
+ }
32
+ connectedCallback() { this.#soon(); }
33
+ disconnectedCallback() { this.#drop(); }
34
+ attributeChangedCallback(name, was, now) {
35
+ if (name === 'height') this.style.height = now == null ? '' : /^\d+(\.\d+)?$/.test(now) ? now + 'px' : now;
36
+ else this.#soon();
37
+ }
38
+ #soon() { // attributes set together draw once
39
+ if (this.#due) return;
40
+ this.#due = true;
41
+ queueMicrotask(() => { this.#due = false; if (this.isConnected) this.#draw(); });
42
+ }
43
+ #drop() {
44
+ this.map?.destroy(); this.map = null;
45
+ const [, slot, box] = this.shadowRoot.children; slot.hidden = false; box.hidden = true;
46
+ }
47
+ #fail(err) { this.#drop(); console.warn('<umlimator-map>:', err); this.dispatchEvent(new Event('error')); } // the children show again
48
+ async #draw() {
49
+ const n = ++this.#drawn, src = this.getAttribute('src'), get = a => this.getAttribute(a); // a later draw wins
50
+ let model = src && src === this.#src ? this.#model : null;
51
+ try {
52
+ if (src && !model) {
53
+ const r = await fetch(src);
54
+ if (!r.ok) throw new Error(`${src}: ${r.status} ${r.statusText}`);
55
+ model = await r.json();
56
+ } else if (!src) {
57
+ const json = this.querySelector(':scope > script[type="application/json"]');
58
+ if (!json) throw new Error('no src, and no <script type="application/json"> child');
59
+ model = JSON.parse(json.textContent);
60
+ }
61
+ } catch (err) { if (n === this.#drawn) this.#fail(err); return; }
62
+ if (n !== this.#drawn || !this.isConnected) return;
63
+ if (src) { this.#src = src; this.#model = model; }
64
+ this.#drop();
65
+ const [, slot, box] = this.shadowRoot.children; slot.hidden = true; box.hidden = false;
66
+ // the map's events, as DOM events on the element; a host that cancels umlimator:open cancels the open
67
+ const on = Object.fromEntries(['select', 'open', 'viewchange'].map(t => [t, e => {
68
+ if (!this.dispatchEvent(new CustomEvent('umlimator:' + t, { detail: e, bubbles: true, composed: true, cancelable: t === 'open' }))) e.preventDefault();
69
+ }]));
70
+ try {
71
+ this.map = render(box, model, { view: get('view') || undefined, minimap: this.hasAttribute('minimap') && get('minimap') !== 'false', controls: get('controls') !== 'false', on });
72
+ } catch (err) { return this.#fail(err); }
73
+ this.dispatchEvent(new Event('load'));
74
+ }
75
+ }
76
+
77
+ if (globalThis.customElements && !customElements.get('umlimator-map')) customElements.define('umlimator-map', UmlimatorMap);
@@ -0,0 +1,6 @@
1
+ // Types for lib/umlimator-markdown-it.js, `umlimator/markdown-it` (#92).
2
+ /** A markdown-it plugin: each ```umlimator fenced block becomes a <umlimator-map> element. md.use(umlimator) */
3
+ export default function umlimator(md: any): void;
4
+ /** A block's text as the element's attributes (src, view, minimap, controls, theme, height), an inline model as a data:
5
+ * URL in src. Throws on a block the element can't use. */
6
+ export function attrs(text: string): Record<string, string>;
@@ -0,0 +1,33 @@
1
+ /* A markdown-it plugin (VitePress, or any markdown-it), as `umlimator/markdown-it` (#92): a ```umlimator fenced block
2
+ becomes a <umlimator-map> element, which `umlimator/element` draws in the browser. md.use(umlimator)
3
+
4
+ ```umlimator
5
+ src: /maps/shop.json
6
+ view: api
7
+ ```
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'];
14
+
15
+ /** A block's text as the element's attributes. */
16
+ export function attrs(text) {
17
+ const at = text.search(/^\s*\{/m), out = {};
18
+ for (const line of (at < 0 ? text : text.slice(0, at)).split('\n').filter(l => l.trim())) {
19
+ const m = line.match(/^\s*([\w-]+)\s*:\s*(.*?)\s*$/);
20
+ if (!m || !KEYS.includes(m[1])) throw new Error(`umlimator block: "${line.trim()}" is not one of ${KEYS.map(k => k + ':').join(' ')} or a model in JSON`);
21
+ out[m[1]] = m[2];
22
+ }
23
+ if ((at >= 0) === !!out.src) throw new Error('umlimator block: give either src: (a model\'s URL) or a model in JSON');
24
+ if (at >= 0) out.src = 'data:application/json,' + encodeURIComponent(JSON.stringify(JSON.parse(text.slice(at)))); // minified; a broken model throws
25
+ return out;
26
+ }
27
+
28
+ const html = text => `<umlimator-map${Object.entries(attrs(text)).map(([k, v]) => ` ${k}="${v.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;')}"`).join('')}></umlimator-map>\n`;
29
+
30
+ export default function umlimator(md) {
31
+ const fence = md.renderer.rules.fence;
32
+ md.renderer.rules.fence = (tokens, i, ...rest) => tokens[i].info.trim().split(/\s+/)[0] === 'umlimator' ? html(tokens[i].content) : fence(tokens, i, ...rest);
33
+ }
@@ -0,0 +1,4 @@
1
+ // Types for lib/umlimator-remark.js, `umlimator/remark` (#92).
2
+ /** A remark plugin: each ```umlimator fenced block becomes a <umlimator-map> element (a node with data.hName, which MDX
3
+ * and remark-rehype draw as it). remarkPlugins: [umlimator] */
4
+ export default function umlimator(): (tree: any) => void;
@@ -0,0 +1,13 @@
1
+ /* A remark plugin (Docusaurus, MDX, or any remark-rehype pipeline), as `umlimator/remark` (#92): a ```umlimator fenced
2
+ block becomes a <umlimator-map> element, with the attributes umlimator/markdown-it gives it (its header says what a
3
+ block holds). The block's node becomes one that names the element (data.hName, data.hProperties), which MDX and
4
+ remark-rehype both turn into it. remarkPlugins: [umlimator] */
5
+ import { attrs } from './umlimator-markdown-it.js';
6
+
7
+ export default function umlimator() {
8
+ const walk = node => node.children?.forEach((c, i) => {
9
+ if (c.type === 'code' && c.lang === 'umlimator') node.children[i] = { type: 'umlimator', data: { hName: 'umlimator-map', hProperties: attrs(c.value) }, children: [] };
10
+ else walk(c);
11
+ });
12
+ return walk;
13
+ }
@@ -1,4 +1,6 @@
1
- /* umlimator's default palette and fonts: light, and dark when the system asks or <html data-theme="dark">.
1
+ /* umlimator's default palette and fonts: light, and dark when the system asks or <html data-theme="dark">; a
2
+ <umlimator-map theme="light|dark"> takes that palette whatever the page's, and one without a theme is dark in a docs
3
+ site's dark mode too (VitePress's <html class="dark">, Docusaurus's <html data-theme="dark">).
2
4
  The canvas reads only these --um-* variables (it bakes in no colour and loads no font), so a host may use this file,
3
5
  set the variables itself (e.g. --um-bg: var(--surface)), or switch light and dark by changing them on any ancestor
4
6
  of the map. Fonts are the host's: --um-sans and --um-mono name families the page already has; nothing is fetched.
@@ -6,7 +8,7 @@
6
8
  The tints (--um-<tone>-f) below are for page chrome: the map mixes its own from the tones and --um-panel in effect
7
9
  where it is, so setting a tone on any ancestor of the map recolours its tint too. */
8
10
  /* Clean flowchart (GitHub Primer-like): light first, dark when the system or the toggle asks. */
9
- :root {
11
+ :root, umlimator-map[theme="light"] {
10
12
  --um-stage-bg: #fbfcfd; --um-logo-tile: #ffffff; --um-logo-tile-dark: #1b1f23; --um-intake-mark: #1b1f23; --um-edited: #bf8700;
11
13
  --um-bg: #ffffff; --um-bg-2: #f6f8fa; --um-panel: #ffffff; --um-panel-2: #f6f8fa; --um-border: #d8dee4; --um-border-2: #d1d9e0;
12
14
  --um-text: #1f2328; --um-muted: #59636e; --um-dim: #818b98; --um-grid: rgba(31,35,40,.06); --um-mask: #ffffff; --um-arrow: #8c959f;
@@ -16,6 +18,7 @@
16
18
  --um-k-public: #0969da; --um-k-lane: #0f766e; --um-k-ext: #8250df; --um-k-verb: #bf3989; --um-k-script: #6e7781; --um-k-hook: #cf222e; --um-k-board: #9a6700;
17
19
  --um-w-calls: #424a53; --um-w-feeds: #0969da; --um-w-triggers: #8250df; --um-w-state: #868f99; --um-w-guard: #cf222e;
18
20
  --um-add-bg: rgba(26,127,55,.10); --um-add-tx: #1a7f37; --um-del-bg: rgba(207,34,46,.08); --um-del-tx: #cf222e;
21
+ --um-mark: #fff1a6; --um-scrim: rgba(15,18,22,.72); /* the details panel's ==highlighter==, and the dark behind an example shown full size */
19
22
  color-scheme: light;
20
23
  --um-accent-f: color-mix(in srgb, var(--um-accent) 12%, var(--um-panel));
21
24
  --um-cyan-f: color-mix(in srgb, var(--um-cyan) 9%, var(--um-panel));
@@ -46,11 +49,12 @@
46
49
  --um-cyan: #4493f8; --um-green: #3fb950; --um-violet: #ab7df8; --um-amber: #d29922; --um-rose: #f85149; --um-orange: #db6d28; --um-slate: #9198a1; --um-fuchsia: #db61a2;
47
50
  --um-k-public: #4493f8; --um-k-lane: #2dd4bf; --um-k-ext: #ab7df8; --um-k-verb: #f778ba; --um-k-script: #9198a1; --um-k-hook: #f85149; --um-k-board: #d29922;
48
51
  --um-w-calls: #b7bdc8; --um-w-feeds: #4493f8; --um-w-triggers: #ab7df8; --um-w-state: #59626e; --um-w-guard: #f85149;
49
- --um-add-bg: rgba(63,185,80,.13); --um-add-tx: #56d364; --um-del-bg: rgba(248,81,73,.12); --um-del-tx: #ff7b72;
52
+ --um-add-bg: rgba(63,185,80,.13); --um-add-tx: #56d364; --um-del-bg: rgba(248,81,73,.12); --um-del-tx: #ff7b72; --um-mark: rgba(250,204,21,.32);
50
53
  color-scheme: dark;
51
54
  }
52
55
  }
53
- :root[data-theme="dark"] {
56
+ /* the docs sites' dark mode: :where() weighs nothing, so a host's own --um-* on the element still win */
57
+ :root[data-theme="dark"], umlimator-map[theme="dark"], :where(html.dark, html[data-theme="dark"]) :where(umlimator-map:not([theme])) {
54
58
  --um-stage-bg: var(--um-bg-2); --um-edited: #d29922;
55
59
  --um-bg: #0d1117; --um-bg-2: #010409; --um-panel: #0d1117; --um-panel-2: #151b23; --um-border: #262c36; --um-border-2: #3d444d;
56
60
  --um-text: #f0f6fc; --um-muted: #9198a1; --um-dim: #656c76; --um-grid: rgba(240,246,252,.05); --um-mask: #0d1117; --um-arrow: #656c76;
@@ -59,6 +63,6 @@
59
63
  --um-cyan: #4493f8; --um-green: #3fb950; --um-violet: #ab7df8; --um-amber: #d29922; --um-rose: #f85149; --um-orange: #db6d28; --um-slate: #9198a1; --um-fuchsia: #db61a2;
60
64
  --um-k-public: #4493f8; --um-k-lane: #2dd4bf; --um-k-ext: #ab7df8; --um-k-verb: #f778ba; --um-k-script: #9198a1; --um-k-hook: #f85149; --um-k-board: #d29922;
61
65
  --um-w-calls: #b7bdc8; --um-w-feeds: #4493f8; --um-w-triggers: #ab7df8; --um-w-state: #59626e; --um-w-guard: #f85149;
62
- --um-add-bg: rgba(63,185,80,.13); --um-add-tx: #56d364; --um-del-bg: rgba(248,81,73,.12); --um-del-tx: #ff7b72;
66
+ --um-add-bg: rgba(63,185,80,.13); --um-add-tx: #56d364; --um-del-bg: rgba(248,81,73,.12); --um-del-tx: #ff7b72; --um-mark: rgba(250,204,21,.32);
63
67
  color-scheme: dark;
64
68
  }
@@ -13,6 +13,8 @@ export interface MapModel {
13
13
  rules?: Rule[];
14
14
  /** Narrated walk-throughs of the map (#69): handle.play(id), the standalone page's Stories menu, umlimator.py record. */
15
15
  stories?: Story[];
16
+ /** Saved ways to look at the map (#94): handle.lens(id), the standalone page's Lenses menu and #lens=<id>. */
17
+ lenses?: Lens[];
16
18
  [key: string]: unknown;
17
19
  }
18
20
  /** A badge: its text, and optionally a palette name for its colour (green, amber, rose, accent, violet ... : --um-<tone>). */
@@ -25,6 +27,8 @@ export interface MapNode {
25
27
  opensView?: string;
26
28
  /** The number on the ⤢ badge of a card whose view is not loaded yet (else ⤢ alone). A loaded view's cards are counted. */
27
29
  opensCount?: number;
30
+ /** Who owns the card (a team, say): selectors pick by it, and a lens with color 'owner' rings the card in its owner's tone. */
31
+ owner?: string;
28
32
  [key: string]: unknown;
29
33
  }
30
34
  /** What a line means, drawn as its colour: calls (the default), feeds (hands work on), triggers (suggests, offers, fires),
@@ -52,6 +56,10 @@ export interface MapEdge {
52
56
  change?: 'added' | 'removed' | 'changed';
53
57
  /** What drift() found: undrawn (traffic ran on it, nobody drew it) or idle (drawn, carried nothing). */
54
58
  drift?: 'undrawn' | 'idle';
59
+ /** Budgets (#95), which critical() sums along a path: how long the call takes, in ms, and what it costs, in any unit.
60
+ * Not drawn. */
61
+ latency?: number;
62
+ cost?: number;
55
63
  [key: string]: unknown;
56
64
  }
57
65
  export interface MapView {
@@ -115,6 +123,20 @@ export interface RenderOptions {
115
123
  /** update() eases into the next model: moved cards glide, new ones fade in, removed ones fade out (#67). Default true;
116
124
  * never under prefers-reduced-motion. false draws the next model at once. */
117
125
  ease?: boolean;
126
+ /** The standalone page's pieces (#99), each on its own or any mix, all off by default. They are drawn inside the map's
127
+ * element, around the canvas, with the page's look, keys and --um-* colours; update() and destroy() keep them in step.
128
+ * nav: the views list (Miller columns) left of the map, opened and shut by its ☰ button or O; picking a view shows it
129
+ * (viewchange). It comes after the map in the tab order, so focus given to the map's element still lands on the map. */
130
+ nav?: boolean;
131
+ /** The kinds of cards and lines the view draws, in a box over the map, bottom left, that folds away. */
132
+ legend?: boolean;
133
+ /** A line over the map's foot: what the pointer does, and for a while what just happened (a polite live region). */
134
+ hints?: boolean;
135
+ /** The selected card's details under the map: its kind, actions and tabs (when, what, how, an example, what it comes
136
+ * from and leads to), [ and ] between tabs. Open and Unfold here emit open and expand as the canvas does; → ← ⇄ light
137
+ * what the card leads to, what leads to it, and every path between two selected cards (> < P); ⏱ ¤ the slowest and the
138
+ * costliest path where the view's lines have latency or cost, and ⊘ what loses its way if the card goes down (#95). */
139
+ details?: boolean;
118
140
  on?: { [K in keyof Events]?: (e: Events[K]) => void };
119
141
  }
120
142
 
@@ -143,8 +165,9 @@ export interface Handle {
143
165
  on<K extends keyof Events>(type: K, fn: (e: Events[K]) => void): () => void;
144
166
  /** Unfolds a stacked card of the view shown into a box in place (#71): its inner view's own cards inside, this view's
145
167
  * lines to it moved onto them, the rest of the picture in its order. In this handle only, never in the model; kept per
146
- * view across update(). False when it can't: no loaded inner view, nothing of its own inside, a numbered (sequence)
147
- * view in or opened, an editable map. No expand event: that is for gestures. */
168
+ * view across update(). A numbered (sequence) inner view keeps its run order: numbers, loops back, lanes (#82). False
169
+ * when it can't: no loaded inner view, nothing of its own inside, a card on a numbered view, an editable map. No
170
+ * expand event: that is for gestures. */
148
171
  expand(id: string): boolean;
149
172
  /** Folds an unfolded card back (and anything unfolded inside it). False when it was not unfolded. */
150
173
  collapse(id: string): boolean;
@@ -159,6 +182,11 @@ export interface Handle {
159
182
  /** The 'highlight' layer: these cards and lines lit, the rest dimmed (in place of the selection's dimming), or null to clear.
160
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. */
161
184
  highlight(refs: Lit[] | null): void;
185
+ /** 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
+ * rest, on every view shown, and the view is not laid out again. With color 'owner', each lit card is ringed in its
187
+ * owner's tone, and a legend over the controls names them. It stays across update() and setView(); a model without the
188
+ * lens takes it off. False when the model has no lens `id`. */
189
+ lens(id: string | null): boolean;
162
190
  /** The tour dot rides the line between each pair of cards in turn (either way), through each card to its next line; a card
163
191
  * it reaches flashes, and a pair with no line between them is skipped. Resolves true at the end, false when cut short (a
164
192
  * new tour or replay, tour(null), a new picture of the view, destroy()). With reduced motion it resolves at once.
@@ -195,8 +223,9 @@ export interface StoryPlayer {
195
223
 
196
224
  /** A mark on a card. tone: a palette name (--um-<tone>), drawn as a ring; badge: a tab after the card's own; level: 0 to 1. */
197
225
  export interface CardMark { tone?: string; badge?: Badge; pulse?: boolean; level?: number }
198
- /** A mark on a line. speed: its flow's speed, as a factor (0 stops it); tone: a glow under it; label: a pill under its own;
199
- * dash: drawn dashed; level: 0 to 1. A drawn line that stands for several edges takes the strongest of their marks. */
226
+ /** A mark on a line. speed: its flow's speed, as a factor (0 stops it); tone: a glow under it; label: a pill under its own,
227
+ * or along the line where it covers nothing (with no room, in the line's tooltip and name instead); dash: drawn dashed;
228
+ * level: 0 to 1. A drawn line that stands for several edges takes the strongest of their marks. */
200
229
  export interface LineMark { tone?: string; speed?: number; pulse?: boolean; label?: string; level?: number; dash?: boolean }
201
230
  /** A layer of marks: cards by node id, lines by edge id ("<from>-><to>#<n>" for an edge without one, as Refs name it).
202
231
  * dim: what the layer doesn't name fades. */
@@ -222,7 +251,8 @@ export function render(container: HTMLElement, model: MapModel, options?: Render
222
251
  export interface History { readonly model: MapModel; readonly canUndo: boolean; readonly canRedo: boolean; push(m: MapModel): MapModel; undo(): MapModel; redo(): MapModel }
223
252
  export function history(model: MapModel, limit?: number): History;
224
253
  /** expand: cards of the view unfolded in place (#71), as handle.expand() draws them; the geometry's `unfolded` lists those
225
- * that did (one inside another after it), and its `families` the boxes they became (`unfolds`: the inner view). */
254
+ * that did (one inside another after it), and its `families` the boxes they became (`unfolds`: the inner view). A
255
+ * numbered inner view's numbers are its lines' and cards' `step`, and its lanes are boxes with `lane: true` (#82). */
226
256
  export function layout(model: MapModel, viewId: string, options?: { repair?: boolean; measure?: (text: string, px: number) => number; expand?: string[] }): Geometry & { unfolded?: string[] };
227
257
  /** A model's own problem. path says where: "views[2].nodeIds[4]". Errors draw as holes or not at all (missing,
228
258
  * repeated or dangling ids, a box inside itself); warnings draw other than they say (an unknown kind, weight or side,
@@ -236,9 +266,16 @@ export interface CheckIssue { message: string; targets: Ref[] }
236
266
  * it measures in the DOM (text overflowing its card, titles, edge labels on cards or each other). In Node, text widths
237
267
  * are an estimate unless `measure` is given, so the layout, and what is found, can differ from a browser's. */
238
268
  export function check(model: MapModel, viewId: string, options?: { repair?: boolean; measure?: (text: string, px: number) => number; expand?: string[] }): CheckIssue[];
239
- /** Picks cards by id, kind (as the map draws it: "db" picks databases) or box (at any depth), each a value or a list;
240
- * every key given must hold, and `not` turns a selector round. A line's end on a box is that box: in it, of no kind. */
241
- export interface Selector { id?: string | string[]; kind?: string | string[]; container?: string | string[]; not?: Selector }
269
+ /** Picks cards by id, kind (as the map draws it: "db" picks databases), box (at any depth) or owner, each a value or a
270
+ * list; every key given must hold, and `not` turns a selector round. A line's end on a box is that box: in it, of no kind
271
+ * and no owner. */
272
+ export interface Selector { id?: string | string[]; kind?: string | string[]; container?: string | string[]; owner?: string | string[]; not?: Selector }
273
+ /** A lens's pick: a Selector, and `line`, line kinds. Without `line` it lights the cards it picks and the lines between two
274
+ * of them; with `line`, the lines of those kinds between two cards the rest of it picks, and their ends. */
275
+ export interface LensSelector extends Selector { line?: WireKind | WireKind[] }
276
+ /** A saved way to look at the map (#94): what `pick` picks lit, the rest dimmed (a pick left out lights everything);
277
+ * color 'owner' rings each lit card in its owner's tone, with a legend. */
278
+ export interface Lens { id: string; title?: string; pick?: LensSelector; color?: 'owner' }
242
279
  /** What the model says must hold (`model.rules`): no line from the cards `from` picks to the cards `to` picks (a side left
243
280
  * out picks every card), or, with noCycles, no boxes that reach each other through their lines (a line counts between
244
281
  * the two boxes that hold its ends right under the box they share). A rule that names a box (a container, or a box's
@@ -258,6 +295,20 @@ export function reach(model: MapModel, viewId: string, from: string, options?: {
258
295
  export function describe(model: MapModel, viewId: string): string;
259
296
  /** The spans of an OpenTelemetry trace in OTLP JSON (resourceSpans), as replay() takes them. */
260
297
  export function otlpSpans(otlp: unknown): Span[];
298
+ /** The path along a view's lines with the highest total of `by` (#95): latency (ms, the default) or cost, summed over its
299
+ * lines (a line without one counts 0), from `from` to `to` (either left out: any card), never coming back to a card. Its
300
+ * cards in order, then its lines, as Refs for highlight(); no refs and 0 when no line leads there. No browser needed. */
301
+ export function critical(model: MapModel, viewId: string, options?: { from?: string; to?: string; by?: 'latency' | 'cost' }): { refs: Ref[]; total: number };
302
+ /** What taking cards out of a view would cost (#95), with the model left as it is. unreachable: what loses its way to them
303
+ * (backward, as reach() finds it: for calls, what depends on them), each with its distance from the nearest, the cards
304
+ * themselves at 0. No rule breaks: rules only forbid lines and cycles, which taking cards out never adds. No browser needed. */
305
+ export function whatIf(model: MapModel, viewId: string, options: { remove: string[] }): { unreachable: (Ref & { distance: number })[] };
306
+ /** A trace kept as a numbered view (#96): `model` with a sequence view (render: "sequence") whose steps are the trace's calls
307
+ * between cards (a span and its parent's), in time order, each line labelled with its span's name and its duration under
308
+ * it. A span's service is a card as replay() matches it (match, else id, else label in any case); the caption names the
309
+ * services with no card. The view (id `view`, default "trace") replaces one with that id, else comes last, under the
310
+ * view that shows the most of its cards. The model passed in is not changed. */
311
+ export function sequenceFromTrace(model: MapModel, spans: Span[], options?: { view?: string; match?: Record<string, string>; title?: string }): MapModel;
261
312
  /** What changed from one model to the next (#66), as one model to draw: `after`, with each card marked added, changed or
262
313
  * removed (`change`: a NEW or CHANGED tab; REMOVED is faded, dashed and struck through) and each line likewise (`style`
263
314
  * added or removed; a changed one `edited`); what left comes back in its views, boxes and places, and a card put into