@vanilla-bean/components 1.1.1 → 2.0.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 (52) hide show
  1. package/Component/Component.js +222 -36
  2. package/Component/Component.test.js +425 -38
  3. package/Component/README.md +480 -0
  4. package/Elem/README.md +373 -0
  5. package/README.md +1 -1
  6. package/components/BottomSheet/BottomSheet.js +8 -16
  7. package/components/Button/Button.js +12 -18
  8. package/components/Calendar/Calendar.js +139 -59
  9. package/components/Calendar/CalendarEvent.js +9 -4
  10. package/components/Calendar/Toolbar.js +28 -20
  11. package/components/Calendar/index.js +1 -0
  12. package/components/Code/Code.js +26 -30
  13. package/components/ColorPicker/ColorPicker.js +57 -57
  14. package/components/Dialog/Dialog.js +78 -72
  15. package/components/Form/Form.js +12 -8
  16. package/components/Icon/Icon.js +19 -11
  17. package/components/Input/Input.js +85 -67
  18. package/components/Input/README.md +0 -2
  19. package/components/Keyboard/Key.js +7 -10
  20. package/components/Keyboard/Keyboard.js +38 -46
  21. package/components/Label/Label.js +52 -50
  22. package/components/Link/Link.js +14 -20
  23. package/components/List/List.js +28 -30
  24. package/components/Menu/Menu.js +19 -10
  25. package/components/Menu/Menu.lld.md +2 -2
  26. package/components/Notify/Notify.js +29 -19
  27. package/components/Popover/Popover.js +49 -44
  28. package/components/RadioButton/RadioButton.js +33 -29
  29. package/components/Router/Router.js +19 -21
  30. package/components/Select/Select.js +24 -30
  31. package/components/Table/Table.js +55 -36
  32. package/components/TagList/Tag.js +16 -14
  33. package/components/TagList/TagList.js +15 -8
  34. package/components/Tooltip/Tooltip.js +12 -25
  35. package/components/TooltipWrapper/TooltipWrapper.js +55 -49
  36. package/components/Whiteboard/Whiteboard.js +45 -43
  37. package/devTools/build.js +43 -0
  38. package/devTools/buildTypes.js +322 -0
  39. package/devTools/createComponent.js +155 -0
  40. package/devTools/extractJSDoc.js +395 -0
  41. package/devTools/processTemplate.js +500 -0
  42. package/devTools/updateComponentIndex.js +16 -0
  43. package/devTools/updateDemoViewIndex.js +90 -0
  44. package/eslint.config.cjs +6 -3
  45. package/index.d.ts +117 -59
  46. package/package.json +67 -22
  47. package/spellcheck.config.cjs +4 -0
  48. package/styled/README.md +329 -0
  49. package/theme/colors.js +15 -12
  50. package/theme/colors.test.js +28 -0
  51. package/utils/element.js +2 -2
  52. package/FontWithASyntaxHighlighter-Regular.woff2 +0 -0
