@vanilla-bean/components 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/Component/Component.js +598 -0
  2. package/Component/Component.scenarios.js +88 -0
  3. package/Component/Component.test.js +717 -0
  4. package/Component/README.md +455 -0
  5. package/Component/index.js +3 -0
  6. package/Component/observeElementConnection.js +52 -0
  7. package/Component/observeElementConnection.test.js +121 -0
  8. package/Elem/Elem.js +304 -0
  9. package/Elem/Elem.test.js +679 -0
  10. package/Elem/README.md +373 -0
  11. package/Elem/index.js +1 -0
  12. package/FontWithASyntaxHighlighter-Regular.woff2 +0 -0
  13. package/LICENSE +21 -0
  14. package/README.md +413 -0
  15. package/components/BottomSheet/BottomSheet.js +192 -0
  16. package/components/BottomSheet/BottomSheet.lld.md +25 -0
  17. package/components/BottomSheet/README.md +66 -0
  18. package/components/BottomSheet/index.js +1 -0
  19. package/components/Button/Button.js +53 -0
  20. package/components/Button/Button.lld.md +21 -0
  21. package/components/Button/index.js +1 -0
  22. package/components/Calendar/Calendar.js +720 -0
  23. package/components/Calendar/Calendar.lld.md +22 -0
  24. package/components/Calendar/CalendarEvent.js +102 -0
  25. package/components/Calendar/Toolbar.js +78 -0
  26. package/components/Calendar/index.js +2 -0
  27. package/components/Calendar/utils.js +56 -0
  28. package/components/Code/Code.js +84 -0
  29. package/components/Code/Code.lld.md +21 -0
  30. package/components/Code/index.js +1 -0
  31. package/components/ColorPicker/ColorPicker.js +445 -0
  32. package/components/ColorPicker/ColorPicker.lld.md +21 -0
  33. package/components/ColorPicker/index.js +1 -0
  34. package/components/ColorPicker/svg.js +5 -0
  35. package/components/Dialog/Dialog.js +278 -0
  36. package/components/Dialog/Dialog.lld.md +20 -0
  37. package/components/Dialog/README.md +96 -0
  38. package/components/Dialog/index.js +1 -0
  39. package/components/Form/Form.js +257 -0
  40. package/components/Form/Form.lld.md +21 -0
  41. package/components/Form/README.md +87 -0
  42. package/components/Form/index.js +1 -0
  43. package/components/Icon/Icon.js +54 -0
  44. package/components/Icon/Icon.lld.md +21 -0
  45. package/components/Icon/index.js +1 -0
  46. package/components/Input/Input.js +173 -0
  47. package/components/Input/Input.lld.md +28 -0
  48. package/components/Input/README.md +97 -0
  49. package/components/Input/index.js +2 -0
  50. package/components/Input/utils.js +122 -0
  51. package/components/Keyboard/Key.js +38 -0
  52. package/components/Keyboard/Keyboard.js +173 -0
  53. package/components/Keyboard/Keyboard.lld.md +21 -0
  54. package/components/Keyboard/index.js +1 -0
  55. package/components/Label/Label.js +214 -0
  56. package/components/Label/Label.lld.md +20 -0
  57. package/components/Label/index.js +1 -0
  58. package/components/Link/Link.js +43 -0
  59. package/components/Link/Link.lld.md +15 -0
  60. package/components/Link/index.js +1 -0
  61. package/components/List/List.js +82 -0
  62. package/components/List/List.lld.md +19 -0
  63. package/components/List/index.js +1 -0
  64. package/components/Menu/Menu.js +93 -0
  65. package/components/Menu/Menu.lld.md +15 -0
  66. package/components/Menu/index.js +1 -0
  67. package/components/Notify/Notify.js +96 -0
  68. package/components/Notify/Notify.lld.md +20 -0
  69. package/components/Notify/index.js +1 -0
  70. package/components/Page/Page.js +67 -0
  71. package/components/Page/Page.lld.md +20 -0
  72. package/components/Page/index.js +1 -0
  73. package/components/Popover/Popover.js +175 -0
  74. package/components/Popover/Popover.lld.md +19 -0
  75. package/components/Popover/index.js +1 -0
  76. package/components/RadioButton/RadioButton.js +108 -0
  77. package/components/RadioButton/RadioButton.lld.md +15 -0
  78. package/components/RadioButton/index.js +1 -0
  79. package/components/Router/README.md +160 -0
  80. package/components/Router/Router.js +150 -0
  81. package/components/Router/Router.lld.md +31 -0
  82. package/components/Router/View.js +15 -0
  83. package/components/Router/index.js +2 -0
  84. package/components/Router/utils.js +17 -0
  85. package/components/Select/README.md +88 -0
  86. package/components/Select/Select.js +74 -0
  87. package/components/Select/Select.lld.md +20 -0
  88. package/components/Select/index.js +1 -0
  89. package/components/Table/README.md +94 -0
  90. package/components/Table/Table.js +171 -0
  91. package/components/Table/Table.lld.md +21 -0
  92. package/components/Table/index.js +1 -0
  93. package/components/TagList/Tag.js +84 -0
  94. package/components/TagList/TagList.js +118 -0
  95. package/components/TagList/TagList.lld.md +30 -0
  96. package/components/TagList/design.excalidraw.png +0 -0
  97. package/components/TagList/index.js +2 -0
  98. package/components/Tooltip/Tooltip.js +139 -0
  99. package/components/Tooltip/Tooltip.lld.md +22 -0
  100. package/components/Tooltip/index.js +1 -0
  101. package/components/TooltipWrapper/TooltipWrapper.js +89 -0
  102. package/components/TooltipWrapper/TooltipWrapper.lld.md +21 -0
  103. package/components/TooltipWrapper/index.js +1 -0
  104. package/components/Whiteboard/Whiteboard.js +198 -0
  105. package/components/Whiteboard/Whiteboard.lld.md +35 -0
  106. package/components/Whiteboard/index.js +1 -0
  107. package/components/index.js +27 -0
  108. package/eslint.config.cjs +118 -0
  109. package/index.d.ts +635 -0
  110. package/index.js +19 -0
  111. package/package.json +123 -0
  112. package/plugins/asText.js +38 -0
  113. package/plugins/loadPlugins.js +5 -0
  114. package/plugins/markdownLoader.js +121 -0
  115. package/prettier.config.cjs +7 -0
  116. package/spellcheck.config.cjs +227 -0
  117. package/styled/README.md +329 -0
  118. package/styled/appendStyles.js +26 -0
  119. package/styled/appendStyles.test.js +45 -0
  120. package/styled/index.js +4 -0
  121. package/styled/shimCSS.js +31 -0
  122. package/styled/shimCSS.test.js +103 -0
  123. package/styled/styled.js +91 -0
  124. package/styled/styled.test.js +586 -0
  125. package/styled/themeStyles.js +36 -0
  126. package/styled/themeStyles.test.js +135 -0
  127. package/test-setup.js +123 -0
  128. package/theme/.test.js +69 -0
  129. package/theme/README.md +607 -0
  130. package/theme/button.js +100 -0
  131. package/theme/code.js +123 -0
  132. package/theme/colors.js +42 -0
  133. package/theme/fonts.js +42 -0
  134. package/theme/index.js +33 -0
  135. package/theme/input.js +64 -0
  136. package/theme/page.js +208 -0
  137. package/theme/scrollbar.js +24 -0
  138. package/theme/table.js +53 -0
  139. package/utils/README.md +176 -0
  140. package/utils/browser.js +92 -0
  141. package/utils/class.js +30 -0
  142. package/utils/color.js +81 -0
  143. package/utils/data.js +164 -0
  144. package/utils/element.js +55 -0
  145. package/utils/index.js +7 -0
  146. package/utils/rand.js +12 -0
  147. package/utils/string.js +72 -0
