@rivinia/toast 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,7 +7,7 @@ An Angular signal-based toast queue with timers, actions, deduplication, and
7
7
  overflow handling. Core is headless; an optional standalone UI is available at
8
8
  `@rivinia/toast/ui`.
9
9
 
10
- - Prepared patch: **0.4.1** (latest published: **0.4.0**)
10
+ - Package version: **0.5.0**
11
11
  - Peer dependency: **`@angular/core` `^22.0.0`**
12
12
  - No `tslib`, router, or HTTP peer dependency.
13
13
 
@@ -57,12 +57,8 @@ import { RiviniaToastList } from '@rivinia/toast/ui';
57
57
 
58
58
  @Component({
59
59
  selector: 'app-notifications',
60
- standalone: true,
61
60
  imports: [RiviniaToastList],
62
- template: `
63
- <button type="button" (click)="save()">Save</button>
64
- <rv-toast-list [manager]="toast" position="bottom-end" />
65
- `,
61
+ templateUrl: './notifications.component.html',
66
62
  })
67
63
  export class NotificationsComponent {
68
64
  readonly toast = injectRiviniaToast();
@@ -86,6 +82,12 @@ export class NotificationsComponent {
86
82
  }
87
83
  ```
88
84
 
85
+ ```html
86
+ <!-- src/app/notifications.component.html -->
87
+ <button type="button" (click)="save()">Save</button>
88
+ <rv-toast-list [manager]="toast" position="bottom-end" />
89
+ ```
90
+
89
91
  The standalone list renders the manager's `active()` signal, pauses timed
90
92
  notifications on hover and keyboard focus, runs actions, and provides a
91
93
  dismiss button by default. Its defaults are `position="bottom-end"`,
@@ -98,17 +100,15 @@ For snapshot-driven UIs, provide the snapshots and handle the emitted IDs
98
100
  yourself:
99
101
 
100
102
  ```ts
103
+ // src/app/toast-snapshots.component.ts
101
104
  import { Component } from '@angular/core';
102
105
  import type { Toast } from '@rivinia/toast';
103
106
  import { RiviniaToastList } from '@rivinia/toast/ui';
104
107
 
105
108
  @Component({
106
109
  selector: 'app-toast-snapshots',
107
- standalone: true,
108
110
  imports: [RiviniaToastList],
109
- template: `
110
- <rv-toast-list [toasts]="items" (dismissed)="remove($event)" (actionPressed)="run($event)" />
111
- `,
111
+ templateUrl: './toast-snapshots.component.html',
112
112
  })
113
113
  export class ToastSnapshotsComponent {
114
114
  items: readonly Toast[] = [
@@ -136,6 +136,11 @@ export class ToastSnapshotsComponent {
136
136
  }
137
137
  ```
138
138
 
139
+ ```html
140
+ <!-- src/app/toast-snapshots.component.html -->
141
+ <rv-toast-list [toasts]="items" (dismissed)="remove($event)" (actionPressed)="run($event)" />
142
+ ```
143
+
139
144
  In this presentational mode your app owns timers, pause behavior, and action
140
145
  handling. If both `manager` and `toasts` are supplied, the manager takes
141
146
  precedence.
@@ -146,18 +151,13 @@ Core can be used without the UI. `active()` and `queue()` are readonly signals;
146
151
  the active list is oldest-first. Timers begin only when a toast becomes active.
147
152
 
148
153
  ```ts
154
+ // src/app/save-status.component.ts
149
155
  import { Component, inject } from '@angular/core';
150
156
  import { resolveToastText, RiviniaToast } from '@rivinia/toast';
151
157
 
152
158
  @Component({
153
159
  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
- `,
160
+ templateUrl: './save-status.component.html',
161
161
  })
162
162
  export class SaveStatusComponent {
163
163
  readonly toast = inject(RiviniaToast);
@@ -177,10 +177,121 @@ export class SaveStatusComponent {
177
177
  }
178
178
  ```
179
179
 
180
+ ```html
181
+ <!-- src/app/save-status.component.html -->
182
+ <button type="button" (click)="save()">Save</button>
183
+ @for (item of toast.active(); track item.id) {
184
+ <p [attr.data-kind]="item.kind">{{ resolveText(item.message) }}</p>
185
+ }
186
+ ```
187
+
180
188
  `message` and action `label` accept either a string or a reactive string
181
189
  signal, so they can reflect language changes. For a custom outlet, use the exported
182
190
  `resolveToastText(message)` utility to read either shape.
183
191
 
192
+ ## Theming
193
+
194
+ Every component reads **its own token → the shared semantic layer → its
195
+ built-in default**:
196
+
197
+ ```css
198
+ --rv-toast-bg → --rv-bg → #ffffff
199
+ ```
200
+
201
+ | Level | Token | Scope |
202
+ | -------------- | --------------- | ----------------------------------- |
203
+ | 1. per package | `--rv-toast-bg` | that toast only |
204
+ | 2. global | `--rv-bg` | every Rivinia component, everywhere |
205
+ | 3. default | — | what you get if you set nothing |
206
+
207
+ ```css
208
+ @import '@rivinia/theme';
209
+
210
+ :root {
211
+ --rv-bg: #ffffff;
212
+ --rv-fg: #111111;
213
+ --rv-border: #e0e0e0;
214
+ }
215
+ ```
216
+
217
+ Its defaults sit in `@layer rivinia-theme`, so your unlayered `:root` rules win
218
+ wherever they appear. **Map it on `:root`, never on a component element** — an
219
+ element-level declaration shadows the root value and the override silently
220
+ stops working.
221
+
222
+ ### The library's own tokens
223
+
224
+ | Token | Default | Applies to |
225
+ | ------------------- | ----------------------------- | -------------- |
226
+ | `--rv-toast-bg` | `#ffffff` | the toast |
227
+ | `--rv-toast-fg` | `#111111` | the toast |
228
+ | `--rv-toast-muted` | `#555555` | the kind rail |
229
+ | `--rv-toast-border` | `#e0e0e0` | the toast |
230
+ | `--rv-toast-radius` | `12px` | the toast |
231
+ | `--rv-toast-shadow` | `0 12px 32px rgba(0,0,0,.32)` | the toast |
232
+ | `--rv-toast-gap` | `8px` | between toasts |
233
+ | `--rv-toast-width` | `22rem` | the list |
234
+ | `--rv-toast-z` | `1000` | the list |
235
+
236
+ The kind colours chain to the semantic layer too:
237
+
238
+ | Token | Semantic token |
239
+ | -------------------- | -------------- |
240
+ | `--rv-toast-success` | `--rv-ok` |
241
+ | `--rv-toast-error` | `--rv-danger` |
242
+ | `--rv-toast-info` | `--rv-info` |
243
+ | `--rv-toast-warning` | `--rv-warning` |
244
+
245
+ **No forced light mode** — there is no `prefers-color-scheme` block in this
246
+ package, so a toast never overrides your explicit choice.
247
+
248
+ ### Which styling stack?
249
+
250
+ | Stack | Import |
251
+ | --------------- | -------------------------------------------- |
252
+ | **Plain CSS** | `@import '@rivinia/theme';` |
253
+ | **Tailwind v4** | `@import '@rivinia/theme/tailwind.css';` |
254
+ | **Bootstrap** | `@import '@rivinia/theme/bootstrap.css';` |
255
+ | **Sass / SCSS** | `@use '@rivinia/theme/scss';` |
256
+ | **Less** | `@import (less) '@rivinia/theme/theme.css';` |
257
+
258
+ ## The list is in the top layer
259
+
260
+ A `position: fixed` list with a `z-index` is still in the DOCUMENT, so it is
261
+ clipped by an `overflow: hidden` ancestor and loses to any stacking context
262
+ above it — including the one a modal `<dialog>` creates.
263
+
264
+ The list is therefore a **manual popover**, which puts it in the browser's top
265
+ layer: the same layer a modal `<dialog>` uses. The top layer stacks in the
266
+ order elements are shown, and the list is re-shown whenever its contents
267
+ change, so a toast raised while a dialog is open lands above it. Measured: the
268
+ element at the toast's position is the toast, not the dialog.
269
+
270
+ A manual popover is not modal: it does not trap focus, does not make the page
271
+ inert, and does not close on an outside click — so it behaves exactly like
272
+ the list it replaces.
273
+
274
+ **The popover's user-agent styles are reset.** A popover gets a background, a
275
+ border, padding and `overflow: auto` from the UA, which puts a shared canvas
276
+ and a scrollbar around the stack — so the toasts read as one block instead of
277
+ separate cards. The list is a transparent, borderless column.
278
+
279
+ Where the browser has no `showPopover` the list stays in the document and a
280
+ `z-index` decides its order. The feature is detected, not assumed.
281
+
282
+ ## The list must be MANAGED
283
+
284
+ `rv-toast-list` renders what it is given. Without a manager it has an empty
285
+ list and **no toast ever appears**:
286
+
287
+ ```html
288
+ <!-- nothing shows: the list is presentational and empty -->
289
+ <rv-toast-list position="bottom-end" />
290
+
291
+ <!-- the manager drives it -->
292
+ <rv-toast-list [manager]="toast" position="bottom-end" />
293
+ ```
294
+
184
295
  ## API and behavior
185
296
 
186
297
  | Export | Kind | Purpose |
@@ -1,8 +1,27 @@
1
1
  import * as i0 from "@angular/core";
2
- import { Component, computed, effect, input, output, signal } from "@angular/core";
2
+ import { Component, computed, effect, input, output, signal, viewChild } from "@angular/core";
3
3
  import { resolveToastText } from "@rivinia/toast";
4
4
  const LEAVE_MS = 150;
5
5
  var RiviniaToastList = class RiviniaToastList {
6
+ topLayerSupported = typeof HTMLElement !== "undefined" && "showPopover" in HTMLElement.prototype;
7
+ panel = viewChild("panel", ...ngDevMode ? [{ debugName: "panel" }] : /* istanbul ignore next */ []);
8
+ ngAfterViewInit() {
9
+ this.promote();
10
+ }
11
+ promote() {
12
+ if (!this.topLayerSupported) return;
13
+ const el = this.panel()?.nativeElement;
14
+ if (!el || typeof el.showPopover !== "function") return;
15
+ el.setAttribute("popover", "manual");
16
+ try {
17
+ if (el.matches(":popover-open")) el.hidePopover?.();
18
+ el.showPopover();
19
+ } catch {}
20
+ }
21
+ keepOnTop = effect(() => {
22
+ this.rendered();
23
+ this.promote();
24
+ }, ...ngDevMode ? [{ debugName: "keepOnTop" }] : /* istanbul ignore next */ []);
6
25
  manager = input(null, ...ngDevMode ? [{ debugName: "manager" }] : /* istanbul ignore next */ []);
7
26
  toasts = input([], ...ngDevMode ? [{ debugName: "toasts" }] : /* istanbul ignore next */ []);
8
27
  position = input("bottom-end", ...ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []);
@@ -150,12 +169,21 @@ var RiviniaToastList = class RiviniaToastList {
150
169
  dismissed: "dismissed",
151
170
  actionPressed: "actionPressed"
152
171
  },
172
+ viewQueries: [{
173
+ propertyName: "panel",
174
+ first: true,
175
+ predicate: ["panel"],
176
+ descendants: true,
177
+ isSignal: true
178
+ }],
153
179
  ngImport: i0,
154
180
  template: `
155
181
  <div
182
+ #panel
156
183
  class="rv-toasts"
157
184
  role="region"
158
185
  aria-label="Notifications"
186
+ [attr.popover]="topLayerSupported ? 'manual' : null"
159
187
  [attr.data-position]="position()"
160
188
  [attr.data-animation]="animation()"
161
189
  [attr.data-rich-colors]="richColors()"
@@ -194,7 +222,7 @@ var RiviniaToastList = class RiviniaToastList {
194
222
  </div>
195
223
  `,
196
224
  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"]
225
+ styles: [".rv-toasts{--rv-toast-gap: .5rem;--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);background:transparent;border:0;padding:0;color:inherit;overflow:visible;height:auto;inset:auto;margin:0;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}.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:.625rem;background:var(--rv-toast-bg, var(--rv-bg, #ffffff));color:var(--rv-toast-fg, var(--rv-fg, #111111));border:1px solid var(--rv-toast-border, var(--rv-border, #e0e0e0));border-inline-start:3px solid var(--rv-toast-muted, var(--rv-muted, #555555));border-radius:var(--rv-toast-radius, var(--rv-radius, 12px));box-shadow:var(--rv-toast-shadow, var(--rv-shadow, 0 12px 32px rgba(0, 0, 0, .32)));padding:12px 14px}.rv-toast[data-kind=success]{border-inline-start-color:var(--rv-toast-success, var(--rv-ok, #16a34a))}.rv-toast[data-kind=error]{border-inline-start-color:var(--rv-toast-error, var(--rv-danger, #dc2626))}.rv-toast[data-kind=info]{border-inline-start-color:var(--rv-toast-info, var(--rv-info, #2563eb))}.rv-toast[data-kind=warning]{border-inline-start-color:var(--rv-toast-warning, var(--rv-warning, #d97706))}.rv-toasts[data-rich-colors=true] .rv-toast[data-kind=success]{background:var(--rv-toast-success, var(--rv-ok, #16a34a));color:#06130a}.rv-toasts[data-rich-colors=true] .rv-toast[data-kind=error]{background:var(--rv-toast-error, var(--rv-danger, #dc2626));color:#1c0605}.rv-toasts[data-rich-colors=true] .rv-toast[data-kind=info]{background:var(--rv-toast-info, var(--rv-info, #2563eb));color:#04121f}.rv-toasts[data-rich-colors=true] .rv-toast[data-kind=warning]{background:var(--rv-toast-warning, var(--rv-warning, #d97706));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:.875rem;line-height:1.45}.rv-toast-action,.rv-toast-close{flex:none;cursor:pointer;border-radius:.5rem;border:1px solid var(--rv-toast-border, var(--rv-border, #e0e0e0));background:transparent;color:inherit;padding:.25rem .625rem;font-size:.8125rem}.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
226
  });
199
227
  };
200
228
  i0.ɵɵngDeclareClassMetadata({
@@ -206,12 +234,13 @@ i0.ɵɵngDeclareClassMetadata({
206
234
  type: Component,
207
235
  args: [{
208
236
  selector: "rv-toast-list",
209
- standalone: true,
210
237
  template: `
211
238
  <div
239
+ #panel
212
240
  class="rv-toasts"
213
241
  role="region"
214
242
  aria-label="Notifications"
243
+ [attr.popover]="topLayerSupported ? 'manual' : null"
215
244
  [attr.data-position]="position()"
216
245
  [attr.data-animation]="animation()"
217
246
  [attr.data-rich-colors]="richColors()"
@@ -249,11 +278,15 @@ i0.ɵɵngDeclareClassMetadata({
249
278
  }
250
279
  </div>
251
280
  `,
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"]
281
+ styles: [".rv-toasts{--rv-toast-gap: .5rem;--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);background:transparent;border:0;padding:0;color:inherit;overflow:visible;height:auto;inset:auto;margin:0;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}.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:.625rem;background:var(--rv-toast-bg, var(--rv-bg, #ffffff));color:var(--rv-toast-fg, var(--rv-fg, #111111));border:1px solid var(--rv-toast-border, var(--rv-border, #e0e0e0));border-inline-start:3px solid var(--rv-toast-muted, var(--rv-muted, #555555));border-radius:var(--rv-toast-radius, var(--rv-radius, 12px));box-shadow:var(--rv-toast-shadow, var(--rv-shadow, 0 12px 32px rgba(0, 0, 0, .32)));padding:12px 14px}.rv-toast[data-kind=success]{border-inline-start-color:var(--rv-toast-success, var(--rv-ok, #16a34a))}.rv-toast[data-kind=error]{border-inline-start-color:var(--rv-toast-error, var(--rv-danger, #dc2626))}.rv-toast[data-kind=info]{border-inline-start-color:var(--rv-toast-info, var(--rv-info, #2563eb))}.rv-toast[data-kind=warning]{border-inline-start-color:var(--rv-toast-warning, var(--rv-warning, #d97706))}.rv-toasts[data-rich-colors=true] .rv-toast[data-kind=success]{background:var(--rv-toast-success, var(--rv-ok, #16a34a));color:#06130a}.rv-toasts[data-rich-colors=true] .rv-toast[data-kind=error]{background:var(--rv-toast-error, var(--rv-danger, #dc2626));color:#1c0605}.rv-toasts[data-rich-colors=true] .rv-toast[data-kind=info]{background:var(--rv-toast-info, var(--rv-info, #2563eb));color:#04121f}.rv-toasts[data-rich-colors=true] .rv-toast[data-kind=warning]{background:var(--rv-toast-warning, var(--rv-warning, #d97706));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:.875rem;line-height:1.45}.rv-toast-action,.rv-toast-close{flex:none;cursor:pointer;border-radius:.5rem;border:1px solid var(--rv-toast-border, var(--rv-border, #e0e0e0));background:transparent;color:inherit;padding:.25rem .625rem;font-size:.8125rem}.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
282
  }]
254
283
  }],
255
284
  ctorParameters: () => [],
256
285
  propDecorators: {
286
+ panel: [{
287
+ type: i0.ViewChild,
288
+ args: ["panel", { isSignal: true }]
289
+ }],
257
290
  manager: [{
258
291
  type: i0.Input,
259
292
  args: [{
@@ -1 +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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"}
1
+ {"version":3,"file":"rivinia-toast-ui.mjs","names":[],"sources":["../../../projects/toast/ui/src/lib/toast-list.ts"],"sourcesContent":["import {\n AfterViewInit,\n Component,\n computed,\n effect,\n ElementRef,\n viewChild,\n input,\n output,\n signal,\n} 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 template: `\n <div\n #panel\n class=\"rv-toasts\"\n role=\"region\"\n aria-label=\"Notifications\"\n [attr.popover]=\"topLayerSupported ? 'manual' : null\"\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 /*\n * The PALETTE tokens are NOT declared here — see the same note in\n * @rivinia/modal. A declaration on the element out-ranks an\n * ancestor's value, so the shared semantic layer would never reach\n * the toast. They are read as\n * var(--rv-toast-*, var(--rv-<semantic>, <default>)) instead.\n *\n * The LAYOUT tokens (gap, width, duration) stay here: they are\n * component-specific and have no semantic counterpart.\n */\n --rv-toast-gap: 0.5rem;\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 /*\n * Reset the POPOVER's UA styles.\n *\n * A popover gets a background, a border, padding and overflow: auto\n * from the user agent — which puts a shared canvas and a scrollbar\n * around the stack, so the toasts read as one block instead of\n * separate cards. The list is a transparent, borderless column.\n */\n background: transparent;\n border: 0;\n padding: 0;\n color: inherit;\n overflow: visible;\n height: auto;\n\n /*\n * The list is a manual popover, so it lives in the browser's TOP\n * LAYER — the same layer a modal <dialog> uses. A top layer stacks\n * in the order elements are shown, so a toast shown while a dialog is\n * open lands ABOVE it. A z-index cannot do this: a dialog in the top\n * layer is above every z-index in the document.\n *\n * The popover UA style sets inset: 0 and margin: auto, which would\n * over-constrain the box. Reset it, so the per-position rules below\n * have the last word.\n */\n inset: auto;\n margin: 0;\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 .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: 0.625rem;\n background: var(--rv-toast-bg, var(--rv-bg, #ffffff));\n color: var(--rv-toast-fg, var(--rv-fg, #111111));\n border: 1px solid var(--rv-toast-border, var(--rv-border, #e0e0e0));\n border-inline-start: 3px solid var(--rv-toast-muted, var(--rv-muted, #555555));\n border-radius: var(--rv-toast-radius, var(--rv-radius, 12px));\n box-shadow: var(--rv-toast-shadow, var(--rv-shadow, 0 12px 32px rgba(0, 0, 0, 0.32)));\n padding: 12px 14px;\n }\n .rv-toast[data-kind='success'] {\n border-inline-start-color: var(--rv-toast-success, var(--rv-ok, #16a34a));\n }\n .rv-toast[data-kind='error'] {\n border-inline-start-color: var(--rv-toast-error, var(--rv-danger, #dc2626));\n }\n .rv-toast[data-kind='info'] {\n border-inline-start-color: var(--rv-toast-info, var(--rv-info, #2563eb));\n }\n .rv-toast[data-kind='warning'] {\n border-inline-start-color: var(--rv-toast-warning, var(--rv-warning, #d97706));\n }\n .rv-toasts[data-rich-colors='true'] .rv-toast[data-kind='success'] {\n background: var(--rv-toast-success, var(--rv-ok, #16a34a));\n color: #06130a;\n }\n .rv-toasts[data-rich-colors='true'] .rv-toast[data-kind='error'] {\n background: var(--rv-toast-error, var(--rv-danger, #dc2626));\n color: #1c0605;\n }\n .rv-toasts[data-rich-colors='true'] .rv-toast[data-kind='info'] {\n background: var(--rv-toast-info, var(--rv-info, #2563eb));\n color: #04121f;\n }\n .rv-toasts[data-rich-colors='true'] .rv-toast[data-kind='warning'] {\n background: var(--rv-toast-warning, var(--rv-warning, #d97706));\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: 0.875rem;\n line-height: 1.45;\n }\n .rv-toast-action,\n .rv-toast-close {\n flex: none;\n cursor: pointer;\n border-radius: 0.5rem;\n border: 1px solid var(--rv-toast-border, var(--rv-border, #e0e0e0));\n background: transparent;\n color: inherit;\n padding: 0.25rem 0.625rem;\n font-size: 0.8125rem;\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 implements AfterViewInit {\n /**\n * Whether the browser can put the list in the top layer.\n *\n * Where it cannot, the list stays in the document and a `z-index` decides\n * its order — which puts it BELOW a modal dialog, because a dialog in the\n * top layer is above every z-index. The limitation is documented.\n */\n protected readonly topLayerSupported =\n typeof HTMLElement !== 'undefined' && 'showPopover' in HTMLElement.prototype;\n\n /**\n * Put the list in the top layer.\n *\n * The top layer stacks in the order elements are shown, so a toast shown\n * while a dialog is open lands above it. A `z-index` cannot do this.\n *\n * A manual popover is not modal: it does not trap focus, does not make the\n * page inert, and does not close on an outside click — so it behaves like\n * the list it replaces.\n */\n /** The panel — the element that becomes the popover. */\n private readonly panel = viewChild<ElementRef<HTMLElement>>('panel');\n\n /**\n * Put the list in the top layer.\n *\n * `ngAfterViewInit` rather than `afterNextRender`: the callback has to run\n * with the view in the DOM and the `popover` attribute already applied, and\n * `showPopover()` on an element that is not a popover yet throws\n * NotSupportedError — measured, not assumed.\n */\n ngAfterViewInit(): void {\n this.promote();\n }\n\n /**\n * Put the list at the TOP of the top layer.\n *\n * The top layer stacks in the order elements are SHOWN, so a list that was\n * shown when the app started sits BELOW a dialog opened later. Re-showing it\n * moves it back to the top — which is what \"a toast appears above the\n * dialog\" means.\n *\n * `showPopover()` on an open popover throws, so it is hidden first.\n */\n private promote(): void {\n if (!this.topLayerSupported) return;\n // The POPOVER is the panel, not the component host: the host is the\n // `<rv-toast-list>` element, and showing THAT would put the wrong box in\n // the top layer.\n const el = this.panel()?.nativeElement as\n | (HTMLElement & { showPopover?: () => void; hidePopover?: () => void })\n | undefined;\n if (!el || typeof el.showPopover !== 'function') return;\n // Set imperatively as well as by the template binding: the binding can\n // land after this hook, and the imperative set makes the order irrelevant.\n el.setAttribute('popover', 'manual');\n try {\n if (el.matches(':popover-open')) el.hidePopover?.();\n el.showPopover();\n } catch {\n // The browser refused. The list still works — it is just not in the top\n // layer.\n }\n }\n\n /**\n * Re-promote the list whenever its contents change.\n *\n * A toast that appears while a dialog is open must be ABOVE it, and a\n * dialog opened after the list was shown pushes the list down in the top\n * layer. Promoting on every change puts the list back on top each time.\n */\n private readonly keepOnTop = effect(() => {\n // Track the rendered items, so the effect runs when the list changes.\n this.rendered();\n this.promote();\n });\n\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.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Headless toast queue for Angular: signals, pause-on-hover timers, actions.",
5
5
  "keywords": [
6
6
  "angular",
@@ -1,5 +1,6 @@
1
- import { Toast, ToastKind, ToastOptions } from "@rivinia/toast";
2
1
  import * as i0 from "@angular/core";
2
+ import { AfterViewInit } from "@angular/core";
3
+ import { Toast, ToastKind, ToastOptions } from "@rivinia/toast";
3
4
  /**
4
5
  * Stack anchor. Logical (`start`/`end`) variants are RTL-aware. The user
5
6
  * picks exactly one — default `'bottom-end'` (sonner-compatible default).
@@ -36,7 +37,55 @@ interface RiviniaToastListManager {
36
37
  * Each item is `role="status"` (polite by default; `error` kind asserts).
37
38
  * Stack order is `active()` order (oldest first), anchored at `position`.
38
39
  */
39
- export declare class RiviniaToastList {
40
+ export declare class RiviniaToastList implements AfterViewInit {
41
+ /**
42
+ * Whether the browser can put the list in the top layer.
43
+ *
44
+ * Where it cannot, the list stays in the document and a `z-index` decides
45
+ * its order — which puts it BELOW a modal dialog, because a dialog in the
46
+ * top layer is above every z-index. The limitation is documented.
47
+ */
48
+ protected readonly topLayerSupported: boolean;
49
+ /**
50
+ * Put the list in the top layer.
51
+ *
52
+ * The top layer stacks in the order elements are shown, so a toast shown
53
+ * while a dialog is open lands above it. A `z-index` cannot do this.
54
+ *
55
+ * A manual popover is not modal: it does not trap focus, does not make the
56
+ * page inert, and does not close on an outside click — so it behaves like
57
+ * the list it replaces.
58
+ */
59
+ /** The panel — the element that becomes the popover. */
60
+ private readonly panel;
61
+ /**
62
+ * Put the list in the top layer.
63
+ *
64
+ * `ngAfterViewInit` rather than `afterNextRender`: the callback has to run
65
+ * with the view in the DOM and the `popover` attribute already applied, and
66
+ * `showPopover()` on an element that is not a popover yet throws
67
+ * NotSupportedError — measured, not assumed.
68
+ */
69
+ ngAfterViewInit(): void;
70
+ /**
71
+ * Put the list at the TOP of the top layer.
72
+ *
73
+ * The top layer stacks in the order elements are SHOWN, so a list that was
74
+ * shown when the app started sits BELOW a dialog opened later. Re-showing it
75
+ * moves it back to the top — which is what "a toast appears above the
76
+ * dialog" means.
77
+ *
78
+ * `showPopover()` on an open popover throws, so it is hidden first.
79
+ */
80
+ private promote;
81
+ /**
82
+ * Re-promote the list whenever its contents change.
83
+ *
84
+ * A toast that appears while a dialog is open must be ABOVE it, and a
85
+ * dialog opened after the list was shown pushes the list down in the top
86
+ * layer. Promoting on every change puts the list back on top each time.
87
+ */
88
+ private readonly keepOnTop;
40
89
  /** Managed mode: the service (or a structural test double). Wins over `toasts`. */
41
90
  readonly manager: import("@angular/core").InputSignal<RiviniaToastListManager | null>;
42
91
  /** Presentational mode: snapshots you own (timers/pause are yours). */