living-documentation 7.37.0 → 7.38.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.
@@ -4,6 +4,7 @@
4
4
  import { st, markDirty } from './state.js';
5
5
  import { visEdgeProps } from './edge-rendering.js';
6
6
  import { pushSnapshot } from './history.js';
7
+ import { t } from './t.js';
7
8
 
8
9
  const DEFAULT_EDGE_COLOR = '#a8a29e';
9
10
  const FREE_ARROW_STYLE_KEY = 'ld-free-arrow-style';
@@ -33,12 +34,55 @@ export function getLastFreeArrowStyle() {
33
34
  catch { return {}; }
34
35
  }
35
36
 
37
+ function isEdgeLocked(edge) {
38
+ if (!edge) return false;
39
+ const fromN = st.nodes && st.nodes.get(edge.from);
40
+ const toN = st.nodes && st.nodes.get(edge.to);
41
+ const isFreeArrow = fromN && fromN.shapeType === 'anchor' && toN && toN.shapeType === 'anchor';
42
+ return isFreeArrow ? !!(fromN.locked && toN.locked) : !!edge.edgeLocked;
43
+ }
44
+
45
+ function selectedEdgeLockState() {
46
+ const edgeIds = (st.selectedEdgeIds || []).filter((id) => st.edges && st.edges.get(id));
47
+ if (!edgeIds.length) return { allLocked: false, edgeIds };
48
+ return {
49
+ allLocked: edgeIds.every((id) => isEdgeLocked(st.edges.get(id))),
50
+ edgeIds,
51
+ };
52
+ }
53
+
54
+ function syncEdgeLockButton() {
55
+ const btn = document.getElementById('btnEdgeLock');
56
+ if (!btn) return;
57
+ const { allLocked } = selectedEdgeLockState();
58
+ btn.textContent = allLocked ? '🔓' : '🔒';
59
+ btn.title = t(allLocked ? 'diagram.edge_panel.unlock' : 'diagram.edge_panel.lock');
60
+ btn.setAttribute('aria-label', btn.title);
61
+ btn.classList.toggle('tool-active', allLocked);
62
+ }
63
+
64
+ function setEdgeLocked(edge, locked) {
65
+ if (!edge) return;
66
+ const fromN = st.nodes && st.nodes.get(edge.from);
67
+ const toN = st.nodes && st.nodes.get(edge.to);
68
+ const isFreeArrow = fromN && fromN.shapeType === 'anchor' && toN && toN.shapeType === 'anchor';
69
+ if (isFreeArrow) {
70
+ [edge.from, edge.to].forEach((nodeId) => {
71
+ st.nodes.update({ id: nodeId, locked, fixed: locked ? { x: true, y: true } : false, draggable: !locked });
72
+ const bn = st.network && st.network.body.nodes[nodeId];
73
+ if (bn) bn.refreshNeeded = true;
74
+ });
75
+ } else {
76
+ st.edges.update({ id: edge.id, edgeLocked: locked });
77
+ }
78
+ }
79
+
36
80
  export function showEdgePanel() {
37
81
  if (!st.selectedEdgeIds.length) return;
38
82
  const e = st.edges.get(st.selectedEdgeIds[0]);
39
83
  if (!e) return;
40
84
 
41
- document.getElementById('btnEdgeLock').classList.remove('tool-active');
85
+ syncEdgeLockButton();
42
86
  document.getElementById('edgePanelControls').classList.remove('hidden');
43
87
 
44
88
  const dir = e.arrowDir ?? 'to';
@@ -69,33 +113,22 @@ export function showEdgePanel() {
69
113
  }
70
114
 
71
115
  export function toggleEdgeLock() {
72
- if (!st.selectedEdgeIds.length) return;
116
+ const { allLocked, edgeIds } = selectedEdgeLockState();
117
+ if (!edgeIds.length) return;
118
+ const nextLocked = !allLocked;
73
119
  pushSnapshot();
74
- // Locking is a one-way UI action — once locked, the only way back is the
75
- // long-press on the shape itself (see unlock-hold.js).
76
- st.selectedEdgeIds.forEach((id) => {
77
- const e = st.edges.get(id);
78
- if (!e) return;
79
- const fromN = st.nodes && st.nodes.get(e.from);
80
- const toN = st.nodes && st.nodes.get(e.to);
81
- const isFreeArrow = fromN && fromN.shapeType === 'anchor' && toN && toN.shapeType === 'anchor';
82
- if (isFreeArrow) {
83
- [e.from, e.to].forEach((nodeId) => {
84
- st.nodes.update({ id: nodeId, locked: true, fixed: { x: true, y: true }, draggable: false });
85
- const bn = st.network && st.network.body.nodes[nodeId];
86
- if (bn) bn.refreshNeeded = true;
87
- });
88
- } else {
89
- st.edges.update({ id, edgeLocked: true });
90
- }
91
- });
120
+ edgeIds.forEach((id) => setEdgeLocked(st.edges.get(id), nextLocked));
92
121
  if (st.network) {
93
- st.network.unselectAll();
94
122
  st.network.redraw();
123
+ if (nextLocked) st.network.unselectAll();
124
+ }
125
+ if (nextLocked) {
126
+ st.selectedNodeIds = [];
127
+ st.selectedEdgeIds = [];
128
+ hideEdgePanel();
129
+ } else {
130
+ syncEdgeLockButton();
95
131
  }
96
- st.selectedNodeIds = [];
97
- st.selectedEdgeIds = [];
98
- hideEdgePanel();
99
132
  markDirty();
100
133
  }
101
134
 
@@ -4,6 +4,7 @@
4
4
  import { st, markDirty } from './state.js';
5
5
  import { SHAPE_DEFAULTS } from './node-rendering.js';
6
6
  import { pushSnapshot } from './history.js';
7
+ import { t } from './t.js';
7
8
 
8
9
  // ── Last-used style persistence (per shape type) ──────────────────────────────
9
10
  // Saves colorKey/fontSize/textAlign/textValign per shapeType to localStorage so
@@ -38,10 +39,61 @@ function forceRedraw() {
38
39
  if (st.network) st.network.redraw();
39
40
  }
40
41
 
42
+ function isEdgeLocked(edge) {
43
+ if (!edge) return false;
44
+ const fromN = st.nodes && st.nodes.get(edge.from);
45
+ const toN = st.nodes && st.nodes.get(edge.to);
46
+ const isFreeArrow = fromN && fromN.shapeType === 'anchor' && toN && toN.shapeType === 'anchor';
47
+ return isFreeArrow ? !!(fromN.locked && toN.locked) : !!(edge.edgeLocked || (fromN && fromN.locked && toN && toN.locked));
48
+ }
49
+
50
+ function selectedLockState() {
51
+ const nodeIds = (st.selectedNodeIds || []).filter((id) => {
52
+ const n = st.nodes && st.nodes.get(id);
53
+ return n && n.shapeType !== 'anchor';
54
+ });
55
+ const edgeIds = (st.selectedEdgeIds || []).filter((id) => st.edges && st.edges.get(id));
56
+ const total = nodeIds.length + edgeIds.length;
57
+ if (!total) return { allLocked: false, nodeIds, edgeIds };
58
+
59
+ const nodesLocked = nodeIds.every((id) => {
60
+ const n = st.nodes.get(id);
61
+ return !!(n && n.locked);
62
+ });
63
+ const edgesLocked = edgeIds.every((id) => isEdgeLocked(st.edges.get(id)));
64
+ return { allLocked: nodesLocked && edgesLocked, nodeIds, edgeIds };
65
+ }
66
+
67
+ function syncNodeLockButton() {
68
+ const btn = document.getElementById('btnNodeLock');
69
+ if (!btn) return;
70
+ const { allLocked } = selectedLockState();
71
+ btn.textContent = allLocked ? '🔓' : '🔒';
72
+ btn.title = t(allLocked ? 'diagram.node_panel.unlock' : 'diagram.node_panel.lock');
73
+ btn.setAttribute('aria-label', btn.title);
74
+ btn.classList.toggle('tool-active', allLocked);
75
+ }
76
+
77
+ function setEdgeLocked(edge, locked) {
78
+ if (!edge) return;
79
+ const fromN = st.nodes && st.nodes.get(edge.from);
80
+ const toN = st.nodes && st.nodes.get(edge.to);
81
+ const isFreeArrow = fromN && fromN.shapeType === 'anchor' && toN && toN.shapeType === 'anchor';
82
+ if (isFreeArrow) {
83
+ [edge.from, edge.to].forEach((nodeId) => {
84
+ st.nodes.update({ id: nodeId, locked, fixed: locked ? { x: true, y: true } : false, draggable: !locked });
85
+ const bn = st.network && st.network.body.nodes[nodeId];
86
+ if (bn) bn.refreshNeeded = true;
87
+ });
88
+ } else {
89
+ st.edges.update({ id: edge.id, edgeLocked: locked });
90
+ }
91
+ }
92
+
41
93
  export function showNodePanel() {
42
94
  document.getElementById('nodePanel').classList.remove('hidden');
43
95
  document.getElementById('nodePanelControls').classList.remove('hidden');
44
- document.getElementById('btnNodeLock').classList.remove('tool-active');
96
+ syncNodeLockButton();
45
97
  // Sync the opacity slider with the first selected node's current value so the
46
98
  // slider reflects the live state rather than whatever position it was left at.
47
99
  const slider = document.getElementById('nodeBgOpacity');
@@ -57,22 +109,27 @@ export function hideNodePanel() {
57
109
  }
58
110
 
59
111
  export function toggleNodeLock() {
60
- if (!st.selectedNodeIds.length) return;
112
+ const { allLocked, nodeIds, edgeIds } = selectedLockState();
113
+ if (!nodeIds.length && !edgeIds.length) return;
114
+ const nextLocked = !allLocked;
61
115
  pushSnapshot();
62
- // Locking is a one-way UI action — once locked, the only way back is the
63
- // long-press on the shape itself (see unlock-hold.js).
64
- st.selectedNodeIds.forEach((id) => {
65
- st.nodes.update({ id, locked: true, fixed: { x: true, y: true }, draggable: false });
116
+ nodeIds.forEach((id) => {
117
+ st.nodes.update({ id, locked: nextLocked, fixed: nextLocked ? { x: true, y: true } : false, draggable: !nextLocked });
66
118
  const bn = st.network && st.network.body.nodes[id];
67
119
  if (bn) bn.refreshNeeded = true;
68
120
  });
121
+ edgeIds.forEach((id) => setEdgeLocked(st.edges.get(id), nextLocked));
69
122
  if (st.network) {
70
- st.network.unselectAll();
71
123
  st.network.redraw();
124
+ if (nextLocked) st.network.unselectAll();
125
+ }
126
+ if (nextLocked) {
127
+ st.selectedNodeIds = [];
128
+ st.selectedEdgeIds = [];
129
+ hideNodePanel();
130
+ } else {
131
+ syncNodeLockButton();
72
132
  }
73
- st.selectedNodeIds = [];
74
- st.selectedEdgeIds = [];
75
- hideNodePanel();
76
133
  markDirty();
77
134
  }
78
135
 
@@ -428,7 +428,8 @@
428
428
  "diagram.sidebar.empty": "No diagrams",
429
429
  "diagram.sidebar.delete_title": "Delete",
430
430
 
431
- "diagram.node_panel.lock": "Lock shape",
431
+ "diagram.node_panel.lock": "Lock selection",
432
+ "diagram.node_panel.unlock": "Unlock selection",
432
433
  "diagram.node_panel.edit_label": "Edit text (double-click)",
433
434
  "diagram.node_panel.edit_link": "Add / edit link",
434
435
  "diagram.node_panel.bg_opacity": "Background opacity",
@@ -461,6 +462,7 @@
461
462
  "diagram.link_panel.remove_btn": "Remove",
462
463
 
463
464
  "diagram.edge_panel.lock": "Lock arrow",
465
+ "diagram.edge_panel.unlock": "Unlock arrow",
464
466
  "diagram.edge_panel.no_arrow": "Simple line",
465
467
  "diagram.edge_panel.arrow_to": "Directional arrow",
466
468
  "diagram.edge_panel.arrow_both": "Bidirectional",
@@ -428,7 +428,8 @@
428
428
  "diagram.sidebar.empty": "Aucun diagramme",
429
429
  "diagram.sidebar.delete_title": "Supprimer",
430
430
 
431
- "diagram.node_panel.lock": "Verrouiller la forme",
431
+ "diagram.node_panel.lock": "Verrouiller la sélection",
432
+ "diagram.node_panel.unlock": "Déverrouiller la sélection",
432
433
  "diagram.node_panel.edit_label": "Modifier le texte (double-clic)",
433
434
  "diagram.node_panel.edit_link": "Ajouter / modifier un lien",
434
435
  "diagram.node_panel.bg_opacity": "Opacité du fond",
@@ -461,6 +462,7 @@
461
462
  "diagram.link_panel.remove_btn": "Retirer",
462
463
 
463
464
  "diagram.edge_panel.lock": "Verrouiller la flèche",
465
+ "diagram.edge_panel.unlock": "Déverrouiller la flèche",
464
466
  "diagram.edge_panel.no_arrow": "Trait simple",
465
467
  "diagram.edge_panel.arrow_to": "Flèche directionnelle",
466
468
  "diagram.edge_panel.arrow_both": "Bidirectionnelle",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "living-documentation",
3
- "version": "7.37.0",
3
+ "version": "7.38.0",
4
4
  "description": "A CLI tool that serves a local Markdown documentation viewer",
5
5
  "main": "dist/src/server.js",
6
6
  "bin": {