@adia-ai/web-components 0.4.4 → 0.4.6

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 (53) hide show
  1. package/README.md +63 -24
  2. package/USAGE.md +584 -0
  3. package/components/calendar-picker/calendar-picker.d.ts +27 -0
  4. package/components/calendar-picker/calendar-picker.js +1 -1
  5. package/components/check/check.d.ts +30 -0
  6. package/components/check/check.js +1 -1
  7. package/components/code/code.d.ts +39 -0
  8. package/components/color-picker/color-picker.d.ts +37 -0
  9. package/components/index.js +1 -0
  10. package/components/input/input.d.ts +61 -0
  11. package/components/input/input.js +9 -9
  12. package/components/option-card/option-card.d.ts +30 -0
  13. package/components/option-card/option-card.js +1 -1
  14. package/components/otp-input/otp-input.d.ts +25 -0
  15. package/components/otp-input/otp-input.js +3 -3
  16. package/components/pane/pane.css +10 -0
  17. package/components/pane/pane.js +28 -4
  18. package/components/radio/radio.d.ts +28 -0
  19. package/components/radio/radio.js +1 -1
  20. package/components/range/range.d.ts +31 -0
  21. package/components/range/range.js +3 -3
  22. package/components/rating/rating.d.ts +33 -0
  23. package/components/rating/rating.js +1 -1
  24. package/components/search/search.d.ts +35 -0
  25. package/components/search/search.js +2 -2
  26. package/components/segmented/segmented.d.ts +24 -0
  27. package/components/select/select.d.ts +57 -0
  28. package/components/select/select.js +2 -2
  29. package/components/slider/slider.d.ts +31 -0
  30. package/components/slider/slider.js +4 -4
  31. package/components/slider/slider.test.js +105 -0
  32. package/components/switch/switch.d.ts +30 -0
  33. package/components/switch/switch.js +1 -1
  34. package/components/textarea/textarea.d.ts +31 -0
  35. package/components/textarea/textarea.js +2 -2
  36. package/components/toggle-scheme/toggle-scheme.a2ui.json +197 -0
  37. package/components/toggle-scheme/toggle-scheme.css +20 -0
  38. package/components/toggle-scheme/toggle-scheme.js +277 -0
  39. package/components/toggle-scheme/toggle-scheme.yaml +173 -0
  40. package/components/upload/upload.d.ts +27 -0
  41. package/components/upload/upload.js +1 -1
  42. package/core/element.d.ts +174 -0
  43. package/core/form.d.ts +108 -0
  44. package/core/index.d.ts +11 -0
  45. package/core/index.js +1 -0
  46. package/core/register.d.ts +25 -0
  47. package/core/register.js +58 -0
  48. package/core/signals.d.ts +94 -0
  49. package/core/template.d.ts +70 -0
  50. package/index.d.ts +161 -0
  51. package/package.json +22 -6
  52. package/styles/design-tokens-export.js +554 -0
  53. package/traits/CATEGORIES.md +1 -1
