@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.
- 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/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 +472 -236
- 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/radio-group/types/radio-group.d.ts +1 -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 +3 -3
- 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 +2 -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/typography/types/typography.d.ts +8 -8
- package/utils/README.md +303 -2
package/checkbox/README.md
CHANGED
|
@@ -1,3 +1,321 @@
|
|
|
1
|
-
# Checkbox
|
|
1
|
+
# Checkbox (`@egose/shadcn-theme-ng/checkbox`)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A shadcn/ui-style checkbox — the Angular port of shadcn/ui `Checkbox`. A single standalone `hlm-checkbox` component wraps `BrnCheckbox` from `@spartan-ng/brain/checkbox`, renders the `lucideCheck` icon when checked, and fully implements `ControlValueAccessor`, so it works with `[(checked)]`, `[(ngModel)]`, and reactive `FormControl`s. Supports the tri-state `boolean | 'indeterminate'` model, separate `disabled` (form-aware) vs `wrapperDisabled` (visual-only) locks, and a `changed` output.
|
|
4
|
+
|
|
5
|
+
Ships as `@egose/shadcn-theme-ng/checkbox` and `@egose/shadcn-theme-ng-tw/checkbox` (the `tw:`-prefixed variant). See the [package README](../../README.md) for install, peer dependencies, and Tailwind setup. Do not publish this project directory independently — it is consumed via the published subpath only.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Plain Tailwind (no prefix):
|
|
11
|
+
npm install @egose/shadcn-theme-ng
|
|
12
|
+
|
|
13
|
+
# Or the tw:-prefixed variant:
|
|
14
|
+
npm install @egose/shadcn-theme-ng-tw
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Peers are listed in the [package README](../../README.md). This subpath declares `@angular/common` and `@angular/core` as peers (see `projects/checkbox/package.json`); `FormsModule`/`ReactiveFormsModule` come from your app when you use form bindings.
|
|
18
|
+
|
|
19
|
+
## Imports
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import {
|
|
23
|
+
HlmCheckbox,
|
|
24
|
+
HlmCheckboxImports,
|
|
25
|
+
HlmCheckboxModule,
|
|
26
|
+
HLM_CHECKBOX_VALUE_ACCESSOR,
|
|
27
|
+
} from '@egose/shadcn-theme-ng/checkbox';
|
|
28
|
+
// tw variant:
|
|
29
|
+
// import { ... } from '@egose/shadcn-theme-ng-tw/checkbox';
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Standalone:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { Component } from '@angular/core';
|
|
36
|
+
import { HlmCheckboxImports } from '@egose/shadcn-theme-ng/checkbox';
|
|
37
|
+
|
|
38
|
+
@Component({
|
|
39
|
+
selector: 'app-demo',
|
|
40
|
+
standalone: true,
|
|
41
|
+
imports: [...HlmCheckboxImports],
|
|
42
|
+
template: `<hlm-checkbox />`,
|
|
43
|
+
})
|
|
44
|
+
export class DemoComponent {}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
NgModule:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { NgModule } from '@angular/core';
|
|
51
|
+
import { HlmCheckboxModule } from '@egose/shadcn-theme-ng/checkbox';
|
|
52
|
+
|
|
53
|
+
@NgModule({ imports: [HlmCheckboxModule] })
|
|
54
|
+
export class DemoModule {}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Anatomy / Structure
|
|
58
|
+
|
|
59
|
+
```html
|
|
60
|
+
<!-- Two-way model binding -->
|
|
61
|
+
<hlm-checkbox [(checked)]="accept" aria-label="Accept terms" />
|
|
62
|
+
|
|
63
|
+
<!-- With visible label -->
|
|
64
|
+
<div class="flex items-center gap-2">
|
|
65
|
+
<hlm-checkbox id="marketing" [(checked)]="marketing" />
|
|
66
|
+
<label for="marketing">Email me product updates</label>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<!-- Reactive form -->
|
|
70
|
+
<hlm-checkbox formControlName="accept" />
|
|
71
|
+
|
|
72
|
+
<!-- Template-driven -->
|
|
73
|
+
<hlm-checkbox name="accept" [(ngModel)]="accept" />
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Real selector: `hlm-checkbox` (element only). The host renders `class="contents peer"` so sibling label/description selectors (`peer-*`) keep working; `id` / `aria-*` attributes are nulled on the host and forwarded to the inner `brn-checkbox` instead. Set a visible label with a native `<label for>` — the component does not render label text itself.
|
|
77
|
+
|
|
78
|
+
## API reference
|
|
79
|
+
|
|
80
|
+
### `HlmCheckbox` (`hlm-checkbox`)
|
|
81
|
+
|
|
82
|
+
| Input | Type | Default | Description |
|
|
83
|
+
| -------------------------------------- | ----------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
84
|
+
| `checked` | `model<boolean \| 'indeterminate'>` | `false` | Checked state. Two-way bindable (`[(checked)]`). `'indeterminate'` renders the checked icon; clicking it resolves to `true`. |
|
|
85
|
+
| `class` (`userClass`) | `ClassValue` | `''` | Extra classes merged into the box. |
|
|
86
|
+
| `id` | `string \| null` | `null` | Forwarded to inner `brn-checkbox`. Pair with `<label for>`. |
|
|
87
|
+
| `aria-label` (`ariaLabel`) | `string \| null` | `null` | Forwarded aria-label. |
|
|
88
|
+
| `aria-labelledby` (`ariaLabelledby`) | `string \| null` | `null` | Forwarded aria-labelledby. |
|
|
89
|
+
| `aria-describedby` (`ariaDescribedby`) | `string \| null` | `null` | Forwarded aria-describedby. |
|
|
90
|
+
| `name` | `string \| null` | `null` | Forwarded `name`. |
|
|
91
|
+
| `required` | `boolean` | `false` | Forwarded `required` (boolean-coerced). |
|
|
92
|
+
| `disabled` | `boolean` | `false` | Visual + interaction lock (boolean-coerced). Clicks are ignored. |
|
|
93
|
+
| `wrapperDisabled` | `boolean` | `false` | Additional visual/interaction lock that does **not** write to a reactive form control (unlike `setDisabledState`). Useful for preview-only states. |
|
|
94
|
+
|
|
95
|
+
| Signal | Type | Description |
|
|
96
|
+
| --------------- | ----------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
97
|
+
| `disabledState` | `Signal<boolean>` | `disabled() \|\| wrapperDisabled() \|\| formDisabled()` — the effective lock. Host reflects `data-disabled`. |
|
|
98
|
+
|
|
99
|
+
| Output | Payload | Description |
|
|
100
|
+
| --------- | --------- | -------------------------------------------------------------------------------------------------------------- |
|
|
101
|
+
| `changed` | `boolean` | Emitted on user toggle with the new boolean (`!previous`). Note: toggling from `'indeterminate'` emits `true`. |
|
|
102
|
+
|
|
103
|
+
ControlValueAccessor: `writeValue(value)`, `registerOnChange`, `registerOnTouched`, `setDisabledState(isDisabled)` (writes `formDisabled`, hence `disabledState`). `HLM_CHECKBOX_VALUE_ACCESSOR` (`NG_VALUE_ACCESSOR`, `forwardRef(HlmCheckbox)`, `multi: true`) is provided on the component.
|
|
104
|
+
|
|
105
|
+
## Examples
|
|
106
|
+
|
|
107
|
+
### 1. Basic with label
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { Component, signal } from '@angular/core';
|
|
111
|
+
import { HlmCheckboxImports } from '@egose/shadcn-theme-ng/checkbox';
|
|
112
|
+
|
|
113
|
+
@Component({
|
|
114
|
+
selector: 'app-basic-checkbox',
|
|
115
|
+
standalone: true,
|
|
116
|
+
imports: [...HlmCheckboxImports],
|
|
117
|
+
template: `
|
|
118
|
+
<div class="flex items-center gap-2">
|
|
119
|
+
<hlm-checkbox id="terms" [(checked)]="accept" />
|
|
120
|
+
<label for="terms" class="text-sm">Accept terms and conditions</label>
|
|
121
|
+
</div>
|
|
122
|
+
<p class="mt-2 text-sm">Value: {{ accept() }}</p>
|
|
123
|
+
`,
|
|
124
|
+
})
|
|
125
|
+
export class BasicCheckboxComponent {
|
|
126
|
+
readonly accept = signal<boolean | 'indeterminate'>(false);
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 2. Reactive form with validation
|
|
131
|
+
|
|
132
|
+
`hlm-checkbox` is a `ControlValueAccessor` — `formControlName` just works, including `disable()`.
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { Component } from '@angular/core';
|
|
136
|
+
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
137
|
+
import { HlmCheckboxImports } from '@egose/shadcn-theme-ng/checkbox';
|
|
138
|
+
|
|
139
|
+
@Component({
|
|
140
|
+
selector: 'app-reactive-checkbox',
|
|
141
|
+
standalone: true,
|
|
142
|
+
imports: [ReactiveFormsModule, ...HlmCheckboxImports],
|
|
143
|
+
template: `
|
|
144
|
+
<form [formGroup]="form" (ngSubmit)="submit()" class="space-y-3">
|
|
145
|
+
<div class="flex items-center gap-2">
|
|
146
|
+
<hlm-checkbox id="accept" formControlName="accept" />
|
|
147
|
+
<label for="accept" class="text-sm">I agree to the privacy policy</label>
|
|
148
|
+
</div>
|
|
149
|
+
@if (form.controls.accept.touched && form.controls.accept.invalid) {
|
|
150
|
+
<p class="text-destructive text-sm">You must accept to continue.</p>
|
|
151
|
+
}
|
|
152
|
+
<button type="submit" [disabled]="form.invalid">Continue</button>
|
|
153
|
+
<button type="button" (click)="form.controls.accept.disable()">Disable via form</button>
|
|
154
|
+
</form>
|
|
155
|
+
`,
|
|
156
|
+
})
|
|
157
|
+
export class ReactiveCheckboxComponent {
|
|
158
|
+
readonly form = new FormGroup({
|
|
159
|
+
accept: new FormControl(false, { validators: Validators.requiredTrue, nonNullable: true }),
|
|
160
|
+
});
|
|
161
|
+
submit() {
|
|
162
|
+
console.log(this.form.getRawValue());
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### 3. Template-driven (`ngModel`)
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
import { Component } from '@angular/core';
|
|
171
|
+
import { FormsModule } from '@angular/forms';
|
|
172
|
+
import { HlmCheckboxImports } from '@egose/shadcn-theme-ng/checkbox';
|
|
173
|
+
|
|
174
|
+
@Component({
|
|
175
|
+
selector: 'app-ngmodel-checkbox',
|
|
176
|
+
standalone: true,
|
|
177
|
+
imports: [FormsModule, ...HlmCheckboxImports],
|
|
178
|
+
template: `
|
|
179
|
+
<div class="flex items-center gap-2">
|
|
180
|
+
<hlm-checkbox id="news" name="news" [(ngModel)]="subscribed" aria-label="Subscribe to newsletter" />
|
|
181
|
+
<label for="news" class="text-sm">Subscribe to newsletter</label>
|
|
182
|
+
</div>
|
|
183
|
+
`,
|
|
184
|
+
})
|
|
185
|
+
export class NgModelCheckboxComponent {
|
|
186
|
+
subscribed = true;
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### 4. Indeterminate + select-all pattern
|
|
191
|
+
|
|
192
|
+
Parent shows `'indeterminate'` when some (not all) children are checked — the classic bulk-select header.
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
import { Component, computed, signal } from '@angular/core';
|
|
196
|
+
import { HlmCheckboxImports } from '@egose/shadcn-theme-ng/checkbox';
|
|
197
|
+
|
|
198
|
+
@Component({
|
|
199
|
+
selector: 'app-select-all',
|
|
200
|
+
standalone: true,
|
|
201
|
+
imports: [...HlmCheckboxImports],
|
|
202
|
+
template: `
|
|
203
|
+
<div class="flex items-center gap-2 border-b pb-2">
|
|
204
|
+
<hlm-checkbox id="all" [checked]="parentState()" (changed)="toggleAll($event)" aria-label="Select all files" />
|
|
205
|
+
<label for="all" class="text-sm font-medium">Select all</label>
|
|
206
|
+
</div>
|
|
207
|
+
@for (f of files(); track f.id) {
|
|
208
|
+
<div class="flex items-center gap-2 py-1">
|
|
209
|
+
<hlm-checkbox [id]="f.id" [checked]="f.done" (changed)="toggleOne(f.id, $event)" />
|
|
210
|
+
<label [for]="f.id" class="text-sm">{{ f.name }}</label>
|
|
211
|
+
</div>
|
|
212
|
+
}
|
|
213
|
+
`,
|
|
214
|
+
})
|
|
215
|
+
export class SelectAllComponent {
|
|
216
|
+
readonly files = signal([
|
|
217
|
+
{ id: 'f1', name: 'invoice.pdf', done: true },
|
|
218
|
+
{ id: 'f2', name: 'photo.png', done: false },
|
|
219
|
+
{ id: 'f3', name: 'notes.md', done: false },
|
|
220
|
+
]);
|
|
221
|
+
readonly parentState = computed<boolean | 'indeterminate'>(() => {
|
|
222
|
+
const all = this.files();
|
|
223
|
+
if (all.every((f) => f.done)) return true;
|
|
224
|
+
if (all.some((f) => f.done)) return 'indeterminate';
|
|
225
|
+
return false;
|
|
226
|
+
});
|
|
227
|
+
toggleAll(next: boolean) {
|
|
228
|
+
this.files.update((fs) => fs.map((f) => ({ ...f, done: next })));
|
|
229
|
+
}
|
|
230
|
+
toggleOne(id: string, next: boolean) {
|
|
231
|
+
this.files.update((fs) => fs.map((f) => (f.id === id ? { ...f, done: next } : f)));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### 5. Disabled / wrapper-disabled / required states
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
import { Component, signal } from '@angular/core';
|
|
240
|
+
import { HlmCheckboxImports } from '@egose/shadcn-theme-ng/checkbox';
|
|
241
|
+
|
|
242
|
+
@Component({
|
|
243
|
+
selector: 'app-states-checkbox',
|
|
244
|
+
standalone: true,
|
|
245
|
+
imports: [...HlmCheckboxImports],
|
|
246
|
+
template: `
|
|
247
|
+
<div class="space-y-2">
|
|
248
|
+
<div class="flex items-center gap-2">
|
|
249
|
+
<hlm-checkbox id="d1" [disabled]="true" aria-label="Disabled unchecked" />
|
|
250
|
+
<label for="d1" class="text-sm opacity-70">Disabled (unchecked)</label>
|
|
251
|
+
</div>
|
|
252
|
+
<div class="flex items-center gap-2">
|
|
253
|
+
<hlm-checkbox id="d2" [checked]="true" [disabled]="true" aria-label="Disabled checked" />
|
|
254
|
+
<label for="d2" class="text-sm opacity-70">Disabled (checked)</label>
|
|
255
|
+
</div>
|
|
256
|
+
<div class="flex items-center gap-2">
|
|
257
|
+
<hlm-checkbox id="w1" [wrapperDisabled]="preview()" aria-label="Preview lock" />
|
|
258
|
+
<label for="w1" class="text-sm">Preview lock (visual only — form value untouched)</label>
|
|
259
|
+
</div>
|
|
260
|
+
<div class="flex items-center gap-2">
|
|
261
|
+
<hlm-checkbox id="r1" [(checked)]="v" [required]="true" aria-label="Required" />
|
|
262
|
+
<label for="r1" class="text-sm">Required</label>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
`,
|
|
266
|
+
})
|
|
267
|
+
export class StatesCheckboxComponent {
|
|
268
|
+
readonly preview = signal(true);
|
|
269
|
+
v = false;
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### 6. Listening to `changed` + custom styling
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
import { Component } from '@angular/core';
|
|
277
|
+
import { HlmCheckboxImports } from '@egose/shadcn-theme-ng/checkbox';
|
|
278
|
+
|
|
279
|
+
@Component({
|
|
280
|
+
selector: 'app-changed-checkbox',
|
|
281
|
+
standalone: true,
|
|
282
|
+
imports: [...HlmCheckboxImports],
|
|
283
|
+
template: `
|
|
284
|
+
<div class="flex items-center gap-2">
|
|
285
|
+
<hlm-checkbox
|
|
286
|
+
id="audit"
|
|
287
|
+
class="data-[state=checked]:border-emerald-600 data-[state=checked]:bg-emerald-600"
|
|
288
|
+
aria-label="Enable audit log"
|
|
289
|
+
(changed)="onChanged($event)"
|
|
290
|
+
/>
|
|
291
|
+
<label for="audit" class="text-sm">Enable audit log</label>
|
|
292
|
+
</div>
|
|
293
|
+
<p class="mt-1 text-sm">Last event: {{ lastChanged === null ? 'none' : lastChanged }}</p>
|
|
294
|
+
`,
|
|
295
|
+
})
|
|
296
|
+
export class ChangedCheckboxComponent {
|
|
297
|
+
lastChanged: boolean | null = null;
|
|
298
|
+
onChanged(next: boolean) {
|
|
299
|
+
this.lastChanged = next;
|
|
300
|
+
console.log('checkbox changed →', next);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Accessibility notes
|
|
306
|
+
|
|
307
|
+
- The component renders a native-checkbox-equivalent `brn-checkbox` with `aria-label`/`labelledby`/`describedby` forwarding — always provide one: visible `<label for>` preferred, `aria-label` for icon-only rows.
|
|
308
|
+
- `disabledState` adds `data-disabled`, `cursor-not-allowed`, and `opacity-50`; disabled boxes are skipped correctly because clicks early-return.
|
|
309
|
+
- The check glyph is decorative (`lucideCheck` icon, no text) — state is exposed via `data-[state]` / native semantics, not the icon.
|
|
310
|
+
- Tri-state: `'indeterminate'` is a visual/model state, not `aria-checked="mixed"` — if you need mixed semantics for a tree, add `aria-checked` handling at your own level and test with a screen reader.
|
|
311
|
+
|
|
312
|
+
## Theming / CSS variables
|
|
313
|
+
|
|
314
|
+
No component-specific CSS variables. The box uses `border-input`, `data-[state=checked]:bg-primary` / `text-primary-foreground`, `focus-visible:ring-ring/50`, `aria-invalid:border-destructive`, `size-4 rounded-[4px]`. Invalid-form styling integrates via `aria-invalid` ring tokens. Pass `class` to extend (merged with `hlm()`).
|
|
315
|
+
|
|
316
|
+
## Related subpaths
|
|
317
|
+
|
|
318
|
+
- `@egose/shadcn-theme-ng/form-checkbox` — labeled + described + validated field composition.
|
|
319
|
+
- `@egose/shadcn-theme-ng/label` — accessible labels for the `for`/`id` pair.
|
|
320
|
+
- `@egose/shadcn-theme-ng/field` — form layout rows that host checkboxes.
|
|
321
|
+
- `@egose/shadcn-theme-ng/icon` — `HlmIcon` sizing used by the inner check glyph.
|
package/collapsible/README.md
CHANGED
|
@@ -1,11 +1,338 @@
|
|
|
1
|
-
# Collapsible
|
|
1
|
+
# Collapsible (`@egose/shadcn-theme-ng/collapsible`)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A show/hide disclosure container — the Angular port of shadcn/ui `Collapsible`. Three thin directives compose it: `HlmCollapsible` (state owner, wraps `BrnCollapsible`), `HlmCollapsibleTrigger` (toggle button, wraps `BrnCollapsibleTrigger`), and `HlmCollapsibleContent` (collapsing panel, wraps `BrnCollapsibleContent`). All behavior — expanded state, disabled locking, `expandedChange` events, trigger/content wiring — comes from `@spartan-ng/brain/collapsible`; this package only adds `data-slot` attributes and (for content) the `data-[state=closed]:hidden` rule.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Ships as `@egose/shadcn-theme-ng/collapsible` and `@egose/shadcn-theme-ng-tw/collapsible` (the `tw:`-prefixed variant). See the [package README](../../README.md) for install, peer dependencies, and Tailwind setup. Do not publish this project directory independently — it is consumed via the published subpath only.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Installation
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
|
|
10
|
+
# Plain Tailwind (no prefix):
|
|
11
|
+
npm install @egose/shadcn-theme-ng
|
|
12
|
+
|
|
13
|
+
# Or the tw:-prefixed variant:
|
|
14
|
+
npm install @egose/shadcn-theme-ng-tw
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Peers are listed in the [package README](../../README.md). This subpath declares `@angular/common`, `@angular/core`, and `@spartan-ng/brain` as peers (see `projects/collapsible/package.json`).
|
|
18
|
+
|
|
19
|
+
## Imports
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import {
|
|
23
|
+
HlmCollapsible,
|
|
24
|
+
HlmCollapsibleTrigger,
|
|
25
|
+
HlmCollapsibleContent,
|
|
26
|
+
HlmCollapsibleImports,
|
|
27
|
+
HlmCollapsibleModule,
|
|
28
|
+
} from '@egose/shadcn-theme-ng/collapsible';
|
|
29
|
+
// tw variant:
|
|
30
|
+
// import { ... } from '@egose/shadcn-theme-ng-tw/collapsible';
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Standalone:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { Component } from '@angular/core';
|
|
37
|
+
import { HlmCollapsibleImports } from '@egose/shadcn-theme-ng/collapsible';
|
|
38
|
+
|
|
39
|
+
@Component({
|
|
40
|
+
selector: 'app-demo',
|
|
41
|
+
standalone: true,
|
|
42
|
+
imports: [...HlmCollapsibleImports],
|
|
43
|
+
template: `...`,
|
|
44
|
+
})
|
|
45
|
+
export class DemoComponent {}
|
|
11
46
|
```
|
|
47
|
+
|
|
48
|
+
NgModule:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { NgModule } from '@angular/core';
|
|
52
|
+
import { HlmCollapsibleModule } from '@egose/shadcn-theme-ng/collapsible';
|
|
53
|
+
|
|
54
|
+
@NgModule({ imports: [HlmCollapsibleModule] })
|
|
55
|
+
export class DemoModule {}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Anatomy / Structure
|
|
59
|
+
|
|
60
|
+
```html
|
|
61
|
+
<hlm-collapsible [expanded]="open" (expandedChange)="open = $event">
|
|
62
|
+
<button hlmCollapsibleTrigger>Toggle details</button>
|
|
63
|
+
<hlm-collapsible-content>
|
|
64
|
+
<p>Hidden until expanded.</p>
|
|
65
|
+
</hlm-collapsible-content>
|
|
66
|
+
</hlm-collapsible>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Real selectors (from source — note the trigger is button-only):
|
|
70
|
+
|
|
71
|
+
| Class | Selector | `data-slot` |
|
|
72
|
+
| ----------------------- | ---------------------------------------------------- | --------------------- |
|
|
73
|
+
| `HlmCollapsible` | `[hlmCollapsible]`, `hlm-collapsible` | `collapsible` |
|
|
74
|
+
| `HlmCollapsibleTrigger` | `button[hlmCollapsibleTrigger]` | `collapsible-trigger` |
|
|
75
|
+
| `HlmCollapsibleContent` | `[hlmCollapsibleContent]`, `hlm-collapsible-content` | `collapsible-content` |
|
|
76
|
+
|
|
77
|
+
The trigger **must** be a `<button>` element (`button[hlmCollapsibleTrigger]` — an `<a>` or `<div>` with the attribute will not match). Trigger and content must be descendants of the same `HlmCollapsible` host.
|
|
78
|
+
|
|
79
|
+
## API reference
|
|
80
|
+
|
|
81
|
+
### `HlmCollapsible` (`[hlmCollapsible]`, `hlm-collapsible`)
|
|
82
|
+
|
|
83
|
+
Thin wrapper around `BrnCollapsible`:
|
|
84
|
+
|
|
85
|
+
| Brain input | Type | Description |
|
|
86
|
+
| ----------- | --------- | ---------------------------------------------------------------------------------- |
|
|
87
|
+
| `expanded` | `boolean` | Controlled expanded state. Bind + listen to `expandedChange` for two-way behavior. |
|
|
88
|
+
| `disabled` | `boolean` | Locks the trigger; content stays as-is. |
|
|
89
|
+
|
|
90
|
+
| Brain output | Payload | Description |
|
|
91
|
+
| ---------------- | --------- | ------------------------------------------------------------------------ |
|
|
92
|
+
| `expandedChange` | `boolean` | Emitted whenever the trigger toggles (or state is set programmatically). |
|
|
93
|
+
|
|
94
|
+
No own inputs/outputs/methods.
|
|
95
|
+
|
|
96
|
+
### `HlmCollapsibleTrigger` (`button[hlmCollapsibleTrigger]`)
|
|
97
|
+
|
|
98
|
+
Thin wrapper around `BrnCollapsibleTrigger`:
|
|
99
|
+
|
|
100
|
+
| Brain input | Type | Description |
|
|
101
|
+
| ----------- | -------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
102
|
+
| `type` | `string` | Forwarded button `type` (`'button'`, `'submit'`, …). Default to `type="button"` inside forms to avoid accidental submits. |
|
|
103
|
+
|
|
104
|
+
No outputs of its own — activation flows through the parent's `expandedChange`.
|
|
105
|
+
|
|
106
|
+
### `HlmCollapsibleContent` (`[hlmCollapsibleContent]`, `hlm-collapsible-content`)
|
|
107
|
+
|
|
108
|
+
Thin wrapper around `BrnCollapsibleContent`:
|
|
109
|
+
|
|
110
|
+
| Brain input | Type | Description |
|
|
111
|
+
| ----------- | -------- | ---------------------------------------- |
|
|
112
|
+
| `id` | `string` | Content panel id (aria-controls wiring). |
|
|
113
|
+
|
|
114
|
+
Adds `data-[state=closed]:hidden` — the open/close animation (if any) is yours to add via `data-[state]` selectors.
|
|
115
|
+
|
|
116
|
+
## Examples
|
|
117
|
+
|
|
118
|
+
### 1. Basic collapsible (two-way binding pattern)
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import { Component } from '@angular/core';
|
|
122
|
+
import { HlmCollapsibleImports } from '@egose/shadcn-theme-ng/collapsible';
|
|
123
|
+
import { HlmButtonImports } from '@egose/shadcn-theme-ng/button';
|
|
124
|
+
|
|
125
|
+
@Component({
|
|
126
|
+
selector: 'app-basic-collapsible',
|
|
127
|
+
standalone: true,
|
|
128
|
+
imports: [...HlmCollapsibleImports, ...HlmButtonImports],
|
|
129
|
+
template: `
|
|
130
|
+
<hlm-collapsible [expanded]="open" (expandedChange)="open = $event" class="w-96 rounded-lg border p-4">
|
|
131
|
+
<div class="flex items-center justify-between">
|
|
132
|
+
<h4 class="text-sm font-medium">Repository settings</h4>
|
|
133
|
+
<button hlmBtn variant="ghost" size="sm" hlmCollapsibleTrigger>
|
|
134
|
+
{{ open ? 'Hide' : 'Show' }}
|
|
135
|
+
</button>
|
|
136
|
+
</div>
|
|
137
|
+
<hlm-collapsible-content>
|
|
138
|
+
<p class="pt-3 text-sm">Visibility, branch protection, and secrets live here.</p>
|
|
139
|
+
</hlm-collapsible-content>
|
|
140
|
+
</hlm-collapsible>
|
|
141
|
+
`,
|
|
142
|
+
})
|
|
143
|
+
export class BasicCollapsibleComponent {
|
|
144
|
+
open = false;
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 2. Default-open + disabled lock
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import { Component } from '@angular/core';
|
|
152
|
+
import { HlmCollapsibleImports } from '@egose/shadcn-theme-ng/collapsible';
|
|
153
|
+
|
|
154
|
+
@Component({
|
|
155
|
+
selector: 'app-disabled-collapsible',
|
|
156
|
+
standalone: true,
|
|
157
|
+
imports: [...HlmCollapsibleImports],
|
|
158
|
+
template: `
|
|
159
|
+
<div class="space-y-4">
|
|
160
|
+
<hlm-collapsible [expanded]="true" class="rounded-lg border p-4">
|
|
161
|
+
<button hlmCollapsibleTrigger type="button" class="text-sm font-medium">Starts open (expanded=true)</button>
|
|
162
|
+
<hlm-collapsible-content>
|
|
163
|
+
<p class="pt-2 text-sm">This panel renders visible on first paint.</p>
|
|
164
|
+
</hlm-collapsible-content>
|
|
165
|
+
</hlm-collapsible>
|
|
166
|
+
|
|
167
|
+
<hlm-collapsible [disabled]="true" class="rounded-lg border p-4 opacity-80">
|
|
168
|
+
<button hlmCollapsibleTrigger type="button" class="text-sm font-medium">
|
|
169
|
+
Locked (disabled=true) — trigger does nothing
|
|
170
|
+
</button>
|
|
171
|
+
<hlm-collapsible-content>
|
|
172
|
+
<p class="pt-2 text-sm">You cannot reach this content while disabled.</p>
|
|
173
|
+
</hlm-collapsible-content>
|
|
174
|
+
</hlm-collapsible>
|
|
175
|
+
</div>
|
|
176
|
+
`,
|
|
177
|
+
})
|
|
178
|
+
export class DisabledCollapsibleComponent {}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### 3. Chevron rotation driven by state
|
|
182
|
+
|
|
183
|
+
Bind the icon rotation to your `open` flag — the classic shadcn pattern.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import { Component } from '@angular/core';
|
|
187
|
+
import { HlmCollapsibleImports } from '@egose/shadcn-theme-ng/collapsible';
|
|
188
|
+
import { NgIcon, provideIcons } from '@ng-icons/core';
|
|
189
|
+
import { lucideChevronsUpDown } from '@ng-icons/lucide';
|
|
190
|
+
|
|
191
|
+
@Component({
|
|
192
|
+
selector: 'app-chevron-collapsible',
|
|
193
|
+
standalone: true,
|
|
194
|
+
imports: [...HlmCollapsibleImports, NgIcon],
|
|
195
|
+
providers: [provideIcons({ lucideChevronsUpDown })],
|
|
196
|
+
template: `
|
|
197
|
+
<hlm-collapsible [expanded]="open" (expandedChange)="open = $event" class="w-96 rounded-lg border p-4">
|
|
198
|
+
<button hlmCollapsibleTrigger type="button" class="flex w-full items-center justify-between text-sm font-medium">
|
|
199
|
+
Can I use this in my project?
|
|
200
|
+
<ng-icon name="lucideChevronsUpDown" class="transition-transform duration-200" [class.rotate-180]="open" />
|
|
201
|
+
</button>
|
|
202
|
+
<hlm-collapsible-content>
|
|
203
|
+
<p class="pt-3 text-sm">Yes — free for personal and commercial use.</p>
|
|
204
|
+
</hlm-collapsible-content>
|
|
205
|
+
</hlm-collapsible>
|
|
206
|
+
`,
|
|
207
|
+
})
|
|
208
|
+
export class ChevronCollapsibleComponent {
|
|
209
|
+
open = false;
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### 4. FAQ list (repeated collapsibles)
|
|
214
|
+
|
|
215
|
+
Each item owns its state — use an array of booleans or a Set of open ids.
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import { Component } from '@angular/core';
|
|
219
|
+
import { HlmCollapsibleImports } from '@egose/shadcn-theme-ng/collapsible';
|
|
220
|
+
|
|
221
|
+
@Component({
|
|
222
|
+
selector: 'app-faq-collapsible',
|
|
223
|
+
standalone: true,
|
|
224
|
+
imports: [...HlmCollapsibleImports],
|
|
225
|
+
template: `
|
|
226
|
+
<div class="w-full max-w-xl space-y-2">
|
|
227
|
+
@for (item of faqs; track item.q; let i = $index) {
|
|
228
|
+
<hlm-collapsible [expanded]="open[i]" (expandedChange)="open[i] = $event" class="rounded-lg border px-4 py-3">
|
|
229
|
+
<button hlmCollapsibleTrigger type="button" class="w-full text-left text-sm font-medium">
|
|
230
|
+
{{ item.q }}
|
|
231
|
+
</button>
|
|
232
|
+
<hlm-collapsible-content>
|
|
233
|
+
<p class="pt-2 text-sm">{{ item.a }}</p>
|
|
234
|
+
</hlm-collapsible-content>
|
|
235
|
+
</hlm-collapsible>
|
|
236
|
+
}
|
|
237
|
+
</div>
|
|
238
|
+
`,
|
|
239
|
+
})
|
|
240
|
+
export class FaqCollapsibleComponent {
|
|
241
|
+
readonly faqs = [
|
|
242
|
+
{ q: 'Is it accessible?', a: 'Yes — triggers are real buttons with aria-expanded wiring from BrnCollapsible.' },
|
|
243
|
+
{ q: 'Is it animated?', a: 'Not by default; add data-[state] transitions (see example 6).' },
|
|
244
|
+
{ q: 'Can I nest them?', a: 'Yes — nest a full hlm-collapsible inside any content panel.' },
|
|
245
|
+
];
|
|
246
|
+
open: boolean[] = [true, false, false];
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### 5. Programmatic control (open all / close all)
|
|
251
|
+
|
|
252
|
+
Because `expanded` is a plain input, external buttons can drive it.
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
import { Component } from '@angular/core';
|
|
256
|
+
import { HlmCollapsibleImports } from '@egose/shadcn-theme-ng/collapsible';
|
|
257
|
+
import { HlmButtonImports } from '@egose/shadcn-theme-ng/button';
|
|
258
|
+
|
|
259
|
+
@Component({
|
|
260
|
+
selector: 'app-controlled-collapsible',
|
|
261
|
+
standalone: true,
|
|
262
|
+
imports: [...HlmCollapsibleImports, ...HlmButtonImports],
|
|
263
|
+
template: `
|
|
264
|
+
<div class="mb-3 flex gap-2">
|
|
265
|
+
<button hlmBtn variant="outline" size="sm" (click)="setAll(true)">Expand all</button>
|
|
266
|
+
<button hlmBtn variant="outline" size="sm" (click)="setAll(false)">Collapse all</button>
|
|
267
|
+
</div>
|
|
268
|
+
@for (s of sections; track s.title; let i = $index) {
|
|
269
|
+
<hlm-collapsible [expanded]="open[i]" (expandedChange)="open[i] = $event" class="mb-2 rounded-lg border p-4">
|
|
270
|
+
<button hlmCollapsibleTrigger type="button" class="text-sm font-medium">{{ s.title }}</button>
|
|
271
|
+
<hlm-collapsible-content>
|
|
272
|
+
<p class="pt-2 text-sm">{{ s.body }}</p>
|
|
273
|
+
</hlm-collapsible-content>
|
|
274
|
+
</hlm-collapsible>
|
|
275
|
+
}
|
|
276
|
+
`,
|
|
277
|
+
})
|
|
278
|
+
export class ControlledCollapsibleComponent {
|
|
279
|
+
readonly sections = [
|
|
280
|
+
{ title: 'Billing', body: 'Invoices, seats, and receipts.' },
|
|
281
|
+
{ title: 'Security', body: 'SSO, 2FA, and audit log.' },
|
|
282
|
+
];
|
|
283
|
+
open = [false, false];
|
|
284
|
+
setAll(v: boolean) {
|
|
285
|
+
this.open = this.open.map(() => v);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### 6. Animated height (advanced, `data-[state]` + grid trick)
|
|
291
|
+
|
|
292
|
+
The content host exposes `data-state="open" | "closed"` from the brain directive — animate with a grid-rows transition instead of `hidden`.
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
import { Component } from '@angular/core';
|
|
296
|
+
import { HlmCollapsibleImports } from '@egose/shadcn-theme-ng/collapsible';
|
|
297
|
+
|
|
298
|
+
@Component({
|
|
299
|
+
selector: 'app-animated-collapsible',
|
|
300
|
+
standalone: true,
|
|
301
|
+
imports: [...HlmCollapsibleImports],
|
|
302
|
+
template: `
|
|
303
|
+
<hlm-collapsible [expanded]="open" (expandedChange)="open = $event" class="w-96 rounded-lg border p-4">
|
|
304
|
+
<button hlmCollapsibleTrigger type="button" class="text-sm font-medium">
|
|
305
|
+
{{ open ? 'Collapse' : 'Expand' }} with animation
|
|
306
|
+
</button>
|
|
307
|
+
<hlm-collapsible-content
|
|
308
|
+
class="grid transition-all duration-200 data-[state=closed]:hidden data-[state=open]:grid-rows-[1fr] data-[state=closed]:grid-rows-[0fr]"
|
|
309
|
+
>
|
|
310
|
+
<div class="overflow-hidden">
|
|
311
|
+
<p class="pt-3 text-sm">Smooth grid-rows open/close. Keep the inner overflow-hidden wrapper.</p>
|
|
312
|
+
</div>
|
|
313
|
+
</hlm-collapsible-content>
|
|
314
|
+
</hlm-collapsible>
|
|
315
|
+
`,
|
|
316
|
+
})
|
|
317
|
+
export class AnimatedCollapsibleComponent {
|
|
318
|
+
open = false;
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## Accessibility notes
|
|
323
|
+
|
|
324
|
+
- Trigger is a native `<button>` with `aria-expanded` / `aria-controls` wired by `BrnCollapsibleTrigger` — never replace it with a `<div (click)>`; keyboard (Enter/Space) support depends on the button.
|
|
325
|
+
- Always use `type="button"` on triggers inside `<form>`s to avoid accidental submits (the `type` input is forwarded).
|
|
326
|
+
- `disabled` on the parent locks the trigger (`aria-disabled` semantics from brain) — announce why it is locked with adjacent hint text.
|
|
327
|
+
- Content is hidden with `data-[state=closed]:hidden` (display:none), so screen readers skip closed panels — correct disclosure behavior.
|
|
328
|
+
|
|
329
|
+
## Theming / CSS variables
|
|
330
|
+
|
|
331
|
+
No component-specific CSS variables and no default open/close animation. Style hooks: `data-slot` (`collapsible`, `collapsible-trigger`, `collapsible-content`) and `data-state="open|closed"` / `data-disabled`. Add your own `data-[state=open]:…` transitions as in example 6.
|
|
332
|
+
|
|
333
|
+
## Related subpaths
|
|
334
|
+
|
|
335
|
+
- `@egose/shadcn-theme-ng/accordion` — mutually-exclusive multi-item disclosures (use instead of hand-rolled FAQ state).
|
|
336
|
+
- `@egose/shadcn-theme-ng/button` — ghost/small trigger styling (`hlmBtn`).
|
|
337
|
+
- `@egose/shadcn-theme-ng/icon` — chevron affordances rotated from your `open` flag.
|
|
338
|
+
- `@egose/shadcn-theme-ng/card` — bordered shells that host collapsible rows.
|