@miragon/modeler-shell 0.1.0 → 0.2.1
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/API.md +1051 -84
- package/README.md +162 -31
- package/dist/appearance-BBaM_4bD.d.ts +323 -0
- package/dist/appearance-XX3JC3pI.d.cts +323 -0
- package/dist/{chunk-6LQOFACK.js → chunk-A4YGKH4Y.js} +1252 -31
- package/dist/chunk-A4YGKH4Y.js.map +1 -0
- package/dist/{chunk-RJRFR6N3.js → chunk-KYDG5DMX.js} +2 -2
- package/dist/{chunk-5JSZ4VGA.js → chunk-OADW3GPA.js} +240 -6
- package/dist/chunk-OADW3GPA.js.map +1 -0
- package/dist/core.cjs +260 -6
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +81 -6
- package/dist/core.d.ts +81 -6
- package/dist/core.js +43 -3
- package/dist/diagram-js.cjs +34 -2
- package/dist/diagram-js.cjs.map +1 -1
- package/dist/diagram-js.d.cts +10 -3
- package/dist/diagram-js.d.ts +10 -3
- package/dist/diagram-js.js +14 -2
- package/dist/diagram-js.js.map +1 -1
- package/dist/index.cjs +1658 -196
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -9
- package/dist/index.d.ts +18 -9
- package/dist/index.js +44 -4
- package/dist/shell.css +692 -3
- package/dist/testing-vscode-stub.js +2 -2
- package/dist/testing.cjs +2849 -550
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +75 -9
- package/dist/testing.d.ts +75 -9
- package/dist/testing.js +2744 -558
- package/dist/testing.js.map +1 -1
- package/dist/toasts-Bp0REqdq.d.cts +142 -0
- package/dist/toasts-Bp0REqdq.d.ts +142 -0
- package/dist/{types-OuFlqZp0.d.cts → types-CnRrINRs.d.cts} +170 -9
- package/dist/{types-DnZn9CPw.d.ts → types-PuDZczzy.d.ts} +170 -9
- package/dist/vscode-extension.js +2 -2
- package/dist/vscode-webview.cjs +1546 -178
- package/dist/vscode-webview.cjs.map +1 -1
- package/dist/vscode-webview.d.cts +6 -4
- package/dist/vscode-webview.d.ts +6 -4
- package/dist/vscode-webview.js +11 -2
- package/dist/vscode-webview.js.map +1 -1
- package/package.json +2 -2
- package/src/core/feedback/appearance.ts +141 -0
- package/src/core/feedback/problems.ts +23 -0
- package/src/core/feedback/toasts.ts +118 -0
- package/src/core/fit.ts +18 -3
- package/src/core/index.ts +43 -0
- package/src/core/keymap.ts +20 -0
- package/src/core/messages.ts +71 -0
- package/src/core/types.ts +24 -2
- package/src/core/web-chrome/bottom-row.ts +37 -0
- package/src/core/web-chrome/drop.ts +78 -0
- package/src/core/web-chrome/top-row.ts +66 -0
- package/src/diagram-js/service.ts +4 -1
- package/src/diagram-js/viewport.ts +19 -0
- package/src/dom/buttons.css +124 -0
- package/src/dom/feedback/appearance.ts +184 -0
- package/src/dom/feedback/feedback.css +435 -0
- package/src/dom/feedback/problems-popover.ts +287 -0
- package/src/dom/feedback/toasts.ts +431 -0
- package/src/dom/index.ts +13 -0
- package/src/dom/keys.ts +8 -0
- package/src/dom/menu.ts +28 -17
- package/src/dom/mount.ts +290 -20
- package/src/dom/problems.ts +5 -4
- package/src/dom/shell-dom.css +7 -4
- package/src/dom/tooltip.ts +10 -0
- package/src/dom/types.ts +188 -6
- package/src/dom/web/drop-zone.ts +181 -0
- package/src/dom/web/empty-state.ts +144 -0
- package/src/dom/web/legal.ts +75 -0
- package/src/dom/web/menu-groups.ts +69 -0
- package/src/dom/web/primary.ts +124 -0
- package/src/dom/web/web.css +386 -0
- package/src/shell.css +3 -0
- package/src/testing/case-tools.ts +164 -0
- package/src/testing/cases.ts +209 -144
- package/src/testing/checks.ts +51 -15
- package/src/testing/facts.ts +56 -2
- package/src/testing/geometry.ts +206 -26
- package/src/testing/harness.ts +56 -7
- package/src/testing/index.ts +4 -0
- package/src/testing/page-tools.ts +24 -1
- package/src/testing/page.ts +10 -0
- package/src/testing/web-cases.ts +913 -0
- package/src/testing/web-checks.ts +1049 -0
- package/src/testing/web-facts.ts +713 -0
- package/src/vscode/webview/mount.ts +13 -1
- package/dist/chunk-5JSZ4VGA.js.map +0 -1
- package/dist/chunk-6LQOFACK.js.map +0 -1
- package/dist/keymap-CObQE6q6.d.cts +0 -151
- package/dist/keymap-DORzHnK-.d.ts +0 -151
- package/dist/types-B6NfkiUG.d.cts +0 -66
- package/dist/types-B6NfkiUG.d.ts +0 -66
- /package/dist/{chunk-RJRFR6N3.js.map → chunk-KYDG5DMX.js.map} +0 -0
package/API.md
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
# @miragon/modeler-shell 0.
|
|
1
|
+
# @miragon/modeler-shell 0.2 — public API
|
|
2
2
|
|
|
3
3
|
The shared, framework-free implementation of everything around a Miragon diagram-js modeler: menu
|
|
4
4
|
button with dropdown, horizontal palette skin, chrome tooltip, zoom panel, zoom and fit keys, fit,
|
|
5
|
-
problems pill, status box, empty-file hint, legend and inspector slots,
|
|
6
|
-
webview
|
|
7
|
-
|
|
5
|
+
problems pill, status box, empty-file hint, legend and inspector slots, the plumbing to a VS Code
|
|
6
|
+
webview, and (0.2) the web-app chrome: primary action, legal line, File and Appearance groups,
|
|
7
|
+
problems popover, toasts, empty-state card, drop zone and the web app's mode. Planned in
|
|
8
|
+
[Miragon/modeler-api#4](https://github.com/Miragon/modeler-api/issues/4) (Phase 4); the first
|
|
9
|
+
consumer is the Event Storming modeler, in VS Code (0.1) and in its web app (0.2, see
|
|
10
|
+
[Usage: Event Storming in the web app](#usage-event-storming-in-the-web-app)).
|
|
8
11
|
|
|
9
12
|
- **Look and behaviour** come from the Miragon design system, guide `modeler-design-system.md`
|
|
10
|
-
(skill `miragon-brand:modeler-tool-design
|
|
11
|
-
§13.5. Section numbers below (§x.y) refer to it.
|
|
13
|
+
(skill `miragon-brand:modeler-tool-design`, design system 1.13.0), §2, §5.1 to §5.15, §6, §7, §8,
|
|
14
|
+
§9, §11, §12 and §13.3 to §13.5. Section numbers below (§x.y) refer to it.
|
|
12
15
|
- **Mechanics** (diagram-js services, VS Code quirks) are in
|
|
13
16
|
[`docs/shell/implementation-notes.md`](../../docs/shell/implementation-notes.md).
|
|
14
17
|
- **Zero runtime dependencies.** The shell never imports `diagram-js`, `didi`, `vscode`, Lucide or
|
|
@@ -17,22 +20,27 @@ webview. Planned in [Miragon/modeler-api#4](https://github.com/Miragon/modeler-a
|
|
|
17
20
|
- **Tokens are the consumer's.** The modeler vendors `cd-tokens.generated.css` unchanged; the shell
|
|
18
21
|
CSS reads only `var(--cd-ui-*)` roles, `var(--cd-shell-*)` sizes and the mode-independent
|
|
19
22
|
non-colour tokens `--cd-radius-*`, `--cd-shadow-*`, `--cd-motion-*`, `--cd-ease`, `--cd-ease-out`.
|
|
20
|
-
It contains no colour literal and
|
|
23
|
+
It contains no colour literal and no fixed palette token (`--cd-blau`, `--cd-schwarz`, …), with
|
|
24
|
+
one exception the guide itself makes: the primary button's fill is `--cd-blau` with `--cd-weiss`
|
|
25
|
+
text in both modes, hover `--cd-blau-link` (§8: "Feste Werte nimmst du nur für die Fläche von
|
|
26
|
+
Primär- und Destruktiv-Button"). Those three tokens appear in `src/dom/buttons.css` only, and the
|
|
27
|
+
stylesheet test allows them nowhere else (see [Buttons](#buttons)). Values the shell needs in
|
|
28
|
+
script (the two `theme-color`s) come from the modeler's vendored JSON as well.
|
|
21
29
|
- **English by default.** Every text comes from one messages table (`ShellMessages`), every number
|
|
22
30
|
goes through `Intl` in the UI language.
|
|
23
31
|
|
|
24
32
|
## Entry points
|
|
25
33
|
|
|
26
|
-
| Entry | For
|
|
27
|
-
| -------------------------------------------- |
|
|
28
|
-
| `@miragon/modeler-shell` | the chrome (`mountShell
|
|
29
|
-
| `@miragon/modeler-shell/core` | fit math, keymap, zoom readout, appearance, sizes, messages | no |
|
|
30
|
-
| `@miragon/modeler-shell/diagram-js` | `shellModule`, viewport, `connectDiagramJs`
|
|
31
|
-
| `@miragon/modeler-shell/vscode/extension` | webview HTML, nonce, theme bridge (extension host)
|
|
32
|
-
| `@miragon/modeler-shell/vscode/webview` | `mountVsCodeShell`, appearance handler, webview state
|
|
33
|
-
| `@miragon/modeler-shell/testing` | Playwright-driven shell conformance kit
|
|
34
|
-
| `@miragon/modeler-shell/testing/vscode-stub` | a stand-in for VS Code around a webview test page
|
|
35
|
-
| `@miragon/modeler-shell/shell.css` | every style of the shell
|
|
34
|
+
| Entry | For | Needs a DOM |
|
|
35
|
+
| -------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------ |
|
|
36
|
+
| `@miragon/modeler-shell` | the chrome (`mountShell`, web chrome included), icons, plus all of `./core` | yes (at call time) |
|
|
37
|
+
| `@miragon/modeler-shell/core` | fit math, keymap, zoom readout, appearance, sizes, messages, the web host's decisions and head script | no |
|
|
38
|
+
| `@miragon/modeler-shell/diagram-js` | `shellModule`, viewport, `connectDiagramJs` | yes |
|
|
39
|
+
| `@miragon/modeler-shell/vscode/extension` | webview HTML, nonce, theme bridge (extension host) | no |
|
|
40
|
+
| `@miragon/modeler-shell/vscode/webview` | `mountVsCodeShell`, appearance handler, webview state | yes |
|
|
41
|
+
| `@miragon/modeler-shell/testing` | Playwright-driven shell conformance kit | runs in Node, drives a browser |
|
|
42
|
+
| `@miragon/modeler-shell/testing/vscode-stub` | a stand-in for VS Code around a webview test page | yes (in the test page) |
|
|
43
|
+
| `@miragon/modeler-shell/shell.css` | every style of the shell | — |
|
|
36
44
|
|
|
37
45
|
All JavaScript entries ship as ESM and CJS with types (`typesVersions` covers `moduleResolution:
|
|
38
46
|
node10`); importing any of them has no side effects. The package also ships `src/` without its tests,
|
|
@@ -91,8 +99,22 @@ interface Insets {
|
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
type ShellHost = "ide" | "web" | "embedded";
|
|
94
|
-
type ChromeIsland =
|
|
95
|
-
|
|
102
|
+
type ChromeIsland =
|
|
103
|
+
| "menu"
|
|
104
|
+
| "palette"
|
|
105
|
+
| "primary"
|
|
106
|
+
| "zoom"
|
|
107
|
+
| "problems"
|
|
108
|
+
| "legend"
|
|
109
|
+
| "inspector"
|
|
110
|
+
| "status"
|
|
111
|
+
// 0.2, web column (toasts and problems-popover also embedded)
|
|
112
|
+
| "legal"
|
|
113
|
+
| "toasts"
|
|
114
|
+
| "empty-state"
|
|
115
|
+
| "drop-zone"
|
|
116
|
+
| "problems-popover";
|
|
117
|
+
const CHROME_ISLANDS: readonly ChromeIsland[]; // in this order
|
|
96
118
|
type PaletteRow = "top" | "second-row";
|
|
97
119
|
|
|
98
120
|
interface ZoomTarget {
|
|
@@ -182,6 +204,8 @@ interface FitLayout {
|
|
|
182
204
|
host: ShellHost;
|
|
183
205
|
paletteRow: PaletteRow;
|
|
184
206
|
problemsVisible: boolean;
|
|
207
|
+
zoomRaised?: boolean; // web: the zoom panel stands above the legal line (zoomRowRaised)
|
|
208
|
+
legalHeight?: number; // web: the legal line's measured height, 20 per line (default one line)
|
|
185
209
|
legendWidth?: number; // open legend's rendered width, undefined when closed/absent
|
|
186
210
|
inspector?: { sheetTop?: number }; // present = open; sheetTop measured when it is a bottom sheet
|
|
187
211
|
}
|
|
@@ -204,7 +228,9 @@ function easeStandard(progress: number): number; // cubic-bezier(0.4, 0, 0.2, 1)
|
|
|
204
228
|
the other dimension. An axis whose insets leave no room (a container lower or narrower than its
|
|
205
229
|
chrome) centres in the whole container.
|
|
206
230
|
- `fitInsets` (§5.9): top 88, 140 in the second row; left and right 24; bottom 64, in the `web` host
|
|
207
|
-
with a visible pill
|
|
231
|
+
with a visible pill or with the zoom panel raised above the legal line (`zoomRaised`) 92, plus 20
|
|
232
|
+
for every extra line of a wrapped legal line (`legalHeight`); right 324 (12 + 288 + 24) with an
|
|
233
|
+
open side inspector (container wider
|
|
208
234
|
than 760); left 12 + legend width + 24 with an open legend in containers wider than 440
|
|
209
235
|
(`legendWidth <= 0` counts as closed); bottom `container.height − sheetTop + 12` with an open
|
|
210
236
|
bottom sheet (up to 760). The sheet hides zoom panel, pill and legend (§5.11), so while it is open
|
|
@@ -263,6 +289,7 @@ interface KeyContext {
|
|
|
263
289
|
dialogOpen: boolean; // any shown [role="dialog"], [role="alertdialog"] or <dialog open>, modal or not
|
|
264
290
|
modalOpen: boolean; // one of them with aria-modal="true" or :modal
|
|
265
291
|
defaultPrevented: boolean; // the canvas or the modeler handled the key
|
|
292
|
+
startScreen?: boolean; // web: the start screen shows; the chrome keys go to the browser
|
|
266
293
|
}
|
|
267
294
|
type KeyAction = ChromeKey | "closeMenu";
|
|
268
295
|
interface KeyDecision {
|
|
@@ -281,6 +308,7 @@ function detectPlatform(navigator?: NavigatorLike): Platform;
|
|
|
281
308
|
function shortcutLabel(key: ChromeKey, platform: Platform, keyNames: ShellMessages["keyNames"]): string;
|
|
282
309
|
function ariaKeyShortcuts(key: ChromeKey, platform: Platform): string;
|
|
283
310
|
function ariaKeyShortcutsFromLabel(label: string): string | undefined;
|
|
311
|
+
function commandKeyLabel(key: string, platform: Platform, keyNames: { ctrl: string }): string; // "⌘O", "Ctrl+O"
|
|
284
312
|
```
|
|
285
313
|
|
|
286
314
|
`matchChromeKey` (§5.8) — Cmd/Ctrl is `metaKey || ctrlKey` on every platform, nothing matches with
|
|
@@ -300,6 +328,7 @@ it. `stopPropagation` is only ever `true` in the `ide` host.
|
|
|
300
328
|
| --------------------------------------------------------- | ----------- | -------------- | ----------------------- |
|
|
301
329
|
| Escape (no modifier), menu open, not composing | `closeMenu` | yes | yes |
|
|
302
330
|
| Escape, popover or dialog open, or menu open while IME | — | no | yes |
|
|
331
|
+
| zoom key or Shift+1 behind the web app's start screen | — | no | — (web only) |
|
|
303
332
|
| zoom key, not composing, not in a text field, no modal | the key | yes | yes |
|
|
304
333
|
| zoom key while composing, in a text field or with a modal | — | no | yes |
|
|
305
334
|
| Shift+1, not composing, not in a text field, no modal | `fit` | yes | yes |
|
|
@@ -386,6 +415,29 @@ interface ShellMessages {
|
|
|
386
415
|
invalidFile(diagram: string): string;
|
|
387
416
|
// "This file isn't a valid <diagram> right now. The diagram shows the last valid version and is read-only until you fix the text."
|
|
388
417
|
keyNames: { ctrl: string; shift: string; plus: string; minus: string }; // Ctrl, Shift, Plus, Minus
|
|
418
|
+
// 0.2, web host (guide §2 shared terms)
|
|
419
|
+
share: string; // "Share": default label of the primary action
|
|
420
|
+
legal: string; // "Legal": aria-label of the legal line's <nav>
|
|
421
|
+
copyright(year: number): string; // "© 2026 Miragon GmbH"
|
|
422
|
+
opensInNewTab: string; // "Opens in a new tab": the legal links' description for screen readers
|
|
423
|
+
newDiagram: string; // "New diagram": File group and the empty state's primary button
|
|
424
|
+
openFile: string; // "Open…"
|
|
425
|
+
showExample: string; // "Show example": File group and the empty state's ghost button
|
|
426
|
+
importFile: string; // "Import…"
|
|
427
|
+
appearance: string; // "Appearance"
|
|
428
|
+
appearanceSystem: string; // "System"
|
|
429
|
+
appearanceLight: string; // "Light"
|
|
430
|
+
appearanceDark: string; // "Dark"
|
|
431
|
+
problemsTitle: string; // "Problems": title of the problems popover
|
|
432
|
+
problemSeverity(severity: ProblemSeverity): string; // "Error", "Warning": a popover row's icon, for screen readers
|
|
433
|
+
close: string; // "Close": a toast's Close button
|
|
434
|
+
dropFile: string; // "Drop file here"
|
|
435
|
+
dropForbidden(reason: DropForbiddenReason): string;
|
|
436
|
+
// "Drop one file at a time", "This modeler can't open this file type", "Close the dialog to drop a file"
|
|
437
|
+
dropHint: string; // "Or drop a file here"
|
|
438
|
+
openFileLink: string; // "Open a file…": the start screen's link to the File group's Open…
|
|
439
|
+
cannotOpenFile(name: string, accepted: string): string;
|
|
440
|
+
// "Couldn't open “map.png”. Drop a .storm, .json, or .txt file." (accepted: Intl.ListFormat disjunction)
|
|
389
441
|
}
|
|
390
442
|
// every key optional; an undefined value keeps the default
|
|
391
443
|
type ShellMessageOverrides = {
|
|
@@ -396,6 +448,8 @@ function resolveMessages(overrides?: ShellMessageOverrides): ShellMessages;
|
|
|
396
448
|
```
|
|
397
449
|
|
|
398
450
|
A localized modeler overrides the whole table (guide §2: one language per tool) and passes `lang`.
|
|
451
|
+
"Impressum" and "Datenschutz" are not in the table: they stay German with `lang="de"` in every tool
|
|
452
|
+
language (§2).
|
|
399
453
|
|
|
400
454
|
### Layout decisions
|
|
401
455
|
|
|
@@ -429,6 +483,15 @@ borders), so the decision does not flip once the palette scrolls in the second r
|
|
|
429
483
|
row 112), above one in the lower half ending 8px above it (bottom row: `bottom: 60px`), centred on
|
|
430
484
|
`anchor`, shifted to keep 12px to the sides.
|
|
431
485
|
|
|
486
|
+
### Web host decisions (0.2)
|
|
487
|
+
|
|
488
|
+
The web column's DOM-free parts are in `./core` as well, documented with their islands under
|
|
489
|
+
[Web host](#web-host-02): `decideTopRow` (label collapse before the second row), `zoomRowRaised`
|
|
490
|
+
(legal line and zoom panel), `dragVerdict` and `acceptsFile` (drop zone), `toastLifetime`,
|
|
491
|
+
`toastToEvict`, `toastStackWidth`, `toastStackBottom` and their constants (toasts),
|
|
492
|
+
`parseAppearanceChoice`, `documentMode`, `webModeScript` and `webHeadMarkup` (mode; they run in a
|
|
493
|
+
Vite config or a build script), `problemSummary` (problems popover).
|
|
494
|
+
|
|
432
495
|
## `.` — the chrome
|
|
433
496
|
|
|
434
497
|
Exports everything of `./core` plus:
|
|
@@ -443,12 +506,19 @@ interface ShellOptions {
|
|
|
443
506
|
messages?: ShellMessageOverrides;
|
|
444
507
|
platform?: Platform; // default detectPlatform(navigator)
|
|
445
508
|
keyScope?: "document" | "root"; // default "document", "root" for embedded
|
|
446
|
-
themes?: readonly Theme[]; // default ["light"]; read in the embedded
|
|
509
|
+
themes?: readonly Theme[]; // default ["light"]; read in the embedded and web hosts, see below
|
|
447
510
|
menu?: MenuGroups;
|
|
448
511
|
problems?: ProblemsOptions;
|
|
449
512
|
legend?: LegendOptions;
|
|
450
513
|
inspector?: InspectorOptions;
|
|
451
514
|
emptyHint?: EmptyHintOptions;
|
|
515
|
+
// 0.2, read in the web host only (see Web host below)
|
|
516
|
+
primary?: PrimaryActionOptions;
|
|
517
|
+
legal?: LegalLineOptions;
|
|
518
|
+
file?: FileMenuOptions;
|
|
519
|
+
appearance?: WebAppearanceOptions; // required when themes includes "dark"
|
|
520
|
+
emptyState?: EmptyStateOptions;
|
|
521
|
+
dropZone?: DropZoneOptions;
|
|
452
522
|
}
|
|
453
523
|
```
|
|
454
524
|
|
|
@@ -474,9 +544,10 @@ surface, planned for a later 1.x minor of `@miragon/modeler-api`, there is no ch
|
|
|
474
544
|
embedded modeler for another mode, so it stays in its first one), and mirrors the host's
|
|
475
545
|
`data-cd-kontrast` from the nearest mode carrier above it for as long as it lives (a
|
|
476
546
|
`MutationObserver` on the document's `data-cd-theme`/`data-cd-kontrast` attributes), because its own
|
|
477
|
-
`data-cd-theme` would otherwise drop the host's high contrast. In
|
|
478
|
-
host writes the mode on `<html>` and `themes` is not read
|
|
479
|
-
|
|
547
|
+
`data-cd-theme` would otherwise drop the host's high contrast. In the `ide` host the
|
|
548
|
+
host writes the mode on `<html>` and `themes` is not read; in the `web` host the page's head script
|
|
549
|
+
writes it before the first paint and `themes` decides whether the shell renders the Appearance
|
|
550
|
+
group (see [Mode](#mode-133)). An empty `themes` throws before anything is mounted.
|
|
480
551
|
|
|
481
552
|
**Order with document listeners.** Listeners on one target run in registration order. A modeler
|
|
482
553
|
whose own keyboard listens on `document` (Event Storming does) constructs its diagram **before**
|
|
@@ -497,6 +568,7 @@ interface MenuGroups {
|
|
|
497
568
|
export?: readonly MenuEntry[];
|
|
498
569
|
help?: readonly MenuEntry[];
|
|
499
570
|
}
|
|
571
|
+
type MenuGroupName = "file" | "model" | "export" | "appearance" | "help"; // data-menu-group
|
|
500
572
|
type MenuEntry = MenuActionEntry | MenuDialogEntry | MenuCheckboxEntry | MenuRadioGroup;
|
|
501
573
|
|
|
502
574
|
interface MenuItemBase {
|
|
@@ -515,6 +587,8 @@ interface MenuActionEntry extends MenuItemBase {
|
|
|
515
587
|
}
|
|
516
588
|
interface MenuDialogEntry extends MenuItemBase {
|
|
517
589
|
kind: "dialog";
|
|
590
|
+
shortcut?: string; // 0.2: the chip of a dialog entry ("?" for "Keyboard shortcuts…")
|
|
591
|
+
ariaKeyShortcuts?: string; // as for an action
|
|
518
592
|
onSelect(): void;
|
|
519
593
|
}
|
|
520
594
|
interface MenuCheckboxEntry extends MenuItemBase {
|
|
@@ -538,22 +612,26 @@ interface MenuRadioOption {
|
|
|
538
612
|
}
|
|
539
613
|
```
|
|
540
614
|
|
|
541
|
-
- Groups render in the fixed order Model, Export, Help with separators; empty
|
|
542
|
-
File and Appearance
|
|
543
|
-
`
|
|
615
|
+
- Groups render in the fixed order File, Model, Export, Appearance, Help with separators; empty
|
|
616
|
+
groups collapse. File and Appearance exist in the web host only and the shell builds them
|
|
617
|
+
(`ShellOptions.file`, the Appearance group of a dark-capable modeler, see
|
|
618
|
+
[File and Appearance groups](#file-and-appearance-groups-54-133)); Model, Export and Help come
|
|
619
|
+
from `MenuGroups`. With a legend (and `legend.menuEntry !== false`) the shell appends the checkbox
|
|
620
|
+
"Legend" to the Model group.
|
|
544
621
|
- `action` and `dialog`: the menu closes, focus returns to the trigger, then `onSelect` runs (a host
|
|
545
622
|
dialog then hands focus back to a visible control). `dialog` adds `aria-haspopup="dialog"`; its
|
|
546
|
-
label ends with "…" (U+2026), and in 0.
|
|
623
|
+
label ends with "…" (U+2026), and in 0.2 the modeler renders the dialog itself (see
|
|
547
624
|
[Modeler dialogs](#modeler-dialogs)).
|
|
548
625
|
- `checkbox` and `radio` toggle and keep the menu open. State is read through `checked()`, `value()`
|
|
549
626
|
and `disabled()` whenever the menu opens and on `menu.refresh()`.
|
|
550
627
|
- Icons only from the common set and only where the guide gives one; the icon column stays 16px wide
|
|
551
|
-
without an icon. An action with a `shortcut` carries
|
|
628
|
+
without an icon. An action or dialog entry with a `shortcut` shows its chip and carries
|
|
629
|
+
`aria-keyshortcuts`.
|
|
552
630
|
- On a read-only canvas (`setReadOnly(true)`, §13.5) every Model entry is `aria-disabled` unless it
|
|
553
631
|
sets `keepEnabledWhenReadOnly`; disabled entries stay focusable and do nothing. View toggles stay
|
|
554
632
|
usable on a read-only canvas: the shell's Legend checkbox carries `keepEnabledWhenReadOnly` itself,
|
|
555
633
|
and a modeler sets it on its own pure view state (Event Storming's "Type captions"), never on an
|
|
556
|
-
entry that changes the document (Level, "Map size…"). Export and Help entries are never locked.
|
|
634
|
+
entry that changes the document (Level, "Map size…"). File, Export, Appearance and Help entries are never locked.
|
|
557
635
|
Guide §13.5 (design system 1.13.0): only Model entries that change the document are `aria-disabled`.
|
|
558
636
|
- The trigger is `hidden` while no group has an entry (no menu groups and no Legend entry).
|
|
559
637
|
- Keys: Enter, Space and ↓ on the trigger open on the first entry, ↑ on the last; ↑/↓ wrap, Home/End
|
|
@@ -568,8 +646,8 @@ interface MenuRadioOption {
|
|
|
568
646
|
|
|
569
647
|
### Modeler dialogs
|
|
570
648
|
|
|
571
|
-
0.
|
|
572
|
-
modeler renders its dialogs, the "Keyboard shortcuts…" dialog of the Help group (§5.4) first among
|
|
649
|
+
0.2 has no dialog component (a later candidate, see [Out of scope for 0.2](#out-of-scope-for-02)), so
|
|
650
|
+
a modeler renders its dialogs, the "Keyboard shortcuts…" dialog of the Help group (§5.4) first among
|
|
573
651
|
them, and meets §8 on its own:
|
|
574
652
|
|
|
575
653
|
- **Layer.** `z-index: var(--cd-shell-ebene-dialog)` (50) applies only to a dialog outside the top
|
|
@@ -598,7 +676,8 @@ them, and meets §8 on its own:
|
|
|
598
676
|
|
|
599
677
|
```ts
|
|
600
678
|
interface ProblemsOptions {
|
|
601
|
-
onActivate(): void; // the IDE host opens its Problems view
|
|
679
|
+
onActivate?(): void; // the IDE host opens its Problems view (give it there); ignored in the web host
|
|
680
|
+
onSelectItem?(item: ProblemItem): void; // 0.2: a row of the problems popover, after it closed
|
|
602
681
|
}
|
|
603
682
|
interface LegendOptions {
|
|
604
683
|
render(body: HTMLElement): void; // once; a throw unwinds the whole mount
|
|
@@ -628,6 +707,8 @@ interface ShellHandle {
|
|
|
628
707
|
readonly tooltip: TooltipHandle;
|
|
629
708
|
readonly legend: LegendHandle | undefined;
|
|
630
709
|
readonly inspector: InspectorHandle | undefined;
|
|
710
|
+
readonly primary: PrimaryActionHandle | undefined; // 0.2, web host with `primary`
|
|
711
|
+
readonly appearance: AppearanceHandle | undefined; // 0.2, web host with "dark" in `themes`
|
|
631
712
|
setZoomTarget(target: ZoomTarget | null): void;
|
|
632
713
|
fitInsets(): Insets;
|
|
633
714
|
registerPalette(palette: PaletteRegistration): () => void;
|
|
@@ -636,6 +717,9 @@ interface ShellHandle {
|
|
|
636
717
|
setEmptyHint(visible: boolean): void;
|
|
637
718
|
setReadOnly(readOnly: boolean, options?: { delayMs?: number }): void;
|
|
638
719
|
isReadOnly(): boolean;
|
|
720
|
+
setEmptyState(shown: boolean): void; // 0.2, web host with `emptyState`
|
|
721
|
+
isEmptyStateShown(): boolean;
|
|
722
|
+
toast(request: ToastRequest): ToastHandle; // 0.2, web and embedded; inert in the ide host
|
|
639
723
|
destroy(): void;
|
|
640
724
|
}
|
|
641
725
|
interface MenuHandle {
|
|
@@ -680,6 +764,7 @@ interface PaletteReadOnlyState {
|
|
|
680
764
|
interface ProblemSummary {
|
|
681
765
|
count: number;
|
|
682
766
|
errors: number;
|
|
767
|
+
items?: readonly ProblemItem[]; // 0.2: the popover's rows; the ide host ignores them
|
|
683
768
|
}
|
|
684
769
|
type StatusSeverity = "danger" | "warning";
|
|
685
770
|
interface StatusMessage {
|
|
@@ -700,7 +785,9 @@ interface StatusMessage {
|
|
|
700
785
|
earlier one, and the returned unregister function is safe to call twice and after `destroy()`.
|
|
701
786
|
- `setProblems`: the pill shows for `count > 0` with `Intl` numbers in `lang` and the icon by severity;
|
|
702
787
|
its tooltip "Show in Problems view" exists only in the `ide` host (`aria-describedby`, since it
|
|
703
|
-
says more than the label).
|
|
788
|
+
says more than the label). A click calls `onActivate` in the `ide` host and in an `embedded` host
|
|
789
|
+
that gives it; in the `web` host (and an `embedded` one without `onActivate`) it opens the
|
|
790
|
+
problems popover with `summary.items` (see [Problems popover](#problems-popover-510)).
|
|
704
791
|
- `setStatus(status, { delayMs })`: shows after `delayMs` (an invalid file: `INVALID_STATUS_DELAY_MS`,
|
|
705
792
|
so typing does not flicker and `role="alert"` does not re-announce), hides at once with `null`.
|
|
706
793
|
Repeated calls while the delay runs keep the first timer (the box appears once the text stayed
|
|
@@ -823,40 +910,55 @@ Feather notices of `THIRD_PARTY_NOTICES.md` itself.
|
|
|
823
910
|
```
|
|
824
911
|
|
|
825
912
|
The DOM order is the tab order of §5.14 (menu trigger, palette, canvas, status box, hint, inspector,
|
|
826
|
-
pill, legend, zoom panel
|
|
913
|
+
pill, legend, zoom panel; the web host adds the primary action after the menu trigger, the
|
|
914
|
+
empty-state card after the hint and the legal line before the zoom panel, see
|
|
915
|
+
[Web host](#web-host-02)); nothing has a positive `tabindex`, and a renderer adds no tab stop of its
|
|
827
916
|
own around the canvas (no `tabindex` on `.djs-container`). Islands that are not shown are `hidden`.
|
|
828
917
|
|
|
829
|
-
| `data-chrome`
|
|
830
|
-
|
|
|
831
|
-
| `menu`
|
|
832
|
-
| `palette`
|
|
833
|
-
| `primary`
|
|
834
|
-
| `zoom`
|
|
835
|
-
| `problems`
|
|
836
|
-
| `legend`
|
|
837
|
-
| `inspector`
|
|
838
|
-
| `status`
|
|
839
|
-
|
|
840
|
-
|
|
|
841
|
-
|
|
|
842
|
-
| `
|
|
843
|
-
| `
|
|
844
|
-
|
|
845
|
-
|
|
|
846
|
-
|
|
|
847
|
-
| `cd-shell-
|
|
848
|
-
| `cd-shell-
|
|
849
|
-
| `cd-shell-
|
|
850
|
-
| `cd-shell-
|
|
851
|
-
| `cd-shell-
|
|
852
|
-
| `cd-shell-
|
|
853
|
-
| `cd-shell-
|
|
854
|
-
| `cd-shell-
|
|
918
|
+
| `data-chrome` | Element | Notes |
|
|
919
|
+
| ------------------ | ----------------------------- | ------------------------------------------ |
|
|
920
|
+
| `menu` | menu trigger | icon only, Lucide `menu` |
|
|
921
|
+
| `palette` | diagram-js `.djs-palette` | set by `shellModule` |
|
|
922
|
+
| `primary` | primary action (web) | only with `primary`, see Web host |
|
|
923
|
+
| `zoom` | zoom panel | `[−][NN%][+] \| [Fit]` |
|
|
924
|
+
| `problems` | problems pill | `hidden` at 0 |
|
|
925
|
+
| `legend` | legend card | only with `legend` |
|
|
926
|
+
| `inspector` | inspector `<aside>` | only with `inspector` |
|
|
927
|
+
| `status` | status box under the palette | `role="alert"` (danger) or `role="status"` |
|
|
928
|
+
| `legal` | legal line (web) | always in the web host |
|
|
929
|
+
| `toasts` | toast stack (web, embedded) | always present, no box while empty |
|
|
930
|
+
| `empty-state` | start screen card (web) | only with `emptyState` |
|
|
931
|
+
| `drop-zone` | drop overlay (web) | only with `dropZone`, during a file drag |
|
|
932
|
+
| `problems-popover` | problems list (web, embedded) | in the layer, opened by the pill |
|
|
933
|
+
|
|
934
|
+
| Class | Part |
|
|
935
|
+
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
|
|
936
|
+
| `cd-shell`, `cd-shell-document`, `cd-shell-canvas`, `cd-shell-layer` | root, own document, canvas slot, layer |
|
|
937
|
+
| `cd-shell-island`, `cd-shell-target` | glass island, 32px target in an island |
|
|
938
|
+
| `cd-shell-menu-trigger`, `cd-shell-menu`, `cd-shell-menu-item`, `cd-shell-menu-item-icon`, `cd-shell-menu-item-label`, `cd-shell-menu-item-end`, `cd-shell-menu-separator`, `cd-shell-menu-heading`, `cd-shell-menu-group` | menu |
|
|
939
|
+
| `cd-shell-menu-shortcut`, `cd-shell-menu-check` | shortcut text and check mark of an entry |
|
|
940
|
+
| `cd-shell-tooltip`, `cd-shell-tooltip-label`, `cd-shell-kbd` | tooltip and shortcut chip |
|
|
941
|
+
| `cd-shell-zoom`, `cd-shell-zoom-readout`, `cd-shell-zoom-separator` | zoom panel |
|
|
942
|
+
| `cd-shell-problems`, `cd-shell-problems-icon`, `cd-shell-problems-text` | pill |
|
|
943
|
+
| `cd-shell-status`, `cd-shell-status-icon`, `cd-shell-status-text` | status box (bar as `::before`) |
|
|
944
|
+
| `cd-shell-hint`, `cd-shell-hint-text`, `cd-shell-hint-action` | empty-file hint |
|
|
945
|
+
| `cd-shell-legend`, `cd-shell-legend-header`, `cd-shell-legend-title`, `cd-shell-legend-close`, `cd-shell-legend-body` | legend card |
|
|
946
|
+
| `cd-shell-legend-row`, `cd-shell-legend-preview`, `cd-shell-legend-separator` | rows the modeler renders into the body |
|
|
947
|
+
| `cd-shell-inspector` | inspector |
|
|
948
|
+
| `cd-shell-no-transition` | on `<html>` for one frame of a mode switch |
|
|
949
|
+
| `cd-shell-button` (`data-variant="primary"` / `"ghost"`) | buttons of the web chrome (0.2) |
|
|
950
|
+
| `cd-shell-primary`, `cd-shell-primary-label` | primary action (0.2) |
|
|
951
|
+
| `cd-shell-legal`, `cd-shell-legal-item` | legal line, a separator with its link (0.2) |
|
|
952
|
+
| `cd-shell-empty-state`, `cd-shell-empty-state-mark`, `cd-shell-empty-state-title`, `cd-shell-empty-state-text`, `cd-shell-empty-state-actions`, `cd-shell-empty-state-hint`, `cd-shell-empty-state-open-item`, `cd-shell-empty-state-open`, `cd-shell-empty-state-key` | empty-state card (0.2) |
|
|
953
|
+
| `cd-shell-drop-zone`, `cd-shell-drop-zone-chip` | drop zone (0.2) |
|
|
954
|
+
| `cd-shell-toasts`, `cd-shell-toast`, `cd-shell-toast-dot`, `cd-shell-toast-text`, `cd-shell-toast-action`, `cd-shell-toast-close` | toasts (0.2) |
|
|
955
|
+
| `cd-shell-live`, `cd-shell-live-region` (`data-live="polite"` / `"assertive"`) | the toasts' live regions (0.2) |
|
|
956
|
+
| `cd-shell-problems-popover`, `cd-shell-problems-popover-title`, `cd-shell-problems-list`, `cd-shell-problem`, `cd-shell-problem-icon`, `cd-shell-problem-message`, `cd-shell-problem-location` | problems popover (0.2) |
|
|
855
957
|
|
|
856
958
|
Internal attributes: `data-severity` on pill and status box, `data-danger` on a dangerous menu
|
|
857
959
|
entry, `data-menu-id` on entries and radio groups (the shell's Legend entry is `data-menu-id="legend"`,
|
|
858
|
-
so a modeler does not use the id `legend`), `data-menu-group` (`model`, `export`, `
|
|
859
|
-
and radio groups, which the kit's `readOnly` case reads, `data-menu-value` on radio options,
|
|
960
|
+
so a modeler does not use the id `legend`), `data-menu-group` (`file`, `model`, `export`, `appearance`,
|
|
961
|
+
`help`) on entries and radio groups, which the kit's `readOnly` case reads, `data-menu-value` on radio options,
|
|
860
962
|
`data-zoom` on the zoom buttons.
|
|
861
963
|
|
|
862
964
|
Modelers style only their own content (legend rows, inspector fields); every other class is
|
|
@@ -879,9 +981,25 @@ Relative to the shell root's padding box (a border a foreign page gives the root
|
|
|
879
981
|
| dropdown | `top: 60px; left: 12px`, at least 248px wide |
|
|
880
982
|
| tooltip | top row `top: 60px`, second row `top: 112px`, bottom row `bottom: 60px` |
|
|
881
983
|
|
|
984
|
+
The web column adds (see [Web host](#web-host-02)):
|
|
985
|
+
|
|
986
|
+
| Island | Box (web host) |
|
|
987
|
+
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
|
|
988
|
+
| `primary` | 40 high at `top: 12px; right: 12px`; 40 wide while `data-primary="collapsed"`, else icon, gap and label with padding `0 16px` |
|
|
989
|
+
| `legal` | 20 high at `left: 12px; bottom: 12px`, at most `100cqw − 24px` wide; wrapped 20 per line, the stack above rising with it |
|
|
990
|
+
| `problems` | `left: 12px; bottom: 40px` (0.1) |
|
|
991
|
+
| `legend` | `bottom: 40px`, with pill `88px` (0.1); up to 440px width at least `60px`, `88px` with `data-zoom-row="raised"` |
|
|
992
|
+
| `zoom` | `right: 12px; bottom: 12px`; `bottom: 40px` with `data-zoom-row="raised"` |
|
|
993
|
+
| `problems-popover` | `left: 12px`, its bottom 8px above the pill's top (`bottom: 88px`), `min(320px, 100cqw − 24px)` wide, at most 320 high |
|
|
994
|
+
| `toasts` | centred, `min(480px, 100cqw − 24px)` wide, `bottom: toastStackBottom(…)` (12px unless its widest card meets the bottom row) |
|
|
995
|
+
| `empty-state` | centred in the container (`centerX`, `centerY`), `min(400px, 100cqw − 24px)` wide |
|
|
996
|
+
| `drop-zone` | `inset: 12px` while a file drag is on the page |
|
|
997
|
+
|
|
882
998
|
Layers (§5.13): the canvas slot is layer 0 with `isolation: isolate` (palette `z-index: 110` above
|
|
883
|
-
context pad and label editor at 100); islands `--cd-shell-ebene-inseln` (20
|
|
884
|
-
|
|
999
|
+
context pad and label editor at 100); islands `--cd-shell-ebene-inseln` (20, the legal line and the
|
|
1000
|
+
empty-state card included), inspector 25, menu trigger and primary action 30, layer 40 (dropdown,
|
|
1001
|
+
tooltip, problems popover, drop zone), modeler dialogs 50, toasts 60 (in the web host a manual
|
|
1002
|
+
popover in the top layer, see [Toasts](#toasts-8-9)). The 50 holds only outside the top layer; a `showModal()`
|
|
885
1003
|
`<dialog>` or a popover-API element lies above everything (see [Modeler dialogs](#modeler-dialogs)).
|
|
886
1004
|
|
|
887
1005
|
Inside the canvas slot, persistent overlays a modeler adds (element toolbars, its own context pad,
|
|
@@ -890,6 +1008,578 @@ opens from them (diagram-js's popup menu at 200, a colour picker) may cover the
|
|
|
890
1008
|
open. All of them stay inside `.djs-container`, so they never cover an island. This is the shell's
|
|
891
1009
|
rule of guide §5.13 (design system 1.13.0).
|
|
892
1010
|
|
|
1011
|
+
## Web host (0.2)
|
|
1012
|
+
|
|
1013
|
+
`host: "web"` is a Miragon web app that owns its page: the web column of guide §5.2. 0.1 covered its
|
|
1014
|
+
keys (the chrome keys prevent the browser's page zoom), fit insets and the pill and legend offsets;
|
|
1015
|
+
0.2 renders the rest of the column. Everything in this section is read in the `web` host only and
|
|
1016
|
+
ignored by `ide` and `embedded` hosts, except toasts and the problems popover, which an embedded
|
|
1017
|
+
modeler shows as well (§5.2: until the contract has a channel for messages to the host).
|
|
1018
|
+
`mountVsCodeShell` does not accept these options at all; the IDE host is unchanged.
|
|
1019
|
+
|
|
1020
|
+
| Anchor (§5.2) | Web app | Option → handle | `data-chrome` |
|
|
1021
|
+
| ------------- | -------------------------------------------------------- | --------------------------------------- | ------------------------------------------------- |
|
|
1022
|
+
| top left | menu trigger: File, Model, Export, Appearance, Help | `file`, `menu`, `themes` + `appearance` | `menu` |
|
|
1023
|
+
| top centre | palette | `shellModule` | `palette` |
|
|
1024
|
+
| top right | primary action, label collapses first (§5.15) | `primary` → `shell.primary` | `primary` |
|
|
1025
|
+
| bottom left | legal line; above it the pill (with its list) and legend | `legal`, `problems`, `legend` | `legal`, `problems`, `problems-popover`, `legend` |
|
|
1026
|
+
| bottom right | zoom panel, raised above the legal line where they meet | — | `zoom` |
|
|
1027
|
+
| bottom centre | toasts | `shell.toast()` | `toasts` |
|
|
1028
|
+
| centre | empty-state card on a fresh start | `emptyState` → `shell.setEmptyState()` | `empty-state` |
|
|
1029
|
+
| whole page | drop zone for files | `dropZone` | `drop-zone` |
|
|
1030
|
+
|
|
1031
|
+
```html
|
|
1032
|
+
<html lang="en" class="cd-shell-document" data-cd-theme="light">
|
|
1033
|
+
<!-- static, see Mode -->
|
|
1034
|
+
<div
|
|
1035
|
+
class="cd-shell"
|
|
1036
|
+
data-cd-shell-host="web"
|
|
1037
|
+
data-palette="top"
|
|
1038
|
+
data-primary="collapsed"
|
|
1039
|
+
data-zoom-row="raised"
|
|
1040
|
+
data-empty-state="shown"
|
|
1041
|
+
data-drop="over|forbidden"
|
|
1042
|
+
data-problems="shown"
|
|
1043
|
+
>
|
|
1044
|
+
<!-- state attributes only while true -->
|
|
1045
|
+
<button class="cd-shell-menu-trigger" data-chrome="menu" …>…</button>
|
|
1046
|
+
<button
|
|
1047
|
+
class="cd-shell-primary cd-shell-button"
|
|
1048
|
+
data-variant="primary"
|
|
1049
|
+
data-chrome="primary"
|
|
1050
|
+
aria-label="Share"
|
|
1051
|
+
>
|
|
1052
|
+
<svg … /><span class="cd-shell-primary-label">Share</span>
|
|
1053
|
+
</button>
|
|
1054
|
+
<div class="cd-shell-canvas" role="main" [inert]>…</div>
|
|
1055
|
+
<!-- the page's main landmark; inert behind the start screen -->
|
|
1056
|
+
<div class="cd-shell-status" data-chrome="status" …>…</div>
|
|
1057
|
+
<div class="cd-shell-hint">…</div>
|
|
1058
|
+
<section
|
|
1059
|
+
class="cd-shell-empty-state"
|
|
1060
|
+
data-chrome="empty-state"
|
|
1061
|
+
aria-labelledby="cd-shell-empty-state-title-1"
|
|
1062
|
+
>
|
|
1063
|
+
…
|
|
1064
|
+
</section>
|
|
1065
|
+
<aside class="cd-shell-inspector" data-chrome="inspector" …>…</aside>
|
|
1066
|
+
<button
|
|
1067
|
+
class="cd-shell-problems"
|
|
1068
|
+
data-chrome="problems"
|
|
1069
|
+
aria-haspopup="dialog"
|
|
1070
|
+
aria-expanded="false"
|
|
1071
|
+
aria-controls="cd-shell-problems-1"
|
|
1072
|
+
…
|
|
1073
|
+
>
|
|
1074
|
+
…
|
|
1075
|
+
</button>
|
|
1076
|
+
<section class="cd-shell-legend" data-chrome="legend" …>…</section>
|
|
1077
|
+
<nav class="cd-shell-legal" data-chrome="legal" aria-label="Legal">…</nav>
|
|
1078
|
+
<div class="cd-shell-zoom" data-chrome="zoom" …>…</div>
|
|
1079
|
+
<div class="cd-shell-layer">
|
|
1080
|
+
<!-- z 40 -->
|
|
1081
|
+
<div class="cd-shell-menu" role="menu" …>…</div>
|
|
1082
|
+
<div class="cd-shell-tooltip" role="tooltip">…</div>
|
|
1083
|
+
<div
|
|
1084
|
+
class="cd-shell-problems-popover"
|
|
1085
|
+
data-chrome="problems-popover"
|
|
1086
|
+
role="dialog"
|
|
1087
|
+
id="cd-shell-problems-1"
|
|
1088
|
+
aria-labelledby="cd-shell-problems-title-1"
|
|
1089
|
+
hidden
|
|
1090
|
+
>
|
|
1091
|
+
…
|
|
1092
|
+
</div>
|
|
1093
|
+
<div class="cd-shell-drop-zone" data-chrome="drop-zone" aria-hidden="true" hidden>…</div>
|
|
1094
|
+
</div>
|
|
1095
|
+
<div class="cd-shell-live">
|
|
1096
|
+
<!-- visually hidden, always rendered: the toasts' live regions -->
|
|
1097
|
+
<div class="cd-shell-live-region" role="status" aria-live="polite" aria-atomic="true"></div>
|
|
1098
|
+
<div class="cd-shell-live-region" role="alert" aria-live="assertive" aria-atomic="true"></div>
|
|
1099
|
+
</div>
|
|
1100
|
+
<div class="cd-shell-toasts" data-chrome="toasts" popover="manual">…</div>
|
|
1101
|
+
<!-- z 60; embedded: no popover -->
|
|
1102
|
+
</div>
|
|
1103
|
+
</html>
|
|
1104
|
+
```
|
|
1105
|
+
|
|
1106
|
+
Tab order (§5.14): menu trigger, primary action, palette, canvas, status box, hint, empty-state card,
|
|
1107
|
+
inspector, pill, legend, legal line, zoom panel, then the toasts' buttons. Each 0.2 island has its
|
|
1108
|
+
`data-chrome` so the kit measures it; ids count on the page-global mount counter as in 0.1. The
|
|
1109
|
+
canvas slot is the page's `main` landmark (`role="main"`, unless it already has a role; the web host
|
|
1110
|
+
only: a foreign page has its own).
|
|
1111
|
+
|
|
1112
|
+
### Primary action (§5.7, §5.15)
|
|
1113
|
+
|
|
1114
|
+
```ts
|
|
1115
|
+
interface PrimaryActionOptions {
|
|
1116
|
+
label?: string; // default messages.share ("Share"); also the aria-label
|
|
1117
|
+
icon?: ShellIconName; // default "share-2"
|
|
1118
|
+
onSelect(): void;
|
|
1119
|
+
disabled?: () => boolean; // read on mount and on primary.refresh(); aria-disabled, stays focusable
|
|
1120
|
+
}
|
|
1121
|
+
interface PrimaryActionHandle {
|
|
1122
|
+
readonly element: HTMLButtonElement;
|
|
1123
|
+
isCollapsed(): boolean; // icon only right now
|
|
1124
|
+
refresh(): void;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// ./core
|
|
1128
|
+
interface PrimaryWidths {
|
|
1129
|
+
withLabel: number;
|
|
1130
|
+
iconOnly: number; // 40
|
|
1131
|
+
}
|
|
1132
|
+
interface TopRowInput {
|
|
1133
|
+
containerWidth: number;
|
|
1134
|
+
paletteWidth: number;
|
|
1135
|
+
menuRight: number;
|
|
1136
|
+
primary?: PrimaryWidths;
|
|
1137
|
+
}
|
|
1138
|
+
interface TopRow {
|
|
1139
|
+
paletteRow: PaletteRow;
|
|
1140
|
+
primaryLabel: "shown" | "collapsed";
|
|
1141
|
+
}
|
|
1142
|
+
function decideTopRow(input: TopRowInput): TopRow;
|
|
1143
|
+
```
|
|
1144
|
+
|
|
1145
|
+
- A blue primary button (§8, [Buttons](#buttons)) at `top: 12px; right: 12px`, 40px high, layer 30
|
|
1146
|
+
(`--cd-shell-ebene-reihe`), padding `0 16px`, Lucide icon 16px, 8px gap, label 13px/600. Hover and
|
|
1147
|
+
active `--cd-blau-link`, `shadow-2`, lift `translateY(-1px)`; disabled opaque and neutral, no lift.
|
|
1148
|
+
- **Label collapse** (§5.15: "Wird es oben eng, verliert in der Web-App zuerst die Primäraktion ihr
|
|
1149
|
+
Label, erst danach rutscht die Palette"): the layout pass (root and palette resize) runs
|
|
1150
|
+
`decideTopRow` with the measured widths. With the label the palette must keep 16px to both the
|
|
1151
|
+
menu trigger and the primary action; failing that, the icon-only width is tried; failing that, the
|
|
1152
|
+
palette moves to the second row and the label comes back if it fits next to the menu trigger.
|
|
1153
|
+
Collapsed: 40 × 40, the label not rendered, `aria-label` unchanged, the chrome tooltip shows the
|
|
1154
|
+
label (no `aria-describedby`, it repeats the name); the root carries `data-primary="collapsed"`.
|
|
1155
|
+
The labelled width is measured afresh while the label shows (a late web font counts) and, while
|
|
1156
|
+
collapsed, in one synchronous layout with the label that is never painted.
|
|
1157
|
+
- `disabled()` maps to `aria-disabled` (read on mount and on `primary.refresh()`); a click on a
|
|
1158
|
+
disabled primary action does nothing.
|
|
1159
|
+
- One primary action in 0.2; the further actions "links daneben mit 8px Lücke" (§5.7) are a later
|
|
1160
|
+
candidate. Hidden on the start screen, never locked by `setReadOnly`.
|
|
1161
|
+
|
|
1162
|
+
### Legal line (§5.10, §2)
|
|
1163
|
+
|
|
1164
|
+
```ts
|
|
1165
|
+
interface LegalLink {
|
|
1166
|
+
label: string;
|
|
1167
|
+
href: string;
|
|
1168
|
+
lang?: string;
|
|
1169
|
+
}
|
|
1170
|
+
interface LegalLineOptions {
|
|
1171
|
+
links?: readonly LegalLink[]; // after Impressum and Datenschutz, e.g. GitHub
|
|
1172
|
+
year?: number; // default the current year at mount
|
|
1173
|
+
}
|
|
1174
|
+
```
|
|
1175
|
+
|
|
1176
|
+
```html
|
|
1177
|
+
<nav class="cd-shell-legal" data-chrome="legal" aria-label="Legal">
|
|
1178
|
+
<a href="https://www.miragon.io/" … aria-describedby="cd-shell-legal-1-new-tab">© 2026 Miragon GmbH</a>
|
|
1179
|
+
<span class="cd-shell-legal-item">
|
|
1180
|
+
<span aria-hidden="true">·</span>
|
|
1181
|
+
<a href="https://www.miragon.io/impressum/" lang="de" target="_blank" rel="noopener noreferrer" …
|
|
1182
|
+
>Impressum</a
|
|
1183
|
+
>
|
|
1184
|
+
</span>
|
|
1185
|
+
<span class="cd-shell-legal-item">
|
|
1186
|
+
<span aria-hidden="true">·</span>
|
|
1187
|
+
<a href="https://www.miragon.io/datenschutz/" lang="de" …>Datenschutz</a>
|
|
1188
|
+
</span>
|
|
1189
|
+
<span class="cd-shell-legal-item">
|
|
1190
|
+
<span aria-hidden="true">·</span>
|
|
1191
|
+
<a href="https://github.com/Miragon/event-storming-modeler" …>GitHub</a>
|
|
1192
|
+
</span>
|
|
1193
|
+
<span id="cd-shell-legal-1-new-tab" hidden>Opens in a new tab</span>
|
|
1194
|
+
</nav>
|
|
1195
|
+
```
|
|
1196
|
+
|
|
1197
|
+
- Always rendered in the web host, also on the start screen ("die Zeile Rechtliches, die eine
|
|
1198
|
+
öffentliche Seite immer zeigt", §8). Copyright, URLs and the two German labels are those of the
|
|
1199
|
+
corporate-identity docs-site kit (`miragon/legal.ts`); `messages.copyright` and `messages.legal`
|
|
1200
|
+
translate, the URLs and "Impressum"/"Datenschutz" never do.
|
|
1201
|
+
- One line, 13px, line height 20px, at `left: 12px; bottom: 12px`, glass (§5.3) without border and
|
|
1202
|
+
shadow, padding `0 8px`, radius `sm`, links `--cd-ui-link` (underlined on hover and focus, always
|
|
1203
|
+
in high contrast and forced colours), separators `--cd-ui-text-leise`, layer 20. Pill and legend
|
|
1204
|
+
stand above it (0.1: pill `bottom: 40px`, legend 40/88px). Every link opens a new tab and says so
|
|
1205
|
+
to screen readers through `aria-describedby` (`messages.opensInNewTab`, "Opens in a new tab").
|
|
1206
|
+
- **Narrow containers.** The line is at most `100cqw − 24px` wide; where it does not fit (with a
|
|
1207
|
+
GitHub link it is about 391px wide, so below about 415px) it wraps between its entries, each
|
|
1208
|
+
separator staying with the link after it, and grows upwards from `bottom: 12px`. Nothing is cut
|
|
1209
|
+
off or scrolled away (Datenschutz must stay reachable; WCAG 1.4.10 at 320 CSS px). The shell
|
|
1210
|
+
measures the line's height in whole lines (`legalLineHeight`) into `--modeler-shell-legal-height`
|
|
1211
|
+
on the root, and everything stacked on it rises with it: the pill and the problems popover, the
|
|
1212
|
+
legend, the raised zoom panel and the fit's bottom (`FitLayout.legalHeight`; 112 instead of 92 on
|
|
1213
|
+
two lines).
|
|
1214
|
+
- **Bottom-row collision.** In a narrow container the legal line and the zoom panel meet:
|
|
1215
|
+
`zoomRowRaised` (`./core`) is true when the zoom panel at `right: 12px` would keep less than 8px to
|
|
1216
|
+
the legal line's measured right edge. The root then carries `data-zoom-row="raised"`, the zoom
|
|
1217
|
+
panel stands above the legal line at `bottom: 40px` like the pill (8px above a wrapped line), the
|
|
1218
|
+
fit keeps 92px at the bottom (`FitLayout.zoomRaised`), and up to 440px the legend's lowest position
|
|
1219
|
+
is 88px. §5.10 and §5.15 do not settle this case; it is the shell's rule until the guide does.
|
|
1220
|
+
|
|
1221
|
+
```ts
|
|
1222
|
+
// ./core
|
|
1223
|
+
const LEGAL_LINE_HEIGHT = 20; // one line of the legal line
|
|
1224
|
+
function legalLineHeight(measured: number | undefined): number; // whole lines, at least one: 20, 40, …
|
|
1225
|
+
interface BottomRowInput {
|
|
1226
|
+
containerWidth: number;
|
|
1227
|
+
legalRight?: number; // measured, relative to the root; absent while not shown
|
|
1228
|
+
zoomWidth?: number; // measured; absent while not shown
|
|
1229
|
+
}
|
|
1230
|
+
function zoomRowRaised(input: BottomRowInput): boolean;
|
|
1231
|
+
```
|
|
1232
|
+
|
|
1233
|
+
### File and Appearance groups (§5.4, §13.3)
|
|
1234
|
+
|
|
1235
|
+
```ts
|
|
1236
|
+
interface FileMenuAction {
|
|
1237
|
+
onSelect(): void;
|
|
1238
|
+
shortcut?: string; // chip, e.g. commandKeyLabel("O", platform, keyNames); the web app binds the key itself
|
|
1239
|
+
dialog?: boolean; // kind "dialog": it opens an in-page dialog, not the browser's file picker
|
|
1240
|
+
disabled?: () => boolean;
|
|
1241
|
+
}
|
|
1242
|
+
interface FileMenuOptions {
|
|
1243
|
+
newDiagram?: FileMenuAction;
|
|
1244
|
+
open?: FileMenuAction;
|
|
1245
|
+
showExample?: FileMenuAction;
|
|
1246
|
+
import?: FileMenuAction;
|
|
1247
|
+
}
|
|
1248
|
+
```
|
|
1249
|
+
|
|
1250
|
+
| Option | Entry (`messages`) | Icon | `data-menu-id` |
|
|
1251
|
+
| ------------- | ------------------ | ------------- | -------------- |
|
|
1252
|
+
| `newDiagram` | "New diagram" | `file-plus` | `new-diagram` |
|
|
1253
|
+
| `open` | "Open…" | `folder-open` | `open` |
|
|
1254
|
+
| `showExample` | "Show example" | none (§5.14) | `show-example` |
|
|
1255
|
+
| `import` | "Import…" | `import` | `import` |
|
|
1256
|
+
|
|
1257
|
+
- The File group comes first, the shell builds it in this order from what the modeler gives; labels
|
|
1258
|
+
and icons are the shared terms of §2 and the common set of §5.14, not the modeler's. Entries are
|
|
1259
|
+
`action`s (`dialog` with `FileMenuAction.dialog`), never locked on a read-only canvas. `open` also
|
|
1260
|
+
becomes the start screen's text link (see [Empty state](#empty-state-8)).
|
|
1261
|
+
- The chip of a Cmd/Ctrl key the app binds itself comes from `./core`, so it reads the same in every
|
|
1262
|
+
modeler and on every platform:
|
|
1263
|
+
|
|
1264
|
+
```ts
|
|
1265
|
+
function commandKeyLabel(key: string, platform: Platform, keyNames: { ctrl: string }): string;
|
|
1266
|
+
// commandKeyLabel("O", "mac", messages.keyNames) → "⌘O"; "other" → "Ctrl+O" (German "Strg+O")
|
|
1267
|
+
```
|
|
1268
|
+
|
|
1269
|
+
- The **Appearance** group is a radio group (`data-menu-id="appearance"`, heading and `aria-label`
|
|
1270
|
+
"Appearance", options `system`, `light`, `dark`: "System", "Light", "Dark") between Export and
|
|
1271
|
+
Help. The shell renders it only when `themes` includes `"dark"`; a light-only modeler has no
|
|
1272
|
+
Appearance group (§13.3 "Nur Hell, bis der Modeler Dunkel kann"). Its value is
|
|
1273
|
+
`shell.appearance.choice()`, selecting runs `shell.appearance.setChoice()` and keeps the menu open.
|
|
1274
|
+
- Neither group exists in the IDE host (native, §13.5) or the embedded host (§5.2).
|
|
1275
|
+
|
|
1276
|
+
### Mode (§13.3)
|
|
1277
|
+
|
|
1278
|
+
```ts
|
|
1279
|
+
// ./core
|
|
1280
|
+
type AppearanceChoice = "system" | "light" | "dark";
|
|
1281
|
+
const APPEARANCE_STORAGE_KEY = "miragon.darstellung";
|
|
1282
|
+
interface ThemeColors {
|
|
1283
|
+
light: string; // the app's theme-color: fest["--cd-blau"] of the vendored JSON
|
|
1284
|
+
dark: string; // dunkel["--cd-ui-grund"]
|
|
1285
|
+
}
|
|
1286
|
+
function parseAppearanceChoice(stored: unknown): AppearanceChoice; // "light" | "dark", else "system"
|
|
1287
|
+
interface DocumentMode {
|
|
1288
|
+
theme: "system" | Theme; // data-cd-theme on <html>
|
|
1289
|
+
colorScheme: "light dark" | Theme; // <meta name="color-scheme">
|
|
1290
|
+
themeColor: ThemeColors; // the two theme-color metas
|
|
1291
|
+
}
|
|
1292
|
+
function documentMode(choice: AppearanceChoice, themes: readonly Theme[], colors: ThemeColors): DocumentMode;
|
|
1293
|
+
interface WebModeScriptOptions {
|
|
1294
|
+
themes: readonly Theme[];
|
|
1295
|
+
themeColors: ThemeColors;
|
|
1296
|
+
}
|
|
1297
|
+
function webModeScript(options: WebModeScriptOptions): string; // body of the inline head <script>
|
|
1298
|
+
function webHeadMarkup(options: WebModeScriptOptions): string; // metas + <script>, for index.html
|
|
1299
|
+
|
|
1300
|
+
// .
|
|
1301
|
+
interface StorageLike {
|
|
1302
|
+
getItem(key: string): string | null;
|
|
1303
|
+
setItem(key: string, value: string): void;
|
|
1304
|
+
removeItem(key: string): void;
|
|
1305
|
+
}
|
|
1306
|
+
interface WebAppearanceOptions {
|
|
1307
|
+
themeColors: ThemeColors;
|
|
1308
|
+
storage?: StorageLike; // default globalThis.localStorage
|
|
1309
|
+
}
|
|
1310
|
+
interface AppearanceHandle {
|
|
1311
|
+
choice(): AppearanceChoice;
|
|
1312
|
+
setChoice(choice: AppearanceChoice): void;
|
|
1313
|
+
}
|
|
1314
|
+
```
|
|
1315
|
+
|
|
1316
|
+
- **Before the first paint** the page itself decides (§13.3, §13.5: one place, the host). A
|
|
1317
|
+
light-only modeler writes `<html lang="en" data-cd-theme="light">` statically, one
|
|
1318
|
+
`<meta name="theme-color">` without `media`, and of the guide's script only the contrast line;
|
|
1319
|
+
`webHeadMarkup({ themes: ["light"], themeColors })` returns exactly that. A dark-capable modeler
|
|
1320
|
+
writes `data-cd-theme="system"` statically and the guide's whole script with both `theme-color`s:
|
|
1321
|
+
`webHeadMarkup({ themes: ["light", "dark"], themeColors })`. The markup goes into `<head>` before
|
|
1322
|
+
the first stylesheet, for example through a Vite `transformIndexHtml` hook:
|
|
1323
|
+
|
|
1324
|
+
```ts
|
|
1325
|
+
// apps/webapp/vite.config.ts
|
|
1326
|
+
import { webHeadMarkup } from "@miragon/modeler-shell/core";
|
|
1327
|
+
import tokens from "./src/theme/cd-tokens.generated.json" with { type: "json" };
|
|
1328
|
+
import { THEMES } from "./src/themes";
|
|
1329
|
+
|
|
1330
|
+
const themeColors = { light: tokens.fest["--cd-blau"], dark: tokens.dunkel["--cd-ui-grund"] };
|
|
1331
|
+
const modePlugin = {
|
|
1332
|
+
name: "miragon-mode",
|
|
1333
|
+
transformIndexHtml: (html: string) =>
|
|
1334
|
+
html.replace("<!-- miragon-mode -->", webHeadMarkup({ themes: THEMES, themeColors })),
|
|
1335
|
+
};
|
|
1336
|
+
```
|
|
1337
|
+
|
|
1338
|
+
- **At runtime** `mountShell` (web host) follows `(prefers-contrast: more)` changes on `<html>`
|
|
1339
|
+
(`data-cd-kontrast="hoch"`), for every modeler. With `"dark"` in `themes` it needs
|
|
1340
|
+
`appearance.themeColors` (it throws without them, before anything mounts) and offers
|
|
1341
|
+
`shell.appearance`: `setChoice` stores the choice (`"system"` removes the entry; a blocked storage
|
|
1342
|
+
is caught and the choice applies for the page), applies `documentMode` on `<html data-cd-theme>`,
|
|
1343
|
+
`color-scheme` and both `theme-color` metas (created when missing) in one task with
|
|
1344
|
+
`cd-shell-no-transition` for one frame (§7), and a `storage` event from another tab applies the
|
|
1345
|
+
same way (a cleared storage means `system`). `choice()` is read from storage once at mount and then
|
|
1346
|
+
kept in memory, so a choice a blocked storage refused still holds for the page; mounting does not
|
|
1347
|
+
re-apply the mode, the head script owns the first paint. Nothing refits (§5.9). Light-only:
|
|
1348
|
+
`shell.appearance` is `undefined`.
|
|
1349
|
+
- `webModeScript` is the guide's script as ES5 in an immediately invoked function; it reads the
|
|
1350
|
+
storage inside `try`/`catch`, tolerates a missing `color-scheme` meta and JSON-escapes every
|
|
1351
|
+
literal (with `<`, U+2028 and U+2029), so no value ends the `<script>` element.
|
|
1352
|
+
- The kit's `appearance` case drives the web host through `PageLike.emulateMedia` and the stored
|
|
1353
|
+
choice (see [`./testing`](#testing)).
|
|
1354
|
+
|
|
1355
|
+
### Problems popover (§5.10)
|
|
1356
|
+
|
|
1357
|
+
```ts
|
|
1358
|
+
// ./core
|
|
1359
|
+
type ProblemSeverity = "error" | "warning";
|
|
1360
|
+
interface ProblemItem {
|
|
1361
|
+
severity: ProblemSeverity;
|
|
1362
|
+
message: string; // 13px --cd-ui-text
|
|
1363
|
+
location?: string; // "Line 12", "“Order Placed”": 13px --cd-ui-text-leise below the message
|
|
1364
|
+
elementId?: string; // the element a click on the row selects
|
|
1365
|
+
}
|
|
1366
|
+
function problemSummary(items: readonly ProblemItem[]): {
|
|
1367
|
+
count: number;
|
|
1368
|
+
errors: number;
|
|
1369
|
+
items: readonly ProblemItem[];
|
|
1370
|
+
};
|
|
1371
|
+
```
|
|
1372
|
+
|
|
1373
|
+
- In the web host the pill (0.1) is the trigger of a non-modal dialog: `aria-haspopup="dialog"`,
|
|
1374
|
+
`aria-expanded`, `aria-controls` on the pill; the popover `role="dialog"` without `aria-modal`,
|
|
1375
|
+
`aria-labelledby` its title "Problems" (13px/600). An embedded shell shows it too unless the
|
|
1376
|
+
modeler gives `problems.onActivate` (an embedding host with a list of its own); the IDE host never.
|
|
1377
|
+
- Above the pill, left-aligned with it: `left: 12px`, its bottom 8px above the pill's top, opaque
|
|
1378
|
+
(§5.3), 1px `--cd-ui-linie`, radius `md`, `shadow-3`, `min(320px, 100cqw − 24px)` wide, at most
|
|
1379
|
+
320px high with a scrolling list, layer 40, pop-in 150ms (§5.14). Rows at least 36px: the pill's
|
|
1380
|
+
icon 16px (`circle-x` in `--cd-ui-danger`, `triangle-alert` in `--cd-ui-warning`), the message,
|
|
1381
|
+
below it the location.
|
|
1382
|
+
- A row with an `elementId` is a button: a click closes the popover, puts the focus on the canvas and
|
|
1383
|
+
then calls `problems.onSelectItem(item)`, where the modeler selects the element. Rows without one
|
|
1384
|
+
are text that still takes the focus (`<div tabindex="-1">`), so the keyboard reads every row of a
|
|
1385
|
+
list longer than its 320px (WCAG 2.1.1); Enter and Space act on row buttons only. The icon carries
|
|
1386
|
+
the severity's name for screen readers (`role="img"`, `messages.problemSeverity`), so the shape and
|
|
1387
|
+
colour are not its only signal (§12).
|
|
1388
|
+
- Opening focuses the first row (else the popover, with an inset focus ring); ↑/↓/Home/End move
|
|
1389
|
+
between all rows and scroll the focused one into the list (only the list scrolls, never the
|
|
1390
|
+
root). Escape and Tab out close it with the focus back on the pill (Tab from the last row then
|
|
1391
|
+
moves on from the pill, Shift+Tab from the first stays on it); an outside `pointerdown` and the
|
|
1392
|
+
window's blur close it without moving the focus, as for the menu. Not modal: the zoom keys keep
|
|
1393
|
+
working. Like the menu it stops Enter, Space, Delete, Backspace and printable keys, so a modeler's
|
|
1394
|
+
keyboard on `document` never acts on them.
|
|
1395
|
+
- `setProblems` re-renders the rows while it is open and keeps the focused problem; `count` 0 hides
|
|
1396
|
+
pill and popover, and a focus in the list goes to the canvas (else the menu trigger).
|
|
1397
|
+
|
|
1398
|
+
### Toasts (§8, §9)
|
|
1399
|
+
|
|
1400
|
+
```ts
|
|
1401
|
+
// ./core
|
|
1402
|
+
type ToastStatus = "success" | "info" | "warning" | "danger";
|
|
1403
|
+
interface ToastAction {
|
|
1404
|
+
label: string;
|
|
1405
|
+
onSelect(): void; // runs after the toast closed
|
|
1406
|
+
}
|
|
1407
|
+
interface ToastRequest {
|
|
1408
|
+
status: ToastStatus;
|
|
1409
|
+
text: string;
|
|
1410
|
+
action?: ToastAction;
|
|
1411
|
+
durationMs?: number; // only to lengthen; ignored with an action
|
|
1412
|
+
}
|
|
1413
|
+
const MAX_TOASTS = 3;
|
|
1414
|
+
const TOAST_DURATION_MS: Readonly<Record<ToastStatus, number>>; // success, info 2800; warning, danger 5000
|
|
1415
|
+
const TOAST_MAX_WIDTH = 480;
|
|
1416
|
+
function toastLifetime(request: ToastRequest): number | undefined; // undefined with an action
|
|
1417
|
+
interface ToastSlot {
|
|
1418
|
+
id: number;
|
|
1419
|
+
hasAction: boolean;
|
|
1420
|
+
engaged: boolean; // hovered or focused
|
|
1421
|
+
}
|
|
1422
|
+
function toastToEvict(stack: readonly ToastSlot[]): number | undefined; // oldest first
|
|
1423
|
+
interface BottomObstacle {
|
|
1424
|
+
left: number;
|
|
1425
|
+
right: number;
|
|
1426
|
+
top: number; // px from the container's bottom to its top edge
|
|
1427
|
+
}
|
|
1428
|
+
interface ToastStackInput {
|
|
1429
|
+
containerWidth: number;
|
|
1430
|
+
obstacles: readonly BottomObstacle[];
|
|
1431
|
+
cardWidth?: number; // the widest card's measured width; absent = the whole stack width
|
|
1432
|
+
}
|
|
1433
|
+
function toastStackWidth(containerWidth: number): number; // min(480, width − 24)
|
|
1434
|
+
function toastStackBottom(input: ToastStackInput): number;
|
|
1435
|
+
|
|
1436
|
+
// .
|
|
1437
|
+
interface ToastHandle {
|
|
1438
|
+
dismiss(): void; // idempotent
|
|
1439
|
+
}
|
|
1440
|
+
```
|
|
1441
|
+
|
|
1442
|
+
- `shell.toast(request)` in the web and embedded hosts; in the IDE host it shows nothing and returns
|
|
1443
|
+
an inert handle (§13.5: the extension shows the host's notification). An import that runs by itself
|
|
1444
|
+
never shows a toast (§9); that is the modeler's discipline, the shell does not know imports.
|
|
1445
|
+
- **Card** (§8): opaque `--cd-ui-flaeche-hoch`, 1px `--cd-ui-linie`, a straight 4px bar and an 8px
|
|
1446
|
+
dot in `--cd-ui-<status>` (a dot, not an icon: the common set has no success or info icon, §5.14),
|
|
1447
|
+
text 13px/1.5 `--cd-ui-text`, radius `md`, `shadow-3`, at least 40px high, padding `4px 4px 4px
|
|
1448
|
+
16px`, 8px gap; the action as a 32px ghost button, then Close as a 32 × 32 ghost icon button
|
|
1449
|
+
(Lucide `x`, `aria-label` "Close", `aria-describedby` the toast's text, so three Close buttons
|
|
1450
|
+
are told apart). Slide and fade in from `translateY(8px)` over 250ms, out the same way; none under
|
|
1451
|
+
reduced motion. A card is as wide as its content, at most the stack's width, as in the Wardley and
|
|
1452
|
+
Team Topologies web apps (§8's `min(480px, 100% − 24px)` read as the maximum).
|
|
1453
|
+
- **Screen readers** (§8, §12): two live regions in the root, rendered from mount on and visually
|
|
1454
|
+
hidden (`.cd-shell-live`): `role="status"` with `aria-live="polite"`, and `role="alert"`. A toast's
|
|
1455
|
+
text goes into the alert region for danger, else into the status region, 100ms after the region
|
|
1456
|
+
was emptied (`TOAST_ANNOUNCE_DELAY_MS`), so a repeated toast ("Link copied" twice) is announced
|
|
1457
|
+
again; the region empties when its toast closes. A live region created together with its text is
|
|
1458
|
+
often not announced, which is why the regions never come and go with the stack. The cards carry no
|
|
1459
|
+
live role, so nothing is read twice.
|
|
1460
|
+
- **Stack**: centred, `toastStackWidth`, column with 8px gap, the newest at the bottom, at most three;
|
|
1461
|
+
a fourth evicts per `toastToEvict` (the oldest without action that is not engaged first). A
|
|
1462
|
+
request equal to a shown toast (status and text) restarts that toast's time instead of stacking,
|
|
1463
|
+
and takes the newer request's action and lifetime (a repeated "Board replaced · Undo" undoes the
|
|
1464
|
+
latest replacement). Layer 60; in the web host the stack is a `popover="manual"`, shown with the
|
|
1465
|
+
first toast and shown again with a toast that arrives while a modal dialog is open, so it stays
|
|
1466
|
+
above a `showModal()` dialog in the top layer (visible, but inert like everything outside a modal
|
|
1467
|
+
until the dialog closes); without a modal it is not hidden and re-shown, which would rebuild it for
|
|
1468
|
+
screen readers. In the embedded host it is absolute inside the root and never leaves its box.
|
|
1469
|
+
- **Time**: `toastLifetime` (about 2.8s, errors about 5s, §8; a warning, a sentence to act on, gets
|
|
1470
|
+
the error's 5s), paused while hovered or focused and resumed with at least 1.5s left; a toast with
|
|
1471
|
+
an action never closes by itself. A closing toast that held the focus hands it to the next
|
|
1472
|
+
toast's Close button, else to the canvas; on the start screen to its "New diagram" (the canvas and
|
|
1473
|
+
the menu trigger cannot take it there), never silently to `<body>`.
|
|
1474
|
+
- **Bottom row**: the stack sits at `bottom: 12px` (§8) unless the centred column of its widest card
|
|
1475
|
+
comes closer than 8px horizontally to a visible zoom panel, legal line or pill; then it stands 8px
|
|
1476
|
+
above the highest of those (`toastStackBottom` with `cardWidth`, recomputed in the layout pass,
|
|
1477
|
+
whenever a toast comes or goes, and whenever the pill, the inspector or the start screen changes;
|
|
1478
|
+
a bottom sheet hides zoom panel and pill). At 1280px with a GitHub link the legal line ends 7.5px
|
|
1479
|
+
before the 480px column, so "Link copied" (146px) stays at 12 while a full-width card rises to 40. §8 does not settle this case; it is the shell's rule until the guide does.
|
|
1480
|
+
|
|
1481
|
+
### Empty state (§8)
|
|
1482
|
+
|
|
1483
|
+
```ts
|
|
1484
|
+
interface EmptyStateOptions {
|
|
1485
|
+
title: string; // the modeler's name: "Event Storming Modeler"
|
|
1486
|
+
text: string; // one line of help: "Start a new diagram, or explore the example."
|
|
1487
|
+
komet: string; // URL of the vendored, unchanged miragon-komet-blau.svg (§2, §11)
|
|
1488
|
+
onNewDiagram(): void; // "New diagram", blue primary button; the shell hides the card first
|
|
1489
|
+
onShowExample?(): void; // "Show example", ghost button; the shell hides the card first
|
|
1490
|
+
dropHint?: string; // shown with a dropZone only; default messages.dropHint
|
|
1491
|
+
}
|
|
1492
|
+
```
|
|
1493
|
+
|
|
1494
|
+
- `shell.setEmptyState(true)` shows the card; the modeler calls it on a fresh start (no document in
|
|
1495
|
+
the URL, nothing restored). While it shows (`data-empty-state="shown"` on the root) the work chrome
|
|
1496
|
+
is hidden (`visibility: hidden`): menu trigger, primary action, palette, zoom panel, pill, legend,
|
|
1497
|
+
status box, hint and inspector; the canvas slot is `inert`, menu and popover close. The chrome keys
|
|
1498
|
+
(Cmd/Ctrl + `+`/`−`/`0`, Shift+1) go to the browser unprevented, which zooms the page: there is no
|
|
1499
|
+
diagram to zoom, and a low-vision user's Ctrl+ must not be swallowed (WCAG 1.4.4); the shell's
|
|
1500
|
+
diagram-js guard returns `false` for them meanwhile, so diagram-js zooms nothing either. Visible:
|
|
1501
|
+
the card, the legal line, toasts and the drop zone (§8: "sichtbar sind nur die Karte und die Zeile
|
|
1502
|
+
Rechtliches").
|
|
1503
|
+
- The card: centred in the container, `min(400px, 100cqw − 24px)` wide, glass (§5.3), `shadow-2`,
|
|
1504
|
+
radius `lg`, padding `32px 40px`, content centred with 16px gaps: the Komet at 64px (`alt=""`), the
|
|
1505
|
+
title as `<h1>` (21px/600), the help line (16px/1.6, `--cd-ui-text-leise`), the two 36px buttons
|
|
1506
|
+
side by side with 8px gap, primary first, then the drop hint (13px, `--cd-ui-text-leise`). Layer 20.
|
|
1507
|
+
In high contrast its border is `--cd-ui-linie` through the role like every other island (§5.3).
|
|
1508
|
+
- **Open on the start screen.** The start screen hides the menu, and a drop needs a pointer; so with
|
|
1509
|
+
`file.open` the hint ends in a text link "Open a file…" (`messages.openFileLink`, `--cd-ui-link`,
|
|
1510
|
+
`aria-keyshortcuts` and a Geist Mono chip from `file.open.shortcut`, the two never parted by a
|
|
1511
|
+
line break): "Or drop a .storm file here
|
|
1512
|
+
· Open a file… ⌘O". It is part of the hint, not a third action (§8 keeps exactly two buttons), and
|
|
1513
|
+
runs `file.open.onSelect` without leaving the start screen: the modeler ends it once the file has
|
|
1514
|
+
loaded.
|
|
1515
|
+
- Either button hides the card (`setEmptyState(false)`), moves the focus to the canvas `<svg>` and
|
|
1516
|
+
then runs its handler. `setEmptyState(true)` while the focus is in the work chrome or the canvas
|
|
1517
|
+
moves it to "New diagram"; a focus on the legal line or a toast stays. The Komet is not draggable
|
|
1518
|
+
(an image drag can carry a file and would open the drop zone). Any other start (Open…, a dropped
|
|
1519
|
+
file, a shared link, the first edit) is the modeler's to end with `setEmptyState(false)`; the card
|
|
1520
|
+
does not come back in the session.
|
|
1521
|
+
- The IDE and embedded hosts have no start screen; they use the empty-file hint (§13.5).
|
|
1522
|
+
|
|
1523
|
+
### Drop zone (§9)
|
|
1524
|
+
|
|
1525
|
+
```ts
|
|
1526
|
+
interface DropZoneOptions {
|
|
1527
|
+
accept: readonly string[]; // ".storm", "application/json", "text/*"; empty: any file
|
|
1528
|
+
multiple?: boolean; // default false
|
|
1529
|
+
onDrop(files: readonly File[]): void; // the accepted files
|
|
1530
|
+
onReject?(files: readonly File[]): void; // default: a danger toast messages.cannotOpenFile
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// ./core
|
|
1534
|
+
type DropForbiddenReason = "multiple" | "type" | "dialog";
|
|
1535
|
+
interface DragItemLike {
|
|
1536
|
+
kind: string;
|
|
1537
|
+
type: string;
|
|
1538
|
+
}
|
|
1539
|
+
interface DragContext {
|
|
1540
|
+
accept: readonly string[];
|
|
1541
|
+
multiple: boolean;
|
|
1542
|
+
dialogOpen: boolean;
|
|
1543
|
+
}
|
|
1544
|
+
type DragVerdict =
|
|
1545
|
+
{ state: "none" } | { state: "over" } | { state: "forbidden"; reason: DropForbiddenReason };
|
|
1546
|
+
function dragVerdict(items: readonly DragItemLike[], context: DragContext): DragVerdict;
|
|
1547
|
+
function acceptsFile(file: { name: string; type: string }, accept: readonly string[]): boolean;
|
|
1548
|
+
```
|
|
1549
|
+
|
|
1550
|
+
- Page-wide (`window` listeners) and for drags that carry files only, so palette drags and text
|
|
1551
|
+
dragged into the label editor stay alone; every file drag is `preventDefault`-ed, so the browser
|
|
1552
|
+
never navigates to the file. `dragVerdict` decides: a modal dialog open → forbidden (`dialog`);
|
|
1553
|
+
more than one file without `multiple` → forbidden (`multiple`); every file of a MIME type that none
|
|
1554
|
+
of `accept`'s MIME entries matches → forbidden (`type`; extensions are unreadable before the drop,
|
|
1555
|
+
so list the MIME types as well; an empty type never forbids); else over. Only items of kind
|
|
1556
|
+
`"file"` count. `accept` entries are trimmed and lower-cased (extensions match the name's suffix
|
|
1557
|
+
case-insensitively, MIME types exactly or as `type/*`), blank ones dropped; a list without a usable
|
|
1558
|
+
entry accepts every file, like `<input accept>`.
|
|
1559
|
+
- **Drag-over**: `data-drop="over"` on the root, the overlay at `inset: 12px`, fill `--cd-ui-hover`,
|
|
1560
|
+
2px dashed `--cd-ui-interaktion`, radius `lg`, the chip "Drop file here" centred on
|
|
1561
|
+
`--cd-ui-flaeche-hoch`, `dropEffect = "copy"`. **Forbidden**: `data-drop="forbidden"`, fill
|
|
1562
|
+
`--cd-ui-danger-flaeche`, dashed `--cd-ui-danger`, the chip `messages.dropForbidden(reason)`,
|
|
1563
|
+
`dropEffect = "none"` (the cursor shows not allowed). Layer 40, `aria-hidden`.
|
|
1564
|
+
- **Drop**: `acceptsFile` splits the files; `onDrop(accepted)` when there are any, `onReject(rest)`
|
|
1565
|
+
for the others (default: one danger toast per file, "Couldn't open “map.png”. Drop a .storm,
|
|
1566
|
+
.json, or .txt file.", the extensions of `accept` joined by `Intl.ListFormat` in `lang`, its MIME
|
|
1567
|
+
entries where it lists no extension). A drop forbidden for `dialog` or `multiple` opens nothing and
|
|
1568
|
+
shows no toast. After a `type` verdict a browser fires no `drop` (`dropEffect = "none"`); a
|
|
1569
|
+
synthetic one is split by `acceptsFile` like any other. Whether the drop replaces the open document is the modeler's
|
|
1570
|
+
(see the usage below for an undo toast instead of `confirm()`).
|
|
1571
|
+
|
|
1572
|
+
### Buttons
|
|
1573
|
+
|
|
1574
|
+
`src/dom/buttons.css` styles `.cd-shell-button` for the web chrome (§8): `data-variant="primary"`
|
|
1575
|
+
(`--cd-blau` with `--cd-weiss` text in both modes, `shadow-1`, hover and active `--cd-blau-link`,
|
|
1576
|
+
`shadow-2` and a 1px lift, no lift under reduced motion; disabled `--cd-ui-hover` with
|
|
1577
|
+
`--cd-ui-linie` border, muted text, no lift) and `data-variant="ghost"` (transparent, hover
|
|
1578
|
+
`--cd-ui-hover`); 36px high, padding `0 16px`,
|
|
1579
|
+
13px/600, radius `md`; focus ring 2px `--cd-ui-fokus` with 2px offset (3px in high contrast, which
|
|
1580
|
+
also drops the shadow); under forced colours borders in `ButtonText`, rings in `Highlight`. It is
|
|
1581
|
+
the only stylesheet that reads fixed palette tokens, and only these three.
|
|
1582
|
+
|
|
893
1583
|
## `./diagram-js`
|
|
894
1584
|
|
|
895
1585
|
```ts
|
|
@@ -960,6 +1650,7 @@ interface Viewport extends ZoomTarget {
|
|
|
960
1650
|
fit(options?: { animate?: boolean }): void;
|
|
961
1651
|
freeRectCenter(): Point | undefined; // diagram coordinates; undefined at 0 × 0 and after destroy
|
|
962
1652
|
afterImport(): void;
|
|
1653
|
+
newDocument(): void; // 0.2, web app: the next afterImport() counts as the first again
|
|
963
1654
|
restore(viewbox: Bounds): void;
|
|
964
1655
|
getViewbox(): Bounds | undefined;
|
|
965
1656
|
isAutomatic(): boolean;
|
|
@@ -976,7 +1667,10 @@ registers one internal didi service named `modelerShell` (a modeler does not reu
|
|
|
976
1667
|
- `paletteTooltip: ["value", null]` replaces diagram-js's (and a renderer's) palette tooltip; the
|
|
977
1668
|
chrome tooltip takes over with `title` and `shortcut` from `palette.getEntries()`.
|
|
978
1669
|
- Palette skin: on `palette.create` it sets `data-chrome="palette"` and moves the container in front
|
|
979
|
-
of the canvas `<svg>` (`canvas.getContainer()`'s child), so tab order puts the palette first.
|
|
1670
|
+
of the canvas `<svg>` (`canvas.getContainer()`'s child), so tab order puts the palette first. A
|
|
1671
|
+
renderer's export (`saveSVG`) must therefore take the container's own `<svg>` child, not the
|
|
1672
|
+
first `<svg>` it finds (`container.querySelector("svg")` now gets a palette icon), and strip the
|
|
1673
|
+
`tabindex`, `role="application"` and `aria-label` the live canvas carries from its copy. After
|
|
980
1674
|
every `palette.changed` it re-applies `role="toolbar"` and `aria-label` (`messages.tools`) on
|
|
981
1675
|
`.djs-palette-entries`, `role="button"` on every entry, `aria-keyshortcuts` from the entry's
|
|
982
1676
|
`ariaKeyShortcuts` or `ariaKeyShortcutsFromLabel(shortcut)` (§5.6; split the shortcut off the
|
|
@@ -1002,7 +1696,8 @@ registers one internal didi service named `modelerShell` (a modeler does not reu
|
|
|
1002
1696
|
`KeyboardBindings` and the modeler's keyboard modules (priority 1000) never act on them; the
|
|
1003
1697
|
shell's `keydown` listener executes them exactly once. It returns `false` (stops them without
|
|
1004
1698
|
`preventDefault`) for a chrome key from a text field inside the `<svg>`, which stays the field's,
|
|
1005
|
-
|
|
1699
|
+
for a chrome key while the web app's start screen shows (the browser zooms the page), and, while
|
|
1700
|
+
the attached shell's host is `ide`, for undo and redo (`isUndoRedoKey`): the canvas
|
|
1006
1701
|
`commandStack` never takes them, VS Code undoes the document (§13.5). A modeler's own DOM undo
|
|
1007
1702
|
binding must go in the IDE host as well; the kit's `keys` case checks it.
|
|
1008
1703
|
- Keys the canvas uses beyond the chrome keys are the modeler's (§13.5). Cmd/Ctrl+A on the focused
|
|
@@ -1054,6 +1749,10 @@ insets are the only margin (§5.9), and a padding would add to them.
|
|
|
1054
1749
|
1 around the centre of the free rectangle (at 0 × 0 once the container has a size). That write is
|
|
1055
1750
|
the viewport's own, not a user move, so the first content still fits; later re-imports and
|
|
1056
1751
|
`restore`s of the empty file keep the view (guide §5.9 and §13.5, design system 1.13.0).
|
|
1752
|
+
- `newDocument()` (0.2): in the web app every new document (New diagram, Open…, Import…, Show
|
|
1753
|
+
example, a shared link, a dropped file) opens like the first (§5.9). Called before the import, it
|
|
1754
|
+
forgets the user's moves, a stored view and a pending restore and stops a fit animation, so the
|
|
1755
|
+
next `afterImport()` jump-fits non-empty content or shows an empty document at 100 %.
|
|
1057
1756
|
- `freeRectCenter()` is the centre of the free rectangle (the container minus `insets()`, as fit
|
|
1058
1757
|
uses them) in diagram coordinates, for the current viewbox; `undefined` while the container is
|
|
1059
1758
|
0 × 0 and after `destroy`. It is where keyboard creation places an element.
|
|
@@ -1108,7 +1807,7 @@ entries["create.domain-event"] = {
|
|
|
1108
1807
|
```
|
|
1109
1808
|
|
|
1110
1809
|
When the centre is already taken, the provider steps 24px diagonally until it is free (§5.5); a
|
|
1111
|
-
shared helper for that is a
|
|
1810
|
+
shared helper for that is a later candidate. The kit's `create` case presses Enter on the first
|
|
1112
1811
|
creating entry and expects exactly one new, selected shape inside the free rectangle.
|
|
1113
1812
|
|
|
1114
1813
|
## `./vscode/extension`
|
|
@@ -1358,20 +2057,46 @@ interface PageLike {
|
|
|
1358
2057
|
up(): Promise<void>;
|
|
1359
2058
|
};
|
|
1360
2059
|
waitForTimeout(timeout: number): Promise<void>;
|
|
2060
|
+
// 0.2: Playwright's page.emulateMedia; the web host's appearance needs it (else a skip, hook "open")
|
|
2061
|
+
emulateMedia?(options: {
|
|
2062
|
+
colorScheme?: "light" | "dark" | "no-preference" | null;
|
|
2063
|
+
contrast?: "more" | "no-preference" | null;
|
|
2064
|
+
reducedMotion?: "reduce" | "no-preference" | null;
|
|
2065
|
+
forcedColors?: "active" | "none" | null;
|
|
2066
|
+
}): Promise<void>;
|
|
1361
2067
|
}
|
|
1362
2068
|
|
|
1363
2069
|
interface OpenContext {
|
|
1364
2070
|
size: Size; // the shell container's size (grow the page by any frame around an embedded root)
|
|
1365
|
-
requested: Appearance; // what the host requests; the harness opens the page with it (embedded: see below)
|
|
2071
|
+
requested: Appearance; // what the host requests; the harness opens the page with it (embedded, web: see below)
|
|
1366
2072
|
}
|
|
1367
2073
|
interface ShowHooks<P extends PageLike = PageLike> {
|
|
1368
|
-
problems?(page: P): Promise<void>;
|
|
2074
|
+
problems?(page: P): Promise<void>; // web: with items, so the popover has rows
|
|
1369
2075
|
status?(page: P): Promise<void>;
|
|
1370
2076
|
legend?(page: P): Promise<void>;
|
|
1371
2077
|
inspector?(page: P): Promise<void>;
|
|
1372
2078
|
invalid?(page: P): Promise<void>; // the invalid-file state, caused the way the host causes it
|
|
2079
|
+
emptyState?(page: P): Promise<void>; // 0.2, web: the start screen of a fresh visit
|
|
2080
|
+
toast?(page: P, toast: ToastFixture): Promise<void>; // 0.2, web and embedded: through shell.toast
|
|
2081
|
+
}
|
|
2082
|
+
type ShowHook = keyof ShowHooks; // … | "emptyState" | "toast"
|
|
2083
|
+
// "problemSelection": the modeler's problems never point at an element (only at lines), so no row selects
|
|
2084
|
+
type AbsentFeature = ShowHook | "drop" | "problemSelection";
|
|
2085
|
+
interface ToastFixture {
|
|
2086
|
+
status: ToastStatus;
|
|
2087
|
+
text: string;
|
|
2088
|
+
actionLabel?: string; // the hook passes { label: actionLabel, onSelect() {} } as action
|
|
2089
|
+
}
|
|
2090
|
+
interface DropFixture {
|
|
2091
|
+
name: string;
|
|
2092
|
+
type: string;
|
|
2093
|
+
text: string; // the kit builds new File([text], name, { type }) in the page
|
|
2094
|
+
}
|
|
2095
|
+
interface DropFixtures {
|
|
2096
|
+
accepted: DropFixture; // dropped on the start screen, it opens (the card goes)
|
|
2097
|
+
rejected?: DropFixture; // opens nothing, shows a danger toast (or the modeler's onReject)
|
|
2098
|
+
multiple?: boolean; // the modeler's DropZoneOptions.multiple; default false: two files are forbidden
|
|
1373
2099
|
}
|
|
1374
|
-
type ShowHook = keyof ShowHooks; // "problems" | "status" | "legend" | "inspector" | "invalid"
|
|
1375
2100
|
interface ShellConformanceHarness<P extends PageLike = PageLike> {
|
|
1376
2101
|
host: ShellHost;
|
|
1377
2102
|
themes: readonly Theme[];
|
|
@@ -1379,11 +2104,12 @@ interface ShellConformanceHarness<P extends PageLike = PageLike> {
|
|
|
1379
2104
|
destroy?(page: P): Promise<void>;
|
|
1380
2105
|
root?: string; // default ".cd-shell"
|
|
1381
2106
|
show?: ShowHooks<P>;
|
|
1382
|
-
|
|
2107
|
+
drop?: DropFixtures; // 0.2, web: the files the dropZone case drops
|
|
2108
|
+
absent?: readonly AbsentFeature[]; // islands, states and the drop zone the modeler does not have
|
|
1383
2109
|
sizes?: readonly Size[]; // default 1280 × 900, 720 × 700, 420 × 700
|
|
1384
2110
|
}
|
|
1385
2111
|
interface ShellSkip {
|
|
1386
|
-
hook: ShowHook | "destroy" | "open"; // the harness field that would feed the skipped part
|
|
2112
|
+
hook: ShowHook | "drop" | "destroy" | "open"; // the harness field that would feed the skipped part
|
|
1387
2113
|
reason: string;
|
|
1388
2114
|
}
|
|
1389
2115
|
interface ShellCaseOutcome {
|
|
@@ -1423,7 +2149,15 @@ interface ChromeMeasurement {
|
|
|
1423
2149
|
container: Size;
|
|
1424
2150
|
paletteRow: PaletteRow;
|
|
1425
2151
|
islands: Partial<Record<ChromeIsland, IslandBox>>;
|
|
1426
|
-
open: {
|
|
2152
|
+
open: {
|
|
2153
|
+
legend: boolean;
|
|
2154
|
+
inspector: boolean;
|
|
2155
|
+
problems: boolean;
|
|
2156
|
+
emptyState: boolean; // 0.2: data-empty-state="shown"
|
|
2157
|
+
primaryCollapsed: boolean; // 0.2: data-primary="collapsed"
|
|
2158
|
+
zoomRaised: boolean; // 0.2: data-zoom-row="raised"
|
|
2159
|
+
};
|
|
2160
|
+
toastCardWidth?: number; // 0.2: the widest visible toast card; absent without a toast
|
|
1427
2161
|
}
|
|
1428
2162
|
interface ExpectedBox {
|
|
1429
2163
|
left?: number;
|
|
@@ -1433,6 +2167,7 @@ interface ExpectedBox {
|
|
|
1433
2167
|
width?: number;
|
|
1434
2168
|
height?: number;
|
|
1435
2169
|
centerX?: number;
|
|
2170
|
+
centerY?: number; // 0.2
|
|
1436
2171
|
}
|
|
1437
2172
|
function measureChrome(page: PageLike, root?: string): Promise<ChromeMeasurement>;
|
|
1438
2173
|
function expectedChromeBoxes(
|
|
@@ -1456,6 +2191,21 @@ Cases (each named `shell conformance: <name>`; failures throw plain `Error`s lis
|
|
|
1456
2191
|
| `appearance` | the harness opens with dark, dark + high contrast and light + high contrast requested. Own document: the mode on `<html>` is `resolveAppearance(requested, themes)`, recomputed by the kit (a light-only modeler stays light, `hoch` passes). Embedded: the kit puts the foreign page's `<html>` in the requested mode, and the root carries its own `data-cd-theme="<themes[0]>"` with the page's `data-cd-kontrast` (§13.3). Both: no themed descendant; `cd-shell-document` on `<html>` in `ide`/`web`, never in a foreign page; tokens loaded; `color-scheme` and four `--cd-ui-*` roles equal on root, canvas, every island, the open dropdown and the visible tooltip; the painted surround, the first non-transparent `background-color` from `.djs-container` out through `.cd-shell-canvas` to the root, equals `--cd-ui-grund` (§10.1); every palette tool entry (group `tools` or `data-action` ending in `-tool`) and its icon (`<svg>` or `::before`) compute to `--cd-ui-text` at rest and pressed and to `--cd-ui-interaktion` hovered, each tool hovered in turn and Lasso armed by a click, which sets `aria-pressed="true"`; notation previews are exempt (§5.5). Never half-dark |
|
|
1457
2192
|
| `cleanup` | with a tooltip timer pending: after `destroy` and 800ms, a resize and chrome keys that must arrive unprevented, no `cd-shell*` class or element, no `data-chrome`, no root state attribute (embedded: also no `data-cd-theme`/`data-cd-kontrast`) and no page error is left; skips with `hook: "destroy"` without a `destroy` |
|
|
1458
2193
|
|
|
2194
|
+
**0.2, the web column.** The 0.1 cases learn the web host's islands, and five cases join them after
|
|
2195
|
+
`cleanup` (`createWebCases` in `src/testing/web-cases.ts`). Each runs in every host; outside the
|
|
2196
|
+
hosts it applies to it asserts that nothing of it shows and passes.
|
|
2197
|
+
|
|
2198
|
+
| Case / extension | Checks |
|
|
2199
|
+
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2200
|
+
| `geometry` (extended) | web: `primary` at 12/12, 40 high, 40 wide while collapsed; the palette keeping 16px to the primary action in the top row; `legal` at 12/12, 20 high per line and at most `100cqw − 24px` wide; pill, legend and zoom panel per `open.zoomRaised` on the legal line's measured lines; the popover above the pill (after a click on it); toasts at `toastStackBottom` recomputed from the measured bottom row and the widest card (`toastCardWidth`); `empty-state` centred after `show.emptyState`; IDE and embedded: `primary`, `legal`, `empty-state`, `drop-zone` never visible |
|
|
2201
|
+
| `aria` (extended) | web: tab order menu trigger, primary action, palette, canvas; the primary action's `aria-label` in both states; the legal `<nav aria-label>`, Impressum and Datenschutz with `lang="de"`, `rel="noopener noreferrer"`; the File group first with its ids and icons, the Appearance group (dark-capable only) between Export and Help with `aria-checked`; the pill's `aria-haspopup="dialog"`, `aria-expanded`, `aria-controls` |
|
|
2202
|
+
| `appearance` (extended) | web, with `emulateMedia`: light-only: `<html data-cd-theme="light">` under a dark system, no Appearance group, `data-cd-kontrast="hoch"` under `prefers-contrast: more`; dark-capable: `system` follows the system, choosing Dark writes `miragon.darstellung=dark`, `data-cd-theme="dark"`, `color-scheme` and both `theme-color` metas in one frame, System removes the entry; never half-dark |
|
|
2203
|
+
| `webChrome` | primary action: label collapse before the palette moves (widths stepped from wide to narrow), tooltip only while collapsed, `onSelect` on click and on Enter (so a real app runs its own handler, a Share without clipboard permission shows its danger toast meanwhile); legal line content and links, at 320px every link whole inside the wrapped line (WCAG 1.4.10); the raised zoom row where legal line and zoom panel meet; File entries run their handlers and close the menu, never locked under `show.invalid` |
|
|
2204
|
+
| `emptyState` | after `show.emptyState`: the card centred, `min(400px, 100cqw − 24px)`, Komet 64px with `alt=""`, `<h1>`, primary "New diagram" before ghost "Show example", both 36px (a text link in the hint is no action); the work chrome `visibility: hidden`, the canvas `inert`, Tab reaching only the card and the legal links, chrome keys reaching the browser unprevented without zooming the diagram; with `show.toast`, closing a focused toast puts the focus on the card; "New diagram" hides it, shows the chrome and focuses the canvas `<svg>` |
|
|
2205
|
+
| `toasts` | through `show.toast`: a `role="status"` and a `role="alert"` live region in the root before the first toast, the toast's text arriving in the one of its status, no live role on the card; Close named "Close", at most three (the fourth evicts the oldest without action), the newest at the bottom, a repeated text not stacked, closing after `toastLifetime` (with a margin) unless hovered or focused, a toast with an action never; web: shown again in the top layer after a `showModal()` stand-in opened (everything outside a modal is inert, so the kit checks the stacking, not a click); IDE: no hook and no toast element ever |
|
|
2206
|
+
| `problemsList` | web (embedded when the pill opens it): after `show.problems`, a click on the pill opens `role="dialog"` titled "Problems" with the rows, Escape returns the focus to the pill, an outside click closes without moving it; Enter on the pill and then ↓ to the last row, Home and End reach every row, text rows included, each scrolled into the list (WCAG 2.1.1); a row with an element closes it and focuses the canvas (skipped with `hook: "problems"` when no item has an `elementId`, unless `absent` lists `"problemSelection"`); geometry left 12, 8px above the pill, at most 320 wide and high |
|
|
2207
|
+
| `dropZone` | web with `harness.drop`: synthetic file drags (a `DataTransfer` built in the page) show `data-drop="over"` with the §9 colours and the 12px inset; two files (unless `drop.multiple`) and a modal stand-in show `forbidden`; a drag without files changes nothing; dropping `accepted` on the start screen opens it, `rejected` opens nothing and shows a danger toast; skips with `hook: "drop"` without the fixtures unless `absent` lists `"drop"` |
|
|
2208
|
+
|
|
1459
2209
|
- **Skips.** A case the harness cannot feed reports what it skipped as `ShellSkip`s, one per missing
|
|
1460
2210
|
harness field, through `onSkip` (default `console.warn`). A hook that is neither in `show` nor in
|
|
1461
2211
|
`absent` is skipped with `hook` set to its name, a missing `destroy` with `hook: "destroy"`, and a
|
|
@@ -1472,6 +2222,10 @@ Cases (each named `shell conformance: <name>`; failures throw plain `Error`s lis
|
|
|
1472
2222
|
`<html>` itself after `open`: every consumer suite then proves that the modeler stays in its own
|
|
1473
2223
|
mode inside a dark host page, the case a foreign page meets in practice (designIQ, a web app on
|
|
1474
2224
|
`system` under a dark OS).
|
|
2225
|
+
- For the `web` host (0.2) the kit calls `page.emulateMedia({ colorScheme, contrast })` from
|
|
2226
|
+
`requested` before `open`, and `open` loads the page without a stored `miragon.darstellung`; the
|
|
2227
|
+
`appearance` case then sets the choice through the Appearance group itself. Without
|
|
2228
|
+
`emulateMedia` the web part of `appearance` skips with `hook: "open"`.
|
|
1475
2229
|
- The second-row tooltip of the `geometry` case may stand centred on its entry or in the container:
|
|
1476
2230
|
guide §5.15 says "mittig im Container", and which of the two it means is an open question; the
|
|
1477
2231
|
shell centres it on its entry.
|
|
@@ -1597,6 +2351,13 @@ state (rest and pressed `--cd-ui-text`, hovered `--cd-ui-interaktion`, read-only
|
|
|
1597
2351
|
diagram-js's rule on the icon or on the stale `highlighted-entry` does not leak in; notation
|
|
1598
2352
|
previews keep their own colours (§5.5). Private helper properties are named `--modeler-shell-*`.
|
|
1599
2353
|
|
|
2354
|
+
It is assembled from `dom/shell-dom.css` (0.1 chrome), `dom/buttons.css` (0.2: the buttons of the
|
|
2355
|
+
web chrome, the one place with fixed palette tokens, see [Buttons](#buttons)), `dom/web/web.css`
|
|
2356
|
+
(0.2: primary action, legal line, empty state, drop zone, raised zoom row) and
|
|
2357
|
+
`dom/feedback/feedback.css` (0.2: toasts and the problems popover; they render in a foreign page too,
|
|
2358
|
+
so they reset the page's element rules like the 0.1 islands), then `diagram-js/palette.css`. A test
|
|
2359
|
+
pins that `shell.css` imports every stylesheet of `src`.
|
|
2360
|
+
|
|
1600
2361
|
The shell bundles no font: the modeler loads Geist and Geist Mono (`@fontsource-variable/geist`,
|
|
1601
2362
|
`@fontsource-variable/geist-mono`), as the stylesheet order above shows, as font files (see
|
|
1602
2363
|
`./vscode/extension`).
|
|
@@ -1857,9 +2618,213 @@ What the Event Storming renderer changes for the shell (the kit's `aria`, `keys`
|
|
|
1857
2618
|
|
|
1858
2619
|
Document sync, echo suppression, minimal edits and undo stay in the modeler for 0.1 (see below).
|
|
1859
2620
|
|
|
1860
|
-
##
|
|
2621
|
+
## Usage: Event Storming in the web app
|
|
2622
|
+
|
|
2623
|
+
The Event Storming web app (`apps/webapp`), the pilot of 0.2. It is light-only until the shared
|
|
2624
|
+
dark-mode release, so its `index.html` is static and its head carries the contrast line only:
|
|
2625
|
+
|
|
2626
|
+
```html
|
|
2627
|
+
<!doctype html>
|
|
2628
|
+
<html lang="en" data-cd-theme="light">
|
|
2629
|
+
<head>
|
|
2630
|
+
<meta charset="UTF-8" />
|
|
2631
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
2632
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
2633
|
+
<!-- miragon-mode: webHeadMarkup({ themes: ["light"], themeColors }) through transformIndexHtml -->
|
|
2634
|
+
<title>Event Storming Modeler</title>
|
|
2635
|
+
</head>
|
|
2636
|
+
<body>
|
|
2637
|
+
<div id="app"></div>
|
|
2638
|
+
<script type="module" src="/src/main.ts"></script>
|
|
2639
|
+
</body>
|
|
2640
|
+
</html>
|
|
2641
|
+
```
|
|
2642
|
+
|
|
2643
|
+
`src/main.ts`, shortened to what touches the shell (the real file is
|
|
2644
|
+
`apps/webapp/src/main.ts` in Miragon/event-storming-modeler):
|
|
2645
|
+
|
|
2646
|
+
```ts
|
|
2647
|
+
import "@fontsource-variable/geist/wght.css";
|
|
2648
|
+
import "@fontsource-variable/geist-mono/wght.css";
|
|
2649
|
+
import "@miragon/event-storming-renderer/assets/event-storming.css"; // the tokens, diagram-js.css
|
|
2650
|
+
import "@miragon/modeler-shell/shell.css"; // last
|
|
2651
|
+
import {
|
|
2652
|
+
Modeler,
|
|
2653
|
+
boardMenu,
|
|
2654
|
+
createShortcutsDialog,
|
|
2655
|
+
importWarningText,
|
|
2656
|
+
type ImportWarning,
|
|
2657
|
+
} from "@miragon/event-storming-renderer";
|
|
2658
|
+
import {
|
|
2659
|
+
commandKeyLabel,
|
|
2660
|
+
defaultMessages,
|
|
2661
|
+
detectPlatform,
|
|
2662
|
+
mountShell,
|
|
2663
|
+
problemSummary,
|
|
2664
|
+
} from "@miragon/modeler-shell";
|
|
2665
|
+
import { connectDiagramJs, shellModule } from "@miragon/modeler-shell/diagram-js";
|
|
2666
|
+
import { copyTextWhenReady } from "./clipboard"; // ClipboardItem with a promise: Safari keeps the gesture
|
|
2667
|
+
import { hashHoldsBoard, readHashMap, shareUrl, writeHashMap } from "./share";
|
|
2668
|
+
import { THEMES } from "./themes"; // ["light"]
|
|
2669
|
+
|
|
2670
|
+
// 1. The diagram first (its keyboard listens on document); the web app keeps the canvas undo keys.
|
|
2671
|
+
const modeler = new Modeler({
|
|
2672
|
+
width: "100%",
|
|
2673
|
+
height: "100%",
|
|
2674
|
+
additionalModules: [shellModule],
|
|
2675
|
+
freeRectInsets: () => shell.fitInsets(), // keyboard creation lands in the free rectangle
|
|
2676
|
+
});
|
|
2677
|
+
modeler.get("canvas");
|
|
2678
|
+
const platform = detectPlatform(navigator);
|
|
2679
|
+
const openKey = commandKeyLabel("O", platform, defaultMessages.keyNames); // "⌘O" / "Ctrl+O"
|
|
2680
|
+
const saveKey = commandKeyLabel("S", platform, defaultMessages.keyNames);
|
|
2681
|
+
|
|
2682
|
+
// 2. The chrome, the whole web column.
|
|
2683
|
+
const shell = mountShell(document.getElementById("app")!, {
|
|
2684
|
+
host: "web",
|
|
2685
|
+
canvas: modeler.container,
|
|
2686
|
+
lang: "en",
|
|
2687
|
+
platform,
|
|
2688
|
+
themes: THEMES,
|
|
2689
|
+
messages: { canvas: "Event Storming board" },
|
|
2690
|
+
primary: { onSelect: () => share() }, // "Share", share-2
|
|
2691
|
+
legal: { links: [{ label: "GitHub", href: "https://github.com/Miragon/event-storming-modeler" }] },
|
|
2692
|
+
file: {
|
|
2693
|
+
newDiagram: { onSelect: () => void newDiagram() },
|
|
2694
|
+
open: { onSelect: () => pickFile(), shortcut: openKey }, // also the start screen's "Open a file…"
|
|
2695
|
+
showExample: { onSelect: () => void showExample() },
|
|
2696
|
+
},
|
|
2697
|
+
// Model (level, type captions), Export and Help: the same entries the VS Code webview shows.
|
|
2698
|
+
menu: boardMenu({
|
|
2699
|
+
levels,
|
|
2700
|
+
typeCaptions,
|
|
2701
|
+
exports: {
|
|
2702
|
+
png: { onSelect: () => void exportPicture("png") },
|
|
2703
|
+
svg: { onSelect: () => void exportPicture("svg") },
|
|
2704
|
+
transparentPng: { onSelect: () => void exportPicture("png-transparent") },
|
|
2705
|
+
json: { onSelect: () => exportBoardText("json") },
|
|
2706
|
+
storm: { onSelect: () => exportBoardText("storm"), shortcut: saveKey },
|
|
2707
|
+
},
|
|
2708
|
+
showKeyboardShortcuts: () => shortcutsDialog.open(),
|
|
2709
|
+
}),
|
|
2710
|
+
problems: { onSelectItem: (item) => selectElement(item.elementId) },
|
|
2711
|
+
emptyState: {
|
|
2712
|
+
title: "Event Storming Modeler",
|
|
2713
|
+
text: "Start a new diagram, or explore the example.",
|
|
2714
|
+
komet: "/miragon-komet-blau.svg", // vendored unchanged from corporate-identity
|
|
2715
|
+
onNewDiagram: () => void newDiagram(),
|
|
2716
|
+
onShowExample: () => void showExample(),
|
|
2717
|
+
dropHint: "Or drop a .storm, .json, or .txt file here", // the types the reject toast names
|
|
2718
|
+
},
|
|
2719
|
+
dropZone: {
|
|
2720
|
+
accept: [".storm", ".json", ".txt", "application/json", "text/plain"],
|
|
2721
|
+
onDrop: ([file]) => file && void openBoardFile(file),
|
|
2722
|
+
},
|
|
2723
|
+
});
|
|
2724
|
+
const shortcutsDialog = createShortcutsDialog(shell, { fileKeys: true });
|
|
2725
|
+
|
|
2726
|
+
// 3. Viewport, palette, keys.
|
|
2727
|
+
const { viewport } = connectDiagramJs(shell, modeler, {
|
|
2728
|
+
isEmpty,
|
|
2729
|
+
contentBounds: () => (isEmpty() ? undefined : boardBounds.contentBounds(0)),
|
|
2730
|
+
});
|
|
2731
|
+
|
|
2732
|
+
// 4. Every new document opens like the first (§5.9); the start screen goes with it.
|
|
2733
|
+
async function openDocument(load: () => Promise<readonly ImportWarning[]>, announce: boolean) {
|
|
2734
|
+
viewport.newDocument();
|
|
2735
|
+
let warnings: readonly ImportWarning[];
|
|
2736
|
+
try {
|
|
2737
|
+
warnings = await load();
|
|
2738
|
+
} finally {
|
|
2739
|
+
viewport.afterImport();
|
|
2740
|
+
}
|
|
2741
|
+
shell.setEmptyState(false);
|
|
2742
|
+
shell.menu.refresh();
|
|
2743
|
+
updateDocumentTitle(); // "Order Checkout · Event Storming Modeler" (§5.4)
|
|
2744
|
+
// The parser names lines: "Line 4 · A -> Nowhere" below the message, no element to select.
|
|
2745
|
+
shell.setProblems(problemSummary(problemItems(warnings)));
|
|
2746
|
+
for (const warning of warnings) console.warn(importWarningText(warning));
|
|
2747
|
+
// A toast only after an import the user started, with the count (§9), never on a restore.
|
|
2748
|
+
if (announce && warnings.length > 0) {
|
|
2749
|
+
const pill = shell.messages.problems(warnings.length, String(warnings.length));
|
|
2750
|
+
shell.toast({
|
|
2751
|
+
status: "warning",
|
|
2752
|
+
text: `Imported with ${warnings.length} warnings. Details under “${pill}” bottom left.`,
|
|
2753
|
+
});
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
// Replacing a board clears the canvas history, so instead of confirm() it offers the board back
|
|
2758
|
+
// (§9 asks for a confirmation; the undo toast is the open guide question): "Board replaced · Undo",
|
|
2759
|
+
// also on Cmd/Ctrl+Z while the canvas has nothing else to undo. An edit withdraws the offer.
|
|
2760
|
+
async function replaceBoard(load: () => Promise<readonly ImportWarning[]>) {
|
|
2761
|
+
const previous = boardIsEmpty() ? undefined : modeler.exportDSL();
|
|
2762
|
+
await openDocument(load, true);
|
|
2763
|
+
withdrawReplaceUndo();
|
|
2764
|
+
if (previous === undefined || previous === modeler.exportDSL()) return;
|
|
2765
|
+
const undo = () => void openDocument(async () => (await modeler.importDSL(previous)).warnings, false);
|
|
2766
|
+
replaceUndo = {
|
|
2767
|
+
toast: shell.toast({ status: "info", text: "Board replaced", action: { label: "Undo", onSelect: undo } }),
|
|
2768
|
+
undo,
|
|
2769
|
+
};
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
// Copy inside the click: Safari drops the user activation at the first await before writeText.
|
|
2773
|
+
function share(): void {
|
|
2774
|
+
const dsl = modeler.exportDSL();
|
|
2775
|
+
const copied = copyTextWhenReady(shareUrl(dsl));
|
|
2776
|
+
void (async () => {
|
|
2777
|
+
try {
|
|
2778
|
+
await writeHashMap(dsl);
|
|
2779
|
+
await copied;
|
|
2780
|
+
shell.toast({ status: "success", text: "Link copied" });
|
|
2781
|
+
} catch {
|
|
2782
|
+
shell.toast({
|
|
2783
|
+
status: "danger",
|
|
2784
|
+
text: "Couldn't copy the link. Copy it from the address bar instead.",
|
|
2785
|
+
});
|
|
2786
|
+
}
|
|
2787
|
+
})();
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
// The app's own keys: Cmd/Ctrl+O opens a file, Cmd/Ctrl+S exports the .storm file (never the
|
|
2791
|
+
// browser's dialogs, also not over a modal dialog; on the start screen there is nothing to save).
|
|
2792
|
+
window.addEventListener("keydown", onAppKey);
|
|
2793
|
+
|
|
2794
|
+
modeler.on("commandStack.changed", (event: { trigger?: string }) => {
|
|
2795
|
+
if (event.trigger !== "clear") {
|
|
2796
|
+
shell.setEmptyState(false); // the first edit is a start as well
|
|
2797
|
+
withdrawReplaceUndo(); // an edit makes the undo offer stale
|
|
2798
|
+
}
|
|
2799
|
+
shell.menu.refresh(); // the level is undoable
|
|
2800
|
+
updateDocumentTitle();
|
|
2801
|
+
});
|
|
2802
|
+
|
|
2803
|
+
// 5. Start: the board from the link, else the start screen on an empty board.
|
|
2804
|
+
const linked = await readHashMap();
|
|
2805
|
+
if (linked !== null) await openDocument(async () => (await modeler.importDSL(linked)).warnings, true);
|
|
2806
|
+
else {
|
|
2807
|
+
await modeler.importMap(createEmptyBoard("New board"));
|
|
2808
|
+
viewport.afterImport(); // an empty board at 100 %
|
|
2809
|
+
shell.setEmptyState(true);
|
|
2810
|
+
if (hashHoldsBoard())
|
|
2811
|
+
shell.toast({
|
|
2812
|
+
status: "danger",
|
|
2813
|
+
text: "Couldn't read the board in this link. Check that you copied the whole link.",
|
|
2814
|
+
});
|
|
2815
|
+
}
|
|
2816
|
+
```
|
|
2817
|
+
|
|
2818
|
+
`openBoardFile(file)` reads the file before the canvas is touched and then runs `replaceBoard`; a
|
|
2819
|
+
parse error shows the danger toast "Couldn't read “name”. Check that it contains a valid Event
|
|
2820
|
+
Storming board." and keeps the board. `pickFile` opens the hidden file input with the same
|
|
2821
|
+
`accept`. The JSON export writes `JSON.stringify(modeler.exportMap(), null, 2)`, the board's own
|
|
2822
|
+
element order (the importer stacks overlapping stickies by it). The web app's own `style.css`,
|
|
2823
|
+
`toast.ts` and the hand-written menu, zoom, fit and legal markup go away.
|
|
2824
|
+
|
|
2825
|
+
## Out of scope for 0.2
|
|
1861
2826
|
|
|
1862
|
-
|
|
2827
|
+
Later candidates unless noted:
|
|
1863
2828
|
|
|
1864
2829
|
- **VS Code document plumbing**: two-way sync with echo suppression, minimal `WorkspaceEdit`s,
|
|
1865
2830
|
undo/redo through the document's history, "Insert example" as one edit, diagnostics to the
|
|
@@ -1869,16 +2834,17 @@ Document sync, echo suppression, minimal edits and undo stay in the modeler for
|
|
|
1869
2834
|
`KeyboardBindings` off undo/redo in the IDE host, stops undo/redo keys in text fields and stops
|
|
1870
2835
|
keys the canvas handled.
|
|
1871
2836
|
- An ES-module webview bundle (`renderWebviewHtml` writes a classic script; a `scriptType` option).
|
|
1872
|
-
- **
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
- **Dialog component and the shared shortcuts dialog** (§5.4, §8
|
|
2837
|
+
- **More of the web column**: further top-right actions left of the primary action (§5.7), the
|
|
2838
|
+
loading state (§9 "Laden": spinner or skeleton during asynchronous work), share-link helpers
|
|
2839
|
+
(each modeler keeps its own hash encoding), the legend's state across sessions (a modeler persists
|
|
2840
|
+
it from `legend.onToggle` in `localStorage`, §5.10).
|
|
2841
|
+
- **Dialog component and the shared shortcuts dialog** (§5.4, §8; also the confirmation of §9
|
|
2842
|
+
"Destruktiv" where an undo toast does not fit): an exported `cd-shell-dialog`
|
|
1877
2843
|
class (panel, header with Close, body, backdrop with `grayscale(1)`, the high-contrast and
|
|
1878
2844
|
forced-colours rules, the top layer through `showModal()`, focus trap and focus return), and on
|
|
1879
2845
|
it `shell.showShortcutsDialog(groups: ShortcutGroup[])` with one table per group and the chips of
|
|
1880
2846
|
the tooltip's `kbd`. The shell fills the View group itself from the chrome keys. New
|
|
1881
|
-
`ShellMessages`: `shortcutsTitle` ("Keyboard shortcuts")
|
|
2847
|
+
`ShellMessages`: `shortcutsTitle` ("Keyboard shortcuts") and the group names
|
|
1882
2848
|
"View", "Edit" and "Tools". Popovers (§5.4) follow on the same class. Until then a modeler meets
|
|
1883
2849
|
the rules of [Modeler dialogs](#modeler-dialogs) itself; one component lets every modeler migrate
|
|
1884
2850
|
once.
|
|
@@ -1887,8 +2853,9 @@ Document sync, echo suppression, minimal edits and undo stay in the modeler for
|
|
|
1887
2853
|
- the `appearance` surface of the modeler-api contract (a later 1.x minor) for embedded hosts and
|
|
1888
2854
|
the `miragon.<modeler>.colorTheme` setting UI (the bridge already honours `settingKey`).
|
|
1889
2855
|
- **Kit**:
|
|
1890
|
-
- cases for reduced motion and forced colours
|
|
1891
|
-
|
|
2856
|
+
- cases for reduced motion and forced colours on every host (`PageLike.emulateMedia` exists since
|
|
2857
|
+
0.2, but only the web `appearance` case uses it); the shell's CSS covers both, its own e2e
|
|
2858
|
+
suite checks VS Code's reduce motion and spot-checks forced colours on the web chrome;
|
|
1892
2859
|
- a CSP helper: serve the production bundle under `renderWebviewHtml`'s CSP inside the VS Code
|
|
1893
2860
|
stub, record every `securitypolicyviolation` and fail on any (until then the recipe under
|
|
1894
2861
|
`./vscode/extension`);
|