@spunto/design-system 0.2.1 → 0.3.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
@@ -41,6 +41,53 @@ const nextConfig = { transpilePackages: ["@spunto/design-system"] }
41
41
  - **Primitives** — `Button` (+`buttonVariants`), `Card` (+parts), `Input`, `Textarea`,
42
42
  `Label`, `Badge` (+`badgeVariants`), `Switch`. Dependency-light (cva + clsx +
43
43
  tailwind-merge), no component framework required.
44
+ - **`SpuntoProvider` + `toast()`** — the imperative toast system (see below).
45
+
46
+ ## Toasts — `SpuntoProvider` + `toast()`
47
+
48
+ `SpuntoProvider` is the design system's client umbrella provider. Mount it once
49
+ around your app; today it renders the toast viewport, and it's the extension
50
+ point future overlay features plug into. `toast()` is a sonner-style imperative
51
+ API callable from **anywhere** — event handlers, async code, even outside the
52
+ React tree (it's backed by Base UI's `createToastManager`).
53
+
54
+ In a Next.js App Router app, the root layout stays a Server Component — just
55
+ render the (client) provider around `{children}`:
56
+
57
+ ```tsx
58
+ // app/layout.tsx
59
+ import { SpuntoProvider } from "@spunto/design-system"
60
+ import "@spunto/design-system/styles.css"
61
+
62
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
63
+ return (
64
+ <html lang="en">
65
+ <body>
66
+ <SpuntoProvider toastPosition="top-right">{children}</SpuntoProvider>
67
+ </body>
68
+ </html>
69
+ )
70
+ }
71
+ ```
72
+
73
+ Then fire toasts from anywhere:
74
+
75
+ ```tsx
76
+ import { toast } from "@spunto/design-system"
77
+
78
+ toast.success("Worker ready", { description: "spunto-abc is up" })
79
+ toast.error("Deploy failed", { action: { label: "Retry", onClick: redeploy } })
80
+
81
+ // Persistent (no auto-dismiss) + manual control:
82
+ const id = toast.info("Deploying…", { duration: 0 })
83
+ toast.dismiss(id)
84
+ ```
85
+
86
+ `toast(message, options)` and the `toast.success/error/warning/info` helpers take
87
+ `{ title?, description?, variant?, duration?, action? }` and return the toast id;
88
+ `toast.dismiss(id?)` closes one toast (or all). `SpuntoProvider` accepts
89
+ `toastPosition` (default `"top-right"`), `toastDuration` (default `5000`, `0` =
90
+ persistent) and `toastLimit` (default `3`).
44
91
 
45
92
  ## Peer requirements
