@miragon/modeler-shell 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) 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-66HCSJNY.js} +1247 -31
  6. package/dist/chunk-66HCSJNY.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 +1654 -197
  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 +1541 -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/types.ts +188 -6
  71. package/src/dom/web/drop-zone.ts +181 -0
  72. package/src/dom/web/empty-state.ts +144 -0
  73. package/src/dom/web/legal.ts +75 -0
  74. package/src/dom/web/menu-groups.ts +69 -0
  75. package/src/dom/web/primary.ts +124 -0
  76. package/src/dom/web/web.css +386 -0
  77. package/src/shell.css +3 -0
  78. package/src/testing/case-tools.ts +164 -0
  79. package/src/testing/cases.ts +209 -144
  80. package/src/testing/checks.ts +51 -15
  81. package/src/testing/facts.ts +56 -2
  82. package/src/testing/geometry.ts +206 -26
  83. package/src/testing/harness.ts +56 -7
  84. package/src/testing/index.ts +4 -0
  85. package/src/testing/page-tools.ts +24 -1
  86. package/src/testing/page.ts +10 -0
  87. package/src/testing/web-cases.ts +913 -0
  88. package/src/testing/web-checks.ts +1049 -0
  89. package/src/testing/web-facts.ts +713 -0
  90. package/src/vscode/webview/mount.ts +13 -1
  91. package/dist/chunk-5JSZ4VGA.js.map +0 -1
  92. package/dist/chunk-6LQOFACK.js.map +0 -1
  93. package/dist/keymap-CObQE6q6.d.cts +0 -151
  94. package/dist/keymap-DORzHnK-.d.ts +0 -151
  95. package/dist/types-B6NfkiUG.d.cts +0 -66
  96. package/dist/types-B6NfkiUG.d.ts +0 -66
  97. /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 {
@@ -1099,6 +1748,380 @@ function createTooltip({ root, layer, id }) {
1099
1748
  };
1100
1749
  }
1101
1750
 
