@formicoidea/labre-framework-bpmn 0.41.0 → 0.43.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/commands.js CHANGED
@@ -1,5 +1,25 @@
1
+ import { StrokeStyle } from '@formicoidea/labre-core/model';
1
2
  import { activateBpmnAssociation, activateBpmnMessageFlow, activateBpmnSequenceFlow, addBpmnLane, bpmnLanesOf, bpmnPoolsForLaneEdit, bpmnPoolsSelected, createBpmnNode, createBpmnPool, exportBpmnXmlFile, importBpmnSvgFile, importBpmnXmlFile, removeBpmnLane, } from './actions.js';
2
- import { bpmnAssociationIcon, bpmnCallActivityIcon, bpmnDataObjectIcon, bpmnDataStoreIcon, bpmnEndIcon, bpmnEndMessageIcon, bpmnEndTerminateIcon, bpmnExportXmlIcon, bpmnGatewayIcon, bpmnGatewayParallelIcon, bpmnGroupIcon, bpmnImportSvgIcon, bpmnImportXmlIcon, bpmnLaneAddIcon, bpmnLaneRemoveIcon, bpmnMessageIcon, bpmnPoolIcon, bpmnSequenceIcon, bpmnStartIcon, bpmnStartMessageIcon, bpmnStartTimerIcon, bpmnSubProcessIcon, bpmnTaskIcon, bpmnTaskServiceIcon, bpmnTaskUserIcon, bpmnTextAnnotationIcon, } from './toolbar/icons.js';
3
+ import { BPMN_EDGE_STYLE, NODE_FILL, NODE_SIZE, } from './consts.js';
4
+ import { bpmnNodeProps } from './presets.js';
5
+ import { BPMN_ROLE, BPMN_ROLE_OF_KIND } from './roles.js';
6
+ import { bpmnAssociationIcon, bpmnCallActivityIcon, bpmnDataObjectIcon, bpmnDataStoreIcon, bpmnEndIcon, bpmnEndMessageIcon, bpmnEndTerminateIcon, bpmnExportXmlIcon, bpmnGatewayIcon, bpmnGatewayParallelIcon, bpmnGroupIcon, bpmnImportSvgIcon, bpmnImportXmlIcon, bpmnLaneAddIcon, bpmnLaneRemoveIcon, bpmnMessageIcon, bpmnPoolIcon, bpmnSequenceIcon, bpmnStartIcon, bpmnStartMessageIcon, bpmnStartTimerIcon, bpmnSubProcessIcon, bpmnTaskIcon, bpmnTaskServiceIcon, bpmnTaskUserIcon, bpmnTextAnnotationIcon, bpmnToolbarIcon, } from './toolbar/icons.js';
7
+ /**
8
+ * What a spec DOES — its own gesture, or the one its {@link Spec.node} implies.
9
+ *
10
+ * Total over {@link SPECS} by inspection, and loud rather than silent if it
11
+ * ever stops being: a spec that drew nothing would be a button that does
12
+ * nothing, which is worse than a module that refuses to load.
13
+ */
14
+ function gestureOf(spec) {
15
+ if (spec.run)
16
+ return spec.run;
17
+ const kind = spec.node;
18
+ if (!kind) {
19
+ throw new Error(`bpmn: "${spec.id}" declares neither a node kind nor a run`);
20
+ }
21
+ return std => createBpmnNode(std, kind);
22
+ }
3
23
  /**
4
24
  * Declaration order is DISPLAY order, and past the cap it is also the COLD
5
25
  * START — which is why it leads with the canonical core rather than with the
@@ -40,7 +60,7 @@ const SPECS = [
40
60
  category: 'events',
41
61
  senior: true,
42
62
  element: 'node:startEvent',
43
- run: std => createBpmnNode(std, 'startEvent'),
63
+ node: 'startEvent',
44
64
  },
45
65
  {
46
66
  id: 'addEndEvent',
@@ -50,7 +70,7 @@ const SPECS = [
50
70
  category: 'events',
51
71
  senior: true,
52
72
  element: 'node:endEvent',
53
- run: std => createBpmnNode(std, 'endEvent'),
73
+ node: 'endEvent',
54
74
  },
55
75
  {
56
76
  id: 'addTask',
@@ -60,7 +80,7 @@ const SPECS = [
60
80
  category: 'activities',
61
81
  senior: true,
62
82
  element: 'node:task',
63
- run: std => createBpmnNode(std, 'task'),
83
+ node: 'task',
64
84
  },
65
85
  {
66
86
  id: 'addExclusiveGateway',
@@ -70,7 +90,7 @@ const SPECS = [
70
90
  category: 'gateways',
71
91
  senior: true,
72
92
  element: 'node:gatewayExclusive',
73
- run: std => createBpmnNode(std, 'gatewayExclusive'),
93
+ node: 'gatewayExclusive',
74
94
  },
75
95
  {
76
96
  id: 'sequenceFlowTool',
@@ -80,6 +100,7 @@ const SPECS = [
80
100
  category: 'flows',
81
101
  senior: true,
82
102
  element: 'connector:sequence',
103
+ edge: 'sequenceFlow',
83
104
  run: activateBpmnSequenceFlow,
84
105
  },
85
106
  {
@@ -104,6 +125,7 @@ const SPECS = [
104
125
  category: 'flows',
105
126
  senior: true,
106
127
  element: 'connector:message',
128
+ edge: 'messageFlow',
107
129
  run: activateBpmnMessageFlow,
108
130
  },
109
131
  /* ── Activities: the typed tasks and the two that stand for a process ─ */