@@ -0,0 +1,39 @@
1
+ /**
2
+ * `<code-ui>` — Syntax-highlighted code block (CodeMirror 6 under the hood).
3
+ *
4
+ * @see https://ui-kit.exe.xyz/site/components/code
5
+ */
6
+
7
+ import { UIFormElement } from '../../core/form.js';
8
+
9
+ export interface CodeChangeEventDetail {
10
+ value: string;
11
+ }
12
+ export type CodeChangeEvent = CustomEvent<CodeChangeEventDetail>;
13
+ export type CodeInputEvent = CustomEvent<CodeChangeEventDetail>;
14
+
15
+ export class UICode extends UIFormElement {
16
+ /** CodeMirror language slug — `javascript` / `typescript` / `html` / `css` / `json` / `markdown` / `python` etc. */
17
+ language: string;
18
+ /** Inline mode — drops chrome, renders as a single-line code fragment. */
19
+ inline: boolean;
20
+ /** Text content of the code block (alternative to slotted children). */
21
+ text: string;
22
+ /** Alias for `text` — set via `el.textContent` round-trips. */
23
+ textContent: string;
24
+ /** Show line numbers in the gutter. */
25
+ lineNumbers: boolean;
26
+ /** Editable mode — user can type. Form-associated only when this is true. */
27
+ editable: boolean;
28
+ /** Drop chrome (border, background) for inline composition. */
29
+ bare: boolean;
30
+ placeholder: string;
31
+
32
+ addEventListener<K extends keyof HTMLElementEventMap>(
33
+ type: K,
34
+ listener: (this: UICode, ev: HTMLElementEventMap[K]) => unknown,
35
+ options?: boolean | AddEventListenerOptions,
36
+ ): void;
37
+ addEventListener(type: 'change', listener: (ev: CodeChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
38
+ addEventListener(type: 'input', listener: (ev: CodeInputEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
39
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * `<color-picker-ui>` — Color picker with hex / rgb / hsl / oklch formats.
3
+ *
4
+ * @see https://ui-kit.exe.xyz/site/components/color-picker
5
+ */
6
+
7
+ import { UIFormElement } from '../../core/form.js';
8
+
9
+ export type ColorFormat = 'hex' | 'rgb' | 'hsl' | 'oklch';
10
+
11
+ export interface ColorPickerChangeEventDetail {
12
+ value: string;
13
+ format: ColorFormat;
14
+ }
15
+ export type ColorPickerChangeEvent = CustomEvent<ColorPickerChangeEventDetail>;
16
+ export type ColorPickerInputEvent = CustomEvent<ColorPickerChangeEventDetail>;
17
+
18
+ export interface ColorPickerFormatChangeEventDetail {
19
+ format: ColorFormat;
20
+ }
21
+ export type ColorPickerFormatChangeEvent = CustomEvent<ColorPickerFormatChangeEventDetail>;
22
+
23
+ export class UIColorPicker extends UIFormElement {
24
+ /** Current color as a string in the active `format`. */
25
+ value: string;
26
+ /** Output format. */
27
+ format: ColorFormat;
28
+
29
+ addEventListener<K extends keyof HTMLElementEventMap>(
30
+ type: K,
31
+ listener: (this: UIColorPicker, ev: HTMLElementEventMap[K]) => unknown,
32
+ options?: boolean | AddEventListenerOptions,
33
+ ): void;
34
+ addEventListener(type: 'change', listener: (ev: ColorPickerChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
35
+ addEventListener(type: 'input', listener: (ev: ColorPickerInputEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
36
+ addEventListener(type: 'format-change', listener: (ev: ColorPickerFormatChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
37
+ }
@@ -87,6 +87,7 @@ export { UIEmbed } from './embed/embed.js';
87
87
  export { UIBlock } from './block/block.js';
88
88
  export { UIText } from './text/text.js';
89
89
  export { UIToggleGroup, UIToggleOption } from './toggle-group/toggle-group.js';
90
+ export { UIToggleScheme } from './toggle-scheme/toggle-scheme.js';
90
91
  export { UIDemoToggle } from './demo-toggle/demo-toggle.js';
91
92
  export { UIRichText } from './richtext/richtext.js';
92
93
  export { UIStream } from './stream/stream.js';
@@ -0,0 +1,61 @@
1
+ /**
2
+ * `<input-ui>` — Text + email + password + number + tel + url + search input.
3
+ *
4
+ * @see https://ui-kit.exe.xyz/site/components/input
5
+ */
6
+
7
+ import { UIFormElement } from '../../core/form.js';
8
+
9
+ /** Native HTML input types this primitive supports. */
10
+ export type UIInputType =
11
+ | 'text'
12
+ | 'email'
13
+ | 'password'
14
+ | 'number'
15
+ | 'tel'
16
+ | 'url'
17
+ | 'search';
18
+
19
+ export interface InputChangeEventDetail {
20
+ value: string;
21
+ }
22
+ export type InputChangeEvent = CustomEvent<InputChangeEventDetail>;
23
+ export type InputInputEvent = CustomEvent<InputChangeEventDetail>;
24
+
25
+ export class UIInput extends UIFormElement {
26
+ placeholder: string;
27
+ type: UIInputType;
28
+ label: string;
29
+ prefix: string;
30
+ suffix: string;
31
+ /** Raw mode — drops chrome (border, padding) for inline composition. */
32
+ raw: boolean;
33
+
34
+ // ── Number-mode properties (active when type="number") ──
35
+ /** Minimum value. `null` to disable. */
36
+ min: number | null;
37
+ /** Maximum value. `null` to disable. */
38
+ max: number | null;
39
+ /** Step size for stepper buttons + arrow keys. */
40
+ step: number;
41
+ /** Decimal precision (places); `null` to derive from step. */
42
+ precision: number | null;
43
+ /**
44
+ * BCP-47 locale tag for number formatting — e.g. `"de-DE"` / `"fr-FR"` /
45
+ * `"en-IN"`. Default empty = en-US (`.` decimal separator, no thousands
46
+ * grouping). `.value` stays canonical (`Number(v)` round-trips).
47
+ */
48
+ locale: string;
49
+
50
+ // ── Number-mode accessor ──
51
+ /** Parsed numeric value. Returns `NaN` if value isn't a number. */
52
+ readonly valueAsNumber: number;
53
+
54
+ addEventListener<K extends keyof HTMLElementEventMap>(
55
+ type: K,
56
+ listener: (this: UIInput, ev: HTMLElementEventMap[K]) => unknown,
57
+ options?: boolean | AddEventListenerOptions,
58
+ ): void;
59
+ addEventListener(type: 'change', listener: (ev: InputChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
60
+ addEventListener(type: 'input', listener: (ev: InputInputEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
61
+ }
@@ -462,8 +462,8 @@ class UIInput extends UIFormElement {
462
462
  if (next === this.valueAsNumber) return;
463
463
  this.value = this.#format(next);
464
464
  this.syncValue(this.value);
465
- this.dispatchEvent(new Event('input', { bubbles: true }));
466
- this.dispatchEvent(new Event('change', { bubbles: true }));
465
+ this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: this.value } }));
466
+ this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: this.value } }));
467
467
  }
468
468
 
469
469
  // ── Event handlers ──
@@ -488,7 +488,7 @@ class UIInput extends UIFormElement {
488
488
  this.value = text;
489
489
  if (!this.#isNativePassword) this.#textEl.toggleAttribute('data-empty', !text);
490
490
  this.syncValue(text);
491
- this.dispatchEvent(new Event('input', { bubbles: true }));
491
+ this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: this.value } }));
492
492
  };
493
493
 
494
494
  #onBeforeInput = (e) => {
@@ -600,7 +600,7 @@ class UIInput extends UIFormElement {
600
600
  e.preventDefault();
601
601
  // Commit normalized value before firing form events.
602
602
  this.#commitOnBlur();
603
- this.dispatchEvent(new Event('change', { bubbles: true }));
603
+ this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: this.value } }));
604
604
  this.dispatchEvent(new Event('submit', { bubbles: true }));
605
605
  return;
606
606
  }