1751
+ // src/dom/web/drop-zone.ts
1752
+ var DRAG_EVENTS = ["dragenter", "dragover", "dragleave", "drop"];
1753
+ function carriesFiles(transfer) {
1754
+ if (!transfer) return false;
1755
+ if (Array.from(transfer.types ?? []).includes("Files")) return true;
1756
+ return Array.from(transfer.items ?? []).some((item) => item.kind === "file");
1757
+ }
1758
+ function dragItems(transfer) {
1759
+ const items = Array.from(transfer.items ?? [], (item) => ({ kind: item.kind, type: item.type }));
1760
+ return items.length > 0 ? items : [{ kind: "file", type: "" }];
1761
+ }
1762
+ function acceptList(accept, lang) {
1763
+ const entries = accept.map((entry) => entry.trim()).filter((entry) => entry !== "");
1764
+ const extensions = entries.filter((entry) => entry.startsWith("."));
1765
+ const listed = extensions.length > 0 ? extensions : entries;
1766
+ let format;
1767
+ try {
1768
+ format = new Intl.ListFormat(lang, { type: "disjunction" });
1769
+ } catch {
1770
+ format = new Intl.ListFormat("en", { type: "disjunction" });
1771
+ }
1772
+ return format.format(listed);
1773
+ }
1774
+ function createDropZone(context) {
1775
+ const { ownerDocument, layer, messages, lang, options, modalOpen, toast, setState } = context;
1776
+ const chip = createElement(ownerDocument, "span", { class: "cd-shell-drop-zone-chip" });
1777
+ const element = createElement(
1778
+ ownerDocument,
1779
+ "div",
1780
+ { class: "cd-shell-drop-zone", "data-chrome": "drop-zone", "aria-hidden": "true" },
1781
+ chip
1782
+ );
1783
+ element.hidden = true;
1784
+ layer.append(element);
1785
+ const view = ownerDocument.defaultView;
1786
+ let depth = 0;
1787
+ let current = null;
1788
+ function verdictFor(items) {
1789
+ return dragVerdict(items, {
1790
+ accept: options.accept,
1791
+ multiple: options.multiple ?? false,
1792
+ dialogOpen: modalOpen()
1793
+ });
1794
+ }
1795
+ function show(verdict) {
1796
+ const next = verdict.state === "none" ? null : verdict.state;
1797
+ if (verdict.state === "over") chip.textContent = messages.dropFile;
1798
+ if (verdict.state === "forbidden") chip.textContent = messages.dropForbidden(verdict.reason);
1799
+ if (next === current) return;
1800
+ current = next;
1801
+ element.hidden = next === null;
1802
+ if (next === null) element.removeAttribute("data-state");
1803
+ else element.setAttribute("data-state", next);
1804
+ setState(next);
1805
+ }
1806
+ function track(transfer) {
1807
+ const verdict = verdictFor(dragItems(transfer));
1808
+ show(verdict);
1809
+ transfer.dropEffect = verdict.state === "forbidden" ? "none" : "copy";
1810
+ }
1811
+ function rejectByDefault(files) {
1812
+ const accepted = acceptList(options.accept, lang);
1813
+ for (const file of files) toast({ status: "danger", text: messages.cannotOpenFile(file.name, accepted) });
1814
+ }
1815
+ function receive(files, verdict) {
1816
+ if (verdict.state === "forbidden" && verdict.reason !== "type") return;
1817
+ const accepted = files.filter((file) => acceptsFile(file, options.accept));
1818
+ const rejected = files.filter((file) => !accepted.includes(file));
1819
+ if (accepted.length > 0) options.onDrop(accepted);
1820
+ if (rejected.length > 0) (options.onReject ?? rejectByDefault)(rejected);
1821
+ }
1822
+ const onDragEvent = (event) => {
1823
+ const transfer = event.dataTransfer;
1824
+ if (!carriesFiles(transfer)) return;
1825
+ event.preventDefault();
1826
+ switch (event.type) {
1827
+ case "dragenter":
1828
+ depth += 1;
1829
+ track(transfer);
1830
+ break;
1831
+ case "dragover":
1832
+ depth = Math.max(depth, 1);
1833
+ track(transfer);
1834
+ break;
1835
+ case "dragleave":
1836
+ depth = Math.max(0, depth - 1);
1837
+ if (depth === 0) show({ state: "none" });
1838
+ break;
1839
+ case "drop": {
1840
+ depth = 0;
1841
+ show({ state: "none" });
1842
+ const files = Array.from(transfer.files ?? []);
1843
+ const items = files.length > 0 ? files.map((file) => ({ kind: "file", type: file.type })) : dragItems(transfer);
1844
+ receive(files, verdictFor(items));
1845
+ break;
1846
+ }
1847
+ }
1848
+ };
1849
+ for (const type of DRAG_EVENTS) view?.addEventListener(type, onDragEvent);
1850
+ return {
1851
+ element,
1852
+ state: () => current,
1853
+ destroy() {
1854
+ for (const type of DRAG_EVENTS) view?.removeEventListener(type, onDragEvent);
1855
+ depth = 0;
1856
+ if (current !== null) {
1857
+ current = null;
1858
+ setState(null);
1859
+ }
1860
+ element.remove();
1861
+ }
1862
+ };
1863
+ }
1864
+
1865
+ // src/dom/web/empty-state.ts
1866
+ var KOMET_SIZE = "64";
1867
+ function createEmptyState(context) {
1868
+ const { ownerDocument, messages, options, dropHint, open, titleId, onLeave } = context;
1869
+ const button = (variant, label, run) => {
1870
+ const element2 = createElement(
1871
+ ownerDocument,
1872
+ "button",
1873
+ { type: "button", class: "cd-shell-button", "data-variant": variant },
1874
+ label
1875
+ );
1876
+ element2.addEventListener("click", () => {
1877
+ onLeave();
1878
+ run();
1879
+ });
1880
+ return element2;
1881
+ };
1882
+ const onShowExample = options.onShowExample;
1883
+ const newDiagram = button("primary", messages.newDiagram, () => options.onNewDiagram());
1884
+ const element = createElement(
1885
+ ownerDocument,
1886
+ "section",
1887
+ { class: "cd-shell-empty-state", "data-chrome": "empty-state", "aria-labelledby": titleId },
1888
+ // Dragging the mark would start an image drag that some browsers carry as a file.
1889
+ createElement(ownerDocument, "img", {
1890
+ class: "cd-shell-empty-state-mark",
1891
+ src: options.komet,
1892
+ width: KOMET_SIZE,
1893
+ height: KOMET_SIZE,
1894
+ alt: "",
1895
+ draggable: "false"
1896
+ }),
1897
+ createElement(ownerDocument, "h1", { class: "cd-shell-empty-state-title", id: titleId }, options.title),
1898
+ createElement(ownerDocument, "p", { class: "cd-shell-empty-state-text" }, options.text),
1899
+ createElement(
1900
+ ownerDocument,
1901
+ "div",
1902
+ { class: "cd-shell-empty-state-actions" },
1903
+ newDiagram,
1904
+ onShowExample && button("ghost", messages.showExample, () => onShowExample())
1905
+ ),
1906
+ hintParagraph()
1907
+ );
1908
+ function hintParagraph() {
1909
+ const hasDropHint = dropHint !== void 0 && dropHint !== "";
1910
+ if (!hasDropHint && !open) return void 0;
1911
+ const paragraph = createElement(ownerDocument, "p", { class: "cd-shell-empty-state-hint" });
1912
+ if (hasDropHint) paragraph.append(dropHint);
1913
+ if (!open) return paragraph;
1914
+ const shortcut = open.shortcut?.trim();
1915
+ const link = createElement(
1916
+ ownerDocument,
1917
+ "button",
1918
+ {
1919
+ type: "button",
1920
+ class: "cd-shell-empty-state-open",
1921
+ "aria-keyshortcuts": shortcut ? ariaKeyShortcutsFromLabel(shortcut) : void 0,
1922
+ "aria-haspopup": open.dialog ? "dialog" : void 0
1923
+ },
1924
+ messages.openFileLink
1925
+ );
1926
+ link.addEventListener("click", () => open.onSelect());
1927
+ if (hasDropHint) {
1928
+ paragraph.append(" ", createElement(ownerDocument, "span", { "aria-hidden": "true" }, "\xB7"), " ");
1929
+ }
1930
+ const openItem = createElement(ownerDocument, "span", { class: "cd-shell-empty-state-open-item" }, link);
1931
+ if (shortcut) {
1932
+ openItem.append(
1933
+ " ",
1934
+ createElement(
1935
+ ownerDocument,
1936
+ "kbd",
1937
+ { class: "cd-shell-empty-state-key", "aria-hidden": "true" },
1938
+ shortcut
1939
+ )
1940
+ );
1941
+ }
1942
+ paragraph.append(openItem);
1943
+ return paragraph;
1944
+ }
1945
+ element.hidden = true;
1946
+ return {
1947
+ element,
1948
+ isShown: () => !element.hidden,
1949
+ setShown(shown) {
1950
+ element.hidden = !shown;
1951
+ },
1952
+ focus() {
1953
+ newDiagram.focus({ preventScroll: true });
1954
+ },
1955
+ destroy() {
1956
+ element.remove();
1957
+ }
1958
+ };
1959
+ }
1960
+
1961
+ // src/dom/web/legal.ts
1962
+ var MIRAGON_HOME = "https://www.miragon.io/";
1963
+ var MIRAGON_IMPRESSUM = "https://www.miragon.io/impressum/";
1964
+ var MIRAGON_DATENSCHUTZ = "https://www.miragon.io/datenschutz/";
1965
+ function createLegalLine(context) {
1966
+ const { ownerDocument, messages, options } = context;
1967
+ const year = options.year ?? (/* @__PURE__ */ new Date()).getFullYear();
1968
+ const newTabId = `${context.idPrefix}-new-tab`;
1969
+ const link = (label, href, lang) => createElement(
1970
+ ownerDocument,
1971
+ "a",
1972
+ { href, lang, target: "_blank", rel: "noopener noreferrer", "aria-describedby": newTabId },
1973
+ label
1974
+ );
1975
+ const separator = () => createElement(ownerDocument, "span", { "aria-hidden": "true" }, "\xB7");
1976
+ const item = (anchor) => createElement(ownerDocument, "span", { class: "cd-shell-legal-item" }, separator(), anchor);
1977
+ const links = [
1978
+ link(messages.copyright(year), MIRAGON_HOME),
1979
+ link("Impressum", MIRAGON_IMPRESSUM, "de"),
1980
+ link("Datenschutz", MIRAGON_DATENSCHUTZ, "de"),
1981
+ ...(options.links ?? []).map((extra) => link(extra.label, extra.href, extra.lang))
1982
+ ];
1983
+ const element = createElement(ownerDocument, "nav", {
1984
+ class: "cd-shell-legal",
1985
+ "data-chrome": "legal",
1986
+ "aria-label": messages.legal
1987
+ });
1988
+ element.append(
1989
+ ...links.map((anchor, index) => index === 0 ? anchor : item(anchor)),
1990
+ createElement(ownerDocument, "span", { id: newTabId, hidden: "" }, messages.opensInNewTab)
1991
+ );
1992
+ return {
1993
+ element,
1994
+ destroy() {
1995
+ element.remove();
1996
+ }
1997
+ };
1998
+ }
1999
+
2000
+ // src/dom/web/menu-groups.ts
2001
+ function fileEntry(spec, action) {
2002
+ const common = {
2003
+ id: spec.id,
2004
+ label: spec.label,
2005
+ ...spec.icon ? { icon: spec.icon } : {},
2006
+ ...action.disabled ? { disabled: action.disabled } : {},
2007
+ ...action.shortcut ? { shortcut: action.shortcut } : {},
2008
+ onSelect: () => action.onSelect()
2009
+ };
2010
+ return action.dialog ? { kind: "dialog", ...common } : { kind: "action", ...common };
2011
+ }
2012
+ function fileMenuEntries(options, messages) {
2013
+ const specs = [
2014
+ { option: "newDiagram", id: "new-diagram", label: messages.newDiagram, icon: "file-plus" },
2015
+ { option: "open", id: "open", label: messages.openFile, icon: "folder-open" },
2016
+ // §5.14 gives "Show example" no icon.
2017
+ { option: "showExample", id: "show-example", label: messages.showExample, icon: void 0 },
2018
+ { option: "import", id: "import", label: messages.importFile, icon: "import" }
2019
+ ];
2020
+ return specs.flatMap((spec) => {
2021
+ const action = options[spec.option];
2022
+ return action ? [fileEntry(spec, action)] : [];
2023
+ });
2024
+ }
2025
+ function appearanceMenuEntries(appearance, messages) {
2026
+ return [
2027
+ {
2028
+ kind: "radio",
2029
+ id: "appearance",
2030
+ label: messages.appearance,
2031
+ options: [
2032
+ { value: "system", label: messages.appearanceSystem },
2033
+ { value: "light", label: messages.appearanceLight },
2034
+ { value: "dark", label: messages.appearanceDark }
2035
+ ],
2036
+ value: () => appearance.choice(),
2037
+ onSelect: (value) => appearance.setChoice(parseAppearanceChoice(value))
2038
+ }
2039
+ ];
2040
+ }
2041
+
2042
+ // src/dom/web/primary.ts
2043
+ var COLLAPSED_ATTRIBUTE = "data-collapsed";
2044
+ function createPrimaryAction(context) {
2045
+ const { ownerDocument, messages, tooltip, options } = context;
2046
+ const label = options.label ?? messages.share;
2047
+ const labelElement = createElement(ownerDocument, "span", { class: "cd-shell-primary-label" }, label);
2048
+ const element = createElement(
2049
+ ownerDocument,
2050
+ "button",
2051
+ {
2052
+ type: "button",
2053
+ class: "cd-shell-primary cd-shell-button",
2054
+ "data-variant": "primary",
2055
+ "data-chrome": "primary",
2056
+ "aria-label": label
2057
+ },
2058
+ iconElement(options.icon ?? "share-2"),
2059
+ labelElement
2060
+ );
2061
+ let collapsed = false;
2062
+ let disabled = false;
2063
+ let labelledWidth;
2064
+ function readDisabled() {
2065
+ disabled = options.disabled?.() ?? false;
2066
+ toggleAttribute(element, "aria-disabled", disabled ? "true" : null);
2067
+ }
2068
+ function showCollapsed(state) {
2069
+ labelElement.hidden = state;
2070
+ toggleAttribute(element, COLLAPSED_ATTRIBUTE, state ? "" : null);
2071
+ }
2072
+ function measureLabelledWidth() {
2073
+ if (!collapsed) return element.offsetWidth;
2074
+ showCollapsed(false);
2075
+ const width = element.offsetWidth;
2076
+ showCollapsed(true);
2077
+ return width;
2078
+ }
2079
+ function isEngaged() {
2080
+ if (ownerDocument.activeElement === element) return true;
2081
+ try {
2082
+ return element.matches(":hover");
2083
+ } catch {
2084
+ return false;
2085
+ }
2086
+ }
2087
+ const onClick = (event) => {
2088
+ if (disabled) {
2089
+ event.preventDefault();
2090
+ return;
2091
+ }
2092
+ options.onSelect();
2093
+ };
2094
+ element.addEventListener("click", onClick);
2095
+ const detachTooltip = tooltip.attach(element, () => collapsed ? { label } : null);
2096
+ readDisabled();
2097
+ return {
2098
+ element,
2099
+ isCollapsed: () => collapsed,
2100
+ refresh() {
2101
+ readDisabled();
2102
+ labelledWidth = void 0;
2103
+ },
2104
+ widths() {
2105
+ if (!collapsed || labelledWidth === void 0) {
2106
+ const measured = measureLabelledWidth();
2107
+ if (measured > 0) labelledWidth = measured;
2108
+ }
2109
+ return { withLabel: labelledWidth ?? 0, iconOnly: SHELL.row };
2110
+ },
2111
+ setCollapsed(next) {
2112
+ if (next === collapsed) return;
2113
+ collapsed = next;
2114
+ showCollapsed(next);
2115
+ if (!next && isEngaged()) tooltip.hide();
2116
+ },
2117
+ destroy() {
2118
+ detachTooltip();
2119
+ element.removeEventListener("click", onClick);
2120
+ element.remove();
2121
+ }
2122
+ };
2123
+ }
2124
+
1102
2125
  // src/dom/zoom-panel.ts
