Add more components, message actions & reactions (#339)

* hide sidebar on small screen

* add reply component

* add radix dependencies

* add dropdown menu component

* add popover component

* add tooltip component

* add react button component

* add reaction popover

* update chat message actions

* add basic dialog and sheet components

* add ref to Button component

* add chat message reactions

* add reply and reactions to chat message

* remove console.log

* add dropdown menu to topbar

* add ref and support aria in IconButton

* yarn.lock

* add stylesheet reset to storybook

* add pinned state to message

* remove extraneous component

* add all button variants

* fix button icons

* use IconButton in actions

* remove erroneous character

* add reply to composer + simplify

* use correct button in reply
This commit is contained in:
Pavel 2023-02-14 17:36:38 +01:00 committed by GitHub
parent a5fdd22d51
commit 84ec492292
No known key found for this signature in database
GPG Key ID: 0EB8D75C775AB6F1
46 changed files with 2134 additions and 303 deletions

View File

@ -88,12 +88,9 @@ function App() {
<Messages />
</div>
<Composer
backgroundColor="$blurBackground"
paddingBottom={56}
isBlurred={shouldBlurBottom}
style={{ marginTop: -112 }}
/>
<div id="composer">
<Composer isBlurred={shouldBlurBottom} reply={false} />
</div>
</main>
<AnimatePresence enterVariant="fromRight" exitVariant="fromLeft">
{showMembers && (

View File

@ -17,6 +17,7 @@ body,
}
#main {
position: relative;
display: grid;
grid-template-rows: 56px 1fr 100px;
height: 100vh;
@ -45,7 +46,24 @@ body,
margin-top: -56px;
}
#composer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
}
#members {
width: 352px;
overflow: auto;
}
@media screen and (max-width: 768px) {
#app {
grid-template-columns: 1fr;
}
#sidebar {
display: none;
}
}

View File

@ -3,6 +3,8 @@ import { TamaguiProvider } from '@tamagui/core'
import { config } from '../src'
import { Parameters, Decorator } from '@storybook/react'
import './reset.css'
export const parameters: Parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {

View File

@ -0,0 +1,100 @@
/*
1. Use a more-intuitive box-sizing model.
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #fff;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
/*
2. Remove default margin
*/
* {
margin: 0;
}
/*
3. Allow percentage-based heights in the application
*/
html,
body {
height: 100vh;
width: 100vw;
overflow: hidden;
overscroll-behavior-y: none; /* not working on Safari */
}
/*
Typographic tweaks!
4. Add accessible line-height
5. Improve text rendering
*/
body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
padding: 0;
-webkit-overflow-scrolling: touch;
}
/*
6. Improve media defaults
*/
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
}
/*
7. Remove built-in form typography styles
*/
input,
button,
textarea,
select {
font: inherit;
}
/*
8. Avoid text overflows
*/
p,
h1,
h2,
h3,
h4,
h5,
h6 {
overflow-wrap: break-word;
}
/*
9. Create a root stacking context
*/
#root,
#__next {
isolation: isolate;
}
/*
10. Remove user selection on buttons
*/
button {
user-select: none;
}

View File

