@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
@@ -11,19 +11,7 @@ const DialogButton = styled(
11
11
  `,
12
12
  );
13
13
 
14
- const size_enum = Object.freeze(['small', 'standard', 'large']);
15
- const variant_enum = Object.freeze(['info', 'success', 'warning', 'error']);
16
-
17
- const defaultOptions = {
18
- tag: 'dialog',
19
- size: 'small',
20
- openOnRender: 16,
21
- modal: true,
22
- get appendTo() {
23
- return document.body;
24
- },
25
- registeredEvents: new Set(['close']),
26
- };
14
+ const defaultOpenDelay = 16;
27
15
 
28
16
  /**
29
17
  * Modal and non-modal dialog component with customizable appearance and behavior.
@@ -149,57 +137,70 @@ class Dialog extends styled(
149
137
  }
150
138
  `,
151
139
  ) {
152
- variant_enum = variant_enum;
153
- size_enum = size_enum;
154
-
155
- static handlers = {
156
- openOnRender(value) {
157
- const delay = typeof value === 'number' ? value : defaultOptions.openOnRender;
158
- const openDelay = value ? setTimeout(() => this.open(), delay) : null;
159
- this.replaceCleanup('openOnRender', () => openDelay && clearTimeout(openDelay));
140
+ static schema = {
141
+ tag: { default: 'dialog' },
142
+ appendTo: {
143
+ get default() {
144
+ return document.body;
145
+ },
160
146
  },
161
- // Claim the key so standard routing doesn't try to call elem.showModal() or set elem.modal.
162
- // The value is read directly from this.options.modal in open().
163
- modal() {},
164
- size(value) {
165
- if (value && !size_enum.includes(value)) {
166
- throw new Error(
167
- `"${value}" is not a valid size. The size must be one of the following values: ${size_enum.join(', ')}`,
168
- );
169
- }
170
-
171
- this.removeClass(/\bsize-\S+\b/g);
172
-
173
- if (value) this.addClass(`size-${value}`);
147
+ openOnRender: {
148
+ default: defaultOpenDelay,
149
+ set(value) {
150
+ const delay = typeof value === 'number' ? value : defaultOpenDelay;
151
+ const openDelay = value ? setTimeout(() => this.open(), delay) : null;
152
+ this.replaceCleanup('openOnRender', () => openDelay && clearTimeout(openDelay));
153
+ },
174
154
  },
175
- variant(value) {
176
- if (value && !variant_enum.includes(value)) {
177
- throw new Error(
178
- `"${value}" is not a valid variant. The variant must be one of the following values: ${variant_enum.join(', ')}`,
179
- );
180
- }
181
-
182
- this.removeClass(/\bvariant-\S+\b/g);
155
+ // Read directly from this.options.modal in open()
156
+ modal: { default: true },
157
+ size: {
158
+ default: 'small',
159
+ enum: ['small', 'standard', 'large'],
160
+ set(value) {
161
+ this.removeClass(/\bsize-\S+\b/g);
162
+
163
+ if (value) this.addClass(`size-${value}`);
164
+ },
165
+ },
166
+ variant: {
167
+ enum: ['info', 'success', 'warning', 'error'],
168
+ set(value) {
169
+ this.removeClass(/\bvariant-\S+\b/g);
183
170
 
184
- if (value) this.addClass(`variant-${value}`);
171
+ if (value) this.addClass(`variant-${value}`);
172
+ },
173
+ },
174
+ body: {
175
+ set(value) {
176
+ this.body?.content(value);
177
+ },
178
+ },
179
+ header: {
180
+ set(value) {
181
+ this._header?.content(value);
182
+ },
185
183
  },
186
- body(value) {
187
- this.body?.content(value);
184
+ buttons: {
185
+ set() {
186
+ this._refreshFooter();
187
+ },
188
188
  },
189
- header(value) {
190
- this._header?.content(value);
189
+ footer: {
190
+ set() {
191
+ this._refreshFooter();
192
+ },
191
193
  },
194
+ // Read from this.options at button-press time
195
+ closeDialog: {},
196
+ onButtonPress: {},
192
197
  };
193
198
 
194
- defaultOptions = { ...super.defaultOptions, ...defaultOptions };
195
-
196
- constructor(options = {}, ...children) {
197
- super({ ...defaultOptions, ...options }, ...children);
199
+ static events = ['close'];
198
200
 
201
+ build() {
199
202
  this.options['aria-labelledby'] = this.uniqueId;
200
- }
201
203
 
202
- build() {
203
204
  this._header = new Elem({
204
205
  tag: 'div',
205
206
  id: this.uniqueId,
@@ -210,25 +211,30 @@ class Dialog extends styled(
210
211
 
211
212
  this._body = new Elem({ addClass: ['content'], appendTo: this });
212
213
 
213
- this._footer = new Elem({
214
- addClass: ['footer'],
215
- append:
216
- this.options.footer ||
217
- this.options.buttons?.map(
218
- button =>
219
- new DialogButton({
220
- onPointerPress: event =>
221
- this.options.onButtonPress?.({
222
- event,
223
- button,
224
- dialog: this,
225
- closeDialog: this.options.closeDialog || (() => this.close()),
226
- }),
227
- ...(typeof button === 'object' ? button : { textContent: button }),
228
- }),
229
- ),
230
- appendTo: this,
231
- });
214
+ this._footer = new Elem({ addClass: ['footer'], append: this._footerContent(), appendTo: this });
215
+ }
216
+
217
+ _refreshFooter() {
218
+ if (this.rendered) this._footer?.content(this._footerContent() || []);
219
+ }
220
+
221
+ _footerContent() {
222
+ return (
223
+ this.options.footer ||
224
+ this.options.buttons?.map(
225
+ button =>
226
+ new DialogButton({
227
+ onPointerPress: event =>
228
+ this.options.onButtonPress?.({
229
+ event,
230
+ button,
231
+ dialog: this,
232
+ closeDialog: this.options.closeDialog || (() => this.close()),
233
+ }),
234
+ ...(typeof button === 'object' ? button : { textContent: button }),
235
+ }),
236
+ )
237
+ );
232
238
  }
233
239
 
234
240
  /**
@@ -65,15 +65,19 @@ export default class Form extends Component {
65
65
  * Rebuild the form when inputs change without recreating the data store.
66
66
  * Existing data is preserved; new keys are added with their initial values.
67
67
  */
68
- static handlers = {
69
- inputs(value) {
70
- if (!this.rendered || !value || !this.options.data) return;
71
- this.empty();
72
- this._resetState();
73
- this._processInputs(value, this);
74
- this._wireConditions();
75
- this._setupAnnouncer();
68
+ static schema = {
69
+ inputs: {
70
+ set(value) {
71
+ if (!this.rendered || !value || !this.options.data) return;
72
+ this.empty();
73
+ this._resetState();
74
+ this._processInputs(value, this);
75
+ this._wireConditions();
76
+ this._setupAnnouncer();
77
+ },
76
78
  },
79
+ // Data store managed by build()
80
+ data: {},
77
81
  };
78
82
 
79
83
  /** @private */
@@ -14,20 +14,28 @@ import { Component } from '../../Component';
14
14
  * @returns {Icon} Icon component instance
15
15
  */
16
16
  export default class Icon extends Component {
17
- static handlers = {
18
- icon() {
19
- this._refreshIcon();
17
+ static schema = {
18
+ icon: {
19
+ set() {
20
+ this._refreshIcon();
21
+ },
20
22
  },
21
- animation() {
22
- this._refreshIcon();
23
+ animation: {
24
+ set() {
25
+ this._refreshIcon();
26
+ },
23
27
  },
24
- content(value, next) {
25
- next(value);
26
- this._refreshIcon();
28
+ content: {
29
+ set(value, next) {
30
+ next(value);
31
+ this._refreshIcon();
32
+ },
27
33
  },
28
- textContent(value, next) {
29
- next(value);
30
- this._refreshIcon();
34
+ textContent: {
35
+ set(value, next) {
36
+ next(value);
37
+ this._refreshIcon();
38
+ },
31
39
  },
32
40
  };
33
41
 
@@ -26,13 +26,11 @@ const type_enum = Object.freeze([
26
26
  'url',
27
27
  'week',
28
28
  ]);
29
- const defaultOptions = {
30
- tag: 'input',
31
- value: '',
29
+ // Only meaningful on input/textarea elements - child classes rendering other tags (e.g. ColorPicker's div) must not receive them
30
+ const textInputDefaults = {
32
31
  autocomplete: 'off',
33
32
  autocapitalize: 'off',
34
33
  autocorrect: 'off',
35
- height: 'auto',
36
34
  placeholder: '',
37
35
  };
38
36
 
@@ -59,19 +57,91 @@ const dataTypeToInputType = { number: 'number', boolean: 'checkbox', string: 'te
59
57
  * @returns {Input} Input component instance
60
58
  */
61
59
  export default class Input extends Component {
62
- defaultOptions = { ...super.defaultOptions, ...defaultOptions };
63
- type_enum = type_enum;
60
+ static schema = {
61
+ tag: { default: 'input' },
62
+ type: { enum: type_enum },
63
+ height: {
64
+ default: 'auto',
65
+ set(value) {
66
+ if (this.tag !== 'textarea') return;
67
+
68
+ if (value === 'auto') {
69
+ this.__updateAutoHeight = () => {
70
+ this.elem.style.height = this.options.syntaxHighlighting
71
+ ? `calc(${((this.elem.value?.match(/\n/g) || '').length + 1) * 1.19}em + 10px)`
72
+ : `calc(${((this.elem.value?.match(/\n/g) || '').length + 1) * 1.25}em + 16px)`;
73
+ };
74
+
75
+ this.__debouncedUpdateAutoHeight =
76
+ this.__debouncedUpdateAutoHeight ||
77
+ (() => {
78
+ clearTimeout(this.__autoHeightTimeout);
79
+ this.__autoHeightTimeout = setTimeout(this.__updateAutoHeight, 16);
80
+ });
81
+
82
+ this.__updateAutoHeight();
83
+
84
+ this.elem.addEventListener('input', this.__debouncedUpdateAutoHeight);
85
+
86
+ this.replaceCleanup('autoHeight', () => {
87
+ clearTimeout(this.__autoHeightTimeout);
88
+ this.elem.removeEventListener?.('input', this.__debouncedUpdateAutoHeight);
89
+ });
90
+ } else {
91
+ this.elem.removeEventListener('input', this.__debouncedUpdateAutoHeight);
92
+ clearTimeout(this.__autoHeightTimeout);
64
93
 
65
- constructor(options = {}, ...children) {
66
- const rawValue = options.value?.__isDerived ? options.value.toJSON() : options.value;
67
- super(
68
- {
69
- ...((options.tag || 'input') === 'input' ? { type: dataTypeToInputType[typeof rawValue] || 'text' } : {}),
70
- ...defaultOptions,
71
- ...options,
94
+ this.elem.style.height = typeof value === 'number' ? `${value + 1}em` : value;
95
+ }
96
+ },
97
+ },
98
+ value: {
99
+ default: '',
100
+ set(value) {
101
+ if (this.options.type === 'checkbox') this.elem.checked = value;
102
+ else this.elem.value = value;
103
+
104
+ if (this.rendered && this.options.validations?.length) retry(() => this.validate(), { delay: 500, max: 1 });
105
+ },
106
+ },
107
+ syntaxHighlighting: {
108
+ set(value) {
109
+ this[value ? 'addClass' : 'removeClass']('syntax-highlighting');
72
110
  },
73
- ...children,
74
- );
111
+ },
112
+ language: {
113
+ set(value) {
114
+ if (!this.options.syntaxHighlighting) return;
115
+ this.removeClass(/\blanguage-\S+\b/g);
116
+ this.addClass(`language-${value}`);
117
+ },
118
+ },
119
+ autocorrect: {
120
+ set(value) {
121
+ this.elem.autocorrect = value;
122
+ },
123
+ },
124
+ // Read by validate() and the value handler
125
+ validations: {
126
+ get default() {
127
+ return [];
128
+ },
129
+ },
130
+ };
131
+
132
+ static prepareOptions(options) {
133
+ const rawValue = options.value?.__isDerived ? options.value.toJSON() : options.value;
134
+ const tag = options.tag || 'input';
135
+
136
+ return {
137
+ ...(tag === 'input' ? { type: dataTypeToInputType[typeof rawValue] || 'text' } : {}),
138
+ ...(tag === 'input' || tag === 'textarea' ? textInputDefaults : {}),
139
+ ...options,
140
+ };
141
+ }
142
+
143
+ constructor(options = {}, ...children) {
144
+ super(options, ...children);
75
145
 
76
146
  this.initialValue = this.options.value;
77
147
  }
@@ -93,58 +163,6 @@ export default class Input extends Component {
93
163
  }
94
164
  }
95
165
 
96
- static handlers = {
97
- height(value) {
98
- if (this.tag !== 'textarea') return;
99
-
100
- if (value === 'auto') {
101
- this.__updateAutoHeight = () => {
102
- this.elem.style.height = this.options.syntaxHighlighting
103
- ? `calc(${((this.elem.value?.match(/\n/g) || '').length + 1) * 1.19}em + 10px)`
104
- : `calc(${((this.elem.value?.match(/\n/g) || '').length + 1) * 1.25}em + 16px)`;
105
- };
106
-
107
- this.__debouncedUpdateAutoHeight =
108
- this.__debouncedUpdateAutoHeight ||
109
- (() => {
110
- clearTimeout(this.__autoHeightTimeout);
111
- this.__autoHeightTimeout = setTimeout(this.__updateAutoHeight, 16);
112
- });
113
-
114
- this.__updateAutoHeight();
115
-
116
- this.elem.addEventListener('input', this.__debouncedUpdateAutoHeight);
117
-
118
- this.replaceCleanup('autoHeight', () => {
119
- clearTimeout(this.__autoHeightTimeout);
120
- this.elem.removeEventListener?.('input', this.__debouncedUpdateAutoHeight);
121
- });
122
- } else {
123
- this.elem.removeEventListener('input', this.__debouncedUpdateAutoHeight);
124
- clearTimeout(this.__autoHeightTimeout);
125
-
126
- this.elem.style.height = typeof value === 'number' ? `${value + 1}em` : value;
127
- }
128
- },
129
- value(value) {
130
- if (this.options.type === 'checkbox') this.elem.checked = value;
131
- else this.elem.value = value;
132
-
133
- if (this.rendered && this.options.validations?.length) retry(() => this.validate(), { delay: 500, max: 1 });
134
- },
135
- syntaxHighlighting(value) {
136
- this[value ? 'addClass' : 'removeClass']('syntax-highlighting');
137
- },
138
- language(value) {
139
- if (!this.options.syntaxHighlighting) return;
140
- this.removeClass(/\blanguage-\S+\b/g);
141
- this.addClass(`language-${value}`);
142
- },
143
- autocorrect(value) {
144
- this.elem.autocorrect = value;
145
- },
146
- };
147
-
148
166
  /**
149
167
  * Checks if the input value has changed from its initial value
150
168
  * @returns {boolean} True if value is no longer the initial value
@@ -69,8 +69,6 @@ input.isDirty: boolean // getter - true if current value differs from the initi
69
69
 
70
70
  Input components emit enhanced native events. All handlers (`onInput`, `onChange`, `onKeyUp`, etc.) receive a standard DOM event augmented with a `.value` property containing the current input value.
71
71
 
72
- The `close` event is registered by default via `registeredEvents`.
73
-
74
72
  ## Example
75
73
 
76
74
  Textarea with auto-height and syntax highlighting:
@@ -21,17 +21,14 @@ const StyledButton = styled(
21
21
  );
22
22
 
23
23
  class Key extends StyledButton {
24
- constructor(options = {}, ...children) {
25
- const { key, text, class: keyClass, ...optionsWithoutConfig } = options;
24
+ static prepareOptions(options) {
25
+ const { key, text, class: keyClass, ...rest } = options;
26
26
 
27
- super(
28
- {
29
- textContent: text ?? key,
30
- ...optionsWithoutConfig,
31
- addClass: [key, keyClass].concat(options.addClass),
32
- },
33
- ...children,
34
- );
27
+ return {
28
+ textContent: text ?? key,
29
+ ...rest,
30
+ addClass: [key, keyClass].concat(options.addClass),
31
+ };
35
32
  }
36
33
  }
37
34
 
@@ -24,54 +24,46 @@ const KeyRow = styled(
24
24
  * @returns {Keyboard} Keyboard component instance
25
25
  */
26
26
  class Keyboard extends Component {
27
- constructor(options = {}, ...children) {
28
- super(
29
- {
30
- layout: 'simple',
31
- tactileResponse: true,
32
- keyDefinitions: {
33
- simple: { text: 'ABC' },
34
- number: { text: '123' },
35
- shift: { class: 'u1_5' },
36
- return: { class: 'u1_5' },
37
- backspace: { class: 'u1_5' },
38
- clear: { class: 'u1_5' },
39
- done: { class: 'u1_5' },
40
- exponent: { key: 'e', text: 'e', class: 'u1_5' },
41
- space: { key: ' ', text: ' ', class: 'u6' },
42
- },
43
- layouts: {
44
- simple: [
45
- ['simple', 'number'],
46
- ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'],
47
- ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'],
48
- ['shift', 'z', 'x', 'c', 'v', 'b', 'n', 'm', 'backspace'],
49
- [',', '.', 'space', '!', 'return'],
50
- ],
51
- number: [
52
- ['simple', 'number'],
53
- ['1', '2', '3', 'backspace'],
54
- ['4', '5', '6', 'clear'],
55
- ['7', '8', '9', 'done'],
56
- ['.', '0', '-', 'exponent'],
57
- ],
58
- },
59
- ...options,
27
+ static schema = {
28
+ layout: {
29
+ default: 'simple',
30
+ set(value) {
31
+ this.setLayout(value);
32
+ },
33
+ },
34
+ // Read directly from this.options in setLayout() and key press handling
35
+ layouts: {
36
+ default: {
37
+ simple: [
38
+ ['simple', 'number'],
39
+ ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'],
40
+ ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'],
41
+ ['shift', 'z', 'x', 'c', 'v', 'b', 'n', 'm', 'backspace'],
42
+ [',', '.', 'space', '!', 'return'],
43
+ ],
44
+ number: [
45
+ ['simple', 'number'],
46
+ ['1', '2', '3', 'backspace'],
47
+ ['4', '5', '6', 'clear'],
48
+ ['7', '8', '9', 'done'],
49
+ ['.', '0', '-', 'exponent'],
50
+ ],
51
+ },
52
+ },
53
+ keyDefinitions: {
54
+ default: {
55
+ simple: { text: 'ABC' },
56
+ number: { text: '123' },
57
+ shift: { class: 'u1_5' },
58
+ return: { class: 'u1_5' },
59
+ backspace: { class: 'u1_5' },
60
+ clear: { class: 'u1_5' },
61
+ done: { class: 'u1_5' },
62
+ exponent: { key: 'e', text: 'e', class: 'u1_5' },
63
+ space: { key: ' ', text: ' ', class: 'u6' },
60
64
  },
61
- ...children,
62
- );
63
-
64
- this.setLayout(this.options.layout);
65
- }
66
-
67
- static handlers = {
68
- // Claim these keys so standard routing doesn't try to assign them as elem properties.
69
- // Both are read directly from this.options in setLayout() and key resolution.
70
- layouts() {},
71
- keyDefinitions() {},
72
- layout(value) {
73
- this.setLayout(value);
74
65
  },
66
+ tactileResponse: { default: true },
75
67
  };
76
68
 
77
69
  // todo native keyboard events https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/KeyboardEvent