@cdevhub/ngx-tw 0.4.0 → 0.6.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 (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-dialog-renderer-DoIhoV3d.mjs +198 -0
  47. package/fesm2022/cdevhub-ngx-tw-dialog-dialog-renderer-DoIhoV3d.mjs.map +1 -0
  48. package/fesm2022/cdevhub-ngx-tw-dialog-testing.mjs +122 -0
  49. package/fesm2022/cdevhub-ngx-tw-dialog-testing.mjs.map +1 -0
  50. package/fesm2022/cdevhub-ngx-tw-dialog.mjs +199 -220
  51. package/fesm2022/cdevhub-ngx-tw-dialog.mjs.map +1 -1
  52. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs +1 -1
  53. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs.map +1 -1
  54. package/fesm2022/cdevhub-ngx-tw-file-upload-testing.mjs +149 -0
  55. package/fesm2022/cdevhub-ngx-tw-file-upload-testing.mjs.map +1 -0
  56. package/fesm2022/cdevhub-ngx-tw-file-upload.mjs +57 -67
  57. package/fesm2022/cdevhub-ngx-tw-file-upload.mjs.map +1 -1
  58. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs +42 -24
  59. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs.map +1 -1
  60. package/fesm2022/cdevhub-ngx-tw-form-field.mjs +114 -21
  61. package/fesm2022/cdevhub-ngx-tw-form-field.mjs.map +1 -1
  62. package/fesm2022/cdevhub-ngx-tw-icon.mjs +16 -6
  63. package/fesm2022/cdevhub-ngx-tw-icon.mjs.map +1 -1
  64. package/fesm2022/cdevhub-ngx-tw-input.mjs +52 -61
  65. package/fesm2022/cdevhub-ngx-tw-input.mjs.map +1 -1
  66. package/fesm2022/cdevhub-ngx-tw-item.mjs +18 -3
  67. package/fesm2022/cdevhub-ngx-tw-item.mjs.map +1 -1
  68. package/fesm2022/cdevhub-ngx-tw-menu-testing.mjs +168 -0
  69. package/fesm2022/cdevhub-ngx-tw-menu-testing.mjs.map +1 -0
  70. package/fesm2022/cdevhub-ngx-tw-menu.mjs +58 -21
  71. package/fesm2022/cdevhub-ngx-tw-menu.mjs.map +1 -1
  72. package/fesm2022/cdevhub-ngx-tw-number-input.mjs +34 -1
  73. package/fesm2022/cdevhub-ngx-tw-number-input.mjs.map +1 -1
  74. package/fesm2022/cdevhub-ngx-tw-paginator.mjs +95 -45
  75. package/fesm2022/cdevhub-ngx-tw-paginator.mjs.map +1 -1
  76. package/fesm2022/cdevhub-ngx-tw-popover.mjs +81 -32
  77. package/fesm2022/cdevhub-ngx-tw-popover.mjs.map +1 -1
  78. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs +1 -1
  79. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs.map +1 -1
  80. package/fesm2022/cdevhub-ngx-tw-radio.mjs +131 -98
  81. package/fesm2022/cdevhub-ngx-tw-radio.mjs.map +1 -1
  82. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs +207 -42
  83. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs.map +1 -1
  84. package/fesm2022/cdevhub-ngx-tw-select-testing.mjs +165 -0
  85. package/fesm2022/cdevhub-ngx-tw-select-testing.mjs.map +1 -0
  86. package/fesm2022/cdevhub-ngx-tw-select.mjs +386 -210
  87. package/fesm2022/cdevhub-ngx-tw-select.mjs.map +1 -1
  88. package/fesm2022/cdevhub-ngx-tw-sheet-sheet-renderer-CqVJwgmp.mjs +256 -0
  89. package/fesm2022/cdevhub-ngx-tw-sheet-sheet-renderer-CqVJwgmp.mjs.map +1 -0
  90. package/fesm2022/cdevhub-ngx-tw-sheet-testing.mjs +129 -0
  91. package/fesm2022/cdevhub-ngx-tw-sheet-testing.mjs.map +1 -0
  92. package/fesm2022/cdevhub-ngx-tw-sheet.mjs +197 -278
  93. package/fesm2022/cdevhub-ngx-tw-sheet.mjs.map +1 -1
  94. package/fesm2022/cdevhub-ngx-tw-slider.mjs +89 -54
  95. package/fesm2022/cdevhub-ngx-tw-slider.mjs.map +1 -1
  96. package/fesm2022/cdevhub-ngx-tw-sort.mjs +122 -17
  97. package/fesm2022/cdevhub-ngx-tw-sort.mjs.map +1 -1
  98. package/fesm2022/cdevhub-ngx-tw-split.mjs +72 -19
  99. package/fesm2022/cdevhub-ngx-tw-split.mjs.map +1 -1
  100. package/fesm2022/cdevhub-ngx-tw-stat.mjs +63 -25
  101. package/fesm2022/cdevhub-ngx-tw-stat.mjs.map +1 -1
  102. package/fesm2022/cdevhub-ngx-tw-stepper.mjs +35 -8
  103. package/fesm2022/cdevhub-ngx-tw-stepper.mjs.map +1 -1
  104. package/fesm2022/cdevhub-ngx-tw-switch.mjs +54 -48
  105. package/fesm2022/cdevhub-ngx-tw-switch.mjs.map +1 -1
  106. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs +12 -15
  107. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs.map +1 -1
  108. package/fesm2022/cdevhub-ngx-tw-table.mjs +74 -13
  109. package/fesm2022/cdevhub-ngx-tw-table.mjs.map +1 -1
  110. package/fesm2022/cdevhub-ngx-tw-tabs.mjs +84 -30
  111. package/fesm2022/cdevhub-ngx-tw-tabs.mjs.map +1 -1
  112. package/fesm2022/cdevhub-ngx-tw-tags-input-testing.mjs +172 -0
  113. package/fesm2022/cdevhub-ngx-tw-tags-input-testing.mjs.map +1 -0
  114. package/fesm2022/cdevhub-ngx-tw-tags-input.mjs +107 -71
  115. package/fesm2022/cdevhub-ngx-tw-tags-input.mjs.map +1 -1
  116. package/fesm2022/cdevhub-ngx-tw-textarea.mjs +41 -9
  117. package/fesm2022/cdevhub-ngx-tw-textarea.mjs.map +1 -1
  118. package/fesm2022/cdevhub-ngx-tw-theme.mjs +257 -45
  119. package/fesm2022/cdevhub-ngx-tw-theme.mjs.map +1 -1
  120. package/fesm2022/cdevhub-ngx-tw-time-picker-testing.mjs +201 -0
  121. package/fesm2022/cdevhub-ngx-tw-time-picker-testing.mjs.map +1 -0
  122. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs +184 -77
  123. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs.map +1 -1
  124. package/fesm2022/cdevhub-ngx-tw-timeline.mjs +110 -27
  125. package/fesm2022/cdevhub-ngx-tw-timeline.mjs.map +1 -1
  126. package/fesm2022/cdevhub-ngx-tw-toast-toast-renderer-BlDxemsU.mjs +616 -0
  127. package/fesm2022/cdevhub-ngx-tw-toast-toast-renderer-BlDxemsU.mjs.map +1 -0
  128. package/fesm2022/cdevhub-ngx-tw-toast.mjs +556 -945
  129. package/fesm2022/cdevhub-ngx-tw-toast.mjs.map +1 -1
  130. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs +102 -15
  131. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs.map +1 -1
  132. package/fesm2022/cdevhub-ngx-tw-transfer-testing.mjs +219 -0
  133. package/fesm2022/cdevhub-ngx-tw-transfer-testing.mjs.map +1 -0
  134. package/fesm2022/cdevhub-ngx-tw-transfer.mjs +88 -69
  135. package/fesm2022/cdevhub-ngx-tw-transfer.mjs.map +1 -1
  136. package/fesm2022/cdevhub-ngx-tw-tree.mjs +68 -19
  137. package/fesm2022/cdevhub-ngx-tw-tree.mjs.map +1 -1
  138. package/index.json +1 -1
  139. package/package.json +63 -2
  140. package/theme/_base.css +13 -0
  141. package/theme/_dark.css +110 -36
  142. package/theme/_high-contrast-dark.css +367 -0
  143. package/theme/_high-contrast.css +13 -2
  144. package/theme/_light.css +276 -0
  145. package/theme/_semantic.css +47 -12
  146. package/theme/_typography.css +17 -0
  147. package/theme/index.css +86 -7
  148. package/types/cdevhub-ngx-tw-accordion.d.ts +12 -4
  149. package/types/cdevhub-ngx-tw-avatar.d.ts +10 -3
  150. package/types/cdevhub-ngx-tw-button.d.ts +1 -0
  151. package/types/cdevhub-ngx-tw-calendar-luxon.d.ts +2 -2
  152. package/types/cdevhub-ngx-tw-calendar-testing.d.ts +1 -1
  153. package/types/cdevhub-ngx-tw-calendar.d.ts +179 -73
  154. package/types/cdevhub-ngx-tw-card.d.ts +11 -3
  155. package/types/cdevhub-ngx-tw-carousel.d.ts +89 -20
  156. package/types/cdevhub-ngx-tw-checkbox.d.ts +40 -12
  157. package/types/cdevhub-ngx-tw-code-block.d.ts +11 -2
  158. package/types/cdevhub-ngx-tw-collapsible.d.ts +32 -5
  159. package/types/cdevhub-ngx-tw-combobox-testing.d.ts +157 -0
  160. package/types/cdevhub-ngx-tw-combobox.d.ts +94 -53
  161. package/types/cdevhub-ngx-tw-command-palette-testing.d.ts +136 -0
  162. package/types/cdevhub-ngx-tw-command-palette.d.ts +33 -5
  163. package/types/cdevhub-ngx-tw-core.d.ts +152 -21
  164. package/types/cdevhub-ngx-tw-date-picker-testing.d.ts +141 -0
  165. package/types/cdevhub-ngx-tw-date-picker.d.ts +37 -29
  166. package/types/cdevhub-ngx-tw-date-range-picker-testing.d.ts +148 -0
  167. package/types/cdevhub-ngx-tw-date-range-picker.d.ts +36 -29
  168. package/types/cdevhub-ngx-tw-dialog-testing.d.ts +98 -0
  169. package/types/cdevhub-ngx-tw-dialog.d.ts +89 -19
  170. package/types/cdevhub-ngx-tw-file-upload-testing.d.ts +120 -0
  171. package/types/cdevhub-ngx-tw-file-upload.d.ts +8 -10
  172. package/types/cdevhub-ngx-tw-flip-card.d.ts +18 -11
  173. package/types/cdevhub-ngx-tw-form-field.d.ts +19 -2
  174. package/types/cdevhub-ngx-tw-icon.d.ts +3 -3
  175. package/types/cdevhub-ngx-tw-input.d.ts +5 -8
  176. package/types/cdevhub-ngx-tw-item.d.ts +2 -0
  177. package/types/cdevhub-ngx-tw-menu-testing.d.ts +131 -0
  178. package/types/cdevhub-ngx-tw-menu.d.ts +2 -2
  179. package/types/cdevhub-ngx-tw-number-input.d.ts +20 -0
  180. package/types/cdevhub-ngx-tw-paginator.d.ts +58 -24
  181. package/types/cdevhub-ngx-tw-popover.d.ts +41 -10
  182. package/types/cdevhub-ngx-tw-progress-bar.d.ts +1 -1
  183. package/types/cdevhub-ngx-tw-radio.d.ts +68 -25
  184. package/types/cdevhub-ngx-tw-segmented-control.d.ts +79 -16
  185. package/types/cdevhub-ngx-tw-select-testing.d.ts +114 -0
  186. package/types/cdevhub-ngx-tw-select.d.ts +104 -33
  187. package/types/cdevhub-ngx-tw-sheet-testing.d.ts +104 -0
  188. package/types/cdevhub-ngx-tw-sheet.d.ts +96 -21
  189. package/types/cdevhub-ngx-tw-slider.d.ts +79 -25
  190. package/types/cdevhub-ngx-tw-sort.d.ts +45 -4
  191. package/types/cdevhub-ngx-tw-split.d.ts +32 -2
  192. package/types/cdevhub-ngx-tw-stat.d.ts +14 -4
  193. package/types/cdevhub-ngx-tw-stepper.d.ts +16 -2
  194. package/types/cdevhub-ngx-tw-switch.d.ts +35 -13
  195. package/types/cdevhub-ngx-tw-tab-nav.d.ts +7 -6
  196. package/types/cdevhub-ngx-tw-table.d.ts +53 -10
  197. package/types/cdevhub-ngx-tw-tabs.d.ts +41 -10
  198. package/types/cdevhub-ngx-tw-tags-input-testing.d.ts +135 -0
  199. package/types/cdevhub-ngx-tw-tags-input.d.ts +26 -11
  200. package/types/cdevhub-ngx-tw-textarea.d.ts +3 -2
  201. package/types/cdevhub-ngx-tw-theme.d.ts +245 -39
  202. package/types/cdevhub-ngx-tw-time-picker-testing.d.ts +129 -0
  203. package/types/cdevhub-ngx-tw-time-picker.d.ts +76 -19
  204. package/types/cdevhub-ngx-tw-timeline.d.ts +36 -12
  205. package/types/cdevhub-ngx-tw-toast.d.ts +61 -12
  206. package/types/cdevhub-ngx-tw-tooltip.d.ts +28 -6
  207. package/types/cdevhub-ngx-tw-transfer-testing.d.ts +152 -0
  208. package/types/cdevhub-ngx-tw-transfer.d.ts +21 -8
  209. package/types/cdevhub-ngx-tw-tree.d.ts +6 -1
@@ -1,18 +1,23 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, makeEnvironmentProviders, Directive, input, contentChildren, inject, DestroyRef, viewChild, signal, computed, afterNextRender, ChangeDetectionStrategy, Component, contentChild } from '@angular/core';
2
+ import { InjectionToken, makeEnvironmentProviders, Directive, input, contentChildren, inject, DestroyRef, viewChild, computed, signal, afterRenderEffect, untracked, ChangeDetectionStrategy, Component, contentChild } from '@angular/core';
3
3
  import { NgTemplateOutlet } from '@angular/common';
