Skip to content

Commit a19848f

Browse files
committed
feat(toast): success/warning/error variants & showToastPromise()
1 parent d2ab19b commit a19848f

4 files changed

Lines changed: 76 additions & 7 deletions

File tree

.changeset/lovely-boxes-lick.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"solidui-cli": patch
3+
---
4+
5+
add succes/warning/error variants to toast & showToastPromise()

apps/docs/public/registry/ui/toast.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"files": [
77
{
88
"name": "toast.tsx",
9-
"content": "import type { Component, JSX } from \"solid-js\"\nimport { splitProps } from \"solid-js\"\nimport { Portal } from \"solid-js/web\"\n\nimport { toaster, Toast as ToastPrimitive } from \"@kobalte/core\"\nimport type { VariantProps } from \"class-variance-authority\"\nimport { cva } from \"class-variance-authority\"\n\nimport { cn } from \"~/lib/utils\"\n\nconst Toaster: Component<ToastPrimitive.ToastListProps> = (props) => {\n const [, rest] = splitProps(props, [\"class\"])\n return (\n <Portal>\n <ToastPrimitive.Region>\n <ToastPrimitive.List\n class={cn(\n \"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse gap-2 p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]\",\n props.class\n )}\n {...rest}\n />\n </ToastPrimitive.Region>\n </Portal>\n )\n}\n\nconst toastVariants = cva(\n \"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--kb-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--kb-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[opened]:animate-in data-[closed]:animate-out data-[swipe=end]:animate-out data-[closed]:fade-out-80 data-[closed]:slide-out-to-right-full data-[opened]:slide-in-from-top-full data-[opened]:sm:slide-in-from-bottom-full\",\n {\n variants: {\n variant: {\n default: \"border bg-background text-foreground\",\n destructive:\n \"destructive group border-destructive bg-destructive text-destructive-foreground\"\n }\n },\n defaultVariants: {\n variant: \"default\"\n }\n }\n)\ntype ToastVariant = NonNullable<VariantProps<typeof toastVariants>[\"variant\"]>\n\nexport interface ToastProps\n extends ToastPrimitive.ToastRootProps,\n VariantProps<typeof toastVariants> {}\n\nconst Toast: Component<ToastProps> = (props) => {\n const [, rest] = splitProps(props, [\"class\", \"variant\"])\n return (\n <ToastPrimitive.Root\n class={cn(toastVariants({ variant: props.variant }), props.class)}\n {...rest}\n />\n )\n}\n\nconst ToastClose: Component<ToastPrimitive.ToastCloseButtonProps> = (props) => {\n const [, rest] = splitProps(props, [\"class\"])\n return (\n <ToastPrimitive.CloseButton\n class={cn(\n \"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600\",\n props.class\n )}\n {...rest}\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n class=\"size-4\"\n >\n <path d=\"M18 6l-12 12\" />\n <path d=\"M6 6l12 12\" />\n </svg>\n </ToastPrimitive.CloseButton>\n )\n}\n\nconst ToastTitle: Component<ToastPrimitive.ToastTitleProps> = (props) => {\n const [, rest] = splitProps(props, [\"class\"])\n return <ToastPrimitive.Title class={cn(\"text-sm font-semibold\", props.class)} {...rest} />\n}\n\nconst ToastDescription: Component<ToastPrimitive.ToastDescriptionProps> = (props) => {\n const [, rest] = splitProps(props, [\"class\"])\n return <ToastPrimitive.Description class={cn(\"text-sm opacity-90\", props.class)} {...rest} />\n}\n\nfunction showToast(props: {\n title?: JSX.Element\n description?: JSX.Element\n variant?: ToastVariant\n duration?: number\n}) {\n toaster.show((data) => (\n <Toast toastId={data.toastId} variant={props.variant} duration={props.duration}>\n <div class=\"grid gap-1\">\n {props.title && <ToastTitle>{props.title}</ToastTitle>}\n {props.description && <ToastDescription>{props.description}</ToastDescription>}\n </div>\n <ToastClose />\n </Toast>\n ))\n}\n\nexport { Toaster, Toast, ToastClose, ToastTitle, ToastDescription, showToast }\n"
9+
"content": "import type { Component, JSX } from \"solid-js\"\nimport { Match, splitProps, Switch } from \"solid-js\"\nimport { Portal } from \"solid-js/web\"\n\nimport { toaster, Toast as ToastPrimitive } from \"@kobalte/core\"\nimport type { VariantProps } from \"class-variance-authority\"\nimport { cva } from \"class-variance-authority\"\n\nimport { cn } from \"~/lib/utils\"\n\nconst Toaster: Component<ToastPrimitive.ToastListProps> = (props) => {\n const [, rest] = splitProps(props, [\"class\"])\n return (\n <Portal>\n <ToastPrimitive.Region>\n <ToastPrimitive.List\n class={cn(\n \"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse gap-2 p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]\",\n props.class\n )}\n {...rest}\n />\n </ToastPrimitive.Region>\n </Portal>\n )\n}\n\nconst toastVariants = cva(\n \"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--kb-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--kb-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[opened]:animate-in data-[closed]:animate-out data-[swipe=end]:animate-out data-[closed]:fade-out-80 data-[closed]:slide-out-to-right-full data-[opened]:slide-in-from-top-full data-[opened]:sm:slide-in-from-bottom-full\",\n {\n variants: {\n variant: {\n default: \"border bg-background text-foreground\",\n destructive:\n \"destructive group border-destructive bg-destructive text-destructive-foreground\",\n success: \"success border-success-foreground bg-success text-success-foreground\",\n warning: \"warning border-warning-foreground bg-warning text-warning-foreground\",\n error: \"error border-error-foreground bg-error text-error-foreground\"\n }\n },\n defaultVariants: {\n variant: \"default\"\n }\n }\n)\ntype ToastVariant = NonNullable<VariantProps<typeof toastVariants>[\"variant\"]>\n\nexport interface ToastProps\n extends ToastPrimitive.ToastRootProps,\n VariantProps<typeof toastVariants> {}\n\nconst Toast: Component<ToastProps> = (props) => {\n const [, rest] = splitProps(props, [\"class\", \"variant\"])\n return (\n <ToastPrimitive.Root\n class={cn(toastVariants({ variant: props.variant }), props.class)}\n {...rest}\n />\n )\n}\n\nconst ToastClose: Component<ToastPrimitive.ToastCloseButtonProps> = (props) => {\n const [, rest] = splitProps(props, [\"class\"])\n return (\n <ToastPrimitive.CloseButton\n class={cn(\n \"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-destructive-foreground group-[.error]:text-error-foreground group-[.success]:text-success-foreground group-[.warning]:text-warning-foreground\",\n props.class\n )}\n {...rest}\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n class=\"size-4\"\n >\n <path d=\"M18 6l-12 12\" />\n <path d=\"M6 6l12 12\" />\n </svg>\n </ToastPrimitive.CloseButton>\n )\n}\n\nconst ToastTitle: Component<ToastPrimitive.ToastTitleProps> = (props) => {\n const [, rest] = splitProps(props, [\"class\"])\n return <ToastPrimitive.Title class={cn(\"text-sm font-semibold\", props.class)} {...rest} />\n}\n\nconst ToastDescription: Component<ToastPrimitive.ToastDescriptionProps> = (props) => {\n const [, rest] = splitProps(props, [\"class\"])\n return <ToastPrimitive.Description class={cn(\"text-sm opacity-90\", props.class)} {...rest} />\n}\n\nfunction showToast(props: {\n title?: JSX.Element\n description?: JSX.Element\n variant?: ToastVariant\n duration?: number\n}) {\n toaster.show((data) => (\n <Toast toastId={data.toastId} variant={props.variant} duration={props.duration}>\n <div class=\"grid gap-1\">\n {props.title && <ToastTitle>{props.title}</ToastTitle>}\n {props.description && <ToastDescription>{props.description}</ToastDescription>}\n </div>\n <ToastClose />\n </Toast>\n ))\n}\n\nfunction showToastPromise<T, U>(\n promise: Promise<T> | (() => Promise<T>),\n options: {\n loading?: JSX.Element\n success?: (data: T) => JSX.Element\n error?: (error: U) => JSX.Element\n duration?: number\n }\n) {\n const variant: { [key in ToastPrimitive.ToastPromiseState]: ToastVariant } = {\n pending: \"default\",\n fulfilled: \"success\",\n rejected: \"error\"\n }\n return toaster.promise<T, U>(promise, (props) => (\n <Toast toastId={props.toastId} variant={variant[props.state]} duration={options.duration}>\n <Switch>\n <Match when={props.state === \"pending\"}>{options.loading}</Match>\n <Match when={props.state === \"fulfilled\"}>{options.success?.(props.data!)}</Match>\n <Match when={props.state === \"rejected\"}>{options.error?.(props.error!)}</Match>\n </Switch>\n </Toast>\n ))\n}\n\nexport { Toaster, Toast, ToastClose, ToastTitle, ToastDescription, showToast, showToastPromise }\n"
1010
}
1111
],
1212
"type": "ui"

