@formicoidea/labre-framework-bpmn 0.58.0 → 0.59.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
@@ -19,7 +19,9 @@ export declare function createBpmnPool(std: BlockStdScope): void;
19
19
  /**
20
20
  * Arm the native connector tool, pre-styled for a BPMN sequence flow:
21
21
  * orthogonal, solid, with a filled triangle head. The user then draws from
22
- * one node to another (endpoints attach to centers).
22
+ * one node to another, attaching at the anchor of each outline they pick (a
23
+ * gateway's corners, an activity's side midpoints, an event's cardinal points;
24
+ * `BPMN_CENTRE_ANCHORED`).
23
25
  */
24
26
  export declare function activateBpmnSequenceFlow(std: BlockStdScope): void;
25
27
  /**
@@ -102,12 +104,14 @@ export declare function bpmnPoolsForLaneEdit(std: BlockStdScope): BpmnPoolElemen
102
104
  */
103
105
  export declare function addBpmnLane(std: BlockStdScope): void;
104
106
  /**
105
- * Remove the LAST lane of every selected pool.
107
+ * Remove the FOCUSED lane of every selected pool, or its LAST lane when none is
108
+ * focused.
106
109
  *
107
- * The last one and not the selected one, because a lane is not selectable: it
108
- * is a slice of the pool's plot, painted by the background primitive, with no
109
- * element of its own. Removing from the end is the gesture that undoes the one
110
- * that added it.
110
+ * Focused and not selected, because a lane is not selectable: it is a slice of
111
+ * the pool's plot, painted by the background primitive, with no element of its
112
+ * own. A single click on its title strip, in a selected pool, focuses it
113
+ * ({@link bpmnFocusedLaneIndex}). With nothing focused, removing from the end
114
+ * is the gesture that undoes the one that added it.
111
115
  *
112
116
  * ## Nothing moves
113
117
  *
@@ -221,3 +225,69 @@ export declare function importBpmnSvgFile(std: BlockStdScope): Promise<void>;
221
225
  * same thing that only the bytes can tell apart.
222
226
  */
223
227
  export declare function renameBpmnLane(std: BlockStdScope, model: BpmnPoolElementModel, index: number, name: string): void;
228
+ /**
229
+ * Move the lane at `from` to `to` inside `pool`, carrying what is drawn in it.
230
+ *
231
+ * A lane is a slice of the pool's plot, and what is "in" it is decided by
232
+ * geometry: the centre of an artefact, read by {@link bpmnLaneOf}. Reordering
233
+ * the `lanes` array alone would leave every task where it was on the canvas and
234
+ * silently hand it to whichever lane now covers that spot — the user moves the
235
+ * Sales lane up and finds their sales tasks filed under Ops. So the gesture
236
+ * moves both: the array is reordered, and every unlocked artefact wholly inside
237
+ * the pool is translated by the vertical shift of the lane its centre was in.
238
+ * Connectors attached to them follow through the connector watcher, as on any
239
+ * other move.
240
+ *
241
+ * A GESTURE-time write, not a cascade: it runs once, on the author's client,
242
+ * from a command, and a peer receives the reorder and the moves as ordinary
243
+ * updates. No stored-format change either: the same `lanes` array, the same
244
+ * `xywh` props.
245
+ *
246
+ * One `captureSync`, then one `transact`, so the reorder and every move are ONE
247
+ * undo step. Refused — and nothing written, so no empty undo entry — on a
248
+ * read-only document, a locked pool, an index that names no lane, or a move to
249
+ * where the lane already is. Returns whether it wrote.
250
+ *
251
+ * ponytail: a connector attached to nothing, drawn inside a lane, stays where
252
+ * it is, and a locked artefact is left behind; the upgrade is to translate the
253
+ * free endpoints too, here and nowhere else.
254
+ */
255
+ export declare function moveBpmnLane(std: BlockStdScope, pool: BpmnPoolElementModel, from: number, to: number): boolean;
256
+ /**
257
+ * Something that knows which lane of a pool is FOCUSED — the pool's view
258
+ * (`BpmnPoolView`) in an editor, a plain object in a test.
259
+ *
260
+ * A lane is not an element, so it cannot be selected; the lane commands need
261
+ * another way to know which lane the user means. The focus is set by a single
262
+ * click on a lane's title strip in a selected pool, and it is on-screen state
263
+ * and nothing more: held by the view, never stored, never synced, dropped when
264
+ * the pool is deselected. Another viewer of the same document has their own or
265
+ * none. An id rather than an index, so the focus follows the lane it names
266
+ * through a move.
267
+ */
268
+ export interface BpmnLaneFocusHolder {
269
+ readonly focusedLaneId: string | null;
270
+ }
271
+ /**
272
+ * The index of `pool`'s focused lane in this editor, or `null` when none is
273
+ * focused or the focused one is gone (removed by a peer, undone away).
274
+ *
275
+ * Read through {@link BpmnLaneFocusHolder} rather than the view class, so the
276
+ * commands do not import the view.
277
+ */
278
+ export declare function bpmnFocusedLaneIndex(std: BlockStdScope, pool: BpmnPoolElementModel): number | null;
279
+ /**
280
+ * The selected pool whose focused lane can move one step `by` (`-1` up, `1`
281
+ * down), and the lane's index — `null` when no such pool is selected.
282
+ *
283
+ * The first one that qualifies: a lane is focused by clicking it, on one pool
284
+ * at a time, so a selection holding two pools still names at most one lane.
285
+ */
286
+ export declare function bpmnLaneToMove(std: BlockStdScope, by: -1 | 1): {
287
+ pool: BpmnPoolElementModel;
288
+ index: number;
289
+ } | null;
290
+ /** Move the focused lane of the selected pool one step up. */
291
+ export declare function moveBpmnLaneUp(std: BlockStdScope): void;
292
+ /** Move the focused lane of the selected pool one step down. */
293
+ export declare function moveBpmnLaneDown(std: BlockStdScope): void;
package/dist/actions.js CHANGED
@@ -1,13 +1,15 @@
1
- import { DefaultTool, EdgelessCRUDIdentifier, generateElementId, materializeInterchangeImport, reportInterchangeImport, runInterchangeImportFile, } from '@formicoidea/labre-core/blocks/surface';
1
+ import { backgroundInstanceZones, backgroundPlot, DefaultTool, EdgelessCRUDIdentifier, generateElementId, materializeInterchangeImport, reportInterchangeImport, runInterchangeImportFile, } from '@formicoidea/labre-core/blocks/surface';
2
2
  import { ConnectorTool } from '@formicoidea/labre-core/gfx/connector';
3
3
  import { createGroupCommand } from '@formicoidea/labre-core/gfx/group';
4
- import { BpmnPoolElementModel, ConnectorMode, } from '@formicoidea/labre-core/model';
4
+ import { BpmnPoolElementModel, ConnectorElementModel, ConnectorMode, } from '@formicoidea/labre-core/model';
5
5
  import { NotificationProvider, translateKey, } from '@formicoidea/labre-core/shared/services';
6
6
  import { downloadBlob } from '@formicoidea/labre-core/shared/utils';
7
7
  import { Bound } from '@formicoidea/labre-core/global/gfx';
8
- import { GfxControllerIdentifier } from '@formicoidea/labre-core/std/gfx';
8
+ import { GfxControllerIdentifier, GfxGroupLikeElementModel, } from '@formicoidea/labre-core/std/gfx';
9
+ import { BPMN_POOL_BACKGROUND } from './background.js';
9
10
  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';
10
11
  import { BPMN_FORMAT_ID, exportBpmnXmlWithWarnings, } from './export.js';
12
+ import { bpmnLaneOf } from './facts.js';
11
13
  import { BPMN_SVG_IMPORT, BPMN_XML_EXPORT, BPMN_XML_FORMAT, BPMN_XML_IMPORT, bpmnBoardFrom, bpmnSafeFilename, } from './interchange.js';
12
14
  import { bpmnLabelBoxFor, bpmnLabelProps, bpmnNodeProps, bpmnPoolProps, } from './presets.js';
13
15
  import { BPMN_ROLE } from './roles.js';
