@miragon/modeler-shell 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/API.md +1902 -0
  2. package/LICENSE +21 -0
  3. package/README.md +435 -2
  4. package/THIRD_PARTY_NOTICES.md +52 -0
  5. package/dist/appearance-D3FUcbu6.d.cts +53 -0
  6. package/dist/appearance-D3FUcbu6.d.ts +53 -0
  7. package/dist/chunk-5JSZ4VGA.js +551 -0
  8. package/dist/chunk-5JSZ4VGA.js.map +1 -0
  9. package/dist/chunk-6LQOFACK.js +1576 -0
  10. package/dist/chunk-6LQOFACK.js.map +1 -0
  11. package/dist/chunk-N4QXFF3A.js +23 -0
  12. package/dist/chunk-N4QXFF3A.js.map +1 -0
  13. package/dist/chunk-RJRFR6N3.js +136 -0
  14. package/dist/chunk-RJRFR6N3.js.map +1 -0
  15. package/dist/core.cjs +619 -0
  16. package/dist/core.cjs.map +1 -0
  17. package/dist/core.d.cts +171 -0
  18. package/dist/core.d.ts +171 -0
  19. package/dist/core.js +91 -0
  20. package/dist/core.js.map +1 -0
  21. package/dist/diagram-js.cjs +1115 -0
  22. package/dist/diagram-js.cjs.map +1 -0
  23. package/dist/diagram-js.d.cts +163 -0
  24. package/dist/diagram-js.d.ts +163 -0
  25. package/dist/diagram-js.js +867 -0
  26. package/dist/diagram-js.js.map +1 -0
  27. package/dist/index.cjs +2175 -0
  28. package/dist/index.cjs.map +1 -0
  29. package/dist/index.d.cts +20 -0
  30. package/dist/index.d.ts +20 -0
  31. package/dist/index.js +103 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/keymap-CObQE6q6.d.cts +151 -0
  34. package/dist/keymap-DORzHnK-.d.ts +151 -0
  35. package/dist/protocol-DhYBakWL.d.cts +15 -0
  36. package/dist/protocol-IeW8bf4f.d.ts +15 -0
  37. package/dist/shell.css +914 -0
  38. package/dist/testing-vscode-stub.cjs +332 -0
  39. package/dist/testing-vscode-stub.cjs.map +1 -0
  40. package/dist/testing-vscode-stub.d.cts +65 -0
  41. package/dist/testing-vscode-stub.d.ts +65 -0
  42. package/dist/testing-vscode-stub.js +154 -0
  43. package/dist/testing-vscode-stub.js.map +1 -0
  44. package/dist/testing.cjs +2364 -0
  45. package/dist/testing.cjs.map +1 -0
  46. package/dist/testing.d.cts +165 -0
  47. package/dist/testing.d.ts +165 -0
  48. package/dist/testing.js +2297 -0
  49. package/dist/testing.js.map +1 -0
  50. package/dist/types-B6NfkiUG.d.cts +66 -0
  51. package/dist/types-B6NfkiUG.d.ts +66 -0
  52. package/dist/types-DnZn9CPw.d.ts +284 -0
  53. package/dist/types-OuFlqZp0.d.cts +284 -0
  54. package/dist/vscode-extension.cjs +215 -0
  55. package/dist/vscode-extension.cjs.map +1 -0
  56. package/dist/vscode-extension.d.cts +103 -0
  57. package/dist/vscode-extension.d.ts +103 -0
  58. package/dist/vscode-extension.js +24 -0
  59. package/dist/vscode-extension.js.map +1 -0
  60. package/dist/vscode-webview.cjs +2198 -0
  61. package/dist/vscode-webview.cjs.map +1 -0
  62. package/dist/vscode-webview.d.cts +82 -0
  63. package/dist/vscode-webview.d.ts +82 -0
  64. package/dist/vscode-webview.js +250 -0
  65. package/dist/vscode-webview.js.map +1 -0
  66. package/package.json +145 -4
  67. package/src/core/appearance.ts +104 -0
  68. package/src/core/fit.ts +205 -0
  69. package/src/core/index.ts +84 -0
  70. package/src/core/keymap.ts +275 -0
  71. package/src/core/layout.ts +69 -0
  72. package/src/core/messages.ts +88 -0
  73. package/src/core/sizes.ts +85 -0
  74. package/src/core/types.ts +84 -0
  75. package/src/core/zoom.ts +74 -0
  76. package/src/core.ts +2 -0
  77. package/src/diagram-js/connect.ts +75 -0
  78. package/src/diagram-js/index.ts +16 -0
  79. package/src/diagram-js/module.ts +20 -0
  80. package/src/diagram-js/palette-skin.ts +267 -0
  81. package/src/diagram-js/palette.css +269 -0
  82. package/src/diagram-js/service.ts +240 -0
  83. package/src/diagram-js/types.ts +67 -0
  84. package/src/diagram-js/viewport.ts +564 -0
  85. package/src/diagram-js.ts +2 -0
  86. package/src/dom/element.ts +51 -0
  87. package/src/dom/hint.ts +51 -0
  88. package/src/dom/icons.ts +309 -0
  89. package/src/dom/index.ts +29 -0
  90. package/src/dom/inspector.ts +35 -0
  91. package/src/dom/keys.ts +59 -0
  92. package/src/dom/ledger.ts +87 -0
  93. package/src/dom/legend.ts +85 -0
  94. package/src/dom/menu.ts +393 -0
  95. package/src/dom/mount.ts +461 -0
  96. package/src/dom/problems.ts +72 -0
  97. package/src/dom/shell-dom.css +966 -0
  98. package/src/dom/status.ts +72 -0
  99. package/src/dom/tooltip.ts +237 -0
  100. package/src/dom/types.ts +277 -0
  101. package/src/dom/zoom-panel.ts +154 -0
  102. package/src/index.ts +3 -0
  103. package/src/shell.css +6 -0
  104. package/src/testing/cases.ts +1012 -0
  105. package/src/testing/checks.ts +832 -0
  106. package/src/testing/facts.ts +587 -0
  107. package/src/testing/geometry.ts +472 -0
  108. package/src/testing/harness.ts +138 -0
  109. package/src/testing/index.ts +15 -0
  110. package/src/testing/page-tools.ts +434 -0
  111. package/src/testing/page.ts +17 -0
  112. package/src/testing/vscode-stub.ts +237 -0
  113. package/src/testing-vscode-stub.ts +2 -0
  114. package/src/testing.ts +2 -0
  115. package/src/vscode/extension/html.ts +107 -0
  116. package/src/vscode/extension/index.ts +8 -0
  117. package/src/vscode/extension/reveal.ts +9 -0
  118. package/src/vscode/extension/theme-bridge.ts +85 -0
  119. package/src/vscode/extension/types.ts +36 -0
  120. package/src/vscode/protocol.ts +31 -0
  121. package/src/vscode/webview/appearance.ts +141 -0
  122. package/src/vscode/webview/index.ts +8 -0
  123. package/src/vscode/webview/mount.ts +76 -0
  124. package/src/vscode/webview/state.ts +116 -0
  125. package/src/vscode-extension.ts +2 -0
  126. package/src/vscode-webview.ts +2 -0
