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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +10 -0
  10. package/data-table/fesm2022/data-table.mjs +48 -12
  11. package/data-table/types/data-table.d.ts +16 -4
  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
package/README.md CHANGED
@@ -167,9 +167,46 @@ export class DemoComponent {
167
167
  }
168
168
  ```
169
169
 
170
+ ## Searchable multiselect
171
+
172
+ `EgSearchableMultiselect` provides local, case-insensitive label search, named removable chips, and a `string[]` CVA. Import it directly from its public subpath:
173
+
174
+ ```ts
175
+ import { Component, signal } from '@angular/core';
176
+ import { EgSearchableMultiselect } from '@egose/shadcn-theme-ng/searchable-multiselect';
177
+
178
+ @Component({
179
+ selector: 'app-assignees',
180
+ imports: [EgSearchableMultiselect],
181
+ template: `
182
+ <eg-searchable-multiselect
183
+ [options]="people"
184
+ [(value)]="selected"
185
+ ariaLabel="Assignees"
186
+ searchLabel="Find people"
187
+ searchPlaceholder="Type a name…"
188
+ emptyMessage="No people match"
189
+ />
190
+ `,
191
+ })
192
+ export class AssigneesComponent {
193
+ readonly people = [
194
+ { value: 'ada', label: 'Ada Lovelace' },
195
+ { value: 'grace', label: 'Grace Hopper' },
196
+ ];
197
+ readonly selected = signal<string[]>(['ada']);
198
+ }
199
+ ```
200
+
201
+ Use `@egose/shadcn-theme-ng-tw/searchable-multiselect` for the prefixed variant. Pass the complete options list: the trimmed query filters labels in source order without changing selected IDs or chip labels. Clearing the query restores choices. Async option replacements update labels and results; unresolved selections retain their IDs. Search is local to supplied options, with no remote requests or virtualization. The query persists across closing/reopening and external value resets.
202
+
203
+ The native search is visibly labeled, empty results use a polite status region, and chip buttons are named `Remove <label>` (raw ID when unresolved). Customize names with `[removeLabel]="formatter"`, where `formatter: (option: SelectOption) => string` is pure and `SelectOption` comes from the same subpath. Search edits neither emit selection changes nor touch forms. Input/wrapper/form disabled flags block search and selection edits. With default popover focus settings, opening focuses search; Tab reaches checkbox buttons, Space/Enter toggles them, and Escape closes and restores trigger focus. Enter in search does not submit a form.
204
+
205
+ For reactive or template-driven forms, import `ReactiveFormsModule` or `FormsModule` and bind `formControl`/`formControlName` or `ngModel` instead of `value`. CVA writes own selection after the first write; null/empty arrays clear it without user emissions. Standalone new `[value]` arrays replace local edits; `[(value)]` synchronizes the parent. Supply unique IDs and immutable array/option updates. `EgFormSearchableMultiselect` from `@egose/shadcn-theme-ng/form-searchable-multiselect` adds the form label/hint/error layout and forwards `searchLabel`, `searchPlaceholder`, `emptyMessage`, and `removeLabel`.
206
+
170
207
  ## Date picker values
171
208
 
172
- `hlm-date-picker` emits a native JS `Date` (or `null` when cleared) via the `dateChange` output. Read it in the
209
+ By default, `hlm-date-picker` emits a native JS `Date` (or `null` when cleared) via the `dateChange` output. Read it in the
173
210
  controller with `(dateChange)`, a template ref (`picker.value()`), or a form binding (`ngModel`/`formControlName`,
174
211
  the picker is a `ControlValueAccessor`):
175
212
 
@@ -216,6 +253,87 @@ export class MyComponent {}
216
253
  then always sees local-midnight dates. Related: the picker stays open after selection by default; add the
217
254
  `autoCloseOnSelect` attribute (or set it in the same config) to close it on select.
218
255
 
256
+ ### Typed commits and constraints (single, range, multi)
257
+
258
+ Text commits on blur or Enter. Both the parsed value and the configured transform result must satisfy
259
+ `min`/`max`, inclusive of the adapter's entire boundary days. Comparisons and validity (`getTime` must be
260
+ finite) use Spartan's configured date adapter; custom date types also need matching parse/format/transform
261
+ config callbacks. A range's transformed endpoints must be ordered; a custom transform may sort them.
262
+ The default range parser accepts a single date as a same-day range, but rejects an explicitly unparseable
263
+ second endpoint.
264
+
265
+ Multi-date commits cannot exceed `maxSelection`. Consistent with calendar selection, `minSelection` is a
266
+ **deselection floor**: reducing the current count below it is rejected, but growing a selection from empty
267
+ is allowed. Counts apply to the supplied arrays before and after transformation. Supply unique dates.
268
+ At the calendar's maximum, its proposed reset to one new date is accepted only if the floor permits it.
269
+
270
+ Rejected text remains editable across blur, Enter and refocus, with native `aria-invalid="true"` and invalid
271
+ styling. It does **not** change the committed value or emit `dateChange`/CVA `onChange`; the interaction
272
+ marks the control touched. `inputInvalid()` on the text-input component exposes this local state. It does
273
+ not add Angular validation errors: validators still inspect the last committed form value. Use
274
+ `ariaDescribedby` for application-specific guidance, and include `inputInvalid()` in submission eligibility
275
+ if an unresolved draft should block submission. Existing `forceInvalid` is combined with this state.
276
+
277
+ A successful text/calendar commit clears the local invalid state. Empty text or the clear button explicitly
278
+ clears (`null` for single/range, `[]` for multi), bypassing transforms and the selection floor. Whitespace is
279
+ passed to the configured parser. Enter retains the edit format so later blur can parse it; blur uses the
280
+ display format. Rejected calendar selections restore the prior selection without emitting a replacement.
281
+
282
+ `updateDate(value)` is the user-commit boundary and returns `false` on rejection/disabled, `true` on success.
283
+ Programmatic CVA `writeValue` (including form `setValue`/reset) instead applies the transform **without**
284
+ user-constraint enforcement or user emissions, and replaces rejected text even for repeated values/null.
285
+ Standalone `[date]` updates retain their direct, untransformed input contract; they are not user commits.
286
+ `reset()` remains an explicit programmatic clear that emits. Parse/transform callbacks should be pure and
287
+ return values compatible with the configured adapter; parsing failure is represented by `null`.
288
+
289
+ ## Validation descriptions on composed fields
290
+
291
+ `EgFormAutocomplete`, `EgFormInputOtp`, `EgFormSlider`, and `EgFormCombobox` connect the
292
+ displayed hint or validation error to their actual native input, slider thumbs, or combobox
293
+ trigger/search input. Errors replace hints when invalid and touched, dirty, or submitted;
294
+ correction and form reset restore the appropriate hint. `HlmError` is styled text, not a live region.
295
+
296
+ Use `[aria-describedby]="'external-help-id'"` on these wrappers to add external descriptions.
297
+ The wrapper normalizes/deduplicates IDs and appends its current message ID. Keep consumer-owned
298
+ description elements mounted, and reserve `<effectiveId>-error` / `-hint` for wrapper messages.
299
+ `[attr.aria-describedby]` only targets the custom host and is not this forwarding contract.
300
+
301
+ For primitive compositions, `HlmAutocompleteInput`, `HlmComboboxInput`, `HlmComboboxTrigger`,
302
+ `HlmSlider`, and `HlmInputOtpControl` accept `[aria-describedby]` and merge it with descriptions
303
+ registered by the enclosing Spartan field. Import `HlmInputOtpControl` (selector `hlm-input-otp`)
304
+ or `HlmInputOtpImports` from `@egose/shadcn-theme-ng/input-otp`; it retains the brain OTP CVA and
305
+ editing behavior while adding native-input descriptions, required and invalid state. Existing
306
+ `brn-input-otp hlmInputOtp` compositions remain available.
307
+
308
+ Single combobox mode labels its trigger with `<effectiveId>` and uses `<effectiveId>-search`
309
+ for its separately labeled popup search. Required/invalid selection state stays on the trigger;
310
+ the popup search filters options. Slider thumbs retain their labels and invalid state; the slider
311
+ role has no `aria-required`. Configure Angular validators independently of wrapper `required`.
312
+ Verification is rendered browser DOM/focus coverage, not screen-reader testing.
313
+
314
+ ## Numbered pagination
315
+
316
+ Import `HlmNumberedPagination` or `HlmNumberedPaginationQueryParams` from
317
+ `@egose/shadcn-theme-ng/pagination` (or the `-tw` package), then bind
318
+ `[(currentPage)]`, `[(itemsPerPage)]` and `[totalItems]`.
319
+
320
+ Both pagers floor/clamp the current page when totals, size or page inputs change.
321
+ Empty/non-finite/non-positive totals or sizes produce one page with no previous/next;
322
+ invalid sizes are not rewritten. Non-finite pages become 1. A correction emits
323
+ `currentPageChange` once. Positive fractional totals/sizes use ceil division, with
324
+ page counts capped at `Number.MAX_SAFE_INTEGER`. `maxSize` budgets window entries,
325
+ including ellipses: finite positive values are floored/clamped to 1–100, otherwise 7.
326
+ Ranges below 5 show a contiguous active-page window. Helpers `createPageArray` and
327
+ `outOfBoundCorrection` use the same policy without side effects or allocation based
328
+ on an unbounded page count.
329
+
330
+ The query-params pager builds bounded `?page=` links using `queryParamsHandling="merge"`,
331
+ preserving unrelated query parameters. The parent owns reading route changes and
332
+ any URL synchronization: model corrections and size changes do not navigate. Handle
333
+ `currentPageChange` in the parent if corrected URLs should be replaced, merging other
334
+ parameters. Keep the previous total during loading if temporary zero results should
335
+ not reset the page.
336
+
219
337
  ## Working example
220
338
 
221
339
  A complete Angular consumer app lives at [`@examples/standard`](https://github.com/egose/shadcn-theme/tree/main/packages/angular/@examples/standard) — see its `src/app/...` for real usage of `@egose/shadcn-theme-ng/button`, `.../layout-simple`, `.../form-text-input`, `.../autocomplete`, `.../select`, `.../form-checkbox`, `.../sheet`, etc.
@@ -9,6 +9,7 @@ import { NgIcon, provideIcons } from '@ng-icons/core';
9
9
  import { lucideX, lucideSearch, lucideCheck } from '@ng-icons/lucide';
10
10
  import * as i2$1 from '@egose/shadcn-theme-ng-tw/input-group';
11
11
  import { HlmInputGroup, HlmInputGroupImports } from '@egose/shadcn-theme-ng-tw/input-group';
12
+ import { BrnFieldControlDescribedBy } from '@spartan-ng/brain/field';
12
13
 
13
14
  class HlmAutocomplete {
14
15
  constructor() {
@@ -103,6 +104,9 @@ class HlmAutocompleteInput {
103
104
  ...(ngDevMode ? [{ debugName: "inputId" }] : /* istanbul ignore next */ []));
104
105
  placeholder = input('', /* @ts-ignore */
105
106
  ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
107
+ /** Native input description IDs, merged with enclosing Spartan field descriptions. */
108
+ ariaDescribedBy = input(null, { ...(ngDevMode ? { debugName: "ariaDescribedBy" } : /* istanbul ignore next */ {}), alias: 'aria-describedby' });
109
+ required = input(false, { ...(ngDevMode ? { debugName: "required" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
106
110
  showSearch = input(true, { ...(ngDevMode ? { debugName: "showSearch" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
107
111
  showClear = input(false, { ...(ngDevMode ? { debugName: "showClear" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
108
112
  /** Forces the invalid state visually, regardless of form control state. */
@@ -114,13 +118,16 @@ class HlmAutocompleteInput {
114
118
  classes(() => 'tw:w-auto');
115
119
  }
116
120
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmAutocompleteInput, deps: [], target: i0.ɵɵFactoryTarget.Component });
117
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: HlmAutocompleteInput, isStandalone: true, selector: "hlm-autocomplete-input", inputs: { inputId: { classPropertyName: "inputId", publicName: "inputId", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, showSearch: { classPropertyName: "showSearch", publicName: "showSearch", isSignal: true, isRequired: false, transformFunction: null }, showClear: { classPropertyName: "showClear", publicName: "showClear", isSignal: true, isRequired: false, transformFunction: null }, forceInvalid: { classPropertyName: "forceInvalid", publicName: "forceInvalid", isSignal: true, isRequired: false, transformFunction: null }, ariaInvalidOverride: { classPropertyName: "ariaInvalidOverride", publicName: "aria-invalid", isSignal: true, isRequired: false, transformFunction: null } }, providers: [provideIcons({ lucideSearch, lucideX })], hostDirectives: [{ directive: i1.BrnAutocompleteAnchor }, { directive: i2$1.HlmInputGroup }], ngImport: i0, template: `
121
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: HlmAutocompleteInput, isStandalone: true, selector: "hlm-autocomplete-input", inputs: { inputId: { classPropertyName: "inputId", publicName: "inputId", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "aria-describedby", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, showSearch: { classPropertyName: "showSearch", publicName: "showSearch", isSignal: true, isRequired: false, transformFunction: null }, showClear: { classPropertyName: "showClear", publicName: "showClear", isSignal: true, isRequired: false, transformFunction: null }, forceInvalid: { classPropertyName: "forceInvalid", publicName: "forceInvalid", isSignal: true, isRequired: false, transformFunction: null }, ariaInvalidOverride: { classPropertyName: "ariaInvalidOverride", publicName: "aria-invalid", isSignal: true, isRequired: false, transformFunction: null } }, providers: [provideIcons({ lucideSearch, lucideX })], hostDirectives: [{ directive: i1.BrnAutocompleteAnchor }, { directive: i2$1.HlmInputGroup }], ngImport: i0, template: `
118
122
  <input
119
123
  brnAutocompleteInput
120
124
  #autocompleteInput="brnAutocompleteInput"
121
125
  hlmInputGroupInput
122
126
  [id]="inputId()"
123
127
  [placeholder]="placeholder()"
128
+ brnFieldControlDescribedBy
129
+ [aria-describedby]="ariaDescribedBy()"
130
+ [attr.aria-required]="required() || null"
124
131
  [aria-invalid]="ariaInvalidOverride()"
125
132
  [forceInvalid]="forceInvalid()"
126
133
  />
@@ -146,13 +153,13 @@ class HlmAutocompleteInput {
146
153
  </hlm-input-group-addon>
147
154
  }
148
155
  <ng-content />
149
- `, isInline: true, dependencies: [{ kind: "directive", type: i2$1.HlmInputGroupAddon, selector: "[hlmInputGroupAddon],hlm-input-group-addon", inputs: ["align"] }, { kind: "directive", type: i2$1.HlmInputGroupButton, selector: "button[hlmInputGroupButton]", inputs: ["size", "type", "class"] }, { kind: "directive", type: i2$1.HlmInputGroupInput, selector: "input[hlmInputGroupInput]" }, { kind: "component", type: NgIcon, selector: "ng-icon", inputs: ["name", "svg", "size", "strokeWidth", "color"] }, { kind: "directive", type: BrnAutocompleteClear, selector: "[brnAutocompleteClear]" }, { kind: "directive", type: BrnAutocompleteInput, selector: "input[brnAutocompleteInput]", inputs: ["id", "aria-invalid", "forceInvalid"], exportAs: ["brnAutocompleteInput"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
156
+ `, isInline: true, dependencies: [{ kind: "directive", type: i2$1.HlmInputGroupAddon, selector: "[hlmInputGroupAddon],hlm-input-group-addon", inputs: ["align"] }, { kind: "directive", type: i2$1.HlmInputGroupButton, selector: "button[hlmInputGroupButton]", inputs: ["size", "type", "class"] }, { kind: "directive", type: i2$1.HlmInputGroupInput, selector: "input[hlmInputGroupInput]" }, { kind: "component", type: NgIcon, selector: "ng-icon", inputs: ["name", "svg", "size", "strokeWidth", "color"] }, { kind: "directive", type: BrnAutocompleteClear, selector: "[brnAutocompleteClear]" }, { kind: "directive", type: BrnAutocompleteInput, selector: "input[brnAutocompleteInput]", inputs: ["id", "aria-invalid", "forceInvalid"], exportAs: ["brnAutocompleteInput"] }, { kind: "directive", type: BrnFieldControlDescribedBy, selector: "[brnFieldControlDescribedBy]", inputs: ["aria-describedby"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
150
157
  }
151
158
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmAutocompleteInput, decorators: [{
152
159
  type: Component,
153
160
  args: [{
154
161
  selector: 'hlm-autocomplete-input',
155
- imports: [HlmInputGroupImports, NgIcon, BrnAutocompleteClear, BrnAutocompleteInput],
162
+ imports: [HlmInputGroupImports, NgIcon, BrnAutocompleteClear, BrnAutocompleteInput, BrnFieldControlDescribedBy],
156
163
  providers: [provideIcons({ lucideSearch, lucideX })],
157
164
  changeDetection: ChangeDetectionStrategy.OnPush,
158
165
  hostDirectives: [BrnAutocompleteAnchor, HlmInputGroup],
@@ -163,6 +170,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
163
170
  hlmInputGroupInput
164
171
  [id]="inputId()"
165
172
  [placeholder]="placeholder()"
173
+ brnFieldControlDescribedBy
174
+ [aria-describedby]="ariaDescribedBy()"
175
+ [attr.aria-required]="required() || null"
166
176
  [aria-invalid]="ariaInvalidOverride()"
167
177
  [forceInvalid]="forceInvalid()"
168
178
  />
@@ -190,7 +200,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
190
200
  <ng-content />
191
201
  `,
192
202
  }]
193
- }], ctorParameters: () => [], propDecorators: { inputId: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputId", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], showSearch: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSearch", required: false }] }], showClear: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClear", required: false }] }], forceInvalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "forceInvalid", required: false }] }], ariaInvalidOverride: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-invalid", required: false }] }] } });
203
+ }], ctorParameters: () => [], propDecorators: { inputId: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputId", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-describedby", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], showSearch: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSearch", required: false }] }], showClear: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClear", required: false }] }], forceInvalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "forceInvalid", required: false }] }], ariaInvalidOverride: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-invalid", required: false }] }] } });
194
204
 
