@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
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
Bounds,
|
|
3
|
+
ChromeIsland,
|
|
4
|
+
Insets,
|
|
5
|
+
PaletteRow,
|
|
6
|
+
Point,
|
|
7
|
+
ScaledViewbox,
|
|
8
|
+
ShellHost,
|
|
9
|
+
Size,
|
|
10
|
+
ViewportState,
|
|
11
|
+
ZoomTarget,
|
|
12
|
+
} from "./types.js";
|
|
13
|
+
export { CHROME_ISLANDS } from "./types.js";
|
|
14
|
+
|
|
15
|
+
export type { ShellSizeName, ShellSizes } from "./sizes.js";
|
|
16
|
+
export { SHELL, SHELL_TOKENS, shellSizesFromTokens } from "./sizes.js";
|
|
17
|
+
|
|
18
|
+
export type { FitLayout } from "./fit.js";
|
|
19
|
+
export {
|
|
20
|
+
FIT_ANIMATION_MS,
|
|
21
|
+
MAX_FIT_ZOOM,
|
|
22
|
+
VIEW_TOLERANCE,
|
|
23
|
+
easeStandard,
|
|
24
|
+
fitInsets,
|
|
25
|
+
fitViewbox,
|
|
26
|
+
freeRectCenter,
|
|
27
|
+
interpolateViewbox,
|
|
28
|
+
sameView,
|
|
29
|
+
} from "./fit.js";
|
|
30
|
+
|
|
31
|
+
export type { ZoomReadout } from "./zoom.js";
|
|
32
|
+
export {
|
|
33
|
+
ZOOM_MAX,
|
|
34
|
+
ZOOM_MIN,
|
|
35
|
+
canZoomIn,
|
|
36
|
+
canZoomOut,
|
|
37
|
+
formatNumber,
|
|
38
|
+
formatPercent,
|
|
39
|
+
zoomReadout,
|
|
40
|
+
} from "./zoom.js";
|
|
41
|
+
|
|
42
|
+
export type {
|
|
43
|
+
ChromeKey,
|
|
44
|
+
KeyAction,
|
|
45
|
+
KeyContext,
|
|
46
|
+
KeyDecision,
|
|
47
|
+
KeyLike,
|
|
48
|
+
NavigatorLike,
|
|
49
|
+
Platform,
|
|
50
|
+
} from "./keymap.js";
|
|
51
|
+
export {
|
|
52
|
+
TEXT_FIELD_SELECTOR,
|
|
53
|
+
ariaKeyShortcuts,
|
|
54
|
+
ariaKeyShortcutsFromLabel,
|
|
55
|
+
commandKeyLabel,
|
|
56
|
+
decideKey,
|
|
57
|
+
detectPlatform,
|
|
58
|
+
isUndoRedoKey,
|
|
59
|
+
matchChromeKey,
|
|
60
|
+
shortcutLabel,
|
|
61
|
+
} from "./keymap.js";
|
|
62
|
+
|
|
63
|
+
export type { Appearance, AppearanceAttributes, ColorThemeSetting, Kontrast, Theme } from "./appearance.js";
|
|
64
|
+
export {
|
|
65
|
+
REDUCED_MOTION_CLASS,
|
|
66
|
+
appearanceAttributes,
|
|
67
|
+
appearanceFromBodyClasses,
|
|
68
|
+
appearanceFromColorThemeKind,
|
|
69
|
+
reducedMotionFromBodyClasses,
|
|
70
|
+
requestedAppearance,
|
|
71
|
+
resolveAppearance,
|
|
72
|
+
} from "./appearance.js";
|
|
73
|
+
|
|
74
|
+
export type { ShellMessageOverrides, ShellMessages } from "./messages.js";
|
|
75
|
+
export { defaultMessages, resolveMessages } from "./messages.js";
|
|
76
|
+
|
|
77
|
+
export type { PaletteRowInput, TooltipPlacementInput } from "./layout.js";
|
|
78
|
+
export {
|
|
79
|
+
INVALID_STATUS_DELAY_MS,
|
|
80
|
+
PALETTE_CLEARANCE,
|
|
81
|
+
TOOLTIP_DELAY_MS,
|
|
82
|
+
decidePaletteRow,
|
|
83
|
+
legendStartsOpen,
|
|
84
|
+
placeTooltip,
|
|
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";
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import type { ShellMessages } from "./messages.js";
|
|
2
|
+
import type { ShellHost } from "./types.js";
|
|
3
|
+
|
|
4
|
+
/** The only keys the chrome handles (guide §5.8). */
|
|
5
|
+
export type ChromeKey = "zoomIn" | "zoomOut" | "zoomToActual" | "fit";
|
|
6
|
+
|
|
7
|
+
/** The `KeyboardEvent` fields the keymap reads. */
|
|
8
|
+
export interface KeyLike {
|
|
9
|
+
key: string;
|
|
10
|
+
code: string;
|
|
11
|
+
metaKey: boolean;
|
|
12
|
+
ctrlKey: boolean;
|
|
13
|
+
shiftKey: boolean;
|
|
14
|
+
altKey: boolean;
|
|
15
|
+
isComposing?: boolean | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Which chrome key `event` is, ignoring where it was pressed:
|
|
20
|
+
* - `zoomIn`: Cmd/Ctrl + `+` or `=` (by `key`, with or without Shift), or `NumpadAdd` (by `code`);
|
|
21
|
+
* - `zoomOut`: Cmd/Ctrl + `-` or `_`, or `NumpadSubtract`;
|
|
22
|
+
* - `zoomToActual`: Cmd/Ctrl + `0`, or `Numpad0` (also with NumLock off);
|
|
23
|
+
* - `fit`: Shift + `Digit1` (by `code`, every layout) without Cmd, Ctrl or Alt.
|
|
24
|
+
* Cmd/Ctrl is `metaKey || ctrlKey` on every platform; nothing matches with `altKey` (AltGr).
|
|
25
|
+
*/
|
|
26
|
+
export function matchChromeKey(event: KeyLike): ChromeKey | undefined {
|
|
27
|
+
if (event.altKey) return undefined;
|
|
28
|
+
if (isCommand(event)) {
|
|
29
|
+
if (event.key === "+" || event.key === "=" || event.code === "NumpadAdd") return "zoomIn";
|
|
30
|
+
if (event.key === "-" || event.key === "_" || event.code === "NumpadSubtract") return "zoomOut";
|
|
31
|
+
if (event.key === "0" || event.code === "Numpad0") return "zoomToActual";
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return event.shiftKey && event.code === "Digit1" ? "fit" : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isCommand(event: KeyLike): boolean {
|
|
38
|
+
return event.metaKey || event.ctrlKey;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Cmd/Ctrl + Z, Cmd/Ctrl + Shift + Z or Cmd/Ctrl + Y, without Alt (by `key`, `code` as fallback).
|
|
43
|
+
* Cmd/Ctrl + Shift + Y is not redo (VS Code toggles its Debug Console with it).
|
|
44
|
+
*/
|
|
45
|
+
export function isUndoRedoKey(event: KeyLike): boolean {
|
|
46
|
+
if (event.altKey || !isCommand(event)) return false;
|
|
47
|
+
const letter = undoRedoLetter(event);
|
|
48
|
+
return letter === "z" || (letter === "y" && !event.shiftKey);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// The produced character decides on Latin layouts (QWERTZ swaps the Y and Z keys); only where the
|
|
52
|
+
// layout produces no ASCII character (Cyrillic, Greek, …) the physical key stands in.
|
|
53
|
+
function undoRedoLetter(event: KeyLike): string | undefined {
|
|
54
|
+
if (event.key.length === 1 && event.key.charCodeAt(0) < 0x80) return event.key.toLowerCase();
|
|
55
|
+
if (event.code === "KeyZ") return "z";
|
|
56
|
+
if (event.code === "KeyY") return "y";
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A text field: every input, select list and editable text (label editor, inspector, dialogs). */
|
|
61
|
+
export const TEXT_FIELD_SELECTOR =
|
|
62
|
+
"input, textarea, select, [contenteditable]:not([contenteditable='false'])";
|
|
63
|
+
|
|
64
|
+
/** Where a key was pressed, as the shell's one `keydown` listener sees it. */
|
|
65
|
+
export interface KeyContext {
|
|
66
|
+
host: ShellHost;
|
|
67
|
+
/** The event target matches `TEXT_FIELD_SELECTOR` or is `isContentEditable`. */
|
|
68
|
+
inTextField: boolean;
|
|
69
|
+
/** The shell's menu dropdown is open. */
|
|
70
|
+
menuOpen: boolean;
|
|
71
|
+
/** A popover or dialog (`[role="dialog"]`, modal or not) is open. */
|
|
72
|
+
dialogOpen: boolean;
|
|
73
|
+
/** A modal dialog (`[role="dialog"][aria-modal="true"]`) is open: chrome keys do not act. */
|
|
74
|
+
modalOpen: boolean;
|
|
75
|
+
/** `event.defaultPrevented` when the listener ran: the canvas (or the modeler) handled the key. */
|
|
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;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type KeyAction = ChromeKey | "closeMenu";
|
|
85
|
+
|
|
86
|
+
export interface KeyDecision {
|
|
87
|
+
/** What the shell executes; `undefined` = nothing. */
|
|
88
|
+
action?: KeyAction;
|
|
89
|
+
preventDefault: boolean;
|
|
90
|
+
stopPropagation: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The key rules of guide §5.8 and §13.5 in one place:
|
|
95
|
+
* 1. Escape with the menu open: `closeMenu`, prevented; in the IDE host stopped whenever the menu
|
|
96
|
+
* or a dialog/popover is open. During IME composition Escape belongs to the IME: no `closeMenu`.
|
|
97
|
+
* 2. Zoom keys: executed and prevented unless composing, in a text field or with a modal dialog;
|
|
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).
|
|
100
|
+
* 3. Shift+1: `fit` under the same conditions; otherwise untouched (it types "!").
|
|
101
|
+
* 4. IDE host, text field, undo/redo key: stopped only (the field keeps its native undo).
|
|
102
|
+
* 5. IDE host, `defaultPrevented`: stopped (the canvas handled it; VS Code must not run it again).
|
|
103
|
+
* Everything else passes untouched. `stopPropagation` is only ever true in the IDE host.
|
|
104
|
+
*/
|
|
105
|
+
export function decideKey(event: KeyLike, context: KeyContext): KeyDecision {
|
|
106
|
+
const stop = context.host === "ide";
|
|
107
|
+
const composing = event.isComposing === true;
|
|
108
|
+
|
|
109
|
+
if (event.key === "Escape" && !event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey) {
|
|
110
|
+
if (context.menuOpen && !composing) {
|
|
111
|
+
return { action: "closeMenu", preventDefault: true, stopPropagation: stop };
|
|
112
|
+
}
|
|
113
|
+
if (context.menuOpen || context.dialogOpen) return { preventDefault: false, stopPropagation: stop };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const chromeKey = matchChromeKey(event);
|
|
117
|
+
if (chromeKey !== undefined) {
|
|
118
|
+
if (context.startScreen === true && !stop) return { preventDefault: false, stopPropagation: false };
|
|
119
|
+
const blocked = composing || context.inTextField || context.modalOpen;
|
|
120
|
+
if (!blocked) return { action: chromeKey, preventDefault: true, stopPropagation: stop };
|
|
121
|
+
// Blocked zoom keys are still kept from VS Code (it would zoom the window); a blocked Shift+1
|
|
122
|
+
// is a character and travels on like any other.
|
|
123
|
+
return { preventDefault: false, stopPropagation: chromeKey === "fit" ? false : stop };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (context.inTextField && isUndoRedoKey(event)) return { preventDefault: false, stopPropagation: stop };
|
|
127
|
+
if (context.defaultPrevented) return { preventDefault: false, stopPropagation: stop };
|
|
128
|
+
return { preventDefault: false, stopPropagation: false };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The keyboard the user sits at; decides the shortcut labels (guide §5.8). */
|
|
132
|
+
export type Platform = "mac" | "other";
|
|
133
|
+
|
|
134
|
+
/** The `navigator` fields `detectPlatform` reads. */
|
|
135
|
+
export interface NavigatorLike {
|
|
136
|
+
platform?: string | undefined;
|
|
137
|
+
userAgentData?: { platform?: string | undefined } | undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* `mac` when `userAgentData.platform ?? platform` matches `/mac/i`, else `other`; `other` without a
|
|
142
|
+
* navigator. Read it in the webview or page itself, never from the extension host (VS Code Remote).
|
|
143
|
+
*/
|
|
144
|
+
export function detectPlatform(navigator?: NavigatorLike | undefined): Platform {
|
|
145
|
+
// An empty userAgentData.platform means "unknown", not "not a Mac": fall back to navigator.platform.
|
|
146
|
+
const platform = navigator?.userAgentData?.platform || navigator?.platform || "";
|
|
147
|
+
return /mac/i.test(platform) ? "mac" : "other";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The tooltip chip text: macOS `⌘+`, `⌘−` (U+2212), `⌘0`, `⇧1`; elsewhere `Ctrl+Plus`,
|
|
152
|
+
* `Ctrl+Minus`, `Ctrl+0`, `Shift+1` with the key names of the messages table.
|
|
153
|
+
*/
|
|
154
|
+
export function shortcutLabel(
|
|
155
|
+
key: ChromeKey,
|
|
156
|
+
platform: Platform,
|
|
157
|
+
keyNames: ShellMessages["keyNames"],
|
|
158
|
+
): string {
|
|
159
|
+
if (platform === "mac") return MAC_SHORTCUT_LABELS[key];
|
|
160
|
+
switch (key) {
|
|
161
|
+
case "zoomIn":
|
|
162
|
+
return `${keyNames.ctrl}+${keyNames.plus}`;
|
|
163
|
+
case "zoomOut":
|
|
164
|
+
return `${keyNames.ctrl}+${keyNames.minus}`;
|
|
165
|
+
case "zoomToActual":
|
|
166
|
+
return `${keyNames.ctrl}+0`;
|
|
167
|
+
case "fit":
|
|
168
|
+
return `${keyNames.shift}+1`;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
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
|
+
|
|
185
|
+
const MAC_SHORTCUT_LABELS: Readonly<Record<ChromeKey, string>> = {
|
|
186
|
+
zoomIn: "⌘+",
|
|
187
|
+
zoomOut: "⌘\u2212",
|
|
188
|
+
zoomToActual: "⌘0",
|
|
189
|
+
fit: "⇧1",
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The `aria-keyshortcuts` value (WAI-ARIA names): zoom in `Meta+= Meta+Plus`, zoom out `Meta+-`,
|
|
194
|
+
* 100 % `Meta+0`, fit `Shift+1`; outside macOS `Control` instead of `Meta`.
|
|
195
|
+
*/
|
|
196
|
+
export function ariaKeyShortcuts(key: ChromeKey, platform: Platform): string {
|
|
197
|
+
const command = platform === "mac" ? "Meta" : "Control";
|
|
198
|
+
switch (key) {
|
|
199
|
+
case "zoomIn":
|
|
200
|
+
return `${command}+= ${command}+Plus`;
|
|
201
|
+
case "zoomOut":
|
|
202
|
+
return `${command}+-`;
|
|
203
|
+
case "zoomToActual":
|
|
204
|
+
return `${command}+0`;
|
|
205
|
+
case "fit":
|
|
206
|
+
return "Shift+1";
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const MAC_MODIFIER_SYMBOLS: Readonly<Record<string, string>> = {
|
|
211
|
+
"⌘": "Meta",
|
|
212
|
+
"⇧": "Shift",
|
|
213
|
+
"⌥": "Alt",
|
|
214
|
+
"⌃": "Control",
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const MODIFIER_NAMES: Readonly<Record<string, string>> = {
|
|
218
|
+
cmd: "Meta",
|
|
219
|
+
command: "Meta",
|
|
220
|
+
meta: "Meta",
|
|
221
|
+
ctrl: "Control",
|
|
222
|
+
control: "Control",
|
|
223
|
+
strg: "Control",
|
|
224
|
+
shift: "Shift",
|
|
225
|
+
umschalt: "Shift",
|
|
226
|
+
alt: "Alt",
|
|
227
|
+
option: "Alt",
|
|
228
|
+
opt: "Alt",
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const NAMED_KEYS: Readonly<Record<string, string>> = {
|
|
232
|
+
esc: "Escape",
|
|
233
|
+
escape: "Escape",
|
|
234
|
+
del: "Delete",
|
|
235
|
+
delete: "Delete",
|
|
236
|
+
entf: "Delete",
|
|
237
|
+
backspace: "Backspace",
|
|
238
|
+
enter: "Enter",
|
|
239
|
+
return: "Enter",
|
|
240
|
+
tab: "Tab",
|
|
241
|
+
space: "Space",
|
|
242
|
+
home: "Home",
|
|
243
|
+
end: "End",
|
|
244
|
+
pageup: "PageUp",
|
|
245
|
+
pagedown: "PageDown",
|
|
246
|
+
arrowup: "ArrowUp",
|
|
247
|
+
arrowdown: "ArrowDown",
|
|
248
|
+
arrowleft: "ArrowLeft",
|
|
249
|
+
arrowright: "ArrowRight",
|
|
250
|
+
"↑": "ArrowUp",
|
|
251
|
+
"↓": "ArrowDown",
|
|
252
|
+
"←": "ArrowLeft",
|
|
253
|
+
"→": "ArrowRight",
|
|
254
|
+
plus: "Plus",
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
function ariaKeyName(key: string): string | undefined {
|
|
258
|
+
const named = NAMED_KEYS[key.toLowerCase()];
|
|
259
|
+
if (named) return named;
|
|
260
|
+
if (/^f\d{1,2}$/i.test(key)) return key.toUpperCase();
|
|
261
|
+
if (key.length !== 1 || MAC_MODIFIER_SYMBOLS[key]) return undefined;
|
|
262
|
+
if (key === "+") return "Plus";
|
|
263
|
+
if (key === " ") return "Space";
|
|
264
|
+
return key.toUpperCase();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* The `aria-keyshortcuts` value (WAI-ARIA names, guide §5.6) for a shortcut chip label: `"L"` →
|
|
269
|
+
* `"L"`, `"⇧L"` → `"Shift+L"`, `"⌘⇧Z"` → `"Meta+Shift+Z"`, `"Ctrl+Shift+E"` → `"Control+Shift+E"`,
|
|
270
|
+
* `"Del"` → `"Delete"`, `"Ctrl++"` → `"Control+Plus"`. `undefined` for a label that is not one key
|
|
271
|
+
* combination (`"G then D"`, `"1–9"`, empty).
|
|
272
|
+
*/
|
|
273
|
+
export function ariaKeyShortcutsFromLabel(label: string): string | undefined {
|
|
274
|
+
let rest = label.trim();
|
|
275
|
+
const modifiers: string[] = [];
|
|
276
|
+
for (let symbol = MAC_MODIFIER_SYMBOLS[rest.charAt(0)]; symbol && rest.length > 1;) {
|
|
277
|
+
modifiers.push(symbol);
|
|
278
|
+
rest = rest.slice(1).trim();
|
|
279
|
+
symbol = MAC_MODIFIER_SYMBOLS[rest.charAt(0)];
|
|
280
|
+
}
|
|
281
|
+
if (!rest || /\s/.test(rest.replace(/\s*\+\s*/g, "+"))) return undefined;
|
|
282
|
+
const parts = rest.split("+").map((part) => part.trim());
|
|
283
|
+
let key = parts.pop() ?? "";
|
|
284
|
+
if (key === "" && parts.at(-1) === "") {
|
|
285
|
+
parts.pop();
|
|
286
|
+
key = "+";
|
|
287
|
+
}
|
|
288
|
+
for (const part of parts) {
|
|
289
|
+
const modifier = MODIFIER_NAMES[part.toLowerCase()];
|
|
290
|
+
if (!modifier) return undefined;
|
|
291
|
+
modifiers.push(modifier);
|
|
292
|
+
}
|
|
293
|
+
const name = ariaKeyName(key);
|
|
294
|
+
return name ? [...new Set(modifiers), name].join("+") : undefined;
|
|
295
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { SHELL } from "./sizes.js";
|
|
2
|
+
import type { Bounds, PaletteRow, Point, Size } from "./types.js";
|
|
3
|
+
|
|
4
|
+
/** Free space the top row needs between the palette and a neighbouring island (guide §5.15). */
|
|
5
|
+
export const PALETTE_CLEARANCE = 16;
|
|
6
|
+
|
|
7
|
+
/** Hover delay of the chrome tooltip, equal to diagram-js `HoverTooltip` `SHOW_DELAY`. */
|
|
8
|
+
export const TOOLTIP_DELAY_MS = 300;
|
|
9
|
+
|
|
10
|
+
/** The status box for an invalid file shows after this long without a valid import (guide §5.12). */
|
|
11
|
+
export const INVALID_STATUS_DELAY_MS = 800;
|
|
12
|
+
|
|
13
|
+
export interface PaletteRowInput {
|
|
14
|
+
containerWidth: number;
|
|
15
|
+
/**
|
|
16
|
+
* The palette's intrinsic border-box width (`scrollWidth` plus its borders), so the decision does
|
|
17
|
+
* not flip once the second row clips it.
|
|
18
|
+
*/
|
|
19
|
+
paletteWidth: number;
|
|
20
|
+
/** Right edge of the menu trigger, relative to the shell root. */
|
|
21
|
+
menuRight: number;
|
|
22
|
+
/** Left edge of the primary action (web app), relative to the shell root; absent in IDE hosts. */
|
|
23
|
+
primaryLeft?: number | undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* `second-row` when the container is at most `SHELL.narrow` wide or a centred palette would keep
|
|
28
|
+
* less than `PALETTE_CLEARANCE` to the menu trigger (or the primary action); `top` otherwise.
|
|
29
|
+
*/
|
|
30
|
+
export function decidePaletteRow(input: PaletteRowInput): PaletteRow {
|
|
31
|
+
const { containerWidth, paletteWidth, menuRight, primaryLeft } = input;
|
|
32
|
+
if (containerWidth <= SHELL.narrow) return "second-row";
|
|
33
|
+
const paletteLeft = (containerWidth - paletteWidth) / 2;
|
|
34
|
+
const paletteRight = (containerWidth + paletteWidth) / 2;
|
|
35
|
+
if (paletteLeft < menuRight + PALETTE_CLEARANCE) return "second-row";
|
|
36
|
+
if (primaryLeft !== undefined && paletteRight > primaryLeft - PALETTE_CLEARANCE) return "second-row";
|
|
37
|
+
return "top";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The legend starts open unless the container is at most `SHELL.low` high (guide §5.10). */
|
|
41
|
+
export function legendStartsOpen(containerHeight: number): boolean {
|
|
42
|
+
return containerHeight > SHELL.low;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface TooltipPlacementInput {
|
|
46
|
+
/** The hovered or focused control, relative to the shell root (horizontal centre). */
|
|
47
|
+
anchor: Bounds;
|
|
48
|
+
/** The island the control sits in, relative to the shell root (vertical placement). */
|
|
49
|
+
island: Bounds;
|
|
50
|
+
tooltip: Size;
|
|
51
|
+
container: Size;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The tooltip's top-left corner relative to the shell root (guide §5.6): below an island in the
|
|
56
|
+
* upper half (`island.bottom + 8`: top row 60, second row 112), above one in the lower half
|
|
57
|
+
* (`island.top − 8 − height`: bottom row ends at `bottom: 60px`); centred on the anchor, shifted to
|
|
58
|
+
* keep `SHELL.edge` to the container's sides.
|
|
59
|
+
*/
|
|
60
|
+
export function placeTooltip(input: TooltipPlacementInput): Point {
|
|
61
|
+
const { anchor, island, tooltip, container } = input;
|
|
62
|
+
const inUpperHalf = island.y + island.height / 2 < container.height / 2;
|
|
63
|
+
const y = inUpperHalf ? island.y + island.height + SHELL.gap : island.y - SHELL.gap - tooltip.height;
|
|
64
|
+
const centred = anchor.x + anchor.width / 2 - tooltip.width / 2;
|
|
65
|
+
const rightmost = container.width - SHELL.edge - tooltip.width;
|
|
66
|
+
// In a container narrower than the tooltip plus both edges, the left edge wins.
|
|
67
|
+
const x = Math.max(SHELL.edge, Math.min(centred, rightmost));
|
|
68
|
+
return { x, y };
|
|
69
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { ProblemSeverity } from "./feedback/problems.js";
|
|
2
|
+
import type { DropForbiddenReason } from "./web-chrome/drop.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Every text the shell shows or announces, in one table (guide §2, shared terms). English by
|
|
6
|
+
* default; a localized modeler overrides the whole table in its language, numbers arrive already
|
|
7
|
+
* formatted through `Intl` in that language.
|
|
8
|
+
*/
|
|
9
|
+
export interface ShellMessages {
|
|
10
|
+
/** Menu trigger `aria-label` and tooltip, dropdown `aria-label`. */
|
|
11
|
+
menu: string;
|
|
12
|
+
/** Palette `aria-label`. */
|
|
13
|
+
tools: string;
|
|
14
|
+
/** `aria-label` of the focusable canvas `<svg>` (`role="application"`); a modeler names its notation. */
|
|
15
|
+
canvas: string;
|
|
16
|
+
/** Zoom panel `aria-label`. */
|
|
17
|
+
zoom: string;
|
|
18
|
+
zoomOut: string;
|
|
19
|
+
zoomIn: string;
|
|
20
|
+
/** Tooltip of the readout, e.g. `(percent) => "Zoom to " + percent` with percent "100%". */
|
|
21
|
+
zoomToActual(percent: string): string;
|
|
22
|
+
/** `aria-label` of the readout: "Zoom to 100% (now 86%)". */
|
|
23
|
+
zoomToActualNow(percent: string, now: string): string;
|
|
24
|
+
fit: string;
|
|
25
|
+
/** Legend title and the Model-group checkbox. */
|
|
26
|
+
legend: string;
|
|
27
|
+
closeLegend: string;
|
|
28
|
+
/** Pill text: "1 problem", "2 problems". */
|
|
29
|
+
problems(count: number, formattedCount: string): string;
|
|
30
|
+
/** Pill `aria-label`: "2 problems, 1 error", "2 problems, no errors". */
|
|
31
|
+
problemsLabel(count: number, errors: number, formattedCount: string, formattedErrors: string): string;
|
|
32
|
+
/** Pill tooltip in the IDE host. */
|
|
33
|
+
showProblems: string;
|
|
34
|
+
/** Empty-file hint (guide §13.5). */
|
|
35
|
+
emptyHint: string;
|
|
36
|
+
insertExample: string;
|
|
37
|
+
/** Status box for an invalid file; `diagram` names the notation ("Event Storming board"). */
|
|
38
|
+
invalidFile(diagram: string): string;
|
|
39
|
+
/** Key names in shortcut chips outside macOS (German: Strg, Umschalt, Plus, Minus). */
|
|
40
|
+
keyNames: { ctrl: string; shift: string; plus: string; minus: string };
|
|
41
|
+
|
|
42
|
+
// ---- Web host (0.2): guide §2 shared terms, §5.4, §5.7, §5.10, §8, §9 ----
|
|
43
|
+
|
|
44
|
+
/** Default label and `aria-label` of the primary action (web app, top right). */
|
|
45
|
+
share: string;
|
|
46
|
+
/** `aria-label` of the legal line's `<nav>`. "Impressum" and "Datenschutz" are never translated (guide §2). */
|
|
47
|
+
legal: string;
|
|
48
|
+
/** First entry of the legal line, linking the Miragon home page: "© 2026 Miragon GmbH". */
|
|
49
|
+
copyright(year: number): string;
|
|
50
|
+
/** The description of every legal link for screen readers: they open in a new tab. */
|
|
51
|
+
opensInNewTab: string;
|
|
52
|
+
/** File group (web app) and the empty state's primary button. */
|
|
53
|
+
newDiagram: string;
|
|
54
|
+
/** File group: "Open…" (U+2026, it opens the browser's file dialog). */
|
|
55
|
+
openFile: string;
|
|
56
|
+
/** File group and the empty state's ghost button. */
|
|
57
|
+
showExample: string;
|
|
58
|
+
/** File group: "Import…". */
|
|
59
|
+
importFile: string;
|
|
60
|
+
/** Heading and `aria-label` of the Appearance radio group (web app with dark only, guide §13.3). */
|
|
61
|
+
appearance: string;
|
|
62
|
+
appearanceSystem: string;
|
|
63
|
+
appearanceLight: string;
|
|
64
|
+
appearanceDark: string;
|
|
65
|
+
/** Title of the problems popover (web app). */
|
|
66
|
+
problemsTitle: string;
|
|
67
|
+
/** A row's severity for screen readers, the `aria-label` of its icon: "Error", "Warning". */
|
|
68
|
+
problemSeverity(severity: ProblemSeverity): string;
|
|
69
|
+
/** `aria-label` of a Close button (toasts). */
|
|
70
|
+
close: string;
|
|
71
|
+
/** Chip of the drop zone while a drag can be dropped (guide §9 Drag-over). */
|
|
72
|
+
dropFile: string;
|
|
73
|
+
/** Chip of the drop zone while the drag cannot be dropped (guide §9 Drop forbidden). */
|
|
74
|
+
dropForbidden(reason: DropForbiddenReason): string;
|
|
75
|
+
/** The empty state's drag-and-drop hint when the modeler has a drop zone. */
|
|
76
|
+
dropHint: string;
|
|
77
|
+
/** The text link after the empty state's hint that runs the File group's "Open…". */
|
|
78
|
+
openFileLink: string;
|
|
79
|
+
/**
|
|
80
|
+
* Danger toast for a dropped file outside `DropZoneOptions.accept`; `accepted` is the accept list
|
|
81
|
+
* joined by `Intl.ListFormat` (disjunction) in `lang`: ".storm, .json, or .txt".
|
|
82
|
+
*/
|
|
83
|
+
cannotOpenFile(name: string, accepted: string): string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Every key optional; an `undefined` value keeps the default (a conditional override). */
|
|
87
|
+
export type ShellMessageOverrides = {
|
|
88
|
+
[Key in Exclude<keyof ShellMessages, "keyNames">]?: ShellMessages[Key] | undefined;
|
|
89
|
+
} & {
|
|
90
|
+
keyNames?: { [Key in keyof ShellMessages["keyNames"]]?: string | undefined } | undefined;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const defaultMessages: ShellMessages = {
|
|
94
|
+
menu: "Menu",
|
|
95
|
+
tools: "Tools",
|
|
96
|
+
canvas: "Diagram",
|
|
97
|
+
zoom: "Zoom",
|
|
98
|
+
zoomOut: "Zoom out",
|
|
99
|
+
zoomIn: "Zoom in",
|
|
100
|
+
zoomToActual: (percent) => `Zoom to ${percent}`,
|
|
101
|
+
zoomToActualNow: (percent, now) => `Zoom to ${percent} (now ${now})`,
|
|
102
|
+
fit: "Fit to view",
|
|
103
|
+
legend: "Legend",
|
|
104
|
+
closeLegend: "Close legend",
|
|
105
|
+
problems: (count, formattedCount) => `${formattedCount} ${count === 1 ? "problem" : "problems"}`,
|
|
106
|
+
problemsLabel: (count, errors, formattedCount, formattedErrors) =>
|
|
107
|
+
`${formattedCount} ${count === 1 ? "problem" : "problems"}, ${
|
|
108
|
+
errors === 0 ? "no errors" : `${formattedErrors} ${errors === 1 ? "error" : "errors"}`
|
|
109
|
+
}`,
|
|
110
|
+
showProblems: "Show in Problems view",
|
|
111
|
+
emptyHint: "Pick a tool from the palette to start.",
|
|
112
|
+
insertExample: "Insert example",
|
|
113
|
+
invalidFile: (diagram) =>
|
|
114
|
+
`This file isn't a valid ${diagram} right now. The diagram shows the last valid version and is read-only until you fix the text.`,
|
|
115
|
+
keyNames: { ctrl: "Ctrl", shift: "Shift", plus: "Plus", minus: "Minus" },
|
|
116
|
+
share: "Share",
|
|
117
|
+
legal: "Legal",
|
|
118
|
+
copyright: (year) => `© ${year} Miragon GmbH`,
|
|
119
|
+
opensInNewTab: "Opens in a new tab",
|
|
120
|
+
newDiagram: "New diagram",
|
|
121
|
+
openFile: "Open…",
|
|
122
|
+
showExample: "Show example",
|
|
123
|
+
importFile: "Import…",
|
|
124
|
+
appearance: "Appearance",
|
|
125
|
+
appearanceSystem: "System",
|
|
126
|
+
appearanceLight: "Light",
|
|
127
|
+
appearanceDark: "Dark",
|
|
128
|
+
problemsTitle: "Problems",
|
|
129
|
+
problemSeverity: (severity) => (severity === "error" ? "Error" : "Warning"),
|
|
130
|
+
close: "Close",
|
|
131
|
+
dropFile: "Drop file here",
|
|
132
|
+
dropForbidden: (reason) =>
|
|
133
|
+
reason === "multiple"
|
|
134
|
+
? "Drop one file at a time"
|
|
135
|
+
: reason === "dialog"
|
|
136
|
+
? "Close the dialog to drop a file"
|
|
137
|
+
: "This modeler can't open this file type",
|
|
138
|
+
dropHint: "Or drop a file here",
|
|
139
|
+
openFileLink: "Open a file…",
|
|
140
|
+
cannotOpenFile: (name, accepted) => `Couldn't open “${name}”. Drop a ${accepted} file.`,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/** `defaultMessages` with `overrides` applied (`keyNames` merged key by key). */
|
|
144
|
+
export function resolveMessages(overrides?: ShellMessageOverrides | undefined): ShellMessages {
|
|
145
|
+
const { keyNames, ...texts } = overrides ?? {};
|
|
146
|
+
return {
|
|
147
|
+
...defaultMessages,
|
|
148
|
+
...definedEntries(texts),
|
|
149
|
+
keyNames: { ...defaultMessages.keyNames, ...definedEntries(keyNames ?? {}) },
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// `{ menu: undefined }` (a spread of optional fields, a conditional override) keeps the default
|
|
154
|
+
// instead of blanking the text.
|
|
155
|
+
function definedEntries<T extends object>(partial: T): { [Key in keyof T]?: Exclude<T[Key], undefined> } {
|
|
156
|
+
return Object.fromEntries(Object.entries(partial).filter(([, value]) => value !== undefined)) as {
|
|
157
|
+
[Key in keyof T]?: Exclude<T[Key], undefined>;
|
|
158
|
+
};
|
|
159
|
+
}
|