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