@egose/shadcn-theme-ng-tw 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.
- package/README.md +1 -1
- package/accordion/README.md +405 -2
- package/alert/README.md +372 -2
- package/alert-dialog/README.md +471 -5
- package/aspect-ratio/README.md +272 -5
- package/autocomplete/README.md +502 -2
- package/avatar/README.md +357 -5
- package/badge/README.md +318 -2
- package/basic-alert/README.md +353 -2
- package/breadcrumb/README.md +406 -5
- package/button/README.md +482 -2
- package/button/fesm2022/button.mjs +85 -107
- package/button/types/button.d.ts +5 -8
- package/button-group/README.md +318 -5
- package/button-group/fesm2022/button-group.mjs +1 -1
- package/calendar/README.md +357 -2
- package/card/README.md +331 -5
- package/carousel/README.md +333 -5
- package/carousel/fesm2022/carousel.mjs +4 -1
- package/checkbox/README.md +320 -2
- package/collapsible/README.md +332 -5
- package/combobox/README.md +507 -5
- package/combobox/fesm2022/combobox.mjs +4 -1
- package/command/README.md +435 -5
- package/confirmation-dialog/README.md +301 -2
- package/context-menu/README.md +366 -5
- package/date-picker/README.md +465 -2
- package/date-picker/fesm2022/date-picker.mjs +2 -2
- package/dialog/README.md +448 -2
- package/drawer/README.md +395 -5
- package/dropdown-menu/README.md +417 -5
- package/empty/README.md +329 -5
- package/field/README.md +385 -5
- package/form-checkbox/README.md +312 -2
- package/form-date-picker/README.md +322 -2
- package/form-field/README.md +356 -2
- package/form-field-simple/README.md +340 -2
- package/form-searchable-multiselect/README.md +361 -2
- package/form-select/README.md +350 -2
- package/form-text-input/README.md +371 -2
- package/form-textarea/README.md +347 -2
- package/hover-card/README.md +256 -5
- package/icon/README.md +239 -2
- package/input/README.md +269 -2
- package/input-group/README.md +335 -5
- package/input-group/fesm2022/input-group.mjs +3 -3
- package/input-otp/README.md +375 -5
- package/item/README.md +385 -5
- package/item/fesm2022/item.mjs +3 -3
- package/kbd/README.md +291 -5
- package/label/README.md +272 -2
- package/layout-simple/README.md +193 -2
- package/layout-simple/fesm2022/layout-simple.mjs +877 -409
- package/layout-simple/types/layout-simple.d.ts +174 -137
- package/menu/README.md +417 -2
- package/menubar/README.md +343 -5
- package/native-select/README.md +323 -5
- package/navigation-menu/README.md +369 -5
- package/package.json +1 -1
- package/pagination/README.md +388 -5
- package/popover/README.md +331 -2
- package/progress/README.md +311 -5
- package/radio-group/README.md +364 -2
- package/radio-group/fesm2022/radio-group.mjs +5 -1
- package/resizable/README.md +269 -5
- package/scroll-area/README.md +233 -5
- package/searchable-multiselect/README.md +323 -2
- package/select/README.md +437 -2
- package/separator/README.md +222 -2
- package/sheet/README.md +311 -2
- package/sidebar/README.md +457 -5
- package/skeleton/README.md +217 -5
- package/slider/README.md +273 -5
- package/slider/fesm2022/slider.mjs +17 -13
- package/sonner/README.md +346 -2
- package/spinner/README.md +284 -2
- package/switch/README.md +310 -2
- package/table/README.md +423 -5
- package/tabs/README.md +411 -2
- package/tabs/fesm2022/tabs.mjs +12 -2
- package/textarea/README.md +282 -5
- package/toggle/README.md +270 -5
- package/toggle-group/README.md +340 -5
- package/tooltip/README.md +269 -2
- package/typography/README.md +271 -5
- package/utils/README.md +303 -2
package/scroll-area/README.md
CHANGED
|
@@ -1,11 +1,239 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Scroll Area (`@egose/shadcn-theme-ng/scroll-area`)
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1
|
+
# Searchable Multiselect (`@egose/shadcn-theme-ng/searchable-multiselect`)
|
|
2
2
|
|
|
3
|
-
|
|
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
|