@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,185 @@
1
+ <script lang="ts">
2
+ // SnapZoomCamera — viewport camera with snap-zoom transitions
3
+ // wt-903: Camera state + wheel zoom + pinch zoom + snap-zoom to target
4
+ //
5
+ // Provides camera state (zoom, panX, panY) to child content.
6
+ // Handles mouse wheel zoom, trackpad pinch, and programmatic snap-zoom.
7
+ //
8
+ // S-02: Easing from rAF tick, not wall-clock.
9
+ // S-05: Animation bounded (maxDuration).
10
+
11
+ import type { Snippet } from 'svelte';
12
+ import { onDestroy } from 'svelte';
13
+ import type { CameraState, SnapZoomTarget, CameraConfig } from './camera-types.js';
14
+ import { DEFAULT_CAMERA_CONFIG } from './camera-types.js';
15
+
16
+ // ── Props ────────────────────────────────────────────────────────────────
17
+
18
+ interface Props {
19
+ /** Container width */
20
+ width: number;
21
+ /** Container height */
22
+ height: number;
23
+ /** Camera configuration */
24
+ config?: CameraConfig;
25
+ /** Content to render inside the camera viewport */
26
+ children: Snippet<[CameraState]>;
27
+ /** Callback when camera state changes */
28
+ onCameraChange?: (state: CameraState) => void;
29
+ }
30
+
31
+ let {
32
+ width,
33
+ height,
34
+ config,
35
+ children,
36
+ onCameraChange,
37
+ }: Props = $props();
38
+
39
+ // ── Config ───────────────────────────────────────────────────────────────
40
+
41
+ const minZoom = $derived(config?.minZoom ?? DEFAULT_CAMERA_CONFIG.minZoom);
42
+ const maxZoom = $derived(config?.maxZoom ?? DEFAULT_CAMERA_CONFIG.maxZoom);
43
+ const zoomSpeed = $derived(config?.zoomSpeed ?? DEFAULT_CAMERA_CONFIG.zoomSpeed);
44
+ const snapDuration = $derived(config?.snapDuration ?? DEFAULT_CAMERA_CONFIG.snapDuration);
45
+
46
+ // ── Camera state ─────────────────────────────────────────────────────────
47
+
48
+ let zoom = $state(1.0);
49
+ let panX = $state(0);
50
+ let panY = $state(0);
51
+
52
+ const cameraState: CameraState = $derived({ zoom, panX, panY });
53
+
54
+ $effect(() => {
55
+ onCameraChange?.(cameraState);
56
+ });
57
+
58
+ // ── Wheel zoom ───────────────────────────────────────────────────────────
59
+
60
+ function handleWheel(e: WheelEvent) {
61
+ e.preventDefault();
62
+
63
+ // Determine zoom center (pointer position relative to container)
64
+ const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
65
+ const mx = e.clientX - rect.left;
66
+ const my = e.clientY - rect.top;
67
+
68
+ // Content point under pointer before zoom
69
+ const cx = (mx - panX) / zoom;
70
+ const cy = (my - panY) / zoom;
71
+
72
+ // Compute new zoom
73
+ const delta = -e.deltaY * zoomSpeed;
74
+ const newZoom = Math.max(minZoom, Math.min(maxZoom, zoom * (1 + delta)));
75
+
76
+ // Adjust pan so content point stays under pointer
77
+ panX = mx - cx * newZoom;
78
+ panY = my - cy * newZoom;
79
+ zoom = newZoom;
80
+ }
81
+
82
+ // ── Pan (middle mouse or shift+drag) ─────────────────────────────────────
83
+
84
+ let isPanning = $state(false);
85
+ let panStart = { x: 0, y: 0, panX: 0, panY: 0 };
86
+
87
+ function handlePointerDown(e: PointerEvent) {
88
+ // Middle mouse button or shift+left click
89
+ if (e.button === 1 || (e.button === 0 && e.shiftKey)) {
90
+ e.preventDefault();
91
+ isPanning = true;
92
+ panStart = { x: e.clientX, y: e.clientY, panX, panY };
93
+ (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
94
+ }
95
+ }
96
+
97
+ function handlePointerMove(e: PointerEvent) {
98
+ if (!isPanning) return;
99
+ panX = panStart.panX + (e.clientX - panStart.x);
100
+ panY = panStart.panY + (e.clientY - panStart.y);
101
+ }
102
+
103
+ function handlePointerUp() {
104
+ isPanning = false;
105
+ }
106
+
107
+ // ── Snap-zoom API ────────────────────────────────────────────────────────
108
+
109
+ let animFrameId: number | null = null;
110
+
111
+ export function snapTo(target: SnapZoomTarget) {
112
+ if (animFrameId !== null) cancelAnimationFrame(animFrameId);
113
+
114
+ const duration = target.duration ?? snapDuration;
115
+ const startZoom = zoom;
116
+ const startPanX = panX;
117
+ const startPanY = panY;
118
+
119
+ // Target: center the point in the viewport at the target zoom
120
+ const targetPanX = width / 2 - target.x * target.zoom;
121
+ const targetPanY = height / 2 - target.y * target.zoom;
122
+
123
+ const startTime = performance.now();
124
+
125
+ function animate() {
126
+ const elapsed = performance.now() - startTime;
127
+ const t = Math.min(1, elapsed / duration);
128
+ // ease-out cubic
129
+ const eased = 1 - Math.pow(1 - t, 3);
130
+
131
+ zoom = startZoom + (target.zoom - startZoom) * eased;
132
+ panX = startPanX + (targetPanX - startPanX) * eased;
133
+ panY = startPanY + (targetPanY - startPanY) * eased;
134
+
135
+ if (t < 1) {
136
+ animFrameId = requestAnimationFrame(animate);
137
+ } else {
138
+ animFrameId = null;
139
+ }
140
+ }
141
+
142
+ animFrameId = requestAnimationFrame(animate);
143
+ }
144
+
145
+ export function resetCamera() {
146
+ snapTo({ x: 0, y: 0, zoom: 1.0 });
147
+ }
148
+
149
+ export function getState(): CameraState {
150
+ return { zoom, panX, panY };
151
+ }
152
+
153
+ // ── Cleanup ──────────────────────────────────────────────────────────────
154
+
155
+ onDestroy(() => {
156
+ if (animFrameId !== null) cancelAnimationFrame(animFrameId);
157
+ });
158
+ </script>
159
+
160
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
161
+ <div
162
+ class="snap-zoom-camera"
163
+ style:width="{width}px"
164
+ style:height="{height}px"
165
+ onwheel={handleWheel}
166
+ onpointerdown={handlePointerDown}
167
+ onpointermove={handlePointerMove}
168
+ onpointerup={handlePointerUp}
169
+ onpointercancel={handlePointerUp}
170
+ >
171
+ {@render children(cameraState)}
172
+ </div>
173
+
174
+ <style>
175
+ .snap-zoom-camera {
176
+ position: relative;
177
+ overflow: hidden;
178
+ cursor: grab;
179
+ touch-action: none;
180
+ }
181
+
182
+ .snap-zoom-camera:active {
183
+ cursor: grabbing;
184
+ }
185
+ </style>
@@ -0,0 +1,311 @@
1
+ <script lang="ts">
2
+ // ZoneTiler — BSP binary space partition zone layout
3
+ // wt-900: Foundational spatial primitive for Loge canvas
4
+ //
5
+ // Renders a recursive BSP tree of zones with draggable dividers.
6
+ // Each zone is a rectangular region that hosts its own content (ForceCanvas, etc.)
7
+ // Keyboard navigation: h/j/k/l to move focus between zones.
8
+ //
9
+ // S-02: No HashMap. Zone lookup via flattenBsp (sorted array).
10
+ // S-05: BSP depth bounded (max 8 levels). No unbounded recursion.
11
+
12
+ import type { Snippet } from 'svelte';
13
+ import type {
14
+ BspNode,
15
+ ZoneRect,
16
+ DividerRect,
17
+ ZoneTilerConfig,
18
+ ZoneLeaf,
19
+ SplitDirection,
20
+ } from './zone-tiler-types.js';
21
+ import { flattenBsp } from './zone-tiler-types.js';
22
+
23
+ // ── Props ────────────────────────────────────────────────────────────────
24
+
25
+ interface Props {
26
+ /** BSP tree defining the zone layout */
27
+ tree: BspNode;
28
+ /** Container width in px */
29
+ width: number;
30
+ /** Container height in px */
31
+ height: number;
32
+ /** Render callback for each zone — receives zone rect */
33
+ zoneContent: Snippet<[ZoneRect]>;
34
+ /** Configuration overrides */
35
+ config?: ZoneTilerConfig;
36
+ /** Currently focused zone id */
37
+ focusedZoneId?: string;
38
+ /** Fired when a divider is dragged and ratio changes */
39
+ onResize?: (tree: BspNode) => void;
40
+ /** Fired when focused zone changes (keyboard nav or click) */
41
+ onFocusChange?: (zoneId: string) => void;
42
+ /** Fired when user requests a zone split (Ctrl+Shift+H/V) */
43
+ onSplitRequest?: (zoneId: string, direction: SplitDirection) => void;
44
+ }
45
+
46
+ let {
47
+ tree,
48
+ width,
49
+ height,
50
+ zoneContent,
51
+ config,
52
+ focusedZoneId = $bindable(''),
53
+ onResize,
54
+ onFocusChange,
55
+ onSplitRequest,
56
+ }: Props = $props();
57
+
58
+ // ── Config defaults ──────────────────────────────────────────────────────
59
+
60
+ const minZoneSize = $derived(config?.minZoneSize ?? 120);
61
+ const dividerThickness = $derived(config?.dividerThickness ?? 6);
62
+ const dividerHitExpand = $derived(config?.dividerHitExpand ?? 4);
63
+
64
+ // ── Computed layout ──────────────────────────────────────────────────────
65
+
66
+ const layout = $derived(flattenBsp(tree, 0, 0, width, height, dividerThickness));
67
+ const zones = $derived(layout.zones);
68
+ const dividers = $derived(layout.dividers);
69
+
70
+ // ── Divider drag state ───────────────────────────────────────────────────
71
+
72
+ let activeDivider: DividerRect | null = $state(null);
73
+ let dragStartPos = $state(0);
74
+ let dragStartRatio = $state(0);
75
+
76
+ function handleDividerDown(e: PointerEvent, divider: DividerRect) {
77
+ e.preventDefault();
78
+ e.stopPropagation();
79
+ activeDivider = divider;
80
+ dragStartPos = divider.direction === 'horizontal' ? e.clientX : e.clientY;
81
+ dragStartRatio = divider.splitRef.ratio;
82
+ (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
83
+ }
84
+
85
+ function handleDividerMove(e: PointerEvent) {
86
+ if (!activeDivider) return;
87
+
88
+ const { direction, splitRef } = activeDivider;
89
+ const currentPos = direction === 'horizontal' ? e.clientX : e.clientY;
90
+ const delta = currentPos - dragStartPos;
91
+ const parentSize = direction === 'horizontal' ? width : height;
92
+ const availableSize = parentSize - dividerThickness;
93
+
94
+ if (availableSize <= 0) return;
95
+
96
+ const ratioShift = delta / availableSize;
97
+ let newRatio = dragStartRatio + ratioShift;
98
+
99
+ // Clamp: ensure both children meet minimum size
100
+ const minRatio = minZoneSize / availableSize;
101
+ const maxRatio = 1 - minRatio;
102
+ newRatio = Math.max(minRatio, Math.min(maxRatio, newRatio));
103
+
104
+ splitRef.ratio = newRatio;
105
+ onResize?.(tree);
106
+ }
107
+
108
+ function handleDividerUp() {
109
+ activeDivider = null;
110
+ }
111
+
112
+ // ── Zone focus + keyboard navigation ─────────────────────────────────────
113
+
114
+ function handleZoneClick(zoneId: string) {
115
+ focusedZoneId = zoneId;
116
+ onFocusChange?.(zoneId);
117
+ }
118
+
119
+ function handleKeydown(e: KeyboardEvent) {
120
+ if (!focusedZoneId || zones.length < 2) return;
121
+
122
+ const focused = zones.find((z) => z.id === focusedZoneId);
123
+ if (!focused) return;
124
+
125
+ // hjkl navigation — find nearest zone in direction
126
+ let dx = 0;
127
+ let dy = 0;
128
+ switch (e.key) {
129
+ case 'h': dx = -1; break;
130
+ case 'l': dx = 1; break;
131
+ case 'k': dy = -1; break;
132
+ case 'j': dy = 1; break;
133
+ default: break;
134
+ }
135
+
136
+ if (dx !== 0 || dy !== 0) {
137
+ e.preventDefault();
138
+ const target = findNearestZone(focused, dx, dy);
139
+ if (target) {
140
+ focusedZoneId = target.id;
141
+ onFocusChange?.(target.id);
142
+ }
143
+ return;
144
+ }
145
+
146
+ // Ctrl+Shift+H/V: split request
147
+ if (e.ctrlKey && e.shiftKey) {
148
+ if (e.key === 'H' || e.key === 'h') {
149
+ e.preventDefault();
150
+ onSplitRequest?.(focusedZoneId, 'horizontal');
151
+ } else if (e.key === 'V' || e.key === 'v') {
152
+ e.preventDefault();
153
+ onSplitRequest?.(focusedZoneId, 'vertical');
154
+ }
155
+ }
156
+ }
157
+
158
+ function findNearestZone(from: ZoneRect, dx: number, dy: number): ZoneRect | null {
159
+ const cx = from.x + from.width / 2;
160
+ const cy = from.y + from.height / 2;
161
+
162
+ let best: ZoneRect | null = null;
163
+ let bestDist = Infinity;
164
+
165
+ for (const zone of zones) {
166
+ if (zone.id === from.id) continue;
167
+
168
+ const zx = zone.x + zone.width / 2;
169
+ const zy = zone.y + zone.height / 2;
170
+ const relX = zx - cx;
171
+ const relY = zy - cy;
172
+
173
+ // Filter: must be in the requested direction
174
+ if (dx < 0 && relX >= 0) continue;
175
+ if (dx > 0 && relX <= 0) continue;
176
+ if (dy < 0 && relY >= 0) continue;
177
+ if (dy > 0 && relY <= 0) continue;
178
+
179
+ const dist = Math.abs(relX) + Math.abs(relY);
180
+ if (dist < bestDist) {
181
+ bestDist = dist;
182
+ best = zone;
183
+ }
184
+ }
185
+
186
+ return best;
187
+ }
188
+
189
+ // ── Focus on first zone if none set ──────────────────────────────────────
190
+
191
+ $effect(() => {
192
+ if (!focusedZoneId && zones.length > 0) {
193
+ focusedZoneId = zones[0].id;
194
+ }
195
+ });
196
+ </script>
197
+
198
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
199
+ <div
200
+ class="zone-tiler"
201
+ style:width="{width}px"
202
+ style:height="{height}px"
203
+ onkeydown={handleKeydown}
204
+ tabindex="0"
205
+ role="application"
206
+ aria-label="Zone tiler — use h/j/k/l to navigate zones"
207
+ >
208
+ <!-- Zone panes -->
209
+ {#each zones as zone (zone.id)}
210
+ <button
211
+ type="button"
212
+ class="zone-pane"
213
+ class:zone-focused={zone.id === focusedZoneId}
214
+ class:zone-scelle={zone.zone.privacyState === 'scelle'}
215
+ style:left="{zone.x}px"
216
+ style:top="{zone.y}px"
217
+ style:width="{zone.width}px"
218
+ style:height="{zone.height}px"
219
+ onclick={() => handleZoneClick(zone.id)}
220
+ data-zone-id={zone.id}
221
+ >
222
+ {@render zoneContent(zone)}
223
+ </button>
224
+ {/each}
225
+
226
+ <!-- Dividers (rendered on top of zones) -->
227
+ {#each dividers as divider (divider.path)}
228
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
229
+ <div
230
+ class="zone-divider"
231
+ class:zone-divider-h={divider.direction === 'horizontal'}
232
+ class:zone-divider-v={divider.direction === 'vertical'}
233
+ class:zone-divider-active={activeDivider?.path === divider.path}
234
+ style:left="{divider.x - dividerHitExpand}px"
235
+ style:top="{divider.y - dividerHitExpand}px"
236
+ style:width="{divider.width + (divider.direction === 'horizontal' ? dividerHitExpand * 2 : 0)}px"
237
+ style:height="{divider.height + (divider.direction === 'vertical' ? dividerHitExpand * 2 : 0)}px"
238
+ onpointerdown={(e) => handleDividerDown(e, divider)}
239
+ onpointermove={handleDividerMove}
240
+ onpointerup={handleDividerUp}
241
+ onpointercancel={handleDividerUp}
242
+ >
243
+ <div class="zone-divider-visual"
244
+ style:left="{divider.direction === 'horizontal' ? dividerHitExpand + 'px' : '0'}"
245
+ style:top="{divider.direction === 'vertical' ? dividerHitExpand + 'px' : '0'}"
246
+ style:width="{divider.direction === 'horizontal' ? dividerThickness + 'px' : '100%'}"
247
+ style:height="{divider.direction === 'vertical' ? dividerThickness + 'px' : '100%'}"
248
+ ></div>
249
+ </div>
250
+ {/each}
251
+ </div>
252
+
253
+ <style>
254
+ .zone-tiler {
255
+ position: relative;
256
+ overflow: hidden;
257
+ outline: none;
258
+ background: var(--color-bg-app, #1a1a1a);
259
+ }
260
+
261
+ .zone-pane {
262
+ position: absolute;
263
+ overflow: hidden;
264
+ border-radius: var(--radius-xs, 2px);
265
+ transition: box-shadow 0.15s ease;
266
+ border: none;
267
+ background: transparent;
268
+ padding: 0;
269
+ cursor: pointer;
270
+ text-align: left;
271
+ font: inherit;
272
+ color: inherit;
273
+ }
274
+
275
+ .zone-focused {
276
+ box-shadow: inset 0 0 0 1.5px var(--color-accent, #b87333);
277
+ }
278
+
279
+ .zone-scelle {
280
+ /* Scelle zone base treatment — components inside add hazard symbology */
281
+ background: #1c1a1a;
282
+ }
283
+
284
+ /* ── Dividers ─────────────────────────────────────────────────────── */
285
+
286
+ .zone-divider {
287
+ position: absolute;
288
+ z-index: 10;
289
+ touch-action: none;
290
+ }
291
+
292
+ .zone-divider-h {
293
+ cursor: col-resize;
294
+ }
295
+
296
+ .zone-divider-v {
297
+ cursor: row-resize;
298
+ }
299
+
300
+ .zone-divider-visual {
301
+ position: absolute;
302
+ background: var(--color-border, #333);
303
+ border-radius: var(--radius-xs, 1px);
304
+ transition: background 0.12s ease;
305
+ }
306
+
307
+ .zone-divider:hover .zone-divider-visual,
308
+ .zone-divider-active .zone-divider-visual {
309
+ background: var(--color-accent, #b87333);
310
+ }
311
+ </style>
@@ -0,0 +1,45 @@
1
+ // SnapZoomCamera — viewport camera types
2
+ // wt-903 implementation is Sonnet; types defined here for interface clarity.
3
+ // S-02: No wall-clock for easing — use requestAnimationFrame tick count.
4
+
5
+ /** Camera state */
6
+ export interface CameraState {
7
+ /** Current zoom factor (1.0 = default, higher = zoomed in) */
8
+ zoom: number;
9
+ /** Pan offset X in px */
10
+ panX: number;
11
+ /** Pan offset Y in px */
12
+ panY: number;
13
+ }
14
+
15
+ /** Snap-zoom target: center on a point at a specific zoom level */
16
+ export interface SnapZoomTarget {
17
+ /** Target center X (in content coords, before zoom) */
18
+ x: number;
19
+ /** Target center Y (in content coords, before zoom) */
20
+ y: number;
21
+ /** Target zoom level */
22
+ zoom: number;
23
+ /** Easing duration in ms. Default 400. */
24
+ duration?: number;
25
+ }
26
+
27
+ /** Camera configuration */
28
+ export interface CameraConfig {
29
+ /** Minimum zoom. Default 0.05. */
30
+ minZoom?: number;
31
+ /** Maximum zoom. Default 4.0. */
32
+ maxZoom?: number;
33
+ /** Zoom speed (scroll wheel multiplier). Default 0.001. */
34
+ zoomSpeed?: number;
35
+ /** Snap-zoom easing duration in ms. Default 400. */
36
+ snapDuration?: number;
37
+ }
38
+
39
+ /** Default camera config */
40
+ export const DEFAULT_CAMERA_CONFIG: Required<CameraConfig> = {
41
+ minZoom: 0.05,
42
+ maxZoom: 4.0,
43
+ zoomSpeed: 0.001,
44
+ snapDuration: 400,
45
+ };