@egose/shadcn-theme-ng 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/README.md +2 -2
  2. package/accordion/README.md +405 -2
  3. package/alert/README.md +372 -2
  4. package/alert-dialog/README.md +471 -5
  5. package/aspect-ratio/README.md +272 -5
  6. package/autocomplete/README.md +502 -2
  7. package/autocomplete/fesm2022/autocomplete.mjs +1 -1
  8. package/avatar/README.md +357 -5
  9. package/badge/README.md +318 -2
  10. package/basic-alert/README.md +353 -2
  11. package/breadcrumb/README.md +406 -5
  12. package/button/README.md +482 -2
  13. package/button/fesm2022/button.mjs +85 -107
  14. package/button/types/button.d.ts +5 -8
  15. package/button-group/README.md +318 -5
  16. package/calendar/README.md +357 -2
  17. package/card/README.md +331 -5
  18. package/carousel/README.md +333 -5
  19. package/carousel/fesm2022/carousel.mjs +4 -1
  20. package/checkbox/README.md +320 -2
  21. package/checkbox/fesm2022/checkbox.mjs +6 -7
  22. package/checkbox/types/checkbox.d.ts +1 -1
  23. package/collapsible/README.md +332 -5
  24. package/combobox/README.md +507 -5
  25. package/combobox/fesm2022/combobox.mjs +5 -2
  26. package/command/README.md +435 -5
  27. package/confirmation-dialog/README.md +301 -2
  28. package/context-menu/README.md +366 -5
  29. package/date-picker/README.md +469 -2
  30. package/date-picker/fesm2022/date-picker.mjs +150 -32
  31. package/date-picker/types/date-picker.d.ts +102 -9
  32. package/dialog/README.md +448 -2
  33. package/drawer/README.md +395 -5
  34. package/dropdown-menu/README.md +417 -5
  35. package/empty/README.md +329 -5
  36. package/field/README.md +385 -5
  37. package/form-autocomplete/README.md +177 -0
  38. package/form-autocomplete/fesm2022/form-autocomplete.mjs +125 -0
  39. package/form-autocomplete/package.json +24 -0
  40. package/form-autocomplete/types/form-autocomplete.d.ts +61 -0
  41. package/form-checkbox/README.md +322 -2
  42. package/form-checkbox/fesm2022/form-checkbox.mjs +22 -9
  43. package/form-checkbox/types/form-checkbox.d.ts +23 -4
  44. package/form-combobox/README.md +202 -0
  45. package/form-combobox/fesm2022/form-combobox.mjs +147 -0
  46. package/form-combobox/package.json +24 -0
  47. package/form-combobox/types/form-combobox.d.ts +73 -0
  48. package/form-date-picker/README.md +348 -2
  49. package/form-date-picker/fesm2022/form-date-picker.mjs +38 -12
  50. package/form-date-picker/types/form-date-picker.d.ts +17 -1
  51. package/form-date-picker-multi/README.md +191 -0
  52. package/form-date-picker-multi/fesm2022/form-date-picker-multi.mjs +129 -0
  53. package/form-date-picker-multi/package.json +24 -0
  54. package/form-date-picker-multi/types/form-date-picker-multi.d.ts +58 -0
  55. package/form-date-range-picker/README.md +253 -0
  56. package/form-date-range-picker/fesm2022/form-date-range-picker.mjs +130 -0
  57. package/form-date-range-picker/package.json +24 -0
  58. package/form-date-range-picker/types/form-date-range-picker.d.ts +55 -0
  59. package/form-field/README.md +356 -2
  60. package/form-field-simple/README.md +340 -2
  61. package/form-input-otp/README.md +194 -0
  62. package/form-input-otp/fesm2022/form-input-otp.mjs +106 -0
  63. package/form-input-otp/package.json +24 -0
  64. package/form-input-otp/types/form-input-otp.d.ts +58 -0
  65. package/form-month-year-picker/README.md +188 -0
  66. package/form-month-year-picker/fesm2022/form-month-year-picker.mjs +125 -0
  67. package/form-month-year-picker/package.json +24 -0
  68. package/form-month-year-picker/types/form-month-year-picker.d.ts +53 -0
  69. package/form-native-select/README.md +205 -0
  70. package/form-native-select/fesm2022/form-native-select.mjs +103 -0
  71. package/form-native-select/package.json +24 -0
  72. package/form-native-select/types/form-native-select.d.ts +57 -0
  73. package/form-phone-input/README.md +188 -0
  74. package/form-phone-input/fesm2022/form-phone-input.mjs +113 -0
  75. package/form-phone-input/package.json +24 -0
  76. package/form-phone-input/types/form-phone-input.d.ts +58 -0
  77. package/form-radio-group/README.md +198 -0
  78. package/form-radio-group/fesm2022/form-radio-group.mjs +111 -0
  79. package/form-radio-group/package.json +24 -0
  80. package/form-radio-group/types/form-radio-group.d.ts +63 -0
  81. package/form-searchable-multiselect/README.md +371 -2
  82. package/form-searchable-multiselect/fesm2022/form-searchable-multiselect.mjs +19 -10
  83. package/form-searchable-multiselect/types/form-searchable-multiselect.d.ts +18 -1
  84. package/form-select/README.md +360 -2
  85. package/form-select/fesm2022/form-select.mjs +20 -11
  86. package/form-select/types/form-select.d.ts +18 -1
  87. package/form-slider/README.md +182 -0
  88. package/form-slider/fesm2022/form-slider.mjs +106 -0
  89. package/form-slider/package.json +24 -0
  90. package/form-slider/types/form-slider.d.ts +60 -0
  91. package/form-switch/README.md +173 -0
  92. package/form-switch/fesm2022/form-switch.mjs +100 -0
  93. package/form-switch/package.json +24 -0
  94. package/form-switch/types/form-switch.d.ts +50 -0
  95. package/form-text-input/README.md +381 -2
  96. package/form-text-input/fesm2022/form-text-input.mjs +19 -10
  97. package/form-text-input/types/form-text-input.d.ts +18 -1
  98. package/form-textarea/README.md +357 -2
  99. package/form-textarea/fesm2022/form-textarea.mjs +19 -10
  100. package/form-textarea/types/form-textarea.d.ts +18 -1
  101. package/form-toggle/README.md +186 -0
  102. package/form-toggle/fesm2022/form-toggle.mjs +159 -0
  103. package/form-toggle/package.json +24 -0
  104. package/form-toggle/types/form-toggle.d.ts +82 -0
  105. package/form-toggle-group/README.md +176 -0
  106. package/form-toggle-group/fesm2022/form-toggle-group.mjs +116 -0
  107. package/form-toggle-group/package.json +24 -0
  108. package/form-toggle-group/types/form-toggle-group.d.ts +65 -0
  109. package/hover-card/README.md +256 -5
  110. package/icon/README.md +239 -2
  111. package/input/README.md +269 -2
  112. package/input-group/README.md +335 -5
  113. package/input-group/fesm2022/input-group.mjs +22 -12
  114. package/input-group/types/input-group.d.ts +4 -1
  115. package/input-otp/README.md +375 -5
  116. package/item/README.md +385 -5
  117. package/item/fesm2022/item.mjs +3 -3
  118. package/kbd/README.md +291 -5
  119. package/label/README.md +272 -2
  120. package/layout-simple/README.md +193 -2
  121. package/layout-simple/fesm2022/layout-simple.mjs +472 -236
  122. package/layout-simple/types/layout-simple.d.ts +174 -137
  123. package/menu/README.md +417 -2
  124. package/menubar/README.md +343 -5
  125. package/native-select/README.md +323 -5
  126. package/native-select/fesm2022/native-select.mjs +18 -6
  127. package/native-select/types/native-select.d.ts +7 -2
  128. package/navigation-menu/README.md +369 -5
  129. package/package.json +57 -1
  130. package/pagination/README.md +388 -5
  131. package/phone-input/README.md +114 -0
  132. package/phone-input/fesm2022/phone-input.mjs +191 -0
  133. package/phone-input/package.json +24 -0
  134. package/phone-input/types/phone-input.d.ts +67 -0
  135. package/popover/README.md +331 -2
  136. package/progress/README.md +311 -5
  137. package/radio-group/README.md +364 -2
  138. package/radio-group/fesm2022/radio-group.mjs +5 -1
  139. package/resizable/README.md +269 -5
  140. package/scroll-area/README.md +233 -5
  141. package/searchable-multiselect/README.md +323 -2
  142. package/select/README.md +437 -2
  143. package/separator/README.md +222 -2
  144. package/sheet/README.md +311 -2
  145. package/sidebar/README.md +457 -5
  146. package/skeleton/README.md +217 -5
  147. package/slider/README.md +273 -5
  148. package/slider/fesm2022/slider.mjs +3 -3
  149. package/sonner/README.md +346 -2
  150. package/spinner/README.md +284 -2
  151. package/switch/README.md +310 -2
  152. package/switch/fesm2022/switch.mjs +7 -5
  153. package/switch/types/switch.d.ts +2 -1
  154. package/table/README.md +423 -5
  155. package/tabs/README.md +411 -2
  156. package/tabs/fesm2022/tabs.mjs +2 -2
  157. package/textarea/README.md +282 -5
  158. package/toggle/README.md +270 -5
  159. package/toggle-group/README.md +340 -5
  160. package/tooltip/README.md +269 -2
  161. package/typography/README.md +271 -5
  162. package/utils/README.md +303 -2
