@miragon/modeler-shell 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/API.md +1051 -84
  2. package/README.md +162 -31
  3. package/dist/appearance-BBaM_4bD.d.ts +323 -0
  4. package/dist/appearance-XX3JC3pI.d.cts +323 -0
  5. package/dist/{chunk-6LQOFACK.js → chunk-A4YGKH4Y.js} +1252 -31
  6. package/dist/chunk-A4YGKH4Y.js.map +1 -0
  7. package/dist/{chunk-RJRFR6N3.js → chunk-KYDG5DMX.js} +2 -2
  8. package/dist/{chunk-5JSZ4VGA.js → chunk-OADW3GPA.js} +240 -6
  9. package/dist/chunk-OADW3GPA.js.map +1 -0
  10. package/dist/core.cjs +260 -6
  11. package/dist/core.cjs.map +1 -1
  12. package/dist/core.d.cts +81 -6
  13. package/dist/core.d.ts +81 -6
  14. package/dist/core.js +43 -3
  15. package/dist/diagram-js.cjs +34 -2
  16. package/dist/diagram-js.cjs.map +1 -1
  17. package/dist/diagram-js.d.cts +10 -3
  18. package/dist/diagram-js.d.ts +10 -3
  19. package/dist/diagram-js.js +14 -2
  20. package/dist/diagram-js.js.map +1 -1
  21. package/dist/index.cjs +1658 -196
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.cts +18 -9
  24. package/dist/index.d.ts +18 -9
  25. package/dist/index.js +44 -4
  26. package/dist/shell.css +692 -3
  27. package/dist/testing-vscode-stub.js +2 -2
  28. package/dist/testing.cjs +2849 -550
  29. package/dist/testing.cjs.map +1 -1
  30. package/dist/testing.d.cts +75 -9
  31. package/dist/testing.d.ts +75 -9
  32. package/dist/testing.js +2744 -558
  33. package/dist/testing.js.map +1 -1
  34. package/dist/toasts-Bp0REqdq.d.cts +142 -0
  35. package/dist/toasts-Bp0REqdq.d.ts +142 -0
  36. package/dist/{types-OuFlqZp0.d.cts → types-CnRrINRs.d.cts} +170 -9
  37. package/dist/{types-DnZn9CPw.d.ts → types-PuDZczzy.d.ts} +170 -9
  38. package/dist/vscode-extension.js +2 -2
  39. package/dist/vscode-webview.cjs +1546 -178
  40. package/dist/vscode-webview.cjs.map +1 -1
  41. package/dist/vscode-webview.d.cts +6 -4
  42. package/dist/vscode-webview.d.ts +6 -4
  43. package/dist/vscode-webview.js +11 -2
  44. package/dist/vscode-webview.js.map +1 -1
  45. package/package.json +2 -2
  46. package/src/core/feedback/appearance.ts +141 -0
  47. package/src/core/feedback/problems.ts +23 -0
  48. package/src/core/feedback/toasts.ts +118 -0
  49. package/src/core/fit.ts +18 -3
  50. package/src/core/index.ts +43 -0
  51. package/src/core/keymap.ts +20 -0
  52. package/src/core/messages.ts +71 -0
  53. package/src/core/types.ts +24 -2
  54. package/src/core/web-chrome/bottom-row.ts +37 -0
  55. package/src/core/web-chrome/drop.ts +78 -0
  56. package/src/core/web-chrome/top-row.ts +66 -0
  57. package/src/diagram-js/service.ts +4 -1
  58. package/src/diagram-js/viewport.ts +19 -0
  59. package/src/dom/buttons.css +124 -0
  60. package/src/dom/feedback/appearance.ts +184 -0
  61. package/src/dom/feedback/feedback.css +435 -0
  62. package/src/dom/feedback/problems-popover.ts +287 -0
  63. package/src/dom/feedback/toasts.ts +431 -0
  64. package/src/dom/index.ts +13 -0
  65. package/src/dom/keys.ts +8 -0
  66. package/src/dom/menu.ts +28 -17
  67. package/src/dom/mount.ts +290 -20
  68. package/src/dom/problems.ts +5 -4
  69. package/src/dom/shell-dom.css +7 -4
  70. package/src/dom/tooltip.ts +10 -0
  71. package/src/dom/types.ts +188 -6
  72. package/src/dom/web/drop-zone.ts +181 -0
  73. package/src/dom/web/empty-state.ts +144 -0
  74. package/src/dom/web/legal.ts +75 -0
  75. package/src/dom/web/menu-groups.ts +69 -0
  76. package/src/dom/web/primary.ts +124 -0
  77. package/src/dom/web/web.css +386 -0
  78. package/src/shell.css +3 -0
  79. package/src/testing/case-tools.ts +164 -0
  80. package/src/testing/cases.ts +209 -144
  81. package/src/testing/checks.ts +51 -15
  82. package/src/testing/facts.ts +56 -2
  83. package/src/testing/geometry.ts +206 -26
  84. package/src/testing/harness.ts +56 -7
  85. package/src/testing/index.ts +4 -0
  86. package/src/testing/page-tools.ts +24 -1
  87. package/src/testing/page.ts +10 -0
  88. package/src/testing/web-cases.ts +913 -0
  89. package/src/testing/web-checks.ts +1049 -0
  90. package/src/testing/web-facts.ts +713 -0
  91. package/src/vscode/webview/mount.ts +13 -1
  92. package/dist/chunk-5JSZ4VGA.js.map +0 -1
  93. package/dist/chunk-6LQOFACK.js.map +0 -1
  94. package/dist/keymap-CObQE6q6.d.cts +0 -151
  95. package/dist/keymap-DORzHnK-.d.ts +0 -151
  96. package/dist/types-B6NfkiUG.d.cts +0 -66
  97. package/dist/types-B6NfkiUG.d.ts +0 -66
  98. /package/dist/{chunk-RJRFR6N3.js.map → chunk-KYDG5DMX.js.map} +0 -0
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
1190
  function attributeList(attributes) {
986
- return attributes.map(([name, value]) => ` ${name}="${escapeAttribute(value)}"`).join("");
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 {
@@ -1483,6 +2353,10 @@ function createTooltip({ root, layer, id }) {
1483
2353
  let warmUntil = 0;
1484
2354
  let pressed;
1485
2355
  const detachers = /* @__PURE__ */ new Set();
2356
+ const resizeObserver = typeof ResizeObserver === "function" ? new ResizeObserver(() => {
2357
+ if (shown) place(shown.control);
2358
+ }) : void 0;
2359
+ resizeObserver?.observe(element);
1486
2360
  function cancelPending() {
1487
2361
  if (pending) clearTimeout(pending.timer);
1488
2362
  pending = void 0;
@@ -1650,6 +2524,381 @@ function createTooltip({ root, layer, id }) {
1650
2524
  destroy() {
1651
2525
  hide();
1652
2526
  for (const detach of [...detachers]) detach();
2527
+ resizeObserver?.disconnect();
2528
+ element.remove();
2529
+ }
2530
+ };
2531
+ }
2532
+
2533
+ // src/dom/web/drop-zone.ts
2534
+ var DRAG_EVENTS = ["dragenter", "dragover", "dragleave", "drop"];
2535
+ function carriesFiles(transfer) {
2536
+ if (!transfer) return false;
2537
+ if (Array.from(transfer.types ?? []).includes("Files")) return true;
2538
+ return Array.from(transfer.items ?? []).some((item) => item.kind === "file");
2539
+ }
2540
+ function dragItems(transfer) {
2541
+ const items = Array.from(transfer.items ?? [], (item) => ({ kind: item.kind, type: item.type }));
2542
+ return items.length > 0 ? items : [{ kind: "file", type: "" }];
2543
+ }
2544
+ function acceptList(accept, lang) {
2545
+ const entries = accept.map((entry) => entry.trim()).filter((entry) => entry !== "");
2546
+ const extensions = entries.filter((entry) => entry.startsWith("."));
2547
+ const listed = extensions.length > 0 ? extensions : entries;
2548
+ let format;
2549
+ try {
2550
+ format = new Intl.ListFormat(lang, { type: "disjunction" });
2551
+ } catch {
2552
+ format = new Intl.ListFormat("en", { type: "disjunction" });
2553
+ }
2554
+ return format.format(listed);
2555
+ }
2556
+ function createDropZone(context) {
2557
+ const { ownerDocument, layer, messages, lang, options, modalOpen, toast, setState } = context;
2558
+ const chip = createElement(ownerDocument, "span", { class: "cd-shell-drop-zone-chip" });
2559
+ const element = createElement(
2560
+ ownerDocument,
2561
+ "div",
2562
+ { class: "cd-shell-drop-zone", "data-chrome": "drop-zone", "aria-hidden": "true" },
2563
+ chip
2564
+ );
2565
+ element.hidden = true;
2566
+ layer.append(element);
2567
+ const view = ownerDocument.defaultView;
2568
+ let depth = 0;
2569
+ let current = null;
2570
+ function verdictFor(items) {
2571
+ return dragVerdict(items, {
2572
+ accept: options.accept,
2573
+ multiple: options.multiple ?? false,
2574
+ dialogOpen: modalOpen()
2575
+ });
2576
+ }
2577
+ function show(verdict) {
2578
+ const next = verdict.state === "none" ? null : verdict.state;
2579
+ if (verdict.state === "over") chip.textContent = messages.dropFile;
2580
+ if (verdict.state === "forbidden") chip.textContent = messages.dropForbidden(verdict.reason);
2581
+ if (next === current) return;
2582
+ current = next;
2583
+ element.hidden = next === null;
2584
+ if (next === null) element.removeAttribute("data-state");
2585
+ else element.setAttribute("data-state", next);
2586
+ setState(next);
2587
+ }
2588
+ function track(transfer) {
2589
+ const verdict = verdictFor(dragItems(transfer));
2590
+ show(verdict);
2591
+ transfer.dropEffect = verdict.state === "forbidden" ? "none" : "copy";
2592
+ }
2593
+ function rejectByDefault(files) {
2594
+ const accepted = acceptList(options.accept, lang);
2595
+ for (const file of files) toast({ status: "danger", text: messages.cannotOpenFile(file.name, accepted) });
2596
+ }
2597
+ function receive(files, verdict) {
2598
+ if (verdict.state === "forbidden" && verdict.reason !== "type") return;
2599
+ const accepted = files.filter((file) => acceptsFile(file, options.accept));
2600
+ const rejected = files.filter((file) => !accepted.includes(file));
2601
+ if (accepted.length > 0) options.onDrop(accepted);
2602
+ if (rejected.length > 0) (options.onReject ?? rejectByDefault)(rejected);
2603
+ }
2604
+ const onDragEvent = (event) => {
2605
+ const transfer = event.dataTransfer;
2606
+ if (!carriesFiles(transfer)) return;
2607
+ event.preventDefault();
2608
+ switch (event.type) {
2609
+ case "dragenter":
2610
+ depth += 1;
2611
+ track(transfer);
2612
+ break;
2613
+ case "dragover":
2614
+ depth = Math.max(depth, 1);
2615
+ track(transfer);
2616
+ break;
2617
+ case "dragleave":
2618
+ depth = Math.max(0, depth - 1);
2619
+ if (depth === 0) show({ state: "none" });
2620
+ break;
2621
+ case "drop": {
2622
+ depth = 0;
2623
+ show({ state: "none" });
2624
+ const files = Array.from(transfer.files ?? []);
2625
+ const items = files.length > 0 ? files.map((file) => ({ kind: "file", type: file.type })) : dragItems(transfer);
2626
+ receive(files, verdictFor(items));
2627
+ break;
2628
+ }
2629
+ }
2630
+ };
2631
+ for (const type of DRAG_EVENTS) view?.addEventListener(type, onDragEvent);
2632
+ return {
2633
+ element,
2634
+ state: () => current,
2635
+ destroy() {
2636
+ for (const type of DRAG_EVENTS) view?.removeEventListener(type, onDragEvent);
2637
+ depth = 0;
2638
+ if (current !== null) {
2639
+ current = null;
2640
+ setState(null);
2641
+ }
2642
+ element.remove();
2643
+ }
2644
+ };
2645
+ }
2646
+
2647
+ // src/dom/web/empty-state.ts
2648
+ var KOMET_SIZE = "64";
2649
+ function createEmptyState(context) {
2650
+ const { ownerDocument, messages, options, dropHint, open, titleId, onLeave } = context;
2651
+ const button = (variant, label, run) => {
2652
+ const element2 = createElement(
2653
+ ownerDocument,
2654
+ "button",
2655
+ { type: "button", class: "cd-shell-button", "data-variant": variant },
2656
+ label
2657
+ );
2658
+ element2.addEventListener("click", () => {
2659
+ onLeave();
2660
+ run();
2661
+ });
2662
+ return element2;
2663
+ };
2664
+ const onShowExample = options.onShowExample;
2665
+ const newDiagram = button("primary", messages.newDiagram, () => options.onNewDiagram());
2666
+ const element = createElement(
2667
+ ownerDocument,
2668
+ "section",
2669
+ { class: "cd-shell-empty-state", "data-chrome": "empty-state", "aria-labelledby": titleId },
2670
+ // Dragging the mark would start an image drag that some browsers carry as a file.
2671
+ createElement(ownerDocument, "img", {
2672
+ class: "cd-shell-empty-state-mark",
2673
+ src: options.komet,
2674
+ width: KOMET_SIZE,
2675
+ height: KOMET_SIZE,
2676
+ alt: "",
2677
+ draggable: "false"
2678
+ }),
2679
+ createElement(ownerDocument, "h1", { class: "cd-shell-empty-state-title", id: titleId }, options.title),
2680
+ createElement(ownerDocument, "p", { class: "cd-shell-empty-state-text" }, options.text),
2681
+ createElement(
2682
+ ownerDocument,
2683
+ "div",
2684
+ { class: "cd-shell-empty-state-actions" },
2685
+ newDiagram,
2686
+ onShowExample && button("ghost", messages.showExample, () => onShowExample())
2687
+ ),
2688
+ hintParagraph()
2689
+ );
2690
+ function hintParagraph() {
2691
+ const hasDropHint = dropHint !== void 0 && dropHint !== "";
2692
+ if (!hasDropHint && !open) return void 0;
2693
+ const paragraph = createElement(ownerDocument, "p", { class: "cd-shell-empty-state-hint" });
2694
+ if (hasDropHint) paragraph.append(dropHint);
2695
+ if (!open) return paragraph;
2696
+ const shortcut = open.shortcut?.trim();
2697
+ const link = createElement(
2698
+ ownerDocument,
2699
+ "button",
2700
+ {
2701
+ type: "button",
2702
+ class: "cd-shell-empty-state-open",
2703
+ "aria-keyshortcuts": shortcut ? ariaKeyShortcutsFromLabel(shortcut) : void 0,
2704
+ "aria-haspopup": open.dialog ? "dialog" : void 0
2705
+ },
2706
+ messages.openFileLink
2707
+ );
2708
+ link.addEventListener("click", () => open.onSelect());
2709
+ if (hasDropHint) {
2710
+ paragraph.append(" ", createElement(ownerDocument, "span", { "aria-hidden": "true" }, "\xB7"), " ");
2711
+ }
2712
+ const openItem = createElement(ownerDocument, "span", { class: "cd-shell-empty-state-open-item" }, link);
2713
+ if (shortcut) {
2714
+ openItem.append(
2715
+ " ",
2716
+ createElement(
2717
+ ownerDocument,
2718
+ "kbd",
2719
+ { class: "cd-shell-empty-state-key", "aria-hidden": "true" },
2720
+ shortcut
2721
+ )
2722
+ );
2723
+ }
2724
+ paragraph.append(openItem);
2725
+ return paragraph;
2726
+ }
2727
+ element.hidden = true;
2728
+ return {
2729
+ element,
2730
+ isShown: () => !element.hidden,
2731
+ setShown(shown) {
2732
+ element.hidden = !shown;
2733
+ },
2734
+ focus() {
2735
+ newDiagram.focus({ preventScroll: true });
2736
+ },
2737
+ destroy() {
2738
+ element.remove();
2739
+ }
2740
+ };
2741
+ }
2742
+
2743
+ // src/dom/web/legal.ts
2744
+ var MIRAGON_HOME = "https://www.miragon.io/";
2745
+ var MIRAGON_IMPRESSUM = "https://www.miragon.io/impressum/";
2746
+ var MIRAGON_DATENSCHUTZ = "https://www.miragon.io/datenschutz/";
2747
+ function createLegalLine(context) {
2748
+ const { ownerDocument, messages, options } = context;
2749
+ const year = options.year ?? (/* @__PURE__ */ new Date()).getFullYear();
2750
+ const newTabId = `${context.idPrefix}-new-tab`;
2751
+ const link = (label, href, lang) => createElement(
2752
+ ownerDocument,
2753
+ "a",
2754
+ { href, lang, target: "_blank", rel: "noopener noreferrer", "aria-describedby": newTabId },
2755
+ label
2756
+ );
2757
+ const separator = () => createElement(ownerDocument, "span", { "aria-hidden": "true" }, "\xB7");
2758
+ const item = (anchor) => createElement(ownerDocument, "span", { class: "cd-shell-legal-item" }, separator(), anchor);
2759
+ const links = [
2760
+ link(messages.copyright(year), MIRAGON_HOME),
2761
+ link("Impressum", MIRAGON_IMPRESSUM, "de"),
2762
+ link("Datenschutz", MIRAGON_DATENSCHUTZ, "de"),
2763
+ ...(options.links ?? []).map((extra) => link(extra.label, extra.href, extra.lang))
2764
+ ];
2765
+ const element = createElement(ownerDocument, "nav", {
2766
+ class: "cd-shell-legal",
2767
+ "data-chrome": "legal",
2768
+ "aria-label": messages.legal
2769
+ });
2770
+ element.append(
2771
+ ...links.map((anchor, index) => index === 0 ? anchor : item(anchor)),
2772
+ createElement(ownerDocument, "span", { id: newTabId, hidden: "" }, messages.opensInNewTab)
2773
+ );
2774
+ return {
2775
+ element,
2776
+ destroy() {
2777
+ element.remove();
2778
+ }
2779
+ };
2780
+ }
2781
+
2782
+ // src/dom/web/menu-groups.ts
2783
+ function fileEntry(spec, action) {
2784
+ const common = {
2785
+ id: spec.id,
2786
+ label: spec.label,
2787
+ ...spec.icon ? { icon: spec.icon } : {},
2788
+ ...action.disabled ? { disabled: action.disabled } : {},
2789
+ ...action.shortcut ? { shortcut: action.shortcut } : {},
2790
+ onSelect: () => action.onSelect()
2791
+ };
2792
+ return action.dialog ? { kind: "dialog", ...common } : { kind: "action", ...common };
2793
+ }
2794
+ function fileMenuEntries(options, messages) {
2795
+ const specs = [
2796
+ { option: "newDiagram", id: "new-diagram", label: messages.newDiagram, icon: "file-plus" },
2797
+ { option: "open", id: "open", label: messages.openFile, icon: "folder-open" },
2798
+ // §5.14 gives "Show example" no icon.
2799
+ { option: "showExample", id: "show-example", label: messages.showExample, icon: void 0 },
2800
+ { option: "import", id: "import", label: messages.importFile, icon: "import" }
2801
+ ];
2802
+ return specs.flatMap((spec) => {
2803
+ const action = options[spec.option];
2804
+ return action ? [fileEntry(spec, action)] : [];
2805
+ });
2806
+ }
2807
+ function appearanceMenuEntries(appearance, messages) {
2808
+ return [
2809
+ {
2810
+ kind: "radio",
2811
+ id: "appearance",
2812
+ label: messages.appearance,
2813
+ options: [
2814
+ { value: "system", label: messages.appearanceSystem },
2815
+ { value: "light", label: messages.appearanceLight },
2816
+ { value: "dark", label: messages.appearanceDark }
2817
+ ],
2818
+ value: () => appearance.choice(),
2819
+ onSelect: (value) => appearance.setChoice(parseAppearanceChoice(value))
2820
+ }
2821
+ ];
2822
+ }
2823
+
2824
+ // src/dom/web/primary.ts
2825
+ var COLLAPSED_ATTRIBUTE = "data-collapsed";
2826
+ function createPrimaryAction(context) {
2827
+ const { ownerDocument, messages, tooltip, options } = context;
2828
+ const label = options.label ?? messages.share;
2829
+ const labelElement = createElement(ownerDocument, "span", { class: "cd-shell-primary-label" }, label);
2830
+ const element = createElement(
2831
+ ownerDocument,
2832
+ "button",
2833
+ {
2834
+ type: "button",
2835
+ class: "cd-shell-primary cd-shell-button",
2836
+ "data-variant": "primary",
2837
+ "data-chrome": "primary",
2838
+ "aria-label": label
2839
+ },
2840
+ iconElement(options.icon ?? "share-2"),
2841
+ labelElement
2842
+ );
2843
+ let collapsed = false;
2844
+ let disabled = false;
2845
+ let labelledWidth;
2846
+ function readDisabled() {
2847
+ disabled = options.disabled?.() ?? false;
2848
+ toggleAttribute(element, "aria-disabled", disabled ? "true" : null);
2849
+ }
2850
+ function showCollapsed(state) {
2851
+ labelElement.hidden = state;
2852
+ toggleAttribute(element, COLLAPSED_ATTRIBUTE, state ? "" : null);
2853
+ }
2854
+ function measureLabelledWidth() {
2855
+ if (!collapsed) return element.offsetWidth;
2856
+ showCollapsed(false);
2857
+ const width = element.offsetWidth;
2858
+ showCollapsed(true);
2859
+ return width;
2860
+ }
2861
+ function isEngaged() {
2862
+ if (ownerDocument.activeElement === element) return true;
2863
+ try {
2864
+ return element.matches(":hover");
2865
+ } catch {
2866
+ return false;
2867
+ }
2868
+ }
2869
+ const onClick = (event) => {
2870
+ if (disabled) {
2871
+ event.preventDefault();
2872
+ return;
2873
+ }
2874
+ options.onSelect();
2875
+ };
2876
+ element.addEventListener("click", onClick);
2877
+ const detachTooltip = tooltip.attach(element, () => collapsed ? { label } : null);
2878
+ readDisabled();
2879
+ return {
2880
+ element,
2881
+ isCollapsed: () => collapsed,
2882
+ refresh() {
2883
+ readDisabled();
2884
+ labelledWidth = void 0;
2885
+ },
2886
+ widths() {
2887
+ if (!collapsed || labelledWidth === void 0) {
2888
+ const measured = measureLabelledWidth();
2889
+ if (measured > 0) labelledWidth = measured;
2890
+ }
2891
+ return { withLabel: labelledWidth ?? 0, iconOnly: SHELL.row };
2892
+ },
2893
+ setCollapsed(next) {
2894
+ if (next === collapsed) return;
2895
+ collapsed = next;
2896
+ showCollapsed(next);
2897
+ if (!next && isEngaged()) tooltip.hide();
2898
+ },
2899
+ destroy() {
2900
+ detachTooltip();
2901
+ element.removeEventListener("click", onClick);
1653
2902
  element.remove();
1654
2903
  }
1655
2904
  };
@@ -1772,6 +3021,16 @@ function nextMountIndex() {
1772
3021
  return index;
1773
3022
  }
1774
3023
  var DEFAULT_THEMES = ["light"];
3024
+ var LEGAL_HEIGHT_PROPERTY = "--modeler-shell-legal-height";
3025
+ var INERT_TOAST = Object.freeze({ dismiss() {
3026
+ } });
3027
+ function visibleBoxIn(element, root) {
3028
+ if (!element || element.hidden || !element.isConnected) return void 0;
3029
+ if (typeof element.checkVisibility === "function" && !element.checkVisibility({ visibilityProperty: true }))
3030
+ return void 0;
3031
+ const box = boxRelativeTo(element, root);
3032
+ return box.width > 0 && box.height > 0 ? box : void 0;
3033
+ }
1775
3034
  function languageOf(root, host, requested) {
1776
3035
  if (requested) return requested;
1777
3036
  const declared = host === "embedded" ? root.getAttribute("lang") : root.closest("[lang]")?.getAttribute("lang");
@@ -1800,6 +3059,7 @@ function renderShell(root, options, themes, ledger) {
1800
3059
  const platform = options.platform ?? detectPlatform(globalThis.navigator);
1801
3060
  const lang = languageOf(root, host, options.lang);
1802
3061
  const keyScope = options.keyScope ?? (host === "embedded" ? "root" : "document");
3062
+ const web = host === "web";
1803
3063
  ledger.addClass(root, "cd-shell");
1804
3064
  ledger.attribute(root, "data-cd-shell-host", host);
1805
3065
  ledger.attribute(root, "data-palette", "top");
@@ -1812,6 +3072,9 @@ function renderShell(root, options, themes, ledger) {
1812
3072
  const setState = (name, value) => {
1813
3073
  if (!ledger.isClosed) ledger.attribute(root, name, value);
1814
3074
  };
3075
+ const appearance = web ? connectWebAppearance({ ownerDocument, themes, options: options.appearance }) : void 0;
3076
+ if (appearance) ledger.defer(() => appearance.destroy());
3077
+ const darkCapable = themes.includes("dark");
1815
3078
  const layer = createElement(ownerDocument, "div", { class: "cd-shell-layer" });
1816
3079
  ledger.defer(() => layer.remove());
1817
3080
  const tooltip = createTooltip({ root, layer, id: `cd-shell-tooltip-${index}` });
@@ -1843,6 +3106,14 @@ function renderShell(root, options, themes, ledger) {
1843
3106
  keepEnabledWhenReadOnly: true
1844
3107
  } : void 0;
1845
3108
  const shellModelEntries = legendEntry ? [legendEntry] : [];
3109
+ const fileEntries = web && options.file ? fileMenuEntries(options.file, messages) : [];
3110
+ const appearanceEntries = appearance && darkCapable ? appearanceMenuEntries(appearance, messages) : [];
3111
+ const shellEntries = (group) => {
3112
+ if (group === "file") return fileEntries;
3113
+ if (group === "model") return shellModelEntries;
3114
+ if (group === "appearance") return appearanceEntries;
3115
+ return [];
3116
+ };
1846
3117
  const menu = createMenu({
1847
3118
  root,
1848
3119
  layer,
@@ -1850,55 +3121,133 @@ function renderShell(root, options, themes, ledger) {
1850
3121
  messages,
1851
3122
  tooltip,
1852
3123
  groups: options.menu ?? {},
1853
- shellModelEntries: () => shellModelEntries,
3124
+ shellEntries,
1854
3125
  isReadOnly: () => readOnly
1855
3126
  });
1856
3127
  ledger.defer(() => menu.destroy());
1857
3128
  ledger.defer(tooltip.attach(menu.trigger, () => menu.isOpen() ? null : { label: messages.menu }));
3129
+ const primary = web && options.primary ? createPrimaryAction({ ownerDocument, messages, tooltip, options: options.primary }) : void 0;
3130
+ if (primary) ledger.defer(() => primary.destroy());
1858
3131
  const canvas = options.canvas ?? createElement(ownerDocument, "div");
1859
3132
  ledger.addClass(canvas, "cd-shell-canvas");
3133
+ if (web && !canvas.hasAttribute("role")) ledger.attribute(canvas, "role", "main");
1860
3134
  if (!options.canvas) ledger.defer(() => canvas.remove());
3135
+ function focusCanvasOrTrigger() {
3136
+ const took = (target) => {
3137
+ target?.focus({ preventScroll: true });
3138
+ return target !== null && target !== void 0 && ownerDocument.activeElement === target;
3139
+ };
3140
+ if (emptyState?.isShown()) {
3141
+ emptyState.focus();
3142
+ if (emptyState.element.contains(ownerDocument.activeElement)) return;
3143
+ }
3144
+ if (took(canvas.querySelector("svg[tabindex]"))) return;
3145
+ if (!menu.trigger.hidden) took(menu.trigger);
3146
+ }
1861
3147
  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 });
3148
+ if (island.contains(ownerDocument.activeElement)) focusCanvasOrTrigger();
1867
3149
  }
1868
3150
  const status = createStatusBox(ownerDocument);
1869
3151
  ledger.defer(() => status.destroy());
1870
3152
  const hint = options.emptyHint ? createEmptyHint(ownerDocument, messages, options.emptyHint) : void 0;
1871
3153
  if (hint) ledger.defer(() => hint.destroy());
3154
+ const emptyState = web && options.emptyState ? createEmptyState({
3155
+ ownerDocument,
3156
+ messages,
3157
+ options: options.emptyState,
3158
+ dropHint: options.dropZone ? options.emptyState.dropHint ?? messages.dropHint : void 0,
3159
+ open: options.file?.open,
3160
+ titleId: `cd-shell-empty-state-title-${index}`,
3161
+ onLeave: () => setEmptyStateShown(false)
3162
+ }) : void 0;
3163
+ if (emptyState) ledger.defer(() => emptyState.destroy());
1872
3164
  const inspector = options.inspector ? createInspector(ownerDocument, options.inspector, (open) => {
1873
3165
  setState("data-inspector", open ? "open" : null);
3166
+ updateBottomRow();
1874
3167
  placeHint();
1875
3168
  }) : void 0;
1876
3169
  if (inspector) ledger.defer(() => inspector.destroy());
1877
- const pill = options.problems ? createProblemsPill({
3170
+ const problemsOptions = options.problems;
3171
+ let popover;
3172
+ const pill = problemsOptions ? createProblemsPill({
1878
3173
  ownerDocument,
1879
3174
  messages,
1880
3175
  lang,
1881
3176
  tooltip,
1882
3177
  showTooltip: host === "ide",
1883
- options: options.problems
3178
+ onActivate() {
3179
+ if (popover) popover.toggle();
3180
+ else problemsOptions.onActivate?.();
3181
+ }
1884
3182
  }) : void 0;
1885
3183
  if (pill) ledger.defer(() => pill.destroy());
3184
+ if (pill && (web || host === "embedded" && !problemsOptions?.onActivate)) {
3185
+ const created = createProblemsPopover({
3186
+ ownerDocument,
3187
+ root,
3188
+ layer,
3189
+ pill: pill.element,
3190
+ id: `cd-shell-problems-${index}`,
3191
+ titleId: `cd-shell-problems-title-${index}`,
3192
+ messages,
3193
+ onSelectItem: problemsOptions?.onSelectItem
3194
+ });
3195
+ popover = created;
3196
+ ledger.defer(() => created.destroy());
3197
+ }
3198
+ const legal = web ? createLegalLine({
3199
+ ownerDocument,
3200
+ messages,
3201
+ options: options.legal ?? {},
3202
+ idPrefix: `cd-shell-legal-${index}`
3203
+ }) : void 0;
3204
+ if (legal) ledger.defer(() => legal.destroy());
1886
3205
  const zoomPanel = createZoomPanel({ ownerDocument, messages, lang, platform, tooltip });
1887
3206
  ledger.defer(() => zoomPanel.destroy());
1888
- root.prepend(menu.trigger, canvas);
3207
+ const toaster = host === "ide" ? void 0 : createToaster({
3208
+ ownerDocument,
3209
+ host,
3210
+ messages,
3211
+ idPrefix: `cd-shell-toast-${index}`,
3212
+ focusFallback: focusCanvasOrTrigger,
3213
+ onChange: () => updateBottomRow()
3214
+ });
3215
+ if (toaster) ledger.defer(() => toaster.destroy());
3216
+ const defined = (element) => element !== void 0;
3217
+ root.prepend(...[menu.trigger, primary?.element, canvas].filter(defined));
1889
3218
  root.append(
1890
3219
  ...[
1891
3220
  status.element,
1892
3221
  hint?.element,
3222
+ emptyState?.element,
1893
3223
  inspector?.element,
1894
3224
  pill?.element,
1895
3225
  legend?.element,
3226
+ legal?.element,
1896
3227
  zoomPanel.element
1897
- ].filter((element) => element !== void 0),
1898
- layer
3228
+ ].filter(defined),
3229
+ layer,
3230
+ ...[toaster?.announcer, toaster?.element].filter(defined)
1899
3231
  );
3232
+ function showToast(request) {
3233
+ return ledger.isClosed || !toaster ? INERT_TOAST : toaster.show(request);
3234
+ }
3235
+ const dropZone = web && options.dropZone ? createDropZone({
3236
+ ownerDocument,
3237
+ layer,
3238
+ messages,
3239
+ lang,
3240
+ options: options.dropZone,
3241
+ modalOpen: () => isModalOpen(ownerDocument),
3242
+ toast: showToast,
3243
+ setState: (state) => setState("data-drop", state)
3244
+ }) : void 0;
3245
+ if (dropZone) ledger.defer(() => dropZone.destroy());
1900
3246
  let palette;
1901
3247
  let paletteRow = "top";
3248
+ let zoomRaised = false;
3249
+ let legalHeight = LEGAL_LINE_HEIGHT;
3250
+ if (legal) ledger.defer(() => root.style.removeProperty(LEGAL_HEIGHT_PROPERTY));
1902
3251
  function containerSize() {
1903
3252
  return { width: root.clientWidth, height: root.clientHeight };
1904
3253
  }
@@ -1918,6 +3267,10 @@ function renderShell(root, options, themes, ledger) {
1918
3267
  paletteRow,
1919
3268
  problemsVisible: pill?.isShown() ?? false
1920
3269
  };
3270
+ if (web) {
3271
+ layout.zoomRaised = zoomRaised;
3272
+ layout.legalHeight = legalHeight;
3273
+ }
1921
3274
  if (legend?.isOpen()) layout.legendWidth = legend.element.getBoundingClientRect().width;
1922
3275
  if (inspector?.isOpen()) {
1923
3276
  layout.inspector = container.width > SHELL.narrow ? {} : { sheetTop: boxRelativeTo(inspector.element, root).y };
@@ -1931,18 +3284,76 @@ function renderShell(root, options, themes, ledger) {
1931
3284
  if (ledger.isClosed) return;
1932
3285
  const size = containerSize();
1933
3286
  if (size.width === 0 || size.height === 0) return;
1934
- paletteRow = decidePaletteRow({
3287
+ const topRow = decideTopRow({
1935
3288
  containerWidth: size.width,
1936
3289
  paletteWidth: paletteWidth(),
1937
- menuRight: menuRight()
3290
+ menuRight: menuRight(),
3291
+ primary: primary?.widths()
1938
3292
  });
3293
+ paletteRow = topRow.paletteRow;
1939
3294
  setState("data-palette", paletteRow);
3295
+ if (primary) {
3296
+ const collapsed = topRow.primaryLabel === "collapsed";
3297
+ primary.setCollapsed(collapsed);
3298
+ setState("data-primary", collapsed ? "collapsed" : null);
3299
+ }
1940
3300
  if (legend && !legend.isStarted()) legend.start(options.legend?.open ?? legendStartsOpen(size.height));
3301
+ updateBottomRow();
1941
3302
  placeHint();
1942
3303
  }
3304
+ function updateBottomRow() {
3305
+ if (ledger.isClosed) return;
3306
+ const size = containerSize();
3307
+ if (size.width === 0 || size.height === 0) return;
3308
+ if (legal) {
3309
+ const legalBox = visibleBoxIn(legal.element, root);
3310
+ const height = legalLineHeight(legalBox?.height);
3311
+ if (height !== legalHeight || !root.style.getPropertyValue(LEGAL_HEIGHT_PROPERTY)) {
3312
+ legalHeight = height;
3313
+ root.style.setProperty(LEGAL_HEIGHT_PROPERTY, `${height}px`);
3314
+ }
3315
+ zoomRaised = zoomRowRaised({
3316
+ containerWidth: size.width,
3317
+ legalRight: legalBox && legalBox.x + legalBox.width,
3318
+ zoomWidth: visibleBoxIn(zoomPanel.element, root)?.width
3319
+ });
3320
+ setState("data-zoom-row", zoomRaised ? "raised" : null);
3321
+ }
3322
+ if (toaster) {
3323
+ const obstacles = [zoomPanel.element, legal?.element, pill?.element].flatMap(
3324
+ (element) => {
3325
+ const box = visibleBoxIn(element, root);
3326
+ return box ? [{ left: box.x, right: box.x + box.width, top: size.height - box.y }] : [];
3327
+ }
3328
+ );
3329
+ toaster.setBottom(
3330
+ toastStackBottom({ containerWidth: size.width, obstacles, cardWidth: toaster.widestCard() })
3331
+ );
3332
+ }
3333
+ }
3334
+ function setEmptyStateShown(shown) {
3335
+ if (ledger.isClosed || !emptyState || emptyState.isShown() === shown) return;
3336
+ const active = ownerDocument.activeElement;
3337
+ const hadFocus = emptyState.element.contains(active);
3338
+ const workHadFocus = shown && active !== null && root.contains(active) && ![emptyState.element, legal?.element, toaster?.element].some((kept) => kept?.contains(active));
3339
+ emptyState.setShown(shown);
3340
+ setState("data-empty-state", shown ? "shown" : null);
3341
+ ledger.attribute(canvas, "inert", shown ? "" : null);
3342
+ if (shown) {
3343
+ menu.close();
3344
+ popover?.close();
3345
+ tooltip.hide();
3346
+ if (workHadFocus) emptyState.focus();
3347
+ } else if (hadFocus) {
3348
+ focusCanvasOrTrigger();
3349
+ }
3350
+ updateLayout();
3351
+ }
1943
3352
  const ResizeObserverClass = globalThis.ResizeObserver;
1944
3353
  const resizeObserver = ResizeObserverClass ? new ResizeObserverClass(() => updateLayout()) : void 0;
1945
3354
  resizeObserver?.observe(root);
3355
+ if (primary) resizeObserver?.observe(primary.element);
3356
+ if (legal) resizeObserver?.observe(legal.element);
1946
3357
  ledger.defer(() => resizeObserver?.disconnect());
1947
3358
  if (legend && options.legend?.open !== void 0) legend.start(options.legend.open);
1948
3359
  updateLayout();
@@ -1951,12 +3362,16 @@ function renderShell(root, options, themes, ledger) {
1951
3362
  host,
1952
3363
  dialogScope: keyTarget,
1953
3364
  menuOpen: () => menu.isOpen(),
3365
+ startScreen: () => emptyState?.isShown() ?? false,
1954
3366
  closeMenu() {
1955
3367
  const active = ownerDocument.activeElement;
1956
3368
  menu.close({ restoreFocus: active === menu.trigger || menu.dropdown.contains(active) });
1957
3369
  },
1958
3370
  hideTooltip: () => tooltip.hide(),
1959
- run: (key) => zoomPanel.run(key)
3371
+ // Behind the start screen the keys reach the browser instead: there is no diagram to zoom.
3372
+ run: (key) => {
3373
+ if (!emptyState?.isShown()) zoomPanel.run(key);
3374
+ }
1960
3375
  });
1961
3376
  keyTarget.addEventListener("keydown", onKeydown);
1962
3377
  ledger.defer(() => keyTarget.removeEventListener("keydown", onKeydown));
@@ -2008,6 +3423,21 @@ function renderShell(root, options, themes, ledger) {
2008
3423
  if (!ledger.isClosed) legend.toggle();
2009
3424
  }
2010
3425
  };
3426
+ const primaryHandle = primary && {
3427
+ element: primary.element,
3428
+ isCollapsed: () => primary.isCollapsed(),
3429
+ refresh: () => {
3430
+ if (!ledger.isClosed) primary.refresh();
3431
+ }
3432
+ };
3433
+ const appearanceHandle = appearance && darkCapable ? {
3434
+ choice: () => appearance.choice(),
3435
+ setChoice: (choice) => {
3436
+ if (ledger.isClosed) return;
3437
+ appearance.setChoice(choice);
3438
+ menu.refresh();
3439
+ }
3440
+ } : void 0;
2011
3441
  const inspectorHandle = inspector && {
2012
3442
  element: inspector.element,
2013
3443
  isOpen: () => inspector.isOpen(),
@@ -2031,6 +3461,8 @@ function renderShell(root, options, themes, ledger) {
2031
3461
  tooltip: tooltipHandle,
2032
3462
  legend: legendHandle,
2033
3463
  inspector: inspectorHandle,
3464
+ primary: primaryHandle,
3465
+ appearance: appearanceHandle,
2034
3466
  setZoomTarget(target) {
2035
3467
  if (!ledger.isClosed) zoomPanel.setTarget(target);
2036
3468
  },
@@ -2059,8 +3491,15 @@ function renderShell(root, options, themes, ledger) {
2059
3491
  },
2060
3492
  setProblems(summary) {
2061
3493
  if (ledger.isClosed || !pill) return;
3494
+ const listHadFocus = popover?.element.contains(ownerDocument.activeElement) ?? false;
2062
3495
  pill.set(summary);
2063
3496
  setState("data-problems", pill.isShown() ? "shown" : null);
3497
+ popover?.setItems(summary.items ?? []);
3498
+ if (!pill.isShown() && popover?.isOpen()) {
3499
+ popover.close();
3500
+ if (listHadFocus) focusCanvasOrTrigger();
3501
+ }
3502
+ updateBottomRow();
2064
3503
  placeHint();
2065
3504
  },
2066
3505
  setStatus(next, statusOptions) {
@@ -2090,6 +3529,9 @@ function renderShell(root, options, themes, ledger) {
2090
3529
  applyReadOnly();
2091
3530
  },
2092
3531
  isReadOnly: () => readOnly,
3532
+ setEmptyState: setEmptyStateShown,
3533
+ isEmptyStateShown: () => emptyState?.isShown() ?? false,
3534
+ toast: showToast,
2093
3535
  destroy() {
2094
3536
  ledger.close();
2095
3537
  }
@@ -2116,21 +3558,27 @@ function carryOwnMode(root, theme, ledger) {
2116
3558
  }
2117
3559
  // Annotate the CommonJS export names for ESM import in node:
2118
3560
  0 && (module.exports = {
3561
+ APPEARANCE_STORAGE_KEY,
2119
3562
  CHROME_ISLANDS,
2120
3563
  FIT_ANIMATION_MS,
2121
3564
  ICONS,
2122
3565
  INVALID_STATUS_DELAY_MS,
3566
+ LEGAL_LINE_HEIGHT,
2123
3567
  MAX_FIT_ZOOM,
3568
+ MAX_TOASTS,
2124
3569
  PALETTE_CLEARANCE,
2125
3570
  PALETTE_TOOL_ICON,
2126
3571
  REDUCED_MOTION_CLASS,
2127
3572
  SHELL,
2128
3573
  SHELL_TOKENS,
2129
3574
  TEXT_FIELD_SELECTOR,
3575
+ TOAST_DURATION_MS,
3576
+ TOAST_MAX_WIDTH,
2130
3577
  TOOLTIP_DELAY_MS,
2131
3578
  VIEW_TOLERANCE,
2132
3579
  ZOOM_MAX,
2133
3580
  ZOOM_MIN,
3581
+ acceptsFile,
2134
3582
  appearanceAttributes,
2135
3583
  appearanceFromBodyClasses,
2136
3584
  appearanceFromColorThemeKind,
@@ -2138,10 +3586,14 @@ function carryOwnMode(root, theme, ledger) {
2138
3586
  ariaKeyShortcutsFromLabel,
2139
3587
  canZoomIn,
2140
3588
  canZoomOut,
3589
+ commandKeyLabel,
2141
3590
  decideKey,
2142
3591
  decidePaletteRow,
3592
+ decideTopRow,
2143
3593
  defaultMessages,
2144
3594
  detectPlatform,
3595
+ documentMode,
3596
+ dragVerdict,
2145
3597
  easeStandard,
2146
3598
  fitInsets,
2147
3599
  fitViewbox,
@@ -2152,10 +3604,13 @@ function carryOwnMode(root, theme, ledger) {
2152
3604
  iconMarkup,
2153
3605
  interpolateViewbox,
2154
3606
  isUndoRedoKey,
3607
+ legalLineHeight,
2155
3608
  legendStartsOpen,
2156
3609
  matchChromeKey,
2157
3610
  mountShell,
3611
+ parseAppearanceChoice,
2158
3612
  placeTooltip,
3613
+ problemSummary,
2159
3614
  reducedMotionFromBodyClasses,
2160
3615
  requestedAppearance,
2161
3616
  resolveAppearance,
@@ -2163,7 +3618,14 @@ function carryOwnMode(root, theme, ledger) {
2163
3618
  sameView,
2164
3619
  shellSizesFromTokens,
2165
3620
  shortcutLabel,
2166
- zoomReadout
3621
+ toastLifetime,
3622
+ toastStackBottom,
3623
+ toastStackWidth,
3624
+ toastToEvict,
3625
+ webHeadMarkup,
3626
+ webModeScript,
3627
+ zoomReadout,
3628
+ zoomRowRaised
2167
3629
  });
2168
3630
  /*! @miragon/modeler-shell icon geometry
2169
3631
  * Lucide (lucide-static 1.59.0) @license ISC