@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,636 @@
1
+ <script lang="ts">
2
+ import { type Snippet, tick, untrack } from 'svelte';
3
+ import { portal } from '../actions/Portal.svelte';
4
+ import { scrollbar } from '../actions/scrollbar';
5
+
6
+ /**
7
+ * A snap point expressed either as a fraction of the viewport height (a value
8
+ * `<= 1`, e.g. `0.5` for half the screen) or as an absolute pixel height (a
9
+ * value `> 1`, e.g. `120` for a 120px peek). Mixing the two is allowed, so
10
+ * `[120, 0.6, 1]` means a 120px peek, 60% of the viewport, then full height.
11
+ */
12
+ type SnapPoint = number;
13
+
14
+ const propId = $props.id();
15
+ let {
16
+ /** Whether the bottom sheet is open */
17
+ open = $bindable(false) as boolean,
18
+
19
+ /**
20
+ * Snap heights the sheet settles to. Each value is a fraction of the
21
+ * viewport height (`<= 1`) or an absolute pixel height (`> 1`). They're
22
+ * sorted ascending internally, and clamped to the content height so a
23
+ * short sheet never opens taller than its content.
24
+ */
25
+ snap_points = [0.5, 1] as SnapPoint[],
26
+
27
+ /**
28
+ * Index of the snap point to open to. Defaults to the largest snap point
29
+ * so the sheet opens up big, the way the legacy sheets did. Set to `0` to
30
+ * open at the smallest/peek height instead.
31
+ */
32
+ default_snap = undefined as undefined | number,
33
+
34
+ /** The current snap point index (`$bindable`) */
35
+ snap = $bindable(0) as number,
36
+
37
+ /**
38
+ * Morph progress (0-1) of the sheet between its collapsed and expanded
39
+ * states. Drives the optional morphing header. By default it interpolates
40
+ * across the gap between the two smallest snap points; override the range
41
+ * with {@link morph_range}. Bind to it (`bind:morph_percent`) or read it from
42
+ * the {@link onmorph} callback / the `--morph-percent` CSS variable.
43
+ */
44
+ morph_percent = $bindable(0) as number,
45
+
46
+ /**
47
+ * The height range `[from, to]` over which {@link morph_percent} animates
48
+ * from 0 to 1. Values follow the same fraction-or-pixel rule as
49
+ * {@link snap_points}. Defaults to `[snap_points[0], snap_points[1]]`.
50
+ */
51
+ morph_range = undefined as undefined | [number, number],
52
+
53
+ /** Whether the sheet can be dismissed by dragging down, the backdrop, or Escape */
54
+ dismissible = true,
55
+
56
+ /**
57
+ * Whether to render the frosted backdrop (blur + tint). When `false` the
58
+ * backdrop is fully transparent but still catches clicks to dismiss the
59
+ * sheet — pass `dismissible={false}` too if you want the page behind to
60
+ * stay interactive.
61
+ */
62
+ backdrop = true,
63
+
64
+ /** Whether to lock body scroll while the sheet is open */
65
+ blocking = true,
66
+
67
+ /** Maximum width of the sheet in pixels (it stays centered when wider) */
68
+ max_width = 500,
69
+
70
+ /** Element ID */
71
+ id = propId,
72
+
73
+ /** Additional CSS classes */
74
+ class: class_name = '',
75
+
76
+ /** Scrollable content. Receives the current morph percent (0-1). */
77
+ children = undefined as undefined | Snippet<[number]>,
78
+
79
+ /**
80
+ * Fixed header rendered above the scrollable content and used as a drag
81
+ * area. Receives the current morph percent (0-1) so you can build a
82
+ * morphing header.
83
+ */
84
+ header = undefined as undefined | Snippet<[number]>,
85
+
86
+ /** Called when the sheet opens */
87
+ onopen = undefined as undefined | (() => void),
88
+
89
+ /** Called when the sheet finishes closing */
90
+ onclose = undefined as undefined | (() => void),
91
+
92
+ /** Called when the sheet settles on a snap point */
93
+ onsnap = undefined as
94
+ | undefined
95
+ | ((detail: { index: number; height: number }) => void),
96
+
97
+ /** Called whenever the morph percent (0-1) changes */
98
+ onmorph = undefined as undefined | ((percent: number) => void),
99
+ } = $props();
100
+
101
+ // --- Tuning constants ---
102
+ const DRAG_THRESHOLD = 4; // px of movement before a press becomes a drag
103
+ const FLICK_VELOCITY = 0.4; // px/ms that counts as a fast swipe
104
+ const DISMISS_FACTOR = 0.5; // release below this fraction of the lowest snap dismisses
105
+ const RUBBER = 0.15; // resistance when overscrolling past the top
106
+
107
+ // --- Element refs ---
108
+ let panel_el = $state<HTMLElement | undefined>();
109
+ let backdrop_el = $state<HTMLElement | undefined>();
110
+ let content_el = $state<HTMLElement | undefined>();
111
+
112
+ // --- Position state ---
113
+ /** How many pixels of the sheet are revealed from the bottom (0 = hidden). */
114
+ let offset = $state(0);
115
+ let viewport_h = $state(0);
116
+ let container_h = $state(0);
117
+
118
+ // --- Interaction state ---
119
+ let dragging = $state(false);
120
+ let animating = $state(false);
121
+ let current_snap = $state(0);
122
+ let was_open = $state(false);
123
+
124
+ // --- Pointer tracking ---
125
+ let active_pointer: number | null = null;
126
+ /** none → idle, pending → press not yet classified, sheet → dragging the sheet, native → let the content scroll. */
127
+ let drag_mode: 'none' | 'pending' | 'sheet' | 'native' = 'none';
128
+ let drag_from_content = false;
129
+ let drag_from_handle = false;
130
+ let start_y = 0;
131
+ let start_offset = 0;
132
+ let last_y = 0;
133
+ let last_t = 0;
134
+ let velocity = 0; // px/ms, positive = moving up
135
+
136
+ // --- Easings (mirror svelte/easing without the import) ---
137
+ const clamp = (v: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, v));
138
+ const quartOut = (t: number) => 1 - Math.pow(1 - t, 4);
139
+ const expoOut = (t: number) => (t >= 1 ? 1 : 1 - Math.pow(2, -10 * t));
140
+ const quadInOut = (t: number) =>
141
+ t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
142
+
143
+ function prefersReducedMotion() {
144
+ return (
145
+ typeof window !== 'undefined' &&
146
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches
147
+ );
148
+ }
149
+
150
+ // --- Derived geometry ---
151
+ /** Resolve a snap value: <= 1 is a viewport fraction, > 1 is absolute pixels. */
152
+ const resolve = (v: number) => (v <= 1 ? v * viewport_h : v);
153
+ const sorted_snaps = $derived([...snap_points].sort((a, b) => a - b));
154
+ const max_offset = $derived(Math.min(viewport_h || Infinity, container_h || Infinity));
155
+ const snap_heights = $derived(
156
+ sorted_snaps.map((v) => Math.min(resolve(v), max_offset || Infinity)),
157
+ );
158
+ const default_index = $derived(
159
+ default_snap == null
160
+ ? sorted_snaps.length - 1
161
+ : clamp(Math.round(default_snap), 0, sorted_snaps.length - 1),
162
+ );
163
+ const at_max = $derived(max_offset > 0 && offset >= max_offset - 1);
164
+ /**
165
+ * Lowest offset the sheet may be dragged to. Dismissible sheets can travel
166
+ * all the way down to 0; non-dismissible sheets are clamped at their lowest
167
+ * snap point so a downward drag can never pull them below rest.
168
+ */
169
+ const min_offset = $derived(dismissible ? 0 : (snap_heights[0] ?? 0));
170
+ /** Whether the sheet has somewhere to expand upward beyond its lowest snap. */
171
+ const can_expand = $derived(
172
+ snap_heights.length > 1 &&
173
+ snap_heights[snap_heights.length - 1] - snap_heights[0] > 1,
174
+ );
175
+ /** The drag handle is only an affordance when dragging can do something. */
176
+ const show_handle = $derived(dismissible || can_expand);
177
+
178
+ // --- Morph ---
179
+ const morph_range_px = $derived<[number, number]>(
180
+ morph_range
181
+ ? [resolve(morph_range[0]), resolve(morph_range[1])]
182
+ : snap_heights.length >= 2
183
+ ? [snap_heights[0], snap_heights[1]]
184
+ : [0, snap_heights[0] ?? 0],
185
+ );
186
+ const morph = $derived.by(() => {
187
+ const [from, to] = morph_range_px;
188
+ if (to <= from) return offset > from ? 1 : 0;
189
+ return quadInOut(clamp((offset - from) / (to - from), 0, 1));
190
+ });
191
+ $effect(() => {
192
+ morph_percent = morph;
193
+ onmorph?.(morph);
194
+ });
195
+
196
+ // --- Backdrop fade: reaches full tint over the lower ~60% of travel ---
197
+ const fade_distance = $derived(
198
+ Math.min(max_offset || Infinity, (viewport_h || 0) * 0.6) || 1,
199
+ );
200
+ const backdrop_opacity = $derived(quartOut(clamp(offset / fade_distance, 0, 1)));
201
+
202
+ // --- Viewport tracking ---
203
+ $effect(() => {
204
+ const update = () => (viewport_h = window.innerHeight);
205
+ update();
206
+ window.addEventListener('resize', update);
207
+ return () => window.removeEventListener('resize', update);
208
+ });
209
+
210
+ // --- Keep the content height fresh ---
211
+ $effect(() => {
212
+ if (!panel_el) return;
213
+ const ro = new ResizeObserver(() => (container_h = panel_el!.clientHeight));
214
+ ro.observe(panel_el);
215
+ container_h = panel_el.clientHeight;
216
+ return () => ro.disconnect();
217
+ });
218
+
219
+ // --- Body scroll lock ---
220
+ $effect(() => {
221
+ if (!open || !blocking) return;
222
+ const original = document.body.style.overflow;
223
+ document.body.style.overflow = 'hidden';
224
+ return () => {
225
+ document.body.style.overflow = original;
226
+ };
227
+ });
228
+
229
+ // --- Open / close lifecycle ---
230
+ $effect(() => {
231
+ const is_open = open;
232
+ untrack(() => {
233
+ if (is_open && !was_open) {
234
+ was_open = true;
235
+ openSheet();
236
+ } else if (!is_open && was_open) {
237
+ was_open = false;
238
+ closeSheet();
239
+ }
240
+ });
241
+ });
242
+
243
+ async function openSheet() {
244
+ viewport_h = window.innerHeight;
245
+ await tick();
246
+ if (panel_el) container_h = panel_el.clientHeight;
247
+ const idx = default_index;
248
+ current_snap = idx;
249
+ snap = idx;
250
+ await animateSheet(snap_heights[idx] ?? max_offset, 550, expoOut);
251
+ onopen?.();
252
+ }
253
+
254
+ async function closeSheet() {
255
+ await animateSheet(0, 350, quartOut);
256
+ offset = 0;
257
+ onclose?.();
258
+ }
259
+
260
+ // --- React to external snap changes ---
261
+ $effect(() => {
262
+ const s = snap;
263
+ untrack(() => {
264
+ if (!open || dragging || animating) return;
265
+ if (s !== current_snap && s >= 0 && s < snap_heights.length) snapTo(s);
266
+ });
267
+ });
268
+
269
+ // --- Snapping ---
270
+ function nearestSnapIndex(y: number): number {
271
+ let best = 0;
272
+ let dist = Infinity;
273
+ for (let i = 0; i < snap_heights.length; i++) {
274
+ const d = Math.abs(y - snap_heights[i]);
275
+ if (d < dist) {
276
+ dist = d;
277
+ best = i;
278
+ }
279
+ }
280
+ return best;
281
+ }
282
+
283
+ function snapTo(index: number) {
284
+ const idx = clamp(index, 0, snap_heights.length - 1);
285
+ current_snap = idx;
286
+ snap = idx;
287
+ const height = snap_heights[idx] ?? 0;
288
+ animateSheet(height, 450, expoOut).then(() => onsnap?.({ index: idx, height }));
289
+ }
290
+
291
+ function toggleSnap() {
292
+ if (current_snap >= snap_heights.length - 1) snapTo(0);
293
+ else snapTo(snap_heights.length - 1);
294
+ }
295
+
296
+ function dismiss() {
297
+ if (!dismissible) {
298
+ snapTo(0);
299
+ return;
300
+ }
301
+ open = false; // open/close effect animates the sheet down and fires onclose
302
+ }
303
+
304
+ // --- Animation ---
305
+ let raf_id = 0;
306
+ let anim_token = 0;
307
+
308
+ function cancelAnimation() {
309
+ cancelAnimationFrame(raf_id);
310
+ anim_token++;
311
+ animating = false;
312
+ }
313
+
314
+ function animateSheet(
315
+ target: number,
316
+ duration = 450,
317
+ easing: (t: number) => number = quartOut,
318
+ ): Promise<void> {
319
+ return new Promise((resolve) => {
320
+ cancelAnimationFrame(raf_id);
321
+ const from = untrack(() => offset);
322
+ if (from === target) return resolve();
323
+ if (prefersReducedMotion()) {
324
+ offset = target;
325
+ animating = false;
326
+ return resolve();
327
+ }
328
+ animating = true;
329
+ const token = ++anim_token;
330
+ const start = performance.now();
331
+ function step(now: number) {
332
+ if (token !== anim_token) return resolve();
333
+ const p = Math.min(1, (now - start) / duration);
334
+ offset = from + (target - from) * easing(p);
335
+ if (p < 1) {
336
+ raf_id = requestAnimationFrame(step);
337
+ } else {
338
+ offset = target;
339
+ animating = false;
340
+ resolve();
341
+ }
342
+ }
343
+ raf_id = requestAnimationFrame(step);
344
+ });
345
+ }
346
+
347
+ // --- Gesture handling ---
348
+ function isWithinContent(target: EventTarget | null) {
349
+ return !!content_el && target instanceof Node && content_el.contains(target);
350
+ }
351
+ function isWithinHandle(target: EventTarget | null) {
352
+ return target instanceof Element && !!target.closest('.handle');
353
+ }
354
+
355
+ function onPanelPointerDown(e: PointerEvent) {
356
+ if (e.button === 2 || active_pointer !== null) return;
357
+ drag_from_content = isWithinContent(e.target);
358
+ drag_from_handle = isWithinHandle(e.target);
359
+
360
+ // Already scrolled down inside the content: let the browser scroll it.
361
+ if (drag_from_content && at_max && content_el && content_el.scrollTop > 0) {
362
+ drag_mode = 'native';
363
+ return;
364
+ }
365
+
366
+ cancelAnimation();
367
+ active_pointer = e.pointerId;
368
+ drag_mode = 'pending';
369
+ start_y = last_y = e.clientY;
370
+ start_offset = untrack(() => offset);
371
+ last_t = e.timeStamp;
372
+ velocity = 0;
373
+
374
+ window.addEventListener('pointermove', onPointerMove, { passive: false });
375
+ window.addEventListener('pointerup', onPointerUp);
376
+ window.addEventListener('pointercancel', onPointerUp);
377
+ }
378
+
379
+ function onPointerMove(e: PointerEvent) {
380
+ if (e.pointerId !== active_pointer) return;
381
+
382
+ const total = start_y - e.clientY; // positive = moved up
383
+ const dt = Math.max(1, e.timeStamp - last_t);
384
+ velocity = (last_y - e.clientY) / dt;
385
+ last_y = e.clientY;
386
+ last_t = e.timeStamp;
387
+
388
+ if (drag_mode === 'pending') {
389
+ if (Math.abs(total) < DRAG_THRESHOLD) return;
390
+ if (drag_from_content && at_max) {
391
+ const pulling_down = total < 0;
392
+ const at_top = !content_el || content_el.scrollTop <= 0;
393
+ if (pulling_down && at_top) {
394
+ drag_mode = 'sheet';
395
+ } else {
396
+ // Hand the gesture back to the browser for native scrolling.
397
+ drag_mode = 'native';
398
+ teardownPointer();
399
+ return;
400
+ }
401
+ } else {
402
+ drag_mode = 'sheet';
403
+ }
404
+ dragging = true;
405
+ document.body.style.userSelect = 'none';
406
+ }
407
+
408
+ if (drag_mode !== 'sheet') return;
409
+ e.preventDefault();
410
+
411
+ let next = start_offset + total;
412
+ if (next > max_offset) next = max_offset + (next - max_offset) * RUBBER;
413
+ // Hard floor: non-dismissible sheets never travel below their lowest
414
+ // snap point (no drag-then-snap-back); dismissible ones stop at 0.
415
+ if (next < min_offset) next = min_offset;
416
+ offset = next;
417
+ }
418
+
419
+ function onPointerUp(e: PointerEvent) {
420
+ if (e.pointerId !== active_pointer) return;
421
+ const mode = drag_mode;
422
+ const tapped_handle =
423
+ drag_from_handle && Math.abs(start_y - e.clientY) < DRAG_THRESHOLD;
424
+ teardownPointer();
425
+
426
+ if (mode === 'sheet') {
427
+ dragging = false;
428
+ releaseToSnap();
429
+ } else if (mode === 'pending' && tapped_handle) {
430
+ toggleSnap();
431
+ }
432
+ }
433
+
434
+ function teardownPointer() {
435
+ window.removeEventListener('pointermove', onPointerMove);
436
+ window.removeEventListener('pointerup', onPointerUp);
437
+ window.removeEventListener('pointercancel', onPointerUp);
438
+ document.body.style.userSelect = '';
439
+ active_pointer = null;
440
+ drag_mode = 'none';
441
+ }
442
+
443
+ function releaseToSnap() {
444
+ const speed = Math.abs(velocity);
445
+ const moving_down = velocity < 0;
446
+ const nearest = nearestSnapIndex(offset);
447
+
448
+ if (speed > FLICK_VELOCITY) {
449
+ if (moving_down) {
450
+ if (nearest <= 0) dismiss();
451
+ else snapTo(nearest - 1);
452
+ } else {
453
+ snapTo(Math.min(nearest + 1, snap_heights.length - 1));
454
+ }
455
+ return;
456
+ }
457
+
458
+ if (dismissible && offset < (snap_heights[0] ?? 0) * DISMISS_FACTOR) dismiss();
459
+ else snapTo(nearest);
460
+ }
461
+
462
+ function onBackdropPointerDown() {
463
+ dismiss();
464
+ }
465
+
466
+ function onKeyDown(e: KeyboardEvent) {
467
+ if (e.key === 'Escape' && dismissible) dismiss();
468
+ }
469
+
470
+ // Attach the press handlers manually rather than via Svelte's delegated
471
+ // `onpointerdown`. The sheet portals to <body>, which can sit outside a
472
+ // consuming app's delegation root and silently break drags otherwise.
473
+ $effect(() => {
474
+ const panel = panel_el;
475
+ const back = backdrop_el;
476
+ panel?.addEventListener('pointerdown', onPanelPointerDown);
477
+ back?.addEventListener('pointerdown', onBackdropPointerDown);
478
+ return () => {
479
+ panel?.removeEventListener('pointerdown', onPanelPointerDown);
480
+ back?.removeEventListener('pointerdown', onBackdropPointerDown);
481
+ };
482
+ });
483
+
484
+ // --- Visibility: keep mounted through the close animation ---
485
+ const visible = $derived(open || offset > 0);
486
+ </script>
487
+
488
+ <svelte:window onkeydown={open ? onKeyDown : undefined} />
489
+
490
+ {#if visible}
491
+ <div
492
+ use:portal={'body'}
493
+ {id}
494
+ class={['bottom-sheet', class_name].filter(Boolean).join(' ')}
495
+ class:dragging
496
+ style:--offset="{offset}px"
497
+ style:--max-offset="{max_offset || 0}px"
498
+ style:--morph-percent={morph}
499
+ style:--max-width="{max_width}px">
500
+ <!-- Backdrop: always catches taps to dismiss; `.frosted` adds the blur + tint. -->
501
+ <div
502
+ bind:this={backdrop_el}
503
+ class="backdrop"
504
+ class:frosted={backdrop}
505
+ style:opacity={backdrop ? backdrop_opacity : 0}
506
+ style:pointer-events={offset > 0 ? 'auto' : 'none'}>
507
+ </div>
508
+
509
+ <div bind:this={panel_el} class="panel" role="dialog" aria-modal="true">
510
+ {#if show_handle}
511
+ <div class="handle" aria-hidden="true">
512
+ <div class="bar"></div>
513
+ </div>
514
+ {/if}
515
+
516
+ {#if header}
517
+ <div class="header">
518
+ {@render header(morph)}
519
+ </div>
520
+ {/if}
521
+
522
+ <div
523
+ bind:this={content_el}
524
+ class="content"
525
+ style:touch-action={at_max ? 'pan-y' : 'pan-x'}
526
+ {@attach scrollbar()}>
527
+ {@render children?.(morph)}
528
+ </div>
529
+ </div>
530
+ </div>
531
+ {/if}
532
+
533
+ <style>
534
+ .bottom-sheet {
535
+ position: fixed;
536
+ inset: 0;
537
+ display: flex;
538
+ justify-content: center;
539
+ pointer-events: none;
540
+ z-index: var(--layer-drawer, 300);
541
+
542
+ &.dragging {
543
+ .backdrop {
544
+ transition: none;
545
+ }
546
+ .panel {
547
+ cursor: grabbing;
548
+ }
549
+ }
550
+ }
551
+
552
+ .backdrop {
553
+ position: absolute;
554
+ inset: 0;
555
+ z-index: 1;
556
+ transition: opacity 150ms ease;
557
+
558
+ &.frosted {
559
+ background-color: var(--bottom-sheet-backdrop, rgb(0 0 0 / 0.18));
560
+ @supports (backdrop-filter: blur(1px)) {
561
+ background-color: var(--bottom-sheet-backdrop, rgb(0 0 0 / 0.12));
562
+ backdrop-filter: blur(var(--bottom-sheet-blur, 12px));
563
+ }
564
+ }
565
+ }
566
+
567
+ .panel {
568
+ position: absolute;
569
+ top: 100%;
570
+ left: 50%;
571
+ z-index: 2;
572
+ width: 100%;
573
+ max-width: var(--max-width, 500px);
574
+ /* Reveal `--offset` pixels from the bottom; never higher than the panel top. */
575
+ transform: translate3d(-50%, clamp(-100%, calc(-1 * var(--offset)), 0px), 0);
576
+ height: max-content;
577
+ max-height: 100svh;
578
+ display: flex;
579
+ flex-direction: column;
580
+ background-color: var(--color-bg, light-dark(#fff, #0a0a0a));
581
+ /* Clamp so an over-rounded --radius-2xl can't blob this large sheet — see --radius-cap. */
582
+ --_radius: min(var(--radius-2xl, 28px), var(--radius-cap, 40px));
583
+ border-top-left-radius: var(--_radius);
584
+ border-top-right-radius: var(--_radius);
585
+ @supports (corner-shape: squircle) {
586
+ corner-shape: squircle;
587
+ border-top-left-radius: calc(var(--_radius) * var(--squircle-ratio, 2));
588
+ border-top-right-radius: calc(var(--_radius) * var(--squircle-ratio, 2));
589
+ }
590
+ box-shadow:
591
+ var(--shadow-xl, 0 -8px 30px rgb(0 0 0 / 0.18)),
592
+ 0 0 0 1px color-mix(in oklch, transparent, var(--color-text, #888) 12%);
593
+ pointer-events: auto;
594
+ cursor: grab;
595
+ touch-action: none;
596
+ &:not(:has(> .handle)) {
597
+ padding-top: 1.5rem;
598
+ }
599
+ }
600
+
601
+ .handle {
602
+ position: relative;
603
+ display: flex;
604
+ justify-content: center;
605
+ align-items: center;
606
+ height: 1.5rem;
607
+ flex-shrink: 0;
608
+ touch-action: none;
609
+
610
+ .bar {
611
+ width: 36px;
612
+ height: 4px;
613
+ border-radius: var(--radius-full, 9999px);
614
+ background-color: color-mix(in oklch, transparent, var(--color-text, #888) 28%);
615
+ }
616
+ }
617
+
618
+ .header {
619
+ flex-shrink: 0;
620
+ touch-action: none;
621
+ }
622
+
623
+ .content {
624
+ flex: 1;
625
+ min-height: 0;
626
+ overflow-y: auto;
627
+ overflow-x: hidden;
628
+ overscroll-behavior: contain;
629
+ padding-bottom: env(safe-area-inset-bottom);
630
+ }
631
+ @media (prefers-reduced-motion: reduce) {
632
+ .backdrop {
633
+ transition: none;
634
+ }
635
+ }
636
+ </style>
@@ -0,0 +1,27 @@
1
+ import { type Snippet } from 'svelte';
2
+ declare const BottomSheet: import("svelte").Component<{
3
+ open?: boolean;
4
+ snap_points?: number[];
5
+ default_snap?: undefined | number;
6
+ snap?: number;
7
+ morph_percent?: number;
8
+ morph_range?: undefined | [number, number];
9
+ dismissible?: boolean;
10
+ backdrop?: boolean;
11
+ blocking?: boolean;
12
+ max_width?: number;
13
+ id?: string;
14
+ class?: string;
15
+ children?: undefined | Snippet<[number]>;
16
+ header?: undefined | Snippet<[number]>;
17
+ onopen?: undefined | (() => void);
18
+ onclose?: undefined | (() => void);
19
+ onsnap?: undefined | ((detail: {
20
+ index: number;
21
+ height: number;
22
+ }) => void);
23
+ onmorph?: undefined | ((percent: number) => void);
24
+ }, {}, "open" | "snap" | "morph_percent">;
25
+ type BottomSheet = ReturnType<typeof BottomSheet>;
26
+ export default BottomSheet;
27
+ //# sourceMappingURL=BottomSheet.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BottomSheet.svelte.d.ts","sourceRoot":"","sources":["../../src/navigation/BottomSheet.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,OAAO,EAAiB,MAAM,QAAQ,CAAC;AA4gBrD,QAAA,MAAM,WAAW;WA3f+D,OAAO;kBAAgB,QAAW;mBAAiB,SAAS,GAAG,MAAM;WAAS,MAAM;oBAAkB,MAAM;kBAAgB,SAAS,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;kBAAgB,OAAO;eAAa,OAAO;eAAa,OAAO;gBAAc,MAAM;;YAA8B,MAAM;eAAa,SAAS,GAAG,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC;aAAW,SAAS,GAAG,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC;aAAW,SAAS,GAAG,CAAC,MAAM,IAAI,CAAC;cAAY,SAAS,GAAG,CAAC,MAAM,IAAI,CAAC;aAAa,SAAS,GACzgB,CAAC,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;cAAY,SAAS,GAAG,CAAC,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;yCA0fpD,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}