@miragon/modeler-shell 0.0.0-stage → 0.2.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 (147) hide show
  1. package/API.md +2869 -0
  2. package/LICENSE +21 -0
  3. package/README.md +566 -2
  4. package/THIRD_PARTY_NOTICES.md +52 -0
  5. package/dist/appearance-BBaM_4bD.d.ts +323 -0
  6. package/dist/appearance-D3FUcbu6.d.cts +53 -0
  7. package/dist/appearance-D3FUcbu6.d.ts +53 -0
  8. package/dist/appearance-XX3JC3pI.d.cts +323 -0
  9. package/dist/chunk-66HCSJNY.js +2792 -0
  10. package/dist/chunk-66HCSJNY.js.map +1 -0
  11. package/dist/chunk-KYDG5DMX.js +136 -0
  12. package/dist/chunk-KYDG5DMX.js.map +1 -0
  13. package/dist/chunk-N4QXFF3A.js +23 -0
  14. package/dist/chunk-N4QXFF3A.js.map +1 -0
  15. package/dist/chunk-OADW3GPA.js +785 -0
  16. package/dist/chunk-OADW3GPA.js.map +1 -0
  17. package/dist/core.cjs +873 -0
  18. package/dist/core.cjs.map +1 -0
  19. package/dist/core.d.cts +246 -0
  20. package/dist/core.d.ts +246 -0
  21. package/dist/core.js +131 -0
  22. package/dist/core.js.map +1 -0
  23. package/dist/diagram-js.cjs +1147 -0
  24. package/dist/diagram-js.cjs.map +1 -0
  25. package/dist/diagram-js.d.cts +170 -0
  26. package/dist/diagram-js.d.ts +170 -0
  27. package/dist/diagram-js.js +879 -0
  28. package/dist/diagram-js.js.map +1 -0
  29. package/dist/index.cjs +3632 -0
  30. package/dist/index.cjs.map +1 -0
  31. package/dist/index.d.cts +29 -0
  32. package/dist/index.d.ts +29 -0
  33. package/dist/index.js +143 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/protocol-DhYBakWL.d.cts +15 -0
  36. package/dist/protocol-IeW8bf4f.d.ts +15 -0
  37. package/dist/shell.css +1603 -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 +4663 -0
  45. package/dist/testing.cjs.map +1 -0
  46. package/dist/testing.d.cts +231 -0
  47. package/dist/testing.d.ts +231 -0
  48. package/dist/testing.js +4483 -0
  49. package/dist/testing.js.map +1 -0
  50. package/dist/toasts-Bp0REqdq.d.cts +142 -0
  51. package/dist/toasts-Bp0REqdq.d.ts +142 -0
  52. package/dist/types-CnRrINRs.d.cts +445 -0
  53. package/dist/types-PuDZczzy.d.ts +445 -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 +3561 -0
  61. package/dist/vscode-webview.cjs.map +1 -0
  62. package/dist/vscode-webview.d.cts +84 -0
  63. package/dist/vscode-webview.d.ts +84 -0
  64. package/dist/vscode-webview.js +259 -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/feedback/appearance.ts +141 -0
  69. package/src/core/feedback/problems.ts +23 -0
  70. package/src/core/feedback/toasts.ts +118 -0
  71. package/src/core/fit.ts +220 -0
  72. package/src/core/index.ts +127 -0
  73. package/src/core/keymap.ts +295 -0
  74. package/src/core/layout.ts +69 -0
  75. package/src/core/messages.ts +159 -0
  76. package/src/core/sizes.ts +85 -0
  77. package/src/core/types.ts +106 -0
  78. package/src/core/web-chrome/bottom-row.ts +37 -0
  79. package/src/core/web-chrome/drop.ts +78 -0
  80. package/src/core/web-chrome/top-row.ts +66 -0
  81. package/src/core/zoom.ts +74 -0
  82. package/src/core.ts +2 -0
  83. package/src/diagram-js/connect.ts +75 -0
  84. package/src/diagram-js/index.ts +16 -0
  85. package/src/diagram-js/module.ts +20 -0
  86. package/src/diagram-js/palette-skin.ts +267 -0
  87. package/src/diagram-js/palette.css +269 -0
  88. package/src/diagram-js/service.ts +243 -0
  89. package/src/diagram-js/types.ts +67 -0
  90. package/src/diagram-js/viewport.ts +583 -0
  91. package/src/diagram-js.ts +2 -0
  92. package/src/dom/buttons.css +124 -0
  93. package/src/dom/element.ts +51 -0
  94. package/src/dom/feedback/appearance.ts +184 -0
  95. package/src/dom/feedback/feedback.css +435 -0
  96. package/src/dom/feedback/problems-popover.ts +287 -0
  97. package/src/dom/feedback/toasts.ts +431 -0
  98. package/src/dom/hint.ts +51 -0
  99. package/src/dom/icons.ts +309 -0
  100. package/src/dom/index.ts +42 -0
  101. package/src/dom/inspector.ts +35 -0
  102. package/src/dom/keys.ts +67 -0
  103. package/src/dom/ledger.ts +87 -0
  104. package/src/dom/legend.ts +85 -0
  105. package/src/dom/menu.ts +404 -0
  106. package/src/dom/mount.ts +731 -0
  107. package/src/dom/problems.ts +73 -0
  108. package/src/dom/shell-dom.css +969 -0
  109. package/src/dom/status.ts +72 -0
  110. package/src/dom/tooltip.ts +237 -0
  111. package/src/dom/types.ts +459 -0
  112. package/src/dom/web/drop-zone.ts +181 -0
  113. package/src/dom/web/empty-state.ts +144 -0
  114. package/src/dom/web/legal.ts +75 -0
  115. package/src/dom/web/menu-groups.ts +69 -0
  116. package/src/dom/web/primary.ts +124 -0
  117. package/src/dom/web/web.css +386 -0
  118. package/src/dom/zoom-panel.ts +154 -0
  119. package/src/index.ts +3 -0
  120. package/src/shell.css +9 -0
  121. package/src/testing/case-tools.ts +164 -0
  122. package/src/testing/cases.ts +1077 -0
  123. package/src/testing/checks.ts +868 -0
  124. package/src/testing/facts.ts +641 -0
  125. package/src/testing/geometry.ts +652 -0
  126. package/src/testing/harness.ts +187 -0
  127. package/src/testing/index.ts +19 -0
  128. package/src/testing/page-tools.ts +457 -0
  129. package/src/testing/page.ts +27 -0
  130. package/src/testing/vscode-stub.ts +237 -0
  131. package/src/testing/web-cases.ts +913 -0
  132. package/src/testing/web-checks.ts +1049 -0
  133. package/src/testing/web-facts.ts +713 -0
  134. package/src/testing-vscode-stub.ts +2 -0
  135. package/src/testing.ts +2 -0
  136. package/src/vscode/extension/html.ts +107 -0
  137. package/src/vscode/extension/index.ts +8 -0
  138. package/src/vscode/extension/reveal.ts +9 -0
  139. package/src/vscode/extension/theme-bridge.ts +85 -0
  140. package/src/vscode/extension/types.ts +36 -0
  141. package/src/vscode/protocol.ts +31 -0
  142. package/src/vscode/webview/appearance.ts +141 -0
  143. package/src/vscode/webview/index.ts +8 -0
  144. package/src/vscode/webview/mount.ts +88 -0
  145. package/src/vscode/webview/state.ts +116 -0
  146. package/src/vscode-extension.ts +2 -0
  147. 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,567 @@
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, the plumbing to a
6
+ VS Code webview, and the web-app chrome (primary action, legal line, File and Appearance groups,
7
+ problems list, toasts, empty-state card, drop zone, the web app's mode). One implementation for every modeler and every host (VS Code webview, web app,
8
+ embedded in a foreign page), so the chrome looks and behaves the same everywhere.
9
+
10
+ - **Look and behaviour** follow the Miragon design system, guide
11
+ [`modeler-design-system.md`][guide] §5, §6, §8, §9, §11, §12 and §13.3 to §13.5 (skill
12
+ `miragon-brand:modeler-tool-design`; the repository is private, the link opens for Miragon staff).
13
+ - **Mechanics** (diagram-js services, VS Code quirks, why things are done the way they are) are in
14
+ the [implementation notes][notes].
15
+ - **API**: every entry point, type and rule in [API.md][api].
16
+ - **Plan**: Phase 4 of [Miragon/modeler-api#4](https://github.com/Miragon/modeler-api/issues/4); the
17
+ Event Storming modeler is the pilot.
18
+
19
+ Zero runtime dependencies: diagram-js services are typed structurally, the `vscode` namespace and
20
+ Playwright's `page` and `test` are passed in, nothing is imported at runtime. The stylesheet reads
21
+ only the design system's `--cd-ui-*` roles and `--cd-shell-*` sizes. All texts are English by default
22
+ and overridable through one messages table; numbers go through `Intl` in the UI language.
23
+
24
+ ## Install
25
+
26
+ ```sh
27
+ npm i --save-exact @miragon/modeler-shell
28
+ ```
29
+
30
+ The modeler brings what the shell styles against and draws into:
31
+
32
+ - **diagram-js** 15.28 or later (directly or through its renderer);
33
+ - **the design tokens**: `cd-tokens.generated.css` (and `cd-tokens.generated.json` where scripts
34
+ need values) of the design system, 1.12.0 or later (the release with the `shell` group). Copy
35
+ both unchanged from the skill's `assets/` or from
36
+ <https://corporate-identity.netlify.app/brand/cd-tokens.generated.css> and
37
+ <https://corporate-identity.netlify.app/brand/cd-tokens.generated.json>, mark them as generated,
38
+ and re-copy instead of editing. The shell ships no tokens and never forks a value;
39
+ - **the fonts** Geist and Geist Mono, bundled locally, for example `@fontsource-variable/geist` and
40
+ `@fontsource-variable/geist-mono` (no CDN);
41
+ - for `./vscode/extension`: **VS Code 1.90 or later**, whose extension host runs Node 20 (the global
42
+ `crypto` that `createNonce` uses; the types are tested against `@types/vscode` 1.96). Declare
43
+ `engines.vscode` accordingly and target `node20` in the extension-host bundle.
44
+
45
+ Stylesheets, in this order. The tokens come first, unless the renderer's stylesheet already imports
46
+ them (Event Storming's does), and are never loaded twice:
47
+
48
+ ```ts
49
+ import "./theme/cd-tokens.generated.css"; // vendored, unchanged, first; left out when the renderer CSS brings it
50
+ import "@fontsource-variable/geist/wght.css";
51
+ import "@fontsource-variable/geist-mono/wght.css";
52
+ import "diagram-js/assets/diagram-js.css"; // usually through the renderer's CSS
53
+ import "@miragon/<notation>-renderer/…css";
54
+ import "@miragon/modeler-shell/shell.css"; // last
55
+ ```
56
+
57
+ The shell computes fit insets, breakpoints and layers from its own copy of the sizes (`SHELL`), while
58
+ its stylesheet reads the vendored `--cd-shell-*` tokens. Pin the two together in a unit test (Vitest
59
+ or `node:test`), not in the e2e suite, since the check is pure data:
60
+
61
+ ```ts
62
+ // test/tokens.sync.test.ts
63
+ import { SHELL, SHELL_TOKENS, shellSizesFromTokens } from "@miragon/modeler-shell/core";
64
+
65
+ expect(SHELL).toEqual(shellSizesFromTokens(vendoredTokens.shell));
66
+ expect(Object.keys(vendoredTokens.shell)).toEqual(expect.arrayContaining(Object.values(SHELL_TOKENS)));
67
+ ```
68
+
69
+ | Entry | For |
70
+ | -------------------------------------------- | ----------------------------------------------------------- |
71
+ | `@miragon/modeler-shell` | `mountShell`, icons, and everything of `./core` |
72
+ | `@miragon/modeler-shell/core` | DOM-free: fit math, keymap, zoom readout, appearance, sizes |
73
+ | `@miragon/modeler-shell/diagram-js` | `shellModule`, the viewport, `connectDiagramJs` |
74
+ | `@miragon/modeler-shell/vscode/extension` | webview HTML with CSP and nonce, theme bridge |
75
+ | `@miragon/modeler-shell/vscode/webview` | `mountVsCodeShell`, appearance handler, webview state |
76
+ | `@miragon/modeler-shell/testing` | the Playwright-driven shell conformance kit |
77
+ | `@miragon/modeler-shell/testing/vscode-stub` | a stand-in for VS Code around a webview test page |
78
+ | `@miragon/modeler-shell/shell.css` | every style of the shell |
79
+
80
+ TypeScript: every entry resolves under `moduleResolution` `node16`, `bundler` and `node10`, and
81
+ every optional input field accepts `undefined`, so a modeler compiling with
82
+ `exactOptionalPropertyTypes` forwards optional values as they are.
83
+
84
+ ## Usage in a VS Code webview
85
+
86
+ Extension host:
87
+
88
+ ```ts
89
+ import * as vscode from "vscode";
90
+ import {
91
+ connectThemeBridge,
92
+ currentAppearance,
93
+ renderWebviewHtml,
94
+ } from "@miragon/modeler-shell/vscode/extension";
95
+
96
+ const THEMES = ["light"] as const; // until the shared dark-mode release of the modelers
97
+
98
+ // activate(): a hidden tab keeps document, view and selection
99
+ vscode.window.registerCustomEditorProvider(viewType, provider, {
100
+ webviewOptions: { retainContextWhenHidden: true },
101
+ });
102
+
103
+ // resolveCustomTextEditor():
104
+ const dist = vscode.Uri.joinPath(extensionUri, "dist");
105
+ webview.options = { enableScripts: true, localResourceRoots: [dist] }; // script, CSS and font files
106
+ const asset = (file: string) => webview.asWebviewUri(vscode.Uri.joinPath(dist, file)).toString();
107
+ webview.html = renderWebviewHtml({
108
+ cspSource: webview.cspSource,
109
+ scriptUri: asset("webview.js"),
110
+ styleUris: [asset("webview.css")],
111
+ title: "Event Storming Board",
112
+ lang: "en",
113
+ appearance: currentAppearance(vscode, { themes: THEMES }), // on <html> before the first paint
114
+ });
115
+ connectThemeBridge(vscode, webviewPanel, { themes: THEMES }); // disposes itself with the panel
116
+ ```
117
+
118
+ The bridge's disposable is only for ending it before the panel closes; never push it into
119
+ `context.subscriptions`, which would keep one dead bridge per opened editor until the extension
120
+ deactivates.
121
+
122
+ Bundle the fonts as files next to the webview bundle, not as data URLs: with esbuild
123
+ `loader: { ".woff2": "file", ".woff": "file" }` and `assetNames: "fonts/[name]-[hash]"` the browser
124
+ fetches only the subsets a page's `unicode-range` needs (Geist Mono alone brings six; inlined they
125
+ roughly double the webview CSS). The CSP of `renderWebviewHtml` allows fonts from `cspSource`, and
126
+ `dist` is in `localResourceRoots`. A webview that bundles zod 4 runs `z.config({ jitless: true })` in
127
+ a module it imports before any schema module: the CSP forbids eval, zod probes once for eval when it
128
+ builds its first object schema, and the browser reports that caught `new Function` as a
129
+ `securitypolicyviolation`; under `jitless` zod skips the probe.
130
+
131
+ Webview:
132
+
133
+ ```ts
134
+ import { INVALID_STATUS_DELAY_MS } from "@miragon/modeler-shell";
135
+ import { connectDiagramJs, shellModule } from "@miragon/modeler-shell/diagram-js";
136
+ import {
137
+ mountVsCodeShell,
138
+ persistView,
139
+ readShellState,
140
+ writeShellState,
141
+ } from "@miragon/modeler-shell/vscode/webview";
142
+
143
+ const vscode = acquireVsCodeApi();
144
+ const stored = readShellState(vscode);
145
+
146
+ // The diagram first: a modeler keyboard on document must run before the shell's key listener.
147
+ // A renderer that builds its diagram lazily builds it now; width and height follow the shell's slot.
148
+ // A renderer with its own Cmd/Ctrl+Z binding turns it off here, because VS Code's document history
149
+ // owns undo (Event Storming: createCollaborativeModeler({ undoRedoKeys: false }), see API.md).
150
+ const modeler = new Modeler({ width: "100%", height: "100%", additionalModules: [shellModule] });
151
+ modeler.get("canvas");
152
+ const shell = mountVsCodeShell(document.getElementById("app")!, {
153
+ themes: ["light"],
154
+ canvas: modeler.container,
155
+ menu: {
156
+ model: [/* Level; Type captions with keepEnabledWhenReadOnly, a view toggle */],
157
+ export: [/* Export as PNG, SVG */],
158
+ help: [/* Keyboard shortcuts… (the modeler's own dialog) */],
159
+ },
160
+ legend: {
161
+ render: renderLegend,
162
+ open: stored.legendOpen,
163
+ onToggle: (open) => writeShellState(vscode, { legendOpen: open }),
164
+ },
165
+ problems: { onActivate: () => vscode.postMessage({ type: "showProblems" }) },
166
+ emptyHint: { onInsertExample: () => vscode.postMessage({ type: "insertExample" }) },
167
+ });
168
+ const connection = connectDiagramJs(shell, modeler, {
169
+ // the tight bounds of the content, no padding: the shell's fit insets are the only margin
170
+ contentBounds: () => elementBounds(), // undefined while there is nothing to fit
171
+ isEmpty: () => elementCount() === 0, // cheap: no export, no validation
172
+ });
173
+ const { viewport } = connection;
174
+ persistView(vscode, viewport); // writes the view once it rested, not on every frame
175
+
176
+ async function importText(text: string, first: boolean): Promise<void> {
177
+ const keep = first ? undefined : viewport.getViewbox();
178
+ try {
179
+ await modeler.importDSL(text);
180
+ lockCanvas(false);
181
+ shell.setReadOnly(false);
182
+ shell.setStatus(null);
183
+ if (keep) viewport.restore(keep);
184
+ else if (first && stored.view) viewport.setState(stored.view);
185
+ viewport.afterImport(); // first content fits, an empty file shows 100 %, the hint follows by itself
186
+ } catch {
187
+ // locked at once, visible after 800ms without a valid import (no flicker while typing)
188
+ lockCanvas(true); // the modeler's own lock: context pad, move, resize, delete, paste, label editor
189
+ shell.setReadOnly(true, { delayMs: INVALID_STATUS_DELAY_MS });
190
+ shell.setStatus(
191
+ { severity: "danger", text: shell.messages.invalidFile("Event Storming board") },
192
+ { delayMs: INVALID_STATUS_DELAY_MS },
193
+ );
194
+ }
195
+ }
196
+
197
+ // Imports strictly one after another; `ready` resolves once the first one settled.
198
+ let imports = Promise.resolve();
199
+ let firstImportSettled = () => {};
200
+ const ready = new Promise<void>((resolve) => (firstImportSettled = resolve));
201
+ window.addEventListener("message", ({ data }) => {
202
+ if (data?.type === "init")
203
+ imports = imports.then(() => importText(data.text, true)).then(firstImportSettled);
204
+ if (data?.type === "update") imports = imports.then(() => importText(data.text, false));
205
+ });
206
+ vscode.postMessage({ type: "ready" });
207
+
208
+ // For the e2e test page below; the bundle publishes nothing on window.
209
+ export const webview = { shell, modeler, connection, ready };
210
+ ```
211
+
212
+ `mountVsCodeShell` stops every key the webview handles on `document`, before VS Code's listener on
213
+ `window` forwards it to the workbench, and keeps the webview in the mode the extension resolved: a
214
+ modeler declaring `["light"]` stays light under any VS Code theme, high contrast passes, and VS
215
+ Code's `workbench.reduceMotion` stops the chrome's animations. Undo and redo on the canvas go to VS
216
+ Code (the shell keeps diagram-js's bindings off them), so a modeler's own keyboard binds no Cmd/Ctrl+Z
217
+ in the webview either. Keys the canvas uses act only there: Cmd/Ctrl+A on the canvas selects every
218
+ element (or is at least prevented), so VS Code's Select All never selects the chrome. The modeler's
219
+ keyboard also ignores keys with `defaultPrevented` and keys from outside the canvas container, and
220
+ puts no `tabindex` on it.
221
+
222
+ `shell.setReadOnly(true)` locks the palette (all but Lasso and Hand) and the Model entries of the
223
+ menu. View toggles stay usable: the shell's Legend checkbox and the legend's close button, and every
224
+ 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,
225
+ resize, delete, paste or label editor. Content the modeler adds to `shell.root` (a "Keyboard
226
+ shortcuts…" dialog) is the modeler's too: it removes it in its own teardown, for example on
227
+ `diagram.destroy`, since `shell.destroy()` removes only what the shell added. The complete Event
228
+ Storming example, including the export notification, is in [API.md][api]. Document sync, undo through
229
+ the document and diagnostics stay in the modeler for now.
230
+
231
+ ## Usage in a web app
232
+
233
+ The page's head writes the mode before the first paint. A light-only modeler keeps
234
+ `<html lang="en" data-cd-theme="light">` static and lets the build put the metas and the contrast
235
+ line in place of a marker in `index.html`:
236
+
237
+ ```ts
238
+ // vite.config.ts
239
+ import { webHeadMarkup } from "@miragon/modeler-shell/core";
240
+ import tokens from "./src/theme/cd-tokens.generated.json" with { type: "json" };
241
+
242
+ const themeColors = { light: tokens.fest["--cd-blau"], dark: tokens.dunkel["--cd-ui-grund"] };
243
+ const modePlugin = {
244
+ name: "miragon-mode",
245
+ transformIndexHtml: (html: string) =>
246
+ html.replace("<!-- miragon-mode -->", webHeadMarkup({ themes: ["light"], themeColors })),
247
+ };
248
+ ```
249
+
250
+ ```ts
251
+ // src/main.ts
252
+ import {
253
+ commandKeyLabel,
254
+ defaultMessages,
255
+ detectPlatform,
256
+ mountShell,
257
+ problemSummary,
258
+ } from "@miragon/modeler-shell";
259
+ import { connectDiagramJs, shellModule } from "@miragon/modeler-shell/diagram-js";
260
+
261
+ // The diagram first: a modeler keyboard on document must run before the shell's key listener.
262
+ const modeler = new Modeler({ width: "100%", height: "100%", additionalModules: [shellModule] });
263
+ modeler.get("canvas");
264
+ const platform = detectPlatform(navigator);
265
+ const shell = mountShell(document.getElementById("app")!, {
266
+ host: "web",
267
+ canvas: modeler.container,
268
+ lang: "en",
269
+ themes: ["light"], // with "dark": also appearance: { themeColors }, and the menu gets Appearance
270
+ primary: { onSelect: share }, // "Share", top right; the label collapses before the palette moves
271
+ legal: { links: [{ label: "GitHub", href: "https://github.com/Miragon/…" }] },
272
+ file: {
273
+ newDiagram: { onSelect: newDiagram },
274
+ // ⌘O or Ctrl+O; also the start screen's "Open a file…" link
275
+ open: { onSelect: pickFile, shortcut: commandKeyLabel("O", platform, defaultMessages.keyNames) },
276
+ showExample: { onSelect: example },
277
+ },
278
+ menu: { model: [/* … */], export: [/* … */], help: [/* Keyboard shortcuts… */] },
279
+ problems: { onSelectItem: (item) => selectElement(item.elementId) },
280
+ legend: { render: renderLegend, open: storedLegendOpen(), onToggle: storeLegendOpen },
281
+ emptyState: {
282
+ title: "BPMN Modeler",
283
+ text: "Start a new diagram, or explore the example.",
284
+ komet: kometUrl, // the vendored, unchanged miragon-komet-blau.svg
285
+ onNewDiagram: newDiagram,
286
+ onShowExample: example,
287
+ },
288
+ dropZone: { accept: [".bpmn", "application/xml", "text/xml"], onDrop: ([file]) => openFile(file) },
289
+ });
290
+ const { viewport } = connectDiagramJs(shell, modeler, { contentBounds: () => elementBounds() });
291
+
292
+ // Every new document opens like the first, and the start screen goes with it.
293
+ async function openDocument(xml: string): Promise<void> {
294
+ viewport.newDocument();
295
+ const { warnings } = await modeler.importXML(xml);
296
+ viewport.afterImport();
297
+ shell.setEmptyState(false);
298
+ shell.setProblems(
299
+ problemSummary(warnings.map((warning) => ({ severity: "warning", message: warning.message }))),
300
+ );
301
+ }
302
+
303
+ // A fresh visit without a document in the link shows the start screen.
304
+ if (!linkedDocument) shell.setEmptyState(true);
305
+ shell.toast({ status: "success", text: "Link copied" }); // after a share
306
+ ```
307
+
308
+ The `web` host renders the whole web column of guide §5.2: the primary action at the top right,
309
+ the legal line at the bottom left with pill and legend above it, the File group first and, for a
310
+ dark-capable modeler, the Appearance group in the menu, the problems list above the pill, toasts at
311
+ the bottom centre (announced through two live regions), the start screen and a page-wide drop zone.
312
+ It prevents the browser's page zoom for the chrome keys (outside text fields, modal dialogs and the
313
+ start screen) and fits with the web insets. The web app's
314
+ own menu, zoom, fit, toast, legal and empty-state markup goes away; what stays the modeler's is its
315
+ content: the share link, file reading, examples, the Model and Export entries and the shortcuts
316
+ dialog. See [API.md][api], "Web host (0.2)", and the complete Event Storming example there.
317
+
318
+ ## Usage in a foreign page
319
+
320
+ `mountShell(container, { host: "embedded", themes: ["light"], canvas })` on an explicitly sized
321
+ container: the root carries the tool's `lang` itself, keys act only while the focus is inside the
322
+ modeler, and the page's `<html>` stays untouched. Until the `@miragon/modeler-api` contract gains
323
+ its `appearance` surface (a later 1.x minor), an embedded modeler has no appearance channel, so the
324
+ root carries its own `data-cd-theme` (the first of `themes`, light) and stays light inside a dark
325
+ host page; it takes the page's `data-cd-kontrast="hoch"` over from the nearest mode carrier above
326
+ it. The page's element styles (`button { text-transform: uppercase }`) do not reach the chrome.
327
+ Toasts show at the bottom centre of the modeler, and the pill opens the problems list unless the
328
+ modeler passes `problems.onActivate` (an embedding host with a list of its own).
329
+
330
+ ## The conformance kit
331
+
332
+ Every modeler runs the kit in its own Playwright suite against its real webview or page. It checks
333
+ the geometry of every island (±1px), ARIA and the keyboard models, that every chrome key acts exactly
334
+ once and never reaches VS Code, that the canvas's own keys (Cmd/Ctrl+A) stay on the canvas, that
335
+ nothing is ever half-dark (the painted canvas surround and the palette's colours included), that
336
+ Enter on a creating palette entry adds one selected element, that an invalid file locks the canvas,
337
+ and that `destroy()` leaves nothing behind. Five more cases cover the web column (`webChrome`,
338
+ `emptyState`, `toasts`, `problemsList`, `dropZone`); in the IDE and embedded hosts they assert that
339
+ none of it shows.
340
+
341
+ For the `ide` host the suite serves a test page that puts a stand-in for VS Code around the
342
+ modeler's webview code: VS Code's default webview styles, `<html>` as `renderWebviewHtml` writes it,
343
+ the host page's theme classes, `acquireVsCodeApi()` and the real theme bridge. Serve
344
+ `e2e/webview/` as the root of a Vite server, so the page is `/webview-test.html`:
345
+
346
+ ```ts
347
+ // e2e/webview/webview-test.ts, the entry of e2e/webview/webview-test.html
348
+ import { installVsCodeWebviewStub, stubOptionsFromSearch } from "@miragon/modeler-shell/testing/vscode-stub";
349
+
350
+ // before the webview runs, which calls acquireVsCodeApi() and mounts the shell
351
+ installVsCodeWebviewStub({ ...stubOptionsFromSearch(location.search), themes: ["light"] });
352
+ const { webview } = await import("../../src/webview/main"); // exports { shell, modeler, connection, ready }
353
+
354
+ // for the harness only: the shipped bundle publishes nothing on window
355
+ window.shell = webview.shell;
356
+ window.modeler = webview.modeler;
357
+ window.connection = webview.connection;
358
+ void webview.ready.then(() => (window.webviewReady = true)); // the first import settled
359
+
360
+ // there is no extension host: post the document the way the extension would
361
+ window.postMessage({ type: "init", text: EXAMPLE_BOARD }, "*");
362
+ ```
363
+
364
+ `ready`, `webviewReady` and the window globals belong to the modeler's test page (declared in a
365
+ `.d.ts` of the suite), not to the shell: the webview resolves `ready` once its first `init` import
366
+ settled.
367
+
368
+ ```ts
369
+ // e2e/shell-conformance.spec.ts
370
+ import { test } from "@playwright/test";
371
+ import { registerShellConformance, vsCodeStubSearch } from "@miragon/modeler-shell/testing";
372
+
373
+ test.describe("shell conformance", () => {
374
+ test.describe.configure({ timeout: 60_000 }); // geometry measures three sizes
375
+ registerShellConformance(
376
+ {
377
+ host: "ide",
378
+ themes: ["light"],
379
+ absent: ["inspector"], // islands this modeler does not have: never visible, never skipped
380
+ async open(page, { size, requested }) {
381
+ await page.setViewportSize(size);
382
+ await page.goto(`/webview-test.html?${vsCodeStubSearch(requested)}`);
383
+ await page.waitForFunction(() => window.webviewReady === true); // set by the test page
384
+ },
385
+ async destroy(page) {
386
+ // the webview's own teardown path: connection, diagram (with the modeler's dialogs), shell
387
+ await page.evaluate(() => {
388
+ window.connection.destroy();
389
+ window.modeler.destroy();
390
+ window.shell.destroy();
391
+ });
392
+ },
393
+ show: {
394
+ async legend(page) {
395
+ await page.evaluate(() => window.shell.legend?.setOpen(true));
396
+ },
397
+ async problems(page) {
398
+ await page.evaluate(() => window.postMessage({ type: "problems", count: 2, errors: 1 }, "*"));
399
+ },
400
+ async status(page) {
401
+ await page.evaluate(() =>
402
+ window.shell.setStatus({ severity: "danger", text: window.shell.messages.invalidFile("board") }),
403
+ );
404
+ },
405
+ async invalid(page) {
406
+ // the way the host makes the file invalid: a document update the parser rejects
407
+ await page.evaluate((text) => window.postMessage({ type: "update", text }, "*"), INVALID_BOARD);
408
+ },
409
+ },
410
+ },
411
+ test,
412
+ {
413
+ onSkip(caseName, skips) {
414
+ // with `absent` listing what the modeler lacks, every remaining skip is a gap of this suite
415
+ throw new Error(`${caseName}: ${skips.map(({ hook, reason }) => `${hook}: ${reason}`).join("; ")}`);
416
+ },
417
+ },
418
+ );
419
+ });
420
+ ```
421
+
422
+ A web app's harness opens its page and adds the web column's hooks and the files to drop. The kit
423
+ emulates the system's colour scheme and contrast (`page.emulateMedia`) before every `open`, so the
424
+ page loads without a stored mode choice:
425
+
426
+ ```ts
427
+ // e2e/shell-conformance.spec.ts of a web app
428
+ registerShellConformance(
429
+ {
430
+ host: "web",
431
+ themes: ["light"],
432
+ absent: ["inspector", "invalid"],
433
+ async open(page, { size }) {
434
+ await page.setViewportSize(size);
435
+ await page.goto("/?example");
436
+ await page.waitForFunction(() => window.appReady === true);
437
+ },
438
+ async destroy(page) {
439
+ await page.evaluate(() => window.app.destroy());
440
+ },
441
+ show: {
442
+ async legend(page) {
443
+ /* … */
444
+ },
445
+ async problems(page) {
446
+ /* setProblems with items, so the list has rows */
447
+ },
448
+ async status(page) {
449
+ /* … */
450
+ },
451
+ async emptyState(page) {
452
+ /* the start screen of a fresh visit */
453
+ },
454
+ async toast(page, toast) {
455
+ await page.evaluate(
456
+ ({ status, text, actionLabel }) =>
457
+ window.app.shell.toast({
458
+ status,
459
+ text,
460
+ ...(actionLabel ? { action: { label: actionLabel, onSelect() {} } } : {}),
461
+ }),
462
+ toast,
463
+ );
464
+ },
465
+ },
466
+ drop: {
467
+ accepted: { name: "board.json", type: "application/json", text: EXAMPLE_JSON },
468
+ rejected: { name: "notes.md", type: "", text: "# Notes" },
469
+ },
470
+ },
471
+ test,
472
+ { onSkip: failOnSkips },
473
+ );
474
+ ```
475
+
476
+ The harness opens the modeler with the appearance the kit requests, the way its host would; for
477
+ the `embedded` host the kit puts the foreign page itself in that mode. A skip names the harness
478
+ field it misses (`{ hook: "legend", reason }`; `hook: "destroy"` without a `destroy`; `hook: "open"`
479
+ when the opened document lacks what a case needs, such as a shape for `readOnly`); without `onSkip`
480
+ the kit warns on the console. What each case asserts, and what the kit installs in the page, is
481
+ listed in [API.md][api].
482
+
483
+ `registerShellConformance` declares every test inside the kit, so a runner reports them at the kit's
484
+ source (the package ships `src/` without its tests, so the location resolves). For test locations in
485
+ the suite's own spec file, register the cases there:
486
+
487
+ ```ts
488
+ import { shellConformanceCases } from "@miragon/modeler-shell/testing";
489
+
490
+ for (const conformanceCase of shellConformanceCases()) {
491
+ test(`shell conformance: ${conformanceCase.name}`, async ({ page }) => {
492
+ const outcome = await conformanceCase.run(page, harness);
493
+ if (outcome?.skipped) failOnSkips(conformanceCase.name, outcome.skipped);
494
+ });
495
+ }
496
+ ```
497
+
498
+ The test page runs the webview from source without a CSP. Add one smoke test of the production bundle
499
+ behind the HTML and CSP of `renderWebviewHtml` that fails on any `securitypolicyviolation` (the recipe
500
+ is in [API.md][api], `./vscode/extension`).
501
+
502
+ ## Development
503
+
504
+ Run from the repository root (`-w packages/shell` for this package only):
505
+
506
+ ```sh
507
+ npm run build -w packages/shell # tsup: ESM + CJS + d.ts, dist/shell.css
508
+ npm run typecheck -w packages/shell # src, core without the DOM library, demo
509
+ npm test -w packages/shell # Vitest: node (core, boundaries) and happy-dom projects
510
+ npx playwright install chromium # once
511
+ npm run test:e2e -w packages/shell # Playwright against demo/
512
+ npm run demo -w packages/shell # the demo page on http://localhost:5181
513
+ ```
514
+
515
+ The demo page (`demo/`) is a small diagram-js modeler around the shell. Its query parameters choose
516
+ the host (`host=ide|web|embedded`), the modes the modeler declares (`themes=light,dark`), what the
517
+ host requests (`theme`, `kontrast=hoch`; embedded: the foreign page's own mode), the `ide` host's
518
+ `colorTheme` setting and `reduceMotion`, and the state to start in (`legend=open`, `inspector`,
519
+ `problems=2:1`, `status=danger`, `invalid`, `empty`, which opens at 100 %). In the `ide` host it runs
520
+ inside the stub of `./testing/vscode-stub`, and keeps view and legend state through
521
+ `acquireVsCodeApi()`.
522
+
523
+ The web demo (`/web.html`) is the same modeler as a Miragon web app passes it to `mountShell`: the
524
+ whole web column, the head markup of `webHeadMarkup` through the Vite config, and `window.__webDemo`
525
+ for the harness. Its query parameters: `themes=light,dark`, `empty` (the start screen of a fresh
526
+ visit), `legend` (`legend=open`), `inspector`, `problems` (the pill with its list) and `invalid`.
527
+
528
+ The e2e suite runs the conformance kit for the `ide`, `web` and `embedded` hosts with a light-only
529
+ and a dark-capable modeler, switches VS Code themes and the `colorTheme` setting at runtime, checks
530
+ VS Code's reduce motion, the chrome's typography against VS Code's default styles and a hostile
531
+ foreign page, the canvas slot's paint and focus ring, the web chrome's colours, radii and states
532
+ (primary action, legal line, toasts, drop zone, empty state, high contrast and forced colours) and
533
+ the web demo's own handlers, and proves with saboteurs that every case of the kit fails when its
534
+ rule is broken.
535
+
536
+ | Directory | Module |
537
+ | --------------------------------------- | ------------------------------------------------------------------------------------------ |
538
+ | `src/core` | DOM-free: fit math, keymap, zoom readout, appearance, sizes, texts |
539
+ | `src/dom` | `mountShell`: menu, zoom panel, tooltip, pill, status, slots, keys |
540
+ | `src/dom/web`, `src/core/web-chrome` | web column: primary action, legal line, File and Appearance groups, empty state, drop zone |
541
+ | `src/dom/feedback`, `src/core/feedback` | toasts, problems popover, the web app's mode |
542
+ | `src/diagram-js` | `shellModule`, viewport, `connectDiagramJs`, palette skin |
543
+ | `src/vscode` | extension side (HTML, theme bridge) and webview side |
544
+ | `src/testing` | the Playwright-driven conformance kit and the VS Code stub |
545
+ | `src/*.ts`, `src/shell.css` | the entry points |
546
+ | `test/` | package boundaries, token drift, the unit tests' helpers |
547
+ | `demo/`, `e2e/` | the demo modeler and the e2e suite |
548
+ | `fixtures/tokens` | the token files, vendored unchanged for the tests and the demo |
549
+
550
+ A module imports another one only through its `index.ts`; `src/core` imports nothing outside
551
+ itself and compiles without the DOM library. `test/package-boundaries.test.ts` enforces this, the
552
+ zero runtime dependencies, the exact devDependency pins, the token-only stylesheets and the published
553
+ files (`dist`, `src` without its tests, `API.md` and the notices).
554
+
555
+ ## License
556
+
557
+ MIT. The inlined Lucide icons are ISC (© Lucide Icons and Contributors), and `check`, `circle-x`,
558
+ `maximize`, `minus`, `plus`, `triangle-alert` and `x` derive from Feather, MIT (© Cole Bemis), as
559
+ Lucide's LICENSE lists them; see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). `dist` keeps that
560
+ notice as a legal comment in the chunk with the icons, but a bundler's `legalComments` setting may
561
+ drop it: a VSIX or a web bundle that contains the shell carries the Lucide and Feather notices of
562
+ THIRD_PARTY_NOTICES.md itself. Vite 8 (Rolldown with the Oxc minifier) drops legal comments by
563
+ default; `build.rolldownOptions.output.comments.legal: true` keeps the notice in the bundle.
564
+
565
+ [guide]: https://github.com/Miragon/corporate-identity/blob/main/plugins/miragon-brand/skills/modeler-tool-design/assets/modeler-design-system.md
566
+ [notes]: https://github.com/Miragon/modeler-api/blob/main/docs/shell/implementation-notes.md
567
+ [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.