@@ -1,11 +1,317 @@
1
- # Progress
1
+ # Progress (`@egose/shadcn-theme-ng/progress`)
2
2
 
3
- This project was generated using [Angular CLI](https://github.com/angular/angular-cli).
3
+ Determinate/indeterminate progress bar (shadcn/ui `progress` equivalent). Thin shadcn styling directives over the spartan-ng `BrnProgress` brain family: `hlm-progress` owns the track + value semantics (`value`, `max`, `getValueLabel`), and the inner `hlmProgressIndicator` bar positions itself from the brain value with RTL awareness and an indeterminate animation state.
4
4
 
5
- ## Building
5
+ Ships as `@egose/shadcn-theme-ng/progress` and `@egose/shadcn-theme-ng-tw/progress` (tw: variant). See the [package README](../../README.md) for installation, peer dependencies, Tailwind setup, and testing. Do not publish this project directory independently.
6
6
 
7
- To build the library, run:
7
+ ## Installation
8
8
 
9
9
  ```bash
10
- ng build progress
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
+ Peer dependencies are inherited from the package root (see [package README](../../README.md)). This subpath itself declares `@angular/common`, `@angular/core`, `@spartan-ng/brain` as peers plus a `tslib` runtime dependency; at runtime the indicator also reads Angular CDK `Directionality` for RTL. No extra install step is needed beyond the package install above.
18
+
19
+ ## Imports
20
+
21
+ Real exported symbols (from `src/public-api.ts`):
22
+
23
+ ```ts
24
+ import {
25
+ HlmProgress, // directive: hlm-progress,[hlmProgress]
26
+ HlmProgressIndicator, // directive: [hlmProgressIndicator],hlm-progress-indicator
27
+ HlmProgressImports, // readonly [HlmProgress, HlmProgressIndicator]
28
+ HlmProgressModule, // NgModule wrapping HlmProgressImports
29
+ } from '@egose/shadcn-theme-ng/progress';
30
+ ```
31
+
32
+ Standalone usage:
33
+
34
+ ```ts
35
+ import { Component } from '@angular/core';
36
+ import { HlmProgressImports } from '@egose/shadcn-theme-ng/progress';
37
+
38
+ @Component({
39
+ selector: 'app-demo',
40
+ standalone: true,
41
+ imports: [...HlmProgressImports],
42
+ template: `
43
+ <hlm-progress [value]="40" [max]="100">
44
+ <hlm-progress-indicator hlmProgressIndicator />
45
+ </hlm-progress>
46
+ `,
47
+ })
48
+ export class DemoComponent {}
49
+ ```
50
+
51
+ NgModule usage:
52
+
53
+ ```ts
54
+ import { NgModule } from '@angular/core';
55
+ import { HlmProgressModule } from '@egose/shadcn-theme-ng/progress';
56
+
57
+ @NgModule({ imports: [HlmProgressModule] })
58
+ export class DemoModule {}
59
+ ```
60
+
61
+ For the `tw:` build, swap the specifier to `@egose/shadcn-theme-ng-tw/progress`. Symbol names are identical.
62
+
63
+ ## Anatomy / Structure
64
+
65
+ ```html
66
+ <!-- determinate -->
67
+ <hlm-progress [value]="value()" [max]="100">
68
+ <hlm-progress-indicator hlmProgressIndicator />
69
+ </hlm-progress>
70
+
71
+ <!-- attribute-selector form -->
72
+ <div hlmProgress [value]="40" [max]="100">
73
+ <div hlmProgressIndicator></div>
74
+ </div>
75
+
76
+ <!-- indeterminate (no value) -->
77
+ <hlm-progress>
78
+ <hlm-progress-indicator hlmProgressIndicator />
79
+ </hlm-progress>
80
+ ```
81
+
82
+ | Class | Selector | Role |
83
+ | ---------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------- |
84
+ | `HlmProgress` | `hlm-progress,[hlmProgress]` | Track (`BrnProgress` host: `value`, `max`, `getValueLabel`); `data-slot="progress"` |
85
+ | `HlmProgressIndicator` | `[hlmProgressIndicator],hlm-progress-indicator` | Fill bar (`BrnProgressIndicator` host); `data-slot="progress-indicator"` |
86
+
87
+ The indicator computes `translateX(-offset%)` from `100 - value` (defaulting `null`/`undefined` value to `100` for the offset math) and flips the sign in RTL via CDK `Directionality`. When the brain value is `null`/`undefined` it adds the `animate-indeterminate` class instead of a static fill.
88
+
89
+ ## API reference
90
+
91
+ ### `HlmProgress` (`hlm-progress,[hlmProgress]`)
92
+
93
+ | Member | Kind | Type | Notes |
94
+ | --------------- | ------------------------- | ----------------------------- | ------------------------------------------------------------ |
95
+ | `value` | input (via `BrnProgress`) | `number \| null \| undefined` | Current value; `null`/`undefined` → indeterminate |
96
+ | `max` | input (via `BrnProgress`) | `number` | Scale maximum (commonly `100`) |
97
+ | `getValueLabel` | input (via `BrnProgress`) | `(value, max) => string` | Accessible value-text factory (see brain docs for signature) |
98
+
99
+ No shadcn inputs of its own. Track styling: `bg-muted h-1.5 rounded-full relative inline-flex w-full overflow-hidden`.
100
+
101
+ ### `HlmProgressIndicator` (`[hlmProgressIndicator],hlm-progress-indicator`)
102
+
103
+ No public inputs/outputs/methods. Internals (protected, for understanding only): `_transform` (computed `translateX()`), `_indeterminate` (computed `value == null`). Fill styling: `bg-primary h-full w-full flex-1 transition-all`; indeterminate state toggles `animate-indeterminate`.
104
+
105
+ ## Examples
106
+
107
+ ### 1. Basic determinate bar
108
+
109
+ ```ts
110
+ import { Component, signal } from '@angular/core';
111
+ import { HlmProgressImports } from '@egose/shadcn-theme-ng/progress';
112
+
113
+ @Component({
114
+ selector: 'app-progress-basic',
115
+ standalone: true,
116
+ imports: [...HlmProgressImports],
117
+ template: `
118
+ <hlm-progress [value]="value()" [max]="100">
119
+ <hlm-progress-indicator hlmProgressIndicator />
120
+ </hlm-progress>
121
+ <p class="tw:text-sm tw:text-muted-foreground">{{ value() }}%</p>
122
+ <button type="button" (click)="value.set(Math.min(100, value() + 10))">+10</button>
123
+ `,
124
+ })
125
+ export class ProgressBasicComponent {
126
+ readonly value = signal(40);
127
+ protected readonly Math = Math;
128
+ }
129
+ ```
130
+
131
+ ### 2. Timer / simulated upload
132
+
133
+ ```ts
134
+ import { Component, signal, OnDestroy } from '@angular/core';
135
+ import { HlmProgressImports } from '@egose/shadcn-theme-ng/progress';
136
+
137
+ @Component({
138
+ selector: 'app-progress-timer',
139
+ standalone: true,
140
+ imports: [...HlmProgressImports],
141
+ template: `
142
+ <hlm-progress [value]="progress()" [max]="100">
143
+ <hlm-progress-indicator hlmProgressIndicator />
144
+ </hlm-progress>
145
+ <div class="tw:flex tw:gap-2">
146
+ <button type="button" (click)="start()">Start</button>
147
+ <button type="button" (click)="reset()">Reset</button>
148
+ </div>
149
+ `,
150
+ })
151
+ export class ProgressTimerComponent implements OnDestroy {
152
+ readonly progress = signal(0);
153
+ private timer: ReturnType<typeof setInterval> | undefined;
154
+
155
+ start(): void {
156
+ this.stop();
157
+ this.timer = setInterval(() => {
158
+ this.progress.update((v) => (v >= 100 ? 100 : v + 2));
159
+ if (this.progress() >= 100) this.stop();
160
+ }, 100);
161
+ }
162
+
163
+ reset(): void {
164
+ this.stop();
165
+ this.progress.set(0);
166
+ }
167
+
168
+ ngOnDestroy(): void {
169
+ this.stop();
170
+ }
171
+
172
+ private stop(): void {
173
+ if (this.timer) clearInterval(this.timer);
174
+ this.timer = undefined;
175
+ }
176
+ }
177
+ ```
178
+
179
+ ### 3. Indeterminate (unknown duration)
180
+
181
+ Omit `value` entirely — the indicator switches to the `animate-indeterminate` treatment.
182
+
183
+ ```ts
184
+ import { Component, signal } from '@angular/core';
185
+ import { HlmProgressImports } from '@egose/shadcn-theme-ng/progress';
186
+
187
+ @Component({
188
+ selector: 'app-progress-indeterminate',
189
+ standalone: true,
190
+ imports: [...HlmProgressImports],
191
+ template: `
192
+ <button type="button" (click)="load()">Fetch report</button>
193
+ @if (loading()) {
194
+ <hlm-progress aria-label="Loading report">
195
+ <hlm-progress-indicator hlmProgressIndicator />
196
+ </hlm-progress>
197
+ }
198
+ `,
199
+ })
200
+ export class ProgressIndeterminateComponent {
201
+ readonly loading = signal(false);
202
+
203
+ async load(): Promise<void> {
204
+ this.loading.set(true);
205
+ await new Promise((r) => setTimeout(r, 1500));
206
+ this.loading.set(false);
207
+ }
208
+ }
11
209
  ```
210
+
211
+ ### 4. Custom scale (`max !== 100`) + accessible label
212
+
213
+ ```ts
214
+ import { Component, signal } from '@angular/core';
215
+ import { HlmProgressImports } from '@egose/shadcn-theme-ng/progress';
216
+
217
+ @Component({
218
+ selector: 'app-progress-scale',
219
+ standalone: true,
220
+ imports: [...HlmProgressImports],
221
+ template: `
222
+ <hlm-progress [value]="done()" [max]="total()" [getValueLabel]="label" aria-label="Migration progress">
223
+ <hlm-progress-indicator hlmProgressIndicator />
224
+ </hlm-progress>
225
+ <p class="tw:text-sm">{{ done() }} of {{ total() }} rows migrated</p>
226
+ `,
227
+ })
228
+ export class ProgressScaleComponent {
229
+ readonly done = signal(37);
230
+ readonly total = signal(200);
231
+
232
+ readonly label = (value: number | null | undefined, max: number): string => `${value ?? 0} of ${max} rows`;
233
+ }
234
+ ```
235
+
236
+ > `getValueLabel` is the brain hook for the `aria-valuetext`; keep the visible text in sync (as above) so sighted and SR users agree.
237
+
238
+ ### 5. Multi-step wizard
239
+
240
+ ```ts
241
+ import { Component, signal, computed } from '@angular/core';
242
+ import { HlmProgressImports } from '@egose/shadcn-theme-ng/progress';
243
+
244
+ @Component({
245
+ selector: 'app-progress-steps',
246
+ standalone: true,
247
+ imports: [...HlmProgressImports],
248
+ template: `
249
+ <hlm-progress [value]="step()" [max]="steps.length">
250
+ <hlm-progress-indicator hlmProgressIndicator />
251
+ </hlm-progress>
252
+ <p class="tw:text-sm">Step {{ step() }} of {{ steps.length }}: {{ steps[step() - 1] }}</p>
253
+ <div class="tw:flex tw:gap-2">
254
+ <button type="button" (click)="prev()" [disabled]="step() <= 1">Back</button>
255
+ <button type="button" (click)="next()" [disabled]="step() >= steps.length">Next</button>
256
+ </div>
257
+ `,
258
+ })
259
+ export class ProgressStepsComponent {
260
+ readonly steps = ['Account', 'Profile', 'Confirm'];
261
+ readonly step = signal(1);
262
+
263
+ prev(): void {
264
+ this.step.update((s) => Math.max(1, s - 1));
265
+ }
266
+ next(): void {
267
+ this.step.update((s) => Math.min(this.steps.length, s + 1));
268
+ }
269
+ }
270
+ ```
271
+
272
+ ### 6. Error / complete states and NgModule form
273
+
274
+ ```ts
275
+ import { NgModule, Component, signal, computed } from '@angular/core';
276
+ import { HlmProgressImports, HlmProgressModule } from '@egose/shadcn-theme-ng/progress';
277
+
278
+ @Component({
279
+ selector: 'app-progress-states',
280
+ standalone: true,
281
+ imports: [...HlmProgressImports],
282
+ template: `
283
+ <hlm-progress [value]="value()" [max]="100" [class]="barClass()">
284
+ <hlm-progress-indicator hlmProgressIndicator />
285
+ </hlm-progress>
286
+ <p class="tw:text-sm" [class.tw:text-destructive]="failed()">
287
+ {{ failed() ? 'Upload failed — retrying…' : value() >= 100 ? 'Complete' : 'Uploading…' }}
288
+ </p>
289
+ `,
290
+ })
291
+ export class ProgressStatesComponent {
292
+ readonly value = signal(75);
293
+ readonly failed = signal(false);
294
+ readonly barClass = computed(() => (this.failed() ? 'tw:[&>[data-slot=progress-indicator]]:tw:bg-destructive' : ''));
295
+ }
296
+
297
+ @NgModule({ imports: [HlmProgressModule] })
298
+ export class ProgressLegacyModule {}
299
+ ```
300
+
301
+ > There is no `state`/`variant` input — success/error tints are done with plain `class` overrides targeting the indicator (as above), since the indicator carries `data-slot="progress-indicator"`.
302
+
303
+ ## Accessibility notes
304
+
305
+ - The brain `BrnProgress` exposes `role="progressbar"` with `aria-valuemin`/`max`/`now` (plus `aria-valuetext` via `getValueLabel`) — always provide `aria-label`/`aria-labelledby` unless surrounding text already names the bar.
306
+ - Indeterminate bars (no `value`) must still be labelled ("Loading report") so SR users know what is pending; pair with status text or an `aria-live` region for completion.
307
+ - Don't use progress as the _only_ conveyor of state — mirror percent/steps in text (examples 1/4/5).
308
+ - Color overrides (example 6) are decorative; keep the text label as the source of truth for error/complete states.
309
+
310
+ ## Theming / CSS variables
311
+
312
+ No theming inputs. Track is `bg-muted`, fill is `bg-primary`; both follow shadcn tokens automatically. Tint the fill per-instance with a `class` override on the track targeting `[data-slot=progress-indicator]`.
313
+
314
+ ## Related subpaths
315
+
316
+ - `@egose/shadcn-theme-ng/skeleton`, `@egose/shadcn-theme-ng/spinner` — alternative loading indicators (skeleton screens, spinners) vs this determinate bar.
317
+ - `@egose/shadcn-theme-ng/sonner` — toast completion notices to pair with a finished upload.
@@ -1,3 +1,365 @@
1
- # Radio Group Subpath
1
+ # Radio Group (`@egose/shadcn-theme-ng/radio-group`)
2
2
 
3
- This project ships only as `@egose/shadcn-theme-ng/radio-group` or `@egose/shadcn-theme-ng-tw/radio-group`. See the [package README](../../README.md) for installation, compatibility, Tailwind variant, test, and release guidance. Do not publish this project directory independently.
3
+ A shadcn/ui-style **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