@surdeddd/bottom-sheet 0.23.4 → 0.24.1

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 (80) hide show
  1. package/README.md +513 -527
  2. package/README.ru.md +533 -545
  3. package/dist/{BottomSheetCore-C5tJYVMY.d.cts → BottomSheetCore-B_r4KCQL.d.cts} +48 -13
  4. package/dist/{BottomSheetCore-DxSrC4QC.d.ts → BottomSheetCore-B_r4KCQL.d.ts} +48 -13
  5. package/dist/{BottomSheetEngine-B3F9qukX.d.cts → BottomSheetEngine-0F7mjyvF.d.cts} +1 -1
  6. package/dist/{BottomSheetEngine-BchzgqGG.d.ts → BottomSheetEngine-CJV_DDrM.d.ts} +1 -1
  7. package/dist/core.cjs +810 -436
  8. package/dist/core.d.cts +2 -3
  9. package/dist/core.d.ts +2 -3
  10. package/dist/core.js +810 -436
  11. package/dist/debug.d.cts +0 -1
  12. package/dist/debug.d.ts +0 -1
  13. package/dist/{default-features-DwVTzDwP.d.ts → default-features-4Xwv6aVt.d.ts} +4 -2
  14. package/dist/{default-features-BGiCSmV1.d.cts → default-features-DX2NOxP6.d.cts} +4 -2
  15. package/dist/element.cjs +1421 -697
  16. package/dist/element.d.cts +11 -3
  17. package/dist/element.d.ts +11 -3
  18. package/dist/element.iife.global.js +2 -2
  19. package/dist/element.js +1421 -697
  20. package/dist/features.cjs +334 -190
  21. package/dist/features.d.cts +3 -4
  22. package/dist/features.d.ts +3 -4
  23. package/dist/features.js +334 -191
  24. package/dist/{index-C7TSQkVK.d.cts → index-2shlxhcG.d.cts} +1 -1
  25. package/dist/{index-CdM1tke5.d.ts → index-CNCRQnpc.d.ts} +1 -1
  26. package/dist/index.cjs +1129 -631
  27. package/dist/index.d.cts +11 -6
  28. package/dist/index.d.ts +11 -6
  29. package/dist/index.js +1129 -632
  30. package/dist/integrations/formik.cjs +21 -10
  31. package/dist/integrations/formik.d.cts +0 -1
  32. package/dist/integrations/formik.d.ts +0 -1
  33. package/dist/integrations/formik.js +22 -11
  34. package/dist/integrations/react-hook-form.cjs +21 -12
  35. package/dist/integrations/react-hook-form.d.cts +0 -1
  36. package/dist/integrations/react-hook-form.d.ts +0 -1
  37. package/dist/integrations/react-hook-form.js +21 -12
  38. package/dist/option-keys-DItuAjUw.d.cts +6 -0
  39. package/dist/option-keys-a5DYNwwO.d.ts +6 -0
  40. package/dist/overlay.cjs +386 -189
  41. package/dist/overlay.d.cts +1 -3
  42. package/dist/overlay.d.ts +1 -3
  43. package/dist/overlay.js +386 -189
  44. package/dist/preact.d.cts +3 -4
  45. package/dist/preact.d.ts +3 -4
  46. package/dist/qwik-lib/index.qwik.cjs +1306 -671
  47. package/dist/qwik-lib/index.qwik.mjs +1306 -671
  48. package/dist/qwik.cjs +1326 -701
  49. package/dist/qwik.d.cts +12 -30
  50. package/dist/qwik.d.ts +12 -30
  51. package/dist/qwik.js +1326 -701
  52. package/dist/react.cjs +1373 -750
  53. package/dist/react.d.cts +26 -12
  54. package/dist/react.d.ts +26 -12
  55. package/dist/react.js +1375 -752
  56. package/dist/solid.cjs +1332 -727
  57. package/dist/solid.d.cts +13 -438
  58. package/dist/solid.d.ts +13 -438
  59. package/dist/solid.js +1334 -729
  60. package/dist/solid.source.jsx +1374 -751
  61. package/dist/styles.css +20 -6
  62. package/dist/svelte-core.cjs +1209 -676
  63. package/dist/svelte-core.js +1207 -677
  64. package/dist/svelte-src/BottomSheet.svelte +101 -58
  65. package/dist/svelte.cjs +1323 -705
  66. package/dist/svelte.d.cts +45 -25
  67. package/dist/svelte.d.ts +45 -25
  68. package/dist/svelte.js +1323 -705
  69. package/dist/vue.cjs +1354 -773
  70. package/dist/vue.d.cts +20 -3
  71. package/dist/vue.d.ts +20 -3
  72. package/dist/vue.js +1354 -773
  73. package/dist/webgl.cjs +159 -53
  74. package/dist/webgl.d.cts +2 -2
  75. package/dist/webgl.d.ts +2 -2
  76. package/dist/webgl.js +159 -53
  77. package/llms.txt +330 -0
  78. package/package.json +42 -20
  79. package/dist/transform-jVfAikE9.d.cts +0 -3
  80. package/dist/transform-jVfAikE9.d.ts +0 -3
package/README.ru.md CHANGED
@@ -1,60 +1,54 @@
1
1
  # @surdeddd/bottom-sheet
2
2
 
