@cdevhub/ngx-tw 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.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +134 -0
  3. package/fesm2022/cdevhub-ngx-tw-accordion.mjs +108 -0
  4. package/fesm2022/cdevhub-ngx-tw-accordion.mjs.map +1 -0
  5. package/fesm2022/cdevhub-ngx-tw-alert.mjs +240 -0
  6. package/fesm2022/cdevhub-ngx-tw-alert.mjs.map +1 -0
  7. package/fesm2022/cdevhub-ngx-tw-avatar.mjs +287 -0
  8. package/fesm2022/cdevhub-ngx-tw-avatar.mjs.map +1 -0
  9. package/fesm2022/cdevhub-ngx-tw-badge.mjs +332 -0
  10. package/fesm2022/cdevhub-ngx-tw-badge.mjs.map +1 -0
  11. package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs +452 -0
  12. package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs.map +1 -0
  13. package/fesm2022/cdevhub-ngx-tw-button.mjs +227 -0
  14. package/fesm2022/cdevhub-ngx-tw-button.mjs.map +1 -0
  15. package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs +417 -0
  16. package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs.map +1 -0
  17. package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs +232 -0
  18. package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs.map +1 -0
  19. package/fesm2022/cdevhub-ngx-tw-calendar.mjs +4482 -0
  20. package/fesm2022/cdevhub-ngx-tw-calendar.mjs.map +1 -0
  21. package/fesm2022/cdevhub-ngx-tw-card.mjs +157 -0
  22. package/fesm2022/cdevhub-ngx-tw-card.mjs.map +1 -0
  23. package/fesm2022/cdevhub-ngx-tw-carousel.mjs +1454 -0
  24. package/fesm2022/cdevhub-ngx-tw-carousel.mjs.map +1 -0
  25. package/fesm2022/cdevhub-ngx-tw-checkbox.mjs +593 -0
  26. package/fesm2022/cdevhub-ngx-tw-checkbox.mjs.map +1 -0
  27. package/fesm2022/cdevhub-ngx-tw-code-block.mjs +228 -0
  28. package/fesm2022/cdevhub-ngx-tw-code-block.mjs.map +1 -0
  29. package/fesm2022/cdevhub-ngx-tw-collapsible.mjs +510 -0
  30. package/fesm2022/cdevhub-ngx-tw-collapsible.mjs.map +1 -0
  31. package/fesm2022/cdevhub-ngx-tw-combobox.mjs +1721 -0
  32. package/fesm2022/cdevhub-ngx-tw-combobox.mjs.map +1 -0
  33. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs +721 -0
  34. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs.map +1 -0
  35. package/fesm2022/cdevhub-ngx-tw-core.mjs +861 -0
  36. package/fesm2022/cdevhub-ngx-tw-core.mjs.map +1 -0
  37. package/fesm2022/cdevhub-ngx-tw-date-picker.mjs +1558 -0
  38. package/fesm2022/cdevhub-ngx-tw-date-picker.mjs.map +1 -0
  39. package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs +1833 -0
  40. package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs.map +1 -0
  41. package/fesm2022/cdevhub-ngx-tw-dialog.mjs +693 -0
  42. package/fesm2022/cdevhub-ngx-tw-dialog.mjs.map +1 -0
  43. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs +331 -0
  44. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs.map +1 -0
  45. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs +289 -0
  46. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs.map +1 -0
  47. package/fesm2022/cdevhub-ngx-tw-form-field.mjs +572 -0
  48. package/fesm2022/cdevhub-ngx-tw-form-field.mjs.map +1 -0
  49. package/fesm2022/cdevhub-ngx-tw-icon-lucide.mjs +36 -0
  50. package/fesm2022/cdevhub-ngx-tw-icon-lucide.mjs.map +1 -0
  51. package/fesm2022/cdevhub-ngx-tw-icon.mjs +278 -0
  52. package/fesm2022/cdevhub-ngx-tw-icon.mjs.map +1 -0
  53. package/fesm2022/cdevhub-ngx-tw-input.mjs +355 -0
  54. package/fesm2022/cdevhub-ngx-tw-input.mjs.map +1 -0
  55. package/fesm2022/cdevhub-ngx-tw-item.mjs +241 -0
  56. package/fesm2022/cdevhub-ngx-tw-item.mjs.map +1 -0
  57. package/fesm2022/cdevhub-ngx-tw-menu.mjs +433 -0
  58. package/fesm2022/cdevhub-ngx-tw-menu.mjs.map +1 -0
  59. package/fesm2022/cdevhub-ngx-tw-paginator.mjs +691 -0
  60. package/fesm2022/cdevhub-ngx-tw-paginator.mjs.map +1 -0
  61. package/fesm2022/cdevhub-ngx-tw-popover.mjs +678 -0
  62. package/fesm2022/cdevhub-ngx-tw-popover.mjs.map +1 -0
  63. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs +335 -0
  64. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs.map +1 -0
  65. package/fesm2022/cdevhub-ngx-tw-radio.mjs +708 -0
  66. package/fesm2022/cdevhub-ngx-tw-radio.mjs.map +1 -0
  67. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs +319 -0
  68. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs.map +1 -0
  69. package/fesm2022/cdevhub-ngx-tw-select.mjs +1722 -0
  70. package/fesm2022/cdevhub-ngx-tw-select.mjs.map +1 -0
  71. package/fesm2022/cdevhub-ngx-tw-separator.mjs +115 -0
  72. package/fesm2022/cdevhub-ngx-tw-separator.mjs.map +1 -0
  73. package/fesm2022/cdevhub-ngx-tw-sheet.mjs +768 -0
  74. package/fesm2022/cdevhub-ngx-tw-sheet.mjs.map +1 -0
  75. package/fesm2022/cdevhub-ngx-tw-skeleton.mjs +149 -0
  76. package/fesm2022/cdevhub-ngx-tw-skeleton.mjs.map +1 -0
  77. package/fesm2022/cdevhub-ngx-tw-slider.mjs +1147 -0
  78. package/fesm2022/cdevhub-ngx-tw-slider.mjs.map +1 -0
  79. package/fesm2022/cdevhub-ngx-tw-sort.mjs +346 -0
  80. package/fesm2022/cdevhub-ngx-tw-sort.mjs.map +1 -0
  81. package/fesm2022/cdevhub-ngx-tw-spinner.mjs +186 -0
  82. package/fesm2022/cdevhub-ngx-tw-spinner.mjs.map +1 -0
  83. package/fesm2022/cdevhub-ngx-tw-split.mjs +1082 -0
  84. package/fesm2022/cdevhub-ngx-tw-split.mjs.map +1 -0
  85. package/fesm2022/cdevhub-ngx-tw-stat.mjs +579 -0
  86. package/fesm2022/cdevhub-ngx-tw-stat.mjs.map +1 -0
  87. package/fesm2022/cdevhub-ngx-tw-stepper.mjs +443 -0
  88. package/fesm2022/cdevhub-ngx-tw-stepper.mjs.map +1 -0
  89. package/fesm2022/cdevhub-ngx-tw-switch.mjs +391 -0
  90. package/fesm2022/cdevhub-ngx-tw-switch.mjs.map +1 -0
  91. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs +315 -0
  92. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs.map +1 -0
  93. package/fesm2022/cdevhub-ngx-tw-table.mjs +994 -0
  94. package/fesm2022/cdevhub-ngx-tw-table.mjs.map +1 -0
  95. package/fesm2022/cdevhub-ngx-tw-tabs.mjs +508 -0
  96. package/fesm2022/cdevhub-ngx-tw-tabs.mjs.map +1 -0
  97. package/fesm2022/cdevhub-ngx-tw-textarea.mjs +147 -0
  98. package/fesm2022/cdevhub-ngx-tw-textarea.mjs.map +1 -0
  99. package/fesm2022/cdevhub-ngx-tw-theme.mjs +178 -0
  100. package/fesm2022/cdevhub-ngx-tw-theme.mjs.map +1 -0
  101. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs +1344 -0
  102. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs.map +1 -0
  103. package/fesm2022/cdevhub-ngx-tw-timeline.mjs +1136 -0
  104. package/fesm2022/cdevhub-ngx-tw-timeline.mjs.map +1 -0
  105. package/fesm2022/cdevhub-ngx-tw-toast.mjs +1311 -0
  106. package/fesm2022/cdevhub-ngx-tw-toast.mjs.map +1 -0
  107. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs +533 -0
  108. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs.map +1 -0
  109. package/fesm2022/cdevhub-ngx-tw.mjs +61 -0
  110. package/fesm2022/cdevhub-ngx-tw.mjs.map +1 -0
  111. package/package.json +265 -0
  112. package/theme/_base.css +394 -0
  113. package/theme/_dark.css +510 -0
  114. package/theme/_high-contrast.css +290 -0
  115. package/theme/_semantic.css +317 -0
  116. package/theme/_typography.css +9 -0
  117. package/theme/index.css +54 -0
  118. package/types/cdevhub-ngx-tw-accordion.d.ts +48 -0
  119. package/types/cdevhub-ngx-tw-alert.d.ts +67 -0
  120. package/types/cdevhub-ngx-tw-avatar.d.ts +79 -0
  121. package/types/cdevhub-ngx-tw-badge.d.ts +94 -0
  122. package/types/cdevhub-ngx-tw-breadcrumbs.d.ts +227 -0
  123. package/types/cdevhub-ngx-tw-button.d.ts +74 -0
  124. package/types/cdevhub-ngx-tw-calendar-luxon.d.ts +200 -0
  125. package/types/cdevhub-ngx-tw-calendar-testing.d.ts +113 -0
  126. package/types/cdevhub-ngx-tw-calendar.d.ts +1704 -0
  127. package/types/cdevhub-ngx-tw-card.d.ts +48 -0
  128. package/types/cdevhub-ngx-tw-carousel.d.ts +406 -0
  129. package/types/cdevhub-ngx-tw-checkbox.d.ts +136 -0
  130. package/types/cdevhub-ngx-tw-code-block.d.ts +93 -0
  131. package/types/cdevhub-ngx-tw-collapsible.d.ts +151 -0
  132. package/types/cdevhub-ngx-tw-combobox.d.ts +389 -0
  133. package/types/cdevhub-ngx-tw-command-palette.d.ts +215 -0
  134. package/types/cdevhub-ngx-tw-core.d.ts +719 -0
  135. package/types/cdevhub-ngx-tw-date-picker.d.ts +317 -0
  136. package/types/cdevhub-ngx-tw-date-range-picker.d.ts +313 -0
  137. package/types/cdevhub-ngx-tw-dialog.d.ts +303 -0
  138. package/types/cdevhub-ngx-tw-empty-state.d.ts +104 -0
  139. package/types/cdevhub-ngx-tw-flip-card.d.ts +95 -0
  140. package/types/cdevhub-ngx-tw-form-field.d.ts +210 -0
  141. package/types/cdevhub-ngx-tw-icon-lucide.d.ts +29 -0
  142. package/types/cdevhub-ngx-tw-icon.d.ts +134 -0
  143. package/types/cdevhub-ngx-tw-input.d.ts +110 -0
  144. package/types/cdevhub-ngx-tw-item.d.ts +70 -0
  145. package/types/cdevhub-ngx-tw-menu.d.ts +99 -0
  146. package/types/cdevhub-ngx-tw-paginator.d.ts +305 -0
  147. package/types/cdevhub-ngx-tw-popover.d.ts +157 -0
  148. package/types/cdevhub-ngx-tw-progress-bar.d.ts +100 -0
  149. package/types/cdevhub-ngx-tw-radio.d.ts +174 -0
  150. package/types/cdevhub-ngx-tw-segmented-control.d.ts +66 -0
  151. package/types/cdevhub-ngx-tw-select.d.ts +384 -0
  152. package/types/cdevhub-ngx-tw-separator.d.ts +28 -0
  153. package/types/cdevhub-ngx-tw-sheet.d.ts +335 -0
  154. package/types/cdevhub-ngx-tw-skeleton.d.ts +52 -0
  155. package/types/cdevhub-ngx-tw-slider.d.ts +258 -0
  156. package/types/cdevhub-ngx-tw-sort.d.ts +136 -0
  157. package/types/cdevhub-ngx-tw-spinner.d.ts +50 -0
  158. package/types/cdevhub-ngx-tw-split.d.ts +265 -0
  159. package/types/cdevhub-ngx-tw-stat.d.ts +140 -0
  160. package/types/cdevhub-ngx-tw-stepper.d.ts +127 -0
  161. package/types/cdevhub-ngx-tw-switch.d.ts +88 -0
  162. package/types/cdevhub-ngx-tw-tab-nav.d.ts +107 -0
  163. package/types/cdevhub-ngx-tw-table.d.ts +479 -0
  164. package/types/cdevhub-ngx-tw-tabs.d.ts +121 -0
  165. package/types/cdevhub-ngx-tw-textarea.d.ts +62 -0
  166. package/types/cdevhub-ngx-tw-theme.d.ts +106 -0
  167. package/types/cdevhub-ngx-tw-time-picker.d.ts +303 -0
  168. package/types/cdevhub-ngx-tw-timeline.d.ts +278 -0
  169. package/types/cdevhub-ngx-tw-toast.d.ts +388 -0
  170. package/types/cdevhub-ngx-tw-tooltip.d.ts +95 -0
  171. package/types/cdevhub-ngx-tw.d.ts +49 -0
