@cdevhub/ngx-tw 0.5.0 → 0.6.1

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 (209) hide show
  1. package/README.md +78 -15
  2. package/fesm2022/cdevhub-ngx-tw-accordion.mjs +16 -5
  3. package/fesm2022/cdevhub-ngx-tw-accordion.mjs.map +1 -1
  4. package/fesm2022/cdevhub-ngx-tw-avatar.mjs +26 -15
  5. package/fesm2022/cdevhub-ngx-tw-avatar.mjs.map +1 -1
  6. package/fesm2022/cdevhub-ngx-tw-badge.mjs +17 -5
  7. package/fesm2022/cdevhub-ngx-tw-badge.mjs.map +1 -1
  8. package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs +27 -6
  9. package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs.map +1 -1
  10. package/fesm2022/cdevhub-ngx-tw-button.mjs +23 -11
  11. package/fesm2022/cdevhub-ngx-tw-button.mjs.map +1 -1
  12. package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs +5 -5
  13. package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs.map +1 -1
  14. package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs +3 -3
  15. package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs.map +1 -1
  16. package/fesm2022/cdevhub-ngx-tw-calendar.mjs +427 -122
  17. package/fesm2022/cdevhub-ngx-tw-calendar.mjs.map +1 -1
  18. package/fesm2022/cdevhub-ngx-tw-card.mjs +21 -11
  19. package/fesm2022/cdevhub-ngx-tw-card.mjs.map +1 -1
  20. package/fesm2022/cdevhub-ngx-tw-carousel.mjs +215 -96
  21. package/fesm2022/cdevhub-ngx-tw-carousel.mjs.map +1 -1
  22. package/fesm2022/cdevhub-ngx-tw-checkbox.mjs +108 -64
  23. package/fesm2022/cdevhub-ngx-tw-checkbox.mjs.map +1 -1
  24. package/fesm2022/cdevhub-ngx-tw-code-block.mjs +18 -6
  25. package/fesm2022/cdevhub-ngx-tw-code-block.mjs.map +1 -1
  26. package/fesm2022/cdevhub-ngx-tw-collapsible.mjs +55 -30
  27. package/fesm2022/cdevhub-ngx-tw-collapsible.mjs.map +1 -1
  28. package/fesm2022/cdevhub-ngx-tw-combobox-testing.mjs +218 -0
  29. package/fesm2022/cdevhub-ngx-tw-combobox-testing.mjs.map +1 -0
  30. package/fesm2022/cdevhub-ngx-tw-combobox.mjs +236 -211
  31. package/fesm2022/cdevhub-ngx-tw-combobox.mjs.map +1 -1
  32. package/fesm2022/cdevhub-ngx-tw-command-palette-testing.mjs +200 -0
  33. package/fesm2022/cdevhub-ngx-tw-command-palette-testing.mjs.map +1 -0
  34. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs +34 -7
  35. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs.map +1 -1
  36. package/fesm2022/cdevhub-ngx-tw-core.mjs +135 -15
  37. package/fesm2022/cdevhub-ngx-tw-core.mjs.map +1 -1
  38. package/fesm2022/cdevhub-ngx-tw-date-picker-testing.mjs +203 -0
  39. package/fesm2022/cdevhub-ngx-tw-date-picker-testing.mjs.map +1 -0
  40. package/fesm2022/cdevhub-ngx-tw-date-picker.mjs +160 -102
  41. package/fesm2022/cdevhub-ngx-tw-date-picker.mjs.map +1 -1
  42. package/fesm2022/cdevhub-ngx-tw-date-range-picker-testing.mjs +209 -0
  43. package/fesm2022/cdevhub-ngx-tw-date-range-picker-testing.mjs.map +1 -0
  44. package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs +182 -107
  45. package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs.map +1 -1
  46. package/fesm2022/cdevhub-ngx-tw-dialog-testing.mjs +122 -0
  47. package/fesm2022/cdevhub-ngx-tw-dialog-testing.mjs.map +1 -0
  48. package/fesm2022/cdevhub-ngx-tw-dialog.mjs +11 -2
  49. package/fesm2022/cdevhub-ngx-tw-dialog.mjs.map +1 -1
  50. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs +1 -1
  51. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs.map +1 -1
  52. package/fesm2022/cdevhub-ngx-tw-file-upload-testing.mjs +149 -0
  53. package/fesm2022/cdevhub-ngx-tw-file-upload-testing.mjs.map +1 -0
  54. package/fesm2022/cdevhub-ngx-tw-file-upload.mjs +57 -67
  55. package/fesm2022/cdevhub-ngx-tw-file-upload.mjs.map +1 -1
  56. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs +42 -24
  57. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs.map +1 -1
  58. package/fesm2022/cdevhub-ngx-tw-form-field.mjs +114 -21
  59. package/fesm2022/cdevhub-ngx-tw-form-field.mjs.map +1 -1
  60. package/fesm2022/cdevhub-ngx-tw-icon.mjs +16 -6
  61. package/fesm2022/cdevhub-ngx-tw-icon.mjs.map +1 -1
  62. package/fesm2022/cdevhub-ngx-tw-input.mjs +52 -61
  63. package/fesm2022/cdevhub-ngx-tw-input.mjs.map +1 -1
  64. package/fesm2022/cdevhub-ngx-tw-item.mjs +28 -4
  65. package/fesm2022/cdevhub-ngx-tw-item.mjs.map +1 -1
  66. package/fesm2022/cdevhub-ngx-tw-menu-testing.mjs +168 -0
  67. package/fesm2022/cdevhub-ngx-tw-menu-testing.mjs.map +1 -0
  68. package/fesm2022/cdevhub-ngx-tw-menu.mjs +58 -21
  69. package/fesm2022/cdevhub-ngx-tw-menu.mjs.map +1 -1
  70. package/fesm2022/cdevhub-ngx-tw-number-input.mjs +34 -1
  71. package/fesm2022/cdevhub-ngx-tw-number-input.mjs.map +1 -1
  72. package/fesm2022/cdevhub-ngx-tw-paginator.mjs +95 -45
  73. package/fesm2022/cdevhub-ngx-tw-paginator.mjs.map +1 -1
  74. package/fesm2022/cdevhub-ngx-tw-popover.mjs +81 -32
  75. package/fesm2022/cdevhub-ngx-tw-popover.mjs.map +1 -1
  76. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs +1 -1
  77. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs.map +1 -1
  78. package/fesm2022/cdevhub-ngx-tw-radio.mjs +131 -98
  79. package/fesm2022/cdevhub-ngx-tw-radio.mjs.map +1 -1
  80. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs +207 -42
  81. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs.map +1 -1
  82. package/fesm2022/cdevhub-ngx-tw-select-testing.mjs +165 -0
  83. package/fesm2022/cdevhub-ngx-tw-select-testing.mjs.map +1 -0
  84. package/fesm2022/cdevhub-ngx-tw-select.mjs +386 -210
  85. package/fesm2022/cdevhub-ngx-tw-select.mjs.map +1 -1
  86. package/fesm2022/{cdevhub-ngx-tw-sheet-sheet-renderer-ByyNluUd.mjs → cdevhub-ngx-tw-sheet-sheet-renderer-CqVJwgmp.mjs} +3 -3
  87. package/fesm2022/cdevhub-ngx-tw-sheet-sheet-renderer-CqVJwgmp.mjs.map +1 -0
  88. package/fesm2022/cdevhub-ngx-tw-sheet-testing.mjs +129 -0
  89. package/fesm2022/cdevhub-ngx-tw-sheet-testing.mjs.map +1 -0
  90. package/fesm2022/cdevhub-ngx-tw-sheet.mjs +23 -7
  91. package/fesm2022/cdevhub-ngx-tw-sheet.mjs.map +1 -1
  92. package/fesm2022/cdevhub-ngx-tw-slider.mjs +89 -54
  93. package/fesm2022/cdevhub-ngx-tw-slider.mjs.map +1 -1
  94. package/fesm2022/cdevhub-ngx-tw-sort.mjs +122 -17
  95. package/fesm2022/cdevhub-ngx-tw-sort.mjs.map +1 -1
  96. package/fesm2022/cdevhub-ngx-tw-split.mjs +72 -19
  97. package/fesm2022/cdevhub-ngx-tw-split.mjs.map +1 -1
  98. package/fesm2022/cdevhub-ngx-tw-stat.mjs +63 -25
  99. package/fesm2022/cdevhub-ngx-tw-stat.mjs.map +1 -1
  100. package/fesm2022/cdevhub-ngx-tw-stepper.mjs +35 -8
  101. package/fesm2022/cdevhub-ngx-tw-stepper.mjs.map +1 -1
  102. package/fesm2022/cdevhub-ngx-tw-switch.mjs +54 -48
  103. package/fesm2022/cdevhub-ngx-tw-switch.mjs.map +1 -1
  104. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs +12 -15
  105. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs.map +1 -1
  106. package/fesm2022/cdevhub-ngx-tw-table.mjs +74 -13
  107. package/fesm2022/cdevhub-ngx-tw-table.mjs.map +1 -1
  108. package/fesm2022/cdevhub-ngx-tw-tabs.mjs +84 -30
  109. package/fesm2022/cdevhub-ngx-tw-tabs.mjs.map +1 -1
  110. package/fesm2022/cdevhub-ngx-tw-tags-input-testing.mjs +172 -0
  111. package/fesm2022/cdevhub-ngx-tw-tags-input-testing.mjs.map +1 -0
  112. package/fesm2022/cdevhub-ngx-tw-tags-input.mjs +107 -71
  113. package/fesm2022/cdevhub-ngx-tw-tags-input.mjs.map +1 -1
  114. package/fesm2022/cdevhub-ngx-tw-textarea.mjs +41 -9
  115. package/fesm2022/cdevhub-ngx-tw-textarea.mjs.map +1 -1
  116. package/fesm2022/cdevhub-ngx-tw-theme.mjs +257 -45
  117. package/fesm2022/cdevhub-ngx-tw-theme.mjs.map +1 -1
  118. package/fesm2022/cdevhub-ngx-tw-time-picker-testing.mjs +201 -0
  119. package/fesm2022/cdevhub-ngx-tw-time-picker-testing.mjs.map +1 -0
  120. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs +184 -77
  121. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs.map +1 -1
  122. package/fesm2022/cdevhub-ngx-tw-timeline.mjs +110 -27
  123. package/fesm2022/cdevhub-ngx-tw-timeline.mjs.map +1 -1
  124. package/fesm2022/{cdevhub-ngx-tw-toast-toast-renderer-DSu4YoTy.mjs → cdevhub-ngx-tw-toast-toast-renderer-BlDxemsU.mjs} +107 -19
  125. package/fesm2022/cdevhub-ngx-tw-toast-toast-renderer-BlDxemsU.mjs.map +1 -0
  126. package/fesm2022/cdevhub-ngx-tw-toast.mjs +21 -13
  127. package/fesm2022/cdevhub-ngx-tw-toast.mjs.map +1 -1
  128. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs +102 -15
  129. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs.map +1 -1
  130. package/fesm2022/cdevhub-ngx-tw-transfer-testing.mjs +219 -0
  131. package/fesm2022/cdevhub-ngx-tw-transfer-testing.mjs.map +1 -0
  132. package/fesm2022/cdevhub-ngx-tw-transfer.mjs +88 -69
  133. package/fesm2022/cdevhub-ngx-tw-transfer.mjs.map +1 -1
  134. package/fesm2022/cdevhub-ngx-tw-tree.mjs +68 -19
  135. package/fesm2022/cdevhub-ngx-tw-tree.mjs.map +1 -1
  136. package/index.json +1 -1
  137. package/package.json +63 -2
  138. package/theme/_base.css +13 -0
  139. package/theme/_dark.css +111 -36
  140. package/theme/_high-contrast-dark.css +367 -0
  141. package/theme/_high-contrast.css +13 -2
  142. package/theme/_light.css +332 -0
  143. package/theme/_semantic.css +60 -26
  144. package/theme/_typography.css +17 -0
  145. package/theme/index.css +86 -7
  146. package/types/cdevhub-ngx-tw-accordion.d.ts +12 -4
  147. package/types/cdevhub-ngx-tw-avatar.d.ts +10 -3
  148. package/types/cdevhub-ngx-tw-button.d.ts +1 -0
  149. package/types/cdevhub-ngx-tw-calendar-luxon.d.ts +2 -2
  150. package/types/cdevhub-ngx-tw-calendar-testing.d.ts +1 -1
  151. package/types/cdevhub-ngx-tw-calendar.d.ts +179 -73
  152. package/types/cdevhub-ngx-tw-card.d.ts +11 -3
  153. package/types/cdevhub-ngx-tw-carousel.d.ts +89 -20
  154. package/types/cdevhub-ngx-tw-checkbox.d.ts +40 -12
  155. package/types/cdevhub-ngx-tw-code-block.d.ts +11 -2
  156. package/types/cdevhub-ngx-tw-collapsible.d.ts +32 -5
  157. package/types/cdevhub-ngx-tw-combobox-testing.d.ts +157 -0
  158. package/types/cdevhub-ngx-tw-combobox.d.ts +94 -53
  159. package/types/cdevhub-ngx-tw-command-palette-testing.d.ts +136 -0
  160. package/types/cdevhub-ngx-tw-command-palette.d.ts +33 -5
  161. package/types/cdevhub-ngx-tw-core.d.ts +152 -21
  162. package/types/cdevhub-ngx-tw-date-picker-testing.d.ts +141 -0
  163. package/types/cdevhub-ngx-tw-date-picker.d.ts +37 -29
  164. package/types/cdevhub-ngx-tw-date-range-picker-testing.d.ts +148 -0
  165. package/types/cdevhub-ngx-tw-date-range-picker.d.ts +36 -29
  166. package/types/cdevhub-ngx-tw-dialog-testing.d.ts +98 -0
  167. package/types/cdevhub-ngx-tw-dialog.d.ts +12 -3
  168. package/types/cdevhub-ngx-tw-file-upload-testing.d.ts +120 -0
  169. package/types/cdevhub-ngx-tw-file-upload.d.ts +8 -10
  170. package/types/cdevhub-ngx-tw-flip-card.d.ts +18 -11
  171. package/types/cdevhub-ngx-tw-form-field.d.ts +19 -2
  172. package/types/cdevhub-ngx-tw-icon.d.ts +3 -3
  173. package/types/cdevhub-ngx-tw-input.d.ts +5 -8
  174. package/types/cdevhub-ngx-tw-item.d.ts +2 -0
  175. package/types/cdevhub-ngx-tw-menu-testing.d.ts +131 -0
  176. package/types/cdevhub-ngx-tw-menu.d.ts +2 -2
  177. package/types/cdevhub-ngx-tw-number-input.d.ts +20 -0
  178. package/types/cdevhub-ngx-tw-paginator.d.ts +58 -24
  179. package/types/cdevhub-ngx-tw-popover.d.ts +41 -10
  180. package/types/cdevhub-ngx-tw-progress-bar.d.ts +1 -1
  181. package/types/cdevhub-ngx-tw-radio.d.ts +68 -25
  182. package/types/cdevhub-ngx-tw-segmented-control.d.ts +79 -16
  183. package/types/cdevhub-ngx-tw-select-testing.d.ts +114 -0
  184. package/types/cdevhub-ngx-tw-select.d.ts +104 -33
  185. package/types/cdevhub-ngx-tw-sheet-testing.d.ts +104 -0
  186. package/types/cdevhub-ngx-tw-sheet.d.ts +19 -3
  187. package/types/cdevhub-ngx-tw-slider.d.ts +79 -25
  188. package/types/cdevhub-ngx-tw-sort.d.ts +45 -4
  189. package/types/cdevhub-ngx-tw-split.d.ts +32 -2
  190. package/types/cdevhub-ngx-tw-stat.d.ts +14 -4
  191. package/types/cdevhub-ngx-tw-stepper.d.ts +16 -2
  192. package/types/cdevhub-ngx-tw-switch.d.ts +35 -13
  193. package/types/cdevhub-ngx-tw-tab-nav.d.ts +7 -6
  194. package/types/cdevhub-ngx-tw-table.d.ts +53 -10
  195. package/types/cdevhub-ngx-tw-tabs.d.ts +41 -10
  196. package/types/cdevhub-ngx-tw-tags-input-testing.d.ts +135 -0
  197. package/types/cdevhub-ngx-tw-tags-input.d.ts +26 -11
  198. package/types/cdevhub-ngx-tw-textarea.d.ts +3 -2
  199. package/types/cdevhub-ngx-tw-theme.d.ts +245 -39
  200. package/types/cdevhub-ngx-tw-time-picker-testing.d.ts +129 -0
  201. package/types/cdevhub-ngx-tw-time-picker.d.ts +76 -19
  202. package/types/cdevhub-ngx-tw-timeline.d.ts +36 -12
  203. package/types/cdevhub-ngx-tw-toast.d.ts +20 -6
  204. package/types/cdevhub-ngx-tw-tooltip.d.ts +28 -6
  205. package/types/cdevhub-ngx-tw-transfer-testing.d.ts +152 -0
  206. package/types/cdevhub-ngx-tw-transfer.d.ts +21 -8
  207. package/types/cdevhub-ngx-tw-tree.d.ts +6 -1
  208. package/fesm2022/cdevhub-ngx-tw-sheet-sheet-renderer-ByyNluUd.mjs.map +0 -1
  209. package/fesm2022/cdevhub-ngx-tw-toast-toast-renderer-DSu4YoTy.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"cdevhub-ngx-tw-progress-bar.mjs","sources":["../../../projects/ngx-tw/progress-bar/progress-bar.ts","../../../projects/ngx-tw/progress-bar/cdevhub-ngx-tw-progress-bar.ts"],"sourcesContent":["/**\n * `tw-progress-bar` — visualises measurable progress.\n *\n * Composition usecases:\n * - Upload progress (determinate linear with `options.formatter` → `\"3.2 MB / 10 MB\"`).\n * - Multi-step wizard (segmented variant, `options.segments` = step count).\n * - Skill / rating display (static determinate linear).\n * - Inline list-row progress (compact `size=\"sm\"`, constrained width via host class).\n * - Card-footer task completion (`options.showValue=true`, `color` switches to `success`/`error`).\n * - Indeterminate loading before first byte (omit `value` — the bar sweeps).\n *\n * For unknown-duration operations with no measurable progress, use `tw-spinner` instead.\n */\nimport {\n ChangeDetectionStrategy,\n Component,\n computed,\n effect,\n input,\n untracked,\n} from '@angular/core';\nimport { tv } from 'tailwind-variants';\nimport type { TwColor } from '@cdevhub/ngx-tw/core';\n\n// `ngDevMode` is a globalThis flag set by Angular's build tooling — true in dev,\n// `false` in production builds. The `typeof` guard keeps the warning path safe\n// in environments (SSR, ad-hoc test harnesses) where the global is undeclared.\n// Preferred over `isDevMode()` because the latter is a runtime function call;\n// `ngDevMode` is a build-time constant the bundler can dead-code-eliminate.\ndeclare const ngDevMode: boolean | undefined;\n\n/** Visual style of the progress bar. */\nexport type ProgressBarVariant = 'linear' | 'segmented';\n\n/** Size scale specific to progress bars (bar thickness). */\nexport type ProgressBarSize = 'sm' | 'md' | 'lg';\n\n/** Function signature for formatting the visible / announced progress value. */\nexport type ProgressBarValueFormatter = (value: number, max: number, min: number) => string;\n\n/**\n * Non-visual configuration bundled into a single input to keep the component's public\n * surface small. Every field is optional and falls back to a sensible default.\n */\nexport interface ProgressBarOptions {\n /** Lower bound of the value range. Defaults to `0`. */\n min?: number;\n /** Upper bound of the value range. Defaults to `100`. */\n max?: number;\n /** Number of equal cells when `variant` is `'segmented'`. Ignored for `'linear'`. Defaults to `5`. */\n segments?: number;\n /** When true, renders the formatted progress value next to the label. Defaults to `false`. */\n showValue?: boolean;\n /** Custom formatter for the displayed and announced value. Defaults to an integer percentage, e.g. `'42%'`. */\n formatter?: ProgressBarValueFormatter;\n /** Accessible name when no visible `label` is provided. Mirrored to `aria-label` on the progressbar element. */\n ariaLabel?: string;\n /** ID of an external element that labels the progress bar. Mirrored to `aria-labelledby`. */\n ariaLabelledby?: string;\n}\n\n// ── Static fill-color lookup (Tailwind v4 scans class strings statically) ──\n\nconst FILL_COLORS: Record<TwColor, string> = {\n primary: 'bg-primary-500',\n secondary: 'bg-secondary-500',\n accent: 'bg-accent-500',\n neutral: 'bg-fg',\n info: 'bg-info-500',\n success: 'bg-success-500',\n warning: 'bg-warning-500',\n error: 'bg-error-500',\n};\n\n// ── tv() config ──\n\nconst progressBarVariants = tv({\n slots: {\n root: 'flex flex-col w-full',\n header: 'flex items-center justify-between gap-3',\n label: 'text-xs text-fg-muted',\n valueText: 'text-xs text-fg-muted font-medium tabular-nums',\n rail: 'relative w-full overflow-hidden rounded-full bg-surface-muted',\n fill: 'absolute inset-y-0 left-0 rounded-full',\n segmentList: 'flex w-full gap-1',\n segment: 'flex-1 rounded-full',\n },\n variants: {\n size: {\n sm: { rail: 'h-1', segment: 'h-1' },\n md: { rail: 'h-2', segment: 'h-2' },\n lg: { rail: 'h-3', segment: 'h-3' },\n },\n variant: {\n linear: { segmentList: 'hidden' },\n segmented: { rail: 'hidden' },\n },\n isIndeterminate: {\n true: { fill: 'w-[30%] animate-progress-bar-indeterminate' },\n false: { fill: 'transition-[width] duration-normal motion-reduce:transition-none' },\n },\n hasHeader: {\n true: { root: 'gap-1.5' },\n false: { root: 'gap-0' },\n },\n },\n defaultVariants: {\n size: 'md',\n variant: 'linear',\n isIndeterminate: false,\n hasHeader: false,\n },\n}, {\n twMerge: true,\n});\n\nlet nextId = 0;\n\nconst DEFAULT_FORMATTER: ProgressBarValueFormatter = (value, max, min) => {\n const range = max - min;\n if (range <= 0) return '0%';\n return `${Math.round(((value - min) / range) * 100)}%`;\n};\n\n/**\n * Indicates progress toward the completion of a task.\n *\n * @example\n * ```html\n * <tw-progress-bar [value]=\"40\" />\n * <tw-progress-bar label=\"Fetching records\" />\n * <tw-progress-bar\n * variant=\"segmented\"\n * [value]=\"step() * 25\"\n * [options]=\"{ segments: 4 }\"\n * />\n * ```\n */\n@Component({\n selector: 'tw-progress-bar',\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n '[class]': 'rootClasses()',\n },\n template: `\n @if (hasHeader()) {\n <div [class]=\"headerClasses()\">\n @if (label(); as lbl) {\n <span [id]=\"labelId\" [class]=\"labelClasses()\">{{ lbl }}</span>\n } @else {\n <span></span>\n }\n @if (resolvedShowValue() && !isIndeterminate()) {\n <span [class]=\"valueTextClasses()\">{{ formattedValue() }}</span>\n }\n </div>\n }\n\n @if (variant() === 'linear') {\n <div\n [class]=\"railClasses()\"\n role=\"progressbar\"\n [attr.aria-valuemin]=\"resolvedMin()\"\n [attr.aria-valuemax]=\"resolvedMax()\"\n [attr.aria-valuenow]=\"isIndeterminate() ? null : clampedValue()\"\n [attr.aria-valuetext]=\"isIndeterminate() ? null : formattedValue()\"\n [attr.aria-busy]=\"isIndeterminate() ? true : null\"\n [attr.aria-label]=\"resolvedAriaLabel()\"\n [attr.aria-labelledby]=\"resolvedAriaLabelledby()\"\n >\n @if (isIndeterminate()) {\n <span [class]=\"fillClasses()\"></span>\n } @else {\n <span [class]=\"fillClasses()\" [style.width.%]=\"progressRatio() * 100\"></span>\n }\n </div>\n } @else {\n <div\n [class]=\"segmentListClasses()\"\n role=\"progressbar\"\n [attr.aria-valuemin]=\"resolvedMin()\"\n [attr.aria-valuemax]=\"resolvedMax()\"\n [attr.aria-valuenow]=\"isIndeterminate() ? null : clampedValue()\"\n [attr.aria-valuetext]=\"isIndeterminate() ? null : formattedValue()\"\n [attr.aria-busy]=\"isIndeterminate() ? true : null\"\n [attr.aria-label]=\"resolvedAriaLabel()\"\n [attr.aria-labelledby]=\"resolvedAriaLabelledby()\"\n >\n @for (i of segmentIndices(); track i) {\n <span [class]=\"segmentClassFor(i)\"></span>\n }\n </div>\n }\n `,\n})\nexport class ProgressBarComponent {\n /** Current progress value. When null or undefined, the bar renders indeterminate. Values outside `[options.min, options.max]` are clamped. */\n readonly value = input<number | null | undefined>(null);\n\n /** Visual style of the bar. `'linear'` renders a single fill; `'segmented'` splits the rail into discrete steps. Defaults to `'linear'`. */\n readonly variant = input<ProgressBarVariant>('linear');\n\n /** Semantic color of the filled portion. Defaults to `'primary'`. Use status colors (`success`/`warning`/`error`) to reflect task outcome. */\n readonly color = input<TwColor>('primary');\n\n /** Bar thickness. `'sm'` = h-1, `'md'` = h-2, `'lg'` = h-3. Defaults to `'md'`. */\n readonly size = input<ProgressBarSize>('md');\n\n /** Visible label rendered above the bar. When set, the bar is wired to the label via `aria-labelledby`. */\n readonly label = input<string | undefined>(undefined);\n\n /** Bundles non-visual configuration: value range (`min`/`max`/`segments`), value readout (`showValue`/`formatter`), and accessibility fallbacks (`ariaLabel`/`ariaLabelledby`). Every field is optional. */\n readonly options = input<ProgressBarOptions | undefined>(undefined);\n\n /** @internal */\n readonly labelId = `tw-progress-bar-${nextId++}-label`;\n\n /** @internal */\n readonly resolvedMin = computed(() => this.options()?.min ?? 0);\n\n /** @internal */\n readonly resolvedMax = computed(() => this.options()?.max ?? 100);\n\n /** @internal */\n readonly resolvedSegments = computed(() => this.options()?.segments ?? 5);\n\n /** @internal */\n readonly resolvedShowValue = computed(() => this.options()?.showValue ?? false);\n\n /** @internal */\n readonly isIndeterminate = computed(() => {\n const v = this.value();\n return v === null || v === undefined;\n });\n\n /** @internal */\n readonly clampedValue = computed(() => {\n const v = this.value();\n if (v === null || v === undefined) return this.resolvedMin();\n const lo = this.resolvedMin();\n const hi = this.resolvedMax();\n if (v < lo) return lo;\n if (v > hi) return hi;\n return v;\n });\n\n /** @internal */\n readonly progressRatio = computed(() => {\n if (this.isIndeterminate()) return 0;\n const range = this.resolvedMax() - this.resolvedMin();\n if (range <= 0) return 0;\n return (this.clampedValue() - this.resolvedMin()) / range;\n });\n\n /** @internal */\n readonly formattedValue = computed(() => {\n if (this.isIndeterminate()) return '';\n const fmt = this.options()?.formatter ?? DEFAULT_FORMATTER;\n return fmt(this.clampedValue(), this.resolvedMax(), this.resolvedMin());\n });\n\n /** @internal */\n readonly hasHeader = computed(() => !!this.label() || this.resolvedShowValue());\n\n /** @internal */\n readonly segmentIndices = computed(() => {\n const n = Math.max(1, Math.floor(this.resolvedSegments()));\n return Array.from({ length: n }, (_, i) => i);\n });\n\n /** @internal */\n readonly resolvedAriaLabel = computed(() => {\n if (this.label()) return null;\n if (this.options()?.ariaLabelledby) return null;\n return this.options()?.ariaLabel ?? null;\n });\n\n /** @internal */\n readonly resolvedAriaLabelledby = computed(() => {\n if (this.label()) return this.labelId;\n return this.options()?.ariaLabelledby ?? null;\n });\n\n // ── tv() slot computeds ──\n\n private readonly variantResult = computed(() =>\n progressBarVariants({\n size: this.size(),\n variant: this.variant(),\n isIndeterminate: this.isIndeterminate(),\n hasHeader: this.hasHeader(),\n }),\n );\n\n readonly rootClasses = computed(() => this.variantResult().root());\n readonly headerClasses = computed(() => this.variantResult().header());\n readonly labelClasses = computed(() => this.variantResult().label());\n readonly valueTextClasses = computed(() => this.variantResult().valueText());\n readonly railClasses = computed(() => this.variantResult().rail());\n readonly fillClasses = computed(() => `${this.variantResult().fill()} ${FILL_COLORS[this.color()]}`);\n readonly segmentListClasses = computed(() => this.variantResult().segmentList());\n private readonly segmentBaseClasses = computed(() => this.variantResult().segment());\n\n /** @internal */\n segmentClassFor(index: number): string {\n const base = this.segmentBaseClasses();\n const filled = (index + 1) / this.segmentIndices().length <= this.progressRatio();\n const bg = filled ? FILL_COLORS[this.color()] : 'bg-surface-muted';\n return `${base} ${bg}`;\n }\n\n // ── Dev-mode accessible-name warning (fires once per instance) ──\n //\n // The effect is reactive on purpose: if a consumer mounts the component\n // without a label and later removes the only accessible name they had, the\n // warning still fires on the *first* render where no name is present. Once\n // it has fired, `warned` short-circuits further evaluations. Production\n // builds skip the entire effect setup because `ngDevMode` is statically\n // `false` and the constructor returns early.\n\n constructor() {\n if (typeof ngDevMode !== 'undefined' && !ngDevMode) return;\n let warned = false;\n effect(() => {\n if (warned) return;\n const opts = this.options();\n if (this.label() || opts?.ariaLabel || opts?.ariaLabelledby) return;\n // `untracked` wraps the side-effect so the console.warn / flag mutation\n // can never create a reactive subscription — defensive in case future\n // work adds signal reads inside the warning block.\n untracked(() => {\n warned = true;\n console.warn(\n 'tw-progress-bar: no accessible name provided. Supply one of `label`, `options.ariaLabel`, or `options.ariaLabelledby`.',\n );\n });\n });\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cdevhub-ngx-tw-progress-bar.mjs","sources":["../../../projects/ngx-tw/progress-bar/progress-bar.ts","../../../projects/ngx-tw/progress-bar/cdevhub-ngx-tw-progress-bar.ts"],"sourcesContent":["/**\n * `tw-progress-bar` — visualises measurable progress.\n *\n * Composition usecases:\n * - Upload progress (determinate linear with `options.formatter` → `\"3.2 MB / 10 MB\"`).\n * - Multi-step wizard (segmented variant, `options.segments` = step count).\n * - Skill / rating display (static determinate linear).\n * - Inline list-row progress (compact `size=\"sm\"`, constrained width via host class).\n * - Card-footer task completion (`options.showValue=true`, `color` switches to `success`/`error`).\n * - Indeterminate loading before first byte (omit `value` — the bar sweeps).\n *\n * For unknown-duration operations with no measurable progress, use `tw-spinner` instead.\n */\nimport {\n ChangeDetectionStrategy,\n Component,\n computed,\n effect,\n input,\n untracked,\n} from '@angular/core';\nimport { tv } from 'tailwind-variants';\nimport type { TwColor } from '@cdevhub/ngx-tw/core';\n\n// `ngDevMode` is a globalThis flag set by Angular's build tooling — true in dev,\n// `false` in production builds. The `typeof` guard keeps the warning path safe\n// in environments (SSR, ad-hoc test harnesses) where the global is undeclared.\n// Preferred over `isDevMode()` because the latter is a runtime function call;\n// `ngDevMode` is a build-time constant the bundler can dead-code-eliminate.\ndeclare const ngDevMode: boolean | undefined;\n\n/** Visual style of the progress bar. */\nexport type ProgressBarVariant = 'linear' | 'segmented';\n\n/** Size scale specific to progress bars (bar thickness). */\nexport type ProgressBarSize = 'sm' | 'md' | 'lg';\n\n/** Function signature for formatting the visible / announced progress value. */\nexport type ProgressBarValueFormatter = (value: number, max: number, min: number) => string;\n\n/**\n * Non-visual configuration bundled into a single input to keep the component's public\n * surface small. Every field is optional and falls back to a sensible default.\n */\nexport interface ProgressBarOptions {\n /** Lower bound of the value range. Defaults to `0`. */\n min?: number;\n /** Upper bound of the value range. Defaults to `100`. */\n max?: number;\n /** Number of equal cells when `variant` is `'segmented'`. Ignored for `'linear'`. Defaults to `5`. */\n segments?: number;\n /** When true, renders the formatted progress value next to the label. Defaults to `false`. */\n showValue?: boolean;\n /** Custom formatter for the displayed and announced value. Defaults to an integer percentage, e.g. `'42%'`. */\n formatter?: ProgressBarValueFormatter;\n /** Accessible name when no visible `label` is provided. Mirrored to `aria-label` on the progressbar element. */\n ariaLabel?: string;\n /** ID of an external element that labels the progress bar. Mirrored to `aria-labelledby`. */\n ariaLabelledby?: string;\n}\n\n// ── Static fill-color lookup (Tailwind v4 scans class strings statically) ──\n\nconst FILL_COLORS: Record<TwColor, string> = {\n primary: 'bg-primary-500',\n secondary: 'bg-secondary-500',\n accent: 'bg-accent-500',\n neutral: 'bg-fg',\n info: 'bg-info-500',\n success: 'bg-success-500',\n warning: 'bg-warning-500',\n error: 'bg-error-500',\n};\n\n// ── tv() config ──\n\nconst progressBarVariants = tv({\n slots: {\n root: 'flex flex-col w-full',\n header: 'flex items-center justify-between gap-3',\n label: 'text-xs text-fg-muted',\n valueText: 'text-xs text-fg-muted font-medium tabular-nums',\n rail: 'relative w-full overflow-hidden rounded-full bg-surface-muted',\n fill: 'absolute inset-y-0 left-0 rounded-full',\n segmentList: 'flex w-full gap-1',\n segment: 'flex-1 rounded-full',\n },\n variants: {\n size: {\n sm: { rail: 'h-1', segment: 'h-1' },\n md: { rail: 'h-2', segment: 'h-2' },\n lg: { rail: 'h-3', segment: 'h-3' },\n },\n variant: {\n linear: { segmentList: 'hidden' },\n segmented: { rail: 'hidden' },\n },\n isIndeterminate: {\n true: { fill: 'w-[30%] animate-progress-bar-indeterminate' },\n false: { fill: 'transition-[width] duration-normal motion-reduce:transition-none' },\n },\n hasHeader: {\n true: { root: 'gap-1.5' },\n false: { root: 'gap-0' },\n },\n },\n defaultVariants: {\n size: 'md',\n variant: 'linear',\n isIndeterminate: false,\n hasHeader: false,\n },\n}, {\n twMerge: true,\n});\n\nlet nextId = 0;\n\nconst DEFAULT_FORMATTER: ProgressBarValueFormatter = (value, max, min) => {\n const range = max - min;\n if (range <= 0) return '0%';\n return `${Math.round(((value - min) / range) * 100)}%`;\n};\n\n/**\n * Indicates progress toward the completion of a task.\n *\n * @example\n * ```html\n * <tw-progress-bar [value]=\"40\" />\n * <tw-progress-bar label=\"Fetching records\" />\n * <tw-progress-bar\n * variant=\"segmented\"\n * [value]=\"step() * 25\"\n * [options]=\"{ segments: 4 }\"\n * />\n * ```\n */\n@Component({\n selector: 'tw-progress-bar',\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n '[class]': 'rootClasses()',\n },\n template: `\n @if (hasHeader()) {\n <div [class]=\"headerClasses()\">\n @if (label(); as lbl) {\n <span [id]=\"labelId\" [class]=\"labelClasses()\">{{ lbl }}</span>\n } @else {\n <span></span>\n }\n @if (resolvedShowValue() && !isIndeterminate()) {\n <span [class]=\"valueTextClasses()\">{{ formattedValue() }}</span>\n }\n </div>\n }\n\n @if (variant() === 'linear') {\n <div\n [class]=\"railClasses()\"\n role=\"progressbar\"\n [attr.aria-valuemin]=\"resolvedMin()\"\n [attr.aria-valuemax]=\"resolvedMax()\"\n [attr.aria-valuenow]=\"isIndeterminate() ? null : clampedValue()\"\n [attr.aria-valuetext]=\"isIndeterminate() ? null : formattedValue()\"\n [attr.aria-busy]=\"isIndeterminate() ? true : null\"\n [attr.aria-label]=\"resolvedAriaLabel()\"\n [attr.aria-labelledby]=\"resolvedAriaLabelledby()\"\n >\n @if (isIndeterminate()) {\n <span [class]=\"fillClasses()\"></span>\n } @else {\n <span [class]=\"fillClasses()\" [style.width.%]=\"progressRatio() * 100\"></span>\n }\n </div>\n } @else {\n <div\n [class]=\"segmentListClasses()\"\n role=\"progressbar\"\n [attr.aria-valuemin]=\"resolvedMin()\"\n [attr.aria-valuemax]=\"resolvedMax()\"\n [attr.aria-valuenow]=\"isIndeterminate() ? null : clampedValue()\"\n [attr.aria-valuetext]=\"isIndeterminate() ? null : formattedValue()\"\n [attr.aria-busy]=\"isIndeterminate() ? true : null\"\n [attr.aria-label]=\"resolvedAriaLabel()\"\n [attr.aria-labelledby]=\"resolvedAriaLabelledby()\"\n >\n @for (i of segmentIndices(); track i) {\n <span [class]=\"segmentClassFor(i)\"></span>\n }\n </div>\n }\n `,\n})\nexport class ProgressBarComponent {\n /** Current progress value. When null or undefined, the bar renders indeterminate. Values outside `[options.min, options.max]` are clamped. Defaults to `null`. */\n readonly value = input<number | null | undefined>(null);\n\n /** Visual style of the bar. `'linear'` renders a single fill; `'segmented'` splits the rail into discrete steps. Defaults to `'linear'`. */\n readonly variant = input<ProgressBarVariant>('linear');\n\n /** Semantic color of the filled portion. Defaults to `'primary'`. Use status colors (`success`/`warning`/`error`) to reflect task outcome. */\n readonly color = input<TwColor>('primary');\n\n /** Bar thickness. `'sm'` = h-1, `'md'` = h-2, `'lg'` = h-3. Defaults to `'md'`. */\n readonly size = input<ProgressBarSize>('md');\n\n /** Visible label rendered above the bar. When set, the bar is wired to the label via `aria-labelledby`. */\n readonly label = input<string | undefined>(undefined);\n\n /** Bundles non-visual configuration: value range (`min`/`max`/`segments`), value readout (`showValue`/`formatter`), and accessibility fallbacks (`ariaLabel`/`ariaLabelledby`). Every field is optional. */\n readonly options = input<ProgressBarOptions | undefined>(undefined);\n\n /** @internal */\n readonly labelId = `tw-progress-bar-${nextId++}-label`;\n\n /** @internal */\n readonly resolvedMin = computed(() => this.options()?.min ?? 0);\n\n /** @internal */\n readonly resolvedMax = computed(() => this.options()?.max ?? 100);\n\n /** @internal */\n readonly resolvedSegments = computed(() => this.options()?.segments ?? 5);\n\n /** @internal */\n readonly resolvedShowValue = computed(() => this.options()?.showValue ?? false);\n\n /** @internal */\n readonly isIndeterminate = computed(() => {\n const v = this.value();\n return v === null || v === undefined;\n });\n\n /** @internal */\n readonly clampedValue = computed(() => {\n const v = this.value();\n if (v === null || v === undefined) return this.resolvedMin();\n const lo = this.resolvedMin();\n const hi = this.resolvedMax();\n if (v < lo) return lo;\n if (v > hi) return hi;\n return v;\n });\n\n /** @internal */\n readonly progressRatio = computed(() => {\n if (this.isIndeterminate()) return 0;\n const range = this.resolvedMax() - this.resolvedMin();\n if (range <= 0) return 0;\n return (this.clampedValue() - this.resolvedMin()) / range;\n });\n\n /** @internal */\n readonly formattedValue = computed(() => {\n if (this.isIndeterminate()) return '';\n const fmt = this.options()?.formatter ?? DEFAULT_FORMATTER;\n return fmt(this.clampedValue(), this.resolvedMax(), this.resolvedMin());\n });\n\n /** @internal */\n readonly hasHeader = computed(() => !!this.label() || this.resolvedShowValue());\n\n /** @internal */\n readonly segmentIndices = computed(() => {\n const n = Math.max(1, Math.floor(this.resolvedSegments()));\n return Array.from({ length: n }, (_, i) => i);\n });\n\n /** @internal */\n readonly resolvedAriaLabel = computed(() => {\n if (this.label()) return null;\n if (this.options()?.ariaLabelledby) return null;\n return this.options()?.ariaLabel ?? null;\n });\n\n /** @internal */\n readonly resolvedAriaLabelledby = computed(() => {\n if (this.label()) return this.labelId;\n return this.options()?.ariaLabelledby ?? null;\n });\n\n // ── tv() slot computeds ──\n\n private readonly variantResult = computed(() =>\n progressBarVariants({\n size: this.size(),\n variant: this.variant(),\n isIndeterminate: this.isIndeterminate(),\n hasHeader: this.hasHeader(),\n }),\n );\n\n readonly rootClasses = computed(() => this.variantResult().root());\n readonly headerClasses = computed(() => this.variantResult().header());\n readonly labelClasses = computed(() => this.variantResult().label());\n readonly valueTextClasses = computed(() => this.variantResult().valueText());\n readonly railClasses = computed(() => this.variantResult().rail());\n readonly fillClasses = computed(() => `${this.variantResult().fill()} ${FILL_COLORS[this.color()]}`);\n readonly segmentListClasses = computed(() => this.variantResult().segmentList());\n private readonly segmentBaseClasses = computed(() => this.variantResult().segment());\n\n /** @internal */\n segmentClassFor(index: number): string {\n const base = this.segmentBaseClasses();\n const filled = (index + 1) / this.segmentIndices().length <= this.progressRatio();\n const bg = filled ? FILL_COLORS[this.color()] : 'bg-surface-muted';\n return `${base} ${bg}`;\n }\n\n // ── Dev-mode accessible-name warning (fires once per instance) ──\n //\n // The effect is reactive on purpose: if a consumer mounts the component\n // without a label and later removes the only accessible name they had, the\n // warning still fires on the *first* render where no name is present. Once\n // it has fired, `warned` short-circuits further evaluations. Production\n // builds skip the entire effect setup because `ngDevMode` is statically\n // `false` and the constructor returns early.\n\n constructor() {\n if (typeof ngDevMode !== 'undefined' && !ngDevMode) return;\n let warned = false;\n effect(() => {\n if (warned) return;\n const opts = this.options();\n if (this.label() || opts?.ariaLabel || opts?.ariaLabelledby) return;\n // `untracked` wraps the side-effect so the console.warn / flag mutation\n // can never create a reactive subscription — defensive in case future\n // work adds signal reads inside the warning block.\n untracked(() => {\n warned = true;\n console.warn(\n 'tw-progress-bar: no accessible name provided. Supply one of `label`, `options.ariaLabel`, or `options.ariaLabelledby`.',\n );\n });\n });\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
@@ -1,11 +1,9 @@
1
1
  import * as i0 from '@angular/core';
2
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';
3
+ import { NgControl } from '@angular/forms';
5
4
  import { FocusMonitor } from '@angular/cdk/a11y';
6
- import { merge } from 'rxjs';
7
5
  import { tv } from 'tailwind-variants';
8
- import { TW_ERROR_STATE_MATCHER } from '@cdevhub/ngx-tw/core';
6
+ import { wireErrorState } from '@cdevhub/ngx-tw/core';
9
7
 
10
8
  // ── Radio tv() config ────────────────────────────────────────────
11
9
  const radioVariants = tv({
@@ -13,6 +11,25 @@ const radioVariants = tv({
13
11
  // `items-start` aligns circleWrap with the top of the label container; combined with
14
12
  // a `min-h-N` matching the label's first-line line-height on circleWrap, the circle
15
13
  // stays centered on the first line whether the label spans one line or many.
14
+ //
15
+ // ── Row-height scale (selection cohort) ──────────────────────────────
16
+ // The rendered row is `max(circleWrap, label first line)`, and the host itself is the
17
+ // click target (`role="radio"` + `tabindex` + `(click)` all sit on the root), so the
18
+ // row IS the interactive target. It runs 16 / 20 / 24 / 28 / 32 px — five distinct
19
+ // steps, every one on the 4px baseline of docs/vertical-rhythm.md, and identical to
20
+ // `checkbox` and `switch`. Selection controls are glyph-scale and deliberately do NOT
21
+ // take the pinned 24/32/36/44/48 form-row scale (vertical-rhythm.md section 4).
22
+ //
23
+ // The row is driven by `circleWrap` min-h, NOT by the circle glyph, which keeps its own
24
+ // 14 / 16 / 20 / 24 / 28 scale. Driving it from circleWrap is what makes the five steps
25
+ // survive when no label is projected.
26
+ //
27
+ // Because centering demands `circleWrap height == label first-line leading`, the label
28
+ // leading carries the same 4 / 5 / 6 / 7 / 8 progression. That is forced, not chosen:
29
+ // the label font scale has only three steps (text-xs, text-sm, text-base), so leading
30
+ // is the only lever that can break the sm/md and lg/xl ties. Before this, sm and md
31
+ // both rendered a 20px row — a dead step. Accepted cost: `text-base leading-8` at xl is
32
+ // a 2.0 line ratio, looser than typographic ideal on a wrapped multi-line label.
16
33
  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
34
  circleWrap: 'relative inline-flex items-center justify-center shrink-0',
18
35
  circle: 'inline-flex items-center justify-center rounded-full border bg-surface transition-colors duration-normal motion-reduce:transition-none',
@@ -27,6 +44,12 @@ const radioVariants = tv({
27
44
  xs: {
28
45
  // `size-3.5` half-step: aligns with text-xs/leading-4 row metric.
29
46
  circle: 'size-3.5',
47
+ // The dot runs 6 / 8 / 10 / 12 / 14px — it is NOT on the status-dot sub-scale of
48
+ // CLAUDE.md (that one is for standalone presence pips). It is a proportional inner
49
+ // selection mark, held at ~50% of the enclosing circle at every size, so its value
50
+ // is derived from `circle` rather than picked off a table. At xs, 50% of a 14px
51
+ // circle is 7px, which no utility expresses; `size-1.5` (6px) is the nearest step
52
+ // down — rounding up to 8px would leave under 3px of ring on each side.
30
53
  dot: 'size-1.5',
31
54
  circleWrap: 'min-h-4',
32
55
  label: 'text-xs leading-4',
@@ -42,15 +65,18 @@ const radioVariants = tv({
42
65
  md: {
43
66
  circle: 'size-5',
44
67
  dot: 'size-2.5',
45
- circleWrap: 'min-h-5',
46
- label: 'text-sm leading-5',
68
+ // 24px row. `text-sm` is shared with sm by the documented font scale, so the
69
+ // leading breaks the tie: leading-6 lifts the row off sm's 20px.
70
+ circleWrap: 'min-h-6',
71
+ label: 'text-sm leading-6',
47
72
  description: 'text-xs leading-4',
48
73
  },
49
74
  lg: {
50
75
  circle: 'size-6',
51
76
  dot: 'size-3',
52
- circleWrap: 'min-h-6',
53
- label: 'text-base leading-6',
77
+ // 28px row — leading-7 keeps the first label line the same height as circleWrap.
78
+ circleWrap: 'min-h-7',
79
+ label: 'text-base leading-7',
54
80
  description: 'text-sm leading-5',
55
81
  },
56
82
  xl: {
@@ -58,10 +84,10 @@ const radioVariants = tv({
58
84
  // xl density: dot uses `size-3.5` (14px) as the proportionally-sized
59
85
  // selection mark inside the size-7 (28px) circle — exactly 50%.
60
86
  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',
87
+ // 32px row. `text-base` is shared with lg, so leading-8 breaks that tie the same
88
+ // way leading-6 breaks sm/md. A 2.0 ratio on a wrapped label is the accepted cost.
89
+ circleWrap: 'min-h-8',
90
+ label: 'text-base leading-8',
65
91
  description: 'text-sm leading-5',
66
92
  },
67
93
  },
@@ -138,7 +164,7 @@ const OUTLINE_DOT = {
138
164
  let nextRadioId = 0;
139
165
  // ── RadioComponent ───────────────────────────────────────────────
140
166
  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. */
167
+ /** The value this radio contributes when selected inside a `tw-radio-group`. Required when nested in a group; ignored when used standalone. Defaults to `undefined`. */
142
168
  value = input(undefined, /* @ts-ignore */
143
169
  ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
144
170
  /** Overrides the parent group's color for this radio. When undefined, inherits from the group (or defaults to `'primary'` standalone). */
@@ -165,36 +191,60 @@ class RadioComponent {
165
191
  /** Optional name attribute for standalone use. Ignored when the parent group provides a name. */
166
192
  name = input(undefined, /* @ts-ignore */
167
193
  ...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
168
- /** Accessible name when no visible label is provided. Mirrored to `aria-label`. */
194
+ /** Accessible name when no visible label is provided. Mirrored to `aria-label`. Defaults to `undefined`. */
169
195
  ariaLabel = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabel" } : /* istanbul ignore next */ {}), alias: 'aria-label' });
170
- /** ID of an external element that labels the radio. Mirrored to `aria-labelledby`. */
196
+ /** ID of an external element that labels the radio. Mirrored to `aria-labelledby`. Defaults to `undefined`. */
171
197
  ariaLabelledby = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabelledby" } : /* istanbul ignore next */ {}), alias: 'aria-labelledby' });
172
- /** ID of an external element that describes the radio. Mirrored to `aria-describedby`. */
198
+ /** ID of an external element that describes the radio. Mirrored to `aria-describedby`. Defaults to `undefined`. */
173
199
  ariaDescribedby = input(undefined, { ...(ngDevMode ? { debugName: "ariaDescribedby" } : /* istanbul ignore next */ {}), alias: 'aria-describedby' });
174
- /** 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. */
200
+ /**
201
+ * Two-way bound checked state. Authoritative only in standalone mode.
202
+ * Defaults to `false`.
203
+ *
204
+ * Standalone, the minted `checkedChange` output is the *any-change* channel:
205
+ * it fires both when the user selects this radio and when a bound form writes
206
+ * into it (`FormControl.setValue`, `ngModel`, `writeValue`). `(change)` is the
207
+ * *user-gesture-only* channel — bind that one for analytics or a confirmation
208
+ * prompt, where a programmatic write must not count as a click.
209
+ *
210
+ * Inside a `tw-radio-group` this model is inert: the group owns selection,
211
+ * the rendered state comes from the group's `value`, and the component never
212
+ * writes `checked` — so `(checkedChange)` never fires on a grouped radio.
213
+ * `(change)` still fires when this radio becomes the selected one. Bind the
214
+ * group's `[(value)]` / `(change)` instead.
215
+ */
175
216
  checked = model(false, /* @ts-ignore */
176
217
  ...(ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []));
177
218
  /** Per-instance override of the {@link ErrorStateMatcher}. When omitted, the radio uses the `TW_ERROR_STATE_MATCHER` token's value. */
178
219
  errorStateMatcher = input(undefined, /* @ts-ignore */
179
220
  ...(ngDevMode ? [{ debugName: "errorStateMatcher" }] : /* istanbul ignore next */ []));
180
- /** 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. */
221
+ /**
222
+ * Fires after the checked state changes from a user interaction on this
223
+ * radio. In grouped mode only fires when this radio becomes the selected one.
224
+ * Does **not** fire when selection is updated programmatically via
225
+ * `writeValue` / `FormControl.setValue` / `ngModel` — standalone, bind the
226
+ * two-way binding's `(checkedChange)` for that, which fires on any change.
227
+ * (Grouped, `(checkedChange)` never fires at all; use the group's
228
+ * `[(value)]`.)
229
+ */
181
230
  change = output();
182
231
  focusMonitor = inject(FocusMonitor);
183
232
  elementRef = inject((ElementRef));
184
233
  destroyRef = inject(DestroyRef);
185
234
  parent = inject(forwardRef(() => RadioGroupComponent), { optional: true });
186
235
  ngControl = inject(NgControl, { optional: true, self: true });
187
- parentForm = inject(NgForm, { optional: true });
188
- parentFormGroup = inject(FormGroupDirective, { optional: true });
189
- defaultMatcher = inject(TW_ERROR_STATE_MATCHER);
190
236
  onChange = () => { };
191
237
  onTouched = () => { };
192
238
  cvaDisabled = signal(false, /* @ts-ignore */
193
239
  ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
194
- _ngControlRev = signal(0, /* @ts-ignore */
195
- ...(ngDevMode ? [{ debugName: "_ngControlRev" }] : /* istanbul ignore next */ []));
196
- _formSubmitRev = signal(0, /* @ts-ignore */
197
- ...(ngDevMode ? [{ debugName: "_formSubmitRev" }] : /* istanbul ignore next */ []));
240
+ /** @internal Shared `errorState` derivation — see `wireErrorState`. */
241
+ errorWiring = wireErrorState({
242
+ ngControl: () => this.ngControl,
243
+ matcher: () => this.errorStateMatcher(),
244
+ // A radio inside a group inherits the group's error state verbatim,
245
+ // including `false`; standalone it falls through to the matcher.
246
+ errorStateOverride: () => (this.parent ? this.parent.errorState() : undefined),
247
+ });
198
248
  uid = nextRadioId++;
199
249
  hostId = `tw-radio-${this.uid}`;
200
250
  labelId = `${this.hostId}-label`;
@@ -224,17 +274,7 @@ class RadioComponent {
224
274
  isDisabled = computed(() => this.disabled() || this.cvaDisabled() || (this.parent?.isDisabled() ?? false), /* @ts-ignore */
225
275
  ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
226
276
  /** 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. */
227
- errorState = computed(() => {
228
- this._ngControlRev();
229
- this._formSubmitRev();
230
- if (this.parent)
231
- return this.parent.errorState();
232
- const matcher = this.errorStateMatcher() ?? this.defaultMatcher;
233
- const form = this.parentFormGroup ??
234
- this.parentForm;
235
- return matcher.isErrorState(this.ngControl?.control ?? null, form);
236
- }, /* @ts-ignore */
237
- ...(ngDevMode ? [{ debugName: "errorState" }] : /* istanbul ignore next */ []));
277
+ errorState = this.errorWiring.errorState;
238
278
  /** Roving tabindex: in a group, only the selected (or first enabled if none selected) radio is focusable. Standalone radios are always focusable unless disabled. */
239
279
  isFocusable = computed(() => {
240
280
  if (this.isDisabled())
@@ -336,7 +376,8 @@ class RadioComponent {
336
376
  this.internalChecked.set(true);
337
377
  this.checked.set(true);
338
378
  this.onChange(true);
339
- this.onTouched();
379
+ // Deliberately NOT `onTouched()` — that is the blur notification, and
380
+ // `onBlur()` owns it. See the note on `RadioGroupComponent.selectValue`.
340
381
  this.change.emit(true);
341
382
  }
342
383
  }
@@ -357,7 +398,7 @@ class RadioComponent {
357
398
  else {
358
399
  this.onTouched();
359
400
  // Blur flips `touched` on `NgControl`; bump the revision so `errorState` recomputes.
360
- this._ngControlRev.update((n) => n + 1);
401
+ this.errorWiring.bump();
361
402
  }
362
403
  }
363
404
  /** Focuses the host element. Required for keyboard navigation from the group. */
@@ -386,21 +427,7 @@ class RadioComponent {
386
427
  });
387
428
  // Track validator status/value changes on a bound NgControl so `errorState`
388
429
  // re-evaluates when status flips. Mirrors `input.ts`'s pattern.
389
- const ctrl = this.ngControl?.control;
390
- if (ctrl) {
391
- const streams = [ctrl.statusChanges, ctrl.valueChanges].filter((s) => !!s);
392
- if (streams.length) {
393
- merge(...streams)
394
- .pipe(takeUntilDestroyed(this.destroyRef))
395
- .subscribe(() => this._ngControlRev.update((n) => n + 1));
396
- }
397
- }
398
- const submit = this.parentFormGroup?.ngSubmit ?? this.parentForm?.ngSubmit;
399
- if (submit) {
400
- submit
401
- .pipe(takeUntilDestroyed(this.destroyRef))
402
- .subscribe(() => this._formSubmitRev.update((n) => n + 1));
403
- }
430
+ this.errorWiring.connect();
404
431
  }
405
432
  hasAccessibleNameHint() {
406
433
  if (this.label() || this.ariaLabel() || this.ariaLabelledby())
@@ -527,31 +554,61 @@ class RadioGroupComponent {
527
554
  /** 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'`. */
528
555
  variant = input('solid', /* @ts-ignore */
529
556
  ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
530
- /** Layout direction of the group. Drives `aria-orientation` and the arrow-key model. Defaults to `'vertical'`. */
557
+ /** Layout direction of the group. Sets `aria-orientation` and the visual axis; both arrow-key pairs navigate in either orientation, per WAI-ARIA APG. Defaults to `'vertical'`. */
531
558
  orientation = input('vertical', /* @ts-ignore */
532
559
  ...(ngDevMode ? [{ debugName: "orientation" }] : /* istanbul ignore next */ []));
533
560
  /** When true, disables every radio in the group and blocks keyboard interaction. Defaults to `false`. */
534
561
  disabled = input(false, /* @ts-ignore */
535
562
  ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
536
- /** When true, sets `aria-required="true"` on the group for assistive tech. Defaults to `false`. */
537
- required = input(false, /* @ts-ignore */
538
- ...(ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []));
563
+ /** When true, sets `aria-required="true"` on the group for assistive tech. Also inferred from `Validators.required` on a bound control, so a reactive/template-driven form does not have to state it twice. Defaults to `false`. */
564
+ requiredInput = input(false, { ...(ngDevMode ? { debugName: "requiredInput" } : /* istanbul ignore next */ {}), alias: 'required' });
565
+ /**
566
+ * @internal Shared `errorState` / `required` / `errors` derivation — see
567
+ * `wireErrorState`. Declared here, above `required`, because class field
568
+ * initializers run in declaration order.
569
+ */
570
+ errorWiring = wireErrorState({
571
+ ngControl: () => this.ngControl,
572
+ matcher: () => this.errorStateMatcher(),
573
+ required: () => this.requiredInput(),
574
+ });
575
+ /**
576
+ * @internal Resolved required state: the `required` input OR'd with
577
+ * `Validators.required` on a bound `NgControl`.
578
+ */
579
+ required = this.errorWiring.required;
539
580
  /** Optional form-association name. Propagated to each child radio's host `name` attribute so standard HTML form semantics still apply. */
540
581
  name = input(undefined, /* @ts-ignore */
541
582
  ...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
542
- /** Accessible name for the group when no visible label is provided. Mirrored to `aria-label`. */
583
+ /** Accessible name for the group when no visible label is provided. Mirrored to `aria-label`. Defaults to `undefined`. */
543
584
  ariaLabel = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabel" } : /* istanbul ignore next */ {}), alias: 'aria-label' });
544
- /** ID of an external element that labels the group. Mirrored to `aria-labelledby`. */
585
+ /** ID of an external element that labels the group. Mirrored to `aria-labelledby`. Defaults to `undefined`. */
545
586
  ariaLabelledby = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabelledby" } : /* istanbul ignore next */ {}), alias: 'aria-labelledby' });
546
- /** ID of an external element that describes the group. Mirrored to `aria-describedby`. */
587
+ /** ID of an external element that describes the group. Mirrored to `aria-describedby`. Defaults to `undefined`. */
547
588
  ariaDescribedby = input(undefined, { ...(ngDevMode ? { debugName: "ariaDescribedby" } : /* istanbul ignore next */ {}), alias: 'aria-describedby' });
548
- /** Two-way bound selected value. Updates when the user picks a radio; fires `valueChange`. `null` means no selection. */
589
+ /**
590
+ * Two-way bound selected value. `null` means no selection. Defaults to
591
+ * `null`.
592
+ *
593
+ * The two-way binding mints a `valueChange` output. That output is the
594
+ * *any-change* channel: it fires when the user picks a radio **and** when a
595
+ * bound form writes into the group (`FormControl.setValue`, `ngModel`,
596
+ * `writeValue`). `(change)` is the *user-gesture-only* channel — bind that
597
+ * one for analytics or a confirmation prompt, where a programmatic write must
598
+ * not count as a click. Writing back into the same form from a
599
+ * `(valueChange)` handler will echo.
600
+ */
549
601
  value = model(null, /* @ts-ignore */
550
602
  ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
551
603
  /** Per-instance override of the {@link ErrorStateMatcher}. When omitted, the group uses the `TW_ERROR_STATE_MATCHER` token's value. */
552
604
  errorStateMatcher = input(undefined, /* @ts-ignore */
553
605
  ...(ngDevMode ? [{ debugName: "errorStateMatcher" }] : /* istanbul ignore next */ []));
554
- /** Fires after the selected value changes from a user interaction. Does not fire when the value is updated programmatically via `writeValue`. */
606
+ /**
607
+ * Fires after the selected value changes from a user interaction. Does
608
+ * **not** fire when the value is updated programmatically via `writeValue` /
609
+ * `FormControl.setValue` / `ngModel` — for those, bind the two-way binding's
610
+ * `(valueChange)` instead, which fires on any change.
611
+ */
555
612
  change = output();
556
613
  /** @internal Child radios discovered via content projection. */
557
614
  radios = contentChildren(RadioComponent, { ...(ngDevMode ? { debugName: "radios" } : /* istanbul ignore next */ {}), descendants: true });
@@ -559,31 +616,16 @@ class RadioGroupComponent {
559
616
  elementRef = inject((ElementRef));
560
617
  destroyRef = inject(DestroyRef);
561
618
  ngControl = inject(NgControl, { optional: true, self: true });
562
- parentForm = inject(NgForm, { optional: true });
563
- parentFormGroup = inject(FormGroupDirective, { optional: true });
564
- defaultMatcher = inject(TW_ERROR_STATE_MATCHER);
565
619
  hostId = `tw-radio-group-${nextGroupId++}`;
566
620
  onChange = () => { };
567
621
  onTouched = () => { };
568
622
  cvaDisabled = signal(false, /* @ts-ignore */
569
623
  ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : /* istanbul ignore next */ []));
570
- _ngControlRev = signal(0, /* @ts-ignore */
571
- ...(ngDevMode ? [{ debugName: "_ngControlRev" }] : /* istanbul ignore next */ []));
572
- _formSubmitRev = signal(0, /* @ts-ignore */
573
- ...(ngDevMode ? [{ debugName: "_formSubmitRev" }] : /* istanbul ignore next */ []));
574
624
  /** Effective disabled state — input OR CVA disabled. */
575
625
  isDisabled = computed(() => this.disabled() || this.cvaDisabled(), /* @ts-ignore */
576
626
  ...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
577
627
  /** 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. */
578
- errorState = computed(() => {
579
- this._ngControlRev();
580
- this._formSubmitRev();
581
- const matcher = this.errorStateMatcher() ?? this.defaultMatcher;
582
- const form = this.parentFormGroup ??
583
- this.parentForm;
584
- return matcher.isErrorState(this.ngControl?.control ?? null, form);
585
- }, /* @ts-ignore */
586
- ...(ngDevMode ? [{ debugName: "errorState" }] : /* istanbul ignore next */ []));
628
+ errorState = this.errorWiring.errorState;
587
629
  activeValue = linkedSignal(() => this.value(), /* @ts-ignore */
588
630
  ...(ngDevMode ? [{ debugName: "activeValue" }] : /* istanbul ignore next */ []));
589
631
  variantResult = computed(() => radioGroupVariants({
@@ -613,20 +655,25 @@ class RadioGroupComponent {
613
655
  return;
614
656
  const typed = next;
615
657
  if (typed === this.activeValue()) {
616
- this.onTouched();
617
658
  return;
618
659
  }
619
660
  this.activeValue.set(typed);
620
661
  this.value.set(typed);
621
662
  this.onChange(typed);
622
- this.onTouched();
663
+ // Deliberately NOT `onTouched()`. Angular's CVA contract registers
664
+ // `onTouched` as the BLUR notification; calling it on selection flipped
665
+ // `touched` with no blur, so a consumer staging error display on `touched`
666
+ // got different behaviour from `tw-radio-group` than from `tw-slider` /
667
+ // `tw-input`. `notifyTouched()` — reached from a child radio's real blur —
668
+ // is the only place that fires it.
623
669
  this.change.emit(typed);
624
670
  }
625
671
  /** @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`. */
626
672
  notifyTouched() {
627
673
  this.onTouched();
628
- this._ngControlRev.update((n) => n + 1);
674
+ this.errorWiring.bump();
629
675
  }
676
+ /** @internal Handles roving-focus keyboard navigation (Arrow keys, Space) across the radios in the group. */
630
677
  onKeydown(event) {
631
678
  if (this.isDisabled())
632
679
  return;
@@ -710,24 +757,10 @@ class RadioGroupComponent {
710
757
  // NgControl's `control` is set by the parent FormControl* directive before
711
758
  // children's `ngOnInit`. Subscribe here so errorState reacts to status/value
712
759
  // changes on the bound control.
713
- const ctrl = this.ngControl?.control;
714
- if (ctrl) {
715
- const streams = [ctrl.statusChanges, ctrl.valueChanges].filter((s) => !!s);
716
- if (streams.length) {
717
- merge(...streams)
718
- .pipe(takeUntilDestroyed(this.destroyRef))
719
- .subscribe(() => this._ngControlRev.update((n) => n + 1));
720
- }
721
- }
722
- const submit = this.parentFormGroup?.ngSubmit ?? this.parentForm?.ngSubmit;
723
- if (submit) {
724
- submit
725
- .pipe(takeUntilDestroyed(this.destroyRef))
726
- .subscribe(() => this._formSubmitRev.update((n) => n + 1));
727
- }
760
+ this.errorWiring.connect();
728
761
  }
729
762
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: RadioGroupComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
730
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.0.7", 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 });
763
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.0.7", 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 }, requiredInput: { classPropertyName: "requiredInput", 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 });
731
764
  }
732
765
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: RadioGroupComponent, decorators: [{
733
766
  type: Component,
@@ -749,7 +782,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
749
782
  '(keydown)': 'onKeydown($event)',
750
783
  },
751
784
  }]
752
- }], 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 }] }] } });
785
+ }], 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 }] }], requiredInput: [{ 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 }] }] } });
753
786
 
754
787
  /**
755
788
  * Generated bundle index. Do not edit.