@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,11 +1,239 @@
1
- # ScrollArea
1
+ # Scroll Area (`@egose/shadcn-theme-ng/scroll-area`)
2
2
 
3
- This project was generated using [Angular CLI](https://github.com/angular/angular-cli).
3
+ A shadcn/ui-style **Scroll Area** — a themed scrollable container with hover-reveal scrollbars. This is the Angular equivalent of shadcn/ui `ScrollArea`.
4
4
 
5
- ## Building
5
+ Unlike most subpaths in this library it is **not** built on spartan-ng/brain: it is a thin directive wrapper over `NgScrollbar` from **ngx-scrollbar**. The directive matches `ng-scrollbar[hlm]` (or `ng-scrollbar[hlmScrollbar]`), forces `visibility: 'hover'` scrollbar options, and pins the shadcn scrollbar CSS variables (thumb color, track color/thickness) so every scroll area looks consistent with the theme.
6
6
 
7
- To build the library, run:
7
+ > **Ships as:** `@egose/shadcn-theme-ng/scroll-area` and `@egose/shadcn-theme-ng-tw/scroll-area` (the `tw:`-prefixed Tailwind variant — same API, class strings prefixed with `tw:`).
8
+ > See the [package README](../../README.md) for installation, peer dependencies, and Tailwind setup. Do not publish this project directory independently.
9
+
10
+ ## Installation
8
11
 
9
12
  ```bash
10
- ng build scroll-area
13
+ # Plain Tailwind (no prefix)
14
+ npm install @egose/shadcn-theme-ng
15
+
16
+ # Or the tw:-prefixed variant
17
+ npm install @egose/shadcn-theme-ng-tw
18
+ ```
19
+
20
+ ```ts
21
+ import { HlmScrollAreaImports } from '@egose/shadcn-theme-ng/scroll-area';
22
+ // tw variant:
23
+ // import { HlmScrollAreaImports } from '@egose/shadcn-theme-ng-tw/scroll-area';
11
24
  ```
25
+
26
+ Peer dependencies (see [package README](../../README.md) for versions): `@angular/core`, `@angular/common`, `@spartan-ng/brain`. Runtime `ngx-scrollbar` is installed transitively — but note you must import `NgScrollbar` itself (from `ngx-scrollbar`) wherever you use the directive, since this package only provides the `hlm` styling layer.
27
+
28
+ ## Imports
29
+
30
+ Real exported symbols (from `src/public-api.ts`):
31
+
32
+ | Symbol | Kind | Description |
33
+ | ---------------------- | ------------- | ------------------------------------------------------------------------------------------------- |
34
+ | `HlmScrollArea` | Directive | Styling/behavior layer for `NgScrollbar`; selector `ng-scrollbar[hlm],ng-scrollbar[hlmScrollbar]` |
35
+ | `HlmScrollAreaImports` | `const` array | `[HlmScrollArea]` standalone imports |
36
+ | `HlmScrollAreaModule` | `NgModule` | NgModule wrapper re-exporting `HlmScrollArea` |
37
+
38
+ Standalone usage (you almost always pair it with `NgScrollbar`):
39
+
40
+ ```ts
41
+ import { Component } from '@angular/core';
42
+ import { NgScrollbar } from 'ngx-scrollbar';
43
+ import { HlmScrollAreaImports } from '@egose/shadcn-theme-ng/scroll-area';
44
+
45
+ @Component({
46
+ selector: 'app-demo',
47
+ standalone: true,
48
+ imports: [NgScrollbar, HlmScrollAreaImports],
49
+ template: ` <ng-scrollbar hlm class="tw:h-64"> ... </ng-scrollbar> `,
50
+ })
51
+ export class DemoComponent {}
52
+ ```
53
+
54
+ NgModule usage:
55
+
56
+ ```ts
57
+ import { NgModule } from '@angular/core';
58
+ import { NgScrollbar } from 'ngx-scrollbar';
59
+ import { HlmScrollAreaModule } from '@egose/shadcn-theme-ng/scroll-area';
60
+
61
+ @NgModule({ imports: [NgScrollbar, HlmScrollAreaModule] })
62
+ export class DemoModule {}
63
+ ```
64
+
65
+ ## Anatomy / Structure
66
+
67
+ ```html
68
+ <ng-scrollbar hlm class="tw:h-72 tw:w-80 tw:rounded-md tw:border">
69
+ <div class="tw:p-4">
70
+ <h4>Title</h4>
71
+ <p>Scrollable content…</p>
72
+ </div>
73
+ </ng-scrollbar>
74
+ ```
75
+
76
+ Real selectors:
77
+
78
+ | Selector | Class | Notes |
79
+ | ------------------------------------------------- | --------------- | -------------------------------------------------------------------------------------------------------- |
80
+ | `ng-scrollbar[hlm]`, `ng-scrollbar[hlmScrollbar]` | `HlmScrollArea` | `data-slot="scroll-area"`; requires the `NgScrollbar` component from `ngx-scrollbar` as the host element |
81
+
82
+ The directive sets these host bindings: `--scrollbar-thumb-color` and `--scrollbar-thumb-hover-color` to `var(--border)`, `--scrollbar-track-color: transparent`, `--scrollbar-track-thickness: 0.625rem`, `--scrollbar-track-offset: 1.5px`, plus a rounded/pill thumb shape and block layout classes. Scrollbars appear on hover (`visibility: 'hover'`).
83
+
84
+ ## API reference
85
+
86
+ `HlmScrollArea` declares **no inputs, outputs, or methods of its own** — it is a pure styling directive. All scrolling behavior/inputs (e.g. `orientation`, `visibility` overrides, viewport access) come from the host `NgScrollbar` component itself; consult the `ngx-scrollbar` API for those.
87
+
88
+ | Host CSS variables set by the directive | Value |
89
+ | --------------------------------------- | -------------------- |
90
+ | `--scrollbar-thumb-color` | `var(--border)` |
91
+ | `--scrollbar-thumb-hover-color` | `var(--border)` |
92
+ | `--scrollbar-track-color` | `transparent` |
93
+ | `--scrollbar-track-thickness` | `0.625rem` |
94
+ | `--scrollbar-track-offset` | `1.5px` |
95
+ | `--scrollbar-thumb-shape` | `9999px` (via class) |
96
+
97
+ ## Examples
98
+
99
+ ### 1. Basic vertical scroll area
100
+
101
+ ```ts
102
+ import { Component } from '@angular/core';
103
+ import { NgScrollbar } from 'ngx-scrollbar';
104
+ import { HlmScrollAreaImports } from '@egose/shadcn-theme-ng/scroll-area';
105
+
106
+ @Component({
107
+ selector: 'app-basic-scroll',
108
+ standalone: true,
109
+ imports: [NgScrollbar, HlmScrollAreaImports],
110
+ template: `
111
+ <ng-scrollbar hlm class="tw:h-64 tw:w-80 tw:rounded-md tw:border">
112
+ <div class="tw:p-4 tw:space-y-2">
113
+ @for (item of items; track item) {
114
+ <p class="tw:text-sm">{{ item }}</p>
115
+ }
116
+ </div>
117
+ </ng-scrollbar>
118
+ `,
119
+ })
120
+ export class BasicScrollComponent {
121
+ readonly items = Array.from({ length: 50 }, (_, i) => `Item ${i + 1}`);
122
+ }
123
+ ```
124
+
125
+ ### 2. Fixed-height log / chat window
126
+
127
+ ```ts
128
+ import { Component, signal } from '@angular/core';
129
+ import { NgScrollbar } from 'ngx-scrollbar';
130
+ import { HlmScrollAreaImports } from '@egose/shadcn-theme-ng/scroll-area';
131
+
132
+ @Component({
133
+ selector: 'app-log-scroll',
134
+ standalone: true,
135
+ imports: [NgScrollbar, HlmScrollAreaImports],
136
+ template: `
137
+ <ng-scrollbar hlm class="tw:h-72 tw:rounded-md tw:border tw:bg-muted/30">
138
+ <div class="tw:p-4 tw:font-mono tw:text-xs tw:space-y-1">
139
+ @for (line of lines(); track $index) {
140
+ <div>{{ line }}</div>
141
+ }
142
+ </div>
143
+ </ng-scrollbar>
144
+ <button type="button" (click)="addLine()">Add line</button>
145
+ `,
146
+ })
147
+ export class LogScrollComponent {
148
+ readonly lines = signal(['[ok] booted', '[ok] connected']);
149
+
150
+ addLine(): void {
151
+ this.lines.update((ls) => [...ls, `[log] event ${ls.length}`]);
152
+ }
153
+ }
154
+ ```
155
+
156
+ ### 3. Horizontal scrolling row
157
+
158
+ `NgScrollbar` supports horizontal viewports — the `hlm` styling applies the same way:
159
+
160
+ ```html
161
+ <ng-scrollbar hlm orientation="horizontal" class="tw:w-full tw:max-w-xl tw:rounded-md tw:border">
162
+ <div class="tw:flex tw:gap-3 tw:p-4 tw:w-max">
163
+ @for (card of cards; track card) {
164
+ <div class="tw:h-24 tw:w-40 tw:shrink-0 tw:rounded-md tw:bg-muted tw:p-2">{{ card }}</div>
165
+ }
166
+ </div>
167
+ </ng-scrollbar>
168
+ ```
169
+
170
+ ### 4. Scroll area inside a card
171
+
172
+ ```html
173
+ <div class="tw:rounded-lg tw:border tw:p-4">
174
+ <h3 class="tw:mb-2 tw:font-semibold">Recent activity</h3>
175
+ <ng-scrollbar hlm class="tw:h-48">
176
+ <ul class="tw:space-y-2 tw:pe-4">
177
+ @for (event of activity; track event.id) {
178
+ <li class="tw:flex tw:justify-between tw:text-sm">
179
+ <span>{{ event.label }}</span>
180
+ <span class="tw:text-muted-foreground">{{ event.at }}</span>
181
+ </li>
182
+ }
183
+ </ul>
184
+ </ng-scrollbar>
185
+ </div>
186
+ ```
187
+
188
+ Leave end-padding (`pe-4`) so text never slides under the hover scrollbar.
189
+
190
+ ### 5. Long select/popover content
191
+
192
+ Scrollable dropdown content with the themed thumb:
193
+
194
+ ```html
195
+ <ng-scrollbar hlm class="tw:max-h-60 tw:w-64 tw:rounded-md tw:border">
196
+ <div class="tw:p-1">
197
+ @for (option of options; track option.value) {
198
+ <button
199
+ type="button"
200
+ (click)="pick(option)"
201
+ class="tw:w-full tw:rounded-sm tw:px-2 tw:py-1.5 tw:text-left tw:text-sm tw:hover:bg-accent"
202
+ >
203
+ {{ option.label }}
204
+ </button>
205
+ }
206
+ </div>
207
+ </ng-scrollbar>
208
+ ```
209
+
210
+ ### 6. Overriding thumb color per instance
211
+
212
+ The colors are CSS variables, so any instance can be re-themed inline:
213
+
214
+ ```html
215
+ <ng-scrollbar
216
+ hlm
217
+ class="tw:h-64 tw:rounded-md tw:border"
218
+ style="--scrollbar-thumb-color: var(--primary); --scrollbar-thumb-hover-color: var(--primary);"
219
+ >
220
+ <div class="tw:p-4">Brand-colored scrollbar…</div>
221
+ </ng-scrollbar>
222
+ ```
223
+
224
+ ## Accessibility notes
225
+
226
+ - `NgScrollbar` keeps native scroll semantics; keyboard users can scroll the region with arrows/PageUp/PageDown when it has focus — make sure scrollable regions with important content are focusable (`tabindex="0"`) and labelled (`aria-label`/`role="region"`).
227
+ - Hover-only scrollbars can be hard to discover for pointer users and invisible to some low-vision users — for primary page-level scrolling prefer native overflow; reserve this component for secondary panes (sidebars, dropdowns, logs).
228
+ - Do not nest scroll areas with competing orientations unless each has a clear label; nested scroll traps confuse both keyboard and screen-reader users.
229
+
230
+ ## Theming / CSS variables
231
+
232
+ The component is driven by `--scrollbar-*` variables (see table above) plus the theme's `--border` token. Override per instance with inline `style` as shown in example 6; dark mode follows automatically through the theme tokens.
233
+
234
+ ## Related subpaths
235
+
236
+ - `@egose/shadcn-theme-ng/resizable` — fixed-size panes that pair well with internal scroll areas
237
+ - `@egose/shadcn-theme-ng/select` — its dropdown content scrolls internally for long option lists
238
+ - `@egose/shadcn-theme-ng/sidebar` — sidebar content areas that often need themed scrolling
239
+ - `@egose/shadcn-theme-ng/card` — bordered containers frequently wrapped around scroll areas
@@ -1,3 +1,324 @@
1
- # Searchable Multiselect Subpath
1
+ # Searchable Multiselect (`@egose/shadcn-theme-ng/searchable-multiselect`)
2
2
 
3
- This project ships only as `@egose/shadcn-theme-ng/searchable-multiselect` or `@egose/shadcn-theme-ng-tw/searchable-multiselect`. See the [package README](../../README.md) for installation, compatibility, Tailwind variant, test, and release guidance. Do not publish this project directory independently.
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.
4
+
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
+
7
+ > **Ships as:** `@egose/shadcn-theme-ng/searchable-multiselect` and `@egose/shadcn-theme-ng-tw/searchable-multiselect` (the `tw:`-prefixed Tailwind variant — same API, class strings prefixed with `tw:`).
8
+ > See the [package README](../../README.md) for installation, peer dependencies, and Tailwind setup. Do not publish this project directory independently.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ # Plain Tailwind (no prefix)
14
+ npm install @egose/shadcn-theme-ng
15
+
16
+ # Or the tw:-prefixed variant
17
+ npm install @egose/shadcn-theme-ng-tw
18
+ ```
19
+
20
+ ```ts
21
+ import { EgSearchableMultiselect } from '@egose/shadcn-theme-ng/searchable-multiselect';
22
+ // tw variant:
23
+ // import { EgSearchableMultiselect } from '@egose/shadcn-theme-ng-tw/searchable-multiselect';
24
+ ```
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.)
27
+
28
+ ## Imports
29
+
30
+ Real exported symbols (from `src/public-api.ts`):
31
+
32
+ | Symbol | Kind | Description |
33
+ | ------------------------- | -------------------------------------------------- | ---------------------------------------------------------- |
34
+ | `EgSearchableMultiselect` | Standalone component (`eg-searchable-multiselect`) | The whole control; provides `NG_VALUE_ACCESSOR` for itself |
35
+ | `SelectOption` | Interface | `{ label: string; value: string }` |
36
+
37
+ > Note: unlike most subpaths, this one exposes **no `*Imports` array and no `*Module`**. Import `EgSearchableMultiselect` directly — it is standalone.
38
+
39
+ ```ts
40
+ import { Component } from '@angular/core';
41
+ import { EgSearchableMultiselect } from '@egose/shadcn-theme-ng/searchable-multiselect';
42
+
43
+ @Component({
44
+ selector: 'app-demo',
45
+ standalone: true,
46
+ imports: [EgSearchableMultiselect],
47
+ template: ` <eg-searchable-multiselect [options]="options" [(value)]="selected" /> `,
48
+ })
49
+ export class DemoComponent {}
50
+ ```
51
+
52
+ ## Anatomy / Structure
53
+
54
+ ```html
55
+ <eg-searchable-multiselect [options]="options" placeholder="Pick frameworks…" [(value)]="selected" />
56
+ ```
57
+
58
+ Renders (internally — you do not write this yourself):
59
+
60
+ ```html
61
+ <eg-searchable-multiselect>
62
+ <!-- chip row: placeholder pill when empty, removable chips otherwise -->
63
+ <div>
64
+ <span><!-- {{ placeholder }} or chip {{ item.label }} + ✕ button --></span>
65
+ </div>
66
+
67
+ <!-- popover trigger + checkbox list -->
68
+ <hlm-popover>
69
+ <button hlmPopoverTrigger hlmButton>N selected</button>
70
+ <hlm-popover-content>
71
+ <label><!-- <hlm-checkbox> per option + label text --></label>
72
+ </hlm-popover-content>
73
+ </hlm-popover>
74
+ </eg-searchable-multiselect>
75
+ ```
76
+
77
+ Real selector: `eg-searchable-multiselect` (element). The `✕` chip buttons and popover trigger honor the disabled state; the popover panel is `tw:w-64` with a `tw:max-h-60` scrolling option list.
78
+
79
+ ## API reference
80
+
81
+ ### EgSearchableMultiselect (component, `ControlValueAccessor`)
82
+
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 |
98
+
99
+ `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
+
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.
102
+
103
+ ## Examples
104
+
105
+ ### 1. Basic two-way binding
106
+
107
+ ```ts
108
+ import { Component, signal } from '@angular/core';
109
+ import { EgSearchableMultiselect, type SelectOption } from '@egose/shadcn-theme-ng/searchable-multiselect';
110
+
111
+ @Component({
112
+ selector: 'app-basic-multi',
113
+ standalone: true,
114
+ imports: [EgSearchableMultiselect],
115
+ template: `
116
+ <eg-searchable-multiselect [options]="frameworks" placeholder="Pick frameworks…" [(value)]="selected" />
117
+ <p>Selected: {{ selected().join(', ') || 'none' }}</p>
118
+ `,
119
+ })
120
+ export class BasicMultiComponent {
121
+ readonly frameworks: SelectOption[] = [
122
+ { label: 'Angular', value: 'angular' },
123
+ { label: 'React', value: 'react' },
124
+ { label: 'Vue', value: 'vue' },
125
+ { label: 'Svelte', value: 'svelte' },
126
+ ];
127
+ readonly selected = signal<string[]>(['angular']);
128
+ }
129
+ ```
130
+
131
+ ### 2. Reactive forms
132
+
133
+ ```ts
134
+ import { Component } from '@angular/core';
135
+ import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
136
+ import { EgSearchableMultiselect, type SelectOption } from '@egose/shadcn-theme-ng/searchable-multiselect';
137
+
138
+ @Component({
139
+ selector: 'app-reactive-multi',
140
+ standalone: true,
141
+ imports: [EgSearchableMultiselect, ReactiveFormsModule],
142
+ template: `
143
+ <form [formGroup]="form" (ngSubmit)="submit()">
144
+ <label for="skills">Skills (pick at least one)</label>
145
+ <eg-searchable-multiselect id="skills" [options]="skills" formControlName="skillIds" />
146
+ @if (form.controls.skillIds.invalid && form.controls.skillIds.touched) {
147
+ <p class="tw:text-destructive tw:text-sm">Choose at least one skill.</p>
148
+ }
149
+ <button type="submit">Save</button>
150
+ </form>
151
+ `,
152
+ })
153
+ export class ReactiveMultiComponent {
154
+ readonly skills: SelectOption[] = [
155
+ { label: 'TypeScript', value: 'ts' },
156
+ { label: 'CSS', value: 'css' },
157
+ { label: 'Testing', value: 'testing' },
158
+ ];
159
+ readonly form = new FormGroup({
160
+ skillIds: new FormControl<string[]>(['ts'], { validators: Validators.required }),
161
+ });
162
+
163
+ submit(): void {
164
+ this.form.markAllAsTouched();
165
+ console.log(this.form.value);
166
+ }
167
+ }
168
+ ```
169
+
170
+ ### 3. Template-driven forms (`ngModel`)
171
+
172
+ ```ts
173
+ import { Component } from '@angular/core';
174
+ import { FormsModule } from '@angular/forms';
175
+ import { EgSearchableMultiselect, type SelectOption } from '@egose/shadcn-theme-ng/searchable-multiselect';
176
+
177
+ @Component({
178
+ selector: 'app-ngmodel-multi',
179
+ standalone: true,
180
+ imports: [EgSearchableMultiselect, FormsModule],
181
+ template: `
182
+ <eg-searchable-multiselect name="tags" [options]="tags" [(ngModel)]="selected" #tagsCtrl="ngModel" />
183
+ @if (tagsCtrl.touched && !selected.length) {
184
+ <p class="tw:text-destructive tw:text-sm">Pick at least one tag.</p>
185
+ }
186
+ `,
187
+ })
188
+ export class NgModelMultiComponent {
189
+ readonly tags: SelectOption[] = [
190
+ { label: 'Bug', value: 'bug' },
191
+ { label: 'Feature', value: 'feature' },
192
+ { label: 'Docs', value: 'docs' },
193
+ ];
194
+ selected: string[] = [];
195
+ }
196
+ ```
197
+
198
+ ### 4. Disabled / read-only states
199
+
200
+ ```ts
201
+ import { Component } from '@angular/core';
202
+ import { EgSearchableMultiselect } from '@egose/shadcn-theme-ng/searchable-multiselect';
203
+
204
+ @Component({
205
+ selector: 'app-disabled-multi',
206
+ standalone: true,
207
+ imports: [EgSearchableMultiselect],
208
+ template: `
209
+ <!-- fully disabled: chips, ✕ buttons, trigger, checkboxes -->
210
+ <eg-searchable-multiselect [options]="options" [value]="['a']" disabled />
211
+
212
+ <!-- wrapper-driven disable (e.g. parent form section locked) -->
213
+ <eg-searchable-multiselect [options]="options" [value]="['a']" [wrapperDisabled]="locked" />
214
+ `,
215
+ })
216
+ export class DisabledMultiComponent {
217
+ readonly locked = true;
218
+ readonly options = [
219
+ { label: 'Alpha', value: 'a' },
220
+ { label: 'Beta', value: 'b' },
221
+ ];
222
+ }
223
+ ```
224
+
225
+ Disabling via `formControl.disable()` works too — it flows through `setDisabledState`.
226
+
227
+ ### 5. Client-side search (filter `options` yourself)
228
+
229
+ Since the popover lists exactly what you pass in `options`, implement search by filtering upstream:
230
+
231
+ ```ts
232
+ import { Component, computed, signal } from '@angular/core';
233
+ import { EgSearchableMultiselect } from '@egose/shadcn-theme-ng/searchable-multiselect';
234
+
235
+ @Component({
236
+ selector: 'app-search-multi',
237
+ standalone: true,
238
+ imports: [EgSearchableMultiselect],
239
+ 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"
246
+ />
247
+ <eg-searchable-multiselect [options]="filtered()" [(value)]="selected" />
248
+ `,
249
+ })
250
+ export class SearchMultiComponent {
251
+ readonly query = signal('');
252
+ readonly selected = signal<string[]>([]);
253
+ private readonly all = [
254
+ { label: 'Angular', value: 'angular' },
255
+ { label: 'React', value: 'react' },
256
+ { label: 'Vue', value: 'vue' },
257
+ { label: 'Svelte', value: 'svelte' },
258
+ { label: 'Solid', value: 'solid' },
259
+ ];
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
+ }
265
+ ```
266
+
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.
268
+
269
+ ### 6. Async options + reacting to changes
270
+
271
+ ```ts
272
+ import { Component, resource, signal } from '@angular/core';
273
+ import { EgSearchableMultiselect, type SelectOption } from '@egose/shadcn-theme-ng/searchable-multiselect';
274
+
275
+ @Component({
276
+ selector: 'app-async-multi',
277
+ standalone: true,
278
+ imports: [EgSearchableMultiselect],
279
+ template: `
280
+ @if (users.isLoading()) {
281
+ <p>Loading users…</p>
282
+ } @else {
283
+ <eg-searchable-multiselect
284
+ [options]="users.value() ?? []"
285
+ [(value)]="assignees"
286
+ (valueChange)="onChange($event)"
287
+ ariaLabel="Assignees"
288
+ />
289
+ }
290
+ `,
291
+ })
292
+ export class AsyncMultiComponent {
293
+ readonly assignees = signal<string[]>([]);
294
+ readonly users = resource({
295
+ loader: async (): Promise<SelectOption[]> => {
296
+ const res = await fetch('/api/users');
297
+ const list = (await res.json()) as Array<{ id: string; name: string }>;
298
+ return list.map((u) => ({ label: u.name, value: u.id }));
299
+ },
300
+ });
301
+
302
+ onChange(values: string[]): void {
303
+ console.log('assignees now:', values);
304
+ }
305
+ }
306
+ ```
307
+
308
+ ## Accessibility notes
309
+
310
+ - 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.
314
+
315
+ ## Theming / CSS variables
316
+
317
+ Class-driven (chips, popover panel, checkbox rows). Extend via the `class` input on the host; inner popover width (`tw:w-64`) and list height (`tw:max-h-60`) are fixed in the template.
318
+
319
+ ## Related subpaths
320
+
321
+ - `@egose/shadcn-theme-ng/form-searchable-multiselect` — form-field wrapper (label/description/error) around this control
322
+ - `@egose/shadcn-theme-ng/select` — single-select dropdown counterpart
323
+ - `@egose/shadcn-theme-ng/popover` — the underlying popover primitive
324
+ - `@egose/shadcn-theme-ng/checkbox` — the underlying option-row primitive