1103
2126
  function createZoomPanel(context) {
1104
2127
  const { ownerDocument, messages, lang, platform, tooltip } = context;
@@ -1216,6 +2239,16 @@ function nextMountIndex() {
1216
2239
  return index;
1217
2240
  }
1218
2241
  var DEFAULT_THEMES = ["light"];
2242
+ var LEGAL_HEIGHT_PROPERTY = "--modeler-shell-legal-height";
2243
+ var INERT_TOAST = Object.freeze({ dismiss() {
2244
+ } });
2245
+ function visibleBoxIn(element, root) {
2246
+ if (!element || element.hidden || !element.isConnected) return void 0;
2247
+ if (typeof element.checkVisibility === "function" && !element.checkVisibility({ visibilityProperty: true }))
2248
+ return void 0;
2249
+ const box = boxRelativeTo(element, root);
2250
+ return box.width > 0 && box.height > 0 ? box : void 0;
2251
+ }
1219
2252
  function languageOf(root, host, requested) {
1220
2253
  if (requested) return requested;
1221
2254
  const declared = host === "embedded" ? root.getAttribute("lang") : root.closest("[lang]")?.getAttribute("lang");
@@ -1244,6 +2277,7 @@ function renderShell(root, options, themes, ledger) {
1244
2277
  const platform = options.platform ?? detectPlatform(globalThis.navigator);
1245
2278
  const lang = languageOf(root, host, options.lang);
1246
2279
  const keyScope = options.keyScope ?? (host === "embedded" ? "root" : "document");
2280
+ const web = host === "web";
1247
2281
  ledger.addClass(root, "cd-shell");
1248
2282
  ledger.attribute(root, "data-cd-shell-host", host);
1249
2283
  ledger.attribute(root, "data-palette", "top");
@@ -1256,6 +2290,9 @@ function renderShell(root, options, themes, ledger) {
1256
2290
  const setState = (name, value) => {
1257
2291
  if (!ledger.isClosed) ledger.attribute(root, name, value);
1258
2292
  };
2293
+ const appearance = web ? connectWebAppearance({ ownerDocument, themes, options: options.appearance }) : void 0;
2294
+ if (appearance) ledger.defer(() => appearance.destroy());
2295
+ const darkCapable = themes.includes("dark");
1259
2296
  const layer = createElement(ownerDocument, "div", { class: "cd-shell-layer" });
1260
2297
  ledger.defer(() => layer.remove());
1261
2298
  const tooltip = createTooltip({ root, layer, id: `cd-shell-tooltip-${index}` });
@@ -1287,6 +2324,14 @@ function renderShell(root, options, themes, ledger) {
1287
2324
  keepEnabledWhenReadOnly: true
1288
2325
  } : void 0;
1289
2326
  const shellModelEntries = legendEntry ? [legendEntry] : [];
2327
+ const fileEntries = web && options.file ? fileMenuEntries(options.file, messages) : [];
2328
+ const appearanceEntries = appearance && darkCapable ? appearanceMenuEntries(appearance, messages) : [];
2329
+ const shellEntries = (group) => {
2330
+ if (group === "file") return fileEntries;
2331
+ if (group === "model") return shellModelEntries;
2332
+ if (group === "appearance") return appearanceEntries;
2333
+ return [];
2334
+ };
1290
2335
  const menu = createMenu({
1291
2336
  root,
1292
2337
  layer,
@@ -1294,55 +2339,133 @@ function renderShell(root, options, themes, ledger) {
1294
2339
  messages,
1295
2340
  tooltip,
1296
2341
  groups: options.menu ?? {},
1297
- shellModelEntries: () => shellModelEntries,
2342
+ shellEntries,
1298
2343
  isReadOnly: () => readOnly
1299
2344
  });
1300
2345
  ledger.defer(() => menu.destroy());
1301
2346
  ledger.defer(tooltip.attach(menu.trigger, () => menu.isOpen() ? null : { label: messages.menu }));
2347
+ const primary = web && options.primary ? createPrimaryAction({ ownerDocument, messages, tooltip, options: options.primary }) : void 0;
2348
+ if (primary) ledger.defer(() => primary.destroy());
1302
2349
  const canvas = options.canvas ?? createElement(ownerDocument, "div");
1303
2350
  ledger.addClass(canvas, "cd-shell-canvas");
2351
+ if (web && !canvas.hasAttribute("role")) ledger.attribute(canvas, "role", "main");
1304
2352
  if (!options.canvas) ledger.defer(() => canvas.remove());
2353
+ function focusCanvasOrTrigger() {
2354
+ const took = (target) => {
2355
+ target?.focus({ preventScroll: true });
2356
+ return target !== null && target !== void 0 && ownerDocument.activeElement === target;
2357
+ };
2358
+ if (emptyState?.isShown()) {
2359
+ emptyState.focus();
2360
+ if (emptyState.element.contains(ownerDocument.activeElement)) return;
2361
+ }
2362
+ if (took(canvas.querySelector("svg[tabindex]"))) return;
2363
+ if (!menu.trigger.hidden) took(menu.trigger);
2364
+ }
1305
2365
  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 });
2366
+ if (island.contains(ownerDocument.activeElement)) focusCanvasOrTrigger();
1311
2367
  }
1312
2368
  const status = createStatusBox(ownerDocument);
1313
2369
  ledger.defer(() => status.destroy());
1314
2370
  const hint = options.emptyHint ? createEmptyHint(ownerDocument, messages, options.emptyHint) : void 0;
1315
2371
  if (hint) ledger.defer(() => hint.destroy());
2372
+ const emptyState = web && options.emptyState ? createEmptyState({
2373
+ ownerDocument,
2374
+ messages,
2375
+ options: options.emptyState,
2376
+ dropHint: options.dropZone ? options.emptyState.dropHint ?? messages.dropHint : void 0,
2377
+ open: options.file?.open,
2378
+ titleId: `cd-shell-empty-state-title-${index}`,
2379
+ onLeave: () => setEmptyStateShown(false)
2380
+ }) : void 0;
2381
+ if (emptyState) ledger.defer(() => emptyState.destroy());
1316
2382
  const inspector = options.inspector ? createInspector(ownerDocument, options.inspector, (open) => {
1317
2383
  setState("data-inspector", open ? "open" : null);
2384
+ updateBottomRow();
1318
2385
  placeHint();
1319
2386
  }) : void 0;
1320
2387
  if (inspector) ledger.defer(() => inspector.destroy());
1321
- const pill = options.problems ? createProblemsPill({
2388
+ const problemsOptions = options.problems;
2389
+ let popover;
2390
+ const pill = problemsOptions ? createProblemsPill({
1322
2391
  ownerDocument,
1323
2392
  messages,
1324
2393
  lang,
1325
2394
  tooltip,
1326
2395
  showTooltip: host === "ide",
1327
- options: options.problems
2396
+ onActivate() {
2397
+ if (popover) popover.toggle();
2398
+ else problemsOptions.onActivate?.();
2399
+ }
1328
2400
  }) : void 0;
1329
2401
  if (pill) ledger.defer(() => pill.destroy());
2402
+ if (pill && (web || host === "embedded" && !problemsOptions?.onActivate)) {
2403
+ const created = createProblemsPopover({
2404
+ ownerDocument,
2405
+ root,
2406
+ layer,
2407
+ pill: pill.element,
2408
+ id: `cd-shell-problems-${index}`,
2409
+ titleId: `cd-shell-problems-title-${index}`,
2410
+ messages,
2411
+ onSelectItem: problemsOptions?.onSelectItem
2412
+ });
2413
+ popover = created;
2414
+ ledger.defer(() => created.destroy());
2415
+ }
2416
+ const legal = web ? createLegalLine({
2417
+ ownerDocument,
2418
+ messages,
2419
+ options: options.legal ?? {},
2420
+ idPrefix: `cd-shell-legal-${index}`
2421
+ }) : void 0;
2422
+ if (legal) ledger.defer(() => legal.destroy());
1330
2423
  const zoomPanel = createZoomPanel({ ownerDocument, messages, lang, platform, tooltip });
1331
2424
  ledger.defer(() => zoomPanel.destroy());
1332
- root.prepend(menu.trigger, canvas);
2425
+ const toaster = host === "ide" ? void 0 : createToaster({
2426
+ ownerDocument,
2427
+ host,
2428
+ messages,
2429
+ idPrefix: `cd-shell-toast-${index}`,
2430
+ focusFallback: focusCanvasOrTrigger,
2431
+ onChange: () => updateBottomRow()
2432
+ });
2433
+ if (toaster) ledger.defer(() => toaster.destroy());
2434
+ const defined = (element) => element !== void 0;
2435
+ root.prepend(...[menu.trigger, primary?.element, canvas].filter(defined));
1333
2436
  root.append(
1334
2437
  ...[
1335
2438
  status.element,
1336
2439
  hint?.element,
2440
+ emptyState?.element,
1337
2441
  inspector?.element,
1338
2442
  pill?.element,
1339
2443
  legend?.element,
2444
+ legal?.element,
1340
2445
  zoomPanel.element
1341
- ].filter((element) => element !== void 0),
1342
- layer
2446
+ ].filter(defined),
2447
+ layer,
2448
+ ...[toaster?.announcer, toaster?.element].filter(defined)
1343
2449
  );
2450
+ function showToast(request) {
2451
+ return ledger.isClosed || !toaster ? INERT_TOAST : toaster.show(request);
2452
+ }
2453
+ const dropZone = web && options.dropZone ? createDropZone({
2454
+ ownerDocument,
2455
+ layer,
2456
+ messages,
2457
+ lang,
2458
+ options: options.dropZone,
2459
+ modalOpen: () => isModalOpen(ownerDocument),
2460
+ toast: showToast,
2461
+ setState: (state) => setState("data-drop", state)
2462
+ }) : void 0;
2463
+ if (dropZone) ledger.defer(() => dropZone.destroy());
1344
2464
  let palette;
1345
2465
  let paletteRow = "top";
2466
+ let zoomRaised = false;
2467
+ let legalHeight = LEGAL_LINE_HEIGHT;
2468
+ if (legal) ledger.defer(() => root.style.removeProperty(LEGAL_HEIGHT_PROPERTY));
1346
2469
  function containerSize() {
1347
2470
  return { width: root.clientWidth, height: root.clientHeight };
1348
2471
  }
@@ -1362,6 +2485,10 @@ function renderShell(root, options, themes, ledger) {
1362
2485
  paletteRow,
1363
2486
  problemsVisible: pill?.isShown() ?? false
1364
2487
  };
2488
+ if (web) {
2489
+ layout.zoomRaised = zoomRaised;
2490
+ layout.legalHeight = legalHeight;
2491
+ }
1365
2492
  if (legend?.isOpen()) layout.legendWidth = legend.element.getBoundingClientRect().width;
1366
2493
  if (inspector?.isOpen()) {
1367
2494
  layout.inspector = container.width > SHELL.narrow ? {} : { sheetTop: boxRelativeTo(inspector.element, root).y };
@@ -1375,18 +2502,76 @@ function renderShell(root, options, themes, ledger) {
1375
2502
  if (ledger.isClosed) return;
1376
2503
  const size = containerSize();
1377
2504
  if (size.width === 0 || size.height === 0) return;
1378
- paletteRow = decidePaletteRow({
2505
+ const topRow = decideTopRow({
1379
2506
  containerWidth: size.width,
1380
2507
  paletteWidth: paletteWidth(),
1381
- menuRight: menuRight()
2508
+ menuRight: menuRight(),
2509
+ primary: primary?.widths()
1382
2510
  });
2511
+ paletteRow = topRow.paletteRow;
1383
2512
  setState("data-palette", paletteRow);
2513
+ if (primary) {
2514
+ const collapsed = topRow.primaryLabel === "collapsed";
2515
+ primary.setCollapsed(collapsed);
2516
+ setState("data-primary", collapsed ? "collapsed" : null);
2517
+ }
1384
2518
  if (legend && !legend.isStarted()) legend.start(options.legend?.open ?? legendStartsOpen(size.height));
2519
+ updateBottomRow();
1385
2520
  placeHint();
1386
2521
  }
2522
+ function updateBottomRow() {
2523
+ if (ledger.isClosed) return;
2524
+ const size = containerSize();
2525
+ if (size.width === 0 || size.height === 0) return;
2526
+ if (legal) {
2527
+ const legalBox = visibleBoxIn(legal.element, root);
2528
+ const height = legalLineHeight(legalBox?.height);
2529
+ if (height !== legalHeight || !root.style.getPropertyValue(LEGAL_HEIGHT_PROPERTY)) {
2530
+ legalHeight = height;
2531
+ root.style.setProperty(LEGAL_HEIGHT_PROPERTY, `${height}px`);
2532
+ }
2533
+ zoomRaised = zoomRowRaised({
2534
+ containerWidth: size.width,
2535
+ legalRight: legalBox && legalBox.x + legalBox.width,
2536
+ zoomWidth: visibleBoxIn(zoomPanel.element, root)?.width
2537
+ });
2538
+ setState("data-zoom-row", zoomRaised ? "raised" : null);
2539
+ }
2540
+ if (toaster) {
2541
+ const obstacles = [zoomPanel.element, legal?.element, pill?.element].flatMap(
2542
+ (element) => {
2543
+ const box = visibleBoxIn(element, root);
2544
+ return box ? [{ left: box.x, right: box.x + box.width, top: size.height - box.y }] : [];
2545
+ }
2546
+ );
2547
+ toaster.setBottom(
2548
+ toastStackBottom({ containerWidth: size.width, obstacles, cardWidth: toaster.widestCard() })
2549
+ );
2550
+ }
2551
+ }
2552
+ function setEmptyStateShown(shown) {
2553
+ if (ledger.isClosed || !emptyState || emptyState.isShown() === shown) return;
2554
+ const active = ownerDocument.activeElement;
2555
+ const hadFocus = emptyState.element.contains(active);
2556
+ const workHadFocus = shown && active !== null && root.contains(active) && ![emptyState.element, legal?.element, toaster?.element].some((kept) => kept?.contains(active));
2557
+ emptyState.setShown(shown);
2558
+ setState("data-empty-state", shown ? "shown" : null);
2559
+ ledger.attribute(canvas, "inert", shown ? "" : null);
2560
+ if (shown) {
2561
+ menu.close();
2562
+ popover?.close();
2563
+ tooltip.hide();
2564
+ if (workHadFocus) emptyState.focus();
2565
+ } else if (hadFocus) {
2566
+ focusCanvasOrTrigger();
2567
+ }
2568
+ updateLayout();
2569
+ }
1387
2570
  const ResizeObserverClass = globalThis.ResizeObserver;
1388
2571
  const resizeObserver = ResizeObserverClass ? new ResizeObserverClass(() => updateLayout()) : void 0;
1389
2572
  resizeObserver?.observe(root);
2573
+ if (primary) resizeObserver?.observe(primary.element);
2574
+ if (legal) resizeObserver?.observe(legal.element);
1390
2575
  ledger.defer(() => resizeObserver?.disconnect());
1391
2576
  if (legend && options.legend?.open !== void 0) legend.start(options.legend.open);
1392
2577
  updateLayout();
@@ -1395,12 +2580,16 @@ function renderShell(root, options, themes, ledger) {
1395
2580
  host,
1396
2581
  dialogScope: keyTarget,
1397
2582
  menuOpen: () => menu.isOpen(),
2583
+ startScreen: () => emptyState?.isShown() ?? false,
1398
2584
  closeMenu() {
1399
2585
  const active = ownerDocument.activeElement;
1400
2586
  menu.close({ restoreFocus: active === menu.trigger || menu.dropdown.contains(active) });
1401
2587
  },
1402
2588
  hideTooltip: () => tooltip.hide(),
1403
- run: (key) => zoomPanel.run(key)
2589
+ // Behind the start screen the keys reach the browser instead: there is no diagram to zoom.
2590
+ run: (key) => {
2591
+ if (!emptyState?.isShown()) zoomPanel.run(key);
2592
+ }
1404
2593
  });
1405
2594
  keyTarget.addEventListener("keydown", onKeydown);
1406
2595
  ledger.defer(() => keyTarget.removeEventListener("keydown", onKeydown));
@@ -1452,6 +2641,21 @@ function renderShell(root, options, themes, ledger) {
1452
2641
  if (!ledger.isClosed) legend.toggle();
1453
2642
  }
1454
2643
  };
2644
+ const primaryHandle = primary && {
2645
+ element: primary.element,
2646
+ isCollapsed: () => primary.isCollapsed(),
2647
+ refresh: () => {
2648
+ if (!ledger.isClosed) primary.refresh();
2649
+ }
2650
+ };
2651
+ const appearanceHandle = appearance && darkCapable ? {
2652
+ choice: () => appearance.choice(),
2653
+ setChoice: (choice) => {
2654
+ if (ledger.isClosed) return;
2655
+ appearance.setChoice(choice);
2656
+ menu.refresh();
2657
+ }
2658
+ } : void 0;
1455
2659
  const inspectorHandle = inspector && {
1456
2660
  element: inspector.element,
1457
2661
  isOpen: () => inspector.isOpen(),
@@ -1475,6 +2679,8 @@ function renderShell(root, options, themes, ledger) {
1475
2679
  tooltip: tooltipHandle,
1476
2680
  legend: legendHandle,
1477
2681
  inspector: inspectorHandle,
2682
+ primary: primaryHandle,
2683
+ appearance: appearanceHandle,
1478
2684
  setZoomTarget(target) {
1479
2685
  if (!ledger.isClosed) zoomPanel.setTarget(target);
1480
2686
  },
@@ -1503,8 +2709,15 @@ function renderShell(root, options, themes, ledger) {
1503
2709
  },
1504
2710
  setProblems(summary) {
1505
2711
  if (ledger.isClosed || !pill) return;
2712
+ const listHadFocus = popover?.element.contains(ownerDocument.activeElement) ?? false;
1506
2713
  pill.set(summary);
1507
2714
  setState("data-problems", pill.isShown() ? "shown" : null);
2715
+ popover?.setItems(summary.items ?? []);
2716
+ if (!pill.isShown() && popover?.isOpen()) {
2717
+ popover.close();
2718
+ if (listHadFocus) focusCanvasOrTrigger();
2719
+ }
2720
+ updateBottomRow();
1508
2721
  placeHint();
1509
2722
  },
1510
2723
  setStatus(next, statusOptions) {
@@ -1534,6 +2747,9 @@ function renderShell(root, options, themes, ledger) {
1534
2747
  applyReadOnly();
1535
2748
  },
1536
2749
  isReadOnly: () => readOnly,
2750
+ setEmptyState: setEmptyStateShown,
2751
+ isEmptyStateShown: () => emptyState?.isShown() ?? false,
2752
+ toast: showToast,
1537
2753
  destroy() {
1538
2754
  ledger.close();
1539
2755
  }
@@ -1573,4 +2789,4 @@ export {
1573
2789
  * minus, plus, triangle-alert (alert-triangle) and x. Feather @license MIT
1574
2790
  * Copyright (c) 2013-present Cole Bemis
1575
2791
  * Full notices: THIRD_PARTY_NOTICES.md of @miragon/modeler-shell */
1576
- //# sourceMappingURL=chunk-6LQOFACK.js.map
2792
+ //# sourceMappingURL=chunk-66HCSJNY.js.map