ngx-com 0.0.1 → 0.0.4

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 (82) hide show
  1. package/fesm2022/ngx-com-components-avatar.mjs +772 -0
  2. package/fesm2022/ngx-com-components-avatar.mjs.map +1 -0
  3. package/fesm2022/ngx-com-components-badge.mjs +138 -0
  4. package/fesm2022/ngx-com-components-badge.mjs.map +1 -0
  5. package/fesm2022/ngx-com-components-button.mjs +146 -0
  6. package/fesm2022/ngx-com-components-button.mjs.map +1 -0
  7. package/fesm2022/ngx-com-components-calendar.mjs +5046 -0
  8. package/fesm2022/ngx-com-components-calendar.mjs.map +1 -0
  9. package/fesm2022/ngx-com-components-card.mjs +590 -0
  10. package/fesm2022/ngx-com-components-card.mjs.map +1 -0
  11. package/fesm2022/ngx-com-components-checkbox.mjs +344 -0
  12. package/fesm2022/ngx-com-components-checkbox.mjs.map +1 -0
  13. package/fesm2022/ngx-com-components-collapsible.mjs +612 -0
  14. package/fesm2022/ngx-com-components-collapsible.mjs.map +1 -0
  15. package/fesm2022/ngx-com-components-confirm.mjs +562 -0
  16. package/fesm2022/ngx-com-components-confirm.mjs.map +1 -0
  17. package/fesm2022/ngx-com-components-dropdown-testing.mjs +255 -0
  18. package/fesm2022/ngx-com-components-dropdown-testing.mjs.map +1 -0
  19. package/fesm2022/ngx-com-components-dropdown.mjs +2692 -0
  20. package/fesm2022/ngx-com-components-dropdown.mjs.map +1 -0
  21. package/fesm2022/ngx-com-components-empty-state.mjs +382 -0
  22. package/fesm2022/ngx-com-components-empty-state.mjs.map +1 -0
  23. package/fesm2022/ngx-com-components-form-field.mjs +924 -0
  24. package/fesm2022/ngx-com-components-form-field.mjs.map +1 -0
  25. package/fesm2022/ngx-com-components-icon.mjs +183 -0
  26. package/fesm2022/ngx-com-components-icon.mjs.map +1 -0
  27. package/fesm2022/ngx-com-components-item.mjs +578 -0
  28. package/fesm2022/ngx-com-components-item.mjs.map +1 -0
  29. package/fesm2022/ngx-com-components-menu.mjs +1200 -0
  30. package/fesm2022/ngx-com-components-menu.mjs.map +1 -0
  31. package/fesm2022/ngx-com-components-paginator.mjs +823 -0
  32. package/fesm2022/ngx-com-components-paginator.mjs.map +1 -0
  33. package/fesm2022/ngx-com-components-popover.mjs +901 -0
  34. package/fesm2022/ngx-com-components-popover.mjs.map +1 -0
  35. package/fesm2022/ngx-com-components-radio.mjs +621 -0
  36. package/fesm2022/ngx-com-components-radio.mjs.map +1 -0
  37. package/fesm2022/ngx-com-components-segmented-control.mjs +538 -0
  38. package/fesm2022/ngx-com-components-segmented-control.mjs.map +1 -0
  39. package/fesm2022/ngx-com-components-sort.mjs +368 -0
  40. package/fesm2022/ngx-com-components-sort.mjs.map +1 -0
  41. package/fesm2022/ngx-com-components-spinner.mjs +189 -0
  42. package/fesm2022/ngx-com-components-spinner.mjs.map +1 -0
  43. package/fesm2022/ngx-com-components-tabs.mjs +1522 -0
  44. package/fesm2022/ngx-com-components-tabs.mjs.map +1 -0
  45. package/fesm2022/ngx-com-components-tooltip.mjs +625 -0
  46. package/fesm2022/ngx-com-components-tooltip.mjs.map +1 -0
  47. package/fesm2022/ngx-com-components.mjs +17 -0
  48. package/fesm2022/ngx-com-components.mjs.map +1 -0
  49. package/fesm2022/ngx-com-tokens.mjs +12 -0
  50. package/fesm2022/ngx-com-tokens.mjs.map +1 -0
  51. package/fesm2022/ngx-com-utils.mjs +601 -0
  52. package/fesm2022/ngx-com-utils.mjs.map +1 -0
  53. package/fesm2022/ngx-com.mjs +9 -23
  54. package/fesm2022/ngx-com.mjs.map +1 -1
  55. package/package.json +105 -1
  56. package/types/ngx-com-components-avatar.d.ts +409 -0
  57. package/types/ngx-com-components-badge.d.ts +97 -0
  58. package/types/ngx-com-components-button.d.ts +69 -0
  59. package/types/ngx-com-components-calendar.d.ts +1665 -0
  60. package/types/ngx-com-components-card.d.ts +373 -0
  61. package/types/ngx-com-components-checkbox.d.ts +116 -0
  62. package/types/ngx-com-components-collapsible.d.ts +379 -0
  63. package/types/ngx-com-components-confirm.d.ts +160 -0
  64. package/types/ngx-com-components-dropdown-testing.d.ts +116 -0
  65. package/types/ngx-com-components-dropdown.d.ts +938 -0
  66. package/types/ngx-com-components-empty-state.d.ts +269 -0
  67. package/types/ngx-com-components-form-field.d.ts +531 -0
  68. package/types/ngx-com-components-icon.d.ts +94 -0
  69. package/types/ngx-com-components-item.d.ts +336 -0
  70. package/types/ngx-com-components-menu.d.ts +479 -0
  71. package/types/ngx-com-components-paginator.d.ts +265 -0
  72. package/types/ngx-com-components-popover.d.ts +309 -0
  73. package/types/ngx-com-components-radio.d.ts +258 -0
  74. package/types/ngx-com-components-segmented-control.d.ts +274 -0
  75. package/types/ngx-com-components-sort.d.ts +133 -0
  76. package/types/ngx-com-components-spinner.d.ts +120 -0
  77. package/types/ngx-com-components-tabs.d.ts +396 -0
  78. package/types/ngx-com-components-tooltip.d.ts +200 -0
  79. package/types/ngx-com-components.d.ts +12 -0
  80. package/types/ngx-com-tokens.d.ts +7 -0
  81. package/types/ngx-com-utils.d.ts +424 -0
  82. package/types/ngx-com.d.ts +10 -7
