@formicoidea/labre-framework-bpmn 0.32.0 → 0.33.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 (47) hide show
  1. package/dist/actions.d.ts +202 -6
  2. package/dist/actions.js +421 -43
  3. package/dist/background.d.ts +2 -0
  4. package/dist/background.js +158 -0
  5. package/dist/commands.js +496 -5
  6. package/dist/consts.d.ts +157 -3
  7. package/dist/consts.js +192 -3
  8. package/dist/element-renderer.d.ts +10 -4
  9. package/dist/element-renderer.js +14 -55
  10. package/dist/element-view.d.ts +100 -8
  11. package/dist/element-view.js +249 -30
  12. package/dist/export.d.ts +277 -0
  13. package/dist/export.js +1802 -0
  14. package/dist/facts.d.ts +48 -0
  15. package/dist/facts.js +127 -0
  16. package/dist/import.d.ts +44 -0
  17. package/dist/import.js +1440 -0
  18. package/dist/index.d.ts +12 -0
  19. package/dist/index.js +44 -0
  20. package/dist/interchange.d.ts +109 -0
  21. package/dist/interchange.js +191 -0
  22. package/dist/morph.d.ts +61 -0
  23. package/dist/morph.js +118 -0
  24. package/dist/node/node-renderer.d.ts +0 -9
  25. package/dist/node/node-renderer.js +294 -17
  26. package/dist/pool-hit.d.ts +98 -0
  27. package/dist/pool-hit.js +130 -0
  28. package/dist/presets.d.ts +114 -0
  29. package/dist/presets.js +232 -0
  30. package/dist/profiles.d.ts +2 -0
  31. package/dist/profiles.js +189 -0
  32. package/dist/roles.d.ts +96 -0
  33. package/dist/roles.js +410 -0
  34. package/dist/rules.d.ts +199 -0
  35. package/dist/rules.js +1539 -0
  36. package/dist/templates/index.js +116 -9
  37. package/dist/toolbar/bpmn-senior-button.js +8 -2
  38. package/dist/toolbar/config.d.ts +27 -2
  39. package/dist/toolbar/config.js +86 -2
  40. package/dist/toolbar/icons.d.ts +67 -0
  41. package/dist/toolbar/icons.js +141 -0
  42. package/dist/toolbar/senior-tool.js +1 -0
  43. package/dist/translations.d.ts +3 -1
  44. package/dist/translations.js +8 -3
  45. package/dist/view.d.ts +6 -2
  46. package/dist/view.js +68 -5
  47. package/package.json +2 -2
package/dist/actions.js CHANGED
@@ -1,21 +1,20 @@
1
- import { DefaultTool } from '@formicoidea/labre-core/blocks/surface';
1
+ import { DefaultTool, EdgelessCRUDIdentifier, generateElementId, materializeInterchangeImport, reportInterchangeImport, runInterchangeImportFile, } from '@formicoidea/labre-core/blocks/surface';
2
2
  import { ConnectorTool } from '@formicoidea/labre-core/gfx/connector';
3
- import { ConnectorMode, FontFamily, PointStyle, ShapeStyle, StrokeStyle, TextFitMode, } from '@formicoidea/labre-core/model';
4
- import { EditPropsStore } from '@formicoidea/labre-core/shared/services';
3
+ import { BpmnPoolElementModel, ConnectorMode, PointStyle, StrokeStyle, } from '@formicoidea/labre-core/model';
4
+ import { NotificationProvider, translateKey, } from '@formicoidea/labre-core/shared/services';
5
+ import { downloadBlob } from '@formicoidea/labre-core/shared/utils';
5
6
  import { Bound } from '@formicoidea/labre-core/global/gfx';
6
7
  import { GfxControllerIdentifier } from '@formicoidea/labre-core/std/gfx';
