@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.
Files changed (147) hide show
  1. package/API.md +2869 -0
  2. package/LICENSE +21 -0
  3. package/README.md +566 -2
  4. package/THIRD_PARTY_NOTICES.md +52 -0
  5. package/dist/appearance-BBaM_4bD.d.ts +323 -0
  6. package/dist/appearance-D3FUcbu6.d.cts +53 -0
  7. package/dist/appearance-D3FUcbu6.d.ts +53 -0
  8. package/dist/appearance-XX3JC3pI.d.cts +323 -0
  9. package/dist/chunk-66HCSJNY.js +2792 -0
  10. package/dist/chunk-66HCSJNY.js.map +1 -0
  11. package/dist/chunk-KYDG5DMX.js +136 -0
  12. package/dist/chunk-KYDG5DMX.js.map +1 -0
  13. package/dist/chunk-N4QXFF3A.js +23 -0
  14. package/dist/chunk-N4QXFF3A.js.map +1 -0
  15. package/dist/chunk-OADW3GPA.js +785 -0
  16. package/dist/chunk-OADW3GPA.js.map +1 -0
  17. package/dist/core.cjs +873 -0
  18. package/dist/core.cjs.map +1 -0
  19. package/dist/core.d.cts +246 -0
  20. package/dist/core.d.ts +246 -0
  21. package/dist/core.js +131 -0
  22. package/dist/core.js.map +1 -0
  23. package/dist/diagram-js.cjs +1147 -0
  24. package/dist/diagram-js.cjs.map +1 -0
  25. package/dist/diagram-js.d.cts +170 -0
  26. package/dist/diagram-js.d.ts +170 -0
  27. package/dist/diagram-js.js +879 -0
  28. package/dist/diagram-js.js.map +1 -0
  29. package/dist/index.cjs +3632 -0
  30. package/dist/index.cjs.map +1 -0
  31. package/dist/index.d.cts +29 -0
  32. package/dist/index.d.ts +29 -0
  33. package/dist/index.js +143 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/protocol-DhYBakWL.d.cts +15 -0
  36. package/dist/protocol-IeW8bf4f.d.ts +15 -0
  37. package/dist/shell.css +1603 -0
  38. package/dist/testing-vscode-stub.cjs +332 -0
  39. package/dist/testing-vscode-stub.cjs.map +1 -0
  40. package/dist/testing-vscode-stub.d.cts +65 -0
  41. package/dist/testing-vscode-stub.d.ts +65 -0
  42. package/dist/testing-vscode-stub.js +154 -0
  43. package/dist/testing-vscode-stub.js.map +1 -0
  44. package/dist/testing.cjs +4663 -0
  45. package/dist/testing.cjs.map +1 -0
  46. package/dist/testing.d.cts +231 -0
  47. package/dist/testing.d.ts +231 -0
  48. package/dist/testing.js +4483 -0
  49. package/dist/testing.js.map +1 -0
  50. package/dist/toasts-Bp0REqdq.d.cts +142 -0
  51. package/dist/toasts-Bp0REqdq.d.ts +142 -0
  52. package/dist/types-CnRrINRs.d.cts +445 -0
  53. package/dist/types-PuDZczzy.d.ts +445 -0
  54. package/dist/vscode-extension.cjs +215 -0
  55. package/dist/vscode-extension.cjs.map +1 -0
  56. package/dist/vscode-extension.d.cts +103 -0
  57. package/dist/vscode-extension.d.ts +103 -0
  58. package/dist/vscode-extension.js +24 -0
  59. package/dist/vscode-extension.js.map +1 -0
  60. package/dist/vscode-webview.cjs +3561 -0
  61. package/dist/vscode-webview.cjs.map +1 -0
  62. package/dist/vscode-webview.d.cts +84 -0
  63. package/dist/vscode-webview.d.ts +84 -0
  64. package/dist/vscode-webview.js +259 -0
  65. package/dist/vscode-webview.js.map +1 -0
  66. package/package.json +145 -4
  67. package/src/core/appearance.ts +104 -0
  68. package/src/core/feedback/appearance.ts +141 -0
  69. package/src/core/feedback/problems.ts +23 -0
  70. package/src/core/feedback/toasts.ts +118 -0
  71. package/src/core/fit.ts +220 -0
  72. package/src/core/index.ts +127 -0
  73. package/src/core/keymap.ts +295 -0
  74. package/src/core/layout.ts +69 -0
  75. package/src/core/messages.ts +159 -0
  76. package/src/core/sizes.ts +85 -0
  77. package/src/core/types.ts +106 -0
  78. package/src/core/web-chrome/bottom-row.ts +37 -0
  79. package/src/core/web-chrome/drop.ts +78 -0
  80. package/src/core/web-chrome/top-row.ts +66 -0
  81. package/src/core/zoom.ts +74 -0
  82. package/src/core.ts +2 -0
  83. package/src/diagram-js/connect.ts +75 -0
  84. package/src/diagram-js/index.ts +16 -0
  85. package/src/diagram-js/module.ts +20 -0
  86. package/src/diagram-js/palette-skin.ts +267 -0
  87. package/src/diagram-js/palette.css +269 -0
  88. package/src/diagram-js/service.ts +243 -0
  89. package/src/diagram-js/types.ts +67 -0
  90. package/src/diagram-js/viewport.ts +583 -0
  91. package/src/diagram-js.ts +2 -0
  92. package/src/dom/buttons.css +124 -0
  93. package/src/dom/element.ts +51 -0
  94. package/src/dom/feedback/appearance.ts +184 -0
  95. package/src/dom/feedback/feedback.css +435 -0
  96. package/src/dom/feedback/problems-popover.ts +287 -0
  97. package/src/dom/feedback/toasts.ts +431 -0
  98. package/src/dom/hint.ts +51 -0
  99. package/src/dom/icons.ts +309 -0
  100. package/src/dom/index.ts +42 -0
  101. package/src/dom/inspector.ts +35 -0
  102. package/src/dom/keys.ts +67 -0
  103. package/src/dom/ledger.ts +87 -0
  104. package/src/dom/legend.ts +85 -0
  105. package/src/dom/menu.ts +404 -0
  106. package/src/dom/mount.ts +731 -0
  107. package/src/dom/problems.ts +73 -0
  108. package/src/dom/shell-dom.css +969 -0
  109. package/src/dom/status.ts +72 -0
  110. package/src/dom/tooltip.ts +237 -0
  111. package/src/dom/types.ts +459 -0
  112. package/src/dom/web/drop-zone.ts +181 -0
  113. package/src/dom/web/empty-state.ts +144 -0
  114. package/src/dom/web/legal.ts +75 -0
  115. package/src/dom/web/menu-groups.ts +69 -0
  116. package/src/dom/web/primary.ts +124 -0
  117. package/src/dom/web/web.css +386 -0
  118. package/src/dom/zoom-panel.ts +154 -0
  119. package/src/index.ts +3 -0
  120. package/src/shell.css +9 -0
  121. package/src/testing/case-tools.ts +164 -0
  122. package/src/testing/cases.ts +1077 -0
  123. package/src/testing/checks.ts +868 -0
  124. package/src/testing/facts.ts +641 -0
  125. package/src/testing/geometry.ts +652 -0
  126. package/src/testing/harness.ts +187 -0
  127. package/src/testing/index.ts +19 -0
  128. package/src/testing/page-tools.ts +457 -0
  129. package/src/testing/page.ts +27 -0
  130. package/src/testing/vscode-stub.ts +237 -0
  131. package/src/testing/web-cases.ts +913 -0
  132. package/src/testing/web-checks.ts +1049 -0
  133. package/src/testing/web-facts.ts +713 -0
  134. package/src/testing-vscode-stub.ts +2 -0
  135. package/src/testing.ts +2 -0
  136. package/src/vscode/extension/html.ts +107 -0
  137. package/src/vscode/extension/index.ts +8 -0
  138. package/src/vscode/extension/reveal.ts +9 -0
  139. package/src/vscode/extension/theme-bridge.ts +85 -0
  140. package/src/vscode/extension/types.ts +36 -0
  141. package/src/vscode/protocol.ts +31 -0
  142. package/src/vscode/webview/appearance.ts +141 -0
  143. package/src/vscode/webview/index.ts +8 -0
  144. package/src/vscode/webview/mount.ts +88 -0
  145. package/src/vscode/webview/state.ts +116 -0
  146. package/src/vscode-extension.ts +2 -0
  147. package/src/vscode-webview.ts +2 -0
