living-documentation 7.42.0 → 7.44.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.
@@ -0,0 +1,104 @@
1
+ import { st } from './state.js';
2
+
3
+ export const CUSTOM_SHAPE_TYPE = 'custom-shape';
4
+ export const CUSTOM_SHAPE_TOOL_PREFIX = 'custom-shape:';
5
+ export const CUSTOM_SHAPE_DEFAULT_SIZE = 65;
6
+
7
+ export const DEFAULT_CUSTOM_ANCHORS = [
8
+ { id: 'N', x: 0.5, y: 0 },
9
+ { id: 'NE', x: 1, y: 0 },
10
+ { id: 'E', x: 1, y: 0.5 },
11
+ { id: 'SE', x: 1, y: 1 },
12
+ { id: 'S', x: 0.5, y: 1 },
13
+ { id: 'SW', x: 0, y: 1 },
14
+ { id: 'W', x: 0, y: 0.5 },
15
+ { id: 'NW', x: 0, y: 0 },
16
+ ];
17
+
18
+ export function isCustomShapeTool(shape) {
19
+ return typeof shape === 'string' && shape.startsWith(CUSTOM_SHAPE_TOOL_PREFIX);
20
+ }
21
+
22
+ export function customShapeIdFromTool(shape) {
23
+ return isCustomShapeTool(shape) ? shape.slice(CUSTOM_SHAPE_TOOL_PREFIX.length) : null;
24
+ }
25
+
26
+ export function getCustomShapeDefinition(id) {
27
+ if (!id) return null;
28
+ return st.customShapeDefs && st.customShapeDefs.get(id) || null;
29
+ }
30
+
31
+ export function getCustomShapeDefaultSize(id) {
32
+ const def = getCustomShapeDefinition(id);
33
+ return [
34
+ (def && def.width) || CUSTOM_SHAPE_DEFAULT_SIZE,
35
+ (def && def.height) || CUSTOM_SHAPE_DEFAULT_SIZE,
36
+ ];
37
+ }
38
+
39
+ export function getCustomShapeAnchors(id) {
40
+ const def = getCustomShapeDefinition(id);
41
+ return def && Array.isArray(def.anchors) && def.anchors.length
42
+ ? def.anchors
43
+ : DEFAULT_CUSTOM_ANCHORS;
44
+ }
45
+
46
+ export function getCustomShapeLabelPlacement(id) {
47
+ const def = getCustomShapeDefinition(id);
48
+ return def && ['center', 'below', 'above', 'right', 'left'].includes(def.labelPlacement)
49
+ ? def.labelPlacement
50
+ : 'below';
51
+ }
52
+
53
+ export async function loadCustomShapeLibraries() {
54
+ try {
55
+ const res = await fetch('/api/shape-libraries');
56
+ if (!res.ok) throw new Error('shape libraries unavailable');
57
+ const store = await res.json();
58
+ const libraries = Array.isArray(store.libraries) ? store.libraries : [];
59
+ st.customShapeLibraries = libraries;
60
+ st.customShapeDefs = new Map();
61
+ libraries.forEach((library) => {
62
+ (library.shapes || []).forEach((shape) => st.customShapeDefs.set(shape.id, shape));
63
+ });
64
+ } catch {
65
+ st.customShapeLibraries = [];
66
+ st.customShapeDefs = new Map();
67
+ }
68
+ }
69
+
70
+ export function renderCustomShapeBar() {
71
+ const bar = document.getElementById('customShapeBar');
72
+ const body = document.getElementById('customShapeBarBody');
73
+ if (!bar || !body) return;
74
+
75
+ body.innerHTML = '';
76
+ const libraries = st.customShapeLibraries || [];
77
+ const shapes = libraries.flatMap((library) =>
78
+ (library.shapes || [])
79
+ .filter((shape) => shape.showInDiagram !== false)
80
+ .map((shape) => ({ ...shape, libraryName: library.name })),
81
+ );
82
+ bar.classList.toggle('hidden', shapes.length === 0);
83
+
84
+ shapes.forEach((shape) => {
85
+ const btn = document.createElement('button');
86
+ btn.type = 'button';
87
+ btn.className = 'custom-shape-btn';
88
+ btn.title = `${shape.libraryName || ''}${shape.libraryName ? ' · ' : ''}${shape.name}`;
89
+ btn.dataset.customShapeId = shape.id;
90
+
91
+ const img = document.createElement('img');
92
+ img.src = shape.imageSrc;
93
+ img.alt = '';
94
+ img.draggable = false;
95
+ btn.appendChild(img);
96
+
97
+ btn.addEventListener('click', () => {
98
+ window.dispatchEvent(new CustomEvent('diagram:setTool', {
99
+ detail: { tool: 'addNode', shape: `${CUSTOM_SHAPE_TOOL_PREFIX}${shape.id}` },
100
+ }));
101
+ });
102
+ body.appendChild(btn);
103
+ });
104
+ }
@@ -3,7 +3,7 @@
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, changeZOrder, activateStamp, cancelStamp, stepRotate, toggleNodeLock } from './node-panel.js';
6
+ import { showNodePanel, hideNodePanel, setNodeColor, setNodeBgOpacity, changeNodeFontSize, setTextAlign, setTextValign, setCustomShapeLabelPlacement, changeZOrder, activateStamp, cancelStamp, stepRotate, toggleNodeLock } from './node-panel.js';
7
7
  import { groupNodes, ungroupNodes } from './groups.js';
