@cdevhub/ngx-tw 0.4.0 → 0.5.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.
@@ -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
  /**
@@ -73,714 +69,6 @@ const TW_TOAST_REF = new InjectionToken('TW_TOAST_REF');
73
69
  /** Injection token carrying application-wide toast defaults. Set via `provideToast(defaults)`. */
74
70
  const TW_TOAST_DEFAULT_OPTIONS = new InjectionToken('TW_TOAST_DEFAULT_OPTIONS');
75
71
 
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
- }] });
195
- /**
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()`.
199
- */
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 */ []));
209
- /**
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.
213
- *
214
- * For arbitrary icon markup, project a `[twToastIcon]` child instead.
215
- */
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>
382
- }
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
- /**
428
- * Toasts currently assigned to this container. The service pushes toasts
429
- * into this signal; the container filters by state to drive `animate.leave`.
430
- */
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);
477
- }
478
- /** @internal Captures the component instance after `cdkPortalOutlet` attaches. */
479
- onPortalAttached(ref, attached) {
480
- if (attached && 'instance' in attached) {
481
- ref.componentInstance = attached.instance;
482
- }
483
- }
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))
524
- return;
525
- ref._setFocused(false);
526
- }
527
- onEscape(ref, event) {
528
- event.stopPropagation();
529
- event.preventDefault();
530
- ref._dismissWith('manual');
531
- }
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);
553
- }
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);
565
- }
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)
581
- 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');
592
- }
593
- else {
594
- ref.swipeTransform.set(null);
595
- ref.swipeOpacity.set(null);
596
- }
597
- }
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
- }
688
- }
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
72
  /** Duration (ms) of the toast enter / leave animation. Must match `_base.css` toast-slide keyframes. */
785
73
  const TOAST_ANIMATION_DURATION = 150;
786
74
  /** Padding added to animation fallback timers to guarantee cleanup if the CSS event is swallowed. */
@@ -881,6 +169,22 @@ class ToastRef {
881
169
  this.ariaLabel = this.ariaLabelSignal.asReadonly();
882
170
  this.data = this.dataSignal.asReadonly();
883
171
  this.autoDismissRemaining = config.duration ?? 0;
172
+ }
173
+ /**
174
+ * @internal Start the enter animation clock. Called by the service once the
175
+ * toast has actually been attached to its container.
176
+ *
177
+ * This deliberately does NOT run from the constructor. The renderer is loaded
178
+ * through a dynamic `import()`, so a ref can exist for an arbitrary stretch
179
+ * before anything is on screen — starting the clock at construction would let
180
+ * a toast burn part (or all) of its auto-dismiss duration while still
181
+ * invisible on a slow connection. Idempotent and safe to call after dismissal.
182
+ */
183
+ _startEnterSequence() {
184
+ if (this.enterTimer !== null)
185
+ return;
186
+ if (this.stateSignal() !== 'entering')
187
+ return;
884
188
  this.enterTimer = setTimeout(() => this._markVisible(), TOAST_ANIMATION_DURATION + TOAST_ANIMATION_FALLBACK_PADDING);
885
189
  }
886
190
  /**
@@ -1070,7 +374,6 @@ class ToastRef {
1070
374
  }
1071
375
  }
1072
376
 
1073
- const OVERLAY_EDGE_OFFSET = '1rem';
1074
377
  let nextToastId = 0;
1075
378
  function generateId() {
1076
379
  return `tw-toast-${++nextToastId}`;
@@ -1080,14 +383,22 @@ function generateId() {
1080
383
  * created per `ToastPosition` on first use and reused for the lifetime of the
1081
384
  * service; toasts stack vertically inside their position container.
1082
385
  *
386
+ * The rendering layer (CDK overlay + the toast components) is loaded through a
387
+ * dynamic `import()` on the first `show()` call, so merely registering this
388
+ * service costs nothing in the initial bundle. Every open method still returns
389
+ * its {@link ToastRef} synchronously — the toast is attached once the chunk
390
+ * lands, and `update()` / `dismiss()` called in the meantime are honoured.
391
+ *
1083
392
  * Not `providedIn: 'root'` — register via {@link provideToast}.
1084
393
  */
