@bpmnkit/canvas 0.0.29 → 0.0.31

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/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  [![ai-assisted](https://img.shields.io/badge/AI--assisted-claude-8b5cf6?style=flat-square)](https://github.com/bpmnkit/monorepo)
10
10
  [![experimental](https://img.shields.io/badge/status-experimental-f59e0b?style=flat-square)](https://github.com/bpmnkit/monorepo)
11
11
 
12
- [Website](https://bpmnkit.com) · [Documentation](https://docs.bpmnkit.com) · [GitHub](https://github.com/bpmnkit/monorepo) · [Changelog](https://github.com/bpmnkit/monorepo/blob/main/packages/canvas/CHANGELOG.md)
12
+ [Website](https://bpmnkit.com) · [Documentation](https://bpmnkit.com/docs) · [GitHub](https://github.com/bpmnkit/monorepo) · [Changelog](https://github.com/bpmnkit/monorepo/blob/main/packages/canvas/CHANGELOG.md)
13
13
  </div>
14
14
 
15
15
  ---
@@ -122,10 +122,12 @@ const myPlugin: CanvasPlugin = {
122
122
  | [`@bpmnkit/plugins`](https://www.npmjs.com/package/@bpmnkit/plugins) | 22 composable canvas plugins |
123
123
  | [`@bpmnkit/api`](https://www.npmjs.com/package/@bpmnkit/api) | Camunda 8 REST API TypeScript client |
124
124
  | [`@bpmnkit/ascii`](https://www.npmjs.com/package/@bpmnkit/ascii) | Render BPMN diagrams as Unicode ASCII art |
125
+ | [`@bpmnkit/docspack`](https://www.npmjs.com/package/@bpmnkit/docspack) | BPMN Kit docs as an offline docspack package for AI agents |
125
126
  | [`@bpmnkit/ui`](https://www.npmjs.com/package/@bpmnkit/ui) | Shared design tokens and UI components |
126
127
  | [`@bpmnkit/profiles`](https://www.npmjs.com/package/@bpmnkit/profiles) | Shared auth, profile storage, and client factories for CLI & proxy |
127
128
  | [`@bpmnkit/operate`](https://www.npmjs.com/package/@bpmnkit/operate) | Monitoring & operations frontend for Camunda clusters |
128
129
  | [`@bpmnkit/connector-gen`](https://www.npmjs.com/package/@bpmnkit/connector-gen) | Generate connector templates from OpenAPI specs |
130
+ | [`@bpmnkit/connectors`](https://www.npmjs.com/package/@bpmnkit/connectors) | Camunda 8 OOTB connector catalog and deterministic template application |
129
131
  | [`@bpmnkit/cli`](https://www.npmjs.com/package/@bpmnkit/cli) | Camunda 8 command-line interface (casen) |
130
132
  | [`@bpmnkit/proxy`](https://www.npmjs.com/package/@bpmnkit/proxy) | Local AI bridge and Camunda API proxy server |
131
133
  | [`@bpmnkit/patterns`](https://www.npmjs.com/package/@bpmnkit/patterns) | Domain process patterns for BPMNKit AIKit |
package/dist/canvas.d.ts CHANGED
@@ -237,9 +237,10 @@ export declare class BpmnCanvas {
237
237
  private _updateBreadcrumb;
238
238
  /**
239
239
  * Resolves the BPMN element id under a pointer/mouse event, or `null`.
240
- * Prefers `elementFromPoint` (correct when native SVG hit-testing returns
241
- * the root `<svg>` in flex/scroll containers) and falls back to the event
242
- * target (for environments where `elementFromPoint` is unavailable).
240
+ * The event target answers directly for most moves; `elementFromPoint`
241
+ * (which forces a layout and hit-test flush) is only consulted when native
242
+ * SVG hit-testing handed us the root `<svg>`, as it does in some flex/scroll
243
+ * containers.
243
244
  */
244
245
  private _elementIdForEvent;
245
246
  /** Diagram-coordinate bounds of a shape (from DI) or edge (waypoint bbox). */
package/dist/canvas.js CHANGED
@@ -445,7 +445,9 @@ export class BpmnCanvas {
445
445
  this._scene.updateElement(id);
446
446
  this._shapes = this._scene.getShapes();
447
447
  this._edges = this._scene.getEdges();
448
- this._keyboard.setShapes(this._shapes);
448
+ const updated = this._scene.getElement(id);
449
+ if (updated && "shape" in updated)
450
+ this._keyboard.updateShape(updated);
449
451
  this._overlays.reposition();
450
452
  }
451
453
  /** Zooms in by 25% centred on the canvas. */
@@ -765,32 +767,45 @@ export class BpmnCanvas {
765
767
  }
766
768
  /**
767
769
  * Resolves the BPMN element id under a pointer/mouse event, or `null`.
768
- * Prefers `elementFromPoint` (correct when native SVG hit-testing returns
769
- * the root `<svg>` in flex/scroll containers) and falls back to the event
770
- * target (for environments where `elementFromPoint` is unavailable).
770
+ * The event target answers directly for most moves; `elementFromPoint`
771
+ * (which forces a layout and hit-test flush) is only consulted when native
772
+ * SVG hit-testing handed us the root `<svg>`, as it does in some flex/scroll
773
+ * containers.
771
774
  */
772
775
  _elementIdForEvent(e) {
776
+ const fromTarget = e.target?.closest("[data-bpmnkit-id]");
777
+ if (fromTarget)
778
+ return fromTarget.getAttribute("data-bpmnkit-id");
773
779
  const fromPoint = document.elementFromPoint(e.clientX, e.clientY);
774
- const target = fromPoint?.closest("[data-bpmnkit-id]") ??
775
- e.target?.closest("[data-bpmnkit-id]");
776
- return target?.getAttribute("data-bpmnkit-id") ?? null;
780
+ return fromPoint?.closest("[data-bpmnkit-id]")?.getAttribute("data-bpmnkit-id") ?? null;
777
781
  }
778
782
  /** Diagram-coordinate bounds of a shape (from DI) or edge (waypoint bbox). */
779
783
  _diagramBounds(id) {
780
- const shape = this._shapes.find((s) => s.id === id);
781
- if (shape) {
782
- const { x, y, width, height } = shape.shape.bounds;
784
+ const el = this._scene.getElement(id);
785
+ if (!el)
786
+ return null;
787
+ if ("shape" in el) {
788
+ const { x, y, width, height } = el.shape.bounds;
783
789
  return { x, y, width, height };
784
790
  }
785
- const edge = this._edges.find((e) => e.id === id);
786
- if (edge && edge.edge.waypoints.length > 0) {
787
- const xs = edge.edge.waypoints.map((w) => w.x);
788
- const ys = edge.edge.waypoints.map((w) => w.y);
789
- const minX = Math.min(...xs);
790
- const minY = Math.min(...ys);
791
- return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };
791
+ const waypoints = el.edge.waypoints;
792
+ if (waypoints.length === 0)
793
+ return null;
794
+ let minX = Number.POSITIVE_INFINITY;
795
+ let minY = Number.POSITIVE_INFINITY;
796
+ let maxX = Number.NEGATIVE_INFINITY;
797
+ let maxY = Number.NEGATIVE_INFINITY;
798
+ for (const w of waypoints) {
799
+ if (w.x < minX)
800
+ minX = w.x;
801
+ if (w.y < minY)
802
+ minY = w.y;
803
+ if (w.x > maxX)
804
+ maxX = w.x;
805
+ if (w.y > maxY)
806
+ maxY = w.y;
792
807
  }
793
- return null;
808
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
794
809
  }
795
810
  _applyTheme(theme) {
796
811
  const resolved = theme === "auto"
@@ -30,6 +30,11 @@ export declare class KeyboardHandler {
30
30
  constructor(_container: HTMLElement, _viewport: ViewportController, _onFit: () => void, _onActivate: (id: string) => void, _onFocus: (id: string) => void, _onBlur: () => void);
31
31
  /** Updates the navigable shape list after a diagram (re)load. */
32
32
  setShapes(shapes: RenderedShape[]): void;
33
+ /**
34
+ * Swaps in the re-rendered graphics of one shape after an incremental
35
+ * update, keeping focus position and avoiding a DOM read per shape.
36
+ */
37
+ updateShape(shape: RenderedShape): void;
33
38
  /** Removes all keyboard event listeners. */
34
39
  destroy(): void;
35
40
  private _focusShape;
package/dist/keyboard.js CHANGED
@@ -42,6 +42,26 @@ export class KeyboardHandler {
42
42
  this._shapes = shapes.filter((s) => s.element.getAttribute("tabindex") === "-1");
43
43
  this._focusIndex = -1;
44
44
  }
45
+ /**
46
+ * Swaps in the re-rendered graphics of one shape after an incremental
47
+ * update, keeping focus position and avoiding a DOM read per shape.
48
+ */
49
+ updateShape(shape) {
50
+ const focusable = shape.element.getAttribute("tabindex") === "-1";
51
+ const index = this._shapes.findIndex((s) => s.id === shape.id);
52
+ if (index === -1) {
53
+ if (focusable)
54
+ this._shapes.push(shape);
55
+ return;
56
+ }
57
+ if (focusable)
58
+ this._shapes[index] = shape;
59
+ else {
60
+ this._shapes.splice(index, 1);
61
+ if (this._focusIndex >= this._shapes.length)
62
+ this._focusIndex = -1;
63
+ }
64
+ }
45
65
  /** Removes all keyboard event listeners. */
46
66
  destroy() {
47
67
  this._container.removeEventListener("keydown", this._onKeyDown);
package/dist/renderer.js CHANGED
@@ -763,8 +763,9 @@ function renderEdge(edge, flow, instanceId, isDefault, isConditional, waypoints)
763
763
  if (waypoints.length < 2)
764
764
  return g;
765
765
  const path = svgEl("path");
766
+ const d = waypointsToRoundedPath(waypoints);
766
767
  const pathAttrs = {
767
- d: waypointsToRoundedPath(waypoints),
768
+ d,
768
769
  class: "bpmnkit-edge-path",
769
770
  "marker-end": `url(#${ids.arrow})`,
770
771
  };
@@ -777,7 +778,7 @@ function renderEdge(edge, flow, instanceId, isDefault, isConditional, waypoints)
777
778
  // Wide transparent stroke so the edge is easy to click
778
779
  const hitPath = svgEl("path");
779
780
  attr(hitPath, {
780
- d: waypointsToRoundedPath(waypoints),
781
+ d,
781
782
  fill: "none",
782
783
  stroke: "transparent",
783
784
  "stroke-width": "12",
package/dist/scene.d.ts CHANGED
@@ -23,6 +23,15 @@ export declare class Scene {
23
23
  private _defs;
24
24
  private _plane;
25
25
  private _drillableIds;
26
+ /**
27
+ * Model index and docking geometry, built once per {@link render}. The
28
+ * model is expected to be mutated in place between incremental updates
29
+ * (the scene already keeps `_defs`/`_plane` by reference), so rebuilding the
30
+ * whole index for every single-element update was pure waste.
31
+ */
32
+ private _ctx;
33
+ private readonly _shapeById;
34
+ private readonly _edgeById;
26
35
  constructor(_layers: SceneLayers, _instanceId: string);
27
36
  /** Renders a plane from scratch, replacing any current content. */
28
37
  render(defs: BpmnDefinitions, plane: BpmnDiPlane, drillableIds?: ReadonlySet<string>): void;
package/dist/scene.js CHANGED
@@ -15,6 +15,15 @@ export class Scene {
15
15
  _defs = null;
16
16
  _plane = null;
17
17
  _drillableIds = new Set();
18
+ /**
19
+ * Model index and docking geometry, built once per {@link render}. The
20
+ * model is expected to be mutated in place between incremental updates
21
+ * (the scene already keeps `_defs`/`_plane` by reference), so rebuilding the
22
+ * whole index for every single-element update was pure waste.
23
+ */
24
+ _ctx = null;
25
+ _shapeById = new Map();
26
+ _edgeById = new Map();
18
27
  constructor(_layers, _instanceId) {
19
28
  this._layers = _layers;
20
29
  this._instanceId = _instanceId;
@@ -25,6 +34,11 @@ export class Scene {
25
34
  this._defs = defs;
26
35
  this._plane = plane;
27
36
  this._drillableIds = drillableIds;
37
+ this._ctx = null;
38
+ for (const shape of plane.shapes)
39
+ this._shapeById.set(shape.bpmnElement, shape);
40
+ for (const edge of plane.edges)
41
+ this._edgeById.set(edge.bpmnElement, edge);
28
42
  const ctx = this._context();
29
43
  for (const edge of plane.edges) {
30
44
  const g = renderEdgeGroup(edge, ctx);
@@ -50,6 +64,9 @@ export class Scene {
50
64
  this._layers.shapes.replaceChildren();
51
65
  this._layers.labels.replaceChildren();
52
66
  this._registry.clear();
67
+ this._shapeById.clear();
68
+ this._edgeById.clear();
69
+ this._ctx = null;
53
70
  }
54
71
  /** Returns the registered shape or edge for an id, or `undefined`. */
55
72
  getElement(id) {
@@ -96,7 +113,7 @@ export class Scene {
96
113
  .split(/\s+/)
97
114
  .filter(Boolean);
98
115
  if (entry.kind === "edge") {
99
- const edge = this._plane.edges.find((e) => e.bpmnElement === id);
116
+ const edge = this._edgeById.get(id);
100
117
  if (!edge)
101
118
  return;
102
119
  const g = renderEdgeGroup(edge, ctx);
@@ -106,9 +123,17 @@ export class Scene {
106
123
  this._registry.set(id, { rendered: { id, element: g, edge }, label: null, kind: "edge" });
107
124
  return;
108
125
  }
109
- const shape = this._plane.shapes.find((s) => s.bpmnElement === id);
126
+ const shape = this._shapeById.get(id);
110
127
  if (!shape)
111
128
  return;
129
+ // A moved or resized shape must dock its edges against its new bounds.
130
+ const geom = ctx.geomById.get(id);
131
+ if (geom) {
132
+ geom.x = shape.bounds.x;
133
+ geom.y = shape.bounds.y;
134
+ geom.width = shape.bounds.width;
135
+ geom.height = shape.bounds.height;
136
+ }
112
137
  const { group, layer, label, rendered } = renderShapeGroup(shape, ctx);
113
138
  for (const c of oldClasses)
114
139
  group.classList.add(c);
@@ -131,10 +156,13 @@ export class Scene {
131
156
  this._registry.delete(id);
132
157
  }
133
158
  _context() {
159
+ if (this._ctx)
160
+ return this._ctx;
134
161
  if (!this._defs || !this._plane) {
135
162
  throw new Error("Scene has no rendered plane");
136
163
  }
137
- return buildRenderContext(this._defs, this._plane, this._drillableIds, this._instanceId);
164
+ this._ctx = buildRenderContext(this._defs, this._plane, this._drillableIds, this._instanceId);
165
+ return this._ctx;
138
166
  }
139
167
  }
140
168
  //# sourceMappingURL=scene.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bpmnkit/canvas",
3
- "version": "0.0.29",
3
+ "version": "0.0.31",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -17,7 +17,7 @@
17
17
  "dist/**/*.d.ts"
18
18
  ],
19
19
  "dependencies": {
20
- "@bpmnkit/core": "0.1.1"
20
+ "@bpmnkit/core": "0.2.0"
21
21
  },
22
22
  "description": "Zero-dependency SVG BPMN viewer with pan/zoom, theming, and a plugin API",
23
23
  "keywords": [