46
93
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spunto/design-system",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Spunto's shared design system — warm/flame tokens, color constants, and UI primitives.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,77 @@
1
+ "use client"
2
+
3
+ import type { ReactNode } from "react"
4
+ import { Toast as ToastPrimitive } from "@base-ui/react/toast"
5
+
6
+ import { ToastList, toastManager, type ToastSwipeDirection } from "./toast"
7
+
8
+ export type ToastPosition =
9
+ | "top-left"
10
+ | "top-center"
11
+ | "top-right"
12
+ | "bottom-left"
13
+ | "bottom-center"
14
+ | "bottom-right"
15
+
16
+ export interface SpuntoProviderProps {
17
+ /** App tree. */
18
+ children?: ReactNode
19
+ /**
20
+ * Where the toast stack docks.
21
+ * @default "top-right"
22
+ */
23
+ toastPosition?: ToastPosition
24
+ /**
25
+ * Default auto-dismiss delay (ms) for toasts that don't set their own.
26
+ * `0` keeps toasts until dismissed.
27
+ * @default 5000
28
+ */
29
+ toastDuration?: number
30
+ /**
31
+ * Maximum number of toasts stacked at once. Older toasts collapse behind the
32
+ * newest and animate out past this count.
33
+ * @default 3
34
+ */
35
+ toastLimit?: number
36
+ }
37
+
38
+ // Toasts swipe toward the nearest screen edge for the current position.
39
+ const SWIPE_DIRECTIONS: Record<ToastPosition, ToastSwipeDirection[]> = {
40
+ "top-left": ["up", "left"],
41
+ "top-center": ["up"],
42
+ "top-right": ["up", "right"],
43
+ "bottom-left": ["down", "left"],
44
+ "bottom-center": ["down"],
45
+ "bottom-right": ["down", "right"],
46
+ }
47
+
48
+ /**
49
+ * Client-side umbrella provider for the Spunto design system. Wrap your app once
50
+ * (in a Next.js `app/layout.tsx`, it's a client component — render it around
51
+ * `{children}` inside the server layout, no `"use client"` needed on the layout
52
+ * itself). Today it mounts the toast viewport; it's the seam future overlay
53
+ * features (dialogs, tooltips, theme config) plug into without a breaking change.
54
+ *
55
+ * ```tsx
56
+ * <SpuntoProvider toastPosition="bottom-right">
57
+ * {children}
58
+ * </SpuntoProvider>
59
+ * ```
60
+ */
61
+ export function SpuntoProvider({
62
+ children,
63
+ toastPosition = "top-right",
64
+ toastDuration = 5000,
65
+ toastLimit = 3,
66
+ }: SpuntoProviderProps) {
67
+ return (
68
+ <ToastPrimitive.Provider toastManager={toastManager} timeout={toastDuration} limit={toastLimit}>
69
+ {children}
70
+ <ToastPrimitive.Portal>
71
+ <ToastPrimitive.Viewport data-slot="toast-viewport" data-position={toastPosition}>
72
+ <ToastList swipeDirection={SWIPE_DIRECTIONS[toastPosition]} />
73
+ </ToastPrimitive.Viewport>
74
+ </ToastPrimitive.Portal>
75
+ </ToastPrimitive.Provider>
76
+ )
77
+ }
@@ -0,0 +1,199 @@
1
+ "use client"
2
+
3
+ import type { ReactNode } from "react"
4
+ import { Toast as ToastPrimitive } from "@base-ui/react/toast"
5
+
6
+ import { cn } from "../utils"
7
+
8
+ export type ToastVariant = "success" | "error" | "warning" | "info"
9
+
10
+ /** Optional call-to-action rendered as a button inside the toast. */
11
+ export interface ToastAction {
12
+ label: string
13
+ onClick: () => void
14
+ }
15
+
16
+ export interface ToastOptions {
17
+ /** Overrides the first positional argument as the toast's heading. */
18
+ title?: ReactNode
19
+ /** Secondary line under the title. */
20
+ description?: ReactNode
21
+ /** Colour + icon. Defaults to a neutral toast when omitted. */
22
+ variant?: ToastVariant
23
+ /**
24
+ * Auto-dismiss delay in ms. `0` keeps the toast until dismissed.
25
+ * Falls back to the `SpuntoProvider` `toastDuration` when omitted.
26
+ */
27
+ duration?: number
28
+ /** Optional action button. Clicking it also dismisses the toast. */
29
+ action?: ToastAction
30
+ }
31
+
32
+ /** Payload we stash on each Base UI toast so the renderer can style it. */
33
+ export interface SpuntoToastData {
34
+ variant?: ToastVariant
35
+ action?: ToastAction
36
+ }
37
+
38
+ /**
39
+ * Singleton manager shared by the imperative `toast()` API and the
40
+ * `SpuntoProvider` that renders it. Created with Base UI's `createToastManager`
41
+ * so `toast()` is callable from anywhere — event handlers, async code, even
42
+ * outside the React tree.
43
+ */
44
+ export const toastManager = ToastPrimitive.createToastManager<SpuntoToastData>()
45
+
46
+ function emit(message: ReactNode, options: ToastOptions = {}, variant?: ToastVariant): string {
47
+ const resolved = variant ?? options.variant
48
+ return toastManager.add({
49
+ title: options.title ?? message,
50
+ description: options.description,
51
+ // `undefined` lets the provider's default timeout apply.
52
+ timeout: options.duration,
53
+ type: resolved,
54
+ data: { variant: resolved, action: options.action },
55
+ })
56
+ }
57
+
58
+ export interface ToastFn {
59
+ (message: ReactNode, options?: ToastOptions): string
60
+ success: (message: ReactNode, options?: ToastOptions) => string
61
+ error: (message: ReactNode, options?: ToastOptions) => string
62
+ warning: (message: ReactNode, options?: ToastOptions) => string
63
+ info: (message: ReactNode, options?: ToastOptions) => string
64
+ /** Dismiss a specific toast by id, or all toasts when called without one. */
65
+ dismiss: (id?: string) => void
66
+ }
67
+
68
+ /**
69
+ * Imperative, sonner-style toast API. Import and call from anywhere.
70
+ *
71
+ * ```ts
72
+ * toast.success("Worker ready", { description: "spunto-abc is up" })
73
+ * const id = toast("Deploying…", { duration: 0 })
74
+ * toast.dismiss(id)
75
+ * ```
76
+ */
77
+ export const toast: ToastFn = Object.assign(
78
+ (message: ReactNode, options?: ToastOptions) => emit(message, options),
79
+ {
80
+ success: (message: ReactNode, options?: ToastOptions) => emit(message, options, "success"),
81
+ error: (message: ReactNode, options?: ToastOptions) => emit(message, options, "error"),
82
+ warning: (message: ReactNode, options?: ToastOptions) => emit(message, options, "warning"),
83
+ info: (message: ReactNode, options?: ToastOptions) => emit(message, options, "info"),
84
+ dismiss: (id?: string) => toastManager.close(id),
85
+ }
86
+ )
87
+
88
+ /** Per-variant accent (icon + left border). Derived from the status tokens. */
89
+ const VARIANT_ACCENT: Record<ToastVariant, string> = {
90
+ success: "text-success border-l-success",
91
+ error: "text-destructive border-l-destructive",
92
+ warning: "text-warning border-l-warning",
93
+ info: "text-info border-l-info",
94
+ }
95
+
96
+ function VariantIcon({ variant }: { variant?: ToastVariant }) {
97
+ const common = { width: 18, height: 18, viewBox: "0 0 24 24", fill: "none", "aria-hidden": true } as const
98
+ switch (variant) {
99
+ case "success":
100
+ return (
101
+ <svg {...common} stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
102
+ <circle cx="12" cy="12" r="9" />
103
+ <path d="m8.5 12 2.5 2.5 4.5-5" />
104
+ </svg>
105
+ )
106
+ case "error":
107
+ return (
108
+ <svg {...common} stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
109
+ <circle cx="12" cy="12" r="9" />
110
+ <path d="M15 9l-6 6M9 9l6 6" />
111
+ </svg>
112
+ )
113
+ case "warning":
114
+ return (
115
+ <svg {...common} stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
116
+ <path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z" />
117
+ <path d="M12 9v4M12 17h.01" />
118
+ </svg>
119
+ )
120
+ case "info":
121
+ return (
122
+ <svg {...common} stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
123
+ <circle cx="12" cy="12" r="9" />
124
+ <path d="M12 11v5M12 8h.01" />
125
+ </svg>
126
+ )
127
+ default:
128
+ return (
129
+ <svg {...common} stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
130
+ <path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
131
+ <path d="M10.3 21a1.94 1.94 0 0 0 3.4 0" />
132
+ </svg>
133
+ )
134
+ }
135
+ }
136
+
137
+ function CloseIcon() {
138
+ return (
139
+ <svg width={14} height={14} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden>
140
+ <path d="M18 6 6 18M6 6l12 12" />
141
+ </svg>
142
+ )
143
+ }
144
+
145
+ /**
146
+ * Renders the live toast stack. Consumes the manager via `useToastManager`, so
147
+ * it must live inside a `Toast.Provider` — `SpuntoProvider` handles that.
148
+ * @internal
149
+ */
150
+ export type ToastSwipeDirection = "up" | "down" | "left" | "right"
151
+
152
+ export function ToastList({ swipeDirection }: { swipeDirection?: ToastSwipeDirection[] }) {
153
+ const { toasts } = ToastPrimitive.useToastManager<SpuntoToastData>()
154
+
155
+ return toasts.map((item) => {
156
+ const variant = item.data?.variant
157
+ const action = item.data?.action
158
+ return (
159
+ <ToastPrimitive.Root
160
+ key={item.id}
161
+ toast={item}
162
+ swipeDirection={swipeDirection}
163
+ data-slot="toast"
164
+ className={cn(
165
+ "pointer-events-auto rounded-lg border border-border border-l-4 bg-popover text-popover-foreground shadow-lg shadow-black/[0.06] outline-none",
166
+ "focus-visible:ring-2 focus-visible:ring-ring/50",
167
+ variant ? VARIANT_ACCENT[variant] : "border-l-muted-foreground/40"
168
+ )}
169
+ >
170
+ <ToastPrimitive.Content className="flex h-full items-start gap-3 p-3.5 transition-opacity duration-200 data-behind:opacity-0 data-expanded:opacity-100">
171
+ <span className={cn("mt-px shrink-0", variant ? VARIANT_ACCENT[variant] : "text-muted-foreground")}>
172
+ <VariantIcon variant={variant} />
173
+ </span>
174
+ <div className="flex min-w-0 flex-1 flex-col gap-1">
175
+ <ToastPrimitive.Title className="text-sm leading-snug font-semibold text-foreground" />
176
+ <ToastPrimitive.Description className="text-sm leading-snug text-muted-foreground" />
177
+ {action && (
178
+ <ToastPrimitive.Action
179
+ className="mt-1.5 inline-flex h-7 w-fit items-center rounded-md border border-border bg-background px-2.5 text-xs font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring/50 outline-none"
180
+ onClick={() => {
181
+ action.onClick()
182
+ toastManager.close(item.id)
183
+ }}
184
+ >
185
+ {action.label}
186
+ </ToastPrimitive.Action>
187
+ )}
188
+ </div>
189
+ <ToastPrimitive.Close
190
+ aria-label="Fermer"
191
+ className="-mt-1 -mr-1 flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground/70 transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 outline-none"
192
+ >
193
+ <CloseIcon />
194
+ </ToastPrimitive.Close>
195
+ </ToastPrimitive.Content>
196
+ </ToastPrimitive.Root>
197
+ )
198
+ })
199
+ }
package/src/index.ts CHANGED
@@ -14,3 +14,10 @@ export { Textarea } from "./components/textarea"
14
14
  export { Label } from "./components/label"