1085
394
  class ToastService {
1086
- overlay = inject(Overlay);
1087
395
  injector = inject(Injector);
1088
396
  defaultOptions = inject(TW_TOAST_DEFAULT_OPTIONS, { optional: true }) ?? {};
1089
397
  parent = inject(ToastService, { optional: true, skipSelf: true });
1090
- positionOverlays = new Map();
398
+ /** Cached renderer chunk. Non-null once the first `show()` has kicked off the import. */
399
+ rendererPromise = null;
400
+ renderer = null;
401
+ destroyed = false;
1091
402
  activeRefsAtThisLevel = signal([], /* @ts-ignore */
1092
403
  ...(ngDevMode ? [{ debugName: "activeRefsAtThisLevel" }] : /* istanbul ignore next */ []));
1093
404
  afterOpenedSubject = new Subject();
@@ -1186,13 +497,14 @@ class ToastService {
1186
497
  return this.activeToasts().find((ref) => ref.id === id);
1187
498
  }
1188
499
  ngOnDestroy() {
500
+ // Set first: an in-flight renderer import resolves after this and must not
501
+ // instantiate overlays for a service that is already gone.
502
+ this.destroyed = true;
1189
503
  const refs = [...this.activeRefsAtThisLevel()];
1190
504
  for (let i = refs.length - 1; i >= 0; i--)
1191
505
  refs[i]._finishDismiss();
1192
- for (const { overlayRef } of this.positionOverlays.values()) {
1193
- overlayRef.dispose();
1194
- }
1195
- this.positionOverlays.clear();
506
+ this.renderer?.dispose();
507
+ this.renderer = null;
1196
508
  this.afterOpenedSubject.complete();
1197
509
  this.afterAllDismissedSubject.complete();
1198
510
  }
@@ -1207,16 +519,53 @@ class ToastService {
1207
519
  const id = config.id ?? generateId();
1208
520
  config.id = id;
1209
521
  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
522
  this.enforceMaxVisible(config.position, config.maxVisible ?? 5);
1215
523
  this.registerOpen(ref);
1216
- this.attachToContainer(ref, config.position);
1217
- this.announceOpen(ref);
524
+ void this.attachWhenRendererReady(ref, config.position);
1218
525
  return ref;
1219
526
  }
527
+ /**
528
+ * Attach a toast once the renderer chunk has loaded. The ref was already
529
+ * returned to the caller, so it may have been dismissed (or the service
530
+ * destroyed) while the import was in flight — both are checked before
531
+ * anything is put on screen.
532
+ */
533
+ async attachWhenRendererReady(ref, position) {
534
+ const renderer = await this.loadRenderer();
535
+ if (!renderer || this.destroyed)
536
+ return;
537
+ const state = ref.state();
538
+ if (state === 'dismissing' || state === 'dismissed')
539
+ return;
540
+ if (!renderer.attach(ref, position))
541
+ return;
542
+ // Only now does the enter animation — and with it the auto-dismiss
543
+ // countdown — begin. See `ToastRef._startEnterSequence`.
544
+ ref._startEnterSequence();
545
+ renderer.announceOpen(ref);
546
+ }
547
+ /**
548
+ * @internal Resolves once the renderer chunk has loaded and pending toasts
549
+ * have been attached. Exposed for tests, which must await the dynamic import
550
+ * before asserting on rendered DOM.
551
+ */
552
+ async _whenRendered() {
553
+ await this.loadRenderer();
554
+ // Let the per-toast `attachWhenRendererReady` continuations run.
555
+ await Promise.resolve();
556
+ }
557
+ /** Load (once) and instantiate the rendering layer. Resolves to `null` if the service was destroyed mid-import. */
558
+ loadRenderer() {
559
+ if (!this.rendererPromise) {
560
+ this.rendererPromise = import('./cdevhub-ngx-tw-toast-toast-renderer-DSu4YoTy.mjs').then(({ ToastRenderer }) => {
561
+ if (this.destroyed)
562
+ return null;
563
+ this.renderer = new ToastRenderer(this.injector, this.defaultOptions.regionAriaLabel ?? 'Notifications');
564
+ return this.renderer;
565
+ });
566
+ }
567
+ return this.rendererPromise;
568
+ }
1220
569
  enforceMaxVisible(position, max) {
1221
570
  if (max <= 0)
1222
571
  return;
@@ -1239,72 +588,18 @@ class ToastService {
1239
588
  handleDismissed(ref) {
1240
589
  const scope = this.parent ?? this;
1241
590
  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
- }
591
+ this.renderer?.detach(ref);
1248
592
  if (scope.activeRefsAtThisLevel().length === 0) {
1249
593
  scope.afterAllDismissedSubject.next();
1250
594
  }
1251
595
  }
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
- }
596
+ /**
597
+ * Re-announce an updated toast. If the renderer has not landed yet there is
598
+ * nothing on screen to announce — the announcement that fires on attach
599
+ * reads the ref's current (already updated) content, so nothing is lost.
600
+ */
1263
601
  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;
602
+ this.renderer?.announceUpdate(ref);
1308
603
  }