@ -22,6 +22,9 @@
"react-native-web": "^0.18.0"
},
"dependencies": {
"@radix-ui/react-dropdown-menu": "^2.0.2",
"@radix-ui/react-popover": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.3",
"@status-im/icons": "*",
"@tamagui/animations-css": "1.0.15",
"@tamagui/animations-react-native": "1.0.15",

View File

@ -1,3 +1,6 @@
import { action } from '@storybook/addon-actions'
import { Stack } from 'tamagui'
import { Button } from './button'
import type { Meta, StoryObj } from '@storybook/react'
@ -5,18 +8,25 @@ import type { Meta, StoryObj } from '@storybook/react'
// More on how to set up stories at: https://storybook.js.org/docs/7.0/react/writing-stories/introduction
const meta: Meta<typeof Button> = {
component: Button,
argTypes: {},
args: {
onPress: action('press'),
},
argTypes: {
disabled: {
defaultValue: false,
},
},
decorators: [
Story => (
<Stack alignItems="flex-start">
<Story />
</Stack>
),
],
}
type Story = StoryObj<typeof Button>
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
export const Primary: Story = {
args: {
children: 'Click me',
},
}
const icon = (
<svg
width="20"
@ -34,14 +44,43 @@ const icon = (
</svg>
)
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
export const Primary: Story = {
args: {
children: 'Click me',
},
}
export const PrimaryDisabled: Story = {
args: {
children: 'Click me',
disabled: true,
},
}
export const PrimaryFullWidth: Story = {
args: {
children: 'Click me',
width: 'full',
},
}
export const Primary32: Story = {
name: 'Primary/32px',
name: 'Primary / 32',
args: {
size: 32,
children: 'Click me',
},
}
export const Primary24: Story = {
name: 'Primary / 24',
args: {
size: 24,
children: 'Click me',
},
}
export const PrimaryIconBefore: Story = {
name: 'Primary icon before',
args: {
@ -85,4 +124,11 @@ export const Ghost: Story = {
},
}
export const Danger: Story = {
args: {
type: 'danger',
children: 'Click me',
},
}
export default meta

View File

@ -1,24 +1,29 @@
import { forwardRef } from 'react'
import { Stack, styled } from '@tamagui/core'
import { Paragraph } from '../typography'
import type { GetProps } from '@tamagui/core'
// import { Pressable } from 'react-native'
import type { Ref } from 'react'
const Base = styled(Stack, {
name: 'Button',
accessibilityRole: 'button',
borderRadius: 12,
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 16,
paddingTop: 7,
paddingBottom: 9,
cursor: 'pointer',
alignItems: 'center',
animation: 'fast',
cursor: 'pointer',
userSelect: 'none',
animation: 'fast',
borderWidth: 1,
borderColor: 'transparent',
variants: {
type: {
@ -28,9 +33,19 @@ const Base = styled(Stack, {
pressStyle: { backgroundColor: '$primaryHover' },
},
positive: {
backgroundColor: '$success',
hoverStyle: { backgroundColor: '$successHover' },
pressStyle: { backgroundColor: '$successHover' },
backgroundColor: '$success-50',
hoverStyle: { backgroundColor: '$success-60' },
pressStyle: { backgroundColor: '$success-50' },
},
grey: {
backgroundColor: '$neutral-10',
hoverStyle: { backgroundColor: '$neutral-20' },
pressStyle: { backgroundColor: '$neutral-30' },
},
darkGrey: {
backgroundColor: '$neutral-20',
hoverStyle: { backgroundColor: '$neutral-30' },
pressStyle: { backgroundColor: '$neutral-40' },
},
outline: {
borderWidth: 1,
@ -43,23 +58,47 @@ const Base = styled(Stack, {
hoverStyle: { backgroundColor: '$neutral-10' },
pressStyle: { backgroundColor: '$neutral-20' },
},
danger: {
backgroundColor: '$danger',
hoverStyle: { backgroundColor: '$danger-60' },
pressStyle: { backgroundColor: '$danger' },
},
},
disabled: {
true: {
opacity: 0.3,
cursor: 'default',
},
},
size: {
40: {
minHeight: 40,
borderRadius: 12,
paddingHorizontal: 16,
paddingTop: 7,
paddingBottom: 9,
},
32: {
minHeight: 32,
borderRadius: 10,
paddingHorizontal: 16,
paddingTop: 4,
paddingBottom: 6,
},
24: {
minHeight: 24,
borderRadius: 8,
paddingHorizontal: 8,
paddingTop: 2,
paddingBottom: 4,
},
},
iconOnly: {
true: {
space: 0,
paddingHorizontal: 8,
},
},
@ -67,11 +106,10 @@ const Base = styled(Stack, {
})
const ButtonText = styled(Paragraph, {
textAlign: 'center',
weight: 'medium',
display: 'flex',
alignItems: 'center',
space: 4,
weight: 'medium',
variants: {
type: {
@ -81,12 +119,33 @@ const ButtonText = styled(Paragraph, {
positive: {
color: '$white-100',
},
grey: {
color: '$neutral-100',
},
darkGrey: {
color: '$neutral-100',
},
outline: {
color: '$neutral-100',
},
ghost: {
color: '$neutral-100',
},
danger: {
color: '$white-100',
},
},
size: {
40: {
variant: 'normal',
},
32: {
variant: 'normal',
},
24: {
variant: 'smaller',
},
},
} as const,
})
@ -95,19 +154,18 @@ type BaseProps = GetProps<typeof Base>
type Props = BaseProps & {
children?: string
icon?: React.ReactNode
type?: BaseProps['type']
size?: BaseProps['size']
disabled?: boolean
icon?: React.ReactNode
iconAfter?: React.ReactNode
onPress?: () => void
}
const Button = (props: Props) => {
const Button = (props: Props, ref: Ref<HTMLButtonElement>) => {
const {
type = 'primary',
size = 40,
children,
onPress,
icon,
iconAfter,
...rest
@ -116,14 +174,8 @@ const Button = (props: Props) => {
const iconOnly = !children && Boolean(icon)
return (
<Base
{...rest}
type={type}
size={size}
iconOnly={iconOnly}
onPress={onPress}
>
<ButtonText type={type}>
<Base {...rest} ref={ref} type={type} size={size} iconOnly={iconOnly}>
<ButtonText type={type} size={size}>
{icon}
{children}
{iconAfter}
@ -132,5 +184,7 @@ const Button = (props: Props) => {
)
}
export { Button }
const _Button = forwardRef(Button)
export { _Button as Button }
export type { Props as ButtonProps }

View File

@ -1 +1 @@
export { Button } from './button'
export { type ButtonProps, Button } from './button'

View File

@ -11,30 +11,19 @@ import { Stack, XStack, YStack } from 'tamagui'
import { IconButton } from '../icon-button'
import { Input } from '../input'
import { Reply } from '../reply'
import type { GetProps } from '@tamagui/core'
import type { ViewProps } from 'react-native'
type BaseProps = GetProps<typeof YStack>
const Composer = (
props: Omit<BaseProps, 'style'> & {
placeholderTextColor?: BaseProps['backgroundColor']
iconOptionsColor?: BaseProps['backgroundColor']
isBlurred?: boolean
style?: ViewProps['style']
interface Props {
isBlurred: boolean
reply: boolean
}
) => {
const { backgroundColor, isBlurred, style: styleFromProps, ...rest } = props
const style = styleFromProps ? Object.assign(styleFromProps) : {}
const Composer = (props: Props) => {
const { isBlurred, reply } = props
const [isFocused, setIsFocused] = useState(false)
const applyVariantStyles:
| {
blurred: boolean
}
| undefined = isBlurred && !isFocused ? { blurred: true } : undefined
const iconButtonBlurred = isBlurred && !isFocused
return (
<BlurView
@ -42,25 +31,35 @@ const Composer = (
style={{
zIndex: 100,
borderRadius: 20,
...style,
}}
>
<YStack
animation="fast"
backgroundColor={isFocused ? '$background' : backgroundColor}
backgroundColor={isFocused ? '$background' : '$blurBackground'}
shadowColor={!isBlurred || isFocused ? 'rgba(9, 16, 28, 0.08)' : 'none'}
shadowOffset={{ width: 4, height: !isBlurred || isFocused ? 4 : 0 }}
shadowRadius={20}
borderTopLeftRadius={20}
borderTopRightRadius={20}
px={16}
pt={8}
width="100%"
py={12}
style={{
elevation: 10,
}}
{...rest}
>
{reply && (
<Stack paddingLeft={4} paddingBottom={4}>
<Reply
type="text"
name="Alisher"
src="https://images.unsplash.com/photo-1524638431109-93d95c968f03?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=500&ixid=MnwxfDB8MXxyYW5kb218MHx8Z2lybHx8fHx8fDE2NzM4ODQ0NzU&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=500"
onClose={() => {
console.log('close')
}}
/>
</Stack>
)}
<Input
className="composer-input"
placeholder="Type something..."
@ -70,35 +69,35 @@ const Composer = (
onBlur={() => setIsFocused(false)}
onFocus={() => setIsFocused(true)}
/>
<XStack
alignItems="center"
justifyContent="space-between"
pt={8}
paddingTop={12}
backgroundColor="transparent"
>
<Stack space={12} flexDirection="row" backgroundColor="transparent">
<IconButton
variant="outline"
icon={<ImageIcon />}
{...applyVariantStyles}
selected
blurred={iconButtonBlurred}
/>
<IconButton
variant="outline"
icon={<ReactionIcon />}
{...applyVariantStyles}
blurred={iconButtonBlurred}
/>
<IconButton
variant="outline"
icon={<FormatIcon />}
disabled
{...applyVariantStyles}
blurred={iconButtonBlurred}
/>
</Stack>
<IconButton
variant="outline"
icon={<AudioIcon />}
{...applyVariantStyles}
blurred={iconButtonBlurred}
/>
</XStack>
</YStack>

View File

@ -0,0 +1,32 @@
import { Button } from '../button'
import { Dialog } from './dialog'
import type { Meta, StoryObj } from '@storybook/react'
// More on how to set up stories at: https://storybook.js.org/docs/7.0/react/writing-stories/introduction
const meta: Meta<typeof Dialog> = {
title: 'Web/Dialog',
// component: Sheet,
argTypes: {},
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/IBmFKgGL1B4GzqD8LQTw6n/Design-System-for-Web?node-id=611%3A36006&t=Gyy71OAckl3b2TWj-4',
},
},
}
type Story = StoryObj<typeof Dialog>
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
export const Default: Story = {
args: {},
render: () => (
<Dialog>
<Button>Trigger</Button>
<Dialog.Content>test</Dialog.Content>
</Dialog>
),
}
export default meta

View File

@ -0,0 +1,116 @@
import { forwardRef } from 'react'
import { Content, Overlay, Portal, Root, Trigger } from '@radix-ui/react-dialog'
import { useMedia } from 'tamagui'
import { Sheet } from '../sheet'
import type { Ref } from 'react'
import type React from 'react'
interface Props {
children: [React.ReactElement, React.ReactElement]
open?: boolean
onOpenChange?: (open: boolean) => void
press?: 'normal' | 'long'
}
// const DialogTrigger = (
// props: DialogTriggerProps & {
// press: Props['press']
// children: React.ReactElement
// }
// ) => {
// const { children, press, onClick, type, ...triggerProps } = props
// const handler = press === 'normal' ? 'onPress' : 'onLongPress'
// // console.log('dialog', press, onClick, { ...triggerProps, [handler]: onClick })
// return cloneElement(children, { ref, ...triggerProps, [handler]: onClick })
// }
// TODO: allow customization of press duration
const Dialog = (props: Props) => {
const { children, open, onOpenChange /* press = 'normal' */ } = props
const [trigger, content] = children
const media = useMedia()
if (media.sm) {
return (
<Sheet>
{trigger}
{content}
</Sheet>
)
}
return (
<Root open={open} onOpenChange={onOpenChange}>
{/* TRIGGER */}
<Trigger asChild>{trigger}</Trigger>
{/* CONTENT */}
<Portal>
<Overlay
style={{
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
}}
/>
{content}
</Portal>
</Root>
)
}
interface DialogContentProps {
// title: string
// description?: string
children: React.ReactNode
// action: string
// onAction: (close: VoidFunction) => void
// onCancel?: () => void
initialFocusRef?: React.RefObject<HTMLElement>
}
const DialogContent = (props: DialogContentProps, ref: Ref<HTMLDivElement>) => {
const { children, initialFocusRef } = props
const handleOpenAutoFocus = (event: Event) => {
if (initialFocusRef?.current) {
event.preventDefault()
initialFocusRef.current.focus()
}
}
const media = useMedia()
if (media.sm) {
return <Sheet.Content>{children}</Sheet.Content>
}
return (
<Content
ref={ref}
onOpenAutoFocus={handleOpenAutoFocus}
style={{
backgroundColor: 'white',
padding: 8,
width: 400,
borderRadius: 8,
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
}}
>
{children}
</Content>
)
}
Dialog.Content = forwardRef(DialogContent)
export { Dialog }

View File

@ -0,0 +1 @@
export { Dialog } from './dialog'

View File

@ -0,0 +1,85 @@
import {
CopyIcon,
DeleteIcon,
EditIcon,
ForwardIcon,
LinkIcon,
PinIcon,
ReplyIcon,
} from '@status-im/icons/20'
import { action } from '@storybook/addon-actions'
import { Button } from '../button'
import { DropdownMenu } from './dropdown-menu'
import type { Meta, StoryObj } from '@storybook/react'
const meta: Meta<typeof DropdownMenu> = {
title: 'Web/dropdown menu',
component: DropdownMenu,
argTypes: {},
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/IBmFKgGL1B4GzqD8LQTw6n/Design-System-for-Web?node-id=1931%3A31188&t=rOKELbVkzya48FJE-0',
},
},
}
type Story = StoryObj<typeof DropdownMenu>
export const Default: Story = {
args: {},
render: args => {
return (
<DropdownMenu {...args}>
<Button>Open</Button>
<DropdownMenu.Content sideOffset={10}>
<DropdownMenu.Item
icon={<EditIcon />}
label="Edit message"
onSelect={action('edit')}
/>
<DropdownMenu.Item
icon={<ReplyIcon />}
label="Reply"
onSelect={action('reply')}
/>
<DropdownMenu.Item
icon={<CopyIcon />}
label="Copy text"
onSelect={action('copy')}
/>
<DropdownMenu.Item
icon={<PinIcon />}
label="Pin to the channel"
onSelect={action('pin')}
/>
<DropdownMenu.Item
icon={<ForwardIcon />}
label="Forward"
onSelect={action('forward')}
/>
<DropdownMenu.Item
icon={<LinkIcon />}
label="Share link to message"
onSelect={action('share')}
/>
<DropdownMenu.Separator />
<DropdownMenu.Item
icon={<DeleteIcon />}
label="Delete message"
danger
onSelect={action('delete')}
/>
</DropdownMenu.Content>
</DropdownMenu>
)
},
}
export default meta

View File

@ -0,0 +1,108 @@
import { cloneElement } from 'react'
import {
DropdownMenuContent,
DropdownMenuItem as RadixDropdownMenuItem,
DropdownMenuSeparator,
// Label,
Portal,
Root,
Trigger,
} from '@radix-ui/react-dropdown-menu'
import { Stack, styled } from 'tamagui'
import { Paragraph } from '../typography'
const Content = styled(DropdownMenuContent, {
name: 'DropdownMenuContent',
acceptsClassName: true,
width: 352,
padding: 8,
borderRadius: 16,
backgroundColor: '$white-100',
shadowRadius: 30,
shadowOffset: '0px 8px',
shadowColor: 'rgba(9, 16, 28, 0.12)',
})
const Item = styled(RadixDropdownMenuItem, {
name: 'DropdownMenuItem',
acceptsClassName: true,
display: 'flex',
alignItems: 'center',
padding: 8,
borderRadius: 12,
cursor: 'pointer',
userSelect: 'none',
hoverStyle: {
backgroundColor: '$neutral-5',
},
pressStyle: {
backgroundColor: '$neutral-10',
},
})
const Separator = styled(DropdownMenuSeparator, {
name: 'DropdownMenuSeparator',
acceptsClassName: true,
height: 1,
backgroundColor: '$neutral-10',
marginVertical: 8,
marginLeft: -8,
marginRight: -8,
})
interface Props {
children: [React.ReactElement, React.ReactElement]
modal?: false
onOpenChange?: (open: boolean) => void
}
const DropdownMenu = (props: Props) => {
const { children, onOpenChange, modal } = props
const [trigger, content] = children
return (
<Root onOpenChange={onOpenChange} modal={modal}>
<Trigger asChild>{trigger}</Trigger>
<Portal>{content}</Portal>
</Root>
)
}
interface DropdownMenuItemProps {
icon: React.ReactElement
label: string
onSelect: () => void
danger?: boolean
}
const DropdownMenuItem = (props: DropdownMenuItemProps) => {
const { icon, label, onSelect, danger } = props
const iconColor = danger ? '$danger-50' : '$neutral-50'
const textColor = danger ? '$danger-50' : '$neutral-100'
return (
<Item onSelect={onSelect}>
<Stack marginRight={12}>{cloneElement(icon, { color: iconColor })}</Stack>
<Paragraph weight="medium" color={textColor}>
{label}
</Paragraph>
</Item>
)
}
DropdownMenu.Content = Content
DropdownMenu.Item = DropdownMenuItem
DropdownMenu.Separator = Separator
export { DropdownMenu }
export type { Props as DropdownMenuProps }

View File

@ -0,0 +1 @@
export { DropdownMenu } from './dropdown-menu'

View File

@ -1,8 +1,8 @@
import { cloneElement } from 'react'
import { cloneElement, forwardRef } from 'react'
import { Stack, styled } from '@tamagui/core'
import type React from 'react'
import type { Ref } from 'react'
const Base = styled(Stack, {
name: 'IconButton',
@ -108,6 +108,9 @@ interface Props {
disabled?: boolean
// FIXME: enforce aria-label for accessibility
// 'aria-label'?: string
// FIXME: update to latest RN
'aria-expanded'?: boolean
'aria-selected'?: boolean
}
const iconColor = {
@ -165,26 +168,22 @@ const getSelectedVariant = ({
return variant
}
const IconButton = (props: Props) => {
const {
icon,
selected,
blurred,
onPress,
variant = 'default',
disabled,
} = props
const IconButton = (props: Props, ref: Ref<HTMLButtonElement>) => {
const { icon, blurred, variant = 'default', ...rest } = props
const selected =
props.selected || props['aria-expanded'] || props['aria-selected']
const state = getStateForIconColor({ blurred, selected })
const selectedVariant = getSelectedVariant({ selected, variant, blurred })
return (
<Base
{...rest}
ref={ref}
variant={variant}
selected={selectedVariant}
onPress={onPress}
blurred={blurred ? variant : undefined}
disabled={disabled}
>
{cloneElement(icon, {
color: iconColor[variant][state],
@ -194,5 +193,7 @@ const IconButton = (props: Props) => {
)
}
export { IconButton }
const _IconButton = forwardRef(IconButton)
export { _IconButton as IconButton }
export type { Props as IconButtonProps }

View File

@ -24,7 +24,7 @@ export const InputFrame = styled(
backgroundColor: 'transparent',
height: 40,
height: 32,
borderRadius: 12,
animation: 'fast',

View File

@ -1,89 +0,0 @@
import React from 'react'
import { Stack, Unspaced, XStack, YStack } from 'tamagui'
import { Author } from '../author/author'
import { Avatar } from '../avatar'
import { Image } from '../image'
import { Paragraph } from '../typography'
import { Actions } from './components/actions'
import { Reactions } from './components/reactions'
interface Props {
text?: React.ReactNode
images?: Array<{ url: string }>
reactions?: []
}
const ChatMessage = (props: Props) => {
const { text, images, reactions } = props
const [hovered, setHovered] = React.useState(false)
return (
<XStack
space={10}
position="relative"
alignItems="flex-start"
paddingHorizontal={8}
paddingVertical={12}
borderRadius={16}
hoverStyle={{
backgroundColor: '$neutral-5',
}}
onHoverIn={() => setHovered(true)}
onHoverOut={() => setHovered(false)}
>
{hovered && (
<Unspaced>
<Actions
onClick={() => {
console.log('clicked')
}}
/>
</Unspaced>
)}
<Avatar
size={32}
src="https://images.unsplash.com/photo-1524638431109-93d95c968f03?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=500&ixid=MnwxfDB8MXxyYW5kb218MHx8Z2lybHx8fHx8fDE2NzM4ODQ0NzU&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=500"
indicator="online"
/>
<YStack flex={1}>
<Author
name="Alisher Yakupov"
address="zQ3...9d4Gs0"
status="verified"
time="09:30"
/>
{text && (
<Paragraph flexGrow={0} weight="regular" color="$neutral-100">
{text}
</Paragraph>
)}
{images?.map(image => (
<Stack
key={image.url}
marginTop={8}
$gtMd={{
maxWidth: 320,
}}
>
<Image src={image.url} width="full" height={320} radius={12} />
</Stack>
))}
{reactions && (
<Stack marginTop={8}>
<Reactions />
</Stack>
)}
</YStack>
</XStack>
)
}
export { ChatMessage }

View File

@ -1,11 +1,21 @@
import { Actions } from './actions'
import type { ReactionsType } from '../types'
import type { Meta, StoryObj } from '@storybook/react'
const reactions: ReactionsType = {
love: new Set(['me', '1', '2', '3']),
'thumbs-up': new Set(['me', '1', '2', '3']),
'thumbs-down': new Set(['me', '1', '2', '3']),
}
// More on how to set up stories at: https://storybook.js.org/docs/7.0/react/writing-stories/introduction
const meta: Meta<typeof Actions> = {
title: 'Messages/actions',
component: Actions,
args: {
reactions,
},
argTypes: {},
parameters: {
layout: 'centered',

View File

@ -1,18 +1,47 @@
import { AddReactionIcon, OptionsIcon, ReplyIcon } from '@status-im/icons/20'
import { useEffect } from 'react'
import {
AddReactionIcon,
CopyIcon,
DeleteIcon,
EditIcon,
ForwardIcon,
LinkIcon,
OptionsIcon,
PinIcon,
ReplyIcon,
} from '@status-im/icons/20'
import { Stack } from 'tamagui'
import { Button } from '../../button'
import { DropdownMenu } from '../../dropdown-menu'
import { IconButton } from '../../icon-button'
import { ReactionPopover } from './reaction-popover'
import type { ReactionsType } from '../types'
interface Props {
onClick: VoidFunction
reactions: ReactionsType
onOpenChange: (open: boolean) => void
onReplyPress: VoidFunction
// onEditPress: VoidFunction
// onDeletePress: VoidFunction
}
export const Actions = (_props: Props) => {
export const Actions = (props: Props) => {
const { reactions, onOpenChange, onReplyPress } = props
useEffect(() => {
return () => onOpenChange(false)
}, [onOpenChange])
return (
<Stack
backgroundColor="$white-100"
borderWidth={1}
borderColor="$neutral-10"
borderRadius={10}
borderRadius={12}
padding={2}
space={2}
overflow="hidden"
position="absolute"
top={-16}
@ -23,9 +52,82 @@ export const Actions = (_props: Props) => {
shadowColor="rgba(9, 16, 28, 0.08)"
zIndex={10}
>
<Button type="ghost" icon={<AddReactionIcon />} borderRadius={0} />
<Button type="ghost" icon={<ReplyIcon />} borderRadius={0} />
<Button type="ghost" icon={<OptionsIcon />} borderRadius={0} />
{/* REACTION */}
<ReactionPopover
reactions={reactions}
side="left"
sideOffset={6}
onOpenChange={onOpenChange}
>
<IconButton variant="outline" icon={<AddReactionIcon />} />
</ReactionPopover>
{/* REPLY */}
<IconButton
variant="outline"
icon={<ReplyIcon />}
onPress={onReplyPress}
/>
{/* EDIT */}
{/* <IconButton
variant="outline"
icon={<EditIcon />}
onPress={onEditPress}
/> */}
{/* DELETE */}
{/* <IconButton
variant="outline"
icon={<DeleteIcon />}
onPress={onDeletePress}
/> */}
{/* OPTIONS MENU */}
<DropdownMenu modal={false} onOpenChange={onOpenChange}>
<IconButton variant="outline" icon={<OptionsIcon />} />
<DropdownMenu.Content align="end" sideOffset={10}>
<DropdownMenu.Item
icon={<EditIcon />}
label="Edit message"
onSelect={() => console.log('edit')}
/>
<DropdownMenu.Item
icon={<ReplyIcon />}
label="Reply"
onSelect={() => console.log('reply')}
/>
<DropdownMenu.Item
icon={<CopyIcon />}
label="Copy text"
onSelect={() => console.log('copy')}
/>
<DropdownMenu.Item
icon={<PinIcon />}
label="Pin to the channel"
onSelect={() => console.log('pin')}
/>
<DropdownMenu.Item
icon={<ForwardIcon />}
label="Forward"
onSelect={() => console.log('forward')}
/>
<DropdownMenu.Item
icon={<LinkIcon />}
label="Share link to message"
onSelect={() => console.log('share')}
/>
<DropdownMenu.Separator />
<DropdownMenu.Item
icon={<DeleteIcon />}
label="Delete message"
danger
onSelect={() => console.log('delete')}
/>
</DropdownMenu.Content>
</DropdownMenu>
</Stack>
)
}

View File

@ -0,0 +1,64 @@
import { XStack } from 'tamagui'
import { Popover } from '../../popover'
import { ReactButton } from '../../react-button'
import type { PopoverProps } from '../../popover'
import type { ReactionsType } from '../types'
type Props = Omit<PopoverProps, 'children'> & {
children: React.ReactElement
reactions: ReactionsType
onOpenChange?: PopoverProps['onOpenChange']
}
export const ReactionPopover = (props: Props) => {
const { children, reactions, onOpenChange, ...popoverProps } = props
return (
<Popover {...popoverProps} onOpenChange={onOpenChange} modal={false}>
{children}
<Popover.Content>
<XStack space={2} padding={2}>
<ReactButton
variant="ghost"
size={32}
icon="love"
selected={reactions['love']?.has('me')}
/>
<ReactButton
variant="ghost"
size={32}
icon="thumbs-up"
selected={reactions['thumbs-up']?.has('me')}
/>
<ReactButton
variant="ghost"
size={32}
icon="thumbs-down"
selected={reactions['thumbs-down']?.has('me')}
/>
<ReactButton
variant="ghost"
size={32}
icon="laugh"
selected={reactions.laugh?.has('me')}
/>
<ReactButton
variant="ghost"
size={32}
icon="sad"
selected={reactions.sad?.has('me')}
/>
<ReactButton
variant="ghost"
size={32}
icon="angry"
selected={reactions.angry?.has('me')}
/>
</XStack>
</Popover.Content>
</Popover>
)
}

View File

@ -1,11 +1,21 @@
import { Reactions } from './reactions'
import type { ReactionsType } from '../types'
import type { Meta, StoryObj } from '@storybook/react'
const reactions: ReactionsType = {
love: new Set(['me', '1', '2', '3']),
'thumbs-up': new Set(['me', '1', '2', '3']),
'thumbs-down': new Set(['me', '1', '2', '3']),
}
// More on how to set up stories at: https://storybook.js.org/docs/7.0/react/writing-stories/introduction
const meta: Meta<typeof Reactions> = {
title: 'messages/reactions',
component: Reactions,
args: {
reactions,
},
argTypes: {},
parameters: {
design: {

View File

@ -1,94 +1,109 @@
import { cloneElement } from 'react'
import { AddReactionIcon } from '@status-im/icons/20'
import {
// AngryIcon,
LaughIcon,
LoveIcon,
SadIcon,
ThumbsDownIcon,
ThumbsUpIcon,
} from '@status-im/icons/reactions'
import { Stack, styled } from '@tamagui/core'
import { XStack } from 'tamagui'
import { Paragraph } from '../../typography'
import { Dialog } from '../../dialog'
import { ReactButton } from '../../react-button'
import { Tooltip } from '../../tooltip/tooltip'
import { UserList } from '../../user-list'
import { ReactionPopover } from './reaction-popover'
import type React from 'react'
// import { Pressable } from 'react-native'
const ReactButton = styled(Stack, {
name: 'ReactButton',
accessibilityRole: 'button',
cursor: 'pointer',
userSelect: 'none',
borderRadius: 8,
display: 'flex',
flexDirection: 'row',
space: 4,
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
minWidth: 36,
height: 24,
paddingHorizontal: 8,
borderWidth: 1,
borderColor: '$neutral-20',
hoverStyle: {
borderColor: '$neutral-30',
},
variants: {
selected: {
true: {
backgroundColor: '$neutral-30',
borderColor: '$neutral-30',
},
},
} as const,
})
type ReactionButtonProps = {
icon: React.ReactElement
count?: number
selected?: boolean
}
const ReactionButton = (props: ReactionButtonProps) => {
const { count, selected, icon } = props
return (
<ReactButton selected={selected}>
{cloneElement(icon, { color: '$neutral-100' })}
{count && (
<Paragraph weight="medium" variant="smaller" whiteSpace="nowrap">
{count}
</Paragraph>
)}
</ReactButton>
)
}
import type { ReactButtonProps } from '../../react-button'
import type { ReactionsType, ReactionType } from '../types'
type Props = {
reactions?: any[]
reactions: ReactionsType
}
export const Reactions = (props: Props) => {
const { reactions } = props
const hasReaction = Object.values(reactions).some(value => value.size > 0)
if (hasReaction === false) {
return null
}
return (
<XStack space={8} flexWrap="wrap">
<ReactionButton count={1} icon={<LoveIcon />} selected />
<ReactionButton count={10} icon={<ThumbsUpIcon />} />
<ReactionButton count={99} icon={<ThumbsDownIcon />} />
<ReactionButton count={100} icon={<LaughIcon />} />
<ReactionButton count={100} icon={<SadIcon />} />
{/* FIX TAMAGUI BUG */}
{/* <ReactionButton count={100} icon={<AngryIcon />} /> */}
<ReactionButton icon={<AddReactionIcon />} />
<XStack space={6} flexWrap="wrap">
{Object.entries(reactions).map(([type, value]) => (
<Reaction
key={type}
size="compact"
icon={type as ReactionType}
count={value.size}
selected={value.has('me')}
/>
))}
<ReactionPopover
reactions={reactions}
side="bottom"
align="start"
sideOffset={8}
>
<ReactButton size="compact" icon="add" selected={false} />
</ReactionPopover>
</XStack>
)
}
const Reaction = (props: ReactButtonProps) => {
return (
<Dialog press="long">
<Tooltip
side="bottom"
sideOffset={4}
content={
<>
You, Mr Gandalf, Ariana Perlona and
<button>3 more</button> reacted with {'[ICON]'}
</>
}
>
<ReactButton {...props} />
</Tooltip>
<Dialog.Content>
<UserList
users={[
{
name: 'Pedro',
src: 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1760&q=80',
address: 'zQ3...9d4Gs0',
indicator: 'online',
},
{
name: 'Pedro',
src: 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1760&q=80',
address: 'zQ3...9d4Gs0',
indicator: 'online',
},
{
name: 'Pedro',
src: 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1760&q=80',
address: 'zQ3...9d4Gs0',
indicator: 'online',
},
{
name: 'Pedro',
src: 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1760&q=80',
address: 'zQ3...9d4Gs0',
indicator: 'online',
},
{
name: 'Pedro',
src: 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1760&q=80',
address: 'zQ3...9d4Gs0',
indicator: 'online',
},
{
name: 'Pedro',
src: 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1760&q=80',
address: 'zQ3...9d4Gs0',
indicator: 'online',
},
]}
/>
</Dialog.Content>
</Dialog>
)
}

View File

@ -1,50 +1,82 @@
import { ChatMessage } from './chat-message'
import { Message } from './message'
export * from './chat-message'
import type { ReactionsType } from './types'
export * from './message'
const reactions: ReactionsType = {
love: new Set(['me', '1']),
'thumbs-up': new Set(['3']),
'thumbs-down': new Set(['me', '1', '2', '3']),
}
export const Messages = () => {
return (
<>
<ChatMessage text="fsdjkf kasldjf ksdlfjksdlfj asdklfj sdkljf" />
<ChatMessage text="fsdjkf kasldjf ksdlfjksdlfj asdklfj sdkljf" />
<ChatMessage text="fsdjkf kasldjf ksdlfjksdlfj asdklfj sdkljf" />
<ChatMessage
<Message
text="Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam a, posuere eu, velit. Nullam sapien sem, ornare ac, nonummy non, lobortis a, enim. Nunc tincidunt ante vitae massa. Duis ante orci, molestie vitae, vehicula venenatis, tincidunt ac, pede. Nulla accumsan, elit sit"
reactions={{}}
/>
<Message
text="Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam a, posuere eu, velit. "
reactions={{}}
reply
pinned
/>
<Message
text="Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam a, posuere eu, velit. "
reactions={{}}
/>
<Message
images={[
{
url: 'https://images.unsplash.com/photo-1673433107234-14d1a4424658?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1470&q=80',
},
]}
reactions={{}}
/>
<ChatMessage text="fsdjkf kasldjf ksdlfjksdlfj asdklfj sdkljf" />
<ChatMessage text="fsdjkf kasldjf ksdlfjksdlfj asdklfj sdkljf" />
<ChatMessage
text="fsdjkf kasldjf ksdlfjksdlfj asdklfj sdkljf"
reactions={['123']}
<Message
text="Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam a, posuere eu, velit. Nullam sapien sem, ornare ac, nonummy non, lobortis a, enim. Nunc tincidunt ante vitae massa. Duis ante orci, molestie vitae, vehicula venenatis, tincidunt ac, pede. Nulla accumsan, elit sit"
reactions={reactions}
/>
<ChatMessage text="fsdjkf kasldjf ksdlfjksdlfj asdklfj sdkljf" />
<ChatMessage text="fsdjkf kasldjf ksdlfjksdlfj asdklfj sdkljf" />
<ChatMessage text="fsdjkf kasldjf ksdlfjksdlfj asdklfj sdkljf" />
<ChatMessage
text="fsdjkf kasldjf ksdlfjksdlfj asdklfj sdkljf"
reactions={['123']}
<Message
text="Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam. "
reactions={{}}
pinned
/>
<ChatMessage
text="fsdjkf kasldjf ksdlfjksdlfj asdklfj sdkljf"
reactions={['123']}
<Message
text="Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam. "
reactions={{}}
reply
/>
<ChatMessage
<Message
text="Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam. "
reactions={{}}
/>
<Message
text="Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam a, posuere eu, velit. Nullam sapien sem, ornare ac, nonummy non, lobortis a, enim.sit"
reactions={reactions}
reply
/>
<Message
text="Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam a, posuere eu, velit. Nullam sapien sem, ornare ac, nonummy non, lobortis a, enim.sit"
reactions={reactions}
/>
<Message
images={[
{
url: 'https://images.unsplash.com/photo-1673433107234-14d1a4424658?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1470&q=80',
},
]}
reactions={{}}
/>
<ChatMessage
<Message
images={[
{
url: 'https://images.unsplash.com/photo-1673433107234-14d1a4424658?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1470&q=80',
},
]}
reactions={{}}
/>
</>
)

View File

@ -1,12 +1,25 @@
import { ChatMessage } from './chat-message'
import { Message } from './message'
import type { ReactionsType } from './types'
import type { Meta, StoryObj } from '@storybook/react'
// More on how to set up stories at: https://storybook.js.org/docs/7.0/react/writing-stories/introduction
const meta: Meta<typeof ChatMessage> = {
// title: 'Messages',
component: ChatMessage,
argTypes: {},
const meta: Meta<typeof Message> = {
title: 'messages',
component: Message,
args: {
reactions: {},
},
argTypes: {
pinned: {
type: 'boolean',
defaultValue: false,
},
reply: {
type: 'boolean',
defaultValue: false,
},
},
parameters: {
design: {
type: 'figma',
@ -15,9 +28,13 @@ const meta: Meta<typeof ChatMessage> = {
},
}
type Story = StoryObj<typeof ChatMessage>
type Story = StoryObj<typeof Message>
const reactions = ['123']
const reactions: ReactionsType = {
love: new Set(['me', '1', '2', '3']),
'thumbs-up': new Set(['me', '1', '2', '3']),
'thumbs-down': new Set(['me', '1', '2', '3']),
}
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
export const Text: Story = {
@ -34,6 +51,22 @@ export const TextWithReactions: Story = {
},
}
export const TextWithReply: Story = {
name: 'Text + Reply',
args: {
text: 'This is a simple message.',
reply: true,
},
}
export const TextPinned: Story = {
name: 'Text + Pinned',
args: {
text: 'This is a simple message.',
pinned: true,
},
}
export const LongText: Story = {
name: 'Long text',
args: {

View File

@ -0,0 +1,127 @@
import React from 'react'
import { PinIcon } from '@status-im/icons/16'
import { Stack, Unspaced, XStack, YStack } from 'tamagui'
import { Author } from '../author/author'
import { Avatar } from '../avatar'
import { Image } from '../image'
import { Reply } from '../reply'
import { Paragraph } from '../typography'
import { Actions } from './components/actions'
import { Reactions } from './components/reactions'
import type { ReactionsType } from './types'
interface Props {
text?: React.ReactNode
images?: Array<{ url: string }>
reactions: ReactionsType
reply?: boolean
pinned?: boolean
}
const Message = (props: Props) => {
const { text, images, reactions, reply, pinned } = props
const [hovered, setHovered] = React.useState(false)
const [actionsOpen, setActionsOpen] = React.useState(false)
const active = actionsOpen || hovered
// <Sheet press="long">
return (
<YStack
position="relative"
alignItems="flex-start"
paddingHorizontal={8}
paddingVertical={8}
borderRadius={16}
backgroundColor={
active ? '$neutral-5' : pinned ? '$blue-50-opa-5' : undefined
}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
{active && (
<Unspaced>
<Actions
reactions={reactions}
onOpenChange={setActionsOpen}
onReplyPress={() => {
console.log('reply')
}}
/>
</Unspaced>
)}
{reply && (
<Stack paddingLeft={16} paddingBottom={12}>
<Reply
type="text"
name="Alisher"
src="https://images.unsplash.com/photo-1524638431109-93d95c968f03?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=500&ixid=MnwxfDB8MXxyYW5kb218MHx8Z2lybHx8fHx8fDE2NzM4ODQ0NzU&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=500"
/>
</Stack>
)}
{pinned && (
<Stack
flexDirection="row"
alignItems="center"
paddingLeft={40}
paddingBottom={2}
space={2}
>
<PinIcon color="$blue-50" />
<Paragraph variant={11} weight="medium" color="$blue-50">
Steve
</Paragraph>
</Stack>
)}
<XStack space={10}>
<Avatar
size={32}
src="https://images.unsplash.com/photo-1524638431109-93d95c968f03?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=500&ixid=MnwxfDB8MXxyYW5kb218MHx8Z2lybHx8fHx8fDE2NzM4ODQ0NzU&ixlib=rb-4.0.3&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=500"
indicator="online"
/>
<YStack flex={1}>
<Author
name="Alisher Yakupov"
address="zQ3...9d4Gs0"
status="verified"
time="09:30"
/>
{text && (
<Paragraph flexGrow={0} weight="regular" color="$neutral-100">
{text}
</Paragraph>
)}
{images?.map(image => (
<Stack
key={image.url}
marginTop={8}
$gtMd={{
maxWidth: 320,
}}
>
<Image src={image.url} width="full" height={320} radius={12} />
</Stack>
))}
{reactions && (
<Stack paddingTop={8}>
<Reactions reactions={reactions} />
</Stack>
)}
</YStack>
</XStack>
</YStack>
)
}
export { Message }

View File

@ -0,0 +1,12 @@
export type ReactionType =
| 'love'
| 'laugh'
| 'thumbs-up'
| 'thumbs-down'
| 'sad'
| 'angry'
| 'add'
export type ReactionsType = {
[key in ReactionType]?: Set<string>
}

View File

@ -0,0 +1 @@
export { type PopoverProps, Popover } from './popover'

View File

@ -0,0 +1,32 @@
import { Button } from '../button'
import { Popover } from './popover'
import type { Meta, StoryObj } from '@storybook/react'
// More on how to set up stories at: https://storybook.js.org/docs/7.0/react/writing-stories/introduction
const meta: Meta<typeof Popover> = {
// title: 'Messages',
component: Popover,
argTypes: {},
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/IBmFKgGL1B4GzqD8LQTw6n/Design-System-for-Web?node-id=611%3A36006&t=Gyy71OAckl3b2TWj-4',
},
},
}
type Story = StoryObj<typeof Popover>
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
export const Default: Story = {
args: {},
render: args => (
<Popover {...args}>
<Button type="primary">Trigger</Button>
<Popover.Content>some content</Popover.Content>
</Popover>
),
}
export default meta

View File

@ -0,0 +1,51 @@
import { Content, Portal, Root, Trigger } from '@radix-ui/react-popover'
import { Stack } from 'tamagui'
import type { PopoverContentProps } from '@radix-ui/react-popover'
import type { FunctionComponent } from 'react'
interface Props {
children: [React.ReactElement, React.ReactElement]
onOpenChange?: (open: boolean) => void
modal?: false
side?: PopoverContentProps['side']
sideOffset?: PopoverContentProps['sideOffset']
align?: PopoverContentProps['align']
alignOffset?: PopoverContentProps['alignOffset']
}
const Popover = (props: Props) => {
const { children, onOpenChange, modal, ...contentProps } = props
const [trigger, content] = children
return (
<Root onOpenChange={onOpenChange} modal={modal}>
<Trigger asChild>{trigger}</Trigger>
<Portal>
<Content {...contentProps}>{content}</Content>
</Portal>
</Root>
)
}
const PopoverContent: FunctionComponent = props => {
const { children } = props
return (
<Stack
backgroundColor="$white-100"
borderRadius={12}
shadowRadius={30}
shadowOffset="0px 8px"
shadowColor="rgba(9, 16, 28, 0.12)"
>
{children}
</Stack>
)
}
Popover.Content = PopoverContent
export { Popover as Popover }
export type { Props as PopoverProps }

View File

@ -0,0 +1 @@
export { type ReactButtonProps, ReactButton } from './react-button'

View File

@ -0,0 +1,67 @@
import { XStack } from 'tamagui'
import { ReactButton } from './react-button'
import type { Meta, StoryObj } from '@storybook/react'
// More on how to set up stories at: https://storybook.js.org/docs/7.0/react/writing-stories/introduction
const meta: Meta<typeof ReactButton> = {
title: 'ReactButton',
component: ReactButton,
args: {},
argTypes: {},
render: args => (
<XStack space={4}>
<ReactButton {...args} icon="laugh" />
<ReactButton {...args} icon="love" />
<ReactButton {...args} icon="sad" />
<ReactButton {...args} icon="thumbs-up" />
<ReactButton {...args} icon="thumbs-down" />
<ReactButton {...args} icon="angry" />
</XStack>
),
}
type Story = StoryObj<typeof ReactButton>
export const Outline: Story = {
name: 'Outline / 40px',
args: { variant: 'outline' },
}
export const OutlineSelected: Story = {
name: 'Outline / 40px / selected',
args: { variant: 'outline', selected: true },
}
export const Outline32: Story = {
name: 'Outline / 32px',
args: { variant: 'outline', size: 32 },
}
export const Outline32Selected: Story = {
name: 'Outline / 32px',
args: { variant: 'outline', size: 32, selected: true },
}
export const Ghost: Story = {
name: 'Ghost / 40px',
args: { variant: 'ghost' },
}
export const GhostSelected: Story = {
name: 'Ghost / 40px / selected',
args: { variant: 'ghost', selected: true },
}
export const Ghost32: Story = {
name: 'Ghost / 32px',
args: { variant: 'ghost', size: 32 },
}
export const Ghost32Selected: Story = {
name: 'Ghost / 32px',
args: { variant: 'ghost', size: 32, selected: true },
}
export default meta

View File

@ -0,0 +1,141 @@
import { forwardRef } from 'react'
import { AddReactionIcon } from '@status-im/icons/20'
import {
AngryIcon,
LaughIcon,
LoveIcon,
SadIcon,
ThumbsDownIcon,
ThumbsUpIcon,
} from '@status-im/icons/reactions'
import { Stack, styled } from '@tamagui/core'
import { Paragraph } from '../typography'
import type { GetProps } from '@tamagui/core'
import type { Ref } from 'react'
import type { PressableProps } from 'react-native'
const Button = styled(Stack, {
name: 'ReactButton',
accessibilityRole: 'button',
cursor: 'pointer',
userSelect: 'none',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderWidth: 1,
animation: 'fast',
space: 4,
variants: {
variant: {
outline: {
borderColor: '$neutral-10',
hoverStyle: { borderColor: '$neutral-30' },
pressStyle: {
backgroundColor: '$neutral-10',
borderColor: '$neutral-20',
},
},
ghost: {
borderColor: 'transparent',
hoverStyle: { backgroundColor: '$neutral-10' },
pressStyle: { backgroundColor: '$neutral-20' },
},
},
selected: {
true: {
backgroundColor: '$neutral-10',
borderColor: '$neutral-30',
},
},
size: {
40: {
borderRadius: 12,
width: 40,
height: 40,
},
32: {
borderRadius: 10,
width: 32,
height: 32,
},
compact: {
borderRadius: 8,
minWidth: 36,
height: 24,
paddingHorizontal: 8,
},
},
} as const,
})
type ButtonProps = GetProps<typeof Button>
export const REACTIONS = {
love: LoveIcon,
laugh: LaughIcon,
'thumbs-up': ThumbsUpIcon,
'thumbs-down': ThumbsDownIcon,
sad: SadIcon,
angry: AngryIcon,
add: AddReactionIcon,
} as const
interface Props extends PressableProps {
icon: keyof typeof REACTIONS
variant?: ButtonProps['variant']
size?: ButtonProps['size']
// FIXME: use aria-selected
selected?: boolean
count?: number
// FIXME: update to latest RN
'aria-expanded'?: boolean
'aria-selected'?: boolean
}
const ReactButton = (props: Props, ref: Ref<HTMLButtonElement>) => {
const {
icon,
variant = 'outline',
size = 40,
count,
...pressableProps
} = props
const Icon = REACTIONS[icon]
const selected =
props.selected || props['aria-expanded'] || props['aria-selected']
return (
<Button
{...(pressableProps as any)}
ref={ref}
variant={variant}
size={size}
selected={selected}
>
<Icon color="$neutral-100" />
{count && (
<Paragraph weight="medium" variant="smaller" whiteSpace="nowrap">
{count}
</Paragraph>
)}
</Button>
)
}
const _ReactButton = forwardRef(ReactButton)
export { _ReactButton as ReactButton }
export type { Props as ReactButtonProps }

View File

@ -0,0 +1 @@
export { type ReplyProps, Reply } from './reply'

View File

@ -0,0 +1,84 @@
import { action } from '@storybook/addon-actions'
import { Reply } from './reply'
import type { Meta, StoryObj } from '@storybook/react'
const meta: Meta<typeof Reply> = {
component: Reply,
argTypes: {},
args: {
name: 'Alisher Yakupov',
src: 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?ixid=Mnw0MDAxMTJ8MHwxfHNlYXJjaHw0fHxhdmF0YXJ8ZW58MHx8fHwxNjc1MjU4NTkw&ixlib=rb-4.0.3',
},
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/IBmFKgGL1B4GzqD8LQTw6n/Design-System-for-Desktop%2FWeb?node-id=3173%3A55936&t=QgRAQPXVREVsrDg7-11',
},
},
}
type Story = StoryObj<typeof Reply>
export const Text: Story = {
args: {
type: 'text',
onClose: undefined,
},
}
export const TextClose: Story = {
name: 'Text + Close',
args: {
...Text.args,
onClose: action('close'),
},
}
export const Image: Story = {
args: {
type: 'image',
onClose: undefined,
},
}
export const ImageClose: Story = {
name: 'Image + Close',
args: {
...Image.args,
onClose: action('close'),
},
}
export const GIF: Story = {
args: {
type: 'gif',
onClose: undefined,
},
}
export const GIFClose: Story = {
name: 'GIF + Close',
args: {
...GIF.args,
onClose: action('close'),
},
}
export const Deleted: Story = {
args: {
type: 'deleted',
onClose: undefined,
},
}
export const DeletedClose: Story = {
name: 'Deleted + Close',
args: {
type: 'deleted',
onClose: action('close'),
},
}
export default meta

View File

@ -0,0 +1,97 @@
import { CloseIcon } from '@status-im/icons/12'
import { SadIcon } from '@status-im/icons/16'
import { Path, Svg } from 'react-native-svg'
import { Stack, Unspaced, XStack } from 'tamagui'
import { Avatar } from '../avatar'
import { Button } from '../button'
import { Paragraph } from '../typography'
interface Props {
type: 'text' | 'gif' | 'image' | 'deleted'
onClose?: VoidFunction
name: string
src: string
}
// FIXME: This should accept message or message ID and render the message accordingly
const Reply = (props: Props) => {
const { type, name, onClose, src } = props
const content =
type !== 'deleted' ? (
<XStack position="relative" space={4} alignItems="center" height={24}>
<Unspaced>
<Stack position="absolute" left={-24} top={10}>
<Connector />
</Stack>
</Unspaced>
<Avatar size={20} src={src} />
<Paragraph variant="smaller" weight="semibold" color="$neutral-100">
{name}
</Paragraph>
<Paragraph variant={11} weight="regular" color="$neutral-50">
{type === 'text' && 'What is the meaning of life? '}
{type === 'gif' && 'GIF'}
{type === 'image' && '5 photos'}
</Paragraph>
</XStack>
) : (
<XStack position="relative" space={4} alignItems="center" height={24}>
<Unspaced>
<Stack position="absolute" left={-24} top={10}>
<Connector />
</Stack>
</Unspaced>
<SadIcon color="$neutral-50" />
<Paragraph variant="smaller" weight="medium" color="$neutral-50">
Message deleted
</Paragraph>
</XStack>
)
return (
<XStack
space={8}
justifyContent="space-between"
alignItems="center"
paddingLeft={24}
>
{content}
{/* FIXME: This should be regular button with size 24 */}
{onClose && (
<Button
type="outline"
size={24}
icon={<CloseIcon />}
onPress={onClose}
/>
)}
</XStack>
)
}
const Connector = () => (
<Svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<Path
d="M16 1V1C8.16344 1 1 8.16344 1 16V16"
stroke="#A1ABBD"
strokeLinecap="round"
/>
</Svg>
)
export { Reply }
export type { Props as ReplyProps }

View File

@ -0,0 +1 @@
export { Sheet } from './sheet'

View File

@ -0,0 +1,32 @@
import { Button } from '../button'
import { Sheet } from './sheet'
import type { Meta, StoryObj } from '@storybook/react'
// More on how to set up stories at: https://storybook.js.org/docs/7.0/react/writing-stories/introduction
const meta: Meta<typeof Sheet> = {
// title: 'Messages',
component: Sheet,
argTypes: {},
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/IBmFKgGL1B4GzqD8LQTw6n/Design-System-for-Web?node-id=611%3A36006&t=Gyy71OAckl3b2TWj-4',
},
},
}
type Story = StoryObj<typeof Sheet>
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
export const Default: Story = {
args: {},
render: args => (
<Sheet {...args}>
<Button>Trigger</Button>
<Sheet.Content>hello</Sheet.Content>
</Sheet>
),
}
export default meta

View File

@ -0,0 +1,79 @@
import { Content, Overlay, Portal, Root, Trigger } from '@radix-ui/react-dialog'
import type React from 'react'
interface Props {
children: [React.ReactElement, React.ReactElement]
open?: boolean
onOpenChange?: (open: boolean) => void
press?: 'normal' | 'long'
}
const Sheet = (props: Props) => {
const { children, open, onOpenChange } = props
const [trigger, content] = children
return (
<Root open={open} onOpenChange={onOpenChange}>
{/* TRIGGER */}
<Trigger asChild>{trigger}</Trigger>
{/* CONTENT */}
<Portal>
<Overlay
style={{
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
}}
/>
{content}
</Portal>
</Root>
)
}
interface DialogContentProps {
children: React.ReactNode
initialFocusRef?: React.RefObject<HTMLElement>
}
const SheetContent = (props: DialogContentProps) => {
const { children, initialFocusRef } = props
const handleOpenAutoFocus = (event: Event) => {
if (initialFocusRef?.current) {
event.preventDefault()
initialFocusRef.current.focus()
}
}
return (
<Content
onOpenAutoFocus={handleOpenAutoFocus}
style={{
backgroundColor: 'white',
padding: 8,
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
position: 'fixed',
bottom: 0,
left: 0,
right: 0,
height: '80%',
// top: 'auto',
// from
// transform: 'translate3d(0,100%,0)',
// to
transform: 'translate3d(0,0,0)',
}}
>
{children}
</Content>
)
}
Sheet.Content = SheetContent
export { Sheet }

View File

@ -0,0 +1 @@
export { type TooltipProps, Tooltip } from './tooltip'

View File

@ -0,0 +1,3 @@
import type { PropsWithChildren } from 'react'
export const Tooltip = ({ children }: PropsWithChildren<unknown>) => children

View File

@ -0,0 +1,30 @@
import { Button } from '../button'
import { Tooltip } from './tooltip'
import type { Meta, StoryObj } from '@storybook/react'
const meta: Meta<typeof Tooltip> = {
component: Tooltip,
argTypes: {},
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/IBmFKgGL1B4GzqD8LQTw6n/Design-System-for-Web?node-id=15032%3A174184&t=PHVNitU0s0KwOi8L-0',
},
},
}
type Story = StoryObj<typeof Tooltip>
export const Default: Story = {
args: {
content: 'Sebastian Vettel reacted with a heart',
},
render: args => (
<Tooltip {...args}>
<Button type="outline">Trigger</Button>
</Tooltip>
),
}
export default meta

View File

@ -0,0 +1,83 @@
import { forwardRef } from 'react'
import {
Arrow,
Content,
Portal,
Root,
TooltipProvider,
Trigger,
} from '@radix-ui/react-tooltip'
import { Stack } from 'tamagui'
import { Paragraph } from '../typography'
import type { TooltipContentProps } from '@radix-ui/react-tooltip'
import type { Ref } from 'react'
interface Props {
children: React.ReactElement
content: React.ReactNode
delayDuration?: number
side?: TooltipContentProps['side']
sideOffset?: TooltipContentProps['sideOffset']
align?: TooltipContentProps['align']
alignOffset?: TooltipContentProps['alignOffset']
}
const Tooltip = (props: Props, ref: Ref<HTMLButtonElement>) => {
const {
children,
content,
delayDuration,
side,
sideOffset,
align,
alignOffset,
...triggerProps
} = props
return (
<TooltipProvider>
<Root delayDuration={delayDuration}>
<Trigger {...triggerProps} ref={ref} asChild>
{children}
</Trigger>
<Portal>
<Content
asChild
side={side}
sideOffset={sideOffset}
align={align}
alignOffset={alignOffset}
>
<Stack
backgroundColor="$neutral-95"
paddingVertical={6}
paddingHorizontal={12}
borderRadius={8}
shadowRadius={30}
shadowOffset="0px 8px"
shadowColor="rgba(9, 16, 28, 0.12)"
>
{typeof content === 'string' ? (
<Paragraph variant="smaller" weight="medium" color="$white-100">
{content}
</Paragraph>
) : (
content
)}
<Arrow width={11} height={5} />
</Stack>
</Content>
</Portal>
</Root>
</TooltipProvider>
)
}
const _Tooltip = forwardRef(Tooltip)
export { _Tooltip as Tooltip }
export type { Props as TooltipProps }

View File

@ -1,13 +1,21 @@
import { Divider, IconButton, Paragraph } from '@status-im/components'
import {
ArrowLeftIcon,
CommunitiesIcon,
DeleteIcon,
DownloadIcon,
LockedIcon,
MembersIcon,
MutedIcon,
OptionsIcon,
ShareIcon,
UpToDateIcon,
} from '@status-im/icons/20'
import { Stack } from '@tamagui/core'
import { BlurView } from 'expo-blur'
import { DropdownMenu } from '../dropdown-menu'
import type { GetProps, StackProps } from '@tamagui/core'
type BaseProps = GetProps<typeof Stack>
@ -104,7 +112,46 @@ const Topbar = (props: Props) => {
blurred={isBlurred}
/>
</Stack>
<IconButton icon={<OptionsIcon />} blurred={isBlurred} />
<DropdownMenu>
<IconButton icon={<OptionsIcon />} />
<DropdownMenu.Content align="end" sideOffset={4}>
<DropdownMenu.Item
icon={<CommunitiesIcon />}
label="View channel members and details"
onSelect={() => console.log('click')}
/>
<DropdownMenu.Item
icon={<MutedIcon />}
label="Mute channel"
onSelect={() => console.log('click')}
/>
<DropdownMenu.Item
icon={<UpToDateIcon />}
label="Mark as read"
onSelect={() => console.log('click')}
/>
<DropdownMenu.Item
icon={<DownloadIcon />}
label="Fetch messages"
onSelect={() => console.log('click')}
/>
<DropdownMenu.Item
icon={<ShareIcon />}
label="Share link to the channel"
onSelect={() => console.log('click')}
/>
<DropdownMenu.Separator />
<DropdownMenu.Item
icon={<DeleteIcon />}
label="Clear history"
onSelect={() => console.log('click')}
danger
/>
</DropdownMenu.Content>
</DropdownMenu>
</Stack>
</Stack>
</BlurView>

View File

@ -23,7 +23,6 @@ export default defineConfig(({ mode }) => {
'./src/reactions/index.ts',
],
fileName(format, entryName) {
console.log('fileName > format, entryName', format, entryName)
// const [name] = entryName.split('/')
return `icons-${entryName}.${format}.js`
},

224
yarn.lock
View File

@ -3612,6 +3612,14 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.0"
"@radix-ui/react-arrow@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz#5246adf79e97f89e819af68da51ddcf349ecf1c4"
integrity sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.1"
"@radix-ui/react-checkbox@^0.1.5":
version "0.1.5"
resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-0.1.5.tgz#3a6bd54ba1720c8e5c03852acf460e35dfbe9da3"
@ -3665,6 +3673,17 @@
"@radix-ui/react-primitive" "1.0.0"
"@radix-ui/react-slot" "1.0.0"
"@radix-ui/react-collection@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.0.1.tgz#259506f97c6703b36291826768d3c1337edd1de5"
integrity sha512-uuiFbs+YCKjn3X1DTSx9G7BHApu4GHbi3kgiwsnFUbOKCrwejAJv4eE4Vc8C0Oaxt9T0aV4ox0WCOdx+39Xo+g==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-context" "1.0.0"
"@radix-ui/react-primitive" "1.0.1"
"@radix-ui/react-slot" "1.0.1"
"@radix-ui/react-compose-refs@0.1.0", "@radix-ui/react-compose-refs@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-0.1.0.tgz#cff6e780a0f73778b976acff2c2a5b6551caab95"
@ -3758,6 +3777,18 @@
"@radix-ui/react-use-callback-ref" "1.0.0"
"@radix-ui/react-use-escape-keydown" "1.0.0"
"@radix-ui/react-dismissable-layer@1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.2.tgz#f04d1061bddf00b1ca304148516b9ddc62e45fb2"
integrity sha512-WjJzMrTWROozDqLB0uRWYvj4UuXsM/2L19EmQ3Au+IJWqwvwq9Bwd+P8ivo0Deg9JDPArR1I6MbWNi1CmXsskg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.0"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-primitive" "1.0.1"
"@radix-ui/react-use-callback-ref" "1.0.0"
"@radix-ui/react-use-escape-keydown" "1.0.2"
"@radix-ui/react-dropdown-menu@^0.1.6":
version "0.1.6"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-0.1.6.tgz#3203229788cd57e552c9f19dcc7008e2b545919c"
@ -3772,6 +3803,20 @@
"@radix-ui/react-primitive" "0.1.4"
"@radix-ui/react-use-controllable-state" "0.1.0"
"@radix-ui/react-dropdown-menu@^2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.2.tgz#758ca7733dc79b3a6523d2d5a8d33970ec7ece1b"
integrity sha512-r0kN0fstrSi+uAdK2GkLxnnbhqVBy/9Q4o4PvGOYipW0BldQlYBMSmZprvCNj2i2mAATx16kvzIn12GnaGjbMw==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.0"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-context" "1.0.0"
"@radix-ui/react-id" "1.0.0"
"@radix-ui/react-menu" "2.0.2"
"@radix-ui/react-primitive" "1.0.1"
"@radix-ui/react-use-controllable-state" "1.0.0"
"@radix-ui/react-focus-guards@0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-0.1.0.tgz#ba3b6f902cba7826569f8edc21ff8223dece7def"
@ -3779,6 +3824,13 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-focus-guards@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.0.tgz#339c1c69c41628c1a5e655f15f7020bf11aa01fa"
integrity sha512-UagjDk4ijOAnGu4WMUPj9ahi7/zJJqNZ9ZAiGPp7waUWJO0O1aWXi/udPphI0IUjvrhBsZJGSN66dR2dsueLWQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-focus-scope@0.1.4":
version "0.1.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-0.1.4.tgz#c830724e212d42ffaaa81aee49533213d09b47df"
@ -3789,6 +3841,16 @@
"@radix-ui/react-primitive" "0.1.4"
"@radix-ui/react-use-callback-ref" "0.1.0"
"@radix-ui/react-focus-scope@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.1.tgz#faea8c25f537c5a5c38c50914b63722db0e7f951"
integrity sha512-Ej2MQTit8IWJiS2uuujGUmxXjF/y5xZptIIQnyd2JHLwtV0R2j9NRVoRj/1j/gJ7e3REdaBw4Hjf4a1ImhkZcQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-primitive" "1.0.1"
"@radix-ui/react-use-callback-ref" "1.0.0"
"@radix-ui/react-id@0.1.5":
version "0.1.5"
resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-0.1.5.tgz#010d311bedd5a2884c1e9bb6aaaa4e6cc1d1d3b8"
@ -3840,6 +3902,31 @@
aria-hidden "^1.1.1"
react-remove-scroll "^2.4.0"
"@radix-ui/react-menu@2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.0.2.tgz#54d4e040407962af95ff3c66612749661a504de7"
integrity sha512-H5dtBi/k3tc45IMd2Pu+Q2PyONFlsYJ5sWUlflSs8BQRghh5GhJHLRuB1yb88VOywuzzvGkaR/HUJJ65Jf2POA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.0"
"@radix-ui/react-collection" "1.0.1"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-context" "1.0.0"
"@radix-ui/react-direction" "1.0.0"
"@radix-ui/react-dismissable-layer" "1.0.2"
"@radix-ui/react-focus-guards" "1.0.0"
"@radix-ui/react-focus-scope" "1.0.1"
"@radix-ui/react-id" "1.0.0"
"@radix-ui/react-popper" "1.1.0"
"@radix-ui/react-portal" "1.0.1"
"@radix-ui/react-presence" "1.0.0"
"@radix-ui/react-primitive" "1.0.1"
"@radix-ui/react-roving-focus" "1.0.2"
"@radix-ui/react-slot" "1.0.1"
"@radix-ui/react-use-callback-ref" "1.0.0"
aria-hidden "^1.1.1"
react-remove-scroll "2.5.5"
"@radix-ui/react-popover@^0.1.6":
version "0.1.6"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-0.1.6.tgz#788e969239d9c55239678e615ab591b6b7ba5cdc"
@ -3861,6 +3948,28 @@
aria-hidden "^1.1.1"
react-remove-scroll "^2.4.0"
"@radix-ui/react-popover@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.0.3.tgz#65ae2ee1fca2d7fd750308549eb8e0857c6160fe"
integrity sha512-YwedSukfWsyJs3/yP3yXUq44k4/JBe3jqU63Z8v2i19qZZ3dsx32oma17ztgclWPNuqp3A+Xa9UiDlZHyVX8Vg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.0"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-context" "1.0.0"
"@radix-ui/react-dismissable-layer" "1.0.2"
"@radix-ui/react-focus-guards" "1.0.0"
"@radix-ui/react-focus-scope" "1.0.1"
"@radix-ui/react-id" "1.0.0"
"@radix-ui/react-popper" "1.1.0"
"@radix-ui/react-portal" "1.0.1"
"@radix-ui/react-presence" "1.0.0"
"@radix-ui/react-primitive" "1.0.1"
"@radix-ui/react-slot" "1.0.1"
"@radix-ui/react-use-controllable-state" "1.0.0"
aria-hidden "^1.1.1"
react-remove-scroll "2.5.5"
"@radix-ui/react-popper@0.1.4":
version "0.1.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-0.1.4.tgz#dfc055dcd7dfae6a2eff7a70d333141d15a5d029"
@ -3892,6 +4001,23 @@
"@radix-ui/react-use-size" "1.0.0"
"@radix-ui/rect" "1.0.0"
"@radix-ui/react-popper@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.0.tgz#2be7e4c0cd4581f54277ca33a981c9037d2a8e60"
integrity sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@floating-ui/react-dom" "0.7.2"
"@radix-ui/react-arrow" "1.0.1"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-context" "1.0.0"
"@radix-ui/react-primitive" "1.0.1"
"@radix-ui/react-use-callback-ref" "1.0.0"
"@radix-ui/react-use-layout-effect" "1.0.0"
"@radix-ui/react-use-rect" "1.0.0"
"@radix-ui/react-use-size" "1.0.0"
"@radix-ui/rect" "1.0.0"
"@radix-ui/react-portal@0.1.4":
version "0.1.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-0.1.4.tgz#17bdce3d7f1a9a0b35cb5e935ab8bc562441a7d2"
@ -3909,6 +4035,14 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.0"
"@radix-ui/react-portal@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.1.tgz#169c5a50719c2bb0079cf4c91a27aa6d37e5dd33"
integrity sha512-NY2vUWI5WENgAT1nfC6JS7RU5xRYBfjZVLq0HmgEN1Ezy3rk/UruMV4+Rd0F40PEaFC5SrLS1ixYvcYIQrb4Ig==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.1"
"@radix-ui/react-presence@0.1.2":
version "0.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-0.1.2.tgz#9f11cce3df73cf65bc348e8b76d891f0d54c1fe3"
@ -3943,6 +4077,14 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-slot" "1.0.0"
"@radix-ui/react-primitive@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.1.tgz#c1ebcce283dd2f02e4fbefdaa49d1cb13dbc990a"
integrity sha512-fHbmislWVkZaIdeF6GZxF0A/NH/3BjrGIYj+Ae6eTmTCr7EB0RQAAVEiqsXK6p3/JcRqVSBQoceZroj30Jj3XA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-slot" "1.0.1"
"@radix-ui/react-roving-focus@0.1.5":
version "0.1.5"
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-0.1.5.tgz#cc48d17a36b56f253d54905b0fd60ee134cb97ee"
@ -3974,6 +4116,22 @@
"@radix-ui/react-use-callback-ref" "1.0.0"
"@radix-ui/react-use-controllable-state" "1.0.0"
"@radix-ui/react-roving-focus@1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.2.tgz#d8ac2e3b8006697bdfc2b0eb06bef7e15b6245de"
integrity sha512-HLK+CqD/8pN6GfJm3U+cqpqhSKYAWiOJDe+A+8MfxBnOue39QEeMa43csUn2CXCHQT0/mewh1LrrG4tfkM9DMA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.0"
"@radix-ui/react-collection" "1.0.1"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-context" "1.0.0"
"@radix-ui/react-direction" "1.0.0"
"@radix-ui/react-id" "1.0.0"
"@radix-ui/react-primitive" "1.0.1"
"@radix-ui/react-use-callback-ref" "1.0.0"
"@radix-ui/react-use-controllable-state" "1.0.0"
"@radix-ui/react-separator@^0.1.4":
version "0.1.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-separator/-/react-separator-0.1.4.tgz#383ad0f82b364d9982a978d752084af3598e4090"
@ -3998,6 +4156,14 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-slot@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.1.tgz#e7868c669c974d649070e9ecbec0b367ee0b4d81"
integrity sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-tabs@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.0.tgz#135c67f1f2bd9ada69a3f6e38dd897d459af5fe5"
@ -4073,6 +4239,25 @@
"@radix-ui/react-use-controllable-state" "1.0.0"
"@radix-ui/react-visually-hidden" "1.0.0"
"@radix-ui/react-tooltip@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.0.3.tgz#a8c7e7b2b542cdfe7e94122af79079f7de3f90ff"
integrity sha512-cmc9qV4KpgqdXVTn1K8KN8MnuSXvw+E719pKwyvpCGrQ+0AA2qTjcIL3uxCj4jc4k3sDR36RF7R3H7N5hPybBQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.0"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-context" "1.0.0"
"@radix-ui/react-dismissable-layer" "1.0.2"
"@radix-ui/react-id" "1.0.0"
"@radix-ui/react-popper" "1.1.0"
"@radix-ui/react-portal" "1.0.1"
"@radix-ui/react-presence" "1.0.0"
"@radix-ui/react-primitive" "1.0.1"
"@radix-ui/react-slot" "1.0.1"
"@radix-ui/react-use-controllable-state" "1.0.0"
"@radix-ui/react-visually-hidden" "1.0.1"
"@radix-ui/react-use-body-pointer-events@0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-body-pointer-events/-/react-use-body-pointer-events-0.1.1.tgz#63e7fd81ca7ffd30841deb584cd2b7f460df2597"
@ -4134,6 +4319,14 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-callback-ref" "1.0.0"
"@radix-ui/react-use-escape-keydown@1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.2.tgz#09ab6455ab240b4f0a61faf06d4e5132c4d639f6"
integrity sha512-DXGim3x74WgUv+iMNCF+cAo8xUHHeqvjx8zs7trKf+FkQKPQXLk2sX7Gx1ysH7Q76xCpZuxIJE7HLPxRE+Q+GA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-callback-ref" "1.0.0"
"@radix-ui/react-use-layout-effect@0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-0.1.0.tgz#ebf71bd6d2825de8f1fbb984abf2293823f0f223"
@ -4202,6 +4395,14 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.0"
"@radix-ui/react-visually-hidden@1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.1.tgz#9a4ac4fc97ae8d72a10e727f16b3121b5f0aa469"
integrity sha512-K1hJcCMfWfiYUibRqf3V8r5Drpyf7rh44jnrwAbdvI5iCCijilBBeyQv9SKidYNZIopMdCyR9FnIjkHxHN0FcQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.1"
"@radix-ui/rect@0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-0.1.1.tgz#95b5ba51f469bea6b1b841e2d427e17e37d38419"
@ -14437,6 +14638,7 @@ node-fetch-native@^1.0.1:
node-fetch@2.6.7, node-fetch@^2.2.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.7, node-fetch@^2.x.x:
version "2.6.7"
uid "1b5d62978f2ed07b99444f64f0df39f960a6d34d"
resolved "https://registry.npmjs.org/@achingbrain/node-fetch/-/node-fetch-2.6.7.tgz#1b5d62978f2ed07b99444f64f0df39f960a6d34d"
node-forge@^1.1.0, node-forge@^1.2.1, node-forge@^1.3.1:
@ -15883,6 +16085,17 @@ react-remove-scroll-bar@^2.3.3:
react-style-singleton "^2.2.1"
tslib "^2.0.0"
react-remove-scroll@2.5.5, react-remove-scroll@^2.5.5:
version "2.5.5"
resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz#1e31a1260df08887a8a0e46d09271b52b3a37e77"
integrity sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==
dependencies:
react-remove-scroll-bar "^2.3.3"
react-style-singleton "^2.2.1"
tslib "^2.1.0"
use-callback-ref "^1.3.0"
use-sidecar "^1.1.2"
react-remove-scroll@^2.4.0:
version "2.4.4"
resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.4.4.tgz#2dfff377cf17efc00de39dad51c143fc7a1b9e3e"
@ -15894,17 +16107,6 @@ react-remove-scroll@^2.4.0:
use-callback-ref "^1.2.3"
use-sidecar "^1.0.1"
react-remove-scroll@^2.5.5:
version "2.5.5"
resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz#1e31a1260df08887a8a0e46d09271b52b3a37e77"
integrity sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==
dependencies:
react-remove-scroll-bar "^2.3.3"
react-style-singleton "^2.2.1"
tslib "^2.1.0"
use-callback-ref "^1.3.0"
use-sidecar "^1.1.2"
react-router-dom@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.3.0.tgz#a0216da813454e521905b5fa55e0e5176123f43d"