3
- English: [README.md](./README.md)
4
-
5
- > Универсальный headless-движок bottom sheet. **Одно ядро, восемь адаптеров, GPU-ускорение, полное покрытие тестами.**
6
-
7
- ![bundle](https://img.shields.io/badge/core-%E2%89%A424%20KB%20gzip%20budget-1a1614?style=flat-square)
8
- ![slim](https://img.shields.io/badge/slim%20core-%E2%89%A419.5%20KB%20gzip-1a1614?style=flat-square)
9
- ![tests](https://img.shields.io/badge/tests-500%2B%20unit-2ea44f?style=flat-square)
10
- ![ts](https://img.shields.io/badge/TypeScript-strict-3178c6?style=flat-square&logo=typescript&logoColor=white)
3
+ Нижняя шторка (bottom sheet), боковая панель и модальный лист для веба. Один
4
+ движок жестов с точками привязки, пружинной физикой и управлением с
5
+ клавиатуры, и тот же движок под адаптерами для **React, Vue 3, Svelte 5,
6
+ Solid, Qwik, Preact** и под **веб-компонентом** `<bottom-sheet>`, который
7
+ работает в Angular, Lit, Astro и в обычном HTML.
8
+
9
+ [![npm](https://img.shields.io/npm/v/@surdeddd/bottom-sheet?style=flat-square&color=1a1614)](https://www.npmjs.com/package/@surdeddd/bottom-sheet)
10
+ [![downloads](https://img.shields.io/npm/dm/@surdeddd/bottom-sheet?style=flat-square&color=1a1614)](https://www.npmjs.com/package/@surdeddd/bottom-sheet)
11
+ ![core size](https://img.shields.io/badge/core-25.6%20KB%20gzip-1a1614?style=flat-square)
12
+ ![tests](https://img.shields.io/badge/tests-1000%2B%20unit%20%C2%B7%20370%20e2e%20per%20engine-2ea44f?style=flat-square)
13
+ ![types](https://img.shields.io/badge/TypeScript-strict-3178c6?style=flat-square&logo=typescript&logoColor=white)
11
14
  ![license](https://img.shields.io/badge/license-MIT-dc3522?style=flat-square)
12
15
 
13
- Фреймворк-независимый примитив bottom sheet. Spring-физика, Pointer Events,
14
- анимации только на GPU, полноценная клавиатурная a11y, перехват аппаратной
15
- кнопки «Назад», стек из нескольких sheet — и один и тот же движок под каждым
16
- адаптером.
17
-
18
- ## 🚀 [Живое демо → bottom-sheet-demo-ashy.vercel.app](https://bottom-sheet-demo-ashy.vercel.app)
19
-
20
- 📚 [Справочник API (typedoc) → surdeddd.github.io/BottomSheet](https://surdeddd.github.io/BottomSheet/)
16
+ **[Живое демо](https://bottom-sheet-demo-ashy.vercel.app)** ·
17
+ **[Справочник API](https://surdeddd.github.io/BottomSheet/)** ·
18
+ [История изменений](CHANGELOG.md) ·
19
+ [English](https://github.com/Surdeddd/BottomSheet/blob/main/README.md)
21
20
 
22
21
  <p align="center">
23
22
  <picture>
24
23
  <source media="(prefers-color-scheme: dark)" srcset="docs/screenshots/sheet-dark.png" />
25
- <img src="docs/screenshots/sheet-light.png" alt="bottom sheet на средней snap point поверх затемнённой страницы" width="420" />
24
+ <img src="docs/screenshots/sheet-light.png" alt="Шторка на средней точке привязки поверх затемнённой страницы" width="380" />
26
25
  </picture>
26
+ <img src="https://raw.githubusercontent.com/Surdeddd/BottomSheet/main/docs/gifs/01-drag-to-close.gif" alt="Шторку перетаскивают между точками привязки и закрывают броском" width="300" />
27
27
  </p>
28
28
 
29
- <p align="center">
30
- <img src="https://raw.githubusercontent.com/Surdeddd/BottomSheet/main/docs/gifs/01-drag-to-close.gif" alt="перетаскивание для закрытия — spring-физика" width="380" />
31
- </p>
32
-
33
- <p align="center">
34
- <img src="docs/assets/architecture.svg" alt="Одно ядро движка питает восемь адаптеров — React, Preact, Vue 3, Svelte, Solid, Qwik, Web Component и Vanilla — плюс отдельный subpath /overlay" width="720" />
35
- </p>
36
-
37
- ## Возможности
38
-
39
- Всё перечисленное ниже можно проверить в `src/` этого репозитория:
40
-
41
- - **8 целей сборки** — React, Preact, Vue 3, Svelte 5, Solid, Qwik, Web
42
- Component (`<bottom-sheet>`, совместимый с Lit) и vanilla-ядро, плюс
43
- subpath `/overlay` (отдельная выезжающая панель).
44
- - **4 оси** — `bottom`, `top`, `left`, `right` (через `mode`).
45
- - **Spring-физика с переносом скорости**, плюс настройка под тип указателя
46
- (отдельные пороги drag для touch / mouse / pen).
47
- - **Перехват аппаратной кнопки «Назад»** на Android — включается опцией
48
- `closeOnBack`.
49
- - **Отступы с учётом `env(safe-area-inset-*)`**, чтобы ряды кнопок не
50
- попадали под индикатор «Домой».
51
- - **Клавиатурный слайдер по WCAG 2.1 AA** — шаги Arrow/Home/End/Esc, focus
52
- trap с возвратом фокуса.
53
- - **Стек из нескольких sheet** и типизированный реестр sheet manager.
54
- - **Отменяемые `before-snap` / `before-close`** — переход можно
55
- синхронно заблокировать.
56
- - Под капотом 500+ unit-тестов и ~200 e2e-прогонов Playwright на трёх
57
- браузерных движках (`mobile-chrome` / `mobile-safari` / `firefox`).
29
+ ## Чем она отличается
30
+
31
+ - **Один движок на все фреймворки.** Компонент в React ведёт себя так же,
32
+ как в Vue, Svelte, Solid, Qwik или в веб-компоненте: это тонкие адаптеры
33
+ над одним ядром.
34
+ - **Точки привязки в любых единицах.** Пиксели, проценты, `fit` (высота
35
+ самого содержимого) или любая длина CSS: `50dvh`,
36
+ `clamp(200px, 60%, 800px)`.
37
+ - **Четыре края.** `bottom`, `top`, `left`, `right`, а также `start` и `end`,
38
+ которые следуют за направлением письма.
39
+ - **Ощущается как нативная.** Пружина, которая подхватывает скорость пальца,
40
+ резиновое сопротивление за крайней точкой, отдельные пороги для пальца и
41
+ мыши, движение только на композиторе.
42
+ - **Доступность.** Ручка работает как слайдер с клавиатуры, фокус можно
43
+ запереть в шторке и вернуть обратно, всё за модальной шторкой получает
44
+ `inert`, учитываются reduced motion и forced colors.
45
+ - **Знает про телефон.** Встаёт на экранную клавиатуру, учитывает безопасные
46
+ зоны, закрывается кнопкой «Назад», блокирует прокрутку страницы способом,
47
+ который работает в iOS.
48
+ - **Стек.** Несколько шторок открыты одновременно, у каждой свой бэкдроп и
49
+ своя область фокуса.
50
+ - **Небольшой размер.** 25.6 KB gzip у полного движка, 21.9 KB у
51
+ облегчённого ядра, зависимостей в рантайме нет.
58
52
 
59
53
  ## Установка
60
54
 
@@ -62,636 +56,630 @@ English: [README.md](./README.md)
62
56
  npm i @surdeddd/bottom-sheet
63
57
  ```
64
58
 
65
- React / Vue / Svelte указаны как **опциональные peer deps** — ставьте только
66
- тот фреймворк, который используете: в бандл попадает только его адаптер.
59
+ Фреймворки объявлены необязательными peer-зависимостями: ставится только
60
+ тот, которым вы пользуетесь.
67
61
 
68
62
  ## Быстрый старт
69
63
 
70
- ### React
64
+ ### Модальная шторка по кнопке
71
65
 
72
66
  ```tsx
67
+ import { useState } from 'react';
73
68
  import { BottomSheet } from '@surdeddd/bottom-sheet/react';
74
69
  import '@surdeddd/bottom-sheet/styles';
75
70
 
71
+ export function Filters() {
72
+ const [open, setOpen] = useState(false);
73
+
74
+ return (
75
+ <>
76
+ <button onClick={() => setOpen(true)}>Filters</button>
77
+
78
+ <BottomSheet
79
+ open={open}
80
+ onOpenChange={setOpen}
81
+ snapPoints={[
82
+ { id: 'closed', size: 0 },
83
+ { id: 'open', size: 'fit' },
84
+ ]}
85
+ focusTrap
86
+ header={<h2>Filters</h2>}
87
+ >
88
+ <FilterForm />
89
+ </BottomSheet>
90
+ </>
91
+ );
92
+ }
93
+ ```
94
+
95
+ Пропсу `open` нужна точка привязки размером `0`: это и есть закрытое
96
+ состояние. Тап по бэкдропу, Escape и перетаскивание вниз закрывают шторку и
97
+ сообщают об этом через `onOpenChange`.
98
+
99
+ ### Шторка, которая всегда на экране
100
+
101
+ Как в картах: полоса снизу, которую тянут вверх.
102
+
103
+ ```tsx
76
104
  <BottomSheet
77
105
  snapPoints={[
78
- { id: 'minimized', size: 96 },
106
+ { id: 'peek', size: 96 },
79
107
  { id: 'half', size: '45dvh' },
80
- { id: 'full', size: '85%' },
108
+ { id: 'full', size: '90%' },
81
109
  ]}
82
- initial='minimized'
83
- animation='spring'
84
- spring={{ stiffness: 260, damping: 28 }}
85
- focusTrap
86
- closeOnEscape
87
- header={<h2>Search vehicles</h2>}
110
+ initial="peek"
111
+ header={<h2>Nearby</h2>}
88
112
  >
89
- <YourList />
90
- </BottomSheet>;
113
+ <PlaceList />
114
+ </BottomSheet>
91
115
  ```
92
116
 
93
- [Полная документация по React →](docs/react.md)
117
+ ### Другие фреймворки
94
118
 
95
- ### Vue 3
119
+ <details>
120
+ <summary><b>Vue 3</b></summary>
96
121
 
97
122
  ```vue
98
- <script setup>
99
- import { BottomSheet } from '@surdeddd/bottom-sheet/vue';
100
- import '@surdeddd/bottom-sheet/styles';
123
+ <script setup lang="ts">
124
+ import { ref } from 'vue';
125
+ import { BottomSheet } from '@surdeddd/bottom-sheet/vue';
126
+ import '@surdeddd/bottom-sheet/styles';
127
+
128
+ const open = ref(false);
129
+ const points = [
130
+ { id: 'closed', size: 0 },
131
+ { id: 'open', size: 'fit' },
132
+ ];
101
133
  </script>
102
134
 
103
135
  <template>
104
- <BottomSheet
105
- :snap-points="snaps"
106
- initial="minimized"
107
- focus-trap
108
- @snap="onSnap"
109
- >
110
- <template #header><h2>Search</h2></template>
111
- <YourList />
136
+ <button @click="open = true">Filters</button>
137
+ <BottomSheet v-model:open="open" :snap-points="points" focus-trap>
138
+ <template #header><h2>Filters</h2></template>
139
+ <FilterForm />
112
140
  </BottomSheet>
113
141
  </template>
114
142
  ```
115
143
 
116
- [Полная документация по Vue →](docs/vue.md)
144
+ [Гайд по Vue →](docs/vue.md)
117
145
 
118
- ### Svelte 5
146
+ </details>
147
+
148
+ <details>
149
+ <summary><b>Svelte 5</b></summary>
119
150
 
120
151
  ```svelte
121
152
  <script lang="ts">
122
- import { createBottomSheet } from "@surdeddd/bottom-sheet/svelte";
123
- import "@surdeddd/bottom-sheet/styles";
124
-
125
- let sheetEl: HTMLElement | undefined = $state();
126
- let handleEl: HTMLElement | undefined = $state();
127
- let contentEl: HTMLElement | undefined = $state();
128
-
129
- const ctrl = createBottomSheet({
130
- snapPoints: [{ id: "min", size: 96 }, { id: "full", size: "85%" }],
131
- initial: "min",
132
- });
133
-
134
- $effect(() => {
135
- if (!sheetEl) return;
136
- return ctrl.attach({ element: sheetEl, handle: handleEl, scrollContainer: contentEl });
137
- });
138
- </script>
139
-
140
- <section class="bs-sheet" bind:this={sheetEl} data-mode="bottom">
141
- <div class="bs-handle" bind:this={handleEl}><h2>Search</h2></div>
142
- <div class="bs-content" bind:this={contentEl}><!-- list --></div>
143
- </section>
144
- ```
153
+ import { BottomSheet } from '@surdeddd/bottom-sheet/svelte';
154
+ import '@surdeddd/bottom-sheet/styles';
145
155
 
146
- [Полная документация по Svelte →](docs/svelte.md)
156
+ let open = $state(false);
157
+ </script>
147
158
 
148
- ### Web Component (любой фреймворк)
159
+ <button onclick={() => (open = true)}>Filters</button>
149
160
 
150
- ```html
151
- <link rel="stylesheet" href="/styles.css" />
152
- <script type="module" src="/element.js"></script>
153
-
154
- <bottom-sheet
155
- snap-points='[{"id":"min","size":96},{"id":"full","size":"85%"}]'
156
- initial="min"
157
- animation="spring"
158
- focus-trap="true"
161
+ <BottomSheet
162
+ bind:open
163
+ snapPoints={[{ id: 'closed', size: 0 }, { id: 'open', size: 'fit' }]}
164
+ focusTrap
159
165
  >
160
- <h2 slot="header">Search</h2>
161
- <ul>
162
- ...
163
- </ul>
164
- </bottom-sheet>
166
+ {#snippet header()}<h2>Filters</h2>{/snippet}
167
+ {#snippet children()}<FilterForm />{/snippet}
168
+ </BottomSheet>
165
169
  ```
166
170
 
167
- [Полная документация по Web Component →](docs/web-component.md)
171
+ [Гайд по Svelte →](docs/svelte.md)
168
172
 
169
- ### CDN — без сборщика и установки
173
+ </details>
170
174
 
171
- Готовое подключение из статической HTML-страницы. IIFE-бандл регистрирует
172
- `<bottom-sheet>` как побочный эффект — JS-обвязка не нужна.
175
+ <details>
176
+ <summary><b>Solid</b></summary>
173
177
 
174
- ```html
175
- <!-- CDN, no bundler -->
176
- <link
177
- rel="stylesheet"
178
- href="https://unpkg.com/@surdeddd/bottom-sheet/dist/styles.css"
179
- />
180
- <script src="https://unpkg.com/@surdeddd/bottom-sheet/dist/element.iife.global.js"></script>
181
- <bottom-sheet
182
- snap-points='[{"id":"min","size":96},{"id":"full","size":"85%"}]'
183
- initial="min"
184
- >
185
- <h2 slot="header">Title</h2>
186
- <p>Content</p>
187
- </bottom-sheet>
188
- ```
189
-
190
- ### Vanilla-ядро (без фреймворка)
191
-
192
- ```ts
193
- import { BottomSheetEngine } from '@surdeddd/bottom-sheet';
178
+ ```tsx
179
+ import { createSignal } from 'solid-js';
180
+ import { BottomSheet } from '@surdeddd/bottom-sheet/solid';
194
181
  import '@surdeddd/bottom-sheet/styles';
195
182
 
196
- const engine = new BottomSheetEngine({
197
- element: document.querySelector('.bs-sheet'),
198
- handle: document.querySelector('.bs-handle'),
199
- scrollContainer: document.querySelector('.bs-content'),
200
- backdrop: document.querySelector('.bs-backdrop'),
201
- snapPoints: [
202
- { id: 'min', size: 96 },
203
- { id: 'full', size: '85%' },
204
- ],
205
- animation: 'spring',
206
- });
207
- engine.snapTo('full');
183
+ export function Filters() {
184
+ const [open, setOpen] = createSignal(false);
185
+ return (
186
+ <>
187
+ <button onClick={() => setOpen(true)}>Filters</button>
188
+ <BottomSheet
189
+ open={open()}
190
+ onOpenChange={setOpen}
191
+ snapPoints={[{ id: 'closed', size: 0 }, { id: 'open', size: 'fit' }]}
192
+ focusTrap
193
+ header={<h2>Filters</h2>}
194
+ >
195
+ <FilterForm />
196
+ </BottomSheet>
197
+ </>
198
+ );
199
+ }
208
200
  ```
209
201
 
210
- [Полная документация по vanilla →](docs/vanilla.md)
211
-
212
- ## API кратко
213
-
214
- ### Snap points
215
-
216
- ```ts
217
- type SnapPoint =
218
- | number // pixels
219
- | `${number}%` // percent of viewport along the axis
220
- | 'fit' // natural height of the whole sheet (handle + header + content + footer)
221
- | 'content' // alias of 'fit'
222
- | 'full' // 100 % of axis
223
- | string; // any CSS length — "50dvh", "clamp(200px, 60%, 800px)"
224
- ```
202
+ [Гайд по Solid →](docs/solid.md)
225
203
 
226
- ### Опции движка
227
-
228
- | Опция | По умолчанию | Что делает |
229
- | -------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------- |
230
- | `snapPoints` | обязательная | Упорядоченный список `{ id, size }` |
231
- | `allowed` | все id | Подмножество snap point, на которых sheet может остановиться сейчас |
232
- | `initial` | первая из allowed | Id snap point, с которой стартовать |
233
- | `mode` | `"bottom"` | `bottom`, `top`, `left`, `right` или логические `start` / `end`; в адаптерах React и Vue реактивна |
234
- | `animation` | `"spring"` | `spring`, `tween`, `ios-spring`, `material-bounce`, `linear`, `snappy` |
235
- | `settleAnimation` | выкл. | `"waapi"` — финальная доводка идёт как Web Animation на compositor (заранее насэмплированные spring-кейфреймы); при необходимости автоматически откатывается на rAF |
236
- | `features` | все по умолчанию | `EngineFeature[]` — переопределить/расширить набор composable-фич (см. «Slim core» ниже) |
237
- | `spring` | snappy | `{ stiffness, damping, mass }` |
238
- | `flickVelocity` | `0.65 px/ms` | Порог флика на мобильных |
239
- | `dragThreshold` | `18 px` | Если drag короче, sheet возвращается назад |
240
- | `rubberBand` | `true` | Мягкое «резиновое» перетягивание в стиле iOS |
241
- | `backdropRange` | `[0, 1]` | Диапазон progress, в котором backdrop проявляется |
242
- | `screenRange` | `[0, 1]` | Диапазон progress, в котором проявляется контент `screen`/scrim |
243
- | `focusTrap` | `false` | Удерживать Tab-фокус внутри открытого sheet |
244
- | `initialFocus` | первое поле | Элемент / селектор, получающий фокус при открытии; `false` фокусирует контейнер sheet (`tabindex=-1`) — так не открывается клавиатура iOS |
245
- | `closeOnEscape` | `true` | Слушать Escape и закрывать |
246
- | `closeOnBack` | `false` | Перехватывать кнопку «Назад» на Android (history.popstate) |
247
- | `closeOnRouteChange` | `false` | Закрывать при смене URL (патчит `pushState`/`replaceState` + popstate) |
248
- | `lockBodyScroll` | `true` | Блокировка прокрутки body, безопасная для iOS (`position: fixed`) |
249
- | `stackEffect` | `false` | Стек карточек как в iOS: нижние sheet уменьшаются с глубиной |
250
- | `inertSiblings` | `false` | Помечать соседние элементы страницы `inert`, пока sheet открыт (полноценная модалка) |
251
- | `persistent` | `false` | Запретить закрытие пользователем (backdrop / Escape / «Назад»); программный `close()` работает |
252
- | `disableClose` | `false` | Запретить любое закрытие, включая программное |
253
- | `disableDrag` | `false` | Отключить жест drag (императивные snap продолжают работать) |
254
- | `dragFrom` | `"handle"` | Какие области запускают drag: `"handle"`, `"sheet"` (весь sheet) или `"zones"` (только поддеревья `[data-bs-drag]`). Если handle не задан, по умолчанию `"sheet"`. `[data-bs-no-drag]` исключает поддерево в любом режиме. Также `setDragFrom()` / `getDragFrom()` — переключение перемонтирует жест, без перемонтирования компонента |
255
- | `dragFromContent` | `true` | Тянет ли sheet touch-жест на scroll-контейнере; переопределяется для отдельной snap point через `{ id, size, dragFromContent: false }`. Также `setDragFromContent()` |
256
- | `fitContentToSnap` | `false` | Делает весь scroll-контейнер достижимым, а footer видимым на каждой snap point, а не только на самой большой. По умолчанию выключено; см. [Контент достижим на каждой snap point](#контент-достижим-на-каждой-snap-point) |
257
- | `radius` | токен по умолчанию | Радиус скругления (`string` — CSS-длина или `number` — px); также `setRadius()` |
258
- | `maxHeight` | нет | Ограничение высоты sheet. `number` (px) или строка (`"92dvh"`, `"50%"`), пересчитывается при смене viewport / ориентации |
259
- | `returnFocusTo` | opener | Куда вернуть фокус при закрытии — `HTMLElement`, строка-селектор или фабрика `() => HTMLElement` |
260
-
261
- ### Anchored-элементы, docked-бары и сцены scrim
262
-
263
- Закрепляйте UI вокруг sheet — он едет вместе с ним на compositor (ноль JS на
264
- кадр) и показывается/скрывается по состоянию с настраиваемой анимацией:
204
+ </details>
265
205
 
266
- ```ts
267
- engine.addAnchor({
268
- element: closeBtn,
269
- position: 'sheet-top-right', // rides the sheet edge; 9 screen positions too
270
- showOn: ['half', 'full'], // snap ids or a predicate
271
- animation: 'pop', // fade | scale | slide | pop | custom keyframes
272
- });
206
+ <details>
207
+ <summary><b>Qwik</b></summary>
273
208
 
274
- engine.addAnchor({
275
- element: tabBar,
276
- position: 'dock-bottom', // full-width bar; the sheet slides under it
277
- });
209
+ ```tsx
210
+ import { component$, useSignal } from '@builder.io/qwik';
211
+ import { BottomSheet } from '@surdeddd/bottom-sheet/qwik';
212
+ import '@surdeddd/bottom-sheet/styles';
278
213
 
279
- engine.setScrimStages({
280
- stages: [
281
- { for: 'peek', element: teaser },
282
- { forRange: [0.5, 1], element: expanded },
283
- ],
284
- animation: 'fade',
214
+ export const Filters = component$(() => {
215
+ const open = useSignal(false);
216
+ return (
217
+ <>
218
+ <button onClick$={() => (open.value = true)}>Filters</button>
219
+ <BottomSheet
220
+ open={open.value}
221
+ onOpenChange$={next => (open.value = next)}
222
+ snapPoints={[{ id: 'closed', size: 0 }, { id: 'open', size: 'fit' }]}
223
+ focusTrap
224
+ >
225
+ <h2 q:slot="header">Filters</h2>
226
+ <FilterForm />
227
+ </BottomSheet>
228
+ </>
229
+ );
285
230
  });
286
231
  ```
287
232
 
288
- [Полная документация по anchors и stages →](docs/anchors.md)
289
-
290
- ### События
291
-
292
- ```ts
293
- engine.on('snap', ({ id, size, progress }) => {}); // settled state change
294
- engine.on('open', ({ id }) => {}); // 0 → >0 (enter starts)
295
- engine.on('opened', ({ id }) => {}); // enter settled
296
- engine.on('close', () => {}); // → 0 (exit starts)
297
- engine.on('closed', () => {}); // exit settled
298
- engine.on('dragstart', ({ size }) => {});
299
- engine.on('drag', ({ size, delta }) => {}); // ~60 fps — payload is pooled, don't retain
300
- engine.on('dragend', ({ size, velocity }) => {});
301
- engine.on('progress', ({ value, size }) => {}); // ~60 fps
302
-
303
- // Cancelable — call cancel() synchronously to veto the transition:
304
- engine.on('before-snap', ({ id, size, previousId, cancel }) => {});
305
- engine.on('before-close', ({ reason, cancel }) => {}); // reason: programmatic | backdrop | escape | back
306
- ```
307
-
308
- ### Императивный API
233
+ [Гайд по Qwik →](docs/qwik.md)
309
234
 
310
- ```ts
311
- engine.snapTo('half');
312
- engine.open(); // → first allowed snap with resolved size > 0
313
- engine.close(reason?); // → first snap that resolves to size 0, else first allowed
314
- engine.expand(); // → largest allowed snap
315
- engine.collapse(); // → smallest allowed snap with size > 0
316
- engine.setAllowed(ids, snapId); // dynamic allowlist
317
- engine.setSnapPoints(points); // re-measure
318
- engine.recompute(); // re-measure a 'fit' / 'content' snap on demand
319
- engine.getResolvedSnaps(); // → [{ id, size }] resolved to pixels right now
320
- engine.setPersistent(bool); // toggle persistence (block dismissal) at runtime
321
- engine.setDisableClose(bool); // toggle close-blocking at runtime
322
- engine.setDisableDrag(bool); // toggle the drag gesture at runtime
323
- engine.setRadius('28px'); // corner radius
324
- engine.setMaxHeight('92dvh'); // height cap (re-resolved on viewport change)
325
- engine.canDismiss(); // false when persistent / disableClose
326
- engine.isTop(); // top of the multi-sheet stack?
327
- engine.depth(); // open sheets above this one
328
- engine.destroy(); // remove listeners + cancel animation
329
- ```
235
+ </details>
330
236
 
331
- `initial` запускает sheet **сразу открытым** на любой snap point, размер
332
- которой разрешается в `> 0` (события `open` / `opened` не срабатывают — перехода
333
- для анонса нет). Вызывать `open()` / `snapTo()` **синхронно в тике монтирования
334
- безопасно** — задержка через `requestAnimationFrame` / `nextTick` не нужна, в том
335
- числе для `fit` / `content` и телепортированных sheet. Sheet, смонтированный
336
- скрытым (предок с `display:none`), «лечится» и один раз испускает свою
337
- последовательность открытия при появлении. См.
338
- [Architecture → Opening at construction](docs/ARCHITECTURE.md).
237
+ <details>
238
+ <summary><b>Веб-компонент</b> (Angular, Lit, Astro, обычный HTML)</summary>
339
239
 
340
- ### Runtime API scrim
240
+ ```html
241
+ <script type="module">
242
+ import '@surdeddd/bottom-sheet/element';
243
+ </script>
341
244
 
342
- Scrim — это затемняющий слой за sheet или вокруг него (`screenComponent`). Все
343
- свойства scrim можно менять в рантайме — перемонтирование не нужно.
245
+ <button onclick="document.querySelector('bottom-sheet').open()">Filters</button>
344
246
 
345
- | Метод | Назначение |
346
- | --- | --- |
347
- | `setScrimMode('full' \| 'above-sheet' \| 'off')` | Переключает позиционирование в рантайме. `'off'` полностью отключает слой scrim. |
348
- | `setScrimEnabled(boolean)` | Удобный переключатель — сохраняет/восстанавливает диапазоны opacity. |
349
- | `setScrimTapToClose(boolean)` | Устанавливает/снимает закрытие по клику на scrim. |
350
- | `setScrimColor(color \| null)` / `setScrimBlur(blur \| null)` | Живое изменение цвета и blur через backdrop-filter. `null` сбрасывает. |
351
- | `setScrimInteractive(boolean)` | Переключает `pointer-events` на слое scrim. |
352
- | `setBackdropRange([s, e])` / `setScreenRange([s, e])` | Отображение диапазона на opacity (progress 0..1 → opacity 0..1). |
353
- | `setScrim({ ... })` | Пакетное обновление — применяет preset и отдельные поля за один проход с единственным `applySize`. |
354
- | `setScrimOverlay({ children, position, interactive })` | Вставляет позиционированный плавающий элемент в область scrim. Возвращает функцию снятия. Полезно для мониторинговых UX вида «затемнить всё, кроме этого бейджа». |
355
- | `getScrimState()` | Read-only снимок `{ mode, enabled }`. Стабильный публичный API для интроспекции без привязки к внутренностям. |
247
+ <bottom-sheet snap-points="closed:0,open:fit" focus-trap="true">
248
+ <h2 slot="header">Filters</h2>
249
+ <form>…</form>
250
+ </bottom-sheet>
251
+ ```
356
252
 
357
- **Пресеты**: `'subtle' | 'standard' | 'monitoring' | 'cinematic'` — задаются опцией конструктора `scrimPreset` или через `setScrim({ preset })`.
253
+ Стили элемент несёт в собственном shadow root, отдельная таблица стилей не
254
+ нужна. Без сборщика подключите его с CDN:
358
255
 
359
- ### Runtime API overlay
256
+ ```html
257
+ <script src="https://unpkg.com/@surdeddd/bottom-sheet/dist/element.iife.global.js"></script>
258
+ ```
360
259
 
361
- Overlay-движок (`@surdeddd/bottom-sheet/overlay`) — это отдельная выезжающая
362
- панель, которая повторяет для контента ту же runtime-изменяемость, что и scrim.
260
+ [Гайд по веб-компоненту →](docs/web-component.md)
363
261
 
364
- | Метод | Назначение |
365
- | --- | --- |
366
- | `setOverlayChildren(node \| fragment \| factory)` | Заменяет содержимое overlay в рантайме. Принимает `Node`, `DocumentFragment` или фабрику `() => Node`. |
367
- | `clearOverlayChildren()` | Удаляет вставленный контент. |
368
- | `setOverlay({ children, ... })` | Пакетное обновление — `children` входит в число допустимых полей наряду с позиционированием и переопределениями preset. |
262
+ </details>
369
263
 
370
- **Пресеты**: `OverlayPreset` — это `'sheet' | 'dialog' | 'sidebar' | 'toast'`;
371
- доступны как константа-словарь `OVERLAY_PRESETS` и принимаются в `setOverlay({ preset })`.
264
+ <details>
265
+ <summary><b>Без фреймворка</b></summary>
372
266
 
373
- ### Sheet manager (реестр по маршрутам)
267
+ ```html
268
+ <div class="bs-root">
269
+ <div class="bs-backdrop"></div>
270
+ <section class="bs-sheet" data-mode="bottom" role="dialog" aria-label="Filters">
271
+ <div class="bs-handle" role="slider" tabindex="0" aria-label="Resize sheet"></div>
272
+ <div class="bs-content">…</div>
273
+ </section>
274
+ </div>
275
+ ```
374
276
 
375
277
  ```ts
376
- import { createSheetManager } from "@surdeddd/bottom-sheet";
278
+ import { BottomSheetEngine } from '@surdeddd/bottom-sheet';
279
+ import '@surdeddd/bottom-sheet/styles';
377
280
 
378
- const sheets = createSheetManager<"home" | "marker" | "panorama">({
379
- home: { snapPoints: [...], allowed: ["min", "half", "full"], onOpen: loadHome },
380
- marker: { snapPoints: [...], allowed: ["half", "full"], onOpen: focusMarker, onClose: blur },
281
+ const root = document.querySelector('.bs-root')!;
282
+ const sheet = new BottomSheetEngine({
283
+ element: root.querySelector('.bs-sheet')!,
284
+ handle: root.querySelector('.bs-handle')!,
285
+ scrollContainer: root.querySelector('.bs-content')!,
286
+ backdrop: root.querySelector('.bs-backdrop')!,
287
+ snapPoints: [
288
+ { id: 'closed', size: 0 },
289
+ { id: 'open', size: 'fit' },
290
+ ],
291
+ initial: 'closed',
381
292
  });
382
293
 
383
- // In your route watcher:
384
- sheets.transition(prev, next);
385
- const cfg = sheets.resolve(currentRoute); // typed!
294
+ document.querySelector('#filters')!.addEventListener('click', () => sheet.open());
386
295
  ```
387
296
 
388
- ## Производительность
297
+ [Гайд по движку без фреймворка →](docs/vanilla.md)
389
298
 
390
- Три осознанных решения держат 60 fps даже на Android среднего уровня:
299
+ </details>
391
300
 
392
- 1. **Позиционирование через transform.** `height` sheet выставляется один раз
393
- по самой большой snap point; изменения размера применяются через
394
- `transform: translate3d(...)`. Всё остаётся на compositor — ноль layout на
395
- кадр.
396
- 2. **Sub-stepping spring с единственной записью в DOM.** Жёсткие пружины
397
- интегрируются внутри на 240 Гц ради стабильности, но движок пишет в DOM
398
- ровно один раз за `requestAnimationFrame`.
399
- 3. **React-снимок только установившегося состояния.** `useSyncExternalStore`
400
- обновляется лишь на `snap` / `dragstart` / `dragend`. Пиксели drag и кадры
401
- анимации передаются через CSS-переменные (`--bs-progress`, `--bs-size`) и
402
- прямые императивные подписки — никаких ре-рендеров React на каждом кадре.
301
+ Preact: `@surdeddd/bottom-sheet/preact` — это React-адаптер, работающий через
302
+ `preact/compat` ([гайд](docs/preact.md)). В Angular, Lit и Astro используется
303
+ веб-компонент или сам движок: [Angular](docs/angular.md), [Lit](docs/lit.md),
304
+ [Astro](docs/astro.md).
403
305
 
404
- ## Slim core и composable-фичи
306
+ Гайды в `docs/` написаны на английском.
405
307
 
406
- Стандартный `BottomSheetEngine` поставляется со всеми опциональными
407
- поведениями. Если нужно только необходимое, импортируйте slim core (~17.5 KB
408
- gzip против ~23 KB) и подключайте только нужные фичи:
308
+ ## Основные понятия
409
309
 
410
- ```ts
411
- import { BottomSheetCore } from '@surdeddd/bottom-sheet/core';
412
- import { routeFeature, contentSwipeFeature } from '@surdeddd/bottom-sheet/features';
310
+ **Точки привязки** (snap points) — размеры, на которых шторка может
311
+ остановиться. У каждой есть `id`, по которому к ней обращаются, и `size`:
413
312
 
414
- const engine = new BottomSheetCore({
415
- element, handle, snapPoints,
416
- closeOnBack: true,
417
- features: [routeFeature(), contentSwipeFeature()],
418
- });
313
+ ```ts
314
+ type SnapPoint =
315
+ | number // пиксели
316
+ | `${number}%` // доля вьюпорта вдоль оси шторки
317
+ | 'fit' | 'content' // высота самой шторки: ручка, шапка, содержимое, подвал
318
+ | 'full' // вся ось
319
+ | string; // любая длина CSS: "50dvh", "clamp(200px, 60%, 800px)"
419
320
  ```
420
321
 
421
- Опциональные фичи: `routeFeature()` (`closeOnBack` / `routedTo` /
422
- `closeOnRouteChange`), `visualViewportFeature()` (ограничение под экранную
423
- клавиатуру), `contentSwipeFeature()`, `persistFeature()`, `autoCollapseFeature()`.
424
- Всё остальное (жесты, математика snap, самовосстановление при resize, a11y-слайдер,
425
- focus trap, scroll lock, scrim, стек) — часть ядра. Если задать опцию, фича
426
- которой не подключена, в dev-режиме будет одно предупреждение, а в production
427
- опция игнорируется. Собственные фичи реализуют `{ name, install(ctx) }` — тот же
428
- шов, что используют встроенные.
429
-
430
- ### WebGL-рендерер (опционально)
322
+ Точка размером `0` — это **закрытое** состояние. Шторку без такой точки
323
+ нельзя закрыть, только свернуть до самой маленькой.
324
+
325
+ **Три слоя.** Сама *шторка*, *бэкдроп*, который затемняет страницу, и
326
+ необязательный *скрим* (`screen`) для содержимого, которое проявляется за
327
+ шторкой: превью карты, размытая подложка.
328
+
329
+ **Состояние** — один объект: `{ activeId, size, progress, phase, isDragging,
330
+ isAnimating }`. `progress` идёт от 0 на самой маленькой разрешённой точке до
331
+ 1 на самой большой. `phase` принимает значения `closed`, `opening`, `open` и
332
+ `closing`.
333
+
334
+ ## API
335
+
336
+ Все опции, методы и события ниже работают одинаково в каждом адаптере.
337
+ Компоненты принимают опции как пропсы (`focus-trap` в шаблонах Vue,
338
+ `focus-trap="true"` у веб-компонента).
339
+
340
+ ### Опции
341
+
342
+ Размер и положение
343
+
344
+ | Опция | По умолчанию | Что делает |
345
+ | --- | --- | --- |
346
+ | `snapPoints` | обязательна | Список `{ id, size }` |
347
+ | `allowed` | все id | Точки, на которых шторка может остановиться сейчас |
348
+ | `initial` | первая разрешённая | Точка, с которой шторка начинает. Если её размер больше `0`, шторка появляется открытой, без события `open` |
349
+ | `mode` | `"bottom"` | Край: `bottom`, `top`, `left`, `right` или `start` / `end` |
350
+ | `maxHeight` | нет | Предел размера шторки: пиксели или длина CSS, которая пересчитывается при изменении вьюпорта |
351
+ | `radius` | `20px` | Радиус скругления |
352
+ | `fitContentToSnap` | `false` | Вся прокручиваемая область и подвал достижимы на каждой точке, а не только на самой большой. [Подробнее](docs/content-fit.md) |
353
+
354
+ Движение
355
+
356
+ | Опция | По умолчанию | Что делает |
357
+ | --- | --- | --- |
358
+ | `animation` | `"spring"` | `spring`, `ios-spring`, `material-bounce`, `snappy`, `tween`, `linear` |
359
+ | `spring` | из пресета | `{ stiffness, damping, mass }` |
360
+ | `duration`, `easing` | `220`, ease-out-back | Длительность в мс и кривая `(t) => number` для анимации `tween` |
361
+ | `settleAnimation` | выключена | `"waapi"` выполняет посадку как Web Animation на композиторе |
362
+ | `respectReducedMotion` | `true` | При `prefers-reduced-motion: reduce` шторка сразу встаёт на точку |
363
+ | `viewTransitions` | `false` | Переход между точками через `document.startViewTransition()`, если браузер его умеет |
364
+
365
+ Жесты
366
+
367
+ | Опция | По умолчанию | Что делает |
368
+ | --- | --- | --- |
369
+ | `dragFrom` | `"handle"` | Откуда можно начать перетаскивание: с ручки (`handle`), со всей шторки (`sheet`) или только из зон `[data-bs-drag]` (`zones`). `[data-bs-no-drag]` исключает поддерево. У шторки без ручки по умолчанию `sheet` |
370
+ | `dragFromContent` | `true` | Тянет ли шторку касание по прокручиваемой области. Можно задать для отдельной точки: `{ id, size, dragFromContent: false }` |
371
+ | `disableDrag` | `false` | Отключает жест; методы продолжают работать |
372
+ | `rubberBand` | `true` | Мягкое сопротивление за крайней точкой |
373
+ | `flickVelocity` | `0.65` px/мс | Скорость, с которой отпускание пальца или пера считается броском. Для мыши порог фиксированный: 0.4 |
374
+ | `dragThreshold` | `18` px | Перетаскивание пальцем или пером короче этого возвращает шторку назад. Для мыши порог фиксированный: 12 |
375
+
376
+ Закрытие
377
+
378
+ | Опция | По умолчанию | Что делает |
379
+ | --- | --- | --- |
380
+ | `closeOnEscape` | `true` | Escape закрывает верхнюю шторку |
381
+ | `closeOnBack` | `false` | Кнопка «Назад» закрывает шторку и не уводит со страницы |
382
+ | `routedTo` | нет | URL, который показывается, пока шторка открыта; «Назад» закрывает её и возвращает адрес |
383
+ | `closeOnRouteChange` | `false` | Закрыть при смене URL |
384
+ | `persistent` | `false` | Пользователь не может закрыть шторку: ни тапом по бэкдропу, ни Escape, ни «Назад», ни жестом, ни клавишами. `close()` по-прежнему закрывает |
385
+ | `disableClose` | `false` | Шторку не закрывает ничто, включая `close()` |
386
+
387
+ Фокус и доступность
388
+
389
+ | Опция | По умолчанию | Что делает |
390
+ | --- | --- | --- |
391
+ | `focusTrap` | `false` | Переносит фокус в шторку при открытии, держит Tab внутри, пока она верхняя, и возвращает фокус при закрытии. Попап, который рисуется в другом месте, пометьте `data-bs-focus-allow`, и фокус сможет туда уйти |
392
+ | `initialFocus` | первый фокусируемый | Вместе с `focusTrap`: элемент или селектор, который получит фокус при открытии. Первый фокусируемый элемент — ручка; `false` ставит фокус на саму шторку |
393
+ | `returnFocusTo` | нет | Элемент, селектор или функция: куда отправить фокус при закрытии. Ловушка фокуса и так возвращает его туда, где он был |
394
+ | `inertSiblings` | `false` | Делает всё вне шторки `inert`, пока она верхняя |
395
+ | `lockBodyScroll` | `true` | Блокирует прокрутку страницы, пока шторка открыта |
396
+
397
+ Бэкдроп и скрим
398
+
399
+ | Опция | По умолчанию | Что делает |
400
+ | --- | --- | --- |
401
+ | `backdropRange` | `[0, 1]` | Отрезок `progress`, на котором проявляется бэкдроп |
402
+ | `backdropOpacity` | `1` | Непрозрачность, которой бэкдроп достигает в конце этого отрезка |
403
+ | `backdropColor` | `rgb(0 0 0 / 0.5)` | Цвет бэкдропа; то же, что токен `--bs-backdrop-color` |
404
+ | `screenRange` | `[0, 1]` | Такой же отрезок для скрима |
405
+ | `scrimMode` | `"full"` | `full` — на весь экран, `above-sheet` — до края шторки, `off` — без слоя |
406
+ | `scrimPreset` | нет | `subtle`, `standard`, `monitoring`, `cinematic`: цвет, размытие и отрезок одной опцией |
407
+ | `scrimColor`, `scrimBlur` | из пресета | Заливка скрима и радиус его размытия `backdrop-filter` |
408
+ | `scrimInteractive` | `false` | Скрим принимает указатель сам, а не пропускает его на страницу |
409
+ | `scrimTapToClose` | `false` | Тап по скриму закрывает шторку |
410
+
411
+ Остальное
412
+
413
+ | Опция | По умолчанию | Что делает |
414
+ | --- | --- | --- |
415
+ | `stackEffect` | `false` | Уменьшает шторки под верхней, как стопка карточек в iOS |
416
+ | `persistKey` | нет | Ключ `localStorage`: шторка начинает с точки, на которой её оставили |
417
+ | `autoCollapseAfter` | выключена | Через сколько миллисекунд покоя шторка вернётся на самую маленькую открытую точку |
418
+ | `linkedSheets` | нет | Другие движки, которые сворачиваются, когда открывается этот |
419
+ | `features` | все | Список возможностей для [облегчённого ядра](#облегчённое-ядро) |
420
+
421
+ Компоненты добавляют несколько собственных пропсов:
422
+
423
+ | Проп | По умолчанию | Что делает |
424
+ | --- | --- | --- |
425
+ | `open`, `onOpenChange` | неуправляемый | Управляемое состояние «открыта». `v-model:open` во Vue, `bind:open` в Svelte, `onOpenChange$` в Qwik, атрибут `open` у веб-компонента |
426
+ | `snap` | неуправляемый | Активная точка привязки как проп: `v-model:snap` во Vue, `bind:snap` в Svelte, `snap` вместе с `onSnap$` в Qwik, атрибут `snap` у веб-компонента. В React и Solid для этого есть ref |
427
+ | `header`, `footer` | нет | Области над прокручиваемой частью и под ней (слоты во Vue, сниппеты в Svelte) |
428
+ | `backdrop` | `true` | Рисовать бэкдроп |
429
+ | `closeOnBackdrop` | `true` | Тап по бэкдропу закрывает шторку |
430
+ | `teleportTo` | нет | Переносит DOM шторки из предка, который её обрезает или трансформирует |
431
+ | `ariaLabel` | `"Bottom sheet"` | Доступное имя шторки |
432
+ | `handleLabel`, `contentLabel` | на английском | Доступные имена ручки и прокручиваемой области, для приложений на другом языке |
433
+
434
+ ### Методы
431
435
 
432
436
  ```ts
433
- import { webglRenderer } from '@surdeddd/bottom-sheet/webgl';
434
-
435
- const engine = new BottomSheetEngine({
436
- element, handle, snapPoints,
437
- features: [webglRenderer({ jelly: 0.6 })],
438
- });
437
+ sheet.open(); // закрыта → первая разрешённая открытая точка; открытая остаётся на месте
438
+ sheet.open('half'); // на конкретную точку
439
+ sheet.close(); // → закрытая точка; если её нет, самая маленькая
440
+ sheet.snapTo('full');
441
+ sheet.expand(); // → самая большая разрешённая точка
442
+ sheet.collapse(); // → самая маленькая разрешённая открытая точка
443
+
444
+ sheet.setAllowed(['half', 'full']); // сменить набор разрешённых точек
445
+ sheet.setSnapPoints(points); // заменить точки
446
+ sheet.recompute(); // перемерить точку 'fit'
447
+ sheet.setMode('right');
448
+ sheet.setPersistent(true);
449
+ sheet.setDisableClose(true);
450
+ sheet.setDisableDrag(true);
451
+ sheet.setRadius('28px');
452
+ sheet.setMaxHeight('92dvh');
453
+ sheet.setBackdropOpacity(0.6);
454
+ sheet.setBackdropColor('rgb(0 0 0 / 0.4)');
455
+
456
+ sheet.state; // { activeId, size, progress, phase, isDragging, isAnimating }
457
+ sheet.getResolvedSnaps(); // [{ id, size }] в пикселях на этот момент
458
+ sheet.canDismiss(); // false при persistent или disableClose
459
+ sheet.isTop(); // верхняя ли она в стеке
460
+ sheet.destroy();
439
461
  ```
440
462
 
441
- Рисует поверхность sheet — панель, скругления, тень, блик по краю — на GPU. В
442
- момент начала drag текст sheet переносится в текстуру, так что весь sheet
443
- изгибается как единая поверхность, а после остановки возвращается обратно в
444
- DOM. Layout, прокрутка, жесты и дерево доступности из DOM никуда не уходят.
445
- 4.2 KB gzip, и только если вы его импортируете.
446
- Откатывается на DOM-рендерер — молча и даже посреди сессии, если потребуется, —
447
- когда WebGL недоступен, при `prefers-reduced-motion` или если браузер сбросил
448
- GL-контекст.
463
+ `open()`, `close()` и `snapTo()` возвращают промис, который завершается,
464
+ когда шторка встала на место. Их можно вызывать в том же тике, в котором
465
+ шторка смонтирована.
449
466
 
450
- [Полная документация по WebGL →](docs/webgl.md)
467
+ Если `setAllowed()` или `setSnapPoints()` убирают активную точку, открытая
468
+ шторка переходит на ближайшую точку, на которой она остаётся открытой.
451
469
 
452
- ### Debug overlay
470
+ В компонентах эти методы доступны через ref (`ref.current.open()`), у
471
+ веб-компонента — на самом элементе (`el.open()`), у композабла — на
472
+ объекте, который он возвращает. Всё, чего там нет, достаётся через
473
+ `getEngine()`.
453
474
 
454
- ```ts
455
- import { attachDebugOverlay } from '@surdeddd/bottom-sheet/debug';
475
+ ### События
456
476
 
457
- const detach = attachDebugOverlay(engine);
477
+ ```ts
478
+ sheet.on('open', ({ id }) => {}); // шторка начинает появляться
479
+ sheet.on('opened', ({ id }) => {}); // встала на место
480
+ sheet.on('close', () => {}); // начинает уходить
481
+ sheet.on('closed', () => {}); // ушла: размонтировать нужно здесь
482
+ sheet.on('snap', ({ id, size, progress }) => {}); // остановилась на точке
483
+
484
+ sheet.on('dragstart', ({ size }) => {});
485
+ sheet.on('drag', ({ size, delta }) => {}); // каждый кадр
486
+ sheet.on('dragend', ({ size, velocity }) => {});
487
+ sheet.on('progress', ({ value, size }) => {}); // каждый кадр любого движения
488
+
489
+ sheet.on('before-snap', ({ id, previousId, cancel }) => {});
490
+ sheet.on('before-close', ({ reason, cancel }) => {});
458
491
  ```
459
492
 
460
- Фиксированная dev-панель с активной snap point, текущими size/progress,
461
- флагами drag/animate и списком разрешённых snap — работает исключительно на
462
- публичных событиях движка и поставляется отдельным entry, так что в
463
- продакшн-бандлы не попадает.
493
+ Чтобы остановить переход, вызовите `cancel()` синхронно внутри `before-snap`
494
+ или `before-close`; `reason` — это `programmatic`, `backdrop`, `escape` или
495
+ `back`.
496
+
497
+ Блокировка прокрутки, ловушка фокуса и запись в истории включаются на
498
+ `open`. Запись в истории снимается на `close`, блокировка и ловушка — на
499
+ `closed`, когда шторки уже нет. Переход, развёрнутый на полпути, снова
500
+ вызывает `open` или `close`, поэтому эти два события всегда чередуются.
464
501
 
465
- ### Qwik SSR
502
+ В компонентах события приходят как пропсы `onSnap`, `onOpen`… (React,
503
+ Solid), `@snap`, `@open`… (Vue), `onsnap`… (Svelte), `onSnap$`… (Qwik) и как
504
+ DOM-события у веб-компонента.
466
505
 
467
- Пакет объявляет поле `qwik`, указывающее на
468
- `dist/qwik-lib/index.qwik.mjs`. Приложения на Qwik подхватывают адаптер
469
- автоматически как vendor root: оптимизатор на стороне потребителя режет его на
470
- QRL-чанки, а `renderToString` выдаёт возобновляемый (`q:container="paused"`)
471
- sheet. Дополнительная конфигурация не нужна —
472
- `import { BottomSheet } from '@surdeddd/bottom-sheet/qwik'` по-прежнему
473
- работает для CSR, а Qwik City SSR теперь работает из коробки.
506
+ ### Клавиатура
474
507
 
475
- ## Темизация
508
+ Ручка работает как слайдер. Стрелка, направленная от края шторки, увеличивает
509
+ её (`ArrowUp` для нижней, `ArrowLeft` для правой), противоположная уменьшает,
510
+ `Home` и `End` переводят на самую маленькую и самую большую точку, `Escape`
511
+ закрывает.
476
512
 
477
- Весь внешний вид управляется CSS custom properties. Переопределяйте где угодно:
513
+ ### Остальной API
514
+
515
+ | Тема | Гайд |
516
+ | --- | --- |
517
+ | Кнопки и панели, закреплённые у края шторки; содержимое скрима для разных состояний | [Anchors and scrim stages](docs/anchors.md) |
518
+ | Изменение скрима на лету | [Theming → the scrim](docs/theming.md#the-scrim) |
519
+ | Лёгкая выезжающая панель без точек привязки | [Overlay](docs/overlay.md) |
520
+ | Шторки, настроенные по маршрутам | [Vanilla → sheet manager](docs/vanilla.md#sheet-manager) |
521
+ | Навигация сразу после закрытия при `closeOnBack` | [Vanilla → waiting for the history](docs/vanilla.md#waiting-for-the-history) |
522
+ | Движение шторки из кода, покадрово | [Recipe: programmatic drag](docs/recipes/programmatic-drag.md) |
523
+ | Свои расширения движка | [Plugins](docs/plugins.md) |
524
+ | Интерфейсы справа налево | [RTL](docs/rtl.md) |
525
+
526
+ ## Стилизация
527
+
528
+ Всё визуальное задано пользовательскими свойствами CSS. Их можно выставить на
529
+ `.bs-root`, на любом предке или на `:root`; до shadow root веб-компонента они
530
+ тоже доходят.
478
531
 
479
532
  ```css
480
533
  .bs-root {
481
534
  --bs-surface: #18181b;
482
- --bs-handle-color: #71717a;
483
535
  --bs-radius: 28px;
484
- --bs-shadow: 0 -8px 30px rgba(0, 0, 0, 0.6);
536
+ --bs-handle-color: #71717a;
537
+ --bs-backdrop-color: rgb(0 0 0 / 0.6);
485
538
  }
486
539
  ```
487
540
 
488
- Движок записывает в элемент sheet `--bs-progress` (0..1) и `--bs-size` (px) —
489
- управляйте любой CSS-анимацией от drag без единой строки JS:
541
+ Движок на каждом кадре пишет `--bs-progress` (от 0 до 1) и `--bs-size`
542
+ (пиксели) на шторку и на `.bs-root`, поэтому всё, что лежит внутри корня,
543
+ например слой `screen` или плавающая кнопка, следует за шторкой без
544
+ JavaScript:
490
545
 
491
546
  ```css
492
- .fab {
493
- transform: scale(calc(1 - var(--bs-progress) * 0.3));
494
- }
495
- .map-canvas {
496
- filter: blur(calc(var(--bs-progress) * 8px));
497
- }
498
- .search-input {
499
- opacity: calc(1 - var(--bs-progress));
500
- }
547
+ .bs-root .map-preview { filter: blur(calc(var(--bs-progress) * 8px)); }
548
+ .bs-root .fab { bottom: calc(var(--bs-size) + 16px); }
501
549
  ```
502
550
 
503
- > **z-index принадлежит движку.** Контроллер стека при каждом изменении стека
504
- > перезаписывает inline-`z-index` у `.bs-sheet`, `.bs-backdrop` и обёрток
505
- > anchor (база `100`, шаг `10` на уровень глубины), поэтому заданное вами
506
- > значение будет перезаписано и начнёт конфликтовать со стеком. Чтобы
507
- > расположить слой sheet относительно UI приложения, переместите контейнер
508
- > (цель teleport / родителя `.bs-root`) или измените порядок открытия — не
509
- > стилизуйте `z-index` у элементов, которыми управляет bs. См.
510
- > [Architecture → z-index & the sheet stack](docs/ARCHITECTURE.md).
511
-
512
- **У каждого sheet свой backdrop.** Откройте список, затем поверх него sheet с
513
- деталями — оба backdrop остаются активными: затемнение усиливается с каждым
514
- уровнем, а нижний sheet остаётся замаскированным всё время, пока закрывается
515
- верхний, так что касания не доходят до страницы посреди анимации. Каждый
516
- backdrop следует за progress собственного sheet, поэтому закрывающийся
517
- «погребённый» sheet убирает свой scrim. Стек сохраняет порядок, в котором
518
- sheet начали открываться: sheet, открытый, пока другой ещё анимируется, остаётся
519
- сверху. Если нужен единый уровень затемнения, передайте `backdrop={false}`
520
- (`:backdrop="false"` во Vue) тем sheet, которые открываете поверх.
521
-
522
- ### Контент достижим на каждой snap point
523
-
524
- Sheet всегда имеет высоту своей самой большой snap point и двигается через
525
- transform — именно это держит drag на compositor. Цена: ниже самой большой
526
- snap point низ scroll-контейнера оказывается под краем экрана ровно на
527
- `largest snap − current size`. На половинной snap point прокручивать можно, но
528
- последние строки не появятся, пока sheet не вытянут выше.
529
-
530
- `fitContentToSnap` закрывает этот зазор:
551
+ Три готовых оформления лежат в отдельных таблицах стилей:
531
552
 
532
- ```tsx
533
- <BottomSheet snapPoints={points} fitContentToSnap>
534
- <LongList />
535
- </BottomSheet>
553
+ ```ts
554
+ import '@surdeddd/bottom-sheet/themes/ios'; // а также /material, /vercel
536
555
  ```
537
556
 
538
- Когда sheet останавливается, движок записывает скрытую величину в
539
- `--bs-content-inset`, а стили уменьшают `.bs-content` ровно на неё, так что
540
- scroll-контейнер заканчивается у края экрана: последняя строка, поле в фокусе
541
- и полоса прокрутки остаются в видимой части, а ползунок рассчитан на то, что
542
- реально видно. В момент начала движения sheet переключается на контейнер
543
- полной высоты со спейсером такого же размера в конце списка, поэтому строки
544
- ниже края уже разложены, когда drag или анимация их открывает. Переключение
545
- меняет и бокс, и диапазон прокрутки на одну и ту же величину, так что на
546
- экране ничего не сдвигается. Это один layout при старте движения и один при
547
- остановке, ни одного на кадр, поэтому движение остаётся чисто transform-овым.
548
- Пока sheet движется, список, прокрученный до самого конца, удерживается на
549
- месте на экране, а не уезжает вверх вместе с sheet и не откатывается обратно
550
- в конце.
551
-
552
- У footer та же проблема, и лечится она так же. Он последний в sheet, поэтому
553
- ниже самой большой snap point он оказывается под краем экрана вместе с концом
554
- списка. При включённом `fitContentToSnap` уменьшенный контейнер ставит footer
555
- у низа видимой части sheet в покое, а пока sheet движется, стили поднимают
556
- `.bs-footer` на `--bs-size − --bs-max-size`, так что он едет по этому краю на
557
- каждом кадре drag или анимации — опять же без layout. Список заканчивается над
558
- ним, а не под ним. На snap point, слишком короткой для обоих, footer уходит под
559
- край экрана, а не закрывает handle. WebGL-рендерер получает ту же раскладку;
560
- пока sheet движется, его footer закрашивается цветом поверхности, а drag
561
- переносит список в текстуру, которая следует за позицией прокрутки списка, так
562
- что список, удерживаемый в конце, остаётся там же.
563
-
564
- #### Прокрутка списка на каждой snap point на таче
565
-
566
- Сделать конец списка *достижимым* и дать пальцу *прокрутить* до него — две
567
- разные вещи. По умолчанию свайп, начатый на контенте, тянет sheet, пока он не
568
- дойдёт до самой большой snap point, и только потом прокручивает список — так
569
- ведут себя нативные sheet. На мыши или трекпаде колесо прокручивает на любой
570
- snap point, на таче — нет. Чтобы прокручивать пальцем на половинной snap point,
571
- отдайте свайпы по контенту списку, а handle (и header) оставьте для движения
572
- sheet:
573
-
574
- ```tsx
575
- <BottomSheet snapPoints={points} fitContentToSnap dragFromContent={false}>
576
- <LongList />
577
- </BottomSheet>
557
+ ```html
558
+ <div class="bs-theme-ios"><!-- шторка --></div>
578
559
  ```
579
560
 
580
- `dragFromContent` можно задать и для отдельной snap point: это сохраняет
581
- нативное ощущение pull-to-expand на маленьком peek и освобождает список начиная
582
- с половины:
561
+ [Все токены, темы, порядок наложения и части веб-компонента →](docs/theming.md)
583
562
 
584
- ```ts
585
- const points = [
586
- { id: "peek", size: 120 },
587
- { id: "half", size: "50%", dragFromContent: false },
588
- { id: "full", size: "90%", dragFromContent: false },
589
- ];
590
- ```
563
+ ## Доступность
591
564
 
592
- Работает для `bottom`-sheet и ожидает, что scroll-контейнер — растущий
593
- потомок flex-колонки sheet, как `.bs-content`. Для любого другого контейнера
594
- атрибут `data-bs-fit-content` на sheet принимает значения `rest` или `moving`,
595
- а `--bs-content-inset` хранит скрытую величину: в покое уменьшайте контейнер на
596
- неё, а при движении добавляйте её как отступ в конец содержимого. На slim core
597
- добавьте `contentFitFeature()` из `@surdeddd/bottom-sheet/features`.
598
-
599
- ### Scrim, blur и высокий контраст
600
-
601
- | Токен | По умолчанию | Область |
602
- | ------------------------ | ------------ | -------------------------------------------------------- |
603
- | `--bs-scrim-color` | transparent | Заливка слоя scrim (`.bs-screen`) |
604
- | `--bs-scrim-blur` | `0px` | `backdrop-filter: blur()`, применяемый к слою scrim |
605
- | `--bs-header-min-height` | `0px` | Минимальная высота области `.bs-header` |
606
-
607
- `--bs-scrim-blur` включает настоящий blur через `backdrop-filter` на scrim —
608
- задайте его для эффекта матового стекла (или используйте пресеты scrim
609
- `monitoring` / `cinematic`, которые задают и цвет, и blur). В режиме Windows
610
- High Contrast / `forced-colors` таблица стилей содержит блок
611
- `@media (forced-colors: active)`, чтобы поверхность sheet, handle и кольцо
612
- фокуса оставались видимыми.
613
-
614
- ### Отступы и раскладка
615
-
616
- Отступы каждой области — это токен: переопределите или обнулите его, чтобы
617
- контент шёл от края до края. Значения по умолчанию соответствуют встроенному
618
- виду. Задавайте переменную на хосте / `.bs-root` / `:root`; custom properties
619
- наследуются через shadow-границу Web Component, поэтому одно и то же
620
- переопределение работает для всех адаптеров:
621
-
622
- | Токен | По умолчанию | Область |
623
- | ---------------------- | --------------------- | ----------------------- |
624
- | `--bs-content-padding` | `0 16px 16px` | прокручиваемое тело |
625
- | `--bs-handle-padding` | `8px 16px 4px` | полоса захвата (сверху) |
626
- | `--bs-header-padding` | `0 16px 8px` | слот header |
627
- | `--bs-footer-padding` | `12px 16px (+ inset)` | слот footer |
628
-
629
- (Значение footer по умолчанию — `12px 16px calc(12px + env(safe-area-inset-bottom))`;
630
- оно не даёт кнопкам действий оказаться под индикатором «Домой».)
565
+ Что встроено и что включается опцией:
631
566
 
632
- ```css
633
- .bs-root { --bs-content-padding: 0; } /* full-bleed content */
634
- .bs-root { --bs-content-padding: 0 24px 24px; } /* custom insets */
635
- bottom-sheet { --bs-handle-padding: 0; } /* Web Component, from the host */
567
+ - **Ручка — это слайдер.** У неё есть имя, текущее значение и шаги по
568
+ стрелкам, и она входит в порядок обхода по Tab. Назовите её на своём языке
569
+ через `handleLabel`; `contentLabel` делает то же для прокручиваемой
570
+ области.
571
+ - **У шторки есть имя.** `ariaLabel` или `ariaLabelledBy`, указывающий на
572
+ заголовок. С `focusTrap` шторка получает роль `dialog` и `aria-modal`.
573
+ - **Фокус.** `focusTrap` переносит фокус в шторку, держит Tab внутри и при
574
+ закрытии возвращает фокус туда, откуда шторку открыли. В стеке фокус держит
575
+ только верхняя шторка.
576
+ - **Страница за шторкой.** `inertSiblings` убирает всё вне верхней шторки из
577
+ досягаемости клавиатуры и экранных читалок.
578
+ - **Объявления.** Каждая остановка объявляется через вежливую live-область.
579
+ - **Движение и контраст.** При `prefers-reduced-motion` анимации заменяются
580
+ мгновенным переходом, при `forced-colors` шторка получает рамку, заметную
581
+ ручку и системное кольцо фокуса.
582
+
583
+ Модальной шторке нужны обе опции: `focusTrap` и `inertSiblings`.
584
+
585
+ ## Формы и экранная клавиатура
586
+
587
+ Нижняя шторка встаёт на экранную клавиатуру, а не уходит под неё, поэтому
588
+ поле, в которое печатают, остаётся на виду. С `focusTrap` фокус при открытии
589
+ попадает на ручку, и клавиатура не поднимается, пока пользователь не коснётся
590
+ поля; `initialFocus="#email"` ставит фокус сразу в поле.
591
+
592
+ Чтобы шторка с несохранённым вводом не закрылась, отмените переход:
593
+
594
+ ```ts
595
+ sheet.on('before-close', ({ cancel }) => {
596
+ if (form.isDirty) cancel();
597
+ });
636
598
  ```
637
599
 
638
- `maxHeight` (проп или атрибут `max-height` у элемента) ограничивает sheet:
639
- snap point `'content'` / `'fit'`, превышающая ограничение, останавливается
640
- вплотную к краю, а тело прокручивается внутри — sheet никогда не отрывается от
641
- своей опоры, а `--bs-size` всегда равен отрисованной высоте.
600
+ Для React Hook Form и Formik это одна строка:
642
601
 
643
- ## Демо
602
+ ```ts
603
+ import { useFormGuard } from '@surdeddd/bottom-sheet/integrations/react-hook-form';
644
604
 
645
- ```bash
646
- npm run demo
647
- # http://localhost:5173
605
+ useFormGuard(sheetRef, form); // не двигается во время отправки, не закрывается с несохранённым вводом
648
606
  ```
649
607
 
650
- Редакторское демо показывает адаптеры, каждый жест, каждый режим
651
- (`bottom · top · left · right · overlay`) и живые показания движка (активная
652
- snap point, progress, скорость, FPS). В углу переключатели EN/RU и светлой/тёмной темы.
608
+ ## Серверный рендеринг
653
609
 
654
- ## Справа налево (RTL)
610
+ Каждый адаптер можно импортировать и отрендерить на сервере; движок
611
+ подключается на клиенте. Шторка, которая начинает закрытой, скрыта уже в
612
+ серверной разметке, поэтому до гидратации ничего не мелькает. Это относится
613
+ к Next.js, Nuxt, SvelteKit, SolidStart, Qwik City и Astro; детали, например
614
+ `"use client"` в Next.js App Router, описаны в гайдах по фреймворкам.
655
615
 
656
- `left` и `right` — физические края и остаются физическими. Для интерфейсов,
657
- которые зеркалятся вместе с направлением письма, используйте логические
658
- режимы — `start` и `end` разрешаются относительно направления, действующего
659
- для sheet, так что `start` — левый край в английском и правый в арабском:
616
+ ## Облегчённое ядро
617
+
618
+ Движок по умолчанию включает все необязательные возможности. Если важнее
619
+ размер, возьмите облегчённое ядро и добавьте к нему только нужное:
660
620
 
661
621
  ```ts
662
- new BottomSheetEngine({ element, handle, mode: 'start', snapPoints });
622
+ import { BottomSheetCore } from '@surdeddd/bottom-sheet/core';
623
+ import { routeFeature, contentSwipeFeature } from '@surdeddd/bottom-sheet/features';
624
+
625
+ const sheet = new BottomSheetCore({
626
+ element, handle, snapPoints,
627
+ closeOnBack: true,
628
+ features: [routeFeature(), contentSwipeFeature()],
629
+ });
663
630
  ```
664
631
 
665
- [Полная документация по RTL →](docs/rtl.md)
632
+ | Точка входа | gzip | Что внутри |
633
+ | --- | --- | --- |
634
+ | `@surdeddd/bottom-sheet` | 25.6 KB | Полный движок |
635
+ | `/core` | 21.9 KB | Жесты, точки привязки, фокус, блокировка прокрутки, скрим, стек |
636
+ | `/features` | 4.2 KB | `routeFeature`, `visualViewportFeature`, `contentSwipeFeature`, `contentFitFeature`, `persistFeature`, `autoCollapseFeature` |
637
+ | `/react` `/vue` `/svelte` `/solid` `/qwik` | от 27 до 33 KB | Полный движок вместе с адаптером |
638
+ | `/element` | 33.3 KB | Полный движок, элемент и его стили |
639
+ | `/overlay` | 8.4 KB | Отдельный [оверлей](docs/overlay.md) |
640
+ | `/webgl` | 6.4 KB | Необязательный [рендерер на GPU](docs/webgl.md) для поверхности шторки |
641
+ | `/debug` | 0.9 KB | `attachDebugOverlay(engine)`: панель с состоянием движка в реальном времени |
642
+
643
+ Опция, для которой не подключена возможность, один раз предупреждает в
644
+ режиме разработки и игнорируется.
666
645
 
667
646
  ## Поддержка браузеров
668
647
 
669
- Базовый уровень: Chrome/Edge 80, Safari 13.1 (iOS 13.4), Firefox 72 — его
670
- задают целевая сборка ES2020 и API, без которых движок работать не может
671
- (Pointer Events, CSS custom properties, `ResizeObserver`). Всё остальное, от
672
- `inert` до `backdrop-filter` и WebGL, проверяется на наличие и деградирует
673
- мягко.
648
+ Chrome и Edge 80, Safari 13.1 (iOS 13.4), Firefox 72. Эту границу задают
649
+ сборка под ES2020 и API, без которых движок не работает: Pointer Events,
650
+ пользовательские свойства CSS, `ResizeObserver`. Всё остальное, от `inert`
651
+ до `backdrop-filter` и WebGL, определяется на месте и при отсутствии мягко
652
+ отключается. [Таблица поддержки →](docs/browser-support.md)
674
653
 
675
- [Полная матрица поддержки →](docs/browser-support.md)
654
+ ## Документация
676
655
 
677
- ## Тестирование
656
+ | | |
657
+ | --- | --- |
658
+ | Фреймворки | [React](docs/react.md) · [Vue](docs/vue.md) · [Svelte](docs/svelte.md) · [Solid](docs/solid.md) · [Qwik](docs/qwik.md) · [Preact](docs/preact.md) · [Веб-компонент](docs/web-component.md) · [Без фреймворка](docs/vanilla.md) · [Angular](docs/angular.md) · [Lit](docs/lit.md) · [Astro](docs/astro.md) |
659
+ | Темы | [Стилизация](docs/theming.md) · [Якоря и сцены скрима](docs/anchors.md) · [Содержимое на каждой точке](docs/content-fit.md) · [Оверлей](docs/overlay.md) · [RTL](docs/rtl.md) · [Рендерер WebGL](docs/webgl.md) · [Плагины](docs/plugins.md) |
660
+ | Рецепты | [Десять разобранных примеров](docs/recipes/README.md): панель фильтров, оформление заказа, тред комментариев, навигационная панель, мини-плеер и другие |
661
+ | Примеры | [Запускаемые проекты](https://github.com/Surdeddd/BottomSheet/tree/main/examples) для React, Vue, Svelte, Solid, Lit, веб-компонента и движка без фреймворка |
662
+ | Переход с другой библиотеки | [С vaul или react-modal-sheet](docs/MIGRATION.md) |
663
+ | Проект | [Архитектура](docs/ARCHITECTURE.md) · [Стабильность API](docs/api-stability.md) · [Поддержка браузеров](docs/browser-support.md) · [Участие в разработке](CONTRIBUTING.md) |
678
664
 
679
- ```bash
680
- npm test # 500+ unit tests via vitest + happy-dom
681
- npx playwright test # e2e via Playwright on mobile-Chrome / mobile-Safari / Firefox
682
- npm run typecheck # TypeScript --noEmit
683
- ```
665
+ ## Для ИИ-ассистентов
684
666
 
685
- CI гоняет все три набора на Node 20.19 / 22 при каждом PR и теге.
667
+ [`llms.txt`](llms.txt) — компактное и полное описание API, написанное для
668
+ чтения моделью: точки входа, все опции, имена методов и событий и ошибки,
669
+ которых стоит избегать. Файл входит в npm-пакет и лежит рядом с этим README.
686
670
 
687
671
  ## Участие в разработке
688
672
 
689
- 1. `npm install`
690
- 2. `npm run dev` — tsup в watch-режиме
691
- 3. `npm run demo` — dev-сервер Vite с живым демо
692
- 4. `npm test` — vitest в watch-режиме
693
- 5. Откройте PR с сфокусированным диффом. Слияние закрыто матрицей CI.
673
+ ```bash
674
+ npm install
675
+ npm run demo # демо-сайт на localhost:5173
676
+ npm test # юнит-тесты
677
+ npm run e2e # тесты в браузере
678
+ ```
679
+
680
+ Подробности в [CONTRIBUTING.md](CONTRIBUTING.md). Ошибки и идеи — в
681
+ [issues](https://github.com/Surdeddd/BottomSheet/issues).
694
682
 
695
683
  ## Лицензия
696
684
 
697
- MIT — см. [LICENSE](LICENSE).
685
+ MIT, см. [LICENSE](LICENSE).