package/README.md ADDED
@@ -0,0 +1,413 @@
1
+ # @vanilla-bean/components
2
+
3
+ A lightweight, reactive component library for building modern web applications with vanilla JavaScript. No build steps, no framework lock-in, no virtual DOM complexity.
4
+
5
+ ## Key Features
6
+
7
+ - **No build step for consumers** - Pure ES modules; import directly in browsers or bundle with any tool
8
+ - **Reactive state management** - Proxy-based, explicit; `component.options` is reactive out of the box, no dependency tracking magic
9
+ - **Scoped CSS styling** - Opinionated dark theme by design; override per-instance, per-class, or replace the theme entirely
10
+ - **Cleanup on disconnect** - no listener accumulation; components clean up after themselves
11
+ - **25 production-ready components** - A second tier of building blocks above the primitives. Button handles keyboard activation and pointer events. Dialog handles focus management and modal behavior. Form handles validation. These are composed concerns your app components build on, not reinvent.
12
+ - **Modern browser APIs** - EventTarget, Proxy, ES6 modules, native CSS nesting
13
+ - **Framework-agnostic** - works alongside Web Components, React islands, or any other approach; no conflicts, no opinions on your stack
14
+
15
+ ## Philosophy
16
+
17
+ Most UI frameworks insert themselves between your code and the DOM. VDOM reconcilers, reactive dependency graphs, compile-time transforms. Each layer runs so you don't have to think about it. The trade sounds appealing until something breaks: you're debugging the framework's decisions, in execution paths that aren't yours, with failure modes that are subtle by design.
18
+
19
+ VBC is built on a different premise. The DOM is stateful, and VBC accepts that and works with it directly. When data changes, you decide what updates, through `_setOption` handlers you write. Structure lives in `build()`. Data flows in through `options`. Updates happen exactly where you put them. No reconciliation loop. No dependency graph. No rerender cascade.
20
+
21
+ This is a position, not a limitation. Automatic systems don't eliminate complexity. They relocate it to places that are harder to reach when they break. VBC's failures are obvious: you forgot a key in `_setOption` and the UI doesn't update. You can see that immediately. The subtle failures, stale closures, dependency cycles, batching surprises, concurrent mode edge cases, belong to a different model entirely.
22
+
23
+ ## Table of Contents
24
+
25
+ - [Installation](#installation)
26
+ - [Interactive Explorer](#interactive-explorer)
27
+ - [Quick Start](#quick-start)
28
+ - [Core Architecture](#core-architecture)
29
+ - [Component Library](#component-library)
30
+ - [Real-World Example](#real-world-example)
31
+ - [Browser Compatibility](#browser-compatibility)
32
+ - [Development](#development)
33
+ - [Contributing](#contributing)
34
+
35
+ ## Installation
36
+
37
+ Install from GitHub:
38
+
39
+ ```bash
40
+ # Using bun
41
+ bun add github:fatlard1993/vanilla-bean-components
42
+
43
+ # Using npm
44
+ npm install github:fatlard1993/vanilla-bean-components
45
+
46
+ # Using yarn
47
+ yarn add github:fatlard1993/vanilla-bean-components
48
+ ```
49
+
50
+ Reactive state (`Oxject`, `derive`) is included in VBC, no separate install. The HTTP examples use [hypertether](https://github.com/fatlard1993/hypertether), which is a separate package:
51
+
52
+ ```bash
53
+ bun add @vanilla-bean/hypertether
54
+ ```
55
+
56
+ Import in your application:
57
+
58
+ ```js
59
+ import { Component, Oxject, styled } from '@vanilla-bean/components';
60
+
61
+ // Specific components
62
+ import { Button, Dialog, Calendar } from '@vanilla-bean/components';
63
+ ```
64
+
65
+ ## Interactive Explorer
66
+
67
+ `bun start` launches the component explorer at `http://localhost:9999` (clone first, see [Development](#development)):
68
+
69
+ - **Live option editor**: every component, every option, type-appropriate controls, updated in real time
70
+ - **Auto-generated API docs**: options tables, method signatures, event listings pulled directly from source annotations
71
+ - **11 real example applications**: async data tables with row actions, localStorage-backed todo, a live HTML/JS/CSS playground with iframe preview, a drawing canvas with a functioning in-game economy
72
+ - **Ancestor chain navigation**: each component shows its full inheritance (EventTarget → Elem → Component → ...) with links to each layer's documentation
73
+
74
+ The explorer is built with VBC itself. It uses the same primitives and patterns you'd use in your own app.
75
+
76
+ ## Quick Start
77
+
78
+ ### Basic Component Creation
79
+
80
+ Every component extends `EventTarget` and wraps an `HTMLElement`:
81
+
82
+ ```js
83
+ import { Component } from '@vanilla-bean/components';
84
+
85
+ const button = new Component({
86
+ tag: 'button',
87
+ textContent: 'Click me',
88
+ className: 'primary-btn',
89
+ onPointerPress: () => alert('Hello!'),
90
+ appendTo: document.body,
91
+ });
92
+ ```
93
+
94
+ ### Reactive State Management
95
+
96
+ Oxject is included in VBC. Import it alongside your components:
97
+
98
+ ```js
99
+ import { Component, Oxject } from '@vanilla-bean/components';
100
+
101
+ const user = new Oxject({ name: 'Alice', online: false });
102
+
103
+ new Component({
104
+ tag: 'div',
105
+ textContent: user.subscriber('name', name => `Welcome, ${name}!`),
106
+ className: user.subscriber('online', online => (online ? 'user-online' : 'user-offline')),
107
+ appendTo: document.body,
108
+ });
109
+
110
+ // UI updates automatically
111
+ user.name = 'Bob';
112
+ user.online = true;
113
+ ```
114
+
115
+ ### Styled Components with Theme Integration
116
+
117
+ Create scoped, themed components:
118
+
119
+ ```js
120
+ import { Component, styled } from '@vanilla-bean/components';
121
+
122
+ const Card = styled(
123
+ Component,
124
+ ({ colors }) => `
125
+ background: ${colors.white};
126
+ border-radius: 8px;
127
+ box-shadow: 0 2px 8px ${colors.black.setAlpha(0.1)};
128
+ padding: 24px;
129
+
130
+ &:hover {
131
+ transform: translateY(-2px);
132
+ transition: transform 0.2s ease;
133
+ }
134
+ `,
135
+ );
136
+
137
+ new Card({
138
+ append: [
139
+ new Component({ tag: 'h3', textContent: 'Card Title' }),
140
+ new Component({ tag: 'p', textContent: 'Card content goes here.' }),
141
+ ],
142
+ appendTo: document.body,
143
+ });
144
+ ```
145
+
146
+ ### Complete Reactive Application
147
+
148
+ ```js
149
+ import { Component, Oxject, styled } from '@vanilla-bean/components';
150
+
151
+ // Reactive state
152
+ const state = new Oxject({ count: 0 });
153
+
154
+ // Styled component
155
+ const Counter = styled(
156
+ Component,
157
+ ({ colors }) => `
158
+ display: flex;
159
+ gap: 12px;
160
+ padding: 16px;
161
+ background: ${colors.dark(colors.gray)};
162
+ border-radius: 6px;
163
+ `,
164
+ );
165
+
166
+ // Component with reactive options
167
+ new Counter({
168
+ textContent: state.subscriber('count', count => `Count: ${count}`),
169
+ append: [
170
+ new Component({
171
+ tag: 'button',
172
+ textContent: 'Increment',
173
+ onPointerPress: () => state.count++,
174
+ }),
175
+ new Component({
176
+ tag: 'button',
177
+ textContent: 'Reset',
178
+ onPointerPress: () => (state.count = 0),
179
+ }),
180
+ ],
181
+ appendTo: document.body,
182
+ });
183
+ ```
184
+
185
+ ## Core Architecture
186
+
187
+ ### Options-First Design Philosophy
188
+
189
+ Components configure through reactive options rather than imperative method calls:
190
+
191
+ ```js
192
+ // ❌ Traditional DOM manipulation
193
+ const elem = createElement('div');
194
+ elem.classList.add('container');
195
+ elem.style.padding = '20px';
196
+ elem.textContent = 'Hello';
197
+ elem.addEventListener('click', handler);
198
+ document.body.appendChild(elem);
199
+
200
+ // ✅ Options-first approach
201
+ new Component({
202
+ tag: 'div',
203
+ addClass: 'container',
204
+ style: { padding: '20px' },
205
+ textContent: 'Hello',
206
+ onPointerPress: handler,
207
+ appendTo: document.body,
208
+ });
209
+ ```
210
+
211
+ ### Core Modules
212
+
213
+ | Module | Purpose | Key Features |
214
+ | --- | --- | --- |
215
+ | **[Component](./Component/README.md)** | Enhanced DOM elements | Lifecycle management, reactive options, automatic cleanup |
216
+ | **Oxject** | Reactive state | Proxy-based reactivity, subscriber system, `derive()` for computed values |
217
+ | **[styled](./styled/README.md)** | Scoped CSS | Theme integration, native CSS nesting, unique class generation |
218
+ | **[Theme](./theme/README.md)** | Design system | Color manipulation, font management, accessibility helpers |
219
+
220
+ All four are available from `'@vanilla-bean/components'` directly. For HTTP caching, [hypertether](https://github.com/fatlard1993/hypertether) is a separate install (`bun add @vanilla-bean/hypertether`).
221
+
222
+ ## Component Library
223
+
224
+ 25 pre-built components handle common UI patterns:
225
+
226
+ ### Forms & Input
227
+
228
+ - **Input** - All HTML input types with validation
229
+ - **Select** - Dropdown selections with search
230
+ - **RadioButton** - Radio group input
231
+ - **Form** - Form management with validation
232
+ - **Label** - Associated labels with accessibility
233
+
234
+ ### Layout & Structure
235
+
236
+ - **Page** - Full-page layouts with header/footer
237
+ - **Router** - Client-side navigation
238
+ - **List** - Dynamic lists with filtering
239
+ - **Table** - Data tables with sorting
240
+
241
+ ### Interactive Elements
242
+
243
+ - **Button** - Action buttons with states
244
+ - **BottomSheet** - Mobile bottom sheet with drag-to-close gesture
245
+ - **Dialog** - Modal dialogs with backdrop
246
+ - **Menu** - Context and dropdown menus
247
+ - **Notify** - Toast-style notifications
248
+ - **Popover** - Floating content containers
249
+ - **Tooltip / TooltipWrapper** - Contextual help and information
250
+
251
+ ### Specialized Components
252
+
253
+ - **Calendar** - Date selection with events
254
+ - **ColorPicker** - Color selection interface
255
+ - **Keyboard** - Virtual keyboard input
256
+ - **Whiteboard** - Drawing and annotation canvas
257
+
258
+ ### Content & Display
259
+
260
+ - **Code** - Syntax-highlighted code blocks
261
+ - **Icon** - Icon system with Font Awesome integration
262
+ - **Link** - Enhanced anchor elements
263
+ - **TagList** - Tag management and display
264
+
265
+ ## Real-World Example
266
+
267
+ Complete user management interface with data loading and state management:
268
+
269
+ ```js
270
+ import { Component, Oxject, styled } from '@vanilla-bean/components';
271
+ import { GET } from '@vanilla-bean/hypertether';
272
+
273
+ // Application state
274
+ const app = new Oxject({
275
+ users: [],
276
+ loading: false,
277
+ selectedUserId: null,
278
+ });
279
+
280
+ // Styled components
281
+ const UserCard = styled(
282
+ Component,
283
+ ({ colors }) => `
284
+ padding: 16px;
285
+ border: 1px solid ${colors.light(colors.gray)};
286
+ border-radius: 6px;
287
+ cursor: pointer;
288
+
289
+ &:hover { background: ${colors.lightest(colors.blue)}; }
290
+ &.selected { border-color: ${colors.blue}; }
291
+ `,
292
+ );
293
+
294
+ const LoadingSpinner = styled(
295
+ Component,
296
+ () => `
297
+ display: inline-block;
298
+ animation: spin 1s linear infinite;
299
+ @keyframes spin { to { transform: rotate(360deg); } }
300
+ `,
301
+ );
302
+
303
+ // Data loading with automatic UI updates
304
+ async function loadUsers() {
305
+ app.loading = true;
306
+
307
+ const { body: users } = await GET('/api/users', {
308
+ onResponse: ({ body }) => (app.users = body), // Auto-refresh on cache invalidation
309
+ });
310
+
311
+ app.users = users;
312
+ app.loading = false;
313
+ }
314
+
315
+ // User list with conditional rendering
316
+ new Component({
317
+ className: 'user-list',
318
+ content: app.subscriber('loading', loading =>
319
+ loading
320
+ ? new LoadingSpinner({ textContent: 'Loading...' })
321
+ : app.subscriber('users', users =>
322
+ users.map(
323
+ user =>
324
+ new UserCard({
325
+ textContent: user.name,
326
+ className: app.subscriber('selectedUserId', selectedId => (selectedId === user.id ? 'selected' : '')),
327
+ onPointerPress: () => (app.selectedUserId = user.id),
328
+ }),
329
+ ),
330
+ ),
331
+ ),
332
+ appendTo: document.body,
333
+ });
334
+
335
+ loadUsers();
336
+ ```
337
+
338
+ ## Browser Compatibility
339
+
340
+ **Evergreen browsers only.** VBC uses native CSS nesting and requires no polyfills or transpilation:
341
+
342
+ | Feature | Chrome/Edge | Firefox | Safari |
343
+ | --------------------------- | ----------- | ------- | ------ |
344
+ | ES6 classes, modules, Proxy | 49+ | 18+ | 10+ |
345
+ | Native CSS nesting | 112+ | 117+ | 16.5+ |
346
+
347
+ **Effective minimum: Chrome/Edge 112, Firefox 117, Safari 16.5** (all released in 2023).
348
+
349
+ If you need older browser support, VBC is not the right tool. The native CSS nesting requirement is not negotiable without reintroducing a build-time CSS processing step.
350
+
351
+ ## Development
352
+
353
+ ### Local Development
354
+
355
+ ```bash
356
+ # Clone and install
357
+ git clone https://github.com/fatlard1993/vanilla-bean-components
358
+ cd vanilla-bean-components
359
+ bun install
360
+
361
+ # Start interactive explorer
362
+ bun start # Component explorer at http://localhost:9999
363
+
364
+ # Run tests
365
+ bun test # Full test suite
366
+ bun test:watch # Watch mode for development
367
+
368
+ # Build and validation
369
+ bun run build # Clean build with component indexing
370
+ bun run lint # ESLint validation
371
+ bun run format # Code formatting with Prettier
372
+ ```
373
+
374
+ ### Project Structure
375
+
376
+ ```
377
+ vanilla-bean-components/
378
+ ├── components/ # 25 pre-built UI components
379
+ ├── Component/ # Core Component class
380
+ ├── styled/ # Scoped styling system
381
+ ├── theme/ # Design tokens and helpers
382
+ ├── demo/ # Component explorer — live option editor, API docs, example apps
383
+ └── docs/ # Additional documentation
384
+ ```
385
+
386
+ ### Creating Components
387
+
388
+ Generate new components with full scaffolding:
389
+
390
+ ```bash
391
+ bun run create:component MyComponent
392
+ ```
393
+
394
+ This creates implementation, demo, tests, and documentation files following project patterns.
395
+
396
+ ## Contributing
397
+
398
+ 1. **Fork and clone** the repository
399
+ 2. **Create feature branch** from `main`
400
+ 3. **Follow existing patterns** - Check similar components for code style
401
+ 4. **Add comprehensive tests** - Use `@testing-library/dom` patterns
402
+ 5. **Update documentation** - Include README updates and demos
403
+ 6. **Run validation** - `bun run lint && bun test` before submitting
404
+
405
+ See individual component READMEs for architecture patterns and implementation guidelines.
406
+
407
+ ## License
408
+
409
+ MIT License - see [LICENSE](./LICENSE) file for details.
410
+
411
+ ---
412
+
413
+ **[Browse Components](./components/)** • **[API Documentation](./docs/)** • **[Live Demo](./demo/)** • **[Getting Started Guide](./docs/GETTING_STARTED.md)** • **[Design Philosophy](./docs/ETHOS.md)**
@@ -0,0 +1,192 @@
1
+ import { styled } from '../../styled';
2
+ import { Component } from '../../Component';
3
+ import { Elem } from '../../Elem';
4
+
5
+ const defaultOptions = {
6
+ get appendTo() {
7
+ return document.body;
8
+ },
9
+ };
10
+
11
+ /**
12
+ * Mobile-friendly bottom sheet with drag-to-close gesture.
13
+ *
14
+ * Mounts to document.body by default. Call show() to slide up, hide() to dismiss.
15
+ * Drag the handle downward past the threshold to dismiss.
16
+ * @param {object} [options={}] - BottomSheet configuration options
17
+ * @param {Function} [options.onClose] - Called when the sheet is dismissed
18
+ * @param {...(Component|HTMLElement|string)} children - Appended to the sheet body
19
+ */
20
+ class BottomSheet extends styled(
21
+ Component,
22
+ ({ colors }) => `
23
+ position: fixed;
24
+ bottom: 0;
25
+ left: 0;
26
+ right: 0;
27
+ display: flex;
28
+ flex-direction: column;
29
+ background: ${colors.darker(colors.gray)};
30
+ border-top: 1px solid ${colors.dark(colors.gray)};
31
+ max-height: 85vh;
32
+ z-index: 1000;
33
+ transform: translateY(100%);
34
+ transition: transform 0.3s ease;
35
+
36
+ &.open {
37
+ transform: translateY(0);
38
+ }
39
+
40
+ & .sheet-drag-zone {
41
+ flex-shrink: 0;
42
+ height: 32px;
43
+ display: flex;
44
+ align-items: center;
45
+ justify-content: center;
46
+ cursor: grab;
47
+ touch-action: none;
48
+ user-select: none;
49
+
50
+ &:active { cursor: grabbing; }
51
+
52
+ & .sheet-handle {
53
+ width: 40px;
54
+ height: 4px;
55
+ border-radius: 3px;
56
+ background: rgba(255,255,255,0.2);
57
+ pointer-events: none;
58
+ }
59
+ }
60
+
61
+ & .sheet-body {
62
+ flex: 1;
63
+ overflow-y: auto;
64
+ overscroll-behavior: contain;
65
+ }
66
+ `,
67
+ ) {
68
+ defaultOptions = { ...super.defaultOptions, ...defaultOptions };
69
+
70
+ // Empty handler prevents _standardSetOption from routing onClose through the event system.
71
+ // The value is accessed directly as this.options.onClose in hide().
72
+ static handlers = {
73
+ onClose() {},
74
+ };
75
+
76
+ constructor(options = {}, ...children) {
77
+ super({ ...defaultOptions, ...options }, ...children);
78
+ }
79
+
80
+ build() {
81
+ this._dragZone = new Elem({ tag: 'div', appendTo: this, addClass: 'sheet-drag-zone' });
82
+ new Elem({ tag: 'div', appendTo: this._dragZone, addClass: 'sheet-handle' });
83
+
84
+ this._body = new Component({ appendTo: this, addClass: 'sheet-body' });
85
+
86
+ this._initDragToClose();
87
+ }
88
+
89
+ /**
90
+ * Content area for child components. Append children here for scrollable content.
91
+ * @returns {Component} The sheet body component
92
+ */
93
+ get body() {
94
+ return this._body;
95
+ }
96
+
97
+ /**
98
+ * Slides the sheet into view and registers navigation listeners to auto-dismiss.
99
+ */
100
+ show() {
101
+ this.addClass('open');
102
+ if (!this._hideOnNavigate) {
103
+ this._hideOnNavigate = () => this.hide();
104
+ window.addEventListener('hashchange', this._hideOnNavigate, { once: true });
105
+ window.addEventListener('popstate', this._hideOnNavigate, { once: true });
106
+ this.replaceDestroyCleanup('hideOnNavigate', () => {
107
+ window.removeEventListener('hashchange', this._hideOnNavigate);
108
+ window.removeEventListener('popstate', this._hideOnNavigate);
109
+ this._hideOnNavigate = null;
110
+ });
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Slides the sheet out of view and calls onClose if set.
116
+ */
117
+ hide() {
118
+ this.removeClass('open');
119
+ if (this._hideOnNavigate) {
120
+ window.removeEventListener('hashchange', this._hideOnNavigate);
121
+ window.removeEventListener('popstate', this._hideOnNavigate);
122
+ this._hideOnNavigate = null;
123
+ this.replaceDestroyCleanup('hideOnNavigate', () => {});
124
+ }
125
+ this.options.onClose?.();
126
+ }
127
+
128
+ _initDragToClose() {
129
+ const el = this.elem;
130
+ const dragZone = this._dragZone.elem;
131
+ let startY = 0;
132
+
133
+ // Mouse drag
134
+ const onMouseMove = e => {
135
+ const dy = e.clientY - startY;
136
+ if (dy <= 0) return;
137
+ el.style.transition = 'none';
138
+ el.style.transform = `translateY(${dy}px)`;
139
+ };
140
+
141
+ const onMouseUp = e => {
142
+ window.removeEventListener('mousemove', onMouseMove);
143
+ window.removeEventListener('mouseup', onMouseUp);
144
+ el.style.transition = '';
145
+ el.style.transform = '';
146
+ const dy = e.clientY - startY;
147
+ if (dy > Math.min(100, el.offsetHeight * 0.25)) this.hide();
148
+ };
149
+
150
+ const onMouseDown = e => {
151
+ startY = e.clientY;
152
+ window.addEventListener('mousemove', onMouseMove);
153
+ window.addEventListener('mouseup', onMouseUp);
154
+ };
155
+
156
+ // Touch drag
157
+ const onTouchMove = e => {
158
+ const dy = e.touches[0].clientY - startY;
159
+ if (dy <= 0) return;
160
+ e.preventDefault();
161
+ el.style.transition = 'none';
162
+ el.style.transform = `translateY(${dy}px)`;
163
+ };
164
+
165
+ const onTouchEnd = e => {
166
+ el.style.transition = '';
167
+ el.style.transform = '';
168
+ const dy = e.changedTouches[0].clientY - startY;
169
+ if (dy > Math.min(100, el.offsetHeight * 0.25)) this.hide();
170
+ };
171
+
172
+ const onTouchStart = e => {
173
+ startY = e.touches[0].clientY;
174
+ };
175
+
176
+ dragZone.addEventListener('mousedown', onMouseDown);
177
+ dragZone.addEventListener('touchstart', onTouchStart, { passive: true });
178
+ dragZone.addEventListener('touchmove', onTouchMove, { passive: false });
179
+ dragZone.addEventListener('touchend', onTouchEnd, { passive: true });
180
+
181
+ this.replaceCleanup('dragToClose', () => {
182
+ dragZone.removeEventListener('mousedown', onMouseDown);
183
+ dragZone.removeEventListener('touchstart', onTouchStart);
184
+ dragZone.removeEventListener('touchmove', onTouchMove);
185
+ dragZone.removeEventListener('touchend', onTouchEnd);
186
+ window.removeEventListener('mousemove', onMouseMove);
187
+ window.removeEventListener('mouseup', onMouseUp);
188
+ });
189
+ }
190
+ }
191
+
192
+ export default BottomSheet;
@@ -0,0 +1,25 @@
1
+ # BottomSheet
2
+
3
+ > ./BottomSheet.js
4
+
5
+ Bottom sheet overlay that slides up from the bottom. Mounts to `document.body` by default. Drag down to dismiss; `hide()` closes it from code.
6
+
7
+ ## show() / hide() control visibility
8
+
9
+ - calling `show()` slides the sheet into view; calling `hide()` slides it out
10
+ - `onClose` fires when the sheet is dismissed (either by drag or by `hide()`)
11
+ - does calling hide() invoke the onClose callback?
12
+ - does the sheet animate out before calling onClose?
13
+
14
+ ## Drag below the threshold dismisses the sheet
15
+
16
+ - a drag handle at the top of the sheet captures pointer events; dragging past a distance threshold triggers `hide()`
17
+ - the sheet should not dismiss on small accidental drags
18
+ - does dragging the handle downward past the threshold dismiss the sheet?
19
+ - does a short drag that doesn't reach the threshold leave the sheet visible?
20
+
21
+ ## Navigating away (hashchange) closes the sheet
22
+
23
+ - the sheet registers a one-time `hashchange` listener on `show()` and removes it on `hide()`
24
+ - the sheet doesn't outlive navigation
25
+ - does navigating to a new hash while the sheet is open dismiss it?
@@ -0,0 +1,66 @@
1
+ # BottomSheet
2
+
3
+ Mobile bottom sheet with drag-to-close gesture. Mounts to `document.body` by default. Slides up from the bottom of the viewport; a downward drag past the threshold dismisses it.
4
+
5
+ ## Usage
6
+
7
+ ```js
8
+ import { BottomSheet } from '@vanilla-bean/components';
9
+
10
+ const sheet = new BottomSheet({
11
+ onClose: () => console.log('dismissed'),
12
+ append: new Component({ tag: 'p', textContent: 'Sheet content here.' }),
13
+ });
14
+
15
+ sheet.show();
16
+ ```
17
+
18
+ ## Options
19
+
20
+ | Option | Type | Default | Description |
21
+ | ---------- | ---------- | --------------- | ----------------------------------------------------------------- |
22
+ | `appendTo` | `Element` | `document.body` | Where to mount the sheet in the DOM |
23
+ | `onClose` | `Function` | — | Called when the sheet is dismissed — by drag or explicit `hide()` |
24
+
25
+ All standard `Component` options are supported.
26
+
27
+ ## Methods
28
+
29
+ ```js
30
+ sheet.show();
31
+ // Slides the sheet into view by adding the 'open' class.
32
+
33
+ sheet.hide();
34
+ // Slides the sheet out and calls options.onClose if set.
35
+ ```
36
+
37
+ ## Drag-to-close behavior
38
+
39
+ The top 60px of the sheet is the drag handle zone. A downward touch drag in this zone tracks the drag position in real time. Releasing past the threshold (120px or 25% of sheet height, whichever is smaller) dismisses the sheet via `hide()`.
40
+
41
+ Scroll position is respected: dragging is blocked while the sheet's content is scrolled away from the top, so normal scroll doesn't accidentally dismiss.
42
+
43
+ ## Example
44
+
45
+ Sheet opened by a button, dismissed by drag or close button:
46
+
47
+ ```js
48
+ import { BottomSheet, Button, Component } from '@vanilla-bean/components';
49
+
50
+ const sheet = new BottomSheet({
51
+ onClose: () => console.log('closed'),
52
+ append: [
53
+ new Component({ tag: 'h3', textContent: 'Options' }),
54
+ new Button({
55
+ textContent: 'Close',
56
+ onPointerPress: () => sheet.hide(),
57
+ }),
58
+ ],
59
+ });
60
+
61
+ new Button({
62
+ textContent: 'Open sheet',
63
+ onPointerPress: () => sheet.show(),
64
+ appendTo: document.body,
65
+ });
66
+ ```
@@ -0,0 +1 @@
1
+ export { default as BottomSheet } from './BottomSheet';