living-documentation 7.37.0 → 7.39.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,78 @@ 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 syncNodeFontSizeValue() {
78
+ const el = document.getElementById('nodeFontSizeValue');
79
+ if (!el) return;
80
+ const sizes = (st.selectedNodeIds || []).map((id) => {
81
+ const n = st.nodes && st.nodes.get(id);
82
+ return n && n.shapeType !== 'anchor' ? (n.fontSize || 13) : null;
83
+ }).filter((size) => size !== null);
84
+
85
+ if (!sizes.length) {
86
+ el.textContent = '–';
87
+ return;
88
+ }
89
+ const first = sizes[0];
90
+ el.textContent = sizes.every((size) => size === first) ? String(first) : '–';
91
+ }
92
+
93
+ function setEdgeLocked(edge, locked) {
94
+ if (!edge) return;
95
+ const fromN = st.nodes && st.nodes.get(edge.from);
96
+ const toN = st.nodes && st.nodes.get(edge.to);
97
+ const isFreeArrow = fromN && fromN.shapeType === 'anchor' && toN && toN.shapeType === 'anchor';
98
+ if (isFreeArrow) {
99
+ [edge.from, edge.to].forEach((nodeId) => {
100
+ st.nodes.update({ id: nodeId, locked, fixed: locked ? { x: true, y: true } : false, draggable: !locked });
101
+ const bn = st.network && st.network.body.nodes[nodeId];
102
+ if (bn) bn.refreshNeeded = true;
103
+ });
104
+ } else {
105
+ st.edges.update({ id: edge.id, edgeLocked: locked });
106
+ }
107
+ }
108
+
41
109
  export function showNodePanel() {
42
110
  document.getElementById('nodePanel').classList.remove('hidden');
43
111
  document.getElementById('nodePanelControls').classList.remove('hidden');
44
- document.getElementById('btnNodeLock').classList.remove('tool-active');
112
+ syncNodeLockButton();
113
+ syncNodeFontSizeValue();
45
114
  // Sync the opacity slider with the first selected node's current value so the
46
115
  // slider reflects the live state rather than whatever position it was left at.
47
116
  const slider = document.getElementById('nodeBgOpacity');
@@ -57,22 +126,27 @@ export function hideNodePanel() {
57
126
  }
58
127
 
59
128
  export function toggleNodeLock() {
60
- if (!st.selectedNodeIds.length) return;
129
+ const { allLocked, nodeIds, edgeIds } = selectedLockState();
130
+ if (!nodeIds.length && !edgeIds.length) return;
131
+ const nextLocked = !allLocked;
61
132
  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 });
133
+ nodeIds.forEach((id) => {
134
+ st.nodes.update({ id, locked: nextLocked, fixed: nextLocked ? { x: true, y: true } : false, draggable: !nextLocked });
66
135
  const bn = st.network && st.network.body.nodes[id];
67
136
  if (bn) bn.refreshNeeded = true;
68
137
  });
138
+ edgeIds.forEach((id) => setEdgeLocked(st.edges.get(id), nextLocked));
69
139
  if (st.network) {
70
- st.network.unselectAll();
71
140
  st.network.redraw();
141
+ if (nextLocked) st.network.unselectAll();
142
+ }
143
+ if (nextLocked) {
144
+ st.selectedNodeIds = [];
145
+ st.selectedEdgeIds = [];
146
+ hideNodePanel();
147
+ } else {
148
+ syncNodeLockButton();
72
149
  }
73
- st.selectedNodeIds = [];
74
- st.selectedEdgeIds = [];
75
- hideNodePanel();
76
150
  markDirty();
77
151
  }
78
152
 
@@ -114,6 +188,7 @@ export function changeNodeFontSize(delta) {
114
188
  st.nodes.update({ id, fontSize: newSize });
115
189
  });
116
190
  persistNodeStyle();
191
+ syncNodeFontSizeValue();
117
192
  forceRedraw();
118
193
  markDirty();
119
194
  }
@@ -681,6 +681,11 @@
681
681
  >
682
682
  Aa−
683
683
  </button>
684
+ <span
685
+ id="nodeFontSizeValue"
686
+ class="inline-flex items-center justify-center min-w-[2.25rem] h-6 px-1 text-[11px] font-mono text-gray-700 dark:text-gray-200 border border-gray-300 dark:border-gray-600 rounded bg-white/80 dark:bg-gray-800/80"
687
+ data-i18n-title="diagram.node_panel.font_size_value"
688
+ >13</span>
684
689
  <button
685
690
  id="btnNodeFontIncrease"
686
691
  class="tool-btn !w-8 !h-6"
@@ -428,11 +428,13 @@
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",
435
436
  "diagram.node_panel.font_decrease": "Decrease font size",
437
+ "diagram.node_panel.font_size_value": "Current font size",
436
438
  "diagram.node_panel.font_increase": "Increase font size",
437
439
  "diagram.node_panel.align_left": "Align left",
438
440
  "diagram.node_panel.align_center": "Align center",
@@ -461,6 +463,7 @@
461
463
  "diagram.link_panel.remove_btn": "Remove",
462
464
 
463
465
  "diagram.edge_panel.lock": "Lock arrow",
466
+ "diagram.edge_panel.unlock": "Unlock arrow",
464
467
  "diagram.edge_panel.no_arrow": "Simple line",
465
468
  "diagram.edge_panel.arrow_to": "Directional arrow",
466
469
  "diagram.edge_panel.arrow_both": "Bidirectional",
@@ -428,11 +428,13 @@
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",
435
436
  "diagram.node_panel.font_decrease": "Réduire la police",
437
+ "diagram.node_panel.font_size_value": "Taille de police actuelle",
436
438
  "diagram.node_panel.font_increase": "Agrandir la police",
437
439
  "diagram.node_panel.align_left": "Aligner à gauche",
438
440
  "diagram.node_panel.align_center": "Centrer horizontalement",
@@ -461,6 +463,7 @@
461
463
  "diagram.link_panel.remove_btn": "Retirer",
462
464
 
463
465
  "diagram.edge_panel.lock": "Verrouiller la flèche",
466
+ "diagram.edge_panel.unlock": "Déverrouiller la flèche",
464
467
  "diagram.edge_panel.no_arrow": "Trait simple",
465
468
  "diagram.edge_panel.arrow_to": "Flèche directionnelle",
466
469
  "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.39.0",
4
4
  "description": "A CLI tool that serves a local Markdown documentation viewer",
5
5
  "main": "dist/src/server.js",
6
6
  "bin": {