@delightstack/components 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 (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +136 -0
  3. package/SKILL.md +149 -0
  4. package/bin/agents.js +63 -0
  5. package/dist/actions/Alert.svelte +202 -0
  6. package/dist/actions/Alert.svelte.d.ts +36 -0
  7. package/dist/actions/Alert.svelte.d.ts.map +1 -0
  8. package/dist/actions/Button.svelte +1450 -0
  9. package/dist/actions/Button.svelte.d.ts +56 -0
  10. package/dist/actions/Button.svelte.d.ts.map +1 -0
  11. package/dist/actions/ButtonGroup.svelte +111 -0
  12. package/dist/actions/ButtonGroup.svelte.d.ts +41 -0
  13. package/dist/actions/ButtonGroup.svelte.d.ts.map +1 -0
  14. package/dist/actions/CommandPalette.svelte +939 -0
  15. package/dist/actions/CommandPalette.svelte.d.ts +37 -0
  16. package/dist/actions/CommandPalette.svelte.d.ts.map +1 -0
  17. package/dist/actions/ContextMenu.svelte +138 -0
  18. package/dist/actions/ContextMenu.svelte.d.ts +54 -0
  19. package/dist/actions/ContextMenu.svelte.d.ts.map +1 -0
  20. package/dist/actions/Modal.svelte +474 -0
  21. package/dist/actions/Modal.svelte.d.ts +28 -0
  22. package/dist/actions/Modal.svelte.d.ts.map +1 -0
  23. package/dist/actions/Popover.svelte +1214 -0
  24. package/dist/actions/Popover.svelte.d.ts +31 -0
  25. package/dist/actions/Popover.svelte.d.ts.map +1 -0
  26. package/dist/actions/Portal.svelte +80 -0
  27. package/dist/actions/Portal.svelte.d.ts +17 -0
  28. package/dist/actions/Portal.svelte.d.ts.map +1 -0
  29. package/dist/actions/ThemeToggle.svelte +345 -0
  30. package/dist/actions/ThemeToggle.svelte.d.ts +15 -0
  31. package/dist/actions/ThemeToggle.svelte.d.ts.map +1 -0
  32. package/dist/actions/index.d.ts +13 -0
  33. package/dist/actions/index.d.ts.map +1 -0
  34. package/dist/actions/index.js +10 -0
  35. package/dist/actions/scrollbar.d.ts +48 -0
  36. package/dist/actions/scrollbar.d.ts.map +1 -0
  37. package/dist/actions/scrollbar.js +404 -0
  38. package/dist/display/Accordion.svelte +586 -0
  39. package/dist/display/Accordion.svelte.d.ts +41 -0
  40. package/dist/display/Accordion.svelte.d.ts.map +1 -0
  41. package/dist/display/Avatar.svelte +527 -0
  42. package/dist/display/Avatar.svelte.d.ts +22 -0
  43. package/dist/display/Avatar.svelte.d.ts.map +1 -0
  44. package/dist/display/AvatarGroup.svelte +298 -0
  45. package/dist/display/AvatarGroup.svelte.d.ts +31 -0
  46. package/dist/display/AvatarGroup.svelte.d.ts.map +1 -0
  47. package/dist/display/Calendar.svelte +1366 -0
  48. package/dist/display/Calendar.svelte.d.ts +58 -0
  49. package/dist/display/Calendar.svelte.d.ts.map +1 -0
  50. package/dist/display/Chart.svelte +1426 -0
  51. package/dist/display/Chart.svelte.d.ts +35 -0
  52. package/dist/display/Chart.svelte.d.ts.map +1 -0
  53. package/dist/display/Code.svelte +780 -0
  54. package/dist/display/Code.svelte.d.ts +19 -0
  55. package/dist/display/Code.svelte.d.ts.map +1 -0
  56. package/dist/display/Comparison.svelte +686 -0
  57. package/dist/display/Comparison.svelte.d.ts +22 -0
  58. package/dist/display/Comparison.svelte.d.ts.map +1 -0
  59. package/dist/display/Counter.svelte +285 -0
  60. package/dist/display/Counter.svelte.d.ts +21 -0
  61. package/dist/display/Counter.svelte.d.ts.map +1 -0
  62. package/dist/display/Expand.svelte +48 -0
  63. package/dist/display/Expand.svelte.d.ts +9 -0
  64. package/dist/display/Expand.svelte.d.ts.map +1 -0
  65. package/dist/display/List.svelte +294 -0
  66. package/dist/display/List.svelte.d.ts +40 -0
  67. package/dist/display/List.svelte.d.ts.map +1 -0
  68. package/dist/display/ListContextReset.svelte +19 -0
  69. package/dist/display/ListContextReset.svelte.d.ts +7 -0
  70. package/dist/display/ListContextReset.svelte.d.ts.map +1 -0
  71. package/dist/display/ListItem.svelte +834 -0
  72. package/dist/display/ListItem.svelte.d.ts +22 -0
  73. package/dist/display/ListItem.svelte.d.ts.map +1 -0
  74. package/dist/display/QR.svelte +1193 -0
  75. package/dist/display/QR.svelte.d.ts +23 -0
  76. package/dist/display/QR.svelte.d.ts.map +1 -0
  77. package/dist/display/SplitPane.svelte +744 -0
  78. package/dist/display/SplitPane.svelte.d.ts +25 -0
  79. package/dist/display/SplitPane.svelte.d.ts.map +1 -0
  80. package/dist/display/Stat.svelte +439 -0
  81. package/dist/display/Stat.svelte.d.ts +24 -0
  82. package/dist/display/Stat.svelte.d.ts.map +1 -0
  83. package/dist/display/Table.svelte +4654 -0
  84. package/dist/display/Table.svelte.d.ts +249 -0
  85. package/dist/display/Table.svelte.d.ts.map +1 -0
  86. package/dist/display/TableCellEditor.svelte +935 -0
  87. package/dist/display/TableCellEditor.svelte.d.ts +58 -0
  88. package/dist/display/TableCellEditor.svelte.d.ts.map +1 -0
  89. package/dist/display/Timeline.svelte +1258 -0
  90. package/dist/display/Timeline.svelte.d.ts +43 -0
  91. package/dist/display/Timeline.svelte.d.ts.map +1 -0
  92. package/dist/display/Tree.svelte +1740 -0
  93. package/dist/display/Tree.svelte.d.ts +74 -0
  94. package/dist/display/Tree.svelte.d.ts.map +1 -0
  95. package/dist/display/Typewriter.svelte +338 -0
  96. package/dist/display/Typewriter.svelte.d.ts +22 -0
  97. package/dist/display/Typewriter.svelte.d.ts.map +1 -0
  98. package/dist/display/index.d.ts +24 -0
  99. package/dist/display/index.d.ts.map +1 -0
  100. package/dist/display/index.js +18 -0
  101. package/dist/feedback/Callout.svelte +529 -0
  102. package/dist/feedback/Callout.svelte.d.ts +24 -0
  103. package/dist/feedback/Callout.svelte.d.ts.map +1 -0
  104. package/dist/feedback/Confetti.svelte +631 -0
  105. package/dist/feedback/Confetti.svelte.d.ts +90 -0
  106. package/dist/feedback/Confetti.svelte.d.ts.map +1 -0
  107. package/dist/feedback/Progress.svelte +382 -0
  108. package/dist/feedback/Progress.svelte.d.ts +25 -0
  109. package/dist/feedback/Progress.svelte.d.ts.map +1 -0
  110. package/dist/feedback/Toast.svelte +967 -0
  111. package/dist/feedback/Toast.svelte.d.ts +54 -0
  112. package/dist/feedback/Toast.svelte.d.ts.map +1 -0
  113. package/dist/feedback/index.d.ts +7 -0
  114. package/dist/feedback/index.d.ts.map +1 -0
  115. package/dist/feedback/index.js +4 -0
  116. package/dist/form/Checkbox.svelte +449 -0
  117. package/dist/form/Checkbox.svelte.d.ts +27 -0
  118. package/dist/form/Checkbox.svelte.d.ts.map +1 -0
  119. package/dist/form/Fieldset.svelte +410 -0
  120. package/dist/form/Fieldset.svelte.d.ts +22 -0
  121. package/dist/form/Fieldset.svelte.d.ts.map +1 -0
  122. package/dist/form/FileUpload.svelte +934 -0
  123. package/dist/form/FileUpload.svelte.d.ts +41 -0
  124. package/dist/form/FileUpload.svelte.d.ts.map +1 -0
  125. package/dist/form/Form.svelte +530 -0
  126. package/dist/form/Form.svelte.d.ts +120 -0
  127. package/dist/form/Form.svelte.d.ts.map +1 -0
  128. package/dist/form/Input.svelte +2858 -0
  129. package/dist/form/Input.svelte.d.ts +66 -0
  130. package/dist/form/Input.svelte.d.ts.map +1 -0
  131. package/dist/form/Radio.svelte +507 -0
  132. package/dist/form/Radio.svelte.d.ts +39 -0
  133. package/dist/form/Radio.svelte.d.ts.map +1 -0
  134. package/dist/form/Range.svelte +912 -0
  135. package/dist/form/Range.svelte.d.ts +33 -0
  136. package/dist/form/Range.svelte.d.ts.map +1 -0
  137. package/dist/form/Rating.svelte +429 -0
  138. package/dist/form/Rating.svelte.d.ts +28 -0
  139. package/dist/form/Rating.svelte.d.ts.map +1 -0
  140. package/dist/form/Select.svelte +1933 -0
  141. package/dist/form/Select.svelte.d.ts +54 -0
  142. package/dist/form/Select.svelte.d.ts.map +1 -0
  143. package/dist/form/Toggle.svelte +645 -0
  144. package/dist/form/Toggle.svelte.d.ts +50 -0
  145. package/dist/form/Toggle.svelte.d.ts.map +1 -0
  146. package/dist/form/index.d.ts +15 -0
  147. package/dist/form/index.d.ts.map +1 -0
  148. package/dist/form/index.js +10 -0
  149. package/dist/index.d.ts +7 -0
  150. package/dist/index.d.ts.map +1 -0
  151. package/dist/index.js +6 -0
  152. package/dist/layout/README.md +172 -0
  153. package/dist/media/Carousel.svelte +2424 -0
  154. package/dist/media/Carousel.svelte.d.ts +47 -0
  155. package/dist/media/Carousel.svelte.d.ts.map +1 -0
  156. package/dist/media/Gallery.svelte +2881 -0
  157. package/dist/media/Gallery.svelte.d.ts +82 -0
  158. package/dist/media/Gallery.svelte.d.ts.map +1 -0
  159. package/dist/media/Image.svelte +389 -0
  160. package/dist/media/Image.svelte.d.ts +33 -0
  161. package/dist/media/Image.svelte.d.ts.map +1 -0
  162. package/dist/media/PDF.svelte +1793 -0
  163. package/dist/media/PDF.svelte.d.ts +44 -0
  164. package/dist/media/PDF.svelte.d.ts.map +1 -0
  165. package/dist/media/Panorama.svelte +1391 -0
  166. package/dist/media/Panorama.svelte.d.ts +47 -0
  167. package/dist/media/Panorama.svelte.d.ts.map +1 -0
  168. package/dist/media/Video.svelte +2501 -0
  169. package/dist/media/Video.svelte.d.ts +58 -0
  170. package/dist/media/Video.svelte.d.ts.map +1 -0
  171. package/dist/media/carousel.d.ts +211 -0
  172. package/dist/media/carousel.d.ts.map +1 -0
  173. package/dist/media/carousel.js +408 -0
  174. package/dist/media/index.d.ts +11 -0
  175. package/dist/media/index.d.ts.map +1 -0
  176. package/dist/media/index.js +5 -0
  177. package/dist/navigation/BottomSheet.svelte +636 -0
  178. package/dist/navigation/BottomSheet.svelte.d.ts +27 -0
  179. package/dist/navigation/BottomSheet.svelte.d.ts.map +1 -0
  180. package/dist/navigation/Breadcrumbs.svelte +611 -0
  181. package/dist/navigation/Breadcrumbs.svelte.d.ts +28 -0
  182. package/dist/navigation/Breadcrumbs.svelte.d.ts.map +1 -0
  183. package/dist/navigation/Pagination.svelte +641 -0
  184. package/dist/navigation/Pagination.svelte.d.ts +27 -0
  185. package/dist/navigation/Pagination.svelte.d.ts.map +1 -0
  186. package/dist/navigation/Steps.svelte +965 -0
  187. package/dist/navigation/Steps.svelte.d.ts +43 -0
  188. package/dist/navigation/Steps.svelte.d.ts.map +1 -0
  189. package/dist/navigation/Tabs.svelte +698 -0
  190. package/dist/navigation/Tabs.svelte.d.ts +41 -0
  191. package/dist/navigation/Tabs.svelte.d.ts.map +1 -0
  192. package/dist/navigation/index.d.ts +8 -0
  193. package/dist/navigation/index.d.ts.map +1 -0
  194. package/dist/navigation/index.js +5 -0
  195. package/package.json +139 -0
@@ -0,0 +1,744 @@
1
+ <script lang="ts">
2
+ import { type Snippet } from 'svelte';
3
+ import { scrollbar } from '../actions/scrollbar';
4
+
5
+ const propId = $props.id();
6
+ let {
7
+ /** Whether the split is vertical (top/bottom) instead of horizontal (left/right) */
8
+ vertical = false,
9
+
10
+ /** First pane size as a percentage (0-100), bindable */
11
+ size = $bindable(50),
12
+
13
+ /** Minimum first pane size as a percentage */
14
+ min_size = 10,
15
+
16
+ /** Maximum first pane size as a percentage */
17
+ max_size = 90,
18
+
19
+ /** Snap points as percentages */
20
+ snap = [] as number[],
21
+
22
+ /** Distance in percentage to trigger snap */
23
+ snap_threshold = 8,
24
+
25
+ /** Whether panes can be collapsed */
26
+ collapsible = false,
27
+
28
+ /** Which pane is currently collapsed, bindable */
29
+ collapsed = $bindable(null) as 'first' | 'second' | null,
30
+
31
+ /** The ID of the element */
32
+ id = propId,
33
+
34
+ /** Specifies a custom class name for the container element */
35
+ class: class_name = '',
36
+
37
+ /** First pane content */
38
+ first = undefined as undefined | Snippet,
39
+
40
+ /** Second pane content */
41
+ second = undefined as undefined | Snippet,
42
+
43
+ /** Called when pane size changes */
44
+ onresize = undefined as ((detail: { size: number }) => void) | undefined,
45
+
46
+ /** Called when a pane is collapsed or expanded */
47
+ oncollapse = undefined as
48
+ | ((detail: { pane: 'first' | 'second' | null }) => void)
49
+ | undefined,
50
+ }: {
51
+ vertical?: boolean;
52
+ size?: number;
53
+ min_size?: number;
54
+ max_size?: number;
55
+ snap?: number[];
56
+ snap_threshold?: number;
57
+ collapsible?: boolean;
58
+ collapsed?: 'first' | 'second' | null;
59
+ id?: string;
60
+ class?: string;
61
+ first?: Snippet;
62
+ second?: Snippet;
63
+ onresize?: (detail: { size: number }) => void;
64
+ oncollapse?: (detail: { pane: 'first' | 'second' | null }) => void;
65
+ } = $props();
66
+
67
+ let container: HTMLElement | undefined = $state(undefined);
68
+ let dragging = $state(false);
69
+ let animating = $state(false);
70
+ let overshoot_px = $state(0);
71
+ let snapping = $state(false);
72
+ let snap_timer: ReturnType<typeof setTimeout> | undefined;
73
+ let animating_timer: ReturnType<typeof setTimeout> | undefined;
74
+ let last_pointer_coord = 0;
75
+ let snapped_to: number | null = null;
76
+ let expanded_during_drag = false;
77
+
78
+ const COLLAPSE_THRESHOLD = 5;
79
+
80
+ /** The size value before a collapse, so we can restore it on expand */
81
+ let size_before_collapse = $state(50);
82
+
83
+ const clamped_size = $derived(Math.min(max_size, Math.max(min_size, size)));
84
+
85
+ /** The flex-basis for the first pane (includes overshoot for smooth snap/rubber band) */
86
+ const first_basis = $derived.by(() => {
87
+ if (collapsed === 'first') return '0%';
88
+ if (collapsed === 'second') return '100%';
89
+ return `calc(${clamped_size}% + ${overshoot_px}px)`;
90
+ });
91
+
92
+ /** The flex-basis for the second pane (includes overshoot for smooth snap/rubber band) */
93
+ const second_basis = $derived.by(() => {
94
+ if (collapsed === 'first') return '100%';
95
+ if (collapsed === 'second') return '0%';
96
+ return `calc(${100 - clamped_size}% - ${overshoot_px}px)`;
97
+ });
98
+
99
+ /** Convert a pointer position to a percentage of the container */
100
+ function pointerToPercent(clientX: number, clientY: number): number {
101
+ if (!container) return size;
102
+ const rect = container.getBoundingClientRect();
103
+ let percent: number;
104
+ if (vertical) {
105
+ percent = ((clientY - rect.top) / rect.height) * 100;
106
+ } else {
107
+ percent = ((clientX - rect.left) / rect.width) * 100;
108
+ }
109
+ return percent;
110
+ }
111
+
112
+ /** Apply snap points and clamping to a raw percentage */
113
+ function applyConstraints(percent: number): number {
114
+ // Clamp to min/max
115
+ let result = Math.min(max_size, Math.max(min_size, percent));
116
+
117
+ // Apply snap points with hysteresis: once snapped, require a much larger
118
+ // movement to escape (2.2x threshold), making snaps feel strongly sticky
119
+ if (snap.length > 0) {
120
+ const threshold = snapped_to !== null ? snap_threshold * 2.2 : snap_threshold;
121
+ let best_snap = -1;
122
+ let min_dist = Infinity;
123
+
124
+ for (const point of snap) {
125
+ const dist = Math.abs(result - point);
126
+ if (dist < min_dist && dist <= threshold) {
127
+ min_dist = dist;
128
+ best_snap = point;
129
+ }
130
+ }
131
+
132
+ if (best_snap >= 0) {
133
+ result = best_snap;
134
+ snapped_to = best_snap;
135
+ } else {
136
+ snapped_to = null;
137
+ }
138
+ }
139
+
140
+ // Round to 2 decimal places for cleanliness
141
+ return Math.round(result * 100) / 100;
142
+ }
143
+
144
+ /** Update the size and fire the onresize event */
145
+ function updateSize(new_size: number) {
146
+ const constrained = applyConstraints(new_size);
147
+ if (constrained !== size) {
148
+ size = constrained;
149
+ onresize?.({ size });
150
+ }
151
+ }
152
+
153
+ /** Compute visual overshoot for magnetic snap gravity and edge rubber band */
154
+ function updateOvershoot() {
155
+ if (!container) return;
156
+ const rect = container.getBoundingClientRect();
157
+ const dimension = vertical ? rect.height : rect.width;
158
+ const raw_pct = vertical
159
+ ? (last_pointer_coord - rect.top) / rect.height
160
+ : (last_pointer_coord - rect.left) / rect.width;
161
+ const raw_percent = raw_pct * 100;
162
+
163
+ if (raw_percent < min_size) {
164
+ // Edge rubber band past min (tanh bounded)
165
+ const overflow_px = ((raw_percent - min_size) / 100) * dimension;
166
+ const max_shift = 24;
167
+ overshoot_px = max_shift * Math.tanh(overflow_px / 80);
168
+ } else if (raw_percent > max_size) {
169
+ // Edge rubber band past max (tanh bounded)
170
+ const overflow_px = ((raw_percent - max_size) / 100) * dimension;
171
+ const max_shift = 24;
172
+ overshoot_px = max_shift * Math.tanh(overflow_px / 80);
173
+ } else if (snapped_to !== null) {
174
+ // Magnetic snap gravity — smooth easing that reaches full-follow
175
+ // at the snap zone boundary, guaranteeing visual continuity.
176
+ // Uses the wider escape threshold to match hysteresis zone.
177
+ const snapped_pct = snapped_to / 100;
178
+ const pull_px = (raw_pct - snapped_pct) * dimension;
179
+ const escape_radius_px = ((snap_threshold * 2.2) / 100) * dimension;
180
+
181
+ if (escape_radius_px < 1) {
182
+ overshoot_px = 0;
183
+ } else {
184
+ const t = Math.min(1, Math.abs(pull_px) / escape_radius_px);
185
+ const gravity = 0.16;
186
+ const eased = gravity * t + (1 - gravity) * t * t;
187
+ overshoot_px = Math.sign(pull_px) * eased * escape_radius_px;
188
+ }
189
+ } else {
190
+ overshoot_px = 0;
191
+ }
192
+ }
193
+
194
+ /** Clean up drag state and listeners */
195
+ function stopDrag(trigger_snap_back = true) {
196
+ dragging = false;
197
+ snapped_to = null;
198
+ if (trigger_snap_back && Math.abs(overshoot_px) > 0.5) {
199
+ snapping = true;
200
+ clearTimeout(snap_timer);
201
+ snap_timer = setTimeout(() => {
202
+ snapping = false;
203
+ }, 400);
204
+ }
205
+ overshoot_px = 0;
206
+ document.removeEventListener('mousemove', handlePointerMove);
207
+ document.removeEventListener('mouseup', handlePointerUp);
208
+ document.removeEventListener('touchmove', handleTouchMove);
209
+ document.removeEventListener('touchend', handleTouchEnd);
210
+ }
211
+
212
+ /** Collapse or expand a pane */
213
+ function setCollapsed(pane: 'first' | 'second' | null) {
214
+ if (!collapsible) return;
215
+
216
+ if (pane !== null && collapsed === null) {
217
+ // Collapsing: save current size
218
+ size_before_collapse = size;
219
+ }
220
+
221
+ animating = true;
222
+ collapsed = pane;
223
+ oncollapse?.({ pane });
224
+
225
+ if (pane === null) {
226
+ // Restoring: set size back
227
+ size = size_before_collapse;
228
+ onresize?.({ size });
229
+ }
230
+
231
+ // Remove animating flag after transition completes
232
+ clearTimeout(animating_timer);
233
+ animating_timer = setTimeout(() => {
234
+ animating = false;
235
+ }, 200);
236
+ }
237
+
238
+ /** Toggle collapse: collapse the smaller pane, or expand if already collapsed */
239
+ function toggleCollapse() {
240
+ if (!collapsible) return;
241
+ if (collapsed !== null) {
242
+ setCollapsed(null);
243
+ } else {
244
+ // Collapse the smaller pane
245
+ setCollapsed(size <= 50 ? 'first' : 'second');
246
+ }
247
+ }
248
+
249
+ // Remove document listeners and pending timers if the component unmounts mid-drag
250
+ $effect(() => {
251
+ return () => {
252
+ stopDrag(false);
253
+ clearTimeout(snap_timer);
254
+ clearTimeout(animating_timer);
255
+ };
256
+ });
257
+
258
+ // ---- Pointer drag handling ----
259
+
260
+ function handlePointerDown(e: MouseEvent) {
261
+ e.preventDefault();
262
+ dragging = true;
263
+ snapping = false;
264
+ expanded_during_drag = false;
265
+ clearTimeout(snap_timer);
266
+ last_pointer_coord = vertical ? e.clientY : e.clientX;
267
+ document.addEventListener('mousemove', handlePointerMove);
268
+ document.addEventListener('mouseup', handlePointerUp);
269
+ }
270
+
271
+ function handlePointerMove(e: MouseEvent) {
272
+ if (!dragging) return;
273
+ e.preventDefault();
274
+ const raw = pointerToPercent(e.clientX, e.clientY);
275
+
276
+ // Expand collapsed pane by dragging away from edge
277
+ if (collapsed !== null) {
278
+ const should_expand =
279
+ (collapsed === 'first' && raw > COLLAPSE_THRESHOLD) ||
280
+ (collapsed === 'second' && raw < 100 - COLLAPSE_THRESHOLD);
281
+ if (should_expand) {
282
+ collapsed = null;
283
+ expanded_during_drag = true;
284
+ oncollapse?.({ pane: null });
285
+ updateSize(raw);
286
+ last_pointer_coord = vertical ? e.clientY : e.clientX;
287
+ updateOvershoot();
288
+ }
289
+ return;
290
+ }
291
+
292
+ // Don't re-collapse in the same drag that expanded a pane
293
+ if (collapsible && !expanded_during_drag) {
294
+ if (raw < COLLAPSE_THRESHOLD) {
295
+ stopDrag(false);
296
+ setCollapsed('first');
297
+ return;
298
+ }
299
+ if (raw > 100 - COLLAPSE_THRESHOLD) {
300
+ stopDrag(false);
301
+ setCollapsed('second');
302
+ return;
303
+ }
304
+ }
305
+
306
+ updateSize(raw);
307
+ last_pointer_coord = vertical ? e.clientY : e.clientX;
308
+ updateOvershoot();
309
+ }
310
+
311
+ function handlePointerUp() {
312
+ if (!dragging) return;
313
+ stopDrag();
314
+ }
315
+
316
+ // Touch handling
317
+ function handleTouchStart(e: TouchEvent) {
318
+ e.preventDefault();
319
+ dragging = true;
320
+ snapping = false;
321
+ expanded_during_drag = false;
322
+ clearTimeout(snap_timer);
323
+ const touch = e.touches[0];
324
+ last_pointer_coord = vertical ? touch.clientY : touch.clientX;
325
+ document.addEventListener('touchmove', handleTouchMove, { passive: false });
326
+ document.addEventListener('touchend', handleTouchEnd);
327
+ }
328
+
329
+ function handleTouchMove(e: TouchEvent) {
330
+ if (!dragging) return;
331
+ e.preventDefault();
332
+ const touch = e.touches[0];
333
+ const raw = pointerToPercent(touch.clientX, touch.clientY);
334
+
335
+ // Expand collapsed pane by dragging away from edge
336
+ if (collapsed !== null) {
337
+ const should_expand =
338
+ (collapsed === 'first' && raw > COLLAPSE_THRESHOLD) ||
339
+ (collapsed === 'second' && raw < 100 - COLLAPSE_THRESHOLD);
340
+ if (should_expand) {
341
+ collapsed = null;
342
+ expanded_during_drag = true;
343
+ oncollapse?.({ pane: null });
344
+ updateSize(raw);
345
+ last_pointer_coord = vertical ? touch.clientY : touch.clientX;
346
+ updateOvershoot();
347
+ }
348
+ return;
349
+ }
350
+
351
+ // Don't re-collapse in the same drag that expanded a pane
352
+ if (collapsible && !expanded_during_drag) {
353
+ if (raw < COLLAPSE_THRESHOLD) {
354
+ stopDrag(false);
355
+ setCollapsed('first');
356
+ return;
357
+ }
358
+ if (raw > 100 - COLLAPSE_THRESHOLD) {
359
+ stopDrag(false);
360
+ setCollapsed('second');
361
+ return;
362
+ }
363
+ }
364
+
365
+ updateSize(raw);
366
+ last_pointer_coord = vertical ? touch.clientY : touch.clientX;
367
+ updateOvershoot();
368
+ }
369
+
370
+ function handleTouchEnd() {
371
+ if (!dragging) return;
372
+ stopDrag();
373
+ }
374
+
375
+ // Double-click to collapse
376
+ function handleDblClick() {
377
+ if (!collapsible) return;
378
+ toggleCollapse();
379
+ }
380
+
381
+ // ---- Keyboard handling ----
382
+
383
+ function handleKeyDown(e: KeyboardEvent) {
384
+ const step = e.shiftKey ? 5 : 1;
385
+ let new_size = collapsed === null ? size : size_before_collapse;
386
+
387
+ switch (e.key) {
388
+ case 'ArrowLeft':
389
+ case 'ArrowUp':
390
+ e.preventDefault();
391
+ if (collapsed !== null) {
392
+ setCollapsed(null);
393
+ return;
394
+ }
395
+ new_size = size - step;
396
+ break;
397
+ case 'ArrowRight':
398
+ case 'ArrowDown':
399
+ e.preventDefault();
400
+ if (collapsed !== null) {
401
+ setCollapsed(null);
402
+ return;
403
+ }
404
+ new_size = size + step;
405
+ break;
406
+ case 'Home':
407
+ e.preventDefault();
408
+ if (collapsed !== null) {
409
+ setCollapsed(null);
410
+ }
411
+ new_size = min_size;
412
+ break;
413
+ case 'End':
414
+ e.preventDefault();
415
+ if (collapsed !== null) {
416
+ setCollapsed(null);
417
+ }
418
+ new_size = max_size;
419
+ break;
420
+ case 'Enter':
421
+ e.preventDefault();
422
+ toggleCollapse();
423
+ return;
424
+ default:
425
+ return;
426
+ }
427
+
428
+ updateSize(new_size);
429
+ }
430
+
431
+ // ---- Expand button handler ----
432
+
433
+ function handleExpand() {
434
+ setCollapsed(null);
435
+ }
436
+ </script>
437
+
438
+ <div
439
+ class={['split-pane', class_name].filter(Boolean).join(' ')}
440
+ class:vertical
441
+ class:horizontal={!vertical}
442
+ class:dragging
443
+ class:snapping
444
+ class:animating
445
+ class:collapsed-first={collapsed === 'first'}
446
+ class:collapsed-second={collapsed === 'second'}
447
+ {id}
448
+ bind:this={container}>
449
+ <!-- First pane -->
450
+ <div
451
+ class="pane first"
452
+ style:flex-basis={first_basis}
453
+ aria-hidden={collapsed === 'first' || undefined}
454
+ {@attach scrollbar()}>
455
+ {#if first}
456
+ {@render first()}
457
+ {/if}
458
+ {#if collapsible && collapsed === 'first'}
459
+ <button
460
+ class:vertical
461
+ type="button"
462
+ aria-label="Expand first pane"
463
+ onclick={handleExpand}>
464
+ <svg viewBox="0 0 16 16" aria-hidden="true">
465
+ {#if vertical}
466
+ <path
467
+ d="M3 6l5 5 5-5"
468
+ fill="none"
469
+ stroke="currentColor"
470
+ stroke-width="2"
471
+ stroke-linecap="round"
472
+ stroke-linejoin="round" />
473
+ {:else}
474
+ <path
475
+ d="M6 3l5 5-5 5"
476
+ fill="none"
477
+ stroke="currentColor"
478
+ stroke-width="2"
479
+ stroke-linecap="round"
480
+ stroke-linejoin="round" />
481
+ {/if}
482
+ </svg>
483
+ </button>
484
+ {/if}
485
+ </div>
486
+
487
+ <!-- Divider -->
488
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
489
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
490
+ <div
491
+ class="divider"
492
+ role="separator"
493
+ tabindex="0"
494
+ aria-orientation={vertical ? 'vertical' : 'horizontal'}
495
+ aria-valuenow={Math.round(clamped_size)}
496
+ aria-valuemin={min_size}
497
+ aria-valuemax={max_size}
498
+ aria-label="Resize panes"
499
+ onmousedown={handlePointerDown}
500
+ ontouchstart={handleTouchStart}
501
+ ondblclick={handleDblClick}
502
+ onkeydown={handleKeyDown}>
503
+ <div class="handle"></div>
504
+ </div>
505
+
506
+ <!-- Second pane -->
507
+ <div
508
+ class="pane second"
509
+ style:flex-basis={second_basis}
510
+ aria-hidden={collapsed === 'second' || undefined}
511
+ {@attach scrollbar()}>
512
+ {#if collapsible && collapsed === 'second'}
513
+ <button
514
+ class:vertical
515
+ type="button"
516
+ aria-label="Expand second pane"
517
+ onclick={handleExpand}>
518
+ <svg viewBox="0 0 16 16" aria-hidden="true">
519
+ {#if vertical}
520
+ <path
521
+ d="M3 10l5-5 5 5"
522
+ fill="none"
523
+ stroke="currentColor"
524
+ stroke-width="2"
525
+ stroke-linecap="round"
526
+ stroke-linejoin="round" />
527
+ {:else}
528
+ <path
529
+ d="M10 3l-5 5 5 5"
530
+ fill="none"
531
+ stroke="currentColor"
532
+ stroke-width="2"
533
+ stroke-linecap="round"
534
+ stroke-linejoin="round" />
535
+ {/if}
536
+ </svg>
537
+ </button>
538
+ {/if}
539
+ {#if second}
540
+ {@render second()}
541
+ {/if}
542
+ </div>
543
+ </div>
544
+
545
+ <style>
546
+ .split-pane {
547
+ display: flex;
548
+ width: 100%;
549
+ height: 100%;
550
+ overflow: hidden;
551
+
552
+ &.horizontal {
553
+ flex-direction: row;
554
+ }
555
+
556
+ &.vertical {
557
+ flex-direction: column;
558
+ }
559
+
560
+ &.dragging {
561
+ user-select: none;
562
+ -webkit-user-select: none;
563
+ cursor: col-resize;
564
+
565
+ &.vertical {
566
+ cursor: row-resize;
567
+ }
568
+ }
569
+ }
570
+
571
+ .pane {
572
+ position: relative;
573
+ overflow: auto;
574
+ min-width: 0;
575
+ min-height: 0;
576
+
577
+ /* Explicit cross-axis size so nested components with height/width: 100% resolve correctly */
578
+ .horizontal > & {
579
+ height: 100%;
580
+ }
581
+ .vertical > & {
582
+ width: 100%;
583
+ }
584
+
585
+ .snapping & {
586
+ transition: flex-basis 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
587
+ }
588
+
589
+ .animating & {
590
+ transition: flex-basis 200ms ease;
591
+ }
592
+
593
+ .dragging & {
594
+ transition: none;
595
+ }
596
+ }
597
+
598
+ .first {
599
+ flex-shrink: 0;
600
+ flex-grow: 0;
601
+
602
+ .collapsed-first & {
603
+ overflow: hidden;
604
+ }
605
+ }
606
+
607
+ .second {
608
+ flex-shrink: 0;
609
+ flex-grow: 0;
610
+
611
+ .collapsed-second & {
612
+ overflow: hidden;
613
+ }
614
+ }
615
+
616
+ .divider {
617
+ flex-shrink: 0;
618
+ position: relative;
619
+ display: flex;
620
+ align-items: center;
621
+ justify-content: center;
622
+ background: light-dark(var(--color-border, #e0e0e0), var(--color-border, #3a3a3a));
623
+ touch-action: none;
624
+ outline: none;
625
+ z-index: 1;
626
+
627
+ &:hover,
628
+ &:active {
629
+ background: light-dark(var(--color-action, #1976d2), var(--color-action, #5c9ce6));
630
+ transition: none;
631
+ }
632
+
633
+ &:focus-visible {
634
+ outline: 2px solid var(--color-action, #1976d2);
635
+ outline-offset: -2px;
636
+ }
637
+
638
+ /* Direct child selectors prevent leaking into nested SplitPane instances */
639
+ .horizontal > & {
640
+ width: 4px;
641
+ cursor: col-resize;
642
+ }
643
+ .vertical > & {
644
+ height: 4px;
645
+ cursor: row-resize;
646
+ }
647
+ .dragging > & {
648
+ background: light-dark(var(--color-action, #1976d2), var(--color-action, #5c9ce6));
649
+ }
650
+ }
651
+
652
+ .handle {
653
+ position: absolute;
654
+
655
+ .horizontal > .divider > & {
656
+ width: 12px;
657
+ height: 100%;
658
+ left: -4px;
659
+ }
660
+ .vertical > .divider > & {
661
+ height: 12px;
662
+ width: 100%;
663
+ top: -4px;
664
+ }
665
+ }
666
+
667
+ /* The expand buttons are the component's only <button>s */
668
+ button {
669
+ position: absolute;
670
+ z-index: 2;
671
+ display: flex;
672
+ align-items: center;
673
+ justify-content: center;
674
+ border: 1px solid
675
+ light-dark(var(--color-border, #e0e0e0), var(--color-border, #3a3a3a));
676
+ background: light-dark(var(--color-bg, #ffffff), var(--color-bg, #1e1e1e));
677
+ color: light-dark(var(--color-text-muted, #666), var(--color-text-muted, #aaa));
678
+ cursor: pointer;
679
+ padding: 0;
680
+ width: 20px;
681
+ height: 20px;
682
+ border-radius: var(--radius-md, 4px);
683
+ @supports (corner-shape: squircle) {
684
+ corner-shape: squircle;
685
+ border-radius: calc(var(--radius-md, 4px) * var(--squircle-ratio, 2));
686
+ }
687
+ transition:
688
+ color 150ms ease,
689
+ background 150ms ease;
690
+
691
+ &:hover {
692
+ color: light-dark(var(--color-action, #1976d2), var(--color-action, #5c9ce6));
693
+ background: light-dark(
694
+ var(--color-bg-active, #f5f5f5),
695
+ var(--color-bg-active, #2a2a2a)
696
+ );
697
+ transition: none;
698
+ }
699
+
700
+ &:focus-visible {
701
+ outline: 2px solid var(--color-action, #1976d2);
702
+ outline-offset: 2px;
703
+ }
704
+
705
+ svg {
706
+ width: 12px;
707
+ height: 12px;
708
+ }
709
+ }
710
+
711
+ /* Expand button positioning for collapsed first pane (horizontal) */
712
+ .collapsed-first.horizontal .first button {
713
+ top: 50%;
714
+ right: 0;
715
+ transform: translateY(-50%);
716
+ }
717
+
718
+ /* Expand button positioning for collapsed first pane (vertical) */
719
+ .collapsed-first.vertical .first button {
720
+ left: 50%;
721
+ bottom: 0;
722
+ transform: translateX(-50%);
723
+ }
724
+
725
+ /* Expand button positioning for collapsed second pane (horizontal) */
726
+ .collapsed-second.horizontal .second button {
727
+ top: 50%;
728
+ left: 0;
729
+ transform: translateY(-50%);
730
+ }
731
+
732
+ /* Expand button positioning for collapsed second pane (vertical) */
733
+ .collapsed-second.vertical .second button {
734
+ left: 50%;
735
+ top: 0;
736
+ transform: translateX(-50%);
737
+ }
738
+
739
+ @media (prefers-reduced-motion: reduce) {
740
+ .pane {
741
+ transition: none;
742
+ }
743
+ }
744
+ </style>