@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,314 @@
1
+ <script lang="ts">
2
+ // N2 ManifoldDisk — Poincaré disk projection wrapping n1-flow-canvas
3
+ // κ = 1.0 fixed (standard Poincaré disk — no prop, YAGNI)
4
+ // Shares GraphState with FlowCanvas for instantaneous mode toggle
5
+ //
6
+ // When `agents` is provided, the disk renders live S³ agent orientations
7
+ // (quaternion → Poincaré disk via axis-angle) as glowing points with
8
+ // trailing history. Task topology is drawn behind the agents.
9
+
10
+ import { untrack } from 'svelte';
11
+ import { Canvas, Layer } from 'svelte-canvas';
12
+ import { getArrow } from 'perfect-arrows';
13
+ import type { GraphSnapshot } from '../n1-flow-canvas/types.js';
14
+ import { GraphState } from '../n1-flow-canvas/graph-state.svelte.js';
15
+ import { EDGE_REVEAL_STAGGER_MS } from '../n1-flow-canvas/types.js';
16
+ import { projectToDisk, quaternionToDisk } from './poincare.js';
17
+
18
+ // ── Props ────────────────────────────────────────────────────────────────────
19
+
20
+ /** Raw inhabitant wire type — mirrors InhabitantView from ryu/src/stores/manifoldStore. */
21
+ interface InhabitantView {
22
+ id: string;
23
+ orientation: [number, number, number, number]; // [w, i, j, k]
24
+ allay: number;
25
+ }
26
+
27
+ interface Props {
28
+ snapshot: GraphSnapshot;
29
+ state: GraphState; // must be the same instance passed to FlowCanvas
30
+ width?: number;
31
+ height?: number;
32
+ selectedId?: string | null; // Visual highlight — does NOT trigger Möbius transform
33
+ /** Raw S³ inhabitant orientations from the manifold store. Projected here at render time. */
34
+ inhabitants?: InhabitantView[];
35
+ }
36
+
37
+ // Destructure as `graphStateProp` to avoid conflict with the `$state` rune.
38
+ let { snapshot, state: graphStateProp, width = 800, height = 600, selectedId = null, inhabitants = [] }: Props = $props();
39
+
40
+ // ── Agent trail history ───────────────────────────────────────────────────────
41
+ // Ring buffer of recent disk-projected positions per agent (last TRAIL_LEN frames).
42
+ // Projection happens here — disk radius is known at render time.
43
+ const TRAIL_LEN = 12;
44
+
45
+ // Map<agentId, Array<{u, v}>> — normalized disk coords (×diskRadius at render).
46
+ let agentTrails = $state<Map<string, Array<{ u: number; v: number }>>>(new Map());
47
+
48
+ $effect(() => {
49
+ // Track `inhabitants` as the sole reactive dependency.
50
+ // `agentTrails` is read via untrack so writing it back does NOT re-trigger
51
+ // this effect — avoids the read→write→re-run infinite loop.
52
+ const snap = inhabitants;
53
+ agentTrails = untrack(() => {
54
+ const next = new Map(agentTrails);
55
+ for (const inh of snap) {
56
+ const [w, i, j, k] = inh.orientation;
57
+ const { u, v } = quaternionToDisk(w, i, j, k, 1.0);
58
+ const trail = next.get(inh.id) ?? [];
59
+ trail.push({ u, v });
60
+ if (trail.length > TRAIL_LEN) trail.shift();
61
+ next.set(inh.id, trail);
62
+ }
63
+ for (const id of next.keys()) {
64
+ if (!snap.find(a => a.id === id)) next.delete(id);
65
+ }
66
+ return next;
67
+ });
68
+ });
69
+
70
+ // Disk radius = half the smallest dimension, leaving some padding
71
+ const diskRadius = $derived(Math.min(width, height) / 2 - 24);
72
+ const diskCx = $derived(width / 2);
73
+ const diskCy = $derived(height / 2);
74
+
75
+ // Project positions into Poincaré disk space
76
+ const projected = $derived(
77
+ projectToDisk(graphStateProp.positions, graphStateProp.focusCenterId, diskRadius)
78
+ );
79
+
80
+ // ── Edge reveal offsets (re-used from GraphState bfsOrder) ──────────────────
81
+
82
+ let edgeOffsets = $state<Map<string, number>>(new Map());
83
+
84
+ $effect(() => {
85
+ const bfsOrder = graphStateProp.bfsOrder;
86
+ const next = new Map<string, number>();
87
+ for (const e of snapshot.edges) next.set(e.id, 0.0);
88
+ edgeOffsets = next;
89
+
90
+ const nodeIndex = new Map<string, number>();
91
+ bfsOrder.forEach((id, i) => nodeIndex.set(id, i));
92
+
93
+ for (const edge of snapshot.edges) {
94
+ const idx = nodeIndex.get(edge.sourceId) ?? 0;
95
+ setTimeout(() => {
96
+ edgeOffsets = new Map(edgeOffsets).set(edge.id, 1.0);
97
+ }, idx * EDGE_REVEAL_STAGGER_MS);
98
+ }
99
+ });
100
+
101
+ // ── Design tokens ──────────────────────────────────────────────────────────
102
+
103
+ const COLORS = {
104
+ bg: '#1a1a1a',
105
+ diskBg: '#111111',
106
+ diskBorder: '#3a3020',
107
+ node: '#2a2a2a',
108
+ nodeBorder: '#b87333',
109
+ nodeText: '#e8e0d0',
110
+ edgeLine: '#b87333',
111
+ edgeFade: '#5a4a30',
112
+ focus: '#d4943a',
113
+ grid: '#1e1e1e',
114
+ selected: '#d4943a',
115
+ };
116
+
117
+ // Column-based node fill colors — matches KOS brass/copper theme.
118
+ // Semi-transparent fills let the dark disk background show through.
119
+ const COLUMN_FILL: Record<string, string> = {
120
+ GO: 'rgba(184, 115, 51, 0.35)', // brass — active
121
+ CONSULT: 'rgba(184, 115, 51, 0.18)', // brass outline — advisory
122
+ WEED: 'rgba(200, 160, 50, 0.22)', // warning amber
123
+ SPROUT: 'rgba(70, 130, 200, 0.20)', // info blue — growing
124
+ FREEZE: 'rgba(80, 110, 180, 0.16)', // cool blue — frozen
125
+ SEED: 'rgba(42, 42, 42, 0.60)', // muted — backlog
126
+ };
127
+
128
+ const COLUMN_BORDER: Record<string, string> = {
129
+ GO: '#b87333',
130
+ CONSULT: 'rgba(184, 115, 51, 0.6)',
131
+ WEED: '#c8a032',
132
+ SPROUT: '#4682c8',
133
+ FREEZE: '#506eb4',
134
+ SEED: '#3a3a3a',
135
+ };
136
+
137
+ const NODE_RADIUS = 4;
138
+ const FONT = '11px var(--font-mono, monospace)';
139
+
140
+ // ── Render ──────────────────────────────────────────────────────────────────
141
+
142
+ function render({ context }: { context: CanvasRenderingContext2D }) {
143
+ const r = diskRadius;
144
+ const cx = diskCx, cy = diskCy;
145
+
146
+ context.clearRect(0, 0, width, height);
147
+
148
+ // Background
149
+ context.fillStyle = COLORS.bg;
150
+ context.fillRect(0, 0, width, height);
151
+
152
+ // Disk background + clip
153
+ context.save();
154
+ context.beginPath();
155
+ context.arc(cx, cy, r, 0, Math.PI * 2);
156
+ context.fillStyle = COLORS.diskBg;
157
+ context.fill();
158
+ context.strokeStyle = COLORS.diskBorder;
159
+ context.lineWidth = 1;
160
+ context.stroke();
161
+ context.clip();
162
+
163
+ // Subtle geodesic grid lines (3 concentric circles)
164
+ context.strokeStyle = COLORS.grid;
165
+ context.lineWidth = 0.5;
166
+ for (const fraction of [0.33, 0.66, 0.88]) {
167
+ context.beginPath();
168
+ context.arc(cx, cy, r * fraction, 0, Math.PI * 2);
169
+ context.stroke();
170
+ }
171
+
172
+ // Offset all positions to disk center
173
+ function toCanvas(x: number, y: number): [number, number] {
174
+ return [x - diskRadius + cx, y - diskRadius + cy];
175
+ }
176
+
177
+ // ── Edges ──
178
+ context.font = FONT;
179
+ for (const edge of snapshot.edges) {
180
+ const src = projected.get(edge.sourceId);
181
+ const tgt = projected.get(edge.targetId);
182
+ if (!src || !tgt) continue;
183
+
184
+ const progress = edgeOffsets.get(edge.id) ?? 0;
185
+ if (progress <= 0) continue;
186
+
187
+ const [sx, sy] = toCanvas(src.x + src.width / 2, src.y + src.height / 2);
188
+ const [tx, ty] = toCanvas(tgt.x + tgt.width / 2, tgt.y + tgt.height / 2);
189
+
190
+ const [ax, ay, pcx, pcy, ex, ey, ae] = getArrow(sx, sy, tx, ty, {
191
+ bow: 0.15,
192
+ stretch: 0.3,
193
+ padStart: src.width / 2 + 4,
194
+ padEnd: tgt.width / 2 + 4,
195
+ });
196
+
197
+ const dx = ex - ax, dy = ey - ay;
198
+ const approxLen = Math.sqrt(dx * dx + dy * dy) * 1.2;
199
+ const dash = approxLen * progress;
200
+
201
+ context.save();
202
+ context.strokeStyle = progress >= 1.0 ? COLORS.edgeLine : COLORS.edgeFade;
203
+ context.lineWidth = 1.5;
204
+ context.setLineDash([dash, approxLen]);
205
+ context.beginPath();
206
+ context.moveTo(ax, ay);
207
+ context.quadraticCurveTo(pcx, pcy, ex, ey);
208
+ context.stroke();
209
+ context.restore();
210
+
211
+ if (progress >= 1.0) {
212
+ context.save();
213
+ context.fillStyle = COLORS.edgeLine;
214
+ context.translate(ex, ey);
215
+ context.rotate(ae);
216
+ context.beginPath();
217
+ context.moveTo(0, 0);
218
+ context.lineTo(-7, -3);
219
+ context.lineTo(-7, 3);
220
+ context.closePath();
221
+ context.fill();
222
+ context.restore();
223
+ }
224
+ }
225
+
226
+ // ── Nodes ──
227
+ for (const node of snapshot.nodes) {
228
+ const pos = projected.get(node.id);
229
+ if (!pos) continue;
230
+
231
+ const column = (node.data?.column as string | undefined) ?? '';
232
+ const isSelected = node.id === selectedId;
233
+ const [px, py] = toCanvas(pos.x, pos.y);
234
+ const { width: w, height: h } = pos;
235
+
236
+ context.fillStyle = isSelected ? COLORS.selected : (COLUMN_FILL[column] ?? COLORS.node);
237
+ context.strokeStyle = isSelected ? COLORS.selected : (COLUMN_BORDER[column] ?? COLORS.nodeBorder);
238
+ context.lineWidth = isSelected ? 2 : 1;
239
+
240
+ context.beginPath();
241
+ context.roundRect(px, py, w, h, NODE_RADIUS);
242
+ context.fill();
243
+ context.stroke();
244
+
245
+ context.fillStyle = COLORS.nodeText;
246
+ context.font = FONT;
247
+ context.textAlign = 'center';
248
+ context.textBaseline = 'middle';
249
+ const label = node.label.length > 16 ? node.label.slice(0, 14) + '…' : node.label;
250
+ context.fillText(label, px + w / 2, py + h / 2, w - 12);
251
+ }
252
+
253
+ // ── Inhabitants (S³ orientations) ────────────────────────────────────────
254
+ // Project each inhabitant quaternion to disk coords here (diskRadius known).
255
+ // Trails are in unit-disk space; scale by r to get pixel coords.
256
+ if (inhabitants.length > 0) {
257
+ for (const inh of inhabitants) {
258
+ const [w, i, j, k] = inh.orientation;
259
+ const agent = quaternionToDisk(w, i, j, k, 1.0); // unit coords; scaled by r below
260
+ const trail = agentTrails.get(inh.id) ?? [];
261
+
262
+ // Draw fading trail
263
+ if (trail.length > 1) {
264
+ for (let t = 0; t < trail.length - 1; t++) {
265
+ const alpha = (t / (trail.length - 1)) * 0.5;
266
+ const pt = trail[t];
267
+ const pn = trail[t + 1];
268
+ context.save();
269
+ context.strokeStyle = `rgba(212, 148, 58, ${alpha})`; // brass fade
270
+ context.lineWidth = 1.5;
271
+ context.beginPath();
272
+ context.moveTo(cx + pt.u * r, cy + pt.v * r);
273
+ context.lineTo(cx + pn.u * r, cy + pn.v * r);
274
+ context.stroke();
275
+ context.restore();
276
+ }
277
+ }
278
+
279
+ // Draw glowing agent dot
280
+ const ax = cx + agent.u * r;
281
+ const ay = cy + agent.v * r;
282
+ const dotR = 5;
283
+
284
+ // Outer glow
285
+ const glow = context.createRadialGradient(ax, ay, 0, ax, ay, dotR * 3);
286
+ glow.addColorStop(0, 'rgba(212, 148, 58, 0.6)');
287
+ glow.addColorStop(1, 'rgba(212, 148, 58, 0)');
288
+ context.fillStyle = glow;
289
+ context.beginPath();
290
+ context.arc(ax, ay, dotR * 3, 0, Math.PI * 2);
291
+ context.fill();
292
+
293
+ // Core dot
294
+ context.fillStyle = '#d4943a';
295
+ context.beginPath();
296
+ context.arc(ax, ay, dotR, 0, Math.PI * 2);
297
+ context.fill();
298
+
299
+ // Short ID label
300
+ context.fillStyle = 'rgba(232, 224, 208, 0.85)';
301
+ context.font = '9px var(--font-mono, monospace)';
302
+ context.textAlign = 'center';
303
+ context.textBaseline = 'top';
304
+ context.fillText(inh.id.slice(0, 6), ax, ay + dotR + 3);
305
+ }
306
+ }
307
+
308
+ context.restore(); // end clip
309
+ }
310
+ </script>
311
+
312
+ <Canvas {width} {height}>
313
+ <Layer {render} />
314
+ </Canvas>
@@ -0,0 +1,6 @@
1
+ // N2 Manifold Disk — Poincaré disk projection wrapping n1-flow-canvas
2
+ // κ = 1.0 fixed (standard Poincaré disk, no prop)
3
+
4
+ export { default as ManifoldDisk } from './ManifoldDisk.svelte';
5
+ export { mobiusTransform, projectToDisk, poincareDistance, quaternionToDisk } from './poincare.js';
6
+ export type { GraphStateApi, GraphSnapshot } from '../n1-flow-canvas/types.js';
@@ -0,0 +1,184 @@
1
+ // Poincaré disk projection — ~50 lines of pure math
2
+ // κ = 1.0 fixed (standard Poincaré disk)
3
+ // S-02: pure functions, no side effects, deterministic
4
+
5
+ import type { NodeLayout } from '../n1-flow-canvas/types.js';
6
+
7
+ // Complex number as [real, imag] tuple
8
+ type Complex = readonly [number, number];
9
+
10
+ // ── Complex arithmetic ────────────────────────────────────────────────────────
11
+
12
+ function cadd(a: Complex, b: Complex): Complex {
13
+ return [a[0] + b[0], a[1] + b[1]];
14
+ }
15
+
16
+ function csub(a: Complex, b: Complex): Complex {
17
+ return [a[0] - b[0], a[1] - b[1]];
18
+ }
19
+
20
+ function cmul(a: Complex, b: Complex): Complex {
21
+ return [a[0] * b[0] - a[1] * b[1], a[0] * b[1] + a[1] * b[0]];
22
+ }
23
+
24
+ function cdiv(a: Complex, b: Complex): Complex {
25
+ const denom = b[0] * b[0] + b[1] * b[1];
26
+ return [(a[0] * b[0] + a[1] * b[1]) / denom, (a[1] * b[0] - a[0] * b[1]) / denom];
27
+ }
28
+
29
+ function cconj(a: Complex): Complex {
30
+ return [a[0], -a[1]];
31
+ }
32
+
33
+ function cabs(a: Complex): number {
34
+ return Math.sqrt(a[0] * a[0] + a[1] * a[1]);
35
+ }
36
+
37
+ // ── Möbius transform ──────────────────────────────────────────────────────────
38
+
39
+ /**
40
+ * Möbius transform that moves z0 to the origin.
41
+ * f(z) = (z − z0) / (1 − conj(z0) · z)
42
+ * Preserves the unit disk: |z| ≤ 1 → |f(z)| ≤ 1
43
+ */
44
+ export function mobiusTransform(z: Complex, z0: Complex): Complex {
45
+ const num = csub(z, z0);
46
+ const den = csub([1, 0], cmul(cconj(z0), z));
47
+ return cdiv(num, den);
48
+ }
49
+
50
+ /**
51
+ * Project Euclidean node positions onto the Poincaré disk.
52
+ *
53
+ * Steps:
54
+ * 1. Normalize all positions to [-1, 1]² using bounding box
55
+ * 2. Apply Möbius transform to center focusNodeId at origin
56
+ * 3. Scale to diskRadius
57
+ *
58
+ * Returns a new Map — does not mutate input.
59
+ */
60
+ export function projectToDisk(
61
+ positions: Map<string, NodeLayout>,
62
+ focusNodeId: string | null,
63
+ diskRadius: number,
64
+ ): Map<string, { x: number; y: number; width: number; height: number }> {
65
+ if (positions.size === 0) return new Map();
66
+
67
+ // Bounding box
68
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
69
+ for (const p of positions.values()) {
70
+ const cx = p.x + p.width / 2;
71
+ const cy = p.y + p.height / 2;
72
+ if (cx < minX) minX = cx;
73
+ if (cy < minY) minY = cy;
74
+ if (cx > maxX) maxX = cx;
75
+ if (cy > maxY) maxY = cy;
76
+ }
77
+
78
+ const rangeX = maxX - minX || 1;
79
+ const rangeY = maxY - minY || 1;
80
+ const range = Math.max(rangeX, rangeY);
81
+
82
+ // Normalize to Poincaré disk unit coords, then scale to 0.9 max radius
83
+ function toComplex(p: NodeLayout): Complex {
84
+ const cx = p.x + p.width / 2;
85
+ const cy = p.y + p.height / 2;
86
+ return [
87
+ ((cx - minX) / range * 2 - rangeX / range) * 0.9,
88
+ ((cy - minY) / range * 2 - rangeY / range) * 0.9,
89
+ ];
90
+ }
91
+
92
+ // Focus center in normalized coords
93
+ const focusPos = focusNodeId ? positions.get(focusNodeId) : null;
94
+ const z0: Complex = focusPos ? toComplex(focusPos) : [0, 0];
95
+
96
+ const result = new Map<string, { x: number; y: number; width: number; height: number }>();
97
+
98
+ for (const [id, p] of positions) {
99
+ const z = toComplex(p);
100
+ const projected = mobiusTransform(z, z0);
101
+ // Clamp to unit disk (numerical safety)
102
+ const r = cabs(projected);
103
+ const clamped: Complex = r > 0.99 ? [projected[0] * 0.99 / r, projected[1] * 0.99 / r] : projected;
104
+
105
+ result.set(id, {
106
+ x: clamped[0] * diskRadius + diskRadius - p.width / 2,
107
+ y: clamped[1] * diskRadius + diskRadius - p.height / 2,
108
+ width: p.width,
109
+ height: p.height,
110
+ });
111
+ }
112
+
113
+ return result;
114
+ }
115
+
116
+ /**
117
+ * Poincaré distance between two points in the disk.
118
+ * d(u,v) = 2 * atanh(|u - v| / |1 - conj(u)v|)
119
+ */
120
+ export function poincareDistance(u: Complex, v: Complex): number {
121
+ const num = cabs(csub(u, v));
122
+ const den = cabs(csub([1, 0], cmul(cconj(u), v)));
123
+ return 2 * Math.atanh(num / den);
124
+ }
125
+
126
+ // ── S³ quaternion projection ──────────────────────────────────────────────────
127
+
128
+ /**
129
+ * A point on the Poincaré disk derived from a unit quaternion on S³.
130
+ *
131
+ * Fields:
132
+ * - `u`, `v` — disk coordinates in pixels, relative to disk centre
133
+ * - `id` — inhabitant identifier
134
+ * - `allay` — accumulated arc length (monotonically increasing)
135
+ */
136
+ export interface AgentPosition {
137
+ id: string;
138
+ u: number; // disk x, relative to centre
139
+ v: number; // disk y, relative to centre
140
+ allay: number;
141
+ }
142
+
143
+ /**
144
+ * Project a unit quaternion [w, i, j, k] onto the Poincaré disk.
145
+ *
146
+ * Uses axis-angle parameterisation of S³:
147
+ * θ = 2·acos(clamp(w, −1, 1)) — total rotation angle
148
+ * axis = (i, j, k) / sin(θ/2) — unit rotation axis
149
+ * r = θ/π · 0.95 · diskRadius — radial distance on disk
150
+ * u = r · axis.i — Accumulation axis → disk X
151
+ * v = r · axis.j — Actuation axis → disk Y
152
+ *
153
+ * Properties:
154
+ * - Identity quaternion (w=1) maps to disk centre — zero rotation.
155
+ * - Maximally rotated quaternion (w=−1, θ=2π) maps to boundary.
156
+ * - The rotation axis angle on disk reflects semantic orientation:
157
+ * i=Accumulation (X), j=Actuation (Y), k=Relation (depth, ignored here).
158
+ *
159
+ * S-02: pure function, no side effects.
160
+ */
161
+ export function quaternionToDisk(
162
+ w: number,
163
+ i: number,
164
+ j: number,
165
+ _k: number,
166
+ diskRadius: number,
167
+ ): { u: number; v: number } {
168
+ const wClamped = Math.max(-1, Math.min(1, w));
169
+ const theta = 2 * Math.acos(wClamped);
170
+
171
+ if (theta < 1e-9) {
172
+ // Identity — centre of disk
173
+ return { u: 0, v: 0 };
174
+ }
175
+
176
+ const sinHalf = Math.sin(theta / 2);
177
+ // r in [0, 0.95 * diskRadius]; maps θ=0 → centre, θ=π → boundary
178
+ const r = (theta / Math.PI) * 0.95 * diskRadius;
179
+
180
+ return {
181
+ u: r * (i / sinHalf),
182
+ v: r * (j / sinHalf),
183
+ };
184
+ }
@@ -0,0 +1,180 @@
1
+ <script lang="ts">
2
+ // N3 ForceGraph — Canvas 2D force-directed graph wrapper (vasturiano/force-graph)
3
+ // wt-797: Accepts ForceGraphData, renders with health-aware node/edge callbacks
4
+ //
5
+ // S-02: No Math.random in rendering. Pulse tick is deterministic counter.
6
+ // S-05: Force simulation has cooldownTicks + cooldownTime bounds.
7
+ // Component destroys graph instance on unmount.
8
+
9
+ import { onMount, onDestroy } from 'svelte';
10
+ import type { ForceGraphData, ForceGraphNode, ForceGraphLink, HealthNodeConfig, AnimatedEdgeConfig, TooltipPosition } from './types.js';
11
+ import { createHealthNodeRenderer, createHealthNodeAreaPaint, advancePulseTick } from './health-node.js';
12
+ import { createAnimatedEdgeRenderer } from './animated-edge.js';
13
+
14
+ // ── Props ────────────────────────────────────────────────────────────────────
15
+
16
+ interface Props {
17
+ /** Graph data: nodes + links */
18
+ data: ForceGraphData;
19
+ /** Container width. Default 800. */
20
+ width?: number;
21
+ /** Container height. Default 600. */
22
+ height?: number;
23
+ /** Health node rendering config */
24
+ nodeConfig?: HealthNodeConfig;
25
+ /** Animated edge rendering config */
26
+ edgeConfig?: AnimatedEdgeConfig;
27
+ /** Background color. Default '#1a1a1a'. */
28
+ backgroundColor?: string;
29
+ /** Callback when a node is clicked */
30
+ onNodeClick?: (node: ForceGraphNode) => void;
31
+ /** Callback when a node is hovered (null = hover left) */
32
+ onNodeHover?: (node: ForceGraphNode | null, position: TooltipPosition) => void;
33
+ /** Callback when a link is clicked */
34
+ onLinkClick?: (link: ForceGraphLink) => void;
35
+ /** Force simulation cooldown ticks. Default 300. S-05: bounded. */
36
+ cooldownTicks?: number;
37
+ /** D3 force charge strength. Default -120. */
38
+ chargeStrength?: number;
39
+ /** D3 force link distance. Default 80. */
40
+ linkDistance?: number;
41
+ }
42
+
43
+ let {
44
+ data,
45
+ width = 800,
46
+ height = 600,
47
+ nodeConfig,
48
+ edgeConfig,
49
+ backgroundColor = '#1a1a1a',
50
+ onNodeClick,
51
+ onNodeHover,
52
+ onLinkClick,
53
+ cooldownTicks = 300,
54
+ chargeStrength = -120,
55
+ linkDistance = 80,
56
+ }: Props = $props();
57
+
58
+ // ── Internal state ──────────────────────────────────────────────────────────
59
+
60
+ let container: HTMLDivElement;
61
+ let graph: any = null; // ForceGraph instance (untyped — library uses chainable API)
62
+
63
+ // ── Lifecycle ───────────────────────────────────────────────────────────────
64
+
65
+ onMount(async () => {
66
+ // Dynamic import: force-graph requires DOM
67
+ const ForceGraphLib = (await import('force-graph')).default;
68
+
69
+ // force-graph ships class-style TS types but is actually a factory function
70
+ graph = (ForceGraphLib as unknown as () => (el: HTMLElement) => any)()(container)
71
+ .width(width)
72
+ .height(height)
73
+ .backgroundColor(backgroundColor)
74
+ // Node rendering
75
+ .nodeCanvasObject(createHealthNodeRenderer(nodeConfig))
76
+ .nodeCanvasObjectMode(() => 'replace')
77
+ .nodePointerAreaPaint(createHealthNodeAreaPaint(nodeConfig))
78
+ // Edge rendering
79
+ .linkCanvasObject(createAnimatedEdgeRenderer(edgeConfig))
80
+ .linkCanvasObjectMode(() => 'replace')
81
+ // Link identifiers
82
+ .linkSource('source')
83
+ .linkTarget('target')
84
+ // Force parameters
85
+ .cooldownTicks(cooldownTicks)
86
+ // Interaction
87
+ .onNodeClick((node: ForceGraphNode) => {
88
+ onNodeClick?.(node);
89
+ })
90
+ .onNodeHover((node: ForceGraphNode | null) => {
91
+ // Change cursor
92
+ container.style.cursor = node ? 'pointer' : 'default';
93
+ // Emit hover with position
94
+ if (onNodeHover) {
95
+ const pos: TooltipPosition = node
96
+ ? { x: (node.x ?? 0), y: (node.y ?? 0), visible: true }
97
+ : { x: 0, y: 0, visible: false };
98
+ onNodeHover(node, pos);
99
+ }
100
+ })
101
+ .onLinkClick((link: ForceGraphLink) => onLinkClick?.(link))
102
+ // Drag to pin
103
+ .onNodeDragEnd((node: ForceGraphNode) => {
104
+ node.fx = node.x;
105
+ node.fy = node.y;
106
+ });
107
+
108
+ // Configure d3 forces after graph creation
109
+ graph.d3Force('charge')?.strength(chargeStrength);
110
+ graph.d3Force('link')?.distance(linkDistance);
111
+
112
+ // Engine tick hook for pulse animation
113
+ graph.onEngineTick(() => advancePulseTick());
114
+
115
+ // Initial data
116
+ graph.graphData({
117
+ nodes: data.nodes,
118
+ links: data.links,
119
+ });
120
+ });
121
+
122
+ onDestroy(() => {
123
+ if (graph) {
124
+ graph._destructor();
125
+ graph = null;
126
+ }
127
+ });
128
+
129
+ // ── Reactive updates ───────────────────────────────────────────────────────
130
+
131
+ $effect(() => {
132
+ if (!graph) return;
133
+ graph.graphData({
134
+ nodes: data.nodes,
135
+ links: data.links,
136
+ });
137
+ });
138
+
139
+ $effect(() => {
140
+ if (!graph) return;
141
+ graph.width(width).height(height);
142
+ });
143
+
144
+ // ── Public API (exposed via bind:this on wrapper) ──────────────────────────
145
+
146
+ export function zoomToFit(duration = 400, padding = 40) {
147
+ graph?.zoomToFit(duration, padding);
148
+ }
149
+
150
+ export function centerAt(x: number, y: number, duration = 400) {
151
+ graph?.centerAt(x, y, duration);
152
+ }
153
+
154
+ export function zoom(factor: number, duration = 400) {
155
+ graph?.zoom(factor, duration);
156
+ }
157
+
158
+ export function reheat() {
159
+ graph?.d3ReheatSimulation();
160
+ }
161
+ </script>
162
+
163
+ <div
164
+ bind:this={container}
165
+ class="n3-force-graph"
166
+ style:width="{width}px"
167
+ style:height="{height}px"
168
+ ></div>
169
+
170
+ <style>
171
+ .n3-force-graph {
172
+ position: relative;
173
+ overflow: hidden;
174
+ border-radius: var(--radius-md, 8px);
175
+ }
176
+
177
+ .n3-force-graph :global(canvas) {
178
+ border-radius: var(--radius-md, 8px);
179
+ }
180
+ </style>