@@ -0,0 +1,480 @@
1
+ # Component
2
+
3
+ Reactive component class extending Elem with Oxject-backed reactive options, lifecycle management, and automatic cleanup.
4
+
5
+ ## Key Features
6
+
7
+ - **Reactive options system** - Property changes backed by Oxject emit events and update DOM automatically
8
+ - **Automatic memory management** - Element-level listeners persist through DOM moves, cleaned up on destroy. External resources (timers, subscriptions) cleaned up on disconnect
9
+ - **Flexible render timing** - Immediate, onload, or animationFrame rendering modes
10
+ - **Enhanced event handling** - Input events include `.value` property, DOM connection detection
11
+ - **Integrated styling** - Inline objects or scoped CSS with theme system integration
12
+ - **Lifecycle hooks** - Built-in hover and press handlers with automatic cleanup
13
+
14
+ ```js
15
+ import { Component } from '@vanilla-bean/components';
16
+ ```
17
+
18
+ ## Basic Usage
19
+
20
+ ### Simple Components
21
+
22
+ Every Component extends EventTarget and wraps an HTMLElement with reactive options:
23
+
24
+ ```js
25
+ import { Component } from '@vanilla-bean/components';
26
+
27
+ const button = new Component({
28
+ tag: 'button',
29
+ textContent: 'Click me',
30
+ className: 'primary-btn',
31
+ onPointerPress: () => alert('Hello!'),
32
+ appendTo: document.body,
33
+ });
34
+ ```
35
+
36
+ ### Reactive Property Updates
37
+
38
+ Options are backed by Oxject. Property changes trigger DOM updates automatically:
39
+
40
+ ```js
41
+ const input = new Component({
42
+ tag: 'input',
43
+ value: 'Initial text',
44
+ onChange: ({ value }) => {
45
+ input.options.value = value; // Updates DOM reactively
46
+ },
47
+ appendTo: document.body,
48
+ });
49
+
50
+ // Display that updates automatically
51
+ new Component({
52
+ tag: 'p',
53
+ textContent: input.options.subscriber('value', value => `Current: ${value}`),
54
+ appendTo: document.body,
55
+ });
56
+ ```
57
+
58
+ ### Extended Component Classes
59
+
60
+ Create reusable component classes with custom behavior:
61
+
62
+ ```js
63
+ class Button extends Component {
64
+ static schema = {
65
+ tag: { default: 'button' },
66
+ mode: {
67
+ default: 'primary',
68
+ enum: ['primary', 'secondary'],
69
+ set(value) {
70
+ this.removeClass(/\bmode-\S+/g).addClass(`mode-${value}`);
71
+ },
72
+ },
73
+ };
74
+ }
75
+ ```
76
+
77
+ ### With Styled Components
78
+
79
+ Components integrate seamlessly with the styled system:
80
+
81
+ ```js
82
+ import { styled } from '@vanilla-bean/components';
83
+
84
+ const StyledComponent = styled(
85
+ Component,
86
+ ({ colors }) => `
87
+ background: ${colors.blue};
88
+ color: ${colors.white};
89
+ padding: 16px;
90
+ border-radius: 8px;
91
+ `,
92
+ );
93
+ ```
94
+
95
+ ## Reactive Options System
96
+
97
+ Component options are stored in an Oxject instance (not a plain object like Elem), enabling automatic reactivity:
98
+
99
+ ```js
100
+ const button = new Component({ textContent: 'Click me' });
101
+
102
+ // Property changes trigger DOM updates
103
+ button.options.textContent = 'Updated'; // Updates DOM reactively
104
+ button.options.addClass = 'active'; // Adds CSS class reactively
105
+
106
+ // Full Oxject features available
107
+ const subscription = button.options.subscribe({
108
+ key: 'textContent',
109
+ callback: value => console.log('Text changed:', value),
110
+ });
111
+ ```
112
+
113
+ ### Option Processing Pipeline
114
+
115
+ Options are routed through `_setOption` based on key and value type:
116
+
117
+ | Condition | Routing | Example |
118
+ | ------------------------------ | -------------------------- | ------------------------- |
119
+ | Schema `set` / `data` / `enum` | Schema chain (see below) | `variant: 'button'` |
120
+ | Key starts with 'on' | Event handler registration | `onPointerPress: handler` |
121
+ | Schema `attribute: true` | `elem.setAttribute()` | `colspan: '2'` |
122
+ | Component has method | Method call with value | `addClass: 'active'` |
123
+ | HTMLElement has property | Direct elem property | `textContent: 'hello'` |
124
+ | Value is function | Component property | `customMethod: fn` |
125
+ | Default | Elem property assignment | `customProp: value` |
126
+
127
+ ## Event Handling
128
+
129
+ ### Automatic Event Registration
130
+
131
+ Event handlers are registered automatically when option keys start with 'on':
132
+
133
+ ```js
134
+ new Component({
135
+ onPointerPress: event => console.log('pressed'),
136
+ onChange: ({ value }) => console.log('value:', value), // Input events include .value
137
+ onConnected: () => console.log('added to DOM'),
138
+ onDisconnected: () => console.log('removed from DOM'),
139
+ });
140
+ ```
141
+
142
+ > **Note:** `click` is not in the supported event set. Use `onPointerPress` for press interactions.
143
+
144
+ ### Enhanced Input Events
145
+
146
+ Input-related events (`keydown`, `keyup`, `change`, `blur`, `input`, `search`) receive enhanced event objects:
147
+
148
+ ```js
149
+ new Component({
150
+ tag: 'input',
151
+ type: 'text',
152
+ onChange: ({ value, event }) => {
153
+ console.log('New value:', value); // Extracted from input
154
+ console.log('Original event:', event);
155
+ },
156
+ });
157
+ ```
158
+
159
+ ### Connection Events
160
+
161
+ DOM connection/disconnection detected via MutationObserver:
162
+
163
+ ```js
164
+ new Component({
165
+ onConnected: () => console.log('Component added to DOM'),
166
+ onDisconnected: () => {
167
+ console.log('Component removed - cleanup triggered');
168
+ },
169
+ });
170
+ ```
171
+
172
+ ### Specialized Event Handlers
173
+
174
+ #### Hover with Movement Tracking
175
+
176
+ ```js
177
+ component.onHover(event => {
178
+ console.log('Mouse position:', event.clientX, event.clientY);
179
+ });
180
+ ```
181
+
182
+ #### Press Detection
183
+
184
+ Fires on `pointerdown` for immediate, reliable response in all contexts including scroll containers and modal dialogs:
185
+
186
+ ```js
187
+ component.onPointerPress(event => {
188
+ console.log('Pressed');
189
+ });
190
+ ```
191
+
192
+ ## Styling
193
+
194
+ ### Inline Style Objects
195
+
196
+ Apply styles as JavaScript objects:
197
+
198
+ ```js
199
+ component.styles({
200
+ color: 'red',
201
+ padding: '10px',
202
+ backgroundColor: '#f0f0f0',
203
+ });
204
+
205
+ // Or as option
206
+ new Component({
207
+ styles: { color: 'red', padding: '10px' },
208
+ });
209
+ ```
210
+
211
+ ### Scoped CSS with Theme Integration
212
+
213
+ Use theme functions for scoped, processed CSS:
214
+
215
+ ```js
216
+ // Theme function with automatic scoping
217
+ component.styles(
218
+ ({ colors, fonts }) => `
219
+ background: ${colors.blue};
220
+ ${fonts.kodeMono}
221
+ padding: 16px;
222
+ border-radius: 8px;
223
+
224
+ &:hover {
225
+ background: ${colors.darker(colors.blue)};
226
+ }
227
+
228
+ @media (max-width: 768px) {
229
+ padding: 8px;
230
+ }
231
+ `,
232
+ );
233
+ ```
234
+
235
+ CSS is scope-wrapped and injected as a scoped <style> tag. No preprocessing.
236
+
237
+ ## Lifecycle Management
238
+
239
+ ### Render Lifecycle
240
+
241
+ `render()` orchestrates a fixed sequence every time it is called:
242
+
243
+ ```
244
+ render()
245
+ ├─ if (this.rendered) ← re-render only: clears children, runs descendant cleanup
246
+ │ empty()
247
+ │ this.rendered = false
248
+ ├─ build() ← subclass structural hook, runs before options are applied
249
+ ├─ _processOptions() ← routes every option through _setOption()
250
+ │ priority keys first (onConnected, textContent, content, appendTo, prependTo, value)
251
+ │ then all remaining keys
252
+ └─ this.rendered = true
253
+ ```
254
+
255
+ **`build()` is the subclass structural hook.** Override `build()`, never `render()`, to create child elements and internal structure. Because `build()` runs before `_processOptions()`, all structure exists by the time `_setOption` receives values.
256
+
257
+ The preferred way to declare options is `static schema` - a per-class schema defining each key once: what can exist, its default, and how it routes. Schemas compose across the constructor chain, with child classes overriding parents per-key:
258
+
259
+ ```js
260
+ class Card extends Component {
261
+ build() {
262
+ this.header = new Component({ tag: 'header', appendTo: this });
263
+ this.body = new Component({ tag: 'section', appendTo: this });
264
+ }
265
+
266
+ static schema = {
267
+ title: {
268
+ default: 'Untitled',
269
+ set(value) {
270
+ this.header.options.textContent = value;
271
+ },
272
+ },
273
+ variant: {
274
+ set(value) {
275
+ this.removeClass(/variant-\S+/).addClass(`variant-${value}`);
276
+ },
277
+ },
278
+ // Component data only - stored in this.options, never routed to the elem
279
+ records: {},
280
+ };
281
+ }
282
+ ```
283
+
284
+ Descriptor fields:
285
+
286
+ - `default` - initial value, merged automatically at construction (no manual `{ ...defaultOptions, ...options }` spread needed). Use a getter (`get default() { return document.body; }`) for values that must evaluate fresh per instance.
287
+ - `set(value, next)` - change handler, called on initial render and every reactive update. Not calling `next(value)` fully owns the key; calling it continues down the chain to parent handlers, then standard routing.
288
+ - A bare `{}` declares a data-only key: declared keys never fall through to fallback guessing, so with no DOM match they live in `this.options` - stored, subscribable, silent. Keys with a real DOM match (Input's `type`, an `onChange` default) still route to it. Reserve bare `{}` for keys whose initial state is genuinely dynamic (computed from other state at use time), optional callbacks where absence is meaningful, and required inputs - everything else declares its `default`, so the read contract lives in the schema instead of scattered `||` fallbacks at consumers. Use a getter default (`get default() { return []; }`) for mutable containers so instances never share one.
289
+ - `data: true` - force store-only routing. Only needed when a data key's name collides with a real DOM property or method that standard routing would otherwise hit (e.g. a data key named `title`).
290
+ - `enum: ['small', 'large']` - the closed set of valid values. Assigning anything else (other than null/undefined) throws, listing the valid values. Readable via `component.optionEnum(key)` - the demo's options editor uses this to render value pickers.
291
+
292
+ Inheritance semantics: `set` functions chain across the class hierarchy (deepest first), while scalar fields (`default`, `enum`, `data`, `attribute`, `priority`) are decided by the nearest class whose descriptor declares them - so a subclass can override a parent default, opt out of a flag with `data: false`, or lift a constraint with `enum: null`, and a subclass that only adds a `set` leaves the parent's scalars intact.
293
+
294
+ Two keys are construction-only: `tag` and `autoRender` accept schema defaults but are consumed before the reactive store is created, so they never appear in `this.options` and cannot change after construction.
295
+
296
+ Unhandled keys fall through to standard routing automatically - no `super._setOption` required.
297
+
298
+ Two more statics complete the declaration surface, and together they eliminate the need for component constructors in almost every case:
299
+
300
+ - `static events = ['select']` - custom event names usable with `on()`/`emit()`. Unioned up the class chain, so a subclass adds to its parent's events instead of replacing them.
301
+ - `static prepareOptions(options, children)` - a pure transform run on the merged (schema defaults + user) options before processing, leaf-first up the class chain. This is the home for computed defaults: Input derives `type` from the value's type here, Code decides its tag from `multiline`, Table normalizes string columns. Because the hook sees merged options, ancestor hooks see subclass schema defaults (Input's hook sees Select's `tag: 'select'`).
302
+
303
+ A fully declared component needs no constructor at all: `static schema` + `static events` + `prepareOptions` + `build()` cover declaration, wiring, and structure. Constructors remain only for genuine construction-time work (Page's eager stylesheet loading, Label's string-shorthand normalization).
304
+
305
+ `_setOption` override still exists for cases that must intercept before the routing chain.
306
+
307
+ ### Derived option patterns
308
+
309
+ There is deliberately no `derive` descriptor. When one option's value depends on another, pick the pattern that matches when the derivation must hold:
310
+
311
+ - **Construction-time** - compute in `prepareOptions`. Right when the result feeds construction itself and never changes after (Code decides its `tag` from `multiline`; Notify picks `icon` and `role` from `type`).
312
+ - **Read-time** - normalize where the value is consumed. Right when the option accepts shorthand and can change reactively; construction and reactive assignment then share one path (Table normalizes string columns inside `_renderTable`).
313
+ - **Change-time** - recompute inside the source key's `set`. Right when a change to one option must immediately update dependent state (Table's `sortDirection` re-sorts `data`).
314
+
315
+ **`empty()` only runs on re-render.** On the initial render it is skipped. On subsequent `render()` calls it removes all child elements and runs cleanup on all descendant components before the DOM is cleared.
316
+
317
+ **`async build()` is not supported.** `render()` does not await `build()`'s return value. Asynchronous initialization belongs in `onConnected`.
318
+
319
+ **Deep hierarchies must chain `super.build()` manually.** If both a parent class and its subclass define `build()`, the subclass must call `super.build()` explicitly. It is not called automatically.
320
+
321
+ ### Rendering Process (summary)
322
+
323
+ ```js
324
+ component.render(); // Re-render: empty → build → _processOptions → rendered = true
325
+ ```
326
+
327
+ Rendering behavior:
328
+
329
+ 1. **Content clearing** - `empty()` removes existing children on re-render only (skipped on first render)
330
+ 2. **Structure creation** - `build()` creates sub-elements before options are applied
331
+ 3. **Priority processing** - `_processOptions()` applies `priority: true` schema keys (and framework priority keys) first
332
+ 4. **Option routing** - Each option processed via `_setOption`
333
+ 5. **Completion flag** - Sets `rendered = true`
334
+
335
+ ### Automatic Cleanup System
336
+
337
+ Components automatically clean up resources when removed from DOM:
338
+
339
+ ```js
340
+ // Manual cleanup registration
341
+ component.addCleanup('unique-id', () => {
342
+ console.log('Custom cleanup executed');
343
+ });
344
+
345
+ // Manual cleanup execution (automatic on disconnect)
346
+ component.processCleanup();
347
+ ```
348
+
349
+ **Automatic cleanup includes:**
350
+
351
+ - Event listener removal
352
+ - Oxject subscription cleanup
353
+ - Style element removal from DOM
354
+ - Recursive child component cleanup
355
+
356
+ ### Render Timing Control
357
+
358
+ Control when components render with `autoRender` option:
359
+
360
+ ```js
361
+ // Immediate rendering (default)
362
+ new Component({ autoRender: true });
363
+
364
+ // Render on window load event
365
+ new Component({ autoRender: 'onload' });
366
+
367
+ // Render on next animation frame
368
+ new Component({ autoRender: 'animationFrame' });
369
+
370
+ // Manual rendering only
371
+ new Component({ autoRender: false });
372
+ ```
373
+
374
+ ## API Reference
375
+
376
+ ### Constructor
377
+
378
+ ```js
379
+ new Component(options?, ...children)
380
+ ```
381
+
382
+ **Parameters:**
383
+
384
+ - `options` (Object) - Configuration options
385
+ - `children` (...Component|Elem) - Child elements appended to `append` option
386
+
387
+ ### Configuration Options
388
+
389
+ #### Component-Specific Options
390
+
391
+ | Option | Type | Description |
392
+ | ------------ | ------------------------------------- | ------------------------------------------- |
393
+ | `tag` | `string` | HTML tag name (default: 'div') |
394
+ | `autoRender` | `boolean\|'onload'\|'animationFrame'` | Render timing control |
395
+ | `styles` | `string\|object\|Function` | CSS string, style object, or theme function |
396
+ | `uniqueId` | `string` | Override auto-generated unique ID |
397
+
398
+ Custom events (`static events`), attribute routing (`attribute: true`), and processing priority (`priority: true`) are declared in the class, not passed as options.
399
+
400
+ #### Event Handler Options
401
+
402
+ | Option | Type | Description |
403
+ | ---------------- | ---------- | -------------------------------------------- |
404
+ | `onHover` | `Function` | Hover handler with move tracking |
405
+ | `onPointerPress` | `Function` | Fires on `pointerdown` |
406
+ | ~~`onClick`~~ | - | Not supported. Use `onPointerPress` instead. |
407
+ | `onChange` | `Function` | Change event handler (input .value included) |
408
+ | `onConnected` | `Function` | DOM connection detection |
409
+ | `onDisconnected` | `Function` | DOM disconnection detection |
410
+
411
+ All standard Elem options work as reactive Component options.
412
+
413
+ ### Methods
414
+
415
+ #### Options-Compatible Methods
416
+
417
+ Can be called directly or used as reactive options:
418
+
419
+ ```js
420
+ component.styles(styleFunction); // Direct call
421
+ component.onHover(callback); // Direct call
422
+ component.addClass('active'); // Direct call
423
+ component.append(child); // Direct call
424
+
425
+ // Or as reactive options:
426
+ new Component({
427
+ styles: styleFunction,
428
+ onHover: callback,
429
+ addClass: 'active',
430
+ append: [child],
431
+ });
432
+ ```
433
+
434
+ #### Lifecycle Methods
435
+
436
+ ```js
437
+ component.build(); // Subclass structural hook - override to create child elements
438
+ component.render(); // Re-render: empty → build → _processOptions → rendered = true
439
+ component.addCleanup(id, fn); // Register cleanup function (chains with existing for same id)
440
+ component.replaceCleanup(id, fn); // Replace cleanup, running the previous one immediately
441
+ component.replaceDestroyCleanup(id, fn); // Destroy-only cleanup: survives disconnect, runs on destroy()
442
+ component.processCleanup(); // Execute all cleanup functions
443
+ component.destroy(); // Disconnect observer, run all cleanup, remove from DOM
444
+ ```
445
+
446
+ #### Event Methods
447
+
448
+ ```js
449
+ component.on({ targetEvent, callback, id? }); // Event registration
450
+ component.emit(eventType, detail); // Event dispatch
451
+ ```
452
+
453
+ #### Utility Methods
454
+
455
+ ```js
456
+ component.ancestry(); // Prototype chain inspection
457
+ ```
458
+
459
+ ### Properties
460
+
461
+ ```js
462
+ component.parent; // Parent Component/Elem instance
463
+ component.children; // Child Component/Elem instances
464
+ component.uniqueId; // Frozen unique identifier
465
+ component.options; // Reactive Oxject instance
466
+ component.elem; // Underlying HTMLElement
467
+ component.rendered; // Boolean render status flag
468
+ ```
469
+
470
+ ## Memory Management
471
+
472
+ Components provide comprehensive automatic memory management:
473
+
474
+ - **Event listeners** - Automatically removed on DOM disconnect
475
+ - **Oxject subscriptions** - Cleaned up when component destroyed
476
+ - **Style elements** - Removed from DOM head on cleanup
477
+ - **Child components** - Recursive cleanup propagation
478
+ - **Custom cleanup** - Manual cleanup function registration
479
+
480
+ No manual cleanup required for typical usage - components handle resource management automatically.