@zag-js/toast 0.9.2 → 0.10.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zag-js/toast",
3
- "version": "0.9.2",
3
+ "version": "0.10.0",
4
4
  "description": "Core logic for the toast widget implemented as a state machine",
5
5
  "keywords": [
6
6
  "js",
@@ -17,7 +17,8 @@
17
17
  "repository": "https://github.com/chakra-ui/zag/tree/main/packages/toast",
18
18
  "sideEffects": false,
19
19
  "files": [
20
- "dist/**/*"
20
+ "dist",
21
+ "src"
21
22
  ],
22
23
  "publishConfig": {
23
24
  "access": "public"
@@ -26,12 +27,12 @@
26
27
  "url": "https://github.com/chakra-ui/zag/issues"
27
28
  },
28
29
  "dependencies": {
29
- "@zag-js/anatomy": "0.9.2",
30
- "@zag-js/core": "0.9.2",
31
- "@zag-js/dom-query": "0.9.2",
32
- "@zag-js/dom-event": "0.9.2",
33
- "@zag-js/utils": "0.9.2",
34
- "@zag-js/types": "0.9.2"
30
+ "@zag-js/anatomy": "0.10.0",
31
+ "@zag-js/core": "0.10.0",
32
+ "@zag-js/dom-query": "0.10.0",
33
+ "@zag-js/dom-event": "0.10.0",
34
+ "@zag-js/utils": "0.10.0",
35
+ "@zag-js/types": "0.10.0"
35
36
  },
36
37
  "devDependencies": {
37
38
  "clean-package": "2.2.0"
package/src/index.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { isDom } from "@zag-js/dom-query"
2
+ import { warn } from "@zag-js/utils"
3
+ import { groupConnect, toaster } from "./toast-group.connect"
4
+ import { groupMachine } from "./toast-group.machine"
5
+ import { createToastMachine as createMachine } from "./toast.machine"
6
+
7
+ export { anatomy } from "./toast.anatomy"
8
+ export { connect } from "./toast.connect"
9
+ export type {
10
+ GroupMachineContext,
11
+ MachineContext,
12
+ MachineState,
13
+ Placement,
14
+ Service,
15
+ ToastOptions,
16
+ Type,
17
+ } from "./toast.types"
18
+ export { createMachine }
19
+
20
+ export const group = {
21
+ connect: groupConnect,
22
+ machine: groupMachine,
23
+ }
24
+
25
+ export function api() {
26
+ if (!isDom()) {
27
+ warn("toast.api() is only available in the browser")
28
+ } else {
29
+ return toaster
30
+ }
31
+ }
@@ -0,0 +1,188 @@
1
+ import { subscribe } from "@zag-js/core"
2
+ import type { NormalizeProps, PropTypes } from "@zag-js/types"
3
+ import { runIfFn, uuid } from "@zag-js/utils"
4
+ import { parts } from "./toast.anatomy"
5
+ import { dom } from "./toast.dom"
6
+ import type {
7
+ GroupMachineContext,
8
+ GroupProps,
9
+ GroupSend,
10
+ GroupState,
11
+ Placement,
12
+ PromiseOptions,
13
+ Toaster,
14
+ Options,
15
+ } from "./toast.types"
16
+ import { getGroupPlacementStyle, getToastsByPlacement } from "./toast.utils"
17
+
18
+ export let toaster = {} as Toaster
19
+
20
+ export function groupConnect<T extends PropTypes>(state: GroupState, send: GroupSend, normalize: NormalizeProps<T>) {
21
+ const group = {
22
+ /**
23
+ * The total number of toasts
24
+ */
25
+ count: state.context.count,
26
+ /**
27
+ * The active toasts
28
+ */
29
+ toasts: state.context.toasts,
30
+ /**
31
+ * The active toasts by placement
32
+ */
33
+ toastsByPlacement: getToastsByPlacement(state.context.toasts),
34
+ /**
35
+ * Returns whether the toast id is visible
36
+ */
37
+ isVisible(id: string) {
38
+ if (!state.context.toasts.length) return false
39
+ return !!state.context.toasts.find((toast) => toast.id == id)
40
+ },
41
+ /**
42
+ * Function to create a toast.
43
+ */
44
+ create(options: Options) {
45
+ const uid = `toast:${uuid()}`
46
+ const id = options.id ? options.id : uid
47
+
48
+ if (group.isVisible(id)) return
49
+ send({ type: "ADD_TOAST", toast: { ...options, id } })
50
+
51
+ return id
52
+ },
53
+ /**
54
+ * Function to create or update a toast.
55
+ */
56
+ upsert(options: Options) {
57
+ const { id } = options
58
+ const isVisible = id ? group.isVisible(id) : false
59
+ if (isVisible && id != null) {
60
+ return group.update(id, options)
61
+ } else {
62
+ return group.create(options)
63
+ }
64
+ },
65
+ /**
66
+ * Function to dismiss a toast by id.
67
+ * If no id is provided, all toasts will be dismissed.
68
+ */
69
+ dismiss(id?: string) {
70
+ if (id == null) {
71
+ send("DISMISS_ALL")
72
+ } else if (group.isVisible(id)) {
73
+ send({ type: "DISMISS_TOAST", id })
74
+ }
75
+ },
76
+ /**
77
+ * Function to remove a toast by id.
78
+ * If no id is provided, all toasts will be removed.
79
+ */
80
+ remove(id?: string) {
81
+ if (id == null) {
82
+ send("REMOVE_ALL")
83
+ } else if (group.isVisible(id)) {
84
+ send({ type: "REMOVE_TOAST", id })
85
+ }
86
+ },
87
+ /**
88
+ * Function to dismiss all toasts by placement.
89
+ */
90
+ dismissByPlacement(placement: Placement) {
91
+ const toasts = group.toastsByPlacement[placement]
92
+ if (toasts) {
93
+ toasts.forEach((toast) => group.dismiss(toast.id))
94
+ }
95
+ },
96
+ /**
97
+ * Function to update a toast's options by id.
98
+ */
99
+ update(id: string, options: Options) {
100
+ if (!group.isVisible(id)) return
101
+ send({ type: "UPDATE_TOAST", id, toast: options })
102
+ return id
103
+ },
104
+ /**
105
+ * Function to create a loading toast.
106
+ */
107
+ loading(options: Options) {
108
+ options.type = "loading"
109
+ return group.upsert(options)
110
+ },
111
+ /**
112
+ * Function to create a success toast.
113
+ */
114
+ success(options: Options) {
115
+ options.type = "success"
116
+ return group.upsert(options)
117
+ },
118
+ /**
119
+ * Function to create an error toast.
120
+ */
121
+ error(options: Options) {
122
+ options.type = "error"
123
+ return group.upsert(options)
124
+ },
125
+ /**
126
+ * Function to create a toast from a promise.
127
+ * - When the promise resolves, the toast will be updated with the success options.
128
+ * - When the promise rejects, the toast will be updated with the error options.
129
+ */
130
+ promise<T>(promise: Promise<T>, options: PromiseOptions<T>, shared: Options = {}) {
131
+ const id = group.loading({ ...shared, ...options.loading })
132
+
133
+ promise
134
+ .then((response) => {
135
+ const successOptions = runIfFn(options.success, response)
136
+ group.success({ ...shared, ...successOptions, id })
137
+ })
138
+ .catch((error) => {
139
+ const errorOptions = runIfFn(options.error, error)
140
+ group.error({ ...shared, ...errorOptions, id })
141
+ })
142
+
143
+ return promise
144
+ },
145
+ /**
146
+ * Function to pause a toast by id.
147
+ */
148
+ pause(id?: string) {
149
+ if (id == null) {
150
+ send("PAUSE_ALL")
151
+ } else if (group.isVisible(id)) {
152
+ send({ type: "PAUSE_TOAST", id })
153
+ }
154
+ },
155
+ /**
156
+ * Function to resume a toast by id.
157
+ */
158
+ resume(id?: string) {
159
+ if (id == null) {
160
+ send("RESUME_ALL")
161
+ } else if (group.isVisible(id)) {
162
+ send({ type: "RESUME_TOAST", id })
163
+ }
164
+ },
165
+
166
+ getGroupProps(options: GroupProps) {
167
+ const { placement, label = "Notifications" } = options
168
+ return normalize.element({
169
+ ...parts.group.attrs,
170
+ tabIndex: -1,
171
+ "aria-label": label,
172
+ id: dom.getGroupId(placement),
173
+ "data-placement": placement,
174
+ "aria-live": "polite",
175
+ role: "region",
176
+ style: getGroupPlacementStyle(state.context, placement),
177
+ })
178
+ },
179
+
180
+ subscribe(fn: (toasts: GroupMachineContext["toasts"]) => void) {
181
+ return subscribe(state.context.toasts, () => fn(state.context.toasts))
182
+ },
183
+ }
184
+
185
+ Object.assign(toaster, group)
186
+
187
+ return group
188
+ }
@@ -0,0 +1,105 @@
1
+ import { createMachine } from "@zag-js/core"
2
+ import { MAX_Z_INDEX } from "@zag-js/dom-query"
3
+ import { compact } from "@zag-js/utils"
4
+ import { createToastMachine } from "./toast.machine"
5
+ import type { GroupMachineContext, UserDefinedGroupContext } from "./toast.types"
6
+
7
+ export function groupMachine(userContext: UserDefinedGroupContext) {
8
+ const ctx = compact(userContext)
9
+ return createMachine<GroupMachineContext>({
10
+ id: "toaster",
11
+ initial: "active",
12
+ context: {
13
+ dir: "ltr",
14
+ max: Number.MAX_SAFE_INTEGER,
15
+ toasts: [],
16
+ gutter: "1rem",
17
+ zIndex: MAX_Z_INDEX,
18
+ pauseOnPageIdle: false,
19
+ pauseOnInteraction: true,
20
+ offsets: { left: "0px", right: "0px", top: "0px", bottom: "0px" },
21
+ ...ctx,
22
+ },
23
+
24
+ computed: {
25
+ count: (ctx) => ctx.toasts.length,
26
+ },
27
+
28
+ on: {
29
+ SETUP: {},
30
+
31
+ PAUSE_TOAST: {
32
+ actions: (_ctx, evt, { self }) => {
33
+ self.sendChild("PAUSE", evt.id)
34
+ },
35
+ },
36
+
37
+ PAUSE_ALL: {
38
+ actions: (ctx) => {
39
+ ctx.toasts.forEach((toast) => toast.send("PAUSE"))
40
+ },
41
+ },
42
+
43
+ RESUME_TOAST: {
44
+ actions: (_ctx, evt, { self }) => {
45
+ self.sendChild("RESUME", evt.id)
46
+ },
47
+ },
48
+
49
+ RESUME_ALL: {
50
+ actions: (ctx) => {
51
+ ctx.toasts.forEach((toast) => toast.send("RESUME"))
52
+ },
53
+ },
54
+
55
+ ADD_TOAST: {
56
+ guard: (ctx) => ctx.toasts.length < ctx.max,
57
+ actions: (ctx, evt, { self }) => {
58
+ const options = {
59
+ ...evt.toast,
60
+ pauseOnPageIdle: ctx.pauseOnPageIdle,
61
+ pauseOnInteraction: ctx.pauseOnInteraction,
62
+ dir: ctx.dir,
63
+ getRootNode: ctx.getRootNode,
64
+ }
65
+ const toast = createToastMachine(options)
66
+ const actor = self.spawn(toast)
67
+ ctx.toasts.push(actor)
68
+ },
69
+ },
70
+
71
+ UPDATE_TOAST: {
72
+ actions: (_ctx, evt, { self }) => {
73
+ self.sendChild({ type: "UPDATE", toast: evt.toast }, evt.id)
74
+ },
75
+ },
76
+
77
+ DISMISS_TOAST: {
78
+ actions: (_ctx, evt, { self }) => {
79
+ self.sendChild("DISMISS", evt.id)
80
+ },
81
+ },
82
+
83
+ DISMISS_ALL: {
84
+ actions: (ctx) => {
85
+ ctx.toasts.forEach((toast) => toast.send("DISMISS"))
86
+ },
87
+ },
88
+
89
+ REMOVE_TOAST: {
90
+ actions: (ctx, evt, { self }) => {
91
+ self.stopChild(evt.id)
92
+ const index = ctx.toasts.findIndex((toast) => toast.id === evt.id)
93
+ ctx.toasts.splice(index, 1)
94
+ },
95
+ },
96
+
97
+ REMOVE_ALL: {
98
+ actions: (ctx, _evt, { self }) => {
99
+ ctx.toasts.forEach((toast) => self.stopChild(toast.id))
100
+ while (ctx.toasts.length) ctx.toasts.pop()
101
+ },
102
+ },
103
+ },
104
+ })
105
+ }
@@ -0,0 +1,4 @@
1
+ import { createAnatomy } from "@zag-js/anatomy"
2
+
3
+ export const anatomy = createAnatomy("toast").parts("group", "root", "title", "description", "closeTrigger")
4
+ export const parts = anatomy.build()
@@ -0,0 +1,143 @@
1
+ import { dataAttr } from "@zag-js/dom-query"
2
+ import type { NormalizeProps, PropTypes } from "@zag-js/types"
3
+ import { parts } from "./toast.anatomy"
4
+ import { dom } from "./toast.dom"
5
+ import type { Send, State } from "./toast.types"
6
+
7
+ export function connect<T extends PropTypes>(state: State, send: Send, normalize: NormalizeProps<T>) {
8
+ const isVisible = state.hasTag("visible")
9
+ const isPaused = state.hasTag("paused")
10
+
11
+ const pauseOnInteraction = state.context.pauseOnInteraction
12
+ const placement = state.context.placement
13
+
14
+ return {
15
+ /**
16
+ * The type of the toast.
17
+ */
18
+ type: state.context.type,
19
+ /**
20
+ * The title of the toast.
21
+ */
22
+ title: state.context.title,
23
+ /**
24
+ * The description of the toast.
25
+ */
26
+ description: state.context.description,
27
+ /**
28
+ * The current placement of the toast.
29
+ */
30
+ placement,
31
+ /**
32
+ * Whether the toast is visible.
33
+ */
34
+ isVisible,
35
+ /**
36
+ * Whether the toast is paused.
37
+ */
38
+ isPaused,
39
+ /**
40
+ * Whether the toast is in RTL mode.
41
+ */
42
+ isRtl: state.context.dir === "rtl",
43
+ /**
44
+ * Function to pause the toast (keeping it visible).
45
+ */
46
+ pause() {
47
+ send("PAUSE")
48
+ },
49
+ /**
50
+ * Function to resume the toast dismissing.
51
+ */
52
+ resume() {
53
+ send("RESUME")
54
+ },
55
+ /**
56
+ * Function to instantly dismiss the toast.
57
+ */
58
+ dismiss() {
59
+ send("DISMISS")
60
+ },
61
+ /**
62
+ * Function render the toast in the DOM (based on the defined `render` property)
63
+ */
64
+ render() {
65
+ return state.context.render?.({
66
+ id: state.context.id,
67
+ type: state.context.type,
68
+ duration: state.context.duration,
69
+ title: state.context.title,
70
+ placement: state.context.placement,
71
+ description: state.context.description,
72
+ dismiss() {
73
+ send("DISMISS")
74
+ },
75
+ })
76
+ },
77
+
78
+ rootProps: normalize.element({
79
+ ...parts.root.attrs,
80
+ dir: state.context.dir,
81
+ id: dom.getRootId(state.context),
82
+ "data-open": dataAttr(isVisible),
83
+ "data-type": state.context.type,
84
+ "data-placement": placement,
85
+ role: "status",
86
+ "aria-atomic": "true",
87
+ tabIndex: 0,
88
+ style: {
89
+ position: "relative",
90
+ pointerEvents: "auto",
91
+ margin: "calc(var(--toast-gutter) / 2)",
92
+ "--remove-delay": `${state.context.removeDelay}ms`,
93
+ "--duration": `${state.context.duration}ms`,
94
+ },
95
+ onKeyDown(event) {
96
+ if (event.key == "Escape") {
97
+ send("DISMISS")
98
+ event.preventDefault()
99
+ }
100
+ },
101
+ onFocus() {
102
+ if (pauseOnInteraction) {
103
+ send("PAUSE")
104
+ }
105
+ },
106
+ onBlur() {
107
+ if (pauseOnInteraction) {
108
+ send("RESUME")
109
+ }
110
+ },
111
+ onPointerEnter() {
112
+ if (pauseOnInteraction) {
113
+ send("PAUSE")
114
+ }
115
+ },
116
+ onPointerLeave() {
117
+ if (pauseOnInteraction) {
118
+ send("RESUME")
119
+ }
120
+ },
121
+ }),
122
+
123
+ titleProps: normalize.element({
124
+ ...parts.title.attrs,
125
+ id: dom.getTitleId(state.context),
126
+ }),
127
+
128
+ descriptionProps: normalize.element({
129
+ ...parts.description.attrs,
130
+ id: dom.getDescriptionId(state.context),
131
+ }),
132
+
133
+ closeTriggerProps: normalize.button({
134
+ id: dom.getCloseTriggerId(state.context),
135
+ ...parts.closeTrigger.attrs,
136
+ type: "button",
137
+ "aria-label": "Dismiss notification",
138
+ onClick() {
139
+ send("DISMISS")
140
+ },
141
+ }),
142
+ }
143
+ }
@@ -0,0 +1,11 @@
1
+ import { createScope } from "@zag-js/dom-query"
2
+ import type { GroupMachineContext as GroupCtx, MachineContext as Ctx, Placement } from "./toast.types"
3
+
4
+ export const dom = createScope({
5
+ getGroupId: (placement: Placement) => `toast-group:${placement}`,
6
+ getRootId: (ctx: Ctx) => `toast:${ctx.id}`,
7
+ getTitleId: (ctx: Ctx) => `toast:${ctx.id}:title`,
8
+ getDescriptionId: (ctx: Ctx) => `toast:${ctx.id}:description`,
9
+ getCloseTriggerId: (ctx: Ctx) => `toast${ctx.id}:close`,
10
+ getPortalId: (ctx: GroupCtx) => `toast-portal:${ctx.id}`,
11
+ })
@@ -0,0 +1,155 @@
1
+ import { createMachine, guards } from "@zag-js/core"
2
+ import { addDomEvent } from "@zag-js/dom-event"
3
+ import { compact } from "@zag-js/utils"
4
+ import { dom } from "./toast.dom"
5
+ import type { MachineContext, MachineState, Options } from "./toast.types"
6
+ import { getToastDuration } from "./toast.utils"
7
+
8
+ const { not, and, or } = guards
9
+
10
+ export function createToastMachine(options: Options = {}) {
11
+ const { type = "info", duration, id = "toast", placement = "bottom", removeDelay = 0, ...restProps } = options
12
+ const ctx = compact(restProps)
13
+
14
+ const computedDuration = getToastDuration(duration, type)
15
+
16
+ return createMachine<MachineContext, MachineState>(
17
+ {
18
+ id,
19
+ entry: "invokeOnOpen",
20
+ initial: type === "loading" ? "persist" : "active",
21
+ context: {
22
+ id,
23
+ type,
24
+ remaining: computedDuration,
25
+ duration: computedDuration,
26
+ removeDelay,
27
+ createdAt: Date.now(),
28
+ placement,
29
+ ...ctx,
30
+ },
31
+
32
+ on: {
33
+ UPDATE: [
34
+ {
35
+ guard: and("hasTypeChanged", "isChangingToLoading"),
36
+ target: "persist",
37
+ actions: ["setContext", "invokeOnUpdate"],
38
+ },
39
+ {
40
+ guard: or("hasDurationChanged", "hasTypeChanged"),
41
+ target: "active:temp",
42
+ actions: ["setContext", "invokeOnUpdate"],
43
+ },
44
+ {
45
+ actions: ["setContext", "invokeOnUpdate"],
46
+ },
47
+ ],
48
+ },
49
+
50
+ states: {
51
+ "active:temp": {
52
+ tags: ["visible", "updating"],
53
+ after: {
54
+ 0: "active",
55
+ },
56
+ },
57
+
58
+ persist: {
59
+ tags: ["visible", "paused"],
60
+ activities: "trackDocumentVisibility",
61
+ on: {
62
+ RESUME: {
63
+ guard: not("isLoadingType"),
64
+ target: "active",
65
+ actions: ["setCreatedAt"],
66
+ },
67
+ DISMISS: "dismissing",
68
+ },
69
+ },
70
+
71
+ active: {
72
+ tags: ["visible"],
73
+ activities: "trackDocumentVisibility",
74
+ after: {
75
+ VISIBLE_DURATION: "dismissing",
76
+ },
77
+ on: {
78
+ DISMISS: "dismissing",
79
+ PAUSE: {
80
+ target: "persist",
81
+ actions: "setRemainingDuration",
82
+ },
83
+ },
84
+ },
85
+
86
+ dismissing: {
87
+ entry: "invokeOnClosing",
88
+ after: {
89
+ REMOVE_DELAY: {
90
+ target: "inactive",
91
+ actions: "notifyParentToRemove",
92
+ },
93
+ },
94
+ },
95
+
96
+ inactive: {
97
+ entry: "invokeOnClose",
98
+ type: "final",
99
+ },
100
+ },
101
+ },
102
+ {
103
+ activities: {
104
+ trackDocumentVisibility(ctx, _evt, { send }) {
105
+ if (!ctx.pauseOnPageIdle) return
106
+ const doc = dom.getDoc(ctx)
107
+ return addDomEvent(doc, "visibilitychange", () => {
108
+ send(doc.visibilityState === "hidden" ? "PAUSE" : "RESUME")
109
+ })
110
+ },
111
+ },
112
+
113
+ guards: {
114
+ isChangingToLoading: (_, evt) => evt.toast?.type === "loading",
115
+ isLoadingType: (ctx) => ctx.type === "loading",
116
+ hasTypeChanged: (ctx, evt) => evt.toast?.type !== ctx.type,
117
+ hasDurationChanged: (ctx, evt) => evt.toast?.duration !== ctx.duration,
118
+ },
119
+
120
+ delays: {
121
+ VISIBLE_DURATION: (ctx) => ctx.remaining,
122
+ REMOVE_DELAY: (ctx) => ctx.removeDelay,
123
+ },
124
+
125
+ actions: {
126
+ setRemainingDuration(ctx) {
127
+ ctx.remaining -= Date.now() - ctx.createdAt
128
+ },
129
+ setCreatedAt(ctx) {
130
+ ctx.createdAt = Date.now()
131
+ },
132
+ notifyParentToRemove(_ctx, _evt, { self }) {
133
+ self.sendParent({ type: "REMOVE_TOAST", id: self.id })
134
+ },
135
+ invokeOnClosing(ctx) {
136
+ ctx.onClosing?.()
137
+ },
138
+ invokeOnClose(ctx) {
139
+ ctx.onClose?.()
140
+ },
141
+ invokeOnOpen(ctx) {
142
+ ctx.onOpen?.()
143
+ },
144
+ invokeOnUpdate(ctx) {
145
+ ctx.onUpdate?.()
146
+ },
147
+ setContext(ctx, evt) {
148
+ const { duration, type } = evt.toast
149
+ const time = getToastDuration(duration, type)
150
+ Object.assign(ctx, { ...evt.toast, duration: time, remaining: time })
151
+ },
152
+ },
153
+ },
154
+ )
155
+ }
@@ -0,0 +1,180 @@
1
+ import type { Machine, StateMachine as S } from "@zag-js/core"
2
+ import type { CommonProperties, Context, Direction, DirectionProperty, RequiredBy, RootProperties } from "@zag-js/types"
3
+
4
+ export type Type = "success" | "error" | "loading" | "info" | "custom"
5
+
6
+ export type Placement = "top-start" | "top" | "top-end" | "bottom-start" | "bottom" | "bottom-end"
7
+
8
+ type SharedContext = {
9
+ /**
10
+ * Whether to pause toast when the user leaves the browser tab
11
+ */
12
+ pauseOnPageIdle?: boolean
13
+ /**
14
+ * Whether to pause the toast when interacted with
15
+ */
16
+ pauseOnInteraction?: boolean
17
+ }
18
+
19
+ export type ToastOptions = {
20
+ /**
21
+ * The unique id of the toast
22
+ */
23
+ id: string
24
+ /**
25
+ * The type of the toast
26
+ */
27
+ type: Type
28
+ /**
29
+ * The placement of the toast
30
+ */
31
+ placement: Placement
32
+ /**
33
+ * The message of the toast
34
+ */
35
+ title?: string
36
+ /**
37
+ * The description of the toast
38
+ */
39
+ description?: string
40
+ /**
41
+ * The duration the toast will be visible
42
+ */
43
+ duration: number
44
+ /**
45
+ * Custom function to render the toast element.
46
+ */
47
+ render?: (options: RenderOptions) => any
48
+ /**
49
+ * The duration for the toast to kept alive before it is removed.
50
+ * Useful for exit transitions.
51
+ */
52
+ removeDelay?: number
53
+ /**
54
+ * Function called when the toast has been closed and removed
55
+ */
56
+ onClose?: VoidFunction
57
+ /**
58
+ * Function called when the toast is leaving
59
+ */
60
+ onClosing?: VoidFunction
61
+ /**
62
+ * Function called when the toast is shown
63
+ */
64
+ onOpen?: VoidFunction
65
+ /**
66
+ * Function called when the toast is updated
67
+ */
68
+ onUpdate?: VoidFunction
69
+ }
70
+
71
+ export type Options = Partial<ToastOptions>
72
+
73
+ export type RenderOptions = Omit<ToastOptions, "render"> & {
74
+ dismiss(): void
75
+ }
76
+
77
+ export type MachineContext = SharedContext &
78
+ RootProperties &
79
+ CommonProperties &
80
+ Omit<ToastOptions, "removeDelay"> & {
81
+ /**
82
+ * The duration for the toast to kept alive before it is removed.
83
+ * Useful for exit transitions.
84
+ */
85
+ removeDelay: number
86
+ /**
87
+ * The document's text/writing direction.
88
+ */
89
+ dir?: Direction
90
+ /**
91
+ * The time the toast was created
92
+ */
93
+ createdAt: number
94
+ /**
95
+ * The time left before the toast is removed
96
+ */
97
+ remaining: number
98
+ }
99
+
100
+ export type MachineState = {
101
+ value: "active" | "active:temp" | "dismissing" | "inactive" | "persist"
102
+ tags: "visible" | "paused" | "updating"
103
+ }
104
+
105
+ export type State = S.State<MachineContext, MachineState>
106
+
107
+ export type Send = S.Send
108
+
109
+ export type Service = Machine<MachineContext, MachineState>
110
+
111
+ type GroupPublicContext = SharedContext &
112
+ DirectionProperty &
113
+ CommonProperties & {
114
+ /**
115
+ * The gutter or spacing between toasts
116
+ */
117
+ gutter: string
118
+ /**
119
+ * The z-index applied to each toast group
120
+ */
121
+ zIndex: number
122
+ /**
123
+ * The maximum number of toasts that can be shown at once
124
+ */
125
+ max: number
126
+ /**
127
+ * The offset from the safe environment edge of the viewport
128
+ */
129
+ offsets: string | Record<"left" | "right" | "bottom" | "top", string>
130
+ }
131
+
132
+ export type UserDefinedGroupContext = RequiredBy<GroupPublicContext, "id">
133
+
134
+ type GroupComputedContext = Readonly<{
135
+ /**
136
+ * @computed
137
+ * The total number of toasts in the group
138
+ */
139
+ readonly count: number
140
+ }>
141
+
142
+ type GroupPrivateContext = Context<{
143
+ /**
144
+ * @internal
145
+ * The child toast machines (spawned by the toast group)
146
+ */
147
+ toasts: Service[]
148
+ }>
149
+
150
+ export type GroupMachineContext = GroupPublicContext & GroupComputedContext & GroupPrivateContext
151
+
152
+ export type GroupState = S.State<GroupMachineContext>
153
+
154
+ export type GroupSend = (event: S.Event<S.AnyEventObject>) => void
155
+
156
+ type MaybeFunction<Value, Args> = Value | ((arg: Args) => Value)
157
+
158
+ export type PromiseOptions<Value> = {
159
+ loading: ToastOptions
160
+ success: MaybeFunction<ToastOptions, Value>
161
+ error: MaybeFunction<ToastOptions, Error>
162
+ }
163
+
164
+ export type GroupProps = {
165
+ placement: Placement
166
+ label?: string
167
+ }
168
+
169
+ export type Toaster = {
170
+ count: number
171
+ isVisible(id: string): boolean
172
+ upsert(options: ToastOptions): string | undefined
173
+ create(options: ToastOptions): string | undefined
174
+ success(options: ToastOptions): string | undefined
175
+ error(options: ToastOptions): string | undefined
176
+ loading(options: ToastOptions): string | undefined
177
+ dismiss(id?: string | undefined): void
178
+ remove(id?: string | undefined): void
179
+ promise<T>(promise: Promise<T>, options: PromiseOptions<T>, shared?: ToastOptions): Promise<T>
180
+ }
@@ -0,0 +1,77 @@
1
+ import type { Style } from "@zag-js/types"
2
+ import type { GroupMachineContext, MachineContext, Placement, Service, Type } from "./toast.types"
3
+
4
+ export function getToastsByPlacement(toasts: Service[]) {
5
+ const result: Partial<Record<Placement, Service[]>> = {}
6
+
7
+ for (const toast of toasts) {
8
+ const placement = toast.state.context.placement!
9
+ result[placement] ||= []
10
+ result[placement]!.push(toast)
11
+ }
12
+
13
+ return result
14
+ }
15
+
16
+ export const defaultTimeouts: Record<Type, number> = {
17
+ info: 5000,
18
+ error: 5000,
19
+ success: 2000,
20
+ loading: Infinity,
21
+ custom: 5000,
22
+ }
23
+
24
+ export function getToastDuration(duration: number | undefined, type: MachineContext["type"]) {
25
+ return duration ?? defaultTimeouts[type]
26
+ }
27
+
28
+ export function getGroupPlacementStyle(ctx: GroupMachineContext, placement: Placement): Style {
29
+ const offset = ctx.offsets
30
+ const computedOffset =
31
+ typeof offset === "string" ? { left: offset, right: offset, bottom: offset, top: offset } : offset
32
+
33
+ const rtl = ctx.dir === "rtl"
34
+ const computedPlacement = placement
35
+ .replace("-start", rtl ? "-right" : "-left")
36
+ .replace("-end", rtl ? "-left" : "-right")
37
+
38
+ const isRighty = computedPlacement.includes("right")
39
+ const isLefty = computedPlacement.includes("left")
40
+
41
+ const styles: Style = {
42
+ position: "fixed",
43
+ pointerEvents: ctx.count > 0 ? undefined : "none",
44
+ display: "flex",
45
+ flexDirection: "column",
46
+ "--toast-gutter": ctx.gutter,
47
+ zIndex: ctx.zIndex,
48
+ }
49
+
50
+ let alignItems: Style["alignItems"] = "center"
51
+ if (isRighty) alignItems = "flex-end"
52
+ if (isLefty) alignItems = "flex-start"
53
+
54
+ styles.alignItems = alignItems
55
+
56
+ if (computedPlacement.includes("top")) {
57
+ const offset = computedOffset.top
58
+ styles.top = `calc(env(safe-area-inset-top, 0px) + ${offset})`
59
+ }
60
+
61
+ if (computedPlacement.includes("bottom")) {
62
+ const offset = computedOffset.bottom
63
+ styles.bottom = `calc(env(safe-area-inset-bottom, 0px) + ${offset})`
64
+ }
65
+
66
+ if (!computedPlacement.includes("left")) {
67
+ const offset = computedOffset.right
68
+ styles.right = `calc(env(safe-area-inset-right, 0px) + ${offset})`
69
+ }
70
+
71
+ if (!computedPlacement.includes("right")) {
72
+ const offset = computedOffset.left
73
+ styles.left = `calc(env(safe-area-inset-left, 0px) + ${offset})`
74
+ }
75
+
76
+ return styles
77
+ }