@@ -0,0 +1,708 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, model, output, inject, ElementRef, DestroyRef, forwardRef, signal, linkedSignal, computed, afterNextRender, isDevMode, ChangeDetectionStrategy, Component, contentChildren } from '@angular/core';
3
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
4
+ import { NgControl, NgForm, FormGroupDirective } from '@angular/forms';
5
+ import { FocusMonitor } from '@angular/cdk/a11y';
6
+ import { merge } from 'rxjs';
7
+ import { tv } from 'tailwind-variants';
8
+ import { TW_ERROR_STATE_MATCHER } from '@cdevhub/ngx-tw/core';
9
+
10
+ // ── Radio tv() config ────────────────────────────────────────────
11
+ const radioVariants = tv({
12
+ slots: {
13
+ // `items-start` aligns circleWrap with the top of the label container; combined with
14
+ // a `min-h-N` matching the label's first-line line-height on circleWrap, the circle
15
+ // stays centered on the first line whether the label spans one line or many.
16
+ root: 'inline-flex items-start gap-3 cursor-pointer select-none rounded-md focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
17
+ circleWrap: 'relative inline-flex items-center justify-center shrink-0',
18
+ circle: 'inline-flex items-center justify-center rounded-full border bg-surface transition-colors duration-normal motion-reduce:transition-none',
19
+ dotWrap: 'inline-flex items-center justify-center pointer-events-none',
20
+ dot: 'inline-block rounded-full',
21
+ labelWrap: 'flex flex-col min-w-0 empty:hidden',
22
+ label: 'font-medium text-fg empty:hidden',
23
+ description: 'text-fg-muted empty:hidden',
24
+ },
25
+ variants: {
26
+ size: {
27
+ xs: {
28
+ // `size-3.5` half-step: aligns with text-xs/leading-4 row metric.
29
+ circle: 'size-3.5',
30
+ dot: 'size-1.5',
31
+ circleWrap: 'min-h-4',
32
+ label: 'text-xs leading-4',
33
+ description: 'text-2xs leading-4',
34
+ },
35
+ sm: {
36
+ circle: 'size-4',
37
+ dot: 'size-2',
38
+ circleWrap: 'min-h-5',
39
+ label: 'text-sm leading-5',
40
+ description: 'text-xs leading-4',
41
+ },
42
+ md: {
43
+ circle: 'size-5',
44
+ dot: 'size-2.5',
45
+ circleWrap: 'min-h-5',
46
+ label: 'text-sm leading-5',
47
+ description: 'text-xs leading-4',
48
+ },
49
+ lg: {
50
+ circle: 'size-6',
51
+ dot: 'size-3',
52
+ circleWrap: 'min-h-6',
53
+ label: 'text-base leading-6',
54
+ description: 'text-sm leading-5',
55
+ },
56
+ xl: {
57
+ circle: 'size-7',
58
+ // xl density: dot uses `size-3.5` (14px) as the proportionally-sized
59
+ // selection mark inside the size-7 (28px) circle — exactly 50%.
60
+ dot: 'size-3.5',
61
+ // xl circle (28px) > default text-base line-height (24px); bump leading to
62
+ // leading-7 so the first line height matches the circle and they align.
63
+ circleWrap: 'min-h-7',
64
+ label: 'text-base leading-7',
65
+ description: 'text-sm leading-5',
66
+ },
67
+ },
68
+ labelPosition: {
69
+ before: { root: 'flex-row-reverse' },
70
+ after: { root: 'flex-row' },
71
+ },
72
+ checked: {
73
+ true: { circle: '' },
74
+ false: { circle: 'border-border hover:border-border-strong' },
75
+ },
76
+ disabled: {
77
+ true: { root: 'opacity-50 pointer-events-none cursor-not-allowed' },
78
+ false: { root: '' },
79
+ },
80
+ errorState: {
81
+ true: { circle: '', label: 'text-error-700', description: 'text-error-600' },
82
+ false: { circle: '', label: '', description: '' },
83
+ },
84
+ },
85
+ compoundVariants: [
86
+ // Error state on an unchecked radio repaints the ring with the error color.
87
+ { errorState: true, checked: false, class: { circle: 'border-error-500 hover:border-error-600' } },
88
+ ],
89
+ defaultVariants: {
90
+ size: 'md',
91
+ labelPosition: 'after',
92
+ checked: false,
93
+ disabled: false,
94
+ errorState: false,
95
+ },
96
+ }, { twMerge: true });
97
+ // ── Static selected-state color lookups (all classes written statically for Tailwind v4 scanning) ──
98
+ const SOLID_RING = {
99
+ primary: 'border-primary-600',
100
+ secondary: 'border-secondary-600',
101
+ accent: 'border-accent-600',
102
+ neutral: 'border-fg',
103
+ info: 'border-info-600',
104
+ success: 'border-success-600',
105
+ warning: 'border-warning-500',
106
+ error: 'border-error-600',
107
+ };
108
+ const SOLID_DOT = {
109
+ primary: 'bg-primary-600',
110
+ secondary: 'bg-secondary-600',
111
+ accent: 'bg-accent-600',
112
+ neutral: 'bg-fg',
113
+ info: 'bg-info-600',
114
+ success: 'bg-success-600',
115
+ warning: 'bg-warning-500',
116
+ error: 'bg-error-600',
117
+ };
118
+ const OUTLINE_RING = {
119
+ primary: 'border-primary-600',
120
+ secondary: 'border-secondary-600',
121
+ accent: 'border-accent-600',
122
+ neutral: 'border-fg',
123
+ info: 'border-info-600',
124
+ success: 'border-success-600',
125
+ warning: 'border-warning-500',
126
+ error: 'border-error-600',
127
+ };
128
+ const OUTLINE_DOT = {
129
+ primary: 'bg-primary-600',
130
+ secondary: 'bg-secondary-600',
131
+ accent: 'bg-accent-600',
132
+ neutral: 'bg-fg',
133
+ info: 'bg-info-600',
134
+ success: 'bg-success-600',
135
+ warning: 'bg-warning-500',
136
+ error: 'bg-error-600',
137
+ };
138
+ let nextRadioId = 0;
139
+ // ── RadioComponent ───────────────────────────────────────────────
140
+ class RadioComponent {
141
+ /** The value this radio contributes when selected inside a `tw-radio-group`. Required when nested in a group; ignored when used standalone. */
142
+ value = input(undefined, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
143
+ /** Overrides the parent group's color for this radio. When undefined, inherits from the group (or defaults to `'primary'` standalone). */
144
+ color = input(undefined, ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
145
+ /** Overrides the parent group's size for this radio. When undefined, inherits from the group (or defaults to `'md'` standalone). */
146
+ size = input(undefined, ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
147
+ /** Overrides the parent group's variant for this radio. When undefined, inherits from the group (or defaults to `'solid'` standalone). */
148
+ variant = input(undefined, ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
149
+ /** When true, disables this radio regardless of the group's state. Group-disabled always wins as an OR. Defaults to `false`. */
150
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
151
+ /** Optional inline label rendered next to the radio. Use default content projection for rich label content instead. */
152
+ label = input(undefined, ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
153
+ /** Optional secondary description rendered under the label. Use `[slot="description"]` content projection for rich content instead. */
154
+ description = input(undefined, ...(ngDevMode ? [{ debugName: "description" }] : /* istanbul ignore next */ []));
155
+ /** Position of the label/description relative to the radio. Defaults to `'after'`. */
156
+ labelPosition = input('after', ...(ngDevMode ? [{ debugName: "labelPosition" }] : /* istanbul ignore next */ []));
157
+ /** Optional name attribute for standalone use. Ignored when the parent group provides a name. */
158
+ name = input(undefined, ...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
159
+ /** Accessible name when no visible label is provided. Mirrored to `aria-label`. */
160
+ ariaLabel = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabel" } : /* istanbul ignore next */ {}), alias: 'aria-label' });
161
+ /** ID of an external element that labels the radio. Mirrored to `aria-labelledby`. */
162
+ ariaLabelledby = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabelledby" } : /* istanbul ignore next */ {}), alias: 'aria-labelledby' });
163
+ /** ID of an external element that describes the radio. Mirrored to `aria-describedby`. */
164
+ ariaDescribedby = input(undefined, { ...(ngDevMode ? { debugName: "ariaDescribedby" } : /* istanbul ignore next */ {}), alias: 'aria-describedby' });
165
+ /** Two-way bound checked state. Authoritative only in standalone mode; when inside a `tw-radio-group`, this model reflects group selection but does NOT drive it. */
166
+ checked = model(false, ...(ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []));
167
+ /** Per-instance override of the {@link ErrorStateMatcher}. When omitted, the radio uses the `TW_ERROR_STATE_MATCHER` token's value. */
168
+ errorStateMatcher = input(undefined, ...(ngDevMode ? [{ debugName: "errorStateMatcher" }] : /* istanbul ignore next */ []));
169
+ /** Fires after the checked state changes from a user interaction on this radio. In grouped mode only fires when this radio becomes the selected one. Does not fire when selection is updated programmatically. */
170
+ change = output();
171
+ focusMonitor = inject(FocusMonitor);
172
+ elementRef = inject((ElementRef));
173
+ destroyRef = inject(DestroyRef);
174
+ parent = inject(forwardRef(() => RadioGroupComponent), { optional: true });
175
+ ngControl = inject(NgControl, { optional: true, self: true });
176
+ parentForm = inject(NgForm, { optional: true });
177
+ parentFormGroup = inject(FormGroupDirective, { optional: true });
178
+ defaultMatcher = inject(TW_ERROR_STATE_MATCHER);
179
+ onChange = () => { };
180
+ onTouched = () => { };
181
+ cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
182
+ _ngControlRev = signal(0, ...(ngDevMode ? [{ debugName: "_ngControlRev" }] : /* istanbul ignore next */ []));
183
+ _formSubmitRev = signal(0, ...(ngDevMode ? [{ debugName: "_formSubmitRev" }] : /* istanbul ignore next */ []));
184
+ uid = nextRadioId++;
185
+ hostId = `tw-radio-${this.uid}`;
186
+ labelId = `${this.hostId}-label`;
187
+ descriptionId = `${this.hostId}-description`;
188
+ internalChecked = linkedSignal(() => this.checked(), ...(ngDevMode ? [{ debugName: "internalChecked" }] : /* istanbul ignore next */ []));
189
+ /** Resolved color — radio's own color wins, then group's, then `'primary'`. */
190
+ effectiveColor = computed(() => this.color() ?? this.parent?.color() ?? 'primary', ...(ngDevMode ? [{ debugName: "effectiveColor" }] : /* istanbul ignore next */ []));
191
+ /** Resolved size — radio's own size wins, then group's, then `'md'`. */
192
+ effectiveSize = computed(() => this.size() ?? this.parent?.size() ?? 'md', ...(ngDevMode ? [{ debugName: "effectiveSize" }] : /* istanbul ignore next */ []));
193
+ /** Resolved variant — radio's own variant wins, then group's, then `'solid'`. */
194
+ effectiveVariant = computed(() => this.variant() ?? this.parent?.variant() ?? 'solid', ...(ngDevMode ? [{ debugName: "effectiveVariant" }] : /* istanbul ignore next */ []));
195
+ /** Resolved name attribute — parent group's name wins for HTML form semantics. */
196
+ effectiveName = computed(() => this.parent?.name() ?? this.name(), ...(ngDevMode ? [{ debugName: "effectiveName" }] : /* istanbul ignore next */ []));
197
+ /** Whether this radio is currently selected (grouped: parent.value === this.value; standalone: internal checked). */
198
+ isSelected = computed(() => {
199
+ if (this.parent)
200
+ return this.parent.value() === this.value();
201
+ return this.internalChecked();
202
+ }, ...(ngDevMode ? [{ debugName: "isSelected" }] : /* istanbul ignore next */ []));
203
+ /** Whether this radio is disabled (own disabled OR parent's disabled OR CVA-disabled). */
204
+ isDisabled = computed(() => this.disabled() || this.cvaDisabled() || (this.parent?.isDisabled() ?? false), ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
205
+ /** Whether the radio is in an error state per the configured `ErrorStateMatcher`. Inherits from the parent group when wrapped; falls back to its own NgControl when standalone. */
206
+ errorState = computed(() => {
207
+ this._ngControlRev();
208
+ this._formSubmitRev();
209
+ if (this.parent)
210
+ return this.parent.errorState();
211
+ const matcher = this.errorStateMatcher() ?? this.defaultMatcher;
212
+ const form = this.parentFormGroup ??
213
+ this.parentForm;
214
+ return matcher.isErrorState(this.ngControl?.control ?? null, form);
215
+ }, ...(ngDevMode ? [{ debugName: "errorState" }] : /* istanbul ignore next */ []));
216
+ /** Roving tabindex: in a group, only the selected (or first enabled if none selected) radio is focusable. Standalone radios are always focusable unless disabled. */
217
+ isFocusable = computed(() => {
218
+ if (this.isDisabled())
219
+ return false;
220
+ if (!this.parent)
221
+ return true;
222
+ if (this.isSelected())
223
+ return true;
224
+ const parentValue = this.parent.value();
225
+ if (parentValue === null || parentValue === undefined) {
226
+ const firstEnabled = this.parent
227
+ .radios()
228
+ .find((r) => !r.isDisabled());
229
+ return firstEnabled === this;
230
+ }
231
+ return false;
232
+ }, ...(ngDevMode ? [{ debugName: "isFocusable" }] : /* istanbul ignore next */ []));
233
+ effectiveAriaLabelledby = computed(() => {
234
+ const external = this.ariaLabelledby();
235
+ if (external)
236
+ return external;
237
+ if (this.ariaLabel())
238
+ return undefined;
239
+ return this.labelId;
240
+ }, ...(ngDevMode ? [{ debugName: "effectiveAriaLabelledby" }] : /* istanbul ignore next */ []));
241
+ effectiveAriaDescribedby = computed(() => {
242
+ const external = this.ariaDescribedby();
243
+ if (external)
244
+ return external;
245
+ return this.descriptionId;
246
+ }, ...(ngDevMode ? [{ debugName: "effectiveAriaDescribedby" }] : /* istanbul ignore next */ []));
247
+ variantResult = computed(() => radioVariants({
248
+ size: this.effectiveSize(),
249
+ labelPosition: this.labelPosition(),
250
+ checked: this.isSelected(),
251
+ disabled: this.isDisabled(),
252
+ errorState: this.errorState(),
253
+ }), ...(ngDevMode ? [{ debugName: "variantResult" }] : /* istanbul ignore next */ []));
254
+ rootClasses = computed(() => this.variantResult().root(), ...(ngDevMode ? [{ debugName: "rootClasses" }] : /* istanbul ignore next */ []));
255
+ circleWrapClasses = computed(() => this.variantResult().circleWrap(), ...(ngDevMode ? [{ debugName: "circleWrapClasses" }] : /* istanbul ignore next */ []));
256
+ labelWrapClasses = computed(() => this.variantResult().labelWrap(), ...(ngDevMode ? [{ debugName: "labelWrapClasses" }] : /* istanbul ignore next */ []));
257
+ labelClasses = computed(() => this.variantResult().label(), ...(ngDevMode ? [{ debugName: "labelClasses" }] : /* istanbul ignore next */ []));
258
+ descriptionClasses = computed(() => this.variantResult().description(), ...(ngDevMode ? [{ debugName: "descriptionClasses" }] : /* istanbul ignore next */ []));
259
+ dotWrapClasses = computed(() => this.variantResult().dotWrap(), ...(ngDevMode ? [{ debugName: "dotWrapClasses" }] : /* istanbul ignore next */ []));
260
+ circleClasses = computed(() => {
261
+ const base = this.variantResult().circle();
262
+ if (!this.isSelected())
263
+ return base;
264
+ const ringLookup = this.effectiveVariant() === 'solid' ? SOLID_RING : OUTLINE_RING;
265
+ return `${base} ${ringLookup[this.effectiveColor()]}`;
266
+ }, ...(ngDevMode ? [{ debugName: "circleClasses" }] : /* istanbul ignore next */ []));
267
+ dotClasses = computed(() => {
268
+ const base = this.variantResult().dot();
269
+ const dotLookup = this.effectiveVariant() === 'solid' ? SOLID_DOT : OUTLINE_DOT;
270
+ return `${base} ${dotLookup[this.effectiveColor()]}`;
271
+ }, ...(ngDevMode ? [{ debugName: "dotClasses" }] : /* istanbul ignore next */ []));
272
+ constructor() {
273
+ // Material-style CVA wiring for standalone `<tw-radio>` use. Mirrors
274
+ // `checkbox.ts` / `slider.ts` — avoids the circular-DI a static
275
+ // `NG_VALUE_ACCESSOR` provider would create when `NgControl` is injected
276
+ // with `self: true` on the same element. When this radio is wrapped in a
277
+ // `tw-radio-group`, the group owns CVA and child `NgControl` bindings are
278
+ // not expected (no `[(ngModel)]` on children).
279
+ if (this.ngControl) {
280
+ this.ngControl.valueAccessor = this;
281
+ }
282
+ afterNextRender(() => {
283
+ if (isDevMode() && !this.hasAccessibleNameHint()) {
284
+ console.warn('[tw-radio] The radio has no accessible name. Provide a `label` input, project label content, or set `aria-label` / `aria-labelledby`.');
285
+ }
286
+ });
287
+ }
288
+ /** Called by user click or group keyboard handler. Selects this radio. */
289
+ onActivate() {
290
+ if (this.isDisabled())
291
+ return;
292
+ if (this.parent) {
293
+ const wasSelected = this.isSelected();
294
+ this.parent.selectValue(this.value());
295
+ if (!wasSelected) {
296
+ this.change.emit(true);
297
+ }
298
+ return;
299
+ }
300
+ // Standalone — match native <input type="radio">: clicking does not toggle back off
301
+ if (!this.internalChecked()) {
302
+ this.internalChecked.set(true);
303
+ this.checked.set(true);
304
+ this.onChange(true);
305
+ this.onTouched();
306
+ this.change.emit(true);
307
+ }
308
+ }
309
+ /** Handles keyboard activation. Space selects; Enter does NOT — matches native `<input type="radio">` semantics. */
310
+ onKeydown(event) {
311
+ if (this.isDisabled())
312
+ return;
313
+ if (event.key === ' ' || event.key === 'Spacebar') {
314
+ event.preventDefault();
315
+ this.onActivate();
316
+ }
317
+ }
318
+ /** @internal Host blur. In a group, notifies the group's CVA touched callback; standalone, notifies the local CVA. */
319
+ onBlur() {
320
+ if (this.parent) {
321
+ this.parent.notifyTouched();
322
+ }
323
+ else {
324
+ this.onTouched();
325
+ // Blur flips `touched` on `NgControl`; bump the revision so `errorState` recomputes.
326
+ this._ngControlRev.update((n) => n + 1);
327
+ }
328
+ }
329
+ /** Focuses the host element. Required for keyboard navigation from the group. */
330
+ focus() {
331
+ this.elementRef.nativeElement.focus();
332
+ }
333
+ // ── ControlValueAccessor (standalone use) ──
334
+ writeValue(value) {
335
+ const next = !!value;
336
+ this.internalChecked.set(next);
337
+ this.checked.set(next);
338
+ }
339
+ registerOnChange(fn) {
340
+ this.onChange = fn;
341
+ }
342
+ registerOnTouched(fn) {
343
+ this.onTouched = fn;
344
+ }
345
+ setDisabledState(isDisabled) {
346
+ this.cvaDisabled.set(isDisabled);
347
+ }
348
+ ngOnInit() {
349
+ this.focusMonitor.monitor(this.elementRef);
350
+ this.destroyRef.onDestroy(() => {
351
+ this.focusMonitor.stopMonitoring(this.elementRef);
352
+ });
353
+ // Track validator status/value changes on a bound NgControl so `errorState`
354
+ // re-evaluates when status flips. Mirrors `input.ts`'s pattern.
355
+ const ctrl = this.ngControl?.control;
356
+ if (ctrl) {
357
+ const streams = [ctrl.statusChanges, ctrl.valueChanges].filter((s) => !!s);
358
+ if (streams.length) {
359
+ merge(...streams)
360
+ .pipe(takeUntilDestroyed(this.destroyRef))
361
+ .subscribe(() => this._ngControlRev.update((n) => n + 1));
362
+ }
363
+ }
364
+ const submit = this.parentFormGroup?.ngSubmit ?? this.parentForm?.ngSubmit;
365
+ if (submit) {
366
+ submit
367
+ .pipe(takeUntilDestroyed(this.destroyRef))
368
+ .subscribe(() => this._formSubmitRev.update((n) => n + 1));
369
+ }
370
+ }
371
+ hasAccessibleNameHint() {
372
+ if (this.label() || this.ariaLabel() || this.ariaLabelledby())
373
+ return true;
374
+ const host = this.elementRef.nativeElement;
375
+ return host.textContent !== null && host.textContent.trim().length > 0;
376
+ }
377
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: RadioComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
378
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: RadioComponent, isStandalone: true, selector: "tw-radio", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", 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 }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, labelPosition: { classPropertyName: "labelPosition", publicName: "labelPosition", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", 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 }, checked: { classPropertyName: "checked", publicName: "checked", isSignal: true, isRequired: false, transformFunction: null }, errorStateMatcher: { classPropertyName: "errorStateMatcher", publicName: "errorStateMatcher", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { checked: "checkedChange", change: "change" }, host: { attributes: { "role": "radio" }, listeners: { "click": "onActivate()", "keydown": "onKeydown($event)", "blur": "onBlur()" }, properties: { "id": "hostId", "class": "rootClasses()", "attr.data-checked": "isSelected()", "attr.aria-checked": "isSelected() ? \"true\" : \"false\"", "attr.aria-disabled": "isDisabled() || null", "attr.aria-invalid": "errorState() || null", "attr.aria-label": "ariaLabel() || null", "attr.aria-labelledby": "effectiveAriaLabelledby() || null", "attr.aria-describedby": "effectiveAriaDescribedby() || null", "attr.tabindex": "isFocusable() ? 0 : -1", "attr.name": "effectiveName() || null" } }, ngImport: i0, template: `
379
+ <span [class]="circleWrapClasses()">
380
+ <span [class]="circleClasses()">
381
+ @if (isSelected()) {
382
+ <span [class]="dotWrapClasses()" animate.enter="check-in">
383
+ <ng-content select="[slot='dot']">
384
+ <span [class]="dotClasses()"></span>
385
+ </ng-content>
386
+ </span>
387
+ }
388
+ </span>
389
+ </span>
390
+
391
+ <span [class]="labelWrapClasses()">
392
+ <span [id]="labelId" [class]="labelClasses()">
393
+ <ng-content />
394
+ @if (label()) {
395
+ {{ label() }}
396
+ }
397
+ </span>
398
+ <span [id]="descriptionId" [class]="descriptionClasses()">
399
+ <ng-content select="[slot='description']" />
400
+ @if (description()) {
401
+ {{ description() }}
402
+ }
403
+ </span>
404
+ </span>
405
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
406
+ }
407
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: RadioComponent, decorators: [{
408
+ type: Component,
409
+ args: [{
410
+ selector: 'tw-radio',
411
+ changeDetection: ChangeDetectionStrategy.OnPush,
412
+ template: `
413
+ <span [class]="circleWrapClasses()">
414
+ <span [class]="circleClasses()">
415
+ @if (isSelected()) {
416
+ <span [class]="dotWrapClasses()" animate.enter="check-in">
417
+ <ng-content select="[slot='dot']">
418
+ <span [class]="dotClasses()"></span>
419
+ </ng-content>
420
+ </span>
421
+ }
422
+ </span>
423
+ </span>
424
+
425
+ <span [class]="labelWrapClasses()">
426
+ <span [id]="labelId" [class]="labelClasses()">
427
+ <ng-content />
428
+ @if (label()) {
429
+ {{ label() }}
430
+ }
431
+ </span>
432
+ <span [id]="descriptionId" [class]="descriptionClasses()">
433
+ <ng-content select="[slot='description']" />
434
+ @if (description()) {
435
+ {{ description() }}
436
+ }
437
+ </span>
438
+ </span>
439
+ `,
440
+ host: {
441
+ 'role': 'radio',
442
+ '[id]': 'hostId',
443
+ '[class]': 'rootClasses()',
444
+ '[attr.data-checked]': 'isSelected()',
445
+ '[attr.aria-checked]': 'isSelected() ? "true" : "false"',
446
+ '[attr.aria-disabled]': 'isDisabled() || null',
447
+ '[attr.aria-invalid]': 'errorState() || null',
448
+ '[attr.aria-label]': 'ariaLabel() || null',
449
+ '[attr.aria-labelledby]': 'effectiveAriaLabelledby() || null',
450
+ '[attr.aria-describedby]': 'effectiveAriaDescribedby() || null',
451
+ '[attr.tabindex]': 'isFocusable() ? 0 : -1',
452
+ '[attr.name]': 'effectiveName() || null',
453
+ '(click)': 'onActivate()',
454
+ '(keydown)': 'onKeydown($event)',
455
+ '(blur)': 'onBlur()',
456
+ },
457
+ }]
458
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], 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 }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], labelPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelPosition", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", 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 }] }], checked: [{ type: i0.Input, args: [{ isSignal: true, alias: "checked", required: false }] }, { type: i0.Output, args: ["checkedChange"] }], errorStateMatcher: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorStateMatcher", required: false }] }], change: [{ type: i0.Output, args: ["change"] }] } });
459
+ // ── RadioGroup tv() config ───────────────────────────────────────
460
+ const radioGroupVariants = tv({
461
+ base: 'flex',
462
+ variants: {
463
+ orientation: {
464
+ vertical: 'flex-col gap-2',
465
+ horizontal: 'flex-row flex-wrap gap-3',
466
+ },
467
+ disabled: {
468
+ true: 'opacity-50 pointer-events-none cursor-not-allowed',
469
+ false: '',
470
+ },
471
+ },
472
+ defaultVariants: {
473
+ orientation: 'vertical',
474
+ disabled: false,
475
+ },
476
+ }, { twMerge: true });
477
+ let nextGroupId = 0;
478
+ // ── RadioGroupComponent ──────────────────────────────────────────
479
+ /**
480
+ * A keyboard-navigable group of `<tw-radio>` children with shared color, size,
481
+ * variant, and an owned selected `value`. The `T` type parameter is the type
482
+ * of each radio's `value` input — typically a string-literal union, but any
483
+ * value that compares with `===` works. Inferred when consumers bind via
484
+ * `[(value)]`; explicit on `[formControl]` of `FormControl<T | null>`.
485
+ */
486
+ class RadioGroupComponent {
487
+ /** Sets the semantic color applied to the selected radio's dot/ring. Propagated to children unless a child overrides it. Defaults to `'primary'`. */
488
+ color = input('primary', ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
489
+ /** Controls the overall scale of radios inside the group. Propagated to children unless a child overrides it. Defaults to `'md'`. */
490
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
491
+ /** Visual style of the selected indicator. `'solid'` fills the dot with the color against a colored ring; `'outline'` keeps a transparent fill with a colored ring and colored dot. Propagated to children unless a child overrides it. Defaults to `'solid'`. */
492
+ variant = input('solid', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
493
+ /** Layout direction of the group. Drives `aria-orientation` and the arrow-key model. Defaults to `'vertical'`. */
494
+ orientation = input('vertical', ...(ngDevMode ? [{ debugName: "orientation" }] : /* istanbul ignore next */ []));
495
+ /** When true, disables every radio in the group and blocks keyboard interaction. Defaults to `false`. */
496
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
497
+ /** When true, sets `aria-required="true"` on the group for assistive tech. Defaults to `false`. */
498
+ required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
499
+ /** Optional form-association name. Propagated to each child radio's host `name` attribute so standard HTML form semantics still apply. */
500
+ name = input(undefined, ...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
501
+ /** Accessible name for the group when no visible label is provided. Mirrored to `aria-label`. */
502
+ ariaLabel = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabel" } : /* istanbul ignore next */ {}), alias: 'aria-label' });
503
+ /** ID of an external element that labels the group. Mirrored to `aria-labelledby`. */
504
+ ariaLabelledby = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabelledby" } : /* istanbul ignore next */ {}), alias: 'aria-labelledby' });
505
+ /** ID of an external element that describes the group. Mirrored to `aria-describedby`. */
506
+ ariaDescribedby = input(undefined, { ...(ngDevMode ? { debugName: "ariaDescribedby" } : /* istanbul ignore next */ {}), alias: 'aria-describedby' });
507
+ /** Two-way bound selected value. Updates when the user picks a radio; fires `valueChange`. `null` means no selection. */
508
+ value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
509
+ /** Per-instance override of the {@link ErrorStateMatcher}. When omitted, the group uses the `TW_ERROR_STATE_MATCHER` token's value. */
510
+ errorStateMatcher = input(undefined, ...(ngDevMode ? [{ debugName: "errorStateMatcher" }] : /* istanbul ignore next */ []));
511
+ /** Fires after the selected value changes from a user interaction. Does not fire when the value is updated programmatically via `writeValue`. */
512
+ change = output();
513
+ /** @internal Child radios discovered via content projection. */
514
+ radios = contentChildren(RadioComponent, { ...(ngDevMode ? { debugName: "radios" } : /* istanbul ignore next */ {}), descendants: true });
515
+ focusMonitor = inject(FocusMonitor);
516
+ elementRef = inject((ElementRef));
517
+ destroyRef = inject(DestroyRef);
518
+ ngControl = inject(NgControl, { optional: true, self: true });
519
+ parentForm = inject(NgForm, { optional: true });
520
+ parentFormGroup = inject(FormGroupDirective, { optional: true });
521
+ defaultMatcher = inject(TW_ERROR_STATE_MATCHER);
522
+ hostId = `tw-radio-group-${nextGroupId++}`;
523
+ onChange = () => { };
524
+ onTouched = () => { };
525
+ cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
526
+ _ngControlRev = signal(0, ...(ngDevMode ? [{ debugName: "_ngControlRev" }] : /* istanbul ignore next */ []));
527
+ _formSubmitRev = signal(0, ...(ngDevMode ? [{ debugName: "_formSubmitRev" }] : /* istanbul ignore next */ []));
528
+ /** Effective disabled state — input OR CVA disabled. */
529
+ isDisabled = computed(() => this.disabled() || this.cvaDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
530
+ /** Group-level error state per the configured `ErrorStateMatcher`. Reads the bound `NgControl.invalid` through the matcher; child radios inherit from this when wrapped in the group. */
531
+ errorState = computed(() => {
532
+ this._ngControlRev();
533
+ this._formSubmitRev();
534
+ const matcher = this.errorStateMatcher() ?? this.defaultMatcher;
535
+ const form = this.parentFormGroup ??
536
+ this.parentForm;
537
+ return matcher.isErrorState(this.ngControl?.control ?? null, form);
538
+ }, ...(ngDevMode ? [{ debugName: "errorState" }] : /* istanbul ignore next */ []));
539
+ activeValue = linkedSignal(() => this.value(), ...(ngDevMode ? [{ debugName: "activeValue" }] : /* istanbul ignore next */ []));
540
+ variantResult = computed(() => radioGroupVariants({
541
+ orientation: this.orientation(),
542
+ disabled: this.isDisabled(),
543
+ }), ...(ngDevMode ? [{ debugName: "variantResult" }] : /* istanbul ignore next */ []));
544
+ rootClasses = computed(() => this.variantResult(), ...(ngDevMode ? [{ debugName: "rootClasses" }] : /* istanbul ignore next */ []));
545
+ constructor() {
546
+ // Material-style CVA wiring: declare ourselves as the value accessor on any
547
+ // host-level `NgControl` (FormControlDirective, NgModel, etc.). This avoids
548
+ // the circular-DI that a static `NG_VALUE_ACCESSOR` provider would create
549
+ // because `NgControl` is injected with `self: true` on the same element.
550
+ if (this.ngControl) {
551
+ this.ngControl.valueAccessor = this;
552
+ }
553
+ afterNextRender(() => {
554
+ if (isDevMode() && !this.ariaLabel() && !this.ariaLabelledby()) {
555
+ console.warn('[tw-radio-group] The radiogroup has no accessible name. Set `aria-label` or `aria-labelledby`.');
556
+ }
557
+ });
558
+ }
559
+ /** Selects the given value from a user interaction. Updates the model and notifies forms. */
560
+ selectValue(next) {
561
+ if (this.isDisabled())
562
+ return;
563
+ const typed = next;
564
+ if (typed === this.activeValue()) {
565
+ this.onTouched();
566
+ return;
567
+ }
568
+ this.activeValue.set(typed);
569
+ this.value.set(typed);
570
+ this.onChange(typed);
571
+ this.onTouched();
572
+ this.change.emit(typed);
573
+ }
574
+ /** @internal Called by child radios on blur — propagates to CVA onTouched and bumps the errorState revision so a blur on a child radio refreshes the group's `aria-invalid`. */
575
+ notifyTouched() {
576
+ this.onTouched();
577
+ this._ngControlRev.update((n) => n + 1);
578
+ }
579
+ onKeydown(event) {
580
+ if (this.isDisabled())
581
+ return;
582
+ const radios = this.radios();
583
+ if (radios.length === 0)
584
+ return;
585
+ const enabledCount = radios.reduce((count, r) => (r.isDisabled() ? count : count + 1), 0);
586
+ if (enabledCount === 0)
587
+ return;
588
+ const currentIdx = radios.findIndex((r) => r.value() === this.activeValue());
589
+ const startIdx = currentIdx >= 0 ? currentIdx : this.firstEnabledIndex(radios);
590
+ let targetIndex = -1;
591
+ switch (event.key) {
592
+ case 'ArrowRight':
593
+ case 'ArrowDown':
594
+ targetIndex = this.findNextEnabledIndex(radios, startIdx, 1);
595
+ break;
596
+ case 'ArrowLeft':
597
+ case 'ArrowUp':
598
+ targetIndex = this.findNextEnabledIndex(radios, startIdx, -1);
599
+ break;
600
+ case 'Home':
601
+ targetIndex = this.firstEnabledIndex(radios);
602
+ break;
603
+ case 'End':
604
+ targetIndex = this.lastEnabledIndex(radios);
605
+ break;
606
+ default:
607
+ return;
608
+ }
609
+ if (targetIndex >= 0) {
610
+ event.preventDefault();
611
+ const target = radios[targetIndex];
612
+ this.selectValue(target.value());
613
+ target.focus();
614
+ }
615
+ }
616
+ findNextEnabledIndex(radios, from, direction) {
617
+ const len = radios.length;
618
+ let idx = from;
619
+ for (let i = 0; i < len; i++) {
620
+ idx = (idx + direction + len) % len;
621
+ if (!radios[idx].isDisabled())
622
+ return idx;
623
+ }
624
+ return -1;
625
+ }
626
+ firstEnabledIndex(radios) {
627
+ for (let i = 0; i < radios.length; i++) {
628
+ if (!radios[i].isDisabled())
629
+ return i;
630
+ }
631
+ return -1;
632
+ }
633
+ lastEnabledIndex(radios) {
634
+ for (let i = radios.length - 1; i >= 0; i--) {
635
+ if (!radios[i].isDisabled())
636
+ return i;
637
+ }
638
+ return -1;
639
+ }
640
+ // ── ControlValueAccessor ──
641
+ writeValue(value) {
642
+ this.activeValue.set(value);
643
+ this.value.set(value ?? null);
644
+ }
645
+ registerOnChange(fn) {
646
+ this.onChange = fn;
647
+ }
648
+ registerOnTouched(fn) {
649
+ this.onTouched = fn;
650
+ }
651
+ setDisabledState(isDisabled) {
652
+ this.cvaDisabled.set(isDisabled);
653
+ }
654
+ ngOnInit() {
655
+ this.focusMonitor.monitor(this.elementRef);
656
+ this.destroyRef.onDestroy(() => {
657
+ this.focusMonitor.stopMonitoring(this.elementRef);
658
+ });
659
+ // NgControl's `control` is set by the parent FormControl* directive before
660
+ // children's `ngOnInit`. Subscribe here so errorState reacts to status/value
661
+ // changes on the bound control.
662
+ const ctrl = this.ngControl?.control;
663
+ if (ctrl) {
664
+ const streams = [ctrl.statusChanges, ctrl.valueChanges].filter((s) => !!s);
665
+ if (streams.length) {
666
+ merge(...streams)
667
+ .pipe(takeUntilDestroyed(this.destroyRef))
668
+ .subscribe(() => this._ngControlRev.update((n) => n + 1));
669
+ }
670
+ }
671
+ const submit = this.parentFormGroup?.ngSubmit ?? this.parentForm?.ngSubmit;
672
+ if (submit) {
673
+ submit
674
+ .pipe(takeUntilDestroyed(this.destroyRef))
675
+ .subscribe(() => this._formSubmitRev.update((n) => n + 1));
676
+ }
677
+ }
678
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: RadioGroupComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
679
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.14", type: RadioGroupComponent, isStandalone: true, selector: "tw-radio-group", inputs: { color: { classPropertyName: "color", publicName: "color", 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 }, orientation: { classPropertyName: "orientation", publicName: "orientation", 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 }, name: { classPropertyName: "name", publicName: "name", 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 }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, errorStateMatcher: { classPropertyName: "errorStateMatcher", publicName: "errorStateMatcher", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", change: "change" }, host: { attributes: { "role": "radiogroup" }, listeners: { "keydown": "onKeydown($event)" }, properties: { "id": "hostId", "class": "rootClasses()", "attr.aria-orientation": "orientation()", "attr.aria-disabled": "isDisabled() || null", "attr.aria-required": "required() || null", "attr.aria-invalid": "errorState() || null", "attr.aria-label": "ariaLabel() || null", "attr.aria-labelledby": "ariaLabelledby() || null", "attr.aria-describedby": "ariaDescribedby() || null" } }, queries: [{ propertyName: "radios", predicate: RadioComponent, descendants: true, isSignal: true }], ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
680
+ }
681
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: RadioGroupComponent, decorators: [{
682
+ type: Component,
683
+ args: [{
684
+ selector: 'tw-radio-group',
685
+ changeDetection: ChangeDetectionStrategy.OnPush,
686
+ template: `<ng-content />`,
687
+ host: {
688
+ 'role': 'radiogroup',
689
+ '[id]': 'hostId',
690
+ '[class]': 'rootClasses()',
691
+ '[attr.aria-orientation]': 'orientation()',
692
+ '[attr.aria-disabled]': 'isDisabled() || null',
693
+ '[attr.aria-required]': 'required() || null',
694
+ '[attr.aria-invalid]': 'errorState() || null',
695
+ '[attr.aria-label]': 'ariaLabel() || null',
696
+ '[attr.aria-labelledby]': 'ariaLabelledby() || null',
697
+ '[attr.aria-describedby]': 'ariaDescribedby() || null',
698
+ '(keydown)': 'onKeydown($event)',
699
+ },
700
+ }]
701
+ }], ctorParameters: () => [], propDecorators: { color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", 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 }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], errorStateMatcher: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorStateMatcher", required: false }] }], change: [{ type: i0.Output, args: ["change"] }], radios: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => RadioComponent), { ...{ descendants: true }, isSignal: true }] }] } });
702
+
703
+ /**
704
+ * Generated bundle index. Do not edit.
705
+ */
706
+
707
+ export { RadioComponent, RadioGroupComponent };
708
+ //# sourceMappingURL=cdevhub-ngx-tw-radio.mjs.map