@miragon/modeler-shell 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/API.md +2869 -0
  2. package/LICENSE +21 -0
  3. package/README.md +566 -2
  4. package/THIRD_PARTY_NOTICES.md +52 -0
  5. package/dist/appearance-BBaM_4bD.d.ts +323 -0
  6. package/dist/appearance-D3FUcbu6.d.cts +53 -0
  7. package/dist/appearance-D3FUcbu6.d.ts +53 -0
  8. package/dist/appearance-XX3JC3pI.d.cts +323 -0
  9. package/dist/chunk-66HCSJNY.js +2792 -0
  10. package/dist/chunk-66HCSJNY.js.map +1 -0
  11. package/dist/chunk-KYDG5DMX.js +136 -0
  12. package/dist/chunk-KYDG5DMX.js.map +1 -0
  13. package/dist/chunk-N4QXFF3A.js +23 -0
  14. package/dist/chunk-N4QXFF3A.js.map +1 -0
  15. package/dist/chunk-OADW3GPA.js +785 -0
  16. package/dist/chunk-OADW3GPA.js.map +1 -0
  17. package/dist/core.cjs +873 -0
  18. package/dist/core.cjs.map +1 -0
  19. package/dist/core.d.cts +246 -0
  20. package/dist/core.d.ts +246 -0
  21. package/dist/core.js +131 -0
  22. package/dist/core.js.map +1 -0
  23. package/dist/diagram-js.cjs +1147 -0
  24. package/dist/diagram-js.cjs.map +1 -0
  25. package/dist/diagram-js.d.cts +170 -0
  26. package/dist/diagram-js.d.ts +170 -0
  27. package/dist/diagram-js.js +879 -0
  28. package/dist/diagram-js.js.map +1 -0
  29. package/dist/index.cjs +3632 -0
  30. package/dist/index.cjs.map +1 -0
  31. package/dist/index.d.cts +29 -0
  32. package/dist/index.d.ts +29 -0
  33. package/dist/index.js +143 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/protocol-DhYBakWL.d.cts +15 -0
  36. package/dist/protocol-IeW8bf4f.d.ts +15 -0
  37. package/dist/shell.css +1603 -0
  38. package/dist/testing-vscode-stub.cjs +332 -0
  39. package/dist/testing-vscode-stub.cjs.map +1 -0
  40. package/dist/testing-vscode-stub.d.cts +65 -0
  41. package/dist/testing-vscode-stub.d.ts +65 -0
  42. package/dist/testing-vscode-stub.js +154 -0
  43. package/dist/testing-vscode-stub.js.map +1 -0
  44. package/dist/testing.cjs +4663 -0
  45. package/dist/testing.cjs.map +1 -0
  46. package/dist/testing.d.cts +231 -0
  47. package/dist/testing.d.ts +231 -0
  48. package/dist/testing.js +4483 -0
  49. package/dist/testing.js.map +1 -0
  50. package/dist/toasts-Bp0REqdq.d.cts +142 -0
  51. package/dist/toasts-Bp0REqdq.d.ts +142 -0
  52. package/dist/types-CnRrINRs.d.cts +445 -0
  53. package/dist/types-PuDZczzy.d.ts +445 -0
  54. package/dist/vscode-extension.cjs +215 -0
  55. package/dist/vscode-extension.cjs.map +1 -0
  56. package/dist/vscode-extension.d.cts +103 -0
  57. package/dist/vscode-extension.d.ts +103 -0
  58. package/dist/vscode-extension.js +24 -0
  59. package/dist/vscode-extension.js.map +1 -0
  60. package/dist/vscode-webview.cjs +3561 -0
  61. package/dist/vscode-webview.cjs.map +1 -0
  62. package/dist/vscode-webview.d.cts +84 -0
  63. package/dist/vscode-webview.d.ts +84 -0
  64. package/dist/vscode-webview.js +259 -0
  65. package/dist/vscode-webview.js.map +1 -0
  66. package/package.json +145 -4
  67. package/src/core/appearance.ts +104 -0
  68. package/src/core/feedback/appearance.ts +141 -0
  69. package/src/core/feedback/problems.ts +23 -0
  70. package/src/core/feedback/toasts.ts +118 -0
  71. package/src/core/fit.ts +220 -0
  72. package/src/core/index.ts +127 -0
  73. package/src/core/keymap.ts +295 -0
  74. package/src/core/layout.ts +69 -0
  75. package/src/core/messages.ts +159 -0
  76. package/src/core/sizes.ts +85 -0
  77. package/src/core/types.ts +106 -0
  78. package/src/core/web-chrome/bottom-row.ts +37 -0
  79. package/src/core/web-chrome/drop.ts +78 -0
  80. package/src/core/web-chrome/top-row.ts +66 -0
  81. package/src/core/zoom.ts +74 -0
  82. package/src/core.ts +2 -0
  83. package/src/diagram-js/connect.ts +75 -0
  84. package/src/diagram-js/index.ts +16 -0
  85. package/src/diagram-js/module.ts +20 -0
  86. package/src/diagram-js/palette-skin.ts +267 -0
  87. package/src/diagram-js/palette.css +269 -0
  88. package/src/diagram-js/service.ts +243 -0
  89. package/src/diagram-js/types.ts +67 -0
  90. package/src/diagram-js/viewport.ts +583 -0
  91. package/src/diagram-js.ts +2 -0
  92. package/src/dom/buttons.css +124 -0
  93. package/src/dom/element.ts +51 -0
  94. package/src/dom/feedback/appearance.ts +184 -0
  95. package/src/dom/feedback/feedback.css +435 -0
  96. package/src/dom/feedback/problems-popover.ts +287 -0
  97. package/src/dom/feedback/toasts.ts +431 -0
  98. package/src/dom/hint.ts +51 -0
  99. package/src/dom/icons.ts +309 -0
  100. package/src/dom/index.ts +42 -0
  101. package/src/dom/inspector.ts +35 -0
  102. package/src/dom/keys.ts +67 -0
  103. package/src/dom/ledger.ts +87 -0
  104. package/src/dom/legend.ts +85 -0
  105. package/src/dom/menu.ts +404 -0
  106. package/src/dom/mount.ts +731 -0
  107. package/src/dom/problems.ts +73 -0
  108. package/src/dom/shell-dom.css +969 -0
  109. package/src/dom/status.ts +72 -0
  110. package/src/dom/tooltip.ts +237 -0
  111. package/src/dom/types.ts +459 -0
  112. package/src/dom/web/drop-zone.ts +181 -0
  113. package/src/dom/web/empty-state.ts +144 -0
  114. package/src/dom/web/legal.ts +75 -0
  115. package/src/dom/web/menu-groups.ts +69 -0
  116. package/src/dom/web/primary.ts +124 -0
  117. package/src/dom/web/web.css +386 -0
  118. package/src/dom/zoom-panel.ts +154 -0
  119. package/src/index.ts +3 -0
  120. package/src/shell.css +9 -0
  121. package/src/testing/case-tools.ts +164 -0
  122. package/src/testing/cases.ts +1077 -0
  123. package/src/testing/checks.ts +868 -0
  124. package/src/testing/facts.ts +641 -0
  125. package/src/testing/geometry.ts +652 -0
  126. package/src/testing/harness.ts +187 -0
  127. package/src/testing/index.ts +19 -0
  128. package/src/testing/page-tools.ts +457 -0
  129. package/src/testing/page.ts +27 -0
  130. package/src/testing/vscode-stub.ts +237 -0
  131. package/src/testing/web-cases.ts +913 -0
  132. package/src/testing/web-checks.ts +1049 -0
  133. package/src/testing/web-facts.ts +713 -0
  134. package/src/testing-vscode-stub.ts +2 -0
  135. package/src/testing.ts +2 -0
  136. package/src/vscode/extension/html.ts +107 -0
  137. package/src/vscode/extension/index.ts +8 -0
  138. package/src/vscode/extension/reveal.ts +9 -0
  139. package/src/vscode/extension/theme-bridge.ts +85 -0
  140. package/src/vscode/extension/types.ts +36 -0
  141. package/src/vscode/protocol.ts +31 -0
  142. package/src/vscode/webview/appearance.ts +141 -0
  143. package/src/vscode/webview/index.ts +8 -0
  144. package/src/vscode/webview/mount.ts +88 -0
  145. package/src/vscode/webview/state.ts +116 -0
  146. package/src/vscode-extension.ts +2 -0
  147. package/src/vscode-webview.ts +2 -0
