@formicoidea/labre-framework-bpmn 0.42.0 → 0.45.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.
package/dist/actions.d.ts CHANGED
@@ -2,7 +2,17 @@ import { type InterchangeReport, type SerializedElementProps } from '@formicoide
2
2
  import { type BpmnLane, type BpmnNodeKind, BpmnPoolElementModel } from '@formicoidea/labre-core/model';
3
3
  import type { BlockStdScope } from '@formicoidea/labre-core/std';
4
4
  import { type BpmnExportBoard } from './export.js';
5
- /** Create a flow-object node (native shape) centred on the viewport. */
5
+ /**
6
+ * Create a flow-object node (native shape) centred on the viewport.
7
+ *
8
+ * An inscribed kind (activities, annotation, group) carries its name as the
9
+ * shape's own inner text. An external kind (events, gateways, data shapes)
10
+ * is born WITHOUT inner text and gets a gravitating label instead: a free
11
+ * `bpmn:label` text centred under the symbol, grouped with it through the
12
+ * native group command — exactly Wardley's `createWardleyNode` (R38). The
13
+ * group is what makes the pair move as one; the role is what tells the name
14
+ * from a note typed beside the process.
15
+ */
6
16
  export declare function createBpmnNode(std: BlockStdScope, kind: BpmnNodeKind): void;
7
17
  /** Create a pool (background container) centred on the viewport. */
8
18
  export declare function createBpmnPool(std: BlockStdScope): void;
package/dist/actions.js CHANGED
@@ -1,14 +1,15 @@
1
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 { createGroupCommand } from '@formicoidea/labre-core/gfx/group';
3
4
  import { BpmnPoolElementModel, ConnectorMode, } from '@formicoidea/labre-core/model';
4
5
  import { NotificationProvider, translateKey, } from '@formicoidea/labre-core/shared/services';
5
6
  import { downloadBlob } from '@formicoidea/labre-core/shared/utils';
6
7
  import { Bound } from '@formicoidea/labre-core/global/gfx';
7
8
  import { GfxControllerIdentifier } from '@formicoidea/labre-core/std/gfx';
8
- import { BPMN_EDGE_STYLE, LANE_NAME_FALLBACK, LANE_NAME_KEY, NODE_LABEL, nodeLabelKey, NODE_SIZE, POOL_NAME_FALLBACK, POOL_NAME_KEY, } from './consts.js';
9
+ import { BPMN_EDGE_STYLE, bpmnLabelMode, LANE_NAME_FALLBACK, LANE_NAME_KEY, NODE_LABEL, nodeLabelKey, NODE_SIZE, POOL_NAME_FALLBACK, POOL_NAME_KEY, } from './consts.js';
9
10
  import { BPMN_FORMAT_ID, exportBpmnXmlWithWarnings, } from './export.js';
10
11
  import { BPMN_SVG_IMPORT, BPMN_XML_EXPORT, BPMN_XML_FORMAT, BPMN_XML_IMPORT, bpmnBoardFrom, bpmnSafeFilename, } from './interchange.js';
11
- import { bpmnNodeProps, bpmnPoolProps } from './presets.js';
12
+ import { bpmnLabelBoxFor, bpmnLabelProps, bpmnNodeProps, bpmnPoolProps, } from './presets.js';
12
13
  import { BPMN_ROLE } from './roles.js';
