@cdevhub/ngx-tw 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +134 -0
  3. package/fesm2022/cdevhub-ngx-tw-accordion.mjs +108 -0
  4. package/fesm2022/cdevhub-ngx-tw-accordion.mjs.map +1 -0
  5. package/fesm2022/cdevhub-ngx-tw-alert.mjs +240 -0
  6. package/fesm2022/cdevhub-ngx-tw-alert.mjs.map +1 -0
  7. package/fesm2022/cdevhub-ngx-tw-avatar.mjs +287 -0
  8. package/fesm2022/cdevhub-ngx-tw-avatar.mjs.map +1 -0
  9. package/fesm2022/cdevhub-ngx-tw-badge.mjs +332 -0
  10. package/fesm2022/cdevhub-ngx-tw-badge.mjs.map +1 -0
  11. package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs +452 -0
  12. package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs.map +1 -0
  13. package/fesm2022/cdevhub-ngx-tw-button.mjs +227 -0
  14. package/fesm2022/cdevhub-ngx-tw-button.mjs.map +1 -0
  15. package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs +417 -0
  16. package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs.map +1 -0
  17. package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs +232 -0
  18. package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs.map +1 -0
  19. package/fesm2022/cdevhub-ngx-tw-calendar.mjs +4482 -0
  20. package/fesm2022/cdevhub-ngx-tw-calendar.mjs.map +1 -0
  21. package/fesm2022/cdevhub-ngx-tw-card.mjs +157 -0
  22. package/fesm2022/cdevhub-ngx-tw-card.mjs.map +1 -0
  23. package/fesm2022/cdevhub-ngx-tw-carousel.mjs +1454 -0
  24. package/fesm2022/cdevhub-ngx-tw-carousel.mjs.map +1 -0
  25. package/fesm2022/cdevhub-ngx-tw-checkbox.mjs +593 -0
  26. package/fesm2022/cdevhub-ngx-tw-checkbox.mjs.map +1 -0
  27. package/fesm2022/cdevhub-ngx-tw-code-block.mjs +228 -0
  28. package/fesm2022/cdevhub-ngx-tw-code-block.mjs.map +1 -0
  29. package/fesm2022/cdevhub-ngx-tw-collapsible.mjs +510 -0
  30. package/fesm2022/cdevhub-ngx-tw-collapsible.mjs.map +1 -0
  31. package/fesm2022/cdevhub-ngx-tw-combobox.mjs +1721 -0
  32. package/fesm2022/cdevhub-ngx-tw-combobox.mjs.map +1 -0
  33. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs +721 -0
  34. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs.map +1 -0
  35. package/fesm2022/cdevhub-ngx-tw-core.mjs +861 -0
  36. package/fesm2022/cdevhub-ngx-tw-core.mjs.map +1 -0
  37. package/fesm2022/cdevhub-ngx-tw-date-picker.mjs +1558 -0
  38. package/fesm2022/cdevhub-ngx-tw-date-picker.mjs.map +1 -0
  39. package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs +1833 -0
  40. package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs.map +1 -0
  41. package/fesm2022/cdevhub-ngx-tw-dialog.mjs +693 -0
  42. package/fesm2022/cdevhub-ngx-tw-dialog.mjs.map +1 -0
  43. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs +331 -0
  44. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs.map +1 -0
  45. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs +289 -0
  46. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs.map +1 -0
  47. package/fesm2022/cdevhub-ngx-tw-form-field.mjs +572 -0
  48. package/fesm2022/cdevhub-ngx-tw-form-field.mjs.map +1 -0
  49. package/fesm2022/cdevhub-ngx-tw-icon-lucide.mjs +36 -0
  50. package/fesm2022/cdevhub-ngx-tw-icon-lucide.mjs.map +1 -0
  51. package/fesm2022/cdevhub-ngx-tw-icon.mjs +278 -0
  52. package/fesm2022/cdevhub-ngx-tw-icon.mjs.map +1 -0
  53. package/fesm2022/cdevhub-ngx-tw-input.mjs +355 -0
  54. package/fesm2022/cdevhub-ngx-tw-input.mjs.map +1 -0
  55. package/fesm2022/cdevhub-ngx-tw-item.mjs +241 -0
  56. package/fesm2022/cdevhub-ngx-tw-item.mjs.map +1 -0
  57. package/fesm2022/cdevhub-ngx-tw-menu.mjs +433 -0
  58. package/fesm2022/cdevhub-ngx-tw-menu.mjs.map +1 -0
  59. package/fesm2022/cdevhub-ngx-tw-paginator.mjs +691 -0
  60. package/fesm2022/cdevhub-ngx-tw-paginator.mjs.map +1 -0
  61. package/fesm2022/cdevhub-ngx-tw-popover.mjs +678 -0
  62. package/fesm2022/cdevhub-ngx-tw-popover.mjs.map +1 -0
  63. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs +335 -0
  64. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs.map +1 -0
  65. package/fesm2022/cdevhub-ngx-tw-radio.mjs +708 -0
  66. package/fesm2022/cdevhub-ngx-tw-radio.mjs.map +1 -0
  67. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs +319 -0
  68. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs.map +1 -0
  69. package/fesm2022/cdevhub-ngx-tw-select.mjs +1722 -0
  70. package/fesm2022/cdevhub-ngx-tw-select.mjs.map +1 -0
  71. package/fesm2022/cdevhub-ngx-tw-separator.mjs +115 -0
  72. package/fesm2022/cdevhub-ngx-tw-separator.mjs.map +1 -0
  73. package/fesm2022/cdevhub-ngx-tw-sheet.mjs +768 -0
  74. package/fesm2022/cdevhub-ngx-tw-sheet.mjs.map +1 -0
  75. package/fesm2022/cdevhub-ngx-tw-skeleton.mjs +149 -0
  76. package/fesm2022/cdevhub-ngx-tw-skeleton.mjs.map +1 -0
  77. package/fesm2022/cdevhub-ngx-tw-slider.mjs +1147 -0
  78. package/fesm2022/cdevhub-ngx-tw-slider.mjs.map +1 -0
  79. package/fesm2022/cdevhub-ngx-tw-sort.mjs +346 -0
  80. package/fesm2022/cdevhub-ngx-tw-sort.mjs.map +1 -0
  81. package/fesm2022/cdevhub-ngx-tw-spinner.mjs +186 -0
  82. package/fesm2022/cdevhub-ngx-tw-spinner.mjs.map +1 -0
  83. package/fesm2022/cdevhub-ngx-tw-split.mjs +1082 -0
  84. package/fesm2022/cdevhub-ngx-tw-split.mjs.map +1 -0
  85. package/fesm2022/cdevhub-ngx-tw-stat.mjs +579 -0
  86. package/fesm2022/cdevhub-ngx-tw-stat.mjs.map +1 -0
  87. package/fesm2022/cdevhub-ngx-tw-stepper.mjs +443 -0
  88. package/fesm2022/cdevhub-ngx-tw-stepper.mjs.map +1 -0
  89. package/fesm2022/cdevhub-ngx-tw-switch.mjs +391 -0
  90. package/fesm2022/cdevhub-ngx-tw-switch.mjs.map +1 -0
  91. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs +315 -0
  92. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs.map +1 -0
  93. package/fesm2022/cdevhub-ngx-tw-table.mjs +994 -0
  94. package/fesm2022/cdevhub-ngx-tw-table.mjs.map +1 -0
  95. package/fesm2022/cdevhub-ngx-tw-tabs.mjs +508 -0
  96. package/fesm2022/cdevhub-ngx-tw-tabs.mjs.map +1 -0
  97. package/fesm2022/cdevhub-ngx-tw-textarea.mjs +147 -0
  98. package/fesm2022/cdevhub-ngx-tw-textarea.mjs.map +1 -0
  99. package/fesm2022/cdevhub-ngx-tw-theme.mjs +178 -0
  100. package/fesm2022/cdevhub-ngx-tw-theme.mjs.map +1 -0
  101. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs +1344 -0
  102. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs.map +1 -0
  103. package/fesm2022/cdevhub-ngx-tw-timeline.mjs +1136 -0
  104. package/fesm2022/cdevhub-ngx-tw-timeline.mjs.map +1 -0
  105. package/fesm2022/cdevhub-ngx-tw-toast.mjs +1311 -0
  106. package/fesm2022/cdevhub-ngx-tw-toast.mjs.map +1 -0
  107. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs +533 -0
  108. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs.map +1 -0
  109. package/fesm2022/cdevhub-ngx-tw.mjs +61 -0
  110. package/fesm2022/cdevhub-ngx-tw.mjs.map +1 -0
  111. package/package.json +265 -0
  112. package/theme/_base.css +394 -0
  113. package/theme/_dark.css +510 -0
  114. package/theme/_high-contrast.css +290 -0
  115. package/theme/_semantic.css +317 -0
  116. package/theme/_typography.css +9 -0
  117. package/theme/index.css +54 -0
  118. package/types/cdevhub-ngx-tw-accordion.d.ts +48 -0
  119. package/types/cdevhub-ngx-tw-alert.d.ts +67 -0
  120. package/types/cdevhub-ngx-tw-avatar.d.ts +79 -0
  121. package/types/cdevhub-ngx-tw-badge.d.ts +94 -0
  122. package/types/cdevhub-ngx-tw-breadcrumbs.d.ts +227 -0
  123. package/types/cdevhub-ngx-tw-button.d.ts +74 -0
  124. package/types/cdevhub-ngx-tw-calendar-luxon.d.ts +200 -0
  125. package/types/cdevhub-ngx-tw-calendar-testing.d.ts +113 -0
  126. package/types/cdevhub-ngx-tw-calendar.d.ts +1704 -0
  127. package/types/cdevhub-ngx-tw-card.d.ts +48 -0
  128. package/types/cdevhub-ngx-tw-carousel.d.ts +406 -0
  129. package/types/cdevhub-ngx-tw-checkbox.d.ts +136 -0
  130. package/types/cdevhub-ngx-tw-code-block.d.ts +93 -0
  131. package/types/cdevhub-ngx-tw-collapsible.d.ts +151 -0
  132. package/types/cdevhub-ngx-tw-combobox.d.ts +389 -0
  133. package/types/cdevhub-ngx-tw-command-palette.d.ts +215 -0
  134. package/types/cdevhub-ngx-tw-core.d.ts +719 -0
  135. package/types/cdevhub-ngx-tw-date-picker.d.ts +317 -0
  136. package/types/cdevhub-ngx-tw-date-range-picker.d.ts +313 -0
  137. package/types/cdevhub-ngx-tw-dialog.d.ts +303 -0
  138. package/types/cdevhub-ngx-tw-empty-state.d.ts +104 -0
  139. package/types/cdevhub-ngx-tw-flip-card.d.ts +95 -0
  140. package/types/cdevhub-ngx-tw-form-field.d.ts +210 -0
  141. package/types/cdevhub-ngx-tw-icon-lucide.d.ts +29 -0
  142. package/types/cdevhub-ngx-tw-icon.d.ts +134 -0
  143. package/types/cdevhub-ngx-tw-input.d.ts +110 -0
  144. package/types/cdevhub-ngx-tw-item.d.ts +70 -0
  145. package/types/cdevhub-ngx-tw-menu.d.ts +99 -0
  146. package/types/cdevhub-ngx-tw-paginator.d.ts +305 -0
  147. package/types/cdevhub-ngx-tw-popover.d.ts +157 -0
  148. package/types/cdevhub-ngx-tw-progress-bar.d.ts +100 -0
  149. package/types/cdevhub-ngx-tw-radio.d.ts +174 -0
  150. package/types/cdevhub-ngx-tw-segmented-control.d.ts +66 -0
  151. package/types/cdevhub-ngx-tw-select.d.ts +384 -0
  152. package/types/cdevhub-ngx-tw-separator.d.ts +28 -0
  153. package/types/cdevhub-ngx-tw-sheet.d.ts +335 -0
  154. package/types/cdevhub-ngx-tw-skeleton.d.ts +52 -0
  155. package/types/cdevhub-ngx-tw-slider.d.ts +258 -0
  156. package/types/cdevhub-ngx-tw-sort.d.ts +136 -0
  157. package/types/cdevhub-ngx-tw-spinner.d.ts +50 -0
  158. package/types/cdevhub-ngx-tw-split.d.ts +265 -0
  159. package/types/cdevhub-ngx-tw-stat.d.ts +140 -0
  160. package/types/cdevhub-ngx-tw-stepper.d.ts +127 -0
  161. package/types/cdevhub-ngx-tw-switch.d.ts +88 -0
  162. package/types/cdevhub-ngx-tw-tab-nav.d.ts +107 -0
  163. package/types/cdevhub-ngx-tw-table.d.ts +479 -0
  164. package/types/cdevhub-ngx-tw-tabs.d.ts +121 -0
  165. package/types/cdevhub-ngx-tw-textarea.d.ts +62 -0
  166. package/types/cdevhub-ngx-tw-theme.d.ts +106 -0
  167. package/types/cdevhub-ngx-tw-time-picker.d.ts +303 -0
  168. package/types/cdevhub-ngx-tw-timeline.d.ts +278 -0
  169. package/types/cdevhub-ngx-tw-toast.d.ts +388 -0
  170. package/types/cdevhub-ngx-tw-tooltip.d.ts +95 -0
  171. package/types/cdevhub-ngx-tw.d.ts +49 -0
