living-ai-documentation 2.1.0 → 2.4.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.
@@ -5,13 +5,55 @@ import { st, markDirty } from './state.js';
5
5
  import { visEdgeProps } from './edge-rendering.js';
6
6
  import { pushSnapshot } from './history.js';
7
7
  import { t } from './t.js';
8
+ import { getArrowDefaults } from './defaults-modal.js';
9
+ import { openColorPickerPopup } from './color-picker.js';
8
10
 
9
11
  const DEFAULT_EDGE_COLOR = '#a8a29e';
10
12
  const FREE_ARROW_STYLE_KEY = 'ld-free-arrow-style';
11
13
 
14
+ // Palette is injected at boot by diagram.html via initEdgeColorSwatch().
15
+ let _edgePaletteEntries = [];
16
+
17
+ function syncEdgeFontSizeValue() {
18
+ const el = document.getElementById('edgeFontSizeValue');
19
+ if (!el) return;
20
+ const sizes = (st.selectedEdgeIds || []).map((id) => {
21
+ const e = st.edges && st.edges.get(id);
22
+ return e ? (e.fontSize || 11) : null;
23
+ }).filter((s) => s !== null);
24
+ if (!sizes.length) { el.textContent = '–'; return; }
25
+ const first = sizes[0];
26
+ el.textContent = sizes.every((s) => s === first) ? String(first) : '–';
27
+ }
28
+
29
+ function syncEdgeColorSwatch(hexColor) {
30
+ const swatch = document.getElementById('edgeColorSwatch');
31
+ if (!swatch) return;
32
+ swatch.style.background = hexColor;
33
+ swatch.style.borderColor = hexColor;
34
+ swatch.dataset.currentColor = hexColor;
35
+ }
36
+
37
+ export function initEdgeColorSwatch(palette) {
38
+ _edgePaletteEntries = palette.map((hex) => ({
39
+ value: hex, bg: hex, border: hex, label: hex,
40
+ }));
41
+ const swatch = document.getElementById('edgeColorSwatch');
42
+ if (!swatch) return;
43
+ swatch.addEventListener('click', (e) => {
44
+ e.stopPropagation();
45
+ const current = swatch.dataset.currentColor || DEFAULT_EDGE_COLOR;
46
+ openColorPickerPopup(swatch, _edgePaletteEntries, current, (entry) => {
47
+ setEdgeColor(entry.value);
48
+ }, { columns: _edgePaletteEntries.length });
49
+ });
50
+ }
51
+
12
52
  // Persist the style of the first free arrow (anchor→anchor) in the current
13
53
  // selection so the next double-click creation reuses it.
