@stephen-netu/design-system 0.1.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/README.md +66 -0
- package/package.json +130 -0
- package/src/b0-app-shell/clarification-overlay/ClarificationOverlay.svelte +221 -0
- package/src/b0-app-shell/clarification-overlay/index.ts +1 -0
- package/src/b0-app-shell/command-bar/CommandBar.svelte +565 -0
- package/src/b0-app-shell/index.ts +14 -0
- package/src/base.css +152 -0
- package/src/d0-data-viz/BarChart.svelte +236 -0
- package/src/d0-data-viz/CheckpointBar.svelte +574 -0
- package/src/d0-data-viz/CheckpointPill.svelte +297 -0
- package/src/d0-data-viz/MetricCard.svelte +159 -0
- package/src/d0-data-viz/SparkLine.svelte +66 -0
- package/src/d0-data-viz/StorageMap.svelte +182 -0
- package/src/d0-data-viz/Timeline.svelte +188 -0
- package/src/d0-data-viz/checkpoint-types.ts +162 -0
- package/src/d0-data-viz/d0-tokens.css +32 -0
- package/src/d0-data-viz/index.ts +58 -0
- package/src/d0-data-viz/storage-map-types.ts +142 -0
- package/src/d0-data-viz/theme-integration.ts +134 -0
- package/src/d0-data-viz/types.ts +53 -0
- package/src/demos/BspSizingInfographic.svelte +386 -0
- package/src/demos/BspZoneDemo.svelte +255 -0
- package/src/demos/ControlRoomDemo.svelte +570 -0
- package/src/demos/NodepadBlocksShowcase.svelte +189 -0
- package/src/demos/index.ts +7 -0
- package/src/editor/CodeEditor.svelte +188 -0
- package/src/editor/FindReplaceDialog.svelte +516 -0
- package/src/editor/MarkdownEditor.svelte +660 -0
- package/src/editor/index.ts +2 -0
- package/src/entity-graph-view/WorldView.svelte +171 -0
- package/src/entity-graph-view/index.ts +6 -0
- package/src/entity-graph-view/types.ts +364 -0
- package/src/epistemic/index.ts +2 -0
- package/src/epistemic/types.ts +134 -0
- package/src/epistemic/utils.ts +32 -0
- package/src/f0-forms/checkbox/Checkbox.svelte +158 -0
- package/src/f0-forms/field/FormField.svelte +91 -0
- package/src/f0-forms/index.ts +11 -0
- package/src/f0-forms/radio/RadioGroup.svelte +192 -0
- package/src/f0-forms/search/SearchBar.svelte +99 -0
- package/src/f0-forms/select/Select.svelte +198 -0
- package/src/f0-forms/textarea/TextArea.svelte +170 -0
- package/src/f0-forms/toggle/FormToggle.svelte +147 -0
- package/src/f0-forms/validation/validate.ts +47 -0
- package/src/fabric/ai/ChatPanel.svelte +648 -0
- package/src/fabric/ai/index.ts +11 -0
- package/src/fabric/data/index.ts +21 -0
- package/src/fabric/feedback/StatusBar.svelte +274 -0
- package/src/fabric/feedback/ToastContainer.svelte +122 -0
- package/src/fabric/feedback/index.ts +14 -0
- package/src/fabric/index.ts +37 -0
- package/src/fabric/input/CommandBar.svelte +79 -0
- package/src/fabric/input/KeyboardShortcuts.svelte +388 -0
- package/src/fabric/input/index.ts +12 -0
- package/src/fabric/layout/GlassBox.svelte +193 -0
- package/src/fabric/layout/KanbanBoard.svelte +158 -0
- package/src/fabric/layout/KanbanCard.svelte +226 -0
- package/src/fabric/layout/KanbanColumn.svelte +278 -0
- package/src/fabric/layout/index.ts +17 -0
- package/src/fabric/navigation/ActivityRail.svelte +271 -0
- package/src/fabric/navigation/index.ts +11 -0
- package/src/g0-geo/SovereignMap.svelte +149 -0
- package/src/g0-geo/index.ts +2 -0
- package/src/index.ts +68 -0
- package/src/l0-layout/block-writer/BlockConnectionLines.svelte +98 -0
- package/src/l0-layout/block-writer/BlockContent.svelte +213 -0
- package/src/l0-layout/block-writer/BlockHeader.svelte +230 -0
- package/src/l0-layout/block-writer/BlockItem.svelte +177 -0
- package/src/l0-layout/block-writer/BlockWriter.svelte +219 -0
- package/src/l0-layout/block-writer/block-writer-types.ts +63 -0
- package/src/l0-layout/block-writer/index.ts +24 -0
- package/src/l0-layout/block-writer/markdown.ts +110 -0
- package/src/l0-layout/chat-view/ChatViewLayout.svelte +61 -0
- package/src/l0-layout/epistemic/AllayDetailPanel.svelte +113 -0
- package/src/l0-layout/epistemic/AssayDetailPanel.svelte +164 -0
- package/src/l0-layout/epistemic/AvenueTrace.svelte +253 -0
- package/src/l0-layout/epistemic/KnowledgeDetailPanel.svelte +204 -0
- package/src/l0-layout/epistemic/ParadigmLens.svelte +170 -0
- package/src/l0-layout/epistemic/ProvenanceChain.svelte +164 -0
- package/src/l0-layout/epistemic/TrustView.svelte +195 -0
- package/src/l0-layout/epistemic/index.ts +7 -0
- package/src/l0-layout/grove/AccordCard.svelte +156 -0
- package/src/l0-layout/grove/GroveCard.svelte +197 -0
- package/src/l0-layout/grove/MemberBadge.svelte +96 -0
- package/src/l0-layout/grove/MemberList.svelte +59 -0
- package/src/l0-layout/grove/grove-types.ts +29 -0
- package/src/l0-layout/grove/index.ts +14 -0
- package/src/l0-layout/index.ts +45 -0
- package/src/l0-layout/kanban/KanbanBoard.svelte +71 -0
- package/src/l0-layout/kanban/KanbanCard.svelte +276 -0
- package/src/l0-layout/kanban/KanbanColumn.svelte +198 -0
- package/src/l0-layout/kanban/KanbanViewLayout.svelte +61 -0
- package/src/l0-layout/workspace/WorkspaceLayout.svelte +93 -0
- package/src/n0-node-graph/GraphEditor.svelte +342 -0
- package/src/n0-node-graph/GraphHistory.svelte +122 -0
- package/src/n0-node-graph/Node.svelte +269 -0
- package/src/n0-node-graph/NodeCanvas.svelte +187 -0
- package/src/n0-node-graph/NodeEdge.svelte +152 -0
- package/src/n0-node-graph/NodePalette.svelte +318 -0
- package/src/n0-node-graph/NodePort.svelte +128 -0
- package/src/n0-node-graph/index.ts +33 -0
- package/src/n0-node-graph/n0-tokens.css +29 -0
- package/src/n0-node-graph/node-port-types.ts +19 -0
- package/src/n0-node-graph/types.ts +96 -0
- package/src/n0-node-graph/validate.test.ts +722 -0
- package/src/n0-node-graph/validate.ts +371 -0
- package/src/n1-flow-canvas/FlowCanvas.svelte +202 -0
- package/src/n1-flow-canvas/graph-state.svelte.ts +95 -0
- package/src/n1-flow-canvas/index.ts +21 -0
- package/src/n1-flow-canvas/layout.ts +118 -0
- package/src/n1-flow-canvas/layout.worker.ts +128 -0
- package/src/n1-flow-canvas/types.ts +102 -0
- package/src/n1-flow-canvas/utils.ts +146 -0
- package/src/n2-manifold-disk/ManifoldDisk.svelte +314 -0
- package/src/n2-manifold-disk/index.ts +6 -0
- package/src/n2-manifold-disk/poincare.ts +184 -0
- package/src/n3-force-graph/ForceGraph.svelte +180 -0
- package/src/n3-force-graph/GraphTooltip.svelte +99 -0
- package/src/n3-force-graph/animated-edge.ts +107 -0
- package/src/n3-force-graph/health-node.ts +128 -0
- package/src/n3-force-graph/index.ts +31 -0
- package/src/n3-force-graph/types.ts +94 -0
- package/src/n4-adaptive-manifold/AdaptiveManifold.svelte +1165 -0
- package/src/n4-adaptive-manifold/DetailCard.svelte +212 -0
- package/src/n4-adaptive-manifold/SuggestionIndicator.svelte +91 -0
- package/src/n4-adaptive-manifold/classifier.ts +139 -0
- package/src/n4-adaptive-manifold/index.ts +45 -0
- package/src/n4-adaptive-manifold/n4-tokens.css +155 -0
- package/src/n4-adaptive-manifold/orchestrator.ts +301 -0
- package/src/n4-adaptive-manifold/phase-theme.ts +92 -0
- package/src/n4-adaptive-manifold/pinnacle-events.ts +135 -0
- package/src/n4-adaptive-manifold/providers/gravitational-provider.ts +191 -0
- package/src/n4-adaptive-manifold/providers/manifold-provider.ts +276 -0
- package/src/n4-adaptive-manifold/transition.ts +90 -0
- package/src/n4-adaptive-manifold/types.ts +313 -0
- package/src/p0-primitives/index.ts +6 -0
- package/src/p0-primitives/tokens/control-room.css +309 -0
- package/src/p0-primitives/tokens/index.ts +6 -0
- package/src/p0-primitives/tokens/tokens.css +140 -0
- package/src/p0-primitives/tokens/tokens.json +132 -0
- package/src/p0-primitives/types/index.ts +123 -0
- package/src/p0-primitives/types/plugin-types.ts +74 -0
- package/src/p0-primitives/utils/index.ts +39 -0
- package/src/platform/hud-protocol.ts +20 -0
- package/src/platform/index.ts +3 -0
- package/src/s0-state/create-store.ts +84 -0
- package/src/s0-state/derived.ts +41 -0
- package/src/s0-state/index.ts +6 -0
- package/src/showcase/Showcase.svelte +401 -0
- package/src/spatial/BspTilingCanvas.svelte +232 -0
- package/src/spatial/ForceCanvas.svelte +289 -0
- package/src/spatial/LodRenderer.svelte +334 -0
- package/src/spatial/SnapZoomCamera.svelte +185 -0
- package/src/spatial/ZoneTiler.svelte +311 -0
- package/src/spatial/camera-types.ts +45 -0
- package/src/spatial/content-proportional-bsp.ts +253 -0
- package/src/spatial/force-canvas-types.ts +102 -0
- package/src/spatial/index.ts +15 -0
- package/src/spatial/lod-types.ts +70 -0
- package/src/spatial/zone-tiler-types.ts +177 -0
- package/src/stores/vaultStore.svelte +14 -0
- package/src/t0-transport/connection.ts +34 -0
- package/src/t0-transport/error.ts +37 -0
- package/src/t0-transport/events.ts +50 -0
- package/src/t0-transport/index.ts +6 -0
- package/src/t0-transport/invoke.ts +30 -0
- package/src/tauri/index.ts +13 -0
- package/src/u0-primitives/allay-gauge/AllayGauge.svelte +90 -0
- package/src/u0-primitives/allay-gauge/index.ts +1 -0
- package/src/u0-primitives/animated-icon/AnimatedIcon.svelte +273 -0
- package/src/u0-primitives/animated-icon/README.md +53 -0
- package/src/u0-primitives/animated-icon/index.ts +31 -0
- package/src/u0-primitives/assay-badge/AssayBadge.svelte +121 -0
- package/src/u0-primitives/assay-badge/AssaySignalBar.svelte +83 -0
- package/src/u0-primitives/assay-badge/index.ts +2 -0
- package/src/u0-primitives/avatar/Avatar.svelte +177 -0
- package/src/u0-primitives/badge/Badge.svelte +149 -0
- package/src/u0-primitives/button/Button.svelte +304 -0
- package/src/u0-primitives/card/Card.svelte +182 -0
- package/src/u0-primitives/dropdown/Dropdown.svelte +305 -0
- package/src/u0-primitives/expert-badge/ExpertBadge.svelte +89 -0
- package/src/u0-primitives/expert-badge/index.ts +1 -0
- package/src/u0-primitives/index.ts +35 -0
- package/src/u0-primitives/input/Input.svelte +237 -0
- package/src/u0-primitives/modal/Modal.svelte +331 -0
- package/src/u0-primitives/modal/index.ts +1 -0
- package/src/u0-primitives/spinner/Spinner.svelte +129 -0
- package/src/u0-primitives/tabs/Tabs.svelte +225 -0
- package/src/u0-primitives/tabs/tabs-types.ts +11 -0
- package/src/u0-primitives/toggle/Toggle.svelte +161 -0
- package/src/u0-primitives/tooltip/Tooltip.svelte +234 -0
- package/src/v0-diagrams/DiagramPanel.svelte +68 -0
- package/src/v0-diagrams/MarkmapDiagram.svelte +56 -0
- package/src/v0-diagrams/MermaidDiagram.svelte +53 -0
- package/src/v0-diagrams/index.ts +4 -0
- package/src/v0-diagrams/types.ts +28 -0
- package/src/v0-diagrams/v0-tokens.css +6 -0
- package/src/vite-env.d.ts +7 -0
- package/src/x0-enchanted-blocks/ENCHANTED_BLOCKS_SPEC.md +457 -0
- package/src/x0-enchanted-blocks/components/EnchantedBlock.svelte +485 -0
- package/src/x0-enchanted-blocks/components/NodepadBlock.svelte +262 -0
- package/src/x0-enchanted-blocks/core/block-layout.ts +211 -0
- package/src/x0-enchanted-blocks/core/block-types.ts +227 -0
- package/src/x0-enchanted-blocks/index.ts +46 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
// N0 Node Graph — Validation
|
|
2
|
+
// Pure TypeScript — no side effects, no mutations
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
GraphSchema,
|
|
6
|
+
NodeInstance,
|
|
7
|
+
EdgeDefinition,
|
|
8
|
+
PortDefinition,
|
|
9
|
+
NodeDefinition,
|
|
10
|
+
ValidationError,
|
|
11
|
+
ValidationResult,
|
|
12
|
+
} from './types';
|
|
13
|
+
|
|
14
|
+
// Helper: Get a node definition by kind
|
|
15
|
+
function getNodeDefinition(
|
|
16
|
+
nodeDefinitions: NodeDefinition[],
|
|
17
|
+
kind: string
|
|
18
|
+
): NodeDefinition | undefined {
|
|
19
|
+
return nodeDefinitions.find((def) => def.kind === kind);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Helper: Get a node instance by id
|
|
23
|
+
function getNodeInstance(
|
|
24
|
+
nodes: NodeInstance[],
|
|
25
|
+
id: string
|
|
26
|
+
): NodeInstance | undefined {
|
|
27
|
+
return nodes.find((node) => node.id === id);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Helper: Get a port definition from a node definition
|
|
31
|
+
function getPortDefinition(
|
|
32
|
+
nodeDefinition: NodeDefinition,
|
|
33
|
+
portId: string
|
|
34
|
+
): PortDefinition | undefined {
|
|
35
|
+
return nodeDefinition.ports.find((port) => port.id === portId);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Helper: Count connections to a specific port
|
|
39
|
+
function countConnectionsToPort(
|
|
40
|
+
edges: EdgeDefinition[],
|
|
41
|
+
nodeId: string,
|
|
42
|
+
portId: string,
|
|
43
|
+
direction: 'source' | 'target'
|
|
44
|
+
): number {
|
|
45
|
+
return edges.filter((edge) => {
|
|
46
|
+
if (direction === 'source') {
|
|
47
|
+
return edge.sourceNodeId === nodeId && edge.sourcePortId === portId;
|
|
48
|
+
}
|
|
49
|
+
return edge.targetNodeId === nodeId && edge.targetPortId === portId;
|
|
50
|
+
}).length;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Helper: Check if an edge is a duplicate
|
|
54
|
+
function isDuplicateEdge(
|
|
55
|
+
edges: EdgeDefinition[],
|
|
56
|
+
edge: EdgeDefinition
|
|
57
|
+
): boolean {
|
|
58
|
+
return edges.some((existing) =>
|
|
59
|
+
existing.id !== edge.id &&
|
|
60
|
+
existing.sourceNodeId === edge.sourceNodeId &&
|
|
61
|
+
existing.sourcePortId === edge.sourcePortId &&
|
|
62
|
+
existing.targetNodeId === edge.targetNodeId &&
|
|
63
|
+
existing.targetPortId === edge.targetPortId
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Validation Rule 1: NodeInstance.kind must reference a registered NodeDefinition.kind
|
|
68
|
+
function validateNodeKindReferences(
|
|
69
|
+
schema: GraphSchema
|
|
70
|
+
): ValidationError[] {
|
|
71
|
+
const errors: ValidationError[] = [];
|
|
72
|
+
|
|
73
|
+
for (const node of schema.nodes) {
|
|
74
|
+
const definition = getNodeDefinition(schema.nodeDefinitions, node.kind);
|
|
75
|
+
if (!definition) {
|
|
76
|
+
errors.push({
|
|
77
|
+
code: 'INVALID_NODE_KIND',
|
|
78
|
+
message: `Node instance "${node.id}" references undefined kind "${node.kind}"`,
|
|
79
|
+
nodeId: node.id,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return errors;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Validation Rule 2: Every edge must connect existing nodes and existing ports
|
|
88
|
+
function validateEdgeConnections(
|
|
89
|
+
schema: GraphSchema
|
|
90
|
+
): ValidationError[] {
|
|
91
|
+
const errors: ValidationError[] = [];
|
|
92
|
+
|
|
93
|
+
for (const edge of schema.edges) {
|
|
94
|
+
// Check source node exists
|
|
95
|
+
const sourceNode = getNodeInstance(schema.nodes, edge.sourceNodeId);
|
|
96
|
+
if (!sourceNode) {
|
|
97
|
+
errors.push({
|
|
98
|
+
code: 'INVALID_SOURCE_NODE',
|
|
99
|
+
message: `Edge "${edge.id}" references non-existent source node "${edge.sourceNodeId}"`,
|
|
100
|
+
edgeId: edge.id,
|
|
101
|
+
});
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Check target node exists
|
|
106
|
+
const targetNode = getNodeInstance(schema.nodes, edge.targetNodeId);
|
|
107
|
+
if (!targetNode) {
|
|
108
|
+
errors.push({
|
|
109
|
+
code: 'INVALID_TARGET_NODE',
|
|
110
|
+
message: `Edge "${edge.id}" references non-existent target node "${edge.targetNodeId}"`,
|
|
111
|
+
edgeId: edge.id,
|
|
112
|
+
});
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Get node definitions
|
|
117
|
+
const sourceDef = getNodeDefinition(schema.nodeDefinitions, sourceNode.kind);
|
|
118
|
+
const targetDef = getNodeDefinition(schema.nodeDefinitions, targetNode.kind);
|
|
119
|
+
|
|
120
|
+
if (!sourceDef) {
|
|
121
|
+
errors.push({
|
|
122
|
+
code: 'INVALID_SOURCE_NODE_KIND',
|
|
123
|
+
message: `Source node "${sourceNode.id}" has no registered definition`,
|
|
124
|
+
edgeId: edge.id,
|
|
125
|
+
nodeId: sourceNode.id,
|
|
126
|
+
});
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!targetDef) {
|
|
131
|
+
errors.push({
|
|
132
|
+
code: 'INVALID_TARGET_NODE_KIND',
|
|
133
|
+
message: `Target node "${targetNode.id}" has no registered definition`,
|
|
134
|
+
edgeId: edge.id,
|
|
135
|
+
nodeId: targetNode.id,
|
|
136
|
+
});
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check source port exists
|
|
141
|
+
const sourcePort = getPortDefinition(sourceDef, edge.sourcePortId);
|
|
142
|
+
if (!sourcePort) {
|
|
143
|
+
errors.push({
|
|
144
|
+
code: 'INVALID_SOURCE_PORT',
|
|
145
|
+
message: `Edge "${edge.id}" references non-existent source port "${edge.sourcePortId}" on node "${sourceNode.id}"`,
|
|
146
|
+
edgeId: edge.id,
|
|
147
|
+
portId: edge.sourcePortId,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check target port exists
|
|
152
|
+
const targetPort = getPortDefinition(targetDef, edge.targetPortId);
|
|
153
|
+
if (!targetPort) {
|
|
154
|
+
errors.push({
|
|
155
|
+
code: 'INVALID_TARGET_PORT',
|
|
156
|
+
message: `Edge "${edge.id}" references non-existent target port "${edge.targetPortId}" on node "${targetNode.id}"`,
|
|
157
|
+
edgeId: edge.id,
|
|
158
|
+
portId: edge.targetPortId,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return errors;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Validation Rule 3: Port direction — edges go from output port to input port only
|
|
167
|
+
function validatePortDirection(
|
|
168
|
+
schema: GraphSchema
|
|
169
|
+
): ValidationError[] {
|
|
170
|
+
const errors: ValidationError[] = [];
|
|
171
|
+
|
|
172
|
+
for (const edge of schema.edges) {
|
|
173
|
+
const sourceNode = getNodeInstance(schema.nodes, edge.sourceNodeId);
|
|
174
|
+
const targetNode = getNodeInstance(schema.nodes, edge.targetNodeId);
|
|
175
|
+
|
|
176
|
+
if (!sourceNode || !targetNode) continue;
|
|
177
|
+
|
|
178
|
+
const sourceDef = getNodeDefinition(schema.nodeDefinitions, sourceNode.kind);
|
|
179
|
+
const targetDef = getNodeDefinition(schema.nodeDefinitions, targetNode.kind);
|
|
180
|
+
|
|
181
|
+
if (!sourceDef || !targetDef) continue;
|
|
182
|
+
|
|
183
|
+
const sourcePort = getPortDefinition(sourceDef, edge.sourcePortId);
|
|
184
|
+
const targetPort = getPortDefinition(targetDef, edge.targetPortId);
|
|
185
|
+
|
|
186
|
+
if (sourcePort && targetPort) {
|
|
187
|
+
if (sourcePort.direction !== 'output') {
|
|
188
|
+
errors.push({
|
|
189
|
+
code: 'INVALID_PORT_DIRECTION',
|
|
190
|
+
message: `Edge "${edge.id}" must start from an output port, but port "${edge.sourcePortId}" on node "${sourceNode.id}" is "${sourcePort.direction}"`,
|
|
191
|
+
edgeId: edge.id,
|
|
192
|
+
portId: edge.sourcePortId,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (targetPort.direction !== 'input') {
|
|
197
|
+
errors.push({
|
|
198
|
+
code: 'INVALID_PORT_DIRECTION',
|
|
199
|
+
message: `Edge "${edge.id}" must end at an input port, but port "${edge.targetPortId}" on node "${targetNode.id}" is "${targetPort.direction}"`,
|
|
200
|
+
edgeId: edge.id,
|
|
201
|
+
portId: edge.targetPortId,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return errors;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Validation Rule 4: Port type compatibility — exact match required
|
|
211
|
+
function validateTypeCompatibility(
|
|
212
|
+
schema: GraphSchema
|
|
213
|
+
): ValidationError[] {
|
|
214
|
+
const errors: ValidationError[] = [];
|
|
215
|
+
|
|
216
|
+
for (const edge of schema.edges) {
|
|
217
|
+
const sourceNode = getNodeInstance(schema.nodes, edge.sourceNodeId);
|
|
218
|
+
const targetNode = getNodeInstance(schema.nodes, edge.targetNodeId);
|
|
219
|
+
|
|
220
|
+
if (!sourceNode || !targetNode) continue;
|
|
221
|
+
|
|
222
|
+
const sourceDef = getNodeDefinition(schema.nodeDefinitions, sourceNode.kind);
|
|
223
|
+
const targetDef = getNodeDefinition(schema.nodeDefinitions, targetNode.kind);
|
|
224
|
+
|
|
225
|
+
if (!sourceDef || !targetDef) continue;
|
|
226
|
+
|
|
227
|
+
const sourcePort = getPortDefinition(sourceDef, edge.sourcePortId);
|
|
228
|
+
const targetPort = getPortDefinition(targetDef, edge.targetPortId);
|
|
229
|
+
|
|
230
|
+
if (sourcePort && targetPort && sourcePort.type !== targetPort.type) {
|
|
231
|
+
errors.push({
|
|
232
|
+
code: 'TYPE_MISMATCH',
|
|
233
|
+
message: `Edge "${edge.id}" connects incompatible types: source port "${edge.sourcePortId}" has type "${sourcePort.type}" but target port "${edge.targetPortId}" has type "${targetPort.type}"`,
|
|
234
|
+
edgeId: edge.id,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return errors;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Validation Rule 5: Connection limits — port.maxConnections not exceeded
|
|
243
|
+
function validateConnectionLimits(
|
|
244
|
+
schema: GraphSchema
|
|
245
|
+
): ValidationError[] {
|
|
246
|
+
const errors: ValidationError[] = [];
|
|
247
|
+
|
|
248
|
+
for (const node of schema.nodes) {
|
|
249
|
+
const definition = getNodeDefinition(schema.nodeDefinitions, node.kind);
|
|
250
|
+
if (!definition) continue;
|
|
251
|
+
|
|
252
|
+
for (const port of definition.ports) {
|
|
253
|
+
const connectionCount = countConnectionsToPort(
|
|
254
|
+
schema.edges,
|
|
255
|
+
node.id,
|
|
256
|
+
port.id,
|
|
257
|
+
port.direction === 'output' ? 'source' : 'target'
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
if (connectionCount > port.maxConnections) {
|
|
261
|
+
errors.push({
|
|
262
|
+
code: 'MAX_CONNECTIONS_EXCEEDED',
|
|
263
|
+
message: `Port "${port.id}" on node "${node.id}" has ${connectionCount} connections but maximum allowed is ${port.maxConnections}`,
|
|
264
|
+
nodeId: node.id,
|
|
265
|
+
portId: port.id,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return errors;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Validation Rule 6: Required ports — all required:true ports must have at least one connection
|
|
275
|
+
function validateRequiredPorts(
|
|
276
|
+
schema: GraphSchema
|
|
277
|
+
): ValidationError[] {
|
|
278
|
+
const errors: ValidationError[] = [];
|
|
279
|
+
|
|
280
|
+
for (const node of schema.nodes) {
|
|
281
|
+
const definition = getNodeDefinition(schema.nodeDefinitions, node.kind);
|
|
282
|
+
if (!definition) continue;
|
|
283
|
+
|
|
284
|
+
for (const port of definition.ports) {
|
|
285
|
+
if (!port.required) continue;
|
|
286
|
+
|
|
287
|
+
const connectionCount = countConnectionsToPort(
|
|
288
|
+
schema.edges,
|
|
289
|
+
node.id,
|
|
290
|
+
port.id,
|
|
291
|
+
port.direction === 'output' ? 'source' : 'target'
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
if (connectionCount === 0) {
|
|
295
|
+
errors.push({
|
|
296
|
+
code: 'REQUIRED_PORT_UNCONNECTED',
|
|
297
|
+
message: `Required port "${port.id}" on node "${node.id}" has no connections`,
|
|
298
|
+
nodeId: node.id,
|
|
299
|
+
portId: port.id,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return errors;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Validation Rule 7: No self-loops (edge source === edge target on same node)
|
|
309
|
+
function validateNoSelfLoops(
|
|
310
|
+
schema: GraphSchema
|
|
311
|
+
): ValidationError[] {
|
|
312
|
+
const errors: ValidationError[] = [];
|
|
313
|
+
|
|
314
|
+
for (const edge of schema.edges) {
|
|
315
|
+
if (edge.sourceNodeId === edge.targetNodeId) {
|
|
316
|
+
errors.push({
|
|
317
|
+
code: 'SELF_LOOP',
|
|
318
|
+
message: `Edge "${edge.id}" creates a self-loop on node "${edge.sourceNodeId}"`,
|
|
319
|
+
edgeId: edge.id,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return errors;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Validation Rule 8: No duplicate edges (same source port → same target port)
|
|
328
|
+
function validateNoDuplicateEdges(
|
|
329
|
+
schema: GraphSchema
|
|
330
|
+
): ValidationError[] {
|
|
331
|
+
const errors: ValidationError[] = [];
|
|
332
|
+
|
|
333
|
+
for (const edge of schema.edges) {
|
|
334
|
+
if (isDuplicateEdge(schema.edges, edge)) {
|
|
335
|
+
errors.push({
|
|
336
|
+
code: 'DUPLICATE_EDGE',
|
|
337
|
+
message: `Edge "${edge.id}" is a duplicate: same source "${edge.sourceNodeId}:${edge.sourcePortId}" to target "${edge.targetNodeId}:${edge.targetPortId}"`,
|
|
338
|
+
edgeId: edge.id,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return errors;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Validates a graph schema against all 8 validation rules.
|
|
348
|
+
*
|
|
349
|
+
* Pure function — no side effects, no mutation of input.
|
|
350
|
+
*
|
|
351
|
+
* @param schema - The graph schema to validate
|
|
352
|
+
* @returns ValidationResult with valid flag and array of errors
|
|
353
|
+
*/
|
|
354
|
+
export function validateGraph(schema: GraphSchema): ValidationResult {
|
|
355
|
+
// Collect all errors from all validation rules
|
|
356
|
+
const allErrors: ValidationError[] = [
|
|
357
|
+
...validateNodeKindReferences(schema),
|
|
358
|
+
...validateEdgeConnections(schema),
|
|
359
|
+
...validatePortDirection(schema),
|
|
360
|
+
...validateTypeCompatibility(schema),
|
|
361
|
+
...validateConnectionLimits(schema),
|
|
362
|
+
...validateRequiredPorts(schema),
|
|
363
|
+
...validateNoSelfLoops(schema),
|
|
364
|
+
...validateNoDuplicateEdges(schema),
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
valid: allErrors.length === 0,
|
|
369
|
+
errors: allErrors,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// N1 FlowCanvas — Canvas 2D graph with ELK layout + BFS edge reveal
|
|
3
|
+
// S-02: BFS order drives edge reveal stagger (no Math.random)
|
|
4
|
+
// S-05: worker terminated on component destroy
|
|
5
|
+
|
|
6
|
+
import { onMount, onDestroy } from 'svelte';
|
|
7
|
+
import { Canvas, Layer } from 'svelte-canvas';
|
|
8
|
+
import { getArrow } from 'perfect-arrows';
|
|
9
|
+
import type { GraphSnapshot, GraphStateApi } from './types.js';
|
|
10
|
+
import { GraphState } from './graph-state.svelte.js';
|
|
11
|
+
import { runLayout } from './layout.js';
|
|
12
|
+
import { EDGE_REVEAL_STAGGER_MS } from './types.js';
|
|
13
|
+
|
|
14
|
+
// ── Props ────────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
snapshot: GraphSnapshot;
|
|
18
|
+
state?: GraphStateApi;
|
|
19
|
+
width?: number;
|
|
20
|
+
height?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Destructure as `graphStateProp` to avoid conflict with the `$state` rune.
|
|
24
|
+
let { snapshot, state: graphStateProp, width = 800, height = 600 }: Props = $props();
|
|
25
|
+
|
|
26
|
+
// ── Internal state ──────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
// svelte-ignore state_referenced_locally
|
|
29
|
+
const _state = graphStateProp instanceof GraphState ? graphStateProp : new GraphState();
|
|
30
|
+
let mounted = false;
|
|
31
|
+
/** Edge reveal offsets: edge id → 0.0 (hidden) .. 1.0 (fully drawn) */
|
|
32
|
+
let edgeOffsets = $state<Map<string, number>>(new Map());
|
|
33
|
+
|
|
34
|
+
// ── Layout (runs on main thread — ELK is async, won't block UI) ─────────────
|
|
35
|
+
// The previous Worker approach failed in Tauri: ELK's bundled JS tries to
|
|
36
|
+
// spawn a sub-Worker inside the layout Worker, which Tauri's WKWebView
|
|
37
|
+
// doesn't support (nested Workers unavailable, Worker = undefined in scope).
|
|
38
|
+
|
|
39
|
+
function requestLayout() {
|
|
40
|
+
if (!mounted) return;
|
|
41
|
+
runLayout(snapshot, width, height).then(result => {
|
|
42
|
+
_state.applyLayout(result);
|
|
43
|
+
scheduleEdgeReveal(result.bfsOrder);
|
|
44
|
+
}).catch(() => {
|
|
45
|
+
// Layout failed — leave existing positions in place
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
onMount(() => {
|
|
50
|
+
mounted = true;
|
|
51
|
+
requestLayout();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
onDestroy(() => {
|
|
55
|
+
mounted = false;
|
|
56
|
+
if (_state instanceof GraphState) _state.destroy();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Re-layout when snapshot changes
|
|
60
|
+
$effect(() => {
|
|
61
|
+
void snapshot;
|
|
62
|
+
requestLayout();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ── Edge reveal animation (BFS stagger) ────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
function scheduleEdgeReveal(bfsOrder: ReadonlyArray<string>) {
|
|
68
|
+
// Reset all edges to hidden
|
|
69
|
+
const next = new Map<string, number>();
|
|
70
|
+
for (const e of snapshot.edges) next.set(e.id, 0.0);
|
|
71
|
+
edgeOffsets = next;
|
|
72
|
+
|
|
73
|
+
// BFS order: each node index N → reveal its outgoing edges after N * STAGGER_MS
|
|
74
|
+
const nodeIndex = new Map<string, number>();
|
|
75
|
+
bfsOrder.forEach((id, i) => nodeIndex.set(id, i));
|
|
76
|
+
|
|
77
|
+
for (const edge of snapshot.edges) {
|
|
78
|
+
const idx = nodeIndex.get(edge.sourceId) ?? 0;
|
|
79
|
+
setTimeout(() => {
|
|
80
|
+
edgeOffsets = new Map(edgeOffsets).set(edge.id, 1.0);
|
|
81
|
+
}, idx * EDGE_REVEAL_STAGGER_MS);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Design tokens ──────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
const COLORS = {
|
|
88
|
+
bg: '#1a1a1a',
|
|
89
|
+
node: '#2a2a2a',
|
|
90
|
+
nodeBorder: '#b87333',
|
|
91
|
+
nodeHover: '#3a3a3a',
|
|
92
|
+
nodeText: '#e8e0d0',
|
|
93
|
+
edgeLine: '#b87333',
|
|
94
|
+
edgeFade: '#5a4a30',
|
|
95
|
+
focus: '#d4943a',
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const NODE_RADIUS = 4;
|
|
99
|
+
const FONT = '12px var(--font-mono, monospace)';
|
|
100
|
+
|
|
101
|
+
// ── Render ──────────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
function render({ context }: { context: CanvasRenderingContext2D }) {
|
|
104
|
+
const positions = _state.positions;
|
|
105
|
+
const focusId = (_state as GraphState).focusCenterId;
|
|
106
|
+
|
|
107
|
+
context.clearRect(0, 0, width, height);
|
|
108
|
+
|
|
109
|
+
// Background
|
|
110
|
+
context.fillStyle = COLORS.bg;
|
|
111
|
+
context.fillRect(0, 0, width, height);
|
|
112
|
+
|
|
113
|
+
// ── Edges ──
|
|
114
|
+
context.font = FONT;
|
|
115
|
+
for (const edge of snapshot.edges) {
|
|
116
|
+
const src = positions.get(edge.sourceId);
|
|
117
|
+
const tgt = positions.get(edge.targetId);
|
|
118
|
+
if (!src || !tgt) continue;
|
|
119
|
+
|
|
120
|
+
const progress = edgeOffsets.get(edge.id) ?? 0;
|
|
121
|
+
if (progress <= 0) continue;
|
|
122
|
+
|
|
123
|
+
const sx = src.x + src.width / 2;
|
|
124
|
+
const sy = src.y + src.height / 2;
|
|
125
|
+
const tx = tgt.x + tgt.width / 2;
|
|
126
|
+
const ty = tgt.y + tgt.height / 2;
|
|
127
|
+
|
|
128
|
+
const [ax, ay, cx, cy, ex, ey, ae] = getArrow(sx, sy, tx, ty, {
|
|
129
|
+
bow: 0.2,
|
|
130
|
+
stretch: 0.4,
|
|
131
|
+
padStart: src.width / 2 + 4,
|
|
132
|
+
padEnd: tgt.width / 2 + 4,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// BFS reveal via lineDashOffset
|
|
136
|
+
const path = new Path2D();
|
|
137
|
+
path.moveTo(ax, ay);
|
|
138
|
+
path.quadraticCurveTo(cx, cy, ex, ey);
|
|
139
|
+
|
|
140
|
+
// Estimate path length for dash sizing (approximation)
|
|
141
|
+
const dx = ex - ax, dy2 = ey - ay;
|
|
142
|
+
const approxLen = Math.sqrt(dx * dx + dy2 * dy2) * 1.2;
|
|
143
|
+
const dash = approxLen * progress;
|
|
144
|
+
|
|
145
|
+
context.save();
|
|
146
|
+
context.strokeStyle = progress >= 1.0 ? COLORS.edgeLine : COLORS.edgeFade;
|
|
147
|
+
context.lineWidth = 1.5;
|
|
148
|
+
context.setLineDash([dash, approxLen]);
|
|
149
|
+
context.lineDashOffset = 0;
|
|
150
|
+
context.stroke(path);
|
|
151
|
+
context.restore();
|
|
152
|
+
|
|
153
|
+
// Arrowhead
|
|
154
|
+
if (progress >= 1.0) {
|
|
155
|
+
context.save();
|
|
156
|
+
context.fillStyle = COLORS.edgeLine;
|
|
157
|
+
context.translate(ex, ey);
|
|
158
|
+
context.rotate(ae);
|
|
159
|
+
context.beginPath();
|
|
160
|
+
context.moveTo(0, 0);
|
|
161
|
+
context.lineTo(-8, -4);
|
|
162
|
+
context.lineTo(-8, 4);
|
|
163
|
+
context.closePath();
|
|
164
|
+
context.fill();
|
|
165
|
+
context.restore();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Nodes ──
|
|
170
|
+
for (const node of snapshot.nodes) {
|
|
171
|
+
const pos = positions.get(node.id);
|
|
172
|
+
if (!pos) continue;
|
|
173
|
+
|
|
174
|
+
const isFocus = node.id === focusId;
|
|
175
|
+
const { x, y, width: w, height: h } = pos;
|
|
176
|
+
|
|
177
|
+
// Node body
|
|
178
|
+
context.fillStyle = isFocus ? COLORS.focus : COLORS.node;
|
|
179
|
+
context.strokeStyle = COLORS.nodeBorder;
|
|
180
|
+
context.lineWidth = isFocus ? 2 : 1;
|
|
181
|
+
|
|
182
|
+
context.beginPath();
|
|
183
|
+
context.roundRect(x, y, w, h, NODE_RADIUS);
|
|
184
|
+
context.fill();
|
|
185
|
+
context.stroke();
|
|
186
|
+
|
|
187
|
+
// Label
|
|
188
|
+
context.fillStyle = COLORS.nodeText;
|
|
189
|
+
context.font = FONT;
|
|
190
|
+
context.textAlign = 'center';
|
|
191
|
+
context.textBaseline = 'middle';
|
|
192
|
+
// Truncate if needed
|
|
193
|
+
const maxWidth = w - 16;
|
|
194
|
+
const label = node.label.length > 18 ? node.label.slice(0, 16) + '…' : node.label;
|
|
195
|
+
context.fillText(label, x + w / 2, y + h / 2, maxWidth);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
</script>
|
|
199
|
+
|
|
200
|
+
<Canvas {width} {height}>
|
|
201
|
+
<Layer {render} />
|
|
202
|
+
</Canvas>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// N1 GraphState — Svelte 5 runes, Spring-animated node positions
|
|
2
|
+
// S-02: positions lerped deterministically via rAF loop (no Math.random)
|
|
3
|
+
// S-05: animation loop bounded by convergence threshold
|
|
4
|
+
|
|
5
|
+
import type { NodeLayout, LayoutResult, GraphStateApi } from './types.js';
|
|
6
|
+
|
|
7
|
+
const LERP_FACTOR = 0.12; // per frame (bounded, S-05)
|
|
8
|
+
const SETTLE_THRESH = 0.5; // pixels — stop animating below this delta
|
|
9
|
+
const MAX_FRAMES = 240; // ~4s at 60fps hard cap (S-05 killability)
|
|
10
|
+
|
|
11
|
+
export class GraphState implements GraphStateApi {
|
|
12
|
+
positions = $state<Map<string, NodeLayout>>(new Map());
|
|
13
|
+
bfsOrder = $state<ReadonlyArray<string>>([]);
|
|
14
|
+
|
|
15
|
+
// Focus semantics
|
|
16
|
+
focusCenterId = $state<string | null>(null);
|
|
17
|
+
layoutRootId = $state<string | null>(null);
|
|
18
|
+
|
|
19
|
+
private _targets = new Map<string, NodeLayout>();
|
|
20
|
+
private _rafId: number | null = null;
|
|
21
|
+
private _frameCount = 0;
|
|
22
|
+
|
|
23
|
+
// Callback invoked by FlowCanvas when layoutRootId changes
|
|
24
|
+
onLayoutRootChange: ((nodeId: string) => void) | null = null;
|
|
25
|
+
|
|
26
|
+
applyLayout(result: LayoutResult): void {
|
|
27
|
+
this._targets = new Map(result.positions);
|
|
28
|
+
this.bfsOrder = result.bfsOrder;
|
|
29
|
+
this._frameCount = 0;
|
|
30
|
+
this._startAnimation();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
setFocusCenter(nodeId: string): void {
|
|
34
|
+
// Re-centers Poincaré disk — does NOT trigger re-layout
|
|
35
|
+
this.focusCenterId = nodeId;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
setLayoutRoot(nodeId: string): void {
|
|
39
|
+
this.layoutRootId = nodeId;
|
|
40
|
+
this.onLayoutRootChange?.(nodeId);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Animation ──────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
private _startAnimation(): void {
|
|
46
|
+
if (this._rafId !== null) cancelAnimationFrame(this._rafId);
|
|
47
|
+
this._tick();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private _tick = (): void => {
|
|
51
|
+
if (this._frameCount >= MAX_FRAMES) {
|
|
52
|
+
// Force-snap to targets on hard cap (S-05)
|
|
53
|
+
this.positions = new Map(this._targets);
|
|
54
|
+
this._rafId = null;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let maxDelta = 0;
|
|
59
|
+
const next = new Map<string, NodeLayout>();
|
|
60
|
+
|
|
61
|
+
for (const [id, target] of this._targets) {
|
|
62
|
+
const current = this.positions.get(id);
|
|
63
|
+
if (!current) {
|
|
64
|
+
// First appearance — snap directly, no lerp
|
|
65
|
+
next.set(id, { ...target });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const nx = current.x + (target.x - current.x) * LERP_FACTOR;
|
|
69
|
+
const ny = current.y + (target.y - current.y) * LERP_FACTOR;
|
|
70
|
+
const dx = Math.abs(target.x - nx);
|
|
71
|
+
const dy = Math.abs(target.y - ny);
|
|
72
|
+
if (dx > maxDelta) maxDelta = dx;
|
|
73
|
+
if (dy > maxDelta) maxDelta = dy;
|
|
74
|
+
next.set(id, { id, x: nx, y: ny, width: target.width, height: target.height });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.positions = next;
|
|
78
|
+
this._frameCount++;
|
|
79
|
+
|
|
80
|
+
if (maxDelta > SETTLE_THRESH) {
|
|
81
|
+
this._rafId = requestAnimationFrame(this._tick);
|
|
82
|
+
} else {
|
|
83
|
+
// Settled — snap to exact targets
|
|
84
|
+
this.positions = new Map(this._targets);
|
|
85
|
+
this._rafId = null;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
destroy(): void {
|
|
90
|
+
if (this._rafId !== null) {
|
|
91
|
+
cancelAnimationFrame(this._rafId);
|
|
92
|
+
this._rafId = null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// N1 Flow Canvas — Canvas 2D graph with ELK layout + BFS edge reveal
|
|
2
|
+
// Depends on: graphology, elkjs, perfect-arrows, svelte-canvas
|
|
3
|
+
|
|
4
|
+
export type {
|
|
5
|
+
GraphNode,
|
|
6
|
+
GraphEdge,
|
|
7
|
+
GraphSnapshot,
|
|
8
|
+
NodeLayout,
|
|
9
|
+
LayoutResult,
|
|
10
|
+
WorkerIn,
|
|
11
|
+
WorkerOut,
|
|
12
|
+
WorkerMessage,
|
|
13
|
+
GraphStateApi,
|
|
14
|
+
EdgeRevealState,
|
|
15
|
+
} from './types.js';
|
|
16
|
+
|
|
17
|
+
export { EDGE_REVEAL_STAGGER_MS } from './types.js';
|
|
18
|
+
|
|
19
|
+
export { default as FlowCanvas } from './FlowCanvas.svelte';
|
|
20
|
+
export { GraphState } from './graph-state.svelte';
|
|
21
|
+
export { nodeHitTest, domEventToCanvasCoords, parseMentionEdges, parseGroupedSequenceEdges } from './utils.js';
|