living-documentation 3.6.0 → 3.8.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.
Potentially problematic release.
This version of living-documentation might be problematic. Click here for more details.
- package/dist/src/frontend/diagram/clipboard.js +70 -1
- package/dist/src/frontend/diagram/constants.js +8 -5
- package/dist/src/frontend/diagram/grid.js +19 -2
- package/dist/src/frontend/diagram/image-upload.js +34 -0
- package/dist/src/frontend/diagram/main.js +58 -5
- package/dist/src/frontend/diagram/network.js +55 -11
- package/dist/src/frontend/diagram/node-panel.js +123 -9
- package/dist/src/frontend/diagram/node-rendering.js +347 -61
- package/dist/src/frontend/diagram/persistence.js +2 -0
- package/dist/src/frontend/diagram/selection-overlay.js +161 -54
- package/dist/src/frontend/diagram/state.js +4 -0
- package/dist/src/frontend/diagram/toast.js +21 -0
- package/dist/src/frontend/diagram.html +174 -1
- package/package.json +1 -1
|
@@ -1,36 +1,44 @@
|
|
|
1
|
-
// ── Selection / resize overlay
|
|
2
|
-
// Dashed selection box
|
|
1
|
+
// ── Selection / resize / rotate overlay ───────────────────────────────────────
|
|
2
|
+
// Dashed selection box, corner resize handles, and top-centre rotation handle.
|
|
3
3
|
|
|
4
4
|
import { st, markDirty } from './state.js';
|
|
5
|
-
import { visNodeProps } from './node-rendering.js';
|
|
5
|
+
import { visNodeProps, SHAPE_DEFAULTS } from './node-rendering.js';
|
|
6
6
|
|
|
7
|
+
// ── Bounding box helper (works for all shapes including ctxRenderer) ──────────
|
|
8
|
+
function nodeBounds(id) {
|
|
9
|
+
const n = st.nodes.get(id);
|
|
10
|
+
const bodyNode = st.network.body.nodes[id];
|
|
11
|
+
if (!bodyNode) return null;
|
|
12
|
+
const cx = bodyNode.x, cy = bodyNode.y;
|
|
13
|
+
const shape = n && n.shapeType || 'box';
|
|
14
|
+
const defaults = SHAPE_DEFAULTS[shape] || [60, 28];
|
|
15
|
+
const W = (n && n.nodeWidth) || defaults[0];
|
|
16
|
+
const H = (n && n.nodeHeight) || defaults[1];
|
|
17
|
+
// Use the axis-aligned envelope of the (possibly rotated) bounding box.
|
|
18
|
+
const rot = (n && n.rotation) || 0;
|
|
19
|
+
if (rot === 0) {
|
|
20
|
+
return { minX: cx - W / 2, minY: cy - H / 2, maxX: cx + W / 2, maxY: cy + H / 2 };
|
|
21
|
+
}
|
|
22
|
+
const cos = Math.abs(Math.cos(rot));
|
|
23
|
+
const sin = Math.abs(Math.sin(rot));
|
|
24
|
+
const hw = (W * cos + H * sin) / 2;
|
|
25
|
+
const hh = (W * sin + H * cos) / 2;
|
|
26
|
+
// Actor: head extends above cy - H/2 when unrotated
|
|
27
|
+
const headExtra = shape === 'actor' ? (28 * (H / 52) - H / 2) : 0;
|
|
28
|
+
return { minX: cx - hw, minY: cy - hh - headExtra, maxX: cx + hw, maxY: cy + hh };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── Overlay position ──────────────────────────────────────────────────────────
|
|
7
32
|
export function updateSelectionOverlay() {
|
|
8
33
|
if (!st.network || !st.selectedNodeIds.length) { hideSelectionOverlay(); return; }
|
|
9
34
|
|
|
10
35
|
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
11
36
|
for (const id of st.selectedNodeIds) {
|
|
12
37
|
try {
|
|
13
|
-
const
|
|
14
|
-
if (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const bodyNode = st.network.body.nodes[id];
|
|
18
|
-
if (!bodyNode) continue;
|
|
19
|
-
const cx = bodyNode.x, cy = bodyNode.y;
|
|
20
|
-
const W = n.nodeWidth || 30;
|
|
21
|
-
const H = n.nodeHeight || 52;
|
|
22
|
-
const sy = H / 52;
|
|
23
|
-
minX = Math.min(minX, cx - W / 2);
|
|
24
|
-
minY = Math.min(minY, cy - 28 * sy); // head top
|
|
25
|
-
maxX = Math.max(maxX, cx + W / 2);
|
|
26
|
-
maxY = Math.max(maxY, cy + 24 * sy); // legs bottom
|
|
27
|
-
} else {
|
|
28
|
-
const bb = st.network.getBoundingBox(id);
|
|
29
|
-
minX = Math.min(minX, bb.left);
|
|
30
|
-
minY = Math.min(minY, bb.top);
|
|
31
|
-
maxX = Math.max(maxX, bb.right);
|
|
32
|
-
maxY = Math.max(maxY, bb.bottom);
|
|
33
|
-
}
|
|
38
|
+
const b = nodeBounds(id);
|
|
39
|
+
if (!b) continue;
|
|
40
|
+
minX = Math.min(minX, b.minX); minY = Math.min(minY, b.minY);
|
|
41
|
+
maxX = Math.max(maxX, b.maxX); maxY = Math.max(maxY, b.maxY);
|
|
34
42
|
} catch (_) { /* node still being created */ }
|
|
35
43
|
}
|
|
36
44
|
if (minX === Infinity) { hideSelectionOverlay(); return; }
|
|
@@ -45,38 +53,41 @@ export function updateSelectionOverlay() {
|
|
|
45
53
|
ov.style.width = br.x - tl.x + PAD * 2 + 'px';
|
|
46
54
|
ov.style.height = br.y - tl.y + PAD * 2 + 'px';
|
|
47
55
|
|
|
56
|
+
// Position rotation handle: top-centre of the overlay, 28px above it.
|
|
57
|
+
const rh = document.getElementById('rh-rotate');
|
|
58
|
+
rh.style.left = (br.x - tl.x) / 2 + PAD - 8 + 'px';
|
|
59
|
+
rh.style.top = '-28px';
|
|
60
|
+
|
|
61
|
+
// Position label rotation handle: top-centre offset left by 24px to avoid overlap.
|
|
62
|
+
const lrh = document.getElementById('rh-label-rotate');
|
|
63
|
+
lrh.style.left = (br.x - tl.x) / 2 + PAD - 8 - 24 + 'px';
|
|
64
|
+
lrh.style.top = '-28px';
|
|
48
65
|
}
|
|
49
66
|
|
|
50
67
|
export function hideSelectionOverlay() {
|
|
51
68
|
document.getElementById('selectionOverlay').style.display = 'none';
|
|
52
69
|
}
|
|
53
70
|
|
|
71
|
+
// ── Resize ────────────────────────────────────────────────────────────────────
|
|
54
72
|
function onResizeStart(e, corner) {
|
|
55
73
|
if (!st.selectedNodeIds.length || !st.network) return;
|
|
56
74
|
e.preventDefault();
|
|
57
75
|
e.stopPropagation();
|
|
58
76
|
|
|
59
77
|
const startBBs = st.selectedNodeIds.map((id) => {
|
|
60
|
-
const n
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
initH = n.nodeHeight || 52;
|
|
67
|
-
} else {
|
|
68
|
-
const bb = st.network.getBoundingBox(id);
|
|
69
|
-
initW = n.nodeWidth || Math.round(bb.right - bb.left);
|
|
70
|
-
initH = n.nodeHeight || Math.round(bb.bottom - bb.top);
|
|
71
|
-
}
|
|
78
|
+
const n = st.nodes.get(id);
|
|
79
|
+
const b = nodeBounds(id);
|
|
80
|
+
const shape = (n && n.shapeType) || 'box';
|
|
81
|
+
const defaults = SHAPE_DEFAULTS[shape] || [60, 28];
|
|
82
|
+
const initW = (n && n.nodeWidth) || (b ? Math.round(b.maxX - b.minX) : defaults[0]);
|
|
83
|
+
const initH = (n && n.nodeHeight) || (b ? Math.round(b.maxY - b.minY) : defaults[1]);
|
|
72
84
|
return { id, node: n, initW, initH };
|
|
73
85
|
});
|
|
74
86
|
|
|
75
|
-
const initBoxW = startBBs.reduce((
|
|
76
|
-
const initBoxH = startBBs.reduce((
|
|
87
|
+
const initBoxW = startBBs.reduce((m, b) => Math.max(m, b.initW), 0);
|
|
88
|
+
const initBoxH = startBBs.reduce((m, b) => Math.max(m, b.initH), 0);
|
|
77
89
|
|
|
78
90
|
st.resizeDrag = { corner, startMouse: { x: e.clientX, y: e.clientY }, startBBs, initBoxW, initBoxH };
|
|
79
|
-
|
|
80
91
|
st.selectedNodeIds.forEach((id) => st.nodes.update({ id, fixed: true }));
|
|
81
92
|
document.getElementById('vis-canvas').style.pointerEvents = 'none';
|
|
82
93
|
document.addEventListener('mousemove', onResizeDrag);
|
|
@@ -88,9 +99,8 @@ function onResizeDrag(e) {
|
|
|
88
99
|
const scale = st.network.getScale();
|
|
89
100
|
const cdx = (e.clientX - st.resizeDrag.startMouse.x) / scale;
|
|
90
101
|
const cdy = (e.clientY - st.resizeDrag.startMouse.y) / scale;
|
|
91
|
-
const MIN =
|
|
102
|
+
const MIN = 20;
|
|
92
103
|
const c = st.resizeDrag.corner;
|
|
93
|
-
|
|
94
104
|
const updatedIds = [];
|
|
95
105
|
|
|
96
106
|
if (st.resizeDrag.startBBs.length === 1) {
|
|
@@ -102,7 +112,7 @@ function onResizeDrag(e) {
|
|
|
102
112
|
if (c === 'tl') { nW = initW - cdx; nH = initH - cdy; }
|
|
103
113
|
nW = Math.max(MIN, Math.round(nW));
|
|
104
114
|
nH = Math.max(MIN, Math.round(nH));
|
|
105
|
-
st.nodes.update({ id, nodeWidth: nW, nodeHeight: nH, ...visNodeProps(node.shapeType || 'box', node.colorKey || 'c-gray', nW, nH, node.fontSize, node.textAlign, node.textValign
|
|
115
|
+
st.nodes.update({ id, nodeWidth: nW, nodeHeight: nH, ...visNodeProps(node.shapeType || 'box', node.colorKey || 'c-gray', nW, nH, node.fontSize, node.textAlign, node.textValign) });
|
|
106
116
|
updatedIds.push(id);
|
|
107
117
|
} else {
|
|
108
118
|
const { initBoxW, initBoxH } = st.resizeDrag;
|
|
@@ -111,25 +121,17 @@ function onResizeDrag(e) {
|
|
|
111
121
|
if (c === 'bl') { sx = (initBoxW - cdx) / initBoxW; sy = (initBoxH + cdy) / initBoxH; }
|
|
112
122
|
if (c === 'tr') { sx = (initBoxW + cdx) / initBoxW; sy = (initBoxH - cdy) / initBoxH; }
|
|
113
123
|
if (c === 'tl') { sx = (initBoxW - cdx) / initBoxW; sy = (initBoxH - cdy) / initBoxH; }
|
|
114
|
-
sx = Math.max(0.1, sx);
|
|
115
|
-
sy = Math.max(0.1, sy);
|
|
124
|
+
sx = Math.max(0.1, sx); sy = Math.max(0.1, sy);
|
|
116
125
|
for (const { id, node, initW, initH } of st.resizeDrag.startBBs) {
|
|
117
126
|
const nW = Math.max(MIN, Math.round(initW * sx));
|
|
118
127
|
const nH = Math.max(MIN, Math.round(initH * sy));
|
|
119
|
-
st.nodes.update({ id, nodeWidth: nW, nodeHeight: nH, ...visNodeProps(node.shapeType || 'box', node.colorKey || 'c-gray', nW, nH, node.fontSize, node.textAlign, node.textValign
|
|
128
|
+
st.nodes.update({ id, nodeWidth: nW, nodeHeight: nH, ...visNodeProps(node.shapeType || 'box', node.colorKey || 'c-gray', nW, nH, node.fontSize, node.textAlign, node.textValign) });
|
|
120
129
|
updatedIds.push(id);
|
|
121
130
|
}
|
|
122
131
|
}
|
|
123
132
|
|
|
124
|
-
|
|
125
|
-
// when only widthConstraint or heightConstraint changes, because the check is based
|
|
126
|
-
// on label/font state, not constraints. Force the internal refresh flag and redraw.
|
|
127
|
-
updatedIds.forEach((id) => {
|
|
128
|
-
const bn = st.network.body.nodes[id];
|
|
129
|
-
if (bn) bn.refreshNeeded = true;
|
|
130
|
-
});
|
|
133
|
+
updatedIds.forEach((id) => { const bn = st.network.body.nodes[id]; if (bn) bn.refreshNeeded = true; });
|
|
131
134
|
st.network.redraw();
|
|
132
|
-
|
|
133
135
|
updateSelectionOverlay();
|
|
134
136
|
}
|
|
135
137
|
|
|
@@ -143,7 +145,112 @@ function onResizeEnd() {
|
|
|
143
145
|
markDirty();
|
|
144
146
|
}
|
|
145
147
|
|
|
146
|
-
// ──
|
|
148
|
+
// ── Rotation ──────────────────────────────────────────────────────────────────
|
|
149
|
+
function onRotateStart(e) {
|
|
150
|
+
if (!st.selectedNodeIds.length || !st.network) return;
|
|
151
|
+
e.preventDefault();
|
|
152
|
+
e.stopPropagation();
|
|
153
|
+
|
|
154
|
+
// Barycentre of the selection in canvas coordinates.
|
|
155
|
+
const positions = st.network.getPositions(st.selectedNodeIds);
|
|
156
|
+
const ids = st.selectedNodeIds;
|
|
157
|
+
const cx = ids.reduce((s, id) => s + (positions[id] ? positions[id].x : 0), 0) / ids.length;
|
|
158
|
+
const cy = ids.reduce((s, id) => s + (positions[id] ? positions[id].y : 0), 0) / ids.length;
|
|
159
|
+
|
|
160
|
+
const nodeAngles = ids.map((id) => {
|
|
161
|
+
const n = st.nodes.get(id);
|
|
162
|
+
const pos = positions[id] || { x: 0, y: 0 };
|
|
163
|
+
return {
|
|
164
|
+
id,
|
|
165
|
+
initRotation: (n && n.rotation) || 0,
|
|
166
|
+
// Position relative to barycentre at drag start
|
|
167
|
+
relX: pos.x - cx,
|
|
168
|
+
relY: pos.y - cy,
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Horizontal drag → rotation: right = clockwise, left = counter-clockwise.
|
|
173
|
+
// 1 px = 1 degree.
|
|
174
|
+
st.rotateDrag = { startX: e.clientX, nodeAngles, cx, cy };
|
|
175
|
+
document.getElementById('vis-canvas').style.pointerEvents = 'none';
|
|
176
|
+
document.addEventListener('mousemove', onRotateDrag);
|
|
177
|
+
document.addEventListener('mouseup', onRotateEnd);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function onRotateDrag(e) {
|
|
181
|
+
if (!st.rotateDrag || !st.network) return;
|
|
182
|
+
const { startX, nodeAngles, cx, cy } = st.rotateDrag;
|
|
183
|
+
const dx = e.clientX - startX;
|
|
184
|
+
const delta = dx * (Math.PI / 180); // 1 px = 1 degree
|
|
185
|
+
const cos = Math.cos(delta);
|
|
186
|
+
const sin = Math.sin(delta);
|
|
187
|
+
|
|
188
|
+
nodeAngles.forEach(({ id, initRotation, relX, relY }) => {
|
|
189
|
+
// Rotate the node's position around the barycentre
|
|
190
|
+
const newX = cx + relX * cos - relY * sin;
|
|
191
|
+
const newY = cy + relX * sin + relY * cos;
|
|
192
|
+
st.network.moveNode(id, newX, newY);
|
|
193
|
+
// Rotate the node's own orientation
|
|
194
|
+
st.nodes.update({ id, rotation: initRotation + delta });
|
|
195
|
+
const bn = st.network.body.nodes[id];
|
|
196
|
+
if (bn) bn.refreshNeeded = true;
|
|
197
|
+
});
|
|
198
|
+
st.network.redraw();
|
|
199
|
+
updateSelectionOverlay();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function onRotateEnd() {
|
|
203
|
+
if (!st.rotateDrag) return;
|
|
204
|
+
document.getElementById('vis-canvas').style.pointerEvents = '';
|
|
205
|
+
document.removeEventListener('mousemove', onRotateDrag);
|
|
206
|
+
document.removeEventListener('mouseup', onRotateEnd);
|
|
207
|
+
st.rotateDrag = null;
|
|
208
|
+
markDirty();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── Label rotation ────────────────────────────────────────────────────────────
|
|
212
|
+
function onLabelRotateStart(e) {
|
|
213
|
+
if (!st.selectedNodeIds.length || !st.network) return;
|
|
214
|
+
e.preventDefault();
|
|
215
|
+
e.stopPropagation();
|
|
216
|
+
|
|
217
|
+
const nodeAngles = st.selectedNodeIds.map((id) => {
|
|
218
|
+
const n = st.nodes.get(id);
|
|
219
|
+
return { id, initLabelRotation: (n && n.labelRotation) || 0 };
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
st.labelRotateDrag = { startX: e.clientX, nodeAngles };
|
|
223
|
+
document.getElementById('vis-canvas').style.pointerEvents = 'none';
|
|
224
|
+
document.addEventListener('mousemove', onLabelRotateDrag);
|
|
225
|
+
document.addEventListener('mouseup', onLabelRotateEnd);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function onLabelRotateDrag(e) {
|
|
229
|
+
if (!st.labelRotateDrag || !st.network) return;
|
|
230
|
+
const { startX, nodeAngles } = st.labelRotateDrag;
|
|
231
|
+
const dx = e.clientX - startX;
|
|
232
|
+
const delta = dx * (Math.PI / 180); // 1 px = 1 degree
|
|
233
|
+
|
|
234
|
+
nodeAngles.forEach(({ id, initLabelRotation }) => {
|
|
235
|
+
st.nodes.update({ id, labelRotation: initLabelRotation + delta });
|
|
236
|
+
const bn = st.network.body.nodes[id];
|
|
237
|
+
if (bn) bn.refreshNeeded = true;
|
|
238
|
+
});
|
|
239
|
+
st.network.redraw();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function onLabelRotateEnd() {
|
|
243
|
+
if (!st.labelRotateDrag) return;
|
|
244
|
+
document.getElementById('vis-canvas').style.pointerEvents = '';
|
|
245
|
+
document.removeEventListener('mousemove', onLabelRotateDrag);
|
|
246
|
+
document.removeEventListener('mouseup', onLabelRotateEnd);
|
|
247
|
+
st.labelRotateDrag = null;
|
|
248
|
+
markDirty();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── Wire handles ──────────────────────────────────────────────────────────────
|
|
147
252
|
['tl', 'tr', 'bl', 'br'].forEach((corner) => {
|
|
148
253
|
document.getElementById('rh-' + corner).addEventListener('mousedown', (e) => onResizeStart(e, corner));
|
|
149
254
|
});
|
|
255
|
+
document.getElementById('rh-rotate').addEventListener('mousedown', onRotateStart);
|
|
256
|
+
document.getElementById('rh-label-rotate').addEventListener('mousedown', onLabelRotateStart);
|
|
@@ -19,6 +19,10 @@ export const st = {
|
|
|
19
19
|
editingNodeId: null,
|
|
20
20
|
editingEdgeId: null,
|
|
21
21
|
resizeDrag: null,
|
|
22
|
+
rotateDrag: null, // { startX, nodeAngles: [{id, initRotation}] }
|
|
23
|
+
labelRotateDrag: null, // { startX, nodeAngles: [{id, initLabelRotation}] }
|
|
24
|
+
activeStamp: null, // 'color' | 'rotation' | 'fontSize' | null
|
|
25
|
+
stampTargetIds: [], // node IDs waiting to receive the stamped property
|
|
22
26
|
clipboard: null, // { nodes: [], edges: [] }
|
|
23
27
|
canonicalOrder: [], // user-defined z-order, immune to vis.js hover reordering
|
|
24
28
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// ── Toast notifications ───────────────────────────────────────────────────────
|
|
2
|
+
// Minimal sonner-style toasts, no dependencies.
|
|
3
|
+
|
|
4
|
+
export function showToast(message, type = 'success', duration = 3500) {
|
|
5
|
+
const container = document.getElementById('toastContainer');
|
|
6
|
+
const el = document.createElement('div');
|
|
7
|
+
el.className = 'ld-toast ld-toast--' + type;
|
|
8
|
+
el.textContent = message;
|
|
9
|
+
container.appendChild(el);
|
|
10
|
+
|
|
11
|
+
// Animate in on next frame
|
|
12
|
+
requestAnimationFrame(() => el.classList.add('ld-toast--visible'));
|
|
13
|
+
|
|
14
|
+
const hide = () => {
|
|
15
|
+
el.classList.remove('ld-toast--visible');
|
|
16
|
+
el.addEventListener('transitionend', () => el.remove(), { once: true });
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const timer = setTimeout(hide, duration);
|
|
20
|
+
el.addEventListener('click', () => { clearTimeout(timer); hide(); });
|
|
21
|
+
}
|
|
@@ -151,7 +151,7 @@
|
|
|
151
151
|
background: rgba(0, 0, 0, 0.78);
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
/* Resize / selection overlay */
|
|
154
|
+
/* Resize / rotation / selection overlay */
|
|
155
155
|
#selectionOverlay {
|
|
156
156
|
position: absolute;
|
|
157
157
|
display: none;
|
|
@@ -174,6 +174,83 @@
|
|
|
174
174
|
.dark .resize-handle {
|
|
175
175
|
background: #374151;
|
|
176
176
|
}
|
|
177
|
+
#rh-rotate {
|
|
178
|
+
position: absolute;
|
|
179
|
+
width: 16px;
|
|
180
|
+
height: 16px;
|
|
181
|
+
background: white;
|
|
182
|
+
border: 2px solid #f97316;
|
|
183
|
+
border-radius: 50%;
|
|
184
|
+
pointer-events: all;
|
|
185
|
+
z-index: 1;
|
|
186
|
+
cursor: grab;
|
|
187
|
+
display: flex;
|
|
188
|
+
align-items: center;
|
|
189
|
+
justify-content: center;
|
|
190
|
+
font-size: 10px;
|
|
191
|
+
color: #f97316;
|
|
192
|
+
}
|
|
193
|
+
#rh-rotate:active { cursor: grabbing; }
|
|
194
|
+
.dark #rh-rotate { background: #374151; }
|
|
195
|
+
#rh-label-rotate {
|
|
196
|
+
position: absolute;
|
|
197
|
+
width: 16px;
|
|
198
|
+
height: 16px;
|
|
199
|
+
background: white;
|
|
200
|
+
border: 2px solid #6366f1;
|
|
201
|
+
border-radius: 50%;
|
|
202
|
+
pointer-events: all;
|
|
203
|
+
z-index: 1;
|
|
204
|
+
cursor: grab;
|
|
205
|
+
display: flex;
|
|
206
|
+
align-items: center;
|
|
207
|
+
justify-content: center;
|
|
208
|
+
font-size: 10px;
|
|
209
|
+
color: #6366f1;
|
|
210
|
+
}
|
|
211
|
+
#rh-label-rotate:active { cursor: grabbing; }
|
|
212
|
+
.dark #rh-label-rotate { background: #374151; }
|
|
213
|
+
|
|
214
|
+
/* Toast notifications */
|
|
215
|
+
#toastContainer {
|
|
216
|
+
position: fixed;
|
|
217
|
+
bottom: 1.5rem;
|
|
218
|
+
right: 1.5rem;
|
|
219
|
+
display: flex;
|
|
220
|
+
flex-direction: column-reverse;
|
|
221
|
+
gap: 0.5rem;
|
|
222
|
+
z-index: 1000;
|
|
223
|
+
pointer-events: none;
|
|
224
|
+
}
|
|
225
|
+
.ld-toast {
|
|
226
|
+
pointer-events: all;
|
|
227
|
+
padding: 0.6rem 1rem;
|
|
228
|
+
border-radius: 0.5rem;
|
|
229
|
+
font-size: 0.8rem;
|
|
230
|
+
font-weight: 500;
|
|
231
|
+
box-shadow: 0 4px 16px rgba(0,0,0,0.18);
|
|
232
|
+
cursor: pointer;
|
|
233
|
+
opacity: 0;
|
|
234
|
+
transform: translateY(0.5rem);
|
|
235
|
+
transition: opacity 0.2s ease, transform 0.2s ease;
|
|
236
|
+
background: #1c1917;
|
|
237
|
+
color: #fafaf9;
|
|
238
|
+
border: 1px solid #292524;
|
|
239
|
+
}
|
|
240
|
+
.dark .ld-toast {
|
|
241
|
+
background: #fafaf9;
|
|
242
|
+
color: #1c1917;
|
|
243
|
+
border-color: #e7e5e4;
|
|
244
|
+
}
|
|
245
|
+
.ld-toast--error {
|
|
246
|
+
background: #7f1d1d;
|
|
247
|
+
color: #fef2f2;
|
|
248
|
+
border-color: #991b1b;
|
|
249
|
+
}
|
|
250
|
+
.ld-toast--visible {
|
|
251
|
+
opacity: 1;
|
|
252
|
+
transform: translateY(0);
|
|
253
|
+
}
|
|
177
254
|
</style>
|
|
178
255
|
</head>
|
|
179
256
|
<body
|
|
@@ -304,6 +381,35 @@
|
|
|
304
381
|
<line x1="6" y1="10" x2="9.5" y2="15" />
|
|
305
382
|
</svg>
|
|
306
383
|
</button>
|
|
384
|
+
<button
|
|
385
|
+
id="toolPostIt"
|
|
386
|
+
class="tool-btn"
|
|
387
|
+
title="Post-it (P)"
|
|
388
|
+
>
|
|
389
|
+
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" stroke="currentColor" stroke-width="1.4">
|
|
390
|
+
<path d="M1 1 H9 L12 4 V12 H1 Z" />
|
|
391
|
+
<path d="M9 1 V4 H12" stroke-opacity="0.5"/>
|
|
392
|
+
</svg>
|
|
393
|
+
</button>
|
|
394
|
+
<button
|
|
395
|
+
id="toolTextFree"
|
|
396
|
+
class="tool-btn"
|
|
397
|
+
title="Texte libre (T)"
|
|
398
|
+
style="font-size:11px; font-weight:600;"
|
|
399
|
+
>
|
|
400
|
+
T
|
|
401
|
+
</button>
|
|
402
|
+
<button
|
|
403
|
+
id="toolImage"
|
|
404
|
+
class="tool-btn"
|
|
405
|
+
title="Image — double-clic sur le canvas pour choisir un fichier, ou coller (⌘V) depuis le presse-papier"
|
|
406
|
+
>
|
|
407
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
|
|
408
|
+
<rect x="1" y="1" width="12" height="12" rx="1.5"/>
|
|
409
|
+
<circle cx="4.5" cy="4.5" r="1.2"/>
|
|
410
|
+
<path d="M1 9.5 L4 6.5 L6.5 9 L9 7 L13 10.5"/>
|
|
411
|
+
</svg>
|
|
412
|
+
</button>
|
|
307
413
|
<div class="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-0.5"></div>
|
|
308
414
|
|
|
309
415
|
<button
|
|
@@ -458,6 +564,9 @@
|
|
|
458
564
|
<!-- Debug overlay layer -->
|
|
459
565
|
<div id="debugLayer"></div>
|
|
460
566
|
|
|
567
|
+
<!-- Stamp overlay: intercepts canvas clicks during stamp mode -->
|
|
568
|
+
<div id="stampOverlay" style="position:absolute;inset:0;display:none;z-index:9;"></div>
|
|
569
|
+
|
|
461
570
|
<!-- Selection / resize overlay -->
|
|
462
571
|
<div id="selectionOverlay">
|
|
463
572
|
<div
|
|
@@ -480,6 +589,15 @@
|
|
|
480
589
|
class="resize-handle"
|
|
481
590
|
style="bottom: -5px; right: -5px; cursor: se-resize"
|
|
482
591
|
></div>
|
|
592
|
+
<div id="rh-rotate" title="Rotation forme">↻</div>
|
|
593
|
+
<div id="rh-label-rotate" title="Rotation texte" style="left: 0; top: -28px;">
|
|
594
|
+
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
|
|
595
|
+
<g transform="rotate(25,5,5)">
|
|
596
|
+
<line x1="2.5" y1="3" x2="7.5" y2="3"/>
|
|
597
|
+
<line x1="5" y1="3" x2="5" y2="8"/>
|
|
598
|
+
</g>
|
|
599
|
+
</svg>
|
|
600
|
+
</div>
|
|
483
601
|
</div>
|
|
484
602
|
|
|
485
603
|
<!-- Node panel -->
|
|
@@ -752,6 +870,60 @@
|
|
|
752
870
|
<rect x="5" y="1" width="8" height="8" rx="1" />
|
|
753
871
|
</svg>
|
|
754
872
|
</button>
|
|
873
|
+
|
|
874
|
+
<div class="panel-sep"></div>
|
|
875
|
+
|
|
876
|
+
<!-- Stamp: copy color (goutte) -->
|
|
877
|
+
<button
|
|
878
|
+
id="btnStampColor"
|
|
879
|
+
class="tool-btn !w-7 !h-6"
|
|
880
|
+
title="Tampon couleur — sélectionner les cibles, cliquer ici, puis cliquer la source"
|
|
881
|
+
>
|
|
882
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
|
|
883
|
+
<path d="M7 2 C7 2 3 6.5 3 9 a4 4 0 0 0 8 0 C11 6.5 7 2 7 2 Z" fill="currentColor" fill-opacity="0.25"/>
|
|
884
|
+
</svg>
|
|
885
|
+
</button>
|
|
886
|
+
|
|
887
|
+
<!-- Stamp: copy font size -->
|
|
888
|
+
<button
|
|
889
|
+
id="btnStampFontSize"
|
|
890
|
+
class="tool-btn !w-7 !h-6 font-mono text-xs font-bold"
|
|
891
|
+
title="Tampon taille police — sélectionner les cibles, cliquer ici, puis cliquer la source"
|
|
892
|
+
>Aa</button>
|
|
893
|
+
|
|
894
|
+
<div class="panel-sep"></div>
|
|
895
|
+
|
|
896
|
+
<!-- Rotation anti-horaire 10° -->
|
|
897
|
+
<button
|
|
898
|
+
id="btnRotateCCW"
|
|
899
|
+
class="tool-btn !w-7 !h-6"
|
|
900
|
+
title="Rotation anti-horaire 10°"
|
|
901
|
+
style="font-size:15px; line-height:1;"
|
|
902
|
+
>↺</button>
|
|
903
|
+
|
|
904
|
+
<!-- Rotation horaire 10° -->
|
|
905
|
+
<button
|
|
906
|
+
id="btnRotateCW"
|
|
907
|
+
class="tool-btn !w-7 !h-6"
|
|
908
|
+
title="Rotation horaire 10°"
|
|
909
|
+
style="font-size:15px; line-height:1;"
|
|
910
|
+
>↻</button>
|
|
911
|
+
|
|
912
|
+
<div class="panel-sep"></div>
|
|
913
|
+
|
|
914
|
+
<!-- Copy as PNG -->
|
|
915
|
+
<button
|
|
916
|
+
id="btnCopyPng"
|
|
917
|
+
class="tool-btn !h-6 px-1.5 font-mono text-xs font-semibold flex items-center gap-0.5"
|
|
918
|
+
title="Copier la sélection en PNG (⌘⇧C)"
|
|
919
|
+
>
|
|
920
|
+
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">
|
|
921
|
+
<line x1="5" y1="7" x2="5" y2="1"/>
|
|
922
|
+
<polyline points="2,4 5,1 8,4"/>
|
|
923
|
+
<line x1="1" y1="9" x2="9" y2="9"/>
|
|
924
|
+
</svg>
|
|
925
|
+
PNG
|
|
926
|
+
</button>
|
|
755
927
|
</div>
|
|
756
928
|
|
|
757
929
|
<!-- Edge panel -->
|
|
@@ -873,6 +1045,7 @@
|
|
|
873
1045
|
</div>
|
|
874
1046
|
</div>
|
|
875
1047
|
|
|
1048
|
+
<div id="toastContainer"></div>
|
|
876
1049
|
<script type="module" src="/diagram/main.js"></script>
|
|
877
1050
|
</body>
|
|
878
1051
|
</html>
|