@miragon/modeler-shell 0.0.0-stage → 0.1.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 +1902 -0
- package/LICENSE +21 -0
- package/README.md +435 -2
- package/THIRD_PARTY_NOTICES.md +52 -0
- package/dist/appearance-D3FUcbu6.d.cts +53 -0
- package/dist/appearance-D3FUcbu6.d.ts +53 -0
- package/dist/chunk-5JSZ4VGA.js +551 -0
- package/dist/chunk-5JSZ4VGA.js.map +1 -0
- package/dist/chunk-6LQOFACK.js +1576 -0
- package/dist/chunk-6LQOFACK.js.map +1 -0
- package/dist/chunk-N4QXFF3A.js +23 -0
- package/dist/chunk-N4QXFF3A.js.map +1 -0
- package/dist/chunk-RJRFR6N3.js +136 -0
- package/dist/chunk-RJRFR6N3.js.map +1 -0
- package/dist/core.cjs +619 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.d.cts +171 -0
- package/dist/core.d.ts +171 -0
- package/dist/core.js +91 -0
- package/dist/core.js.map +1 -0
- package/dist/diagram-js.cjs +1115 -0
- package/dist/diagram-js.cjs.map +1 -0
- package/dist/diagram-js.d.cts +163 -0
- package/dist/diagram-js.d.ts +163 -0
- package/dist/diagram-js.js +867 -0
- package/dist/diagram-js.js.map +1 -0
- package/dist/index.cjs +2175 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +20 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +103 -0
- package/dist/index.js.map +1 -0
- package/dist/keymap-CObQE6q6.d.cts +151 -0
- package/dist/keymap-DORzHnK-.d.ts +151 -0
- package/dist/protocol-DhYBakWL.d.cts +15 -0
- package/dist/protocol-IeW8bf4f.d.ts +15 -0
- package/dist/shell.css +914 -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 +2364 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +165 -0
- package/dist/testing.d.ts +165 -0
- package/dist/testing.js +2297 -0
- package/dist/testing.js.map +1 -0
- package/dist/types-B6NfkiUG.d.cts +66 -0
- package/dist/types-B6NfkiUG.d.ts +66 -0
- package/dist/types-DnZn9CPw.d.ts +284 -0
- package/dist/types-OuFlqZp0.d.cts +284 -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 +2198 -0
- package/dist/vscode-webview.cjs.map +1 -0
- package/dist/vscode-webview.d.cts +82 -0
- package/dist/vscode-webview.d.ts +82 -0
- package/dist/vscode-webview.js +250 -0
- package/dist/vscode-webview.js.map +1 -0
- package/package.json +145 -4
- package/src/core/appearance.ts +104 -0
- package/src/core/fit.ts +205 -0
- package/src/core/index.ts +84 -0
- package/src/core/keymap.ts +275 -0
- package/src/core/layout.ts +69 -0
- package/src/core/messages.ts +88 -0
- package/src/core/sizes.ts +85 -0
- package/src/core/types.ts +84 -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 +240 -0
- package/src/diagram-js/types.ts +67 -0
- package/src/diagram-js/viewport.ts +564 -0
- package/src/diagram-js.ts +2 -0
- package/src/dom/element.ts +51 -0
- package/src/dom/hint.ts +51 -0
- package/src/dom/icons.ts +309 -0
- package/src/dom/index.ts +29 -0
- package/src/dom/inspector.ts +35 -0
- package/src/dom/keys.ts +59 -0
- package/src/dom/ledger.ts +87 -0
- package/src/dom/legend.ts +85 -0
- package/src/dom/menu.ts +393 -0
- package/src/dom/mount.ts +461 -0
- package/src/dom/problems.ts +72 -0
- package/src/dom/shell-dom.css +966 -0
- package/src/dom/status.ts +72 -0
- package/src/dom/tooltip.ts +237 -0
- package/src/dom/types.ts +277 -0
- package/src/dom/zoom-panel.ts +154 -0
- package/src/index.ts +3 -0
- package/src/shell.css +6 -0
- package/src/testing/cases.ts +1012 -0
- package/src/testing/checks.ts +832 -0
- package/src/testing/facts.ts +587 -0
- package/src/testing/geometry.ts +472 -0
- package/src/testing/harness.ts +138 -0
- package/src/testing/index.ts +15 -0
- package/src/testing/page-tools.ts +434 -0
- package/src/testing/page.ts +17 -0
- package/src/testing/vscode-stub.ts +237 -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 +76 -0
- package/src/vscode/webview/state.ts +116 -0
- package/src/vscode-extension.ts +2 -0
- package/src/vscode-webview.ts +2 -0
package/src/dom/icons.ts
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/*! @miragon/modeler-shell icon geometry
|
|
2
|
+
* Lucide (lucide-static 1.59.0) @license ISC
|
|
3
|
+
* Copyright (c) 2026 Lucide Icons and Contributors
|
|
4
|
+
* Derived from Feather, as Lucide's LICENSE lists them: check, circle-x (x-circle), maximize,
|
|
5
|
+
* minus, plus, triangle-alert (alert-triangle) and x. Feather @license MIT
|
|
6
|
+
* Copyright (c) 2013-present Cole Bemis
|
|
7
|
+
* Full notices: THIRD_PARTY_NOTICES.md of @miragon/modeler-shell */
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* The common Lucide set of the chrome (guide §5.14), inlined so the package has no icon runtime
|
|
11
|
+
* dependency, copied unchanged from `icon-nodes.json`. One exception: `line-segment` (Draw) is
|
|
12
|
+
* Lucide's `spline` with a straight segment between its end points, because the curve read as a
|
|
13
|
+
* telephone receiver at 20px. The comment above is a legal comment, so
|
|
14
|
+
* esbuild keeps it in the bundle chunk that carries this geometry.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** One Lucide icon: its SVG child elements on the 24 × 24 grid. */
|
|
18
|
+
export type IconNode = ReadonlyArray<readonly [tag: string, attributes: Readonly<Record<string, string>>]>;
|
|
19
|
+
|
|
20
|
+
export type ShellIconName =
|
|
21
|
+
| "menu"
|
|
22
|
+
| "minus"
|
|
23
|
+
| "plus"
|
|
24
|
+
| "maximize"
|
|
25
|
+
| "check"
|
|
26
|
+
| "x"
|
|
27
|
+
| "file-plus"
|
|
28
|
+
| "folder-open"
|
|
29
|
+
| "import"
|
|
30
|
+
| "file-image"
|
|
31
|
+
| "file-code-corner"
|
|
32
|
+
| "keyboard"
|
|
33
|
+
| "share-2"
|
|
34
|
+
| "circle-x"
|
|
35
|
+
| "triangle-alert"
|
|
36
|
+
| "square-dashed-mouse-pointer"
|
|
37
|
+
| "hand"
|
|
38
|
+
| "line-segment"
|
|
39
|
+
| "unfold-horizontal"
|
|
40
|
+
| "workflow";
|
|
41
|
+
|
|
42
|
+
export const ICONS: Readonly<Record<ShellIconName, IconNode>> = {
|
|
43
|
+
menu: [
|
|
44
|
+
["path", { d: "M4 5h16" }],
|
|
45
|
+
["path", { d: "M4 12h16" }],
|
|
46
|
+
["path", { d: "M4 19h16" }],
|
|
47
|
+
],
|
|
48
|
+
minus: [["path", { d: "M5 12h14" }]],
|
|
49
|
+
plus: [
|
|
50
|
+
["path", { d: "M5 12h14" }],
|
|
51
|
+
["path", { d: "M12 5v14" }],
|
|
52
|
+
],
|
|
53
|
+
maximize: [
|
|
54
|
+
["path", { d: "M8 3H5a2 2 0 0 0-2 2v3" }],
|
|
55
|
+
["path", { d: "M21 8V5a2 2 0 0 0-2-2h-3" }],
|
|
56
|
+
["path", { d: "M3 16v3a2 2 0 0 0 2 2h3" }],
|
|
57
|
+
["path", { d: "M16 21h3a2 2 0 0 0 2-2v-3" }],
|
|
58
|
+
],
|
|
59
|
+
check: [["path", { d: "M20 6 9 17l-5-5" }]],
|
|
60
|
+
x: [
|
|
61
|
+
["path", { d: "M18 6 6 18" }],
|
|
62
|
+
["path", { d: "m6 6 12 12" }],
|
|
63
|
+
],
|
|
64
|
+
"file-plus": [
|
|
65
|
+
[
|
|
66
|
+
"path",
|
|
67
|
+
{
|
|
68
|
+
d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z",
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
["path", { d: "M14 2v5a1 1 0 0 0 1 1h5" }],
|
|
72
|
+
["path", { d: "M9 15h6" }],
|
|
73
|
+
["path", { d: "M12 18v-6" }],
|
|
74
|
+
],
|
|
75
|
+
"folder-open": [
|
|
76
|
+
[
|
|
77
|
+
"path",
|
|
78
|
+
{
|
|
79
|
+
d: "m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2",
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
],
|
|
83
|
+
import: [
|
|
84
|
+
["path", { d: "M12 3v12" }],
|
|
85
|
+
["path", { d: "m8 11 4 4 4-4" }],
|
|
86
|
+
["path", { d: "M8 5H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-4" }],
|
|
87
|
+
],
|
|
88
|
+
"file-image": [
|
|
89
|
+
[
|
|
90
|
+
"path",
|
|
91
|
+
{
|
|
92
|
+
d: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z",
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
["path", { d: "M14 2v5a1 1 0 0 0 1 1h5" }],
|
|
96
|
+
["circle", { cx: "10", cy: "12", r: "2" }],
|
|
97
|
+
["path", { d: "m20 17-1.296-1.296a2.41 2.41 0 0 0-3.408 0L9 22" }],
|
|
98
|
+
],
|
|
99
|
+
"file-code-corner": [
|
|
100
|
+
[
|
|
101
|
+
"path",
|
|
102
|
+
{
|
|
103
|
+
d: "M4 12.15V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2h-3.35",
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
["path", { d: "M14 2v5a1 1 0 0 0 1 1h5" }],
|
|
107
|
+
["path", { d: "m5 16-3 3 3 3" }],
|
|
108
|
+
["path", { d: "m9 22 3-3-3-3" }],
|
|
109
|
+
],
|
|
110
|
+
keyboard: [
|
|
111
|
+
["path", { d: "M10 8h.01" }],
|
|
112
|
+
["path", { d: "M12 12h.01" }],
|
|
113
|
+
["path", { d: "M14 8h.01" }],
|
|
114
|
+
["path", { d: "M16 12h.01" }],
|
|
115
|
+
["path", { d: "M18 8h.01" }],
|
|
116
|
+
["path", { d: "M6 8h.01" }],
|
|
117
|
+
["path", { d: "M7 16h10" }],
|
|
118
|
+
["path", { d: "M8 12h.01" }],
|
|
119
|
+
["rect", { width: "20", height: "16", x: "2", y: "4", rx: "2" }],
|
|
120
|
+
],
|
|
121
|
+
"share-2": [
|
|
122
|
+
["circle", { cx: "18", cy: "5", r: "3" }],
|
|
123
|
+
["circle", { cx: "6", cy: "12", r: "3" }],
|
|
124
|
+
["circle", { cx: "18", cy: "19", r: "3" }],
|
|
125
|
+
["line", { x1: "8.59", x2: "15.42", y1: "13.51", y2: "17.49" }],
|
|
126
|
+
["line", { x1: "15.41", x2: "8.59", y1: "6.51", y2: "10.49" }],
|
|
127
|
+
],
|
|
128
|
+
"circle-x": [
|
|
129
|
+
["circle", { cx: "12", cy: "12", r: "10" }],
|
|
130
|
+
["path", { d: "m15 9-6 6" }],
|
|
131
|
+
["path", { d: "m9 9 6 6" }],
|
|
132
|
+
],
|
|
133
|
+
"triangle-alert": [
|
|
134
|
+
["path", { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" }],
|
|
135
|
+
["path", { d: "M12 9v4" }],
|
|
136
|
+
["path", { d: "M12 17h.01" }],
|
|
137
|
+
],
|
|
138
|
+
"square-dashed-mouse-pointer": [
|
|
139
|
+
[
|
|
140
|
+
"path",
|
|
141
|
+
{
|
|
142
|
+
d: "M12.034 12.681a.498.498 0 0 1 .647-.647l9 3.5a.5.5 0 0 1-.033.943l-3.444 1.068a1 1 0 0 0-.66.66l-1.067 3.443a.5.5 0 0 1-.943.033z",
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
["path", { d: "M5 3a2 2 0 0 0-2 2" }],
|
|
146
|
+
["path", { d: "M19 3a2 2 0 0 1 2 2" }],
|
|
147
|
+
["path", { d: "M5 21a2 2 0 0 1-2-2" }],
|
|
148
|
+
["path", { d: "M9 3h1" }],
|
|
149
|
+
["path", { d: "M9 21h2" }],
|
|
150
|
+
["path", { d: "M14 3h1" }],
|
|
151
|
+
["path", { d: "M3 9v1" }],
|
|
152
|
+
["path", { d: "M21 9v2" }],
|
|
153
|
+
["path", { d: "M3 14v1" }],
|
|
154
|
+
],
|
|
155
|
+
hand: [
|
|
156
|
+
["path", { d: "M18 11V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2" }],
|
|
157
|
+
["path", { d: "M14 10V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2v2" }],
|
|
158
|
+
["path", { d: "M10 10.5V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2v8" }],
|
|
159
|
+
[
|
|
160
|
+
"path",
|
|
161
|
+
{
|
|
162
|
+
d: "M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15",
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
],
|
|
166
|
+
"line-segment": [
|
|
167
|
+
["circle", { cx: "19", cy: "5", r: "2" }],
|
|
168
|
+
["circle", { cx: "5", cy: "19", r: "2" }],
|
|
169
|
+
["path", { d: "M6.5 17.5 17.5 6.5" }],
|
|
170
|
+
],
|
|
171
|
+
"unfold-horizontal": [
|
|
172
|
+
["path", { d: "M16 12h6" }],
|
|
173
|
+
["path", { d: "M8 12H2" }],
|
|
174
|
+
["path", { d: "M12 2v2" }],
|
|
175
|
+
["path", { d: "M12 8v2" }],
|
|
176
|
+
["path", { d: "M12 14v2" }],
|
|
177
|
+
["path", { d: "M12 20v2" }],
|
|
178
|
+
["path", { d: "m19 15 3-3-3-3" }],
|
|
179
|
+
["path", { d: "m5 9-3 3 3 3" }],
|
|
180
|
+
],
|
|
181
|
+
workflow: [
|
|
182
|
+
["rect", { width: "8", height: "8", x: "3", y: "3", rx: "2" }],
|
|
183
|
+
["path", { d: "M7 11v4a2 2 0 0 0 2 2h4" }],
|
|
184
|
+
["rect", { width: "8", height: "8", x: "13", y: "13", rx: "2" }],
|
|
185
|
+
],
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export interface IconOptions {
|
|
189
|
+
/** Rendered width and height in px; default 16 (chrome). */
|
|
190
|
+
size?: number;
|
|
191
|
+
/** Stroke width on the 24 grid; default 2.5. */
|
|
192
|
+
strokeWidth?: number;
|
|
193
|
+
className?: string;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Palette tool icons: 20px with stroke 2 (guide §5.5), as heavy as the 16px chrome icons. */
|
|
197
|
+
export const PALETTE_TOOL_ICON: Readonly<IconOptions> = { size: 20, strokeWidth: 2 };
|
|
198
|
+
|
|
199
|
+
const DEFAULT_SIZE = 16;
|
|
200
|
+
const DEFAULT_STROKE_WIDTH = 2.5;
|
|
201
|
+
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
202
|
+
|
|
203
|
+
// Everything Lucide icon nodes are made of. Markup goes into innerHTML (palette entry `html`), so
|
|
204
|
+
// any other element or attribute (a <script>, an onload, an href) is dropped.
|
|
205
|
+
const ICON_ELEMENTS: ReadonlySet<string> = new Set([
|
|
206
|
+
"path",
|
|
207
|
+
"circle",
|
|
208
|
+
"ellipse",
|
|
209
|
+
"line",
|
|
210
|
+
"polyline",
|
|
211
|
+
"polygon",
|
|
212
|
+
"rect",
|
|
213
|
+
]);
|
|
214
|
+
const GEOMETRY_ATTRIBUTES: ReadonlySet<string> = new Set([
|
|
215
|
+
"d",
|
|
216
|
+
"cx",
|
|
217
|
+
"cy",
|
|
218
|
+
"r",
|
|
219
|
+
"rx",
|
|
220
|
+
"ry",
|
|
221
|
+
"x",
|
|
222
|
+
"y",
|
|
223
|
+
"x1",
|
|
224
|
+
"y1",
|
|
225
|
+
"x2",
|
|
226
|
+
"y2",
|
|
227
|
+
"width",
|
|
228
|
+
"height",
|
|
229
|
+
"points",
|
|
230
|
+
]);
|
|
231
|
+
|
|
232
|
+
interface SanitizedIcon {
|
|
233
|
+
root: [name: string, value: string][];
|
|
234
|
+
children: [tag: string, attributes: [name: string, value: string][]][];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function positiveOr(value: number | undefined, fallback: number): number {
|
|
238
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function iconNodeOf(icon: ShellIconName | IconNode): IconNode {
|
|
242
|
+
if (typeof icon !== "string") return icon;
|
|
243
|
+
return Object.prototype.hasOwnProperty.call(ICONS, icon) ? ICONS[icon] : [];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function sanitize(icon: ShellIconName | IconNode, options: IconOptions = {}): SanitizedIcon {
|
|
247
|
+
const size = String(positiveOr(options.size, DEFAULT_SIZE));
|
|
248
|
+
const root: SanitizedIcon["root"] = [];
|
|
249
|
+
if (options.className) root.push(["class", options.className]);
|
|
250
|
+
root.push(
|
|
251
|
+
["width", size],
|
|
252
|
+
["height", size],
|
|
253
|
+
["viewBox", "0 0 24 24"],
|
|
254
|
+
["fill", "none"],
|
|
255
|
+
["stroke", "currentColor"],
|
|
256
|
+
["stroke-width", String(positiveOr(options.strokeWidth, DEFAULT_STROKE_WIDTH))],
|
|
257
|
+
["stroke-linecap", "round"],
|
|
258
|
+
["stroke-linejoin", "round"],
|
|
259
|
+
["aria-hidden", "true"],
|
|
260
|
+
["focusable", "false"],
|
|
261
|
+
);
|
|
262
|
+
const children: SanitizedIcon["children"] = [];
|
|
263
|
+
for (const node of iconNodeOf(icon)) {
|
|
264
|
+
const [tag, attributes] = node;
|
|
265
|
+
if (!ICON_ELEMENTS.has(tag)) continue;
|
|
266
|
+
const kept = Object.entries(attributes)
|
|
267
|
+
.filter(([name]) => GEOMETRY_ATTRIBUTES.has(name))
|
|
268
|
+
.map(([name, value]): [string, string] => [name, String(value)]);
|
|
269
|
+
children.push([tag, kept]);
|
|
270
|
+
}
|
|
271
|
+
return { root, children };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function escapeAttribute(value: string): string {
|
|
275
|
+
return value
|
|
276
|
+
.replace(/&/g, "&")
|
|
277
|
+
.replace(/"/g, """)
|
|
278
|
+
.replace(/'/g, "'")
|
|
279
|
+
.replace(/</g, "<")
|
|
280
|
+
.replace(/>/g, ">");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function attributeList(attributes: readonly [string, string][]): string {
|
|
284
|
+
return attributes.map(([name, value]) => ` ${name}="${escapeAttribute(value)}"`).join("");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Decorative inline SVG markup (`aria-hidden="true"`, `focusable="false"`, `fill="none"`,
|
|
289
|
+
* `stroke="currentColor"`, round caps and joins). Only Lucide's elements and geometry attributes
|
|
290
|
+
* pass, so the result is safe for `innerHTML` (palette entry `html`).
|
|
291
|
+
*/
|
|
292
|
+
export function iconMarkup(icon: ShellIconName | IconNode, options?: IconOptions): string {
|
|
293
|
+
const { root, children } = sanitize(icon, options);
|
|
294
|
+
const content = children.map(([tag, attributes]) => `<${tag}${attributeList(attributes)}/>`).join("");
|
|
295
|
+
return `<svg xmlns="${SVG_NAMESPACE}"${attributeList(root)}>${content}</svg>`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** The same icon as an `SVGSVGElement`. */
|
|
299
|
+
export function iconElement(icon: ShellIconName | IconNode, options?: IconOptions): SVGSVGElement {
|
|
300
|
+
const { root, children } = sanitize(icon, options);
|
|
301
|
+
const svg = document.createElementNS(SVG_NAMESPACE, "svg");
|
|
302
|
+
for (const [name, value] of root) svg.setAttribute(name, value);
|
|
303
|
+
for (const [tag, attributes] of children) {
|
|
304
|
+
const child = document.createElementNS(SVG_NAMESPACE, tag);
|
|
305
|
+
for (const [name, value] of attributes) child.setAttribute(name, value);
|
|
306
|
+
svg.append(child);
|
|
307
|
+
}
|
|
308
|
+
return svg;
|
|
309
|
+
}
|
package/src/dom/index.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
EmptyHintOptions,
|
|
3
|
+
InspectorHandle,
|
|
4
|
+
InspectorOptions,
|
|
5
|
+
LegendHandle,
|
|
6
|
+
LegendOptions,
|
|
7
|
+
MenuActionEntry,
|
|
8
|
+
MenuCheckboxEntry,
|
|
9
|
+
MenuDialogEntry,
|
|
10
|
+
MenuEntry,
|
|
11
|
+
MenuGroups,
|
|
12
|
+
MenuHandle,
|
|
13
|
+
MenuRadioGroup,
|
|
14
|
+
MenuRadioOption,
|
|
15
|
+
PaletteReadOnlyState,
|
|
16
|
+
PaletteRegistration,
|
|
17
|
+
ProblemSummary,
|
|
18
|
+
ProblemsOptions,
|
|
19
|
+
ShellHandle,
|
|
20
|
+
ShellOptions,
|
|
21
|
+
StatusMessage,
|
|
22
|
+
StatusSeverity,
|
|
23
|
+
TooltipContent,
|
|
24
|
+
TooltipHandle,
|
|
25
|
+
} from "./types.js";
|
|
26
|
+
export { mountShell } from "./mount.js";
|
|
27
|
+
|
|
28
|
+
export type { IconNode, IconOptions, ShellIconName } from "./icons.js";
|
|
29
|
+
export { ICONS, PALETTE_TOOL_ICON, iconElement, iconMarkup } from "./icons.js";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createElement } from "./element.js";
|
|
2
|
+
import type { InspectorHandle, InspectorOptions } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export interface InspectorController extends InspectorHandle {
|
|
5
|
+
destroy(): void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function createInspector(
|
|
9
|
+
ownerDocument: Document,
|
|
10
|
+
options: InspectorOptions,
|
|
11
|
+
onChange: (open: boolean) => void,
|
|
12
|
+
): InspectorController {
|
|
13
|
+
const element = createElement(ownerDocument, "aside", {
|
|
14
|
+
class: "cd-shell-inspector",
|
|
15
|
+
"data-chrome": "inspector",
|
|
16
|
+
"aria-label": options.label,
|
|
17
|
+
});
|
|
18
|
+
element.hidden = true;
|
|
19
|
+
|
|
20
|
+
function setOpen(open: boolean): void {
|
|
21
|
+
if (open === !element.hidden) return;
|
|
22
|
+
element.hidden = !open;
|
|
23
|
+
onChange(open);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
element,
|
|
28
|
+
isOpen: () => !element.hidden,
|
|
29
|
+
open: () => setOpen(true),
|
|
30
|
+
close: () => setOpen(false),
|
|
31
|
+
destroy() {
|
|
32
|
+
element.remove();
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
package/src/dom/keys.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { TEXT_FIELD_SELECTOR, decideKey, type ChromeKey, type ShellHost } from "../core/index.js";
|
|
2
|
+
|
|
3
|
+
export interface KeyListenerContext {
|
|
4
|
+
host: ShellHost;
|
|
5
|
+
/** Where dialogs and popovers count: the document in an own document, else the shell root. */
|
|
6
|
+
dialogScope: ParentNode;
|
|
7
|
+
menuOpen(): boolean;
|
|
8
|
+
closeMenu(): void;
|
|
9
|
+
hideTooltip(): void;
|
|
10
|
+
run(key: ChromeKey): void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isTextField(target: EventTarget | null): boolean {
|
|
14
|
+
if (!target || typeof (target as Element).closest !== "function") return false;
|
|
15
|
+
const element = target as HTMLElement;
|
|
16
|
+
return element.isContentEditable === true || element.closest(TEXT_FIELD_SELECTOR) !== null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// A dialog a modeler hides through CSS (a class with display: none) is closed as well.
|
|
20
|
+
function isShown(element: Element): boolean {
|
|
21
|
+
if (element.localName === "dialog" && !element.hasAttribute("open")) return false;
|
|
22
|
+
if (element.closest("[hidden]")) return false;
|
|
23
|
+
return (
|
|
24
|
+
typeof element.checkVisibility !== "function" || element.checkVisibility({ visibilityProperty: true })
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isModal(element: Element): boolean {
|
|
29
|
+
if (element.getAttribute("aria-modal") === "true") return true;
|
|
30
|
+
try {
|
|
31
|
+
return element.matches(":modal");
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function openDialogs(scope: ParentNode): Element[] {
|
|
38
|
+
return [...scope.querySelectorAll('[role="dialog"], [role="alertdialog"], dialog')].filter(isShown);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The shell's one `keydown` listener (bubble phase): applies `decideKey` (guide §5.8, §13.5). */
|
|
42
|
+
export function createKeyListener(context: KeyListenerContext): (event: KeyboardEvent) => void {
|
|
43
|
+
return (event) => {
|
|
44
|
+
if (event.key === "Escape") context.hideTooltip();
|
|
45
|
+
const dialogs = openDialogs(context.dialogScope);
|
|
46
|
+
const decision = decideKey(event, {
|
|
47
|
+
host: context.host,
|
|
48
|
+
inTextField: isTextField(event.target),
|
|
49
|
+
menuOpen: context.menuOpen(),
|
|
50
|
+
dialogOpen: dialogs.length > 0,
|
|
51
|
+
modalOpen: dialogs.some(isModal),
|
|
52
|
+
defaultPrevented: event.defaultPrevented,
|
|
53
|
+
});
|
|
54
|
+
if (decision.preventDefault) event.preventDefault();
|
|
55
|
+
if (decision.stopPropagation) event.stopPropagation();
|
|
56
|
+
if (decision.action === "closeMenu") context.closeMenu();
|
|
57
|
+
else if (decision.action) context.run(decision.action);
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything the shell changes outside the elements it creates (classes and attributes on the root,
|
|
3
|
+
* `<html>` and a passed canvas, listeners, observers, timers), recorded so `destroy()` puts it back
|
|
4
|
+
* exactly as it was before `mountShell`.
|
|
5
|
+
*/
|
|
6
|
+
export class Ledger {
|
|
7
|
+
private readonly undo: (() => void)[] = [];
|
|
8
|
+
private readonly originalAttributes = new Map<Element, Map<string, string | null>>();
|
|
9
|
+
private closed = false;
|
|
10
|
+
|
|
11
|
+
get isClosed(): boolean {
|
|
12
|
+
return this.closed;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Runs `task` on `close()`, in reverse order of registration. */
|
|
16
|
+
defer(task: () => void): void {
|
|
17
|
+
this.undo.push(task);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Adds `className` unless present; `close()` removes it only if this ledger added it. */
|
|
21
|
+
addClass(element: Element, className: string): void {
|
|
22
|
+
if (element.classList.contains(className)) return;
|
|
23
|
+
const hadClassAttribute = element.hasAttribute("class");
|
|
24
|
+
element.classList.add(className);
|
|
25
|
+
this.defer(() => {
|
|
26
|
+
element.classList.remove(className);
|
|
27
|
+
if (!hadClassAttribute && element.classList.length === 0) element.removeAttribute("class");
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Sets (string) or removes (`null`) an attribute; `close()` restores the value before the first change. */
|
|
32
|
+
attribute(element: Element, name: string, value: string | null): void {
|
|
33
|
+
let originals = this.originalAttributes.get(element);
|
|
34
|
+
if (!originals) {
|
|
35
|
+
originals = new Map();
|
|
36
|
+
this.originalAttributes.set(element, originals);
|
|
37
|
+
}
|
|
38
|
+
if (!originals.has(name)) {
|
|
39
|
+
originals.set(name, element.getAttribute(name));
|
|
40
|
+
this.defer(() => {
|
|
41
|
+
const original = originals.get(name) ?? null;
|
|
42
|
+
if (original === null) element.removeAttribute(name);
|
|
43
|
+
else element.setAttribute(name, original);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (value === null) element.removeAttribute(name);
|
|
47
|
+
else element.setAttribute(name, value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
listen<K extends keyof DocumentEventMap>(
|
|
51
|
+
target: Document,
|
|
52
|
+
type: K,
|
|
53
|
+
listener: (event: DocumentEventMap[K]) => void,
|
|
54
|
+
options?: AddEventListenerOptions,
|
|
55
|
+
): void;
|
|
56
|
+
listen<K extends keyof HTMLElementEventMap>(
|
|
57
|
+
target: HTMLElement,
|
|
58
|
+
type: K,
|
|
59
|
+
listener: (event: HTMLElementEventMap[K]) => void,
|
|
60
|
+
options?: AddEventListenerOptions,
|
|
61
|
+
): void;
|
|
62
|
+
listen(
|
|
63
|
+
target: EventTarget,
|
|
64
|
+
type: string,
|
|
65
|
+
listener: (event: never) => void,
|
|
66
|
+
options?: AddEventListenerOptions,
|
|
67
|
+
): void {
|
|
68
|
+
const handler = listener as EventListener;
|
|
69
|
+
target.addEventListener(type, handler, options);
|
|
70
|
+
this.defer(() => target.removeEventListener(type, handler, options));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Runs every undo task once; later calls are no-ops. */
|
|
74
|
+
close(): void {
|
|
75
|
+
if (this.closed) return;
|
|
76
|
+
this.closed = true;
|
|
77
|
+
for (const task of this.undo.reverse()) {
|
|
78
|
+
try {
|
|
79
|
+
task();
|
|
80
|
+
} catch {
|
|
81
|
+
// one failing undo (an element the modeler already removed) must not keep the rest
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
this.undo.length = 0;
|
|
85
|
+
this.originalAttributes.clear();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ShellMessages } from "../core/index.js";
|
|
2
|
+
import { createElement } from "./element.js";
|
|
3
|
+
import { iconElement } from "./icons.js";
|
|
4
|
+
import type { TooltipController } from "./tooltip.js";
|
|
5
|
+
import type { LegendHandle, LegendOptions } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export interface LegendController extends LegendHandle {
|
|
8
|
+
/** The start state once the container height is known; never reported through `onToggle`. */
|
|
9
|
+
start(open: boolean): void;
|
|
10
|
+
isStarted(): boolean;
|
|
11
|
+
destroy(): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface LegendContext {
|
|
15
|
+
ownerDocument: Document;
|
|
16
|
+
messages: ShellMessages;
|
|
17
|
+
tooltip: TooltipController;
|
|
18
|
+
titleId: string;
|
|
19
|
+
options: LegendOptions;
|
|
20
|
+
onChange(open: boolean): void;
|
|
21
|
+
/** Where the focus goes when the close button hides the legend under it. */
|
|
22
|
+
focusAfterClose(): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createLegend(context: LegendContext): LegendController {
|
|
26
|
+
const { ownerDocument, messages, tooltip, titleId, options } = context;
|
|
27
|
+
const close = createElement(ownerDocument, "button", {
|
|
28
|
+
type: "button",
|
|
29
|
+
class: "cd-shell-legend-close",
|
|
30
|
+
"aria-label": messages.closeLegend,
|
|
31
|
+
});
|
|
32
|
+
close.append(iconElement("x"));
|
|
33
|
+
const body = createElement(ownerDocument, "div", { class: "cd-shell-legend-body" });
|
|
34
|
+
const element = createElement(
|
|
35
|
+
ownerDocument,
|
|
36
|
+
"section",
|
|
37
|
+
{ class: "cd-shell-legend cd-shell-island", "data-chrome": "legend", "aria-labelledby": titleId },
|
|
38
|
+
createElement(
|
|
39
|
+
ownerDocument,
|
|
40
|
+
"div",
|
|
41
|
+
{ class: "cd-shell-legend-header" },
|
|
42
|
+
createElement(ownerDocument, "div", { class: "cd-shell-legend-title", id: titleId }, messages.legend),
|
|
43
|
+
close,
|
|
44
|
+
),
|
|
45
|
+
body,
|
|
46
|
+
);
|
|
47
|
+
element.hidden = true;
|
|
48
|
+
options.render(body);
|
|
49
|
+
|
|
50
|
+
let open = false;
|
|
51
|
+
let started = false;
|
|
52
|
+
const detachTooltip = tooltip.attach(close, { label: messages.closeLegend });
|
|
53
|
+
|
|
54
|
+
function apply(next: boolean): boolean {
|
|
55
|
+
started = true;
|
|
56
|
+
if (next === open) return false;
|
|
57
|
+
open = next;
|
|
58
|
+
element.hidden = !open;
|
|
59
|
+
if (!open && element.contains(ownerDocument.activeElement)) context.focusAfterClose();
|
|
60
|
+
context.onChange(open);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function setOpen(next: boolean): void {
|
|
65
|
+
if (apply(next)) options.onToggle?.(next);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
close.addEventListener("click", () => setOpen(false));
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
element,
|
|
72
|
+
body,
|
|
73
|
+
isOpen: () => open,
|
|
74
|
+
setOpen,
|
|
75
|
+
toggle: () => setOpen(!open),
|
|
76
|
+
start(initial) {
|
|
77
|
+
if (!started) apply(initial);
|
|
78
|
+
},
|
|
79
|
+
isStarted: () => started,
|
|
80
|
+
destroy() {
|
|
81
|
+
detachTooltip();
|
|
82
|
+
element.remove();
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|