@@ -608,7 +608,7 @@ class UIInput extends UIFormElement {
608
608
  }
609
609
  if (e.key === 'Enter') {
610
610
  e.preventDefault();
611
- this.dispatchEvent(new Event('change', { bubbles: true }));
611
+ this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: this.value } }));
612
612
  this.dispatchEvent(new Event('submit', { bubbles: true }));
613
613
  }
614
614
  };
@@ -631,7 +631,7 @@ class UIInput extends UIFormElement {
631
631
 
632
632
  #onBlur = () => {
633
633
  if (this.#isNumberMode) this.#commitOnBlur();
634
- this.dispatchEvent(new Event('change', { bubbles: true }));
634
+ this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: this.value } }));
635
635
  };
636
636
 
637
637
  #commitOnBlur() {
@@ -650,7 +650,7 @@ class UIInput extends UIFormElement {
650
650
  if (this.value !== stored) {
651
651
  this.value = stored;
652
652
  this.syncValue(stored);
653
- this.dispatchEvent(new Event('input', { bubbles: true }));
653
+ this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: this.value } }));
654
654
  }
655
655
  if (this.#textEl.textContent !== displayed) {
656
656
  this.#textEl.textContent = displayed;
@@ -665,8 +665,8 @@ class UIInput extends UIFormElement {
665
665
  this.syncValue(this.value);
666
666
  this.#textEl.textContent = this.value;
667
667
  this.#textEl.toggleAttribute('data-empty', !this.value);
668
- this.dispatchEvent(new Event('input', { bubbles: true }));
669
- this.dispatchEvent(new Event('change', { bubbles: true }));
668
+ this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: this.value } }));
669
+ this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: this.value } }));
670
670
  }
671
671
 
672
672
  #onPaste = (e) => {
