@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.
Files changed (98) hide show
  1. package/API.md +1051 -84
  2. package/README.md +162 -31
  3. package/dist/appearance-BBaM_4bD.d.ts +323 -0
  4. package/dist/appearance-XX3JC3pI.d.cts +323 -0
  5. package/dist/{chunk-6LQOFACK.js → chunk-A4YGKH4Y.js} +1252 -31
  6. package/dist/chunk-A4YGKH4Y.js.map +1 -0
  7. package/dist/{chunk-RJRFR6N3.js → chunk-KYDG5DMX.js} +2 -2
  8. package/dist/{chunk-5JSZ4VGA.js → chunk-OADW3GPA.js} +240 -6
  9. package/dist/chunk-OADW3GPA.js.map +1 -0
  10. package/dist/core.cjs +260 -6
  11. package/dist/core.cjs.map +1 -1
  12. package/dist/core.d.cts +81 -6
  13. package/dist/core.d.ts +81 -6
  14. package/dist/core.js +43 -3
  15. package/dist/diagram-js.cjs +34 -2
  16. package/dist/diagram-js.cjs.map +1 -1
  17. package/dist/diagram-js.d.cts +10 -3
  18. package/dist/diagram-js.d.ts +10 -3
  19. package/dist/diagram-js.js +14 -2
  20. package/dist/diagram-js.js.map +1 -1
  21. package/dist/index.cjs +1658 -196
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.cts +18 -9
  24. package/dist/index.d.ts +18 -9
  25. package/dist/index.js +44 -4
  26. package/dist/shell.css +692 -3
  27. package/dist/testing-vscode-stub.js +2 -2
  28. package/dist/testing.cjs +2849 -550
  29. package/dist/testing.cjs.map +1 -1
  30. package/dist/testing.d.cts +75 -9
  31. package/dist/testing.d.ts +75 -9
  32. package/dist/testing.js +2744 -558
  33. package/dist/testing.js.map +1 -1
  34. package/dist/toasts-Bp0REqdq.d.cts +142 -0
  35. package/dist/toasts-Bp0REqdq.d.ts +142 -0
  36. package/dist/{types-OuFlqZp0.d.cts → types-CnRrINRs.d.cts} +170 -9
  37. package/dist/{types-DnZn9CPw.d.ts → types-PuDZczzy.d.ts} +170 -9
  38. package/dist/vscode-extension.js +2 -2
  39. package/dist/vscode-webview.cjs +1546 -178
  40. package/dist/vscode-webview.cjs.map +1 -1
  41. package/dist/vscode-webview.d.cts +6 -4
  42. package/dist/vscode-webview.d.ts +6 -4
  43. package/dist/vscode-webview.js +11 -2
  44. package/dist/vscode-webview.js.map +1 -1
  45. package/package.json +2 -2
  46. package/src/core/feedback/appearance.ts +141 -0
  47. package/src/core/feedback/problems.ts +23 -0
  48. package/src/core/feedback/toasts.ts +118 -0
  49. package/src/core/fit.ts +18 -3
  50. package/src/core/index.ts +43 -0
  51. package/src/core/keymap.ts +20 -0
  52. package/src/core/messages.ts +71 -0
  53. package/src/core/types.ts +24 -2
  54. package/src/core/web-chrome/bottom-row.ts +37 -0
  55. package/src/core/web-chrome/drop.ts +78 -0
  56. package/src/core/web-chrome/top-row.ts +66 -0
  57. package/src/diagram-js/service.ts +4 -1
  58. package/src/diagram-js/viewport.ts +19 -0
  59. package/src/dom/buttons.css +124 -0
  60. package/src/dom/feedback/appearance.ts +184 -0
  61. package/src/dom/feedback/feedback.css +435 -0
  62. package/src/dom/feedback/problems-popover.ts +287 -0
  63. package/src/dom/feedback/toasts.ts +431 -0
  64. package/src/dom/index.ts +13 -0
  65. package/src/dom/keys.ts +8 -0
  66. package/src/dom/menu.ts +28 -17
  67. package/src/dom/mount.ts +290 -20
  68. package/src/dom/problems.ts +5 -4
  69. package/src/dom/shell-dom.css +7 -4
  70. package/src/dom/tooltip.ts +10 -0
  71. package/src/dom/types.ts +188 -6
  72. package/src/dom/web/drop-zone.ts +181 -0
  73. package/src/dom/web/empty-state.ts +144 -0
  74. package/src/dom/web/legal.ts +75 -0
  75. package/src/dom/web/menu-groups.ts +69 -0
  76. package/src/dom/web/primary.ts +124 -0
  77. package/src/dom/web/web.css +386 -0
  78. package/src/shell.css +3 -0
  79. package/src/testing/case-tools.ts +164 -0
  80. package/src/testing/cases.ts +209 -144
  81. package/src/testing/checks.ts +51 -15
  82. package/src/testing/facts.ts +56 -2
  83. package/src/testing/geometry.ts +206 -26
  84. package/src/testing/harness.ts +56 -7
  85. package/src/testing/index.ts +4 -0
  86. package/src/testing/page-tools.ts +24 -1
  87. package/src/testing/page.ts +10 -0
  88. package/src/testing/web-cases.ts +913 -0
  89. package/src/testing/web-checks.ts +1049 -0
  90. package/src/testing/web-facts.ts +713 -0
  91. package/src/vscode/webview/mount.ts +13 -1
  92. package/dist/chunk-5JSZ4VGA.js.map +0 -1
  93. package/dist/chunk-6LQOFACK.js.map +0 -1
  94. package/dist/keymap-CObQE6q6.d.cts +0 -151
  95. package/dist/keymap-DORzHnK-.d.ts +0 -151
  96. package/dist/types-B6NfkiUG.d.cts +0 -66
  97. package/dist/types-B6NfkiUG.d.ts +0 -66
  98. /package/dist/{chunk-RJRFR6N3.js.map → chunk-KYDG5DMX.js.map} +0 -0
@@ -1,24 +1,37 @@
1
1
  import {
2
+ APPEARANCE_STORAGE_KEY,
3
+ LEGAL_LINE_HEIGHT,
4
+ MAX_TOASTS,
5
+ REDUCED_MOTION_CLASS,
2
6
  SHELL,
3
7
  TEXT_FIELD_SELECTOR,
4
8
  TOOLTIP_DELAY_MS,
9
+ acceptsFile,
5
10
  ariaKeyShortcuts,
6
11
  ariaKeyShortcutsFromLabel,
7
12
  canZoomIn,
8
13
  canZoomOut,
9
14
  decideKey,
10
- decidePaletteRow,
15
+ decideTopRow,
11
16
  detectPlatform,
17
+ documentMode,
18
+ dragVerdict,
12
19
  fitInsets,
13
20
  formatNumber,
14
21
  formatPercent,
22
+ legalLineHeight,
15
23
  legendStartsOpen,
16
24
  matchChromeKey,
25
+ parseAppearanceChoice,
17
26
  placeTooltip,
18
27
  resolveMessages,
19
28
  shortcutLabel,
20
- zoomReadout
21
- } from "./chunk-5JSZ4VGA.js";
29
+ toastLifetime,
30
+ toastStackBottom,
31
+ toastToEvict,
32
+ zoomReadout,
33
+ zoomRowRaised
34
+ } from "./chunk-OADW3GPA.js";
22
35
 
23
36
  // src/dom/icons.ts
24
37
  var ICONS = {
@@ -288,6 +301,636 @@ function hasVisibleFocus(element) {
288
301
  }
289
302
  }
290
303
 
