@egose/shadcn-theme-ng-tw 0.6.0 → 0.6.2

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 (50) hide show
  1. package/README.md +119 -1
  2. package/autocomplete/fesm2022/autocomplete.mjs +14 -4
  3. package/autocomplete/types/autocomplete.d.ts +4 -1
  4. package/checkbox/README.md +6 -2
  5. package/checkbox/fesm2022/checkbox.mjs +15 -9
  6. package/checkbox/types/checkbox.d.ts +6 -4
  7. package/combobox/fesm2022/combobox.mjs +22 -6
  8. package/combobox/types/combobox.d.ts +8 -2
  9. package/data-table/README.md +11 -1
  10. package/data-table/fesm2022/data-table.mjs +76 -26
  11. package/data-table/types/data-table.d.ts +25 -6
  12. package/date-picker/README.md +41 -9
  13. package/date-picker/fesm2022/date-picker.mjs +265 -59
  14. package/date-picker/types/date-picker.d.ts +58 -27
  15. package/form-autocomplete/README.md +4 -2
  16. package/form-autocomplete/fesm2022/form-autocomplete.mjs +33 -8
  17. package/form-autocomplete/types/form-autocomplete.d.ts +9 -3
  18. package/form-combobox/README.md +4 -2
  19. package/form-combobox/fesm2022/form-combobox.mjs +65 -14
  20. package/form-combobox/types/form-combobox.d.ts +10 -3
  21. package/form-input-otp/README.md +7 -5
  22. package/form-input-otp/fesm2022/form-input-otp.mjs +28 -14
  23. package/form-input-otp/types/form-input-otp.d.ts +9 -3
  24. package/form-searchable-multiselect/README.md +15 -1
  25. package/form-searchable-multiselect/fesm2022/form-searchable-multiselect.mjs +23 -3
  26. package/form-searchable-multiselect/types/form-searchable-multiselect.d.ts +9 -1
  27. package/form-slider/README.md +3 -1
  28. package/form-slider/fesm2022/form-slider.mjs +21 -6
  29. package/form-slider/types/form-slider.d.ts +9 -3
  30. package/form-toggle/README.md +22 -18
  31. package/form-toggle/fesm2022/form-toggle.mjs +25 -8
  32. package/form-toggle/types/form-toggle.d.ts +6 -2
  33. package/input-otp/README.md +33 -9
  34. package/input-otp/fesm2022/input-otp.mjs +127 -11
  35. package/input-otp/types/input-otp.d.ts +24 -4
  36. package/package.json +1 -1
  37. package/pagination/README.md +21 -12
  38. package/pagination/fesm2022/pagination.mjs +83 -75
  39. package/pagination/types/pagination.d.ts +21 -8
  40. package/phone-input/README.md +16 -0
  41. package/phone-input/fesm2022/phone-input.mjs +41 -3
  42. package/phone-input/types/phone-input.d.ts +5 -2
  43. package/searchable-multiselect/README.md +62 -40
  44. package/searchable-multiselect/fesm2022/searchable-multiselect.mjs +97 -36
  45. package/searchable-multiselect/types/searchable-multiselect.d.ts +23 -6
  46. package/slider/fesm2022/slider.mjs +15 -5
  47. package/slider/types/slider.d.ts +6 -2
  48. package/stepper/README.md +39 -19
  49. package/stepper/fesm2022/stepper.mjs +71 -17
  50. package/stepper/types/stepper.d.ts +12 -1
@@ -1,6 +1,6 @@
1
1
  # Searchable Multiselect (`@egose/shadcn-theme-ng/searchable-multiselect`)
2
2
 
3
- A shadcn/ui-style **multi-select with chips + popover picker** — selected values render as removable chips, and a popover holds the checkbox option list. There is no exact single shadcn/ui counterpart; it composes the `Popover`, `Button`, and `Checkbox` patterns into one opinionated control.
3
+ A shadcn/ui-style **multi-select with chips + searchable popover picker** — selected values render as named removable chips, and a labeled local search filters the checkbox option list. There is no exact single shadcn/ui counterpart; it composes the `Popover`, `Button`, and `Checkbox` patterns into one opinionated control.
4
4
 
5
5
  It is a **standalone `ControlValueAccessor` component** (`EgSearchableMultiselect`), so it binds directly to Angular reactive forms (`formControlName`) and template-driven forms (`ngModel`) with a `string[]` value. Internally it reuses `HlmPopover`/`HlmCheckbox`/`HlmButton` — you do not import those yourself.
6
6
 
@@ -23,7 +23,7 @@ import { EgSearchableMultiselect } from '@egose/shadcn-theme-ng/searchable-multi
23
23
  // import { EgSearchableMultiselect } from '@egose/shadcn-theme-ng-tw/searchable-multiselect';
