diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index 95b8ca8f..e7e42812 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -6,7 +6,7 @@ import '@tamagui/font-inter/css/700.css' import { StrictMode } from 'react' -import { Provider } from '@status-im/components' +import { Provider, ToastContainer } from '@status-im/components' import { createRoot } from 'react-dom/client' import App from './app' @@ -17,6 +17,7 @@ createRoot(root).render( + ) diff --git a/apps/web/styles/app.css b/apps/web/styles/app.css index f1e960bf..cfecd650 100644 --- a/apps/web/styles/app.css +++ b/apps/web/styles/app.css @@ -17,6 +17,7 @@ body, } #app { + isolation: isolate; height: 100%; display: grid; grid-template-columns: 352px 1fr auto; diff --git a/packages/components/.storybook/preview.tsx b/packages/components/.storybook/preview.tsx index 0d1d407e..2941a73d 100644 --- a/packages/components/.storybook/preview.tsx +++ b/packages/components/.storybook/preview.tsx @@ -1,4 +1,4 @@ -import { Provider } from '../src' +import { Provider, ToastContainer } from '../src' import { Parameters, Decorator } from '@storybook/react' import './reset.css' @@ -17,6 +17,7 @@ const withThemeProvider: Decorator = (Story, _context) => { return ( + ) } diff --git a/packages/components/package.json b/packages/components/package.json index 310c3279..fdc5743c 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -33,6 +33,7 @@ "@radix-ui/react-dropdown-menu": "^2.0.4", "@radix-ui/react-popover": "^1.0.5", "@radix-ui/react-tabs": "^1.0.3", + "@radix-ui/react-toast": "^1.1.3", "@radix-ui/react-tooltip": "^1.0.5", "@status-im/icons": "*", "@tamagui/animations-css": "1.7.7", diff --git a/packages/components/src/index.tsx b/packages/components/src/index.tsx index 0470d07c..f158813f 100644 --- a/packages/components/src/index.tsx +++ b/packages/components/src/index.tsx @@ -12,6 +12,7 @@ export * from './provider' export * from './sidebar' export * from './sidebar-members' export * from './text' +export * from './toast' export * from './topbar' export * from './user-list' diff --git a/packages/components/src/toast/index.tsx b/packages/components/src/toast/index.tsx new file mode 100644 index 00000000..a8612795 --- /dev/null +++ b/packages/components/src/toast/index.tsx @@ -0,0 +1,3 @@ +export type { ToastProps } from './toast' +export { Toast } from './toast' +export { ToastContainer, useToast } from './toast-container' diff --git a/packages/components/src/toast/toast-container.tsx b/packages/components/src/toast/toast-container.tsx new file mode 100644 index 00000000..ed15f6e1 --- /dev/null +++ b/packages/components/src/toast/toast-container.tsx @@ -0,0 +1,89 @@ +import { useMemo } from 'react' + +import { Provider, Root, Viewport } from '@radix-ui/react-toast' +import { styled } from 'tamagui' +import { create } from 'zustand' + +import { Toast } from './toast' + +import type { ToastProps } from './toast' + +type ToastState = { + toast: ToastProps | null + dismiss: () => void + positive: ( + message: string, + actionProps?: Pick + ) => void + negative: ( + message: string, + actionProps?: Pick + ) => void + custom: ( + message: string, + icon: React.ReactElement, + actionProps?: Pick + ) => void +} + +const useStore = create()(set => ({ + toast: null, + positive: (message, actionProps) => + set({ toast: { ...actionProps, message, type: 'positive' } }), + negative: (message, actionProps) => + set({ toast: { ...actionProps, message, type: 'negative' } }), + custom: (message, icon, actionProps) => + set({ toast: { ...actionProps, message, icon } }), + dismiss: () => set({ toast: null }), +})) + +const ToastContainer = () => { + const store = useStore() + + if (store.toast === null) { + return null + } + + const handleOpenChange = (open: boolean) => { + if (!open) { + store.dismiss() + } + } + + return ( + + + + + + + ) +} + +const useToast = () => { + const store = useStore() + + return useMemo( + () => ({ + positive: store.positive, + negative: store.negative, + custom: store.custom, + }), + [store] + ) +} + +export { ToastContainer, useToast } + +const ToastRoot = styled(Root, { + name: 'ToastRoot', + acceptsClassName: true, + + bottom: 12, + right: 12, + zIndex: 1000, +}) diff --git a/packages/components/src/toast/toast.stories.tsx b/packages/components/src/toast/toast.stories.tsx new file mode 100644 index 00000000..346ded2c --- /dev/null +++ b/packages/components/src/toast/toast.stories.tsx @@ -0,0 +1,73 @@ +import { PlaceholderIcon } from '@status-im/icons/20' +import { Stack } from '@tamagui/core' + +import { Button } from '../button' +import { Toast, useToast } from './' + +import type { Meta, StoryObj } from '@storybook/react' + +const meta: Meta = { + component: Toast, + args: {}, + argTypes: {}, + parameters: { + design: { + type: 'figma', + url: 'https://www.figma.com/file/IBmFKgGL1B4GzqD8LQTw6n/Design-System-for-Desktop%2FWeb?node-id=3928-77614&t=hp2XwjFgrFl3hhDm-4', + }, + }, +} + +type Story = StoryObj + +const ToastWithHook = () => { + const toast = useToast() + + return ( + + ) +} + +export const Default: Story = { + args: {}, + render: () => ( + + + + ), +} + +export const AllVariants: Story = { + args: {}, + render: () => ( + + + + } message="Something happened" /> + + + + + ), +} + +export default meta diff --git a/packages/components/src/toast/toast.tsx b/packages/components/src/toast/toast.tsx new file mode 100644 index 00000000..7361e23b --- /dev/null +++ b/packages/components/src/toast/toast.tsx @@ -0,0 +1,91 @@ +import { cloneElement, forwardRef } from 'react' + +import { Action, Description } from '@radix-ui/react-toast' +import { CorrectIcon, IncorrectIcon } from '@status-im/icons/20' +import { Stack, styled } from '@tamagui/core' + +import { Button } from '../button' +import { Text } from '../text' + +type Props = { + message: string + action?: string + onAction?: () => void +} & ( + | { + type: 'positive' | 'negative' + } + | { + type?: never + icon: React.ReactElement + } +) + +const Toast = (props: Props) => { + const { message, action, onAction } = props + + const renderIcon = () => { + if (!props.type) { + return cloneElement(props.icon, { color: '$white-70' }) + } + + if (props.type === 'positive') { + return + } + + return + } + + return ( + + + {renderIcon()} + + + {message} + + + + {action && ( + + + + + + )} + + ) +} + +const _Toast = forwardRef(Toast) + +export { _Toast as Toast } +export type { Props as ToastProps } + +const Base = styled(Stack, { + name: 'Toast', + + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 8, + gap: 12, + width: 351, + minHeight: 40, + backgroundColor: '$neutral-80-opa-70', + borderRadius: 12, + justifyContent: 'space-between', + + variants: { + action: { + true: { + paddingVertical: 8, + }, + false: { + paddingVertical: 10, + }, + }, + }, +}) diff --git a/packages/icons/20/correct-icon.tsx b/packages/icons/20/correct-icon.tsx index d6108798..f374c527 100644 --- a/packages/icons/20/correct-icon.tsx +++ b/packages/icons/20/correct-icon.tsx @@ -18,15 +18,8 @@ const SvgCorrectIcon = (props: IconProps) => { xmlns="http://www.w3.org/2000/svg" {...props} > - - + + ) } diff --git a/packages/icons/20/incorrect-icon.tsx b/packages/icons/20/incorrect-icon.tsx new file mode 100644 index 00000000..bf9077b6 --- /dev/null +++ b/packages/icons/20/incorrect-icon.tsx @@ -0,0 +1,31 @@ +import { useTheme } from '@tamagui/core' +import { Circle, Path, Svg } from 'react-native-svg' + +import type { IconProps } from '../types' + +const SvgIncorrectIcon = (props: IconProps) => { + const { color: token = '$neutral-100' } = props + const theme = useTheme() + /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const color = theme[token]?.val ?? token + return ( + + + + + ) +} +export default SvgIncorrectIcon diff --git a/packages/icons/20/index.ts b/packages/icons/20/index.ts index cce26a42..9321de0a 100644 --- a/packages/icons/20/index.ts +++ b/packages/icons/20/index.ts @@ -85,6 +85,7 @@ export { default as HistoryIcon } from './history-icon' export { default as HoldIcon } from './hold-icon' export { default as ImageIcon } from './image-icon' export { default as InactiveIcon } from './inactive-icon' +export { default as IncorrectIcon } from './incorrect-icon' export { default as InfoBadgeIcon } from './info-badge-icon' export { default as InfoIcon } from './info-icon' export { default as ItalicIcon } from './italic-icon' diff --git a/packages/icons/src/20/correct-icon.svg b/packages/icons/src/20/correct-icon.svg index 7dab968d..5a5656e2 100644 --- a/packages/icons/src/20/correct-icon.svg +++ b/packages/icons/src/20/correct-icon.svg @@ -5,17 +5,10 @@ fill="none" xmlns="http://www.w3.org/2000/svg" > - + diff --git a/packages/icons/src/20/incorrect-icon.svg b/packages/icons/src/20/incorrect-icon.svg new file mode 100644 index 00000000..35d5e858 --- /dev/null +++ b/packages/icons/src/20/incorrect-icon.svg @@ -0,0 +1,15 @@ + + + + diff --git a/yarn.lock b/yarn.lock index b48e9863..e35a37c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3561,6 +3561,25 @@ "@radix-ui/react-roving-focus" "1.0.3" "@radix-ui/react-use-controllable-state" "1.0.0" +"@radix-ui/react-toast@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-toast/-/react-toast-1.1.3.tgz#41098f05bace7976cd4c07f6ff418261f86ede6e" + integrity sha512-yHFgpxi9wjbfPvpSPdYAzivCqw48eA1ofT8m/WqYOVTxKPdmQMuVKRYPlMmj4C1d6tJdFj/LBa1J4iY3fL4OwQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.0" + "@radix-ui/react-collection" "1.0.2" + "@radix-ui/react-compose-refs" "1.0.0" + "@radix-ui/react-context" "1.0.0" + "@radix-ui/react-dismissable-layer" "1.0.3" + "@radix-ui/react-portal" "1.0.2" + "@radix-ui/react-presence" "1.0.0" + "@radix-ui/react-primitive" "1.0.2" + "@radix-ui/react-use-callback-ref" "1.0.0" + "@radix-ui/react-use-controllable-state" "1.0.0" + "@radix-ui/react-use-layout-effect" "1.0.0" + "@radix-ui/react-visually-hidden" "1.0.2" + "@radix-ui/react-tooltip@^1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.0.5.tgz#fe20274aeac874db643717fc7761d5a8abdd62d1" @@ -6364,7 +6383,7 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== -"@types/react-dom@^18.0.11": +"@types/react-dom@18.0.11", "@types/react-dom@^18.0.11": version "18.0.11" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.11.tgz#321351c1459bc9ca3d216aefc8a167beec334e33" integrity sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw== @@ -6378,7 +6397,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@>=16", "@types/react@^18.0.28": +"@types/react@*", "@types/react@18.0.28", "@types/react@>=16", "@types/react@^18.0.28": version "18.0.28" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.28.tgz#accaeb8b86f4908057ad629a26635fe641480065" integrity sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==