@zag-js/toast 0.45.0 → 0.47.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.mjs CHANGED
@@ -1,41 +1,46 @@
1
1
  // src/toast-group.connect.ts
2
- import { subscribe } from "@zag-js/core";
2
+ import { isMachine, subscribe } from "@zag-js/core";
3
+ import { contains } from "@zag-js/dom-query";
3
4
  import { runIfFn, uuid } from "@zag-js/utils";
4
5
 
5
6
  // src/toast.anatomy.ts
6
7
  import { createAnatomy } from "@zag-js/anatomy";
7
- var anatomy = createAnatomy("toast").parts("group", "root", "title", "description", "closeTrigger");
8
+ var anatomy = createAnatomy("toast").parts(
9
+ "group",
10
+ "root",
11
+ "title",
12
+ "description",
13
+ "actionTrigger",
14
+ "closeTrigger"
15
+ );
8
16
  var parts = anatomy.build();
9
17
 
10
18
  // src/toast.dom.ts
11
19
  import { createScope } from "@zag-js/dom-query";
12
20
  var dom = createScope({
13
- getGroupId: (placement) => `toast-group:${placement}`,
21
+ getRegionId: (placement) => `toast-group:${placement}`,
22
+ getRegionEl: (ctx, placement) => dom.getById(ctx, `toast-group:${placement}`),
14
23
  getRootId: (ctx) => `toast:${ctx.id}`,
24
+ getRootEl: (ctx) => dom.getById(ctx, dom.getRootId(ctx)),
15
25
  getTitleId: (ctx) => `toast:${ctx.id}:title`,
16
26
  getDescriptionId: (ctx) => `toast:${ctx.id}:description`,
17
27
  getCloseTriggerId: (ctx) => `toast${ctx.id}:close`
18
28
  });
19
29
 
20
30
  // src/toast.utils.ts
