@cdevhub/ngx-tw 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/README.md +78 -15
  2. package/fesm2022/cdevhub-ngx-tw-accordion.mjs +16 -5
  3. package/fesm2022/cdevhub-ngx-tw-accordion.mjs.map +1 -1
  4. package/fesm2022/cdevhub-ngx-tw-avatar.mjs +26 -15
  5. package/fesm2022/cdevhub-ngx-tw-avatar.mjs.map +1 -1
  6. package/fesm2022/cdevhub-ngx-tw-badge.mjs +17 -5
  7. package/fesm2022/cdevhub-ngx-tw-badge.mjs.map +1 -1
  8. package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs +27 -6
  9. package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs.map +1 -1
  10. package/fesm2022/cdevhub-ngx-tw-button.mjs +23 -11
  11. package/fesm2022/cdevhub-ngx-tw-button.mjs.map +1 -1
  12. package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs +5 -5
  13. package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs.map +1 -1
  14. package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs +3 -3
  15. package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs.map +1 -1
  16. package/fesm2022/cdevhub-ngx-tw-calendar.mjs +427 -122
  17. package/fesm2022/cdevhub-ngx-tw-calendar.mjs.map +1 -1
  18. package/fesm2022/cdevhub-ngx-tw-card.mjs +21 -11
  19. package/fesm2022/cdevhub-ngx-tw-card.mjs.map +1 -1
  20. package/fesm2022/cdevhub-ngx-tw-carousel.mjs +215 -96
  21. package/fesm2022/cdevhub-ngx-tw-carousel.mjs.map +1 -1
  22. package/fesm2022/cdevhub-ngx-tw-checkbox.mjs +108 -64
  23. package/fesm2022/cdevhub-ngx-tw-checkbox.mjs.map +1 -1
  24. package/fesm2022/cdevhub-ngx-tw-code-block.mjs +18 -6
  25. package/fesm2022/cdevhub-ngx-tw-code-block.mjs.map +1 -1
  26. package/fesm2022/cdevhub-ngx-tw-collapsible.mjs +55 -30
  27. package/fesm2022/cdevhub-ngx-tw-collapsible.mjs.map +1 -1
  28. package/fesm2022/cdevhub-ngx-tw-combobox-testing.mjs +218 -0
  29. package/fesm2022/cdevhub-ngx-tw-combobox-testing.mjs.map +1 -0
  30. package/fesm2022/cdevhub-ngx-tw-combobox.mjs +236 -211
  31. package/fesm2022/cdevhub-ngx-tw-combobox.mjs.map +1 -1
  32. package/fesm2022/cdevhub-ngx-tw-command-palette-testing.mjs +200 -0
  33. package/fesm2022/cdevhub-ngx-tw-command-palette-testing.mjs.map +1 -0
  34. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs +34 -7
  35. package/fesm2022/cdevhub-ngx-tw-command-palette.mjs.map +1 -1
  36. package/fesm2022/cdevhub-ngx-tw-core.mjs +135 -15
  37. package/fesm2022/cdevhub-ngx-tw-core.mjs.map +1 -1
  38. package/fesm2022/cdevhub-ngx-tw-date-picker-testing.mjs +203 -0
  39. package/fesm2022/cdevhub-ngx-tw-date-picker-testing.mjs.map +1 -0
  40. package/fesm2022/cdevhub-ngx-tw-date-picker.mjs +160 -102
  41. package/fesm2022/cdevhub-ngx-tw-date-picker.mjs.map +1 -1
  42. package/fesm2022/cdevhub-ngx-tw-date-range-picker-testing.mjs +209 -0
  43. package/fesm2022/cdevhub-ngx-tw-date-range-picker-testing.mjs.map +1 -0
  44. package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs +182 -107
  45. package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs.map +1 -1
  46. package/fesm2022/cdevhub-ngx-tw-dialog-dialog-renderer-DoIhoV3d.mjs +198 -0
  47. package/fesm2022/cdevhub-ngx-tw-dialog-dialog-renderer-DoIhoV3d.mjs.map +1 -0
  48. package/fesm2022/cdevhub-ngx-tw-dialog-testing.mjs +122 -0
  49. package/fesm2022/cdevhub-ngx-tw-dialog-testing.mjs.map +1 -0
  50. package/fesm2022/cdevhub-ngx-tw-dialog.mjs +199 -220
  51. package/fesm2022/cdevhub-ngx-tw-dialog.mjs.map +1 -1
  52. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs +1 -1
  53. package/fesm2022/cdevhub-ngx-tw-empty-state.mjs.map +1 -1
  54. package/fesm2022/cdevhub-ngx-tw-file-upload-testing.mjs +149 -0
  55. package/fesm2022/cdevhub-ngx-tw-file-upload-testing.mjs.map +1 -0
  56. package/fesm2022/cdevhub-ngx-tw-file-upload.mjs +57 -67
  57. package/fesm2022/cdevhub-ngx-tw-file-upload.mjs.map +1 -1
  58. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs +42 -24
  59. package/fesm2022/cdevhub-ngx-tw-flip-card.mjs.map +1 -1
  60. package/fesm2022/cdevhub-ngx-tw-form-field.mjs +114 -21
  61. package/fesm2022/cdevhub-ngx-tw-form-field.mjs.map +1 -1
  62. package/fesm2022/cdevhub-ngx-tw-icon.mjs +16 -6
  63. package/fesm2022/cdevhub-ngx-tw-icon.mjs.map +1 -1
  64. package/fesm2022/cdevhub-ngx-tw-input.mjs +52 -61
  65. package/fesm2022/cdevhub-ngx-tw-input.mjs.map +1 -1
  66. package/fesm2022/cdevhub-ngx-tw-item.mjs +18 -3
  67. package/fesm2022/cdevhub-ngx-tw-item.mjs.map +1 -1
  68. package/fesm2022/cdevhub-ngx-tw-menu-testing.mjs +168 -0
  69. package/fesm2022/cdevhub-ngx-tw-menu-testing.mjs.map +1 -0
  70. package/fesm2022/cdevhub-ngx-tw-menu.mjs +58 -21
  71. package/fesm2022/cdevhub-ngx-tw-menu.mjs.map +1 -1
  72. package/fesm2022/cdevhub-ngx-tw-number-input.mjs +34 -1
  73. package/fesm2022/cdevhub-ngx-tw-number-input.mjs.map +1 -1
  74. package/fesm2022/cdevhub-ngx-tw-paginator.mjs +95 -45
  75. package/fesm2022/cdevhub-ngx-tw-paginator.mjs.map +1 -1
  76. package/fesm2022/cdevhub-ngx-tw-popover.mjs +81 -32
  77. package/fesm2022/cdevhub-ngx-tw-popover.mjs.map +1 -1
  78. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs +1 -1
  79. package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs.map +1 -1
  80. package/fesm2022/cdevhub-ngx-tw-radio.mjs +131 -98
  81. package/fesm2022/cdevhub-ngx-tw-radio.mjs.map +1 -1
  82. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs +207 -42
  83. package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs.map +1 -1
  84. package/fesm2022/cdevhub-ngx-tw-select-testing.mjs +165 -0
  85. package/fesm2022/cdevhub-ngx-tw-select-testing.mjs.map +1 -0
  86. package/fesm2022/cdevhub-ngx-tw-select.mjs +386 -210
  87. package/fesm2022/cdevhub-ngx-tw-select.mjs.map +1 -1
  88. package/fesm2022/cdevhub-ngx-tw-sheet-sheet-renderer-CqVJwgmp.mjs +256 -0
  89. package/fesm2022/cdevhub-ngx-tw-sheet-sheet-renderer-CqVJwgmp.mjs.map +1 -0
  90. package/fesm2022/cdevhub-ngx-tw-sheet-testing.mjs +129 -0
  91. package/fesm2022/cdevhub-ngx-tw-sheet-testing.mjs.map +1 -0
  92. package/fesm2022/cdevhub-ngx-tw-sheet.mjs +197 -278
  93. package/fesm2022/cdevhub-ngx-tw-sheet.mjs.map +1 -1
  94. package/fesm2022/cdevhub-ngx-tw-slider.mjs +89 -54
  95. package/fesm2022/cdevhub-ngx-tw-slider.mjs.map +1 -1
  96. package/fesm2022/cdevhub-ngx-tw-sort.mjs +122 -17
  97. package/fesm2022/cdevhub-ngx-tw-sort.mjs.map +1 -1
  98. package/fesm2022/cdevhub-ngx-tw-split.mjs +72 -19
  99. package/fesm2022/cdevhub-ngx-tw-split.mjs.map +1 -1
  100. package/fesm2022/cdevhub-ngx-tw-stat.mjs +63 -25
  101. package/fesm2022/cdevhub-ngx-tw-stat.mjs.map +1 -1
  102. package/fesm2022/cdevhub-ngx-tw-stepper.mjs +35 -8
  103. package/fesm2022/cdevhub-ngx-tw-stepper.mjs.map +1 -1
  104. package/fesm2022/cdevhub-ngx-tw-switch.mjs +54 -48
  105. package/fesm2022/cdevhub-ngx-tw-switch.mjs.map +1 -1
  106. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs +12 -15
  107. package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs.map +1 -1
  108. package/fesm2022/cdevhub-ngx-tw-table.mjs +74 -13
  109. package/fesm2022/cdevhub-ngx-tw-table.mjs.map +1 -1
  110. package/fesm2022/cdevhub-ngx-tw-tabs.mjs +84 -30
  111. package/fesm2022/cdevhub-ngx-tw-tabs.mjs.map +1 -1
  112. package/fesm2022/cdevhub-ngx-tw-tags-input-testing.mjs +172 -0
  113. package/fesm2022/cdevhub-ngx-tw-tags-input-testing.mjs.map +1 -0
  114. package/fesm2022/cdevhub-ngx-tw-tags-input.mjs +107 -71
  115. package/fesm2022/cdevhub-ngx-tw-tags-input.mjs.map +1 -1
  116. package/fesm2022/cdevhub-ngx-tw-textarea.mjs +41 -9
  117. package/fesm2022/cdevhub-ngx-tw-textarea.mjs.map +1 -1
  118. package/fesm2022/cdevhub-ngx-tw-theme.mjs +257 -45
  119. package/fesm2022/cdevhub-ngx-tw-theme.mjs.map +1 -1
  120. package/fesm2022/cdevhub-ngx-tw-time-picker-testing.mjs +201 -0
  121. package/fesm2022/cdevhub-ngx-tw-time-picker-testing.mjs.map +1 -0
  122. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs +184 -77
  123. package/fesm2022/cdevhub-ngx-tw-time-picker.mjs.map +1 -1
  124. package/fesm2022/cdevhub-ngx-tw-timeline.mjs +110 -27
  125. package/fesm2022/cdevhub-ngx-tw-timeline.mjs.map +1 -1
  126. package/fesm2022/cdevhub-ngx-tw-toast-toast-renderer-BlDxemsU.mjs +616 -0
  127. package/fesm2022/cdevhub-ngx-tw-toast-toast-renderer-BlDxemsU.mjs.map +1 -0
  128. package/fesm2022/cdevhub-ngx-tw-toast.mjs +556 -945
  129. package/fesm2022/cdevhub-ngx-tw-toast.mjs.map +1 -1
  130. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs +102 -15
  131. package/fesm2022/cdevhub-ngx-tw-tooltip.mjs.map +1 -1
  132. package/fesm2022/cdevhub-ngx-tw-transfer-testing.mjs +219 -0
  133. package/fesm2022/cdevhub-ngx-tw-transfer-testing.mjs.map +1 -0
  134. package/fesm2022/cdevhub-ngx-tw-transfer.mjs +88 -69
  135. package/fesm2022/cdevhub-ngx-tw-transfer.mjs.map +1 -1
  136. package/fesm2022/cdevhub-ngx-tw-tree.mjs +68 -19
  137. package/fesm2022/cdevhub-ngx-tw-tree.mjs.map +1 -1
  138. package/index.json +1 -1
  139. package/package.json +63 -2
  140. package/theme/_base.css +13 -0
  141. package/theme/_dark.css +110 -36
  142. package/theme/_high-contrast-dark.css +367 -0
  143. package/theme/_high-contrast.css +13 -2
  144. package/theme/_light.css +276 -0
  145. package/theme/_semantic.css +47 -12
  146. package/theme/_typography.css +17 -0
  147. package/theme/index.css +86 -7
  148. package/types/cdevhub-ngx-tw-accordion.d.ts +12 -4
  149. package/types/cdevhub-ngx-tw-avatar.d.ts +10 -3
  150. package/types/cdevhub-ngx-tw-button.d.ts +1 -0
  151. package/types/cdevhub-ngx-tw-calendar-luxon.d.ts +2 -2
  152. package/types/cdevhub-ngx-tw-calendar-testing.d.ts +1 -1
  153. package/types/cdevhub-ngx-tw-calendar.d.ts +179 -73
  154. package/types/cdevhub-ngx-tw-card.d.ts +11 -3
  155. package/types/cdevhub-ngx-tw-carousel.d.ts +89 -20
  156. package/types/cdevhub-ngx-tw-checkbox.d.ts +40 -12
  157. package/types/cdevhub-ngx-tw-code-block.d.ts +11 -2
  158. package/types/cdevhub-ngx-tw-collapsible.d.ts +32 -5
  159. package/types/cdevhub-ngx-tw-combobox-testing.d.ts +157 -0
  160. package/types/cdevhub-ngx-tw-combobox.d.ts +94 -53
  161. package/types/cdevhub-ngx-tw-command-palette-testing.d.ts +136 -0
  162. package/types/cdevhub-ngx-tw-command-palette.d.ts +33 -5
  163. package/types/cdevhub-ngx-tw-core.d.ts +152 -21
  164. package/types/cdevhub-ngx-tw-date-picker-testing.d.ts +141 -0
  165. package/types/cdevhub-ngx-tw-date-picker.d.ts +37 -29
  166. package/types/cdevhub-ngx-tw-date-range-picker-testing.d.ts +148 -0
  167. package/types/cdevhub-ngx-tw-date-range-picker.d.ts +36 -29
  168. package/types/cdevhub-ngx-tw-dialog-testing.d.ts +98 -0
  169. package/types/cdevhub-ngx-tw-dialog.d.ts +89 -19
  170. package/types/cdevhub-ngx-tw-file-upload-testing.d.ts +120 -0
  171. package/types/cdevhub-ngx-tw-file-upload.d.ts +8 -10
  172. package/types/cdevhub-ngx-tw-flip-card.d.ts +18 -11
  173. package/types/cdevhub-ngx-tw-form-field.d.ts +19 -2
  174. package/types/cdevhub-ngx-tw-icon.d.ts +3 -3
  175. package/types/cdevhub-ngx-tw-input.d.ts +5 -8
  176. package/types/cdevhub-ngx-tw-item.d.ts +2 -0
  177. package/types/cdevhub-ngx-tw-menu-testing.d.ts +131 -0
  178. package/types/cdevhub-ngx-tw-menu.d.ts +2 -2
  179. package/types/cdevhub-ngx-tw-number-input.d.ts +20 -0
  180. package/types/cdevhub-ngx-tw-paginator.d.ts +58 -24
  181. package/types/cdevhub-ngx-tw-popover.d.ts +41 -10
  182. package/types/cdevhub-ngx-tw-progress-bar.d.ts +1 -1
  183. package/types/cdevhub-ngx-tw-radio.d.ts +68 -25
  184. package/types/cdevhub-ngx-tw-segmented-control.d.ts +79 -16
  185. package/types/cdevhub-ngx-tw-select-testing.d.ts +114 -0
  186. package/types/cdevhub-ngx-tw-select.d.ts +104 -33
  187. package/types/cdevhub-ngx-tw-sheet-testing.d.ts +104 -0
  188. package/types/cdevhub-ngx-tw-sheet.d.ts +96 -21
  189. package/types/cdevhub-ngx-tw-slider.d.ts +79 -25
  190. package/types/cdevhub-ngx-tw-sort.d.ts +45 -4
  191. package/types/cdevhub-ngx-tw-split.d.ts +32 -2
  192. package/types/cdevhub-ngx-tw-stat.d.ts +14 -4
  193. package/types/cdevhub-ngx-tw-stepper.d.ts +16 -2
  194. package/types/cdevhub-ngx-tw-switch.d.ts +35 -13
  195. package/types/cdevhub-ngx-tw-tab-nav.d.ts +7 -6
  196. package/types/cdevhub-ngx-tw-table.d.ts +53 -10
  197. package/types/cdevhub-ngx-tw-tabs.d.ts +41 -10
  198. package/types/cdevhub-ngx-tw-tags-input-testing.d.ts +135 -0
  199. package/types/cdevhub-ngx-tw-tags-input.d.ts +26 -11
  200. package/types/cdevhub-ngx-tw-textarea.d.ts +3 -2
  201. package/types/cdevhub-ngx-tw-theme.d.ts +245 -39
  202. package/types/cdevhub-ngx-tw-time-picker-testing.d.ts +129 -0
  203. package/types/cdevhub-ngx-tw-time-picker.d.ts +76 -19
  204. package/types/cdevhub-ngx-tw-timeline.d.ts +36 -12
  205. package/types/cdevhub-ngx-tw-toast.d.ts +61 -12
  206. package/types/cdevhub-ngx-tw-tooltip.d.ts +28 -6
  207. package/types/cdevhub-ngx-tw-transfer-testing.d.ts +152 -0
  208. package/types/cdevhub-ngx-tw-transfer.d.ts +21 -8
  209. package/types/cdevhub-ngx-tw-tree.d.ts +6 -1
