@formicoidea/labre-framework-bpmn 0.31.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 (52) hide show
  1. package/dist/actions.d.ts +213 -0
  2. package/dist/actions.js +467 -0
  3. package/dist/background.d.ts +2 -0
  4. package/dist/background.js +158 -0
  5. package/dist/commands.d.ts +4 -0
  6. package/dist/commands.js +567 -0
  7. package/dist/consts.d.ts +157 -3
  8. package/dist/consts.js +192 -3
  9. package/dist/descriptor.d.ts +8 -3
  10. package/dist/descriptor.js +6 -3
  11. package/dist/element-renderer.d.ts +10 -4
  12. package/dist/element-renderer.js +14 -55
  13. package/dist/element-view.d.ts +100 -8
  14. package/dist/element-view.js +249 -30
  15. package/dist/export.d.ts +277 -0
  16. package/dist/export.js +1802 -0
  17. package/dist/facts.d.ts +48 -0
  18. package/dist/facts.js +127 -0
  19. package/dist/import.d.ts +44 -0
  20. package/dist/import.js +1440 -0
  21. package/dist/index.d.ts +14 -1
  22. package/dist/index.js +46 -1
  23. package/dist/interchange.d.ts +109 -0
  24. package/dist/interchange.js +191 -0
  25. package/dist/morph.d.ts +61 -0
  26. package/dist/morph.js +118 -0
  27. package/dist/node/node-renderer.d.ts +0 -9
  28. package/dist/node/node-renderer.js +294 -17
  29. package/dist/pool-hit.d.ts +98 -0
  30. package/dist/pool-hit.js +130 -0
  31. package/dist/presets.d.ts +114 -0
  32. package/dist/presets.js +232 -0
  33. package/dist/profiles.d.ts +2 -0
  34. package/dist/profiles.js +189 -0
  35. package/dist/roles.d.ts +96 -0
  36. package/dist/roles.js +410 -0
  37. package/dist/rules.d.ts +199 -0
  38. package/dist/rules.js +1539 -0
  39. package/dist/templates/index.js +116 -9
  40. package/dist/toolbar/bpmn-menu.d.ts +6 -21
  41. package/dist/toolbar/bpmn-menu.js +6 -173
  42. package/dist/toolbar/bpmn-senior-button.js +8 -2
  43. package/dist/toolbar/config.d.ts +27 -2
  44. package/dist/toolbar/config.js +86 -2
  45. package/dist/toolbar/icons.d.ts +67 -0
  46. package/dist/toolbar/icons.js +141 -0
  47. package/dist/toolbar/senior-tool.js +1 -0
  48. package/dist/translations.d.ts +16 -0
  49. package/dist/translations.js +20 -0
  50. package/dist/view.d.ts +18 -0
  51. package/dist/view.js +95 -7
  52. package/package.json +2 -2
