@ailuracode/alpine-toast 0.1.1 → 1.0.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/dist/index.js CHANGED
@@ -1,156 +1,394 @@
1
+ // src/controller.ts
2
+ import {
3
+ BaseController,
4
+ clearSingleton,
5
+ createSingleton,
6
+ generateId
7
+ } from "@ailuracode/alpine-core";
8
+
1
9
  // src/types.ts
2
10
  var TOAST_STORE_KEY = "toast";
3
11
 
4
- // src/config.ts
5
- var DEFAULT_PLUGIN_OPTIONS = {
6
- defaultPosition: "bottom-right",
7
- defaultDuration: 4e3,
8
- maxToasts: 5,
9
- listenToWindowEvents: true
10
- };
11
- function variantFromList(variants, preferred) {
12
- if (variants.includes(preferred)) {
13
- return preferred;
14
- }
15
- return "default";
16
- }
17
- function resolvePromiseConfig(variants, promise = {}) {
18
- return {
19
- loading: promise.loading ?? "Loading...",
20
- error: promise.error ?? "Error",
21
- duration: promise.duration ?? 4e3,
22
- loadingVariant: promise.loadingVariant ?? variantFromList(variants, "loading"),
23
- successVariant: promise.successVariant ?? variantFromList(variants, "success"),
24
- errorVariant: promise.errorVariant ?? variantFromList(variants, "error")
25
- };
12
+ // src/controller.ts
13
+ var DISMISS_DELAY_MS = 400;
14
+ var DEFAULT_MAX_TOASTS = 5;
15
+ var DEFAULT_DURATION_MS = 4e3;
16
+ var TOAST_SINGLETON_KEY = "@ailuracode/alpine-toast/default";
17
+ function createToastController(options = {}) {
18
+ return createSingleton(TOAST_SINGLETON_KEY, () => {
19
+ const controller = new ToastController(options);
20
+ controller.mount();
21
+ return controller;
22
+ });
26
23
  }
27
- function resolveToastPluginConfig(options = {}) {
28
- const variants = options.variants ?? [];
29
- const positions = options.positions ?? [];
30
- const maxToasts = options.maxToasts ?? DEFAULT_PLUGIN_OPTIONS.maxToasts;
31
- let maxVisible = options.maxVisible ?? maxToasts;
32
- if (maxToasts > 0 && maxVisible > maxToasts) {
33
- maxVisible = maxToasts;
24
+ var ToastController = class extends BaseController {
25
+ #defaultPosition;
26
+ #stackPositions;
27
+ #defaultDuration;
28
+ #maxToasts;
29
+ #maxVisible;
30
+ #storeKey;
31
+ #listenToWindowEvents;
32
+ #getStore;
33
+ #dismissTimers = /* @__PURE__ */ new Map();
34
+ #purgeTimers = /* @__PURE__ */ new Map();
35
+ #items = [];
36
+ constructor(options) {
37
+ super(options.id ?? generateId("toast"));
38
+ this.#defaultPosition = options.defaultPosition ?? "bottom-right";
39
+ this.#stackPositions = resolveStackPositions(this.#defaultPosition, options.positions);
40
+ this.#defaultDuration = options.defaultDuration ?? DEFAULT_DURATION_MS;
41
+ const limits = resolveToastLimits({
42
+ maxToasts: options.maxToasts,
43
+ maxVisible: options.maxVisible
44
+ });
45
+ this.#maxToasts = limits.maxToasts;
46
+ this.#maxVisible = limits.maxVisible;
47
+ this.#storeKey = options.storeKey ?? TOAST_STORE_KEY;
48
+ this.#listenToWindowEvents = options.listenToWindowEvents !== false;
49
+ this.#getStore = options.getStore;
34
50
  }
35
- return {
36
- defaultPosition: options.defaultPosition ?? DEFAULT_PLUGIN_OPTIONS.defaultPosition,
37
- defaultDuration: options.defaultDuration ?? DEFAULT_PLUGIN_OPTIONS.defaultDuration,
38
- maxToasts,
39
- maxVisible,
40
- listenToWindowEvents: options.listenToWindowEvents ?? DEFAULT_PLUGIN_OPTIONS.listenToWindowEvents,
41
- storeKey: options.storeKey ?? TOAST_STORE_KEY,
42
- variants,
43
- positions,
44
- promise: resolvePromiseConfig(variants, options.promise)
45
- };
46
- }
47
- function toastOptions(options) {
48
- return options;
49
- }
50
- function toastVariants(variants) {
51
- return variants;
52
- }
53
- function toastPositions(positions) {
54
- return positions;
55
- }
56
-
57
- // src/store.ts
58
- function resolveToastLimits(options = {}) {
59
- const maxToasts = options.maxToasts ?? 5;
60
- let maxVisible = options.maxVisible ?? maxToasts;
61
- if (maxToasts > 0 && maxVisible > maxToasts) {
62
- maxVisible = maxToasts;
51
+ /**
52
+ * Tears down every side effect. Idempotent. `super.destroy()` runs
53
+ * the registered cleanups (currently just the window listener)
54
+ * first; the timer cleanup runs as a final step. Also releases
55
+ * the singleton slot so the next `createToastController()` call
56
+ * builds a fresh controller.
57
+ */
58
+ destroy() {
59
+ if (this.isDestroyed) {
60
+ return;
61
+ }
62
+ super.destroy();
63
+ this.#clearAllTimers();
64
+ clearSingleton(TOAST_SINGLETON_KEY);
63
65
  }
64
- return { maxToasts, maxVisible };
65
- }
66
- function resolveStackPositions(defaultPosition, positions) {
67
- const stacks = [defaultPosition];
68
- for (const position of positions ?? []) {
69
- if (!stacks.includes(position)) {
70
- stacks.push(position);
66
+ /**
67
+ * Starts the controller's side effects. The constructor leaves
68
+ * `#items` empty; `mount()` registers the `toast` window event
69
+ * listener (when enabled) and emits the initialization event.
70
+ *
71
+ * Calling `mount()` more than once is a no-op — `BaseController`
72
+ * guards the phase. Calling it after `destroy()` throws.
73
+ */
74
+ mount() {
75
+ if (this.isMounted) {
76
+ return;
71
77
  }
78
+ super.mount();
79
+ if (this.#listenToWindowEvents && typeof window !== "undefined") {
80
+ const abort = new AbortController();
81
+ const handler = (event) => {
82
+ if (event instanceof CustomEvent) {
83
+ this.fromPayload(event.detail ?? {});
84
+ }
85
+ };
86
+ window.addEventListener("toast", handler, { signal: abort.signal });
87
+ this.registerCleanup(() => abort.abort());
88
+ }
89
+ queueMicrotask(() => {
90
+ if (this.isDestroyed) {
91
+ return;
92
+ }
93
+ this.#emitChange("initialization");
94
+ });
72
95
  }
73
- return stacks;
74
- }
75
- function createToastId() {
76
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
77
- return crypto.randomUUID();
96
+ // ── Public state surface ────────────────────────────────────────
97
+ get defaultPosition() {
98
+ return this.#defaultPosition;
78
99
  }
79
- return `toast-${Date.now()}-${Math.random().toString(36).slice(2)}`;
80
- }
81
- function normalizeToastDuration(duration) {
82
- if (duration === 0) {
83
- return false;
100
+ get stackPositions() {
101
+ return this.#stackPositions;
84
102
  }
85
- return duration;
86
- }
87
- function resolveToastDuration(duration, defaultDuration) {
88
- if (duration === void 0) {
89
- return normalizeToastDuration(defaultDuration);
103
+ get maxToasts() {
104
+ return this.#maxToasts;
90
105
  }
91
- return normalizeToastDuration(duration);
92
- }
93
- function shouldAutoDismiss(duration) {
94
- return typeof duration === "number" && duration > 0 && Number.isFinite(duration);
95
- }
96
- function isPersistentDuration(duration) {
97
- return !shouldAutoDismiss(duration);
98
- }
99
- var PROMISE_LOADING_DURATION = 36e5;
100
- function itemsAtPosition(items, position) {
101
- return items.filter((item) => item.position === position);
102
- }
103
- function timedStackAt(items, position) {
104
- return itemsAtPosition(items, position).filter((item) => shouldAutoDismiss(item.duration));
105
- }
106
- function persistentStackAt(items, position) {
107
- return itemsAtPosition(items, position).filter((item) => isPersistentDuration(item.duration));
108
- }
109
- function enforcePositionLimit(items, position, maxToasts, dismissMany, stack) {
110
- if (maxToasts <= 0) {
111
- return;
112
- }
113
- const stackAtPosition = stack === "persistent" ? persistentStackAt(items, position) : timedStackAt(items, position);
114
- const activeAtPosition = stackAtPosition.filter((item) => !item.removed);
115
- const overflowIds = activeAtPosition.slice(maxToasts).map((item) => item.id);
116
- dismissMany(overflowIds);
117
- }
118
- function applyToastPatch(item, payload) {
119
- const next = { ...item };
120
- for (const [key, value] of Object.entries(payload)) {
121
- if (value !== void 0) {
122
- if (key === "duration") {
123
- Object.assign(next, { duration: normalizeToastDuration(value) });
124
- } else {
125
- Object.assign(next, { [key]: value });
106
+ get maxVisible() {
107
+ return this.#maxVisible;
108
+ }
109
+ get items() {
110
+ return this.#items;
111
+ }
112
+ // ── Public commands ─────────────────────────────────────────────
113
+ push(payload = {}) {
114
+ if (this.isDestroyed) {
115
+ return "";
116
+ }
117
+ const position = payload.position ?? this.#defaultPosition;
118
+ const id = generateId("toast");
119
+ const toast = {
120
+ id,
121
+ key: payload.key ?? null,
122
+ content: payload.content ?? null,
123
+ title: payload.title ?? null,
124
+ description: payload.description ?? null,
125
+ variant: payload.variant ?? "default",
126
+ position,
127
+ duration: resolveToastDuration(payload.duration, this.#defaultDuration),
128
+ action: payload.action ?? null,
129
+ removed: false
130
+ };
131
+ this.#items = [toast, ...this.#items];
132
+ this.#enforcePositionLimit(
133
+ position,
134
+ shouldAutoDismiss(toast.duration) ? "timed" : "persistent"
135
+ );
136
+ this.#scheduleDismiss(id, toast.duration);
137
+ this.#emitChange("push");
138
+ return id;
139
+ }
140
+ pushUnique(key, payload = {}) {
141
+ if (this.isDestroyed) {
142
+ return "";
143
+ }
144
+ const activeIds = this.#items.filter((item) => !item.removed && item.key === key).map((item) => item.id);
145
+ this.#markRemoved(activeIds);
146
+ return this.push({ ...payload, key });
147
+ }
148
+ update(id, payload = {}) {
149
+ if (this.isDestroyed) {
150
+ return;
151
+ }
152
+ const current = this.#items.find((toast) => toast.id === id);
153
+ if (!current) {
154
+ return;
155
+ }
156
+ const previousPosition = current.position;
157
+ const wasPersistent = isPersistentDuration(current.duration);
158
+ this.#items = this.#items.map(
159
+ (item) => item.id === id ? applyToastPatch(item, payload) : item
160
+ );
161
+ const updated = this.#items.find((toast) => toast.id === id);
162
+ if (!updated) {
163
+ return;
164
+ }
165
+ const nextPosition = updated.position;
166
+ const isNowPersistent = isPersistentDuration(updated.duration);
167
+ const durationStackChanged = payload.duration !== void 0 && wasPersistent !== isNowPersistent;
168
+ const positionChanged = nextPosition !== previousPosition;
169
+ if (positionChanged) {
170
+ this.#enforcePositionLimit(previousPosition, "timed");
171
+ this.#enforcePositionLimit(previousPosition, "persistent");
172
+ }
173
+ if (positionChanged || durationStackChanged) {
174
+ this.#enforcePositionLimit(nextPosition, "timed");
175
+ this.#enforcePositionLimit(nextPosition, "persistent");
176
+ }
177
+ if (payload.duration !== void 0) {
178
+ this.#scheduleDismiss(id, updated.duration);
179
+ }
180
+ this.#emitChange("update");
181
+ }
182
+ dismiss(id) {
183
+ if (this.isDestroyed) {
184
+ return;
185
+ }
186
+ this.#markRemoved([id]);
187
+ this.#emitChange("dismiss");
188
+ }
189
+ dismissAt(position) {
190
+ if (this.isDestroyed) {
191
+ return;
192
+ }
193
+ const ids = this.itemsAt(position).filter((item) => !item.removed).map((item) => item.id);
194
+ this.#markRemoved(ids);
195
+ this.#emitChange("dismissAt");
196
+ }
197
+ dismissAll() {
198
+ if (this.isDestroyed) {
199
+ return;
200
+ }
201
+ const ids = this.#items.filter((item) => !item.removed).map((item) => item.id);
202
+ this.#markRemoved(ids);
203
+ this.#emitChange("dismissAll");
204
+ }
205
+ // ── Query helpers ───────────────────────────────────────────────
206
+ itemsAt(position) {
207
+ return this.#items.filter((item) => item.position === position);
208
+ }
209
+ timedItemsAt(position) {
210
+ return this.itemsAt(position).filter((item) => shouldAutoDismiss(item.duration));
211
+ }
212
+ persistentItemsAt(position) {
213
+ return this.itemsAt(position).filter((item) => isPersistentDuration(item.duration));
214
+ }
215
+ activeTimedItemsAt(position) {
216
+ return this.timedItemsAt(position).filter((item) => !item.removed);
217
+ }
218
+ activePersistentItemsAt(position) {
219
+ return this.persistentItemsAt(position).filter((item) => !item.removed);
220
+ }
221
+ isVisibleAt(position, index) {
222
+ const stack = this.timedItemsAt(position);
223
+ const item = stack[index];
224
+ if (!item || item.removed) {
225
+ return false;
226
+ }
227
+ if (this.#maxVisible <= 0) {
228
+ return true;
229
+ }
230
+ let rank = 0;
231
+ for (let i = 0; i <= index; i++) {
232
+ if (stack[i] && !stack[i].removed) {
233
+ rank++;
126
234
  }
127
235
  }
236
+ return rank <= this.#maxVisible;
128
237
  }
129
- return next;
130
- }
131
- function createToastStore(options = {}) {
132
- const defaultPosition = options.defaultPosition ?? "bottom-right";
133
- const stackPositions = resolveStackPositions(defaultPosition, options.positions);
134
- const defaultDuration = options.defaultDuration ?? 4e3;
135
- const { maxToasts, maxVisible } = resolveToastLimits(options);
136
- const dismissDelayMs = 400;
137
- const dismissTimers = /* @__PURE__ */ new Map();
138
- const purgeTimers = /* @__PURE__ */ new Map();
139
- const getStore = options.getStore;
238
+ /**
239
+ * Public escape hatch for callers (mainly the magic's `fromPayload`)
240
+ * that need to push a toast from a raw `ToastEventPayload` shape.
241
+ */
242
+ fromPayload(payload = {}) {
243
+ const { title = null, content = null, variant = "default", ...options } = payload;
244
+ return this.push({
245
+ title,
246
+ content,
247
+ variant,
248
+ ...options
249
+ });
250
+ }
251
+ // ── Internals ───────────────────────────────────────────────────
252
+ #emitChange(source) {
253
+ if (this.isDestroyed) {
254
+ return;
255
+ }
256
+ const detail = {
257
+ source,
258
+ items: this.#items
259
+ };
260
+ this.emit("change", detail);
261
+ }
262
+ /**
263
+ * Marks the given ids as `removed`. Schedules a purge timer so
264
+ * they drop out of the queue after `DISMISS_DELAY_MS`. Does NOT
265
+ * emit — the caller (push / update / dismiss / dismissAt /
266
+ * dismissAll / pushUnique) emits once after the mutation
267
+ * completes to avoid event storms on bulk dismisses.
268
+ */
269
+ #markRemoved(ids) {
270
+ if (ids.length === 0) {
271
+ return;
272
+ }
273
+ const idsToRemove = [];
274
+ for (const id of ids) {
275
+ const toast = this.#items.find((item) => item.id === id);
276
+ if (!toast || toast.removed) {
277
+ continue;
278
+ }
279
+ this.#clearDismissTimer(id);
280
+ idsToRemove.push(id);
281
+ }
282
+ if (idsToRemove.length === 0) {
283
+ return;
284
+ }
285
+ const removeSet = new Set(idsToRemove);
286
+ this.#items = this.#items.map(
287
+ (item) => removeSet.has(item.id) ? { ...item, removed: true } : item
288
+ );
289
+ const batchId = generateId("toast-purge");
290
+ const timer = setTimeout(() => {
291
+ this.#purgeTimers.delete(batchId);
292
+ if (this.isDestroyed) {
293
+ return;
294
+ }
295
+ this.#items = this.#items.filter((item) => !removeSet.has(item.id));
296
+ this.#emitChange("dismiss");
297
+ }, DISMISS_DELAY_MS);
298
+ this.#purgeTimers.set(batchId, timer);
299
+ }
300
+ /**
301
+ * Trims the active timed or persistent stack at `position` so it
302
+ * doesn't exceed `maxToasts`. Overflow items are marked as
303
+ * `removed` (with the standard delay before purge) — no exception
304
+ * is thrown.
305
+ */
306
+ #enforcePositionLimit(position, stack) {
307
+ if (this.#maxToasts <= 0) {
308
+ return;
309
+ }
310
+ const itemsAtPosition = this.itemsAt(position);
311
+ const stackItems = stack === "persistent" ? itemsAtPosition.filter((item) => isPersistentDuration(item.duration)) : itemsAtPosition.filter((item) => shouldAutoDismiss(item.duration));
312
+ const activeAtPosition = stackItems.filter((item) => !item.removed);
313
+ const overflowIds = activeAtPosition.slice(this.#maxToasts).map((item) => item.id);
314
+ this.#markRemoved(overflowIds);
315
+ }
316
+ #scheduleDismiss(id, duration) {
317
+ this.#clearDismissTimer(id);
318
+ if (duration === PROMISE_LOADING_DURATION) {
319
+ return;
320
+ }
321
+ if (shouldAutoDismiss(duration)) {
322
+ const timer = setTimeout(() => {
323
+ this.#dismissTimers.delete(id);
324
+ if (this.isDestroyed) {
325
+ return;
326
+ }
327
+ const target = this.#getStore?.() ?? null;
328
+ if (target) {
329
+ target.dismiss(id);
330
+ } else {
331
+ this.dismiss(id);
332
+ }
333
+ }, duration);
334
+ this.#dismissTimers.set(id, timer);
335
+ }
336
+ }
337
+ #clearDismissTimer(id) {
338
+ const timer = this.#dismissTimers.get(id);
339
+ if (timer !== void 0) {
340
+ clearTimeout(timer);
341
+ this.#dismissTimers.delete(id);
342
+ }
343
+ }
344
+ #clearAllTimers() {
345
+ for (const timer of this.#dismissTimers.values()) {
346
+ clearTimeout(timer);
347
+ }
348
+ this.#dismissTimers.clear();
349
+ for (const timer of this.#purgeTimers.values()) {
350
+ clearTimeout(timer);
351
+ }
352
+ this.#purgeTimers.clear();
353
+ }
354
+ };
355
+ function wrapToastStore(controller) {
140
356
  const store = {
141
- defaultPosition,
142
- stackPositions,
143
- maxToasts,
144
- maxVisible,
145
- items: [],
357
+ defaultPosition: controller.defaultPosition,
358
+ stackPositions: controller.stackPositions,
359
+ maxToasts: controller.maxToasts,
360
+ maxVisible: controller.maxVisible,
361
+ items: [...controller.items],
362
+ push(payload) {
363
+ return controller.push(payload);
364
+ },
365
+ pushUnique(key, payload) {
366
+ return controller.pushUnique(key, payload ?? {});
367
+ },
368
+ update(id, payload) {
369
+ controller.update(id, payload);
370
+ },
371
+ dismiss(id) {
372
+ controller.dismiss(id);
373
+ },
374
+ dismissAt(position) {
375
+ controller.dismissAt(position);
376
+ },
377
+ dismissAll() {
378
+ controller.dismissAll();
379
+ },
380
+ destroy() {
381
+ unsubscribe();
382
+ controller.destroy();
383
+ },
146
384
  itemsAt(position) {
147
- return itemsAtPosition(this.items, position);
385
+ return this.items.filter((item) => item.position === position);
148
386
  },
149
387
  timedItemsAt(position) {
150
- return timedStackAt(this.items, position);
388
+ return this.itemsAt(position).filter((item) => shouldAutoDismiss(item.duration));
151
389
  },
152
390
  persistentItemsAt(position) {
153
- return persistentStackAt(this.items, position);
391
+ return this.itemsAt(position).filter((item) => isPersistentDuration(item.duration));
154
392
  },
155
393
  activeTimedItemsAt(position) {
156
394
  return this.timedItemsAt(position).filter((item) => !item.removed);
@@ -174,167 +412,90 @@ function createToastStore(options = {}) {
174
412
  }
175
413
  }
176
414
  return rank <= this.maxVisible;
177
- },
178
- push(payload = {}) {
179
- const position = payload.position ?? this.defaultPosition;
180
- const id = createToastId();
181
- const toast = {
182
- id,
183
- key: payload.key ?? null,
184
- content: payload.content ?? null,
185
- title: payload.title ?? null,
186
- description: payload.description ?? null,
187
- variant: payload.variant ?? "default",
188
- position,
189
- duration: resolveToastDuration(payload.duration, defaultDuration),
190
- action: payload.action ?? null,
191
- removed: false
192
- };
193
- this.items = [toast, ...this.items];
194
- enforcePositionLimit(
195
- this.items,
196
- position,
197
- this.maxToasts,
198
- (ids) => markRemoved(ids),
199
- shouldAutoDismiss(toast.duration) ? "timed" : "persistent"
200
- );
201
- scheduleDismiss(id, toast.duration);
202
- return id;
203
- },
204
- pushUnique(key, payload = {}) {
205
- const activeIds = this.items.filter((item) => !item.removed && item.key === key).map((item) => item.id);
206
- markRemoved(activeIds);
207
- return this.push({ ...payload, key });
208
- },
209
- update(id, payload = {}) {
210
- const current = this.items.find((toast) => toast.id === id);
211
- if (!current) {
212
- return;
213
- }
214
- const previousPosition = current.position;
215
- const wasPersistent = isPersistentDuration(current.duration);
216
- this.items = this.items.map(
217
- (item) => item.id === id ? applyToastPatch(item, payload) : item
218
- );
219
- const updated = this.items.find((toast) => toast.id === id);
220
- if (!updated) {
221
- return;
222
- }
223
- const nextPosition = updated.position;
224
- const isNowPersistent = isPersistentDuration(updated.duration);
225
- const durationStackChanged = payload.duration !== void 0 && wasPersistent !== isNowPersistent;
226
- const positionChanged = nextPosition !== previousPosition;
227
- if (positionChanged) {
228
- enforcePositionLimit(
229
- this.items,
230
- previousPosition,
231
- this.maxToasts,
232
- (ids) => markRemoved(ids),
233
- "timed"
234
- );
235
- enforcePositionLimit(
236
- this.items,
237
- previousPosition,
238
- this.maxToasts,
239
- (ids) => markRemoved(ids),
240
- "persistent"
241
- );
242
- }
243
- if (positionChanged || durationStackChanged) {
244
- enforcePositionLimit(
245
- this.items,
246
- nextPosition,
247
- this.maxToasts,
248
- (ids) => markRemoved(ids),
249
- "timed"
250
- );
251
- enforcePositionLimit(
252
- this.items,
253
- nextPosition,
254
- this.maxToasts,
255
- (ids) => markRemoved(ids),
256
- "persistent"
257
- );
258
- }
259
- if (payload.duration !== void 0) {
260
- scheduleDismiss(id, updated.duration);
261
- }
262
- },
263
- dismiss(id) {
264
- markRemoved([id]);
265
- },
266
- dismissAt(position) {
267
- const ids = this.itemsAt(position).filter((item) => !item.removed).map((item) => item.id);
268
- markRemoved(ids);
269
- },
270
- dismissAll() {
271
- const ids = this.items.filter((item) => !item.removed).map((item) => item.id);
272
- markRemoved(ids);
273
- },
274
- destroy() {
275
- for (const timer of dismissTimers.values()) {
276
- clearTimeout(timer);
277
- }
278
- dismissTimers.clear();
279
- for (const timer of purgeTimers.values()) {
280
- clearTimeout(timer);
281
- }
282
- purgeTimers.clear();
283
415
  }
284
416
  };
285
- function clearDismissTimer(id) {
286
- const timer = dismissTimers.get(id);
287
- if (timer !== void 0) {
288
- clearTimeout(timer);
289
- dismissTimers.delete(id);
290
- }
417
+ const unsubscribe = controller.on("change", (detail) => {
418
+ store.items = [...detail.items];
419
+ });
420
+ return store;
421
+ }
422
+ function createToastStore(options = {}) {
423
+ const controller = new ToastController(options);
424
+ controller.mount();
425
+ return wrapToastStore(controller);
426
+ }
427
+ var PROMISE_LOADING_DURATION = 36e5;
428
+ function normalizeToastDuration(duration) {
429
+ if (duration === 0) {
430
+ return false;
291
431
  }
292
- function markRemoved(ids) {
293
- if (ids.length === 0) {
294
- return;
295
- }
296
- const activeStore = getStore?.() ?? store;
297
- const idsToRemove = [];
298
- for (const id of ids) {
299
- const toast = activeStore.items.find((item) => item.id === id);
300
- if (!toast || toast.removed) {
301
- continue;
302
- }
303
- clearDismissTimer(id);
304
- idsToRemove.push(id);
305
- }
306
- if (idsToRemove.length === 0) {
307
- return;
308
- }
309
- const removeSet = new Set(idsToRemove);
310
- activeStore.items = activeStore.items.map(
311
- (item) => removeSet.has(item.id) ? { ...item, removed: true } : item
312
- );
313
- const batchId = createToastId();
314
- const timer = setTimeout(() => {
315
- purgeTimers.delete(batchId);
316
- const storeForFilter = getStore?.() ?? store;
317
- storeForFilter.items = storeForFilter.items.filter((item) => !removeSet.has(item.id));
318
- }, dismissDelayMs);
319
- purgeTimers.set(batchId, timer);
320
- }
321
- function scheduleDismiss(id, duration) {
322
- clearDismissTimer(id);
323
- if (duration === PROMISE_LOADING_DURATION) {
324
- return;
325
- }
326
- if (shouldAutoDismiss(duration)) {
327
- const timer = setTimeout(() => {
328
- dismissTimers.delete(id);
329
- (getStore?.() ?? store).dismiss(id);
330
- }, duration);
331
- dismissTimers.set(id, timer);
432
+ return duration;
433
+ }
434
+ function resolveToastDuration(duration, defaultDuration) {
435
+ if (duration === void 0) {
436
+ return normalizeToastDuration(defaultDuration);
437
+ }
438
+ return normalizeToastDuration(duration);
439
+ }
440
+ function shouldAutoDismiss(duration) {
441
+ return typeof duration === "number" && duration > 0 && Number.isFinite(duration);
442
+ }
443
+ function isPersistentDuration(duration) {
444
+ return !shouldAutoDismiss(duration);
445
+ }
446
+ function resolveToastLimits(options = {}) {
447
+ const maxToasts = options.maxToasts ?? DEFAULT_MAX_TOASTS;
448
+ let maxVisible = options.maxVisible ?? maxToasts;
449
+ if (maxToasts > 0 && maxVisible > maxToasts) {
450
+ maxVisible = maxToasts;
451
+ }
452
+ return { maxToasts, maxVisible };
453
+ }
454
+ function resolveStackPositions(defaultPosition, positions) {
455
+ const stacks = [defaultPosition];
456
+ for (const position of positions ?? []) {
457
+ if (!stacks.includes(position)) {
458
+ stacks.push(position);
332
459
  }
333
460
  }
334
- return store;
461
+ return stacks;
462
+ }
463
+ function applyToastPatch(item, payload) {
464
+ let next = { ...item };
465
+ if (payload.content !== void 0) {
466
+ next = { ...next, content: payload.content };
467
+ }
468
+ if (payload.title !== void 0) {
469
+ next = { ...next, title: payload.title };
470
+ }
471
+ if (payload.description !== void 0) {
472
+ next = { ...next, description: payload.description };
473
+ }
474
+ if (payload.variant !== void 0) {
475
+ next = { ...next, variant: payload.variant };
476
+ }
477
+ if (payload.position !== void 0) {
478
+ next = { ...next, position: payload.position };
479
+ }
480
+ if (payload.duration !== void 0) {
481
+ next = { ...next, duration: normalizeToastDuration(payload.duration) };
482
+ }
483
+ if (payload.action !== void 0) {
484
+ next = { ...next, action: payload.action };
485
+ }
486
+ if (payload.key !== void 0) {
487
+ next = { ...next, key: payload.key };
488
+ }
489
+ return next;
335
490
  }
336
491
 
337
- // src/magic.ts
492
+ // src/plugin.ts
493
+ var DEFAULT_PLUGIN_OPTIONS = {
494
+ defaultPosition: "bottom-right",
495
+ defaultDuration: 4e3,
496
+ maxToasts: 5,
497
+ listenToWindowEvents: true
498
+ };
338
499
  var RESERVED_TOAST_MAGIC_KEYS = /* @__PURE__ */ new Set([
339
500
  "dismiss",
340
501
  "update",
@@ -344,67 +505,79 @@ var RESERVED_TOAST_MAGIC_KEYS = /* @__PURE__ */ new Set([
344
505
  "fromPayload",
345
506
  "promise"
346
507
  ]);
347
- function pushToast(store, titleOrPayload, options = {}) {
348
- if (typeof titleOrPayload === "string") {
349
- return store.push({
350
- title: titleOrPayload,
351
- ...options
352
- });
353
- }
354
- return store.push(titleOrPayload);
355
- }
356
- function pushVariantToast(store, variant, titleOrPayload, options = {}) {
357
- if (typeof titleOrPayload === "string") {
358
- return store.push({
359
- title: titleOrPayload,
360
- ...options,
361
- variant
362
- });
508
+ function variantFromList(variants, preferred) {
509
+ if (variants.includes(preferred)) {
510
+ return preferred;
363
511
  }
364
- return store.push({
365
- ...titleOrPayload,
366
- ...options,
367
- variant
368
- });
512
+ return "default";
369
513
  }
370
- function resolveSuccessTitle(data, messages) {
371
- if (typeof messages.success === "function") {
372
- return messages.success(data);
373
- }
374
- return messages.success;
514
+ function resolvePromiseConfig(variants, promise = {}) {
515
+ return {
516
+ loading: promise.loading ?? "Loading...",
517
+ error: promise.error ?? "Error",
518
+ duration: promise.duration ?? 4e3,
519
+ loadingVariant: promise.loadingVariant ?? variantFromList(variants, "loading"),
520
+ successVariant: promise.successVariant ?? variantFromList(variants, "success"),
521
+ errorVariant: promise.errorVariant ?? variantFromList(variants, "error")
522
+ };
375
523
  }
376
- function resolveSuccessContent(data, messages) {
377
- const { successContent } = messages;
378
- if (typeof successContent === "function") {
379
- return successContent(data);
524
+ function resolveToastPluginConfig(options = {}) {
525
+ const variants = options.variants ?? [];
526
+ const positions = options.positions ?? [];
527
+ const maxToasts = options.maxToasts ?? DEFAULT_PLUGIN_OPTIONS.maxToasts;
528
+ let maxVisible = options.maxVisible ?? maxToasts;
529
+ if (maxToasts > 0 && maxVisible > maxToasts) {
530
+ maxVisible = maxToasts;
380
531
  }
381
- return successContent;
532
+ return {
533
+ defaultPosition: options.defaultPosition ?? DEFAULT_PLUGIN_OPTIONS.defaultPosition,
534
+ defaultDuration: options.defaultDuration ?? DEFAULT_PLUGIN_OPTIONS.defaultDuration,
535
+ maxToasts,
536
+ maxVisible,
537
+ listenToWindowEvents: options.listenToWindowEvents ?? DEFAULT_PLUGIN_OPTIONS.listenToWindowEvents,
538
+ storeKey: options.storeKey ?? TOAST_STORE_KEY,
539
+ variants,
540
+ positions,
541
+ promise: resolvePromiseConfig(variants, options.promise)
542
+ };
382
543
  }
383
- function resolveErrorTitle(messages, fallbackError) {
384
- return messages.error ?? fallbackError;
544
+ function toastOptions(options) {
545
+ return options;
385
546
  }
386
- function resolveToastPromise(factoryOrPromise) {
387
- if (typeof factoryOrPromise === "function") {
388
- return Promise.resolve(factoryOrPromise());
389
- }
390
- return Promise.resolve(factoryOrPromise);
547
+ function toastVariants(variants) {
548
+ return variants;
391
549
  }
392
- function buildPromiseSuccessPatch(data, messages, successVariant, settledDuration) {
393
- const successTitle = resolveSuccessTitle(data, messages);
394
- const successContent = resolveSuccessContent(data, messages);
395
- return {
396
- ...successTitle !== void 0 ? { title: successTitle } : {},
397
- ...successContent !== void 0 ? { content: successContent } : {},
398
- variant: successVariant,
399
- duration: settledDuration
400
- };
550
+ function toastPositions(positions) {
551
+ return positions;
401
552
  }
402
- function buildPromiseErrorPatch(messages, promiseConfig, errorVariant, settledDuration) {
403
- return {
404
- title: resolveErrorTitle(messages, promiseConfig.error),
405
- ...messages.errorContent !== void 0 ? { content: messages.errorContent } : {},
406
- variant: errorVariant,
407
- duration: settledDuration
553
+ function toastPlugin(options = {}) {
554
+ return function registerToast(alpine) {
555
+ const Alpine = alpine;
556
+ const controllerOptions = {
557
+ defaultPosition: options.defaultPosition,
558
+ positions: options.positions,
559
+ defaultDuration: options.defaultDuration,
560
+ maxToasts: options.maxToasts,
561
+ maxVisible: options.maxVisible,
562
+ listenToWindowEvents: options.listenToWindowEvents,
563
+ storeKey: options.storeKey
564
+ };
565
+ const controller = createToastController(controllerOptions);
566
+ const config = resolveToastPluginConfig(options);
567
+ const store = wrapToastStore(controller);
568
+ Alpine.store(config.storeKey, store);
569
+ const reactiveStore = Alpine.store(config.storeKey);
570
+ controller.on("change", (detail) => {
571
+ reactiveStore.items = [...detail.items];
572
+ });
573
+ const toast = createToastMagic(
574
+ config,
575
+ () => reactiveStore
576
+ );
577
+ Alpine.magic("toast", () => toast);
578
+ if (typeof Alpine.cleanup === "function") {
579
+ Alpine.cleanup(() => controller.destroy());
580
+ }
408
581
  };
409
582
  }
410
583
  function createToastMagic(config, getStore) {
@@ -460,69 +633,75 @@ function createToastMagic(config, getStore) {
460
633
  }
461
634
  return magic;
462
635
  }
463
-
464
- // src/index.ts
465
- function registerAlpineToastStore(Alpine, storeKey, store) {
466
- const registerStore = Alpine.store;
467
- registerStore(storeKey, store);
468
- }
469
- function getToastStore(Alpine, storeKey) {
470
- return Alpine.store(storeKey);
636
+ function pushToast(store, titleOrPayload, options = {}) {
637
+ if (typeof titleOrPayload === "string") {
638
+ return store.push({
639
+ title: titleOrPayload,
640
+ ...options
641
+ });
642
+ }
643
+ return store.push(titleOrPayload);
471
644
  }
472
- function registerToastPlugin(Alpine, options = {}) {
473
- const config = resolveToastPluginConfig(options);
474
- let reactiveStore;
475
- let windowEventsAbort;
476
- const store = createToastStore({
477
- defaultPosition: config.defaultPosition,
478
- positions: config.positions,
479
- defaultDuration: config.defaultDuration,
480
- maxToasts: config.maxToasts,
481
- maxVisible: config.maxVisible,
482
- getStore: () => reactiveStore ?? store
645
+ function pushVariantToast(store, variant, titleOrPayload, options = {}) {
646
+ if (typeof titleOrPayload === "string") {
647
+ return store.push({
648
+ title: titleOrPayload,
649
+ ...options,
650
+ variant
651
+ });
652
+ }
653
+ return store.push({
654
+ ...titleOrPayload,
655
+ ...options,
656
+ variant
483
657
  });
484
- const baseDestroy = store.destroy.bind(store);
485
- store.destroy = () => {
486
- windowEventsAbort?.abort();
487
- windowEventsAbort = void 0;
488
- baseDestroy();
489
- };
490
- registerAlpineToastStore(Alpine, config.storeKey, store);
491
- reactiveStore = getToastStore(Alpine, config.storeKey);
492
- const toast = createToastMagic(
493
- config,
494
- () => getToastStore(Alpine, config.storeKey)
495
- );
496
- Alpine.magic("toast", () => toast);
497
- if (config.listenToWindowEvents && typeof window !== "undefined") {
498
- windowEventsAbort = new AbortController();
499
- window.addEventListener(
500
- "toast",
501
- (event) => {
502
- if (event instanceof CustomEvent) {
503
- toast.fromPayload(
504
- event.detail ?? {}
505
- );
506
- }
507
- },
508
- { signal: windowEventsAbort.signal }
509
- );
658
+ }
659
+ function resolveSuccessTitle(data, messages) {
660
+ if (typeof messages.success === "function") {
661
+ return messages.success(data);
510
662
  }
663
+ return messages.success;
511
664
  }
512
- function toastPlugin(optionsOrAlpine) {
513
- if (optionsOrAlpine && typeof optionsOrAlpine.magic === "function") {
514
- registerToastPlugin(optionsOrAlpine, {});
515
- return;
516
- }
517
- const options = optionsOrAlpine ?? {};
518
- return (Alpine) => {
519
- registerToastPlugin(Alpine, options);
665
+ function resolveSuccessContent(data, messages) {
666
+ const { successContent } = messages;
667
+ if (typeof successContent === "function") {
668
+ return successContent(data);
669
+ }
670
+ return successContent;
671
+ }
672
+ function resolveErrorTitle(messages, fallbackError) {
673
+ return messages.error ?? fallbackError;
674
+ }
675
+ function resolveToastPromise(factoryOrPromise) {
676
+ if (typeof factoryOrPromise === "function") {
677
+ return Promise.resolve(factoryOrPromise());
678
+ }
679
+ return Promise.resolve(factoryOrPromise);
680
+ }
681
+ function buildPromiseSuccessPatch(data, messages, successVariant, settledDuration) {
682
+ const successTitle = resolveSuccessTitle(data, messages);
683
+ const successContent = resolveSuccessContent(data, messages);
684
+ return {
685
+ ...successTitle !== void 0 ? { title: successTitle } : {},
686
+ ...successContent !== void 0 ? { content: successContent } : {},
687
+ variant: successVariant,
688
+ duration: settledDuration
689
+ };
690
+ }
691
+ function buildPromiseErrorPatch(messages, promiseConfig, errorVariant, settledDuration) {
692
+ return {
693
+ title: resolveErrorTitle(messages, promiseConfig.error),
694
+ ...messages.errorContent !== void 0 ? { content: messages.errorContent } : {},
695
+ variant: errorVariant,
696
+ duration: settledDuration
520
697
  };
521
698
  }
522
699
  export {
523
700
  PROMISE_LOADING_DURATION,
524
701
  RESERVED_TOAST_MAGIC_KEYS,
525
702
  TOAST_STORE_KEY,
703
+ ToastController,
704
+ createToastController,
526
705
  createToastMagic,
527
706
  createToastStore,
528
707
  toastPlugin as default,
@@ -534,7 +713,8 @@ export {
534
713
  resolveToastPluginConfig,
535
714
  shouldAutoDismiss,
536
715
  toastOptions,
716
+ toastPlugin,
537
717
  toastPositions,
538
- toastVariants
718
+ toastVariants,
719
+ wrapToastStore
539
720
  };
540
- //# sourceMappingURL=index.js.map