@vanduo-oss/vd3-flowchart 1.2.0 → 1.3.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/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  All notable changes to `@vanduo-oss/vd3-flowchart` are documented here.
4
4
 
5
+ ## 1.3.0 — 2026-09-17
6
+
7
+ ### Added
8
+
9
+ - Keyboard canvas navigation (arrows, Home/End, Enter to edit) and a native
10
+ **Graph outline** with labelled Edit/Connect controls.
11
+ - `FLOWCHART_DOCUMENT_VERSION` owns saved `version`. Package release stays on
12
+ `VD_FLOWCHART_VERSION`.
13
+
14
+ ### Fixed
15
+
16
+ - Option updates (read-only, grid, history) keep the live editor, camera,
17
+ selection, and applicable history. Parent echoes of emitted documents do
18
+ not loop.
19
+ - Malformed JSON and unsupported future documents throw before the active
20
+ document, selection, or history change. Unversioned input and numeric or
21
+ shortened 1.x versions through 1.2.0 still load.
22
+ - Graph outline refuses self-connection.
23
+ - Node drag translates the moved SVG node and rebuilds only incident edges;
24
+ pointer-up still records history.
25
+
5
26
  ## 1.2.0 — 2026-09-13
6
27
 
7
28
  Extracted from `@vanduo-oss/vd3-cbun@1.4.2` as a standalone package. Component
package/README.md CHANGED
@@ -6,8 +6,9 @@ Vanduo **flowchart** for Vue 3: a node/edge editor with undo/redo, layout
6
6
  modes, and a framework-agnostic core. Extracted 1-to-1 from
7
7
  `@vanduo-oss/vd3-cbun` flowchart **1.2.0**.
8
8
 
9
- **Status: 1.2.0.** `VD_FLOWCHART_VERSION` matches the package version and is
10
- **load-bearing** — `toJSON()` writes it into user documents. Do not reset it.
9
+ **Status: 1.3.0.** `VD_FLOWCHART_VERSION` tracks the package release.
10
+ `FLOWCHART_DOCUMENT_VERSION` (currently `1.2.0`) is the value `toJSON()`
11
+ writes. Do not reset either independently without a compatibility plan.
11
12
 
12
13
  ## Install
13
14
 
@@ -30,6 +31,7 @@ import {
30
31
  LAYOUT_MODES,
31
32
  FLOWCHART_NODE_TYPES,
32
33
  VD_FLOWCHART_VERSION,
34
+ FLOWCHART_DOCUMENT_VERSION,
33
35
  } from '@vanduo-oss/vd3-flowchart';
34
36
  import '@vanduo-oss/vd3-flowchart/css';
35
37
  ```
@@ -47,10 +49,14 @@ if anything but `vue` is externalized. The package declares
47
49
 
48
50
  ## Version policy
49
51
 
50
- `package.json` version **is** `VD_FLOWCHART_VERSION` (`1.2.0`). Bump both
51
- together, and only when the serialized document format changes. This continues
52
- the old-line `@vanduo-oss/flowchart` lineage (never reset to `1.0.0`). Do not
53
- reuse the retired npm name `@vanduo-oss/flowchart`.
52
+ `package.json` version **is** `VD_FLOWCHART_VERSION` (`1.3.0`). Bump those
53
+ together for a package release. Keep `FLOWCHART_DOCUMENT_VERSION` unchanged
54
+ unless the serialized schema changes; add fixtures and explicit compatibility
55
+ rules for a format change. Unversioned 1.x documents through 1.2.0 still load.
56
+ Malformed JSON and unsupported future versions throw before the active
57
+ document, selection, or history change. This continues the old-line
58
+ `@vanduo-oss/flowchart` lineage (never reset to `1.0.0`). Do not reuse the
59
+ retired npm name `@vanduo-oss/flowchart`.
54
60
 
55
61
  ## Theming
56
62
 
@@ -60,10 +66,13 @@ package dependency; any provider of the tokens works.
60
66
 
61
67
  ```js
62
68
  import '@vanduo-oss/vd3/css';
63
- // …or the tokens-only layer:
69
+ // Same component styles without bundled icon fonts:
64
70
  import '@vanduo-oss/vd3/css/core';
