@ailuracode/alpine-toast 1.0.0 → 2.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/README.md CHANGED
@@ -1,26 +1,22 @@
1
1
  # @ailuracode/alpine-toast
2
2
 
3
- Headless in-app toast queue for Alpine.js via the `$toast` magic.
4
-
5
- **CSS-framework agnostic** — no markup, no styles. Only `default`, `bottom-right`, and `promise` are built into the API; you declare variants and positions your UI needs.
6
-
7
- **[Full documentation →](../../docs/plugins/toast.md)**
3
+ Headless in-app toast queue for Alpine.js. Registers the `$toast` magic and an internal reactive store for UI integrators.
8
4
 
9
5
  ## Install
10
6
 
11
7
  ```bash
12
- pnpm add @ailuracode/alpine-toast alpinejs
8
+ pnpm add @ailuracode/alpine-toast @ailuracode/alpine-core alpinejs
13
9
  ```
14
10
 
15
- ## Quick example
11
+ ## Quick start
16
12
 
17
- ```ts
13
+ ```js
18
14
  import Alpine from "alpinejs";
19
- import { toastPlugin, toastVariants, toastPositions } from "@ailuracode/alpine-toast";
15
+ import toast, { toastOptions, toastPositions, toastVariants } from "@ailuracode/alpine-toast";
20
16
 
