umlimator 0.12.1 → 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 +167 -18
- package/lib/umlimator-element.d.ts +21 -0
- package/lib/umlimator-element.js +89 -0
- package/lib/umlimator-markdown-it.d.ts +6 -0
- package/lib/umlimator-markdown-it.js +33 -0
- package/lib/umlimator-remark.d.ts +4 -0
- package/lib/umlimator-remark.js +13 -0
- package/lib/umlimator-theme.css +9 -5
- package/lib/umlimator.d.ts +118 -13
- package/lib/umlimator.js +1485 -532
- package/lib/umlimator.schema.json +164 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -18,25 +18,41 @@ 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,
|
|
22
|
-
that never run through a card, room for every label, containers kept apart, and cards with
|
|
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 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`).
|
|
44
|
+
- **In docs**: `<umlimator-map src="model.json">` in any page, and an `umlimator` fenced block in VitePress, Docusaurus,
|
|
45
|
+
MkDocs or any Markdown that allows HTML.
|
|
35
46
|
- **A layout check**: crossings, shared lines, detours, labels on lines, text that overflows its card.
|
|
36
47
|
- **Rules**: what the model says must hold ("apps never call a database directly", "no cycles between teams' boxes"),
|
|
37
48
|
marked on the map where it breaks; `umlimator.py check` fails on a broken rule, so CI does.
|
|
38
49
|
- **Change**: what a pull request changes, marked on the map and said in words (with a GitHub Action that posts it);
|
|
39
50
|
a time-lapse of a model's history, where cards glide, fade in and fade out; and drift, the map against the traffic seen.
|
|
51
|
+
- **Import**: a Structurizr workspace, a C4-PlantUML or Mermaid diagram, or what `docker compose config` and `kubectl get`
|
|
52
|
+
print as JSON, read into a model (`umlimator.py import`), with what it can't read listed line by line.
|
|
53
|
+
- **An AI assistant on the map**: an MCP server (`umlimator.py mcp`) through which an assistant such as Claude Code reads
|
|
54
|
+
the model (a view in words, paths, what a card reaches, broken rules, problems, a diff) and drives its page in your
|
|
55
|
+
browser (a view, a highlight, a tour, a story).
|
|
40
56
|
|
|
41
57
|
## Install
|
|
42
58
|
|
|
@@ -44,7 +60,7 @@ drill-down, a data pipeline, an AI agent's skills, and host pages for embedding,
|
|
|
44
60
|
npm install umlimator
|
|
45
61
|
```
|
|
46
62
|
|
|
47
|
-
Or from GitHub, at a tag or any commit: `"umlimator": "github:zintaen/umlimator#v0.
|
|
63
|
+
Or from GitHub, at a tag or any commit: `"umlimator": "github:zintaen/umlimator#v0.14.0"`. npm 12 refuses git
|
|
48
64
|
dependencies unless the project opts in, so that also needs `allow-git=root` in the `.npmrc` next to `package.json`.
|
|
49
65
|
|
|
50
66
|
## Embed
|
|
@@ -85,15 +101,77 @@ function MapView({ model, onSelect }) {
|
|
|
85
101
|
rule the model breaks, view by view.
|
|
86
102
|
- `map.overlay(name, layer)`, `highlight(refs)`, `tour(path)` and `replay(spans)` put the host's marks, a tour and a
|
|
87
103
|
trace on the map; `paths()`, `reach()` and `describe()` walk a view and put it in words, in Node too.
|
|
104
|
+
- `map.lens(id)` puts one of the model's lenses on the map. `critical(model, view, { from, to, by })` finds the slowest
|
|
105
|
+
or costliest path, `whatIf(model, view, { remove })` what loses its way when cards go down, and
|
|
106
|
+
`sequenceFromTrace(model, spans, { view, match })` keeps a trace as a numbered view; all three in Node too.
|
|
88
107
|
- `editable: true` turns on editing gestures ([docs/editing.md](docs/editing.md); `examples/edit.html` is a working host).
|
|
89
108
|
- `diff(before, after)` returns `after` with each card and line marked added, changed or removed, what changed in words,
|
|
90
109
|
and the views that show it; `drift(model, observed)` marks the lines traffic ran on that nobody drew, and the drawn ones
|
|
91
110
|
that carried nothing. Both are models to draw, in Node too. `update()` eases into the next model (`examples/timelapse.html`);
|
|
92
111
|
the option `ease: false` draws it at once.
|
|
93
|
-
- `mountPage` is the whole standalone viewer that the CLI writes.
|
|
112
|
+
- `mountPage` is the whole standalone viewer that the CLI writes. `render()` can draw its pieces too, each on its own or
|
|
113
|
+
any mix, inside the map's element; the host keeps its own header:
|
|
114
|
+
|
|
115
|
+
```js
|
|
116
|
+
render(el, model, { nav: true, details: true, legend: true, hints: true });
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
- `nav`: the views list left of the map, opened and shut by its ☰ button or O. Picking a view emits `viewchange`.
|
|
120
|
+
- `details`: the selected card's panel under the map, with its tabs (`[` and `]`). Its Open and Unfold here buttons
|
|
121
|
+
emit `open` and `expand`, as the canvas does, so a host can load a view first.
|
|
122
|
+
- `legend` and `hints`: the legend, and the line saying what the pointer does, over the map.
|
|
123
|
+
|
|
124
|
+
They share the page's code and look, take the `--um-*` colours (the theme adds `--um-mark` and `--um-scrim` for them),
|
|
125
|
+
and follow `update()` and `destroy()`. `examples/embed.html` uses them.
|
|
94
126
|
|
|
95
127
|
The API, its types and the decisions behind it: [docs/module-api.md](docs/module-api.md) and `lib/umlimator.d.ts`.
|
|
96
128
|
|
|
129
|
+
### In a page or a docs site, as an element
|
|
130
|
+
|
|
131
|
+
`<umlimator-map>` (`umlimator/element`) loads a model and draws it, with no script of your own. Its children show until
|
|
132
|
+
the map is drawn, and stay where scripts don't run ([`examples/element.html`](examples/element.html)):
|
|
133
|
+
|
|
134
|
+
```html
|
|
135
|
+
<link rel="stylesheet" href="node_modules/umlimator/lib/umlimator-theme.css">
|
|
136
|
+
<script type="module" src="node_modules/umlimator/lib/umlimator-element.js"></script>
|
|
137
|
+
|
|
138
|
+
<umlimator-map src="bookshop.json" view="api" minimap height="520">
|
|
139
|
+
<img src="stills/api.png" alt="An order, step by step">
|
|
140
|
+
</umlimator-map>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
- **The model**: `src`, a URL resolved as an `<img>`'s is (a `data:` URL too), or the model in a child
|
|
144
|
+
`<script type="application/json">`.
|
|
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)
|
|
147
|
+
and `theme` (`light` or `dark`: theme.css's palette for that map, whatever the page's). Changing one draws the map
|
|
148
|
+
again, and removing the element destroys it.
|
|
149
|
+
- **Events**: `umlimator:select`, `umlimator:open` (cancel it to stay) and `umlimator:viewchange`, the map's event as
|
|
150
|
+
`detail`; `load` once it is drawn, `error` when it can't be. `el.map` is the `render()` handle.
|
|
151
|
+
- **Colours**: theme.css, as above, or your own `--um-*` variables. The element fetches nothing but its model.
|
|
152
|
+
- **Dark mode**: without `theme`, the map follows the page, a docs site's dark mode included: VitePress's
|
|
153
|
+
`<html class="dark">` and Docusaurus's `<html data-theme="dark">`.
|
|
154
|
+
|
|
155
|
+
In a docs site, a fenced block becomes the element. It names the model's `src`, with `view` or any other attribute, or
|
|
156
|
+
holds the model in JSON:
|
|
157
|
+
|
|
158
|
+
````markdown
|
|
159
|
+
```umlimator
|
|
160
|
+
src: /maps/bookshop.json
|
|
161
|
+
view: api
|
|
162
|
+
```
|
|
163
|
+
````
|
|
164
|
+
|
|
165
|
+
- **VitePress**: in `.vitepress/config`, `markdown: { config: md => md.use(umlimator) }` (`import umlimator from
|
|
166
|
+
'umlimator/markdown-it'`) and `vue: { template: { compilerOptions: { isCustomElement: t => t === 'umlimator-map' } } }`;
|
|
167
|
+
in `.vitepress/theme/index`, `import 'umlimator/element'` and `import 'umlimator/theme.css'`.
|
|
168
|
+
- **Docusaurus**: `remarkPlugins: [umlimator]` (`import umlimator from 'umlimator/remark'`) in the docs preset's options,
|
|
169
|
+
and a client module (`clientModules: ['./src/umlimator.js']`) that imports `umlimator/element` and
|
|
170
|
+
`umlimator/theme.css`.
|
|
171
|
+
- **MkDocs, or any site that allows HTML**: write the element itself in the Markdown. Copy `lib/umlimator.js`,
|
|
172
|
+
`lib/umlimator-element.js` and `lib/umlimator-theme.css` into `docs/js/`, and load them in `mkdocs.yml`:
|
|
173
|
+
`extra_javascript: [{ path: js/umlimator-element.js, type: module }]` and `extra_css: [js/umlimator-theme.css]`.
|
|
174
|
+
|
|
97
175
|
## Model
|
|
98
176
|
|
|
99
177
|
A model has shared `nodes` and `edges`, and `views` that each show a subset, with optional `containers` (boxes),
|
|
@@ -111,6 +189,7 @@ kinds such as `public`, `lane`, `script`, `hook` and `board` work too). Fields b
|
|
|
111
189
|
| node | `icon` | a logo or line icon from `icons/manifest.json` |
|
|
112
190
|
| node | `noSub` | the name only; the description stays in the details panel |
|
|
113
191
|
| node | `opensView`, `opensCount` | the view the card opens, which may not be loaded yet, and the ⤢ number for it |
|
|
192
|
+
| node | `owner` | who owns the card (a team): rules and lenses pick by it, and a lens can ring cards by owner |
|
|
114
193
|
| node, edge | `edited` | a marker on the card or line |
|
|
115
194
|
| edge | `kind` | what the line means, drawn as its colour: `calls` (default), `feeds`, `triggers`, `state`, `guard` |
|
|
116
195
|
| edge | `weight` | 1, 2 or 3: how much the line stands for, drawn as its width |
|
|
@@ -119,14 +198,16 @@ kinds such as `public`, `lane`, `script`, `hook` and `board` work too). Fields b
|
|
|
119
198
|
| edge | `from`, `to` | may name a container of the view: the line ends on its box |
|
|
120
199
|
| edge | `style: "faded"` | the line in its kind's colour, faint and with no flow (drift's idle lines) |
|
|
121
200
|
| edge | `drift` | what `drift()` found: `undrawn` or `idle` |
|
|
201
|
+
| edge | `latency`, `cost` | budgets, in ms and in any unit: `critical()` sums them along a path (not drawn) |
|
|
122
202
|
| view | `gap`, `rgap` | the space between columns and between rows |
|
|
123
203
|
| view | `keepPairs` | A→B and B→A drawn as two lines |
|
|
124
204
|
| view | `repair: false` | no port repair |
|
|
125
205
|
| view | `subAndChips` | cards 80 px tall that show their sub-line, then their chips |
|
|
126
206
|
| view | `kindsFromLabels` | a line whose edges name no `kind` takes one guessed from its label |
|
|
127
207
|
| 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
|
|
208
|
+
| model | `rules` | what must hold: no line `from` some cards `to` others (picked by id, kind, box or owner), or `noCycles` between boxes |
|
|
129
209
|
| model | `stories` | walk-throughs: steps that each set the view, camera, selection, highlight, tour and caption |
|
|
210
|
+
| 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
211
|
|
|
131
212
|
## Command line
|
|
132
213
|
|
|
@@ -141,15 +222,43 @@ python3 umlimator.py drift model.json traffic.json --out drift.html # drawn vs
|
|
|
141
222
|
python3 umlimator.py timelapse model.json --out history.html # the model's versions in git history, on a slider
|
|
142
223
|
python3 umlimator.py check page.html --expand # also each view with each stacked card unfolded
|
|
143
224
|
python3 umlimator.py record bike-share.html --story ride --out ride.png # bike-share's story as an animated PNG, its tours riding
|
|
225
|
+
python3 umlimator.py import workspace.dsl --out model.json # a diagram you have already, as a model
|
|
226
|
+
docker compose config --format json > compose.json && python3 umlimator.py import compose.json --out model.json
|
|
227
|
+
kubectl get deploy,sts,svc,ing -A -o json > cluster.json && python3 umlimator.py import cluster.json --out model.json
|
|
228
|
+
python3 umlimator.py pdf page.html --out map.pdf # every view in one PDF: a cover, each view with its tables, the rules
|
|
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
|
|
230
|
+
python3 umlimator.py mcp model.json # an MCP server over stdio, for an AI assistant (below)
|
|
144
231
|
```
|
|
145
232
|
|
|
146
233
|
`check` also reports each rule the model breaks, and then exits 1.
|
|
147
234
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
`
|
|
151
|
-
|
|
152
|
-
and
|
|
235
|
+
`import` reads Structurizr DSL (people, systems, containers and components, their relationships, and the workspace's
|
|
236
|
+
views), C4-PlantUML (its element, relationship and boundary macros), Mermaid (`flowchart` or `graph`, and `C4Context`,
|
|
237
|
+
`C4Container` or `C4Component`), and the JSON of `docker compose config` (services, `depends_on`, networks) and of
|
|
238
|
+
`kubectl get` (workloads, Services, Ingresses). It tells the format from the content, then the extension; `--format`
|
|
239
|
+
decides. Groups, boundaries, subgraphs, networks and namespaces become boxes; Structurizr's views stay views, a card
|
|
240
|
+
opening the view below it; `examples/clinic.json` is `examples/clinic.dsl`, imported. What each element becomes, and
|
|
241
|
+
what is left out: [docs/module-api.md § Import](docs/module-api.md#import-90). Whatever the file holds that `import`
|
|
242
|
+
doesn't read is listed on stderr with its line (in JSON, its path).
|
|
243
|
+
|
|
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).
|
|
252
|
+
|
|
253
|
+
The page has breadcrumbs, a details panel per card (with the slowest path from it and what-if), ⌘K search, deep links
|
|
254
|
+
(`#view=<id>&node=<id>`, `#story=<id>&step=<n>` for a story's step, and `#lens=<id>`), stories, lenses, and export. For
|
|
255
|
+
scripts and tests, it keeps its map's handle on `window.umlimatorMap`: the `Handle` that `render()` returns (`setView()`,
|
|
256
|
+
`highlight()`, `tour()`, `play()` ...).
|
|
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
|
|
260
|
+
3.9+ (standard library only), Google Chrome or Chromium for `stills`, `check`, `record` and `pdf`, and Node 18+ for
|
|
261
|
+
`diff`, `drift`, `trace` and `mcp`.
|
|
153
262
|
|
|
154
263
|
`record` writes an animated PNG, which browsers and GitHub show as it is. For a GIF or an MP4 (for chat or slides),
|
|
155
264
|
convert it with ffmpeg:
|
|
@@ -160,21 +269,57 @@ ffmpeg -f apng -i ride.png -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" -pix_fmt yuv4
|
|
|
160
269
|
```
|
|
161
270
|
|
|
162
271
|
On pull requests, the diff Action posts what they change in your maps: one comment with the summary and a sketch, and
|
|
163
|
-
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.
|
|
164
274
|
|
|
165
275
|
```yaml
|
|
166
276
|
on: pull_request
|
|
167
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
|
|
168
279
|
jobs:
|
|
169
280
|
maps:
|
|
170
281
|
runs-on: ubuntu-latest
|
|
171
282
|
steps:
|
|
172
283
|
- uses: actions/checkout@v4
|
|
173
|
-
- uses: zintaen/umlimator@v0.
|
|
284
|
+
- uses: zintaen/umlimator@v0.14.0
|
|
174
285
|
with:
|
|
175
286
|
models: "docs/*.json" # optional: where the models are (default: every .json that is a map model)
|
|
176
287
|
```
|
|
177
288
|
|
|
289
|
+
## An AI assistant on the map (MCP)
|
|
290
|
+
|
|
291
|
+
`umlimator.py mcp model.json` is a [Model Context Protocol](https://modelcontextprotocol.io) server over stdio: an AI
|
|
292
|
+
assistant reads the model, and shows you what it finds on the map in your browser. Add it to Claude Code:
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
claude mcp add umlimator -- python3 /path/to/umlimator.py mcp /path/to/model.json
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
| Tool | What it does |
|
|
299
|
+
|---|---|
|
|
300
|
+
| `list_views()` | the views (id, title, parent, number of cards) and the stories |
|
|
301
|
+
| `describe_view(view)` | the view in sentences (`describe()`), then each card's id |
|
|
302
|
+
| `paths(view, from, to)` | every card and line on a path from one card to another (`paths()`), as Refs for `highlight` |
|
|
303
|
+
| `reach(view, card, backward?, depth?)` | what a card leads to, or what leads to it, with the distance of each (`reach()`) |
|
|
304
|
+
| `broken_rules()`, `validate()` | each break of the model's `rules` (`rules()`), and its own problems (`validate()`) |
|
|
305
|
+
| `diff(other_model_path)` | what changed from another model file to this one, in words, and the views that show it (`diff()`) |
|
|
306
|
+
| `show_view(view)` | shows a view on the page |
|
|
307
|
+
| `highlight(refs, view?)` | lights cards and lines (their ids, or the Refs `paths` and `reach` give) and dims the rest |
|
|
308
|
+
| `tour(cards, view?)` | a dot rides the lines through these cards |
|
|
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 |
|
|
311
|
+
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
322
|
+
|
|
178
323
|
## Map a repo with Claude Code
|
|
179
324
|
|
|
180
325
|
The `map-repo` skill reads a codebase, writes a C4-style model (context, containers, components), renders it to a page
|
|
@@ -203,11 +348,13 @@ python3 -I tools/fetch_icons.py icons "name:lucide/<icon>" # a line icon
|
|
|
203
348
|
python3 umlimator.py pin-icons # records each new logo's sha256
|
|
204
349
|
```
|
|
205
350
|
|
|
351
|
+
With `UMLIMATOR_OFFLINE=1` the CLI fetches nothing: a logo not in the cache keeps its card's sigil (`mcp` sets it).
|
|
352
|
+
|
|
206
353
|
## Development
|
|
207
354
|
|
|
208
355
|
```bash
|
|
209
356
|
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
|
|
357
|
+
python3 tests/embed.py # render() in headless Chrome: events, keyboard, colours, contrast, editing, the element
|
|
211
358
|
python3 bench/bench.py --check-determinism --expect bench/expected.json --max-ms m50=2000
|
|
212
359
|
```
|
|
213
360
|
|
|
@@ -223,9 +370,11 @@ Release with that section as its notes, and publishes the package to npm (truste
|
|
|
223
370
|
CI also publishes the examples to GitHub Pages from `main`.
|
|
224
371
|
|
|
225
372
|
```
|
|
226
|
-
umlimator.py CLI: render, stills, check, record, pin-icons, diff, drift, timelapse
|
|
373
|
+
umlimator.py CLI: render, stills, check, record, pdf, pin-icons, diff, drift, timelapse, import, trace, mcp
|
|
374
|
+
umlimator_import.py import's readers: Structurizr DSL, C4-PlantUML, Mermaid, compose and Kubernetes JSON
|
|
375
|
+
umlimator_mcp.py the MCP server that `umlimator.py mcp` runs
|
|
227
376
|
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
|
|
377
|
+
lib/ the module (umlimator.js), its types, the default theme, <umlimator-map> and the docs plugins
|
|
229
378
|
engine/ the page templates the CLI inlines the module into; the upstream renderer and schema
|
|
230
379
|
docs/ the module's API and the editing design
|
|
231
380
|
tests/, bench/ tests, benchmark models and results
|
|
@@ -0,0 +1,21 @@
|
|
|
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 wheel 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. Its `state` is the map's MapState. */
|
|
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;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
declare global {
|
|
14
|
+
interface HTMLElementTagNameMap { 'umlimator-map': UmlimatorMap }
|
|
15
|
+
/** The map's events, re-dispatched on the element (they bubble); preventDefault() on umlimator:open keeps the view. */
|
|
16
|
+
interface HTMLElementEventMap {
|
|
17
|
+
'umlimator:select': CustomEvent<Events['select']>;
|
|
18
|
+
'umlimator:open': CustomEvent<Events['open']>;
|
|
19
|
+
'umlimator:viewchange': CustomEvent<Events['viewchange']>;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
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, 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:
|
|
12
|
+
"light" or "dark", that map's palette from lib/umlimator-theme.css, whatever the page's; without it the map follows the
|
|
13
|
+
page, a docs site's dark mode too (VitePress's html.dark, Docusaurus's html[data-theme="dark"]). The colours come from
|
|
14
|
+
that file or the host's own --um-* variables; the element fetches nothing but its model.
|
|
15
|
+
Events: the map's select, open and viewchange as umlimator:select, umlimator:open and umlimator:viewchange, the map's
|
|
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. */
|
|
21
|
+
import { render } from './umlimator.js';
|
|
22
|
+
|
|
23
|
+
const Base = globalThis.HTMLElement || class {}; // importable where there is no DOM (a docs site's server render)
|
|
24
|
+
|
|
25
|
+
export class UmlimatorMap extends Base {
|
|
26
|
+
static observedAttributes = ['src', 'view', 'minimap', 'controls', 'wheel', 'height'];
|
|
27
|
+
map = null;
|
|
28
|
+
#drawn = 0; #due = false; #src = null; #model = null; #loading = false;
|
|
29
|
+
|
|
30
|
+
constructor() {
|
|
31
|
+
super();
|
|
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>'
|
|
33
|
+
+ '<slot></slot><div hidden style="height: 100%"></div>';
|
|
34
|
+
}
|
|
35
|
+
get loading() { return this.#loading; } // a draw under way, waiting for its model (0.14.0)
|
|
36
|
+
connectedCallback() { this.#soon(); }
|
|
37
|
+
disconnectedCallback() { this.#loading = false; this.#drop(); }
|
|
38
|
+
attributeChangedCallback(name, was, now) {
|
|
39
|
+
if (was === now) return; // set to the value it has: nothing to draw again (0.14.0)
|
|
40
|
+
if (name === 'height') this.style.height = now == null ? '' : /^\d+(\.\d+)?$/.test(now) ? now + 'px' : now;
|
|
41
|
+
else this.#soon();
|
|
42
|
+
}
|
|
43
|
+
#soon() { // attributes set together draw once
|
|
44
|
+
this.#loading = true;
|
|
45
|
+
if (this.#due) return;
|
|
46
|
+
this.#due = true;
|
|
47
|
+
queueMicrotask(() => { this.#due = false; if (this.isConnected) this.#draw(); else this.#loading = false; });
|
|
48
|
+
}
|
|
49
|
+
#drop() {
|
|
50
|
+
this.map?.destroy(); this.map = null;
|
|
51
|
+
const [, slot, box] = this.shadowRoot.children; slot.hidden = false; box.hidden = true;
|
|
52
|
+
}
|
|
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
|
+
}
|
|
57
|
+
async #draw() {
|
|
58
|
+
const n = ++this.#drawn, src = this.getAttribute('src'), get = a => this.getAttribute(a); // a later draw wins
|
|
59
|
+
let model = src && src === this.#src ? this.#model : null;
|
|
60
|
+
try {
|
|
61
|
+
if (src && !model) {
|
|
62
|
+
const r = await fetch(src);
|
|
63
|
+
if (!r.ok) throw new Error(`${src}: ${r.status} ${r.statusText}`);
|
|
64
|
+
model = await r.json();
|
|
65
|
+
} else if (!src) {
|
|
66
|
+
const json = this.querySelector(':scope > script[type="application/json"]');
|
|
67
|
+
if (!json) throw new Error('no src, and no <script type="application/json"> child');
|
|
68
|
+
model = JSON.parse(json.textContent);
|
|
69
|
+
}
|
|
70
|
+
} catch (err) { if (n === this.#drawn) this.#fail(err); return; }
|
|
71
|
+
if (n !== this.#drawn) return; // a later draw has the element
|
|
72
|
+
if (!this.isConnected) { this.#loading = false; return; }
|
|
73
|
+
if (src) { this.#src = src; this.#model = model; }
|
|
74
|
+
this.#drop();
|
|
75
|
+
const [, slot, box] = this.shadowRoot.children; slot.hidden = true; box.hidden = false;
|
|
76
|
+
// the map's events, as DOM events on the element; a host that cancels umlimator:open cancels the open
|
|
77
|
+
const on = Object.fromEntries(['select', 'open', 'viewchange'].map(t => [t, e => {
|
|
78
|
+
if (!this.dispatchEvent(new CustomEvent('umlimator:' + t, { detail: e, bubbles: true, composed: true, cancelable: t === 'open' }))) e.preventDefault();
|
|
79
|
+
}]));
|
|
80
|
+
try {
|
|
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 });
|
|
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;
|
|
85
|
+
this.dispatchEvent(new Event('load'));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
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, 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
|
+
|
|
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, '&').replace(/"/g, '"').replace(/</g, '<')}"`).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
|
+
}
|
package/lib/umlimator-theme.css
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|