@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
@@ -60,13 +60,28 @@ var SHELL = {
60
60
  layerToast: 60
61
61
  };
62
62
 
63
+ // src/core/web-chrome/bottom-row.ts
64
+ var LEGAL_LINE_HEIGHT = 20;
65
+ function legalLineHeight(measured) {
66
+ if (measured === void 0 || !Number.isFinite(measured) || measured <= 0) return LEGAL_LINE_HEIGHT;
67
+ return Math.max(1, Math.round(measured / LEGAL_LINE_HEIGHT)) * LEGAL_LINE_HEIGHT;
68
+ }
69
+ function zoomRowRaised(input) {
70
+ const { containerWidth, legalRight, zoomWidth } = input;
71
+ if (legalRight === void 0 || zoomWidth === void 0) return false;
72
+ const zoomLeft = containerWidth - SHELL.edge - zoomWidth;
73
+ return legalRight + SHELL.gap > zoomLeft;
74
+ }
75
+
63
76
  // src/core/fit.ts
64
77
  function fitInsets(layout) {
65
- const { container, host, paletteRow, problemsVisible, legendWidth, inspector } = layout;
78
+ const { container, host, paletteRow, problemsVisible, zoomRaised, legendWidth, inspector } = layout;
79
+ const raisedBottomRow = host === "web" && (problemsVisible || zoomRaised === true);
80
+ const wrappedLegalLines = legalLineHeight(layout.legalHeight) - LEGAL_LINE_HEIGHT;
66
81
  const insets = {
67
82
  top: paletteRow === "second-row" ? SHELL.fitInsetTopSecondRow : SHELL.fitInsetTop,
68
83
  right: SHELL.fitInsetSide,
69
- bottom: host === "web" && problemsVisible ? SHELL.fitInsetBottomWeb : SHELL.fitInsetBottom,
84
+ bottom: raisedBottomRow ? SHELL.fitInsetBottomWeb + wrappedLegalLines : SHELL.fitInsetBottom,
70
85
  left: SHELL.fitInsetSide
71
86
  };
72
87
  const bottomSheetOpen = inspector !== void 0 && container.width <= SHELL.narrow;
@@ -201,6 +216,7 @@ function decideKey(event, context) {
201
216
  }
202
217
  const chromeKey = matchChromeKey(event);
203
218
  if (chromeKey !== void 0) {
219
+ if (context.startScreen === true && !stop) return { preventDefault: false, stopPropagation: false };
204
220
  const blocked = composing || context.inTextField || context.modalOpen;
205
221
  if (!blocked) return { action: chromeKey, preventDefault: true, stopPropagation: stop };
206
222
  return { preventDefault: false, stopPropagation: chromeKey === "fit" ? false : stop };
@@ -371,7 +387,27 @@ var defaultMessages = {
371
387
  emptyHint: "Pick a tool from the palette to start.",
372
388
  insertExample: "Insert example",
373
389
  invalidFile: (diagram) => `This file isn't a valid ${diagram} right now. The diagram shows the last valid version and is read-only until you fix the text.`,
374
- keyNames: { ctrl: "Ctrl", shift: "Shift", plus: "Plus", minus: "Minus" }
390
+ keyNames: { ctrl: "Ctrl", shift: "Shift", plus: "Plus", minus: "Minus" },
391
+ share: "Share",
392
+ legal: "Legal",
393
+ copyright: (year) => `\xA9 ${year} Miragon GmbH`,
394
+ opensInNewTab: "Opens in a new tab",
395
+ newDiagram: "New diagram",
396
+ openFile: "Open\u2026",
397
+ showExample: "Show example",
398
+ importFile: "Import\u2026",
399
+ appearance: "Appearance",
400
+ appearanceSystem: "System",
401
+ appearanceLight: "Light",
402
+ appearanceDark: "Dark",
403
+ problemsTitle: "Problems",
404
+ problemSeverity: (severity) => severity === "error" ? "Error" : "Warning",
405
+ close: "Close",
406
+ dropFile: "Drop file here",
407
+ dropForbidden: (reason) => reason === "multiple" ? "Drop one file at a time" : reason === "dialog" ? "Close the dialog to drop a file" : "This modeler can't open this file type",
408
+ dropHint: "Or drop a file here",
409
+ openFileLink: "Open a file\u2026",
410
+ cannotOpenFile: (name, accepted) => `Couldn't open \u201C${name}\u201D. Drop a ${accepted} file.`
375
411
  };
376
412
  function resolveMessages(overrides) {
377
413
  const { keyNames, ...texts } = overrides ?? {};
@@ -410,6 +446,121 @@ function placeTooltip(input) {
410
446
  return { x, y };
411
447
  }
412
448
 
449
+ // src/core/web-chrome/top-row.ts
450
+ function decideTopRow(input) {
451
+ const { containerWidth, paletteWidth, menuRight, primary } = input;
452
+ if (!primary) {
453
+ return {
454
+ paletteRow: decidePaletteRow({ containerWidth, paletteWidth, menuRight }),
455
+ primaryLabel: "shown"
456
+ };
457
+ }
458
+ const paletteRowBeside = (primaryWidth) => decidePaletteRow({
459
+ containerWidth,
460
+ paletteWidth,
461
+ menuRight,
462
+ primaryLeft: containerWidth - SHELL.edge - primaryWidth
463
+ });
464
+ if (paletteRowBeside(primary.withLabel) === "top") return { paletteRow: "top", primaryLabel: "shown" };
465
+ if (paletteRowBeside(primary.iconOnly) === "top") return { paletteRow: "top", primaryLabel: "collapsed" };
466
+ const labelledLeft = containerWidth - SHELL.edge - primary.withLabel;
467
+ return {
468
+ paletteRow: "second-row",
469
+ primaryLabel: menuRight + PALETTE_CLEARANCE <= labelledLeft ? "shown" : "collapsed"
470
+ };
471
+ }
472
+
473
+ // src/core/web-chrome/drop.ts
474
+ function acceptEntries(accept) {
475
+ const entries = accept.map((entry) => entry.trim().toLowerCase()).filter((entry) => entry !== "");
476
+ return {
477
+ extensions: entries.filter((entry) => entry.startsWith(".")),
478
+ mimeTypes: entries.filter((entry) => !entry.startsWith(".") && entry.includes("/"))
479
+ };
480
+ }
481
+ function matchesMimeType(type, mimeTypes) {
482
+ const normalized = type.trim().toLowerCase();
483
+ if (normalized === "") return false;
484
+ return mimeTypes.some(
485
+ (entry) => entry.endsWith("/*") ? normalized.startsWith(entry.slice(0, -1)) : normalized === entry
486
+ );
487
+ }
488
+ function dragVerdict(items, context) {
489
+ const files = items.filter((item) => item.kind === "file");
490
+ if (files.length === 0) return { state: "none" };
491
+ if (context.dialogOpen) return { state: "forbidden", reason: "dialog" };
492
+ if (files.length > 1 && !context.multiple) return { state: "forbidden", reason: "multiple" };
493
+ const { mimeTypes } = acceptEntries(context.accept);
494
+ const foreignType = (item) => item.type.trim() !== "" && !matchesMimeType(item.type, mimeTypes);
495
+ if (mimeTypes.length > 0 && files.every(foreignType)) return { state: "forbidden", reason: "type" };
496
+ return { state: "over" };
497
+ }
498
+ function acceptsFile(file, accept) {
499
+ const { extensions, mimeTypes } = acceptEntries(accept);
500
+ if (extensions.length === 0 && mimeTypes.length === 0) return true;
501
+ const name = file.name.toLowerCase();
502
+ return extensions.some((extension) => name.endsWith(extension)) || matchesMimeType(file.type, mimeTypes);
503
+ }
504
+
505
+ // src/core/feedback/toasts.ts
506
+ var MAX_TOASTS = 3;
507
+ var TOAST_DURATION_MS = {
508
+ success: 2800,
509
+ info: 2800,
510
+ warning: 5e3,
511
+ danger: 5e3
512
+ };
513
+ var TOAST_MAX_WIDTH = 480;
514
+ function toastLifetime(request) {
515
+ if (request.action) return void 0;
516
+ const requested = request.durationMs;
517
+ const longer = requested !== void 0 && Number.isFinite(requested) ? requested : 0;
518
+ return Math.max(TOAST_DURATION_MS[request.status], longer);
519
+ }
520
+ function toastToEvict(stack) {
521
+ if (stack.length < MAX_TOASTS) return void 0;
522
+ const evicted = stack.find((slot) => !slot.engaged && !slot.hasAction) ?? stack.find((slot) => !slot.engaged) ?? stack[0];
523
+ return evicted?.id;
524
+ }
525
+ function toastStackWidth(containerWidth) {
526
+ return Math.max(0, Math.min(TOAST_MAX_WIDTH, containerWidth - 2 * SHELL.edge));
527
+ }
528
+ function toastStackBottom(input) {
529
+ const stackWidth = toastStackWidth(input.containerWidth);
530
+ const { cardWidth } = input;
531
+ const width = cardWidth !== void 0 && Number.isFinite(cardWidth) ? Math.min(stackWidth, Math.max(0, cardWidth)) : stackWidth;
532
+ const left = (input.containerWidth - width) / 2;
533
+ const right = left + width;
534
+ let highest;
535
+ for (const obstacle of input.obstacles) {
536
+ const distance = Math.max(obstacle.left - right, left - obstacle.right);
537
+ if (distance >= SHELL.gap) continue;
538
+ highest = highest === void 0 ? obstacle.top : Math.max(highest, obstacle.top);
539
+ }
540
+ return highest === void 0 ? SHELL.edge : Math.max(SHELL.edge, highest + SHELL.gap);
541
+ }
542
+
543
+ // src/core/feedback/appearance.ts
544
+ var APPEARANCE_STORAGE_KEY = "miragon.darstellung";
545
+ function parseAppearanceChoice(stored) {
546
+ return stored === "light" || stored === "dark" ? stored : "system";
547
+ }
548
+ function documentMode(choice, themes, colors) {
549
+ if (!themes.includes("dark")) return fixedMode("light", colors.light);
550
+ const parsed = parseAppearanceChoice(choice);
551
+ if (parsed === "system") {
552
+ return {
553
+ theme: "system",
554
+ colorScheme: "light dark",
555
+ themeColor: { light: colors.light, dark: colors.dark }
556
+ };
557
+ }
558
+ return fixedMode(parsed, colors[parsed]);
559
+ }
560
+ function fixedMode(theme, color) {
561
+ return { theme, colorScheme: theme, themeColor: { light: color, dark: color } };
562
+ }
563
+
413
564
  // src/vscode/protocol.ts
414
565
  var APPEARANCE_MESSAGE_TYPE = "modelerShell.appearance";
415
566
  function appearanceMessage(appearance) {
@@ -554,163 +705,134 @@ function hasVisibleFocus(element) {
554
705
  }
555
706
  }
556
707
 
557
- // src/dom/hint.ts
558
- function createEmptyHint(ownerDocument, messages, options) {
559
- const { onInsertExample } = options;
560
- const action = onInsertExample ? createElement(
561
- ownerDocument,
562
- "button",
563
- { type: "button", class: "cd-shell-hint-action" },
564
- messages.insertExample
565
- ) : void 0;
566
- action?.addEventListener("click", () => onInsertExample?.());
567
- const element = createElement(
568
- ownerDocument,
569
- "div",
570
- { class: "cd-shell-hint" },
571
- createElement(ownerDocument, "p", { class: "cd-shell-hint-text" }, messages.emptyHint),
572
- action
573
- );
574
- element.hidden = true;
575
- return {
576
- element,
577
- isShown: () => !element.hidden,
578
- setShown(shown) {
579
- element.hidden = !shown;
580
- },
581
- place({ top, right, bottom, left }) {
582
- element.style.inset = `${top}px ${right}px ${bottom}px ${left}px`;
583
- },
584
- destroy() {
585
- element.remove();
586
- }
587
- };
588
- }
589
-
590
- // src/dom/inspector.ts
591
- function createInspector(ownerDocument, options, onChange) {
592
- const element = createElement(ownerDocument, "aside", {
593
- class: "cd-shell-inspector",
594
- "data-chrome": "inspector",
595
- "aria-label": options.label
596
- });
597
- element.hidden = true;
598
- function setOpen(open) {
599
- if (open === !element.hidden) return;
600
- element.hidden = !open;
601
- onChange(open);
708
+ // src/dom/feedback/appearance.ts
709
+ var NO_TRANSITION_CLASS2 = "cd-shell-no-transition";
710
+ var CONTRAST_QUERY = "(prefers-contrast: more)";
711
+ var SCHEME_MEDIA = {
712
+ light: "(prefers-color-scheme: light)",
713
+ dark: "(prefers-color-scheme: dark)"
714
+ };
715
+ function onMediaChange(query, listener) {
716
+ if (typeof query.addEventListener === "function") {
717
+ query.addEventListener("change", listener);
718
+ return () => query.removeEventListener("change", listener);
719
+ }
720
+ query.addListener(listener);
721
+ return () => query.removeListener(listener);
722
+ }
723
+ function connectWebAppearance(context) {
724
+ const { ownerDocument, themes, options } = context;
725
+ const darkCapable = themes.includes("dark");
726
+ if (darkCapable && !options) {
727
+ throw new Error(
728
+ '@miragon/modeler-shell: a web modeler whose themes include "dark" needs the appearance option ({ themeColors } from its vendored cd-tokens.generated.json)'
729
+ );
602
730
  }
603
- return {
604
- element,
605
- isOpen: () => !element.hidden,
606
- open: () => setOpen(true),
607
- close: () => setOpen(false),
608
- destroy() {
609
- element.remove();
731
+ const html = ownerDocument.documentElement;
732
+ const view = ownerDocument.defaultView;
733
+ const cleanups = [];
734
+ const contrast = typeof view?.matchMedia === "function" ? view.matchMedia(CONTRAST_QUERY) : void 0;
735
+ if (contrast) {
736
+ const mirrorContrast = () => toggleAttribute(html, "data-cd-kontrast", contrast.matches && "hoch");
737
+ mirrorContrast();
738
+ cleanups.push(onMediaChange(contrast, mirrorContrast));
739
+ }
740
+ function destroy() {
741
+ for (const cleanup of cleanups.splice(0)) cleanup();
742
+ }
743
+ if (!darkCapable || !options) {
744
+ return { choice: () => "system", setChoice() {
745
+ }, destroy };
746
+ }
747
+ const { themeColors } = options;
748
+ function storage() {
749
+ if (options?.storage) return options.storage;
750
+ try {
751
+ return globalThis.localStorage;
752
+ } catch {
753
+ return void 0;
610
754
  }
611
- };
612
- }
613
-
614
- // src/dom/keys.ts
615
- function isTextField(target) {
616
- if (!target || typeof target.closest !== "function") return false;
617
- const element = target;
618
- return element.isContentEditable === true || element.closest(TEXT_FIELD_SELECTOR) !== null;
619
- }
620
- function isShown(element) {
621
- if (element.localName === "dialog" && !element.hasAttribute("open")) return false;
622
- if (element.closest("[hidden]")) return false;
623
- return typeof element.checkVisibility !== "function" || element.checkVisibility({ visibilityProperty: true });
624
- }
625
- function isModal(element) {
626
- if (element.getAttribute("aria-modal") === "true") return true;
627
- try {
628
- return element.matches(":modal");
629
- } catch {
630
- return false;
631
755
  }
632
- }
633
- function openDialogs(scope) {
634
- return [...scope.querySelectorAll('[role="dialog"], [role="alertdialog"], dialog')].filter(isShown);
635
- }
636
- function createKeyListener(context) {
637
- return (event) => {
638
- if (event.key === "Escape") context.hideTooltip();
639
- const dialogs = openDialogs(context.dialogScope);
640
- const decision = decideKey(event, {
641
- host: context.host,
642
- inTextField: isTextField(event.target),
643
- menuOpen: context.menuOpen(),
644
- dialogOpen: dialogs.length > 0,
645
- modalOpen: dialogs.some(isModal),
646
- defaultPrevented: event.defaultPrevented
647
- });
648
- if (decision.preventDefault) event.preventDefault();
649
- if (decision.stopPropagation) event.stopPropagation();
650
- if (decision.action === "closeMenu") context.closeMenu();
651
- else if (decision.action) context.run(decision.action);
652
- };
653
- }
654
-
655
- // src/dom/ledger.ts
656
- var Ledger = class {
657
- undo = [];
658
- originalAttributes = /* @__PURE__ */ new Map();
659
- closed = false;
660
- get isClosed() {
661
- return this.closed;
662
- }
663
- /** Runs `task` on `close()`, in reverse order of registration. */
664
- defer(task) {
665
- this.undo.push(task);
666
- }
667
- /** Adds `className` unless present; `close()` removes it only if this ledger added it. */
668
- addClass(element, className) {
669
- if (element.classList.contains(className)) return;
670
- const hadClassAttribute = element.hasAttribute("class");
671
- element.classList.add(className);
672
- this.defer(() => {
673
- element.classList.remove(className);
674
- if (!hadClassAttribute && element.classList.length === 0) element.removeAttribute("class");
675
- });
756
+ function readStored() {
757
+ try {
758
+ return parseAppearanceChoice(storage()?.getItem(APPEARANCE_STORAGE_KEY));
759
+ } catch {
760
+ return "system";
761
+ }
676
762
  }
677
- /** Sets (string) or removes (`null`) an attribute; `close()` restores the value before the first change. */
678
- attribute(element, name, value) {
679
- let originals = this.originalAttributes.get(element);
680
- if (!originals) {
681
- originals = /* @__PURE__ */ new Map();
682
- this.originalAttributes.set(element, originals);
763
+ function store(choice) {
764
+ try {
765
+ const target = storage();
766
+ if (choice === "system") target?.removeItem(APPEARANCE_STORAGE_KEY);
767
+ else target?.setItem(APPEARANCE_STORAGE_KEY, choice);
768
+ } catch {
683
769
  }
684
- if (!originals.has(name)) {
685
- originals.set(name, element.getAttribute(name));
686
- this.defer(() => {
687
- const original = originals.get(name) ?? null;
688
- if (original === null) element.removeAttribute(name);
689
- else element.setAttribute(name, original);
690
- });
770
+ }
771
+ function nextFrame(callback) {
772
+ if (view && typeof view.requestAnimationFrame === "function") {
773
+ const frame = view.requestAnimationFrame(() => callback());
774
+ return () => view.cancelAnimationFrame(frame);
691
775
  }
692
- if (value === null) element.removeAttribute(name);
693
- else element.setAttribute(name, value);
776
+ const timer = setTimeout(callback, 16);
777
+ return () => clearTimeout(timer);
694
778
  }
695
- listen(target, type, listener, options) {
696
- const handler = listener;
697
- target.addEventListener(type, handler, options);
698
- this.defer(() => target.removeEventListener(type, handler, options));
779
+ let cancelTransitionFrame;
780
+ function allowTransitions() {
781
+ cancelTransitionFrame?.();
782
+ cancelTransitionFrame = void 0;
783
+ html.classList.remove(NO_TRANSITION_CLASS2);
699
784
  }
700
- /** Runs every undo task once; later calls are no-ops. */
701
- close() {
702
- if (this.closed) return;
703
- this.closed = true;
704
- for (const task of this.undo.reverse()) {
705
- try {
706
- task();
707
- } catch {
708
- }
709
- }
710
- this.undo.length = 0;
711
- this.originalAttributes.clear();
785
+ cleanups.push(() => {
786
+ if (cancelTransitionFrame) allowTransitions();
787
+ });
788
+ function suspendTransitions() {
789
+ cancelTransitionFrame?.();
790
+ html.classList.add(NO_TRANSITION_CLASS2);
791
+ cancelTransitionFrame = nextFrame(() => {
792
+ cancelTransitionFrame = nextFrame(allowTransitions);
793
+ });
712
794
  }
713
- };
795
+ function metaFor(name, media) {
796
+ const normalize = (value) => (value ?? "").replace(/\s+/g, "").toLowerCase();
797
+ const existing = [...ownerDocument.querySelectorAll(`meta[name="${name}"]`)].find(
798
+ (meta) => media === void 0 ? true : normalize(meta.getAttribute("media")) === normalize(media)
799
+ );
800
+ if (existing) return existing;
801
+ const created = createElement(ownerDocument, "meta", { name, media });
802
+ (ownerDocument.head ?? html).append(created);
803
+ return created;
804
+ }
805
+ function apply(choice) {
806
+ const mode = documentMode(choice, themes, themeColors);
807
+ suspendTransitions();
808
+ html.setAttribute("data-cd-theme", mode.theme);
809
+ metaFor("color-scheme").setAttribute("content", mode.colorScheme);
810
+ metaFor("theme-color", SCHEME_MEDIA.light).setAttribute("content", mode.themeColor.light);
811
+ metaFor("theme-color", SCHEME_MEDIA.dark).setAttribute("content", mode.themeColor.dark);
812
+ }
813
+ let current = readStored();
814
+ const onStorage = (event) => {
815
+ if (event.key !== null && event.key !== APPEARANCE_STORAGE_KEY) return;
816
+ const next = event.key === null ? "system" : parseAppearanceChoice(event.newValue);
817
+ if (next === current) return;
818
+ current = next;
819
+ apply(next);
820
+ };
821
+ if (view) {
822
+ view.addEventListener("storage", onStorage);
823
+ cleanups.push(() => view.removeEventListener("storage", onStorage));
824
+ }
825
+ return {
826
+ choice: () => current,
827
+ setChoice(choice) {
828
+ const next = parseAppearanceChoice(choice);
829
+ current = next;
830
+ store(next);
831
+ apply(next);
832
+ },
833
+ destroy
834
+ };
835
+ }
714
836
 
715
837
  // src/dom/icons.ts
716
838
  var ICONS = {
@@ -928,9 +1050,672 @@ function iconElement(icon, options) {
928
1050
  for (const [name, value] of attributes) child.setAttribute(name, value);
929
1051
  svg.append(child);
930
1052
  }
931
- return svg;
1053
+ return svg;
1054
+ }
1055
+
1056
+ // src/dom/feedback/problems-popover.ts
1057
+ var PILL_ATTRIBUTES = ["aria-haspopup", "aria-expanded", "aria-controls"];
1058
+ function isPrintable(event) {
1059
+ return event.key.length === 1 && /\S/.test(event.key) && !event.ctrlKey && !event.metaKey && !event.altKey;
1060
+ }
1061
+ function createProblemsPopover(context) {
1062
+ const { ownerDocument, root, layer, pill, id, titleId, messages } = context;
1063
+ const view = ownerDocument.defaultView;
1064
+ const list = createElement(ownerDocument, "ul", { class: "cd-shell-problems-list", role: "list" });
1065
+ const element = createElement(
1066
+ ownerDocument,
1067
+ "div",
1068
+ {
1069
+ class: "cd-shell-problems-popover",
1070
+ "data-chrome": "problems-popover",
1071
+ role: "dialog",
1072
+ id,
1073
+ "aria-labelledby": titleId,
1074
+ tabindex: "-1"
1075
+ },
1076
+ createElement(
1077
+ ownerDocument,
1078
+ "h2",
1079
+ { class: "cd-shell-problems-popover-title", id: titleId },
1080
+ messages.problemsTitle
1081
+ ),
1082
+ list
1083
+ );
1084
+ element.hidden = true;
1085
+ layer.append(element);
1086
+ pill.setAttribute("aria-haspopup", "dialog");
1087
+ pill.setAttribute("aria-expanded", "false");
1088
+ pill.setAttribute("aria-controls", id);
1089
+ let items = [];
1090
+ let rows = [];
1091
+ let destroyed = false;
1092
+ const isOpen = () => !element.hidden;
1093
+ function keyOf(item) {
1094
+ return `${item.elementId ?? ""}\0${item.severity}\0${item.message}`;
1095
+ }
1096
+ function rowElement(item) {
1097
+ const severity = item.severity === "error" ? "error" : "warning";
1098
+ const parts = [
1099
+ // The shape and colour of the icon carry the severity on screen, its name for screen readers.
1100
+ createElement(
1101
+ ownerDocument,
1102
+ "span",
1103
+ { class: "cd-shell-problem-icon", role: "img", "aria-label": messages.problemSeverity(severity) },
1104
+ iconElement(severity === "error" ? "circle-x" : "triangle-alert")
1105
+ ),
1106
+ createElement(ownerDocument, "span", { class: "cd-shell-problem-message" }, item.message),
1107
+ item.location ? createElement(ownerDocument, "span", { class: "cd-shell-problem-location" }, item.location) : void 0
1108
+ ];
1109
+ const attributes = { class: "cd-shell-problem", "data-severity": severity };
1110
+ if (!item.elementId) {
1111
+ const text = createElement(ownerDocument, "div", { ...attributes, tabindex: "-1" }, ...parts);
1112
+ rows.push({ key: keyOf(item), element: text });
1113
+ return text;
1114
+ }
1115
+ const button = createElement(ownerDocument, "button", { type: "button", ...attributes }, ...parts);
1116
+ button.addEventListener("click", () => select(item));
1117
+ rows.push({ key: keyOf(item), element: button });
1118
+ return button;
1119
+ }
1120
+ function focusedRowIndex() {
1121
+ return rows.findIndex((row) => row.element === ownerDocument.activeElement);
1122
+ }
1123
+ function render() {
1124
+ const focusedIndex = focusedRowIndex();
1125
+ const focusedKey = rows[focusedIndex]?.key;
1126
+ rows = [];
1127
+ list.replaceChildren(...items.map((item) => createElement(ownerDocument, "li", {}, rowElement(item))));
1128
+ if (focusedKey === void 0 || !isOpen()) return;
1129
+ const kept = rows.find((row) => row.key === focusedKey) ?? rows[Math.min(focusedIndex, rows.length - 1)];
1130
+ if (kept) focusRowElement(kept.element);
1131
+ else element.focus({ preventScroll: true });
1132
+ }
1133
+ function revealInList(row) {
1134
+ const rowBox = row.getBoundingClientRect();
1135
+ const listBox = list.getBoundingClientRect();
1136
+ if (rowBox.top < listBox.top) list.scrollTop -= listBox.top - rowBox.top;
1137
+ else if (rowBox.bottom > listBox.bottom) list.scrollTop += rowBox.bottom - listBox.bottom;
1138
+ }
1139
+ function focusRowElement(row) {
1140
+ row.focus({ preventScroll: true });
1141
+ revealInList(row);
1142
+ }
1143
+ function focusCanvas() {
1144
+ const canvas = root.querySelector(".cd-shell-canvas svg[tabindex]");
1145
+ canvas?.focus({ preventScroll: true });
1146
+ if ((!canvas || ownerDocument.activeElement !== canvas) && !pill.hidden)
1147
+ pill.focus({ preventScroll: true });
1148
+ }
1149
+ function select(item) {
1150
+ close();
1151
+ focusCanvas();
1152
+ context.onSelectItem?.(item);
1153
+ }
1154
+ function focusRow(index) {
1155
+ if (rows.length === 0) return;
1156
+ const row = rows[(index + rows.length) % rows.length];
1157
+ if (row) focusRowElement(row.element);
1158
+ }
1159
+ const onOutsidePointerDown = (event) => {
1160
+ const target = event.target;
1161
+ if (target && (element.contains(target) || pill.contains(target))) return;
1162
+ close();
1163
+ };
1164
+ const onWindowBlur = () => close();
1165
+ function open() {
1166
+ if (destroyed || isOpen()) return;
1167
+ element.hidden = false;
1168
+ pill.setAttribute("aria-expanded", "true");
1169
+ ownerDocument.addEventListener("pointerdown", onOutsidePointerDown, true);
1170
+ view?.addEventListener("blur", onWindowBlur);
1171
+ list.scrollTop = 0;
1172
+ const first = rows[0];
1173
+ if (first) focusRowElement(first.element);
1174
+ else element.focus({ preventScroll: true });
1175
+ }
1176
+ function close(options = {}) {
1177
+ if (!isOpen()) return;
1178
+ element.hidden = true;
1179
+ pill.setAttribute("aria-expanded", "false");
1180
+ ownerDocument.removeEventListener("pointerdown", onOutsidePointerDown, true);
1181
+ view?.removeEventListener("blur", onWindowBlur);
1182
+ if (options.restoreFocus && !pill.hidden) pill.focus({ preventScroll: true });
1183
+ }
1184
+ function consume(event) {
1185
+ event.preventDefault();
1186
+ event.stopPropagation();
1187
+ }
1188
+ element.addEventListener("keydown", (event) => {
1189
+ if (matchChromeKey(event)) return;
1190
+ const index = focusedRowIndex();
1191
+ switch (event.key) {
1192
+ case "Escape":
1193
+ consume(event);
1194
+ close({ restoreFocus: true });
1195
+ return;
1196
+ case "Tab": {
1197
+ const first = index <= 0;
1198
+ const last = rows.length === 0 || index === rows.length - 1;
1199
+ if (event.shiftKey && first) {
1200
+ consume(event);
1201
+ close({ restoreFocus: true });
1202
+ } else if (!event.shiftKey && last) {
1203
+ event.stopPropagation();
1204
+ close({ restoreFocus: true });
1205
+ }
1206
+ return;
1207
+ }
1208
+ case "ArrowDown":
1209
+ focusRow(index + 1);
1210
+ break;
1211
+ case "ArrowUp":
1212
+ focusRow(index < 0 ? rows.length - 1 : index - 1);
1213
+ break;
1214
+ case "Home":
1215
+ focusRow(0);
1216
+ break;
1217
+ case "End":
1218
+ focusRow(rows.length - 1);
1219
+ break;
1220
+ case "Enter":
1221
+ case " ":
1222
+ case "Delete":
1223
+ case "Backspace":
1224
+ event.stopPropagation();
1225
+ return;
1226
+ default:
1227
+ if (isPrintable(event)) event.stopPropagation();
1228
+ return;
1229
+ }
1230
+ consume(event);
1231
+ });
1232
+ return {
1233
+ element,
1234
+ isOpen,
1235
+ toggle() {
1236
+ if (isOpen()) close({ restoreFocus: true });
1237
+ else open();
1238
+ },
1239
+ open,
1240
+ close,
1241
+ setItems(next) {
1242
+ items = next;
1243
+ if (!destroyed) render();
1244
+ },
1245
+ destroy() {
1246
+ if (destroyed) return;
1247
+ close();
1248
+ destroyed = true;
1249
+ for (const name of PILL_ATTRIBUTES) pill.removeAttribute(name);
1250
+ element.remove();
1251
+ }
1252
+ };
1253
+ }
1254
+
1255
+ // src/dom/feedback/toasts.ts
1256
+ var TOAST_RESUME_MIN_MS = 1500;
1257
+ var TOAST_LEAVE_FALLBACK_MS = 400;
1258
+ var LEAVE_ANIMATION = "cd-shell-toast-out";
1259
+ var TOAST_ANNOUNCE_DELAY_MS = 100;
1260
+ var INERT_HANDLE = Object.freeze({ dismiss() {
1261
+ } });
1262
+ function createToaster(context) {
1263
+ const { ownerDocument, messages } = context;
1264
+ const web = context.host === "web";
1265
+ const element = createElement(ownerDocument, "div", {
1266
+ class: "cd-shell-toasts",
1267
+ "data-chrome": "toasts",
1268
+ popover: web ? "manual" : void 0
1269
+ });
1270
+ const liveRegion = (politeness) => createElement(ownerDocument, "div", {
1271
+ class: "cd-shell-live-region",
1272
+ "data-live": politeness,
1273
+ role: politeness === "polite" ? "status" : "alert",
1274
+ "aria-live": politeness,
1275
+ "aria-atomic": "true"
1276
+ });
1277
+ const politeRegion = liveRegion("polite");
1278
+ const assertiveRegion = liveRegion("assertive");
1279
+ const announcer = createElement(
1280
+ ownerDocument,
1281
+ "div",
1282
+ { class: "cd-shell-live" },
1283
+ politeRegion,
1284
+ assertiveRegion
1285
+ );
1286
+ const live = [];
1287
+ const leaveTimers = /* @__PURE__ */ new Set();
1288
+ const announceTimers = /* @__PURE__ */ new Map();
1289
+ let lastId = 0;
1290
+ let destroyed = false;
1291
+ function regionFor(request) {
1292
+ return request.status === "danger" ? assertiveRegion : politeRegion;
1293
+ }
1294
+ function announce(request) {
1295
+ const region = regionFor(request);
1296
+ clearTimeout(announceTimers.get(region));
1297
+ region.textContent = "";
1298
+ announceTimers.set(
1299
+ region,
1300
+ setTimeout(() => {
1301
+ announceTimers.delete(region);
1302
+ if (!destroyed) region.textContent = request.text;
1303
+ }, TOAST_ANNOUNCE_DELAY_MS)
1304
+ );
1305
+ }
1306
+ function silence(request) {
1307
+ const region = regionFor(request);
1308
+ if (region.textContent === request.text) region.textContent = "";
1309
+ }
1310
+ function changed() {
1311
+ if (!destroyed) context.onChange?.();
1312
+ }
1313
+ function supportsPopover() {
1314
+ return web && typeof element.showPopover === "function" && typeof element.hidePopover === "function";
1315
+ }
1316
+ function popoverOpen() {
1317
+ try {
1318
+ return element.matches(":popover-open");
1319
+ } catch {
1320
+ return false;
1321
+ }
1322
+ }
1323
+ function modalOpen() {
1324
+ try {
1325
+ return ownerDocument.querySelector(":modal") !== null;
1326
+ } catch {
1327
+ return true;
1328
+ }
1329
+ }
1330
+ function raise() {
1331
+ if (!supportsPopover() || !element.isConnected) return;
1332
+ if (popoverOpen() && !modalOpen()) return;
1333
+ const focused = ownerDocument.activeElement;
1334
+ const keepFocus = focused && element.contains(focused) ? focused : void 0;
1335
+ try {
1336
+ element.hidePopover();
1337
+ element.showPopover();
1338
+ } catch {
1339
+ return;
1340
+ }
1341
+ if (keepFocus && ownerDocument.activeElement !== keepFocus) keepFocus.focus({ preventScroll: true });
1342
+ }
1343
+ function lowerWhenEmpty() {
1344
+ if (element.childElementCount > 0 || !supportsPopover()) return;
1345
+ try {
1346
+ element.hidePopover();
1347
+ } catch {
1348
+ }
1349
+ }
1350
+ function engaged(toast) {
1351
+ return toast.hovered || toast.focused;
1352
+ }
1353
+ function stopTimer(toast) {
1354
+ clearTimeout(toast.timer);
1355
+ toast.timer = void 0;
1356
+ }
1357
+ function runTimer(toast) {
1358
+ stopTimer(toast);
1359
+ if (toast.remaining === void 0 || engaged(toast)) return;
1360
+ toast.startedAt = Date.now();
1361
+ toast.timer = setTimeout(() => close(toast), toast.remaining);
1362
+ }
1363
+ function pause(toast) {
1364
+ if (toast.timer === void 0) return;
1365
+ stopTimer(toast);
1366
+ if (toast.remaining !== void 0) {
1367
+ toast.remaining = Math.max(0, toast.remaining - (Date.now() - toast.startedAt));
1368
+ }
1369
+ }
1370
+ function setEngagement(toast, hovered, focused) {
1371
+ if (!live.includes(toast)) return;
1372
+ const wasEngaged = engaged(toast);
1373
+ toast.hovered = hovered;
1374
+ toast.focused = focused;
1375
+ if (engaged(toast) === wasEngaged) return;
1376
+ if (wasEngaged) {
1377
+ if (toast.remaining !== void 0) toast.remaining = Math.max(toast.remaining, TOAST_RESUME_MIN_MS);
1378
+ runTimer(toast);
1379
+ } else {
1380
+ pause(toast);
1381
+ }
1382
+ }
1383
+ function reducedMotion() {
1384
+ if (ownerDocument.documentElement.classList.contains(REDUCED_MOTION_CLASS)) return true;
1385
+ const view = ownerDocument.defaultView;
1386
+ return typeof view?.matchMedia === "function" && view.matchMedia("(prefers-reduced-motion: reduce)").matches;
1387
+ }
1388
+ function removeElement(toastElement) {
1389
+ toastElement.remove();
1390
+ lowerWhenEmpty();
1391
+ changed();
1392
+ }
1393
+ function leave(toastElement) {
1394
+ toastElement.setAttribute("data-leaving", "");
1395
+ toastElement.inert = true;
1396
+ if (reducedMotion()) {
1397
+ removeElement(toastElement);
1398
+ return;
1399
+ }
1400
+ let done = false;
1401
+ const finish = () => {
1402
+ if (done) return;
1403
+ done = true;
1404
+ clearTimeout(fallback);
1405
+ leaveTimers.delete(fallback);
1406
+ removeElement(toastElement);
1407
+ };
1408
+ toastElement.addEventListener("animationend", (event) => {
1409
+ if (event.target === toastElement && event.animationName === LEAVE_ANIMATION) finish();
1410
+ });
1411
+ const fallback = setTimeout(finish, TOAST_LEAVE_FALLBACK_MS);
1412
+ leaveTimers.add(fallback);
1413
+ }
1414
+ function close(toast) {
1415
+ const index = live.indexOf(toast);
1416
+ if (index < 0) return;
1417
+ live.splice(index, 1);
1418
+ stopTimer(toast);
1419
+ silence(toast.request);
1420
+ const heldFocus = toast.element.contains(ownerDocument.activeElement);
1421
+ if (heldFocus) {
1422
+ const next = live[index] ?? live[index - 1];
1423
+ if (next) next.closeButton.focus({ preventScroll: true });
1424
+ else context.focusFallback();
1425
+ }
1426
+ leave(toast.element);
1427
+ }
1428
+ function setAction(toast, action) {
1429
+ const previous = toast.actionButton;
1430
+ const hadFocus = previous !== void 0 && ownerDocument.activeElement === previous;
1431
+ previous?.remove();
1432
+ toast.actionButton = void 0;
1433
+ if (action) {
1434
+ const button = createElement(
1435
+ ownerDocument,
1436
+ "button",
1437
+ { type: "button", class: "cd-shell-toast-action cd-shell-button", "data-variant": "ghost" },
1438
+ action.label
1439
+ );
1440
+ button.addEventListener("click", () => {
1441
+ close(toast);
1442
+ action.onSelect();
1443
+ });
1444
+ toast.closeButton.before(button);
1445
+ toast.actionButton = button;
1446
+ }
1447
+ if (hadFocus) (toast.actionButton ?? toast.closeButton).focus({ preventScroll: true });
1448
+ }
1449
+ function slotOf(toast) {
1450
+ return { id: toast.id, hasAction: toast.request.action !== void 0, engaged: engaged(toast) };
1451
+ }
1452
+ function render(request) {
1453
+ const id = ++lastId;
1454
+ const textId = `${context.idPrefix}-${id}`;
1455
+ const closeButton = createElement(
1456
+ ownerDocument,
1457
+ "button",
1458
+ {
1459
+ type: "button",
1460
+ class: "cd-shell-toast-close",
1461
+ "aria-label": messages.close,
1462
+ "aria-describedby": textId
1463
+ },
1464
+ iconElement("x")
1465
+ );
1466
+ const toastElement = createElement(
1467
+ ownerDocument,
1468
+ "div",
1469
+ { class: "cd-shell-toast", "data-status": request.status },
1470
+ createElement(ownerDocument, "span", { class: "cd-shell-toast-dot", "aria-hidden": "true" }),
1471
+ createElement(ownerDocument, "span", { class: "cd-shell-toast-text", id: textId }, request.text),
1472
+ closeButton
1473
+ );
1474
+ const toast = {
1475
+ id,
1476
+ element: toastElement,
1477
+ closeButton,
1478
+ handle: Object.freeze({ dismiss: () => close(toast) }),
1479
+ request,
1480
+ actionButton: void 0,
1481
+ hovered: false,
1482
+ focused: false,
1483
+ remaining: toastLifetime(request),
1484
+ startedAt: Date.now(),
1485
+ timer: void 0
1486
+ };
1487
+ setAction(toast, request.action);
1488
+ closeButton.addEventListener("click", () => close(toast));
1489
+ toastElement.addEventListener("mouseenter", () => setEngagement(toast, true, toast.focused));
1490
+ toastElement.addEventListener("mouseleave", () => setEngagement(toast, false, toast.focused));
1491
+ toastElement.addEventListener("focusin", () => setEngagement(toast, toast.hovered, true));
1492
+ toastElement.addEventListener(
1493
+ "focusout",
1494
+ (event) => setEngagement(toast, toast.hovered, toastElement.contains(event.relatedTarget))
1495
+ );
1496
+ return toast;
1497
+ }
1498
+ function show(request) {
1499
+ if (destroyed) return INERT_HANDLE;
1500
+ const same = live.find(
1501
+ (toast2) => toast2.request.status === request.status && toast2.request.text === request.text
1502
+ );
1503
+ if (same) {
1504
+ same.request = request;
1505
+ setAction(same, request.action);
1506
+ same.remaining = toastLifetime(request);
1507
+ runTimer(same);
1508
+ raise();
1509
+ announce(request);
1510
+ changed();
1511
+ return same.handle;
1512
+ }
1513
+ while (live.length >= MAX_TOASTS) {
1514
+ const evictedId = toastToEvict(live.map(slotOf));
1515
+ const evicted = live.find((toast2) => toast2.id === evictedId) ?? live[0];
1516
+ if (!evicted) break;
1517
+ close(evicted);
1518
+ }
1519
+ const toast = render(request);
1520
+ live.push(toast);
1521
+ element.append(toast.element);
1522
+ raise();
1523
+ runTimer(toast);
1524
+ announce(request);
1525
+ changed();
1526
+ return toast.handle;
1527
+ }
1528
+ return {
1529
+ element,
1530
+ announcer,
1531
+ show,
1532
+ setBottom(bottom) {
1533
+ element.style.bottom = `${bottom}px`;
1534
+ },
1535
+ widestCard() {
1536
+ let widest;
1537
+ for (const card of element.children) {
1538
+ const width = card.getBoundingClientRect().width;
1539
+ if (width > 0) widest = Math.max(widest ?? 0, width);
1540
+ }
1541
+ return widest;
1542
+ },
1543
+ destroy() {
1544
+ if (destroyed) return;
1545
+ destroyed = true;
1546
+ for (const toast of live.splice(0)) stopTimer(toast);
1547
+ for (const timer of leaveTimers) clearTimeout(timer);
1548
+ leaveTimers.clear();
1549
+ for (const timer of announceTimers.values()) clearTimeout(timer);
1550
+ announceTimers.clear();
1551
+ element.remove();
1552
+ announcer.remove();
1553
+ }
1554
+ };
1555
+ }
1556
+
1557
+ // src/dom/hint.ts
1558
+ function createEmptyHint(ownerDocument, messages, options) {
1559
+ const { onInsertExample } = options;
1560
+ const action = onInsertExample ? createElement(
1561
+ ownerDocument,
1562
+ "button",
1563
+ { type: "button", class: "cd-shell-hint-action" },
1564
+ messages.insertExample
1565
+ ) : void 0;
1566
+ action?.addEventListener("click", () => onInsertExample?.());
1567
+ const element = createElement(
1568
+ ownerDocument,
1569
+ "div",
1570
+ { class: "cd-shell-hint" },
1571
+ createElement(ownerDocument, "p", { class: "cd-shell-hint-text" }, messages.emptyHint),
1572
+ action
1573
+ );
1574
+ element.hidden = true;
1575
+ return {
1576
+ element,
1577
+ isShown: () => !element.hidden,
1578
+ setShown(shown) {
1579
+ element.hidden = !shown;
1580
+ },
1581
+ place({ top, right, bottom, left }) {
1582
+ element.style.inset = `${top}px ${right}px ${bottom}px ${left}px`;
1583
+ },
1584
+ destroy() {
1585
+ element.remove();
1586
+ }
1587
+ };
1588
+ }
1589
+
1590
+ // src/dom/inspector.ts
1591
+ function createInspector(ownerDocument, options, onChange) {
1592
+ const element = createElement(ownerDocument, "aside", {
1593
+ class: "cd-shell-inspector",
1594
+ "data-chrome": "inspector",
1595
+ "aria-label": options.label
1596
+ });
1597
+ element.hidden = true;
1598
+ function setOpen(open) {
1599
+ if (open === !element.hidden) return;
1600
+ element.hidden = !open;
1601
+ onChange(open);
1602
+ }
1603
+ return {
1604
+ element,
1605
+ isOpen: () => !element.hidden,
1606
+ open: () => setOpen(true),
1607
+ close: () => setOpen(false),
1608
+ destroy() {
1609
+ element.remove();
1610
+ }
1611
+ };
1612
+ }
1613
+
1614
+ // src/dom/keys.ts
1615
+ function isTextField(target) {
1616
+ if (!target || typeof target.closest !== "function") return false;
1617
+ const element = target;
1618
+ return element.isContentEditable === true || element.closest(TEXT_FIELD_SELECTOR) !== null;
1619
+ }
1620
+ function isShown(element) {
1621
+ if (element.localName === "dialog" && !element.hasAttribute("open")) return false;
1622
+ if (element.closest("[hidden]")) return false;
1623
+ return typeof element.checkVisibility !== "function" || element.checkVisibility({ visibilityProperty: true });
1624
+ }
1625
+ function isModal(element) {
1626
+ if (element.getAttribute("aria-modal") === "true") return true;
1627
+ try {
1628
+ return element.matches(":modal");
1629
+ } catch {
1630
+ return false;
1631
+ }
1632
+ }
1633
+ function openDialogs(scope) {
1634
+ return [...scope.querySelectorAll('[role="dialog"], [role="alertdialog"], dialog')].filter(isShown);
1635
+ }
1636
+ function isModalOpen(scope) {
1637
+ return openDialogs(scope).some(isModal);
1638
+ }
1639
+ function createKeyListener(context) {
1640
+ return (event) => {
1641
+ if (event.key === "Escape") context.hideTooltip();
1642
+ const dialogs = openDialogs(context.dialogScope);
1643
+ const decision = decideKey(event, {
1644
+ host: context.host,
1645
+ inTextField: isTextField(event.target),
1646
+ menuOpen: context.menuOpen(),
1647
+ dialogOpen: dialogs.length > 0,
1648
+ modalOpen: dialogs.some(isModal),
1649
+ defaultPrevented: event.defaultPrevented,
1650
+ startScreen: context.startScreen?.() ?? false
1651
+ });
1652
+ if (decision.preventDefault) event.preventDefault();
1653
+ if (decision.stopPropagation) event.stopPropagation();
1654
+ if (decision.action === "closeMenu") context.closeMenu();
1655
+ else if (decision.action) context.run(decision.action);
1656
+ };
932
1657
  }
933
1658
 
1659
+ // src/dom/ledger.ts
1660
+ var Ledger = class {
1661
+ undo = [];
1662
+ originalAttributes = /* @__PURE__ */ new Map();
1663
+ closed = false;
1664
+ get isClosed() {
1665
+ return this.closed;
1666
+ }
1667
+ /** Runs `task` on `close()`, in reverse order of registration. */
1668
+ defer(task) {
1669
+ this.undo.push(task);
1670
+ }
1671
+ /** Adds `className` unless present; `close()` removes it only if this ledger added it. */
1672
+ addClass(element, className) {
1673
+ if (element.classList.contains(className)) return;
1674
+ const hadClassAttribute = element.hasAttribute("class");
1675
+ element.classList.add(className);
1676
+ this.defer(() => {
1677
+ element.classList.remove(className);
1678
+ if (!hadClassAttribute && element.classList.length === 0) element.removeAttribute("class");
1679
+ });
1680
+ }
1681
+ /** Sets (string) or removes (`null`) an attribute; `close()` restores the value before the first change. */
1682
+ attribute(element, name, value) {
1683
+ let originals = this.originalAttributes.get(element);
1684
+ if (!originals) {
1685
+ originals = /* @__PURE__ */ new Map();
1686
+ this.originalAttributes.set(element, originals);
1687
+ }
1688
+ if (!originals.has(name)) {
1689
+ originals.set(name, element.getAttribute(name));
1690
+ this.defer(() => {
1691
+ const original = originals.get(name) ?? null;
1692
+ if (original === null) element.removeAttribute(name);
1693
+ else element.setAttribute(name, original);
1694
+ });
1695
+ }
1696
+ if (value === null) element.removeAttribute(name);
1697
+ else element.setAttribute(name, value);
1698
+ }
1699
+ listen(target, type, listener, options) {
1700
+ const handler = listener;
1701
+ target.addEventListener(type, handler, options);
1702
+ this.defer(() => target.removeEventListener(type, handler, options));
1703
+ }
1704
+ /** Runs every undo task once; later calls are no-ops. */
1705
+ close() {
1706
+ if (this.closed) return;
1707
+ this.closed = true;
1708
+ for (const task of this.undo.reverse()) {
1709
+ try {
1710
+ task();
1711
+ } catch {
1712
+ }
1713
+ }
1714
+ this.undo.length = 0;
1715
+ this.originalAttributes.clear();
1716
+ }
1717
+ };
1718
+
934
1719
  // src/dom/legend.ts
935
1720
  function createLegend(context) {
936
1721
  const { ownerDocument, messages, tooltip, titleId, options } = context;
@@ -990,8 +1775,11 @@ function createLegend(context) {
990
1775
  }
991
1776
 
992
1777
  // src/dom/menu.ts
993
- var GROUP_ORDER = ["model", "export", "help"];
994
- function isPrintable(event) {
1778
+ var GROUP_ORDER = ["file", "model", "export", "appearance", "help"];
1779
+ function isModelerGroup(name) {
1780
+ return name === "model" || name === "export" || name === "help";
1781
+ }
1782
+ function isPrintable2(event) {
995
1783
  return event.key.length === 1 && /\S/.test(event.key) && !event.ctrlKey && !event.metaKey && !event.altKey;
996
1784
  }
997
1785
  function createMenu(context) {
@@ -1020,8 +1808,9 @@ function createMenu(context) {
1020
1808
  layer.prepend(dropdown);
1021
1809
  const isOpen = () => !dropdown.hidden;
1022
1810
  function entriesOf(name) {
1023
- const own = groups[name] ?? [];
1024
- return name === "model" ? [...own, ...context.shellModelEntries()] : own;
1811
+ const own = isModelerGroup(name) ? groups[name] ?? [] : [];
1812
+ const added = context.shellEntries(name);
1813
+ return added.length > 0 ? [...own, ...added] : own;
1025
1814
  }
1026
1815
  function hasEntries() {
1027
1816
  return GROUP_ORDER.some((name) => entriesOf(name).length > 0);
@@ -1075,14 +1864,12 @@ function createMenu(context) {
1075
1864
  }
1076
1865
  };
1077
1866
  }
1078
- const shortcut = entry.kind === "action" && entry.shortcut ? createElement(ownerDocument, "span", { class: "cd-shell-menu-shortcut" }, entry.shortcut) : void 0;
1867
+ const shortcut = entry.shortcut ? createElement(ownerDocument, "span", { class: "cd-shell-menu-shortcut" }, entry.shortcut) : void 0;
1079
1868
  const element = itemElement("menuitem", entry.label, { icon: entry.icon, end: shortcut, disabled });
1080
1869
  element.dataset.menuId = entry.id;
1081
1870
  element.dataset.menuGroup = group;
1082
- if (entry.kind === "action") {
1083
- const keyShortcuts = entry.ariaKeyShortcuts ?? (entry.shortcut ? ariaKeyShortcutsFromLabel(entry.shortcut) : void 0);
1084
- toggleAttribute(element, "aria-keyshortcuts", keyShortcuts || null);
1085
- }
1871
+ const keyShortcuts = entry.ariaKeyShortcuts ?? (entry.shortcut ? ariaKeyShortcutsFromLabel(entry.shortcut) : void 0);
1872
+ toggleAttribute(element, "aria-keyshortcuts", keyShortcuts || null);
1086
1873
  if (entry.kind === "dialog") element.setAttribute("aria-haspopup", "dialog");
1087
1874
  if (entry.kind === "action" && entry.danger) element.dataset.danger = "";
1088
1875
  return {
@@ -1267,7 +2054,7 @@ function createMenu(context) {
1267
2054
  event.stopPropagation();
1268
2055
  return;
1269
2056
  default:
1270
- if (!isPrintable(event)) return;
2057
+ if (!isPrintable2(event)) return;
1271
2058
  typeAhead(event.key);
1272
2059
  }
1273
2060
  consume(event);
@@ -1297,7 +2084,7 @@ function createMenu(context) {
1297
2084
 
1298
2085
  // src/dom/problems.ts
1299
2086
  function createProblemsPill(context) {
1300
- const { ownerDocument, messages, lang, tooltip, options } = context;
2087
+ const { ownerDocument, messages, lang, tooltip } = context;
1301
2088
  const icon = createElement(ownerDocument, "span", { class: "cd-shell-problems-icon" });
1302
2089
  const text = createElement(ownerDocument, "span", { class: "cd-shell-problems-text" });
1303
2090
  const element = createElement(
@@ -1308,7 +2095,7 @@ function createProblemsPill(context) {
1308
2095
  text
1309
2096
  );
1310
2097
  element.hidden = true;
1311
- element.addEventListener("click", () => options.onActivate());
2098
+ element.addEventListener("click", () => context.onActivate());
1312
2099
  const detachTooltip = context.showTooltip ? tooltip.attach(element, { label: messages.showProblems }) : void 0;
1313
2100
  let severity;
1314
2101
  return {
@@ -1584,6 +2371,380 @@ function createTooltip({ root, layer, id }) {
1584
2371
  };
1585
2372
  }
1586
2373
 
2374
+ // src/dom/web/drop-zone.ts
2375
+ var DRAG_EVENTS = ["dragenter", "dragover", "dragleave", "drop"];
2376
+ function carriesFiles(transfer) {
2377
+ if (!transfer) return false;
2378
+ if (Array.from(transfer.types ?? []).includes("Files")) return true;
2379
+ return Array.from(transfer.items ?? []).some((item) => item.kind === "file");
2380
+ }
2381
+ function dragItems(transfer) {
2382
+ const items = Array.from(transfer.items ?? [], (item) => ({ kind: item.kind, type: item.type }));
2383
+ return items.length > 0 ? items : [{ kind: "file", type: "" }];
2384
+ }
2385
+ function acceptList(accept, lang) {
2386
+ const entries = accept.map((entry) => entry.trim()).filter((entry) => entry !== "");
2387
+ const extensions = entries.filter((entry) => entry.startsWith("."));
2388
+ const listed = extensions.length > 0 ? extensions : entries;
2389
+ let format;
2390
+ try {
2391
+ format = new Intl.ListFormat(lang, { type: "disjunction" });
2392
+ } catch {
2393
+ format = new Intl.ListFormat("en", { type: "disjunction" });
2394
+ }
2395
+ return format.format(listed);
2396
+ }
2397
+ function createDropZone(context) {
2398
+ const { ownerDocument, layer, messages, lang, options, modalOpen, toast, setState } = context;
2399
+ const chip = createElement(ownerDocument, "span", { class: "cd-shell-drop-zone-chip" });
2400
+ const element = createElement(
2401
+ ownerDocument,
2402
+ "div",
2403
+ { class: "cd-shell-drop-zone", "data-chrome": "drop-zone", "aria-hidden": "true" },
2404
+ chip
2405
+ );
2406
+ element.hidden = true;
2407
+ layer.append(element);
2408
+ const view = ownerDocument.defaultView;
2409
+ let depth = 0;
2410
+ let current = null;
2411
+ function verdictFor(items) {
2412
+ return dragVerdict(items, {
2413
+ accept: options.accept,
2414
+ multiple: options.multiple ?? false,
2415
+ dialogOpen: modalOpen()
2416
+ });
2417
+ }
2418
+ function show(verdict) {
2419
+ const next = verdict.state === "none" ? null : verdict.state;
2420
+ if (verdict.state === "over") chip.textContent = messages.dropFile;
2421
+ if (verdict.state === "forbidden") chip.textContent = messages.dropForbidden(verdict.reason);
2422
+ if (next === current) return;
2423
+ current = next;
2424
+ element.hidden = next === null;
2425
+ if (next === null) element.removeAttribute("data-state");
2426
+ else element.setAttribute("data-state", next);
2427
+ setState(next);
2428
+ }
2429
+ function track(transfer) {
2430
+ const verdict = verdictFor(dragItems(transfer));
2431
+ show(verdict);
2432
+ transfer.dropEffect = verdict.state === "forbidden" ? "none" : "copy";
2433
+ }
2434
+ function rejectByDefault(files) {
2435
+ const accepted = acceptList(options.accept, lang);
2436
+ for (const file of files) toast({ status: "danger", text: messages.cannotOpenFile(file.name, accepted) });
2437
+ }
2438
+ function receive(files, verdict) {
2439
+ if (verdict.state === "forbidden" && verdict.reason !== "type") return;
2440
+ const accepted = files.filter((file) => acceptsFile(file, options.accept));
2441
+ const rejected = files.filter((file) => !accepted.includes(file));
2442
+ if (accepted.length > 0) options.onDrop(accepted);
2443
+ if (rejected.length > 0) (options.onReject ?? rejectByDefault)(rejected);
2444
+ }
2445
+ const onDragEvent = (event) => {
2446
+ const transfer = event.dataTransfer;
2447
+ if (!carriesFiles(transfer)) return;
2448
+ event.preventDefault();
2449
+ switch (event.type) {
2450
+ case "dragenter":
2451
+ depth += 1;
2452
+ track(transfer);
2453
+ break;
2454
+ case "dragover":
2455
+ depth = Math.max(depth, 1);
2456
+ track(transfer);
2457
+ break;
2458
+ case "dragleave":
2459
+ depth = Math.max(0, depth - 1);
2460
+ if (depth === 0) show({ state: "none" });
2461
+ break;
2462
+ case "drop": {
2463
+ depth = 0;
2464
+ show({ state: "none" });
2465
+ const files = Array.from(transfer.files ?? []);
2466
+ const items = files.length > 0 ? files.map((file) => ({ kind: "file", type: file.type })) : dragItems(transfer);
2467
+ receive(files, verdictFor(items));
2468
+ break;
2469
+ }
2470
+ }
2471
+ };
2472
+ for (const type of DRAG_EVENTS) view?.addEventListener(type, onDragEvent);
2473
+ return {
2474
+ element,
2475
+ state: () => current,
2476
+ destroy() {
2477
+ for (const type of DRAG_EVENTS) view?.removeEventListener(type, onDragEvent);
2478
+ depth = 0;
2479
+ if (current !== null) {
2480
+ current = null;
2481
+ setState(null);
2482
+ }
2483
+ element.remove();
2484
+ }
2485
+ };
2486
+ }
2487
+
2488
+ // src/dom/web/empty-state.ts
2489
+ var KOMET_SIZE = "64";
2490
+ function createEmptyState(context) {
2491
+ const { ownerDocument, messages, options, dropHint, open, titleId, onLeave } = context;
2492
+ const button = (variant, label, run) => {
2493
+ const element2 = createElement(
2494
+ ownerDocument,
2495
+ "button",
2496
+ { type: "button", class: "cd-shell-button", "data-variant": variant },
2497
+ label
2498
+ );
2499
+ element2.addEventListener("click", () => {
2500
+ onLeave();
2501
+ run();
2502
+ });
2503
+ return element2;
2504
+ };
2505
+ const onShowExample = options.onShowExample;
2506
+ const newDiagram = button("primary", messages.newDiagram, () => options.onNewDiagram());
2507
+ const element = createElement(
2508
+ ownerDocument,
2509
+ "section",
2510
+ { class: "cd-shell-empty-state", "data-chrome": "empty-state", "aria-labelledby": titleId },
2511
+ // Dragging the mark would start an image drag that some browsers carry as a file.
2512
+ createElement(ownerDocument, "img", {
2513
+ class: "cd-shell-empty-state-mark",
2514
+ src: options.komet,
2515
+ width: KOMET_SIZE,
2516
+ height: KOMET_SIZE,
2517
+ alt: "",
2518
+ draggable: "false"
2519
+ }),
2520
+ createElement(ownerDocument, "h1", { class: "cd-shell-empty-state-title", id: titleId }, options.title),
2521
+ createElement(ownerDocument, "p", { class: "cd-shell-empty-state-text" }, options.text),
2522
+ createElement(
2523
+ ownerDocument,
2524
+ "div",
2525
+ { class: "cd-shell-empty-state-actions" },
2526
+ newDiagram,
2527
+ onShowExample && button("ghost", messages.showExample, () => onShowExample())
2528
+ ),
2529
+ hintParagraph()
2530
+ );
2531
+ function hintParagraph() {
2532
+ const hasDropHint = dropHint !== void 0 && dropHint !== "";
2533
+ if (!hasDropHint && !open) return void 0;
2534
+ const paragraph = createElement(ownerDocument, "p", { class: "cd-shell-empty-state-hint" });
2535
+ if (hasDropHint) paragraph.append(dropHint);
2536
+ if (!open) return paragraph;
2537
+ const shortcut = open.shortcut?.trim();
2538
+ const link = createElement(
2539
+ ownerDocument,
2540
+ "button",
2541
+ {
2542
+ type: "button",
2543
+ class: "cd-shell-empty-state-open",
2544
+ "aria-keyshortcuts": shortcut ? ariaKeyShortcutsFromLabel(shortcut) : void 0,
2545
+ "aria-haspopup": open.dialog ? "dialog" : void 0
2546
+ },
2547
+ messages.openFileLink
2548
+ );
2549
+ link.addEventListener("click", () => open.onSelect());
2550
+ if (hasDropHint) {
2551
+ paragraph.append(" ", createElement(ownerDocument, "span", { "aria-hidden": "true" }, "\xB7"), " ");
2552
+ }
2553
+ const openItem = createElement(ownerDocument, "span", { class: "cd-shell-empty-state-open-item" }, link);
2554
+ if (shortcut) {
2555
+ openItem.append(
2556
+ " ",
2557
+ createElement(
2558
+ ownerDocument,
2559
+ "kbd",
2560
+ { class: "cd-shell-empty-state-key", "aria-hidden": "true" },
2561
+ shortcut
2562
+ )
2563
+ );
2564
+ }
2565
+ paragraph.append(openItem);
2566
+ return paragraph;
2567
+ }
2568
+ element.hidden = true;
2569
+ return {
2570
+ element,
2571
+ isShown: () => !element.hidden,
2572
+ setShown(shown) {
2573
+ element.hidden = !shown;
2574
+ },
2575
+ focus() {
2576
+ newDiagram.focus({ preventScroll: true });
2577
+ },
2578
+ destroy() {
2579
+ element.remove();
2580
+ }
2581
+ };
2582
+ }
2583
+
2584
+ // src/dom/web/legal.ts
2585
+ var MIRAGON_HOME = "https://www.miragon.io/";
2586
+ var MIRAGON_IMPRESSUM = "https://www.miragon.io/impressum/";
2587
+ var MIRAGON_DATENSCHUTZ = "https://www.miragon.io/datenschutz/";
2588
+ function createLegalLine(context) {
2589
+ const { ownerDocument, messages, options } = context;
2590
+ const year = options.year ?? (/* @__PURE__ */ new Date()).getFullYear();
2591
+ const newTabId = `${context.idPrefix}-new-tab`;
2592
+ const link = (label, href, lang) => createElement(
2593
+ ownerDocument,
2594
+ "a",
2595
+ { href, lang, target: "_blank", rel: "noopener noreferrer", "aria-describedby": newTabId },
2596
+ label
2597
+ );
2598
+ const separator = () => createElement(ownerDocument, "span", { "aria-hidden": "true" }, "\xB7");
2599
+ const item = (anchor) => createElement(ownerDocument, "span", { class: "cd-shell-legal-item" }, separator(), anchor);
2600
+ const links = [
2601
+ link(messages.copyright(year), MIRAGON_HOME),
2602
+ link("Impressum", MIRAGON_IMPRESSUM, "de"),
2603
+ link("Datenschutz", MIRAGON_DATENSCHUTZ, "de"),
2604
+ ...(options.links ?? []).map((extra) => link(extra.label, extra.href, extra.lang))
2605
+ ];
2606
+ const element = createElement(ownerDocument, "nav", {
2607
+ class: "cd-shell-legal",
2608
+ "data-chrome": "legal",
2609
+ "aria-label": messages.legal
2610
+ });
2611
+ element.append(
2612
+ ...links.map((anchor, index) => index === 0 ? anchor : item(anchor)),
2613
+ createElement(ownerDocument, "span", { id: newTabId, hidden: "" }, messages.opensInNewTab)
2614
+ );
2615
+ return {
2616
+ element,
2617
+ destroy() {
2618
+ element.remove();
2619
+ }
2620
+ };
2621
+ }
2622
+
2623
+ // src/dom/web/menu-groups.ts
2624
+ function fileEntry(spec, action) {
2625
+ const common = {
2626
+ id: spec.id,
2627
+ label: spec.label,
2628
+ ...spec.icon ? { icon: spec.icon } : {},
2629
+ ...action.disabled ? { disabled: action.disabled } : {},
2630
+ ...action.shortcut ? { shortcut: action.shortcut } : {},
2631
+ onSelect: () => action.onSelect()
2632
+ };
2633
+ return action.dialog ? { kind: "dialog", ...common } : { kind: "action", ...common };
2634
+ }
2635
+ function fileMenuEntries(options, messages) {
2636
+ const specs = [
2637
+ { option: "newDiagram", id: "new-diagram", label: messages.newDiagram, icon: "file-plus" },
2638
+ { option: "open", id: "open", label: messages.openFile, icon: "folder-open" },
2639
+ // §5.14 gives "Show example" no icon.
2640
+ { option: "showExample", id: "show-example", label: messages.showExample, icon: void 0 },
2641
+ { option: "import", id: "import", label: messages.importFile, icon: "import" }
2642
+ ];
2643
+ return specs.flatMap((spec) => {
2644
+ const action = options[spec.option];
2645
+ return action ? [fileEntry(spec, action)] : [];
2646
+ });
2647
+ }
2648
+ function appearanceMenuEntries(appearance, messages) {
2649
+ return [
2650
+ {
2651
+ kind: "radio",
2652
+ id: "appearance",
2653
+ label: messages.appearance,
2654
+ options: [
2655
+ { value: "system", label: messages.appearanceSystem },
2656
+ { value: "light", label: messages.appearanceLight },
2657
+ { value: "dark", label: messages.appearanceDark }
2658
+ ],
2659
+ value: () => appearance.choice(),
2660
+ onSelect: (value) => appearance.setChoice(parseAppearanceChoice(value))
2661
+ }
2662
+ ];
2663
+ }
2664
+
2665
+ // src/dom/web/primary.ts
2666
+ var COLLAPSED_ATTRIBUTE = "data-collapsed";
2667
+ function createPrimaryAction(context) {
2668
+ const { ownerDocument, messages, tooltip, options } = context;
2669
+ const label = options.label ?? messages.share;
2670
+ const labelElement = createElement(ownerDocument, "span", { class: "cd-shell-primary-label" }, label);
2671
+ const element = createElement(
2672
+ ownerDocument,
2673
+ "button",
2674
+ {
2675
+ type: "button",
2676
+ class: "cd-shell-primary cd-shell-button",
2677
+ "data-variant": "primary",
2678
+ "data-chrome": "primary",
2679
+ "aria-label": label
2680
+ },
2681
+ iconElement(options.icon ?? "share-2"),
2682
+ labelElement
2683
+ );
2684
+ let collapsed = false;
2685
+ let disabled = false;
2686
+ let labelledWidth;
2687
+ function readDisabled() {
2688
+ disabled = options.disabled?.() ?? false;
2689
+ toggleAttribute(element, "aria-disabled", disabled ? "true" : null);
2690
+ }
2691
+ function showCollapsed(state) {
2692
+ labelElement.hidden = state;
2693
+ toggleAttribute(element, COLLAPSED_ATTRIBUTE, state ? "" : null);
2694
+ }
2695
+ function measureLabelledWidth() {
2696
+ if (!collapsed) return element.offsetWidth;
2697
+ showCollapsed(false);
2698
+ const width = element.offsetWidth;
2699
+ showCollapsed(true);
2700
+ return width;
2701
+ }
2702
+ function isEngaged() {
2703
+ if (ownerDocument.activeElement === element) return true;
2704
+ try {
2705
+ return element.matches(":hover");
2706
+ } catch {
2707
+ return false;
2708
+ }
2709
+ }
2710
+ const onClick = (event) => {
2711
+ if (disabled) {
2712
+ event.preventDefault();
2713
+ return;
2714
+ }
2715
+ options.onSelect();
2716
+ };
2717
+ element.addEventListener("click", onClick);
2718
+ const detachTooltip = tooltip.attach(element, () => collapsed ? { label } : null);
2719
+ readDisabled();
2720
+ return {
2721
+ element,
2722
+ isCollapsed: () => collapsed,
2723
+ refresh() {
2724
+ readDisabled();
2725
+ labelledWidth = void 0;
2726
+ },
2727
+ widths() {
2728
+ if (!collapsed || labelledWidth === void 0) {
2729
+ const measured = measureLabelledWidth();
2730
+ if (measured > 0) labelledWidth = measured;
2731
+ }
2732
+ return { withLabel: labelledWidth ?? 0, iconOnly: SHELL.row };
2733
+ },
2734
+ setCollapsed(next) {
2735
+ if (next === collapsed) return;
2736
+ collapsed = next;
2737
+ showCollapsed(next);
2738
+ if (!next && isEngaged()) tooltip.hide();
2739
+ },
2740
+ destroy() {
2741
+ detachTooltip();
2742
+ element.removeEventListener("click", onClick);
2743
+ element.remove();
2744
+ }
2745
+ };
2746
+ }
2747
+
1587
2748
  // src/dom/zoom-panel.ts
1588
2749
  function createZoomPanel(context) {
1589
2750
  const { ownerDocument, messages, lang, platform, tooltip } = context;
@@ -1701,6 +2862,16 @@ function nextMountIndex() {
1701
2862
  return index;
1702
2863
  }
1703
2864
  var DEFAULT_THEMES = ["light"];
2865
+ var LEGAL_HEIGHT_PROPERTY = "--modeler-shell-legal-height";
2866
+ var INERT_TOAST = Object.freeze({ dismiss() {
2867
+ } });
2868
+ function visibleBoxIn(element, root) {
2869
+ if (!element || element.hidden || !element.isConnected) return void 0;
2870
+ if (typeof element.checkVisibility === "function" && !element.checkVisibility({ visibilityProperty: true }))
2871
+ return void 0;
2872
+ const box = boxRelativeTo(element, root);
2873
+ return box.width > 0 && box.height > 0 ? box : void 0;
2874
+ }
1704
2875
  function languageOf(root, host, requested) {
1705
2876
  if (requested) return requested;
1706
2877
  const declared = host === "embedded" ? root.getAttribute("lang") : root.closest("[lang]")?.getAttribute("lang");
@@ -1729,6 +2900,7 @@ function renderShell(root, options, themes, ledger) {
1729
2900
  const platform = options.platform ?? detectPlatform(globalThis.navigator);
1730
2901
  const lang = languageOf(root, host, options.lang);
1731
2902
  const keyScope = options.keyScope ?? (host === "embedded" ? "root" : "document");
2903
+ const web = host === "web";
1732
2904
  ledger.addClass(root, "cd-shell");
1733
2905
  ledger.attribute(root, "data-cd-shell-host", host);
1734
2906
  ledger.attribute(root, "data-palette", "top");
@@ -1741,6 +2913,9 @@ function renderShell(root, options, themes, ledger) {
1741
2913
  const setState = (name, value) => {
1742
2914
  if (!ledger.isClosed) ledger.attribute(root, name, value);
1743
2915
  };
2916
+ const appearance = web ? connectWebAppearance({ ownerDocument, themes, options: options.appearance }) : void 0;
2917
+ if (appearance) ledger.defer(() => appearance.destroy());
2918
+ const darkCapable = themes.includes("dark");
1744
2919
  const layer = createElement(ownerDocument, "div", { class: "cd-shell-layer" });
1745
2920
  ledger.defer(() => layer.remove());
1746
2921
  const tooltip = createTooltip({ root, layer, id: `cd-shell-tooltip-${index}` });
@@ -1772,6 +2947,14 @@ function renderShell(root, options, themes, ledger) {
1772
2947
  keepEnabledWhenReadOnly: true
1773
2948
  } : void 0;
1774
2949
  const shellModelEntries = legendEntry ? [legendEntry] : [];
2950
+ const fileEntries = web && options.file ? fileMenuEntries(options.file, messages) : [];
2951
+ const appearanceEntries = appearance && darkCapable ? appearanceMenuEntries(appearance, messages) : [];
2952
+ const shellEntries = (group) => {
2953
+ if (group === "file") return fileEntries;
2954
+ if (group === "model") return shellModelEntries;
2955
+ if (group === "appearance") return appearanceEntries;
2956
+ return [];
2957
+ };
1775
2958
  const menu = createMenu({
1776
2959
  root,
1777
2960
  layer,
@@ -1779,55 +2962,133 @@ function renderShell(root, options, themes, ledger) {
1779
2962
  messages,
1780
2963
  tooltip,
1781
2964
  groups: options.menu ?? {},
1782
- shellModelEntries: () => shellModelEntries,
2965
+ shellEntries,
1783
2966
  isReadOnly: () => readOnly
1784
2967
  });
1785
2968
  ledger.defer(() => menu.destroy());
1786
2969
  ledger.defer(tooltip.attach(menu.trigger, () => menu.isOpen() ? null : { label: messages.menu }));
2970
+ const primary = web && options.primary ? createPrimaryAction({ ownerDocument, messages, tooltip, options: options.primary }) : void 0;
2971
+ if (primary) ledger.defer(() => primary.destroy());
1787
2972
  const canvas = options.canvas ?? createElement(ownerDocument, "div");
1788
2973
  ledger.addClass(canvas, "cd-shell-canvas");
2974
+ if (web && !canvas.hasAttribute("role")) ledger.attribute(canvas, "role", "main");
1789
2975
  if (!options.canvas) ledger.defer(() => canvas.remove());
2976
+ function focusCanvasOrTrigger() {
2977
+ const took = (target) => {
2978
+ target?.focus({ preventScroll: true });
2979
+ return target !== null && target !== void 0 && ownerDocument.activeElement === target;
2980
+ };
2981
+ if (emptyState?.isShown()) {
2982
+ emptyState.focus();
2983
+ if (emptyState.element.contains(ownerDocument.activeElement)) return;
2984
+ }
2985
+ if (took(canvas.querySelector("svg[tabindex]"))) return;
2986
+ if (!menu.trigger.hidden) took(menu.trigger);
2987
+ }
1790
2988
  function moveFocusOutOf(island) {
1791
- if (!island.contains(ownerDocument.activeElement)) return;
1792
- const canvasTarget = canvas.querySelector("svg[tabindex]");
1793
- canvasTarget?.focus({ preventScroll: true });
1794
- if (canvasTarget && ownerDocument.activeElement === canvasTarget) return;
1795
- if (!menu.trigger.hidden) menu.trigger.focus({ preventScroll: true });
2989
+ if (island.contains(ownerDocument.activeElement)) focusCanvasOrTrigger();
1796
2990
  }
1797
2991
  const status = createStatusBox(ownerDocument);
1798
2992
  ledger.defer(() => status.destroy());
1799
2993
  const hint = options.emptyHint ? createEmptyHint(ownerDocument, messages, options.emptyHint) : void 0;
1800
2994
  if (hint) ledger.defer(() => hint.destroy());
2995
+ const emptyState = web && options.emptyState ? createEmptyState({
2996
+ ownerDocument,
2997
+ messages,
2998
+ options: options.emptyState,
2999
+ dropHint: options.dropZone ? options.emptyState.dropHint ?? messages.dropHint : void 0,
3000
+ open: options.file?.open,
3001
+ titleId: `cd-shell-empty-state-title-${index}`,
3002
+ onLeave: () => setEmptyStateShown(false)
3003
+ }) : void 0;
3004
+ if (emptyState) ledger.defer(() => emptyState.destroy());
1801
3005
  const inspector = options.inspector ? createInspector(ownerDocument, options.inspector, (open) => {
1802
3006
  setState("data-inspector", open ? "open" : null);
3007
+ updateBottomRow();
1803
3008
  placeHint();
1804
3009
  }) : void 0;
1805
3010
  if (inspector) ledger.defer(() => inspector.destroy());
1806
- const pill = options.problems ? createProblemsPill({
3011
+ const problemsOptions = options.problems;
3012
+ let popover;
3013
+ const pill = problemsOptions ? createProblemsPill({
1807
3014
  ownerDocument,
1808
3015
  messages,
1809
3016
  lang,
1810
3017
  tooltip,
1811
3018
  showTooltip: host === "ide",
1812
- options: options.problems
3019
+ onActivate() {
3020
+ if (popover) popover.toggle();
3021
+ else problemsOptions.onActivate?.();
3022
+ }
1813
3023
  }) : void 0;
1814
3024
  if (pill) ledger.defer(() => pill.destroy());
3025
+ if (pill && (web || host === "embedded" && !problemsOptions?.onActivate)) {
3026
+ const created = createProblemsPopover({
3027
+ ownerDocument,
3028
+ root,
3029
+ layer,
3030
+ pill: pill.element,
3031
+ id: `cd-shell-problems-${index}`,
3032
+ titleId: `cd-shell-problems-title-${index}`,
3033
+ messages,
3034
+ onSelectItem: problemsOptions?.onSelectItem
3035
+ });
3036
+ popover = created;
3037
+ ledger.defer(() => created.destroy());
3038
+ }
3039
+ const legal = web ? createLegalLine({
3040
+ ownerDocument,
3041
+ messages,
3042
+ options: options.legal ?? {},
3043
+ idPrefix: `cd-shell-legal-${index}`
3044
+ }) : void 0;
3045
+ if (legal) ledger.defer(() => legal.destroy());
1815
3046
  const zoomPanel = createZoomPanel({ ownerDocument, messages, lang, platform, tooltip });
1816
3047
  ledger.defer(() => zoomPanel.destroy());
1817
- root.prepend(menu.trigger, canvas);
3048
+ const toaster = host === "ide" ? void 0 : createToaster({
3049
+ ownerDocument,
3050
+ host,
3051
+ messages,
3052
+ idPrefix: `cd-shell-toast-${index}`,
3053
+ focusFallback: focusCanvasOrTrigger,
3054
+ onChange: () => updateBottomRow()
3055
+ });
3056
+ if (toaster) ledger.defer(() => toaster.destroy());
3057
+ const defined = (element) => element !== void 0;
3058
+ root.prepend(...[menu.trigger, primary?.element, canvas].filter(defined));
1818
3059
  root.append(
1819
3060
  ...[
1820
3061
  status.element,
1821
3062
  hint?.element,
3063
+ emptyState?.element,
1822
3064
  inspector?.element,
1823
3065
  pill?.element,
1824
3066
  legend?.element,
3067
+ legal?.element,
1825
3068
  zoomPanel.element
1826
- ].filter((element) => element !== void 0),
1827
- layer
3069
+ ].filter(defined),
3070
+ layer,
3071
+ ...[toaster?.announcer, toaster?.element].filter(defined)
1828
3072
  );
3073
+ function showToast(request) {
3074
+ return ledger.isClosed || !toaster ? INERT_TOAST : toaster.show(request);
3075
+ }
3076
+ const dropZone = web && options.dropZone ? createDropZone({
3077
+ ownerDocument,
3078
+ layer,
3079
+ messages,
3080
+ lang,
3081
+ options: options.dropZone,
3082
+ modalOpen: () => isModalOpen(ownerDocument),
3083
+ toast: showToast,
3084
+ setState: (state) => setState("data-drop", state)
3085
+ }) : void 0;
3086
+ if (dropZone) ledger.defer(() => dropZone.destroy());
1829
3087
  let palette;
1830
3088
  let paletteRow = "top";
3089
+ let zoomRaised = false;
3090
+ let legalHeight = LEGAL_LINE_HEIGHT;
3091
+ if (legal) ledger.defer(() => root.style.removeProperty(LEGAL_HEIGHT_PROPERTY));
1831
3092
  function containerSize() {
1832
3093
  return { width: root.clientWidth, height: root.clientHeight };
1833
3094
  }
@@ -1847,6 +3108,10 @@ function renderShell(root, options, themes, ledger) {
1847
3108
  paletteRow,
1848
3109
  problemsVisible: pill?.isShown() ?? false
1849
3110
  };
3111
+ if (web) {
3112
+ layout.zoomRaised = zoomRaised;
3113
+ layout.legalHeight = legalHeight;
3114
+ }
1850
3115
  if (legend?.isOpen()) layout.legendWidth = legend.element.getBoundingClientRect().width;
1851
3116
  if (inspector?.isOpen()) {
1852
3117
  layout.inspector = container.width > SHELL.narrow ? {} : { sheetTop: boxRelativeTo(inspector.element, root).y };
@@ -1860,18 +3125,76 @@ function renderShell(root, options, themes, ledger) {
1860
3125
  if (ledger.isClosed) return;
1861
3126
  const size = containerSize();
1862
3127
  if (size.width === 0 || size.height === 0) return;
1863
- paletteRow = decidePaletteRow({
3128
+ const topRow = decideTopRow({
1864
3129
  containerWidth: size.width,
1865
3130
  paletteWidth: paletteWidth(),
1866
- menuRight: menuRight()
3131
+ menuRight: menuRight(),
3132
+ primary: primary?.widths()
1867
3133
  });
3134
+ paletteRow = topRow.paletteRow;
1868
3135
  setState("data-palette", paletteRow);
3136
+ if (primary) {
3137
+ const collapsed = topRow.primaryLabel === "collapsed";
3138
+ primary.setCollapsed(collapsed);
3139
+ setState("data-primary", collapsed ? "collapsed" : null);
3140
+ }
1869
3141
  if (legend && !legend.isStarted()) legend.start(options.legend?.open ?? legendStartsOpen(size.height));
3142
+ updateBottomRow();
1870
3143
  placeHint();
1871
3144
  }
3145
+ function updateBottomRow() {
3146
+ if (ledger.isClosed) return;
3147
+ const size = containerSize();
3148
+ if (size.width === 0 || size.height === 0) return;
3149
+ if (legal) {
3150
+ const legalBox = visibleBoxIn(legal.element, root);
3151
+ const height = legalLineHeight(legalBox?.height);
3152
+ if (height !== legalHeight || !root.style.getPropertyValue(LEGAL_HEIGHT_PROPERTY)) {
3153
+ legalHeight = height;
3154
+ root.style.setProperty(LEGAL_HEIGHT_PROPERTY, `${height}px`);
3155
+ }
3156
+ zoomRaised = zoomRowRaised({
3157
+ containerWidth: size.width,
3158
+ legalRight: legalBox && legalBox.x + legalBox.width,
3159
+ zoomWidth: visibleBoxIn(zoomPanel.element, root)?.width
3160
+ });
3161
+ setState("data-zoom-row", zoomRaised ? "raised" : null);
3162
+ }
3163
+ if (toaster) {
3164
+ const obstacles = [zoomPanel.element, legal?.element, pill?.element].flatMap(
3165
+ (element) => {
3166
+ const box = visibleBoxIn(element, root);
3167
+ return box ? [{ left: box.x, right: box.x + box.width, top: size.height - box.y }] : [];
3168
+ }
3169
+ );
3170
+ toaster.setBottom(
3171
+ toastStackBottom({ containerWidth: size.width, obstacles, cardWidth: toaster.widestCard() })
3172
+ );
3173
+ }
3174
+ }
3175
+ function setEmptyStateShown(shown) {
3176
+ if (ledger.isClosed || !emptyState || emptyState.isShown() === shown) return;
3177
+ const active = ownerDocument.activeElement;
3178
+ const hadFocus = emptyState.element.contains(active);
3179
+ const workHadFocus = shown && active !== null && root.contains(active) && ![emptyState.element, legal?.element, toaster?.element].some((kept) => kept?.contains(active));
3180
+ emptyState.setShown(shown);
3181
+ setState("data-empty-state", shown ? "shown" : null);
3182
+ ledger.attribute(canvas, "inert", shown ? "" : null);
3183
+ if (shown) {
3184
+ menu.close();
3185
+ popover?.close();
3186
+ tooltip.hide();
3187
+ if (workHadFocus) emptyState.focus();
3188
+ } else if (hadFocus) {
3189
+ focusCanvasOrTrigger();
3190
+ }
3191
+ updateLayout();
3192
+ }
1872
3193
  const ResizeObserverClass = globalThis.ResizeObserver;
1873
3194
  const resizeObserver = ResizeObserverClass ? new ResizeObserverClass(() => updateLayout()) : void 0;
1874
3195
  resizeObserver?.observe(root);
3196
+ if (primary) resizeObserver?.observe(primary.element);
3197
+ if (legal) resizeObserver?.observe(legal.element);
1875
3198
  ledger.defer(() => resizeObserver?.disconnect());
1876
3199
  if (legend && options.legend?.open !== void 0) legend.start(options.legend.open);
1877
3200
  updateLayout();
@@ -1880,12 +3203,16 @@ function renderShell(root, options, themes, ledger) {
1880
3203
  host,
1881
3204
  dialogScope: keyTarget,
1882
3205
  menuOpen: () => menu.isOpen(),
3206
+ startScreen: () => emptyState?.isShown() ?? false,
1883
3207
  closeMenu() {
1884
3208
  const active = ownerDocument.activeElement;
1885
3209
  menu.close({ restoreFocus: active === menu.trigger || menu.dropdown.contains(active) });
1886
3210
  },
1887
3211
  hideTooltip: () => tooltip.hide(),
1888
- run: (key) => zoomPanel.run(key)
3212
+ // Behind the start screen the keys reach the browser instead: there is no diagram to zoom.
3213
+ run: (key) => {
3214
+ if (!emptyState?.isShown()) zoomPanel.run(key);
3215
+ }
1889
3216
  });
1890
3217
  keyTarget.addEventListener("keydown", onKeydown);
1891
3218
  ledger.defer(() => keyTarget.removeEventListener("keydown", onKeydown));
@@ -1937,6 +3264,21 @@ function renderShell(root, options, themes, ledger) {
1937
3264
  if (!ledger.isClosed) legend.toggle();
1938
3265
  }
1939
3266
  };
3267
+ const primaryHandle = primary && {
3268
+ element: primary.element,
3269
+ isCollapsed: () => primary.isCollapsed(),
3270
+ refresh: () => {
3271
+ if (!ledger.isClosed) primary.refresh();
3272
+ }
3273
+ };
3274
+ const appearanceHandle = appearance && darkCapable ? {
3275
+ choice: () => appearance.choice(),
3276
+ setChoice: (choice) => {
3277
+ if (ledger.isClosed) return;
3278
+ appearance.setChoice(choice);
3279
+ menu.refresh();
3280
+ }
3281
+ } : void 0;
1940
3282
  const inspectorHandle = inspector && {
1941
3283
  element: inspector.element,
1942
3284
  isOpen: () => inspector.isOpen(),
@@ -1960,6 +3302,8 @@ function renderShell(root, options, themes, ledger) {
1960
3302
  tooltip: tooltipHandle,
1961
3303
  legend: legendHandle,
1962
3304
  inspector: inspectorHandle,
3305
+ primary: primaryHandle,
3306
+ appearance: appearanceHandle,
1963
3307
  setZoomTarget(target) {
1964
3308
  if (!ledger.isClosed) zoomPanel.setTarget(target);
1965
3309
  },
@@ -1988,8 +3332,15 @@ function renderShell(root, options, themes, ledger) {
1988
3332
  },
1989
3333
  setProblems(summary) {
1990
3334
  if (ledger.isClosed || !pill) return;
3335
+ const listHadFocus = popover?.element.contains(ownerDocument.activeElement) ?? false;
1991
3336
  pill.set(summary);
1992
3337
  setState("data-problems", pill.isShown() ? "shown" : null);
3338
+ popover?.setItems(summary.items ?? []);
3339
+ if (!pill.isShown() && popover?.isOpen()) {
3340
+ popover.close();
3341
+ if (listHadFocus) focusCanvasOrTrigger();
3342
+ }
3343
+ updateBottomRow();
1993
3344
  placeHint();
1994
3345
  },
1995
3346
  setStatus(next, statusOptions) {
@@ -2019,6 +3370,9 @@ function renderShell(root, options, themes, ledger) {
2019
3370
  applyReadOnly();
2020
3371
  },
2021
3372
  isReadOnly: () => readOnly,
3373
+ setEmptyState: setEmptyStateShown,
3374
+ isEmptyStateShown: () => emptyState?.isShown() ?? false,
3375
+ toast: showToast,
2022
3376
  destroy() {
2023
3377
  ledger.close();
2024
3378
  }
@@ -2077,6 +3431,12 @@ function withDestroyHook(shell, beforeDestroy) {
2077
3431
  get inspector() {
2078
3432
  return shell.inspector;
2079
3433
  },
3434
+ get primary() {
3435
+ return shell.primary;
3436
+ },
3437
+ get appearance() {
3438
+ return shell.appearance;
3439
+ },
2080
3440
  setZoomTarget: (target) => shell.setZoomTarget(target),
2081
3441
  fitInsets: () => shell.fitInsets(),
2082
3442
  registerPalette: (palette) => shell.registerPalette(palette),
@@ -2085,6 +3445,9 @@ function withDestroyHook(shell, beforeDestroy) {
2085
3445
  setEmptyHint: (visible) => shell.setEmptyHint(visible),
2086
3446
  setReadOnly: (readOnly, options) => shell.setReadOnly(readOnly, options),
2087
3447
  isReadOnly: () => shell.isReadOnly(),
3448
+ setEmptyState: (shown) => shell.setEmptyState(shown),
3449
+ isEmptyStateShown: () => shell.isEmptyStateShown(),
3450
+ toast: (request) => shell.toast(request),
2088
3451
  destroy() {
2089
3452
  beforeDestroy();
2090
3453
  shell.destroy();