@rivinia/toast 0.2.0 → 0.3.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.2.0**
11
+ - Version: **0.3.0**
12
12
  - Requires: **Angular `^22.0.0`**, `tslib`
13
13
 
14
14
  ## Install
@@ -65,8 +65,13 @@ export class Checkout {
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
67
67
  (and repeated dismissal) instead of throwing. `duration: 0` = persistent.
68
+ `pause()` suspends only running timers (persistent toasts can never pause, so
69
+ hover-leave cannot dismiss them); `resume()` on anything unpaused is a no-op;
70
+ each toast exposes its `paused` flag for progress UI.
68
71
  Timers start at queue **promotion**, never while queued. Per-toast options
69
- always beat globals.
72
+ always beat globals; a kind change re-resolves the per-kind default unless the
73
+ toast carries an explicit duration. Overflow eviction prefers timed,
74
+ actionless toasts — persistent errors with actions are last resort.
70
75
 
71
76
  ## Recipes (app-owned, deliberately not in the library)
72
77
 
@@ -17,6 +17,8 @@ var RiviniaToast = class RiviniaToast {
17
17
  active = this._active.asReadonly();
18
18
  queue = this._queue.asReadonly();
19
19
  _timers = /* @__PURE__ */ new Map();
20
+ _paused = /* @__PURE__ */ new Set();
21
+ _explicitDuration = /* @__PURE__ */ new Set();
20
22
  _generations = /* @__PURE__ */ new Map();
21
23
  _dedupeByKey = /* @__PURE__ */ new Map();
22
24
  _dedupeKeyById = /* @__PURE__ */ new Map();
@@ -40,12 +42,14 @@ var RiviniaToast = class RiviniaToast {
40
42
  const id = ++this._nextId;
41
43
  const kind = options.kind ?? "info";
42
44
  const duration = this.resolveDuration(kind, options.duration);
45
+ if (options.duration !== void 0) this._explicitDuration.add(id);
43
46
  const toast = {
44
47
  id,
45
48
  kind,
46
49
  message: options.message,
47
50
  duration,
48
51
  remaining: duration,
52
+ paused: false,
49
53
  action: options.action ?? null
50
54
  };
51
55
  if (options.dedupeKey !== void 0) {
@@ -69,9 +73,10 @@ var RiviniaToast = class RiviniaToast {
69
73
  this.forgetDedupe(id);
70
74
  return null;
71
75
  default: {
72
- const oldest = this._active()[0];
73
- if (oldest) this.dismiss(oldest.id);
74
- this._active.update((list) => [...list, toast]);
76
+ const list = this._active();
77
+ const victim = list.find((t) => t.duration > 0 && !t.action) ?? list[0];
78
+ if (victim) this.dismiss(victim.id);
79
+ this._active.update((next) => [...next, toast]);
75
80
  this.arm(id, duration);
76
81
  return id;
77
82
  }
@@ -79,14 +84,22 @@ var RiviniaToast = class RiviniaToast {
79
84
  }
80
85
  update(id, changes) {
81
86
  const inActive = this._active().some((t) => t.id === id);
82
- const apply = (t) => ({
83
- ...t,
84
- message: changes.message ?? t.message,
85
- kind: changes.kind ?? t.kind,
86
- duration: changes.duration !== void 0 ? clampDuration(changes.duration, t.duration) : t.duration,
87
- remaining: changes.duration !== void 0 ? clampDuration(changes.duration, t.duration) : t.duration,
88
- action: changes.action !== void 0 ? changes.action : t.action
89
- });
87
+ let nextDuration = 0;
88
+ const apply = (t) => {
89
+ if (changes.duration !== void 0) {
90
+ nextDuration = clampDuration(changes.duration, t.duration);
91
+ this._explicitDuration.add(id);
92
+ } else if (changes.kind !== void 0 && !this._explicitDuration.has(id)) nextDuration = this.fallbackDuration(changes.kind);
93
+ else nextDuration = t.duration;
94
+ return {
95
+ ...t,
96
+ message: changes.message ?? t.message,
97
+ kind: changes.kind ?? t.kind,
98
+ duration: nextDuration,
99
+ remaining: nextDuration,
100
+ action: changes.action !== void 0 ? changes.action : t.action
101
+ };
102
+ };
90
103
  if (inActive) {
91
104
  this.disarm(id);
92
105
  let next;
@@ -95,7 +108,7 @@ var RiviniaToast = class RiviniaToast {
95
108
  next = apply(t);
96
109
  return next;
97
110
  }));
98
- if (next) this.arm(id, next.duration);
111
+ if (next && !this._paused.has(id)) this.arm(id, next.duration);
99
112
  return true;
100
113
  }
101
114
  let found = false;
@@ -108,6 +121,8 @@ var RiviniaToast = class RiviniaToast {
108
121
  }
109
122
  dismiss(id) {
110
123
  this.disarm(id);
124
+ this._paused.delete(id);
125
+ this._explicitDuration.delete(id);
111
126
  let removed = false;
112
127
  this._active.update((list) => {
113
128
  const next = list.filter((t) => t.id !== id);
@@ -127,6 +142,8 @@ var RiviniaToast = class RiviniaToast {
127
142
  }
128
143
  dismissAll() {
129
144
  this.clearTimers();
145
+ this._paused.clear();
146
+ this._explicitDuration.clear();
130
147
  this._active.set([]);
131
148
  this._queue.set([]);
132
149
  this._dedupeByKey.clear();
@@ -137,21 +154,28 @@ var RiviniaToast = class RiviniaToast {
137
154
  if (!record) return false;
138
155
  clearTimeout(record.handle);
139
156
  this._timers.delete(id);
157
+ this._paused.add(id);
140
158
  const elapsed = Date.now() - record.startedAt;
141
159
  this._active.update((list) => list.map((t) => t.id === id ? {
142
160
  ...t,
143
- remaining: Math.max(0, t.remaining - elapsed)
161
+ remaining: Math.max(0, t.remaining - elapsed),
162
+ paused: true
144
163
  } : t));
145
164
  return true;
146
165
  }
147
166
  resume(id) {
148
- if (this._timers.has(id)) return false;
167
+ if (!this._paused.has(id)) return false;
168
+ this._paused.delete(id);
149
169
  const toast = this._active().find((t) => t.id === id);
150
170
  if (!toast) return false;
151
171
  if (toast.remaining <= 0) {
152
172
  this.dismiss(id);
153
173
  return true;
154
174
  }
175
+ this._active.update((list) => list.map((t) => t.id === id ? {
176
+ ...t,
177
+ paused: false
178
+ } : t));
155
179
  this.arm(id, toast.remaining);
156
180
  return true;
157
181
  }
@@ -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 _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 const toast: Toast = {\n id,\n kind,\n message: options.message,\n duration,\n remaining: duration,\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 const oldest = this._active()[0];\n if (oldest) this.dismiss(oldest.id);\n this._active.update((list) => [...list, 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 const apply = (t: Toast): Toast => ({\n ...t,\n message: changes.message ?? t.message,\n kind: changes.kind ?? t.kind,\n duration: changes.duration !== undefined ? clampDuration(changes.duration, t.duration) : t.duration,\n remaining:\n changes.duration !== undefined ? clampDuration(changes.duration, t.duration) : t.duration,\n action: changes.action !== undefined ? changes.action : t.action,\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.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 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._active.set([]);\n this._queue.set([]);\n this._dedupeByKey.clear();\n this._dedupeKeyById.clear();\n }\n\n /**\n * Suspends the active timer, preserving `remaining`. UI binds this to\n * hover/focus (WCAG 2.2.1 timing control). Returns `false` for unknown ids,\n * queued/persistent toasts, or already-paused timers.\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 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) } : t,\n ),\n );\n return true;\n }\n\n /**\n * Restarts a paused timer with the preserved `remaining`. A toast paused at\n * zero remaining is dismissed instead (it logically already expired).\n */\n resume(id: number): boolean {\n if (this._timers.has(id)) return false;\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.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 {\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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rivinia/toast",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Headless toast queue for Angular with optional UI outlet: signals, pause-on-hover timers, actions.",
5
5
  "keywords": [
6
6
  "angular",
@@ -13,9 +13,6 @@
13
13
  "peerDependencies": {
14
14
  "@angular/core": "^22.0.0"
15
15
  },
16
- "dependencies": {
17
- "tslib": "^2.3.0"
18
- },
19
16
  "sideEffects": false,
20
17
  "schematics": "./schematics/collection.json",
21
18
  "module": "fesm2022/rivinia-toast.mjs",
@@ -29,5 +26,8 @@
29
26
  "default": "./fesm2022/rivinia-toast.mjs"
30
27
  }
31
28
  },
32
- "type": "module"
29
+ "type": "module",
30
+ "dependencies": {
31
+ "tslib": "^2.3.0"
32
+ }
33
33
  }
@@ -36,6 +36,12 @@ interface Toast {
36
36
  readonly duration: number;
37
37
  /** Milliseconds left on the active timer. Equals `duration` when fresh/queued. */
38
38
  readonly remaining: number;
39
+ /**
40
+ * Whether the timer is currently suspended (`pause()` called, `resume()`
41
+ * pending). UI binds this to hover/focus state and progress display.
42
+ * Persistent toasts are never paused.
43
+ */
44
+ readonly paused: boolean;
39
45
  readonly action: ToastAction | null;
40
46
  }
41
47
  /** Arguments to `show()`. */
@@ -142,6 +148,9 @@ export declare class RiviniaToast {
142
148
  /** Overflow waiting for room. No timers run while queued. */
143
149
  readonly queue: import("@angular/core").Signal<Toast[]>;
144
150
  private readonly _timers;
151
+ private readonly _paused;
152
+ /** Ids whose duration was set explicitly (per-toast override wins forever). */
153
+ private readonly _explicitDuration;
145
154
  private readonly _generations;
146
155
  private readonly _dedupeByKey;
147
156
  private readonly _dedupeKeyById;
@@ -167,14 +176,17 @@ export declare class RiviniaToast {
167
176
  /** Clears timers, active and queue. Ids keep incrementing (never reused). */
168
177
  dismissAll(): void;
169
178
  /**
170
- * Suspends the active timer, preserving `remaining`. UI binds this to
171
- * hover/focus (WCAG 2.2.1 timing control). Returns `false` for unknown ids,
172
- * queued/persistent toasts, or already-paused timers.
179
+ * Suspends the active timer, preserving `remaining` and marking `paused`.
180
+ * UI binds this to hover/focus (WCAG 2.2.1 timing control). Returns `false`
181
+ * for unknown ids, queued/persistent toasts, or already-paused timers —
182
+ * a persistent toast can never be paused, so hover-leave cannot dismiss it.
173
183
  */
174
184
  pause(id: number): boolean;
175
185
  /**
176
- * Restarts a paused timer with the preserved `remaining`. A toast paused at
177
- * zero remaining is dismissed instead (it logically already expired).
186
+ * Restarts a paused timer with the preserved `remaining`. No-op (`false`) on
187
+ * anything that was never paused — including persistent toasts. A paused
188
+ * toast whose remaining reached zero is dismissed instead (it logically
189
+ * already expired).
178
190
  */
179
191
  resume(id: number): boolean;
180
192
  /**