@@ -1,10 +1,6 @@
1
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';
2
+ import { InjectionToken, signal, inject, Injector, Injectable, makeEnvironmentProviders, Directive, input, output, contentChild, computed, ChangeDetectionStrategy, Component } from '@angular/core';
5
3
  import { ReplaySubject, Subject, defer, startWith } from 'rxjs';
6
- import { NgTemplateOutlet } from '@angular/common';
7
- import { LiveAnnouncer } from '@angular/cdk/a11y';
8
4
  import { tv } from 'tailwind-variants';
9
5
 
10
6
  /**
@@ -19,7 +15,16 @@ class ToastConfig {
19
15
  severity = 'info';
20
16
  /** Screen position of the toast stack. Defaults to `'bottom-right'`. */
21
17
  position = 'bottom-right';
22
- /** Auto-dismiss duration in ms. `0` disables auto-dismiss. Defaults to `5000`. */
18
+ /**
19
+ * Auto-dismiss duration in ms. `0` disables auto-dismiss. Defaults to `5000`.
20
+ *
21
+ * The duration alone would fail SC 2.2.1 (Timing Adjustable) — a time limit the
22
+ * user cannot influence. What makes 5s conformant is `pauseOnInteraction`
23
+ * (default `true`): the clock stops while the pointer is over the toast or
24
+ * focus is anywhere inside it, and resumes on leave / blur. Setting
25
+ * `pauseOnInteraction: false` alongside a non-zero duration re-introduces the
26
+ * failure, so pair it with `duration: 0`.
27
+ */
23
28
  duration = 5000;
24
29
  // Default true: toasts must offer an escape hatch — sticky / long-duration toasts
25
30
  // otherwise trap users; opt-out only for ephemeral progress markers paired with `duration`.
@@ -46,9 +51,11 @@ class ToastConfig {
46
51
  * primitives for the string-content path.
47
52
  */
48
53
  icon;
49
- // Default true: matches Material/Radix/Sonner conventions prevents a toast from
50
- // disappearing mid-read; opt-out only for fully ambient progress indicators.
51
- /** When true, pause the auto-dismiss timer while the toast is hovered or holds focus. Defaults to `true`. */
54
+ // Default true: this is what keeps a non-zero `duration` conformant with
55
+ // SC 2.2.1 (Timing Adjustable) it is the user's control over the time limit,
56
+ // not a nicety. Also matches Material/Radix/Sonner. Opt out only together with
57
+ // `duration: 0`, for fully ambient progress indicators.
58
+ /** When true, pause the auto-dismiss timer while the toast is hovered or holds focus anywhere inside it, resuming on leave / blur. Defaults to `true`. */
52
59
  pauseOnInteraction = true;
53
60
  // Default true: matches mobile-platform expectations and gives non-precise pointers
54
61
  // (touch) an easy dismiss gesture; opt-out only if the gesture conflicts with content.
@@ -73,886 +80,194 @@ const TW_TOAST_REF = new InjectionToken('TW_TOAST_REF');
73
80
  /** Injection token carrying application-wide toast defaults. Set via `provideToast(defaults)`. */
74
81
  const TW_TOAST_DEFAULT_OPTIONS = new InjectionToken('TW_TOAST_DEFAULT_OPTIONS');
75
82
 
76
- const toastVariants = tv({
77
- slots: {
78
- 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',
79
- icon: 'size-5 shrink-0 mt-0.5',
80
- title: 'text-sm font-semibold',
81
- description: 'text-sm',
82
- content: 'flex-1 min-w-0',
83
- 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',
84
- // Dismiss container is `size-6` (24px — xs square-interactive-target
85
- // scale per CLAUDE.md icon sub-scale). The inner SVG is `size-4` (16px
86
- // glyph scale), centred via flex; the larger container gives the
87
- // touch/click affordance without enlarging the visual glyph.
88
- 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',
89
- },
90
- variants: {
91
- severity: {
92
- info: {},
93
- success: {},
94
- warning: {},
95
- error: {},
96
- neutral: {},
97
- },
98
- },
99
- // Color slot tokens (`{role}-soft`, `-soft-fg`, `-icon`, …) own light/dark
100
- // contrast — components consume the slot only. See `_semantic.css`.
101
- // Slot-token safelist: see `projects/ngx-tw/theme/index.css` `@source inline(...)`
102
- // — Tailwind v4's JIT cannot scan template-literal class names, so every
103
- // `bg-${severity}-soft` / `text-${severity}-icon` permutation below must be
104
- // enumerated there.
105
- compoundVariants: ['info', 'success', 'warning', 'error', 'neutral'].map((severity) => ({
106
- severity,
107
- class: {
108
- root: `bg-${severity}-soft text-${severity}-soft-fg-muted border-${severity}-border`,
109
- icon: `text-${severity}-icon`,
110
- title: `text-${severity}-soft-fg`,
111
- description: `text-${severity}-soft-fg-muted`,
112
- action: `text-${severity}-soft-fg hover:bg-${severity}-soft-hover`,
113
- dismiss: `text-${severity}-icon hover:bg-${severity}-soft-hover`,
114
- },
115
- })),
116
- defaultVariants: {
117
- severity: 'info',
118
- },
119
- }, { twMerge: true });
120
- /** Slot directive for the icon. When projected, overrides the severity-default icon. */
121
- class ToastIconDirective {
122
- toast = inject(ToastComponent);
123
- /** @internal */
124
- classes = this.toast.iconClasses;
125
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastIconDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
126
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.7", type: ToastIconDirective, isStandalone: true, selector: "[twToastIcon]", host: { attributes: { "aria-hidden": "true" }, properties: { "class": "classes()" } }, ngImport: i0 });
127
- }
128
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastIconDirective, decorators: [{
129
- type: Directive,
130
- args: [{
131
- selector: '[twToastIcon]',
132
- host: {
133
- '[class]': 'classes()',
134
- 'aria-hidden': 'true',
135
- },
136
- }]
137
- }] });
138
- /** Slot directive for the bold title line inside a toast. */
139
- class ToastTitleDirective {
140
- toast = inject(ToastComponent);
141
- /** @internal */
142
- classes = this.toast.titleClasses;
143
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastTitleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
144
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.7", type: ToastTitleDirective, isStandalone: true, selector: "[twToastTitle]", host: { properties: { "class": "classes()" } }, ngImport: i0 });
145
- }
146
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastTitleDirective, decorators: [{
147
- type: Directive,
148
- args: [{
149
- selector: '[twToastTitle]',
150
- host: {
151
- '[class]': 'classes()',
152
- },
153
- }]
154
- }] });
155
- /** Slot directive for a secondary description line inside a toast. */
156
- class ToastDescriptionDirective {
157
- toast = inject(ToastComponent);
158
- /** @internal */
159
- classes = this.toast.descriptionClasses;
160
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastDescriptionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
161
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.7", type: ToastDescriptionDirective, isStandalone: true, selector: "[twToastDescription]", host: { properties: { "class": "classes()" } }, ngImport: i0 });
162
- }
163
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastDescriptionDirective, decorators: [{
164
- type: Directive,
165
- args: [{
166
- selector: '[twToastDescription]',
167
- host: {
168
- '[class]': 'classes()',
169
- },
170
- }]
171
- }] });
172
- /** Slot directive for the action button. Apply to a native `<button>` to inherit the severity-aware styling. */
173
- class ToastActionDirective {
174
- toast = inject(ToastComponent);
175
- /** @internal */
176
- classes = this.toast.actionClasses;
177
- /** @internal */
178
- onClick() {
179
- this.toast.actionClicked.emit();
180
- }
181
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastActionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
182
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.7", type: ToastActionDirective, isStandalone: true, selector: "[twToastAction]", host: { attributes: { "type": "button" }, listeners: { "click": "onClick()" }, properties: { "class": "classes()" } }, ngImport: i0 });
183
- }
184
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastActionDirective, decorators: [{
185
- type: Directive,
186
- args: [{
187
- selector: '[twToastAction]',
188
- host: {
189
- type: 'button',
190
- '[class]': 'classes()',
191
- '(click)': 'onClick()',
192
- },
193
- }]
194
- }] });
83
+ /** Duration (ms) of the toast enter / leave animation. Must match `_base.css` toast-slide keyframes. */
84
+ const TOAST_ANIMATION_DURATION = 150;
85
+ /** Padding added to animation fallback timers to guarantee cleanup if the CSS event is swallowed. */
86
+ const TOAST_ANIMATION_FALLBACK_PADDING = 50;
195
87
  /**
196
- * Visual toast / snackbar panel. Rendered internally by {@link ToastService} for
197
- * string content, and exported for consumers who want to compose the same
198
- * visual inside a custom `TemplateRef` or component class passed to `show()`.
88
+ * Reference to a toast opened via {@link ToastService}. Use to dismiss the
89
+ * toast, pause / resume its auto-dismiss timer, or observe its lifecycle.
90
+ *
91
+ * The service returns a ref synchronously from every open method; the toast
92
+ * itself is attached asynchronously inside the CDK overlay.
199
93
  */
