@miragon/modeler-shell 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-66HCSJNY.js} +1247 -31
- package/dist/chunk-66HCSJNY.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 +1654 -197
- 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 +1541 -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/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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { T as Theme, A as Appearance } from './appearance-D3FUcbu6.cjs';
|
|
2
|
-
import { S as ShellOptions, a as ShellHandle } from './types-
|
|
3
|
-
import { V as ViewportState } from './
|
|
2
|
+
import { S as ShellOptions, a as ShellHandle } from './types-CnRrINRs.cjs';
|
|
3
|
+
import { V as ViewportState } from './toasts-Bp0REqdq.cjs';
|
|
4
4
|
export { A as APPEARANCE_MESSAGE_TYPE, a as AppearanceMessage, b as appearanceMessage, i as isAppearanceMessage } from './protocol-DhYBakWL.cjs';
|
|
5
|
-
import './
|
|
5
|
+
import './appearance-XX3JC3pI.cjs';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Writes `data-cd-theme` and `data-cd-kontrast` on `element` (default `<html>`) in one task, with
|
|
@@ -34,7 +34,9 @@ declare function connectAppearance(options: AppearanceListenerOptions): () => vo
|
|
|
34
34
|
*/
|
|
35
35
|
declare function connectReducedMotion(): () => void;
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
/** The web column's options (0.2): a VS Code webview has none of them (guide §5.2, §13.5). */
|
|
38
|
+
type WebHostOptionKey = "primary" | "legal" | "file" | "appearance" | "emptyState" | "dropZone";
|
|
39
|
+
interface VsCodeShellOptions extends Omit<ShellOptions, "host" | "keyScope" | "themes" | WebHostOptionKey>, AppearanceListenerOptions {
|
|
38
40
|
}
|
|
39
41
|
/**
|
|
40
42
|
* `mountShell` for a VS Code webview: `host: "ide"`, the key listener on `document` (VS Code's own
|
package/dist/vscode-webview.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { T as Theme, A as Appearance } from './appearance-D3FUcbu6.js';
|
|
2
|
-
import { S as ShellOptions, a as ShellHandle } from './types-
|
|
3
|
-
import { V as ViewportState } from './
|
|
2
|
+
import { S as ShellOptions, a as ShellHandle } from './types-PuDZczzy.js';
|
|
3
|
+
import { V as ViewportState } from './toasts-Bp0REqdq.js';
|
|
4
4
|
export { A as APPEARANCE_MESSAGE_TYPE, a as AppearanceMessage, b as appearanceMessage, i as isAppearanceMessage } from './protocol-IeW8bf4f.js';
|
|
5
|
-
import './
|
|
5
|
+
import './appearance-BBaM_4bD.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Writes `data-cd-theme` and `data-cd-kontrast` on `element` (default `<html>`) in one task, with
|
|
@@ -34,7 +34,9 @@ declare function connectAppearance(options: AppearanceListenerOptions): () => vo
|
|
|
34
34
|
*/
|
|
35
35
|
declare function connectReducedMotion(): () => void;
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
/** The web column's options (0.2): a VS Code webview has none of them (guide §5.2, §13.5). */
|
|
38
|
+
type WebHostOptionKey = "primary" | "legal" | "file" | "appearance" | "emptyState" | "dropZone";
|
|
39
|
+
interface VsCodeShellOptions extends Omit<ShellOptions, "host" | "keyScope" | "themes" | WebHostOptionKey>, AppearanceListenerOptions {
|
|
38
40
|
}
|
|
39
41
|
/**
|
|
40
42
|
* `mountShell` for a VS Code webview: `host: "ide"`, the key listener on `document` (VS Code's own
|
package/dist/vscode-webview.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
mountShell
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-66HCSJNY.js";
|
|
4
4
|
import {
|
|
5
5
|
APPEARANCE_MESSAGE_TYPE,
|
|
6
6
|
appearanceMessage,
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
appearanceFromBodyClasses,
|
|
13
13
|
reducedMotionFromBodyClasses,
|
|
14
14
|
resolveAppearance
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-OADW3GPA.js";
|
|
16
16
|
|
|
17
17
|
// src/vscode/webview/appearance.ts
|
|
18
18
|
var NO_TRANSITION_CLASS = "cd-shell-no-transition";
|
|
@@ -137,6 +137,12 @@ function withDestroyHook(shell, beforeDestroy) {
|
|
|
137
137
|
get inspector() {
|
|
138
138
|
return shell.inspector;
|
|
139
139
|
},
|
|
140
|
+
get primary() {
|
|
141
|
+
return shell.primary;
|
|
142
|
+
},
|
|
143
|
+
get appearance() {
|
|
144
|
+
return shell.appearance;
|
|
145
|
+
},
|
|
140
146
|
setZoomTarget: (target) => shell.setZoomTarget(target),
|
|
141
147
|
fitInsets: () => shell.fitInsets(),
|
|
142
148
|
registerPalette: (palette) => shell.registerPalette(palette),
|
|
@@ -145,6 +151,9 @@ function withDestroyHook(shell, beforeDestroy) {
|
|
|
145
151
|
setEmptyHint: (visible) => shell.setEmptyHint(visible),
|
|
146
152
|
setReadOnly: (readOnly, options) => shell.setReadOnly(readOnly, options),
|
|
147
153
|
isReadOnly: () => shell.isReadOnly(),
|
|
154
|
+
setEmptyState: (shown) => shell.setEmptyState(shown),
|
|
155
|
+
isEmptyStateShown: () => shell.isEmptyStateShown(),
|
|
156
|
+
toast: (request) => shell.toast(request),
|
|
148
157
|
destroy() {
|
|
149
158
|
beforeDestroy();
|
|
150
159
|
shell.destroy();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/vscode/webview/appearance.ts","../src/vscode/webview/mount.ts","../src/vscode/webview/state.ts"],"sourcesContent":["import {\n REDUCED_MOTION_CLASS,\n appearanceAttributes,\n appearanceFromBodyClasses,\n reducedMotionFromBodyClasses,\n resolveAppearance,\n type Appearance,\n type Theme,\n} from \"../../core/index.js\";\nimport { isAppearanceMessage } from \"../protocol.js\";\n\nconst NO_TRANSITION_CLASS = \"cd-shell-no-transition\";\n\n/** The latest mode switch per `<html>`: only its second frame may lift the transition stop. */\nconst latestSwitch = new WeakMap<Element, number>();\n\nfunction showsAppearance(element: Element, appearance: Appearance): boolean {\n const attributes = appearanceAttributes(appearance);\n return (\n element.getAttribute(\"data-cd-theme\") === attributes[\"data-cd-theme\"] &&\n element.getAttribute(\"data-cd-kontrast\") === (attributes[\"data-cd-kontrast\"] ?? null)\n );\n}\n\n/**\n * Writes `data-cd-theme` and `data-cd-kontrast` on `element` (default `<html>`) in one task, with\n * transitions suppressed for one frame: adds `cd-shell-no-transition` to `<html>`, sets both\n * attributes and removes the class in a nested `requestAnimationFrame` (after the last of several\n * quick switches).\n */\nexport function applyAppearance(appearance: Appearance, element?: HTMLElement): void {\n const target = element ?? document.documentElement;\n const html = target.ownerDocument.documentElement;\n const attributes = appearanceAttributes(appearance);\n const kontrast = attributes[\"data-cd-kontrast\"];\n\n const switchId = (latestSwitch.get(html) ?? 0) + 1;\n latestSwitch.set(html, switchId);\n html.classList.add(NO_TRANSITION_CLASS);\n target.setAttribute(\"data-cd-theme\", attributes[\"data-cd-theme\"]);\n if (kontrast) target.setAttribute(\"data-cd-kontrast\", kontrast);\n else target.removeAttribute(\"data-cd-kontrast\");\n\n requestAnimationFrame(() =>\n requestAnimationFrame(() => {\n if (latestSwitch.get(html) !== switchId) return;\n latestSwitch.delete(html);\n html.classList.remove(NO_TRANSITION_CLASS);\n }),\n );\n}\n\nexport interface AppearanceListenerOptions {\n /** The modes the modeler can show; the body-class fallback resolves against them. */\n themes: readonly Theme[];\n}\n\n/**\n * Listens for `AppearanceMessage`s on `window` and applies them to `<html>`, resolved against\n * `themes` once more so a webview never shows a mode its bundle cannot. A page whose `<html>` has\n * no `data-cd-theme` at connect (no `renderWebviewHtml`) falls back to the VS Code body classes,\n * through a `MutationObserver` on `<body>` until the first message (resolved against `themes`).\n * With the attributes of `renderWebviewHtml` there is no fallback: the extension's bridge knows the\n * `colorTheme` setting, the body classes do not, and VS Code changes them before the bridge's\n * message arrives, which would flash the mode without the setting. Returns the disconnect\n * function. Throws when `themes` is empty.\n */\nexport function connectAppearance(options: AppearanceListenerOptions): () => void {\n const { themes } = options;\n if (themes.length === 0) {\n throw new Error(\"@miragon/modeler-shell: connectAppearance needs at least one theme\");\n }\n const html = document.documentElement;\n const show = (requested: Appearance): void => {\n const resolved = resolveAppearance(requested, themes);\n if (!showsAppearance(html, resolved)) applyAppearance(resolved, html);\n };\n\n let fallbackActive = !html.hasAttribute(\"data-cd-theme\");\n const applyBodyClasses = (): void => {\n if (!fallbackActive || !document.body) return;\n const requested = appearanceFromBodyClasses(document.body.classList);\n if (requested) show(requested);\n };\n const observer = new MutationObserver(applyBodyClasses);\n const startFallback = (): void => {\n if (!fallbackActive || !document.body) return;\n observer.observe(document.body, { attributes: true, attributeFilter: [\"class\"] });\n applyBodyClasses();\n };\n const stopFallback = (): void => {\n if (!fallbackActive) return;\n fallbackActive = false;\n observer.disconnect();\n document.removeEventListener(\"DOMContentLoaded\", startFallback);\n };\n\n const onMessage = (event: MessageEvent): void => {\n if (!isAppearanceMessage(event.data)) return;\n stopFallback();\n show(event.data.appearance);\n };\n\n window.addEventListener(\"message\", onMessage);\n if (document.body) startFallback();\n else if (fallbackActive) document.addEventListener(\"DOMContentLoaded\", startFallback, { once: true });\n\n return () => {\n window.removeEventListener(\"message\", onMessage);\n fallbackActive = false;\n observer.disconnect();\n document.removeEventListener(\"DOMContentLoaded\", startFallback);\n };\n}\n\n/**\n * Mirrors VS Code's `workbench.reduceMotion` (the body class `vscode-reduce-motion`, which VS Code\n * keeps up to date) as `REDUCED_MOTION_CLASS` on `<html>` for the whole session: the shell's\n * animations stop and an explicit fit jumps, as under `prefers-reduced-motion` (guide §5.14, §7).\n * Returns the disconnect function, which removes the class again.\n */\nexport function connectReducedMotion(): () => void {\n const html = document.documentElement;\n const apply = (): void => {\n if (document.body)\n html.classList.toggle(REDUCED_MOTION_CLASS, reducedMotionFromBodyClasses(document.body.classList));\n };\n const observer = new MutationObserver(apply);\n const start = (): void => {\n if (!document.body) return;\n observer.observe(document.body, { attributes: true, attributeFilter: [\"class\"] });\n apply();\n };\n if (document.body) start();\n else document.addEventListener(\"DOMContentLoaded\", start, { once: true });\n return () => {\n observer.disconnect();\n document.removeEventListener(\"DOMContentLoaded\", start);\n html.classList.remove(REDUCED_MOTION_CLASS);\n };\n}\n","import { mountShell, type ShellHandle, type ShellOptions } from \"../../dom/index.js\";\nimport { connectAppearance, connectReducedMotion, type AppearanceListenerOptions } from \"./appearance.js\";\n\nexport interface VsCodeShellOptions\n extends Omit<ShellOptions, \"host\" | \"keyScope\" | \"themes\">, AppearanceListenerOptions {}\n\n/** The shell's handle with a `destroy` that runs `beforeDestroy` first; every other member delegates. */\nfunction withDestroyHook(shell: ShellHandle, beforeDestroy: () => void): ShellHandle {\n return {\n get root() {\n return shell.root;\n },\n get canvas() {\n return shell.canvas;\n },\n get host() {\n return shell.host;\n },\n get lang() {\n return shell.lang;\n },\n get platform() {\n return shell.platform;\n },\n get messages() {\n return shell.messages;\n },\n get menu() {\n return shell.menu;\n },\n get tooltip() {\n return shell.tooltip;\n },\n get legend() {\n return shell.legend;\n },\n get inspector() {\n return shell.inspector;\n },\n setZoomTarget: (target) => shell.setZoomTarget(target),\n fitInsets: () => shell.fitInsets(),\n registerPalette: (palette) => shell.registerPalette(palette),\n setProblems: (summary) => shell.setProblems(summary),\n setStatus: (status, options) => shell.setStatus(status, options),\n setEmptyHint: (visible) => shell.setEmptyHint(visible),\n setReadOnly: (readOnly, options) => shell.setReadOnly(readOnly, options),\n isReadOnly: () => shell.isReadOnly(),\n destroy() {\n beforeDestroy();\n shell.destroy();\n },\n };\n}\n\n/**\n * `mountShell` for a VS Code webview: `host: \"ide\"`, the key listener on `document` (VS Code's own\n * listener sits on `window`, so handled keys are stopped before it), `cd-shell-document` on\n * `<html>`, plus `connectAppearance` and `connectReducedMotion`. `destroy()` also disconnects both.\n */\nexport function mountVsCodeShell(root: HTMLElement, options: VsCodeShellOptions): ShellHandle {\n const { themes } = options;\n const disconnectAppearance = connectAppearance({ themes });\n const disconnectReducedMotion = connectReducedMotion();\n const disconnect = (): void => {\n disconnectAppearance();\n disconnectReducedMotion();\n };\n let shell: ShellHandle;\n try {\n shell = mountShell(root, { ...options, host: \"ide\", keyScope: \"document\" });\n } catch (error) {\n disconnect();\n throw error;\n }\n return withDestroyHook(shell, disconnect);\n}\n","import type { Bounds, ViewportState } from \"../../core/index.js\";\n\n/** `acquireVsCodeApi()` as far as the state helpers need it. */\nexport interface WebviewStateApi {\n getState(): unknown;\n setState(state: unknown): void;\n}\n\n/** What the shell keeps across a webview rebuild (guide §13.5: legend state and view). */\nexport interface ShellWebviewState {\n legendOpen?: boolean | undefined;\n view?: ViewportState | undefined;\n}\n\n/** The key under which the shell's part lives in the webview state; other keys stay untouched. */\nexport const SHELL_STATE_KEY = \"modelerShell\";\n\ntype StateRecord = Record<string, unknown>;\n\nfunction isRecord(value: unknown): value is StateRecord {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction storedState(api: WebviewStateApi): StateRecord {\n const state = api.getState();\n return isRecord(state) ? state : {};\n}\n\nfunction viewboxFrom(value: unknown): Bounds | undefined {\n if (!isRecord(value)) return undefined;\n const { x, y, width, height } = value;\n if (typeof x !== \"number\" || typeof y !== \"number\") return undefined;\n if (typeof width !== \"number\" || typeof height !== \"number\") return undefined;\n if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(width) || !Number.isFinite(height)) {\n return undefined;\n }\n return width > 0 && height > 0 ? { x, y, width, height } : undefined;\n}\n\nfunction viewFrom(value: unknown): ViewportState | undefined {\n if (!isRecord(value) || typeof value.userMoved !== \"boolean\") return undefined;\n const viewbox = viewboxFrom(value.viewbox);\n return viewbox ? { viewbox, userMoved: value.userMoved } : undefined;\n}\n\n/** The shell's part of `api.getState()`; `{}` when there is none or it is malformed. */\nexport function readShellState(api: WebviewStateApi): ShellWebviewState {\n const part = storedState(api)[SHELL_STATE_KEY];\n if (!isRecord(part)) return {};\n const state: ShellWebviewState = {};\n if (typeof part.legendOpen === \"boolean\") state.legendOpen = part.legendOpen;\n const view = viewFrom(part.view);\n if (view) state.view = view;\n return state;\n}\n\n/**\n * Merges `patch` into the shell's part and writes the whole state back, keeping every other key.\n * A value that is `undefined` (for example `viewport.getState()` before the first import) or\n * malformed leaves the stored value alone; nothing is written when nothing changed.\n */\nexport function writeShellState(api: WebviewStateApi, patch: ShellWebviewState): void {\n const state = storedState(api);\n const part = isRecord(state[SHELL_STATE_KEY]) ? (state[SHELL_STATE_KEY] as StateRecord) : {};\n const next: StateRecord = { ...part };\n if (typeof patch.legendOpen === \"boolean\") next.legendOpen = patch.legendOpen;\n const view = viewFrom(patch.view);\n if (view) next.view = view;\n if (JSON.stringify(next) === JSON.stringify(state[SHELL_STATE_KEY])) return;\n api.setState({ ...state, [SHELL_STATE_KEY]: next });\n}\n\n/** What `persistView` reads of a viewport (`connectDiagramJs(…).viewport`). */\nexport interface PersistableView {\n onChange(listener: () => void): () => void;\n getState(): ViewportState | undefined;\n}\n\nexport interface PersistViewOptions {\n /** Quiet time before the write, default 250ms. */\n delayMs?: number | undefined;\n}\n\n/** A pan, a wheel zoom or a fit animation changes the view on every frame. */\nconst PERSIST_VIEW_DELAY_MS = 250;\n\n/**\n * Keeps the stored view (`writeShellState(api, { view })`) up to date once the view has rested for\n * `delayMs`, instead of one `vscode.setState` IPC message per animation frame. Returns the stop\n * function; it writes a pending change at once.\n */\nexport function persistView(\n api: WebviewStateApi,\n viewport: PersistableView,\n options: PersistViewOptions = {},\n): () => void {\n const delayMs = options.delayMs ?? PERSIST_VIEW_DELAY_MS;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const write = (): void => {\n timer = undefined;\n writeShellState(api, { view: viewport.getState() });\n };\n const unsubscribe = viewport.onChange(() => {\n clearTimeout(timer);\n timer = setTimeout(write, delayMs);\n });\n let stopped = false;\n return () => {\n if (stopped) return;\n stopped = true;\n unsubscribe();\n if (timer === undefined) return;\n clearTimeout(timer);\n write();\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/vscode/webview/appearance.ts","../src/vscode/webview/mount.ts","../src/vscode/webview/state.ts"],"sourcesContent":["import {\n REDUCED_MOTION_CLASS,\n appearanceAttributes,\n appearanceFromBodyClasses,\n reducedMotionFromBodyClasses,\n resolveAppearance,\n type Appearance,\n type Theme,\n} from \"../../core/index.js\";\nimport { isAppearanceMessage } from \"../protocol.js\";\n\nconst NO_TRANSITION_CLASS = \"cd-shell-no-transition\";\n\n/** The latest mode switch per `<html>`: only its second frame may lift the transition stop. */\nconst latestSwitch = new WeakMap<Element, number>();\n\nfunction showsAppearance(element: Element, appearance: Appearance): boolean {\n const attributes = appearanceAttributes(appearance);\n return (\n element.getAttribute(\"data-cd-theme\") === attributes[\"data-cd-theme\"] &&\n element.getAttribute(\"data-cd-kontrast\") === (attributes[\"data-cd-kontrast\"] ?? null)\n );\n}\n\n/**\n * Writes `data-cd-theme` and `data-cd-kontrast` on `element` (default `<html>`) in one task, with\n * transitions suppressed for one frame: adds `cd-shell-no-transition` to `<html>`, sets both\n * attributes and removes the class in a nested `requestAnimationFrame` (after the last of several\n * quick switches).\n */\nexport function applyAppearance(appearance: Appearance, element?: HTMLElement): void {\n const target = element ?? document.documentElement;\n const html = target.ownerDocument.documentElement;\n const attributes = appearanceAttributes(appearance);\n const kontrast = attributes[\"data-cd-kontrast\"];\n\n const switchId = (latestSwitch.get(html) ?? 0) + 1;\n latestSwitch.set(html, switchId);\n html.classList.add(NO_TRANSITION_CLASS);\n target.setAttribute(\"data-cd-theme\", attributes[\"data-cd-theme\"]);\n if (kontrast) target.setAttribute(\"data-cd-kontrast\", kontrast);\n else target.removeAttribute(\"data-cd-kontrast\");\n\n requestAnimationFrame(() =>\n requestAnimationFrame(() => {\n if (latestSwitch.get(html) !== switchId) return;\n latestSwitch.delete(html);\n html.classList.remove(NO_TRANSITION_CLASS);\n }),\n );\n}\n\nexport interface AppearanceListenerOptions {\n /** The modes the modeler can show; the body-class fallback resolves against them. */\n themes: readonly Theme[];\n}\n\n/**\n * Listens for `AppearanceMessage`s on `window` and applies them to `<html>`, resolved against\n * `themes` once more so a webview never shows a mode its bundle cannot. A page whose `<html>` has\n * no `data-cd-theme` at connect (no `renderWebviewHtml`) falls back to the VS Code body classes,\n * through a `MutationObserver` on `<body>` until the first message (resolved against `themes`).\n * With the attributes of `renderWebviewHtml` there is no fallback: the extension's bridge knows the\n * `colorTheme` setting, the body classes do not, and VS Code changes them before the bridge's\n * message arrives, which would flash the mode without the setting. Returns the disconnect\n * function. Throws when `themes` is empty.\n */\nexport function connectAppearance(options: AppearanceListenerOptions): () => void {\n const { themes } = options;\n if (themes.length === 0) {\n throw new Error(\"@miragon/modeler-shell: connectAppearance needs at least one theme\");\n }\n const html = document.documentElement;\n const show = (requested: Appearance): void => {\n const resolved = resolveAppearance(requested, themes);\n if (!showsAppearance(html, resolved)) applyAppearance(resolved, html);\n };\n\n let fallbackActive = !html.hasAttribute(\"data-cd-theme\");\n const applyBodyClasses = (): void => {\n if (!fallbackActive || !document.body) return;\n const requested = appearanceFromBodyClasses(document.body.classList);\n if (requested) show(requested);\n };\n const observer = new MutationObserver(applyBodyClasses);\n const startFallback = (): void => {\n if (!fallbackActive || !document.body) return;\n observer.observe(document.body, { attributes: true, attributeFilter: [\"class\"] });\n applyBodyClasses();\n };\n const stopFallback = (): void => {\n if (!fallbackActive) return;\n fallbackActive = false;\n observer.disconnect();\n document.removeEventListener(\"DOMContentLoaded\", startFallback);\n };\n\n const onMessage = (event: MessageEvent): void => {\n if (!isAppearanceMessage(event.data)) return;\n stopFallback();\n show(event.data.appearance);\n };\n\n window.addEventListener(\"message\", onMessage);\n if (document.body) startFallback();\n else if (fallbackActive) document.addEventListener(\"DOMContentLoaded\", startFallback, { once: true });\n\n return () => {\n window.removeEventListener(\"message\", onMessage);\n fallbackActive = false;\n observer.disconnect();\n document.removeEventListener(\"DOMContentLoaded\", startFallback);\n };\n}\n\n/**\n * Mirrors VS Code's `workbench.reduceMotion` (the body class `vscode-reduce-motion`, which VS Code\n * keeps up to date) as `REDUCED_MOTION_CLASS` on `<html>` for the whole session: the shell's\n * animations stop and an explicit fit jumps, as under `prefers-reduced-motion` (guide §5.14, §7).\n * Returns the disconnect function, which removes the class again.\n */\nexport function connectReducedMotion(): () => void {\n const html = document.documentElement;\n const apply = (): void => {\n if (document.body)\n html.classList.toggle(REDUCED_MOTION_CLASS, reducedMotionFromBodyClasses(document.body.classList));\n };\n const observer = new MutationObserver(apply);\n const start = (): void => {\n if (!document.body) return;\n observer.observe(document.body, { attributes: true, attributeFilter: [\"class\"] });\n apply();\n };\n if (document.body) start();\n else document.addEventListener(\"DOMContentLoaded\", start, { once: true });\n return () => {\n observer.disconnect();\n document.removeEventListener(\"DOMContentLoaded\", start);\n html.classList.remove(REDUCED_MOTION_CLASS);\n };\n}\n","import { mountShell, type ShellHandle, type ShellOptions } from \"../../dom/index.js\";\nimport { connectAppearance, connectReducedMotion, type AppearanceListenerOptions } from \"./appearance.js\";\n\n/** The web column's options (0.2): a VS Code webview has none of them (guide §5.2, §13.5). */\ntype WebHostOptionKey = \"primary\" | \"legal\" | \"file\" | \"appearance\" | \"emptyState\" | \"dropZone\";\n\nexport interface VsCodeShellOptions\n extends Omit<ShellOptions, \"host\" | \"keyScope\" | \"themes\" | WebHostOptionKey>, AppearanceListenerOptions {}\n\n/** The shell's handle with a `destroy` that runs `beforeDestroy` first; every other member delegates. */\nfunction withDestroyHook(shell: ShellHandle, beforeDestroy: () => void): ShellHandle {\n return {\n get root() {\n return shell.root;\n },\n get canvas() {\n return shell.canvas;\n },\n get host() {\n return shell.host;\n },\n get lang() {\n return shell.lang;\n },\n get platform() {\n return shell.platform;\n },\n get messages() {\n return shell.messages;\n },\n get menu() {\n return shell.menu;\n },\n get tooltip() {\n return shell.tooltip;\n },\n get legend() {\n return shell.legend;\n },\n get inspector() {\n return shell.inspector;\n },\n get primary() {\n return shell.primary;\n },\n get appearance() {\n return shell.appearance;\n },\n setZoomTarget: (target) => shell.setZoomTarget(target),\n fitInsets: () => shell.fitInsets(),\n registerPalette: (palette) => shell.registerPalette(palette),\n setProblems: (summary) => shell.setProblems(summary),\n setStatus: (status, options) => shell.setStatus(status, options),\n setEmptyHint: (visible) => shell.setEmptyHint(visible),\n setReadOnly: (readOnly, options) => shell.setReadOnly(readOnly, options),\n isReadOnly: () => shell.isReadOnly(),\n setEmptyState: (shown) => shell.setEmptyState(shown),\n isEmptyStateShown: () => shell.isEmptyStateShown(),\n toast: (request) => shell.toast(request),\n destroy() {\n beforeDestroy();\n shell.destroy();\n },\n };\n}\n\n/**\n * `mountShell` for a VS Code webview: `host: \"ide\"`, the key listener on `document` (VS Code's own\n * listener sits on `window`, so handled keys are stopped before it), `cd-shell-document` on\n * `<html>`, plus `connectAppearance` and `connectReducedMotion`. `destroy()` also disconnects both.\n */\nexport function mountVsCodeShell(root: HTMLElement, options: VsCodeShellOptions): ShellHandle {\n const { themes } = options;\n const disconnectAppearance = connectAppearance({ themes });\n const disconnectReducedMotion = connectReducedMotion();\n const disconnect = (): void => {\n disconnectAppearance();\n disconnectReducedMotion();\n };\n let shell: ShellHandle;\n try {\n shell = mountShell(root, { ...options, host: \"ide\", keyScope: \"document\" });\n } catch (error) {\n disconnect();\n throw error;\n }\n return withDestroyHook(shell, disconnect);\n}\n","import type { Bounds, ViewportState } from \"../../core/index.js\";\n\n/** `acquireVsCodeApi()` as far as the state helpers need it. */\nexport interface WebviewStateApi {\n getState(): unknown;\n setState(state: unknown): void;\n}\n\n/** What the shell keeps across a webview rebuild (guide §13.5: legend state and view). */\nexport interface ShellWebviewState {\n legendOpen?: boolean | undefined;\n view?: ViewportState | undefined;\n}\n\n/** The key under which the shell's part lives in the webview state; other keys stay untouched. */\nexport const SHELL_STATE_KEY = \"modelerShell\";\n\ntype StateRecord = Record<string, unknown>;\n\nfunction isRecord(value: unknown): value is StateRecord {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction storedState(api: WebviewStateApi): StateRecord {\n const state = api.getState();\n return isRecord(state) ? state : {};\n}\n\nfunction viewboxFrom(value: unknown): Bounds | undefined {\n if (!isRecord(value)) return undefined;\n const { x, y, width, height } = value;\n if (typeof x !== \"number\" || typeof y !== \"number\") return undefined;\n if (typeof width !== \"number\" || typeof height !== \"number\") return undefined;\n if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(width) || !Number.isFinite(height)) {\n return undefined;\n }\n return width > 0 && height > 0 ? { x, y, width, height } : undefined;\n}\n\nfunction viewFrom(value: unknown): ViewportState | undefined {\n if (!isRecord(value) || typeof value.userMoved !== \"boolean\") return undefined;\n const viewbox = viewboxFrom(value.viewbox);\n return viewbox ? { viewbox, userMoved: value.userMoved } : undefined;\n}\n\n/** The shell's part of `api.getState()`; `{}` when there is none or it is malformed. */\nexport function readShellState(api: WebviewStateApi): ShellWebviewState {\n const part = storedState(api)[SHELL_STATE_KEY];\n if (!isRecord(part)) return {};\n const state: ShellWebviewState = {};\n if (typeof part.legendOpen === \"boolean\") state.legendOpen = part.legendOpen;\n const view = viewFrom(part.view);\n if (view) state.view = view;\n return state;\n}\n\n/**\n * Merges `patch` into the shell's part and writes the whole state back, keeping every other key.\n * A value that is `undefined` (for example `viewport.getState()` before the first import) or\n * malformed leaves the stored value alone; nothing is written when nothing changed.\n */\nexport function writeShellState(api: WebviewStateApi, patch: ShellWebviewState): void {\n const state = storedState(api);\n const part = isRecord(state[SHELL_STATE_KEY]) ? (state[SHELL_STATE_KEY] as StateRecord) : {};\n const next: StateRecord = { ...part };\n if (typeof patch.legendOpen === \"boolean\") next.legendOpen = patch.legendOpen;\n const view = viewFrom(patch.view);\n if (view) next.view = view;\n if (JSON.stringify(next) === JSON.stringify(state[SHELL_STATE_KEY])) return;\n api.setState({ ...state, [SHELL_STATE_KEY]: next });\n}\n\n/** What `persistView` reads of a viewport (`connectDiagramJs(…).viewport`). */\nexport interface PersistableView {\n onChange(listener: () => void): () => void;\n getState(): ViewportState | undefined;\n}\n\nexport interface PersistViewOptions {\n /** Quiet time before the write, default 250ms. */\n delayMs?: number | undefined;\n}\n\n/** A pan, a wheel zoom or a fit animation changes the view on every frame. */\nconst PERSIST_VIEW_DELAY_MS = 250;\n\n/**\n * Keeps the stored view (`writeShellState(api, { view })`) up to date once the view has rested for\n * `delayMs`, instead of one `vscode.setState` IPC message per animation frame. Returns the stop\n * function; it writes a pending change at once.\n */\nexport function persistView(\n api: WebviewStateApi,\n viewport: PersistableView,\n options: PersistViewOptions = {},\n): () => void {\n const delayMs = options.delayMs ?? PERSIST_VIEW_DELAY_MS;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const write = (): void => {\n timer = undefined;\n writeShellState(api, { view: viewport.getState() });\n };\n const unsubscribe = viewport.onChange(() => {\n clearTimeout(timer);\n timer = setTimeout(write, delayMs);\n });\n let stopped = false;\n return () => {\n if (stopped) return;\n stopped = true;\n unsubscribe();\n if (timer === undefined) return;\n clearTimeout(timer);\n write();\n };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miragon/modeler-shell",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "The shared, framework-free chrome around Miragon diagram-js modelers: menu, palette skin, tooltip, zoom panel, keys, fit, problems pill, status box, legend and inspector slots,
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "The shared, framework-free chrome around Miragon diagram-js modelers: menu, palette skin, tooltip, zoom panel, keys, fit, problems pill, status box, legend and inspector slots, the VS Code host plumbing and the web-app chrome (primary action, legal line, toasts, empty state, drop zone).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { Theme } from "../appearance.js";
|
|
2
|
+
|
|
3
|
+
/** The web app's mode choice (guide §13.3): Appearance, System · Light · Dark. */
|
|
4
|
+
export type AppearanceChoice = "system" | "light" | "dark";
|
|
5
|
+
|
|
6
|
+
/** Where the web app keeps the choice: `light` or `dark`; "System" removes the entry (guide §13.3). */
|
|
7
|
+
export const APPEARANCE_STORAGE_KEY = "miragon.darstellung";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The two `theme-color` values (guide §11, §13.3): light is the app's own value (`#335DE5`, `fest`
|
|
11
|
+
* `--cd-blau` of the vendored `cd-tokens.generated.json`), dark `#1D1D1D` (`dunkel`
|
|
12
|
+
* `--cd-ui-grund`). The modeler reads both from its vendored JSON; the shell keeps no colour.
|
|
13
|
+
*/
|
|
14
|
+
export interface ThemeColors {
|
|
15
|
+
light: string;
|
|
16
|
+
dark: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** `"light"` and `"dark"` pass, everything else (a missing entry, a foreign value) is `"system"`. */
|
|
20
|
+
export function parseAppearanceChoice(stored: unknown): AppearanceChoice {
|
|
21
|
+
return stored === "light" || stored === "dark" ? stored : "system";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** What `<html>` and the meta tags carry for a choice. */
|
|
25
|
+
export interface DocumentMode {
|
|
26
|
+
/** `data-cd-theme` on `<html>`. */
|
|
27
|
+
theme: "system" | Theme;
|
|
28
|
+
/** `<meta name="color-scheme" content>`. */
|
|
29
|
+
colorScheme: "light dark" | Theme;
|
|
30
|
+
/** The `content` of the `theme-color` metas with `media="(prefers-color-scheme: light|dark)"`. */
|
|
31
|
+
themeColor: ThemeColors;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The document mode for `choice` (guide §13.3). A modeler that cannot show dark (`themes` without
|
|
36
|
+
* `"dark"`) is `light` whatever the choice: theme `light`, color-scheme `light`, both theme
|
|
37
|
+
* colours `colors.light`. Dark-capable: `system` → theme `system`, color-scheme `light dark`,
|
|
38
|
+
* `colors` as they are; `light`/`dark` → that theme and color-scheme, both theme colours the
|
|
39
|
+
* chosen mode's, so the browser bar matches a fixed choice under the other system mode.
|
|
40
|
+
*/
|
|
41
|
+
export function documentMode(
|
|
42
|
+
choice: AppearanceChoice,
|
|
43
|
+
themes: readonly Theme[],
|
|
44
|
+
colors: ThemeColors,
|
|
45
|
+
): DocumentMode {
|
|
46
|
+
if (!themes.includes("dark")) return fixedMode("light", colors.light);
|
|
47
|
+
const parsed = parseAppearanceChoice(choice);
|
|
48
|
+
if (parsed === "system") {
|
|
49
|
+
return {
|
|
50
|
+
theme: "system",
|
|
51
|
+
colorScheme: "light dark",
|
|
52
|
+
themeColor: { light: colors.light, dark: colors.dark },
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return fixedMode(parsed, colors[parsed]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function fixedMode(theme: Theme, color: string): DocumentMode {
|
|
59
|
+
return { theme, colorScheme: theme, themeColor: { light: color, dark: color } };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface WebModeScriptOptions {
|
|
63
|
+
/** The modes the modeler can show, as passed to `mountShell`. */
|
|
64
|
+
themes: readonly Theme[];
|
|
65
|
+
themeColors: ThemeColors;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The body of the inline `<script>` in `<head>`, before the first stylesheet (guide §13.3). With
|
|
70
|
+
* `themes` including `"dark"`: the guide's script, which reads `APPEARANCE_STORAGE_KEY` inside
|
|
71
|
+
* `try`/`catch`, writes `data-cd-theme`, `color-scheme` and both `theme-color` metas for a fixed
|
|
72
|
+
* choice and sets `data-cd-kontrast="hoch"` under `(prefers-contrast: more)`. Light-only: the
|
|
73
|
+
* contrast line alone. Plain ES5 without the modeler's bundle, so it runs before the first paint.
|
|
74
|
+
*/
|
|
75
|
+
export function webModeScript(options: WebModeScriptOptions): string {
|
|
76
|
+
const contrast = [
|
|
77
|
+
`if (window.matchMedia && window.matchMedia(${scriptString("(prefers-contrast: more)")}).matches)`,
|
|
78
|
+
` document.documentElement.setAttribute("data-cd-kontrast", "hoch");`,
|
|
79
|
+
];
|
|
80
|
+
if (!options.themes.includes("dark")) return contrast.join("\n");
|
|
81
|
+
const { light, dark } = options.themeColors;
|
|
82
|
+
return [
|
|
83
|
+
"(function () {",
|
|
84
|
+
" var html = document.documentElement;",
|
|
85
|
+
" var choice = null;",
|
|
86
|
+
" try {",
|
|
87
|
+
` choice = window.localStorage.getItem(${scriptString(APPEARANCE_STORAGE_KEY)});`,
|
|
88
|
+
" } catch (error) {}",
|
|
89
|
+
` if (choice === "light" || choice === "dark") {`,
|
|
90
|
+
` var color = choice === "dark" ? ${scriptString(dark)} : ${scriptString(light)};`,
|
|
91
|
+
` html.setAttribute("data-cd-theme", choice);`,
|
|
92
|
+
` var scheme = document.querySelector("meta[name=color-scheme]");`,
|
|
93
|
+
` if (scheme) scheme.setAttribute("content", choice);`,
|
|
94
|
+
` var metas = document.querySelectorAll("meta[name=theme-color]");`,
|
|
95
|
+
` for (var index = 0; index < metas.length; index++) metas[index].setAttribute("content", color);`,
|
|
96
|
+
" }",
|
|
97
|
+
...contrast.map((line) => ` ${line}`),
|
|
98
|
+
"})();",
|
|
99
|
+
].join("\n");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// The script sits inline in <head>: "<" never appears in a literal, so a value cannot end the
|
|
103
|
+
// element ("</script>"), and U+2028/U+2029 are escaped because ES5 forbids them in string literals.
|
|
104
|
+
function scriptString(value: string): string {
|
|
105
|
+
return JSON.stringify(value)
|
|
106
|
+
.replace(/</g, "\\u003c")
|
|
107
|
+
.replace(/\u2028/g, "\\u2028")
|
|
108
|
+
.replace(/\u2029/g, "\\u2029");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The `<head>` markup the web app's `index.html` carries for its mode, as one string (for a Vite
|
|
113
|
+
* `transformIndexHtml` hook or to paste): dark-capable `<meta name="color-scheme" content="light
|
|
114
|
+
* dark">`, two `theme-color` metas with `media` and the script; light-only one `theme-color`
|
|
115
|
+
* without `media` and the contrast script. `<html>` itself carries `lang` and
|
|
116
|
+
* `data-cd-theme="<documentMode("system", themes, colors).theme>"` statically.
|
|
117
|
+
*/
|
|
118
|
+
export function webHeadMarkup(options: WebModeScriptOptions): string {
|
|
119
|
+
const mode = documentMode("system", options.themes, options.themeColors);
|
|
120
|
+
const script = `<script>\n${webModeScript(options)}\n</script>`;
|
|
121
|
+
if (mode.theme !== "system") {
|
|
122
|
+
return [`<meta name="theme-color" content="${escapeAttribute(mode.themeColor.light)}">`, script].join(
|
|
123
|
+
"\n",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return [
|
|
127
|
+
`<meta name="color-scheme" content="${escapeAttribute(mode.colorScheme)}">`,
|
|
128
|
+
`<meta name="theme-color" content="${escapeAttribute(mode.themeColor.light)}" media="(prefers-color-scheme: light)">`,
|
|
129
|
+
`<meta name="theme-color" content="${escapeAttribute(mode.themeColor.dark)}" media="(prefers-color-scheme: dark)">`,
|
|
130
|
+
script,
|
|
131
|
+
].join("\n");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function escapeAttribute(value: string): string {
|
|
135
|
+
return value
|
|
136
|
+
.replace(/&/g, "&")
|
|
137
|
+
.replace(/"/g, """)
|
|
138
|
+
.replace(/'/g, "'")
|
|
139
|
+
.replace(/</g, "<")
|
|
140
|
+
.replace(/>/g, ">");
|
|
141
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** The severity of one problem; the pill and the list show `circle-x` or `triangle-alert` (guide §5.10). */
|
|
2
|
+
export type ProblemSeverity = "error" | "warning";
|
|
3
|
+
|
|
4
|
+
/** One row of the web app's problems list (guide §5.10). */
|
|
5
|
+
export interface ProblemItem {
|
|
6
|
+
severity: ProblemSeverity;
|
|
7
|
+
/** 13px `--cd-ui-text`: "Unknown sticky type “evnt”." */
|
|
8
|
+
message: string;
|
|
9
|
+
/** The place in 13px `--cd-ui-text-leise` below the message: "Line 12", "“Order Placed”". */
|
|
10
|
+
location?: string | undefined;
|
|
11
|
+
/** The diagram element a click on the row selects (`ProblemsOptions.onSelectItem`). */
|
|
12
|
+
elementId?: string | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** `ShellHandle.setProblems` input built from the items: count, errors and the items themselves. */
|
|
16
|
+
export function problemSummary(items: readonly ProblemItem[]): {
|
|
17
|
+
count: number;
|
|
18
|
+
errors: number;
|
|
19
|
+
items: readonly ProblemItem[];
|
|
20
|
+
} {
|
|
21
|
+
const errors = items.filter((item) => item.severity === "error").length;
|
|
22
|
+
return { count: items.length, errors, items };
|
|
23
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { SHELL } from "../sizes.js";
|
|
2
|
+
|
|
3
|
+
/** The statuses of guide §3.3 and §9; each colours bar and dot through `--cd-ui-<status>`. */
|
|
4
|
+
export type ToastStatus = "success" | "info" | "warning" | "danger";
|
|
5
|
+
|
|
6
|
+
/** A ghost button before Close (guide §8); the toast then never closes by itself. */
|
|
7
|
+
export interface ToastAction {
|
|
8
|
+
label: string;
|
|
9
|
+
/** Runs after the toast closed. */
|
|
10
|
+
onSelect(): void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ToastRequest {
|
|
14
|
+
status: ToastStatus;
|
|
15
|
+
/** What happened and what you can do (guide §2): "Link copied", "Couldn't read this file. …". */
|
|
16
|
+
text: string;
|
|
17
|
+
action?: ToastAction | undefined;
|
|
18
|
+
/** Only to give a long text more time than `TOAST_DURATION_MS`; ignored with an `action`. */
|
|
19
|
+
durationMs?: number | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** At most three toasts at once, the newest at the bottom (guide §8). */
|
|
23
|
+
export const MAX_TOASTS = 3;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* How long a toast stays without hover or focus (guide §8: about 2.8 s, errors about 5 s). A
|
|
27
|
+
* warning asks you to read a sentence and act on it like an error, so it gets the error's 5 s.
|
|
28
|
+
*/
|
|
29
|
+
export const TOAST_DURATION_MS: Readonly<Record<ToastStatus, number>> = {
|
|
30
|
+
success: 2800,
|
|
31
|
+
info: 2800,
|
|
32
|
+
warning: 5000,
|
|
33
|
+
danger: 5000,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** The stack's widest toast: `min(480px, 100cqw − 24px)` (guide §8). */
|
|
37
|
+
export const TOAST_MAX_WIDTH = 480;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Milliseconds until the toast closes by itself: `undefined` with an `action` (it never closes by
|
|
41
|
+
* itself, guide §8), else `max(TOAST_DURATION_MS[status], durationMs ?? 0)`.
|
|
42
|
+
*/
|
|
43
|
+
export function toastLifetime(request: ToastRequest): number | undefined {
|
|
44
|
+
if (request.action) return undefined;
|
|
45
|
+
const requested = request.durationMs;
|
|
46
|
+
// A NaN or an infinite duration would reach setTimeout, which fires at once for both.
|
|
47
|
+
const longer = requested !== undefined && Number.isFinite(requested) ? requested : 0;
|
|
48
|
+
return Math.max(TOAST_DURATION_MS[request.status], longer);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A toast in the stack as the eviction sees it, oldest first. */
|
|
52
|
+
export interface ToastSlot {
|
|
53
|
+
id: number;
|
|
54
|
+
hasAction: boolean;
|
|
55
|
+
/** Hovered or holding the focus. */
|
|
56
|
+
engaged: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Which toast leaves when a fourth arrives (`MAX_TOASTS`): the oldest that is neither engaged nor
|
|
61
|
+
* has an action, else the oldest that is not engaged, else the oldest. `undefined` while the
|
|
62
|
+
* stack has room.
|
|
63
|
+
*/
|
|
64
|
+
export function toastToEvict(stack: readonly ToastSlot[]): number | undefined {
|
|
65
|
+
if (stack.length < MAX_TOASTS) return undefined;
|
|
66
|
+
const evicted =
|
|
67
|
+
stack.find((slot) => !slot.engaged && !slot.hasAction) ?? stack.find((slot) => !slot.engaged) ?? stack[0];
|
|
68
|
+
return evicted?.id;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Something in the bottom row the toast stack must clear, relative to the shell root. */
|
|
72
|
+
export interface BottomObstacle {
|
|
73
|
+
left: number;
|
|
74
|
+
right: number;
|
|
75
|
+
/** Distance of its top edge from the container's bottom edge. */
|
|
76
|
+
top: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ToastStackInput {
|
|
80
|
+
containerWidth: number;
|
|
81
|
+
/** The visible zoom panel, legal line and problems pill (not the legend card). */
|
|
82
|
+
obstacles: readonly BottomObstacle[];
|
|
83
|
+
/**
|
|
84
|
+
* Measured width of the widest toast in the stack. The cards are sized to their text and centred
|
|
85
|
+
* in the stack, so only this column can touch an obstacle; absent = the whole `toastStackWidth`.
|
|
86
|
+
*/
|
|
87
|
+
cardWidth?: number | undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** `min(TOAST_MAX_WIDTH, containerWidth − 2 · SHELL.edge)`. */
|
|
91
|
+
export function toastStackWidth(containerWidth: number): number {
|
|
92
|
+
return Math.max(0, Math.min(TOAST_MAX_WIDTH, containerWidth - 2 * SHELL.edge));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The stack's `bottom` in px. Guide §8 anchors it at `bottom: 12px`, centred; where the column of
|
|
97
|
+
* its widest card (`cardWidth`, at most `toastStackWidth`, centred) comes closer than `SHELL.gap`
|
|
98
|
+
* horizontally to an obstacle, the stack stands `SHELL.gap` above the highest such obstacle instead
|
|
99
|
+
* (above the legal line 40, above the zoom panel 60, above a raised zoom panel or the web pill 88).
|
|
100
|
+
* Guide §8 does not settle this collision of a narrow container; it is the shell's rule until it does.
|
|
101
|
+
*/
|
|
102
|
+
export function toastStackBottom(input: ToastStackInput): number {
|
|
103
|
+
const stackWidth = toastStackWidth(input.containerWidth);
|
|
104
|
+
const { cardWidth } = input;
|
|
105
|
+
const width =
|
|
106
|
+
cardWidth !== undefined && Number.isFinite(cardWidth)
|
|
107
|
+
? Math.min(stackWidth, Math.max(0, cardWidth))
|
|
108
|
+
: stackWidth;
|
|
109
|
+
const left = (input.containerWidth - width) / 2;
|
|
110
|
+
const right = left + width;
|
|
111
|
+
let highest: number | undefined;
|
|
112
|
+
for (const obstacle of input.obstacles) {
|
|
113
|
+
const distance = Math.max(obstacle.left - right, left - obstacle.right);
|
|
114
|
+
if (distance >= SHELL.gap) continue;
|
|
115
|
+
highest = highest === undefined ? obstacle.top : Math.max(highest, obstacle.top);
|
|
116
|
+
}
|
|
117
|
+
return highest === undefined ? SHELL.edge : Math.max(SHELL.edge, highest + SHELL.gap);
|
|
118
|
+
}
|
package/src/core/fit.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SHELL } from "./sizes.js";
|
|
2
|
+
import { LEGAL_LINE_HEIGHT, legalLineHeight } from "./web-chrome/bottom-row.js";
|
|
2
3
|
import type { Bounds, Insets, PaletteRow, Point, ScaledViewbox, ShellHost, Size } from "./types.js";
|
|
3
4
|
|
|
4
5
|
/** Fit never zooms above 100 % (guide §5.9). */
|
|
@@ -73,6 +74,17 @@ export interface FitLayout {
|
|
|
73
74
|
paletteRow: PaletteRow;
|
|
74
75
|
/** The problems pill is shown (only the web app reserves room for it). */
|
|
75
76
|
problemsVisible: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Web app: the zoom panel stands above the legal line (`bottom: 40px`) because the two would
|
|
79
|
+
* meet in the bottom row of a narrow container (`zoomRowRaised`); the bottom then keeps 92px as
|
|
80
|
+
* with the pill.
|
|
81
|
+
*/
|
|
82
|
+
zoomRaised?: boolean | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* Web app: the measured height of the legal line (`LEGAL_LINE_HEIGHT`, more where a narrow
|
|
85
|
+
* container wraps it); the raised bottom row stands on it. Default one line.
|
|
86
|
+
*/
|
|
87
|
+
legalHeight?: number | undefined;
|
|
76
88
|
/** Rendered width of the open legend; `undefined` when it is closed or the modeler has none. */
|
|
77
89
|
legendWidth?: number | undefined;
|
|
78
90
|
/**
|
|
@@ -83,16 +95,19 @@ export interface FitLayout {
|
|
|
83
95
|
}
|
|
84
96
|
|
|
85
97
|
/**
|
|
86
|
-
* The fit insets of guide §5.9: top 88 (second row 140), sides 24, bottom 64 (web app with pill
|
|
98
|
+
* The fit insets of guide §5.9: top 88 (second row 140), sides 24, bottom 64 (web app with pill or
|
|
99
|
+
* with the zoom panel raised above the legal line 92, plus 20 per extra line of a wrapped legal line),
|
|
87
100
|
* right 324 with an open side inspector, left 12 + legend width + 24 with an open legend above
|
|
88
101
|
* `SHELL.veryNarrow`, bottom = distance to the sheet's top edge + 12 with an open bottom sheet.
|
|
89
102
|
*/
|
|
90
103
|
export function fitInsets(layout: FitLayout): Insets {
|
|
91
|
-
const { container, host, paletteRow, problemsVisible, legendWidth, inspector } = layout;
|
|
104
|
+
const { container, host, paletteRow, problemsVisible, zoomRaised, legendWidth, inspector } = layout;
|
|
105
|
+
const raisedBottomRow = host === "web" && (problemsVisible || zoomRaised === true);
|
|
106
|
+
const wrappedLegalLines = legalLineHeight(layout.legalHeight) - LEGAL_LINE_HEIGHT;
|
|
92
107
|
const insets: Insets = {
|
|
93
108
|
top: paletteRow === "second-row" ? SHELL.fitInsetTopSecondRow : SHELL.fitInsetTop,
|
|
94
109
|
right: SHELL.fitInsetSide,
|
|
95
|
-
bottom:
|
|
110
|
+
bottom: raisedBottomRow ? SHELL.fitInsetBottomWeb + wrappedLegalLines : SHELL.fitInsetBottom,
|
|
96
111
|
left: SHELL.fitInsetSide,
|
|
97
112
|
};
|
|
98
113
|
|
package/src/core/index.ts
CHANGED
|
@@ -52,6 +52,7 @@ export {
|
|
|
52
52
|
TEXT_FIELD_SELECTOR,
|
|
53
53
|
ariaKeyShortcuts,
|
|
54
54
|
ariaKeyShortcutsFromLabel,
|
|
55
|
+
commandKeyLabel,
|
|
55
56
|
decideKey,
|
|
56
57
|
detectPlatform,
|
|
57
58
|
isUndoRedoKey,
|
|
@@ -82,3 +83,45 @@ export {
|
|
|
82
83
|
legendStartsOpen,
|
|
83
84
|
placeTooltip,
|
|
84
85
|
} from "./layout.js";
|
|
86
|
+
|
|
87
|
+
// ---- Web host (0.2) ----
|
|
88
|
+
|
|
89
|
+
export type { PrimaryWidths, TopRow, TopRowInput } from "./web-chrome/top-row.js";
|
|
90
|
+
export { decideTopRow } from "./web-chrome/top-row.js";
|
|
91
|
+
export type { BottomRowInput } from "./web-chrome/bottom-row.js";
|
|
92
|
+
export { LEGAL_LINE_HEIGHT, legalLineHeight, zoomRowRaised } from "./web-chrome/bottom-row.js";
|
|
93
|
+
export type { DragContext, DragItemLike, DragVerdict, DropForbiddenReason } from "./web-chrome/drop.js";
|
|
94
|
+
export { acceptsFile, dragVerdict } from "./web-chrome/drop.js";
|
|
95
|
+
|
|
96
|
+
export type {
|
|
97
|
+
BottomObstacle,
|
|
98
|
+
ToastAction,
|
|
99
|
+
ToastRequest,
|
|
100
|
+
ToastSlot,
|
|
101
|
+
ToastStackInput,
|
|
102
|
+
ToastStatus,
|
|
103
|
+
} from "./feedback/toasts.js";
|
|
104
|
+
export {
|
|
105
|
+
MAX_TOASTS,
|
|
106
|
+
TOAST_DURATION_MS,
|
|
107
|
+
TOAST_MAX_WIDTH,
|
|
108
|
+
toastLifetime,
|
|
109
|
+
toastStackBottom,
|
|
110
|
+
toastStackWidth,
|
|
111
|
+
toastToEvict,
|
|
112
|
+
} from "./feedback/toasts.js";
|
|
113
|
+
export type {
|
|
114
|
+
AppearanceChoice,
|
|
115
|
+
DocumentMode,
|
|
116
|
+
ThemeColors,
|
|
117
|
+
WebModeScriptOptions,
|
|
118
|
+
} from "./feedback/appearance.js";
|
|
119
|
+
export {
|
|
120
|
+
APPEARANCE_STORAGE_KEY,
|
|
121
|
+
documentMode,
|
|
122
|
+
parseAppearanceChoice,
|
|
123
|
+
webHeadMarkup,
|
|
124
|
+
webModeScript,
|
|
125
|
+
} from "./feedback/appearance.js";
|
|
126
|
+
export type { ProblemItem, ProblemSeverity } from "./feedback/problems.js";
|
|
127
|
+
export { problemSummary } from "./feedback/problems.js";
|
package/src/core/keymap.ts
CHANGED
|
@@ -74,6 +74,11 @@ export interface KeyContext {
|
|
|
74
74
|
modalOpen: boolean;
|
|
75
75
|
/** `event.defaultPrevented` when the listener ran: the canvas (or the modeler) handled the key. */
|
|
76
76
|
defaultPrevented: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* The web app's start screen shows (`setEmptyState(true)`): there is no diagram to zoom, so the
|
|
79
|
+
* zoom keys and Shift+1 are left to the browser, which zooms the page.
|
|
80
|
+
*/
|
|
81
|
+
startScreen?: boolean | undefined;
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
export type KeyAction = ChromeKey | "closeMenu";
|
|
@@ -91,6 +96,7 @@ export interface KeyDecision {
|
|
|
91
96
|
* or a dialog/popover is open. During IME composition Escape belongs to the IME: no `closeMenu`.
|
|
92
97
|
* 2. Zoom keys: executed and prevented unless composing, in a text field or with a modal dialog;
|
|
93
98
|
* in the IDE host always stopped, also when not executed (VS Code must never zoom the window).
|
|
99
|
+
* Behind the web app's start screen untouched: the browser zooms the page (WCAG 1.4.4).
|
|
94
100
|
* 3. Shift+1: `fit` under the same conditions; otherwise untouched (it types "!").
|
|
95
101
|
* 4. IDE host, text field, undo/redo key: stopped only (the field keeps its native undo).
|
|
96
102
|
* 5. IDE host, `defaultPrevented`: stopped (the canvas handled it; VS Code must not run it again).
|
|
@@ -109,6 +115,7 @@ export function decideKey(event: KeyLike, context: KeyContext): KeyDecision {
|
|
|
109
115
|
|
|
110
116
|
const chromeKey = matchChromeKey(event);
|
|
111
117
|
if (chromeKey !== undefined) {
|
|
118
|
+
if (context.startScreen === true && !stop) return { preventDefault: false, stopPropagation: false };
|
|
112
119
|
const blocked = composing || context.inTextField || context.modalOpen;
|
|
113
120
|
if (!blocked) return { action: chromeKey, preventDefault: true, stopPropagation: stop };
|
|
114
121
|
// Blocked zoom keys are still kept from VS Code (it would zoom the window); a blocked Shift+1
|
|
@@ -162,6 +169,19 @@ export function shortcutLabel(
|
|
|
162
169
|
}
|
|
163
170
|
}
|
|
164
171
|
|
|
172
|
+
/**
|
|
173
|
+
* The chip of a Cmd/Ctrl key a modeler binds itself (the web app's File entries, guide §5.4): macOS
|
|
174
|
+
* `⌘O`, elsewhere `Ctrl+O` with the key name of the messages table (`Strg+O`). `key` as printed on
|
|
175
|
+
* the key cap; pass the result to `FileMenuAction.shortcut` or `MenuActionEntry.shortcut`.
|
|
176
|
+
*/
|
|
177
|
+
export function commandKeyLabel(
|
|
178
|
+
key: string,
|
|
179
|
+
platform: Platform,
|
|
180
|
+
keyNames: Pick<ShellMessages["keyNames"], "ctrl">,
|
|
181
|
+
): string {
|
|
182
|
+
return platform === "mac" ? `⌘${key}` : `${keyNames.ctrl}+${key}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
165
185
|
const MAC_SHORTCUT_LABELS: Readonly<Record<ChromeKey, string>> = {
|
|
166
186
|
zoomIn: "⌘+",
|
|
167
187
|
zoomOut: "⌘\u2212",
|