15
15
  export { Badge, badgeVariants } from "./components/badge"
16
16
  export { Switch } from "./components/switch"
17
+
18
+ // Toasts — imperative `toast()` API + the umbrella client provider that renders
19
+ // them. `SpuntoProvider` is the extension point for future overlay features.
20
+ export { SpuntoProvider } from "./components/spunto-provider"
21
+ export type { SpuntoProviderProps, ToastPosition } from "./components/spunto-provider"
22
+ export { toast } from "./components/toast"
23
+ export type { ToastFn, ToastOptions, ToastVariant, ToastAction } from "./components/toast"
package/styles.css CHANGED
@@ -33,6 +33,13 @@
33
33
  --color-input: var(--input);
34
34
  --color-border: var(--border);
35
35
  --color-destructive: var(--destructive);
36
+ /* Status semantics — used by feedback surfaces (Toast…) */
37
+ --color-success: var(--success);
38
+ --color-success-foreground: var(--success-foreground);
39
+ --color-warning: var(--warning);
40
+ --color-warning-foreground: var(--warning-foreground);
41
+ --color-info: var(--info);
42
+ --color-info-foreground: var(--info-foreground);
36
43
  --color-accent-foreground: var(--accent-foreground);
37
44
  --color-accent: var(--accent);
38
45
  --color-muted-foreground: var(--muted-foreground);