@@ -115,7 +137,7 @@ const SPECS = [
115
137
  category: 'activities',
116
138
  senior: true,
117
139
  element: 'node:taskUser',
118
- run: std => createBpmnNode(std, 'taskUser'),
140
+ node: 'taskUser',
119
141
  },
120
142
  {
121
143
  id: 'addServiceTask',
@@ -125,7 +147,7 @@ const SPECS = [
125
147
  category: 'activities',
126
148
  senior: true,
127
149
  element: 'node:taskService',
128
- run: std => createBpmnNode(std, 'taskService'),
150
+ node: 'taskService',
129
151
  },
130
152
  {
131
153
  id: 'addSubProcess',
@@ -135,7 +157,7 @@ const SPECS = [
135
157
  category: 'activities',
136
158
  senior: true,
137
159
  element: 'node:subProcess',
138
- run: std => createBpmnNode(std, 'subProcess'),
160
+ node: 'subProcess',
139
161
  },
140
162
  {
141
163
  id: 'addCallActivity',
@@ -145,7 +167,7 @@ const SPECS = [
145
167
  category: 'activities',
146
168
  senior: true,
147
169
  element: 'node:callActivity',
148
- run: std => createBpmnNode(std, 'callActivity'),
170
+ node: 'callActivity',
149
171
  },
150
172
  /* ── The other gateway ──────────────────────────────────────────────── */
151
173
  {
@@ -159,7 +181,7 @@ const SPECS = [
159
181
  category: 'gateways',
160
182
  senior: true,
161
183
  element: 'node:gatewayParallel',
162
- run: std => createBpmnNode(std, 'gatewayParallel'),
184
+ node: 'gatewayParallel',
163
185
  },
164
186
  /* ── Event variants: what TRIGGERS a start, what an end does on the way
165
187
  out. Refinements of the two plain events above, so they follow them. ─ */
@@ -171,7 +193,7 @@ const SPECS = [
171
193
  category: 'events',
172
194
  senior: false,
173
195
  element: 'node:startEventMessage',
174
- run: std => createBpmnNode(std, 'startEventMessage'),
196
+ node: 'startEventMessage',
175
197
  },
176
198
  {
177
199
  id: 'addTimerStartEvent',
@@ -181,7 +203,7 @@ const SPECS = [
181
203
  category: 'events',
182
204
  senior: false,
183
205
  element: 'node:startEventTimer',
184
- run: std => createBpmnNode(std, 'startEventTimer'),
206
+ node: 'startEventTimer',
185
207
  },
186
208
  {
187
209
  id: 'addMessageEndEvent',
@@ -191,7 +213,7 @@ const SPECS = [
191
213
  category: 'events',
192
214
  senior: false,
193
215
  element: 'node:endEventMessage',
194
- run: std => createBpmnNode(std, 'endEventMessage'),
216
+ node: 'endEventMessage',
195
217
  },
196
218
  {
197
219
  id: 'addTerminateEndEvent',
@@ -201,7 +223,7 @@ const SPECS = [
201
223
  category: 'events',
202
224
  senior: false,
203
225
  element: 'node:endEventTerminate',
204
- run: std => createBpmnNode(std, 'endEventTerminate'),
226
+ node: 'endEventTerminate',
205
227
  },
206
228
  /* ── The last connecting object ─────────────────────────────────────── */
207
229
  {
@@ -212,6 +234,7 @@ const SPECS = [
212
234
  category: 'flows',
213
235
  senior: false,
214
236
  element: 'connector:association',
237
+ edge: 'association',
215
238
  run: activateBpmnAssociation,
216
239
  },
217
240
  /* ── Data ───────────────────────────────────────────────────────────── */
@@ -223,7 +246,7 @@ const SPECS = [
223
246
  category: 'data',
224
247
  senior: true,
225
248
  element: 'node:dataObject',
226
- run: std => createBpmnNode(std, 'dataObject'),
249
+ node: 'dataObject',
227
250
  },
228
251
  {
229
252
  id: 'addDataStore',
@@ -233,7 +256,7 @@ const SPECS = [
233
256
  category: 'data',
234
257
  senior: false,
235
258
  element: 'node:dataStore',
236
- run: std => createBpmnNode(std, 'dataStore'),
259
+ node: 'dataStore',
237
260
  },
238
261
  /* ── Artifacts: what an author writes ON the picture ────────────────── */
239
262
  {
@@ -244,7 +267,7 @@ const SPECS = [
244
267
  category: 'annotations',
245
268
  senior: true,
246
269
  element: 'node:textAnnotation',
247
- run: std => createBpmnNode(std, 'textAnnotation'),
270
+ node: 'textAnnotation',
248
271
  },
249
272
  {
250
273
  // Filed with the annotation and not in a section of its own: both are
@@ -261,29 +284,160 @@ const SPECS = [
261
284
  // senior row is a shortcut to.
262
285
  senior: false,
263
286
  element: 'node:group',
264
- run: std => createBpmnNode(std, 'group'),
287
+ node: 'group',
265
288
  },
266
289
  ];
267
- const toolboxCommands = SPECS.map((spec, order) => ({
268
- id: `bpmn.${spec.id}`,
269
- owner: 'bpmn',
270
- kind: spec.kind,
271
- labelKey: `com.labre.commands.bpmn.${spec.id}`,
272
- labelFallback: spec.label,
273
- category: spec.category,
274
- iconKey: spec.iconKey,
275
- // The catalogue holds all of them; `senior` decides which fourteen the
276
- // sub-menu opens on before this user has reached for anything.
277
- surfaces: spec.senior
278
- ? ['senior-menu', 'catalogue', 'palette', 'agent']
279
- : ['catalogue', 'palette', 'agent'],
280
- order,
281
- scope: 'edgeless',
282
- defaultKeys: { mac: [], other: [] },
283
- availability: 'always',
284
- run: spec.run,
285
- telemetry: { framework: 'bpmn', element: spec.element, board: spec.board },
286
- }));
290
+ /* ── The legend, subscribed rather than tabulated ──────────────────────── */
291
+ /**
292
+ * The legend of a pool is not a table: every row below is DERIVED from the
293
+ * command that draws the artefact, from the same preset the gesture creates it
294
+ * with, and it is listed only when the pool actually contains one (ADR 0026,
295
+ * `legendFromCommands`).
296
+ *
297
+ * ## The swatch box, and why BPMN asks for a bigger one than UML
298
+ *
299
+ * UML tells its figures apart by full-box silhouettes (a stick figure, a
300
+ * divided box, an ellipse) and is comfortable at 34 × 24. BPMN puts its
301
+ * distinctions in CORNER MARKERS — the person and the cog are `0.24 × unit` in
302
+ * the top-left (`node/node-renderer.ts`) — so it needs about 30 % more: at
303
+ * 44 × 30 a task's marker is ~6 px and a gateway's arms ~6 px, which is the
304
+ * floor at which they read at all.
305
+ *
306
+ * ## The one known visual limit
307
+ *
308
+ * The group's dashed lasso uses the renderer's fixed `[12, 12]` dash pattern
309
+ * (`consts.ts` says why there is no dotted stroke to ask for): across 44 units
310
+ * that is about two dashes, and down 29 it is none. The row is still
311
+ * distinguishable from the text annotation — which draws no closed rectangle —
312
+ * but it does not READ as dotted, and nothing in this file can change that.
313
+ */
314
+ const SWATCH_W = 44;
315
+ const SWATCH_H = 30;
316
+ const BPMN_LEGEND_BOX = {
317
+ // No `titleWording`: every board that has a legend says the same generic
318
+ // "Legend", which is what the platform falls back to (`BOARD_LEGEND_TITLE`).
319
+ width: 300,
320
+ rowHeight: 36,
321
+ swatchWidth: SWATCH_W,
322
+ swatchHeight: SWATCH_H,
323
+ };
324
+ /**
325
+ * The three roles whose row must match EXACTLY, and no others.
326
+ *
327
+ * `bpmn:start-event`, `bpmn:end-event` and `bpmn:task` are the three roles with
328
+ * a row that also have children (`roles.ts`). Without `exact`, a pool holding
329
+ * only a message start event would light the plain "Start event" row too — a
330
+ * bare circle nobody drew. The families that have no row at all
331
+ * (`bpmn:event`, `bpmn:activity`, `bpmn:gateway`, `bpmn:data`,
332
+ * `bpmn:flow-object`) need no such care: they are never stamped.
333
+ */
334
+ const EXACT_ROLES = new Set([
335
+ BPMN_ROLE.startEvent,
336
+ BPMN_ROLE.endEvent,
337
+ BPMN_ROLE.task,
338
+ ]);
339
+ /**
340
+ * A glyph row: the artefact itself, painted at swatch size by the very renderer
341
+ * that paints it on the board — the creation preset minus three keys.
342
+ *
343
+ * - `role`, because the legend is drawn IN the pool it documents and the scan
344
+ * detects by role: a swatch carrying one would list itself the next time,
345
+ * and all 21 validation rules would count it (`rules.ts`);
346
+ * - `xywh`, because the box belongs to the layout, which fits {@link
347
+ * CommandLegendRow.aspect} inside the swatch column;
348
+ * - `text`, because `bpmnNodeProps` writes the artefact's default caption at
349
+ * 18 px with `TextFitMode.Overflow`: a "Task" that size would spill across
350
+ * the whole box. Without it `fontSize` / `textAlign` / `textFitMode` are
351
+ * inert, so there is no reason to strip those too.
352
+ *
353
+ * `radius` is the one prop SCALED to the swatch, and it has to be: it is an
354
+ * absolute, unbounded value (`drawRect` calls `arcTo` with it), so the group's
355
+ * 20 and the task's 10 would both round a 44 × 30 box into a pill.
356
+ * `strokeWidth` is deliberately NOT scaled — the thin ring against the thick
357
+ * one, and the call activity's heavy border against the sub-process's, IS the
358
+ * notation, and at 30 units the 2-against-4 ratio reads better than at 56.
359
+ */
360
+ function nodeRow(kind) {
361
+ const { role: _role, xywh: _xywh, text: _text, ...props } = bpmnNodeProps(kind, { xywh: '[0,0,0,0]' });
362
+ const size = NODE_SIZE[kind];
363
+ return {
364
+ swatch: 'glyph',
365
+ // Read by nothing for a glyph row — the element carries its own fill and
366
+ // stroke — and written all the same, because `CommandLegendRow` asks for a
367
+ // colour and the paper is the honest answer. UML does the same.
368
+ color: NODE_FILL,
369
+ props: {
370
+ ...props,
371
+ radius: Math.round((Number(props.radius) || 0) * (SWATCH_H / size.h)),
372
+ },
373
+ aspect: size.h > 0 ? size.w / size.h : 1,
374
+ };
375
+ }
376
+ /**
377
+ * An edge row: the line the tool actually arms, endpoints and all, drawn by the
378
+ * connector renderer at swatch width rather than approximated by a bar.
379
+ *
380
+ * `dashed` is kept beside the props although the connector carries its own
381
+ * `strokeStyle`: it is the one fact about an edge row a pure test can read
382
+ * without a renderer, and it is DERIVED from the same table, so the two can
383
+ * never disagree.
384
+ */
385
+ function edgeRow(key) {
386
+ const style = BPMN_EDGE_STYLE[key];
387
+ return {
388
+ swatch: 'edge',
389
+ color: style.stroke,
390
+ dashed: style.strokeStyle === StrokeStyle.Dash,
391
+ props: { ...style },
392
+ };
393
+ }
394
+ /**
395
+ * The row a spec subscribes, or none.
396
+ *
397
+ * The pool subscribes nothing on purpose: it is the SHEET the legend is drawn
398
+ * on, and listing it would be listing the paper — the same call C4's board and
399
+ * UML's frame make about themselves.
400
+ */
401
+ function legendOf(spec) {
402
+ if (spec.edge) {
403
+ return { role: BPMN_ROLE[spec.edge], row: edgeRow(spec.edge) };
404
+ }
405
+ if (!spec.node)
406
+ return undefined;
407
+ const role = BPMN_ROLE_OF_KIND[spec.node];
408
+ return {
409
+ role,
410
+ row: nodeRow(spec.node),
411
+ ...(EXACT_ROLES.has(role) ? { exact: true } : {}),
412
+ };
413
+ }
414
+ const toolboxCommands = SPECS.map((spec, order) => {
415
+ const legend = legendOf(spec);
416
+ return {
417
+ id: `bpmn.${spec.id}`,
418
+ owner: 'bpmn',
419
+ kind: spec.kind,
420
+ labelKey: `com.labre.commands.bpmn.${spec.id}`,
421
+ labelFallback: spec.label,
422
+ category: spec.category,
423
+ iconKey: spec.iconKey,
424
+ // The catalogue holds all of them; `senior` decides which fourteen the
425
+ // sub-menu opens on before this user has reached for anything.
426
+ surfaces: spec.senior
427
+ ? ['senior-menu', 'catalogue', 'palette', 'agent']
428
+ : ['catalogue', 'palette', 'agent'],
429
+ order,
430
+ scope: 'edgeless',
431
+ defaultKeys: { mac: [], other: [] },
432
+ availability: 'always',
433
+ run: gestureOf(spec),
434
+ telemetry: { framework: 'bpmn', element: spec.element, board: spec.board },
435
+ ...(legend ? { legend } : {}),
436
+ // The board carries the BOX the rows are laid out in, and only the board:
437
+ // exactly one command per framework may declare it.
438
+ ...(spec.board ? { legendBox: BPMN_LEGEND_BOX } : {}),
439
+ };
440
+ });
287
441
  /**
288
442
  * The LANE commands (B4) — the first BPMN entries that are not a toolbox slot.
289
443
  *
@@ -379,7 +533,12 @@ const exportCommand = {
379
533
  labelKey: 'com.labre.commands.bpmn.exportXml',
380
534
  labelFallback: 'Export BPMN XML',
381
535
  descriptionKey: 'com.labre.commands.bpmn.exportXml.description',
382
- descriptionFallback: 'Download the whole board as a BPMN 2.0 XML file, ready to open in any BPMN tool.',
536
+ // The second sentence is the PO's ruling of 2026-09-17 said BEFORE the click
537
+ // rather than after it: only artefacts carrying a BPMN role are written, and
538
+ // the board's generic SVG export (ADR 0025, R34) is what takes the drawing.
539
+ // The export also SAYS what it left out, once, in its own report — but a
540
+ // promise a command makes in the catalogue is cheaper than a surprise.
541
+ descriptionFallback: 'Download the whole board as a BPMN 2.0 XML file, ready to open in any BPMN tool. Only BPMN artefacts are written; export SVG to get everything drawn in the pool.',
383
542
  // Filed with the import it is the other half of — see {@link INTERCHANGE}.
384
543
  // It shipped filed under `swimlanes`, because the pool's "⋮" is where it is
385
544
  // REACHED from and there was no better section for a command with no sibling;
@@ -565,4 +724,7 @@ export const bpmnCommandIcons = {
565
724
  'bpmn.export-xml': bpmnExportXmlIcon,
566
725
  'bpmn.import-xml': bpmnImportXmlIcon,
567
726
  'bpmn.import-svg': bpmnImportSvgIcon,
727
+ // The senior button's 56×56 glyph, so `FrameworkDescriptor.iconKey` resolves
728
+ // through `getCommandIcon`.
729
+ 'bpmn.toolbar': bpmnToolbarIcon,
568
730
  };
package/dist/consts.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { BpmnNodeKind } from '@formicoidea/labre-core/model';
2
+ import { PointStyle, StrokeStyle } from '@formicoidea/labre-core/model';
2
3
  /**
3
4
  * Visual constants for the BPMN basics. Style "C" (hybrid): spec-accurate
4
5
  * shapes and line weights, with accent colour only on the event rings — the
@@ -23,8 +24,9 @@ export declare const START_WIDTH = 2;
23
24
  export declare const END_WIDTH = 4;
24
25
  export declare const NODE_STROKE_WIDTH = 2;
25
26
  /**
26
- * The call activity's border, which the spec draws THICK — it is the one way to
27
- * tell it from the sub-process, since both carry the same `+` marker. Same
27
+ * The call activity's border, which the spec draws THICK — and here it is the
28
+ * ONLY way to tell it from the sub-process, since Labre draws neither of them
29
+ * with the notation's collapsed `+` (`node/node-renderer.ts`). Same
28
30
  * weight as the end-event ring, and for the same reason: this is the heaviest
29
31
  * line the notation uses, and it is spent on "this one stands for a whole
30
32
  * process defined somewhere else".
@@ -260,3 +262,33 @@ export declare const MESSAGE_WIDTH = 2;
260
262
  */
261
263
  export declare const ASSOCIATION_STROKE: string;
262
264
  export declare const ASSOCIATION_WIDTH = 2;
265
+ /** The three connecting objects, keyed the way {@link BPMN_EDGE_STYLE} is. */
266
+ export type BpmnEdgeKey = 'sequenceFlow' | 'messageFlow' | 'association';
267
+ /**
268
+ * What a BPMN connecting object LOOKS like — the five props, and never a sixth.
269
+ *
270
+ * `mode` is deliberately absent. The toolbox arms the connector tool
271
+ * orthogonally, but that is a property of the DRAWING gesture, not of the line:
272
+ * a legend swatch draws the same notation across 44 units in a straight
273
+ * segment, and an orthogonal mode there would put a right angle in a sample
274
+ * that has nothing to route around.
275
+ */
276
+ export interface BpmnEdgeStyle {
277
+ stroke: string;
278
+ strokeStyle: StrokeStyle;
279
+ strokeWidth: number;
280
+ frontEndpointStyle: PointStyle;
281
+ rearEndpointStyle: PointStyle;
282
+ }
283
+ /**
284
+ * The whole BPMN line notation, in one table — read by the tool that ARMS the
285
+ * connector (`actions.ts`) and by the legend row that PICTURES it
286
+ * (`commands.ts`).
287
+ *
288
+ * One table for the same reason `presets.ts` is one builder for the nodes: two
289
+ * spellings of the same line agree the day they are written and drift on the
290
+ * first restyle, and the drift shows up as a legend that documents a notation
291
+ * the board does not use. Same argument, and the same shape, as UML's
292
+ * `UML_EDGE_STYLE`.
293
+ */
294
+ export declare const BPMN_EDGE_STYLE: Record<BpmnEdgeKey, BpmnEdgeStyle>;
package/dist/consts.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { PointStyle, StrokeStyle } from '@formicoidea/labre-core/model';
1
2
  import { NOTATION_NEUTRALS } from '@formicoidea/labre-core/shared/consts';
2
3
  /**
3
4
  * Visual constants for the BPMN basics. Style "C" (hybrid): spec-accurate
@@ -23,8 +24,9 @@ export const START_WIDTH = 2;
23
24
  export const END_WIDTH = 4;
24
25
  export const NODE_STROKE_WIDTH = 2;
25
26
  /**
26
- * The call activity's border, which the spec draws THICK — it is the one way to
27
- * tell it from the sub-process, since both carry the same `+` marker. Same
27
+ * The call activity's border, which the spec draws THICK — and here it is the
28
+ * ONLY way to tell it from the sub-process, since Labre draws neither of them
29
+ * with the notation's collapsed `+` (`node/node-renderer.ts`). Same
28
30
  * weight as the end-event ring, and for the same reason: this is the heaviest
29
31
  * line the notation uses, and it is spent on "this one stands for a whole
30
32
  * process defined somewhere else".
@@ -302,3 +304,37 @@ export const MESSAGE_WIDTH = 2;
302
304
  */
303
305
  export const ASSOCIATION_STROKE = NOTATION_NEUTRALS.ink;
304
306
  export const ASSOCIATION_WIDTH = 2;
307
+ /**
308
+ * The whole BPMN line notation, in one table — read by the tool that ARMS the
309
+ * connector (`actions.ts`) and by the legend row that PICTURES it
310
+ * (`commands.ts`).
311
+ *
312
+ * One table for the same reason `presets.ts` is one builder for the nodes: two
313
+ * spellings of the same line agree the day they are written and drift on the
314
+ * first restyle, and the drift shows up as a legend that documents a notation
315
+ * the board does not use. Same argument, and the same shape, as UML's
316
+ * `UML_EDGE_STYLE`.
317
+ */
318
+ export const BPMN_EDGE_STYLE = {
319
+ sequenceFlow: {
320
+ stroke: SEQUENCE_STROKE,
321
+ strokeStyle: StrokeStyle.Solid,
322
+ strokeWidth: SEQUENCE_WIDTH,
323
+ frontEndpointStyle: PointStyle.None,
324
+ rearEndpointStyle: PointStyle.Triangle,
325
+ },
326
+ messageFlow: {
327
+ stroke: MESSAGE_STROKE,
328
+ strokeStyle: StrokeStyle.Dash,
329
+ strokeWidth: MESSAGE_WIDTH,
330
+ frontEndpointStyle: PointStyle.Circle,
331
+ rearEndpointStyle: PointStyle.Arrow,
332
+ },
333
+ association: {
334
+ stroke: ASSOCIATION_STROKE,
335
+ strokeStyle: StrokeStyle.Dash,
336
+ strokeWidth: ASSOCIATION_WIDTH,
337
+ frontEndpointStyle: PointStyle.None,
338
+ rearEndpointStyle: PointStyle.None,
339
+ },
340
+ };
@@ -301,7 +301,7 @@ export class BpmnPoolView extends GfxElementModelView {
301
301
  font: '14px Inter, sans-serif',
302
302
  color: 'var(--affine-text-primary-color, #1f2328)',
303
303
  background: 'var(--affine-background-overlay-panel-color, #ffffff)',
304
- border: '1px solid var(--affine-primary-color, #1e96eb)',
304
+ border: '1px solid var(--affine-primary-color)',
305
305
  borderRadius: '6px',
306
306
  boxShadow: 'var(--affine-shadow-2, 0 2px 8px rgba(0,0,0,0.18))',
307
307
  outline: 'none',
package/dist/export.d.ts CHANGED
@@ -217,6 +217,18 @@ export interface BpmnExportBoard {
217
217
  pools: readonly BpmnPoolElementModel[];
218
218
  nodes: readonly BpmnNodeElementModel[];
219
219
  connectors: readonly ConnectorElementModel[];
220
+ /**
221
+ * How many things drawn INSIDE a pool the picking left behind because they
222
+ * carry no BPMN role — a free shape, a text, an artefact drawn before roles
223
+ * existed (2026-08-26).
224
+ *
225
+ * Counted by whoever picked the board (`bpmnBoardFrom`), because that is the
226
+ * only place that ever sees what was NOT picked. The behaviour is the PO's
227
+ * ruling of 2026-09-17 and does not change: a `.bpmn` carries the author's
228
+ * statements, and the board's generic SVG export (ADR 0025, R34) carries the
229
+ * drawing. What was missing was SAYING so, which is what this number buys.
230
+ */
231
+ leftOut?: number;
220
232
  }
221
233
  export interface BpmnExportOptions {
222
234
  /** Names the `collaboration` / lone `process` and the `BPMNDiagram`. */
@@ -237,7 +249,7 @@ export interface BpmnExportWarning {
237
249
  /** Values for the fallback's placeholders — a count, a list of names. */
238
250
  params?: Record<string, string | number>;
239
251
  }
240
- /** All nine, for `translations.ts`'s manifest contribution. */
252
+ /** All ten, for `translations.ts`'s manifest contribution. */
241
253
  export declare const BPMN_EXPORT_WARNING_KEYS: readonly (readonly [string, string])[];
242
254
  /**
243
255
  * The document, plus what writing it could not say.
package/dist/export.js CHANGED
@@ -764,7 +764,7 @@ class IdMinter {
764
764
  }
765
765
  }
766
766
  /**
767
- * The nine export-warning keys, each paired with its English fallback
767
+ * The ten export-warning keys, each paired with its English fallback
768
768
  * (`{{name}}` placeholders included) — declared ONCE, here, and read both by
769
769
  * the `if` blocks below (which add the live `params`) and by
770
770
  * `translations.ts`'s manifest (which needs the pair with no board to run
@@ -812,7 +812,11 @@ const UNWRITABLE_EDGES_WARNING = [
812
812
  'com.labre.bpmn.export.warning.unwritable-edges',
813
813
  '{{count}} arrow(s) were left out: BPMN requires both ends of a flow to be named, and they have an end that is loose or attached to something that is not a BPMN artefact.',
814
814
  ];
815
- /** All nine, for `translations.ts`'s manifest contribution. */
815
+ const LEFT_OUT_WARNING = [
816
+ 'com.labre.bpmn.export.warning.left-out',
817
+ '{{count}} element(s) inside the pool are not BPMN elements and were left out of the .bpmn file. Export SVG to get everything drawn in the pool.',
818
+ ];
819
+ /** All ten, for `translations.ts`'s manifest contribution. */
816
820
  export const BPMN_EXPORT_WARNING_KEYS = [
817
821
  UNDRAWN_ARTEFACTS_WARNING,
818
822
  DROPPED_MESSAGE_FLOWS_WARNING,
@@ -823,6 +827,7 @@ export const BPMN_EXPORT_WARNING_KEYS = [
823
827
  CONFLICTING_IDS_WARNING,
824
828
  REFUSED_ATTRIBUTE_NAMES_WARNING,
825
829
  UNWRITABLE_EDGES_WARNING,
830
+ LEFT_OUT_WARNING,
826
831
  ];
827
832
  /* ── The plan ─────────────────────────────────────────────────────────── */
828
833
  /**
@@ -1425,9 +1430,11 @@ export function exportBpmnXmlWithWarnings(board, options = {}) {
1425
1430
  id: minter.mint('Shape', node.id),
1426
1431
  bpmnElement: node.id,
1427
1432
  };
1428
- // The pack draws the COLLAPSED sub-process only — a task-sized box with a
1429
- // `+` — and `isExpanded="false"` is how DI says exactly that. Without it a
1430
- // tool is free to draw an expanded container the author never made.
1433
+ // The pack draws the COLLAPSED sub-process only — a task-sized box, with
1434
+ // no body folded open inside it — and `isExpanded="false"` is how DI says
1435
+ // exactly that. Without it a tool is free to draw an expanded container the
1436
+ // author never made. (Labre leaves the `+` marker itself off the box, which
1437
+ // is a drawing choice and says nothing about the DI flag.)
1431
1438
  if (node.model.kind === 'subProcess' ||
1432
1439
  node.model.kind === 'callActivity') {
1433
1440
  attrs.isExpanded = 'false';
@@ -1621,6 +1628,13 @@ export function exportBpmnXmlWithWarnings(board, options = {}) {
1621
1628
  const [key, fallback] = UNWRITABLE_EDGES_WARNING;
1622
1629
  warnings.push({ key, fallback, params: { count: unwritableEdges } });
1623
1630
  }
1631
+ // What the PICKING left behind (see {@link BpmnExportBoard.leftOut}). One
1632
+ // line for the whole pool, because the point is not which shape it was: it is
1633
+ // that the file is the process and the SVG is the picture.
1634
+ if (board.leftOut !== undefined && board.leftOut > 0) {
1635
+ const [key, fallback] = LEFT_OUT_WARNING;
1636
+ warnings.push({ key, fallback, params: { count: board.leftOut } });
1637
+ }
1624
1638
  return {
1625
1639
  text: `<?xml version="1.0" encoding="UTF-8"?>\n${serializeElement(definitions, '')}\n`,
1626
1640
  warnings,
package/dist/import.d.ts CHANGED
@@ -62,6 +62,16 @@ export declare const BPMN_IMPORT_ERRORS: {
62
62
  readonly malformedXml: readonly ["com.labre.bpmn.import.error.malformed-xml", "This file is not well-formed XML, so no BPMN can be read out of it: {{detail}}"];
63
63
  readonly notDefinitions: readonly ["com.labre.bpmn.import.error.not-definitions", "A BPMN file opens on <definitions>; this one opens on <{{tag}}>."];
64
64
  readonly wrongNamespace: readonly ["com.labre.bpmn.import.error.wrong-namespace", "This <definitions> is in \"{{namespace}}\", not in BPMN 2.0's (\"{{bpmnNamespace}}\"). A DMN decision model and a BPMN process open on the same element name and are not the same file."];
65
+ /**
66
+ * Its OWN entry rather than `wrongNamespace` with the words "no namespace"
67
+ * poured into `{{namespace}}` (#390). That fragment was English prose
68
+ * substituted into a translated sentence — the one shape a `messageParams`
69
+ * hole cannot carry, because the reader is pure and has no `std` to resolve
70
+ * it with. It is also a different FACT, which is what this table is a list
71
+ * of: a `<definitions>` in the wrong namespace names one, a `<definitions>`
72
+ * in none names nothing at all.
73
+ */
74
+ readonly missingNamespace: readonly ["com.labre.bpmn.import.error.missing-namespace", "This <definitions> declares no namespace, so it does not say it is BPMN 2.0 (\"{{bpmnNamespace}}\"). A DMN decision model and a BPMN process open on the same element name and are not the same file."];
65
75
  readonly declinedRootKind: readonly ["com.labre.bpmn.import.error.declined-root-kind", "This file is a BPMN {{kind}}, which Labre does not draw. Only a process or a collaboration can be imported."];
66
76
  };
67
77
  /**
package/dist/import.js CHANGED
@@ -120,6 +120,19 @@ export const BPMN_IMPORT_ERRORS = {
120
120
  'com.labre.bpmn.import.error.wrong-namespace',
121
121
  'This <definitions> is in "{{namespace}}", not in BPMN 2.0\'s ("{{bpmnNamespace}}"). A DMN decision model and a BPMN process open on the same element name and are not the same file.',
122
122
  ],
123
+ /**
124
+ * Its OWN entry rather than `wrongNamespace` with the words "no namespace"
125
+ * poured into `{{namespace}}` (#390). That fragment was English prose
126
+ * substituted into a translated sentence — the one shape a `messageParams`
127
+ * hole cannot carry, because the reader is pure and has no `std` to resolve
128
+ * it with. It is also a different FACT, which is what this table is a list
129
+ * of: a `<definitions>` in the wrong namespace names one, a `<definitions>`
130
+ * in none names nothing at all.
131
+ */
132
+ missingNamespace: [
133
+ 'com.labre.bpmn.import.error.missing-namespace',
134
+ 'This <definitions> declares no namespace, so it does not say it is BPMN 2.0 ("{{bpmnNamespace}}"). A DMN decision model and a BPMN process open on the same element name and are not the same file.',
135
+ ],
123
136
  declinedRootKind: [
124
137
  'com.labre.bpmn.import.error.declined-root-kind',
125
138
  'This file is a BPMN {{kind}}, which Labre does not draw. Only a process or a collaboration can be imported.',
@@ -469,7 +482,19 @@ function parseDefinitions(source) {
469
482
  // Without this a `.dmn` imports as an empty board, which is exactly the
470
483
  // "three zeroes claiming an empty process" this reader refuses to return.
471
484
  if (root.namespaceURI !== BPMN_NS.model) {
472
- const namespace = root.namespaceURI ?? 'no namespace';
485
+ // Two facts, two refusals: a namespace this reader can quote back, and no
486
+ // namespace at all. The second used to borrow the first's sentence with
487
+ // the English words "no namespace" poured into its `{{namespace}}` hole,
488
+ // which put a fragment of English inside a translated sentence (#390).
489
+ const namespace = root.namespaceURI;
490
+ if (namespace === null) {
491
+ throw new InterchangeImportError(`This <definitions> declares no namespace, so it does not say it is ` +
492
+ `BPMN 2.0 ("${BPMN_NS.model}"). A DMN decision model and a BPMN ` +
493
+ `process open on the same element name and are not the same file.`, {
494
+ messageKey: BPMN_IMPORT_ERRORS.missingNamespace[0],
495
+ messageParams: { bpmnNamespace: BPMN_NS.model },
496
+ });
497
+ }
473
498
  throw new InterchangeImportError(`This <definitions> is in "${namespace}", not ` +
474
499
  `in BPMN 2.0's ("${BPMN_NS.model}"). A DMN decision model and a BPMN ` +
475
500
  `process open on the same element name and are not the same file.`, {