13
14
  /**
14
15
  * Standalone creation/activation actions for the BPMN toolbox — lifted out of
@@ -22,7 +23,28 @@ function finish(gfx, id) {
22
23
  gfx.selection.set({ elements: [id], editing: false });
23
24
  // Keep the palette open (native sub-menu behaviour).
24
25
  }
25
- /** Create a flow-object node (native shape) centred on the viewport. */
26
+ /**
27
+ * Group elements; returns the group id, or the first id if grouping failed —
28
+ * the same fallback as Wardley's helper, so a failed group still leaves the
29
+ * symbol selected rather than nothing.
30
+ */
31
+ function group(gfx, ids) {
32
+ const [, result] = gfx.std.command.exec(createGroupCommand, {
33
+ elements: ids,
34
+ });
35
+ return result.groupId || ids[0];
36
+ }
37
+ /**
38
+ * Create a flow-object node (native shape) centred on the viewport.
39
+ *
40
+ * An inscribed kind (activities, annotation, group) carries its name as the
41
+ * shape's own inner text. An external kind (events, gateways, data shapes)
42
+ * is born WITHOUT inner text and gets a gravitating label instead: a free
43
+ * `bpmn:label` text centred under the symbol, grouped with it through the
44
+ * native group command — exactly Wardley's `createWardleyNode` (R38). The
45
+ * group is what makes the pair move as one; the role is what tells the name
46
+ * from a note typed beside the process.
47
+ */
26
48
  export function createBpmnNode(std, kind) {
27
49
  const gfx = gfxOf(std);
28
50
  const surface = gfx.surface;
@@ -30,6 +52,14 @@ export function createBpmnNode(std, kind) {
30
52
  return;
31
53
  const { w, h } = NODE_SIZE[kind];
32
54
  const { centerX: cx, centerY: cy } = gfx.viewport;
55
+ // Translated HERE and once: the caption is document content the moment it
56
+ // lands, so the host's catalogue is asked at placement and never again
57
+ // (`nodeLabelKey`, and #183 on why a French board should not start in
58
+ // English).
59
+ const seed = NODE_LABEL[kind]
60
+ ? translateKey(std, nodeLabelKey(kind), NODE_LABEL[kind])
61
+ : undefined;
62
+ const external = bpmnLabelMode(kind) === 'external';
33
63
  // What a node IS lives in one place (`./presets.ts`), because the importer
34
64
  // creates the same artefacts out of a `.bpmn` file and the two must not
35
65
  // drift: a task read from a file and a task drawn here are one element type
@@ -37,15 +67,15 @@ export function createBpmnNode(std, kind) {
37
67
  // nothing else.
38
68
  const id = surface.addElement(bpmnNodeProps(kind, {
39
69
  xywh: new Bound(cx - w / 2, cy - h / 2, w, h).serialize(),
40
- // Translated HERE and once: the caption is document content the moment
41
- // it lands, so the host's catalogue is asked at placement and never
42
- // again (`nodeLabelKey`, and #183 on why a French board should not
43
- // start in English).
44
- text: NODE_LABEL[kind]
45
- ? translateKey(std, nodeLabelKey(kind), NODE_LABEL[kind])
46
- : undefined,
70
+ text: external ? undefined : seed,
47
71
  }));
48
- finish(gfx, id);
72
+ if (!external || seed === undefined) {
73
+ finish(gfx, id);
74
+ return;
75
+ }
76
+ const { x, y } = bpmnLabelBoxFor(kind, cx, cy);
77
+ const labelId = surface.addElement(bpmnLabelProps(seed, x, y));
78
+ finish(gfx, group(gfx, [id, labelId]));
49
79
  }
50
80
  /** Create a pool (background container) centred on the viewport. */
51
81
  export function createBpmnPool(std) {
package/dist/consts.d.ts CHANGED
@@ -35,8 +35,9 @@ export declare const CALL_ACTIVITY_WIDTH = 4;
35
35
  /** Task corner radius (absolute px — a lightly rounded rectangle). */
36
36
  export declare const TASK_RADIUS = 10;
37
37
  /**
38
- * The group's corner radius — twice a task's, because it is drawn at three
39
- * times the size and a 10-unit corner on a 300-unit box reads as a square one.
38
+ * The group's corner radius — twice a task's, because a group is born the
39
+ * biggest artefact on the board and a 10-unit corner on a 300-unit box reads
40
+ * as a square one.
40
41
  */
41
42
  export declare const GROUP_RADIUS = 20;
42
43
  /**
@@ -58,7 +59,8 @@ export declare const INNER_FONT_SIZE = 18;
58
59
  *
59
60
  * A file draws its artefacts at whatever scale its author's tool uses, and
60
61
  * bpmn.io's normative sizes are about six tenths of this pack's: a 36-unit
61
- * event against a 56-unit one, a 100×72 task against a 120×72. The label of an
62
+ * event against a 56-unit one, a 100×80 task against the 120×72 activities are
63
+ * fitted against (`ACTIVITY_FIT_REF`). The label of an
62
64
  * imported artefact is therefore asked for at a size proportional to the box
63
65
  * the FILE gave, and 10 is where that shrinking stops — under it a label is no
64
66
  * longer read, it is guessed at.
@@ -75,19 +77,20 @@ export declare const LABEL_MIN_FONT_SIZE = 10;
75
77
  * How much of a box a fitted label gives up to its margin, per side.
76
78
  *
77
79
  * A shape's native inset is a FIXED 20 units horizontally
78
- * (`SHAPE_TEXT_PADDING`), which is a sixth of this pack's 120-unit task and
79
- * therefore invisible on a drawn board — and more than the whole width of a
80
- * 36-unit event, which leaves NEGATIVE room for the text and is why an imported
81
- * label breaks in the middle of a word. Expressed as a ratio, the margin
82
- * follows the artefact down: it is exactly the native inset at the pack's own
83
- * task width and it never grows past it, so a drawn node is untouched and a
84
- * small one keeps a usable line.
80
+ * (`SHAPE_TEXT_PADDING`), which is a sixth of the 120-unit activity width
81
+ * labels are fitted against (`ACTIVITY_FIT_REF`) and therefore invisible on a
82
+ * drawn board — and more than the whole width of a 36-unit event, which leaves
83
+ * NEGATIVE room for the text and is why an imported label breaks in the middle
84
+ * of a word. Expressed as a ratio, the margin follows the artefact down: it is
85
+ * exactly the native inset at 120 units and it never grows past it (the cap in
86
+ * `bpmnLabelFit`), so a drawn node — 180 wide since activities grew — is
87
+ * untouched and a small one keeps a usable line.
85
88
  */
86
89
  export declare const LABEL_INSET_RATIO: number;
87
90
  /**
88
91
  * Default node sizes (model units) per kind.
89
92
  *
90
- * Three sizes carry the whole scale: the 56-unit event, the 120×72 task and the
93
+ * Three sizes carry the whole scale: the 56-unit event, the 180×108 task and the
91
94
  * 72-unit gateway. Everything the descriptive profile adds takes one of them —
92
95
  * a message start is a start event, a user task is a task — except the three
93
96
  * data/artifact shapes, which have no sibling to inherit from:
@@ -96,32 +99,74 @@ export declare const LABEL_INSET_RATIO: number;
96
99
  * 56-unit event without looking like a shrunken task;
97
100
  * - `dataStore` is the event's own diameter, which is what a cylinder needs to
98
101
  * read as one rather than as a squashed ellipse;
99
- * - `textAnnotation` is wider than a task and shorter — it holds a sentence,
100
- * not a verb phrase.
102
+ * - `textAnnotation` is a wide, short strip (140×48) — it holds a sentence,
103
+ * not a verb phrase. It was sized against the 120×72 task and kept when the
104
+ * activities grew.
101
105
  *
102
106
  * These three are ~1.2–1.4× bpmn.io's normative pixel sizes, which is the ratio
103
- * this pack's event and task already sit at against the same reference.
107
+ * this pack's event sits at against the same reference (the activities sat
108
+ * there too until they grew 1.5×, see below).
104
109
  *
105
110
  * `group` is on no scale at all: it is a LASSO, so it has to be born big enough
106
- * to have something in it. 300×200 holds two tasks and the arrow between them,
107
- * which is the smallest thing anybody draws a group around.
111
+ * to have something in it — bigger than any other artefact on both axes. It
112
+ * was sized for two 120×72 tasks and the arrow between them; since the
113
+ * activities grew it holds one task with room around it, and the author
114
+ * stretches it from there.
108
115
  */
109
116
  export declare const NODE_SIZE: Record<BpmnNodeKind, {
110
117
  w: number;
111
118
  h: number;
112
119
  }>;
113
120
  /**
114
- * Default inner text per kind.
121
+ * The box an ACTIVITY's inscribed label is fitted against on import
122
+ * (`bpmnLabelFit`, `presets.ts`) — the 120×72 the drawn task shipped at, kept
123
+ * as the fit reference when the drawn task grew to 180×108.
115
124
  *
116
- * The activities carry one, because a rectangle with nothing written in it says
117
- * nothing at all. Events and gateways do not: their meaning is the glyph, and
118
- * BPMN puts whatever name they have OUTSIDE the symbol.
125
+ * The fit scales the 18-unit type by how much smaller a file's box is than
126
+ * "the box this typography is comfortable in". That box did not change when
127
+ * the palette's did: 18 units were comfortable at 120×72 (that is why the pack
128
+ * shipped there), so a bpmn.io task of 100×80 still reads at 15 units rather
129
+ * than falling to the 10-unit floor it would hit against 180×108.
130
+ */
131
+ export declare const ACTIVITY_FIT_REF: {
132
+ w: number;
133
+ h: number;
134
+ };
135
+ /** The five activity kinds — the ones drawn at the 180×108 box. */
136
+ export declare const isBpmnActivityKind: (kind: BpmnNodeKind) => boolean;
137
+ /**
138
+ * The kinds whose name GRAVITATES: a free text element centred under the
139
+ * symbol, grouped with it — never the shape's own inner text (R38).
140
+ *
141
+ * Events, gateways and the two data shapes are small, punctual symbols: two
142
+ * five-letter words at 18 units do not fit a 56-unit ring or a 72-unit diamond
143
+ * without enlarging it, and the glyph is already inside. BPMN itself draws
144
+ * their name below the symbol, which is what `bpmnLabelBoxFor` places.
145
+ *
146
+ * The activities, the annotation and the group stay INSCRIBED: the label fits
147
+ * (`label-mode.unit.spec.ts` proves it against the rule's arithmetic), and for
148
+ * the annotation the text IS the artefact.
149
+ *
150
+ * A stored node keeps whatever it was born with: an event drawn before this
151
+ * list existed carries inner text and keeps painting it. Nothing is migrated.
152
+ */
153
+ export declare const BPMN_EXTERNAL_LABEL_KINDS: readonly ["startEvent", "startEventMessage", "startEventTimer", "endEvent", "endEventMessage", "endEventTerminate", "gatewayExclusive", "gatewayParallel", "dataObject", "dataStore"];
154
+ /** Whether a kind's name is the shape's inner text or a grouped text beside it. */
155
+ export declare const bpmnLabelMode: (kind: BpmnNodeKind) => "inscribed" | "external";
156
+ /** Gravitating-label metrics: a 120-wide, one-line box under the symbol. */
157
+ export declare const LABEL_FONT_SIZE = 18;
158
+ export declare const BPMN_LABEL_W = 120;
159
+ export declare const BPMN_LABEL_H: number;
160
+ /** Gap between the symbol's bottom edge and the label's top. */
161
+ export declare const BPMN_LABEL_GAP = 6;
162
+ /**
163
+ * Default text per kind — the shape's inner text for an inscribed kind, the
164
+ * grouped label's text for an external one ({@link bpmnLabelMode}).
119
165
  *
120
- * `dataObject` and `dataStore` are empty for the same reason plus one of our
121
- * own: the spec puts their name under the shape, the native inner text can only
122
- * go inside it, and inside is where the folded page and the cylinder already
123
- * are. The user can still type — the text simply overflows, which is the
124
- * honest failure rather than a label painted over the glyph.
166
+ * Every kind carries one, because a symbol whose label is born empty is a text
167
+ * element nobody can find to click (PO, 24/09/2026 — the Wardley precedent
168
+ * seeds "Component" for the same reason). The external kinds are seeded with
169
+ * the spec's own name for the artefact, the wording their role already carries.
125
170
  *
126
171
  * The caption is asked of the host's catalogue under {@link nodeLabelKey} when
127
172
  * the artefact is placed, with the entry below as the English default.
@@ -133,8 +178,8 @@ export declare const NODE_LABEL: Record<BpmnNodeKind, string>;
133
178
  * Resolved AT PLACEMENT (`createBpmnNode`) and never afterwards: what a gesture
134
179
  * writes into the document is content the author owns from that moment on, and
135
180
  * a renderer that re-translated it on every paint would silently overwrite a
136
- * name somebody typed. The kinds with an empty label ask for nothing — there is
137
- * no word to translate, and BPMN puts an event's name outside its glyph anyway.
181
+ * name somebody typed. Every kind has a seed now; for an external kind it is
182
+ * written into the grouped `bpmn:label` rather than the shape (R38).
138
183
  */
139
184
  export declare const nodeLabelKey: (kind: BpmnNodeKind) => string;
140
185
  /**
package/dist/consts.js CHANGED
@@ -35,8 +35,9 @@ export const CALL_ACTIVITY_WIDTH = END_WIDTH;
35
35
  /** Task corner radius (absolute px — a lightly rounded rectangle). */
36
36
  export const TASK_RADIUS = 10;
37
37
  /**
38
- * The group's corner radius — twice a task's, because it is drawn at three
39
- * times the size and a 10-unit corner on a 300-unit box reads as a square one.
38
+ * The group's corner radius — twice a task's, because a group is born the
39
+ * biggest artefact on the board and a 10-unit corner on a 300-unit box reads
40
+ * as a square one.
40
41
  */
41
42
  export const GROUP_RADIUS = 20;
42
43
  /**
@@ -58,7 +59,8 @@ export const INNER_FONT_SIZE = 18;
58
59
  *
59
60
  * A file draws its artefacts at whatever scale its author's tool uses, and
60
61
  * bpmn.io's normative sizes are about six tenths of this pack's: a 36-unit
61
- * event against a 56-unit one, a 100×72 task against a 120×72. The label of an
62
+ * event against a 56-unit one, a 100×80 task against the 120×72 activities are
63
+ * fitted against (`ACTIVITY_FIT_REF`). The label of an
62
64
  * imported artefact is therefore asked for at a size proportional to the box
63
65
  * the FILE gave, and 10 is where that shrinking stops — under it a label is no
64
66
  * longer read, it is guessed at.
@@ -75,19 +77,20 @@ export const LABEL_MIN_FONT_SIZE = 10;
75
77
  * How much of a box a fitted label gives up to its margin, per side.
76
78
  *
77
79
  * A shape's native inset is a FIXED 20 units horizontally
78
- * (`SHAPE_TEXT_PADDING`), which is a sixth of this pack's 120-unit task and
79
- * therefore invisible on a drawn board — and more than the whole width of a
80
- * 36-unit event, which leaves NEGATIVE room for the text and is why an imported
81
- * label breaks in the middle of a word. Expressed as a ratio, the margin
82
- * follows the artefact down: it is exactly the native inset at the pack's own
83
- * task width and it never grows past it, so a drawn node is untouched and a
84
- * small one keeps a usable line.
80
+ * (`SHAPE_TEXT_PADDING`), which is a sixth of the 120-unit activity width
81
+ * labels are fitted against (`ACTIVITY_FIT_REF`) and therefore invisible on a
82
+ * drawn board — and more than the whole width of a 36-unit event, which leaves
83
+ * NEGATIVE room for the text and is why an imported label breaks in the middle
84
+ * of a word. Expressed as a ratio, the margin follows the artefact down: it is
85
+ * exactly the native inset at 120 units and it never grows past it (the cap in
86
+ * `bpmnLabelFit`), so a drawn node — 180 wide since activities grew — is
87
+ * untouched and a small one keeps a usable line.
85
88
  */
86
89
  export const LABEL_INSET_RATIO = 1 / 6;
87
90
  /**
88
91
  * Default node sizes (model units) per kind.
89
92
  *
90
- * Three sizes carry the whole scale: the 56-unit event, the 120×72 task and the
93
+ * Three sizes carry the whole scale: the 56-unit event, the 180×108 task and the
91
94
  * 72-unit gateway. Everything the descriptive profile adds takes one of them —
92
95
  * a message start is a start event, a user task is a task — except the three
93
96
  * data/artifact shapes, which have no sibling to inherit from:
@@ -96,15 +99,19 @@ export const LABEL_INSET_RATIO = 1 / 6;
96
99
  * 56-unit event without looking like a shrunken task;
97
100
  * - `dataStore` is the event's own diameter, which is what a cylinder needs to
98
101
  * read as one rather than as a squashed ellipse;
99
- * - `textAnnotation` is wider than a task and shorter — it holds a sentence,
100
- * not a verb phrase.
102
+ * - `textAnnotation` is a wide, short strip (140×48) — it holds a sentence,
103
+ * not a verb phrase. It was sized against the 120×72 task and kept when the
104
+ * activities grew.
101
105
  *
102
106
  * These three are ~1.2–1.4× bpmn.io's normative pixel sizes, which is the ratio
103
- * this pack's event and task already sit at against the same reference.
107
+ * this pack's event sits at against the same reference (the activities sat
108
+ * there too until they grew 1.5×, see below).
104
109
  *
105
110
  * `group` is on no scale at all: it is a LASSO, so it has to be born big enough
106
- * to have something in it. 300×200 holds two tasks and the arrow between them,
107
- * which is the smallest thing anybody draws a group around.
111
+ * to have something in it — bigger than any other artefact on both axes. It
112
+ * was sized for two 120×72 tasks and the arrow between them; since the
113
+ * activities grew it holds one task with room around it, and the author
114
+ * stretches it from there.
108
115
  */
109
116
  export const NODE_SIZE = {
110
117
  startEvent: { w: 56, h: 56 },
@@ -113,11 +120,14 @@ export const NODE_SIZE = {
113
120
  endEvent: { w: 56, h: 56 },
114
121
  endEventMessage: { w: 56, h: 56 },
115
122
  endEventTerminate: { w: 56, h: 56 },
116
- task: { w: 120, h: 72 },
117
- taskUser: { w: 120, h: 72 },
118
- taskService: { w: 120, h: 72 },
119
- subProcess: { w: 120, h: 72 },
120
- callActivity: { w: 120, h: 72 },
123
+ // Activities: 1.5× the 120×72 they shipped at (user feedback, 24/09/2026):
124
+ // the inscribed label at 18 units needs the room, and the type does not
125
+ // grow with the box (R38, `docs/add-a-framework/02-framework-rules.md`).
126
+ task: { w: 180, h: 108 },
127
+ taskUser: { w: 180, h: 108 },
128
+ taskService: { w: 180, h: 108 },
129
+ subProcess: { w: 180, h: 108 },
130
+ callActivity: { w: 180, h: 108 },
121
131
  gatewayExclusive: { w: 72, h: 72 },
122
132
  gatewayParallel: { w: 72, h: 72 },
123
133
  dataObject: { w: 48, h: 64 },
@@ -126,37 +136,91 @@ export const NODE_SIZE = {
126
136
  group: { w: 300, h: 200 },
127
137
  };
128
138
  /**
129
- * Default inner text per kind.
139
+ * The box an ACTIVITY's inscribed label is fitted against on import
140
+ * (`bpmnLabelFit`, `presets.ts`) — the 120×72 the drawn task shipped at, kept
141
+ * as the fit reference when the drawn task grew to 180×108.
130
142
  *
131
- * The activities carry one, because a rectangle with nothing written in it says
132
- * nothing at all. Events and gateways do not: their meaning is the glyph, and
133
- * BPMN puts whatever name they have OUTSIDE the symbol.
143
+ * The fit scales the 18-unit type by how much smaller a file's box is than
144
+ * "the box this typography is comfortable in". That box did not change when
145
+ * the palette's did: 18 units were comfortable at 120×72 (that is why the pack
146
+ * shipped there), so a bpmn.io task of 100×80 still reads at 15 units rather
147
+ * than falling to the 10-unit floor it would hit against 180×108.
148
+ */
149
+ export const ACTIVITY_FIT_REF = { w: 120, h: 72 };
150
+ const ACTIVITY_KINDS = new Set([
151
+ 'task',
152
+ 'taskUser',
153
+ 'taskService',
154
+ 'subProcess',
155
+ 'callActivity',
156
+ ]);
157
+ /** The five activity kinds — the ones drawn at the 180×108 box. */
158
+ export const isBpmnActivityKind = (kind) => ACTIVITY_KINDS.has(kind);
159
+ /**
160
+ * The kinds whose name GRAVITATES: a free text element centred under the
161
+ * symbol, grouped with it — never the shape's own inner text (R38).
162
+ *
163
+ * Events, gateways and the two data shapes are small, punctual symbols: two
164
+ * five-letter words at 18 units do not fit a 56-unit ring or a 72-unit diamond
165
+ * without enlarging it, and the glyph is already inside. BPMN itself draws
166
+ * their name below the symbol, which is what `bpmnLabelBoxFor` places.
167
+ *
168
+ * The activities, the annotation and the group stay INSCRIBED: the label fits
169
+ * (`label-mode.unit.spec.ts` proves it against the rule's arithmetic), and for
170
+ * the annotation the text IS the artefact.
171
+ *
172
+ * A stored node keeps whatever it was born with: an event drawn before this
173
+ * list existed carries inner text and keeps painting it. Nothing is migrated.
174
+ */
175
+ export const BPMN_EXTERNAL_LABEL_KINDS = [
176
+ 'startEvent',
177
+ 'startEventMessage',
178
+ 'startEventTimer',
179
+ 'endEvent',
180
+ 'endEventMessage',
181
+ 'endEventTerminate',
182
+ 'gatewayExclusive',
183
+ 'gatewayParallel',
184
+ 'dataObject',
185
+ 'dataStore',
186
+ ];
187
+ const EXTERNAL_LABEL_KINDS = new Set(BPMN_EXTERNAL_LABEL_KINDS);
188
+ /** Whether a kind's name is the shape's inner text or a grouped text beside it. */
189
+ export const bpmnLabelMode = (kind) => EXTERNAL_LABEL_KINDS.has(kind) ? 'external' : 'inscribed';
190
+ /** Gravitating-label metrics: a 120-wide, one-line box under the symbol. */
191
+ export const LABEL_FONT_SIZE = INNER_FONT_SIZE;
192
+ export const BPMN_LABEL_W = 120;
193
+ export const BPMN_LABEL_H = LABEL_FONT_SIZE + 8;
194
+ /** Gap between the symbol's bottom edge and the label's top. */
195
+ export const BPMN_LABEL_GAP = 6;
196
+ /**
197
+ * Default text per kind — the shape's inner text for an inscribed kind, the
198
+ * grouped label's text for an external one ({@link bpmnLabelMode}).
134
199
  *
135
- * `dataObject` and `dataStore` are empty for the same reason plus one of our
136
- * own: the spec puts their name under the shape, the native inner text can only
137
- * go inside it, and inside is where the folded page and the cylinder already
138
- * are. The user can still type — the text simply overflows, which is the
139
- * honest failure rather than a label painted over the glyph.
200
+ * Every kind carries one, because a symbol whose label is born empty is a text
201
+ * element nobody can find to click (PO, 24/09/2026 — the Wardley precedent
202
+ * seeds "Component" for the same reason). The external kinds are seeded with
203
+ * the spec's own name for the artefact, the wording their role already carries.
140
204
  *
141
205
  * The caption is asked of the host's catalogue under {@link nodeLabelKey} when
142
206
  * the artefact is placed, with the entry below as the English default.
143
207
  */
144
208
  export const NODE_LABEL = {
145
- startEvent: '',
146
- startEventMessage: '',
147
- startEventTimer: '',
148
- endEvent: '',
149
- endEventMessage: '',
150
- endEventTerminate: '',
209
+ startEvent: 'Start event',
210
+ startEventMessage: 'Message start event',
211
+ startEventTimer: 'Timer start event',
212
+ endEvent: 'End event',
213
+ endEventMessage: 'Message end event',
214
+ endEventTerminate: 'Terminate end event',
151
215
  task: 'Task',
152
216
  taskUser: 'User task',
153
217
  taskService: 'Service task',
154
218
  subProcess: 'Sub-process',
155
219
  callActivity: 'Call activity',
156
- gatewayExclusive: '',
157
- gatewayParallel: '',
158
- dataObject: '',
159
- dataStore: '',
220
+ gatewayExclusive: 'Exclusive gateway',
221
+ gatewayParallel: 'Parallel gateway',
222
+ dataObject: 'Data object',
223
+ dataStore: 'Data store',
160
224
  // The one artefact that IS its text.
161
225
  textAnnotation: 'Annotation',
162
226
  // The group's label is a CategoryValue in the spec. A plain editable string
@@ -170,8 +234,8 @@ export const NODE_LABEL = {
170
234
  * Resolved AT PLACEMENT (`createBpmnNode`) and never afterwards: what a gesture
171
235
  * writes into the document is content the author owns from that moment on, and
172
236
  * a renderer that re-translated it on every paint would silently overwrite a
173
- * name somebody typed. The kinds with an empty label ask for nothing — there is
174
- * no word to translate, and BPMN puts an event's name outside its glyph anyway.
237
+ * name somebody typed. Every kind has a seed now; for an external kind it is
238
+ * written into the grouped `bpmn:label` rather than the shape (R38).
175
239
  */
176
240
  export const nodeLabelKey = (kind) => `com.labre.bpmn.seed.${kind}`;
177
241
  /**
@@ -13,6 +13,6 @@ import type { BpmnPoolElementModel } from '@formicoidea/labre-core/model';
13
13
  * drives it directly with a canvas stub.
14
14
  */
15
15
  export declare const bpmnPool: ElementRenderer<BpmnPoolElementModel>;
16
- export declare const BpmnPoolRendererExtension: import("@formicoidea/labre-core/store").ExtensionType & {
16
+ export declare const BpmnPoolRendererExtension: import("@formicoidea/labre-core/_pkgs/store").ExtensionType & {
17
17
  identifier: import("@formicoidea/labre-core/_pkgs/global/di").ServiceIdentifier<ElementRenderer<BpmnPoolElementModel>>;
18
18
  };
@@ -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
+ * A node's gravitating name, keyed by the node's surface id: the text of the
222
+ * `bpmn:label` that shares its native group (R38, `bpmnLabelMode`).
223
+ *
224
+ * Resolved by whoever picked the board (`bpmnBoardFrom`), because group
225
+ * membership is a fact about the surface and this writer sees none of it.
226
+ * GROUP membership is the binding and never geometry: a label dragged away
227
+ * from its event is still its name, and a text that happens to sit under an
228
+ * event is not. A node with no entry falls back to its own `text` — every
229
+ * inscribed kind, and an event drawn before its name gravitated.
230
+ */
231
+ labels?: ReadonlyMap<string, string>;
220
232
  /**
221
233
  * How many things drawn INSIDE a pool the picking left behind because they
222
234
  * carry no BPMN role — a free shape, a text, an artefact drawn before roles
package/dist/export.js CHANGED
@@ -1128,7 +1128,9 @@ export function exportBpmnXmlWithWarnings(board, options = {}) {
1128
1128
  : hasCollaboration && mapping.slot === 'artifact'
1129
1129
  ? COLLABORATION
1130
1130
  : orphanProcessIndex();
1131
- const name = labelOf(model.text);
1131
+ // The grouped label first, then the shape's own text: a legacy event kept
1132
+ // its inner text and must still export the same name.
1133
+ const name = labelOf(board.labels?.get(model.id)) || labelOf(model.text);
1132
1134
  const node = {
1133
1135
  model,
1134
1136
  mapping,
package/dist/import.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { type InterchangeImportContext, type InterchangeImportResult } from '@formicoidea/labre-core/blocks/surface';
2
2
  import type { BpmnNodeKind } from '@formicoidea/labre-core/model';
3
- import { BPMN_SCOPE } from './export.js';
4
3
  /**
5
4
  * The remarks whose wording is FIXED, as `[key, English]` pairs — one entry
6
5
  * per SHAPE of sentence, never one per exact string: a remark that names
@@ -62,6 +61,16 @@ export declare const BPMN_IMPORT_ERRORS: {
62
61
  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
62
  readonly notDefinitions: readonly ["com.labre.bpmn.import.error.not-definitions", "A BPMN file opens on <definitions>; this one opens on <{{tag}}>."];
64
63
  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."];
64
+ /**
65
+ * Its OWN entry rather than `wrongNamespace` with the words "no namespace"
66
+ * poured into `{{namespace}}` (#390). That fragment was English prose
67
+ * substituted into a translated sentence — the one shape a `messageParams`
68
+ * hole cannot carry, because the reader is pure and has no `std` to resolve
69
+ * it with. It is also a different FACT, which is what this table is a list
70
+ * of: a `<definitions>` in the wrong namespace names one, a `<definitions>`
71
+ * in none names nothing at all.
72
+ */
73
+ 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
74
  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
75
  };
67
76
  /**
@@ -81,16 +90,6 @@ export declare const BPMN_IMPORT_ERRORS: {
81
90
  * instead of this line assuming it.
82
91
  */
83
92
  export declare const BPMN_KIND_OF_XML: ReadonlyMap<string, BpmnNodeKind>;
84
- /**
85
- * `.bpmn`'s scope vocabulary — where a carried fragment came off (D2, as
86
- * amended in #157), and where the writer puts it back.
87
- *
88
- * Declared in `export.ts` and re-exported here, for the reason
89
- * {@link BPMN_FORMAT_ID} is: the reader files a fragment under a scope and the
90
- * writer looks it up under one, and a table written twice is a table that
91
- * drifts. See its doc comment there for what a scope means.
92
- */
93
- export { BPMN_SCOPE };
94
93
  /** The four reasons a fragment is kept and not written back (D5). Closed. */
95
94
  export declare const BPMN_QUARANTINE_REASON: {
96
95
  readonly colour: string;