@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.
Files changed (204) hide show
  1. package/README.md +66 -0
  2. package/package.json +130 -0
  3. package/src/b0-app-shell/clarification-overlay/ClarificationOverlay.svelte +221 -0
  4. package/src/b0-app-shell/clarification-overlay/index.ts +1 -0
  5. package/src/b0-app-shell/command-bar/CommandBar.svelte +565 -0
  6. package/src/b0-app-shell/index.ts +14 -0
  7. package/src/base.css +152 -0
  8. package/src/d0-data-viz/BarChart.svelte +236 -0
  9. package/src/d0-data-viz/CheckpointBar.svelte +574 -0
  10. package/src/d0-data-viz/CheckpointPill.svelte +297 -0
  11. package/src/d0-data-viz/MetricCard.svelte +159 -0
  12. package/src/d0-data-viz/SparkLine.svelte +66 -0
  13. package/src/d0-data-viz/StorageMap.svelte +182 -0
  14. package/src/d0-data-viz/Timeline.svelte +188 -0
  15. package/src/d0-data-viz/checkpoint-types.ts +162 -0
  16. package/src/d0-data-viz/d0-tokens.css +32 -0
  17. package/src/d0-data-viz/index.ts +58 -0
  18. package/src/d0-data-viz/storage-map-types.ts +142 -0
  19. package/src/d0-data-viz/theme-integration.ts +134 -0
  20. package/src/d0-data-viz/types.ts +53 -0
  21. package/src/demos/BspSizingInfographic.svelte +386 -0
  22. package/src/demos/BspZoneDemo.svelte +255 -0
  23. package/src/demos/ControlRoomDemo.svelte +570 -0
  24. package/src/demos/NodepadBlocksShowcase.svelte +189 -0
  25. package/src/demos/index.ts +7 -0
  26. package/src/editor/CodeEditor.svelte +188 -0
  27. package/src/editor/FindReplaceDialog.svelte +516 -0
  28. package/src/editor/MarkdownEditor.svelte +660 -0
  29. package/src/editor/index.ts +2 -0
  30. package/src/entity-graph-view/WorldView.svelte +171 -0
  31. package/src/entity-graph-view/index.ts +6 -0
  32. package/src/entity-graph-view/types.ts +364 -0
  33. package/src/epistemic/index.ts +2 -0
  34. package/src/epistemic/types.ts +134 -0
  35. package/src/epistemic/utils.ts +32 -0
  36. package/src/f0-forms/checkbox/Checkbox.svelte +158 -0
  37. package/src/f0-forms/field/FormField.svelte +91 -0
  38. package/src/f0-forms/index.ts +11 -0
  39. package/src/f0-forms/radio/RadioGroup.svelte +192 -0
  40. package/src/f0-forms/search/SearchBar.svelte +99 -0
  41. package/src/f0-forms/select/Select.svelte +198 -0
  42. package/src/f0-forms/textarea/TextArea.svelte +170 -0
  43. package/src/f0-forms/toggle/FormToggle.svelte +147 -0
  44. package/src/f0-forms/validation/validate.ts +47 -0
  45. package/src/fabric/ai/ChatPanel.svelte +648 -0
  46. package/src/fabric/ai/index.ts +11 -0
  47. package/src/fabric/data/index.ts +21 -0
  48. package/src/fabric/feedback/StatusBar.svelte +274 -0
  49. package/src/fabric/feedback/ToastContainer.svelte +122 -0
  50. package/src/fabric/feedback/index.ts +14 -0
  51. package/src/fabric/index.ts +37 -0
  52. package/src/fabric/input/CommandBar.svelte +79 -0
  53. package/src/fabric/input/KeyboardShortcuts.svelte +388 -0
  54. package/src/fabric/input/index.ts +12 -0
  55. package/src/fabric/layout/GlassBox.svelte +193 -0
  56. package/src/fabric/layout/KanbanBoard.svelte +158 -0
  57. package/src/fabric/layout/KanbanCard.svelte +226 -0
  58. package/src/fabric/layout/KanbanColumn.svelte +278 -0
  59. package/src/fabric/layout/index.ts +17 -0
  60. package/src/fabric/navigation/ActivityRail.svelte +271 -0
  61. package/src/fabric/navigation/index.ts +11 -0
  62. package/src/g0-geo/SovereignMap.svelte +149 -0
  63. package/src/g0-geo/index.ts +2 -0
  64. package/src/index.ts +68 -0
  65. package/src/l0-layout/block-writer/BlockConnectionLines.svelte +98 -0
  66. package/src/l0-layout/block-writer/BlockContent.svelte +213 -0
  67. package/src/l0-layout/block-writer/BlockHeader.svelte +230 -0
  68. package/src/l0-layout/block-writer/BlockItem.svelte +177 -0
  69. package/src/l0-layout/block-writer/BlockWriter.svelte +219 -0
  70. package/src/l0-layout/block-writer/block-writer-types.ts +63 -0
  71. package/src/l0-layout/block-writer/index.ts +24 -0
  72. package/src/l0-layout/block-writer/markdown.ts +110 -0
  73. package/src/l0-layout/chat-view/ChatViewLayout.svelte +61 -0
  74. package/src/l0-layout/epistemic/AllayDetailPanel.svelte +113 -0
  75. package/src/l0-layout/epistemic/AssayDetailPanel.svelte +164 -0
  76. package/src/l0-layout/epistemic/AvenueTrace.svelte +253 -0
  77. package/src/l0-layout/epistemic/KnowledgeDetailPanel.svelte +204 -0
  78. package/src/l0-layout/epistemic/ParadigmLens.svelte +170 -0
  79. package/src/l0-layout/epistemic/ProvenanceChain.svelte +164 -0
  80. package/src/l0-layout/epistemic/TrustView.svelte +195 -0
  81. package/src/l0-layout/epistemic/index.ts +7 -0
  82. package/src/l0-layout/grove/AccordCard.svelte +156 -0
  83. package/src/l0-layout/grove/GroveCard.svelte +197 -0
  84. package/src/l0-layout/grove/MemberBadge.svelte +96 -0
  85. package/src/l0-layout/grove/MemberList.svelte +59 -0
  86. package/src/l0-layout/grove/grove-types.ts +29 -0
  87. package/src/l0-layout/grove/index.ts +14 -0
  88. package/src/l0-layout/index.ts +45 -0
  89. package/src/l0-layout/kanban/KanbanBoard.svelte +71 -0
  90. package/src/l0-layout/kanban/KanbanCard.svelte +276 -0
  91. package/src/l0-layout/kanban/KanbanColumn.svelte +198 -0
  92. package/src/l0-layout/kanban/KanbanViewLayout.svelte +61 -0
  93. package/src/l0-layout/workspace/WorkspaceLayout.svelte +93 -0
  94. package/src/n0-node-graph/GraphEditor.svelte +342 -0
  95. package/src/n0-node-graph/GraphHistory.svelte +122 -0
  96. package/src/n0-node-graph/Node.svelte +269 -0
  97. package/src/n0-node-graph/NodeCanvas.svelte +187 -0
  98. package/src/n0-node-graph/NodeEdge.svelte +152 -0
  99. package/src/n0-node-graph/NodePalette.svelte +318 -0
  100. package/src/n0-node-graph/NodePort.svelte +128 -0
  101. package/src/n0-node-graph/index.ts +33 -0
  102. package/src/n0-node-graph/n0-tokens.css +29 -0
  103. package/src/n0-node-graph/node-port-types.ts +19 -0
  104. package/src/n0-node-graph/types.ts +96 -0
  105. package/src/n0-node-graph/validate.test.ts +722 -0
  106. package/src/n0-node-graph/validate.ts +371 -0
  107. package/src/n1-flow-canvas/FlowCanvas.svelte +202 -0
  108. package/src/n1-flow-canvas/graph-state.svelte.ts +95 -0
  109. package/src/n1-flow-canvas/index.ts +21 -0
  110. package/src/n1-flow-canvas/layout.ts +118 -0
  111. package/src/n1-flow-canvas/layout.worker.ts +128 -0
  112. package/src/n1-flow-canvas/types.ts +102 -0
  113. package/src/n1-flow-canvas/utils.ts +146 -0
  114. package/src/n2-manifold-disk/ManifoldDisk.svelte +314 -0
  115. package/src/n2-manifold-disk/index.ts +6 -0
  116. package/src/n2-manifold-disk/poincare.ts +184 -0
  117. package/src/n3-force-graph/ForceGraph.svelte +180 -0
  118. package/src/n3-force-graph/GraphTooltip.svelte +99 -0
  119. package/src/n3-force-graph/animated-edge.ts +107 -0
  120. package/src/n3-force-graph/health-node.ts +128 -0
  121. package/src/n3-force-graph/index.ts +31 -0
  122. package/src/n3-force-graph/types.ts +94 -0
  123. package/src/n4-adaptive-manifold/AdaptiveManifold.svelte +1165 -0
  124. package/src/n4-adaptive-manifold/DetailCard.svelte +212 -0
  125. package/src/n4-adaptive-manifold/SuggestionIndicator.svelte +91 -0
  126. package/src/n4-adaptive-manifold/classifier.ts +139 -0
  127. package/src/n4-adaptive-manifold/index.ts +45 -0
  128. package/src/n4-adaptive-manifold/n4-tokens.css +155 -0
  129. package/src/n4-adaptive-manifold/orchestrator.ts +301 -0
  130. package/src/n4-adaptive-manifold/phase-theme.ts +92 -0
  131. package/src/n4-adaptive-manifold/pinnacle-events.ts +135 -0
  132. package/src/n4-adaptive-manifold/providers/gravitational-provider.ts +191 -0
  133. package/src/n4-adaptive-manifold/providers/manifold-provider.ts +276 -0
  134. package/src/n4-adaptive-manifold/transition.ts +90 -0
  135. package/src/n4-adaptive-manifold/types.ts +313 -0
  136. package/src/p0-primitives/index.ts +6 -0
  137. package/src/p0-primitives/tokens/control-room.css +309 -0
  138. package/src/p0-primitives/tokens/index.ts +6 -0
  139. package/src/p0-primitives/tokens/tokens.css +140 -0
  140. package/src/p0-primitives/tokens/tokens.json +132 -0
  141. package/src/p0-primitives/types/index.ts +123 -0
  142. package/src/p0-primitives/types/plugin-types.ts +74 -0
  143. package/src/p0-primitives/utils/index.ts +39 -0
  144. package/src/platform/hud-protocol.ts +20 -0
  145. package/src/platform/index.ts +3 -0
  146. package/src/s0-state/create-store.ts +84 -0
  147. package/src/s0-state/derived.ts +41 -0
  148. package/src/s0-state/index.ts +6 -0
  149. package/src/showcase/Showcase.svelte +401 -0
  150. package/src/spatial/BspTilingCanvas.svelte +232 -0
  151. package/src/spatial/ForceCanvas.svelte +289 -0
  152. package/src/spatial/LodRenderer.svelte +334 -0
  153. package/src/spatial/SnapZoomCamera.svelte +185 -0
  154. package/src/spatial/ZoneTiler.svelte +311 -0
  155. package/src/spatial/camera-types.ts +45 -0
  156. package/src/spatial/content-proportional-bsp.ts +253 -0
  157. package/src/spatial/force-canvas-types.ts +102 -0
  158. package/src/spatial/index.ts +15 -0
  159. package/src/spatial/lod-types.ts +70 -0
  160. package/src/spatial/zone-tiler-types.ts +177 -0
  161. package/src/stores/vaultStore.svelte +14 -0
  162. package/src/t0-transport/connection.ts +34 -0
  163. package/src/t0-transport/error.ts +37 -0
  164. package/src/t0-transport/events.ts +50 -0
  165. package/src/t0-transport/index.ts +6 -0
  166. package/src/t0-transport/invoke.ts +30 -0
  167. package/src/tauri/index.ts +13 -0
  168. package/src/u0-primitives/allay-gauge/AllayGauge.svelte +90 -0
  169. package/src/u0-primitives/allay-gauge/index.ts +1 -0
  170. package/src/u0-primitives/animated-icon/AnimatedIcon.svelte +273 -0
  171. package/src/u0-primitives/animated-icon/README.md +53 -0
  172. package/src/u0-primitives/animated-icon/index.ts +31 -0
  173. package/src/u0-primitives/assay-badge/AssayBadge.svelte +121 -0
  174. package/src/u0-primitives/assay-badge/AssaySignalBar.svelte +83 -0
  175. package/src/u0-primitives/assay-badge/index.ts +2 -0
  176. package/src/u0-primitives/avatar/Avatar.svelte +177 -0
  177. package/src/u0-primitives/badge/Badge.svelte +149 -0
  178. package/src/u0-primitives/button/Button.svelte +304 -0
  179. package/src/u0-primitives/card/Card.svelte +182 -0
  180. package/src/u0-primitives/dropdown/Dropdown.svelte +305 -0
  181. package/src/u0-primitives/expert-badge/ExpertBadge.svelte +89 -0
  182. package/src/u0-primitives/expert-badge/index.ts +1 -0
  183. package/src/u0-primitives/index.ts +35 -0
  184. package/src/u0-primitives/input/Input.svelte +237 -0
  185. package/src/u0-primitives/modal/Modal.svelte +331 -0
  186. package/src/u0-primitives/modal/index.ts +1 -0
  187. package/src/u0-primitives/spinner/Spinner.svelte +129 -0
  188. package/src/u0-primitives/tabs/Tabs.svelte +225 -0
  189. package/src/u0-primitives/tabs/tabs-types.ts +11 -0
  190. package/src/u0-primitives/toggle/Toggle.svelte +161 -0
  191. package/src/u0-primitives/tooltip/Tooltip.svelte +234 -0
  192. package/src/v0-diagrams/DiagramPanel.svelte +68 -0
  193. package/src/v0-diagrams/MarkmapDiagram.svelte +56 -0
  194. package/src/v0-diagrams/MermaidDiagram.svelte +53 -0
  195. package/src/v0-diagrams/index.ts +4 -0
  196. package/src/v0-diagrams/types.ts +28 -0
  197. package/src/v0-diagrams/v0-tokens.css +6 -0
  198. package/src/vite-env.d.ts +7 -0
  199. package/src/x0-enchanted-blocks/ENCHANTED_BLOCKS_SPEC.md +457 -0
  200. package/src/x0-enchanted-blocks/components/EnchantedBlock.svelte +485 -0
  201. package/src/x0-enchanted-blocks/components/NodepadBlock.svelte +262 -0
  202. package/src/x0-enchanted-blocks/core/block-layout.ts +211 -0
  203. package/src/x0-enchanted-blocks/core/block-types.ts +227 -0
  204. package/src/x0-enchanted-blocks/index.ts +46 -0
