@formicoidea/labre-framework-uml 0.42.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 (114) hide show
  1. package/LICENSE +384 -0
  2. package/dist/actions.d.ts +430 -0
  3. package/dist/actions.js +724 -0
  4. package/dist/background.d.ts +153 -0
  5. package/dist/background.js +557 -0
  6. package/dist/board-hit.d.ts +137 -0
  7. package/dist/board-hit.js +191 -0
  8. package/dist/commands-manifest.d.ts +19 -0
  9. package/dist/commands-manifest.js +571 -0
  10. package/dist/commands.d.ts +4 -0
  11. package/dist/commands.js +983 -0
  12. package/dist/component.d.ts +521 -0
  13. package/dist/component.js +708 -0
  14. package/dist/consts.d.ts +365 -0
  15. package/dist/consts.js +463 -0
  16. package/dist/descriptor.d.ts +12 -0
  17. package/dist/descriptor.js +10 -0
  18. package/dist/drawio-decode.d.ts +32 -0
  19. package/dist/drawio-decode.js +96 -0
  20. package/dist/drawio-import.d.ts +133 -0
  21. package/dist/drawio-import.js +885 -0
  22. package/dist/edge-morph.d.ts +154 -0
  23. package/dist/edge-morph.js +153 -0
  24. package/dist/edge-styles.d.ts +78 -0
  25. package/dist/edge-styles.js +210 -0
  26. package/dist/effects.d.ts +9 -0
  27. package/dist/effects.js +6 -0
  28. package/dist/element-renderer.d.ts +64 -0
  29. package/dist/element-renderer.js +114 -0
  30. package/dist/element-view.d.ts +234 -0
  31. package/dist/element-view.js +430 -0
  32. package/dist/export.d.ts +61 -0
  33. package/dist/export.js +41 -0
  34. package/dist/filename.d.ts +17 -0
  35. package/dist/filename.js +26 -0
  36. package/dist/frame-tag.d.ts +70 -0
  37. package/dist/frame-tag.js +78 -0
  38. package/dist/grammar.d.ts +695 -0
  39. package/dist/grammar.js +1436 -0
  40. package/dist/import.d.ts +306 -0
  41. package/dist/import.js +1270 -0
  42. package/dist/index.d.ts +47 -0
  43. package/dist/index.js +67 -0
  44. package/dist/interchange.d.ts +125 -0
  45. package/dist/interchange.js +372 -0
  46. package/dist/keywords.d.ts +204 -0
  47. package/dist/keywords.js +361 -0
  48. package/dist/kinds.d.ts +73 -0
  49. package/dist/kinds.js +91 -0
  50. package/dist/legend.d.ts +30 -0
  51. package/dist/legend.js +255 -0
  52. package/dist/model.d.ts +862 -0
  53. package/dist/model.js +1316 -0
  54. package/dist/morph.d.ts +178 -0
  55. package/dist/morph.js +496 -0
  56. package/dist/node/act-glyphs.d.ts +30 -0
  57. package/dist/node/act-glyphs.js +142 -0
  58. package/dist/node/compartment-watcher.d.ts +107 -0
  59. package/dist/node/compartment-watcher.js +163 -0
  60. package/dist/node/fit.d.ts +85 -0
  61. package/dist/node/fit.js +129 -0
  62. package/dist/node/node-renderer.d.ts +49 -0
  63. package/dist/node/node-renderer.js +611 -0
  64. package/dist/node/node-view.d.ts +51 -0
  65. package/dist/node/node-view.js +89 -0
  66. package/dist/node/paint.d.ts +84 -0
  67. package/dist/node/paint.js +143 -0
  68. package/dist/node/sd-glyphs.d.ts +36 -0
  69. package/dist/node/sd-glyphs.js +79 -0
  70. package/dist/node/stm-glyphs.d.ts +22 -0
  71. package/dist/node/stm-glyphs.js +111 -0
  72. package/dist/node/tier-metrics.d.ts +38 -0
  73. package/dist/node/tier-metrics.js +45 -0
  74. package/dist/operators.d.ts +45 -0
  75. package/dist/operators.js +75 -0
  76. package/dist/plantuml-import.d.ts +113 -0
  77. package/dist/plantuml-import.js +1723 -0
  78. package/dist/plantuml.d.ts +94 -0
  79. package/dist/plantuml.js +985 -0
  80. package/dist/presets.d.ts +149 -0
  81. package/dist/presets.js +302 -0
  82. package/dist/profiles.d.ts +2 -0
  83. package/dist/profiles.js +358 -0
  84. package/dist/reading.d.ts +160 -0
  85. package/dist/reading.js +648 -0
  86. package/dist/roles.d.ts +257 -0
  87. package/dist/roles.js +997 -0
  88. package/dist/rules.d.ts +267 -0
  89. package/dist/rules.js +3090 -0
  90. package/dist/templates.d.ts +2 -0
  91. package/dist/templates.js +197 -0
  92. package/dist/toolbar/config.d.ts +228 -0
  93. package/dist/toolbar/config.js +833 -0
  94. package/dist/toolbar/icons.d.ts +440 -0
  95. package/dist/toolbar/icons.js +711 -0
  96. package/dist/toolbar/senior-tool.d.ts +11 -0
  97. package/dist/toolbar/senior-tool.js +21 -0
  98. package/dist/toolbar/uml-menu.d.ts +17 -0
  99. package/dist/toolbar/uml-menu.js +20 -0
  100. package/dist/toolbar/uml-senior-button.d.ts +18 -0
  101. package/dist/toolbar/uml-senior-button.js +22 -0
  102. package/dist/translations.d.ts +27 -0
  103. package/dist/translations.js +117 -0
  104. package/dist/view.d.ts +33 -0
  105. package/dist/view.js +236 -0
  106. package/dist/xmi-import.d.ts +174 -0
  107. package/dist/xmi-import.js +2268 -0
  108. package/dist/xmi.d.ts +61 -0
  109. package/dist/xmi.js +1860 -0
  110. package/dist/xml-reader.d.ts +157 -0
  111. package/dist/xml-reader.js +430 -0
  112. package/dist/xml.d.ts +72 -0
  113. package/dist/xml.js +96 -0
  114. package/package.json +38 -0
