@egose/shadcn-theme-ng 0.0.28 → 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 (237) hide show
  1. package/README.md +130 -13
  2. package/accordion/README.md +376 -34
  3. package/accordion/fesm2022/accordion.mjs +27 -39
  4. package/alert/README.md +343 -34
  5. package/alert/fesm2022/alert.mjs +47 -47
  6. package/alert-dialog/README.md +471 -5
  7. package/alert-dialog/fesm2022/alert-dialog.mjs +47 -47
  8. package/aspect-ratio/README.md +272 -5
  9. package/aspect-ratio/fesm2022/aspect-ratio.mjs +9 -9
  10. package/autocomplete/README.md +473 -34
  11. package/autocomplete/fesm2022/autocomplete.mjs +53 -71
  12. package/avatar/README.md +357 -5
  13. package/avatar/fesm2022/avatar.mjs +32 -32
  14. package/badge/README.md +289 -34
  15. package/badge/fesm2022/badge.mjs +57 -57
  16. package/basic-alert/README.md +324 -34
  17. package/basic-alert/fesm2022/basic-alert.mjs +6 -18
  18. package/breadcrumb/README.md +406 -5
  19. package/breadcrumb/fesm2022/breadcrumb.mjs +33 -39
  20. package/button/README.md +453 -34
  21. package/button/fesm2022/button.mjs +147 -219
  22. package/button/types/button.d.ts +7 -10
  23. package/button-group/README.md +318 -5
  24. package/button-group/fesm2022/button-group.mjs +20 -20
  25. package/calendar/README.md +328 -34
  26. package/calendar/fesm2022/calendar.mjs +29 -743
  27. package/card/README.md +331 -5
  28. package/card/fesm2022/card.mjs +33 -33
  29. package/carousel/README.md +333 -5
  30. package/carousel/fesm2022/carousel.mjs +39 -78
  31. package/checkbox/README.md +291 -34
  32. package/checkbox/fesm2022/checkbox.mjs +20 -58
  33. package/checkbox/types/checkbox.d.ts +5 -4
  34. package/collapsible/README.md +332 -5
  35. package/collapsible/fesm2022/collapsible.mjs +15 -15
  36. package/combobox/README.md +507 -5
  37. package/combobox/fesm2022/combobox.mjs +91 -224
  38. package/command/README.md +435 -5
  39. package/command/fesm2022/command.mjs +49 -109
  40. package/confirmation-dialog/README.md +272 -34
  41. package/confirmation-dialog/fesm2022/confirmation-dialog.mjs +15 -40
  42. package/confirmation-dialog/types/confirmation-dialog.d.ts +8 -4
  43. package/context-menu/README.md +366 -5
  44. package/context-menu/fesm2022/context-menu.mjs +9 -9
  45. package/date-picker/README.md +436 -34
  46. package/date-picker/fesm2022/date-picker.mjs +64 -267
  47. package/date-picker/types/date-picker.d.ts +7 -3
  48. package/dialog/README.md +419 -34
  49. package/dialog/fesm2022/dialog.mjs +45 -71
  50. package/drawer/README.md +395 -5
  51. package/drawer/fesm2022/drawer.mjs +44 -54
  52. package/dropdown-menu/README.md +417 -5
  53. package/dropdown-menu/fesm2022/dropdown-menu.mjs +82 -70
  54. package/dropdown-menu/types/dropdown-menu.d.ts +4 -0
  55. package/empty/README.md +329 -5
  56. package/empty/fesm2022/empty.mjs +30 -30
  57. package/field/README.md +385 -5
  58. package/field/fesm2022/field.mjs +56 -72
  59. package/form-checkbox/README.md +283 -34
  60. package/form-checkbox/fesm2022/form-checkbox.mjs +25 -85
  61. package/form-checkbox/types/form-checkbox.d.ts +7 -1
  62. package/form-date-picker/README.md +293 -34
  63. package/form-date-picker/fesm2022/form-date-picker.mjs +31 -84
  64. package/form-date-picker/types/form-date-picker.d.ts +7 -1
  65. package/form-field/README.md +327 -34
  66. package/form-field/fesm2022/form-field.mjs +33 -19
  67. package/form-field/types/form-field.d.ts +28 -20
  68. package/form-field-simple/README.md +311 -34
  69. package/form-field-simple/fesm2022/form-field-simple.mjs +4 -4
  70. package/form-searchable-multiselect/README.md +332 -34
  71. package/form-searchable-multiselect/fesm2022/form-searchable-multiselect.mjs +31 -91
  72. package/form-searchable-multiselect/types/form-searchable-multiselect.d.ts +7 -1
  73. package/form-select/README.md +321 -34
  74. package/form-select/fesm2022/form-select.mjs +32 -149
  75. package/form-select/types/form-select.d.ts +7 -1
  76. package/form-text-input/README.md +342 -34
  77. package/form-text-input/fesm2022/form-text-input.mjs +35 -149
  78. package/form-text-input/types/form-text-input.d.ts +9 -12
  79. package/form-textarea/README.md +318 -34
  80. package/form-textarea/fesm2022/form-textarea.mjs +34 -109
  81. package/form-textarea/types/form-textarea.d.ts +8 -1
  82. package/hover-card/README.md +256 -5
  83. package/hover-card/fesm2022/hover-card.mjs +19 -19
  84. package/icon/README.md +210 -34
  85. package/icon/fesm2022/icon.mjs +8 -8
  86. package/input/README.md +240 -34
  87. package/input/fesm2022/input.mjs +17 -12
  88. package/input/types/input.d.ts +2 -1
  89. package/input-group/README.md +335 -5
  90. package/input-group/fesm2022/input-group.mjs +34 -34
  91. package/input-otp/README.md +375 -5
  92. package/input-otp/fesm2022/input-otp.mjs +26 -26
  93. package/item/README.md +385 -5
  94. package/item/fesm2022/item.mjs +53 -53
  95. package/kbd/README.md +291 -5
  96. package/kbd/fesm2022/kbd.mjs +12 -12
  97. package/label/README.md +243 -34
  98. package/label/fesm2022/label.mjs +10 -10
  99. package/label/types/label.d.ts +1 -1
  100. package/layout-simple/README.md +174 -44
  101. package/layout-simple/fesm2022/layout-simple.mjs +508 -603
  102. package/layout-simple/types/layout-simple.d.ts +179 -128
  103. package/menu/README.md +418 -0
  104. package/menu/fesm2022/menu.mjs +455 -0
  105. package/menu/package.json +24 -0
  106. package/menu/types/menu.d.ts +137 -0
  107. package/menubar/README.md +343 -5
  108. package/menubar/fesm2022/menubar.mjs +13 -13
  109. package/native-select/README.md +323 -5
  110. package/native-select/fesm2022/native-select.mjs +19 -19
  111. package/navigation-menu/README.md +369 -5
  112. package/navigation-menu/fesm2022/navigation-menu.mjs +35 -47
  113. package/package.json +183 -154
  114. package/pagination/README.md +388 -5
  115. package/pagination/fesm2022/pagination.mjs +45 -331
  116. package/popover/README.md +302 -34
  117. package/popover/fesm2022/popover.mjs +30 -30
  118. package/progress/README.md +311 -5
  119. package/progress/fesm2022/progress.mjs +13 -13
  120. package/radio-group/README.md +335 -34
  121. package/radio-group/fesm2022/radio-group.mjs +26 -19
  122. package/radio-group/types/radio-group.d.ts +1 -1
  123. package/resizable/README.md +269 -5
  124. package/resizable/fesm2022/resizable.mjs +18 -26
  125. package/scroll-area/README.md +233 -5
  126. package/scroll-area/fesm2022/scroll-area.mjs +9 -9
  127. package/searchable-multiselect/README.md +294 -34
  128. package/searchable-multiselect/fesm2022/searchable-multiselect.mjs +28 -117
  129. package/searchable-multiselect/types/searchable-multiselect.d.ts +8 -2
  130. package/select/README.md +408 -34
  131. package/select/fesm2022/select.mjs +71 -111
  132. package/select/types/select.d.ts +3 -1
  133. package/separator/README.md +193 -34
  134. package/separator/fesm2022/separator.mjs +9 -9
  135. package/separator/types/separator.d.ts +1 -1
  136. package/sheet/README.md +282 -34
  137. package/sheet/fesm2022/sheet.mjs +41 -53
  138. package/sidebar/README.md +457 -5
  139. package/sidebar/fesm2022/sidebar.mjs +125 -227
  140. package/skeleton/README.md +217 -5
  141. package/skeleton/fesm2022/skeleton.mjs +9 -9
  142. package/slider/README.md +273 -5
  143. package/slider/fesm2022/slider.mjs +10 -80
  144. package/sonner/README.md +317 -34
  145. package/sonner/fesm2022/sonner.mjs +13 -13
  146. package/spinner/README.md +255 -34
  147. package/spinner/fesm2022/spinner.mjs +11 -11
  148. package/switch/README.md +281 -34
  149. package/switch/fesm2022/switch.mjs +13 -15
  150. package/table/README.md +423 -5
  151. package/table/fesm2022/table.mjs +41 -41
  152. package/tabs/README.md +382 -34
  153. package/tabs/fesm2022/tabs.mjs +35 -111
  154. package/textarea/README.md +282 -5
  155. package/textarea/fesm2022/textarea.mjs +9 -9
  156. package/toggle/README.md +270 -5
  157. package/toggle/fesm2022/toggle.mjs +13 -13
  158. package/toggle-group/README.md +340 -5
  159. package/toggle-group/fesm2022/toggle-group.mjs +13 -13
  160. package/toggle-group/types/toggle-group.d.ts +13 -7
  161. package/tooltip/README.md +240 -34
  162. package/tooltip/fesm2022/tooltip.mjs +14 -14
  163. package/typography/README.md +271 -5
  164. package/typography/fesm2022/typography.mjs +53 -53
  165. package/typography/types/typography.d.ts +19 -19
  166. package/utils/README.md +274 -34
  167. package/utils/fesm2022/utils.mjs +68 -44
  168. package/accordion/fesm2022/accordion.mjs.map +0 -1
  169. package/alert/fesm2022/alert.mjs.map +0 -1
  170. package/alert-dialog/fesm2022/alert-dialog.mjs.map +0 -1
  171. package/aspect-ratio/fesm2022/aspect-ratio.mjs.map +0 -1
  172. package/autocomplete/fesm2022/autocomplete.mjs.map +0 -1
  173. package/avatar/fesm2022/avatar.mjs.map +0 -1
  174. package/badge/fesm2022/badge.mjs.map +0 -1
  175. package/basic-alert/fesm2022/basic-alert.mjs.map +0 -1
  176. package/breadcrumb/fesm2022/breadcrumb.mjs.map +0 -1
  177. package/button/fesm2022/button.mjs.map +0 -1
  178. package/button-group/fesm2022/button-group.mjs.map +0 -1
  179. package/calendar/fesm2022/calendar.mjs.map +0 -1
  180. package/card/fesm2022/card.mjs.map +0 -1
  181. package/carousel/fesm2022/carousel.mjs.map +0 -1
  182. package/checkbox/fesm2022/checkbox.mjs.map +0 -1
  183. package/collapsible/fesm2022/collapsible.mjs.map +0 -1
  184. package/combobox/fesm2022/combobox.mjs.map +0 -1
  185. package/command/fesm2022/command.mjs.map +0 -1
  186. package/confirmation-dialog/fesm2022/confirmation-dialog.mjs.map +0 -1
  187. package/context-menu/fesm2022/context-menu.mjs.map +0 -1
  188. package/date-picker/fesm2022/date-picker.mjs.map +0 -1
  189. package/dialog/fesm2022/dialog.mjs.map +0 -1
  190. package/drawer/fesm2022/drawer.mjs.map +0 -1
  191. package/dropdown-menu/fesm2022/dropdown-menu.mjs.map +0 -1
  192. package/empty/fesm2022/empty.mjs.map +0 -1
  193. package/field/fesm2022/field.mjs.map +0 -1
  194. package/form-checkbox/fesm2022/form-checkbox.mjs.map +0 -1
  195. package/form-date-picker/fesm2022/form-date-picker.mjs.map +0 -1
  196. package/form-field/fesm2022/form-field.mjs.map +0 -1
  197. package/form-field-simple/fesm2022/form-field-simple.mjs.map +0 -1
  198. package/form-searchable-multiselect/fesm2022/form-searchable-multiselect.mjs.map +0 -1
  199. package/form-select/fesm2022/form-select.mjs.map +0 -1
  200. package/form-text-input/fesm2022/form-text-input.mjs.map +0 -1
  201. package/form-textarea/fesm2022/form-textarea.mjs.map +0 -1
  202. package/hover-card/fesm2022/hover-card.mjs.map +0 -1
  203. package/icon/fesm2022/icon.mjs.map +0 -1
  204. package/input/fesm2022/input.mjs.map +0 -1
  205. package/input-group/fesm2022/input-group.mjs.map +0 -1
  206. package/input-otp/fesm2022/input-otp.mjs.map +0 -1
  207. package/item/fesm2022/item.mjs.map +0 -1
  208. package/kbd/fesm2022/kbd.mjs.map +0 -1
  209. package/label/fesm2022/label.mjs.map +0 -1
  210. package/layout-simple/fesm2022/layout-simple.mjs.map +0 -1
  211. package/menubar/fesm2022/menubar.mjs.map +0 -1
  212. package/native-select/fesm2022/native-select.mjs.map +0 -1
  213. package/navigation-menu/fesm2022/navigation-menu.mjs.map +0 -1
  214. package/pagination/fesm2022/pagination.mjs.map +0 -1
  215. package/popover/fesm2022/popover.mjs.map +0 -1
  216. package/progress/fesm2022/progress.mjs.map +0 -1
  217. package/radio-group/fesm2022/radio-group.mjs.map +0 -1
  218. package/resizable/fesm2022/resizable.mjs.map +0 -1
  219. package/scroll-area/fesm2022/scroll-area.mjs.map +0 -1
  220. package/searchable-multiselect/fesm2022/searchable-multiselect.mjs.map +0 -1
  221. package/select/fesm2022/select.mjs.map +0 -1
  222. package/separator/fesm2022/separator.mjs.map +0 -1
  223. package/sheet/fesm2022/sheet.mjs.map +0 -1
  224. package/sidebar/fesm2022/sidebar.mjs.map +0 -1
  225. package/skeleton/fesm2022/skeleton.mjs.map +0 -1
  226. package/slider/fesm2022/slider.mjs.map +0 -1
  227. package/sonner/fesm2022/sonner.mjs.map +0 -1
  228. package/spinner/fesm2022/spinner.mjs.map +0 -1
  229. package/switch/fesm2022/switch.mjs.map +0 -1
  230. package/table/fesm2022/table.mjs.map +0 -1
  231. package/tabs/fesm2022/tabs.mjs.map +0 -1
  232. package/textarea/fesm2022/textarea.mjs.map +0 -1
  233. package/toggle/fesm2022/toggle.mjs.map +0 -1
  234. package/toggle-group/fesm2022/toggle-group.mjs.map +0 -1
  235. package/tooltip/fesm2022/tooltip.mjs.map +0 -1
  236. package/typography/fesm2022/typography.mjs.map +0 -1
  237. package/utils/fesm2022/utils.mjs.map +0 -1
