@vanilla-bean/components 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/Component/Component.js +226 -40
  2. package/Component/Component.scenarios.js +2 -2
  3. package/Component/Component.test.js +427 -40
  4. package/Component/README.md +480 -0
  5. package/Component/observeElementConnection.js +1 -1
  6. package/Elem/README.md +373 -0
  7. package/README.md +2 -2
  8. package/components/BottomSheet/BottomSheet.js +8 -16
  9. package/components/BottomSheet/README.md +1 -1
  10. package/components/Button/Button.js +12 -18
  11. package/components/Button/Button.lld.md +1 -1
  12. package/components/Calendar/Calendar.js +139 -59
  13. package/components/Calendar/CalendarEvent.js +9 -4
  14. package/components/Calendar/Toolbar.js +28 -20
  15. package/components/Calendar/index.js +1 -0
  16. package/components/Code/Code.js +26 -30
  17. package/components/Code/Code.lld.md +1 -1
  18. package/components/ColorPicker/ColorPicker.js +58 -58
  19. package/components/Dialog/Dialog.js +79 -73
  20. package/components/Dialog/Dialog.lld.md +1 -1
  21. package/components/Dialog/README.md +10 -10
  22. package/components/Form/Form.js +21 -17
  23. package/components/Form/Form.lld.md +2 -2
  24. package/components/Form/README.md +3 -3
  25. package/components/Icon/Icon.js +19 -11
  26. package/components/Input/Input.js +85 -67
  27. package/components/Input/README.md +7 -9
  28. package/components/Keyboard/Key.js +7 -10
  29. package/components/Keyboard/Keyboard.js +38 -46
  30. package/components/Keyboard/Keyboard.lld.md +1 -1
  31. package/components/Label/Label.js +52 -50
  32. package/components/Link/Link.js +14 -20
  33. package/components/List/List.js +28 -30
  34. package/components/Menu/Menu.js +19 -10
  35. package/components/Menu/Menu.lld.md +2 -2
  36. package/components/Notify/Notify.js +29 -19
  37. package/components/Notify/Notify.lld.md +2 -2
  38. package/components/Page/Page.lld.md +1 -1
  39. package/components/Popover/Popover.js +49 -44
  40. package/components/RadioButton/RadioButton.js +33 -29
  41. package/components/RadioButton/RadioButton.lld.md +1 -1
  42. package/components/Router/README.md +12 -12
  43. package/components/Router/Router.js +19 -21
  44. package/components/Select/README.md +5 -5
  45. package/components/Select/Select.js +25 -31
  46. package/components/Table/README.md +4 -4
  47. package/components/Table/Table.js +55 -36
  48. package/components/Table/Table.lld.md +1 -1
  49. package/components/TagList/Tag.js +16 -14
  50. package/components/TagList/TagList.js +15 -8
  51. package/components/TagList/TagList.lld.md +1 -1
  52. package/components/Tooltip/Tooltip.js +12 -25
  53. package/components/TooltipWrapper/TooltipWrapper.js +55 -49
  54. package/components/TooltipWrapper/TooltipWrapper.lld.md +2 -2
  55. package/components/Whiteboard/Whiteboard.js +45 -43
  56. package/components/Whiteboard/Whiteboard.lld.md +1 -1
  57. package/devTools/build.js +43 -0
  58. package/devTools/buildTypes.js +322 -0
  59. package/devTools/createComponent.js +155 -0
  60. package/devTools/extractJSDoc.js +395 -0
  61. package/devTools/processTemplate.js +500 -0
  62. package/devTools/updateComponentIndex.js +16 -0
  63. package/devTools/updateDemoViewIndex.js +90 -0
  64. package/eslint.config.cjs +5 -2
  65. package/index.d.ts +120 -62
  66. package/package.json +23 -22
  67. package/spellcheck.config.cjs +3 -0
  68. package/styled/README.md +329 -0
  69. package/theme/.test.js +2 -2
  70. package/theme/README.md +8 -8
  71. package/theme/colors.js +16 -13
  72. package/theme/colors.test.js +28 -0
  73. package/utils/browser.js +1 -1
  74. package/utils/element.js +2 -2
  75. package/FontWithASyntaxHighlighter-Regular.woff2 +0 -0