@@ -77,6 +84,13 @@
77
84
  --accent: oklch(0.935 0.024 72);
78
85
  --accent-foreground: oklch(0.34 0.045 52);
79
86
  --destructive: oklch(0.577 0.245 27.325);
87
+ /* Status semantics — warm-leaning green / amber / blue, tuned for the cream bg */
88
+ --success: oklch(0.62 0.145 152);
89
+ --success-foreground: oklch(0.99 0.012 86);
90
+ --warning: oklch(0.74 0.16 68);
91
+ --warning-foreground: oklch(0.24 0.05 60);
92
+ --info: oklch(0.58 0.13 245);
93
+ --info-foreground: oklch(0.99 0.012 86);
80
94
  --border: oklch(0.89 0.016 76);
81
95
  --input: oklch(0.89 0.016 76);
82
96
  --ring: oklch(0.64 0.205 41);
@@ -122,6 +136,13 @@
122
136
  --accent: oklch(0.28 0.018 54);
123
137
  --accent-foreground: oklch(0.95 0.009 78);
124
138
  --destructive: oklch(0.704 0.191 22.216);
139
+ /* Status semantics — lifted for the warm charcoal bg */
140
+ --success: oklch(0.72 0.15 154);
141
+ --success-foreground: oklch(0.16 0.012 52);
142
+ --warning: oklch(0.80 0.155 72);
143
+ --warning-foreground: oklch(0.16 0.012 52);
144
+ --info: oklch(0.68 0.13 240);
145
+ --info-foreground: oklch(0.16 0.012 52);
125
146
  --border: oklch(0.92 0.03 70 / 11%);