195
205
  class HlmAutocompleteItem {
196
206
  _brnAutocompleteItem = inject(BrnAutocompleteItem);
@@ -32,6 +32,9 @@ declare class HlmAutocompleteInput {
32
32
  private static _id;
33
33
  readonly inputId: i0.InputSignal<string>;
34
34
  readonly placeholder: i0.InputSignal<string>;
35
+ /** Native input description IDs, merged with enclosing Spartan field descriptions. */
36
+ readonly ariaDescribedBy: i0.InputSignal<string | null>;
37
+ readonly required: i0.InputSignalWithTransform<boolean, BooleanInput>;
35
38
  readonly showSearch: i0.InputSignalWithTransform<boolean, BooleanInput>;
36
39
  readonly showClear: i0.InputSignalWithTransform<boolean, BooleanInput>;
37
40
  /** Forces the invalid state visually, regardless of form control state. */
@@ -40,7 +43,7 @@ declare class HlmAutocompleteInput {
40
43
  readonly ariaInvalidOverride: i0.InputSignalWithTransform<boolean | undefined, BooleanInput>;
41
44
  constructor();
42
45
  static ɵfac: i0.ɵɵFactoryDeclaration<HlmAutocompleteInput, never>;
43
- static ɵcmp: i0.ɵɵComponentDeclaration<HlmAutocompleteInput, "hlm-autocomplete-input", never, { "inputId": { "alias": "inputId"; "required": false; "isSignal": true; }; "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "showSearch": { "alias": "showSearch"; "required": false; "isSignal": true; }; "showClear": { "alias": "showClear"; "required": false; "isSignal": true; }; "forceInvalid": { "alias": "forceInvalid"; "required": false; "isSignal": true; }; "ariaInvalidOverride": { "alias": "aria-invalid"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, [{ directive: typeof i1.BrnAutocompleteAnchor; inputs: {}; outputs: {}; }, { directive: typeof i2$1.HlmInputGroup; inputs: {}; outputs: {}; }]>;
46
+ static ɵcmp: i0.ɵɵComponentDeclaration<HlmAutocompleteInput, "hlm-autocomplete-input", never, { "inputId": { "alias": "inputId"; "required": false; "isSignal": true; }; "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "ariaDescribedBy": { "alias": "aria-describedby"; "required": false; "isSignal": true; }; "required": { "alias": "required"; "required": false; "isSignal": true; }; "showSearch": { "alias": "showSearch"; "required": false; "isSignal": true; }; "showClear": { "alias": "showClear"; "required": false; "isSignal": true; }; "forceInvalid": { "alias": "forceInvalid"; "required": false; "isSignal": true; }; "ariaInvalidOverride": { "alias": "aria-invalid"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, [{ directive: typeof i1.BrnAutocompleteAnchor; inputs: {}; outputs: {}; }, { directive: typeof i2$1.HlmInputGroup; inputs: {}; outputs: {}; }]>;
44
47
  }
45
48
 
46
49
  declare class HlmAutocompleteItem {
@@ -75,6 +75,8 @@ export class DemoModule {}
75
75
 
76
76
  Real selector: `hlm-checkbox` (element only). The host renders `class="contents peer"` so sibling label/description selectors (`peer-*`) keep working; `id` / `aria-*` attributes are nulled on the host and forwarded to the inner `brn-checkbox` instead. Set a visible label with a native `<label for>` — the component does not render label text itself.
77
77
 
78
+ Omitted/null IDs use `HlmFormIdGenerator` from `@egose/shadcn-theme-ng/form-field`: `hlm-checkbox-${APP_ID}-${sequence}`. The fallback is allocated once per instance from the application-scoped service, so separate applications restart their sequence and matching server/client `APP_ID` plus component creation order reproduce IDs. Explicit IDs are preserved on the focusable button; reverting to null restores that instance's fallback. Supply distinct `APP_ID` values for multiple applications on one page and unique explicit IDs. Checkboxes nested inside separate native labels each receive their own generated label association through Spartan; existing label IDs are retained.
79
+
78
80
  ## API reference
79
81
 
80
82
  ### `HlmCheckbox` (`hlm-checkbox`)
@@ -83,7 +85,7 @@ Real selector: `hlm-checkbox` (element only). The host renders `class="contents
83
85
  | -------------------------------------- | ----------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
84
86
  | `checked` | `model<boolean \| 'indeterminate'>` | `false` | Checked state. Two-way bindable (`[(checked)]`). `'indeterminate'` renders the checked icon; clicking it resolves to `true`. |
85
87
  | `class` (`userClass`) | `ClassValue` | `''` | Extra classes merged into the box. |
86
- | `id` | `string \| null` | `null` | Forwarded to inner `brn-checkbox`. Pair with `<label for>`. |
88
+ | `id` | `string \| null` | `null` | Explicit native button ID, or application-scoped fallback when null. Pair explicit IDs with `<label for>`. |
87
89
  | `aria-label` (`ariaLabel`) | `string \| null` | `null` | Forwarded aria-label. |
88
90
  | `aria-labelledby` (`ariaLabelledby`) | `string \| null` | `null` | Forwarded aria-labelledby. |
89
91
  | `aria-describedby` (`ariaDescribedby`) | `string \| null` | `null` | Forwarded aria-describedby. |
@@ -102,6 +104,8 @@ Real selector: `hlm-checkbox` (element only). The host renders `class="contents
102
104
 
103
105
  ControlValueAccessor: `writeValue(value)`, `registerOnChange`, `registerOnTouched`, `setDisabledState(isDisabled)` (writes `formDisabled`, hence `disabledState`). `HLM_CHECKBOX_VALUE_ACCESSOR` (`NG_VALUE_ACCESSOR`, `forwardRef(HlmCheckbox)`, `multi: true`) is provided on the component.
104
106
 
107
+ Both `[checked]` and CVA writes preserve `'indeterminate'`. The primitive forwards boolean checked and indeterminate separately to Spartan: the actual `button[role="checkbox"]` exposes `aria-checked="mixed"` and `data-state="indeterminate"`. An enabled click resolves mixed to `true`, then subsequent clicks toggle true/false; `changed` and CVA `onChange` emit the resulting boolean once. Thus clicking a partially selected table header selects its displayed page. Programmatic CVA writes do not call `changed`/`onChange` or touch the control; null resets to false. Any active input, wrapper, or form disabled lock blocks user toggles without discarding mixed state.
108
+
105
109
  ## Examples
106
110
 
107
111
  ### 1. Basic with label
@@ -307,7 +311,7 @@ export class ChangedCheckboxComponent {
307
311
  - The component renders a native-checkbox-equivalent `brn-checkbox` with `aria-label`/`labelledby`/`describedby` forwarding — always provide one: visible `<label for>` preferred, `aria-label` for icon-only rows.
308
312
  - `disabledState` adds `data-disabled`, `cursor-not-allowed`, and `opacity-50`; disabled boxes are skipped correctly because clicks early-return.
309
313
  - The check glyph is decorative (`lucideCheck` icon, no text) — state is exposed via `data-[state]` / native semantics, not the icon.
310
- - Tri-state: `'indeterminate'` is a visual/model state, not `aria-checked="mixed"` — if you need mixed semantics for a tree, add `aria-checked` handling at your own level and test with a screen reader.
314
+ - Tri-state: `'indeterminate'` exposes `aria-checked="mixed"` on the focusable checkbox button. State and nested-label associations are covered by rendered browser DOM tests; screen-reader announcement behavior is not asserted by those tests.
311
315
 
312
316
  ## Theming / CSS variables
313
317
 
@@ -1,9 +1,10 @@
1
1
  import * as i0 from '@angular/core';
2
- import { forwardRef, input, computed, model, booleanAttribute, signal, output, ChangeDetectionStrategy, Component, NgModule } from '@angular/core';
2
+ import { forwardRef, input, computed, inject, model, booleanAttribute, signal, output, ChangeDetectionStrategy, Component, NgModule } from '@angular/core';
3
3
  import { NG_VALUE_ACCESSOR } from '@angular/forms';
4
4
  import { provideIcons, NgIcon } from '@ng-icons/core';
5
5
  import { lucideCheck } from '@ng-icons/lucide';
6
6
  import { BrnCheckbox } from '@spartan-ng/brain/checkbox';
7
+ import { HlmFormIdGenerator } from '@egose/shadcn-theme-ng-tw/form-field';
7
8
  import { HlmIcon } from '@egose/shadcn-theme-ng-tw/icon';
8
9
  import { hlm } from '@egose/shadcn-theme-ng-tw/utils';
9
10
 
@@ -16,16 +17,19 @@ class HlmCheckbox {
16
17
  userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: 'class' });
17
18
  _computedClass = computed(() => hlm('tw:border-input tw:dark:bg-input/30 tw:data-[state=checked]:bg-primary tw:data-[state=checked]:text-primary-foreground tw:dark:data-[state=checked]:bg-primary tw:data-[state=checked]:border-primary tw:focus-visible:border-ring tw:focus-visible:ring-ring/50 tw:aria-invalid:ring-destructive/20 tw:dark:aria-invalid:ring-destructive/40 tw:aria-invalid:border-destructive tw:shadow-xs tw:peer tw:size-4 tw:shrink-0 tw:cursor-default tw:rounded-[4px] tw:border tw:outline-none tw:transition-shadow tw:focus-visible:ring-[3px] tw:disabled:cursor-not-allowed tw:disabled:opacity-50', this.userClass(), this.disabledState() ? 'tw:cursor-not-allowed tw:opacity-50' : ''), /* @ts-ignore */
18
19
  ...(ngDevMode ? [{ debugName: "_computedClass" }] : /* istanbul ignore next */ []));
19
- /** Used to set the id on the underlying brn element. */
20
+ /** Native checkbox ID. Null uses a stable application-scoped fallback; explicit IDs are preserved. */
20
21
  id = input(null, /* @ts-ignore */
21
22
  ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
23
+ _generatedId = inject(HlmFormIdGenerator).generate('hlm-checkbox');
24
+ _id = computed(() => this.id() ?? this._generatedId, /* @ts-ignore */
25
+ ...(ngDevMode ? [{ debugName: "_id" }] : /* istanbul ignore next */ []));
22
26
  /** Used to set the aria-label attribute on the underlying brn element. */
23
27
  ariaLabel = input(null, { ...(ngDevMode ? { debugName: "ariaLabel" } : /* istanbul ignore next */ {}), alias: 'aria-label' });
24
28
  /** Used to set the aria-labelledby attribute on the underlying brn element. */
25
29
  ariaLabelledby = input(null, { ...(ngDevMode ? { debugName: "ariaLabelledby" } : /* istanbul ignore next */ {}), alias: 'aria-labelledby' });
26
30
  /** Used to set the aria-describedby attribute on the underlying brn element. */
27
31
  ariaDescribedby = input(null, { ...(ngDevMode ? { debugName: "ariaDescribedby" } : /* istanbul ignore next */ {}), alias: 'aria-describedby' });
28
- /** The checked state of the checkbox. */
32
+ /** Tri-state model. Indeterminate exposes native aria-checked="mixed"; clicking it selects (true). */
29
33
  checked = model(false, /* @ts-ignore */
30
34
  ...(ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []));
31
35
  /** The name attribute of the checkbox. */
@@ -51,9 +55,9 @@ class HlmCheckbox {
51
55
  this._onChange?.(checked);
52
56
  this.changed.emit(checked);
53
57
  }
54
- /** CONTROL VALUE ACCESSOR */
58
+ /** Programmatic writes preserve indeterminate; null resets to false without changed/onChange notifications. */
55
59
  writeValue(value) {
56
- this.checked.set(!!value);
60
+ this.checked.set(value === 'indeterminate' ? value : !!value);
57
61
  }
58
62
  registerOnChange(fn) {
59
63
  this._onChange = fn;
@@ -67,10 +71,11 @@ class HlmCheckbox {
67
71
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmCheckbox, deps: [], target: i0.ɵɵFactoryTarget.Component });
68
72
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: HlmCheckbox, isStandalone: true, selector: "hlm-checkbox", inputs: { userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "aria-labelledby", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedby: { classPropertyName: "ariaDescribedby", publicName: "aria-describedby", isSignal: true, isRequired: false, transformFunction: null }, checked: { classPropertyName: "checked", publicName: "checked", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", 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 } }, outputs: { checked: "checkedChange", changed: "changed" }, host: { properties: { "attr.id": "null", "attr.aria-label": "null", "attr.aria-labelledby": "null", "attr.aria-describedby": "null", "attr.data-disabled": "disabledState() ? \"\" : null" }, classAttribute: "contents peer" }, providers: [HLM_CHECKBOX_VALUE_ACCESSOR], ngImport: i0, template: `
69
73
  <brn-checkbox
70
- [id]="id()"
74
+ [id]="_id()"
71
75
  [name]="name()"
72
76
  [class]="_computedClass()"
73
- [checked]="checked()"
77
+ [checked]="checked() === true"
78
+ [indeterminate]="checked() === 'indeterminate'"
74
79
  [disabled]="disabledState()"
75
80
  [required]="required()"
76
81
  [aria-label]="ariaLabel()"
@@ -94,10 +99,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
94
99
  imports: [BrnCheckbox, NgIcon, HlmIcon],
95
100
  template: `
96
101
  <brn-checkbox
97
- [id]="id()"
102
+ [id]="_id()"
98
103
  [name]="name()"
99
104
  [class]="_computedClass()"
100
- [checked]="checked()"
105
+ [checked]="checked() === true"
106
+ [indeterminate]="checked() === 'indeterminate'"
101
107
  [disabled]="disabledState()"
102
108
  [required]="required()"
103
109
  [aria-label]="ariaLabel()"
@@ -12,15 +12,17 @@ declare const HLM_CHECKBOX_VALUE_ACCESSOR: {
12
12
  declare class HlmCheckbox implements ControlValueAccessor {
13
13
  readonly userClass: _angular_core.InputSignal<ClassValue>;
14
14
  protected readonly _computedClass: _angular_core.Signal<string>;
15
- /** Used to set the id on the underlying brn element. */
15
+ /** Native checkbox ID. Null uses a stable application-scoped fallback; explicit IDs are preserved. */
16
16
  readonly id: _angular_core.InputSignal<string | null>;
17
+ private readonly _generatedId;
18
+ protected readonly _id: _angular_core.Signal<string>;
17
19
  /** Used to set the aria-label attribute on the underlying brn element. */
18
20
  readonly ariaLabel: _angular_core.InputSignal<string | null>;
19
21
  /** Used to set the aria-labelledby attribute on the underlying brn element. */
20
22
  readonly ariaLabelledby: _angular_core.InputSignal<string | null>;
21
23
  /** Used to set the aria-describedby attribute on the underlying brn element. */
22
24
  readonly ariaDescribedby: _angular_core.InputSignal<string | null>;
23
- /** The checked state of the checkbox. */
25
+ /** Tri-state model. Indeterminate exposes native aria-checked="mixed"; clicking it selects (true). */
24
26
  readonly checked: _angular_core.ModelSignal<CheckboxValue>;
25
27
  /** The name attribute of the checkbox. */
26
28
  readonly name: _angular_core.InputSignal<string | null>;
@@ -36,8 +38,8 @@ declare class HlmCheckbox implements ControlValueAccessor {
36
38
  protected _onChange?: ChangeFn<CheckboxValue>;
37
39
  protected _onTouched?: TouchFn;
38
40
  protected _handleCheckedChange(checked: boolean): void;
39
- /** CONTROL VALUE ACCESSOR */
40
- writeValue(value: CheckboxValue): void;
41
+ /** Programmatic writes preserve indeterminate; null resets to false without changed/onChange notifications. */
42
+ writeValue(value: CheckboxValue | null): void;
41
43
  registerOnChange(fn: ChangeFn<CheckboxValue>): void;
42
44
  registerOnTouched(fn: TouchFn): void;
43
45
  setDisabledState(isDisabled: boolean): void;
@@ -211,6 +211,9 @@ class HlmComboboxInput {
211
211
  ...(ngDevMode ? [{ debugName: "inputId" }] : /* istanbul ignore next */ []));
212
212
  placeholder = input('', /* @ts-ignore */
213
213
  ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
214
+ /** Native input description IDs, merged with enclosing Spartan field descriptions. */
215
+ ariaDescribedBy = input(null, { ...(ngDevMode ? { debugName: "ariaDescribedBy" } : /* istanbul ignore next */ {}), alias: 'aria-describedby' });
216
+ ariaLabelledBy = input(null, { ...(ngDevMode ? { debugName: "ariaLabelledBy" } : /* istanbul ignore next */ {}), alias: 'aria-labelledby' });
214
217
  showTrigger = input(true, { ...(ngDevMode ? { debugName: "showTrigger" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
215
218
  showClear = input(false, { ...(ngDevMode ? { debugName: "showClear" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
216
219
  forceInvalid = input(false, { ...(ngDevMode ? { debugName: "forceInvalid" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
@@ -221,7 +224,7 @@ class HlmComboboxInput {
221
224
  classes(() => 'tw:w-auto');
222
225
  }
223
226
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmComboboxInput, deps: [], target: i0.ɵɵFactoryTarget.Component });
224
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: HlmComboboxInput, isStandalone: true, selector: "hlm-combobox-input", inputs: { inputId: { classPropertyName: "inputId", publicName: "inputId", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, showTrigger: { classPropertyName: "showTrigger", publicName: "showTrigger", isSignal: true, isRequired: false, transformFunction: null }, showClear: { classPropertyName: "showClear", publicName: "showClear", isSignal: true, isRequired: false, transformFunction: null }, forceInvalid: { classPropertyName: "forceInvalid", publicName: "forceInvalid", isSignal: true, isRequired: false, transformFunction: null }, ariaInvalidOverride: { classPropertyName: "ariaInvalidOverride", publicName: "aria-invalid", isSignal: true, isRequired: false, transformFunction: null } }, providers: [provideIcons({ lucideChevronDown, lucideX })], hostDirectives: [{ directive: i1.BrnComboboxAnchor }, { directive: i2$1.HlmInputGroup }], ngImport: i0, template: `
227
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: HlmComboboxInput, isStandalone: true, selector: "hlm-combobox-input", inputs: { inputId: { classPropertyName: "inputId", publicName: "inputId", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "aria-describedby", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledBy: { classPropertyName: "ariaLabelledBy", publicName: "aria-labelledby", isSignal: true, isRequired: false, transformFunction: null }, showTrigger: { classPropertyName: "showTrigger", publicName: "showTrigger", isSignal: true, isRequired: false, transformFunction: null }, showClear: { classPropertyName: "showClear", publicName: "showClear", isSignal: true, isRequired: false, transformFunction: null }, forceInvalid: { classPropertyName: "forceInvalid", publicName: "forceInvalid", isSignal: true, isRequired: false, transformFunction: null }, ariaInvalidOverride: { classPropertyName: "ariaInvalidOverride", publicName: "aria-invalid", isSignal: true, isRequired: false, transformFunction: null } }, providers: [provideIcons({ lucideChevronDown, lucideX })], hostDirectives: [{ directive: i1.BrnComboboxAnchor }, { directive: i2$1.HlmInputGroup }], ngImport: i0, template: `
225
228
  <input
226
229
  brnComboboxInput
227
230
  #comboboxInput="brnComboboxInput"
@@ -230,6 +233,9 @@ class HlmComboboxInput {
230
233
  hlmInputGroupInput
231
234
  [id]="inputId()"
232
235
  [placeholder]="placeholder()"
236
+ brnFieldControlDescribedBy
237
+ [aria-describedby]="ariaDescribedBy()"
238
+ [attr.aria-labelledby]="ariaLabelledBy()"
233
239
  [forceInvalid]="forceInvalid()"
234
240
  [aria-invalid]="ariaInvalidOverride()"
235
241
  />
@@ -264,13 +270,13 @@ class HlmComboboxInput {
264
270
  </hlm-input-group-addon>
265
271
 
266
272
  <ng-content />
267
- `, isInline: true, dependencies: [{ kind: "directive", type: i2$1.HlmInputGroupAddon, selector: "[hlmInputGroupAddon],hlm-input-group-addon", inputs: ["align"] }, { kind: "directive", type: i2$1.HlmInputGroupButton, selector: "button[hlmInputGroupButton]", inputs: ["size", "type", "class"] }, { kind: "directive", type: i2$1.HlmInputGroupInput, selector: "input[hlmInputGroupInput]" }, { kind: "component", type: NgIcon, selector: "ng-icon", inputs: ["name", "svg", "size", "strokeWidth", "color"] }, { kind: "directive", type: i1.BrnComboboxClear, selector: "[brnComboboxClear]" }, { kind: "directive", type: i1.BrnComboboxInput, selector: "input[brnComboboxInput]", inputs: ["id", "aria-invalid", "forceInvalid"], exportAs: ["brnComboboxInput"] }, { kind: "directive", type: i1.BrnComboboxPopoverTrigger, selector: "[brnComboboxPopoverTrigger]", inputs: ["closeOnTriggerClick"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
273
+ `, isInline: true, dependencies: [{ kind: "directive", type: i2$1.HlmInputGroupAddon, selector: "[hlmInputGroupAddon],hlm-input-group-addon", inputs: ["align"] }, { kind: "directive", type: i2$1.HlmInputGroupButton, selector: "button[hlmInputGroupButton]", inputs: ["size", "type", "class"] }, { kind: "directive", type: i2$1.HlmInputGroupInput, selector: "input[hlmInputGroupInput]" }, { kind: "component", type: NgIcon, selector: "ng-icon", inputs: ["name", "svg", "size", "strokeWidth", "color"] }, { kind: "directive", type: i1.BrnComboboxClear, selector: "[brnComboboxClear]" }, { kind: "directive", type: i1.BrnComboboxInput, selector: "input[brnComboboxInput]", inputs: ["id", "aria-invalid", "forceInvalid"], exportAs: ["brnComboboxInput"] }, { kind: "directive", type: i1.BrnComboboxPopoverTrigger, selector: "[brnComboboxPopoverTrigger]", inputs: ["closeOnTriggerClick"] }, { kind: "directive", type: BrnFieldControlDescribedBy, selector: "[brnFieldControlDescribedBy]", inputs: ["aria-describedby"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
268
274
  }
269
275
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmComboboxInput, decorators: [{
270
276
  type: Component,
271
277
  args: [{
272
278
  selector: 'hlm-combobox-input',
273
- imports: [HlmInputGroupImports, NgIcon, BrnComboboxImports, BrnComboboxPopoverTrigger],
279
+ imports: [HlmInputGroupImports, NgIcon, BrnComboboxImports, BrnComboboxPopoverTrigger, BrnFieldControlDescribedBy],
274
280
  providers: [provideIcons({ lucideChevronDown, lucideX })],
275
281
  changeDetection: ChangeDetectionStrategy.OnPush,
276
282
  hostDirectives: [BrnComboboxAnchor, HlmInputGroup],
@@ -283,6 +289,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
283
289
  hlmInputGroupInput
284
290
  [id]="inputId()"
285
291
  [placeholder]="placeholder()"
292
+ brnFieldControlDescribedBy
293
+ [aria-describedby]="ariaDescribedBy()"
294
+ [attr.aria-labelledby]="ariaLabelledBy()"
286
295
  [forceInvalid]="forceInvalid()"
287
296
  [aria-invalid]="ariaInvalidOverride()"
288
297
  />
@@ -319,7 +328,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
319
328
  <ng-content />
320
329
  `,
321
330
  }]
322
- }], ctorParameters: () => [], propDecorators: { inputId: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputId", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], showTrigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTrigger", required: false }] }], showClear: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClear", required: false }] }], forceInvalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "forceInvalid", required: false }] }], ariaInvalidOverride: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-invalid", required: false }] }] } });
331
+ }], ctorParameters: () => [], propDecorators: { inputId: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputId", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-describedby", required: false }] }], ariaLabelledBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-labelledby", required: false }] }], showTrigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTrigger", required: false }] }], showClear: [{ type: i0.Input, args: [{ isSignal: true, alias: "showClear", required: false }] }], forceInvalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "forceInvalid", required: false }] }], ariaInvalidOverride: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-invalid", required: false }] }] } });
323
332
 
324
333
  class HlmComboboxItem {
325
334
  _brnComboboxItem = inject(BrnComboboxItem);
@@ -509,11 +518,14 @@ class HlmComboboxTrigger {
509
518
  ...(ngDevMode ? [{ debugName: "_computedClass" }] : /* istanbul ignore next */ []));
510
519
  buttonId = input(`hlm-combobox-trigger-${HlmComboboxTrigger._id++}`, /* @ts-ignore */
511
520
  ...(ngDevMode ? [{ debugName: "buttonId" }] : /* istanbul ignore next */ []));
521
+ /** Native button description IDs, merged with enclosing Spartan field descriptions. */
522
+ ariaDescribedBy = input(null, { ...(ngDevMode ? { debugName: "ariaDescribedBy" } : /* istanbul ignore next */ {}), alias: 'aria-describedby' });
523
+ required = input(false, { ...(ngDevMode ? { debugName: "required" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
512
524
  variant = input('outline', /* @ts-ignore */
513
525
  ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
514
526
  forceInvalid = input(false, { ...(ngDevMode ? { debugName: "forceInvalid" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
515
527
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmComboboxTrigger, deps: [], target: i0.ɵɵFactoryTarget.Component });
516
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.3", type: HlmComboboxTrigger, isStandalone: true, selector: "hlm-combobox-trigger", inputs: { userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, buttonId: { classPropertyName: "buttonId", publicName: "buttonId", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, forceInvalid: { classPropertyName: "forceInvalid", publicName: "forceInvalid", isSignal: true, isRequired: false, transformFunction: null } }, providers: [provideIcons({ lucideChevronDown })], ngImport: i0, template: `
528
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.1.3", type: HlmComboboxTrigger, isStandalone: true, selector: "hlm-combobox-trigger", inputs: { userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, buttonId: { classPropertyName: "buttonId", publicName: "buttonId", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "aria-describedby", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, forceInvalid: { classPropertyName: "forceInvalid", publicName: "forceInvalid", isSignal: true, isRequired: false, transformFunction: null } }, providers: [provideIcons({ lucideChevronDown })], ngImport: i0, template: `
517
529
  <button
518
530
  brnComboboxTrigger
519
531
  brnComboboxAnchor
@@ -522,6 +534,8 @@ class HlmComboboxTrigger {
522
534
  hlmBtn
523
535
  data-slot="combobox-trigger"
524
536
  [id]="buttonId()"
537
+ [aria-describedby]="ariaDescribedBy()"
538
+ [attr.aria-required]="required() || null"
525
539
  [class]="_computedClass()"
526
540
  [variant]="variant()"
527
541
  [forceInvalid]="forceInvalid()"
@@ -554,6 +568,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
554
568
  hlmBtn
555
569
  data-slot="combobox-trigger"
556
570
  [id]="buttonId()"
571
+ [aria-describedby]="ariaDescribedBy()"
572
+ [attr.aria-required]="required() || null"
557
573
  [class]="_computedClass()"
558
574
  [variant]="variant()"
559
575
  [forceInvalid]="forceInvalid()"
@@ -563,7 +579,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
563
579
  </button>
564
580
  `,
565
581
  }]
566
- }], propDecorators: { userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], buttonId: [{ type: i0.Input, args: [{ isSignal: true, alias: "buttonId", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], forceInvalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "forceInvalid", required: false }] }] } });
582
+ }], propDecorators: { userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], buttonId: [{ type: i0.Input, args: [{ isSignal: true, alias: "buttonId", required: false }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-describedby", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], forceInvalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "forceInvalid", required: false }] }] } });
567
583
 
568
584
  class HlmComboboxValue {
569
585
  constructor() {
@@ -55,6 +55,9 @@ declare class HlmComboboxInput {
55
55
  private static _id;
56
56
  readonly inputId: i0.InputSignal<string>;
57
57
  readonly placeholder: i0.InputSignal<string>;
58
+ /** Native input description IDs, merged with enclosing Spartan field descriptions. */
59
+ readonly ariaDescribedBy: i0.InputSignal<string | null>;
60
+ readonly ariaLabelledBy: i0.InputSignal<string | null>;
58
61
  readonly showTrigger: i0.InputSignalWithTransform<boolean, BooleanInput>;
59
62
  readonly showClear: i0.InputSignalWithTransform<boolean, BooleanInput>;
60
63
  readonly forceInvalid: i0.InputSignalWithTransform<boolean, BooleanInput>;
@@ -62,7 +65,7 @@ declare class HlmComboboxInput {
62
65
  readonly ariaInvalidOverride: i0.InputSignalWithTransform<boolean | undefined, BooleanInput>;
63
66
  constructor();
64
67
  static ɵfac: i0.ɵɵFactoryDeclaration<HlmComboboxInput, never>;
65
- static ɵcmp: i0.ɵɵComponentDeclaration<HlmComboboxInput, "hlm-combobox-input", never, { "inputId": { "alias": "inputId"; "required": false; "isSignal": true; }; "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "showTrigger": { "alias": "showTrigger"; "required": false; "isSignal": true; }; "showClear": { "alias": "showClear"; "required": false; "isSignal": true; }; "forceInvalid": { "alias": "forceInvalid"; "required": false; "isSignal": true; }; "ariaInvalidOverride": { "alias": "aria-invalid"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, [{ directive: typeof i1.BrnComboboxAnchor; inputs: {}; outputs: {}; }, { directive: typeof i2$1.HlmInputGroup; inputs: {}; outputs: {}; }]>;
68
+ static ɵcmp: i0.ɵɵComponentDeclaration<HlmComboboxInput, "hlm-combobox-input", never, { "inputId": { "alias": "inputId"; "required": false; "isSignal": true; }; "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "ariaDescribedBy": { "alias": "aria-describedby"; "required": false; "isSignal": true; }; "ariaLabelledBy": { "alias": "aria-labelledby"; "required": false; "isSignal": true; }; "showTrigger": { "alias": "showTrigger"; "required": false; "isSignal": true; }; "showClear": { "alias": "showClear"; "required": false; "isSignal": true; }; "forceInvalid": { "alias": "forceInvalid"; "required": false; "isSignal": true; }; "ariaInvalidOverride": { "alias": "aria-invalid"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, [{ directive: typeof i1.BrnComboboxAnchor; inputs: {}; outputs: {}; }, { directive: typeof i2$1.HlmInputGroup; inputs: {}; outputs: {}; }]>;
66
69
  }
67
70
 
68
71
  declare class HlmComboboxItem {
@@ -119,10 +122,13 @@ declare class HlmComboboxTrigger {
119
122
  readonly userClass: i0.InputSignal<ClassValue>;
120
123
  protected readonly _computedClass: i0.Signal<string>;
121
124
  readonly buttonId: i0.InputSignal<string>;
125
+ /** Native button description IDs, merged with enclosing Spartan field descriptions. */
126
+ readonly ariaDescribedBy: i0.InputSignal<string | null>;
127
+ readonly required: i0.InputSignalWithTransform<boolean, BooleanInput>;
122
128
  readonly variant: i0.InputSignal<"default" | "primary" | "secondary" | "success" | "warning" | "danger" | "info" | "light" | "dark" | "accent" | "destructive" | "muted" | "outline" | "link" | "ghost" | null | undefined>;
123
129
  readonly forceInvalid: i0.InputSignalWithTransform<boolean, BooleanInput>;
124
130
  static ɵfac: i0.ɵɵFactoryDeclaration<HlmComboboxTrigger, never>;
125
- static ɵcmp: i0.ɵɵComponentDeclaration<HlmComboboxTrigger, "hlm-combobox-trigger", never, { "userClass": { "alias": "class"; "required": false; "isSignal": true; }; "buttonId": { "alias": "buttonId"; "required": false; "isSignal": true; }; "variant": { "alias": "variant"; "required": false; "isSignal": true; }; "forceInvalid": { "alias": "forceInvalid"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
131
+ static ɵcmp: i0.ɵɵComponentDeclaration<HlmComboboxTrigger, "hlm-combobox-trigger", never, { "userClass": { "alias": "class"; "required": false; "isSignal": true; }; "buttonId": { "alias": "buttonId"; "required": false; "isSignal": true; }; "ariaDescribedBy": { "alias": "aria-describedby"; "required": false; "isSignal": true; }; "required": { "alias": "required"; "required": false; "isSignal": true; }; "variant": { "alias": "variant"; "required": false; "isSignal": true; }; "forceInvalid": { "alias": "forceInvalid"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
126
132
  }
127
133
 
128
134
  declare class HlmComboboxValue {
@@ -108,12 +108,14 @@ export class ServerPaymentsComponent {
108
108
  [gridColumns]="gridColumns"
109
109
  [data]="payments()"
110
110
  [enableSelection]="true"
111
+ [getRowId]="paymentId"
111
112
  [(layout)]="layout"
112
113
  (selectionChange)="onSelect($event)"
113
114
  />
114
115
  ```
115
116
 
116
117
  - `enableSelection` auto-prepends a checkbox column; `selectionChange` emits the selected rows.
118
+ - Define `readonly paymentId = (payment: Payment): string => payment.id` on the host. `getRowId` is a pure callback returning a unique, stable string per entity; keep the callback reference stable.
117
119
  - `[(layout)]` toggles `'table' | 'grid'` (grid requires `gridColumns`). Column `meta: { hideInTable: true }` hides a column in table layout; `meta: { thumbnail: true }` renders it as the card banner.
118
120
  - Sortable plain-string headers get an inline sort button; richer headers should use `EgDataTableColumnHeader` via `flexRenderComponent` (no type arguments needed — its `column` input accepts any TanStack column through the `EgSortableColumn` structural type):
119
121
 
@@ -126,6 +128,14 @@ helper.accessor('email', {
126
128
  });
127
129
  ```
128
130
 
131
+ ### Selection identity and scope
132
+
133
+ - **Stable IDs:** supply `getRowId` whenever rows can be refreshed, reordered, or fetched from a server. Selection follows IDs still present in the supplied rows, and refreshes emit the current objects, not stale snapshots. Removed IDs are discarded and are not selected if they later return. Changing the callback clears selection.
134
+ - **Server pages:** selection is scoped to the loaded slice, not an accumulated cross-page bulk-action set. Replacing a page keeps only selected IDs also in the new slice. Refreshing the same slice preserves selection when IDs match. Passing `null` or an empty slice clears it; keep the current slice and set `isLoading` if selection should survive loading a refresh.
135
+ - **Client filtering/paging:** `filterRows` defines the available selection scope; removing a row there discards its selection. Built-in column filters and client pagination only hide rows: hidden selections remain in `selectionChange`. Header selection toggles only the displayed page; table and grid share selection. The footer selection count describes the filtered subset.
136
+ - **Without `getRowId`:** existing positional `initialRowSelection` keys (`'0'`, `'1'`, etc.) remain supported for the initial rows. Replacing the source array (including a same-record refresh) or changing the `filterRows` result array clears selection rather than transferring an index to a different entity. Built-in sorting/filtering/paging with the same source array preserves it. This intentionally tightens the former unsafe positional behavior.
137
+ - **Updates and outputs:** update row arrays/objects immutably; in-place mutations are not tracked. `initialRowSelection` seeds only IDs present on initialization; later seed changes are ignored. `selectionChange` fires once on initialization, then when the selected objects or their source order change, including removal and refreshed object references. Unchanged selections do not re-emit for sorting, layout, or other unrelated state changes. IDs must be unique and must not be reused for different entities.
138
+
129
139
  ## API reference
130
140
 
131
141
  | Symbol | Kind |