65
71
  ```
66
72
 
73
+ `@vanduo-oss/vd3/css/core` is **not** tokens-only. Token JSON is
74
+ `@vanduo-oss/vd3/tokens.json`.
75
+
67
76
  Tokens consumed include `--vd-bg-primary`, `--vd-bg-secondary`,
68
77
  `--vd-text-primary`, `--vd-text-muted`, `--vd-border-color`, and
69
78
  `--vd-color-primary` (via `--vd-flowchart-*` locals).
@@ -83,9 +92,24 @@ Tokens consumed include `--vd-bg-primary`, `--vd-bg-secondary`,
83
92
 
84
93
  | Export | Contents |
85
94
  | --- | --- |
86
- | `@vanduo-oss/vd3-flowchart` | `VdFlowchart` + `VdFlowchartCore`, layout, `FLOWCHART_*`, `VD_FLOWCHART_VERSION` |
95
+ | `@vanduo-oss/vd3-flowchart` | `VdFlowchart` + `VdFlowchartCore`, layout, `FLOWCHART_*`, `VD_FLOWCHART_VERSION`, `FLOWCHART_DOCUMENT_VERSION` |
87
96
  | `@vanduo-oss/vd3-flowchart/css` | Stylesheet (`dist/vd3-flowchart.css`) |
88
97
 
98
+ ## Tested interaction envelope
99
+
100
+ Observed on a Mac mini (Apple M4, 10 cores, 24 GB, macOS 26.6.2) with
101
+ Chromium 153 at 1440×1000, no CPU throttling. Two warmups and ten samples per
102
+ synchronous operation; drag used 24 pointer steps. Times include forced layout
103
+ and exclude compositor paint. These are observations, not supported-size
104
+ guarantees.
105
+
106
+ - Graph label edit: 25 / 100 / 250 / 500 nodes → 2.9 / 11.2 / 29.9 / 57 ms.
107
+ - Graph undo + redo: 3.6 / 14 / 34.5 / 69.8 ms.
108
+ - Graph tree/grid layout: 2.9 / 11.1 / 28.4 / 58.9 ms.
109
+ - Graph drag (translate the moved node SVG and rebuild only incident edges): 0.2 / 0.3 / 0.2 / 0.3 ms, down from 1.6 / 4.5 / 10.7 / 20.7 ms before that path.
110
+
111
+ Raw JSON: `vd3-docs/reviews/2026-09-16/performance-results.json`.
112
+
89
113
  ## Development
90
114
 
91
115
  ```sh
package/SKILL.md CHANGED
@@ -1,47 +1,40 @@
1
1
  ---
2
2
  name: vanduo-vd3-flowchart
3
- description: Use when adding Vanduo Vue 3 flowchart with @vanduo-oss/vd3-flowchart — node/edge editor, undo/redo, layout modes, and a framework-agnostic core. Covers install, CSS, theming, and the load-bearing VD_FLOWCHART_VERSION 1.2.0.
3
+ description: Integrate @vanduo-oss/vd3-flowchart into Vue 3 apps with controlled documents, editing, layouts, undo, and JSON persistence.
4
4
  ---
5
5
 
6
- # @vanduo-oss/vd3-flowchart
6
+ # Vanduo flowchart
7
7
 
8
- Standalone Vue 3 flowchart package. `vue >=3.3.0` is a required peer. The root
9
- import re-exports the Vue wrapper AND the framework-agnostic core
10
- (`VdFlowchartCore`). `VD_FLOWCHART_VERSION` is `1.2.0` and matches
11
- `package.json`.
8
+ Install `@vanduo-oss/vd3-flowchart` alongside Vue. Import `VdFlowchart` and
9
+ `@vanduo-oss/vd3-flowchart/css`; no global registration is needed. Styles have
10
+ token fallbacks. In a VD3 app, share the base stylesheet already imported at
11
+ app entry; choose `/css` or `/css/core`, not both.
12
12
 
13
- ## Install
13
+ Start from [the complete editor recipe](recipes/editor.vue). It includes two
14
+ nodes, an edge, a controlled `data` prop, change events, read-only state, and undo.
15
+ Give the editor a definite height. Set `auto-fit` for its first measurable layout.
14
16
 
15
- ```sh
16
- pnpm add @vanduo-oss/vd3-flowchart
17
- ```
17
+ ## State and persistence
18
18
 
