@miragon/modeler-shell 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +2869 -0
- package/LICENSE +21 -0
- package/README.md +566 -2
- package/THIRD_PARTY_NOTICES.md +52 -0
- package/dist/appearance-BBaM_4bD.d.ts +323 -0
- package/dist/appearance-D3FUcbu6.d.cts +53 -0
- package/dist/appearance-D3FUcbu6.d.ts +53 -0
- package/dist/appearance-XX3JC3pI.d.cts +323 -0
- package/dist/chunk-66HCSJNY.js +2792 -0
- package/dist/chunk-66HCSJNY.js.map +1 -0
- package/dist/chunk-KYDG5DMX.js +136 -0
- package/dist/chunk-KYDG5DMX.js.map +1 -0
- package/dist/chunk-N4QXFF3A.js +23 -0
- package/dist/chunk-N4QXFF3A.js.map +1 -0
- package/dist/chunk-OADW3GPA.js +785 -0
- package/dist/chunk-OADW3GPA.js.map +1 -0
- package/dist/core.cjs +873 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.d.cts +246 -0
- package/dist/core.d.ts +246 -0
- package/dist/core.js +131 -0
- package/dist/core.js.map +1 -0
- package/dist/diagram-js.cjs +1147 -0
- package/dist/diagram-js.cjs.map +1 -0
- package/dist/diagram-js.d.cts +170 -0
- package/dist/diagram-js.d.ts +170 -0
- package/dist/diagram-js.js +879 -0
- package/dist/diagram-js.js.map +1 -0
- package/dist/index.cjs +3632 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +29 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +143 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol-DhYBakWL.d.cts +15 -0
- package/dist/protocol-IeW8bf4f.d.ts +15 -0
- package/dist/shell.css +1603 -0
- package/dist/testing-vscode-stub.cjs +332 -0
- package/dist/testing-vscode-stub.cjs.map +1 -0
- package/dist/testing-vscode-stub.d.cts +65 -0
- package/dist/testing-vscode-stub.d.ts +65 -0
- package/dist/testing-vscode-stub.js +154 -0
- package/dist/testing-vscode-stub.js.map +1 -0
- package/dist/testing.cjs +4663 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +231 -0
- package/dist/testing.d.ts +231 -0
- package/dist/testing.js +4483 -0
- package/dist/testing.js.map +1 -0
- package/dist/toasts-Bp0REqdq.d.cts +142 -0
- package/dist/toasts-Bp0REqdq.d.ts +142 -0
- package/dist/types-CnRrINRs.d.cts +445 -0
- package/dist/types-PuDZczzy.d.ts +445 -0
- package/dist/vscode-extension.cjs +215 -0
- package/dist/vscode-extension.cjs.map +1 -0
- package/dist/vscode-extension.d.cts +103 -0
- package/dist/vscode-extension.d.ts +103 -0
- package/dist/vscode-extension.js +24 -0
- package/dist/vscode-extension.js.map +1 -0
- package/dist/vscode-webview.cjs +3561 -0
- package/dist/vscode-webview.cjs.map +1 -0
- package/dist/vscode-webview.d.cts +84 -0
- package/dist/vscode-webview.d.ts +84 -0
- package/dist/vscode-webview.js +259 -0
- package/dist/vscode-webview.js.map +1 -0
- package/package.json +145 -4
- package/src/core/appearance.ts +104 -0
- 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 +220 -0
- package/src/core/index.ts +127 -0
- package/src/core/keymap.ts +295 -0
- package/src/core/layout.ts +69 -0
- package/src/core/messages.ts +159 -0
- package/src/core/sizes.ts +85 -0
- package/src/core/types.ts +106 -0
- 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/core/zoom.ts +74 -0
- package/src/core.ts +2 -0
- package/src/diagram-js/connect.ts +75 -0
- package/src/diagram-js/index.ts +16 -0
- package/src/diagram-js/module.ts +20 -0
- package/src/diagram-js/palette-skin.ts +267 -0
- package/src/diagram-js/palette.css +269 -0
- package/src/diagram-js/service.ts +243 -0
- package/src/diagram-js/types.ts +67 -0
- package/src/diagram-js/viewport.ts +583 -0
- package/src/diagram-js.ts +2 -0
- package/src/dom/buttons.css +124 -0
- package/src/dom/element.ts +51 -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/hint.ts +51 -0
- package/src/dom/icons.ts +309 -0
- package/src/dom/index.ts +42 -0
- package/src/dom/inspector.ts +35 -0
- package/src/dom/keys.ts +67 -0
- package/src/dom/ledger.ts +87 -0
- package/src/dom/legend.ts +85 -0
- package/src/dom/menu.ts +404 -0
- package/src/dom/mount.ts +731 -0
- package/src/dom/problems.ts +73 -0
- package/src/dom/shell-dom.css +969 -0
- package/src/dom/status.ts +72 -0
- package/src/dom/tooltip.ts +237 -0
- package/src/dom/types.ts +459 -0
- 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/dom/zoom-panel.ts +154 -0
- package/src/index.ts +3 -0
- package/src/shell.css +9 -0
- package/src/testing/case-tools.ts +164 -0
- package/src/testing/cases.ts +1077 -0
- package/src/testing/checks.ts +868 -0
- package/src/testing/facts.ts +641 -0
- package/src/testing/geometry.ts +652 -0
- package/src/testing/harness.ts +187 -0
- package/src/testing/index.ts +19 -0
- package/src/testing/page-tools.ts +457 -0
- package/src/testing/page.ts +27 -0
- package/src/testing/vscode-stub.ts +237 -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/testing-vscode-stub.ts +2 -0
- package/src/testing.ts +2 -0
- package/src/vscode/extension/html.ts +107 -0
- package/src/vscode/extension/index.ts +8 -0
- package/src/vscode/extension/reveal.ts +9 -0
- package/src/vscode/extension/theme-bridge.ts +85 -0
- package/src/vscode/extension/types.ts +36 -0
- package/src/vscode/protocol.ts +31 -0
- package/src/vscode/webview/appearance.ts +141 -0
- package/src/vscode/webview/index.ts +8 -0
- package/src/vscode/webview/mount.ts +88 -0
- package/src/vscode/webview/state.ts +116 -0
- package/src/vscode-extension.ts +2 -0
- package/src/vscode-webview.ts +2 -0
package/src/dom/mount.ts
ADDED
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LEGAL_LINE_HEIGHT,
|
|
3
|
+
SHELL,
|
|
4
|
+
decideTopRow,
|
|
5
|
+
detectPlatform,
|
|
6
|
+
fitInsets,
|
|
7
|
+
legalLineHeight,
|
|
8
|
+
legendStartsOpen,
|
|
9
|
+
resolveMessages,
|
|
10
|
+
toastStackBottom,
|
|
11
|
+
zoomRowRaised,
|
|
12
|
+
type BottomObstacle,
|
|
13
|
+
type FitLayout,
|
|
14
|
+
type Insets,
|
|
15
|
+
type PaletteRow,
|
|
16
|
+
type Size,
|
|
17
|
+
type Theme,
|
|
18
|
+
type ToastRequest,
|
|
19
|
+
type ZoomTarget,
|
|
20
|
+
} from "../core/index.js";
|
|
21
|
+
import { boxRelativeTo, createElement } from "./element.js";
|
|
22
|
+
import { connectWebAppearance } from "./feedback/appearance.js";
|
|
23
|
+
import { createProblemsPopover, type ProblemsPopover } from "./feedback/problems-popover.js";
|
|
24
|
+
import { createToaster } from "./feedback/toasts.js";
|
|
25
|
+
import { createEmptyHint } from "./hint.js";
|
|
26
|
+
import { createInspector } from "./inspector.js";
|
|
27
|
+
import { createKeyListener, isModalOpen } from "./keys.js";
|
|
28
|
+
import { Ledger } from "./ledger.js";
|
|
29
|
+
import { createLegend } from "./legend.js";
|
|
30
|
+
import { createMenu, type MenuGroupName } from "./menu.js";
|
|
31
|
+
import { createProblemsPill } from "./problems.js";
|
|
32
|
+
import { createStatusBox } from "./status.js";
|
|
33
|
+
import { createTooltip } from "./tooltip.js";
|
|
34
|
+
import type {
|
|
35
|
+
AppearanceHandle,
|
|
36
|
+
InspectorHandle,
|
|
37
|
+
LegendHandle,
|
|
38
|
+
MenuCheckboxEntry,
|
|
39
|
+
MenuEntry,
|
|
40
|
+
MenuHandle,
|
|
41
|
+
PaletteReadOnlyState,
|
|
42
|
+
PaletteRegistration,
|
|
43
|
+
PrimaryActionHandle,
|
|
44
|
+
ShellHandle,
|
|
45
|
+
ShellOptions,
|
|
46
|
+
ToastHandle,
|
|
47
|
+
TooltipHandle,
|
|
48
|
+
} from "./types.js";
|
|
49
|
+
import { createDropZone } from "./web/drop-zone.js";
|
|
50
|
+
import { createEmptyState } from "./web/empty-state.js";
|
|
51
|
+
import { createLegalLine } from "./web/legal.js";
|
|
52
|
+
import { appearanceMenuEntries, fileMenuEntries } from "./web/menu-groups.js";
|
|
53
|
+
import { createPrimaryAction } from "./web/primary.js";
|
|
54
|
+
import { createZoomPanel } from "./zoom-panel.js";
|
|
55
|
+
|
|
56
|
+
// Page-global: every modeler bundles its own copy of the shell (and the CJS entries inline one each),
|
|
57
|
+
// and two shells on one foreign page must not share element ids.
|
|
58
|
+
const MOUNT_COUNTER = Symbol.for("@miragon/modeler-shell.mounts");
|
|
59
|
+
|
|
60
|
+
function nextMountIndex(): number {
|
|
61
|
+
const scope = globalThis as unknown as Record<symbol, number | undefined>;
|
|
62
|
+
const index = (scope[MOUNT_COUNTER] ?? 0) + 1;
|
|
63
|
+
scope[MOUNT_COUNTER] = index;
|
|
64
|
+
return index;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const DEFAULT_THEMES: readonly Theme[] = ["light"];
|
|
68
|
+
|
|
69
|
+
/** The legal line's height on the root, which the stack above it (pill, legend, zoom row) adds. */
|
|
70
|
+
const LEGAL_HEIGHT_PROPERTY = "--modeler-shell-legal-height";
|
|
71
|
+
|
|
72
|
+
/** What `toast()` returns where no toast shows (the IDE host, after `destroy`). */
|
|
73
|
+
const INERT_TOAST: ToastHandle = Object.freeze({ dismiss() {} });
|
|
74
|
+
|
|
75
|
+
/** The element's box relative to the root while it is rendered and visible, else `undefined`. */
|
|
76
|
+
function visibleBoxIn(element: HTMLElement | undefined, root: HTMLElement) {
|
|
77
|
+
if (!element || element.hidden || !element.isConnected) return undefined;
|
|
78
|
+
if (typeof element.checkVisibility === "function" && !element.checkVisibility({ visibilityProperty: true }))
|
|
79
|
+
return undefined;
|
|
80
|
+
const box = boxRelativeTo(element, root);
|
|
81
|
+
return box.width > 0 && box.height > 0 ? box : undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// In a foreign page the closest [lang] is the page's language, not the tool's (guide §2).
|
|
85
|
+
function languageOf(root: HTMLElement, host: ShellOptions["host"], requested: string | undefined): string {
|
|
86
|
+
if (requested) return requested;
|
|
87
|
+
const declared =
|
|
88
|
+
host === "embedded" ? root.getAttribute("lang") : root.closest("[lang]")?.getAttribute("lang");
|
|
89
|
+
return declared || "en";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The contrast of the host page around an embedded root: the nearest mode carrier above it. */
|
|
93
|
+
function hostKontrast(root: HTMLElement): "hoch" | null {
|
|
94
|
+
const carrier = root.parentElement?.closest("[data-cd-theme], [data-cd-kontrast]");
|
|
95
|
+
return carrier?.getAttribute("data-cd-kontrast") === "hoch" ? "hoch" : null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Turns `root` into the shell (`.cd-shell`, container `cd-shell`) and renders the chrome in the
|
|
100
|
+
* DOM order of guide §5.14: menu trigger, primary action (web), canvas slot, status box,
|
|
101
|
+
* empty-file hint, empty state (web), inspector, problems pill, legend, legal line (web), zoom
|
|
102
|
+
* panel, then the layer for dropdown, tooltip, problems popover and drop zone, and last the toast
|
|
103
|
+
* stack (web, embedded). Installs the one `keydown` listener (see `ShellOptions.keyScope`) and the
|
|
104
|
+
* root `ResizeObserver`.
|
|
105
|
+
*
|
|
106
|
+
* `root` must get its size from outside (`container-type: size`): `height: 100%` in an own
|
|
107
|
+
* document, an explicitly sized element in a foreign page. When rendering throws (a modeler's
|
|
108
|
+
* `legend.render`), everything added so far is removed again before the error propagates.
|
|
109
|
+
*/
|
|
110
|
+
export function mountShell(root: HTMLElement, options: ShellOptions): ShellHandle {
|
|
111
|
+
const themes = options.themes ?? DEFAULT_THEMES;
|
|
112
|
+
if (themes.length === 0) throw new Error("@miragon/modeler-shell: mountShell needs at least one theme");
|
|
113
|
+
const ledger = new Ledger();
|
|
114
|
+
try {
|
|
115
|
+
return renderShell(root, options, themes, ledger);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
ledger.close();
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function renderShell(
|
|
123
|
+
root: HTMLElement,
|
|
124
|
+
options: ShellOptions,
|
|
125
|
+
themes: readonly Theme[],
|
|
126
|
+
ledger: Ledger,
|
|
127
|
+
): ShellHandle {
|
|
128
|
+
const ownerDocument = root.ownerDocument;
|
|
129
|
+
const { host } = options;
|
|
130
|
+
const index = nextMountIndex();
|
|
131
|
+
const messages = resolveMessages(options.messages);
|
|
132
|
+
const platform = options.platform ?? detectPlatform(globalThis.navigator);
|
|
133
|
+
const lang = languageOf(root, host, options.lang);
|
|
134
|
+
const keyScope = options.keyScope ?? (host === "embedded" ? "root" : "document");
|
|
135
|
+
const web = host === "web";
|
|
136
|
+
|
|
137
|
+
ledger.addClass(root, "cd-shell");
|
|
138
|
+
ledger.attribute(root, "data-cd-shell-host", host);
|
|
139
|
+
ledger.attribute(root, "data-palette", "top");
|
|
140
|
+
if (host === "embedded") {
|
|
141
|
+
ledger.attribute(root, "lang", lang);
|
|
142
|
+
carryOwnMode(root, themes[0] ?? "light", ledger);
|
|
143
|
+
} else {
|
|
144
|
+
ledger.addClass(ownerDocument.documentElement, "cd-shell-document");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const setState = (name: string, value: string | null): void => {
|
|
148
|
+
if (!ledger.isClosed) ledger.attribute(root, name, value);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// The head script set the mode before the first paint; this follows contrast and the menu choice.
|
|
152
|
+
const appearance = web
|
|
153
|
+
? connectWebAppearance({ ownerDocument, themes, options: options.appearance })
|
|
154
|
+
: undefined;
|
|
155
|
+
if (appearance) ledger.defer(() => appearance.destroy());
|
|
156
|
+
const darkCapable = themes.includes("dark");
|
|
157
|
+
|
|
158
|
+
const layer = createElement(ownerDocument, "div", { class: "cd-shell-layer" });
|
|
159
|
+
ledger.defer(() => layer.remove());
|
|
160
|
+
const tooltip = createTooltip({ root, layer, id: `cd-shell-tooltip-${index}` });
|
|
161
|
+
ledger.defer(() => tooltip.destroy());
|
|
162
|
+
|
|
163
|
+
/** The lock of `setReadOnly(true)`; `readOnlyShown` its marks, which may follow after a delay. */
|
|
164
|
+
let readOnly = false;
|
|
165
|
+
let readOnlyShown = false;
|
|
166
|
+
let readOnlyTimer: ReturnType<typeof setTimeout> | undefined;
|
|
167
|
+
ledger.defer(() => clearTimeout(readOnlyTimer));
|
|
168
|
+
|
|
169
|
+
const legend = options.legend
|
|
170
|
+
? createLegend({
|
|
171
|
+
ownerDocument,
|
|
172
|
+
messages,
|
|
173
|
+
tooltip,
|
|
174
|
+
titleId: `cd-shell-legend-title-${index}`,
|
|
175
|
+
options: options.legend,
|
|
176
|
+
onChange(open) {
|
|
177
|
+
setState("data-legend", open ? "open" : null);
|
|
178
|
+
placeHint();
|
|
179
|
+
menu.refresh();
|
|
180
|
+
},
|
|
181
|
+
focusAfterClose: () => menu.trigger.focus(),
|
|
182
|
+
})
|
|
183
|
+
: undefined;
|
|
184
|
+
if (legend) ledger.defer(() => legend.destroy());
|
|
185
|
+
|
|
186
|
+
const legendEntry: MenuCheckboxEntry | undefined =
|
|
187
|
+
legend && options.legend?.menuEntry !== false
|
|
188
|
+
? {
|
|
189
|
+
kind: "checkbox",
|
|
190
|
+
id: "legend",
|
|
191
|
+
label: messages.legend,
|
|
192
|
+
checked: () => legend.isOpen(),
|
|
193
|
+
onToggle: (checked) => legend.setOpen(checked),
|
|
194
|
+
keepEnabledWhenReadOnly: true,
|
|
195
|
+
}
|
|
196
|
+
: undefined;
|
|
197
|
+
const shellModelEntries: readonly MenuCheckboxEntry[] = legendEntry ? [legendEntry] : [];
|
|
198
|
+
const fileEntries: readonly MenuEntry[] =
|
|
199
|
+
web && options.file ? fileMenuEntries(options.file, messages) : [];
|
|
200
|
+
const appearanceEntries: readonly MenuEntry[] =
|
|
201
|
+
appearance && darkCapable ? appearanceMenuEntries(appearance, messages) : [];
|
|
202
|
+
const shellEntries = (group: MenuGroupName): readonly MenuEntry[] => {
|
|
203
|
+
if (group === "file") return fileEntries;
|
|
204
|
+
if (group === "model") return shellModelEntries;
|
|
205
|
+
if (group === "appearance") return appearanceEntries;
|
|
206
|
+
return [];
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const menu = createMenu({
|
|
210
|
+
root,
|
|
211
|
+
layer,
|
|
212
|
+
id: `cd-shell-menu-${index}`,
|
|
213
|
+
messages,
|
|
214
|
+
tooltip,
|
|
215
|
+
groups: options.menu ?? {},
|
|
216
|
+
shellEntries,
|
|
217
|
+
isReadOnly: () => readOnly,
|
|
218
|
+
});
|
|
219
|
+
ledger.defer(() => menu.destroy());
|
|
220
|
+
ledger.defer(tooltip.attach(menu.trigger, () => (menu.isOpen() ? null : { label: messages.menu })));
|
|
221
|
+
|
|
222
|
+
const primary =
|
|
223
|
+
web && options.primary
|
|
224
|
+
? createPrimaryAction({ ownerDocument, messages, tooltip, options: options.primary })
|
|
225
|
+
: undefined;
|
|
226
|
+
if (primary) ledger.defer(() => primary.destroy());
|
|
227
|
+
|
|
228
|
+
const canvas = options.canvas ?? createElement(ownerDocument, "div");
|
|
229
|
+
ledger.addClass(canvas, "cd-shell-canvas");
|
|
230
|
+
// An own page needs a main landmark; the diagram is its content. A foreign page has its own.
|
|
231
|
+
if (web && !canvas.hasAttribute("role")) ledger.attribute(canvas, "role", "main");
|
|
232
|
+
if (!options.canvas) ledger.defer(() => canvas.remove());
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* The focus target when the element holding it goes away: on the start screen its "New
|
|
236
|
+
* diagram", else the canvas, else the menu trigger. A target that cannot take the focus (inert,
|
|
237
|
+
* hidden behind the start screen) is passed over, so the focus never falls to `<body>` unseen.
|
|
238
|
+
*/
|
|
239
|
+
function focusCanvasOrTrigger(): void {
|
|
240
|
+
const took = (target: HTMLElement | SVGElement | null | undefined): boolean => {
|
|
241
|
+
target?.focus({ preventScroll: true });
|
|
242
|
+
return target !== null && target !== undefined && ownerDocument.activeElement === target;
|
|
243
|
+
};
|
|
244
|
+
if (emptyState?.isShown()) {
|
|
245
|
+
emptyState.focus();
|
|
246
|
+
if (emptyState.element.contains(ownerDocument.activeElement)) return;
|
|
247
|
+
}
|
|
248
|
+
if (took(canvas.querySelector<HTMLElement | SVGElement>("svg[tabindex]"))) return;
|
|
249
|
+
if (!menu.trigger.hidden) took(menu.trigger);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Where the focus goes when the island holding it hides. */
|
|
253
|
+
function moveFocusOutOf(island: HTMLElement): void {
|
|
254
|
+
if (island.contains(ownerDocument.activeElement)) focusCanvasOrTrigger();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const status = createStatusBox(ownerDocument);
|
|
258
|
+
ledger.defer(() => status.destroy());
|
|
259
|
+
const hint = options.emptyHint ? createEmptyHint(ownerDocument, messages, options.emptyHint) : undefined;
|
|
260
|
+
if (hint) ledger.defer(() => hint.destroy());
|
|
261
|
+
const emptyState =
|
|
262
|
+
web && options.emptyState
|
|
263
|
+
? createEmptyState({
|
|
264
|
+
ownerDocument,
|
|
265
|
+
messages,
|
|
266
|
+
options: options.emptyState,
|
|
267
|
+
dropHint: options.dropZone ? (options.emptyState.dropHint ?? messages.dropHint) : undefined,
|
|
268
|
+
open: options.file?.open,
|
|
269
|
+
titleId: `cd-shell-empty-state-title-${index}`,
|
|
270
|
+
onLeave: () => setEmptyStateShown(false),
|
|
271
|
+
})
|
|
272
|
+
: undefined;
|
|
273
|
+
if (emptyState) ledger.defer(() => emptyState.destroy());
|
|
274
|
+
const inspector = options.inspector
|
|
275
|
+
? createInspector(ownerDocument, options.inspector, (open) => {
|
|
276
|
+
setState("data-inspector", open ? "open" : null);
|
|
277
|
+
// Up to 760px the sheet hides zoom panel and pill, so the toasts' bottom row changes.
|
|
278
|
+
updateBottomRow();
|
|
279
|
+
placeHint();
|
|
280
|
+
})
|
|
281
|
+
: undefined;
|
|
282
|
+
if (inspector) ledger.defer(() => inspector.destroy());
|
|
283
|
+
const problemsOptions = options.problems;
|
|
284
|
+
let popover: ProblemsPopover | undefined;
|
|
285
|
+
const pill = problemsOptions
|
|
286
|
+
? createProblemsPill({
|
|
287
|
+
ownerDocument,
|
|
288
|
+
messages,
|
|
289
|
+
lang,
|
|
290
|
+
tooltip,
|
|
291
|
+
showTooltip: host === "ide",
|
|
292
|
+
onActivate() {
|
|
293
|
+
if (popover) popover.toggle();
|
|
294
|
+
else problemsOptions.onActivate?.();
|
|
295
|
+
},
|
|
296
|
+
})
|
|
297
|
+
: undefined;
|
|
298
|
+
if (pill) ledger.defer(() => pill.destroy());
|
|
299
|
+
// The web app lists its problems itself; an embedding host only when it has no list of its own.
|
|
300
|
+
if (pill && (web || (host === "embedded" && !problemsOptions?.onActivate))) {
|
|
301
|
+
const created = createProblemsPopover({
|
|
302
|
+
ownerDocument,
|
|
303
|
+
root,
|
|
304
|
+
layer,
|
|
305
|
+
pill: pill.element,
|
|
306
|
+
id: `cd-shell-problems-${index}`,
|
|
307
|
+
titleId: `cd-shell-problems-title-${index}`,
|
|
308
|
+
messages,
|
|
309
|
+
onSelectItem: problemsOptions?.onSelectItem,
|
|
310
|
+
});
|
|
311
|
+
popover = created;
|
|
312
|
+
ledger.defer(() => created.destroy());
|
|
313
|
+
}
|
|
314
|
+
const legal = web
|
|
315
|
+
? createLegalLine({
|
|
316
|
+
ownerDocument,
|
|
317
|
+
messages,
|
|
318
|
+
options: options.legal ?? {},
|
|
319
|
+
idPrefix: `cd-shell-legal-${index}`,
|
|
320
|
+
})
|
|
321
|
+
: undefined;
|
|
322
|
+
if (legal) ledger.defer(() => legal.destroy());
|
|
323
|
+
const zoomPanel = createZoomPanel({ ownerDocument, messages, lang, platform, tooltip });
|
|
324
|
+
ledger.defer(() => zoomPanel.destroy());
|
|
325
|
+
const toaster =
|
|
326
|
+
host === "ide"
|
|
327
|
+
? undefined
|
|
328
|
+
: createToaster({
|
|
329
|
+
ownerDocument,
|
|
330
|
+
host,
|
|
331
|
+
messages,
|
|
332
|
+
idPrefix: `cd-shell-toast-${index}`,
|
|
333
|
+
focusFallback: focusCanvasOrTrigger,
|
|
334
|
+
onChange: () => updateBottomRow(),
|
|
335
|
+
});
|
|
336
|
+
if (toaster) ledger.defer(() => toaster.destroy());
|
|
337
|
+
|
|
338
|
+
const defined = (element: HTMLElement | undefined): element is HTMLElement => element !== undefined;
|
|
339
|
+
root.prepend(...[menu.trigger, primary?.element, canvas].filter(defined));
|
|
340
|
+
root.append(
|
|
341
|
+
...[
|
|
342
|
+
status.element,
|
|
343
|
+
hint?.element,
|
|
344
|
+
emptyState?.element,
|
|
345
|
+
inspector?.element,
|
|
346
|
+
pill?.element,
|
|
347
|
+
legend?.element,
|
|
348
|
+
legal?.element,
|
|
349
|
+
zoomPanel.element,
|
|
350
|
+
].filter(defined),
|
|
351
|
+
layer,
|
|
352
|
+
...[toaster?.announcer, toaster?.element].filter(defined),
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
function showToast(request: ToastRequest): ToastHandle {
|
|
356
|
+
return ledger.isClosed || !toaster ? INERT_TOAST : toaster.show(request);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const dropZone =
|
|
360
|
+
web && options.dropZone
|
|
361
|
+
? createDropZone({
|
|
362
|
+
ownerDocument,
|
|
363
|
+
layer,
|
|
364
|
+
messages,
|
|
365
|
+
lang,
|
|
366
|
+
options: options.dropZone,
|
|
367
|
+
modalOpen: () => isModalOpen(ownerDocument),
|
|
368
|
+
toast: showToast,
|
|
369
|
+
setState: (state) => setState("data-drop", state),
|
|
370
|
+
})
|
|
371
|
+
: undefined;
|
|
372
|
+
if (dropZone) ledger.defer(() => dropZone.destroy());
|
|
373
|
+
|
|
374
|
+
let palette: { registration: PaletteRegistration; detachTooltip: () => void } | undefined;
|
|
375
|
+
let paletteRow: PaletteRow = "top";
|
|
376
|
+
/** Web: the zoom panel stands above the legal line (`data-zoom-row="raised"`). */
|
|
377
|
+
let zoomRaised = false;
|
|
378
|
+
/** Web: the legal line's measured height, more than one line where a narrow container wraps it. */
|
|
379
|
+
let legalHeight = LEGAL_LINE_HEIGHT;
|
|
380
|
+
if (legal) ledger.defer(() => root.style.removeProperty(LEGAL_HEIGHT_PROPERTY));
|
|
381
|
+
|
|
382
|
+
function containerSize(): Size {
|
|
383
|
+
return { width: root.clientWidth, height: root.clientHeight };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function menuRight(): number {
|
|
387
|
+
const box = boxRelativeTo(menu.trigger, root);
|
|
388
|
+
return box.width > 0 ? box.x + box.width : SHELL.edge + SHELL.row;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** The palette's intrinsic border-box width: it does not shrink once the second row clips it. */
|
|
392
|
+
function paletteWidth(): number {
|
|
393
|
+
const element = palette?.registration.element;
|
|
394
|
+
return element ? element.offsetWidth - element.clientWidth + element.scrollWidth : 0;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function currentFitInsets(): Insets {
|
|
398
|
+
const container = containerSize();
|
|
399
|
+
const layout: FitLayout = {
|
|
400
|
+
container,
|
|
401
|
+
host,
|
|
402
|
+
paletteRow,
|
|
403
|
+
problemsVisible: pill?.isShown() ?? false,
|
|
404
|
+
};
|
|
405
|
+
if (web) {
|
|
406
|
+
layout.zoomRaised = zoomRaised;
|
|
407
|
+
layout.legalHeight = legalHeight;
|
|
408
|
+
}
|
|
409
|
+
if (legend?.isOpen()) layout.legendWidth = legend.element.getBoundingClientRect().width;
|
|
410
|
+
if (inspector?.isOpen()) {
|
|
411
|
+
layout.inspector =
|
|
412
|
+
container.width > SHELL.narrow ? {} : { sheetTop: boxRelativeTo(inspector.element, root).y };
|
|
413
|
+
}
|
|
414
|
+
return fitInsets(layout);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function placeHint(): void {
|
|
418
|
+
if (hint?.isShown()) hint.place(currentFitInsets());
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function updateLayout(): void {
|
|
422
|
+
if (ledger.isClosed) return;
|
|
423
|
+
const size = containerSize();
|
|
424
|
+
// A hidden tab reports 0 × 0; it neither measures nor decides anything (guide §5.9).
|
|
425
|
+
if (size.width === 0 || size.height === 0) return;
|
|
426
|
+
const topRow = decideTopRow({
|
|
427
|
+
containerWidth: size.width,
|
|
428
|
+
paletteWidth: paletteWidth(),
|
|
429
|
+
menuRight: menuRight(),
|
|
430
|
+
primary: primary?.widths(),
|
|
431
|
+
});
|
|
432
|
+
paletteRow = topRow.paletteRow;
|
|
433
|
+
setState("data-palette", paletteRow);
|
|
434
|
+
if (primary) {
|
|
435
|
+
const collapsed = topRow.primaryLabel === "collapsed";
|
|
436
|
+
primary.setCollapsed(collapsed);
|
|
437
|
+
setState("data-primary", collapsed ? "collapsed" : null);
|
|
438
|
+
}
|
|
439
|
+
if (legend && !legend.isStarted()) legend.start(options.legend?.open ?? legendStartsOpen(size.height));
|
|
440
|
+
updateBottomRow();
|
|
441
|
+
placeHint();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** Web and embedded: the raised zoom row (legal line) and the toast stack's bottom, from what is visible. */
|
|
445
|
+
function updateBottomRow(): void {
|
|
446
|
+
if (ledger.isClosed) return;
|
|
447
|
+
const size = containerSize();
|
|
448
|
+
if (size.width === 0 || size.height === 0) return;
|
|
449
|
+
if (legal) {
|
|
450
|
+
const legalBox = visibleBoxIn(legal.element, root);
|
|
451
|
+
const height = legalLineHeight(legalBox?.height);
|
|
452
|
+
if (height !== legalHeight || !root.style.getPropertyValue(LEGAL_HEIGHT_PROPERTY)) {
|
|
453
|
+
legalHeight = height;
|
|
454
|
+
root.style.setProperty(LEGAL_HEIGHT_PROPERTY, `${height}px`);
|
|
455
|
+
}
|
|
456
|
+
zoomRaised = zoomRowRaised({
|
|
457
|
+
containerWidth: size.width,
|
|
458
|
+
legalRight: legalBox && legalBox.x + legalBox.width,
|
|
459
|
+
zoomWidth: visibleBoxIn(zoomPanel.element, root)?.width,
|
|
460
|
+
});
|
|
461
|
+
setState("data-zoom-row", zoomRaised ? "raised" : null);
|
|
462
|
+
}
|
|
463
|
+
if (toaster) {
|
|
464
|
+
const obstacles = [zoomPanel.element, legal?.element, pill?.element].flatMap(
|
|
465
|
+
(element): BottomObstacle[] => {
|
|
466
|
+
const box = visibleBoxIn(element, root);
|
|
467
|
+
return box ? [{ left: box.x, right: box.x + box.width, top: size.height - box.y }] : [];
|
|
468
|
+
},
|
|
469
|
+
);
|
|
470
|
+
toaster.setBottom(
|
|
471
|
+
toastStackBottom({ containerWidth: size.width, obstacles, cardWidth: toaster.widestCard() }),
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function setEmptyStateShown(shown: boolean): void {
|
|
477
|
+
if (ledger.isClosed || !emptyState || emptyState.isShown() === shown) return;
|
|
478
|
+
const active = ownerDocument.activeElement;
|
|
479
|
+
const hadFocus = emptyState.element.contains(active);
|
|
480
|
+
// The work chrome and the canvas are about to hide: a focus there would fall to <body>. The
|
|
481
|
+
// legal line and the toasts stay.
|
|
482
|
+
const workHadFocus =
|
|
483
|
+
shown &&
|
|
484
|
+
active !== null &&
|
|
485
|
+
root.contains(active) &&
|
|
486
|
+
![emptyState.element, legal?.element, toaster?.element].some((kept) => kept?.contains(active));
|
|
487
|
+
emptyState.setShown(shown);
|
|
488
|
+
setState("data-empty-state", shown ? "shown" : null);
|
|
489
|
+
// Nothing behind the card takes a click or the Tab (guide §8: nothing else competes).
|
|
490
|
+
ledger.attribute(canvas, "inert", shown ? "" : null);
|
|
491
|
+
if (shown) {
|
|
492
|
+
menu.close();
|
|
493
|
+
popover?.close();
|
|
494
|
+
tooltip.hide();
|
|
495
|
+
if (workHadFocus) emptyState.focus();
|
|
496
|
+
} else if (hadFocus) {
|
|
497
|
+
focusCanvasOrTrigger();
|
|
498
|
+
}
|
|
499
|
+
updateLayout();
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const ResizeObserverClass = globalThis.ResizeObserver;
|
|
503
|
+
const resizeObserver = ResizeObserverClass ? new ResizeObserverClass(() => updateLayout()) : undefined;
|
|
504
|
+
resizeObserver?.observe(root);
|
|
505
|
+
// The label widens once the web font has loaded; the top row decides again then. The legal line
|
|
506
|
+
// may wrap onto a second line then, which lifts everything stacked above it.
|
|
507
|
+
if (primary) resizeObserver?.observe(primary.element);
|
|
508
|
+
if (legal) resizeObserver?.observe(legal.element);
|
|
509
|
+
ledger.defer(() => resizeObserver?.disconnect());
|
|
510
|
+
|
|
511
|
+
if (legend && options.legend?.open !== undefined) legend.start(options.legend.open);
|
|
512
|
+
updateLayout();
|
|
513
|
+
|
|
514
|
+
const keyTarget: HTMLElement | Document = keyScope === "root" ? root : ownerDocument;
|
|
515
|
+
const onKeydown = createKeyListener({
|
|
516
|
+
host,
|
|
517
|
+
dialogScope: keyTarget,
|
|
518
|
+
menuOpen: () => menu.isOpen(),
|
|
519
|
+
startScreen: () => emptyState?.isShown() ?? false,
|
|
520
|
+
closeMenu() {
|
|
521
|
+
const active = ownerDocument.activeElement;
|
|
522
|
+
menu.close({ restoreFocus: active === menu.trigger || menu.dropdown.contains(active) });
|
|
523
|
+
},
|
|
524
|
+
hideTooltip: () => tooltip.hide(),
|
|
525
|
+
// Behind the start screen the keys reach the browser instead: there is no diagram to zoom.
|
|
526
|
+
run: (key) => {
|
|
527
|
+
if (!emptyState?.isShown()) zoomPanel.run(key);
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
keyTarget.addEventListener("keydown", onKeydown as EventListener);
|
|
531
|
+
ledger.defer(() => keyTarget.removeEventListener("keydown", onKeydown as EventListener));
|
|
532
|
+
|
|
533
|
+
function readOnlyState(): PaletteReadOnlyState {
|
|
534
|
+
return { locked: readOnly, shown: readOnlyShown };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function applyReadOnly(): void {
|
|
538
|
+
setState("data-readonly", readOnlyShown ? "" : null);
|
|
539
|
+
palette?.registration.setReadOnly(readOnlyState());
|
|
540
|
+
menu.refresh();
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function showReadOnly(): void {
|
|
544
|
+
readOnlyTimer = undefined;
|
|
545
|
+
if (ledger.isClosed || !readOnly) return;
|
|
546
|
+
readOnlyShown = true;
|
|
547
|
+
applyReadOnly();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function unregisterPalette(): void {
|
|
551
|
+
if (!palette) return;
|
|
552
|
+
palette.detachTooltip();
|
|
553
|
+
resizeObserver?.unobserve(palette.registration.element);
|
|
554
|
+
palette = undefined;
|
|
555
|
+
}
|
|
556
|
+
ledger.defer(unregisterPalette);
|
|
557
|
+
|
|
558
|
+
const menuHandle: MenuHandle = {
|
|
559
|
+
trigger: menu.trigger,
|
|
560
|
+
dropdown: menu.dropdown,
|
|
561
|
+
isOpen: () => menu.isOpen(),
|
|
562
|
+
open: (focus) => menu.open(focus),
|
|
563
|
+
close: (closeOptions) => menu.close(closeOptions),
|
|
564
|
+
refresh: () => menu.refresh(),
|
|
565
|
+
setGroups: (groups) => {
|
|
566
|
+
if (!ledger.isClosed) menu.setGroups(groups);
|
|
567
|
+
},
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
const tooltipHandle: TooltipHandle = {
|
|
571
|
+
attach: (control, content) => (ledger.isClosed ? () => {} : tooltip.attach(control, content)),
|
|
572
|
+
hide: () => tooltip.hide(),
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
const legendHandle: LegendHandle | undefined = legend && {
|
|
576
|
+
element: legend.element,
|
|
577
|
+
body: legend.body,
|
|
578
|
+
isOpen: () => legend.isOpen(),
|
|
579
|
+
setOpen: (open) => {
|
|
580
|
+
if (!ledger.isClosed) legend.setOpen(open);
|
|
581
|
+
},
|
|
582
|
+
toggle: () => {
|
|
583
|
+
if (!ledger.isClosed) legend.toggle();
|
|
584
|
+
},
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
const primaryHandle: PrimaryActionHandle | undefined = primary && {
|
|
588
|
+
element: primary.element,
|
|
589
|
+
isCollapsed: () => primary.isCollapsed(),
|
|
590
|
+
refresh: () => {
|
|
591
|
+
if (!ledger.isClosed) primary.refresh();
|
|
592
|
+
},
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
const appearanceHandle: AppearanceHandle | undefined =
|
|
596
|
+
appearance && darkCapable
|
|
597
|
+
? {
|
|
598
|
+
choice: () => appearance.choice(),
|
|
599
|
+
setChoice: (choice) => {
|
|
600
|
+
if (ledger.isClosed) return;
|
|
601
|
+
appearance.setChoice(choice);
|
|
602
|
+
menu.refresh();
|
|
603
|
+
},
|
|
604
|
+
}
|
|
605
|
+
: undefined;
|
|
606
|
+
|
|
607
|
+
const inspectorHandle: InspectorHandle | undefined = inspector && {
|
|
608
|
+
element: inspector.element,
|
|
609
|
+
isOpen: () => inspector.isOpen(),
|
|
610
|
+
open: () => {
|
|
611
|
+
if (!ledger.isClosed) inspector.open();
|
|
612
|
+
},
|
|
613
|
+
close: () => {
|
|
614
|
+
if (ledger.isClosed) return;
|
|
615
|
+
if (inspector.isOpen()) moveFocusOutOf(inspector.element);
|
|
616
|
+
inspector.close();
|
|
617
|
+
},
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
return {
|
|
621
|
+
root,
|
|
622
|
+
canvas,
|
|
623
|
+
host,
|
|
624
|
+
lang,
|
|
625
|
+
platform,
|
|
626
|
+
messages,
|
|
627
|
+
menu: menuHandle,
|
|
628
|
+
tooltip: tooltipHandle,
|
|
629
|
+
legend: legendHandle,
|
|
630
|
+
inspector: inspectorHandle,
|
|
631
|
+
primary: primaryHandle,
|
|
632
|
+
appearance: appearanceHandle,
|
|
633
|
+
setZoomTarget(target: ZoomTarget | null) {
|
|
634
|
+
if (!ledger.isClosed) zoomPanel.setTarget(target);
|
|
635
|
+
},
|
|
636
|
+
fitInsets: currentFitInsets,
|
|
637
|
+
registerPalette(registration) {
|
|
638
|
+
if (ledger.isClosed) return () => {};
|
|
639
|
+
unregisterPalette();
|
|
640
|
+
palette = {
|
|
641
|
+
registration,
|
|
642
|
+
detachTooltip: tooltip.delegate(registration.element, ".entry", (entry) =>
|
|
643
|
+
registration.entryTooltip(entry),
|
|
644
|
+
),
|
|
645
|
+
};
|
|
646
|
+
resizeObserver?.observe(registration.element);
|
|
647
|
+
registration.setReadOnly(readOnlyState());
|
|
648
|
+
updateLayout();
|
|
649
|
+
const registered = palette;
|
|
650
|
+
return () => {
|
|
651
|
+
if (palette !== registered) return;
|
|
652
|
+
unregisterPalette();
|
|
653
|
+
updateLayout();
|
|
654
|
+
};
|
|
655
|
+
},
|
|
656
|
+
setProblems(summary) {
|
|
657
|
+
if (ledger.isClosed || !pill) return;
|
|
658
|
+
const listHadFocus = popover?.element.contains(ownerDocument.activeElement) ?? false;
|
|
659
|
+
pill.set(summary);
|
|
660
|
+
setState("data-problems", pill.isShown() ? "shown" : null);
|
|
661
|
+
popover?.setItems(summary.items ?? []);
|
|
662
|
+
if (!pill.isShown() && popover?.isOpen()) {
|
|
663
|
+
popover.close();
|
|
664
|
+
// The pill went with the list, so the focus cannot return to it.
|
|
665
|
+
if (listHadFocus) focusCanvasOrTrigger();
|
|
666
|
+
}
|
|
667
|
+
updateBottomRow();
|
|
668
|
+
placeHint();
|
|
669
|
+
},
|
|
670
|
+
setStatus(next, statusOptions) {
|
|
671
|
+
if (!ledger.isClosed) status.set(next, statusOptions?.delayMs ?? 0);
|
|
672
|
+
},
|
|
673
|
+
setEmptyHint(visible) {
|
|
674
|
+
if (ledger.isClosed || !hint) return;
|
|
675
|
+
if (!visible && hint.isShown()) moveFocusOutOf(hint.element);
|
|
676
|
+
hint.setShown(visible);
|
|
677
|
+
placeHint();
|
|
678
|
+
},
|
|
679
|
+
setReadOnly(next, readOnlyOptions) {
|
|
680
|
+
if (ledger.isClosed) return;
|
|
681
|
+
if (!next) {
|
|
682
|
+
clearTimeout(readOnlyTimer);
|
|
683
|
+
readOnlyTimer = undefined;
|
|
684
|
+
readOnly = false;
|
|
685
|
+
readOnlyShown = false;
|
|
686
|
+
} else {
|
|
687
|
+
readOnly = true;
|
|
688
|
+
const delayMs = readOnlyOptions?.delayMs ?? 0;
|
|
689
|
+
// Repeated invalid imports while typing keep the first timer, like the status box.
|
|
690
|
+
if (!readOnlyShown && readOnlyTimer === undefined) {
|
|
691
|
+
if (delayMs > 0) readOnlyTimer = setTimeout(showReadOnly, delayMs);
|
|
692
|
+
else readOnlyShown = true;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
applyReadOnly();
|
|
696
|
+
},
|
|
697
|
+
isReadOnly: () => readOnly,
|
|
698
|
+
setEmptyState: setEmptyStateShown,
|
|
699
|
+
isEmptyStateShown: () => emptyState?.isShown() ?? false,
|
|
700
|
+
toast: showToast,
|
|
701
|
+
destroy() {
|
|
702
|
+
ledger.close();
|
|
703
|
+
},
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* An embedded root carries its own mode (guide §13.3): the token CSS resolves the roles on the
|
|
709
|
+
* nearest `[data-cd-theme]`, so without it a light-only modeler would inherit a dark host page and
|
|
710
|
+
* be half-dark against its light-drawn diagram. The root's own `data-cd-theme` would also drop the
|
|
711
|
+
* host's high contrast, so `data-cd-kontrast` is mirrored from the host for as long as it lives.
|
|
712
|
+
*/
|
|
713
|
+
function carryOwnMode(root: HTMLElement, theme: Theme, ledger: Ledger): void {
|
|
714
|
+
ledger.attribute(root, "data-cd-theme", theme);
|
|
715
|
+
const mirror = (): void => {
|
|
716
|
+
if (ledger.isClosed) return;
|
|
717
|
+
const kontrast = hostKontrast(root);
|
|
718
|
+
if (root.getAttribute("data-cd-kontrast") !== kontrast)
|
|
719
|
+
ledger.attribute(root, "data-cd-kontrast", kontrast);
|
|
720
|
+
};
|
|
721
|
+
mirror();
|
|
722
|
+
const MutationObserverClass = globalThis.MutationObserver;
|
|
723
|
+
if (!MutationObserverClass) return;
|
|
724
|
+
const observer = new MutationObserverClass(mirror);
|
|
725
|
+
observer.observe(root.ownerDocument.documentElement, {
|
|
726
|
+
attributes: true,
|
|
727
|
+
attributeFilter: ["data-cd-theme", "data-cd-kontrast"],
|
|
728
|
+
subtree: true,
|
|
729
|
+
});
|
|
730
|
+
ledger.defer(() => observer.disconnect());
|
|
731
|
+
}
|