@@ -0,0 +1,30 @@
1
+ /**
2
+ * `<option-card-ui>` — Radio-style card with heading + description + icon.
3
+ *
4
+ * @see https://ui-kit.exe.xyz/site/components/option-card
5
+ */
6
+
7
+ import { UIFormElement } from '../../core/form.js';
8
+
9
+ export interface OptionCardChangeEventDetail {
10
+ value: string;
11
+ checked: boolean;
12
+ }
13
+ export type OptionCardChangeEvent = CustomEvent<OptionCardChangeEventDetail>;
14
+
15
+ export class UIOptionCard extends UIFormElement {
16
+ /** Selected state — reflected; setting `checked=true` deselects siblings in the group. */
17
+ checked: boolean;
18
+ heading: string;
19
+ description: string;
20
+ icon: string;
21
+ /** Layout — `default` (vertical) / `horizontal` / `compact`. */
22
+ layout: 'default' | 'horizontal' | 'compact';
23
+
24
+ addEventListener<K extends keyof HTMLElementEventMap>(
25
+ type: K,
26
+ listener: (this: UIOptionCard, ev: HTMLElementEventMap[K]) => unknown,
27
+ options?: boolean | AddEventListenerOptions,
28
+ ): void;
29
+ addEventListener(type: 'change', listener: (ev: OptionCardChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
30
+ }
@@ -116,7 +116,7 @@ class UIOptionCard extends UIFormElement {
116
116
  }
117
117
  }
118
118
  this.checked = true;
119
- this.dispatchEvent(new Event('change', { bubbles: true }));
119
+ this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: this.value, checked: this.checked } }));
120
120
  };
121
121
 
122
122
  #onKey = (e) => {
@@ -0,0 +1,25 @@
1
+ /**
2
+ * `<otp-input-ui>` — Auto-focus-advancing one-time-passcode input (6 digits by default).
3
+ *
4
+ * @see https://ui-kit.exe.xyz/site/components/otp-input
5
+ */
6
+
7
+ import { UIFormElement } from '../../core/form.js';
8
+
9
+ export interface OtpInputEventDetail {
10
+ /** Combined digits as a string (e.g. `"123456"`). */
11
+ value: string;
12
+ }
13
+ export type OtpInputEvent = CustomEvent<OtpInputEventDetail>;
14
+
15
+ export class UIOtpInput extends UIFormElement {
16
+ /** Number of digit slots. */
17
+ length: number;
18
+
19
+ addEventListener<K extends keyof HTMLElementEventMap>(
20
+ type: K,
21
+ listener: (this: UIOtpInput, ev: HTMLElementEventMap[K]) => unknown,
22
+ options?: boolean | AddEventListenerOptions,
23
+ ): void;
24
+ addEventListener(type: 'input', listener: (ev: OtpInputEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
25
+ }
@@ -93,7 +93,7 @@ class UIOtpInput extends UIFormElement {
93
93
  input.value = input.value.replace(/\D/g, '').slice(0, 1);
94
94
 
95
95
  this.#syncCombined();
96
- this.dispatchEvent(new Event('input', { bubbles: true }));
96
+ this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: this.value } }));
97
97
 
98
98
  if (input.value && index < this.#inputs.length - 1) {
99
99
  this.#inputs[index + 1].focus();
@@ -107,7 +107,7 @@ class UIOtpInput extends UIFormElement {
107
107
  this.#inputs[index - 1].focus();
108
108
  this.#inputs[index - 1].value = '';
109
109
  this.#syncCombined();
110
- this.dispatchEvent(new Event('input', { bubbles: true }));
110
+ this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: this.value } }));
111
111
  }
112
112
  }
113
113
 
@@ -118,7 +118,7 @@ class UIOtpInput extends UIFormElement {
118
118
  this.#inputs[i].value = text[i] || '';
119
119
  }
120
120
  this.#syncCombined();
121
- this.dispatchEvent(new Event('input', { bubbles: true }));
121
+ this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: this.value } }));
122
122
 
123
123
  // Focus last filled or first empty
124
124
  const firstEmpty = this.#inputs.findIndex(inp => !inp.value);
@@ -54,6 +54,15 @@
54
54
  overflow: hidden;
55
55
  }
56
56
 
