@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,1311 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, inject, Directive, input, output, contentChild, computed, ChangeDetectionStrategy, Component, signal, Injector, ElementRef, TemplateRef, untracked, ViewEncapsulation, Injectable, makeEnvironmentProviders } from '@angular/core';
3
+ import { Overlay, createGlobalPositionStrategy } from '@angular/cdk/overlay';
4
+ import { CdkPortalOutlet, ComponentPortal } from '@angular/cdk/portal';
5
+ import { ReplaySubject, Subject, defer, startWith } from 'rxjs';
6
+ import { NgTemplateOutlet } from '@angular/common';
7
+ import { LiveAnnouncer } from '@angular/cdk/a11y';
8
+ import { tv } from 'tailwind-variants';
9
+
10
+ /**
11
+ * Per-call configuration for {@link ToastService} methods. Also used as the
12
+ * payload of {@link TW_TOAST_DEFAULT_OPTIONS} for app-wide defaults.
13
+ */
14
+ class ToastConfig {
15
+ /** Severity of the toast. Defaults to `'info'`. */
16
+ severity = 'info';
17
+ /** Screen position of the toast stack. Defaults to `'bottom-right'`. */
18
+ position = 'bottom-right';
19
+ /** Auto-dismiss duration in ms. `0` disables auto-dismiss. Defaults to `5000`. */
20
+ duration = 5000;
21
+ // Default true: toasts must offer an escape hatch — sticky / long-duration toasts
22
+ // otherwise trap users; opt-out only for ephemeral progress markers paired with `duration`.
23
+ /** When true, renders a close (×) button. Defaults to `true`. */
24
+ dismissible = true;
25
+ /**
26
+ * Live-region politeness. When omitted, resolves to `'assertive'` for
27
+ * `severity: 'error'` and `'polite'` otherwise. Pass `'off'` to skip
28
+ * `LiveAnnouncer` entirely.
29
+ */
30
+ politeness;
31
+ /** Action button configuration. Omit to hide the action button. */
32
+ action;
33
+ /** Arbitrary data provided to component / template content via `TW_TOAST_DATA`. */
34
+ data = null;
35
+ /** Extra CSS classes merged onto the toast panel root via `twMerge`. */
36
+ panelClass;
37
+ /**
38
+ * Icon override. Pass a string (treated as text glyph / icon name) or
39
+ * `false` to hide the icon. When omitted the severity-default icon is used.
40
+ *
41
+ * Note: To render arbitrary icon markup, pass a `TemplateRef` to `show()`
42
+ * itself (the whole toast is a custom template) — the icon slot accepts only
43
+ * primitives for the string-content path.
44
+ */
45
+ icon;
46
+ // Default true: matches Material/Radix/Sonner conventions — prevents a toast from
47
+ // disappearing mid-read; opt-out only for fully ambient progress indicators.
48
+ /** When true, pause the auto-dismiss timer while the toast is hovered or holds focus. Defaults to `true`. */
49
+ pauseOnInteraction = true;
50
+ // Default true: matches mobile-platform expectations and gives non-precise pointers
51
+ // (touch) an easy dismiss gesture; opt-out only if the gesture conflicts with content.
52
+ /** Enable horizontal swipe-to-dismiss via pointer gestures. Defaults to `true`. */
53
+ swipeToDismiss = true;
54
+ /**
55
+ * Maximum visible toasts *per position*. When exceeded, the oldest toast in
56
+ * that position dismisses with reason `'max-exceeded'`. Defaults to `5`.
57
+ */
58
+ maxVisible = 5;
59
+ /** Explicit id. When omitted a unique id is generated. */
60
+ id;
61
+ /** Accessible label applied to the toast element. When omitted, text content is used. */
62
+ ariaLabel;
63
+ /** Accessible label applied to the per-position `role="region"` container. Defaults to `'Notifications'`. */
64
+ regionAriaLabel;
65
+ }
66
+ /** Injection token providing `config.data` to projected component / template content. */
67
+ const TW_TOAST_DATA = new InjectionToken('TW_TOAST_DATA');
68
+ /** Injection token providing the owning {@link ToastRef} to projected component content. */
69
+ const TW_TOAST_REF = new InjectionToken('TW_TOAST_REF');
70
+ /** Injection token carrying application-wide toast defaults. Set via `provideToast(defaults)`. */
71
+ const TW_TOAST_DEFAULT_OPTIONS = new InjectionToken('TW_TOAST_DEFAULT_OPTIONS');
72
+
73
+ const toastVariants = tv({
74
+ slots: {
75
+ root: 'pointer-events-auto relative flex items-start gap-3 w-full max-w-sm p-4 rounded-lg border shadow-md text-sm will-change-transform transition-colors duration-normal motion-reduce:transition-none',
76
+ icon: 'size-5 shrink-0 mt-0.5',
77
+ title: 'text-sm font-semibold',
78
+ description: 'text-sm',
79
+ content: 'flex-1 min-w-0',
80
+ action: 'inline-flex items-center justify-center px-3 py-1.5 rounded-md text-sm font-medium cursor-pointer transition-colors duration-normal motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
81
+ // Dismiss container is `size-6` (24px — xs square-interactive-target
82
+ // scale per CLAUDE.md icon sub-scale). The inner SVG is `size-4` (16px
83
+ // glyph scale), centred via flex; the larger container gives the
84
+ // touch/click affordance without enlarging the visual glyph.
85
+ dismiss: 'absolute top-3 right-3 inline-flex items-center justify-center size-6 rounded-md cursor-pointer transition-colors duration-normal motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
86
+ },
87
+ variants: {
88
+ severity: {
89
+ info: {},
90
+ success: {},
91
+ warning: {},
92
+ error: {},
93
+ neutral: {},
94
+ },
95
+ },
96
+ // Color slot tokens (`{role}-soft`, `-soft-fg`, `-icon`, …) own light/dark
97
+ // contrast — components consume the slot only. See `_semantic.css`.
98
+ // Slot-token safelist: see `projects/ngx-tw/theme/index.css` `@source inline(...)`
99
+ // — Tailwind v4's JIT cannot scan template-literal class names, so every
100
+ // `bg-${severity}-soft` / `text-${severity}-icon` permutation below must be
101
+ // enumerated there.
102
+ compoundVariants: ['info', 'success', 'warning', 'error', 'neutral'].map((severity) => ({
103
+ severity,
104
+ class: {
105
+ root: `bg-${severity}-soft text-${severity}-soft-fg-muted border-${severity}-border`,
106
+ icon: `text-${severity}-icon`,
107
+ title: `text-${severity}-soft-fg`,
108
+ description: `text-${severity}-soft-fg-muted`,
109
+ action: `text-${severity}-soft-fg hover:bg-${severity}-soft-hover`,
110
+ dismiss: `text-${severity}-icon hover:bg-${severity}-soft-hover`,
111
+ },
112
+ })),
113
+ defaultVariants: {
114
+ severity: 'info',
115
+ },
116
+ }, { twMerge: true });
117
+ /** Slot directive for the icon. When projected, overrides the severity-default icon. */
118
+ class ToastIconDirective {
119
+ toast = inject(ToastComponent);
120
+ /** @internal */
121
+ classes = this.toast.iconClasses;
122
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ToastIconDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
123
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: ToastIconDirective, isStandalone: true, selector: "[twToastIcon]", host: { attributes: { "aria-hidden": "true" }, properties: { "class": "classes()" } }, ngImport: i0 });
124
+ }
125
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ToastIconDirective, decorators: [{
126
+ type: Directive,
127
+ args: [{
128
+ selector: '[twToastIcon]',
129
+ host: {
130
+ '[class]': 'classes()',
131
+ 'aria-hidden': 'true',
132
+ },
133
+ }]
134
+ }] });
135
+ /** Slot directive for the bold title line inside a toast. */
136
+ class ToastTitleDirective {
137
+ toast = inject(ToastComponent);
138
+ /** @internal */
139
+ classes = this.toast.titleClasses;
140
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ToastTitleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
141
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: ToastTitleDirective, isStandalone: true, selector: "[twToastTitle]", host: { properties: { "class": "classes()" } }, ngImport: i0 });
142
+ }
143
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ToastTitleDirective, decorators: [{
144
+ type: Directive,
145
+ args: [{
146
+ selector: '[twToastTitle]',
147
+ host: {
148
+ '[class]': 'classes()',
149
+ },
150
+ }]
151
+ }] });
152
+ /** Slot directive for a secondary description line inside a toast. */
153
+ class ToastDescriptionDirective {
154
+ toast = inject(ToastComponent);
155
+ /** @internal */
156
+ classes = this.toast.descriptionClasses;
157
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ToastDescriptionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
158
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: ToastDescriptionDirective, isStandalone: true, selector: "[twToastDescription]", host: { properties: { "class": "classes()" } }, ngImport: i0 });
159
+ }
160
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ToastDescriptionDirective, decorators: [{
161
+ type: Directive,
162
+ args: [{
163
+ selector: '[twToastDescription]',
164
+ host: {
165
+ '[class]': 'classes()',
166
+ },
167
+ }]
168
+ }] });
169
+ /** Slot directive for the action button. Apply to a native `<button>` to inherit the severity-aware styling. */
170
+ class ToastActionDirective {
171
+ toast = inject(ToastComponent);
172
+ /** @internal */
173
+ classes = this.toast.actionClasses;
174
+ /** @internal */
175
+ onClick() {
176
+ this.toast.actionClicked.emit();
177
+ }
178
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ToastActionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
179
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: ToastActionDirective, isStandalone: true, selector: "[twToastAction]", host: { attributes: { "type": "button" }, listeners: { "click": "onClick()" }, properties: { "class": "classes()" } }, ngImport: i0 });
180
+ }
181
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ToastActionDirective, decorators: [{
182
+ type: Directive,
183
+ args: [{
184
+ selector: '[twToastAction]',
185
+ host: {
186
+ type: 'button',
187
+ '[class]': 'classes()',
188
+ '(click)': 'onClick()',
189
+ },
190
+ }]
191
+ }] });
192
+ /**
193
+ * Visual toast / snackbar panel. Rendered internally by {@link ToastService} for
194
+ * string content, and exported for consumers who want to compose the same
195
+ * visual inside a custom `TemplateRef` or component class passed to `show()`.
196
+ */
197
+ class ToastComponent {
198
+ /** Severity variant. Drives color palette, default icon, and ARIA role / live politeness. Defaults to `'info'`. */
199
+ severity = input('info', ...(ngDevMode ? [{ debugName: "severity" }] : /* istanbul ignore next */ []));
200
+ // Default true: mirrors `ToastConfig.dismissible` — toasts must offer an escape
201
+ // hatch by default; opt-out only for ephemeral progress markers paired with `duration`.
202
+ /** Whether to render the close button. Defaults to `true`. */
203
+ dismissible = input(true, ...(ngDevMode ? [{ debugName: "dismissible" }] : /* istanbul ignore next */ []));
204
+ /**
205
+ * Icon override. Pass a string to render as text inside the icon slot, or
206
+ * `false` to hide the built-in severity icon. Ignored when a `[twToastIcon]`
207
+ * child is projected. When omitted, the severity-default icon renders.
208
+ *
209
+ * For arbitrary icon markup, project a `[twToastIcon]` child instead.
210
+ */
211
+ icon = input(undefined, ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
212
+ /** Explicit aria-label for the toast wrapper. When omitted, text content is used by assistive tech. */
213
+ ariaLabel = input(undefined, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
214
+ /** Fires when the close button is clicked. */
215
+ dismissed = output();
216
+ /** Fires when a `[twToastAction]` button is clicked. */
217
+ actionClicked = output();
218
+ /** @internal */
219
+ projectedIcon = contentChild(ToastIconDirective, ...(ngDevMode ? [{ debugName: "projectedIcon" }] : /* istanbul ignore next */ []));
220
+ /** @internal */
221
+ hasProjectedIcon = computed(() => !!this.projectedIcon(), ...(ngDevMode ? [{ debugName: "hasProjectedIcon" }] : /* istanbul ignore next */ []));
222
+ variantResult = computed(() => toastVariants({ severity: this.severity() }), ...(ngDevMode ? [{ debugName: "variantResult" }] : /* istanbul ignore next */ []));
223
+ /** @internal */
224
+ rootClasses = computed(() => {
225
+ const base = this.variantResult().root();
226
+ return this.dismissible() ? `${base} pr-10` : base;
227
+ }, ...(ngDevMode ? [{ debugName: "rootClasses" }] : /* istanbul ignore next */ []));
228
+ /** @internal */
229
+ iconClasses = computed(() => this.variantResult().icon(), ...(ngDevMode ? [{ debugName: "iconClasses" }] : /* istanbul ignore next */ []));
230
+ /** @internal */
231
+ titleClasses = computed(() => this.variantResult().title(), ...(ngDevMode ? [{ debugName: "titleClasses" }] : /* istanbul ignore next */ []));
232
+ /** @internal */
233
+ descriptionClasses = computed(() => this.variantResult().description(), ...(ngDevMode ? [{ debugName: "descriptionClasses" }] : /* istanbul ignore next */ []));
234
+ /** @internal */
235
+ contentClasses = computed(() => this.variantResult().content(), ...(ngDevMode ? [{ debugName: "contentClasses" }] : /* istanbul ignore next */ []));
236
+ /** @internal */
237
+ actionClasses = computed(() => this.variantResult().action(), ...(ngDevMode ? [{ debugName: "actionClasses" }] : /* istanbul ignore next */ []));
238
+ /** @internal */
239
+ dismissClasses = computed(() => this.variantResult().dismiss(), ...(ngDevMode ? [{ debugName: "dismissClasses" }] : /* istanbul ignore next */ []));
240
+ /** @internal */
241
+ roleAttr = computed(() => this.severity() === 'error' ? 'alert' : 'status', ...(ngDevMode ? [{ debugName: "roleAttr" }] : /* istanbul ignore next */ []));
242
+ /** @internal */
243
+ ariaLiveAttr = computed(() => this.severity() === 'error' ? 'assertive' : 'polite', ...(ngDevMode ? [{ debugName: "ariaLiveAttr" }] : /* istanbul ignore next */ []));
244
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ToastComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
245
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: ToastComponent, isStandalone: true, selector: "tw-toast", inputs: { severity: { classPropertyName: "severity", publicName: "severity", isSignal: true, isRequired: false, transformFunction: null }, dismissible: { classPropertyName: "dismissible", publicName: "dismissible", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { dismissed: "dismissed", actionClicked: "actionClicked" }, host: { attributes: { "aria-atomic": "true" }, properties: { "class": "rootClasses()", "attr.role": "roleAttr()", "attr.aria-live": "ariaLiveAttr()", "attr.aria-label": "ariaLabel() || null", "attr.data-severity": "severity()" } }, queries: [{ propertyName: "projectedIcon", first: true, predicate: ToastIconDirective, descendants: true, isSignal: true }], ngImport: i0, template: `
246
+ @if (hasProjectedIcon()) {
247
+ <ng-content select="[twToastIcon]" />
248
+ } @else if (icon() !== false) {
249
+ <span [class]="iconClasses()" aria-hidden="true">
250
+ @switch (severity()) {
251
+ @case ('info') {
252
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
253
+ <path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm0-11a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm-.75 2.5a.75.75 0 0 0 0 1.5h.25v3h-.25a.75.75 0 0 0 0 1.5h2a.75.75 0 0 0 0-1.5h-.25v-3.75a.75.75 0 0 0-.75-.75h-1Z" clip-rule="evenodd"/>
254
+ </svg>
255
+ }
256
+ @case ('success') {
257
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
258
+ <path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm3.857-9.809a.75.75 0 0 0-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 1 0-1.06 1.061l2.5 2.5a.75.75 0 0 0 1.137-.089l4-5.5Z" clip-rule="evenodd"/>
259
+ </svg>
260
+ }
261
+ @case ('warning') {
262
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
263
+ <path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495ZM10 5a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 10 5Zm0 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" clip-rule="evenodd"/>
264
+ </svg>
265
+ }
266
+ @case ('error') {
267
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
268
+ <path fill-rule="evenodd" d="M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0Zm-8-5a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-1.5 0v-4.5A.75.75 0 0 1 10 5Zm0 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" clip-rule="evenodd"/>
269
+ </svg>
270
+ }
271
+ @default {
272
+ @if (icon() && icon() !== false) {
273
+ {{ icon() }}
274
+ }
275
+ }
276
+ }
277
+ </span>
278
+ }
279
+ <div [class]="contentClasses()">
280
+ <ng-content select="[twToastTitle]" />
281
+ <ng-content select="[twToastDescription]" />
282
+ <ng-content />
283
+ <ng-content select="[twToastAction]" />
284
+ </div>
285
+ @if (dismissible()) {
286
+ <button
287
+ type="button"
288
+ aria-label="Dismiss"
289
+ [class]="dismissClasses()"
290
+ (click)="dismissed.emit()"
291
+ >
292
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-4">
293
+ <path d="M5.28 4.22a.75.75 0 0 0-1.06 1.06L6.94 8l-2.72 2.72a.75.75 0 1 0 1.06 1.06L8 9.06l2.72 2.72a.75.75 0 1 0 1.06-1.06L9.06 8l2.72-2.72a.75.75 0 0 0-1.06-1.06L8 6.94 5.28 4.22Z"/>
294
+ </svg>
295
+ </button>
296
+ }
297
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
298
+ }
299
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ToastComponent, decorators: [{
300
+ type: Component,
301
+ args: [{
302
+ selector: 'tw-toast',
303
+ changeDetection: ChangeDetectionStrategy.OnPush,
304
+ host: {
305
+ '[class]': 'rootClasses()',
306
+ '[attr.role]': 'roleAttr()',
307
+ '[attr.aria-live]': 'ariaLiveAttr()',
308
+ 'aria-atomic': 'true',
309
+ '[attr.aria-label]': 'ariaLabel() || null',
310
+ '[attr.data-severity]': 'severity()',
311
+ },
312
+ template: `
313
+ @if (hasProjectedIcon()) {
314
+ <ng-content select="[twToastIcon]" />
315
+ } @else if (icon() !== false) {
316
+ <span [class]="iconClasses()" aria-hidden="true">
317
+ @switch (severity()) {
318
+ @case ('info') {
319
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
320
+ <path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm0-11a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm-.75 2.5a.75.75 0 0 0 0 1.5h.25v3h-.25a.75.75 0 0 0 0 1.5h2a.75.75 0 0 0 0-1.5h-.25v-3.75a.75.75 0 0 0-.75-.75h-1Z" clip-rule="evenodd"/>
321
+ </svg>
322
+ }
323
+ @case ('success') {
324
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
325
+ <path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm3.857-9.809a.75.75 0 0 0-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 1 0-1.06 1.061l2.5 2.5a.75.75 0 0 0 1.137-.089l4-5.5Z" clip-rule="evenodd"/>
326
+ </svg>
327
+ }
328
+ @case ('warning') {
329
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
330
+ <path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495ZM10 5a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 10 5Zm0 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" clip-rule="evenodd"/>
331
+ </svg>
332
+ }
333
+ @case ('error') {
334
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
335
+ <path fill-rule="evenodd" d="M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0Zm-8-5a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-1.5 0v-4.5A.75.75 0 0 1 10 5Zm0 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" clip-rule="evenodd"/>
336
+ </svg>
337
+ }
338
+ @default {
339
+ @if (icon() && icon() !== false) {
340
+ {{ icon() }}
341
+ }
342
+ }
343
+ }
344
+ </span>
345
+ }
346
+ <div [class]="contentClasses()">
347
+ <ng-content select="[twToastTitle]" />
348
+ <ng-content select="[twToastDescription]" />
349
+ <ng-content />
350
+ <ng-content select="[twToastAction]" />
351
+ </div>
352
+ @if (dismissible()) {
353
+ <button
354
+ type="button"
355
+ aria-label="Dismiss"
356
+ [class]="dismissClasses()"
357
+ (click)="dismissed.emit()"
358
+ >
359
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-4">
360
+ <path d="M5.28 4.22a.75.75 0 0 0-1.06 1.06L6.94 8l-2.72 2.72a.75.75 0 1 0 1.06 1.06L8 9.06l2.72 2.72a.75.75 0 1 0 1.06-1.06L9.06 8l2.72-2.72a.75.75 0 0 0-1.06-1.06L8 6.94 5.28 4.22Z"/>
361
+ </svg>
362
+ </button>
363
+ }
364
+ `,
365
+ }]
366
+ }], propDecorators: { severity: [{ type: i0.Input, args: [{ isSignal: true, alias: "severity", required: false }] }], dismissible: [{ type: i0.Input, args: [{ isSignal: true, alias: "dismissible", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], dismissed: [{ type: i0.Output, args: ["dismissed"] }], actionClicked: [{ type: i0.Output, args: ["actionClicked"] }], projectedIcon: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ToastIconDirective), { isSignal: true }] }] } });
367
+
368
+ const POSITION_AXIS = {
369
+ 'top-right': 'right',
370
+ 'bottom-right': 'right',
371
+ 'top-left': 'left',
372
+ 'bottom-left': 'left',
373
+ 'top-center': 'top',
374
+ 'bottom-center': 'bottom',
375
+ };
376
+ const POSITION_HOST_CLASSES = {
377
+ 'top-right': 'items-end',
378
+ 'bottom-right': 'items-end',
379
+ 'top-left': 'items-start',
380
+ 'bottom-left': 'items-start',
381
+ 'top-center': 'items-center',
382
+ 'bottom-center': 'items-center',
383
+ };
384
+ const POSITION_ORDER_REVERSED = {
385
+ 'top-right': false,
386
+ 'top-left': false,
387
+ 'top-center': false,
388
+ 'bottom-right': true,
389
+ 'bottom-left': true,
390
+ 'bottom-center': true,
391
+ };
392
+ const SWIPE_DISMISS_FRACTION = 0.4;
393
+ const SWIPE_MAX_OPACITY_FADE = 0.6;
394
+ /**
395
+ * Internal flex-column host rendered inside each per-position CDK overlay.
396
+ * Stacks visible toasts via `@for`, wires pause-on-interaction, Escape
397
+ * dismissal, swipe gestures, and `LiveAnnouncer` announcements.
398
+ *
399
+ * @docs-private
400
+ */
401
+ class ToastContainerComponent {
402
+ /** Per-position stacking anchor. Mutated from {@link ToastService} via `.instance.position.set(...)`. */
403
+ position = signal('bottom-right', ...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
404
+ /** Accessible label applied to the `role="region"` host. Mutated from the service on init. */
405
+ regionLabel = signal('Notifications', ...(ngDevMode ? [{ debugName: "regionLabel" }] : /* istanbul ignore next */ []));
406
+ /**
407
+ * Toasts currently assigned to this container. The service pushes toasts
408
+ * into this signal; the container filters by state to drive `animate.leave`.
409
+ */
410
+ visibleRefs = signal([], ...(ngDevMode ? [{ debugName: "visibleRefs" }] : /* istanbul ignore next */ []));
411
+ injector = inject(Injector);
412
+ liveAnnouncer = inject(LiveAnnouncer);
413
+ host = inject((ElementRef));
414
+ swipeSessions = new WeakMap();
415
+ entries = computed(() => {
416
+ const pos = this.position();
417
+ const axis = POSITION_AXIS[pos];
418
+ const enter = `toast-enter-${axis}`;
419
+ const leave = `toast-leave-${axis}`;
420
+ const refs = this.visibleRefs().filter((ref) => {
421
+ const s = ref.state();
422
+ return s === 'entering' || s === 'visible' || s === 'paused';
423
+ });
424
+ return refs.map((ref) => ({
425
+ ref,
426
+ kind: resolveKind(ref.content()),
427
+ enterClass: enter,
428
+ leaveClass: leave,
429
+ }));
430
+ }, ...(ngDevMode ? [{ debugName: "entries" }] : /* istanbul ignore next */ []));
431
+ orderedEntries = computed(() => {
432
+ const list = this.entries();
433
+ return POSITION_ORDER_REVERSED[this.position()] ? [...list].reverse() : list;
434
+ }, ...(ngDevMode ? [{ debugName: "orderedEntries" }] : /* istanbul ignore next */ []));
435
+ hostClasses = computed(() => {
436
+ const base = 'flex flex-col gap-2 w-full max-w-sm';
437
+ const align = POSITION_HOST_CLASSES[this.position()];
438
+ return `${base} ${align}`;
439
+ }, ...(ngDevMode ? [{ debugName: "hostClasses" }] : /* istanbul ignore next */ []));
440
+ /** @internal Called by the service right after attaching a new toast. Runs `LiveAnnouncer`. */
441
+ _announceOpen(ref) {
442
+ const politeness = this.resolvePoliteness(ref);
443
+ if (politeness === 'off')
444
+ return;
445
+ const msg = this.resolveAnnouncementText(ref);
446
+ if (msg)
447
+ this.liveAnnouncer.announce(msg, politeness);
448
+ }
449
+ /** @internal Called by the service when `ref.update()` fires — re-announces with the new severity / content. */
450
+ _announceUpdate(ref) {
451
+ this._announceOpen(ref);
452
+ }
453
+ /** @internal Captures the component instance after `cdkPortalOutlet` attaches. */
454
+ onPortalAttached(ref, attached) {
455
+ if (attached && 'instance' in attached) {
456
+ ref.componentInstance = attached.instance;
457
+ }
458
+ }
459
+ /** @internal Injector factory for component-class content. Provides `TW_TOAST_DATA` + `TW_TOAST_REF`. */
460
+ _createContentInjector(ref) {
461
+ return Injector.create({
462
+ parent: this.injector,
463
+ providers: [
464
+ { provide: TW_TOAST_REF, useValue: ref },
465
+ { provide: TW_TOAST_DATA, useValue: ref.data() },
466
+ ],
467
+ });
468
+ }
469
+ // ── Template helpers ──
470
+ asString(content) {
471
+ return typeof content === 'string' ? content : '';
472
+ }
473
+ asTemplate(content) {
474
+ return content instanceof TemplateRef ? content : null;
475
+ }
476
+ templateContext(ref) {
477
+ return { $implicit: ref.data(), ref };
478
+ }
479
+ panelClassFor(ref) {
480
+ const raw = ref.config.panelClass;
481
+ if (!raw)
482
+ return '';
483
+ return Array.isArray(raw) ? raw.join(' ') : raw;
484
+ }
485
+ // ── Interaction handlers ──
486
+ onPointerEnter(ref) {
487
+ ref._setHovered(true);
488
+ }
489
+ onPointerLeave(ref) {
490
+ ref._setHovered(false);
491
+ }
492
+ onFocusIn(ref) {
493
+ ref._setFocused(true);
494
+ }
495
+ onFocusOut(ref, event) {
496
+ const related = event.relatedTarget;
497
+ const target = event.currentTarget;
498
+ if (related && target && target.contains(related))
499
+ return;
500
+ ref._setFocused(false);
501
+ }
502
+ onEscape(ref, event) {
503
+ event.stopPropagation();
504
+ event.preventDefault();
505
+ ref._dismissWith('manual');
506
+ }
507
+ onSwipeStart(ref, event, el) {
508
+ if (!ref.config.swipeToDismiss)
509
+ return;
510
+ if (event.button !== 0)
511
+ return;
512
+ const target = event.target;
513
+ if (target?.closest('button, a, input, select, textarea, [role="button"]'))
514
+ return;
515
+ const width = el.getBoundingClientRect().width;
516
+ this.swipeSessions.set(ref, {
517
+ pointerId: event.pointerId,
518
+ startX: event.clientX,
519
+ width,
520
+ active: false,
521
+ });
522
+ el.setPointerCapture(event.pointerId);
523
+ const onMove = (e) => this.onSwipeMove(ref, e, el);
524
+ const onUp = (e) => this.onSwipeEnd(ref, e, el, onMove, onUp);
525
+ el.addEventListener('pointermove', onMove);
526
+ el.addEventListener('pointerup', onUp);
527
+ el.addEventListener('pointercancel', onUp);
528
+ }
529
+ onSwipeMove(ref, event, _el) {
530
+ const session = this.swipeSessions.get(ref);
531
+ if (!session || event.pointerId !== session.pointerId)
532
+ return;
533
+ const dx = event.clientX - session.startX;
534
+ if (!session.active && Math.abs(dx) < 6)
535
+ return;
536
+ session.active = true;
537
+ ref.swipeTransform.set(`translate3d(${dx}px, 0, 0)`);
538
+ const fade = 1 - Math.min(Math.abs(dx) / session.width, 1) * SWIPE_MAX_OPACITY_FADE;
539
+ ref.swipeOpacity.set(fade);
540
+ }
541
+ onSwipeEnd(ref, event, el, onMove, onUp) {
542
+ const session = this.swipeSessions.get(ref);
543
+ el.removeEventListener('pointermove', onMove);
544
+ el.removeEventListener('pointerup', onUp);
545
+ el.removeEventListener('pointercancel', onUp);
546
+ if (!session || event.pointerId !== session.pointerId)
547
+ return;
548
+ try {
549
+ el.releasePointerCapture(session.pointerId);
550
+ }
551
+ catch {
552
+ /* no-op */
553
+ }
554
+ this.swipeSessions.delete(ref);
555
+ if (!session.active)
556
+ return;
557
+ const dx = event.clientX - session.startX;
558
+ const threshold = session.width * SWIPE_DISMISS_FRACTION;
559
+ const allowed = this.swipeDirectionAllowed(dx);
560
+ if (Math.abs(dx) >= threshold && allowed) {
561
+ untracked(() => {
562
+ ref.swipeTransform.set(`translate3d(${Math.sign(dx) * session.width * 1.2}px, 0, 0)`);
563
+ ref.swipeOpacity.set(0);
564
+ ref.leaveAnimationOverride.set('fade-out');
565
+ });
566
+ ref._dismissWith('swipe');
567
+ }
568
+ else {
569
+ ref.swipeTransform.set(null);
570
+ ref.swipeOpacity.set(null);
571
+ }
572
+ }
573
+ swipeDirectionAllowed(dx) {
574
+ const pos = this.position();
575
+ if (pos === 'top-right' || pos === 'bottom-right')
576
+ return dx > 0;
577
+ if (pos === 'top-left' || pos === 'bottom-left')
578
+ return dx < 0;
579
+ return true;
580
+ }
581
+ resolvePoliteness(ref) {
582
+ if (ref.config.politeness)
583
+ return ref.config.politeness;
584
+ return ref.severity() === 'error' ? 'assertive' : 'polite';
585
+ }
586
+ resolveAnnouncementText(ref) {
587
+ const explicit = ref.ariaLabel();
588
+ if (explicit)
589
+ return explicit;
590
+ const content = ref.content();
591
+ if (typeof content === 'string')
592
+ return content;
593
+ const host = this.host.nativeElement.querySelector(`[data-toast-id="${ref.id}"]`);
594
+ if (host)
595
+ return host.textContent?.trim() ?? '';
596
+ return this.host.nativeElement.textContent?.trim() ?? '';
597
+ }
598
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ToastContainerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
599
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: ToastContainerComponent, isStandalone: true, selector: "tw-toast-container", host: { attributes: { "role": "region" }, properties: { "class": "hostClasses()", "attr.aria-label": "regionLabel()", "attr.data-position": "position()" } }, ngImport: i0, template: `
600
+ @for (entry of orderedEntries(); track entry.ref.id) {
601
+ <div
602
+ class="pointer-events-auto w-full max-w-sm"
603
+ [attr.data-toast-id]="entry.ref.id"
604
+ [style.transform]="entry.ref.swipeTransform() || null"
605
+ [style.opacity]="entry.ref.swipeOpacity() ?? null"
606
+ [style.touch-action]="'pan-y'"
607
+ [animate.enter]="entry.enterClass"
608
+ [animate.leave]="entry.ref.leaveAnimationOverride() ?? entry.leaveClass"
609
+ (pointerenter)="onPointerEnter(entry.ref)"
610
+ (pointerleave)="onPointerLeave(entry.ref)"
611
+ (focusin)="onFocusIn(entry.ref)"
612
+ (focusout)="onFocusOut(entry.ref, $event)"
613
+ (keydown.escape)="onEscape(entry.ref, $event)"
614
+ (pointerdown)="onSwipeStart(entry.ref, $event, swipeEl)"
615
+ #swipeEl
616
+ >
617
+ @switch (entry.kind) {
618
+ @case ('string') {
619
+ <tw-toast
620
+ [severity]="entry.ref.severity()"
621
+ [dismissible]="entry.ref.dismissible()"
622
+ [icon]="entry.ref.icon()"
623
+ [ariaLabel]="entry.ref.ariaLabel()"
624
+ [class]="panelClassFor(entry.ref)"
625
+ (dismissed)="entry.ref._dismissWith('manual')"
626
+ (actionClicked)="entry.ref.triggerAction()"
627
+ >
628
+ {{ asString(entry.ref.content()) }}
629
+ @if (entry.ref.action(); as action) {
630
+ <button twToastAction>{{ action.label }}</button>
631
+ }
632
+ </tw-toast>
633
+ }
634
+ @case ('template') {
635
+ <tw-toast
636
+ [severity]="entry.ref.severity()"
637
+ [dismissible]="entry.ref.dismissible()"
638
+ [icon]="entry.ref.icon()"
639
+ [ariaLabel]="entry.ref.ariaLabel()"
640
+ [class]="panelClassFor(entry.ref)"
641
+ (dismissed)="entry.ref._dismissWith('manual')"
642
+ (actionClicked)="entry.ref.triggerAction()"
643
+ >
644
+ <ng-container
645
+ [ngTemplateOutlet]="asTemplate(entry.ref.content())"
646
+ [ngTemplateOutletContext]="templateContext(entry.ref)"
647
+ />
648
+ @if (entry.ref.action(); as action) {
649
+ <button twToastAction>{{ action.label }}</button>
650
+ }
651
+ </tw-toast>
652
+ }
653
+ @case ('component') {
654
+ <ng-template
655
+ [cdkPortalOutlet]="entry.ref._portal"
656
+ (attached)="onPortalAttached(entry.ref, $event)"
657
+ />
658
+ }
659
+ }
660
+ </div>
661
+ }
662
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: CdkPortalOutlet, selector: "[cdkPortalOutlet]", inputs: ["cdkPortalOutlet"], outputs: ["attached"], exportAs: ["cdkPortalOutlet"] }, { kind: "component", type: ToastComponent, selector: "tw-toast", inputs: ["severity", "dismissible", "icon", "ariaLabel"], outputs: ["dismissed", "actionClicked"] }, { kind: "directive", type: ToastActionDirective, selector: "[twToastAction]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
663
+ }
664
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ToastContainerComponent, decorators: [{
665
+ type: Component,
666
+ args: [{
667
+ selector: 'tw-toast-container',
668
+ changeDetection: ChangeDetectionStrategy.OnPush,
669
+ encapsulation: ViewEncapsulation.None,
670
+ imports: [NgTemplateOutlet, CdkPortalOutlet, ToastComponent, ToastActionDirective],
671
+ host: {
672
+ role: 'region',
673
+ '[class]': 'hostClasses()',
674
+ '[attr.aria-label]': 'regionLabel()',
675
+ '[attr.data-position]': 'position()',
676
+ },
677
+ template: `
678
+ @for (entry of orderedEntries(); track entry.ref.id) {
679
+ <div
680
+ class="pointer-events-auto w-full max-w-sm"
681
+ [attr.data-toast-id]="entry.ref.id"
682
+ [style.transform]="entry.ref.swipeTransform() || null"
683
+ [style.opacity]="entry.ref.swipeOpacity() ?? null"
684
+ [style.touch-action]="'pan-y'"
685
+ [animate.enter]="entry.enterClass"
686
+ [animate.leave]="entry.ref.leaveAnimationOverride() ?? entry.leaveClass"
687
+ (pointerenter)="onPointerEnter(entry.ref)"
688
+ (pointerleave)="onPointerLeave(entry.ref)"
689
+ (focusin)="onFocusIn(entry.ref)"
690
+ (focusout)="onFocusOut(entry.ref, $event)"
691
+ (keydown.escape)="onEscape(entry.ref, $event)"
692
+ (pointerdown)="onSwipeStart(entry.ref, $event, swipeEl)"
693
+ #swipeEl
694
+ >
695
+ @switch (entry.kind) {
696
+ @case ('string') {
697
+ <tw-toast
698
+ [severity]="entry.ref.severity()"
699
+ [dismissible]="entry.ref.dismissible()"
700
+ [icon]="entry.ref.icon()"
701
+ [ariaLabel]="entry.ref.ariaLabel()"
702
+ [class]="panelClassFor(entry.ref)"
703
+ (dismissed)="entry.ref._dismissWith('manual')"
704
+ (actionClicked)="entry.ref.triggerAction()"
705
+ >
706
+ {{ asString(entry.ref.content()) }}
707
+ @if (entry.ref.action(); as action) {
708
+ <button twToastAction>{{ action.label }}</button>
709
+ }
710
+ </tw-toast>
711
+ }
712
+ @case ('template') {
713
+ <tw-toast
714
+ [severity]="entry.ref.severity()"
715
+ [dismissible]="entry.ref.dismissible()"
716
+ [icon]="entry.ref.icon()"
717
+ [ariaLabel]="entry.ref.ariaLabel()"
718
+ [class]="panelClassFor(entry.ref)"
719
+ (dismissed)="entry.ref._dismissWith('manual')"
720
+ (actionClicked)="entry.ref.triggerAction()"
721
+ >
722
+ <ng-container
723
+ [ngTemplateOutlet]="asTemplate(entry.ref.content())"
724
+ [ngTemplateOutletContext]="templateContext(entry.ref)"
725
+ />
726
+ @if (entry.ref.action(); as action) {
727
+ <button twToastAction>{{ action.label }}</button>
728
+ }
729
+ </tw-toast>
730
+ }
731
+ @case ('component') {
732
+ <ng-template
733
+ [cdkPortalOutlet]="entry.ref._portal"
734
+ (attached)="onPortalAttached(entry.ref, $event)"
735
+ />
736
+ }
737
+ }
738
+ </div>
739
+ }
740
+ `,
741
+ }]
742
+ }] });
743
+ function resolveKind(content) {
744
+ if (typeof content === 'string')
745
+ return 'string';
746
+ if (content instanceof TemplateRef)
747
+ return 'template';
748
+ return 'component';
749
+ }
750
+
751
+ /** Duration (ms) of the toast enter / leave animation. Must match `_base.css` toast-slide keyframes. */
752
+ const TOAST_ANIMATION_DURATION = 150;
753
+ /** Padding added to animation fallback timers to guarantee cleanup if the CSS event is swallowed. */
754
+ const TOAST_ANIMATION_FALLBACK_PADDING = 50;
755
+ /**
756
+ * Reference to a toast opened via {@link ToastService}. Use to dismiss the
757
+ * toast, pause / resume its auto-dismiss timer, or observe its lifecycle.
758
+ *
759
+ * The service returns a ref synchronously from every open method; the toast
760
+ * itself is attached asynchronously inside the CDK overlay.
761
+ */
762
+ class ToastRef {
763
+ dismissCallback;
764
+ /** Unique id. Generated if `config.id` was not provided. */
765
+ id;
766
+ /** Instance of the component rendered inside the toast (only for component-class content). */
767
+ componentInstance = null;
768
+ /** Resolved static config for this toast (defaults merged, severity shorthand applied). */
769
+ config;
770
+ /** Reactive lifecycle state. */
771
+ state;
772
+ /** Reactive severity — may change after `update()` (used by the `promise()` helper). */
773
+ severity;
774
+ /** Reactive dismissibility. */
775
+ dismissible;
776
+ /** Reactive icon override. */
777
+ icon;
778
+ /** Reactive content (the source of truth for what gets rendered). */
779
+ content;
780
+ /** Reactive action button configuration. */
781
+ action;
782
+ /** Reactive accessible label. */
783
+ ariaLabel;
784
+ /** Reactive data payload — re-read by the container to update `TW_TOAST_DATA`. */
785
+ data;
786
+ /** @internal Reactive "transient" class applied by swipe gestures (e.g. `translate3d(...)`). */
787
+ swipeTransform = signal(null, ...(ngDevMode ? [{ debugName: "swipeTransform" }] : /* istanbul ignore next */ []));
788
+ /** @internal Reactive opacity applied during swipe gestures. */
789
+ swipeOpacity = signal(null, ...(ngDevMode ? [{ debugName: "swipeOpacity" }] : /* istanbul ignore next */ []));
790
+ /** @internal Leave-animation class override. When non-null, container uses this instead of the position default. */
791
+ leaveAnimationOverride = signal(null, ...(ngDevMode ? [{ debugName: "leaveAnimationOverride" }] : /* istanbul ignore next */ []));
792
+ /** @internal Portal for component-class content. Set by the service; null for string / template content. */
793
+ _portal = null;
794
+ /** @internal True while the pointer hovers this toast. */
795
+ _hovered = false;
796
+ /** @internal True while focus is within this toast. */
797
+ _focused = false;
798
+ /** @internal Set by the service so the ref can reach the CDK-owned OverlayRef if it needs to. */
799
+ _overlayPanelElement = null;
800
+ stateSignal = signal('entering', ...(ngDevMode ? [{ debugName: "stateSignal" }] : /* istanbul ignore next */ []));
801
+ severitySignal;
802
+ dismissibleSignal;
803
+ iconSignal;
804
+ contentSignal;
805
+ actionSignal;
806
+ ariaLabelSignal;
807
+ dataSignal;
808
+ afterOpenedSubject = new ReplaySubject(1);
809
+ beforeDismissedSubject = new ReplaySubject(1);
810
+ afterDismissedSubject = new ReplaySubject(1);
811
+ updatedSubject = new Subject();
812
+ dismissReason = 'programmatic';
813
+ dismissResult;
814
+ autoDismissTimer = null;
815
+ autoDismissStartedAt = 0;
816
+ autoDismissRemaining = 0;
817
+ paused = false;
818
+ enterTimer = null;
819
+ leaveTimer = null;
820
+ constructor(id, content, config, dismissCallback) {
821
+ this.dismissCallback = dismissCallback;
822
+ this.id = id;
823
+ this.config = config;
824
+ this.severitySignal = signal(config.severity ?? 'info', ...(ngDevMode ? [{ debugName: "severitySignal" }] : /* istanbul ignore next */ []));
825
+ this.dismissibleSignal = signal(config.dismissible ?? true, ...(ngDevMode ? [{ debugName: "dismissibleSignal" }] : /* istanbul ignore next */ []));
826
+ this.iconSignal = signal(typeof config.icon === 'string' || config.icon === false ? config.icon : undefined, ...(ngDevMode ? [{ debugName: "iconSignal" }] : /* istanbul ignore next */ []));
827
+ this.contentSignal = signal(content, ...(ngDevMode ? [{ debugName: "contentSignal" }] : /* istanbul ignore next */ []));
828
+ this.actionSignal = signal(config.action, ...(ngDevMode ? [{ debugName: "actionSignal" }] : /* istanbul ignore next */ []));
829
+ this.ariaLabelSignal = signal(config.ariaLabel, ...(ngDevMode ? [{ debugName: "ariaLabelSignal" }] : /* istanbul ignore next */ []));
830
+ this.dataSignal = signal(config.data ?? null, ...(ngDevMode ? [{ debugName: "dataSignal" }] : /* istanbul ignore next */ []));
831
+ this.state = this.stateSignal.asReadonly();
832
+ this.severity = this.severitySignal.asReadonly();
833
+ this.dismissible = this.dismissibleSignal.asReadonly();
834
+ this.icon = this.iconSignal.asReadonly();
835
+ this.content = this.contentSignal.asReadonly();
836
+ this.action = this.actionSignal.asReadonly();
837
+ this.ariaLabel = this.ariaLabelSignal.asReadonly();
838
+ this.data = this.dataSignal.asReadonly();
839
+ this.autoDismissRemaining = config.duration ?? 0;
840
+ this.enterTimer = setTimeout(() => this._markVisible(), TOAST_ANIMATION_DURATION + TOAST_ANIMATION_FALLBACK_PADDING);
841
+ }
842
+ /**
843
+ * Dismiss the toast programmatically. The leave animation plays before the
844
+ * element is removed and subscribers are notified. Safe to call multiple times.
845
+ */
846
+ dismiss(result) {
847
+ this._dismissWith('programmatic', result);
848
+ }
849
+ /** Pause the auto-dismiss timer (if any). Called automatically on hover / focus when `pauseOnInteraction` is true. */
850
+ pause() {
851
+ if (this.paused)
852
+ return;
853
+ this.paused = true;
854
+ if (this.autoDismissTimer !== null) {
855
+ const elapsed = Date.now() - this.autoDismissStartedAt;
856
+ this.autoDismissRemaining = Math.max(0, this.autoDismissRemaining - elapsed);
857
+ clearTimeout(this.autoDismissTimer);
858
+ this.autoDismissTimer = null;
859
+ }
860
+ const current = this.stateSignal();
861
+ if (current === 'visible')
862
+ this.stateSignal.set('paused');
863
+ }
864
+ /** Resume the auto-dismiss timer with the remaining time. */
865
+ resume() {
866
+ if (!this.paused)
867
+ return;
868
+ this.paused = false;
869
+ if (this.stateSignal() === 'paused')
870
+ this.stateSignal.set('visible');
871
+ if (this.config.duration && this.config.duration > 0) {
872
+ if (this.autoDismissRemaining <= 0) {
873
+ this._dismissWith('timeout');
874
+ return;
875
+ }
876
+ this._startAutoDismissTimer();
877
+ }
878
+ }
879
+ /**
880
+ * @internal Update the hover state. When `pauseOnInteraction` is set and
881
+ * either hover or focus is active, the auto-dismiss timer is paused.
882
+ */
883
+ _setHovered(hovered) {
884
+ this._hovered = hovered;
885
+ this._syncInteractionPause();
886
+ }
887
+ /** @internal Update the focus-within state. See `_setHovered`. */
888
+ _setFocused(focused) {
889
+ this._focused = focused;
890
+ this._syncInteractionPause();
891
+ }
892
+ _syncInteractionPause() {
893
+ if (!this.config.pauseOnInteraction)
894
+ return;
895
+ if (this._hovered || this._focused) {
896
+ this.pause();
897
+ }
898
+ else {
899
+ this.resume();
900
+ }
901
+ }
902
+ /** Invoke the configured action handler (or dismiss with reason `'action'` if none is set). */
903
+ triggerAction() {
904
+ const action = this.actionSignal();
905
+ if (!action)
906
+ return;
907
+ if (action.handler) {
908
+ action.handler(this);
909
+ }
910
+ else {
911
+ this._dismissWith('action');
912
+ }
913
+ }
914
+ /** Mutate the live toast in place. Used by the `promise()` helper to swap loading → success / error. */
915
+ update(patch) {
916
+ const currentState = this.stateSignal();
917
+ if (currentState === 'dismissing' || currentState === 'dismissed')
918
+ return;
919
+ if (patch.severity !== undefined)
920
+ this.severitySignal.set(patch.severity);
921
+ if (patch.dismissible !== undefined)
922
+ this.dismissibleSignal.set(patch.dismissible);
923
+ if (patch.icon !== undefined)
924
+ this.iconSignal.set(patch.icon);
925
+ if (patch.content !== undefined)
926
+ this.contentSignal.set(patch.content);
927
+ if (patch.action !== undefined)
928
+ this.actionSignal.set(patch.action ?? undefined);
929
+ if (patch.ariaLabel !== undefined)
930
+ this.ariaLabelSignal.set(patch.ariaLabel);
931
+ if (patch.data !== undefined)
932
+ this.dataSignal.set(patch.data);
933
+ if (patch.duration !== undefined) {
934
+ this.config.duration = patch.duration;
935
+ this.autoDismissRemaining = patch.duration;
936
+ if (this.autoDismissTimer !== null) {
937
+ clearTimeout(this.autoDismissTimer);
938
+ this.autoDismissTimer = null;
939
+ }
940
+ if (!this.paused && (currentState === 'visible' || currentState === 'paused')) {
941
+ this._startAutoDismissTimer();
942
+ }
943
+ }
944
+ this.updatedSubject.next(patch);
945
+ }
946
+ /** Observable that emits once after the enter animation completes. */
947
+ afterOpened() {
948
+ return this.afterOpenedSubject.asObservable();
949
+ }
950
+ /** Observable that emits once when the dismiss sequence starts. */
951
+ beforeDismissed() {
952
+ return this.beforeDismissedSubject.asObservable();
953
+ }
954
+ /** Observable that emits once after the leave animation completes and the ref is cleaned up. */
955
+ afterDismissed() {
956
+ return this.afterDismissedSubject.asObservable();
957
+ }
958
+ /** @internal Observable of `update()` calls. The container subscribes to re-announce to `LiveAnnouncer`. */
959
+ _updates() {
960
+ return this.updatedSubject.asObservable();
961
+ }
962
+ /** @internal Called by the service when the max-visible cap is exceeded. */
963
+ _dismissAsMaxExceeded() {
964
+ this._dismissWith('max-exceeded');
965
+ }
966
+ /** @internal Start the dismiss sequence with the given reason. */
967
+ _dismissWith(reason, result) {
968
+ const currentState = this.stateSignal();
969
+ if (currentState === 'dismissing' || currentState === 'dismissed')
970
+ return;
971
+ this.dismissReason = reason;
972
+ this.dismissResult = result;
973
+ this.stateSignal.set('dismissing');
974
+ if (this.autoDismissTimer !== null) {
975
+ clearTimeout(this.autoDismissTimer);
976
+ this.autoDismissTimer = null;
977
+ }
978
+ if (this.enterTimer !== null) {
979
+ clearTimeout(this.enterTimer);
980
+ this.enterTimer = null;
981
+ }
982
+ this.beforeDismissedSubject.next(result);
983
+ this.beforeDismissedSubject.complete();
984
+ this.leaveTimer = setTimeout(() => this._finishDismiss(), TOAST_ANIMATION_DURATION + TOAST_ANIMATION_FALLBACK_PADDING);
985
+ }
986
+ /** @internal Finalise dismissal — clears timers, emits `afterDismissed`, and hands control back to the service. */
987
+ _finishDismiss() {
988
+ if (this.stateSignal() === 'dismissed')
989
+ return;
990
+ this.stateSignal.set('dismissed');
991
+ if (this.leaveTimer !== null) {
992
+ clearTimeout(this.leaveTimer);
993
+ this.leaveTimer = null;
994
+ }
995
+ const dismissal = {
996
+ reason: this.dismissReason,
997
+ result: this.dismissResult,
998
+ };
999
+ this.afterDismissedSubject.next(dismissal);
1000
+ this.afterDismissedSubject.complete();
1001
+ this.updatedSubject.complete();
1002
+ this.dismissCallback(this);
1003
+ }
1004
+ _markVisible() {
1005
+ if (this.enterTimer !== null) {
1006
+ clearTimeout(this.enterTimer);
1007
+ this.enterTimer = null;
1008
+ }
1009
+ const current = this.stateSignal();
1010
+ if (current !== 'entering')
1011
+ return;
1012
+ this.stateSignal.set('visible');
1013
+ this.afterOpenedSubject.next();
1014
+ this.afterOpenedSubject.complete();
1015
+ if (this.config.duration && this.config.duration > 0 && !this.paused) {
1016
+ this._startAutoDismissTimer();
1017
+ }
1018
+ }
1019
+ _startAutoDismissTimer() {
1020
+ if (this.autoDismissRemaining <= 0)
1021
+ return;
1022
+ if (this.autoDismissTimer !== null)
1023
+ clearTimeout(this.autoDismissTimer);
1024
+ this.autoDismissStartedAt = Date.now();
1025
+ this.autoDismissTimer = setTimeout(() => this._dismissWith('timeout'), this.autoDismissRemaining);
1026
+ }
1027
+ }
1028
+
1029
+ const OVERLAY_EDGE_OFFSET = '1rem';
1030
+ let nextToastId = 0;
1031
+ function generateId() {
1032
+ return `tw-toast-${++nextToastId}`;
1033
+ }
1034
+ /**
1035
+ * Opens Tailwind-styled toasts / snackbars inside CDK overlays. One overlay is
1036
+ * created per `ToastPosition` on first use and reused for the lifetime of the
1037
+ * service; toasts stack vertically inside their position container.
1038
+ *
1039
+ * Not `providedIn: 'root'` — register via {@link provideToast}.
1040
+ */
1041
+ class ToastService {
1042
+ overlay = inject(Overlay);
1043
+ injector = inject(Injector);
1044
+ defaultOptions = inject(TW_TOAST_DEFAULT_OPTIONS, { optional: true }) ?? {};
1045
+ parent = inject(ToastService, { optional: true, skipSelf: true });
1046
+ positionOverlays = new Map();
1047
+ activeRefsAtThisLevel = signal([], ...(ngDevMode ? [{ debugName: "activeRefsAtThisLevel" }] : /* istanbul ignore next */ []));
1048
+ afterOpenedSubject = new Subject();
1049
+ afterAllDismissedSubject = new Subject();
1050
+ /** Reactive list of every toast that has not yet fully dismissed. */
1051
+ activeToasts = this.parent
1052
+ ? this.parent.activeToasts
1053
+ : this.activeRefsAtThisLevel.asReadonly();
1054
+ /** Emits every time a new toast is opened. */
1055
+ afterOpened = this.parent
1056
+ ? this.parent.afterOpened
1057
+ : this.afterOpenedSubject.asObservable();
1058
+ /** Emits when the active-toasts list transitions back to empty. Emits immediately on subscribe if none are open. */
1059
+ afterAllDismissed = defer(() => this.activeToasts().length
1060
+ ? this.getAfterAllDismissedSource()
1061
+ : this.getAfterAllDismissedSource().pipe(startWith(undefined)));
1062
+ /**
1063
+ * Open a toast. Content may be a string (rendered inside the default
1064
+ * `ToastComponent`), a `TemplateRef` (rendered via `TemplatePortal`), or a
1065
+ * component class (rendered via `ComponentPortal` with `TW_TOAST_REF` +
1066
+ * `TW_TOAST_DATA` injected).
1067
+ */
1068
+ show(content, config) {
1069
+ const merged = this.resolveConfig(config);
1070
+ return this.openInternal(content, merged);
1071
+ }
1072
+ /** Shorthand for `show(message, { severity: 'success', ... })`. */
1073
+ success(message, config) {
1074
+ return this.openWithSeverity('success', message, config);
1075
+ }
1076
+ /** Shorthand for `show(message, { severity: 'error', politeness: 'assertive', ... })`. */
1077
+ error(message, config) {
1078
+ return this.openWithSeverity('error', message, config, 'assertive');
1079
+ }
1080
+ /** Shorthand for `show(message, { severity: 'warning', ... })`. */
1081
+ warning(message, config) {
1082
+ return this.openWithSeverity('warning', message, config);
1083
+ }
1084
+ /** Shorthand for `show(message, { severity: 'info', ... })`. */
1085
+ info(message, config) {
1086
+ return this.openWithSeverity('info', message, config);
1087
+ }
1088
+ /**
1089
+ * Show a loading toast, then swap it to success / error when the promise
1090
+ * settles. The same ref is returned and re-used across all three states.
1091
+ * The loading toast is pinned (duration forced to 0, dismissible forced to
1092
+ * false) until the promise resolves.
1093
+ */
1094
+ promise(promise, messages, config) {
1095
+ const resolvedDuration = config?.duration ?? this.defaultOptions.duration ?? 5000;
1096
+ const ref = this.show(messages.loading, {
1097
+ ...config,
1098
+ severity: 'neutral',
1099
+ duration: 0,
1100
+ dismissible: false,
1101
+ });
1102
+ promise.then((value) => {
1103
+ if (ref.state() === 'dismissed' || ref.state() === 'dismissing')
1104
+ return;
1105
+ const msg = typeof messages.success === 'function' ? messages.success(value) : messages.success;
1106
+ ref.update({
1107
+ severity: 'success',
1108
+ content: msg,
1109
+ duration: resolvedDuration,
1110
+ dismissible: config?.dismissible ?? true,
1111
+ });
1112
+ this.announceUpdate(ref);
1113
+ }, (err) => {
1114
+ if (ref.state() === 'dismissed' || ref.state() === 'dismissing')
1115
+ return;
1116
+ const msg = typeof messages.error === 'function' ? messages.error(err) : messages.error;
1117
+ ref.update({
1118
+ severity: 'error',
1119
+ content: msg,
1120
+ duration: resolvedDuration,
1121
+ dismissible: config?.dismissible ?? true,
1122
+ });
1123
+ this.announceUpdate(ref);
1124
+ });
1125
+ return ref;
1126
+ }
1127
+ /** Dismiss a toast by id. No-op if no matching toast is active. */
1128
+ dismiss(id) {
1129
+ const match = this.activeToasts().find((ref) => ref.id === id);
1130
+ if (match)
1131
+ match.dismiss();
1132
+ }
1133
+ /** Dismiss every active toast across every position. */
1134
+ dismissAll() {
1135
+ const refs = [...this.activeToasts()];
1136
+ for (let i = refs.length - 1; i >= 0; i--)
1137
+ refs[i].dismiss();
1138
+ }
1139
+ /** Look up an active toast by id. */
1140
+ getToastById(id) {
1141
+ return this.activeToasts().find((ref) => ref.id === id);
1142
+ }
1143
+ ngOnDestroy() {
1144
+ const refs = [...this.activeRefsAtThisLevel()];
1145
+ for (let i = refs.length - 1; i >= 0; i--)
1146
+ refs[i]._finishDismiss();
1147
+ for (const { overlayRef } of this.positionOverlays.values()) {
1148
+ overlayRef.dispose();
1149
+ }
1150
+ this.positionOverlays.clear();
1151
+ this.afterOpenedSubject.complete();
1152
+ this.afterAllDismissedSubject.complete();
1153
+ }
1154
+ openWithSeverity(severity, message, config, forcedPoliteness) {
1155
+ const merged = this.resolveConfig(config);
1156
+ merged.severity = severity;
1157
+ if (forcedPoliteness && !config?.politeness)
1158
+ merged.politeness = forcedPoliteness;
1159
+ return this.openInternal(message, merged);
1160
+ }
1161
+ openInternal(content, config) {
1162
+ const id = config.id ?? generateId();
1163
+ config.id = id;
1164
+ const ref = new ToastRef(id, content, config, (dismissed) => this.handleDismissed(dismissed));
1165
+ if (isComponentConstructor(content)) {
1166
+ const injector = this.getContainerForPosition(config.position).containerRef.instance._createContentInjector(ref);
1167
+ ref._portal = new ComponentPortal(content, null, injector);
1168
+ }
1169
+ this.enforceMaxVisible(config.position, config.maxVisible ?? 5);
1170
+ this.registerOpen(ref);
1171
+ this.attachToContainer(ref, config.position);
1172
+ this.announceOpen(ref);
1173
+ return ref;
1174
+ }
1175
+ enforceMaxVisible(position, max) {
1176
+ if (max <= 0)
1177
+ return;
1178
+ const atPosition = this.activeToasts().filter((ref) => (ref.config.position ?? 'bottom-right') === position &&
1179
+ ref.state() !== 'dismissing' &&
1180
+ ref.state() !== 'dismissed');
1181
+ while (atPosition.length >= max) {
1182
+ const oldest = atPosition.shift();
1183
+ if (!oldest)
1184
+ break;
1185
+ oldest._dismissAsMaxExceeded();
1186
+ }
1187
+ }
1188
+ registerOpen(ref) {
1189
+ const scope = this.parent ?? this;
1190
+ const anyRef = ref;
1191
+ scope.activeRefsAtThisLevel.update((list) => [...list, anyRef]);
1192
+ scope.afterOpenedSubject.next(anyRef);
1193
+ }
1194
+ handleDismissed(ref) {
1195
+ const scope = this.parent ?? this;
1196
+ scope.activeRefsAtThisLevel.update((list) => list.filter((r) => r !== ref));
1197
+ const position = ref.config.position ?? 'bottom-right';
1198
+ const entry = this.positionOverlays.get(position);
1199
+ if (entry) {
1200
+ const instance = entry.containerRef.instance;
1201
+ instance.visibleRefs.update((list) => list.filter((r) => r !== ref));
1202
+ }
1203
+ if (scope.activeRefsAtThisLevel().length === 0) {
1204
+ scope.afterAllDismissedSubject.next();
1205
+ }
1206
+ }
1207
+ attachToContainer(ref, position) {
1208
+ const entry = this.getContainerForPosition(position);
1209
+ const instance = entry.containerRef.instance;
1210
+ const anyRef = ref;
1211
+ instance.visibleRefs.update((list) => [...list, anyRef]);
1212
+ ref._overlayPanelElement = entry.overlayRef.overlayElement;
1213
+ }
1214
+ announceOpen(ref) {
1215
+ const entry = this.positionOverlays.get(ref.config.position ?? 'bottom-right');
1216
+ entry?.containerRef.instance._announceOpen(ref);
1217
+ }
1218
+ announceUpdate(ref) {
1219
+ const entry = this.positionOverlays.get(ref.config.position ?? 'bottom-right');
1220
+ entry?.containerRef.instance._announceUpdate(ref);
1221
+ }
1222
+ getContainerForPosition(position) {
1223
+ const existing = this.positionOverlays.get(position);
1224
+ if (existing)
1225
+ return existing;
1226
+ const overlayRef = this.overlay.create({
1227
+ positionStrategy: this.buildPositionStrategy(position),
1228
+ scrollStrategy: this.overlay.scrollStrategies.noop(),
1229
+ hasBackdrop: false,
1230
+ panelClass: ['tw-toast-overlay', `tw-toast-overlay-${position}`],
1231
+ });
1232
+ const containerPortal = new ComponentPortal(ToastContainerComponent, null, this.injector);
1233
+ const containerRef = overlayRef.attach(containerPortal);
1234
+ containerRef.instance.position.set(position);
1235
+ containerRef.instance.regionLabel.set(this.defaultOptions.regionAriaLabel ?? 'Notifications');
1236
+ const entry = { overlayRef, containerRef };
1237
+ this.positionOverlays.set(position, entry);
1238
+ return entry;
1239
+ }
1240
+ buildPositionStrategy(position) {
1241
+ const strategy = createGlobalPositionStrategy(this.injector);
1242
+ switch (position) {
1243
+ case 'top-right':
1244
+ strategy.top(OVERLAY_EDGE_OFFSET).right(OVERLAY_EDGE_OFFSET);
1245
+ break;
1246
+ case 'top-left':
1247
+ strategy.top(OVERLAY_EDGE_OFFSET).left(OVERLAY_EDGE_OFFSET);
1248
+ break;
1249
+ case 'top-center':
1250
+ strategy.top(OVERLAY_EDGE_OFFSET).centerHorizontally();
1251
+ break;
1252
+ case 'bottom-right':
1253
+ strategy.bottom(OVERLAY_EDGE_OFFSET).right(OVERLAY_EDGE_OFFSET);
1254
+ break;
1255
+ case 'bottom-left':
1256
+ strategy.bottom(OVERLAY_EDGE_OFFSET).left(OVERLAY_EDGE_OFFSET);
1257
+ break;
1258
+ case 'bottom-center':
1259
+ strategy.bottom(OVERLAY_EDGE_OFFSET).centerHorizontally();
1260
+ break;
1261
+ }
1262
+ return strategy;
1263
+ }
1264
+ resolveConfig(config) {
1265
+ const merged = new ToastConfig();
1266
+ Object.assign(merged, this.defaultOptions, config);
1267
+ return merged;
1268
+ }
1269
+ getAfterAllDismissedSource() {
1270
+ return this.parent ? this.parent.getAfterAllDismissedSource() : this.afterAllDismissedSubject;
1271
+ }
1272
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ToastService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1273
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ToastService });
1274
+ }
1275
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: ToastService, decorators: [{
1276
+ type: Injectable
1277
+ }] });
1278
+ function isComponentConstructor(value) {
1279
+ return typeof value === 'function' && !(value instanceof TemplateRef);
1280
+ }
1281
+ /**
1282
+ * Registers {@link ToastService} for dependency injection and installs optional
1283
+ * application-wide defaults.
1284
+ *
1285
+ * @example
1286
+ * ```ts
1287
+ * bootstrapApplication(AppComponent, {
1288
+ * providers: [provideToast({ position: 'top-right', duration: 4000 })]
1289
+ * });
1290
+ * ```
1291
+ */
1292
+ function provideToast(defaultOptions) {
1293
+ const providers = [ToastService];
1294
+ if (defaultOptions) {
1295
+ providers.push({ provide: TW_TOAST_DEFAULT_OPTIONS, useValue: defaultOptions });
1296
+ }
1297
+ return makeEnvironmentProviders(providers);
1298
+ }
1299
+ const ToastInternals = {
1300
+ /** @internal Reset id counter for tests. */
1301
+ _resetIdForTesting() {
1302
+ nextToastId = 0;
1303
+ },
1304
+ };
1305
+
1306
+ /**
1307
+ * Generated bundle index. Do not edit.
1308
+ */
1309
+
1310
+ export { TW_TOAST_DATA, TW_TOAST_DEFAULT_OPTIONS, TW_TOAST_REF, ToastActionDirective, ToastComponent, ToastConfig, ToastDescriptionDirective, ToastIconDirective, ToastRef, ToastService, ToastTitleDirective, provideToast };
1311
+ //# sourceMappingURL=cdevhub-ngx-tw-toast.mjs.map