@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,301 @@
1
+ // N4 LayoutOrchestrator — Coordinates algorithm providers, manages transitions
2
+ // Owns the current layout state and drives the TransitionAnimator.
3
+ // Includes 500ms debounce and 3-tick stability before suggesting.
4
+
5
+ import type {
6
+ AlgorithmProvider,
7
+ LayoutAlgorithm,
8
+ ManifoldSnapshot,
9
+ LayoutPreferences,
10
+ ClassificationResult,
11
+ LayoutSuggestion,
12
+ NodeLayout,
13
+ } from './types.js';
14
+ import { DEFAULT_LAYOUT_PREFERENCES } from './types.js';
15
+ import { classifyTopology, STABILITY_TICKS } from './classifier.js';
16
+ import { TransitionAnimator, EASING_FUNCTIONS } from './transition.js';
17
+
18
+ /** Debounce interval matching REFLEX tick cadence. */
19
+ const CLASSIFY_DEBOUNCE_MS = 500;
20
+
21
+ export interface OrchestratorState {
22
+ /** Currently active layout algorithm. */
23
+ activeAlgorithm: LayoutAlgorithm;
24
+ /** Current node positions (interpolated during transition). */
25
+ positions: Map<string, NodeLayout>;
26
+ /** Current BFS order for edge reveal. */
27
+ bfsOrder: ReadonlyArray<string>;
28
+ /** Whether a transition is in progress. */
29
+ transitioning: boolean;
30
+ /** Last classification result. */
31
+ lastClassification: ClassificationResult | null;
32
+ /** Current suggestion (null if none, or dismissed/pinned). */
33
+ suggestion: LayoutSuggestion | null;
34
+ }
35
+
36
+ export type OrchestratorListener = (state: OrchestratorState) => void;
37
+
38
+ export class LayoutOrchestrator {
39
+ private providers: Map<LayoutAlgorithm, AlgorithmProvider>;
40
+ private prefs: LayoutPreferences;
41
+ private animator: TransitionAnimator;
42
+ private listeners: OrchestratorListener[] = [];
43
+
44
+ // Current state
45
+ private _activeAlgorithm: LayoutAlgorithm = 'manifold';
46
+ private _positions: Map<string, NodeLayout> = new Map();
47
+ private _bfsOrder: ReadonlyArray<string> = [];
48
+ private _transitioning = false;
49
+ private _lastClassification: ClassificationResult | null = null;
50
+ private _suggestion: LayoutSuggestion | null = null;
51
+ private _abortTransition: (() => void) | null = null;
52
+
53
+ // Debounce state
54
+ private _debounceTimer: ReturnType<typeof setTimeout> | null = null;
55
+ private _pendingSnapshot: ManifoldSnapshot | null = null;
56
+ private _pendingWidth = 0;
57
+ private _pendingHeight = 0;
58
+
59
+ // 3-tick stability counter
60
+ private _stabilityAlgorithm: LayoutAlgorithm | null = null;
61
+ private _stabilityCount = 0;
62
+
63
+ constructor(
64
+ providers: AlgorithmProvider[],
65
+ prefs: LayoutPreferences = DEFAULT_LAYOUT_PREFERENCES,
66
+ ) {
67
+ this.providers = new Map();
68
+ for (const p of providers) {
69
+ this.providers.set(p.algorithm, p);
70
+ }
71
+ this.prefs = prefs;
72
+ this.animator = new TransitionAnimator();
73
+ }
74
+
75
+ get state(): OrchestratorState {
76
+ return {
77
+ activeAlgorithm: this._activeAlgorithm,
78
+ positions: this._positions,
79
+ bfsOrder: this._bfsOrder,
80
+ transitioning: this._transitioning,
81
+ lastClassification: this._lastClassification,
82
+ suggestion: this._suggestion,
83
+ };
84
+ }
85
+
86
+ /** Subscribe to state changes. Returns unsubscribe function. */
87
+ subscribe(listener: OrchestratorListener): () => void {
88
+ this.listeners.push(listener);
89
+ return () => {
90
+ this.listeners = this.listeners.filter(l => l !== listener);
91
+ };
92
+ }
93
+
94
+ private notify(): void {
95
+ const state = this.state;
96
+ for (const l of this.listeners) l(state);
97
+ }
98
+
99
+ /**
100
+ * Request layout for the given snapshot. Debounced to 500ms.
101
+ * If currently transitioning (AM-Morph), the request is queued.
102
+ */
103
+ requestLayout(
104
+ snapshot: ManifoldSnapshot,
105
+ canvasWidth: number,
106
+ canvasHeight: number,
107
+ ): void {
108
+ this._pendingSnapshot = snapshot;
109
+ this._pendingWidth = canvasWidth;
110
+ this._pendingHeight = canvasHeight;
111
+
112
+ // If transitioning, don't schedule — will pick up after morph completes
113
+ if (this._transitioning) return;
114
+
115
+ // Debounce
116
+ if (this._debounceTimer !== null) {
117
+ clearTimeout(this._debounceTimer);
118
+ }
119
+
120
+ // First layout (no positions yet) runs immediately
121
+ if (this._positions.size === 0) {
122
+ this.executeLayout();
123
+ return;
124
+ }
125
+
126
+ this._debounceTimer = setTimeout(() => {
127
+ this._debounceTimer = null;
128
+ this.executeLayout();
129
+ }, CLASSIFY_DEBOUNCE_MS);
130
+ }
131
+
132
+ private async executeLayout(): Promise<void> {
133
+ const snapshot = this._pendingSnapshot;
134
+ if (!snapshot || snapshot.nodes.length === 0) return;
135
+
136
+ const canvasWidth = this._pendingWidth;
137
+ const canvasHeight = this._pendingHeight;
138
+
139
+ // Abort any in-flight transition
140
+ this._abortTransition?.();
141
+
142
+ // Classify
143
+ const classification = classifyTopology(snapshot);
144
+ this._lastClassification = classification;
145
+
146
+ // Determine target algorithm
147
+ let targetAlgorithm: LayoutAlgorithm;
148
+ if (this.prefs.lockedAlgorithm !== null) {
149
+ targetAlgorithm = this.prefs.lockedAlgorithm;
150
+ } else {
151
+ targetAlgorithm = classification.recommended;
152
+ }
153
+
154
+ // Update suggestion with 3-tick stability
155
+ this.updateSuggestion(classification, targetAlgorithm);
156
+
157
+ const provider = this.providers.get(targetAlgorithm);
158
+ if (!provider) return;
159
+
160
+ // Compute target layout
161
+ const targetLayout = await provider.layout(snapshot, canvasWidth, canvasHeight);
162
+
163
+ // If no current positions or animation disabled, apply immediately
164
+ if (this._positions.size === 0 || !this.prefs.animate) {
165
+ this._activeAlgorithm = targetAlgorithm;
166
+ this._positions = targetLayout.positions;
167
+ this._bfsOrder = targetLayout.bfsOrder;
168
+ this._transitioning = false;
169
+ this.notify();
170
+ return;
171
+ }
172
+
173
+ // AM-Morph: animate from current → target
174
+ this._transitioning = true;
175
+ this._activeAlgorithm = targetAlgorithm;
176
+ this.notify();
177
+
178
+ const easing = EASING_FUNCTIONS[this.prefs.easing] ?? EASING_FUNCTIONS.easeInOutCubic;
179
+
180
+ await new Promise<void>((resolve) => {
181
+ let cancelled = false;
182
+ this._abortTransition = () => { cancelled = true; };
183
+
184
+ this.animator.animate(
185
+ this._positions,
186
+ targetLayout.positions,
187
+ this.prefs.morphDurationMs,
188
+ easing,
189
+ (frame) => {
190
+ if (cancelled) return;
191
+ this._positions = frame.positions;
192
+ this.notify();
193
+ },
194
+ () => {
195
+ this._abortTransition = null;
196
+ if (cancelled) {
197
+ resolve();
198
+ return;
199
+ }
200
+ this._positions = targetLayout.positions;
201
+ this._bfsOrder = targetLayout.bfsOrder;
202
+ this._transitioning = false;
203
+ this.notify();
204
+
205
+ // Check if a new snapshot arrived during the morph.
206
+ // Route through requestLayout (debounce) rather than calling executeLayout()
207
+ // directly — prevents unbounded promise chaining under fast data arrival.
208
+ if (this._pendingSnapshot !== snapshot) {
209
+ this.requestLayout(this._pendingSnapshot!, this._pendingWidth, this._pendingHeight);
210
+ }
211
+ resolve();
212
+ },
213
+ );
214
+ });
215
+ }
216
+
217
+ /** 3-tick stability: only suggest after classifier recommends the same alternative 3 times. */
218
+ private updateSuggestion(classification: ClassificationResult, currentTarget: LayoutAlgorithm): void {
219
+ if (this.prefs.lockedAlgorithm !== null) {
220
+ // User has pinned — no suggestions
221
+ this._suggestion = null;
222
+ return;
223
+ }
224
+
225
+ if (classification.recommended === currentTarget) {
226
+ // Classifier agrees with current — clear stability counter
227
+ this._stabilityAlgorithm = null;
228
+ this._stabilityCount = 0;
229
+ this._suggestion = null;
230
+ return;
231
+ }
232
+
233
+ // Classifier disagrees — track stability
234
+ if (this._stabilityAlgorithm === classification.recommended) {
235
+ this._stabilityCount++;
236
+ } else {
237
+ this._stabilityAlgorithm = classification.recommended;
238
+ this._stabilityCount = 1;
239
+ }
240
+
241
+ if (this._stabilityCount >= STABILITY_TICKS) {
242
+ this._suggestion = {
243
+ algorithm: classification.recommended,
244
+ confidence: classification.confidence,
245
+ reason: classification.reason,
246
+ dismissed: false,
247
+ pinned: false,
248
+ };
249
+ }
250
+ }
251
+
252
+ /** Accept a layout suggestion — switch to suggested algorithm. */
253
+ acceptSuggestion(): void {
254
+ if (!this._suggestion) return;
255
+ this._activeAlgorithm = this._suggestion.algorithm;
256
+ this._suggestion = null;
257
+ this._stabilityCount = 0;
258
+ if (this._pendingSnapshot) {
259
+ // Set transitioning before executeLayout so requestLayout calls that arrive
260
+ // before executeLayout's first await are queued rather than double-fired.
261
+ this._transitioning = true;
262
+ this.setPreferences({ lockedAlgorithm: this._activeAlgorithm });
263
+ this.executeLayout();
264
+ }
265
+ }
266
+
267
+ /** Dismiss a layout suggestion — keep current algorithm. */
268
+ dismissSuggestion(): void {
269
+ if (this._suggestion) {
270
+ this._suggestion = { ...this._suggestion, dismissed: true };
271
+ }
272
+ this._stabilityCount = 0;
273
+ this.notify();
274
+ }
275
+
276
+ /** Pin the current layout (disable auto-classification). */
277
+ pinLayout(): void {
278
+ this.prefs.lockedAlgorithm = this._activeAlgorithm;
279
+ this._suggestion = null;
280
+ this.notify();
281
+ }
282
+
283
+ /** Unpin — re-enable auto-classification. */
284
+ unpinLayout(): void {
285
+ this.prefs.lockedAlgorithm = null;
286
+ this._stabilityCount = 0;
287
+ this.notify();
288
+ }
289
+
290
+ /** Update preferences (e.g. user toggles animation). */
291
+ setPreferences(prefs: Partial<LayoutPreferences>): void {
292
+ this.prefs = { ...this.prefs, ...prefs };
293
+ }
294
+
295
+ /** Clean up. */
296
+ destroy(): void {
297
+ this._abortTransition?.();
298
+ if (this._debounceTimer !== null) clearTimeout(this._debounceTimer);
299
+ this.listeners = [];
300
+ }
301
+ }
@@ -0,0 +1,92 @@
1
+ import type { PhaseTheme, PhaseVisualParams } from './types.js';
2
+
3
+ const PHASES = ['seed', 'sprout', 'go', 'consult', 'weed', 'freeze', 'compost', 'prune'] as const;
4
+
5
+ function resolvePhaseParams(
6
+ style: CSSStyleDeclaration,
7
+ phase: string,
8
+ ): PhaseVisualParams {
9
+ const get = (prop: string) =>
10
+ style.getPropertyValue(`--n4-phase-${phase}-${prop}`).trim();
11
+
12
+ return {
13
+ background: get('background') || '#1a1a1a',
14
+ brightness: parseFloat(get('brightness')) || 0.5,
15
+ indicatorIntensity: parseFloat(get('indicator-intensity')) || 0.5,
16
+ ambientPulseSpeed: parseFloat(get('pulse-speed')) || 3,
17
+ saturation: parseFloat(get('saturation')) || 0.8,
18
+ };
19
+ }
20
+
21
+ /**
22
+ * Resolve PhaseTheme from CSS custom properties on a container element.
23
+ * Re-call on data-theme or class mutation.
24
+ */
25
+ export function resolvePhaseTheme(el: HTMLElement): PhaseTheme {
26
+ const style = getComputedStyle(el);
27
+ const theme: Record<string, PhaseVisualParams> = {};
28
+ for (const phase of PHASES) {
29
+ theme[phase] = resolvePhaseParams(style, phase);
30
+ }
31
+ return theme as unknown as PhaseTheme;
32
+ }
33
+
34
+ /** Fallback theme if CSS tokens are not set */
35
+ export const DEFAULT_PHASE_THEME: PhaseTheme = {
36
+ seed: {
37
+ background: '#1a1a1a',
38
+ brightness: 0.4,
39
+ indicatorIntensity: 0.3,
40
+ ambientPulseSpeed: 4,
41
+ saturation: 0.6,
42
+ },
43
+ sprout: {
44
+ background: '#1a1c1a',
45
+ brightness: 0.7,
46
+ indicatorIntensity: 0.7,
47
+ ambientPulseSpeed: 2.5,
48
+ saturation: 0.85,
49
+ },
50
+ go: {
51
+ background: '#1c1a18',
52
+ brightness: 1.0,
53
+ indicatorIntensity: 1.0,
54
+ ambientPulseSpeed: 1.5,
55
+ saturation: 1.0,
56
+ },
57
+ consult: {
58
+ background: '#1b1a1c',
59
+ brightness: 0.85,
60
+ indicatorIntensity: 0.85,
61
+ ambientPulseSpeed: 2.0,
62
+ saturation: 0.9,
63
+ },
64
+ weed: {
65
+ background: '#1c1b18',
66
+ brightness: 0.65,
67
+ indicatorIntensity: 0.6,
68
+ ambientPulseSpeed: 2.5,
69
+ saturation: 0.75,
70
+ },
71
+ freeze: {
72
+ background: '#1a1a1e',
73
+ brightness: 0.2,
74
+ indicatorIntensity: 0.1,
75
+ ambientPulseSpeed: 8,
76
+ saturation: 0.2,
77
+ },
78
+ compost: {
79
+ background: '#1a1a1a',
80
+ brightness: 0.25,
81
+ indicatorIntensity: 0.15,
82
+ ambientPulseSpeed: 6,
83
+ saturation: 0.3,
84
+ },
85
+ prune: {
86
+ background: '#1c1a1a',
87
+ brightness: 0.2,
88
+ indicatorIntensity: 0.1,
89
+ ambientPulseSpeed: 8,
90
+ saturation: 0.2,
91
+ },
92
+ };
@@ -0,0 +1,135 @@
1
+ import type { PinnacleEvent, LoopPhase, NodeIndicatorState } from './types.js';
2
+
3
+ /** Minimum gap (ms) between claim and action to infer "thinking" */
4
+ const THINK_GAP_MS = 2000;
5
+
6
+ /** Track per-agent state for gap-based inference */
7
+ interface AgentTracker {
8
+ lastEventSequence: number;
9
+ lastEventTime: number;
10
+ currentTask: string | null;
11
+ loopPhase: LoopPhase;
12
+ }
13
+
14
+ /**
15
+ * Tier A inference layer: derives PinnacleEvents from sigchain snapshot deltas.
16
+ *
17
+ * When tier B (direct Garden telemetry) arrives, this class is replaced.
18
+ * The PinnacleEvent shape is the same — canvas code doesn't change.
19
+ */
20
+ export class PinnacleEventInferrer {
21
+ private agents: Map<string, AgentTracker> = new Map();
22
+
23
+ /**
24
+ * Process a batch of sigchain-derived changes and return PinnacleEvents.
25
+ * Called by PinnacleView on each poller tick.
26
+ */
27
+ inferFromSnapshot(
28
+ nodes: ReadonlyArray<{ id: string; kind: string; metadata: Record<string, unknown> }>,
29
+ _edges: ReadonlyArray<{ source: string; target: string; relation: string }>,
30
+ sequence: number,
31
+ ): PinnacleEvent[] {
32
+ const events: PinnacleEvent[] = [];
33
+ const now = performance.now();
34
+
35
+ // Find all agent nodes
36
+ const agentNodes = nodes.filter((n) => n.kind === 'agent');
37
+
38
+ for (const agent of agentNodes) {
39
+ const existingTracker = this.agents.get(agent.id);
40
+ const isFirstSeen = !existingTracker;
41
+ const tracker = existingTracker ?? {
42
+ lastEventSequence: 0,
43
+ lastEventTime: now,
44
+ currentTask: null,
45
+ loopPhase: 'idle' as LoopPhase,
46
+ };
47
+
48
+ // Determine what the agent is doing from metadata
49
+ const claimedTask = agent.metadata?.current_task as string | undefined;
50
+ const actionSequence = (agent.metadata?.last_action_sequence as number) ?? 0;
51
+
52
+ let loopPhase: LoopPhase = 'idle';
53
+ let targetId: string | null = null;
54
+
55
+ if (claimedTask && claimedTask !== tracker.currentTask) {
56
+ // Agent just claimed a new task (or first seen with a task) → perceive
57
+ loopPhase = 'perceive';
58
+ targetId = claimedTask;
59
+ } else if (claimedTask && actionSequence > tracker.lastEventSequence) {
60
+ // Agent has a task and just performed an action → act
61
+ loopPhase = 'act';
62
+ targetId = claimedTask;
63
+ } else if (!isFirstSeen && claimedTask && now - tracker.lastEventTime > THINK_GAP_MS) {
64
+ // Gap inference only after baseline established — avoids false 'think' on first poll.
65
+ // If constructed 3s before first poll, every agent would immediately enter think.
66
+ loopPhase = 'think';
67
+ targetId = claimedTask;
68
+ } else if (claimedTask) {
69
+ // Agent has a task, recent action, within gap → still acting
70
+ loopPhase = tracker.loopPhase === 'idle' ? 'perceive' : tracker.loopPhase;
71
+ targetId = claimedTask;
72
+ }
73
+
74
+ // Only emit event if phase changed or target changed
75
+ if (loopPhase !== tracker.loopPhase || targetId !== tracker.currentTask) {
76
+ events.push({
77
+ agentId: agent.id,
78
+ targetId,
79
+ loopPhase,
80
+ sequence,
81
+ });
82
+ }
83
+
84
+ this.agents.set(agent.id, {
85
+ lastEventSequence: actionSequence || tracker.lastEventSequence,
86
+ lastEventTime:
87
+ actionSequence > tracker.lastEventSequence ? now : tracker.lastEventTime,
88
+ currentTask: claimedTask ?? null,
89
+ loopPhase,
90
+ });
91
+ }
92
+
93
+ // Detect departed agents (were tracked, no longer in nodes)
94
+ const currentAgentIds = new Set(agentNodes.map((a) => a.id));
95
+ for (const [agentId, tracker] of this.agents) {
96
+ if (!currentAgentIds.has(agentId) && tracker.loopPhase !== 'idle') {
97
+ events.push({
98
+ agentId,
99
+ targetId: null,
100
+ loopPhase: 'idle',
101
+ sequence,
102
+ });
103
+ this.agents.delete(agentId);
104
+ }
105
+ }
106
+
107
+ return events;
108
+ }
109
+
110
+ /**
111
+ * Convert persistent agent tracker state into NodeIndicatorState map for the canvas.
112
+ *
113
+ * Reads from this.agents (updated by inferFromSnapshot), NOT from emitted events.
114
+ * Events only fire on state changes — reading from the tracker ensures a stable
115
+ * 'think' pulse persists across ticks where nothing changes.
116
+ *
117
+ * Must be called after inferFromSnapshot on the same tick.
118
+ */
119
+ toIndicatorStates(): Map<string, NodeIndicatorState> {
120
+ const states = new Map<string, NodeIndicatorState>();
121
+
122
+ for (const [agentId, tracker] of this.agents) {
123
+ if (tracker.loopPhase !== 'idle') {
124
+ states.set(agentId, {
125
+ loopPhase: tracker.loopPhase,
126
+ phaseElapsedMs: 0,
127
+ traceTargetId: tracker.currentTask,
128
+ });
129
+ }
130
+ }
131
+
132
+ return states;
133
+ }
134
+
135
+ }