@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,289 @@
1
+ <script lang="ts">
2
+ // ForceCanvas — d3-force simulation for a single zone
3
+ // wt-901: Per-zone semantic gravity, wall forces, cell entry velocity
4
+ //
5
+ // Wraps d3-force (not force-graph library) for direct control over forces.
6
+ // Each zone gets an independent simulation. Cells cluster by semantic similarity.
7
+ //
8
+ // S-02: No Math.random — d3-force uses internal PRNG seeded by node index.
9
+ // Integer permille for weights.
10
+ // S-05: Simulation bounded by maxIterations (default 300). alpha decay ensures halt.
11
+
12
+ import { onMount, onDestroy } from 'svelte';
13
+ import type { ForceCell, ForceLink, ForceCanvasConfig, CellPosition } from './force-canvas-types.js';
14
+ import {
15
+ cosineSimilarity,
16
+ semanticSpringLength,
17
+ weightToRadius,
18
+ } from './force-canvas-types.js';
19
+
20
+ // ── Props ────────────────────────────────────────────────────────────────
21
+
22
+ interface Props {
23
+ /** Cells to position in this zone */
24
+ cells: ForceCell[];
25
+ /** Zone width in px */
26
+ width: number;
27
+ /** Zone height in px */
28
+ height: number;
29
+ /** Configuration overrides */
30
+ config?: ForceCanvasConfig;
31
+ /** Whether simulation is active. Default true. Set false for Scelle zones. */
32
+ active?: boolean;
33
+ /** Callback with updated cell positions on each tick */
34
+ onPositionsUpdate?: (positions: CellPosition[]) => void;
35
+ /** Callback when a cell is clicked */
36
+ onCellClick?: (cell: ForceCell) => void;
37
+ /** Monotonic tick counter (passed from parent for deterministic pulse). */
38
+ tick?: number;
39
+ }
40
+
41
+ let {
42
+ cells,
43
+ width,
44
+ height,
45
+ config,
46
+ active = true,
47
+ onPositionsUpdate,
48
+ onCellClick,
49
+ tick = 0,
50
+ }: Props = $props();
51
+
52
+ // ── Config defaults ──────────────────────────────────────────────────────
53
+
54
+ const chargeStrength = $derived(config?.chargeStrength ?? -80);
55
+ const collisionPadding = $derived(config?.collisionPadding ?? 4);
56
+ const wallMargin = $derived(config?.wallMargin ?? 20);
57
+ const wallStrength = $derived(config?.wallStrength ?? 0.3);
58
+ const minSpringLength = $derived(config?.minSpringLength ?? 30);
59
+ const maxSpringLength = $derived(config?.maxSpringLength ?? 200);
60
+ const maxIterations = $derived(config?.maxIterations ?? 300);
61
+ const velocityDecay = $derived(config?.velocityDecay ?? 0.4);
62
+ const entryVelocity = $derived(config?.entryVelocity ?? 0.1);
63
+
64
+ // ── Simulation state ─────────────────────────────────────────────────────
65
+
66
+ let simulation: any = null; // d3-force simulation
67
+ let positions: CellPosition[] = $state([]);
68
+
69
+ // ── Build semantic links from embeddings ─────────────────────────────────
70
+
71
+ function buildSemanticLinks(cells: ForceCell[]): ForceLink[] {
72
+ const links: ForceLink[] = [];
73
+ const SIMILARITY_THRESHOLD = 0.3;
74
+
75
+ for (let i = 0; i < cells.length; i++) {
76
+ for (let j = i + 1; j < cells.length; j++) {
77
+ const sim = cosineSimilarity(cells[i].embedding, cells[j].embedding);
78
+ if (sim > SIMILARITY_THRESHOLD) {
79
+ links.push({
80
+ source: cells[i].id,
81
+ target: cells[j].id,
82
+ distance: 1 - sim,
83
+ });
84
+ }
85
+ }
86
+ }
87
+ return links;
88
+ }
89
+
90
+ // ── Wall force (custom d3 force) ─────────────────────────────────────────
91
+
92
+ function createWallForce(w: number, h: number, margin: number, strength: number) {
93
+ let nodes: ForceCell[] = [];
94
+
95
+ function force(alpha: number) {
96
+ for (const node of nodes) {
97
+ const x = node.x ?? 0;
98
+ const y = node.y ?? 0;
99
+ const r = weightToRadius(node.weight);
100
+
101
+ // Left wall
102
+ if (x - r < margin) {
103
+ node.vx = (node.vx ?? 0) + (margin - x + r) * strength * alpha;
104
+ }
105
+ // Right wall
106
+ if (x + r > w - margin) {
107
+ node.vx = (node.vx ?? 0) - (x + r - w + margin) * strength * alpha;
108
+ }
109
+ // Top wall
110
+ if (y - r < margin) {
111
+ node.vy = (node.vy ?? 0) + (margin - y + r) * strength * alpha;
112
+ }
113
+ // Bottom wall
114
+ if (y + r > h - margin) {
115
+ node.vy = (node.vy ?? 0) - (y + r - h + margin) * strength * alpha;
116
+ }
117
+ }
118
+ }
119
+
120
+ force.initialize = (n: ForceCell[]) => { nodes = n; };
121
+ return force;
122
+ }
123
+
124
+ // ── Initialize / update simulation ───────────────────────────────────────
125
+
126
+ async function initSimulation() {
127
+ if (!active || cells.length === 0) {
128
+ positions = [];
129
+ onPositionsUpdate?.([]);
130
+ return;
131
+ }
132
+
133
+ const d3 = await import('d3-force');
134
+
135
+ const links = buildSemanticLinks(cells);
136
+
137
+ // Place new cells near center with slight offset
138
+ const cx = width / 2;
139
+ const cy = height / 2;
140
+ for (const cell of cells) {
141
+ if (cell.x === undefined) {
142
+ cell.x = cx + (cell.index ?? 0) * 3;
143
+ cell.y = cy + (cell.index ?? 0) * 2;
144
+ // Entry velocity: nudge away from center
145
+ if (entryVelocity > 0) {
146
+ const angle = ((cell.index ?? 0) / Math.max(1, cells.length)) * Math.PI * 2;
147
+ cell.vx = Math.cos(angle) * entryVelocity * 100;
148
+ cell.vy = Math.sin(angle) * entryVelocity * 100;
149
+ }
150
+ }
151
+ }
152
+
153
+ simulation?.stop();
154
+
155
+ simulation = d3
156
+ .forceSimulation(cells as any)
157
+ .alphaDecay(0.02)
158
+ .velocityDecay(velocityDecay)
159
+ .force(
160
+ 'charge',
161
+ d3.forceManyBody().strength(chargeStrength)
162
+ )
163
+ .force(
164
+ 'collision',
165
+ d3.forceCollide<ForceCell>()
166
+ .radius((d) => weightToRadius(d.weight) + collisionPadding)
167
+ )
168
+ .force(
169
+ 'link',
170
+ d3
171
+ .forceLink(links as any)
172
+ .id((d: any) => d.id)
173
+ .distance((link: any) =>
174
+ semanticSpringLength(1 - link.distance, minSpringLength, maxSpringLength)
175
+ )
176
+ .strength(0.5)
177
+ )
178
+ .force('center', d3.forceCenter(cx, cy).strength(0.03))
179
+ .force('wall', createWallForce(width, height, wallMargin, wallStrength))
180
+ .on('tick', () => {
181
+ positions = cells.map((cell) => ({
182
+ cell,
183
+ x: cell.x ?? 0,
184
+ y: cell.y ?? 0,
185
+ radius: weightToRadius(cell.weight),
186
+ }));
187
+ onPositionsUpdate?.(positions);
188
+ });
189
+
190
+ // S-05: bound total iterations
191
+ simulation.stop();
192
+ for (let i = 0; i < maxIterations && simulation.alpha() > simulation.alphaMin(); i++) {
193
+ simulation.tick();
194
+ }
195
+
196
+ // After initial settle, switch to live ticking
197
+ simulation.restart();
198
+ }
199
+
200
+ // ── Lifecycle ────────────────────────────────────────────────────────────
201
+
202
+ onMount(() => {
203
+ initSimulation();
204
+ });
205
+
206
+ onDestroy(() => {
207
+ simulation?.stop();
208
+ simulation = null;
209
+ });
210
+
211
+ // ── React to cell changes ────────────────────────────────────────────────
212
+
213
+ let prevCellIds = '';
214
+ $effect(() => {
215
+ const ids = cells.map((c) => c.id).sort().join(',');
216
+ if (ids !== prevCellIds) {
217
+ prevCellIds = ids;
218
+ initSimulation();
219
+ }
220
+ });
221
+
222
+ // React to zone resize
223
+ $effect(() => {
224
+ if (simulation && (width || height)) {
225
+ simulation.force('center')?.x(width / 2).y(height / 2);
226
+ simulation.force('wall', createWallForce(width, height, wallMargin, wallStrength));
227
+ simulation.alpha(0.3).restart();
228
+ }
229
+ });
230
+
231
+ // ── Pin/unpin API ────────────────────────────────────────────────────────
232
+
233
+ export function pinCell(cellId: string, x: number, y: number) {
234
+ const cell = cells.find((c) => c.id === cellId);
235
+ if (cell) {
236
+ cell.fx = x;
237
+ cell.fy = y;
238
+ simulation?.alpha(0.1).restart();
239
+ }
240
+ }
241
+
242
+ export function unpinCell(cellId: string) {
243
+ const cell = cells.find((c) => c.id === cellId);
244
+ if (cell) {
245
+ cell.fx = null;
246
+ cell.fy = null;
247
+ simulation?.alpha(0.3).restart();
248
+ }
249
+ }
250
+
251
+ export function reheat(alpha = 0.5) {
252
+ simulation?.alpha(alpha).restart();
253
+ }
254
+ </script>
255
+
256
+ <!-- ForceCanvas is headless — it computes positions, doesn't render DOM.
257
+ The parent reads positions via onPositionsUpdate and renders cells at those coords.
258
+ This is a div solely for lifecycle binding. -->
259
+ <div class="force-canvas" style:display="contents">
260
+ {#each positions as pos (pos.cell.id)}
261
+ <slot name="cell" {pos}>
262
+ <!-- Default: colored circle at computed position -->
263
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
264
+ <div
265
+ class="force-cell-default"
266
+ style:left="{pos.x - pos.radius}px"
267
+ style:top="{pos.y - pos.radius}px"
268
+ style:width="{pos.radius * 2}px"
269
+ style:height="{pos.radius * 2}px"
270
+ onclick={() => onCellClick?.(pos.cell)}
271
+ ></div>
272
+ </slot>
273
+ {/each}
274
+ </div>
275
+
276
+ <style>
277
+ .force-cell-default {
278
+ position: absolute;
279
+ border-radius: 50%;
280
+ background: var(--color-accent, #b87333);
281
+ opacity: 0.7;
282
+ cursor: pointer;
283
+ transition: opacity 0.1s;
284
+ }
285
+
286
+ .force-cell-default:hover {
287
+ opacity: 1;
288
+ }
289
+ </style>
@@ -0,0 +1,334 @@
1
+ <script lang="ts">
2
+ // LodRenderer — 4-level LOD rendering with cross-fade transitions
3
+ // wt-902: The signature visual system — recursive zoom experience
4
+ //
5
+ // Renders each cell at the appropriate LOD level based on current zoom.
6
+ // Manages cross-fade transitions between levels (300ms default).
7
+ // Parent provides render callbacks per level; LodRenderer manages which shows.
8
+ //
9
+ // L0 (>0.8x): Full cell — editable, titlebar, resize handles
10
+ // L1 (0.4x-0.8x): Compact — title + type icon + first-line preview
11
+ // L2 (0.15x-0.4x): Chip — colored circle, 2-letter abbrev
12
+ // L3 (<0.15x): Pixel field — cells dissolve into color mosaic
13
+ //
14
+ // S-02: Transition timing from monotonic tick counter, not wall-clock.
15
+ // S-05: Bounded cell count. No unbounded iteration.
16
+
17
+ import type { Snippet } from 'svelte';
18
+ import type { CellPosition } from './force-canvas-types.js';
19
+ import type { LodLevel, LodRendererConfig, CellLodState } from './lod-types.js';
20
+ import { zoomToLod, DEFAULT_LOD_THRESHOLDS, chipAbbrev } from './lod-types.js';
21
+
22
+ // ── Props ────────────────────────────────────────────────────────────────
23
+
24
+ interface Props {
25
+ /** Cell positions from ForceCanvas */
26
+ positions: CellPosition[];
27
+ /** Current camera zoom factor (0–∞, 1.0 = default) */
28
+ zoom: number;
29
+ /** Container width in px */
30
+ width: number;
31
+ /** Container height in px */
32
+ height: number;
33
+ /** Camera pan offset X */
34
+ panX?: number;
35
+ /** Camera pan offset Y */
36
+ panY?: number;
37
+ /** Config overrides */
38
+ config?: LodRendererConfig;
39
+
40
+ // ── Level render callbacks (snippets) ──────────────────────────────
41
+
42
+ /** L0: Full cell editor view */
43
+ renderL0?: Snippet<[CellPosition, LodLevel]>;
44
+ /** L1: Compact card view */
45
+ renderL1?: Snippet<[CellPosition, LodLevel]>;
46
+ /** L2: Dot/chip view */
47
+ renderL2?: Snippet<[CellPosition, LodLevel]>;
48
+ /** L3 renders as a full-zone pixel field — no per-cell callback.
49
+ * Instead, the parent provides an L3 overlay snippet. */
50
+ renderL3?: Snippet<[CellPosition[], number]>; // positions[], zoom
51
+
52
+ /** Click handler — forwarded from individual cells */
53
+ onCellClick?: (cellId: string, level: LodLevel) => void;
54
+ }
55
+
56
+ let {
57
+ positions,
58
+ zoom,
59
+ width,
60
+ height,
61
+ panX = 0,
62
+ panY = 0,
63
+ config,
64
+ renderL0,
65
+ renderL1,
66
+ renderL2,
67
+ renderL3,
68
+ onCellClick,
69
+ }: Props = $props();
70
+
71
+ // ── Config ───────────────────────────────────────────────────────────────
72
+
73
+ const thresholds = $derived(config?.thresholds ?? DEFAULT_LOD_THRESHOLDS);
74
+ const crossFadeDuration = $derived(config?.crossFadeDuration ?? 300);
75
+
76
+ // ── Current LOD level ────────────────────────────────────────────────────
77
+
78
+ const currentLod = $derived(zoomToLod(zoom, thresholds));
79
+
80
+ // ── Per-cell LOD state for cross-fade ────────────────────────────────────
81
+
82
+ // Map<cellId, CellLodState> — tracks transition state per cell
83
+ let lodStates: Map<string, CellLodState> = $state(new Map());
84
+ let animFrameId: number | null = null;
85
+
86
+ // Update LOD states when level changes
87
+ $effect(() => {
88
+ const level = currentLod;
89
+ const now = performance.now();
90
+ let needsAnimation = false;
91
+
92
+ for (const pos of positions) {
93
+ const existing = lodStates.get(pos.cell.id);
94
+
95
+ if (!existing) {
96
+ // New cell — no transition
97
+ lodStates.set(pos.cell.id, {
98
+ cellId: pos.cell.id,
99
+ currentLevel: level,
100
+ previousLevel: null,
101
+ transitionProgress: null,
102
+ transitionStart: null,
103
+ });
104
+ } else if (existing.currentLevel !== level) {
105
+ // Level changed — start cross-fade
106
+ lodStates.set(pos.cell.id, {
107
+ cellId: pos.cell.id,
108
+ currentLevel: level,
109
+ previousLevel: existing.currentLevel,
110
+ transitionProgress: 0,
111
+ transitionStart: now,
112
+ });
113
+ needsAnimation = true;
114
+ }
115
+ }
116
+
117
+ // Clean up cells that no longer exist
118
+ const activeIds = new Set(positions.map((p) => p.cell.id));
119
+ for (const id of lodStates.keys()) {
120
+ if (!activeIds.has(id)) lodStates.delete(id);
121
+ }
122
+
123
+ if (needsAnimation) startAnimation();
124
+ });
125
+
126
+ function startAnimation() {
127
+ if (animFrameId !== null) return;
128
+ animFrameId = requestAnimationFrame(animateTransitions);
129
+ }
130
+
131
+ function animateTransitions() {
132
+ const now = performance.now();
133
+ let anyActive = false;
134
+
135
+ for (const [id, state] of lodStates) {
136
+ if (state.transitionProgress === null || state.transitionStart === null) continue;
137
+
138
+ const elapsed = now - state.transitionStart;
139
+ const progress = Math.min(1, elapsed / crossFadeDuration);
140
+
141
+ lodStates.set(id, { ...state, transitionProgress: progress });
142
+
143
+ if (progress < 1) {
144
+ anyActive = true;
145
+ } else {
146
+ // Transition complete — clear previous level
147
+ lodStates.set(id, {
148
+ ...state,
149
+ previousLevel: null,
150
+ transitionProgress: null,
151
+ transitionStart: null,
152
+ });
153
+ }
154
+ }
155
+
156
+ if (anyActive) {
157
+ animFrameId = requestAnimationFrame(animateTransitions);
158
+ } else {
159
+ animFrameId = null;
160
+ }
161
+ }
162
+
163
+ // ── Helpers ──────────────────────────────────────────────────────────────
164
+
165
+ function getOpacity(cellId: string, forLevel: 'current' | 'previous'): number {
166
+ const state = lodStates.get(cellId);
167
+ if (!state || state.transitionProgress === null) {
168
+ return forLevel === 'current' ? 1 : 0;
169
+ }
170
+ // ease-out cubic
171
+ const t = state.transitionProgress;
172
+ const eased = 1 - Math.pow(1 - t, 3);
173
+ return forLevel === 'current' ? eased : 1 - eased;
174
+ }
175
+
176
+ function getLodState(cellId: string): CellLodState | undefined {
177
+ return lodStates.get(cellId);
178
+ }
179
+
180
+ // ── Cleanup ──────────────────────────────────────────────────────────────
181
+
182
+ import { onDestroy } from 'svelte';
183
+
184
+ onDestroy(() => {
185
+ if (animFrameId !== null) cancelAnimationFrame(animFrameId);
186
+ });
187
+ </script>
188
+
189
+ <div
190
+ class="lod-renderer"
191
+ style:width="{width}px"
192
+ style:height="{height}px"
193
+ >
194
+ <div
195
+ class="lod-viewport"
196
+ style:transform="translate({panX}px, {panY}px) scale({zoom})"
197
+ style:transform-origin="0 0"
198
+ >
199
+ {#if currentLod === 3 && renderL3}
200
+ <!-- L3: pixel field — render full-zone overlay -->
201
+ <div class="lod-l3-field" style:width="{width / zoom}px" style:height="{height / zoom}px">
202
+ {@render renderL3(positions, zoom)}
203
+ </div>
204
+ {:else}
205
+ <!-- L0/L1/L2: per-cell rendering -->
206
+ {#each positions as pos (pos.cell.id)}
207
+ {@const state = getLodState(pos.cell.id)}
208
+ {@const isTransitioning = state?.transitionProgress !== null && state?.transitionProgress !== undefined}
209
+
210
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
211
+ <div
212
+ class="lod-cell"
213
+ style:left="{pos.x - pos.radius}px"
214
+ style:top="{pos.y - pos.radius}px"
215
+ onclick={() => onCellClick?.(pos.cell.id, currentLod)}
216
+ data-lod={currentLod}
217
+ data-cell-id={pos.cell.id}
218
+ >
219
+ <!-- Current level -->
220
+ <div
221
+ class="lod-layer lod-current"
222
+ style:opacity={getOpacity(pos.cell.id, 'current')}
223
+ >
224
+ {#if currentLod === 0 && renderL0}
225
+ {@render renderL0(pos, currentLod)}
226
+ {:else if currentLod === 1 && renderL1}
227
+ {@render renderL1(pos, currentLod)}
228
+ {:else if currentLod === 2 && renderL2}
229
+ {@render renderL2(pos, currentLod)}
230
+ {:else}
231
+ <!-- Fallback: chip -->
232
+ <div
233
+ class="lod-chip"
234
+ style:width="{pos.radius * 2}px"
235
+ style:height="{pos.radius * 2}px"
236
+ >
237
+ <span class="lod-chip-label">{chipAbbrev(pos.cell.label)}</span>
238
+ </div>
239
+ {/if}
240
+ </div>
241
+
242
+ <!-- Previous level (cross-fade out) -->
243
+ {#if isTransitioning && state?.previousLevel !== null}
244
+ <div
245
+ class="lod-layer lod-previous"
246
+ style:opacity={getOpacity(pos.cell.id, 'previous')}
247
+ >
248
+ {#if state.previousLevel === 0 && renderL0}
249
+ {@render renderL0(pos, state.previousLevel)}
250
+ {:else if state.previousLevel === 1 && renderL1}
251
+ {@render renderL1(pos, state.previousLevel)}
252
+ {:else if state.previousLevel === 2 && renderL2}
253
+ {@render renderL2(pos, state.previousLevel)}
254
+ {:else}
255
+ <div
256
+ class="lod-chip"
257
+ style:width="{pos.radius * 2}px"
258
+ style:height="{pos.radius * 2}px"
259
+ >
260
+ <span class="lod-chip-label">{chipAbbrev(pos.cell.label)}</span>
261
+ </div>
262
+ {/if}
263
+ </div>
264
+ {/if}
265
+ </div>
266
+ {/each}
267
+ {/if}
268
+ </div>
269
+ </div>
270
+
271
+ <style>
272
+ .lod-renderer {
273
+ position: relative;
274
+ overflow: hidden;
275
+ }
276
+
277
+ .lod-viewport {
278
+ position: absolute;
279
+ top: 0;
280
+ left: 0;
281
+ will-change: transform;
282
+ }
283
+
284
+ .lod-cell {
285
+ position: absolute;
286
+ cursor: pointer;
287
+ }
288
+
289
+ .lod-layer {
290
+ position: absolute;
291
+ top: 0;
292
+ left: 0;
293
+ width: 100%;
294
+ height: 100%;
295
+ will-change: opacity;
296
+ }
297
+
298
+ .lod-previous {
299
+ pointer-events: none;
300
+ }
301
+
302
+ /* ── L2 chip fallback ──────────────────────────────────────────── */
303
+
304
+ .lod-chip {
305
+ border-radius: 50%;
306
+ background: var(--color-accent, #b87333);
307
+ display: flex;
308
+ align-items: center;
309
+ justify-content: center;
310
+ opacity: 0.85;
311
+ transition: opacity 0.1s;
312
+ }
313
+
314
+ .lod-chip:hover {
315
+ opacity: 1;
316
+ }
317
+
318
+ .lod-chip-label {
319
+ font-family: var(--font-mono, monospace);
320
+ font-size: 9px;
321
+ font-weight: 600;
322
+ color: var(--color-text-primary, #e8e0d0);
323
+ letter-spacing: 0.5px;
324
+ user-select: none;
325
+ }
326
+
327
+ /* ── L3 field ──────────────────────────────────────────────────── */
328
+
329
+ .lod-l3-field {
330
+ position: absolute;
331
+ top: 0;
332
+ left: 0;
333
+ }
334
+ </style>