@@ -105,7 +107,9 @@ export function createBpmnPool(std) {
105
107
  /**
106
108
  * Arm the native connector tool, pre-styled for a BPMN sequence flow:
107
109
  * orthogonal, solid, with a filled triangle head. The user then draws from
108
- * one node to another (endpoints attach to centers).
110
+ * one node to another, attaching at the anchor of each outline they pick (a
111
+ * gateway's corners, an activity's side midpoints, an event's cardinal points;
112
+ * `BPMN_CENTRE_ANCHORED`).
109
113
  */
110
114
  export function activateBpmnSequenceFlow(std) {
111
115
  gfxOf(std).tool.setTool(ConnectorTool, {
@@ -280,12 +284,14 @@ export function addBpmnLane(std) {
280
284
  }
281
285
  }
282
286
  /**
283
- * Remove the LAST lane of every selected pool.
287
+ * Remove the FOCUSED lane of every selected pool, or its LAST lane when none is
288
+ * focused.
284
289
  *
285
- * The last one and not the selected one, because a lane is not selectable: it
286
- * is a slice of the pool's plot, painted by the background primitive, with no
287
- * element of its own. Removing from the end is the gesture that undoes the one
288
- * that added it.
290
+ * Focused and not selected, because a lane is not selectable: it is a slice of
291
+ * the pool's plot, painted by the background primitive, with no element of its
292
+ * own. A single click on its title strip, in a selected pool, focuses it
293
+ * ({@link bpmnFocusedLaneIndex}). With nothing focused, removing from the end
294
+ * is the gesture that undoes the one that added it.
289
295
  *
290
296
  * ## Nothing moves
291
297
  *
@@ -304,7 +310,13 @@ export function removeBpmnLane(std) {
304
310
  return;
305
311
  std.store.captureSync();
306
312
  for (const model of withLanes) {
307
- writeLanes(std, model, bpmnLanesOf(model).slice(0, -1));
313
+ const lanes = bpmnLanesOf(model);
314
+ // The FOCUSED lane when the user picked one (a click on its title strip),
315
+ // the last one otherwise — which is still the one the previous "Add lane"
316
+ // added.
317
+ const focused = bpmnFocusedLaneIndex(std, model);
318
+ const gone = focused ?? lanes.length - 1;
319
+ writeLanes(std, model, lanes.filter((_, i) => i !== gone));
308
320
  }
309
321
  }
310
322
  /* ── Export (BPMN 2.0 XML) ────────────────────────────────────────────── */
@@ -510,3 +522,126 @@ export function renameBpmnLane(std, model, index, name) {
510
522
  std.store.captureSync();
511
523
  writeLanes(std, model, next);
512
524
  }
525
+ /**
526
+ * Move the lane at `from` to `to` inside `pool`, carrying what is drawn in it.
527
+ *
528
+ * A lane is a slice of the pool's plot, and what is "in" it is decided by
529
+ * geometry: the centre of an artefact, read by {@link bpmnLaneOf}. Reordering
530
+ * the `lanes` array alone would leave every task where it was on the canvas and
531
+ * silently hand it to whichever lane now covers that spot — the user moves the
532
+ * Sales lane up and finds their sales tasks filed under Ops. So the gesture
533
+ * moves both: the array is reordered, and every unlocked artefact wholly inside
534
+ * the pool is translated by the vertical shift of the lane its centre was in.
535
+ * Connectors attached to them follow through the connector watcher, as on any
536
+ * other move.
537
+ *
538
+ * A GESTURE-time write, not a cascade: it runs once, on the author's client,
539
+ * from a command, and a peer receives the reorder and the moves as ordinary
540
+ * updates. No stored-format change either: the same `lanes` array, the same
541
+ * `xywh` props.
542
+ *
543
+ * One `captureSync`, then one `transact`, so the reorder and every move are ONE
544
+ * undo step. Refused — and nothing written, so no empty undo entry — on a
545
+ * read-only document, a locked pool, an index that names no lane, or a move to
546
+ * where the lane already is. Returns whether it wrote.
547
+ *
548
+ * ponytail: a connector attached to nothing, drawn inside a lane, stays where
549
+ * it is, and a locked artefact is left behind; the upgrade is to translate the
550
+ * free endpoints too, here and nowhere else.
551
+ */
552
+ export function moveBpmnLane(std, pool, from, to) {
553
+ if (std.store.readonly || pool.isLocked())
554
+ return false;
555
+ if (from === to)
556
+ return false;
557
+ const lanes = bpmnLanesOf(pool);
558
+ if (!lanes[from] || !lanes[to])
559
+ return false;
560
+ const next = [...lanes];
561
+ const [moved] = next.splice(from, 1);
562
+ next.splice(to, 0, moved);
563
+ // Each band's top, before and after, read off the painted partition: the
564
+ // normalisation the renderer and the audit use, so an artefact moves by
565
+ // exactly the distance its lane moves on the canvas.
566
+ const frame = pool.elementBound;
567
+ const plot = backgroundPlot(BPMN_POOL_BACKGROUND, frame.w, frame.h);
568
+ const topsOf = (rows) => new Map(backgroundInstanceZones(BPMN_POOL_BACKGROUND, { lanes: rows }).map(zone => [zone.id, zone.rect.y * plot.height]));
569
+ const before = topsOf(lanes);
570
+ const after = topsOf(next);
571
+ const prefix = BPMN_POOL_BACKGROUND.instanceZones?.idPrefix ?? 'lane';
572
+ // Membership BEFORE the write: once the array is reordered the bands are
573
+ // where the lanes are going, and a centre would read the wrong lane.
574
+ const moves = [];
575
+ for (const element of gfxOf(std).grid.search(frame, { strict: true })) {
576
+ if (element === pool || element.isLocked())
577
+ continue;
578
+ // A connector follows its endpoints; a group or a mind map derives its
579
+ // bound from its children, which are moved on their own.
580
+ if (element instanceof ConnectorElementModel)
581
+ continue;
582
+ if (element instanceof GfxGroupLikeElementModel)
583
+ continue;
584
+ const bound = Bound.deserialize(element.xywh);
585
+ const lane = bpmnLaneOf(pool, bound);
586
+ if (!lane)
587
+ continue;
588
+ const key = `${prefix}:${lane.id}`;
589
+ const dy = (after.get(key) ?? 0) - (before.get(key) ?? 0);
590
+ if (dy === 0)
591
+ continue;
592
+ moves.push({ id: element.id, xywh: bound.moveDelta(0, dy).serialize() });
593
+ }
594
+ std.store.captureSync();
595
+ std.store.transact(() => {
596
+ writeLanes(std, pool, next);
597
+ const crud = std.get(EdgelessCRUDIdentifier);
598
+ for (const { id, xywh } of moves)
599
+ crud.updateElement(id, { xywh });
600
+ });
601
+ return true;
602
+ }
603
+ /**
604
+ * The index of `pool`'s focused lane in this editor, or `null` when none is
605
+ * focused or the focused one is gone (removed by a peer, undone away).
606
+ *
607
+ * Read through {@link BpmnLaneFocusHolder} rather than the view class, so the
608
+ * commands do not import the view.
609
+ */
610
+ export function bpmnFocusedLaneIndex(std, pool) {
611
+ const holder = gfxOf(std).view?.get(pool.id);
612
+ const id = holder?.focusedLaneId;
613
+ if (!id)
614
+ return null;
615
+ const index = bpmnLanesOf(pool).findIndex(lane => lane.id === id);
616
+ return index < 0 ? null : index;
617
+ }
618
+ /**
619
+ * The selected pool whose focused lane can move one step `by` (`-1` up, `1`
620
+ * down), and the lane's index — `null` when no such pool is selected.
621
+ *
622
+ * The first one that qualifies: a lane is focused by clicking it, on one pool
623
+ * at a time, so a selection holding two pools still names at most one lane.
624
+ */
625
+ export function bpmnLaneToMove(std, by) {
626
+ for (const pool of bpmnPoolsForLaneEdit(std)) {
627
+ const index = bpmnFocusedLaneIndex(std, pool);
628
+ if (index === null)
629
+ continue;
630
+ if (!bpmnLanesOf(pool)[index + by])
631
+ continue;
632
+ return { pool, index };
633
+ }
634
+ return null;
635
+ }
636
+ /** Move the focused lane of the selected pool one step up. */
637
+ export function moveBpmnLaneUp(std) {
638
+ const target = bpmnLaneToMove(std, -1);
639
+ if (target)
640
+ moveBpmnLane(std, target.pool, target.index, target.index - 1);
641
+ }
642
+ /** Move the focused lane of the selected pool one step down. */
643
+ export function moveBpmnLaneDown(std) {
644
+ const target = bpmnLaneToMove(std, 1);
645
+ if (target)
646
+ moveBpmnLane(std, target.pool, target.index, target.index + 1);
647
+ }
@@ -26,7 +26,15 @@ import { BPMN_ROLE } from './roles.js';
26
26
  *
27
27
  * The left margin IS the name band: `margin.left` is both the room the flow
28
28
  * area gives up and the width of the strip the participant name is written in
29
- * (see `BackgroundSideBandDef`).
29
+ * (see `BackgroundSideBandDef`) — its MINIMUM width. The name WRAPS up the band
30
+ * (`fit: 'wrap'`), on words, into as many as three lines side by side, and the
31
+ * band thickens to hold them: one line fits the declared 28 units, every
32
+ * further line adds one line height. The PO preferred this to the ellipsis cut
33
+ * PR #532 shipped (2026-10-27): a band that grows with its name reads as a
34
+ * title, a name cut short reads as a bug. A name past three lines is still cut
35
+ * with "…" on the last one, so a pasted paragraph does not eat the pool. The
36
+ * thickness is derived at paint time (`backgroundBandWidths`) and never
37
+ * stored, and the stored name stays whole.
30
38
  *
31
39
  * ## The lanes
32
40
  *
@@ -43,6 +51,10 @@ import { BPMN_ROLE } from './roles.js';
43
51
  *
44
52
  * Each lane wears its own title band inside the pool's — a narrower strip, no
45
53
  * fill, one divider, the name turned on its side, exactly as BPMN 2.0 draws it.
54
+ * A lane name wraps the same way, up to two lines, and every lane's strip takes
55
+ * the thickness the most demanding name needs (PO, 2026-10-27): strips of
56
+ * different widths would stagger the lanes' leading edges, and lanes of one
57
+ * pool are siblings, so they share one edge.
46
58
  * The strip is CHROME inside the lane and not a smaller lane: a task dropped on
47
59
  * a lane's title band is in that lane, because in BPMN the band belongs to it.
48
60
  */
@@ -99,6 +111,13 @@ export const BPMN_POOL_BACKGROUND = {
99
111
  // pool is the same stroke, so the lanes read as the frame continued.
100
112
  divider: { color: '@frame', width: POOL_FRAME_WIDTH },
101
113
  },
114
+ // Wrapped across the strip, which grows to hold the lines — every lane's
115
+ // strip to the same thickness. Two lines at most: a lane title is a
116
+ // label, and past two a third line would cost the flow area more than
117
+ // the name gains; past that it is cut with "…". The stored name stays
118
+ // whole, and the rename editor shows it in full.
119
+ fit: 'wrap',
120
+ maxLines: 2,
102
121
  style: {
103
122
  size: POOL_LANE_NAME_FONT_SIZE,
104
123
  color: '@name',
@@ -151,6 +170,12 @@ export const BPMN_POOL_BACKGROUND = {
151
170
  },
152
171
  // Read bottom-to-top, as the spec draws a vertical pool name.
153
172
  vertical: true,
173
+ // Wrapped up the band, which grows to hold the lines (three at
174
+ // most, the last cut with "…" if the name is longer still). The
175
+ // anchor stays half the DECLARED width in from the plot edge; the
176
+ // primitive keeps it in the middle of the grown band.
177
+ fit: 'wrap',
178
+ maxLines: 3,
154
179
  },
155
180
  },
156
181
  ],
@@ -199,6 +199,22 @@ export const bpmnCommandsManifest = [
199
199
  scope: 'edgeless',
200
200
  defaultKeys: { mac: [], other: [] },
201
201
  },
202
+ {
203
+ id: 'bpmn.moveLaneUp',
204
+ owner: 'bpmn',
205
+ labelKey: 'com.labre.commands.bpmn.moveLaneUp',
206
+ labelFallback: 'Move lane up',
207
+ scope: 'edgeless',
208
+ defaultKeys: { mac: [], other: [] },
209
+ },
210
+ {
211
+ id: 'bpmn.moveLaneDown',
212
+ owner: 'bpmn',
213
+ labelKey: 'com.labre.commands.bpmn.moveLaneDown',
214
+ labelFallback: 'Move lane down',
215
+ scope: 'edgeless',
216
+ defaultKeys: { mac: [], other: [] },
217
+ },
202
218
  {
203
219
  id: 'bpmn.exportXml',
204
220
  owner: 'bpmn',
package/dist/commands.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { StrokeStyle } from '@formicoidea/labre-core/model';
2
- import { activateBpmnAssociation, activateBpmnMessageFlow, activateBpmnSequenceFlow, addBpmnLane, bpmnLanesOf, bpmnPoolsForLaneEdit, bpmnPoolsSelected, createBpmnNode, createBpmnPool, exportBpmnXmlFile, importBpmnSvgFile, importBpmnXmlFile, removeBpmnLane, } from './actions.js';
2
+ import { activateBpmnAssociation, activateBpmnMessageFlow, activateBpmnSequenceFlow, addBpmnLane, bpmnLanesOf, bpmnPoolsForLaneEdit, bpmnPoolsSelected, createBpmnNode, createBpmnPool, exportBpmnXmlFile, importBpmnSvgFile, importBpmnXmlFile, bpmnLaneToMove, moveBpmnLaneDown, moveBpmnLaneUp, removeBpmnLane, } from './actions.js';
3
3
  import { BPMN_EDGE_STYLE, NODE_FILL, NODE_SIZE, } from './consts.js';
4
4
  import { bpmnNodeProps } from './presets.js';
5
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';
6
+ import { bpmnAssociationIcon, bpmnCallActivityIcon, bpmnDataObjectIcon, bpmnDataStoreIcon, bpmnEndIcon, bpmnEndMessageIcon, bpmnEndTerminateIcon, bpmnExportXmlIcon, bpmnGatewayIcon, bpmnGatewayParallelIcon, bpmnGroupIcon, bpmnImportSvgIcon, bpmnImportXmlIcon, bpmnLaneAddIcon, bpmnLaneDownIcon, bpmnLaneRemoveIcon, bpmnLaneUpIcon, bpmnMessageIcon, bpmnPoolIcon, bpmnSequenceIcon, bpmnStartIcon, bpmnStartMessageIcon, bpmnStartTimerIcon, bpmnSubProcessIcon, bpmnTaskIcon, bpmnTaskServiceIcon, bpmnTaskUserIcon, bpmnTextAnnotationIcon, bpmnToolbarIcon, } from './toolbar/icons.js';
7
7
  /**
8
8
  * What a spec DOES — its own gesture, or the one its {@link Spec.node} implies.
9
9
  *
@@ -493,7 +493,7 @@ const laneCommands = [
493
493
  labelKey: 'com.labre.commands.bpmn.removeLane',
494
494
  labelFallback: 'Remove lane',
495
495
  descriptionKey: 'com.labre.commands.bpmn.removeLane.description',
496
- descriptionFallback: 'Remove the last lane of the selected pool. Nothing drawn in it moves.',
496
+ descriptionFallback: 'Remove the focused lane of the selected pool, or its last lane. Nothing drawn in it moves.',
497
497
  iconKey: 'bpmn.lane-remove',
498
498
  order: SPECS.length + 1,
499
499
  run: removeBpmnLane,
@@ -501,6 +501,36 @@ const laneCommands = [
501
501
  // …and additionally: a pool with no lane has none to remove.
502
502
  when: std => bpmnPoolsForLaneEdit(std).some(model => bpmnLanesOf(model).length > 0),
503
503
  },
504
+ // The two REORDER commands act on the FOCUSED lane — the one a single click
505
+ // on its title strip picked in the selected pool (`bpmnFocusedLaneIndex`).
506
+ // A lane is not an element and cannot be selected, so without a focus there
507
+ // is no lane to move and the commands are not offered at all.
508
+ {
509
+ ...LANE_COMMON,
510
+ id: 'bpmn.moveLaneUp',
511
+ labelKey: 'com.labre.commands.bpmn.moveLaneUp',
512
+ labelFallback: 'Move lane up',
513
+ descriptionKey: 'com.labre.commands.bpmn.moveLaneUp.description',
514
+ descriptionFallback: 'Move the focused lane above the one before it, with everything drawn in it.',
515
+ iconKey: 'bpmn.lane-up',
516
+ order: SPECS.length + 5,
517
+ run: moveBpmnLaneUp,
518
+ telemetry: { framework: 'bpmn', element: 'pool:lane-move-up' },
519
+ when: std => bpmnLaneToMove(std, -1) !== null,
520
+ },
521
+ {
522
+ ...LANE_COMMON,
523
+ id: 'bpmn.moveLaneDown',
524
+ labelKey: 'com.labre.commands.bpmn.moveLaneDown',
525
+ labelFallback: 'Move lane down',
526
+ descriptionKey: 'com.labre.commands.bpmn.moveLaneDown.description',
527
+ descriptionFallback: 'Move the focused lane below the one after it, with everything drawn in it.',
528
+ iconKey: 'bpmn.lane-down',
529
+ order: SPECS.length + 6,
530
+ run: moveBpmnLaneDown,
531
+ telemetry: { framework: 'bpmn', element: 'pool:lane-move-down' },
532
+ when: std => bpmnLaneToMove(std, 1) !== null,
533
+ },
504
534
  ];
505
535
  /**
506
536
  * The EXPORT — the first BPMN command whose subject is the whole board.
@@ -721,6 +751,8 @@ export const bpmnCommandIcons = {
721
751
  'bpmn.pool': bpmnPoolIcon,
722
752
  'bpmn.lane-add': bpmnLaneAddIcon,
723
753
  'bpmn.lane-remove': bpmnLaneRemoveIcon,
754
+ 'bpmn.lane-up': bpmnLaneUpIcon,
755
+ 'bpmn.lane-down': bpmnLaneDownIcon,
724
756
  'bpmn.export-xml': bpmnExportXmlIcon,
725
757
  'bpmn.import-xml': bpmnImportXmlIcon,
726
758
  'bpmn.import-svg': bpmnImportSvgIcon,
@@ -1,18 +1,26 @@
1
1
  import { type ElementRenderer } from '@formicoidea/labre-core/blocks/surface';
2
2
  import type { BpmnPoolElementModel } from '@formicoidea/labre-core/model';
3
3
  /**
4
- * Canvas renderer for the BPMN pool.
4
+ * Canvas renderer for the BPMN pool: the declared background, then the outline
5
+ * of the FOCUSED lane, if this editor has one (`BpmnLaneFocusHolder`, held by
6
+ * the pool's view and never stored).
5
7
  *
6
- * There is no BPMN drawing code any more: the pool is an INSTANTIATION of the
7
- * framework-background primitive, configured by the `BPMN_POOL_BACKGROUND`
8
- * declaration. What used to be ninety lines of hand-traced rounded rectangle,
9
- * filled band and rotated name is now a declaration any other framework can
10
- * write for itself.
8
+ * The outline is the editor talking about the drawing — "this is the lane the
9
+ * reorder commands will move" — not part of it, so it is the theme's accent
10
+ * (`--affine-primary-color`, resolved through the renderer) rather than a
11
+ * notation colour. It is painted only onto the editor's OWN canvases: the SVG
12
+ * and PNG exports paint through this same renderer into a context of their
13
+ * own, and a file must not carry an on-screen focus. A renderer with no editor
14
+ * behind it (a bare unit test) paints none either, and a host that defines no
15
+ * accent gets none.
16
+ *
17
+ * Painted in the transform the primitive left on the context — element-local
18
+ * units, rotated about the centre like the rest of the pool.
11
19
  *
12
20
  * Exported as a function as well as an extension because the fidelity suite
13
21
  * drives it directly with a canvas stub.
14
22
  */
15
23
  export declare const bpmnPool: ElementRenderer<BpmnPoolElementModel>;
16
- export declare const BpmnPoolRendererExtension: import("@formicoidea/labre-core/_pkgs/store").ExtensionType & {
17
- identifier: import("@formicoidea/labre-core/_pkgs/global/di").ServiceIdentifier<ElementRenderer<BpmnPoolElementModel>>;
24
+ export declare const BpmnPoolRendererExtension: import("@formicoidea/labre-core/_pkgs/store/index.js").ExtensionType & {
25
+ identifier: import("@formicoidea/labre-core/_pkgs/global/di/types.js").ServiceIdentifier<ElementRenderer<BpmnPoolElementModel>>;
18
26
  };
@@ -1,16 +1,80 @@
1
- import { createFrameworkBackgroundRenderer, ElementRendererExtension, } from '@formicoidea/labre-core/blocks/surface';
1
+ import { backgroundInstanceZones, backgroundPlot, createFrameworkBackgroundRenderer, ElementRendererExtension, } from '@formicoidea/labre-core/blocks/surface';
2
+ import { GfxControllerIdentifier } from '@formicoidea/labre-core/std/gfx';
2
3
  import { BPMN_POOL_BACKGROUND } from './background.js';
4
+ import { bpmnPoolLayout, cachePaintedBands } from './pool-hit.js';
3
5
  /**
4
- * Canvas renderer for the BPMN pool.
6
+ * The pool as the framework-background primitive paints it.
5
7
  *
6
8
  * There is no BPMN drawing code any more: the pool is an INSTANTIATION of the
7
9
  * framework-background primitive, configured by the `BPMN_POOL_BACKGROUND`
8
10
  * declaration. What used to be ninety lines of hand-traced rounded rectangle,
9
11
  * filled band and rotated name is now a declaration any other framework can
10
12
  * write for itself.
13
+ */
14
+ const paintPool = createFrameworkBackgroundRenderer(BPMN_POOL_BACKGROUND, {
15
+ // The model cannot measure text, so picking would select by the DECLARED
16
+ // band widths while a long name paints them wider. Cache what was painted
17
+ // on the model's `@local()` `paintedBands` (never stored), and only when
18
+ // it changed: a local write is an `elementUpdated`, which repaints. The
19
+ // view writes the same cache before any paint (`element-view.ts`).
20
+ onBands: (model, layout) => {
21
+ cachePaintedBands(model, layout);
22
+ },
23
+ });
24
+ /** Width of the focused lane's outline, in model units. */
25
+ const FOCUS_OUTLINE_WIDTH = 2;
26
+ /**
27
+ * Canvas renderer for the BPMN pool: the declared background, then the outline
28
+ * of the FOCUSED lane, if this editor has one (`BpmnLaneFocusHolder`, held by
29
+ * the pool's view and never stored).
30
+ *
31
+ * The outline is the editor talking about the drawing — "this is the lane the
32
+ * reorder commands will move" — not part of it, so it is the theme's accent
33
+ * (`--affine-primary-color`, resolved through the renderer) rather than a
34
+ * notation colour. It is painted only onto the editor's OWN canvases: the SVG
35
+ * and PNG exports paint through this same renderer into a context of their
36
+ * own, and a file must not carry an on-screen focus. A renderer with no editor
37
+ * behind it (a bare unit test) paints none either, and a host that defines no
38
+ * accent gets none.
39
+ *
40
+ * Painted in the transform the primitive left on the context — element-local
41
+ * units, rotated about the centre like the rest of the pool.
11
42
  *
12
43
  * Exported as a function as well as an extension because the fidelity suite
13
44
  * drives it directly with a canvas stub.
14
45
  */
15
- export const bpmnPool = createFrameworkBackgroundRenderer(BPMN_POOL_BACKGROUND);
46
+ export const bpmnPool = (model, ctx, matrix, renderer, rc, viewportBound) => {
47
+ paintPool(model, ctx, matrix, renderer, rc, viewportBound);
48
+ const canvas = renderer;
49
+ if (!canvas?.std)
50
+ return;
51
+ const onScreen = ctx.canvas === canvas.canvas ||
52
+ canvas.stackingCanvas.includes(ctx.canvas);
53
+ if (!onScreen)
54
+ return;
55
+ const holder = canvas.std
56
+ .getOptional(GfxControllerIdentifier)
57
+ ?.view?.get(model.id);
58
+ const id = holder?.focusedLaneId;
59
+ if (!id)
60
+ return;
61
+ const [, , w, h] = model.deserializedXYWH;
62
+ // The outline hugs the flow area as PAINTED, i.e. right of a participant
63
+ // band a long name may have widened.
64
+ const plot = backgroundPlot(BPMN_POOL_BACKGROUND, w, h, bpmnPoolLayout(model).margin);
65
+ const prefix = BPMN_POOL_BACKGROUND.instanceZones?.idPrefix ?? 'lane';
66
+ const zone = backgroundInstanceZones(BPMN_POOL_BACKGROUND, model).find(candidate => candidate.id === `${prefix}:${id}`);
67
+ if (!zone)
68
+ return;
69
+ const accent = canvas.getPropertyValue('--affine-primary-color').trim();
70
+ if (!accent)
71
+ return;
72
+ const inset = FOCUS_OUTLINE_WIDTH / 2;
73
+ ctx.save();
74
+ ctx.strokeStyle = accent;
75
+ ctx.lineWidth = FOCUS_OUTLINE_WIDTH;
76
+ ctx.setLineDash([]);
77
+ ctx.strokeRect(plot.x0 + inset, plot.y0 + zone.rect.y * plot.height + inset, plot.width - FOCUS_OUTLINE_WIDTH, zone.rect.h * plot.height - FOCUS_OUTLINE_WIDTH);
78
+ ctx.restore();
79
+ };
16
80
  export const BpmnPoolRendererExtension = ElementRendererExtension(BPMN_POOL_BACKGROUND.type, bpmnPool);
@@ -2,6 +2,7 @@ import type { BpmnPoolElementModel } from '@formicoidea/labre-core/model';
2
2
  import type { EditorHost } from '@formicoidea/labre-core/std';
3
3
  import type { PointTestOptions } from '@formicoidea/labre-core/std/gfx';
4
4
  import { GfxElementModelView } from '@formicoidea/labre-core/std/gfx';
5
+ import { type BpmnLaneFocusHolder } from './actions.js';
5
6
  /**
6
7
  * View for a BPMN pool. Three direct gestures live here:
7
8
  *
@@ -14,7 +15,11 @@ import { GfxElementModelView } from '@formicoidea/labre-core/std/gfx';
14
15
  * renderer paints the name in, rather than a second set of metrics that would
15
16
  * one day disagree with it;
16
17
  * - **drag on an internal lane boundary** moves the separator, taking from one
17
- * lane and giving to the other.
18
+ * lane and giving to the other;
19
+ * - **click in a lane's title band of a SELECTED pool** focuses that lane —
20
+ * the lane "Move lane up / down" and "Remove lane" act on. On-screen state
21
+ * only ({@link BpmnLaneFocusHolder}): never stored, outlined on the canvas
22
+ * by the renderer, dropped when the pool leaves the selection.
18
23
  *
19
24
  * Both renames are ZONED to their band (PO recette, 2026-08-26). The whole pool
20
25
  * used to open the participant editor, which was right while a pool had one
@@ -55,14 +60,18 @@ import { GfxElementModelView } from '@formicoidea/labre-core/std/gfx';
55
60
  * local point by `-model.rotate` about the element centre, at the one function
56
61
  * that has the element in hand (`_localPoint`), not a new declared field.
57
62
  */
58
- export declare class BpmnPoolView extends GfxElementModelView<BpmnPoolElementModel> {
63
+ export declare class BpmnPoolView extends GfxElementModelView<BpmnPoolElementModel> implements BpmnLaneFocusHolder {
59
64
  static type: string;
60
65
  private _nameEditor;
66
+ private _focusedLaneId;
67
+ /** The focused lane's id — see {@link BpmnLaneFocusHolder}. */
68
+ get focusedLaneId(): string | null;
61
69
  /** The boundary the pointer is over, and the handlers armed for it. */
62
70
  private _armed;
63
71
  /** Everything a separator drag needs, frozen at `dragstart`. */
64
72
  private _drag;
65
73
  onCreated(): void;
74
+ private _refreshPaintedBands;
66
75
  onDestroyed(): void;
67
76
  /** Hand the cursor back on the way out; nothing here owns it for long. */
68
77
  private _leave;
@@ -90,9 +99,14 @@ export declare class BpmnPoolView extends GfxElementModelView<BpmnPoolElementMod
90
99
  * - the internal LANE SEPARATORS, twelve-unit strips in the middle of the
91
100
  * flow area, which arm the drag that moves them;
92
101
  * - the zoom-GROWN rename targets, which reach past the painted bands when
93
- * the board is small on screen so a fingertip can still land on one.
102
+ * the board is small on screen so a fingertip can still land on one;
103
+ * - the part of a title band a long name has WIDENED past its declared
104
+ * width (the name wraps onto more lines and the band grows with it,
105
+ * `backgroundBandWidths`). The model selects by the declared widths, since
106
+ * it cannot measure text; the rename double-click must still land on every
107
+ * line painted, so `pool-hit.ts` aims at the band as painted.
94
108
  *
95
- * Neither is a selection zone — a click in the flow area still goes to
109
+ * None is a selection zone — a click in the flow area still goes to
96
110
  * whatever the user dropped there, because picking asks the MODEL. This
97
111
  * override only decides where the pool's own gestures are heard, which is why
98
112
  * it is declared here, beside the code that draws and handles them, rather
@@ -115,6 +129,18 @@ export declare class BpmnPoolView extends GfxElementModelView<BpmnPoolElementMod
115
129
  private _onDragStart;
116
130
  private _onDragMove;
117
131
  private _onDragEnd;
132
+ /**
133
+ * A press in a lane's title band of a SELECTED pool focuses that lane; a
134
+ * press in the participant band clears the focus. Anywhere else leaves it.
135
+ *
136
+ * On `pointerdown`, which this view already listens to, and NOT on `click`:
137
+ * a view that registers a handler reports the event as handled, and a
138
+ * registered `click` would take click-to-select away from the default tool.
139
+ * Selected first, for the reason the separator drag gives: a first click on
140
+ * a pool is "work on this pool", not "pick this lane".
141
+ */
142
+ private _focusAt;
143
+ private _setFocus;
118
144
  /**
119
145
  * A double-click renames whatever TITLE BAND it landed in, and nothing
120
146
  * otherwise (PO recette, 2026-08-26).
@@ -1,8 +1,15 @@
1
- import { EdgelessCRUDIdentifier } from '@formicoidea/labre-core/blocks/surface';
1
+ import { EdgelessCRUDIdentifier, } from '@formicoidea/labre-core/blocks/surface';
2
+ import { ToolbarFlag, ToolbarRegistryIdentifier, } from '@formicoidea/labre-core/shared/services';
2
3
  import { GfxElementModelView } from '@formicoidea/labre-core/std/gfx';
3
- import { bpmnLanesOf, renameBpmnLane } from './actions.js';
4
+ import { bpmnLanesOf, renameBpmnLane, } from './actions.js';
4
5
  import { POOL_LANE_MIN_HEIGHT } from './consts.js';
5
- import { bpmnLaneBoundaryAt, bpmnPoolBands, bpmnPoolTargetAt, } from './pool-hit.js';
6
+ import { bpmnLaneBoundaryAt, bpmnPoolBands, bpmnPoolMeasuredLayout, bpmnPoolTargetAt, cachePaintedBands, } from './pool-hit.js';
7
+ /**
8
+ * The props whose change moves a title band: the names it is sized by, and the
9
+ * box whose height a name wraps along. `paintedBands` itself is NOT here — the
10
+ * cache's own write must never re-enter the code that writes it.
11
+ */
12
+ const BAND_PROPS = new Set(['name', 'lanes', 'xywh']);
6
13
  /**
7
14
  * View for a BPMN pool. Three direct gestures live here:
8
15
  *
@@ -15,7 +22,11 @@ import { bpmnLaneBoundaryAt, bpmnPoolBands, bpmnPoolTargetAt, } from './pool-hit
15
22
  * renderer paints the name in, rather than a second set of metrics that would
16
23
  * one day disagree with it;
17
24
  * - **drag on an internal lane boundary** moves the separator, taking from one
18
- * lane and giving to the other.
25
+ * lane and giving to the other;
26
+ * - **click in a lane's title band of a SELECTED pool** focuses that lane —
27
+ * the lane "Move lane up / down" and "Remove lane" act on. On-screen state
28
+ * only ({@link BpmnLaneFocusHolder}): never stored, outlined on the canvas
29
+ * by the renderer, dropped when the pool leaves the selection.
19
30
  *
20
31
  * Both renames are ZONED to their band (PO recette, 2026-08-26). The whole pool
21
32
  * used to open the participant editor, which was right while a pool had one
@@ -60,18 +71,49 @@ export class BpmnPoolView extends GfxElementModelView {
60
71
  constructor() {
61
72
  super(...arguments);
62
73
  this._nameEditor = null;
74
+ this._focusedLaneId = null;
63
75
  /** The boundary the pointer is over, and the handlers armed for it. */
64
76
  this._armed = null;
65
77
  /** Everything a separator drag needs, frozen at `dragstart`. */
66
78
  this._drag = null;
67
79
  }
68
80
  static { this.type = 'bpmnPool'; }
81
+ /** The focused lane's id — see {@link BpmnLaneFocusHolder}. */
82
+ get focusedLaneId() {
83
+ return this._focusedLaneId;
84
+ }
69
85
  onCreated() {
70
86
  super.onCreated();
87
+ // Picking asks the MODEL, which selects by `paintedBands`. The renderer
88
+ // writes it after each paint, but a hidden tab never paints and the first
89
+ // click can come before the first frame: the view measures it now, and
90
+ // again whenever a band can have moved, so picking never waits for a
91
+ // frame. One measure per update; the renderer's write after the next
92
+ // paint lands on the same numbers and writes nothing.
93
+ this._refreshPaintedBands();
94
+ this.disposable.add(this.model.propsUpdated.subscribe(({ key }) => {
95
+ if (BAND_PROPS.has(key))
96
+ this._refreshPaintedBands();
97
+ }));
71
98
  this.on('dblclick', e => this._onDblClick(e));
72
99
  this.on('pointermove', e => this._updateHover(e));
73
- this.on('pointerdown', e => this._updateHover(e));
100
+ this.on('pointerdown', e => {
101
+ this._updateHover(e);
102
+ this._focusAt(e);
103
+ });
74
104
  this.on('pointerleave', () => this._leave());
105
+ // The focus belongs to a pool the user is working on: once it leaves the
106
+ // selection, there is no lane to act on and nothing to outline.
107
+ this.disposable.add(this.gfx.selection.slots.updated.subscribe(() => {
108
+ if (this._focusedLaneId === null)
109
+ return;
110
+ if (this.gfx.selection.selectedIds.includes(this.model.id))
111
+ return;
112
+ this._setFocus(null);
113
+ }));
114
+ }
115
+ _refreshPaintedBands() {
116
+ cachePaintedBands(this.model, bpmnPoolMeasuredLayout(this.model));
75
117
  }
76
118
  onDestroyed() {
77
119
  this._closeEditor();
@@ -121,9 +163,14 @@ export class BpmnPoolView extends GfxElementModelView {
121
163
  * - the internal LANE SEPARATORS, twelve-unit strips in the middle of the
122
164
  * flow area, which arm the drag that moves them;
123
165
  * - the zoom-GROWN rename targets, which reach past the painted bands when
124
- * the board is small on screen so a fingertip can still land on one.
166
+ * the board is small on screen so a fingertip can still land on one;
167
+ * - the part of a title band a long name has WIDENED past its declared
168
+ * width (the name wraps onto more lines and the band grows with it,
169
+ * `backgroundBandWidths`). The model selects by the declared widths, since
170
+ * it cannot measure text; the rename double-click must still land on every
171
+ * line painted, so `pool-hit.ts` aims at the band as painted.
125
172
  *
126
- * Neither is a selection zone — a click in the flow area still goes to
173
+ * None is a selection zone — a click in the flow area still goes to
127
174
  * whatever the user dropped there, because picking asks the MODEL. This
128
175
  * override only decides where the pool's own gestures are heard, which is why
129
176
  * it is declared here, beside the code that draws and handles them, rather
@@ -255,6 +302,37 @@ export class BpmnPoolView extends GfxElementModelView {
255
302
  this.model.pop('lanes');
256
303
  this._disarm();
257
304
  }
305
+ /* ── Lane focus ────────────────────────────────────────────────────── */
306
+ /**
307
+ * A press in a lane's title band of a SELECTED pool focuses that lane; a
308
+ * press in the participant band clears the focus. Anywhere else leaves it.
309
+ *
310
+ * On `pointerdown`, which this view already listens to, and NOT on `click`:
311
+ * a view that registers a handler reports the event as handled, and a
312
+ * registered `click` would take click-to-select away from the default tool.
313
+ * Selected first, for the reason the separator drag gives: a first click on
314
+ * a pool is "work on this pool", not "pick this lane".
315
+ */
316
+ _focusAt(e) {
317
+ if (!this.gfx.selection.selectedIds.includes(this.model.id))
318
+ return;
319
+ const target = this._targetAt(this._localPoint(e));
320
+ if (target === null)
321
+ return;
322
+ const lane = target.kind === 'lane' ? bpmnLanesOf(this.model)[target.index] : null;
323
+ this._setFocus(lane?.id ?? null);
324
+ }
325
+ _setFocus(id) {
326
+ if (this._focusedLaneId === id)
327
+ return;
328
+ this._focusedLaneId = id;
329
+ // The outline is painted by the canvas renderer, and the toolbar decides
330
+ // whether to offer the reorder commands: both have to be told.
331
+ this.gfx.surfaceComponent?.refresh();
332
+ this.gfx.std
333
+ .getOptional(ToolbarRegistryIdentifier)
334
+ ?.flags.refresh(ToolbarFlag.Surface);
335
+ }
258
336
  /* ── In-place naming ───────────────────────────────────────────────── */
259
337
  /**
260
338
  * A double-click renames whatever TITLE BAND it landed in, and nothing
@@ -1,4 +1,4 @@
1
- import { type BackgroundRect, backgroundPlot } from '@formicoidea/labre-core/blocks/surface';
1
+ import { type BackgroundInset, type BackgroundRect, backgroundPlot } from '@formicoidea/labre-core/blocks/surface';
2
2
  /**
3
3
  * Where a pointer is on a pool, in ELEMENT-LOCAL model units.
4
4
  *
@@ -9,8 +9,18 @@ import { type BackgroundRect, backgroundPlot } from '@formicoidea/labre-core/blo
9
9
  *
10
10
  * Every box is derived from `backgroundInstanceZones` /
11
11
  * `backgroundInstanceZoneBand` — the same two functions the renderer paints
12
- * from and the audit reports from. Nothing here restates a coordinate, so a
13
- * target cannot drift away from the thing it is a target for.
12
+ * from and the audit reports from — at the band widths `backgroundBandWidths`
13
+ * resolves, the one resolver the renderer sizes the bands with. A long name
14
+ * WIDENS its band (it wraps onto more lines), and the rename target widens with
15
+ * it: a double-click on the second line of a participant name is a
16
+ * double-click on that name. Measured with `measureBackgroundText`, the same
17
+ * metrics the canvas paints with wherever a document exists. Nothing here
18
+ * restates a coordinate, so a target cannot drift away from the thing it is a
19
+ * target for.
20
+ *
21
+ * The model's own `includesPoint` (`affine-model`, headless, no text
22
+ * measurement) still selects by the DECLARED widths; the grown part of a band
23
+ * answers the pool's gestures through the view, which asks these functions.
14
24
  *
15
25
  * ponytail: a ROTATED pool is not accounted for — the caller converts by
16
26
  * subtraction, so every box assumes an upright pool. Same reserve
@@ -18,11 +28,60 @@ import { type BackgroundRect, backgroundPlot } from '@formicoidea/labre-core/blo
18
28
  * framework background today. Upgrade: rotate the local point by
19
29
  * `-model.rotate` about the element centre, in the caller that has the element.
20
30
  */
21
- /** What these functions need of a pool: its box and its lanes. */
31
+ /**
32
+ * What these functions need of a pool: its box, its lanes and its name — the
33
+ * name and the lane names size the bands.
34
+ */
22
35
  export interface BpmnPoolGeometry {
23
36
  deserializedXYWH: readonly number[];
24
37
  lanes?: unknown;
38
+ name?: unknown;
39
+ /** What the renderer last painted — `BpmnPoolElementModel.paintedBands`. */
40
+ paintedBands?: {
41
+ margin: number;
42
+ zoneBandWidth: number;
43
+ };
25
44
  }
45
+ /**
46
+ * The pool's bands as painted: the margin and strip width the names need.
47
+ *
48
+ * The widths the renderer cached on the model when it has painted the pool —
49
+ * the very numbers the model's `includesPoint` selects by, so a rename target
50
+ * and a selection click agree to the unit; measured here otherwise (a pool not
51
+ * painted yet, a headless caller).
52
+ */
53
+ export declare function bpmnPoolLayout(model: BpmnPoolGeometry): {
54
+ margin: BackgroundInset;
55
+ zoneBandWidth: number | undefined;
56
+ };
57
+ /**
58
+ * The pool's bands MEASURED now, from its name and lanes, ignoring any cache:
59
+ * what the view writes into `paintedBands` before the first paint and after
60
+ * every change that moves a band.
61
+ */
62
+ export declare function bpmnPoolMeasuredLayout(model: BpmnPoolGeometry): {
63
+ margin: BackgroundInset;
64
+ zoneBandWidth: number | undefined;
65
+ };
66
+ /**
67
+ * Write the band widths into the model's `paintedBands` cache — the one place
68
+ * that does, for both writers: the renderer after each paint (the measured
69
+ * truth on the canvas) and the view on creation and on every change that moves
70
+ * a band, so picking never waits for a frame (a hidden tab never paints).
71
+ *
72
+ * Writes ONLY when a width changed and returns whether it wrote: a `@local()`
73
+ * write emits `elementUpdated`, which repaints, so writing an equal value on
74
+ * every paint would repaint forever.
75
+ */
76
+ export declare function cachePaintedBands(model: {
77
+ paintedBands?: {
78
+ margin: number;
79
+ zoneBandWidth: number;
80
+ };
81
+ }, layout: {
82
+ margin: BackgroundInset;
83
+ zoneBandWidth: number | undefined;
84
+ }): boolean;
26
85
  /** A lane band and, when the declaration asks for one, its title strip. */
27
86
  export interface BpmnLaneBand {
28
87
  top: number;
package/dist/pool-hit.js CHANGED
@@ -1,10 +1,59 @@
1
- import { backgroundInstanceZoneBand, backgroundInstanceZones, backgroundPlot, } from '@formicoidea/labre-core/blocks/surface';
1
+ import { backgroundBandWidths, backgroundInstanceZoneBand, backgroundInstanceZones, backgroundPlot, measureBackgroundText, } from '@formicoidea/labre-core/blocks/surface';
2
2
  import { BPMN_POOL_BACKGROUND } from './background.js';
3
- import { POOL_LANE_GRAB } from './consts.js';
3
+ import { POOL_LANE_BAND_WIDTH, POOL_LANE_GRAB } from './consts.js';
4
+ /**
5
+ * The pool's bands as painted: the margin and strip width the names need.
6
+ *
7
+ * The widths the renderer cached on the model when it has painted the pool —
8
+ * the very numbers the model's `includesPoint` selects by, so a rename target
9
+ * and a selection click agree to the unit; measured here otherwise (a pool not
10
+ * painted yet, a headless caller).
11
+ */
12
+ export function bpmnPoolLayout(model) {
13
+ const declared = BPMN_POOL_BACKGROUND.geometry.margin;
14
+ const painted = model.paintedBands;
15
+ if (painted) {
16
+ return {
17
+ margin: { ...declared, left: painted.margin },
18
+ zoneBandWidth: painted.zoneBandWidth,
19
+ };
20
+ }
21
+ return bpmnPoolMeasuredLayout(model);
22
+ }
23
+ /**
24
+ * The pool's bands MEASURED now, from its name and lanes, ignoring any cache:
25
+ * what the view writes into `paintedBands` before the first paint and after
26
+ * every change that moves a band.
27
+ */
28
+ export function bpmnPoolMeasuredLayout(model) {
29
+ const [, , w, h] = model.deserializedXYWH;
30
+ return backgroundBandWidths(BPMN_POOL_BACKGROUND, model, w, h, measureBackgroundText);
31
+ }
32
+ /**
33
+ * Write the band widths into the model's `paintedBands` cache — the one place
34
+ * that does, for both writers: the renderer after each paint (the measured
35
+ * truth on the canvas) and the view on creation and on every change that moves
36
+ * a band, so picking never waits for a frame (a hidden tab never paints).
37
+ *
38
+ * Writes ONLY when a width changed and returns whether it wrote: a `@local()`
39
+ * write emits `elementUpdated`, which repaints, so writing an equal value on
40
+ * every paint would repaint forever.
41
+ */
42
+ export function cachePaintedBands(model, layout) {
43
+ const margin = layout.margin.left;
44
+ const zoneBandWidth = layout.zoneBandWidth ?? POOL_LANE_BAND_WIDTH;
45
+ const previous = model.paintedBands;
46
+ if (previous?.margin === margin && previous.zoneBandWidth === zoneBandWidth) {
47
+ return false;
48
+ }
49
+ model.paintedBands = { margin, zoneBandWidth };
50
+ return true;
51
+ }
4
52
  /** The lane bands of this pool, or `null` when it has no usable partition. */
5
53
  export function bpmnPoolBands(model) {
6
54
  const [, , w, h] = model.deserializedXYWH;
7
- const plot = backgroundPlot(BPMN_POOL_BACKGROUND, w, h);
55
+ const layout = bpmnPoolLayout(model);
56
+ const plot = backgroundPlot(BPMN_POOL_BACKGROUND, w, h, layout.margin);
8
57
  if (!(plot.width > 0) || !(plot.height > 0))
9
58
  return null;
10
59
  const zones = backgroundInstanceZones(BPMN_POOL_BACKGROUND, model);
@@ -15,7 +64,7 @@ export function bpmnPoolBands(model) {
15
64
  bands: zones.map(zone => ({
16
65
  top: plot.y0 + zone.rect.y * plot.height,
17
66
  height: zone.rect.h * plot.height,
18
- strip: backgroundInstanceZoneBand(BPMN_POOL_BACKGROUND, zone, plot),
67
+ strip: backgroundInstanceZoneBand(BPMN_POOL_BACKGROUND, zone, plot, layout.zoneBandWidth),
19
68
  })),
20
69
  };
21
70
  }
@@ -94,10 +143,10 @@ export function bpmnLaneTitleBandAt(model, local, zoom) {
94
143
  */
95
144
  export function bpmnInPoolTitleBand(model, local, zoom) {
96
145
  const [, , w, h] = model.deserializedXYWH;
97
- const plot = backgroundPlot(BPMN_POOL_BACKGROUND, w, h);
98
- // The band IS the margin, clamped to a pool narrower than its own margin —
99
- // the same degenerate case the renderer clamps.
100
- const painted = Math.min(plot.x0, w);
146
+ // The band IS the margin — as wide as the wrapped name needs — clamped to a
147
+ // pool narrower than its own margin, the same degenerate case the renderer
148
+ // clamps.
149
+ const painted = Math.min(bpmnPoolLayout(model).margin.left, w);
101
150
  if (!(painted > 0))
102
151
  return false;
103
152
  // Grown rightwards, capped at twice the margin: past that lies the lane
@@ -24,6 +24,22 @@ export declare const bpmnPoolToolbarConfig: {
24
24
  tooltip: string;
25
25
  run: (runCtx: ToolbarContext) => void;
26
26
  };
27
+ }, {
28
+ id: string;
29
+ when: (ctx: ToolbarContext) => boolean;
30
+ generate: (ctx: ToolbarContext) => {
31
+ icon: TemplateResult;
32
+ tooltip: string;
33
+ run: (runCtx: ToolbarContext) => void;
34
+ };
35
+ }, {
36
+ id: string;
37
+ when: (ctx: ToolbarContext) => boolean;
38
+ generate: (ctx: ToolbarContext) => {
39
+ icon: TemplateResult;
40
+ tooltip: string;
41
+ run: (runCtx: ToolbarContext) => void;
42
+ };
27
43
  }, import("@formicoidea/labre-core/shared/services").ToolbarActionGenerator];
28
44
  readonly when: (ctx: ToolbarContext) => boolean;
29
45
  };
@@ -35,8 +51,8 @@ export declare const bpmnPoolToolbarExtension: import("@formicoidea/labre-core/_
35
51
  * up — the same merge C4's board makes for the same reason).
36
52
  *
37
53
  * `c.legend` and not `b.legend`: the toolbar sorts lexicographically by id
38
- * after partitioning by placement, and `'b.add-lane' < 'b.legend' <
39
- * 'b.remove-lane'` would wedge the button between the two lane gestures. `c.`
54
+ * after partitioning by placement, and `'b.lane-1-add' < 'b.legend' <
55
+ * 'b.lane-4-remove'` would wedge the button between the two lane gestures. `c.`
40
56
  * puts it where it belongs — resize · add lane · remove lane · **Legend** ·
41
57
  * Validation, with Export XML in the "⋮".
42
58
  *
@@ -3,7 +3,7 @@ import { BpmnPoolElementModel } from '@formicoidea/labre-core/model';
3
3
  import { BOARD_RESIZE_TOGGLE, commandMoreAction, ToolbarModuleExtension, translateKey, } from '@formicoidea/labre-core/shared/services';
4
4
  import { BlockFlavourIdentifier, getRegisteredCommands, runCommand, } from '@formicoidea/labre-core/std';
5
5
  import { html } from 'lit';
6
- import { bpmnExportXmlIcon, bpmnLaneAddIcon, bpmnLaneRemoveIcon, } from './icons.js';
6
+ import { bpmnExportXmlIcon, bpmnLaneAddIcon, bpmnLaneDownIcon, bpmnLaneRemoveIcon, bpmnLaneUpIcon, } from './icons.js';
7
7
  const ResizeIcon = html `<svg
8
8
  width="24"
9
9
  height="24"
@@ -90,10 +90,14 @@ export const bpmnPoolToolbarConfig = {
90
90
  booleanToggle('a.toggle-resize', BOARD_RESIZE_TOGGLE, ResizeIcon, 'resizeEnabled'),
91
91
  // The two lane gestures, next to the resize toggle: everything on this row
92
92
  // is about the SHAPE of the pool rather than about what is drawn in it.
93
- // Add before remove, because that is the order they can be used in — a pool
94
- // has no lane to remove until one has been added.
95
- commandAction('b.add-lane', 'bpmn.addLane', 'com.labre.commands.bpmn.addLane', 'Add lane', bpmnLaneAddIcon),
96
- commandAction('b.remove-lane', 'bpmn.removeLane', 'com.labre.commands.bpmn.removeLane', 'Remove lane', bpmnLaneRemoveIcon),
93
+ // Actions sort by id: add, move up, move down, remove — the order a lane
94
+ // is handled in, with remove last because it is the destructive one.
95
+ commandAction('b.lane-1-add', 'bpmn.addLane', 'com.labre.commands.bpmn.addLane', 'Add lane', bpmnLaneAddIcon),
96
+ commandAction('b.lane-4-remove', 'bpmn.removeLane', 'com.labre.commands.bpmn.removeLane', 'Remove lane', bpmnLaneRemoveIcon),
97
+ // Reorder, beside add and remove: they act on the FOCUSED lane (a click on
98
+ // its title strip) and only show while one is focused and can move.
99
+ commandAction('b.lane-2-up', 'bpmn.moveLaneUp', 'com.labre.commands.bpmn.moveLaneUp', 'Move lane up', bpmnLaneUpIcon),
100
+ commandAction('b.lane-3-down', 'bpmn.moveLaneDown', 'com.labre.commands.bpmn.moveLaneDown', 'Move lane down', bpmnLaneDownIcon),
97
101
  // …and, in the "⋮", the one thing you do to a finished process rather than
98
102
  // to the shape of a pool: take it away as a file.
99
103
  commandMoreAction('z.export-xml', 'bpmn.exportXml', 'com.labre.commands.bpmn.exportXml', 'Export BPMN XML', bpmnExportXmlIcon),
@@ -111,8 +115,8 @@ export const bpmnPoolToolbarExtension = ToolbarModuleExtension({
111
115
  * up — the same merge C4's board makes for the same reason).
112
116
  *
113
117
  * `c.legend` and not `b.legend`: the toolbar sorts lexicographically by id
114
- * after partitioning by placement, and `'b.add-lane' < 'b.legend' <
115
- * 'b.remove-lane'` would wedge the button between the two lane gestures. `c.`
118
+ * after partitioning by placement, and `'b.lane-1-add' < 'b.legend' <
119
+ * 'b.lane-4-remove'` would wedge the button between the two lane gestures. `c.`
116
120
  * puts it where it belongs — resize · add lane · remove lane · **Legend** ·
117
121
  * Validation, with Export XML in the "⋮".
118
122
  *
@@ -87,3 +87,7 @@ export declare const bpmnPoolIcon: import("lit-html").TemplateResult<2>;
87
87
  export declare const bpmnLaneAddIcon: import("lit-html").TemplateResult<2>;
88
88
  /** Remove lane — the same pool, with a minus on the band that goes. */
89
89
  export declare const bpmnLaneRemoveIcon: import("lit-html").TemplateResult<2>;
90
+ /** Move lane up — the pool cut in two, with an arrow rising out of the lower band. */
91
+ export declare const bpmnLaneUpIcon: import("lit-html").TemplateResult<2>;
92
+ /** Move lane down — the same pool, with an arrow falling out of the upper band. */
93
+ export declare const bpmnLaneDownIcon: import("lit-html").TemplateResult<2>;
@@ -179,3 +179,17 @@ export const bpmnLaneRemoveIcon = svg `<svg width="24" height="24" viewBox="0 0
179
179
  <path d="M8 12 H20.5" stroke="currentColor" stroke-width="1.6"/>
180
180
  <path d="M12.4 15.1 H16.1" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>
181
181
  </svg>`;
182
+ /** Move lane up — the pool cut in two, with an arrow rising out of the lower band. */
183
+ export const bpmnLaneUpIcon = svg `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
184
+ <rect x="3.5" y="5.5" width="17" height="13" rx="1.5" stroke="currentColor" stroke-width="1.6"/>
185
+ <path d="M8 5.5 V18.5" stroke="currentColor" stroke-width="1.6"/>
186
+ <path d="M8 12 H20.5" stroke="currentColor" stroke-width="1.6"/>
187
+ <path d="M14.25 16.6 V13.6 M12.6 15 L14.25 13.4 L15.9 15" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
188
+ </svg>`;
189
+ /** Move lane down — the same pool, with an arrow falling out of the upper band. */
190
+ export const bpmnLaneDownIcon = svg `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
191
+ <rect x="3.5" y="5.5" width="17" height="13" rx="1.5" stroke="currentColor" stroke-width="1.6"/>
192
+ <path d="M8 5.5 V18.5" stroke="currentColor" stroke-width="1.6"/>
193
+ <path d="M8 12 H20.5" stroke="currentColor" stroke-width="1.6"/>
194
+ <path d="M14.25 7.4 V10.4 M12.6 9 L14.25 10.6 L15.9 9" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
195
+ </svg>`;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@formicoidea/labre-framework-bpmn",
3
3
  "description": "Labre bpmn framework for @formicoidea/labre-core.",
4
- "version": "0.58.0",
4
+ "version": "0.59.0",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "author": "lajola",
@@ -35,7 +35,7 @@
35
35
  "dist"
36
36
  ],
37
37
  "dependencies": {
38
- "@formicoidea/labre-core": "0.58.0",
38
+ "@formicoidea/labre-core": "0.59.0",
39
39
  "lit": "^3.2.0"
40
40
  }
41
41
  }