@@ -1,64 +1,324 @@
1
- # MultiselectAutocomplete
1
+ # Searchable Multiselect (`@egose/shadcn-theme-ng/searchable-multiselect`)
2
2
 
3
- This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.3.0.
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
4
 
5
- ## Code scaffolding
5
+ It is a **standalone `ControlValueAccessor` component** (`EgSearchableMultiselect`), so it binds directly to Angular reactive forms (`formControlName`) and template-driven forms (`ngModel`) with a `string[]` value. Internally it reuses `HlmPopover`/`HlmCheckbox`/`HlmButton` — you do not import those yourself.
6
6
 
7
- Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
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
8
11
 
9
12
  ```bash
10
- ng generate component component-name
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';
11
24
  ```
12
25
 
13
- For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
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.)
14
27
 
15
- ```bash
16
- ng generate --help
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" />
17
56
  ```
18
57
 
19
- ## Building
58
+ Renders (internally — you do not write this yourself):
20
59
 
21
- To build the library, run:
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>
22
66
 
23
- ```bash
24
- ng build searchable-multiselect
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>
25
75
  ```
26
76
 
27
- This command will compile your project, and the build artifacts will be placed in the `dist/` directory.
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.
28
78
 
29
- ### Publishing the Library
79
+ ## API reference
30
80
 
