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