@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.
- package/Component/Component.js +598 -0
- package/Component/Component.scenarios.js +88 -0
- package/Component/Component.test.js +717 -0
- package/Component/README.md +455 -0
- package/Component/index.js +3 -0
- package/Component/observeElementConnection.js +52 -0
- package/Component/observeElementConnection.test.js +121 -0
- package/Elem/Elem.js +304 -0
- package/Elem/Elem.test.js +679 -0
- package/Elem/README.md +373 -0
- package/Elem/index.js +1 -0
- package/FontWithASyntaxHighlighter-Regular.woff2 +0 -0
- package/LICENSE +21 -0
- package/README.md +413 -0
- package/components/BottomSheet/BottomSheet.js +192 -0
- package/components/BottomSheet/BottomSheet.lld.md +25 -0
- package/components/BottomSheet/README.md +66 -0
- package/components/BottomSheet/index.js +1 -0
- package/components/Button/Button.js +53 -0
- package/components/Button/Button.lld.md +21 -0
- package/components/Button/index.js +1 -0
- package/components/Calendar/Calendar.js +720 -0
- package/components/Calendar/Calendar.lld.md +22 -0
- package/components/Calendar/CalendarEvent.js +102 -0
- package/components/Calendar/Toolbar.js +78 -0
- package/components/Calendar/index.js +2 -0
- package/components/Calendar/utils.js +56 -0
- package/components/Code/Code.js +84 -0
- package/components/Code/Code.lld.md +21 -0
- package/components/Code/index.js +1 -0
- package/components/ColorPicker/ColorPicker.js +445 -0
- package/components/ColorPicker/ColorPicker.lld.md +21 -0
- package/components/ColorPicker/index.js +1 -0
- package/components/ColorPicker/svg.js +5 -0
- package/components/Dialog/Dialog.js +278 -0
- package/components/Dialog/Dialog.lld.md +20 -0
- package/components/Dialog/README.md +96 -0
- package/components/Dialog/index.js +1 -0
- package/components/Form/Form.js +257 -0
- package/components/Form/Form.lld.md +21 -0
- package/components/Form/README.md +87 -0
- package/components/Form/index.js +1 -0
- package/components/Icon/Icon.js +54 -0
- package/components/Icon/Icon.lld.md +21 -0
- package/components/Icon/index.js +1 -0
- package/components/Input/Input.js +173 -0
- package/components/Input/Input.lld.md +28 -0
- package/components/Input/README.md +97 -0
- package/components/Input/index.js +2 -0
- package/components/Input/utils.js +122 -0
- package/components/Keyboard/Key.js +38 -0
- package/components/Keyboard/Keyboard.js +173 -0
- package/components/Keyboard/Keyboard.lld.md +21 -0
- package/components/Keyboard/index.js +1 -0
- package/components/Label/Label.js +214 -0
- package/components/Label/Label.lld.md +20 -0
- package/components/Label/index.js +1 -0
- package/components/Link/Link.js +43 -0
- package/components/Link/Link.lld.md +15 -0
- package/components/Link/index.js +1 -0
- package/components/List/List.js +82 -0
- package/components/List/List.lld.md +19 -0
- package/components/List/index.js +1 -0
- package/components/Menu/Menu.js +93 -0
- package/components/Menu/Menu.lld.md +15 -0
- package/components/Menu/index.js +1 -0
- package/components/Notify/Notify.js +96 -0
- package/components/Notify/Notify.lld.md +20 -0
- package/components/Notify/index.js +1 -0
- package/components/Page/Page.js +67 -0
- package/components/Page/Page.lld.md +20 -0
- package/components/Page/index.js +1 -0
- package/components/Popover/Popover.js +175 -0
- package/components/Popover/Popover.lld.md +19 -0
- package/components/Popover/index.js +1 -0
- package/components/RadioButton/RadioButton.js +108 -0
- package/components/RadioButton/RadioButton.lld.md +15 -0
- package/components/RadioButton/index.js +1 -0
- package/components/Router/README.md +160 -0
- package/components/Router/Router.js +150 -0
- package/components/Router/Router.lld.md +31 -0
- package/components/Router/View.js +15 -0
- package/components/Router/index.js +2 -0
- package/components/Router/utils.js +17 -0
- package/components/Select/README.md +88 -0
- package/components/Select/Select.js +74 -0
- package/components/Select/Select.lld.md +20 -0
- package/components/Select/index.js +1 -0
- package/components/Table/README.md +94 -0
- package/components/Table/Table.js +171 -0
- package/components/Table/Table.lld.md +21 -0
- package/components/Table/index.js +1 -0
- package/components/TagList/Tag.js +84 -0
- package/components/TagList/TagList.js +118 -0
- package/components/TagList/TagList.lld.md +30 -0
- package/components/TagList/design.excalidraw.png +0 -0
- package/components/TagList/index.js +2 -0
- package/components/Tooltip/Tooltip.js +139 -0
- package/components/Tooltip/Tooltip.lld.md +22 -0
- package/components/Tooltip/index.js +1 -0
- package/components/TooltipWrapper/TooltipWrapper.js +89 -0
- package/components/TooltipWrapper/TooltipWrapper.lld.md +21 -0
- package/components/TooltipWrapper/index.js +1 -0
- package/components/Whiteboard/Whiteboard.js +198 -0
- package/components/Whiteboard/Whiteboard.lld.md +35 -0
- package/components/Whiteboard/index.js +1 -0
- package/components/index.js +27 -0
- package/eslint.config.cjs +118 -0
- package/index.d.ts +635 -0
- package/index.js +19 -0
- package/package.json +123 -0
- package/plugins/asText.js +38 -0
- package/plugins/loadPlugins.js +5 -0
- package/plugins/markdownLoader.js +121 -0
- package/prettier.config.cjs +7 -0
- package/spellcheck.config.cjs +227 -0
- package/styled/README.md +329 -0
- package/styled/appendStyles.js +26 -0
- package/styled/appendStyles.test.js +45 -0
- package/styled/index.js +4 -0
- package/styled/shimCSS.js +31 -0
- package/styled/shimCSS.test.js +103 -0
- package/styled/styled.js +91 -0
- package/styled/styled.test.js +586 -0
- package/styled/themeStyles.js +36 -0
- package/styled/themeStyles.test.js +135 -0
- package/test-setup.js +123 -0
- package/theme/.test.js +69 -0
- package/theme/README.md +607 -0
- package/theme/button.js +100 -0
- package/theme/code.js +123 -0
- package/theme/colors.js +42 -0
- package/theme/fonts.js +42 -0
- package/theme/index.js +33 -0
- package/theme/input.js +64 -0
- package/theme/page.js +208 -0
- package/theme/scrollbar.js +24 -0
- package/theme/table.js +53 -0
- package/utils/README.md +176 -0
- package/utils/browser.js +92 -0
- package/utils/class.js +30 -0
- package/utils/color.js +81 -0
- package/utils/data.js +164 -0
- package/utils/element.js +55 -0
- package/utils/index.js +7 -0
- package/utils/rand.js +12 -0
- 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';
|