@@ -0,0 +1,287 @@
1
+ import { matchChromeKey, type ProblemItem, type ShellMessages } from "../../core/index.js";
2
+ import { createElement } from "../element.js";
3
+ import { iconElement } from "../icons.js";
4
+
5
+ export interface ProblemsPopoverContext {
6
+ ownerDocument: Document;
7
+ /** The shell root: outside clicks and the window's blur close the popover, as the menu does. */
8
+ root: HTMLElement;
9
+ /** The shell's layer (40): the popover lives there, like the dropdown. */
10
+ layer: HTMLElement;
11
+ /** The pill, its trigger: gets `aria-haspopup="dialog"`, `aria-expanded` and `aria-controls`. */
12
+ pill: HTMLButtonElement;
13
+ /** `cd-shell-problems-N` on the popover. */
14
+ id: string;
15
+ /** `cd-shell-problems-title-N` on its title. */
16
+ titleId: string;
17
+ messages: ShellMessages;
18
+ onSelectItem?: ((item: ProblemItem) => void) | undefined;
19
+ }
20
+
21
+ export interface ProblemsPopover {
22
+ /** The `<div data-chrome="problems-popover" role="dialog">`. */
23
+ readonly element: HTMLElement;
24
+ isOpen(): boolean;
25
+ /** Open (focus on the first row, else the popover) or close (focus back to the pill). */
26
+ toggle(): void;
27
+ open(): void;
28
+ close(options?: { restoreFocus?: boolean | undefined }): void;
29
+ /** The rows; while open the list re-renders and keeps the focused row. */
30
+ setItems(items: readonly ProblemItem[]): void;
31
+ destroy(): void;
32
+ }
33
+
34
+ interface Row {
35
+ /** The same problem across `setItems`, so a re-render keeps the focus on it. */
36
+ key: string;
37
+ /** A `<button>` for a row with an element, else a `<div tabindex="-1">`. */
38
+ element: HTMLElement;
39
+ }
40
+
41
+ const PILL_ATTRIBUTES = ["aria-haspopup", "aria-expanded", "aria-controls"] as const;
42
+
43
+ function isPrintable(event: KeyboardEvent): boolean {
44
+ return event.key.length === 1 && /\S/.test(event.key) && !event.ctrlKey && !event.metaKey && !event.altKey;
45
+ }
46
+
47
+ /**
48
+ * The web app's problems list (guide §5.10): a popover above the pill, left-aligned with it
49
+ * (`left: 12px`, its bottom 8px above the pill's top), `role="dialog"` without `aria-modal`,
50
+ * `aria-labelledby` its title `messages.problemsTitle` (13px/600); opaque `--cd-ui-flaeche-hoch`,
51
+ * 1px `--cd-ui-linie`, radius `md`, `shadow-3`, `min(320px, 100cqw − 24px)` wide, at most 320px
52
+ * high with a scrolling list; pop-in 150ms from `translateY(4px)`. A row is at least 36px: the 16px
53
+ * icon of the pill (`circle-x` in `--cd-ui-danger`, `triangle-alert` in `--cd-ui-warning`), the
54
+ * message 13px `--cd-ui-text`, below it the location 13px `--cd-ui-text-leise`. Rows with an
55
+ * `elementId` are buttons: a click closes the popover, returns the focus to the canvas and then
56
+ * calls `onSelectItem(item)`; rows without one are text that still takes the focus (`tabindex="-1"`),
57
+ * so the keyboard reads every row. ↑/↓/Home/End move between all rows and scroll the focused one
58
+ * into the list; Enter and Space act only on a row button. Escape (the popover's own handler) and
59
+ * Tab out close it with the focus back on the pill (Tab then moves on from the pill, Shift+Tab stays
60
+ * on it); an outside `pointerdown` (capture) and the window's blur close it without moving the
61
+ * focus. The zoom keys keep working (not modal).
62
+ */
63
+ export function createProblemsPopover(context: ProblemsPopoverContext): ProblemsPopover {
64
+ const { ownerDocument, root, layer, pill, id, titleId, messages } = context;
65
+ const view = ownerDocument.defaultView;
66
+
67
+ const list = createElement(ownerDocument, "ul", { class: "cd-shell-problems-list", role: "list" });
68
+ const element = createElement(
69
+ ownerDocument,
70
+ "div",
71
+ {
72
+ class: "cd-shell-problems-popover",
73
+ "data-chrome": "problems-popover",
74
+ role: "dialog",
75
+ id,
76
+ "aria-labelledby": titleId,
77
+ tabindex: "-1",
78
+ },
79
+ createElement(
80
+ ownerDocument,
81
+ "h2",
82
+ { class: "cd-shell-problems-popover-title", id: titleId },
83
+ messages.problemsTitle,
84
+ ),
85
+ list,
86
+ );
87
+ element.hidden = true;
88
+ layer.append(element);
89
+
90
+ pill.setAttribute("aria-haspopup", "dialog");
91
+ pill.setAttribute("aria-expanded", "false");
92
+ pill.setAttribute("aria-controls", id);
93
+
94
+ let items: readonly ProblemItem[] = [];
95
+ let rows: Row[] = [];
96
+ let destroyed = false;
97
+
98
+ const isOpen = (): boolean => !element.hidden;
99
+
100
+ function keyOf(item: ProblemItem): string {
101
+ return `${item.elementId ?? ""}\u0000${item.severity}\u0000${item.message}`;
102
+ }
103
+
104
+ function rowElement(item: ProblemItem): HTMLElement {
105
+ const severity = item.severity === "error" ? "error" : "warning";
106
+ const parts = [
107
+ // The shape and colour of the icon carry the severity on screen, its name for screen readers.
108
+ createElement(
109
+ ownerDocument,
110
+ "span",
111
+ { class: "cd-shell-problem-icon", role: "img", "aria-label": messages.problemSeverity(severity) },
112
+ iconElement(severity === "error" ? "circle-x" : "triangle-alert"),
113
+ ),
114
+ createElement(ownerDocument, "span", { class: "cd-shell-problem-message" }, item.message),
115
+ item.location
116
+ ? createElement(ownerDocument, "span", { class: "cd-shell-problem-location" }, item.location)
117
+ : undefined,
118
+ ];
119
+ const attributes = { class: "cd-shell-problem", "data-severity": severity };
120
+ if (!item.elementId) {
121
+ const text = createElement(ownerDocument, "div", { ...attributes, tabindex: "-1" }, ...parts);
122
+ rows.push({ key: keyOf(item), element: text });
123
+ return text;
124
+ }
125
+ const button = createElement(ownerDocument, "button", { type: "button", ...attributes }, ...parts);
126
+ button.addEventListener("click", () => select(item));
127
+ rows.push({ key: keyOf(item), element: button });
128
+ return button;
129
+ }
130
+
131
+ function focusedRowIndex(): number {
132
+ return rows.findIndex((row) => row.element === ownerDocument.activeElement);
133
+ }
134
+
135
+ function render(): void {
136
+ const focusedIndex = focusedRowIndex();
137
+ const focusedKey = rows[focusedIndex]?.key;
138
+ rows = [];
139
+ list.replaceChildren(...items.map((item) => createElement(ownerDocument, "li", {}, rowElement(item))));
140
+ if (focusedKey === undefined || !isOpen()) return;
141
+ const kept = rows.find((row) => row.key === focusedKey) ?? rows[Math.min(focusedIndex, rows.length - 1)];
142
+ if (kept) focusRowElement(kept.element);
143
+ else element.focus({ preventScroll: true });
144
+ }
145
+
146
+ // Scrolls only the list: `focus()` and `scrollIntoView()` would also scroll the shell root.
147
+ function revealInList(row: HTMLElement): void {
148
+ const rowBox = row.getBoundingClientRect();
149
+ const listBox = list.getBoundingClientRect();
150
+ if (rowBox.top < listBox.top) list.scrollTop -= listBox.top - rowBox.top;
151
+ else if (rowBox.bottom > listBox.bottom) list.scrollTop += rowBox.bottom - listBox.bottom;
152
+ }
153
+
154
+ function focusRowElement(row: HTMLElement): void {
155
+ row.focus({ preventScroll: true });
156
+ revealInList(row);
157
+ }
158
+
159
+ function focusCanvas(): void {
160
+ const canvas = root.querySelector<HTMLElement | SVGElement>(".cd-shell-canvas svg[tabindex]");
161
+ canvas?.focus({ preventScroll: true });
162
+ if ((!canvas || ownerDocument.activeElement !== canvas) && !pill.hidden)
163
+ pill.focus({ preventScroll: true });
164
+ }
165
+
166
+ function select(item: ProblemItem): void {
167
+ close();
168
+ focusCanvas();
169
+ context.onSelectItem?.(item);
170
+ }
171
+
172
+ function focusRow(index: number): void {
173
+ if (rows.length === 0) return;
174
+ const row = rows[(index + rows.length) % rows.length];
175
+ if (row) focusRowElement(row.element);
176
+ }
177
+
178
+ const onOutsidePointerDown = (event: PointerEvent): void => {
179
+ const target = event.target as Node | null;
180
+ if (target && (element.contains(target) || pill.contains(target))) return;
181
+ close();
182
+ };
183
+
184
+ // The focus left the document (another window, an iframe of the host page): from the user's
185
+ // view a click outside, so the popover closes without moving the focus.
186
+ const onWindowBlur = (): void => close();
187
+
188
+ function open(): void {
189
+ if (destroyed || isOpen()) return;
190
+ element.hidden = false;
191
+ pill.setAttribute("aria-expanded", "true");
192
+ // Capture phase: diagram-js stops pointer events on palette, context pad and canvas.
193
+ ownerDocument.addEventListener("pointerdown", onOutsidePointerDown, true);
194
+ view?.addEventListener("blur", onWindowBlur);
195
+ list.scrollTop = 0;
196
+ const first = rows[0];
197
+ if (first) focusRowElement(first.element);
198
+ else element.focus({ preventScroll: true });
199
+ }
200
+
201
+ function close(options: { restoreFocus?: boolean | undefined } = {}): void {
202
+ if (!isOpen()) return;
203
+ element.hidden = true;
204
+ pill.setAttribute("aria-expanded", "false");
205
+ ownerDocument.removeEventListener("pointerdown", onOutsidePointerDown, true);
206
+ view?.removeEventListener("blur", onWindowBlur);
207
+ if (options.restoreFocus && !pill.hidden) pill.focus({ preventScroll: true });
208
+ }
209
+
210
+ // Keys typed in the list stop here, like the menu's: a modeler's keyboard on document (Event
211
+ // Storming deletes the selection on Delete) must never act on them.
212
+ function consume(event: KeyboardEvent): void {
213
+ event.preventDefault();
214
+ event.stopPropagation();
215
+ }
216
+
217
+ element.addEventListener("keydown", (event) => {
218
+ // Zoom and fit keys still act with the popover open; the shell's key listener runs them.
219
+ if (matchChromeKey(event)) return;
220
+ const index = focusedRowIndex();
221
+ switch (event.key) {
222
+ case "Escape":
223
+ consume(event);
224
+ close({ restoreFocus: true });
225
+ return;
226
+ case "Tab": {
227
+ // Without a row the popover itself holds the focus and is first and last at once.
228
+ const first = index <= 0;
229
+ const last = rows.length === 0 || index === rows.length - 1;
230
+ if (event.shiftKey && first) {
231
+ consume(event);
232
+ close({ restoreFocus: true });
233
+ } else if (!event.shiftKey && last) {
234
+ // The browser's Tab runs after this handler from the pill, so it moves on to what follows it.
235
+ event.stopPropagation();
236
+ close({ restoreFocus: true });
237
+ }
238
+ return;
239
+ }
240
+ case "ArrowDown":
241
+ focusRow(index + 1);
242
+ break;
243
+ case "ArrowUp":
244
+ focusRow(index < 0 ? rows.length - 1 : index - 1);
245
+ break;
246
+ case "Home":
247
+ focusRow(0);
248
+ break;
249
+ case "End":
250
+ focusRow(rows.length - 1);
251
+ break;
252
+ case "Enter":
253
+ case " ":
254
+ case "Delete":
255
+ case "Backspace":
256
+ // A row button's native click handles Enter and Space; nothing else may act on these keys.
257
+ event.stopPropagation();
258
+ return;
259
+ default:
260
+ if (isPrintable(event)) event.stopPropagation();
261
+ return;
262
+ }
263
+ consume(event);
264
+ });
265
+
266
+ return {
267
+ element,
268
+ isOpen,
269
+ toggle() {
270
+ if (isOpen()) close({ restoreFocus: true });
271
+ else open();
272
+ },
273
+ open,
274
+ close,
275
+ setItems(next) {
276
+ items = next;
277
+ if (!destroyed) render();
278
+ },
279
+ destroy() {
280
+ if (destroyed) return;
281
+ close();
282
+ destroyed = true;
283
+ for (const name of PILL_ATTRIBUTES) pill.removeAttribute(name);
284
+ element.remove();
285
+ },
286
+ };
287
+ }