@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.
- package/LICENSE +21 -0
- package/README.md +136 -0
- package/SKILL.md +149 -0
- package/bin/agents.js +63 -0
- package/dist/actions/Alert.svelte +202 -0
- package/dist/actions/Alert.svelte.d.ts +36 -0
- package/dist/actions/Alert.svelte.d.ts.map +1 -0
- package/dist/actions/Button.svelte +1450 -0
- package/dist/actions/Button.svelte.d.ts +56 -0
- package/dist/actions/Button.svelte.d.ts.map +1 -0
- package/dist/actions/ButtonGroup.svelte +111 -0
- package/dist/actions/ButtonGroup.svelte.d.ts +41 -0
- package/dist/actions/ButtonGroup.svelte.d.ts.map +1 -0
- package/dist/actions/CommandPalette.svelte +939 -0
- package/dist/actions/CommandPalette.svelte.d.ts +37 -0
- package/dist/actions/CommandPalette.svelte.d.ts.map +1 -0
- package/dist/actions/ContextMenu.svelte +138 -0
- package/dist/actions/ContextMenu.svelte.d.ts +54 -0
- package/dist/actions/ContextMenu.svelte.d.ts.map +1 -0
- package/dist/actions/Modal.svelte +474 -0
- package/dist/actions/Modal.svelte.d.ts +28 -0
- package/dist/actions/Modal.svelte.d.ts.map +1 -0
- package/dist/actions/Popover.svelte +1214 -0
- package/dist/actions/Popover.svelte.d.ts +31 -0
- package/dist/actions/Popover.svelte.d.ts.map +1 -0
- package/dist/actions/Portal.svelte +80 -0
- package/dist/actions/Portal.svelte.d.ts +17 -0
- package/dist/actions/Portal.svelte.d.ts.map +1 -0
- package/dist/actions/ThemeToggle.svelte +345 -0
- package/dist/actions/ThemeToggle.svelte.d.ts +15 -0
- package/dist/actions/ThemeToggle.svelte.d.ts.map +1 -0
- package/dist/actions/index.d.ts +13 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/actions/index.js +10 -0
- package/dist/actions/scrollbar.d.ts +48 -0
- package/dist/actions/scrollbar.d.ts.map +1 -0
- package/dist/actions/scrollbar.js +404 -0
- package/dist/display/Accordion.svelte +586 -0
- package/dist/display/Accordion.svelte.d.ts +41 -0
- package/dist/display/Accordion.svelte.d.ts.map +1 -0
- package/dist/display/Avatar.svelte +527 -0
- package/dist/display/Avatar.svelte.d.ts +22 -0
- package/dist/display/Avatar.svelte.d.ts.map +1 -0
- package/dist/display/AvatarGroup.svelte +298 -0
- package/dist/display/AvatarGroup.svelte.d.ts +31 -0
- package/dist/display/AvatarGroup.svelte.d.ts.map +1 -0
- package/dist/display/Calendar.svelte +1366 -0
- package/dist/display/Calendar.svelte.d.ts +58 -0
- package/dist/display/Calendar.svelte.d.ts.map +1 -0
- package/dist/display/Chart.svelte +1426 -0
- package/dist/display/Chart.svelte.d.ts +35 -0
- package/dist/display/Chart.svelte.d.ts.map +1 -0
- package/dist/display/Code.svelte +780 -0
- package/dist/display/Code.svelte.d.ts +19 -0
- package/dist/display/Code.svelte.d.ts.map +1 -0
- package/dist/display/Comparison.svelte +686 -0
- package/dist/display/Comparison.svelte.d.ts +22 -0
- package/dist/display/Comparison.svelte.d.ts.map +1 -0
- package/dist/display/Counter.svelte +285 -0
- package/dist/display/Counter.svelte.d.ts +21 -0
- package/dist/display/Counter.svelte.d.ts.map +1 -0
- package/dist/display/Expand.svelte +48 -0
- package/dist/display/Expand.svelte.d.ts +9 -0
- package/dist/display/Expand.svelte.d.ts.map +1 -0
- package/dist/display/List.svelte +294 -0
- package/dist/display/List.svelte.d.ts +40 -0
- package/dist/display/List.svelte.d.ts.map +1 -0
- package/dist/display/ListContextReset.svelte +19 -0
- package/dist/display/ListContextReset.svelte.d.ts +7 -0
- package/dist/display/ListContextReset.svelte.d.ts.map +1 -0
- package/dist/display/ListItem.svelte +834 -0
- package/dist/display/ListItem.svelte.d.ts +22 -0
- package/dist/display/ListItem.svelte.d.ts.map +1 -0
- package/dist/display/QR.svelte +1193 -0
- package/dist/display/QR.svelte.d.ts +23 -0
- package/dist/display/QR.svelte.d.ts.map +1 -0
- package/dist/display/SplitPane.svelte +744 -0
- package/dist/display/SplitPane.svelte.d.ts +25 -0
- package/dist/display/SplitPane.svelte.d.ts.map +1 -0
- package/dist/display/Stat.svelte +439 -0
- package/dist/display/Stat.svelte.d.ts +24 -0
- package/dist/display/Stat.svelte.d.ts.map +1 -0
- package/dist/display/Table.svelte +4654 -0
- package/dist/display/Table.svelte.d.ts +249 -0
- package/dist/display/Table.svelte.d.ts.map +1 -0
- package/dist/display/TableCellEditor.svelte +935 -0
- package/dist/display/TableCellEditor.svelte.d.ts +58 -0
- package/dist/display/TableCellEditor.svelte.d.ts.map +1 -0
- package/dist/display/Timeline.svelte +1258 -0
- package/dist/display/Timeline.svelte.d.ts +43 -0
- package/dist/display/Timeline.svelte.d.ts.map +1 -0
- package/dist/display/Tree.svelte +1740 -0
- package/dist/display/Tree.svelte.d.ts +74 -0
- package/dist/display/Tree.svelte.d.ts.map +1 -0
- package/dist/display/Typewriter.svelte +338 -0
- package/dist/display/Typewriter.svelte.d.ts +22 -0
- package/dist/display/Typewriter.svelte.d.ts.map +1 -0
- package/dist/display/index.d.ts +24 -0
- package/dist/display/index.d.ts.map +1 -0
- package/dist/display/index.js +18 -0
- package/dist/feedback/Callout.svelte +529 -0
- package/dist/feedback/Callout.svelte.d.ts +24 -0
- package/dist/feedback/Callout.svelte.d.ts.map +1 -0
- package/dist/feedback/Confetti.svelte +631 -0
- package/dist/feedback/Confetti.svelte.d.ts +90 -0
- package/dist/feedback/Confetti.svelte.d.ts.map +1 -0
- package/dist/feedback/Progress.svelte +382 -0
- package/dist/feedback/Progress.svelte.d.ts +25 -0
- package/dist/feedback/Progress.svelte.d.ts.map +1 -0
- package/dist/feedback/Toast.svelte +967 -0
- package/dist/feedback/Toast.svelte.d.ts +54 -0
- package/dist/feedback/Toast.svelte.d.ts.map +1 -0
- package/dist/feedback/index.d.ts +7 -0
- package/dist/feedback/index.d.ts.map +1 -0
- package/dist/feedback/index.js +4 -0
- package/dist/form/Checkbox.svelte +449 -0
- package/dist/form/Checkbox.svelte.d.ts +27 -0
- package/dist/form/Checkbox.svelte.d.ts.map +1 -0
- package/dist/form/Fieldset.svelte +410 -0
- package/dist/form/Fieldset.svelte.d.ts +22 -0
- package/dist/form/Fieldset.svelte.d.ts.map +1 -0
- package/dist/form/FileUpload.svelte +934 -0
- package/dist/form/FileUpload.svelte.d.ts +41 -0
- package/dist/form/FileUpload.svelte.d.ts.map +1 -0
- package/dist/form/Form.svelte +530 -0
- package/dist/form/Form.svelte.d.ts +120 -0
- package/dist/form/Form.svelte.d.ts.map +1 -0
- package/dist/form/Input.svelte +2858 -0
- package/dist/form/Input.svelte.d.ts +66 -0
- package/dist/form/Input.svelte.d.ts.map +1 -0
- package/dist/form/Radio.svelte +507 -0
- package/dist/form/Radio.svelte.d.ts +39 -0
- package/dist/form/Radio.svelte.d.ts.map +1 -0
- package/dist/form/Range.svelte +912 -0
- package/dist/form/Range.svelte.d.ts +33 -0
- package/dist/form/Range.svelte.d.ts.map +1 -0
- package/dist/form/Rating.svelte +429 -0
- package/dist/form/Rating.svelte.d.ts +28 -0
- package/dist/form/Rating.svelte.d.ts.map +1 -0
- package/dist/form/Select.svelte +1933 -0
- package/dist/form/Select.svelte.d.ts +54 -0
- package/dist/form/Select.svelte.d.ts.map +1 -0
- package/dist/form/Toggle.svelte +645 -0
- package/dist/form/Toggle.svelte.d.ts +50 -0
- package/dist/form/Toggle.svelte.d.ts.map +1 -0
- package/dist/form/index.d.ts +15 -0
- package/dist/form/index.d.ts.map +1 -0
- package/dist/form/index.js +10 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/layout/README.md +172 -0
- package/dist/media/Carousel.svelte +2424 -0
- package/dist/media/Carousel.svelte.d.ts +47 -0
- package/dist/media/Carousel.svelte.d.ts.map +1 -0
- package/dist/media/Gallery.svelte +2881 -0
- package/dist/media/Gallery.svelte.d.ts +82 -0
- package/dist/media/Gallery.svelte.d.ts.map +1 -0
- package/dist/media/Image.svelte +389 -0
- package/dist/media/Image.svelte.d.ts +33 -0
- package/dist/media/Image.svelte.d.ts.map +1 -0
- package/dist/media/PDF.svelte +1793 -0
- package/dist/media/PDF.svelte.d.ts +44 -0
- package/dist/media/PDF.svelte.d.ts.map +1 -0
- package/dist/media/Panorama.svelte +1391 -0
- package/dist/media/Panorama.svelte.d.ts +47 -0
- package/dist/media/Panorama.svelte.d.ts.map +1 -0
- package/dist/media/Video.svelte +2501 -0
- package/dist/media/Video.svelte.d.ts +58 -0
- package/dist/media/Video.svelte.d.ts.map +1 -0
- package/dist/media/carousel.d.ts +211 -0
- package/dist/media/carousel.d.ts.map +1 -0
- package/dist/media/carousel.js +408 -0
- package/dist/media/index.d.ts +11 -0
- package/dist/media/index.d.ts.map +1 -0
- package/dist/media/index.js +5 -0
- package/dist/navigation/BottomSheet.svelte +636 -0
- package/dist/navigation/BottomSheet.svelte.d.ts +27 -0
- package/dist/navigation/BottomSheet.svelte.d.ts.map +1 -0
- package/dist/navigation/Breadcrumbs.svelte +611 -0
- package/dist/navigation/Breadcrumbs.svelte.d.ts +28 -0
- package/dist/navigation/Breadcrumbs.svelte.d.ts.map +1 -0
- package/dist/navigation/Pagination.svelte +641 -0
- package/dist/navigation/Pagination.svelte.d.ts +27 -0
- package/dist/navigation/Pagination.svelte.d.ts.map +1 -0
- package/dist/navigation/Steps.svelte +965 -0
- package/dist/navigation/Steps.svelte.d.ts +43 -0
- package/dist/navigation/Steps.svelte.d.ts.map +1 -0
- package/dist/navigation/Tabs.svelte +698 -0
- package/dist/navigation/Tabs.svelte.d.ts +41 -0
- package/dist/navigation/Tabs.svelte.d.ts.map +1 -0
- package/dist/navigation/index.d.ts +8 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/index.js +5 -0
- 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"}
|