@@ -0,0 +1,621 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, inject, signal, input, model, booleanAttribute, output, linkedSignal, computed, ViewEncapsulation, ChangeDetectionStrategy, Component, DestroyRef, viewChild } from '@angular/core';
3
+ import { cva } from 'class-variance-authority';
4
+ import { NgControl, NgForm, FormGroupDirective } from '@angular/forms';
5
+ import { ErrorStateMatcher } from 'ngx-com/components/form-field';
6
+
7
+ /**
8
+ * CVA variants for the visual radio circle.
9
+ *
10
+ * Uses `peer` selectors to style based on native input state:
11
+ * - `peer-checked:` for checked state
12
+ * - `peer-focus-visible:` for keyboard focus
13
+ * - `peer-disabled:` for disabled state
14
+ *
15
+ * @tokens `--color-border`, `--color-primary`, `--color-primary-hover`,
16
+ * `--color-accent`, `--color-accent-hover`,
17
+ * `--color-warn`, `--color-warn-hover`,
18
+ * `--color-disabled`, `--color-ring`
19
+ */
20
+ const radioCircleVariants = cva([
21
+ 'com-radio__circle',
22
+ 'inline-flex shrink-0 items-center justify-center',
23
+ 'rounded-full border-2 border-border',
24
+ 'transition-colors duration-150',
25
+ 'peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-ring',
26
+ 'peer-disabled:cursor-not-allowed peer-disabled:border-disabled peer-disabled:bg-disabled',
27
+ ], {
28
+ variants: {
29
+ variant: {
30
+ primary: [
31
+ 'peer-checked:border-primary peer-checked:bg-primary peer-checked:text-primary-foreground',
32
+ 'group-hover:border-primary-hover',
33
+ 'peer-checked:group-hover:bg-primary-hover peer-checked:group-hover:border-primary-hover',
34
+ ],
35
+ accent: [
36
+ 'peer-checked:border-accent peer-checked:bg-accent peer-checked:text-accent-foreground',
37
+ 'group-hover:border-accent-hover',
38
+ 'peer-checked:group-hover:bg-accent-hover peer-checked:group-hover:border-accent-hover',
39
+ ],
40
+ warn: [
41
+ 'peer-checked:border-warn peer-checked:bg-warn peer-checked:text-warn-foreground',
42
+ 'group-hover:border-warn-hover',
43
+ 'peer-checked:group-hover:bg-warn-hover peer-checked:group-hover:border-warn-hover',
44
+ ],
45
+ },
46
+ size: {
47
+ sm: 'size-4',
48
+ md: 'size-5',
49
+ lg: 'size-6',
50
+ },
51
+ },
52
+ defaultVariants: {
53
+ variant: 'primary',
54
+ size: 'md',
55
+ },
56
+ });
57
+ /** Size-based classes for the inner dot indicator. */
58
+ const RADIO_DOT_SIZES = {
59
+ sm: 'size-1.5',
60
+ md: 'size-2',
61
+ lg: 'size-2.5',
62
+ };
63
+ /** Size-based classes for the label content. */
64
+ const RADIO_LABEL_SIZES = {
65
+ sm: 'text-sm ms-2',
66
+ md: 'text-base ms-2.5',
67
+ lg: 'text-lg ms-3',
68
+ };
69
+ /** Base classes for the radio group container. */
70
+ const RADIO_GROUP_BASE = 'com-radio-group__container flex';
71
+ /** Orientation-based classes for the radio group container. */
72
+ const RADIO_GROUP_ORIENTATIONS = {
73
+ vertical: `${RADIO_GROUP_BASE} flex-col gap-2`,
74
+ horizontal: `${RADIO_GROUP_BASE} flex-row flex-wrap gap-4`,
75
+ };
76
+
77
+ /** Auto-incrementing ID counter for unique radio IDs. */
78
+ let nextRadioId = 0;
79
+ /** Generates a unique radio ID. */
80
+ function generateRadioId() {
81
+ return `com-radio-${nextRadioId++}`;
82
+ }
83
+ /** Auto-incrementing ID counter for unique radio group IDs. */
84
+ let nextGroupId = 0;
85
+ /** Generates a unique radio group ID. */
86
+ function generateRadioGroupId() {
87
+ return `com-radio-group-${nextGroupId++}`;
88
+ }
89
+
90
+ /** Injection token for radio group context. */
91
+ const COM_RADIO_GROUP = new InjectionToken('COM_RADIO_GROUP');
92
+ /**
93
+ * Radio group component that manages a set of radio buttons.
94
+ *
95
+ * Provides mutual exclusion, shared name, and roving tabindex keyboard navigation.
96
+ * Implements `ControlValueAccessor` for Reactive Forms integration.
97
+ *
98
+ * @tokens `--color-border`, `--color-primary`, `--color-primary-foreground`, `--color-primary-hover`,
99
+ * `--color-accent`, `--color-accent-foreground`, `--color-accent-hover`,
100
+ * `--color-warn`, `--color-warn-foreground`, `--color-warn-hover`,
101
+ * `--color-disabled`, `--color-disabled-foreground`, `--color-ring`
102
+ *
103
+ * @example Basic usage
104
+ * ```html
105
+ * <com-radio-group [(value)]="selectedFruit" aria-label="Select a fruit">
106
+ * <com-radio value="apple">Apple</com-radio>
107
+ * <com-radio value="banana">Banana</com-radio>
108
+ * <com-radio value="cherry">Cherry</com-radio>
109
+ * </com-radio-group>
110
+ * ```
111
+ *
112
+ * @example With reactive forms
113
+ * ```html
114
+ * <com-radio-group formControlName="size" aria-label="Select size">
115
+ * <com-radio value="sm">Small</com-radio>
116
+ * <com-radio value="md">Medium</com-radio>
117
+ * <com-radio value="lg">Large</com-radio>
118
+ * </com-radio-group>
119
+ * ```
120
+ *
121
+ * @example Horizontal orientation
122
+ * ```html
123
+ * <com-radio-group [(value)]="color" orientation="horizontal">
124
+ * <com-radio value="red">Red</com-radio>
125
+ * <com-radio value="green">Green</com-radio>
126
+ * <com-radio value="blue">Blue</com-radio>
127
+ * </com-radio-group>
128
+ * ```
129
+ *
130
+ * @example With variants
131
+ * ```html
132
+ * <com-radio-group [(value)]="priority" variant="warn" size="lg">
133
+ * <com-radio value="low">Low</com-radio>
134
+ * <com-radio value="medium">Medium</com-radio>
135
+ * <com-radio value="high">High</com-radio>
136
+ * </com-radio-group>
137
+ * ```
138
+ */
139
+ class ComRadioGroup {
140
+ /** Optional NgControl for reactive forms integration. */
141
+ ngControl = inject(NgControl, { optional: true, self: true });
142
+ /** Error state matcher for determining when to show validation errors. */
143
+ defaultErrorStateMatcher = inject(ErrorStateMatcher);
144
+ parentForm = inject(NgForm, { optional: true });
145
+ parentFormGroup = inject(FormGroupDirective, { optional: true });
146
+ /** Unique ID for this radio group instance. */
147
+ uniqueId = generateRadioGroupId();
148
+ /** ID for the error message element. */
149
+ errorId = `${this.uniqueId}-error`;
150
+ /** Registered radio items. */
151
+ registeredRadios = signal([], ...(ngDevMode ? [{ debugName: "registeredRadios" }] : []));
152
+ // Inputs
153
+ name = input(this.uniqueId, ...(ngDevMode ? [{ debugName: "name" }] : []));
154
+ value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
155
+ disabled = model(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
156
+ required = input(false, { ...(ngDevMode ? { debugName: "required" } : {}), transform: booleanAttribute });
157
+ orientation = input('vertical', ...(ngDevMode ? [{ debugName: "orientation" }] : []));
158
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : []));
159
+ variant = input('primary', ...(ngDevMode ? [{ debugName: "variant" }] : []));
160
+ errorMessage = input('', ...(ngDevMode ? [{ debugName: "errorMessage" }] : []));
161
+ errorStateMatcher = input(...(ngDevMode ? [undefined, { debugName: "errorStateMatcher" }] : []));
162
+ /** Internal signal to track when control is touched, used to trigger error state re-evaluation. */
163
+ _touched = signal(false, ...(ngDevMode ? [{ debugName: "_touched" }] : []));
164
+ ariaLabel = input(null, { ...(ngDevMode ? { debugName: "ariaLabel" } : {}), alias: 'aria-label' });
165
+ ariaLabelledby = input(null, { ...(ngDevMode ? { debugName: "ariaLabelledby" } : {}), alias: 'aria-labelledby' });
166
+ ariaDescribedby = input(null, { ...(ngDevMode ? { debugName: "ariaDescribedby" } : {}), alias: 'aria-describedby' });
167
+ // Outputs
168
+ /** Emits when the selection changes, with full event details. */
169
+ selectionChange = output();
170
+ /**
171
+ * Tracks the currently focused radio value for roving tabindex.
172
+ * Resets to the current selection (or first focusable) when value or radios change.
173
+ */
174
+ focusedValueSignal = linkedSignal({ ...(ngDevMode ? { debugName: "focusedValueSignal" } : {}), source: () => ({ value: this.value(), radios: this.registeredRadios() }),
175
+ computation: ({ value, radios }) => {
176
+ if (value && radios.some((r) => r.value() === value && !r.isDisabled())) {
177
+ return value;
178
+ }
179
+ const firstFocusable = radios.find((r) => !r.isDisabled());
180
+ return firstFocusable?.value() ?? null;
181
+ } });
182
+ // Computed
183
+ /**
184
+ * Computed error state derived from form validation.
185
+ * Shows errors when control is invalid and touched/submitted.
186
+ */
187
+ errorState = computed(() => {
188
+ // Read _touched to trigger re-evaluation when touched changes
189
+ this._touched();
190
+ const matcher = this.errorStateMatcher() ?? this.defaultErrorStateMatcher;
191
+ const form = this.parentFormGroup ?? this.parentForm;
192
+ return matcher.isErrorState(this.ngControl?.control ?? null, form);
193
+ }, ...(ngDevMode ? [{ debugName: "errorState" }] : []));
194
+ computedAriaDescribedby = computed(() => {
195
+ const userDescribedby = this.ariaDescribedby();
196
+ if (this.errorState() && this.errorMessage()) {
197
+ return userDescribedby ? `${userDescribedby} ${this.errorId}` : this.errorId;
198
+ }
199
+ return userDescribedby;
200
+ }, ...(ngDevMode ? [{ debugName: "computedAriaDescribedby" }] : []));
201
+ groupClasses = computed(() => RADIO_GROUP_ORIENTATIONS[this.orientation()], ...(ngDevMode ? [{ debugName: "groupClasses" }] : []));
202
+ // CVA callbacks
203
+ onChange = () => { };
204
+ onTouchedCallback = () => { };
205
+ constructor() {
206
+ if (this.ngControl) {
207
+ this.ngControl.valueAccessor = this;
208
+ }
209
+ }
210
+ /** Creates the context object for child radios. */
211
+ createContext() {
212
+ return {
213
+ name: this.name,
214
+ value: this.value,
215
+ disabled: this.disabled,
216
+ size: this.size,
217
+ variant: this.variant,
218
+ orientation: this.orientation,
219
+ focusedValue: this.focusedValueSignal,
220
+ select: this.select.bind(this),
221
+ focusNext: this.focusNext.bind(this),
222
+ focusPrevious: this.focusPrevious.bind(this),
223
+ register: this.register.bind(this),
224
+ unregister: this.unregister.bind(this),
225
+ onTouched: () => this.onTouchedCallback(),
226
+ };
227
+ }
228
+ /** Register a radio item with the group. */
229
+ register(radio) {
230
+ this.registeredRadios.update((radios) => [...radios, radio]);
231
+ }
232
+ /** Unregister a radio item from the group. */
233
+ unregister(radio) {
234
+ this.registeredRadios.update((radios) => radios.filter((r) => r !== radio));
235
+ }
236
+ // ControlValueAccessor implementation
237
+ writeValue(value) {
238
+ this.value.set(value);
239
+ }
240
+ registerOnChange(fn) {
241
+ this.onChange = fn;
242
+ }
243
+ registerOnTouched(fn) {
244
+ this.onTouchedCallback = () => {
245
+ this._touched.set(true);
246
+ fn();
247
+ };
248
+ }
249
+ setDisabledState(isDisabled) {
250
+ this.disabled.set(isDisabled);
251
+ }
252
+ // Public API
253
+ /** Selects a radio by value. */
254
+ select(newValue) {
255
+ if (this.disabled()) {
256
+ return;
257
+ }
258
+ this.value.set(newValue);
259
+ this.focusedValueSignal.set(newValue);
260
+ this.onChange(newValue);
261
+ this.selectionChange.emit({ value: newValue });
262
+ }
263
+ /** Focuses the next non-disabled radio (with cyclic wrap). */
264
+ focusNext(currentValue) {
265
+ const allRadios = this.registeredRadios();
266
+ const focusableRadios = allRadios.filter((r) => !r.isDisabled());
267
+ if (focusableRadios.length === 0) {
268
+ return;
269
+ }
270
+ const currentIndex = focusableRadios.findIndex((r) => r.value() === currentValue);
271
+ const nextIndex = (currentIndex + 1) % focusableRadios.length;
272
+ const nextRadio = focusableRadios[nextIndex];
273
+ if (nextRadio) {
274
+ this.focusedValueSignal.set(nextRadio.value());
275
+ this.select(nextRadio.value());
276
+ nextRadio.focus();
277
+ }
278
+ }
279
+ /** Focuses the previous non-disabled radio (with cyclic wrap). */
280
+ focusPrevious(currentValue) {
281
+ const allRadios = this.registeredRadios();
282
+ const focusableRadios = allRadios.filter((r) => !r.isDisabled());
283
+ if (focusableRadios.length === 0) {
284
+ return;
285
+ }
286
+ const currentIndex = focusableRadios.findIndex((r) => r.value() === currentValue);
287
+ const prevIndex = (currentIndex - 1 + focusableRadios.length) % focusableRadios.length;
288
+ const prevRadio = focusableRadios[prevIndex];
289
+ if (prevRadio) {
290
+ this.focusedValueSignal.set(prevRadio.value());
291
+ this.select(prevRadio.value());
292
+ prevRadio.focus();
293
+ }
294
+ }
295
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComRadioGroup, deps: [], target: i0.ɵɵFactoryTarget.Component });
296
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: ComRadioGroup, isStandalone: true, selector: "com-radio-group", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, errorMessage: { classPropertyName: "errorMessage", publicName: "errorMessage", isSignal: true, isRequired: false, transformFunction: null }, errorStateMatcher: { classPropertyName: "errorStateMatcher", publicName: "errorStateMatcher", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "aria-labelledby", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedby: { classPropertyName: "ariaDescribedby", publicName: "aria-describedby", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", disabled: "disabledChange", selectionChange: "selectionChange" }, host: { properties: { "class.com-radio-group--disabled": "disabled()", "class.com-radio-group--error": "errorState()" }, classAttribute: "com-radio-group block" }, providers: [
297
+ {
298
+ provide: COM_RADIO_GROUP,
299
+ useFactory: () => {
300
+ const group = inject(ComRadioGroup);
301
+ return group.createContext();
302
+ },
303
+ },
304
+ ], exportAs: ["comRadioGroup"], ngImport: i0, template: `
305
+ <div
306
+ role="radiogroup"
307
+ [class]="groupClasses()"
308
+ [attr.aria-label]="ariaLabel()"
309
+ [attr.aria-labelledby]="ariaLabelledby()"
310
+ [attr.aria-describedby]="computedAriaDescribedby()"
311
+ [attr.aria-required]="required() || null"
312
+ [attr.aria-invalid]="errorState() || null"
313
+ >
314
+ <ng-content />
315
+ </div>
316
+ @if (errorState() && errorMessage()) {
317
+ <div
318
+ [id]="errorId"
319
+ class="com-radio-group__error mt-1.5 text-sm text-warn"
320
+ role="alert"
321
+ >
322
+ {{ errorMessage() }}
323
+ </div>
324
+ }
325
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
326
+ }
327
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComRadioGroup, decorators: [{
328
+ type: Component,
329
+ args: [{
330
+ selector: 'com-radio-group',
331
+ exportAs: 'comRadioGroup',
332
+ template: `
333
+ <div
334
+ role="radiogroup"
335
+ [class]="groupClasses()"
336
+ [attr.aria-label]="ariaLabel()"
337
+ [attr.aria-labelledby]="ariaLabelledby()"
338
+ [attr.aria-describedby]="computedAriaDescribedby()"
339
+ [attr.aria-required]="required() || null"
340
+ [attr.aria-invalid]="errorState() || null"
341
+ >
342
+ <ng-content />
343
+ </div>
344
+ @if (errorState() && errorMessage()) {
345
+ <div
346
+ [id]="errorId"
347
+ class="com-radio-group__error mt-1.5 text-sm text-warn"
348
+ role="alert"
349
+ >
350
+ {{ errorMessage() }}
351
+ </div>
352
+ }
353
+ `,
354
+ changeDetection: ChangeDetectionStrategy.OnPush,
355
+ encapsulation: ViewEncapsulation.None,
356
+ providers: [
357
+ {
358
+ provide: COM_RADIO_GROUP,
359
+ useFactory: () => {
360
+ const group = inject(ComRadioGroup);
361
+ return group.createContext();
362
+ },
363
+ },
364
+ ],
365
+ host: {
366
+ class: 'com-radio-group block',
367
+ '[class.com-radio-group--disabled]': 'disabled()',
368
+ '[class.com-radio-group--error]': 'errorState()',
369
+ },
370
+ }]
371
+ }], ctorParameters: () => [], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], errorMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorMessage", required: false }] }], errorStateMatcher: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorStateMatcher", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-label", required: false }] }], ariaLabelledby: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-labelledby", required: false }] }], ariaDescribedby: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-describedby", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }] } });
372
+
373
+ /**
374
+ * Production-grade radio component with full accessibility support.
375
+ *
376
+ * Uses a native `<input type="radio">` for built-in keyboard handling,
377
+ * `:checked` pseudo-class, and screen reader support.
378
+ *
379
+ * Must be used within a `ComRadioGroup` which manages the selected value
380
+ * and provides the shared `name` attribute.
381
+ *
382
+ * @tokens `--color-border`, `--color-primary`, `--color-primary-foreground`, `--color-primary-hover`,
383
+ * `--color-accent`, `--color-accent-foreground`, `--color-accent-hover`,
384
+ * `--color-warn`, `--color-warn-foreground`, `--color-warn-hover`,
385
+ * `--color-disabled`, `--color-disabled-foreground`, `--color-ring`
386
+ *
387
+ * @example Basic usage within a group
388
+ * ```html
389
+ * <com-radio-group [(value)]="selectedOption">
390
+ * <com-radio value="option1">Option 1</com-radio>
391
+ * <com-radio value="option2">Option 2</com-radio>
392
+ * <com-radio value="option3">Option 3</com-radio>
393
+ * </com-radio-group>
394
+ * ```
395
+ *
396
+ * @example Disabled option
397
+ * ```html
398
+ * <com-radio-group [(value)]="selected">
399
+ * <com-radio value="enabled">Enabled option</com-radio>
400
+ * <com-radio value="disabled" [disabled]="true">Disabled option</com-radio>
401
+ * </com-radio-group>
402
+ * ```
403
+ */
404
+ class ComRadio {
405
+ /** Optional parent radio group context. */
406
+ group = inject(COM_RADIO_GROUP, {
407
+ optional: true,
408
+ });
409
+ /** DestroyRef for cleanup. */
410
+ destroyRef = inject(DestroyRef);
411
+ /** Reference to the native input element. */
412
+ inputRef = viewChild('inputElement', ...(ngDevMode ? [{ debugName: "inputRef" }] : []));
413
+ /** Unique ID for this radio instance. */
414
+ uniqueId = generateRadioId();
415
+ // Inputs
416
+ value = input.required(...(ngDevMode ? [{ debugName: "value" }] : []));
417
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : []));
418
+ variant = input('primary', ...(ngDevMode ? [{ debugName: "variant" }] : []));
419
+ disabled = model(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
420
+ id = input(...(ngDevMode ? [undefined, { debugName: "id" }] : []));
421
+ ariaLabel = input(null, { ...(ngDevMode ? { debugName: "ariaLabel" } : {}), alias: 'aria-label' });
422
+ ariaLabelledby = input(null, { ...(ngDevMode ? { debugName: "ariaLabelledby" } : {}), alias: 'aria-labelledby' });
423
+ ariaDescribedby = input(null, { ...(ngDevMode ? { debugName: "ariaDescribedby" } : {}), alias: 'aria-describedby' });
424
+ // Outputs
425
+ changed = output();
426
+ // Computed state
427
+ inputId = computed(() => this.id() ?? this.uniqueId, ...(ngDevMode ? [{ debugName: "inputId" }] : []));
428
+ /** Resolve size from group or local input. */
429
+ resolvedSize = computed(() => this.group?.size() ?? this.size(), ...(ngDevMode ? [{ debugName: "resolvedSize" }] : []));
430
+ /** Resolve variant from group or local input. */
431
+ resolvedVariant = computed(() => this.group?.variant() ?? this.variant(), ...(ngDevMode ? [{ debugName: "resolvedVariant" }] : []));
432
+ /** Whether this radio is checked based on group value. */
433
+ isChecked = computed(() => {
434
+ if (!this.group) {
435
+ return false;
436
+ }
437
+ return this.group.value() === this.value();
438
+ }, ...(ngDevMode ? [{ debugName: "isChecked" }] : []));
439
+ /** Whether this radio is disabled (from local or group). */
440
+ isDisabled = computed(() => this.disabled() || (this.group?.disabled() ?? false), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
441
+ /** Get name from group. */
442
+ groupName = computed(() => this.group?.name(), ...(ngDevMode ? [{ debugName: "groupName" }] : []));
443
+ /** Tab index for roving tabindex pattern. */
444
+ tabIndex = computed(() => {
445
+ if (this.isDisabled()) {
446
+ return -1;
447
+ }
448
+ if (!this.group) {
449
+ return 0;
450
+ }
451
+ // Roving tabindex: only the selected or first focusable item gets tabindex 0
452
+ const isSelected = this.isChecked();
453
+ const isFocusTarget = this.group.focusedValue() === this.value();
454
+ if (isSelected || isFocusTarget) {
455
+ return 0;
456
+ }
457
+ return -1;
458
+ }, ...(ngDevMode ? [{ debugName: "tabIndex" }] : []));
459
+ circleClasses = computed(() => radioCircleVariants({ variant: this.resolvedVariant(), size: this.resolvedSize() }), ...(ngDevMode ? [{ debugName: "circleClasses" }] : []));
460
+ dotSizeClass = computed(() => RADIO_DOT_SIZES[this.resolvedSize()], ...(ngDevMode ? [{ debugName: "dotSizeClass" }] : []));
461
+ labelSizeClass = computed(() => RADIO_LABEL_SIZES[this.resolvedSize()], ...(ngDevMode ? [{ debugName: "labelSizeClass" }] : []));
462
+ ngOnInit() {
463
+ // Register with the group
464
+ this.group?.register(this);
465
+ // Unregister on destroy
466
+ this.destroyRef.onDestroy(() => {
467
+ this.group?.unregister(this);
468
+ });
469
+ }
470
+ // Event handlers
471
+ onInputChange(event) {
472
+ const input = event.target;
473
+ if (input.checked && this.group) {
474
+ this.group.select(this.value());
475
+ this.changed.emit({ value: this.value(), source: this });
476
+ }
477
+ }
478
+ onBlur() {
479
+ this.group?.onTouched?.();
480
+ }
481
+ onKeyDown(event) {
482
+ if (!this.group) {
483
+ return;
484
+ }
485
+ const { key } = event;
486
+ const isVertical = this.group.orientation() === 'vertical';
487
+ const isHorizontal = this.group.orientation() === 'horizontal';
488
+ let handled = false;
489
+ if ((isVertical && key === 'ArrowDown') ||
490
+ (isHorizontal && key === 'ArrowRight')) {
491
+ this.group.focusNext(this.value());
492
+ handled = true;
493
+ }
494
+ else if ((isVertical && key === 'ArrowUp') ||
495
+ (isHorizontal && key === 'ArrowLeft')) {
496
+ this.group.focusPrevious(this.value());
497
+ handled = true;
498
+ }
499
+ else if (key === ' ') {
500
+ // Space selects the focused radio
501
+ if (!this.isChecked()) {
502
+ this.group.select(this.value());
503
+ this.changed.emit({ value: this.value(), source: this });
504
+ }
505
+ handled = true;
506
+ }
507
+ if (handled) {
508
+ event.preventDefault();
509
+ event.stopPropagation();
510
+ }
511
+ }
512
+ // Public API
513
+ /** Focuses this radio's input element. */
514
+ focus() {
515
+ this.inputRef()?.nativeElement.focus();
516
+ }
517
+ /** Selects this radio programmatically. */
518
+ select() {
519
+ if (this.isDisabled() || !this.group) {
520
+ return;
521
+ }
522
+ this.group.select(this.value());
523
+ this.changed.emit({ value: this.value(), source: this });
524
+ }
525
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComRadio, deps: [], target: i0.ɵɵFactoryTarget.Component });
526
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.0", type: ComRadio, isStandalone: true, selector: "com-radio", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "aria-labelledby", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedby: { classPropertyName: "ariaDescribedby", publicName: "aria-describedby", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { disabled: "disabledChange", changed: "changed" }, host: { properties: { "class.com-radio--disabled": "isDisabled()", "class.com-radio--checked": "isChecked()" }, classAttribute: "com-radio inline-block align-middle" }, viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["inputElement"], descendants: true, isSignal: true }], exportAs: ["comRadio"], ngImport: i0, template: `
527
+ <label
528
+ class="group relative inline-flex items-center"
529
+ [class.cursor-pointer]="!isDisabled()"
530
+ [class.cursor-not-allowed]="isDisabled()"
531
+ >
532
+ <span><input
533
+ #inputElement
534
+ type="radio"
535
+ class="peer sr-only"
536
+ [id]="inputId()"
537
+ [checked]="isChecked()"
538
+ [disabled]="isDisabled()"
539
+ [attr.name]="groupName()"
540
+ [attr.value]="value()"
541
+ [attr.aria-label]="ariaLabel()"
542
+ [attr.aria-labelledby]="ariaLabelledby()"
543
+ [attr.aria-describedby]="ariaDescribedby()"
544
+ [attr.tabindex]="tabIndex()"
545
+ (change)="onInputChange($event)"
546
+ (blur)="onBlur()"
547
+ (keydown)="onKeyDown($event)"
548
+ /></span>
549
+ <div [class]="circleClasses()">
550
+ <div
551
+ class="com-radio__dot rounded-full bg-current transition-transform duration-150 peer-disabled:bg-disabled-foreground"
552
+ [class]="dotSizeClass()"
553
+ [class.scale-100]="isChecked()"
554
+ [class.scale-0]="!isChecked()"
555
+ ></div>
556
+ </div>
557
+ <span
558
+ class="com-radio__label select-none peer-disabled:cursor-not-allowed peer-disabled:text-disabled-foreground"
559
+ [class]="labelSizeClass()"
560
+ >
561
+ <ng-content />
562
+ </span>
563
+ </label>
564
+ `, isInline: true, styles: [".sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
565
+ }
566
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: ComRadio, decorators: [{
567
+ type: Component,
568
+ args: [{ selector: 'com-radio', exportAs: 'comRadio', template: `
569
+ <label
570
+ class="group relative inline-flex items-center"
571
+ [class.cursor-pointer]="!isDisabled()"
572
+ [class.cursor-not-allowed]="isDisabled()"
573
+ >
574
+ <span><input
575
+ #inputElement
576
+ type="radio"
577
+ class="peer sr-only"
578
+ [id]="inputId()"
579
+ [checked]="isChecked()"
580
+ [disabled]="isDisabled()"
581
+ [attr.name]="groupName()"
582
+ [attr.value]="value()"
583
+ [attr.aria-label]="ariaLabel()"
584
+ [attr.aria-labelledby]="ariaLabelledby()"
585
+ [attr.aria-describedby]="ariaDescribedby()"
586
+ [attr.tabindex]="tabIndex()"
587
+ (change)="onInputChange($event)"
588
+ (blur)="onBlur()"
589
+ (keydown)="onKeyDown($event)"
590
+ /></span>
591
+ <div [class]="circleClasses()">
592
+ <div
593
+ class="com-radio__dot rounded-full bg-current transition-transform duration-150 peer-disabled:bg-disabled-foreground"
594
+ [class]="dotSizeClass()"
595
+ [class.scale-100]="isChecked()"
596
+ [class.scale-0]="!isChecked()"
597
+ ></div>
598
+ </div>
599
+ <span
600
+ class="com-radio__label select-none peer-disabled:cursor-not-allowed peer-disabled:text-disabled-foreground"
601
+ [class]="labelSizeClass()"
602
+ >
603
+ <ng-content />
604
+ </span>
605
+ </label>
606
+ `, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
607
+ class: 'com-radio inline-block align-middle',
608
+ '[class.com-radio--disabled]': 'isDisabled()',
609
+ '[class.com-radio--checked]': 'isChecked()',
610
+ }, styles: [".sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"] }]
611
+ }], propDecorators: { inputRef: [{ type: i0.ViewChild, args: ['inputElement', { isSignal: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }, { type: i0.Output, args: ["disabledChange"] }], id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-label", required: false }] }], ariaLabelledby: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-labelledby", required: false }] }], ariaDescribedby: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-describedby", required: false }] }], changed: [{ type: i0.Output, args: ["changed"] }] } });
612
+
613
+ // Public API for the radio component
614
+ // Main components
615
+
616
+ /**
617
+ * Generated bundle index. Do not edit.
618
+ */
619
+
620
+ export { COM_RADIO_GROUP, ComRadio, ComRadioGroup, RADIO_DOT_SIZES, RADIO_GROUP_ORIENTATIONS, RADIO_LABEL_SIZES, radioCircleVariants };
621
+ //# sourceMappingURL=ngx-com-components-radio.mjs.map