@rivinia/toast 0.3.0 → 0.4.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.
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ dedupe, overflow policies — zero DOM, zero styles, zero router dependency.
|
|
|
8
8
|
A future `@rivinia/toast/ui` entry point will render this state; Core never
|
|
9
9
|
imports UI.
|
|
10
10
|
|
|
11
|
-
- Version: **0.
|
|
11
|
+
- Version: **0.4.0**
|
|
12
12
|
- Requires: **Angular `^22.0.0`**, `tslib`
|
|
13
13
|
|
|
14
14
|
## Install
|
|
@@ -47,20 +47,20 @@ export class Checkout {
|
|
|
47
47
|
```html
|
|
48
48
|
<!-- your outlet (or the future @rivinia/toast/ui): -->
|
|
49
49
|
@for (t of toast.active(); track t.id) {
|
|
50
|
-
|
|
50
|
+
<div [attr.data-kind]="t.kind">{{ resolveToastText(t.message) }}</div>
|
|
51
51
|
}
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
## API
|
|
55
55
|
|
|
56
|
-
| Export
|
|
57
|
-
|
|
58
|
-
| `provideRiviniaToast(config?)` | provider
|
|
59
|
-
| `RiviniaToast`
|
|
60
|
-
| `injectRiviniaToast()`
|
|
61
|
-
| `resolveToastText(msg)`
|
|
62
|
-
| `clampDuration(v, fallback)`
|
|
63
|
-
| types
|
|
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
64
|
|
|
65
65
|
`show()` returns the stable id, or `null` under `'reject'` (or a full finite
|
|
66
66
|
queue). `dismiss/update/pause/resume/runAction` return `false` for unknown ids
|
|
@@ -80,18 +80,51 @@ import { filter } from 'rxjs';
|
|
|
80
80
|
import { NavigationStart } from '@angular/router';
|
|
81
81
|
|
|
82
82
|
// Clear timed toasts on navigation (BRT behavior, 3 lines, no library peer needed):
|
|
83
|
-
router.events
|
|
83
|
+
router.events
|
|
84
|
+
.pipe(filter((e) => e instanceof NavigationStart))
|
|
84
85
|
.subscribe(() => toast.active().forEach((t) => t.duration > 0 && toast.dismiss(t.id)));
|
|
85
86
|
|
|
86
87
|
// Backend-shaped errors stay in your error mapper, then:
|
|
87
|
-
toast.show({
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
toast.show({
|
|
89
|
+
message: 'Order failed',
|
|
90
|
+
kind: 'error',
|
|
91
|
+
action: {
|
|
92
|
+
label: 'Retry',
|
|
93
|
+
onPress: () => retry(),
|
|
94
|
+
},
|
|
95
|
+
}); // dismisses after press unless dismissAfterPress: false
|
|
90
96
|
```
|
|
91
97
|
|
|
92
|
-
Future UI
|
|
93
|
-
hover **and** keyboard focus (WCAG 2.2.1 timing control), and
|
|
94
|
-
dismiss button —
|
|
98
|
+
Future UI renders inside `role="region" aria-live="polite"`, pauses on
|
|
99
|
+
hover **and** keyboard focus (WCAG 2.2.1 timing control), and offers a labelled
|
|
100
|
+
dismiss button — and now it exists:
|
|
101
|
+
|
|
102
|
+
## Ready-made UI (`@rivinia/toast/ui`)
|
|
103
|
+
|
|
104
|
+
Optional secondary entry point. Zero new runtime dependencies, no design
|
|
105
|
+
system. Headless consumers never load it (proven: core FESM has zero UI
|
|
106
|
+
markers).
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { RiviniaToastList } from '@rivinia/toast/ui';
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```html
|
|
113
|
+
<!-- Managed: signals, hover-pause, actions, X button, exit animation -->
|
|
114
|
+
<rv-toast-list [manager]="injectRiviniaToast()" [position]="'bottom-end'" />
|
|
115
|
+
|
|
116
|
+
<!-- Presentational: you own the snapshots (no manager needed) -->
|
|
117
|
+
<rv-toast-list [toasts]="items" (dismissed)="remove($event)" (actionPressed)="act($event)" />
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Positions (user-picked, RTL-aware): `top/bottom × start/center/end`
|
|
121
|
+
(default `bottom-end`, sonner-compatible). `richColors` swaps the kind rail
|
|
122
|
+
for full-bleed kind colors. Items are `role="status"` (`assertive` for
|
|
123
|
+
errors); stack order is `active()` order; hover/focus pauses via
|
|
124
|
+
`pause()/resume()`; throwing actions propagate to your `ErrorHandler`
|
|
125
|
+
(never swallowed). Dismissed rows play a 150 ms exit from their last
|
|
126
|
+
snapshot on the browser (SSR drops them — renders never hang). When both
|
|
127
|
+
`manager` and `toasts` are given, the manager wins: the UI never races Core.
|
|
95
128
|
|
|
96
129
|
## Testing your app code
|
|
97
130
|
|
|
@@ -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">×</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">×</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\">×</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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"}
|
|
@@ -136,6 +136,7 @@ var RiviniaToast = class RiviniaToast {
|
|
|
136
136
|
});
|
|
137
137
|
if (removed) {
|
|
138
138
|
this.forgetDedupe(id);
|
|
139
|
+
this._generations.delete(id);
|
|
139
140
|
this.promote();
|
|
140
141
|
}
|
|
141
142
|
return removed;
|
|
@@ -144,6 +145,7 @@ var RiviniaToast = class RiviniaToast {
|
|
|
144
145
|
this.clearTimers();
|
|
145
146
|
this._paused.clear();
|
|
146
147
|
this._explicitDuration.clear();
|
|
148
|
+
this._generations.clear();
|
|
147
149
|
this._active.set([]);
|
|
148
150
|
this._queue.set([]);
|
|
149
151
|
this._dedupeByKey.clear();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rivinia-toast.mjs","names":[],"sources":["../../../projects/toast/src/lib/toast-utils.ts","../../../projects/toast/src/lib/toast-config.ts","../../../projects/toast/src/lib/rivinia-toast.ts","../../../projects/toast/src/lib/provide-toast.ts"],"sourcesContent":["import type { Signal } from '@angular/core';\nimport type { ToastMessage } from './toast-types';\n\n/**\n * Reads a toast message regardless of shape. Future UI entry points call this\n * in templates — one place to support `string | Signal<string>`.\n */\nexport function resolveToastText(message: ToastMessage): string {\n return typeof message === 'string' ? message : (message as Signal<string>)();\n}\n\n/**\n * Normalizes a duration into a safe non-negative integer.\n * `0`, negatives, `NaN` and non-finite values all mean persistent (no timer).\n * Rationale: a `setTimeout(Infinity)` silently overflows to ~1ms in browsers —\n * converting to persistent is the only non-surprising mapping.\n */\nexport function clampDuration(value: number | undefined, fallback: number): number {\n const candidate = value === undefined ? fallback : value;\n if (typeof candidate !== 'number' || !Number.isFinite(candidate) || candidate <= 0) {\n return 0;\n }\n return Math.floor(candidate);\n}\n","import { InjectionToken } from '@angular/core';\nimport type { ToastKind, ToastOverflowPolicy } from './toast-types';\n\n/**\n * Global configuration for `@rivinia/toast`.\n *\n * Every field is optional. Per-toast options always win over globals.\n * URLs, icons, positions, styles: intentionally absent — presentation is\n * app-owned (a future `@rivinia/toast/ui` entry point will consume this Core).\n */\nexport interface RiviniaToastConfig {\n /**\n * Default auto-dismiss delay in milliseconds.\n * @default 5000\n */\n duration?: number;\n /**\n * Per-kind default delays. A per-toast `duration` beats both levels.\n * @default {} (every kind falls back to `duration`)\n */\n durations?: Partial<Record<ToastKind, number>>;\n /**\n * Maximum simultaneously visible (active) toasts. Clamped to ≥ 1.\n * @default 5 (industry standard: shadcn `TOAST_LIMIT = 3`, BRT `MAX_VISIBLE = 5`)\n */\n maxVisible?: number;\n /**\n * Maximum queued toasts when `overflow: 'queue'`. Further shows are rejected.\n * @default Infinity (bounded only if you bound it — an unbounded queue is a\n * documented leak risk for hot paths; prefer `'dismiss-oldest'` there)\n */\n maxQueue?: number;\n /**\n * What happens when a new toast arrives while `maxVisible` are active.\n * - `'dismiss-oldest'`: dismiss the oldest active toast, show the new one.\n * - `'queue'`: hold the new toast (no timer runs) until room frees up.\n * - `'reject'`: `show()` returns `null`; nothing changes.\n * @default 'dismiss-oldest'\n */\n overflow?: ToastOverflowPolicy;\n}\n\n/**\n * Internal configuration token. Consumers must use `provideRiviniaToast()`.\n * @internal\n */\nexport const RIVINIA_TOAST_CONFIG = new InjectionToken<RiviniaToastConfig>(\n 'RIVINIA_TOAST_CONFIG',\n);\n\nexport const DEFAULT_DURATION = 5000;\nexport const DEFAULT_MAX_VISIBLE = 5;\n","import { DestroyRef, inject, Service, signal } from '@angular/core';\nimport {\n DEFAULT_DURATION,\n DEFAULT_MAX_VISIBLE,\n RIVINIA_TOAST_CONFIG,\n type RiviniaToastConfig,\n} from './toast-config';\nimport type {\n Toast,\n ToastChanges,\n ToastKind,\n ToastOptions,\n} from './toast-types';\nimport { clampDuration } from './toast-utils';\n\ninterface TimerRecord {\n handle: ReturnType<typeof setTimeout>;\n startedAt: number;\n generation: number;\n}\n\n/**\n * Headless toast queue owner.\n *\n * State: two readonly signals — `active` (visible, capped) and `queue`\n * (overflow waiting, timerless). Who changes it: only the methods below.\n * Timers: one `setTimeout` per active timed toast, guarded by a per-toast\n * generation so a stale fire can never dismiss an updated toast. No DOM,\n * no styles, no router — presentation and navigation policy are app-owned\n * (see README recipes).\n */\n@Service()\nexport class RiviniaToast {\n private readonly config = inject<RiviniaToastConfig>(RIVINIA_TOAST_CONFIG, {\n optional: true,\n });\n\n private readonly _active = signal<Toast[]>([]);\n private readonly _queue = signal<Toast[]>([]);\n\n /** Visible toasts, oldest first. Never exceeds effective `maxVisible`. */\n readonly active = this._active.asReadonly();\n /** Overflow waiting for room. No timers run while queued. */\n readonly queue = this._queue.asReadonly();\n\n private readonly _timers = new Map<number, TimerRecord>();\n private readonly _paused = new Set<number>();\n /** Ids whose duration was set explicitly (per-toast override wins forever). */\n private readonly _explicitDuration = new Set<number>();\n private readonly _generations = new Map<number, number>();\n private readonly _dedupeByKey = new Map<string, number>();\n private readonly _dedupeKeyById = new Map<number, string>();\n private _nextId = 0;\n\n constructor() {\n inject(DestroyRef).onDestroy(() => this.clearTimers());\n }\n\n /**\n * Shows a toast. Returns its stable id, or `null` when rejected by the\n * `'reject'` overflow policy (or a full finite queue).\n */\n show(options: ToastOptions): number | null {\n if (options.dedupeKey !== undefined) {\n const existing = this.findByDedupeKey(options.dedupeKey);\n if (existing !== null) {\n this.update(existing, {\n message: options.message,\n kind: options.kind,\n duration: options.duration,\n action: options.action ?? null,\n });\n return existing;\n }\n }\n\n const id = ++this._nextId;\n const kind = options.kind ?? 'info';\n const duration = this.resolveDuration(kind, options.duration);\n if (options.duration !== undefined) this._explicitDuration.add(id);\n const toast: Toast = {\n id,\n kind,\n message: options.message,\n duration,\n remaining: duration,\n paused: false,\n action: options.action ?? null,\n };\n if (options.dedupeKey !== undefined) {\n this._dedupeByKey.set(options.dedupeKey, id);\n this._dedupeKeyById.set(id, options.dedupeKey);\n }\n\n if (this._active().length < this.maxVisible()) {\n this._active.update((list) => [...list, toast]);\n this.arm(id, duration);\n return id;\n }\n\n switch (this.config?.overflow ?? 'dismiss-oldest') {\n case 'queue': {\n if (this._queue().length >= this.maxQueue()) {\n this.forgetDedupe(id);\n return null;\n }\n this._queue.update((list) => [...list, toast]);\n return id;\n }\n case 'reject':\n this.forgetDedupe(id);\n return null;\n case 'dismiss-oldest':\n default: {\n // Prefer evicting the oldest dismissible toast: timed and actionless\n // first (a persistent error with a Retry action is never collateral).\n // Only when everything is persistent/with-action does the oldest go.\n const list = this._active();\n const victim = list.find((t) => t.duration > 0 && !t.action) ?? list[0];\n if (victim) this.dismiss(victim.id);\n this._active.update((next) => [...next, toast]);\n this.arm(id, duration);\n return id;\n }\n }\n }\n\n /**\n * Merges changes into an active or queued toast and restarts its timer\n * with the new duration (unchanged when `changes.duration` is absent).\n * Unknown ids (never shown or already dismissed): no-op, returns `false`.\n */\n update(id: number, changes: ToastChanges): boolean {\n const inActive = this._active().some((t) => t.id === id);\n // Duration resolution on update: an explicit new duration always wins and\n // becomes sticky; otherwise a kind change re-resolves the per-kind default\n // (unless the toast already carries an explicit duration).\n let nextDuration = 0;\n const apply = (t: Toast): Toast => {\n if (changes.duration !== undefined) {\n nextDuration = clampDuration(changes.duration, t.duration);\n this._explicitDuration.add(id);\n } else if (changes.kind !== undefined && !this._explicitDuration.has(id)) {\n nextDuration = this.fallbackDuration(changes.kind);\n } else {\n nextDuration = t.duration;\n }\n return {\n ...t,\n message: changes.message ?? t.message,\n kind: changes.kind ?? t.kind,\n duration: nextDuration,\n remaining: nextDuration,\n action: changes.action !== undefined ? changes.action : t.action,\n };\n };\n if (inActive) {\n this.disarm(id);\n let next: Toast | undefined;\n this._active.update((list) =>\n list.map((t) => {\n if (t.id !== id) return t;\n next = apply(t);\n return next;\n }),\n );\n if (next && !this._paused.has(id)) this.arm(id, next.duration);\n return true;\n }\n let found = false;\n this._queue.update((list) =>\n list.map((t) => {\n if (t.id !== id) return t;\n found = true;\n return apply(t);\n }),\n );\n return found;\n }\n\n /**\n * Removes a toast wherever it lives, clearing its timer. Unknown ids and\n * repeated calls: no-op, returns `false`. Removing an active toast promotes\n * the queue head (if any), whose timer starts at promotion.\n */\n dismiss(id: number): boolean {\n this.disarm(id);\n this._paused.delete(id);\n this._explicitDuration.delete(id);\n let removed = false;\n this._active.update((list) => {\n const next = list.filter((t) => t.id !== id);\n removed = next.length !== list.length;\n return next;\n });\n if (!removed) {\n this._queue.update((list) => {\n const next = list.filter((t) => t.id !== id);\n removed = next.length !== list.length;\n return next;\n });\n }\n if (removed) {\n this.forgetDedupe(id);\n this.promote();\n }\n return removed;\n }\n\n /** Clears timers, active and queue. Ids keep incrementing (never reused). */\n dismissAll(): void {\n this.clearTimers();\n this._paused.clear();\n this._explicitDuration.clear();\n this._active.set([]);\n this._queue.set([]);\n this._dedupeByKey.clear();\n this._dedupeKeyById.clear();\n }\n\n /**\n * Suspends the active timer, preserving `remaining` and marking `paused`.\n * UI binds this to hover/focus (WCAG 2.2.1 timing control). Returns `false`\n * for unknown ids, queued/persistent toasts, or already-paused timers —\n * a persistent toast can never be paused, so hover-leave cannot dismiss it.\n */\n pause(id: number): boolean {\n const record = this._timers.get(id);\n if (!record) return false;\n clearTimeout(record.handle);\n this._timers.delete(id);\n this._paused.add(id);\n const elapsed = Date.now() - record.startedAt;\n this._active.update((list) =>\n list.map((t) =>\n t.id === id\n ? { ...t, remaining: Math.max(0, t.remaining - elapsed), paused: true }\n : t,\n ),\n );\n return true;\n }\n\n /**\n * Restarts a paused timer with the preserved `remaining`. No-op (`false`) on\n * anything that was never paused — including persistent toasts. A paused\n * toast whose remaining reached zero is dismissed instead (it logically\n * already expired).\n */\n resume(id: number): boolean {\n if (!this._paused.has(id)) return false;\n this._paused.delete(id);\n const toast = this._active().find((t) => t.id === id);\n if (!toast) return false;\n if (toast.remaining <= 0) {\n this.dismiss(id);\n return true;\n }\n this._active.update((list) => list.map((t) => (t.id === id ? { ...t, paused: false } : t)));\n this.arm(id, toast.remaining);\n return true;\n }\n\n /**\n * Runs the toast action callback, then dismisses unless the action opts out\n * via `dismissAfterPress: false`. Callback exceptions propagate to the caller.\n * Returns `false` when the toast or its action is missing.\n */\n runAction(id: number): boolean {\n const toast =\n this._active().find((t) => t.id === id) ?? this._queue().find((t) => t.id === id);\n if (!toast?.action) return false;\n toast.action.onPress();\n if (toast.action.dismissAfterPress ?? true) this.dismiss(id);\n return true;\n }\n\n private maxVisible(): number {\n const value = this.config?.maxVisible ?? DEFAULT_MAX_VISIBLE;\n return Math.max(1, Math.floor(value));\n }\n\n private maxQueue(): number {\n const value = this.config?.maxQueue ?? Number.POSITIVE_INFINITY;\n return value < 0 ? 0 : value;\n }\n\n private resolveDuration(kind: ToastKind, override?: number): number {\n if (override !== undefined) return clampDuration(override, this.fallbackDuration(kind));\n return this.fallbackDuration(kind);\n }\n\n private fallbackDuration(kind: ToastKind): number {\n const perKind = this.config?.durations?.[kind];\n if (perKind !== undefined) return clampDuration(perKind, DEFAULT_DURATION);\n return clampDuration(this.config?.duration, DEFAULT_DURATION);\n }\n\n private arm(id: number, delay: number): void {\n if (delay <= 0) return;\n const generation = (this._generations.get(id) ?? 0) + 1;\n this._generations.set(id, generation);\n this._timers.set(id, {\n handle: setTimeout(() => {\n // Stale-guard: only the latest generation may dismiss.\n if (this._timers.get(id)?.generation === generation) this.dismiss(id);\n }, delay),\n startedAt: Date.now(),\n generation,\n });\n }\n\n private disarm(id: number): void {\n const record = this._timers.get(id);\n if (record) {\n clearTimeout(record.handle);\n this._timers.delete(id);\n }\n }\n\n private clearTimers(): void {\n for (const record of this._timers.values()) clearTimeout(record.handle);\n this._timers.clear();\n }\n\n private promote(): void {\n while (this._active().length < this.maxVisible() && this._queue().length > 0) {\n const head = this._queue()[0];\n this._queue.update((list) => list.slice(1));\n const fresh: Toast = { ...head, remaining: head.duration };\n this._active.update((list) => [...list, fresh]);\n this.arm(fresh.id, fresh.duration);\n }\n }\n\n private findByDedupeKey(key: string): number | null {\n const id = this._dedupeByKey.get(key);\n if (id === undefined) return null;\n const present =\n this._active().some((t) => t.id === id) || this._queue().some((t) => t.id === id);\n if (!present) {\n this.forgetDedupe(id);\n return null;\n }\n return id;\n }\n\n private forgetDedupe(id: number): void {\n const key = this._dedupeKeyById.get(id);\n if (key !== undefined) {\n this._dedupeKeyById.delete(id);\n if (this._dedupeByKey.get(key) === id) this._dedupeByKey.delete(key);\n }\n }\n}\n","import { EnvironmentProviders, inject, makeEnvironmentProviders } from '@angular/core';\nimport { RIVINIA_TOAST_CONFIG, type RiviniaToastConfig } from './toast-config';\nimport { RiviniaToast } from './rivinia-toast';\n\n/**\n * Configures `@rivinia/toast`. The queue is a root singleton; call once at\n * bootstrap. Presentation (outlet component, position, styles, icons,\n * translations) stays app-owned — a future `@rivinia/toast/ui` entry point\n * will consume the same `RiviniaToast` state.\n *\n * @example\n * ```ts\n * provideRiviniaToast({ duration: 5000, maxVisible: 5 })\n * ```\n */\nexport function provideRiviniaToast(config: RiviniaToastConfig = {}): EnvironmentProviders {\n return makeEnvironmentProviders([{ provide: RIVINIA_TOAST_CONFIG, useValue: config }]);\n}\n\n/** Shorthand for `inject(RiviniaToast)`. */\nexport function injectRiviniaToast(): RiviniaToast {\n return inject(RiviniaToast);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"rivinia-toast.mjs","names":[],"sources":["../../../projects/toast/src/lib/toast-utils.ts","../../../projects/toast/src/lib/toast-config.ts","../../../projects/toast/src/lib/rivinia-toast.ts","../../../projects/toast/src/lib/provide-toast.ts"],"sourcesContent":["import type { Signal } from '@angular/core';\nimport type { ToastMessage } from './toast-types';\n\n/**\n * Reads a toast message regardless of shape. Future UI entry points call this\n * in templates — one place to support `string | Signal<string>`.\n */\nexport function resolveToastText(message: ToastMessage): string {\n return typeof message === 'string' ? message : (message as Signal<string>)();\n}\n\n/**\n * Normalizes a duration into a safe non-negative integer.\n * `0`, negatives, `NaN` and non-finite values all mean persistent (no timer).\n * Rationale: a `setTimeout(Infinity)` silently overflows to ~1ms in browsers —\n * converting to persistent is the only non-surprising mapping.\n */\nexport function clampDuration(value: number | undefined, fallback: number): number {\n const candidate = value === undefined ? fallback : value;\n if (typeof candidate !== 'number' || !Number.isFinite(candidate) || candidate <= 0) {\n return 0;\n }\n return Math.floor(candidate);\n}\n","import { InjectionToken } from '@angular/core';\nimport type { ToastKind, ToastOverflowPolicy } from './toast-types';\n\n/**\n * Global configuration for `@rivinia/toast`.\n *\n * Every field is optional. Per-toast options always win over globals.\n * URLs, icons, positions, styles: intentionally absent — presentation is\n * app-owned (a future `@rivinia/toast/ui` entry point will consume this Core).\n */\nexport interface RiviniaToastConfig {\n /**\n * Default auto-dismiss delay in milliseconds.\n * @default 5000\n */\n duration?: number;\n /**\n * Per-kind default delays. A per-toast `duration` beats both levels.\n * @default {} (every kind falls back to `duration`)\n */\n durations?: Partial<Record<ToastKind, number>>;\n /**\n * Maximum simultaneously visible (active) toasts. Clamped to ≥ 1.\n * @default 5 (industry standard: shadcn `TOAST_LIMIT = 3`, BRT `MAX_VISIBLE = 5`)\n */\n maxVisible?: number;\n /**\n * Maximum queued toasts when `overflow: 'queue'`. Further shows are rejected.\n * @default Infinity (bounded only if you bound it — an unbounded queue is a\n * documented leak risk for hot paths; prefer `'dismiss-oldest'` there)\n */\n maxQueue?: number;\n /**\n * What happens when a new toast arrives while `maxVisible` are active.\n * - `'dismiss-oldest'`: dismiss the oldest active toast, show the new one.\n * - `'queue'`: hold the new toast (no timer runs) until room frees up.\n * - `'reject'`: `show()` returns `null`; nothing changes.\n * @default 'dismiss-oldest'\n */\n overflow?: ToastOverflowPolicy;\n}\n\n/**\n * Internal configuration token. Consumers must use `provideRiviniaToast()`.\n * @internal\n */\nexport const RIVINIA_TOAST_CONFIG = new InjectionToken<RiviniaToastConfig>(\n 'RIVINIA_TOAST_CONFIG',\n);\n\nexport const DEFAULT_DURATION = 5000;\nexport const DEFAULT_MAX_VISIBLE = 5;\n","import { DestroyRef, inject, Service, signal } from '@angular/core';\nimport {\n DEFAULT_DURATION,\n DEFAULT_MAX_VISIBLE,\n RIVINIA_TOAST_CONFIG,\n type RiviniaToastConfig,\n} from './toast-config';\nimport type { Toast, ToastChanges, ToastKind, ToastOptions } from './toast-types';\nimport { clampDuration } from './toast-utils';\n\ninterface TimerRecord {\n handle: ReturnType<typeof setTimeout>;\n startedAt: number;\n generation: number;\n}\n\n/**\n * Headless toast queue owner.\n *\n * State: two readonly signals — `active` (visible, capped) and `queue`\n * (overflow waiting, timerless). Who changes it: only the methods below.\n * Timers: one `setTimeout` per active timed toast, guarded by a per-toast\n * generation so a stale fire can never dismiss an updated toast. No DOM,\n * no styles, no router — presentation and navigation policy are app-owned\n * (see README recipes).\n */\n@Service()\nexport class RiviniaToast {\n private readonly config = inject<RiviniaToastConfig>(RIVINIA_TOAST_CONFIG, {\n optional: true,\n });\n\n private readonly _active = signal<Toast[]>([]);\n private readonly _queue = signal<Toast[]>([]);\n\n /** Visible toasts, oldest first. Never exceeds effective `maxVisible`. */\n readonly active = this._active.asReadonly();\n /** Overflow waiting for room. No timers run while queued. */\n readonly queue = this._queue.asReadonly();\n\n private readonly _timers = new Map<number, TimerRecord>();\n private readonly _paused = new Set<number>();\n /** Ids whose duration was set explicitly (per-toast override wins forever). */\n private readonly _explicitDuration = new Set<number>();\n private readonly _generations = new Map<number, number>();\n private readonly _dedupeByKey = new Map<string, number>();\n private readonly _dedupeKeyById = new Map<number, string>();\n private _nextId = 0;\n\n constructor() {\n inject(DestroyRef).onDestroy(() => this.clearTimers());\n }\n\n /**\n * Shows a toast. Returns its stable id, or `null` when rejected by the\n * `'reject'` overflow policy (or a full finite queue).\n */\n show(options: ToastOptions): number | null {\n if (options.dedupeKey !== undefined) {\n const existing = this.findByDedupeKey(options.dedupeKey);\n if (existing !== null) {\n this.update(existing, {\n message: options.message,\n kind: options.kind,\n duration: options.duration,\n action: options.action ?? null,\n });\n return existing;\n }\n }\n\n const id = ++this._nextId;\n const kind = options.kind ?? 'info';\n const duration = this.resolveDuration(kind, options.duration);\n if (options.duration !== undefined) this._explicitDuration.add(id);\n const toast: Toast = {\n id,\n kind,\n message: options.message,\n duration,\n remaining: duration,\n paused: false,\n action: options.action ?? null,\n };\n if (options.dedupeKey !== undefined) {\n this._dedupeByKey.set(options.dedupeKey, id);\n this._dedupeKeyById.set(id, options.dedupeKey);\n }\n\n if (this._active().length < this.maxVisible()) {\n this._active.update((list) => [...list, toast]);\n this.arm(id, duration);\n return id;\n }\n\n switch (this.config?.overflow ?? 'dismiss-oldest') {\n case 'queue': {\n if (this._queue().length >= this.maxQueue()) {\n this.forgetDedupe(id);\n return null;\n }\n this._queue.update((list) => [...list, toast]);\n return id;\n }\n case 'reject':\n this.forgetDedupe(id);\n return null;\n case 'dismiss-oldest':\n default: {\n // Prefer evicting the oldest dismissible toast: timed and actionless\n // first (a persistent error with a Retry action is never collateral).\n // Only when everything is persistent/with-action does the oldest go.\n const list = this._active();\n const victim = list.find((t) => t.duration > 0 && !t.action) ?? list[0];\n if (victim) this.dismiss(victim.id);\n this._active.update((next) => [...next, toast]);\n this.arm(id, duration);\n return id;\n }\n }\n }\n\n /**\n * Merges changes into an active or queued toast and restarts its timer\n * with the new duration (unchanged when `changes.duration` is absent).\n * Unknown ids (never shown or already dismissed): no-op, returns `false`.\n */\n update(id: number, changes: ToastChanges): boolean {\n const inActive = this._active().some((t) => t.id === id);\n // Duration resolution on update: an explicit new duration always wins and\n // becomes sticky; otherwise a kind change re-resolves the per-kind default\n // (unless the toast already carries an explicit duration).\n let nextDuration = 0;\n const apply = (t: Toast): Toast => {\n if (changes.duration !== undefined) {\n nextDuration = clampDuration(changes.duration, t.duration);\n this._explicitDuration.add(id);\n } else if (changes.kind !== undefined && !this._explicitDuration.has(id)) {\n nextDuration = this.fallbackDuration(changes.kind);\n } else {\n nextDuration = t.duration;\n }\n return {\n ...t,\n message: changes.message ?? t.message,\n kind: changes.kind ?? t.kind,\n duration: nextDuration,\n remaining: nextDuration,\n action: changes.action !== undefined ? changes.action : t.action,\n };\n };\n if (inActive) {\n this.disarm(id);\n let next: Toast | undefined;\n this._active.update((list) =>\n list.map((t) => {\n if (t.id !== id) return t;\n next = apply(t);\n return next;\n }),\n );\n if (next && !this._paused.has(id)) this.arm(id, next.duration);\n return true;\n }\n let found = false;\n this._queue.update((list) =>\n list.map((t) => {\n if (t.id !== id) return t;\n found = true;\n return apply(t);\n }),\n );\n return found;\n }\n\n /**\n * Removes a toast wherever it lives, clearing its timer. Unknown ids and\n * repeated calls: no-op, returns `false`. Removing an active toast promotes\n * the queue head (if any), whose timer starts at promotion.\n */\n dismiss(id: number): boolean {\n this.disarm(id);\n this._paused.delete(id);\n this._explicitDuration.delete(id);\n let removed = false;\n this._active.update((list) => {\n const next = list.filter((t) => t.id !== id);\n removed = next.length !== list.length;\n return next;\n });\n if (!removed) {\n this._queue.update((list) => {\n const next = list.filter((t) => t.id !== id);\n removed = next.length !== list.length;\n return next;\n });\n }\n if (removed) {\n this.forgetDedupe(id);\n this._generations.delete(id);\n this.promote();\n }\n return removed;\n }\n\n /** Clears timers, active and queue. Ids keep incrementing (never reused). */\n dismissAll(): void {\n this.clearTimers();\n this._paused.clear();\n this._explicitDuration.clear();\n this._generations.clear();\n this._active.set([]);\n this._queue.set([]);\n this._dedupeByKey.clear();\n this._dedupeKeyById.clear();\n }\n\n /**\n * Suspends the active timer, preserving `remaining` and marking `paused`.\n * UI binds this to hover/focus (WCAG 2.2.1 timing control). Returns `false`\n * for unknown ids, queued/persistent toasts, or already-paused timers —\n * a persistent toast can never be paused, so hover-leave cannot dismiss it.\n */\n pause(id: number): boolean {\n const record = this._timers.get(id);\n if (!record) return false;\n clearTimeout(record.handle);\n this._timers.delete(id);\n this._paused.add(id);\n const elapsed = Date.now() - record.startedAt;\n this._active.update((list) =>\n list.map((t) =>\n t.id === id ? { ...t, remaining: Math.max(0, t.remaining - elapsed), paused: true } : t,\n ),\n );\n return true;\n }\n\n /**\n * Restarts a paused timer with the preserved `remaining`. No-op (`false`) on\n * anything that was never paused — including persistent toasts. A paused\n * toast whose remaining reached zero is dismissed instead (it logically\n * already expired).\n */\n resume(id: number): boolean {\n if (!this._paused.has(id)) return false;\n this._paused.delete(id);\n const toast = this._active().find((t) => t.id === id);\n if (!toast) return false;\n if (toast.remaining <= 0) {\n this.dismiss(id);\n return true;\n }\n this._active.update((list) => list.map((t) => (t.id === id ? { ...t, paused: false } : t)));\n this.arm(id, toast.remaining);\n return true;\n }\n\n /**\n * Runs the toast action callback, then dismisses unless the action opts out\n * via `dismissAfterPress: false`. Callback exceptions propagate to the caller.\n * Returns `false` when the toast or its action is missing.\n */\n runAction(id: number): boolean {\n const toast = this._active().find((t) => t.id === id) ?? this._queue().find((t) => t.id === id);\n if (!toast?.action) return false;\n toast.action.onPress();\n if (toast.action.dismissAfterPress ?? true) this.dismiss(id);\n return true;\n }\n\n private maxVisible(): number {\n const value = this.config?.maxVisible ?? DEFAULT_MAX_VISIBLE;\n return Math.max(1, Math.floor(value));\n }\n\n private maxQueue(): number {\n const value = this.config?.maxQueue ?? Number.POSITIVE_INFINITY;\n return value < 0 ? 0 : value;\n }\n\n private resolveDuration(kind: ToastKind, override?: number): number {\n if (override !== undefined) return clampDuration(override, this.fallbackDuration(kind));\n return this.fallbackDuration(kind);\n }\n\n private fallbackDuration(kind: ToastKind): number {\n const perKind = this.config?.durations?.[kind];\n if (perKind !== undefined) return clampDuration(perKind, DEFAULT_DURATION);\n return clampDuration(this.config?.duration, DEFAULT_DURATION);\n }\n\n private arm(id: number, delay: number): void {\n if (delay <= 0) return;\n const generation = (this._generations.get(id) ?? 0) + 1;\n this._generations.set(id, generation);\n this._timers.set(id, {\n handle: setTimeout(() => {\n // Stale-guard: only the latest generation may dismiss.\n if (this._timers.get(id)?.generation === generation) this.dismiss(id);\n }, delay),\n startedAt: Date.now(),\n generation,\n });\n }\n\n private disarm(id: number): void {\n const record = this._timers.get(id);\n if (record) {\n clearTimeout(record.handle);\n this._timers.delete(id);\n }\n }\n\n private clearTimers(): void {\n for (const record of this._timers.values()) clearTimeout(record.handle);\n this._timers.clear();\n }\n\n private promote(): void {\n while (this._active().length < this.maxVisible() && this._queue().length > 0) {\n const head = this._queue()[0];\n this._queue.update((list) => list.slice(1));\n const fresh: Toast = { ...head, remaining: head.duration };\n this._active.update((list) => [...list, fresh]);\n this.arm(fresh.id, fresh.duration);\n }\n }\n\n private findByDedupeKey(key: string): number | null {\n const id = this._dedupeByKey.get(key);\n if (id === undefined) return null;\n const present =\n this._active().some((t) => t.id === id) || this._queue().some((t) => t.id === id);\n if (!present) {\n this.forgetDedupe(id);\n return null;\n }\n return id;\n }\n\n private forgetDedupe(id: number): void {\n const key = this._dedupeKeyById.get(id);\n if (key !== undefined) {\n this._dedupeKeyById.delete(id);\n if (this._dedupeByKey.get(key) === id) this._dedupeByKey.delete(key);\n }\n }\n}\n","import { EnvironmentProviders, inject, makeEnvironmentProviders } from '@angular/core';\nimport { RIVINIA_TOAST_CONFIG, type RiviniaToastConfig } from './toast-config';\nimport { RiviniaToast } from './rivinia-toast';\n\n/**\n * Configures `@rivinia/toast`. The queue is a root singleton; call once at\n * bootstrap. Presentation (outlet component, position, styles, icons,\n * translations) stays app-owned — a future `@rivinia/toast/ui` entry point\n * will consume the same `RiviniaToast` state.\n *\n * @example\n * ```ts\n * provideRiviniaToast({ duration: 5000, maxVisible: 5 })\n * ```\n */\nexport function provideRiviniaToast(config: RiviniaToastConfig = {}): EnvironmentProviders {\n return makeEnvironmentProviders([{ provide: RIVINIA_TOAST_CONFIG, useValue: config }]);\n}\n\n/** Shorthand for `inject(RiviniaToast)`. */\nexport function injectRiviniaToast(): RiviniaToast {\n return inject(RiviniaToast);\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rivinia/toast",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Headless toast queue for Angular
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Headless toast queue for Angular: signals, pause-on-hover timers, actions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"angular",
|
|
7
7
|
"toast",
|
|
@@ -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 };
|