living-documentation 4.2.0 → 4.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/README.md +7 -6
- package/dist/src/frontend/diagram/main.js +2 -1
- package/dist/src/frontend/diagram/network.js +167 -12
- package/dist/src/frontend/diagram/node-rendering.js +440 -167
- package/dist/src/frontend/diagram/persistence.js +4 -2
- package/dist/src/frontend/diagram/state.js +1 -0
- package/dist/src/frontend/diagram.html +10 -0
- package/dist/src/frontend/index.html +63 -12
- package/dist/src/frontend/wordcloud.js +572 -209
- package/dist/src/routes/browse.d.ts.map +1 -1
- package/dist/src/routes/browse.js +2 -1
- package/dist/src/routes/browse.js.map +1 -1
- package/dist/src/routes/diagrams.d.ts.map +1 -1
- package/dist/src/routes/diagrams.js +2 -1
- package/dist/src/routes/diagrams.js.map +1 -1
- package/dist/src/routes/wordcloud.d.ts.map +1 -1
- package/dist/src/routes/wordcloud.js +42 -20
- package/dist/src/routes/wordcloud.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,15 +26,16 @@ ExtraFiles (added in the admin section) are always first, always expanded in a `
|
|
|
26
26
|
- **Syntax highlighting** — always dark, high-contrast code blocks
|
|
27
27
|
[](/diagram?id=d1775399110713)
|
|
28
28
|
|
|
29
|
-
- **Full-text search** — instant filter + server-side content search
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
- **Admin panel** — configure title, theme, filename pattern, and extra files in the browser
|
|
29
|
+
- **Full-text search** — instant filter + server-side content search. Returns all the files containing searched occurences, and for each file lists all the occurences, highlight them, and visit them.
|
|
30
|
+
[](/diagram?id=d1775399110713)
|
|
31
|
+
|
|
33
32
|
- **Inline editing** — edit any document directly in the browser, saves to disk instantly
|
|
34
33
|
- **Image paste** — paste an image from clipboard in the editor; auto-uploaded and inserted as Markdown
|
|
34
|
+
- **Export to PDF** — Export the markdown as a PDF document
|
|
35
|
+
- **Diagram editor** — built-in canvas diagram editor; deep-link to any diagram in the C4 Model Style; Paste images into diagrams; Export PNG From Images; And Many more features ...
|
|
36
|
+
|
|
37
|
+
- **Admin panel** — configure title, theme, filename pattern, and extra files in the browser
|
|
35
38
|
- **Word Cloud** — visualise the dominant vocabulary of any folder on disk; supports `.md`, `.ts`, `.java`, `.kt`, `.py`, `.go`, `.rs`, `.cs`, `.swift`, `.rb`, `.html`, `.css`, `.yml`, `.json` and more; stop words filtered per language
|
|
36
|
-
- **Diagram editor** — built-in canvas diagram editor (vis-network); deep-link to any diagram with `?id=`
|
|
37
|
-
- **Zero frontend build** — Tailwind and highlight.js loaded from CDN
|
|
38
39
|
|
|
39
40
|
---
|
|
40
41
|
|
|
@@ -14,7 +14,7 @@ import { toggleDebug } from './debug.js';
|
|
|
14
14
|
import { adjustZoom, resetZoom } from './zoom.js';
|
|
15
15
|
import { loadDiagramList, newDiagram, saveDiagram } from './persistence.js';
|
|
16
16
|
import { copySelected, pasteClipboard, copySelectionAsPng } from './clipboard.js';
|
|
17
|
-
import { createImageNode } from './network.js';
|
|
17
|
+
import { createImageNode, toggleEdgeStraight } from './network.js';
|
|
18
18
|
import { uploadImageBlob } from './image-upload.js';
|
|
19
19
|
import { promptImageName } from './image-name-modal.js';
|
|
20
20
|
import { showToast } from './toast.js';
|
|
@@ -91,6 +91,7 @@ document.getElementById('toolArrow').addEventListener('click', () => setTool(
|
|
|
91
91
|
document.getElementById('btnDelete').addEventListener('click', deleteSelected);
|
|
92
92
|
document.getElementById('btnPhysics').addEventListener('click', togglePhysics);
|
|
93
93
|
document.getElementById('btnGrid').addEventListener('click', toggleGrid);
|
|
94
|
+
document.getElementById('btnEdgeStraight').addEventListener('click', toggleEdgeStraight);
|
|
94
95
|
|
|
95
96
|
document.getElementById('btnZoomOut').addEventListener('click', () => adjustZoom(-0.2));
|
|
96
97
|
document.getElementById('btnZoomIn').addEventListener('click', () => adjustZoom(0.2));
|
|
@@ -18,9 +18,11 @@ import { updateZoomDisplay } from './zoom.js';
|
|
|
18
18
|
import { expandSelectionToGroup, drawGroupOutlines } from './groups.js';
|
|
19
19
|
import { navigateNodeLink, hideLinkPanel } from './link-panel.js';
|
|
20
20
|
|
|
21
|
-
export function initNetwork(savedNodes, savedEdges) {
|
|
21
|
+
export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
|
|
22
22
|
const container = document.getElementById('vis-canvas');
|
|
23
23
|
|
|
24
|
+
const edgeSmooth = edgesStraight ? { enabled: false } : { type: 'continuous' };
|
|
25
|
+
|
|
24
26
|
st.nodes = new vis.DataSet(
|
|
25
27
|
savedNodes.map((n) => ({
|
|
26
28
|
...n,
|
|
@@ -28,11 +30,16 @@ export function initNetwork(savedNodes, savedEdges) {
|
|
|
28
30
|
}))
|
|
29
31
|
);
|
|
30
32
|
st.edges = new vis.DataSet(
|
|
31
|
-
savedEdges.map((e) =>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
savedEdges.map((e) => {
|
|
34
|
+
const toNode = savedNodes.find((n) => n.id === e.to);
|
|
35
|
+
const isAnchor = toNode && toNode.shapeType === 'anchor';
|
|
36
|
+
return {
|
|
37
|
+
...e,
|
|
38
|
+
...visEdgeProps(e.arrowDir ?? 'to', e.dashes ?? false),
|
|
39
|
+
smooth: isAnchor ? { enabled: false } : edgeSmooth,
|
|
40
|
+
...(e.fontSize ? { font: { size: e.fontSize, align: 'middle', color: '#6b7280' } } : {}),
|
|
41
|
+
};
|
|
42
|
+
})
|
|
36
43
|
);
|
|
37
44
|
|
|
38
45
|
const options = {
|
|
@@ -43,7 +50,7 @@ export function initNetwork(savedNodes, savedEdges) {
|
|
|
43
50
|
},
|
|
44
51
|
interaction: { hover: true, navigationButtons: false, keyboard: false, multiselect: true },
|
|
45
52
|
nodes: { font: { size: 13, face: 'system-ui,-apple-system,sans-serif' }, borderWidth: 1.5, borderWidthSelected: 2.5, shadow: false, widthConstraint: { minimum: 60 }, heightConstraint: { minimum: 28 } },
|
|
46
|
-
edges: { smooth:
|
|
53
|
+
edges: { smooth: edgeSmooth, color: { color: '#a8a29e', highlight: '#f97316', hover: '#f97316' }, width: 1.5, selectionWidth: 2.5, font: { size: 11, align: 'middle', color: '#6b7280' } },
|
|
47
54
|
manipulation: {
|
|
48
55
|
enabled: false,
|
|
49
56
|
addEdge(data, callback) {
|
|
@@ -82,16 +89,70 @@ export function initNetwork(savedNodes, savedEdges) {
|
|
|
82
89
|
for (const edgeId of Object.keys(bodyEdges)) {
|
|
83
90
|
const edge = bodyEdges[edgeId];
|
|
84
91
|
if (!edge.connected) continue;
|
|
85
|
-
|
|
92
|
+
// Edges are drawn just before the node at their assigned level, so they
|
|
93
|
+
// appear on top of all nodes below that level.
|
|
94
|
+
// Use Math.max so the edge follows the higher-z endpoint — it stays visible
|
|
95
|
+
// above any intermediate nodes between the two endpoints.
|
|
96
|
+
// Anchor nodes are floating endpoints that must not raise the edge above the
|
|
97
|
+
// real source: for anchor edges, use the non-anchor endpoint's level.
|
|
98
|
+
const fromData = st.nodes.get(edge.fromId);
|
|
99
|
+
const toData = st.nodes.get(edge.toId);
|
|
100
|
+
const fromIsAnchor = fromData && fromData.shapeType === 'anchor';
|
|
101
|
+
const toIsAnchor = toData && toData.shapeType === 'anchor';
|
|
102
|
+
let level;
|
|
103
|
+
if (toIsAnchor && !fromIsAnchor) {
|
|
104
|
+
level = orderMap.get(edge.fromId) ?? 0;
|
|
105
|
+
} else if (fromIsAnchor && !toIsAnchor) {
|
|
106
|
+
level = orderMap.get(edge.toId) ?? 0;
|
|
107
|
+
} else {
|
|
108
|
+
level = Math.min(orderMap.get(edge.fromId) ?? 0, orderMap.get(edge.toId) ?? 0);
|
|
109
|
+
}
|
|
86
110
|
if (!edgesByLevel.has(level)) edgesByLevel.set(level, []);
|
|
87
111
|
edgesByLevel.get(level).push(edge);
|
|
88
112
|
}
|
|
89
113
|
|
|
114
|
+
// Build a map: anchorId → the edge(s) that connect to it, so we can draw
|
|
115
|
+
// each anchor dot AFTER its edge (giving the "planted arrowhead" effect).
|
|
116
|
+
const anchorEdgeLevel = new Map(); // anchorId → level at which its edge is drawn
|
|
117
|
+
for (const [level, edges] of edgesByLevel) {
|
|
118
|
+
for (const edge of edges) {
|
|
119
|
+
const fromData = st.nodes.get(edge.fromId);
|
|
120
|
+
const toData = st.nodes.get(edge.toId);
|
|
121
|
+
if (toData && toData.shapeType === 'anchor') anchorEdgeLevel.set(edge.toId, level);
|
|
122
|
+
if (fromData && fromData.shapeType === 'anchor') anchorEdgeLevel.set(edge.fromId, level);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Anchors with no connected edge are drawn at level 0 (bottom).
|
|
127
|
+
for (const id of st.canonicalOrder) {
|
|
128
|
+
const n = st.nodes.get(id);
|
|
129
|
+
if (!n || n.shapeType !== 'anchor') continue;
|
|
130
|
+
if (!anchorEdgeLevel.has(id)) anchorEdgeLevel.set(id, 0);
|
|
131
|
+
}
|
|
132
|
+
|
|
90
133
|
for (let i = 0; i < st.canonicalOrder.length; i++) {
|
|
91
|
-
const id
|
|
92
|
-
|
|
134
|
+
const id = st.canonicalOrder[i];
|
|
135
|
+
const n = st.nodes.get(id);
|
|
136
|
+
|
|
137
|
+
// Draw edges whose level is i, before drawing the node at level i.
|
|
93
138
|
const edges = edgesByLevel.get(i);
|
|
94
|
-
if (edges)
|
|
139
|
+
if (edges) {
|
|
140
|
+
edges.forEach((e) => e.draw(ctx));
|
|
141
|
+
// Draw any anchor whose edge was just drawn, so the dot appears on top.
|
|
142
|
+
for (const [anchorId, level] of anchorEdgeLevel) {
|
|
143
|
+
if (level !== i) continue;
|
|
144
|
+
const anchorNode = bodyNodes[anchorId];
|
|
145
|
+
if (!anchorNode) continue;
|
|
146
|
+
if (alwaysShow === true || anchorNode.isBoundingBoxOverlappingWith(viewableArea) === true) {
|
|
147
|
+
anchorNode.draw(ctx);
|
|
148
|
+
} else {
|
|
149
|
+
anchorNode.updateBoundingBox(ctx, anchorNode.selected);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Skip anchor nodes here — they were drawn right after their edge above.
|
|
155
|
+
if (n && n.shapeType === 'anchor') continue;
|
|
95
156
|
|
|
96
157
|
const node = bodyNodes[id];
|
|
97
158
|
if (!node) continue;
|
|
@@ -116,9 +177,30 @@ export function initNetwork(savedNodes, savedEdges) {
|
|
|
116
177
|
const existing = new Set(st.canonicalOrder);
|
|
117
178
|
items.forEach((id) => { if (!existing.has(id)) st.canonicalOrder.push(id); });
|
|
118
179
|
});
|
|
119
|
-
st.nodes.on('remove', (_, { items }) => {
|
|
180
|
+
st.nodes.on('remove', (_, { items, oldData }) => {
|
|
120
181
|
const removed = new Set(items);
|
|
121
182
|
st.canonicalOrder = st.canonicalOrder.filter((id) => !removed.has(id));
|
|
183
|
+
// If an anchor was deleted directly, also remove its connected edges.
|
|
184
|
+
(oldData || []).forEach((n) => {
|
|
185
|
+
if (n.shapeType !== 'anchor') return;
|
|
186
|
+
const connected = st.edges.get({ filter: (e) => e.from === n.id || e.to === n.id });
|
|
187
|
+
if (connected.length) st.edges.remove(connected.map((e) => e.id));
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// When an edge is removed, delete any anchor node that has no remaining edges.
|
|
192
|
+
st.edges.on('remove', (_, { oldData }) => {
|
|
193
|
+
const anchorsToCheck = new Set();
|
|
194
|
+
(oldData || []).forEach((edge) => {
|
|
195
|
+
const toData = st.nodes.get(edge.to);
|
|
196
|
+
const fromData = st.nodes.get(edge.from);
|
|
197
|
+
if (toData && toData.shapeType === 'anchor') anchorsToCheck.add(edge.to);
|
|
198
|
+
if (fromData && fromData.shapeType === 'anchor') anchorsToCheck.add(edge.from);
|
|
199
|
+
});
|
|
200
|
+
anchorsToCheck.forEach((anchorId) => {
|
|
201
|
+
const remaining = st.edges.get({ filter: (e) => e.from === anchorId || e.to === anchorId });
|
|
202
|
+
if (remaining.length === 0) st.nodes.remove(anchorId);
|
|
203
|
+
});
|
|
122
204
|
});
|
|
123
205
|
|
|
124
206
|
st.network.on('click', onClickNode);
|
|
@@ -135,6 +217,41 @@ export function initNetwork(savedNodes, savedEdges) {
|
|
|
135
217
|
st.network.on('afterDrawing', (ctx) => drawGroupOutlines(ctx));
|
|
136
218
|
st.network.on('afterDrawing', () => drawDebugOverlay());
|
|
137
219
|
|
|
220
|
+
// ── Free-floating edge: drop on empty canvas creates an anchor node ──────────
|
|
221
|
+
// vis-network only fires addEdge callback when dropping on an existing node.
|
|
222
|
+
// We intercept mousedown (capture source) + mouseup (detect empty-canvas drop).
|
|
223
|
+
let _addEdgeFromId = null;
|
|
224
|
+
const visCanvas = document.getElementById('vis-canvas');
|
|
225
|
+
|
|
226
|
+
visCanvas.addEventListener('mousedown', (e) => {
|
|
227
|
+
if (st.currentTool !== 'addEdge') return;
|
|
228
|
+
_addEdgeFromId = st.network.getNodeAt({ x: e.offsetX, y: e.offsetY }) || null;
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
visCanvas.addEventListener('mouseup', (e) => {
|
|
232
|
+
if (st.currentTool !== 'addEdge' || !_addEdgeFromId) { _addEdgeFromId = null; return; }
|
|
233
|
+
const pos = { x: e.offsetX, y: e.offsetY };
|
|
234
|
+
if (!st.network.getNodeAt(pos)) {
|
|
235
|
+
const cp = st.network.DOMtoCanvas(pos);
|
|
236
|
+
const anchorId = 'a' + Date.now();
|
|
237
|
+
st.nodes.add({
|
|
238
|
+
id: anchorId, label: '', shapeType: 'anchor', colorKey: 'c-gray',
|
|
239
|
+
nodeWidth: 8, nodeHeight: 8, fontSize: null, rotation: 0, labelRotation: 0,
|
|
240
|
+
x: cp.x, y: cp.y,
|
|
241
|
+
...visNodeProps('anchor', 'c-gray', 8, 8, null, null, null),
|
|
242
|
+
});
|
|
243
|
+
st.edges.add({
|
|
244
|
+
id: 'e' + Date.now(), from: _addEdgeFromId, to: anchorId,
|
|
245
|
+
arrowDir: 'to', dashes: false,
|
|
246
|
+
smooth: { enabled: false },
|
|
247
|
+
...visEdgeProps('to', false),
|
|
248
|
+
});
|
|
249
|
+
markDirty();
|
|
250
|
+
setTimeout(() => st.network.addEdgeMode(), 0);
|
|
251
|
+
}
|
|
252
|
+
_addEdgeFromId = null;
|
|
253
|
+
});
|
|
254
|
+
|
|
138
255
|
document.getElementById('emptyState').classList.add('hidden');
|
|
139
256
|
updateZoomDisplay();
|
|
140
257
|
}
|
|
@@ -248,9 +365,47 @@ export function createImageNode(imageSrc, canvasX, canvasY) {
|
|
|
248
365
|
img.src = imageSrc;
|
|
249
366
|
}
|
|
250
367
|
|
|
368
|
+
// ── Edge straight / curved toggle ────────────────────────────────────────────
|
|
369
|
+
export function toggleEdgeStraight() {
|
|
370
|
+
if (!st.network) return;
|
|
371
|
+
st.edgesStraight = !st.edgesStraight;
|
|
372
|
+
const smooth = st.edgesStraight ? { enabled: false } : { type: 'continuous' };
|
|
373
|
+
// Update global network option first (overrides per-edge inherited defaults).
|
|
374
|
+
st.network.setOptions({ edges: { smooth } });
|
|
375
|
+
// Then update each edge individually, keeping anchor edges always straight.
|
|
376
|
+
const updates = st.edges.get().map((e) => {
|
|
377
|
+
const toData = st.nodes.get(e.to);
|
|
378
|
+
const s = (toData && toData.shapeType === 'anchor') ? { enabled: false } : smooth;
|
|
379
|
+
return { id: e.id, smooth: s };
|
|
380
|
+
});
|
|
381
|
+
if (updates.length) st.edges.update(updates);
|
|
382
|
+
document.getElementById('btnEdgeStraight').classList.toggle('tool-active', st.edgesStraight);
|
|
383
|
+
markDirty();
|
|
384
|
+
}
|
|
385
|
+
|
|
251
386
|
function onClickNode(params) {
|
|
252
387
|
if (params.nodes.length === 1 && params.event.srcEvent.shiftKey) {
|
|
253
388
|
navigateNodeLink(params.nodes[0]);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
// Anchor nodes have nodeDimensions=0 so vis-network cannot detect clicks on
|
|
392
|
+
// them. Manually check proximity (8px threshold in canvas space).
|
|
393
|
+
if (params.nodes.length === 0 && params.edges.length === 0) {
|
|
394
|
+
const cp = params.pointer.canvas;
|
|
395
|
+
const THRESHOLD = 8;
|
|
396
|
+
const anchors = st.nodes.get({ filter: (n) => n.shapeType === 'anchor' });
|
|
397
|
+
const positions = st.network.getPositions(anchors.map((a) => a.id));
|
|
398
|
+
for (const anchor of anchors) {
|
|
399
|
+
const pos = positions[anchor.id];
|
|
400
|
+
if (!pos) continue;
|
|
401
|
+
const dx = cp.x - pos.x, dy = cp.y - pos.y;
|
|
402
|
+
if (Math.sqrt(dx * dx + dy * dy) <= THRESHOLD) {
|
|
403
|
+
st.network.selectNodes([anchor.id]);
|
|
404
|
+
st.selectedNodeIds = [anchor.id];
|
|
405
|
+
showNodePanel();
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
254
409
|
}
|
|
255
410
|
}
|
|
256
411
|
|