19
- Nothing registers globally. For correct theming, provide the Vanduo `--vd-*`
20
- design tokens (see [Theming](#theming)).
19
+ Local edits emit `change` with `{ reason, document }`. Feeding `document` back
20
+ into `data` is safe. External replacements are silent and undoable; identical
21
+ echoes preserve selection/history. Ordinary option changes keep the live editor.
22
+ Disabling history clears it; reenabling starts from the current document.
21
23
 
22
- ## Flowchart
24
+ Save `event.document` or `getInstance().toJSON()`. Core `load()` emits by default;
25
+ catch validation errors so a failed import can be reported to the user. Unknown
26
+ future versions and malformed JSON are rejected before changing current work.
27
+ `FLOWCHART_DOCUMENT_VERSION` describes saved JSON; `VD_FLOWCHART_VERSION`
28
+ describes the package release. Do not replace document versions with app versions.
23
29
 
24
- ```js
25
- import { VdFlowchart, VdFlowchartCore } from '@vanduo-oss/vd3-flowchart';
26
- import '@vanduo-oss/vd3-flowchart/css';
27
- ```
30
+ ## Verification
28
31
 
29
- `VdFlowchart` is the Vue 3 wrapper; the framework-agnostic editor core is the
30
- same class name upstream, so it is re-exported as `VdFlowchartCore`. The entry
31
- also re-exports `computeLayout`, `LAYOUT_MODES`, and the `FLOWCHART_*` constant
32
- tables (`FLOWCHART_NODE_TYPES`, `FLOWCHART_PORTS`, `FLOWCHART_EDGE_MARKERS`,
33
- `FLOWCHART_EDGE_ROUTES`) plus `VD_FLOWCHART_VERSION`. CSS ships at
34
- `@vanduo-oss/vd3-flowchart/css`. `VD_FLOWCHART_VERSION` is `1.2.0` — it
35
- continues the old-line lineage (never reset to `1.0.0`) because the value is
36
- serialized into user documents via `toJSON().version`; resetting it would
37
- mislabel documents saved by the old line.
32
+ Add a node, toggle read-only, and confirm the edit remains. Undo/redo, save/reload,
33
+ and echo a change back through `data`. With only the keyboard, select nodes using
34
+ canvas arrows, edit with Enter, save with Ctrl/Cmd+Enter, and use Graph outline
35
+ to read relationships and connect nodes. Check the consumer's screen reader.
38
36
 
39
- ## Theming
40
-
41
- Flowchart uses `--vd-*` tokens (via `--vd-flowchart-*` locals) with built-in
42
- fallbacks. vd3 is not a package dependency.
43
-
44
- ```js
45
- import '@vanduo-oss/vd3/css';
46
- import '@vanduo-oss/vd3/css/core';
47
- ```
37
+ Use [Vue declarations](dist/vue.d.ts) for component props/events/exposed methods
38
+ and [core declarations](dist/core.d.ts) for document types and editor methods.
39
+ Core `VdFlowchartCore` requires a browser element and explicit `destroy()`;
40
+ the Vue wrapper mounts and cleans it up automatically.
package/dist/core.d.ts CHANGED
@@ -134,6 +134,8 @@ export interface MutationOptions {
134
134
  export interface LoadOptions {
135
135
  /** Keep the prior selection if the entity still exists after load. */
136
136
  preserveSelection?: boolean;
137
+ /** Suppress change notification; an identical document is a no-op. Used by Vue data updates. */
138
+ silent?: boolean;
137
139
  }
138
140
 
139
141
  export interface RemoveNodeOptions {
@@ -269,6 +271,13 @@ export class VdFlowchart {
269
271
  stopTextEdit(options?: { commit?: boolean }): void;
270
272
 
271
273
  // Lifecycle
274
+ /** Update editor options without replacing the document, camera or selection. */
275
+ updateOptions(
276
+ options: Pick<
277
+ VdFlowchartOptions,
278
+ 'readonly' | 'gridSize' | 'autoFit' | 'history' | 'historyLimit'
279
+ >,
280
+ ): this;
272
281
  destroy(): void;
273
282
  }
274
283
 
@@ -282,6 +291,8 @@ export function computeLayout(
282
291
  export const LAYOUT_MODES: readonly LayoutMode[];
283
292
 
284
293
  export const VD_FLOWCHART_VERSION: string;
294
+ /** Schema version used by toJSON(), independent of the package release. */
295
+ export const FLOWCHART_DOCUMENT_VERSION: string;
285
296
  export const FLOWCHART_NODE_TYPES: readonly FlowchartNodeType[];
286
297
  export const FLOWCHART_PORTS: readonly FlowchartPort[];
287
298
  export const FLOWCHART_EDGE_MARKERS: readonly FlowchartEdgeMarker[];
package/dist/index.cjs CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.js
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ FLOWCHART_DOCUMENT_VERSION: () => FLOWCHART_DOCUMENT_VERSION,
23
24
  FLOWCHART_EDGE_MARKERS: () => FLOWCHART_EDGE_MARKERS,
24
25
  FLOWCHART_EDGE_ROUTES: () => FLOWCHART_EDGE_ROUTES,
25
26
  FLOWCHART_NODE_TYPES: () => FLOWCHART_NODE_TYPES,
@@ -292,7 +293,8 @@ var EDGE_STROKE_PRESETS = [
292
293
  { id: "medium", label: "Medium", width: DEFAULT_EDGE_STROKE_WIDTH },
293
294
  { id: "bold", label: "Bold", width: 3.5 }
294
295
  ];
295
- var VD_FLOWCHART_VERSION = "1.2.0";
296
+ var VD_FLOWCHART_VERSION = "1.3.0";
297
+ var FLOWCHART_DOCUMENT_VERSION = "1.2.0";
296
298
  var FLOWCHART_NODE_TYPES = [
297
299
  "rounded-rect",
298
300
  "rect",
@@ -529,17 +531,45 @@ function normalizeEdge(rawEdge, index, nodeIds, usedIds) {
529
531
  syncEdgeKind(edge);
530
532
  return edge;
531
533
  }
534
+ function parseDocumentVersion(value) {
535
+ const raw = typeof value === "number" && Number.isFinite(value) ? String(value) : typeof value === "string" ? value.trim() : "";
536
+ const match = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?$/.exec(raw);
537
+ if (!match) return null;
538
+ return {
539
+ major: Number(match[1]),
540
+ minor: Number(match[2] ?? 0),
541
+ patch: Number(match[3] ?? 0)
542
+ };
543
+ }
544
+ function isSupportedDocumentVersion(value) {
545
+ const parsed = parseDocumentVersion(value);
546
+ const max = parseDocumentVersion(FLOWCHART_DOCUMENT_VERSION);
547
+ if (!parsed || !max || parsed.major !== max.major) return false;
548
+ if (parsed.minor > max.minor) return false;
549
+ if (parsed.minor === max.minor && parsed.patch > max.patch) return false;
550
+ return true;
551
+ }
532
552
  function normalizeDocument(input) {
533
553
  let source = input;
534
554
  if (typeof source === "string") {
535
555
  try {
536
556
  source = JSON.parse(source);
537
557
  } catch {
538
- source = {};
558
+ throw new TypeError("Invalid flowchart JSON. The current document was not changed.");
539
559
  }
540
560
  }
541
561
  if (!isPlainObject(source)) {
542
- source = {};
562
+ throw new TypeError("A flowchart document must be an object.");
563
+ }
564
+ if (source.version !== void 0 && !isSupportedDocumentVersion(source.version)) {
565
+ throw new RangeError(
566
+ `Unsupported flowchart document version: ${String(source.version)}. Supported through ${FLOWCHART_DOCUMENT_VERSION}.`
567
+ );
568
+ }
569
+ for (const key of ["nodes", "edges"]) {
570
+ if (source[key] !== void 0 && !Array.isArray(source[key])) {
571
+ throw new TypeError(`Flowchart ${key} must be an array.`);
572
+ }
543
573
  }
544
574
  const usedNodeIds = /* @__PURE__ */ new Set();
545
575
  const nodes = (Array.isArray(source.nodes) ? source.nodes : []).slice(0, MAX_NODES).map((node, index) => normalizeNode(node, index, usedNodeIds));
@@ -547,7 +577,7 @@ function normalizeDocument(input) {
547
577
  const usedEdgeIds = /* @__PURE__ */ new Set();
548
578
  const edges = (Array.isArray(source.edges) ? source.edges : []).slice(0, MAX_EDGES).map((edge, index) => normalizeEdge(edge, index, nodeIds, usedEdgeIds)).filter(Boolean);
549
579
  return {
550
- version: VD_FLOWCHART_VERSION,
580
+ version: FLOWCHART_DOCUMENT_VERSION,
551
581
  viewport: normalizeViewport(source.viewport),
552
582
  nodes,
553
583
  edges
@@ -1002,6 +1032,8 @@ function getBounds(nodes) {
1002
1032
  function createField(labelText, control) {
1003
1033
  const wrapper = createElement("div", { className: "vd-flowchart-field" });
1004
1034
  const label = createElement("label", { text: labelText });
1035
+ control.id || (control.id = nextId("flowchart-field"));
1036
+ label.htmlFor = control.id;
1005
1037
  wrapper.appendChild(label);
1006
1038
  wrapper.appendChild(control);
1007
1039
  return wrapper;
@@ -1386,14 +1418,127 @@ var VdFlowchart = class {
1386
1418
  this.jsonActions.appendChild(this.loadJsonButton);
1387
1419
  this.jsonPanel.appendChild(this.jsonTextarea);
1388
1420
  this.jsonPanel.appendChild(this.jsonActions);
1421
+ this.jsonStatus = createElement("p", { className: "vd-flowchart-json-status" });
1422
+ this.jsonStatus.setAttribute("role", "status");
1423
+ this.jsonPanel.appendChild(this.jsonStatus);
1389
1424
  this.inspectorPanel.appendChild(this.jsonPanel);
1390
1425
  this.body.appendChild(this.palettePanel);
1391
1426
  this.body.appendChild(this.canvasEl);
1392
1427
  this.body.appendChild(this.inspectorPanel);
1393
1428
  this.root.appendChild(this.toolbar);
1394
1429
  this.root.appendChild(this.body);
1430
+ this.buildGraphOutline();
1395
1431
  this.element.appendChild(this.root);
1396
1432
  }
1433
+ buildGraphOutline() {
1434
+ this.graphOutline = createElement("details", { className: "vd-flowchart-outline" });
1435
+ this.graphOutline.appendChild(createElement("summary", { text: "Graph outline" }));
1436
+ this.graphOutline.addEventListener("toggle", () => {
1437
+ if (this.graphOutline.open) this.renderGraphOutline();
1438
+ });
1439
+ this.graphStatus = createElement("p", { className: "vd-flowchart-sr-only" });
1440
+ this.graphStatus.setAttribute("role", "status");
1441
+ this.canvasEl.appendChild(this.graphStatus);
1442
+ this.graphControls = createElement("div", { className: "vd-flowchart-outline-controls" });
1443
+ this.graphNodeSelect = createElement("select");
1444
+ this.graphNodeSelect.setAttribute("aria-label", "Selected node");
1445
+ this.graphNodeSelect.addEventListener(
1446
+ "change",
1447
+ () => this.selectNode(this.graphNodeSelect.value)
1448
+ );
1449
+ this.graphTargetSelect = createElement("select");
1450
+ this.graphTargetSelect.setAttribute("aria-label", "Connection target");
1451
+ this.graphTargetSelect.addEventListener("change", () => this.renderGraphOutline());
1452
+ this.graphConnectButton = createElement("button", {
1453
+ text: "Connect nodes",
1454
+ type: "button",
1455
+ className: "vd-flowchart-btn"
1456
+ });
1457
+ this.graphConnectButton.addEventListener("click", () => {
1458
+ if (this.readonly || this.graphNodeSelect.value === this.graphTargetSelect.value) {
1459
+ return;
1460
+ }
1461
+ this.addEdge({
1462
+ from: this.graphNodeSelect.value,
1463
+ to: this.graphTargetSelect.value,
1464
+ autoPort: true
1465
+ });
1466
+ });
1467
+ this.graphEditButton = createElement("button", {
1468
+ text: "Edit selected label",
1469
+ type: "button",
1470
+ className: "vd-flowchart-btn"
1471
+ });
1472
+ this.graphEditButton.addEventListener(
1473
+ "click",
1474
+ () => this.startTextEdit(this.graphNodeSelect.value)
1475
+ );
1476
+ this.graphControls.append(
1477
+ createField("Selected node", this.graphNodeSelect),
1478
+ createField("Connection target", this.graphTargetSelect),
1479
+ this.graphConnectButton,
1480
+ this.graphEditButton
1481
+ );
1482
+ this.graphOutline.appendChild(this.graphControls);
1483
+ this.graphList = createElement("ul");
1484
+ this.graphList.setAttribute("aria-label", "Nodes and connections");
1485
+ this.graphOutline.appendChild(this.graphList);
1486
+ this.root.appendChild(this.graphOutline);
1487
+ this.canvasEl.setAttribute("role", "group");
1488
+ this.canvasEl.setAttribute(
1489
+ "aria-label",
1490
+ "Diagram canvas. Arrow keys select nodes; Enter edits a label. Graph outline provides connections."
1491
+ );
1492
+ this.jsonTextarea.setAttribute("aria-label", "Flowchart JSON");
1493
+ }
1494
+ renderGraphOutline() {
1495
+ if (!this.graphOutline) return;
1496
+ const nodes = this.documentData.nodes;
1497
+ const edges = this.documentData.edges;
1498
+ const labels = new Map(nodes.map((node) => [node.id, node.text || node.id]));
1499
+ const selected = this.selection?.kind === "node" ? this.selection.id : null;
1500
+ this.graphStatus.textContent = selected ? `Selected: ${labels.get(selected)}. ${nodes.length} nodes, ${edges.length} connections.` : `${nodes.length} nodes, ${edges.length} connections.`;
1501
+ if (!this.graphOutline.open) return;
1502
+ const signature = JSON.stringify([
1503
+ nodes.map((node) => [node.id, node.text]),
1504
+ edges.map((edge) => [edge.id, edge.from.nodeId, edge.to.nodeId, edge.label])
1505
+ ]);
1506
+ if (signature !== this.graphSignature) {
1507
+ this.graphSignature = signature;
1508
+ for (const select of [this.graphNodeSelect, this.graphTargetSelect]) {
1509
+ const previous = select.value;
1510
+ select.replaceChildren();
1511
+ for (const node of nodes)
1512
+ select.appendChild(
1513
+ createElement("option", { value: node.id, text: labels.get(node.id) })
1514
+ );
1515
+ if (labels.has(previous)) select.value = previous;
1516
+ }
1517
+ this.graphList.replaceChildren();
1518
+ const outgoing = new Map(nodes.map((node) => [node.id, []]));
1519
+ const incoming = new Map(nodes.map((node) => [node.id, []]));
1520
+ for (const edge of edges) {
1521
+ outgoing.get(edge.from.nodeId)?.push(`${labels.get(edge.to.nodeId)}${edge.label ? ` (${edge.label})` : ""}`);
1522
+ incoming.get(edge.to.nodeId)?.push(labels.get(edge.from.nodeId));
1523
+ }
1524
+ for (const node of nodes) {
1525
+ const to = outgoing.get(node.id);
1526
+ const from = incoming.get(node.id);
1527
+ const relationships = [
1528
+ to.length ? `Connects to ${to.join(", ")}.` : "No outgoing connections.",
1529
+ from.length ? `Connected from ${from.join(", ")}.` : ""
1530
+ ];
1531
+ this.graphList.appendChild(
1532
+ createElement("li", { text: `${labels.get(node.id)}. ${relationships.join(" ")}` })
1533
+ );
1534
+ }
1535
+ }
1536
+ if (selected) this.graphNodeSelect.value = selected;
1537
+ this.graphNodeSelect.disabled = nodes.length === 0;
1538
+ this.graphTargetSelect.disabled = this.readonly || nodes.length < 2;
1539
+ this.graphConnectButton.disabled = this.readonly || nodes.length < 2 || this.graphNodeSelect.value === this.graphTargetSelect.value;
1540
+ this.graphEditButton.disabled = this.readonly || nodes.length === 0;
1541
+ }
1397
1542
  bindEvents() {
1398
1543
  this.toolbar.addEventListener("click", this.handleToolbarClick);
1399
1544
  this.arrangeSelect.addEventListener("change", this.handleArrangeChange);
@@ -1500,7 +1645,12 @@ var VdFlowchart = class {
1500
1645
  return;
1501
1646
  }
1502
1647
  if (action === "load" && !this.readonly) {
1503
- this.load(this.jsonTextarea.value);
1648
+ try {
1649
+ this.load(this.jsonTextarea.value);
1650
+ this.jsonStatus.textContent = "";
1651
+ } catch (error) {
1652
+ this.jsonStatus.textContent = error.message;
1653
+ }
1504
1654
  }
1505
1655
  }
1506
1656
  handleSelectionFieldInput(event) {
@@ -1571,10 +1721,31 @@ var VdFlowchart = class {
1571
1721
  }
1572
1722
  }
1573
1723
  handleKeyDown(event) {
1574
- if (this.readonly) return;
1575
1724
  if (event.target && (event.target.tagName === "INPUT" || event.target.tagName === "TEXTAREA" || event.target.tagName === "SELECT")) {
1576
1725
  return;
1577
1726
  }
1727
+ if (event.target === this.canvasEl) {
1728
+ const nodes = this.documentData.nodes;
1729
+ const index = nodes.findIndex((node) => node.id === this.selection?.id);
1730
+ let next = -1;
1731
+ if (event.key === "ArrowDown" || event.key === "ArrowRight")
1732
+ next = (index + 1) % nodes.length;
1733
+ if (event.key === "ArrowUp" || event.key === "ArrowLeft")
1734
+ next = index < 0 ? nodes.length - 1 : (index - 1 + nodes.length) % nodes.length;
1735
+ if (event.key === "Home") next = 0;
1736
+ if (event.key === "End") next = nodes.length - 1;
1737
+ if (nodes[next]) {
1738
+ event.preventDefault();
1739
+ this.selectNode(nodes[next].id);
1740
+ return;
1741
+ }
1742
+ if (event.key === "Enter" && !this.readonly && this.selection?.kind === "node") {
1743
+ event.preventDefault();
1744
+ this.startTextEdit(this.selection.id);
1745
+ return;
1746
+ }
1747
+ }
1748
+ if (this.readonly) return;
1578
1749
  if (event.key === "Escape" && this.activeTool) {
1579
1750
  event.preventDefault();
1580
1751
  this.setActiveTool(null);
@@ -1865,7 +2036,7 @@ var VdFlowchart = class {
1865
2036
  node.x = nextX;
1866
2037
  node.y = nextY;
1867
2038
  this.interaction.moved = true;
1868
- this.render({ inspector: false, json: false });
2039
+ this.renderDraggedNode(node);
1869
2040
  }
1870
2041
  return;
1871
2042
  }
@@ -2240,11 +2411,13 @@ var VdFlowchart = class {
2240
2411
  if (event.key === "Escape") {
2241
2412
  event.preventDefault();
2242
2413
  this.stopTextEdit({ commit: false });
2414
+ this.canvasEl.focus();
2243
2415
  return;
2244
2416
  }
2245
2417
  if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
2246
2418
  event.preventDefault();
2247
2419
  this.stopTextEdit({ commit: true });
2420
+ this.canvasEl.focus();
2248
2421
  }
2249
2422
  });
2250
2423
  textarea.addEventListener("blur", () => {
@@ -2318,6 +2491,7 @@ var VdFlowchart = class {
2318
2491
  if (shouldRenderScene) this.renderScene();
2319
2492
  if (shouldRenderInspector) this.renderSelectionPanel();
2320
2493
  if (shouldRenderJson) this.syncJsonTextarea();
2494
+ if (shouldRenderInspector) this.renderGraphOutline();
2321
2495
  this.updateToolbarLabel();
2322
2496
  this.updatePaletteState();
2323
2497
  this.positionTextEditor();
@@ -2333,15 +2507,28 @@ var VdFlowchart = class {
2333
2507
  clearChildren(this.nodesLayer);
2334
2508
  clearChildren(this.overlayLayer);
2335
2509
  const nodeMap = new Map(this.documentData.nodes.map((node) => [node.id, node]));
2510
+ this.sceneNodeMap = nodeMap;
2511
+ this.nodeElements = /* @__PURE__ */ new Map();
2512
+ this.edgeElements = /* @__PURE__ */ new Map();
2513
+ this.incidentEdges = /* @__PURE__ */ new Map();
2336
2514
  this.documentData.edges.forEach((edge) => {
2337
2515
  const edgeElement = this.renderEdge(edge, nodeMap);
2338
- if (edgeElement) this.edgesLayer.appendChild(edgeElement);
2516
+ if (edgeElement) {
2517
+ this.edgesLayer.appendChild(edgeElement);
2518
+ this.edgeElements.set(edge.id, edgeElement);
2519
+ for (const id of /* @__PURE__ */ new Set([edge.from.nodeId, edge.to.nodeId])) {
2520
+ if (!this.incidentEdges.has(id)) this.incidentEdges.set(id, []);
2521
+ this.incidentEdges.get(id).push(edge);
2522
+ }
2523
+ }
2339
2524
  });
2340
2525
  if (this.interaction?.kind === "connect" || this.interaction?.kind === "reconnect") {
2341
2526
  this.previewLayer.appendChild(this.renderPreviewEdge(this.interaction));
2342
2527
  }
2343
2528
  this.documentData.nodes.forEach((node) => {
2344
- this.nodesLayer.appendChild(this.renderNode(node));
2529
+ const element = this.renderNode(node);
2530
+ this.nodesLayer.appendChild(element);
2531
+ this.nodeElements.set(node.id, element);
2345
2532
  });
2346
2533
  if (this.reconnectEdgeId && !this.readonly) {
2347
2534
  const reconnectEdge = this.findEdge(this.reconnectEdgeId);
@@ -2357,6 +2544,25 @@ var VdFlowchart = class {
2357
2544
  }
2358
2545
  this.syncConnectingState();
2359
2546
  }
2547
+ // Drag frames change one translation and its connected paths. Keep unrelated
2548
+ // SVG elements intact; the release event still refreshes inspector/history.
2549
+ renderDraggedNode(node) {
2550
+ const element = this.nodeElements?.get(node.id);
2551
+ if (!element) {
2552
+ this.render({ inspector: false, json: false });
2553
+ return;
2554
+ }
2555
+ element.setAttribute("transform", `translate(${formatNumber(node.x)} ${formatNumber(node.y)})`);
2556
+ element.classList.add("is-dragging");
2557
+ for (const edge of this.incidentEdges.get(node.id) || []) {
2558
+ const previous = this.edgeElements.get(edge.id);
2559
+ const next = this.renderEdge(edge, this.sceneNodeMap);
2560
+ if (previous && next) {
2561
+ previous.replaceWith(next);
2562
+ this.edgeElements.set(edge.id, next);
2563
+ }
2564
+ }
2565
+ }
2360
2566
  applyEdgeMarkers(pathElement, edge) {
2361
2567
  if (edge.startMarker && edge.startMarker !== "none") {
2362
2568
  pathElement.setAttribute(
@@ -3371,20 +3577,69 @@ var VdFlowchart = class {
3371
3577
  this.emitChange("clear");
3372
3578
  return this;
3373
3579
  }
3580
+ updateOptions(options = {}) {
3581
+ if (this.destroyed) return this;
3582
+ if ("readonly" in options) {
3583
+ if (options.readonly && !this.readonly) {
3584
+ this.stopTextEdit({ commit: true });
3585
+ this.interaction = null;
3586
+ this.activeTool = null;
3587
+ this.reconnectEdgeId = null;
3588
+ }
3589
+ this.readonly = Boolean(options.readonly);
3590
+ this.root.classList.toggle("vd-flowchart-readonly", this.readonly);
3591
+ this.arrangeSelect.disabled = this.readonly;
3592
+ this.clearButton.disabled = this.readonly;
3593
+ this.loadJsonButton.disabled = this.readonly;
3594
+ }
3595
+ if ("gridSize" in options) {
3596
+ this.gridSize = clamp(toFiniteNumber(options.gridSize, DEFAULT_GRID_SIZE), 12, 64);
3597
+ const pattern = this.svg.querySelector("pattern");
3598
+ pattern.setAttribute("width", this.gridSize);
3599
+ pattern.setAttribute("height", this.gridSize);
3600
+ pattern.querySelector("path").setAttribute("d", `M ${this.gridSize} 0 L 0 0 0 ${this.gridSize}`);
3601
+ }
3602
+ if ("autoFit" in options) this.autoFit = Boolean(options.autoFit);
3603
+ if ("history" in options && options.history !== false !== this.historyEnabled) {
3604
+ this.historyEnabled = options.history !== false;
3605
+ this.history = [];
3606
+ this.historyIndex = -1;
3607
+ this.seedHistory();
3608
+ }
3609
+ if ("historyLimit" in options) {
3610
+ this.historyLimit = Math.max(1, Math.floor(toFiniteNumber(options.historyLimit, 100)));
3611
+ if (this.history.length > this.historyLimit) {
3612
+ const start = Math.max(0, this.historyIndex - this.historyLimit + 1);
3613
+ this.history = this.history.slice(start, start + this.historyLimit);
3614
+ this.historyIndex -= start;
3615
+ }
3616
+ }
3617
+ this.render();
3618
+ this.updateHistoryButtons();
3619
+ return this;
3620
+ }
3374
3621
  load(data, options = {}) {
3622
+ const documentData = normalizeDocument(data);
3623
+ if (options.silent && JSON.stringify(documentData) === JSON.stringify(this.documentData)) {
3624
+ return this;
3625
+ }
3375
3626
  this.stopTextEdit({ commit: false });
3376
3627
  this.activeTool = null;
3377
3628
  this.reconnectEdgeId = null;
3378
3629
  const previousSelection = this.selection;
3379
- this.documentData = normalizeDocument(data);
3630
+ this.documentData = documentData;
3380
3631
  this.selection = this.resolvePreservedSelection(previousSelection, options.preserveSelection);
3381
3632
  this.render();
3382
- this.emitChange("load");
3633
+ if (options.silent) {
3634
+ if (this.historyEnabled) this.recordHistory("load");
3635
+ } else {
3636
+ this.emitChange("load");
3637
+ }
3383
3638
  return this;
3384
3639
  }
3385
3640
  toJSON() {
3386
3641
  return deepClone({
3387
- version: VD_FLOWCHART_VERSION,
3642
+ version: FLOWCHART_DOCUMENT_VERSION,
3388
3643
  viewport: this.documentData.viewport,
3389
3644
  nodes: this.documentData.nodes,
3390
3645
  edges: this.documentData.edges
@@ -3398,6 +3653,10 @@ var VdFlowchart = class {
3398
3653
  this.resizeObserver?.disconnect();
3399
3654
  this.resizeObserver = null;
3400
3655
  this.unbindEvents();
3656
+ this.sceneNodeMap?.clear();
3657
+ this.nodeElements?.clear();
3658
+ this.edgeElements?.clear();
3659
+ this.incidentEdges?.clear();
3401
3660
  this.element.innerHTML = "";
3402
3661
  this.element.classList.remove("vd-flowchart-host");
3403
3662
  }
@@ -3449,7 +3708,7 @@ var VdFlowchart2 = (0, import_vue.defineComponent)({
3449
3708
  () => props.data,
3450
3709
  (next) => {
3451
3710
  if (instance && typeof instance.load === "function") {
3452
- instance.load(next, { preserveSelection: props.preserveSelection });
3711
+ instance.load(next, { preserveSelection: props.preserveSelection, silent: true });
3453
3712
  }
3454
3713
  },
3455
3714
  { deep: true }
@@ -3458,8 +3717,13 @@ var VdFlowchart2 = (0, import_vue.defineComponent)({
3458
3717
  () => [props.readonly, props.gridSize, props.autoFit, props.history, props.historyLimit],
3459
3718
  () => {
3460
3719
  if (!instance) return;
3461
- instance.destroy();
3462
- create();
3720
+ instance.updateOptions({
3721
+ readonly: props.readonly,
3722
+ gridSize: props.gridSize,
3723
+ autoFit: props.autoFit,
3724
+ history: props.history,
3725
+ historyLimit: props.historyLimit
3726
+ });
3463
3727
  }
3464
3728
  );
3465
3729
  (0, import_vue.onBeforeUnmount)(() => {