126
147
  --input: oklch(0.92 0.03 70 / 14%);
127
148
  --ring: oklch(0.70 0.20 43);
@@ -184,3 +205,133 @@
184
205
  linear-gradient(to right, oklch(0.92 0.04 70 / 0.04) 1px, transparent 1px),
185
206
  linear-gradient(to bottom, oklch(0.92 0.04 70 / 0.04) 1px, transparent 1px);
186
207
  }
208
+
209
+ /* ─── Toast — stacking + enter/exit motion (mounted by SpuntoProvider) ─────
210
+ *
211
+ * The look (colors, spacing, typography) lives on the components as Tailwind
212
+ * utilities; this block owns only the parts Tailwind can't express cleanly —
213
+ * the position-aware stacking math driven by the CSS vars Base UI sets on each
214
+ * toast (`--toast-index`, `--toast-offset-y`, `--toast-frontmost-height`,
215
+ * `--toast-swipe-movement-*`). `data-position` on the viewport flips the whole
216
+ * system between the top and bottom anchors. Adapted from the Base UI toast
217
+ * reference example. */
218
+ [data-slot="toast-viewport"] {
219
+ --toast-gap: 0.75rem;
220
+ --toast-peek: 0.6rem;
221
+ position: fixed;
222
+ z-index: 60;
223
+ display: flex;
224
+ width: min(24rem, calc(100vw - 2rem));
225
+ }
226
+ [data-slot="toast-viewport"][data-position$="right"] { right: 1.25rem; left: auto; }
227
+ [data-slot="toast-viewport"][data-position$="left"] { left: 1.25rem; right: auto; }
228
+ [data-slot="toast-viewport"][data-position$="center"] {
229
+ left: 50%;
230
+ right: auto;
231
+ transform: translateX(-50%);
232
+ }
233
+ [data-slot="toast-viewport"][data-position^="top"] { top: 1.25rem; bottom: auto; }
234
+ [data-slot="toast-viewport"][data-position^="bottom"] { bottom: 1.25rem; top: auto; }
235
+
236
+ [data-slot="toast"] {
237
+ --scale: max(0, 1 - (var(--toast-index) * 0.1));
238
+ --shrink: calc(1 - var(--scale));
239
+ --height: var(--toast-frontmost-height, var(--toast-height));
240
+ position: absolute;
241
+ right: 0;
242
+ left: auto;
243
+ width: 100%;
244
+ box-sizing: border-box;
245
+ z-index: calc(1000 - var(--toast-index));
246
+ height: var(--height);
247
+ transition:
248
+ transform 0.5s cubic-bezier(0.22, 1, 0.36, 1),
249
+ opacity 0.4s ease,
250
+ height 0.2s ease;
251
+ }
252
+ /* Invisible bridge so the pointer can travel across the gap between toasts
253
+ without the stack collapsing. */
254
+ [data-slot="toast"]::after {
255
+ content: "";
256
+ position: absolute;
257
+ left: 0;
258
+ width: 100%;
259
+ height: calc(var(--toast-gap) + 1px);
260
+ }
261
+ [data-slot="toast"][data-limited],
262
+ [data-slot="toast"][data-starting-style],
263
+ [data-slot="toast"][data-ending-style] { opacity: 0; }
264
+
265
+ /* Bottom-anchored (default) — newest at the bottom, older peek upward. */
266
+ [data-position^="bottom"] [data-slot="toast"] {
267
+ bottom: 0;
268
+ transform-origin: bottom center;
269
+ --offset-y: calc(
270
+ var(--toast-offset-y) * -1 + (var(--toast-index) * var(--toast-gap) * -1) +
271
+ var(--toast-swipe-movement-y)
272
+ );
273
+ transform:
274
+ translateX(var(--toast-swipe-movement-x))
275
+ translateY(
276
+ calc(
277
+ var(--toast-swipe-movement-y) - (var(--toast-index) * var(--toast-peek)) -
278
+ (var(--shrink) * var(--height))
279
+ )
280
+ )
281
+ scale(var(--scale));
282
+ }
283
+ [data-position^="bottom"] [data-slot="toast"]::after { top: 100%; }
284
+ [data-position^="bottom"] [data-slot="toast"][data-expanded] {
285
+ transform: translateX(var(--toast-swipe-movement-x)) translateY(var(--offset-y));
286
+ height: var(--toast-height);
287
+ }
288
+ [data-position^="bottom"] [data-slot="toast"][data-starting-style],
289
+ [data-position^="bottom"] [data-slot="toast"][data-ending-style]:not([data-swipe-direction]) {
290
+ transform: translateY(150%);
291
+ }
292
+
293
+ /* Top-anchored — newest at the top, older peek downward. */
294
+ [data-position^="top"] [data-slot="toast"] {
295
+ top: 0;
296
+ transform-origin: top center;
297
+ --offset-y: calc(
298
+ var(--toast-offset-y) + (var(--toast-index) * var(--toast-gap)) +
299
+ var(--toast-swipe-movement-y)
300
+ );
301
+ transform:
302
+ translateX(var(--toast-swipe-movement-x))
303
+ translateY(
304
+ calc(
305
+ var(--toast-swipe-movement-y) + (var(--toast-index) * var(--toast-peek)) +
306
+ (var(--shrink) * var(--height))
307
+ )
308
+ )
309
+ scale(var(--scale));
310
+ }
311
+ [data-position^="top"] [data-slot="toast"]::after { bottom: 100%; }
312
+ [data-position^="top"] [data-slot="toast"][data-expanded] {
313
+ transform: translateX(var(--toast-swipe-movement-x)) translateY(var(--offset-y));
314
+ height: var(--toast-height);
315
+ }
316
+ [data-position^="top"] [data-slot="toast"][data-starting-style],
317
+ [data-position^="top"] [data-slot="toast"][data-ending-style]:not([data-swipe-direction]) {
318
+ transform: translateY(-150%);
319
+ }
320
+
321
+ /* Swipe-to-dismiss exits — fly out toward the swiped edge (both anchors). */
322
+ [data-slot="toast"][data-ending-style][data-swipe-direction="up"] {
323
+ transform: translateY(calc(var(--toast-swipe-movement-y) - 150%));
324
+ }
325
+ [data-slot="toast"][data-ending-style][data-swipe-direction="down"] {
326
+ transform: translateY(calc(var(--toast-swipe-movement-y) + 150%));
327
+ }
328
+ [data-slot="toast"][data-ending-style][data-swipe-direction="left"] {
329
+ transform: translateX(calc(var(--toast-swipe-movement-x) - 150%)) translateY(var(--offset-y));
330
+ }
331
+ [data-slot="toast"][data-ending-style][data-swipe-direction="right"] {
332
+ transform: translateX(calc(var(--toast-swipe-movement-x) + 150%)) translateY(var(--offset-y));
333
+ }
334
+
335
+ @media (prefers-reduced-motion: reduce) {
336
+ [data-slot="toast"] { transition-duration: 0.01ms; }
337
+ }