apps/docs/src/registry/example/toast-demo.tsx

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { Button } from "~/registry/ui/button"
2-
import { Toaster, showToast } from "~/registry/ui/toast"
2+
import { showToast, Toaster } from "~/registry/ui/toast"
33

44
export default function ToastDemo() {
55
return (
6-
<div class="gap-2">
6+
<div class="flex gap-2">
77
<Button
88
onClick={() =>
99
showToast({
@@ -26,6 +26,42 @@ export default function ToastDemo() {
2626
>
2727
Delete Event
2828
</Button>
29+
<Button
30+
variant="outline"
31+
onClick={() =>
32+
showToast({
33+
title: "SUCCESS!",
34+
description: "Monday, January 3rd at 6:00pm",
35+
variant: "success"
36+
})
37+
}
38+
>
39+
Success
40+
</Button>
41+
<Button
42+
variant="outline"
43+
onClick={() =>
44+
showToast({
45+
title: "WARING!",
46+
description: "Monday, January 3rd at 6:00pm",
47+
variant: "warning"
48+
})
49+
}
50+
>
51+
Warning
52+
</Button>
53+
<Button
54+
variant="outline"
55+
onClick={() =>
56+
showToast({
57+
title: "ERROR!",
58+
description: "Monday, January 3rd at 6:00pm",
59+
variant: "error"
60+
})
61+
}
62+
>
63+
Error
64+
</Button>
2965
<Toaster />
3066
</div>
3167
)

apps/docs/src/registry/ui/toast.tsx

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Component, JSX } from "solid-js"
2-
import { splitProps } from "solid-js"
2+
import { Match, splitProps, Switch } from "solid-js"
33
import { Portal } from "solid-js/web"
44

55
import { toaster, Toast as ToastPrimitive } from "@kobalte/core"
@@ -32,7 +32,10 @@ const toastVariants = cva(
3232
variant: {
3333
default: "border bg-background text-foreground",
3434
destructive:
35-
"destructive group border-destructive bg-destructive text-destructive-foreground"
35+
"destructive group border-destructive bg-destructive text-destructive-foreground",
36+
success: "success border-success-foreground bg-success text-success-foreground",
37+
warning: "warning border-warning-foreground bg-warning text-warning-foreground",
38+
error: "error border-error-foreground bg-error text-error-foreground"
3639
}
3740
},
3841
defaultVariants: {
@@ -61,7 +64,7 @@ const ToastClose: Component<ToastPrimitive.ToastCloseButtonProps> = (props) => {
6164
return (
6265
<ToastPrimitive.CloseButton
6366
class={cn(
64-
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
67+
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-destructive-foreground group-[.error]:text-error-foreground group-[.success]:text-success-foreground group-[.warning]:text-warning-foreground",
6568
props.class
6669
)}
6770
{...rest}
@@ -110,4 +113,29 @@ function showToast(props: {
110113
))
111114
}
112115

113-
export { Toaster, Toast, ToastClose, ToastTitle, ToastDescription, showToast }
116+
function showToastPromise<T, U>(
117+
promise: Promise<T> | (() => Promise<T>),
118+
options: {
119+
loading?: JSX.Element
120+
success?: (data: T) => JSX.Element
121+
error?: (error: U) => JSX.Element
122+
duration?: number
123+
}
124+
) {
125+
const variant: { [key in ToastPrimitive.ToastPromiseState]: ToastVariant } = {
126+
pending: "default",
127+
fulfilled: "success",
128+
rejected: "error"
129+
}
130+
return toaster.promise<T, U>(promise, (props) => (
131+
<Toast toastId={props.toastId} variant={variant[props.state]} duration={options.duration}>
132+
<Switch>
133+
<Match when={props.state === "pending"}>{options.loading}</Match>
134+
<Match when={props.state === "fulfilled"}>{options.success?.(props.data!)}</Match>
135+
<Match when={props.state === "rejected"}>{options.error?.(props.error!)}</Match>
136+
</Switch>
137+
</Toast>
138+
))
139+
}
140+
141+
export { Toaster, Toast, ToastClose, ToastTitle, ToastDescription, showToast, showToastPromise }

0 commit comments

Comments
 (0)