@vanilla-bean/components 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,12 +18,12 @@ const StyledList = styled(
18
18
  }
19
19
 
20
20
  &:hover, &:focus, &:focus-visible, & a:hover, & a:focus, & a:focus-visible {
21
- color: ${colors.light(colors.blue)} !important;
22
- border-color: ${colors.light(colors.blue)};
21
+ color: ${colors.light(colors.selected)} !important;
22
+ border-color: ${colors.light(colors.selected)};
23
23
  }
24
24
 
25
25
  &:focus-visible, & a:focus-visible {
26
- outline: 2px solid ${colors.light(colors.blue)};
26
+ outline: 2px solid ${colors.light(colors.selected)};
27
27
  outline-offset: -2px;
28
28
  }
29
29
  }
package/index.d.ts CHANGED
@@ -25,6 +25,7 @@ export interface ThemeColors {
25
25
  teal: ThemeColor;
26
26
  white: ThemeColor;
27
27
  black: ThemeColor;
28
+ selected: ThemeColor;
28
29
  transparent: ThemeColor;
29
30
  superWhite: ThemeColor;
30
31
  vantablack: ThemeColor;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vanilla-bean/components",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "module": "./index.js",
6
6
  "types": "./index.d.ts",
package/theme/.test.js CHANGED
@@ -2,11 +2,14 @@ import { TinyColor } from '@ctrl/tinycolor';
2
2
  import theme from '.';
3
3
 
4
4
  const ORIGINAL_BLUE = 'hsl(209, 55%, 45%)';
5
+ const ORIGINAL_YELLOW = 'hsl(44, 55%, 45%)';
5
6
  const ORIGINAL_GRAY = 'hsl(0, 0%, 45%)';
6
7
 
7
8
  afterEach(() => {
8
9
  theme.colors.blue = new TinyColor(ORIGINAL_BLUE);
10
+ theme.colors.yellow = new TinyColor(ORIGINAL_YELLOW);
9
11
  theme.colors.gray = new TinyColor(ORIGINAL_GRAY);
12
+ theme.colors.selected = undefined;
10
13
  });
11
14
 
12
15
  describe('theme colors', () => {
@@ -21,6 +24,19 @@ describe('theme colors', () => {
21
24
  theme.colors.gray = new TinyColor('hsl(200, 30%, 45%)');
22
25
  expect(theme.colors.white.toString()).not.toBe(before);
23
26
  });
27
+
28
+ test('selected follows yellow — updates when yellow is reassigned', () => {
29
+ const custom = new TinyColor('hsl(120, 55%, 45%)');
30
+ theme.colors.yellow = custom;
31
+ expect(theme.colors.selected.toString()).toBe(custom.toString());
32
+ });
33
+
34
+ test('selected can be assigned independently of yellow', () => {
35
+ const custom = new TinyColor('hsl(280, 55%, 45%)');
36
+ theme.colors.selected = custom;
37
+ expect(theme.colors.selected.toString()).toBe(custom.toString());
38
+ expect(theme.colors.yellow.toString()).toBe(new TinyColor(ORIGINAL_YELLOW).toString());
39
+ });
24
40
  });
25
41
 
26
42
  describe('theme lazy strings', () => {
@@ -32,19 +48,19 @@ describe('theme lazy strings', () => {
32
48
 
33
49
  test('theme.table reflects a color mutation', () => {
34
50
  const before = theme.table;
35
- theme.colors.blue = new TinyColor('hsl(120, 55%, 45%)');
51
+ theme.colors.yellow = new TinyColor('hsl(120, 55%, 45%)');
36
52
  expect(theme.table).not.toBe(before);
37
53
  });
38
54
 
39
55
  test('theme.scrollbar reflects a color mutation', () => {
40
56
  const custom = new TinyColor('hsl(120, 55%, 45%)');
41
- theme.colors.blue = custom;
57
+ theme.colors.yellow = custom;
42
58
  expect(theme.scrollbar).toContain(custom.toString());
43
59
  });
44
60
 
45
61
  test('theme.input reflects a color mutation', () => {
46
62
  const custom = new TinyColor('hsl(120, 55%, 45%)');
47
- theme.colors.blue = custom;
63
+ theme.colors.yellow = custom;
48
64
  expect(theme.input).toContain(custom.toString());
49
65
  });
50
66
 
package/theme/README.md CHANGED
@@ -284,6 +284,12 @@ colors.black; // Pure black - text on light backgrounds
284
284
  colors.superWhite; // #fefefe - slightly warmer white
285
285
  colors.vantablack; // #0a0a0a - rich black alternative
286
286
 
287
+ // Semantic accent - the "focused/selected/active" color (focus outlines,
288
+ // ::selection, hover highlights, checked controls, scrollbar thumbs).
289
+ // Follows colors.yellow unless assigned its own color:
290
+ colors.selected;
291
+ theme.colors.selected = new TinyColor('#3d7aed'); // decouple from yellow
292
+
287
293
  // Accessibility functions
288
294
  colors.mostReadable(baseColor, [colors.white, colors.black]);
289
295
  // Returns the highest contrast color for optimal readability
@@ -566,6 +572,7 @@ interface ColorSystem {
566
572
  transparent: TinyColor;
567
573
  white: TinyColor;
568
574
  black: TinyColor;
575
+ selected: TinyColor;
569
576
  superWhite: TinyColor;
570
577
  vantablack: TinyColor;
571
578
 
package/theme/button.js CHANGED
@@ -6,7 +6,7 @@ export default ({ colors, fonts }) => `
6
6
  text-decoration: none;
7
7
  color: ${colors.white};
8
8
  background-color: ${colors.blue};
9
- outline-color: ${colors.lighter(colors.orange)};
9
+ outline-color: ${colors.lighter(colors.selected)};
10
10
  text-align: center;
11
11
  position: relative;
12
12
  white-space: nowrap;
package/theme/colors.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import { TinyColor, random, readability, isReadable, mostReadable } from '@ctrl/tinycolor';
2
2
 
3
+ let selectedOverride;
4
+
3
5
  const colors = {
4
6
  random,
5
7
  readability,
6
8
  isReadable,
7
9
  mostReadable,
8
10
 
9
- // Base colors — plain writable properties so theme.colors.X = new TinyColor(...) works
10
11
  orange: new TinyColor('hsl(29, 55%, 45%)'),
11
12
  gray: new TinyColor('hsl(0, 0%, 45%)'),
12
13
  yellow: new TinyColor('hsl(44, 55%, 45%)'),
@@ -20,12 +21,18 @@ const colors = {
20
21
  superWhite: new TinyColor('hsl(0, 100%, 100%)'),
21
22
  vantablack: new TinyColor('hsl(0, 0%, 0%)'),
22
23
 
23
- // Derived — getters so they recompute when gray is reassigned
24
24
  get white() {
25
25
  return colors.whiteish();
26
26
  },
27
27
  get black() {
28
- return colors.blackish();
28
+ return colors.gray.darken(45);
29
+ },
30
+
31
+ get selected() {
32
+ return selectedOverride ?? colors.yellow;
33
+ },
34
+ set selected(color) {
35
+ selectedOverride = color;
29
36
  },
30
37
 
31
38
  whiteish: (color = colors.gray) => color.lighten(45),
package/theme/input.js CHANGED
@@ -31,7 +31,7 @@ export const checkbox = ({ colors }) => `
31
31
  }
32
32
 
33
33
  &:checked:before {
34
- box-shadow: inset 1em 1em ${colors.blue};
34
+ box-shadow: inset 1em 1em ${colors.selected};
35
35
  }
36
36
 
37
37
  &:focus {
@@ -47,9 +47,9 @@ export default ({ colors }) => `
47
47
  box-sizing: border-box;
48
48
  width: 100%;
49
49
  color: ${colors.light(colors.red)};
50
- accent-color: ${colors.blue};
50
+ accent-color: ${colors.selected};
51
51
  background-color: ${colors.black};
52
- outline-color: ${colors.lighter(colors.orange)};
52
+ outline-color: ${colors.lighter(colors.selected)};
53
53
  padding: 2px 4px;
54
54
 
55
55
  &:disabled {
package/theme/page.js CHANGED
@@ -9,6 +9,7 @@ import _table from './table';
9
9
  export default theme => `
10
10
  html {
11
11
  height: 100%;
12
+ color-scheme: ${colors.black.isDark() ? 'dark' : 'light'};
12
13
  }
13
14
 
14
15
  body {
@@ -25,6 +26,11 @@ export default theme => `
25
26
  margin: 0;
26
27
  }
27
28
 
29
+ ::selection {
30
+ background: ${colors.lighter(colors.selected)};
31
+ color: ${colors.black};
32
+ }
33
+
28
34
  * {
29
35
  touch-action: manipulation;
30
36
  -webkit-text-size-adjust: none;
@@ -1,4 +1,8 @@
1
1
  export default ({ colors }) => `
2
+ html {
3
+ scrollbar-color: ${colors.alpha(colors.selected, 0.6)} ${colors.white.setAlpha(0.06)};
4
+ }
5
+
2
6
  ::-webkit-scrollbar {
3
7
  width: 24px;
4
8
  height: 24px;
@@ -10,15 +14,15 @@ export default ({ colors }) => `
10
14
  }
11
15
  ::-webkit-scrollbar-thumb {
12
16
  background: ${colors.white.setAlpha(0.06)};
13
- -webkit-box-shadow: inset 0 0 2px 1px ${colors.blue};
17
+ -webkit-box-shadow: inset 0 0 2px 1px ${colors.selected};
14
18
  border-radius: 12px;
15
19
  }
16
20
  ::-webkit-scrollbar-thumb:hover {
17
21
  background: ${colors.white.setAlpha(0.09)};
18
- -webkit-box-shadow: inset 0 0 3px 1px ${colors.blue};
22
+ -webkit-box-shadow: inset 0 0 3px 1px ${colors.selected};
19
23
  }
20
24
  ::-webkit-scrollbar-thumb:active {
21
25
  background: ${colors.white.setAlpha(0.12)};
22
- -webkit-box-shadow: inset 0 0 4px 2px ${colors.blue};
26
+ -webkit-box-shadow: inset 0 0 4px 2px ${colors.selected};
23
27
  }
24
28
  `;
package/theme/table.js CHANGED
@@ -42,12 +42,12 @@ export default ({ colors }) => `
42
42
  }
43
43
 
44
44
  tr:hover {
45
- background-color: ${colors.blackish(colors.blue)};
45
+ background-color: ${colors.blackish(colors.selected)};
46
46
  color: ${colors.lighter(colors.gray)};
47
47
  }
48
48
 
49
49
  td:hover, th:hover {
50
- background-color: ${colors.blue.darken(32)};
50
+ background-color: ${colors.selected.darken(32)};
51
51
  color: ${colors.white};
52
52
  }
53
53
  `;
@@ -1,455 +0,0 @@
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
- constructor(options = {}, ...children) {
65
- super(
66
- {
67
- tag: 'button',
68
- ...options,
69
- styles: ({ button }) => `
70
- ${button}
71
- ${options.styles?.({ button }) || ''}
72
- `,
73
- },
74
- ...children,
75
- );
76
- }
77
-
78
- _setOption(key, value) {
79
- if (key === 'mode') {
80
- this.removeClass(/\bmode-\S+/g).addClass(`mode-${value}`);
81
- } else {
82
- super._setOption(key, value);
83
- }
84
- }
85
- }
86
- ```
87
-
88
- ### With Styled Components
89
-
90
- Components integrate seamlessly with the styled system:
91
-
92
- ```js
93
- import { styled } from '@vanilla-bean/components';
94
-
95
- const StyledComponent = styled(
96
- Component,
97
- ({ colors }) => `
98
- background: ${colors.blue};
99
- color: ${colors.white};
100
- padding: 16px;
101
- border-radius: 8px;
102
- `,
103
- );
104
- ```
105
-
106
- ## Reactive Options System
107
-
108
- Component options are stored in an Oxject instance (not a plain object like Elem), enabling automatic reactivity:
109
-
110
- ```js
111
- const button = new Component({ textContent: 'Click me' });
112
-
113
- // Property changes trigger DOM updates
114
- button.options.textContent = 'Updated'; // Updates DOM reactively
115
- button.options.addClass = 'active'; // Adds CSS class reactively
116
-
117
- // Full Oxject features available
118
- const subscription = button.options.subscribe({
119
- key: 'textContent',
120
- callback: value => console.log('Text changed:', value),
121
- });
122
- ```
123
-
124
- ### Option Processing Pipeline
125
-
126
- Options are routed through `_setOption` based on key and value type:
127
-
128
- | Condition | Routing | Example |
129
- | ------------------------ | -------------------------- | ------------------------- |
130
- | Key starts with 'on' | Event handler registration | `onPointerPress: handler` |
131
- | Key in `knownAttributes` | `elem.setAttribute()` | `id: 'my-id'` |
132
- | Component has method | Method call with value | `addClass: 'active'` |
133
- | HTMLElement has property | Direct elem property | `textContent: 'hello'` |
134
- | Value is function | Component property | `customMethod: fn` |
135
- | Default | Elem property assignment | `customProp: value` |
136
-
137
- ## Event Handling
138
-
139
- ### Automatic Event Registration
140
-
141
- Event handlers are registered automatically when option keys start with 'on':
142
-
143
- ```js
144
- new Component({
145
- onPointerPress: event => console.log('pressed'),
146
- onChange: ({ value }) => console.log('value:', value), // Input events include .value
147
- onConnected: () => console.log('added to DOM'),
148
- onDisconnected: () => console.log('removed from DOM'),
149
- });
150
- ```
151
-
152
- > **Note:** `click` is not in the supported event set. Use `onPointerPress` for press interactions.
153
-
154
- ### Enhanced Input Events
155
-
156
- Input-related events (`keydown`, `keyup`, `change`, `blur`, `input`, `search`) receive enhanced event objects:
157
-
158
- ```js
159
- new Component({
160
- tag: 'input',
161
- type: 'text',
162
- onChange: ({ value, event }) => {
163
- console.log('New value:', value); // Extracted from input
164
- console.log('Original event:', event);
165
- },
166
- });
167
- ```
168
-
169
- ### Connection Events
170
-
171
- DOM connection/disconnection detected via MutationObserver:
172
-
173
- ```js
174
- new Component({
175
- onConnected: () => console.log('Component added to DOM'),
176
- onDisconnected: () => {
177
- console.log('Component removed - cleanup triggered');
178
- },
179
- });
180
- ```
181
-
182
- ### Specialized Event Handlers
183
-
184
- #### Hover with Movement Tracking
185
-
186
- ```js
187
- component.onHover(event => {
188
- console.log('Mouse position:', event.clientX, event.clientY);
189
- });
190
- ```
191
-
192
- #### Press Detection
193
-
194
- Fires on `pointerdown` for immediate, reliable response in all contexts including scroll containers and modal dialogs:
195
-
196
- ```js
197
- component.onPointerPress(event => {
198
- console.log('Pressed');
199
- });
200
- ```
201
-
202
- ## Styling
203
-
204
- ### Inline Style Objects
205
-
206
- Apply styles as JavaScript objects:
207
-
208
- ```js
209
- component.styles({
210
- color: 'red',
211
- padding: '10px',
212
- backgroundColor: '#f0f0f0',
213
- });
214
-
215
- // Or as option
216
- new Component({
217
- styles: { color: 'red', padding: '10px' },
218
- });
219
- ```
220
-
221
- ### Scoped CSS with Theme Integration
222
-
223
- Use theme functions for scoped, processed CSS:
224
-
225
- ```js
226
- // Theme function with automatic scoping
227
- component.styles(
228
- ({ colors, fonts }) => `
229
- background: ${colors.blue};
230
- ${fonts.kodeMono}
231
- padding: 16px;
232
- border-radius: 8px;
233
-
234
- &:hover {
235
- background: ${colors.darker(colors.blue)};
236
- }
237
-
238
- @media (max-width: 768px) {
239
- padding: 8px;
240
- }
241
- `,
242
- );
243
- ```
244
-
245
- CSS is scope-wrapped and injected as a scoped <style> tag. No preprocessing.
246
-
247
- ## Lifecycle Management
248
-
249
- ### Render Lifecycle
250
-
251
- `render()` orchestrates a fixed sequence every time it is called:
252
-
253
- ```
254
- render()
255
- ├─ if (this.rendered) ← re-render only: clears children, runs descendant cleanup
256
- │ empty()
257
- │ this.rendered = false
258
- ├─ build() ← subclass structural hook, runs before options are applied
259
- ├─ _processOptions() ← routes every option through _setOption()
260
- │ priority keys first (onConnected, textContent, content, appendTo, prependTo, value)
261
- │ then all remaining keys
262
- └─ this.rendered = true
263
- ```
264
-
265
- **`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.
266
-
267
- The preferred way to handle specific options is `static handlers` — a per-class dispatch map that composes across the constructor chain without requiring `super._setOption`:
268
-
269
- ```js
270
- class Card extends Component {
271
- build() {
272
- this.header = new Component({ tag: 'header', appendTo: this });
273
- this.body = new Component({ tag: 'section', appendTo: this });
274
- }
275
-
276
- static handlers = {
277
- title(value) {
278
- this.header.options.textContent = value;
279
- },
280
- variant(value) {
281
- this.removeClass(/variant-\S+/).addClass(`variant-${value}`);
282
- },
283
- };
284
- }
285
- ```
286
-
287
- Handlers that don't call `next(value)` fully own their key. Unhandled keys fall through to standard routing automatically — no `super._setOption` required. Use `_setOption` override only for enum validation or cases that need to intercept before the routing chain.
288
-
289
- **`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.
290
-
291
- **`async build()` is not supported.** `render()` does not await `build()`'s return value. Asynchronous initialization belongs in `onConnected`.
292
-
293
- **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.
294
-
295
- ### Rendering Process (summary)
296
-
297
- ```js
298
- component.render(); // Re-render: empty → build → _processOptions → rendered = true
299
- ```
300
-
301
- Rendering behavior:
302
-
303
- 1. **Content clearing** - `empty()` removes existing children on re-render only (skipped on first render)
304
- 2. **Structure creation** - `build()` creates sub-elements before options are applied
305
- 3. **Priority processing** - `_processOptions()` applies `priorityOptions` keys first
306
- 4. **Option routing** - Each option processed via `_setOption`
307
- 5. **Completion flag** - Sets `rendered = true`
308
-
309
- ### Automatic Cleanup System
310
-
311
- Components automatically clean up resources when removed from DOM:
312
-
313
- ```js
314
- // Manual cleanup registration
315
- component.addCleanup('unique-id', () => {
316
- console.log('Custom cleanup executed');
317
- });
318
-
319
- // Manual cleanup execution (automatic on disconnect)
320
- component.processCleanup();
321
- ```
322
-
323
- **Automatic cleanup includes:**
324
-
325
- - Event listener removal
326
- - Oxject subscription cleanup
327
- - Style element removal from DOM
328
- - Recursive child component cleanup
329
-
330
- ### Render Timing Control
331
-
332
- Control when components render with `autoRender` option:
333
-
334
- ```js
335
- // Immediate rendering (default)
336
- new Component({ autoRender: true });
337
-
338
- // Render on window load event
339
- new Component({ autoRender: 'onload' });
340
-
341
- // Render on next animation frame
342
- new Component({ autoRender: 'animationFrame' });
343
-
344
- // Manual rendering only
345
- new Component({ autoRender: false });
346
- ```
347
-
348
- ## API Reference
349
-
350
- ### Constructor
351
-
352
- ```js
353
- new Component(options?, ...children)
354
- ```
355
-
356
- **Parameters:**
357
-
358
- - `options` (Object) - Configuration options
359
- - `children` (...Component|Elem) - Child elements appended to `append` option
360
-
361
- ### Configuration Options
362
-
363
- #### Component-Specific Options
364
-
365
- | Option | Type | Description |
366
- | ------------------ | ------------------------------------- | ------------------------------------------- |
367
- | `tag` | `string` | HTML tag name (default: 'div') |
368
- | `autoRender` | `boolean\|'onload'\|'animationFrame'` | Render timing control |
369
- | `registeredEvents` | `Set<string>` | Custom event types for on() method |
370
- | `knownAttributes` | `Set<string>` | Attribute names for elem.setAttribute() |
371
- | `priorityOptions` | `Set<string>` | Option keys processed first during render |
372
- | `styles` | `string\|object\|Function` | CSS string, style object, or theme function |
373
- | `uniqueId` | `string` | Override auto-generated unique ID |
374
-
375
- #### Event Handler Options
376
-
377
- | Option | Type | Description |
378
- | ---------------- | ---------- | -------------------------------------------- |
379
- | `onHover` | `Function` | Hover handler with move tracking |
380
- | `onPointerPress` | `Function` | Fires on `pointerdown` |
381
- | ~~`onClick`~~ | — | Not supported. Use `onPointerPress` instead. |
382
- | `onChange` | `Function` | Change event handler (input .value included) |
383
- | `onConnected` | `Function` | DOM connection detection |
384
- | `onDisconnected` | `Function` | DOM disconnection detection |
385
-
386
- All standard Elem options work as reactive Component options.
387
-
388
- ### Methods
389
-
390
- #### Options-Compatible Methods
391
-
392
- Can be called directly or used as reactive options:
393
-
394
- ```js
395
- component.styles(styleFunction); // Direct call
396
- component.onHover(callback); // Direct call
397
- component.addClass('active'); // Direct call
398
- component.append(child); // Direct call
399
-
400
- // Or as reactive options:
401
- new Component({
402
- styles: styleFunction,
403
- onHover: callback,
404
- addClass: 'active',
405
- append: [child],
406
- });
407
- ```
408
-
409
- #### Lifecycle Methods
410
-
411
- ```js
412
- component.build(); // Subclass structural hook — override to create child elements
413
- component.render(); // Re-render: empty → build → _processOptions → rendered = true
414
- component.addCleanup(id, fn); // Register cleanup function (chains with existing for same id)
415
- component.replaceCleanup(id, fn); // Replace cleanup, running the previous one immediately
416
- component.replaceDestroyCleanup(id, fn); // Destroy-only cleanup: survives disconnect, runs on destroy()
417
- component.processCleanup(); // Execute all cleanup functions
418
- component.destroy(); // Disconnect observer, run all cleanup, remove from DOM
419
- ```
420
-
421
- #### Event Methods
422
-
423
- ```js
424
- component.on({ targetEvent, callback, id? }); // Event registration
425
- component.emit(eventType, detail); // Event dispatch
426
- ```
427
-
428
- #### Utility Methods
429
-
430
- ```js
431
- component.ancestry(); // Prototype chain inspection
432
- ```
433
-
434
- ### Properties
435
-
436
- ```js
437
- component.parent; // Parent Component/Elem instance
438
- component.children; // Child Component/Elem instances
439
- component.uniqueId; // Frozen unique identifier
440
- component.options; // Reactive Oxject instance
441
- component.elem; // Underlying HTMLElement
442
- component.rendered; // Boolean render status flag
443
- ```
444
-
445
- ## Memory Management
446
-
447
- Components provide comprehensive automatic memory management:
448
-
449
- - **Event listeners** - Automatically removed on DOM disconnect
450
- - **Oxject subscriptions** - Cleaned up when component destroyed
451
- - **Style elements** - Removed from DOM head on cleanup
452
- - **Child components** - Recursive cleanup propagation
453
- - **Custom cleanup** - Manual cleanup function registration
454
-
455
- No manual cleanup required for typical usage - components handle resource management automatically.
package/Elem/README.md DELETED
@@ -1,373 +0,0 @@
1
- # Elem
2
-
3
- Enhanced DOM element wrapper providing fluent API methods while maintaining direct access to the underlying HTMLElement.
4
-
5
- ## Basic Usage
6
-
7
- ### Simple Element Creation
8
-
9
- Create DOM elements with enhanced manipulation capabilities:
10
-
11
- ```js
12
- import { Elem } from '@vanilla-bean/components';
13
-
14
- const button = new Elem({
15
- tag: 'button',
16
- textContent: 'Click me',
17
- className: 'btn btn-primary',
18
- onclick: () => alert('Clicked!'),
19
- appendTo: document.body,
20
- });
21
- ```
22
-
23
- ### Complex Nested Structures
24
-
25
- Build complex DOM hierarchies with nested elements:
26
-
27
- ```js
28
- const card = new Elem(
29
- {
30
- tag: 'div',
31
- className: 'card',
32
- style: { padding: '20px', margin: '10px' },
33
- },
34
- new Elem({ tag: 'h3', textContent: 'Card Title' }),
35
- new Elem({ tag: 'p', textContent: 'Card content goes here.' }),
36
- );
37
- ```
38
-
39
- ### Method Chaining
40
-
41
- Chain methods for fluent DOM construction:
42
-
43
- ```js
44
- const navigation = new Elem({ tag: 'nav' })
45
- .addClass('menu', 'horizontal')
46
- .setStyle({ display: 'flex', gap: '16px' })
47
- .append(
48
- new Elem({ tag: 'a', textContent: 'Home', href: '/' }),
49
- new Elem({ tag: 'a', textContent: 'About', href: '/about' }),
50
- new Elem({ tag: 'a', textContent: 'Contact', href: '/contact' }),
51
- )
52
- .appendTo(document.body);
53
- ```
54
-
55
- ### Direct DOM Access
56
-
57
- Access the full HTMLElement API when needed:
58
-
59
- ```js
60
- const input = new Elem({ tag: 'input', type: 'text' });
61
-
62
- // Enhanced methods
63
- input.addClass('form-control').setStyle({ width: '100%' });
64
-
65
- // Direct DOM access
66
- input.elem.focus();
67
- input.elem.select();
68
- input.elem.scrollIntoView({ behavior: 'smooth' });
69
- ```
70
-
71
- ## Configuration Options
72
-
73
- ### Constructor Syntax
74
-
75
- ```js
76
- new Elem(options?, ...children)
77
- ```
78
-
79
- **Parameters:**
80
-
81
- - `options` (Object) - Configuration options and HTML properties
82
- - `children` (...Elem|HTMLElement|string) - Child elements or text content
83
-
84
- ### Core Options
85
-
86
- | Option | Type | Description |
87
- | ------------ | --------------------------- | ---------------------------------- |
88
- | `tag` | `string` | HTML tag name (default: 'div') |
89
- | `style` | `object` | CSS properties as key-value pairs |
90
- | `attributes` | `object` | HTML attributes as key-value pairs |
91
- | `content` | `string\|Elem\|HTMLElement` | Element content |
92
- | `appendTo` | `Elem\|HTMLElement` | Parent element to append to |
93
- | `prependTo` | `Elem\|HTMLElement` | Parent element to prepend to |
94
- | `append` | `Array` | Child elements to append |
95
- | `prepend` | `Array` | Child elements to prepend |
96
-
97
- ### HTML Properties
98
-
99
- All standard HTMLElement properties work as options:
100
-
101
- ```js
102
- new Elem({
103
- // Content properties
104
- textContent: 'Button text',
105
- innerHTML: '<span>HTML content</span>',
106
-
107
- // Form properties
108
- value: 'input value',
109
- checked: true,
110
- disabled: false,
111
-
112
- // Element properties
113
- id: 'unique-id',
114
- className: 'btn primary',
115
- title: 'Tooltip text',
116
-
117
- // Link properties
118
- href: 'https://example.com',
119
- target: '_blank',
120
-
121
- // Image properties
122
- src: 'image.jpg',
123
- alt: 'Image description',
124
- });
125
- ```
126
-
127
- ### Event Handler Properties
128
-
129
- Event handlers can be assigned directly:
130
-
131
- ```js
132
- new Elem({
133
- tag: 'button',
134
- onclick: event => console.log('clicked'),
135
- onmouseover: event => console.log('hover'),
136
- onchange: event => console.log('changed'),
137
- onfocus: event => console.log('focused'),
138
- });
139
- ```
140
-
141
- ## DOM Manipulation
142
-
143
- ### Content Management
144
-
145
- ```js
146
- // Set content (replaces existing)
147
- elem.content('New text content');
148
- elem.content(new Elem({ tag: 'span', textContent: 'HTML element' }));
149
-
150
- // Clear all content
151
- elem.empty();
152
- ```
153
-
154
- ### Child Element Management
155
-
156
- ```js
157
- // Add children
158
- elem.append(child1, child2, 'text content');
159
- elem.prepend(child1, child2);
160
-
161
- // Access children
162
- const childElements = elem.children; // Array of Elem instances
163
- const nativeChildren = elem.elem.children; // HTMLCollection
164
- ```
165
-
166
- ### Hierarchy Management
167
-
168
- ```js
169
- // Add to DOM
170
- elem.appendTo(document.body);
171
- elem.prependTo(document.querySelector('.container'));
172
-
173
- // Navigate hierarchy
174
- const parentElem = elem.parent; // Parent Elem instance (if exists)
175
- const nativeParent = elem.parentElem; // Parent HTMLElement
176
- ```
177
-
178
- ### Style and Attribute Management
179
-
180
- ```js
181
- // Set multiple styles
182
- elem.setStyle({
183
- color: 'red',
184
- fontSize: '16px',
185
- backgroundColor: '#f0f0f0',
186
- });
187
-
188
- // Set multiple attributes
189
- elem.setAttributes({
190
- 'data-id': '123',
191
- 'aria-label': 'Close button',
192
- role: 'button',
193
- });
194
- ```
195
-
196
- ## Class Management
197
-
198
- ### Enhanced Class Operations
199
-
200
- Elem provides enhanced class manipulation with regular expression support:
201
-
202
- ```js
203
- // Check for classes
204
- elem.hasClass('active'); // Check single class
205
- elem.hasClass('btn', 'primary'); // Check multiple classes
206
- elem.hasClass(/^btn-/); // Check with regex pattern
207
-
208
- // Add classes
209
- elem.addClass('new-class');
210
- elem.addClass('class1', 'class2', 'class3');
211
-
212
- // Remove classes
213
- elem.removeClass('old-class');
214
- elem.removeClass(/^temp-/); // Remove all classes starting with 'temp-'
215
- elem.removeClass(/\bmobile-\w+/g); // Remove classes matching pattern
216
-
217
- // Toggle class based on a condition (add when true, remove when false)
218
- elem.toggleClass('active', isActive);
219
- ```
220
-
221
- ### Class Manipulation Examples
222
-
223
- ```js
224
- const button = new Elem({ tag: 'button', className: 'btn btn-primary temp-123' });
225
-
226
- // Remove all temporary classes
227
- button.removeClass(/^temp-/);
228
-
229
- // Add state classes
230
- button.addClass('btn-large', 'btn-rounded');
231
-
232
- // Conditional classes
233
- if (isActive) {
234
- button.addClass('active', 'selected');
235
- }
236
-
237
- // Check for button variants
238
- if (button.hasClass(/^btn-(primary|secondary|danger)$/)) {
239
- console.log('Has button variant class');
240
- }
241
- ```
242
-
243
- ## Event Handling
244
-
245
- ### EventTarget Integration
246
-
247
- Elem extends EventTarget, providing full event capabilities:
248
-
249
- ```js
250
- const button = new Elem({ tag: 'button', textContent: 'Click me' });
251
-
252
- // Option-based event handlers
253
- new Elem({
254
- tag: 'input',
255
- onchange: event => console.log('Value changed:', event.target.value),
256
- onfocus: event => event.target.select(),
257
- });
258
-
259
- // addEventListener method
260
- button.addEventListener('click', event => {
261
- console.log('Button clicked');
262
- });
263
-
264
- // Custom events
265
- button.addEventListener('customEvent', event => {
266
- console.log('Custom event data:', event.detail);
267
- });
268
-
269
- // Dispatch events
270
- button.dispatchEvent(
271
- new CustomEvent('customEvent', {
272
- detail: { message: 'Hello' },
273
- }),
274
- );
275
- ```
276
-
277
- ### Event Handler Options vs Methods
278
-
279
- ```js
280
- // Via constructor options (preferred for initial setup)
281
- const elem = new Elem({
282
- tag: 'button',
283
- onclick: handleClick,
284
- onmouseover: handleHover,
285
- });
286
-
287
- // Via addEventListener (preferred for dynamic binding)
288
- elem.addEventListener('click', handleClick);
289
- elem.addEventListener('mouseover', handleHover);
290
-
291
- // Via native element
292
- elem.elem.addEventListener('click', handleClick);
293
- ```
294
-
295
- ## API Reference
296
-
297
- ### Constructor
298
-
299
- ```js
300
- new Elem(options?, ...children)
301
- ```
302
-
303
- ### Core Methods
304
-
305
- #### Content Methods
306
-
307
- ```js
308
- elem.content(content); // Set element content
309
- elem.empty(); // Remove all child elements
310
- ```
311
-
312
- #### Child Management Methods
313
-
314
- ```js
315
- elem.append(...children); // Append child elements
316
- elem.prepend(...children); // Prepend child elements
317
- elem.appendTo(parent); // Append to parent element
318
- elem.prependTo(parent); // Prepend to parent element
319
- ```
320
-
321
- #### Style and Attribute Methods
322
-
323
- ```js
324
- elem.setStyle(styles); // Set CSS properties
325
- elem.setAttributes(attributes); // Set HTML attributes
326
- elem.setOptions(options); // Set multiple options at once
327
- ```
328
-
329
- #### Class Methods
330
-
331
- ```js
332
- elem.hasClass(...classes); // Check for classes (supports regex)
333
- elem.addClass(...classes); // Add CSS classes
334
- elem.removeClass(...classes); // Remove CSS classes (supports regex)
335
- elem.toggleClass(className, condition); // Add when condition is true, remove when false
336
- ```
337
-
338
- ### Properties
339
-
340
- ```js
341
- elem.elem; // Underlying HTMLElement
342
- elem.parent; // Parent Elem instance (if created by Elem)
343
- elem.parentElem; // Parent HTMLElement
344
- elem.children; // Array of child Elem instances
345
- elem.options; // Configuration options object
346
- ```
347
-
348
- ### Utility Methods
349
-
350
- ```js
351
- elem.toString(); // Returns '[object Elem]'
352
- ```
353
-
354
- ## Integration with Component
355
-
356
- Elem serves as the foundation for the Component class:
357
-
358
- ```js
359
- import { Component } from '@vanilla-bean/components';
360
-
361
- // Component extends Elem with reactive options
362
- const component = new Component({
363
- tag: 'div',
364
- textContent: 'I am reactive!',
365
- });
366
-
367
- // All Elem methods available
368
- component.addClass('component-class');
369
- component.setStyle({ padding: '16px' });
370
-
371
- // Plus Component-specific features
372
- component.options.textContent = 'Updated reactively!';
373
- ```
package/styled/README.md DELETED
@@ -1,329 +0,0 @@
1
- # styled
2
-
3
- Create Component subclasses with scoped CSS: each `styled()` call generates a unique class, processes the theme, and injects a `<style>` element into `<head>`.
4
-
5
- ## Basic Usage
6
-
7
- ### Template Literal Syntax
8
-
9
- Create styled components using template literals with theme integration:
10
-
11
- ```js
12
- const StyledIcon = styled.Icon`
13
- background-color: ${({ colors }) => colors.black};
14
- width: 24px;
15
- height: 24px;
16
-
17
- &:before {
18
- ${({ fonts }) => fonts.fontAwesomeSolid}
19
- content: "\f015";
20
- }
21
- `;
22
- ```
23
-
24
- ### Function Syntax with Configuration
25
-
26
- Use function syntax when you need to pass component configuration options:
27
-
28
- ```js
29
- const ConfiguredComponent = styled(
30
- Component,
31
- ({ colors }) => `
32
- background-color: ${colors.black};
33
- color: ${colors.lighter(colors.blue)};
34
- padding: 12px;
35
-
36
- &:hover {
37
- background-color: ${colors.dark(colors.blue)};
38
- }
39
- `,
40
- {
41
- tag: 'section',
42
- role: 'banner',
43
- textContent: 'Default Text',
44
- },
45
- );
46
- ```
47
-
48
- ### Named Component Shortcuts
49
-
50
- All top-level components are available as shorthand methods:
51
-
52
- ```js
53
- const StyledButton = styled.Button`
54
- background-color: ${({ colors }) => colors.green};
55
- border-radius: 8px;
56
- `;
57
-
58
- const StyledInput = styled.Input`
59
- ${({ fonts }) => fonts.kodeMono}
60
- border: 2px solid ${({ colors }) => colors.blue};
61
- `;
62
- ```
63
-
64
- **Note**: Template literal syntax creates components with empty configuration. Use function syntax to pass component options.
65
-
66
- ## Theme Integration
67
-
68
- Style functions receive the complete theme object containing colors, fonts, and component styles:
69
-
70
- ```js
71
- const ThemedComponent = styled.Component`
72
- ${({ button }) => button} /* Apply base button styles */
73
- ${({ fonts }) => fonts.kodeMono}
74
- background: ${({ colors }) => colors.darker(colors.blue)};
75
- color: ${({ colors }) => colors.mostReadable(colors.blue, [colors.white, colors.black])};
76
- `;
77
- ```
78
-
79
- ### Runtime Style Override
80
-
81
- Override or extend styles when creating component instances:
82
-
83
- ```js
84
- const instance = new StyledComponent({
85
- styles: ({ colors }) => ({
86
- backgroundColor: colors.red,
87
- border: `2px solid ${colors.darker(colors.red)}`,
88
- }),
89
- });
90
- ```
91
-
92
- Object-based styles apply as inline styles. Function-based styles generate scoped CSS.
93
-
94
- ## Component Inheritance
95
-
96
- ### Extending Styled Components
97
-
98
- Build component hierarchies by extending existing styled components:
99
-
100
- ```js
101
- const BaseButton = styled.Button`
102
- padding: 8px 16px;
103
- border-radius: 4px;
104
- `;
105
-
106
- const PrimaryButton = styled(BaseButton)`
107
- background: ${({ colors }) => colors.blue};
108
- color: ${({ colors }) => colors.white};
109
- `;
110
- ```
111
-
112
- ### Template Literals with Any Styled Component
113
-
114
- Template literal syntax works with any styled component, including those created with function syntax:
115
-
116
- ```js
117
- const BaseComponent = styled(Component, () => 'color: red;');
118
-
119
- // Extend any styled component with template literals
120
- const ExtendedComponent = styled(BaseComponent)`
121
- background: ${({ colors }) => colors.blue};
122
- padding: 16px;
123
- `;
124
-
125
- // Use in class definitions for custom methods
126
- class MyComponent extends (styled(BaseComponent)`
127
- font-weight: bold;
128
- border-radius: 4px;
129
- `) {
130
- // Add custom methods here
131
- }
132
- ```
133
-
134
- ### Component Functionality Inheritance
135
-
136
- Styled components inherit all functionality from their base component:
137
-
138
- ```js
139
- const StyledInput = styled.Input`
140
- border: 2px solid ${({ colors }) => colors.blue};
141
- `;
142
-
143
- const instance = new StyledInput({
144
- value: 'initial value',
145
- onChange: event => console.log(event.value),
146
- placeholder: 'Enter text...',
147
- });
148
- ```
149
-
150
- ## CSS Processing
151
-
152
- ### Native CSS Nesting
153
-
154
- Styles are injected as written, no transformation, no runtime compilation. VBC requires Chrome 112+, Firefox 117+, and Safari 16.5+, all of which support the `&` nesting syntax natively.
155
-
156
- ```js
157
- const NestedComponent = styled.Component`
158
- padding: 16px;
159
-
160
- & .child {
161
- margin: 8px;
162
-
163
- &:hover {
164
- background: ${({ colors }) => colors.blue};
165
- }
166
- }
167
-
168
- @media (max-width: 768px) {
169
- padding: 8px;
170
- }
171
- `;
172
- ```
173
-
174
- ### Automatic Scoping
175
-
176
- Each styled component receives a unique class identifier to prevent CSS conflicts:
177
-
178
- ```js
179
- const StyledDiv = styled(Component, () => `color: red;`);
180
- const instance = new StyledDiv();
181
-
182
- // Generated CSS: .a1b2c3d4 { color: red; }
183
- // Component class: "a1b2c3d4"
184
- ```
185
-
186
- ### Conditional Styles
187
-
188
- Apply conditional styles using CSS classes and selectors:
189
-
190
- ```js
191
- const ConditionalComponent = styled.Component`
192
- padding: 12px;
193
- background: ${({ colors }) => colors.white};
194
-
195
- &.active {
196
- background: ${({ colors }) => colors.blue};
197
- color: ${({ colors }) => colors.white};
198
- }
199
-
200
- &.disabled {
201
- opacity: 0.5;
202
- pointer-events: none;
203
- }
204
- `;
205
-
206
- const instance = new ConditionalComponent({
207
- addClass: 'active',
208
- textContent: 'Active Button',
209
- });
210
- ```
211
-
212
- ## Performance
213
-
214
- ### Processing Pipeline
215
-
216
- The styled system processes styles through this pipeline:
217
-
218
- 1. **Component Creation** - Generates unique class identifier via `classSafeNanoid()`
219
- 2. **Style Processing** - Converts template literals into theme functions
220
- 3. **Theme Application** - Injects complete theme object into style functions
221
- 4. **CSS Processing** - Processes styles through `shimCSS()` pipeline
222
- 5. **DOM Injection** - Injects final CSS via `appendStyles()`
223
-
224
- ### Load-Time Optimization
225
-
226
- Style processing timing depends on document state:
227
-
228
- | Document State | Behavior |
229
- | ------------------- | ------------------------------------------ |
230
- | Complete | Processes and injects immediately |
231
- | Loading | Queues for batch processing on window load |
232
- | Multiple Components | Batches together for efficiency |
233
-
234
- ```js
235
- // Document loaded - processes immediately
236
- const StyledComponent = styled(Component, () => 'color: red;');
237
-
238
- // Document loading - queued for batch processing
239
- const AnotherStyled = styled(Component, () => 'color: blue;');
240
- ```
241
-
242
- ## API Reference
243
-
244
- ### styled(BaseComponent, styles?, options?)
245
-
246
- Creates a styled component class.
247
-
248
- **Parameters:**
249
-
250
- - `BaseComponent` (Function) - Component class to extend
251
- - `styles` (Function|String) - Style function or CSS string
252
- - `options` (Object) - Component configuration options
253
-
254
- **Returns:** Extended component class with scoped styling
255
-
256
- ### configured(BaseComponent, options)
257
-
258
- Creates a component with configuration but no styles:
259
-
260
- ```js
261
- const ConfiguredComponent = configured(Component, {
262
- tag: 'article',
263
- role: 'main',
264
- textContent: 'Default Content',
265
- });
266
- ```
267
-
268
- ### Utility Functions
269
-
270
- #### appendStyles(css, id?)
271
-
272
- Inject CSS directly into the page:
273
-
274
- ```js
275
- appendStyles(
276
- `
277
- .my-global-class {
278
- font-weight: bold;
279
- color: red;
280
- }
281
- `,
282
- 'my-global-styles',
283
- );
284
- ```
285
-
286
- #### themeStyles({ styles, scope })
287
-
288
- Generate themed CSS with optional scoping:
289
-
290
- ```js
291
- const themedCSS = themeStyles({
292
- styles: ({ colors }) => `color: ${colors.white}; background: ${colors.black};`,
293
- scope: '.my-component',
294
- });
295
- // Returns: ".my-component { color: hsl(0, 0%, 90%); background: hsl(0, 0%, 10%); }"
296
- ```
297
-
298
- #### shimCSS(styleConfig)
299
-
300
- Complete style processing pipeline:
301
-
302
- ```js
303
- shimCSS({
304
- styles: ({ colors }) => `
305
- display: flex;
306
- background: ${colors.blue};
307
- & .item { padding: 8px; }
308
- `,
309
- scope: '.my-scoped-component',
310
- });
311
- ```
312
-
313
- ## Development Features
314
-
315
- ### Debug Class Names
316
-
317
- Development mode adds inheritance-based class names for easier debugging:
318
-
319
- ```js
320
- // Development classes: "a1b2c3 MyCustomComponent Component Elem"
321
- class MyCustomComponent extends Component {}
322
- const StyledCustom = styled(MyCustomComponent, () => 'color: blue;');
323
- ```
324
-
325
- ### Memory Management
326
-
327
- Class-level styles injected by `styled()` persist for the page lifetime. They are scoped to a unique class and do not interfere with other components, but are not removed when instances disconnect.
328
-
329
- Per-instance styles set via the `styles` option on a component instance are cleaned up when that component disconnects from the DOM.