1309
604
  resolveConfig(config) {
1310
605
  const merged = new ToastConfig();
@@ -1317,36 +612,344 @@ class ToastService {
1317
612
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1318
613
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastService });
1319
614
  }
1320
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastService, decorators: [{
1321
- type: Injectable
615
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastService, decorators: [{
616
+ type: Injectable
617
+ }] });
618
+ /**
619
+ * Registers {@link ToastService} for dependency injection and installs optional
620
+ * application-wide defaults.
621
+ *
622
+ * @example
623
+ * ```ts
624
+ * bootstrapApplication(AppComponent, {
625
+ * providers: [provideToast({ position: 'top-right', duration: 4000 })]
626
+ * });
627
+ * ```
628
+ */
629
+ function provideToast(defaultOptions) {
630
+ const providers = [ToastService];
631
+ if (defaultOptions) {
632
+ providers.push({ provide: TW_TOAST_DEFAULT_OPTIONS, useValue: defaultOptions });
633
+ }
634
+ return makeEnvironmentProviders(providers);
635
+ }
636
+ const ToastInternals = {
637
+ /** @internal Reset id counter for tests. */
638
+ _resetIdForTesting() {
639
+ nextToastId = 0;
640
+ },
641
+ };
642
+
643
+ const toastVariants = tv({
644
+ slots: {
645
+ 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',
646
+ icon: 'size-5 shrink-0 mt-0.5',
647
+ title: 'text-sm font-semibold',
648
+ description: 'text-sm',
649
+ content: 'flex-1 min-w-0',
650
+ 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',
651
+ // Dismiss container is `size-6` (24px — xs square-interactive-target
652
+ // scale per CLAUDE.md icon sub-scale). The inner SVG is `size-4` (16px
653
+ // glyph scale), centred via flex; the larger container gives the
654
+ // touch/click affordance without enlarging the visual glyph.
655
+ 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',
656
+ },
657
+ variants: {
658
+ severity: {
659
+ info: {},
660
+ success: {},
661
+ warning: {},
662
+ error: {},
663
+ neutral: {},
664
+ },
665
+ },
666
+ // Color slot tokens (`{role}-soft`, `-soft-fg`, `-icon`, …) own light/dark
667
+ // contrast — components consume the slot only. See `_semantic.css`.
668
+ // Slot-token safelist: see `projects/ngx-tw/theme/index.css` `@source inline(...)`
669
+ // — Tailwind v4's JIT cannot scan template-literal class names, so every
670
+ // `bg-${severity}-soft` / `text-${severity}-icon` permutation below must be
671
+ // enumerated there.
672
+ compoundVariants: ['info', 'success', 'warning', 'error', 'neutral'].map((severity) => ({
673
+ severity,
674
+ class: {
675
+ root: `bg-${severity}-soft text-${severity}-soft-fg-muted border-${severity}-border`,
676
+ icon: `text-${severity}-icon`,
677
+ title: `text-${severity}-soft-fg`,
678
+ description: `text-${severity}-soft-fg-muted`,
679
+ action: `text-${severity}-soft-fg hover:bg-${severity}-soft-hover`,
680
+ dismiss: `text-${severity}-icon hover:bg-${severity}-soft-hover`,
681
+ },
682
+ })),
683
+ defaultVariants: {
684
+ severity: 'info',
685
+ },
686
+ }, { twMerge: true });
687
+ /** Slot directive for the icon. When projected, overrides the severity-default icon. */
688
+ class ToastIconDirective {
689
+ toast = inject(ToastComponent);
690
+ /** @internal */
691
+ classes = this.toast.iconClasses;
692
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastIconDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
693
+ 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 });
694
+ }
695
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastIconDirective, decorators: [{
696
+ type: Directive,
697
+ args: [{
698
+ selector: '[twToastIcon]',
699
+ host: {
700
+ '[class]': 'classes()',
701
+ 'aria-hidden': 'true',
702
+ },
703
+ }]
704
+ }] });
705
+ /** Slot directive for the bold title line inside a toast. */
706
+ class ToastTitleDirective {
707
+ toast = inject(ToastComponent);
708
+ /** @internal */
709
+ classes = this.toast.titleClasses;
710
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastTitleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
711
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.7", type: ToastTitleDirective, isStandalone: true, selector: "[twToastTitle]", host: { properties: { "class": "classes()" } }, ngImport: i0 });
712
+ }
713
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastTitleDirective, decorators: [{
714
+ type: Directive,
715
+ args: [{
716
+ selector: '[twToastTitle]',
717
+ host: {
718
+ '[class]': 'classes()',
719
+ },
720
+ }]
721
+ }] });
722
+ /** Slot directive for a secondary description line inside a toast. */
723
+ class ToastDescriptionDirective {
724
+ toast = inject(ToastComponent);
725
+ /** @internal */
726
+ classes = this.toast.descriptionClasses;
727
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastDescriptionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
728
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.7", type: ToastDescriptionDirective, isStandalone: true, selector: "[twToastDescription]", host: { properties: { "class": "classes()" } }, ngImport: i0 });
729
+ }
730
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastDescriptionDirective, decorators: [{
731
+ type: Directive,
732
+ args: [{
733
+ selector: '[twToastDescription]',
734
+ host: {
735
+ '[class]': 'classes()',
736
+ },
737
+ }]
1322
738
  }] });