@@ -0,0 +1,1136 @@
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';
3
+ import { NgTemplateOutlet } from '@angular/common';
4
+ import { Directionality } from '@angular/cdk/bidi';
5
+ import { tv } from 'tailwind-variants';
6
+
7
+ /** Default English labels used when `TW_TIMELINE_SCROLL_LABELS` is not provided. */
8
+ const DEFAULT_TW_TIMELINE_SCROLL_LABELS = {
9
+ scrollPrevious: 'Scroll to previous events',
10
+ scrollNext: 'Scroll to next events',
11
+ };
12
+ /**
13
+ * 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.
16
+ *
17
+ * Provide via `provideTwTimelineScrollLabels({ ... })` at the root or feature level.
18
+ * If both keys are present they override the English defaults; if only one is present
19
+ * the other falls back to the English default.
20
+ *
21
+ * First concrete token under the `TW_TIMELINE_I18N` reservation in
22
+ * `docs/requirements/timeline.requirements.md` § 11.2.
23
+ */
24
+ const TW_TIMELINE_SCROLL_LABELS = new InjectionToken('TW_TIMELINE_SCROLL_LABELS');
25
+ /**
26
+ * Configures the localised labels used by `tw-timeline` overflow chevrons.
27
+ * Add to `bootstrapApplication`'s `providers` (or any feature/route provider array).
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * bootstrapApplication(App, {
32
+ * providers: [
33
+ * provideTwTimelineScrollLabels({
34
+ * scrollPrevious: 'Vorherige Ereignisse',
35
+ * scrollNext: 'Nächste Ereignisse',
36
+ * }),
37
+ * ],
38
+ * });
39
+ * ```
40
+ */
41
+ function provideTwTimelineScrollLabels(labels) {
42
+ return makeEnvironmentProviders([
43
+ { provide: TW_TIMELINE_SCROLL_LABELS, useValue: labels },
44
+ ]);
45
+ }
46
+ // ── tv() config ──
47
+ const timelineVariants = tv({
48
+ slots: {
49
+ // Container host.
50
+ root: 'flex w-full',
51
+ // Item host.
52
+ item: 'relative flex min-w-0',
53
+ // The marker column (vertical) or marker row (horizontal). Carries the bubble plus
54
+ // its leading + trailing connector segments.
55
+ markerSide: 'relative flex shrink-0',
56
+ // The bubble itself. Color × state styling is applied via static lookup, not tv().
57
+ marker: 'relative z-[1] inline-flex items-center justify-center shrink-0 rounded-full transition-[color,background-color,border-color,box-shadow] duration-200 motion-reduce:transition-none',
58
+ // Connector segments. Geometry comes from compound variants; color is applied via static lookup.
59
+ connectorLeading: 'shrink-0',
60
+ connectorTrailing: 'shrink-0',
61
+ // Body wrapper next to the marker. min-w-0 so flex children can truncate.
62
+ body: 'relative min-w-0 flex flex-col',
63
+ // Opposite wrapper for align="alternate" / align="split". Same min-width constraints as body.
64
+ opposite: 'min-w-0 flex flex-col',
65
+ // Auto-number / single-glyph slot inside a circle marker.
66
+ number: 'leading-none font-semibold',
67
+ // Timestamp element (whether <time> or <span>).
68
+ timestamp: 'text-fg-muted',
69
+ },
70
+ variants: {
71
+ orientation: {
72
+ vertical: {
73
+ root: 'flex-col',
74
+ // `items-stretch` (the flex default) lets the marker column stretch to
75
+ // the body's full height — required for the trailing connector to
76
+ // physically reach the next item's marker.
77
+ item: 'flex-row',
78
+ markerSide: 'flex-col items-center',
79
+ },
80
+ horizontal: {
81
+ // `relative` anchors the absolutely-positioned chevron buttons. The
82
+ // `overflow-x-auto` lives on the inner viewport (see template) so the
83
+ // chevrons stay fixed against the host while content scrolls.
84
+ root: 'flex-row items-start relative',
85
+ // Grid-row participation: each item spans both rows of the viewport's
86
+ // `grid-template-rows: minmax(0, 1fr) auto`. `grid-rows-subgrid`
87
+ // inherits the parent grid's row tracks so every item's marker-side
88
+ // lands on a shared row-2 baseline regardless of body height in row 1.
89
+ // Without this, per-item body-height variance pushes each marker to a
90
+ // different y-coordinate and the horizontal connector breaks across
91
+ // item boundaries. Row placement (body → row 1, markerSide → row 2)
92
+ // is applied via the horizontal compoundVariant below.
93
+ item: 'grid grid-rows-subgrid row-span-2 items-center text-center',
94
+ markerSide: 'flex-row items-center w-full',
95
+ body: 'items-center',
96
+ },
97
+ },
98
+ size: {
99
+ // The trailing `pb-*` on body provides inter-item visual separation in
100
+ // vertical orientation. The trailing connector lives inside the marker
101
+ // column and stretches through this padding to touch the next item's
102
+ // marker — items butt against each other with no container `gap-*` so
103
+ // the line stays continuous.
104
+ xs: { item: 'gap-3', body: 'text-xs gap-1 pb-3', timestamp: 'text-2xs', number: 'text-2xs' },
105
+ sm: { item: 'gap-4', body: 'text-sm gap-1 pb-4', timestamp: 'text-xs', number: 'text-xs' },
106
+ md: { item: 'gap-5', body: 'text-sm gap-1.5 pb-6', timestamp: 'text-xs', number: 'text-xs' },
107
+ lg: { item: 'gap-6', body: 'text-sm gap-1.5 pb-8', timestamp: 'text-xs', number: 'text-sm' },
108
+ // `xl` density holds at text-sm — `text-base` is reserved for the codified
109
+ // tw-item lg exception and does not apply to timeline (per requirements § 4.3).
110
+ xl: { item: 'gap-8', body: 'text-sm gap-2 pb-10', timestamp: 'text-xs', number: 'text-sm' },
111
+ },
112
+ marker: {
113
+ dot: {},
114
+ circle: {},
115
+ },
116
+ lineStyle: {
117
+ solid: {},
118
+ dashed: {
119
+ // Dashed connectors replace the solid bg fill with a CSS border. The base
120
+ // `connectorLeading`/`connectorTrailing` classes still apply; `bg-transparent`
121
+ // is forced so twMerge doesn't end up with both a `bg-*` and a `border-*` fill.
122
+ connectorLeading: 'border-dashed bg-transparent',
123
+ connectorTrailing: 'border-dashed bg-transparent',
124
+ },
125
+ },
126
+ state: {
127
+ // The `state` axis exists in `tv()` only to control the auto-number's text
128
+ // color step — color × state combinations on the marker itself are applied
129
+ // via static lookup tables (Tailwind v4 cannot resolve interpolated class names).
130
+ reached: { number: 'text-fg' },
131
+ pending: { number: 'text-fg-muted' },
132
+ current: { number: 'text-fg' },
133
+ error: { number: 'text-fg' },
134
+ },
135
+ },
136
+ compoundVariants: [
137
+ // ── Horizontal item min-width per density. Values chosen so a marker
138
+ // plus ~16–24 chars of body text fit without truncation at each step;
139
+ // the lower bound is the diameter of the corresponding circle marker
140
+ // (size-6 → 24px) plus a comfortable ~120px text gutter, scaling up to
141
+ // leave room for richer descriptions at xl. Once any item's content
142
+ // exceeds the floor the items expand naturally; the inner scroll
143
+ // viewport handles overflow beyond the host's width.
144
+ { orientation: 'horizontal', size: 'xs', class: { item: 'min-w-32' } },
145
+ { orientation: 'horizontal', size: 'sm', class: { item: 'min-w-36' } },
146
+ { orientation: 'horizontal', size: 'md', class: { item: 'min-w-40' } },
147
+ { orientation: 'horizontal', size: 'lg', class: { item: 'min-w-44' } },
148
+ { orientation: 'horizontal', size: 'xl', class: { item: 'min-w-48' } },
149
+ // Subgrid row placement (horizontal only). Body occupies row 1 with
150
+ // `self-end` so short bodies hang at the row's bottom edge — sitting
151
+ // close to the marker row directly below — instead of floating at the
152
+ // top and opening a visible gap. Marker-side occupies the shared row 2.
153
+ {
154
+ orientation: 'horizontal',
155
+ class: { body: 'row-start-1 self-end', markerSide: 'row-start-2' },
156
+ },
157
+ // ── Marker geometry per size × marker variant (requirements § 4.3) ──
158
+ // Diameter is orientation-agnostic.
159
+ { marker: 'dot', size: 'xs', class: { marker: 'size-2' } },
160
+ { marker: 'dot', size: 'sm', class: { marker: 'size-2.5' } },
161
+ { marker: 'dot', size: 'md', class: { marker: 'size-3' } },
162
+ { marker: 'dot', size: 'lg', class: { marker: 'size-3' } },
163
+ { marker: 'dot', size: 'xl', class: { marker: 'size-3' } },
164
+ { marker: 'circle', size: 'xs', class: { marker: 'size-6' } },
165
+ { marker: 'circle', size: 'sm', class: { marker: 'size-7' } },
166
+ { marker: 'circle', size: 'md', class: { marker: 'size-8' } },
167
+ { marker: 'circle', size: 'lg', class: { marker: 'size-10' } },
168
+ { marker: 'circle', size: 'xl', class: { marker: 'size-12' } },
169
+ // Vertical-only: `mt-*` nudges the small dot down so its vertical center
170
+ // aligns with the title's first-line optical center (text sits to the
171
+ // side of the marker in vertical layout). Circle markers are large
172
+ // enough that no nudge is needed — the bubble visually wraps the title's
173
+ // first line. In horizontal layout the dot must NOT carry margin-top:
174
+ // the marker-side is `flex-row items-center` and the connector segments
175
+ // sit on the same vertical axis, so margin-top would push the dot below
176
+ // the connector line.
177
+ { orientation: 'vertical', marker: 'dot', size: 'xs', class: { marker: 'mt-1.5' } },
178
+ { orientation: 'vertical', marker: 'dot', size: 'sm', class: { marker: 'mt-1.5' } },
179
+ { orientation: 'vertical', marker: 'dot', size: 'md', class: { marker: 'mt-1.5' } },
180
+ { orientation: 'vertical', marker: 'dot', size: 'lg', class: { marker: 'mt-2' } },
181
+ { orientation: 'vertical', marker: 'dot', size: 'xl', class: { marker: 'mt-2' } },
182
+ // ── Vertical connector geometry: thin line that runs through the marker column.
183
+ // `flex-1` stretches the segment to fill the gap between adjacent markers.
184
+ {
185
+ orientation: 'vertical',
186
+ lineStyle: 'solid',
187
+ class: { connectorLeading: 'w-px flex-1', connectorTrailing: 'w-px flex-1' },
188
+ },
189
+ {
190
+ orientation: 'vertical',
191
+ lineStyle: 'dashed',
192
+ class: { connectorLeading: 'border-l flex-1 min-h-2', connectorTrailing: 'border-l flex-1 min-h-2' },
193
+ },
194
+ {
195
+ orientation: 'vertical',
196
+ size: 'lg',
197
+ lineStyle: 'solid',
198
+ class: { connectorLeading: 'w-0.5', connectorTrailing: 'w-0.5' },
199
+ },
200
+ {
201
+ orientation: 'vertical',
202
+ size: 'xl',
203
+ lineStyle: 'solid',
204
+ class: { connectorLeading: 'w-0.5', connectorTrailing: 'w-0.5' },
205
+ },
206
+ // ── Horizontal connector geometry ──
207
+ {
208
+ orientation: 'horizontal',
209
+ lineStyle: 'solid',
210
+ class: { connectorLeading: 'h-px flex-1', connectorTrailing: 'h-px flex-1' },
211
+ },
212
+ {
213
+ orientation: 'horizontal',
214
+ lineStyle: 'dashed',
215
+ class: { connectorLeading: 'border-t flex-1 min-w-2', connectorTrailing: 'border-t flex-1 min-w-2' },
216
+ },
217
+ {
218
+ orientation: 'horizontal',
219
+ size: 'lg',
220
+ lineStyle: 'solid',
221
+ class: { connectorLeading: 'h-0.5', connectorTrailing: 'h-0.5' },
222
+ },
223
+ {
224
+ orientation: 'horizontal',
225
+ size: 'xl',
226
+ lineStyle: 'solid',
227
+ class: { connectorLeading: 'h-0.5', connectorTrailing: 'h-0.5' },
228
+ },
229
+ ],
230
+ defaultVariants: {
231
+ orientation: 'vertical',
232
+ size: 'md',
233
+ marker: 'dot',
234
+ state: 'reached',
235
+ lineStyle: 'solid',
236
+ },
237
+ }, { twMerge: true });
238
+ // ── Static color × state class lookups ──
239
+ // Tailwind v4 only resolves statically-written class names. Per the stepper
240
+ // precedent (`INDICATOR_ACTIVE`, `INDICATOR_COMPLETED`, `CONNECTOR_REACHED`),
241
+ // all color × state combinations are enumerated as exhaustive maps.
242
+ const MARKER_PENDING = 'bg-surface border-2 border-border text-fg-muted';
243
+ const MARKER_ERROR = 'bg-error-solid text-error-solid-fg border border-error-border-strong';
244
+ const MARKER_REACHED = {
245
+ primary: 'bg-primary-solid text-primary-solid-fg border border-primary-border-strong',
246
+ secondary: 'bg-secondary-solid text-secondary-solid-fg border border-secondary-border-strong',
247
+ accent: 'bg-accent-solid text-accent-solid-fg border border-accent-border-strong',
248
+ neutral: 'bg-neutral-solid text-neutral-solid-fg border border-neutral-border-strong',
249
+ info: 'bg-info-solid text-info-solid-fg border border-info-border-strong',
250
+ success: 'bg-success-solid text-success-solid-fg border border-success-border-strong',
251
+ warning: 'bg-warning-solid text-warning-solid-fg border border-warning-border-strong',
252
+ error: 'bg-error-solid text-error-solid-fg border border-error-border-strong',
253
+ };
254
+ const MARKER_CURRENT = {
255
+ primary: 'bg-primary-solid text-primary-solid-fg border border-primary-border-strong ring-4 ring-primary-soft',
256
+ secondary: 'bg-secondary-solid text-secondary-solid-fg border border-secondary-border-strong ring-4 ring-secondary-soft',
257
+ accent: 'bg-accent-solid text-accent-solid-fg border border-accent-border-strong ring-4 ring-accent-soft',
258
+ neutral: 'bg-neutral-solid text-neutral-solid-fg border border-neutral-border-strong ring-4 ring-neutral-soft',
259
+ info: 'bg-info-solid text-info-solid-fg border border-info-border-strong ring-4 ring-info-soft',
260
+ success: 'bg-success-solid text-success-solid-fg border border-success-border-strong ring-4 ring-success-soft',
261
+ warning: 'bg-warning-solid text-warning-solid-fg border border-warning-border-strong ring-4 ring-warning-soft',
262
+ error: 'bg-error-solid text-error-solid-fg border border-error-border-strong ring-4 ring-error-soft',
263
+ };
264
+ // When marker='circle' AND a `[twTimelineMarker]` slot is projected, the bubble
265
+ // switches to a SOFT background so the projected glyph/avatar reads against a
266
+ // tinted surface rather than competing with a solid fill.
267
+ const MARKER_REACHED_SOFT = {
268
+ primary: 'bg-primary-soft text-primary-fg border border-primary-border-strong',
269
+ secondary: 'bg-secondary-soft text-secondary-fg border border-secondary-border-strong',
270
+ accent: 'bg-accent-soft text-accent-fg border border-accent-border-strong',
271
+ neutral: 'bg-surface-muted text-fg border border-border',
272
+ info: 'bg-info-soft text-info-fg border border-info-border-strong',
273
+ success: 'bg-success-soft text-success-fg border border-success-border-strong',
274
+ warning: 'bg-warning-soft text-warning-fg border border-warning-border-strong',
275
+ error: 'bg-error-soft text-error-fg border border-error-border-strong',
276
+ };
277
+ const MARKER_CURRENT_SOFT = {
278
+ primary: 'bg-primary-soft text-primary-fg border border-primary-border-strong ring-4 ring-primary-soft',
279
+ secondary: 'bg-secondary-soft text-secondary-fg border border-secondary-border-strong ring-4 ring-secondary-soft',
280
+ accent: 'bg-accent-soft text-accent-fg border border-accent-border-strong ring-4 ring-accent-soft',
281
+ neutral: 'bg-surface-muted text-fg border border-border ring-4 ring-border',
282
+ info: 'bg-info-soft text-info-fg border border-info-border-strong ring-4 ring-info-soft',
283
+ success: 'bg-success-soft text-success-fg border border-success-border-strong ring-4 ring-success-soft',
284
+ warning: 'bg-warning-soft text-warning-fg border border-warning-border-strong ring-4 ring-warning-soft',
285
+ error: 'bg-error-soft text-error-fg border border-error-border-strong ring-4 ring-error-soft',
286
+ };
287
+ // ── Connector color ──
288
+ // The trailing connector takes its color from the item the connector leaves;
289
+ // the leading connector takes its color from the *previous* item so a single
290
+ // physical line between two items reads as one consistent color.
291
+ const CONNECTOR_DEFAULT = 'bg-border';
292
+ const CONNECTOR_ERROR = 'bg-error-border-strong';
293
+ const CONNECTOR_DASHED_DEFAULT = 'border-border';
294
+ const CONNECTOR_DASHED_ERROR = 'border-error-border-strong';
295
+ const CONNECTOR_REACHED = {
296
+ primary: 'bg-primary-border-strong',
297
+ secondary: 'bg-secondary-border-strong',
298
+ accent: 'bg-accent-border-strong',
299
+ neutral: 'bg-neutral-border-strong',
300
+ info: 'bg-info-border-strong',
301
+ success: 'bg-success-border-strong',
302
+ warning: 'bg-warning-border-strong',
303
+ error: 'bg-error-border-strong',
304
+ };
305
+ const CONNECTOR_DASHED_REACHED = {
306
+ primary: 'border-primary-border-strong',
307
+ secondary: 'border-secondary-border-strong',
308
+ accent: 'border-accent-border-strong',
309
+ neutral: 'border-neutral-border-strong',
310
+ info: 'border-info-border-strong',
311
+ success: 'border-success-border-strong',
312
+ warning: 'border-warning-border-strong',
313
+ error: 'border-error-border-strong',
314
+ };
315
+ function resolveMarkerClasses(state, color, hasMarkerSlot) {
316
+ switch (state) {
317
+ case 'pending':
318
+ return MARKER_PENDING;
319
+ case 'error':
320
+ return MARKER_ERROR;
321
+ case 'reached':
322
+ return hasMarkerSlot ? MARKER_REACHED_SOFT[color] : MARKER_REACHED[color];
323
+ case 'current':
324
+ return hasMarkerSlot ? MARKER_CURRENT_SOFT[color] : MARKER_CURRENT[color];
325
+ }
326
+ }
327
+ function resolveConnectorClasses(state, color, lineStyle) {
328
+ // 'current' and 'pending' both render the neutral default — a connector that
329
+ // leaves a 'current' item points into territory that has not been reached yet.
330
+ if (lineStyle === 'dashed') {
331
+ if (state === 'error')
332
+ return CONNECTOR_DASHED_ERROR;
333
+ if (state === 'reached')
334
+ return CONNECTOR_DASHED_REACHED[color];
335
+ return CONNECTOR_DASHED_DEFAULT;
336
+ }
337
+ if (state === 'error')
338
+ return CONNECTOR_ERROR;
339
+ if (state === 'reached')
340
+ return CONNECTOR_REACHED[color];
341
+ return CONNECTOR_DEFAULT;
342
+ }
343
+ // ── Horizontal viewport class string ──
344
+ // CSS grid (not flex-row): rows are minmax(0, 1fr) / auto. Row 1 absorbs every
345
+ // item's body, row 2 carries the marker-side. Items participate via
346
+ // `grid-rows-subgrid row-span-2`, so every marker lands on a shared row-2
347
+ // baseline regardless of body-height variance — fixing the prior flex layout
348
+ // where shorter bodies pushed their marker bubble off the connector row.
349
+ //
350
+ // `auto-cols-max` is load-bearing: columns size to max-content so the grid's
351
+ // scrollWidth can exceed clientWidth and the chevron-overflow contract
352
+ // (docs/prompts/tw-timeline-horizontal-overflow.md) keeps working. DO NOT
353
+ // switch to `auto-cols-fr` or `auto-cols-[1fr]` — those collapse scrollWidth
354
+ // to clientWidth, the overflow check fails permanently, and the chevrons
355
+ // never enable. The per-density min-w-* on each item still establishes the
356
+ // floor.
357
+ const HORIZONTAL_VIEWPORT_CLASSES = 'tw-scrollbar-none grid grid-flow-col auto-cols-max ' +
358
+ '[grid-template-rows:minmax(0,1fr)_auto] ' +
359
+ 'overflow-x-auto scroll-smooth motion-reduce:scroll-auto w-full';
360
+ // ── Chevron button class string ──
361
+ // Shared by both overflow chevron buttons. Positioned absolutely against the
362
+ // timeline host (which carries `relative` in horizontal orientation via the
363
+ // tv() config) so `left-0` / `right-0` anchor correctly.
364
+ // `top-1/2 -translate-y-1/2` centres the button vertically — a pragmatic
365
+ // approximation for the canonical horizontal layout where the marker row sits
366
+ // roughly mid-host. `size-8` matches the canonical "square interactive target"
367
+ // md size; the inner chevron glyph is `size-4` (glyph-icon sub-scale).
368
+ // The disabled hover overrides suppress the hover state when disabled, otherwise
369
+ // twMerge would unify them.
370
+ const CHEVRON_CLASSES = 'absolute top-1/2 -translate-y-1/2 z-10 inline-flex items-center justify-center ' +
371
+ 'size-8 rounded-full bg-surface-raised border border-border shadow-sm ' +
372
+ 'text-fg-muted hover:text-fg hover:bg-surface-muted ' +
373
+ 'transition-colors duration-200 motion-reduce:transition-none ' +
374
+ 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500 ' +
375
+ 'disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-surface-raised disabled:hover:text-fg-muted';
376
+ // ── Slot directives ──
377
+ /**
378
+ * Marker-slot directive. Apply on any element projected inside a `<tw-timeline-item>`
379
+ * to render that element inside the marker bubble (only when `marker="circle"`).
380
+ *
381
+ * @example
382
+ * ```html
383
+ * <tw-timeline-item marker="circle" color="success">
384
+ * <tw-icon twTimelineMarker name="check" />
385
+ * <p class="text-sm">Done</p>
386
+ * </tw-timeline-item>
387
+ * ```
388
+ */
389
+ class TimelineMarkerDirective {
390
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: TimelineMarkerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
391
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: TimelineMarkerDirective, isStandalone: true, selector: "[twTimelineMarker]", ngImport: i0 });
392
+ }
393
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: TimelineMarkerDirective, decorators: [{
394
+ type: Directive,
395
+ args: [{
396
+ selector: '[twTimelineMarker]',
397
+ }]
398
+ }] });
399
+ /**
400
+ * Timestamp-slot directive. Replaces the rendered `timestamp` / `dateTime`
401
+ * output. Use for relative-time components ("2 hours ago").
402
+ */
403
+ class TimelineTimestampDirective {
404
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: TimelineTimestampDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
405
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: TimelineTimestampDirective, isStandalone: true, selector: "[twTimelineTimestamp]", ngImport: i0 });
406
+ }
407
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: TimelineTimestampDirective, decorators: [{
408
+ type: Directive,
409
+ args: [{
410
+ selector: '[twTimelineTimestamp]',
411
+ }]
412
+ }] });
413
+ /**
414
+ * Opposite-slot directive. Renders on the side opposite the body in
415
+ * `align="alternate"` / `align="split"` (vertical orientation only).
416
+ */
417
+ class TimelineOppositeDirective {
418
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: TimelineOppositeDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
419
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: TimelineOppositeDirective, isStandalone: true, selector: "[twTimelineOpposite]", ngImport: i0 });
420
+ }
421
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: TimelineOppositeDirective, decorators: [{
422
+ type: Directive,
423
+ args: [{
424
+ selector: '[twTimelineOpposite]',
425
+ }]
426
+ }] });
427
+ // ── TimelineComponent ──
428
+ /**
429
+ * Presentational chronological-sequence layout primitive. Renders a list of
430
+ * `<tw-timeline-item>` children connected by a line that runs through their
431
+ * markers. The container owns orientation, alignment, density, and line-style
432
+ * decisions; items style their own marker, state, and connector colors.
433
+ *
434
+ * **Not interactive.** Unlike `tw-stepper`, the timeline does not install a
435
+ * keyboard map, does not own panels, does not trap focus, and item hosts are
436
+ * not focusable. Consumers needing row-level activation project an
437
+ * interactive primitive (`<tw-item interactive>`, `<button>`, an anchor) inside
438
+ * the default slot.
439
+ */
440
+ class TimelineComponent {
441
+ /** Axis along which items are laid out. `'vertical'` stacks items top-to-bottom; `'horizontal'` lays them out left-to-right (RTL-aware). Defaults to `'vertical'`. */
442
+ orientation = input('vertical', ...(ngDevMode ? [{ debugName: "orientation" }] : /* istanbul ignore next */ []));
443
+ /** Vertical layout strategy. `'left'` / `'right'` place the marker on that side; `'alternate'` centers the marker and flips the body left ↔ right per item; `'split'` centers the marker with body on the right and the opposite slot on the left. Ignored when orientation is `'horizontal'`. Defaults to `'left'`. */
444
+ align = input('left', ...(ngDevMode ? [{ debugName: "align" }] : /* istanbul ignore next */ []));
445
+ /** Density and typography scale. Controls marker diameter, gap between items, and body typography step. Defaults to `'md'`. */
446
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
447
+ /** Connector line style applied to every gap between items. Defaults to `'solid'`. */
448
+ lineStyle = input('solid', ...(ngDevMode ? [{ debugName: "lineStyle" }] : /* istanbul ignore next */ []));
449
+ /**
450
+ * Visibility policy for the horizontal-overflow chevron buttons. `'auto'`
451
+ * shows them only when the inner scroll region can scroll in that direction;
452
+ * `'always'` renders both regardless of scroll state (disabled when at an
453
+ * edge); `'never'` hides them entirely (consumer manages overflow
454
+ * externally). Ignored when orientation is `'vertical'`. Defaults to
455
+ * `'auto'`.
456
+ *
457
+ * Counts as the 5th container input, justified by the "Overflow-control axis
458
+ * on layout primitives" cap exception: a layout primitive whose primary axes
459
+ * (orientation, alignment, size, line/style) already saturate ≤ 4 inputs MAY
460
+ * add a single additional input that toggles overflow-navigation
461
+ * affordances when overflow is a real concern for at least one axis value.
462
+ * The added input MUST be a single tri-state and MUST be inert on axis
463
+ * values where overflow cannot occur (here: `orientation === 'vertical'`).
464
+ */
465
+ scrollControls = input('auto', ...(ngDevMode ? [{ debugName: "scrollControls" }] : /* istanbul ignore next */ []));
466
+ /** @internal Static class string for the horizontal scroll viewport — see `HORIZONTAL_VIEWPORT_CLASSES`. */
467
+ horizontalViewportClasses = HORIZONTAL_VIEWPORT_CLASSES;
468
+ /** @internal Projected `tw-timeline-item` children in DOM order. */
469
+ items = contentChildren(TimelineItemComponent, { ...(ngDevMode ? { debugName: "items" } : /* istanbul ignore next */ {}), descendants: false });
470
+ // ── Horizontal scroll / chevron wiring ──
471
+ _destroyRef = inject(DestroyRef);
472
+ _directionality = inject(Directionality, { optional: true });
473
+ _viewportRef = viewChild('scrollViewport', ...(ngDevMode ? [{ debugName: "_viewportRef" }] : /* istanbul ignore next */ []));
474
+ _scrollLabelsOverride = inject(TW_TIMELINE_SCROLL_LABELS, {
475
+ optional: true,
476
+ });
477
+ /** @internal Live `dir` from CDK `Directionality`. */
478
+ _cdkDir = signal(this._directionality?.value ?? 'ltr', ...(ngDevMode ? [{ debugName: "_cdkDir" }] : /* istanbul ignore next */ []));
479
+ /** @internal True when the host renders inside a `dir="rtl"` ancestor. */
480
+ _isRtl = computed(() => this._cdkDir() === 'rtl', ...(ngDevMode ? [{ debugName: "_isRtl" }] : /* istanbul ignore next */ []));
481
+ /**
482
+ * @internal Defaults to `false` until the first measurement runs in
483
+ * `afterNextRender`. This avoids a one-frame "enabled-then-disabled" flash
484
+ * where the buttons appear live before the scroll state has been read.
485
+ */
486
+ _canScrollPrev = signal(false, ...(ngDevMode ? [{ debugName: "_canScrollPrev" }] : /* istanbul ignore next */ []));
487
+ _canScrollNext = signal(false, ...(ngDevMode ? [{ debugName: "_canScrollNext" }] : /* istanbul ignore next */ []));
488
+ /** @internal Resolved label record. Falls back to English defaults for any missing key. */
489
+ scrollLabels = computed(() => ({
490
+ ...DEFAULT_TW_TIMELINE_SCROLL_LABELS,
491
+ ...(this._scrollLabelsOverride ?? {}),
492
+ }), ...(ngDevMode ? [{ debugName: "scrollLabels" }] : /* istanbul ignore next */ []));
493
+ /** @internal True when the prev button should be aria-hidden + visually suppressed. */
494
+ _prevButtonHidden = computed(() => {
495
+ const policy = this.scrollControls();
496
+ if (policy === 'never')
497
+ return true;
498
+ if (policy === 'always')
499
+ return false;
500
+ return !this._canScrollPrev();
501
+ }, ...(ngDevMode ? [{ debugName: "_prevButtonHidden" }] : /* istanbul ignore next */ []));
502
+ /** @internal True when the next button should be aria-hidden + visually suppressed. */
503
+ _nextButtonHidden = computed(() => {
504
+ const policy = this.scrollControls();
505
+ if (policy === 'never')
506
+ return true;
507
+ if (policy === 'always')
508
+ return false;
509
+ return !this._canScrollNext();
510
+ }, ...(ngDevMode ? [{ debugName: "_nextButtonHidden" }] : /* istanbul ignore next */ []));
511
+ /** @internal Native `disabled` attribute. Always true for hidden buttons (defensive); also true for `'always'` policy when can't scroll. */
512
+ _prevButtonDisabled = computed(() => this._prevButtonHidden() || !this._canScrollPrev(), ...(ngDevMode ? [{ debugName: "_prevButtonDisabled" }] : /* istanbul ignore next */ []));
513
+ _nextButtonDisabled = computed(() => this._nextButtonHidden() || !this._canScrollNext(), ...(ngDevMode ? [{ debugName: "_nextButtonDisabled" }] : /* istanbul ignore next */ []));
514
+ /**
515
+ * @internal Final class string for the prev chevron — combines the shared
516
+ * `CHEVRON_CLASSES`, the directional anchor (`left-0` in LTR, `right-0` in
517
+ * RTL), and the visibility variant driven by `scrollControls`.
518
+ */
519
+ prevChevronClasses = computed(() => this._chevronClasses('prev'), ...(ngDevMode ? [{ debugName: "prevChevronClasses" }] : /* istanbul ignore next */ []));
520
+ nextChevronClasses = computed(() => this._chevronClasses('next'), ...(ngDevMode ? [{ debugName: "nextChevronClasses" }] : /* istanbul ignore next */ []));
521
+ _chevronClasses(side) {
522
+ const isRtl = this._isRtl();
523
+ // In LTR the prev arrow anchors to the host's left, next to the right.
524
+ // RTL flips: prev anchors right, next anchors left. The arrow glyph also
525
+ // flips (handled in the template).
526
+ const anchor = side === 'prev' ? (isRtl ? 'right-0' : 'left-0') : isRtl ? 'left-0' : 'right-0';
527
+ const policy = this.scrollControls();
528
+ const hidden = side === 'prev' ? this._prevButtonHidden() : this._nextButtonHidden();
529
+ // `'never'` → wholly hidden (no layout space, ignored by AT).
530
+ // `'auto'` + can't scroll that way → invisible but still occupies space
531
+ // (prevents the chevron from popping into view and shifting layout when
532
+ // scroll becomes possible mid-interaction).
533
+ // `'always'` → always visible; native `disabled` styling handles edges.
534
+ let visibility = '';
535
+ if (policy === 'never')
536
+ visibility = 'hidden';
537
+ else if (policy === 'auto' && hidden)
538
+ visibility = 'invisible';
539
+ return `${CHEVRON_CLASSES} ${anchor} ${visibility}`.trim();
540
+ }
541
+ _resizeObserver = null;
542
+ constructor() {
543
+ // The viewport materialises on the first frame after horizontal
544
+ // orientation is committed; afterNextRender hooks into that point.
545
+ afterNextRender(() => this._setupScrollDetection());
546
+ this._destroyRef.onDestroy(() => {
547
+ this._resizeObserver?.disconnect();
548
+ this._resizeObserver = null;
549
+ });
550
+ if (this._directionality) {
551
+ const sub = this._directionality.change.subscribe((value) => {
552
+ this._cdkDir.set(value);
553
+ });
554
+ this._destroyRef.onDestroy(() => sub.unsubscribe());
555
+ }
556
+ }
557
+ _setupScrollDetection() {
558
+ // Vertical orientation never renders the viewport — nothing to do.
559
+ if (this.orientation() !== 'horizontal')
560
+ return;
561
+ const el = this._viewportRef()?.nativeElement;
562
+ if (!el)
563
+ return;
564
+ this._updateScrollState();
565
+ // ResizeObserver fires when items are appended/removed (the viewport's
566
+ // scrollWidth changes), so a separate items-change effect is not needed.
567
+ if (typeof ResizeObserver !== 'undefined') {
568
+ this._resizeObserver = new ResizeObserver(() => this._updateScrollState());
569
+ this._resizeObserver.observe(el);
570
+ }
571
+ }
572
+ /** @internal Wired to the viewport's `(scroll)` event. */
573
+ _onScroll() {
574
+ this._updateScrollState();
575
+ }
576
+ _updateScrollState() {
577
+ const el = this._viewportRef()?.nativeElement;
578
+ if (!el)
579
+ return;
580
+ // `> 1` thresholds handle sub-pixel scroll positions on retina displays —
581
+ // smooth-scroll landings can settle at scrollLeft = 0.5 and would flicker
582
+ // an enabled/disabled boundary without the guard.
583
+ this._canScrollPrev.set(el.scrollLeft > 1);
584
+ this._canScrollNext.set(el.scrollLeft + el.clientWidth < el.scrollWidth - 1);
585
+ }
586
+ /** Page-scroll amount: 75% of the visible width, leaving ~25% overlap for context. */
587
+ _pageScrollAmount() {
588
+ const el = this._viewportRef()?.nativeElement;
589
+ if (!el)
590
+ return 0;
591
+ return Math.floor(el.clientWidth * 0.75);
592
+ }
593
+ /** @internal Click handler for the prev chevron. */
594
+ _scrollPrev() {
595
+ const el = this._viewportRef()?.nativeElement;
596
+ if (!el)
597
+ return;
598
+ // In RTL "previous" (the visual left button) maps to a positive scrollLeft
599
+ // delta; in LTR it's a negative delta toward 0.
600
+ const direction = this._isRtl() ? 1 : -1;
601
+ el.scrollBy({ left: direction * this._pageScrollAmount(), behavior: 'smooth' });
602
+ }
603
+ /** @internal Click handler for the next chevron. */
604
+ _scrollNext() {
605
+ const el = this._viewportRef()?.nativeElement;
606
+ if (!el)
607
+ return;
608
+ const direction = this._isRtl() ? -1 : 1;
609
+ el.scrollBy({ left: direction * this._pageScrollAmount(), behavior: 'smooth' });
610
+ }
611
+ variantResult = computed(() => timelineVariants({
612
+ orientation: this.orientation(),
613
+ size: this.size(),
614
+ lineStyle: this.lineStyle(),
615
+ }), ...(ngDevMode ? [{ debugName: "variantResult" }] : /* istanbul ignore next */ []));
616
+ rootClasses = computed(() => this.variantResult().root(), ...(ngDevMode ? [{ debugName: "rootClasses" }] : /* istanbul ignore next */ []));
617
+ /** @internal Item slot classes shared with `TimelineItemComponent`. */
618
+ itemSlotClasses = computed(() => this.variantResult().item(), ...(ngDevMode ? [{ debugName: "itemSlotClasses" }] : /* istanbul ignore next */ []));
619
+ markerSideSlotClasses = computed(() => this.variantResult().markerSide(), ...(ngDevMode ? [{ debugName: "markerSideSlotClasses" }] : /* istanbul ignore next */ []));
620
+ markerSlotClasses = computed(() => this.variantResult().marker(), ...(ngDevMode ? [{ debugName: "markerSlotClasses" }] : /* istanbul ignore next */ []));
621
+ connectorLeadingSlotClasses = computed(() => this.variantResult().connectorLeading(), ...(ngDevMode ? [{ debugName: "connectorLeadingSlotClasses" }] : /* istanbul ignore next */ []));
622
+ connectorTrailingSlotClasses = computed(() => this.variantResult().connectorTrailing(), ...(ngDevMode ? [{ debugName: "connectorTrailingSlotClasses" }] : /* istanbul ignore next */ []));
623
+ bodySlotClasses = computed(() => this.variantResult().body(), ...(ngDevMode ? [{ debugName: "bodySlotClasses" }] : /* istanbul ignore next */ []));
624
+ oppositeSlotClasses = computed(() => this.variantResult().opposite(), ...(ngDevMode ? [{ debugName: "oppositeSlotClasses" }] : /* istanbul ignore next */ []));
625
+ numberSlotClasses = computed(() => this.variantResult().number(), ...(ngDevMode ? [{ debugName: "numberSlotClasses" }] : /* istanbul ignore next */ []));
626
+ timestampSlotClasses = computed(() => this.variantResult().timestamp(), ...(ngDevMode ? [{ debugName: "timestampSlotClasses" }] : /* istanbul ignore next */ []));
627
+ /** @internal Variant builder reused by items so per-item state can recompose without rebuilding the base tv() result. */
628
+ itemMarkerVariant = (state, marker) => timelineVariants({
629
+ orientation: this.orientation(),
630
+ size: this.size(),
631
+ lineStyle: this.lineStyle(),
632
+ marker,
633
+ state,
634
+ }).marker();
635
+ /** @internal Variant builder for the auto-number slot (state drives text color). */
636
+ itemNumberVariant = (state) => timelineVariants({
637
+ orientation: this.orientation(),
638
+ size: this.size(),
639
+ lineStyle: this.lineStyle(),
640
+ state,
641
+ }).number();
642
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: TimelineComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
643
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", 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: `
644
+ <ng-template #itemsTpl>
645
+ <ng-content />
646
+ </ng-template>
647
+
648
+ @if (orientation() === 'horizontal') {
649
+ <button
650
+ type="button"
651
+ [class]="prevChevronClasses()"
652
+ [attr.aria-label]="scrollLabels().scrollPrevious"
653
+ [attr.aria-hidden]="_prevButtonHidden() ? 'true' : null"
654
+ [disabled]="_prevButtonDisabled()"
655
+ (click)="_scrollPrev()"
656
+ >
657
+ @if (_isRtl()) {
658
+ <svg class="size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor"
659
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
660
+ <path d="M9 18l6-6-6-6" />
661
+ </svg>
662
+ } @else {
663
+ <svg class="size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor"
664
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
665
+ <path d="M15 18l-6-6 6-6" />
666
+ </svg>
667
+ }
668
+ </button>
669
+ <div
670
+ #scrollViewport
671
+ [class]="horizontalViewportClasses"
672
+ (scroll)="_onScroll()"
673
+ >
674
+ <ng-container [ngTemplateOutlet]="itemsTpl" />
675
+ </div>
676
+ <button
677
+ type="button"
678
+ [class]="nextChevronClasses()"
679
+ [attr.aria-label]="scrollLabels().scrollNext"
680
+ [attr.aria-hidden]="_nextButtonHidden() ? 'true' : null"
681
+ [disabled]="_nextButtonDisabled()"
682
+ (click)="_scrollNext()"
683
+ >
684
+ @if (_isRtl()) {
685
+ <svg class="size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor"
686
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
687
+ <path d="M15 18l-6-6 6-6" />
688
+ </svg>
689
+ } @else {
690
+ <svg class="size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor"
691
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
692
+ <path d="M9 18l6-6-6-6" />
693
+ </svg>
694
+ }
695
+ </button>
696
+ } @else {
697
+ <ng-container [ngTemplateOutlet]="itemsTpl" />
698
+ }
699
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
700
+ }
701
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: TimelineComponent, decorators: [{
702
+ type: Component,
703
+ args: [{
704
+ selector: 'tw-timeline',
705
+ changeDetection: ChangeDetectionStrategy.OnPush,
706
+ imports: [NgTemplateOutlet],
707
+ host: {
708
+ role: 'list',
709
+ '[class]': 'rootClasses()',
710
+ '[attr.aria-orientation]': 'orientation() === "horizontal" ? "horizontal" : null',
711
+ },
712
+ // The projected items live inside a single `<ng-template #itemsTpl>` and are
713
+ // outletted into either the horizontal viewport or the vertical pass-through.
714
+ // Placing two `<ng-content />` slots directly inside `@if/@else` would drop
715
+ // the projection — Angular resolves content projection at component creation
716
+ // and only the first matching slot fills, even if the `@if` flips later.
717
+ // Wrapping in an `<ng-template>` defers the `<ng-content />` materialisation
718
+ // to the outlet's view-instantiation, which works correctly across flips.
719
+ template: `
720
+ <ng-template #itemsTpl>
721
+ <ng-content />
722
+ </ng-template>
723
+
724
+ @if (orientation() === 'horizontal') {
725
+ <button
726
+ type="button"
727
+ [class]="prevChevronClasses()"
728
+ [attr.aria-label]="scrollLabels().scrollPrevious"
729
+ [attr.aria-hidden]="_prevButtonHidden() ? 'true' : null"
730
+ [disabled]="_prevButtonDisabled()"
731
+ (click)="_scrollPrev()"
732
+ >
733
+ @if (_isRtl()) {
734
+ <svg class="size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor"
735
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
736
+ <path d="M9 18l6-6-6-6" />
737
+ </svg>
738
+ } @else {
739
+ <svg class="size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor"
740
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
741
+ <path d="M15 18l-6-6 6-6" />
742
+ </svg>
743
+ }
744
+ </button>
745
+ <div
746
+ #scrollViewport
747
+ [class]="horizontalViewportClasses"
748
+ (scroll)="_onScroll()"
749
+ >
750
+ <ng-container [ngTemplateOutlet]="itemsTpl" />
751
+ </div>
752
+ <button
753
+ type="button"
754
+ [class]="nextChevronClasses()"
755
+ [attr.aria-label]="scrollLabels().scrollNext"
756
+ [attr.aria-hidden]="_nextButtonHidden() ? 'true' : null"
757
+ [disabled]="_nextButtonDisabled()"
758
+ (click)="_scrollNext()"
759
+ >
760
+ @if (_isRtl()) {
761
+ <svg class="size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor"
762
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
763
+ <path d="M15 18l-6-6 6-6" />
764
+ </svg>
765
+ } @else {
766
+ <svg class="size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor"
767
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
768
+ <path d="M9 18l6-6-6-6" />
769
+ </svg>
770
+ }
771
+ </button>
772
+ } @else {
773
+ <ng-container [ngTemplateOutlet]="itemsTpl" />
774
+ }
775
+ `,
776
+ }]
777
+ }], ctorParameters: () => [], propDecorators: { orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }], align: [{ type: i0.Input, args: [{ isSignal: true, alias: "align", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], lineStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "lineStyle", required: false }] }], scrollControls: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollControls", required: false }] }], items: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => TimelineItemComponent), { ...{ descendants: false }, isSignal: true }] }], _viewportRef: [{ type: i0.ViewChild, args: ['scrollViewport', { isSignal: true }] }] } });
778
+ // ── TimelineItemComponent ──
779
+ /**
780
+ * A single event in a `tw-timeline`. Renders a marker (dot or circle) plus
781
+ * content. Connectors leading into and out of the marker are emitted by this
782
+ * component and conditionally elided when the item is first or last in the
783
+ * timeline. The item is not focusable — row activation belongs to projected
784
+ * interactive children.
785
+ */
786
+ class TimelineItemComponent {
787
+ /** Semantic color for the marker fill and the trailing connector when the item is in a reached state. Ignored when state is `'error'` (which forces the error palette). Defaults to `'primary'`. */
788
+ color = input('primary', ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
789
+ /** Marker geometry. `'dot'` is a small filled circle. `'circle'` is a larger ring that may contain a projected icon, avatar, or an auto-computed 1-based index. Defaults to `'dot'`. */
790
+ marker = input('dot', ...(ngDevMode ? [{ debugName: "marker" }] : /* istanbul ignore next */ []));
791
+ /** Semantic state of the event. Drives marker fill, ring, and trailing-connector color, and applies `aria-current="step"` when `'current'`. Defaults to `'reached'`. */
792
+ state = input('reached', ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
793
+ /** Timestamp shown in the timestamp slot. A `Date` is formatted via `Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' })`; a string is rendered verbatim; `null` omits the timestamp element. Overridden by projected `[twTimelineTimestamp]` content when present. Defaults to `null`. */
794
+ timestamp = input(null, ...(ngDevMode ? [{ debugName: "timestamp" }] : /* istanbul ignore next */ []));
795
+ /** Machine-readable ISO 8601 datetime for the rendered `<time datetime="…">` attribute. Derived from `timestamp` when it is a `Date` and this input is `null`; required (explicit) when `timestamp` is a string and machine-readability is desired. When `null` with a string timestamp, the timestamp renders as a `<span>` instead. Defaults to `null`. */
796
+ dateTime = input(null, ...(ngDevMode ? [{ debugName: "dateTime" }] : /* istanbul ignore next */ []));
797
+ /** @internal Direct parent timeline. A `tw-timeline-item` outside a `tw-timeline` is a programmer error — the DI failure throws naturally. */
798
+ timeline = inject(TimelineComponent);
799
+ /** @internal */
800
+ markerSlot = contentChild(TimelineMarkerDirective, ...(ngDevMode ? [{ debugName: "markerSlot" }] : /* istanbul ignore next */ []));
801
+ /** @internal */
802
+ timestampSlot = contentChild(TimelineTimestampDirective, ...(ngDevMode ? [{ debugName: "timestampSlot" }] : /* istanbul ignore next */ []));
803
+ /** @internal */
804
+ oppositeSlot = contentChild(TimelineOppositeDirective, ...(ngDevMode ? [{ debugName: "oppositeSlot" }] : /* istanbul ignore next */ []));
805
+ hasMarkerSlot = computed(() => !!this.markerSlot(), ...(ngDevMode ? [{ debugName: "hasMarkerSlot" }] : /* istanbul ignore next */ []));
806
+ hasTimestampSlot = computed(() => !!this.timestampSlot(), ...(ngDevMode ? [{ debugName: "hasTimestampSlot" }] : /* istanbul ignore next */ []));
807
+ hasOppositeSlot = computed(() => !!this.oppositeSlot(), ...(ngDevMode ? [{ debugName: "hasOppositeSlot" }] : /* istanbul ignore next */ []));
808
+ /** @internal 0-based index in the parent timeline's content. -1 until contentChildren materialises. */
809
+ index = computed(() => this.timeline.items().indexOf(this), ...(ngDevMode ? [{ debugName: "index" }] : /* istanbul ignore next */ []));
810
+ isFirst = computed(() => this.index() === 0, ...(ngDevMode ? [{ debugName: "isFirst" }] : /* istanbul ignore next */ []));
811
+ isLast = computed(() => {
812
+ const idx = this.index();
813
+ if (idx < 0)
814
+ return false;
815
+ return idx === this.timeline.items().length - 1;
816
+ }, ...(ngDevMode ? [{ debugName: "isLast" }] : /* istanbul ignore next */ []));
817
+ /**
818
+ * Leading-connector visibility. In vertical orientation the marker sits at
819
+ * the top of the body and only the trailing segment is rendered. In
820
+ * horizontal orientation the marker must stay centered in its row even at
821
+ * the ends of the timeline, so the leading segment is always rendered — as a
822
+ * coloured line for non-first items, as an invisible spacer for the first.
823
+ */
824
+ renderLeadingConnector = computed(() => this.timeline.orientation() === 'horizontal', ...(ngDevMode ? [{ debugName: "renderLeadingConnector" }] : /* istanbul ignore next */ []));
825
+ /**
826
+ * Trailing-connector visibility. Rendered for every item in horizontal
827
+ * orientation (as a spacer on the last item) so all markers stay centered.
828
+ * In vertical orientation it is suppressed on the last item — the timeline
829
+ * ends with the marker, no trailing segment.
830
+ */
831
+ renderTrailingConnector = computed(() => {
832
+ if (this.timeline.orientation() === 'horizontal')
833
+ return true;
834
+ return !this.isLast();
835
+ }, ...(ngDevMode ? [{ debugName: "renderTrailingConnector" }] : /* istanbul ignore next */ []));
836
+ /** @internal `error` state forces the error palette regardless of the color input. */
837
+ resolvedColor = computed(() => this.state() === 'error' ? 'error' : this.color(), ...(ngDevMode ? [{ debugName: "resolvedColor" }] : /* istanbul ignore next */ []));
838
+ /** @internal Previous item's state (drives the leading connector color so a line between two items reads as one color). */
839
+ previousState = computed(() => {
840
+ const idx = this.index();
841
+ if (idx <= 0)
842
+ return 'reached';
843
+ const prev = this.timeline.items()[idx - 1];
844
+ return prev.state();
845
+ }, ...(ngDevMode ? [{ debugName: "previousState" }] : /* istanbul ignore next */ []));
846
+ /** @internal Previous item's resolved color, for the same reason as `previousState`. */
847
+ previousColor = computed(() => {
848
+ const idx = this.index();
849
+ if (idx <= 0)
850
+ return 'primary';
851
+ const prev = this.timeline.items()[idx - 1];
852
+ return prev.resolvedColor();
853
+ }, ...(ngDevMode ? [{ debugName: "previousColor" }] : /* istanbul ignore next */ []));
854
+ stateLabel = computed(() => {
855
+ switch (this.state()) {
856
+ case 'pending':
857
+ return 'Pending: ';
858
+ case 'current':
859
+ return 'Current: ';
860
+ case 'error':
861
+ return 'Error: ';
862
+ default:
863
+ return null;
864
+ }
865
+ }, ...(ngDevMode ? [{ debugName: "stateLabel" }] : /* istanbul ignore next */ []));
866
+ formattedTimestamp = computed(() => {
867
+ const ts = this.timestamp();
868
+ if (ts === null)
869
+ return '';
870
+ if (ts instanceof Date) {
871
+ return new Intl.DateTimeFormat(undefined, {
872
+ dateStyle: 'medium',
873
+ timeStyle: 'short',
874
+ }).format(ts);
875
+ }
876
+ return ts;
877
+ }, ...(ngDevMode ? [{ debugName: "formattedTimestamp" }] : /* istanbul ignore next */ []));
878
+ resolvedDateTime = computed(() => {
879
+ const dt = this.dateTime();
880
+ if (dt !== null)
881
+ return dt;
882
+ const ts = this.timestamp();
883
+ if (ts instanceof Date)
884
+ return ts.toISOString();
885
+ return null;
886
+ }, ...(ngDevMode ? [{ debugName: "resolvedDateTime" }] : /* istanbul ignore next */ []));
887
+ /** @internal True when the layout needs a third (opposite) column. */
888
+ hasOppositeColumn = computed(() => {
889
+ if (this.timeline.orientation() === 'horizontal')
890
+ return false;
891
+ const align = this.timeline.align();
892
+ return align === 'alternate' || align === 'split';
893
+ }, ...(ngDevMode ? [{ debugName: "hasOppositeColumn" }] : /* istanbul ignore next */ []));
894
+ /** @internal Order of the marker side wrapper. Always center column (2) in three-column layouts; in two-column vertical layouts the order varies. Horizontal layouts use CSS grid placement (`row-start-*`) instead, so flex-order utilities are noise — return ''. */
895
+ markerOrderClass = computed(() => {
896
+ if (this.timeline.orientation() === 'horizontal')
897
+ return '';
898
+ const align = this.timeline.align();
899
+ if (this.hasOppositeColumn())
900
+ return 'order-2';
901
+ if (align === 'right')
902
+ return 'order-2';
903
+ return 'order-1';
904
+ }, ...(ngDevMode ? [{ debugName: "markerOrderClass" }] : /* istanbul ignore next */ []));
905
+ /** @internal Order of the body wrapper. Alternate flips body left/right per item. Horizontal layouts use CSS grid placement (`row-start-*`) instead, so flex-order utilities are noise — return ''. */
906
+ bodyOrderClass = computed(() => {
907
+ if (this.timeline.orientation() === 'horizontal')
908
+ return '';
909
+ const align = this.timeline.align();
910
+ if (align === 'right')
911
+ return 'order-1';
912
+ if (align === 'alternate' && this.index() % 2 === 1)
913
+ return 'order-1';
914
+ if (this.hasOppositeColumn())
915
+ return 'order-3';
916
+ return 'order-2';
917
+ }, ...(ngDevMode ? [{ debugName: "bodyOrderClass" }] : /* istanbul ignore next */ []));
918
+ /** @internal Order of the opposite wrapper. Always left of the marker except on odd-indexed alternate items. */
919
+ oppositeOrderClass = computed(() => {
920
+ const align = this.timeline.align();
921
+ if (align === 'alternate' && this.index() % 2 === 1)
922
+ return 'order-3';
923
+ return 'order-1';
924
+ }, ...(ngDevMode ? [{ debugName: "oppositeOrderClass" }] : /* istanbul ignore next */ []));
925
+ /** @internal Width affordance for the marker side. Vertical needs a fixed-ish column; horizontal needs to span the row. */
926
+ markerSideSizeClass = computed(() => {
927
+ if (this.timeline.orientation() === 'horizontal')
928
+ return '';
929
+ // Pin the column to the marker's own width so siblings stay aligned even
930
+ // when items render slightly different bubble content.
931
+ switch (this.timeline.size()) {
932
+ case 'xs':
933
+ return 'w-6';
934
+ case 'sm':
935
+ return 'w-7';
936
+ case 'md':
937
+ return 'w-8';
938
+ case 'lg':
939
+ return 'w-10';
940
+ case 'xl':
941
+ return 'w-12';
942
+ }
943
+ }, ...(ngDevMode ? [{ debugName: "markerSideSizeClass" }] : /* istanbul ignore next */ []));
944
+ /** @internal Width affordance for the body in alternate/split layouts so the marker stays centered. */
945
+ bodyFlexClass = computed(() => {
946
+ if (this.timeline.orientation() === 'horizontal')
947
+ return '';
948
+ if (this.hasOppositeColumn())
949
+ return 'flex-1';
950
+ return 'flex-1';
951
+ }, ...(ngDevMode ? [{ debugName: "bodyFlexClass" }] : /* istanbul ignore next */ []));
952
+ /** @internal Width affordance for the opposite column (mirrors the body to keep the marker centered). */
953
+ oppositeFlexClass = computed(() => 'flex-1', ...(ngDevMode ? [{ debugName: "oppositeFlexClass" }] : /* istanbul ignore next */ []));
954
+ // ── Composed class outputs ──
955
+ itemClasses = computed(() => this.timeline.itemSlotClasses(), ...(ngDevMode ? [{ debugName: "itemClasses" }] : /* istanbul ignore next */ []));
956
+ markerSideClasses = computed(() => `${this.timeline.markerSideSlotClasses()} ${this.markerSideSizeClass()} ${this.markerOrderClass()}`, ...(ngDevMode ? [{ debugName: "markerSideClasses" }] : /* istanbul ignore next */ []));
957
+ markerClasses = computed(() => `${this.timeline.itemMarkerVariant(this.state(), this.marker())} ${resolveMarkerClasses(this.state(), this.resolvedColor(), this.hasMarkerSlot())}`, ...(ngDevMode ? [{ debugName: "markerClasses" }] : /* istanbul ignore next */ []));
958
+ leadingConnectorClasses = computed(() => {
959
+ const base = this.timeline.connectorLeadingSlotClasses();
960
+ // The first horizontal item gets an invisible spacer — same geometry,
961
+ // no colour fill — so the marker still anchors to the row's centre.
962
+ if (this.isFirst())
963
+ return base;
964
+ return `${base} ${resolveConnectorClasses(this.previousState(), this.previousColor(), this.timeline.lineStyle())}`;
965
+ }, ...(ngDevMode ? [{ debugName: "leadingConnectorClasses" }] : /* istanbul ignore next */ []));
966
+ trailingConnectorClasses = computed(() => {
967
+ const base = this.timeline.connectorTrailingSlotClasses();
968
+ // The last horizontal item also gets an invisible spacer; in vertical
969
+ // orientation the trailing connector is not rendered at all for the last
970
+ // item (the marker sits at the body top, nothing to bridge below).
971
+ if (this.isLast() && this.timeline.orientation() === 'horizontal') {
972
+ return base;
973
+ }
974
+ return `${base} ${resolveConnectorClasses(this.state(), this.resolvedColor(), this.timeline.lineStyle())}`;
975
+ }, ...(ngDevMode ? [{ debugName: "trailingConnectorClasses" }] : /* istanbul ignore next */ []));
976
+ bodyClasses = computed(() => {
977
+ const base = `${this.timeline.bodySlotClasses()} ${this.bodyFlexClass()} ${this.bodyOrderClass()}`;
978
+ // The per-size `pb-*` on the body slot exists only to space adjacent
979
+ // vertical items. Horizontal layouts and the final vertical item don't
980
+ // need it — strip with `!pb-0` so the trailing tone wins (twMerge keeps
981
+ // the last padding-bottom utility and `!` settles tied specificity).
982
+ if (this.timeline.orientation() === 'horizontal') {
983
+ return `${base} !pb-0`;
984
+ }
985
+ if (this.isLast()) {
986
+ return `${base} !pb-0`;
987
+ }
988
+ return base;
989
+ }, ...(ngDevMode ? [{ debugName: "bodyClasses" }] : /* istanbul ignore next */ []));
990
+ oppositeClasses = computed(() => `${this.timeline.oppositeSlotClasses()} ${this.oppositeFlexClass()} ${this.oppositeOrderClass()}`, ...(ngDevMode ? [{ debugName: "oppositeClasses" }] : /* istanbul ignore next */ []));
991
+ numberClasses = computed(() => this.timeline.itemNumberVariant(this.state()), ...(ngDevMode ? [{ debugName: "numberClasses" }] : /* istanbul ignore next */ []));
992
+ timestampClasses = computed(() => this.timeline.timestampSlotClasses(), ...(ngDevMode ? [{ debugName: "timestampClasses" }] : /* istanbul ignore next */ []));
993
+ enterAnimationClass = computed(() => this.timeline.orientation() === 'horizontal'
994
+ ? 'timeline-item-enter-horizontal'
995
+ : 'timeline-item-enter', ...(ngDevMode ? [{ debugName: "enterAnimationClass" }] : /* istanbul ignore next */ []));
996
+ ngAfterContentInit() {
997
+ // One-shot dev warning when a [twTimelineMarker] slot is projected onto a
998
+ // marker="dot" item. Runs in ngAfterContentInit because contentChild signals
999
+ // are not yet populated during ngOnInit. Not an effect — the warning should
1000
+ // fire once at mount, not re-fire on every input change. Production builds
1001
+ // skip the warning entirely because ngDevMode is statically `false`.
1002
+ if (typeof ngDevMode === 'undefined' || ngDevMode) {
1003
+ if (this.marker() === 'dot' && this.hasMarkerSlot()) {
1004
+ console.warn('[tw-timeline-item] [twTimelineMarker] content is ignored when marker="dot". ' +
1005
+ 'Set marker="circle" to render projected marker content.');
1006
+ }
1007
+ }
1008
+ }
1009
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: TimelineItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1010
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: TimelineItemComponent, isStandalone: true, selector: "tw-timeline-item", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, marker: { classPropertyName: "marker", publicName: "marker", isSignal: true, isRequired: false, transformFunction: null }, state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: false, transformFunction: null }, timestamp: { classPropertyName: "timestamp", publicName: "timestamp", isSignal: true, isRequired: false, transformFunction: null }, dateTime: { classPropertyName: "dateTime", publicName: "dateTime", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "listitem" }, properties: { "class": "itemClasses()", "attr.aria-current": "state() === \"current\" ? \"step\" : null", "animate.enter": "enterAnimationClass()" } }, queries: [{ propertyName: "markerSlot", first: true, predicate: TimelineMarkerDirective, descendants: true, isSignal: true }, { propertyName: "timestampSlot", first: true, predicate: TimelineTimestampDirective, descendants: true, isSignal: true }, { propertyName: "oppositeSlot", first: true, predicate: TimelineOppositeDirective, descendants: true, isSignal: true }], ngImport: i0, template: `
1011
+ <div [class]="markerSideClasses()">
1012
+ @if (renderLeadingConnector()) {
1013
+ <span [class]="leadingConnectorClasses()" aria-hidden="true"></span>
1014
+ }
1015
+ <div [class]="markerClasses()" aria-hidden="true">
1016
+ @if (marker() === 'circle') {
1017
+ @if (hasMarkerSlot()) {
1018
+ <ng-content select="[twTimelineMarker]" />
1019
+ } @else if (state() === 'error') {
1020
+ <svg
1021
+ class="size-3/5 shrink-0"
1022
+ viewBox="0 0 20 20"
1023
+ fill="currentColor"
1024
+ aria-hidden="true"
1025
+ >
1026
+ <path
1027
+ fill-rule="evenodd"
1028
+ d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm.75-11.25a.75.75 0 0 0-1.5 0v4.5a.75.75 0 0 0 1.5 0v-4.5ZM10 15a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
1029
+ clip-rule="evenodd"
1030
+ />
1031
+ </svg>
1032
+ } @else {
1033
+ <span [class]="numberClasses()">{{ index() + 1 }}</span>
1034
+ }
1035
+ }
1036
+ </div>
1037
+ @if (renderTrailingConnector()) {
1038
+ <span [class]="trailingConnectorClasses()" aria-hidden="true"></span>
1039
+ }
1040
+ </div>
1041
+ <div [class]="bodyClasses()">
1042
+ @if (stateLabel(); as label) {
1043
+ <span class="sr-only">{{ label }}</span>
1044
+ }
1045
+ <ng-content />
1046
+ @if (hasTimestampSlot()) {
1047
+ <ng-content select="[twTimelineTimestamp]" />
1048
+ } @else if (timestamp() !== null) {
1049
+ @let dt = resolvedDateTime();
1050
+ @if (dt !== null) {
1051
+ <time [attr.datetime]="dt" [class]="timestampClasses()">{{ formattedTimestamp() }}</time>
1052
+ } @else {
1053
+ <span [class]="timestampClasses()">{{ formattedTimestamp() }}</span>
1054
+ }
1055
+ }
1056
+ </div>
1057
+ @if (hasOppositeColumn()) {
1058
+ <div [class]="oppositeClasses()">
1059
+ <ng-content select="[twTimelineOpposite]" />
1060
+ </div>
1061
+ }
1062
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
1063
+ }
1064
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: TimelineItemComponent, decorators: [{
1065
+ type: Component,
1066
+ args: [{
1067
+ selector: 'tw-timeline-item',
1068
+ changeDetection: ChangeDetectionStrategy.OnPush,
1069
+ host: {
1070
+ role: 'listitem',
1071
+ '[class]': 'itemClasses()',
1072
+ '[attr.aria-current]': 'state() === "current" ? "step" : null',
1073
+ '[animate.enter]': 'enterAnimationClass()',
1074
+ },
1075
+ template: `
1076
+ <div [class]="markerSideClasses()">
1077
+ @if (renderLeadingConnector()) {
1078
+ <span [class]="leadingConnectorClasses()" aria-hidden="true"></span>
1079
+ }
1080
+ <div [class]="markerClasses()" aria-hidden="true">
1081
+ @if (marker() === 'circle') {
1082
+ @if (hasMarkerSlot()) {
1083
+ <ng-content select="[twTimelineMarker]" />
1084
+ } @else if (state() === 'error') {
1085
+ <svg
1086
+ class="size-3/5 shrink-0"
1087
+ viewBox="0 0 20 20"
1088
+ fill="currentColor"
1089
+ aria-hidden="true"
1090
+ >
1091
+ <path
1092
+ fill-rule="evenodd"
1093
+ d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm.75-11.25a.75.75 0 0 0-1.5 0v4.5a.75.75 0 0 0 1.5 0v-4.5ZM10 15a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
1094
+ clip-rule="evenodd"
1095
+ />
1096
+ </svg>
1097
+ } @else {
1098
+ <span [class]="numberClasses()">{{ index() + 1 }}</span>
1099
+ }
1100
+ }
1101
+ </div>
1102
+ @if (renderTrailingConnector()) {
1103
+ <span [class]="trailingConnectorClasses()" aria-hidden="true"></span>
1104
+ }
1105
+ </div>
1106
+ <div [class]="bodyClasses()">
1107
+ @if (stateLabel(); as label) {
1108
+ <span class="sr-only">{{ label }}</span>
1109
+ }
1110
+ <ng-content />
1111
+ @if (hasTimestampSlot()) {
1112
+ <ng-content select="[twTimelineTimestamp]" />
1113
+ } @else if (timestamp() !== null) {
1114
+ @let dt = resolvedDateTime();
1115
+ @if (dt !== null) {
1116
+ <time [attr.datetime]="dt" [class]="timestampClasses()">{{ formattedTimestamp() }}</time>
1117
+ } @else {
1118
+ <span [class]="timestampClasses()">{{ formattedTimestamp() }}</span>
1119
+ }
1120
+ }
1121
+ </div>
1122
+ @if (hasOppositeColumn()) {
1123
+ <div [class]="oppositeClasses()">
1124
+ <ng-content select="[twTimelineOpposite]" />
1125
+ </div>
1126
+ }
1127
+ `,
1128
+ }]
1129
+ }], propDecorators: { color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], marker: [{ type: i0.Input, args: [{ isSignal: true, alias: "marker", required: false }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], timestamp: [{ type: i0.Input, args: [{ isSignal: true, alias: "timestamp", required: false }] }], dateTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateTime", required: false }] }], markerSlot: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TimelineMarkerDirective), { isSignal: true }] }], timestampSlot: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TimelineTimestampDirective), { isSignal: true }] }], oppositeSlot: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TimelineOppositeDirective), { isSignal: true }] }] } });
1130
+
1131
+ /**
1132
+ * Generated bundle index. Do not edit.
1133
+ */
1134
+
1135
+ export { DEFAULT_TW_TIMELINE_SCROLL_LABELS, TW_TIMELINE_SCROLL_LABELS, TimelineComponent, TimelineItemComponent, TimelineMarkerDirective, TimelineOppositeDirective, TimelineTimestampDirective, provideTwTimelineScrollLabels };
1136
+ //# sourceMappingURL=cdevhub-ngx-tw-timeline.mjs.map