304
+ // src/dom/feedback/appearance.ts
305
+ var NO_TRANSITION_CLASS = "cd-shell-no-transition";
306
+ var CONTRAST_QUERY = "(prefers-contrast: more)";
307
+ var SCHEME_MEDIA = {
308
+ light: "(prefers-color-scheme: light)",
309
+ dark: "(prefers-color-scheme: dark)"
310
+ };
311
+ function onMediaChange(query, listener) {
312
+ if (typeof query.addEventListener === "function") {
313
+ query.addEventListener("change", listener);
314
+ return () => query.removeEventListener("change", listener);
315
+ }
316
+ query.addListener(listener);
317
+ return () => query.removeListener(listener);
318
+ }
319
+ function connectWebAppearance(context) {
320
+ const { ownerDocument, themes, options } = context;
321
+ const darkCapable = themes.includes("dark");
322
+ if (darkCapable && !options) {
323
+ throw new Error(
324
+ '@miragon/modeler-shell: a web modeler whose themes include "dark" needs the appearance option ({ themeColors } from its vendored cd-tokens.generated.json)'
325
+ );
326
+ }
327
+ const html = ownerDocument.documentElement;
328
+ const view = ownerDocument.defaultView;
329
+ const cleanups = [];
330
+ const contrast = typeof view?.matchMedia === "function" ? view.matchMedia(CONTRAST_QUERY) : void 0;
331
+ if (contrast) {
332
+ const mirrorContrast = () => toggleAttribute(html, "data-cd-kontrast", contrast.matches && "hoch");
333
+ mirrorContrast();
334
+ cleanups.push(onMediaChange(contrast, mirrorContrast));
335
+ }
336
+ function destroy() {
337
+ for (const cleanup of cleanups.splice(0)) cleanup();
338
+ }
339
+ if (!darkCapable || !options) {
340
+ return { choice: () => "system", setChoice() {
341
+ }, destroy };
342
+ }
343
+ const { themeColors } = options;
344
+ function storage() {
345
+ if (options?.storage) return options.storage;
346
+ try {
347
+ return globalThis.localStorage;
348
+ } catch {
349
+ return void 0;
350
+ }
351
+ }
352
+ function readStored() {
353
+ try {
354
+ return parseAppearanceChoice(storage()?.getItem(APPEARANCE_STORAGE_KEY));
355
+ } catch {
356
+ return "system";
357
+ }
358
+ }
359
+ function store(choice) {
360
+ try {
361
+ const target = storage();
362
+ if (choice === "system") target?.removeItem(APPEARANCE_STORAGE_KEY);
363
+ else target?.setItem(APPEARANCE_STORAGE_KEY, choice);
364
+ } catch {
365
+ }
366
+ }
367
+ function nextFrame(callback) {
368
+ if (view && typeof view.requestAnimationFrame === "function") {
369
+ const frame = view.requestAnimationFrame(() => callback());
370
+ return () => view.cancelAnimationFrame(frame);
371
+ }
372
+ const timer = setTimeout(callback, 16);
373
+ return () => clearTimeout(timer);
374
+ }
375
+ let cancelTransitionFrame;
376
+ function allowTransitions() {
377
+ cancelTransitionFrame?.();
378
+ cancelTransitionFrame = void 0;
379
+ html.classList.remove(NO_TRANSITION_CLASS);
380
+ }
381
+ cleanups.push(() => {
382
+ if (cancelTransitionFrame) allowTransitions();
383
+ });
384
+ function suspendTransitions() {
385
+ cancelTransitionFrame?.();
386
+ html.classList.add(NO_TRANSITION_CLASS);
387
+ cancelTransitionFrame = nextFrame(() => {
388
+ cancelTransitionFrame = nextFrame(allowTransitions);
389
+ });
390
+ }
391
+ function metaFor(name, media) {
392
+ const normalize = (value) => (value ?? "").replace(/\s+/g, "").toLowerCase();
393
+ const existing = [...ownerDocument.querySelectorAll(`meta[name="${name}"]`)].find(
394
+ (meta) => media === void 0 ? true : normalize(meta.getAttribute("media")) === normalize(media)
395
+ );
396
+ if (existing) return existing;
397
+ const created = createElement(ownerDocument, "meta", { name, media });
398
+ (ownerDocument.head ?? html).append(created);
399
+ return created;
400
+ }
401
+ function apply(choice) {
402
+ const mode = documentMode(choice, themes, themeColors);
403
+ suspendTransitions();
404
+ html.setAttribute("data-cd-theme", mode.theme);
405
+ metaFor("color-scheme").setAttribute("content", mode.colorScheme);
406
+ metaFor("theme-color", SCHEME_MEDIA.light).setAttribute("content", mode.themeColor.light);
407
+ metaFor("theme-color", SCHEME_MEDIA.dark).setAttribute("content", mode.themeColor.dark);
408
+ }
409
+ let current = readStored();
410
+ const onStorage = (event) => {
411
+ if (event.key !== null && event.key !== APPEARANCE_STORAGE_KEY) return;
412
+ const next = event.key === null ? "system" : parseAppearanceChoice(event.newValue);
413
+ if (next === current) return;
414
+ current = next;
415
+ apply(next);
416
+ };
417
+ if (view) {
418
+ view.addEventListener("storage", onStorage);
419
+ cleanups.push(() => view.removeEventListener("storage", onStorage));
420
+ }
421
+ return {
422
+ choice: () => current,
423
+ setChoice(choice) {
424
+ const next = parseAppearanceChoice(choice);
425
+ current = next;
426
+ store(next);
427
+ apply(next);
428
+ },
429
+ destroy
430
+ };
431
+ }
432
+
433
+ // src/dom/feedback/problems-popover.ts
434
+ var PILL_ATTRIBUTES = ["aria-haspopup", "aria-expanded", "aria-controls"];
435
+ function isPrintable(event) {
436
+ return event.key.length === 1 && /\S/.test(event.key) && !event.ctrlKey && !event.metaKey && !event.altKey;
437
+ }
438
+ function createProblemsPopover(context) {
439
+ const { ownerDocument, root, layer, pill, id, titleId, messages } = context;
440
+ const view = ownerDocument.defaultView;
441
+ const list = createElement(ownerDocument, "ul", { class: "cd-shell-problems-list", role: "list" });
442
+ const element = createElement(
443
+ ownerDocument,
444
+ "div",
445
+ {
446
+ class: "cd-shell-problems-popover",
447
+ "data-chrome": "problems-popover",
448
+ role: "dialog",
449
+ id,
450
+ "aria-labelledby": titleId,
451
+ tabindex: "-1"
452
+ },
453
+ createElement(
454
+ ownerDocument,
455
+ "h2",
456
+ { class: "cd-shell-problems-popover-title", id: titleId },
457
+ messages.problemsTitle
458
+ ),
459
+ list
460
+ );
461
+ element.hidden = true;
462
+ layer.append(element);
463
+ pill.setAttribute("aria-haspopup", "dialog");
464
+ pill.setAttribute("aria-expanded", "false");
465
+ pill.setAttribute("aria-controls", id);
466
+ let items = [];
467
+ let rows = [];
468
+ let destroyed = false;
469
+ const isOpen = () => !element.hidden;
470
+ function keyOf(item) {
471
+ return `${item.elementId ?? ""}\0${item.severity}\0${item.message}`;
472
+ }
473
+ function rowElement(item) {
474
+ const severity = item.severity === "error" ? "error" : "warning";
475
+ const parts = [
476
+ // The shape and colour of the icon carry the severity on screen, its name for screen readers.
477
+ createElement(
478
+ ownerDocument,
479
+ "span",
480
+ { class: "cd-shell-problem-icon", role: "img", "aria-label": messages.problemSeverity(severity) },
481
+ iconElement(severity === "error" ? "circle-x" : "triangle-alert")
482
+ ),
483
+ createElement(ownerDocument, "span", { class: "cd-shell-problem-message" }, item.message),
484
+ item.location ? createElement(ownerDocument, "span", { class: "cd-shell-problem-location" }, item.location) : void 0
485
+ ];
486
+ const attributes = { class: "cd-shell-problem", "data-severity": severity };
487
+ if (!item.elementId) {
488
+ const text = createElement(ownerDocument, "div", { ...attributes, tabindex: "-1" }, ...parts);
489
+ rows.push({ key: keyOf(item), element: text });
490
+ return text;
491
+ }
492
+ const button = createElement(ownerDocument, "button", { type: "button", ...attributes }, ...parts);
493
+ button.addEventListener("click", () => select(item));
494
+ rows.push({ key: keyOf(item), element: button });
495
+ return button;
496
+ }
497
+ function focusedRowIndex() {
498
+ return rows.findIndex((row) => row.element === ownerDocument.activeElement);
499
+ }
500
+ function render() {
501
+ const focusedIndex = focusedRowIndex();
502
+ const focusedKey = rows[focusedIndex]?.key;
503
+ rows = [];
504
+ list.replaceChildren(...items.map((item) => createElement(ownerDocument, "li", {}, rowElement(item))));
505
+ if (focusedKey === void 0 || !isOpen()) return;
506
+ const kept = rows.find((row) => row.key === focusedKey) ?? rows[Math.min(focusedIndex, rows.length - 1)];
507
+ if (kept) focusRowElement(kept.element);
508
+ else element.focus({ preventScroll: true });
509
+ }
510
+ function revealInList(row) {
511
+ const rowBox = row.getBoundingClientRect();
512
+ const listBox = list.getBoundingClientRect();
513
+ if (rowBox.top < listBox.top) list.scrollTop -= listBox.top - rowBox.top;
514
+ else if (rowBox.bottom > listBox.bottom) list.scrollTop += rowBox.bottom - listBox.bottom;
515
+ }
516
+ function focusRowElement(row) {
517
+ row.focus({ preventScroll: true });
518
+ revealInList(row);
519
+ }
520
+ function focusCanvas() {
521
+ const canvas = root.querySelector(".cd-shell-canvas svg[tabindex]");
522
+ canvas?.focus({ preventScroll: true });
523
+ if ((!canvas || ownerDocument.activeElement !== canvas) && !pill.hidden)
524
+ pill.focus({ preventScroll: true });
525
+ }
526
+ function select(item) {
527
+ close();
528
+ focusCanvas();
529
+ context.onSelectItem?.(item);
530
+ }
531
+ function focusRow(index) {
532
+ if (rows.length === 0) return;
533
+ const row = rows[(index + rows.length) % rows.length];
534
+ if (row) focusRowElement(row.element);
535
+ }
536
+ const onOutsidePointerDown = (event) => {
537
+ const target = event.target;
538
+ if (target && (element.contains(target) || pill.contains(target))) return;
539
+ close();
540
+ };
541
+ const onWindowBlur = () => close();
542
+ function open() {
543
+ if (destroyed || isOpen()) return;
544
+ element.hidden = false;
545
+ pill.setAttribute("aria-expanded", "true");
546
+ ownerDocument.addEventListener("pointerdown", onOutsidePointerDown, true);
547
+ view?.addEventListener("blur", onWindowBlur);
548
+ list.scrollTop = 0;
549
+ const first = rows[0];
550
+ if (first) focusRowElement(first.element);
551
+ else element.focus({ preventScroll: true });
552
+ }
553
+ function close(options = {}) {
554
+ if (!isOpen()) return;
555
+ element.hidden = true;
556
+ pill.setAttribute("aria-expanded", "false");
557
+ ownerDocument.removeEventListener("pointerdown", onOutsidePointerDown, true);
558
+ view?.removeEventListener("blur", onWindowBlur);
559
+ if (options.restoreFocus && !pill.hidden) pill.focus({ preventScroll: true });
560
+ }
561
+ function consume(event) {
562
+ event.preventDefault();
563
+ event.stopPropagation();
564
+ }
565
+ element.addEventListener("keydown", (event) => {
566
+ if (matchChromeKey(event)) return;
567
+ const index = focusedRowIndex();
568
+ switch (event.key) {
569
+ case "Escape":
570
+ consume(event);
571
+ close({ restoreFocus: true });
572
+ return;
573
+ case "Tab": {
574
+ const first = index <= 0;
575
+ const last = rows.length === 0 || index === rows.length - 1;
576
+ if (event.shiftKey && first) {
577
+ consume(event);
578
+ close({ restoreFocus: true });
579
+ } else if (!event.shiftKey && last) {
580
+ event.stopPropagation();
581
+ close({ restoreFocus: true });
582
+ }
583
+ return;
584
+ }
585
+ case "ArrowDown":
586
+ focusRow(index + 1);
587
+ break;
588
+ case "ArrowUp":
589
+ focusRow(index < 0 ? rows.length - 1 : index - 1);
590
+ break;
591
+ case "Home":
592
+ focusRow(0);
593
+ break;
594
+ case "End":
595
+ focusRow(rows.length - 1);
596
+ break;
597
+ case "Enter":
598
+ case " ":
599
+ case "Delete":
600
+ case "Backspace":
601
+ event.stopPropagation();
602
+ return;
603
+ default:
604
+ if (isPrintable(event)) event.stopPropagation();
605
+ return;
606
+ }
607
+ consume(event);
608
+ });
609
+ return {
610
+ element,
611
+ isOpen,
612
+ toggle() {
613
+ if (isOpen()) close({ restoreFocus: true });
614
+ else open();
615
+ },
616
+ open,
617
+ close,
618
+ setItems(next) {
619
+ items = next;
620
+ if (!destroyed) render();
621
+ },
622
+ destroy() {
623
+ if (destroyed) return;
624
+ close();
625
+ destroyed = true;
626
+ for (const name of PILL_ATTRIBUTES) pill.removeAttribute(name);
627
+ element.remove();
628
+ }
629
+ };
630
+ }
631
+
632
+ // src/dom/feedback/toasts.ts
633
+ var TOAST_RESUME_MIN_MS = 1500;
634
+ var TOAST_LEAVE_FALLBACK_MS = 400;
635
+ var LEAVE_ANIMATION = "cd-shell-toast-out";
636
+ var TOAST_ANNOUNCE_DELAY_MS = 100;
637
+ var INERT_HANDLE = Object.freeze({ dismiss() {
638
+ } });
639
+ function createToaster(context) {
640
+ const { ownerDocument, messages } = context;
641
+ const web = context.host === "web";
642
+ const element = createElement(ownerDocument, "div", {
643
+ class: "cd-shell-toasts",
644
+ "data-chrome": "toasts",
645
+ popover: web ? "manual" : void 0
646
+ });
647
+ const liveRegion = (politeness) => createElement(ownerDocument, "div", {
648
+ class: "cd-shell-live-region",
649
+ "data-live": politeness,
650
+ role: politeness === "polite" ? "status" : "alert",
651
+ "aria-live": politeness,
652
+ "aria-atomic": "true"
653
+ });
654
+ const politeRegion = liveRegion("polite");
655
+ const assertiveRegion = liveRegion("assertive");
656
+ const announcer = createElement(
657
+ ownerDocument,
658
+ "div",
659
+ { class: "cd-shell-live" },
660
+ politeRegion,
661
+ assertiveRegion
662
+ );
663
+ const live = [];
664
+ const leaveTimers = /* @__PURE__ */ new Set();
665
+ const announceTimers = /* @__PURE__ */ new Map();
666
+ let lastId = 0;
667
+ let destroyed = false;
668
+ function regionFor(request) {
669
+ return request.status === "danger" ? assertiveRegion : politeRegion;
670
+ }
671
+ function announce(request) {
672
+ const region = regionFor(request);
673
+ clearTimeout(announceTimers.get(region));
674
+ region.textContent = "";
675
+ announceTimers.set(
676
+ region,
677
+ setTimeout(() => {
678
+ announceTimers.delete(region);
679
+ if (!destroyed) region.textContent = request.text;
680
+ }, TOAST_ANNOUNCE_DELAY_MS)
681
+ );
682
+ }
683
+ function silence(request) {
684
+ const region = regionFor(request);
685
+ if (region.textContent === request.text) region.textContent = "";
686
+ }
687
+ function changed() {
688
+ if (!destroyed) context.onChange?.();
689
+ }
690
+ function supportsPopover() {
691
+ return web && typeof element.showPopover === "function" && typeof element.hidePopover === "function";
692
+ }
693
+ function popoverOpen() {
694
+ try {
695
+ return element.matches(":popover-open");
696
+ } catch {
697
+ return false;
698
+ }
699
+ }
700
+ function modalOpen() {
701
+ try {
702
+ return ownerDocument.querySelector(":modal") !== null;
703
+ } catch {
704
+ return true;
705
+ }
706
+ }
707
+ function raise() {
708
+ if (!supportsPopover() || !element.isConnected) return;
709
+ if (popoverOpen() && !modalOpen()) return;
710
+ const focused = ownerDocument.activeElement;
711
+ const keepFocus = focused && element.contains(focused) ? focused : void 0;
712
+ try {
713
+ element.hidePopover();
714
+ element.showPopover();
715
+ } catch {
716
+ return;
717
+ }
718
+ if (keepFocus && ownerDocument.activeElement !== keepFocus) keepFocus.focus({ preventScroll: true });
719
+ }
720
+ function lowerWhenEmpty() {
721
+ if (element.childElementCount > 0 || !supportsPopover()) return;
722
+ try {
723
+ element.hidePopover();
724
+ } catch {
725
+ }
726
+ }
727
+ function engaged(toast) {
728
+ return toast.hovered || toast.focused;
729
+ }
730
+ function stopTimer(toast) {
731
+ clearTimeout(toast.timer);
732
+ toast.timer = void 0;
733
+ }
734
+ function runTimer(toast) {
735
+ stopTimer(toast);
736
+ if (toast.remaining === void 0 || engaged(toast)) return;
737
+ toast.startedAt = Date.now();
738
+ toast.timer = setTimeout(() => close(toast), toast.remaining);
739
+ }
740
+ function pause(toast) {
741
+ if (toast.timer === void 0) return;
742
+ stopTimer(toast);
743
+ if (toast.remaining !== void 0) {
744
+ toast.remaining = Math.max(0, toast.remaining - (Date.now() - toast.startedAt));
745
+ }
746
+ }
747
+ function setEngagement(toast, hovered, focused) {
748
+ if (!live.includes(toast)) return;
749
+ const wasEngaged = engaged(toast);
750
+ toast.hovered = hovered;
751
+ toast.focused = focused;
752
+ if (engaged(toast) === wasEngaged) return;
753
+ if (wasEngaged) {
754
+ if (toast.remaining !== void 0) toast.remaining = Math.max(toast.remaining, TOAST_RESUME_MIN_MS);
755
+ runTimer(toast);
756
+ } else {
757
+ pause(toast);
758
+ }
759
+ }
760
+ function reducedMotion() {
761
+ if (ownerDocument.documentElement.classList.contains(REDUCED_MOTION_CLASS)) return true;
762
+ const view = ownerDocument.defaultView;
763
+ return typeof view?.matchMedia === "function" && view.matchMedia("(prefers-reduced-motion: reduce)").matches;
764
+ }
765
+ function removeElement(toastElement) {
766
+ toastElement.remove();
767
+ lowerWhenEmpty();
768
+ changed();
769
+ }
770
+ function leave(toastElement) {
771
+ toastElement.setAttribute("data-leaving", "");
772
+ toastElement.inert = true;
773
+ if (reducedMotion()) {
774
+ removeElement(toastElement);
775
+ return;
776
+ }
777
+ let done = false;
778
+ const finish = () => {
779
+ if (done) return;
780
+ done = true;
781
+ clearTimeout(fallback);
782
+ leaveTimers.delete(fallback);
783
+ removeElement(toastElement);
784
+ };
785
+ toastElement.addEventListener("animationend", (event) => {
786
+ if (event.target === toastElement && event.animationName === LEAVE_ANIMATION) finish();
787
+ });
788
+ const fallback = setTimeout(finish, TOAST_LEAVE_FALLBACK_MS);
789
+ leaveTimers.add(fallback);
790
+ }
791
+ function close(toast) {
792
+ const index = live.indexOf(toast);
793
+ if (index < 0) return;
794
+ live.splice(index, 1);
795
+ stopTimer(toast);
796
+ silence(toast.request);
797
+ const heldFocus = toast.element.contains(ownerDocument.activeElement);
798
+ if (heldFocus) {
799
+ const next = live[index] ?? live[index - 1];
800
+ if (next) next.closeButton.focus({ preventScroll: true });
801
+ else context.focusFallback();
802
+ }
803
+ leave(toast.element);
804
+ }
805
+ function setAction(toast, action) {
806
+ const previous = toast.actionButton;
807
+ const hadFocus = previous !== void 0 && ownerDocument.activeElement === previous;
808
+ previous?.remove();
809
+ toast.actionButton = void 0;
810
+ if (action) {
811
+ const button = createElement(
812
+ ownerDocument,
813
+ "button",
814
+ { type: "button", class: "cd-shell-toast-action cd-shell-button", "data-variant": "ghost" },
815
+ action.label
816
+ );
817
+ button.addEventListener("click", () => {
818
+ close(toast);
819
+ action.onSelect();
820
+ });
821
+ toast.closeButton.before(button);
822
+ toast.actionButton = button;
823
+ }
824
+ if (hadFocus) (toast.actionButton ?? toast.closeButton).focus({ preventScroll: true });
825
+ }
826
+ function slotOf(toast) {
827
+ return { id: toast.id, hasAction: toast.request.action !== void 0, engaged: engaged(toast) };
828
+ }
829
+ function render(request) {
830
+ const id = ++lastId;
831
+ const textId = `${context.idPrefix}-${id}`;
832
+ const closeButton = createElement(
833
+ ownerDocument,
834
+ "button",
835
+ {
836
+ type: "button",
837
+ class: "cd-shell-toast-close",
838
+ "aria-label": messages.close,
839
+ "aria-describedby": textId
840
+ },
841
+ iconElement("x")
842
+ );
843
+ const toastElement = createElement(
844
+ ownerDocument,
845
+ "div",
846
+ { class: "cd-shell-toast", "data-status": request.status },
847
+ createElement(ownerDocument, "span", { class: "cd-shell-toast-dot", "aria-hidden": "true" }),
848
+ createElement(ownerDocument, "span", { class: "cd-shell-toast-text", id: textId }, request.text),
849
+ closeButton
850
+ );
851
+ const toast = {
852
+ id,
853
+ element: toastElement,
854
+ closeButton,
855
+ handle: Object.freeze({ dismiss: () => close(toast) }),
856
+ request,
857
+ actionButton: void 0,
858
+ hovered: false,
859
+ focused: false,
860
+ remaining: toastLifetime(request),
861
+ startedAt: Date.now(),
862
+ timer: void 0
863
+ };
864
+ setAction(toast, request.action);
865
+ closeButton.addEventListener("click", () => close(toast));
866
+ toastElement.addEventListener("mouseenter", () => setEngagement(toast, true, toast.focused));
867
+ toastElement.addEventListener("mouseleave", () => setEngagement(toast, false, toast.focused));
868
+ toastElement.addEventListener("focusin", () => setEngagement(toast, toast.hovered, true));
869
+ toastElement.addEventListener(
870
+ "focusout",
871
+ (event) => setEngagement(toast, toast.hovered, toastElement.contains(event.relatedTarget))
872
+ );
873
+ return toast;
874
+ }
875
+ function show(request) {
876
+ if (destroyed) return INERT_HANDLE;
877
+ const same = live.find(
878
+ (toast2) => toast2.request.status === request.status && toast2.request.text === request.text
879
+ );
880
+ if (same) {
881
+ same.request = request;
882
+ setAction(same, request.action);
883
+ same.remaining = toastLifetime(request);
884
+ runTimer(same);
885
+ raise();
886
+ announce(request);
887
+ changed();
888
+ return same.handle;
889
+ }
890
+ while (live.length >= MAX_TOASTS) {
891
+ const evictedId = toastToEvict(live.map(slotOf));
892
+ const evicted = live.find((toast2) => toast2.id === evictedId) ?? live[0];
893
+ if (!evicted) break;
894
+ close(evicted);
895
+ }
896
+ const toast = render(request);
897
+ live.push(toast);
898
+ element.append(toast.element);
899
+ raise();
900
+ runTimer(toast);
901
+ announce(request);
902
+ changed();
903
+ return toast.handle;
904
+ }
905
+ return {
906
+ element,
907
+ announcer,
908
+ show,
909
+ setBottom(bottom) {
910
+ element.style.bottom = `${bottom}px`;
911
+ },
912
+ widestCard() {
913
+ let widest;
914
+ for (const card of element.children) {
915
+ const width = card.getBoundingClientRect().width;
916
+ if (width > 0) widest = Math.max(widest ?? 0, width);
917
+ }
918
+ return widest;
919
+ },
920
+ destroy() {
921
+ if (destroyed) return;
922
+ destroyed = true;
923
+ for (const toast of live.splice(0)) stopTimer(toast);
924
+ for (const timer of leaveTimers) clearTimeout(timer);
925
+ leaveTimers.clear();
926
+ for (const timer of announceTimers.values()) clearTimeout(timer);
927
+ announceTimers.clear();
928
+ element.remove();
929
+ announcer.remove();
930
+ }
931
+ };
932
+ }
933
+
291
934
  // src/dom/hint.ts
