@egose/shadcn-theme-ng 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +1 -1
  2. package/accordion/README.md +405 -2
  3. package/alert/README.md +372 -2
  4. package/alert-dialog/README.md +471 -5
  5. package/aspect-ratio/README.md +272 -5
  6. package/autocomplete/README.md +502 -2
  7. package/avatar/README.md +357 -5
  8. package/badge/README.md +318 -2
  9. package/basic-alert/README.md +353 -2
  10. package/breadcrumb/README.md +406 -5
  11. package/button/README.md +482 -2
  12. package/button/fesm2022/button.mjs +85 -107
  13. package/button/types/button.d.ts +5 -8
  14. package/button-group/README.md +318 -5
  15. package/calendar/README.md +357 -2
  16. package/card/README.md +331 -5
  17. package/carousel/README.md +333 -5
  18. package/carousel/fesm2022/carousel.mjs +4 -1
  19. package/checkbox/README.md +320 -2
  20. package/collapsible/README.md +332 -5
  21. package/combobox/README.md +507 -5
  22. package/combobox/fesm2022/combobox.mjs +4 -1
  23. package/command/README.md +435 -5
  24. package/confirmation-dialog/README.md +301 -2
  25. package/context-menu/README.md +366 -5
  26. package/date-picker/README.md +465 -2
  27. package/date-picker/fesm2022/date-picker.mjs +2 -2
  28. package/dialog/README.md +448 -2
  29. package/drawer/README.md +395 -5
  30. package/dropdown-menu/README.md +417 -5
  31. package/empty/README.md +329 -5
  32. package/field/README.md +385 -5
  33. package/form-checkbox/README.md +312 -2
  34. package/form-date-picker/README.md +322 -2
  35. package/form-field/README.md +356 -2
  36. package/form-field-simple/README.md +340 -2
  37. package/form-searchable-multiselect/README.md +361 -2
  38. package/form-select/README.md +350 -2
  39. package/form-text-input/README.md +371 -2
  40. package/form-textarea/README.md +347 -2
  41. package/hover-card/README.md +256 -5
  42. package/icon/README.md +239 -2
  43. package/input/README.md +269 -2
  44. package/input-group/README.md +335 -5
  45. package/input-group/fesm2022/input-group.mjs +3 -3
  46. package/input-otp/README.md +375 -5
  47. package/item/README.md +385 -5
  48. package/item/fesm2022/item.mjs +3 -3
  49. package/kbd/README.md +291 -5
  50. package/label/README.md +272 -2
  51. package/layout-simple/README.md +193 -2
  52. package/layout-simple/fesm2022/layout-simple.mjs +472 -236
  53. package/layout-simple/types/layout-simple.d.ts +174 -137
  54. package/menu/README.md +417 -2
  55. package/menubar/README.md +343 -5
  56. package/native-select/README.md +323 -5
  57. package/navigation-menu/README.md +369 -5
  58. package/package.json +1 -1
  59. package/pagination/README.md +388 -5
  60. package/popover/README.md +331 -2
  61. package/progress/README.md +311 -5
  62. package/radio-group/README.md +364 -2
  63. package/radio-group/fesm2022/radio-group.mjs +5 -1
  64. package/radio-group/types/radio-group.d.ts +1 -1
  65. package/resizable/README.md +269 -5
  66. package/scroll-area/README.md +233 -5
  67. package/searchable-multiselect/README.md +323 -2
  68. package/select/README.md +437 -2
  69. package/separator/README.md +222 -2
  70. package/sheet/README.md +311 -2
  71. package/sidebar/README.md +457 -5
  72. package/skeleton/README.md +217 -5
  73. package/slider/README.md +273 -5
  74. package/slider/fesm2022/slider.mjs +3 -3
  75. package/sonner/README.md +346 -2
  76. package/spinner/README.md +284 -2
  77. package/switch/README.md +310 -2
  78. package/table/README.md +423 -5
  79. package/tabs/README.md +411 -2
  80. package/tabs/fesm2022/tabs.mjs +2 -2
  81. package/textarea/README.md +282 -5
  82. package/toggle/README.md +270 -5
  83. package/toggle-group/README.md +340 -5
  84. package/tooltip/README.md +269 -2
  85. package/typography/README.md +271 -5
  86. package/typography/types/typography.d.ts +8 -8
  87. package/utils/README.md +303 -2