21
- function getToastsByPlacement(toasts) {
22
- const result = {};
23
- for (const toast of toasts) {
24
- const placement = toast.state.context.placement;
25
- result[placement] || (result[placement] = []);
26
- result[placement].push(toast);
27
- }
28
- return result;
31
+ import { MAX_Z_INDEX } from "@zag-js/dom-query";
32
+ function getToastsByPlacement(toasts, placement) {
33
+ return toasts.filter((toast) => toast.state.context.placement === placement);
29
34
  }
30
35
  var defaultTimeouts = {
31
36
  info: 5e3,
32
37
  error: 5e3,
33
38
  success: 2e3,
34
39
  loading: Infinity,
35
- custom: 5e3
40
+ DEFAULT: 5e3
36
41
  };
37
42
  function getToastDuration(duration, type) {
38
- return duration ?? defaultTimeouts[type];
43
+ return duration ?? defaultTimeouts[type] ?? defaultTimeouts.DEFAULT;
39
44
  }
40
45
  function getGroupPlacementStyle(ctx, placement) {
41
46
  const offset = ctx.offsets;
@@ -49,8 +54,9 @@ function getGroupPlacementStyle(ctx, placement) {
49
54
  pointerEvents: ctx.count > 0 ? void 0 : "none",
50
55
  display: "flex",
51
56
  flexDirection: "column",
52
- "--toast-gutter": ctx.gutter,
53
- zIndex: ctx.zIndex
57
+ "--gap": `${ctx.gap}px`,
58
+ "--first-height": `${ctx.heights[0]?.height || 0}px`,
59
+ zIndex: MAX_Z_INDEX
54
60
  };
55
61
  let alignItems = "center";
56
62
  if (isRighty)
@@ -60,42 +66,150 @@ function getGroupPlacementStyle(ctx, placement) {
60
66
  styles.alignItems = alignItems;
61
67
  if (computedPlacement.includes("top")) {
62
68
  const offset2 = computedOffset.top;
63
- styles.top = `calc(env(safe-area-inset-top, 0px) + ${offset2})`;
69
+ styles.top = `max(env(safe-area-inset-top, 0px), ${offset2})`;
64
70
  }
65
71
  if (computedPlacement.includes("bottom")) {
66
72
  const offset2 = computedOffset.bottom;
67
- styles.bottom = `calc(env(safe-area-inset-bottom, 0px) + ${offset2})`;
73
+ styles.bottom = `max(env(safe-area-inset-bottom, 0px), ${offset2})`;
68
74
  }
69
75
  if (!computedPlacement.includes("left")) {
70
76
  const offset2 = computedOffset.right;
71
- styles.right = `calc(env(safe-area-inset-right, 0px) + ${offset2})`;
77
+ styles.insetInlineEnd = `calc(env(safe-area-inset-right, 0px) + ${offset2})`;
72
78
  }
73
79
  if (!computedPlacement.includes("right")) {
74
80
  const offset2 = computedOffset.left;
75
- styles.left = `calc(env(safe-area-inset-left, 0px) + ${offset2})`;
81
+ styles.insetInlineStart = `calc(env(safe-area-inset-left, 0px) + ${offset2})`;
82
+ }
83
+ return styles;
84
+ }
85
+ function getPlacementStyle(ctx, visible) {
86
+ const [side] = ctx.placement.split("-");
87
+ const sibling = !ctx.frontmost;
88
+ const overlap = !ctx.stacked;
89
+ const styles = {
90
+ position: "absolute",
91
+ pointerEvents: "auto",
92
+ "--opacity": "0",
93
+ "--remove-delay": `${ctx.removeDelay}ms`,
94
+ "--duration": `${ctx.type === "loading" ? Number.MAX_SAFE_INTEGER : ctx.duration}ms`,
95
+ "--initial-height": `${ctx.height}px`,
96
+ "--offset": `${ctx.offset}px`,
97
+ "--index": ctx.index,
98
+ "--z-index": ctx.zIndex,
99
+ "--lift-amount": "calc(var(--lift) * var(--gap))",
100
+ "--y": "100%",
101
+ "--x": "0"
102
+ };
103
+ const assign = (overrides) => Object.assign(styles, overrides);
104
+ if (side === "top") {
105
+ assign({
106
+ top: "0",
107
+ "--sign": "-1",
108
+ "--y": "-100%",
109
+ "--lift": "1"
110
+ });
111
+ } else if (side === "bottom") {
112
+ assign({
113
+ bottom: "0",
114
+ "--sign": "1",
115
+ "--y": "100%",
116
+ "--lift": "-1"
117
+ });
118
+ }
119
+ if (ctx.mounted) {
120
+ assign({
121
+ "--y": "0",
122
+ "--opacity": "1"
123
+ });
124
+ if (ctx.stacked) {
125
+ assign({
126
+ "--y": "calc(var(--lift) * var(--offset))",
127
+ "--height": "var(--initial-height)"
128
+ });
129
+ }
130
+ }
131
+ if (!visible) {
132
+ assign({
133
+ "--opacity": "0",
134
+ pointerEvents: "none"
135
+ });
136
+ }
137
+ if (sibling && overlap) {
138
+ assign({
139
+ "--base-scale": "var(--index) * 0.05 + 1",
140
+ "--y": "calc(var(--lift-amount) * var(--index))",
141
+ "--scale": "calc(-1 * var(--base-scale))",
142
+ "--height": "var(--first-height)"
143
+ });
144
+ if (!visible) {
145
+ assign({
146
+ "--y": "calc(var(--sign) * 40%)"
147
+ });
148
+ }
149
+ }
150
+ if (sibling && ctx.stacked && !visible) {
151
+ assign({
152
+ "--y": "calc(var(--lift) * var(--offset) + var(--lift) * -100%)"
153
+ });
154
+ }
155
+ if (ctx.frontmost && !visible) {
156
+ assign({
157
+ "--y": "calc(var(--lift) * -100%)"
158
+ });
76
159
  }
77
160
  return styles;
78
161
  }
162
+ function getGhostBeforeStyle(ctx, visible) {
163
+ const styles = {
164
+ position: "absolute",
165
+ inset: "0",
166
+ scale: "1 2",
167
+ pointerEvents: visible ? "none" : "auto"
168
+ };
169
+ const assign = (overrides) => Object.assign(styles, overrides);
170
+ if (ctx.frontmost && !visible) {
171
+ assign({
172
+ height: "calc(var(--initial-height) + 80%)"
173
+ });
174
+ }
175
+ return styles;
176
+ }
177
+ function getGhostAfterStyle(_ctx, _visible) {
178
+ return {
179
+ position: "absolute",
180
+ left: "0",
181
+ height: "calc(var(--gap) + 2px)",
182
+ bottom: "100%",
183
+ width: "100%"
184
+ };
185
+ }
79
186
 
80
187
  // src/toast-group.connect.ts
81
- function groupConnect(state, send, normalize) {
82
- const toastsByPlacement = getToastsByPlacement(state.context.toasts);
188
+ function groupConnect(serviceOrState, send, normalize) {
189
+ function getState() {
190
+ const result = isMachine(serviceOrState) ? serviceOrState.getState() : serviceOrState;
191
+ return result;
192
+ }
193
+ function getToastsByPlacementImpl(placement) {
194
+ return getToastsByPlacement(getState().context.toasts, placement);
195
+ }
83
196
  function isVisible(id) {
84
- if (!state.context.toasts.length)
197
+ const toasts = getState().context.toasts;
198
+ if (!toasts.length)
85
199
  return false;
86
- return !!state.context.toasts.find((toast) => toast.id == id);
200
+ return !!toasts.find((toast) => toast.id == id);
87
201
  }
88
202
  function create(options) {
89
203
  const uid = `toast:${uuid()}`;
90
204
  const id = options.id ? options.id : uid;
91
205
  if (isVisible(id))
92
- return;
206
+ return id;
93
207
  send({ type: "ADD_TOAST", toast: { ...options, id } });
94
208
  return id;
95
209
  }
96
210
  function update(id, options) {
97
211
  if (!isVisible(id))
98
- return;
212
+ return id;
99
213
  send({ type: "UPDATE_TOAST", id, toast: options });
100
214
  return id;
101
215
  }
@@ -116,9 +230,15 @@ function groupConnect(state, send, normalize) {
116
230
  }
117
231
  }
118
232
  return {
119
- count: state.context.count,
120
- toasts: state.context.toasts,
121
- toastsByPlacement,
233
+ getCount() {
234
+ return getState().context.count;
235
+ },
236
+ getPlacements() {
237
+ const toasts = getState().context.toasts;
238
+ const placements = toasts.map((toast) => toast.state.context.placement);
239
+ return Array.from(new Set(placements));
240
+ },
241
+ getToastsByPlacement: getToastsByPlacementImpl,
122
242
  isVisible,
123
243
  create,
124
244
  update,
@@ -132,10 +252,8 @@ function groupConnect(state, send, normalize) {
132
252
  }
133
253
  },
134
254
  dismissByPlacement(placement) {
135
- const toasts = toastsByPlacement[placement];
136
- if (toasts) {
137
- toasts.forEach((toast) => dismiss(toast.id));
138
- }
255
+ const toasts = getToastsByPlacementImpl(placement);
256
+ toasts.forEach((toast) => dismiss(toast.id));
139
257
  },
140
258
  loading(options) {
141
259
  return upsert({ ...options, type: "loading" });
@@ -148,14 +266,16 @@ function groupConnect(state, send, normalize) {
148
266
  },
149
267
  promise(promise, options, shared = {}) {
150
268
  const id = upsert({ ...shared, ...options.loading, type: "loading" });
151
- promise.then((response) => {
269
+ runIfFn(promise).then((response) => {
152
270
  const successOptions = runIfFn(options.success, response);
153
271
  upsert({ ...shared, ...successOptions, id, type: "success" });
154
272
  }).catch((error) => {
155
273
  const errorOptions = runIfFn(options.error, error);
156
274
  upsert({ ...shared, ...errorOptions, id, type: "error" });
275
+ }).finally(() => {
276
+ options.finally?.();
157
277
  });
158
- return promise;
278
+ return id;
159
279
  },
160
280
  pause(id) {
161
281
  if (id == null) {
@@ -173,43 +293,66 @@ function groupConnect(state, send, normalize) {
173
293
  },
174
294
  getGroupProps(options) {
175
295
  const { placement, label = "Notifications" } = options;
296
+ const state = getState();
297
+ const hotkeyLabel = state.context.hotkey.join("+").replace(/Key/g, "").replace(/Digit/g, "");
298
+ const [side, align = "center"] = placement.split("-");
176
299
  return normalize.element({
177
300
  ...parts.group.attrs,
178
301
  dir: state.context.dir,
179
302
  tabIndex: -1,
180
- "aria-label": `${placement} ${label}`,
181
- id: dom.getGroupId(placement),
303
+ "aria-label": `${placement} ${label} ${hotkeyLabel}`,
304
+ id: dom.getRegionId(placement),
182
305
  "data-placement": placement,
306
+ "data-side": side,
307
+ "data-align": align,
183
308
  "aria-live": "polite",
184
309
  role: "region",
185
- style: getGroupPlacementStyle(state.context, placement)
310
+ style: getGroupPlacementStyle(state.context, placement),
311
+ onMouseMove() {
312
+ send({ type: "REGION.POINTER_ENTER", placement });
313
+ },
314
+ onMouseLeave() {
315
+ send({ type: "REGION.POINTER_LEAVE", placement });
316
+ },
317
+ onFocus(event) {
318
+ send({ type: "REGION.FOCUS", target: event.relatedTarget });
319
+ },
320
+ onBlur(event) {
321
+ if (state.context.isFocusWithin && !contains(event.currentTarget, event.relatedTarget)) {
322
+ send({ type: "REGION.BLUR" });
323
+ }
324
+ }
186
325
  });
187
326
  },
188
327
  subscribe(fn) {
189
- return subscribe(state.context.toasts, () => fn(state.context.toasts));
328
+ const state = getState();
329
+ return subscribe(state.context.toasts, () => {
330
+ const toasts = getToastsByPlacementImpl(state.context.placement);
331
+ const contexts = toasts.map((toast) => toast.getState().context);
332
+ fn(contexts);
333
+ });
190
334
  }
191
335
  };
192
336
  }
193
337
 
194
338
  // src/toast-group.machine.ts
195
- import { createMachine as createMachine2 } from "@zag-js/core";
196
- import { MAX_Z_INDEX } from "@zag-js/dom-query";
339
+ import { createMachine as createMachine2, ref } from "@zag-js/core";
340
+ import { trackDismissableBranch } from "@zag-js/dismissable";
341
+ import { addDomEvent } from "@zag-js/dom-event";
197
342
  import { compact as compact2 } from "@zag-js/utils";
198
343
 
199
344
  // src/toast.machine.ts
200
345
  import { createMachine, guards } from "@zag-js/core";
201
- import { addDomEvent } from "@zag-js/dom-event";
202
- import { compact } from "@zag-js/utils";
346
+ import { queryAll, raf } from "@zag-js/dom-query";
347
+ import { compact, warn } from "@zag-js/utils";
203
348
  var { not, and, or } = guards;
204
- function createToastMachine(options = {}) {
205
- const { type = "info", duration, id = "toast", placement = "bottom", removeDelay = 0, ...restProps } = options;
349
+ function createToastMachine(options) {
350
+ const { type = "info", duration, id = "1", placement = "bottom", removeDelay = 200, ...restProps } = options;
206
351
  const ctx = compact(restProps);
207
352
  const computedDuration = getToastDuration(duration, type);
208
353
  return createMachine(
209
354
  {
210
355
  id,
211
- entry: "invokeOnOpen",
212
- initial: type === "loading" ? "persist" : "active",
213
356
  context: {
214
357
  id,
215
358
  type,
@@ -218,95 +361,139 @@ function createToastMachine(options = {}) {
218
361
  removeDelay,
219
362
  createdAt: Date.now(),
220
363
  placement,
221
- ...ctx
364
+ ...ctx,
365
+ height: 0,
366
+ offset: 0,
367
+ frontmost: false,
368
+ mounted: false,
369
+ index: -1,
370
+ zIndex: 0
222
371
  },
372
+ initial: type === "loading" ? "visible:persist" : "visible",
223
373
  on: {
224
374
  UPDATE: [
225
375
  {
226
376
  guard: and("hasTypeChanged", "isChangingToLoading"),
227
- target: "persist",
228
- actions: ["setContext", "invokeOnUpdate"]
377
+ target: "visible:persist",
378
+ actions: ["setContext"]
229
379
  },
230
380
  {
231
381
  guard: or("hasDurationChanged", "hasTypeChanged"),
232
- target: "active:temp",
233
- actions: ["setContext", "invokeOnUpdate"]
382
+ target: "visible:updating",
383
+ actions: ["setContext"]
234
384
  },
235
385
  {
236
- actions: ["setContext", "invokeOnUpdate"]
386
+ actions: ["setContext"]
237
387
  }
238
- ]
388
+ ],
389
+ MEASURE: {
390
+ actions: ["measureHeight"]
391
+ }
239
392
  },
393
+ entry: ["invokeOnVisible"],
394
+ activities: ["trackHeight"],
240
395
  states: {
241
- "active:temp": {
396
+ "visible:updating": {
242
397
  tags: ["visible", "updating"],
243
398
  after: {
244
- 0: "active"
399
+ 0: "visible"
245
400
  }
246
401
  },
247
- persist: {
402
+ "visible:persist": {
248
403
  tags: ["visible", "paused"],
249
- activities: "trackDocumentVisibility",
250
404
  on: {
251
405
  RESUME: {
252
406
  guard: not("isLoadingType"),
253
- target: "active",
407
+ target: "visible",
254
408
  actions: ["setCreatedAt"]
255
409
  },
256
410
  DISMISS: "dismissing"
257
411
  }
258
412
  },
259
- active: {
413
+ visible: {
260
414
  tags: ["visible"],
261
- activities: "trackDocumentVisibility",
262
415
  after: {
263
416
  VISIBLE_DURATION: "dismissing"
264
417
  },
265
418
  on: {
266
419
  DISMISS: "dismissing",
267
420
  PAUSE: {
268
- target: "persist",
421
+ target: "visible:persist",
269
422
  actions: "setRemainingDuration"
270
423
  }
271
424
  }
272
425
  },
273
426
  dismissing: {
274
- entry: "invokeOnClosing",
427
+ entry: "invokeOnDismiss",
275
428
  after: {
276
429
  REMOVE_DELAY: {
277
- target: "inactive",
430
+ target: "unmounted",
278
431
  actions: "notifyParentToRemove"
279
432
  }
280
433
  }
281
434
  },
282
- inactive: {
283
- entry: "invokeOnClose",
435
+ unmounted: {
436
+ entry: "invokeOnUnmount",
284
437
  type: "final"
285
438
  }
286
439
  }
287
440
  },
288
441
  {
289
442
  activities: {
290
- trackDocumentVisibility(ctx2, _evt, { send }) {
291
- if (!ctx2.pauseOnPageIdle)
292
- return;
293
- const doc = dom.getDoc(ctx2);
294
- return addDomEvent(doc, "visibilitychange", () => {
295
- send(doc.visibilityState === "hidden" ? "PAUSE" : "RESUME");
443
+ trackHeight(ctx2, _evt, { self }) {
444
+ let cleanup;
445
+ raf(() => {
446
+ const rootEl = dom.getRootEl(ctx2);
447
+ if (!rootEl)
448
+ return;
449
+ ctx2.mounted = true;
450
+ const ghosts = queryAll(rootEl, "[data-ghost]");
451
+ warn(
452
+ ghosts.length !== 2,
453
+ "[toast] No ghost element found in toast. Render the `ghostBefore` and `ghostAfter` elements"
454
+ );
455
+ const syncHeight = () => {
456
+ const originalHeight = rootEl.style.height;
457
+ rootEl.style.height = "auto";
458
+ const newHeight = rootEl.offsetHeight;
459
+ rootEl.style.height = originalHeight;
460
+ ctx2.height = newHeight;
461
+ self.sendParent({ type: "UPDATE_HEIGHT", id: self.id, height: newHeight, placement: ctx2.placement });
462
+ };
463
+ syncHeight();
464
+ const win = dom.getWin(ctx2);
465
+ const observer = new win.MutationObserver(syncHeight);
466
+ observer.observe(rootEl, { childList: true, subtree: true, characterData: true });
467
+ cleanup = () => observer.disconnect();
296
468
  });
469
+ return () => cleanup?.();
297
470
  }
298
471
  },
299
472
  guards: {
300
473
  isChangingToLoading: (_, evt) => evt.toast?.type === "loading",
301
474
  isLoadingType: (ctx2) => ctx2.type === "loading",
302
- hasTypeChanged: (ctx2, evt) => evt.toast?.type !== ctx2.type,
303
- hasDurationChanged: (ctx2, evt) => evt.toast?.duration !== ctx2.duration
475
+ hasTypeChanged: (ctx2, evt) => evt.toast?.type != null && evt.toast.type !== ctx2.type,
476
+ hasDurationChanged: (ctx2, evt) => evt.toast?.duration != null && evt.toast.duration !== ctx2.duration
304
477
  },
305
478
  delays: {
306
479
  VISIBLE_DURATION: (ctx2) => ctx2.remaining,
307
480
  REMOVE_DELAY: (ctx2) => ctx2.removeDelay
308
481
  },
309
482
  actions: {
483
+ measureHeight(ctx2, _evt, { self }) {
484
+ raf(() => {
485
+ const rootEl = dom.getRootEl(ctx2);
486
+ if (!rootEl)
487
+ return;
488
+ ctx2.mounted = true;
489
+ const originalHeight = rootEl.style.height;
490
+ rootEl.style.height = "auto";
491
+ const newHeight = rootEl.offsetHeight;
492
+ rootEl.style.height = originalHeight;
493
+ ctx2.height = newHeight;
494
+ self.sendParent({ type: "UPDATE_HEIGHT", id: self.id, height: newHeight, placement: ctx2.placement });
495
+ });
496
+ },
310
497
  setRemainingDuration(ctx2) {
311
498
  ctx2.remaining -= Date.now() - ctx2.createdAt;
312
499
  },
@@ -316,22 +503,24 @@ function createToastMachine(options = {}) {
316
503
  notifyParentToRemove(_ctx, _evt, { self }) {
317
504
  self.sendParent({ type: "REMOVE_TOAST", id: self.id });
318
505
  },
319
- invokeOnClosing(ctx2) {
320
- ctx2.onClosing?.();
506
+ invokeOnDismiss(ctx2) {
507
+ ctx2.onStatusChange?.({ status: "dismissing" });
321
508
  },
322
- invokeOnClose(ctx2) {
323
- ctx2.onClose?.();
509
+ invokeOnUnmount(ctx2) {
510
+ ctx2.onStatusChange?.({ status: "unmounted" });
324
511
  },
325
- invokeOnOpen(ctx2) {
326
- ctx2.onOpen?.();
327
- },
328
- invokeOnUpdate(ctx2) {
329
- ctx2.onUpdate?.();
512
+ invokeOnVisible(ctx2) {
513
+ ctx2.onStatusChange?.({ status: "visible" });
330
514
  },
331
515
  setContext(ctx2, evt) {
332
- const { duration: duration2, type: type2 } = evt.toast;
333
- const time = getToastDuration(duration2, type2);
334
- Object.assign(ctx2, { ...evt.toast, duration: time, remaining: time });
516
+ const duration2 = evt.toast?.duration ?? ctx2.duration;
517
+ const type2 = evt.toast?.type ?? ctx2.type;
518
+ const computedDuration2 = getToastDuration(duration2, type2);
519
+ Object.assign(ctx2, {
520
+ ...evt.toast,
521
+ duration: computedDuration2,
522
+ remaining: computedDuration2
523
+ });
335
524
  }
336
525
  }
337
526
  }
@@ -341,104 +530,305 @@ function createToastMachine(options = {}) {
341
530
  // src/toast-group.machine.ts
342
531
  function groupMachine(userContext) {
343
532
  const ctx = compact2(userContext);
344
- return createMachine2({
345
- id: "toaster",
346
- initial: "active",
347
- context: {
348
- dir: "ltr",
349
- max: Number.MAX_SAFE_INTEGER,
350
- toasts: [],
351
- gutter: "1rem",
352
- zIndex: MAX_Z_INDEX,
353
- pauseOnPageIdle: false,
354
- pauseOnInteraction: true,
355
- offsets: { left: "0px", right: "0px", top: "0px", bottom: "0px" },
356
- ...ctx
357
- },
358
- computed: {
359
- count: (ctx2) => ctx2.toasts.length
360
- },
361
- on: {
362
- PAUSE_TOAST: {
363
- actions: (_ctx, evt, { self }) => {
364
- self.sendChild("PAUSE", evt.id);
365
- }
533
+ return createMachine2(
534
+ {
535
+ id: "toaster",
536
+ initial: ctx.overlap ? "overlap" : "stack",
537
+ context: {
538
+ dir: "ltr",
539
+ max: Number.MAX_SAFE_INTEGER,
540
+ toasts: [],
541
+ gap: 16,
542
+ pauseOnPageIdle: false,
543
+ hotkey: ["altKey", "KeyT"],
544
+ offsets: "1rem",
545
+ placement: "bottom",
546
+ removeDelay: 200,
547
+ ...ctx,
548
+ lastFocusedEl: null,
549
+ isFocusWithin: false,
550
+ heights: []
366
551
  },
367
- PAUSE_ALL: {
368
- actions: (ctx2) => {
369
- ctx2.toasts.forEach((toast) => toast.send("PAUSE"));
370
- }
552
+ computed: {
553
+ count: (ctx2) => ctx2.toasts.length
371
554
  },
372
- RESUME_TOAST: {
373
- actions: (_ctx, evt, { self }) => {
374
- self.sendChild("RESUME", evt.id);
555
+ activities: ["trackDocumentVisibility", "trackHotKeyPress"],
556
+ watch: {
557
+ toasts: ["collapsedIfEmpty", "setDismissableBranch"]
558
+ },
559
+ exit: ["removeToasts", "clearDismissableBranch", "clearLastFocusedEl"],
560
+ on: {
561
+ PAUSE_TOAST: {
562
+ actions: ["pauseToast"]
563
+ },
564
+ PAUSE_ALL: {
565
+ actions: ["pauseToasts"]
566
+ },
567
+ RESUME_TOAST: {
568
+ actions: ["resumeToast"]
569
+ },
570
+ RESUME_ALL: {
571
+ actions: ["resumeToasts"]
572
+ },
573
+ ADD_TOAST: {
574
+ guard: "isWithinRange",
575
+ actions: ["createToast", "syncToastIndex"]
576
+ },
577
+ UPDATE_TOAST: {
578
+ actions: ["updateToast"]
579
+ },
580
+ DISMISS_TOAST: {
581
+ actions: ["dismissToast"]
582
+ },
583
+ DISMISS_ALL: {
584
+ actions: ["dismissToasts"]
585
+ },
586
+ REMOVE_TOAST: {
587
+ actions: ["removeToast", "syncToastIndex", "syncToastOffset"]
588
+ },
589
+ REMOVE_ALL: {
590
+ actions: ["removeToasts"]
591
+ },
592
+ UPDATE_HEIGHT: {
593
+ actions: ["syncHeights", "syncToastOffset"]
594
+ },
595
+ "DOC.HOTKEY": {
596
+ actions: ["focusRegionEl"]
597
+ },
598
+ "REGION.BLUR": [
599
+ {
600
+ guard: "isOverlapping",
601
+ target: "overlap",
602
+ actions: ["resumeToasts", "restoreLastFocusedEl"]
603
+ },
604
+ {
605
+ actions: ["resumeToasts", "restoreLastFocusedEl"]
606
+ }
607
+ ]
608
+ },
609
+ states: {
610
+ stack: {
611
+ entry: ["expandToasts"],
612
+ on: {
613
+ "REGION.POINTER_LEAVE": [
614
+ {
615
+ guard: "isOverlapping",
616
+ target: "overlap",
617
+ actions: ["resumeToasts"]
618
+ },
619
+ {
620
+ actions: ["resumeToasts"]
621
+ }
622
+ ],
623
+ "REGION.OVERLAP": {
624
+ target: "overlap"
625
+ },
626
+ "REGION.FOCUS": {
627
+ actions: ["setLastFocusedEl", "pauseToasts"]
628
+ },
629
+ "REGION.POINTER_ENTER": {
630
+ actions: ["pauseToasts"]
631
+ }
632
+ }
633
+ },
634
+ overlap: {
635
+ entry: ["collapseToasts"],
636
+ on: {
637
+ "REGION.STACK": {
638
+ target: "stack"
639
+ },
640
+ "REGION.POINTER_ENTER": {
641
+ target: "stack",
642
+ actions: ["pauseToasts"]
643
+ },
644
+ "REGION.FOCUS": {
645
+ target: "stack",
646
+ actions: ["setLastFocusedEl", "pauseToasts"]
647
+ }
648
+ }
375
649
  }
650
+ }
651
+ },
652
+ {
653
+ guards: {
654
+ isWithinRange: (ctx2) => ctx2.toasts.length < ctx2.max,
655
+ isOverlapping: (ctx2) => !!ctx2.overlap
376
656
  },
377
- RESUME_ALL: {
378
- actions: (ctx2) => {
379
- ctx2.toasts.forEach((toast) => toast.send("RESUME"));
657
+ activities: {
658
+ trackHotKeyPress(ctx2, _evt, { send }) {
659
+ const handleKeyDown = (event) => {
660
+ const isHotkeyPressed = ctx2.hotkey.every((key) => event[key] || event.code === key);
661
+ if (!isHotkeyPressed)
662
+ return;
663
+ send({ type: "DOC.HOTKEY" });
664
+ };
665
+ return addDomEvent(document, "keydown", handleKeyDown, { capture: true });
666
+ },
667
+ trackDocumentVisibility(ctx2, _evt, { send }) {
668
+ if (!ctx2.pauseOnPageIdle)
669
+ return;
670
+ const doc = dom.getDoc(ctx2);
671
+ return addDomEvent(doc, "visibilitychange", () => {
672
+ send(doc.visibilityState === "hidden" ? "PAUSE_ALL" : "RESUME_ALL");
673
+ });
380
674
  }
381
675
  },
382
- ADD_TOAST: {
383
- guard: (ctx2) => ctx2.toasts.length < ctx2.max,
384
- actions: (ctx2, evt, { self }) => {
676
+ actions: {
677
+ setDismissableBranch(ctx2) {
678
+ const currentToasts = getToastsByPlacement(ctx2.toasts, ctx2.placement);
679
+ const hasToasts = currentToasts.length > 0;
680
+ if (!hasToasts) {
681
+ ctx2._cleanup?.();
682
+ return;
683
+ }
684
+ if (hasToasts && ctx2._cleanup) {
685
+ return;
686
+ }
687
+ const groupEl = () => dom.getRegionEl(ctx2, ctx2.placement);
688
+ ctx2._cleanup = trackDismissableBranch(groupEl, { defer: true });
689
+ },
690
+ clearDismissableBranch(ctx2) {
691
+ ctx2._cleanup?.();
692
+ },
693
+ focusRegionEl(ctx2) {
694
+ queueMicrotask(() => {
695
+ dom.getRegionEl(ctx2, ctx2.placement)?.focus();
696
+ });
697
+ },
698
+ expandToasts(ctx2) {
699
+ each(ctx2, (toast) => {
700
+ toast.state.context.stacked = true;
701
+ });
702
+ },
703
+ collapseToasts(ctx2) {
704
+ each(ctx2, (toast) => {
705
+ toast.state.context.stacked = false;
706
+ });
707
+ },
708
+ collapsedIfEmpty(ctx2, _evt, { send }) {
709
+ if (!ctx2.overlap || ctx2.toasts.length > 1)
710
+ return;
711
+ send("REGION.OVERLAP");
712
+ },
713
+ pauseToast(_ctx, evt, { self }) {
714
+ self.sendChild("PAUSE", evt.id);
715
+ },
716
+ pauseToasts(ctx2) {
717
+ ctx2.toasts.forEach((toast) => toast.send("PAUSE"));
718
+ },
719
+ resumeToast(_ctx, evt, { self }) {
720
+ self.sendChild("RESUME", evt.id);
721
+ },
722
+ resumeToasts(ctx2) {
723
+ ctx2.toasts.forEach((toast) => toast.send("RESUME"));
724
+ },
725
+ measureToasts(ctx2) {
726
+ ctx2.toasts.forEach((toast) => toast.send("MEASURE"));
727
+ },
728
+ createToast(ctx2, evt, { self, getState }) {
385
729
  const options = {
386
730
  placement: ctx2.placement,
387
731
  duration: ctx2.duration,
388
732
  removeDelay: ctx2.removeDelay,
389
- render: ctx2.render,
390
733
  ...evt.toast,
391
- pauseOnPageIdle: ctx2.pauseOnPageIdle,
392
- pauseOnInteraction: ctx2.pauseOnInteraction,
393
734
  dir: ctx2.dir,
394
- getRootNode: ctx2.getRootNode
735
+ getRootNode: ctx2.getRootNode,
736
+ stacked: getState().matches("stack")
395
737
  };
396
738
  const toast = createToastMachine(options);
397
739
  const actor = self.spawn(toast);
398
- ctx2.toasts.push(actor);
399
- }
400
- },
401
- UPDATE_TOAST: {
402
- actions: (_ctx, evt, { self }) => {
740
+ ctx2.toasts = [actor, ...ctx2.toasts];
741
+ },
742
+ updateToast(_ctx, evt, { self }) {
403
743
  self.sendChild({ type: "UPDATE", toast: evt.toast }, evt.id);
404
- }
405
- },
406
- DISMISS_TOAST: {
407
- actions: (_ctx, evt, { self }) => {
744
+ },
745
+ dismissToast(_ctx, evt, { self }) {
408
746
  self.sendChild("DISMISS", evt.id);
409
- }
410
- },
411
- DISMISS_ALL: {
412
- actions: (ctx2) => {
747
+ },
748
+ dismissToasts(ctx2) {
413
749
  ctx2.toasts.forEach((toast) => toast.send("DISMISS"));
414
- }
415
- },
416
- REMOVE_TOAST: {
417
- actions: (ctx2, evt, { self }) => {
750
+ },
751
+ removeToast(ctx2, evt, { self }) {
418
752
  self.stopChild(evt.id);
419
- const index = ctx2.toasts.findIndex((toast) => toast.id === evt.id);
420
- ctx2.toasts.splice(index, 1);
421
- }
422
- },
423
- REMOVE_ALL: {
424
- actions: (ctx2, _evt, { self }) => {
753
+ ctx2.toasts = ctx2.toasts.filter((toast) => toast.id !== evt.id);
754
+ ctx2.heights = ctx2.heights.filter((height) => height.id !== evt.id);
755
+ },
756
+ removeToasts(ctx2, _evt, { self }) {
425
757
  ctx2.toasts.forEach((toast) => self.stopChild(toast.id));
426
- while (ctx2.toasts.length)
427
- ctx2.toasts.pop();
758
+ ctx2.toasts = [];
759
+ ctx2.heights = [];
760
+ },
761
+ syncHeights(ctx2, evt) {
762
+ const existing = ctx2.heights.find((height) => height.id === evt.id);
763
+ if (existing) {
764
+ existing.height = evt.height;
765
+ existing.placement = evt.placement;
766
+ } else {
767
+ const newHeight = { id: evt.id, height: evt.height, placement: evt.placement };
768
+ ctx2.heights = [newHeight, ...ctx2.heights];
769
+ }
770
+ },
771
+ syncToastIndex(ctx2) {
772
+ each(ctx2, (toast, index, toasts) => {
773
+ toast.state.context.index = index;
774
+ toast.state.context.frontmost = index === 0;
775
+ toast.state.context.zIndex = toasts.length - index;
776
+ });
777
+ },
778
+ syncToastOffset(ctx2, evt) {
779
+ const placement = evt.placement ?? ctx2.placement;
780
+ each({ ...ctx2, placement }, (toast) => {
781
+ const heightIndex = Math.max(
782
+ ctx2.heights.findIndex((height) => height.id === toast.id),
783
+ 0
784
+ );
785
+ const toastsHeightBefore = ctx2.heights.reduce((prev, curr, reducerIndex) => {
786
+ if (reducerIndex >= heightIndex)
787
+ return prev;
788
+ return prev + curr.height;
789
+ }, 0);
790
+ toast.state.context.offset = heightIndex * ctx2.gap + toastsHeightBefore;
791
+ });
792
+ },
793
+ setLastFocusedEl(ctx2, evt) {
794
+ if (ctx2.isFocusWithin || !evt.target)
795
+ return;
796
+ ctx2.isFocusWithin = true;
797
+ ctx2.lastFocusedEl = ref(evt.target);
798
+ },
799
+ restoreLastFocusedEl(ctx2) {
800
+ ctx2.isFocusWithin = false;
801
+ if (!ctx2.lastFocusedEl)
802
+ return;
803
+ ctx2.lastFocusedEl.focus({ preventScroll: true });
804
+ ctx2.lastFocusedEl = null;
805
+ },
806
+ clearLastFocusedEl(ctx2) {
807
+ if (!ctx2.lastFocusedEl)
808
+ return;
809
+ ctx2.lastFocusedEl.focus({ preventScroll: true });
810
+ ctx2.lastFocusedEl = null;
811
+ ctx2.isFocusWithin = false;
428
812
  }
429
813
  }
430
814
  }
431
- });
815
+ );
816
+ }
817
+ function each(ctx, fn) {
818
+ const currentToasts = getToastsByPlacement(ctx.toasts, ctx.placement);
819
+ currentToasts.forEach(fn);
432
820
  }
433
821
 
434
822
  // src/toast.connect.ts
823
+ import { dataAttr } from "@zag-js/dom-query";
435
824
  function connect(state, send, normalize) {
436
825
  const isVisible = state.hasTag("visible");
437
826
  const isPaused = state.hasTag("paused");
438
- const pauseOnInteraction = state.context.pauseOnInteraction;
439
827
  const placement = state.context.placement;
828
+ const type = state.context.type;
829
+ const [side, align = "center"] = placement.split("-");
440
830
  return {
441
- type: state.context.type,
831
+ type,
442
832
  title: state.context.title,
443
833
  description: state.context.description,
444
834
  placement,
@@ -459,45 +849,37 @@ function connect(state, send, normalize) {
459
849
  dir: state.context.dir,
460
850
  id: dom.getRootId(state.context),
461
851
  "data-state": isVisible ? "open" : "closed",
462
- "data-type": state.context.type,
852
+ "data-type": type,
463
853
  "data-placement": placement,
854
+ "data-align": align,
855
+ "data-side": side,
856
+ "data-mounted": dataAttr(state.context.mounted),
857
+ "data-paused": dataAttr(isPaused),
858
+ "data-first": dataAttr(state.context.frontmost),
859
+ "data-sibling": dataAttr(!state.context.frontmost),
860
+ "data-stack": dataAttr(state.context.stacked),
861
+ "data-overlap": dataAttr(!state.context.stacked),
464
862
  role: "status",
465
863
  "aria-atomic": "true",
466
864
  tabIndex: 0,
467
- style: {
468
- position: "relative",
469
- pointerEvents: "auto",
470
- margin: "calc(var(--toast-gutter) / 2)",
471
- "--remove-delay": `${state.context.removeDelay}ms`,
472
- "--duration": `${state.context.duration}ms`
473
- },
865
+ style: getPlacementStyle(state.context, isVisible),
474
866
  onKeyDown(event) {
475
867
  if (event.key == "Escape") {
476
868
  send("DISMISS");
477
869
  event.preventDefault();
478
870
  }
479
- },
480
- onFocus() {
481
- if (pauseOnInteraction) {
482
- send("PAUSE");
483
- }
484
- },
485
- onBlur() {
486
- if (pauseOnInteraction) {
487
- send("RESUME");
488
- }
489
- },
490
- onPointerEnter() {
491
- if (pauseOnInteraction) {
492
- send("PAUSE");
493
- }
494
- },
495
- onPointerLeave() {
496
- if (pauseOnInteraction) {
497
- send("RESUME");
498
- }
499
871
  }
500
872
  }),
873
+ /* Leave a ghost div to avoid setting hover to false when transitioning out */
874
+ ghostBeforeProps: normalize.element({
875
+ "data-ghost": "before",
876
+ style: getGhostBeforeStyle(state.context, isVisible)
877
+ }),
878
+ /* Needed to avoid setting hover to false when in between toasts */
879
+ ghostAfterProps: normalize.element({
880
+ "data-ghost": "after",
881
+ style: getGhostAfterStyle(state.context, isVisible)
882
+ }),
501
883
  titleProps: normalize.element({
502
884
  ...parts.title.attrs,
503
885
  id: dom.getTitleId(state.context)
@@ -506,6 +888,13 @@ function connect(state, send, normalize) {
506
888
  ...parts.description.attrs,
507
889
  id: dom.getDescriptionId(state.context)
508
890
  }),
891
+ actionTriggerProps: normalize.button({
892
+ ...parts.actionTrigger.attrs,
893
+ type: "button",
894
+ onClick() {
895
+ send("DISMISS");
896
+ }
897
+ }),
509
898
  closeTriggerProps: normalize.button({
510
899
  id: dom.getCloseTriggerId(state.context),
511
900
  ...parts.closeTrigger.attrs,