1323
- function isComponentConstructor(value) {
1324
- return typeof value === 'function' && !(value instanceof TemplateRef);
739
+ /** Slot directive for the action button. Apply to a native `<button>` to inherit the severity-aware styling. */
740
+ class ToastActionDirective {
741
+ toast = inject(ToastComponent);
742
+ /** @internal */
743
+ classes = this.toast.actionClasses;
744
+ /** @internal */
745
+ onClick() {
746
+ this.toast.actionClicked.emit();
747
+ }
748
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastActionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
749
+ 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 });
1325
750
  }
751
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastActionDirective, decorators: [{
752
+ type: Directive,
753
+ args: [{
754
+ selector: '[twToastAction]',
755
+ host: {
756
+ type: 'button',
757
+ '[class]': 'classes()',
758
+ '(click)': 'onClick()',
759
+ },
760
+ }]
761
+ }] });
1326
762
  /**
1327
- * Registers {@link ToastService} for dependency injection and installs optional
1328
- * application-wide defaults.
1329
- *
1330
- * @example
1331
- * ```ts
1332
- * bootstrapApplication(AppComponent, {
1333
- * providers: [provideToast({ position: 'top-right', duration: 4000 })]
1334
- * });
1335
- * ```
763
+ * Visual toast / snackbar panel. Rendered internally by {@link ToastService} for
764
+ * string content, and exported for consumers who want to compose the same
765
+ * visual inside a custom `TemplateRef` or component class passed to `show()`.
1336
766
  */