@@ -0,0 +1,213 @@
1
+ import { type InterchangeReport, type SerializedElementProps } from '@formicoidea/labre-core/blocks/surface';
2
+ import { type BpmnLane, type BpmnNodeKind, BpmnPoolElementModel } from '@formicoidea/labre-core/model';
3
+ import type { BlockStdScope } from '@formicoidea/labre-core/std';
4
+ import { type BpmnExportBoard } from './export.js';
5
+ /** Create a flow-object node (native shape) centred on the viewport. */
6
+ export declare function createBpmnNode(std: BlockStdScope, kind: BpmnNodeKind): void;
7
+ /** Create a pool (background container) centred on the viewport. */
8
+ export declare function createBpmnPool(std: BlockStdScope): void;
9
+ /**
10
+ * Arm the native connector tool, pre-styled for a BPMN sequence flow:
11
+ * orthogonal, solid, with a filled triangle head. The user then draws from
12
+ * one node to another (endpoints attach to centers).
13
+ */
14
+ export declare function activateBpmnSequenceFlow(std: BlockStdScope): void;
15
+ /**
16
+ * Arm the native connector tool, pre-styled for a BPMN message flow:
17
+ * orthogonal, DASHED, an open circle where the message leaves and an open
18
+ * arrowhead where it lands.
19
+ *
20
+ * ## The two endpoint styles, and why these two
21
+ *
22
+ * The spec's message flow starts on a small hollow circle and ends on a hollow
23
+ * (line-drawn) arrowhead — never the solid triangle the sequence flow uses,
24
+ * which is the whole visual difference between "then this happens" and "and
25
+ * this is what I told them". `PointStyle` offers `Circle` and `Arrow`, and they
26
+ * are exactly those two shapes: `Arrow` is drawn as an unfilled V
27
+ * (`renderRoundedPolygon(…, false)`), against `Triangle`'s filled one. The only
28
+ * deviation is that `Circle` is painted with the connector's `fillColor` rather
29
+ * than left transparent — the shape, the size and the position are the spec's.
30
+ */
31
+ export declare function activateBpmnMessageFlow(std: BlockStdScope): void;
32
+ /**
33
+ * Arm the native connector tool, pre-styled for a BPMN association: dashed, and
34
+ * with NO endpoint marker at either end.
35
+ *
36
+ * The missing arrowheads are the point, not an omission. An association names
37
+ * no relation — `bpmn:association` is the one edge role in this vocabulary
38
+ * declared without a `direction` block — so "this note is about that task"
39
+ * reads identically from either end, and an arrowhead would be the picture
40
+ * claiming a direction the role explicitly refuses to have. See the role's own
41
+ * doc comment in `./roles.ts`.
42
+ *
43
+ * On the dash (there is no dotted stroke to ask for) see
44
+ * {@link ASSOCIATION_STROKE}'s neighbours in `./consts.ts`.
45
+ */
46
+ export declare function activateBpmnAssociation(std: BlockStdScope): void;
47
+ /**
48
+ * The lanes a pool carries, as an array that is safe to read.
49
+ *
50
+ * The value comes out of a Y.Map, so it is whatever a peer wrote: a client that
51
+ * got it wrong must not break the gesture on this one. Same defensive read the
52
+ * validation engine does on `validationExceptions`, for the same reason.
53
+ */
54
+ export declare function bpmnLanesOf(model: BpmnPoolElementModel): readonly BpmnLane[];
55
+ /**
56
+ * The pools a lane gesture acts on: every pool of the current selection that is
57
+ * not locked.
58
+ *
59
+ * EVERY one, not the single one — `some`, not `every`, in the same spirit as
60
+ * the typed-edge inversion (`docs/adr/0010` M3): lassoing two pools used to be
61
+ * a way to lose an affordance, and a gesture that says what it did on each of
62
+ * them beats an entry that vanishes. With one pool selected — the ordinary
63
+ * case, and the only one the toolbar offers — it is exactly "the selected pool".
64
+ */
65
+ export declare function bpmnPoolsForLaneEdit(std: BlockStdScope): BpmnPoolElementModel[];
66
+ /**
67
+ * Append a lane to every selected pool.
68
+ *
69
+ * ## One lane, not two
70
+ *
71
+ * Adding the FIRST lane creates ONE lane, covering the whole pool. Seeding two
72
+ * would invent a subdivision the user did not ask for and then make them delete
73
+ * half of it. One lane is the honest reading of "add a lane": the pool now has
74
+ * a lane, it happens to be all of it, and the second click gives them the
75
+ * second one.
76
+ *
77
+ * Since the lane title band (PO recette, 2026-08-26) that first click is also
78
+ * VISIBLE: a named strip appears down the leading edge of a pool that had none.
79
+ * Before it, a single lane was indistinguishable from no lane at all, and the
80
+ * gesture looked broken until the second click.
81
+ *
82
+ * ## `Lane N` is DOCUMENT DATA, not vocabulary
83
+ *
84
+ * The default name is a plain persisted string, exactly like the pool's own
85
+ * `'Pool'` default: it is written into the document by this action and is the
86
+ * user's to rewrite from that moment on. It is deliberately NOT a `labelKey`
87
+ * through the translation seam — a host that ships a French catalogue must not
88
+ * silently retitle a lane an author named, and a name that changed language
89
+ * when the reader's locale did would be a document that says different things
90
+ * to different people. `N` is the count AFTER this lane, so the first is
91
+ * `Lane 1`.
92
+ */
93
+ export declare function addBpmnLane(std: BlockStdScope): void;
94
+ /**
95
+ * Remove the LAST lane of every selected pool.
96
+ *
97
+ * The last one and not the selected one, because a lane is not selectable: it
98
+ * is a slice of the pool's plot, painted by the background primitive, with no
99
+ * element of its own. Removing from the end is the gesture that undoes the one
100
+ * that added it.
101
+ *
102
+ * ## Nothing moves
103
+ *
104
+ * Elements sitting in the removed lane do NOT move. Containment here is
105
+ * geometric — a task is "in" a lane because its centre falls in that rectangle,
106
+ * which is how the audit reports it — so the lane below simply grows over them
107
+ * and they are in that one now. Nothing on the canvas jumps under the user's
108
+ * hand, and the sequence flow they drew still lands where they drew it.
109
+ */
110
+ export declare function removeBpmnLane(std: BlockStdScope): void;
111
+ /**
112
+ * The pools of the current selection, WITHOUT the two filters a lane gesture
113
+ * applies.
114
+ *
115
+ * `bpmnPoolsForLaneEdit` refuses a read-only document and a locked pool because
116
+ * it is about to write to them. An export writes nothing: it reads the board
117
+ * and hands the reader a file. A process published read-only, or a pool an
118
+ * author locked precisely because it is finished, is exactly the board somebody
119
+ * wants to take to bpmn.io — refusing it there would be a filter copied for the
120
+ * shape of it rather than for the reason.
121
+ */
122
+ export declare function bpmnPoolsSelected(std: BlockStdScope): BpmnPoolElementModel[];
123
+ /**
124
+ * Everything on the surface the exporter speaks about, in document order.
125
+ *
126
+ * The half that needs an editor, and only that half: reading the surface. The
127
+ * picking is {@link bpmnBoardFrom}, which the interchange capability calls with
128
+ * the same elements and no `std` at all (`docs/adr/0012`, P3).
129
+ */
130
+ export declare function bpmnBoardOf(std: BlockStdScope): BpmnExportBoard;
131
+ /**
132
+ * What the downloaded file is called, minus the extension.
133
+ *
134
+ * The document's own title first — a board is what the file is OF — then the
135
+ * name of the pool whose toolbar launched the export, then a last resort. Which
136
+ * of the three it is, is the only thing this function decides; making the
137
+ * answer safe to write to disk is {@link bpmnSafeFilename}, so the command and
138
+ * the interchange capability cannot name the same board differently.
139
+ */
140
+ export declare function bpmnExportFilename(std: BlockStdScope): string;
141
+ /**
142
+ * Serialize the whole board as BPMN 2.0 XML and hand it to the browser.
143
+ *
144
+ * Three steps, and only the first and the last know what an editor is: read the
145
+ * surface, run the DECLARED capability (`docs/adr/0012`), download what it
146
+ * produced. The middle step is not re-implemented here — the document, the
147
+ * filename and the content type all come out of `BPMN_XML_EXPORT.run`, so the
148
+ * command and the registry cannot describe the same board differently. There is
149
+ * one door; the registry is the label on it.
150
+ *
151
+ * A plain import rather than a DI lookup: the capability is a pure function and
152
+ * a value, resolving it through the container would buy nothing here, and P3 is
153
+ * explicit that the registry is the editor's view of these functions, not a
154
+ * gate in front of them.
155
+ */
156
+ export declare function exportBpmnXmlFile(std: BlockStdScope): void;
157
+ /**
158
+ * Write an imported board onto the surface, and give back the ids it minted.
159
+ *
160
+ * BPMN's name for {@link materializeInterchangeImport}, which is where the two
161
+ * passes live and are documented (`docs/adr/0012`, D3). Nothing in them was
162
+ * ever about BPMN except the payload key the source ids ride under, so the
163
+ * function moved to the surface package when the second format asked for it and
164
+ * this name stayed: it is what the chromium round trip calls, and a test that
165
+ * proves the shipped command has to keep calling the shipped function.
166
+ */
167
+ export declare function materializeBpmnImport(std: BlockStdScope, elements: readonly SerializedElementProps[]): string[];
168
+ /**
169
+ * Say what the import did — the summary, and the remarks.
170
+ *
171
+ * BPMN's name for {@link reportInterchangeImport}, which is where the argument
172
+ * for a toast plus a console table lives (ADR 0012's open question 4, v1). The
173
+ * format is a word in the sentence now rather than a wording per format, so
174
+ * what a user reads is unchanged: the counts, and `BPMN` before the version the
175
+ * reader actually read.
176
+ */
177
+ export declare function reportBpmnImport(std: BlockStdScope, report: InterchangeReport): void;
178
+ /**
179
+ * Read a `.bpmn` file the user picks, draw it, and say what it cost.
180
+ *
181
+ * The whole gesture is {@link runInterchangeImportFile}, over the capability
182
+ * BPMN declares: pick the file, run the DECLARED reader, write what it
183
+ * returned, fit the drawing, report. `BPMN_XML_IMPORT.run` is the same function
184
+ * labre-mcp calls, so the command and the registry cannot read the same file
185
+ * differently — one door, and the registry is the label on it. The picker's
186
+ * filter comes off `BPMN_XML_FORMAT` rather than off the shared `FileTypes`
187
+ * table, which is why `.xml` is declared there: half the tools in the wild
188
+ * write a process under the generic extension, and what the file actually IS is
189
+ * decided by the reader, which throws on anything that is not a BPMN
190
+ * `<definitions>`.
191
+ */
192
+ export declare function importBpmnXmlFile(std: BlockStdScope): Promise<void>;
193
+ /**
194
+ * Read an SVG the user picks as a SKETCH, and say what it cost.
195
+ *
196
+ * The same four steps as the `.bpmn` import, over a different declared
197
+ * capability — which is the whole point of the seam: a second format costs a
198
+ * declaration and a command, not a pipeline. What differs is the PROMISE, and
199
+ * the promise is made by the command's own label and description before the
200
+ * picker ever opens (`docs/adr/0012`, P2): recognition, best effort, no
201
+ * round-trip, and a level-1 sketch the author then promotes.
202
+ */
203
+ export declare function importBpmnSvgFile(std: BlockStdScope): Promise<void>;
204
+ /**
205
+ * Rename one lane of one pool. Called by the in-place editor
206
+ * (`element-view.ts`); exported so a unit test can assert the write without an
207
+ * editor around it.
208
+ *
209
+ * An empty name REMOVES the key rather than storing `''`: the renderer already
210
+ * treats both as "no name", and one of the two would be a second way to say the
211
+ * same thing that only the bytes can tell apart.
212
+ */
213
+ export declare function renameBpmnLane(std: BlockStdScope, model: BpmnPoolElementModel, index: number, name: string): void;
@@ -0,0 +1,467 @@
1
+ import { DefaultTool, EdgelessCRUDIdentifier, generateElementId, materializeInterchangeImport, reportInterchangeImport, runInterchangeImportFile, } from '@formicoidea/labre-core/blocks/surface';
2
+ import { ConnectorTool } from '@formicoidea/labre-core/gfx/connector';
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';
6
+ import { Bound } from '@formicoidea/labre-core/global/gfx';
7
+ import { GfxControllerIdentifier } from '@formicoidea/labre-core/std/gfx';
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
+ */
18
+ const gfxOf = (std) => std.get(GfxControllerIdentifier);
19
+ function finish(gfx, id) {
20
+ gfx.doc.captureSync();
21
+ gfx.tool.setTool(DefaultTool);
22
+ gfx.selection.set({ elements: [id], editing: false });
23
+ // Keep the palette open (native sub-menu behaviour).
24
+ }
25
+ /** Create a flow-object node (native shape) centred on the viewport. */
26
+ export function createBpmnNode(std, kind) {
27
+ const gfx = gfxOf(std);
28
+ const surface = gfx.surface;
29
+ if (!surface)
30
+ return;
31
+ const { w, h } = NODE_SIZE[kind];
32
+ const { centerX: cx, centerY: cy } = gfx.viewport;
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, {
39
+ xywh: new Bound(cx - w / 2, cy - h / 2, w, h).serialize(),
40
+ text: NODE_LABEL[kind] || undefined,
41
+ }));
42
+ finish(gfx, id);
43
+ }
44
+ /** Create a pool (background container) centred on the viewport. */
45
+ export function createBpmnPool(std) {
46
+ const gfx = gfxOf(std);
47
+ const surface = gfx.surface;
48
+ if (!surface)
49
+ return;
50
+ const w = 560;
51
+ const h = 200;
52
+ const { centerX: cx, centerY: cy } = gfx.viewport;
53
+ const id = surface.addElement({
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,
58
+ xywh: new Bound(cx - w / 2, cy - h / 2, w, h).serialize(),
59
+ });
60
+ finish(gfx, id);
61
+ }
62
+ /**
63
+ * Arm the native connector tool, pre-styled for a BPMN sequence flow:
64
+ * orthogonal, solid, with a filled triangle head. The user then draws from
65
+ * one node to another (endpoints attach to centers).
66
+ */
67
+ export function activateBpmnSequenceFlow(std) {
68
+ gfxOf(std).tool.setTool(ConnectorTool, {
69
+ mode: ConnectorMode.Orthogonal,
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
+ },
84
+ });
85
+ // Keep the palette open (native sub-menu behaviour).
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;