@rivinia/toast 0.3.1 → 0.4.1

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.
package/README.md CHANGED
@@ -3,103 +3,251 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@rivinia/toast.svg)](https://www.npmjs.com/package/@rivinia/toast)
4
4
  [![license](https://img.shields.io/npm/l/@rivinia/toast.svg)](https://www.npmjs.com/package/@rivinia/toast)
5
5
 
6
- Headless toast queue for Angular 22+. Signals, pause-on-hover timers, actions,
7
- dedupe, overflow policies — zero DOM, zero styles, zero router dependency.
8
- A future `@rivinia/toast/ui` entry point will render this state; Core never
9
- imports UI.
6
+ An Angular signal-based toast queue with timers, actions, deduplication, and
7
+ overflow handling. Core is headless; an optional standalone UI is available at
8
+ `@rivinia/toast/ui`.
10
9
 
11
- - Version: **0.3.1**
12
- - Requires: **Angular `^22.0.0`**, `tslib`
10
+ - Prepared patch: **0.4.1** (latest published: **0.4.0**)
11
+ - Peer dependency: **`@angular/core` `^22.0.0`**
12
+ - No `tslib`, router, or HTTP peer dependency.
13
13
 
14
- ## Install
14
+ ## Install and configure
15
15
 
16
16
  ```bash
17
17
  npm install @rivinia/toast
18
- ng add @rivinia/toast # wires provideRiviniaToast() into app.config.ts
18
+ ng add @rivinia/toast
19
19
  ```
20
20
 
21
+ `ng add` adds `provideRiviniaToast()` to a standalone app's `app.config.ts`; it
22
+ does not add the optional UI component. You can also configure the service
23
+ manually:
24
+
21
25
  ```ts
22
- // app.config.ts
26
+ // src/app/app.config.ts
27
+ import type { ApplicationConfig } from '@angular/core';
23
28
  import { provideRiviniaToast } from '@rivinia/toast';
24
29
 
25
- providers: [provideRiviniaToast({ duration: 5000, maxVisible: 5 })];
30
+ export const appConfig: ApplicationConfig = {
31
+ providers: [
32
+ provideRiviniaToast({
33
+ duration: 5000,
34
+ durations: { error: 8000 },
35
+ maxVisible: 5,
36
+ overflow: 'dismiss-oldest',
37
+ }),
38
+ ],
39
+ };
26
40
  ```
27
41
 
28
- ## Quick start
42
+ All provider fields are optional. Defaults are 5000 ms, five active toasts,
43
+ an unbounded queue, and `dismiss-oldest` overflow. `ng add` also accepts
44
+ `--duration` and `--max-visible`.
29
45
 
30
- ```ts
31
- export class Checkout {
32
- private toast = injectRiviniaToast();
46
+ ## Use with the optional UI
33
47
 
34
- pay() {
35
- const id = this.toast.show({ message: 'Payment sent', kind: 'success' });
36
- // id: number | null (null = rejected by the 'reject' overflow policy)
48
+ The UI is a separate secondary entry point; import its component explicitly.
49
+ This standalone example keeps the injected manager in a class field (injection
50
+ must not be called from the template):
51
+
52
+ ```ts
53
+ // src/app/notifications.component.ts
54
+ import { Component } from '@angular/core';
55
+ import { injectRiviniaToast } from '@rivinia/toast';
56
+ import { RiviniaToastList } from '@rivinia/toast/ui';
57
+
58
+ @Component({
59
+ selector: 'app-notifications',
60
+ standalone: true,
61
+ imports: [RiviniaToastList],
62
+ template: `
63
+ <button type="button" (click)="save()">Save</button>
64
+ <rv-toast-list [manager]="toast" position="bottom-end" />
65
+ `,
66
+ })
67
+ export class NotificationsComponent {
68
+ readonly toast = injectRiviniaToast();
69
+
70
+ save(): void {
71
+ this.toast.show({
72
+ message: 'Changes saved',
73
+ kind: 'success',
74
+ dedupeKey: 'save',
75
+ });
37
76
  }
38
77
 
39
- saving() {
40
- // Same key updates in place instead of stacking duplicates:
41
- this.toast.show({ message: 'Saving…', dedupeKey: 'save' });
42
- this.toast.show({ message: 'Saved', kind: 'success', dedupeKey: 'save' });
78
+ saveWithRetry(retry: () => void): void {
79
+ this.toast.show({
80
+ message: 'Could not save',
81
+ kind: 'error',
82
+ duration: 0,
83
+ action: { label: 'Retry', onPress: retry },
84
+ });
43
85
  }
44
86
  }
45
87
  ```
46
88
 
47
- ```html
48
- <!-- your outlet (or the future @rivinia/toast/ui): -->
49
- @for (t of toast.active(); track t.id) {
50
- <div [attr.data-kind]="t.kind">{{ resolveToastText(t.message) }}</div>
89
+ The standalone list renders the manager's `active()` signal, pauses timed
90
+ notifications on hover and keyboard focus, runs actions, and provides a
91
+ dismiss button by default. Its defaults are `position="bottom-end"`,
92
+ `animation="slide"`, `richColors=false`, and `showCloseButton=true`.
93
+ `position` supports `top`/`bottom` × `start`/`center`/`end` (logical start/end
94
+ work in RTL); `animation` supports `slide`, `fade`, and `none`. Errors use an
95
+ assertive live region; other kinds use polite announcements.
96
+
97
+ For snapshot-driven UIs, provide the snapshots and handle the emitted IDs
98
+ yourself:
99
+
100
+ ```ts
101
+ import { Component } from '@angular/core';
102
+ import type { Toast } from '@rivinia/toast';
103
+ import { RiviniaToastList } from '@rivinia/toast/ui';
104
+
105
+ @Component({
106
+ selector: 'app-toast-snapshots',
107
+ standalone: true,
108
+ imports: [RiviniaToastList],
109
+ template: `
110
+ <rv-toast-list [toasts]="items" (dismissed)="remove($event)" (actionPressed)="run($event)" />
111
+ `,
112
+ })
113
+ export class ToastSnapshotsComponent {
114
+ items: readonly Toast[] = [
115
+ {
116
+ id: 1,
117
+ kind: 'info',
118
+ message: 'A background task is ready',
119
+ duration: 0,
120
+ remaining: 0,
121
+ paused: false,
122
+ action: { label: 'Open', onPress: () => console.log('Open task') },
123
+ },
124
+ ];
125
+
126
+ remove(id: number): void {
127
+ this.items = this.items.filter((item) => item.id !== id);
128
+ }
129
+
130
+ run(id: number): void {
131
+ const item = this.items.find((candidate) => candidate.id === id);
132
+ if (!item?.action) return;
133
+ item.action.onPress();
134
+ if (item.action.dismissAfterPress ?? true) this.remove(id);
135
+ }
51
136
  }
52
137
  ```
53
138
 
54
- ## API
55
-
56
- | Export | Kind | Purpose |
57
- |---|---|---|
58
- | `provideRiviniaToast(config?)` | provider | `{ duration?=5000, durations?, maxVisible?=5, maxQueue?=∞, overflow?='dismiss-oldest' }` |
59
- | `RiviniaToast` | `@Service()` | `show/dismiss/dismissAll/update/pause/resume/runAction`, `active` + `queue` signals |
60
- | `injectRiviniaToast()` | function | shorthand for `inject(RiviniaToast)` |
61
- | `resolveToastText(msg)` | util | reads `string \| Signal<string>` (reactive re-translation without i18n coupling) |
62
- | `clampDuration(v, fallback)` | util | normalizes durations (`0`/negative/`NaN`/infinite = persistent) |
63
- | types | types | `Toast`, `ToastKind`, `ToastMessage`, `ToastAction`, `ToastOptions`, `ToastChanges`, `ToastOverflowPolicy`, `RiviniaToastConfig` |
64
-
65
- `show()` returns the stable id, or `null` under `'reject'` (or a full finite
66
- queue). `dismiss/update/pause/resume/runAction` return `false` for unknown ids
67
- (and repeated dismissal) instead of throwing. `duration: 0` = persistent.
68
- `pause()` suspends only running timers (persistent toasts can never pause, so
69
- hover-leave cannot dismiss them); `resume()` on anything unpaused is a no-op;
70
- each toast exposes its `paused` flag for progress UI.
71
- Timers start at queue **promotion**, never while queued. Per-toast options
72
- always beat globals; a kind change re-resolves the per-kind default unless the
73
- toast carries an explicit duration. Overflow eviction prefers timed,
74
- actionless toasts — persistent errors with actions are last resort.
75
-
76
- ## Recipes (app-owned, deliberately not in the library)
139
+ In this presentational mode your app owns timers, pause behavior, and action
140
+ handling. If both `manager` and `toasts` are supplied, the manager takes
141
+ precedence.
77
142
 
78
- ```ts
79
- import { filter } from 'rxjs';
80
- import { NavigationStart } from '@angular/router';
143
+ ## Use the headless service
81
144
 
82
- // Clear timed toasts on navigation (BRT behavior, 3 lines, no library peer needed):
83
- router.events.pipe(filter((e) => e instanceof NavigationStart))
84
- .subscribe(() => toast.active().forEach((t) => t.duration > 0 && toast.dismiss(t.id)));
145
+ Core can be used without the UI. `active()` and `queue()` are readonly signals;
146
+ the active list is oldest-first. Timers begin only when a toast becomes active.
85
147
 
86
- // Backend-shaped errors stay in your error mapper, then:
87
- toast.show({ message: 'Order failed', kind: 'error', action: {
88
- label: 'Retry', onPress: () => retry(),
89
- } }); // dismisses after press unless dismissAfterPress: false
148
+ ```ts
149
+ import { Component, inject } from '@angular/core';
150
+ import { resolveToastText, RiviniaToast } from '@rivinia/toast';
151
+
152
+ @Component({
153
+ selector: 'app-save-status',
154
+ standalone: true,
155
+ template: `
156
+ <button type="button" (click)="save()">Save</button>
157
+ @for (item of toast.active(); track item.id) {
158
+ <p [attr.data-kind]="item.kind">{{ resolveText(item.message) }}</p>
159
+ }
160
+ `,
161
+ })
162
+ export class SaveStatusComponent {
163
+ readonly toast = inject(RiviniaToast);
164
+ readonly resolveText = resolveToastText;
165
+
166
+ save(): void {
167
+ const id: number | null = this.toast.show({ message: 'Saving…' });
168
+ if (id === null) return; // possible with overflow: 'reject'
169
+
170
+ // A matching key updates the existing active or queued item.
171
+ this.toast.show({
172
+ message: 'Saved',
173
+ kind: 'success',
174
+ dedupeKey: 'save',
175
+ });
176
+ }
177
+ }
90
178
  ```
91
179
 
92
- Future UI must render inside `role="region" aria-live="polite"`, pause on
93
- hover **and** keyboard focus (WCAG 2.2.1 timing control), and offer a labelled
94
- dismiss button — the Core API (`pause/resume/dismiss`) already supports all of it.
95
-
96
- ## Testing your app code
180
+ `message` and action `label` accept either a string or a reactive string
181
+ signal, so they can reflect language changes. For a custom outlet, use the exported
182
+ `resolveToastText(message)` utility to read either shape.
183
+
184
+ ## API and behavior
185
+
186
+ | Export | Kind | Purpose |
187
+ | -------------------------------------------------------------------------------------------------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------- |
188
+ | `provideRiviniaToast(config?)` | Provider | Configure defaults; see `RiviniaToastConfig`. |
189
+ | `RiviniaToast` | Service | `show`, `dismiss`, `dismissAll`, `update`, `pause`, `resume`, `runAction`; `active` and `queue` signals. |
190
+ | `injectRiviniaToast()` | Function | Shorthand for `inject(RiviniaToast)`. |
191
+ | UI secondary entry point | `@rivinia/toast/ui` | Optional managed or presentational standalone list and its UI types. |
192
+ | `Toast`, `ToastKind`, `ToastMessage`, `ToastAction`, `ToastOptions`, `ToastChanges`, `ToastOverflowPolicy`, `RiviniaToastConfig` | Types | Public service, configuration, and UI input types. |
193
+ | `resolveToastText`, `clampDuration` | Utilities | Resolve signal/string text; normalize a duration. |
194
+
195
+ Important details:
196
+
197
+ - `show()` returns a stable `number` ID, or `null` when rejected by the
198
+ `reject` policy or when a finite queue is full. `dismiss()` and `update()`
199
+ return `false` for unknown or dismissed IDs; `pause()`/`resume()` also return
200
+ `false` when the timer cannot be changed.
201
+ - A repeated `dedupeKey` reuses the active or queued toast ID and updates it
202
+ rather than stacking another toast. A matching `show()` without an `action`
203
+ clears the previous action; include it again if it should remain.
204
+ - A toast-specific `duration` overrides per-kind and global defaults.
205
+ `duration: 0`, negative values, not-a-number, and infinities mean persistent (no
206
+ timer). Per-kind defaults are set with `durations`, for example
207
+ `{ error: 8000 }`.
208
+ - `pause()` preserves the remaining time on an active timed toast. Persistent
209
+ or queued toasts cannot be paused. `resume()` restarts the saved time; if it
210
+ has already elapsed, the toast is dismissed.
211
+ - With `overflow: 'queue'`, new toasts wait without timers and are promoted
212
+ when a visible slot opens. Set `maxQueue` to bound that queue. With
213
+ `overflow: 'reject'`, `show()` returns `null`. `dismiss-oldest` replaces an
214
+ active toast, preferring timed, actionless toasts for eviction.
215
+ - `update(id, changes)` restarts an active timer; a queued item's timer starts
216
+ only on promotion. Changing `kind` re-applies that kind's default duration
217
+ unless the toast was given an explicit duration. `action: null` removes an
218
+ action.
219
+ - `runAction(id)` runs the callback, then dismisses by default.
220
+ `dismissAfterPress: false` keeps the toast visible. Callback errors propagate
221
+ to the caller (and, in managed UI use, Angular's `ErrorHandler`).
222
+ - No routing policy is built in. To clear timed notifications on navigation,
223
+ subscribe to your router in app code and call `dismiss(id)` as appropriate.
224
+
225
+ ## Testing app code
226
+
227
+ Use fake timers to make timer-dependent tests deterministic:
97
228
 
98
229
  ```ts
99
- vi.useFakeTimers();
100
- toast.show({ message: 'x' });
101
- vi.advanceTimersByTime(5000);
102
- expect(toast.active()).toEqual([]);
230
+ import { TestBed } from '@angular/core/testing';
231
+ import { afterEach, beforeEach, expect, it, vi } from 'vitest';
232
+ import { provideRiviniaToast, RiviniaToast } from '@rivinia/toast';
233
+
234
+ beforeEach(() => {
235
+ TestBed.configureTestingModule({ providers: [provideRiviniaToast()] });
236
+ vi.useFakeTimers();
237
+ });
238
+
239
+ afterEach(() => {
240
+ TestBed.resetTestingModule();
241
+ vi.useRealTimers();
242
+ });
243
+
244
+ it('auto-dismisses after the configured duration', () => {
245
+ const toast = TestBed.inject(RiviniaToast);
246
+ const id = toast.show({ message: 'Saved', duration: 1000 });
247
+
248
+ vi.advanceTimersByTime(1000);
249
+ expect(toast.active().some((item) => item.id === id)).toBe(false);
250
+ });
103
251
  ```
104
252
 
105
253
  ## License
@@ -0,0 +1,317 @@
1
+ import * as i0 from "@angular/core";
2
+ import { Component, computed, effect, input, output, signal } from "@angular/core";
3
+ import { resolveToastText } from "@rivinia/toast";
4
+ const LEAVE_MS = 150;
5
+ var RiviniaToastList = class RiviniaToastList {
6
+ manager = input(null, ...ngDevMode ? [{ debugName: "manager" }] : /* istanbul ignore next */ []);
7
+ toasts = input([], ...ngDevMode ? [{ debugName: "toasts" }] : /* istanbul ignore next */ []);
8
+ position = input("bottom-end", ...ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []);
9
+ animation = input("slide", ...ngDevMode ? [{ debugName: "animation" }] : /* istanbul ignore next */ []);
10
+ richColors = input(false, ...ngDevMode ? [{ debugName: "richColors" }] : /* istanbul ignore next */ []);
11
+ showCloseButton = input(true, ...ngDevMode ? [{ debugName: "showCloseButton" }] : /* istanbul ignore next */ []);
12
+ dismissed = output();
13
+ actionPressed = output();
14
+ leaving = signal([], ...ngDevMode ? [{ debugName: "leaving" }] : /* istanbul ignore next */ []);
15
+ seen = /* @__PURE__ */ new Set();
16
+ known = /* @__PURE__ */ new Map();
17
+ rendered = computed(() => {
18
+ const manager = this.manager();
19
+ const live = manager ? manager.active() : this.toasts();
20
+ const liveIds = new Set(live.map((t) => t.id));
21
+ const gone = this.leaving().filter((l) => !liveIds.has(l.toast.id));
22
+ return [...live.map((toast) => ({
23
+ toast,
24
+ leaving: false
25
+ })), ...gone.map((l) => ({
26
+ ...l,
27
+ leaving: true
28
+ }))];
29
+ }, ...ngDevMode ? [{ debugName: "rendered" }] : /* istanbul ignore next */ []);
30
+ constructor() {
31
+ effect(() => {
32
+ const manager = this.manager();
33
+ const live = manager ? manager.active() : this.toasts();
34
+ const ids = /* @__PURE__ */ new Set();
35
+ for (const toast of live) {
36
+ ids.add(toast.id);
37
+ this.known.set(toast.id, toast);
38
+ }
39
+ for (const id of this.seen) if (!ids.has(id)) this.beginLeave(id);
40
+ this.seen = ids;
41
+ });
42
+ }
43
+ textOf(toast) {
44
+ try {
45
+ return resolveToastText(toast.message);
46
+ } catch {
47
+ return "";
48
+ }
49
+ }
50
+ textOfAction(toast) {
51
+ const action = toast.action;
52
+ if (!action) return "";
53
+ try {
54
+ return resolveToastText(action.label);
55
+ } catch {
56
+ return "";
57
+ }
58
+ }
59
+ iconFor(_kind) {
60
+ return "";
61
+ }
62
+ pause(id) {
63
+ this.manager()?.pause(id);
64
+ }
65
+ resume(id) {
66
+ this.manager()?.resume(id);
67
+ }
68
+ close(id) {
69
+ const manager = this.manager();
70
+ if (manager) manager.dismiss(id);
71
+ else this.dismissed.emit(id);
72
+ }
73
+ act(id) {
74
+ const manager = this.manager();
75
+ if (manager) manager.runAction(id);
76
+ else this.actionPressed.emit(id);
77
+ }
78
+ beginLeave(id) {
79
+ if (typeof window === "undefined") return;
80
+ const current = this.snapshot(id);
81
+ if (!current) return;
82
+ const entry = { toast: current };
83
+ this.leaving.update((list) => [...list.filter((l) => l.toast.id !== id), entry]);
84
+ setTimeout(() => {
85
+ this.leaving.update((list) => list.filter((l) => l.toast.id !== id));
86
+ }, LEAVE_MS);
87
+ }
88
+ snapshot(id) {
89
+ return this.known.get(id) ?? null;
90
+ }
91
+ static ɵfac = i0.ɵɵngDeclareFactory({
92
+ minVersion: "12.0.0",
93
+ version: "22.2.2",
94
+ ngImport: i0,
95
+ type: RiviniaToastList,
96
+ deps: [],
97
+ target: i0.ɵɵFactoryTarget.Component
98
+ });
99
+ static ɵcmp = i0.ɵɵngDeclareComponent({
100
+ minVersion: "17.0.0",
101
+ version: "22.2.2",
102
+ type: RiviniaToastList,
103
+ isStandalone: true,
104
+ selector: "rv-toast-list",
105
+ inputs: {
106
+ manager: {
107
+ classPropertyName: "manager",
108
+ publicName: "manager",
109
+ isSignal: true,
110
+ isRequired: false,
111
+ transformFunction: null
112
+ },
113
+ toasts: {
114
+ classPropertyName: "toasts",
115
+ publicName: "toasts",
116
+ isSignal: true,
117
+ isRequired: false,
118
+ transformFunction: null
119
+ },
120
+ position: {
121
+ classPropertyName: "position",
122
+ publicName: "position",
123
+ isSignal: true,
124
+ isRequired: false,
125
+ transformFunction: null
126
+ },
127
+ animation: {
128
+ classPropertyName: "animation",
129
+ publicName: "animation",
130
+ isSignal: true,
131
+ isRequired: false,
132
+ transformFunction: null
133
+ },
134
+ richColors: {
135
+ classPropertyName: "richColors",
136
+ publicName: "richColors",
137
+ isSignal: true,
138
+ isRequired: false,
139
+ transformFunction: null
140
+ },
141
+ showCloseButton: {
142
+ classPropertyName: "showCloseButton",
143
+ publicName: "showCloseButton",
144
+ isSignal: true,
145
+ isRequired: false,
146
+ transformFunction: null
147
+ }
148
+ },
149
+ outputs: {
150
+ dismissed: "dismissed",
151
+ actionPressed: "actionPressed"
152
+ },
153
+ ngImport: i0,
154
+ template: `
155
+ <div
156
+ class="rv-toasts"
157
+ role="region"
158
+ aria-label="Notifications"
159
+ [attr.data-position]="position()"
160
+ [attr.data-animation]="animation()"
161
+ [attr.data-rich-colors]="richColors()"
162
+ >
163
+ @for (item of rendered(); track item.toast.id) {
164
+ <div
165
+ class="rv-toast"
166
+ [attr.data-kind]="item.toast.kind"
167
+ [attr.data-state]="item.leaving ? 'leaving' : item.toast.paused ? 'paused' : 'active'"
168
+ role="status"
169
+ [attr.aria-live]="item.toast.kind === 'error' ? 'assertive' : 'polite'"
170
+ (mouseenter)="pause(item.toast.id)"
171
+ (mouseleave)="resume(item.toast.id)"
172
+ (focusin)="pause(item.toast.id)"
173
+ (focusout)="resume(item.toast.id)"
174
+ >
175
+ <span class="rv-toast-icon" aria-hidden="true">{{ iconFor(item.toast.kind) }}</span>
176
+ <p class="rv-toast-message">{{ textOf(item.toast) }}</p>
177
+ @if (item.toast.action) {
178
+ <button type="button" class="rv-toast-action" (click)="act(item.toast.id)">
179
+ {{ textOfAction(item.toast) }}
180
+ </button>
181
+ }
182
+ @if (showCloseButton()) {
183
+ <button
184
+ type="button"
185
+ class="rv-toast-close"
186
+ (click)="close(item.toast.id)"
187
+ aria-label="Dismiss notification"
188
+ >
189
+ <span aria-hidden="true">&times;</span>
190
+ </button>
191
+ }
192
+ </div>
193
+ }
194
+ </div>
195
+ `,
196
+ isInline: true,
197
+ styles: [".rv-toasts{--rv-toast-bg: #1c1c1e;--rv-toast-fg: #f5f5f7;--rv-toast-muted: #a8a8a8;--rv-toast-border: #38383a;--rv-toast-radius: 12px;--rv-toast-shadow: 0 12px 32px rgba(0, 0, 0, .32);--rv-toast-gap: 8px;--rv-toast-width: 22rem;--rv-toast-duration: .18s;--rv-toast-easing: cubic-bezier(.21, 1.02, .73, 1);--rv-toast-success: #34c759;--rv-toast-error: #ff453a;--rv-toast-info: #0a84ff;--rv-toast-warning: #ff9f0a;position:fixed;z-index:var(--rv-toast-z, 1000);display:flex;flex-direction:column;gap:var(--rv-toast-gap);width:var(--rv-toast-width);max-width:calc(100dvw - 2rem);pointer-events:none}.rv-toasts>*{pointer-events:auto}@media(prefers-color-scheme:light){.rv-toasts{--rv-toast-bg: #ffffff;--rv-toast-fg: #111111;--rv-toast-muted: #555555;--rv-toast-border: #e0e0e0;--rv-toast-shadow: 0 12px 32px rgba(0, 0, 0, .18)}}.rv-toasts[data-position=top-start],.rv-toasts[data-position=bottom-start]{inset-inline-start:1rem}.rv-toasts[data-position=top-end],.rv-toasts[data-position=bottom-end]{inset-inline-end:1rem}.rv-toasts[data-position=top-center],.rv-toasts[data-position=bottom-center]{inset-inline:0;margin-inline:auto}.rv-toasts[data-position^=top]{inset-block-start:1rem}.rv-toasts[data-position^=bottom]{inset-block-end:1rem}.rv-toast{display:flex;align-items:flex-start;gap:10px;background:var(--rv-toast-bg);color:var(--rv-toast-fg);border:1px solid var(--rv-toast-border);border-inline-start:3px solid var(--rv-toast-muted);border-radius:var(--rv-toast-radius);box-shadow:var(--rv-toast-shadow);padding:12px 14px}.rv-toast[data-kind=success]{border-inline-start-color:var(--rv-toast-success)}.rv-toast[data-kind=error]{border-inline-start-color:var(--rv-toast-error)}.rv-toast[data-kind=info]{border-inline-start-color:var(--rv-toast-info)}.rv-toast[data-kind=warning]{border-inline-start-color:var(--rv-toast-warning)}.rv-toasts[data-rich-colors=true] .rv-toast[data-kind=success]{background:var(--rv-toast-success);color:#06130a}.rv-toasts[data-rich-colors=true] .rv-toast[data-kind=error]{background:var(--rv-toast-error);color:#1c0605}.rv-toasts[data-rich-colors=true] .rv-toast[data-kind=info]{background:var(--rv-toast-info);color:#04121f}.rv-toasts[data-rich-colors=true] .rv-toast[data-kind=warning]{background:var(--rv-toast-warning);color:#1d1204}.rv-toast-icon{flex:none;font-weight:700}.rv-toast[data-kind=success] .rv-toast-icon:before{content:\"\\2713\"}.rv-toast[data-kind=error] .rv-toast-icon:before{content:\"!\"}.rv-toast[data-kind=info] .rv-toast-icon:before{content:\"i\"}.rv-toast[data-kind=warning] .rv-toast-icon:before{content:\"\\26a0\"}.rv-toast-message{margin:0;flex:1;font-size:14px;line-height:1.45}.rv-toast-action,.rv-toast-close{flex:none;cursor:pointer;border-radius:8px;border:1px solid var(--rv-toast-border);background:transparent;color:inherit;padding:4px 10px;font-size:13px}.rv-toast-action:focus-visible,.rv-toast-close:focus-visible{outline:2px solid currentColor;outline-offset:2px}.rv-toasts[data-animation=slide] .rv-toast[data-state=active]{animation:rv-toast-in var(--rv-toast-duration) var(--rv-toast-easing)}.rv-toasts[data-animation=fade] .rv-toast[data-state=active]{animation:rv-toast-in-fade var(--rv-toast-duration) ease-out}.rv-toast[data-state=leaving]{animation:rv-toast-out .15s ease-in forwards}@keyframes rv-toast-in{0%{opacity:0;transform:translateY(12px)}}@keyframes rv-toast-in-fade{0%{opacity:0}}@keyframes rv-toast-out{to{opacity:0;transform:translateY(8px)}}@media(prefers-reduced-motion:reduce){.rv-toasts .rv-toast{animation:none}}\n"]
198
+ });
199
+ };
200
+ i0.ɵɵngDeclareClassMetadata({
201
+ minVersion: "12.0.0",
202
+ version: "22.2.2",
203
+ ngImport: i0,
204
+ type: RiviniaToastList,
205
+ decorators: [{
206
+ type: Component,
207
+ args: [{
208
+ selector: "rv-toast-list",
209
+ standalone: true,
210
+ template: `
211
+ <div
212
+ class="rv-toasts"
213
+ role="region"
214
+ aria-label="Notifications"
215
+ [attr.data-position]="position()"
216
+ [attr.data-animation]="animation()"
217
+ [attr.data-rich-colors]="richColors()"
218
+ >
219
+ @for (item of rendered(); track item.toast.id) {
220
+ <div
221
+ class="rv-toast"
222
+ [attr.data-kind]="item.toast.kind"
223
+ [attr.data-state]="item.leaving ? 'leaving' : item.toast.paused ? 'paused' : 'active'"
224
+ role="status"
225
+ [attr.aria-live]="item.toast.kind === 'error' ? 'assertive' : 'polite'"
226
+ (mouseenter)="pause(item.toast.id)"
227
+ (mouseleave)="resume(item.toast.id)"
228
+ (focusin)="pause(item.toast.id)"
229
+ (focusout)="resume(item.toast.id)"
230
+ >
231
+ <span class="rv-toast-icon" aria-hidden="true">{{ iconFor(item.toast.kind) }}</span>
232
+ <p class="rv-toast-message">{{ textOf(item.toast) }}</p>
233
+ @if (item.toast.action) {
234
+ <button type="button" class="rv-toast-action" (click)="act(item.toast.id)">
235
+ {{ textOfAction(item.toast) }}
236
+ </button>
237
+ }
238
+ @if (showCloseButton()) {
239
+ <button
240
+ type="button"
241
+ class="rv-toast-close"
242
+ (click)="close(item.toast.id)"
243
+ aria-label="Dismiss notification"
244
+ >
245
+ <span aria-hidden="true">&times;</span>
246
+ </button>
247
+ }
248
+ </div>
249
+ }
250
+ </div>
251
+ `,
252
+ styles: [".rv-toasts{--rv-toast-bg: #1c1c1e;--rv-toast-fg: #f5f5f7;--rv-toast-muted: #a8a8a8;--rv-toast-border: #38383a;--rv-toast-radius: 12px;--rv-toast-shadow: 0 12px 32px rgba(0, 0, 0, .32);--rv-toast-gap: 8px;--rv-toast-width: 22rem;--rv-toast-duration: .18s;--rv-toast-easing: cubic-bezier(.21, 1.02, .73, 1);--rv-toast-success: #34c759;--rv-toast-error: #ff453a;--rv-toast-info: #0a84ff;--rv-toast-warning: #ff9f0a;position:fixed;z-index:var(--rv-toast-z, 1000);display:flex;flex-direction:column;gap:var(--rv-toast-gap);width:var(--rv-toast-width);max-width:calc(100dvw - 2rem);pointer-events:none}.rv-toasts>*{pointer-events:auto}@media(prefers-color-scheme:light){.rv-toasts{--rv-toast-bg: #ffffff;--rv-toast-fg: #111111;--rv-toast-muted: #555555;--rv-toast-border: #e0e0e0;--rv-toast-shadow: 0 12px 32px rgba(0, 0, 0, .18)}}.rv-toasts[data-position=top-start],.rv-toasts[data-position=bottom-start]{inset-inline-start:1rem}.rv-toasts[data-position=top-end],.rv-toasts[data-position=bottom-end]{inset-inline-end:1rem}.rv-toasts[data-position=top-center],.rv-toasts[data-position=bottom-center]{inset-inline:0;margin-inline:auto}.rv-toasts[data-position^=top]{inset-block-start:1rem}.rv-toasts[data-position^=bottom]{inset-block-end:1rem}.rv-toast{display:flex;align-items:flex-start;gap:10px;background:var(--rv-toast-bg);color:var(--rv-toast-fg);border:1px solid var(--rv-toast-border);border-inline-start:3px solid var(--rv-toast-muted);border-radius:var(--rv-toast-radius);box-shadow:var(--rv-toast-shadow);padding:12px 14px}.rv-toast[data-kind=success]{border-inline-start-color:var(--rv-toast-success)}.rv-toast[data-kind=error]{border-inline-start-color:var(--rv-toast-error)}.rv-toast[data-kind=info]{border-inline-start-color:var(--rv-toast-info)}.rv-toast[data-kind=warning]{border-inline-start-color:var(--rv-toast-warning)}.rv-toasts[data-rich-colors=true] .rv-toast[data-kind=success]{background:var(--rv-toast-success);color:#06130a}.rv-toasts[data-rich-colors=true] .rv-toast[data-kind=error]{background:var(--rv-toast-error);color:#1c0605}.rv-toasts[data-rich-colors=true] .rv-toast[data-kind=info]{background:var(--rv-toast-info);color:#04121f}.rv-toasts[data-rich-colors=true] .rv-toast[data-kind=warning]{background:var(--rv-toast-warning);color:#1d1204}.rv-toast-icon{flex:none;font-weight:700}.rv-toast[data-kind=success] .rv-toast-icon:before{content:\"\\2713\"}.rv-toast[data-kind=error] .rv-toast-icon:before{content:\"!\"}.rv-toast[data-kind=info] .rv-toast-icon:before{content:\"i\"}.rv-toast[data-kind=warning] .rv-toast-icon:before{content:\"\\26a0\"}.rv-toast-message{margin:0;flex:1;font-size:14px;line-height:1.45}.rv-toast-action,.rv-toast-close{flex:none;cursor:pointer;border-radius:8px;border:1px solid var(--rv-toast-border);background:transparent;color:inherit;padding:4px 10px;font-size:13px}.rv-toast-action:focus-visible,.rv-toast-close:focus-visible{outline:2px solid currentColor;outline-offset:2px}.rv-toasts[data-animation=slide] .rv-toast[data-state=active]{animation:rv-toast-in var(--rv-toast-duration) var(--rv-toast-easing)}.rv-toasts[data-animation=fade] .rv-toast[data-state=active]{animation:rv-toast-in-fade var(--rv-toast-duration) ease-out}.rv-toast[data-state=leaving]{animation:rv-toast-out .15s ease-in forwards}@keyframes rv-toast-in{0%{opacity:0;transform:translateY(12px)}}@keyframes rv-toast-in-fade{0%{opacity:0}}@keyframes rv-toast-out{to{opacity:0;transform:translateY(8px)}}@media(prefers-reduced-motion:reduce){.rv-toasts .rv-toast{animation:none}}\n"]
253
+ }]
254
+ }],
255
+ ctorParameters: () => [],
256
+ propDecorators: {
257
+ manager: [{
258
+ type: i0.Input,
259
+ args: [{
260
+ isSignal: true,
261
+ alias: "manager",
262
+ required: false
263
+ }]
264
+ }],
265
+ toasts: [{
266
+ type: i0.Input,
267
+ args: [{
268
+ isSignal: true,
269
+ alias: "toasts",
270
+ required: false
271
+ }]
272
+ }],
273
+ position: [{
274
+ type: i0.Input,
275
+ args: [{
276
+ isSignal: true,
277
+ alias: "position",
278
+ required: false
279
+ }]
280
+ }],
281
+ animation: [{
282
+ type: i0.Input,
283
+ args: [{
284
+ isSignal: true,
285
+ alias: "animation",
286
+ required: false
287
+ }]
288
+ }],
289
+ richColors: [{
290
+ type: i0.Input,
291
+ args: [{
292
+ isSignal: true,
293
+ alias: "richColors",
294
+ required: false
295
+ }]
296
+ }],
297
+ showCloseButton: [{
298
+ type: i0.Input,
299
+ args: [{
300
+ isSignal: true,
301
+ alias: "showCloseButton",
302
+ required: false
303
+ }]
304
+ }],
305
+ dismissed: [{
306
+ type: i0.Output,
307
+ args: ["dismissed"]
308
+ }],
309
+ actionPressed: [{
310
+ type: i0.Output,
311
+ args: ["actionPressed"]
312
+ }]
313
+ }
314
+ });
315
+ export { RiviniaToastList };
316
+
317
+ //# sourceMappingURL=rivinia-toast-ui.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rivinia-toast-ui.mjs","names":[],"sources":["../../../projects/toast/ui/src/lib/toast-list.ts"],"sourcesContent":["import { Component, computed, effect, input, output, signal } from '@angular/core';\nimport type { Toast, ToastKind, ToastOptions } from '@rivinia/toast';\nimport { resolveToastText } from '@rivinia/toast';\nimport type { ToastAnimation, ToastPosition } from './ui-toast-types';\n\nconst LEAVE_MS = 150;\n\n/**\n * The structural slice of `RiviniaToast` the list actually drives. The full\n * manager satisfies it from any module realm; test doubles need five methods.\n * When both `manager` and `toasts` are given, the manager wins (single\n * source of truth — the UI never races Core).\n */\nexport interface RiviniaToastListManager {\n readonly active: () => readonly Toast[];\n show(options: ToastOptions): number | null;\n dismiss(id: number): boolean;\n pause(id: number): boolean;\n resume(id: number): boolean;\n runAction(id: number): boolean;\n}\n\n/** A toast snapshot kept rendering while its 150 ms exit plays. */\ninterface LeavingToast {\n readonly toast: Toast;\n}\n\n/**\n * Ready-made toast stack. Two modes (manager wins when both are given):\n *\n * - Managed: `[manager]=\"injectRiviniaToast()\"` — renders `active()`,\n * hover pauses, action button runs `runAction` (throws propagate to the\n * app's `ErrorHandler`), X dismisses. Dismissed items play a 150 ms exit\n * from their last snapshot, then vanish (browser only — SSR drops them).\n * - Presentational: `[toasts]=\"[...]\"` + `(dismissed)/(actionPressed)` —\n * no manager needed. Timer/pause state is yours.\n *\n * Each item is `role=\"status\"` (polite by default; `error` kind asserts).\n * Stack order is `active()` order (oldest first), anchored at `position`.\n */\n@Component({\n selector: 'rv-toast-list',\n standalone: true,\n template: `\n <div\n class=\"rv-toasts\"\n role=\"region\"\n aria-label=\"Notifications\"\n [attr.data-position]=\"position()\"\n [attr.data-animation]=\"animation()\"\n [attr.data-rich-colors]=\"richColors()\"\n >\n @for (item of rendered(); track item.toast.id) {\n <div\n class=\"rv-toast\"\n [attr.data-kind]=\"item.toast.kind\"\n [attr.data-state]=\"item.leaving ? 'leaving' : item.toast.paused ? 'paused' : 'active'\"\n role=\"status\"\n [attr.aria-live]=\"item.toast.kind === 'error' ? 'assertive' : 'polite'\"\n (mouseenter)=\"pause(item.toast.id)\"\n (mouseleave)=\"resume(item.toast.id)\"\n (focusin)=\"pause(item.toast.id)\"\n (focusout)=\"resume(item.toast.id)\"\n >\n <span class=\"rv-toast-icon\" aria-hidden=\"true\">{{ iconFor(item.toast.kind) }}</span>\n <p class=\"rv-toast-message\">{{ textOf(item.toast) }}</p>\n @if (item.toast.action) {\n <button type=\"button\" class=\"rv-toast-action\" (click)=\"act(item.toast.id)\">\n {{ textOfAction(item.toast) }}\n </button>\n }\n @if (showCloseButton()) {\n <button\n type=\"button\"\n class=\"rv-toast-close\"\n (click)=\"close(item.toast.id)\"\n aria-label=\"Dismiss notification\"\n >\n <span aria-hidden=\"true\">&times;</span>\n </button>\n }\n </div>\n }\n </div>\n `,\n styles: [\n `\n .rv-toasts {\n --rv-toast-bg: #1c1c1e;\n --rv-toast-fg: #f5f5f7;\n --rv-toast-muted: #a8a8a8;\n --rv-toast-border: #38383a;\n --rv-toast-radius: 12px;\n --rv-toast-shadow: 0 12px 32px rgba(0, 0, 0, 0.32);\n --rv-toast-gap: 8px;\n --rv-toast-width: 22rem;\n --rv-toast-duration: 180ms;\n --rv-toast-easing: cubic-bezier(0.21, 1.02, 0.73, 1);\n --rv-toast-success: #34c759;\n --rv-toast-error: #ff453a;\n --rv-toast-info: #0a84ff;\n --rv-toast-warning: #ff9f0a;\n position: fixed;\n z-index: var(--rv-toast-z, 1000);\n display: flex;\n flex-direction: column;\n gap: var(--rv-toast-gap);\n width: var(--rv-toast-width);\n max-width: calc(100dvw - 2rem);\n pointer-events: none;\n }\n .rv-toasts > * {\n pointer-events: auto;\n }\n @media (prefers-color-scheme: light) {\n .rv-toasts {\n --rv-toast-bg: #ffffff;\n --rv-toast-fg: #111111;\n --rv-toast-muted: #555555;\n --rv-toast-border: #e0e0e0;\n --rv-toast-shadow: 0 12px 32px rgba(0, 0, 0, 0.18);\n }\n }\n .rv-toasts[data-position='top-start'],\n .rv-toasts[data-position='bottom-start'] {\n inset-inline-start: 1rem;\n }\n .rv-toasts[data-position='top-end'],\n .rv-toasts[data-position='bottom-end'] {\n inset-inline-end: 1rem;\n }\n .rv-toasts[data-position='top-center'],\n .rv-toasts[data-position='bottom-center'] {\n inset-inline: 0;\n margin-inline: auto;\n }\n .rv-toasts[data-position^='top'] {\n inset-block-start: 1rem;\n }\n .rv-toasts[data-position^='bottom'] {\n inset-block-end: 1rem;\n }\n .rv-toast {\n display: flex;\n align-items: flex-start;\n gap: 10px;\n background: var(--rv-toast-bg);\n color: var(--rv-toast-fg);\n border: 1px solid var(--rv-toast-border);\n border-inline-start: 3px solid var(--rv-toast-muted);\n border-radius: var(--rv-toast-radius);\n box-shadow: var(--rv-toast-shadow);\n padding: 12px 14px;\n }\n .rv-toast[data-kind='success'] {\n border-inline-start-color: var(--rv-toast-success);\n }\n .rv-toast[data-kind='error'] {\n border-inline-start-color: var(--rv-toast-error);\n }\n .rv-toast[data-kind='info'] {\n border-inline-start-color: var(--rv-toast-info);\n }\n .rv-toast[data-kind='warning'] {\n border-inline-start-color: var(--rv-toast-warning);\n }\n .rv-toasts[data-rich-colors='true'] .rv-toast[data-kind='success'] {\n background: var(--rv-toast-success);\n color: #06130a;\n }\n .rv-toasts[data-rich-colors='true'] .rv-toast[data-kind='error'] {\n background: var(--rv-toast-error);\n color: #1c0605;\n }\n .rv-toasts[data-rich-colors='true'] .rv-toast[data-kind='info'] {\n background: var(--rv-toast-info);\n color: #04121f;\n }\n .rv-toasts[data-rich-colors='true'] .rv-toast[data-kind='warning'] {\n background: var(--rv-toast-warning);\n color: #1d1204;\n }\n .rv-toast-icon {\n flex: none;\n font-weight: 700;\n }\n .rv-toast[data-kind='success'] .rv-toast-icon::before {\n content: '✓';\n }\n .rv-toast[data-kind='error'] .rv-toast-icon::before {\n content: '!';\n }\n .rv-toast[data-kind='info'] .rv-toast-icon::before {\n content: 'i';\n }\n .rv-toast[data-kind='warning'] .rv-toast-icon::before {\n content: '⚠';\n }\n .rv-toast-message {\n margin: 0;\n flex: 1;\n font-size: 14px;\n line-height: 1.45;\n }\n .rv-toast-action,\n .rv-toast-close {\n flex: none;\n cursor: pointer;\n border-radius: 8px;\n border: 1px solid var(--rv-toast-border);\n background: transparent;\n color: inherit;\n padding: 4px 10px;\n font-size: 13px;\n }\n .rv-toast-action:focus-visible,\n .rv-toast-close:focus-visible {\n outline: 2px solid currentColor;\n outline-offset: 2px;\n }\n .rv-toasts[data-animation='slide'] .rv-toast[data-state='active'] {\n animation: rv-toast-in var(--rv-toast-duration) var(--rv-toast-easing);\n }\n .rv-toasts[data-animation='fade'] .rv-toast[data-state='active'] {\n animation: rv-toast-in-fade var(--rv-toast-duration) ease-out;\n }\n .rv-toast[data-state='leaving'] {\n animation: rv-toast-out 150ms ease-in forwards;\n }\n @keyframes rv-toast-in {\n from {\n opacity: 0;\n transform: translateY(12px);\n }\n }\n @keyframes rv-toast-in-fade {\n from {\n opacity: 0;\n }\n }\n @keyframes rv-toast-out {\n to {\n opacity: 0;\n transform: translateY(8px);\n }\n }\n @media (prefers-reduced-motion: reduce) {\n .rv-toasts .rv-toast {\n animation: none;\n }\n }\n `,\n ],\n})\nexport class RiviniaToastList {\n /** Managed mode: the service (or a structural test double). Wins over `toasts`. */\n readonly manager = input<RiviniaToastListManager | null>(null);\n /** Presentational mode: snapshots you own (timers/pause are yours). */\n readonly toasts = input<readonly Toast[]>([]);\n /** Stack anchor. @default 'bottom-end' */\n readonly position = input<ToastPosition>('bottom-end');\n /** Entrance animation. @default 'slide' */\n readonly animation = input<ToastAnimation>('slide');\n /** Kind-tinted rail (default) vs full-bleed kind colors. @default false */\n readonly richColors = input<boolean>(false);\n /** Render the X button. @default true */\n readonly showCloseButton = input<boolean>(true);\n\n /** Presentational-mode dismiss (id). Managed mode settles via the manager. */\n readonly dismissed = output<number>();\n /** Presentational-mode action press (id). */\n readonly actionPressed = output<number>();\n\n private readonly leaving = signal<readonly LeavingToast[]>([]);\n private seen = new Set<number>();\n private readonly known = new Map<number, Toast>();\n\n /** Rendered rows: live items plus exit-playing snapshots (browser only). */\n protected readonly rendered = computed(() => {\n const manager = this.manager();\n const live: readonly Toast[] = manager ? manager.active() : this.toasts();\n const liveIds = new Set(live.map((t) => t.id));\n const gone = this.leaving().filter((l) => !liveIds.has(l.toast.id));\n return [\n ...live.map((toast) => ({ toast, leaving: false })),\n ...gone.map((l) => ({ ...l, leaving: true })),\n ];\n });\n\n constructor() {\n effect(() => {\n // Track removals: anything that was live and isn't anymore plays exit.\n // Every run also refreshes the last-known snapshots the exit needs.\n const manager = this.manager();\n const live = manager ? manager.active() : this.toasts();\n const ids = new Set<number>();\n for (const toast of live) {\n ids.add(toast.id);\n this.known.set(toast.id, toast);\n }\n for (const id of this.seen) {\n if (!ids.has(id)) this.beginLeave(id);\n }\n this.seen = ids;\n });\n }\n\n protected textOf(toast: Toast): string {\n try {\n return resolveToastText(toast.message);\n } catch {\n return '';\n }\n }\n\n protected textOfAction(toast: Toast): string {\n const action = toast.action;\n if (!action) return '';\n try {\n return resolveToastText(action.label);\n } catch {\n return '';\n }\n }\n\n protected iconFor(_kind: ToastKind): string {\n // Glyph comes from CSS ::before per kind (the span stays empty for AT).\n return '';\n }\n\n protected pause(id: number): void {\n this.manager()?.pause(id);\n }\n\n protected resume(id: number): void {\n this.manager()?.resume(id);\n }\n\n protected close(id: number): void {\n const manager = this.manager();\n if (manager) manager.dismiss(id);\n else this.dismissed.emit(id);\n }\n\n protected act(id: number): void {\n const manager = this.manager();\n if (manager) manager.runAction(id);\n else this.actionPressed.emit(id);\n }\n\n private beginLeave(id: number): void {\n if (typeof window === 'undefined') return; // SSR: drop, never hang render\n const current = this.snapshot(id);\n if (!current) return;\n const entry: LeavingToast = { toast: current };\n this.leaving.update((list) => [...list.filter((l) => l.toast.id !== id), entry]);\n setTimeout(() => {\n this.leaving.update((list) => list.filter((l) => l.toast.id !== id));\n }, LEAVE_MS);\n }\n\n private snapshot(id: number): Toast | null {\n return this.known.get(id) ?? null;\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rivinia/toast",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Headless toast queue for Angular: signals, pause-on-hover timers, actions.",
5
5
  "keywords": [
6
6
  "angular",
@@ -24,6 +24,10 @@
24
24
  ".": {
25
25
  "types": "./types/rivinia-toast.d.ts",
26
26
  "default": "./fesm2022/rivinia-toast.mjs"
27
+ },
28
+ "./ui": {
29
+ "types": "./types/rivinia-toast-ui.d.ts",
30
+ "default": "./fesm2022/rivinia-toast-ui.mjs"
27
31
  }
28
32
  },
29
33
  "type": "module",
@@ -0,0 +1,111 @@
1
+ import { Toast, ToastKind, ToastOptions } from "@rivinia/toast";
2
+ import * as i0 from "@angular/core";
3
+ /**
4
+ * Stack anchor. Logical (`start`/`end`) variants are RTL-aware. The user
5
+ * picks exactly one — default `'bottom-end'` (sonner-compatible default).
6
+ * Items render in `active()` order (oldest first); the container anchors
7
+ * that end of the viewport.
8
+ */
9
+ type ToastPosition = 'top-start' | 'top-center' | 'top-end' | 'bottom-start' | 'bottom-center' | 'bottom-end';
10
+ /** Entrance animation. Exit is a quick fade (see `RiviniaToastList`). */
11
+ type ToastAnimation = 'slide' | 'fade' | 'none';
12
+ /**
13
+ * The structural slice of `RiviniaToast` the list actually drives. The full
14
+ * manager satisfies it from any module realm; test doubles need five methods.
15
+ * When both `manager` and `toasts` are given, the manager wins (single
16
+ * source of truth — the UI never races Core).
17
+ */
18
+ interface RiviniaToastListManager {
19
+ readonly active: () => readonly Toast[];
20
+ show(options: ToastOptions): number | null;
21
+ dismiss(id: number): boolean;
22
+ pause(id: number): boolean;
23
+ resume(id: number): boolean;
24
+ runAction(id: number): boolean;
25
+ }
26
+ /**
27
+ * Ready-made toast stack. Two modes (manager wins when both are given):
28
+ *
29
+ * - Managed: `[manager]="injectRiviniaToast()"` — renders `active()`,
30
+ * hover pauses, action button runs `runAction` (throws propagate to the
31
+ * app's `ErrorHandler`), X dismisses. Dismissed items play a 150 ms exit
32
+ * from their last snapshot, then vanish (browser only — SSR drops them).
33
+ * - Presentational: `[toasts]="[...]"` + `(dismissed)/(actionPressed)` —
34
+ * no manager needed. Timer/pause state is yours.
35
+ *
36
+ * Each item is `role="status"` (polite by default; `error` kind asserts).
37
+ * Stack order is `active()` order (oldest first), anchored at `position`.
38
+ */
39
+ export declare class RiviniaToastList {
40
+ /** Managed mode: the service (or a structural test double). Wins over `toasts`. */
41
+ readonly manager: import("@angular/core").InputSignal<RiviniaToastListManager | null>;
42
+ /** Presentational mode: snapshots you own (timers/pause are yours). */
43
+ readonly toasts: import("@angular/core").InputSignal<readonly Toast[]>;
44
+ /** Stack anchor. @default 'bottom-end' */
45
+ readonly position: import("@angular/core").InputSignal<ToastPosition>;
46
+ /** Entrance animation. @default 'slide' */
47
+ readonly animation: import("@angular/core").InputSignal<ToastAnimation>;
48
+ /** Kind-tinted rail (default) vs full-bleed kind colors. @default false */
49
+ readonly richColors: import("@angular/core").InputSignal<boolean>;
50
+ /** Render the X button. @default true */
51
+ readonly showCloseButton: import("@angular/core").InputSignal<boolean>;
52
+ /** Presentational-mode dismiss (id). Managed mode settles via the manager. */
53
+ readonly dismissed: import("@angular/core").OutputEmitterRef<number>;
54
+ /** Presentational-mode action press (id). */
55
+ readonly actionPressed: import("@angular/core").OutputEmitterRef<number>;
56
+ private readonly leaving;
57
+ private seen;
58
+ private readonly known;
59
+ /** Rendered rows: live items plus exit-playing snapshots (browser only). */
60
+ protected readonly rendered: import("@angular/core").Signal<{
61
+ toast: Toast;
62
+ leaving: boolean;
63
+ }[]>;
64
+ constructor();
65
+ protected textOf(toast: Toast): string;
66
+ protected textOfAction(toast: Toast): string;
67
+ protected iconFor(_kind: ToastKind): string;
68
+ protected pause(id: number): void;
69
+ protected resume(id: number): void;
70
+ protected close(id: number): void;
71
+ protected act(id: number): void;
72
+ private beginLeave;
73
+ private snapshot;
74
+ static ɵfac: i0.ɵɵFactoryDeclaration<RiviniaToastList, never>;
75
+ static ɵcmp: i0.ɵɵComponentDeclaration<RiviniaToastList, "rv-toast-list", never, {
76
+ "manager": {
77
+ "alias": "manager";
78
+ "required": false;
79
+ "isSignal": true;
80
+ };
81
+ "toasts": {
82
+ "alias": "toasts";
83
+ "required": false;
84
+ "isSignal": true;
85
+ };
86
+ "position": {
87
+ "alias": "position";
88
+ "required": false;
89
+ "isSignal": true;
90
+ };
91
+ "animation": {
92
+ "alias": "animation";
93
+ "required": false;
94
+ "isSignal": true;
95
+ };
96
+ "richColors": {
97
+ "alias": "richColors";
98
+ "required": false;
99
+ "isSignal": true;
100
+ };
101
+ "showCloseButton": {
102
+ "alias": "showCloseButton";
103
+ "required": false;
104
+ "isSignal": true;
105
+ };
106
+ }, {
107
+ "dismissed": "dismissed";
108
+ "actionPressed": "actionPressed";
109
+ }, never, never, true, never>;
110
+ }
111
+ export type { RiviniaToastListManager, ToastAnimation, ToastPosition };