1337
- function provideToast(defaultOptions) {
1338
- const providers = [ToastService];
1339
- if (defaultOptions) {
1340
- providers.push({ provide: TW_TOAST_DEFAULT_OPTIONS, useValue: defaultOptions });
767
+ class ToastComponent {
768
+ /** Severity variant. Drives color palette, default icon, and ARIA role / live politeness. Defaults to `'info'`. */
769
+ severity = input('info', /* @ts-ignore */
770
+ ...(ngDevMode ? [{ debugName: "severity" }] : /* istanbul ignore next */ []));
771
+ // Default true: mirrors `ToastConfig.dismissible` — toasts must offer an escape
772
+ // hatch by default; opt-out only for ephemeral progress markers paired with `duration`.
773
+ /** Whether to render the close button. Defaults to `true`. */
774
+ dismissible = input(true, /* @ts-ignore */
775
+ ...(ngDevMode ? [{ debugName: "dismissible" }] : /* istanbul ignore next */ []));
776
+ /**
777
+ * Icon override. Pass a string to render as text inside the icon slot, or
778
+ * `false` to hide the built-in severity icon. Ignored when a `[twToastIcon]`
779
+ * child is projected. When omitted, the severity-default icon renders.
780
+ *
781
+ * For arbitrary icon markup, project a `[twToastIcon]` child instead.
782
+ */
783
+ icon = input(undefined, /* @ts-ignore */
784
+ ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
785
+ /** Explicit aria-label for the toast wrapper. When omitted, text content is used by assistive tech. */
786
+ ariaLabel = input(undefined, /* @ts-ignore */
787
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
788
+ /** Fires when the close button is clicked. */
789
+ dismissed = output();
790
+ /** Fires when a `[twToastAction]` button is clicked. */
791
+ actionClicked = output();
792
+ /** @internal */
793
+ projectedIcon = contentChild(ToastIconDirective, /* @ts-ignore */
794
+ ...(ngDevMode ? [{ debugName: "projectedIcon" }] : /* istanbul ignore next */ []));
795
+ /** @internal */
796
+ hasProjectedIcon = computed(() => !!this.projectedIcon(), /* @ts-ignore */
797
+ ...(ngDevMode ? [{ debugName: "hasProjectedIcon" }] : /* istanbul ignore next */ []));
798
+ variantResult = computed(() => toastVariants({ severity: this.severity() }), /* @ts-ignore */
799
+ ...(ngDevMode ? [{ debugName: "variantResult" }] : /* istanbul ignore next */ []));
800
+ /** @internal */
801
+ rootClasses = computed(() => {
802
+ const base = this.variantResult().root();
803
+ return this.dismissible() ? `${base} pr-10` : base;
804
+ }, /* @ts-ignore */
805
+ ...(ngDevMode ? [{ debugName: "rootClasses" }] : /* istanbul ignore next */ []));
806
+ /** @internal */
807
+ iconClasses = computed(() => this.variantResult().icon(), /* @ts-ignore */
808
+ ...(ngDevMode ? [{ debugName: "iconClasses" }] : /* istanbul ignore next */ []));
809
+ /** @internal */
810
+ titleClasses = computed(() => this.variantResult().title(), /* @ts-ignore */
811
+ ...(ngDevMode ? [{ debugName: "titleClasses" }] : /* istanbul ignore next */ []));
812
+ /** @internal */
813
+ descriptionClasses = computed(() => this.variantResult().description(), /* @ts-ignore */
814
+ ...(ngDevMode ? [{ debugName: "descriptionClasses" }] : /* istanbul ignore next */ []));
815
+ /** @internal */
816
+ contentClasses = computed(() => this.variantResult().content(), /* @ts-ignore */
817
+ ...(ngDevMode ? [{ debugName: "contentClasses" }] : /* istanbul ignore next */ []));
818
+ /** @internal */
819
+ actionClasses = computed(() => this.variantResult().action(), /* @ts-ignore */
820
+ ...(ngDevMode ? [{ debugName: "actionClasses" }] : /* istanbul ignore next */ []));
821
+ /** @internal */
822
+ dismissClasses = computed(() => this.variantResult().dismiss(), /* @ts-ignore */
823
+ ...(ngDevMode ? [{ debugName: "dismissClasses" }] : /* istanbul ignore next */ []));
824
+ /** @internal */
825
+ roleAttr = computed(() => this.severity() === 'error' ? 'alert' : 'status', /* @ts-ignore */
826
+ ...(ngDevMode ? [{ debugName: "roleAttr" }] : /* istanbul ignore next */ []));
827
+ /** @internal */
828
+ ariaLiveAttr = computed(() => this.severity() === 'error' ? 'assertive' : 'polite', /* @ts-ignore */
829
+ ...(ngDevMode ? [{ debugName: "ariaLiveAttr" }] : /* istanbul ignore next */ []));
830
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
831
+ 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: `
832
+ @if (hasProjectedIcon()) {
833
+ <ng-content select="[twToastIcon]" />
834
+ } @else if (icon() !== false) {
835
+ <span [class]="iconClasses()" aria-hidden="true">
836
+ @switch (severity()) {
837
+ @case ('info') {
838
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
839
+ <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"/>
840
+ </svg>
841
+ }
842
+ @case ('success') {
843
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
844
+ <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"/>
845
+ </svg>
846
+ }
847
+ @case ('warning') {
848
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
849
+ <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"/>
850
+ </svg>
851
+ }
852
+ @case ('error') {
853
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
854
+ <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"/>
855
+ </svg>
856
+ }
857
+ @default {
858
+ @if (icon() && icon() !== false) {
859
+ {{ icon() }}
860
+ }
861
+ }
862
+ }
863
+ </span>
1341
864
  }
1342
- return makeEnvironmentProviders(providers);
865
+ <div [class]="contentClasses()">
866
+ <ng-content select="[twToastTitle]" />
867
+ <ng-content select="[twToastDescription]" />
868
+ <ng-content />
869
+ <ng-content select="[twToastAction]" />
870
+ </div>
871
+ @if (dismissible()) {
872
+ <button
873
+ type="button"
874
+ aria-label="Dismiss"
875
+ [class]="dismissClasses()"
876
+ (click)="dismissed.emit()"
877
+ >
878
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-4">
879
+ <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"/>
880
+ </svg>
881
+ </button>
882
+ }
883
+ `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
1343
884
  }
1344
- const ToastInternals = {
1345
- /** @internal Reset id counter for tests. */
1346
- _resetIdForTesting() {
1347
- nextToastId = 0;
1348
- },
1349
- };
885
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.7", ngImport: i0, type: ToastComponent, decorators: [{
886
+ type: Component,
887
+ args: [{
888
+ selector: 'tw-toast',
889
+ changeDetection: ChangeDetectionStrategy.OnPush,
890
+ host: {
891
+ '[class]': 'rootClasses()',
892
+ '[attr.role]': 'roleAttr()',
893
+ '[attr.aria-live]': 'ariaLiveAttr()',
894
+ 'aria-atomic': 'true',
895
+ '[attr.aria-label]': 'ariaLabel() || null',
896
+ '[attr.data-severity]': 'severity()',
897
+ },
898
+ template: `
899
+ @if (hasProjectedIcon()) {
900
+ <ng-content select="[twToastIcon]" />
901
+ } @else if (icon() !== false) {
902
+ <span [class]="iconClasses()" aria-hidden="true">
903
+ @switch (severity()) {
904
+ @case ('info') {
905
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
906
+ <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"/>
907
+ </svg>
908
+ }
909
+ @case ('success') {
910
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
911
+ <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"/>
912
+ </svg>
913
+ }
914
+ @case ('warning') {
915
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
916
+ <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"/>
917
+ </svg>
918
+ }
919
+ @case ('error') {
920
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-full">
921
+ <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"/>
922
+ </svg>
923
+ }
924
+ @default {
925
+ @if (icon() && icon() !== false) {
926
+ {{ icon() }}
927
+ }
928
+ }
929
+ }
930
+ </span>
931
+ }
932
+ <div [class]="contentClasses()">
933
+ <ng-content select="[twToastTitle]" />
934
+ <ng-content select="[twToastDescription]" />
935
+ <ng-content />
936
+ <ng-content select="[twToastAction]" />
937
+ </div>
938
+ @if (dismissible()) {
939
+ <button
940
+ type="button"
941
+ aria-label="Dismiss"
942
+ [class]="dismissClasses()"
943
+ (click)="dismissed.emit()"
944
+ >
945
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-4">
946
+ <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"/>
947
+ </svg>
948
+ </button>
949
+ }
950
+ `,
951
+ }]
952
+ }], 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 }] }] } });
1350
953
 
1351
954
  /**
1352
955
  * Generated bundle index. Do not edit.