living-ai-documentation 1.0.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/LICENSE +661 -0
- package/README.fr.md +344 -0
- package/README.md +344 -0
- package/dist/bin/cli.d.ts +3 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +262 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/src/frontend/accuracy-gauge.js +70 -0
- package/dist/src/frontend/admin.html +1532 -0
- package/dist/src/frontend/annotations.js +585 -0
- package/dist/src/frontend/boot.js +101 -0
- package/dist/src/frontend/config.js +29 -0
- package/dist/src/frontend/confirm-modal.js +82 -0
- package/dist/src/frontend/context.html +1252 -0
- package/dist/src/frontend/dark-mode.js +20 -0
- package/dist/src/frontend/diagram/alignment.js +161 -0
- package/dist/src/frontend/diagram/clipboard.js +187 -0
- package/dist/src/frontend/diagram/constants.js +109 -0
- package/dist/src/frontend/diagram/custom-shapes.js +104 -0
- package/dist/src/frontend/diagram/debug.js +43 -0
- package/dist/src/frontend/diagram/drawio-export.js +649 -0
- package/dist/src/frontend/diagram/edge-panel.js +293 -0
- package/dist/src/frontend/diagram/edge-rendering.js +12 -0
- package/dist/src/frontend/diagram/evidence.js +146 -0
- package/dist/src/frontend/diagram/grid.js +78 -0
- package/dist/src/frontend/diagram/groups.js +102 -0
- package/dist/src/frontend/diagram/history.js +157 -0
- package/dist/src/frontend/diagram/image-name-modal.js +48 -0
- package/dist/src/frontend/diagram/image-upload.js +36 -0
- package/dist/src/frontend/diagram/label-editor.js +115 -0
- package/dist/src/frontend/diagram/link-panel.js +144 -0
- package/dist/src/frontend/diagram/main.js +364 -0
- package/dist/src/frontend/diagram/network.js +2214 -0
- package/dist/src/frontend/diagram/node-panel.js +389 -0
- package/dist/src/frontend/diagram/node-rendering.js +964 -0
- package/dist/src/frontend/diagram/persistence.js +168 -0
- package/dist/src/frontend/diagram/ports.js +421 -0
- package/dist/src/frontend/diagram/selection-overlay.js +387 -0
- package/dist/src/frontend/diagram/state.js +43 -0
- package/dist/src/frontend/diagram/t.js +3 -0
- package/dist/src/frontend/diagram/toast.js +21 -0
- package/dist/src/frontend/diagram/unlock-hold.js +206 -0
- package/dist/src/frontend/diagram/zoom.js +20 -0
- package/dist/src/frontend/diagram-link-modal.js +137 -0
- package/dist/src/frontend/diagram.html +1494 -0
- package/dist/src/frontend/documents.js +479 -0
- package/dist/src/frontend/export.js +338 -0
- package/dist/src/frontend/file-attach.js +178 -0
- package/dist/src/frontend/files-modal.js +243 -0
- package/dist/src/frontend/i18n/en.json +624 -0
- package/dist/src/frontend/i18n/fr.json +624 -0
- package/dist/src/frontend/i18n.js +32 -0
- package/dist/src/frontend/image-paste.js +126 -0
- package/dist/src/frontend/index.html +2806 -0
- package/dist/src/frontend/local-search.js +476 -0
- package/dist/src/frontend/metadata.js +318 -0
- package/dist/src/frontend/misc.js +92 -0
- package/dist/src/frontend/new-doc-modal.js +285 -0
- package/dist/src/frontend/new-folder-modal.js +169 -0
- package/dist/src/frontend/search.js +194 -0
- package/dist/src/frontend/shape-editor.html +685 -0
- package/dist/src/frontend/sidebar-helpers.js +96 -0
- package/dist/src/frontend/sidebar-resize.js +98 -0
- package/dist/src/frontend/sidebar.js +351 -0
- package/dist/src/frontend/snippet-detect.js +25 -0
- package/dist/src/frontend/snippet-table.js +85 -0
- package/dist/src/frontend/snippet-tree.js +94 -0
- package/dist/src/frontend/snippets.js +1146 -0
- package/dist/src/frontend/state.js +46 -0
- package/dist/src/frontend/utils.js +21 -0
- package/dist/src/frontend/validate.js +107 -0
- package/dist/src/frontend/vendor/wordcloud2.js +1187 -0
- package/dist/src/frontend/wordcloud.js +693 -0
- package/dist/src/lib/config.d.ts +26 -0
- package/dist/src/lib/config.d.ts.map +1 -0
- package/dist/src/lib/config.js +195 -0
- package/dist/src/lib/config.js.map +1 -0
- package/dist/src/lib/hash.d.ts +2 -0
- package/dist/src/lib/hash.d.ts.map +1 -0
- package/dist/src/lib/hash.js +18 -0
- package/dist/src/lib/hash.js.map +1 -0
- package/dist/src/lib/metadata.d.ts +31 -0
- package/dist/src/lib/metadata.d.ts.map +1 -0
- package/dist/src/lib/metadata.js +128 -0
- package/dist/src/lib/metadata.js.map +1 -0
- package/dist/src/lib/parser.d.ts +11 -0
- package/dist/src/lib/parser.d.ts.map +1 -0
- package/dist/src/lib/parser.js +111 -0
- package/dist/src/lib/parser.js.map +1 -0
- package/dist/src/lib/status.d.ts +9 -0
- package/dist/src/lib/status.d.ts.map +1 -0
- package/dist/src/lib/status.js +72 -0
- package/dist/src/lib/status.js.map +1 -0
- package/dist/src/mcp/server.d.ts +3 -0
- package/dist/src/mcp/server.d.ts.map +1 -0
- package/dist/src/mcp/server.js +2046 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/dist/src/mcp/tools/diagrams.d.ts +82 -0
- package/dist/src/mcp/tools/diagrams.d.ts.map +1 -0
- package/dist/src/mcp/tools/diagrams.js +594 -0
- package/dist/src/mcp/tools/diagrams.js.map +1 -0
- package/dist/src/mcp/tools/documents.d.ts +44 -0
- package/dist/src/mcp/tools/documents.d.ts.map +1 -0
- package/dist/src/mcp/tools/documents.js +186 -0
- package/dist/src/mcp/tools/documents.js.map +1 -0
- package/dist/src/mcp/tools/git.d.ts +10 -0
- package/dist/src/mcp/tools/git.d.ts.map +1 -0
- package/dist/src/mcp/tools/git.js +217 -0
- package/dist/src/mcp/tools/git.js.map +1 -0
- package/dist/src/mcp/tools/metadata.d.ts +57 -0
- package/dist/src/mcp/tools/metadata.d.ts.map +1 -0
- package/dist/src/mcp/tools/metadata.js +222 -0
- package/dist/src/mcp/tools/metadata.js.map +1 -0
- package/dist/src/mcp/tools/source.d.ts +29 -0
- package/dist/src/mcp/tools/source.d.ts.map +1 -0
- package/dist/src/mcp/tools/source.js +196 -0
- package/dist/src/mcp/tools/source.js.map +1 -0
- package/dist/src/routes/annotations.d.ts +3 -0
- package/dist/src/routes/annotations.d.ts.map +1 -0
- package/dist/src/routes/annotations.js +83 -0
- package/dist/src/routes/annotations.js.map +1 -0
- package/dist/src/routes/browse-source.d.ts +3 -0
- package/dist/src/routes/browse-source.d.ts.map +1 -0
- package/dist/src/routes/browse-source.js +79 -0
- package/dist/src/routes/browse-source.js.map +1 -0
- package/dist/src/routes/browse.d.ts +3 -0
- package/dist/src/routes/browse.d.ts.map +1 -0
- package/dist/src/routes/browse.js +91 -0
- package/dist/src/routes/browse.js.map +1 -0
- package/dist/src/routes/config.d.ts +3 -0
- package/dist/src/routes/config.d.ts.map +1 -0
- package/dist/src/routes/config.js +145 -0
- package/dist/src/routes/config.js.map +1 -0
- package/dist/src/routes/context.d.ts +3 -0
- package/dist/src/routes/context.d.ts.map +1 -0
- package/dist/src/routes/context.js +287 -0
- package/dist/src/routes/context.js.map +1 -0
- package/dist/src/routes/diagrams.d.ts +3 -0
- package/dist/src/routes/diagrams.d.ts.map +1 -0
- package/dist/src/routes/diagrams.js +69 -0
- package/dist/src/routes/diagrams.js.map +1 -0
- package/dist/src/routes/documents.d.ts +11 -0
- package/dist/src/routes/documents.d.ts.map +1 -0
- package/dist/src/routes/documents.js +450 -0
- package/dist/src/routes/documents.js.map +1 -0
- package/dist/src/routes/export.d.ts +3 -0
- package/dist/src/routes/export.d.ts.map +1 -0
- package/dist/src/routes/export.js +280 -0
- package/dist/src/routes/export.js.map +1 -0
- package/dist/src/routes/files.d.ts +3 -0
- package/dist/src/routes/files.d.ts.map +1 -0
- package/dist/src/routes/files.js +180 -0
- package/dist/src/routes/files.js.map +1 -0
- package/dist/src/routes/images.d.ts +3 -0
- package/dist/src/routes/images.d.ts.map +1 -0
- package/dist/src/routes/images.js +49 -0
- package/dist/src/routes/images.js.map +1 -0
- package/dist/src/routes/metadata.d.ts +3 -0
- package/dist/src/routes/metadata.d.ts.map +1 -0
- package/dist/src/routes/metadata.js +131 -0
- package/dist/src/routes/metadata.js.map +1 -0
- package/dist/src/routes/shape-libraries.d.ts +3 -0
- package/dist/src/routes/shape-libraries.d.ts.map +1 -0
- package/dist/src/routes/shape-libraries.js +118 -0
- package/dist/src/routes/shape-libraries.js.map +1 -0
- package/dist/src/routes/wordcloud.d.ts +3 -0
- package/dist/src/routes/wordcloud.d.ts.map +1 -0
- package/dist/src/routes/wordcloud.js +95 -0
- package/dist/src/routes/wordcloud.js.map +1 -0
- package/dist/src/server.d.ts +7 -0
- package/dist/src/server.d.ts.map +1 -0
- package/dist/src/server.js +93 -0
- package/dist/src/server.js.map +1 -0
- package/dist/starter-doc/.living-doc.json +52 -0
- package/dist/starter-doc/ADRS/2026_01_01_[ADR]_example_architecture_decision.md +59 -0
- package/dist/starter-doc/AI/2026_01_01_how_to.md +112 -0
- package/dist/starter-doc/AI/PROJECT-INSTRUCTIONS.md +172 -0
- package/dist/starter-doc/AI/PROJECT-STACK.md +77 -0
- package/dist/starter-doc/AI/PROJECT-USEFUL-COMMANDS.md +80 -0
- package/dist/starter-doc/AI/default/AGENTS.md +31 -0
- package/dist/starter-doc/AI/default/CLAUDE.md +31 -0
- package/dist/starter-doc/AI/default/MEMORY.md +24 -0
- package/dist/starter-doc/AI/rules/no-magic-numbers.md +18 -0
- package/dist/starter-doc/AI/rules/track-current-work.md +23 -0
- package/dist/starter-doc/WORKLOG/current-task.md +57 -0
- package/dist/starter-doc-fr/.living-doc.json +52 -0
- package/dist/starter-doc-fr/ADRS/2026_01_01_[ADR]_example_architecture_decision.md +59 -0
- package/dist/starter-doc-fr/AI/2026_01_01_how_to.md +100 -0
- package/dist/starter-doc-fr/AI/PROJECT-INSTRUCTIONS.md +172 -0
- package/dist/starter-doc-fr/AI/PROJECT-STACK.md +77 -0
- package/dist/starter-doc-fr/AI/PROJECT-USEFUL-COMMANDS.md +80 -0
- package/dist/starter-doc-fr/AI/default/AGENTS.md +31 -0
- package/dist/starter-doc-fr/AI/default/CLAUDE.md +31 -0
- package/dist/starter-doc-fr/AI/default/MEMORY.md +24 -0
- package/dist/starter-doc-fr/AI/rules/no-magic-numbers.md +18 -0
- package/dist/starter-doc-fr/AI/rules/track-current-work.md +23 -0
- package/dist/starter-doc-fr/WORKLOG/current-task.md +57 -0
- package/images/living_documentation.jpg +0 -0
- package/images/readme-extra-files.png +0 -0
- package/images/readme-filename-pattern.png +0 -0
- package/images/readme-intelligent-search-demo.jpg +0 -0
- package/images/readme-sidebar.png +0 -0
- package/package.json +72 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// ── Undo / Redo history ────────────────────────────────────────────────────────
|
|
2
|
+
// Snapshot-based undo/redo. Captures semantic node/edge data (same format as
|
|
3
|
+
// saveDiagram) so vis-network rendering props are reconstructed cleanly on
|
|
4
|
+
// restore. History is scoped to the current diagram — call resetHistory() on
|
|
5
|
+
// every diagram switch so stacks don't bleed across diagrams.
|
|
6
|
+
|
|
7
|
+
import { st, markDirty } from './state.js';
|
|
8
|
+
import { visNodeProps } from './node-rendering.js';
|
|
9
|
+
import { visEdgeProps } from './edge-rendering.js';
|
|
10
|
+
|
|
11
|
+
const MAX_HISTORY = 50;
|
|
12
|
+
|
|
13
|
+
let _undoStack = [];
|
|
14
|
+
let _redoStack = [];
|
|
15
|
+
|
|
16
|
+
// ── State capture ──────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function captureState() {
|
|
19
|
+
if (!st.nodes || !st.edges || !st.network) return null;
|
|
20
|
+
const positions = st.network.getPositions();
|
|
21
|
+
const nodes = st.nodes.get().map((n) => ({
|
|
22
|
+
id: n.id, label: n.label,
|
|
23
|
+
shapeType: n.shapeType || 'box', colorKey: n.colorKey || 'c-gray',
|
|
24
|
+
kind: n.kind || null, renderAs: n.renderAs || null,
|
|
25
|
+
description: n.description || null,
|
|
26
|
+
evidence: Array.isArray(n.evidence) ? n.evidence : null,
|
|
27
|
+
nodeWidth: n.nodeWidth || null, nodeHeight: n.nodeHeight || null,
|
|
28
|
+
fontSize: n.fontSize || null, textAlign: n.textAlign || null, textValign: n.textValign || null,
|
|
29
|
+
bgOpacity: n.bgOpacity ?? null,
|
|
30
|
+
rotation: n.rotation || 0, labelRotation: n.labelRotation || 0,
|
|
31
|
+
imageSrc: n.imageSrc || null, groupId: n.groupId || null,
|
|
32
|
+
nodeLink: n.nodeLink || null, locked: n.locked || false,
|
|
33
|
+
x: positions[n.id]?.x ?? n.x, y: positions[n.id]?.y ?? n.y,
|
|
34
|
+
}));
|
|
35
|
+
const edges = st.edges.get().map((e) => ({
|
|
36
|
+
id: e.id, from: e.from, to: e.to,
|
|
37
|
+
label: e.label || '', arrowDir: e.arrowDir || 'to',
|
|
38
|
+
dashes: e.dashes || false, fontSize: e.fontSize || null,
|
|
39
|
+
labelRotation: e.labelRotation || 0,
|
|
40
|
+
fromPort: e.fromPort || null, toPort: e.toPort || null,
|
|
41
|
+
edgeColor: e.edgeColor || null, edgeWidth: e.edgeWidth || null,
|
|
42
|
+
edgeLocked: e.edgeLocked || false,
|
|
43
|
+
evidence: Array.isArray(e.evidence) ? e.evidence : null,
|
|
44
|
+
}));
|
|
45
|
+
return {
|
|
46
|
+
nodes,
|
|
47
|
+
edges,
|
|
48
|
+
canonicalOrder: [...st.canonicalOrder],
|
|
49
|
+
edgesStraight: st.edgesStraight,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Public API ─────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Call BEFORE any mutating operation to record the current state for undo.
|
|
57
|
+
* Clears the redo stack (new action invalidates any undone future).
|
|
58
|
+
*/
|
|
59
|
+
export function pushSnapshot() {
|
|
60
|
+
const snap = captureState();
|
|
61
|
+
if (!snap) return;
|
|
62
|
+
_undoStack.push(snap);
|
|
63
|
+
if (_undoStack.length > MAX_HISTORY) _undoStack.shift();
|
|
64
|
+
_redoStack = [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Reset both stacks — call whenever a new diagram is opened. */
|
|
68
|
+
export function resetHistory() {
|
|
69
|
+
_undoStack = [];
|
|
70
|
+
_redoStack = [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Undo the last action. No-op if stack is empty. */
|
|
74
|
+
export function undo() {
|
|
75
|
+
if (!_undoStack.length) return;
|
|
76
|
+
const before = _undoStack.pop();
|
|
77
|
+
const current = captureState();
|
|
78
|
+
if (current) _redoStack.push(current);
|
|
79
|
+
_restoreState(before);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Redo the last undone action. No-op if stack is empty. */
|
|
83
|
+
export function redo() {
|
|
84
|
+
if (!_redoStack.length) return;
|
|
85
|
+
const after = _redoStack.pop();
|
|
86
|
+
const current = captureState();
|
|
87
|
+
if (current) _undoStack.push(current);
|
|
88
|
+
_restoreState(after);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── State restore ──────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
function _restoreState(snapshot) {
|
|
94
|
+
if (!st.network || !st.nodes || !st.edges || !snapshot) return;
|
|
95
|
+
|
|
96
|
+
const pos = st.network.getViewPosition();
|
|
97
|
+
const scale = st.network.getScale();
|
|
98
|
+
|
|
99
|
+
// Clear edges first: the edge-remove listener detects orphaned anchors and
|
|
100
|
+
// removes them from st.nodes, which avoids cascade issues when clearing nodes.
|
|
101
|
+
st.edges.clear();
|
|
102
|
+
st.nodes.clear();
|
|
103
|
+
|
|
104
|
+
// Restore edge-straight flag + toolbar button
|
|
105
|
+
if (snapshot.edgesStraight !== undefined) {
|
|
106
|
+
st.edgesStraight = snapshot.edgesStraight;
|
|
107
|
+
document.getElementById('btnEdgeStraight').classList.toggle('tool-active', st.edgesStraight);
|
|
108
|
+
}
|
|
109
|
+
const edgeSmooth = st.edgesStraight ? { enabled: false } : { type: 'continuous' };
|
|
110
|
+
|
|
111
|
+
st.nodes.add(snapshot.nodes.map((n) => ({
|
|
112
|
+
...n,
|
|
113
|
+
...visNodeProps(
|
|
114
|
+
n.shapeType || 'box', n.colorKey || 'c-gray',
|
|
115
|
+
n.nodeWidth, n.nodeHeight, n.fontSize, n.textAlign, n.textValign,
|
|
116
|
+
),
|
|
117
|
+
...(n.locked ? { fixed: { x: true, y: true }, draggable: false } : {}),
|
|
118
|
+
})));
|
|
119
|
+
|
|
120
|
+
st.edges.add(snapshot.edges.map((e) => {
|
|
121
|
+
const toNode = snapshot.nodes.find((n) => n.id === e.to);
|
|
122
|
+
const isAnchor = toNode && toNode.shapeType === 'anchor';
|
|
123
|
+
const edgeObj = {
|
|
124
|
+
...e,
|
|
125
|
+
...visEdgeProps(e.arrowDir ?? 'to', e.dashes ?? false),
|
|
126
|
+
smooth: isAnchor ? { enabled: false } : edgeSmooth,
|
|
127
|
+
...(e.edgeColor ? { color: { color: e.edgeColor, highlight: '#f97316', hover: '#f97316' } } : {}),
|
|
128
|
+
...(e.edgeWidth ? { width: e.edgeWidth } : {}),
|
|
129
|
+
...(e.fontSize || e.labelRotation ? {
|
|
130
|
+
font: {
|
|
131
|
+
size: e.fontSize || 11,
|
|
132
|
+
align: 'middle',
|
|
133
|
+
color: (e.labelRotation && Math.abs(e.labelRotation) > 0.001) ? 'rgba(0,0,0,0)' : '#6b7280',
|
|
134
|
+
},
|
|
135
|
+
} : {}),
|
|
136
|
+
};
|
|
137
|
+
if (e.fromPort || e.toPort) {
|
|
138
|
+
edgeObj.color = { color: 'rgba(0,0,0,0)', highlight: 'rgba(0,0,0,0)', hover: 'rgba(0,0,0,0)' };
|
|
139
|
+
edgeObj.arrows = { to: { enabled: false }, from: { enabled: false } };
|
|
140
|
+
}
|
|
141
|
+
return edgeObj;
|
|
142
|
+
}));
|
|
143
|
+
|
|
144
|
+
// Override the canonical order that the add-listener built in insertion order
|
|
145
|
+
st.canonicalOrder = [...snapshot.canonicalOrder];
|
|
146
|
+
|
|
147
|
+
// Force all nodes to re-render (custom shapes cache ctx closures)
|
|
148
|
+
Object.keys(st.network.body.nodes).forEach((id) => {
|
|
149
|
+
const bn = st.network.body.nodes[id];
|
|
150
|
+
if (bn) bn.refreshNeeded = true;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Restore viewport without animation
|
|
154
|
+
st.network.moveTo({ position: pos, scale, animation: false });
|
|
155
|
+
markDirty();
|
|
156
|
+
st.network.redraw();
|
|
157
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// ── Image filename modal ───────────────────────────────────────────────────────
|
|
2
|
+
// Returns a Promise<string|null>:
|
|
3
|
+
// string (possibly empty) → user confirmed (empty = use auto name)
|
|
4
|
+
// null → user cancelled
|
|
5
|
+
|
|
6
|
+
const IMAGE_NAME_RE = /^[a-z0-9_-]*$/i;
|
|
7
|
+
|
|
8
|
+
export function promptImageName() {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
const modal = document.getElementById('imageNameModal');
|
|
11
|
+
const input = document.getElementById('imageNameInput');
|
|
12
|
+
const error = document.getElementById('imageNameError');
|
|
13
|
+
const confirm = document.getElementById('imageNameConfirm');
|
|
14
|
+
const cancel = document.getElementById('imageNameCancel');
|
|
15
|
+
|
|
16
|
+
input.value = '';
|
|
17
|
+
error.classList.add('hidden');
|
|
18
|
+
modal.style.display = 'flex';
|
|
19
|
+
setTimeout(() => input.focus(), 50);
|
|
20
|
+
|
|
21
|
+
function validate() {
|
|
22
|
+
const val = input.value.trim();
|
|
23
|
+
const ok = val === '' || IMAGE_NAME_RE.test(val);
|
|
24
|
+
error.classList.toggle('hidden', ok);
|
|
25
|
+
return ok;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function close(name) {
|
|
29
|
+
modal.style.display = 'none';
|
|
30
|
+
confirm.removeEventListener('click', onConfirm);
|
|
31
|
+
cancel.removeEventListener('click', onCancel);
|
|
32
|
+
input.removeEventListener('input', validate);
|
|
33
|
+
input.removeEventListener('keydown', onKey);
|
|
34
|
+
resolve(name);
|
|
35
|
+
}
|
|
36
|
+
function onConfirm() { if (validate()) close(input.value.trim()); }
|
|
37
|
+
function onCancel() { close(null); }
|
|
38
|
+
function onKey(e) {
|
|
39
|
+
if (e.key === 'Enter') { e.preventDefault(); if (validate()) close(input.value.trim()); }
|
|
40
|
+
if (e.key === 'Escape') { e.preventDefault(); close(null); }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
confirm.addEventListener('click', onConfirm);
|
|
44
|
+
cancel.addEventListener('click', onCancel);
|
|
45
|
+
input.addEventListener('input', validate);
|
|
46
|
+
input.addEventListener('keydown', onKey);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// ── Image upload helper ───────────────────────────────────────────────────────
|
|
2
|
+
// Converts a File or Blob to base64 and uploads it via POST /api/images/upload.
|
|
3
|
+
// Returns the absolute URL path usable in an <img> or ctx.drawImage(), e.g. "/images/foo.png".
|
|
4
|
+
|
|
5
|
+
async function toBase64(blob) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const reader = new FileReader();
|
|
8
|
+
reader.onload = () => resolve(reader.result);
|
|
9
|
+
reader.onerror = reject;
|
|
10
|
+
reader.readAsDataURL(blob);
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function uploadImageFile(file, name = '') {
|
|
15
|
+
const ext = (file.name.split('.').pop() || 'png').toLowerCase();
|
|
16
|
+
const base64 = await toBase64(file);
|
|
17
|
+
return _upload(base64, ext, name);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function uploadImageBlob(blob, ext = 'png', name = '') {
|
|
21
|
+
const base64 = await toBase64(blob);
|
|
22
|
+
return _upload(base64, ext, name);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function _upload(base64, ext, name = '') {
|
|
26
|
+
const body = { data: base64, ext };
|
|
27
|
+
if (name) body.name = name;
|
|
28
|
+
const res = await fetch('/api/images/upload', {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: { 'Content-Type': 'application/json' },
|
|
31
|
+
body: JSON.stringify(body),
|
|
32
|
+
});
|
|
33
|
+
if (!res.ok) throw new Error('Upload failed');
|
|
34
|
+
const { filename } = await res.json();
|
|
35
|
+
return `/images/${filename}`;
|
|
36
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// ── Label editor ──────────────────────────────────────────────────────────────
|
|
2
|
+
// Floating textarea for in-place editing of node and edge labels.
|
|
3
|
+
|
|
4
|
+
import { st, markDirty } from './state.js';
|
|
5
|
+
import { pushSnapshot } from './history.js';
|
|
6
|
+
|
|
7
|
+
export function autoResizeTextarea(ta) {
|
|
8
|
+
ta.style.height = 'auto';
|
|
9
|
+
ta.style.height = ta.scrollHeight + 'px';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function startLabelEdit() {
|
|
13
|
+
if (!st.selectedNodeIds.length || !st.network) return;
|
|
14
|
+
const nodeId = st.selectedNodeIds[0];
|
|
15
|
+
const n = st.nodes.get(nodeId);
|
|
16
|
+
if (!n) return;
|
|
17
|
+
|
|
18
|
+
st.editingNodeId = nodeId;
|
|
19
|
+
st.editingEdgeId = null;
|
|
20
|
+
|
|
21
|
+
const pos = st.network.getPositions([nodeId])[nodeId] || { x: 0, y: 0 };
|
|
22
|
+
const dom = st.network.canvasToDOM(pos);
|
|
23
|
+
const bn = st.network.body.nodes[nodeId];
|
|
24
|
+
const scale = st.network.getScale();
|
|
25
|
+
// Use the node's actual rendered canvas width scaled to DOM pixels.
|
|
26
|
+
// bn.shape.width is set by shape.resize() on every draw — correct for all shapes.
|
|
27
|
+
const canvasW = (bn && bn.shape && bn.shape.width) || (n.nodeWidth || 150);
|
|
28
|
+
const domW = Math.max(120, Math.round(canvasW * scale));
|
|
29
|
+
const ta = document.getElementById('labelInput');
|
|
30
|
+
ta.value = n.label || '';
|
|
31
|
+
ta.style.left = dom.x - domW / 2 + 'px';
|
|
32
|
+
ta.style.top = dom.y - 30 + 'px';
|
|
33
|
+
ta.style.width = domW + 'px';
|
|
34
|
+
ta.classList.remove('hidden');
|
|
35
|
+
autoResizeTextarea(ta);
|
|
36
|
+
ta.focus();
|
|
37
|
+
ta.select();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function startEdgeLabelEdit() {
|
|
41
|
+
if (!st.selectedEdgeIds.length || !st.network) return;
|
|
42
|
+
const edgeId = st.selectedEdgeIds[0];
|
|
43
|
+
const e = st.edges.get(edgeId);
|
|
44
|
+
if (!e) return;
|
|
45
|
+
|
|
46
|
+
st.editingEdgeId = edgeId;
|
|
47
|
+
st.editingNodeId = null;
|
|
48
|
+
|
|
49
|
+
const recorded = st.edgeLabelCanvasPos && st.edgeLabelCanvasPos[edgeId];
|
|
50
|
+
let mid;
|
|
51
|
+
if (recorded) {
|
|
52
|
+
mid = recorded;
|
|
53
|
+
} else {
|
|
54
|
+
const positions = st.network.getPositions([e.from, e.to]);
|
|
55
|
+
const fp = positions[e.from] || { x: 0, y: 0 };
|
|
56
|
+
const tp = positions[e.to] || { x: 0, y: 0 };
|
|
57
|
+
mid = st.network.canvasToDOM({ x: (fp.x + tp.x) / 2, y: (fp.y + tp.y) / 2 });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const ta = document.getElementById('labelInput');
|
|
61
|
+
ta.value = e.label || '';
|
|
62
|
+
ta.style.left = mid.x - 60 + 'px';
|
|
63
|
+
ta.style.top = mid.y - 14 + 'px';
|
|
64
|
+
ta.style.width = '120px';
|
|
65
|
+
ta.classList.remove('hidden');
|
|
66
|
+
autoResizeTextarea(ta);
|
|
67
|
+
ta.focus();
|
|
68
|
+
ta.select();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function commitLabelEdit() {
|
|
72
|
+
const ta = document.getElementById('labelInput');
|
|
73
|
+
if (st.editingNodeId) {
|
|
74
|
+
const n = st.nodes.get(st.editingNodeId);
|
|
75
|
+
if (n && ta.value !== (n.label || '')) {
|
|
76
|
+
pushSnapshot();
|
|
77
|
+
st.nodes.update({ id: st.editingNodeId, label: ta.value });
|
|
78
|
+
markDirty();
|
|
79
|
+
}
|
|
80
|
+
} else if (st.editingEdgeId) {
|
|
81
|
+
const e = st.edges.get(st.editingEdgeId);
|
|
82
|
+
if (e && ta.value !== (e.label || '')) {
|
|
83
|
+
pushSnapshot();
|
|
84
|
+
const update = { id: st.editingEdgeId, label: ta.value };
|
|
85
|
+
// Always hide vis-network's native edge label; drawEdgeLabels() in
|
|
86
|
+
// afterDrawing is the single rendering path for edge labels.
|
|
87
|
+
update.font = { size: e.fontSize || 11, align: 'middle', color: 'rgba(0,0,0,0)' };
|
|
88
|
+
st.edges.update(update);
|
|
89
|
+
markDirty();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
st.editingNodeId = null;
|
|
93
|
+
st.editingEdgeId = null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function hideLabelInput() {
|
|
97
|
+
document.getElementById('labelInput').classList.add('hidden');
|
|
98
|
+
st.editingNodeId = null;
|
|
99
|
+
st.editingEdgeId = null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Wire labelInput DOM events ────────────────────────────────────────────────
|
|
103
|
+
const labelInput = document.getElementById('labelInput');
|
|
104
|
+
|
|
105
|
+
labelInput.addEventListener('keydown', (e) => {
|
|
106
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
107
|
+
commitLabelEdit();
|
|
108
|
+
hideLabelInput();
|
|
109
|
+
e.preventDefault();
|
|
110
|
+
} else if (e.key === 'Escape') {
|
|
111
|
+
hideLabelInput();
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
labelInput.addEventListener('input', (e) => autoResizeTextarea(e.target));
|
|
115
|
+
labelInput.addEventListener('blur', () => { commitLabelEdit(); hideLabelInput(); });
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// ── Node link panel ───────────────────────────────────────────────────────────
|
|
2
|
+
// Lets the user attach a URL or diagram link to a node.
|
|
3
|
+
|
|
4
|
+
import { st, markDirty } from './state.js';
|
|
5
|
+
import { pushSnapshot } from './history.js';
|
|
6
|
+
import { openDiagram } from './persistence.js';
|
|
7
|
+
import { showToast } from './toast.js';
|
|
8
|
+
import { t } from './t.js';
|
|
9
|
+
|
|
10
|
+
let _panelNodeId = null;
|
|
11
|
+
|
|
12
|
+
export function showLinkPanel(nodeId) {
|
|
13
|
+
_panelNodeId = nodeId;
|
|
14
|
+
const n = st.nodes.get(nodeId);
|
|
15
|
+
const link = (n && n.nodeLink) || null;
|
|
16
|
+
|
|
17
|
+
const panel = document.getElementById('linkPanel');
|
|
18
|
+
panel.classList.remove('hidden');
|
|
19
|
+
|
|
20
|
+
// Populate form from existing link
|
|
21
|
+
const typeUrl = document.getElementById('linkTypeUrl');
|
|
22
|
+
const typeDiagram = document.getElementById('linkTypeDiagram');
|
|
23
|
+
const typeNew = document.getElementById('linkTypeNew');
|
|
24
|
+
const urlInput = document.getElementById('linkUrlInput');
|
|
25
|
+
const diagSelect = document.getElementById('linkDiagramSelect');
|
|
26
|
+
const newNameInput = document.getElementById('linkNewName');
|
|
27
|
+
|
|
28
|
+
// Populate diagram list
|
|
29
|
+
diagSelect.innerHTML = '';
|
|
30
|
+
st.diagrams.forEach((d) => {
|
|
31
|
+
if (d.id === st.currentDiagramId) return; // skip current
|
|
32
|
+
const opt = document.createElement('option');
|
|
33
|
+
opt.value = d.id;
|
|
34
|
+
opt.textContent = d.title;
|
|
35
|
+
diagSelect.appendChild(opt);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (link && link.type === 'url') {
|
|
39
|
+
typeUrl.checked = true;
|
|
40
|
+
urlInput.value = link.value;
|
|
41
|
+
newNameInput.value = '';
|
|
42
|
+
} else if (link && link.type === 'diagram') {
|
|
43
|
+
typeDiagram.checked = true;
|
|
44
|
+
diagSelect.value = link.value;
|
|
45
|
+
urlInput.value = '';
|
|
46
|
+
newNameInput.value = '';
|
|
47
|
+
} else {
|
|
48
|
+
typeUrl.checked = true;
|
|
49
|
+
urlInput.value = '';
|
|
50
|
+
newNameInput.value = '';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_syncLinkPanelVisibility();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function hideLinkPanel() {
|
|
57
|
+
document.getElementById('linkPanel').classList.add('hidden');
|
|
58
|
+
_panelNodeId = null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function _syncLinkPanelVisibility() {
|
|
62
|
+
const typeUrl = document.getElementById('linkTypeUrl').checked;
|
|
63
|
+
const typeDiagram = document.getElementById('linkTypeDiagram').checked;
|
|
64
|
+
const typeNew = document.getElementById('linkTypeNew').checked;
|
|
65
|
+
document.getElementById('linkUrlRow').classList.toggle('hidden', !typeUrl);
|
|
66
|
+
document.getElementById('linkDiagramRow').classList.toggle('hidden', !typeDiagram);
|
|
67
|
+
document.getElementById('linkNewRow').classList.toggle('hidden', !typeNew);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function saveLinkPanel() {
|
|
71
|
+
if (_panelNodeId === null) return;
|
|
72
|
+
const typeUrl = document.getElementById('linkTypeUrl').checked;
|
|
73
|
+
const typeDiagram = document.getElementById('linkTypeDiagram').checked;
|
|
74
|
+
const typeNew = document.getElementById('linkTypeNew').checked;
|
|
75
|
+
|
|
76
|
+
if (typeUrl) {
|
|
77
|
+
const value = document.getElementById('linkUrlInput').value.trim();
|
|
78
|
+
pushSnapshot();
|
|
79
|
+
st.nodes.update({ id: _panelNodeId, nodeLink: value ? { type: 'url', value } : null });
|
|
80
|
+
markDirty();
|
|
81
|
+
hideLinkPanel();
|
|
82
|
+
} else if (typeDiagram) {
|
|
83
|
+
const value = document.getElementById('linkDiagramSelect').value;
|
|
84
|
+
if (!value) return;
|
|
85
|
+
pushSnapshot();
|
|
86
|
+
st.nodes.update({ id: _panelNodeId, nodeLink: { type: 'diagram', value } });
|
|
87
|
+
markDirty();
|
|
88
|
+
hideLinkPanel();
|
|
89
|
+
} else if (typeNew) {
|
|
90
|
+
const name = document.getElementById('linkNewName').value.trim();
|
|
91
|
+
if (!name) return;
|
|
92
|
+
_createAndLinkDiagram(name);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function _createAndLinkDiagram(title) {
|
|
97
|
+
const id = 'd' + Date.now();
|
|
98
|
+
await fetch(`/api/diagrams/${id}`, {
|
|
99
|
+
method: 'PUT',
|
|
100
|
+
headers: { 'Content-Type': 'application/json' },
|
|
101
|
+
body: JSON.stringify({ title, nodes: [], edges: [] }),
|
|
102
|
+
});
|
|
103
|
+
const res = await fetch('/api/diagrams');
|
|
104
|
+
st.diagrams = await res.json();
|
|
105
|
+
pushSnapshot();
|
|
106
|
+
st.nodes.update({ id: _panelNodeId, nodeLink: { type: 'diagram', value: id } });
|
|
107
|
+
markDirty();
|
|
108
|
+
hideLinkPanel();
|
|
109
|
+
showToast(t('diagram.toast.diagram_linked').replace('{title}', title));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function removeLinkPanel() {
|
|
113
|
+
if (_panelNodeId === null) return;
|
|
114
|
+
pushSnapshot();
|
|
115
|
+
st.nodes.update({ id: _panelNodeId, nodeLink: null });
|
|
116
|
+
markDirty();
|
|
117
|
+
hideLinkPanel();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Navigation ────────────────────────────────────────────────────────────────
|
|
121
|
+
// Called on click (not drag). Returns true if navigation happened.
|
|
122
|
+
|
|
123
|
+
export function navigateNodeLink(nodeId) {
|
|
124
|
+
const n = st.nodes.get(nodeId);
|
|
125
|
+
const link = n && n.nodeLink;
|
|
126
|
+
if (!link) return false;
|
|
127
|
+
if (link.type === 'url') {
|
|
128
|
+
window.open(link.value, '_blank', 'noopener');
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
if (link.type === 'diagram') {
|
|
132
|
+
openDiagram(link.value);
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Wire radio buttons ────────────────────────────────────────────────────────
|
|
139
|
+
['linkTypeUrl', 'linkTypeDiagram', 'linkTypeNew'].forEach((id) => {
|
|
140
|
+
document.getElementById(id).addEventListener('change', _syncLinkPanelVisibility);
|
|
141
|
+
});
|
|
142
|
+
document.getElementById('btnLinkSave').addEventListener('click', saveLinkPanel);
|
|
143
|
+
document.getElementById('btnLinkRemove').addEventListener('click', removeLinkPanel);
|
|
144
|
+
document.getElementById('btnLinkCancel').addEventListener('click', hideLinkPanel);
|