@miragon/modeler-shell 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/API.md +1902 -0
  2. package/LICENSE +21 -0
  3. package/README.md +435 -2
  4. package/THIRD_PARTY_NOTICES.md +52 -0
  5. package/dist/appearance-D3FUcbu6.d.cts +53 -0
  6. package/dist/appearance-D3FUcbu6.d.ts +53 -0
  7. package/dist/chunk-5JSZ4VGA.js +551 -0
  8. package/dist/chunk-5JSZ4VGA.js.map +1 -0
  9. package/dist/chunk-6LQOFACK.js +1576 -0
  10. package/dist/chunk-6LQOFACK.js.map +1 -0
  11. package/dist/chunk-N4QXFF3A.js +23 -0
  12. package/dist/chunk-N4QXFF3A.js.map +1 -0
  13. package/dist/chunk-RJRFR6N3.js +136 -0
  14. package/dist/chunk-RJRFR6N3.js.map +1 -0
  15. package/dist/core.cjs +619 -0
  16. package/dist/core.cjs.map +1 -0
  17. package/dist/core.d.cts +171 -0
  18. package/dist/core.d.ts +171 -0
  19. package/dist/core.js +91 -0
  20. package/dist/core.js.map +1 -0
  21. package/dist/diagram-js.cjs +1115 -0
  22. package/dist/diagram-js.cjs.map +1 -0
  23. package/dist/diagram-js.d.cts +163 -0
  24. package/dist/diagram-js.d.ts +163 -0
  25. package/dist/diagram-js.js +867 -0
  26. package/dist/diagram-js.js.map +1 -0
  27. package/dist/index.cjs +2175 -0
  28. package/dist/index.cjs.map +1 -0
  29. package/dist/index.d.cts +20 -0
  30. package/dist/index.d.ts +20 -0
  31. package/dist/index.js +103 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/keymap-CObQE6q6.d.cts +151 -0
  34. package/dist/keymap-DORzHnK-.d.ts +151 -0
  35. package/dist/protocol-DhYBakWL.d.cts +15 -0
  36. package/dist/protocol-IeW8bf4f.d.ts +15 -0
  37. package/dist/shell.css +914 -0
  38. package/dist/testing-vscode-stub.cjs +332 -0
  39. package/dist/testing-vscode-stub.cjs.map +1 -0
  40. package/dist/testing-vscode-stub.d.cts +65 -0
  41. package/dist/testing-vscode-stub.d.ts +65 -0
  42. package/dist/testing-vscode-stub.js +154 -0
  43. package/dist/testing-vscode-stub.js.map +1 -0
  44. package/dist/testing.cjs +2364 -0
  45. package/dist/testing.cjs.map +1 -0
  46. package/dist/testing.d.cts +165 -0
  47. package/dist/testing.d.ts +165 -0
  48. package/dist/testing.js +2297 -0
  49. package/dist/testing.js.map +1 -0
  50. package/dist/types-B6NfkiUG.d.cts +66 -0
  51. package/dist/types-B6NfkiUG.d.ts +66 -0
  52. package/dist/types-DnZn9CPw.d.ts +284 -0
  53. package/dist/types-OuFlqZp0.d.cts +284 -0
  54. package/dist/vscode-extension.cjs +215 -0
  55. package/dist/vscode-extension.cjs.map +1 -0
  56. package/dist/vscode-extension.d.cts +103 -0
  57. package/dist/vscode-extension.d.ts +103 -0
  58. package/dist/vscode-extension.js +24 -0
  59. package/dist/vscode-extension.js.map +1 -0
  60. package/dist/vscode-webview.cjs +2198 -0
  61. package/dist/vscode-webview.cjs.map +1 -0
  62. package/dist/vscode-webview.d.cts +82 -0
  63. package/dist/vscode-webview.d.ts +82 -0
  64. package/dist/vscode-webview.js +250 -0
  65. package/dist/vscode-webview.js.map +1 -0
  66. package/package.json +145 -4
  67. package/src/core/appearance.ts +104 -0
  68. package/src/core/fit.ts +205 -0
  69. package/src/core/index.ts +84 -0
  70. package/src/core/keymap.ts +275 -0
  71. package/src/core/layout.ts +69 -0
  72. package/src/core/messages.ts +88 -0
  73. package/src/core/sizes.ts +85 -0
  74. package/src/core/types.ts +84 -0
  75. package/src/core/zoom.ts +74 -0
  76. package/src/core.ts +2 -0
  77. package/src/diagram-js/connect.ts +75 -0
  78. package/src/diagram-js/index.ts +16 -0
  79. package/src/diagram-js/module.ts +20 -0
  80. package/src/diagram-js/palette-skin.ts +267 -0
  81. package/src/diagram-js/palette.css +269 -0
  82. package/src/diagram-js/service.ts +240 -0
  83. package/src/diagram-js/types.ts +67 -0
  84. package/src/diagram-js/viewport.ts +564 -0
  85. package/src/diagram-js.ts +2 -0
  86. package/src/dom/element.ts +51 -0
  87. package/src/dom/hint.ts +51 -0
  88. package/src/dom/icons.ts +309 -0
  89. package/src/dom/index.ts +29 -0
  90. package/src/dom/inspector.ts +35 -0
  91. package/src/dom/keys.ts +59 -0
  92. package/src/dom/ledger.ts +87 -0
  93. package/src/dom/legend.ts +85 -0
  94. package/src/dom/menu.ts +393 -0
  95. package/src/dom/mount.ts +461 -0
  96. package/src/dom/problems.ts +72 -0
  97. package/src/dom/shell-dom.css +966 -0
  98. package/src/dom/status.ts +72 -0
  99. package/src/dom/tooltip.ts +237 -0
  100. package/src/dom/types.ts +277 -0
  101. package/src/dom/zoom-panel.ts +154 -0
  102. package/src/index.ts +3 -0
  103. package/src/shell.css +6 -0
  104. package/src/testing/cases.ts +1012 -0
  105. package/src/testing/checks.ts +832 -0
  106. package/src/testing/facts.ts +587 -0
  107. package/src/testing/geometry.ts +472 -0
  108. package/src/testing/harness.ts +138 -0
  109. package/src/testing/index.ts +15 -0
  110. package/src/testing/page-tools.ts +434 -0
  111. package/src/testing/page.ts +17 -0
  112. package/src/testing/vscode-stub.ts +237 -0
  113. package/src/testing-vscode-stub.ts +2 -0
  114. package/src/testing.ts +2 -0
  115. package/src/vscode/extension/html.ts +107 -0
  116. package/src/vscode/extension/index.ts +8 -0
  117. package/src/vscode/extension/reveal.ts +9 -0
  118. package/src/vscode/extension/theme-bridge.ts +85 -0
  119. package/src/vscode/extension/types.ts +36 -0
  120. package/src/vscode/protocol.ts +31 -0
  121. package/src/vscode/webview/appearance.ts +141 -0
  122. package/src/vscode/webview/index.ts +8 -0
  123. package/src/vscode/webview/mount.ts +76 -0
  124. package/src/vscode/webview/state.ts +116 -0
  125. package/src/vscode-extension.ts +2 -0
  126. package/src/vscode-webview.ts +2 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Miragon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,436 @@
