@surdeddd/bottom-sheet 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 +377 -0
- package/dist/BottomSheetEngine-CvSVbo6c.d.cts +704 -0
- package/dist/BottomSheetEngine-DbO8rW_3.d.ts +704 -0
- package/dist/element.cjs +3442 -0
- package/dist/element.d.cts +88 -0
- package/dist/element.d.ts +88 -0
- package/dist/element.iife.global.js +3 -0
- package/dist/element.js +3442 -0
- package/dist/index.cjs +3693 -0
- package/dist/index.d.cts +184 -0
- package/dist/index.d.ts +184 -0
- package/dist/index.js +3669 -0
- package/dist/integrations/formik.cjs +27 -0
- package/dist/integrations/formik.d.cts +27 -0
- package/dist/integrations/formik.d.ts +27 -0
- package/dist/integrations/formik.js +25 -0
- package/dist/integrations/react-hook-form.cjs +28 -0
- package/dist/integrations/react-hook-form.d.cts +30 -0
- package/dist/integrations/react-hook-form.d.ts +30 -0
- package/dist/integrations/react-hook-form.js +26 -0
- package/dist/overlay.cjs +1058 -0
- package/dist/overlay.d.cts +282 -0
- package/dist/overlay.d.ts +282 -0
- package/dist/overlay.js +1053 -0
- package/dist/preact.cjs +2 -0
- package/dist/preact.d.cts +5 -0
- package/dist/preact.d.ts +5 -0
- package/dist/preact.js +2 -0
- package/dist/qwik.cjs +3046 -0
- package/dist/qwik.d.cts +55 -0
- package/dist/qwik.d.ts +55 -0
- package/dist/qwik.js +3044 -0
- package/dist/react.cjs +4162 -0
- package/dist/react.d.cts +301 -0
- package/dist/react.d.ts +301 -0
- package/dist/react.js +4156 -0
- package/dist/solid.cjs +3063 -0
- package/dist/solid.d.cts +66 -0
- package/dist/solid.d.ts +66 -0
- package/dist/solid.js +3061 -0
- package/dist/styles.css +508 -0
- package/dist/svelte.cjs +3200 -0
- package/dist/svelte.d.ts +89 -0
- package/dist/svelte.js +3177 -0
- package/dist/themes/ios.css +24 -0
- package/dist/themes/material.css +24 -0
- package/dist/themes/vercel.css +24 -0
- package/dist/transform-Brkf-wzo.d.cts +4 -0
- package/dist/transform-Brkf-wzo.d.ts +4 -0
- package/dist/vue.cjs +3361 -0
- package/dist/vue.d.ts +68 -0
- package/dist/vue.js +3358 -0
- package/package.json +288 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Maxim Kravtsov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# @surdeddd/bottom-sheet
|
|
2
|
+
|
|
3
|
+
> Universal, headless bottom-sheet engine. **One core, eight adapters, GPU-accelerated, fully tested.**
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
A framework-agnostic bottom-sheet primitive. Spring physics, Pointer Events,
|
|
11
|
+
GPU-only motion, full keyboard a11y, hardware-back interception, multi-sheet
|
|
12
|
+
stacking — and the exact same engine behind every adapter.
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
┌───────────────────────────────────────┐
|
|
17
|
+
│ ENGINE (~9.15 KB gzip) │
|
|
18
|
+
│ spring · gestures · snap math │
|
|
19
|
+
│ focus trap · scroll lock · stack │
|
|
20
|
+
└────────────┬──────────────────────────┘
|
|
21
|
+
│
|
|
22
|
+
┌──────┬──────┬──────┬──────┬──────┬─────────────┬─────────┐
|
|
23
|
+
▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼
|
|
24
|
+
React Vue 3 Svelte Solid Lit Web Comp. Vanilla Angular
|
|
25
|
+
(recipe)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Why another bottom-sheet?
|
|
29
|
+
|
|
30
|
+
| Feature | vaul | react-modal-sheet | **this lib** |
|
|
31
|
+
| ------------------------------------ | :-----: | :---------------: | :-----------: |
|
|
32
|
+
| React adapter | ✓ | ✓ | ✓ |
|
|
33
|
+
| Vue / Svelte / Web Component | — | — | **✓ ✓ ✓** |
|
|
34
|
+
| Vanilla / no framework | — | — | **✓** |
|
|
35
|
+
| `bottom` mode | ✓ | ✓ | ✓ |
|
|
36
|
+
| `top` / `left` / `right` / overlay | — | — | **✓** |
|
|
37
|
+
| Spring physics with velocity carry | ✓ | partial | **✓** |
|
|
38
|
+
| Pointer-type-aware tuning | — | — | **✓** |
|
|
39
|
+
| Hardware-back interception (Android) | — | — | **✓** |
|
|
40
|
+
| `env(safe-area-inset-*)` | partial | — | **✓** |
|
|
41
|
+
| WCAG 2.1 AA keyboard slider | partial | — | **✓** |
|
|
42
|
+
| Multi-sheet stacking | — | — | **✓** |
|
|
43
|
+
| Sheet-manager registry (typed) | — | — | **✓** |
|
|
44
|
+
| Tests (unit + e2e) | partial | partial | **253 + 32** |
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm i @surdeddd/bottom-sheet
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
React / Vue / Svelte are listed as **optional peer deps** — install only
|
|
53
|
+
the framework you use, the bundle pays only for that adapter.
|
|
54
|
+
|
|
55
|
+
## Quick start
|
|
56
|
+
|
|
57
|
+
### React
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
import { BottomSheet } from '@surdeddd/bottom-sheet/react';
|
|
61
|
+
import '@surdeddd/bottom-sheet/styles';
|
|
62
|
+
|
|
63
|
+
<BottomSheet
|
|
64
|
+
snapPoints={[
|
|
65
|
+
{ id: 'minimized', size: 96 },
|
|
66
|
+
{ id: 'half', size: '45dvh' },
|
|
67
|
+
{ id: 'full', size: '85%' },
|
|
68
|
+
]}
|
|
69
|
+
initial='minimized'
|
|
70
|
+
animation='spring'
|
|
71
|
+
spring={{ stiffness: 260, damping: 28 }}
|
|
72
|
+
focusTrap
|
|
73
|
+
closeOnEscape
|
|
74
|
+
header={<h2>Search vehicles</h2>}
|
|
75
|
+
>
|
|
76
|
+
<YourList />
|
|
77
|
+
</BottomSheet>;
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
[Full React docs →](docs/react.md)
|
|
81
|
+
|
|
82
|
+
### Vue 3
|
|
83
|
+
|
|
84
|
+
```vue
|
|
85
|
+
<script setup>
|
|
86
|
+
import { BottomSheet } from '@surdeddd/bottom-sheet/vue';
|
|
87
|
+
import '@surdeddd/bottom-sheet/styles';
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
<template>
|
|
91
|
+
<BottomSheet
|
|
92
|
+
:snap-points="snaps"
|
|
93
|
+
initial="minimized"
|
|
94
|
+
focus-trap
|
|
95
|
+
@snap="onSnap"
|
|
96
|
+
>
|
|
97
|
+
<template #header><h2>Search</h2></template>
|
|
98
|
+
<YourList />
|
|
99
|
+
</BottomSheet>
|
|
100
|
+
</template>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
[Full Vue docs →](docs/vue.md)
|
|
104
|
+
|
|
105
|
+
### Svelte 5
|
|
106
|
+
|
|
107
|
+
```svelte
|
|
108
|
+
<script lang="ts">
|
|
109
|
+
import { createBottomSheet } from "@surdeddd/bottom-sheet/svelte";
|
|
110
|
+
import "@surdeddd/bottom-sheet/styles";
|
|
111
|
+
|
|
112
|
+
let sheetEl: HTMLElement | undefined = $state();
|
|
113
|
+
let handleEl: HTMLElement | undefined = $state();
|
|
114
|
+
let contentEl: HTMLElement | undefined = $state();
|
|
115
|
+
|
|
116
|
+
const ctrl = createBottomSheet({
|
|
117
|
+
snapPoints: [{ id: "min", size: 96 }, { id: "full", size: "85%" }],
|
|
118
|
+
initial: "min",
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
$effect(() => {
|
|
122
|
+
if (!sheetEl) return;
|
|
123
|
+
return ctrl.attach({ element: sheetEl, handle: handleEl, scrollContainer: contentEl });
|
|
124
|
+
});
|
|
125
|
+
</script>
|
|
126
|
+
|
|
127
|
+
<section class="bs-sheet" bind:this={sheetEl} data-mode="bottom">
|
|
128
|
+
<div class="bs-handle" bind:this={handleEl}><h2>Search</h2></div>
|
|
129
|
+
<div class="bs-content" bind:this={contentEl}><!-- list --></div>
|
|
130
|
+
</section>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
[Full Svelte docs →](docs/svelte.md)
|
|
134
|
+
|
|
135
|
+
### Web Component (any framework)
|
|
136
|
+
|
|
137
|
+
```html
|
|
138
|
+
<link rel="stylesheet" href="/styles.css" />
|
|
139
|
+
<script type="module" src="/element.js"></script>
|
|
140
|
+
|
|
141
|
+
<bottom-sheet
|
|
142
|
+
snap-points='[{"id":"min","size":96},{"id":"full","size":"85%"}]'
|
|
143
|
+
initial="min"
|
|
144
|
+
animation="spring"
|
|
145
|
+
focus-trap="true"
|
|
146
|
+
>
|
|
147
|
+
<h2 slot="header">Search</h2>
|
|
148
|
+
<ul>
|
|
149
|
+
...
|
|
150
|
+
</ul>
|
|
151
|
+
</bottom-sheet>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
[Full Web-Component docs →](docs/web-component.md)
|
|
155
|
+
|
|
156
|
+
### CDN — no bundler, no install
|
|
157
|
+
|
|
158
|
+
Drop-in usage from a static HTML page. The IIFE bundle registers
|
|
159
|
+
`<bottom-sheet>` as a side effect — no JS glue required.
|
|
160
|
+
|
|
161
|
+
```html
|
|
162
|
+
<!-- CDN, no bundler -->
|
|
163
|
+
<link
|
|
164
|
+
rel="stylesheet"
|
|
165
|
+
href="https://unpkg.com/@surdeddd/bottom-sheet/dist/styles.css"
|
|
166
|
+
/>
|
|
167
|
+
<script src="https://unpkg.com/@surdeddd/bottom-sheet/dist/element.iife.js"></script>
|
|
168
|
+
<bottom-sheet
|
|
169
|
+
snap-points='[{"id":"min","size":96},{"id":"full","size":"85%"}]'
|
|
170
|
+
initial="min"
|
|
171
|
+
>
|
|
172
|
+
<h2 slot="header">Title</h2>
|
|
173
|
+
<p>Content</p>
|
|
174
|
+
</bottom-sheet>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Vanilla core (no framework)
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
import { BottomSheetEngine } from '@surdeddd/bottom-sheet';
|
|
181
|
+
import '@surdeddd/bottom-sheet/styles';
|
|
182
|
+
|
|
183
|
+
const engine = new BottomSheetEngine({
|
|
184
|
+
element: document.querySelector('.bs-sheet'),
|
|
185
|
+
handle: document.querySelector('.bs-handle'),
|
|
186
|
+
scrollContainer: document.querySelector('.bs-content'),
|
|
187
|
+
backdrop: document.querySelector('.bs-backdrop'),
|
|
188
|
+
snapPoints: [
|
|
189
|
+
{ id: 'min', size: 96 },
|
|
190
|
+
{ id: 'full', size: '85%' },
|
|
191
|
+
],
|
|
192
|
+
animation: 'spring',
|
|
193
|
+
});
|
|
194
|
+
engine.snapTo('full');
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
[Full vanilla docs →](docs/vanilla.md)
|
|
198
|
+
|
|
199
|
+
## API at a glance
|
|
200
|
+
|
|
201
|
+
### Snap points
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
type SnapPoint =
|
|
205
|
+
| number // pixels
|
|
206
|
+
| `${number}%` // percent of viewport along the axis
|
|
207
|
+
| 'fit' // natural height of header
|
|
208
|
+
| 'full' // 100 % of axis
|
|
209
|
+
| string; // any CSS length — "50dvh", "clamp(200px, 60%, 800px)"
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Engine options
|
|
213
|
+
|
|
214
|
+
| Option | Default | What it does |
|
|
215
|
+
| ---------------- | ------------- | -------------------------------------------------- |
|
|
216
|
+
| `snapPoints` | required | Ordered list of `{ id, size }` |
|
|
217
|
+
| `allowed` | all ids | Subset the sheet may settle on right now |
|
|
218
|
+
| `initial` | first allowed | Snap id to start at |
|
|
219
|
+
| `mode` | `"bottom"` | `bottom \| top \| left \| right` |
|
|
220
|
+
| `animation` | `"spring"` | `spring \| tween` |
|
|
221
|
+
| `spring` | snappy | `{ stiffness, damping, mass }` |
|
|
222
|
+
| `flickVelocity` | `0.65 px/ms` | Mobile flick threshold |
|
|
223
|
+
| `dragThreshold` | `18 px` | Below this, drag snaps back |
|
|
224
|
+
| `rubberBand` | `true` | iOS-style soft over-drag |
|
|
225
|
+
| `backdropRange` | `[0, 1]` | Progress range over which backdrop fades |
|
|
226
|
+
| `focusTrap` | `false` | Trap Tab focus inside the sheet when open |
|
|
227
|
+
| `closeOnEscape` | `true` | Listen for Escape and close |
|
|
228
|
+
| `closeOnBack` | `false` | Intercept Android back button (history.popstate) |
|
|
229
|
+
| `lockBodyScroll` | `true` | iOS-safe body lock (`position: fixed`) |
|
|
230
|
+
| `inertSiblings` | `false` | Mark page siblings `inert` while open (full modal) |
|
|
231
|
+
|
|
232
|
+
### Events
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
engine.on('snap', ({ id, size }) => {}); // settled state change
|
|
236
|
+
engine.on('open', ({ id }) => {}); // 0 → >0
|
|
237
|
+
engine.on('close', () => {}); // → 0
|
|
238
|
+
engine.on('dragstart', ({ size }) => {});
|
|
239
|
+
engine.on('drag', ({ size, delta }) => {}); // ~60 fps
|
|
240
|
+
engine.on('dragend', ({ size, velocity }) => {});
|
|
241
|
+
engine.on('progress', ({ value, size }) => {}); // ~60 fps
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Imperative
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
engine.snapTo('half');
|
|
248
|
+
engine.open(); // → first non-closed allowed
|
|
249
|
+
engine.close(); // → "closed" or first allowed
|
|
250
|
+
engine.setAllowed(ids, snapId); // dynamic allowlist
|
|
251
|
+
engine.setSnapPoints(points); // re-measure
|
|
252
|
+
engine.destroy(); // remove listeners + cancel animation
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Scrim runtime API
|
|
256
|
+
|
|
257
|
+
The scrim is the dim layer behind/around the sheet (`screenComponent`). All
|
|
258
|
+
scrim properties can be mutated at runtime — no remount needed.
|
|
259
|
+
|
|
260
|
+
| Method | Purpose |
|
|
261
|
+
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
262
|
+
| `setScrimMode('full' \| 'above-sheet' \| 'off')` | Switch positioning at runtime. `'off'` fully disables the scrim layer. |
|
|
263
|
+
| `setScrimEnabled(boolean)` | Convenience switch — stashes/restores opacity ranges. |
|
|
264
|
+
| `setScrimTapToClose(boolean)` | Install/teardown click-to-close on the scrim. |
|
|
265
|
+
| `setScrimColor(color \| null)` / `setScrimBlur(blur \| null)` | Live color & backdrop-filter blur. `null` clears. |
|
|
266
|
+
| `setScrimInteractive(boolean)` | Toggle `pointer-events` on the scrim layer. |
|
|
267
|
+
| `setBackdropRange([s, e])` / `setScreenRange([s, e])` | Opacity-range mapping (progress 0..1 → opacity 0..1). |
|
|
268
|
+
| `setScrim({ ... })` | Batch — applies preset + individual fields in one pass with a single `applySize`. |
|
|
269
|
+
| `setScrimOverlay({ children, position, interactive })` | Inject a positioned floating element into the scrim area. Returns a teardown. Useful for monitoring-style "dim everything except this badge" UX. |
|
|
270
|
+
| `getScrimState()` | Read-only `{ mode, enabled }` snapshot. Stable public API for introspection without coupling to internals. |
|
|
271
|
+
|
|
272
|
+
**Presets**: `'subtle' | 'standard' | 'monitoring' | 'cinematic'` — use via `scrimPreset` constructor option or `setScrim({ preset })`.
|
|
273
|
+
|
|
274
|
+
### Overlay runtime API
|
|
275
|
+
|
|
276
|
+
The overlay engine (`@surdeddd/bottom-sheet/overlay`) is a standalone slide-up
|
|
277
|
+
panel that mirrors the scrim's runtime-mutability story for content.
|
|
278
|
+
|
|
279
|
+
| Method | Purpose |
|
|
280
|
+
| ------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
281
|
+
| `setOverlayChildren(node \| fragment \| factory)` | Replace overlay contents at runtime. Pass a `Node`, a `DocumentFragment`, or a `() => Node` factory. |
|
|
282
|
+
| `clearOverlayChildren()` | Remove any injected content. |
|
|
283
|
+
| `setOverlay({ children, ... })` | Batch update — `children` is one of the accepted fields alongside positioning/preset overrides. |
|
|
284
|
+
|
|
285
|
+
**Presets**: `OverlayPreset` is `'sheet' | 'dialog' | 'sidebar' | 'toast'` —
|
|
286
|
+
exposed as the `OVERLAY_PRESETS` const map and accepted by `setOverlay({ preset })`.
|
|
287
|
+
|
|
288
|
+
### Sheet manager (route-based registry)
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
import { createSheetManager } from "@surdeddd/bottom-sheet";
|
|
292
|
+
|
|
293
|
+
const sheets = createSheetManager<"home" | "marker" | "panorama">({
|
|
294
|
+
home: { snapPoints: [...], allowed: ["min", "half", "full"], onOpen: loadHome },
|
|
295
|
+
marker: { snapPoints: [...], allowed: ["half", "full"], onOpen: focusMarker, onClose: blur },
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// In your route watcher:
|
|
299
|
+
sheets.transition(prev, next);
|
|
300
|
+
const cfg = sheets.resolve(currentRoute); // typed!
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## Performance
|
|
304
|
+
|
|
305
|
+
Three deliberate choices keep things 60 fps even on mid-range Android:
|
|
306
|
+
|
|
307
|
+
1. **Transform-based positioning.** The sheet's `height` is set once to the
|
|
308
|
+
largest snap; size changes apply via `transform: translate3d(...)`. Stays
|
|
309
|
+
on the compositor — zero layout per frame.
|
|
310
|
+
2. **Spring sub-stepping with single DOM write.** Stiff springs integrate at
|
|
311
|
+
240 Hz internally for stability, but the engine writes to the DOM exactly
|
|
312
|
+
once per `requestAnimationFrame`.
|
|
313
|
+
3. **Settled-state-only React snapshot.** `useSyncExternalStore` refreshes
|
|
314
|
+
only on `snap` / `dragstart` / `dragend`. Drag pixels and animation frames
|
|
315
|
+
propagate via CSS variables (`--bs-progress`, `--bs-size`) and direct
|
|
316
|
+
imperative subscriptions — no React re-renders on every frame.
|
|
317
|
+
|
|
318
|
+
## Theming
|
|
319
|
+
|
|
320
|
+
All visuals are driven by CSS custom properties. Override anywhere:
|
|
321
|
+
|
|
322
|
+
```css
|
|
323
|
+
.bs-root {
|
|
324
|
+
--bs-surface: #18181b;
|
|
325
|
+
--bs-handle-color: #71717a;
|
|
326
|
+
--bs-radius: 28px;
|
|
327
|
+
--bs-shadow: 0 -8px 30px rgba(0, 0, 0, 0.6);
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
The engine writes `--bs-progress` (0..1) and `--bs-size` (px) to the sheet
|
|
332
|
+
element — drive any CSS animation from drag without touching JS:
|
|
333
|
+
|
|
334
|
+
```css
|
|
335
|
+
.fab {
|
|
336
|
+
transform: scale(calc(1 - var(--bs-progress) * 0.3));
|
|
337
|
+
}
|
|
338
|
+
.map-canvas {
|
|
339
|
+
filter: blur(calc(var(--bs-progress) * 8px));
|
|
340
|
+
}
|
|
341
|
+
.search-input {
|
|
342
|
+
opacity: calc(1 - var(--bs-progress));
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## Demo
|
|
347
|
+
|
|
348
|
+
```bash
|
|
349
|
+
npm run demo
|
|
350
|
+
# http://localhost:5173
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
The editorial demo showcases all five adapters, every gesture, every mode
|
|
354
|
+
(`bottom · top · left · right · overlay`), and live engine readouts (active
|
|
355
|
+
snap, progress, velocity, FPS). EN/RU and light/dark toggles in the corner.
|
|
356
|
+
|
|
357
|
+
## Testing
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
npm test # 253 unit tests via vitest + happy-dom (~6s)
|
|
361
|
+
npx playwright test # 32 e2e via Playwright on mobile-Chrome (~25s)
|
|
362
|
+
npm run typecheck # TypeScript --noEmit
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
CI runs all three across Node 18/20/22 on every PR and tag.
|
|
366
|
+
|
|
367
|
+
## Contributing
|
|
368
|
+
|
|
369
|
+
1. `npm install`
|
|
370
|
+
2. `npm run dev` — tsup watch mode
|
|
371
|
+
3. `npm run demo` — Vite dev server with live demo
|
|
372
|
+
4. `npm test` — vitest watch
|
|
373
|
+
5. Open a PR with a focused diff. The CI matrix gates merging.
|
|
374
|
+
|
|
375
|
+
## License
|
|
376
|
+
|
|
377
|
+
MIT — see [LICENSE](LICENSE).
|