292
935
  function createEmptyHint(ownerDocument, messages, options) {
293
936
  const { onInsertExample } = options;
@@ -367,6 +1010,9 @@ function isModal(element) {
367
1010
  function openDialogs(scope) {
368
1011
  return [...scope.querySelectorAll('[role="dialog"], [role="alertdialog"], dialog')].filter(isShown);
369
1012
  }
1013
+ function isModalOpen(scope) {
1014
+ return openDialogs(scope).some(isModal);
1015
+ }
370
1016
  function createKeyListener(context) {
371
1017
  return (event) => {
372
1018
  if (event.key === "Escape") context.hideTooltip();
@@ -377,7 +1023,8 @@ function createKeyListener(context) {
377
1023
  menuOpen: context.menuOpen(),
378
1024
  dialogOpen: dialogs.length > 0,
379
1025
  modalOpen: dialogs.some(isModal),
380
- defaultPrevented: event.defaultPrevented
1026
+ defaultPrevented: event.defaultPrevented,
1027
+ startScreen: context.startScreen?.() ?? false
381
1028
  });
382
1029
  if (decision.preventDefault) event.preventDefault();
383
1030
  if (decision.stopPropagation) event.stopPropagation();
@@ -505,8 +1152,11 @@ function createLegend(context) {
505
1152
  }
506
1153
 
507
1154
  // src/dom/menu.ts
508
- var GROUP_ORDER = ["model", "export", "help"];
509
- function isPrintable(event) {
1155
+ var GROUP_ORDER = ["file", "model", "export", "appearance", "help"];
1156
+ function isModelerGroup(name) {
1157
+ return name === "model" || name === "export" || name === "help";
1158
+ }
1159
+ function isPrintable2(event) {
510
1160
  return event.key.length === 1 && /\S/.test(event.key) && !event.ctrlKey && !event.metaKey && !event.altKey;
511
1161
  }
512
1162
  function createMenu(context) {
@@ -535,8 +1185,9 @@ function createMenu(context) {
535
1185
  layer.prepend(dropdown);
536
1186
  const isOpen = () => !dropdown.hidden;
537
1187
  function entriesOf(name) {
538
- const own = groups[name] ?? [];
539
- return name === "model" ? [...own, ...context.shellModelEntries()] : own;
1188
+ const own = isModelerGroup(name) ? groups[name] ?? [] : [];
1189
+ const added = context.shellEntries(name);
1190
+ return added.length > 0 ? [...own, ...added] : own;
540
1191
  }
541
1192
  function hasEntries() {
542
1193
  return GROUP_ORDER.some((name) => entriesOf(name).length > 0);
@@ -590,14 +1241,12 @@ function createMenu(context) {
590
1241
  }
591
1242
  };
592
1243
  }
593
- const shortcut = entry.kind === "action" && entry.shortcut ? createElement(ownerDocument, "span", { class: "cd-shell-menu-shortcut" }, entry.shortcut) : void 0;
1244
+ const shortcut = entry.shortcut ? createElement(ownerDocument, "span", { class: "cd-shell-menu-shortcut" }, entry.shortcut) : void 0;
594
1245
  const element = itemElement("menuitem", entry.label, { icon: entry.icon, end: shortcut, disabled });
595
1246
  element.dataset.menuId = entry.id;
596
1247
  element.dataset.menuGroup = group;
597
- if (entry.kind === "action") {
598
- const keyShortcuts = entry.ariaKeyShortcuts ?? (entry.shortcut ? ariaKeyShortcutsFromLabel(entry.shortcut) : void 0);
599
- toggleAttribute(element, "aria-keyshortcuts", keyShortcuts || null);
600
- }
1248
+ const keyShortcuts = entry.ariaKeyShortcuts ?? (entry.shortcut ? ariaKeyShortcutsFromLabel(entry.shortcut) : void 0);
1249
+ toggleAttribute(element, "aria-keyshortcuts", keyShortcuts || null);
601
1250
  if (entry.kind === "dialog") element.setAttribute("aria-haspopup", "dialog");
602
1251
  if (entry.kind === "action" && entry.danger) element.dataset.danger = "";
603
1252
  return {
@@ -782,7 +1431,7 @@ function createMenu(context) {
782
1431
  event.stopPropagation();
783
1432
  return;
784
1433
  default:
785
- if (!isPrintable(event)) return;
1434
+ if (!isPrintable2(event)) return;
786
1435
  typeAhead(event.key);
787
1436
  }
788
1437
  consume(event);
@@ -812,7 +1461,7 @@ function createMenu(context) {
812
1461
 
813
1462
  // src/dom/problems.ts
814
1463
  function createProblemsPill(context) {
815
- const { ownerDocument, messages, lang, tooltip, options } = context;
1464
+ const { ownerDocument, messages, lang, tooltip } = context;
816
1465
  const icon = createElement(ownerDocument, "span", { class: "cd-shell-problems-icon" });
817
1466
  const text = createElement(ownerDocument, "span", { class: "cd-shell-problems-text" });
818
1467
  const element = createElement(
@@ -823,7 +1472,7 @@ function createProblemsPill(context) {
823
1472
  text
824
1473
  );
825
1474
  element.hidden = true;
826
- element.addEventListener("click", () => options.onActivate());
1475
+ element.addEventListener("click", () => context.onActivate());
827
1476
  const detachTooltip = context.showTooltip ? tooltip.attach(element, { label: messages.showProblems }) : void 0;
828
1477
  let severity;
829
1478
  return {
@@ -927,6 +1576,10 @@ function createTooltip({ root, layer, id }) {
927
1576
  let warmUntil = 0;
928
1577
  let pressed;
929
1578
  const detachers = /* @__PURE__ */ new Set();
1579
+ const resizeObserver = typeof ResizeObserver === "function" ? new ResizeObserver(() => {
1580
+ if (shown) place(shown.control);
1581
+ }) : void 0;
1582
+ resizeObserver?.observe(element);
930
1583
  function cancelPending() {
931
1584
  if (pending) clearTimeout(pending.timer);
932
1585
  pending = void 0;
@@ -1094,6 +1747,381 @@ function createTooltip({ root, layer, id }) {
1094
1747
  destroy() {
1095
1748
  hide();
1096
1749
  for (const detach of [...detachers]) detach();
1750
+ resizeObserver?.disconnect();
1751
+ element.remove();
1752
+ }
1753
+ };
1754
+ }
1755
+
1756
+ // src/dom/web/drop-zone.ts
1757
+ var DRAG_EVENTS = ["dragenter", "dragover", "dragleave", "drop"];
1758
+ function carriesFiles(transfer) {
1759
+ if (!transfer) return false;
1760
+ if (Array.from(transfer.types ?? []).includes("Files")) return true;
1761
+ return Array.from(transfer.items ?? []).some((item) => item.kind === "file");
1762
+ }
1763
+ function dragItems(transfer) {
1764
+ const items = Array.from(transfer.items ?? [], (item) => ({ kind: item.kind, type: item.type }));
1765
+ return items.length > 0 ? items : [{ kind: "file", type: "" }];
1766
+ }
1767
+ function acceptList(accept, lang) {
1768
+ const entries = accept.map((entry) => entry.trim()).filter((entry) => entry !== "");
1769
+ const extensions = entries.filter((entry) => entry.startsWith("."));
1770
+ const listed = extensions.length > 0 ? extensions : entries;
1771
+ let format;
1772
+ try {
1773
+ format = new Intl.ListFormat(lang, { type: "disjunction" });
1774
+ } catch {
1775
+ format = new Intl.ListFormat("en", { type: "disjunction" });
1776
+ }
1777
+ return format.format(listed);
1778
+ }
1779
+ function createDropZone(context) {
1780
+ const { ownerDocument, layer, messages, lang, options, modalOpen, toast, setState } = context;
1781
+ const chip = createElement(ownerDocument, "span", { class: "cd-shell-drop-zone-chip" });
1782
+ const element = createElement(
1783
+ ownerDocument,
1784
+ "div",
1785
+ { class: "cd-shell-drop-zone", "data-chrome": "drop-zone", "aria-hidden": "true" },
1786
+ chip
1787
+ );
1788
+ element.hidden = true;
1789
+ layer.append(element);
1790
+ const view = ownerDocument.defaultView;
1791
+ let depth = 0;
1792
+ let current = null;
1793
+ function verdictFor(items) {
1794
+ return dragVerdict(items, {
1795
+ accept: options.accept,
1796
+ multiple: options.multiple ?? false,
1797
+ dialogOpen: modalOpen()
1798
+ });
1799
+ }
1800
+ function show(verdict) {
1801
+ const next = verdict.state === "none" ? null : verdict.state;
1802
+ if (verdict.state === "over") chip.textContent = messages.dropFile;
1803
+ if (verdict.state === "forbidden") chip.textContent = messages.dropForbidden(verdict.reason);
1804
+ if (next === current) return;
1805
+ current = next;
1806
+ element.hidden = next === null;
1807
+ if (next === null) element.removeAttribute("data-state");
1808
+ else element.setAttribute("data-state", next);
1809
+ setState(next);
1810
+ }
1811
+ function track(transfer) {
1812
+ const verdict = verdictFor(dragItems(transfer));
1813
+ show(verdict);
1814
+ transfer.dropEffect = verdict.state === "forbidden" ? "none" : "copy";
1815
+ }
1816
+ function rejectByDefault(files) {
1817
+ const accepted = acceptList(options.accept, lang);
1818
+ for (const file of files) toast({ status: "danger", text: messages.cannotOpenFile(file.name, accepted) });
1819
+ }
1820
+ function receive(files, verdict) {
1821
+ if (verdict.state === "forbidden" && verdict.reason !== "type") return;
1822
+ const accepted = files.filter((file) => acceptsFile(file, options.accept));
1823
+ const rejected = files.filter((file) => !accepted.includes(file));
1824
+ if (accepted.length > 0) options.onDrop(accepted);
1825
+ if (rejected.length > 0) (options.onReject ?? rejectByDefault)(rejected);
1826
+ }
1827
+ const onDragEvent = (event) => {
1828
+ const transfer = event.dataTransfer;
1829
+ if (!carriesFiles(transfer)) return;
1830
+ event.preventDefault();
1831
+ switch (event.type) {
1832
+ case "dragenter":
1833
+ depth += 1;
1834
+ track(transfer);
1835
+ break;
1836
+ case "dragover":
1837
+ depth = Math.max(depth, 1);
1838
+ track(transfer);
1839
+ break;
1840
+ case "dragleave":
1841
+ depth = Math.max(0, depth - 1);
1842
+ if (depth === 0) show({ state: "none" });
1843
+ break;
1844
+ case "drop": {
1845
+ depth = 0;
1846
+ show({ state: "none" });
1847
+ const files = Array.from(transfer.files ?? []);
1848
+ const items = files.length > 0 ? files.map((file) => ({ kind: "file", type: file.type })) : dragItems(transfer);
1849
+ receive(files, verdictFor(items));
1850
+ break;
1851
+ }
1852
+ }
1853
+ };
1854
+ for (const type of DRAG_EVENTS) view?.addEventListener(type, onDragEvent);
1855
+ return {
1856
+ element,
1857
+ state: () => current,
1858
+ destroy() {
1859
+ for (const type of DRAG_EVENTS) view?.removeEventListener(type, onDragEvent);
1860
+ depth = 0;
1861
+ if (current !== null) {
1862
+ current = null;
1863
+ setState(null);
1864
+ }
1865
+ element.remove();
1866
+ }
1867
+ };
1868
+ }
1869
+
1870
+ // src/dom/web/empty-state.ts
1871
+ var KOMET_SIZE = "64";
1872
+ function createEmptyState(context) {
1873
+ const { ownerDocument, messages, options, dropHint, open, titleId, onLeave } = context;
1874
+ const button = (variant, label, run) => {
1875
+ const element2 = createElement(
1876
+ ownerDocument,
1877
+ "button",
1878
+ { type: "button", class: "cd-shell-button", "data-variant": variant },
1879
+ label
1880
+ );
1881
+ element2.addEventListener("click", () => {
1882
+ onLeave();
1883
+ run();
1884
+ });
1885
+ return element2;
1886
+ };
1887
+ const onShowExample = options.onShowExample;
1888
+ const newDiagram = button("primary", messages.newDiagram, () => options.onNewDiagram());
1889
+ const element = createElement(
1890
+ ownerDocument,
1891
+ "section",
1892
+ { class: "cd-shell-empty-state", "data-chrome": "empty-state", "aria-labelledby": titleId },
1893
+ // Dragging the mark would start an image drag that some browsers carry as a file.
1894
+ createElement(ownerDocument, "img", {
1895
+ class: "cd-shell-empty-state-mark",
1896
+ src: options.komet,
1897
+ width: KOMET_SIZE,
1898
+ height: KOMET_SIZE,
1899
+ alt: "",
1900
+ draggable: "false"
1901
+ }),
1902
+ createElement(ownerDocument, "h1", { class: "cd-shell-empty-state-title", id: titleId }, options.title),
1903
+ createElement(ownerDocument, "p", { class: "cd-shell-empty-state-text" }, options.text),
1904
+ createElement(
1905
+ ownerDocument,
1906
+ "div",
1907
+ { class: "cd-shell-empty-state-actions" },
1908
+ newDiagram,
1909
+ onShowExample && button("ghost", messages.showExample, () => onShowExample())
1910
+ ),
1911
+ hintParagraph()
1912
+ );
1913
+ function hintParagraph() {
1914
+ const hasDropHint = dropHint !== void 0 && dropHint !== "";
1915
+ if (!hasDropHint && !open) return void 0;
1916
+ const paragraph = createElement(ownerDocument, "p", { class: "cd-shell-empty-state-hint" });
1917
+ if (hasDropHint) paragraph.append(dropHint);
1918
+ if (!open) return paragraph;
1919
+ const shortcut = open.shortcut?.trim();
1920
+ const link = createElement(
1921
+ ownerDocument,
1922
+ "button",
1923
+ {
1924
+ type: "button",
1925
+ class: "cd-shell-empty-state-open",
1926
+ "aria-keyshortcuts": shortcut ? ariaKeyShortcutsFromLabel(shortcut) : void 0,
1927
+ "aria-haspopup": open.dialog ? "dialog" : void 0
1928
+ },
1929
+ messages.openFileLink
1930
+ );
1931
+ link.addEventListener("click", () => open.onSelect());
1932
+ if (hasDropHint) {
1933
+ paragraph.append(" ", createElement(ownerDocument, "span", { "aria-hidden": "true" }, "\xB7"), " ");
1934
+ }
1935
+ const openItem = createElement(ownerDocument, "span", { class: "cd-shell-empty-state-open-item" }, link);
1936
+ if (shortcut) {
1937
+ openItem.append(
1938
+ " ",
1939
+ createElement(
1940
+ ownerDocument,
1941
+ "kbd",
1942
+ { class: "cd-shell-empty-state-key", "aria-hidden": "true" },
1943
+ shortcut
1944
+ )
1945
+ );
1946
+ }
1947
+ paragraph.append(openItem);
1948
+ return paragraph;
1949
+ }
1950
+ element.hidden = true;
1951
+ return {
1952
+ element,
1953
+ isShown: () => !element.hidden,
1954
+ setShown(shown) {
1955
+ element.hidden = !shown;
1956
+ },
1957
+ focus() {
1958
+ newDiagram.focus({ preventScroll: true });
1959
+ },
1960
+ destroy() {
1961
+ element.remove();
1962
+ }
1963
+ };
1964
+ }
1965
+
1966
+ // src/dom/web/legal.ts
1967
+ var MIRAGON_HOME = "https://www.miragon.io/";
1968
+ var MIRAGON_IMPRESSUM = "https://www.miragon.io/impressum/";
1969
+ var MIRAGON_DATENSCHUTZ = "https://www.miragon.io/datenschutz/";
1970
+ function createLegalLine(context) {
1971
+ const { ownerDocument, messages, options } = context;
1972
+ const year = options.year ?? (/* @__PURE__ */ new Date()).getFullYear();
1973
+ const newTabId = `${context.idPrefix}-new-tab`;
1974
+ const link = (label, href, lang) => createElement(
1975
+ ownerDocument,
1976
+ "a",
1977
+ { href, lang, target: "_blank", rel: "noopener noreferrer", "aria-describedby": newTabId },
1978
+ label
1979
+ );
1980
+ const separator = () => createElement(ownerDocument, "span", { "aria-hidden": "true" }, "\xB7");
1981
+ const item = (anchor) => createElement(ownerDocument, "span", { class: "cd-shell-legal-item" }, separator(), anchor);
1982
+ const links = [
1983
+ link(messages.copyright(year), MIRAGON_HOME),
1984
+ link("Impressum", MIRAGON_IMPRESSUM, "de"),
1985
+ link("Datenschutz", MIRAGON_DATENSCHUTZ, "de"),
1986
+ ...(options.links ?? []).map((extra) => link(extra.label, extra.href, extra.lang))
1987
+ ];
1988
+ const element = createElement(ownerDocument, "nav", {
1989
+ class: "cd-shell-legal",
1990
+ "data-chrome": "legal",
1991
+ "aria-label": messages.legal
1992
+ });
1993
+ element.append(
1994
+ ...links.map((anchor, index) => index === 0 ? anchor : item(anchor)),
1995
+ createElement(ownerDocument, "span", { id: newTabId, hidden: "" }, messages.opensInNewTab)
1996
+ );
1997
+ return {
1998
+ element,
1999
+ destroy() {
2000
+ element.remove();
2001
+ }
2002
+ };
2003
+ }
2004
+
2005
+ // src/dom/web/menu-groups.ts
2006
+ function fileEntry(spec, action) {
2007
+ const common = {
2008
+ id: spec.id,
2009
+ label: spec.label,
2010
+ ...spec.icon ? { icon: spec.icon } : {},
2011
+ ...action.disabled ? { disabled: action.disabled } : {},
2012
+ ...action.shortcut ? { shortcut: action.shortcut } : {},
2013
+ onSelect: () => action.onSelect()
2014
+ };
2015
+ return action.dialog ? { kind: "dialog", ...common } : { kind: "action", ...common };
2016
+ }
2017
+ function fileMenuEntries(options, messages) {
2018
+ const specs = [
2019
+ { option: "newDiagram", id: "new-diagram", label: messages.newDiagram, icon: "file-plus" },
2020
+ { option: "open", id: "open", label: messages.openFile, icon: "folder-open" },
2021
+ // §5.14 gives "Show example" no icon.
2022
+ { option: "showExample", id: "show-example", label: messages.showExample, icon: void 0 },
2023
+ { option: "import", id: "import", label: messages.importFile, icon: "import" }
2024
+ ];
2025
+ return specs.flatMap((spec) => {
2026
+ const action = options[spec.option];
2027
+ return action ? [fileEntry(spec, action)] : [];
2028
+ });
2029
+ }
2030
+ function appearanceMenuEntries(appearance, messages) {
2031
+ return [
2032
+ {
2033
+ kind: "radio",
2034
+ id: "appearance",
2035
+ label: messages.appearance,
2036
+ options: [
2037
+ { value: "system", label: messages.appearanceSystem },
2038
+ { value: "light", label: messages.appearanceLight },
2039
+ { value: "dark", label: messages.appearanceDark }
2040
+ ],
2041
+ value: () => appearance.choice(),
2042
+ onSelect: (value) => appearance.setChoice(parseAppearanceChoice(value))
2043
+ }
2044
+ ];
2045
+ }
2046
+
2047
+ // src/dom/web/primary.ts
2048
+ var COLLAPSED_ATTRIBUTE = "data-collapsed";
2049
+ function createPrimaryAction(context) {
2050
+ const { ownerDocument, messages, tooltip, options } = context;
2051
+ const label = options.label ?? messages.share;
2052
+ const labelElement = createElement(ownerDocument, "span", { class: "cd-shell-primary-label" }, label);
2053
+ const element = createElement(
2054
+ ownerDocument,
2055
+ "button",
2056
+ {
2057
+ type: "button",
2058
+ class: "cd-shell-primary cd-shell-button",
2059
+ "data-variant": "primary",
2060
+ "data-chrome": "primary",
2061
+ "aria-label": label
2062
+ },
2063
+ iconElement(options.icon ?? "share-2"),
2064
+ labelElement
2065
+ );
2066
+ let collapsed = false;
2067
+ let disabled = false;
2068
+ let labelledWidth;
2069
+ function readDisabled() {
2070
+ disabled = options.disabled?.() ?? false;
2071
+ toggleAttribute(element, "aria-disabled", disabled ? "true" : null);
2072
+ }
2073
+ function showCollapsed(state) {
2074
+ labelElement.hidden = state;
2075
+ toggleAttribute(element, COLLAPSED_ATTRIBUTE, state ? "" : null);
2076
+ }
2077
+ function measureLabelledWidth() {
2078
+ if (!collapsed) return element.offsetWidth;
2079
+ showCollapsed(false);
2080
+ const width = element.offsetWidth;
2081
+ showCollapsed(true);
2082
+ return width;
2083
+ }
2084
+ function isEngaged() {
2085
+ if (ownerDocument.activeElement === element) return true;
2086
+ try {
2087
+ return element.matches(":hover");
2088
+ } catch {
2089
+ return false;
2090
+ }
2091
+ }
2092
+ const onClick = (event) => {
2093
+ if (disabled) {
2094
+ event.preventDefault();
2095
+ return;
2096
+ }
2097
+ options.onSelect();
2098
+ };
2099
+ element.addEventListener("click", onClick);
2100
+ const detachTooltip = tooltip.attach(element, () => collapsed ? { label } : null);
2101
+ readDisabled();
2102
+ return {
2103
+ element,
2104
+ isCollapsed: () => collapsed,
2105
+ refresh() {
2106
+ readDisabled();
2107
+ labelledWidth = void 0;
2108
+ },
2109
+ widths() {
2110
+ if (!collapsed || labelledWidth === void 0) {
2111
+ const measured = measureLabelledWidth();
2112
+ if (measured > 0) labelledWidth = measured;
2113
+ }
2114
+ return { withLabel: labelledWidth ?? 0, iconOnly: SHELL.row };
2115
+ },
2116
+ setCollapsed(next) {
2117
+ if (next === collapsed) return;
2118
+ collapsed = next;
2119
+ showCollapsed(next);
2120
+ if (!next && isEngaged()) tooltip.hide();
2121
+ },
2122
+ destroy() {
2123
+ detachTooltip();
2124
+ element.removeEventListener("click", onClick);
1097
2125
  element.remove();
1098
2126
  }
1099
2127
  };
@@ -1216,6 +2244,16 @@ function nextMountIndex() {
1216
2244
  return index;
1217
2245
  }
1218
2246
  var DEFAULT_THEMES = ["light"];
2247
+ var LEGAL_HEIGHT_PROPERTY = "--modeler-shell-legal-height";
2248
+ var INERT_TOAST = Object.freeze({ dismiss() {
2249
+ } });
2250
+ function visibleBoxIn(element, root) {
2251
+ if (!element || element.hidden || !element.isConnected) return void 0;
2252
+ if (typeof element.checkVisibility === "function" && !element.checkVisibility({ visibilityProperty: true }))
2253
+ return void 0;
2254
+ const box = boxRelativeTo(element, root);
2255
+ return box.width > 0 && box.height > 0 ? box : void 0;
2256
+ }
1219
2257
  function languageOf(root, host, requested) {
1220
2258
  if (requested) return requested;
1221
2259
  const declared = host === "embedded" ? root.getAttribute("lang") : root.closest("[lang]")?.getAttribute("lang");
@@ -1244,6 +2282,7 @@ function renderShell(root, options, themes, ledger) {
1244
2282
  const platform = options.platform ?? detectPlatform(globalThis.navigator);
1245
2283
  const lang = languageOf(root, host, options.lang);
1246
2284
  const keyScope = options.keyScope ?? (host === "embedded" ? "root" : "document");
2285
+ const web = host === "web";
1247
2286
  ledger.addClass(root, "cd-shell");
1248
2287
  ledger.attribute(root, "data-cd-shell-host", host);
1249
2288
  ledger.attribute(root, "data-palette", "top");
@@ -1256,6 +2295,9 @@ function renderShell(root, options, themes, ledger) {
1256
2295
  const setState = (name, value) => {
1257
2296
  if (!ledger.isClosed) ledger.attribute(root, name, value);
1258
2297
  };
2298
+ const appearance = web ? connectWebAppearance({ ownerDocument, themes, options: options.appearance }) : void 0;
2299
+ if (appearance) ledger.defer(() => appearance.destroy());
2300
+ const darkCapable = themes.includes("dark");
1259
2301
  const layer = createElement(ownerDocument, "div", { class: "cd-shell-layer" });
1260
2302
  ledger.defer(() => layer.remove());
1261
2303
  const tooltip = createTooltip({ root, layer, id: `cd-shell-tooltip-${index}` });
@@ -1287,6 +2329,14 @@ function renderShell(root, options, themes, ledger) {
1287
2329
  keepEnabledWhenReadOnly: true
1288
2330
  } : void 0;
1289
2331
  const shellModelEntries = legendEntry ? [legendEntry] : [];
2332
+ const fileEntries = web && options.file ? fileMenuEntries(options.file, messages) : [];
2333
+ const appearanceEntries = appearance && darkCapable ? appearanceMenuEntries(appearance, messages) : [];
2334
+ const shellEntries = (group) => {
2335
+ if (group === "file") return fileEntries;
2336
+ if (group === "model") return shellModelEntries;
2337
+ if (group === "appearance") return appearanceEntries;
2338
+ return [];
2339
+ };
1290
2340
  const menu = createMenu({
1291
2341
  root,
1292
2342
  layer,
@@ -1294,55 +2344,133 @@ function renderShell(root, options, themes, ledger) {
1294
2344
  messages,
1295
2345
  tooltip,
1296
2346
  groups: options.menu ?? {},
1297
- shellModelEntries: () => shellModelEntries,
2347
+ shellEntries,
1298
2348
  isReadOnly: () => readOnly
1299
2349
  });
1300
2350
  ledger.defer(() => menu.destroy());
1301
2351
  ledger.defer(tooltip.attach(menu.trigger, () => menu.isOpen() ? null : { label: messages.menu }));
2352
+ const primary = web && options.primary ? createPrimaryAction({ ownerDocument, messages, tooltip, options: options.primary }) : void 0;
2353
+ if (primary) ledger.defer(() => primary.destroy());
1302
2354
  const canvas = options.canvas ?? createElement(ownerDocument, "div");
1303
2355
  ledger.addClass(canvas, "cd-shell-canvas");
2356
+ if (web && !canvas.hasAttribute("role")) ledger.attribute(canvas, "role", "main");
1304
2357
  if (!options.canvas) ledger.defer(() => canvas.remove());
2358
+ function focusCanvasOrTrigger() {
2359
+ const took = (target) => {
2360
+ target?.focus({ preventScroll: true });
2361
+ return target !== null && target !== void 0 && ownerDocument.activeElement === target;
2362
+ };
2363
+ if (emptyState?.isShown()) {
2364
+ emptyState.focus();
2365
+ if (emptyState.element.contains(ownerDocument.activeElement)) return;
2366
+ }
2367
+ if (took(canvas.querySelector("svg[tabindex]"))) return;
2368
+ if (!menu.trigger.hidden) took(menu.trigger);
2369
+ }
1305
2370
  function moveFocusOutOf(island) {
1306
- if (!island.contains(ownerDocument.activeElement)) return;
1307
- const canvasTarget = canvas.querySelector("svg[tabindex]");
1308
- canvasTarget?.focus({ preventScroll: true });
1309
- if (canvasTarget && ownerDocument.activeElement === canvasTarget) return;
1310
- if (!menu.trigger.hidden) menu.trigger.focus({ preventScroll: true });
2371
+ if (island.contains(ownerDocument.activeElement)) focusCanvasOrTrigger();
1311
2372
  }
1312
2373
  const status = createStatusBox(ownerDocument);
1313
2374
  ledger.defer(() => status.destroy());
1314
2375
  const hint = options.emptyHint ? createEmptyHint(ownerDocument, messages, options.emptyHint) : void 0;
1315
2376
  if (hint) ledger.defer(() => hint.destroy());
2377
+ const emptyState = web && options.emptyState ? createEmptyState({
2378
+ ownerDocument,
2379
+ messages,
2380
+ options: options.emptyState,
2381
+ dropHint: options.dropZone ? options.emptyState.dropHint ?? messages.dropHint : void 0,
2382
+ open: options.file?.open,
2383
+ titleId: `cd-shell-empty-state-title-${index}`,
2384
+ onLeave: () => setEmptyStateShown(false)
2385
+ }) : void 0;
2386
+ if (emptyState) ledger.defer(() => emptyState.destroy());
1316
2387
  const inspector = options.inspector ? createInspector(ownerDocument, options.inspector, (open) => {
1317
2388
  setState("data-inspector", open ? "open" : null);
2389
+ updateBottomRow();
1318
2390
  placeHint();
1319
2391
  }) : void 0;
1320
2392
  if (inspector) ledger.defer(() => inspector.destroy());
1321
- const pill = options.problems ? createProblemsPill({
2393
+ const problemsOptions = options.problems;
2394
+ let popover;
2395
+ const pill = problemsOptions ? createProblemsPill({
1322
2396
  ownerDocument,
1323
2397
  messages,
1324
2398
  lang,
1325
2399
  tooltip,
1326
2400
  showTooltip: host === "ide",
1327
- options: options.problems
2401
+ onActivate() {
2402
+ if (popover) popover.toggle();
2403
+ else problemsOptions.onActivate?.();
2404
+ }
1328
2405
  }) : void 0;
1329
2406
  if (pill) ledger.defer(() => pill.destroy());
2407
+ if (pill && (web || host === "embedded" && !problemsOptions?.onActivate)) {
2408
+ const created = createProblemsPopover({
2409
+ ownerDocument,
2410
+ root,
2411
+ layer,
2412
+ pill: pill.element,
2413
+ id: `cd-shell-problems-${index}`,
2414
+ titleId: `cd-shell-problems-title-${index}`,
2415
+ messages,
2416
+ onSelectItem: problemsOptions?.onSelectItem
2417
+ });
2418
+ popover = created;
2419
+ ledger.defer(() => created.destroy());
2420
+ }
2421
+ const legal = web ? createLegalLine({
2422
+ ownerDocument,
2423
+ messages,
2424
+ options: options.legal ?? {},
2425
+ idPrefix: `cd-shell-legal-${index}`
2426
+ }) : void 0;
2427
+ if (legal) ledger.defer(() => legal.destroy());
1330
2428
  const zoomPanel = createZoomPanel({ ownerDocument, messages, lang, platform, tooltip });
1331
2429
  ledger.defer(() => zoomPanel.destroy());
1332
- root.prepend(menu.trigger, canvas);
2430
+ const toaster = host === "ide" ? void 0 : createToaster({
2431
+ ownerDocument,
2432
+ host,
2433
+ messages,
2434
+ idPrefix: `cd-shell-toast-${index}`,
2435
+ focusFallback: focusCanvasOrTrigger,
2436
+ onChange: () => updateBottomRow()
2437
+ });
2438
+ if (toaster) ledger.defer(() => toaster.destroy());
2439
+ const defined = (element) => element !== void 0;
2440
+ root.prepend(...[menu.trigger, primary?.element, canvas].filter(defined));
1333
2441
  root.append(
1334
2442
  ...[
1335
2443
  status.element,
1336
2444
  hint?.element,
2445
+ emptyState?.element,
1337
2446
  inspector?.element,
1338
2447
  pill?.element,
1339
2448
  legend?.element,
2449
+ legal?.element,
1340
2450
  zoomPanel.element
1341
- ].filter((element) => element !== void 0),
1342
- layer
2451
+ ].filter(defined),
2452
+ layer,
2453
+ ...[toaster?.announcer, toaster?.element].filter(defined)
1343
2454
  );
2455
+ function showToast(request) {
2456
+ return ledger.isClosed || !toaster ? INERT_TOAST : toaster.show(request);
2457
+ }
2458
+ const dropZone = web && options.dropZone ? createDropZone({
2459
+ ownerDocument,
2460
+ layer,
2461
+ messages,
2462
+ lang,
2463
+ options: options.dropZone,
2464
+ modalOpen: () => isModalOpen(ownerDocument),
2465
+ toast: showToast,
2466
+ setState: (state) => setState("data-drop", state)
2467
+ }) : void 0;
2468
+ if (dropZone) ledger.defer(() => dropZone.destroy());
1344
2469
  let palette;
1345
2470
  let paletteRow = "top";
2471
+ let zoomRaised = false;
2472
+ let legalHeight = LEGAL_LINE_HEIGHT;
2473
+ if (legal) ledger.defer(() => root.style.removeProperty(LEGAL_HEIGHT_PROPERTY));
1346
2474
  function containerSize() {
1347
2475
  return { width: root.clientWidth, height: root.clientHeight };
1348
2476
  }
@@ -1362,6 +2490,10 @@ function renderShell(root, options, themes, ledger) {
1362
2490
  paletteRow,
1363
2491
  problemsVisible: pill?.isShown() ?? false
1364
2492
  };
2493
+ if (web) {
2494
+ layout.zoomRaised = zoomRaised;
2495
+ layout.legalHeight = legalHeight;
2496
+ }
1365
2497
  if (legend?.isOpen()) layout.legendWidth = legend.element.getBoundingClientRect().width;
1366
2498
  if (inspector?.isOpen()) {
1367
2499
  layout.inspector = container.width > SHELL.narrow ? {} : { sheetTop: boxRelativeTo(inspector.element, root).y };
@@ -1375,18 +2507,76 @@ function renderShell(root, options, themes, ledger) {
1375
2507
  if (ledger.isClosed) return;
1376
2508
  const size = containerSize();
1377
2509
  if (size.width === 0 || size.height === 0) return;
1378
- paletteRow = decidePaletteRow({
2510
+ const topRow = decideTopRow({
1379
2511
  containerWidth: size.width,
1380
2512
  paletteWidth: paletteWidth(),
1381
- menuRight: menuRight()
2513
+ menuRight: menuRight(),
2514
+ primary: primary?.widths()
1382
2515
  });
2516
+ paletteRow = topRow.paletteRow;
1383
2517
  setState("data-palette", paletteRow);
2518
+ if (primary) {
2519
+ const collapsed = topRow.primaryLabel === "collapsed";
2520
+ primary.setCollapsed(collapsed);
2521
+ setState("data-primary", collapsed ? "collapsed" : null);
2522
+ }
1384
2523
  if (legend && !legend.isStarted()) legend.start(options.legend?.open ?? legendStartsOpen(size.height));
2524
+ updateBottomRow();
1385
2525
  placeHint();
1386
2526
  }
2527
+ function updateBottomRow() {
2528
+ if (ledger.isClosed) return;
2529
+ const size = containerSize();
2530
+ if (size.width === 0 || size.height === 0) return;
2531
+ if (legal) {
2532
+ const legalBox = visibleBoxIn(legal.element, root);
2533
+ const height = legalLineHeight(legalBox?.height);
2534
+ if (height !== legalHeight || !root.style.getPropertyValue(LEGAL_HEIGHT_PROPERTY)) {
2535
+ legalHeight = height;
2536
+ root.style.setProperty(LEGAL_HEIGHT_PROPERTY, `${height}px`);
2537
+ }
2538
+ zoomRaised = zoomRowRaised({
2539
+ containerWidth: size.width,
2540
+ legalRight: legalBox && legalBox.x + legalBox.width,
2541
+ zoomWidth: visibleBoxIn(zoomPanel.element, root)?.width
2542
+ });
2543
+ setState("data-zoom-row", zoomRaised ? "raised" : null);
2544
+ }
2545
+ if (toaster) {
2546
+ const obstacles = [zoomPanel.element, legal?.element, pill?.element].flatMap(
2547
+ (element) => {
2548
+ const box = visibleBoxIn(element, root);
2549
+ return box ? [{ left: box.x, right: box.x + box.width, top: size.height - box.y }] : [];
2550
+ }
2551
+ );
2552
+ toaster.setBottom(
2553
+ toastStackBottom({ containerWidth: size.width, obstacles, cardWidth: toaster.widestCard() })
2554
+ );
2555
+ }
2556
+ }
2557
+ function setEmptyStateShown(shown) {
2558
+ if (ledger.isClosed || !emptyState || emptyState.isShown() === shown) return;
2559
+ const active = ownerDocument.activeElement;
2560
+ const hadFocus = emptyState.element.contains(active);
2561
+ const workHadFocus = shown && active !== null && root.contains(active) && ![emptyState.element, legal?.element, toaster?.element].some((kept) => kept?.contains(active));
2562
+ emptyState.setShown(shown);
2563
+ setState("data-empty-state", shown ? "shown" : null);
2564
+ ledger.attribute(canvas, "inert", shown ? "" : null);
2565
+ if (shown) {
2566
+ menu.close();
2567
+ popover?.close();
2568
+ tooltip.hide();
2569
+ if (workHadFocus) emptyState.focus();
2570
+ } else if (hadFocus) {
2571
+ focusCanvasOrTrigger();
2572
+ }
2573
+ updateLayout();
2574
+ }
1387
2575
  const ResizeObserverClass = globalThis.ResizeObserver;
1388
2576
  const resizeObserver = ResizeObserverClass ? new ResizeObserverClass(() => updateLayout()) : void 0;
1389
2577
  resizeObserver?.observe(root);
2578
+ if (primary) resizeObserver?.observe(primary.element);
2579
+ if (legal) resizeObserver?.observe(legal.element);
1390
2580
  ledger.defer(() => resizeObserver?.disconnect());
1391
2581
  if (legend && options.legend?.open !== void 0) legend.start(options.legend.open);
1392
2582
  updateLayout();
@@ -1395,12 +2585,16 @@ function renderShell(root, options, themes, ledger) {
1395
2585
  host,
1396
2586
  dialogScope: keyTarget,
1397
2587
  menuOpen: () => menu.isOpen(),
2588
+ startScreen: () => emptyState?.isShown() ?? false,
1398
2589
  closeMenu() {
1399
2590
  const active = ownerDocument.activeElement;
1400
2591
  menu.close({ restoreFocus: active === menu.trigger || menu.dropdown.contains(active) });
1401
2592
  },
1402
2593
  hideTooltip: () => tooltip.hide(),
1403
- run: (key) => zoomPanel.run(key)
2594
+ // Behind the start screen the keys reach the browser instead: there is no diagram to zoom.
2595
+ run: (key) => {
2596
+ if (!emptyState?.isShown()) zoomPanel.run(key);
2597
+ }
1404
2598
  });
1405
2599
  keyTarget.addEventListener("keydown", onKeydown);
1406
2600
  ledger.defer(() => keyTarget.removeEventListener("keydown", onKeydown));
@@ -1452,6 +2646,21 @@ function renderShell(root, options, themes, ledger) {
1452
2646
  if (!ledger.isClosed) legend.toggle();
1453
2647
  }
1454
2648
  };
2649
+ const primaryHandle = primary && {
2650
+ element: primary.element,
2651
+ isCollapsed: () => primary.isCollapsed(),
2652
+ refresh: () => {
2653
+ if (!ledger.isClosed) primary.refresh();
2654
+ }
2655
+ };
2656
+ const appearanceHandle = appearance && darkCapable ? {
2657
+ choice: () => appearance.choice(),
2658
+ setChoice: (choice) => {
2659
+ if (ledger.isClosed) return;
2660
+ appearance.setChoice(choice);
2661
+ menu.refresh();
2662
+ }
2663
+ } : void 0;
1455
2664
  const inspectorHandle = inspector && {
1456
2665
  element: inspector.element,
1457
2666
  isOpen: () => inspector.isOpen(),
@@ -1475,6 +2684,8 @@ function renderShell(root, options, themes, ledger) {
1475
2684
  tooltip: tooltipHandle,
1476
2685
  legend: legendHandle,
1477
2686
  inspector: inspectorHandle,
2687
+ primary: primaryHandle,
2688
+ appearance: appearanceHandle,
1478
2689
  setZoomTarget(target) {
1479
2690
  if (!ledger.isClosed) zoomPanel.setTarget(target);
1480
2691
  },
@@ -1503,8 +2714,15 @@ function renderShell(root, options, themes, ledger) {
1503
2714
  },
1504
2715
  setProblems(summary) {
1505
2716
  if (ledger.isClosed || !pill) return;
2717
+ const listHadFocus = popover?.element.contains(ownerDocument.activeElement) ?? false;
1506
2718
  pill.set(summary);
1507
2719
  setState("data-problems", pill.isShown() ? "shown" : null);
2720
+ popover?.setItems(summary.items ?? []);
2721
+ if (!pill.isShown() && popover?.isOpen()) {
2722
+ popover.close();
2723
+ if (listHadFocus) focusCanvasOrTrigger();
2724
+ }
2725
+ updateBottomRow();
1508
2726
  placeHint();
1509
2727
  },
1510
2728
  setStatus(next, statusOptions) {
@@ -1534,6 +2752,9 @@ function renderShell(root, options, themes, ledger) {
1534
2752
  applyReadOnly();
1535
2753
  },
1536
2754
  isReadOnly: () => readOnly,
2755
+ setEmptyState: setEmptyStateShown,
2756
+ isEmptyStateShown: () => emptyState?.isShown() ?? false,
2757
+ toast: showToast,
1537
2758
  destroy() {
1538
2759
  ledger.close();
1539
2760
  }
@@ -1573,4 +2794,4 @@ export {
1573
2794
  * minus, plus, triangle-alert (alert-triangle) and x. Feather @license MIT
1574
2795
  * Copyright (c) 2013-present Cole Bemis
1575
2796
  * Full notices: THIRD_PARTY_NOTICES.md of @miragon/modeler-shell */
1576
- //# sourceMappingURL=chunk-6LQOFACK.js.map
2797
+ //# sourceMappingURL=chunk-A4YGKH4Y.js.map