7
- import { END_WIDTH, EVENT_END, EVENT_START, INNER_FONT_SIZE, NEUTRAL_STROKE, NODE_FILL, NODE_LABEL, NODE_SIZE, NODE_STROKE_WIDTH, SEQUENCE_STROKE, SEQUENCE_WIDTH, START_WIDTH, TASK_RADIUS, } from './consts.js';
8
- /** Per-kind native shape + accent presets (style C). */
9
- const NODE_PRESETS = {
10
- startEvent: { shapeType: 'ellipse', stroke: EVENT_START, width: START_WIDTH },
11
- endEvent: { shapeType: 'ellipse', stroke: EVENT_END, width: END_WIDTH },
12
- task: { shapeType: 'rect', stroke: NEUTRAL_STROKE, width: NODE_STROKE_WIDTH },
13
- gatewayExclusive: {
14
- shapeType: 'diamond',
15
- stroke: NEUTRAL_STROKE,
16
- width: NODE_STROKE_WIDTH,
17
- },
18
- };
8
+ import { ASSOCIATION_STROKE, ASSOCIATION_WIDTH, MESSAGE_STROKE, MESSAGE_WIDTH, NODE_LABEL, NODE_SIZE, SEQUENCE_STROKE, SEQUENCE_WIDTH, } from './consts.js';
9
+ import { BPMN_FORMAT_ID } from './export.js';
10
+ import { BPMN_SVG_IMPORT, BPMN_XML_EXPORT, BPMN_XML_FORMAT, BPMN_XML_IMPORT, bpmnBoardFrom, bpmnSafeFilename, } from './interchange.js';
11
+ import { bpmnNodeProps } from './presets.js';
12
+ import { BPMN_ROLE } from './roles.js';
13
+ /**
14
+ * Standalone creation/activation actions for the BPMN toolbox — lifted out of
15
+ * `toolbar/bpmn-menu.ts` by PF3 so the menu becomes a pure renderer over the
16
+ * command registry. Telemetry is emitted once, by `runCommand`.
17
+ */
19
18
  const gfxOf = (std) => std.get(GfxControllerIdentifier);