24
24
  ```
25
25
 
26
- Peer dependencies (see [package README](../../README.md) for versions): `@angular/core`, `@angular/common`. (No `@spartan-ng/brain` peer — popover/checkbox/button come along as regular library dependencies.)
26
+ Peer dependencies include Angular and `@spartan-ng/brain` (`>=1.3.2 <2.0.0`); see the [package README](../../README.md) for the complete peer list and supported versions. The composed popover/checkbox/button implementations are included in the theme package, but their Spartan behavior is supplied by that peer.
27
27
 
28
28
  ## Imports
29
29
 
@@ -64,11 +64,13 @@ Renders (internally — you do not write this yourself):
64
64
  <span><!-- {{ placeholder }} or chip {{ item.label }} + ✕ button --></span>
65
65
  </div>
66
66
 
67
- <!-- popover trigger + checkbox list -->
67
+ <!-- popover trigger + local search + checkbox list -->
68
68
  <hlm-popover>
69
69
  <button hlmPopoverTrigger hlmButton>N selected</button>
70
70
  <hlm-popover-content>
71
+ <label>Search options <input type="search" /></label>
71
72
  <label><!-- <hlm-checkbox> per option + label text --></label>
73
+ <div role="status" aria-live="polite"><!-- No matching options, when empty --></div>
72
74
  </hlm-popover-content>
73
75
  </hlm-popover>
74
76
  </eg-searchable-multiselect>
@@ -80,25 +82,47 @@ Real selector: `eg-searchable-multiselect` (element). The `✕` chip buttons and
80
82
 
81
83
  ### EgSearchableMultiselect (component, `ControlValueAccessor`)
82
84
 
83
- | Input | Type | Default | Description |
84
- | --------------------- | --------------------- | ------------------------ | -------------------------------------------------------------------------------- |
85
- | `options` | `SelectOption[]` | `[]` | Full option list (`{ label, value }`) |
86
- | `value` | `string[]` | `[]` | Selected values (one-way in; pairs with `valueChange` for two-way `[(value)]`) |
87
- | `placeholder` | `string` | `'Start typing to add…'` | Text of the pill shown when nothing is selected |
88
- | `id` | `string` | `''` | `id` placed on the trigger button |
89
- | `disabled` | `boolean` | `false` | Disables chips + trigger + checkboxes |
90
- | `wrapperDisabled` | `boolean` | `false` | Second disable flag (e.g. set by wrapper form components); OR-ed with `disabled` |
91
- | `ariaLabel` | `string \| undefined` | `undefined` | `aria-label` for the trigger button |
92
- | `ariaDescribedby` | `string \| null` | `null` | `aria-describedby` for the trigger button |
93
- | `class` (`userClass`) | `ClassValue` | `''` | Extra classes on the host |
94
-
95
- | Output | Type | Description |
96
- | ------------- | ---------- | ---------------------------------------------------- |
97
- | `valueChange` | `string[]` | Emitted with the new value array on every add/remove |
85
+ | Input | Type | Default | Description |
86
+ | --------------------- | --------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------- |
87
+ | `options` | `SelectOption[]` | `[]` | Full option list (`{ label, value }`) |
88
+ | `value` | `string[]` | `[]` | Standalone selected IDs; pairs with `valueChange` for `[(value)]`. Ignored after the first CVA write. |
89
+ | `placeholder` | `string` | `'Start typing to add…'` | Text of the pill shown when nothing is selected |
90
+ | `id` | `string` | `''` | `id` placed on the trigger button |
91
+ | `disabled` | `boolean` | `false` | Disables chips + trigger + search + checkboxes |
92
+ | `wrapperDisabled` | `boolean` | `false` | Second disable flag (e.g. set by wrapper form components); OR-ed with `disabled` |
93
+ | `ariaLabel` | `string \| undefined` | `undefined` | `aria-label` for the trigger button |
94
+ | `ariaDescribedby` | `string \| null` | `null` | `aria-describedby` for the trigger button |
95
+ | `class` (`userClass`) | `ClassValue` | `''` | Extra classes on the host |
96
+
97
+ | Output | Type | Description |
98
+ | ------------- | ---------- | ---------------------------------------------------------------- |
99
+ | `valueChange` | `string[]` | Emitted with a new value array on each effective user add/remove |
98
100
 
99
101
  `ControlValueAccessor` contract: `writeValue(values)`, `registerOnChange`, `registerOnTouched`, `setDisabledState` are implemented, so `formControl` / `formControlName` / `ngModel` all work. Effective disabled state = `disabled() \|\| wrapperDisabled() \|\| formDisabled()` (the last set by forms via `setDisabledState`).
100
102
 
101
- API surprise worth knowing: despite the "searchable" name, the current template ships **no filter text field** — the popover shows the full checkbox list and empty state reads "No options". Treat `options` as the complete visible list (filter it yourself before passing it in if you need search). Also `value` is a plain `input`, not a `model`: form writes flow through `writeValue`, and user edits flow out through `valueChange` + the CVA `onChange` callback.
103
+ `value` is a plain `input`, not a `model`: form writes flow through `writeValue`, and user edits flow out through `valueChange` + the CVA `onChange` callback.
104
+
105
+ ### Local search
106
+
107
+ Additional copy inputs:
108
+
109
+ - `searchLabel: string` (default `'Search options'`): visible, associated native search label; supply nonempty localized text.
110
+ - `searchPlaceholder: string` (default `'Type to filter…'`): search input hint, separate from the empty-selection pill.
111
+ - `emptyMessage: string` (default `'No matching options'`): polite live-region copy for no matches or no supplied options.
112
+ - `removeLabel: (option: SelectOption) => string` (default ``option => `Remove ${option.label}` ``): pure formatter for each chip button's accessible name; unresolved labels are raw IDs.
113
+
114
+ Pass the complete `options` list. The native search field trims the query and performs case-insensitive substring matching on **labels**, retaining source order. An empty/whitespace query restores all choices. Search is local to the supplied list: it does not search IDs, fetch remote options, or virtualize results. New option arrays and label replacements recompute the current filter immediately.
115
+
116
+ Filtering hides checkbox rows only. All selections, including unresolved IDs, remain selected; chip labels still resolve from the complete options list. Search edits never emit `valueChange`/CVA changes or mark touched. The query persists across close/reopen and external value writes/resets; clear its text to restore all choices. The empty message occupies a persistent polite status region while the popover is open.
117
+
118
+ ### Selection ownership and asynchronous options
119
+
120
+ - **Standalone:** `[value]` initializes selection, and each new input array replaces it (including `[]` to clear). User edits update the displayed selection immediately and emit `valueChange`; `[(value)]` keeps the parent synchronized. With one-way binding, local edits persist until a new input array arrives.
121
+ - **Angular forms:** the first `writeValue` takes ownership for the component's lifetime. Subsequent `[value]` changes are ignored; use `formControl`, `formControlName`, or `ngModel` as the source of truth. Every form write replaces selection; `null` and `[]` clear it, including resets.
122
+ - Selected IDs are stored independently of `options`, in value-array order. Unresolved IDs render as removable chips labeled with the raw ID and count toward the selected total. When options arrive or labels change, chips update automatically. When an option disappears, its chip falls back to the ID. Adding/removing another option never discards these unresolved IDs; explicitly remove their chips or write a replacement value to clear them.
123
+ - External input/form writes and option updates never emit `valueChange`, call CVA `onChange`, or mark touched. Effective user adds/removes emit once per channel and call `onTouched`. Opening/focusing the picker alone does not mark touched; duplicate adds and absent removals are no-ops.
124
+ - Disabled state combines the input, wrapper, and form flags. It blocks trigger/chip/search/checkbox edits, including when disabled with the panel open, while still accepting external value and option updates.
125
+ - Input arrays and option objects are never mutated. Output and CVA callbacks receive separate fresh arrays. Supply unique selected IDs and unique option values, and replace arrays/option objects rather than mutating them in place so signal updates are observed.
102
126
 
103
127
  ## Examples
104
128
 
@@ -224,47 +248,44 @@ export class DisabledMultiComponent {
224
248
 
225
249
  Disabling via `formControl.disable()` works too — it flows through `setDisabledState`.
226
250
 
227
- ### 5. Client-side search (filter `options` yourself)
251
+ ### 5. Local search with configurable copy
228
252
 
229
- Since the popover lists exactly what you pass in `options`, implement search by filtering upstream:
253
+ Supply all options so hidden selections retain their labels:
230
254
 
231
255
  ```ts
