project-graph-mcp 2.2.6 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +81 -0
- package/CHANGELOG.md +57 -0
- package/README.md +9 -4
- package/package.json +6 -13
- package/src/compact/expand.js +1 -1
- package/src/core/graph-builder.js +2 -2
- package/src/core/parser.js +2 -2
- package/src/network/server.js +1 -2
- package/vendor/symbiote-node/CHANGELOG.md +31 -0
- package/vendor/symbiote-node/LICENSE +21 -0
- package/vendor/symbiote-node/README.md +206 -0
- package/vendor/symbiote-node/canvas/AutoLayout.js +725 -0
- package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.css.js +73 -0
- package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.js +93 -0
- package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.tpl.js +9 -0
- package/vendor/symbiote-node/canvas/CanvasConnectionRenderer.js +962 -0
- package/vendor/symbiote-node/canvas/ConnectionRenderer.js +1468 -0
- package/vendor/symbiote-node/canvas/FlowSimulator.js +323 -0
- package/vendor/symbiote-node/canvas/ForceLayout.js +189 -0
- package/vendor/symbiote-node/canvas/ForceWorker.js +1325 -0
- package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.css.js +97 -0
- package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.js +176 -0
- package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.tpl.js +12 -0
- package/vendor/symbiote-node/canvas/LODManager.js +88 -0
- package/vendor/symbiote-node/canvas/Minimap/Minimap.css.js +71 -0
- package/vendor/symbiote-node/canvas/Minimap/Minimap.js +207 -0
- package/vendor/symbiote-node/canvas/Minimap/Minimap.tpl.js +9 -0
- package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.css.js +261 -0
- package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.js +1840 -0
- package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.tpl.js +22 -0
- package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.css.js +97 -0
- package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.js +132 -0
- package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.tpl.js +21 -0
- package/vendor/symbiote-node/canvas/NodeViewManager.js +584 -0
- package/vendor/symbiote-node/canvas/PinExpansion.js +131 -0
- package/vendor/symbiote-node/canvas/PseudoConnection.js +80 -0
- package/vendor/symbiote-node/canvas/SubgraphManager.js +201 -0
- package/vendor/symbiote-node/canvas/SubgraphRouter.js +443 -0
- package/vendor/symbiote-node/canvas/ViewportActions.js +446 -0
- package/vendor/symbiote-node/core/Connection.js +45 -0
- package/vendor/symbiote-node/core/Editor.js +451 -0
- package/vendor/symbiote-node/core/Frame.js +31 -0
- package/vendor/symbiote-node/core/GraphMermaid.js +348 -0
- package/vendor/symbiote-node/core/GraphText.js +210 -0
- package/vendor/symbiote-node/core/Node.js +143 -0
- package/vendor/symbiote-node/core/Portal.js +104 -0
- package/vendor/symbiote-node/core/Socket.js +185 -0
- package/vendor/symbiote-node/core/SubgraphNode.js +125 -0
- package/vendor/symbiote-node/index.js +103 -0
- package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.css.js +361 -0
- package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.js +332 -0
- package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.tpl.js +96 -0
- package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.css.js +104 -0
- package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.js +133 -0
- package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.tpl.js +33 -0
- package/vendor/symbiote-node/interactions/ConnectFlow.js +307 -0
- package/vendor/symbiote-node/interactions/Drag.js +102 -0
- package/vendor/symbiote-node/interactions/Selector.js +132 -0
- package/vendor/symbiote-node/interactions/SnapGrid.js +65 -0
- package/vendor/symbiote-node/interactions/Zoom.js +140 -0
- package/vendor/symbiote-node/layout/ActionZone/ActionZone.css.js +88 -0
- package/vendor/symbiote-node/layout/ActionZone/ActionZone.js +254 -0
- package/vendor/symbiote-node/layout/ActionZone/ActionZone.tpl.js +11 -0
- package/vendor/symbiote-node/layout/Layout/Layout.css.js +88 -0
- package/vendor/symbiote-node/layout/Layout/Layout.js +622 -0
- package/vendor/symbiote-node/layout/Layout/Layout.tpl.js +25 -0
- package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.css.js +293 -0
- package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.js +467 -0
- package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.tpl.js +33 -0
- package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.css.js +46 -0
- package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.js +102 -0
- package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.tpl.js +6 -0
- package/vendor/symbiote-node/layout/LayoutRouter/LayoutRouter.js +156 -0
- package/vendor/symbiote-node/layout/LayoutRouter/routerSync.js +250 -0
- package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.css.js +379 -0
- package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.js +263 -0
- package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.tpl.js +20 -0
- package/vendor/symbiote-node/layout/LayoutSidebar/SidebarSection.js +183 -0
- package/vendor/symbiote-node/layout/LayoutTree.js +246 -0
- package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.css.js +43 -0
- package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.js +89 -0
- package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.tpl.js +14 -0
- package/vendor/symbiote-node/layout/index.js +16 -0
- package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.css.js +61 -0
- package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.js +79 -0
- package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.tpl.js +19 -0
- package/vendor/symbiote-node/node/CtrlItem/CtrlItem.css.js +41 -0
- package/vendor/symbiote-node/node/CtrlItem/CtrlItem.js +24 -0
- package/vendor/symbiote-node/node/CtrlItem/CtrlItem.tpl.js +16 -0
- package/vendor/symbiote-node/node/GraphFrame/GraphFrame.css.js +65 -0
- package/vendor/symbiote-node/node/GraphFrame/GraphFrame.js +29 -0
- package/vendor/symbiote-node/node/GraphFrame/GraphFrame.tpl.js +13 -0
- package/vendor/symbiote-node/node/GraphNode/GraphNode.css.js +683 -0
- package/vendor/symbiote-node/node/GraphNode/GraphNode.js +92 -0
- package/vendor/symbiote-node/node/GraphNode/GraphNode.tpl.js +17 -0
- package/vendor/symbiote-node/node/NodeSocket/NodeSocket.js +25 -0
- package/vendor/symbiote-node/node/NodeSocket/NodeSocket.tpl.js +7 -0
- package/vendor/symbiote-node/node/PortItem/PortItem.css.js +90 -0
- package/vendor/symbiote-node/node/PortItem/PortItem.js +87 -0
- package/vendor/symbiote-node/node/PortItem/PortItem.tpl.js +10 -0
- package/vendor/symbiote-node/package.json +59 -0
- package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.css.js +143 -0
- package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.js +131 -0
- package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.tpl.js +16 -0
- package/vendor/symbiote-node/plugins/History.js +384 -0
- package/vendor/symbiote-node/plugins/Readonly.js +59 -0
- package/vendor/symbiote-node/shapes/CircleShape.js +80 -0
- package/vendor/symbiote-node/shapes/CommentShape.js +35 -0
- package/vendor/symbiote-node/shapes/DiamondShape.js +115 -0
- package/vendor/symbiote-node/shapes/NodeShape.js +80 -0
- package/vendor/symbiote-node/shapes/PillShape.js +91 -0
- package/vendor/symbiote-node/shapes/RectShape.js +72 -0
- package/vendor/symbiote-node/shapes/SVGShape.js +494 -0
- package/vendor/symbiote-node/shapes/index.js +53 -0
- package/vendor/symbiote-node/themes/Palette.js +32 -0
- package/vendor/symbiote-node/themes/Skin.js +113 -0
- package/vendor/symbiote-node/themes/Theme.js +84 -0
- package/vendor/symbiote-node/themes/carbon.js +137 -0
- package/vendor/symbiote-node/themes/dark.js +137 -0
- package/vendor/symbiote-node/themes/ebook.js +138 -0
- package/vendor/symbiote-node/themes/grey.js +137 -0
- package/vendor/symbiote-node/themes/light.js +137 -0
- package/vendor/symbiote-node/themes/neon.js +138 -0
- package/vendor/symbiote-node/themes/pcb.js +273 -0
- package/vendor/symbiote-node/themes/synthwave.js +137 -0
- package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.css.js +86 -0
- package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.js +128 -0
- package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.tpl.js +29 -0
- package/web/app.js +6 -5
- package/web/components/canvas-graph.js +1666 -0
- package/web/components/event-feed/CodeWidget.js +32 -0
- package/web/components/event-feed/EventWidget.js +97 -0
- package/web/components/event-feed/ListWidget.js +57 -0
- package/web/components/event-feed/MiniGraphWidget.js +69 -0
- package/web/dashboard.js +1 -1
- package/web/index.html +4 -0
- package/web/panels/ActionBoard/ActionBoard.js +1 -1
- package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -1
- package/web/panels/code-viewer.js +50 -15
- package/web/panels/dep-graph.js +2712 -7
- package/web/panels/file-tree.js +5 -2
- package/web/panels/live-monitor.js +75 -3
- package/web/style.css +33 -0
- package/docs/img/explorer-compact.jpg +0 -0
- package/docs/img/explorer-expanded.jpg +0 -0
- package/src/.contextignore +0 -22
- package/src/.project-graph-cache.json +0 -1
- package/src/compact/.project-graph-cache.json +0 -1
- package/web/.project-graph-cache.json +0 -1
- package/web/panels/SettingsPanel/.project-graph-cache.json +0 -1
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SVGShape — universal shape from any SVG path
|
|
3
|
+
*
|
|
4
|
+
* Uses SVGPathElement.getPointAtLength() for dynamic connector placement.
|
|
5
|
+
* Any SVG icon path can become a node shape — the outer contour defines
|
|
6
|
+
* the visual fill and connector positions are computed along the perimeter.
|
|
7
|
+
*
|
|
8
|
+
* Port placement strategy:
|
|
9
|
+
* - Inputs placed on left portion of path perimeter
|
|
10
|
+
* - Outputs placed on right portion of path perimeter
|
|
11
|
+
* - Connector angle = normal from center → edge point
|
|
12
|
+
* - Aspect ratio of original SVG is preserved (xMidYMid meet)
|
|
13
|
+
*
|
|
14
|
+
* @module symbiote-node/shapes/SVGShape
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { NodeShape } from './NodeShape.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Offscreen SVG namespace for path computation
|
|
21
|
+
* @type {SVGSVGElement|null}
|
|
22
|
+
*/
|
|
23
|
+
let _offscreenSVG = null;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get or create an offscreen SVG element for path calculations
|
|
27
|
+
* @returns {SVGSVGElement}
|
|
28
|
+
*/
|
|
29
|
+
function getOffscreenSVG() {
|
|
30
|
+
if (_offscreenSVG) return _offscreenSVG;
|
|
31
|
+
if (typeof document === 'undefined') return null;
|
|
32
|
+
_offscreenSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
33
|
+
_offscreenSVG.style.position = 'absolute';
|
|
34
|
+
_offscreenSVG.style.width = '0';
|
|
35
|
+
_offscreenSVG.style.height = '0';
|
|
36
|
+
_offscreenSVG.style.overflow = 'hidden';
|
|
37
|
+
document.body.appendChild(_offscreenSVG);
|
|
38
|
+
return _offscreenSVG;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Compute scaling params for viewBox → element mapping
|
|
43
|
+
* Preserves aspect ratio (equivalent to SVG preserveAspectRatio="xMidYMid meet")
|
|
44
|
+
*
|
|
45
|
+
* @param {number[]} vb - [x, y, w, h] viewBox
|
|
46
|
+
* @param {{ width: number, height: number }} size - element size
|
|
47
|
+
* @returns {{ scale: number, offsetX: number, offsetY: number }}
|
|
48
|
+
*/
|
|
49
|
+
function computeMapping(vb, size) {
|
|
50
|
+
const [vx, vy, vw, vh] = vb;
|
|
51
|
+
const scale = Math.min(size.width / vw, size.height / vh);
|
|
52
|
+
const renderedW = vw * scale;
|
|
53
|
+
const renderedH = vh * scale;
|
|
54
|
+
return {
|
|
55
|
+
scale,
|
|
56
|
+
offsetX: (size.width - renderedW) / 2 - vx * scale,
|
|
57
|
+
offsetY: (size.height - renderedH) / 2 - vy * scale,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class SVGShape extends NodeShape {
|
|
62
|
+
/** @type {string} */
|
|
63
|
+
name;
|
|
64
|
+
|
|
65
|
+
/** @type {string} - SVG path d attribute */
|
|
66
|
+
pathData;
|
|
67
|
+
|
|
68
|
+
/** @type {string} - viewBox of original SVG */
|
|
69
|
+
viewBox;
|
|
70
|
+
|
|
71
|
+
/** @type {number[]} - parsed viewBox [x, y, w, h] */
|
|
72
|
+
#vb;
|
|
73
|
+
|
|
74
|
+
/** @type {boolean} - whether shape has standard header */
|
|
75
|
+
#header;
|
|
76
|
+
|
|
77
|
+
/** @type {{ minWidth: number, minHeight: number }} */
|
|
78
|
+
#minSize;
|
|
79
|
+
|
|
80
|
+
/** @type {Map<string, {x:number,y:number,angle:number}>} - position cache */
|
|
81
|
+
#posCache = new Map();
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {string} name - Shape identifier
|
|
85
|
+
* @param {object} options
|
|
86
|
+
* @param {string} options.pathData - SVG path d attribute
|
|
87
|
+
* @param {string} [options.viewBox='0 0 24 24'] - Original SVG viewBox
|
|
88
|
+
* @param {boolean} [options.header=false] - Show header/controls
|
|
89
|
+
* @param {{ minWidth?: number, minHeight?: number }} [options.minSize]
|
|
90
|
+
*/
|
|
91
|
+
constructor(name, { pathData, viewBox = '0 0 24 24', header = false, minSize = {} }) {
|
|
92
|
+
super();
|
|
93
|
+
this.name = name;
|
|
94
|
+
this.pathData = pathData;
|
|
95
|
+
this.viewBox = viewBox;
|
|
96
|
+
this.#vb = viewBox.split(' ').map(Number);
|
|
97
|
+
this.#header = header;
|
|
98
|
+
this.#minSize = {
|
|
99
|
+
minWidth: minSize.minWidth || 100,
|
|
100
|
+
minHeight: minSize.minHeight || 100,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Scale a point from viewBox coordinates to element pixel coordinates
|
|
106
|
+
* Uses aspect-ratio-preserving mapping (xMidYMid meet)
|
|
107
|
+
*
|
|
108
|
+
* @param {number} px - x in viewBox
|
|
109
|
+
* @param {number} py - y in viewBox
|
|
110
|
+
* @param {{ width: number, height: number }} size - element size
|
|
111
|
+
* @returns {{ x: number, y: number }}
|
|
112
|
+
*/
|
|
113
|
+
#scalePoint(px, py, size) {
|
|
114
|
+
const { scale, offsetX, offsetY } = computeMapping(this.#vb, size);
|
|
115
|
+
return {
|
|
116
|
+
x: px * scale + offsetX,
|
|
117
|
+
y: py * scale + offsetY,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get center of SVG shape in viewBox coordinates
|
|
123
|
+
* @returns {{ x: number, y: number }}
|
|
124
|
+
*/
|
|
125
|
+
#getCenter() {
|
|
126
|
+
const [vx, vy, vw, vh] = this.#vb;
|
|
127
|
+
return { x: vx + vw / 2, y: vy + vh / 2 };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get a path element for computations (uses offscreen SVG)
|
|
132
|
+
* @returns {SVGPathElement|null}
|
|
133
|
+
*/
|
|
134
|
+
#getPathElement() {
|
|
135
|
+
const svg = getOffscreenSVG();
|
|
136
|
+
if (!svg) return null;
|
|
137
|
+
let pathEl = svg.querySelector(`[data-shape="${this.name}"]`);
|
|
138
|
+
if (!pathEl) {
|
|
139
|
+
pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
140
|
+
pathEl.setAttribute('d', this.pathData);
|
|
141
|
+
pathEl.setAttribute('data-shape', this.name);
|
|
142
|
+
svg.appendChild(pathEl);
|
|
143
|
+
}
|
|
144
|
+
return pathEl;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Find the point on the SVG path that a ray from center at a given angle hits.
|
|
149
|
+
* Uses dense sampling along the path perimeter and finds the point
|
|
150
|
+
* whose angle from center best matches the target angle.
|
|
151
|
+
*
|
|
152
|
+
* @param {number} targetAngle - angle in radians from center
|
|
153
|
+
* @param {SVGPathElement} pathEl
|
|
154
|
+
* @returns {{ x: number, y: number }} - point in viewBox coordinates
|
|
155
|
+
*/
|
|
156
|
+
#findPointAtAngle(targetAngle, pathEl) {
|
|
157
|
+
const totalLen = pathEl.getTotalLength();
|
|
158
|
+
const center = this.#getCenter();
|
|
159
|
+
|
|
160
|
+
// Phase 1: coarse scan (128 samples)
|
|
161
|
+
let bestDist = Infinity;
|
|
162
|
+
let bestLen = 0;
|
|
163
|
+
const COARSE = 128;
|
|
164
|
+
|
|
165
|
+
for (let i = 0; i <= COARSE; i++) {
|
|
166
|
+
const len = (totalLen * i) / COARSE;
|
|
167
|
+
const pt = pathEl.getPointAtLength(len);
|
|
168
|
+
const angle = Math.atan2(pt.y - center.y, pt.x - center.x);
|
|
169
|
+
let diff = Math.abs(angle - targetAngle);
|
|
170
|
+
if (diff > Math.PI) diff = 2 * Math.PI - diff;
|
|
171
|
+
if (diff < bestDist) {
|
|
172
|
+
bestDist = diff;
|
|
173
|
+
bestLen = len;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Phase 2: refine with binary-like search around bestLen
|
|
178
|
+
const searchRadius = totalLen / COARSE;
|
|
179
|
+
const FINE = 32;
|
|
180
|
+
const startLen = Math.max(0, bestLen - searchRadius);
|
|
181
|
+
const endLen = Math.min(totalLen, bestLen + searchRadius);
|
|
182
|
+
|
|
183
|
+
for (let i = 0; i <= FINE; i++) {
|
|
184
|
+
const len = startLen + ((endLen - startLen) * i) / FINE;
|
|
185
|
+
const pt = pathEl.getPointAtLength(len);
|
|
186
|
+
const angle = Math.atan2(pt.y - center.y, pt.x - center.x);
|
|
187
|
+
let diff = Math.abs(angle - targetAngle);
|
|
188
|
+
if (diff > Math.PI) diff = 2 * Math.PI - diff;
|
|
189
|
+
if (diff < bestDist) {
|
|
190
|
+
bestDist = diff;
|
|
191
|
+
bestLen = len;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return pathEl.getPointAtLength(bestLen);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get socket position on the shape outline.
|
|
200
|
+
* Results are cached — same params always return same position.
|
|
201
|
+
*
|
|
202
|
+
* @param {'input'|'output'} side
|
|
203
|
+
* @param {number} index - ordinal index of this port
|
|
204
|
+
* @param {number} total - total ports on this side
|
|
205
|
+
* @param {{ width: number, height: number }} size - node dimensions
|
|
206
|
+
* @returns {{ x: number, y: number, angle: number }}
|
|
207
|
+
*/
|
|
208
|
+
getSocketPosition(side, index, total, size) {
|
|
209
|
+
// Cache key: position depends on side, index, total, and element size
|
|
210
|
+
const key = `${side}|${index}|${total}|${size.width}|${size.height}`;
|
|
211
|
+
if (this.#posCache.has(key)) return this.#posCache.get(key);
|
|
212
|
+
|
|
213
|
+
const pathEl = this.#getPathElement();
|
|
214
|
+
|
|
215
|
+
if (!pathEl) {
|
|
216
|
+
const y = size.height * (index + 1) / (total + 1);
|
|
217
|
+
const result = side === 'input'
|
|
218
|
+
? { x: 0, y, angle: 180 }
|
|
219
|
+
: { x: size.width, y, angle: 0 };
|
|
220
|
+
this.#posCache.set(key, result);
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Distribute ports along the relevant side of the path perimeter
|
|
225
|
+
const centerAngle = side === 'input' ? Math.PI : 0;
|
|
226
|
+
const arcSpan = Math.PI * 0.6; // 108° spread
|
|
227
|
+
let targetAngle;
|
|
228
|
+
|
|
229
|
+
if (total === 1) {
|
|
230
|
+
targetAngle = centerAngle;
|
|
231
|
+
} else {
|
|
232
|
+
const startAngle = centerAngle - arcSpan / 2;
|
|
233
|
+
const step = arcSpan / (total - 1);
|
|
234
|
+
targetAngle = startAngle + step * index;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const pt = this.#findPointAtAngle(targetAngle, pathEl);
|
|
238
|
+
const scaled = this.#scalePoint(pt.x, pt.y, size);
|
|
239
|
+
|
|
240
|
+
// Compute outward surface normal (same as getEdgePoint)
|
|
241
|
+
const totalLen = pathEl.getTotalLength();
|
|
242
|
+
const center = this.#getCenter();
|
|
243
|
+
let bestLen = 0, bestDist = Infinity;
|
|
244
|
+
const SCAN = 128;
|
|
245
|
+
for (let i = 0; i <= SCAN; i++) {
|
|
246
|
+
const len = (totalLen * i) / SCAN;
|
|
247
|
+
const p = pathEl.getPointAtLength(len);
|
|
248
|
+
const dist = (p.x - pt.x) ** 2 + (p.y - pt.y) ** 2;
|
|
249
|
+
if (dist < bestDist) { bestDist = dist; bestLen = len; }
|
|
250
|
+
}
|
|
251
|
+
const delta = 0.5;
|
|
252
|
+
const prevPt = pathEl.getPointAtLength(Math.max(0, bestLen - delta));
|
|
253
|
+
const nextPt = pathEl.getPointAtLength(Math.min(totalLen, bestLen + delta));
|
|
254
|
+
const tx = nextPt.x - prevPt.x, ty = nextPt.y - prevPt.y;
|
|
255
|
+
let nx = -ty, ny = tx;
|
|
256
|
+
const radX = pt.x - center.x, radY = pt.y - center.y;
|
|
257
|
+
if (nx * radX + ny * radY < 0) { nx = ty; ny = -tx; }
|
|
258
|
+
const angleDeg = Math.atan2(ny, nx) * 180 / Math.PI;
|
|
259
|
+
|
|
260
|
+
const result = { x: scaled.x, y: scaled.y, angle: angleDeg };
|
|
261
|
+
this.#posCache.set(key, result);
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Get edge point at a specific angle (direction toward target node).
|
|
267
|
+
* Used for dynamic connectors that slide along the perimeter.
|
|
268
|
+
*
|
|
269
|
+
* @param {number} angle - angle in radians from center to target
|
|
270
|
+
* @param {{ width: number, height: number }} size - element dimensions
|
|
271
|
+
* @returns {{ x: number, y: number, angle: number }}
|
|
272
|
+
*/
|
|
273
|
+
getEdgePoint(angle, size) {
|
|
274
|
+
// Round to 3 decimal places (~0.06° precision, invisible jitter)
|
|
275
|
+
const rounded = Math.round(angle * 1000) / 1000;
|
|
276
|
+
const key = `edge|${rounded}|${size.width}|${size.height}`;
|
|
277
|
+
if (this.#posCache.has(key)) return this.#posCache.get(key);
|
|
278
|
+
|
|
279
|
+
const pathEl = this.#getPathElement();
|
|
280
|
+
if (!pathEl) {
|
|
281
|
+
const cx = size.width / 2;
|
|
282
|
+
const cy = size.height / 2;
|
|
283
|
+
return { x: cx + Math.cos(angle) * cx, y: cy + Math.sin(angle) * cy, angle: angle * 180 / Math.PI };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const pt = this.#findPointAtAngle(angle, pathEl);
|
|
287
|
+
const scaled = this.#scalePoint(pt.x, pt.y, size);
|
|
288
|
+
|
|
289
|
+
// Compute outward surface normal (perpendicular to edge), NOT radial angle.
|
|
290
|
+
// For polygons, the radial angle varies across a flat edge,
|
|
291
|
+
// but the surface normal is constant — this gives correct perpendicular stubs.
|
|
292
|
+
const totalLen = pathEl.getTotalLength();
|
|
293
|
+
const center = this.#getCenter();
|
|
294
|
+
|
|
295
|
+
// Find the path length for this point (re-use findPointAtAngle's logic)
|
|
296
|
+
let bestLen = 0, bestDist = Infinity;
|
|
297
|
+
const SCAN = 128;
|
|
298
|
+
for (let i = 0; i <= SCAN; i++) {
|
|
299
|
+
const len = (totalLen * i) / SCAN;
|
|
300
|
+
const p = pathEl.getPointAtLength(len);
|
|
301
|
+
const dist = (p.x - pt.x) ** 2 + (p.y - pt.y) ** 2;
|
|
302
|
+
if (dist < bestDist) { bestDist = dist; bestLen = len; }
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Sample neighbors to get tangent vector
|
|
306
|
+
const delta = 0.5; // small step along path
|
|
307
|
+
const prevLen = Math.max(0, bestLen - delta);
|
|
308
|
+
const nextLen = Math.min(totalLen, bestLen + delta);
|
|
309
|
+
const prevPt = pathEl.getPointAtLength(prevLen);
|
|
310
|
+
const nextPt = pathEl.getPointAtLength(nextLen);
|
|
311
|
+
|
|
312
|
+
// Tangent = direction along edge
|
|
313
|
+
const tx = nextPt.x - prevPt.x;
|
|
314
|
+
const ty = nextPt.y - prevPt.y;
|
|
315
|
+
|
|
316
|
+
// Normal = tangent rotated 90° (two candidates: +90° and -90°)
|
|
317
|
+
// Pick the one pointing outward (away from center)
|
|
318
|
+
let nx = -ty, ny = tx; // candidate 1: rotate -90°
|
|
319
|
+
const radX = pt.x - center.x;
|
|
320
|
+
const radY = pt.y - center.y;
|
|
321
|
+
// Dot product with radial vector — if negative, flip
|
|
322
|
+
if (nx * radX + ny * radY < 0) {
|
|
323
|
+
nx = ty; ny = -tx; // candidate 2: rotate +90°
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const angleDeg = Math.atan2(ny, nx) * 180 / Math.PI;
|
|
327
|
+
|
|
328
|
+
const result = { x: scaled.x, y: scaled.y, angle: angleDeg };
|
|
329
|
+
|
|
330
|
+
// Bounded cache: evict oldest when exceeding 360 entries
|
|
331
|
+
if (this.#posCache.size > 360) {
|
|
332
|
+
const first = this.#posCache.keys().next().value;
|
|
333
|
+
this.#posCache.delete(first);
|
|
334
|
+
}
|
|
335
|
+
this.#posCache.set(key, result);
|
|
336
|
+
return result;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Get a pin position on a specific side of the shape.
|
|
341
|
+
*
|
|
342
|
+
* Side-based placement: pins are placed along a side edge (top/right/bottom/left).
|
|
343
|
+
* The position within the side is controlled by t (0 = start, 1 = end).
|
|
344
|
+
*
|
|
345
|
+
* @param {'top'|'right'|'bottom'|'left'} side - which side to place pin on
|
|
346
|
+
* @param {number} t - position along the side (0..1), 0.5 = center
|
|
347
|
+
* @param {{ width: number, height: number }} size - element dimensions
|
|
348
|
+
* @returns {{ x: number, y: number, angle: number }} - angle is outward normal in degrees
|
|
349
|
+
*/
|
|
350
|
+
getSidePosition(side, t, size) {
|
|
351
|
+
const key = `side|${side}|${(t * 100) | 0}|${size.width}|${size.height}`;
|
|
352
|
+
if (this.#posCache.has(key)) return this.#posCache.get(key);
|
|
353
|
+
|
|
354
|
+
// Outward normal angles for each side (screen coords: Y down)
|
|
355
|
+
const NORMALS = { top: -90, right: 0, bottom: 90, left: 180 };
|
|
356
|
+
const angleDeg = NORMALS[side];
|
|
357
|
+
|
|
358
|
+
// For shapes with pathData: sample points on the side's angular range
|
|
359
|
+
// and interpolate along the edge
|
|
360
|
+
const pathEl = this.#getPathElement();
|
|
361
|
+
if (pathEl) {
|
|
362
|
+
// Angular ranges for each side (radians, screen coords)
|
|
363
|
+
// Generous ranges that cover the flat + diagonal edges
|
|
364
|
+
const RANGES = {
|
|
365
|
+
right: { from: -Math.PI / 4, to: Math.PI / 4 },
|
|
366
|
+
bottom: { from: Math.PI / 4, to: 3 * Math.PI / 4 },
|
|
367
|
+
left: { from: 3 * Math.PI / 4, to: 5 * Math.PI / 4 },
|
|
368
|
+
top: { from: -3 * Math.PI / 4, to: -Math.PI / 4 },
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const range = RANGES[side];
|
|
372
|
+
// Use side-level caching for points to prevent SVG DOM explosion
|
|
373
|
+
const sideKey = `sidepts|${side}|${size.width}|${size.height}`;
|
|
374
|
+
let sidePoints;
|
|
375
|
+
|
|
376
|
+
if (this.#posCache.has(sideKey)) {
|
|
377
|
+
sidePoints = this.#posCache.get(sideKey);
|
|
378
|
+
} else {
|
|
379
|
+
sidePoints = [];
|
|
380
|
+
const SAMPLES = 16;
|
|
381
|
+
for (let i = 0; i <= SAMPLES; i++) {
|
|
382
|
+
const a = range.from + (range.to - range.from) * (i / SAMPLES);
|
|
383
|
+
const pt = this.#findPointAtAngle(a, pathEl);
|
|
384
|
+
const sp = this.#scalePoint(pt.x, pt.y, size);
|
|
385
|
+
sidePoints.push(sp);
|
|
386
|
+
}
|
|
387
|
+
this.#posCache.set(sideKey, sidePoints);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Use inset range (20%-80%) to avoid placing on corners
|
|
391
|
+
const MARGIN = 0.2;
|
|
392
|
+
const effectiveT = MARGIN + t * (1 - 2 * MARGIN);
|
|
393
|
+
const idx = effectiveT * (sidePoints.length - 1);
|
|
394
|
+
const lo = Math.floor(idx);
|
|
395
|
+
const hi = Math.min(lo + 1, sidePoints.length - 1);
|
|
396
|
+
const frac = idx - lo;
|
|
397
|
+
|
|
398
|
+
const x = sidePoints[lo].x + (sidePoints[hi].x - sidePoints[lo].x) * frac;
|
|
399
|
+
const y = sidePoints[lo].y + (sidePoints[hi].y - sidePoints[lo].y) * frac;
|
|
400
|
+
|
|
401
|
+
const result = { x, y, angle: angleDeg };
|
|
402
|
+
this.#posCache.set(key, result);
|
|
403
|
+
return result;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Fallback for shapes without path: rectangle approximation
|
|
407
|
+
let x, y;
|
|
408
|
+
switch (side) {
|
|
409
|
+
case 'top': x = size.width * (0.2 + t * 0.6); y = 0; break;
|
|
410
|
+
case 'right': x = size.width; y = size.height * (0.2 + t * 0.6); break;
|
|
411
|
+
case 'bottom': x = size.width * (0.2 + t * 0.6); y = size.height; break;
|
|
412
|
+
case 'left': x = 0; y = size.height * (0.2 + t * 0.6); break;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const result = { x, y, angle: angleDeg };
|
|
416
|
+
this.#posCache.set(key, result);
|
|
417
|
+
return result;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
getClipPath(size) {
|
|
421
|
+
return null; // We use SVG background layer instead of clip-path
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
getOutlinePath(size) {
|
|
425
|
+
return this.pathData;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
getBorderRadius() {
|
|
429
|
+
return '0';
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
get hasHeader() {
|
|
433
|
+
return this.#header;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
get hasControls() {
|
|
437
|
+
return this.#header;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
getMinSize() {
|
|
441
|
+
return this.#minSize;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// --- Preset SVG shapes (Material Symbols paths) ---
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Register an SVG shape from a path string
|
|
449
|
+
* @param {string} name
|
|
450
|
+
* @param {string} pathData - SVG d attribute
|
|
451
|
+
* @param {object} [options]
|
|
452
|
+
*/
|
|
453
|
+
export function createSVGShape(name, pathData, options = {}) {
|
|
454
|
+
return new SVGShape(name, { pathData, ...options });
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Common icon paths from Material Symbols (24x24 viewBox)
|
|
458
|
+
export const SVG_PRESETS = {
|
|
459
|
+
// Hexagon
|
|
460
|
+
hexagon: 'M12 2L22 8.5V15.5L12 22L2 15.5V8.5Z',
|
|
461
|
+
|
|
462
|
+
// Pentagon
|
|
463
|
+
pentagon: 'M12 2L22 9.27L18.18 21H5.82L2 9.27Z',
|
|
464
|
+
|
|
465
|
+
// Star (5-pointed)
|
|
466
|
+
star: 'M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26Z',
|
|
467
|
+
|
|
468
|
+
// Cloud
|
|
469
|
+
cloud: 'M19.35 10.04A7.49 7.49 0 0012 4C9.11 4 6.6 5.64 5.35 8.04A5.994 5.994 0 000 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96z',
|
|
470
|
+
|
|
471
|
+
// Shield
|
|
472
|
+
shield: 'M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4z',
|
|
473
|
+
|
|
474
|
+
// Octagon
|
|
475
|
+
octagon: 'M7.86 2H16.14L22 7.86V16.14L16.14 22H7.86L2 16.14V7.86Z',
|
|
476
|
+
|
|
477
|
+
// Parallelogram
|
|
478
|
+
parallelogram: 'M6 2H22L18 22H2Z',
|
|
479
|
+
|
|
480
|
+
// Trapezoid
|
|
481
|
+
trapezoid: 'M4 22H20L23 2H1Z',
|
|
482
|
+
|
|
483
|
+
// Cylinder (approximation)
|
|
484
|
+
cylinder: 'M4 6C4 4 8 2 12 2S20 4 20 6V18C20 20 16 22 12 22S4 20 4 18Z',
|
|
485
|
+
|
|
486
|
+
// Database
|
|
487
|
+
database: 'M12 3C7.58 3 4 4.79 4 7V17C4 19.21 7.59 21 12 21S20 19.21 20 17V7C20 4.79 16.42 3 12 3Z',
|
|
488
|
+
|
|
489
|
+
// Lightning bolt
|
|
490
|
+
bolt: 'M7 2V13H10V22L17 10H13L17 2Z',
|
|
491
|
+
|
|
492
|
+
// Heart
|
|
493
|
+
heart: 'M12 21.35L10.55 20.03C5.4 15.36 2 12.28 2 8.5C2 5.42 4.42 3 7.5 3C9.24 3 10.91 3.81 12 5.09C13.09 3.81 14.76 3 16.5 3C19.58 3 22 5.42 22 8.5C22 12.28 18.6 15.36 13.45 20.04L12 21.35Z',
|
|
494
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shape registry — maps shape names to implementations
|
|
3
|
+
* @module symbiote-node/shapes/index
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { NodeShape } from './NodeShape.js';
|
|
7
|
+
import { RectShape } from './RectShape.js';
|
|
8
|
+
import { PillShape } from './PillShape.js';
|
|
9
|
+
import { CircleShape } from './CircleShape.js';
|
|
10
|
+
import { DiamondShape } from './DiamondShape.js';
|
|
11
|
+
import { CommentShape } from './CommentShape.js';
|
|
12
|
+
import { SVGShape, createSVGShape, SVG_PRESETS } from './SVGShape.js';
|
|
13
|
+
|
|
14
|
+
/** @type {Map<string, NodeShape>} */
|
|
15
|
+
const registry = new Map();
|
|
16
|
+
|
|
17
|
+
// Register built-in shapes
|
|
18
|
+
const RECT = new RectShape();
|
|
19
|
+
const PILL = new PillShape();
|
|
20
|
+
const CIRCLE = new CircleShape();
|
|
21
|
+
const DIAMOND = new DiamondShape();
|
|
22
|
+
const COMMENT = new CommentShape();
|
|
23
|
+
|
|
24
|
+
registry.set('rect', RECT);
|
|
25
|
+
registry.set('pill', PILL);
|
|
26
|
+
registry.set('circle', CIRCLE);
|
|
27
|
+
registry.set('diamond', DIAMOND);
|
|
28
|
+
registry.set('comment', COMMENT);
|
|
29
|
+
|
|
30
|
+
// Register SVG preset shapes
|
|
31
|
+
for (const [name, pathData] of Object.entries(SVG_PRESETS)) {
|
|
32
|
+
registry.set(name, createSVGShape(name, pathData));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get shape by name
|
|
37
|
+
* @param {string} name
|
|
38
|
+
* @returns {NodeShape}
|
|
39
|
+
*/
|
|
40
|
+
export function getShape(name) {
|
|
41
|
+
return registry.get(name) || RECT;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Register custom shape
|
|
46
|
+
* @param {string} name
|
|
47
|
+
* @param {NodeShape} shape
|
|
48
|
+
*/
|
|
49
|
+
export function registerShape(name, shape) {
|
|
50
|
+
registry.set(name, shape);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { NodeShape, RectShape, PillShape, CircleShape, DiamondShape, CommentShape, SVGShape, createSVGShape, SVG_PRESETS };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Palette — color-only design tokens
|
|
3
|
+
*
|
|
4
|
+
* Contains only chromatic properties: backgrounds, text colors,
|
|
5
|
+
* accents, category colors, connection colors.
|
|
6
|
+
* Separated from geometry (Skin) for independent swapping.
|
|
7
|
+
*
|
|
8
|
+
* @module symbiote-node/themes/Palette
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Object} PaletteDefinition
|
|
13
|
+
* @property {string} name
|
|
14
|
+
* @property {Object<string, string>} colors
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Re-export all built-in palettes
|
|
18
|
+
export { DARK_PALETTE } from './dark.js';
|
|
19
|
+
export { LIGHT_PALETTE } from './light.js';
|
|
20
|
+
export { SYNTHWAVE_PALETTE } from './synthwave.js';
|
|
21
|
+
export { GREY_PALETTE } from './grey.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Apply palette to element
|
|
25
|
+
* @param {HTMLElement} element
|
|
26
|
+
* @param {PaletteDefinition} palette
|
|
27
|
+
*/
|
|
28
|
+
export function applyPalette(element, palette) {
|
|
29
|
+
for (const [key, value] of Object.entries(palette.colors)) {
|
|
30
|
+
element.style.setProperty(key, value);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skin — geometry-only design tokens
|
|
3
|
+
*
|
|
4
|
+
* Built on a T-shirt spacing scale: change the scale values and all
|
|
5
|
+
* semantic tokens (radius, sockets, grid) update harmoniously.
|
|
6
|
+
* Shadow is split into geometry (here) and color (from Palette).
|
|
7
|
+
*
|
|
8
|
+
* @module symbiote-node/themes/Skin
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Object} SkinDefinition
|
|
13
|
+
* @property {string} name
|
|
14
|
+
* @property {Object<string, string>} geometry
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** @type {SkinDefinition} */
|
|
18
|
+
export const MODERN_SKIN = {
|
|
19
|
+
name: 'modern',
|
|
20
|
+
geometry: {
|
|
21
|
+
// === Spacing scale (atomic — AI changes these) ===
|
|
22
|
+
'--sn-space-xs': '4px',
|
|
23
|
+
'--sn-space-sm': '8px',
|
|
24
|
+
'--sn-space-md': '12px',
|
|
25
|
+
'--sn-space-lg': '16px',
|
|
26
|
+
'--sn-space-xl': '24px',
|
|
27
|
+
|
|
28
|
+
// === Semantic geometry (references scale) ===
|
|
29
|
+
'--sn-node-radius': 'var(--sn-space-md)',
|
|
30
|
+
'--sn-comment-radius': 'var(--sn-space-sm)',
|
|
31
|
+
'--sn-socket-size': 'var(--sn-space-md)',
|
|
32
|
+
'--sn-socket-border-width': '2px',
|
|
33
|
+
'--sn-grid-size': 'var(--sn-space-xl)',
|
|
34
|
+
'--sn-conn-width': '2',
|
|
35
|
+
|
|
36
|
+
// === Typography ===
|
|
37
|
+
'--sn-font': "'Inter', sans-serif",
|
|
38
|
+
'--sn-font-size': '13px',
|
|
39
|
+
|
|
40
|
+
// === Shadow geometry (color comes from Palette) ===
|
|
41
|
+
'--sn-shadow-geometry': '0 4px 16px',
|
|
42
|
+
'--sn-node-shadow': 'var(--sn-shadow-geometry) var(--sn-shadow-color, rgba(0, 0, 0, 0.3))',
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** @type {SkinDefinition} */
|
|
47
|
+
export const COMPACT_SKIN = {
|
|
48
|
+
name: 'compact',
|
|
49
|
+
geometry: {
|
|
50
|
+
// === Spacing scale ===
|
|
51
|
+
'--sn-space-xs': '3px',
|
|
52
|
+
'--sn-space-sm': '5px',
|
|
53
|
+
'--sn-space-md': '8px',
|
|
54
|
+
'--sn-space-lg': '12px',
|
|
55
|
+
'--sn-space-xl': '16px',
|
|
56
|
+
|
|
57
|
+
// === Semantic geometry ===
|
|
58
|
+
'--sn-node-radius': 'var(--sn-space-md)',
|
|
59
|
+
'--sn-comment-radius': 'var(--sn-space-sm)',
|
|
60
|
+
'--sn-socket-size': 'var(--sn-space-md)',
|
|
61
|
+
'--sn-socket-border-width': '1.5px',
|
|
62
|
+
'--sn-grid-size': 'var(--sn-space-xl)',
|
|
63
|
+
'--sn-conn-width': '1.5',
|
|
64
|
+
|
|
65
|
+
// === Typography ===
|
|
66
|
+
'--sn-font': "'Inter', sans-serif",
|
|
67
|
+
'--sn-font-size': '12px',
|
|
68
|
+
|
|
69
|
+
// === Shadow geometry ===
|
|
70
|
+
'--sn-shadow-geometry': '0 2px 8px',
|
|
71
|
+
'--sn-node-shadow': 'var(--sn-shadow-geometry) var(--sn-shadow-color, rgba(0, 0, 0, 0.2))',
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** @type {SkinDefinition} */
|
|
76
|
+
export const ROUNDED_SKIN = {
|
|
77
|
+
name: 'rounded',
|
|
78
|
+
geometry: {
|
|
79
|
+
// === Spacing scale ===
|
|
80
|
+
'--sn-space-xs': '5px',
|
|
81
|
+
'--sn-space-sm': '10px',
|
|
82
|
+
'--sn-space-md': '14px',
|
|
83
|
+
'--sn-space-lg': '20px',
|
|
84
|
+
'--sn-space-xl': '28px',
|
|
85
|
+
|
|
86
|
+
// === Semantic geometry ===
|
|
87
|
+
'--sn-node-radius': 'var(--sn-space-lg)', // larger radius for "rounded" feel
|
|
88
|
+
'--sn-comment-radius': 'var(--sn-space-md)',
|
|
89
|
+
'--sn-socket-size': 'var(--sn-space-md)',
|
|
90
|
+
'--sn-socket-border-width': '2.5px',
|
|
91
|
+
'--sn-grid-size': 'var(--sn-space-xl)',
|
|
92
|
+
'--sn-conn-width': '2.5',
|
|
93
|
+
|
|
94
|
+
// === Typography ===
|
|
95
|
+
'--sn-font': "'Space Grotesk', sans-serif",
|
|
96
|
+
'--sn-font-size': '14px',
|
|
97
|
+
|
|
98
|
+
// === Shadow geometry ===
|
|
99
|
+
'--sn-shadow-geometry': '0 6px 24px',
|
|
100
|
+
'--sn-node-shadow': 'var(--sn-shadow-geometry) var(--sn-shadow-color, rgba(0, 0, 0, 0.25))',
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Apply skin to element
|
|
106
|
+
* @param {HTMLElement} element
|
|
107
|
+
* @param {SkinDefinition} skin
|
|
108
|
+
*/
|
|
109
|
+
export function applySkin(element, skin) {
|
|
110
|
+
for (const [key, value] of Object.entries(skin.geometry)) {
|
|
111
|
+
element.style.setProperty(key, value);
|
|
112
|
+
}
|
|
113
|
+
}
|