@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,99 @@
1
+ <script lang="ts">
2
+ // N3 GraphTooltip — Floating tooltip positioned by graph hover events
3
+ // wt-800: Slot-based content, positioned near hovered node
4
+ // Zero domain knowledge — any content can be rendered inside
5
+ //
6
+ // S-05: visibility toggled by prop, no timers
7
+
8
+ import type { Snippet } from 'svelte';
9
+
10
+ interface Props {
11
+ /** Whether the tooltip is visible */
12
+ visible?: boolean;
13
+ /** Screen X position (from graph coordinate transform) */
14
+ x?: number;
15
+ /** Screen Y position */
16
+ y?: number;
17
+ /** Offset from anchor point. Default 12. */
18
+ offset?: number;
19
+ /** Max width in px. Default 280. */
20
+ maxWidth?: number;
21
+ /** Content slot */
22
+ children?: Snippet;
23
+ }
24
+
25
+ let {
26
+ visible = false,
27
+ x = 0,
28
+ y = 0,
29
+ offset = 12,
30
+ maxWidth = 280,
31
+ children,
32
+ }: Props = $props();
33
+
34
+ // Keep tooltip within viewport
35
+ let tooltipEl: HTMLDivElement | undefined = $state();
36
+
37
+ const style = $derived.by(() => {
38
+ // Position below and to the right of the anchor by default
39
+ let left = x + offset;
40
+ let top = y + offset;
41
+
42
+ // Viewport clamping (approximate — exact requires measuring after render)
43
+ if (typeof window !== 'undefined') {
44
+ if (left + maxWidth > window.innerWidth - 16) {
45
+ left = x - maxWidth - offset;
46
+ }
47
+ if (top + 120 > window.innerHeight - 16) {
48
+ top = y - 120 - offset;
49
+ }
50
+ }
51
+
52
+ return `left: ${left}px; top: ${top}px; max-width: ${maxWidth}px;`;
53
+ });
54
+ </script>
55
+
56
+ {#if visible && children}
57
+ <div
58
+ bind:this={tooltipEl}
59
+ class="n3-graph-tooltip"
60
+ style={style}
61
+ role="tooltip"
62
+ >
63
+ {@render children()}
64
+ </div>
65
+ {/if}
66
+
67
+ <style>
68
+ .n3-graph-tooltip {
69
+ position: fixed;
70
+ z-index: 1000;
71
+ pointer-events: none;
72
+
73
+ background: var(--bg-elevated, #2a2a2a);
74
+ border: 1px solid var(--border-subtle, rgba(255, 255, 255, 0.06));
75
+ border-radius: var(--radius-sm, 6px);
76
+ padding: var(--space-2, 0.5rem) var(--space-3, 0.75rem);
77
+
78
+ color: var(--text-primary, #f2efe9);
79
+ font-family: var(--font-mono, 'JetBrains Mono', monospace);
80
+ font-size: 0.75rem;
81
+ line-height: 1.4;
82
+
83
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
84
+
85
+ /* Fade in */
86
+ animation: n3-tooltip-fade 120ms ease-out;
87
+ }
88
+
89
+ @keyframes n3-tooltip-fade {
90
+ from {
91
+ opacity: 0;
92
+ transform: translateY(4px);
93
+ }
94
+ to {
95
+ opacity: 1;
96
+ transform: translateY(0);
97
+ }
98
+ }
99
+ </style>
@@ -0,0 +1,107 @@
1
+ // N3 Animated Edge — linkCanvasObject callback for force-graph
2
+ // wt-799: Three modes: marching-ants (active), dashed-static (stub), solid (wired)
3
+ // Zero domain knowledge — pure visual primitive
4
+ //
5
+ // S-02: animation driven by tick counter, not wall clock
6
+ // S-05: bounded by force-graph's own cooldown/frame budget
7
+
8
+ import type { ForceGraphNode, ForceGraphLink, AnimatedEdgeConfig } from './types.js';
9
+ import { getPulseTick } from './health-node.js';
10
+
11
+ const DEFAULTS: Required<AnimatedEdgeConfig> = {
12
+ dashLength: 8,
13
+ gapLength: 4,
14
+ marchSpeed: 0.5,
15
+ };
16
+
17
+ /**
18
+ * Create a `linkCanvasObject` callback for force-graph.
19
+ *
20
+ * Modes:
21
+ * - `active`: marching-ants animation (data flowing)
22
+ * - `stub`: dashed static line (connection exists but not wired)
23
+ * - `wired`: solid line (fully connected)
24
+ *
25
+ * Usage:
26
+ * ```ts
27
+ * import { createAnimatedEdgeRenderer } from '@kos/design-system/n3-force-graph';
28
+ * graph.linkCanvasObject(createAnimatedEdgeRenderer());
29
+ * graph.linkCanvasObjectMode(() => 'replace');
30
+ * ```
31
+ */
32
+ export function createAnimatedEdgeRenderer(
33
+ config?: AnimatedEdgeConfig,
34
+ ): (link: ForceGraphLink, ctx: CanvasRenderingContext2D, globalScale: number) => void {
35
+ const { dashLength, gapLength, marchSpeed } = { ...DEFAULTS, ...config };
36
+
37
+ return (link: ForceGraphLink, ctx: CanvasRenderingContext2D, globalScale: number) => {
38
+ // force-graph mutates source/target to resolved node objects at runtime
39
+ const source = link.source as unknown as ForceGraphNode;
40
+ const target = link.target as unknown as ForceGraphNode;
41
+
42
+ const sx = source.x ?? 0;
43
+ const sy = source.y ?? 0;
44
+ const tx = target.x ?? 0;
45
+ const ty = target.y ?? 0;
46
+
47
+ ctx.save();
48
+
49
+ const mode = link.mode ?? 'wired';
50
+
51
+ switch (mode) {
52
+ case 'active': {
53
+ // Marching-ants: animated dash offset driven by pulse tick
54
+ const offset = getPulseTick() * marchSpeed;
55
+ ctx.setLineDash([dashLength / globalScale, gapLength / globalScale]);
56
+ ctx.lineDashOffset = -offset / globalScale;
57
+ ctx.strokeStyle = '#b87333'; // brass — active flow
58
+ ctx.lineWidth = 2 / globalScale;
59
+ break;
60
+ }
61
+ case 'stub': {
62
+ // Dashed static: connection exists but unwired
63
+ ctx.setLineDash([4 / globalScale, 6 / globalScale]);
64
+ ctx.lineDashOffset = 0;
65
+ ctx.strokeStyle = '#5a4a30'; // muted brass
66
+ ctx.lineWidth = 1 / globalScale;
67
+ break;
68
+ }
69
+ case 'wired':
70
+ default: {
71
+ // Solid: fully connected
72
+ ctx.setLineDash([]);
73
+ ctx.strokeStyle = '#706858'; // tertiary text color
74
+ ctx.lineWidth = 1.5 / globalScale;
75
+ break;
76
+ }
77
+ }
78
+
79
+ // Draw line
80
+ ctx.beginPath();
81
+ ctx.moveTo(sx, sy);
82
+ ctx.lineTo(tx, ty);
83
+ ctx.stroke();
84
+
85
+ // Arrowhead for directed links (except stub)
86
+ if (mode !== 'stub') {
87
+ const angle = Math.atan2(ty - sy, tx - sx);
88
+ const arrowSize = 6 / globalScale;
89
+
90
+ ctx.fillStyle = ctx.strokeStyle;
91
+ ctx.beginPath();
92
+ ctx.moveTo(tx, ty);
93
+ ctx.lineTo(
94
+ tx - arrowSize * Math.cos(angle - Math.PI / 7),
95
+ ty - arrowSize * Math.sin(angle - Math.PI / 7),
96
+ );
97
+ ctx.lineTo(
98
+ tx - arrowSize * Math.cos(angle + Math.PI / 7),
99
+ ty - arrowSize * Math.sin(angle + Math.PI / 7),
100
+ );
101
+ ctx.closePath();
102
+ ctx.fill();
103
+ }
104
+
105
+ ctx.restore();
106
+ };
107
+ }
@@ -0,0 +1,128 @@
1
+ // N3 Health Node — nodeCanvasObject callback for force-graph
2
+ // wt-798: Renders nodes with health-derived color, pulse animation, size scaling
3
+ // Zero domain knowledge — pure visual primitive
4
+ //
5
+ // S-02: health values are integer permille. No Math.random.
6
+ // S-05: pulse uses rAF tick count (bounded by force-graph's own cooldown).
7
+
8
+ import type { ForceGraphNode, HealthNodeConfig } from './types.js';
9
+ import { healthToColor, healthToGlow } from './types.js';
10
+
11
+ const DEFAULTS: Required<HealthNodeConfig> = {
12
+ baseRadius: 6,
13
+ pulseEnabled: true,
14
+ pulseAmplitude: 0.02,
15
+ };
16
+
17
+ // Global tick counter for deterministic pulse — incremented by the ForceGraph
18
+ // component on each engine tick. Avoids Date.now() / performance.now() in render.
19
+ let _pulseTick = 0;
20
+ export function advancePulseTick(): void { _pulseTick++; }
21
+ export function getPulseTick(): number { return _pulseTick; }
22
+
23
+ /**
24
+ * Create a `nodeCanvasObject` callback for force-graph.
25
+ *
26
+ * Usage:
27
+ * ```ts
28
+ * import { createHealthNodeRenderer } from '@kos/design-system/n3-force-graph';
29
+ * graph.nodeCanvasObject(createHealthNodeRenderer({ baseRadius: 8 }));
30
+ * graph.nodeCanvasObjectMode(() => 'replace');
31
+ * ```
32
+ */
33
+ export function createHealthNodeRenderer(
34
+ config?: HealthNodeConfig,
35
+ ): (node: ForceGraphNode, ctx: CanvasRenderingContext2D, globalScale: number) => void {
36
+ const { baseRadius, pulseEnabled, pulseAmplitude } = { ...DEFAULTS, ...config };
37
+
38
+ return (node: ForceGraphNode, ctx: CanvasRenderingContext2D, globalScale: number) => {
39
+ const x = node.x ?? 0;
40
+ const y = node.y ?? 0;
41
+ const weight = node.weight ?? 1;
42
+ const health = node.health ?? 0;
43
+
44
+ // Size: base * sqrt(weight) for area-proportional scaling
45
+ let radius = baseRadius * Math.sqrt(weight);
46
+
47
+ // Pulse: subtle scale oscillation on healthy nodes
48
+ if (pulseEnabled && health >= 500) {
49
+ // Deterministic sine wave from tick counter, not wall clock
50
+ const phase = (_pulseTick * 0.05) % (2 * Math.PI);
51
+ const pulse = 1.0 + pulseAmplitude * Math.sin(phase);
52
+ radius *= pulse;
53
+ }
54
+
55
+ const color = healthToColor(health);
56
+ const glowAlpha = healthToGlow(health);
57
+
58
+ // Glow (outer ring via shadow)
59
+ if (glowAlpha > 0) {
60
+ ctx.beginPath();
61
+ ctx.arc(x, y, radius + 3 / globalScale, 0, 2 * Math.PI);
62
+ // Parse hex → rgba for the fill
63
+ const r = parseInt(color.slice(1, 3), 16);
64
+ const g = parseInt(color.slice(3, 5), 16);
65
+ const b = parseInt(color.slice(5, 7), 16);
66
+ ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${glowAlpha})`;
67
+ ctx.shadowColor = color;
68
+ ctx.shadowBlur = 8 / globalScale;
69
+ ctx.fill();
70
+ ctx.shadowColor = 'transparent';
71
+ ctx.shadowBlur = 0;
72
+ }
73
+
74
+ // Node circle
75
+ ctx.beginPath();
76
+ ctx.arc(x, y, radius, 0, 2 * Math.PI);
77
+ ctx.fillStyle = color;
78
+ ctx.fill();
79
+
80
+ // Border
81
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)';
82
+ ctx.lineWidth = 1 / globalScale;
83
+ ctx.stroke();
84
+
85
+ // Label (below node, only when zoomed in enough)
86
+ if (globalScale >= 0.6) {
87
+ const fontSize = Math.max(10, 12 / globalScale);
88
+ ctx.font = `${fontSize}px var(--font-mono, 'JetBrains Mono', monospace)`;
89
+ ctx.textAlign = 'center';
90
+ ctx.textBaseline = 'top';
91
+ ctx.fillStyle = '#e8e0d0';
92
+
93
+ const label = node.label.length > 20 ? node.label.slice(0, 18) + '…' : node.label;
94
+ ctx.fillText(label, x, y + radius + 4 / globalScale);
95
+ }
96
+
97
+ // Type indicator: app nodes get a ring, ADR nodes get a diamond
98
+ if (node.type === 'app') {
99
+ ctx.beginPath();
100
+ ctx.arc(x, y, radius + 1.5 / globalScale, 0, 2 * Math.PI);
101
+ ctx.strokeStyle = '#b87333'; // brass
102
+ ctx.lineWidth = 2 / globalScale;
103
+ ctx.stroke();
104
+ }
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Create a `nodePointerAreaPaint` callback for hit detection.
110
+ * Returns a circle matching the visual node size.
111
+ */
112
+ export function createHealthNodeAreaPaint(
113
+ config?: HealthNodeConfig,
114
+ ): (node: ForceGraphNode, paintColor: string, ctx: CanvasRenderingContext2D, globalScale: number) => void {
115
+ const { baseRadius } = { ...DEFAULTS, ...config };
116
+
117
+ return (node: ForceGraphNode, paintColor: string, ctx: CanvasRenderingContext2D) => {
118
+ const x = node.x ?? 0;
119
+ const y = node.y ?? 0;
120
+ const weight = node.weight ?? 1;
121
+ const radius = baseRadius * Math.sqrt(weight) + 4; // generous hit area
122
+
123
+ ctx.beginPath();
124
+ ctx.arc(x, y, radius, 0, 2 * Math.PI);
125
+ ctx.fillStyle = paintColor;
126
+ ctx.fill();
127
+ };
128
+ }
@@ -0,0 +1,31 @@
1
+ // N3 Force Graph — Force-directed graph with health-aware rendering
2
+ // Depends on: force-graph (vasturiano)
3
+ //
4
+ // Components:
5
+ // ForceGraph.svelte — Canvas 2D wrapper (wt-797)
6
+ // health-node.ts — nodeCanvasObject callback (wt-798)
7
+ // animated-edge.ts — linkCanvasObject callback (wt-799)
8
+ // GraphTooltip.svelte — Floating tooltip (wt-800)
9
+
10
+ export type {
11
+ ForceGraphNode,
12
+ ForceGraphLink,
13
+ ForceGraphData,
14
+ HealthNodeConfig,
15
+ AnimatedEdgeConfig,
16
+ TooltipPosition,
17
+ } from './types.js';
18
+
19
+ export { healthToColor, healthToGlow } from './types.js';
20
+
21
+ export { default as ForceGraph } from './ForceGraph.svelte';
22
+ export { default as GraphTooltip } from './GraphTooltip.svelte';
23
+
24
+ export {
25
+ createHealthNodeRenderer,
26
+ createHealthNodeAreaPaint,
27
+ advancePulseTick,
28
+ getPulseTick,
29
+ } from './health-node.js';
30
+
31
+ export { createAnimatedEdgeRenderer } from './animated-edge.js';
@@ -0,0 +1,94 @@
1
+ // N3 Force Graph — Types
2
+ // Pure TypeScript — no Svelte, no DOM dependencies
3
+ // S-02: all health values are integer permille (0–1000)
4
+
5
+ // Re-export shared graph primitives from n1
6
+ export type { GraphNode, GraphEdge, GraphSnapshot } from '../n1-flow-canvas/types.js';
7
+
8
+ // === Force Graph Node (extends GraphNode with health/visual data) ===
9
+
10
+ export interface ForceGraphNode {
11
+ id: string;
12
+ label: string;
13
+ /** Node category for visual differentiation */
14
+ type: 'app' | 'adr' | 'domain' | string;
15
+ /** Health score in permille (0–1000). 0 = critical, 1000 = healthy. */
16
+ health: number;
17
+ /** Node visual weight — controls circle radius. Default 1. */
18
+ weight?: number;
19
+ /** Optional: pinned position (drag-to-pin). Set fx/fy to pin. */
20
+ fx?: number;
21
+ fy?: number;
22
+ /** Arbitrary domain data for tooltip/detail rendering */
23
+ data?: Record<string, unknown>;
24
+ // d3-force internal (populated by simulation)
25
+ x?: number;
26
+ y?: number;
27
+ vx?: number;
28
+ vy?: number;
29
+ index?: number;
30
+ }
31
+
32
+ export interface ForceGraphLink {
33
+ id: string;
34
+ source: string;
35
+ target: string;
36
+ /** Edge rendering mode */
37
+ mode: 'active' | 'stub' | 'wired';
38
+ /** Optional label (e.g. dependency name) */
39
+ label?: string;
40
+ }
41
+
42
+ // === Health Node Config ===
43
+
44
+ export interface HealthNodeConfig {
45
+ /** Base circle radius in px. Default 6. */
46
+ baseRadius?: number;
47
+ /** Whether to animate pulse on healthy nodes. Default true. */
48
+ pulseEnabled?: boolean;
49
+ /** Pulse amplitude (scale factor above 1.0). Default 0.02. */
50
+ pulseAmplitude?: number;
51
+ }
52
+
53
+ // === Animated Edge Config ===
54
+
55
+ export interface AnimatedEdgeConfig {
56
+ /** Marching-ants dash length. Default 8. */
57
+ dashLength?: number;
58
+ /** Marching-ants gap length. Default 4. */
59
+ gapLength?: number;
60
+ /** Animation speed (px/frame). Default 0.5. */
61
+ marchSpeed?: number;
62
+ }
63
+
64
+ // === Graph Data (input to ForceGraph) ===
65
+
66
+ export interface ForceGraphData {
67
+ nodes: ForceGraphNode[];
68
+ links: ForceGraphLink[];
69
+ }
70
+
71
+ // === Tooltip ===
72
+
73
+ export interface TooltipPosition {
74
+ x: number;
75
+ y: number;
76
+ visible: boolean;
77
+ }
78
+
79
+ // === Health → Color mapping (integer permille thresholds) ===
80
+
81
+ /** Map health permille to CSS color string. Pure function, no side effects. */
82
+ export function healthToColor(permille: number): string {
83
+ if (permille >= 800) return '#27ae60'; // green — healthy
84
+ if (permille >= 500) return '#f39c12'; // amber — partial
85
+ if (permille >= 200) return '#c0392b'; // red — blocked/stub-heavy
86
+ return '#3a3a3a'; // dark — no data / dead
87
+ }
88
+
89
+ /** Map health permille to glow opacity (0.0–0.4). Healthy = brighter glow. */
90
+ export function healthToGlow(permille: number): number {
91
+ if (permille >= 800) return 0.3;
92
+ if (permille >= 500) return 0.15;
93
+ return 0.0; // No glow for unhealthy/dead
94
+ }