31
- Once the project is built, you can publish your library by following these steps:
81
+ ### EgSearchableMultiselect (component, `ControlValueAccessor`)
32
82
 
33
- 1. Navigate to the `dist` directory:
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 |
34
94
 
35
- ```bash
36
- cd dist/searchable-multiselect
37
- ```
95
+ | Output | Type | Description |
96
+ | ------------- | ---------- | ---------------------------------------------------- |
97
+ | `valueChange` | `string[]` | Emitted with the new value array on every add/remove |
38
98
 
39
- 2. Run the `npm publish` command to publish your library to the npm registry:
40
- ```bash
41
- npm publish
42
- ```
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`).
43
100
 
44
- ## Running unit tests
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.
45
102
 
46
- To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
103
+ ## Examples
47
104
 
48
- ```bash
49
- ng test
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
+ }
50
129
  ```
51
130
 
52
- ## Running end-to-end tests
131
+ ### 2. Reactive forms
53
132
 
54
- For end-to-end (e2e) testing, run:
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';
55
137
 
56
- ```bash
57
- ng e2e
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
+ }
58
168
  ```
59
169
 
60
- Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
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.
61
318
 
62
- ## Additional Resources
319
+ ## Related subpaths
63
320
 
64
- For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
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
@@ -1,7 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { input, output, signal, computed, forwardRef, ChangeDetectionStrategy, Component } from '@angular/core';
3
3
  import { NG_VALUE_ACCESSOR } from '@angular/forms';
4
- import { HlmPopover, HlmPopoverTrigger, HlmPopoverContent } from '@egose/shadcn-theme-ng/popover';
4
+ import { HlmPopover, HlmPopoverTrigger, HlmPopoverContent, HlmPopoverPortal } from '@egose/shadcn-theme-ng/popover';
5
5
  import { HlmButton } from '@egose/shadcn-theme-ng/button';
6
6
  import { HlmCheckbox } from '@egose/shadcn-theme-ng/checkbox';
7
7
  import { hlm } from '@egose/shadcn-theme-ng/utils';
@@ -12,6 +12,16 @@ class EgSearchableMultiselect {
12
12
  ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
13
13
  placeholder = input('Start typing to add…', /* @ts-ignore */
14
14
  ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
15
+ id = input('', /* @ts-ignore */
16
+ ...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
17
+ disabled = input(false, /* @ts-ignore */
18
+ ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
19
+ wrapperDisabled = input(false, /* @ts-ignore */
20
+ ...(ngDevMode ? [{ debugName: "wrapperDisabled" }] : /* istanbul ignore next */ []));
21
+ ariaLabel = input(undefined, /* @ts-ignore */
22
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
23
+ ariaDescribedby = input(null, /* @ts-ignore */
24
+ ...(ngDevMode ? [{ debugName: "ariaDescribedby" }] : /* istanbul ignore next */ []));
15
25
  userClass = input('', { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: 'class' });
16
26
  /** Value: array of selected values (string[]) */
17
27
  value = input([], /* @ts-ignore */
@@ -19,7 +29,10 @@ class EgSearchableMultiselect {
19
29
  valueChange = output();
20
30
  selectedItems = signal([], /* @ts-ignore */
21
31
  ...(ngDevMode ? [{ debugName: "selectedItems" }] : /* istanbul ignore next */ []));
22
- isDisabled = false;
32
+ formDisabled = signal(false, /* @ts-ignore */
33
+ ...(ngDevMode ? [{ debugName: "formDisabled" }] : /* istanbul ignore next */ []));
34
+ disabledState = computed(() => this.disabled() || this.wrapperDisabled() || this.formDisabled(), /* @ts-ignore */
35
+ ...(ngDevMode ? [{ debugName: "disabledState" }] : /* istanbul ignore next */ []));
23
36
  isFormBound = false;
24
37
  _hostClass = computed(() => hlm(this.userClass()), /* @ts-ignore */
25
38
  ...(ngDevMode ? [{ debugName: "_hostClass" }] : /* istanbul ignore next */ []));
@@ -27,6 +40,8 @@ class EgSearchableMultiselect {
27
40
  return this.selectedItems().some((s) => s.value === value);
28
41
  }
29
42
  toggle(value, checked) {
43
+ if (this.disabledState())
44
+ return;
30
45
  if (checked) {
31
46
  this.addItem(value);
32
47
  }
@@ -44,6 +59,8 @@ class EgSearchableMultiselect {
44
59
  }
45
60
  }
46
61
  removeItem(value) {
62
+ if (this.disabledState())
63
+ return;
47
64
  const updated = this.selectedItems().filter((i) => i.value !== value);
48
65
  this.updateValueFromSelected(updated);
49
66
  }
@@ -75,76 +92,23 @@ class EgSearchableMultiselect {
75
92
  this.onTouched = fn;
76
93
  }
77
94
  setDisabledState(isDisabled) {
78
- this.isDisabled = isDisabled;
95
+ this.formDisabled.set(isDisabled);
79
96
  }
80
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: EgSearchableMultiselect, deps: [], target: i0.ɵɵFactoryTarget.Component });
81
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: EgSearchableMultiselect, isStandalone: true, selector: "eg-searchable-multiselect", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, host: { properties: { "class": "_hostClass()" } }, providers: [
97
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: EgSearchableMultiselect, deps: [], target: i0.ɵɵFactoryTarget.Component });
98
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.3", type: EgSearchableMultiselect, isStandalone: true, selector: "eg-searchable-multiselect", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, wrapperDisabled: { classPropertyName: "wrapperDisabled", publicName: "wrapperDisabled", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedby: { classPropertyName: "ariaDescribedby", publicName: "ariaDescribedby", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { valueChange: "valueChange" }, host: { properties: { "class": "_hostClass()" } }, providers: [
82
99
  {
83
100
  provide: NG_VALUE_ACCESSOR,
84
101
  useExisting: forwardRef(() => EgSearchableMultiselect),
85
102
  multi: true,
86
103
  },
87
- ], ngImport: i0, template: `
88
- <div class="flex flex-col gap-2 w-full">
89
- <!-- Selected chips -->
90
- <div class="flex flex-wrap gap-2">
91
- @if (selectedItems().length === 0) {
92
- <span
93
- class="bg-gray-100 text-gray-400 rounded-full
94
- px-2 py-1 inline-flex items-center text-xs"
95
- >
96
- {{ placeholder() }}
97
- </span>
98
- } @else {
99
- @for (item of selectedItems(); track item.value) {
100
- <span
101
- class="bg-gray-200 rounded-full px-2 py-1
102
- inline-flex items-center gap-2 text-xs"
103
- >
104
- {{ item.label }}
105
- <button
106
- type="button"
107
- (click)="removeItem(item.value)"
108
- class="bg-transparent border-0 text-gray-600
109
- hover:text-red-500 cursor-pointer"
110
- >
111
- ✕
112
- </button>
113
- </span>
114
- }
115
- }
116
- </div>
117
-
118
- <!-- Trigger -->
119
- <hlm-popover>
120
- <button hlmPopoverTrigger hlmButton variant="secondary" appearance="outline" type="button">
121
- {{ selectedItems().length }} selected
122
- </button>
123
-
124
- <hlm-popover-content class="w-64 p-2">
125
- <div class="max-h-60 overflow-auto flex flex-col gap-1">
126
- @for (option of options(); track option.value) {
127
- <label
128
- class="flex items-center gap-2 cursor-pointer px-2 py-1 rounded-sm hover:bg-secondary"
129
- >
130
- <hlm-checkbox [checked]="isSelected(option.value)" (changed)="toggle(option.value, $event)" />
131
- <span class="text-sm">{{ option.label }}</span>
132
- </label>
133
- } @empty {
134
- <span class="text-xs text-muted-foreground px-2 py-1">No options</span>
135
- }
136
- </div>
137
- </hlm-popover-content>
138
- </hlm-popover>
139
- </div>
140
- `, isInline: true, dependencies: [{ kind: "directive", type: HlmPopover, selector: "[hlmPopover],hlm-popover" }, { kind: "directive", type: HlmPopoverTrigger, selector: "button[hlmPopoverTrigger],button[hlmPopoverTriggerFor]" }, { kind: "directive", type: HlmPopoverContent, selector: "[hlmPopoverContent],hlm-popover-content" }, { kind: "component", type: HlmButton, selector: "button[hlmButton], a[hlmButton]", inputs: ["variant", "size", "appearance", "loading", "icon", "iconPosition", "class", "spinnerUserClass", "disabled", "type"] }, { kind: "component", type: HlmCheckbox, selector: "hlm-checkbox", inputs: ["class", "id", "aria-label", "aria-labelledby", "aria-describedby", "checked", "name", "required", "disabled"], outputs: ["checkedChange", "changed"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
104
+ ], ngImport: i0, template: "\n <div class=\"flex flex-col gap-2 w-full\">\n <!-- Selected chips -->\n <div class=\"flex flex-wrap gap-2\">\n @if (selectedItems().length === 0) {\n <span\n class=\"bg-gray-100 text-gray-400 rounded-full\n px-2 py-1 inline-flex items-center text-xs\"\n >\n {{ placeholder() }}\n </span>\n } @else {\n @for (item of selectedItems(); track item.value) {\n <span\n class=\"bg-gray-200 rounded-full px-2 py-1\n inline-flex items-center gap-2 text-xs\"\n >\n {{ item.label }}\n <button\n type=\"button\"\n [disabled]=\"disabledState()\"\n (click)=\"removeItem(item.value)\"\n class=\"bg-transparent border-0 text-gray-600\n hover:text-red-500 cursor-pointer\"\n >\n ✕\n </button>\n </span>\n }\n }\n </div>\n\n <!-- Trigger -->\n <hlm-popover>\n <button\n hlmPopoverTrigger\n hlmButton\n variant=\"secondary\"\n appearance=\"outline\"\n type=\"button\"\n [id]=\"id()\"\n [disabled]=\"disabledState()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-describedby]=\"ariaDescribedby()\"\n >\n {{ selectedItems().length }} selected\n </button>\n\n <hlm-popover-content class=\"w-64 p-2\" *hlmPopoverPortal=\"let ctx\">\n <div class=\"max-h-60 overflow-auto flex flex-col gap-1\">\n @for (option of options(); track option.value) {\n <label\n class=\"flex items-center gap-2 cursor-pointer px-2 py-1 rounded-sm hover:bg-secondary\"\n >\n <hlm-checkbox\n [checked]=\"isSelected(option.value)\"\n [disabled]=\"disabledState()\"\n (changed)=\"toggle(option.value, $event)\"\n />\n <span class=\"text-sm\">{{ option.label }}</span>\n </label>\n } @empty {\n <span class=\"text-xs text-muted-foreground px-2 py-1\">No options</span>\n }\n </div>\n </hlm-popover-content>\n </hlm-popover>\n </div>\n ", isInline: true, dependencies: [{ kind: "directive", type: HlmPopover, selector: "[hlmPopover],hlm-popover" }, { kind: "directive", type: HlmPopoverTrigger, selector: "button[hlmPopoverTrigger],button[hlmPopoverTriggerFor]" }, { kind: "directive", type: HlmPopoverContent, selector: "[hlmPopoverContent],hlm-popover-content" }, { kind: "directive", type: HlmPopoverPortal, selector: "[hlmPopoverPortal]" }, { kind: "component", type: HlmButton, selector: "button[hlmButton], a[hlmButton]", inputs: ["variant", "size", "appearance", "loading", "icon", "iconPosition", "class", "spinnerUserClass", "disabled", "type"] }, { kind: "component", type: HlmCheckbox, selector: "hlm-checkbox", inputs: ["class", "id", "aria-label", "aria-labelledby", "aria-describedby", "checked", "name", "required", "disabled", "wrapperDisabled"], outputs: ["checkedChange", "changed"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
141
105
  }
142
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: EgSearchableMultiselect, decorators: [{
106
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: EgSearchableMultiselect, decorators: [{
143
107
  type: Component,
144
108
  args: [{
145
109
  selector: 'eg-searchable-multiselect',
146
110
  standalone: true,
147
- imports: [HlmPopover, HlmPopoverTrigger, HlmPopoverContent, HlmButton, HlmCheckbox],
111
+ imports: [HlmPopover, HlmPopoverTrigger, HlmPopoverContent, HlmPopoverPortal, HlmButton, HlmCheckbox],
148
112
  changeDetection: ChangeDetectionStrategy.OnPush,
149
113
  providers: [
150
114
  {
@@ -156,62 +120,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
156
120
  host: {
157
121
  '[class]': '_hostClass()',
158
122
  },
159
- template: `
160
- <div class="flex flex-col gap-2 w-full">
161
- <!-- Selected chips -->
162
- <div class="flex flex-wrap gap-2">
163
- @if (selectedItems().length === 0) {
164
- <span
165
- class="bg-gray-100 text-gray-400 rounded-full
166
- px-2 py-1 inline-flex items-center text-xs"
167
- >
168
- {{ placeholder() }}
169
- </span>
170
- } @else {
171
- @for (item of selectedItems(); track item.value) {
172
- <span
173
- class="bg-gray-200 rounded-full px-2 py-1
174
- inline-flex items-center gap-2 text-xs"
175
- >
176
- {{ item.label }}
177
- <button
178
- type="button"
179
- (click)="removeItem(item.value)"
180
- class="bg-transparent border-0 text-gray-600
181
- hover:text-red-500 cursor-pointer"
182
- >
183
- ✕
184
- </button>
185
- </span>
186
- }
187
- }
188
- </div>
189
-
190
- <!-- Trigger -->
191
- <hlm-popover>
192
- <button hlmPopoverTrigger hlmButton variant="secondary" appearance="outline" type="button">
193
- {{ selectedItems().length }} selected
194
- </button>
195
-
196
- <hlm-popover-content class="w-64 p-2">
197
- <div class="max-h-60 overflow-auto flex flex-col gap-1">
198
- @for (option of options(); track option.value) {
199
- <label
200
- class="flex items-center gap-2 cursor-pointer px-2 py-1 rounded-sm hover:bg-secondary"
201
- >
202
- <hlm-checkbox [checked]="isSelected(option.value)" (changed)="toggle(option.value, $event)" />
203
- <span class="text-sm">{{ option.label }}</span>
204
- </label>
205
- } @empty {
206
- <span class="text-xs text-muted-foreground px-2 py-1">No options</span>
207
- }
208
- </div>
209
- </hlm-popover-content>
210
- </hlm-popover>
211
- </div>
212
- `,
123
+ template: "\n <div class=\"flex flex-col gap-2 w-full\">\n <!-- Selected chips -->\n <div class=\"flex flex-wrap gap-2\">\n @if (selectedItems().length === 0) {\n <span\n class=\"bg-gray-100 text-gray-400 rounded-full\n px-2 py-1 inline-flex items-center text-xs\"\n >\n {{ placeholder() }}\n </span>\n } @else {\n @for (item of selectedItems(); track item.value) {\n <span\n class=\"bg-gray-200 rounded-full px-2 py-1\n inline-flex items-center gap-2 text-xs\"\n >\n {{ item.label }}\n <button\n type=\"button\"\n [disabled]=\"disabledState()\"\n (click)=\"removeItem(item.value)\"\n class=\"bg-transparent border-0 text-gray-600\n hover:text-red-500 cursor-pointer\"\n >\n ✕\n </button>\n </span>\n }\n }\n </div>\n\n <!-- Trigger -->\n <hlm-popover>\n <button\n hlmPopoverTrigger\n hlmButton\n variant=\"secondary\"\n appearance=\"outline\"\n type=\"button\"\n [id]=\"id()\"\n [disabled]=\"disabledState()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-describedby]=\"ariaDescribedby()\"\n >\n {{ selectedItems().length }} selected\n </button>\n\n <hlm-popover-content class=\"w-64 p-2\" *hlmPopoverPortal=\"let ctx\">\n <div class=\"max-h-60 overflow-auto flex flex-col gap-1\">\n @for (option of options(); track option.value) {\n <label\n class=\"flex items-center gap-2 cursor-pointer px-2 py-1 rounded-sm hover:bg-secondary\"\n >\n <hlm-checkbox\n [checked]=\"isSelected(option.value)\"\n [disabled]=\"disabledState()\"\n (changed)=\"toggle(option.value, $event)\"\n />\n <span class=\"text-sm\">{{ option.label }}</span>\n </label>\n } @empty {\n <span class=\"text-xs text-muted-foreground px-2 py-1\">No options</span>\n }\n </div>\n </hlm-popover-content>\n </hlm-popover>\n </div>\n ",
213
124
  }]
214
- }], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }] } });
125
+ }], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], wrapperDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "wrapperDisabled", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaDescribedby: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaDescribedby", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }] } });
215
126
 
216
127
  /*
217
128
  * Public API Surface of searchable-multiselect
@@ -222,4 +133,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
222
133
  */
223
134
 
224
135
  export { EgSearchableMultiselect };
225
- //# sourceMappingURL=searchable-multiselect.mjs.map
136
+
@@ -10,12 +10,18 @@ declare class EgSearchableMultiselect implements ControlValueAccessor {
10
10
  /** Full option list with labels/values */
11
11
  options: _angular_core.InputSignal<SelectOption[]>;
12
12
  placeholder: _angular_core.InputSignal<string>;
13
+ id: _angular_core.InputSignal<string>;
14
+ disabled: _angular_core.InputSignal<boolean>;
15
+ wrapperDisabled: _angular_core.InputSignal<boolean>;
16
+ ariaLabel: _angular_core.InputSignal<string | undefined>;
17
+ ariaDescribedby: _angular_core.InputSignal<string | null>;
13
18
  userClass: _angular_core.InputSignal<ClassValue>;
14
19
  /** Value: array of selected values (string[]) */
15
20
  value: _angular_core.InputSignal<string[]>;
16
21
  valueChange: _angular_core.OutputEmitterRef<string[]>;
17
22
  protected readonly selectedItems: _angular_core.WritableSignal<SelectOption[]>;
18
- protected isDisabled: boolean;
23
+ protected readonly formDisabled: _angular_core.WritableSignal<boolean>;
24
+ protected readonly disabledState: _angular_core.Signal<boolean>;
19
25
  private isFormBound;
20
26
  protected readonly _hostClass: _angular_core.Signal<string>;
21
27
  protected isSelected(value: string): boolean;
@@ -32,7 +38,7 @@ declare class EgSearchableMultiselect implements ControlValueAccessor {
32
38
  registerOnTouched(fn: () => void): void;
33
39
  setDisabledState(isDisabled: boolean): void;
34
40
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<EgSearchableMultiselect, never>;
35
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<EgSearchableMultiselect, "eg-searchable-multiselect", never, { "options": { "alias": "options"; "required": false; "isSignal": true; }; "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "userClass": { "alias": "class"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; }, { "valueChange": "valueChange"; }, never, never, true, never>;
41
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<EgSearchableMultiselect, "eg-searchable-multiselect", never, { "options": { "alias": "options"; "required": false; "isSignal": true; }; "placeholder": { "alias": "placeholder"; "required": false; "isSignal": true; }; "id": { "alias": "id"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "wrapperDisabled": { "alias": "wrapperDisabled"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "ariaDescribedby": { "alias": "ariaDescribedby"; "required": false; "isSignal": true; }; "userClass": { "alias": "class"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; }, { "valueChange": "valueChange"; }, never, never, true, never>;
36
42
  }
37
43
 
38
44
  export { EgSearchableMultiselect };