4
4
  import { Directionality } from '@angular/cdk/bidi';
5
5
  import { tv } from 'tailwind-variants';
6
6
 
7
- /** Default English labels used when `TW_TIMELINE_SCROLL_LABELS` is not provided. */
7
+ /**
8
+ * Default English labels used when `TW_TIMELINE_SCROLL_LABELS` is not provided.
9
+ * Typed `Required<TwTimelineScrollLabels>` so readers keep a non-optional
10
+ * `string` for every key even though the interface itself is all-optional.
11
+ */
8
12
  const DEFAULT_TW_TIMELINE_SCROLL_LABELS = {
9
13
  scrollPrevious: 'Scroll to previous events',
10
14
  scrollNext: 'Scroll to next events',
15
+ scrollRegion: 'Timeline events',
11
16
  };
12
17
  /**
13
18
  * Injection token carrying localisable `aria-label` strings for the timeline's
14
- * horizontal-overflow chevron buttons. The container uses these labels exclusively
15
- * on the prev/next buttons; they do not affect any other rendered text.
19
+ * horizontal-overflow affordances the prev/next chevron buttons and the
20
+ * scrollable viewport between them. They do not affect any other rendered text.
16
21
  *
17
22
  * Provide via `provideTwTimelineScrollLabels({ ... })` at the root or feature level.
18
23
  * If both keys are present they override the English defaults; if only one is present
@@ -101,6 +106,17 @@ const timelineVariants = tv({
101
106
  // column and stretches through this padding to touch the next item's
102
107
  // marker — items butt against each other with no container `gap-*` so
103
108
  // the line stays continuous.
109
+ //
110
+ // `item: gap-*` is NOT that vertical separation — it is the gutter
111
+ // between the marker column and the body (a column gap in vertical
112
+ // orientation, the subgrid row gap in horizontal). It runs past
113
+ // CLAUDE.md's `gap-3` ceiling because that scale governs spacing
114
+ // *inside* an element (icon↔label, button groups) against a fixed
115
+ // ~16–20px glyph, whereas this gutter is measured against a marker
116
+ // column that itself scales `w-6`→`w-12`. Held at `gap-3`, an `xl`
117
+ // 48px bubble sits 12px from its text — the body reads as welded to
118
+ // the marker and every step above `md` becomes visually identical.
119
+ // The gutter therefore tracks the column: 12/16/20/24/32px.
104
120
  xs: { item: 'gap-3', body: 'text-xs gap-1 pb-3', timestamp: 'text-2xs', number: 'text-2xs' },
105
121
  sm: { item: 'gap-4', body: 'text-sm gap-1 pb-4', timestamp: 'text-xs', number: 'text-xs' },
106
122
  md: { item: 'gap-5', body: 'text-sm gap-1.5 pb-6', timestamp: 'text-xs', number: 'text-xs' },
@@ -164,6 +180,17 @@ const timelineVariants = tv({
164
180
  { marker: 'circle', size: 'xs', class: { marker: 'size-6' } },
165
181
  { marker: 'circle', size: 'sm', class: { marker: 'size-7' } },
166
182
  { marker: 'circle', size: 'md', class: { marker: 'size-8' } },
183
+ // `size-10` / `size-12` sit above the glyph scale's `size-10` ceiling and
184
+ // outside the square-interactive scale (which stops at `size-9`), and
185
+ // neither scale governs here: the circle marker is a *container* — it
186
+ // wraps a projected `[twTimelineMarker]` icon/avatar or the auto-number —
187
+ // and the item host is never focusable, so it is not a hit target either.
188
+ // A container has to out-measure its contents: `<tw-icon size="lg">` is
189
+ // `size-6` (24px) and `size="xl"` is `size-8` (32px), so capping the bubble
190
+ // at the `size-9` (36px) hit-target ceiling would leave a 6px rim at `lg`
191
+ // and a 2px rim at `xl` — the glyph reads as cropped and the ring/border
192
+ // states have nowhere to render. `size-10` / `size-12` hold a constant 8px
193
+ // inset at both steps instead.
167
194
  { marker: 'circle', size: 'lg', class: { marker: 'size-10' } },
168
195
  { marker: 'circle', size: 'xl', class: { marker: 'size-12' } },
169
196
  // Vertical-only: `mt-*` nudges the small dot down so its vertical center
@@ -354,9 +381,13 @@ function resolveConnectorClasses(state, color, lineStyle) {
354
381
  // to clientWidth, the overflow check fails permanently, and the chevrons
355
382
  // never enable. The per-density min-w-* on each item still establishes the
356
383
  // floor.
384
+ // The viewport carries `tabindex="0"` so keyboard-only users can reach and
385
+ // arrow-scroll it (axe: scrollable-region-focusable), which makes it a
386
+ // focusable element and therefore obliges it to carry the canonical focus ring.
357
387
  const HORIZONTAL_VIEWPORT_CLASSES = 'tw-scrollbar-none grid grid-flow-col auto-cols-max ' +
358
388
  '[grid-template-rows:minmax(0,1fr)_auto] ' +
359
- 'overflow-x-auto scroll-smooth motion-reduce:scroll-auto w-full';
389
+ 'overflow-x-auto scroll-smooth motion-reduce:scroll-auto w-full ' +
390
+ 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500';
360
391
  // ── Chevron button class string ──
361
392
  // Shared by both overflow chevron buttons. Positioned absolutely against the
362
393
  // timeline host (which carries `relative` in horizontal orientation via the
@@ -480,26 +511,36 @@ class TimelineComponent {
480
511
  _scrollLabelsOverride = inject(TW_TIMELINE_SCROLL_LABELS, {
481
512
  optional: true,
482
513
  });
483
- /** @internal Live `dir` from CDK `Directionality`. */
484
- _cdkDir = signal(this._directionality?.value ?? 'ltr', /* @ts-ignore */
514
+ /**
515
+ * @internal Live `dir` from CDK `Directionality`. Read straight from
516
+ * `valueSignal` (CDK 22+) so `_isRtl` re-evaluates on a runtime `dir` flip
517
+ * with no manual subscription mirror.
518
+ */
519
+ _cdkDir = computed(() => this._directionality?.valueSignal() ?? 'ltr', /* @ts-ignore */
485
520
  ...(ngDevMode ? [{ debugName: "_cdkDir" }] : /* istanbul ignore next */ []));
486
521
  /** @internal True when the host renders inside a `dir="rtl"` ancestor. */
487
522
  _isRtl = computed(() => this._cdkDir() === 'rtl', /* @ts-ignore */
488
523
  ...(ngDevMode ? [{ debugName: "_isRtl" }] : /* istanbul ignore next */ []));
489
524
  /**
490
- * @internal Defaults to `false` until the first measurement runs in
491
- * `afterNextRender`. This avoids a one-frame "enabled-then-disabled" flash
492
- * where the buttons appear live before the scroll state has been read.
525
+ * @internal Defaults to `false` until the first measurement runs in the
526
+ * `afterRenderEffect` in the constructor. This avoids a one-frame
527
+ * "enabled-then-disabled" flash where the buttons appear live before the
528
+ * scroll state has been read.
493
529
  */
494
530
  _canScrollPrev = signal(false, /* @ts-ignore */
495
531
  ...(ngDevMode ? [{ debugName: "_canScrollPrev" }] : /* istanbul ignore next */ []));
496
532
  _canScrollNext = signal(false, /* @ts-ignore */
497
533
  ...(ngDevMode ? [{ debugName: "_canScrollNext" }] : /* istanbul ignore next */ []));
498
534
  /** @internal Resolved label record. Falls back to English defaults for any missing key. */
499
- scrollLabels = computed(() => ({
500
- ...DEFAULT_TW_TIMELINE_SCROLL_LABELS,
501
- ...(this._scrollLabelsOverride ?? {}),
502
- }), /* @ts-ignore */
535
+ scrollLabels = computed(() => {
536
+ // Drop explicitly-undefined keys before merging. A plain spread would let
537
+ // `{ scrollRegion: undefined }` overwrite the default with undefined,
538
+ // which reaches the template as a null `aria-label` and silently strips
539
+ // the scroll region's accessible name. `Required<>` then guarantees the
540
+ // template a string for every label.
541
+ const override = Object.fromEntries(Object.entries(this._scrollLabelsOverride ?? {}).filter(([, v]) => v !== undefined));
542
+ return { ...DEFAULT_TW_TIMELINE_SCROLL_LABELS, ...override };
543
+ }, /* @ts-ignore */
503
544
  ...(ngDevMode ? [{ debugName: "scrollLabels" }] : /* istanbul ignore next */ []));
504
545
  /** @internal True when the prev button should be aria-hidden + visually suppressed. */
505
546
  _prevButtonHidden = computed(() => {
@@ -557,21 +598,45 @@ class TimelineComponent {
557
598
  }
558
599
  _resizeObserver = null;
559
600
  constructor() {
560
- // The viewport materialises on the first frame after horizontal
561
- // orientation is committed; afterNextRender hooks into that point.
562
- afterNextRender(() => this._setupScrollDetection());
601
+ // The horizontal viewport is gated behind `@if (orientation() ===
602
+ // 'horizontal')`, so scroll detection has to re-run whenever orientation
603
+ // is (re)committed not just once at mount. The previous
604
+ // `afterNextRender(() => this._setupScrollDetection())` fired a single
605
+ // time, and `_setupScrollDetection()` early-returns when the orientation
606
+ // is vertical: a timeline mounted vertical and later flipped to horizontal
607
+ // (`[orientation]="isWide() ? 'horizontal' : 'vertical'"`) never got a
608
+ // ResizeObserver and never measured, so `_canScrollPrev`/`_canScrollNext`
609
+ // stayed `false` and both chevrons were permanently invisible AND
610
+ // disabled. Same bug class `carousel.ts` records having already fixed.
611
+ //
612
+ // `afterRenderEffect` is the right primitive rather than `effect()`: the
613
+ // work is a DOM *measurement* that must observe the embedded view holding
614
+ // the viewport, and a plain `effect()` runs before this cycle's embedded
615
+ // views are refreshed. Tracking `_viewportRef()` as well as `orientation()`
616
+ // makes it self-healing — if the view query has not resolved on the pass
617
+ // that commits `'horizontal'`, the effect re-runs when it does.
618
+ //
619
+ // The body is wrapped in `untracked()` so the tracked region is exactly
620
+ // `{ orientation, _viewportRef }`: `_setupScrollDetection()` writes
621
+ // `_canScrollPrev`/`_canScrollNext`, and reading them here would be a
622
+ // read-write cycle of the shape CLAUDE.md bans.
623
+ afterRenderEffect(() => {
624
+ this.orientation();
625
+ this._viewportRef();
626
+ untracked(() => this._setupScrollDetection());
627
+ });
563
628
  this._destroyRef.onDestroy(() => {
564
629
  this._resizeObserver?.disconnect();
565
630
  this._resizeObserver = null;
566
631
  });
567
- if (this._directionality) {
568
- const sub = this._directionality.change.subscribe((value) => {
569
- this._cdkDir.set(value);
570
- });
571
- this._destroyRef.onDestroy(() => sub.unsubscribe());
572
- }
573
632
  }
574
633
  _setupScrollDetection() {
634
+ // Re-entrant: this runs on every orientation/viewport change, so drop any
635
+ // observer bound to the previous (now detached) viewport element first.
636
+ // Done before the early return so flipping back to vertical also releases
637
+ // it. `_destroyRef.onDestroy` still owns the final teardown.
638
+ this._resizeObserver?.disconnect();
639
+ this._resizeObserver = null;
575
640
  // Vertical orientation never renders the viewport — nothing to do.
576
641
  if (this.orientation() !== 'horizontal')
577
642
  return;
@@ -668,7 +733,7 @@ class TimelineComponent {
668
733
  state,
669
734
  }).number();
670
735
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: TimelineComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
671
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: TimelineComponent, isStandalone: true, selector: "tw-timeline", inputs: { orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, lineStyle: { classPropertyName: "lineStyle", publicName: "lineStyle", isSignal: true, isRequired: false, transformFunction: null }, scrollControls: { classPropertyName: "scrollControls", publicName: "scrollControls", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "list" }, properties: { "class": "rootClasses()", "attr.aria-orientation": "orientation() === \"horizontal\" ? \"horizontal\" : null" } }, queries: [{ propertyName: "items", predicate: TimelineItemComponent, isSignal: true }], viewQueries: [{ propertyName: "_viewportRef", first: true, predicate: ["scrollViewport"], descendants: true, isSignal: true }], ngImport: i0, template: `
736
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", type: TimelineComponent, isStandalone: true, selector: "tw-timeline", inputs: { orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, lineStyle: { classPropertyName: "lineStyle", publicName: "lineStyle", isSignal: true, isRequired: false, transformFunction: null }, scrollControls: { classPropertyName: "scrollControls", publicName: "scrollControls", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.role": "orientation() === \"horizontal\" ? null : \"list\"", "class": "rootClasses()" } }, queries: [{ propertyName: "items", predicate: TimelineItemComponent, isSignal: true }], viewQueries: [{ propertyName: "_viewportRef", first: true, predicate: ["scrollViewport"], descendants: true, isSignal: true }], ngImport: i0, template: `
672
737
  <ng-template #itemsTpl>
673
738
  <ng-content />
674
739
  </ng-template>
@@ -696,6 +761,9 @@ class TimelineComponent {
696
761
  </button>
697
762
  <div
698
763
  #scrollViewport
764
+ role="list"
765
+ tabindex="0"
766
+ [attr.aria-label]="scrollLabels().scrollRegion"
699
767
  [class]="horizontalViewportClasses"
700
768
  (scroll)="_onScroll()"
701
769
  >
@@ -733,9 +801,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
733
801
  changeDetection: ChangeDetectionStrategy.OnPush,
734
802
  imports: [NgTemplateOutlet],
735
803
  host: {
736
- role: 'list',
804
+ // The `list` role lives on the host only in vertical orientation, where the
805
+ // projected `tw-timeline-item` elements (role="listitem") are its direct
806
+ // children. In horizontal orientation the host also owns the two scroll
807
+ // chevrons, which are not `listitem`s — a `list` here would own children its
808
+ // role forbids (axe: aria-required-children). The role moves to the scroll
809
+ // viewport instead, which does contain exactly the items.
810
+ //
811
+ // `aria-orientation` is deliberately absent: `list` does not support it
812
+ // (axe: aria-allowed-attr), and the axis is a purely visual decision that
813
+ // carries no meaning for assistive tech.
814
+ '[attr.role]': 'orientation() === "horizontal" ? null : "list"',
737
815
  '[class]': 'rootClasses()',
738
- '[attr.aria-orientation]': 'orientation() === "horizontal" ? "horizontal" : null',
739
816
  },
740
817
  // The projected items live inside a single `<ng-template #itemsTpl>` and are
741
818
  // outletted into either the horizontal viewport or the vertical pass-through.
@@ -772,6 +849,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImpor
772
849
  </button>
773
850
  <div
774
851
  #scrollViewport
852
+ role="list"
853
+ tabindex="0"
854
+ [attr.aria-label]="scrollLabels().scrollRegion"
775
855
  [class]="horizontalViewportClasses"
776
856
  (scroll)="_onScroll()"
777
857
  >
@@ -981,7 +1061,10 @@ class TimelineItemComponent {
981
1061
  if (this.timeline.orientation() === 'horizontal')
982
1062
  return '';
983
1063
  // Pin the column to the marker's own width so siblings stay aligned even
984
- // when items render slightly different bubble content.
1064
+ // when items render slightly different bubble content. These mirror the
1065
+ // circle-marker diameters one-for-one (`size-6`…`size-12` above) — the
1066
+ // `w-10`/`w-12` steps are off-scale for the same reason the bubbles are,
1067
+ // and diverging here would knock every marker off the connector's axis.
985
1068
  switch (this.timeline.size()) {
986
1069
  case 'xs':
987
1070
  return 'w-6';