200
- class ToastComponent {
201
- /** Severity variant. Drives color palette, default icon, and ARIA role / live politeness. Defaults to `'info'`. */
202
- severity = input('info', /* @ts-ignore */
203
- ...(ngDevMode ? [{ debugName: "severity" }] : /* istanbul ignore next */ []));
204
- // Default true: mirrors `ToastConfig.dismissible` toasts must offer an escape
205
- // hatch by default; opt-out only for ephemeral progress markers paired with `duration`.
206
- /** Whether to render the close button. Defaults to `true`. */
207
- dismissible = input(true, /* @ts-ignore */
208
- ...(ngDevMode ? [{ debugName: "dismissible" }] : /* istanbul ignore next */ []));
94
+ class ToastRef {
95
+ dismissCallback;
96
+ /** Unique id. Generated if `config.id` was not provided. */
97
+ id;
98
+ /** Instance of the component rendered inside the toast (only for component-class content). */
99
+ componentInstance = null;
100
+ /** Resolved static config for this toast (defaults merged, severity shorthand applied). */
101
+ config;
102
+ /** Reactive lifecycle state. */
103
+ state;
104
+ /** Reactive severity — may change after `update()` (used by the `promise()` helper). */
105
+ severity;
106
+ /** Reactive dismissibility. */
107
+ dismissible;
108
+ /** Reactive icon override. */
109
+ icon;
110
+ /** Reactive content (the source of truth for what gets rendered). */
111
+ content;
112
+ /** Reactive action button configuration. */
113
+ action;
114
+ /** Reactive accessible label. */
115
+ ariaLabel;
116
+ /** Reactive data payload — re-read by the container to update `TW_TOAST_DATA`. */
117
+ data;
118
+ /** @internal Reactive "transient" class applied by swipe gestures (e.g. `translate3d(...)`). */
119
+ swipeTransform = signal(null, /* @ts-ignore */
120
+ ...(ngDevMode ? [{ debugName: "swipeTransform" }] : /* istanbul ignore next */ []));
121
+ /** @internal Reactive opacity applied during swipe gestures. */
122
+ swipeOpacity = signal(null, /* @ts-ignore */
123
+ ...(ngDevMode ? [{ debugName: "swipeOpacity" }] : /* istanbul ignore next */ []));
124
+ /** @internal Leave-animation class override. When non-null, container uses this instead of the position default. */
125
+ leaveAnimationOverride = signal(null, /* @ts-ignore */
126
+ ...(ngDevMode ? [{ debugName: "leaveAnimationOverride" }] : /* istanbul ignore next */ []));
127
+ /** @internal Portal for component-class content. Set by the service; null for string / template content. */
128
+ _portal = null;
129
+ /** @internal True while the pointer hovers this toast. */
130
+ _hovered = false;
131
+ /** @internal True while focus is within this toast. */
132
+ _focused = false;
133
+ /** @internal Set by the service so the ref can reach the CDK-owned OverlayRef if it needs to. */
134
+ _overlayPanelElement = null;
135
+ stateSignal = signal('entering', /* @ts-ignore */
136
+ ...(ngDevMode ? [{ debugName: "stateSignal" }] : /* istanbul ignore next */ []));
137
+ severitySignal;
138
+ dismissibleSignal;
139
+ iconSignal;
140
+ contentSignal;
141
+ actionSignal;
142
+ ariaLabelSignal;
143
+ dataSignal;
144
+ afterOpenedSubject = new ReplaySubject(1);
145
+ beforeDismissedSubject = new ReplaySubject(1);
146
+ afterDismissedSubject = new ReplaySubject(1);
147
+ updatedSubject = new Subject();
148
+ dismissReason = 'programmatic';
149
+ dismissResult;
150
+ autoDismissTimer = null;
151
+ autoDismissStartedAt = 0;
152
+ autoDismissRemaining = 0;
153
+ paused = false;
154
+ enterTimer = null;
155
+ leaveTimer = null;
156
+ constructor(id, content, config, dismissCallback) {
157
+ this.dismissCallback = dismissCallback;
158
+ this.id = id;
159
+ this.config = config;
160
+ this.severitySignal = signal(config.severity ?? 'info', /* @ts-ignore */
161
+ ...(ngDevMode ? [{ debugName: "severitySignal" }] : /* istanbul ignore next */ []));
162
+ this.dismissibleSignal = signal(config.dismissible ?? true, /* @ts-ignore */
163
+ ...(ngDevMode ? [{ debugName: "dismissibleSignal" }] : /* istanbul ignore next */ []));
164
+ this.iconSignal = signal(typeof config.icon === 'string' || config.icon === false ? config.icon : undefined, /* @ts-ignore */
165
+ ...(ngDevMode ? [{ debugName: "iconSignal" }] : /* istanbul ignore next */ []));
166
+ this.contentSignal = signal(content, /* @ts-ignore */
167
+ ...(ngDevMode ? [{ debugName: "contentSignal" }] : /* istanbul ignore next */ []));
168
+ this.actionSignal = signal(config.action, /* @ts-ignore */
169
+ ...(ngDevMode ? [{ debugName: "actionSignal" }] : /* istanbul ignore next */ []));
170
+ this.ariaLabelSignal = signal(config.ariaLabel, /* @ts-ignore */
171
+ ...(ngDevMode ? [{ debugName: "ariaLabelSignal" }] : /* istanbul ignore next */ []));
172
+ this.dataSignal = signal(config.data ?? null, /* @ts-ignore */
173
+ ...(ngDevMode ? [{ debugName: "dataSignal" }] : /* istanbul ignore next */ []));
174
+ this.state = this.stateSignal.asReadonly();
175
+ this.severity = this.severitySignal.asReadonly();
176
+ this.dismissible = this.dismissibleSignal.asReadonly();
177
+ this.icon = this.iconSignal.asReadonly();
178
+ this.content = this.contentSignal.asReadonly();
179
+ this.action = this.actionSignal.asReadonly();
180
+ this.ariaLabel = this.ariaLabelSignal.asReadonly();
181
+ this.data = this.dataSignal.asReadonly();
182
+ this.autoDismissRemaining = config.duration ?? 0;
183
+ }
209
184
  /**
210
- * Icon override. Pass a string to render as text inside the icon slot, or
211
- * `false` to hide the built-in severity icon. Ignored when a `[twToastIcon]`
212
- * child is projected. When omitted, the severity-default icon renders.
185
+ * @internal Start the enter animation clock. Called by the service once the
186
+ * toast has actually been attached to its container.
213
187
  *
214
- * For arbitrary icon markup, project a `[twToastIcon]` child instead.
188
+ * This deliberately does NOT run from the constructor. The renderer is loaded
189
+ * through a dynamic `import()`, so a ref can exist for an arbitrary stretch
190
+ * before anything is on screen — starting the clock at construction would let
191
+ * a toast burn part (or all) of its auto-dismiss duration while still
192
+ * invisible on a slow connection. Idempotent and safe to call after dismissal.
215
193
  */
216
- icon = input(undefined, /* @ts-ignore */
217
- ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
218
- /** Explicit aria-label for the toast wrapper. When omitted, text content is used by assistive tech. */
219
- ariaLabel = input(undefined, /* @ts-ignore */
220
- ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
221
- /** Fires when the close button is clicked. */
222
- dismissed = output();
223
- /** Fires when a `[twToastAction]` button is clicked. */
224
- actionClicked = output();
225
- /** @internal */
226
- projectedIcon = contentChild(ToastIconDirective, /* @ts-ignore */
227
- ...(ngDevMode ? [{ debugName: "projectedIcon" }] : /* istanbul ignore next */ []));
228
- /** @internal */
229
- hasProjectedIcon = computed(() => !!this.projectedIcon(), /* @ts-ignore */
230
- ...(ngDevMode ? [{ debugName: "hasProjectedIcon" }] : /* istanbul ignore next */ []));
231
- variantResult = computed(() => toastVariants({ severity: this.severity() }), /* @ts-ignore */
232
- ...(ngDevMode ? [{ debugName: "variantResult" }] : /* istanbul ignore next */ []));
233
- /** @internal */
234
- rootClasses = computed(() => {
235
- const base = this.variantResult().root();
236
- return this.dismissible() ? `${base} pr-10` : base;
237
- }, /* @ts-ignore */
238
- ...(ngDevMode ? [{ debugName: "rootClasses" }] : /* istanbul ignore next */ []));
239
- /** @internal */
240
- iconClasses = computed(() => this.variantResult().icon(), /* @ts-ignore */
241
- ...(ngDevMode ? [{ debugName: "iconClasses" }] : /* istanbul ignore next */ []));
242
- /** @internal */
243
- titleClasses = computed(() => this.variantResult().title(), /* @ts-ignore */
244
- ...(ngDevMode ? [{ debugName: "titleClasses" }] : /* istanbul ignore next */ []));
245
- /** @internal */
246
- descriptionClasses = computed(() => this.variantResult().description(), /* @ts-ignore */
247
- ...(ngDevMode ? [{ debugName: "descriptionClasses" }] : /* istanbul ignore next */ []));
248
- /** @internal */
249
- contentClasses = computed(() => this.variantResult().content(), /* @ts-ignore */
250
- ...(ngDevMode ? [{ debugName: "contentClasses" }] : /* istanbul ignore next */ []));
251
- /** @internal */
252
- actionClasses = computed(() => this.variantResult().action(), /* @ts-ignore */
253
- ...(ngDevMode ? [{ debugName: "actionClasses" }] : /* istanbul ignore next */ []));
254
- /** @internal */
255
- dismissClasses = computed(() => this.variantResult().dismiss(), /* @ts-ignore */
256
- ...(ngDevMode ? [{ debugName: "dismissClasses" }] : /* istanbul ignore next */ []));
257
- /** @internal */
258
- roleAttr = computed(() => this.severity() === 'error' ? 'alert' : 'status', /* @ts-ignore */
259
- ...(ngDevMode ? [{ debugName: "roleAttr" }] : /* istanbul ignore next */ []));
260
- /** @internal */
261
- ariaLiveAttr = computed(() => this.severity() === 'error' ? 'assertive' : 'polite', /* @ts-ignore */
262
- ...(ngDevMode ? [{ debugName: "ariaLiveAttr" }] : /* istanbul ignore next */ []));
263
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
264
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", 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: `
265
- @if (hasProjectedIcon()) {
266
- <ng-content select="[twToastIcon]" />
267
- } @else if (icon() !== false) {
268
- <span [class]="iconClasses()" aria-hidden="true">
269
- @switch (severity()) {
270
- @case ('info') {
271
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
272
- <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"/>
273
- </svg>
274
- }
275
- @case ('success') {
276
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
277
- <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"/>
278
- </svg>
279
- }
280
- @case ('warning') {
281
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
282
- <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"/>
283
- </svg>
284
- }
285
- @case ('error') {
286
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
287
- <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"/>
288
- </svg>
289
- }
290
- @default {
291
- @if (icon() && icon() !== false) {
292
- {{ icon() }}
293
- }
294
- }
295
- }
296
- </span>
297
- }
298
- <div [class]="contentClasses()">
299
- <ng-content select="[twToastTitle]" />
300
- <ng-content select="[twToastDescription]" />
301
- <ng-content />
302
- <ng-content select="[twToastAction]" />
303
- </div>
304
- @if (dismissible()) {
305
- <button
306
- type="button"
307
- aria-label="Dismiss"
308
- [class]="dismissClasses()"
309
- (click)="dismissed.emit()"
310
- >
311
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-4">
312
- <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"/>
313
- </svg>
314
- </button>
315
- }
316
- `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
317
- }
318
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastComponent, decorators: [{
319
- type: Component,
320
- args: [{
321
- selector: 'tw-toast',
322
- changeDetection: ChangeDetectionStrategy.OnPush,
323
- host: {
324
- '[class]': 'rootClasses()',
325
- '[attr.role]': 'roleAttr()',
326
- '[attr.aria-live]': 'ariaLiveAttr()',
327
- 'aria-atomic': 'true',
328
- '[attr.aria-label]': 'ariaLabel() || null',
329
- '[attr.data-severity]': 'severity()',
330
- },
331
- template: `
332
- @if (hasProjectedIcon()) {
333
- <ng-content select="[twToastIcon]" />
334
- } @else if (icon() !== false) {
335
- <span [class]="iconClasses()" aria-hidden="true">
336
- @switch (severity()) {
337
- @case ('info') {
338
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
339
- <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"/>
340
- </svg>
341
- }
342
- @case ('success') {
343
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
344
- <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"/>
345
- </svg>
346
- }
347
- @case ('warning') {
348
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
349
- <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"/>
350
- </svg>
351
- }
352
- @case ('error') {
353
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
354
- <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"/>
355
- </svg>
356
- }
357
- @default {
358
- @if (icon() && icon() !== false) {
359
- {{ icon() }}
360
- }
361
- }
362
- }
363
- </span>
364
- }
365
- <div [class]="contentClasses()">
366
- <ng-content select="[twToastTitle]" />
367
- <ng-content select="[twToastDescription]" />
368
- <ng-content />
369
- <ng-content select="[twToastAction]" />
370
- </div>
371
- @if (dismissible()) {
372
- <button
373
- type="button"
374
- aria-label="Dismiss"
375
- [class]="dismissClasses()"
376
- (click)="dismissed.emit()"
377
- >
378
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-4">
379
- <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"/>
380
- </svg>
381
- </button>
194
+ _startEnterSequence() {
195
+ if (this.enterTimer !== null)
196
+ return;
197
+ if (this.stateSignal() !== 'entering')
198
+ return;
199
+ this.enterTimer = setTimeout(() => this._markVisible(), TOAST_ANIMATION_DURATION + TOAST_ANIMATION_FALLBACK_PADDING);
382
200
  }
383
- `,
384
- }]
385
- }], 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 }] }] } });
386
-
387
- const POSITION_AXIS = {
388
- 'top-right': 'right',
389
- 'bottom-right': 'right',
390
- 'top-left': 'left',
391
- 'bottom-left': 'left',
392
- 'top-center': 'top',
393
- 'bottom-center': 'bottom',
394
- };
395
- const POSITION_HOST_CLASSES = {
396
- 'top-right': 'items-end',
397
- 'bottom-right': 'items-end',
398
- 'top-left': 'items-start',
399
- 'bottom-left': 'items-start',
400
- 'top-center': 'items-center',
401
- 'bottom-center': 'items-center',
402
- };
403
- const POSITION_ORDER_REVERSED = {
404
- 'top-right': false,
405
- 'top-left': false,
406
- 'top-center': false,
407
- 'bottom-right': true,
408
- 'bottom-left': true,
409
- 'bottom-center': true,
410
- };
411
- const SWIPE_DISMISS_FRACTION = 0.4;
412
- const SWIPE_MAX_OPACITY_FADE = 0.6;
413
- /**
414
- * Internal flex-column host rendered inside each per-position CDK overlay.
415
- * Stacks visible toasts via `@for`, wires pause-on-interaction, Escape
416
- * dismissal, swipe gestures, and `LiveAnnouncer` announcements.
417
- *
418
- * @docs-private
419
- */
420
- class ToastContainerComponent {
421
- /** Per-position stacking anchor. Mutated from {@link ToastService} via `.instance.position.set(...)`. */
422
- position = signal('bottom-right', /* @ts-ignore */
423
- ...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
424
- /** Accessible label applied to the `role="region"` host. Mutated from the service on init. */
425
- regionLabel = signal('Notifications', /* @ts-ignore */
426
- ...(ngDevMode ? [{ debugName: "regionLabel" }] : /* istanbul ignore next */ []));
427
201
  /**
428
- * Toasts currently assigned to this container. The service pushes toasts
429
- * into this signal; the container filters by state to drive `animate.leave`.
202
+ * Dismiss the toast programmatically. The leave animation plays before the
203
+ * element is removed and subscribers are notified. Safe to call multiple times.
430
204
  */
431
- visibleRefs = signal([], /* @ts-ignore */
432
- ...(ngDevMode ? [{ debugName: "visibleRefs" }] : /* istanbul ignore next */ []));
433
- injector = inject(Injector);
434
- liveAnnouncer = inject(LiveAnnouncer);
435
- host = inject((ElementRef));
436
- swipeSessions = new WeakMap();
437
- entries = computed(() => {
438
- const pos = this.position();
439
- const axis = POSITION_AXIS[pos];
440
- const enter = `toast-enter-${axis}`;
441
- const leave = `toast-leave-${axis}`;
442
- const refs = this.visibleRefs().filter((ref) => {
443
- const s = ref.state();
444
- return s === 'entering' || s === 'visible' || s === 'paused';
445
- });
446
- return refs.map((ref) => ({
447
- ref,
448
- kind: resolveKind(ref.content()),
449
- enterClass: enter,
450
- leaveClass: leave,
451
- }));
452
- }, /* @ts-ignore */
453
- ...(ngDevMode ? [{ debugName: "entries" }] : /* istanbul ignore next */ []));
454
- orderedEntries = computed(() => {
455
- const list = this.entries();
456
- return POSITION_ORDER_REVERSED[this.position()] ? [...list].reverse() : list;
457
- }, /* @ts-ignore */
458
- ...(ngDevMode ? [{ debugName: "orderedEntries" }] : /* istanbul ignore next */ []));
459
- hostClasses = computed(() => {
460
- const base = 'flex flex-col gap-2 w-full max-w-sm';
461
- const align = POSITION_HOST_CLASSES[this.position()];
462
- return `${base} ${align}`;
463
- }, /* @ts-ignore */
464
- ...(ngDevMode ? [{ debugName: "hostClasses" }] : /* istanbul ignore next */ []));
465
- /** @internal Called by the service right after attaching a new toast. Runs `LiveAnnouncer`. */
466
- _announceOpen(ref) {
467
- const politeness = this.resolvePoliteness(ref);
468
- if (politeness === 'off')
469
- return;
470
- const msg = this.resolveAnnouncementText(ref);
471
- if (msg)
472
- this.liveAnnouncer.announce(msg, politeness);
473
- }
474
- /** @internal Called by the service when `ref.update()` fires — re-announces with the new severity / content. */
475
- _announceUpdate(ref) {
476
- this._announceOpen(ref);
205
+ dismiss(result) {
206
+ this._dismissWith('programmatic', result);
477
207
  }
478
- /** @internal Captures the component instance after `cdkPortalOutlet` attaches. */
479
- onPortalAttached(ref, attached) {
480
- if (attached && 'instance' in attached) {
481
- ref.componentInstance = attached.instance;
208
+ /** Pause the auto-dismiss timer (if any). Called automatically on hover / focus when `pauseOnInteraction` is true. */
209
+ pause() {
210
+ if (this.paused)
211
+ return;
212
+ this.paused = true;
213
+ if (this.autoDismissTimer !== null) {
214
+ const elapsed = Date.now() - this.autoDismissStartedAt;
215
+ this.autoDismissRemaining = Math.max(0, this.autoDismissRemaining - elapsed);
216
+ clearTimeout(this.autoDismissTimer);
217
+ this.autoDismissTimer = null;
482
218
  }
219
+ const current = this.stateSignal();
220
+ if (current === 'visible')
221
+ this.stateSignal.set('paused');
483
222
  }
484
- /** @internal Injector factory for component-class content. Provides `TW_TOAST_DATA` + `TW_TOAST_REF`. */
485
- _createContentInjector(ref) {
486
- return Injector.create({
487
- parent: this.injector,
488
- providers: [
489
- { provide: TW_TOAST_REF, useValue: ref },
490
- { provide: TW_TOAST_DATA, useValue: ref.data() },
491
- ],
492
- });
493
- }
494
- // ── Template helpers ──
495
- asString(content) {
496
- return typeof content === 'string' ? content : '';
497
- }
498
- asTemplate(content) {
499
- return content instanceof TemplateRef ? content : null;
500
- }
501
- templateContext(ref) {
502
- return { $implicit: ref.data(), ref };
503
- }
504
- panelClassFor(ref) {
505
- const raw = ref.config.panelClass;
506
- if (!raw)
507
- return '';
508
- return Array.isArray(raw) ? raw.join(' ') : raw;
509
- }
510
- // ── Interaction handlers ──
511
- onPointerEnter(ref) {
512
- ref._setHovered(true);
513
- }
514
- onPointerLeave(ref) {
515
- ref._setHovered(false);
516
- }
517
- onFocusIn(ref) {
518
- ref._setFocused(true);
519
- }
520
- onFocusOut(ref, event) {
521
- const related = event.relatedTarget;
522
- const target = event.currentTarget;
523
- if (related && target && target.contains(related))
223
+ /** Resume the auto-dismiss timer with the remaining time. */
224
+ resume() {
225
+ if (!this.paused)
524
226
  return;
525
- ref._setFocused(false);
526
- }
527
- onEscape(ref, event) {
528
- event.stopPropagation();
529
- event.preventDefault();
530
- ref._dismissWith('manual');
227
+ this.paused = false;
228
+ if (this.stateSignal() === 'paused')
229
+ this.stateSignal.set('visible');
230
+ if (this.config.duration && this.config.duration > 0) {
231
+ if (this.autoDismissRemaining <= 0) {
232
+ this._dismissWith('timeout');
233
+ return;
234
+ }
235
+ this._startAutoDismissTimer();
236
+ }
531
237
  }
532
- onSwipeStart(ref, event, el) {
533
- if (!ref.config.swipeToDismiss)
534
- return;
535
- if (event.button !== 0)
536
- return;
537
- const target = event.target;
538
- if (target?.closest('button, a, input, select, textarea, [role="button"]'))
539
- return;
540
- const width = el.getBoundingClientRect().width;
541
- this.swipeSessions.set(ref, {
542
- pointerId: event.pointerId,
543
- startX: event.clientX,
544
- width,
545
- active: false,
546
- });
547
- el.setPointerCapture(event.pointerId);
548
- const onMove = (e) => this.onSwipeMove(ref, e, el);
549
- const onUp = (e) => this.onSwipeEnd(ref, e, el, onMove, onUp);
550
- el.addEventListener('pointermove', onMove);
551
- el.addEventListener('pointerup', onUp);
552
- el.addEventListener('pointercancel', onUp);
238
+ /**
239
+ * @internal Update the hover state. When `pauseOnInteraction` is set and
240
+ * either hover or focus is active, the auto-dismiss timer is paused.
241
+ */
242
+ _setHovered(hovered) {
243
+ this._hovered = hovered;
244
+ this._syncInteractionPause();
553
245
  }
554
- onSwipeMove(ref, event, _el) {
555
- const session = this.swipeSessions.get(ref);
556
- if (!session || event.pointerId !== session.pointerId)
557
- return;
558
- const dx = event.clientX - session.startX;
559
- if (!session.active && Math.abs(dx) < 6)
560
- return;
561
- session.active = true;
562
- ref.swipeTransform.set(`translate3d(${dx}px, 0, 0)`);
563
- const fade = 1 - Math.min(Math.abs(dx) / session.width, 1) * SWIPE_MAX_OPACITY_FADE;
564
- ref.swipeOpacity.set(fade);
246
+ /** @internal Update the focus-within state. See `_setHovered`. */
247
+ _setFocused(focused) {
248
+ this._focused = focused;
249
+ this._syncInteractionPause();
565
250
  }
566
- onSwipeEnd(ref, event, el, onMove, onUp) {
567
- const session = this.swipeSessions.get(ref);
568
- el.removeEventListener('pointermove', onMove);
569
- el.removeEventListener('pointerup', onUp);
570
- el.removeEventListener('pointercancel', onUp);
571
- if (!session || event.pointerId !== session.pointerId)
572
- return;
573
- try {
574
- el.releasePointerCapture(session.pointerId);
575
- }
576
- catch {
577
- /* no-op */
578
- }
579
- this.swipeSessions.delete(ref);
580
- if (!session.active)
251
+ _syncInteractionPause() {
252
+ if (!this.config.pauseOnInteraction)
581
253
  return;
582
- const dx = event.clientX - session.startX;
583
- const threshold = session.width * SWIPE_DISMISS_FRACTION;
584
- const allowed = this.swipeDirectionAllowed(dx);
585
- if (Math.abs(dx) >= threshold && allowed) {
586
- untracked(() => {
587
- ref.swipeTransform.set(`translate3d(${Math.sign(dx) * session.width * 1.2}px, 0, 0)`);
588
- ref.swipeOpacity.set(0);
589
- ref.leaveAnimationOverride.set('fade-out');
590
- });
591
- ref._dismissWith('swipe');
254
+ if (this._hovered || this._focused) {
255
+ this.pause();
592
256
  }
593
257
  else {
594
- ref.swipeTransform.set(null);
595
- ref.swipeOpacity.set(null);
258
+ this.resume();
596
259
  }
597
260
  }
598
- swipeDirectionAllowed(dx) {
599
- const pos = this.position();
600
- if (pos === 'top-right' || pos === 'bottom-right')
601
- return dx > 0;
602
- if (pos === 'top-left' || pos === 'bottom-left')
603
- return dx < 0;
604
- return true;
605
- }
606
- resolvePoliteness(ref) {
607
- if (ref.config.politeness)
608
- return ref.config.politeness;
609
- return ref.severity() === 'error' ? 'assertive' : 'polite';
610
- }
611
- resolveAnnouncementText(ref) {
612
- const explicit = ref.ariaLabel();
613
- if (explicit)
614
- return explicit;
615
- const content = ref.content();
616
- if (typeof content === 'string')
617
- return content;
618
- const host = this.host.nativeElement.querySelector(`[data-toast-id="${ref.id}"]`);
619
- if (host)
620
- return host.textContent?.trim() ?? '';
621
- return this.host.nativeElement.textContent?.trim() ?? '';
622
- }
623
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastContainerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
624
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", 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: `
625
- @for (entry of orderedEntries(); track entry.ref.id) {
626
- <!-- Toast entry wrapper; the projected <tw-toast> is the focusable affordance.
627
- This element only forwards pointer/focus events so the container can
628
- pause auto-dismiss while the user is interacting. -->
629
- <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->
630
- <div
631
- class="pointer-events-auto w-full max-w-sm"
632
- [attr.data-toast-id]="entry.ref.id"
633
- [style.transform]="entry.ref.swipeTransform() || null"
634
- [style.opacity]="entry.ref.swipeOpacity() ?? null"
635
- [style.touch-action]="'pan-y'"
636
- [animate.enter]="entry.enterClass"
637
- [animate.leave]="entry.ref.leaveAnimationOverride() ?? entry.leaveClass"
638
- (pointerenter)="onPointerEnter(entry.ref)"
639
- (pointerleave)="onPointerLeave(entry.ref)"
640
- (focusin)="onFocusIn(entry.ref)"
641
- (focusout)="onFocusOut(entry.ref, $event)"
642
- (keydown.escape)="onEscape(entry.ref, $event)"
643
- (pointerdown)="onSwipeStart(entry.ref, $event, swipeEl)"
644
- #swipeEl
645
- >
646
- @switch (entry.kind) {
647
- @case ('string') {
648
- <tw-toast
649
- [severity]="entry.ref.severity()"
650
- [dismissible]="entry.ref.dismissible()"
651
- [icon]="entry.ref.icon()"
652
- [ariaLabel]="entry.ref.ariaLabel()"
653
- [class]="panelClassFor(entry.ref)"
654
- (dismissed)="entry.ref._dismissWith('manual')"
655
- (actionClicked)="entry.ref.triggerAction()"
656
- >
657
- {{ asString(entry.ref.content()) }}
658
- @if (entry.ref.action(); as action) {
659
- <button twToastAction>{{ action.label }}</button>
660
- }
661
- </tw-toast>
662
- }
663
- @case ('template') {
664
- <tw-toast
665
- [severity]="entry.ref.severity()"
666
- [dismissible]="entry.ref.dismissible()"
667
- [icon]="entry.ref.icon()"
668
- [ariaLabel]="entry.ref.ariaLabel()"
669
- [class]="panelClassFor(entry.ref)"
670
- (dismissed)="entry.ref._dismissWith('manual')"
671
- (actionClicked)="entry.ref.triggerAction()"
672
- >
673
- <ng-container
674
- [ngTemplateOutlet]="asTemplate(entry.ref.content())"
675
- [ngTemplateOutletContext]="templateContext(entry.ref)"
676
- />
677
- @if (entry.ref.action(); as action) {
678
- <button twToastAction>{{ action.label }}</button>
679
- }
680
- </tw-toast>
681
- }
682
- @case ('component') {
683
- <ng-template
684
- [cdkPortalOutlet]="entry.ref._portal"
685
- (attached)="onPortalAttached(entry.ref, $event)"
686
- />
687
- }
261
+ /** Invoke the configured action handler (or dismiss with reason `'action'` if none is set). */
262
+ triggerAction() {
263
+ const action = this.actionSignal();
264
+ if (!action)
265
+ return;
266
+ if (action.handler) {
267
+ action.handler(this);
688
268
  }
689
- </div>
690
- }
691
- `, 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 });
692
- }
693
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastContainerComponent, decorators: [{
694
- type: Component,
695
- args: [{
696
- selector: 'tw-toast-container',
697
- changeDetection: ChangeDetectionStrategy.OnPush,
698
- encapsulation: ViewEncapsulation.None,
699
- imports: [NgTemplateOutlet, CdkPortalOutlet, ToastComponent, ToastActionDirective],
700
- host: {
701
- role: 'region',
702
- '[class]': 'hostClasses()',
703
- '[attr.aria-label]': 'regionLabel()',
704
- '[attr.data-position]': 'position()',
705
- },
706
- template: `
707
- @for (entry of orderedEntries(); track entry.ref.id) {
708
- <!-- Toast entry wrapper; the projected <tw-toast> is the focusable affordance.
709
- This element only forwards pointer/focus events so the container can
710
- pause auto-dismiss while the user is interacting. -->
711
- <!-- eslint-disable-next-line @angular-eslint/template/interactive-supports-focus -->
712
- <div
713
- class="pointer-events-auto w-full max-w-sm"
714
- [attr.data-toast-id]="entry.ref.id"
715
- [style.transform]="entry.ref.swipeTransform() || null"
716
- [style.opacity]="entry.ref.swipeOpacity() ?? null"
717
- [style.touch-action]="'pan-y'"
718
- [animate.enter]="entry.enterClass"
719
- [animate.leave]="entry.ref.leaveAnimationOverride() ?? entry.leaveClass"
720
- (pointerenter)="onPointerEnter(entry.ref)"
721
- (pointerleave)="onPointerLeave(entry.ref)"
722
- (focusin)="onFocusIn(entry.ref)"
723
- (focusout)="onFocusOut(entry.ref, $event)"
724
- (keydown.escape)="onEscape(entry.ref, $event)"
725
- (pointerdown)="onSwipeStart(entry.ref, $event, swipeEl)"
726
- #swipeEl
727
- >
728
- @switch (entry.kind) {
729
- @case ('string') {
730
- <tw-toast
731
- [severity]="entry.ref.severity()"
732
- [dismissible]="entry.ref.dismissible()"
733
- [icon]="entry.ref.icon()"
734
- [ariaLabel]="entry.ref.ariaLabel()"
735
- [class]="panelClassFor(entry.ref)"
736
- (dismissed)="entry.ref._dismissWith('manual')"
737
- (actionClicked)="entry.ref.triggerAction()"
738
- >
739
- {{ asString(entry.ref.content()) }}
740
- @if (entry.ref.action(); as action) {
741
- <button twToastAction>{{ action.label }}</button>
742
- }
743
- </tw-toast>
744
- }
745
- @case ('template') {
746
- <tw-toast
747
- [severity]="entry.ref.severity()"
748
- [dismissible]="entry.ref.dismissible()"
749
- [icon]="entry.ref.icon()"
750
- [ariaLabel]="entry.ref.ariaLabel()"
751
- [class]="panelClassFor(entry.ref)"
752
- (dismissed)="entry.ref._dismissWith('manual')"
753
- (actionClicked)="entry.ref.triggerAction()"
754
- >
755
- <ng-container
756
- [ngTemplateOutlet]="asTemplate(entry.ref.content())"
757
- [ngTemplateOutletContext]="templateContext(entry.ref)"
758
- />
759
- @if (entry.ref.action(); as action) {
760
- <button twToastAction>{{ action.label }}</button>
761
- }
762
- </tw-toast>
763
- }
764
- @case ('component') {
765
- <ng-template
766
- [cdkPortalOutlet]="entry.ref._portal"
767
- (attached)="onPortalAttached(entry.ref, $event)"
768
- />
769
- }
770
- }
771
- </div>
772
- }
773
- `,
774
- }]
775
- }] });
776
- function resolveKind(content) {
777
- if (typeof content === 'string')
778
- return 'string';
779
- if (content instanceof TemplateRef)
780
- return 'template';
781
- return 'component';
782
- }
783
-
784
- /** Duration (ms) of the toast enter / leave animation. Must match `_base.css` toast-slide keyframes. */
785
- const TOAST_ANIMATION_DURATION = 150;
786
- /** Padding added to animation fallback timers to guarantee cleanup if the CSS event is swallowed. */
787
- const TOAST_ANIMATION_FALLBACK_PADDING = 50;
788
- /**
789
- * Reference to a toast opened via {@link ToastService}. Use to dismiss the
790
- * toast, pause / resume its auto-dismiss timer, or observe its lifecycle.
791
- *
792
- * The service returns a ref synchronously from every open method; the toast
793
- * itself is attached asynchronously inside the CDK overlay.
794
- */
795
- class ToastRef {
796
- dismissCallback;
797
- /** Unique id. Generated if `config.id` was not provided. */
798
- id;
799
- /** Instance of the component rendered inside the toast (only for component-class content). */
800
- componentInstance = null;
801
- /** Resolved static config for this toast (defaults merged, severity shorthand applied). */
802
- config;
803
- /** Reactive lifecycle state. */
804
- state;
805
- /** Reactive severity — may change after `update()` (used by the `promise()` helper). */
806
- severity;
807
- /** Reactive dismissibility. */
808
- dismissible;
809
- /** Reactive icon override. */
810
- icon;
811
- /** Reactive content (the source of truth for what gets rendered). */
812
- content;
813
- /** Reactive action button configuration. */
814
- action;
815
- /** Reactive accessible label. */
816
- ariaLabel;
817
- /** Reactive data payload — re-read by the container to update `TW_TOAST_DATA`. */
818
- data;
819
- /** @internal Reactive "transient" class applied by swipe gestures (e.g. `translate3d(...)`). */
820
- swipeTransform = signal(null, /* @ts-ignore */
821
- ...(ngDevMode ? [{ debugName: "swipeTransform" }] : /* istanbul ignore next */ []));
822
- /** @internal Reactive opacity applied during swipe gestures. */
823
- swipeOpacity = signal(null, /* @ts-ignore */
824
- ...(ngDevMode ? [{ debugName: "swipeOpacity" }] : /* istanbul ignore next */ []));
825
- /** @internal Leave-animation class override. When non-null, container uses this instead of the position default. */
826
- leaveAnimationOverride = signal(null, /* @ts-ignore */
827
- ...(ngDevMode ? [{ debugName: "leaveAnimationOverride" }] : /* istanbul ignore next */ []));
828
- /** @internal Portal for component-class content. Set by the service; null for string / template content. */
829
- _portal = null;
830
- /** @internal True while the pointer hovers this toast. */
831
- _hovered = false;
832
- /** @internal True while focus is within this toast. */
833
- _focused = false;
834
- /** @internal Set by the service so the ref can reach the CDK-owned OverlayRef if it needs to. */
835
- _overlayPanelElement = null;
836
- stateSignal = signal('entering', /* @ts-ignore */
837
- ...(ngDevMode ? [{ debugName: "stateSignal" }] : /* istanbul ignore next */ []));
838
- severitySignal;
839
- dismissibleSignal;
840
- iconSignal;
841
- contentSignal;
842
- actionSignal;
843
- ariaLabelSignal;
844
- dataSignal;
845
- afterOpenedSubject = new ReplaySubject(1);
846
- beforeDismissedSubject = new ReplaySubject(1);
847
- afterDismissedSubject = new ReplaySubject(1);
848
- updatedSubject = new Subject();
849
- dismissReason = 'programmatic';
850
- dismissResult;
851
- autoDismissTimer = null;
852
- autoDismissStartedAt = 0;
853
- autoDismissRemaining = 0;
854
- paused = false;
855
- enterTimer = null;
856
- leaveTimer = null;
857
- constructor(id, content, config, dismissCallback) {
858
- this.dismissCallback = dismissCallback;
859
- this.id = id;
860
- this.config = config;
861
- this.severitySignal = signal(config.severity ?? 'info', /* @ts-ignore */
862
- ...(ngDevMode ? [{ debugName: "severitySignal" }] : /* istanbul ignore next */ []));
863
- this.dismissibleSignal = signal(config.dismissible ?? true, /* @ts-ignore */
864
- ...(ngDevMode ? [{ debugName: "dismissibleSignal" }] : /* istanbul ignore next */ []));
865
- this.iconSignal = signal(typeof config.icon === 'string' || config.icon === false ? config.icon : undefined, /* @ts-ignore */
866
- ...(ngDevMode ? [{ debugName: "iconSignal" }] : /* istanbul ignore next */ []));
867
- this.contentSignal = signal(content, /* @ts-ignore */
868
- ...(ngDevMode ? [{ debugName: "contentSignal" }] : /* istanbul ignore next */ []));
869
- this.actionSignal = signal(config.action, /* @ts-ignore */
870
- ...(ngDevMode ? [{ debugName: "actionSignal" }] : /* istanbul ignore next */ []));
871
- this.ariaLabelSignal = signal(config.ariaLabel, /* @ts-ignore */
872
- ...(ngDevMode ? [{ debugName: "ariaLabelSignal" }] : /* istanbul ignore next */ []));
873
- this.dataSignal = signal(config.data ?? null, /* @ts-ignore */
874
- ...(ngDevMode ? [{ debugName: "dataSignal" }] : /* istanbul ignore next */ []));
875
- this.state = this.stateSignal.asReadonly();
876
- this.severity = this.severitySignal.asReadonly();
877
- this.dismissible = this.dismissibleSignal.asReadonly();
878
- this.icon = this.iconSignal.asReadonly();
879
- this.content = this.contentSignal.asReadonly();
880
- this.action = this.actionSignal.asReadonly();
881
- this.ariaLabel = this.ariaLabelSignal.asReadonly();
882
- this.data = this.dataSignal.asReadonly();
883
- this.autoDismissRemaining = config.duration ?? 0;
884
- this.enterTimer = setTimeout(() => this._markVisible(), TOAST_ANIMATION_DURATION + TOAST_ANIMATION_FALLBACK_PADDING);
885
- }
886
- /**
887
- * Dismiss the toast programmatically. The leave animation plays before the
888
- * element is removed and subscribers are notified. Safe to call multiple times.
889
- */
890
- dismiss(result) {
891
- this._dismissWith('programmatic', result);
892
- }
893
- /** Pause the auto-dismiss timer (if any). Called automatically on hover / focus when `pauseOnInteraction` is true. */
894
- pause() {
895
- if (this.paused)
896
- return;
897
- this.paused = true;
898
- if (this.autoDismissTimer !== null) {
899
- const elapsed = Date.now() - this.autoDismissStartedAt;
900
- this.autoDismissRemaining = Math.max(0, this.autoDismissRemaining - elapsed);
901
- clearTimeout(this.autoDismissTimer);
902
- this.autoDismissTimer = null;
903
- }
904
- const current = this.stateSignal();
905
- if (current === 'visible')
906
- this.stateSignal.set('paused');
907
- }
908
- /** Resume the auto-dismiss timer with the remaining time. */
909
- resume() {
910
- if (!this.paused)
911
- return;
912
- this.paused = false;
913
- if (this.stateSignal() === 'paused')
914
- this.stateSignal.set('visible');
915
- if (this.config.duration && this.config.duration > 0) {
916
- if (this.autoDismissRemaining <= 0) {
917
- this._dismissWith('timeout');
918
- return;
919
- }
920
- this._startAutoDismissTimer();
921
- }
922
- }
923
- /**
924
- * @internal Update the hover state. When `pauseOnInteraction` is set and
925
- * either hover or focus is active, the auto-dismiss timer is paused.
926
- */
927
- _setHovered(hovered) {
928
- this._hovered = hovered;
929
- this._syncInteractionPause();
930
- }
931
- /** @internal Update the focus-within state. See `_setHovered`. */
932
- _setFocused(focused) {
933
- this._focused = focused;
934
- this._syncInteractionPause();
935
- }
936
- _syncInteractionPause() {
937
- if (!this.config.pauseOnInteraction)
938
- return;
939
- if (this._hovered || this._focused) {
940
- this.pause();
941
- }
942
- else {
943
- this.resume();
944
- }
945
- }
946
- /** Invoke the configured action handler (or dismiss with reason `'action'` if none is set). */
947
- triggerAction() {
948
- const action = this.actionSignal();
949
- if (!action)
950
- return;
951
- if (action.handler) {
952
- action.handler(this);
953
- }
954
- else {
955
- this._dismissWith('action');
269
+ else {
270
+ this._dismissWith('action');
956
271
  }
957
272
  }
958
273
  /** Mutate the live toast in place. Used by the `promise()` helper to swap loading → success / error. */
@@ -1070,7 +385,6 @@ class ToastRef {
1070
385
  }
1071
386
  }
1072
387
 
1073
- const OVERLAY_EDGE_OFFSET = '1rem';
1074
388
  let nextToastId = 0;
1075
389
  function generateId() {
1076
390
  return `tw-toast-${++nextToastId}`;
@@ -1080,14 +394,22 @@ function generateId() {
1080
394
  * created per `ToastPosition` on first use and reused for the lifetime of the
1081
395
  * service; toasts stack vertically inside their position container.
1082
396
  *
397
+ * The rendering layer (CDK overlay + the toast components) is loaded through a
398
+ * dynamic `import()` on the first `show()` call, so merely registering this
399
+ * service costs nothing in the initial bundle. Every open method still returns
400
+ * its {@link ToastRef} synchronously — the toast is attached once the chunk
401
+ * lands, and `update()` / `dismiss()` called in the meantime are honoured.
402
+ *
1083
403
  * Not `providedIn: 'root'` — register via {@link provideToast}.
1084
404
  */
1085
405
  class ToastService {
1086
- overlay = inject(Overlay);
1087
406
  injector = inject(Injector);
1088
407
  defaultOptions = inject(TW_TOAST_DEFAULT_OPTIONS, { optional: true }) ?? {};
1089
408
  parent = inject(ToastService, { optional: true, skipSelf: true });
1090
- positionOverlays = new Map();
409
+ /** Cached renderer chunk. Non-null once the first `show()` has kicked off the import. */
410
+ rendererPromise = null;
411
+ renderer = null;
412
+ destroyed = false;
1091
413
  activeRefsAtThisLevel = signal([], /* @ts-ignore */
1092
414
  ...(ngDevMode ? [{ debugName: "activeRefsAtThisLevel" }] : /* istanbul ignore next */ []));
1093
415
  afterOpenedSubject = new Subject();
@@ -1186,13 +508,14 @@ class ToastService {
1186
508
  return this.activeToasts().find((ref) => ref.id === id);
1187
509
  }
1188
510
  ngOnDestroy() {
511
+ // Set first: an in-flight renderer import resolves after this and must not
512
+ // instantiate overlays for a service that is already gone.
513
+ this.destroyed = true;
1189
514
  const refs = [...this.activeRefsAtThisLevel()];
1190
515
  for (let i = refs.length - 1; i >= 0; i--)
1191
516
  refs[i]._finishDismiss();
1192
- for (const { overlayRef } of this.positionOverlays.values()) {
1193
- overlayRef.dispose();
1194
- }
1195
- this.positionOverlays.clear();
517
+ this.renderer?.dispose();
518
+ this.renderer = null;
1196
519
  this.afterOpenedSubject.complete();
1197
520
  this.afterAllDismissedSubject.complete();
1198
521
  }
@@ -1207,16 +530,53 @@ class ToastService {
1207
530
  const id = config.id ?? generateId();
1208
531
  config.id = id;
1209
532
  const ref = new ToastRef(id, content, config, (dismissed) => this.handleDismissed(dismissed));
1210
- if (isComponentConstructor(content)) {
1211
- const injector = this.getContainerForPosition(config.position).containerRef.instance._createContentInjector(ref);
1212
- ref._portal = new ComponentPortal(content, null, injector);
1213
- }
1214
533
  this.enforceMaxVisible(config.position, config.maxVisible ?? 5);
1215
534
  this.registerOpen(ref);
1216
- this.attachToContainer(ref, config.position);
1217
- this.announceOpen(ref);
535
+ void this.attachWhenRendererReady(ref, config.position);
1218
536
  return ref;
1219
537
  }
538
+ /**
539
+ * Attach a toast once the renderer chunk has loaded. The ref was already
540
+ * returned to the caller, so it may have been dismissed (or the service
541
+ * destroyed) while the import was in flight — both are checked before
542
+ * anything is put on screen.
543
+ */
544
+ async attachWhenRendererReady(ref, position) {
545
+ const renderer = await this.loadRenderer();
546
+ if (!renderer || this.destroyed)
547
+ return;
548
+ const state = ref.state();
549
+ if (state === 'dismissing' || state === 'dismissed')
550
+ return;
551
+ if (!renderer.attach(ref, position))
552
+ return;
553
+ // Only now does the enter animation — and with it the auto-dismiss
554
+ // countdown — begin. See `ToastRef._startEnterSequence`.
555
+ ref._startEnterSequence();
556
+ renderer.announceOpen(ref);
557
+ }
558
+ /**
559
+ * @internal Resolves once the renderer chunk has loaded and pending toasts
560
+ * have been attached. Exposed for tests, which must await the dynamic import
561
+ * before asserting on rendered DOM.
562
+ */
563
+ async _whenRendered() {
564
+ await this.loadRenderer();
565
+ // Let the per-toast `attachWhenRendererReady` continuations run.
566
+ await Promise.resolve();
567
+ }
568
+ /** Load (once) and instantiate the rendering layer. Resolves to `null` if the service was destroyed mid-import. */
569
+ loadRenderer() {
570
+ if (!this.rendererPromise) {
571
+ this.rendererPromise = import('./cdevhub-ngx-tw-toast-toast-renderer-BlDxemsU.mjs').then(({ ToastRenderer }) => {
572
+ if (this.destroyed)
573
+ return null;
574
+ this.renderer = new ToastRenderer(this.injector, this.defaultOptions.regionAriaLabel ?? 'Notifications');
575
+ return this.renderer;
576
+ });
577
+ }
578
+ return this.rendererPromise;
579
+ }
1220
580
  enforceMaxVisible(position, max) {
1221
581
  if (max <= 0)
1222
582
  return;
@@ -1239,72 +599,18 @@ class ToastService {
1239
599
  handleDismissed(ref) {
1240
600
  const scope = this.parent ?? this;
1241
601
  scope.activeRefsAtThisLevel.update((list) => list.filter((r) => r !== ref));
1242
- const position = ref.config.position ?? 'bottom-right';
1243
- const entry = this.positionOverlays.get(position);
1244
- if (entry) {
1245
- const instance = entry.containerRef.instance;
1246
- instance.visibleRefs.update((list) => list.filter((r) => r !== ref));
1247
- }
602
+ this.renderer?.detach(ref);
1248
603
  if (scope.activeRefsAtThisLevel().length === 0) {
1249
604
  scope.afterAllDismissedSubject.next();
1250
605
  }
1251
606
  }
1252
- attachToContainer(ref, position) {
1253
- const entry = this.getContainerForPosition(position);
1254
- const instance = entry.containerRef.instance;
1255
- const anyRef = ref;
1256
- instance.visibleRefs.update((list) => [...list, anyRef]);
1257
- ref._overlayPanelElement = entry.overlayRef.overlayElement;
1258
- }
1259
- announceOpen(ref) {
1260
- const entry = this.positionOverlays.get(ref.config.position ?? 'bottom-right');
1261
- entry?.containerRef.instance._announceOpen(ref);
1262
- }
607
+ /**
608
+ * Re-announce an updated toast. If the renderer has not landed yet there is
609
+ * nothing on screen to announce — the announcement that fires on attach
610
+ * reads the ref's current (already updated) content, so nothing is lost.
611
+ */
1263
612
  announceUpdate(ref) {
1264
- const entry = this.positionOverlays.get(ref.config.position ?? 'bottom-right');
1265
- entry?.containerRef.instance._announceUpdate(ref);
1266
- }
1267
- getContainerForPosition(position) {
1268
- const existing = this.positionOverlays.get(position);
1269
- if (existing)
1270
- return existing;
1271
- const overlayRef = this.overlay.create({
1272
- positionStrategy: this.buildPositionStrategy(position),
1273
- scrollStrategy: this.overlay.scrollStrategies.noop(),
1274
- hasBackdrop: false,
1275
- panelClass: ['tw-toast-overlay', `tw-toast-overlay-${position}`],
1276
- });
1277
- const containerPortal = new ComponentPortal(ToastContainerComponent, null, this.injector);
1278
- const containerRef = overlayRef.attach(containerPortal);
1279
- containerRef.instance.position.set(position);
1280
- containerRef.instance.regionLabel.set(this.defaultOptions.regionAriaLabel ?? 'Notifications');
1281
- const entry = { overlayRef, containerRef };
1282
- this.positionOverlays.set(position, entry);
1283
- return entry;
1284
- }
1285
- buildPositionStrategy(position) {
1286
- const strategy = createGlobalPositionStrategy(this.injector);
1287
- switch (position) {
1288
- case 'top-right':
1289
- strategy.top(OVERLAY_EDGE_OFFSET).right(OVERLAY_EDGE_OFFSET);
1290
- break;
1291
- case 'top-left':
1292
- strategy.top(OVERLAY_EDGE_OFFSET).left(OVERLAY_EDGE_OFFSET);
1293
- break;
1294
- case 'top-center':
1295
- strategy.top(OVERLAY_EDGE_OFFSET).centerHorizontally();
1296
- break;
1297
- case 'bottom-right':
1298
- strategy.bottom(OVERLAY_EDGE_OFFSET).right(OVERLAY_EDGE_OFFSET);
1299
- break;
1300
- case 'bottom-left':
1301
- strategy.bottom(OVERLAY_EDGE_OFFSET).left(OVERLAY_EDGE_OFFSET);
1302
- break;
1303
- case 'bottom-center':
1304
- strategy.bottom(OVERLAY_EDGE_OFFSET).centerHorizontally();
1305
- break;
1306
- }
1307
- return strategy;
613
+ this.renderer?.announceUpdate(ref);
1308
614
  }
1309
615
  resolveConfig(config) {
1310
616
  const merged = new ToastConfig();
@@ -1320,9 +626,6 @@ class ToastService {
1320
626
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastService, decorators: [{
1321
627
  type: Injectable
1322
628
  }] });
1323
- function isComponentConstructor(value) {
1324
- return typeof value === 'function' && !(value instanceof TemplateRef);
1325
- }
1326
629
  /**
1327
630
  * Registers {@link ToastService} for dependency injection and installs optional
1328
631
  * application-wide defaults.
@@ -1348,6 +651,314 @@ const ToastInternals = {
1348
651
  },
1349
652
  };
1350
653
 
654
+ const toastVariants = tv({
655
+ slots: {
656
+ 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',
657
+ icon: 'size-5 shrink-0 mt-0.5',
658
+ title: 'text-sm font-semibold',
659
+ description: 'text-sm',
660
+ content: 'flex-1 min-w-0',
661
+ 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',
662
+ // Dismiss container is `size-6` (24px — xs square-interactive-target
663
+ // scale per CLAUDE.md icon sub-scale). The inner SVG is `size-4` (16px
664
+ // glyph scale), centred via flex; the larger container gives the
665
+ // touch/click affordance without enlarging the visual glyph.
666
+ 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',
667
+ },
668
+ variants: {
669
+ severity: {
670
+ info: {},
671
+ success: {},
672
+ warning: {},
673
+ error: {},
674
+ neutral: {},
675
+ },
676
+ },
677
+ // Color slot tokens (`{role}-soft`, `-soft-fg`, `-icon`, …) own light/dark
678
+ // contrast — components consume the slot only. See `_semantic.css`.
679
+ // Slot-token safelist: see `projects/ngx-tw/theme/index.css` `@source inline(...)`
680
+ // — Tailwind v4's JIT cannot scan template-literal class names, so every
681
+ // `bg-${severity}-soft` / `text-${severity}-icon` permutation below must be
682
+ // enumerated there.
683
+ compoundVariants: ['info', 'success', 'warning', 'error', 'neutral'].map((severity) => ({
684
+ severity,
685
+ class: {
686
+ root: `bg-${severity}-soft text-${severity}-soft-fg-muted border-${severity}-border`,
687
+ icon: `text-${severity}-icon`,
688
+ title: `text-${severity}-soft-fg`,
689
+ description: `text-${severity}-soft-fg-muted`,
690
+ action: `text-${severity}-soft-fg hover:bg-${severity}-soft-hover`,
691
+ dismiss: `text-${severity}-icon hover:bg-${severity}-soft-hover`,
692
+ },
693
+ })),
694
+ defaultVariants: {
695
+ severity: 'info',
696
+ },
697
+ }, { twMerge: true });
698
+ /** Slot directive for the icon. When projected, overrides the severity-default icon. */
699
+ class ToastIconDirective {
700
+ toast = inject(ToastComponent);
701
+ /** @internal */
702
+ classes = this.toast.iconClasses;
703
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastIconDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
704
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.7", type: ToastIconDirective, isStandalone: true, selector: "[twToastIcon]", host: { attributes: { "aria-hidden": "true" }, properties: { "class": "classes()" } }, ngImport: i0 });
705
+ }
706
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastIconDirective, decorators: [{
707
+ type: Directive,
708
+ args: [{
709
+ selector: '[twToastIcon]',
710
+ host: {
711
+ '[class]': 'classes()',
712
+ 'aria-hidden': 'true',
713
+ },
714
+ }]
715
+ }] });
716
+ /** Slot directive for the bold title line inside a toast. */
717
+ class ToastTitleDirective {
718
+ toast = inject(ToastComponent);
719
+ /** @internal */
720
+ classes = this.toast.titleClasses;
721
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastTitleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
722
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.7", type: ToastTitleDirective, isStandalone: true, selector: "[twToastTitle]", host: { properties: { "class": "classes()" } }, ngImport: i0 });
723
+ }
724
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastTitleDirective, decorators: [{
725
+ type: Directive,
726
+ args: [{
727
+ selector: '[twToastTitle]',
728
+ host: {
729
+ '[class]': 'classes()',
730
+ },
731
+ }]
732
+ }] });
733
+ /** Slot directive for a secondary description line inside a toast. */
734
+ class ToastDescriptionDirective {
735
+ toast = inject(ToastComponent);
736
+ /** @internal */
737
+ classes = this.toast.descriptionClasses;
738
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastDescriptionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
739
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.7", type: ToastDescriptionDirective, isStandalone: true, selector: "[twToastDescription]", host: { properties: { "class": "classes()" } }, ngImport: i0 });
740
+ }
741
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastDescriptionDirective, decorators: [{
742
+ type: Directive,
743
+ args: [{
744
+ selector: '[twToastDescription]',
745
+ host: {
746
+ '[class]': 'classes()',
747
+ },
748
+ }]
749
+ }] });
750
+ /** Slot directive for the action button. Apply to a native `<button>` to inherit the severity-aware styling. */
751
+ class ToastActionDirective {
752
+ toast = inject(ToastComponent);
753
+ /** @internal */
754
+ classes = this.toast.actionClasses;
755
+ /** @internal */
756
+ onClick() {
757
+ this.toast.actionClicked.emit();
758
+ }
759
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastActionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
760
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.7", type: ToastActionDirective, isStandalone: true, selector: "[twToastAction]", host: { attributes: { "type": "button" }, listeners: { "click": "onClick()" }, properties: { "class": "classes()" } }, ngImport: i0 });
761
+ }
762
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastActionDirective, decorators: [{
763
+ type: Directive,
764
+ args: [{
765
+ selector: '[twToastAction]',
766
+ host: {
767
+ type: 'button',
768
+ '[class]': 'classes()',
769
+ '(click)': 'onClick()',
770
+ },
771
+ }]
772
+ }] });
773
+ /**
774
+ * Visual toast / snackbar panel. Rendered internally by {@link ToastService} for
775
+ * string content, and exported for consumers who want to compose the same
776
+ * visual inside a custom `TemplateRef` or component class passed to `show()`.
777
+ */
778
+ class ToastComponent {
779
+ /** Severity variant. Drives color palette, default icon, and ARIA role / live politeness. Defaults to `'info'`. */
780
+ severity = input('info', /* @ts-ignore */
781
+ ...(ngDevMode ? [{ debugName: "severity" }] : /* istanbul ignore next */ []));
782
+ /** Whether to render the close button. Defaults to `true` — mirroring `ToastConfig.dismissible`, toasts must offer an escape hatch; the special case is an ephemeral progress marker paired with `duration`. */
783
+ dismissible = input(true, /* @ts-ignore */
784
+ ...(ngDevMode ? [{ debugName: "dismissible" }] : /* istanbul ignore next */ []));
785
+ /**
786
+ * Icon override. Pass a string to render as text inside the icon slot, or
787
+ * `false` to hide the built-in severity icon. Ignored when a `[twToastIcon]`
788
+ * child is projected. When omitted, the severity-default icon renders.
789
+ *
790
+ * For arbitrary icon markup, project a `[twToastIcon]` child instead.
791
+ */
792
+ icon = input(undefined, /* @ts-ignore */
793
+ ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
794
+ /** Explicit aria-label for the toast wrapper. Mirrored to `aria-label`. When omitted, text content is used by assistive tech. Defaults to `undefined`. */
795
+ ariaLabel = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabel" } : /* istanbul ignore next */ {}), alias: 'aria-label' });
796
+ /** Fires when the close button is clicked. */
797
+ dismissed = output();
798
+ /** Fires when a `[twToastAction]` button is clicked. */
799
+ actionClicked = output();
800
+ /** @internal */
801
+ projectedIcon = contentChild(ToastIconDirective, /* @ts-ignore */
802
+ ...(ngDevMode ? [{ debugName: "projectedIcon" }] : /* istanbul ignore next */ []));
803
+ /** @internal */
804
+ hasProjectedIcon = computed(() => !!this.projectedIcon(), /* @ts-ignore */
805
+ ...(ngDevMode ? [{ debugName: "hasProjectedIcon" }] : /* istanbul ignore next */ []));
806
+ variantResult = computed(() => toastVariants({ severity: this.severity() }), /* @ts-ignore */
807
+ ...(ngDevMode ? [{ debugName: "variantResult" }] : /* istanbul ignore next */ []));
808
+ /** @internal */
809
+ rootClasses = computed(() => {
810
+ const base = this.variantResult().root();
811
+ return this.dismissible() ? `${base} pr-10` : base;
812
+ }, /* @ts-ignore */
813
+ ...(ngDevMode ? [{ debugName: "rootClasses" }] : /* istanbul ignore next */ []));
814
+ /** @internal */
815
+ iconClasses = computed(() => this.variantResult().icon(), /* @ts-ignore */
816
+ ...(ngDevMode ? [{ debugName: "iconClasses" }] : /* istanbul ignore next */ []));
817
+ /** @internal */
818
+ titleClasses = computed(() => this.variantResult().title(), /* @ts-ignore */
819
+ ...(ngDevMode ? [{ debugName: "titleClasses" }] : /* istanbul ignore next */ []));
820
+ /** @internal */
821
+ descriptionClasses = computed(() => this.variantResult().description(), /* @ts-ignore */
822
+ ...(ngDevMode ? [{ debugName: "descriptionClasses" }] : /* istanbul ignore next */ []));
823
+ /** @internal */
824
+ contentClasses = computed(() => this.variantResult().content(), /* @ts-ignore */
825
+ ...(ngDevMode ? [{ debugName: "contentClasses" }] : /* istanbul ignore next */ []));
826
+ /** @internal */
827
+ actionClasses = computed(() => this.variantResult().action(), /* @ts-ignore */
828
+ ...(ngDevMode ? [{ debugName: "actionClasses" }] : /* istanbul ignore next */ []));
829
+ /** @internal */
830
+ dismissClasses = computed(() => this.variantResult().dismiss(), /* @ts-ignore */
831
+ ...(ngDevMode ? [{ debugName: "dismissClasses" }] : /* istanbul ignore next */ []));
832
+ /** @internal */
833
+ roleAttr = computed(() => this.severity() === 'error' ? 'alert' : 'status', /* @ts-ignore */
834
+ ...(ngDevMode ? [{ debugName: "roleAttr" }] : /* istanbul ignore next */ []));
835
+ /** @internal */
836
+ ariaLiveAttr = computed(() => this.severity() === 'error' ? 'assertive' : 'polite', /* @ts-ignore */
837
+ ...(ngDevMode ? [{ debugName: "ariaLiveAttr" }] : /* istanbul ignore next */ []));
838
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
839
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.7", 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: "aria-label", 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: `
840
+ @if (hasProjectedIcon()) {
841
+ <ng-content select="[twToastIcon]" />
842
+ } @else if (icon() !== false) {
843
+ <span [class]="iconClasses()" aria-hidden="true">
844
+ @switch (severity()) {
845
+ @case ('info') {
846
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
847
+ <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"/>
848
+ </svg>
849
+ }
850
+ @case ('success') {
851
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
852
+ <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"/>
853
+ </svg>
854
+ }
855
+ @case ('warning') {
856
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
857
+ <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"/>
858
+ </svg>
859
+ }
860
+ @case ('error') {
861
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
862
+ <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"/>
863
+ </svg>
864
+ }
865
+ @default {
866
+ @if (icon() && icon() !== false) {
867
+ {{ icon() }}
868
+ }
869
+ }
870
+ }
871
+ </span>
872
+ }
873
+ <div [class]="contentClasses()">
874
+ <ng-content select="[twToastTitle]" />
875
+ <ng-content select="[twToastDescription]" />
876
+ <ng-content />
877
+ <ng-content select="[twToastAction]" />
878
+ </div>
879
+ @if (dismissible()) {
880
+ <button
881
+ type="button"
882
+ aria-label="Dismiss"
883
+ [class]="dismissClasses()"
884
+ (click)="dismissed.emit()"
885
+ >
886
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-4">
887
+ <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"/>
888
+ </svg>
889
+ </button>
890
+ }
891
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
892
+ }
893
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastComponent, decorators: [{
894
+ type: Component,
895
+ args: [{
896
+ selector: 'tw-toast',
897
+ changeDetection: ChangeDetectionStrategy.OnPush,
898
+ host: {
899
+ '[class]': 'rootClasses()',
900
+ '[attr.role]': 'roleAttr()',
901
+ '[attr.aria-live]': 'ariaLiveAttr()',
902
+ 'aria-atomic': 'true',
903
+ '[attr.aria-label]': 'ariaLabel() || null',
904
+ '[attr.data-severity]': 'severity()',
905
+ },
906
+ template: `
907
+ @if (hasProjectedIcon()) {
908
+ <ng-content select="[twToastIcon]" />
909
+ } @else if (icon() !== false) {
910
+ <span [class]="iconClasses()" aria-hidden="true">
911
+ @switch (severity()) {
912
+ @case ('info') {
913
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
914
+ <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"/>
915
+ </svg>
916
+ }
917
+ @case ('success') {
918
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
919
+ <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"/>
920
+ </svg>
921
+ }
922
+ @case ('warning') {
923
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
924
+ <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"/>
925
+ </svg>
926
+ }
927
+ @case ('error') {
928
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
929
+ <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"/>
930
+ </svg>
931
+ }
932
+ @default {
933
+ @if (icon() && icon() !== false) {
934
+ {{ icon() }}
935
+ }
936
+ }
937
+ }
938
+ </span>
939
+ }
940
+ <div [class]="contentClasses()">
941
+ <ng-content select="[twToastTitle]" />
942
+ <ng-content select="[twToastDescription]" />
943
+ <ng-content />
944
+ <ng-content select="[twToastAction]" />
945
+ </div>
946
+ @if (dismissible()) {
947
+ <button
948
+ type="button"
949
+ aria-label="Dismiss"
950
+ [class]="dismissClasses()"
951
+ (click)="dismissed.emit()"
952
+ >
953
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-4">
954
+ <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"/>
955
+ </svg>
956
+ </button>
957
+ }
958
+ `,
959
+ }]
960
+ }], 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: "aria-label", required: false }] }], dismissed: [{ type: i0.Output, args: ["dismissed"] }], actionClicked: [{ type: i0.Output, args: ["actionClicked"] }], projectedIcon: [{ type: i0.ContentChild, args: [i0.forwardRef(() => ToastIconDirective), { isSignal: true }] }] } });
961
+
1351
962
  /**
1352
963
  * Generated bundle index. Do not edit.
1353
964
  */