@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
package/dist/index.cjs CHANGED
@@ -20,21 +20,27 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
+ APPEARANCE_STORAGE_KEY: () => APPEARANCE_STORAGE_KEY,
23
24
  CHROME_ISLANDS: () => CHROME_ISLANDS,
24
25
  FIT_ANIMATION_MS: () => FIT_ANIMATION_MS,
25
26
  ICONS: () => ICONS,
26
27
  INVALID_STATUS_DELAY_MS: () => INVALID_STATUS_DELAY_MS,
28
+ LEGAL_LINE_HEIGHT: () => LEGAL_LINE_HEIGHT,
27
29
  MAX_FIT_ZOOM: () => MAX_FIT_ZOOM,
30
+ MAX_TOASTS: () => MAX_TOASTS,
28
31
  PALETTE_CLEARANCE: () => PALETTE_CLEARANCE,
29
32
  PALETTE_TOOL_ICON: () => PALETTE_TOOL_ICON,
30
33
  REDUCED_MOTION_CLASS: () => REDUCED_MOTION_CLASS,
31
34
  SHELL: () => SHELL,
32
35
  SHELL_TOKENS: () => SHELL_TOKENS,
33
36
  TEXT_FIELD_SELECTOR: () => TEXT_FIELD_SELECTOR,
37
+ TOAST_DURATION_MS: () => TOAST_DURATION_MS,
38
+ TOAST_MAX_WIDTH: () => TOAST_MAX_WIDTH,
34
39
  TOOLTIP_DELAY_MS: () => TOOLTIP_DELAY_MS,
35
40
  VIEW_TOLERANCE: () => VIEW_TOLERANCE,
36
41
  ZOOM_MAX: () => ZOOM_MAX,
37
42
  ZOOM_MIN: () => ZOOM_MIN,
43
+ acceptsFile: () => acceptsFile,
38
44
  appearanceAttributes: () => appearanceAttributes,
39
45
  appearanceFromBodyClasses: () => appearanceFromBodyClasses,
40
46
  appearanceFromColorThemeKind: () => appearanceFromColorThemeKind,