8
8
  import { showLinkPanel, hideLinkPanel } from './link-panel.js';
9
9
  import { hideEdgePanel, setEdgeArrow, setEdgeDashes, changeEdgeFontSize, stepEdgeLabelRotation, clearEdgePorts, setEdgeColor, changeEdgeWidth, toggleEdgeLock, resetEdgeLabelWidth, stepEdgeLabelOffset, resetEdgeLabelOffset } from './edge-panel.js';
@@ -22,6 +22,7 @@ import { promptImageName } from './image-name-modal.js';
22
22
  import { showToast } from './toast.js';
23
23
  import { t } from './t.js';
24
24
  import { initEvidenceMode, toggleEvidenceMode } from './evidence.js';
25
+ import { customShapeIdFromTool, isCustomShapeTool } from './custom-shapes.js';
25
26
 
26
27
  // ── Tool management ───────────────────────────────────────────────────────────
27
28
 
@@ -36,6 +37,12 @@ function setTool(tool, shape) {
36
37
  const key = tool === 'addNode' ? `addNode:${shape || st.pendingShape}` : tool;
37
38
  const btn = document.getElementById(TOOL_BTN_MAP[key]);
38
39
  if (btn) btn.classList.add('tool-active');
40
+ document.querySelectorAll('.custom-shape-btn').forEach((b) => {
41
+ b.classList.toggle(
42
+ 'tool-active',
43
+ tool === 'addNode' && isCustomShapeTool(shape || st.pendingShape) && b.dataset.customShapeId === customShapeIdFromTool(shape || st.pendingShape),
44
+ );
45
+ });
39
46
 
40
47
  document.getElementById('vis-canvas').classList.toggle('cursor-crosshair', tool === 'addNode' || tool === 'addEdge');
41
48
 
@@ -157,6 +164,8 @@ initEvidenceMode();
157
164
  document.getElementById('nodePanel').addEventListener('click', (e) => {
158
165
  const colorBtn = e.target.closest('[data-color]');
159
166
  if (colorBtn) setNodeColor(colorBtn.dataset.color);
167
+ const labelPlacementBtn = e.target.closest('[data-label-placement]');
168
+ if (labelPlacementBtn) setCustomShapeLabelPlacement(labelPlacementBtn.dataset.labelPlacement);
160
169
  });
161
170
  document.getElementById('btnNodeLock').addEventListener('click', toggleNodeLock);
162
171
  document.getElementById('btnNodeLabelEdit').addEventListener('click', startLabelEdit);