mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 06:30:10 +08:00
feat(sonner): rewrite toast to use native api
- Simplify toast API to match sonner's interface - Add support for colored backgrounds and custom durations - Improve icon rendering with unique IDs to prevent conflicts - Remove unused code and consolidate styles - Update stories to reflect new API changes
This commit is contained in:
parent
36a63b4331
commit
45865a0c2e
@ -1,7 +1,8 @@
|
||||
import { cn } from '@cherrystudio/ui/utils'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import { Loader2Icon } from 'lucide-react'
|
||||
import { type ReactNode, type SVGProps, useCallback, useMemo } from 'react'
|
||||
import { merge } from 'lodash'
|
||||
import { type ReactNode, type SVGProps, useId } from 'react'
|
||||
import type { Action, ExternalToast, ToastClassnames } from 'sonner'
|
||||
import { toast as sonnerToast, Toaster as Sonner, type ToasterProps } from 'sonner'
|
||||
|
||||
const InfoIcon = ({ className }: SVGProps<SVGSVGElement>) => (
|
||||
@ -70,135 +71,159 @@ const InfoIcon = ({ className }: SVGProps<SVGSVGElement>) => (
|
||||
</svg>
|
||||
)
|
||||
|
||||
const WarningIcon = ({ className }: SVGProps<SVGSVGElement>) => (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<g filter="url(#filter0_dd_1669_13487)">
|
||||
<path
|
||||
d="M15.728 22H8.272C8.00681 21.9999 7.75249 21.8946 7.565 21.707L2.293 16.435C2.10545 16.2475 2.00006 15.9932 2 15.728V8.272C2.00006 8.00681 2.10545 7.75249 2.293 7.565L7.565 2.293C7.75249 2.10545 8.00681 2.00006 8.272 2H15.728C15.9932 2.00006 16.2475 2.10545 16.435 2.293L21.707 7.565C21.8946 7.75249 21.9999 8.00681 22 8.272V15.728C21.9999 15.9932 21.8946 16.2475 21.707 16.435L16.435 21.707C16.2475 21.8946 15.9932 21.9999 15.728 22Z"
|
||||
fill="url(#paint0_linear_1669_13487)"
|
||||
/>
|
||||
<path
|
||||
d="M12 17C12.5523 17 13 16.5523 13 16C13 15.4477 12.5523 15 12 15C11.4477 15 11 15.4477 11 16C11 16.5523 11.4477 17 12 17Z"
|
||||
fill="#FAFAFA"
|
||||
/>
|
||||
<path
|
||||
d="M12 13C11.7348 13 11.4804 12.8946 11.2929 12.7071C11.1054 12.5196 11 12.2652 11 12V8C11 7.73478 11.1054 7.48043 11.2929 7.29289C11.4804 7.10536 11.7348 7 12 7C12.2652 7 12.5196 7.10536 12.7071 7.29289C12.8946 7.48043 13 7.73478 13 8V12C13 12.2652 12.8946 12.5196 12.7071 12.7071C12.5196 12.8946 12.2652 13 12 13Z"
|
||||
fill="#FAFAFA"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_dd_1669_13487"
|
||||
x="-3"
|
||||
y="-2"
|
||||
width="30"
|
||||
height="30"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="1" />
|
||||
<feGaussianBlur stdDeviation="1.5" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0" />
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1669_13487" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feMorphology radius="2" operator="dilate" in="SourceAlpha" result="effect2_dropShadow_1669_13487" />
|
||||
<feOffset dy="1" />
|
||||
<feGaussianBlur stdDeviation="1.5" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0" />
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_1669_13487" result="effect2_dropShadow_1669_13487" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_1669_13487" result="shape" />
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_1669_13487" x1="12" y1="12" x2="12" y2="30.5" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F59E0B" />
|
||||
<stop offset="1" stop-color="white" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const SuccessIcon = ({ className }: SVGProps<SVGSVGElement>) => (
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<mask id="mask0_1669_13491" style={{ maskType: 'luminance' }} maskUnits="userSpaceOnUse" x="0" y="0">
|
||||
<path d="M24 0H0V24H24V0Z" fill="white" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_1669_13491)">
|
||||
<foreignObject x="-3" y="-2">
|
||||
<div
|
||||
// xmlns="http://www.w3.org/1999/xhtml"
|
||||
style={{
|
||||
backdropFilter: 'blur(2px)',
|
||||
clipPath: 'url(#bgblur_0_1669_13491_clip_path)',
|
||||
height: '100%',
|
||||
width: '100%'
|
||||
}}></div>
|
||||
</foreignObject>
|
||||
<g filter="url(#filter0_dd_1669_13491)" data-figma-bg-blur-radius="4">
|
||||
const WarningIcon = ({ className, ...props }: SVGProps<SVGSVGElement>) => {
|
||||
// Remove colons to prevent ID recognition issues in some CSS environments
|
||||
const id = useId().replace(/:/g, '')
|
||||
const filterId = `filter_${id}`
|
||||
const maskId = `mask_${id}`
|
||||
const gradientId = `paint_${id}`
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
{...props}>
|
||||
<defs>
|
||||
<mask id={maskId}>
|
||||
{/* Show white part */}
|
||||
<rect width="24" height="24" fill="white" />
|
||||
{/* Clip black part */}
|
||||
<g fill="black">
|
||||
<path d="M12 17C12.5523 17 13 16.5523 13 16C13 15.4477 12.5523 15 12 15C11.4477 15 11 15.4477 11 16C11 16.5523 11.4477 17 12 17Z" />
|
||||
<path d="M12 13C11.7348 13 11.4804 12.8946 11.2929 12.7071C11.1054 12.5196 11 12.2652 11 12V8C11 7.73478 11.1054 7.48043 11.2929 7.29289C11.4804 7.10536 11.7348 7 12 7C12.2652 7 12.5196 7.10536 12.7071 7.29289C12.8946 7.48043 13 7.73478 13 8V12C13 12.2652 12.8946 12.5196 12.7071 12.7071C12.5196 12.8946 12.2652 13 12 13Z" />
|
||||
</g>
|
||||
</mask>
|
||||
<filter
|
||||
id={filterId}
|
||||
x="-3"
|
||||
y="-2"
|
||||
width="30"
|
||||
height="30"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB">
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="1" />
|
||||
<feGaussianBlur stdDeviation="1.5" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0" />
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape" />
|
||||
</filter>
|
||||
<linearGradient id={gradientId} x1="12" y1="12" x2="12" y2="30.5" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#F59E0B" />
|
||||
<stop offset="1" stopColor="white" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g filter={`url(#${filterId})`}>
|
||||
<path
|
||||
d="M13.2121 2.57414C12.5853 1.80862 11.4146 1.80862 10.788 2.57414L9.90009 3.65856C9.83924 3.73288 9.73773 3.76009 9.64787 3.72614L8.33677 3.23092C7.41123 2.88134 6.39741 3.46667 6.23738 4.44301L6.0107 5.82606C5.99517 5.92086 5.92086 5.99516 5.82606 6.0107L4.44301 6.23738C3.46668 6.39741 2.88134 7.41122 3.23092 8.33676L3.72614 9.64787C3.76009 9.73773 3.73288 9.83924 3.65856 9.90009L2.57414 10.7879C1.80862 11.4147 1.80862 12.5854 2.57414 13.2121L3.65856 14.0999C3.73288 14.1608 3.76009 14.2623 3.72614 14.3522L3.23092 15.6633C2.88135 16.5888 3.46667 17.6026 4.44301 17.7627L5.82606 17.9893C5.92086 18.0049 5.99517 18.0792 6.0107 18.174L6.23738 19.557C6.39741 20.5333 7.41122 21.1186 8.33677 20.7691L9.64787 20.2739C9.73773 20.24 9.83924 20.2671 9.90009 20.3415L10.788 21.4259C11.4146 22.1914 12.5853 22.1914 13.2121 21.4259L14.0999 20.3415C14.1608 20.2671 14.2623 20.24 14.3521 20.2739L15.6633 20.7691C16.5888 21.1186 17.6027 20.5333 17.7626 19.557L17.9894 18.174C18.0049 18.0792 18.0791 18.0049 18.1739 17.9893L19.557 17.7627C20.5334 17.6026 21.1187 16.5888 20.7691 15.6633L20.2739 14.3522C20.2399 14.2623 20.2671 14.1608 20.3414 14.0999L21.4259 13.2121C22.1914 12.5854 22.1914 11.4147 21.4259 10.7879L20.3414 9.90009C20.2671 9.83924 20.2399 9.73773 20.2739 9.64787L20.7691 8.33676C21.1187 7.41122 20.5334 6.39741 19.557 6.23738L18.1739 6.0107C18.0791 5.99516 18.0049 5.92086 17.9894 5.82606L17.7626 4.44301C17.6027 3.46668 16.5888 2.88134 15.6633 3.23092L14.3521 3.72614C14.2623 3.76009 14.1608 3.73288 14.0999 3.65856L13.2121 2.57414Z"
|
||||
fill="url(#paint0_linear_1669_13491)"
|
||||
d="M15.728 22H8.272C8.00681 21.9999 7.75249 21.8946 7.565 21.707L2.293 16.435C2.10545 16.2475 2.00006 15.9932 2 15.728V8.272C2.00006 8.00681 2.10545 7.75249 2.293 7.565L7.565 2.293C7.75249 2.10545 8.00681 2.00006 8.272 2H15.728C15.9932 2.00006 16.2475 2.10545 16.435 2.293L21.707 7.565C21.8946 7.75249 21.9999 8.00681 22 8.272V15.728C21.9999 15.9932 21.8946 16.2475 21.707 16.435L16.435 21.707C16.2475 21.8946 15.9932 21.9999 15.728 22Z"
|
||||
fill={`url(#${gradientId})`}
|
||||
mask={`url(#${maskId})`}
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M17.3974 8.39243C17.6596 8.65461 17.6596 9.0797 17.3974 9.34187L11.1314 15.6078C11.0055 15.7338 10.8347 15.8045 10.6567 15.8045C10.4787 15.8045 10.3079 15.7338 10.182 15.6078L6.60142 12.0273C6.33924 11.7651 6.33924 11.3401 6.60142 11.0779C6.8636 10.8157 7.28868 10.8157 7.55086 11.0779L10.6567 14.1837L16.448 8.39243C16.7102 8.13026 17.1352 8.13026 17.3974 8.39243Z"
|
||||
fill="#FAFAFA"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter
|
||||
id="filter0_dd_1669_13491"
|
||||
x="-3"
|
||||
y="-2"
|
||||
width="30"
|
||||
height="30"
|
||||
filterUnits="userSpaceOnUse"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="1" />
|
||||
<feGaussianBlur stdDeviation="1.5" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0" />
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1669_13491" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feMorphology radius="2" operator="dilate" in="SourceAlpha" result="effect2_dropShadow_1669_13491" />
|
||||
<feOffset dy="1" />
|
||||
<feGaussianBlur stdDeviation="1.5" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0" />
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_1669_13491" result="effect2_dropShadow_1669_13491" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_1669_13491" result="shape" />
|
||||
</filter>
|
||||
<clipPath id="bgblur_0_1669_13491_clip_path" transform="translate(3 2)">
|
||||
<path d="M13.2121 2.57414C12.5853 1.80862 11.4146 1.80862 10.788 2.57414L9.90009 3.65856C9.83924 3.73288 9.73773 3.76009 9.64787 3.72614L8.33677 3.23092C7.41123 2.88134 6.39741 3.46667 6.23738 4.44301L6.0107 5.82606C5.99517 5.92086 5.92086 5.99516 5.82606 6.0107L4.44301 6.23738C3.46668 6.39741 2.88134 7.41122 3.23092 8.33676L3.72614 9.64787C3.76009 9.73773 3.73288 9.83924 3.65856 9.90009L2.57414 10.7879C1.80862 11.4147 1.80862 12.5854 2.57414 13.2121L3.65856 14.0999C3.73288 14.1608 3.76009 14.2623 3.72614 14.3522L3.23092 15.6633C2.88135 16.5888 3.46667 17.6026 4.44301 17.7627L5.82606 17.9893C5.92086 18.0049 5.99517 18.0792 6.0107 18.174L6.23738 19.557C6.39741 20.5333 7.41122 21.1186 8.33677 20.7691L9.64787 20.2739C9.73773 20.24 9.83924 20.2671 9.90009 20.3415L10.788 21.4259C11.4146 22.1914 12.5853 22.1914 13.2121 21.4259L14.0999 20.3415C14.1608 20.2671 14.2623 20.24 14.3521 20.2739L15.6633 20.7691C16.5888 21.1186 17.6027 20.5333 17.7626 19.557L17.9894 18.174C18.0049 18.0792 18.0791 18.0049 18.1739 17.9893L19.557 17.7627C20.5334 17.6026 21.1187 16.5888 20.7691 15.6633L20.2739 14.3522C20.2399 14.2623 20.2671 14.1608 20.3414 14.0999L21.4259 13.2121C22.1914 12.5854 22.1914 11.4147 21.4259 10.7879L20.3414 9.90009C20.2671 9.83924 20.2399 9.73773 20.2739 9.64787L20.7691 8.33676C21.1187 7.41122 20.5334 6.39741 19.557 6.23738L18.1739 6.0107C18.0791 5.99516 18.0049 5.92086 17.9894 5.82606L17.7626 4.44301C17.6027 3.46668 16.5888 2.88134 15.6633 3.23092L14.3521 3.72614C14.2623 3.76009 14.1608 3.73288 14.0999 3.65856L13.2121 2.57414Z" />
|
||||
</clipPath>
|
||||
<linearGradient id="paint0_linear_1669_13491" x1="12" y1="7.5" x2="12" y2="41.5" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3CD45A" />
|
||||
<stop offset="1" stop-color="white" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const SuccessIcon = ({ className, ...props }: SVGProps<SVGSVGElement>) => {
|
||||
const id = useId().replace(/:/g, '')
|
||||
const maskId = `mask_${id}`
|
||||
const filterId = `filter_${id}`
|
||||
const gradientId = `paint_${id}`
|
||||
const blurClipId = `blur_clip_${id}`
|
||||
|
||||
const checkPathData =
|
||||
'M17.3974 8.39243C17.6596 8.65461 17.6596 9.0797 17.3974 9.34187L11.1314 15.6078C11.0055 15.7338 10.8347 15.8045 10.6567 15.8045C10.4787 15.8045 10.3079 15.7338 10.182 15.6078L6.60142 12.0273C6.33924 11.7651 6.33924 11.3401 6.60142 11.0779C6.8636 10.8157 7.28868 10.8157 7.55086 11.0779L10.6567 14.1837L16.448 8.39243C16.7102 8.13026 17.1352 8.13026 17.3974 8.39243Z'
|
||||
|
||||
const polygonPathData =
|
||||
'M13.2121 2.57414C12.5853 1.80862 11.4146 1.80862 10.788 2.57414L9.90009 3.65856C9.83924 3.73288 9.73773 3.76009 9.64787 3.72614L8.33677 3.23092C7.41123 2.88134 6.39741 3.46667 6.23738 4.44301L6.0107 5.82606C5.99517 5.92086 5.92086 5.99516 5.82606 6.0107L4.44301 6.23738C3.46668 6.39741 2.88134 7.41122 3.23092 8.33676L3.72614 9.64787C3.76009 9.73773 3.73288 9.83924 3.65856 9.90009L2.57414 10.7879C1.80862 11.4147 1.80862 12.5854 2.57414 13.2121L3.65856 14.0999C3.73288 14.1608 3.76009 14.2623 3.72614 14.3522L3.23092 15.6633C2.88135 16.5888 3.46667 17.6026 4.44301 17.7627L5.82606 17.9893C5.92086 18.0049 5.99517 18.0792 6.0107 18.174L6.23738 19.557C6.39741 20.5333 7.41122 21.1186 8.33677 20.7691L9.64787 20.2739C9.73773 20.24 9.83924 20.2671 9.90009 20.3415L10.788 21.4259C11.4146 22.1914 12.5853 22.1914 13.2121 21.4259L14.0999 20.3415C14.1608 20.2671 14.2623 20.24 14.3521 20.2739L15.6633 20.7691C16.5888 21.1186 17.6027 20.5333 17.7626 19.557L17.9894 18.174C18.0049 18.0792 18.0791 18.0049 18.1739 17.9893L19.557 17.7627C20.5334 17.6026 21.1187 16.5888 20.7691 15.6633L20.2739 14.3522C20.2399 14.2623 20.2671 14.1608 20.3414 14.0999L21.4259 13.2121C22.1914 12.5854 22.1914 11.4147 21.4259 10.7879L20.3414 9.90009C20.2671 9.83924 20.2399 9.73773 20.2739 9.64787L20.7691 8.33676C21.1187 7.41122 20.5334 6.39741 19.557 6.23738L18.1739 6.0107C18.0791 5.99516 18.0791 5.92086 17.9894 5.82606L17.7626 4.44301C17.6027 3.46668 16.5888 2.88134 15.6633 3.23092L14.3521 3.72614C14.2623 3.76009 14.1608 3.73288 14.0999 3.65856L13.2121 2.57414Z'
|
||||
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className} {...props}>
|
||||
<defs>
|
||||
<mask
|
||||
id={maskId}
|
||||
style={{ maskType: 'luminance' }}
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="0"
|
||||
width="24"
|
||||
height="24">
|
||||
{/* Show white part */}
|
||||
<rect width="24" height="24" fill="white" />
|
||||
{/* Clip black part */}
|
||||
<path d={checkPathData} fill="black" />
|
||||
</mask>
|
||||
|
||||
<clipPath id={blurClipId} transform="translate(3 2)">
|
||||
<path d={polygonPathData} />
|
||||
</clipPath>
|
||||
|
||||
<filter
|
||||
id={filterId}
|
||||
x="-3"
|
||||
y="-2"
|
||||
width="30"
|
||||
height="30"
|
||||
filterUnits="userSpaceOnUse"
|
||||
colorInterpolationFilters="sRGB">
|
||||
<feFlood floodOpacity="0" result="BackgroundImageFix" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feOffset dy="1" />
|
||||
<feGaussianBlur stdDeviation="1.5" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0" />
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow" />
|
||||
<feColorMatrix
|
||||
in="SourceAlpha"
|
||||
type="matrix"
|
||||
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
|
||||
result="hardAlpha"
|
||||
/>
|
||||
<feMorphology radius="2" operator="dilate" in="SourceAlpha" result="effect2_dropShadow" />
|
||||
<feOffset dy="1" />
|
||||
<feGaussianBlur stdDeviation="1.5" />
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0" />
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow" />
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape" />
|
||||
</filter>
|
||||
<linearGradient id={gradientId} x1="12" y1="7.5" x2="12" y2="41.5" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#3CD45A" />
|
||||
<stop offset="1" stopColor="white" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<g mask={`url(#${maskId})`}>
|
||||
<foreignObject x="-3" y="-2" width="30" height="30">
|
||||
<div
|
||||
style={{
|
||||
backdropFilter: 'blur(2px)',
|
||||
clipPath: `url(#${blurClipId})`,
|
||||
height: '100%',
|
||||
width: '100%'
|
||||
}}
|
||||
/>
|
||||
</foreignObject>
|
||||
|
||||
<g filter={`url(#${filterId})`}>
|
||||
<path d={polygonPathData} fill={`url(#${gradientId})`} />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const ErrorIcon = ({ className }: SVGProps<SVGSVGElement>) => (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
@ -267,83 +292,137 @@ const ErrorIcon = ({ className }: SVGProps<SVGSVGElement>) => (
|
||||
</svg>
|
||||
)
|
||||
|
||||
const CloseIcon = ({ className }: SVGProps<SVGSVGElement>) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none" className={className}>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M15.4419 5.44194C15.686 5.19786 15.686 4.80214 15.4419 4.55806C15.1979 4.31398 14.8021 4.31398 14.5581 4.55806L10 9.11612L5.44194 4.55806C5.19786 4.31398 4.80214 4.31398 4.55806 4.55806C4.31398 4.80214 4.31398 5.19786 4.55806 5.44194L9.11612 10L4.55806 14.5581C4.31398 14.8021 4.31398 15.1979 4.55806 15.4419C4.80214 15.686 5.19786 15.686 5.44194 15.4419L10 10.8839L14.5581 15.4419C14.8021 15.686 15.1979 15.686 15.4419 15.4419C15.686 15.1979 15.686 14.8021 15.4419 14.5581L10.8839 10L15.4419 5.44194Z"
|
||||
fill="black"
|
||||
fill-opacity="0.4"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
interface ToastProps {
|
||||
// const CloseIcon = ({ className }: SVGProps<SVGSVGElement>) => (
|
||||
// <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none" className={className}>
|
||||
// <path
|
||||
// fill-rule="evenodd"
|
||||
// clip-rule="evenodd"
|
||||
// d="M15.4419 5.44194C15.686 5.19786 15.686 4.80214 15.4419 4.55806C15.1979 4.31398 14.8021 4.31398 14.5581 4.55806L10 9.11612L5.44194 4.55806C5.19786 4.31398 4.80214 4.31398 4.55806 4.55806C4.31398 4.80214 4.31398 5.19786 4.55806 5.44194L9.11612 10L4.55806 14.5581C4.31398 14.8021 4.31398 15.1979 4.55806 15.4419C4.80214 15.686 5.19786 15.686 5.44194 15.4419L10 10.8839L14.5581 15.4419C14.8021 15.686 15.1979 15.686 15.4419 15.4419C15.686 15.1979 15.686 14.8021 15.4419 14.5581L10.8839 10L15.4419 5.44194Z"
|
||||
// fill="black"
|
||||
// fill-opacity="0.4"
|
||||
// />
|
||||
// </svg>
|
||||
// )
|
||||
interface ToastProps<ToastData = unknown> {
|
||||
id: string | number
|
||||
type: 'info' | 'warning' | 'error' | 'success' | 'loading'
|
||||
title: string
|
||||
description?: string
|
||||
coloredMessage?: string
|
||||
coloredBackground?: boolean
|
||||
type?: 'info' | 'warning' | 'error' | 'success' | 'loading' | 'custom'
|
||||
title: ReactNode
|
||||
description?: ReactNode
|
||||
colored?: boolean
|
||||
duration?: number
|
||||
dismissable?: boolean
|
||||
onDismiss?: () => void
|
||||
button?: {
|
||||
icon?: ReactNode
|
||||
label: string
|
||||
onClick: () => void
|
||||
}
|
||||
link?: {
|
||||
label: string
|
||||
href?: string
|
||||
onClick?: () => void
|
||||
}
|
||||
promise?: Promise<unknown>
|
||||
button?: Action | ReactNode
|
||||
promise?: Promise<ToastData>
|
||||
classNames?: ToastClassnames
|
||||
jsx?: (id: number | string) => React.ReactElement
|
||||
}
|
||||
|
||||
function toast(props: Omit<ToastProps, 'id'>) {
|
||||
return sonnerToast.custom((id) => <Toast id={id} {...props} />, {
|
||||
classNames: { toast: props.coloredBackground ? 'backdrop-blur-md rounded-xs' : undefined }
|
||||
})
|
||||
const type = props.type ?? 'info'
|
||||
|
||||
const baseClassNames: ToastClassnames = {
|
||||
toast: cn(
|
||||
'flex rounded-xs p-4 bg-background border-border border-[0.5px] shadow-lg items-center',
|
||||
props.button ? 'gap-3' : 'gap-4',
|
||||
props.colored && type !== 'custom' && toastBgColorVariants({ type }),
|
||||
props.classNames?.toast
|
||||
),
|
||||
content: cn('flex flex-col', props.description && (props.button ? 'gap-1' : 'gap-2')),
|
||||
title: cn(
|
||||
'text-md font-medium leading-4.5',
|
||||
props.description === undefined && 'text-xs leading-3.5 tracking-normal',
|
||||
props.classNames?.title
|
||||
),
|
||||
description: cn('text-foreground-secondary text-xs leading-3.5 tracking-normal', props.classNames?.description),
|
||||
actionButton: cn(
|
||||
'py-1 px-2 rounded-3xs flex items-center h-7 max-h-7 bg-background-subtle border-[0.5px] border-border',
|
||||
'text-foreground text-sm leading-4 tracking-normal min-w-fit',
|
||||
props.colored && 'bg-white/10',
|
||||
props.classNames?.actionButton
|
||||
),
|
||||
icon: cn('size-6 min-w-6', props.description && 'self-start'),
|
||||
loader: cn('!static ![--size:24px]')
|
||||
}
|
||||
const { classNames: externalClassNames, ...rest } = props
|
||||
delete externalClassNames?.toast
|
||||
const classNames = merge(baseClassNames, externalClassNames)
|
||||
const data = {
|
||||
classNames,
|
||||
unstyled: true,
|
||||
description: rest.description,
|
||||
duration: rest.duration,
|
||||
action: rest.button,
|
||||
dismissible: rest.dismissable,
|
||||
onDismiss: rest.onDismiss
|
||||
} satisfies ExternalToast
|
||||
switch (type) {
|
||||
case 'info':
|
||||
return sonnerToast.info(props.title, data)
|
||||
case 'warning':
|
||||
return sonnerToast.warning(props.title, data)
|
||||
case 'error':
|
||||
return sonnerToast.error(props.title, data)
|
||||
case 'success':
|
||||
return sonnerToast.success(props.title, data)
|
||||
case 'loading':
|
||||
const id = sonnerToast.loading(props.title, data)
|
||||
if (props.promise) {
|
||||
// Auto dismiss when promise is settled
|
||||
props.promise.finally(() => {
|
||||
sonnerToast.dismiss(id)
|
||||
})
|
||||
}
|
||||
return id
|
||||
default:
|
||||
console.warn('Using custom toast without a jsx.')
|
||||
return sonnerToast.custom(props.jsx ?? ((id) => <div id={String(id)}>{props.title}</div>))
|
||||
}
|
||||
}
|
||||
|
||||
interface QuickApiProps extends Omit<ToastProps, 'type' | 'id'> {}
|
||||
interface QuickApiProps extends Omit<ToastProps, 'type' | 'id' | 'title'> {}
|
||||
|
||||
interface QuickLoadingProps extends QuickApiProps {
|
||||
promise: ToastProps['promise']
|
||||
}
|
||||
|
||||
toast.info = (props: QuickApiProps) => {
|
||||
toast.info = (message: ReactNode, data?: QuickApiProps) => {
|
||||
toast({
|
||||
type: 'info',
|
||||
...props
|
||||
title: message,
|
||||
...data
|
||||
})
|
||||
}
|
||||
|
||||
toast.success = (props: QuickApiProps) => {
|
||||
toast.success = (message: ReactNode, data?: QuickApiProps) => {
|
||||
toast({
|
||||
type: 'success',
|
||||
...props
|
||||
title: message,
|
||||
...data
|
||||
})
|
||||
}
|
||||
|
||||
toast.warning = (props: QuickApiProps) => {
|
||||
toast.warning = (message: ReactNode, data?: QuickApiProps) => {
|
||||
toast({
|
||||
type: 'warning',
|
||||
...props
|
||||
title: message,
|
||||
...data
|
||||
})
|
||||
}
|
||||
|
||||
toast.error = (props: QuickApiProps) => {
|
||||
toast.error = (message: ReactNode, data?: QuickApiProps) => {
|
||||
toast({
|
||||
type: 'error',
|
||||
...props
|
||||
title: message,
|
||||
...data
|
||||
})
|
||||
}
|
||||
|
||||
toast.loading = (props: QuickLoadingProps) => {
|
||||
toast.loading = (message: ReactNode, data: QuickLoadingProps) => {
|
||||
toast({
|
||||
type: 'loading',
|
||||
...props
|
||||
title: message,
|
||||
...data
|
||||
})
|
||||
}
|
||||
|
||||
@ -351,125 +430,136 @@ toast.dismiss = (id: ToastProps['id']) => {
|
||||
sonnerToast.dismiss(id)
|
||||
}
|
||||
|
||||
const toastColorVariants = cva(undefined, {
|
||||
variants: {
|
||||
type: {
|
||||
info: 'text-blue-500',
|
||||
warning: 'text-warning-base',
|
||||
error: 'text-error-base',
|
||||
success: 'text-success-base',
|
||||
loading: 'text-foreground-muted'
|
||||
}
|
||||
}
|
||||
})
|
||||
// const toastColorVariants = cva(undefined, {
|
||||
// variants: {
|
||||
// type: {
|
||||
// info: 'text-blue-500',
|
||||
// warning: 'text-warning-base',
|
||||
// error: 'text-error-base',
|
||||
// success: 'text-success-base',
|
||||
// loading: 'text-foreground-muted'
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
|
||||
const toastBgColorVariants = cva(undefined, {
|
||||
variants: {
|
||||
type: {
|
||||
info: 'bg-blue-500/10 border-blue-500/20',
|
||||
warning: 'bg-orange-500/10 border-orange-500/20',
|
||||
error: 'bg-red-500/10 border-red-500/20',
|
||||
success: 'bg-primary/10 border-primary/20',
|
||||
info: 'backdrop-blur-md bg-blue-500/10 border-blue-500/20',
|
||||
warning: 'backdrop-blur-md bg-orange-500/10 border-orange-500/20',
|
||||
error: 'backdrop-blur-md bg-red-500/10 border-red-500/20',
|
||||
success: 'backdrop-blur-md bg-primary/10 border-primary/20',
|
||||
loading: 'backdrop-blur-none'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function Toast({
|
||||
id,
|
||||
type,
|
||||
title,
|
||||
description,
|
||||
coloredMessage,
|
||||
coloredBackground,
|
||||
dismissable,
|
||||
onDismiss,
|
||||
button,
|
||||
link
|
||||
}: ToastProps) {
|
||||
const icon = useMemo(() => {
|
||||
switch (type) {
|
||||
case 'info':
|
||||
return <InfoIcon className="size-6" />
|
||||
case 'error':
|
||||
return <ErrorIcon className="size-6" />
|
||||
case 'loading':
|
||||
return <Loader2Icon className="size-6 animate-spin" />
|
||||
case 'success':
|
||||
return <SuccessIcon className="size-6" />
|
||||
case 'warning':
|
||||
return <WarningIcon className="size-6" />
|
||||
}
|
||||
}, [type])
|
||||
// function Toast({
|
||||
// id,
|
||||
// type,
|
||||
// title,
|
||||
// description,
|
||||
// coloredMessage,
|
||||
// colored: coloredBackground,
|
||||
// dismissable,
|
||||
// onDismiss,
|
||||
// button,
|
||||
// link
|
||||
// }: ToastProps) {
|
||||
// const icon = useMemo(() => {
|
||||
// switch (type) {
|
||||
// case 'info':
|
||||
// return <InfoIcon className="size-6" />
|
||||
// case 'error':
|
||||
// return <ErrorIcon className="size-6" />
|
||||
// case 'loading':
|
||||
// return <Loader2Icon className="size-6 animate-spin" />
|
||||
// case 'success':
|
||||
// return <SuccessIcon className="size-6" />
|
||||
// case 'warning':
|
||||
// return <WarningIcon className="size-6" />
|
||||
// }
|
||||
// }, [type])
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
sonnerToast.dismiss(id)
|
||||
onDismiss?.()
|
||||
}, [id, onDismiss])
|
||||
// const handleDismiss = useCallback(() => {
|
||||
// sonnerToast.dismiss(id)
|
||||
// onDismiss?.()
|
||||
// }, [id, onDismiss])
|
||||
|
||||
// return (
|
||||
// <div
|
||||
// id={String(id)}
|
||||
// className={cn(
|
||||
// 'flex p-4 rounded-xs bg-background border-border border-[0.5px] items-center shadow-lg',
|
||||
// coloredBackground && toastBgColorVariants({ type })
|
||||
// )}
|
||||
// aria-label="Toast">
|
||||
// {dismissable && (
|
||||
// <button type="button" aria-label="Dismiss the toast" onClick={handleDismiss}>
|
||||
// <CloseIcon className="size-5 absolute top-[5px] right-1.5" />
|
||||
// </button>
|
||||
// )}
|
||||
// <div className={cn('flex items-start flex-1', button !== undefined ? 'gap-3' : 'gap-4')}>
|
||||
// {icon}
|
||||
// <div className="cs-toast-content flex flex-col gap-1">
|
||||
// <div className="cs-toast-title font-medium leading-4.5" role="heading">
|
||||
// {title}
|
||||
// </div>
|
||||
// <div className="cs-toast-description">
|
||||
// <p className="text-foreground-secondary text-xs leading-3.5 tracking-normal">
|
||||
// {coloredMessage && <span className={toastColorVariants({ type })}>{coloredMessage} </span>}
|
||||
// {description}
|
||||
// </p>
|
||||
// </div>
|
||||
// {link && (
|
||||
// // FIXME: missing typography/typography components/p/letter-spacing
|
||||
// <div className="cs-toast-link text-foreground-muted text-xs leading-3.5 tracking-normal">
|
||||
// <a
|
||||
// href={link.href}
|
||||
// onClick={link.onClick}
|
||||
// className={cn(
|
||||
// 'underline decoration-foreground-muted cursor-pointer',
|
||||
// 'hover:text-foreground-secondary',
|
||||
// // FIXME: missing active style in design
|
||||
// 'active:text-black'
|
||||
// )}>
|
||||
// {link.label}
|
||||
// </a>
|
||||
// </div>
|
||||
// )}
|
||||
// </div>
|
||||
// </div>
|
||||
// {button !== undefined && (
|
||||
// <button
|
||||
// type="button"
|
||||
// // FIXME: missing hover/active style
|
||||
// className={cn(
|
||||
// 'py-1 px-2 rounded-3xs flex items-center h-7 bg-background-subtle border-[0.5px] border-border',
|
||||
// 'text-foreground text-sm leading-4 tracking-normal',
|
||||
// button.icon !== undefined && 'gap-2'
|
||||
// )}
|
||||
// onClick={button.onClick}>
|
||||
// <div>{button.icon}</div>
|
||||
// <div>{button.label}</div>
|
||||
// </button>
|
||||
// )}
|
||||
// </div>
|
||||
// )
|
||||
// }
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
return (
|
||||
<div
|
||||
id={String(id)}
|
||||
className={cn(
|
||||
'flex p-4 rounded-xs bg-background border-border border-[0.5px] items-center shadow-lg',
|
||||
coloredBackground && toastBgColorVariants({ type })
|
||||
)}
|
||||
aria-label="Toast">
|
||||
{dismissable && (
|
||||
<button type="button" aria-label="Dismiss the toast" onClick={handleDismiss}>
|
||||
<CloseIcon className="size-5 absolute top-[5px] right-1.5" />
|
||||
</button>
|
||||
)}
|
||||
<div className={cn('flex items-start flex-1', button !== undefined ? 'gap-3' : 'gap-4')}>
|
||||
{icon}
|
||||
<div className="cs-toast-content flex flex-col gap-1">
|
||||
<div className="cs-toast-title font-medium leading-4.5" role="heading">
|
||||
{title}
|
||||
</div>
|
||||
<div className="cs-toast-description">
|
||||
<p className="text-foreground-secondary text-xs leading-3.5 tracking-normal">
|
||||
{coloredMessage && <span className={toastColorVariants({ type })}>{coloredMessage} </span>}
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
{link && (
|
||||
// FIXME: missing typography/typography components/p/letter-spacing
|
||||
<div className="cs-toast-link text-foreground-muted text-xs leading-3.5 tracking-normal">
|
||||
<a
|
||||
href={link.href}
|
||||
onClick={link.onClick}
|
||||
className={cn(
|
||||
'underline decoration-foreground-muted cursor-pointer',
|
||||
'hover:text-foreground-secondary',
|
||||
// FIXME: missing active style in design
|
||||
'active:text-black'
|
||||
)}>
|
||||
{link.label}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{button !== undefined && (
|
||||
<button
|
||||
type="button"
|
||||
// FIXME: missing hover/active style
|
||||
className={cn(
|
||||
'py-1 px-2 rounded-3xs flex items-center h-7 bg-background-subtle border-[0.5px] border-border',
|
||||
'text-foreground text-sm leading-4 tracking-normal',
|
||||
button.icon !== undefined && 'gap-2'
|
||||
)}
|
||||
onClick={button.onClick}>
|
||||
<div>{button.icon}</div>
|
||||
<div>{button.label}</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<Sonner
|
||||
className="toaster group"
|
||||
icons={{
|
||||
info: <InfoIcon />,
|
||||
success: <SuccessIcon />,
|
||||
warning: <WarningIcon />,
|
||||
error: <ErrorIcon />
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
return <Sonner className="toaster group" {...props} />
|
||||
}
|
||||
|
||||
export { toast, Toaster }
|
||||
|
||||
@ -1,7 +1,16 @@
|
||||
import { Button } from '@cherrystudio/ui'
|
||||
import { toast, Toaster } from '@cherrystudio/ui'
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import { RefreshCwIcon } from 'lucide-react'
|
||||
|
||||
interface PlaygroundArgs {
|
||||
type: 'info' | 'success' | 'warning' | 'error' | 'loading'
|
||||
title: string
|
||||
description: string
|
||||
colored: boolean
|
||||
duration: number
|
||||
withButton: boolean
|
||||
buttonLabel: string
|
||||
}
|
||||
|
||||
const meta: Meta<typeof Toaster> = {
|
||||
title: 'Components/Primitives/Sonner',
|
||||
@ -29,14 +38,113 @@ const meta: Meta<typeof Toaster> = {
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
// Playground
|
||||
export const Playground: StoryObj<PlaygroundArgs> = {
|
||||
args: {
|
||||
type: 'info',
|
||||
title: 'Notification Title',
|
||||
description: 'This is a description that provides more details about the notification.',
|
||||
colored: false,
|
||||
duration: 4000,
|
||||
withButton: false,
|
||||
buttonLabel: 'Action'
|
||||
},
|
||||
argTypes: {
|
||||
type: {
|
||||
control: 'select',
|
||||
options: ['info', 'success', 'warning', 'error', 'loading'],
|
||||
description: 'Type of toast notification'
|
||||
},
|
||||
title: {
|
||||
control: 'text',
|
||||
description: 'Main message of the toast'
|
||||
},
|
||||
description: {
|
||||
control: 'text',
|
||||
description: 'Optional detailed description'
|
||||
},
|
||||
colored: {
|
||||
control: 'boolean',
|
||||
description: 'Enable colored background'
|
||||
},
|
||||
duration: {
|
||||
control: { type: 'number', min: 1000, max: 10000, step: 1000 },
|
||||
description: 'Duration in milliseconds (use Infinity for persistent)'
|
||||
},
|
||||
withButton: {
|
||||
control: 'boolean',
|
||||
description: 'Show action button'
|
||||
},
|
||||
buttonLabel: {
|
||||
control: 'text',
|
||||
description: 'Label for the action button',
|
||||
if: { arg: 'withButton', truthy: true }
|
||||
}
|
||||
},
|
||||
render: (args: PlaygroundArgs) => {
|
||||
const handleToast = () => {
|
||||
const toastOptions: {
|
||||
description?: string
|
||||
colored: boolean
|
||||
duration: number
|
||||
button?: {
|
||||
label: string
|
||||
onClick: () => void
|
||||
}
|
||||
promise?: Promise<void>
|
||||
} = {
|
||||
description: args.description || undefined,
|
||||
colored: args.colored,
|
||||
duration: args.duration
|
||||
}
|
||||
|
||||
if (args.withButton) {
|
||||
toastOptions.button = {
|
||||
label: args.buttonLabel || 'Action',
|
||||
onClick: () => toast.info('Button clicked!')
|
||||
}
|
||||
}
|
||||
|
||||
switch (args.type) {
|
||||
case 'info':
|
||||
toast.info(args.title, toastOptions)
|
||||
break
|
||||
case 'success':
|
||||
toast.success(args.title, toastOptions)
|
||||
break
|
||||
case 'warning':
|
||||
toast.warning(args.title, toastOptions)
|
||||
break
|
||||
case 'error':
|
||||
toast.error(args.title, toastOptions)
|
||||
break
|
||||
case 'loading':
|
||||
toast.loading(args.title, {
|
||||
...toastOptions,
|
||||
promise: new Promise<void>((resolve) => setTimeout(resolve, 2000))
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button onClick={handleToast}>Show Toast</Button>
|
||||
<div className="text-sm text-muted-foreground max-w-md">
|
||||
Use the controls panel below to customize the toast properties and click the button to preview.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Basic Toast Types
|
||||
export const Info: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.info({
|
||||
title: 'Information',
|
||||
toast.info('Information', {
|
||||
description: 'This is an informational message.'
|
||||
})
|
||||
}>
|
||||
@ -51,8 +159,7 @@ export const Success: Story = {
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.success({
|
||||
title: 'Success!',
|
||||
toast.success('Success!', {
|
||||
description: 'Operation completed successfully.'
|
||||
})
|
||||
}>
|
||||
@ -67,8 +174,7 @@ export const ErrorToast: Story = {
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.error({
|
||||
title: 'Error',
|
||||
toast.error('Error', {
|
||||
description: 'Something went wrong. Please try again.'
|
||||
})
|
||||
}>
|
||||
@ -83,8 +189,7 @@ export const Warning: Story = {
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.warning({
|
||||
title: 'Warning',
|
||||
toast.warning('Warning', {
|
||||
description: 'Please be careful with this action.'
|
||||
})
|
||||
}>
|
||||
@ -104,8 +209,7 @@ export const Loading: Story = {
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.loading({
|
||||
title: 'Loading...',
|
||||
toast.loading('Loading...', {
|
||||
description: 'Please wait while we process your request.',
|
||||
promise: mockPromise
|
||||
})
|
||||
@ -121,14 +225,13 @@ export const Loading: Story = {
|
||||
export const AllTypes: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button onClick={() => toast.info({ title: 'Info Toast' })}>Info</Button>
|
||||
<Button onClick={() => toast.success({ title: 'Success Toast' })}>Success</Button>
|
||||
<Button onClick={() => toast.warning({ title: 'Warning Toast' })}>Warning</Button>
|
||||
<Button onClick={() => toast.error({ title: 'Error Toast' })}>Error</Button>
|
||||
<Button onClick={() => toast.info('Info Toast')}>Info</Button>
|
||||
<Button onClick={() => toast.success('Success Toast')}>Success</Button>
|
||||
<Button onClick={() => toast.warning('Warning Toast')}>Warning</Button>
|
||||
<Button onClick={() => toast.error('Error Toast')}>Error</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.loading({
|
||||
title: 'Loading Toast',
|
||||
toast.loading('Loading Toast', {
|
||||
promise: new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
})
|
||||
}>
|
||||
@ -144,8 +247,7 @@ export const WithDescription: Story = {
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.success({
|
||||
title: 'Event Created',
|
||||
toast.success('Event Created', {
|
||||
description: 'Your event has been created successfully. You can now share it with others.'
|
||||
})
|
||||
}>
|
||||
@ -155,29 +257,88 @@ export const WithDescription: Story = {
|
||||
)
|
||||
}
|
||||
|
||||
// With Colored Message
|
||||
export const WithColoredMessage: Story = {
|
||||
// With Custom Duration
|
||||
export const WithCustomDuration: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.info({
|
||||
title: 'System Update',
|
||||
coloredMessage: 'New version available!',
|
||||
description: 'Click the button to update now.'
|
||||
toast.info('Quick message', {
|
||||
description: 'This will disappear in 1 second',
|
||||
duration: 1000
|
||||
})
|
||||
}>
|
||||
Info with Colored Message
|
||||
1 Second
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.warning({
|
||||
title: 'Disk Space Low',
|
||||
coloredMessage: '95% used',
|
||||
description: 'Please free up some space.'
|
||||
toast.success('Normal duration', {
|
||||
description: 'This uses default duration (4 seconds)'
|
||||
})
|
||||
}>
|
||||
Warning with Colored Message
|
||||
Default (4s)
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.warning('Important message', {
|
||||
description: 'This will stay for 10 seconds',
|
||||
duration: 10000
|
||||
})
|
||||
}>
|
||||
10 Seconds
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.info('Persistent message', {
|
||||
description: 'This will stay until manually dismissed',
|
||||
duration: Number.POSITIVE_INFINITY
|
||||
})
|
||||
}>
|
||||
Infinite
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// With Action Button
|
||||
export const WithActionButton: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.success('Changes Saved', {
|
||||
description: 'Your changes have been saved successfully.',
|
||||
button: {
|
||||
label: 'Undo',
|
||||
onClick: () => toast.info('Undoing changes...')
|
||||
}
|
||||
})
|
||||
}>
|
||||
Success with Action
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.error('Update Failed', {
|
||||
description: 'Failed to update the record.',
|
||||
button: {
|
||||
label: 'Retry',
|
||||
onClick: () => toast.info('Retrying...')
|
||||
}
|
||||
})
|
||||
}>
|
||||
Error with Action
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.info('Update Available', {
|
||||
description: 'A new version is ready to install.',
|
||||
button: {
|
||||
label: 'Update',
|
||||
onClick: () => toast.info('Starting update...')
|
||||
}
|
||||
})
|
||||
}>
|
||||
Info with Action
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
@ -189,40 +350,36 @@ export const WithColoredBackground: Story = {
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.info({
|
||||
title: 'Information',
|
||||
toast.info('Information', {
|
||||
description: 'This toast has a colored background.',
|
||||
coloredBackground: true
|
||||
colored: true
|
||||
})
|
||||
}>
|
||||
Info Background
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.success({
|
||||
title: 'Success!',
|
||||
toast.success('Success!', {
|
||||
description: 'This toast has a colored background.',
|
||||
coloredBackground: true
|
||||
colored: true
|
||||
})
|
||||
}>
|
||||
Success Background
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.warning({
|
||||
title: 'Warning',
|
||||
toast.warning('Warning', {
|
||||
description: 'This toast has a colored background.',
|
||||
coloredBackground: true
|
||||
colored: true
|
||||
})
|
||||
}>
|
||||
Warning Background
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.error({
|
||||
title: 'Error',
|
||||
toast.error('Error', {
|
||||
description: 'This toast has a colored background.',
|
||||
coloredBackground: true
|
||||
colored: true
|
||||
})
|
||||
}>
|
||||
Error Background
|
||||
@ -231,19 +388,18 @@ export const WithColoredBackground: Story = {
|
||||
)
|
||||
}
|
||||
|
||||
// Colored Background with Actions
|
||||
export const ColoredBackgroundWithActions: Story = {
|
||||
// Colored Background with Action
|
||||
export const ColoredBackgroundWithAction: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.success({
|
||||
title: 'File Uploaded',
|
||||
toast.success('File Uploaded', {
|
||||
description: 'Your file has been uploaded successfully.',
|
||||
coloredBackground: true,
|
||||
colored: true,
|
||||
button: {
|
||||
label: 'View',
|
||||
onClick: () => toast.info({ title: 'Opening file...' })
|
||||
onClick: () => toast.info('Opening file...')
|
||||
}
|
||||
})
|
||||
}>
|
||||
@ -251,138 +407,29 @@ export const ColoredBackgroundWithActions: Story = {
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.warning({
|
||||
title: 'Action Required',
|
||||
toast.warning('Action Required', {
|
||||
description: 'Please review the changes.',
|
||||
coloredBackground: true,
|
||||
link: {
|
||||
colored: true,
|
||||
button: {
|
||||
label: 'Review',
|
||||
onClick: () => toast.info({ title: 'Opening review...' })
|
||||
onClick: () => toast.info('Opening review...')
|
||||
}
|
||||
})
|
||||
}>
|
||||
Warning with Link
|
||||
Warning with Button
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.error({
|
||||
title: 'Update Failed',
|
||||
toast.error('Update Failed', {
|
||||
description: 'Failed to update the record.',
|
||||
coloredBackground: true,
|
||||
colored: true,
|
||||
button: {
|
||||
icon: <RefreshCwIcon className="h-4 w-4" />,
|
||||
label: 'Retry',
|
||||
onClick: () => toast.info({ title: 'Retrying...' })
|
||||
},
|
||||
link: {
|
||||
label: 'Learn More',
|
||||
onClick: () => toast.info({ title: 'Opening help...' })
|
||||
onClick: () => toast.info('Retrying...')
|
||||
}
|
||||
})
|
||||
}>
|
||||
Error with Button & Link
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// With Action Button
|
||||
export const WithActionButton: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.success({
|
||||
title: 'Changes Saved',
|
||||
description: 'Your changes have been saved successfully.',
|
||||
button: {
|
||||
icon: <RefreshCwIcon className="h-4 w-4" />,
|
||||
label: 'Undo',
|
||||
onClick: () => toast.info({ title: 'Undoing changes...' })
|
||||
}
|
||||
})
|
||||
}>
|
||||
Show Toast with Action Button
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// With Link
|
||||
export const WithLink: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.info({
|
||||
title: 'Update Available',
|
||||
description: 'A new version is ready to install.',
|
||||
link: {
|
||||
label: 'View Details',
|
||||
onClick: () => toast.info({ title: 'Opening details...' })
|
||||
}
|
||||
})
|
||||
}>
|
||||
Toast with Click Handler
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.success({
|
||||
title: 'Documentation Updated',
|
||||
description: 'Check out the new features.',
|
||||
link: {
|
||||
label: 'Read More',
|
||||
href: 'https://example.com',
|
||||
onClick: () => console.log('Link clicked')
|
||||
}
|
||||
})
|
||||
}>
|
||||
Toast with Link
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// With Button and Link
|
||||
export const WithButtonAndLink: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.warning({
|
||||
title: 'Action Required',
|
||||
description: 'Please review the changes before proceeding.',
|
||||
button: {
|
||||
icon: <RefreshCwIcon className="h-4 w-4" />,
|
||||
label: 'Review',
|
||||
onClick: () => toast.info({ title: 'Opening review...' })
|
||||
},
|
||||
link: {
|
||||
label: 'Learn More',
|
||||
onClick: () => toast.info({ title: 'Opening documentation...' })
|
||||
}
|
||||
})
|
||||
}>
|
||||
Show Toast with Button and Link
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Dismissable Toast
|
||||
export const DismissableToast: Story = {
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button
|
||||
onClick={() =>
|
||||
toast.info({
|
||||
title: 'Dismissable Toast',
|
||||
description: 'You can close this toast by clicking the X button.',
|
||||
dismissable: true,
|
||||
onDismiss: () => console.log('Toast dismissed')
|
||||
})
|
||||
}>
|
||||
Show Dismissable Toast
|
||||
Error with Button
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
@ -392,10 +439,10 @@ export const DismissableToast: Story = {
|
||||
export const MultipleToasts: Story = {
|
||||
render: () => {
|
||||
const showMultiple = () => {
|
||||
toast.success({ title: 'First notification', description: 'This is the first message' })
|
||||
setTimeout(() => toast.info({ title: 'Second notification', description: 'This is the second message' }), 100)
|
||||
setTimeout(() => toast.warning({ title: 'Third notification', description: 'This is the third message' }), 200)
|
||||
setTimeout(() => toast.error({ title: 'Fourth notification', description: 'This is the fourth message' }), 300)
|
||||
toast.success('First notification', { description: 'This is the first message' })
|
||||
setTimeout(() => toast.info('Second notification', { description: 'This is the second message' }), 100)
|
||||
setTimeout(() => toast.warning('Third notification', { description: 'This is the third message' }), 200)
|
||||
setTimeout(() => toast.error('Fourth notification', { description: 'This is the fourth message' }), 300)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -416,8 +463,7 @@ export const PromiseExample: Story = {
|
||||
}, 2000)
|
||||
})
|
||||
|
||||
toast.loading({
|
||||
title: 'Fetching data...',
|
||||
toast.loading('Fetching data...', {
|
||||
description: 'Please wait while we load your information.',
|
||||
promise
|
||||
})
|
||||
@ -436,60 +482,54 @@ export const RealWorldExamples: Story = {
|
||||
render: () => {
|
||||
const handleFileSave = () => {
|
||||
const promise = new Promise((resolve) => setTimeout(resolve, 1500))
|
||||
toast.loading({
|
||||
title: 'Saving file...',
|
||||
toast.loading('Saving file...', {
|
||||
promise
|
||||
})
|
||||
promise.then(() => {
|
||||
toast.success({
|
||||
title: 'File saved',
|
||||
description: 'Your file has been saved successfully.'
|
||||
toast.success('File saved', {
|
||||
description: 'Your file has been saved successfully.',
|
||||
button: {
|
||||
label: 'View',
|
||||
onClick: () => toast.info('Opening file...')
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const handleFormSubmit = () => {
|
||||
toast.success({
|
||||
title: 'Form submitted',
|
||||
toast.success('Form submitted', {
|
||||
description: 'Your changes have been saved successfully.',
|
||||
button: {
|
||||
label: 'View',
|
||||
onClick: () => toast.info({ title: 'Opening form...' })
|
||||
label: 'Undo',
|
||||
onClick: () => toast.info('Undoing changes...')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
toast.error({
|
||||
title: 'Failed to delete',
|
||||
toast.error('Failed to delete', {
|
||||
description: 'You do not have permission to delete this item.',
|
||||
button: {
|
||||
icon: <RefreshCwIcon className="h-4 w-4" />,
|
||||
label: 'Retry',
|
||||
onClick: () => toast.info({ title: 'Retrying...' })
|
||||
onClick: () => toast.info('Retrying...')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText('https://example.com')
|
||||
toast.success({
|
||||
title: 'Copied to clipboard',
|
||||
toast.success('Copied to clipboard', {
|
||||
description: 'The link has been copied to your clipboard.'
|
||||
})
|
||||
}
|
||||
|
||||
const handleUpdate = () => {
|
||||
toast.info({
|
||||
title: 'Update available',
|
||||
toast.info('Update available', {
|
||||
description: 'A new version of the application is ready to install.',
|
||||
colored: true,
|
||||
button: {
|
||||
label: 'Update Now',
|
||||
onClick: () => toast.info({ title: 'Starting update...' })
|
||||
},
|
||||
link: {
|
||||
label: 'Release Notes',
|
||||
onClick: () => toast.info({ title: 'Opening release notes...' })
|
||||
onClick: () => toast.info('Starting update...')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user