@@ -0,0 +1,833 @@
1
+ import { EdgelessCRUDIdentifier, generateElementId, legendToolbarAction, validationToolbarConfig, } from '@formicoidea/labre-core/blocks/surface';
2
+ import { UmlDiagramElementModel, UmlFragmentElementModel, UmlPartitionElementModel, UmlRegionElementModel, } from '@formicoidea/labre-core/model';
3
+ import { BOARD_ADD_BAND, BOARD_ORIENTATION_TOGGLE, BOARD_RESIZE_TOGGLE, commandMoreAction, TelemetryProvider, ToolbarModuleExtension, translateKey, } from '@formicoidea/labre-core/shared/services';
4
+ import { BlockFlavourIdentifier } from '@formicoidea/labre-core/std';
5
+ import { html, nothing } from 'lit';
6
+ import { UML_DIAGRAM_KIND_MENU, UML_FRAGMENT_OPERATOR_MENU, } from '../kinds.js';
7
+ import { umlExportPlantumlIcon, umlExportXmiIcon } from './icons.js';
8
+ const ResizeIcon = html `<svg
9
+ width="24"
10
+ height="24"
11
+ viewBox="0 0 24 24"
12
+ fill="none"
13
+ stroke="currentColor"
14
+ stroke-width="1.6"
15
+ stroke-linecap="round"
16
+ stroke-linejoin="round"
17
+ >
18
+ <path d="M9 5H5v4M15 19h4v-4" />
19
+ <path d="M5 5l6 6M19 19l-6-6" />
20
+ </svg>`;
21
+ /**
22
+ * The two glyphs the kind picker needs, drawn inline rather than pulled from
23
+ * `@labre/affine-components` — a dependency this package does not have and which
24
+ * is not worth adding for two paths. The same call the C4 board's row makes for
25
+ * its own, and the same call {@link ResizeIcon} above already makes.
26
+ */
27
+ const ChevronDownIcon = html `<svg
28
+ width="16"
29
+ height="16"
30
+ viewBox="0 0 24 24"
31
+ fill="none"
32
+ stroke="currentColor"
33
+ stroke-width="2"
34
+ stroke-linecap="round"
35
+ stroke-linejoin="round"
36
+ >
37
+ <path d="M6 9l6 6 6-6" />
38
+ </svg>`;
39
+ /** The tick on the kind in force — rendered at the END of the row. */
40
+ const CheckIcon = html `<svg
41
+ width="20"
42
+ height="20"
43
+ viewBox="0 0 24 24"
44
+ fill="none"
45
+ stroke="currentColor"
46
+ stroke-width="2"
47
+ stroke-linecap="round"
48
+ stroke-linejoin="round"
49
+ >
50
+ <path d="M5 12.5l4.5 4.5L19 7.5" />
51
+ </svg>`;
52
+ /**
53
+ * The selected UML diagram frame's contextual toolbar: the resize toggle, and
54
+ * the two exports in the "⋮".
55
+ *
56
+ * ## Why the exports sit in the ALWAYS-ON half
57
+ *
58
+ * `ActionPlacement.More` partitions an entry out of the row and into the
59
+ * overflow menu, and the entries it collects come from EVERY module contributing
60
+ * to this element — so a slice adds itself there without owning this file. The
61
+ * two interchange exports are the rarest thing anybody does to a frame, and the
62
+ * `y.` / `z.` id prefixes are the sort keys that keep them last, in that order:
63
+ * PlantUML is what an architect pastes into a renderer, XMI is what they hand to
64
+ * another tool. The generic board export (`z.z-export-svg`, contributed by the
65
+ * surface's wildcard module) sorts after both.
66
+ *
67
+ * Each entry is built by the toolbar service's `commandMoreAction`, the one
68
+ * shape every framework's "⋮" shares (`docs/adr/0010` M3): it invokes the
69
+ * registered command rather than restating what it does.
70
+ *
71
+ * They are declared HERE, in the always-on module, rather than beside the legend
72
+ * in the flag-gated one — which is exactly where the C4 board puts its own
73
+ * export. The two kinds of entry look alike and are gated by different
74
+ * mechanisms, and that is the point: the legend button CALLS an action directly,
75
+ * so nothing but the module's registration can hide it, while these ask the
76
+ * registry for a command that only the flag-gated half registers and hide
77
+ * themselves when it is not there. With the `uml` button off the row is the
78
+ * resize toggle alone either way, and nothing on it can be clicked into a no-op.
79
+ *
80
+ * Declaring them in the always-on module is what makes that guard the SINGLE
81
+ * thing deciding whether an export is offered — the same rule the palette, the
82
+ * catalogue and the agent read — instead of the guard plus a second registration
83
+ * that could one day disagree with it.
84
+ *
85
+ * There is no rename entry, and there is nothing missing: both UML frames edit
86
+ * their one word in place on a double-click (`element-view.ts`).
87
+ */
88
+ export const umlDiagramToolbarConfig = {
89
+ actions: [
90
+ {
91
+ id: 'a.toggle-resize',
92
+ tooltipWording: BOARD_RESIZE_TOGGLE,
93
+ icon: ResizeIcon,
94
+ active(ctx) {
95
+ const models = ctx.getSurfaceModelsByType(UmlDiagramElementModel);
96
+ return models.length > 0 && models.every(model => model.resizeEnabled);
97
+ },
98
+ run(ctx) {
99
+ const models = ctx.getSurfaceModelsByType(UmlDiagramElementModel);
100
+ if (!models.length)
101
+ return;
102
+ const enable = !models.every(model => model.resizeEnabled);
103
+ ctx.std.store.captureSync();
104
+ const crud = ctx.std.get(EdgelessCRUDIdentifier);
105
+ for (const model of models) {
106
+ crud.updateElement(model.id, { resizeEnabled: enable });
107
+ }
108
+ },
109
+ },
110
+ // …and, in the "⋮", the two things you do to a finished diagram rather than
111
+ // to the frame it is drawn on: take it away as a file.
112
+ commandMoreAction('y.export-plantuml', 'uml.exportPlantuml', 'com.labre.commands.uml.exportPlantuml', 'Export as PlantUML', umlExportPlantumlIcon),
113
+ commandMoreAction('z.export-xmi', 'uml.exportXmi', 'com.labre.commands.uml.exportXmi', 'Export as XMI', umlExportXmiIcon),
114
+ ],
115
+ when: (ctx) => ctx.getSurfaceModelsByType(UmlDiagramElementModel).length > 0,
116
+ };
117
+ export const umlDiagramToolbarExtension = ToolbarModuleExtension({
118
+ id: BlockFlavourIdentifier('affine:surface:umlDiagram'),
119
+ config: umlDiagramToolbarConfig,
120
+ });
121
+ /**
122
+ * The frame the kind picker is about, or `null`.
123
+ *
124
+ * ONE frame, deliberately: a kind is one statement about one sheet, and a
125
+ * selection spanning two frames has no honest current value to show — the same
126
+ * call the C4 level picker and the generic Validation dropdown both make.
127
+ */
128
+ function selectedDiagram(ctx) {
129
+ const models = ctx.getSurfaceModels();
130
+ if (models.length !== 1)
131
+ return null;
132
+ const [model] = models;
133
+ return model instanceof UmlDiagramElementModel ? model : null;
134
+ }
135
+ /**
136
+ * Put the frame on `option`, and report it.
137
+ *
138
+ * `captureSync` first, so one click is one undo — the rule every write on this
139
+ * toolbar follows. Written through {@link EdgelessCRUDIdentifier} rather than by
140
+ * assigning the field, because the heading is DERIVED from it: the CRUD write is
141
+ * what the surface observes, and the frame has to repaint its tag.
142
+ *
143
+ * A choice that changes nothing writes nothing and reports nothing — an undo
144
+ * checkpoint for a no-op is a click the user has to press twice to get back
145
+ * past.
146
+ *
147
+ * There is no "clear" here, unlike the C4 level picker: a UML frame's kind is
148
+ * required (`UmlDiagramElementModel.kind`), because Annex A's heading is
149
+ * `<kind> <name>` and a frame with no kind has no heading to draw.
150
+ */
151
+ function pickKind(ctx, option) {
152
+ const diagram = selectedDiagram(ctx);
153
+ if (!diagram)
154
+ return;
155
+ const previous = diagram.kind;
156
+ if (previous === option.kind)
157
+ return;
158
+ ctx.std.store.captureSync();
159
+ ctx.std
160
+ .get(EdgelessCRUDIdentifier)
161
+ .updateElement(diagram.id, { kind: option.kind });
162
+ // The same event the C4 level picker emits, with the same wire values: the
163
+ // two frameworks' "the author said which diagram this is" gesture is one
164
+ // metric on a dashboard, and `level` is the field it is reported in.
165
+ ctx.std.getOptional(TelemetryProvider)?.track('FrameworkViewLevelSet', {
166
+ page: 'whiteboard editor',
167
+ segment: 'element toolbar',
168
+ module: 'uml toolbox',
169
+ control: 'kind',
170
+ framework: 'uml',
171
+ level: option.kind,
172
+ previousLevel: previous,
173
+ });
174
+ }
175
+ /**
176
+ * The kind picker: which of UML's diagrams this frame draws.
177
+ *
178
+ * ## Why it is a DECLARED fact and not a rename
179
+ *
180
+ * A frame's name is free text — "Orders", "Checkout", whatever the author writes
181
+ * — and nothing in it says which kind of diagram it is. The kind is a second,
182
+ * small, closed statement, and picking one leaves the words alone: the author
183
+ * keeps their name, the heading gains its tag, and the exporters get a fact they
184
+ * can write into a file (`<kind>` is what tells PlantUML and XMI what they are
185
+ * looking at).
186
+ *
187
+ * ## Why it is flag-gated tooling
188
+ *
189
+ * Declaring a kind is deciding how the diagram is to be READ, which is exactly
190
+ * what `docs/adr/0009` calls tooling: with the `uml` flag off, a frame already
191
+ * carrying a kind keeps it written, keeps painting its heading and simply stops
192
+ * being offered the choice. Nothing a stored document needs to load or paint is
193
+ * behind it.
194
+ *
195
+ * ## Shape
196
+ *
197
+ * A dropdown: four mutually exclusive options with a current value are a menu,
198
+ * not four buttons competing for a toolbar's width. The trigger NAMES the kind
199
+ * in force, so a reader of the row knows what the sheet claims without opening
200
+ * anything.
201
+ */
202
+ const kindPickerAction = {
203
+ // After the legend (`b.`) and before the Validation dropdown (`z.`): the kind
204
+ // is a statement about the sheet, read before the level of requirement
205
+ // applied to it.
206
+ id: 'c.kind',
207
+ when: (ctx) => selectedDiagram(ctx) !== null,
208
+ content(ctx) {
209
+ const diagram = selectedDiagram(ctx);
210
+ if (!diagram)
211
+ return null;
212
+ const menuLabel = translateKey(ctx.std, UML_DIAGRAM_KIND_MENU.labelKey, UML_DIAGRAM_KIND_MENU.labelFallback);
213
+ const wordsFor = (option) => translateKey(ctx.std, option.labelKey, option.labelFallback);
214
+ // A frame carrying a kind this build does not know — a phase-2 value on a
215
+ // phase-1 build, an import — shows the menu's own heading rather than
216
+ // silently reading as a class diagram.
217
+ const current = UML_DIAGRAM_KIND_MENU.options.find(option => option.kind === diagram.kind);
218
+ const options = UML_DIAGRAM_KIND_MENU.options.map(option => {
219
+ const selected = option.kind === diagram.kind;
220
+ return html `<editor-menu-action
221
+ data-option
222
+ data-testid="uml-kind-option"
223
+ data-kind=${option.kind}
224
+ data-selected=${selected ? 'true' : nothing}
225
+ aria-label=${wordsFor(option)}
226
+ aria-pressed=${selected}
227
+ @click=${() => pickKind(ctx, option)}
228
+ >
229
+ <span class="label">${wordsFor(option)}</span>
230
+ ${selected ? CheckIcon : nothing}
231
+ </editor-menu-action>`;
232
+ });
233
+ return html `<editor-menu-button
234
+ data-testid="uml-kind-entry"
235
+ .contentPadding=${'8px'}
236
+ .button=${html `
237
+ <editor-icon-button
238
+ data-testid="uml-kind-button"
239
+ aria-label=${menuLabel}
240
+ .tooltip=${menuLabel}
241
+ .justify=${'space-between'}
242
+ .labelHeight=${'20px'}
243
+ >
244
+ <span class="label"
245
+ >${current === undefined ? menuLabel : wordsFor(current)}</span
246
+ >
247
+ ${ChevronDownIcon}
248
+ </editor-icon-button>
249
+ `}
250
+ >
251
+ <div
252
+ data-testid="uml-kind-menu"
253
+ data-orientation="vertical"
254
+ data-size="large"
255
+ >
256
+ <div
257
+ role="group"
258
+ aria-label=${menuLabel}
259
+ style="display: flex; flex-direction: column;"
260
+ >
261
+ ${options}
262
+ </div>
263
+ </div>
264
+ </editor-menu-button>`;
265
+ },
266
+ };
267
+ /**
268
+ * The legend button — the flag-gated half of the row.
269
+ *
270
+ * ## The legend is a BUTTON, not a command
271
+ *
272
+ * Every other UML gesture is a registered `CommandDescriptor`, which is the
273
+ * bottleneck `docs/adr/0008` asks for. The legend is the arbitrated exception,
274
+ * carried over from C4 and the Context Map (PO arbitration, 27/08/2026):
275
+ * generating one belongs to a frame you have SELECTED and to nothing else — it
276
+ * is not an artefact to pick off a palette, and an entry in a catalogue of
277
+ * things UML draws would offer it to a user with no diagram in front of them. So
278
+ * there is no `uml.legend` command, and this button is the only way to reach it.
279
+ *
280
+ * The cost is the one the bottleneck exists to avoid: the telemetry it owes is
281
+ * emitted outside `runCommand`. It is emitted ONCE, by the shared factory
282
+ * (`legend-toolbar.ts`), so the seven copies of those wire values that used to
283
+ * sit in seven files can no longer drift apart.
284
+ *
285
+ * `b.` — the factory's default — sorts it after the resize toggle, so the two
286
+ * modules render as the one row a user sees rather than in registration order.
287
+ */
288
+ export const umlLegendToolbarConfig = {
289
+ actions: [
290
+ // The SHARED factory, not a UML copy of it: the button, the first selected
291
+ // frame and no other, the scan, the placement, the box and the one
292
+ // `FrameworkLegendCreated` emission all live in the surface block
293
+ // (`legend-toolbar.ts`, `docs/adr/0026`). UML hands it the frame model to
294
+ // look for, whose commands to read the rows off, and the two historical wire
295
+ // values — `module` is `uml toolbox` here where the other six say
296
+ // `<framework> toolbar`, so it is passed rather than derived.
297
+ legendToolbarAction({
298
+ Model: UmlDiagramElementModel,
299
+ owner: 'uml',
300
+ framework: 'uml',
301
+ module: 'uml toolbox',
302
+ }),
303
+ ],
304
+ when: (ctx) => ctx.getSurfaceModelsByType(UmlDiagramElementModel).length > 0,
305
+ };
306
+ /**
307
+ * The frame's flag-gated row, WHOLE: the legend button and the kind picker, in
308
+ * ONE module.
309
+ *
310
+ * ## Why they cannot be two modules
311
+ *
312
+ * `renderToolbar` merges exactly four slots per element — `<flavour>`,
313
+ * `custom:<flavour>`, and the two `affine:surface:*` wildcards — and
314
+ * `ToolbarModuleExtension` binds by DI variant, so a second module claiming
315
+ * `custom:affine:surface:umlDiagram` throws `DuplicateServiceDefinitionError`
316
+ * before the editor finishes setting up. Two slots, and UML has three things to
317
+ * put on a selected frame: the resize toggle (always-on, `<flavour>`), the
318
+ * legend and the kind.
319
+ *
320
+ * Both of the latter are gated by the same flag and appear together or not at
321
+ * all, so one module is the honest grouping rather than a workaround: `uml` off
322
+ * takes away the gesture that CREATES legend elements and the choice of which
323
+ * kind of diagram this sheet is, and leaves the stored frame its handles, its
324
+ * heading and everything already drawn on it.
325
+ *
326
+ * Sorting keeps the row readable across the merge — `b.legend`, `c.kind`, then
327
+ * `z.validation` from {@link validationToolbarConfig} — so the user sees resize,
328
+ * legend, the diagram's KIND, then the level of requirement, whatever order the
329
+ * modules were registered in.
330
+ */
331
+ export const umlDiagramToolingToolbarConfig = {
332
+ actions: [
333
+ ...umlLegendToolbarConfig.actions,
334
+ // Which of the four UML diagrams this sheet draws — a fact about the frame,
335
+ // written on the frame, read by `uml.not-admissible-on-kind`.
336
+ kindPickerAction,
337
+ // The generic dropdown, not a UML variant of it: the config names no
338
+ // framework — it reads the registered rules and profiles — so this is the
339
+ // very same object wardley, bpmn, the context map and C4 register.
340
+ ...validationToolbarConfig.actions,
341
+ ],
342
+ when: umlLegendToolbarConfig.when,
343
+ };
344
+ export const umlDiagramToolingToolbarExtension = ToolbarModuleExtension({
345
+ id: BlockFlavourIdentifier('custom:affine:surface:umlDiagram'),
346
+ config: umlDiagramToolingToolbarConfig,
347
+ });
348
+ /**
349
+ * The SUBJECT claims no toolbar flavour at all, and that is a decision rather
350
+ * than an omission.
351
+ *
352
+ * The same arbitration the C4 boundary records (PO, 28/08/2026): **the sheet
353
+ * alone arbitrates the checklist.** One diagram, one level of requirement, one
354
+ * place to set it — a second picker on a frame drawn INSIDE the first is a way
355
+ * to make a diagram disagree with itself, and no reader could tell which answer
356
+ * it was being held to. `inheritChosenProfiles` makes a frame that names no
357
+ * profile inherit the innermost containing frame's choice, so a subject drawn on
358
+ * a diagram set to Review checklist is itself on Review checklist.
359
+ *
360
+ * It has no kind picker either, and for a plainer reason: §18.1.4 gives the
361
+ * subject one rectangle with one name and nothing to choose between.
362
+ */
363
+ /* ── Phase 2: the two banded frames of the behaviour diagrams ───────────── */
364
+ /**
365
+ * The two arrows of the orientation toggle — the bands turned a quarter turn.
366
+ *
367
+ * Drawn inline, as every other glyph on this row is and for the same reason:
368
+ * `@labre/affine-components` is not a dependency of this package and two paths
369
+ * are not worth making it one.
370
+ */
371
+ const OrientationIcon = html `<svg
372
+ width="24"
373
+ height="24"
374
+ viewBox="0 0 24 24"
375
+ fill="none"
376
+ stroke="currentColor"
377
+ stroke-width="1.6"
378
+ stroke-linecap="round"
379
+ stroke-linejoin="round"
380
+ >
381
+ <path d="M4 4h5v16H4zM15 4h5v16h-5z" />
382
+ <path d="M11.5 9.5l1 1 1-1M12.5 10.5v3" />
383
+ </svg>`;
384
+ /**
385
+ * The resize toggle, for a frame that is neither the diagram nor the subject.
386
+ *
387
+ * Written once for the two phase-2 backgrounds rather than a third time inline:
388
+ * a partition and a region toggle the very same declared field on the very same
389
+ * primitive, and the only thing that differs between their two rows is which
390
+ * model class the selection is filtered by.
391
+ *
392
+ * `a.` sorts it first, exactly as it does on the diagram's row, so the merged
393
+ * row reads the same wherever the modules were registered.
394
+ */
395
+ function resizeToggle(klass) {
396
+ return {
397
+ id: 'a.toggle-resize',
398
+ tooltipWording: BOARD_RESIZE_TOGGLE,
399
+ icon: ResizeIcon,
400
+ active(ctx) {
401
+ const models = ctx.getSurfaceModelsByType(klass);
402
+ return models.length > 0 && models.every(model => model.resizeEnabled);
403
+ },
404
+ run(ctx) {
405
+ const models = ctx.getSurfaceModelsByType(klass);
406
+ if (!models.length)
407
+ return;
408
+ const enable = !models.every(model => model.resizeEnabled);
409
+ ctx.std.store.captureSync();
410
+ const crud = ctx.std.get(EdgelessCRUDIdentifier);
411
+ for (const model of models) {
412
+ crud.updateElement(model.id, { resizeEnabled: enable });
413
+ }
414
+ },
415
+ };
416
+ }
417
+ /**
418
+ * The ACTIVITY PARTITION's row: the resize toggle, and the one thing a swimlane
419
+ * can be said differently — which way its bands run.
420
+ *
421
+ * ## Why the orientation toggle is ALWAYS-ON
422
+ *
423
+ * It writes a stored field, so it is not obviously tooling — and that is exactly
424
+ * the question `docs/adr/0009` makes people ask. The answer is the one the
425
+ * resize toggle already gives: this is a GESTURE ON A SELECTED ELEMENT, not a
426
+ * way to put a new one on the canvas. A partition drawn while the `uml` flag was
427
+ * on must stay usable with it off — moved, stretched, renamed, and turned the
428
+ * right way round for the sheet it sits on — because every one of those is
429
+ * something the author does to a thing that is already there.
430
+ *
431
+ * The line the ADR actually draws is between the ways to CREATE and the ways to
432
+ * work with what exists. `resizeEnabled` writes content too (it is a field of
433
+ * the model, undoable like any other); so does the rename on a double-click; so
434
+ * does dragging the frame. What the flag takes away is the button that adds a
435
+ * partition in the first place, and the menu it lives in.
436
+ *
437
+ * ## Why a toggle and not a two-option dropdown
438
+ *
439
+ * §15.6.4 gives the orientation no meaning: a partition drawn in columns and the
440
+ * same partition drawn in rows say the same thing, and the choice is made
441
+ * against the sheet — a wide activity diagram wants rows, a tall one wants
442
+ * columns. Two mutually exclusive values with no semantics between them are a
443
+ * flip, and a dropdown would spend a menu on a question whose two answers are
444
+ * "this way" and "the other way". The C4 level picker is a dropdown because
445
+ * there are four levels and each MEANS something.
446
+ *
447
+ * `active` reports HORIZONTAL — the value that is not the default — so the
448
+ * button reads as pressed exactly when the author has turned the bands.
449
+ *
450
+ * ponytail: the flip writes `orientation` and nothing else. It does not swap the
451
+ * frame's width and height, so a tall partition turned horizontal is a wide band
452
+ * stack in a tall box until the author drags it — the notation has nothing to
453
+ * say about the box, and guessing a new one is a gesture the user did not ask
454
+ * for and cannot half-undo.
455
+ */
456
+ export const umlPartitionToolbarConfig = {
457
+ actions: [
458
+ resizeToggle(UmlPartitionElementModel),
459
+ {
460
+ // After the resize toggle, before anything a later phase hangs off the
461
+ // `custom:` twin — the row reads resize, then orientation.
462
+ id: 'b.orientation',
463
+ tooltipWording: BOARD_ORIENTATION_TOGGLE,
464
+ icon: OrientationIcon,
465
+ active(ctx) {
466
+ const models = ctx.getSurfaceModelsByType(UmlPartitionElementModel);
467
+ return (models.length > 0 &&
468
+ models.every(model => model.orientation === 'horizontal'));
469
+ },
470
+ run(ctx) {
471
+ const models = ctx.getSurfaceModelsByType(UmlPartitionElementModel);
472
+ if (!models.length)
473
+ return;
474
+ // One click, one answer for the whole selection: turn them all
475
+ // horizontal unless they already all are, in which case turn them back.
476
+ // The same rule the resize toggle follows, and the reason a mixed
477
+ // selection converges instead of alternating.
478
+ const next = models.every(model => model.orientation === 'horizontal')
479
+ ? 'vertical'
480
+ : 'horizontal';
481
+ ctx.std.store.captureSync();
482
+ const crud = ctx.std.get(EdgelessCRUDIdentifier);
483
+ for (const model of models) {
484
+ crud.updateElement(model.id, { orientation: next });
485
+ }
486
+ },
487
+ },
488
+ ],
489
+ when: (ctx) => ctx.getSurfaceModelsByType(UmlPartitionElementModel).length > 0,
490
+ };
491
+ export const umlPartitionToolbarExtension = ToolbarModuleExtension({
492
+ id: BlockFlavourIdentifier('affine:surface:umlPartition'),
493
+ config: umlPartitionToolbarConfig,
494
+ });
495
+ /**
496
+ * The REGION's row: the resize toggle, and nothing else.
497
+ *
498
+ * A region is a composite state's inside (§14.2.4) — one rounded rectangle with
499
+ * one name compartment and sub-states drawn in it. It has no orientation: its
500
+ * band is its name compartment, which §14.2.4 puts at the top and nowhere else,
501
+ * and there is no second reading of the same picture to flip to. So the row is
502
+ * the one entry every framework background carries, and the rename is on the
503
+ * band under a double-click (`element-view.ts`) as it is on all the others.
504
+ *
505
+ * Registered as its own module rather than folded into the partition's because
506
+ * `renderToolbar` merges BY FLAVOUR: these are two element types, so two rows.
507
+ */
508
+ export const umlRegionToolbarConfig = {
509
+ actions: [resizeToggle(UmlRegionElementModel)],
510
+ when: (ctx) => ctx.getSurfaceModelsByType(UmlRegionElementModel).length > 0,
511
+ };
512
+ export const umlRegionToolbarExtension = ToolbarModuleExtension({
513
+ id: BlockFlavourIdentifier('affine:surface:umlRegion'),
514
+ config: umlRegionToolbarConfig,
515
+ });
516
+ /* ── Phase 3: the COMBINED FRAGMENT's two rows (§17.6.4) ────────────────── */
517
+ /**
518
+ * The COMBINED FRAGMENT's always-on row: the resize toggle, and nothing else.
519
+ *
520
+ * The same argument the region's makes, and the same one entry: a fragment is a
521
+ * rectangle drawn round part of an interaction, and a document that holds one
522
+ * must keep it movable and stretchable with the UML button switched off
523
+ * (`docs/adr/0009`). What the flag takes away is the two gestures below.
524
+ */
525
+ export const umlFragmentToolbarConfig = {
526
+ actions: [resizeToggle(UmlFragmentElementModel)],
527
+ when: (ctx) => ctx.getSurfaceModelsByType(UmlFragmentElementModel).length > 0,
528
+ };
529
+ export const umlFragmentToolbarExtension = ToolbarModuleExtension({
530
+ id: BlockFlavourIdentifier('affine:surface:umlFragment'),
531
+ config: umlFragmentToolbarConfig,
532
+ });
533
+ /**
534
+ * The fragments an operand gesture acts on: every one of the current selection
535
+ * that is not locked, on an editable document.
536
+ *
537
+ * Lifted from `bpmnPoolsForLaneEdit`, filter for filter and for the same
538
+ * reasons: EVERY selected fragment rather than the single one (a gesture that
539
+ * says what it did on each beats an entry that vanishes on a lasso), and the
540
+ * read-only and lock filters because this is about to write.
541
+ */
542
+ function umlFragmentsForOperandEdit(ctx) {
543
+ if (ctx.std.store.readonly)
544
+ return [];
545
+ return ctx
546
+ .getSurfaceModelsByType(UmlFragmentElementModel)
547
+ .filter(model => !model.isLocked());
548
+ }
549
+ /** A fragment's operand list, as an array whatever the document holds. */
550
+ const operandsOf = (model) => Array.isArray(model.operands) ? model.operands : [];
551
+ /**
552
+ * Append an operand to every selected fragment — the second branch of an
553
+ * `alt`, the `else` of a `par`, the next case of a fragment that has one
554
+ * (§17.6.4).
555
+ *
556
+ * ## Why the first click writes TWO
557
+ *
558
+ * Because §17.6.4's picture is unambiguous and BPMN's lane gesture is the wrong
559
+ * analogy here. A pool with no lanes is a pool; a pool with one lane is a pool
560
+ * with a lane drawn in it, and both are legal BPMN. A combined fragment with no
561
+ * `operands` is the ONE-operand fragment §17.6.4 draws — a box with a tag and
562
+ * no dashed rule — and what an author asks for by pressing this is the SECOND
563
+ * branch, with the separator between the two. Writing one zone would divide the
564
+ * fragment into a single band and draw nothing at all.
565
+ *
566
+ * So the first press seeds the operand that was implicit plus the new one, and
567
+ * every press after it appends a single band. Equal shares throughout: the
568
+ * average of what is there is the weight that leaves the existing operands the
569
+ * same size relative to each other, and gives the newcomer the room a typical
570
+ * one has.
571
+ *
572
+ * ## No guard is invented, and the operator is not restated
573
+ *
574
+ * A new band arrives with no `name` at all, which is the decision the model
575
+ * records for the fragment's own guard and the same one for the same reason:
576
+ * §17.6.4 writes a condition in an operand's corner only where there is one,
577
+ * and a band born carrying `[condition]` would be a guard the author never
578
+ * wrote. The first operand, made explicit here, carries whatever guard the
579
+ * fragment already had — nothing is invented and nothing is lost.
580
+ *
581
+ * Nothing here touches `operator` either: which kind of fragment this is is one
582
+ * statement about the whole box, and the picker beside this button is where it
583
+ * is made.
584
+ *
585
+ * ## Nothing moves
586
+ *
587
+ * Messages already drawn inside the fragment do NOT move. Membership here is
588
+ * geometric — an occurrence is "in" an operand because it falls in that band —
589
+ * so the bands simply redraw around what is there, and the line an author drew
590
+ * still lands where they drew it. The same promise `removeBpmnLane` makes, for
591
+ * the same reason.
592
+ */
593
+ function addUmlOperand(ctx) {
594
+ const fragments = umlFragmentsForOperandEdit(ctx);
595
+ if (fragments.length === 0)
596
+ return;
597
+ // Before the writes: one capture for the whole gesture is what makes several
598
+ // fragments take their operand in a single undo step.
599
+ ctx.std.store.captureSync();
600
+ const crud = ctx.std.get(EdgelessCRUDIdentifier);
601
+ for (const model of fragments) {
602
+ const operands = operandsOf(model);
603
+ const size = operands.length
604
+ ? operands.reduce((sum, operand) => sum + operand.size, 0) /
605
+ operands.length
606
+ : 1;
607
+ // The implicit first operand, made explicit — see the header. A fragment
608
+ // that already declares its zones simply gains one.
609
+ const existing = operands.length
610
+ ? operands
611
+ : [{ id: generateElementId(), name: model.name || undefined, size }];
612
+ crud.updateElement(model.id, {
613
+ // …and the guard is MOVED, never copied. The declared `guard` zone and
614
+ // operand zero's label are anchored in the same corner (`background.ts`),
615
+ // so a fragment left carrying both would paint the condition twice, one
616
+ // string over the other. One guard, one place.
617
+ name: '',
618
+ operands: [...existing, { id: generateElementId(), size }],
619
+ });
620
+ }
621
+ }
622
+ /**
623
+ * The two arrows and the rule between them: an operand being added under the
624
+ * one that is there.
625
+ *
626
+ * Drawn inline like every other glyph on this row, for the reason
627
+ * {@link ResizeIcon} and {@link OrientationIcon} give — this package does not
628
+ * depend on `@labre/affine-components`, and one path is not worth making it.
629
+ */
630
+ const AddOperandIcon = html `<svg
631
+ width="24"
632
+ height="24"
633
+ viewBox="0 0 24 24"
634
+ fill="none"
635
+ stroke="currentColor"
636
+ stroke-width="1.6"
637
+ stroke-linecap="round"
638
+ stroke-linejoin="round"
639
+ >
640
+ <path d="M4 4h16v16H4z" />
641
+ <path d="M4 12h16" stroke-dasharray="2.5 2.5" />
642
+ <path d="M12 14.5v4M10 16.5h4" />
643
+ </svg>`;
644
+ /**
645
+ * Which kind of combined fragment this is — the operator picker (§17.6.4).
646
+ *
647
+ * The same control as the diagram frame's kind picker, on the other frame, and
648
+ * it is the same argument: a closed set of mutually exclusive values with a
649
+ * current one is a MENU, not a row of buttons, and the trigger names the value
650
+ * in force so a reader of the row knows what the box claims without opening
651
+ * anything. `UML_FRAGMENT_OPERATOR_MENU` is DATA (`operators.ts`), walked by the
652
+ * translation manifest exactly as the diagram's table is.
653
+ *
654
+ * ## Why the operator is a picker and not a morph
655
+ *
656
+ * A morph retypes an ELEMENT — it rewrites the props that make a rectangle look
657
+ * like an interface rather than a class. An operator changes one field of one
658
+ * element and changes no prop at all: the box is the same box, the border is
659
+ * the same border, and what moves is the word in the pentagon. There is nothing
660
+ * for a family declaration to declare.
661
+ *
662
+ * ## `ref` is in the menu, and that is deliberate
663
+ *
664
+ * An interaction use is this element with `ref` in the tag (§17.7.4), so it is
665
+ * one of the values the field takes and hiding it here would mean an author who
666
+ * drew an `alt` and meant a `ref` has to delete the box and draw another. The
667
+ * command that creates one is separate because it is a different modelling act
668
+ * (`actions.ts`); the field is one field.
669
+ */
670
+ const operatorPickerAction = {
671
+ // After the add-operand button (`b.`) and before the Validation dropdown
672
+ // (`z.`), which is the diagram frame's own ordering: what the frame IS, then
673
+ // the level of requirement applied to it.
674
+ id: 'c.operator',
675
+ when: (ctx) => selectedFragment(ctx) !== null,
676
+ content(ctx) {
677
+ const fragment = selectedFragment(ctx);
678
+ if (!fragment)
679
+ return null;
680
+ const menuLabel = translateKey(ctx.std, UML_FRAGMENT_OPERATOR_MENU.labelKey, UML_FRAGMENT_OPERATOR_MENU.labelFallback);
681
+ const wordsFor = (option) => translateKey(ctx.std, option.labelKey, option.labelFallback);
682
+ // A fragment carrying an operator this build does not know — an import, a
683
+ // later phase's value — shows the menu's own heading rather than silently
684
+ // reading as an `alt`.
685
+ const current = UML_FRAGMENT_OPERATOR_MENU.options.find(option => option.operator === fragment.operator);
686
+ const options = UML_FRAGMENT_OPERATOR_MENU.options.map(option => {
687
+ const selected = option.operator === fragment.operator;
688
+ return html `<editor-menu-action
689
+ data-option
690
+ data-testid="uml-operator-option"
691
+ data-operator=${option.operator}
692
+ data-selected=${selected ? 'true' : nothing}
693
+ aria-label=${wordsFor(option)}
694
+ aria-pressed=${selected}
695
+ @click=${() => pickOperator(ctx, option)}
696
+ >
697
+ <span class="label">${wordsFor(option)}</span>
698
+ ${selected ? CheckIcon : nothing}
699
+ </editor-menu-action>`;
700
+ });
701
+ return html `<editor-menu-button
702
+ data-testid="uml-operator-entry"
703
+ .contentPadding=${'8px'}
704
+ .button=${html `
705
+ <editor-icon-button
706
+ data-testid="uml-operator-button"
707
+ aria-label=${menuLabel}
708
+ .tooltip=${menuLabel}
709
+ .justify=${'space-between'}
710
+ .labelHeight=${'20px'}
711
+ >
712
+ <span class="label"
713
+ >${current === undefined ? menuLabel : wordsFor(current)}</span
714
+ >
715
+ ${ChevronDownIcon}
716
+ </editor-icon-button>
717
+ `}
718
+ >
719
+ <div
720
+ data-testid="uml-operator-menu"
721
+ data-orientation="vertical"
722
+ data-size="large"
723
+ >
724
+ <div
725
+ role="group"
726
+ aria-label=${menuLabel}
727
+ style="display: flex; flex-direction: column;"
728
+ >
729
+ ${options}
730
+ </div>
731
+ </div>
732
+ </editor-menu-button>`;
733
+ },
734
+ };
735
+ /**
736
+ * The fragment the operator picker is about, or `null` — ONE of them, for the
737
+ * reason {@link selectedDiagram} gives: an operator is one statement about one
738
+ * box, and a selection spanning two has no honest current value to show.
739
+ */
740
+ function selectedFragment(ctx) {
741
+ const models = ctx.getSurfaceModels();
742
+ if (models.length !== 1)
743
+ return null;
744
+ const [model] = models;
745
+ return model instanceof UmlFragmentElementModel ? model : null;
746
+ }
747
+ /**
748
+ * Put the fragment on `option`, and report it.
749
+ *
750
+ * The same three rules `pickKind` follows: `captureSync` first so one click is
751
+ * one undo, written through {@link EdgelessCRUDIdentifier} because the tag is
752
+ * DERIVED from the field and the surface has to repaint it, and a choice that
753
+ * changes nothing writes nothing and reports nothing.
754
+ *
755
+ * The guard is NOT rewritten. `name` holds the author's own `[condition]` (or
756
+ * the name of the interaction a `ref` points at), and a picker that cleared it
757
+ * on the way from `alt` to `opt` would be taking away words somebody typed —
758
+ * the very rule the node morph's `afterMorph` is written round.
759
+ */
760
+ function pickOperator(ctx, option) {
761
+ const fragment = selectedFragment(ctx);
762
+ if (!fragment)
763
+ return;
764
+ const previous = fragment.operator;
765
+ if (previous === option.operator)
766
+ return;
767
+ ctx.std.store.captureSync();
768
+ ctx.std
769
+ .get(EdgelessCRUDIdentifier)
770
+ .updateElement(fragment.id, { operator: option.operator });
771
+ // The same event and the same wire values the diagram's kind picker emits:
772
+ // "the author said which kind of thing this frame is" is one metric on a
773
+ // dashboard, and `level` is the field it is reported in. `control` is what
774
+ // separates the two — `kind` for the sheet, `operator` for the fragment.
775
+ ctx.std.getOptional(TelemetryProvider)?.track('FrameworkViewLevelSet', {
776
+ page: 'whiteboard editor',
777
+ segment: 'element toolbar',
778
+ module: 'uml toolbox',
779
+ control: 'operator',
780
+ framework: 'uml',
781
+ level: option.operator,
782
+ previousLevel: previous,
783
+ });
784
+ }
785
+ /**
786
+ * The fragment's FLAG-GATED row: add an operand, pick the operator, and the
787
+ * generic Validation dropdown.
788
+ *
789
+ * ## Why the two gestures are here and the resize toggle is not
790
+ *
791
+ * The line `docs/adr/0009` draws is between the ways to CREATE and the ways to
792
+ * work with what already exists, and these two fall on the creating side of it:
793
+ * adding an operand puts a band on the canvas that was not there (the same
794
+ * thing `bpmn.addLane` does, and BPMN gates it with its own flag through the
795
+ * command registry), and the operator picker declares how the drawing is to be
796
+ * READ — which is what the ADR calls tooling in as many words, and where the
797
+ * diagram frame's kind picker already sits.
798
+ *
799
+ * With the `uml` flag off a stored fragment keeps its operator written, keeps
800
+ * painting its pentagon and its dashed rules, keeps every band it has and keeps
801
+ * its handles. What goes away is the button that adds a band and the menu that
802
+ * changes the word.
803
+ *
804
+ * ## One module, for the reason the diagram's row is one module
805
+ *
806
+ * `renderToolbar` merges four slots per element and `ToolbarModuleExtension`
807
+ * binds by DI variant, so a second module claiming
808
+ * `custom:affine:surface:umlFragment` throws before the editor finishes setting
809
+ * up. Both entries are gated by the same flag and appear together or not at
810
+ * all, so one module is the honest grouping rather than a workaround.
811
+ */
812
+ export const umlFragmentToolingToolbarConfig = {
813
+ actions: [
814
+ {
815
+ // After the resize toggle (`a.`, from the always-on module) and before
816
+ // the operator picker — the row reads resize, add a branch, say which
817
+ // kind of branching this is.
818
+ id: 'b.add-operand',
819
+ tooltipWording: BOARD_ADD_BAND,
820
+ icon: AddOperandIcon,
821
+ run: addUmlOperand,
822
+ },
823
+ operatorPickerAction,
824
+ // The generic dropdown, not a UML variant of it — the very same object
825
+ // every framework frame registers.
826
+ ...validationToolbarConfig.actions,
827
+ ],
828
+ when: (ctx) => ctx.getSurfaceModelsByType(UmlFragmentElementModel).length > 0,
829
+ };
830
+ export const umlFragmentToolingToolbarExtension = ToolbarModuleExtension({
831
+ id: BlockFlavourIdentifier('custom:affine:surface:umlFragment'),
832
+ config: umlFragmentToolingToolbarConfig,
833
+ });