@@ -3,7 +3,7 @@ import { styled } from '../../styled';
3
3
  import { Component } from '../../Component';
4
4
 
5
5
  import CalendarEvent from './CalendarEvent';
6
- import Toolbar from './Toolbar';
6
+ import Toolbar, { VIEWS } from './Toolbar';
7
7
  import { getDaysInMonth, toNth } from './utils';
8
8
 
9
9
  const StyledComponent = styled(
@@ -18,12 +18,17 @@ const StyledComponent = styled(
18
18
  & tr.title {
19
19
  font-size: 1.3em;
20
20
  height: 1.3em;
21
- color: ${colors.black};
22
- background-color: ${colors.lightest(colors.gray)};
21
+ color: ${colors.lightest(colors.gray)};
22
+ background-color: ${colors.darkest(colors.gray)};
23
23
 
24
24
  & td {
25
25
  text-align: center;
26
26
  }
27
+
28
+ & td:hover {
29
+ background-color: unset;
30
+ color: unset;
31
+ }
27
32
  }
28
33
  }
29
34
 
@@ -47,11 +52,11 @@ const StyledComponent = styled(
47
52
  & div.time-block {
48
53
  height: 3em;
49
54
  cursor: pointer;
50
- color: black;
51
- background-color: ${colors.light(colors.gray)};
55
+ color: ${colors.lighter(colors.gray)};
56
+ background-color: ${colors.darker(colors.gray)};
52
57
 
53
58
  &:nth-child(odd) {
54
- background-color: ${colors.lighter(colors.gray)};
59
+ background-color: ${colors.darkest(colors.gray)};
55
60
  }
56
61
 
57
62
  &:last-of-type{
@@ -61,8 +66,15 @@ const StyledComponent = styled(
61
66
  & span.time {
62
67
  font-size: 12px;
63
68
  margin: 2px;
69
+ padding: 1px 3px;
64
70
  pointer-events: none;
65
71
  }
72
+
73
+ &:hover span.time {
74
+ background-color: ${colors.light(colors.selected)};
75
+ color: ${colors.mostReadable(colors.light(colors.selected), [colors.white, colors.black])};
76
+ font-weight: 700;
77
+ }
66
78
  }
67
79
  }
68
80
  `,
@@ -99,40 +111,63 @@ const MonthDayCell = styled(
99
111
  position: relative;
100
112
  border-collapse: collapse;
101
113
  vertical-align: top;
102
- background-color: ${colors.lighter(colors.gray)};
114
+ background-color: ${colors.darker(colors.gray)};
103
115
  cursor: pointer;
104
116
  padding: 3px;
117
+ /* definite height so the inner flex column can fill the cell */
118
+ height: 1px;
119
+
120
+ & div.cell-body {
121
+ display: flex;
122
+ flex-direction: column;
123
+ height: 100%;
124
+ }
105
125
 
106
126
  & span.day-title {
107
127
  display: block;
108
128
  font-size: 1em;
109
129
  padding: 2px;
110
- background-color: ${colors.black.setAlpha(0.2)};
111
- color: ${colors.black};
130
+ background-color: ${colors.alpha(colors.black, 0.2)};
131
+ color: ${colors.lightest(colors.gray)};
112
132
  cursor: pointer;
113
133
  }
114
134
 
115
- &:hover {
116
- background-color: ${colors.blue};
135
+ & div.event-container {
136
+ background: ${colors.alpha(colors.black, 0.05)};
137
+ flex: 1;
138
+ min-height: 0;
139
+ margin-top: 2px;
140
+ border-radius: 6px;
141
+ padding: 3px;
142
+ }
117
143
 
118
- & span.day-title {
119
- background-color: ${colors.white.setAlpha(0.3)};
120
- }
144
+ /* Pin the box color - the page theme's global td:hover darkening must not show through */
145
+ &:hover {
146
+ background-color: ${colors.darker(colors.gray)};
121
147
  }
122
148
 
123
- &.today {
124
- background-color: ${colors.purple};
149
+ &:hover span.day-title,
150
+ &.today:hover span.day-title,
151
+ &.not-in-month:hover span.day-title {
152
+ background-color: ${colors.light(colors.selected)};
153
+ color: ${colors.mostReadable(colors.light(colors.selected), [colors.white, colors.black])};
154
+ font-weight: 700;
155
+ }
125
156
 
126
- & span.day-title {
127
- background-color: ${colors.white.setAlpha(0.3)};
128
- }
157
+ &.today span.day-title {
158
+ box-shadow: inset 0 -3px 0 ${colors.selected};
159
+ color: ${colors.white};
160
+ font-weight: 700;
129
161
  }
130
162
 
131
163
  &.not-in-month {
132
- background: #222;
133
-
134
164
  & span.day-title {
135
165
  color: ${colors.gray};
166
+ background: ${colors.alpha(colors.black, 0.3)};
167
+ }
168
+
169
+ & div.event-container {
170
+ background: ${colors.alpha(colors.black, 0.3)};
136
171
  }
137
172
  }
138
173
  `,
@@ -142,19 +177,28 @@ const WeekDayCell = styled(
142
177
  Component,
143
178
  ({ colors }) => `
144
179
  cursor: pointer;
145
- background-color: ${colors.lighter(colors.gray)};
180
+ background-color: ${colors.darker(colors.gray)};
146
181
  min-height: 14.285714285714286%;
147
182
 
148
- &.today {
149
- background-color: ${colors.blue};
150
- }
151
-
152
183
  & div.day-title {
153
184
  cursor: pointer;
154
185
  padding: 6px;
155
- background-color: ${colors.black.setAlpha(0.2)};
186
+ background-color: ${colors.alpha(colors.black, 0.2)};
156
187
  font-size: 1em;
157
- color: black;
188
+ color: ${colors.lightest(colors.gray)};
189
+ }
190
+
191
+ &:hover div.day-title,
192
+ &.today:hover div.day-title {
193
+ background-color: ${colors.light(colors.selected)};
194
+ color: ${colors.mostReadable(colors.light(colors.selected), [colors.white, colors.black])};
195
+ font-weight: 700;
196
+ }
197
+
198
+ &.today div.day-title {
199
+ box-shadow: inset 0 -3px 0 ${colors.selected};
200
+ color: ${colors.white};
201
+ font-weight: 700;
158
202
  }
159
203
 
160
204
  & .event {
@@ -209,27 +253,64 @@ export const MONTHS = [
209
253
  * @returns {Calendar} Calendar component instance
210
254
  */
211
255
  class Calendar extends StyledComponent {
212
- constructor(options = {}, ...children) {
213
- super(
214
- {
215
- view: 'month',
216
- height: '420px',
217
- registeredEvents: new Set(['selectDay', 'selectTime', 'selectEvent', 'newEvent']),
218
- ...options,
219
- events: (options.events || []).map(eventItem => new CalendarEvent(eventItem)),
256
+ static schema = {
257
+ height: {
258
+ default: '420px',
259
+ set(value) {
260
+ this.elem.style.height = value;
220
261
  },
221
- ...children,
222
- );
223
- }
262
+ },
263
+ // Date state read from this.options by the view renderers; navigation methods
264
+ // (setDate, next, previous) mutate several keys then re-render once
265
+ view: {
266
+ default: 'month',
267
+ enum: ['month', 'week', 'day'],
268
+ set() {
269
+ if (!this.rendered) return;
270
+ this.adjustDateToView();
271
+ this.renderView();
272
+ },
273
+ },
274
+ views: { default: VIEWS },
275
+ events: {
276
+ get default() {
277
+ return [];
278
+ },
279
+ set(value) {
280
+ // Normalize raw event objects; the re-assignment re-enters this set with instances only
281
+ if (value?.some(eventItem => !(eventItem instanceof CalendarEvent))) {
282
+ this.options.events = value.map(eventItem =>
283
+ eventItem instanceof CalendarEvent ? eventItem : new CalendarEvent(eventItem),
284
+ );
285
+ return;
286
+ }
224
287
 
225
- static handlers = {
226
- height(value) {
227
- this.elem.style.height = value;
288
+ if (this.rendered) this.renderView();
289
+ },
228
290
  },
291
+ year: {},
292
+ month: {},
293
+ day: {},
294
+ weekday: {},
295
+ display24h: { default: false },
229
296
  };
230
297
 
298
+ static events = ['selectDay', 'selectTime', 'selectEvent', 'newEvent'];
299
+
300
+ static prepareOptions(options) {
301
+ return {
302
+ ...options,
303
+ events: options.events.map(eventItem => new CalendarEvent(eventItem)),
304
+ };
305
+ }
306
+
231
307
  build() {
232
- this.toolbar = new Toolbar({ appendTo: this, calendar: this, views: this.options.views, view: this.options.view });
308
+ this.toolbar = new Toolbar({
309
+ appendTo: this,
310
+ calendar: this,
311
+ views: this.options.views,
312
+ view: this.options.subscriber('view'),
313
+ });
233
314
  this.wrapper = new CalendarWrapper({ appendTo: this });
234
315
 
235
316
  if (this.options.month == null) {
@@ -240,9 +321,7 @@ class Calendar extends StyledComponent {
240
321
  this.adjustDateToView();
241
322
  }
242
323
 
243
- if (this.options.view === 'day') this.renderDay();
244
- else if (this.options.view === 'week') this.renderWeek();
245
- else this.renderMonth();
324
+ this.renderView();
246
325
 
247
326
  this.wrapper.elem.scrollTop = 0;
248
327
 
@@ -360,7 +439,6 @@ class Calendar extends StyledComponent {
360
439
 
361
440
  const weekdayCell = new WeekDayCell({
362
441
  addClass: isToday ? 'today' : undefined,
363
- data: { at: cDay.getTime() },
364
442
  onPointerPress: ({ target }) => {
365
443
  this.emit('selectDay', { target, date: cellDate });
366
444
  },
@@ -449,12 +527,12 @@ class Calendar extends StyledComponent {
449
527
  const td = new MonthDayCell({
450
528
  tag: 'td',
451
529
  addClass: isCurrentMonth && currentDay === dayX ? 'today' : undefined,
452
- data: { at: date.getTime() },
453
530
  appendTo: tr,
454
531
  onPointerPress: event => {
455
532
  this.emit('selectDay', { target: event.target, date });
456
533
  },
457
- append: [
534
+ append: new Component(
535
+ { addClass: 'cell-body' },
458
536
  new Component({
459
537
  tag: 'span',
460
538
  textContent: date.getDate(),
@@ -464,7 +542,7 @@ class Calendar extends StyledComponent {
464
542
  },
465
543
  }),
466
544
  eventContainer,
467
- ],
545
+ ),
468
546
  });
469
547
 
470
548
  if (dayX <= 0 || dayX > month.numberOfDays) td.addClass('not-in-month');
@@ -620,18 +698,20 @@ class Calendar extends StyledComponent {
620
698
  * @returns {Calendar} This calendar instance for method chaining
621
699
  */
622
700
  setView(view) {
623
- if (this.options.view !== view) {
624
- this.options.view = view;
701
+ if (this.options.view !== view) this.options.view = view;
625
702
 
626
- this.adjustDateToView();
627
- this.wrapper.empty();
703
+ return this;
704
+ }
628
705
 
629
- if (view === 'day') this.renderDay();
630
- else if (view === 'week') this.renderWeek();
631
- else this.renderMonth();
632
- }
706
+ /**
707
+ * Clears the wrapper and renders the current view.
708
+ */
709
+ renderView() {
710
+ this.wrapper.empty();
633
711
 
634
- return this;
712
+ if (this.options.view === 'day') this.renderDay();
713
+ else if (this.options.view === 'week') this.renderWeek();
714
+ else this.renderMonth();
635
715
  }
636
716
 
637
717
  /**
@@ -7,12 +7,18 @@ const StyledEvent = styled(
7
7
  Component,
8
8
  ({ colors }) => `
9
9
  cursor: pointer;
10
- color: ${colors.black};
11
- background-color: ${colors.yellow};
10
+ color: ${colors.mostReadable(colors.selected, [colors.white, colors.black])};
11
+ background-color: ${colors.selected};
12
12
 
13
13
  & div {
14
14
  pointer-events: none;
15
15
  }
16
+
17
+ &:hover,
18
+ &:focus-visible {
19
+ outline: 2px solid ${colors.light(colors.selected)};
20
+ outline-offset: -2px;
21
+ }
16
22
  `,
17
23
  );
18
24
 
@@ -51,10 +57,9 @@ export default class CalendarEvent {
51
57
  };
52
58
 
53
59
  this.eventElem = new StyledEvent({
54
- data: { at: this.at },
55
60
  className: 'event',
56
61
  role: 'button',
57
- tabindex: '0',
62
+ tabIndex: 0,
58
63
  'aria-label': `${this.label}, ${formattedTime}${this.duration ? formatDuration(this.duration, ', ') : ''}`,
59
64
  style: { backgroundColor: this.color },
60
65
  append: [
@@ -22,24 +22,30 @@ const Right = styled(
22
22
  `,
23
23
  );
24
24
 
25
- const VIEWS = ['day', 'week', 'month'];
25
+ export const VIEWS = Object.freeze(['day', 'week', 'month']);
26
26
 
27
27
  class Toolbar extends Component {
28
- constructor({ calendar, view, ...options } = {}, ...children) {
29
- super(
30
- {
31
- ...options,
32
- style: {
33
- display: 'flex',
34
- justifyContent: 'space-between',
35
- ...options.style,
36
- },
28
+ static schema = {
29
+ views: { default: VIEWS },
30
+ calendar: {},
31
+ view: {
32
+ set(value) {
33
+ for (const [view, button] of Object.entries(this._viewButtons || {})) {
34
+ button.toggleClass('pressed', view === value);
35
+ }
37
36
  },
38
- ...children,
39
- );
37
+ },
38
+ };
40
39
 
41
- this.calendar = calendar;
42
- this._currentView = view;
40
+ static prepareOptions(options) {
41
+ return {
42
+ ...options,
43
+ style: {
44
+ display: 'flex',
45
+ justifyContent: 'space-between',
46
+ ...options.style,
47
+ },
48
+ };
43
49
  }
44
50
 
45
51
  build() {
@@ -51,7 +57,7 @@ class Toolbar extends Component {
51
57
  textContent: { next: '>', today: 'Today', previous: '<' }[action],
52
58
  'aria-label': action,
53
59
  onPointerPress: () => {
54
- this.calendar[action]();
60
+ this.options.calendar[action]();
55
61
  },
56
62
  }),
57
63
  ),
@@ -59,17 +65,19 @@ class Toolbar extends Component {
59
65
 
60
66
  this.title = new Title({ appendTo: this });
61
67
 
68
+ this._viewButtons = {};
69
+
62
70
  new Right({
63
71
  appendTo: this,
64
- append: (this.options.views || VIEWS).map(
72
+ append: this.options.views.map(
65
73
  view =>
66
- new Button({
67
- addClass: [`set-${view}`, ...(view === this._currentView ? ['pressed'] : [])],
74
+ (this._viewButtons[view] = new Button({
75
+ addClass: [`set-${view}`],
68
76
  textContent: capitalize(view),
69
77
  onPointerPress: () => {
70
- this.calendar.setView(view);
78
+ this.options.calendar.setView(view);
71
79
  },
72
- }),
80
+ })),
73
81
  ),
74
82
  });
75
83
  }
@@ -1,2 +1,3 @@
1
1
  export { default as Calendar, DAYS, MONTHS } from './Calendar';
2
+ export { default as CalendarEvent } from './CalendarEvent';
2
3
  export * from './utils';
@@ -4,8 +4,6 @@ import { Component } from '../../Component';
4
4
  import { Button } from '../Button';
5
5
  import { Notify } from '../Notify';
6
6
 
7
- const defaultOptions = { tag: 'code', language: 'javascript', multiline: 'auto' };
8
-
9
7
  /**
10
8
  * Code display component with syntax highlighting and optional copy functionality.
11
9
  *
@@ -21,21 +19,34 @@ const defaultOptions = { tag: 'code', language: 'javascript', multiline: 'auto'
21
19
  * @returns {Code} Code component instance
22
20
  */
23
21
  class Code extends Component {
24
- defaultOptions = { ...super.defaultOptions, ...defaultOptions };
25
-
26
- constructor(options = {}, ...children) {
27
- if ((options.multiline || defaultOptions.multiline) === 'auto') {
28
- options.multiline = (options.code || '').includes('\n');
29
- }
22
+ static schema = {
23
+ language: {
24
+ default: 'javascript',
25
+ set(value) {
26
+ if (this.options.multiline && this._code) {
27
+ this._code.removeClass(/\blanguage-\S+\b/g);
28
+ this._code.addClass(`language-${value}`);
29
+ }
30
30
 
31
- super(
32
- {
33
- ...defaultOptions,
34
- ...options,
35
- ...(options.multiline ? { tag: 'pre' } : { tag: 'code' }),
31
+ this.removeClass(/\blanguage-\S+\b/g);
32
+ this.addClass(`language-${value}`);
36
33
  },
37
- ...children,
38
- );
34
+ },
35
+ code: {
36
+ set(value) {
37
+ (this.options.multiline && this._code ? this._code : this).elem.textContent = value;
38
+ },
39
+ },
40
+ // Resolved in prepareOptions (decides the tag); read from this.options in build()
41
+ multiline: { default: 'auto' },
42
+ copyButton: { default: false },
43
+ };
44
+
45
+ static prepareOptions(options) {
46
+ const multiline =
47
+ (options.multiline ?? 'auto') === 'auto' ? (options.code || '').includes('\n') : options.multiline;
48
+
49
+ return { ...options, multiline, tag: multiline ? 'pre' : 'code' };
39
50
  }
40
51
 
41
52
  build() {
@@ -64,21 +75,6 @@ class Code extends Component {
64
75
  }),
65
76
  );
66
77
  }
67
-
68
- static handlers = {
69
- language(value) {
70
- if (this.options.multiline && this._code) {
71
- this._code.removeClass(/\blanguage-\S+\b/g);
72
- this._code.addClass(`language-${value}`);
73
- }
74
-
75
- this.removeClass(/\blanguage-\S+\b/g);
76
- this.addClass(`language-${value}`);
77
- },
78
- code(value) {
79
- (this.options.multiline && this._code ? this._code : this).elem.textContent = value;
80
- },
81
- };
82
78
  }
83
79
 
84
80
  export default Code;
@@ -15,7 +15,7 @@ Code display that automatically promotes from inline to block when the content s
15
15
  - the `language` option adds a `language-{name}` class; highlighting libraries target this class by convention
16
16
  - does the language option add an identifiable class to the code element?
17
17
 
18
- ## Copy confirms success visually — clipboard writes can fail silently
18
+ ## Copy confirms success visually - clipboard writes can fail silently
19
19
 
20
20
  - the copy button shows a notification on success so the user knows the copy worked rather than discovering it failed on paste
21
21
  - does a successful copy show the user a confirmation?
@@ -143,8 +143,6 @@ const ColorSwatch = styled(
143
143
  { icon: 'fill-drip' },
144
144
  );
145
145
 
146
- const defaultOptions = { tag: 'div', value: '#666', onChange: () => {} };
147
-
148
146
  /**
149
147
  * Interactive color picker component with HSL controls and color swatches.
150
148
  *
@@ -159,19 +157,53 @@ const defaultOptions = { tag: 'div', value: '#666', onChange: () => {} };
159
157
  * @returns {ColorPicker} ColorPicker component instance
160
158
  */
161
159
  class ColorPicker extends StyledInput {
162
- defaultOptions = { ...super.defaultOptions, ...defaultOptions };
163
-
164
- constructor(options = {}, ...children) {
165
- options = { ...defaultOptions, ...options };
166
-
167
- if (options.value === 'random') options.value = randomColor().toHslString();
168
-
169
- super(options, ...children);
160
+ static schema = {
161
+ tag: { default: 'div' },
162
+ value: {
163
+ default: '#666',
164
+ set(value) {
165
+ if (!value) return;
166
+
167
+ const { h, sv, v, color, hslString } = this.parseValue(value);
168
+
169
+ this.hslString = hslString;
170
+ this.color = color;
171
+ // Preserve hue when achromatic (sv=0 loses hue info) or when TinyColor
172
+ // wraps 360 to 0 (same color, different position on the slider)
173
+ const isCircularWrap = sv > 0 && h === 0 && this.hue > 350;
174
+ if (!isCircularWrap) this.hue = sv > 0 ? h : (this.hue ?? h);
175
+ this.sv = sv;
176
+ this.v = v;
177
+
178
+ this.elem.style.backgroundColor = hslString;
179
+
180
+ this.pickerArea?.elem && (this.pickerArea.elem.style.backgroundColor = `hsl(${this.hue}, 100%, 50%)`);
181
+
182
+ const tryPosition = () => {
183
+ if (this.pickerArea?.elem?.clientWidth > 0) {
184
+ this._positionIndicators();
185
+ } else {
186
+ requestAnimationFrame(tryPosition);
187
+ }
188
+ };
189
+ requestAnimationFrame(tryPosition);
190
+ },
191
+ },
192
+ onChange: { default: () => {} },
193
+ swatches: {
194
+ get default() {
195
+ return [];
196
+ },
197
+ },
198
+ };
170
199
 
171
- this.pointers = {};
200
+ static prepareOptions(options) {
201
+ return options.value === 'random' ? { ...options, value: randomColor().toHslString() } : options;
172
202
  }
173
203
 
174
204
  build() {
205
+ this.pointers = {};
206
+
175
207
  this.textInput = new Input({
176
208
  type: 'text',
177
209
  value: this.options.subscriber('value', value => this.hslString || value),
@@ -204,23 +236,21 @@ class ColorPicker extends StyledInput {
204
236
  sizeObserver.observe(this.pickerArea.elem);
205
237
  this.addCleanup('sizeObserver', () => sizeObserver.disconnect());
206
238
 
207
- if (this.options.swatches) {
208
- this.options.swatches.forEach(color => {
209
- new ColorSwatch({
210
- appendTo: this.elem,
211
-
212
- onPointerPress: () => this.change(color),
213
- ...(color === 'random'
214
- ? { addClass: ['rainbow'] }
215
- : {
216
- style: {
217
- backgroundColor: color,
218
- color: theme.colors.mostReadable(color, [theme.colors.white, theme.colors.black]),
219
- },
220
- }),
221
- });
239
+ this.options.swatches.forEach(color => {
240
+ new ColorSwatch({
241
+ appendTo: this.elem,
242
+
243
+ onPointerPress: () => this.change(color),
244
+ ...(color === 'random'
245
+ ? { addClass: ['rainbow'] }
246
+ : {
247
+ style: {
248
+ backgroundColor: color,
249
+ color: theme.colors.mostReadable(color, [theme.colors.white, theme.colors.black]),
250
+ },
251
+ }),
222
252
  });
223
- }
253
+ });
224
254
  }
225
255
 
226
256
  /**
@@ -283,7 +313,7 @@ class ColorPicker extends StyledInput {
283
313
 
284
314
  const clamp = (val, min, max) => Math.min(max, Math.max(min, val));
285
315
 
286
- // HSV: x = saturation, y = (1 - value) — matches the gradient exactly
316
+ // HSV: x = saturation, y = (1 - value) - matches the gradient exactly
287
317
  const pickerX = clamp((this.sv ?? 0) * pickerW, 0, pickerW);
288
318
  const pickerY = clamp((1 - (this.v ?? 1)) * pickerH, 0, pickerH);
289
319
  this.pickerIndicator.elem.style.transform = `translate3d(${pickerX}px, ${pickerY}px, 0)`;
@@ -292,36 +322,6 @@ class ColorPicker extends StyledInput {
292
322
  this._setHueBackground(this.hue ?? 0, hueW);
293
323
  }
294
324
 
295
- static handlers = {
296
- value(value) {
297
- if (!value) return;
298
-
299
- const { h, sv, v, color, hslString } = this.parseValue(value);
300
-
301
- this.hslString = hslString;
302
- this.color = color;
303
- // Preserve hue when achromatic (sv=0 loses hue info) or when TinyColor
304
- // wraps 360 to 0 (same color, different position on the slider)
305
- const isCircularWrap = sv > 0 && h === 0 && this.hue > 350;
306
- if (!isCircularWrap) this.hue = sv > 0 ? h : (this.hue ?? h);
307
- this.sv = sv;
308
- this.v = v;
309
-
310
- this.elem.style.backgroundColor = hslString;
311
-
312
- this.pickerArea?.elem && (this.pickerArea.elem.style.backgroundColor = `hsl(${this.hue}, 100%, 50%)`);
313
-
314
- const tryPosition = () => {
315
- if (this.pickerArea?.elem?.clientWidth > 0) {
316
- this._positionIndicators();
317
- } else {
318
- requestAnimationFrame(tryPosition);
319
- }
320
- };
321
- requestAnimationFrame(tryPosition);
322
- },
323
- };
324
-
325
325
  getPosition({ clientX, clientY }) {
326
326
  return { x: clientX, y: clientY };
327
327
  }