21
17
  Alpine.plugin(
22
- toastPlugin({
23
- variants: toastVariants(["success", "error", "loading"] as const),
18
+ toastOptions({
19
+ variants: toastVariants(["success", "info", "warning", "error", "loading"] as const),
24
20
  positions: toastPositions(["top-center", "bottom-right"] as const),
25
21
  defaultPosition: "bottom-right",
26
22
  promise: {
@@ -33,82 +29,296 @@ Alpine.plugin(
33
29
  Alpine.start();
34
30
  ```
35
31
 
36
- ```html
37
- <button @click="$toast('Hello')">Default</button>
38
- <button @click="$toast.success('Saved')">Success</button>
39
- <button @click="$toast('Notice', { position: 'top-center' })">Top</button>
40
- <button @click="() => $toast.promise(save, { loading: 'Saving...', success: 'Saved!' })">
41
- Save
42
- </button>
32
+ `toastOptions()`, `toastVariants()`, and `toastPositions()` preserve literal types for strongly typed payloads and `$toast.<variant>()` shortcuts.
33
+
34
+ ## CSS-framework agnostic
35
+
36
+ This plugin ships **no HTML, no CSS, and no design tokens**. Variant and position names are **not hardcoded** — you declare the sets your UI needs. The only built-in concepts are:
37
+
38
+ - **`default`** — `$toast('Message')` or `{ variant: 'default' }`
39
+ - **`bottom-right`** — default `position` when omitted
40
+ - **`promise`** — `$toast.promise(factoryOrPromise, messages?)`
41
+
42
+ Map `toast.variant` and `toast.position` to layout/CSS in your own renderer (e.g. `data-position`, Tailwind classes, coordinates).
43
+
44
+ ## Magic API
45
+
46
+ ```js
47
+ $toast("Hello")
48
+ $toast({ title: "Saved", variant: "success", position: "top-center" })
49
+ $toast.success("Saved") // only when "success" is in `variants`
50
+ $toast.dismiss(id)
51
+ $toast.dismissAt("top-center")
52
+ $toast.dismissAll()
53
+ $toast.fromPayload({ title: "Done", variant: "success" })
54
+ await $toast.promise(() => save(), {
55
+ loading: "Saving...",
56
+ success: "Saved",
57
+ error: "Could not save",
58
+ })
59
+ ```
60
+
61
+ | Method | Description |
62
+ |--------|-------------|
63
+ | `$toast(title, options?)` | Push a `default` toast. Returns the toast id. |
64
+ | `$toast(payload)` | Push with a full payload object. |
65
+ | `$toast.<variant>(title, options?)` | One shortcut per entry in `variants`. |
66
+ | `$toast.dismiss(id)` | Close one toast by id (returned from `$toast()`). |
67
+ | `$toast.update(id, patch)` | Patch the same toast in place (variant, title, content, action, …). |
68
+ | `$toast.dismissAt(position)` | Close every toast in one position stack. |
69
+ | `$toast.dismissAll()` | Close every toast in every stack. |
70
+ | `$toast.pushUnique(key, payload?)` | Dismiss active toasts with the same `key`, then push. |
71
+ | `$toast.fromPayload(payload)` | Push from a plain payload (events, session flash, etc.). |
72
+ | `$toast.promise(factoryOrPromise, messages?)` | `loading` → `success` / `error` on the same toast. |
73
+
74
+ ### Payload options
75
+
76
+ | Field | Type | Default |
77
+ |-------|------|---------|
78
+ | `content` | `TContent \| null` | `null` |
79
+ | `title` | `string \| null` | `null` |
80
+ | `description` | `string \| null` | `null` |
81
+ | `variant` | `default \| …your variants` | `default` |
82
+ | `position` | `bottom-right \| …your positions` | `bottom-right` (or `defaultPosition`) |
83
+ | `duration` | `number` (ms) | `4000` (`false` or `0` = no auto-dismiss; stored as `false`) |
84
+ | `action` | `{ label, onClick? }` | `null` |
85
+ | `key` | `string \| null` | `null` — use with `pushUnique` for single-slot toasts |
86
+
87
+ `title` / `description` are optional string shorthands. Use `content` for any shape your renderer understands (objects, arrays, HTML snippets, etc.). The plugin stores it as-is — rendering is up to your UI.
88
+
89
+ ```ts
90
+ type AppToastContent = { user: { name: string; avatar: string } } | { html: string };
91
+
92
+ type Item = ToastItem<typeof variants, typeof positions, AppToastContent>;
93
+
94
+ $toast({
95
+ content: { user: { name: "Ada", avatar: "/ada.png" } },
96
+ variant: "success",
97
+ });
43
98
  ```
44
99
 
45
100
  ```html
46
- <button @click="$toast.dismiss(toast.id)" aria-label="Close">✕</button>
101
+ <template x-for="toast in $store.toast.itemsAt(position)" :key="toast.id">
102
+ <div x-show="toast.content?.user">
103
+ <img :src="toast.content.user.avatar" alt="" />
104
+ <span x-text="toast.content.user.name"></span>
105
+ </div>
106
+ <p x-show="toast.title" x-text="toast.title"></p>
107
+ </template>
47
108
  ```
48
109
 
49
- Also available on the store: `$store.toast.dismiss(id)`.
110
+ For `$toast.promise`, optional `loadingContent`, `successContent`, and `errorContent` update the same toast item.
50
111
 
51
- ## API summary
112
+ ## Custom variants
52
113
 
53
- | | |
54
- |-|-|
55
- | **Content** | `content` on each toast — any type your UI renders |
56
- | **Magic** | `$toast(title, options?)` toast id (`default` variant, `bottom-right` position) |
57
- | **Close** | `$toast.dismiss(id)`, `$toast.dismissAt(position)`, `$toast.dismissAll()` |
58
- | **Dedupe** | `$toast.pushUnique(key, payload)` — one active toast per key (undo flows) |
59
- | **Promise** | `$toast.promise(factoryOrPromise, messages?)` |
60
- | **Variants** | `variants: toastVariants([...])` → `$toast.<name>()` |
61
- | **Positions** | `positions: toastPositions([...])` → one stack per position |
62
- | **Queue** | `maxToasts` / `maxVisible` per timed or persistent stack |
63
- | **UI** | Bring your own markup and CSS |
114
+ ```ts
115
+ import toast, { toastOptions, toastVariants, type ToastMagic } from "@ailuracode/alpine-toast";
116
+
117
+ const variants = toastVariants(["queued", "published", "failed"] as const);
64
118
 
65
- ## Standalone usage (no Alpine)
119
+ Alpine.plugin(toast({ variants }));
120
+
121
+ type AppToast = ToastMagic<typeof variants>;
122
+ // AppToast has .queued(), .published(), .failed()
123
+ ```
124
+
125
+ Without `variants`, only `$toast()`, `$toast.promise()`, `$toast.dismiss()`, and `$toast.fromPayload()` are available.
126
+
127
+ ## Custom positions
128
+
129
+ Each declared position gets its **own stack**. `maxToasts` and `maxVisible` apply **per position**, not globally.
66
130
 
67
131
  ```ts
68
- import { createToastController } from "@ailuracode/alpine-toast";
132
+ import toast, { toastPositions, type ToastPosition } from "@ailuracode/alpine-toast";
133
+
134
+ const positions = toastPositions(["top-center", "bottom-right"] as const);
135
+
136
+ Alpine.plugin(
137
+ toast({
138
+ positions,
139
+ defaultPosition: "bottom-right",
140
+ })
141
+ );
142
+
143
+ type AppPosition = ToastPosition<typeof positions>;
144
+ // "bottom-right" | "top-center"
145
+ ```
69
146
 
70
- const controller = createToastController({
71
- maxToasts: 5,
72
- maxVisible: 3,
147
+ The plugin stores the position id on each toast. Render one stack per position in your UI:
148
+
149
+ ```html
150
+ <template x-for="position in $store.toast.stackPositions" :key="position">
151
+ <div
152
+ x-bind:data-position="position"
153
+ x-bind:class="{
154
+ 'fixed top-4 left-1/2 -translate-x-1/2': position === 'top-center',
155
+ 'fixed bottom-4 right-4': position === 'bottom-right',
156
+ }"
157
+ >
158
+ <template x-for="(toast, index) in $store.toast.itemsAt(position)" :key="toast.id">
159
+ <div x-show="!toast.removed && $store.toast.isVisibleAt(position, index)">
160
+ <p x-text="toast.title"></p>
161
+ </div>
162
+ </template>
163
+ </div>
164
+ </template>
165
+ ```
166
+
167
+ | Store API | Description |
168
+ |-----------|-------------|
169
+ | `stackPositions` | All stack keys (`defaultPosition` + `positions`) |
170
+ | `itemsAt(position)` | Toasts in that stack, newest first (includes exiting `removed` items) |
171
+ | `timedItemsAt(position)` | Auto-dismiss stack — same order, includes exiting items |
172
+ | `persistentItemsAt(position)` | Persistent stack (`duration: false`) — includes exiting items |
173
+ | `activeTimedItemsAt(position)` | Timed stack without `removed` items — preferred for simple `x-for` |
174
+ | `activePersistentItemsAt(position)` | Persistent stack without `removed` items |
175
+ | `isVisibleAt(position, index)` | Timed stack only — peek/limit visibility (`maxVisible`) |
176
+ | `pushUnique(key, payload?)` | Same as `$toast.pushUnique` |
177
+ | `destroy()` | Clear timers — call when tearing down the plugin |
178
+ | `dismiss(id)` | Close one toast (same as `$toast.dismiss`) |
179
+ | `dismissAt(position)` | Close one entire position (timed + persistent) |
180
+ | `dismissAll()` | Close all stacks |
181
+
182
+ ## Promise flow
183
+
184
+ Configure default promise variants in plugin options, or override per call:
185
+
186
+ ```js
187
+ Alpine.plugin(
188
+ toast({
189
+ variants: toastVariants(["loading", "success", "error"] as const),
190
+ promise: {
191
+ loading: "Loading...",
192
+ error: "Something went wrong",
193
+ loadingVariant: "loading",
194
+ successVariant: "success",
195
+ errorVariant: "error",
196
+ duration: 4000,
197
+ },
198
+ })
199
+ );
200
+ ```
201
+
202
+ ```js
203
+ await $toast.promise(() => save(), {
204
+ loading: "Saving...",
205
+ success: (data) => `Saved ${data.id}`,
206
+ error: "Could not save",
73
207
  });
208
+ ```
209
+
210
+ If a named variant is missing from `variants`, promise states fall back to `default`.
211
+
212
+ On failure, the toast updates to the error state and the returned promise **rejects** with the original error so callers can still use `try/catch` or `.catch()`.
213
+
214
+ The **loading** state uses a long timed duration (`PROMISE_LOADING_DURATION`) so it stays in the **timed stack** (not perpetual). The timer is replaced when the promise settles to success or error.
215
+
216
+ Reserved variant names (`dismiss`, `update`, `dismissAt`, `dismissAll`, `fromPayload`, `promise`) cannot override core `$toast` methods.
217
+
218
+ In Alpine templates, wrap multi-argument calls in an arrow function:
219
+
220
+ ```html
221
+ <button
222
+ @click="() => $toast.promise(() => save(), { loading: 'Saving...', success: 'Saved!' })"
223
+ >
224
+ Save
225
+ </button>
226
+ ```
227
+
228
+ ## Queue limits
229
+
230
+ Each **position** has two independent stacks:
231
+
232
+ 1. **Timed** — auto-dismiss toasts (`duration > 0`). `maxVisible` applies here (peek/stack UI).
233
+ 2. **Persistent** — `duration: false` (or `0` in payloads; normalized to `false`). Always fully visible in your UI.
234
+
235
+ `maxToasts` applies **per stack per position** (not globally). Example: `maxToasts: 5` allows up to 5 timed + 5 persistent toasts at `bottom-right`.
236
+
237
+ | Option | Default | Description |
238
+ |--------|---------|-------------|
239
+ | `maxToasts` | `5` | Maximum active toasts **per stack per position**. `0` = unlimited. |
240
+ | `maxVisible` | `maxToasts` | Maximum timed toasts shown at once per position (`isVisibleAt`). |
241
+
242
+ Use `activeTimedItemsAt` / `activePersistentItemsAt` when your renderer does not need exiting (`removed`) items. Keep `timedItemsAt` / `persistentItemsAt` when you animate dismiss (Sonner-style).
243
+
244
+ ## Window events
245
+
246
+ ```js
247
+ window.dispatchEvent(
248
+ new CustomEvent("toast", {
249
+ detail: { title: "From anywhere", variant: "success", position: "top-center" },
250
+ })
251
+ );
252
+ ```
74
253
 
75
- controller.push({ content: "Hello!", variant: "default", position: "bottom-right" });
76
- controller.dismissAll();
77
- controller.destroy();
254
+ Disable with `toast({ listenToWindowEvents: false })`.
255
+
256
+ ## Rendering UI
257
+
258
+ ```html
259
+ <template x-for="position in $store.toast.stackPositions" :key="position">
260
+ <div
261
+ role="region"
262
+ x-bind:aria-label="'Toasts ' + position"
263
+ x-bind:data-position="position"
264
+ >
265
+ <template x-for="(toast, index) in $store.toast.itemsAt(position)" :key="toast.id">
266
+ <div
267
+ role="status"
268
+ x-show="!toast.removed && $store.toast.isVisibleAt(position, index)"
269
+ x-bind:data-variant="toast.variant"
270
+ >
271
+ <p x-text="toast.title"></p>
272
+ </div>
273
+ </template>
274
+ </div>
275
+ </template>
78
276
  ```
79
277
 
278
+ Style `[data-variant="…"]` and `[data-position="…"]` in your own CSS or component library.
279
+
80
280
  ## Plugin options
81
281
 
82
- ```ts
83
- toastPlugin({
84
- variants?: string[], // toastVariants([...])
85
- positions?: string[], // toastPositions([...])
86
- defaultPosition?: string, // default: "bottom-right"
87
- defaultDuration?: number, // default: 5000 (ms), false for persistent
88
- maxToasts?: number, // max toasts per position
89
- maxVisible?: number, // max visible per position
90
- promise?: {
91
- loadingVariant?: string,
92
- successVariant?: string,
93
- errorVariant?: string,
94
- loadingMessages?: string[],
95
- },
96
- onDismiss?: (id: string) => void,
97
- onChange?: (detail: ToastChangeDetail) => void,
98
- });
282
+ ```js
283
+ Alpine.plugin(
284
+ toast({
285
+ variants: toastVariants(["success", "error"] as const),
286
+ positions: toastPositions(["top-center", "bottom-right"] as const),
287
+ defaultPosition: "bottom-right",
288
+ defaultDuration: 5000,
289
+ maxToasts: 5,
290
+ maxVisible: 3,
291
+ listenToWindowEvents: true,
292
+ storeKey: "toast",
293
+ magicKey: "toast",
294
+ promise: {
295
+ loadingVariant: "success",
296
+ successVariant: "success",
297
+ errorVariant: "error",
298
+ },
299
+ })
300
+ );
99
301
  ```
100
302
 
101
- ## Store API
303
+ ### Avoiding name collisions
304
+
305
+ If your application already owns a `$toast` store or magic — or another toolkit plugin registers on that name — rename the integration surface without touching the controller:
102
306
 
103
307
  ```ts
104
- $store.toast.items // active toast items
105
- $store.toast.length // total count
106
- $store.toast.push(payload) // add a toast, returns id
107
- $store.toast.dismiss(id) // remove by id
108
- $store.toast.dismissAt(position) // remove all at position
109
- $store.toast.dismissAll() // remove all
308
+ Alpine.plugin(
309
+ toast({
310
+ storeKey: "alerts", // $store.alerts
311
+ magicKey: "snack", // $snack
312
+ }),
313
+ );
110
314
  ```
111
315
 
316
+ The exposed constants `TOAST_STORE_KEY` and `TOAST_MAGIC_KEY` keep the renames discoverable from TypeScript.
317
+
318
+ ## Related packages
319
+
320
+ - [`@ailuracode/alpine-notify`](./notify.md) — OS-level Web Notifications (`$notify`)
321
+
112
322
  ## License
113
323
 
114
324
  MIT
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Alpine, Unsubscribe, PluginCallback, BaseController } from '@ailuracode/alpine-core';
1
+ import { SingletonScope, Alpine, Unsubscribe, PluginCallback, BaseController } from '@ailuracode/alpine-core';
2
2
  export { Unsubscribe } from '@ailuracode/alpine-core';
3
3
  import { Alpine as Alpine$1 } from 'alpinejs';
4
4
 
@@ -13,7 +13,10 @@ import { Alpine as Alpine$1 } from 'alpinejs';
13
13
 
14
14
  /** Built-in Alpine store key for the toast queue. */
15
15
  declare const TOAST_STORE_KEY: "toast";
16
+ /** Built-in `$toast` magic key registered by {@link toastPlugin}. */
17
+ declare const TOAST_MAGIC_KEY: "toast";
16
18
  type ToastStoreKey = typeof TOAST_STORE_KEY;
19
+ type ToastMagicKey = typeof TOAST_MAGIC_KEY;
17
20
  /** Built-in position used when none is provided. Map to CSS in your UI layer. */
18
21
  type DefaultToastPosition = "bottom-right";
19
22
  /** Union of `bottom-right` plus developer-defined positions from plugin options. */
@@ -87,6 +90,14 @@ interface ToastPluginOptions<TVariants extends readonly string[] = readonly [],
87
90
  listenToWindowEvents?: boolean;
88
91
  /** Internal store key. Default: `"toast"`. */
89
92
  storeKey?: ToastStoreKey;
93
+ /**
94
+ * `$toast` magic key the Alpine plugin registers under. Defaults to
95
+ * {@link TOAST_MAGIC_KEY}. Set when the host already owns a `toast`
96
+ * magic or another toolkit plugin would collide on that name — the
97
+ * rename avoids the collision without touching the controller.
98
+ * Ignored by the standalone `createToastController` helper.
99
+ */
100
+ magicKey?: string;
90
101
  }
91
102
  interface ToastPromiseMessages<T = unknown, TVariant extends string = string, TContent = unknown> {
92
103
  loading?: string;
@@ -166,6 +177,7 @@ type ResolvedToastPluginConfig<TVariants extends readonly string[] = readonly []
166
177
  maxVisible: number;
167
178
  listenToWindowEvents: boolean;
168
179
  storeKey: ToastStoreKey;
180
+ magicKey: string;
169
181
  variants: TVariants;
170
182
  positions: TPositions;
171
183
  promise: ResolvedPromiseConfig<TVariants>;
@@ -220,6 +232,12 @@ interface CreateToastControllerOptions<TPositions extends readonly string[] = re
220
232
  * the store in a proxy and observe timer-fired dismissals.
221
233
  */
222
234
  readonly getStore?: () => ToastStore<readonly [], TPositions, unknown>;
235
+ /**
236
+ * Singleton scope for this controller. Defaults to the active
237
+ * `document`, an ambient `runWithSingletonScope()` context, or —
238
+ * in SSR — must be provided explicitly as a plain object.
239
+ */
240
+ readonly scope?: SingletonScope;
223
241
  }
224
242
  /**
225
243
  * Public, framework-agnostic manager returned by {@link createToastController}.
@@ -572,4 +590,4 @@ declare function toastPlugin<const TVariants extends readonly string[] = readonl
572
590
  */
573
591
  declare function createToastMagic<const TVariants extends readonly string[], const TPositions extends readonly string[] = readonly [], TContent = unknown>(config: ResolvedToastPluginConfig<TVariants, TPositions>, getStore: () => ToastStore<TVariants, TPositions, TContent>): ToastMagic<TVariants, TPositions, TContent>;
574
592
 
575
- export { type CreateToastControllerOptions, type CreateToastStoreOptions, type DefaultToastPosition, type DefaultToastVariant, PROMISE_LOADING_DURATION, RESERVED_TOAST_MAGIC_KEYS, type ResolvedPromiseConfig, type ResolvedToastPluginConfig, TOAST_STORE_KEY, type ToastAction, type ToastAlpine, type ToastChangeDetail, type ToastChangeSource, ToastController, type ToastDuration, type ToastEventPayload, type ToastEvents, type ToastItem, type ToastListener, type ToastMagic, type ToastManager, type ToastPayload, type ToastPayloadWithoutVariant, type ToastPluginCallback, type ToastPluginOptions, type ToastPosition, type ToastPromiseInput, type ToastPromiseMessages, type ToastPromiseOptions, type ToastStore, type ToastStoreKey, type ToastVariant, type ToastVariantMethods, createToastController, createToastMagic, createToastStore, toastPlugin as default, isPersistentDuration, normalizeToastDuration, resolveStackPositions, resolveToastDuration, resolveToastLimits, resolveToastPluginConfig, shouldAutoDismiss, toastOptions, toastPlugin, toastPositions, toastVariants, wrapToastStore };
593
+ export { type CreateToastControllerOptions, type CreateToastStoreOptions, type DefaultToastPosition, type DefaultToastVariant, PROMISE_LOADING_DURATION, RESERVED_TOAST_MAGIC_KEYS, type ResolvedPromiseConfig, type ResolvedToastPluginConfig, TOAST_MAGIC_KEY, TOAST_STORE_KEY, type ToastAction, type ToastAlpine, type ToastChangeDetail, type ToastChangeSource, ToastController, type ToastDuration, type ToastEventPayload, type ToastEvents, type ToastItem, type ToastListener, type ToastMagic, type ToastMagicKey, type ToastManager, type ToastPayload, type ToastPayloadWithoutVariant, type ToastPluginCallback, type ToastPluginOptions, type ToastPosition, type ToastPromiseInput, type ToastPromiseMessages, type ToastPromiseOptions, type ToastStore, type ToastStoreKey, type ToastVariant, type ToastVariantMethods, createToastController, createToastMagic, createToastStore, toastPlugin as default, isPersistentDuration, normalizeToastDuration, resolveStackPositions, resolveToastDuration, resolveToastLimits, resolveToastPluginConfig, shouldAutoDismiss, toastOptions, toastPlugin, toastPositions, toastVariants, wrapToastStore };
package/dist/index.js CHANGED
@@ -1,720 +1 @@
1
- // src/controller.ts
2
- import {
3
- BaseController,
4
- clearSingleton,
5
- createSingleton,
6
- generateId
7
- } from "@ailuracode/alpine-core";
8
-
9
- // src/types.ts
10
- var TOAST_STORE_KEY = "toast";
11
-
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
- });
23
- }
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;
50
- }
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);
65
- }
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;
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
- });
95
- }
96
- // ── Public state surface ────────────────────────────────────────
97
- get defaultPosition() {
98
- return this.#defaultPosition;
99
- }
100
- get stackPositions() {
101
- return this.#stackPositions;
102
- }
103
- get maxToasts() {
104
- return this.#maxToasts;
105
- }
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++;
234
- }
235
- }
236
- return rank <= this.#maxVisible;
237
- }
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) {
356
- const store = {
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
- },
384
- itemsAt(position) {
385
- return this.items.filter((item) => item.position === position);
386
- },
387
- timedItemsAt(position) {
388
- return this.itemsAt(position).filter((item) => shouldAutoDismiss(item.duration));
389
- },
390
- persistentItemsAt(position) {
391
- return this.itemsAt(position).filter((item) => isPersistentDuration(item.duration));
392
- },
393
- activeTimedItemsAt(position) {
394
- return this.timedItemsAt(position).filter((item) => !item.removed);
395
- },
396
- activePersistentItemsAt(position) {
397
- return this.persistentItemsAt(position).filter((item) => !item.removed);
398
- },
399
- isVisibleAt(position, index) {
400
- const stack = this.timedItemsAt(position);
401
- const item = stack[index];
402
- if (!item || item.removed) {
403
- return false;
404
- }
405
- if (this.maxVisible <= 0) {
406
- return true;
407
- }
408
- let rank = 0;
409
- for (let i = 0; i <= index; i++) {
410
- if (stack[i] && !stack[i].removed) {
411
- rank++;
412
- }
413
- }
414
- return rank <= this.maxVisible;
415
- }
416
- };
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;
431
- }
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);
459
- }
460
- }
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;
490
- }
491
-
492
- // src/plugin.ts
493
- var DEFAULT_PLUGIN_OPTIONS = {
494
- defaultPosition: "bottom-right",
495
- defaultDuration: 4e3,
496
- maxToasts: 5,
497
- listenToWindowEvents: true
498
- };
499
- var RESERVED_TOAST_MAGIC_KEYS = /* @__PURE__ */ new Set([
500
- "dismiss",
501
- "update",
502
- "dismissAt",
503
- "dismissAll",
504
- "pushUnique",
505
- "fromPayload",
506
- "promise"
507
- ]);
508
- function variantFromList(variants, preferred) {
509
- if (variants.includes(preferred)) {
510
- return preferred;
511
- }
512
- return "default";
513
- }
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
- };
523
- }
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;
531
- }
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
- };
543
- }
544
- function toastOptions(options) {
545
- return options;
546
- }
547
- function toastVariants(variants) {
548
- return variants;
549
- }
550
- function toastPositions(positions) {
551
- return positions;
552
- }
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
- }
581
- };
582
- }
583
- function createToastMagic(config, getStore) {
584
- const magic = ((titleOrPayload, options) => pushToast(getStore(), titleOrPayload, options ?? {}));
585
- magic.dismiss = (id) => getStore().dismiss(id);
586
- magic.update = (id, payload) => getStore().update(id, payload);
587
- magic.dismissAt = (position) => getStore().dismissAt(position);
588
- magic.dismissAll = () => getStore().dismissAll();
589
- magic.pushUnique = (key, payload) => getStore().pushUnique(key, payload ?? {});
590
- magic.fromPayload = (payload = {}) => {
591
- const { title = null, content = null, variant = "default", ...options } = payload;
592
- return pushToast(getStore(), {
593
- title,
594
- content,
595
- variant,
596
- ...options
597
- });
598
- };
599
- magic.promise = async (factoryOrPromise, messages = {}) => {
600
- const promiseConfig = config.promise;
601
- const loadingVariant = messages.loadingVariant ?? promiseConfig.loadingVariant;
602
- const successVariant = messages.successVariant ?? promiseConfig.successVariant;
603
- const errorVariant = messages.errorVariant ?? promiseConfig.errorVariant;
604
- const settledDuration = messages.duration ?? promiseConfig.duration;
605
- const id = getStore().push({
606
- title: messages.loading ?? promiseConfig.loading,
607
- content: messages.loadingContent ?? null,
608
- variant: loadingVariant,
609
- duration: PROMISE_LOADING_DURATION
610
- });
611
- try {
612
- const data = await resolveToastPromise(factoryOrPromise);
613
- getStore().update(
614
- id,
615
- buildPromiseSuccessPatch(data, messages, successVariant, settledDuration)
616
- );
617
- return data;
618
- } catch (error) {
619
- getStore().update(
620
- id,
621
- buildPromiseErrorPatch(messages, promiseConfig, errorVariant, settledDuration)
622
- );
623
- throw error;
624
- }
625
- };
626
- for (const variant of config.variants) {
627
- if (RESERVED_TOAST_MAGIC_KEYS.has(variant)) {
628
- continue;
629
- }
630
- Object.assign(magic, {
631
- [variant]: (titleOrPayload, options = {}) => pushVariantToast(getStore(), variant, titleOrPayload, options)
632
- });
633
- }
634
- return magic;
635
- }
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);
644
- }
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
657
- });
658
- }
659
- function resolveSuccessTitle(data, messages) {
660
- if (typeof messages.success === "function") {
661
- return messages.success(data);
662
- }
663
- return messages.success;
664
- }
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
697
- };
698
- }
699
- export {
700
- PROMISE_LOADING_DURATION,
701
- RESERVED_TOAST_MAGIC_KEYS,
702
- TOAST_STORE_KEY,
703
- ToastController,
704
- createToastController,
705
- createToastMagic,
706
- createToastStore,
707
- toastPlugin as default,
708
- isPersistentDuration,
709
- normalizeToastDuration,
710
- resolveStackPositions,
711
- resolveToastDuration,
712
- resolveToastLimits,
713
- resolveToastPluginConfig,
714
- shouldAutoDismiss,
715
- toastOptions,
716
- toastPlugin,
717
- toastPositions,
718
- toastVariants,
719
- wrapToastStore
720
- };
1
+ import{BaseController as _,createSingleton as M,generateId as h,releaseSingleton as K}from"@ailuracode/alpine-core";var c="toast",C="toast";var R=400,U=5,W=4e3,A="@ailuracode/alpine-toast/default";function x(s={}){let{scope:t,...o}=s;return M(A,()=>{let n=new y(o);return n.mount(),n},{scope:t})}var y=class extends _{#e;#l;#d;#a;#r;#y;#u;#P;#s=new Map;#T=new Map;#t=[];constructor(t){super(t.id??h("toast")),this.#e=t.defaultPosition??"bottom-right",this.#l=S(this.#e,t.positions),this.#d=t.defaultDuration??W;let o=I({maxToasts:t.maxToasts,maxVisible:t.maxVisible});this.#a=o.maxToasts,this.#r=o.maxVisible,this.#y=t.storeKey??c,this.#u=t.listenToWindowEvents!==!1,this.#P=t.getStore}destroy(){this.isDestroyed||(super.destroy(),this.#p(),K(A,this))}mount(){if(!this.isMounted){if(super.mount(),this.#u&&typeof window<"u"){let t=new AbortController,o=n=>{n instanceof CustomEvent&&this.fromPayload(n.detail??{})};window.addEventListener("toast",o,{signal:t.signal}),this.registerCleanup(()=>t.abort())}queueMicrotask(()=>{this.isDestroyed||this.#o("initialization")})}}get defaultPosition(){return this.#e}get stackPositions(){return this.#l}get maxToasts(){return this.#a}get maxVisible(){return this.#r}get items(){return this.#t}push(t={}){if(this.isDestroyed)return"";let o=t.position??this.#e,n=h("toast"),i={id:n,key:t.key??null,content:t.content??null,title:t.title??null,description:t.description??null,variant:t.variant??"default",position:o,duration:b(t.duration,this.#d),action:t.action??null,removed:!1};return this.#t=[i,...this.#t],this.#i(o,u(i.duration)?"timed":"persistent"),this.#m(n,i.duration),this.#o("push"),n}pushUnique(t,o={}){if(this.isDestroyed)return"";let n=this.#t.filter(i=>!i.removed&&i.key===t).map(i=>i.id);return this.#n(n),this.push({...o,key:t})}update(t,o={}){if(this.isDestroyed)return;let n=this.#t.find(T=>T.id===t);if(!n)return;let i=n.position,a=m(n.duration);this.#t=this.#t.map(T=>T.id===t?N(T,o):T);let e=this.#t.find(T=>T.id===t);if(!e)return;let r=e.position,l=m(e.duration),P=o.duration!==void 0&&a!==l,d=r!==i;d&&(this.#i(i,"timed"),this.#i(i,"persistent")),(d||P)&&(this.#i(r,"timed"),this.#i(r,"persistent")),o.duration!==void 0&&this.#m(t,e.duration),this.#o("update")}dismiss(t){this.isDestroyed||(this.#n([t]),this.#o("dismiss"))}dismissAt(t){if(this.isDestroyed)return;let o=this.itemsAt(t).filter(n=>!n.removed).map(n=>n.id);this.#n(o),this.#o("dismissAt")}dismissAll(){if(this.isDestroyed)return;let t=this.#t.filter(o=>!o.removed).map(o=>o.id);this.#n(t),this.#o("dismissAll")}itemsAt(t){return this.#t.filter(o=>o.position===t)}timedItemsAt(t){return this.itemsAt(t).filter(o=>u(o.duration))}persistentItemsAt(t){return this.itemsAt(t).filter(o=>m(o.duration))}activeTimedItemsAt(t){return this.timedItemsAt(t).filter(o=>!o.removed)}activePersistentItemsAt(t){return this.persistentItemsAt(t).filter(o=>!o.removed)}isVisibleAt(t,o){let n=this.timedItemsAt(t),i=n[o];if(!i||i.removed)return!1;if(this.#r<=0)return!0;let a=0;for(let e=0;e<=o;e++)n[e]&&!n[e].removed&&a++;return a<=this.#r}fromPayload(t={}){let{title:o=null,content:n=null,variant:i="default",...a}=t;return this.push({title:o,content:n,variant:i,...a})}#o(t){if(this.isDestroyed)return;let o={source:t,items:this.#t};this.emit("change",o)}#n(t){if(t.length===0)return;let o=[];for(let e of t){let r=this.#t.find(l=>l.id===e);!r||r.removed||(this.#c(e),o.push(e))}if(o.length===0)return;let n=new Set(o);this.#t=this.#t.map(e=>n.has(e.id)?{...e,removed:!0}:e);let i=h("toast-purge"),a=setTimeout(()=>{this.#T.delete(i),!this.isDestroyed&&(this.#t=this.#t.filter(e=>!n.has(e.id)),this.#o("dismiss"))},R);this.#T.set(i,a)}#i(t,o){if(this.#a<=0)return;let n=this.itemsAt(t),e=(o==="persistent"?n.filter(r=>m(r.duration)):n.filter(r=>u(r.duration))).filter(r=>!r.removed).slice(this.#a).map(r=>r.id);this.#n(e)}#m(t,o){if(this.#c(t),o!==g&&u(o)){let n=setTimeout(()=>{if(this.#s.delete(t),this.isDestroyed)return;let i=this.#P?.()??null;i?i.dismiss(t):this.dismiss(t)},o);this.#s.set(t,n)}}#c(t){let o=this.#s.get(t);o!==void 0&&(clearTimeout(o),this.#s.delete(t))}#p(){for(let t of this.#s.values())clearTimeout(t);this.#s.clear();for(let t of this.#T.values())clearTimeout(t);this.#T.clear()}};function f(s){let t={defaultPosition:s.defaultPosition,stackPositions:s.stackPositions,maxToasts:s.maxToasts,maxVisible:s.maxVisible,items:[...s.items],push(n){return s.push(n)},pushUnique(n,i){return s.pushUnique(n,i??{})},update(n,i){s.update(n,i)},dismiss(n){s.dismiss(n)},dismissAt(n){s.dismissAt(n)},dismissAll(){s.dismissAll()},destroy(){o(),s.destroy()},itemsAt(n){return this.items.filter(i=>i.position===n)},timedItemsAt(n){return this.itemsAt(n).filter(i=>u(i.duration))},persistentItemsAt(n){return this.itemsAt(n).filter(i=>m(i.duration))},activeTimedItemsAt(n){return this.timedItemsAt(n).filter(i=>!i.removed)},activePersistentItemsAt(n){return this.persistentItemsAt(n).filter(i=>!i.removed)},isVisibleAt(n,i){let a=this.timedItemsAt(n),e=a[i];if(!e||e.removed)return!1;if(this.maxVisible<=0)return!0;let r=0;for(let l=0;l<=i;l++)a[l]&&!a[l].removed&&r++;return r<=this.maxVisible}},o=s.on("change",n=>{t.items=[...n.items]});return t}function L(s={}){let t=new y(s);return t.mount(),f(t)}var g=36e5;function p(s){return s===0?!1:s}function b(s,t){return p(s===void 0?t:s)}function u(s){return typeof s=="number"&&s>0&&Number.isFinite(s)}function m(s){return!u(s)}function I(s={}){let t=s.maxToasts??U,o=s.maxVisible??t;return t>0&&o>t&&(o=t),{maxToasts:t,maxVisible:o}}function S(s,t){let o=[s];for(let n of t??[])o.includes(n)||o.push(n);return o}function N(s,t){let o={...s};return t.content!==void 0&&(o={...o,content:t.content}),t.title!==void 0&&(o={...o,title:t.title}),t.description!==void 0&&(o={...o,description:t.description}),t.variant!==void 0&&(o={...o,variant:t.variant}),t.position!==void 0&&(o={...o,position:t.position}),t.duration!==void 0&&(o={...o,duration:p(t.duration)}),t.action!==void 0&&(o={...o,action:t.action}),t.key!==void 0&&(o={...o,key:t.key}),o}import{bridgeControllerStore as Y}from"@ailuracode/alpine-core";var V={defaultPosition:"bottom-right",defaultDuration:4e3,maxToasts:5,listenToWindowEvents:!0},k=new Set(["dismiss","update","dismissAt","dismissAll","pushUnique","fromPayload","promise"]);function v(s,t){return s.includes(t)?t:"default"}function q(s,t={}){return{loading:t.loading??"Loading...",error:t.error??"Error",duration:t.duration??4e3,loadingVariant:t.loadingVariant??v(s,"loading"),successVariant:t.successVariant??v(s,"success"),errorVariant:t.errorVariant??v(s,"error")}}function O(s={}){let t=s.variants??[],o=s.positions??[],n=s.maxToasts??V.maxToasts,i=s.maxVisible??n;return n>0&&i>n&&(i=n),{defaultPosition:s.defaultPosition??V.defaultPosition,defaultDuration:s.defaultDuration??V.defaultDuration,maxToasts:n,maxVisible:i,listenToWindowEvents:s.listenToWindowEvents??V.listenToWindowEvents,storeKey:s.storeKey??c,magicKey:s.magicKey??C,variants:t,positions:o,promise:q(t,s.promise)}}function G(s){return s}function F(s){return s}function z(s){return s}function w(s={}){return function(o){let n=o,i={defaultPosition:s.defaultPosition,positions:s.positions,defaultDuration:s.defaultDuration,maxToasts:s.maxToasts,maxVisible:s.maxVisible,listenToWindowEvents:s.listenToWindowEvents,storeKey:s.storeKey},a=x(i),e=O(s),r=f(a),l=E(e,()=>r);Y({alpine:n,storeKey:e.storeKey,magicKey:e.magicKey,store:r,controller:a,packageName:"toast",magicAccessor:()=>l,subscribe:P=>{let d=P;return a.on("change",T=>{d.items=[...T.items]})}})}}function E(s,t){let o=((n,i)=>D(t(),n,i??{}));o.dismiss=n=>t().dismiss(n),o.update=(n,i)=>t().update(n,i),o.dismissAt=n=>t().dismissAt(n),o.dismissAll=()=>t().dismissAll(),o.pushUnique=(n,i)=>t().pushUnique(n,i??{}),o.fromPayload=(n={})=>{let{title:i=null,content:a=null,variant:e="default",...r}=n;return D(t(),{title:i,content:a,variant:e,...r})},o.promise=async(n,i={})=>{let a=s.promise,e=i.loadingVariant??a.loadingVariant,r=i.successVariant??a.successVariant,l=i.errorVariant??a.errorVariant,P=i.duration??a.duration,d=t().push({title:i.loading??a.loading,content:i.loadingContent??null,variant:e,duration:g});try{let T=await J(n);return t().update(d,Q(T,i,r,P)),T}catch(T){throw t().update(d,Z(i,a,l,P)),T}};for(let n of s.variants)k.has(n)||Object.assign(o,{[n]:(i,a={})=>j(t(),n,i,a)});return o}function D(s,t,o={}){return typeof t=="string"?s.push({title:t,...o}):s.push(t)}function j(s,t,o,n={}){return typeof o=="string"?s.push({title:o,...n,variant:t}):s.push({...o,...n,variant:t})}function B(s,t){return typeof t.success=="function"?t.success(s):t.success}function X(s,t){let{successContent:o}=t;return typeof o=="function"?o(s):o}function H(s,t){return s.error??t}function J(s){return typeof s=="function"?Promise.resolve(s()):Promise.resolve(s)}function Q(s,t,o,n){let i=B(s,t),a=X(s,t);return{...i!==void 0?{title:i}:{},...a!==void 0?{content:a}:{},variant:o,duration:n}}function Z(s,t,o,n){return{title:H(s,t.error),...s.errorContent!==void 0?{content:s.errorContent}:{},variant:o,duration:n}}export{g as PROMISE_LOADING_DURATION,k as RESERVED_TOAST_MAGIC_KEYS,C as TOAST_MAGIC_KEY,c as TOAST_STORE_KEY,y as ToastController,x as createToastController,E as createToastMagic,L as createToastStore,w as default,m as isPersistentDuration,p as normalizeToastDuration,S as resolveStackPositions,b as resolveToastDuration,I as resolveToastLimits,O as resolveToastPluginConfig,u as shouldAutoDismiss,G as toastOptions,w as toastPlugin,z as toastPositions,F as toastVariants,f as wrapToastStore};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ailuracode/alpine-toast",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Alpine.js in-app toast queue magic",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,7 +33,7 @@
33
33
  }
34
34
  },
35
35
  "peerDependencies": {
36
- "@ailuracode/alpine-core": "^0.2.0",
36
+ "@ailuracode/alpine-core": "^0.3.0",
37
37
  "alpinejs": "^3.0.0"
38
38
  },
39
39
  "peerDependenciesMeta": {
@@ -43,7 +43,8 @@
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/alpinejs": "^3.13.11",
46
- "@ailuracode/alpine-core": "0.2.0"
46
+ "alpinejs": "^3.15.12",
47
+ "@ailuracode/alpine-core": "0.3.0"
47
48
  },
48
49
  "keywords": [
49
50
  "alpinejs",
@@ -53,8 +54,29 @@
53
54
  "sonner",
54
55
  "notifications"
55
56
  ],
57
+ "toolkit": {
58
+ "bundleBudget": {
59
+ "category": "complex-feature"
60
+ }
61
+ },
62
+ "size-limit": [
63
+ {
64
+ "name": "full surface",
65
+ "path": "dist/index.js",
66
+ "import": "*",
67
+ "ignore": [
68
+ "alpinejs",
69
+ "@ailuracode/alpine-core"
70
+ ],
71
+ "gzip": true,
72
+ "brotli": true,
73
+ "limit": "3.7 kB"
74
+ }
75
+ ],
56
76
  "scripts": {
57
- "build": "tsup src/index.ts --format esm --dts --clean --out-dir dist && cp src/global.d.ts dist/global.d.ts",
58
- "test": "vitest run --config ../../vitest.config.ts test"
77
+ "build": "tsup src/index.ts --format esm --dts --clean --out-dir dist --minify && cp src/global.d.ts dist/global.d.ts",
78
+ "size": "size-limit",
79
+ "test": "vitest run --config ../../vitest.config.ts packages/toast",
80
+ "test:e2e": "playwright test --config playwright.config.ts"
59
81
  }
60
82
  }