@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/radio-group/README.md
CHANGED
|
@@ -1,3 +1,365 @@
|
|
|
1
|
-
# Radio Group
|
|
1
|
+
# Radio Group (`@egose/shadcn-theme-ng/radio-group`)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A shadcn/ui-style **Radio Group** for Angular — a set of mutually exclusive options where exactly one can be selected. This is the Angular equivalent of shadcn/ui `RadioGroup` / `RadioGroupItem`.
|
|
4
|
+
|
|
5
|
+
The primitives come from **spartan-ng/brain** (`BrnRadioGroup`, `BrnRadio`, `BrnFieldControlDescribedBy`): keyboard navigation (arrow keys), roving tabindex, and form integration are handled by `BrnRadioGroup`/`BrnRadio`, while this package adds the shadcn look (grid layout, circular indicator, focus ring, error styling).
|
|
6
|
+
|
|
7
|
+
> **Ships as:** `@egose/shadcn-theme-ng/radio-group` and `@egose/shadcn-theme-ng-tw/radio-group` (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
|
+
Import from the subpath (not the package root):
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { HlmRadioGroupImports } from '@egose/shadcn-theme-ng/radio-group';
|
|
24
|
+
// tw variant:
|
|
25
|
+
// import { HlmRadioGroupImports } from '@egose/shadcn-theme-ng-tw/radio-group';
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Peer dependencies (see [package README](../../README.md) for versions): `@angular/core`, `@angular/common`, `@spartan-ng/brain`. `@angular/cdk` is required transitively by `HlmRadio` (boolean coercion).
|
|
29
|
+
|
|
30
|
+
## Imports
|
|
31
|
+
|
|
32
|
+
Real exported symbols (from `src/public-api.ts`):
|
|
33
|
+
|
|
34
|
+
| Symbol | Kind | Description |
|
|
35
|
+
| ---------------------- | ------------- | ----------------------------------------------------------------- |
|
|
36
|
+
| `HlmRadioGroup` | Directive | Group container; forwards `BrnRadioGroup` |
|
|
37
|
+
| `HlmRadio` | Component | Single radio item (`hlm-radio`), wraps `BrnRadio` |
|
|
38
|
+
| `HlmRadioIndicator` | Component | Circular visual indicator dot |
|
|
39
|
+
| `HlmRadioGroupImports` | `const` array | `[HlmRadioGroup, HlmRadio, HlmRadioIndicator]` standalone imports |
|
|
40
|
+
| `HlmRadioGroupModule` | `NgModule` | NgModule wrapper re-exporting the three above |
|
|
41
|
+
|
|
42
|
+
Standalone usage:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { Component } from '@angular/core';
|
|
46
|
+
import { HlmRadioGroupImports } from '@egose/shadcn-theme-ng/radio-group';
|
|
47
|
+
|
|
48
|
+
@Component({
|
|
49
|
+
selector: 'app-demo',
|
|
50
|
+
standalone: true,
|
|
51
|
+
imports: [HlmRadioGroupImports],
|
|
52
|
+
template: `...`,
|
|
53
|
+
})
|
|
54
|
+
export class DemoComponent {}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
NgModule usage:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { NgModule } from '@angular/core';
|
|
61
|
+
import { HlmRadioGroupModule } from '@egose/shadcn-theme-ng/radio-group';
|
|
62
|
+
|
|
63
|
+
@NgModule({ imports: [HlmRadioGroupModule] })
|
|
64
|
+
export class DemoModule {}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
You can also import the pieces individually (`import { HlmRadioGroup, HlmRadio, HlmRadioIndicator } from '...'`).
|
|
68
|
+
|
|
69
|
+
## Anatomy / Structure
|
|
70
|
+
|
|
71
|
+
```html
|
|
72
|
+
<!-- Attribute form on a div -->
|
|
73
|
+
<div hlmRadioGroup name="plan" [value]="plan()" (valueChange)="plan.set($event)">
|
|
74
|
+
<hlm-radio value="free" inputId="plan-free">
|
|
75
|
+
<hlm-radio-indicator />
|
|
76
|
+
Free
|
|
77
|
+
</hlm-radio>
|
|
78
|
+
|
|
79
|
+
<hlm-radio value="pro" inputId="plan-pro">
|
|
80
|
+
<hlm-radio-indicator />
|
|
81
|
+
Pro
|
|
82
|
+
</hlm-radio>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<!-- Element form also works -->
|
|
86
|
+
<hlm-radio-group name="plan">
|
|
87
|
+
<hlm-radio value="free"><hlm-radio-indicator />Free</hlm-radio>
|
|
88
|
+
</hlm-radio-group>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Real selectors:
|
|
92
|
+
|
|
93
|
+
| Selector | Class | Notes |
|
|
94
|
+
| ------------------------------------ | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
95
|
+
| `[hlmRadioGroup]`, `hlm-radio-group` | `HlmRadioGroup` | Group host; `data-slot="radio-group"` |
|
|
96
|
+
| `hlm-radio` | `HlmRadio<T>` | Item host; `data-slot="radio-group-item"`; projects `[target],[indicator],hlm-radio-indicator` into the indicator slot, everything else as label content |
|
|
97
|
+
| `hlm-radio-indicator` | `HlmRadioIndicator` | Visual dot; `data-slot="radio-group-indicator"` |
|
|
98
|
+
|
|
99
|
+
## API reference
|
|
100
|
+
|
|
101
|
+
### HlmRadioGroup (directive)
|
|
102
|
+
|
|
103
|
+
Thin directive wrapper over `BrnRadioGroup` (plus `BrnFieldControlDescribedBy` for form-field `aria-describedby` wiring). Own inputs:
|
|
104
|
+
|
|
105
|
+
| Input | Type | Default | Description |
|
|
106
|
+
| --------------------- | ------------ | ------- | -------------------------------------------- |
|
|
107
|
+
| `class` (`userClass`) | `ClassValue` | `''` | Extra classes appended to `tw:grid tw:gap-3` |
|
|
108
|
+
|
|
109
|
+
Forwarded `BrnRadioGroup` host-directive bindings:
|
|
110
|
+
|
|
111
|
+
| Binding | Kind | Description |
|
|
112
|
+
| ------------- | ------ | ---------------------------------------- |
|
|
113
|
+
| `name` | input | Radio group name (native input grouping) |
|
|
114
|
+
| `value` | input | Currently selected value |
|
|
115
|
+
| `disabled` | input | Disables the whole group |
|
|
116
|
+
| `required` | input | Marks the group as required |
|
|
117
|
+
| `valueChange` | output | Emits the newly selected value |
|
|
118
|
+
|
|
119
|
+
The host also reflects form state as attributes: `aria-invalid`/`data-invalid` when the bound control is invalid, plus `data-dirty` and `data-touched`.
|
|
120
|
+
|
|
121
|
+
### HlmRadio\<T\> (component)
|
|
122
|
+
|
|
123
|
+
| Input | Type | Default | Description |
|
|
124
|
+
| -------------------------------------- | --------------------- | ------------ | -------------------------------------------------------------------------------------------------- |
|
|
125
|
+
| `value` | `T` | **required** | The value this item represents |
|
|
126
|
+
| `inputId` | `string \| undefined` | `undefined` | `id` placed on the underlying `brn-radio` element; also used to find an associated `<label [for]>` |
|
|
127
|
+
| `aria-label` (`ariaLabel`) | `string \| undefined` | `undefined` | Accessible name when there is no visible label |
|
|
128
|
+
| `aria-labelledby` (`ariaLabelledby`) | `string \| undefined` | `undefined` | Id(s) of labelling element(s) |
|
|
129
|
+
| `aria-describedby` (`ariaDescribedby`) | `string \| undefined` | `undefined` | Id(s) of describing element(s) |
|
|
130
|
+
| `required` | `boolean` | `false` | Native required flag (boolean-coerced) |
|
|
131
|
+
| `disabled` | `boolean` | `false` | Disables this item (boolean-coerced); mirrors `data-disabled` onto an associated `<label>` |
|
|
132
|
+
| `class` (`userClass`) | `ClassValue` | `''` | Extra classes |
|
|
133
|
+
|
|
134
|
+
| Output | Type | Description |
|
|
135
|
+
| -------- | ------------------- | ---------------------------------------------- |
|
|
136
|
+
| `change` | `BrnRadioChange<T>` | Emitted when this item's checked state changes |
|
|
137
|
+
|
|
138
|
+
Label association detail: on init the component looks for `closest('label')`, falling back to `document.querySelector('label[for=inputId]')`, and mirrors `data-disabled="true"/"false"` onto that label so label styling follows the disabled state. IDs containing special characters (e.g. quotes/brackets) are matched safely via `htmlFor` comparison, not a CSS selector.
|
|
139
|
+
|
|
140
|
+
### HlmRadioIndicator (component)
|
|
141
|
+
|
|
142
|
+
No inputs/outputs. Renders the styled outer circle; the inner dot fills via `group-data-[checked=true]` when the parent `brn-radio` reports checked. Always place it inside `hlm-radio` (it matches the `hlm-radio-indicator` content slot).
|
|
143
|
+
|
|
144
|
+
## Examples
|
|
145
|
+
|
|
146
|
+
### 1. Basic usage
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import { Component, signal } from '@angular/core';
|
|
150
|
+
import { HlmRadioGroupImports } from '@egose/shadcn-theme-ng/radio-group';
|
|
151
|
+
|
|
152
|
+
@Component({
|
|
153
|
+
selector: 'app-basic-radio',
|
|
154
|
+
standalone: true,
|
|
155
|
+
imports: [HlmRadioGroupImports],
|
|
156
|
+
template: `
|
|
157
|
+
<div hlmRadioGroup name="fruit" [value]="fruit()" (valueChange)="fruit.set($event)">
|
|
158
|
+
<hlm-radio value="apple" inputId="fruit-apple">
|
|
159
|
+
<hlm-radio-indicator />
|
|
160
|
+
Apple
|
|
161
|
+
</hlm-radio>
|
|
162
|
+
<hlm-radio value="banana" inputId="fruit-banana">
|
|
163
|
+
<hlm-radio-indicator />
|
|
164
|
+
Banana
|
|
165
|
+
</hlm-radio>
|
|
166
|
+
<hlm-radio value="orange" inputId="fruit-orange">
|
|
167
|
+
<hlm-radio-indicator />
|
|
168
|
+
Orange
|
|
169
|
+
</hlm-radio>
|
|
170
|
+
</div>
|
|
171
|
+
<p>Selected: {{ fruit() }}</p>
|
|
172
|
+
`,
|
|
173
|
+
})
|
|
174
|
+
export class BasicRadioComponent {
|
|
175
|
+
readonly fruit = signal('apple');
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### 2. Element selector form + external labels
|
|
180
|
+
|
|
181
|
+
`hlm-radio-group` works as an element, and labels can live outside the item via `for`/`inputId`:
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
import { Component, signal } from '@angular/core';
|
|
185
|
+
import { HlmRadioGroupImports } from '@egose/shadcn-theme-ng/radio-group';
|
|
186
|
+
|
|
187
|
+
@Component({
|
|
188
|
+
selector: 'app-labelled-radio',
|
|
189
|
+
standalone: true,
|
|
190
|
+
imports: [HlmRadioGroupImports],
|
|
191
|
+
template: `
|
|
192
|
+
<hlm-radio-group name="contact">
|
|
193
|
+
<label for="c-email">Email me</label>
|
|
194
|
+
<hlm-radio value="email" inputId="c-email"><hlm-radio-indicator /></hlm-radio>
|
|
195
|
+
|
|
196
|
+
<label for="c-sms">Text me</label>
|
|
197
|
+
<hlm-radio value="sms" inputId="c-sms"><hlm-radio-indicator /></hlm-radio>
|
|
198
|
+
</hlm-radio-group>
|
|
199
|
+
`,
|
|
200
|
+
})
|
|
201
|
+
export class LabelledRadioComponent {}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Wrapping the item in a `<label>` also works — the item finds it with `closest('label')`:
|
|
205
|
+
|
|
206
|
+
```html
|
|
207
|
+
<hlm-radio-group name="contact">
|
|
208
|
+
<label>
|
|
209
|
+
<hlm-radio value="email"><hlm-radio-indicator /></hlm-radio>
|
|
210
|
+
Email me
|
|
211
|
+
</label>
|
|
212
|
+
</hlm-radio-group>
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### 3. Reactive forms
|
|
216
|
+
|
|
217
|
+
`BrnRadioGroup` is a `ControlValueAccessor`, so `formControlName`/`formControl` bind on the group host:
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
import { Component } from '@angular/core';
|
|
221
|
+
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
222
|
+
import { HlmRadioGroupImports } from '@egose/shadcn-theme-ng/radio-group';
|
|
223
|
+
|
|
224
|
+
@Component({
|
|
225
|
+
selector: 'app-reactive-radio',
|
|
226
|
+
standalone: true,
|
|
227
|
+
imports: [HlmRadioGroupImports, ReactiveFormsModule],
|
|
228
|
+
template: `
|
|
229
|
+
<form [formGroup]="form" (ngSubmit)="submit()">
|
|
230
|
+
<div hlmRadioGroup formControlName="plan">
|
|
231
|
+
<hlm-radio value="hobby"><hlm-radio-indicator />Hobby</hlm-radio>
|
|
232
|
+
<hlm-radio value="pro"><hlm-radio-indicator />Pro</hlm-radio>
|
|
233
|
+
<hlm-radio value="enterprise"><hlm-radio-indicator />Enterprise</hlm-radio>
|
|
234
|
+
</div>
|
|
235
|
+
@if (form.controls.plan.invalid && form.controls.plan.touched) {
|
|
236
|
+
<p class="tw:text-destructive tw:text-sm">Please pick a plan.</p>
|
|
237
|
+
}
|
|
238
|
+
<button type="submit">Continue</button>
|
|
239
|
+
</form>
|
|
240
|
+
`,
|
|
241
|
+
})
|
|
242
|
+
export class ReactiveRadioComponent {
|
|
243
|
+
readonly form = new FormGroup({
|
|
244
|
+
plan: new FormControl<string | null>(null, Validators.required),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
submit(): void {
|
|
248
|
+
this.form.markAllAsTouched();
|
|
249
|
+
console.log(this.form.value);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
When the control is invalid + touched, the group automatically gets `data-invalid="true"` (and destructive text styling) via the forwarded control state — no manual class juggling needed.
|
|
255
|
+
|
|
256
|
+
### 4. Disabled states (group vs item)
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
import { Component, signal } from '@angular/core';
|
|
260
|
+
import { HlmRadioGroupImports } from '@egose/shadcn-theme-ng/radio-group';
|
|
261
|
+
|
|
262
|
+
@Component({
|
|
263
|
+
selector: 'app-disabled-radio',
|
|
264
|
+
standalone: true,
|
|
265
|
+
imports: [HlmRadioGroupImports],
|
|
266
|
+
template: `
|
|
267
|
+
<!-- Whole group disabled -->
|
|
268
|
+
<div hlmRadioGroup name="a" value="one" disabled>
|
|
269
|
+
<hlm-radio value="one"><hlm-radio-indicator />One</hlm-radio>
|
|
270
|
+
<hlm-radio value="two"><hlm-radio-indicator />Two</hlm-radio>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
<!-- Single item disabled; its <label> gets data-disabled="true" -->
|
|
274
|
+
<div hlmRadioGroup name="b" [value]="choice()" (valueChange)="choice.set($event)">
|
|
275
|
+
<label for="b-one">One (soon unavailable)</label>
|
|
276
|
+
<hlm-radio value="one" inputId="b-one" disabled><hlm-radio-indicator /></hlm-radio>
|
|
277
|
+
<label for="b-two">Two</label>
|
|
278
|
+
<hlm-radio value="two" inputId="b-two"><hlm-radio-indicator /></hlm-radio>
|
|
279
|
+
</div>
|
|
280
|
+
`,
|
|
281
|
+
})
|
|
282
|
+
export class DisabledRadioComponent {
|
|
283
|
+
readonly choice = signal('two');
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### 5. Per-item change events + typed values
|
|
288
|
+
|
|
289
|
+
`HlmRadio` is generic — `value` can be any type, and `change` emits `BrnRadioChange<T>`:
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
import { Component, signal } from '@angular/core';
|
|
293
|
+
import { HlmRadioGroupImports } from '@egose/shadcn-theme-ng/radio-group';
|
|
294
|
+
import type { BrnRadioChange } from '@spartan-ng/brain/radio-group';
|
|
295
|
+
|
|
296
|
+
interface Tier {
|
|
297
|
+
id: string;
|
|
298
|
+
price: number;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
@Component({
|
|
302
|
+
selector: 'app-typed-radio',
|
|
303
|
+
standalone: true,
|
|
304
|
+
imports: [HlmRadioGroupImports],
|
|
305
|
+
template: `
|
|
306
|
+
<div hlmRadioGroup name="tier" (valueChange)="onGroupChange($event)">
|
|
307
|
+
@for (tier of tiers; track tier.id) {
|
|
308
|
+
<hlm-radio [value]="tier" (change)="onItemChange($event)">
|
|
309
|
+
<hlm-radio-indicator />
|
|
310
|
+
{{ tier.id }} — ${{ tier.price }}/mo
|
|
311
|
+
</hlm-radio>
|
|
312
|
+
}
|
|
313
|
+
</div>
|
|
314
|
+
`,
|
|
315
|
+
})
|
|
316
|
+
export class TypedRadioComponent {
|
|
317
|
+
readonly tiers: Tier[] = [
|
|
318
|
+
{ id: 'starter', price: 0 },
|
|
319
|
+
{ id: 'growth', price: 29 },
|
|
320
|
+
];
|
|
321
|
+
|
|
322
|
+
onGroupChange(value: Tier): void {
|
|
323
|
+
console.log('group selected:', value.id);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
onItemChange(event: BrnRadioChange<Tier>): void {
|
|
327
|
+
console.log('item checked:', event.value.id, event.checked);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### 6. Custom indicator content (target slot)
|
|
333
|
+
|
|
334
|
+
Anything projected with `[target]` or `[indicator]` goes into the indicator slot instead of the default label position:
|
|
335
|
+
|
|
336
|
+
```html
|
|
337
|
+
<div hlmRadioGroup name="layout">
|
|
338
|
+
<hlm-radio value="grid">
|
|
339
|
+
<span target class="tw:flex tw:items-center tw:gap-2">
|
|
340
|
+
<hlm-radio-indicator />
|
|
341
|
+
<strong>Grid</strong>
|
|
342
|
+
</span>
|
|
343
|
+
<span class="tw:text-muted-foreground tw:text-sm">Cards in a grid</span>
|
|
344
|
+
</hlm-radio>
|
|
345
|
+
</div>
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## Accessibility notes
|
|
349
|
+
|
|
350
|
+
- The group uses the native `radiogroup` semantics from `BrnRadioGroup`, including arrow-key navigation and roving tabindex — keep all `hlm-radio` items inside one group container.
|
|
351
|
+
- Always provide `name` so assistive tech (and native form serialization) treats the items as one group.
|
|
352
|
+
- Prefer visible text content inside `hlm-radio`; use `aria-label`/`aria-labelledby` only when the item has no visible label.
|
|
353
|
+
- Disabled items expose `data-disabled` (and the native disabled state on the inner input) and are skipped in keyboard navigation.
|
|
354
|
+
- Invalid form state is announced via `aria-invalid="true"` on the group host.
|
|
355
|
+
|
|
356
|
+
## Theming / CSS variables
|
|
357
|
+
|
|
358
|
+
Styling is class-driven (no component-specific CSS variables). Override via the `class` input on any of the three pieces; the indicator's checked dot keys off `group-data-[checked=true]`, and the group invalid state off `data-[invalid=true]`.
|
|
359
|
+
|
|
360
|
+
## Related subpaths
|
|
361
|
+
|
|
362
|
+
- `@egose/shadcn-theme-ng/label` — labelling radio items and form rows
|
|
363
|
+
- `@egose/shadcn-theme-ng/field` / `form-field` — form rows, descriptions, and error text wired via `BrnFieldControlDescribedBy`
|
|
364
|
+
- `@egose/shadcn-theme-ng/checkbox` — multi-select counterpart
|
|
365
|
+
- `@egose/shadcn-theme-ng/form-field-simple` — lightweight wrapper for reactive-form controls
|
|
@@ -136,7 +136,11 @@ class HlmRadioGroup {
|
|
|
136
136
|
_errorState = computed(() => this._brnRadioGroup.controlState?.()?.spartanInvalid, /* @ts-ignore */
|
|
137
137
|
...(ngDevMode ? [{ debugName: "_errorState" }] : /* istanbul ignore next */ []));
|
|
138
138
|
constructor() {
|
|
139
|
-
classes(() => [
|
|
139
|
+
classes(() => [
|
|
140
|
+
"grid gap-3",
|
|
141
|
+
this.userClass(),
|
|
142
|
+
this._errorState() ? "data-[invalid=true]:text-destructive" : '',
|
|
143
|
+
]);
|
|
140
144
|
}
|
|
141
145
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: HlmRadioGroup, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
142
146
|
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.3", type: HlmRadioGroup, isStandalone: true, selector: "[hlmRadioGroup],hlm-radio-group", inputs: { userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "data-slot": "radio-group" }, properties: { "attr.aria-invalid": "_ariaInvalid() ? \"true\" : null", "attr.data-invalid": "_ariaInvalid() ? \"true\" : null", "attr.data-dirty": "_dirty() ? \"true\" : null", "attr.data-touched": "_touched() ? \"true\" : null" } }, hostDirectives: [{ directive: i1.BrnRadioGroup, inputs: ["name", "name", "value", "value", "disabled", "disabled", "required", "required"], outputs: ["valueChange", "valueChange"] }, { directive: i2.BrnFieldControlDescribedBy }], ngImport: i0 });
|
|
@@ -15,7 +15,7 @@ declare class HlmRadio<T = unknown> {
|
|
|
15
15
|
protected readonly _touched: _angular_core.Signal<boolean | undefined>;
|
|
16
16
|
protected readonly _dirty: _angular_core.Signal<boolean | undefined>;
|
|
17
17
|
protected readonly _groupSpartanInvalid: _angular_core.Signal<boolean | undefined>;
|
|
18
|
-
protected readonly _errorStateClass: _angular_core.Signal<"
|
|
18
|
+
protected readonly _errorStateClass: _angular_core.Signal<"" | "text-destructive">;
|
|
19
19
|
readonly userClass: _angular_core.InputSignal<ClassValue>;
|
|
20
20
|
protected readonly _computedClass: _angular_core.Signal<string>;
|
|
21
21
|
/** Used to set the id on the underlying brn element. */
|
package/resizable/README.md
CHANGED
|
@@ -1,11 +1,275 @@
|
|
|
1
|
-
# Resizable
|
|
1
|
+
# Resizable (`@egose/shadcn-theme-ng/resizable`)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A shadcn/ui-style **Resizable** panel layout — draggable split views (side-by-side or stacked) with collapsible panels. This is the Angular equivalent of shadcn/ui `ResizablePanelGroup` / `ResizablePanel` / `ResizableHandle`.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The behavior comes from **spartan-ng/brain** (`BrnResizableGroup`, `BrnResizablePanel`, `BrnResizableHandle`): pointer-drag resizing, panel size constraints, and layout events. This package is a thin styling/directive layer — a flex group container, constraint-forwarding panel directive with a `setSize` helper, and a handle with an optional grip pill.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
> **Ships as:** `@egose/shadcn-theme-ng/resizable` and `@egose/shadcn-theme-ng-tw/resizable` (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 { HlmResizableImports } from '@egose/shadcn-theme-ng/resizable';
|
|
22
|
+
// tw variant:
|
|
23
|
+
// import { HlmResizableImports } from '@egose/shadcn-theme-ng-tw/resizable';
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Peer dependencies (see [package README](../../README.md) for versions): `@angular/core`, `@angular/common`, `@spartan-ng/brain`.
|
|
27
|
+
|
|
28
|
+
## Imports
|
|
29
|
+
|
|
30
|
+
Real exported symbols (from `src/public-api.ts`):
|
|
31
|
+
|
|
32
|
+
| Symbol | Kind | Description |
|
|
33
|
+
| --------------------- | ------------- | ------------------------------------------------------------------------------------- |
|
|
34
|
+
| `HlmResizableGroup` | Directive | Flex row/column container; forwards `BrnResizableGroup` |
|
|
35
|
+
| `HlmResizablePanel` | Directive | Size-constrained panel; forwards `BrnResizablePanel`, `exportAs: 'hlmResizablePanel'` |
|
|
36
|
+
| `HlmResizableHandle` | Component | Drag handle with optional grip pill; `exportAs: 'hlmResizableHandle'` |
|
|
37
|
+
| `HlmResizableImports` | `const` array | `[HlmResizableGroup, HlmResizablePanel, HlmResizableHandle]` standalone imports |
|
|
38
|
+
| `HlmResizableModule` | `NgModule` | NgModule wrapper re-exporting the three above |
|
|
39
|
+
|
|
40
|
+
Standalone usage:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { Component } from '@angular/core';
|
|
44
|
+
import { HlmResizableImports } from '@egose/shadcn-theme-ng/resizable';
|
|
45
|
+
|
|
46
|
+
@Component({
|
|
47
|
+
selector: 'app-demo',
|
|
48
|
+
standalone: true,
|
|
49
|
+
imports: [HlmResizableImports],
|
|
50
|
+
template: `...`,
|
|
51
|
+
})
|
|
52
|
+
export class DemoComponent {}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
NgModule usage:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { NgModule } from '@angular/core';
|
|
59
|
+
import { HlmResizableModule } from '@egose/shadcn-theme-ng/resizable';
|
|
60
|
+
|
|
61
|
+
@NgModule({ imports: [HlmResizableModule] })
|
|
62
|
+
export class DemoModule {}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Anatomy / Structure
|
|
66
|
+
|
|
67
|
+
```html
|
|
68
|
+
<div hlmResizableGroup direction="horizontal">
|
|
69
|
+
<div hlmResizablePanel defaultSize="25">Sidebar</div>
|
|
70
|
+
|
|
71
|
+
<hlm-resizable-handle withHandle />
|
|
72
|
+
|
|
73
|
+
<div hlmResizablePanel defaultSize="75">Main content</div>
|
|
74
|
+
</div>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Real selectors:
|
|
78
|
+
|
|
79
|
+
| Selector | Class | Notes |
|
|
80
|
+
| -------------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------ |
|
|
81
|
+
| `[hlmResizableGroup]`, `hlm-resizable-group` | `HlmResizableGroup` | `data-slot="resizable-group"`; row by default, column when the panel-group direction is vertical |
|
|
82
|
+
| `[hlmResizablePanel]`, `hlm-resizable-panel` | `HlmResizablePanel` | `data-slot="resizable-panel"` |
|
|
83
|
+
| `hlm-resizable-handle` | `HlmResizableHandle` | `data-slot="resizable-handle"`; renders a grip `<div>` only when `withHandle` is true |
|
|
84
|
+
|
|
85
|
+
## API reference
|
|
86
|
+
|
|
87
|
+
### HlmResizableGroup (directive)
|
|
88
|
+
|
|
89
|
+
Thin directive wrapper — no own inputs. Forwarded `BrnResizableGroup` bindings:
|
|
90
|
+
|
|
91
|
+
| Binding | Kind | Description |
|
|
92
|
+
| -------------- | ------ | --------------------------------------------- |
|
|
93
|
+
| `direction` | input | `'horizontal' \| 'vertical'` layout direction |
|
|
94
|
+
| `layout` | input | Controlled panel layout (sizes) |
|
|
95
|
+
| `dragStart` | output | Emitted when a drag resize starts |
|
|
96
|
+
| `dragEnd` | output | Emitted when a drag resize ends |
|
|
97
|
+
| `layoutChange` | output | Emitted when the panel layout changes |
|
|
98
|
+
|
|
99
|
+
### HlmResizablePanel (directive)
|
|
100
|
+
|
|
101
|
+
Thin directive wrapper plus one helper method. No own inputs. Forwarded `BrnResizablePanel` inputs:
|
|
102
|
+
|
|
103
|
+
| Input | Description |
|
|
104
|
+
| ------------- | ------------------------------------------------ |
|
|
105
|
+
| `defaultSize` | Initial size (percentage of the group) |
|
|
106
|
+
| `id` | Panel id (used for persisted/controlled layouts) |
|
|
107
|
+
| `collapsible` | Whether the panel can collapse to zero |
|
|
108
|
+
| `minSize` | Minimum size (percentage) |
|
|
109
|
+
| `maxSize` | Maximum size (percentage) |
|
|
110
|
+
|
|
111
|
+
| Method | Signature | Description |
|
|
112
|
+
| --------- | ----------------------------- | --------------------------------------------------------------------------- |
|
|
113
|
+
| `setSize` | `setSize(size: number): void` | Programmatically resize the panel; delegates to `BrnResizablePanel.setSize` |
|
|
114
|
+
|
|
115
|
+
Because the directive is `exportAs: 'hlmResizablePanel'`, grab it with a template reference: `<div hlmResizablePanel #panel="hlmResizablePanel">` then call `panel.setSize(50)`.
|
|
116
|
+
|
|
117
|
+
### HlmResizableHandle (component)
|
|
118
|
+
|
|
119
|
+
| Input (forwarded to `BrnResizableHandle`) | Description |
|
|
120
|
+
| ----------------------------------------- | ------------------------------------ |
|
|
121
|
+
| `withHandle` | Show the grip pill inside the handle |
|
|
122
|
+
| `disabled` | Disable dragging on this handle |
|
|
123
|
+
|
|
124
|
+
When `withHandle` is true the component renders a short rounded pill (`<div class="...">`) centered in the handle; otherwise the handle is just the thin hover/drag strip. Hover cursors adapt to direction (`ew-resize` horizontal, `ns-resize` vertical).
|
|
125
|
+
|
|
126
|
+
## Examples
|
|
127
|
+
|
|
128
|
+
### 1. Basic horizontal split
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import { Component } from '@angular/core';
|
|
132
|
+
import { HlmResizableImports } from '@egose/shadcn-theme-ng/resizable';
|
|
133
|
+
|
|
134
|
+
@Component({
|
|
135
|
+
selector: 'app-basic-resizable',
|
|
136
|
+
standalone: true,
|
|
137
|
+
imports: [HlmResizableImports],
|
|
138
|
+
template: `
|
|
139
|
+
<div hlmResizableGroup direction="horizontal" class="tw:h-64 tw:rounded-lg tw:border">
|
|
140
|
+
<div hlmResizablePanel defaultSize="30" class="tw:p-4">Nav</div>
|
|
141
|
+
<hlm-resizable-handle />
|
|
142
|
+
<div hlmResizablePanel defaultSize="70" class="tw:p-4">Content</div>
|
|
143
|
+
</div>
|
|
144
|
+
`,
|
|
145
|
+
})
|
|
146
|
+
export class BasicResizableComponent {}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### 2. Handle with grip pill
|
|
150
|
+
|
|
151
|
+
```html
|
|
152
|
+
<div hlmResizableGroup direction="horizontal" class="tw:h-64 tw:rounded-lg tw:border">
|
|
153
|
+
<div hlmResizablePanel defaultSize="25" class="tw:p-4">Files</div>
|
|
154
|
+
|
|
155
|
+
<hlm-resizable-handle withHandle />
|
|
156
|
+
|
|
157
|
+
<div hlmResizablePanel defaultSize="75" class="tw:p-4">Editor</div>
|
|
158
|
+
</div>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 3. Vertical stacking
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { Component } from '@angular/core';
|
|
165
|
+
import { HlmResizableImports } from '@egose/shadcn-theme-ng/resizable';
|
|
166
|
+
|
|
167
|
+
@Component({
|
|
168
|
+
selector: 'app-vertical-resizable',
|
|
169
|
+
standalone: true,
|
|
170
|
+
imports: [HlmResizableImports],
|
|
171
|
+
template: `
|
|
172
|
+
<div hlmResizableGroup direction="vertical" class="tw:h-96 tw:rounded-lg tw:border">
|
|
173
|
+
<div hlmResizablePanel defaultSize="40" class="tw:p-4">Preview</div>
|
|
174
|
+
<hlm-resizable-handle withHandle />
|
|
175
|
+
<div hlmResizablePanel defaultSize="60" class="tw:p-4">Console</div>
|
|
176
|
+
</div>
|
|
177
|
+
`,
|
|
178
|
+
})
|
|
179
|
+
export class VerticalResizableComponent {}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The group flips to `flex-col` automatically for vertical direction, and the handle renders horizontally with an `ns-resize` cursor.
|
|
183
|
+
|
|
184
|
+
### 4. Constrained + collapsible panels
|
|
185
|
+
|
|
186
|
+
```html
|
|
187
|
+
<div hlmResizableGroup direction="horizontal" class="tw:h-64 tw:rounded-lg tw:border">
|
|
188
|
+
<div hlmResizablePanel defaultSize="20" minSize="10" maxSize="40" collapsible class="tw:p-4">
|
|
189
|
+
Collapsible sidebar (10–40%)
|
|
190
|
+
</div>
|
|
191
|
+
<hlm-resizable-handle withHandle />
|
|
192
|
+
<div hlmResizablePanel defaultSize="80" minSize="30" class="tw:p-4">Main (≥30%)</div>
|
|
193
|
+
</div>
|
|
11
194
|
```
|
|
195
|
+
|
|
196
|
+
### 5. Programmatic control via `setSize`
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
import { Component } from '@angular/core';
|
|
200
|
+
import { HlmResizableImports } from '@egose/shadcn-theme-ng/resizable';
|
|
201
|
+
|
|
202
|
+
@Component({
|
|
203
|
+
selector: 'app-controlled-resizable',
|
|
204
|
+
standalone: true,
|
|
205
|
+
imports: [HlmResizableImports],
|
|
206
|
+
template: `
|
|
207
|
+
<div class="tw:mb-2 tw:flex tw:gap-2">
|
|
208
|
+
<button type="button" (click)="left.setSize(20)">Sidebar 20%</button>
|
|
209
|
+
<button type="button" (click)="left.setSize(50)">Split 50/50</button>
|
|
210
|
+
</div>
|
|
211
|
+
<div hlmResizableGroup direction="horizontal" class="tw:h-64 tw:rounded-lg tw:border">
|
|
212
|
+
<div hlmResizablePanel #left="hlmResizablePanel" defaultSize="30" class="tw:p-4">Nav</div>
|
|
213
|
+
<hlm-resizable-handle withHandle />
|
|
214
|
+
<div hlmResizablePanel defaultSize="70" class="tw:p-4">Content</div>
|
|
215
|
+
</div>
|
|
216
|
+
`,
|
|
217
|
+
})
|
|
218
|
+
export class ControlledResizableComponent {}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### 6. Layout events + three-pane IDE layout
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
import { Component, signal } from '@angular/core';
|
|
225
|
+
import { HlmResizableImports } from '@egose/shadcn-theme-ng/resizable';
|
|
226
|
+
|
|
227
|
+
@Component({
|
|
228
|
+
selector: 'app-ide-resizable',
|
|
229
|
+
standalone: true,
|
|
230
|
+
imports: [HlmResizableImports],
|
|
231
|
+
template: `
|
|
232
|
+
<div
|
|
233
|
+
hlmResizableGroup
|
|
234
|
+
direction="horizontal"
|
|
235
|
+
class="tw:h-96 tw:rounded-lg tw:border"
|
|
236
|
+
(dragStart)="dragging.set(true)"
|
|
237
|
+
(dragEnd)="dragging.set(false)"
|
|
238
|
+
(layoutChange)="onLayout($event)"
|
|
239
|
+
>
|
|
240
|
+
<div hlmResizablePanel defaultSize="20" minSize="12" collapsible class="tw:p-4">Explorer</div>
|
|
241
|
+
<hlm-resizable-handle withHandle />
|
|
242
|
+
<div hlmResizablePanel defaultSize="55" minSize="30" class="tw:p-4">Editor</div>
|
|
243
|
+
<hlm-resizable-handle />
|
|
244
|
+
<div hlmResizablePanel defaultSize="25" minSize="15" collapsible class="tw:p-4">Outline</div>
|
|
245
|
+
</div>
|
|
246
|
+
@if (dragging()) {
|
|
247
|
+
<p class="tw:text-muted-foreground tw:text-sm">Resizing…</p>
|
|
248
|
+
}
|
|
249
|
+
`,
|
|
250
|
+
})
|
|
251
|
+
export class IdeResizableComponent {
|
|
252
|
+
readonly dragging = signal(false);
|
|
253
|
+
|
|
254
|
+
onLayout(layout: unknown): void {
|
|
255
|
+
console.log('panel layout:', layout);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Accessibility notes
|
|
261
|
+
|
|
262
|
+
- The drag handle is keyboard-focusable via the underlying `BrnResizableHandle` — ensure handles remain in the tab order (do not set `tabindex="-1"` on them).
|
|
263
|
+
- A handle without `withHandle` is only a 1px strip visually; prefer `withHandle` when pointer users need a bigger grab target, and keep `disabled` handles out of confusing layouts (they still occupy space).
|
|
264
|
+
- Give each region a landmark or label (`role="region"` + `aria-label`, or headings) so screen-reader users understand the pane structure, since drag-to-resize itself is pointer/keyboard driven on the handle.
|
|
265
|
+
|
|
266
|
+
## Theming / CSS variables
|
|
267
|
+
|
|
268
|
+
Class-driven only (flex direction, borders, cursors). Size the group with your own height/width utilities — panels size themselves as percentages of it.
|
|
269
|
+
|
|
270
|
+
## Related subpaths
|
|
271
|
+
|
|
272
|
+
- `@egose/shadcn-theme-ng/card` — paneled content inside resizable regions
|
|
273
|
+
- `@egose/shadcn-theme-ng/scroll-area` — scrollable overflow inside fixed-size panels
|
|
274
|
+
- `@egose/shadcn-theme-ng/sidebar` — app-level collapsible navigation (alternative to a resizable nav pane)
|
|
275
|
+
- `@egose/shadcn-theme-ng/separator` — static visual dividers where resizing is not needed
|