@rivinia/toast 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +34 -345
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.1
4
+
5
+ - Documentation: README restructured into docs/toast/ pages; stale pre-1.0 wording removed.
6
+
3
7
  ## 1.0.0
4
8
 
5
9
  - First stable release of the notification queue, action handling, and optional UI list.
package/README.md CHANGED
@@ -3,372 +3,61 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@rivinia/toast.svg)](https://www.npmjs.com/package/@rivinia/toast)
4
4
  [![license](https://img.shields.io/npm/l/@rivinia/toast.svg)](https://www.npmjs.com/package/@rivinia/toast)
5
5
 
6
- <p align="center">
7
- <img src="https://raw.githubusercontent.com/abdelfattahqandil21-oss/Revinia/toast/v1.0.0/assets/rivinia-abstract-mark-light.svg" alt="Rivinia" width="340" />
8
- </p>
6
+ A signal-based Angular toast queue with an optional ready-made UI.
9
7
 
10
- An Angular signal-based toast queue with timers, actions, deduplication, and
11
- overflow handling. Core is headless; an optional standalone UI is available at
12
- `@rivinia/toast/ui`.
13
-
14
- - Package version: **1.0.0**
15
- - Peer dependency: **`@angular/core` `^22.0.0`**
16
- - No `tslib`, router, or HTTP peer dependency.
17
-
18
- ## Install and configure
19
-
20
- ```bash
21
- npm install @rivinia/toast
22
- ng add @rivinia/toast
23
- ```
24
-
25
- `ng add` adds `provideRiviniaToast()` to a standalone app's `app.config.ts`; it
26
- does not add the optional UI component. You can also configure the service
27
- manually:
8
+ Package version: **1.0.1**
28
9
 
29
10
  ```ts
30
- // src/app/app.config.ts
31
- import type { ApplicationConfig } from '@angular/core';
32
- import { provideRiviniaToast } from '@rivinia/toast';
33
-
34
- export const appConfig: ApplicationConfig = {
35
- providers: [
36
- provideRiviniaToast({
37
- duration: 5000,
38
- durations: { error: 8000 },
39
- maxVisible: 5,
40
- overflow: 'dismiss-oldest',
41
- }),
42
- ],
43
- };
44
- ```
45
-
46
- All provider fields are optional. Defaults are 5000 ms, five active toasts,
47
- an unbounded queue, and `dismiss-oldest` overflow. `ng add` also accepts
48
- `--duration` and `--max-visible`.
49
-
50
- ## Use with the optional UI
51
-
52
- The UI is a separate secondary entry point; import its component explicitly.
53
- This standalone example keeps the injected manager in a class field (injection
54
- must not be called from the template):
55
-
56
- ```ts
57
- // src/app/notifications.component.ts
58
11
  import { Component } from '@angular/core';
59
12
  import { injectRiviniaToast } from '@rivinia/toast';
60
13
  import { RiviniaToastList } from '@rivinia/toast/ui';
61
-
62
- @Component({
63
- selector: 'app-notifications',
64
- imports: [RiviniaToastList],
65
- templateUrl: './notifications.component.html',
66
- })
14
+ @Component({ imports: [RiviniaToastList], template: '<rv-toast-list />' })
67
15
  export class NotificationsComponent {
68
16
  readonly toast = injectRiviniaToast();
69
-
70
- save(): void {
71
- this.toast.show({
72
- message: 'Changes saved',
73
- kind: 'success',
74
- dedupeKey: 'save',
75
- });
76
- }
77
-
78
- saveWithRetry(retry: () => void): void {
79
- this.toast.show({
80
- message: 'Could not save',
81
- kind: 'error',
82
- duration: 0,
83
- action: { label: 'Retry', onPress: retry },
84
- });
85
- }
17
+ save() { this.toast.show({ message: 'Changes saved', kind: 'success' }); }
86
18
  }
87
19
  ```
88
20
 
89
- ```html
90
- <!-- src/app/notifications.component.html -->
91
- <button type="button" (click)="save()">Save</button>
92
- <rv-toast-list [manager]="toast" position="bottom-end" />
93
- ```
21
+ ## Why @rivinia/toast
94
22
 
95
- The standalone list renders the manager's `active()` signal, pauses timed
96
- notifications on hover and keyboard focus, runs actions, and provides a
97
- dismiss button by default. Its defaults are `position="bottom-end"`,
98
- `animation="slide"`, `richColors=false`, and `showCloseButton=true`.
99
- `position` supports `top`/`bottom` × `start`/`center`/`end` (logical start/end
100
- work in RTL); `animation` supports `slide`, `fade`, and `none`. Errors use an
101
- assertive live region; other kinds use polite announcements.
23
+ - A signal-based queue handles timers, actions, deduplication, and overflow.
24
+ - The service can be used without rendering the optional list component.
25
+ - Package-specific tokens and shared theme tokens can both style the UI.
102
26
 
103
- For snapshot-driven UIs, provide the snapshots and handle the emitted IDs
104
- yourself:
27
+ ## Install
105
28
 
106
- ```ts
107
- // src/app/toast-snapshots.component.ts
108
- import { Component } from '@angular/core';
109
- import type { Toast } from '@rivinia/toast';
110
- import { RiviniaToastList } from '@rivinia/toast/ui';
111
-
112
- @Component({
113
- selector: 'app-toast-snapshots',
114
- imports: [RiviniaToastList],
115
- templateUrl: './toast-snapshots.component.html',
116
- })
117
- export class ToastSnapshotsComponent {
118
- items: readonly Toast[] = [
119
- {
120
- id: 1,
121
- kind: 'info',
122
- message: 'A background task is ready',
123
- duration: 0,
124
- remaining: 0,
125
- paused: false,
126
- action: { label: 'Open', onPress: () => console.log('Open task') },
127
- },
128
- ];
129
-
130
- remove(id: number): void {
131
- this.items = this.items.filter((item) => item.id !== id);
132
- }
133
-
134
- run(id: number): void {
135
- const item = this.items.find((candidate) => candidate.id === id);
136
- if (!item?.action) return;
137
- item.action.onPress();
138
- if (item.action.dismissAfterPress ?? true) this.remove(id);
139
- }
140
- }
141
- ```
142
-
143
- ```html
144
- <!-- src/app/toast-snapshots.component.html -->
145
- <rv-toast-list [toasts]="items" (dismissed)="remove($event)" (actionPressed)="run($event)" />
146
- ```
147
-
148
- In this presentational mode your app owns timers, pause behavior, and action
149
- handling. If both `manager` and `toasts` are supplied, the manager takes
150
- precedence.
151
-
152
- ## Use the headless service
153
-
154
- Core can be used without the UI. `active()` and `queue()` are readonly signals;
155
- the active list is oldest-first. Timers begin only when a toast becomes active.
156
-
157
- ```ts
158
- // src/app/save-status.component.ts
159
- import { Component, inject } from '@angular/core';
160
- import { resolveToastText, RiviniaToast } from '@rivinia/toast';
161
-
162
- @Component({
163
- selector: 'app-save-status',
164
- templateUrl: './save-status.component.html',
165
- })
166
- export class SaveStatusComponent {
167
- readonly toast = inject(RiviniaToast);
168
- readonly resolveText = resolveToastText;
169
-
170
- save(): void {
171
- const id: number | null = this.toast.show({ message: 'Saving…' });
172
- if (id === null) return; // possible with overflow: 'reject'
173
-
174
- // A matching key updates the existing active or queued item.
175
- this.toast.show({
176
- message: 'Saved',
177
- kind: 'success',
178
- dedupeKey: 'save',
179
- });
180
- }
181
- }
182
- ```
183
-
184
- ```html
185
- <!-- src/app/save-status.component.html -->
186
- <button type="button" (click)="save()">Save</button>
187
- @for (item of toast.active(); track item.id) {
188
- <p [attr.data-kind]="item.kind">{{ resolveText(item.message) }}</p>
189
- }
190
- ```
191
-
192
- `message` and action `label` accept either a string or a reactive string
193
- signal, so they can reflect language changes. For a custom outlet, use the exported
194
- `resolveToastText(message)` utility to read either shape.
195
-
196
- ## Theming
197
-
198
- Every component reads **its own token → the shared semantic layer → its
199
- built-in default**:
200
-
201
- ```css
202
- --rv-toast-bg → --rv-bg → #ffffff
203
- ```
204
-
205
- | Level | Token | Scope |
206
- | -------------- | --------------- | ----------------------------------- |
207
- | 1. per package | `--rv-toast-bg` | that toast only |
208
- | 2. global | `--rv-bg` | every Rivinia component, everywhere |
209
- | 3. default | — | what you get if you set nothing |
210
-
211
- ```css
212
- @import '@rivinia/theme';
213
-
214
- :root {
215
- --rv-bg: #ffffff;
216
- --rv-fg: #111111;
217
- --rv-border: #e0e0e0;
218
- }
219
- ```
220
-
221
- Its defaults sit in `@layer rivinia-theme`, so your unlayered `:root` rules win
222
- wherever they appear. **Map it on `:root`, never on a component element** — an
223
- element-level declaration shadows the root value and the override silently
224
- stops working.
225
-
226
- ### The library's own tokens
227
-
228
- | Token | Default | Applies to |
229
- | ------------------- | ----------------------------- | -------------- |
230
- | `--rv-toast-bg` | `#ffffff` | the toast |
231
- | `--rv-toast-fg` | `#111111` | the toast |
232
- | `--rv-toast-muted` | `#555555` | the kind rail |
233
- | `--rv-toast-border` | `#e0e0e0` | the toast |
234
- | `--rv-toast-radius` | `12px` | the toast |
235
- | `--rv-toast-shadow` | `0 12px 32px rgba(0,0,0,.32)` | the toast |
236
- | `--rv-toast-gap` | `8px` | between toasts |
237
- | `--rv-toast-width` | `22rem` | the list |
238
- | `--rv-toast-z` | `1000` | the list |
239
-
240
- The kind colours chain to the semantic layer too:
241
-
242
- | Token | Semantic token |
243
- | -------------------- | -------------- |
244
- | `--rv-toast-success` | `--rv-ok` |
245
- | `--rv-toast-error` | `--rv-danger` |
246
- | `--rv-toast-info` | `--rv-info` |
247
- | `--rv-toast-warning` | `--rv-warning` |
248
-
249
- **No forced light mode** — there is no `prefers-color-scheme` block in this
250
- package, so a toast never overrides your explicit choice.
251
-
252
- ### Which styling stack?
253
-
254
- | Stack | Import |
255
- | --------------- | -------------------------------------------- |
256
- | **Plain CSS** | `@import '@rivinia/theme';` |
257
- | **Tailwind v4** | `@import '@rivinia/theme/tailwind.css';` |
258
- | **Bootstrap** | `@import '@rivinia/theme/bootstrap.css';` |
259
- | **Sass / SCSS** | `@use '@rivinia/theme/scss';` |
260
- | **Less** | `@import (less) '@rivinia/theme/theme.css';` |
261
-
262
- ## The list is in the top layer
263
-
264
- A `position: fixed` list with a `z-index` is still in the DOCUMENT, so it is
265
- clipped by an `overflow: hidden` ancestor and loses to any stacking context
266
- above it — including the one a modal `<dialog>` creates.
267
-
268
- The list is therefore a **manual popover**, which puts it in the browser's top
269
- layer: the same layer a modal `<dialog>` uses. The top layer stacks in the
270
- order elements are shown, and the list is re-shown whenever its contents
271
- change, so a toast raised while a dialog is open lands above it. Measured: the
272
- element at the toast's position is the toast, not the dialog.
273
-
274
- A manual popover is not modal: it does not trap focus, does not make the page
275
- inert, and does not close on an outside click — so it behaves exactly like
276
- the list it replaces.
277
-
278
- **The popover's user-agent styles are reset.** A popover gets a background, a
279
- border, padding and `overflow: auto` from the UA, which puts a shared canvas
280
- and a scrollbar around the stack — so the toasts read as one block instead of
281
- separate cards. The list is a transparent, borderless column.
282
-
283
- Where the browser has no `showPopover` the list stays in the document and a
284
- `z-index` decides its order. The feature is detected, not assumed.
285
-
286
- ## The list must be MANAGED
287
-
288
- `rv-toast-list` renders what it is given. Without a manager it has an empty
289
- list and **no toast ever appears**:
290
-
291
- ```html
292
- <!-- nothing shows: the list is presentational and empty -->
293
- <rv-toast-list position="bottom-end" />
294
-
295
- <!-- the manager drives it -->
296
- <rv-toast-list [manager]="toast" position="bottom-end" />
29
+ ```bash
30
+ npm install @rivinia/toast
31
+ ng add @rivinia/toast
297
32
  ```
298
33
 
299
- ## API and behavior
300
-
301
- | Export | Kind | Purpose |
302
- | -------------------------------------------------------------------------------------------------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------- |
303
- | `provideRiviniaToast(config?)` | Provider | Configure defaults; see `RiviniaToastConfig`. |
304
- | `RiviniaToast` | Service | `show`, `dismiss`, `dismissAll`, `update`, `pause`, `resume`, `runAction`; `active` and `queue` signals. |
305
- | `injectRiviniaToast()` | Function | Shorthand for `inject(RiviniaToast)`. |
306
- | UI secondary entry point | `@rivinia/toast/ui` | Optional managed or presentational standalone list and its UI types. |
307
- | `Toast`, `ToastKind`, `ToastMessage`, `ToastAction`, `ToastOptions`, `ToastChanges`, `ToastOverflowPolicy`, `RiviniaToastConfig` | Types | Public service, configuration, and UI input types. |
308
- | `resolveToastText`, `clampDuration` | Utilities | Resolve signal/string text; normalize a duration. |
309
-
310
- Important details:
311
-
312
- - `show()` returns a stable `number` ID, or `null` when rejected by the
313
- `reject` policy or when a finite queue is full. `dismiss()` and `update()`
314
- return `false` for unknown or dismissed IDs; `pause()`/`resume()` also return
315
- `false` when the timer cannot be changed.
316
- - A repeated `dedupeKey` reuses the active or queued toast ID and updates it
317
- rather than stacking another toast. A matching `show()` without an `action`
318
- clears the previous action; include it again if it should remain.
319
- - A toast-specific `duration` overrides per-kind and global defaults.
320
- `duration: 0`, negative values, not-a-number, and infinities mean persistent (no
321
- timer). Per-kind defaults are set with `durations`, for example
322
- `{ error: 8000 }`.
323
- - `pause()` preserves the remaining time on an active timed toast. Persistent
324
- or queued toasts cannot be paused. `resume()` restarts the saved time; if it
325
- has already elapsed, the toast is dismissed.
326
- - With `overflow: 'queue'`, new toasts wait without timers and are promoted
327
- when a visible slot opens. Set `maxQueue` to bound that queue. With
328
- `overflow: 'reject'`, `show()` returns `null`. `dismiss-oldest` replaces an
329
- active toast, preferring timed, actionless toasts for eviction.
330
- - `update(id, changes)` restarts an active timer; a queued item's timer starts
331
- only on promotion. Changing `kind` re-applies that kind's default duration
332
- unless the toast was given an explicit duration. `action: null` removes an
333
- action.
334
- - `runAction(id)` runs the callback, then dismisses by default.
335
- `dismissAfterPress: false` keeps the toast visible. Callback errors propagate
336
- to the caller (and, in managed UI use, Angular's `ErrorHandler`).
337
- - No routing policy is built in. To clear timed notifications on navigation,
338
- subscribe to your router in app code and call `dismiss(id)` as appropriate.
34
+ ## Quick start
339
35
 
340
- ## Testing app code
36
+ 1. Register `provideRiviniaToast()` and import `RiviniaToastList`.
37
+ 2. Inject the toast service in a component.
38
+ 3. Render the list and show a notification.
341
39
 
342
- Use fake timers to make timer-dependent tests deterministic:
40
+ ## Two ways to use it
343
41
 
344
- ```ts
345
- import { TestBed } from '@angular/core/testing';
346
- import { afterEach, beforeEach, expect, it, vi } from 'vitest';
347
- import { provideRiviniaToast, RiviniaToast } from '@rivinia/toast';
348
-
349
- beforeEach(() => {
350
- TestBed.configureTestingModule({ providers: [provideRiviniaToast()] });
351
- vi.useFakeTimers();
352
- });
353
-
354
- afterEach(() => {
355
- TestBed.resetTestingModule();
356
- vi.useRealTimers();
357
- });
42
+ | Ready-made UI | Headless service |
43
+ | --- | --- |
44
+ | `@rivinia/toast/ui` | `@rivinia/toast` |
358
45
 
359
- it('auto-dismisses after the configured duration', () => {
360
- const toast = TestBed.inject(RiviniaToast);
361
- const id = toast.show({ message: 'Saved', duration: 1000 });
362
-
363
- vi.advanceTimersByTime(1000);
364
- expect(toast.active().some((item) => item.id === id)).toBe(false);
365
- });
366
- ```
46
+ The UI and controller are independent; use either or combine them.
367
47
 
368
- ## License
48
+ ## Compatibility
369
49
 
370
- MIT.
50
+ | Angular | Node | Browser verification |
51
+ | --- | --- | --- |
52
+ | `^22.0.0` | `^22.22.3 or ^24.15.0 or >=26.0.0` | Chromium and Firefox; native Safari not verified |
371
53
 
372
- ## Further reading
54
+ ## Go further
373
55
 
374
- [Package design and usage notes](https://github.com/abdelfattahqandil21-oss/Revinia/blob/toast/v1.0.0/docs/decisions/D030-rivinia-toast-ui-list-component-no-new-deps.md).
56
+ | Topic | Read |
57
+ | --- | --- |
58
+ | Ready-made UI | [Ready-made UI](https://github.com/abdelfattahqandil21-oss/Revinia/blob/toast/v1.0.1/docs/toast/ready-made-ui.md) |
59
+ | Headless usage | [Headless usage](https://github.com/abdelfattahqandil21-oss/Revinia/blob/toast/v1.0.1/docs/toast/headless.md) |
60
+ | Theming | [Theming](https://github.com/abdelfattahqandil21-oss/Revinia/blob/toast/v1.0.1/docs/toast/theming.md) |
61
+ | Top layer and managed list | [Top layer and managed list](https://github.com/abdelfattahqandil21-oss/Revinia/blob/toast/v1.0.1/docs/toast/top-layer-and-managed-list.md) |
62
+ | API reference | [API reference](https://github.com/abdelfattahqandil21-oss/Revinia/blob/toast/v1.0.1/docs/toast/api.md) |
63
+ | Testing | [Testing](https://github.com/abdelfattahqandil21-oss/Revinia/blob/toast/v1.0.1/docs/toast/testing.md) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rivinia/toast",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Headless toast queue for Angular: signals, pause-on-hover timers, actions.",
5
5
  "keywords": [
6
6
  "angular",