@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
@@ -14,7 +14,6 @@ const _internalProperties = new Set([
14
14
  'elem',
15
15
  'tag',
16
16
  'defaultOptions',
17
- 'handlers',
18
17
  '__registeredEvents',
19
18
  '__knownAttributes',
20
19
  '__priorityOptions',
@@ -47,30 +46,169 @@ const commonEvents = new Set([
47
46
  const defaultOptions = {
48
47
  tag: 'div',
49
48
  autoRender: true,
50
- registeredEvents: new Set([]),
51
- knownAttributes: new Set(['role', 'name', 'colspan', 'anchor', 'popover', 'popovertarget', 'popovertargetaction']),
52
- get priorityOptions() {
53
- return new Set(['onConnected', 'textContent', 'content', 'appendTo', 'prependTo', 'value']);
54
- },
55
49
  };
56
50
 
51
+ const baseKnownAttributes = new Set([
52
+ 'role',
53
+ 'name',
54
+ 'colspan',
55
+ 'anchor',
56
+ 'popover',
57
+ 'popovertarget',
58
+ 'popovertargetaction',
59
+ ]);
60
+ const basePriorityOptions = new Set(['onConnected', 'textContent', 'content', 'appendTo', 'prependTo', 'value']);
61
+
62
+ const _staticsCache = new WeakMap();
63
+
64
+ /**
65
+ * Collects a static field's own values up the constructor chain, leaf class first,
66
+ * stopping before Component. The single walk behind every schema mechanism.
67
+ * Results are cached per leaf class - static declarations are treated as immutable
68
+ * after a class's first construction.
69
+ * @param {Function} klass - Leaf constructor (new.target or this.constructor) to walk from
70
+ * @param {string} name - Static field name ('schema', 'events', 'prepareOptions')
71
+ * @returns {Array<*>} Own static values from leaf to root
72
+ */
73
+ const collectStatics = (klass, name) => {
74
+ let cache = _staticsCache.get(klass);
75
+ if (cache?.[name]) return cache[name];
76
+
77
+ const values = [];
78
+ let current = klass;
79
+
80
+ while (current && current !== Component) {
81
+ if (Object.prototype.hasOwnProperty.call(current, name) && current[name]) values.push(current[name]);
82
+ current = Object.getPrototypeOf(current);
83
+ }
84
+
85
+ if (!cache) _staticsCache.set(klass, (cache = {}));
86
+ cache[name] = values;
87
+
88
+ return values;
89
+ };
90
+
91
+ /**
92
+ * Collects `default` values from static schemas up the constructor chain.
93
+ * Parent schemas apply first so child classes override per-key. Descriptor `default`
94
+ * is read at construction time, so getter defaults (e.g. `get default() { return document.body; }`)
95
+ * evaluate fresh per instance.
96
+ * @param {Function} klass - Leaf constructor (new.target) to walk from
97
+ * @returns {object} Merged default values keyed by option name
98
+ */
99
+ const collectSchemaDefaults = klass => {
100
+ const defaults = {};
101
+
102
+ // Copied before reversing - the collected array is cached and must stay leaf-first
103
+ for (const schema of [...collectStatics(klass, 'schema')].reverse()) {
104
+ for (const [key, descriptor] of Object.entries(schema)) {
105
+ if (descriptor && 'default' in descriptor) defaults[key] = descriptor.default;
106
+ }
107
+ }
108
+
109
+ return defaults;
110
+ };
111
+
112
+ /**
113
+ * Collects schema keys carrying a given flag (e.g. `attribute`, `priority`) up the constructor chain.
114
+ * The nearest class whose descriptor declares the flag decides, so a subclass can opt out
115
+ * of a parent's flag with e.g. `attribute: false`.
116
+ * @param {Function} klass - Leaf constructor to walk from
117
+ * @param {string} flag - Descriptor flag name to collect
118
+ * @returns {Set<string>} Option keys with the flag enabled
119
+ */
120
+ const collectSchemaFlags = (klass, flag) => {
121
+ const keys = new Set();
122
+ const decided = new Set();
123
+
124
+ for (const schema of collectStatics(klass, 'schema')) {
125
+ for (const [key, descriptor] of Object.entries(schema)) {
126
+ if (descriptor && flag in descriptor && !decided.has(key)) {
127
+ decided.add(key);
128
+ if (descriptor[flag]) keys.add(key);
129
+ }
130
+ }
131
+ }
132
+
133
+ return keys;
134
+ };
135
+
136
+ /**
137
+ * Collects `static events` declarations up the constructor chain into one union.
138
+ * @param {Function} klass - Leaf constructor to walk from
139
+ * @returns {Set<string>} Union of every class's declared event names
140
+ */
141
+ const collectStaticEvents = klass => new Set(collectStatics(klass, 'events').flat());
142
+
143
+ /**
144
+ * Runs `static prepareOptions(options, children)` hooks leaf-first up the constructor chain.
145
+ * Hooks receive the merged (schema defaults + user) options and return a transformed copy,
146
+ * which handles computed defaults that previously required a constructor.
147
+ * @param {Function} klass - Leaf constructor to walk from
148
+ * @param {object} options - Merged options to transform
149
+ * @param {Array} children - Constructor children, for hooks that derive options from them
150
+ * @returns {object} Transformed options
151
+ */
152
+ const runPrepareOptions = (klass, options, children) => {
153
+ for (const prepare of collectStatics(klass, 'prepareOptions')) {
154
+ options = prepare(options, children) ?? options;
155
+ }
156
+
157
+ return options;
158
+ };
159
+
160
+ const union = (base, additions) => (additions.size > 0 ? new Set([...(base || []), ...additions]) : base);
161
+
57
162
  /**
58
163
  * General purpose reactive component with automatic cleanup and lifecycle management.
59
164
  * Extends Elem with Oxject-driven options, event handling, and style processing.
165
+ *
166
+ * Subclasses declare their option schema once via `static schema` - each key maps to a
167
+ * descriptor defining what the option is:
168
+ * - `default` - initial value, merged automatically (child classes override parents per-key)
169
+ * - `set(value, next)` - change handler, chained deepest-class-first;
170
+ * omit `next()` to own the key, call it to continue down the chain
171
+ * - `attribute: true` - routed to elem.setAttribute
172
+ * - `priority: true` - processed before other keys during render
173
+ * - `data: true` - force store-only routing; needed only when a data key's name collides
174
+ * with a real DOM property that standard routing would otherwise hit
175
+ * - `enum: [...]` - valid values; assigning anything else (other than null/undefined) throws.
176
+ * Readable via optionEnum(key)
177
+ *
178
+ * Declared keys route through standard option processing (elem properties, events, methods)
179
+ * when a real match exists, so DOM-bound defaults still reach the element - but they never
180
+ * fall through to guessing: with no match they live in this.options, silently. Undeclared
181
+ * keys keep the full fallback behavior including the dev warning.
182
+ *
183
+ * Two more statics:
184
+ * - `static events = ['select']` - custom event names usable with on()/emit(); collected as a
185
+ * union up the class chain, so subclasses add to their parents' events instead of replacing them
186
+ * - `static prepareOptions(options, children)` - pure transform of the merged
187
+ * (schema defaults + user) options, run leaf-first, for computed defaults that
188
+ * would otherwise require a constructor
189
+ * @example
190
+ * static schema = {
191
+ * tag: { default: 'canvas' },
192
+ * background: { default: '#FFF', set(value) { this.elem.style.background = value; } },
193
+ * color: { default: '#000' },
194
+ * };
195
+ * static events = ['line', 'draw'];
196
+ * static prepareOptions(options) {
197
+ * return { ...options, style: { cursor: 'crosshair', ...options.style } };
198
+ * }
60
199
  * @augments Elem
61
200
  * @augments EventTarget
62
201
  */
63
202
  class Component extends Elem {
64
- defaultOptions = defaultOptions;
203
+ get defaultOptions() {
204
+ return { ...defaultOptions, ...collectSchemaDefaults(this.constructor) };
205
+ }
65
206
 
66
207
  /**
67
208
  * Creates reactive component with Oxject-driven options, automatic cleanup, and lifecycle management.
68
209
  * @param {object} [options] - Component configuration object with reactive properties
69
210
  * @param {string} [options.tag] - HTML tag name for the root element
70
211
  * @param {boolean|'onload'|'animationFrame'} [options.autoRender] - Render timing: true (immediate), 'onload' (window load), 'animationFrame' (next frame), false (manual)
71
- * @param {Set<string>} [options.registeredEvents] - Additional event types to handle via on() method
72
- * @param {Set<string>} [options.knownAttributes] - Attribute names routed to elem.setAttribute() instead of property assignment
73
- * @param {Set<string>} [options.priorityOptions] - Option keys processed first during render
74
212
  * @param {object} [options.style] - Inline CSS properties applied as HTMLElement.style
75
213
  * @param {object} [options.attributes] - HTML attributes applied via setAttribute()
76
214
  * @param {string|object|Function} [options.styles] - CSS definition: string/function processed through theme system, object applied inline
@@ -84,16 +222,21 @@ class Component extends Elem {
84
222
  * @returns {Component} Component instance with reactive options accessible via this.options
85
223
  */
86
224
  constructor(options = {}, ...children) {
87
- const { tag, autoRender, registeredEvents, knownAttributes, priorityOptions, ...optionsWithoutConfig } = {
88
- ...defaultOptions,
89
- ...options,
90
- };
225
+ const { tag, autoRender, ...optionsWithoutConfig } = runPrepareOptions(
226
+ new.target,
227
+ {
228
+ ...defaultOptions,
229
+ ...collectSchemaDefaults(new.target),
230
+ ...options,
231
+ },
232
+ children,
233
+ );
91
234
 
92
235
  super({ tag });
93
236
 
94
- this.__registeredEvents = registeredEvents;
95
- this.__knownAttributes = knownAttributes;
96
- this.__priorityOptions = priorityOptions;
237
+ this.__registeredEvents = collectStaticEvents(new.target);
238
+ this.__knownAttributes = union(baseKnownAttributes, collectSchemaFlags(new.target, 'attribute'));
239
+ this.__priorityOptions = union(basePriorityOptions, collectSchemaFlags(new.target, 'priority'));
97
240
 
98
241
  this.elem._component = this;
99
242
 
@@ -201,48 +344,73 @@ class Component extends Elem {
201
344
  }
202
345
 
203
346
  /**
204
- * Routes option changes through the static handlers chain, then standard routing.
347
+ * Routes option changes through the schema set chain, then standard routing.
205
348
  *
206
- * Walks the constructor chain collecting all handlers for the given key (deepest class
207
- * first), then executes them in order. Each handler receives `next(value?)` - call it
208
- * to continue to the next handler in the chain, or to standard routing when the chain
209
- * is exhausted. Handlers that do not call `next` fully own the key.
349
+ * Walks the constructor chain collecting every `static schema` set function for the
350
+ * given key (deepest class first), then executes them in order. Each receives
351
+ * `next(value?)` - call it to continue to the next in the chain, or to standard
352
+ * routing when the chain is exhausted. Not calling `next` fully owns the key.
353
+ * Keys flagged `data: true` never reach standard routing at all.
210
354
  * @param {string} key - Option property name being changed
211
355
  * @param {*} value - New value being assigned to the option
212
356
  * @private
213
357
  */
214
358
  _setOption(key, value) {
215
359
  const chain = [];
216
- let klass = this.constructor;
217
- while (klass && klass !== Component) {
218
- if (Object.prototype.hasOwnProperty.call(klass, 'handlers') && klass.handlers?.[key]) {
219
- chain.push(klass.handlers[key]);
360
+ let dataOption;
361
+ let enumValues;
362
+ let enumDecided = false;
363
+
364
+ // set functions chain across the class hierarchy; scalar fields (data, enum) are
365
+ // decided by the nearest class whose descriptor declares them, so subclasses can
366
+ // opt out with data: false or enum: null
367
+ let declared = false;
368
+ for (const schema of collectStatics(this.constructor, 'schema')) {
369
+ const descriptor = schema[key];
370
+ if (!descriptor) continue;
371
+ declared = true;
372
+ if (descriptor.set) chain.push(descriptor.set);
373
+ if (dataOption === undefined && 'data' in descriptor) dataOption = !!descriptor.data;
374
+ if (!enumDecided && 'enum' in descriptor) {
375
+ enumDecided = true;
376
+ enumValues = descriptor.enum;
220
377
  }
221
- klass = Object.getPrototypeOf(klass);
378
+ }
379
+
380
+ if (enumValues && value !== undefined && value !== null && !enumValues.includes(value)) {
381
+ throw new Error(
382
+ `"${value}" is not a valid ${key}. The ${key} must be one of the following values: ${enumValues.join(', ')}`,
383
+ );
222
384
  }
223
385
 
224
386
  if (chain.length > 0) {
225
387
  let i = 0;
226
388
  const next = (v = value) => {
227
389
  if (i < chain.length) chain[i++].call(this, v, next);
228
- else this._standardSetOption(key, v);
390
+ else if (!dataOption) this._standardSetOption(key, v, declared);
229
391
  };
230
392
  chain[i++].call(this, value, next);
231
393
  return;
232
394
  }
233
395
 
234
- this._standardSetOption(key, value);
396
+ if (dataOption) return;
397
+ this._standardSetOption(key, value, declared);
235
398
  }
236
399
 
237
400
  /**
238
401
  * Standard option routing pipeline - event handlers, special keys, attributes, methods, properties.
239
- * Called by _setOption when no handler claims the key, or when a handler calls next() past
402
+ * Called by _setOption when no set claims the key, or when a set calls next() past
240
403
  * the end of its chain.
404
+ *
405
+ * Declared keys never fall through to guessing: they route to a real match (elem property,
406
+ * registered event, attribute) or stay store-only, silently. The unknown-key warning and
407
+ * elem expando assignment apply only to undeclared keys.
241
408
  * @param {string} key - Option property name
242
409
  * @param {*} value - Value to apply
410
+ * @param {boolean} [declared] - Whether any class's schema declares this key
243
411
  * @private
244
412
  */
245
- _standardSetOption(key, value) {
413
+ _standardSetOption(key, value, declared = false) {
246
414
  if (key === 'onRendered') {
247
415
  this.onRendered = value;
248
416
  return;
@@ -252,10 +420,12 @@ class Component extends Elem {
252
420
  const targetEvent = key.replace(/^on/, '').toLowerCase();
253
421
  if (this.on({ targetEvent, id: key, callback: value })) return;
254
422
  if (typeof this[key] !== 'function') {
255
- if (isDev) {
423
+ // Declared callback options (e.g. onSort, onButtonPress) live in this.options,
424
+ // invoked by the component itself rather than the event system
425
+ if (!declared && isDev) {
256
426
  // eslint-disable-next-line no-console
257
427
  console.warn(
258
- `Component._setOption(): "${key}" starts with "on" but "${targetEvent}" is not a recognized event and there is no "${key}" method. Add "${targetEvent}" to registeredEvents to register it as a custom event.`,
428
+ `Component._setOption(): "${key}" starts with "on" but "${targetEvent}" is not a recognized event and there is no "${key}" method. Add "${targetEvent}" to static events to register it as a custom event.`,
259
429
  );
260
430
  }
261
431
  return;
@@ -277,18 +447,34 @@ class Component extends Elem {
277
447
  if (value?.elem) value = value.elem;
278
448
 
279
449
  this.elem[key].call(this.elem, value);
280
- } else if (typeof value === 'function') this[key] = value;
281
- else {
450
+ } else if (typeof value === 'function') {
451
+ if (declared) return;
452
+ this[key] = value;
453
+ } else {
454
+ if (!(key in this.elem) && declared) return;
282
455
  if (isDev && !(key in this.elem)) {
283
456
  // eslint-disable-next-line no-console
284
457
  console.warn(
285
- `Component._setOption(): unknown key "${key}" assigned directly to elem. If intentional, add to knownAttributes.`,
458
+ `Component._setOption(): unknown key "${key}" assigned directly to elem. If intentional, declare it in the static schema.`,
286
459
  );
287
460
  }
288
461
  this.elem[key] = value;
289
462
  }
290
463
  }
291
464
 
465
+ /**
466
+ * Looks up the declared enum for an option key from the schema chain (nearest class wins).
467
+ * @param {string} key - Option property name
468
+ * @returns {Array<*>|undefined} Valid values declared for the key, or undefined if unconstrained
469
+ */
470
+ optionEnum(key) {
471
+ for (const schema of collectStatics(this.constructor, 'schema')) {
472
+ if (schema[key]?.enum) return schema[key].enum;
473
+ }
474
+
475
+ return undefined;
476
+ }
477
+
292
478
  /**
293
479
  * Get parent Component instance.
294
480
  * @returns {Component|undefined} Parent component or undefined if none