57
+ /* During an active drag-resize: kill all transitions so pointer-driven
58
+ width updates track 1:1 instead of interpolating through a consumer-
59
+ declared width transition (e.g. editor-shell's collapse animation).
60
+ Mirrors the admin-sidebar pattern. */
61
+ :scope[data-resizing] {
62
+ user-select: none;
63
+ transition: none;
64
+ }
65
+
57
66
  /* Side-aware border treatment.
58
67
  `leading` — pane sits at the leading edge of a horizontal row, so
59
68
  the only visual separator it needs is on its trailing
@@ -187,6 +196,7 @@
187
196
  cursor: col-resize;
188
197
  background: transparent;
189
198
  transition: background var(--pane-duration) var(--pane-easing);
199
+ touch-action: none;
190
200
  z-index: 1;
191
201
  }
192
202
 
@@ -48,6 +48,8 @@ class UIPane extends UIElement {
48
48
  #dragging = false;
49
49
  #startX = 0;
50
50
  #startW = 0;
51
+ #pointerId = null;
52
+ #prevDocCursor = '';
51
53
  #bound = false;
52
54
 
53
55
  connected() {
@@ -110,15 +112,30 @@ class UIPane extends UIElement {
110
112
  };
111
113
 
112
114
  // ── Resize ──
115
+ //
116
+ // Pointer capture + handle-level listeners (not document-level): keeps
117
+ // pointer events flowing to the grabber even when the cursor races past
118
+ // it, and works under touch. Cursor is locked at the documentElement
119
+ // level so it stays `col-resize` across the whole viewport during drag,
120
+ // not just over the 4px handle. CSS bypasses any width transition while
121
+ // `[data-resizing]` is set, so resizes track the pointer 1:1.
113
122
 
114
123
  #onResizeDown = (e) => {
124
+ if (!e.isPrimary) return;
115
125
  e.preventDefault();
116
126
  this.#dragging = true;
117
127
  this.#startX = e.clientX;
118
128
  this.#startW = this.getBoundingClientRect().width;
129
+ this.#pointerId = e.pointerId;
119
130
  this.setAttribute('data-resizing', '');
120
- document.addEventListener('pointermove', this.#onResizeMove);
121
- document.addEventListener('pointerup', this.#onResizeUp, { once: true });
131
+
132
+ try { this.#resizeEl.setPointerCapture(e.pointerId); } catch {}
133
+ this.#resizeEl.addEventListener('pointermove', this.#onResizeMove);
134
+ this.#resizeEl.addEventListener('pointerup', this.#onResizeUp, { once: true });
135
+ this.#resizeEl.addEventListener('pointercancel', this.#onResizeUp, { once: true });
136
+
137
+ this.#prevDocCursor = document.documentElement.style.cursor;
138
+ document.documentElement.style.cursor = 'col-resize';
122
139
  };
123
140
 
124
141
  #onResizeMove = (e) => {
@@ -134,16 +151,23 @@ class UIPane extends UIElement {
134
151
  };
135
152
 
136
153
  #onResizeUp = () => {
154
+ if (!this.#dragging) return;
137
155
  this.#dragging = false;
138
156
  this.removeAttribute('data-resizing');
139
- document.removeEventListener('pointermove', this.#onResizeMove);
157
+ try { this.#resizeEl?.releasePointerCapture(this.#pointerId); } catch {}
158
+ this.#pointerId = null;
159
+ this.#resizeEl?.removeEventListener('pointermove', this.#onResizeMove);
160
+ document.documentElement.style.cursor = this.#prevDocCursor;
140
161
  };
141
162
 
142
163
  disconnected() {
143
164
  this.removeEventListener('click', this.#onClick);
144
165
  this.removeEventListener('keydown', this.#onKey);
145
166
  this.#resizeEl?.removeEventListener('pointerdown', this.#onResizeDown);
146
- document.removeEventListener('pointermove', this.#onResizeMove);
167
+ this.#resizeEl?.removeEventListener('pointermove', this.#onResizeMove);
168
+ if (this.#dragging) {
169
+ document.documentElement.style.cursor = this.#prevDocCursor;
170
+ }
147
171
  this.#bound = false;
148
172
  }
149
173
  }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * `<radio-ui>` — Radio button (boolean-semantic form control; group via shared name).
3
+ *
4
+ * @see https://ui-kit.exe.xyz/site/components/radio
5
+ */
6
+
7
+ import { UIFormElement } from '../../core/form.js';
8
+
9
+ export interface RadioChangeEventDetail {
10
+ /** Submitted value (the radio's `value` attribute). */
11
+ value: string;
12
+ /** Current checked state. */
13
+ checked: boolean;
14
+ }
15
+ export type RadioChangeEvent = CustomEvent<RadioChangeEventDetail>;
16
+
17
+ export class UIRadio extends UIFormElement {
18
+ /** Checked state — reflected; setting `checked=true` deselects siblings in the group. */
19
+ checked: boolean;
20
+ label: string;
21
+
22
+ addEventListener<K extends keyof HTMLElementEventMap>(
23
+ type: K,
24
+ listener: (this: UIRadio, ev: HTMLElementEventMap[K]) => unknown,
25
+ options?: boolean | AddEventListenerOptions,
26
+ ): void;
27
+ addEventListener(type: 'change', listener: (ev: RadioChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
28
+ }
@@ -38,7 +38,7 @@ class UIRadio extends UIFormElement {
38
38
  }
39
39
  }
40
40
  this.checked = true;
41
- this.dispatchEvent(new Event('change', { bubbles: true }));
41
+ this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: this.value, checked: this.checked } }));
42
42
  };