1
- # Temporary Holding Version
1
+ # @miragon/modeler-shell
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The shared, framework-free chrome around Miragon diagram-js modelers: the menu button with its
4
+ dropdown, the horizontal palette skin, the chrome tooltip, the zoom panel with its keys and fit, the
5
+ problems pill, the status box, the empty-file hint, legend and inspector slots, and the plumbing to
6
+ a VS Code webview. One implementation for every modeler and every host (VS Code webview, web app,
7
+ embedded in a foreign page), so the chrome looks and behaves the same everywhere.
8
+
9
+ - **Look and behaviour** follow the Miragon design system, guide
10
+ [`modeler-design-system.md`][guide] §5, §6, §8, §9, §11, §12 and §13.3 to §13.5 (skill
11
+ `miragon-brand:modeler-tool-design`; the repository is private, the link opens for Miragon staff).
12
+ - **Mechanics** (diagram-js services, VS Code quirks, why things are done the way they are) are in
13
+ the [implementation notes][notes].
14
+ - **API**: every entry point, type and rule in [API.md][api].
15
+ - **Plan**: Phase 4 of [Miragon/modeler-api#4](https://github.com/Miragon/modeler-api/issues/4); the
16
+ Event Storming modeler is the pilot.
17
+
18
+ Zero runtime dependencies: diagram-js services are typed structurally, the `vscode` namespace and
19
+ Playwright's `page` and `test` are passed in, nothing is imported at runtime. The stylesheet reads
20
+ only the design system's `--cd-ui-*` roles and `--cd-shell-*` sizes. All texts are English by default
21
+ and overridable through one messages table; numbers go through `Intl` in the UI language.
22
+
23
+ ## Install
24
+
25
+ ```sh
26
+ npm i --save-exact @miragon/modeler-shell
27
+ ```
28
+
29
+ The modeler brings what the shell styles against and draws into:
30
+
31
+ - **diagram-js** 15.28 or later (directly or through its renderer);
32
+ - **the design tokens**: `cd-tokens.generated.css` (and `cd-tokens.generated.json` where scripts
33
+ need values) of the design system, 1.12.0 or later (the release with the `shell` group). Copy
34
+ both unchanged from the skill's `assets/` or from
35
+ <https://corporate-identity.netlify.app/brand/cd-tokens.generated.css> and
36
+ <https://corporate-identity.netlify.app/brand/cd-tokens.generated.json>, mark them as generated,
37
+ and re-copy instead of editing. The shell ships no tokens and never forks a value;
38
+ - **the fonts** Geist and Geist Mono, bundled locally, for example `@fontsource-variable/geist` and
39
+ `@fontsource-variable/geist-mono` (no CDN);
40
+ - for `./vscode/extension`: **VS Code 1.90 or later**, whose extension host runs Node 20 (the global
41
+ `crypto` that `createNonce` uses; the types are tested against `@types/vscode` 1.96). Declare
42
+ `engines.vscode` accordingly and target `node20` in the extension-host bundle.
43
+
44
+ Stylesheets, in this order. The tokens come first, unless the renderer's stylesheet already imports
45
+ them (Event Storming's does), and are never loaded twice:
46
+
47
+ ```ts
48
+ import "./theme/cd-tokens.generated.css"; // vendored, unchanged, first; left out when the renderer CSS brings it
49
+ import "@fontsource-variable/geist/wght.css";
50
+ import "@fontsource-variable/geist-mono/wght.css";
51
+ import "diagram-js/assets/diagram-js.css"; // usually through the renderer's CSS
52
+ import "@miragon/<notation>-renderer/…css";
53
+ import "@miragon/modeler-shell/shell.css"; // last
54
+ ```
55
+
56
+ The shell computes fit insets, breakpoints and layers from its own copy of the sizes (`SHELL`), while
57
+ its stylesheet reads the vendored `--cd-shell-*` tokens. Pin the two together in a unit test (Vitest
58
+ or `node:test`), not in the e2e suite, since the check is pure data:
59
+
60
+ ```ts
61
+ // test/tokens.sync.test.ts
62
+ import { SHELL, SHELL_TOKENS, shellSizesFromTokens } from "@miragon/modeler-shell/core";
63
+
64
+ expect(SHELL).toEqual(shellSizesFromTokens(vendoredTokens.shell));
65
+ expect(Object.keys(vendoredTokens.shell)).toEqual(expect.arrayContaining(Object.values(SHELL_TOKENS)));
66
+ ```
67
+
68
+ | Entry | For |
69
+ | -------------------------------------------- | ----------------------------------------------------------- |
70
+ | `@miragon/modeler-shell` | `mountShell`, icons, and everything of `./core` |
71
+ | `@miragon/modeler-shell/core` | DOM-free: fit math, keymap, zoom readout, appearance, sizes |
72
+ | `@miragon/modeler-shell/diagram-js` | `shellModule`, the viewport, `connectDiagramJs` |
73
+ | `@miragon/modeler-shell/vscode/extension` | webview HTML with CSP and nonce, theme bridge |
74
+ | `@miragon/modeler-shell/vscode/webview` | `mountVsCodeShell`, appearance handler, webview state |
75
+ | `@miragon/modeler-shell/testing` | the Playwright-driven shell conformance kit |
76
+ | `@miragon/modeler-shell/testing/vscode-stub` | a stand-in for VS Code around a webview test page |
77
+ | `@miragon/modeler-shell/shell.css` | every style of the shell |
78
+
79
+ TypeScript: every entry resolves under `moduleResolution` `node16`, `bundler` and `node10`, and
80
+ every optional input field accepts `undefined`, so a modeler compiling with
81
+ `exactOptionalPropertyTypes` forwards optional values as they are.
82
+
83
+ ## Usage in a VS Code webview
84
+
85
+ Extension host:
86
+
87
+ ```ts
88
+ import * as vscode from "vscode";
89
+ import {
90
+ connectThemeBridge,
91
+ currentAppearance,
92
+ renderWebviewHtml,
93
+ } from "@miragon/modeler-shell/vscode/extension";
94
+
95
+ const THEMES = ["light"] as const; // until the shared dark-mode release of the modelers
96
+
97
+ // activate(): a hidden tab keeps document, view and selection
98
+ vscode.window.registerCustomEditorProvider(viewType, provider, {
99
+ webviewOptions: { retainContextWhenHidden: true },
100
+ });
101
+
102
+ // resolveCustomTextEditor():
103
+ const dist = vscode.Uri.joinPath(extensionUri, "dist");
104
+ webview.options = { enableScripts: true, localResourceRoots: [dist] }; // script, CSS and font files
105
+ const asset = (file: string) => webview.asWebviewUri(vscode.Uri.joinPath(dist, file)).toString();
106
+ webview.html = renderWebviewHtml({
107
+ cspSource: webview.cspSource,
108
+ scriptUri: asset("webview.js"),
109
+ styleUris: [asset("webview.css")],
110
+ title: "Event Storming Board",
111
+ lang: "en",
112
+ appearance: currentAppearance(vscode, { themes: THEMES }), // on <html> before the first paint
113
+ });
114
+ connectThemeBridge(vscode, webviewPanel, { themes: THEMES }); // disposes itself with the panel
115
+ ```
116
+
117
+ The bridge's disposable is only for ending it before the panel closes; never push it into
118
+ `context.subscriptions`, which would keep one dead bridge per opened editor until the extension
119
+ deactivates.
120
+
121
+ Bundle the fonts as files next to the webview bundle, not as data URLs: with esbuild
122
+ `loader: { ".woff2": "file", ".woff": "file" }` and `assetNames: "fonts/[name]-[hash]"` the browser
123
+ fetches only the subsets a page's `unicode-range` needs (Geist Mono alone brings six; inlined they
124
+ roughly double the webview CSS). The CSP of `renderWebviewHtml` allows fonts from `cspSource`, and
125
+ `dist` is in `localResourceRoots`. A webview that bundles zod 4 runs `z.config({ jitless: true })` in
126
+ a module it imports before any schema module: the CSP forbids eval, zod probes once for eval when it
127
+ builds its first object schema, and the browser reports that caught `new Function` as a
128
+ `securitypolicyviolation`; under `jitless` zod skips the probe.
129
+
130
+ Webview:
131
+
132
+ ```ts
133
+ import { INVALID_STATUS_DELAY_MS } from "@miragon/modeler-shell";
134
+ import { connectDiagramJs, shellModule } from "@miragon/modeler-shell/diagram-js";
135
+ import {
136
+ mountVsCodeShell,
137
+ persistView,
138
+ readShellState,
139
+ writeShellState,
140
+ } from "@miragon/modeler-shell/vscode/webview";
141
+
142
+ const vscode = acquireVsCodeApi();
143
+ const stored = readShellState(vscode);
144
+
145
+ // The diagram first: a modeler keyboard on document must run before the shell's key listener.
146
+ // A renderer that builds its diagram lazily builds it now; width and height follow the shell's slot.
147
+ // A renderer with its own Cmd/Ctrl+Z binding turns it off here, because VS Code's document history
148
+ // owns undo (Event Storming: createCollaborativeModeler({ undoRedoKeys: false }), see API.md).
149
+ const modeler = new Modeler({ width: "100%", height: "100%", additionalModules: [shellModule] });
150
+ modeler.get("canvas");
151
+ const shell = mountVsCodeShell(document.getElementById("app")!, {
152
+ themes: ["light"],
153
+ canvas: modeler.container,
154
+ menu: {
155
+ model: [/* Level; Type captions with keepEnabledWhenReadOnly, a view toggle */],
156
+ export: [/* Export as PNG, SVG */],
157
+ help: [/* Keyboard shortcuts… (the modeler's dialog in 0.1) */],
158
+ },
159
+ legend: {
160
+ render: renderLegend,
161
+ open: stored.legendOpen,
162
+ onToggle: (open) => writeShellState(vscode, { legendOpen: open }),
163
+ },
164
+ problems: { onActivate: () => vscode.postMessage({ type: "showProblems" }) },
165
+ emptyHint: { onInsertExample: () => vscode.postMessage({ type: "insertExample" }) },
166
+ });
167
+ const connection = connectDiagramJs(shell, modeler, {
168
+ // the tight bounds of the content, no padding: the shell's fit insets are the only margin
169
+ contentBounds: () => elementBounds(), // undefined while there is nothing to fit
170
+ isEmpty: () => elementCount() === 0, // cheap: no export, no validation
171
+ });
172
+ const { viewport } = connection;
173
+ persistView(vscode, viewport); // writes the view once it rested, not on every frame
174
+
175
+ async function importText(text: string, first: boolean): Promise<void> {
176
+ const keep = first ? undefined : viewport.getViewbox();
177
+ try {
178
+ await modeler.importDSL(text);
179
+ lockCanvas(false);
180
+ shell.setReadOnly(false);
181
+ shell.setStatus(null);
182
+ if (keep) viewport.restore(keep);
183
+ else if (first && stored.view) viewport.setState(stored.view);
184
+ viewport.afterImport(); // first content fits, an empty file shows 100 %, the hint follows by itself
185
+ } catch {
186
+ // locked at once, visible after 800ms without a valid import (no flicker while typing)
187
+ lockCanvas(true); // the modeler's own lock: context pad, move, resize, delete, paste, label editor
188
+ shell.setReadOnly(true, { delayMs: INVALID_STATUS_DELAY_MS });
189
+ shell.setStatus(
190
+ { severity: "danger", text: shell.messages.invalidFile("Event Storming board") },
191
+ { delayMs: INVALID_STATUS_DELAY_MS },
192
+ );
193
+ }
194
+ }
195
+
196
+ // Imports strictly one after another; `ready` resolves once the first one settled.
197
+ let imports = Promise.resolve();
198
+ let firstImportSettled = () => {};
199
+ const ready = new Promise<void>((resolve) => (firstImportSettled = resolve));
200
+ window.addEventListener("message", ({ data }) => {
201
+ if (data?.type === "init")
202
+ imports = imports.then(() => importText(data.text, true)).then(firstImportSettled);
203
+ if (data?.type === "update") imports = imports.then(() => importText(data.text, false));
204
+ });
205
+ vscode.postMessage({ type: "ready" });
206
+
207
+ // For the e2e test page below; the bundle publishes nothing on window.
208
+ export const webview = { shell, modeler, connection, ready };
209
+ ```
210
+
211
+ `mountVsCodeShell` stops every key the webview handles on `document`, before VS Code's listener on
212
+ `window` forwards it to the workbench, and keeps the webview in the mode the extension resolved: a
213
+ modeler declaring `["light"]` stays light under any VS Code theme, high contrast passes, and VS
214
+ Code's `workbench.reduceMotion` stops the chrome's animations. Undo and redo on the canvas go to VS
215
+ Code (the shell keeps diagram-js's bindings off them), so a modeler's own keyboard binds no Cmd/Ctrl+Z
216
+ in the webview either. Keys the canvas uses act only there: Cmd/Ctrl+A on the canvas selects every
217
+ element (or is at least prevented), so VS Code's Select All never selects the chrome. The modeler's
218
+ keyboard also ignores keys with `defaultPrevented` and keys from outside the canvas container, and
219
+ puts no `tabindex` on it.
220
+
221
+ `shell.setReadOnly(true)` locks the palette (all but Lasso and Hand) and the Model entries of the
222
+ menu. View toggles stay usable: the shell's Legend checkbox and the legend's close button, and every
223
+ entry with `keepEnabledWhenReadOnly` (guide §13.5 since design system 1.13.0). The canvas itself is the modeler's to lock: no context pad, move,
224
+ resize, delete, paste or label editor. Content the modeler adds to `shell.root` (a "Keyboard
225
+ shortcuts…" dialog) is the modeler's too: it removes it in its own teardown, for example on
226
+ `diagram.destroy`, since `shell.destroy()` removes only what the shell added. The complete Event
227
+ Storming example, including the export notification, is in [API.md][api]. Document sync, undo through
228
+ the document and diagnostics stay in the modeler for 0.1.
229
+
230
+ ## Usage in a web app
231
+
232
+ ```ts
233
+ import { mountShell } from "@miragon/modeler-shell";
234
+ import { connectDiagramJs, shellModule } from "@miragon/modeler-shell/diagram-js";
235
+
236
+ // index.html carries <html lang="en" data-cd-theme="light"> statically while the modeler is light-only
237
+ const modeler = new Modeler({ width: "100%", height: "100%", additionalModules: [shellModule] });
238
+ const shell = mountShell(document.getElementById("app")!, {
239
+ host: "web",
240
+ canvas: modeler.container,
241
+ menu: { export: [/* … */], help: [/* Keyboard shortcuts… */] },
242
+ legend: { render: renderLegend, open: storedLegendOpen(), onToggle: storeLegendOpen },
243
+ });
244
+ const { viewport } = connectDiagramJs(shell, modeler, { contentBounds: () => elementBounds() });
245
+ await modeler.importXML(xml);
246
+ viewport.afterImport();
247
+ ```
248
+
249
+ The `web` host prevents the browser's page zoom for the chrome keys (outside text fields and
250
+ dialogs), lifts pill and legend above the legal line and fits with the web insets. The rest of the
251
+ web-app chrome (primary action, legal line, File and Appearance groups, toasts, empty state) is not
252
+ part of 0.1.
253
+
254
+ ## Usage in a foreign page
255
+
256
+ `mountShell(container, { host: "embedded", themes: ["light"], canvas })` on an explicitly sized
257
+ container: the root carries the tool's `lang` itself, keys act only while the focus is inside the
258
+ modeler, and the page's `<html>` stays untouched. Until the `@miragon/modeler-api` contract gains
259
+ its `appearance` surface (a later 1.x minor), an embedded modeler has no appearance channel, so the
260
+ root carries its own `data-cd-theme` (the first of `themes`, light) and stays light inside a dark
261
+ host page; it takes the page's `data-cd-kontrast="hoch"` over from the nearest mode carrier above
262
+ it. The page's element styles
263
+ (`button { text-transform: uppercase }`) do not reach the chrome.
264
+
265
+ ## The conformance kit
266
+
267
+ Every modeler runs the kit in its own Playwright suite against its real webview or page. It checks
268
+ the geometry of every island (±1px), ARIA and the keyboard models, that every chrome key acts exactly
269
+ once and never reaches VS Code, that the canvas's own keys (Cmd/Ctrl+A) stay on the canvas, that
270
+ nothing is ever half-dark (the painted canvas surround and the palette's colours included), that
271
+ Enter on a creating palette entry adds one selected element, that an invalid file locks the canvas,
272
+ and that `destroy()` leaves nothing behind.
273
+
274
+ For the `ide` host the suite serves a test page that puts a stand-in for VS Code around the
275
+ modeler's webview code: VS Code's default webview styles, `<html>` as `renderWebviewHtml` writes it,
276
+ the host page's theme classes, `acquireVsCodeApi()` and the real theme bridge. Serve
277
+ `e2e/webview/` as the root of a Vite server, so the page is `/webview-test.html`:
278
+
279
+ ```ts
280
+ // e2e/webview/webview-test.ts, the entry of e2e/webview/webview-test.html
281
+ import { installVsCodeWebviewStub, stubOptionsFromSearch } from "@miragon/modeler-shell/testing/vscode-stub";
282
+
283
+ // before the webview runs, which calls acquireVsCodeApi() and mounts the shell
284
+ installVsCodeWebviewStub({ ...stubOptionsFromSearch(location.search), themes: ["light"] });
285
+ const { webview } = await import("../../src/webview/main"); // exports { shell, modeler, connection, ready }
286
+
287
+ // for the harness only: the shipped bundle publishes nothing on window
288
+ window.shell = webview.shell;
289
+ window.modeler = webview.modeler;
290
+ window.connection = webview.connection;
291
+ void webview.ready.then(() => (window.webviewReady = true)); // the first import settled
292
+
293
+ // there is no extension host: post the document the way the extension would
294
+ window.postMessage({ type: "init", text: EXAMPLE_BOARD }, "*");
295
+ ```
296
+
297
+ `ready`, `webviewReady` and the window globals belong to the modeler's test page (declared in a
298
+ `.d.ts` of the suite), not to the shell: the webview resolves `ready` once its first `init` import
299
+ settled.
300
+
301
+ ```ts
302
+ // e2e/shell-conformance.spec.ts
303
+ import { test } from "@playwright/test";
304
+ import { registerShellConformance, vsCodeStubSearch } from "@miragon/modeler-shell/testing";
305
+
306
+ test.describe("shell conformance", () => {
307
+ test.describe.configure({ timeout: 60_000 }); // geometry measures three sizes
308
+ registerShellConformance(
309
+ {
310
+ host: "ide",
311
+ themes: ["light"],
312
+ absent: ["inspector"], // islands this modeler does not have: never visible, never skipped
313
+ async open(page, { size, requested }) {
314
+ await page.setViewportSize(size);
315
+ await page.goto(`/webview-test.html?${vsCodeStubSearch(requested)}`);
316
+ await page.waitForFunction(() => window.webviewReady === true); // set by the test page
317
+ },
318
+ async destroy(page) {
319
+ // the webview's own teardown path: connection, diagram (with the modeler's dialogs), shell
320
+ await page.evaluate(() => {
321
+ window.connection.destroy();
322
+ window.modeler.destroy();
323
+ window.shell.destroy();
324
+ });
325
+ },
326
+ show: {
327
+ async legend(page) {
328
+ await page.evaluate(() => window.shell.legend?.setOpen(true));
329
+ },
330
+ async problems(page) {
331
+ await page.evaluate(() => window.postMessage({ type: "problems", count: 2, errors: 1 }, "*"));
332
+ },
333
+ async status(page) {
334
+ await page.evaluate(() =>
335
+ window.shell.setStatus({ severity: "danger", text: window.shell.messages.invalidFile("board") }),
336
+ );
337
+ },
338
+ async invalid(page) {
339
+ // the way the host makes the file invalid: a document update the parser rejects
340
+ await page.evaluate((text) => window.postMessage({ type: "update", text }, "*"), INVALID_BOARD);
341
+ },
342
+ },
343
+ },
344
+ test,
345
+ {
346
+ onSkip(caseName, skips) {
347
+ // with `absent` listing what the modeler lacks, every remaining skip is a gap of this suite
348
+ throw new Error(`${caseName}: ${skips.map(({ hook, reason }) => `${hook}: ${reason}`).join("; ")}`);
349
+ },
350
+ },
351
+ );
352
+ });
353
+ ```
354
+
355
+ The harness opens the modeler with the appearance the kit requests, the way its host would; for
356
+ the `embedded` host the kit puts the foreign page itself in that mode. A skip names the harness
357
+ field it misses (`{ hook: "legend", reason }`; `hook: "destroy"` without a `destroy`; `hook: "open"`
358
+ when the opened document lacks what a case needs, such as a shape for `readOnly`); without `onSkip`
359
+ the kit warns on the console. What each case asserts, and what the kit installs in the page, is
360
+ listed in [API.md][api].
361
+
362
+ `registerShellConformance` declares every test inside the kit, so a runner reports them at the kit's
363
+ source (the package ships `src/` without its tests, so the location resolves). For test locations in
364
+ the suite's own spec file, register the cases there:
365
+
366
+ ```ts
367
+ import { shellConformanceCases } from "@miragon/modeler-shell/testing";
368
+
369
+ for (const conformanceCase of shellConformanceCases()) {
370
+ test(`shell conformance: ${conformanceCase.name}`, async ({ page }) => {
371
+ const outcome = await conformanceCase.run(page, harness);
372
+ if (outcome?.skipped) failOnSkips(conformanceCase.name, outcome.skipped);
373
+ });
374
+ }
375
+ ```
376
+
377
+ The test page runs the webview from source without a CSP. Add one smoke test of the production bundle
378
+ behind the HTML and CSP of `renderWebviewHtml` that fails on any `securitypolicyviolation` (the recipe
379
+ is in [API.md][api], `./vscode/extension`).
380
+
381
+ ## Development
382
+
383
+ Run from the repository root (`-w packages/shell` for this package only):
384
+
385
+ ```sh
386
+ npm run build -w packages/shell # tsup: ESM + CJS + d.ts, dist/shell.css
387
+ npm run typecheck -w packages/shell # src, core without the DOM library, demo
388
+ npm test -w packages/shell # Vitest: node (core, boundaries) and happy-dom projects
389
+ npx playwright install chromium # once
390
+ npm run test:e2e -w packages/shell # Playwright against demo/
391
+ npm run demo -w packages/shell # the demo page on http://localhost:5181
392
+ ```
393
+
394
+ The demo page (`demo/`) is a small diagram-js modeler around the shell. Its query parameters choose
395
+ the host (`host=ide|web|embedded`), the modes the modeler declares (`themes=light,dark`), what the
396
+ host requests (`theme`, `kontrast=hoch`; embedded: the foreign page's own mode), the `ide` host's
397
+ `colorTheme` setting and `reduceMotion`, and the state to start in (`legend=open`, `inspector`,
398
+ `problems=2:1`, `status=danger`, `invalid`, `empty`, which opens at 100 %). In the `ide` host it runs
399
+ inside the stub of `./testing/vscode-stub`, and keeps view and legend state through
400
+ `acquireVsCodeApi()`.
401
+
402
+ The e2e suite runs the conformance kit for the `ide`, `web` and `embedded` hosts with a light-only
403
+ and a dark-capable modeler, switches VS Code themes and the `colorTheme` setting at runtime, checks
404
+ VS Code's reduce motion, the chrome's typography against VS Code's default styles and a hostile
405
+ foreign page, the canvas slot's paint and focus ring, and proves with saboteurs that every case of
406
+ the kit fails when its rule is broken.
407
+
408
+ | Directory | Module |
409
+ | --------------------------- | ------------------------------------------------------------------ |
410
+ | `src/core` | DOM-free: fit math, keymap, zoom readout, appearance, sizes, texts |
411
+ | `src/dom` | `mountShell`: menu, zoom panel, tooltip, pill, status, slots, keys |
412
+ | `src/diagram-js` | `shellModule`, viewport, `connectDiagramJs`, palette skin |
413
+ | `src/vscode` | extension side (HTML, theme bridge) and webview side |
414
+ | `src/testing` | the Playwright-driven conformance kit and the VS Code stub |
415
+ | `src/*.ts`, `src/shell.css` | the entry points |
416
+ | `test/` | package boundaries, token drift, the unit tests' helpers |
417
+ | `demo/`, `e2e/` | the demo modeler and the e2e suite |
418
+ | `fixtures/tokens` | the token files, vendored unchanged for the tests and the demo |
419
+
420
+ A module imports another one only through its `index.ts`; `src/core` imports nothing outside
421
+ itself and compiles without the DOM library. `test/package-boundaries.test.ts` enforces this, the
422
+ zero runtime dependencies, the exact devDependency pins, the token-only stylesheets and the published
423
+ files (`dist`, `src` without its tests, `API.md` and the notices).
424
+
425
+ ## License
426
+
427
+ MIT. The inlined Lucide icons are ISC (© Lucide Icons and Contributors), and `check`, `circle-x`,
428
+ `maximize`, `minus`, `plus`, `triangle-alert` and `x` derive from Feather, MIT (© Cole Bemis), as
429
+ Lucide's LICENSE lists them; see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). `dist` keeps that
430
+ notice as a legal comment in the chunk with the icons, but a bundler's `legalComments` setting may
431
+ drop it: a VSIX or a web bundle that contains the shell carries the Lucide and Feather notices of
432
+ THIRD_PARTY_NOTICES.md itself.
433
+
434
+ [guide]: https://github.com/Miragon/corporate-identity/blob/main/plugins/miragon-brand/skills/modeler-tool-design/assets/modeler-design-system.md
435
+ [notes]: https://github.com/Miragon/modeler-api/blob/main/docs/shell/implementation-notes.md
436
+ [api]: https://github.com/Miragon/modeler-api/blob/main/packages/shell/API.md
@@ -0,0 +1,52 @@
1
+ # Third-party notices
2
+
3
+ `@miragon/modeler-shell` inlines icon geometry from [Lucide](https://lucide.dev)
4
+ (`lucide-static` 1.59.0) in `src/dom/icons.ts` and its builds. That file opens with a legal comment
5
+ (`/*! … @license … */`) naming both notices below, which esbuild and most minifiers keep in the
6
+ output chunk that carries the geometry. A bundle built with legal comments dropped or moved to a
7
+ separate file must ship these notices itself.
8
+
9
+ ## Lucide
10
+
11
+ ISC License
12
+
13
+ Copyright (c) 2026 Lucide Icons and Contributors
14
+
15
+ Permission to use, copy, modify, and/or distribute this software for any
16
+ purpose with or without fee is hereby granted, provided that the above
17
+ copyright notice and this permission notice appear in all copies.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
20
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
21
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
22
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
23
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
24
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
25
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
26
+
27
+ Lucide's LICENSE (`lucide-static` 1.59.0) lists the icons derived from the
28
+ Feather project, by their Feather names. Of those the shell inlines `check`,
29
+ `circle-x` (Feather's `x-circle`), `maximize`, `minus`, `plus`,
30
+ `triangle-alert` (Feather's `alert-triangle`) and `x`:
31
+
32
+ The MIT License (MIT)
33
+
34
+ Copyright (c) 2013-present Cole Bemis
35
+
36
+ Permission is hereby granted, free of charge, to any person obtaining a copy
37
+ of this software and associated documentation files (the "Software"), to deal
38
+ in the Software without restriction, including without limitation the rights
39
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
40
+ copies of the Software, and to permit persons to whom the Software is
41
+ furnished to do so, subject to the following conditions:
42
+
43
+ The above copyright notice and this permission notice shall be included in all
44
+ copies or substantial portions of the Software.
45
+
46
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
47
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
48
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
49
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
50
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
51
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
52
+ SOFTWARE.
@@ -0,0 +1,53 @@
1
+ /** The modes a Miragon modeler can show (guide §13.3). */
2
+ type Theme = "light" | "dark";
3
+ /** High contrast; written as `data-cd-kontrast="hoch"` on the same element as `data-cd-theme`. */
4
+ type Kontrast = "hoch";
5
+ interface Appearance {
6
+ theme: Theme;
7
+ kontrast?: Kontrast | undefined;
8
+ }
9
+ /**
10
+ * The one appearance decision of guide §13.5: a theme the modeler cannot show is never shown
11
+ * (`themes[0]` wins, so `["light"]` stays light whatever the host requests); `kontrast` always
12
+ * passes through. Throws when `themes` is empty.
13
+ */
14
+ declare function resolveAppearance(requested: Appearance, themes: readonly Theme[]): Appearance;
15
+ /**
16
+ * The appearance VS Code requests for `vscode.window.activeColorTheme.kind`:
17
+ * 1 Light → light, 2 Dark → dark, 3 HighContrast → dark + hoch, 4 HighContrastLight → light + hoch;
18
+ * any other value → light.
19
+ */
20
+ declare function appearanceFromColorThemeKind(kind: number): Appearance;
21
+ /** The values of the setting `miragon.<modeler>.colorTheme` (it ships once a modeler can show dark). */
22
+ type ColorThemeSetting = "automatic" | "light" | "dark";
23
+ /**
24
+ * The requested appearance in an IDE host: the setting's theme unless it is `automatic` (or
25
+ * missing), the contrast always from the color theme kind.
26
+ */
27
+ declare function requestedAppearance(kind: number, setting?: ColorThemeSetting | undefined): Appearance;
28
+ /**
29
+ * The fallback signal inside a VS Code webview: the body classes `vscode-high-contrast-light`
30
+ * (checked first, VS Code adds `vscode-high-contrast` to it as well), `vscode-high-contrast`,
31
+ * `vscode-dark`, `vscode-light`. `undefined` when none is set. Never use them for styling.
32
+ */
33
+ declare function appearanceFromBodyClasses(classes: Iterable<string>): Appearance | undefined;
34
+ /**
35
+ * VS Code's `workbench.reduceMotion` as the webview sees it: the body class `vscode-reduce-motion`
36
+ * (the webview host page sets it). Never use it for styling; the shell mirrors it as
37
+ * `REDUCED_MOTION_CLASS` on `<html>`.
38
+ */
39
+ declare function reducedMotionFromBodyClasses(classes: Iterable<string>): boolean;
40
+ /**
41
+ * On `<html>` while the host asks for reduced motion although the OS does not (VS Code's
42
+ * `workbench.reduceMotion`): the shell's animations stop as under `prefers-reduced-motion`, and an
43
+ * explicit fit jumps.
44
+ */
45
+ declare const REDUCED_MOTION_CLASS = "cd-shell-reduce-motion";
46
+ interface AppearanceAttributes {
47
+ "data-cd-theme": Theme;
48
+ "data-cd-kontrast"?: Kontrast;
49
+ }
50
+ /** The attributes for `<html>` (own document) or the modeler container (foreign page). */
51
+ declare function appearanceAttributes(appearance: Appearance): AppearanceAttributes;
52
+
53
+ export { type Appearance as A, type ColorThemeSetting as C, type Kontrast as K, REDUCED_MOTION_CLASS as R, type Theme as T, type AppearanceAttributes as a, appearanceAttributes as b, appearanceFromBodyClasses as c, appearanceFromColorThemeKind as d, requestedAppearance as e, resolveAppearance as f, reducedMotionFromBodyClasses as r };
@@ -0,0 +1,53 @@
1
+ /** The modes a Miragon modeler can show (guide §13.3). */
2
+ type Theme = "light" | "dark";
3
+ /** High contrast; written as `data-cd-kontrast="hoch"` on the same element as `data-cd-theme`. */
4
+ type Kontrast = "hoch";
5
+ interface Appearance {
6
+ theme: Theme;
7
+ kontrast?: Kontrast | undefined;
8
+ }
9
+ /**
10
+ * The one appearance decision of guide §13.5: a theme the modeler cannot show is never shown
11
+ * (`themes[0]` wins, so `["light"]` stays light whatever the host requests); `kontrast` always
12
+ * passes through. Throws when `themes` is empty.
13
+ */
14
+ declare function resolveAppearance(requested: Appearance, themes: readonly Theme[]): Appearance;
15
+ /**
16
+ * The appearance VS Code requests for `vscode.window.activeColorTheme.kind`:
17
+ * 1 Light → light, 2 Dark → dark, 3 HighContrast → dark + hoch, 4 HighContrastLight → light + hoch;
18
+ * any other value → light.
19
+ */
20
+ declare function appearanceFromColorThemeKind(kind: number): Appearance;
21
+ /** The values of the setting `miragon.<modeler>.colorTheme` (it ships once a modeler can show dark). */
22
+ type ColorThemeSetting = "automatic" | "light" | "dark";
23
+ /**
24
+ * The requested appearance in an IDE host: the setting's theme unless it is `automatic` (or
25
+ * missing), the contrast always from the color theme kind.
26
+ */
27
+ declare function requestedAppearance(kind: number, setting?: ColorThemeSetting | undefined): Appearance;
28
+ /**
29
+ * The fallback signal inside a VS Code webview: the body classes `vscode-high-contrast-light`
30
+ * (checked first, VS Code adds `vscode-high-contrast` to it as well), `vscode-high-contrast`,
31
+ * `vscode-dark`, `vscode-light`. `undefined` when none is set. Never use them for styling.
32
+ */
33
+ declare function appearanceFromBodyClasses(classes: Iterable<string>): Appearance | undefined;
34
+ /**
35
+ * VS Code's `workbench.reduceMotion` as the webview sees it: the body class `vscode-reduce-motion`
36
+ * (the webview host page sets it). Never use it for styling; the shell mirrors it as
37
+ * `REDUCED_MOTION_CLASS` on `<html>`.
38
+ */
39
+ declare function reducedMotionFromBodyClasses(classes: Iterable<string>): boolean;
40
+ /**
41
+ * On `<html>` while the host asks for reduced motion although the OS does not (VS Code's
42
+ * `workbench.reduceMotion`): the shell's animations stop as under `prefers-reduced-motion`, and an
43
+ * explicit fit jumps.
44
+ */
45
+ declare const REDUCED_MOTION_CLASS = "cd-shell-reduce-motion";
46
+ interface AppearanceAttributes {
47
+ "data-cd-theme": Theme;
48
+ "data-cd-kontrast"?: Kontrast;
49
+ }
50
+ /** The attributes for `<html>` (own document) or the modeler container (foreign page). */
51
+ declare function appearanceAttributes(appearance: Appearance): AppearanceAttributes;
52
+
53
+ export { type Appearance as A, type ColorThemeSetting as C, type Kontrast as K, REDUCED_MOTION_CLASS as R, type Theme as T, type AppearanceAttributes as a, appearanceAttributes as b, appearanceFromBodyClasses as c, appearanceFromColorThemeKind as d, requestedAppearance as e, resolveAppearance as f, reducedMotionFromBodyClasses as r };