@@ -0,0 +1,587 @@
1
+ import type { ElementFacts, RelativeBox, ToolWindow } from "./page-tools.js";
2
+
3
+ /*
4
+ * Snapshots the cases judge in Node (`checks.ts`). Each probe runs in the page and must stay
5
+ * self-contained (see page-tools.ts).
6
+ */
7
+
8
+ export type ZoomButton = "out" | "actual" | "in" | "fit";
9
+
10
+ /** The chrome parts in the DOM (and tab) order of guide §5.14. */
11
+ export const DOM_ORDER = [
12
+ "menu",
13
+ "primary",
14
+ "palette",
15
+ "canvas",
16
+ "status",
17
+ "hint",
18
+ "inspector",
19
+ "problems",
20
+ "legend",
21
+ "zoom",
22
+ ] as const;
23
+
24
+ export type OrderedPart = (typeof DOM_ORDER)[number];
25
+
26
+ export interface PaletteEntryFacts extends ElementFacts {
27
+ group: string | null;
28
+ }
29
+
30
+ export interface AriaFacts {
31
+ platform: "mac" | "other";
32
+ documentLang: string;
33
+ rootLang: string | null;
34
+ trigger: ElementFacts | null;
35
+ /** The element the trigger's `aria-controls` names. */
36
+ controlled: ElementFacts | null;
37
+ palette: ElementFacts | null;
38
+ toolbar: ElementFacts | null;
39
+ /** The focusable canvas `<svg>` (`.djs-container > svg`). */
40
+ canvas: ElementFacts | null;
41
+ entries: PaletteEntryFacts[];
42
+ zoom: ElementFacts | null;
43
+ zoomButtons: Partial<Record<ZoomButton, ElementFacts>>;
44
+ /** The parts that exist, in DOM order. */
45
+ order: OrderedPart[];
46
+ /** Elements inside the root with a positive `tabindex`. */
47
+ positiveTabindex: string[];
48
+ /** Chrome controls with a native `title` tooltip. */
49
+ titled: string[];
50
+ problems: ElementFacts | null;
51
+ status: ElementFacts | null;
52
+ legend: ElementFacts | null;
53
+ /** Text of the legend's `aria-labelledby` targets. */
54
+ legendTitle: string | null;
55
+ legendClose: ElementFacts | null;
56
+ inspector: ElementFacts | null;
57
+ }
58
+
59
+ export const ariaProbe = ({ root }: { root: string }): AriaFacts | null => {
60
+ const tools = (window as ToolWindow).__modelerShellConformance!;
61
+ const rootElement = document.querySelector<HTMLElement>(root);
62
+ if (!rootElement) return null;
63
+ const find = (selector: string) => rootElement.querySelector(selector);
64
+ const factsOf = (element: Element | null | undefined) => (element ? tools.facts(element) : null);
65
+ const navigatorLike = navigator as Navigator & { userAgentData?: { platform?: string } };
66
+ const trigger = find('[data-chrome="menu"]');
67
+ const controlsId = trigger?.getAttribute("aria-controls");
68
+ const palette = find('[data-chrome="palette"]');
69
+ const toolbar =
70
+ palette?.querySelector('[role="toolbar"]') ?? palette?.querySelector(".djs-palette-entries");
71
+ const zoom = find('[data-chrome="zoom"]');
72
+ const zoomButtons: Partial<Record<ZoomButton, ElementFacts>> = {};
73
+ for (const name of ["out", "actual", "in", "fit"] as const) {
74
+ const button = zoom?.querySelector(`[data-zoom="${name}"]`);
75
+ if (button) zoomButtons[name] = tools.facts(button);
76
+ }
77
+ const legend = find('[data-chrome="legend"]');
78
+ const labelledBy = legend?.getAttribute("aria-labelledby");
79
+ const parts: Array<[string, Element | null]> = [
80
+ ["menu", trigger],
81
+ ["primary", find('[data-chrome="primary"]')],
82
+ ["palette", palette],
83
+ ["canvas", find(".djs-container > svg")],
84
+ ["status", find('[data-chrome="status"]')],
85
+ ["hint", find(".cd-shell-hint")],
86
+ ["inspector", find('[data-chrome="inspector"]')],
87
+ ["problems", find('[data-chrome="problems"]')],
88
+ ["legend", legend],
89
+ ["zoom", zoom],
90
+ ];
91
+ const present = parts.filter((part): part is [string, Element] => part[1] !== null);
92
+ present.sort(([, a], [, b]) => (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1));
93
+ const titledSelector = [
94
+ '[data-chrome="menu"][title]',
95
+ '[data-chrome="palette"] [data-action][title]',
96
+ '[data-chrome="zoom"] [title]',
97
+ '[data-chrome="problems"][title]',
98
+ ].join(", ");
99
+ return {
100
+ platform: /mac/i.test(navigatorLike.userAgentData?.platform ?? navigatorLike.platform ?? "")
101
+ ? "mac"
102
+ : "other",
103
+ documentLang: document.documentElement.lang,
104
+ rootLang: rootElement.getAttribute("lang"),
105
+ trigger: factsOf(trigger),
106
+ controlled: factsOf(controlsId ? document.getElementById(controlsId) : null),
107
+ palette: factsOf(palette),
108
+ toolbar: factsOf(toolbar),
109
+ canvas: factsOf(find(".djs-container > svg")),
110
+ entries: [...(palette?.querySelectorAll("[data-action]") ?? [])].map((entry) => ({
111
+ ...tools.facts(entry),
112
+ group: entry.closest("[data-group]")?.getAttribute("data-group") ?? null,
113
+ })),
114
+ zoom: factsOf(zoom),
115
+ zoomButtons,
116
+ order: present.map(([name]) => name) as OrderedPart[],
117
+ positiveTabindex: [...rootElement.querySelectorAll("[tabindex]")]
118
+ .filter((element) => Number(element.getAttribute("tabindex")) > 0)
119
+ .map((element) => `${tools.describe(element)} (tabindex="${element.getAttribute("tabindex")}")`),
120
+ titled: [...rootElement.querySelectorAll(titledSelector)].map((element) => tools.describe(element)),
121
+ problems: factsOf(find('[data-chrome="problems"]')),
122
+ status: factsOf(find('[data-chrome="status"]')),
123
+ legend: factsOf(legend),
124
+ legendTitle: labelledBy
125
+ ? labelledBy
126
+ .split(/\s+/)
127
+ .map((id) => document.getElementById(id)?.textContent?.trim() ?? "")
128
+ .join(" ")
129
+ .trim()
130
+ : null,
131
+ legendClose: factsOf(legend?.querySelector("button")),
132
+ inspector: factsOf(find('[data-chrome="inspector"]')),
133
+ };
134
+ };
135
+
136
+ export interface MenuItemFacts {
137
+ role: string;
138
+ name: string;
139
+ checked: string | null;
140
+ disabled: string | null;
141
+ haspopup: string | null;
142
+ /** `aria-label` of the enclosing `role="group"`; `null` outside a group. */
143
+ group: string | null;
144
+ }
145
+
146
+ export interface MenuFacts {
147
+ /** The trigger's `aria-expanded` and `aria-controls`. */
148
+ expanded: string | null;
149
+ controls: string | null;
150
+ /** The visible `role="menu"` inside the root. */
151
+ menu: ElementFacts | null;
152
+ items: MenuItemFacts[];
153
+ /** Index of the focused item; -1 when the focus is elsewhere. */
154
+ active: number;
155
+ activeIsTrigger: boolean;
156
+ }
157
+
158
+ export const menuProbe = ({ root }: { root: string }): MenuFacts | null => {
159
+ const tools = (window as ToolWindow).__modelerShellConformance!;
160
+ const rootElement = document.querySelector<HTMLElement>(root);
161
+ if (!rootElement) return null;
162
+ const trigger = rootElement.querySelector('[data-chrome="menu"]');
163
+ const menu =
164
+ [...rootElement.querySelectorAll('[role="menu"]')].find((element) => tools.visible(element)) ?? null;
165
+ const items = menu
166
+ ? [
167
+ ...menu.querySelectorAll('[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]'),
168
+ ].filter((item) => tools.visible(item))
169
+ : [];
170
+ return {
171
+ expanded: trigger?.getAttribute("aria-expanded") ?? null,
172
+ controls: trigger?.getAttribute("aria-controls") ?? null,
173
+ menu: menu ? tools.facts(menu) : null,
174
+ items: items.map((item) => {
175
+ const group = item.closest('[role="group"]');
176
+ return {
177
+ role: item.getAttribute("role") ?? "",
178
+ name: (item.getAttribute("aria-label") ?? item.textContent ?? "").replace(/\s+/g, " ").trim(),
179
+ checked: item.getAttribute("aria-checked"),
180
+ disabled: item.getAttribute("aria-disabled"),
181
+ haspopup: item.getAttribute("aria-haspopup"),
182
+ group: group && menu?.contains(group) ? (group.getAttribute("aria-label") ?? "") : null,
183
+ };
184
+ }),
185
+ active: items.findIndex((item) => item === document.activeElement),
186
+ activeIsTrigger: Boolean(trigger) && document.activeElement === trigger,
187
+ };
188
+ };
189
+
190
+ export interface PaletteFocusFacts {
191
+ /** `data-action` of every entry, in order. */
192
+ entries: string[];
193
+ /** Entries with `tabindex="0"`. */
194
+ tabStops: string[];
195
+ /** `data-action` of the focused entry; `null` when the focus is elsewhere. */
196
+ active: string | null;
197
+ }
198
+
199
+ export const paletteFocusProbe = ({ root }: { root: string }): PaletteFocusFacts | null => {
200
+ const palette = document.querySelector(root)?.querySelector('[data-chrome="palette"]');
201
+ if (!palette) return null;
202
+ const entries = [...palette.querySelectorAll("[data-action]")];
203
+ const active = document.activeElement;
204
+ return {
205
+ entries: entries.map((entry) => entry.getAttribute("data-action") ?? ""),
206
+ tabStops: entries
207
+ .filter((entry) => entry.getAttribute("tabindex") === "0")
208
+ .map((entry) => entry.getAttribute("data-action") ?? ""),
209
+ active: active && entries.includes(active) ? active.getAttribute("data-action") : null,
210
+ };
211
+ };
212
+
213
+ export const zoomButtonsProbe = ({ root }: { root: string }): Partial<Record<ZoomButton, ElementFacts>> => {
214
+ const tools = (window as ToolWindow).__modelerShellConformance!;
215
+ const zoom = document.querySelector(root)?.querySelector('[data-chrome="zoom"]');
216
+ const buttons: Partial<Record<ZoomButton, ElementFacts>> = {};
217
+ for (const name of ["out", "actual", "in", "fit"] as const) {
218
+ const button = zoom?.querySelector(`[data-zoom="${name}"]`);
219
+ if (button) buttons[name] = tools.facts(button);
220
+ }
221
+ return buttons;
222
+ };
223
+
224
+ /** The design-system roles whose resolved values must agree on every part (no half-dark). */
225
+ export const MODE_ROLES = ["--cd-ui-grund", "--cd-ui-text", "--cd-ui-flaeche-hoch", "--cd-ui-linie"] as const;
226
+
227
+ export interface ModeFacts {
228
+ name: string;
229
+ description: string;
230
+ colorScheme: string;
231
+ roles: Record<string, string>;
232
+ }
233
+
234
+ export interface ThemedElementFacts {
235
+ description: string;
236
+ isDocument: boolean;
237
+ /** Where it sits relative to the shell root. */
238
+ relation: "self" | "ancestor" | "descendant" | "elsewhere";
239
+ theme: string | null;
240
+ kontrast: string | null;
241
+ }
242
+
243
+ export interface AppearanceFacts {
244
+ documentClasses: string[];
245
+ /** Every element with `data-cd-theme` or `data-cd-kontrast`. */
246
+ themed: ThemedElementFacts[];
247
+ /** Root, canvas, every visible island, the open dropdown and the visible tooltip. */
248
+ parts: ModeFacts[];
249
+ }
250
+
251
+ export const appearanceProbe = ({
252
+ root,
253
+ roles,
254
+ }: {
255
+ root: string;
256
+ roles: readonly string[];
257
+ }): AppearanceFacts | null => {
258
+ const tools = (window as ToolWindow).__modelerShellConformance!;
259
+ const rootElement = document.querySelector<HTMLElement>(root);
260
+ if (!rootElement) return null;
261
+ const mode = (name: string, element: Element) => {
262
+ const style = getComputedStyle(element);
263
+ const resolved: Record<string, string> = {};
264
+ for (const role of roles) resolved[role] = style.getPropertyValue(role).trim();
265
+ return { name, description: tools.describe(element), colorScheme: style.colorScheme, roles: resolved };
266
+ };
267
+ const parts = [mode("root", rootElement)];
268
+ const canvas = rootElement.querySelector(".djs-container");
269
+ if (canvas) parts.push(mode("canvas", canvas));
270
+ for (const island of rootElement.querySelectorAll("[data-chrome]")) {
271
+ if (tools.visible(island)) parts.push(mode(island.getAttribute("data-chrome") ?? "island", island));
272
+ }
273
+ for (const [name, role] of [
274
+ ["dropdown", "menu"],
275
+ ["tooltip", "tooltip"],
276
+ ] as const) {
277
+ const popup = [...rootElement.querySelectorAll(`[role="${role}"]`)].find((element) =>
278
+ tools.visible(element),
279
+ );
280
+ if (popup) parts.push(mode(name, popup));
281
+ }
282
+ return {
283
+ documentClasses: [...document.documentElement.classList],
284
+ themed: [...document.querySelectorAll("[data-cd-theme], [data-cd-kontrast]")].map((element) => ({
285
+ description: tools.describe(element),
286
+ isDocument: element === document.documentElement,
287
+ relation:
288
+ element === rootElement
289
+ ? "self"
290
+ : element.contains(rootElement)
291
+ ? "ancestor"
292
+ : rootElement.contains(element)
293
+ ? "descendant"
294
+ : "elsewhere",
295
+ theme: element.getAttribute("data-cd-theme"),
296
+ kontrast: element.getAttribute("data-cd-kontrast"),
297
+ })),
298
+ parts,
299
+ };
300
+ };
301
+
302
+ /** The element with the focus, classified for the tab-sequence check. */
303
+ export const activeStopProbe = ({ root }: { root: string }): { description: string; kind: string } => {
304
+ const tools = (window as ToolWindow).__modelerShellConformance!;
305
+ const active = document.activeElement;
306
+ const rootElement = document.querySelector(root);
307
+ if (!active || active === document.body || !rootElement) return { description: "<body>", kind: "other" };
308
+ const description = tools.describe(active);
309
+ if (!rootElement.contains(active))
310
+ return { description: `${description} outside the shell root`, kind: "other" };
311
+ if (active.matches('[data-chrome="palette"] [data-action]')) return { description, kind: "palette" };
312
+ if (active.matches(".djs-container > svg")) return { description, kind: "canvas" };
313
+ if (active.closest('[data-chrome="primary"]')) return { description, kind: "primary" };
314
+ const tabindex = active.getAttribute("tabindex");
315
+ return {
316
+ description: tabindex === null ? description : `${description}[tabindex="${tabindex}"]`,
317
+ kind: "other",
318
+ };
319
+ };
320
+
321
+ /** The focused palette entry, its `aria-keyshortcuts` and the chip of the visible tooltip. */
322
+ export const paletteShortcutProbe = ({
323
+ root,
324
+ }: {
325
+ root: string;
326
+ }): { action: string; chip: string; keyShortcuts: string | null } | null => {
327
+ const tools = (window as ToolWindow).__modelerShellConformance!;
328
+ const rootElement = document.querySelector(root);
329
+ const active = document.activeElement;
330
+ if (!rootElement || !active?.matches('[data-chrome="palette"] [data-action]')) return null;
331
+ const tooltip = [...rootElement.querySelectorAll('[role="tooltip"]')].find((element) =>
332
+ tools.visible(element),
333
+ );
334
+ const chip = tooltip
335
+ ? [...tooltip.querySelectorAll("kbd")].find((element) => tools.visible(element))
336
+ : undefined;
337
+ return {
338
+ action: active.getAttribute("data-action") ?? "",
339
+ chip: (chip?.textContent ?? "").trim(),
340
+ keyShortcuts: active.getAttribute("aria-keyshortcuts"),
341
+ };
342
+ };
343
+
344
+ /** The canvas surround (guide §10.1) and the role it must show. */
345
+ export interface SurroundFacts {
346
+ /** Whether the root holds a `.djs-container`. */
347
+ container: boolean;
348
+ /** The first element from `.djs-container` out to the root that paints a background colour. */
349
+ painted: { description: string; color: string } | null;
350
+ /** `--cd-ui-grund` resolved to a computed colour inside the root. */
351
+ grund: string;
352
+ }
353
+
354
+ export const surroundProbe = ({ root }: { root: string }): SurroundFacts | null => {
355
+ const tools = (window as ToolWindow).__modelerShellConformance!;
356
+ const rootElement = document.querySelector<HTMLElement>(root);
357
+ if (!rootElement) return null;
358
+ const transparent = (color: string): boolean => {
359
+ if (color === "" || color === "transparent") return true;
360
+ const legacy = /^rgba\([^)]*,\s*([\d.]+)\)$/.exec(color);
361
+ if (legacy) return Number(legacy[1]) === 0;
362
+ const modern = /\/\s*([\d.]+)%?\s*\)$/.exec(color);
363
+ return modern ? Number(modern[1]) === 0 : false;
364
+ };
365
+ const start = rootElement.querySelector(".djs-container");
366
+ let painted: SurroundFacts["painted"] = null;
367
+ for (let node: Element | null = start; node && !painted; node = node.parentElement) {
368
+ const color = getComputedStyle(node).backgroundColor;
369
+ if (!transparent(color)) painted = { description: tools.describe(node), color };
370
+ if (node === rootElement) break;
371
+ }
372
+ const probe = document.createElement("span");
373
+ probe.setAttribute("data-modeler-shell-conformance", "role-probe");
374
+ probe.style.cssText = "position:absolute;visibility:hidden;background-color:var(--cd-ui-grund);";
375
+ rootElement.append(probe);
376
+ const grund = getComputedStyle(probe).backgroundColor;
377
+ probe.remove();
378
+ return { container: start !== null, painted, grund };
379
+ };
380
+
381
+ /** One visible palette entry, as the keyboard walk and the read-only lock read it. */
382
+ export interface PaletteEntryState {
383
+ action: string;
384
+ /** `data-group` of the enclosing `.group`; `null` outside one. */
385
+ group: string | null;
386
+ pressed: boolean;
387
+ disabled: boolean;
388
+ /** Viewport coordinates of the centre, for `page.mouse`. */
389
+ client: { x: number; y: number };
390
+ }
391
+
392
+ export const paletteEntriesProbe = ({ root }: { root: string }): PaletteEntryState[] => {
393
+ const tools = (window as ToolWindow).__modelerShellConformance!;
394
+ const palette = document.querySelector(root)?.querySelector('[data-chrome="palette"]');
395
+ if (!palette) return [];
396
+ return [...palette.querySelectorAll("[data-action]")]
397
+ .filter((entry) => tools.visible(entry))
398
+ .map((entry) => {
399
+ const rect = entry.getBoundingClientRect();
400
+ return {
401
+ action: entry.getAttribute("data-action") ?? "",
402
+ group: entry.closest("[data-group]")?.getAttribute("data-group") ?? null,
403
+ pressed: entry.getAttribute("aria-pressed") === "true",
404
+ disabled: entry.getAttribute("aria-disabled") === "true",
405
+ client: { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 },
406
+ };
407
+ });
408
+ };
409
+
410
+ /** The computed colours of one visible palette entry and of its icon. */
411
+ export interface PaletteColorEntry extends Omit<PaletteEntryState, "client"> {
412
+ hovered: boolean;
413
+ color: string;
414
+ /** The entry's `<svg>`, else its `::before` when that has content; `null` for an image or a text entry. */
415
+ icon: { kind: "svg" | "::before"; color: string } | null;
416
+ }
417
+
418
+ export interface PaletteColorFacts {
419
+ /** The roles of guide §5.5 and §13.5, resolved to computed colours inside the palette. */
420
+ roles: { text: string; interaktion: string; textLeise: string };
421
+ entries: PaletteColorEntry[];
422
+ }
423
+
424
+ export const paletteColorProbe = ({ root }: { root: string }): PaletteColorFacts | null => {
425
+ const tools = (window as ToolWindow).__modelerShellConformance!;
426
+ const palette = document.querySelector(root)?.querySelector('[data-chrome="palette"]');
427
+ if (!palette) return null;
428
+ const resolve = (role: string): string => {
429
+ const probe = document.createElement("span");
430
+ probe.setAttribute("data-modeler-shell-conformance", "role-probe");
431
+ probe.style.cssText = `position:absolute;visibility:hidden;color:var(${role});`;
432
+ palette.append(probe);
433
+ const color = getComputedStyle(probe).color;
434
+ probe.remove();
435
+ return color;
436
+ };
437
+ const iconOf = (entry: Element): PaletteColorEntry["icon"] => {
438
+ const svg = entry.querySelector("svg");
439
+ if (svg) return { kind: "svg", color: getComputedStyle(svg).color };
440
+ const before = getComputedStyle(entry, "::before");
441
+ if (before.content && before.content !== "none" && before.content !== "normal") {
442
+ return { kind: "::before", color: before.color };
443
+ }
444
+ return null;
445
+ };
446
+ return {
447
+ roles: {
448
+ text: resolve("--cd-ui-text"),
449
+ interaktion: resolve("--cd-ui-interaktion"),
450
+ textLeise: resolve("--cd-ui-text-leise"),
451
+ },
452
+ entries: [...palette.querySelectorAll("[data-action]")]
453
+ .filter((entry) => tools.visible(entry))
454
+ .map((entry) => ({
455
+ action: entry.getAttribute("data-action") ?? "",
456
+ group: entry.closest("[data-group]")?.getAttribute("data-group") ?? null,
457
+ pressed: entry.getAttribute("aria-pressed") === "true",
458
+ disabled: entry.getAttribute("aria-disabled") === "true",
459
+ hovered: entry.matches(":hover"),
460
+ color: getComputedStyle(entry).color,
461
+ icon: iconOf(entry),
462
+ })),
463
+ };
464
+ };
465
+
466
+ /** One diagram-js shape on the canvas (`.djs-shape[data-element-id]`). */
467
+ export interface ShapeFacts {
468
+ id: string;
469
+ /** diagram-js's selection marker on the shape's `<g>`. */
470
+ selected: boolean;
471
+ /** The `<g>`'s `transform` (its position in the diagram, unaffected by panning). */
472
+ transform: string;
473
+ /** Size of its `.djs-visual` in diagram units. */
474
+ size: { width: number; height: number };
475
+ /** Its `.djs-visual` relative to the root's padding box. */
476
+ box: RelativeBox;
477
+ /** Viewport coordinates of the centre, for `page.mouse`. */
478
+ client: { x: number; y: number };
479
+ /** Whether a pointer at the centre reaches the shape and not chrome above it. */
480
+ hit: boolean;
481
+ }
482
+
483
+ export const shapesProbe = ({ root }: { root: string }): ShapeFacts[] => {
484
+ const tools = (window as ToolWindow).__modelerShellConformance!;
485
+ const rootElement = document.querySelector<HTMLElement>(root);
486
+ if (!rootElement) return [];
487
+ return [...rootElement.querySelectorAll<SVGGraphicsElement>(".djs-container .djs-shape[data-element-id]")]
488
+ .filter((shape) => tools.visible(shape))
489
+ .map((shape) => {
490
+ const visual = shape.querySelector<SVGGraphicsElement>(".djs-visual") ?? shape;
491
+ const rect = visual.getBoundingClientRect();
492
+ const client = { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
493
+ const local = typeof visual.getBBox === "function" ? visual.getBBox() : rect;
494
+ const hitElement = document.elementFromPoint(client.x, client.y);
495
+ return {
496
+ id: shape.getAttribute("data-element-id") ?? "",
497
+ selected: shape.classList.contains("selected"),
498
+ transform: shape.getAttribute("transform") ?? "",
499
+ size: { width: local.width, height: local.height },
500
+ box: tools.box(visual, rootElement),
501
+ client,
502
+ hit: hitElement !== null && shape.contains(hitElement),
503
+ };
504
+ });
505
+ };
506
+
507
+ /** What the canvas offers at the moment: focus, context pad, open text fields. */
508
+ export interface CanvasStateFacts {
509
+ /** The focus is inside `.djs-container` (the `<svg>` or a label editor). */
510
+ focusInCanvas: boolean;
511
+ focused: string;
512
+ /** Visible entries of a diagram-js context pad. */
513
+ contextPadEntries: string[];
514
+ /** Visible text fields inside `.djs-container` (a label editor), the kit's stand-ins aside. */
515
+ textFields: string[];
516
+ }
517
+
518
+ export const canvasStateProbe = ({ root }: { root: string }): CanvasStateFacts => {
519
+ const tools = (window as ToolWindow).__modelerShellConformance!;
520
+ const container = document.querySelector(root)?.querySelector(".djs-container");
521
+ const active = document.activeElement;
522
+ const ours = (element: Element) => element.closest("[data-modeler-shell-conformance]") !== null;
523
+ const visibleIn = (selector: string) =>
524
+ container
525
+ ? [...container.querySelectorAll(selector)].filter(
526
+ (element) => !ours(element) && tools.visible(element),
527
+ )
528
+ : [];
529
+ return {
530
+ focusInCanvas: Boolean(container && active && container.contains(active)),
531
+ focused: !active || active === document.body ? "<body>" : tools.describe(active),
532
+ contextPadEntries: visibleIn(".djs-context-pad .entry").map(
533
+ (entry) => entry.getAttribute("data-action") ?? tools.describe(entry),
534
+ ),
535
+ textFields: visibleIn('input, textarea, [contenteditable]:not([contenteditable="false"])').map((field) =>
536
+ tools.describe(field),
537
+ ),
538
+ };
539
+ };
540
+
541
+ /** The read-only marks of the root and the status box (guide §5.12, §13.5). */
542
+ export interface ReadOnlyStateFacts {
543
+ readOnly: boolean;
544
+ status: ElementFacts | null;
545
+ }
546
+
547
+ export const readOnlyStateProbe = ({ root }: { root: string }): ReadOnlyStateFacts | null => {
548
+ const tools = (window as ToolWindow).__modelerShellConformance!;
549
+ const rootElement = document.querySelector(root);
550
+ if (!rootElement) return null;
551
+ const status = rootElement.querySelector('[data-chrome="status"]');
552
+ return { readOnly: rootElement.hasAttribute("data-readonly"), status: status ? tools.facts(status) : null };
553
+ };
554
+
555
+ /** One item of the open menu with the group it stands in, counted by the separators. */
556
+ export interface MenuEntryFacts {
557
+ name: string;
558
+ role: string;
559
+ /** `data-menu-id` of the item or its radio group. */
560
+ menuId: string | null;
561
+ disabled: boolean;
562
+ /** `data-menu-group` of the item or its radio group: the shell's `model`, `export` or `help`. */
563
+ group: string | null;
564
+ }
565
+
566
+ export const menuEntriesProbe = ({ root }: { root: string }): MenuEntryFacts[] | null => {
567
+ const tools = (window as ToolWindow).__modelerShellConformance!;
568
+ const rootElement = document.querySelector(root);
569
+ const menu = rootElement
570
+ ? [...rootElement.querySelectorAll('[role="menu"]')].find((element) => tools.visible(element))
571
+ : undefined;
572
+ if (!menu) return null;
573
+ const entries: MenuEntryFacts[] = [];
574
+ const selector = '[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]';
575
+ for (const element of menu.querySelectorAll(selector)) {
576
+ const role = element.getAttribute("role") ?? "";
577
+ if (!tools.visible(element)) continue;
578
+ entries.push({
579
+ name: (element.getAttribute("aria-label") ?? element.textContent ?? "").replace(/\s+/g, " ").trim(),
580
+ role,
581
+ menuId: element.closest("[data-menu-id]")?.getAttribute("data-menu-id") ?? null,
582
+ disabled: element.getAttribute("aria-disabled") === "true",
583
+ group: element.closest("[data-menu-group]")?.getAttribute("data-menu-group") ?? null,
584
+ });
585
+ }
586
+ return entries;
587
+ };