@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,861 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, inject, DestroyRef, signal, Injectable, computed, EventEmitter } from '@angular/core';
3
+ import { filter, takeUntil } from 'rxjs/operators';
4
+ import { FocusTrapFactory, _IdGenerator } from '@angular/cdk/a11y';
5
+ import { Overlay } from '@angular/cdk/overlay';
6
+ import { ComponentPortal } from '@angular/cdk/portal';
7
+ import { Subject } from 'rxjs';
8
+ import { tv } from 'tailwind-variants';
9
+
10
+ /**
11
+ * Default error-state strategy: errored when the control is `invalid` and
12
+ * either has been interacted with (`dirty` or `touched`) or its parent form
13
+ * has been submitted. Matches Material's default behavior.
14
+ */
15
+ const defaultErrorStateMatcher = {
16
+ isErrorState(control, form) {
17
+ if (!control) {
18
+ return false;
19
+ }
20
+ const interacted = control.dirty || control.touched;
21
+ return !!control.invalid && (interacted || !!form?.submitted);
22
+ },
23
+ };
24
+ /**
25
+ * Injection token carrying the {@link ErrorStateMatcher} used by every ngx-tw
26
+ * form control. Defaults to {@link defaultErrorStateMatcher}; provide a
27
+ * different matcher at root to change global error-display policy, or at any
28
+ * descendant injector to scope the change.
29
+ */
30
+ const TW_ERROR_STATE_MATCHER = new InjectionToken('TW_ERROR_STATE_MATCHER', {
31
+ providedIn: 'root',
32
+ factory: () => defaultErrorStateMatcher,
33
+ });
34
+
35
+ /**
36
+ * DI token through which sort containers expose their state to consumers. Provided by
37
+ * `SortDirective` (`[twSort]`). Inject with `{ optional: true }` — components that need
38
+ * aria-sort plumbing should degrade gracefully when no sort directive is present.
39
+ */
40
+ const TW_SORT_HANDLE = new InjectionToken('TwSortHandle');
41
+
42
+ /**
43
+ * Connected-overlay position list for "select-like" overlays — overlays whose
44
+ * panel attaches directly under (or above) a trigger element and falls back to
45
+ * the opposite vertical side when there is not enough room. Returns four
46
+ * fallback positions: below-start, below-end, above-start, above-end.
47
+ *
48
+ * Used by `SelectComponent`, `ComboboxComponent`, `DatePickerComponent`, and
49
+ * `DateRangePickerComponent` — any overlay-bearing form control that anchors
50
+ * a listbox / menu / calendar panel to its trigger. The shape is identical for
51
+ * all four; the historical "select-like" name refers to the original consumer.
52
+ *
53
+ * @param offset Vertical offset in pixels applied between the trigger edge and
54
+ * the panel edge. Below-positions use `+offset`; above-positions use
55
+ * `-offset` so the panel pulls away from the trigger consistently.
56
+ */
57
+ function buildSelectLikePositions(offset = 0) {
58
+ return [
59
+ { originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: offset },
60
+ { originX: 'end', originY: 'bottom', overlayX: 'end', overlayY: 'top', offsetY: offset },
61
+ { originX: 'start', originY: 'top', overlayX: 'start', overlayY: 'bottom', offsetY: -offset },
62
+ { originX: 'end', originY: 'top', overlayX: 'end', overlayY: 'bottom', offsetY: -offset },
63
+ ];
64
+ }
65
+
66
+ /**
67
+ * Maps a named scroll-strategy variant to the corresponding CDK
68
+ * `ScrollStrategy` instance. `'reposition'` is the default for select-like
69
+ * overlays; `'close'` dismisses the panel on scroll; `'block'` locks page
70
+ * scrolling while the panel is open.
71
+ *
72
+ * Used by `SelectComponent`, `ComboboxComponent`, `DatePickerComponent`, and
73
+ * `DateRangePickerComponent` — any overlay-bearing form control that exposes
74
+ * the same three-option scroll-strategy input.
75
+ */
76
+ function resolveSelectScrollStrategy(name, overlay) {
77
+ switch (name) {
78
+ case 'close':
79
+ return overlay.scrollStrategies.close();
80
+ case 'block':
81
+ return overlay.scrollStrategies.block();
82
+ default:
83
+ return overlay.scrollStrategies.reposition();
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Subscribes to overlay-level `Escape` keydown events and invokes `onEscape`
89
+ * for each one. Returns an unsubscribe function so callers can scope teardown
90
+ * to the open lifecycle of the overlay (typically tied into a `Subscription`
91
+ * aggregate or a `takeUntilDestroyed` flow).
92
+ *
93
+ * Listens via `OverlayRef.keydownEvents()` so the handler fires for any
94
+ * keystroke originating inside the overlay, regardless of which element holds
95
+ * DOM focus — useful for select-style overlays whose search/listbox children
96
+ * may receive focus apart from the trigger.
97
+ *
98
+ * Callers remain responsible for calling `event.preventDefault()` /
99
+ * `stopPropagation()` inside `onEscape` if they want to short-circuit further
100
+ * key handling.
101
+ */
102
+ function consumeOverlayEscape(overlayRef, onEscape) {
103
+ const subscription = overlayRef
104
+ .keydownEvents()
105
+ .pipe(filter((event) => event.key === 'Escape'))
106
+ .subscribe((event) => onEscape(event));
107
+ return () => subscription.unsubscribe();
108
+ }
109
+
110
+ /**
111
+ * Enter-animation duration for date-picker / date-range-picker overlays.
112
+ * Matches `theme/_base.css` `.scale-in 140ms` — the keyframe the picker
113
+ * overlays apply via `animate.enter="scale-in"`. The coordinator delays
114
+ * `opened$` emission by this duration so consumers can hook the moment the
115
+ * panel has fully appeared instead of the moment `open()` was called.
116
+ */
117
+ const PICKER_ENTER_DURATION = 140;
118
+ /**
119
+ * Leave-animation duration for date-picker / date-range-picker overlays.
120
+ * Matches `theme/_base.css` `.scale-out 120ms` — the keyframe the picker
121
+ * overlays apply via `animate.leave="scale-out"`. The coordinator delays
122
+ * overlay detach by this duration so the leave animation can play through.
123
+ */
124
+ const PICKER_LEAVE_DURATION = 120;
125
+ /**
126
+ * Coordinator that owns the CDK `OverlayRef`, focus trap, panel-id, and
127
+ * animation timing for an overlay-bearing form control. Consumed by
128
+ * `DatePickerComponent` and `DateRangePickerComponent`; both pickers register
129
+ * the coordinator at the component level (`providers: [PickerOverlayCoordinator]`)
130
+ * so each picker instance owns its own coordinator state.
131
+ *
132
+ * Per the library "no `providedIn: 'root'` for services" rule (see
133
+ * `.claude/CLAUDE.md` → "What NOT To Do"): the coordinator holds per-overlay
134
+ * `OverlayRef` / `FocusTrap` state and MUST be component-scoped.
135
+ *
136
+ * Scope of responsibility:
137
+ * - Create / dispose the CDK `OverlayRef`.
138
+ * - Attach a `ComponentPortal` and return the instance.
139
+ * - Set up / tear down a `FocusTrap` around the overlay element.
140
+ * - Emit `opened$` after the enter animation completes (closes the
141
+ * synchronous-emit bug previously present in both pickers).
142
+ * - Expose `backdropClick$` / `overlayKeydown$` / `escape$` streams scoped
143
+ * to the current open lifecycle.
144
+ * - Run a leave-animation timer with an `isAttached` guard so close races
145
+ * never touch a detached `OverlayRef` (mirrors the S14 command-palette
146
+ * pattern).
147
+ *
148
+ * Out of scope (stays in the consuming picker):
149
+ * - "Restore previous value on close" — semantics differ per picker.
150
+ * - Per-picker portal callbacks (calendar selection, action-bar buttons).
151
+ * - View-mode change-detection nudges (the range-picker calls
152
+ * `changeDetectorRef.detectChanges()` after pushing its initial overlay
153
+ * config; the date-picker does not).
154
+ */
155
+ class PickerOverlayCoordinator {
156
+ overlay = inject(Overlay);
157
+ focusTrapFactory = inject(FocusTrapFactory);
158
+ destroyRef = inject(DestroyRef);
159
+ idGenerator = inject(_IdGenerator);
160
+ overlayRef = null;
161
+ focusTrap = null;
162
+ closeTimer = null;
163
+ openedTimer = null;
164
+ currentPanelId = null;
165
+ currentClose$ = null;
166
+ openedSubject = null;
167
+ attachedSignal = signal(false, ...(ngDevMode ? [{ debugName: "attachedSignal" }] : /* istanbul ignore next */ []));
168
+ openedSignal = signal(false, ...(ngDevMode ? [{ debugName: "openedSignal" }] : /* istanbul ignore next */ []));
169
+ /** Whether the overlay is currently attached (between `open()` and detach). */
170
+ attached = this.attachedSignal.asReadonly();
171
+ /** Whether the enter animation has completed (`opened$` has fired). */
172
+ opened = this.openedSignal.asReadonly();
173
+ constructor() {
174
+ this.destroyRef.onDestroy(() => this.disposeImmediate());
175
+ }
176
+ /**
177
+ * Creates and attaches the overlay. Returns the live overlay metadata
178
+ * synchronously; callers should subscribe to {@link opened$} to observe the
179
+ * moment the enter animation completes. Returns `null` if an overlay is
180
+ * already attached.
181
+ */
182
+ open(config) {
183
+ if (this.overlayRef)
184
+ return null;
185
+ const positionStrategy = this.overlay
186
+ .position()
187
+ .flexibleConnectedTo(config.origin)
188
+ .withPositions(config.positions)
189
+ .withFlexibleDimensions(false)
190
+ .withPush(false)
191
+ .withViewportMargin(config.viewportMargin ?? 8);
192
+ this.overlayRef = this.overlay.create({
193
+ positionStrategy,
194
+ scrollStrategy: config.scrollStrategy,
195
+ hasBackdrop: true,
196
+ backdropClass: 'cdk-overlay-transparent-backdrop',
197
+ panelClass: config.panelClass,
198
+ });
199
+ const portal = new ComponentPortal(config.portalComponent, config.viewContainerRef, config.injector);
200
+ const ref = this.overlayRef.attach(portal);
201
+ const instance = ref.instance;
202
+ this.currentPanelId = this.idGenerator.getId('tw-picker-overlay-');
203
+ this.currentClose$ = new Subject();
204
+ this.openedSubject = new Subject();
205
+ this.attachedSignal.set(true);
206
+ this.openedSignal.set(false);
207
+ this.setupFocusTrap();
208
+ this.scheduleOpenedEmission();
209
+ return {
210
+ overlayRef: this.overlayRef,
211
+ componentRef: ref,
212
+ instance,
213
+ panelId: this.currentPanelId,
214
+ };
215
+ }
216
+ /**
217
+ * Starts the close sequence — destroys the focus trap immediately so focus
218
+ * can return to the trigger, then detaches the overlay after the leave
219
+ * animation runs ({@link PICKER_LEAVE_DURATION}). Invokes `onAfterClose`
220
+ * once the overlay is fully detached. No-op if no overlay is open or a
221
+ * close is already in flight.
222
+ *
223
+ * Mirrors the S14 command-palette `isAttached`-guarded close pattern:
224
+ * after the timer fires we re-check `attachedSignal` before touching the
225
+ * overlay so a race (programmatic dispose, double-close) cannot touch a
226
+ * destroyed instance.
227
+ */
228
+ close(onAfterClose = () => { }) {
229
+ if (!this.overlayRef || this.closeTimer !== null)
230
+ return;
231
+ this.destroyFocusTrap();
232
+ this.clearOpenedTimer();
233
+ this.closeTimer = setTimeout(() => {
234
+ this.closeTimer = null;
235
+ if (!this.attachedSignal()) {
236
+ // Lost race with disposeImmediate / destroyRef.onDestroy.
237
+ return;
238
+ }
239
+ if (this.overlayRef?.hasAttached()) {
240
+ this.overlayRef.detach();
241
+ }
242
+ // Dispose the OverlayRef so the next open() builds a fresh one with the
243
+ // current inputs (offset, scrollStrategy, panelClass). Without this the
244
+ // `if (this.overlayRef) return null;` guard at the top of open() would
245
+ // permanently block re-opens after the first close.
246
+ this.overlayRef?.dispose();
247
+ this.overlayRef = null;
248
+ this.currentClose$?.next();
249
+ this.currentClose$?.complete();
250
+ this.currentClose$ = null;
251
+ this.openedSubject?.complete();
252
+ this.openedSubject = null;
253
+ this.attachedSignal.set(false);
254
+ this.openedSignal.set(false);
255
+ this.currentPanelId = null;
256
+ onAfterClose();
257
+ }, PICKER_LEAVE_DURATION);
258
+ }
259
+ /**
260
+ * Stream of overlay-level backdrop clicks for the current open lifecycle.
261
+ * Completes when the overlay closes.
262
+ */
263
+ backdropClick$() {
264
+ this.assertOpen('backdropClick$');
265
+ return this.overlayRef.backdropClick().pipe(takeUntil(this.currentClose$));
266
+ }
267
+ /**
268
+ * Stream of overlay-level keydown events for the current open lifecycle.
269
+ * Completes when the overlay closes.
270
+ */
271
+ overlayKeydown$() {
272
+ this.assertOpen('overlayKeydown$');
273
+ return this.overlayRef.keydownEvents().pipe(takeUntil(this.currentClose$));
274
+ }
275
+ /**
276
+ * Stream filtered to `Escape` keydowns inside the overlay for the current
277
+ * open lifecycle. Equivalent to `overlayKeydown$().pipe(filter(e => e.key === 'Escape'))`
278
+ * but spelled out so consumers don't import RxJS operators just for the
279
+ * common case.
280
+ */
281
+ escape$() {
282
+ return this.overlayKeydown$().pipe(filter((e) => e.key === 'Escape'));
283
+ }
284
+ /**
285
+ * Emits exactly once after the enter animation completes
286
+ * ({@link PICKER_ENTER_DURATION}ms after `open()`). Used by consumers to
287
+ * fire their `opened` output at the moment the overlay panel is actually
288
+ * visible — closes the synchronous-emit bug the pickers carried before
289
+ * this coordinator existed.
290
+ *
291
+ * Completes when the overlay closes.
292
+ */
293
+ opened$() {
294
+ this.assertOpen('opened$');
295
+ return this.openedSubject.asObservable().pipe(takeUntil(this.currentClose$));
296
+ }
297
+ /** Exposes the live `OverlayRef` for advanced consumers (e.g. width sync). */
298
+ ref() {
299
+ return this.overlayRef;
300
+ }
301
+ /**
302
+ * Exposes the current panel id (auto-generated via CDK `_IdGenerator`,
303
+ * stable for the open lifecycle, reset to `null` on close). Neither
304
+ * consuming picker uses this today — both keep their own
305
+ * `${hostId}-dialog` id for `aria-controls` wiring — but the helper is
306
+ * exposed for future consumers (e.g. a picker wrapper that wants its
307
+ * panel id auto-managed).
308
+ */
309
+ panelId() {
310
+ return this.currentPanelId;
311
+ }
312
+ /**
313
+ * Immediately disposes the overlay and tears down the focus trap without
314
+ * running the leave animation. Called from {@link DestroyRef.onDestroy};
315
+ * consumers should NOT call this — use {@link close} for the user-visible
316
+ * close path.
317
+ */
318
+ disposeImmediate() {
319
+ this.clearCloseTimer();
320
+ this.clearOpenedTimer();
321
+ this.destroyFocusTrap();
322
+ if (this.currentClose$) {
323
+ this.currentClose$.next();
324
+ this.currentClose$.complete();
325
+ this.currentClose$ = null;
326
+ }
327
+ this.openedSubject?.complete();
328
+ this.openedSubject = null;
329
+ this.overlayRef?.dispose();
330
+ this.overlayRef = null;
331
+ this.attachedSignal.set(false);
332
+ this.openedSignal.set(false);
333
+ this.currentPanelId = null;
334
+ }
335
+ setupFocusTrap() {
336
+ if (!this.overlayRef)
337
+ return;
338
+ this.focusTrap = this.focusTrapFactory.create(this.overlayRef.overlayElement);
339
+ }
340
+ destroyFocusTrap() {
341
+ this.focusTrap?.destroy();
342
+ this.focusTrap = null;
343
+ }
344
+ scheduleOpenedEmission() {
345
+ this.clearOpenedTimer();
346
+ if (!this.overlayRef || !this.overlayRef.hasAttached())
347
+ return;
348
+ this.openedTimer = setTimeout(() => {
349
+ this.openedTimer = null;
350
+ if (!this.attachedSignal())
351
+ return;
352
+ this.openedSignal.set(true);
353
+ this.openedSubject?.next();
354
+ }, PICKER_ENTER_DURATION);
355
+ }
356
+ clearCloseTimer() {
357
+ if (this.closeTimer !== null) {
358
+ clearTimeout(this.closeTimer);
359
+ this.closeTimer = null;
360
+ }
361
+ }
362
+ clearOpenedTimer() {
363
+ if (this.openedTimer !== null) {
364
+ clearTimeout(this.openedTimer);
365
+ this.openedTimer = null;
366
+ }
367
+ }
368
+ assertOpen(method) {
369
+ if (!this.overlayRef || !this.currentClose$ || !this.openedSubject) {
370
+ throw new Error(`PickerOverlayCoordinator.${method}() called before open() — no overlay attached.`);
371
+ }
372
+ }
373
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PickerOverlayCoordinator, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
374
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PickerOverlayCoordinator });
375
+ }
376
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PickerOverlayCoordinator, decorators: [{
377
+ type: Injectable
378
+ }], ctorParameters: () => [] });
379
+
380
+ /**
381
+ * Pure helpers shared by `DialogContainer` and `SheetContainer` (both live
382
+ * under `projects/ngx-tw/{dialog,sheet}/`) — and consumable by any future
383
+ * `CdkDialogContainer` subclass that needs the same plumbing.
384
+ *
385
+ * No DOM, no Angular DI. The DI-bound state lives in
386
+ * {@link OverlayContainerCoordinator}; this file is the data-only layer.
387
+ */
388
+ /**
389
+ * Fallback padding (ms) added on top of an enter/exit animation duration when
390
+ * scheduling the `transitionend` fallback timer. The browser SHOULD fire
391
+ * `transitionend` at the configured duration, but transitions can be
392
+ * swallowed (focus changes during the animation, interrupted transitions,
393
+ * etc.) — the padding gives the browser a small grace period before our
394
+ * fallback runs.
395
+ *
396
+ * Both dialog and sheet containers used the same constant — extracted here so
397
+ * a future tweak applies to both at once.
398
+ */
399
+ const OVERLAY_ANIMATION_FALLBACK_PADDING = 50;
400
+ /**
401
+ * Coerces a user-supplied animation duration to a safe positive integer, or
402
+ * falls back to a default if the input is `null`, `undefined`, negative, or
403
+ * non-finite (`NaN`, `Infinity`). Dialog and sheet containers both used this
404
+ * same standalone function; centralised here.
405
+ */
406
+ function coerceOverlayDuration(value, fallback) {
407
+ if (value == null || value < 0 || !Number.isFinite(value))
408
+ return fallback;
409
+ return value;
410
+ }
411
+ /**
412
+ * Merges a consumer-supplied `panelClass` (single class, list, or
413
+ * `undefined`) with the container's internal class string. Returns a single
414
+ * space-separated class string suitable for `[class]` host binding.
415
+ *
416
+ * `consumer` always wins ordering (appended after `internal`) so consumer
417
+ * overrides resolve correctly through `tailwind-merge` upstream.
418
+ */
419
+ function mergeOverlayPanelClass(internal, consumer) {
420
+ if (!consumer)
421
+ return internal;
422
+ return Array.isArray(consumer) ? [internal, ...consumer].join(' ') : `${internal} ${consumer}`;
423
+ }
424
+ /**
425
+ * Append-only id list with idempotent insertion, used for the
426
+ * `aria-describedby` queue both `DialogContainer` and `SheetContainer`
427
+ * maintain (the matching `aria-labelledby` queue lives in CDK's
428
+ * `CdkDialogContainer`).
429
+ *
430
+ * Pure data structure — no DOM, no Angular signals. Consumers wrap an
431
+ * instance in their own change-detection mechanism (the
432
+ * {@link OverlayContainerCoordinator} keeps the live snapshot in a signal so
433
+ * the container's `[attr.aria-describedby]` binding refreshes via OnPush
434
+ * without a manual `markForCheck()`).
435
+ *
436
+ * First-registered-wins semantics for `first()` mirror CDK's
437
+ * `_ariaLabelledByQueue[0]` host binding — describing the dialog by the
438
+ * earliest registered description prevents a late-mounted nested directive
439
+ * from silently re-aiming the description target.
440
+ */
441
+ class AriaIdQueue {
442
+ ids = [];
443
+ /** Inserts an id at the tail. No-op if the id is already present. */
444
+ add(id) {
445
+ if (this.ids.includes(id))
446
+ return;
447
+ this.ids.push(id);
448
+ }
449
+ /** Removes the given id. No-op if the id is not present. */
450
+ remove(id) {
451
+ const index = this.ids.indexOf(id);
452
+ if (index >= 0)
453
+ this.ids.splice(index, 1);
454
+ }
455
+ /** First registered id (or `null` if empty). Matches CDK's `_ariaLabelledByQueue[0]` semantics. */
456
+ first() {
457
+ return this.ids[0] ?? null;
458
+ }
459
+ /** Returns a fresh snapshot of all registered ids in insertion order. */
460
+ snapshot() {
461
+ return [...this.ids];
462
+ }
463
+ }
464
+
465
+ /**
466
+ * Component-scoped coordinator that owns the enter/exit animation state
467
+ * machine, ARIA-describedby id queue, and panel-class merge for an overlay
468
+ * container that subclasses `@angular/cdk/dialog`'s `CdkDialogContainer`.
469
+ *
470
+ * Consumed by `DialogContainer` and `SheetContainer`; both register the
471
+ * coordinator at the component level (`providers: [OverlayContainerCoordinator]`)
472
+ * so each container instance owns its own animation state and queue.
473
+ *
474
+ * Per the library "no `providedIn: 'root'` for services" rule (see
475
+ * `.claude/CLAUDE.md` → "What NOT To Do"): the coordinator holds per-overlay
476
+ * state (the current animation timer, the describedby queue, the lifecycle
477
+ * signal) and MUST be component-scoped.
478
+ *
479
+ * Scope of responsibility:
480
+ * - Animation state signal (`state`) and `transitionDuration` computed.
481
+ * - Enter/exit animation timing (`startEnterAnimation`, `startExitAnimation`)
482
+ * with a `transitionend`-fallback timer.
483
+ * - `animationStateChanged` EventEmitter forwarded to the consuming `Ref`.
484
+ * - ARIA-describedby id queue (the matching labelledby queue is already
485
+ * owned by CDK's `CdkDialogContainer._ariaLabelledByQueue`).
486
+ *
487
+ * Out of scope (stays on the container subclass):
488
+ * - The `CdkDialogContainer` contract — focus trap, escape key, backdrop
489
+ * click, overlay attach/detach. The container subclass keeps inheriting
490
+ * `CdkDialogContainer` directly; this coordinator layers on top.
491
+ * - Tailwind class resolution (`tv()` variant slots) — that's per-container.
492
+ * - Host bindings on the container element — that's per-container.
493
+ */
494
+ class OverlayContainerCoordinator {
495
+ destroyRef = inject(DestroyRef);
496
+ stateSignal = signal('opening', ...(ngDevMode ? [{ debugName: "stateSignal" }] : /* istanbul ignore next */ []));
497
+ ariaDescribedByQueue = signal([], ...(ngDevMode ? [{ debugName: "ariaDescribedByQueue" }] : /* istanbul ignore next */ []));
498
+ queue = new AriaIdQueue();
499
+ animationTimer = null;
500
+ enterDuration = 0;
501
+ exitDuration = 0;
502
+ /** Lifecycle state of the container's enter/exit animation. */
503
+ state = this.stateSignal.asReadonly();
504
+ /** Live snapshot of the describedby id queue. */
505
+ describedByIds = this.ariaDescribedByQueue.asReadonly();
506
+ /**
507
+ * Active transition duration for the current state — the enter duration for
508
+ * `opening` / `open`, the exit duration for `closing`, and 0 once `closed`.
509
+ * Drives the container's `[style.transition-duration.ms]` host binding so a
510
+ * one-line `data-[state]` transition rule can run with per-call timing.
511
+ */
512
+ transitionDuration = computed(() => {
513
+ const current = this.stateSignal();
514
+ if (current === 'opening' || current === 'open')
515
+ return this.enterDuration;
516
+ if (current === 'closing')
517
+ return this.exitDuration;
518
+ return 0;
519
+ }, ...(ngDevMode ? [{ debugName: "transitionDuration" }] : /* istanbul ignore next */ []));
520
+ /**
521
+ * Emits whenever the animation state transitions. The container's `Ref`
522
+ * (`TwDialogRef` / `SheetRef`) subscribes to drive its own lifecycle
523
+ * (`afterOpened`, `afterClosed`, exit-animation completion).
524
+ */
525
+ animationStateChanged = new EventEmitter();
526
+ constructor() {
527
+ this.destroyRef.onDestroy(() => {
528
+ this.clearAnimationTimer();
529
+ this.animationStateChanged.complete();
530
+ });
531
+ }
532
+ /**
533
+ * Records the resolved enter/exit durations for the open lifecycle.
534
+ * Called once from the container constructor with values coerced via
535
+ * `coerceOverlayDuration`. Stored on the coordinator so
536
+ * `transitionDuration()` and the timer-driven state transitions read from
537
+ * a single source.
538
+ */
539
+ setDurations(enter, exit) {
540
+ this.enterDuration = enter;
541
+ this.exitDuration = exit;
542
+ }
543
+ /**
544
+ * Drives the enter animation: emits `opening` immediately, then defers the
545
+ * state flip to `open` by one frame so the browser applies the initial
546
+ * (hidden / off-screen) styles before transitioning. If `enter` is `0`
547
+ * everything resolves synchronously (no animation).
548
+ */
549
+ startEnterAnimation() {
550
+ this.animationStateChanged.emit({ state: 'opening', totalTime: this.enterDuration });
551
+ if (this.enterDuration === 0) {
552
+ this.stateSignal.set('open');
553
+ this.animationStateChanged.emit({ state: 'open', totalTime: 0 });
554
+ return;
555
+ }
556
+ requestAnimationFrame(() => {
557
+ this.stateSignal.set('open');
558
+ this.runAnimationTimer(this.enterDuration, () => {
559
+ this.animationStateChanged.emit({
560
+ state: 'open',
561
+ totalTime: this.enterDuration,
562
+ });
563
+ });
564
+ });
565
+ }
566
+ /**
567
+ * Drives the exit animation: emits `closing` synchronously and schedules
568
+ * the `closed` emission for after the exit duration (+ a small fallback
569
+ * padding) elapses. The consuming `Ref` listens for the `closed` emission
570
+ * to detach the CDK overlay.
571
+ */
572
+ startExitAnimation() {
573
+ this.stateSignal.set('closing');
574
+ this.animationStateChanged.emit({ state: 'closing', totalTime: this.exitDuration });
575
+ this.runAnimationTimer(this.exitDuration, () => {
576
+ this.stateSignal.set('closed');
577
+ this.animationStateChanged.emit({ state: 'closed', totalTime: this.exitDuration });
578
+ });
579
+ }
580
+ /** Registers a description id with the `aria-describedby` queue. */
581
+ addAriaDescribedBy(id) {
582
+ this.queue.add(id);
583
+ this.ariaDescribedByQueue.set(this.queue.snapshot());
584
+ }
585
+ /** Removes a previously registered description id. */
586
+ removeAriaDescribedBy(id) {
587
+ this.queue.remove(id);
588
+ this.ariaDescribedByQueue.set(this.queue.snapshot());
589
+ }
590
+ /** First-registered-wins resolution for the `aria-describedby` attribute. */
591
+ firstDescribedBy() {
592
+ return this.queue.first();
593
+ }
594
+ runAnimationTimer(duration, callback) {
595
+ this.clearAnimationTimer();
596
+ if (duration === 0) {
597
+ callback();
598
+ return;
599
+ }
600
+ this.animationTimer = setTimeout(callback, duration + OVERLAY_ANIMATION_FALLBACK_PADDING);
601
+ }
602
+ clearAnimationTimer() {
603
+ if (this.animationTimer !== null) {
604
+ clearTimeout(this.animationTimer);
605
+ this.animationTimer = null;
606
+ }
607
+ }
608
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: OverlayContainerCoordinator, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
609
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: OverlayContainerCoordinator });
610
+ }
611
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: OverlayContainerCoordinator, decorators: [{
612
+ type: Injectable
613
+ }], ctorParameters: () => [] });
614
+
615
+ /**
616
+ * Trigger-only tailwind-variants config shared by tabs and tab-nav.
617
+ *
618
+ * Both components own additional component-local slots (tablist/list/panel/nav,
619
+ * etc.) — only the trigger shape is canonical enough to share here. The
620
+ * resulting class string is merged with each component's local trigger
621
+ * additions (e.g. tab-nav prepends `no-underline` because anchor elements need
622
+ * to override the default underline; tabs adds nothing extra at the base).
623
+ *
624
+ * Active and inactive trigger state is applied separately via
625
+ * {@link getActiveTriggerClasses} / {@link getInactiveTriggerClasses} so the
626
+ * `Record<TwColor, string>` lookups stay statically scannable by the Tailwind
627
+ * v4 content scanner.
628
+ */
629
+ const tabTriggerVariants = tv({
630
+ slots: {
631
+ trigger: 'inline-flex items-center gap-1.5 font-medium whitespace-nowrap cursor-pointer transition-colors duration-normal motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
632
+ },
633
+ variants: {
634
+ variant: {
635
+ underline: {
636
+ trigger: 'border-b-2 border-transparent -mb-px text-fg-muted hover:text-fg',
637
+ },
638
+ enclosed: {
639
+ trigger: 'border border-transparent bg-surface-muted text-fg-muted hover:text-fg -mb-px',
640
+ },
641
+ pill: {
642
+ trigger: 'rounded-md text-fg-muted hover:text-fg',
643
+ },
644
+ },
645
+ size: {
646
+ xs: { trigger: 'px-2 py-1 text-xs' },
647
+ sm: { trigger: 'px-3 py-1.5 text-sm' },
648
+ md: { trigger: 'px-4 py-2 text-sm' },
649
+ lg: { trigger: 'px-5 py-2.5 text-base' },
650
+ xl: { trigger: 'px-6 py-3 text-base' },
651
+ },
652
+ fitted: {
653
+ true: { trigger: 'flex-1 justify-center' },
654
+ false: {},
655
+ },
656
+ color: {
657
+ primary: {},
658
+ secondary: {},
659
+ accent: {},
660
+ neutral: {},
661
+ info: {},
662
+ success: {},
663
+ warning: {},
664
+ error: {},
665
+ },
666
+ },
667
+ defaultVariants: {
668
+ variant: 'underline',
669
+ color: 'primary',
670
+ size: 'md',
671
+ fitted: false,
672
+ },
673
+ }, { twMerge: true });
674
+ // ── Active-state class lookups ──
675
+ // Slot tokens own light/dark contrast — no `dark:`, no shade picks. Classes
676
+ // stay statically written so the Tailwind v4 scanner sees them. The neutral
677
+ // rows use surface/fg/border tokens which already auto-adapt to dark mode.
678
+ /** @internal Active trigger classes for horizontal underline variants, keyed by color. */
679
+ const UNDERLINE_ACTIVE_HORIZONTAL = {
680
+ primary: 'border-b-2 border-primary-border-strong text-primary-fg',
681
+ secondary: 'border-b-2 border-secondary-border-strong text-secondary-fg',
682
+ accent: 'border-b-2 border-accent-border-strong text-accent-fg',
683
+ neutral: 'border-b-2 border-border-strong text-fg',
684
+ info: 'border-b-2 border-info-border-strong text-info-fg',
685
+ success: 'border-b-2 border-success-border-strong text-success-fg',
686
+ warning: 'border-b-2 border-warning-border-strong text-warning-fg',
687
+ error: 'border-b-2 border-error-border-strong text-error-fg',
688
+ };
689
+ /** @internal Active trigger classes for vertical underline variants, keyed by color. */
690
+ const UNDERLINE_ACTIVE_VERTICAL = {
691
+ primary: 'border-r-2 border-primary-border-strong text-primary-fg',
692
+ secondary: 'border-r-2 border-secondary-border-strong text-secondary-fg',
693
+ accent: 'border-r-2 border-accent-border-strong text-accent-fg',
694
+ neutral: 'border-r-2 border-border-strong text-fg',
695
+ info: 'border-r-2 border-info-border-strong text-info-fg',
696
+ success: 'border-r-2 border-success-border-strong text-success-fg',
697
+ warning: 'border-r-2 border-warning-border-strong text-warning-fg',
698
+ error: 'border-r-2 border-error-border-strong text-error-fg',
699
+ };
700
+ /** @internal Active trigger classes for horizontal enclosed variants, keyed by color. */
701
+ const ENCLOSED_ACTIVE_HORIZONTAL = {
702
+ primary: 'bg-surface border border-border border-b-transparent text-primary-fg',
703
+ secondary: 'bg-surface border border-border border-b-transparent text-secondary-fg',
704
+ accent: 'bg-surface border border-border border-b-transparent text-accent-fg',
705
+ neutral: 'bg-surface border border-border border-b-transparent text-fg',
706
+ info: 'bg-surface border border-border border-b-transparent text-info-fg',
707
+ success: 'bg-surface border border-border border-b-transparent text-success-fg',
708
+ warning: 'bg-surface border border-border border-b-transparent text-warning-fg',
709
+ error: 'bg-surface border border-border border-b-transparent text-error-fg',
710
+ };
711
+ /** @internal Active trigger classes for vertical enclosed variants, keyed by color. */
712
+ const ENCLOSED_ACTIVE_VERTICAL = {
713
+ primary: 'bg-surface border border-border border-r-transparent text-primary-fg',
714
+ secondary: 'bg-surface border border-border border-r-transparent text-secondary-fg',
715
+ accent: 'bg-surface border border-border border-r-transparent text-accent-fg',
716
+ neutral: 'bg-surface border border-border border-r-transparent text-fg',
717
+ info: 'bg-surface border border-border border-r-transparent text-info-fg',
718
+ success: 'bg-surface border border-border border-r-transparent text-success-fg',
719
+ warning: 'bg-surface border border-border border-r-transparent text-warning-fg',
720
+ error: 'bg-surface border border-border border-r-transparent text-error-fg',
721
+ };
722
+ /** @internal Active trigger classes for pill variants, keyed by color. */
723
+ const PILL_ACTIVE = {
724
+ primary: 'bg-surface shadow-sm text-primary-fg',
725
+ secondary: 'bg-surface shadow-sm text-secondary-fg',
726
+ accent: 'bg-surface shadow-sm text-accent-fg',
727
+ neutral: 'bg-surface shadow-sm text-fg',
728
+ info: 'bg-surface shadow-sm text-info-fg',
729
+ success: 'bg-surface shadow-sm text-success-fg',
730
+ warning: 'bg-surface shadow-sm text-warning-fg',
731
+ error: 'bg-surface shadow-sm text-error-fg',
732
+ };
733
+ /** @internal Inactive trigger classes keyed by variant. */
734
+ const INACTIVE_TRIGGER_CLASSES = {
735
+ underline: 'border-transparent',
736
+ enclosed: 'border-transparent bg-surface-muted',
737
+ pill: '',
738
+ };
739
+ /**
740
+ * Returns the active-state trigger class string for the given variant, color,
741
+ * and orientation. Tab-nav callers pass `'horizontal'` since it is horizontal-only.
742
+ */
743
+ function getActiveTriggerClasses(variant, color, orientation = 'horizontal') {
744
+ switch (variant) {
745
+ case 'underline':
746
+ return orientation === 'vertical'
747
+ ? UNDERLINE_ACTIVE_VERTICAL[color]
748
+ : UNDERLINE_ACTIVE_HORIZONTAL[color];
749
+ case 'enclosed':
750
+ return orientation === 'vertical'
751
+ ? ENCLOSED_ACTIVE_VERTICAL[color]
752
+ : ENCLOSED_ACTIVE_HORIZONTAL[color];
753
+ case 'pill':
754
+ return PILL_ACTIVE[color];
755
+ }
756
+ }
757
+ /** Returns the inactive-state trigger class string for the given variant. */
758
+ function getInactiveTriggerClasses(variant) {
759
+ return INACTIVE_TRIGGER_CLASSES[variant];
760
+ }
761
+
762
+ /**
763
+ * Pure helpers shared by `tw-time-picker` and `tw-calendar`'s `withTime`
764
+ * controls. No Angular / CDK imports — safe to drop into either package
765
+ * without pulling a circular dependency.
766
+ */
767
+ /** Zero-pads a non-negative integer to exactly two digits. */
768
+ function padTwo(value) {
769
+ return value < 10 ? `0${value}` : `${value}`;
770
+ }
771
+ /** Converts a 24h hour (0–23) to its 12h display value (1–12). */
772
+ function to12h(hour24) {
773
+ const modded = hour24 % 12;
774
+ return modded === 0 ? 12 : modded;
775
+ }
776
+ /** Builds a canonical 0–23 hour from a 12h display hour and meridiem. */
777
+ function from12h(hour12, meridiem) {
778
+ if (hour12 === 12)
779
+ return meridiem === 'AM' ? 0 : 12;
780
+ return meridiem === 'AM' ? hour12 : hour12 + 12;
781
+ }
782
+ /** Maximum allowed value for a field given the picker format. */
783
+ function fieldMax(field, format) {
784
+ if (field === 'hour')
785
+ return format === '12h' ? 12 : 23;
786
+ return 59;
787
+ }
788
+ /** Minimum allowed value for a field given the picker format. */
789
+ function fieldMin(field, format) {
790
+ return field === 'hour' && format === '12h' ? 1 : 0;
791
+ }
792
+ /**
793
+ * Buffers a typed digit onto the current field text, matching the standard
794
+ * two-digit time-field behaviour:
795
+ * - empty + 'x' → 'x'
796
+ * - 'x' + 'y' → 'xy' (if value stays in range)
797
+ * - 'xy' + 'z' → 'z' (overflow → reset)
798
+ * - any combo that would exceed `max` resets to the new digit alone.
799
+ */
800
+ function appendDigit(current, digit, max) {
801
+ if (!/^\d$/.test(digit))
802
+ return current;
803
+ if (current.length >= 2)
804
+ return digit;
805
+ const candidate = current + digit;
806
+ const numeric = Number(candidate);
807
+ if (numeric > max)
808
+ return digit;
809
+ return candidate;
810
+ }
811
+ /**
812
+ * Reports whether `current` + `digit` unambiguously fills the field — either
813
+ * because the buffer reaches two chars or because the first digit alone
814
+ * already excludes a valid second digit (e.g., `'6'` for minutes, `'3'` for 24h
815
+ * hour). Used to auto-advance focus to the next field.
816
+ */
817
+ function isTerminalDigit(current, digit, max) {
818
+ if (current.length === 1)
819
+ return true;
820
+ if (!/^\d$/.test(digit))
821
+ return false;
822
+ const maxFirst = Math.floor(max / 10);
823
+ return Number(digit) > maxFirst;
824
+ }
825
+ /**
826
+ * Steps a numeric value by `step`, wrapping inside `[min, max]`. Works for
827
+ * arbitrary step sizes; a step of 0 behaves as 1 to protect against mis-configs.
828
+ */
829
+ function stepWithWrap(value, step, direction, min, max) {
830
+ const safeStep = Math.max(1, Math.abs(step));
831
+ const range = max - min + 1;
832
+ const delta = safeStep * direction;
833
+ return ((((value - min + delta) % range) + range) % range) + min;
834
+ }
835
+ /** Clamps a number into `[min, max]` without wrapping. */
836
+ function clamp(value, min, max) {
837
+ if (value < min)
838
+ return min;
839
+ if (value > max)
840
+ return max;
841
+ return value;
842
+ }
843
+ /** Parses a 1- or 2-digit text field; returns `null` if empty or non-numeric. */
844
+ function parseField(text) {
845
+ if (!text)
846
+ return null;
847
+ if (!/^\d{1,2}$/.test(text))
848
+ return null;
849
+ return Number(text);
850
+ }
851
+ /** Total seconds since midnight for a (h, m, s) tuple — useful for min/max compare. */
852
+ function timeOfDaySeconds(hour, minute, second) {
853
+ return hour * 3600 + minute * 60 + second;
854
+ }
855
+
856
+ /**
857
+ * Generated bundle index. Do not edit.
858
+ */
859
+
860
+ export { AriaIdQueue, ENCLOSED_ACTIVE_HORIZONTAL, ENCLOSED_ACTIVE_VERTICAL, INACTIVE_TRIGGER_CLASSES, OVERLAY_ANIMATION_FALLBACK_PADDING, OverlayContainerCoordinator, PICKER_ENTER_DURATION, PICKER_LEAVE_DURATION, PILL_ACTIVE, PickerOverlayCoordinator, TW_ERROR_STATE_MATCHER, TW_SORT_HANDLE, UNDERLINE_ACTIVE_HORIZONTAL, UNDERLINE_ACTIVE_VERTICAL, appendDigit, buildSelectLikePositions, clamp, coerceOverlayDuration, consumeOverlayEscape, defaultErrorStateMatcher, fieldMax, fieldMin, from12h, getActiveTriggerClasses, getInactiveTriggerClasses, isTerminalDigit, mergeOverlayPanelClass, padTwo, parseField, resolveSelectScrollStrategy, stepWithWrap, tabTriggerVariants, timeOfDaySeconds, to12h };
861
+ //# sourceMappingURL=cdevhub-ngx-tw-core.mjs.map