@@ -42,10 +48,14 @@ __export(src_exports, {
42
48
  ariaKeyShortcutsFromLabel: () => ariaKeyShortcutsFromLabel,
43
49
  canZoomIn: () => canZoomIn,
44
50
  canZoomOut: () => canZoomOut,
51
+ commandKeyLabel: () => commandKeyLabel,
45
52
  decideKey: () => decideKey,
46
53
  decidePaletteRow: () => decidePaletteRow,
54
+ decideTopRow: () => decideTopRow,
47
55
  defaultMessages: () => defaultMessages,
48
56
  detectPlatform: () => detectPlatform,
57
+ documentMode: () => documentMode,
58
+ dragVerdict: () => dragVerdict,
49
59
  easeStandard: () => easeStandard,
50
60
  fitInsets: () => fitInsets,
51
61
  fitViewbox: () => fitViewbox,
@@ -56,10 +66,13 @@ __export(src_exports, {
56
66
  iconMarkup: () => iconMarkup,
57
67
  interpolateViewbox: () => interpolateViewbox,
58
68
  isUndoRedoKey: () => isUndoRedoKey,
69
+ legalLineHeight: () => legalLineHeight,
59
70
  legendStartsOpen: () => legendStartsOpen,
60
71
  matchChromeKey: () => matchChromeKey,
61
72
  mountShell: () => mountShell,
73
+ parseAppearanceChoice: () => parseAppearanceChoice,
62
74
  placeTooltip: () => placeTooltip,
75
+ problemSummary: () => problemSummary,
63
76
  reducedMotionFromBodyClasses: () => reducedMotionFromBodyClasses,
64
77
  requestedAppearance: () => requestedAppearance,
65
78
  resolveAppearance: () => resolveAppearance,
@@ -67,7 +80,14 @@ __export(src_exports, {
67
80
  sameView: () => sameView,
68
81
  shellSizesFromTokens: () => shellSizesFromTokens,
69
82
  shortcutLabel: () => shortcutLabel,
70
- zoomReadout: () => zoomReadout
83
+ toastLifetime: () => toastLifetime,
84
+ toastStackBottom: () => toastStackBottom,
85
+ toastStackWidth: () => toastStackWidth,
86
+ toastToEvict: () => toastToEvict,
87
+ webHeadMarkup: () => webHeadMarkup,
88
+ webModeScript: () => webModeScript,
89
+ zoomReadout: () => zoomReadout,
90
+ zoomRowRaised: () => zoomRowRaised
71
91
  });
72
92
  module.exports = __toCommonJS(src_exports);
73
93
 
@@ -80,7 +100,12 @@ var CHROME_ISLANDS = [
80
100
  "problems",
81
101
  "legend",
82
102
  "inspector",
83
- "status"
103
+ "status",
104
+ "legal",
105
+ "toasts",
106
+ "empty-state",
107
+ "drop-zone",
108
+ "problems-popover"
84
109
  ];
85
110
 
86
111
  // src/core/sizes.ts
@@ -151,6 +176,19 @@ function shellSizesFromTokens(shellGroup) {
151
176
  return sizes;
152
177
  }
153
178
 
179
+ // src/core/web-chrome/bottom-row.ts
180
+ var LEGAL_LINE_HEIGHT = 20;
181
+ function legalLineHeight(measured) {
182
+ if (measured === void 0 || !Number.isFinite(measured) || measured <= 0) return LEGAL_LINE_HEIGHT;
183
+ return Math.max(1, Math.round(measured / LEGAL_LINE_HEIGHT)) * LEGAL_LINE_HEIGHT;
184
+ }
185
+ function zoomRowRaised(input) {
186
+ const { containerWidth, legalRight, zoomWidth } = input;
187
+ if (legalRight === void 0 || zoomWidth === void 0) return false;
188
+ const zoomLeft = containerWidth - SHELL.edge - zoomWidth;
189
+ return legalRight + SHELL.gap > zoomLeft;
190
+ }
191
+
154
192
  // src/core/fit.ts
155
193
  var MAX_FIT_ZOOM = 1;
156
194
  var FIT_ANIMATION_MS = 250;
@@ -191,11 +229,13 @@ function freeRectCenter(container, insets) {
191
229
  };
192
230
  }
193
231
  function fitInsets(layout) {
194
- const { container, host, paletteRow, problemsVisible, legendWidth, inspector } = layout;
232
+ const { container, host, paletteRow, problemsVisible, zoomRaised, legendWidth, inspector } = layout;
233
+ const raisedBottomRow = host === "web" && (problemsVisible || zoomRaised === true);
234
+ const wrappedLegalLines = legalLineHeight(layout.legalHeight) - LEGAL_LINE_HEIGHT;
195
235
  const insets = {
196
236
  top: paletteRow === "second-row" ? SHELL.fitInsetTopSecondRow : SHELL.fitInsetTop,
197
237
  right: SHELL.fitInsetSide,
198
- bottom: host === "web" && problemsVisible ? SHELL.fitInsetBottomWeb : SHELL.fitInsetBottom,
238
+ bottom: raisedBottomRow ? SHELL.fitInsetBottomWeb + wrappedLegalLines : SHELL.fitInsetBottom,
199
239
  left: SHELL.fitInsetSide
200
240
  };
201
241
  const bottomSheetOpen = inspector !== void 0 && container.width <= SHELL.narrow;
@@ -349,6 +389,7 @@ function decideKey(event, context) {
349
389
  }
350
390
  const chromeKey = matchChromeKey(event);
351
391
  if (chromeKey !== void 0) {
392
+ if (context.startScreen === true && !stop) return { preventDefault: false, stopPropagation: false };
352
393
  const blocked = composing || context.inTextField || context.modalOpen;
353
394
  if (!blocked) return { action: chromeKey, preventDefault: true, stopPropagation: stop };
354
395
  return { preventDefault: false, stopPropagation: chromeKey === "fit" ? false : stop };
@@ -374,6 +415,9 @@ function shortcutLabel(key, platform, keyNames) {
374
415
  return `${keyNames.shift}+1`;
375
416
  }
376
417
  }
418
+ function commandKeyLabel(key, platform, keyNames) {
419
+ return platform === "mac" ? `\u2318${key}` : `${keyNames.ctrl}+${key}`;
420
+ }
377
421
  var MAC_SHORTCUT_LABELS = {
378
422
  zoomIn: "\u2318+",
379
423
  zoomOut: "\u2318\u2212",
@@ -536,7 +580,27 @@ var defaultMessages = {
536
580
  emptyHint: "Pick a tool from the palette to start.",
537
581
  insertExample: "Insert example",
538
582
  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.`,
539
- keyNames: { ctrl: "Ctrl", shift: "Shift", plus: "Plus", minus: "Minus" }
583
+ keyNames: { ctrl: "Ctrl", shift: "Shift", plus: "Plus", minus: "Minus" },
584
+ share: "Share",
585
+ legal: "Legal",
586
+ copyright: (year) => `\xA9 ${year} Miragon GmbH`,
587
+ opensInNewTab: "Opens in a new tab",
588
+ newDiagram: "New diagram",
589
+ openFile: "Open\u2026",
590
+ showExample: "Show example",
591
+ importFile: "Import\u2026",
592
+ appearance: "Appearance",
593
+ appearanceSystem: "System",
594
+ appearanceLight: "Light",
595
+ appearanceDark: "Dark",
596
+ problemsTitle: "Problems",
597
+ problemSeverity: (severity) => severity === "error" ? "Error" : "Warning",
598
+ close: "Close",
599
+ dropFile: "Drop file here",
600
+ 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",
601
+ dropHint: "Or drop a file here",
602
+ openFileLink: "Open a file\u2026",
603
+ cannotOpenFile: (name, accepted) => `Couldn't open \u201C${name}\u201D. Drop a ${accepted} file.`
540
604
  };
541
605
  function resolveMessages(overrides) {
542
606
  const { keyNames, ...texts } = overrides ?? {};
@@ -576,6 +640,176 @@ function placeTooltip(input) {
576
640
  return { x, y };
577
641
  }
578
642
 
643
+ // src/core/web-chrome/top-row.ts
644
+ function decideTopRow(input) {
645
+ const { containerWidth, paletteWidth, menuRight, primary } = input;
646
+ if (!primary) {
647
+ return {
648
+ paletteRow: decidePaletteRow({ containerWidth, paletteWidth, menuRight }),
649
+ primaryLabel: "shown"
650
+ };
651
+ }
652
+ const paletteRowBeside = (primaryWidth) => decidePaletteRow({
653
+ containerWidth,
654
+ paletteWidth,
655
+ menuRight,
656
+ primaryLeft: containerWidth - SHELL.edge - primaryWidth
657
+ });
658
+ if (paletteRowBeside(primary.withLabel) === "top") return { paletteRow: "top", primaryLabel: "shown" };
659
+ if (paletteRowBeside(primary.iconOnly) === "top") return { paletteRow: "top", primaryLabel: "collapsed" };
660
+ const labelledLeft = containerWidth - SHELL.edge - primary.withLabel;
661
+ return {
662
+ paletteRow: "second-row",
663
+ primaryLabel: menuRight + PALETTE_CLEARANCE <= labelledLeft ? "shown" : "collapsed"
664
+ };
665
+ }
666
+
667
+ // src/core/web-chrome/drop.ts
668
+ function acceptEntries(accept) {
669
+ const entries = accept.map((entry) => entry.trim().toLowerCase()).filter((entry) => entry !== "");
670
+ return {
671
+ extensions: entries.filter((entry) => entry.startsWith(".")),
672
+ mimeTypes: entries.filter((entry) => !entry.startsWith(".") && entry.includes("/"))
673
+ };
674
+ }
675
+ function matchesMimeType(type, mimeTypes) {
676
+ const normalized = type.trim().toLowerCase();
677
+ if (normalized === "") return false;
678
+ return mimeTypes.some(
679
+ (entry) => entry.endsWith("/*") ? normalized.startsWith(entry.slice(0, -1)) : normalized === entry
680
+ );
681
+ }
682
+ function dragVerdict(items, context) {
683
+ const files = items.filter((item) => item.kind === "file");
684
+ if (files.length === 0) return { state: "none" };
685
+ if (context.dialogOpen) return { state: "forbidden", reason: "dialog" };
686
+ if (files.length > 1 && !context.multiple) return { state: "forbidden", reason: "multiple" };
687
+ const { mimeTypes } = acceptEntries(context.accept);
688
+ const foreignType = (item) => item.type.trim() !== "" && !matchesMimeType(item.type, mimeTypes);
689
+ if (mimeTypes.length > 0 && files.every(foreignType)) return { state: "forbidden", reason: "type" };
690
+ return { state: "over" };
691
+ }
692
+ function acceptsFile(file, accept) {
693
+ const { extensions, mimeTypes } = acceptEntries(accept);
694
+ if (extensions.length === 0 && mimeTypes.length === 0) return true;
695
+ const name = file.name.toLowerCase();
696
+ return extensions.some((extension) => name.endsWith(extension)) || matchesMimeType(file.type, mimeTypes);
697
+ }
698
+
699
+ // src/core/feedback/toasts.ts
700
+ var MAX_TOASTS = 3;
701
+ var TOAST_DURATION_MS = {
702
+ success: 2800,
703
+ info: 2800,
704
+ warning: 5e3,
705
+ danger: 5e3
706
+ };
707
+ var TOAST_MAX_WIDTH = 480;
708
+ function toastLifetime(request) {
709
+ if (request.action) return void 0;
710
+ const requested = request.durationMs;
711
+ const longer = requested !== void 0 && Number.isFinite(requested) ? requested : 0;
712
+ return Math.max(TOAST_DURATION_MS[request.status], longer);
713
+ }
714
+ function toastToEvict(stack) {
715
+ if (stack.length < MAX_TOASTS) return void 0;
716
+ const evicted = stack.find((slot) => !slot.engaged && !slot.hasAction) ?? stack.find((slot) => !slot.engaged) ?? stack[0];
717
+ return evicted?.id;
718
+ }
719
+ function toastStackWidth(containerWidth) {
720
+ return Math.max(0, Math.min(TOAST_MAX_WIDTH, containerWidth - 2 * SHELL.edge));
721
+ }
722
+ function toastStackBottom(input) {
723
+ const stackWidth = toastStackWidth(input.containerWidth);
724
+ const { cardWidth } = input;
725
+ const width = cardWidth !== void 0 && Number.isFinite(cardWidth) ? Math.min(stackWidth, Math.max(0, cardWidth)) : stackWidth;
726
+ const left = (input.containerWidth - width) / 2;
727
+ const right = left + width;
728
+ let highest;
729
+ for (const obstacle of input.obstacles) {
730
+ const distance = Math.max(obstacle.left - right, left - obstacle.right);
731
+ if (distance >= SHELL.gap) continue;
732
+ highest = highest === void 0 ? obstacle.top : Math.max(highest, obstacle.top);
733
+ }
734
+ return highest === void 0 ? SHELL.edge : Math.max(SHELL.edge, highest + SHELL.gap);
735
+ }
736
+
737
+ // src/core/feedback/appearance.ts
738
+ var APPEARANCE_STORAGE_KEY = "miragon.darstellung";
739
+ function parseAppearanceChoice(stored) {
740
+ return stored === "light" || stored === "dark" ? stored : "system";
741
+ }
742
+ function documentMode(choice, themes, colors) {
743
+ if (!themes.includes("dark")) return fixedMode("light", colors.light);
744
+ const parsed = parseAppearanceChoice(choice);
745
+ if (parsed === "system") {
746
+ return {
747
+ theme: "system",
748
+ colorScheme: "light dark",
749
+ themeColor: { light: colors.light, dark: colors.dark }
750
+ };
751
+ }
752
+ return fixedMode(parsed, colors[parsed]);
753
+ }
754
+ function fixedMode(theme, color) {
755
+ return { theme, colorScheme: theme, themeColor: { light: color, dark: color } };
756
+ }
757
+ function webModeScript(options) {
758
+ const contrast = [
759
+ `if (window.matchMedia && window.matchMedia(${scriptString("(prefers-contrast: more)")}).matches)`,
760
+ ` document.documentElement.setAttribute("data-cd-kontrast", "hoch");`
761
+ ];
762
+ if (!options.themes.includes("dark")) return contrast.join("\n");
763
+ const { light, dark } = options.themeColors;
764
+ return [
765
+ "(function () {",
766
+ " var html = document.documentElement;",
767
+ " var choice = null;",
768
+ " try {",
769
+ ` choice = window.localStorage.getItem(${scriptString(APPEARANCE_STORAGE_KEY)});`,
770
+ " } catch (error) {}",
771
+ ` if (choice === "light" || choice === "dark") {`,
772
+ ` var color = choice === "dark" ? ${scriptString(dark)} : ${scriptString(light)};`,
773
+ ` html.setAttribute("data-cd-theme", choice);`,
774
+ ` var scheme = document.querySelector("meta[name=color-scheme]");`,
775
+ ` if (scheme) scheme.setAttribute("content", choice);`,
776
+ ` var metas = document.querySelectorAll("meta[name=theme-color]");`,
777
+ ` for (var index = 0; index < metas.length; index++) metas[index].setAttribute("content", color);`,
778
+ " }",
779
+ ...contrast.map((line) => ` ${line}`),
780
+ "})();"
781
+ ].join("\n");
782
+ }
783
+ function scriptString(value) {
784
+ return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
785
+ }
786
+ function webHeadMarkup(options) {
787
+ const mode = documentMode("system", options.themes, options.themeColors);
788
+ const script = `<script>
789
+ ${webModeScript(options)}
790
+ </script>`;
791
+ if (mode.theme !== "system") {
792
+ return [`<meta name="theme-color" content="${escapeAttribute(mode.themeColor.light)}">`, script].join(
793
+ "\n"
794
+ );
795
+ }
796
+ return [
797
+ `<meta name="color-scheme" content="${escapeAttribute(mode.colorScheme)}">`,
798
+ `<meta name="theme-color" content="${escapeAttribute(mode.themeColor.light)}" media="(prefers-color-scheme: light)">`,
799
+ `<meta name="theme-color" content="${escapeAttribute(mode.themeColor.dark)}" media="(prefers-color-scheme: dark)">`,
800
+ script
801
+ ].join("\n");
802
+ }
803
+ function escapeAttribute(value) {
804
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
805
+ }
806
+
807
+ // src/core/feedback/problems.ts
808
+ function problemSummary(items) {
809
+ const errors = items.filter((item) => item.severity === "error").length;
810
+ return { count: items.length, errors, items };
811
+ }
812
+
579
813
  // src/dom/element.ts
580
814
  function createElement(ownerDocument, tag, attributes = {}, ...children) {
581
815
  const element = ownerDocument.createElement(tag);
@@ -613,163 +847,134 @@ function hasVisibleFocus(element) {
613
847
  }
614
848
  }
615
849
 
616
- // src/dom/hint.ts
617
- function createEmptyHint(ownerDocument, messages, options) {
618
- const { onInsertExample } = options;
619
- const action = onInsertExample ? createElement(
620
- ownerDocument,
621
- "button",
622
- { type: "button", class: "cd-shell-hint-action" },
623
- messages.insertExample
624
- ) : void 0;
625
- action?.addEventListener("click", () => onInsertExample?.());
626
- const element = createElement(
627
- ownerDocument,
628
- "div",
629
- { class: "cd-shell-hint" },
630
- createElement(ownerDocument, "p", { class: "cd-shell-hint-text" }, messages.emptyHint),
631
- action
632
- );
633
- element.hidden = true;
634
- return {
635
- element,
636
- isShown: () => !element.hidden,
637
- setShown(shown) {
638
- element.hidden = !shown;
639
- },
640
- place({ top, right, bottom, left }) {
641
- element.style.inset = `${top}px ${right}px ${bottom}px ${left}px`;
642
- },
643
- destroy() {
644
- element.remove();
645
- }
646
- };
647
- }
648
-
649
- // src/dom/inspector.ts
650
- function createInspector(ownerDocument, options, onChange) {
651
- const element = createElement(ownerDocument, "aside", {
652
- class: "cd-shell-inspector",
653
- "data-chrome": "inspector",
654
- "aria-label": options.label
655
- });
656
- element.hidden = true;
657
- function setOpen(open) {
658
- if (open === !element.hidden) return;
659
- element.hidden = !open;
660
- onChange(open);
850
+ // src/dom/feedback/appearance.ts
851
+ var NO_TRANSITION_CLASS = "cd-shell-no-transition";
852
+ var CONTRAST_QUERY = "(prefers-contrast: more)";
853
+ var SCHEME_MEDIA = {
854
+ light: "(prefers-color-scheme: light)",
855
+ dark: "(prefers-color-scheme: dark)"
856
+ };
857
+ function onMediaChange(query, listener) {
858
+ if (typeof query.addEventListener === "function") {
859
+ query.addEventListener("change", listener);
860
+ return () => query.removeEventListener("change", listener);
861
+ }
862
+ query.addListener(listener);
863
+ return () => query.removeListener(listener);
864
+ }
865
+ function connectWebAppearance(context) {
866
+ const { ownerDocument, themes, options } = context;
867
+ const darkCapable = themes.includes("dark");
868
+ if (darkCapable && !options) {
869
+ throw new Error(
870
+ '@miragon/modeler-shell: a web modeler whose themes include "dark" needs the appearance option ({ themeColors } from its vendored cd-tokens.generated.json)'
871
+ );
661
872
  }
662
- return {
663
- element,
664
- isOpen: () => !element.hidden,
665
- open: () => setOpen(true),
666
- close: () => setOpen(false),
667
- destroy() {
668
- element.remove();
873
+ const html = ownerDocument.documentElement;
874
+ const view = ownerDocument.defaultView;
875
+ const cleanups = [];
876
+ const contrast = typeof view?.matchMedia === "function" ? view.matchMedia(CONTRAST_QUERY) : void 0;
877
+ if (contrast) {
878
+ const mirrorContrast = () => toggleAttribute(html, "data-cd-kontrast", contrast.matches && "hoch");
879
+ mirrorContrast();
880
+ cleanups.push(onMediaChange(contrast, mirrorContrast));
881
+ }
882
+ function destroy() {
883
+ for (const cleanup of cleanups.splice(0)) cleanup();
884
+ }
885
+ if (!darkCapable || !options) {
886
+ return { choice: () => "system", setChoice() {
887
+ }, destroy };
888
+ }
889
+ const { themeColors } = options;
890
+ function storage() {
891
+ if (options?.storage) return options.storage;
892
+ try {
893
+ return globalThis.localStorage;
894
+ } catch {
895
+ return void 0;
669
896
  }
670
- };
671
- }
672
-
673
- // src/dom/keys.ts
674
- function isTextField(target) {
675
- if (!target || typeof target.closest !== "function") return false;
676
- const element = target;
677
- return element.isContentEditable === true || element.closest(TEXT_FIELD_SELECTOR) !== null;
678
- }
679
- function isShown(element) {
680
- if (element.localName === "dialog" && !element.hasAttribute("open")) return false;
681
- if (element.closest("[hidden]")) return false;
682
- return typeof element.checkVisibility !== "function" || element.checkVisibility({ visibilityProperty: true });
683
- }
684
- function isModal(element) {
685
- if (element.getAttribute("aria-modal") === "true") return true;
686
- try {
687
- return element.matches(":modal");
688
- } catch {
689
- return false;
690
- }
691
- }
692
- function openDialogs(scope) {
693
- return [...scope.querySelectorAll('[role="dialog"], [role="alertdialog"], dialog')].filter(isShown);
694
- }
695
- function createKeyListener(context) {
696
- return (event) => {
697
- if (event.key === "Escape") context.hideTooltip();
698
- const dialogs = openDialogs(context.dialogScope);
699
- const decision = decideKey(event, {
700
- host: context.host,
701
- inTextField: isTextField(event.target),
702
- menuOpen: context.menuOpen(),
703
- dialogOpen: dialogs.length > 0,
704
- modalOpen: dialogs.some(isModal),
705
- defaultPrevented: event.defaultPrevented
706
- });
707
- if (decision.preventDefault) event.preventDefault();
708
- if (decision.stopPropagation) event.stopPropagation();
709
- if (decision.action === "closeMenu") context.closeMenu();
710
- else if (decision.action) context.run(decision.action);
711
- };
712
- }
713
-
714
- // src/dom/ledger.ts
715
- var Ledger = class {
716
- undo = [];
717
- originalAttributes = /* @__PURE__ */ new Map();
718
- closed = false;
719
- get isClosed() {
720
- return this.closed;
721
897
  }
722
- /** Runs `task` on `close()`, in reverse order of registration. */
723
- defer(task) {
724
- this.undo.push(task);
725
- }
726
- /** Adds `className` unless present; `close()` removes it only if this ledger added it. */
727
- addClass(element, className) {
728
- if (element.classList.contains(className)) return;
729
- const hadClassAttribute = element.hasAttribute("class");
730
- element.classList.add(className);
731
- this.defer(() => {
732
- element.classList.remove(className);
733
- if (!hadClassAttribute && element.classList.length === 0) element.removeAttribute("class");
734
- });
898
+ function readStored() {
899
+ try {
900
+ return parseAppearanceChoice(storage()?.getItem(APPEARANCE_STORAGE_KEY));
901
+ } catch {
902
+ return "system";
903
+ }
735
904
  }
736
- /** Sets (string) or removes (`null`) an attribute; `close()` restores the value before the first change. */
737
- attribute(element, name, value) {
738
- let originals = this.originalAttributes.get(element);
739
- if (!originals) {
740
- originals = /* @__PURE__ */ new Map();
741
- this.originalAttributes.set(element, originals);
905
+ function store(choice) {
906
+ try {
907
+ const target = storage();
908
+ if (choice === "system") target?.removeItem(APPEARANCE_STORAGE_KEY);
909
+ else target?.setItem(APPEARANCE_STORAGE_KEY, choice);
910
+ } catch {
742
911
  }
743
- if (!originals.has(name)) {
744
- originals.set(name, element.getAttribute(name));
745
- this.defer(() => {
746
- const original = originals.get(name) ?? null;
747
- if (original === null) element.removeAttribute(name);
748
- else element.setAttribute(name, original);
749
- });
912
+ }
913
+ function nextFrame(callback) {
914
+ if (view && typeof view.requestAnimationFrame === "function") {
915
+ const frame = view.requestAnimationFrame(() => callback());
916
+ return () => view.cancelAnimationFrame(frame);
750
917
  }
751
- if (value === null) element.removeAttribute(name);
752
- else element.setAttribute(name, value);
918
+ const timer = setTimeout(callback, 16);
919
+ return () => clearTimeout(timer);
753
920
  }
754
- listen(target, type, listener, options) {
755
- const handler = listener;
756
- target.addEventListener(type, handler, options);
757
- this.defer(() => target.removeEventListener(type, handler, options));
921
+ let cancelTransitionFrame;
922
+ function allowTransitions() {
923
+ cancelTransitionFrame?.();
924
+ cancelTransitionFrame = void 0;
925
+ html.classList.remove(NO_TRANSITION_CLASS);
758
926
  }
759
- /** Runs every undo task once; later calls are no-ops. */
760
- close() {
761
- if (this.closed) return;
762
- this.closed = true;
763
- for (const task of this.undo.reverse()) {
764
- try {
765
- task();
766
- } catch {
767
- }
768
- }
769
- this.undo.length = 0;
770
- this.originalAttributes.clear();
927
+ cleanups.push(() => {
928
+ if (cancelTransitionFrame) allowTransitions();
929
+ });
930
+ function suspendTransitions() {
931
+ cancelTransitionFrame?.();
932
+ html.classList.add(NO_TRANSITION_CLASS);
933
+ cancelTransitionFrame = nextFrame(() => {
934
+ cancelTransitionFrame = nextFrame(allowTransitions);
935
+ });
771
936
  }
772
- };
937
+ function metaFor(name, media) {
938
+ const normalize = (value) => (value ?? "").replace(/\s+/g, "").toLowerCase();
939
+ const existing = [...ownerDocument.querySelectorAll(`meta[name="${name}"]`)].find(
940
+ (meta) => media === void 0 ? true : normalize(meta.getAttribute("media")) === normalize(media)
941
+ );
942
+ if (existing) return existing;
943
+ const created = createElement(ownerDocument, "meta", { name, media });
944
+ (ownerDocument.head ?? html).append(created);
945
+ return created;
946
+ }
947
+ function apply(choice) {
948
+ const mode = documentMode(choice, themes, themeColors);
949
+ suspendTransitions();
950
+ html.setAttribute("data-cd-theme", mode.theme);
951
+ metaFor("color-scheme").setAttribute("content", mode.colorScheme);
952
+ metaFor("theme-color", SCHEME_MEDIA.light).setAttribute("content", mode.themeColor.light);
953
+ metaFor("theme-color", SCHEME_MEDIA.dark).setAttribute("content", mode.themeColor.dark);
954
+ }
955
+ let current = readStored();
956
+ const onStorage = (event) => {
957
+ if (event.key !== null && event.key !== APPEARANCE_STORAGE_KEY) return;
958
+ const next = event.key === null ? "system" : parseAppearanceChoice(event.newValue);
959
+ if (next === current) return;
960
+ current = next;
961
+ apply(next);
962
+ };
963
+ if (view) {
964
+ view.addEventListener("storage", onStorage);
965
+ cleanups.push(() => view.removeEventListener("storage", onStorage));
966
+ }
967
+ return {
968
+ choice: () => current,
969
+ setChoice(choice) {
970
+ const next = parseAppearanceChoice(choice);
971
+ current = next;
972
+ store(next);
973
+ apply(next);
974
+ },
975
+ destroy
976
+ };
977
+ }
773
978
 
774
979
  // src/dom/icons.ts
775
980
  var ICONS = {
@@ -979,28 +1184,691 @@ function sanitize(icon, options = {}) {
979
1184
  }
980
1185
  return { root, children };
981
1186
  }
982
- function escapeAttribute(value) {
1187
+ function escapeAttribute2(value) {
983
1188
  return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
984
1189
  }
985
- function attributeList(attributes) {
986
- return attributes.map(([name, value]) => ` ${name}="${escapeAttribute(value)}"`).join("");
1190
+ function attributeList(attributes) {
1191
+ return attributes.map(([name, value]) => ` ${name}="${escapeAttribute2(value)}"`).join("");
1192
+ }
1193
+ function iconMarkup(icon, options) {
1194
+ const { root, children } = sanitize(icon, options);
1195
+ const content = children.map(([tag, attributes]) => `<${tag}${attributeList(attributes)}/>`).join("");
1196
+ return `<svg xmlns="${SVG_NAMESPACE}"${attributeList(root)}>${content}</svg>`;
1197
+ }
1198
+ function iconElement(icon, options) {
1199
+ const { root, children } = sanitize(icon, options);
1200
+ const svg = document.createElementNS(SVG_NAMESPACE, "svg");
1201
+ for (const [name, value] of root) svg.setAttribute(name, value);
1202
+ for (const [tag, attributes] of children) {
1203
+ const child = document.createElementNS(SVG_NAMESPACE, tag);
1204
+ for (const [name, value] of attributes) child.setAttribute(name, value);
1205
+ svg.append(child);
1206
+ }
1207
+ return svg;
1208
+ }
1209
+
1210
+ // src/dom/feedback/problems-popover.ts
1211
+ var PILL_ATTRIBUTES = ["aria-haspopup", "aria-expanded", "aria-controls"];
1212
+ function isPrintable(event) {
1213
+ return event.key.length === 1 && /\S/.test(event.key) && !event.ctrlKey && !event.metaKey && !event.altKey;
1214
+ }
1215
+ function createProblemsPopover(context) {
1216
+ const { ownerDocument, root, layer, pill, id, titleId, messages } = context;
1217
+ const view = ownerDocument.defaultView;
1218
+ const list = createElement(ownerDocument, "ul", { class: "cd-shell-problems-list", role: "list" });
1219
+ const element = createElement(
1220
+ ownerDocument,
1221
+ "div",
1222
+ {
1223
+ class: "cd-shell-problems-popover",
1224
+ "data-chrome": "problems-popover",
1225
+ role: "dialog",
1226
+ id,
1227
+ "aria-labelledby": titleId,
1228
+ tabindex: "-1"
1229
+ },
1230
+ createElement(
1231
+ ownerDocument,
1232
+ "h2",
1233
+ { class: "cd-shell-problems-popover-title", id: titleId },
1234
+ messages.problemsTitle
1235
+ ),
1236
+ list
1237
+ );
1238
+ element.hidden = true;
1239
+ layer.append(element);
1240
+ pill.setAttribute("aria-haspopup", "dialog");
1241
+ pill.setAttribute("aria-expanded", "false");
1242
+ pill.setAttribute("aria-controls", id);
1243
+ let items = [];
1244
+ let rows = [];
1245
+ let destroyed = false;
1246
+ const isOpen = () => !element.hidden;
1247
+ function keyOf(item) {
1248
+ return `${item.elementId ?? ""}\0${item.severity}\0${item.message}`;
1249
+ }
1250
+ function rowElement(item) {
1251
+ const severity = item.severity === "error" ? "error" : "warning";
1252
+ const parts = [
1253
+ // The shape and colour of the icon carry the severity on screen, its name for screen readers.
1254
+ createElement(
1255
+ ownerDocument,
1256
+ "span",
1257
+ { class: "cd-shell-problem-icon", role: "img", "aria-label": messages.problemSeverity(severity) },
1258
+ iconElement(severity === "error" ? "circle-x" : "triangle-alert")
1259
+ ),
1260
+ createElement(ownerDocument, "span", { class: "cd-shell-problem-message" }, item.message),
1261
+ item.location ? createElement(ownerDocument, "span", { class: "cd-shell-problem-location" }, item.location) : void 0
1262
+ ];
1263
+ const attributes = { class: "cd-shell-problem", "data-severity": severity };
1264
+ if (!item.elementId) {
1265
+ const text = createElement(ownerDocument, "div", { ...attributes, tabindex: "-1" }, ...parts);
1266
+ rows.push({ key: keyOf(item), element: text });
1267
+ return text;
1268
+ }
1269
+ const button = createElement(ownerDocument, "button", { type: "button", ...attributes }, ...parts);
1270
+ button.addEventListener("click", () => select(item));
1271
+ rows.push({ key: keyOf(item), element: button });
1272
+ return button;
1273
+ }
1274
+ function focusedRowIndex() {
1275
+ return rows.findIndex((row) => row.element === ownerDocument.activeElement);
1276
+ }
1277
+ function render() {
1278
+ const focusedIndex = focusedRowIndex();
1279
+ const focusedKey = rows[focusedIndex]?.key;
1280
+ rows = [];
1281
+ list.replaceChildren(...items.map((item) => createElement(ownerDocument, "li", {}, rowElement(item))));
1282
+ if (focusedKey === void 0 || !isOpen()) return;
1283
+ const kept = rows.find((row) => row.key === focusedKey) ?? rows[Math.min(focusedIndex, rows.length - 1)];
1284
+ if (kept) focusRowElement(kept.element);
1285
+ else element.focus({ preventScroll: true });
1286
+ }
1287
+ function revealInList(row) {
1288
+ const rowBox = row.getBoundingClientRect();
1289
+ const listBox = list.getBoundingClientRect();
1290
+ if (rowBox.top < listBox.top) list.scrollTop -= listBox.top - rowBox.top;
1291
+ else if (rowBox.bottom > listBox.bottom) list.scrollTop += rowBox.bottom - listBox.bottom;
1292
+ }
1293
+ function focusRowElement(row) {
1294
+ row.focus({ preventScroll: true });
1295
+ revealInList(row);
1296
+ }
1297
+ function focusCanvas() {
1298
+ const canvas = root.querySelector(".cd-shell-canvas svg[tabindex]");
1299
+ canvas?.focus({ preventScroll: true });
1300
+ if ((!canvas || ownerDocument.activeElement !== canvas) && !pill.hidden)
1301
+ pill.focus({ preventScroll: true });
1302
+ }
1303
+ function select(item) {
1304
+ close();
1305
+ focusCanvas();
1306
+ context.onSelectItem?.(item);
1307
+ }
1308
+ function focusRow(index) {
1309
+ if (rows.length === 0) return;
1310
+ const row = rows[(index + rows.length) % rows.length];
1311
+ if (row) focusRowElement(row.element);
1312
+ }
1313
+ const onOutsidePointerDown = (event) => {
1314
+ const target = event.target;
1315
+ if (target && (element.contains(target) || pill.contains(target))) return;
1316
+ close();
1317
+ };
1318
+ const onWindowBlur = () => close();
1319
+ function open() {
1320
+ if (destroyed || isOpen()) return;
1321
+ element.hidden = false;
1322
+ pill.setAttribute("aria-expanded", "true");
1323
+ ownerDocument.addEventListener("pointerdown", onOutsidePointerDown, true);
1324
+ view?.addEventListener("blur", onWindowBlur);
1325
+ list.scrollTop = 0;
1326
+ const first = rows[0];
1327
+ if (first) focusRowElement(first.element);
1328
+ else element.focus({ preventScroll: true });
1329
+ }
1330
+ function close(options = {}) {
1331
+ if (!isOpen()) return;
1332
+ element.hidden = true;
1333
+ pill.setAttribute("aria-expanded", "false");
1334
+ ownerDocument.removeEventListener("pointerdown", onOutsidePointerDown, true);
1335
+ view?.removeEventListener("blur", onWindowBlur);
1336
+ if (options.restoreFocus && !pill.hidden) pill.focus({ preventScroll: true });
1337
+ }
1338
+ function consume(event) {
1339
+ event.preventDefault();
1340
+ event.stopPropagation();
1341
+ }
1342
+ element.addEventListener("keydown", (event) => {
1343
+ if (matchChromeKey(event)) return;
1344
+ const index = focusedRowIndex();
1345
+ switch (event.key) {
1346
+ case "Escape":
1347
+ consume(event);
1348
+ close({ restoreFocus: true });
1349
+ return;
1350
+ case "Tab": {
1351
+ const first = index <= 0;
1352
+ const last = rows.length === 0 || index === rows.length - 1;
1353
+ if (event.shiftKey && first) {
1354
+ consume(event);
1355
+ close({ restoreFocus: true });
1356
+ } else if (!event.shiftKey && last) {
1357
+ event.stopPropagation();
1358
+ close({ restoreFocus: true });
1359
+ }
1360
+ return;
1361
+ }
1362
+ case "ArrowDown":
1363
+ focusRow(index + 1);
1364
+ break;
1365
+ case "ArrowUp":
1366
+ focusRow(index < 0 ? rows.length - 1 : index - 1);
1367
+ break;
1368
+ case "Home":
1369
+ focusRow(0);
1370
+ break;
1371
+ case "End":
1372
+ focusRow(rows.length - 1);
1373
+ break;
1374
+ case "Enter":
1375
+ case " ":
1376
+ case "Delete":
1377
+ case "Backspace":
1378
+ event.stopPropagation();
1379
+ return;
1380
+ default:
1381
+ if (isPrintable(event)) event.stopPropagation();
1382
+ return;
1383
+ }
1384
+ consume(event);
1385
+ });
1386
+ return {
1387
+ element,
1388
+ isOpen,
1389
+ toggle() {
1390
+ if (isOpen()) close({ restoreFocus: true });
1391
+ else open();
1392
+ },
1393
+ open,
1394
+ close,
1395
+ setItems(next) {
1396
+ items = next;
1397
+ if (!destroyed) render();
1398
+ },
1399
+ destroy() {
1400
+ if (destroyed) return;
1401
+ close();
1402
+ destroyed = true;
1403
+ for (const name of PILL_ATTRIBUTES) pill.removeAttribute(name);
1404
+ element.remove();
1405
+ }
1406
+ };
1407
+ }
1408
+
1409
+ // src/dom/feedback/toasts.ts
1410
+ var TOAST_RESUME_MIN_MS = 1500;
1411
+ var TOAST_LEAVE_FALLBACK_MS = 400;
1412
+ var LEAVE_ANIMATION = "cd-shell-toast-out";
1413
+ var TOAST_ANNOUNCE_DELAY_MS = 100;
1414
+ var INERT_HANDLE = Object.freeze({ dismiss() {
1415
+ } });
1416
+ function createToaster(context) {
1417
+ const { ownerDocument, messages } = context;
1418
+ const web = context.host === "web";
1419
+ const element = createElement(ownerDocument, "div", {
1420
+ class: "cd-shell-toasts",
1421
+ "data-chrome": "toasts",
1422
+ popover: web ? "manual" : void 0
1423
+ });
1424
+ const liveRegion = (politeness) => createElement(ownerDocument, "div", {
1425
+ class: "cd-shell-live-region",
1426
+ "data-live": politeness,
1427
+ role: politeness === "polite" ? "status" : "alert",
1428
+ "aria-live": politeness,
1429
+ "aria-atomic": "true"
1430
+ });
1431
+ const politeRegion = liveRegion("polite");
1432
+ const assertiveRegion = liveRegion("assertive");
1433
+ const announcer = createElement(
1434
+ ownerDocument,
1435
+ "div",
1436
+ { class: "cd-shell-live" },
1437
+ politeRegion,
1438
+ assertiveRegion
1439
+ );
1440
+ const live = [];
1441
+ const leaveTimers = /* @__PURE__ */ new Set();
1442
+ const announceTimers = /* @__PURE__ */ new Map();
1443
+ let lastId = 0;
1444
+ let destroyed = false;
1445
+ function regionFor(request) {
1446
+ return request.status === "danger" ? assertiveRegion : politeRegion;
1447
+ }
1448
+ function announce(request) {
1449
+ const region = regionFor(request);
1450
+ clearTimeout(announceTimers.get(region));
1451
+ region.textContent = "";
1452
+ announceTimers.set(
1453
+ region,
1454
+ setTimeout(() => {
1455
+ announceTimers.delete(region);
1456
+ if (!destroyed) region.textContent = request.text;
1457
+ }, TOAST_ANNOUNCE_DELAY_MS)
1458
+ );
1459
+ }
1460
+ function silence(request) {
1461
+ const region = regionFor(request);
1462
+ if (region.textContent === request.text) region.textContent = "";
1463
+ }
1464
+ function changed() {
1465
+ if (!destroyed) context.onChange?.();
1466
+ }
1467
+ function supportsPopover() {
1468
+ return web && typeof element.showPopover === "function" && typeof element.hidePopover === "function";
1469
+ }
1470
+ function popoverOpen() {
1471
+ try {
1472
+ return element.matches(":popover-open");
1473
+ } catch {
1474
+ return false;
1475
+ }
1476
+ }
1477
+ function modalOpen() {
1478
+ try {
1479
+ return ownerDocument.querySelector(":modal") !== null;
1480
+ } catch {
1481
+ return true;
1482
+ }
1483
+ }
1484
+ function raise() {
1485
+ if (!supportsPopover() || !element.isConnected) return;
1486
+ if (popoverOpen() && !modalOpen()) return;
1487
+ const focused = ownerDocument.activeElement;
1488
+ const keepFocus = focused && element.contains(focused) ? focused : void 0;
1489
+ try {
1490
+ element.hidePopover();
1491
+ element.showPopover();
1492
+ } catch {
1493
+ return;
1494
+ }
1495
+ if (keepFocus && ownerDocument.activeElement !== keepFocus) keepFocus.focus({ preventScroll: true });
1496
+ }
1497
+ function lowerWhenEmpty() {
1498
+ if (element.childElementCount > 0 || !supportsPopover()) return;
1499
+ try {
1500
+ element.hidePopover();
1501
+ } catch {
1502
+ }
1503
+ }
1504
+ function engaged(toast) {
1505
+ return toast.hovered || toast.focused;
1506
+ }
1507
+ function stopTimer(toast) {
1508
+ clearTimeout(toast.timer);
1509
+ toast.timer = void 0;
1510
+ }
1511
+ function runTimer(toast) {
1512
+ stopTimer(toast);
1513
+ if (toast.remaining === void 0 || engaged(toast)) return;
1514
+ toast.startedAt = Date.now();
1515
+ toast.timer = setTimeout(() => close(toast), toast.remaining);
1516
+ }
1517
+ function pause(toast) {
1518
+ if (toast.timer === void 0) return;
1519
+ stopTimer(toast);
1520
+ if (toast.remaining !== void 0) {
1521
+ toast.remaining = Math.max(0, toast.remaining - (Date.now() - toast.startedAt));
1522
+ }
1523
+ }
1524
+ function setEngagement(toast, hovered, focused) {
1525
+ if (!live.includes(toast)) return;
1526
+ const wasEngaged = engaged(toast);
1527
+ toast.hovered = hovered;
1528
+ toast.focused = focused;
1529
+ if (engaged(toast) === wasEngaged) return;
1530
+ if (wasEngaged) {
1531
+ if (toast.remaining !== void 0) toast.remaining = Math.max(toast.remaining, TOAST_RESUME_MIN_MS);
1532
+ runTimer(toast);
1533
+ } else {
1534
+ pause(toast);
1535
+ }
1536
+ }
1537
+ function reducedMotion() {
1538
+ if (ownerDocument.documentElement.classList.contains(REDUCED_MOTION_CLASS)) return true;
1539
+ const view = ownerDocument.defaultView;
1540
+ return typeof view?.matchMedia === "function" && view.matchMedia("(prefers-reduced-motion: reduce)").matches;
1541
+ }
1542
+ function removeElement(toastElement) {
1543
+ toastElement.remove();
1544
+ lowerWhenEmpty();
1545
+ changed();
1546
+ }
1547
+ function leave(toastElement) {
1548
+ toastElement.setAttribute("data-leaving", "");
1549
+ toastElement.inert = true;
1550
+ if (reducedMotion()) {
1551
+ removeElement(toastElement);
1552
+ return;
1553
+ }
1554
+ let done = false;
1555
+ const finish = () => {
1556
+ if (done) return;
1557
+ done = true;
1558
+ clearTimeout(fallback);
1559
+ leaveTimers.delete(fallback);
1560
+ removeElement(toastElement);
1561
+ };
1562
+ toastElement.addEventListener("animationend", (event) => {
1563
+ if (event.target === toastElement && event.animationName === LEAVE_ANIMATION) finish();
1564
+ });
1565
+ const fallback = setTimeout(finish, TOAST_LEAVE_FALLBACK_MS);
1566
+ leaveTimers.add(fallback);
1567
+ }
1568
+ function close(toast) {
1569
+ const index = live.indexOf(toast);
1570
+ if (index < 0) return;
1571
+ live.splice(index, 1);
1572
+ stopTimer(toast);
1573
+ silence(toast.request);
1574
+ const heldFocus = toast.element.contains(ownerDocument.activeElement);
1575
+ if (heldFocus) {
1576
+ const next = live[index] ?? live[index - 1];
1577
+ if (next) next.closeButton.focus({ preventScroll: true });
1578
+ else context.focusFallback();
1579
+ }
1580
+ leave(toast.element);
1581
+ }
1582
+ function setAction(toast, action) {
1583
+ const previous = toast.actionButton;
1584
+ const hadFocus = previous !== void 0 && ownerDocument.activeElement === previous;
1585
+ previous?.remove();
1586
+ toast.actionButton = void 0;
1587
+ if (action) {
1588
+ const button = createElement(
1589
+ ownerDocument,
1590
+ "button",
1591
+ { type: "button", class: "cd-shell-toast-action cd-shell-button", "data-variant": "ghost" },
1592
+ action.label
1593
+ );
1594
+ button.addEventListener("click", () => {
1595
+ close(toast);
1596
+ action.onSelect();
1597
+ });
1598
+ toast.closeButton.before(button);
1599
+ toast.actionButton = button;
1600
+ }
1601
+ if (hadFocus) (toast.actionButton ?? toast.closeButton).focus({ preventScroll: true });
1602
+ }
1603
+ function slotOf(toast) {
1604
+ return { id: toast.id, hasAction: toast.request.action !== void 0, engaged: engaged(toast) };
1605
+ }
1606
+ function render(request) {
1607
+ const id = ++lastId;
1608
+ const textId = `${context.idPrefix}-${id}`;
1609
+ const closeButton = createElement(
1610
+ ownerDocument,
1611
+ "button",
1612
+ {
1613
+ type: "button",
1614
+ class: "cd-shell-toast-close",
1615
+ "aria-label": messages.close,
1616
+ "aria-describedby": textId
1617
+ },
1618
+ iconElement("x")
1619
+ );
1620
+ const toastElement = createElement(
1621
+ ownerDocument,
1622
+ "div",
1623
+ { class: "cd-shell-toast", "data-status": request.status },
1624
+ createElement(ownerDocument, "span", { class: "cd-shell-toast-dot", "aria-hidden": "true" }),
1625
+ createElement(ownerDocument, "span", { class: "cd-shell-toast-text", id: textId }, request.text),
1626
+ closeButton
1627
+ );
1628
+ const toast = {
1629
+ id,
1630
+ element: toastElement,
1631
+ closeButton,
1632
+ handle: Object.freeze({ dismiss: () => close(toast) }),
1633
+ request,
1634
+ actionButton: void 0,
1635
+ hovered: false,
1636
+ focused: false,
1637
+ remaining: toastLifetime(request),
1638
+ startedAt: Date.now(),
1639
+ timer: void 0
1640
+ };
1641
+ setAction(toast, request.action);
1642
+ closeButton.addEventListener("click", () => close(toast));
1643
+ toastElement.addEventListener("mouseenter", () => setEngagement(toast, true, toast.focused));
1644
+ toastElement.addEventListener("mouseleave", () => setEngagement(toast, false, toast.focused));
1645
+ toastElement.addEventListener("focusin", () => setEngagement(toast, toast.hovered, true));
1646
+ toastElement.addEventListener(
1647
+ "focusout",
1648
+ (event) => setEngagement(toast, toast.hovered, toastElement.contains(event.relatedTarget))
1649
+ );
1650
+ return toast;
1651
+ }
1652
+ function show(request) {
1653
+ if (destroyed) return INERT_HANDLE;
1654
+ const same = live.find(
1655
+ (toast2) => toast2.request.status === request.status && toast2.request.text === request.text
1656
+ );
1657
+ if (same) {
1658
+ same.request = request;
1659
+ setAction(same, request.action);
1660
+ same.remaining = toastLifetime(request);
1661
+ runTimer(same);
1662
+ raise();
1663
+ announce(request);
1664
+ changed();
1665
+ return same.handle;
1666
+ }
1667
+ while (live.length >= MAX_TOASTS) {
1668
+ const evictedId = toastToEvict(live.map(slotOf));
1669
+ const evicted = live.find((toast2) => toast2.id === evictedId) ?? live[0];
1670
+ if (!evicted) break;
1671
+ close(evicted);
1672
+ }
1673
+ const toast = render(request);
1674
+ live.push(toast);
1675
+ element.append(toast.element);
1676
+ raise();
1677
+ runTimer(toast);
1678
+ announce(request);
1679
+ changed();
1680
+ return toast.handle;
1681
+ }
1682
+ return {
1683
+ element,
1684
+ announcer,
1685
+ show,
1686
+ setBottom(bottom) {
1687
+ element.style.bottom = `${bottom}px`;
1688
+ },
1689
+ widestCard() {
1690
+ let widest;
1691
+ for (const card of element.children) {
1692
+ const width = card.getBoundingClientRect().width;
1693
+ if (width > 0) widest = Math.max(widest ?? 0, width);
1694
+ }
1695
+ return widest;
1696
+ },
1697
+ destroy() {
1698
+ if (destroyed) return;
1699
+ destroyed = true;
1700
+ for (const toast of live.splice(0)) stopTimer(toast);
1701
+ for (const timer of leaveTimers) clearTimeout(timer);
1702
+ leaveTimers.clear();
1703
+ for (const timer of announceTimers.values()) clearTimeout(timer);
1704
+ announceTimers.clear();
1705
+ element.remove();
1706
+ announcer.remove();
1707
+ }
1708
+ };
1709
+ }
1710
+
1711
+ // src/dom/hint.ts
1712
+ function createEmptyHint(ownerDocument, messages, options) {
1713
+ const { onInsertExample } = options;
1714
+ const action = onInsertExample ? createElement(
1715
+ ownerDocument,
1716
+ "button",
1717
+ { type: "button", class: "cd-shell-hint-action" },
1718
+ messages.insertExample
1719
+ ) : void 0;
1720
+ action?.addEventListener("click", () => onInsertExample?.());
1721
+ const element = createElement(
1722
+ ownerDocument,
1723
+ "div",
1724
+ { class: "cd-shell-hint" },
1725
+ createElement(ownerDocument, "p", { class: "cd-shell-hint-text" }, messages.emptyHint),
1726
+ action
1727
+ );
1728
+ element.hidden = true;
1729
+ return {
1730
+ element,
1731
+ isShown: () => !element.hidden,
1732
+ setShown(shown) {
1733
+ element.hidden = !shown;
1734
+ },
1735
+ place({ top, right, bottom, left }) {
1736
+ element.style.inset = `${top}px ${right}px ${bottom}px ${left}px`;
1737
+ },
1738
+ destroy() {
1739
+ element.remove();
1740
+ }
1741
+ };
1742
+ }
1743
+
1744
+ // src/dom/inspector.ts
1745
+ function createInspector(ownerDocument, options, onChange) {
1746
+ const element = createElement(ownerDocument, "aside", {
1747
+ class: "cd-shell-inspector",
1748
+ "data-chrome": "inspector",
1749
+ "aria-label": options.label
1750
+ });
1751
+ element.hidden = true;
1752
+ function setOpen(open) {
1753
+ if (open === !element.hidden) return;
1754
+ element.hidden = !open;
1755
+ onChange(open);
1756
+ }
1757
+ return {
1758
+ element,
1759
+ isOpen: () => !element.hidden,
1760
+ open: () => setOpen(true),
1761
+ close: () => setOpen(false),
1762
+ destroy() {
1763
+ element.remove();
1764
+ }
1765
+ };
1766
+ }
1767
+
1768
+ // src/dom/keys.ts
1769
+ function isTextField(target) {
1770
+ if (!target || typeof target.closest !== "function") return false;
1771
+ const element = target;
1772
+ return element.isContentEditable === true || element.closest(TEXT_FIELD_SELECTOR) !== null;
1773
+ }
1774
+ function isShown(element) {
1775
+ if (element.localName === "dialog" && !element.hasAttribute("open")) return false;
1776
+ if (element.closest("[hidden]")) return false;
1777
+ return typeof element.checkVisibility !== "function" || element.checkVisibility({ visibilityProperty: true });
1778
+ }
1779
+ function isModal(element) {
1780
+ if (element.getAttribute("aria-modal") === "true") return true;
1781
+ try {
1782
+ return element.matches(":modal");
1783
+ } catch {
1784
+ return false;
1785
+ }
1786
+ }
1787
+ function openDialogs(scope) {
1788
+ return [...scope.querySelectorAll('[role="dialog"], [role="alertdialog"], dialog')].filter(isShown);
1789
+ }
1790
+ function isModalOpen(scope) {
1791
+ return openDialogs(scope).some(isModal);
987
1792
  }
988
- function iconMarkup(icon, options) {
989
- const { root, children } = sanitize(icon, options);
990
- const content = children.map(([tag, attributes]) => `<${tag}${attributeList(attributes)}/>`).join("");
991
- return `<svg xmlns="${SVG_NAMESPACE}"${attributeList(root)}>${content}</svg>`;
1793
+ function createKeyListener(context) {
1794
+ return (event) => {
1795
+ if (event.key === "Escape") context.hideTooltip();
1796
+ const dialogs = openDialogs(context.dialogScope);
1797
+ const decision = decideKey(event, {
1798
+ host: context.host,
1799
+ inTextField: isTextField(event.target),
1800
+ menuOpen: context.menuOpen(),
1801
+ dialogOpen: dialogs.length > 0,
1802
+ modalOpen: dialogs.some(isModal),
1803
+ defaultPrevented: event.defaultPrevented,
1804
+ startScreen: context.startScreen?.() ?? false
1805
+ });
1806
+ if (decision.preventDefault) event.preventDefault();
1807
+ if (decision.stopPropagation) event.stopPropagation();
1808
+ if (decision.action === "closeMenu") context.closeMenu();
1809
+ else if (decision.action) context.run(decision.action);
1810
+ };
992
1811
  }
993
- function iconElement(icon, options) {
994
- const { root, children } = sanitize(icon, options);
995
- const svg = document.createElementNS(SVG_NAMESPACE, "svg");
996
- for (const [name, value] of root) svg.setAttribute(name, value);
997
- for (const [tag, attributes] of children) {
998
- const child = document.createElementNS(SVG_NAMESPACE, tag);
999
- for (const [name, value] of attributes) child.setAttribute(name, value);
1000
- svg.append(child);
1812
+
1813
+ // src/dom/ledger.ts
1814
+ var Ledger = class {
1815
+ undo = [];
1816
+ originalAttributes = /* @__PURE__ */ new Map();
1817
+ closed = false;
1818
+ get isClosed() {
1819
+ return this.closed;
1001
1820
  }
1002
- return svg;
1003
- }
1821
+ /** Runs `task` on `close()`, in reverse order of registration. */
1822
+ defer(task) {
1823
+ this.undo.push(task);
1824
+ }
1825
+ /** Adds `className` unless present; `close()` removes it only if this ledger added it. */
1826
+ addClass(element, className) {
1827
+ if (element.classList.contains(className)) return;
1828
+ const hadClassAttribute = element.hasAttribute("class");
1829
+ element.classList.add(className);
1830
+ this.defer(() => {
1831
+ element.classList.remove(className);
1832
+ if (!hadClassAttribute && element.classList.length === 0) element.removeAttribute("class");
1833
+ });
1834
+ }
1835
+ /** Sets (string) or removes (`null`) an attribute; `close()` restores the value before the first change. */
1836
+ attribute(element, name, value) {
1837
+ let originals = this.originalAttributes.get(element);
1838
+ if (!originals) {
1839
+ originals = /* @__PURE__ */ new Map();
1840
+ this.originalAttributes.set(element, originals);
1841
+ }
1842
+ if (!originals.has(name)) {
1843
+ originals.set(name, element.getAttribute(name));
1844
+ this.defer(() => {
1845
+ const original = originals.get(name) ?? null;
1846
+ if (original === null) element.removeAttribute(name);
1847
+ else element.setAttribute(name, original);
1848
+ });
1849
+ }
1850
+ if (value === null) element.removeAttribute(name);
1851
+ else element.setAttribute(name, value);
1852
+ }
1853
+ listen(target, type, listener, options) {
1854
+ const handler = listener;
1855
+ target.addEventListener(type, handler, options);
1856
+ this.defer(() => target.removeEventListener(type, handler, options));
1857
+ }
1858
+ /** Runs every undo task once; later calls are no-ops. */
1859
+ close() {
1860
+ if (this.closed) return;
1861
+ this.closed = true;
1862
+ for (const task of this.undo.reverse()) {
1863
+ try {
1864
+ task();
1865
+ } catch {
1866
+ }
1867
+ }
1868
+ this.undo.length = 0;
1869
+ this.originalAttributes.clear();
1870
+ }
1871
+ };
1004
1872
 
1005
1873
  // src/dom/legend.ts
1006
1874
  function createLegend(context) {
@@ -1061,8 +1929,11 @@ function createLegend(context) {
1061
1929
  }
1062
1930
 
1063
1931
  // src/dom/menu.ts
1064
- var GROUP_ORDER = ["model", "export", "help"];
1065
- function isPrintable(event) {
1932
+ var GROUP_ORDER = ["file", "model", "export", "appearance", "help"];
1933
+ function isModelerGroup(name) {
1934
+ return name === "model" || name === "export" || name === "help";
1935
+ }
1936
+ function isPrintable2(event) {
1066
1937
  return event.key.length === 1 && /\S/.test(event.key) && !event.ctrlKey && !event.metaKey && !event.altKey;
1067
1938
  }
1068
1939
  function createMenu(context) {
@@ -1091,8 +1962,9 @@ function createMenu(context) {
1091
1962
  layer.prepend(dropdown);
1092
1963
  const isOpen = () => !dropdown.hidden;
1093
1964
  function entriesOf(name) {
1094
- const own = groups[name] ?? [];
1095
- return name === "model" ? [...own, ...context.shellModelEntries()] : own;
1965
+ const own = isModelerGroup(name) ? groups[name] ?? [] : [];
1966
+ const added = context.shellEntries(name);
1967
+ return added.length > 0 ? [...own, ...added] : own;
1096
1968
  }
1097
1969
  function hasEntries() {
1098
1970
  return GROUP_ORDER.some((name) => entriesOf(name).length > 0);
@@ -1146,14 +2018,12 @@ function createMenu(context) {
1146
2018
  }
1147
2019
  };
1148
2020
  }
1149
- const shortcut = entry.kind === "action" && entry.shortcut ? createElement(ownerDocument, "span", { class: "cd-shell-menu-shortcut" }, entry.shortcut) : void 0;
2021
+ const shortcut = entry.shortcut ? createElement(ownerDocument, "span", { class: "cd-shell-menu-shortcut" }, entry.shortcut) : void 0;
1150
2022
  const element = itemElement("menuitem", entry.label, { icon: entry.icon, end: shortcut, disabled });
1151
2023
  element.dataset.menuId = entry.id;
1152
2024
  element.dataset.menuGroup = group;
1153
- if (entry.kind === "action") {
1154
- const keyShortcuts = entry.ariaKeyShortcuts ?? (entry.shortcut ? ariaKeyShortcutsFromLabel(entry.shortcut) : void 0);
1155
- toggleAttribute(element, "aria-keyshortcuts", keyShortcuts || null);
1156
- }
2025
+ const keyShortcuts = entry.ariaKeyShortcuts ?? (entry.shortcut ? ariaKeyShortcutsFromLabel(entry.shortcut) : void 0);
2026
+ toggleAttribute(element, "aria-keyshortcuts", keyShortcuts || null);
1157
2027
  if (entry.kind === "dialog") element.setAttribute("aria-haspopup", "dialog");
1158
2028
  if (entry.kind === "action" && entry.danger) element.dataset.danger = "";
1159
2029
  return {
@@ -1338,7 +2208,7 @@ function createMenu(context) {
1338
2208
  event.stopPropagation();
1339
2209
  return;
1340
2210
  default:
1341
- if (!isPrintable(event)) return;
2211
+ if (!isPrintable2(event)) return;
1342
2212
  typeAhead(event.key);
1343
2213
  }
1344
2214
  consume(event);
@@ -1368,7 +2238,7 @@ function createMenu(context) {
1368
2238
 
1369
2239
  // src/dom/problems.ts
1370
2240
  function createProblemsPill(context) {
1371
- const { ownerDocument, messages, lang, tooltip, options } = context;
2241
+ const { ownerDocument, messages, lang, tooltip } = context;
1372
2242
  const icon = createElement(ownerDocument, "span", { class: "cd-shell-problems-icon" });
1373
2243
  const text = createElement(ownerDocument, "span", { class: "cd-shell-problems-text" });
1374
2244
  const element = createElement(
@@ -1379,7 +2249,7 @@ function createProblemsPill(context) {
1379
2249
  text
1380
2250
  );
1381
2251
  element.hidden = true;
1382
- element.addEventListener("click", () => options.onActivate());
2252
+ element.addEventListener("click", () => context.onActivate());
1383
2253
  const detachTooltip = context.showTooltip ? tooltip.attach(element, { label: messages.showProblems }) : void 0;
1384
2254
  let severity;
1385
2255
  return {
@@ -1655,6 +2525,380 @@ function createTooltip({ root, layer, id }) {
1655
2525
  };
1656
2526
  }
1657
2527
 
2528
+ // src/dom/web/drop-zone.ts
2529
+ var DRAG_EVENTS = ["dragenter", "dragover", "dragleave", "drop"];
2530
+ function carriesFiles(transfer) {
2531
+ if (!transfer) return false;
2532
+ if (Array.from(transfer.types ?? []).includes("Files")) return true;
2533
+ return Array.from(transfer.items ?? []).some((item) => item.kind === "file");
2534
+ }
2535
+ function dragItems(transfer) {
2536
+ const items = Array.from(transfer.items ?? [], (item) => ({ kind: item.kind, type: item.type }));
2537
+ return items.length > 0 ? items : [{ kind: "file", type: "" }];
2538
+ }
2539
+ function acceptList(accept, lang) {
2540
+ const entries = accept.map((entry) => entry.trim()).filter((entry) => entry !== "");
2541
+ const extensions = entries.filter((entry) => entry.startsWith("."));
2542
+ const listed = extensions.length > 0 ? extensions : entries;
2543
+ let format;
2544
+ try {
2545
+ format = new Intl.ListFormat(lang, { type: "disjunction" });
2546
+ } catch {
2547
+ format = new Intl.ListFormat("en", { type: "disjunction" });
2548
+ }
2549
+ return format.format(listed);
2550
+ }
2551
+ function createDropZone(context) {
2552
+ const { ownerDocument, layer, messages, lang, options, modalOpen, toast, setState } = context;
2553
+ const chip = createElement(ownerDocument, "span", { class: "cd-shell-drop-zone-chip" });
2554
+ const element = createElement(
2555
+ ownerDocument,
2556
+ "div",
2557
+ { class: "cd-shell-drop-zone", "data-chrome": "drop-zone", "aria-hidden": "true" },
2558
+ chip
2559
+ );
2560
+ element.hidden = true;
2561
+ layer.append(element);
2562
+ const view = ownerDocument.defaultView;
2563
+ let depth = 0;
2564
+ let current = null;
2565
+ function verdictFor(items) {
2566
+ return dragVerdict(items, {
2567
+ accept: options.accept,
2568
+ multiple: options.multiple ?? false,
2569
+ dialogOpen: modalOpen()
2570
+ });
2571
+ }
2572
+ function show(verdict) {
2573
+ const next = verdict.state === "none" ? null : verdict.state;
2574
+ if (verdict.state === "over") chip.textContent = messages.dropFile;
2575
+ if (verdict.state === "forbidden") chip.textContent = messages.dropForbidden(verdict.reason);
2576
+ if (next === current) return;
2577
+ current = next;
2578
+ element.hidden = next === null;
2579
+ if (next === null) element.removeAttribute("data-state");
2580
+ else element.setAttribute("data-state", next);
2581
+ setState(next);
2582
+ }
2583
+ function track(transfer) {
2584
+ const verdict = verdictFor(dragItems(transfer));
2585
+ show(verdict);
2586
+ transfer.dropEffect = verdict.state === "forbidden" ? "none" : "copy";
2587
+ }
2588
+ function rejectByDefault(files) {
2589
+ const accepted = acceptList(options.accept, lang);
2590
+ for (const file of files) toast({ status: "danger", text: messages.cannotOpenFile(file.name, accepted) });
2591
+ }
2592
+ function receive(files, verdict) {
2593
+ if (verdict.state === "forbidden" && verdict.reason !== "type") return;
2594
+ const accepted = files.filter((file) => acceptsFile(file, options.accept));
2595
+ const rejected = files.filter((file) => !accepted.includes(file));
2596
+ if (accepted.length > 0) options.onDrop(accepted);
2597
+ if (rejected.length > 0) (options.onReject ?? rejectByDefault)(rejected);
2598
+ }
2599
+ const onDragEvent = (event) => {
2600
+ const transfer = event.dataTransfer;
2601
+ if (!carriesFiles(transfer)) return;
2602
+ event.preventDefault();
2603
+ switch (event.type) {
2604
+ case "dragenter":
2605
+ depth += 1;
2606
+ track(transfer);
2607
+ break;
2608
+ case "dragover":
2609
+ depth = Math.max(depth, 1);
2610
+ track(transfer);
2611
+ break;
2612
+ case "dragleave":
2613
+ depth = Math.max(0, depth - 1);
2614
+ if (depth === 0) show({ state: "none" });
2615
+ break;
2616
+ case "drop": {
2617
+ depth = 0;
2618
+ show({ state: "none" });
2619
+ const files = Array.from(transfer.files ?? []);
2620
+ const items = files.length > 0 ? files.map((file) => ({ kind: "file", type: file.type })) : dragItems(transfer);
2621
+ receive(files, verdictFor(items));
2622
+ break;
2623
+ }
2624
+ }
2625
+ };
2626
+ for (const type of DRAG_EVENTS) view?.addEventListener(type, onDragEvent);
2627
+ return {
2628
+ element,
2629
+ state: () => current,
2630
+ destroy() {
2631
+ for (const type of DRAG_EVENTS) view?.removeEventListener(type, onDragEvent);
2632
+ depth = 0;
2633
+ if (current !== null) {
2634
+ current = null;
2635
+ setState(null);
2636
+ }
2637
+ element.remove();
2638
+ }
2639
+ };
2640
+ }
2641
+
2642
+ // src/dom/web/empty-state.ts
2643
+ var KOMET_SIZE = "64";
2644
+ function createEmptyState(context) {
2645
+ const { ownerDocument, messages, options, dropHint, open, titleId, onLeave } = context;
2646
+ const button = (variant, label, run) => {
2647
+ const element2 = createElement(
2648
+ ownerDocument,
2649
+ "button",
2650
+ { type: "button", class: "cd-shell-button", "data-variant": variant },
2651
+ label
2652
+ );
2653
+ element2.addEventListener("click", () => {
2654
+ onLeave();
2655
+ run();
2656
+ });
2657
+ return element2;
2658
+ };
2659
+ const onShowExample = options.onShowExample;
2660
+ const newDiagram = button("primary", messages.newDiagram, () => options.onNewDiagram());
2661
+ const element = createElement(
2662
+ ownerDocument,
2663
+ "section",
2664
+ { class: "cd-shell-empty-state", "data-chrome": "empty-state", "aria-labelledby": titleId },
2665
+ // Dragging the mark would start an image drag that some browsers carry as a file.
2666
+ createElement(ownerDocument, "img", {
2667
+ class: "cd-shell-empty-state-mark",
2668
+ src: options.komet,
2669
+ width: KOMET_SIZE,
2670
+ height: KOMET_SIZE,
2671
+ alt: "",
2672
+ draggable: "false"
2673
+ }),
2674
+ createElement(ownerDocument, "h1", { class: "cd-shell-empty-state-title", id: titleId }, options.title),
2675
+ createElement(ownerDocument, "p", { class: "cd-shell-empty-state-text" }, options.text),
2676
+ createElement(
2677
+ ownerDocument,
2678
+ "div",
2679
+ { class: "cd-shell-empty-state-actions" },
2680
+ newDiagram,
2681
+ onShowExample && button("ghost", messages.showExample, () => onShowExample())
2682
+ ),
2683
+ hintParagraph()
2684
+ );
2685
+ function hintParagraph() {
2686
+ const hasDropHint = dropHint !== void 0 && dropHint !== "";
2687
+ if (!hasDropHint && !open) return void 0;
2688
+ const paragraph = createElement(ownerDocument, "p", { class: "cd-shell-empty-state-hint" });
2689
+ if (hasDropHint) paragraph.append(dropHint);
2690
+ if (!open) return paragraph;
2691
+ const shortcut = open.shortcut?.trim();
2692
+ const link = createElement(
2693
+ ownerDocument,
2694
+ "button",
2695
+ {
2696
+ type: "button",
2697
+ class: "cd-shell-empty-state-open",
2698
+ "aria-keyshortcuts": shortcut ? ariaKeyShortcutsFromLabel(shortcut) : void 0,
2699
+ "aria-haspopup": open.dialog ? "dialog" : void 0
2700
+ },
2701
+ messages.openFileLink
2702
+ );
2703
+ link.addEventListener("click", () => open.onSelect());
2704
+ if (hasDropHint) {
2705
+ paragraph.append(" ", createElement(ownerDocument, "span", { "aria-hidden": "true" }, "\xB7"), " ");
2706
+ }
2707
+ const openItem = createElement(ownerDocument, "span", { class: "cd-shell-empty-state-open-item" }, link);
2708
+ if (shortcut) {
2709
+ openItem.append(
2710
+ " ",
2711
+ createElement(
2712
+ ownerDocument,
2713
+ "kbd",
2714
+ { class: "cd-shell-empty-state-key", "aria-hidden": "true" },
2715
+ shortcut
2716
+ )
2717
+ );
2718
+ }
2719
+ paragraph.append(openItem);
2720
+ return paragraph;
2721
+ }
2722
+ element.hidden = true;
2723
+ return {
2724
+ element,
2725
+ isShown: () => !element.hidden,
2726
+ setShown(shown) {
2727
+ element.hidden = !shown;
2728
+ },
2729
+ focus() {
2730
+ newDiagram.focus({ preventScroll: true });
2731
+ },
2732
+ destroy() {
2733
+ element.remove();
2734
+ }
2735
+ };
2736
+ }
2737
+
2738
+ // src/dom/web/legal.ts
2739
+ var MIRAGON_HOME = "https://www.miragon.io/";
2740
+ var MIRAGON_IMPRESSUM = "https://www.miragon.io/impressum/";
2741
+ var MIRAGON_DATENSCHUTZ = "https://www.miragon.io/datenschutz/";
2742
+ function createLegalLine(context) {
2743
+ const { ownerDocument, messages, options } = context;
2744
+ const year = options.year ?? (/* @__PURE__ */ new Date()).getFullYear();
2745
+ const newTabId = `${context.idPrefix}-new-tab`;
2746
+ const link = (label, href, lang) => createElement(
2747
+ ownerDocument,
2748
+ "a",
2749
+ { href, lang, target: "_blank", rel: "noopener noreferrer", "aria-describedby": newTabId },
2750
+ label
2751
+ );
2752
+ const separator = () => createElement(ownerDocument, "span", { "aria-hidden": "true" }, "\xB7");
2753
+ const item = (anchor) => createElement(ownerDocument, "span", { class: "cd-shell-legal-item" }, separator(), anchor);
2754
+ const links = [
2755
+ link(messages.copyright(year), MIRAGON_HOME),
2756
+ link("Impressum", MIRAGON_IMPRESSUM, "de"),
2757
+ link("Datenschutz", MIRAGON_DATENSCHUTZ, "de"),
2758
+ ...(options.links ?? []).map((extra) => link(extra.label, extra.href, extra.lang))
2759
+ ];
2760
+ const element = createElement(ownerDocument, "nav", {
2761
+ class: "cd-shell-legal",
2762
+ "data-chrome": "legal",
2763
+ "aria-label": messages.legal
2764
+ });
2765
+ element.append(
2766
+ ...links.map((anchor, index) => index === 0 ? anchor : item(anchor)),
2767
+ createElement(ownerDocument, "span", { id: newTabId, hidden: "" }, messages.opensInNewTab)
2768
+ );
2769
+ return {
2770
+ element,
2771
+ destroy() {
2772
+ element.remove();
2773
+ }
2774
+ };
2775
+ }
2776
+
2777
+ // src/dom/web/menu-groups.ts
2778
+ function fileEntry(spec, action) {
2779
+ const common = {
2780
+ id: spec.id,
2781
+ label: spec.label,
2782
+ ...spec.icon ? { icon: spec.icon } : {},
2783
+ ...action.disabled ? { disabled: action.disabled } : {},
2784
+ ...action.shortcut ? { shortcut: action.shortcut } : {},
2785
+ onSelect: () => action.onSelect()
2786
+ };
2787
+ return action.dialog ? { kind: "dialog", ...common } : { kind: "action", ...common };
2788
+ }
2789
+ function fileMenuEntries(options, messages) {
2790
+ const specs = [
2791
+ { option: "newDiagram", id: "new-diagram", label: messages.newDiagram, icon: "file-plus" },
2792
+ { option: "open", id: "open", label: messages.openFile, icon: "folder-open" },
2793
+ // §5.14 gives "Show example" no icon.
2794
+ { option: "showExample", id: "show-example", label: messages.showExample, icon: void 0 },
2795
+ { option: "import", id: "import", label: messages.importFile, icon: "import" }
2796
+ ];
2797
+ return specs.flatMap((spec) => {
2798
+ const action = options[spec.option];
2799
+ return action ? [fileEntry(spec, action)] : [];
2800
+ });
2801
+ }
2802
+ function appearanceMenuEntries(appearance, messages) {
2803
+ return [
2804
+ {
2805
+ kind: "radio",
2806
+ id: "appearance",
2807
+ label: messages.appearance,
2808
+ options: [
2809
+ { value: "system", label: messages.appearanceSystem },
2810
+ { value: "light", label: messages.appearanceLight },
2811
+ { value: "dark", label: messages.appearanceDark }
2812
+ ],
2813
+ value: () => appearance.choice(),
2814
+ onSelect: (value) => appearance.setChoice(parseAppearanceChoice(value))
2815
+ }
2816
+ ];
2817
+ }
2818
+
2819
+ // src/dom/web/primary.ts
2820
+ var COLLAPSED_ATTRIBUTE = "data-collapsed";
2821
+ function createPrimaryAction(context) {
2822
+ const { ownerDocument, messages, tooltip, options } = context;
2823
+ const label = options.label ?? messages.share;
2824
+ const labelElement = createElement(ownerDocument, "span", { class: "cd-shell-primary-label" }, label);
2825
+ const element = createElement(
2826
+ ownerDocument,
2827
+ "button",
2828
+ {
2829
+ type: "button",
2830
+ class: "cd-shell-primary cd-shell-button",
2831
+ "data-variant": "primary",
2832
+ "data-chrome": "primary",
2833
+ "aria-label": label
2834
+ },
2835
+ iconElement(options.icon ?? "share-2"),
2836
+ labelElement
2837
+ );
2838
+ let collapsed = false;
2839
+ let disabled = false;
2840
+ let labelledWidth;
2841
+ function readDisabled() {
2842
+ disabled = options.disabled?.() ?? false;
2843
+ toggleAttribute(element, "aria-disabled", disabled ? "true" : null);
2844
+ }
2845
+ function showCollapsed(state) {
2846
+ labelElement.hidden = state;
2847
+ toggleAttribute(element, COLLAPSED_ATTRIBUTE, state ? "" : null);
2848
+ }
2849
+ function measureLabelledWidth() {
2850
+ if (!collapsed) return element.offsetWidth;
2851
+ showCollapsed(false);
2852
+ const width = element.offsetWidth;
2853
+ showCollapsed(true);
2854
+ return width;
2855
+ }
2856
+ function isEngaged() {
2857
+ if (ownerDocument.activeElement === element) return true;
2858
+ try {
2859
+ return element.matches(":hover");
2860
+ } catch {
2861
+ return false;
2862
+ }
2863
+ }
2864
+ const onClick = (event) => {
2865
+ if (disabled) {
2866
+ event.preventDefault();
2867
+ return;
2868
+ }
2869
+ options.onSelect();
2870
+ };
2871
+ element.addEventListener("click", onClick);
2872
+ const detachTooltip = tooltip.attach(element, () => collapsed ? { label } : null);
2873
+ readDisabled();
2874
+ return {
2875
+ element,
2876
+ isCollapsed: () => collapsed,
2877
+ refresh() {
2878
+ readDisabled();
2879
+ labelledWidth = void 0;
2880
+ },
2881
+ widths() {
2882
+ if (!collapsed || labelledWidth === void 0) {
2883
+ const measured = measureLabelledWidth();
2884
+ if (measured > 0) labelledWidth = measured;
2885
+ }
2886
+ return { withLabel: labelledWidth ?? 0, iconOnly: SHELL.row };
2887
+ },
2888
+ setCollapsed(next) {
2889
+ if (next === collapsed) return;
2890
+ collapsed = next;
2891
+ showCollapsed(next);
2892
+ if (!next && isEngaged()) tooltip.hide();
2893
+ },
2894
+ destroy() {
2895
+ detachTooltip();
2896
+ element.removeEventListener("click", onClick);
2897
+ element.remove();
2898
+ }
2899
+ };
2900
+ }
2901
+
1658
2902
  // src/dom/zoom-panel.ts
1659
2903
  function createZoomPanel(context) {
1660
2904
  const { ownerDocument, messages, lang, platform, tooltip } = context;
@@ -1772,6 +3016,16 @@ function nextMountIndex() {
1772
3016
  return index;
1773
3017
  }
1774
3018
  var DEFAULT_THEMES = ["light"];
3019
+ var LEGAL_HEIGHT_PROPERTY = "--modeler-shell-legal-height";
3020
+ var INERT_TOAST = Object.freeze({ dismiss() {
3021
+ } });
3022
+ function visibleBoxIn(element, root) {
3023
+ if (!element || element.hidden || !element.isConnected) return void 0;
3024
+ if (typeof element.checkVisibility === "function" && !element.checkVisibility({ visibilityProperty: true }))
3025
+ return void 0;
3026
+ const box = boxRelativeTo(element, root);
3027
+ return box.width > 0 && box.height > 0 ? box : void 0;
3028
+ }
1775
3029
  function languageOf(root, host, requested) {
1776
3030
  if (requested) return requested;
1777
3031
  const declared = host === "embedded" ? root.getAttribute("lang") : root.closest("[lang]")?.getAttribute("lang");
@@ -1800,6 +3054,7 @@ function renderShell(root, options, themes, ledger) {
1800
3054
  const platform = options.platform ?? detectPlatform(globalThis.navigator);
1801
3055
  const lang = languageOf(root, host, options.lang);
1802
3056
  const keyScope = options.keyScope ?? (host === "embedded" ? "root" : "document");
3057
+ const web = host === "web";
1803
3058
  ledger.addClass(root, "cd-shell");
1804
3059
  ledger.attribute(root, "data-cd-shell-host", host);
1805
3060
  ledger.attribute(root, "data-palette", "top");
@@ -1812,6 +3067,9 @@ function renderShell(root, options, themes, ledger) {
1812
3067
  const setState = (name, value) => {
1813
3068
  if (!ledger.isClosed) ledger.attribute(root, name, value);
1814
3069
  };
3070
+ const appearance = web ? connectWebAppearance({ ownerDocument, themes, options: options.appearance }) : void 0;
3071
+ if (appearance) ledger.defer(() => appearance.destroy());
3072
+ const darkCapable = themes.includes("dark");
1815
3073
  const layer = createElement(ownerDocument, "div", { class: "cd-shell-layer" });
1816
3074
  ledger.defer(() => layer.remove());
1817
3075
  const tooltip = createTooltip({ root, layer, id: `cd-shell-tooltip-${index}` });
@@ -1843,6 +3101,14 @@ function renderShell(root, options, themes, ledger) {
1843
3101
  keepEnabledWhenReadOnly: true
1844
3102
  } : void 0;
1845
3103
  const shellModelEntries = legendEntry ? [legendEntry] : [];
3104
+ const fileEntries = web && options.file ? fileMenuEntries(options.file, messages) : [];
3105
+ const appearanceEntries = appearance && darkCapable ? appearanceMenuEntries(appearance, messages) : [];
3106
+ const shellEntries = (group) => {
3107
+ if (group === "file") return fileEntries;
3108
+ if (group === "model") return shellModelEntries;
3109
+ if (group === "appearance") return appearanceEntries;
3110
+ return [];
3111
+ };
1846
3112
  const menu = createMenu({
1847
3113
  root,
1848
3114
  layer,
@@ -1850,55 +3116,133 @@ function renderShell(root, options, themes, ledger) {
1850
3116
  messages,
1851
3117
  tooltip,
1852
3118
  groups: options.menu ?? {},
1853
- shellModelEntries: () => shellModelEntries,
3119
+ shellEntries,
1854
3120
  isReadOnly: () => readOnly
1855
3121
  });
1856
3122
  ledger.defer(() => menu.destroy());
1857
3123
  ledger.defer(tooltip.attach(menu.trigger, () => menu.isOpen() ? null : { label: messages.menu }));
3124
+ const primary = web && options.primary ? createPrimaryAction({ ownerDocument, messages, tooltip, options: options.primary }) : void 0;
3125
+ if (primary) ledger.defer(() => primary.destroy());
1858
3126
  const canvas = options.canvas ?? createElement(ownerDocument, "div");
1859
3127
  ledger.addClass(canvas, "cd-shell-canvas");
3128
+ if (web && !canvas.hasAttribute("role")) ledger.attribute(canvas, "role", "main");
1860
3129
  if (!options.canvas) ledger.defer(() => canvas.remove());
3130
+ function focusCanvasOrTrigger() {
3131
+ const took = (target) => {
3132
+ target?.focus({ preventScroll: true });
3133
+ return target !== null && target !== void 0 && ownerDocument.activeElement === target;
3134
+ };
3135
+ if (emptyState?.isShown()) {
3136
+ emptyState.focus();
3137
+ if (emptyState.element.contains(ownerDocument.activeElement)) return;
3138
+ }
3139
+ if (took(canvas.querySelector("svg[tabindex]"))) return;
3140
+ if (!menu.trigger.hidden) took(menu.trigger);
3141
+ }
1861
3142
  function moveFocusOutOf(island) {
1862
- if (!island.contains(ownerDocument.activeElement)) return;
1863
- const canvasTarget = canvas.querySelector("svg[tabindex]");
1864
- canvasTarget?.focus({ preventScroll: true });
1865
- if (canvasTarget && ownerDocument.activeElement === canvasTarget) return;
1866
- if (!menu.trigger.hidden) menu.trigger.focus({ preventScroll: true });
3143
+ if (island.contains(ownerDocument.activeElement)) focusCanvasOrTrigger();
1867
3144
  }
1868
3145
  const status = createStatusBox(ownerDocument);
1869
3146
  ledger.defer(() => status.destroy());
1870
3147
  const hint = options.emptyHint ? createEmptyHint(ownerDocument, messages, options.emptyHint) : void 0;
1871
3148
  if (hint) ledger.defer(() => hint.destroy());
3149
+ const emptyState = web && options.emptyState ? createEmptyState({
3150
+ ownerDocument,
3151
+ messages,
3152
+ options: options.emptyState,
3153
+ dropHint: options.dropZone ? options.emptyState.dropHint ?? messages.dropHint : void 0,
3154
+ open: options.file?.open,
3155
+ titleId: `cd-shell-empty-state-title-${index}`,
3156
+ onLeave: () => setEmptyStateShown(false)
3157
+ }) : void 0;
3158
+ if (emptyState) ledger.defer(() => emptyState.destroy());
1872
3159
  const inspector = options.inspector ? createInspector(ownerDocument, options.inspector, (open) => {
1873
3160
  setState("data-inspector", open ? "open" : null);
3161
+ updateBottomRow();
1874
3162
  placeHint();
1875
3163
  }) : void 0;
1876
3164
  if (inspector) ledger.defer(() => inspector.destroy());
1877
- const pill = options.problems ? createProblemsPill({
3165
+ const problemsOptions = options.problems;
3166
+ let popover;
3167
+ const pill = problemsOptions ? createProblemsPill({
1878
3168
  ownerDocument,
1879
3169
  messages,
1880
3170
  lang,
1881
3171
  tooltip,
1882
3172
  showTooltip: host === "ide",
1883
- options: options.problems
3173
+ onActivate() {
3174
+ if (popover) popover.toggle();
3175
+ else problemsOptions.onActivate?.();
3176
+ }
1884
3177
  }) : void 0;
1885
3178
  if (pill) ledger.defer(() => pill.destroy());
3179
+ if (pill && (web || host === "embedded" && !problemsOptions?.onActivate)) {
3180
+ const created = createProblemsPopover({
3181
+ ownerDocument,
3182
+ root,
3183
+ layer,
3184
+ pill: pill.element,
3185
+ id: `cd-shell-problems-${index}`,
3186
+ titleId: `cd-shell-problems-title-${index}`,
3187
+ messages,
3188
+ onSelectItem: problemsOptions?.onSelectItem
3189
+ });
3190
+ popover = created;
3191
+ ledger.defer(() => created.destroy());
3192
+ }
3193
+ const legal = web ? createLegalLine({
3194
+ ownerDocument,
3195
+ messages,
3196
+ options: options.legal ?? {},
3197
+ idPrefix: `cd-shell-legal-${index}`
3198
+ }) : void 0;
3199
+ if (legal) ledger.defer(() => legal.destroy());
1886
3200
  const zoomPanel = createZoomPanel({ ownerDocument, messages, lang, platform, tooltip });
1887
3201
  ledger.defer(() => zoomPanel.destroy());
1888
- root.prepend(menu.trigger, canvas);
3202
+ const toaster = host === "ide" ? void 0 : createToaster({
3203
+ ownerDocument,
3204
+ host,
3205
+ messages,
3206
+ idPrefix: `cd-shell-toast-${index}`,
3207
+ focusFallback: focusCanvasOrTrigger,
3208
+ onChange: () => updateBottomRow()
3209
+ });
3210
+ if (toaster) ledger.defer(() => toaster.destroy());
3211
+ const defined = (element) => element !== void 0;
3212
+ root.prepend(...[menu.trigger, primary?.element, canvas].filter(defined));
1889
3213
  root.append(
1890
3214
  ...[
1891
3215
  status.element,
1892
3216
  hint?.element,
3217
+ emptyState?.element,
1893
3218
  inspector?.element,
1894
3219
  pill?.element,
1895
3220
  legend?.element,
3221
+ legal?.element,
1896
3222
  zoomPanel.element
1897
- ].filter((element) => element !== void 0),
1898
- layer
3223
+ ].filter(defined),
3224
+ layer,
3225
+ ...[toaster?.announcer, toaster?.element].filter(defined)
1899
3226
  );
3227
+ function showToast(request) {
3228
+ return ledger.isClosed || !toaster ? INERT_TOAST : toaster.show(request);
3229
+ }
3230
+ const dropZone = web && options.dropZone ? createDropZone({
3231
+ ownerDocument,
3232
+ layer,
3233
+ messages,
3234
+ lang,
3235
+ options: options.dropZone,
3236
+ modalOpen: () => isModalOpen(ownerDocument),
3237
+ toast: showToast,
3238
+ setState: (state) => setState("data-drop", state)
3239
+ }) : void 0;
3240
+ if (dropZone) ledger.defer(() => dropZone.destroy());
1900
3241
  let palette;
1901
3242
  let paletteRow = "top";
3243
+ let zoomRaised = false;
3244
+ let legalHeight = LEGAL_LINE_HEIGHT;
3245
+ if (legal) ledger.defer(() => root.style.removeProperty(LEGAL_HEIGHT_PROPERTY));
1902
3246
  function containerSize() {
1903
3247
  return { width: root.clientWidth, height: root.clientHeight };
1904
3248
  }
@@ -1918,6 +3262,10 @@ function renderShell(root, options, themes, ledger) {
1918
3262
  paletteRow,
1919
3263
  problemsVisible: pill?.isShown() ?? false
1920
3264
  };
3265
+ if (web) {
3266
+ layout.zoomRaised = zoomRaised;
3267
+ layout.legalHeight = legalHeight;
3268
+ }
1921
3269
  if (legend?.isOpen()) layout.legendWidth = legend.element.getBoundingClientRect().width;
1922
3270
  if (inspector?.isOpen()) {
1923
3271
  layout.inspector = container.width > SHELL.narrow ? {} : { sheetTop: boxRelativeTo(inspector.element, root).y };
@@ -1931,18 +3279,76 @@ function renderShell(root, options, themes, ledger) {
1931
3279
  if (ledger.isClosed) return;
1932
3280
  const size = containerSize();
1933
3281
  if (size.width === 0 || size.height === 0) return;
1934
- paletteRow = decidePaletteRow({
3282
+ const topRow = decideTopRow({
1935
3283
  containerWidth: size.width,
1936
3284
  paletteWidth: paletteWidth(),
1937
- menuRight: menuRight()
3285
+ menuRight: menuRight(),
3286
+ primary: primary?.widths()
1938
3287
  });
3288
+ paletteRow = topRow.paletteRow;
1939
3289
  setState("data-palette", paletteRow);
3290
+ if (primary) {
3291
+ const collapsed = topRow.primaryLabel === "collapsed";
3292
+ primary.setCollapsed(collapsed);
3293
+ setState("data-primary", collapsed ? "collapsed" : null);
3294
+ }
1940
3295
  if (legend && !legend.isStarted()) legend.start(options.legend?.open ?? legendStartsOpen(size.height));
3296
+ updateBottomRow();
1941
3297
  placeHint();
1942
3298
  }
3299
+ function updateBottomRow() {
3300
+ if (ledger.isClosed) return;
3301
+ const size = containerSize();
3302
+ if (size.width === 0 || size.height === 0) return;
3303
+ if (legal) {
3304
+ const legalBox = visibleBoxIn(legal.element, root);
3305
+ const height = legalLineHeight(legalBox?.height);
3306
+ if (height !== legalHeight || !root.style.getPropertyValue(LEGAL_HEIGHT_PROPERTY)) {
3307
+ legalHeight = height;
3308
+ root.style.setProperty(LEGAL_HEIGHT_PROPERTY, `${height}px`);
3309
+ }
3310
+ zoomRaised = zoomRowRaised({
3311
+ containerWidth: size.width,
3312
+ legalRight: legalBox && legalBox.x + legalBox.width,
3313
+ zoomWidth: visibleBoxIn(zoomPanel.element, root)?.width
3314
+ });
3315
+ setState("data-zoom-row", zoomRaised ? "raised" : null);
3316
+ }
3317
+ if (toaster) {
3318
+ const obstacles = [zoomPanel.element, legal?.element, pill?.element].flatMap(
3319
+ (element) => {
3320
+ const box = visibleBoxIn(element, root);
3321
+ return box ? [{ left: box.x, right: box.x + box.width, top: size.height - box.y }] : [];
3322
+ }
3323
+ );
3324
+ toaster.setBottom(
3325
+ toastStackBottom({ containerWidth: size.width, obstacles, cardWidth: toaster.widestCard() })
3326
+ );
3327
+ }
3328
+ }
3329
+ function setEmptyStateShown(shown) {
3330
+ if (ledger.isClosed || !emptyState || emptyState.isShown() === shown) return;
3331
+ const active = ownerDocument.activeElement;
3332
+ const hadFocus = emptyState.element.contains(active);
3333
+ const workHadFocus = shown && active !== null && root.contains(active) && ![emptyState.element, legal?.element, toaster?.element].some((kept) => kept?.contains(active));
3334
+ emptyState.setShown(shown);
3335
+ setState("data-empty-state", shown ? "shown" : null);
3336
+ ledger.attribute(canvas, "inert", shown ? "" : null);
3337
+ if (shown) {
3338
+ menu.close();
3339
+ popover?.close();
3340
+ tooltip.hide();
3341
+ if (workHadFocus) emptyState.focus();
3342
+ } else if (hadFocus) {
3343
+ focusCanvasOrTrigger();
3344
+ }
3345
+ updateLayout();
3346
+ }
1943
3347
  const ResizeObserverClass = globalThis.ResizeObserver;
1944
3348
  const resizeObserver = ResizeObserverClass ? new ResizeObserverClass(() => updateLayout()) : void 0;
1945
3349
  resizeObserver?.observe(root);
3350
+ if (primary) resizeObserver?.observe(primary.element);
3351
+ if (legal) resizeObserver?.observe(legal.element);
1946
3352
  ledger.defer(() => resizeObserver?.disconnect());
1947
3353
  if (legend && options.legend?.open !== void 0) legend.start(options.legend.open);
1948
3354
  updateLayout();
@@ -1951,12 +3357,16 @@ function renderShell(root, options, themes, ledger) {
1951
3357
  host,
1952
3358
  dialogScope: keyTarget,
1953
3359
  menuOpen: () => menu.isOpen(),
3360
+ startScreen: () => emptyState?.isShown() ?? false,
1954
3361
  closeMenu() {
1955
3362
  const active = ownerDocument.activeElement;
1956
3363
  menu.close({ restoreFocus: active === menu.trigger || menu.dropdown.contains(active) });
1957
3364
  },
1958
3365
  hideTooltip: () => tooltip.hide(),
1959
- run: (key) => zoomPanel.run(key)
3366
+ // Behind the start screen the keys reach the browser instead: there is no diagram to zoom.
3367
+ run: (key) => {
3368
+ if (!emptyState?.isShown()) zoomPanel.run(key);
3369
+ }
1960
3370
  });
1961
3371
  keyTarget.addEventListener("keydown", onKeydown);
1962
3372
  ledger.defer(() => keyTarget.removeEventListener("keydown", onKeydown));
@@ -2008,6 +3418,21 @@ function renderShell(root, options, themes, ledger) {
2008
3418
  if (!ledger.isClosed) legend.toggle();
2009
3419
  }
2010
3420
  };
3421
+ const primaryHandle = primary && {
3422
+ element: primary.element,
3423
+ isCollapsed: () => primary.isCollapsed(),
3424
+ refresh: () => {
3425
+ if (!ledger.isClosed) primary.refresh();
3426
+ }
3427
+ };
3428
+ const appearanceHandle = appearance && darkCapable ? {
3429
+ choice: () => appearance.choice(),
3430
+ setChoice: (choice) => {
3431
+ if (ledger.isClosed) return;
3432
+ appearance.setChoice(choice);
3433
+ menu.refresh();
3434
+ }
3435
+ } : void 0;
2011
3436
  const inspectorHandle = inspector && {
2012
3437
  element: inspector.element,
2013
3438
  isOpen: () => inspector.isOpen(),
@@ -2031,6 +3456,8 @@ function renderShell(root, options, themes, ledger) {
2031
3456
  tooltip: tooltipHandle,
2032
3457
  legend: legendHandle,
2033
3458
  inspector: inspectorHandle,
3459
+ primary: primaryHandle,
3460
+ appearance: appearanceHandle,
2034
3461
  setZoomTarget(target) {
2035
3462
  if (!ledger.isClosed) zoomPanel.setTarget(target);
2036
3463
  },
@@ -2059,8 +3486,15 @@ function renderShell(root, options, themes, ledger) {
2059
3486
  },
2060
3487
  setProblems(summary) {
2061
3488
  if (ledger.isClosed || !pill) return;
3489
+ const listHadFocus = popover?.element.contains(ownerDocument.activeElement) ?? false;
2062
3490
  pill.set(summary);
2063
3491
  setState("data-problems", pill.isShown() ? "shown" : null);
3492
+ popover?.setItems(summary.items ?? []);
3493
+ if (!pill.isShown() && popover?.isOpen()) {
3494
+ popover.close();
3495
+ if (listHadFocus) focusCanvasOrTrigger();
3496
+ }
3497
+ updateBottomRow();
2064
3498
  placeHint();
2065
3499
  },
2066
3500
  setStatus(next, statusOptions) {
@@ -2090,6 +3524,9 @@ function renderShell(root, options, themes, ledger) {
2090
3524
  applyReadOnly();
2091
3525
  },
2092
3526
  isReadOnly: () => readOnly,
3527
+ setEmptyState: setEmptyStateShown,
3528
+ isEmptyStateShown: () => emptyState?.isShown() ?? false,
3529
+ toast: showToast,
2093
3530
  destroy() {
2094
3531
  ledger.close();
2095
3532
  }
@@ -2116,21 +3553,27 @@ function carryOwnMode(root, theme, ledger) {
2116
3553
  }
2117
3554
  // Annotate the CommonJS export names for ESM import in node:
2118
3555
  0 && (module.exports = {
3556
+ APPEARANCE_STORAGE_KEY,
2119
3557
  CHROME_ISLANDS,
2120
3558
  FIT_ANIMATION_MS,
2121
3559
  ICONS,
2122
3560
  INVALID_STATUS_DELAY_MS,
3561
+ LEGAL_LINE_HEIGHT,
2123
3562
  MAX_FIT_ZOOM,
3563
+ MAX_TOASTS,
2124
3564
  PALETTE_CLEARANCE,
2125
3565
  PALETTE_TOOL_ICON,
2126
3566
  REDUCED_MOTION_CLASS,
2127
3567
  SHELL,
2128
3568
  SHELL_TOKENS,
2129
3569
  TEXT_FIELD_SELECTOR,
3570
+ TOAST_DURATION_MS,
3571
+ TOAST_MAX_WIDTH,
2130
3572
  TOOLTIP_DELAY_MS,
2131
3573
  VIEW_TOLERANCE,
2132
3574
  ZOOM_MAX,
2133
3575
  ZOOM_MIN,
3576
+ acceptsFile,
2134
3577
  appearanceAttributes,
2135
3578
  appearanceFromBodyClasses,
2136
3579
  appearanceFromColorThemeKind,
@@ -2138,10 +3581,14 @@ function carryOwnMode(root, theme, ledger) {
2138
3581
  ariaKeyShortcutsFromLabel,
2139
3582
  canZoomIn,
2140
3583
  canZoomOut,
3584
+ commandKeyLabel,
2141
3585
  decideKey,
2142
3586
  decidePaletteRow,
3587
+ decideTopRow,
2143
3588
  defaultMessages,
2144
3589
  detectPlatform,
3590
+ documentMode,
3591
+ dragVerdict,
2145
3592
  easeStandard,
2146
3593
  fitInsets,
2147
3594
  fitViewbox,
@@ -2152,10 +3599,13 @@ function carryOwnMode(root, theme, ledger) {
2152
3599
  iconMarkup,
2153
3600
  interpolateViewbox,
2154
3601
  isUndoRedoKey,
3602
+ legalLineHeight,
2155
3603
  legendStartsOpen,
2156
3604
  matchChromeKey,
2157
3605
  mountShell,
3606
+ parseAppearanceChoice,
2158
3607
  placeTooltip,
3608
+ problemSummary,
2159
3609
  reducedMotionFromBodyClasses,
2160
3610
  requestedAppearance,
2161
3611
  resolveAppearance,
@@ -2163,7 +3613,14 @@ function carryOwnMode(root, theme, ledger) {
2163
3613
  sameView,
2164
3614
  shellSizesFromTokens,
2165
3615
  shortcutLabel,
2166
- zoomReadout
3616
+ toastLifetime,
3617
+ toastStackBottom,
3618
+ toastStackWidth,
3619
+ toastToEvict,
3620
+ webHeadMarkup,
3621
+ webModeScript,
3622
+ zoomReadout,
3623
+ zoomRowRaised
2167
3624
  });
2168
3625
  /*! @miragon/modeler-shell icon geometry
2169
3626
  * Lucide (lucide-static 1.59.0) @license ISC