@@ -1,3 +1,503 @@
1
- # Autocomplete Subpath
1
+ # Autocomplete (`@egose/shadcn-theme-ng/autocomplete`)
2
2
 
3
- This project ships only as `@egose/shadcn-theme-ng/autocomplete` or `@egose/shadcn-theme-ng-tw/autocomplete`. See the [package README](../../README.md) for installation, compatibility, Tailwind variant, test, and release guidance. Do not publish this project directory independently.
3
+ A filter-as-you-type popup input in the shadcn/ui Command/Combobox style: a text field anchored to
4
+ a floating listbox with grouped options, an empty state, a clear button, and async status rows.
5
+ Use it for country pickers, command palettes backing a text field, tag inputs, and searchable
6
+ option lists.
7
+
8
+ The implementation styles the headless autocomplete primitives from
9
+ `@spartan-ng/brain/autocomplete` (state, filtering, active-item tracking) combined with
10
+ `BrnPopover` / `BrnPopoverContent` from `@spartan-ng/brain/popover` for floating placement.
11
+ Everything is a thin directive (shadcn classes via `classes()`) except `HlmAutocompleteInput`
12
+ and `HlmAutocompleteItem`, which are small components. `HlmAutocomplete` is the form-field style
13
+ root; `HlmAutocompleteSearch` is the same shell wired for live search callbacks.
14
+
15
+ > **Ships as:** `@egose/shadcn-theme-ng/autocomplete` and `@egose/shadcn-theme-ng-tw/autocomplete`
16
+ > (the `tw:`-prefixed Tailwind variant). Both expose the identical TypeScript surface; only the
17
+ > emitted Tailwind class strings differ. See the [package README](../../README.md) for install
18
+ > steps, peer dependencies, and Tailwind setup. Do not publish this project directory independently.
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ # Plain Tailwind (no prefix)
24
+ npm install @egose/shadcn-theme-ng
25
+
26
+ # tw:-prefixed Tailwind variant
27
+ npm install @egose/shadcn-theme-ng-tw
28
+ ```
29
+
30
+ Peer dependencies (Angular, `@spartan-ng/brain`, `@ng-icons/*`, `rxjs`, …) are documented in the
31
+ [package README](../../README.md#peer-dependencies). This subpath additionally relies at runtime on
32
+ `@egose/shadcn-theme-ng/utils` (`classes()`), `@egose/shadcn-theme-ng/input-group`
33
+ (`HlmInputGroup*`, composed inside `HlmAutocompleteInput`), and `@ng-icons/lucide`
34
+ (`lucideSearch`, `lucideX`, `lucideCheck`).
35
+
36
+ ## Imports
37
+
38
+ All symbols are exported from the subpath root (`projects/autocomplete/src/public-api.ts`):
39
+
40
+ ```ts
41
+ import {
42
+ HlmAutocomplete,
43
+ HlmAutocompleteSearch,
44
+ HlmAutocompleteInput,
45
+ HlmAutocompleteContent,
46
+ HlmAutocompletePortal,
47
+ HlmAutocompleteList,
48
+ HlmAutocompleteItem,
49
+ HlmAutocompleteGroup,
50
+ HlmAutocompleteLabel,
51
+ HlmAutocompleteEmpty,
52
+ HlmAutocompleteSeparator,
53
+ HlmAutocompleteStatus,
54
+ HlmAutocompleteImports,
55
+ HlmAutocompleteModule,
56
+ } from '@egose/shadcn-theme-ng/autocomplete';
57
+ // tw variant: replace with '@egose/shadcn-theme-ng-tw/autocomplete'
58
+ ```
59
+
60
+ Standalone component — spread the `*Imports` array:
61
+
62
+ ```ts
63
+ import { Component } from '@angular/core';
64
+ import { HlmAutocompleteImports } from '@egose/shadcn-theme-ng/autocomplete';
65
+
66
+ @Component({
67
+ selector: 'app-demo',
68
+ standalone: true,
69
+ imports: [...HlmAutocompleteImports],
70
+ template: `...`,
71
+ })
72
+ export class DemoComponent {}
73
+ ```
74
+
75
+ NgModule-based consumer — import the module:
76
+
77
+ ```ts
78
+ import { NgModule } from '@angular/core';
79
+ import { HlmAutocompleteModule } from '@egose/shadcn-theme-ng/autocomplete';
80
+
81
+ @NgModule({ imports: [HlmAutocompleteModule] })
82
+ export class DemoModule {}
83
+ ```
84
+
85
+ ## Anatomy / Structure
86
+
87
+ ```html
88
+ <div hlmAutocomplete [(value)]="selected" [(search)]="query">
89
+ <hlm-autocomplete-input placeholder="Search frameworks..." />
90
+
91
+ <div hlmAutocompletePortal>
92
+ <div hlmAutocompleteContent>
93
+ <ul hlmAutocompleteList>
94
+ <li hlmAutocompleteGroup>
95
+ <span hlmAutocompleteLabel>Suggestions</span>
96
+ <hlm-autocomplete-item [value]="opt" [id]="opt.id"> {{ opt.label }} </hlm-autocomplete-item>
97
+ </li>
98
+ <div hlmAutocompleteSeparator></div>
99
+ <hlm-autocomplete-empty>No results for "{{ query }}".</hlm-autocomplete-empty>
100
+ <hlm-autocomplete-status>Loading…</hlm-autocomplete-status>
101
+ </ul>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ ```
106
+
107
+ Real selectors (from source):
108
+
109
+ | Class | Selector(s) | Kind |
110
+ | -------------------------- | ---------------------------------------------------- | --------- |
111
+ | `HlmAutocomplete` | `[hlmAutocomplete], hlm-autocomplete` | Directive |
112
+ | `HlmAutocompleteSearch` | `[hlmAutocompleteSearch], hlm-autocomplete-search` | Directive |
113
+ | `HlmAutocompleteInput` | `hlm-autocomplete-input` | Component |
114
+ | `HlmAutocompleteContent` | `[hlmAutocompleteContent], hlm-autocomplete-content` | Directive |
115
+ | `HlmAutocompletePortal` | `[hlmAutocompletePortal]` | Directive |
116
+ | `HlmAutocompleteList` | `[hlmAutocompleteList]` | Directive |
117
+ | `HlmAutocompleteItem` | `hlm-autocomplete-item` | Component |
118
+ | `HlmAutocompleteGroup` | `[hlmAutocompleteGroup]` | Directive |
119
+ | `HlmAutocompleteLabel` | `[hlmAutocompleteLabel]` | Directive |
120
+ | `HlmAutocompleteEmpty` | `[hlmAutocompleteEmpty], hlm-autocomplete-empty` | Directive |
121
+ | `HlmAutocompleteSeparator` | `[hlmAutocompleteSeparator]` | Directive |
122
+ | `HlmAutocompleteStatus` | `[hlmAutocompleteStatus], hlm-autocomplete-status` | Directive |
123
+
124
+ ## API reference
125
+
126
+ Inputs/outputs below come from the `hostDirectives` declarations in source. `HlmAutocomplete`
127
+ and `HlmAutocompleteSearch` both also carry `BrnPopover` behavior
128
+ (`align` default `'start'`, `sideOffset` default `6`).
129
+
130
+ ### `HlmAutocomplete` — `[hlmAutocomplete], hlm-autocomplete`
131
+
132
+ `BrnAutocomplete` + `BrnPopover`.
133
+
134
+ | Input | Description |
135
+ | ------------------------------------------------------------------------ | ----------------------------------------------------------------------- |
136
+ | `autoHighlight` | Auto-highlight the first matching item. |
137
+ | `disabled` | Disable the whole autocomplete. |
138
+ | `value` | Currently selected value (two-way with `valueChange`). |
139
+ | `search` | Current search text (two-way with `searchChange`). |
140
+ | `itemToString` | Maps an item value to its display string. |
141
+ | `isItemEqualToValue` | Equality check for selection (`HlmAutocomplete` only, not on `Search`). |
142
+ | `align`, `closeOnOutsidePointerEvents`, `sideOffset`, `state`, `offsetX` | Popover placement/state (via `BrnPopover`). |
143
+
144
+ | Output | Description |
145
+ | -------------- | ---------------------------------------------- |
146
+ | `valueChange` | Emits the newly selected value. |
147
+ | `searchChange` | Emits the search text on each keystroke. |
148
+ | `stateChanged` | Popover open-state changes (via `BrnPopover`). |
149
+ | `closed` | Popover closed (via `BrnPopover`). |
150
+
151
+ ### `HlmAutocompleteSearch` — `[hlmAutocompleteSearch], hlm-autocomplete-search`
152
+
153
+ Same as `HlmAutocomplete` **minus** `isItemEqualToValue`. Prefer it when the list is driven by a
154
+ server query rather than local equality.
155
+
156
+ ### `HlmAutocompleteInput` — `hlm-autocomplete-input`
157
+
158
+ Component composing `BrnAutocompleteAnchor` + `HlmInputGroup` around a native `<input
159
+ brnAutocompleteInput hlmInputGroupInput>`, with optional search/clear addons.
160
+
161
+ | Input | Type | Default | Description |
162
+ | ----------------------------------------------- | ---------------------- | ------------------------------------------------- | --------------------------------------------------------- |
163
+ | `inputId` | `string` | `'hlm-autocomplete-input-<n>'` (auto-incremented) | `id` forwarded to the inner input. |
164
+ | `placeholder` | `string` | `''` | Placeholder text. |
165
+ | `showSearch` | `boolean` | `true` | Show the leading search icon addon. |
166
+ | `showClear` | `boolean` | `false` | Show the trailing clear (`*brnAutocompleteClear`) button. |
167
+ | `forceInvalid` | `boolean` | `false` | Force the invalid visual state. |
168
+ | `aria-invalid` (alias of `ariaInvalidOverride`) | `boolean \| undefined` | `undefined` (auto-detect from parent error state) | Manual override for `aria-invalid`. |
169
+
170
+ ### `HlmAutocompleteItem` — `hlm-autocomplete-item`
171
+
172
+ Component wrapping `BrnAutocompleteItem`; renders a check icon at the inline-end when the item is
173
+ active (`_active()` signal read from the injected brain item).
174
+
175
+ | Input (via brain) | Description |
176
+ | ----------------- | ---------------------------- |
177
+ | `id` | Item id. |
178
+ | `disabled` | Disable this option. |
179
+ | `value` | The option value (any type). |
180
+
181
+ ### `HlmAutocompleteList` / `HlmAutocompleteLabel` / `HlmAutocompleteSeparator`
182
+
183
+ | Directive | Brain inputs | Notes |
184
+ | --------------------------------------------------------- | ------------- | ----------------------------------------------------------------------- |
185
+ | `HlmAutocompleteList` (`[hlmAutocompleteList]`) | `id` | Scrollable list shell; collapses padding when empty (`data-empty:p-0`). |
186
+ | `HlmAutocompleteLabel` (`[hlmAutocompleteLabel]`) | `id` | Small muted group caption. |
187
+ | `HlmAutocompleteGroup` (`[hlmAutocompleteGroup]`) | — | Groups items; hides via `data-hidden`. |
188
+ | `HlmAutocompleteSeparator` (`[hlmAutocompleteSeparator]`) | `orientation` | 1px divider between groups. |
189
+
190
+ ### `HlmAutocompleteContent` / `HlmAutocompletePortal`
191
+
192
+ | Directive | Brain primitive | Notes |
193
+ | ------------------------ | ----------------------------------------------- | --------------------------------------------------------------------------------------- |
194
+ | `HlmAutocompleteContent` | `BrnAutocompleteContent` | Floating panel (`max-h-72`, popover theme, width synced to `--brn-autocomplete-width`). |
195
+ | `HlmAutocompletePortal` | `BrnPopoverContent` (`context`, `class` inputs) | CDK-portal outlet — wrap the content in it. |
196
+
197
+ ### `HlmAutocompleteEmpty` / `HlmAutocompleteStatus`
198
+
199
+ Layout-only states, no inputs. `Empty` only displays when the content reports
200
+ `group-data-empty` (no matches); `Status` is a centered row for spinners / "loading…" /
201
+ result counts.
202
+
203
+ ## Examples
204
+
205
+ ### 1. Basic local filtering
206
+
207
+ ```ts
208
+ import { Component, computed, signal } from '@angular/core';
209
+ import { HlmAutocompleteImports } from '@egose/shadcn-theme-ng/autocomplete';
210
+
211
+ interface Fruit {
212
+ id: string;
213
+ label: string;
214
+ }
215
+
216
+ @Component({
217
+ selector: 'app-autocomplete-basic',
218
+ standalone: true,
219
+ imports: [...HlmAutocompleteImports],
220
+ template: `
221
+ <div hlmAutocomplete [(search)]="query" [(value)]="selected">
222
+ <hlm-autocomplete-input placeholder="Search fruit..." [showClear]="true" />
223
+ <div hlmAutocompletePortal>
224
+ <div hlmAutocompleteContent>
225
+ <ul hlmAutocompleteList>
226
+ @for (f of filtered(); track f.id) {
227
+ <hlm-autocomplete-item [value]="f" [id]="f.id">{{ f.label }}</hlm-autocomplete-item>
228
+ }
229
+ <hlm-autocomplete-empty>No fruit matches "{{ query() }}".</hlm-autocomplete-empty>
230
+ </ul>
231
+ </div>
232
+ </div>
233
+ </div>
234
+ <p class="tw:mt-2 tw:text-sm">Selected: {{ selected()?.label ?? 'none' }}</p>
235
+ `,
236
+ })
237
+ export class AutocompleteBasicComponent {
238
+ private readonly all: Fruit[] = [
239
+ { id: 'apple', label: 'Apple' },
240
+ { id: 'banana', label: 'Banana' },
241
+ { id: 'cherry', label: 'Cherry' },
242
+ { id: 'date', label: 'Date' },
243
+ ];
244
+ readonly query = signal('');
245
+ readonly selected = signal<Fruit | null>(null);
246
+ readonly filtered = computed(() => {
247
+ const q = this.query().trim().toLowerCase();
248
+ return q ? this.all.filter((f) => f.label.toLowerCase().includes(q)) : this.all;
249
+ });
250
+ }
251
+ ```
252
+
253
+ ### 2. Grouped options with labels and separators
254
+
255
+ ```html
256
+ <div hlmAutocomplete [(search)]="query" [(value)]="picked">
257
+ <hlm-autocomplete-input placeholder="Pick a city..." />
258
+ <div hlmAutocompletePortal>
259
+ <div hlmAutocompleteContent>
260
+ <ul hlmAutocompleteList>
261
+ <li hlmAutocompleteGroup>
262
+ <span hlmAutocompleteLabel>Europe</span>
263
+ <hlm-autocomplete-item [value]="'berlin'" id="berlin">Berlin</hlm-autocomplete-item>
264
+ <hlm-autocomplete-item [value]="'paris'" id="paris">Paris</hlm-autocomplete-item>
265
+ </li>
266
+ <div hlmAutocompleteSeparator></div>
267
+ <li hlmAutocompleteGroup>
268
+ <span hlmAutocompleteLabel>Asia</span>
269
+ <hlm-autocomplete-item [value]="'tokyo'" id="tokyo">Tokyo</hlm-autocomplete-item>
270
+ <hlm-autocomplete-item [value]="'seoul'" id="seoul" [disabled]="true">
271
+ Seoul (disabled)
272
+ </hlm-autocomplete-item>
273
+ </li>
274
+ <hlm-autocomplete-empty>Nothing found.</hlm-autocomplete-empty>
275
+ </ul>
276
+ </div>
277
+ </div>
278
+ </div>
279
+ ```
280
+
281
+ ```ts
282
+ import { Component, signal } from '@angular/core';
283
+ import { HlmAutocompleteImports } from '@egose/shadcn-theme-ng/autocomplete';
284
+
285
+ @Component({
286
+ selector: 'app-autocomplete-groups',
287
+ standalone: true,
288
+ imports: [...HlmAutocompleteImports],
289
+ templateUrl: './autocomplete-groups.html',
290
+ })
291
+ export class AutocompleteGroupsComponent {
292
+ readonly query = signal('');
293
+ readonly picked = signal<string | null>(null);
294
+ }
295
+ ```
296
+
297
+ ### 3. Async server search with a status row
298
+
299
+ Use `HlmAutocompleteSearch` and react to `searchChange` with a debounced fetch:
300
+
301
+ ```ts
302
+ import { Component, signal } from '@angular/core';
303
+ import { toObservable } from '@angular/core/rxjs-interop';
304
+ import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
305
+ import { HlmAutocompleteImports } from '@egose/shadcn-theme-ng/autocomplete';
306
+
307
+ @Component({
308
+ selector: 'app-autocomplete-async',
309
+ standalone: true,
310
+ imports: [...HlmAutocompleteImports],
311
+ template: `
312
+ <div hlmAutocompleteSearch [(search)]="query" [(value)]="user" (searchChange)="onSearch($event)">
313
+ <hlm-autocomplete-input placeholder="Search users..." />
314
+ <div hlmAutocompletePortal>
315
+ <div hlmAutocompleteContent>
316
+ <ul hlmAutocompleteList>
317
+ @for (u of results(); track u.id) {
318
+ <hlm-autocomplete-item [value]="u" [id]="u.id">{{ u.name }}</hlm-autocomplete-item>
319
+ }
320
+ @if (loading()) {
321
+ <hlm-autocomplete-status>Searching…</hlm-autocomplete-status>
322
+ } @else {
323
+ <hlm-autocomplete-empty>No users for "{{ query() }}".</hlm-autocomplete-empty>
324
+ }
325
+ </ul>
326
+ </div>
327
+ </div>
328
+ </div>
329
+ `,
330
+ })
331
+ export class AutocompleteAsyncComponent {
332
+ readonly query = signal('');
333
+ readonly user = signal<{ id: string; name: string } | null>(null);
334
+ readonly results = signal<{ id: string; name: string }[]>([]);
335
+ readonly loading = signal(false);
336
+
337
+ constructor() {
338
+ toObservable(this.query)
339
+ .pipe(debounceTime(250), distinctUntilChanged())
340
+ .subscribe((q) => void this.fetch(q));
341
+ }
342
+
343
+ onSearch(_: string) {
344
+ this.loading.set(true);
345
+ }
346
+
347
+ private async fetch(q: string) {
348
+ if (!q.trim()) {
349
+ this.results.set([]);
350
+ this.loading.set(false);
351
+ return;
352
+ }
353
+ const res = await fetch(`/api/users?q=${encodeURIComponent(q)}`).then((r) => r.json());
354
+ this.results.set(res);
355
+ this.loading.set(false);
356
+ }
357
+ }
358
+ ```
359
+
360
+ > `switchMap`-based cancellation is preferable for real apps; the manual version above keeps the
361
+ > example dependency-free. `onSearch` only flips the spinner — the debounced fetch does the work.
362
+
363
+ ### 4. Reactive-forms binding with validation visuals
364
+
365
+ ```ts
366
+ import { Component } from '@angular/core';
367
+ import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
368
+ import { HlmAutocompleteImports } from '@egose/shadcn-theme-ng/autocomplete';
369
+
370
+ @Component({
371
+ selector: 'app-autocomplete-form',
372
+ standalone: true,
373
+ imports: [...HlmAutocompleteImports, ReactiveFormsModule],
374
+ template: `
375
+ <div hlmAutocomplete [value]="control.value" (valueChange)="control.setValue($event)">
376
+ <hlm-autocomplete-input placeholder="Country (required)..." [forceInvalid]="control.touched && control.invalid" />
377
+ <div hlmAutocompletePortal>
378
+ <div hlmAutocompleteContent>
379
+ <ul hlmAutocompleteList>
380
+ @for (c of countries; track c) {
381
+ <hlm-autocomplete-item [value]="c" [id]="c">{{ c }}</hlm-autocomplete-item>
382
+ }
383
+ <hlm-autocomplete-empty>No match.</hlm-autocomplete-empty>
384
+ </ul>
385
+ </div>
386
+ </div>
387
+ </div>
388
+ @if (control.touched && control.invalid) {
389
+ <p class="tw:mt-1 tw:text-sm tw:text-destructive">Please choose a country.</p>
390
+ }
391
+ `,
392
+ })
393
+ export class AutocompleteFormComponent {
394
+ readonly control = new FormControl<string | null>(null, Validators.required);
395
+ readonly countries = ['Austria', 'France', 'Japan', 'Kenya', 'Peru'];
396
+ }
397
+ ```
398
+
399
+ ### 5. Custom display strings with `itemToString` (object values)
400
+
401
+ ```ts
402
+ import { Component, signal } from '@angular/core';
403
+ import { HlmAutocompleteImports } from '@egose/shadcn-theme-ng/autocomplete';
404
+
405
+ interface Repo {
406
+ id: number;
407
+ fullName: string;
408
+ stars: number;
409
+ }
410
+
411
+ @Component({
412
+ selector: 'app-autocomplete-tostring',
413
+ standalone: true,
414
+ imports: [...HlmAutocompleteImports],
415
+ template: `
416
+ <div hlmAutocomplete [(search)]="query" [(value)]="repo" [itemToString]="toLabel" [isItemEqualToValue]="sameRepo">
417
+ <hlm-autocomplete-input placeholder="Search repos..." [showClear]="true" />
418
+ <div hlmAutocompletePortal>
419
+ <div hlmAutocompleteContent>
420
+ <ul hlmAutocompleteList>
421
+ @for (r of repos; track r.id) {
422
+ <hlm-autocomplete-item [value]="r" [id]="String(r.id)">
423
+ {{ r.fullName }} ★ {{ r.stars }}
424
+ </hlm-autocomplete-item>
425
+ }
426
+ <hlm-autocomplete-empty>No repositories found.</hlm-autocomplete-empty>
427
+ </ul>
428
+ </div>
429
+ </div>
430
+ </div>
431
+ `,
432
+ })
433
+ export class AutocompleteToStringComponent {
434
+ readonly query = signal('');
435
+ readonly repo = signal<Repo | null>(null);
436
+ readonly repos: Repo[] = [
437
+ { id: 1, fullName: 'spartan-ng/spartan', stars: 4200 },
438
+ { id: 2, fullName: 'angular/angular', stars: 96000 },
439
+ ];
440
+ readonly toLabel = (r: Repo | null) => r?.fullName ?? '';
441
+ readonly sameRepo = (a: Repo | null, b: Repo | null) => a?.id === b?.id;
442
+ }
443
+ ```
444
+
445
+ ### 6. Disabled state + custom ids for a11y wiring
446
+
447
+ ```ts
448
+ import { Component, signal } from '@angular/core';
449
+ import { HlmAutocompleteImports } from '@egose/shadcn-theme-ng/autocomplete';
450
+
451
+ @Component({
452
+ selector: 'app-autocomplete-disabled',
453
+ standalone: true,
454
+ imports: [...HlmAutocompleteImports],
455
+ template: `
456
+ <label for="city-input" class="tw:mb-1 tw:block tw:text-sm tw:font-medium">City</label>
457
+ <div hlmAutocomplete [disabled]="locked()" [(value)]="city">
458
+ <hlm-autocomplete-input inputId="city-input" placeholder="Pick a city..." />
459
+ <div hlmAutocompletePortal>
460
+ <div hlmAutocompleteContent>
461
+ <ul hlmAutocompleteList>
462
+ <hlm-autocomplete-item [value]="'oslo'" id="oslo">Oslo</hlm-autocomplete-item>
463
+ <hlm-autocomplete-item [value]="'lima'" id="lima">Lima</hlm-autocomplete-item>
464
+ <hlm-autocomplete-empty>No match.</hlm-autocomplete-empty>
465
+ </ul>
466
+ </div>
467
+ </div>
468
+ </div>
469
+ <button class="tw:mt-2" (click)="locked.update((v) => !v)">
470
+ {{ locked() ? 'Unlock' : 'Lock' }}
471
+ </button>
472
+ `,
473
+ })
474
+ export class AutocompleteDisabledComponent {
475
+ readonly locked = signal(true);
476
+ readonly city = signal<string | null>(null);
477
+ }
478
+ ```
479
+
480
+ ## Accessibility notes
481
+
482
+ - The input is a real text field (`brnAutocompleteInput`) with listbox semantics from the brain:
483
+ arrow keys move the highlight, `Enter` selects, `Escape` dismisses. The clear button is a native
484
+ `<button>` disabled in sync with the input.
485
+ - `HlmAutocompleteEmpty` / `HlmAutocompleteStatus` give screen-reader users feedback for the two
486
+ critical non-visual states (no matches / loading) — always include at least the empty row.
487
+ - Label the field: either set `inputId` and pair it with a `<label for>`, or wrap the group with
488
+ `HlmField`/`hlmLabel` from `@egose/shadcn-theme-ng/field` / `.../label`.
489
+ - `aria-invalid` auto-detects the parent error state; only set the override when you manage
490
+ validity yourself (e.g. cross-field rules), and pair it with visible error text.
491
+
492
+ ## Theming / CSS variables
493
+
494
+ Class-based styling; the floating panel width tracks the anchor via the
495
+ `--brn-autocomplete-width` custom property set by the brain (`w-(--brn-autocomplete-width)`).
496
+ No theme variables of its own — adjust popover placement with the `align` / `sideOffset` inputs.
497
+
498
+ ## Related subpaths
499
+
500
+ - `@egose/shadcn-theme-ng/input-group` — the input shell composed inside `HlmAutocompleteInput`
501
+ - `@egose/shadcn-theme-ng/combobox` — button-triggered (rather than text-anchored) picker
502
+ - `@egose/shadcn-theme-ng/command` — command-palette list primitives
503
+ - `@egose/shadcn-theme-ng/popover` — lower-level floating panels