@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/src/dom/mount.ts
CHANGED
|
@@ -1,38 +1,56 @@
|
|
|
1
1
|
import {
|
|
2
|
+
LEGAL_LINE_HEIGHT,
|
|
2
3
|
SHELL,
|
|
3
|
-
|
|
4
|
+
decideTopRow,
|
|
4
5
|
detectPlatform,
|
|
5
6
|
fitInsets,
|
|
7
|
+
legalLineHeight,
|
|
6
8
|
legendStartsOpen,
|
|
7
9
|
resolveMessages,
|
|
10
|
+
toastStackBottom,
|
|
11
|
+
zoomRowRaised,
|
|
12
|
+
type BottomObstacle,
|
|
8
13
|
type FitLayout,
|
|
9
14
|
type Insets,
|
|
10
15
|
type PaletteRow,
|
|
11
16
|
type Size,
|
|
12
17
|
type Theme,
|
|
18
|
+
type ToastRequest,
|
|
13
19
|
type ZoomTarget,
|
|
14
20
|
} from "../core/index.js";
|
|
15
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";
|
|
16
25
|
import { createEmptyHint } from "./hint.js";
|
|
17
26
|
import { createInspector } from "./inspector.js";
|
|
18
|
-
import { createKeyListener } from "./keys.js";
|
|
27
|
+
import { createKeyListener, isModalOpen } from "./keys.js";
|
|
19
28
|
import { Ledger } from "./ledger.js";
|
|
20
29
|
import { createLegend } from "./legend.js";
|
|
21
|
-
import { createMenu } from "./menu.js";
|
|
30
|
+
import { createMenu, type MenuGroupName } from "./menu.js";
|
|
22
31
|
import { createProblemsPill } from "./problems.js";
|
|
23
32
|
import { createStatusBox } from "./status.js";
|
|
24
33
|
import { createTooltip } from "./tooltip.js";
|
|
25
34
|
import type {
|
|
35
|
+
AppearanceHandle,
|
|
26
36
|
InspectorHandle,
|
|
27
37
|
LegendHandle,
|
|
28
38
|
MenuCheckboxEntry,
|
|
39
|
+
MenuEntry,
|
|
29
40
|
MenuHandle,
|
|
30
41
|
PaletteReadOnlyState,
|
|
31
42
|
PaletteRegistration,
|
|
43
|
+
PrimaryActionHandle,
|
|
32
44
|
ShellHandle,
|
|
33
45
|
ShellOptions,
|
|
46
|
+
ToastHandle,
|
|
34
47
|
TooltipHandle,
|
|
35
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";
|
|
36
54
|
import { createZoomPanel } from "./zoom-panel.js";
|
|
37
55
|
|
|
38
56
|
// Page-global: every modeler bundles its own copy of the shell (and the CJS entries inline one each),
|
|
@@ -48,6 +66,21 @@ function nextMountIndex(): number {
|
|
|
48
66
|
|
|
49
67
|
const DEFAULT_THEMES: readonly Theme[] = ["light"];
|
|
50
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
|
+
|
|
51
84
|
// In a foreign page the closest [lang] is the page's language, not the tool's (guide §2).
|
|
52
85
|
function languageOf(root: HTMLElement, host: ShellOptions["host"], requested: string | undefined): string {
|
|
53
86
|
if (requested) return requested;
|
|
@@ -64,9 +97,11 @@ function hostKontrast(root: HTMLElement): "hoch" | null {
|
|
|
64
97
|
|
|
65
98
|
/**
|
|
66
99
|
* Turns `root` into the shell (`.cd-shell`, container `cd-shell`) and renders the chrome in the
|
|
67
|
-
* DOM order of guide §5.14: menu trigger, canvas slot, status box,
|
|
68
|
-
*
|
|
69
|
-
*
|
|
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`.
|
|
70
105
|
*
|
|
71
106
|
* `root` must get its size from outside (`container-type: size`): `height: 100%` in an own
|
|
72
107
|
* document, an explicitly sized element in a foreign page. When rendering throws (a modeler's
|
|
@@ -97,6 +132,7 @@ function renderShell(
|
|
|
97
132
|
const platform = options.platform ?? detectPlatform(globalThis.navigator);
|
|
98
133
|
const lang = languageOf(root, host, options.lang);
|
|
99
134
|
const keyScope = options.keyScope ?? (host === "embedded" ? "root" : "document");
|
|
135
|
+
const web = host === "web";
|
|
100
136
|
|
|
101
137
|
ledger.addClass(root, "cd-shell");
|
|
102
138
|
ledger.attribute(root, "data-cd-shell-host", host);
|
|
@@ -112,6 +148,13 @@ function renderShell(
|
|
|
112
148
|
if (!ledger.isClosed) ledger.attribute(root, name, value);
|
|
113
149
|
};
|
|
114
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
|
+
|
|
115
158
|
const layer = createElement(ownerDocument, "div", { class: "cd-shell-layer" });
|
|
116
159
|
ledger.defer(() => layer.remove());
|
|
117
160
|
const tooltip = createTooltip({ root, layer, id: `cd-shell-tooltip-${index}` });
|
|
@@ -152,6 +195,16 @@ function renderShell(
|
|
|
152
195
|
}
|
|
153
196
|
: undefined;
|
|
154
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
|
+
};
|
|
155
208
|
|
|
156
209
|
const menu = createMenu({
|
|
157
210
|
root,
|
|
@@ -160,65 +213,171 @@ function renderShell(
|
|
|
160
213
|
messages,
|
|
161
214
|
tooltip,
|
|
162
215
|
groups: options.menu ?? {},
|
|
163
|
-
|
|
216
|
+
shellEntries,
|
|
164
217
|
isReadOnly: () => readOnly,
|
|
165
218
|
});
|
|
166
219
|
ledger.defer(() => menu.destroy());
|
|
167
220
|
ledger.defer(tooltip.attach(menu.trigger, () => (menu.isOpen() ? null : { label: messages.menu })));
|
|
168
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
|
+
|
|
169
228
|
const canvas = options.canvas ?? createElement(ownerDocument, "div");
|
|
170
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");
|
|
171
232
|
if (!options.canvas) ledger.defer(() => canvas.remove());
|
|
172
233
|
|
|
173
|
-
/**
|
|
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. */
|
|
174
253
|
function moveFocusOutOf(island: HTMLElement): void {
|
|
175
|
-
if (
|
|
176
|
-
const canvasTarget = canvas.querySelector<HTMLElement | SVGElement>("svg[tabindex]");
|
|
177
|
-
canvasTarget?.focus({ preventScroll: true });
|
|
178
|
-
if (canvasTarget && ownerDocument.activeElement === canvasTarget) return;
|
|
179
|
-
if (!menu.trigger.hidden) menu.trigger.focus({ preventScroll: true });
|
|
254
|
+
if (island.contains(ownerDocument.activeElement)) focusCanvasOrTrigger();
|
|
180
255
|
}
|
|
181
256
|
|
|
182
257
|
const status = createStatusBox(ownerDocument);
|
|
183
258
|
ledger.defer(() => status.destroy());
|
|
184
259
|
const hint = options.emptyHint ? createEmptyHint(ownerDocument, messages, options.emptyHint) : undefined;
|
|
185
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());
|
|
186
274
|
const inspector = options.inspector
|
|
187
275
|
? createInspector(ownerDocument, options.inspector, (open) => {
|
|
188
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();
|
|
189
279
|
placeHint();
|
|
190
280
|
})
|
|
191
281
|
: undefined;
|
|
192
282
|
if (inspector) ledger.defer(() => inspector.destroy());
|
|
193
|
-
const
|
|
283
|
+
const problemsOptions = options.problems;
|
|
284
|
+
let popover: ProblemsPopover | undefined;
|
|
285
|
+
const pill = problemsOptions
|
|
194
286
|
? createProblemsPill({
|
|
195
287
|
ownerDocument,
|
|
196
288
|
messages,
|
|
197
289
|
lang,
|
|
198
290
|
tooltip,
|
|
199
291
|
showTooltip: host === "ide",
|
|
200
|
-
|
|
292
|
+
onActivate() {
|
|
293
|
+
if (popover) popover.toggle();
|
|
294
|
+
else problemsOptions.onActivate?.();
|
|
295
|
+
},
|
|
201
296
|
})
|
|
202
297
|
: undefined;
|
|
203
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());
|
|
204
323
|
const zoomPanel = createZoomPanel({ ownerDocument, messages, lang, platform, tooltip });
|
|
205
324
|
ledger.defer(() => zoomPanel.destroy());
|
|
206
|
-
|
|
207
|
-
|
|
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));
|
|
208
340
|
root.append(
|
|
209
341
|
...[
|
|
210
342
|
status.element,
|
|
211
343
|
hint?.element,
|
|
344
|
+
emptyState?.element,
|
|
212
345
|
inspector?.element,
|
|
213
346
|
pill?.element,
|
|
214
347
|
legend?.element,
|
|
348
|
+
legal?.element,
|
|
215
349
|
zoomPanel.element,
|
|
216
|
-
].filter(
|
|
350
|
+
].filter(defined),
|
|
217
351
|
layer,
|
|
352
|
+
...[toaster?.announcer, toaster?.element].filter(defined),
|
|
218
353
|
);
|
|
219
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
|
+
|
|
220
374
|
let palette: { registration: PaletteRegistration; detachTooltip: () => void } | undefined;
|
|
221
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));
|
|
222
381
|
|
|
223
382
|
function containerSize(): Size {
|
|
224
383
|
return { width: root.clientWidth, height: root.clientHeight };
|
|
@@ -243,6 +402,10 @@ function renderShell(
|
|
|
243
402
|
paletteRow,
|
|
244
403
|
problemsVisible: pill?.isShown() ?? false,
|
|
245
404
|
};
|
|
405
|
+
if (web) {
|
|
406
|
+
layout.zoomRaised = zoomRaised;
|
|
407
|
+
layout.legalHeight = legalHeight;
|
|
408
|
+
}
|
|
246
409
|
if (legend?.isOpen()) layout.legendWidth = legend.element.getBoundingClientRect().width;
|
|
247
410
|
if (inspector?.isOpen()) {
|
|
248
411
|
layout.inspector =
|
|
@@ -260,19 +423,89 @@ function renderShell(
|
|
|
260
423
|
const size = containerSize();
|
|
261
424
|
// A hidden tab reports 0 × 0; it neither measures nor decides anything (guide §5.9).
|
|
262
425
|
if (size.width === 0 || size.height === 0) return;
|
|
263
|
-
|
|
426
|
+
const topRow = decideTopRow({
|
|
264
427
|
containerWidth: size.width,
|
|
265
428
|
paletteWidth: paletteWidth(),
|
|
266
429
|
menuRight: menuRight(),
|
|
430
|
+
primary: primary?.widths(),
|
|
267
431
|
});
|
|
432
|
+
paletteRow = topRow.paletteRow;
|
|
268
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
|
+
}
|
|
269
439
|
if (legend && !legend.isStarted()) legend.start(options.legend?.open ?? legendStartsOpen(size.height));
|
|
440
|
+
updateBottomRow();
|
|
270
441
|
placeHint();
|
|
271
442
|
}
|
|
272
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
|
+
|
|
273
502
|
const ResizeObserverClass = globalThis.ResizeObserver;
|
|
274
503
|
const resizeObserver = ResizeObserverClass ? new ResizeObserverClass(() => updateLayout()) : undefined;
|
|
275
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);
|
|
276
509
|
ledger.defer(() => resizeObserver?.disconnect());
|
|
277
510
|
|
|
278
511
|
if (legend && options.legend?.open !== undefined) legend.start(options.legend.open);
|
|
@@ -283,12 +516,16 @@ function renderShell(
|
|
|
283
516
|
host,
|
|
284
517
|
dialogScope: keyTarget,
|
|
285
518
|
menuOpen: () => menu.isOpen(),
|
|
519
|
+
startScreen: () => emptyState?.isShown() ?? false,
|
|
286
520
|
closeMenu() {
|
|
287
521
|
const active = ownerDocument.activeElement;
|
|
288
522
|
menu.close({ restoreFocus: active === menu.trigger || menu.dropdown.contains(active) });
|
|
289
523
|
},
|
|
290
524
|
hideTooltip: () => tooltip.hide(),
|
|
291
|
-
|
|
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
|
+
},
|
|
292
529
|
});
|
|
293
530
|
keyTarget.addEventListener("keydown", onKeydown as EventListener);
|
|
294
531
|
ledger.defer(() => keyTarget.removeEventListener("keydown", onKeydown as EventListener));
|
|
@@ -347,6 +584,26 @@ function renderShell(
|
|
|
347
584
|
},
|
|
348
585
|
};
|
|
349
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
|
+
|
|
350
607
|
const inspectorHandle: InspectorHandle | undefined = inspector && {
|
|
351
608
|
element: inspector.element,
|
|
352
609
|
isOpen: () => inspector.isOpen(),
|
|
@@ -371,6 +628,8 @@ function renderShell(
|
|
|
371
628
|
tooltip: tooltipHandle,
|
|
372
629
|
legend: legendHandle,
|
|
373
630
|
inspector: inspectorHandle,
|
|
631
|
+
primary: primaryHandle,
|
|
632
|
+
appearance: appearanceHandle,
|
|
374
633
|
setZoomTarget(target: ZoomTarget | null) {
|
|
375
634
|
if (!ledger.isClosed) zoomPanel.setTarget(target);
|
|
376
635
|
},
|
|
@@ -396,8 +655,16 @@ function renderShell(
|
|
|
396
655
|
},
|
|
397
656
|
setProblems(summary) {
|
|
398
657
|
if (ledger.isClosed || !pill) return;
|
|
658
|
+
const listHadFocus = popover?.element.contains(ownerDocument.activeElement) ?? false;
|
|
399
659
|
pill.set(summary);
|
|
400
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();
|
|
401
668
|
placeHint();
|
|
402
669
|
},
|
|
403
670
|
setStatus(next, statusOptions) {
|
|
@@ -428,6 +695,9 @@ function renderShell(
|
|
|
428
695
|
applyReadOnly();
|
|
429
696
|
},
|
|
430
697
|
isReadOnly: () => readOnly,
|
|
698
|
+
setEmptyState: setEmptyStateShown,
|
|
699
|
+
isEmptyStateShown: () => emptyState?.isShown() ?? false,
|
|
700
|
+
toast: showToast,
|
|
431
701
|
destroy() {
|
|
432
702
|
ledger.close();
|
|
433
703
|
},
|
package/src/dom/problems.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { formatNumber, type ShellMessages } from "../core/index.js";
|
|
|
2
2
|
import { createElement } from "./element.js";
|
|
3
3
|
import { iconElement } from "./icons.js";
|
|
4
4
|
import type { TooltipController } from "./tooltip.js";
|
|
5
|
-
import type { ProblemSummary
|
|
5
|
+
import type { ProblemSummary } from "./types.js";
|
|
6
6
|
|
|
7
7
|
export interface ProblemsPill {
|
|
8
8
|
readonly element: HTMLButtonElement;
|
|
@@ -18,11 +18,12 @@ export interface ProblemsContext {
|
|
|
18
18
|
tooltip: TooltipController;
|
|
19
19
|
/** The IDE host opens its Problems view, so only there the pill explains itself. */
|
|
20
20
|
showTooltip: boolean;
|
|
21
|
-
|
|
21
|
+
/** A click: `ProblemsOptions.onActivate` (IDE host) or the problems popover's toggle (web host). */
|
|
22
|
+
onActivate(): void;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export function createProblemsPill(context: ProblemsContext): ProblemsPill {
|
|
25
|
-
const { ownerDocument, messages, lang, tooltip
|
|
26
|
+
const { ownerDocument, messages, lang, tooltip } = context;
|
|
26
27
|
const icon = createElement(ownerDocument, "span", { class: "cd-shell-problems-icon" });
|
|
27
28
|
const text = createElement(ownerDocument, "span", { class: "cd-shell-problems-text" });
|
|
28
29
|
const element = createElement(
|
|
@@ -33,7 +34,7 @@ export function createProblemsPill(context: ProblemsContext): ProblemsPill {
|
|
|
33
34
|
text,
|
|
34
35
|
);
|
|
35
36
|
element.hidden = true;
|
|
36
|
-
element.addEventListener("click", () =>
|
|
37
|
+
element.addEventListener("click", () => context.onActivate());
|
|
37
38
|
const detachTooltip = context.showTooltip
|
|
38
39
|
? tooltip.attach(element, { label: messages.showProblems })
|
|
39
40
|
: undefined;
|
package/src/dom/shell-dom.css
CHANGED
|
@@ -491,9 +491,9 @@ html.cd-shell-document kbd {
|
|
|
491
491
|
color: var(--cd-ui-warning);
|
|
492
492
|
}
|
|
493
493
|
|
|
494
|
-
/* The web app keeps its legal line (20px) below the pill. */
|
|
494
|
+
/* The web app keeps its legal line (20px, more where it wraps) below the pill. */
|
|
495
495
|
.cd-shell[data-cd-shell-host="web"] .cd-shell-problems {
|
|
496
|
-
bottom: calc(var(--cd-shell-rand) + 20px + var(--cd-shell-luecke));
|
|
496
|
+
bottom: calc(var(--cd-shell-rand) + var(--modeler-shell-legal-height, 20px) + var(--cd-shell-luecke));
|
|
497
497
|
}
|
|
498
498
|
|
|
499
499
|
/* ---- Status box under the palette (guide §5.12) --------------------------------------------- */
|
|
@@ -651,12 +651,15 @@ html.cd-shell-document kbd {
|
|
|
651
651
|
}
|
|
652
652
|
|
|
653
653
|
.cd-shell[data-cd-shell-host="web"] .cd-shell-legend {
|
|
654
|
-
--modeler-shell-legend-bottom: calc(
|
|
654
|
+
--modeler-shell-legend-bottom: calc(
|
|
655
|
+
var(--cd-shell-rand) + var(--modeler-shell-legal-height, 20px) + var(--cd-shell-luecke)
|
|
656
|
+
);
|
|
655
657
|
}
|
|
656
658
|
|
|
657
659
|
.cd-shell[data-cd-shell-host="web"][data-problems="shown"] .cd-shell-legend {
|
|
658
660
|
--modeler-shell-legend-bottom: calc(
|
|
659
|
-
var(--cd-shell-rand) +
|
|
661
|
+
var(--cd-shell-rand) + var(--modeler-shell-legal-height, 20px) + var(--cd-shell-luecke) +
|
|
662
|
+
var(--cd-shell-zeile) + var(--cd-shell-luecke)
|
|
660
663
|
);
|
|
661
664
|
}
|
|
662
665
|
|
package/src/dom/tooltip.ts
CHANGED
|
@@ -43,6 +43,15 @@ export function createTooltip({ root, layer, id }: TooltipContext): TooltipContr
|
|
|
43
43
|
// A clicked control keeps its tooltip hidden until the pointer leaves it.
|
|
44
44
|
let pressed: HTMLElement | undefined;
|
|
45
45
|
const detachers = new Set<() => void>();
|
|
46
|
+
// The tooltip grows when a web font arrives after it was placed; place it again so it keeps
|
|
47
|
+
// its edge distance instead of running past the container.
|
|
48
|
+
const resizeObserver =
|
|
49
|
+
typeof ResizeObserver === "function"
|
|
50
|
+
? new ResizeObserver(() => {
|
|
51
|
+
if (shown) place(shown.control);
|
|
52
|
+
})
|
|
53
|
+
: undefined;
|
|
54
|
+
resizeObserver?.observe(element);
|
|
46
55
|
|
|
47
56
|
function cancelPending(): void {
|
|
48
57
|
if (pending) clearTimeout(pending.timer);
|
|
@@ -231,6 +240,7 @@ export function createTooltip({ root, layer, id }: TooltipContext): TooltipContr
|
|
|
231
240
|
destroy() {
|
|
232
241
|
hide();
|
|
233
242
|
for (const detach of [...detachers]) detach();
|
|
243
|
+
resizeObserver?.disconnect();
|
|
234
244
|
element.remove();
|
|
235
245
|
},
|
|
236
246
|
};
|