@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,1454 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, inject, ElementRef, signal, computed, ChangeDetectionStrategy, Component, model, output, DestroyRef, viewChild, contentChildren, afterNextRender, isDevMode, effect, Directive } from '@angular/core';
3
+ import { tv } from 'tailwind-variants';
4
+
5
+ // `CarouselComponent` is the canonical "structural-layout primitive" exception
6
+ // to CLAUDE.md's 5–6 input cap (precedent: `SplitComponent`). The 17 inputs
7
+ // here are each an independent geometry/behavior axis (orientation, slides
8
+ // math, gap, loop, autoplay knobs, drag, keyboard, snap, active index, ARIA
9
+ // label sources, i18n) — removing any would force consumers to coordinate two
10
+ // carousels or replicate logic externally. Documented in
11
+ // docs/requirements/carousel.requirements.md § 2.2 and § 12.
12
+ /** Default English labels used when consumers do not override via the `labels` input. */
13
+ const DEFAULT_CAROUSEL_LABELS = {
14
+ previous: 'Previous slide',
15
+ next: 'Next slide',
16
+ pauseAutoplay: 'Pause autoplay',
17
+ resumeAutoplay: 'Resume autoplay',
18
+ indicator: 'Go to slide {page}',
19
+ slideOfWithLabel: '{index} of {total}: {label}',
20
+ slideOf: '{index} of {total}',
21
+ };
22
+ // ── Helpers ────────────────────────────────────────────────────────
23
+ /** Replaces `{key}` placeholders in `template` with values from `vars`. Ported from paginator. */
24
+ function formatLabel(template, vars) {
25
+ return template.replace(/\{(\w+)\}/g, (match, key) => Object.prototype.hasOwnProperty.call(vars, key) ? String(vars[key]) : match);
26
+ }
27
+ /** Pixel value of the inter-slide gap, used to compute the slide-basis CSS variable. */
28
+ const GAP_PX = {
29
+ xs: 8,
30
+ sm: 12,
31
+ md: 16,
32
+ lg: 24,
33
+ xl: 32,
34
+ };
35
+ // ── Static color × state class lookups ──
36
+ // Tailwind v4 only resolves statically-written class names. Per the stepper
37
+ // precedent, every color × active/inactive indicator combination is enumerated.
38
+ // Active indicators are also distinguishable by more than color (WCAG 1.4.1):
39
+ // `dots` scale 1.5×; `lines` widen to w-12; `numbers` gain a filled background.
40
+ const INDICATOR_ACTIVE_DOTS = {
41
+ primary: 'bg-primary-solid scale-150',
42
+ secondary: 'bg-secondary-solid scale-150',
43
+ accent: 'bg-accent-solid scale-150',
44
+ neutral: 'bg-neutral-solid scale-150',
45
+ info: 'bg-info-solid scale-150',
46
+ success: 'bg-success-solid scale-150',
47
+ warning: 'bg-warning-solid scale-150',
48
+ error: 'bg-error-solid scale-150',
49
+ };
50
+ const INDICATOR_ACTIVE_LINES = {
51
+ primary: 'bg-primary-solid w-12',
52
+ secondary: 'bg-secondary-solid w-12',
53
+ accent: 'bg-accent-solid w-12',
54
+ neutral: 'bg-neutral-solid w-12',
55
+ info: 'bg-info-solid w-12',
56
+ success: 'bg-success-solid w-12',
57
+ warning: 'bg-warning-solid w-12',
58
+ error: 'bg-error-solid w-12',
59
+ };
60
+ const INDICATOR_ACTIVE_NUMBERS = {
61
+ primary: 'bg-primary-solid text-primary-solid-fg',
62
+ secondary: 'bg-secondary-solid text-secondary-solid-fg',
63
+ accent: 'bg-accent-solid text-accent-solid-fg',
64
+ neutral: 'bg-neutral-solid text-neutral-solid-fg',
65
+ info: 'bg-info-solid text-info-solid-fg',
66
+ success: 'bg-success-solid text-success-solid-fg',
67
+ warning: 'bg-warning-solid text-warning-solid-fg',
68
+ error: 'bg-error-solid text-error-solid-fg',
69
+ };
70
+ const INDICATOR_INACTIVE_DOTS = 'bg-fg-muted opacity-50 hover:opacity-100';
71
+ const INDICATOR_INACTIVE_LINES = 'bg-fg-muted opacity-50 hover:opacity-100';
72
+ const INDICATOR_INACTIVE_NUMBERS = 'bg-surface-muted text-fg hover:bg-surface-sunken';
73
+ function resolveIndicatorActiveClasses(variant, color) {
74
+ switch (variant) {
75
+ case 'dots':
76
+ return INDICATOR_ACTIVE_DOTS[color];
77
+ case 'lines':
78
+ return INDICATOR_ACTIVE_LINES[color];
79
+ case 'numbers':
80
+ return INDICATOR_ACTIVE_NUMBERS[color];
81
+ }
82
+ }
83
+ function resolveIndicatorInactiveClasses(variant) {
84
+ switch (variant) {
85
+ case 'dots':
86
+ return INDICATOR_INACTIVE_DOTS;
87
+ case 'lines':
88
+ return INDICATOR_INACTIVE_LINES;
89
+ case 'numbers':
90
+ return INDICATOR_INACTIVE_NUMBERS;
91
+ }
92
+ }
93
+ // ── tv() config ───────────────────────────────────────────────────
94
+ const carouselVariants = tv({
95
+ slots: {
96
+ // `relative` anchors the absolutely-positioned pause control and the
97
+ // overlay-position indicators.
98
+ root: 'relative flex w-full',
99
+ // The scrollable viewport. `tw-scrollbar-none` is the existing utility in
100
+ // theme/_base.css (the requirements doc names this `.tw-scrollbar-hidden`
101
+ // but the codebase ships `.tw-scrollbar-none` — reusing it).
102
+ viewport: 'flex min-w-0 w-full snap-mandatory scroll-smooth motion-reduce:scroll-auto tw-scrollbar-none ' +
103
+ 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
104
+ // Per-slide. `flex-basis` is applied via host style binding on
105
+ // `CarouselSlideComponent` (reads `carousel.slideBasis()`).
106
+ slide: 'flex-none snap-always min-w-0 transition-opacity duration-150 motion-reduce:transition-none',
107
+ // Component-rendered pause control. `bg-overlay-control` /
108
+ // `hover:bg-overlay-control-hover` resolve through
109
+ // `--color-overlay-control{,-hover}` in `theme/_semantic.css`: a fixed
110
+ // translucent dark capsule whose contrast contract is against the
111
+ // *consumer's slide content underneath*, not the surface palette. See
112
+ // requirements § 12.
113
+ pauseControl: 'absolute z-10 inline-flex items-center justify-center size-6 rounded-full bg-overlay-control ' +
114
+ 'text-white hover:bg-overlay-control-hover transition-colors duration-200 motion-reduce:transition-none ' +
115
+ 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
116
+ indicators: 'flex items-center justify-center',
117
+ indicator: 'inline-block transition-colors duration-200 motion-reduce:transition-none cursor-pointer ' +
118
+ 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
119
+ },
120
+ variants: {
121
+ orientation: {
122
+ horizontal: {
123
+ root: 'flex-col',
124
+ viewport: 'flex-row overflow-x-auto overflow-y-hidden snap-x',
125
+ indicators: 'flex-row',
126
+ pauseControl: 'bottom-2 start-2',
127
+ },
128
+ vertical: {
129
+ root: 'flex-row',
130
+ viewport: 'flex-col overflow-y-auto overflow-x-hidden snap-y',
131
+ indicators: 'flex-col',
132
+ pauseControl: 'top-2 start-2',
133
+ },
134
+ },
135
+ snapAlign: {
136
+ start: { slide: 'snap-start' },
137
+ center: { slide: 'snap-center' },
138
+ end: { slide: 'snap-end' },
139
+ },
140
+ gap: {
141
+ xs: {},
142
+ sm: {},
143
+ md: {},
144
+ lg: {},
145
+ xl: {},
146
+ },
147
+ variant: {
148
+ dots: { indicator: 'rounded-full' },
149
+ lines: { indicator: 'rounded-full' },
150
+ numbers: {
151
+ indicator: 'rounded-md inline-flex items-center justify-center font-medium',
152
+ },
153
+ },
154
+ size: {
155
+ xs: {},
156
+ sm: {},
157
+ md: {},
158
+ lg: {},
159
+ xl: {},
160
+ },
161
+ position: {
162
+ below: {},
163
+ overlay: {
164
+ // `bg-overlay-control` capsule — same translucent dark token as the
165
+ // pause control. See the `pauseControl` slot above for the rationale.
166
+ indicators: 'absolute z-10 px-2 py-1 rounded-full bg-overlay-control',
167
+ },
168
+ },
169
+ },
170
+ compoundVariants: [
171
+ // Indicator geometry per variant × size.
172
+ { variant: 'dots', size: 'xs', class: { indicator: 'size-2' } },
173
+ { variant: 'dots', size: 'sm', class: { indicator: 'size-2.5' } },
174
+ { variant: 'dots', size: 'md', class: { indicator: 'size-3' } },
175
+ { variant: 'dots', size: 'lg', class: { indicator: 'size-3' } },
176
+ { variant: 'dots', size: 'xl', class: { indicator: 'size-3' } },
177
+ { variant: 'lines', size: 'xs', class: { indicator: 'h-1 w-4' } },
178
+ { variant: 'lines', size: 'sm', class: { indicator: 'h-1 w-5' } },
179
+ { variant: 'lines', size: 'md', class: { indicator: 'h-1.5 w-6' } },
180
+ { variant: 'lines', size: 'lg', class: { indicator: 'h-1.5 w-8' } },
181
+ { variant: 'lines', size: 'xl', class: { indicator: 'h-2 w-10' } },
182
+ { variant: 'numbers', size: 'xs', class: { indicator: 'size-5 text-2xs' } },
183
+ { variant: 'numbers', size: 'sm', class: { indicator: 'size-6 text-xs' } },
184
+ { variant: 'numbers', size: 'md', class: { indicator: 'size-7 text-xs' } },
185
+ { variant: 'numbers', size: 'lg', class: { indicator: 'size-8 text-sm' } },
186
+ { variant: 'numbers', size: 'xl', class: { indicator: 'size-9 text-sm' } },
187
+ // Indicator gap per size — flex-direction is set by the orientation
188
+ // variant so plain `gap-*` works for both row and column.
189
+ { size: 'xs', class: { indicators: 'gap-1' } },
190
+ { size: 'sm', class: { indicators: 'gap-1.5' } },
191
+ { size: 'md', class: { indicators: 'gap-2' } },
192
+ { size: 'lg', class: { indicators: 'gap-2' } },
193
+ { size: 'xl', class: { indicators: 'gap-3' } },
194
+ // "below" position spacing for indicators relative to the viewport.
195
+ { orientation: 'horizontal', position: 'below', class: { indicators: 'mt-3' } },
196
+ { orientation: 'vertical', position: 'below', class: { indicators: 'ms-3' } },
197
+ // Overlay placement — centered along the axis, offset from the edge.
198
+ {
199
+ orientation: 'horizontal',
200
+ position: 'overlay',
201
+ class: { indicators: 'bottom-3 start-1/2 -translate-x-1/2' },
202
+ },
203
+ {
204
+ orientation: 'vertical',
205
+ position: 'overlay',
206
+ class: { indicators: 'top-1/2 -translate-y-1/2 end-3' },
207
+ },
208
+ // Per-orientation flex gap on the viewport (slide spacing).
209
+ { orientation: 'horizontal', gap: 'xs', class: { viewport: 'gap-x-2' } },
210
+ { orientation: 'horizontal', gap: 'sm', class: { viewport: 'gap-x-3' } },
211
+ { orientation: 'horizontal', gap: 'md', class: { viewport: 'gap-x-4' } },
212
+ { orientation: 'horizontal', gap: 'lg', class: { viewport: 'gap-x-6' } },
213
+ { orientation: 'horizontal', gap: 'xl', class: { viewport: 'gap-x-8' } },
214
+ { orientation: 'vertical', gap: 'xs', class: { viewport: 'gap-y-2' } },
215
+ { orientation: 'vertical', gap: 'sm', class: { viewport: 'gap-y-3' } },
216
+ { orientation: 'vertical', gap: 'md', class: { viewport: 'gap-y-4' } },
217
+ { orientation: 'vertical', gap: 'lg', class: { viewport: 'gap-y-6' } },
218
+ { orientation: 'vertical', gap: 'xl', class: { viewport: 'gap-y-8' } },
219
+ ],
220
+ defaultVariants: {
221
+ orientation: 'horizontal',
222
+ snapAlign: 'start',
223
+ gap: 'md',
224
+ variant: 'dots',
225
+ size: 'md',
226
+ position: 'below',
227
+ },
228
+ }, { twMerge: true });
229
+ // ── CarouselSlideComponent ────────────────────────────────────────
230
+ /**
231
+ * A single slide inside a `<tw-carousel>`. Projects arbitrary content via its
232
+ * default slot. Reports its visibility back to the carousel container via a
233
+ * shared `IntersectionObserver`; hidden slides receive `aria-hidden="true"` and
234
+ * the `inert` attribute so their focusable descendants are removed from the tab
235
+ * order.
236
+ */
237
+ class CarouselSlideComponent {
238
+ /** Optional human-readable label for the slide. When provided, used in the slide's `aria-label` as `"{index + 1} of {total}: {label}"`. When `null`, uses `"{index + 1} of {total}"` only. Defaults to `null`. */
239
+ label = input(null, ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
240
+ /** When `true`, the slide is rendered but skipped by Prev/Next, Indicators, keyboard nav, and autoplay. Programmatic `scrollTo` still lands on it. Visually muted (`opacity-50`, `cursor-not-allowed`). Defaults to `false`. */
241
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
242
+ /** @internal Direct parent carousel. A `tw-carousel-slide` outside a `tw-carousel` is a programmer error — the DI failure throws naturally. */
243
+ carousel = inject(CarouselComponent);
244
+ /** @internal Host element reference used by the carousel's `IntersectionObserver`. */
245
+ _hostEl = inject((ElementRef)).nativeElement;
246
+ /** @internal Visibility flag set by the carousel's IntersectionObserver. Initial value is `true` so the first paint does not flash `inert` on every slide. */
247
+ _isVisible = signal(true, ...(ngDevMode ? [{ debugName: "_isVisible" }] : /* istanbul ignore next */ []));
248
+ _isHidden = computed(() => !this._isVisible(), ...(ngDevMode ? [{ debugName: "_isHidden" }] : /* istanbul ignore next */ []));
249
+ /** @internal 0-based index in the parent carousel's slide content. -1 until contentChildren materialises. */
250
+ index = computed(() => this.carousel.slides().indexOf(this), ...(ngDevMode ? [{ debugName: "index" }] : /* istanbul ignore next */ []));
251
+ /** @internal Resolved `aria-label` per the `slideOf` / `slideOfWithLabel` template. */
252
+ accessibleName = computed(() => {
253
+ const labels = this.carousel.resolvedLabels();
254
+ const idx = this.index();
255
+ const total = this.carousel.slides().length;
256
+ const oneBasedIndex = idx < 0 ? 1 : idx + 1;
257
+ const label = this.label();
258
+ if (label !== null && label !== '') {
259
+ return formatLabel(labels.slideOfWithLabel, {
260
+ index: oneBasedIndex,
261
+ total,
262
+ label,
263
+ });
264
+ }
265
+ return formatLabel(labels.slideOf, {
266
+ index: oneBasedIndex,
267
+ total,
268
+ });
269
+ }, ...(ngDevMode ? [{ debugName: "accessibleName" }] : /* istanbul ignore next */ []));
270
+ slideClasses = computed(() => {
271
+ const base = this.carousel._slotClasses().slide();
272
+ return this.disabled() ? `${base} opacity-50 cursor-not-allowed` : base;
273
+ }, ...(ngDevMode ? [{ debugName: "slideClasses" }] : /* istanbul ignore next */ []));
274
+ /** @internal Per-slide `flex-basis`. Reads from the carousel so each slide sizes along the viewport's main axis (width for horizontal, height for vertical via `flex-direction`). */
275
+ _basis = computed(() => this.carousel.slideBasis(), ...(ngDevMode ? [{ debugName: "_basis" }] : /* istanbul ignore next */ []));
276
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CarouselSlideComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
277
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.14", type: CarouselSlideComponent, isStandalone: true, selector: "tw-carousel-slide", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "group" }, properties: { "attr.aria-roledescription": "\"slide\"", "attr.aria-label": "accessibleName()", "attr.aria-hidden": "_isHidden() ? \"true\" : null", "attr.inert": "_isHidden() ? \"\" : null", "class": "slideClasses()", "style.flex-basis": "_basis()" } }, ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
278
+ }
279
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CarouselSlideComponent, decorators: [{
280
+ type: Component,
281
+ args: [{
282
+ selector: 'tw-carousel-slide',
283
+ changeDetection: ChangeDetectionStrategy.OnPush,
284
+ host: {
285
+ role: 'group',
286
+ '[attr.aria-roledescription]': '"slide"',
287
+ '[attr.aria-label]': 'accessibleName()',
288
+ '[attr.aria-hidden]': '_isHidden() ? "true" : null',
289
+ '[attr.inert]': '_isHidden() ? "" : null',
290
+ '[class]': 'slideClasses()',
291
+ '[style.flex-basis]': '_basis()',
292
+ },
293
+ template: `<ng-content />`,
294
+ }]
295
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }] } });
296
+ // ── CarouselComponent ─────────────────────────────────────────────
297
+ /**
298
+ * Slide / swipe gallery primitive. Renders projected `<tw-carousel-slide>`
299
+ * children in a horizontally (or vertically) scrolling viewport with native
300
+ * CSS scroll-snap, optional autoplay, mouse-pointer drag, keyboard navigation,
301
+ * prev/next directive hosts, and an `<tw-carousel-indicators>` companion.
302
+ * Implements the W3C APG carousel pattern: `role="region"` +
303
+ * `aria-roledescription="carousel"` on the host; the inner viewport carries
304
+ * `tabindex="0"` and a polite `aria-live` region when autoplay is off.
305
+ */
306
+ class CarouselComponent {
307
+ // ── Inputs (17, cap-exempt: structural-layout primitive — precedent SplitComponent) ──
308
+ /** Axis along which slides flow. `'horizontal'` is the canonical case; `'vertical'` is supported for tickers and feature lists. Defaults to `'horizontal'`. */
309
+ orientation = input('horizontal', ...(ngDevMode ? [{ debugName: "orientation" }] : /* istanbul ignore next */ []));
310
+ /** Number of slides visible in the viewport. May be fractional (e.g. `1.2` for a "peek" of the next slide). Values below `0.5` are clamped to `0.5`; values above the slide count are clamped to the slide count. Defaults to `1`. */
311
+ slidesPerView = input(1, ...(ngDevMode ? [{ debugName: "slidesPerView" }] : /* istanbul ignore next */ []));
312
+ /** Number of slides advanced per navigation action (button click, keyboard, indicator). Non-integer values are floored. Defaults to `1`. */
313
+ slidesToScroll = input(1, ...(ngDevMode ? [{ debugName: "slidesToScroll" }] : /* istanbul ignore next */ []));
314
+ /** Inter-slide gap on the scroll axis. Mapped via the canonical spacing scale. Defaults to `'md'`. */
315
+ gap = input('md', ...(ngDevMode ? [{ debugName: "gap" }] : /* istanbul ignore next */ []));
316
+ /** When `true`, navigation wraps around at the boundaries. Prev at slide 0 jumps to the last page; Next at the last page jumps to slide 0. Implementation is jumpless via a brief opacity mask. Defaults to `false`. */
317
+ loop = input(false, ...(ngDevMode ? [{ debugName: "loop" }] : /* istanbul ignore next */ []));
318
+ /** When `true`, the carousel auto-advances by `slidesToScroll` every `autoplayInterval` ms. Pauses on hover, focus-in, drag, document hidden, or user interaction. Defaults to `false`. */
319
+ autoplay = input(false, ...(ngDevMode ? [{ debugName: "autoplay" }] : /* istanbul ignore next */ []));
320
+ /** Milliseconds between autoplay advances. Values below `1000` are clamped to `1000` per WCAG 2.2.2. Defaults to `5000`. */
321
+ autoplayInterval = input(5000, ...(ngDevMode ? [{ debugName: "autoplayInterval" }] : /* istanbul ignore next */ []));
322
+ /** Pauses autoplay while the pointer is over the container. Defaults to `true` because losing autoplay on hover is the expected gallery behavior — opt-out is the special case. */
323
+ pauseOnHover = input(true, ...(ngDevMode ? [{ debugName: "pauseOnHover" }] : /* istanbul ignore next */ []));
324
+ /** Pauses autoplay while keyboard focus is anywhere inside the container. Defaults to `true` for WCAG 2.2.2 compliance — opt-out is the special case. */
325
+ pauseOnFocusIn = input(true, ...(ngDevMode ? [{ debugName: "pauseOnFocusIn" }] : /* istanbul ignore next */ []));
326
+ /** When `true`, the user may pan the slides via mouse pointer drag; touch is left to native scroll. Pointer events are intercepted only when the drag exceeds a 6-pixel threshold so clicks inside slides still work. Defaults to `true` because galleries are draggable by user expectation — opt-out is the special case. */
327
+ draggable = input(true, ...(ngDevMode ? [{ debugName: "draggable" }] : /* istanbul ignore next */ []));
328
+ /** When `true`, the viewport responds to Arrow / Home / End / PageUp / PageDown when focus is inside it. Defaults to `true` for keyboard accessibility — opt-out is the special case. */
329
+ keyboard = input(true, ...(ngDevMode ? [{ debugName: "keyboard" }] : /* istanbul ignore next */ []));
330
+ /** CSS `scroll-snap-align` value applied to each slide. `'start'` is the standard gallery behavior; `'center'` is used for peek/preview layouts. Defaults to `'start'`. */
331
+ snapAlign = input('start', ...(ngDevMode ? [{ debugName: "snapAlign" }] : /* istanbul ignore next */ []));
332
+ /** Two-way bound 0-based index of the first visible slide in the current page. Setting from the parent scrolls the viewport smoothly to align that slide; reading reflects user-driven scroll position after `scrollend`. Defaults to `0`. */
333
+ activeIndex = model(0, ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
334
+ /** Accessible name for the carousel region. If both this and `ariaLabelledBy` are `null`, a one-time dev-mode `console.warn` is logged (production builds never log). Defaults to `null`. */
335
+ ariaLabel = input(null, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
336
+ /** ID of an element labeling the carousel. Either `ariaLabel` or `ariaLabelledBy` SHOULD be provided. Defaults to `null`. */
337
+ ariaLabelledBy = input(null, ...(ngDevMode ? [{ debugName: "ariaLabelledBy" }] : /* istanbul ignore next */ []));
338
+ /** Localizable strings for prev/next/pause/resume/indicator/slide-of templates. Unset keys fall back to the English defaults in `DEFAULT_CAROUSEL_LABELS`. Defaults to `{}`. */
339
+ labels = input({}, ...(ngDevMode ? [{ debugName: "labels" }] : /* istanbul ignore next */ []));
340
+ // ── Outputs ───────────────────────────────────────────────────
341
+ /** Fires when the active index changes. Payload identifies the previous and new index plus the trigger source. */
342
+ slideChange = output();
343
+ /** Fires when autoplay transitions from running to paused. Payload is the pause reason. */
344
+ autoplayPaused = output();
345
+ /** Fires when autoplay transitions from paused to running. */
346
+ autoplayResumed = output();
347
+ // ── Internal references ────────────────────────────────────────
348
+ _destroyRef = inject(DestroyRef);
349
+ /** @internal The scrollable viewport element. */
350
+ _viewportRef = viewChild.required('viewport');
351
+ /** @internal Projected slides in DOM order. */
352
+ slides = contentChildren(CarouselSlideComponent, { ...(ngDevMode ? { debugName: "slides" } : /* istanbul ignore next */ {}), descendants: false });
353
+ // ── Resolved configuration ─────────────────────────────────────
354
+ /** @internal Resolved label record with English defaults filled in. */
355
+ resolvedLabels = computed(() => ({
356
+ ...DEFAULT_CAROUSEL_LABELS,
357
+ ...this.labels(),
358
+ }), ...(ngDevMode ? [{ debugName: "resolvedLabels" }] : /* istanbul ignore next */ []));
359
+ /** @internal Effective `slidesPerView` clamped to `[0.5, slideCount]`. */
360
+ _effectiveSlidesPerView = computed(() => {
361
+ const count = this.slides().length;
362
+ const raw = this.slidesPerView();
363
+ if (count === 0)
364
+ return Math.max(0.5, raw);
365
+ return Math.max(0.5, Math.min(raw, count));
366
+ }, ...(ngDevMode ? [{ debugName: "_effectiveSlidesPerView" }] : /* istanbul ignore next */ []));
367
+ /** @internal Effective `slidesToScroll` floored to a positive integer. Read by child indicators directive (`CarouselIndicatorsComponent._onClick`) as a signal. */
368
+ _effectiveSlidesToScroll = computed(() => {
369
+ return Math.max(1, Math.floor(this.slidesToScroll()));
370
+ }, ...(ngDevMode ? [{ debugName: "_effectiveSlidesToScroll" }] : /* istanbul ignore next */ []));
371
+ /** @internal Autoplay interval clamped to ≥ 1000 ms per WCAG 2.2.2. */
372
+ _effectiveAutoplayInterval = computed(() => {
373
+ return Math.max(1000, this.autoplayInterval());
374
+ }, ...(ngDevMode ? [{ debugName: "_effectiveAutoplayInterval" }] : /* istanbul ignore next */ []));
375
+ /** Number of distinct pages (groups of `slidesToScroll` slides) the carousel can land on. Reactive. */
376
+ pageCount = computed(() => {
377
+ const count = this.slides().length;
378
+ if (count === 0)
379
+ return 1;
380
+ const perView = this._effectiveSlidesPerView();
381
+ const toScroll = this._effectiveSlidesToScroll();
382
+ return Math.max(1, Math.ceil((count - perView) / toScroll) + 1);
383
+ }, ...(ngDevMode ? [{ debugName: "pageCount" }] : /* istanbul ignore next */ []));
384
+ /** 0-based page index that contains `activeIndex`. Reactive. */
385
+ activePage = computed(() => {
386
+ return Math.floor(this.activeIndex() / this._effectiveSlidesToScroll());
387
+ }, ...(ngDevMode ? [{ debugName: "activePage" }] : /* istanbul ignore next */ []));
388
+ /** The highest slide index the carousel can land on given the current `slidesPerView`. Equals `max(0, slideCount - ceil(effectiveSlidesPerView))`. Reactive. Every site that asks "where is the last legal landing position?" goes through this signal. */
389
+ lastReachableIndex = computed(() => {
390
+ const count = this.slides().length;
391
+ if (count === 0)
392
+ return 0;
393
+ return Math.max(0, count - Math.ceil(this._effectiveSlidesPerView()));
394
+ }, ...(ngDevMode ? [{ debugName: "lastReachableIndex" }] : /* istanbul ignore next */ []));
395
+ /** @internal `true` when the carousel is at slide 0 (drives prev directive `disabled`). */
396
+ isAtStart = computed(() => this.activeIndex() <= 0, ...(ngDevMode ? [{ debugName: "isAtStart" }] : /* istanbul ignore next */ []));
397
+ /** @internal `true` when the carousel is at the last reachable slide (drives next directive `disabled`). */
398
+ isAtEnd = computed(() => this.activeIndex() >= this.lastReachableIndex(), ...(ngDevMode ? [{ debugName: "isAtEnd" }] : /* istanbul ignore next */ []));
399
+ /** @internal Per-slide `flex-basis`. Read by each `CarouselSlideComponent` host style binding to size slides along the viewport's main axis. Reactive. */
400
+ slideBasis = computed(() => {
401
+ const perView = this._effectiveSlidesPerView();
402
+ const gapPx = GAP_PX[this.gap()];
403
+ // calc((100% - ((perView - 1) * gap-px)) / perView)
404
+ return `calc((100% - ${(perView - 1) * gapPx}px) / ${perView})`;
405
+ }, ...(ngDevMode ? [{ debugName: "slideBasis" }] : /* istanbul ignore next */ []));
406
+ // ── tv() resolution ────────────────────────────────────────────
407
+ _variantResult = computed(() => carouselVariants({
408
+ orientation: this.orientation(),
409
+ snapAlign: this.snapAlign(),
410
+ gap: this.gap(),
411
+ }), ...(ngDevMode ? [{ debugName: "_variantResult" }] : /* istanbul ignore next */ []));
412
+ /** @internal Slot-class accessor used by child slides and indicators. */
413
+ _slotClasses = computed(() => this._variantResult(), ...(ngDevMode ? [{ debugName: "_slotClasses" }] : /* istanbul ignore next */ []));
414
+ rootClasses = computed(() => this._variantResult().root(), ...(ngDevMode ? [{ debugName: "rootClasses" }] : /* istanbul ignore next */ []));
415
+ viewportClasses = computed(() => this._variantResult().viewport(), ...(ngDevMode ? [{ debugName: "viewportClasses" }] : /* istanbul ignore next */ []));
416
+ pauseControlClasses = computed(() => this._variantResult().pauseControl(), ...(ngDevMode ? [{ debugName: "pauseControlClasses" }] : /* istanbul ignore next */ []));
417
+ // ── State signals ──────────────────────────────────────────────
418
+ /** @internal Last interaction source — written before any navigation triggers scroll, read by the settle handler. */
419
+ _lastInteractionSource = signal(null, ...(ngDevMode ? [{ debugName: "_lastInteractionSource" }] : /* istanbul ignore next */ []));
420
+ /** @internal Suppresses the scroll-end recompute during a loop-jump. */
421
+ _isLoopJumping = signal(false, ...(ngDevMode ? [{ debugName: "_isLoopJumping" }] : /* istanbul ignore next */ []));
422
+ _isHovered = signal(false, ...(ngDevMode ? [{ debugName: "_isHovered" }] : /* istanbul ignore next */ []));
423
+ _hasFocusInside = signal(false, ...(ngDevMode ? [{ debugName: "_hasFocusInside" }] : /* istanbul ignore next */ []));
424
+ _isTabVisible = signal(true, ...(ngDevMode ? [{ debugName: "_isTabVisible" }] : /* istanbul ignore next */ []));
425
+ _isDragging = signal(false, ...(ngDevMode ? [{ debugName: "_isDragging" }] : /* istanbul ignore next */ []));
426
+ _isManuallyPaused = signal(false, ...(ngDevMode ? [{ debugName: "_isManuallyPaused" }] : /* istanbul ignore next */ []));
427
+ _postInteractionPauseUntil = signal(null, ...(ngDevMode ? [{ debugName: "_postInteractionPauseUntil" }] : /* istanbul ignore next */ []));
428
+ /** @internal RTL sign factor applied to scroll-position math. `+1` LTR, `-1` RTL. */
429
+ _rtlSign = signal(1, ...(ngDevMode ? [{ debugName: "_rtlSign" }] : /* istanbul ignore next */ []));
430
+ // ── Resolved pause-control labels ──────────────────────────────
431
+ _pauseControlAriaLabel = computed(() => {
432
+ const labels = this.resolvedLabels();
433
+ return this._isManuallyPaused() ? labels.resumeAutoplay : labels.pauseAutoplay;
434
+ }, ...(ngDevMode ? [{ debugName: "_pauseControlAriaLabel" }] : /* istanbul ignore next */ []));
435
+ /** @internal Public accessor for the template. */
436
+ _isManuallyPausedView = this._isManuallyPaused.asReadonly();
437
+ // ── Internal API used by child directives / indicators ─────────
438
+ /**
439
+ * @internal Sets the "last interaction source" signal so the upcoming
440
+ * navigation call emits `slideChange` with the correct trigger. Called by
441
+ * `[twCarouselPrev]`, `[twCarouselNext]`, and the indicators before they
442
+ * invoke `next()` / `prev()` / `scrollTo()`.
443
+ */
444
+ _setLastInteractionSource(source) {
445
+ this._lastInteractionSource.set(source);
446
+ }
447
+ // ── Lifecycle ──────────────────────────────────────────────────
448
+ constructor() {
449
+ // One-time dev warnings.
450
+ afterNextRender(() => {
451
+ this._setupViewport();
452
+ });
453
+ // Dev-mode warnings. Each one is deduped per component instance so they
454
+ // re-evaluate when consumer inputs change but never spam the console. The
455
+ // earlier implementation used `afterNextRender` which fires once at
456
+ // construction — if a consumer later set `slidesPerView = 0.1`, the
457
+ // threshold warning never surfaced (audit (i)).
458
+ if (isDevMode()) {
459
+ let warnedMissingLabel = false;
460
+ let warnedLowInterval = false;
461
+ let warnedLowPerView = false;
462
+ effect(() => {
463
+ if (!warnedMissingLabel &&
464
+ this.ariaLabel() === null &&
465
+ this.ariaLabelledBy() === null) {
466
+ warnedMissingLabel = true;
467
+ console.warn('[tw-carousel] Provide either `ariaLabel` or `ariaLabelledBy` so the carousel region has an accessible name (WCAG 4.1.2).');
468
+ }
469
+ if (!warnedLowInterval && this.autoplayInterval() < 1000) {
470
+ warnedLowInterval = true;
471
+ console.warn(`[tw-carousel] autoplayInterval=${this.autoplayInterval()} is below the 1000 ms WCAG 2.2.2 floor; clamped to 1000.`);
472
+ }
473
+ if (!warnedLowPerView && this.slidesPerView() < 0.5) {
474
+ warnedLowPerView = true;
475
+ console.warn(`[tw-carousel] slidesPerView=${this.slidesPerView()} is below the 0.5 floor; clamped.`);
476
+ }
477
+ });
478
+ }
479
+ // Autoplay state machine — start/stop the interval based on inputs + pause state.
480
+ effect((onCleanup) => {
481
+ const autoplay = this.autoplay();
482
+ const paused = this._isPaused();
483
+ const interval = this._effectiveAutoplayInterval();
484
+ if (!autoplay || paused)
485
+ return;
486
+ const handle = setInterval(() => {
487
+ this._lastInteractionSource.set('autoplay');
488
+ this.next();
489
+ }, interval);
490
+ onCleanup(() => clearInterval(handle));
491
+ });
492
+ // Pause/resume transition emissions. Track previous state across effect
493
+ // invocations and emit on actual transitions while autoplay is on.
494
+ // The initial `false` is correct ONLY because `autoplay` defaults to
495
+ // `false`: the first effect run sees `autoplay=false` and short-circuits
496
+ // without emitting, regardless of the pause state. If the default ever
497
+ // flips to `true`, this needs rethinking (audit (f)).
498
+ let prevPaused = false;
499
+ effect(() => {
500
+ const autoplay = this.autoplay();
501
+ const paused = this._isPaused();
502
+ if (autoplay) {
503
+ if (paused && !prevPaused) {
504
+ const reason = this._currentPauseReason();
505
+ if (reason !== null)
506
+ this.autoplayPaused.emit(reason);
507
+ }
508
+ else if (!paused && prevPaused) {
509
+ this.autoplayResumed.emit();
510
+ }
511
+ }
512
+ prevPaused = paused;
513
+ });
514
+ // Slide-set changes: re-observe with IntersectionObserver, clamp activeIndex.
515
+ effect(() => {
516
+ const slides = this.slides();
517
+ this._reobserveSlides(slides);
518
+ const count = slides.length;
519
+ if (count > 0 && this.activeIndex() > count - 1) {
520
+ this.activeIndex.set(count - 1);
521
+ }
522
+ });
523
+ this._destroyRef.onDestroy(() => {
524
+ this._teardownViewport();
525
+ });
526
+ }
527
+ // ── Pause state aggregation ────────────────────────────────────
528
+ _isPaused = computed(() => {
529
+ if (this.pauseOnHover() && this._isHovered())
530
+ return true;
531
+ if (this.pauseOnFocusIn() && this._hasFocusInside())
532
+ return true;
533
+ if (!this._isTabVisible())
534
+ return true;
535
+ if (this._isDragging())
536
+ return true;
537
+ if (this._isManuallyPaused())
538
+ return true;
539
+ const until = this._postInteractionPauseUntil();
540
+ if (until !== null && Date.now() < until)
541
+ return true;
542
+ return false;
543
+ }, ...(ngDevMode ? [{ debugName: "_isPaused" }] : /* istanbul ignore next */ []));
544
+ _currentPauseReason() {
545
+ if (this._isManuallyPaused())
546
+ return 'manual';
547
+ if (!this._isTabVisible())
548
+ return 'visibility';
549
+ if (this._isDragging())
550
+ return 'interaction';
551
+ const until = this._postInteractionPauseUntil();
552
+ if (until !== null && Date.now() < until)
553
+ return 'interaction';
554
+ if (this.pauseOnFocusIn() && this._hasFocusInside())
555
+ return 'focus';
556
+ if (this.pauseOnHover() && this._isHovered())
557
+ return 'hover';
558
+ return null;
559
+ }
560
+ // ── Viewport / observer setup ───────────────────────────────────
561
+ _intersectionObserver = null;
562
+ _resizeObserver = null;
563
+ _onScrollEndBound = null;
564
+ _onScrollDebouncedBound = null;
565
+ _scrollDebounceTimer = null;
566
+ _onVisibilityChangeBound = null;
567
+ _setupViewport() {
568
+ const viewport = this._viewportRef().nativeElement;
569
+ // RTL detection (read once; consumers needing dynamic dir changes can
570
+ // re-render the host).
571
+ try {
572
+ const direction = getComputedStyle(viewport).direction;
573
+ this._rtlSign.set(direction === 'rtl' ? -1 : 1);
574
+ }
575
+ catch {
576
+ this._rtlSign.set(1);
577
+ }
578
+ // Active-index detection: scrollend if available, debounced scroll fallback.
579
+ const hasScrollEnd = 'onscrollend' in window;
580
+ if (hasScrollEnd) {
581
+ this._onScrollEndBound = () => this._handleScrollSettled();
582
+ viewport.addEventListener('scrollend', this._onScrollEndBound, { passive: true });
583
+ }
584
+ else {
585
+ this._onScrollDebouncedBound = () => {
586
+ if (this._scrollDebounceTimer !== null) {
587
+ clearTimeout(this._scrollDebounceTimer);
588
+ }
589
+ this._scrollDebounceTimer = setTimeout(() => {
590
+ this._scrollDebounceTimer = null;
591
+ this._handleScrollSettled();
592
+ }, 150);
593
+ };
594
+ viewport.addEventListener('scroll', this._onScrollDebouncedBound, {
595
+ passive: true,
596
+ });
597
+ }
598
+ // Tab visibility for autoplay gating.
599
+ if (typeof document !== 'undefined') {
600
+ this._isTabVisible.set(document.visibilityState !== 'hidden');
601
+ this._onVisibilityChangeBound = () => {
602
+ this._isTabVisible.set(document.visibilityState !== 'hidden');
603
+ };
604
+ document.addEventListener('visibilitychange', this._onVisibilityChangeBound, {
605
+ passive: true,
606
+ });
607
+ }
608
+ // Observers for slide visibility and viewport-size changes.
609
+ if (typeof IntersectionObserver !== 'undefined') {
610
+ this._intersectionObserver = new IntersectionObserver((entries) => {
611
+ for (const entry of entries) {
612
+ const slide = entry.target
613
+ ._twSlide;
614
+ if (slide) {
615
+ slide._isVisible.set(entry.isIntersecting && entry.intersectionRatio >= 0.5);
616
+ }
617
+ }
618
+ }, { root: viewport, threshold: 0.5 });
619
+ this._reobserveSlides(this.slides());
620
+ }
621
+ if (typeof ResizeObserver !== 'undefined') {
622
+ this._resizeObserver = new ResizeObserver(() => {
623
+ // Reflow: keep activeIndex aligned to its slide position.
624
+ this._scrollToIndex(this.activeIndex(), 'instant');
625
+ });
626
+ this._resizeObserver.observe(viewport);
627
+ }
628
+ }
629
+ _teardownViewport() {
630
+ const viewport = this._viewportRef()?.nativeElement;
631
+ if (viewport && this._onScrollEndBound) {
632
+ viewport.removeEventListener('scrollend', this._onScrollEndBound);
633
+ }
634
+ if (viewport && this._onScrollDebouncedBound) {
635
+ viewport.removeEventListener('scroll', this._onScrollDebouncedBound);
636
+ }
637
+ if (this._scrollDebounceTimer !== null) {
638
+ clearTimeout(this._scrollDebounceTimer);
639
+ this._scrollDebounceTimer = null;
640
+ }
641
+ if (typeof document !== 'undefined' && this._onVisibilityChangeBound) {
642
+ document.removeEventListener('visibilitychange', this._onVisibilityChangeBound);
643
+ }
644
+ this._intersectionObserver?.disconnect();
645
+ this._intersectionObserver = null;
646
+ this._resizeObserver?.disconnect();
647
+ this._resizeObserver = null;
648
+ if (this._loopJumpHandle !== null) {
649
+ clearTimeout(this._loopJumpHandle);
650
+ this._loopJumpHandle = null;
651
+ }
652
+ if (this._loopJumpSafetyHandle !== null) {
653
+ clearTimeout(this._loopJumpSafetyHandle);
654
+ this._loopJumpSafetyHandle = null;
655
+ }
656
+ if (this._dragCleanup !== null) {
657
+ this._dragCleanup();
658
+ this._dragCleanup = null;
659
+ }
660
+ }
661
+ _reobserveSlides(slides) {
662
+ const observer = this._intersectionObserver;
663
+ if (!observer)
664
+ return;
665
+ observer.disconnect();
666
+ for (const slide of slides) {
667
+ // Locate the slide host element via document inspection. The host element
668
+ // is the first ancestor with the `tw-carousel-slide` tag — but with
669
+ // `contentChildren` we don't have direct ElementRef access on each slide.
670
+ // Use the public `_hostEl` set in the slide constructor.
671
+ const el = slide._hostEl;
672
+ if (el) {
673
+ el._twSlide = slide;
674
+ observer.observe(el);
675
+ }
676
+ }
677
+ }
678
+ // ── Scroll/active-index logic ───────────────────────────────────
679
+ // The scroll-math contract: both reads (`_handleScrollSettled`) and writes
680
+ // (`_scrollToIndex`) consult the actually-rendered slide geometry via each
681
+ // slide host's `offsetLeft` / `offsetTop`. The synthetic
682
+ // `step = (dimension + gap) / perView` from earlier revisions has been
683
+ // removed — fractional `slidesPerView`, container resize, or sub-pixel gap
684
+ // rounding could cause that step to land between snap points and the
685
+ // browser would snap back to the previously-visible tile. Offsets are
686
+ // authoritative.
687
+ //
688
+ // RTL: in modern browsers (negative-scroll model) `viewport.scrollLeft` is
689
+ // 0 at the logical start and decreases toward `-(scrollWidth - clientWidth)`
690
+ // as the user scrolls toward the logical end. Slide DOM order is preserved
691
+ // but layout is mirrored: `slide[0].offsetLeft` is the largest, not zero.
692
+ // We normalize by transforming each slide's offset into a "logical offset"
693
+ // that increases monotonically along the scroll axis, and read the current
694
+ // scroll as `Math.abs(scrollLeft)`. `_rtlSign()` is applied **at most once**
695
+ // — on writes (`viewport.scrollTo({ left: -logicalOffset })`).
696
+ _logicalOffset(slideEl) {
697
+ const viewport = this._viewportRef().nativeElement;
698
+ if (this.orientation() === 'horizontal') {
699
+ const isRtl = this._rtlSign() === -1;
700
+ return isRtl
701
+ ? viewport.scrollWidth - slideEl.offsetLeft - slideEl.offsetWidth
702
+ : slideEl.offsetLeft;
703
+ }
704
+ return slideEl.offsetTop;
705
+ }
706
+ _currentLogicalScroll() {
707
+ const viewport = this._viewportRef().nativeElement;
708
+ if (this.orientation() === 'horizontal') {
709
+ // `Math.abs` is the single sign normalization on reads. Works for both
710
+ // LTR (scrollLeft ≥ 0) and RTL negative-scroll (scrollLeft ≤ 0).
711
+ return Math.abs(viewport.scrollLeft);
712
+ }
713
+ return viewport.scrollTop;
714
+ }
715
+ _findClosestSlideIndex(targetLogicalScroll) {
716
+ const slides = this.slides();
717
+ if (slides.length === 0)
718
+ return 0;
719
+ let bestIndex = 0;
720
+ let bestDistance = Number.POSITIVE_INFINITY;
721
+ for (let i = 0; i < slides.length; i++) {
722
+ const el = slides[i]._hostEl;
723
+ if (!el)
724
+ continue;
725
+ const distance = Math.abs(this._logicalOffset(el) - targetLogicalScroll);
726
+ if (distance < bestDistance) {
727
+ bestDistance = distance;
728
+ bestIndex = i;
729
+ }
730
+ }
731
+ return bestIndex;
732
+ }
733
+ _handleScrollSettled() {
734
+ if (this._isLoopJumping())
735
+ return;
736
+ if (this.slides().length === 0) {
737
+ this._lastInteractionSource.set(null);
738
+ return;
739
+ }
740
+ const closest = this._findClosestSlideIndex(this._currentLogicalScroll());
741
+ const clamped = Math.max(0, Math.min(closest, this.lastReachableIndex()));
742
+ const from = this.activeIndex();
743
+ if (clamped !== from) {
744
+ this._emitSlideChange(from, clamped, this._lastInteractionSource() ?? 'pointer');
745
+ this.activeIndex.set(clamped);
746
+ }
747
+ this._lastInteractionSource.set(null);
748
+ }
749
+ _emitSlideChange(from, to, trigger) {
750
+ this.slideChange.emit({ from, to, trigger });
751
+ }
752
+ // ── Navigation methods (imperative API) ─────────────────────────
753
+ /** Advance by `slidesToScroll`. Wraps if `loop` is `true`; no-op at the last page when `loop` is `false`. Emits `slideChange` with trigger `'programmatic'` when called externally. */
754
+ next() {
755
+ const trigger = this._lastInteractionSource() ?? 'programmatic';
756
+ this._lastInteractionSource.set(trigger);
757
+ const count = this.slides().length;
758
+ if (count === 0)
759
+ return;
760
+ const toScroll = this._effectiveSlidesToScroll();
761
+ const lastReachable = this.lastReachableIndex();
762
+ let target = this._findNextEnabled(this.activeIndex() + toScroll, +1);
763
+ if (target > lastReachable) {
764
+ // Past the last reachable index.
765
+ if (this.loop()) {
766
+ this._loopJumpTo(0);
767
+ return;
768
+ }
769
+ target = lastReachable;
770
+ if (target === this.activeIndex()) {
771
+ this._lastInteractionSource.set(null);
772
+ return;
773
+ }
774
+ }
775
+ this._scrollToIndex(target);
776
+ }
777
+ /** Retreat by `slidesToScroll`. Wraps if `loop` is `true`; no-op at slide 0 when `loop` is `false`. Emits `slideChange` with trigger `'programmatic'` when called externally. */
778
+ prev() {
779
+ const trigger = this._lastInteractionSource() ?? 'programmatic';
780
+ this._lastInteractionSource.set(trigger);
781
+ const count = this.slides().length;
782
+ if (count === 0)
783
+ return;
784
+ const toScroll = this._effectiveSlidesToScroll();
785
+ let target = this._findNextEnabled(this.activeIndex() - toScroll, -1);
786
+ if (target < 0) {
787
+ if (this.loop()) {
788
+ this._loopJumpTo(this.lastReachableIndex());
789
+ return;
790
+ }
791
+ target = 0;
792
+ if (target === this.activeIndex()) {
793
+ this._lastInteractionSource.set(null);
794
+ return;
795
+ }
796
+ }
797
+ this._scrollToIndex(target);
798
+ }
799
+ /** Jump to a specific 0-based slide index. `opts.behavior` is `'smooth' | 'instant'`; default is `'smooth'` unless `prefers-reduced-motion: reduce` is set, in which case the default is `'instant'`. */
800
+ scrollTo(index, opts) {
801
+ const trigger = this._lastInteractionSource() ?? 'programmatic';
802
+ this._lastInteractionSource.set(trigger);
803
+ this._scrollToIndex(index, this._resolveBehavior(opts));
804
+ }
805
+ /** Pause autoplay. `reason` defaults to `'manual'`. */
806
+ pause(_reason = 'manual') {
807
+ this._isManuallyPaused.set(true);
808
+ }
809
+ /** Resume autoplay if `autoplay` input is `true`. */
810
+ resume() {
811
+ this._isManuallyPaused.set(false);
812
+ }
813
+ _findNextEnabled(start, direction) {
814
+ const slides = this.slides();
815
+ const count = slides.length;
816
+ if (count === 0)
817
+ return 0;
818
+ let idx = start;
819
+ while (idx >= 0 && idx < count) {
820
+ if (!slides[idx].disabled())
821
+ return idx;
822
+ idx += direction;
823
+ }
824
+ return start;
825
+ }
826
+ _scrollToIndex(index, behavior = 'smooth') {
827
+ const viewport = this._viewportRef()?.nativeElement;
828
+ if (!viewport)
829
+ return;
830
+ const slides = this.slides();
831
+ const count = slides.length;
832
+ if (count === 0)
833
+ return;
834
+ const clamped = Math.max(0, Math.min(index, count - 1));
835
+ const from = this.activeIndex();
836
+ const targetEl = slides[clamped]
837
+ ._hostEl;
838
+ const horizontal = this.orientation() === 'horizontal';
839
+ // Map our public `'instant'` to the DOM-standard `'auto'` literal.
840
+ const domBehavior = behavior === 'instant' ? 'auto' : 'smooth';
841
+ if (targetEl) {
842
+ const logicalOffset = this._logicalOffset(targetEl);
843
+ if (typeof viewport.scrollTo === 'function') {
844
+ // RTL writes apply `_rtlSign()` exactly once — no double-multiplication
845
+ // anywhere on the read/write paths (see audit (a)).
846
+ const opts = horizontal
847
+ ? { left: logicalOffset * this._rtlSign(), behavior: domBehavior }
848
+ : { top: logicalOffset, behavior: domBehavior };
849
+ viewport.scrollTo(opts);
850
+ }
851
+ else if (horizontal) {
852
+ viewport.scrollLeft = logicalOffset * this._rtlSign();
853
+ }
854
+ else {
855
+ viewport.scrollTop = logicalOffset;
856
+ }
857
+ }
858
+ if (clamped !== from) {
859
+ this._emitSlideChange(from, clamped, this._lastInteractionSource() ?? 'programmatic');
860
+ this.activeIndex.set(clamped);
861
+ }
862
+ // Clear the source — `_handleScrollSettled` will not re-emit because the
863
+ // index already matches.
864
+ this._lastInteractionSource.set(null);
865
+ }
866
+ /**
867
+ * @internal Resolves the effective scroll behavior. Explicit caller-supplied
868
+ * values are honoured verbatim; otherwise checks
869
+ * `matchMedia('(prefers-reduced-motion: reduce)')` and returns `'instant'`
870
+ * when the user has reduced-motion enabled, `'smooth'` otherwise. See B(d).
871
+ */
872
+ _resolveBehavior(opts) {
873
+ if (opts?.behavior)
874
+ return opts.behavior;
875
+ if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {
876
+ try {
877
+ if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
878
+ return 'instant';
879
+ }
880
+ }
881
+ catch {
882
+ // matchMedia may throw in non-browser environments; fall through.
883
+ }
884
+ }
885
+ return 'smooth';
886
+ }
887
+ _loopJumpHandle = null;
888
+ _loopJumpSafetyHandle = null;
889
+ _loopJumpTo(index) {
890
+ this._isLoopJumping.set(true);
891
+ const viewport = this._viewportRef().nativeElement;
892
+ viewport.classList.add('tw-carousel-loop-jump');
893
+ // If a previous loop-jump is still pending, drop its timers — the new
894
+ // jump supersedes it.
895
+ if (this._loopJumpHandle !== null)
896
+ clearTimeout(this._loopJumpHandle);
897
+ if (this._loopJumpSafetyHandle !== null)
898
+ clearTimeout(this._loopJumpSafetyHandle);
899
+ // Sequencing the opacity mask. The keyframe in `theme/_base.css`
900
+ // (`tw-carousel-loop-jump`) runs 0% → 40% → 100% over 200ms with the
901
+ // opacity nadir (0.55) at 40% — i.e. 80ms in. We defer the instant scroll
902
+ // until ~80ms after class-add so the swap happens at peak occlusion,
903
+ // *under* the mask, rather than before the dip starts (audit (e)). Under
904
+ // `prefers-reduced-motion: reduce`, `_base.css:361` zeroes the animation
905
+ // duration; the scroll still fires at +80ms, which is a negligible delay
906
+ // and avoids a code branch.
907
+ this._loopJumpHandle = setTimeout(() => {
908
+ this._loopJumpHandle = null;
909
+ // _scrollToIndex handles the slideChange emission and the activeIndex
910
+ // update; we just need the loop-jump flag set so the scrollend
911
+ // recompute does not re-emit.
912
+ this._scrollToIndex(index, 'instant');
913
+ }, 80);
914
+ const cleanup = () => {
915
+ viewport.classList.remove('tw-carousel-loop-jump');
916
+ this._isLoopJumping.set(false);
917
+ viewport.removeEventListener('animationend', cleanup);
918
+ if (this._loopJumpSafetyHandle !== null) {
919
+ clearTimeout(this._loopJumpSafetyHandle);
920
+ this._loopJumpSafetyHandle = null;
921
+ }
922
+ };
923
+ viewport.addEventListener('animationend', cleanup, { once: true });
924
+ // Safety net — if the animation never fires (reduced-motion override,
925
+ // jsdom environment), clear after the keyframe duration.
926
+ this._loopJumpSafetyHandle = setTimeout(cleanup, 220);
927
+ }
928
+ // ── Host event handlers ─────────────────────────────────────────
929
+ _onPointerEnter() {
930
+ this._isHovered.set(true);
931
+ }
932
+ _onPointerLeave() {
933
+ this._isHovered.set(false);
934
+ }
935
+ _onFocusIn() {
936
+ this._hasFocusInside.set(true);
937
+ }
938
+ _onFocusOut(event) {
939
+ const root = event.currentTarget ?? null;
940
+ const next = event.relatedTarget;
941
+ if (root && next && root.contains(next)) {
942
+ return;
943
+ }
944
+ this._hasFocusInside.set(false);
945
+ }
946
+ // ── Pointer drag handler ───────────────────────────────────────
947
+ _dragStart = null;
948
+ // Per-drag cleanup closure. Set in `_onPointerDown`, invoked from
949
+ // `_onPointerUp` AND from `_teardownViewport` (the DestroyRef hook) — that
950
+ // covers the destroy-mid-drag race where the component is torn down before
951
+ // the `pointerup`/`pointercancel` listeners fire (audit (g)). The closure
952
+ // nulls the field itself so a second invocation is a no-op.
953
+ _dragCleanup = null;
954
+ _onPointerDown(event) {
955
+ if (!this.draggable())
956
+ return;
957
+ if (!event.isPrimary || event.pointerType === 'touch')
958
+ return;
959
+ const viewport = this._viewportRef().nativeElement;
960
+ this._dragStart = {
961
+ x: event.clientX,
962
+ y: event.clientY,
963
+ scrollLeft: viewport.scrollLeft,
964
+ scrollTop: viewport.scrollTop,
965
+ pointerId: event.pointerId,
966
+ };
967
+ const moveHandler = (e) => this._onPointerMove(e);
968
+ const upHandler = (e) => {
969
+ // Invoke the unified cleanup so the destroy path and the natural-up
970
+ // path go through the same code.
971
+ if (this._dragCleanup !== null)
972
+ this._dragCleanup();
973
+ this._onPointerUp(e);
974
+ };
975
+ window.addEventListener('pointermove', moveHandler);
976
+ window.addEventListener('pointerup', upHandler);
977
+ window.addEventListener('pointercancel', upHandler);
978
+ this._dragCleanup = () => {
979
+ window.removeEventListener('pointermove', moveHandler);
980
+ window.removeEventListener('pointerup', upHandler);
981
+ window.removeEventListener('pointercancel', upHandler);
982
+ this._dragCleanup = null;
983
+ };
984
+ }
985
+ _onPointerMove(event) {
986
+ const start = this._dragStart;
987
+ if (!start)
988
+ return;
989
+ const horizontal = this.orientation() === 'horizontal';
990
+ const delta = horizontal ? event.clientX - start.x : event.clientY - start.y;
991
+ if (!this._isDragging() && Math.abs(delta) <= 6)
992
+ return;
993
+ const viewport = this._viewportRef().nativeElement;
994
+ if (!this._isDragging()) {
995
+ this._isDragging.set(true);
996
+ viewport.style.scrollSnapType = 'none';
997
+ viewport.classList.add('cursor-grabbing');
998
+ try {
999
+ viewport.setPointerCapture(start.pointerId);
1000
+ }
1001
+ catch {
1002
+ // setPointerCapture may throw if the element isn't focusable in some browsers.
1003
+ }
1004
+ }
1005
+ if (horizontal) {
1006
+ viewport.scrollLeft = start.scrollLeft - delta * this._rtlSign();
1007
+ }
1008
+ else {
1009
+ viewport.scrollTop = start.scrollTop - delta;
1010
+ }
1011
+ }
1012
+ _onPointerUp(_event) {
1013
+ // Capture `_dragStart` into a local *before* nulling the field so the
1014
+ // `releasePointerCapture` call below still has the pointerId. Order
1015
+ // matters: a re-entrant pointer event (some browsers fire `pointercancel`
1016
+ // synchronously inside `releasePointerCapture`) must see `_dragStart === null`
1017
+ // and bail at the `!this._isDragging()` guard or via `_dragCleanup` no-op.
1018
+ const start = this._dragStart;
1019
+ this._dragStart = null;
1020
+ if (!this._isDragging())
1021
+ return;
1022
+ const viewport = this._viewportRef().nativeElement;
1023
+ viewport.style.scrollSnapType = '';
1024
+ viewport.classList.remove('cursor-grabbing');
1025
+ try {
1026
+ if (start)
1027
+ viewport.releasePointerCapture(start.pointerId);
1028
+ }
1029
+ catch {
1030
+ // ignore
1031
+ }
1032
+ this._isDragging.set(false);
1033
+ this._lastInteractionSource.set('pointer');
1034
+ // Suppress the click that immediately follows a drag-release.
1035
+ const swallowClick = (e) => {
1036
+ e.stopPropagation();
1037
+ e.preventDefault();
1038
+ viewport.removeEventListener('click', swallowClick, true);
1039
+ };
1040
+ viewport.addEventListener('click', swallowClick, true);
1041
+ queueMicrotask(() => {
1042
+ viewport.removeEventListener('click', swallowClick, true);
1043
+ });
1044
+ // Post-interaction autoplay pause.
1045
+ this._postInteractionPauseUntil.set(Date.now() + this._effectiveAutoplayInterval() * 2);
1046
+ setTimeout(() => {
1047
+ if (this._postInteractionPauseUntil() !== null &&
1048
+ Date.now() >= (this._postInteractionPauseUntil() ?? 0)) {
1049
+ this._postInteractionPauseUntil.set(null);
1050
+ }
1051
+ }, this._effectiveAutoplayInterval() * 2 + 16);
1052
+ }
1053
+ // ── Keyboard handler ───────────────────────────────────────────
1054
+ _onKeydown(event) {
1055
+ if (!this.keyboard())
1056
+ return;
1057
+ const horizontal = this.orientation() === 'horizontal';
1058
+ const rtl = this._rtlSign() === -1;
1059
+ let handled = false;
1060
+ switch (event.key) {
1061
+ case 'ArrowLeft':
1062
+ if (horizontal) {
1063
+ this._lastInteractionSource.set('keyboard');
1064
+ rtl ? this.next() : this.prev();
1065
+ handled = true;
1066
+ }
1067
+ break;
1068
+ case 'ArrowRight':
1069
+ if (horizontal) {
1070
+ this._lastInteractionSource.set('keyboard');
1071
+ rtl ? this.prev() : this.next();
1072
+ handled = true;
1073
+ }
1074
+ break;
1075
+ case 'ArrowUp':
1076
+ this._lastInteractionSource.set('keyboard');
1077
+ this.prev();
1078
+ handled = true;
1079
+ break;
1080
+ case 'ArrowDown':
1081
+ this._lastInteractionSource.set('keyboard');
1082
+ this.next();
1083
+ handled = true;
1084
+ break;
1085
+ case 'Home':
1086
+ this._lastInteractionSource.set('keyboard');
1087
+ this.scrollTo(0);
1088
+ handled = true;
1089
+ break;
1090
+ case 'End': {
1091
+ this._lastInteractionSource.set('keyboard');
1092
+ const lastIndex = Math.max(0, this.slides().length - 1);
1093
+ this.scrollTo(lastIndex);
1094
+ handled = true;
1095
+ break;
1096
+ }
1097
+ case 'PageUp':
1098
+ this._lastInteractionSource.set('keyboard');
1099
+ this.prev();
1100
+ handled = true;
1101
+ break;
1102
+ case 'PageDown':
1103
+ this._lastInteractionSource.set('keyboard');
1104
+ this.next();
1105
+ handled = true;
1106
+ break;
1107
+ }
1108
+ if (handled) {
1109
+ event.preventDefault();
1110
+ }
1111
+ }
1112
+ // ── Pause control toggle ───────────────────────────────────────
1113
+ _togglePauseControl() {
1114
+ const next = !this._isManuallyPaused();
1115
+ this._isManuallyPaused.set(next);
1116
+ }
1117
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CarouselComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1118
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: CarouselComponent, isStandalone: true, selector: "tw-carousel", inputs: { orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, slidesPerView: { classPropertyName: "slidesPerView", publicName: "slidesPerView", isSignal: true, isRequired: false, transformFunction: null }, slidesToScroll: { classPropertyName: "slidesToScroll", publicName: "slidesToScroll", isSignal: true, isRequired: false, transformFunction: null }, gap: { classPropertyName: "gap", publicName: "gap", isSignal: true, isRequired: false, transformFunction: null }, loop: { classPropertyName: "loop", publicName: "loop", isSignal: true, isRequired: false, transformFunction: null }, autoplay: { classPropertyName: "autoplay", publicName: "autoplay", isSignal: true, isRequired: false, transformFunction: null }, autoplayInterval: { classPropertyName: "autoplayInterval", publicName: "autoplayInterval", isSignal: true, isRequired: false, transformFunction: null }, pauseOnHover: { classPropertyName: "pauseOnHover", publicName: "pauseOnHover", isSignal: true, isRequired: false, transformFunction: null }, pauseOnFocusIn: { classPropertyName: "pauseOnFocusIn", publicName: "pauseOnFocusIn", isSignal: true, isRequired: false, transformFunction: null }, draggable: { classPropertyName: "draggable", publicName: "draggable", isSignal: true, isRequired: false, transformFunction: null }, keyboard: { classPropertyName: "keyboard", publicName: "keyboard", isSignal: true, isRequired: false, transformFunction: null }, snapAlign: { classPropertyName: "snapAlign", publicName: "snapAlign", isSignal: true, isRequired: false, transformFunction: null }, activeIndex: { classPropertyName: "activeIndex", publicName: "activeIndex", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledBy: { classPropertyName: "ariaLabelledBy", publicName: "ariaLabelledBy", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activeIndex: "activeIndexChange", slideChange: "slideChange", autoplayPaused: "autoplayPaused", autoplayResumed: "autoplayResumed" }, host: { attributes: { "role": "region" }, listeners: { "pointerenter": "_onPointerEnter()", "pointerleave": "_onPointerLeave()", "focusin": "_onFocusIn()", "focusout": "_onFocusOut($event)" }, properties: { "attr.aria-roledescription": "\"carousel\"", "attr.aria-label": "ariaLabel()", "attr.aria-labelledby": "ariaLabelledBy()", "class": "rootClasses()" } }, queries: [{ propertyName: "slides", predicate: CarouselSlideComponent, isSignal: true }], viewQueries: [{ propertyName: "_viewportRef", first: true, predicate: ["viewport"], descendants: true, isSignal: true }], ngImport: i0, template: `
1119
+ <div
1120
+ #viewport
1121
+ data-tw-carousel-viewport
1122
+ tabindex="0"
1123
+ [class]="viewportClasses()"
1124
+ [attr.aria-live]="autoplay() ? 'off' : 'polite'"
1125
+ (pointerdown)="_onPointerDown($event)"
1126
+ (keydown)="_onKeydown($event)"
1127
+ >
1128
+ <ng-content select="tw-carousel-slide" />
1129
+ </div>
1130
+
1131
+ <!--
1132
+ Pause/play glyphs are inline SVG (not <tw-icon>) to avoid coupling the
1133
+ carousel entry point to ngx-tw/icon's registry. The carousel must work
1134
+ without consumer icon registration; two structural primitives should
1135
+ not couple through an icon registry (audit (h)).
1136
+ -->
1137
+ @if (autoplay()) {
1138
+ <button
1139
+ type="button"
1140
+ [class]="pauseControlClasses()"
1141
+ [attr.aria-label]="_pauseControlAriaLabel()"
1142
+ (click)="_togglePauseControl()"
1143
+ >
1144
+ @if (_isManuallyPausedView()) {
1145
+ <svg viewBox="0 0 24 24" class="size-3 shrink-0" aria-hidden="true">
1146
+ <path d="M8 5v14l11-7L8 5z" fill="currentColor" />
1147
+ </svg>
1148
+ } @else {
1149
+ <svg viewBox="0 0 24 24" class="size-3 shrink-0" aria-hidden="true">
1150
+ <path d="M6 5h4v14H6V5zm8 0h4v14h-4V5z" fill="currentColor" />
1151
+ </svg>
1152
+ }
1153
+ </button>
1154
+ }
1155
+
1156
+ <ng-content select="[twCarouselPrev], [twCarouselNext], tw-carousel-indicators" />
1157
+ <!-- Default slot: catches arbitrary footer content (custom control rows,
1158
+ captions, secondary buttons). Render after the named-selector slot so
1159
+ dedicated controls keep their canonical position. -->
1160
+ <ng-content />
1161
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
1162
+ }
1163
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CarouselComponent, decorators: [{
1164
+ type: Component,
1165
+ args: [{
1166
+ selector: 'tw-carousel',
1167
+ changeDetection: ChangeDetectionStrategy.OnPush,
1168
+ host: {
1169
+ role: 'region',
1170
+ '[attr.aria-roledescription]': '"carousel"',
1171
+ '[attr.aria-label]': 'ariaLabel()',
1172
+ '[attr.aria-labelledby]': 'ariaLabelledBy()',
1173
+ '[class]': 'rootClasses()',
1174
+ '(pointerenter)': '_onPointerEnter()',
1175
+ '(pointerleave)': '_onPointerLeave()',
1176
+ '(focusin)': '_onFocusIn()',
1177
+ '(focusout)': '_onFocusOut($event)',
1178
+ },
1179
+ template: `
1180
+ <div
1181
+ #viewport
1182
+ data-tw-carousel-viewport
1183
+ tabindex="0"
1184
+ [class]="viewportClasses()"
1185
+ [attr.aria-live]="autoplay() ? 'off' : 'polite'"
1186
+ (pointerdown)="_onPointerDown($event)"
1187
+ (keydown)="_onKeydown($event)"
1188
+ >
1189
+ <ng-content select="tw-carousel-slide" />
1190
+ </div>
1191
+
1192
+ <!--
1193
+ Pause/play glyphs are inline SVG (not <tw-icon>) to avoid coupling the
1194
+ carousel entry point to ngx-tw/icon's registry. The carousel must work
1195
+ without consumer icon registration; two structural primitives should
1196
+ not couple through an icon registry (audit (h)).
1197
+ -->
1198
+ @if (autoplay()) {
1199
+ <button
1200
+ type="button"
1201
+ [class]="pauseControlClasses()"
1202
+ [attr.aria-label]="_pauseControlAriaLabel()"
1203
+ (click)="_togglePauseControl()"
1204
+ >
1205
+ @if (_isManuallyPausedView()) {
1206
+ <svg viewBox="0 0 24 24" class="size-3 shrink-0" aria-hidden="true">
1207
+ <path d="M8 5v14l11-7L8 5z" fill="currentColor" />
1208
+ </svg>
1209
+ } @else {
1210
+ <svg viewBox="0 0 24 24" class="size-3 shrink-0" aria-hidden="true">
1211
+ <path d="M6 5h4v14H6V5zm8 0h4v14h-4V5z" fill="currentColor" />
1212
+ </svg>
1213
+ }
1214
+ </button>
1215
+ }
1216
+
1217
+ <ng-content select="[twCarouselPrev], [twCarouselNext], tw-carousel-indicators" />
1218
+ <!-- Default slot: catches arbitrary footer content (custom control rows,
1219
+ captions, secondary buttons). Render after the named-selector slot so
1220
+ dedicated controls keep their canonical position. -->
1221
+ <ng-content />
1222
+ `,
1223
+ }]
1224
+ }], ctorParameters: () => [], propDecorators: { orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }], slidesPerView: [{ type: i0.Input, args: [{ isSignal: true, alias: "slidesPerView", required: false }] }], slidesToScroll: [{ type: i0.Input, args: [{ isSignal: true, alias: "slidesToScroll", required: false }] }], gap: [{ type: i0.Input, args: [{ isSignal: true, alias: "gap", required: false }] }], loop: [{ type: i0.Input, args: [{ isSignal: true, alias: "loop", required: false }] }], autoplay: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoplay", required: false }] }], autoplayInterval: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoplayInterval", required: false }] }], pauseOnHover: [{ type: i0.Input, args: [{ isSignal: true, alias: "pauseOnHover", required: false }] }], pauseOnFocusIn: [{ type: i0.Input, args: [{ isSignal: true, alias: "pauseOnFocusIn", required: false }] }], draggable: [{ type: i0.Input, args: [{ isSignal: true, alias: "draggable", required: false }] }], keyboard: [{ type: i0.Input, args: [{ isSignal: true, alias: "keyboard", required: false }] }], snapAlign: [{ type: i0.Input, args: [{ isSignal: true, alias: "snapAlign", required: false }] }], activeIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeIndex", required: false }] }, { type: i0.Output, args: ["activeIndexChange"] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaLabelledBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabelledBy", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], slideChange: [{ type: i0.Output, args: ["slideChange"] }], autoplayPaused: [{ type: i0.Output, args: ["autoplayPaused"] }], autoplayResumed: [{ type: i0.Output, args: ["autoplayResumed"] }], _viewportRef: [{ type: i0.ViewChild, args: ['viewport', { isSignal: true }] }], slides: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => CarouselSlideComponent), { ...{ descendants: false }, isSignal: true }] }] } });
1225
+ // ── CarouselIndicatorsComponent ───────────────────────────────────
1226
+ /**
1227
+ * Renders one button per **page** (not per slide) inside a `<tw-carousel>`.
1228
+ * The active button is marked with `aria-current="true"` and a distinguishing
1229
+ * scale / width / fill so it is identifiable beyond color alone (WCAG 1.4.1).
1230
+ */
1231
+ class CarouselIndicatorsComponent {
1232
+ /** Visual style. `'dots'` = small filled circles; `'lines'` = short horizontal/vertical bars; `'numbers'` = text 1, 2, 3 inside small pills. Defaults to `'dots'`. */
1233
+ variant = input('dots', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
1234
+ /** Color of the active indicator. Inactive indicators use neutral `fg-muted` tokens. Defaults to `'primary'`. */
1235
+ color = input('primary', ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
1236
+ /** Indicator size (diameter/length and gap between indicators). Defaults to `'md'`. */
1237
+ size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
1238
+ /** When `'overlay'`, the indicators float absolutely-positioned over the carousel with a `bg-overlay-control` translucent capsule backdrop for contrast. When `'below'`, they sit below the carousel as a normal block. Defaults to `'below'`. */
1239
+ position = input('below', ...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
1240
+ /** @internal Direct parent carousel. */
1241
+ carousel = inject(CarouselComponent);
1242
+ _variantResult = computed(() => carouselVariants({
1243
+ orientation: this.carousel.orientation(),
1244
+ variant: this.variant(),
1245
+ size: this.size(),
1246
+ position: this.position(),
1247
+ gap: this.carousel.gap(),
1248
+ snapAlign: this.carousel.snapAlign(),
1249
+ }), ...(ngDevMode ? [{ debugName: "_variantResult" }] : /* istanbul ignore next */ []));
1250
+ rootClasses = computed(() => this._variantResult().indicators(), ...(ngDevMode ? [{ debugName: "rootClasses" }] : /* istanbul ignore next */ []));
1251
+ /**
1252
+ * @internal Sequence of page indices to render. Returns `[]` when the
1253
+ * parent carousel's `slidesPerView` is not `1` — a single dot can never
1254
+ * match a viewport that shows two or more slides, so the indicator UX is
1255
+ * unambiguously broken at multi-item display (issue A1). The host element
1256
+ * stays mounted so layout doesn't reflow if the consumer toggles
1257
+ * `slidesPerView` back to `1` — the buttons reappear at that point without
1258
+ * a remount.
1259
+ */
1260
+ _pages = computed(() => {
1261
+ if (this.carousel.slidesPerView() !== 1)
1262
+ return [];
1263
+ const count = this.carousel.pageCount();
1264
+ const out = new Array(count);
1265
+ for (let i = 0; i < count; i++)
1266
+ out[i] = i;
1267
+ return out;
1268
+ }, ...(ngDevMode ? [{ debugName: "_pages" }] : /* istanbul ignore next */ []));
1269
+ constructor() {
1270
+ if (isDevMode()) {
1271
+ let warned = false;
1272
+ effect(() => {
1273
+ if (!warned && this.carousel.slidesPerView() !== 1) {
1274
+ warned = true;
1275
+ console.warn(`[tw-carousel-indicators] Indicators are only shown when slidesPerView === 1. Got slidesPerView=${this.carousel.slidesPerView()}; indicators are hidden until slidesPerView returns to 1.`);
1276
+ }
1277
+ });
1278
+ }
1279
+ }
1280
+ _buttonClasses(page) {
1281
+ const base = this._variantResult().indicator();
1282
+ const active = page === this.carousel.activePage();
1283
+ const stateClasses = active
1284
+ ? resolveIndicatorActiveClasses(this.variant(), this.color())
1285
+ : resolveIndicatorInactiveClasses(this.variant());
1286
+ return `${base} ${stateClasses}`;
1287
+ }
1288
+ _buttonLabel(page) {
1289
+ return formatLabel(this.carousel.resolvedLabels().indicator, {
1290
+ page: page + 1,
1291
+ });
1292
+ }
1293
+ _onClick(page) {
1294
+ const toScroll = this.carousel._effectiveSlidesToScroll();
1295
+ const targetIndex = page * toScroll;
1296
+ this.carousel._setLastInteractionSource('indicator');
1297
+ this.carousel.scrollTo(targetIndex);
1298
+ }
1299
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CarouselIndicatorsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1300
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: CarouselIndicatorsComponent, isStandalone: true, selector: "tw-carousel-indicators", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "group" }, properties: { "class": "rootClasses()" } }, ngImport: i0, template: `
1301
+ @for (page of _pages(); track page) {
1302
+ <button
1303
+ type="button"
1304
+ [class]="_buttonClasses(page)"
1305
+ [attr.aria-label]="_buttonLabel(page)"
1306
+ [attr.aria-current]="page === carousel.activePage() ? 'true' : null"
1307
+ (click)="_onClick(page)"
1308
+ >
1309
+ @if (variant() === 'numbers') {
1310
+ {{ page + 1 }}
1311
+ }
1312
+ </button>
1313
+ }
1314
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
1315
+ }
1316
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CarouselIndicatorsComponent, decorators: [{
1317
+ type: Component,
1318
+ args: [{
1319
+ selector: 'tw-carousel-indicators',
1320
+ changeDetection: ChangeDetectionStrategy.OnPush,
1321
+ host: {
1322
+ role: 'group',
1323
+ '[class]': 'rootClasses()',
1324
+ },
1325
+ template: `
1326
+ @for (page of _pages(); track page) {
1327
+ <button
1328
+ type="button"
1329
+ [class]="_buttonClasses(page)"
1330
+ [attr.aria-label]="_buttonLabel(page)"
1331
+ [attr.aria-current]="page === carousel.activePage() ? 'true' : null"
1332
+ (click)="_onClick(page)"
1333
+ >
1334
+ @if (variant() === 'numbers') {
1335
+ {{ page + 1 }}
1336
+ }
1337
+ </button>
1338
+ }
1339
+ `,
1340
+ }]
1341
+ }], ctorParameters: () => [], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }] } });
1342
+ // ── Prev / Next directives ────────────────────────────────────────
1343
+ /**
1344
+ * Apply to any focusable element (typically `<button>`) inside a `<tw-carousel>`
1345
+ * to navigate to the previous page. Auto-disables at the first slide when the
1346
+ * carousel is not looping. Sets `aria-label` to `labels.previous` unless the
1347
+ * host already carries `aria-label` or `aria-labelledby`.
1348
+ */
1349
+ class CarouselPrevDirective {
1350
+ /** @internal Parent carousel. */
1351
+ carousel = inject(CarouselComponent);
1352
+ _hostEl = inject((ElementRef)).nativeElement;
1353
+ _consumerLabel = this._hostEl.getAttribute('aria-label');
1354
+ _hasLabelledBy = this._hostEl.hasAttribute('aria-labelledby');
1355
+ _isButton = this._hostEl.tagName === 'BUTTON';
1356
+ /** @internal True when prev is unavailable (at start and !loop). */
1357
+ _isDisabled = computed(() => !this.carousel.loop() && this.carousel.isAtStart(), ...(ngDevMode ? [{ debugName: "_isDisabled" }] : /* istanbul ignore next */ []));
1358
+ _ariaLabel() {
1359
+ // Consumer-provided aria-label wins outright. Consumer-provided
1360
+ // aria-labelledby suppresses our binding so the two labels don't compete.
1361
+ if (this._consumerLabel !== null)
1362
+ return this._consumerLabel;
1363
+ if (this._hasLabelledBy)
1364
+ return null;
1365
+ return this.carousel.resolvedLabels().previous;
1366
+ }
1367
+ _isButtonDisabled() {
1368
+ return this._isButton && this._isDisabled() ? '' : null;
1369
+ }
1370
+ _isTabindex() {
1371
+ return !this._isButton && this._isDisabled() ? '-1' : null;
1372
+ }
1373
+ _onClick(event) {
1374
+ if (this._isDisabled()) {
1375
+ event.preventDefault();
1376
+ return;
1377
+ }
1378
+ this.carousel._setLastInteractionSource('button');
1379
+ this.carousel.prev();
1380
+ }
1381
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CarouselPrevDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1382
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: CarouselPrevDirective, isStandalone: true, selector: "[twCarouselPrev]", host: { listeners: { "click": "_onClick($event)" }, properties: { "attr.aria-label": "_ariaLabel()", "attr.disabled": "_isButtonDisabled()", "attr.aria-disabled": "_isDisabled() ? \"true\" : null", "attr.tabindex": "_isTabindex()" } }, ngImport: i0 });
1383
+ }
1384
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CarouselPrevDirective, decorators: [{
1385
+ type: Directive,
1386
+ args: [{
1387
+ selector: '[twCarouselPrev]',
1388
+ host: {
1389
+ '(click)': '_onClick($event)',
1390
+ '[attr.aria-label]': '_ariaLabel()',
1391
+ '[attr.disabled]': '_isButtonDisabled()',
1392
+ '[attr.aria-disabled]': '_isDisabled() ? "true" : null',
1393
+ '[attr.tabindex]': '_isTabindex()',
1394
+ },
1395
+ }]
1396
+ }] });
1397
+ /**
1398
+ * Apply to any focusable element (typically `<button>`) inside a `<tw-carousel>`
1399
+ * to navigate to the next page. Auto-disables at the last page when the
1400
+ * carousel is not looping. Sets `aria-label` to `labels.next` unless the host
1401
+ * already carries `aria-label` or `aria-labelledby`.
1402
+ */
1403
+ class CarouselNextDirective {
1404
+ /** @internal Parent carousel. */
1405
+ carousel = inject(CarouselComponent);
1406
+ _hostEl = inject((ElementRef)).nativeElement;
1407
+ _consumerLabel = this._hostEl.getAttribute('aria-label');
1408
+ _hasLabelledBy = this._hostEl.hasAttribute('aria-labelledby');
1409
+ _isButton = this._hostEl.tagName === 'BUTTON';
1410
+ _isDisabled = computed(() => !this.carousel.loop() && this.carousel.isAtEnd(), ...(ngDevMode ? [{ debugName: "_isDisabled" }] : /* istanbul ignore next */ []));
1411
+ _ariaLabel() {
1412
+ if (this._consumerLabel !== null)
1413
+ return this._consumerLabel;
1414
+ if (this._hasLabelledBy)
1415
+ return null;
1416
+ return this.carousel.resolvedLabels().next;
1417
+ }
1418
+ _isButtonDisabled() {
1419
+ return this._isButton && this._isDisabled() ? '' : null;
1420
+ }
1421
+ _isTabindex() {
1422
+ return !this._isButton && this._isDisabled() ? '-1' : null;
1423
+ }
1424
+ _onClick(event) {
1425
+ if (this._isDisabled()) {
1426
+ event.preventDefault();
1427
+ return;
1428
+ }
1429
+ this.carousel._setLastInteractionSource('button');
1430
+ this.carousel.next();
1431
+ }
1432
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CarouselNextDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1433
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: CarouselNextDirective, isStandalone: true, selector: "[twCarouselNext]", host: { listeners: { "click": "_onClick($event)" }, properties: { "attr.aria-label": "_ariaLabel()", "attr.disabled": "_isButtonDisabled()", "attr.aria-disabled": "_isDisabled() ? \"true\" : null", "attr.tabindex": "_isTabindex()" } }, ngImport: i0 });
1434
+ }
1435
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CarouselNextDirective, decorators: [{
1436
+ type: Directive,
1437
+ args: [{
1438
+ selector: '[twCarouselNext]',
1439
+ host: {
1440
+ '(click)': '_onClick($event)',
1441
+ '[attr.aria-label]': '_ariaLabel()',
1442
+ '[attr.disabled]': '_isButtonDisabled()',
1443
+ '[attr.aria-disabled]': '_isDisabled() ? "true" : null',
1444
+ '[attr.tabindex]': '_isTabindex()',
1445
+ },
1446
+ }]
1447
+ }] });
1448
+
1449
+ /**
1450
+ * Generated bundle index. Do not edit.
1451
+ */
1452
+
1453
+ export { CarouselComponent, CarouselIndicatorsComponent, CarouselNextDirective, CarouselPrevDirective, CarouselSlideComponent, DEFAULT_CAROUSEL_LABELS };
1454
+ //# sourceMappingURL=cdevhub-ngx-tw-carousel.mjs.map