@@ -0,0 +1,269 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { NodeInstance, NodeDefinition, PortDefinition } from './types';
4
+ import type { PortDragDetail, PortDropDetail } from './node-port-types';
5
+ import NodePort from './NodePort.svelte';
6
+
7
+ const HEADER_HEIGHT = 32;
8
+ const PORT_SPACING = 24;
9
+ const PORT_PADDING = 12;
10
+
11
+ interface NodeProps {
12
+ node: NodeInstance;
13
+ definition: NodeDefinition;
14
+ selected?: boolean;
15
+ children?: Snippet;
16
+ onmove?: (id: string, position: { x: number; y: number }) => void;
17
+ onselect?: (id: string) => void;
18
+ onportdragstart?: (detail: PortDragDetail) => void;
19
+ onportdrop?: (detail: PortDropDetail) => void;
20
+ }
21
+
22
+ let {
23
+ node,
24
+ definition,
25
+ selected = false,
26
+ children,
27
+ onmove,
28
+ onselect,
29
+ onportdragstart,
30
+ onportdrop,
31
+ }: NodeProps = $props();
32
+
33
+ // svelte-ignore state_referenced_locally -- intentional: local copy of initial collapsed state
34
+ let collapsed = $state(node.collapsed);
35
+ let dragging = $state(false);
36
+ let dragStart = $state<{ x: number; y: number; nodeX: number; nodeY: number } | null>(null);
37
+
38
+ let inputPorts = $derived(
39
+ definition.ports.filter((p: PortDefinition) => p.direction === 'input')
40
+ );
41
+
42
+ let outputPorts = $derived(
43
+ definition.ports.filter((p: PortDefinition) => p.direction === 'output')
44
+ );
45
+
46
+ let bodyHeight = $derived(
47
+ collapsed ? 0 : node.size.height - HEADER_HEIGHT
48
+ );
49
+
50
+ let displayHeight = $derived(
51
+ collapsed ? HEADER_HEIGHT : node.size.height
52
+ );
53
+
54
+ function portYPosition(index: number, totalPorts: number): number {
55
+ if (totalPorts === 1) {
56
+ return HEADER_HEIGHT + PORT_PADDING + PORT_SPACING / 2;
57
+ }
58
+ return HEADER_HEIGHT + PORT_PADDING + index * PORT_SPACING;
59
+ }
60
+
61
+ function handleHeaderPointerDown(e: PointerEvent) {
62
+ if ((e.target as Element)?.closest('.n0-collapse-btn')) return;
63
+ e.stopPropagation();
64
+ dragging = true;
65
+ dragStart = {
66
+ x: e.clientX,
67
+ y: e.clientY,
68
+ nodeX: node.position.x,
69
+ nodeY: node.position.y,
70
+ };
71
+ (e.currentTarget as Element)?.setPointerCapture?.(e.pointerId);
72
+ }
73
+
74
+ function handlePointerMove(e: PointerEvent) {
75
+ if (!dragging || !dragStart) return;
76
+ e.stopPropagation();
77
+ const dx = e.clientX - dragStart.x;
78
+ const dy = e.clientY - dragStart.y;
79
+ onmove?.(node.id, {
80
+ x: dragStart.nodeX + dx,
81
+ y: dragStart.nodeY + dy,
82
+ });
83
+ }
84
+
85
+ function handlePointerUp(e: PointerEvent) {
86
+ if (dragging) {
87
+ dragging = false;
88
+ dragStart = null;
89
+ (e.currentTarget as Element)?.releasePointerCapture?.(e.pointerId);
90
+ }
91
+ }
92
+
93
+ function handleClick(e: MouseEvent) {
94
+ if (dragging) return;
95
+ onselect?.(node.id);
96
+ }
97
+
98
+ function toggleCollapse(e: MouseEvent) {
99
+ e.stopPropagation();
100
+ collapsed = !collapsed;
101
+ }
102
+ </script>
103
+
104
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
105
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
106
+ <g
107
+ class="n0-node"
108
+ class:selected
109
+ class:dragging
110
+ transform="translate({node.position.x}, {node.position.y})"
111
+ onclick={handleClick}
112
+ >
113
+ <!-- Node background -->
114
+ <rect
115
+ class="n0-node-bg"
116
+ width={node.size.width}
117
+ height={displayHeight}
118
+ rx={8}
119
+ ry={8}
120
+ />
121
+
122
+ <!-- Header bar -->
123
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
124
+ <rect
125
+ class="n0-node-header"
126
+ width={node.size.width}
127
+ height={HEADER_HEIGHT}
128
+ rx={8}
129
+ ry={8}
130
+ onpointerdown={handleHeaderPointerDown}
131
+ onpointermove={handlePointerMove}
132
+ onpointerup={handlePointerUp}
133
+ />
134
+ <!-- Square off header bottom corners when not collapsed -->
135
+ {#if !collapsed}
136
+ <rect
137
+ class="n0-node-header"
138
+ x={0}
139
+ y={HEADER_HEIGHT - 8}
140
+ width={node.size.width}
141
+ height={8}
142
+ />
143
+ {/if}
144
+
145
+ <!-- Header label -->
146
+ <text
147
+ class="n0-node-label"
148
+ x={28}
149
+ y={HEADER_HEIGHT / 2 + 1}
150
+ dominant-baseline="central"
151
+ onpointerdown={handleHeaderPointerDown}
152
+ onpointermove={handlePointerMove}
153
+ onpointerup={handlePointerUp}
154
+ >
155
+ {definition.label}
156
+ </text>
157
+
158
+ <!-- Collapse toggle -->
159
+ <g
160
+ class="n0-collapse-btn"
161
+ transform="translate(10, {HEADER_HEIGHT / 2})"
162
+ onclick={toggleCollapse}
163
+ role="button"
164
+ tabindex="0"
165
+ >
166
+ <text
167
+ class="n0-collapse-icon"
168
+ text-anchor="middle"
169
+ dominant-baseline="central"
170
+ font-size="10"
171
+ >
172
+ {collapsed ? '\u25B6' : '\u25BC'}
173
+ </text>
174
+ </g>
175
+
176
+ <!-- Input ports -->
177
+ {#each inputPorts as port, i (port.id)}
178
+ <NodePort
179
+ nodeId={node.id}
180
+ {port}
181
+ position={{ x: 0, y: portYPosition(i, inputPorts.length) }}
182
+ connected={false}
183
+ {onportdragstart}
184
+ {onportdrop}
185
+ />
186
+ {/each}
187
+
188
+ <!-- Output ports -->
189
+ {#each outputPorts as port, i (port.id)}
190
+ <NodePort
191
+ nodeId={node.id}
192
+ {port}
193
+ position={{ x: node.size.width, y: portYPosition(i, outputPorts.length) }}
194
+ connected={false}
195
+ {onportdragstart}
196
+ {onportdrop}
197
+ />
198
+ {/each}
199
+
200
+ <!-- Body content via foreignObject -->
201
+ {#if !collapsed && children}
202
+ <foreignObject
203
+ x={0}
204
+ y={HEADER_HEIGHT}
205
+ width={node.size.width}
206
+ height={bodyHeight}
207
+ >
208
+ <div class="n0-node-body" xmlns="http://www.w3.org/1999/xhtml">
209
+ {@render children()}
210
+ </div>
211
+ </foreignObject>
212
+ {/if}
213
+ </g>
214
+
215
+ <style>
216
+ .n0-node {
217
+ cursor: default;
218
+ }
219
+
220
+ .n0-node.dragging {
221
+ cursor: grabbing;
222
+ }
223
+
224
+ .n0-node-bg {
225
+ fill: var(--n0-node-bg, #222);
226
+ stroke: var(--n0-node-border, rgba(184, 115, 51, 0.2));
227
+ stroke-width: 1.5;
228
+ transition: stroke 150ms ease;
229
+ }
230
+
231
+ .n0-node.selected .n0-node-bg {
232
+ stroke: var(--n0-node-border-selected, #b87333);
233
+ stroke-width: 2;
234
+ }
235
+
236
+ .n0-node-header {
237
+ fill: var(--n0-node-header-bg, rgba(184, 115, 51, 0.1));
238
+ cursor: grab;
239
+ }
240
+
241
+ .n0-node.dragging .n0-node-header {
242
+ cursor: grabbing;
243
+ }
244
+
245
+ .n0-node-label {
246
+ fill: var(--color-text, #e8e0d0);
247
+ font-family: system-ui, sans-serif;
248
+ font-size: 13px;
249
+ font-weight: 500;
250
+ pointer-events: none;
251
+ user-select: none;
252
+ }
253
+
254
+ .n0-collapse-btn {
255
+ cursor: pointer;
256
+ }
257
+
258
+ .n0-collapse-icon {
259
+ fill: var(--color-text-muted, #a09880);
260
+ }
261
+
262
+ .n0-node-body {
263
+ overflow: hidden;
264
+ padding: 8px;
265
+ color: var(--color-text, #e8e0d0);
266
+ font-family: system-ui, sans-serif;
267
+ font-size: 13px;
268
+ }
269
+ </style>
@@ -0,0 +1,187 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { Transform } from './types';
4
+
5
+ interface NodeCanvasProps {
6
+ transform?: Transform;
7
+ ontransform?: (t: Transform) => void;
8
+ gridVisible?: boolean;
9
+ children: Snippet;
10
+ }
11
+
12
+ const MIN_ZOOM = 0.1;
13
+ const MAX_ZOOM = 3.0;
14
+ const ZOOM_SENSITIVITY = 0.001;
15
+
16
+ let {
17
+ transform: initialTransform,
18
+ ontransform,
19
+ gridVisible = true,
20
+ children,
21
+ }: NodeCanvasProps = $props();
22
+
23
+ // svelte-ignore state_referenced_locally -- intentional: transform is initialized from prop then managed locally
24
+ let transform: Transform = $state(
25
+ initialTransform ?? { x: 0, y: 0, scale: 1 }
26
+ );
27
+
28
+ let isPanning = $state(false);
29
+ let panOrigin = $state({ x: 0, y: 0 });
30
+ let transformAtPanStart = $state({ x: 0, y: 0 });
31
+
32
+ let svgEl: SVGSVGElement | undefined = $state(undefined);
33
+
34
+ // Grid size from token (matches --n0-canvas-grid-size default)
35
+ const GRID_SIZE = 20;
36
+ let scaledGridSize = $derived(GRID_SIZE * transform.scale);
37
+
38
+ function notifyTransform() {
39
+ ontransform?.(transform);
40
+ }
41
+
42
+ function onPointerDown(e: PointerEvent) {
43
+ // Only pan on primary button, only when clicking the SVG background
44
+ if (e.button !== 0) return;
45
+ const target = e.target as Element;
46
+ if (target !== svgEl && target.id !== 'n0-canvas-bg') return;
47
+
48
+ isPanning = true;
49
+ panOrigin = { x: e.clientX, y: e.clientY };
50
+ transformAtPanStart = { x: transform.x, y: transform.y };
51
+ (e.currentTarget as Element).setPointerCapture(e.pointerId);
52
+ e.preventDefault();
53
+ }
54
+
55
+ function onPointerMove(e: PointerEvent) {
56
+ if (!isPanning) return;
57
+ const dx = e.clientX - panOrigin.x;
58
+ const dy = e.clientY - panOrigin.y;
59
+ transform = {
60
+ ...transform,
61
+ x: transformAtPanStart.x + dx,
62
+ y: transformAtPanStart.y + dy,
63
+ };
64
+ notifyTransform();
65
+ }
66
+
67
+ function onPointerUp(e: PointerEvent) {
68
+ if (!isPanning) return;
69
+ isPanning = false;
70
+ (e.currentTarget as Element).releasePointerCapture(e.pointerId);
71
+ }
72
+
73
+ function onWheel(e: WheelEvent) {
74
+ e.preventDefault();
75
+ if (!svgEl) return;
76
+
77
+ const rect = svgEl.getBoundingClientRect();
78
+ // Cursor position relative to the SVG element
79
+ const cursorX = e.clientX - rect.left;
80
+ const cursorY = e.clientY - rect.top;
81
+
82
+ const delta = -e.deltaY * ZOOM_SENSITIVITY;
83
+ const newScale = Math.min(
84
+ MAX_ZOOM,
85
+ Math.max(MIN_ZOOM, transform.scale * (1 + delta))
86
+ );
87
+ const scaleRatio = newScale / transform.scale;
88
+
89
+ // Zoom centered on cursor: adjust translation so the point under the
90
+ // cursor stays fixed after scaling.
91
+ const newX = cursorX - scaleRatio * (cursorX - transform.x);
92
+ const newY = cursorY - scaleRatio * (cursorY - transform.y);
93
+
94
+ transform = { x: newX, y: newY, scale: newScale };
95
+ notifyTransform();
96
+ }
97
+ </script>
98
+
99
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
100
+ <svg
101
+ bind:this={svgEl}
102
+ class="n0-canvas"
103
+ class:is-panning={isPanning}
104
+ role="application"
105
+ aria-label="Graph canvas"
106
+ onpointerdown={onPointerDown}
107
+ onpointermove={onPointerMove}
108
+ onpointerup={onPointerUp}
109
+ onpointercancel={onPointerUp}
110
+ onwheel={onWheel}
111
+ >
112
+ <defs>
113
+ {#if gridVisible}
114
+ <pattern
115
+ id="n0-grid-pattern"
116
+ width={scaledGridSize}
117
+ height={scaledGridSize}
118
+ patternUnits="userSpaceOnUse"
119
+ x={transform.x % scaledGridSize}
120
+ y={transform.y % scaledGridSize}
121
+ >
122
+ <line
123
+ x1={scaledGridSize}
124
+ y1="0"
125
+ x2={scaledGridSize}
126
+ y2={scaledGridSize}
127
+ class="n0-grid-line"
128
+ />
129
+ <line
130
+ x1="0"
131
+ y1={scaledGridSize}
132
+ x2={scaledGridSize}
133
+ y2={scaledGridSize}
134
+ class="n0-grid-line"
135
+ />
136
+ </pattern>
137
+ {/if}
138
+ </defs>
139
+
140
+ <!-- Background fill -->
141
+ <rect
142
+ id="n0-canvas-bg"
143
+ width="100%"
144
+ height="100%"
145
+ class="n0-canvas-background"
146
+ />
147
+
148
+ <!-- Grid overlay -->
149
+ {#if gridVisible}
150
+ <rect
151
+ width="100%"
152
+ height="100%"
153
+ fill="url(#n0-grid-pattern)"
154
+ pointer-events="none"
155
+ />
156
+ {/if}
157
+
158
+ <!-- Transformed content layer -->
159
+ <g transform="translate({transform.x},{transform.y}) scale({transform.scale})">
160
+ {@render children()}
161
+ </g>
162
+ </svg>
163
+
164
+ <style>
165
+ .n0-canvas {
166
+ width: 100%;
167
+ height: 100%;
168
+ display: block;
169
+ cursor: grab;
170
+ touch-action: none;
171
+ user-select: none;
172
+ -webkit-user-select: none;
173
+ }
174
+
175
+ .n0-canvas.is-panning {
176
+ cursor: grabbing;
177
+ }
178
+
179
+ .n0-canvas-background {
180
+ fill: var(--n0-canvas-bg);
181
+ }
182
+
183
+ .n0-grid-line {
184
+ stroke: var(--n0-canvas-grid);
185
+ stroke-width: 1;
186
+ }
187
+ </style>
@@ -0,0 +1,152 @@
1
+ <script lang="ts">
2
+ import type { EdgeDefinition } from './types';
3
+
4
+ interface NodeEdgeProps {
5
+ edge?: EdgeDefinition;
6
+ sourcePosition: { x: number; y: number };
7
+ targetPosition: { x: number; y: number };
8
+ selected?: boolean;
9
+ creating?: boolean;
10
+ onselect?: (edgeId: string) => void;
11
+ ondelete?: (edgeId: string) => void;
12
+ }
13
+
14
+ let {
15
+ edge,
16
+ sourcePosition,
17
+ targetPosition,
18
+ selected = false,
19
+ creating = false,
20
+ onselect,
21
+ ondelete,
22
+ }: NodeEdgeProps = $props();
23
+
24
+ let hovered = $state(false);
25
+
26
+ let d = $derived.by(() => {
27
+ const dx = Math.abs(targetPosition.x - sourcePosition.x);
28
+ const offset = Math.min(dx * 0.5, 150);
29
+ return `M ${sourcePosition.x} ${sourcePosition.y} C ${sourcePosition.x + offset} ${sourcePosition.y}, ${targetPosition.x - offset} ${targetPosition.y}, ${targetPosition.x} ${targetPosition.y}`;
30
+ });
31
+
32
+ let midpoint = $derived({
33
+ x: (sourcePosition.x + targetPosition.x) / 2,
34
+ y: (sourcePosition.y + targetPosition.y) / 2,
35
+ });
36
+
37
+ let showDelete = $derived(hovered && !creating && edge !== undefined);
38
+
39
+ function handlePathClick(e: MouseEvent) {
40
+ e.stopPropagation();
41
+ if (edge && onselect) {
42
+ onselect(edge.id);
43
+ }
44
+ }
45
+
46
+ function handleDelete(e: MouseEvent) {
47
+ e.stopPropagation();
48
+ if (edge && ondelete) {
49
+ ondelete(edge.id);
50
+ }
51
+ }
52
+ </script>
53
+
54
+ <g
55
+ class="n0-edge"
56
+ class:is-selected={selected}
57
+ class:is-creating={creating}
58
+ class:is-hovered={hovered}
59
+ role="graphics-symbol"
60
+ >
61
+ <!-- Invisible wide hit-area path for hover/click detection -->
62
+ <path
63
+ d={d}
64
+ class="n0-edge-hitarea"
65
+ onpointerenter={() => hovered = true}
66
+ onpointerleave={() => hovered = false}
67
+ onclick={handlePathClick}
68
+ onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') handlePathClick(e as unknown as MouseEvent); }}
69
+ role="button"
70
+ tabindex="-1"
71
+ aria-label="Select edge"
72
+ />
73
+
74
+ <!-- Visible bezier path -->
75
+ <path
76
+ d={d}
77
+ class="n0-edge-line"
78
+ pointer-events="none"
79
+ />
80
+
81
+ <!-- Delete button at midpoint (shown on hover) -->
82
+ {#if showDelete}
83
+ <g
84
+ class="n0-edge-delete"
85
+ transform="translate({midpoint.x},{midpoint.y})"
86
+ onclick={handleDelete}
87
+ onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') handleDelete(e as unknown as MouseEvent); }}
88
+ role="button"
89
+ tabindex="-1"
90
+ >
91
+ <circle r="8" class="n0-edge-delete-bg" />
92
+ <line x1="-3.5" y1="-3.5" x2="3.5" y2="3.5" class="n0-edge-delete-x" />
93
+ <line x1="3.5" y1="-3.5" x2="-3.5" y2="3.5" class="n0-edge-delete-x" />
94
+ </g>
95
+ {/if}
96
+ </g>
97
+
98
+ <style>
99
+ .n0-edge-hitarea {
100
+ fill: none;
101
+ stroke: transparent;
102
+ stroke-width: 12px;
103
+ cursor: pointer;
104
+ }
105
+
106
+ .n0-edge-line {
107
+ fill: none;
108
+ stroke: var(--n0-edge-color);
109
+ stroke-width: var(--n0-edge-width, 2px);
110
+ transition: stroke 0.15s ease, stroke-width 0.15s ease;
111
+ }
112
+
113
+ .n0-edge.is-hovered .n0-edge-line {
114
+ stroke: var(--n0-edge-color-hover);
115
+ stroke-width: 3px;
116
+ }
117
+
118
+ .n0-edge.is-selected .n0-edge-line {
119
+ stroke: var(--n0-edge-color-hover);
120
+ stroke-width: 3px;
121
+ }
122
+
123
+ .n0-edge.is-creating .n0-edge-line {
124
+ stroke: var(--n0-edge-color-creating);
125
+ stroke-dasharray: 8 4;
126
+ }
127
+
128
+ .n0-edge-delete {
129
+ cursor: pointer;
130
+ }
131
+
132
+ .n0-edge-delete-bg {
133
+ fill: var(--n0-node-bg, #222);
134
+ stroke: var(--n0-edge-color-hover);
135
+ stroke-width: 1.5px;
136
+ }
137
+
138
+ .n0-edge-delete-x {
139
+ stroke: var(--n0-edge-color-hover);
140
+ stroke-width: 1.5px;
141
+ stroke-linecap: round;
142
+ }
143
+
144
+ .n0-edge-delete:hover .n0-edge-delete-bg {
145
+ fill: #c0392b;
146
+ stroke: #c0392b;
147
+ }
148
+
149
+ .n0-edge-delete:hover .n0-edge-delete-x {
150
+ stroke: #fff;
151
+ }
152
+ </style>