20
19
  function finish(gfx, id) {
21
20
  gfx.doc.captureSync();
@@ -31,28 +30,15 @@ export function createBpmnNode(std, kind) {
31
30
  return;
32
31
  const { w, h } = NODE_SIZE[kind];
33
32
  const { centerX: cx, centerY: cy } = gfx.viewport;
34
- const preset = NODE_PRESETS[kind];
35
- const id = surface.addElement({
36
- type: 'bpmnNode',
37
- kind,
38
- shapeType: preset.shapeType,
39
- filled: true,
40
- fillColor: NODE_FILL,
41
- strokeColor: preset.stroke,
42
- strokeWidth: preset.width,
43
- shapeStyle: ShapeStyle.General,
44
- roughness: 0,
45
- radius: kind === 'task' ? TASK_RADIUS : 0,
46
- text: NODE_LABEL[kind] || undefined,
47
- color: NEUTRAL_STROKE,
48
- fontFamily: FontFamily.Inter,
49
- fontSize: INNER_FONT_SIZE,
50
- textAlign: 'center',
51
- // BPMN symbols have normative sizes: a long label overflows rather
52
- // than deforming the node
53
- textFitMode: TextFitMode.Overflow,
33
+ // What a node IS lives in one place (`./presets.ts`), because the importer
34
+ // creates the same artefacts out of a `.bpmn` file and the two must not
35
+ // drift: a task read from a file and a task drawn here are one element type
36
+ // in the document, down to the stroke width. The gesture owns the BOX and
37
+ // nothing else.
38
+ const id = surface.addElement(bpmnNodeProps(kind, {
54
39
  xywh: new Bound(cx - w / 2, cy - h / 2, w, h).serialize(),
55
- });
40
+ text: NODE_LABEL[kind] || undefined,
41
+ }));
56
42
  finish(gfx, id);
57
43
  }
58
44
  /** Create a pool (background container) centred on the viewport. */
@@ -66,6 +52,9 @@ export function createBpmnPool(std) {
66
52
  const { centerX: cx, centerY: cy } = gfx.viewport;
67
53
  const id = surface.addElement({
68
54
  type: 'bpmnPool',
55
+ // The FRAME the flow objects are drawn in, and a role of its own: a rule
56
+ // written on the artefacts must never fall on the lane that holds them.
57
+ role: BPMN_ROLE.pool,
69
58
  xywh: new Bound(cx - w / 2, cy - h / 2, w, h).serialize(),
70
59
  });
71
60
  finish(gfx, id);
@@ -76,14 +65,403 @@ export function createBpmnPool(std) {
76
65
  * one node to another (endpoints attach to centers).
77
66
  */
78
67
  export function activateBpmnSequenceFlow(std) {
79
- std.get(EditPropsStore).recordLastProps('connector', {
68
+ gfxOf(std).tool.setTool(ConnectorTool, {
80
69
  mode: ConnectorMode.Orthogonal,
81
- stroke: SEQUENCE_STROKE,
82
- strokeStyle: StrokeStyle.Solid,
83
- strokeWidth: SEQUENCE_WIDTH,
84
- frontEndpointStyle: PointStyle.None,
85
- rearEndpointStyle: PointStyle.Triangle,
70
+ // A TYPED edge (`docs/adr/0010`): the arrow the user is about to draw says
71
+ // "is followed by", so its source is what happens first. The tool carries
72
+ // the role so the connector is born with it rather than acquiring one
73
+ // afterwards.
74
+ role: BPMN_ROLE.sequenceFlow,
75
+ // The flow's look rides on the activation, never through the last-props
76
+ // store: the plain connector tool must keep the user's own style (#144 M1).
77
+ style: {
78
+ stroke: SEQUENCE_STROKE,
79
+ strokeStyle: StrokeStyle.Solid,
80
+ strokeWidth: SEQUENCE_WIDTH,
81
+ frontEndpointStyle: PointStyle.None,
82
+ rearEndpointStyle: PointStyle.Triangle,
83
+ },
86
84
  });
87
- gfxOf(std).tool.setTool(ConnectorTool, { mode: ConnectorMode.Orthogonal });
88
85
  // Keep the palette open (native sub-menu behaviour).
89
86
  }
87
+ /**
88
+ * Arm the native connector tool, pre-styled for a BPMN message flow:
89
+ * orthogonal, DASHED, an open circle where the message leaves and an open
90
+ * arrowhead where it lands.
91
+ *
92
+ * ## The two endpoint styles, and why these two
93
+ *
94
+ * The spec's message flow starts on a small hollow circle and ends on a hollow
95
+ * (line-drawn) arrowhead — never the solid triangle the sequence flow uses,
96
+ * which is the whole visual difference between "then this happens" and "and
97
+ * this is what I told them". `PointStyle` offers `Circle` and `Arrow`, and they
98
+ * are exactly those two shapes: `Arrow` is drawn as an unfilled V
99
+ * (`renderRoundedPolygon(…, false)`), against `Triangle`'s filled one. The only
100
+ * deviation is that `Circle` is painted with the connector's `fillColor` rather
101
+ * than left transparent — the shape, the size and the position are the spec's.
102
+ */
103
+ export function activateBpmnMessageFlow(std) {
104
+ gfxOf(std).tool.setTool(ConnectorTool, {
105
+ mode: ConnectorMode.Orthogonal,
106
+ // A TYPED edge (`docs/adr/0010`), and the role its vocabulary already
107
+ // declared with the verb "sends a message to": the source is the
108
+ // participant that sends, the target the one that receives.
109
+ role: BPMN_ROLE.messageFlow,
110
+ // The flow's look rides on the activation, never through the last-props
111
+ // store: the plain connector tool must keep the user's own style (#144 M1).
112
+ style: {
113
+ stroke: MESSAGE_STROKE,
114
+ strokeStyle: StrokeStyle.Dash,
115
+ strokeWidth: MESSAGE_WIDTH,
116
+ frontEndpointStyle: PointStyle.Circle,
117
+ rearEndpointStyle: PointStyle.Arrow,
118
+ },
119
+ });
120
+ // Keep the palette open (native sub-menu behaviour).
121
+ }
122
+ /**
123
+ * Arm the native connector tool, pre-styled for a BPMN association: dashed, and
124
+ * with NO endpoint marker at either end.
125
+ *
126
+ * The missing arrowheads are the point, not an omission. An association names
127
+ * no relation — `bpmn:association` is the one edge role in this vocabulary
128
+ * declared without a `direction` block — so "this note is about that task"
129
+ * reads identically from either end, and an arrowhead would be the picture
130
+ * claiming a direction the role explicitly refuses to have. See the role's own
131
+ * doc comment in `./roles.ts`.
132
+ *
133
+ * On the dash (there is no dotted stroke to ask for) see
134
+ * {@link ASSOCIATION_STROKE}'s neighbours in `./consts.ts`.
135
+ */
136
+ export function activateBpmnAssociation(std) {
137
+ gfxOf(std).tool.setTool(ConnectorTool, {
138
+ mode: ConnectorMode.Orthogonal,
139
+ role: BPMN_ROLE.association,
140
+ // The association's look rides on the activation, never through the
141
+ // last-props store: the plain connector tool must keep the user's own
142
+ // style (#144 M1).
143
+ style: {
144
+ stroke: ASSOCIATION_STROKE,
145
+ strokeStyle: StrokeStyle.Dash,
146
+ strokeWidth: ASSOCIATION_WIDTH,
147
+ frontEndpointStyle: PointStyle.None,
148
+ rearEndpointStyle: PointStyle.None,
149
+ },
150
+ });
151
+ // Keep the palette open (native sub-menu behaviour).
152
+ }
153
+ /* ── Lanes (couloirs) ─────────────────────────────────────────────────── */
154
+ /**
155
+ * The lanes a pool carries, as an array that is safe to read.
156
+ *
157
+ * The value comes out of a Y.Map, so it is whatever a peer wrote: a client that
158
+ * got it wrong must not break the gesture on this one. Same defensive read the
159
+ * validation engine does on `validationExceptions`, for the same reason.
160
+ */
161
+ export function bpmnLanesOf(model) {
162
+ const stored = model.lanes;
163
+ return Array.isArray(stored) ? stored : [];
164
+ }
165
+ /**
166
+ * The pools a lane gesture acts on: every pool of the current selection that is
167
+ * not locked.
168
+ *
169
+ * EVERY one, not the single one — `some`, not `every`, in the same spirit as
170
+ * the typed-edge inversion (`docs/adr/0010` M3): lassoing two pools used to be
171
+ * a way to lose an affordance, and a gesture that says what it did on each of
172
+ * them beats an entry that vanishes. With one pool selected — the ordinary
173
+ * case, and the only one the toolbar offers — it is exactly "the selected pool".
174
+ */
175
+ export function bpmnPoolsForLaneEdit(std) {
176
+ if (std.store.readonly)
177
+ return [];
178
+ return gfxOf(std)
179
+ .selection.selectedElements.filter((model) => model instanceof BpmnPoolElementModel)
180
+ .filter(model => !model.isLocked());
181
+ }
182
+ /**
183
+ * Write a pool's lane list back, or REMOVE the prop when there is none left.
184
+ *
185
+ * The second half is what keeps the "nothing is written until the first lane"
186
+ * promise reversible: assigning `undefined` through the accessor would leave
187
+ * the key in the Y.Map holding an undefined value — invisible through the
188
+ * getter, but synced to every peer and shipped in every snapshot. A pool whose
189
+ * last lane is removed goes back to its pre-lane bytes.
190
+ */
191
+ function writeLanes(std, model, lanes) {
192
+ if (lanes.length === 0) {
193
+ model.clearField('lanes');
194
+ return;
195
+ }
196
+ std.get(EdgelessCRUDIdentifier).updateElement(model.id, {
197
+ lanes: [...lanes],
198
+ });
199
+ }
200
+ /**
201
+ * Append a lane to every selected pool.
202
+ *
203
+ * ## One lane, not two
204
+ *
205
+ * Adding the FIRST lane creates ONE lane, covering the whole pool. Seeding two
206
+ * would invent a subdivision the user did not ask for and then make them delete
207
+ * half of it. One lane is the honest reading of "add a lane": the pool now has
208
+ * a lane, it happens to be all of it, and the second click gives them the
209
+ * second one.
210
+ *
211
+ * Since the lane title band (PO recette, 2026-08-26) that first click is also
212
+ * VISIBLE: a named strip appears down the leading edge of a pool that had none.
213
+ * Before it, a single lane was indistinguishable from no lane at all, and the
214
+ * gesture looked broken until the second click.
215
+ *
216
+ * ## `Lane N` is DOCUMENT DATA, not vocabulary
217
+ *
218
+ * The default name is a plain persisted string, exactly like the pool's own
219
+ * `'Pool'` default: it is written into the document by this action and is the
220
+ * user's to rewrite from that moment on. It is deliberately NOT a `labelKey`
221
+ * through the translation seam — a host that ships a French catalogue must not
222
+ * silently retitle a lane an author named, and a name that changed language
223
+ * when the reader's locale did would be a document that says different things
224
+ * to different people. `N` is the count AFTER this lane, so the first is
225
+ * `Lane 1`.
226
+ */
227
+ export function addBpmnLane(std) {
228
+ const pools = bpmnPoolsForLaneEdit(std);
229
+ if (pools.length === 0)
230
+ return;
231
+ // Before the writes: `Store.transact` is not an undo boundary, and one
232
+ // capture for the whole gesture is what makes several pools take their lane
233
+ // in a single undo step.
234
+ std.store.captureSync();
235
+ for (const model of pools) {
236
+ const lanes = bpmnLanesOf(model);
237
+ // The new lane takes an equal share: the average of what is already there
238
+ // is the weight that leaves every existing lane the same size relative to
239
+ // its neighbours, and gives the newcomer the room a typical one has. `1`
240
+ // for the first, where there is no average and the unit is arbitrary.
241
+ const size = lanes.length
242
+ ? lanes.reduce((sum, lane) => sum + lane.size, 0) / lanes.length
243
+ : 1;
244
+ writeLanes(std, model, [
245
+ ...lanes,
246
+ { id: generateElementId(), name: `Lane ${lanes.length + 1}`, size },
247
+ ]);
248
+ }
249
+ }
250
+ /**
251
+ * Remove the LAST lane of every selected pool.
252
+ *
253
+ * The last one and not the selected one, because a lane is not selectable: it
254
+ * is a slice of the pool's plot, painted by the background primitive, with no
255
+ * element of its own. Removing from the end is the gesture that undoes the one
256
+ * that added it.
257
+ *
258
+ * ## Nothing moves
259
+ *
260
+ * Elements sitting in the removed lane do NOT move. Containment here is
261
+ * geometric — a task is "in" a lane because its centre falls in that rectangle,
262
+ * which is how the audit reports it — so the lane below simply grows over them
263
+ * and they are in that one now. Nothing on the canvas jumps under the user's
264
+ * hand, and the sequence flow they drew still lands where they drew it.
265
+ */
266
+ export function removeBpmnLane(std) {
267
+ const pools = bpmnPoolsForLaneEdit(std);
268
+ if (pools.length === 0)
269
+ return;
270
+ const withLanes = pools.filter(model => bpmnLanesOf(model).length > 0);
271
+ if (withLanes.length === 0)
272
+ return;
273
+ std.store.captureSync();
274
+ for (const model of withLanes) {
275
+ writeLanes(std, model, bpmnLanesOf(model).slice(0, -1));
276
+ }
277
+ }
278
+ /* ── Export (BPMN 2.0 XML) ────────────────────────────────────────────── */
279
+ /**
280
+ * The pools of the current selection, WITHOUT the two filters a lane gesture
281
+ * applies.
282
+ *
283
+ * `bpmnPoolsForLaneEdit` refuses a read-only document and a locked pool because
284
+ * it is about to write to them. An export writes nothing: it reads the board
285
+ * and hands the reader a file. A process published read-only, or a pool an
286
+ * author locked precisely because it is finished, is exactly the board somebody
287
+ * wants to take to bpmn.io — refusing it there would be a filter copied for the
288
+ * shape of it rather than for the reason.
289
+ */
290
+ export function bpmnPoolsSelected(std) {
291
+ return gfxOf(std).selection.selectedElements.filter((model) => model instanceof BpmnPoolElementModel);
292
+ }
293
+ /**
294
+ * Everything on the surface the exporter speaks about, in document order.
295
+ *
296
+ * The half that needs an editor, and only that half: reading the surface. The
297
+ * picking is {@link bpmnBoardFrom}, which the interchange capability calls with
298
+ * the same elements and no `std` at all (`docs/adr/0012`, P3).
299
+ */
300
+ export function bpmnBoardOf(std) {
301
+ return bpmnBoardFrom(gfxOf(std).surface?.elementModels ?? []);
302
+ }
303
+ /**
304
+ * What the downloaded file is called, minus the extension.
305
+ *
306
+ * The document's own title first — a board is what the file is OF — then the
307
+ * name of the pool whose toolbar launched the export, then a last resort. Which
308
+ * of the three it is, is the only thing this function decides; making the
309
+ * answer safe to write to disk is {@link bpmnSafeFilename}, so the command and
310
+ * the interchange capability cannot name the same board differently.
311
+ */
312
+ export function bpmnExportFilename(std) {
313
+ const title = std.store.workspace.meta.getDocMeta(std.store.id)?.title;
314
+ const pool = bpmnPoolsSelected(std)[0]?.name;
315
+ return bpmnSafeFilename(title || pool);
316
+ }
317
+ /**
318
+ * Serialize the whole board as BPMN 2.0 XML and hand it to the browser.
319
+ *
320
+ * Three steps, and only the first and the last know what an editor is: read the
321
+ * surface, run the DECLARED capability (`docs/adr/0012`), download what it
322
+ * produced. The middle step is not re-implemented here — the document, the
323
+ * filename and the content type all come out of `BPMN_XML_EXPORT.run`, so the
324
+ * command and the registry cannot describe the same board differently. There is
325
+ * one door; the registry is the label on it.
326
+ *
327
+ * A plain import rather than a DI lookup: the capability is a pure function and
328
+ * a value, resolving it through the container would buy nothing here, and P3 is
329
+ * explicit that the registry is the editor's view of these functions, not a
330
+ * gate in front of them.
331
+ */
332
+ export function exportBpmnXmlFile(std) {
333
+ const elements = gfxOf(std).surface?.elementModels ?? [];
334
+ const { text, filename, mime, warnings } = BPMN_XML_EXPORT.run(elements, {
335
+ name: bpmnExportFilename(std),
336
+ });
337
+ // The charset is the browser's business, not the format's: `mime` is what
338
+ // `.bpmn` IS, and this is how a blob is told to carry it.
339
+ downloadBlob(new Blob([text], { type: `${mime};charset=utf-8` }), filename);
340
+ // The `warnings` channel, spent. The writer has been populating it since
341
+ // #149 and the command dropped it on the floor — a #159 review nit, and the
342
+ // one thing that made "an export loses things too, and the user who clicked
343
+ // Export is the one person entitled to be told" false in the only place a
344
+ // user stands. A warning is never an error: the file downloaded, and it is
345
+ // valid; what it could not say is what this names.
346
+ if (!warnings || warnings.length === 0)
347
+ return;
348
+ notifyBpmn(std, {
349
+ title: translateKey(std, EXPORT_WARNINGS_KEY, EXPORT_WARNINGS_FALLBACK),
350
+ message: warnings.join('\n'),
351
+ accent: 'warning',
352
+ });
353
+ }
354
+ /**
355
+ * The one wording this file still owns: what a WRITER could not say.
356
+ *
357
+ * The import's wordings moved to the pipeline that writes them
358
+ * (`affine-block-surface`, `extensions/interchange-import.ts`) — one set of
359
+ * keys for every format, with the format's own name composed into them, so a
360
+ * host translates "file imported" once instead of once per format. This one
361
+ * stays because no other format's writer speaks through it.
362
+ */
363
+ const EXPORT_WARNINGS_KEY = 'com.labre.commands.bpmn.exportXml.warnings';
364
+ const EXPORT_WARNINGS_FALLBACK = 'What this export could not write down';
365
+ /**
366
+ * The notification seam, or silence.
367
+ *
368
+ * `getOptional`, like every other call site in the library: the host injects a
369
+ * `NotificationService` (labreapp does, the standalone playground does not), and
370
+ * a framework that assumed one would be a framework the playground cannot run.
371
+ * Nothing here decides that an export said nothing because nobody was
372
+ * listening — the file downloaded either way.
373
+ */
374
+ function notifyBpmn(std, options) {
375
+ std.getOptional(NotificationProvider)?.notify({
376
+ title: options.title,
377
+ message: options.message,
378
+ accent: options.accent,
379
+ // Long enough to read a paragraph of remarks, and still self-dismissing:
380
+ // an import report is not a modal, and a toast the user has to close is a
381
+ // toast that interrupts the next thing they were doing.
382
+ duration: 8000,
383
+ });
384
+ }
385
+ /* ── Import (BPMN 2.0 XML) ────────────────────────────────────────────── */
386
+ /**
387
+ * Write an imported board onto the surface, and give back the ids it minted.
388
+ *
389
+ * BPMN's name for {@link materializeInterchangeImport}, which is where the two
390
+ * passes live and are documented (`docs/adr/0012`, D3). Nothing in them was
391
+ * ever about BPMN except the payload key the source ids ride under, so the
392
+ * function moved to the surface package when the second format asked for it and
393
+ * this name stayed: it is what the chromium round trip calls, and a test that
394
+ * proves the shipped command has to keep calling the shipped function.
395
+ */
396
+ export function materializeBpmnImport(std, elements) {
397
+ return materializeInterchangeImport(std, BPMN_FORMAT_ID, elements);
398
+ }
399
+ /**
400
+ * Say what the import did — the summary, and the remarks.
401
+ *
402
+ * BPMN's name for {@link reportInterchangeImport}, which is where the argument
403
+ * for a toast plus a console table lives (ADR 0012's open question 4, v1). The
404
+ * format is a word in the sentence now rather than a wording per format, so
405
+ * what a user reads is unchanged: the counts, and `BPMN` before the version the
406
+ * reader actually read.
407
+ */
408
+ export function reportBpmnImport(std, report) {
409
+ reportInterchangeImport(std, BPMN_XML_FORMAT, report);
410
+ }
411
+ /**
412
+ * Read a `.bpmn` file the user picks, draw it, and say what it cost.
413
+ *
414
+ * The whole gesture is {@link runInterchangeImportFile}, over the capability
415
+ * BPMN declares: pick the file, run the DECLARED reader, write what it
416
+ * returned, fit the drawing, report. `BPMN_XML_IMPORT.run` is the same function
417
+ * labre-mcp calls, so the command and the registry cannot read the same file
418
+ * differently — one door, and the registry is the label on it. The picker's
419
+ * filter comes off `BPMN_XML_FORMAT` rather than off the shared `FileTypes`
420
+ * table, which is why `.xml` is declared there: half the tools in the wild
421
+ * write a process under the generic extension, and what the file actually IS is
422
+ * decided by the reader, which throws on anything that is not a BPMN
423
+ * `<definitions>`.
424
+ */
425
+ export async function importBpmnXmlFile(std) {
426
+ await runInterchangeImportFile(std, BPMN_XML_IMPORT);
427
+ }
428
+ /**
429
+ * Read an SVG the user picks as a SKETCH, and say what it cost.
430
+ *
431
+ * The same four steps as the `.bpmn` import, over a different declared
432
+ * capability — which is the whole point of the seam: a second format costs a
433
+ * declaration and a command, not a pipeline. What differs is the PROMISE, and
434
+ * the promise is made by the command's own label and description before the
435
+ * picker ever opens (`docs/adr/0012`, P2): recognition, best effort, no
436
+ * round-trip, and a level-1 sketch the author then promotes.
437
+ */
438
+ export async function importBpmnSvgFile(std) {
439
+ await runInterchangeImportFile(std, BPMN_SVG_IMPORT);
440
+ }
441
+ /**
442
+ * Rename one lane of one pool. Called by the in-place editor
443
+ * (`element-view.ts`); exported so a unit test can assert the write without an
444
+ * editor around it.
445
+ *
446
+ * An empty name REMOVES the key rather than storing `''`: the renderer already
447
+ * treats both as "no name", and one of the two would be a second way to say the
448
+ * same thing that only the bytes can tell apart.
449
+ */
450
+ export function renameBpmnLane(std, model, index, name) {
451
+ const lanes = bpmnLanesOf(model);
452
+ const lane = lanes[index];
453
+ if (!lane)
454
+ return;
455
+ const trimmed = name.trim();
456
+ if ((lane.name ?? '') === trimmed)
457
+ return;
458
+ const next = lanes.map((entry, i) => i === index
459
+ ? {
460
+ id: entry.id,
461
+ ...(trimmed ? { name: trimmed } : {}),
462
+ size: entry.size,
463
+ }
464
+ : entry);
465
+ std.store.captureSync();
466
+ writeLanes(std, model, next);
467
+ }
@@ -0,0 +1,2 @@
1
+ import type { FrameworkBackgroundDef } from '@formicoidea/labre-core/blocks/surface';
2
+ export declare const BPMN_POOL_BACKGROUND: FrameworkBackgroundDef;
@@ -0,0 +1,158 @@
1
+ import { POOL_BAND_FILL, POOL_BAND_WIDTH, POOL_CARD_FILL, POOL_CORNER_RADIUS, POOL_FONT_FAMILY, POOL_FRAME_COLOR, POOL_FRAME_WIDTH, POOL_LANE_BAND_WIDTH, POOL_LANE_NAME_FONT_SIZE, POOL_NAME_COLOR, POOL_NAME_FONT_SIZE, POOL_REF_HEIGHT, POOL_REF_WIDTH, } from './consts.js';
2
+ import { BPMN_ROLE } from './roles.js';
3
+ /**
4
+ * The BPMN pool, DECLARED (the `FrameworkBackgroundDef` primitive).
5
+ *
6
+ * This file is the whole of what makes a pool look like a pool. There is no
7
+ * BPMN drawing code left: the primitive paints this declaration, and would
8
+ * paint any other framework's the same way — what used to be ninety lines of
9
+ * `ctx.fillRect` / `ctx.arcTo` / `ctx.rotate` is now data a reviewer can read.
10
+ *
11
+ * Nothing here changes the DOCUMENT. The persisted element type is still
12
+ * `bpmnPool` and its props are untouched — `name`, `resizeEnabled`, `rotate`,
13
+ * `xywh` — they are simply named by the declaration instead of being read by
14
+ * hand-written drawing code. A pool authored before this file existed opens
15
+ * with the same frame, the same band and the same participant name.
16
+ *
17
+ * ## No frame of reference
18
+ *
19
+ * A pool declares no axis and no zone, and that is deliberate: a pool is a
20
+ * PARTICIPANT, not a chart. Nothing about where a task sits inside the lane
21
+ * means anything — left-to-right is the sequence flow's business, and the flow
22
+ * says so with an arrow. Declaring a time axis here would invent a semantic
23
+ * BPMN puts on the connectors, and then judge people against it.
24
+ *
25
+ * ## The band
26
+ *
27
+ * The left margin IS the name band: `margin.left` is both the room the flow
28
+ * area gives up and the width of the strip the participant name is written in
29
+ * (see `BackgroundSideBandDef`).
30
+ *
31
+ * ## The lanes
32
+ *
33
+ * A lane (couloir) is NOT a second element type, and it is not a band either:
34
+ * it is a slice of THIS pool's plot, so it is declared as an instance partition
35
+ * (`BackgroundInstanceZonesDef`) read off the pool's own `lanes` prop. How many
36
+ * there are, what they are called and how the height is shared between them is
37
+ * a property of this pool; that a pool CAN be sliced that way is the property
38
+ * of BPMN declared here.
39
+ *
40
+ * Which is also why the lanes are not zones: a `zones` entry is part of the
41
+ * framework and identical on every element of it, and no two pools have the
42
+ * same lanes.
43
+ *
44
+ * Each lane wears its own title band inside the pool's — a narrower strip, no
45
+ * fill, one divider, the name turned on its side, exactly as BPMN 2.0 draws it.
46
+ * The strip is CHROME inside the lane and not a smaller lane: a task dropped on
47
+ * a lane's title band is in that lane, because in BPMN the band belongs to it.
48
+ */
49
+ /** The colour code: every colour named once, never repeated as a hex. */
50
+ const PALETTE = {
51
+ card: POOL_CARD_FILL,
52
+ frame: POOL_FRAME_COLOR,
53
+ band: POOL_BAND_FILL,
54
+ name: POOL_NAME_COLOR,
55
+ };
56
+ export const BPMN_POOL_BACKGROUND = {
57
+ type: 'bpmnPool',
58
+ // The pool is a first-class role: rules frame against `bpmn:pool`, never
59
+ // against the `bpmnPool` element type. Same vocabulary the creation site
60
+ // stamps (`actions.ts`) and the templates ship, named once in `roles.ts`.
61
+ role: BPMN_ROLE.pool,
62
+ geometry: {
63
+ width: POOL_REF_WIDTH,
64
+ height: POOL_REF_HEIGHT,
65
+ // A lane is stretched in one direction all the time — long and thin as the
66
+ // process grows sideways, tall only when it has to hold more. Locking the
67
+ // proportion would fight the hand on every drag.
68
+ lockAspectRatio: false,
69
+ resizable: true,
70
+ margin: { top: 0, right: 0, bottom: 0, left: POOL_BAND_WIDTH },
71
+ },
72
+ instanceZones: {
73
+ prop: 'lanes',
74
+ // Horizontal bands, top to bottom in array order: a lane runs ALONG the
75
+ // flow, and the flow runs left to right.
76
+ stack: 'y',
77
+ // A namespace, so a lane the user calls `early` can never shadow a
78
+ // framework zone of that name. A pool declares no zones today; the
79
+ // separation is what makes that stay true if it ever does.
80
+ idPrefix: 'lane',
81
+ // The same stroke as the band divider — the two lines meet at the band's
82
+ // inner edge, and a lane separator that did not match would read as a
83
+ // different KIND of line rather than the same frame continued.
84
+ divider: { color: '@frame', width: POOL_FRAME_WIDTH },
85
+ label: {
86
+ // The BAND placement: a title strip at the lane's leading edge with the
87
+ // name turned on its side, which is how BPMN 2.0 draws a lane and how
88
+ // bpmn.io, Camunda and Visio all render one. The corner placement this
89
+ // used to declare put the name across the lane's top-left instead; the
90
+ // PO's visual recette (2026-08-26) settled it against the corner on
91
+ // notation rather than taste — a reader who knows BPMN reads a strip as a
92
+ // lane title and a floating corner word as a note.
93
+ //
94
+ // No fill: the strip is the participant band's subordinate, and a second
95
+ // grey gutter beside it would leave the flow area looking inset twice.
96
+ band: {
97
+ width: POOL_LANE_BAND_WIDTH,
98
+ // The frame's own line again, as the band divider is: every rule on a
99
+ // pool is the same stroke, so the lanes read as the frame continued.
100
+ divider: { color: '@frame', width: POOL_FRAME_WIDTH },
101
+ },
102
+ style: {
103
+ size: POOL_LANE_NAME_FONT_SIZE,
104
+ color: '@name',
105
+ weight: 600,
106
+ },
107
+ },
108
+ },
109
+ chrome: {
110
+ fontFamily: POOL_FONT_FAMILY,
111
+ palette: PALETTE,
112
+ // An opaque white card, like every other framework background (PO recette,
113
+ // 26/08/2026). The hand-written renderer left the pool transparent, on the
114
+ // reasoning that a lane is a frame you drop nodes INTO; the review settled
115
+ // it the other way, and settled it on identity: a pool is a map background,
116
+ // so it paints a card, and a board where one framework's backdrop is
117
+ // see-through and every other one is not reads as a bug.
118
+ //
119
+ // The consequence is the standard framework-background behaviour, not a
120
+ // pool quirk: dropping a pool over strokes already on the canvas covers
121
+ // them, exactly as dropping a Wardley map over them does. The user's answer
122
+ // is the same in both cases — send the background to the back.
123
+ surface: {
124
+ fill: '@card',
125
+ border: {
126
+ color: '@frame',
127
+ width: POOL_FRAME_WIDTH,
128
+ radius: POOL_CORNER_RADIUS,
129
+ },
130
+ },
131
+ sideBands: [
132
+ {
133
+ side: 'left',
134
+ fill: '@band',
135
+ divider: { color: '@frame', width: POOL_FRAME_WIDTH },
136
+ label: {
137
+ id: 'name',
138
+ // The user's own words, and only those: a participant is named by
139
+ // whoever draws the process, so there is no vocabulary to fall back
140
+ // to and no `labelKey` to declare.
141
+ prop: 'name',
142
+ // `x: 0` is the inner edge of the band; half a band width back from
143
+ // it is the middle of the strip.
144
+ anchor: { x: 0, y: 0.5, dx: -POOL_BAND_WIDTH / 2 },
145
+ style: {
146
+ size: POOL_NAME_FONT_SIZE,
147
+ weight: 600,
148
+ color: '@name',
149
+ // Centred ACROSS the band, not sitting on a baseline inside it.
150
+ baseline: 'middle',
151
+ },
152
+ // Read bottom-to-top, as the spec draws a vertical pool name.
153
+ vertical: true,
154
+ },
155
+ },
156
+ ],
157
+ },
158
+ };