232
- import { Component, computed, signal } from '@angular/core';
233
- import { EgSearchableMultiselect } from '@egose/shadcn-theme-ng/searchable-multiselect';
256
+ import { Component, signal } from '@angular/core';
257
+ import { EgSearchableMultiselect, type SelectOption } from '@egose/shadcn-theme-ng/searchable-multiselect';
234
258
 
235
259
  @Component({
236
260
  selector: 'app-search-multi',
237
261
  standalone: true,
238
262
  imports: [EgSearchableMultiselect],
239
263
  template: `
240
- <input
241
- type="search"
242
- placeholder="Filter options…"
243
- [value]="query()"
244
- (input)="query.set($any($event.target).value)"
245
- aria-label="Filter options"
264
+ <eg-searchable-multiselect
265
+ [options]="frameworks"
266
+ [(value)]="selected"
267
+ ariaLabel="Framework assignments"
268
+ searchLabel="Find frameworks"
269
+ searchPlaceholder="Type a framework name…"
270
+ emptyMessage="No frameworks match your search"
271
+ [removeLabel]="removeFrameworkLabel"
246
272
  />
247
- <eg-searchable-multiselect [options]="filtered()" [(value)]="selected" />
248
273
  `,
249
274
  })
250
275
  export class SearchMultiComponent {
251
- readonly query = signal('');
252
276
  readonly selected = signal<string[]>([]);
253
- private readonly all = [
277
+ readonly removeFrameworkLabel = (option: SelectOption) => `Unassign ${option.label}`;
278
+ readonly frameworks: SelectOption[] = [
254
279
  { label: 'Angular', value: 'angular' },
255
280
  { label: 'React', value: 'react' },
256
281
  { label: 'Vue', value: 'vue' },
257
282
  { label: 'Svelte', value: 'svelte' },
258
283
  { label: 'Solid', value: 'solid' },
259
284
  ];
260
- readonly filtered = computed(() => {
261
- const q = this.query().trim().toLowerCase();
262
- return q ? this.all.filter((o) => o.label.toLowerCase().includes(q)) : this.all;
263
- });
264
285
  }
265
286
  ```
266
287
 
267
- Note: selections whose option is currently filtered out stay selected internally (chips still show) but have no checkbox row until the filter matches again. Unknown values passed via `value`/`writeValue` that match no option are dropped from the chip row.
288
+ The form wrapper `EgFormSearchableMultiselect` forwards the same `searchLabel`, `searchPlaceholder`, `emptyMessage`, and `removeLabel` inputs. For localization, the remove formatter receives the current label and value, so it can include an ID to distinguish duplicate display names.
268
289
 
269
290
  ### 6. Async options + reacting to changes
270
291
 
@@ -308,9 +329,10 @@ export class AsyncMultiComponent {
308
329
  ## Accessibility notes
309
330
 
310
331
  - The trigger is a real `<button>` — give it an accessible name via `ariaLabel` (or a visible `<label>` paired with `id`) and descriptions via `ariaDescribedby`.
311
- - Options render as native checkbox-backed `hlm-checkbox` rows inside `<label>` elements, so they are keyboard-operable and announced per option.
312
- - Chip `✕` buttons are real buttons and disabled along with the control; keep chip text concise so screen readers announce removals cleanly.
313
- - The empty state is a plain text pill (not focusable) — the popover trigger remains the single keyboard entry point, which keeps tab order simple.
332
+ - Open the trigger with Enter/Space. With the default popover focus configuration, focus moves to the labeled native search field. Type to filter, clear the text to restore choices, and Tab/Shift+Tab among controls. Enter in search does not submit a surrounding form. Escape closes the popover and restores trigger focus.
333
+ - Options use native buttons with checkbox roles inside `<label>` elements. Use Space/Enter to toggle a focused choice.
334
+ - Chip `✕` buttons are named `Remove <label>` by default (`Remove <ID>` when unresolved); customize with `removeLabel`. They are disabled along with the control.
335
+ - No matches (including an empty options list) displays `emptyMessage` in a polite, atomic status region. The search remains editable when there are no results. Browser DOM/focus tests verify these contracts; they do not constitute screen-reader testing.
314
336
 
315
337
  ## Theming / CSS variables
316
338
 
@@ -1,17 +1,34 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, output, signal, computed, forwardRef, ChangeDetectionStrategy, Component } from '@angular/core';
2
+ import { input, output, signal, linkedSignal, computed, forwardRef, ChangeDetectionStrategy, Component } from '@angular/core';
3
3
  import { NG_VALUE_ACCESSOR } from '@angular/forms';
4
4
  import { HlmPopover, HlmPopoverTrigger, HlmPopoverContent, HlmPopoverPortal } from '@egose/shadcn-theme-ng-tw/popover';
5
5
  import { HlmButton } from '@egose/shadcn-theme-ng-tw/button';
6
6
  import { HlmCheckbox } from '@egose/shadcn-theme-ng-tw/checkbox';
7
7
  import { hlm } from '@egose/shadcn-theme-ng-tw/utils';
8
8
 
9
+ /**
10
+ * Chip multiselect with local, case-insensitive label search over the complete options input.
11
+ * Filtering never changes selected IDs or chip labels. Search edits do not emit or touch the form.
12
+ * Import from `@egose/shadcn-theme-ng-tw/searchable-multiselect` (or the `-tw` variant).
13
+ */
9
14
  class EgSearchableMultiselect {
10
15
  /** Full option list with labels/values */
11
16
  options = input([], /* @ts-ignore */
12
17
  ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
13
18
  placeholder = input('Start typing to add…', /* @ts-ignore */
14
19
  ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
20
+ /** Visible, associated label for the native search field; provide nonempty localized text. */
21
+ searchLabel = input('Search options', /* @ts-ignore */
22
+ ...(ngDevMode ? [{ debugName: "searchLabel" }] : /* istanbul ignore next */ []));
23
+ /** Search field hint, independent of the empty-selection chip placeholder. */
24
+ searchPlaceholder = input('Type to filter…', /* @ts-ignore */
25
+ ...(ngDevMode ? [{ debugName: "searchPlaceholder" }] : /* istanbul ignore next */ []));
26
+ /** Polite live-region message when no supplied option labels match, including an empty options list. */
27
+ emptyMessage = input('No matching options', /* @ts-ignore */
28
+ ...(ngDevMode ? [{ debugName: "emptyMessage" }] : /* istanbul ignore next */ []));
29
+ /** Accessible remove name from the current chip label (raw ID when unresolved). Supply a pure formatter. */
30
+ removeLabel = input((option) => `Remove ${option.label}`, /* @ts-ignore */
31
+ ...(ngDevMode ? [{ debugName: "removeLabel" }] : /* istanbul ignore next */ []));
15
32
  id = input('', /* @ts-ignore */
16
33
  ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
17
34
  disabled = input(false, /* @ts-ignore */
@@ -23,21 +40,42 @@ class EgSearchableMultiselect {
23
40
  ariaDescribedby = input(null, /* @ts-ignore */
24
41
  ...(ngDevMode ? [{ debugName: "ariaDescribedby" }] : /* istanbul ignore next */ []));
25
42
  userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: 'class' });
26
- /** Value: array of selected values (string[]) */
43
+ /** Standalone selected IDs. New input arrays replace local edits; ignored after the first CVA writeValue. */
27
44
  value = input([], /* @ts-ignore */
28
45
  ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
46
+ /** User selection changes only; external input/CVA writes and option updates do not emit. */
29
47
  valueChange = output();
30
- selectedItems = signal([], /* @ts-ignore */
48
+ // Undefined means standalone ownership; even a null CVA write takes form ownership with an empty array.
49
+ formValue = signal(undefined, /* @ts-ignore */
50
+ ...(ngDevMode ? [{ debugName: "formValue" }] : /* istanbul ignore next */ []));
51
+ selectedValues = linkedSignal(() => [...(this.formValue() ?? this.value())], /* @ts-ignore */
52
+ ...(ngDevMode ? [{ debugName: "selectedValues" }] : /* istanbul ignore next */ []));
53
+ selectedItems = computed(() => {
54
+ const options = new Map(this.options().map((option) => [option.value, option]));
55
+ return this.selectedValues().map((value) => options.get(value) ?? { value, label: value });
56
+ }, /* @ts-ignore */
31
57
  ...(ngDevMode ? [{ debugName: "selectedItems" }] : /* istanbul ignore next */ []));
32
58
  formDisabled = signal(false, /* @ts-ignore */
33
59
  ...(ngDevMode ? [{ debugName: "formDisabled" }] : /* istanbul ignore next */ []));
34
60
  disabledState = computed(() => this.disabled() || this.wrapperDisabled() || this.formDisabled(), /* @ts-ignore */
35
61
  ...(ngDevMode ? [{ debugName: "disabledState" }] : /* istanbul ignore next */ []));
36
- isFormBound = false;
62
+ // Local UI state persists across popover closes and external writes; it never owns selection.
63
+ searchQuery = signal('', /* @ts-ignore */
64
+ ...(ngDevMode ? [{ debugName: "searchQuery" }] : /* istanbul ignore next */ []));
65
+ filteredOptions = computed(() => {
66
+ const query = this.searchQuery().trim().toLowerCase();
67
+ return this.options().filter((option) => option.label.toLowerCase().includes(query));
68
+ }, /* @ts-ignore */
69
+ ...(ngDevMode ? [{ debugName: "filteredOptions" }] : /* istanbul ignore next */ []));
37
70
  _hostClass = computed(() => hlm(this.userClass()), /* @ts-ignore */
38
71
  ...(ngDevMode ? [{ debugName: "_hostClass" }] : /* istanbul ignore next */ []));
72
+ updateSearch(query) {
73
+ if (this.disabledState())
74
+ return;
75
+ this.searchQuery.set(query);
76
+ }
39
77
  isSelected(value) {
40
- return this.selectedItems().some((s) => s.value === value);
78
+ return this.selectedValues().includes(value);
41
79
  }
42
80
  toggle(value, checked) {
43
81
  if (this.disabledState())
@@ -50,40 +88,29 @@ class EgSearchableMultiselect {
50
88
  }
51
89
  }
52
90
  addItem(value) {
53
- if (!this.selectedItems().some((s) => s.value === value)) {
54
- const opt = this.options()?.find((o) => o.value === value);
55
- if (!opt)
56
- return;
57
- const updated = [...this.selectedItems(), opt];
58
- this.updateValueFromSelected(updated);
59
- }
91
+ if (this.disabledState() || this.isSelected(value) || !this.options().some((option) => option.value === value))
92
+ return;
93
+ this.updateValueFromSelected([...this.selectedValues(), value]);
60
94
  }
61
95
  removeItem(value) {
62
- if (this.disabledState())
96
+ if (this.disabledState() || !this.isSelected(value))
63
97
  return;
64
- const updated = this.selectedItems().filter((i) => i.value !== value);
98
+ const updated = this.selectedValues().filter((selected) => selected !== value);
65
99
  this.updateValueFromSelected(updated);
66
100
  }
67
- updateSelectedFromValues(values) {
68
- const opts = this.options() || [];
69
- this.selectedItems.set(opts.filter((o) => values.includes(o.value)));
70
- }
71
- updateValueFromSelected(items) {
72
- this.selectedItems.set(items);
73
- const values = items.map((i) => i.value);
74
- this.valueChange.emit(values);
75
- this.onChange(values);
101
+ updateValueFromSelected(values) {
102
+ this.selectedValues.set(values);
103
+ // Neither consumer channel owns the internal array or the other channel's payload.
104
+ this.valueChange.emit([...values]);
105
+ this.onChange([...values]);
76
106
  this.onTouched();
77
107
  }
78
- constructor() {
79
- // No effect needed; we read `value` input directly via writeValue for form-bound usage.
80
- }
81
108
  // --- ControlValueAccessor ---
82
109
  onChange = () => { };
83
110
  onTouched = () => { };
111
+ /** Takes CVA ownership, retaining all IDs independently of options. Null clears; never emits or touches. */
84
112
  writeValue(values) {
85
- this.isFormBound = true;
86
- this.updateSelectedFromValues(values || []);
113
+ this.formValue.set([...(values ?? [])]);
87
114
  }
88
115
  registerOnChange(fn) {
89
116
  this.onChange = fn;
@@ -95,7 +122,7 @@ class EgSearchableMultiselect {
95
122
  this.formDisabled.set(isDisabled);
96
123
  }
97
124
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: EgSearchableMultiselect, deps: [], target: i0.ɵɵFactoryTarget.Component });
98
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: EgSearchableMultiselect, isStandalone: true, selector: "eg-searchable-multiselect", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, wrapperDisabled: { classPropertyName: "wrapperDisabled", publicName: "wrapperDisabled", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedby: { classPropertyName: "ariaDescribedby", publicName: "ariaDescribedby", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, host: { properties: { "class": "_hostClass()" } }, providers: [
125
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: EgSearchableMultiselect, isStandalone: true, selector: "eg-searchable-multiselect", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, searchLabel: { classPropertyName: "searchLabel", publicName: "searchLabel", isSignal: true, isRequired: false, transformFunction: null }, searchPlaceholder: { classPropertyName: "searchPlaceholder", publicName: "searchPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, removeLabel: { classPropertyName: "removeLabel", publicName: "removeLabel", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, wrapperDisabled: { classPropertyName: "wrapperDisabled", publicName: "wrapperDisabled", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedby: { classPropertyName: "ariaDescribedby", publicName: "ariaDescribedby", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, host: { properties: { "class": "_hostClass()" } }, providers: [
99
126
  {
100
127
  provide: NG_VALUE_ACCESSOR,
101
128
  useExisting: forwardRef(() => EgSearchableMultiselect),
@@ -122,6 +149,7 @@ class EgSearchableMultiselect {
122
149
  <button
123
150
  type="button"
124
151
  [disabled]="disabledState()"
152
+ [attr.aria-label]="removeLabel()(item)"
125
153
  (click)="removeItem(item.value)"
126
154
  class="tw:bg-transparent tw:border-0 tw:text-gray-600
127
155
  tw:hover:text-red-500 tw:cursor-pointer"
@@ -150,8 +178,21 @@ class EgSearchableMultiselect {
150
178
  </button>
151
179
 
152
180
  <hlm-popover-content class="tw:w-64 tw:p-2" *hlmPopoverPortal="let ctx">
181
+ <label class="tw:flex tw:flex-col tw:gap-1 tw:text-sm">
182
+ <span>{{ searchLabel() }}</span>
183
+ <input
184
+ #search
185
+ type="search"
186
+ [value]="searchQuery()"
187
+ [placeholder]="searchPlaceholder()"
188
+ [disabled]="disabledState()"
189
+ (input)="updateSearch(search.value)"
190
+ (keydown.enter)="$event.preventDefault()"
191
+ class="tw:w-full tw:rounded-md tw:border tw:border-input tw:bg-transparent tw:px-2 tw:py-1 tw:outline-none tw:focus-visible:ring-2 tw:focus-visible:ring-ring tw:disabled:opacity-50"
192
+ />
193
+ </label>
153
194
  <div class="tw:max-h-60 tw:overflow-auto tw:flex tw:flex-col tw:gap-1">
154
- @for (option of options(); track option.value) {
195
+ @for (option of filteredOptions(); track option.value) {
155
196
  <label
156
197
  class="tw:flex tw:items-center tw:gap-2 tw:cursor-pointer tw:px-2 tw:py-1 tw:rounded-sm tw:hover:bg-secondary"
157
198
  >
@@ -162,8 +203,11 @@ class EgSearchableMultiselect {
162
203
  />
163
204
  <span class="tw:text-sm">{{ option.label }}</span>
164
205
  </label>
165
- } @empty {
166
- <span class="tw:text-xs tw:text-muted-foreground tw:px-2 tw:py-1">No options</span>
206
+ }
207
+ </div>
208
+ <div role="status" aria-live="polite" aria-atomic="true" class="tw:text-xs tw:text-muted-foreground">
209
+ @if (filteredOptions().length === 0) {
210
+ {{ emptyMessage() }}
167
211
  }
168
212
  </div>
169
213
  </hlm-popover-content>
@@ -209,6 +253,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
209
253
  <button
210
254
  type="button"
211
255
  [disabled]="disabledState()"
256
+ [attr.aria-label]="removeLabel()(item)"
212
257
  (click)="removeItem(item.value)"
213
258
  class="tw:bg-transparent tw:border-0 tw:text-gray-600
214
259
  tw:hover:text-red-500 tw:cursor-pointer"
@@ -237,8 +282,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
237
282
  </button>
238
283
 
239
284
  <hlm-popover-content class="tw:w-64 tw:p-2" *hlmPopoverPortal="let ctx">
285
+ <label class="tw:flex tw:flex-col tw:gap-1 tw:text-sm">
286
+ <span>{{ searchLabel() }}</span>
287
+ <input
288
+ #search
289
+ type="search"
290
+ [value]="searchQuery()"
291
+ [placeholder]="searchPlaceholder()"
292
+ [disabled]="disabledState()"
293
+ (input)="updateSearch(search.value)"
294
+ (keydown.enter)="$event.preventDefault()"
295
+ class="tw:w-full tw:rounded-md tw:border tw:border-input tw:bg-transparent tw:px-2 tw:py-1 tw:outline-none tw:focus-visible:ring-2 tw:focus-visible:ring-ring tw:disabled:opacity-50"
296
+ />
297
+ </label>
240
298
  <div class="tw:max-h-60 tw:overflow-auto tw:flex tw:flex-col tw:gap-1">
241
- @for (option of options(); track option.value) {
299
+ @for (option of filteredOptions(); track option.value) {
242
300
  <label
243
301
  class="tw:flex tw:items-center tw:gap-2 tw:cursor-pointer tw:px-2 tw:py-1 tw:rounded-sm tw:hover:bg-secondary"
244
302
  >
@@ -249,8 +307,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
249
307
  />
250
308
  <span class="tw:text-sm">{{ option.label }}</span>
251
309
  </label>
252
- } @empty {
253
- <span class="tw:text-xs tw:text-muted-foreground tw:px-2 tw:py-1">No options</span>
310
+ }
311
+ </div>
312
+ <div role="status" aria-live="polite" aria-atomic="true" class="tw:text-xs tw:text-muted-foreground">
313
+ @if (filteredOptions().length === 0) {
314
+ {{ emptyMessage() }}
254
315
  }
255
316
  </div>
256
317
  </hlm-popover-content>
@@ -258,7 +319,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
258
319
  </div>
259
320
  `,
260
321
  }]