14
54
  function persistFreeArrowStyle() {
55
+ // Persist from any selected edge — free arrows (anchor→anchor) take priority,
56
+ // but shape→shape edges also update the style memory.
15
57
  const freeId = st.selectedEdgeIds.find((id) => {
16
58
  const e = st.edges.get(id);
17
59
  if (!e) return false;
@@ -19,19 +61,30 @@ function persistFreeArrowStyle() {
19
61
  const toN = st.nodes && st.nodes.get(e.to);
20
62
  return fromN && fromN.shapeType === 'anchor' && toN && toN.shapeType === 'anchor';
21
63
  });
22
- if (!freeId) return;
23
- const e = st.edges.get(freeId);
64
+ const edgeId = freeId || st.selectedEdgeIds[0];
65
+ if (!edgeId) return;
66
+ const e = st.edges.get(edgeId);
67
+ if (!e) return;
24
68
  localStorage.setItem(FREE_ARROW_STYLE_KEY, JSON.stringify({
25
69
  arrowDir: e.arrowDir || 'to',
26
70
  dashes: e.dashes || false,
27
71
  edgeColor: e.edgeColor || null,
28
72
  edgeWidth: e.edgeWidth || null,
73
+ fontSize: e.fontSize || null,
29
74
  }));
30
75
  }
31
76
 
32
77
  export function getLastFreeArrowStyle() {
33
- try { return JSON.parse(localStorage.getItem(FREE_ARROW_STYLE_KEY)) || {}; }
34
- catch { return {}; }
78
+ try {
79
+ const stored = JSON.parse(localStorage.getItem(FREE_ARROW_STYLE_KEY));
80
+ if (stored) {
81
+ // Merge with defaults: stored values override defaults, but null/undefined
82
+ // values in stored fall back to defaults (handles legacy keys without fontSize).
83
+ const clean = Object.fromEntries(Object.entries(stored).filter(([, v]) => v != null));
84
+ return { ...getArrowDefaults(), ...clean };
85
+ }
86
+ } catch { /* ignore */ }
87
+ return getArrowDefaults();
35
88
  }
36
89
 
37
90
  function isEdgeLocked(edge) {
@@ -97,13 +150,10 @@ export function showEdgePanel() {
97
150
  document.getElementById(id).classList.remove('edge-btn-active'));
98
151
  document.getElementById(dashes ? 'edgeBtnDashed' : 'edgeBtnSolid').classList.add('edge-btn-active');
99
152
 
100
- // Highlight active color dot.
101
- const activeColor = (e.edgeColor || DEFAULT_EDGE_COLOR).toLowerCase();
102
- document.querySelectorAll('#edgePanel [data-edge-color]').forEach((btn) => {
103
- const isActive = btn.dataset.edgeColor.toLowerCase() === activeColor;
104
- btn.style.outline = isActive ? '2px solid #f97316' : '';
105
- btn.style.outlineOffset = isActive ? '2px' : '';
106
- });
153
+ // Sync edge color swatch and font size display.
154
+ const activeColor = (e.edgeColor || DEFAULT_EDGE_COLOR);
155
+ syncEdgeColorSwatch(activeColor);
156
+ syncEdgeFontSizeValue();
107
157
 
108
158
  // Show/hide the clear-ports button based on whether this edge has ports.
109
159
  const hasPorts = !!(e.fromPort || e.toPort);
@@ -180,6 +230,54 @@ export function setEdgeColor(hex) {
180
230
  markDirty();
181
231
  }
182
232
 
233
+ export async function saveEdgeAsDefault() {
234
+ const freeId = st.selectedEdgeIds.find((id) => {
235
+ const e = st.edges.get(id);
236
+ if (!e) return false;
237
+ const fromN = st.nodes && st.nodes.get(e.from);
238
+ const toN = st.nodes && st.nodes.get(e.to);
239
+ return fromN && fromN.shapeType === 'anchor' && toN && toN.shapeType === 'anchor';
240
+ });
241
+ const edgeId = freeId || st.selectedEdgeIds[0];
242
+ if (!edgeId) return;
243
+ const e = st.edges.get(edgeId);
244
+ if (!e) return;
245
+
246
+ const { getDiagramDefaults, initDiagramDefaults } = await import('./defaults-modal.js');
247
+ const current = getDiagramDefaults() || {};
248
+ const arrows = {
249
+ arrowDir: e.arrowDir || 'to',
250
+ dashes: e.dashes || false,
251
+ fontSize: e.fontSize || 11,
252
+ };
253
+ const updated = { arrows, shapes: current.shapes || null };
254
+
255
+ try {
256
+ const res = await fetch('/api/config', {
257
+ method: 'PUT',
258
+ headers: { 'Content-Type': 'application/json' },
259
+ body: JSON.stringify({ diagramDefaults: updated }),
260
+ });
261
+ if (!res.ok) throw new Error(await res.text());
262
+ initDiagramDefaults({ diagramDefaults: updated });
263
+ // Sync localStorage so the next arrow creation uses the new default immediately
264
+ localStorage.setItem(FREE_ARROW_STYLE_KEY, JSON.stringify({
265
+ arrowDir: arrows.arrowDir,
266
+ dashes: arrows.dashes,
267
+ fontSize: arrows.fontSize,
268
+ edgeColor: null,
269
+ edgeWidth: null,
270
+ }));
271
+ const btn = document.getElementById('btnSaveEdgeDefault');
272
+ if (btn) {
273
+ btn.classList.add('tool-active');
274
+ setTimeout(() => btn.classList.remove('tool-active'), 800);
275
+ }
276
+ } catch (err) {
277
+ console.error('Failed to save edge default', err);
278
+ }
279
+ }
280
+
183
281
  export function changeEdgeWidth(delta) {
184
282
  if (!st.selectedEdgeIds.length) return;
185
283
  pushSnapshot();
@@ -244,6 +342,7 @@ export function changeEdgeFontSize(delta) {
244
342
  // Keep native label transparent — drawEdgeLabels() is the single render path.
245
343
  st.edges.update({ id, fontSize: newSize, font: { size: newSize, align: 'middle', color: 'rgba(0,0,0,0)' } });
246
344
  });
345
+ syncEdgeFontSizeValue();
247
346
  markDirty();
248
347
  }
249
348
 
@@ -3,10 +3,10 @@
3
3
 
4
4
  import { st, markDirty } from './state.js';
5
5
  import { TOOL_BTN_MAP } from './constants.js';
6
- import { showNodePanel, hideNodePanel, setNodeColor, setNodeBgOpacity, changeNodeFontSize, setTextAlign, setTextValign, setCustomShapeLabelPlacement, changeZOrder, activateStamp, cancelStamp, stepRotate, toggleNodeLock } from './node-panel.js';
6
+ import { showNodePanel, hideNodePanel, setNodeColor, setNodeBgOpacity, changeNodeFontSize, setTextAlign, setTextValign, setCustomShapeLabelPlacement, changeZOrder, activateStamp, cancelStamp, stepRotate, toggleNodeLock, saveShapeAsDefault } from './node-panel.js';
7
7
  import { groupNodes, ungroupNodes } from './groups.js';
8
8
  import { showLinkPanel, hideLinkPanel } from './link-panel.js';
9
- import { hideEdgePanel, setEdgeArrow, setEdgeDashes, changeEdgeFontSize, stepEdgeLabelRotation, clearEdgePorts, setEdgeColor, changeEdgeWidth, toggleEdgeLock, resetEdgeLabelWidth, stepEdgeLabelOffset, resetEdgeLabelOffset } from './edge-panel.js';
9
+ import { hideEdgePanel, setEdgeArrow, setEdgeDashes, changeEdgeFontSize, stepEdgeLabelRotation, clearEdgePorts, setEdgeColor, changeEdgeWidth, toggleEdgeLock, resetEdgeLabelWidth, stepEdgeLabelOffset, resetEdgeLabelOffset, saveEdgeAsDefault } from './edge-panel.js';
10
10
  import { startLabelEdit, startEdgeLabelEdit, hideLabelInput } from './label-editor.js';
11
11
  import { hideSelectionOverlay, toggleResizeMode } from './selection-overlay.js';
12
12
  import { toggleGrid } from './grid.js';
@@ -207,8 +207,6 @@ initEvidenceMode();
207
207
  // ── Node panel wiring ─────────────────────────────────────────────────────────
208
208
 
209
209
  document.getElementById('nodePanel').addEventListener('click', (e) => {
210
- const colorBtn = e.target.closest('[data-color]');
211
- if (colorBtn) setNodeColor(colorBtn.dataset.color);
212
210
  const labelPlacementBtn = e.target.closest('[data-label-placement]');
213
211
  if (labelPlacementBtn) setCustomShapeLabelPlacement(labelPlacementBtn.dataset.labelPlacement);
214
212
  });
@@ -277,10 +275,9 @@ document.getElementById('btnEdgeLabelWidthReset').addEventListener('click', rese
277
275
  document.getElementById('btnEdgeLock').addEventListener('click', toggleEdgeLock);
278
276
  document.getElementById('btnEdgeWidthDecrease').addEventListener('click', () => changeEdgeWidth(-0.5));
279
277
  document.getElementById('btnEdgeWidthIncrease').addEventListener('click', () => changeEdgeWidth(0.5));
280
- document.getElementById('edgePanel').addEventListener('click', (e) => {
281
- const colorBtn = e.target.closest('[data-edge-color]');
282
- if (colorBtn) setEdgeColor(colorBtn.dataset.edgeColor);
283
- });
278
+ // Edge color is handled by #edgeColorSwatch via initEdgeColorSwatch() in edge-panel.js
279
+ document.getElementById('btnSaveShapeDefault').addEventListener('click', saveShapeAsDefault);
280
+ document.getElementById('btnSaveEdgeDefault').addEventListener('click', saveEdgeAsDefault);
284
281
 
285
282
  // ── Keyboard shortcuts ────────────────────────────────────────────────────────
286
283
 
@@ -205,14 +205,19 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
205
205
  }
206
206
  pushSnapshot();
207
207
  data.id = "e" + Date.now();
208
- data.arrowDir = "to";
209
- data.dashes = false;
208
+ const edgeStyle = getLastFreeArrowStyle();
209
+ data.arrowDir = edgeStyle.arrowDir || "to";
210
+ data.dashes = edgeStyle.dashes || false;
211
+ if (edgeStyle.fontSize) {
212
+ data.fontSize = edgeStyle.fontSize;
213
+ data.font = { size: edgeStyle.fontSize, align: 'middle', color: 'rgba(0,0,0,0)' };
214
+ }
210
215
  // Attach captured port selections — skip for anchor nodes.
211
216
  const fromIsAnchor = fromNode.shapeType === "anchor";
212
217
  const toIsAnchor = toNode.shapeType === "anchor";
213
218
  if (!fromIsAnchor) data.fromPort = _addEdgeFromPort || null;
214
219
  if (!toIsAnchor) data.toPort = _hoveredPortKey || null;
215
- Object.assign(data, visEdgeProps("to", false));
220
+ Object.assign(data, visEdgeProps(data.arrowDir, data.dashes));
216
221
  // Port edges: make vis-network's ghost transparent so only drawPortEdge is visible.
217
222
  if (data.fromPort || data.toPort) {
218
223
  data.color = {
@@ -1142,6 +1147,7 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
1142
1147
  const lastStyle = getLastFreeArrowStyle();
1143
1148
  const arrowDir = lastStyle.arrowDir || "to";
1144
1149
  const dashes = lastStyle.dashes || false;
1150
+ const edgeFontSize = lastStyle.fontSize || null;
1145
1151
  st.edges.add({
1146
1152
  id: edgeId,
1147
1153
  from: fromId,
@@ -1150,6 +1156,7 @@ export function initNetwork(savedNodes, savedEdges, edgesStraight = false) {
1150
1156
  dashes,
1151
1157
  edgeColor: lastStyle.edgeColor || null,
1152
1158
  edgeWidth: lastStyle.edgeWidth || null,
1159
+ ...(edgeFontSize ? { fontSize: edgeFontSize, font: { size: edgeFontSize, align: 'middle', color: 'rgba(0,0,0,0)' } } : {}),
1153
1160
  smooth: { enabled: false },
1154
1161
  ...visEdgeProps(arrowDir, dashes),
1155
1162
  ...(lastStyle.edgeColor
@@ -1720,6 +1727,8 @@ function onDoubleClick(params) {
1720
1727
  const fontSize = lastStyle.fontSize || null;
1721
1728
  const textAlign = lastStyle.textAlign || null;
1722
1729
  const textValign = lastStyle.textValign || null;
1730
+ const nodeWidth = lastStyle.width || defaults[0];
1731
+ const nodeHeight = lastStyle.height || defaults[1];
1723
1732
  const rawPos = params.pointer.canvas;
1724
1733
  const pos = st.gridEnabled ? snapToGrid(rawPos.x, rawPos.y) : rawPos;
1725
1734
  st.nodes.add({
@@ -1733,8 +1742,8 @@ function onDoubleClick(params) {
1733
1742
  shapeType,
1734
1743
  customShapeId: customShapeId || null,
1735
1744
  colorKey,
1736
- nodeWidth: defaults[0],
1737
- nodeHeight: defaults[1],
1745
+ nodeWidth,
1746
+ nodeHeight,
1738
1747
  fontSize,
1739
1748
  textAlign,
1740
1749
  textValign,
@@ -1745,8 +1754,8 @@ function onDoubleClick(params) {
1745
1754
  ...visNodeProps(
1746
1755
  shapeType,
1747
1756
  colorKey,
1748
- defaults[0],
1749
- defaults[1],
1757
+ nodeWidth,
1758
+ nodeHeight,
1750
1759
  fontSize,
1751
1760
  textAlign,
1752
1761
  textValign,
@@ -6,6 +6,37 @@ import { SHAPE_DEFAULTS } from './node-rendering.js';
6
6
  import { CUSTOM_SHAPE_TYPE, getCustomShapeLabelPlacement } from './custom-shapes.js';
7
7
  import { pushSnapshot } from './history.js';
8
8
  import { t } from './t.js';
9
+ import { getShapeDefaults } from './defaults-modal.js';
10
+ import { NODE_COLORS, DEFAULT_NODE_PALETTE } from './constants.js';
11
+ import { openColorPickerPopup } from './color-picker.js';
12
+
13
+ // Reads the effective color for a key, respecting runtime overrides.
14
+ function getEffectiveNodeColor(key) {
15
+ return st.nodeColorOverrides[key] || NODE_COLORS[key] || NODE_COLORS['c-gray'];
16
+ }
17
+
18
+ // Built dynamically so overrides applied at boot are reflected in the picker.
19
+ function buildNodeColorEntries() {
20
+ return DEFAULT_NODE_PALETTE.map((key) => {
21
+ const c = getEffectiveNodeColor(key);
22
+ return { value: key, bg: c.bg, border: c.border, label: key.replace('c-', '') };
23
+ });
24
+ }
25
+
26
+ function syncNodeColorSwatch() {
27
+ const swatch = document.getElementById('nodeColorSwatch');
28
+ if (!swatch) return;
29
+ const firstId = (st.selectedNodeIds || []).find((id) => {
30
+ const n = st.nodes && st.nodes.get(id);
31
+ return n && n.shapeType !== 'anchor';
32
+ });
33
+ const n = firstId && st.nodes && st.nodes.get(firstId);
34
+ const key = (n && n.colorKey) || 'c-gray';
35
+ const c = getEffectiveNodeColor(key);
36
+ swatch.style.background = c.bg;
37
+ swatch.style.borderColor = c.border;
38
+ swatch.dataset.currentColor = key;
39
+ }
9
40
 
10
41
  const CUSTOM_LABEL_PLACEMENTS = ['below', 'above', 'right', 'left', 'center'];
11
42
 
@@ -27,8 +58,16 @@ function persistNodeStyle() {
27
58
  }
28
59
 
29
60
  export function getLastNodeStyle(shapeType) {
30
- try { return JSON.parse(localStorage.getItem('ld-node-style-' + shapeType)) || {}; }
31
- catch { return {}; }
61
+ try {
62
+ const stored = JSON.parse(localStorage.getItem('ld-node-style-' + shapeType));
63
+ if (stored) {
64
+ // Merge with defaults: stored values override defaults, but null/undefined
65
+ // values in stored fall back to defaults (handles legacy keys without width/height).
66
+ const clean = Object.fromEntries(Object.entries(stored).filter(([, v]) => v != null));
67
+ return { ...getShapeDefaults(shapeType), ...clean };
68
+ }
69
+ } catch { /* ignore */ }
70
+ return getShapeDefaults(shapeType);
32
71
  }
33
72
 
34
73
  // All shapes are ctxRenderers — vis-network never re-reads the closure after
@@ -145,6 +184,7 @@ export function showNodePanel() {
145
184
  syncNodeLockButton();
146
185
  syncNodeFontSizeValue();
147
186
  syncCustomShapeLabelPlacementControls();
187
+ syncNodeColorSwatch();
148
188
  // Sync the opacity slider with the first selected node's current value so the
149
189
  // slider reflects the live state rather than whatever position it was left at.
150
190
  const slider = document.getElementById('nodeBgOpacity');
@@ -159,6 +199,67 @@ export function hideNodePanel() {
159
199
  document.getElementById('nodePanel').classList.add('hidden');
160
200
  }
161
201
 
202
+ export function initNodeColorSwatch() {
203
+ const swatch = document.getElementById('nodeColorSwatch');
204
+ if (!swatch) return;
205
+ swatch.addEventListener('click', (e) => {
206
+ e.stopPropagation();
207
+ const current = swatch.dataset.currentColor || 'c-gray';
208
+ openColorPickerPopup(swatch, buildNodeColorEntries(), current, (entry) => {
209
+ setNodeColor(entry.value);
210
+ });
211
+ });
212
+ }
213
+
214
+ export async function saveShapeAsDefault() {
215
+ const firstId = (st.selectedNodeIds || []).find((id) => {
216
+ const n = st.nodes && st.nodes.get(id);
217
+ return n && n.shapeType !== 'anchor' && n.shapeType !== CUSTOM_SHAPE_TYPE;
218
+ });
219
+ if (!firstId) return;
220
+ const n = st.nodes.get(firstId);
221
+ if (!n || !n.shapeType) return;
222
+
223
+ const { getDiagramDefaults } = await import('./defaults-modal.js');
224
+ const current = getDiagramDefaults() || {};
225
+ const shapes = Object.assign({}, current.shapes || {});
226
+ shapes[n.shapeType] = {
227
+ width: n.nodeWidth || SHAPE_DEFAULTS[n.shapeType]?.[0] || 100,
228
+ height: n.nodeHeight || SHAPE_DEFAULTS[n.shapeType]?.[1] || 40,
229
+ fontSize: n.fontSize || 13,
230
+ colorKey: n.colorKey || 'c-gray',
231
+ };
232
+ const updated = { arrows: current.arrows || null, shapes };
233
+
234
+ try {
235
+ const res = await fetch('/api/config', {
236
+ method: 'PUT',
237
+ headers: { 'Content-Type': 'application/json' },
238
+ body: JSON.stringify({ diagramDefaults: updated }),
239
+ });
240
+ if (!res.ok) throw new Error(await res.text());
241
+ const { initDiagramDefaults } = await import('./defaults-modal.js');
242
+ initDiagramDefaults({ diagramDefaults: updated });
243
+ // Sync localStorage so the next creation uses the new default immediately
244
+ localStorage.setItem('ld-node-style-' + n.shapeType, JSON.stringify({
245
+ colorKey: shapes[n.shapeType].colorKey,
246
+ fontSize: shapes[n.shapeType].fontSize,
247
+ width: shapes[n.shapeType].width,
248
+ height: shapes[n.shapeType].height,
249
+ textAlign: null,
250
+ textValign: null,
251
+ }));
252
+ // Visual feedback: flash the button orange briefly
253
+ const btn = document.getElementById('btnSaveShapeDefault');
254
+ if (btn) {
255
+ btn.classList.add('tool-active');
256
+ setTimeout(() => btn.classList.remove('tool-active'), 800);
257
+ }
258
+ } catch (err) {
259
+ console.error('Failed to save shape default', err);
260
+ }
261
+ }
262
+
162
263
  export function toggleNodeLock() {
163
264
  const { allLocked, nodeIds, edgeIds } = selectedLockState();
164
265
  if (!nodeIds.length && !edgeIds.length) return;
@@ -195,6 +296,7 @@ export function setNodeColor(colorKey) {
195
296
  persistNodeStyle();
196
297
  forceRedraw();
197
298
  markDirty();
299
+ syncNodeColorSwatch();
198
300
  }
199
301
 
200
302
  // The slider fires `input` on every step during a drag. The caller is expected
@@ -559,6 +559,16 @@
559
559
  </button>
560
560
  <div class="w-px h-6 bg-gray-200 dark:bg-gray-700 mx-0.5"></div>
561
561
 
562
+ <button
563
+ id="btnDiagramDefaults"
564
+ class="tool-btn"
565
+ data-i18n-title="diagram.toolbar.defaults"
566
+ >
567
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
568
+ <path d="M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492M5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0"/>
569
+ <path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52zm-2.633.283c.246-.835 1.428-.835 1.674 0l.094.319a1.873 1.873 0 0 0 2.693 1.115l.291-.16c.764-.415 1.6.42 1.184 1.185l-.159.292a1.873 1.873 0 0 0 1.116 2.692l.318.094c.835.246.835 1.428 0 1.674l-.319.094a1.873 1.873 0 0 0-1.115 2.693l.16.291c.415.764-.42 1.6-1.185 1.184l-.291-.159a1.873 1.873 0 0 0-2.693 1.116l-.094.318c-.246.835-1.428.835-1.674 0l-.094-.319a1.873 1.873 0 0 0-2.692-1.115l-.292.16c-.764.415-1.6-.42-1.184-1.185l.159-.291A1.873 1.873 0 0 0 1.945 8.93l-.319-.094c-.835-.246-.835-1.428 0-1.674l.319-.094A1.873 1.873 0 0 0 3.06 4.474l-.16-.292c-.415-.764.42-1.6 1.185-1.184l.292.159a1.873 1.873 0 0 0 2.692-1.115z"/>
570
+ </svg>
571
+ </button>
562
572
  <button
563
573
  id="btnAlign"
564
574
  class="tool-btn"
@@ -812,7 +822,9 @@
812
822
  >🔒</button>
813
823
  <div class="panel-sep"></div>
814
824
  <div id="nodePanelControls" class="contents">
815
- <div id="nodePaletteContainer" class="contents"></div>
825
+ <button id="nodeColorSwatch"
826
+ style="width:1.5rem;height:1.5rem;border-radius:0.25rem;border:2px solid #a8a29e;background:#f5f5f4;cursor:pointer;flex-shrink:0;"
827
+ data-i18n-title="diagram.node_panel.color"></button>
816
828
  <div class="panel-sep"></div>
817
829
  <input
818
830
  id="nodeBgOpacity"
@@ -1158,6 +1170,19 @@
1158
1170
  <rect x="19" y="4.5" width="3" height="5" rx="1" fill="currentColor"/>
1159
1171
  </svg>
1160
1172
  </button>
1173
+ <div class="panel-sep"></div>
1174
+
1175
+ <!-- Save as default for this shape type -->
1176
+ <button
1177
+ id="btnSaveShapeDefault"
1178
+ class="tool-btn !w-7 !h-6"
1179
+ data-i18n-title="diagram.node_panel.save_as_default"
1180
+ >
1181
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
1182
+ <path d="M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492M5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0"/>
1183
+ <path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52zm-2.633.283c.246-.835 1.428-.835 1.674 0l.094.319a1.873 1.873 0 0 0 2.693 1.115l.291-.16c.764-.415 1.6.42 1.184 1.185l-.159.292a1.873 1.873 0 0 0 1.116 2.692l.318.094c.835.246.835 1.428 0 1.674l-.319.094a1.873 1.873 0 0 0-1.115 2.693l.16.291c.415.764-.42 1.6-1.185 1.184l-.291-.159a1.873 1.873 0 0 0-2.693 1.116l-.094.318c-.246.835-1.428.835-1.674 0l-.094-.319a1.873 1.873 0 0 0-2.692-1.115l-.292.16c-.764.415-1.6-.42-1.184-1.185l.159-.291A1.873 1.873 0 0 0 1.945 8.93l-.319-.094c-.835-.246-.835-1.428 0-1.674l.319-.094A1.873 1.873 0 0 0 3.06 4.474l-.16-.292c-.415-.764.42-1.6 1.185-1.184l.292.159a1.873 1.873 0 0 0 2.692-1.115z"/>
1184
+ </svg>
1185
+ </button>
1161
1186
  </div><!-- /nodePanelControls -->
1162
1187
  </div>
1163
1188
 
@@ -1282,6 +1307,11 @@
1282
1307
  >
1283
1308
  Aa−
1284
1309
  </button>
1310
+ <span
1311
+ id="edgeFontSizeValue"
1312
+ 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"
1313
+ data-i18n-title="diagram.edge_panel.font_size_value"
1314
+ >11</span>
1285
1315
  <button
1286
1316
  id="btnEdgeFontIncrease"
1287
1317
  class="tool-btn !w-8 !h-6"
@@ -1350,7 +1380,9 @@
1350
1380
  <button id="btnEdgeWidthDecrease" class="tool-btn !w-7 !h-6" style="font-size:10px" data-i18n-title="diagram.edge_panel.width_decrease">W−</button>
1351
1381
  <button id="btnEdgeWidthIncrease" class="tool-btn !w-7 !h-6" style="font-size:10px" data-i18n-title="diagram.edge_panel.width_increase">W+</button>
1352
1382
  <div class="panel-sep"></div>
1353
- <div id="edgePaletteContainer" class="contents"></div>
1383
+ <button id="edgeColorSwatch"
1384
+ style="width:1.5rem;height:1.5rem;border-radius:0.25rem;border:2px solid #a8a29e;background:#a8a29e;cursor:pointer;flex-shrink:0;"
1385
+ data-i18n-title="diagram.edge_panel.color"></button>
1354
1386
  <div class="panel-sep"></div>
1355
1387
  <button
1356
1388
  id="btnEdgeClearPorts"
@@ -1359,6 +1391,17 @@
1359
1391
  >
1360
1392
 
1361
1393
  </button>
1394
+ <div class="panel-sep"></div>
1395
+ <button
1396
+ id="btnSaveEdgeDefault"
1397
+ class="tool-btn !w-7 !h-6"
1398
+ data-i18n-title="diagram.edge_panel.save_as_default"
1399
+ >
1400
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
1401
+ <path d="M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492M5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0"/>
1402
+ <path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52zm-2.633.283c.246-.835 1.428-.835 1.674 0l.094.319a1.873 1.873 0 0 0 2.693 1.115l.291-.16c.764-.415 1.6.42 1.184 1.185l-.159.292a1.873 1.873 0 0 0 1.116 2.692l.318.094c.835.246.835 1.428 0 1.674l-.319.094a1.873 1.873 0 0 0-1.115 2.693l.16.291c.415.764-.42 1.6-1.185 1.184l-.291-.159a1.873 1.873 0 0 0-2.693 1.116l-.094.318c-.246.835-1.428.835-1.674 0l-.094-.319a1.873 1.873 0 0 0-2.692-1.115l-.292.16c-.764.415-1.6-.42-1.184-1.185l.159-.291A1.873 1.873 0 0 0 1.945 8.93l-.319-.094c-.835-.246-.835-1.428 0-1.674l.319-.094A1.873 1.873 0 0 0 3.06 4.474l-.16-.292c-.415-.764.42-1.6 1.185-1.184l.292.159a1.873 1.873 0 0 0 2.692-1.115z"/>
1403
+ </svg>
1404
+ </button>
1362
1405
  </div><!-- /edgePanelControls -->
1363
1406
  </div>
1364
1407
 
@@ -1437,6 +1480,9 @@
1437
1480
  import { st } from '/diagram/state.js';
1438
1481
  import { loadDiagramList } from '/diagram/persistence.js';
1439
1482
  import { loadCustomShapeLibraries, renderCustomShapeBar } from '/diagram/custom-shapes.js';
1483
+ import { initDiagramDefaults, openDefaultsModal } from '/diagram/defaults-modal.js';
1484
+ import { initNodeColorSwatch } from '/diagram/node-panel.js';
1485
+ import { initEdgeColorSwatch } from '/diagram/edge-panel.js';
1440
1486
  (async () => {
1441
1487
  // diagramNodePalette: array of 15 bg hex strings (positional, matching DEFAULT_NODE_PALETTE)
1442
1488
  // diagramEdgePalette: array of hex strings
@@ -1447,6 +1493,7 @@
1447
1493
  await window.initI18n(cfg.language);
1448
1494
  if (Array.isArray(cfg.diagramNodePalette) && cfg.diagramNodePalette.length) nodeBgOverrides = cfg.diagramNodePalette;
1449
1495
  if (Array.isArray(cfg.diagramEdgePalette) && cfg.diagramEdgePalette.length) edgePalette = cfg.diagramEdgePalette;
1496
+ initDiagramDefaults(cfg);
1450
1497
  } catch { await window.initI18n('en'); /* config unavailable — use defaults */ }
1451
1498
 
1452
1499
  // Apply node color overrides to shared state so renderers pick them up.
@@ -1457,36 +1504,12 @@
1457
1504
  }
1458
1505
  });
1459
1506
 
1460
- // Build node palette buttons.
1461
- const nc = document.getElementById('nodePaletteContainer');
1462
- DEFAULT_NODE_PALETTE.forEach((key, i) => {
1463
- if (!nc) return;
1464
- const customBg = nodeBgOverrides && nodeBgOverrides[i];
1465
- const c = (customBg ? deriveNodeColors(customBg, NODE_L_RATIOS[key] || 0.60) : null) || NODE_COLORS[key];
1466
- const btn = document.createElement('button');
1467
- btn.dataset.color = key;
1468
- btn.className = 'w-5 h-5 rounded-full border-2 hover:scale-125 transition-transform';
1469
- btn.style.background = c.bg;
1470
- btn.style.borderColor = c.border;
1471
- btn.title = key.replace('c-', '');
1472
- nc.appendChild(btn);
1473
- });
1474
-
1475
- // Build edge palette buttons.
1476
- const ec = document.getElementById('edgePaletteContainer');
1477
- edgePalette.forEach((hex) => {
1478
- if (!ec) return;
1479
- const btn = document.createElement('button');
1480
- btn.dataset.edgeColor = hex;
1481
- btn.className = 'edge-color-btn w-4 h-4 rounded-full hover:scale-125 transition-transform';
1482
- btn.style.background = hex;
1483
- btn.style.border = '2px solid rgba(0,0,0,0.2)';
1484
- btn.title = hex;
1485
- ec.appendChild(btn);
1486
- });
1507
+ initNodeColorSwatch();
1508
+ initEdgeColorSwatch(edgePalette);
1487
1509
  await loadCustomShapeLibraries();
1488
1510
  renderCustomShapeBar();
1489
1511
  window.applyI18n();
1512
+ document.getElementById('btnDiagramDefaults').addEventListener('click', openDefaultsModal);
1490
1513
  loadDiagramList();
1491
1514
  })();
1492
1515
  </script>
@@ -29,12 +29,30 @@
29
29
  "nav.export_all_pdf": "Export all documents as PDF",
30
30
  "nav.new_folder": "New folder",
31
31
  "nav.new_document": "New document",
32
+ "nav.ai_agents": "Agents IA",
32
33
  "nav.word_cloud": "☁ Word Cloud",
33
34
  "nav.diagram": "◇ Diagram",
34
35
  "nav.ai_context": "AI Context",
35
36
  "nav.files": "📁 Attached files",
36
37
  "nav.admin": "⚙ Admin",
37
38
 
39
+ "agents.title": "Agents IA",
40
+ "agents.subtitle": "Launch a configured workspace agent.",
41
+ "agents.empty": "No workspace agent is configured yet.",
42
+ "agents.ready": "Ready",
43
+ "agents.requires_input": "Requires input",
44
+ "agents.not_ready": "Configuration incomplete",
45
+ "agents.no_provider": "No LLM provider",
46
+ "agents.input_title": "Agent input",
47
+ "agents.input_hint": "Provide the input required for this run.",
48
+ "agents.run": "Run agent",
49
+ "agents.loading": "Running agent…",
50
+ "agents.success": "Agent run completed",
51
+ "agents.failure": "Agent run failed",
52
+ "agents.open_document": "Open document",
53
+ "agents.load_failed": "Failed to load workspace agents.",
54
+ "agents.run_failed": "Agent run failed.",
55
+
38
56
  "files.title": "📁 Attached files",
39
57
  "files.empty": "No files yet — upload one from a document editor (paperclip, drag & drop, or paste).",
40
58
  "files.replace": "Replace",
@@ -591,7 +609,26 @@
591
609
  "diagram.toolbar.shape_editor": "Custom shape editor",
592
610
  "diagram.toolbar.arrow": "Arrow (F)",
593
611
  "diagram.toolbar.delete": "Delete (Del)",
612
+ "diagram.toolbar.defaults": "Diagram defaults — configure default sizes, colors and styles",
594
613
  "diagram.toolbar.align_guides": "Alignment guides — highlights same-type shape alignment",
614
+ "diagram.defaults.title": "Diagram defaults",
615
+ "diagram.defaults.section.arrows": "Arrows",
616
+ "diagram.defaults.section.shapes": "Shapes",
617
+ "diagram.defaults.field.direction": "Direction",
618
+ "diagram.defaults.field.style": "Style",
619
+ "diagram.defaults.field.font_size": "Font size",
620
+ "diagram.defaults.field.width": "W",
621
+ "diagram.defaults.field.height": "H",
622
+ "diagram.defaults.field.color": "Color",
623
+ "diagram.defaults.style.solid": "Solid",
624
+ "diagram.defaults.style.dashed": "Dashed",
625
+ "diagram.defaults.shape.box": "Rectangle",
626
+ "diagram.defaults.shape.ellipse": "Ellipse",
627
+ "diagram.defaults.shape.circle": "Circle",
628
+ "diagram.defaults.shape.database": "Database",
629
+ "diagram.defaults.shape.actor": "Actor",
630
+ "diagram.defaults.shape.postit": "Post-it",
631
+ "diagram.defaults.shape.text_free": "Free text",
595
632
  "diagram.toolbar.grid": "Grid / Snap to grid (G)",
596
633
  "diagram.toolbar.edge_style": "Toggle curved / straight arrows",
597
634
  "diagram.toolbar.resize_corner": "Resize mode: fixed corner (click for fixed centre)",
@@ -609,6 +646,8 @@
609
646
  "diagram.sidebar.empty": "No diagrams",
610
647
  "diagram.sidebar.delete_title": "Delete",
611
648
 
649
+ "diagram.node_panel.color": "Shape color",
650
+ "diagram.node_panel.save_as_default": "Save as default for this shape type",
612
651
  "diagram.node_panel.lock": "Lock selection",
613
652
  "diagram.node_panel.unlock": "Unlock selection",
614
653
  "diagram.node_panel.edit_label": "Edit text (double-click)",
@@ -648,6 +687,9 @@
648
687
  "diagram.link_panel.save_btn": "Save",
649
688
  "diagram.link_panel.remove_btn": "Remove",
650
689
 
690
+ "diagram.edge_panel.font_size_value": "Font size",
691
+ "diagram.edge_panel.color": "Arrow color",
692
+ "diagram.edge_panel.save_as_default": "Save as default for arrows",
651
693
  "diagram.edge_panel.lock": "Lock arrow",
652
694
  "diagram.edge_panel.unlock": "Unlock arrow",
653
695
  "diagram.edge_panel.no_arrow": "Simple line",