@@ -0,0 +1,459 @@
1
+ import type {
2
+ AppearanceChoice,
3
+ Insets,
4
+ Platform,
5
+ ProblemItem,
6
+ ShellHost,
7
+ ShellMessageOverrides,
8
+ ShellMessages,
9
+ Theme,
10
+ ThemeColors,
11
+ ToastRequest,
12
+ ZoomTarget,
13
+ } from "../core/index.js";
14
+ import type { ShellIconName } from "./icons.js";
15
+
16
+ export interface ShellOptions {
17
+ /** The host column of guide §5.2. */
18
+ host: ShellHost;
19
+ /**
20
+ * The element diagram-js renders into (for example a renderer's `modeler.container`). The shell
21
+ * moves it into its canvas slot and adds `cd-shell-canvas`. Omitted: the shell creates the slot,
22
+ * and the modeler is constructed with `container: handle.canvas`.
23
+ */
24
+ canvas?: HTMLElement | undefined;
25
+ /**
26
+ * The tool's UI language (BCP 47) for `Intl` and `<html lang>` (guide §2: one language per tool).
27
+ * Default in an own document (`ide`, `web`): the closest `[lang]` of the root, else `"en"`. In the
28
+ * `embedded` host the page's language is not the tool's, so the default is the root's own `lang`
29
+ * attribute, else `"en"`, and the root carries `lang` itself.
30
+ */
31
+ lang?: string | undefined;
32
+ messages?: ShellMessageOverrides | undefined;
33
+ /** Platform of the shortcut labels; default `detectPlatform(navigator)`. */
34
+ platform?: Platform | undefined;
35
+ /** Where the one `keydown` listener sits; default `document`, `root` in the `embedded` host. */
36
+ keyScope?: "document" | "root" | undefined;
37
+ /**
38
+ * The modes the modeler can show (a static value of the modeler package), default `["light"]`.
39
+ * Read in the `embedded` and `web` hosts. Embedded: the root carries `data-cd-theme="<themes[0]>"`
40
+ * itself (until the contract's `appearance` surface an embedded modeler stays in its first mode,
41
+ * whatever the page shows) and mirrors the host's `data-cd-kontrast` (guide §13.3). Web: with
42
+ * `"dark"` the shell renders the Appearance group and applies the choice on `<html>` (needs
43
+ * `appearance`); light-only it renders no group. In the `ide` host the host writes the mode.
44
+ */
45
+ themes?: readonly Theme[] | undefined;
46
+ /** The fixed menu groups this modeler fills (guide §5.4); omitted groups and empty ones collapse. */
47
+ menu?: MenuGroups | undefined;
48
+ /** The problems pill; omitted = the modeler has no diagnostics. */
49
+ problems?: ProblemsOptions | undefined;
50
+ /** The legend card; omitted = the notation has no legend. */
51
+ legend?: LegendOptions | undefined;
52
+ /** The inspector slot; omitted = the modeler has no inspector. */
53
+ inspector?: InspectorOptions | undefined;
54
+ /** The empty-file hint of IDE and embedded hosts (guide §13.5); omitted = no hint. */
55
+ emptyHint?: EmptyHintOptions | undefined;
56
+
57
+ // ---- Web host (0.2): read in `host: "web"` only, ignored elsewhere (guide §5.2) ----
58
+
59
+ /** The primary action at the top right (guide §5.7); omitted = the top right stays empty. */
60
+ primary?: PrimaryActionOptions | undefined;
61
+ /**
62
+ * The legal line at the bottom left (guide §5.10), always rendered in the web host: copyright,
63
+ * Impressum and Datenschutz (`lang="de"`), then `links`.
64
+ */
65
+ legal?: LegalLineOptions | undefined;
66
+ /** The File group, first in the menu (guide §5.4); omitted = no File group. */
67
+ file?: FileMenuOptions | undefined;
68
+ /** The mode choice of a dark-capable modeler (guide §13.3); required when `themes` includes `"dark"`. */
69
+ appearance?: WebAppearanceOptions | undefined;
70
+ /** The start screen (guide §8 Empty State); omitted = none. Shown by `setEmptyState(true)`. */
71
+ emptyState?: EmptyStateOptions | undefined;
72
+ /** Files dropped anywhere on the page (guide §9 Drag-over, Drop forbidden); omitted = none. */
73
+ dropZone?: DropZoneOptions | undefined;
74
+ }
75
+
76
+ /** Rendered in this order, separated by separators: Model, Export, Help (guide §5.4). */
77
+ export interface MenuGroups {
78
+ model?: readonly MenuEntry[] | undefined;
79
+ export?: readonly MenuEntry[] | undefined;
80
+ help?: readonly MenuEntry[] | undefined;
81
+ }
82
+
83
+ export type MenuEntry = MenuActionEntry | MenuDialogEntry | MenuCheckboxEntry | MenuRadioGroup;
84
+
85
+ interface MenuItemBase {
86
+ /** Stable id, rendered as `data-menu-id`. */
87
+ id: string;
88
+ label: string;
89
+ /** Only the entries the guide gives an icon (the common set, guide §5.14). */
90
+ icon?: ShellIconName | undefined;
91
+ /** Read whenever the menu opens or `refresh()` runs; disabled entries stay focusable. */
92
+ disabled?: (() => boolean) | undefined;
93
+ /**
94
+ * A Model entry that stays enabled on a read-only canvas: a view-only toggle that never touches
95
+ * the document (Legend, Type captions). Default `false`: the entry is aria-disabled while the
96
+ * canvas is read-only (guide §13.5). The shell's own Legend checkbox sets it.
97
+ */
98
+ keepEnabledWhenReadOnly?: boolean | undefined;
99
+ }
100
+
101
+ /** `menuitem`: closes the menu, returns focus to the trigger, then runs `onSelect`. */
102
+ export interface MenuActionEntry extends MenuItemBase {
103
+ kind: "action";
104
+ /** Shortcut chip text shown on the right, verbatim. */
105
+ shortcut?: string | undefined;
106
+ /**
107
+ * `aria-keyshortcuts` (WAI-ARIA names, guide §5.6); default `ariaKeyShortcutsFromLabel(shortcut)`.
108
+ * An empty string sets none.
109
+ */
110
+ ariaKeyShortcuts?: string | undefined;
111
+ /** Destructive: icon in `--cd-ui-danger` (confirm per guide §9 in `onSelect`). */
112
+ danger?: boolean | undefined;
113
+ onSelect(): void;
114
+ }
115
+
116
+ /**
117
+ * `menuitem` with `aria-haspopup="dialog"`; its label ends with "…" (U+2026). Behaves like an
118
+ * action: the menu closes and focus returns to the trigger before `onSelect` opens the dialog.
119
+ */
120
+ export interface MenuDialogEntry extends MenuItemBase {
121
+ kind: "dialog";
122
+ /** Shortcut chip text shown on the right, verbatim ("?" for "Keyboard shortcuts…"). */
123
+ shortcut?: string | undefined;
124
+ /** As for `MenuActionEntry`: default `ariaKeyShortcutsFromLabel(shortcut)`, `""` sets none. */
125
+ ariaKeyShortcuts?: string | undefined;
126
+ onSelect(): void;
127
+ }
128
+
129
+ /** `menuitemcheckbox`: toggles and keeps the menu open; the check mark carries the state. */
130
+ export interface MenuCheckboxEntry extends MenuItemBase {
131
+ kind: "checkbox";
132
+ checked(): boolean;
133
+ onToggle(checked: boolean): void;
134
+ }
135
+
136
+ /** A `role="group"` with a heading and `menuitemradio` entries; keeps the menu open. */
137
+ export interface MenuRadioGroup {
138
+ kind: "radio";
139
+ id: string;
140
+ /** Heading and group `aria-label`. */
141
+ label: string;
142
+ options: readonly MenuRadioOption[];
143
+ value(): string | undefined;
144
+ onSelect(value: string): void;
145
+ disabled?: (() => boolean) | undefined;
146
+ /** As `MenuItemBase.keepEnabledWhenReadOnly`. */
147
+ keepEnabledWhenReadOnly?: boolean | undefined;
148
+ }
149
+
150
+ export interface MenuRadioOption {
151
+ value: string;
152
+ label: string;
153
+ }
154
+
155
+ export interface ProblemsOptions {
156
+ /**
157
+ * A click on the pill where the host has a problems view of its own: the IDE host opens its
158
+ * Problems view (give it there), an embedding host its own list. Ignored in the web host, where
159
+ * the pill opens the problems popover; an embedded shell without it opens the popover as well.
160
+ */
161
+ onActivate?: (() => void) | undefined;
162
+ /**
163
+ * A click on a row of the problems popover, after the popover closed (guide §5.10): select the
164
+ * element the item names, where there is one.
165
+ */
166
+ onSelectItem?: ((item: ProblemItem) => void) | undefined;
167
+ }
168
+
169
+ export interface LegendOptions {
170
+ /** Renders the legend rows into `body` once (classes `cd-shell-legend-*`, see API.md). */
171
+ render(body: HTMLElement): void;
172
+ /** Start state the host remembered; default (also `undefined`) `legendStartsOpen(container height)`. */
173
+ open?: boolean | undefined;
174
+ /**
175
+ * Every change of the open state (close button, menu checkbox, `setOpen`, `toggle`), so the host
176
+ * can persist it; not the automatic start decision.
177
+ */
178
+ onToggle?: ((open: boolean) => void) | undefined;
179
+ /** Append the "Legend" checkbox to the Model group; default `true`. */
180
+ menuEntry?: boolean | undefined;
181
+ }
182
+
183
+ export interface InspectorOptions {
184
+ /** `aria-label` of the inspector `<aside>`. */
185
+ label: string;
186
+ }
187
+
188
+ export interface EmptyHintOptions {
189
+ /** Shows the "Insert example" button; the modeler writes the example as one document change. */
190
+ onInsertExample?: (() => void) | undefined;
191
+ }
192
+
193
+ export interface ShellHandle {
194
+ /** The element passed to `mountShell`, now `.cd-shell`. */
195
+ readonly root: HTMLElement;
196
+ /** The canvas slot (`.cd-shell-canvas`), diagram-js's container. */
197
+ readonly canvas: HTMLElement;
198
+ readonly host: ShellHost;
199
+ readonly lang: string;
200
+ readonly platform: Platform;
201
+ /** The resolved messages table. */
202
+ readonly messages: ShellMessages;
203
+ readonly menu: MenuHandle;
204
+ readonly tooltip: TooltipHandle;
205
+ readonly legend: LegendHandle | undefined;
206
+ readonly inspector: InspectorHandle | undefined;
207
+ /** The web app's primary action; `undefined` without `primary` and outside the web host. */
208
+ readonly primary: PrimaryActionHandle | undefined;
209
+ /** The web app's mode choice; `undefined` outside the web host and for a light-only modeler. */
210
+ readonly appearance: AppearanceHandle | undefined;
211
+ /** Binds zoom panel, readout and chrome keys; `null` unbinds (buttons disabled). */
212
+ setZoomTarget(target: ZoomTarget | null): void;
213
+ /** The fit insets for what is open right now (palette row, legend, inspector, pill). */
214
+ fitInsets(): Insets;
215
+ /** Adapter API: the diagram-js palette island; returns the unregister function. */
216
+ registerPalette(palette: PaletteRegistration): () => void;
217
+ /** Shows the pill for `count > 0`, hides it at 0. */
218
+ setProblems(summary: ProblemSummary): void;
219
+ /** Shows (after `delayMs`, default 0) or hides (at once) the status box under the palette. */
220
+ setStatus(status: StatusMessage | null, options?: { delayMs?: number | undefined }): void;
221
+ /**
222
+ * Shows or hides the empty-file hint. `connectDiagramJs` keeps it in sync with the diagram
223
+ * (`ConnectOptions.isEmpty`). Hiding it while it holds the focus moves the focus to the canvas.
224
+ */
225
+ setEmptyHint(visible: boolean): void;
226
+ /**
227
+ * Read-only canvas (invalid file, guide §13.5). `true` locks at once: `isReadOnly()`, the palette
228
+ * veto (every entry but Lasso and Hand) and the Model entries of the menu without
229
+ * `keepEnabledWhenReadOnly`; the legend and its Legend checkbox stay usable. `data-readonly` and the
230
+ * `aria-disabled` palette entries show after `delayMs` (default 0; an invalid file passes
231
+ * `INVALID_STATUS_DELAY_MS`, as for `setStatus`), so typing in the text editor does not flicker.
232
+ * Repeated calls while the delay runs keep the first timer; `false` unlocks and hides at once.
233
+ */
234
+ setReadOnly(readOnly: boolean, options?: { delayMs?: number | undefined }): void;
235
+ /** The lock, true from the first `setReadOnly(true)` on, also while its marks are still delayed. */
236
+ isReadOnly(): boolean;
237
+ /**
238
+ * Shows or hides the web app's start screen (guide §8): while it shows, menu trigger, primary
239
+ * action, palette, zoom panel, pill, legend, status box, hint and inspector are hidden, the canvas
240
+ * slot is `inert` and the chrome keys do nothing; the legal line, toasts and the drop zone stay.
241
+ * A no-op without `emptyState`. Hiding it while it holds the focus moves the focus to the canvas.
242
+ */
243
+ setEmptyState(shown: boolean): void;
244
+ isEmptyStateShown(): boolean;
245
+ /**
246
+ * Shows a toast (guide §8, §9) in the web and embedded hosts: bottom centre, at most three, the
247
+ * newest at the bottom, closing by itself after `toastLifetime` unless hovered or focused. In the
248
+ * `ide` host it shows nothing and returns an inert handle: there the extension shows the host's
249
+ * notification (§13.5).
250
+ */
251
+ toast(request: ToastRequest): ToastHandle;
252
+ /** Removes every element, attribute, listener, observer and timer the shell added. Idempotent. */
253
+ destroy(): void;
254
+ }
255
+
256
+ export interface MenuHandle {
257
+ readonly trigger: HTMLButtonElement;
258
+ readonly dropdown: HTMLElement;
259
+ isOpen(): boolean;
260
+ /** Opens on the first (default) or last entry. */
261
+ open(focus?: "first" | "last"): void;
262
+ /** Closes; `restoreFocus` moves focus back to the trigger (default `false`). */
263
+ close(options?: { restoreFocus?: boolean | undefined }): void;
264
+ /** Re-reads `checked`, `value` and `disabled` (call after an external state change). */
265
+ refresh(): void;
266
+ setGroups(groups: MenuGroups): void;
267
+ }
268
+
269
+ export interface TooltipContent {
270
+ label: string;
271
+ /** Shortcut chip text, verbatim (`⌘+`). */
272
+ shortcut?: string | undefined;
273
+ }
274
+
275
+ export interface TooltipHandle {
276
+ /**
277
+ * The chrome tooltip for `control`: after `TOOLTIP_DELAY_MS` hover, at once on keyboard focus,
278
+ * hidden on click, Escape, leave and menu open. Returns the detach function.
279
+ */
280
+ attach(control: HTMLElement, content: TooltipContent | (() => TooltipContent | null)): () => void;
281
+ hide(): void;
282
+ }
283
+
284
+ export interface LegendHandle {
285
+ /** The legend card (`[data-chrome="legend"]`). */
286
+ readonly element: HTMLElement;
287
+ /** Where `render` put the rows. */
288
+ readonly body: HTMLElement;
289
+ isOpen(): boolean;
290
+ setOpen(open: boolean): void;
291
+ toggle(): void;
292
+ }
293
+
294
+ export interface InspectorHandle {
295
+ /** The `<aside data-chrome="inspector">`; the modeler renders its fields into it. */
296
+ readonly element: HTMLElement;
297
+ isOpen(): boolean;
298
+ open(): void;
299
+ /** Closes; a focus inside it moves to the canvas. */
300
+ close(): void;
301
+ }
302
+
303
+ /** What the diagram-js adapter hands the shell about the palette. */
304
+ export interface PaletteRegistration {
305
+ /** The `.djs-palette` element (already carrying `data-chrome="palette"`). */
306
+ element: HTMLElement;
307
+ /** Tooltip of an entry element (`.entry`), from `palette.getEntries()` title and shortcut. */
308
+ entryTooltip(entry: HTMLElement): TooltipContent | null;
309
+ /** The read-only state, on registration and on every `setReadOnly` and its delayed marks. */
310
+ setReadOnly(state: PaletteReadOnlyState): void;
311
+ }
312
+
313
+ /** The read-only canvas as the palette sees it (guide §13.5). */
314
+ export interface PaletteReadOnlyState {
315
+ /** Veto every entry but Lasso and Hand on `palette.trigger`, from the invalid import on. */
316
+ locked: boolean;
317
+ /** Mark those entries `aria-disabled` and muted, once the shell's delay has passed. */
318
+ shown: boolean;
319
+ }
320
+
321
+ export interface ProblemSummary {
322
+ count: number;
323
+ /** How many of them are errors; decides icon and wording. */
324
+ errors: number;
325
+ /**
326
+ * The rows of the problems popover (web, embedded without `onActivate`), in this order; the IDE
327
+ * host ignores them. `problemSummary(items)` builds the whole summary from them.
328
+ */
329
+ items?: readonly ProblemItem[] | undefined;
330
+ }
331
+
332
+ export type StatusSeverity = "danger" | "warning";
333
+
334
+ export interface StatusMessage {
335
+ severity: StatusSeverity;
336
+ text: string;
337
+ }
338
+
339
+ // ---- Web host (0.2) ----
340
+
341
+ /** The web app's primary action (guide §5.7): a blue primary button, icon and label, at the top right. */
342
+ export interface PrimaryActionOptions {
343
+ /** Label and `aria-label`; default `messages.share` ("Share"). */
344
+ label?: string | undefined;
345
+ /** Default `"share-2"`. */
346
+ icon?: ShellIconName | undefined;
347
+ onSelect(): void;
348
+ /** Read on mount and on `primary.refresh()`; disabled stays focusable (`aria-disabled`). */
349
+ disabled?: (() => boolean) | undefined;
350
+ }
351
+
352
+ export interface PrimaryActionHandle {
353
+ /** The `<button data-chrome="primary">`. */
354
+ readonly element: HTMLButtonElement;
355
+ /** True while the label is collapsed to the icon (guide §5.15); the root then carries `data-primary="collapsed"`. */
356
+ isCollapsed(): boolean;
357
+ /** Re-reads `disabled()`. */
358
+ refresh(): void;
359
+ }
360
+
361
+ /** A link of the legal line after Impressum and Datenschutz. */
362
+ export interface LegalLink {
363
+ label: string;
364
+ href: string;
365
+ /** For a label in another language than the tool's. */
366
+ lang?: string | undefined;
367
+ }
368
+
369
+ export interface LegalLineOptions {
370
+ /** After Impressum and Datenschutz: `{ label: "GitHub", href: "https://github.com/Miragon/…" }`. */
371
+ links?: readonly LegalLink[] | undefined;
372
+ /** The copyright year; default the current year at mount. */
373
+ year?: number | undefined;
374
+ }
375
+
376
+ /** One entry of the File group; label and icon come from the shell (guide §2, §5.4). */
377
+ export interface FileMenuAction {
378
+ onSelect(): void;
379
+ /** Shortcut chip, e.g. "⌘O"; the web app binds the key itself. */
380
+ shortcut?: string | undefined;
381
+ /** `kind: "dialog"` (`aria-haspopup="dialog"`) when it opens an in-page dialog, not the browser's file picker. */
382
+ dialog?: boolean | undefined;
383
+ disabled?: (() => boolean) | undefined;
384
+ }
385
+
386
+ /**
387
+ * The File group of the web app (guide §5.4), in this order, as far as the modeler has them:
388
+ * "New diagram" (`file-plus`), "Open…" (`folder-open`), "Show example", "Import…" (`import`).
389
+ * Menu ids `new-diagram`, `open`, `show-example`, `import`, `data-menu-group="file"`.
390
+ */
391
+ export interface FileMenuOptions {
392
+ newDiagram?: FileMenuAction | undefined;
393
+ open?: FileMenuAction | undefined;
394
+ showExample?: FileMenuAction | undefined;
395
+ import?: FileMenuAction | undefined;
396
+ }
397
+
398
+ /** What `localStorage` offers; a blocked storage throws, the shell catches it. */
399
+ export interface StorageLike {
400
+ getItem(key: string): string | null;
401
+ setItem(key: string, value: string): void;
402
+ removeItem(key: string): void;
403
+ }
404
+
405
+ export interface WebAppearanceOptions {
406
+ /** The two `theme-color` values from the vendored token JSON (see `ThemeColors`). */
407
+ themeColors: ThemeColors;
408
+ /** Default `globalThis.localStorage`. */
409
+ storage?: StorageLike | undefined;
410
+ }
411
+
412
+ /** The mode choice of a dark-capable web app (guide §13.3), as the Appearance group drives it. */
413
+ export interface AppearanceHandle {
414
+ /** The stored choice, `"system"` without one. */
415
+ choice(): AppearanceChoice;
416
+ /**
417
+ * Stores the choice (`"system"` removes `APPEARANCE_STORAGE_KEY`) and applies `documentMode` on
418
+ * `<html>` and the metas in one step, with `cd-shell-no-transition` for one frame (guide §7).
419
+ */
420
+ setChoice(choice: AppearanceChoice): void;
421
+ }
422
+
423
+ /** The web app's start screen (guide §8): a centred glass card with the Komet and two actions. */
424
+ export interface EmptyStateOptions {
425
+ /** The card's title (h1, 21px/600), the modeler's name: "Event Storming Modeler". */
426
+ title: string;
427
+ /** One line of help (16px/1.6, `--cd-ui-text-leise`): "Start a new diagram, or explore the example." */
428
+ text: string;
429
+ /**
430
+ * URL of the official Miragon icon `miragon-komet-blau.svg`, vendored unchanged from
431
+ * `Miragon/corporate-identity` (guide §2, §11), shown at 64px with `alt=""`.
432
+ */
433
+ komet: string;
434
+ /** "New diagram", the blue primary button. The shell hides the card first. */
435
+ onNewDiagram(): void;
436
+ /** "Show example", the ghost button; omitted = no example button. The shell hides the card first. */
437
+ onShowExample?: (() => void) | undefined;
438
+ /** The drag-and-drop hint (13px, `--cd-ui-text-leise`), shown with a `dropZone` only; default `messages.dropHint`. */
439
+ dropHint?: string | undefined;
440
+ }
441
+
442
+ export interface DropZoneOptions {
443
+ /**
444
+ * Like `<input accept>`: extensions (".storm") and MIME types ("application/json", "text/*").
445
+ * List the MIME types too, so a drag can already show a foreign type as forbidden. Empty = any file.
446
+ */
447
+ accept: readonly string[];
448
+ /** Default `false`: a drag with more than one file is forbidden. */
449
+ multiple?: boolean | undefined;
450
+ /** The dropped files that match `accept` (one unless `multiple`). */
451
+ onDrop(files: readonly File[]): void;
452
+ /** The dropped files outside `accept`; default a danger toast `messages.cannotOpenFile`. */
453
+ onReject?: ((files: readonly File[]) => void) | undefined;
454
+ }
455
+
456
+ export interface ToastHandle {
457
+ /** Closes the toast (slide and fade out); a second call does nothing. */
458
+ dismiss(): void;
459
+ }
@@ -0,0 +1,181 @@
1
+ import {
2
+ acceptsFile,
3
+ dragVerdict,
4
+ type DragItemLike,
5
+ type DragVerdict,
6
+ type ShellMessages,
7
+ type ToastRequest,
8
+ } from "../../core/index.js";
9
+ import { createElement } from "../element.js";
10
+ import type { DropZoneOptions, ToastHandle } from "../types.js";
11
+
12
+ /** The drop zone as the root shows it: `data-drop="over"` or `"forbidden"` while a file drag is on the page. */
13
+ export type DropZoneState = "over" | "forbidden" | null;
14
+
15
+ export interface DropZoneContext {
16
+ ownerDocument: Document;
17
+ /** Where the overlay goes: the shell's layer (40, guide §5.13). */
18
+ layer: HTMLElement;
19
+ messages: ShellMessages;
20
+ lang: string;
21
+ options: DropZoneOptions;
22
+ /** A modal dialog is open in the document (the shell's dialog test). */
23
+ modalOpen(): boolean;
24
+ /** For the default `onReject`. */
25
+ toast(request: ToastRequest): ToastHandle;
26
+ /** Writes the root's `data-drop`. */
27
+ setState(state: DropZoneState): void;
28
+ }
29
+
30
+ export interface DropZoneController {
31
+ /** The overlay `<div data-chrome="drop-zone" aria-hidden="true">`. */
32
+ readonly element: HTMLElement;
33
+ state(): DropZoneState;
34
+ destroy(): void;
35
+ }
36
+
37
+ const DRAG_EVENTS = ["dragenter", "dragover", "dragleave", "drop"] as const;
38
+
39
+ function carriesFiles(transfer: DataTransfer | null): transfer is DataTransfer {
40
+ if (!transfer) return false;
41
+ if (Array.from(transfer.types ?? []).includes("Files")) return true;
42
+ return Array.from(transfer.items ?? []).some((item) => item.kind === "file");
43
+ }
44
+
45
+ /** What the drag shows of its items; a browser without `items` still names "Files" in `types`. */
46
+ function dragItems(transfer: DataTransfer): DragItemLike[] {
47
+ const items = Array.from(transfer.items ?? [], (item) => ({ kind: item.kind, type: item.type }));
48
+ return items.length > 0 ? items : [{ kind: "file", type: "" }];
49
+ }
50
+
51
+ /** The extensions of `accept` for the reject toast ("a .storm, .json, or .txt file"), else every entry. */
52
+ function acceptList(accept: readonly string[], lang: string): string {
53
+ const entries = accept.map((entry) => entry.trim()).filter((entry) => entry !== "");
54
+ const extensions = entries.filter((entry) => entry.startsWith("."));
55
+ const listed = extensions.length > 0 ? extensions : entries;
56
+ let format: Intl.ListFormat;
57
+ try {
58
+ format = new Intl.ListFormat(lang, { type: "disjunction" });
59
+ } catch {
60
+ format = new Intl.ListFormat("en", { type: "disjunction" });
61
+ }
62
+ return format.format(listed);
63
+ }
64
+
65
+ /**
66
+ * The page-wide drop zone of the web app (guide §9). Listens on the window (`dragenter`,
67
+ * `dragover`, `dragleave`, `drop`; outermost pair counted) for drags carrying files only, so
68
+ * palette drags and text drags into the label editor stay alone; `dragVerdict` decides the state.
69
+ * Drag-over: overlay `inset: 12px`, fill `--cd-ui-hover`, 2px dashed `--cd-ui-interaktion`, radius
70
+ * `lg`, chip `messages.dropFile` centred on `--cd-ui-flaeche-hoch` (13px/600, radius `md`,
71
+ * `shadow-2`), `dropEffect = "copy"`. Forbidden: fill `--cd-ui-danger-flaeche`, dashed
72
+ * `--cd-ui-danger`, chip `messages.dropForbidden(reason)`, `dropEffect = "none"` (cursor
73
+ * `not-allowed`). Every file drag is `preventDefault`-ed, so the browser never navigates to the
74
+ * file. A drop: files through `acceptsFile` → `onDrop(accepted)`, the rest → `onReject` (default
75
+ * a danger toast `cannotOpenFile(name, accept joined by Intl.ListFormat disjunction)`); a forbidden
76
+ * drop opens nothing. Hidden while no file drag is on the page.
77
+ */
78
+ export function createDropZone(context: DropZoneContext): DropZoneController {
79
+ const { ownerDocument, layer, messages, lang, options, modalOpen, toast, setState } = context;
80
+ const chip = createElement(ownerDocument, "span", { class: "cd-shell-drop-zone-chip" });
81
+ const element = createElement(
82
+ ownerDocument,
83
+ "div",
84
+ { class: "cd-shell-drop-zone", "data-chrome": "drop-zone", "aria-hidden": "true" },
85
+ chip,
86
+ );
87
+ element.hidden = true;
88
+ layer.append(element);
89
+
90
+ const view = ownerDocument.defaultView;
91
+ // dragenter and dragleave fire for every element the pointer crosses: only the outermost pair counts.
92
+ let depth = 0;
93
+ let current: DropZoneState = null;
94
+
95
+ function verdictFor(items: readonly DragItemLike[]): DragVerdict {
96
+ return dragVerdict(items, {
97
+ accept: options.accept,
98
+ multiple: options.multiple ?? false,
99
+ dialogOpen: modalOpen(),
100
+ });
101
+ }
102
+
103
+ function show(verdict: DragVerdict): void {
104
+ const next: DropZoneState = verdict.state === "none" ? null : verdict.state;
105
+ if (verdict.state === "over") chip.textContent = messages.dropFile;
106
+ if (verdict.state === "forbidden") chip.textContent = messages.dropForbidden(verdict.reason);
107
+ if (next === current) return;
108
+ current = next;
109
+ element.hidden = next === null;
110
+ if (next === null) element.removeAttribute("data-state");
111
+ else element.setAttribute("data-state", next);
112
+ setState(next);
113
+ }
114
+
115
+ function track(transfer: DataTransfer): void {
116
+ const verdict = verdictFor(dragItems(transfer));
117
+ show(verdict);
118
+ transfer.dropEffect = verdict.state === "forbidden" ? "none" : "copy";
119
+ }
120
+
121
+ function rejectByDefault(files: readonly File[]): void {
122
+ const accepted = acceptList(options.accept, lang);
123
+ for (const file of files) toast({ status: "danger", text: messages.cannotOpenFile(file.name, accepted) });
124
+ }
125
+
126
+ function receive(files: readonly File[], verdict: DragVerdict): void {
127
+ // The type verdict only guesses while names are unreadable; at the drop acceptsFile knows.
128
+ if (verdict.state === "forbidden" && verdict.reason !== "type") return;
129
+ const accepted = files.filter((file) => acceptsFile(file, options.accept));
130
+ const rejected = files.filter((file) => !accepted.includes(file));
131
+ if (accepted.length > 0) options.onDrop(accepted);
132
+ if (rejected.length > 0) (options.onReject ?? rejectByDefault)(rejected);
133
+ }
134
+
135
+ const onDragEvent = (event: Event): void => {
136
+ const transfer = (event as DragEvent).dataTransfer;
137
+ if (!carriesFiles(transfer)) return;
138
+ // Unprevented, the browser opens the file in place of the app.
139
+ event.preventDefault();
140
+ switch (event.type) {
141
+ case "dragenter":
142
+ depth += 1;
143
+ track(transfer);
144
+ break;
145
+ case "dragover":
146
+ // A drag that entered before the listener existed is on the page all the same.
147
+ depth = Math.max(depth, 1);
148
+ track(transfer);
149
+ break;
150
+ case "dragleave":
151
+ depth = Math.max(0, depth - 1);
152
+ if (depth === 0) show({ state: "none" });
153
+ break;
154
+ case "drop": {
155
+ depth = 0;
156
+ show({ state: "none" });
157
+ const files = Array.from(transfer.files ?? []);
158
+ const items =
159
+ files.length > 0 ? files.map((file) => ({ kind: "file", type: file.type })) : dragItems(transfer);
160
+ receive(files, verdictFor(items));
161
+ break;
162
+ }
163
+ }
164
+ };
165
+
166
+ for (const type of DRAG_EVENTS) view?.addEventListener(type, onDragEvent);
167
+
168
+ return {
169
+ element,
170
+ state: () => current,
171
+ destroy() {
172
+ for (const type of DRAG_EVENTS) view?.removeEventListener(type, onDragEvent);
173
+ depth = 0;
174
+ if (current !== null) {
175
+ current = null;
176
+ setState(null);
177
+ }
178
+ element.remove();
179
+ },
180
+ };
181
+ }