-
-
Notifications
You must be signed in to change notification settings - Fork 135
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
8ac704a
commit ac2d80d
Showing
7 changed files
with
272 additions
and
123 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@saas-ui/react': minor | ||
--- | ||
|
||
Improved toaster styles and allow setting global defaults |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,3 @@ | ||
export * from './toaster.tsx' | ||
export { Toaster, toast } from './toaster.tsx' | ||
|
||
export type { ToasterProps } from './toaster.tsx' |
93 changes: 93 additions & 0 deletions
93
packages/saas-ui-react/src/components/toaster/toast.recipe.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import { defineSlotRecipe } from '@chakra-ui/react' | ||
import { toastAnatomy } from '@chakra-ui/react/anatomy' | ||
|
||
export const toastSlotRecipe = defineSlotRecipe({ | ||
slots: toastAnatomy.keys(), | ||
className: 'chakra-toast', | ||
base: { | ||
root: { | ||
width: 'full', | ||
display: 'flex', | ||
alignItems: 'flex-start', | ||
position: 'relative', | ||
gap: '2', | ||
py: '3', | ||
ps: '3', | ||
pe: '6', | ||
borderRadius: 'md', | ||
borderWidth: '1px', | ||
translate: 'var(--x) var(--y)', | ||
scale: 'var(--scale)', | ||
zIndex: 'var(--z-index)', | ||
height: 'var(--height, var(--toast-height))', | ||
opacity: 'var(--opacity)', | ||
willChange: 'translate, opacity, scale, height', | ||
transition: | ||
'translate 400ms, scale 400ms, opacity 400ms, height 200ms, box-shadow 200ms', | ||
transitionTimingFunction: 'bounce-in', | ||
_closed: { | ||
transition: 'translate 400ms, scale 400ms, height 200ms, opacity 200ms', | ||
transitionTimingFunction: 'bounce-out', | ||
}, | ||
|
||
bg: 'bg.panel', | ||
color: 'fg', | ||
boxShadow: 'lg', | ||
overflow: 'hidden', | ||
'--toast-indicated-color': 'colors.fg.muted', | ||
'&[data-type=warning]': { | ||
'--toast-indicated-color': 'colors.fg.warning', | ||
}, | ||
'&[data-type=success]': { | ||
'--toast-indicated-color': 'colors.fg.success', | ||
}, | ||
'&[data-type=error]': { | ||
'--toast-indicated-color': 'colors.fg.error', | ||
}, | ||
'&[data-overlap]': { | ||
_before: { | ||
content: '""', | ||
position: 'absolute', | ||
inset: '0', | ||
maskImage: 'linear-gradient(to bottom, transparent, black 50%)', | ||
}, | ||
}, | ||
}, | ||
title: { | ||
fontWeight: 'medium', | ||
textStyle: 'sm', | ||
marginEnd: '2', | ||
}, | ||
description: { | ||
display: 'inline', | ||
textStyle: 'sm', | ||
opacity: '0.8', | ||
}, | ||
indicator: { | ||
flexShrink: '0', | ||
boxSize: '5', | ||
color: 'var(--toast-indicated-color)', | ||
}, | ||
actionTrigger: { | ||
cursor: 'button', | ||
textStyle: 'sm', | ||
fontWeight: 'medium', | ||
height: '6', | ||
px: '3', | ||
ms: '-3', | ||
borderRadius: 'sm', | ||
alignSelf: 'flex-start', | ||
transition: 'background 200ms', | ||
color: 'colorPalette.solid/80', | ||
_hover: { | ||
bg: 'bg.subtle', | ||
color: 'colorPalette.solid', | ||
}, | ||
}, | ||
closeTrigger: { | ||
position: 'absolute', | ||
top: '2', | ||
insetEnd: '2', | ||
}, | ||
}, | ||
}) |
89 changes: 89 additions & 0 deletions
89
packages/saas-ui-react/src/components/toaster/toaster.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import React from 'react' | ||
|
||
import { Button, HStack } from '@chakra-ui/react' | ||
|
||
import { Toaster, toast } from './index.ts' | ||
|
||
export default { | ||
title: 'Components/Toaster', | ||
decorators: [ | ||
(Story) => ( | ||
<> | ||
<Toaster overlap /> | ||
<Story /> | ||
</> | ||
), | ||
], | ||
} | ||
|
||
export const Default = () => { | ||
return ( | ||
<HStack> | ||
<Button | ||
onClick={() => toast.create({ title: 'Changes saved successfully' })} | ||
> | ||
Default toast | ||
</Button> | ||
<Button | ||
onClick={() => toast.success({ title: 'Changes saved successfully' })} | ||
> | ||
Success toast | ||
</Button> | ||
<Button | ||
onClick={() => | ||
toast.error({ | ||
title: 'Something went wrong', | ||
description: | ||
'Please try again or contact support if the issue persists.', | ||
}) | ||
} | ||
> | ||
Error toast | ||
</Button> | ||
<Button | ||
onClick={() => | ||
toast.create({ | ||
type: 'warning', | ||
title: 'Connection disrupted', | ||
description: 'Please check your internet connection.', | ||
}) | ||
} | ||
> | ||
Warning toast | ||
</Button> | ||
<Button | ||
onClick={() => | ||
toast.success({ | ||
title: 'Task created', | ||
description: 'TSK-123456', | ||
action: { label: 'View task', onClick: () => alert('Clicked') }, | ||
}) | ||
} | ||
> | ||
Toast with action | ||
</Button> | ||
<Button | ||
onClick={() => | ||
toast.promise( | ||
new Promise((resolve) => { | ||
setTimeout(() => resolve('Task created'), 1000) | ||
}), | ||
{ | ||
loading: { | ||
title: 'Creating your account', | ||
}, | ||
success: { | ||
title: 'Account created', | ||
}, | ||
error: { | ||
title: 'Error', | ||
}, | ||
}, | ||
) | ||
} | ||
> | ||
Promise | ||
</Button> | ||
</HStack> | ||
) | ||
} |
106 changes: 81 additions & 25 deletions
106
packages/saas-ui-react/src/components/toaster/toaster.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,48 +1,104 @@ | ||
'use client' | ||
|
||
import React, { useCallback, useMemo, useState } from 'react' | ||
|
||
import { | ||
Toaster as ChakraToaster, | ||
CreateToasterReturn, | ||
type CreateToasterProps, | ||
Portal, | ||
Spinner, | ||
Stack, | ||
Toast, | ||
createToaster, | ||
} from '@chakra-ui/react' | ||
|
||
export const toast = createToaster({ | ||
import { CloseButton } from '#components/close-button/close-button.js' | ||
|
||
const defaultOptions: CreateToasterProps = { | ||
placement: 'bottom-end', | ||
pauseOnPageIdle: true, | ||
}) | ||
} | ||
|
||
export interface ToasterProps extends CreateToasterReturn { | ||
children: React.ReactNode | ||
export let toast = createToaster(defaultOptions) | ||
|
||
export interface ToasterProps extends Partial<CreateToasterProps> { | ||
closable?: boolean | ||
} | ||
|
||
export const Toaster = () => { | ||
export const Toaster = (props?: ToasterProps) => { | ||
const { closable: defaultClosable = true, ...options } = props || {} | ||
|
||
const toaster = useMemo(() => { | ||
toast = createToaster({ | ||
...defaultOptions, | ||
...options, | ||
}) | ||
|
||
return toast | ||
}, [options]) | ||
|
||
return ( | ||
<Portal> | ||
<ChakraToaster toaster={toast} insetInline={{ mdDown: '4' }}> | ||
{(toast) => ( | ||
<Toast.Root width={{ md: 'sm' }}> | ||
{toast.type === 'loading' ? ( | ||
<Spinner size="sm" color="colorPalette.solid" /> | ||
) : ( | ||
<Toast.Indicator /> | ||
)} | ||
<Stack gap="1" flex="1" maxWidth="100%"> | ||
{toast.title && <Toast.Title>{toast.title}</Toast.Title>} | ||
{toast.description && ( | ||
<Toast.Description>{toast.description}</Toast.Description> | ||
<ChakraToaster toaster={toaster} insetInline={{ mdDown: '4' }}> | ||
{(toast) => { | ||
const closable = | ||
toast.meta?.closable === false | ||
? false | ||
: defaultClosable && toast.type !== 'loading' | ||
|
||
return ( | ||
<ToastRoot> | ||
{toast.type === 'loading' ? ( | ||
<Spinner size="sm" color="colorPalette.solid" mt="0.5" /> | ||
) : ( | ||
<Toast.Indicator /> | ||
)} | ||
</Stack> | ||
{toast.action && ( | ||
<Toast.ActionTrigger>{toast.action.label}</Toast.ActionTrigger> | ||
)} | ||
{toast.meta?.closable && <Toast.CloseTrigger />} | ||
</Toast.Root> | ||
)} | ||
<Stack gap="1" flex="1" maxWidth="100%"> | ||
{toast.title && <Toast.Title>{toast.title}</Toast.Title>} | ||
{toast.description && ( | ||
<Toast.Description>{toast.description}</Toast.Description> | ||
)} | ||
|
||
{toast.action && ( | ||
<Toast.ActionTrigger> | ||
{toast.action.label} | ||
</Toast.ActionTrigger> | ||
)} | ||
</Stack> | ||
|
||
{closable !== false && ( | ||
<Toast.CloseTrigger> | ||
<CloseButton size="xs" /> | ||
</Toast.CloseTrigger> | ||
)} | ||
</ToastRoot> | ||
) | ||
}} | ||
</ChakraToaster> | ||
</Portal> | ||
) | ||
} | ||
|
||
/** | ||
* Since the height of the toast is dynamic, we need to set the height | ||
* in the CSS variable `--toast-height` so css transitions can be applied. | ||
*/ | ||
function ToastRoot(props: { children: React.ReactNode }) { | ||
const [rect, setRect] = useState<DOMRect>() | ||
|
||
const rectCallbackRef = useCallback((el: HTMLDivElement) => { | ||
setRect(el?.getBoundingClientRect()) | ||
}, []) | ||
|
||
return ( | ||
<Toast.Root | ||
ref={rectCallbackRef} | ||
width={{ md: 'sm' }} | ||
css={{ | ||
'--toast-height': `${rect?.height}px`, | ||
}} | ||
> | ||
{props.children} | ||
</Toast.Root> | ||
) | ||
} |
Oops, something went wrong.