43
43
 
44
44
  #onKey = (e) => {
@@ -0,0 +1,31 @@
1
+ /**
2
+ * `<range-ui>` — Range track with click-to-jump + drag.
3
+ *
4
+ * @see https://ui-kit.exe.xyz/site/components/range
5
+ */
6
+
7
+ import { UIFormElement } from '../../core/form.js';
8
+
9
+ export interface RangeChangeEventDetail {
10
+ value: number;
11
+ }
12
+ export type RangeChangeEvent = CustomEvent<RangeChangeEventDetail>;
13
+ export type RangeInputEvent = CustomEvent<RangeChangeEventDetail>;
14
+
15
+ export class UIRange extends UIFormElement {
16
+ /** Numeric value — overrides UIFormElement.value (which is String). */
17
+ value: number;
18
+ min: number;
19
+ max: number;
20
+ step: number;
21
+ label: string;
22
+ suffix: string;
23
+
24
+ addEventListener<K extends keyof HTMLElementEventMap>(
25
+ type: K,
26
+ listener: (this: UIRange, ev: HTMLElementEventMap[K]) => unknown,
27
+ options?: boolean | AddEventListenerOptions,
28
+ ): void;
29
+ addEventListener(type: 'change', listener: (ev: RangeChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
30
+ addEventListener(type: 'input', listener: (ev: RangeInputEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
31
+ }
@@ -103,7 +103,7 @@ class UIRange extends UIFormElement {
103
103
  const snapped = this.#snap(v);
104
104
  if (snapped === this.value) return;
105
105
  this.value = snapped;
106
- this.dispatchEvent(new Event('input', { bubbles: true }));
106
+ this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: this.value } }));
107
107
  }
108
108
 
109
109
  // ── Pointer drag ──
@@ -147,7 +147,7 @@ class UIRange extends UIFormElement {
147
147
  this.#fieldEl.releasePointerCapture(e.pointerId);
148
148
  this.#fieldEl.removeEventListener('pointermove', this.#onPointerMove);
149
149
  this.#fieldEl.removeEventListener('pointerup', this.#onPointerUp);
150
- this.dispatchEvent(new Event('change', { bubbles: true }));
150
+ this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: this.value } }));
151
151
  };
152
152
 
153
153
  // ── Keyboard ──
@@ -166,7 +166,7 @@ class UIRange extends UIFormElement {
166
166
  }
167
167
  e.preventDefault();
168
168
  this.#setValue(v);
169
- this.dispatchEvent(new Event('change', { bubbles: true }));
169
+ this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: this.value } }));
170
170
  };
171
171
 