261
- }], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], wrapperDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "wrapperDisabled", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaDescribedby: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedby", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }] } });
322
+ }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], searchLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchLabel", required: false }] }], searchPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchPlaceholder", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], removeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "removeLabel", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], wrapperDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "wrapperDisabled", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaDescribedby: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedby", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }] } });
262
323
 
263
324
  /*
264
325
  * Public API Surface of searchable-multiselect
@@ -6,39 +6,56 @@ interface SelectOption {
6
6
  label: string;
7
7
  value: string;
8
8
  }
9
+ /**
10
+ * Chip multiselect with local, case-insensitive label search over the complete options input.
11
+ * Filtering never changes selected IDs or chip labels. Search edits do not emit or touch the form.
12
+ * Import from `@egose/shadcn-theme-ng-tw/searchable-multiselect` (or the `-tw` variant).
13
+ */
9
14
  declare class EgSearchableMultiselect implements ControlValueAccessor {
10
15
  /** Full option list with labels/values */
11
16
  options: _angular_core.InputSignal<SelectOption[]>;
12
17
  placeholder: _angular_core.InputSignal<string>;
18
+ /** Visible, associated label for the native search field; provide nonempty localized text. */
19
+ searchLabel: _angular_core.InputSignal<string>;
20
+ /** Search field hint, independent of the empty-selection chip placeholder. */
21
+ searchPlaceholder: _angular_core.InputSignal<string>;
22
+ /** Polite live-region message when no supplied option labels match, including an empty options list. */
23
+ emptyMessage: _angular_core.InputSignal<string>;
24
+ /** Accessible remove name from the current chip label (raw ID when unresolved). Supply a pure formatter. */
25
+ removeLabel: _angular_core.InputSignal<(option: SelectOption) => string>;
13
26
  id: _angular_core.InputSignal<string>;
14
27
  disabled: _angular_core.InputSignal<boolean>;
15
28
  wrapperDisabled: _angular_core.InputSignal<boolean>;
16
29
  ariaLabel: _angular_core.InputSignal<string | undefined>;
17
30
  ariaDescribedby: _angular_core.InputSignal<string | null>;
18
31
  userClass: _angular_core.InputSignal<ClassValue>;
19
- /** Value: array of selected values (string[]) */
32
+ /** Standalone selected IDs. New input arrays replace local edits; ignored after the first CVA writeValue. */
20
33
  value: _angular_core.InputSignal<string[]>;
34
+ /** User selection changes only; external input/CVA writes and option updates do not emit. */
21
35
  valueChange: _angular_core.OutputEmitterRef<string[]>;
22
- protected readonly selectedItems: _angular_core.WritableSignal<SelectOption[]>;
36
+ private readonly formValue;
37
+ private readonly selectedValues;
38
+ protected readonly selectedItems: _angular_core.Signal<SelectOption[]>;
23
39
  protected readonly formDisabled: _angular_core.WritableSignal<boolean>;
24
40
  protected readonly disabledState: _angular_core.Signal<boolean>;
25
- private isFormBound;
41
+ protected readonly searchQuery: _angular_core.WritableSignal<string>;
42
+ protected readonly filteredOptions: _angular_core.Signal<SelectOption[]>;
26
43
  protected readonly _hostClass: _angular_core.Signal<string>;
44
+ protected updateSearch(query: string): void;
27
45
  protected isSelected(value: string): boolean;
28
46
  protected toggle(value: string, checked: boolean): void;
29
47
  protected addItem(value: string): void;
30
48
  protected removeItem(value: string): void;
31
- private updateSelectedFromValues;
32
49
  private updateValueFromSelected;
33
- constructor();
34
50
  private onChange;
35
51
  private onTouched;
52
+ /** Takes CVA ownership, retaining all IDs independently of options. Null clears; never emits or touches. */
36
53
  writeValue(values: string[] | null): void;
37
54
  registerOnChange(fn: (value: string[]) => void): void;
38
55
  registerOnTouched(fn: () => void): void;
39
56
  setDisabledState(isDisabled: boolean): void;
40
57
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<EgSearchableMultiselect, never>;
41
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<EgSearchableMultiselect, "eg-searchable-multiselect", never, { "options": { "alias": "options"; "required": false; "isSignal": true; }; "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "id": { "alias": "id"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "wrapperDisabled": { "alias": "wrapperDisabled"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "ariaDescribedby": { "alias": "ariaDescribedby"; "required": false; "isSignal": true; }; "userClass": { "alias": "class"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; }, { "valueChange": "valueChange"; }, never, never, true, never>;
58
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<EgSearchableMultiselect, "eg-searchable-multiselect", never, { "options": { "alias": "options"; "required": false; "isSignal": true; }; "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "searchLabel": { "alias": "searchLabel"; "required": false; "isSignal": true; }; "searchPlaceholder": { "alias": "searchPlaceholder"; "required": false; "isSignal": true; }; "emptyMessage": { "alias": "emptyMessage"; "required": false; "isSignal": true; }; "removeLabel": { "alias": "removeLabel"; "required": false; "isSignal": true; }; "id": { "alias": "id"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "wrapperDisabled": { "alias": "wrapperDisabled"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "ariaDescribedby": { "alias": "ariaDescribedby"; "required": false; "isSignal": true; }; "userClass": { "alias": "class"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; }, { "valueChange": "valueChange"; }, never, never, true, never>;
42
59
  }
43
60
 
44
61
  export { EgSearchableMultiselect };
@@ -1,18 +1,22 @@
1
1
  import * as i0 from '@angular/core';
2
- import { ChangeDetectionStrategy, Component, NgModule } from '@angular/core';
2
+ import { inject, input, ChangeDetectionStrategy, Component, NgModule } from '@angular/core';
3
+ import { BrnFieldControl, BrnFieldControlDescribedBy } from '@spartan-ng/brain/field';
3
4
  import * as i1 from '@spartan-ng/brain/slider';
4
5
  import { injectBrnSlider, BrnSlider, BrnSliderImports } from '@spartan-ng/brain/slider';
5
6
  import { classes } from '@egose/shadcn-theme-ng-tw/utils';
6
7
 
7
8
  class HlmSlider {
8
9
  _slider = injectBrnSlider();
10
+ _fieldControl = inject(BrnFieldControl);
11
+ /** Description IDs applied to every thumb, merged with enclosing Spartan field descriptions. */
12
+ ariaDescribedBy = input(null, { ...(ngDevMode ? { debugName: "ariaDescribedBy" } : /* istanbul ignore next */ {}), alias: 'aria-describedby' });
9
13
  constructor() {
10
14
  classes(() => [
11
15
  'tw:group tw:flex tw:w-full tw:touch-none tw:flex-col tw:select-none tw:data-[orientation=vertical]:h-full tw:data-[orientation=vertical]:min-h-40 tw:data-[orientation=vertical]:w-auto tw:data-[orientation=vertical]:flex-row tw:data-[disabled]:pointer-events-none tw:data-[disabled]:opacity-50',
12
16
  ]);
13
17
  }
14
18
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmSlider, deps: [], target: i0.ɵɵFactoryTarget.Component });
15
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: HlmSlider, isStandalone: true, selector: "hlm-slider, brn-slider [hlm]", hostDirectives: [{ directive: i1.BrnSlider, inputs: ["id", "id", "value", "value", "disabled", "disabled", "min", "min", "max", "max", "step", "step", "minStepsBetweenThumbs", "minStepsBetweenThumbs", "inverted", "inverted", "orientation", "orientation", "showTicks", "showTicks", "maxTicks", "maxTicks", "tickLabelInterval", "tickLabelInterval", "formatTick", "formatTick", "draggableRange", "draggableRange", "draggableRangeOnly", "draggableRangeOnly", "aria-label", "aria-label", "aria-labelledby", "aria-labelledby"], outputs: ["valueChange", "valueChange"] }], ngImport: i0, template: `
19
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: HlmSlider, isStandalone: true, selector: "hlm-slider, brn-slider [hlm]", inputs: { ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "aria-describedby", isSignal: true, isRequired: false, transformFunction: null } }, hostDirectives: [{ directive: i1.BrnSlider, inputs: ["id", "id", "value", "value", "disabled", "disabled", "min", "min", "max", "max", "step", "step", "minStepsBetweenThumbs", "minStepsBetweenThumbs", "inverted", "inverted", "orientation", "orientation", "showTicks", "showTicks", "maxTicks", "maxTicks", "tickLabelInterval", "tickLabelInterval", "formatTick", "formatTick", "draggableRange", "draggableRange", "draggableRangeOnly", "draggableRangeOnly", "aria-label", "aria-label", "aria-labelledby", "aria-labelledby"], outputs: ["valueChange", "valueChange"] }], ngImport: i0, template: `
16
20
  <div
17
21
  class="tw:relative tw:flex tw:w-full tw:items-center tw:group-data-[orientation=vertical]:w-auto tw:group-data-[orientation=vertical]:flex-col"
18
22
  >
@@ -30,6 +34,9 @@ class HlmSlider {
30
34
  <span
31
35
  class="tw:border-primary tw:ring-ring/50 tw:size-4 tw:rounded-full tw:border tw:bg-white tw:shadow-sm tw:transition-[color,box-shadow] tw:hover:ring-4 tw:focus-visible:ring-4 tw:focus-visible:outline-hidden tw:absolute tw:block tw:shrink-0 tw:select-none tw:after:absolute tw:after:-inset-2"
32
36
  brnSliderThumb
37
+ brnFieldControlDescribedBy
38
+ [aria-describedby]="ariaDescribedBy()"
39
+ [attr.aria-invalid]="_fieldControl.invalid() ? 'true' : null"
33
40
  ></span>
34
41
  }
35
42
  </div>
@@ -49,13 +56,13 @@ class HlmSlider {
49
56
  </div>
50
57
  </div>
51
58
  }
52
- `, isInline: true, dependencies: [{ kind: "directive", type: i1.BrnSliderTrack, selector: "[brnSliderTrack]" }, { kind: "directive", type: i1.BrnSliderThumb, selector: "[brnSliderThumb]" }, { kind: "directive", type: i1.BrnSliderRange, selector: "[brnSliderRange]" }, { kind: "directive", type: i1.BrnSliderTick, selector: "[brnSliderTick]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
59
+ `, isInline: true, dependencies: [{ kind: "directive", type: i1.BrnSliderTrack, selector: "[brnSliderTrack]" }, { kind: "directive", type: i1.BrnSliderThumb, selector: "[brnSliderThumb]" }, { kind: "directive", type: i1.BrnSliderRange, selector: "[brnSliderRange]" }, { kind: "directive", type: i1.BrnSliderTick, selector: "[brnSliderTick]" }, { kind: "directive", type: BrnFieldControlDescribedBy, selector: "[brnFieldControlDescribedBy]", inputs: ["aria-describedby"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
53
60
  }
54
61
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmSlider, decorators: [{
55
62
  type: Component,
56
63
  args: [{
57
64
  selector: 'hlm-slider, brn-slider [hlm]',
58
- imports: [BrnSliderImports],
65
+ imports: [BrnSliderImports, BrnFieldControlDescribedBy],
59
66
  changeDetection: ChangeDetectionStrategy.OnPush,
60
67
  hostDirectives: [
61
68
  {
@@ -100,6 +107,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
100
107
  <span
101
108
  class="tw:border-primary tw:ring-ring/50 tw:size-4 tw:rounded-full tw:border tw:bg-white tw:shadow-sm tw:transition-[color,box-shadow] tw:hover:ring-4 tw:focus-visible:ring-4 tw:focus-visible:outline-hidden tw:absolute tw:block tw:shrink-0 tw:select-none tw:after:absolute tw:after:-inset-2"
102
109
  brnSliderThumb
110
+ brnFieldControlDescribedBy
111
+ [aria-describedby]="ariaDescribedBy()"
112
+ [attr.aria-invalid]="_fieldControl.invalid() ? 'true' : null"
103
113
  ></span>
104
114
  }
105
115
  </div>
@@ -121,7 +131,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
121
131
  }
122
132
  `,
123
133
  }]
124
- }], ctorParameters: () => [] });
134
+ }], ctorParameters: () => [], propDecorators: { ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-describedby", required: false }] }] } });
125
135
 
126
136
  /*
127
137
  * Public API Surface of slider
@@ -1,12 +1,16 @@
1
+ import * as i0 from '@angular/core';
2
+ import { BrnFieldControl } from '@spartan-ng/brain/field';
1
3
  import * as i1 from '@spartan-ng/brain/slider';
2
4
  import { BrnSlider } from '@spartan-ng/brain/slider';
3
- import * as i0 from '@angular/core';
4
5
 
5
6
  declare class HlmSlider {
6
7
  protected readonly _slider: BrnSlider;
8
+ protected readonly _fieldControl: BrnFieldControl;
9
+ /** Description IDs applied to every thumb, merged with enclosing Spartan field descriptions. */
10
+ readonly ariaDescribedBy: i0.InputSignal<string | null>;
7
11
  constructor();
8
12
  static ɵfac: i0.ɵɵFactoryDeclaration<HlmSlider, never>;
9
- static ɵcmp: i0.ɵɵComponentDeclaration<HlmSlider, "hlm-slider, brn-slider [hlm]", never, {}, {}, never, never, true, [{ directive: typeof i1.BrnSlider; inputs: { "id": "id"; "value": "value"; "disabled": "disabled"; "min": "min"; "max": "max"; "step": "step"; "minStepsBetweenThumbs": "minStepsBetweenThumbs"; "inverted": "inverted"; "orientation": "orientation"; "showTicks": "showTicks"; "maxTicks": "maxTicks"; "tickLabelInterval": "tickLabelInterval"; "formatTick": "formatTick"; "draggableRange": "draggableRange"; "draggableRangeOnly": "draggableRangeOnly"; "aria-label": "aria-label"; "aria-labelledby": "aria-labelledby"; }; outputs: { "valueChange": "valueChange"; }; }]>;
13
+ static ɵcmp: i0.ɵɵComponentDeclaration<HlmSlider, "hlm-slider, brn-slider [hlm]", never, { "ariaDescribedBy": { "alias": "aria-describedby"; "required": false; "isSignal": true; }; }, {}, never, never, true, [{ directive: typeof i1.BrnSlider; inputs: { "id": "id"; "value": "value"; "disabled": "disabled"; "min": "min"; "max": "max"; "step": "step"; "minStepsBetweenThumbs": "minStepsBetweenThumbs"; "inverted": "inverted"; "orientation": "orientation"; "showTicks": "showTicks"; "maxTicks": "maxTicks"; "tickLabelInterval": "tickLabelInterval"; "formatTick": "formatTick"; "draggableRange": "draggableRange"; "draggableRangeOnly": "draggableRangeOnly"; "aria-label": "aria-label"; "aria-labelledby": "aria-labelledby"; }; outputs: { "valueChange": "valueChange"; }; }]>;
10
14
  }
11
15
 
12
16
  declare const HlmSliderImports: readonly [typeof HlmSlider];