172
172
  disconnected() {
@@ -0,0 +1,33 @@
1
+ /**
2
+ * `<rating-ui>` — Star rating input (or other icon set).
3
+ *
4
+ * @see https://ui-kit.exe.xyz/site/components/rating
5
+ */
6
+
7
+ import { UIFormElement } from '../../core/form.js';
8
+
9
+ export interface RatingChangeEventDetail {
10
+ value: number;
11
+ }
12
+ export type RatingChangeEvent = CustomEvent<RatingChangeEventDetail>;
13
+
14
+ export class UIRating extends UIFormElement {
15
+ /** Numeric rating value — overrides UIFormElement.value (which is String). */
16
+ value: number;
17
+ /** Maximum rating — number of icons rendered. */
18
+ max: number;
19
+ /** Phosphor icon name (default: `star`). */
20
+ icon: string;
21
+ /** Read-only mode (display only; no interaction). */
22
+ readonly: boolean;
23
+ /** Allow half-step values (0.5, 1.5, …). */
24
+ allowHalf: boolean;
25
+ size: 'sm' | 'md' | 'lg';
26
+
27
+ addEventListener<K extends keyof HTMLElementEventMap>(
28
+ type: K,
29
+ listener: (this: UIRating, ev: HTMLElementEventMap[K]) => unknown,
30
+ options?: boolean | AddEventListenerOptions,
31
+ ): void;
32
+ addEventListener(type: 'change', listener: (ev: RatingChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
33
+ }
@@ -121,7 +121,7 @@ class UIRating extends UIFormElement {
121
121
  this.value = v;
122
122
  this.#hoverValue = null;
123
123
  this.syncValue(String(v));
124
- this.dispatchEvent(new Event('change', { bubbles: true }));
124
+ this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: this.value } }));
125
125
  this.render();
126
126
  }
127
127
 
@@ -0,0 +1,35 @@
1
+ /**
2
+ * `<search-ui>` — Search input with debounce + clear button + Enter-to-search.
3
+ *
4
+ * @see https://ui-kit.exe.xyz/site/components/search
5
+ */
6
+
7
+ import { UIFormElement } from '../../core/form.js';
8
+
9
+ export interface SearchInputEventDetail {
10
+ value: string;
11
+ }
12
+ export type SearchInputEvent = CustomEvent<SearchInputEventDetail>;
13
+
14
+ export interface SearchEventDetail {
15
+ value: string;
16
+ }
17
+ /** Fired after debounce OR on Enter — your "do the search" trigger. */
18
+ export type SearchEvent = CustomEvent<SearchEventDetail>;
19
+
20
+ export class UISearch extends UIFormElement {
21
+ placeholder: string;
22
+ /** Debounce delay in ms before `search` event fires. */
23
+ debounce: number;
24
+
25
+ /** Programmatically focus the inner input. */
26
+ focus(): void;
27
+
28
+ addEventListener<K extends keyof HTMLElementEventMap>(
29
+ type: K,
30
+ listener: (this: UISearch, ev: HTMLElementEventMap[K]) => unknown,
31
+ options?: boolean | AddEventListenerOptions,
32
+ ): void;
33
+ addEventListener(type: 'input', listener: (ev: SearchInputEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
34
+ addEventListener(type: 'search', listener: (ev: SearchEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
35
+ }
@@ -62,7 +62,7 @@ class UISearch extends UIFormElement {
62
62
  #onInput = () => {
63
63
  this.value = this.#inputEl.value;
64
64
  this.syncValue(this.value);
65
- this.dispatchEvent(new Event('input', { bubbles: true }));
65
+ this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: this.value } }));
66
66
 
67
67
  clearTimeout(this.#timer);
68
68
  this.#timer = setTimeout(() => {
@@ -92,7 +92,7 @@ class UISearch extends UIFormElement {
92
92
  if (this.#inputEl) this.#inputEl.value = '';
93
93
  this.syncValue('');
94
94
  this.setAttribute('value', '');
95
- this.dispatchEvent(new Event('input', { bubbles: true }));
95
+ this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: this.value } }));
96
96
  this.dispatchEvent(new CustomEvent('search', {
97
97
  bubbles: true,
98
98
  detail: { value: '' },
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `<segmented-ui>` — Segmented control for 2-5 mutually-exclusive options.
3
+ *
4
+ * @see https://ui-kit.exe.xyz/site/components/segmented
5
+ */
6
+
7
+ import { UIFormElement } from '../../core/form.js';
8
+
9
+ export interface SegmentedChangeEventDetail {
10
+ value: string;
11
+ }
12
+ export type SegmentedChangeEvent = CustomEvent<SegmentedChangeEventDetail>;
13
+
14
+ export class UISegmented extends UIFormElement {
15
+ /** Selected segment's value. */
16
+ value: string;
17
+
18
+ addEventListener<K extends keyof HTMLElementEventMap>(
19
+ type: K,
20
+ listener: (this: UISegmented, ev: HTMLElementEventMap[K]) => unknown,
21
+ options?: boolean | AddEventListenerOptions,
22
+ ): void;
23
+ addEventListener(type: 'change', listener: (ev: SegmentedChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
24
+ }