Message reactions, add Tabs, update Dialog and Tooltip (#361)

* add Tabs component

* update Tooltip styling

* add reactions dialog

* use IconButton and simplify ReactButton

* add PressableTrigger to dialog

* update css reset

* update pressable type

* fix text story name

* update dynamic button props

* add counter to TabsTrigger

* fix casing in USerListProps

* make Dialog.Content customizable

* update dialogs
This commit is contained in:
Pavel 2023-03-30 14:41:56 +02:00 committed by GitHub
parent 6856d27e4f
commit 1e5e10eadd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 580 additions and 348 deletions

View File

@ -71,6 +71,7 @@ button,
textarea,
select {
font: inherit;
all: unset;
}
/*
8. Avoid text overflows

View File

@ -71,6 +71,7 @@ button,
textarea,
select {
font: inherit;
all: unset;
}
/*
8. Avoid text overflows

View File

@ -32,6 +32,7 @@
"@radix-ui/react-dialog": "^1.0.3",
"@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-tooltip": "^1.0.5",
"@status-im/icons": "*",
"@tamagui/animations-css": "1.7.7",

View File

@ -17,7 +17,7 @@ const Counter = (props: Props) => {
return (
<Base type={type}>
<Text size={11} color={textColor[type]}>
<Text size={11} weight="medium" color={textColors[type]}>
{value > 99 ? '99+' : value}
</Text>
</Base>
@ -31,7 +31,7 @@ const Base = styled(View, {
backgroundColor: '$primary-50',
paddingHorizontal: 3,
paddingVertical: 0,
borderRadius: '6px', // TODO: use tokens when fixed its definition
borderRadius: 6, // TODO: use tokens when fixed its definition
height: 16,
minWidth: 16,
maxWidth: 28,
@ -39,6 +39,8 @@ const Base = styled(View, {
justifyContent: 'center',
alignItems: 'center',
flexBasis: 'fit-content',
borderWidth: 1,
borderColor: 'transparent',
variants: {
type: {
@ -54,13 +56,12 @@ const Base = styled(View, {
outline: {
backgroundColor: 'transparent',
borderColor: '$neutral-20',
borderWidth: '1px',
},
},
},
})
const textColor: Record<NonNullable<Props['type']>, ColorTokens> = {
const textColors: Record<NonNullable<Props['type']>, ColorTokens> = {
default: '$white-100',
secondary: '$neutral-100',
outline: '$neutral-100',

View File

@ -24,7 +24,10 @@ export const Default: Story = {
render: () => (
<Dialog>
<Button>Trigger</Button>
<Dialog.Content>test</Dialog.Content>
<Dialog.Content borderRadius={16} width={400}>
test
</Dialog.Content>
</Dialog>
),
}

View File

@ -1,10 +1,15 @@
import { forwardRef } from 'react'
import { cloneElement, forwardRef } from 'react'
import { Content, Overlay, Portal, Root, Trigger } from '@radix-ui/react-dialog'
import { Stack, styled, useMedia } from 'tamagui'
import { Sheet } from '../sheet'
import {
Close,
Content,
Overlay,
Portal,
Root,
Trigger,
} from '@radix-ui/react-dialog'
import type { DialogTriggerProps } from '@radix-ui/react-dialog'
import type { Ref } from 'react'
import type React from 'react'
@ -15,74 +20,61 @@ interface Props {
press?: 'normal' | 'long'
}
const Wrapper = styled(Stack, {
position: 'absolute',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100vw',
height: '100vh',
})
// 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 { children, open, onOpenChange, press = 'normal' } = props
const [trigger, content] = children
const media = useMedia()
// const media = useMedia()
if (media.sm) {
return (
<Sheet>
{trigger}
{content}
</Sheet>
)
}
// if (media.sm) {
// return (
// <Sheet>
// {trigger}
// {content}
// </Sheet>
// )
// }
return (
<Root open={open} onOpenChange={onOpenChange}>
{/* TRIGGER */}
<Trigger asChild>{trigger}</Trigger>
<Trigger asChild>
<PressableTrigger press={press}>{trigger}</PressableTrigger>
</Trigger>
{/* CONTENT */}
<Portal>
<Wrapper>
<Overlay
style={{
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
}}
/>
{content}
</Wrapper>
<Overlay
style={{
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
}}
/>
{content}
</Portal>
</Root>
)
}
const PressableTrigger = forwardRef(function _PressableTrigger(
props: DialogTriggerProps & {
press: Props['press']
children: React.ReactElement
},
ref
) {
const { children, press, onClick, ...triggerProps } = props
const handler = press === 'normal' ? 'onPress' : 'onLongPress'
return cloneElement(children, { ref, ...triggerProps, [handler]: onClick })
})
interface DialogContentProps {
// title: string
// description?: string
children: React.ReactNode
// action: string
// onAction: (close: VoidFunction) => void
// onCancel?: () => void
borderRadius: 8 | 12 | 16
width: number
initialFocusRef?: React.RefObject<HTMLElement>
}
@ -96,25 +88,25 @@ const DialogContent = (props: DialogContentProps, ref: Ref<HTMLDivElement>) => {
}
}
const media = useMedia()
// const media = useMedia()
if (media.sm) {
return <Sheet.Content>{children}</Sheet.Content>
}
// if (media.sm) {
// return <Sheet.Content>{children}</Sheet.Content>
// }
return (
<Content
ref={ref}
onOpenAutoFocus={handleOpenAutoFocus}
// TODO: use tamagui components
style={{
backgroundColor: 'white',
padding: 8,
width: 400,
borderRadius: 8,
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: 'white',
borderRadius: props.borderRadius,
width: props.width,
}}
>
{children}
@ -124,4 +116,4 @@ const DialogContent = (props: DialogContentProps, ref: Ref<HTMLDivElement>) => {
Dialog.Content = forwardRef(DialogContent)
export { Dialog }
export { Close, Dialog }

View File

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

View File

@ -6,7 +6,6 @@ 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 DynamicButton> = {
title: 'DynamicButton',
component: DynamicButton,
args: {},
argTypes: {},

View File

@ -6,10 +6,9 @@ import { Stack, styled } from '@tamagui/core'
import { Shadow } from '../shadow'
import { Text } from '../text'
import type { GetVariants } from '../types'
import type { ColorTokens, StackProps } from '@tamagui/core'
import type { GetVariants, PressableProps } from '../types'
import type { ColorTokens } from '@tamagui/core'
import type { Ref } from 'react'
import type { PressableProps } from 'react-native'
type Variants = GetVariants<typeof Button>
@ -27,7 +26,7 @@ const DynamicButton = (props: Props, ref: Ref<HTMLButtonElement>) => {
return (
<Shadow variant="$2" borderRadius={999}>
<Button
{...(pressableProps as unknown as StackProps)} // TODO: Tamagui has incorrect types for PressableProps
{...(pressableProps as unknown as object)} // TODO: Tamagui has incorrect types for PressableProps
ref={ref}
type={type}
iconOnly={showCount === false}
@ -51,8 +50,8 @@ export type { Props as DynamicButtonProps }
const Button = styled(Stack, {
name: 'DynamicButton',
accessibilityRole: 'button',
tag: 'button',
accessibilityRole: 'button',
cursor: 'pointer',
userSelect: 'none',

View File

@ -1,7 +1,8 @@
import { useState } from 'react'
import type { PressableProps } from '../types'
import type { ColorTokens } from 'tamagui'
import type { MouseEvent } from 'react-native'
import type { ColorTokens, TamaguiComponentPropsBase } from 'tamagui'
type Config = {
default: ColorTokens
@ -12,8 +13,9 @@ type Config = {
type Return = {
color: ColorTokens
// FIXME: use PressableProps instead TamaguiComponentPropsBase, fix necessary in Tamagui
pressableProps: Pick<
PressableProps,
TamaguiComponentPropsBase,
'onHoverIn' | 'onHoverOut' | 'onPressIn' | 'onPressOut'
>
}
@ -49,11 +51,11 @@ export const usePressableColors = (
color: styles[key],
pressableProps: {
onHoverIn: event => {
props.onHoverIn?.(event)
props.onHoverIn?.(event as unknown as MouseEvent)
setHovered(true)
},
onHoverOut: event => {
props.onHoverOut?.(event)
props.onHoverOut?.(event as unknown as MouseEvent)
setHovered(false)
},
onPressIn: event => {

View File

@ -5,7 +5,6 @@ import { Stack, styled } from 'tamagui'
import { usePressableColors } from '../hooks/use-pressable-colors'
import type { GetVariants, PressableProps } from '../types'
import type { StackProps } from '@tamagui/core'
import type { Ref } from 'react'
type Variants = GetVariants<typeof Base>
@ -41,8 +40,8 @@ const IconButton = (props: Props, ref: Ref<HTMLButtonElement>) => {
return (
<Base
{...(buttonProps as unknown as StackProps)} // TODO: Tamagui has incorrect types for PressableProps
{...(pressableProps as unknown as StackProps)} // TODO: Tamagui has incorrect types for PressableProps
{...(buttonProps as unknown as object)} // TODO: Tamagui has incorrect types for PressableProps
{...pressableProps}
ref={ref}
variant={blur ? undefined : variant}
active={blur ? undefined : selected ? variant : undefined}

View File

@ -1,7 +1,15 @@
import {
AngryIcon,
LaughIcon,
LoveIcon,
SadIcon,
ThumbsDownIcon,
ThumbsUpIcon,
} from '@status-im/icons/reactions'
import { XStack } from 'tamagui'
import { IconButton } from '../../icon-button'
import { Popover } from '../../popover'
import { ReactButton } from '../../react-button'
import type { PopoverProps } from '../../popover'
import type { ReactionsType } from '../types'
@ -12,6 +20,15 @@ type Props = Omit<PopoverProps, 'children'> & {
onOpenChange?: PopoverProps['onOpenChange']
}
export const REACTIONS_ICONS = {
love: <LoveIcon />,
laugh: <LaughIcon />,
'thumbs-up': <ThumbsUpIcon />,
'thumbs-down': <ThumbsDownIcon />,
sad: <SadIcon />,
angry: <AngryIcon />,
} as const
export const ReactionPopover = (props: Props) => {
const { children, reactions, onOpenChange, ...popoverProps } = props
@ -21,40 +38,34 @@ export const ReactionPopover = (props: Props) => {
<Popover.Content>
<XStack space={2} padding={2}>
<ReactButton
<IconButton
icon={REACTIONS_ICONS['love']}
variant="ghost"
size={32}
icon="love"
selected={reactions['love']?.has('me')}
/>
<ReactButton
<IconButton
icon={REACTIONS_ICONS['thumbs-up']}
variant="ghost"
size={32}
icon="thumbs-up"
selected={reactions['thumbs-up']?.has('me')}
/>
<ReactButton
<IconButton
icon={REACTIONS_ICONS['thumbs-down']}
variant="ghost"
size={32}
icon="thumbs-down"
selected={reactions['thumbs-down']?.has('me')}
/>
<ReactButton
<IconButton
icon={REACTIONS_ICONS['laugh']}
variant="ghost"
size={32}
icon="laugh"
selected={reactions.laugh?.has('me')}
/>
<ReactButton
<IconButton
icon={REACTIONS_ICONS['sad']}
variant="ghost"
size={32}
icon="sad"
selected={reactions.sad?.has('me')}
/>
<ReactButton
<IconButton
icon={REACTIONS_ICONS['angry']}
variant="ghost"
size={32}
icon="angry"
selected={reactions.angry?.has('me')}
/>
</XStack>

View File

@ -0,0 +1,89 @@
import { useMemo } from 'react'
import { Stack } from '@tamagui/web'
import { Dialog } from '../../dialog'
import { REACTIONS_ICONS } from '../../react-button/react-button'
import { Tabs } from '../../tabs'
import { UserList } from '../../user-list'
import type { UserListProps } from '../../user-list'
import type { ReactionsType, ReactionType } from '../types'
type Props = {
initialReactionType: ReactionType
reactions: ReactionsType
}
export const ReactionsDialog = (props: Props) => {
const { initialReactionType, reactions } = props
const users: UserListProps['users'] = useMemo(() => {
return [
{
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',
},
]
}, [reactions])
return (
<Dialog.Content width={352} borderRadius={12}>
<Tabs defaultValue={initialReactionType}>
<Stack padding={16}>
<Tabs.List size={24}>
{Object.entries(reactions).map(([reaction, value]) => {
const Icon = REACTIONS_ICONS[reaction as keyof ReactionsType]
return (
<Tabs.Trigger key={reaction} value={reaction} icon={<Icon />}>
{value.size.toString()}
</Tabs.Trigger>
)
})}
</Tabs.List>
</Stack>
<Stack padding={8} paddingTop={0}>
{Object.entries(reactions).map(([reaction]) => {
return (
<Tabs.Content key={reaction} value={reaction}>
<UserList users={users} />
</Tabs.Content>
)
})}
</Stack>
</Tabs>
</Dialog.Content>
)
}

View File

@ -1,12 +1,15 @@
import { XStack } from 'tamagui'
import { createElement, useState } from 'react'
import { Stack, XStack } from 'tamagui'
import { Dialog } from '../../dialog'
import { ReactButton } from '../../react-button'
import { REACTIONS_ICONS } from '../../react-button/react-button'
import { Text } from '../../text'
import { Tooltip } from '../../tooltip/tooltip'
import { UserList } from '../../user-list'
import { ReactionPopover } from './reaction-popover'
import { ReactionsDialog } from './reactions-dialog'
import type { ReactButtonProps } from '../../react-button'
import type { ReactionsType, ReactionType } from '../types'
type Props = {
@ -24,13 +27,11 @@ export const Reactions = (props: Props) => {
return (
<XStack space={6} flexWrap="wrap">
{Object.entries(reactions).map(([type, value]) => (
{Object.keys(reactions).map(type => (
<Reaction
key={type}
size="compact"
icon={type as ReactionType}
count={value.size}
selected={value.has('me')}
type={type as ReactionType}
reactions={reactions}
/>
))}
@ -40,70 +41,67 @@ export const Reactions = (props: Props) => {
align="start"
sideOffset={8}
>
<ReactButton size="compact" icon="add" selected={false} />
<ReactButton type="add" />
</ReactionPopover>
</XStack>
)
}
const Reaction = (props: ReactButtonProps) => {
type ReactionProps = {
type: ReactionType
reactions: ReactionsType
}
const Reaction = (props: ReactionProps) => {
const { type, reactions } = props
const value = reactions[type]!
const icon = REACTIONS_ICONS[type]
const [dialogOpen, setDialogOpen] = useState(false)
return (
<Dialog press="long">
<Dialog press="long" open={dialogOpen} onOpenChange={setDialogOpen}>
<Tooltip
side="bottom"
sideOffset={4}
content={
<>
You, Mr Gandalf, Ariana Perlona and
<button>3 more</button> reacted with {'[ICON]'}
</>
<Stack
tag="button"
cursor="pointer"
onPress={() => setDialogOpen(true)}
>
<Text size={13} weight="medium" color="$neutral-100">
You, Mr Gandalf, Ariana Perlona
</Text>
<Stack flexDirection="row" alignItems="center" gap={4}>
<Text size={13} weight="medium" color="$neutral-100">
and
</Text>
<Stack
backgroundColor="$turquoise-50-opa-10"
borderRadius={6}
paddingHorizontal={4}
>
<Text size={13} weight="medium" color="$turquoise-50">
3 more
</Text>
</Stack>
<Text size={13} weight="medium" color="$neutral-100">
reacted with
</Text>
{createElement(icon)}
</Stack>
</Stack>
}
>
<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',
},
]}
<ReactButton
type={type as ReactionType}
count={value.size}
selected={value.has('me')}
/>
</Dialog.Content>
</Tooltip>
<ReactionsDialog initialReactionType={type} reactions={reactions} />
</Dialog>
)
}

View File

@ -1,13 +1,11 @@
import { useState } from 'react'
import { CloseIcon, PinIcon } from '@status-im/icons/20'
import { styled } from '@tamagui/core'
import { Pressable, View } from 'react-native'
import { Stack } from '@tamagui/core'
import { Pressable } from 'react-native'
import { Banner } from '../banner'
import { Button } from '../button'
import { ContextTag } from '../context-tag'
import { Dialog } from '../dialog'
import { Close, Dialog } from '../dialog'
import { Message } from '../messages'
import { Text } from '../text'
@ -19,78 +17,39 @@ type Props = {
const PinnedMessage = (props: Props) => {
const { messages } = props
const [isDetailVisible, setIsDetailVisible] = useState(false)
return messages.length > 0 ? (
<Dialog open={isDetailVisible}>
<Pressable onPress={() => setIsDetailVisible(true)}>
return (
<Dialog>
<Pressable>
<Banner count={messages.length} icon={<PinIcon />}>
{messages[0].text}
</Banner>
</Pressable>
<Base>
<Button
variant="grey"
onPress={() => setIsDetailVisible(false)}
size={32}
icon={<CloseIcon />}
/>
<DialogHeader>
<Text size={27} weight="semibold">
Pinned Messages
</Text>
<ContextTag
src="https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fi.seadn.io%2Fgae%2FFG0QJ00fN3c_FWuPeUr9-T__iQl63j9hn5d6svW8UqOmia5zp3lKHPkJuHcvhZ0f_Pd6P2COo9tt9zVUvdPxG_9BBw%3Fw%3D500%26auto%3Dformat&f=1&nofb=1&ipt=c177cd71d8d0114080cfc6efd3f9e098ddaeb1b347919bd3089bf0aacb003b3e&ipo=images"
label={['Rarible', '# random']}
/>
</DialogHeader>
<DialogContent>
<Dialog.Content width={480} borderRadius={16}>
<Stack padding={16} alignItems="flex-start">
<Close asChild>
<Button variant="grey" size={32} icon={<CloseIcon />} />
</Close>
<Stack paddingTop={24} gap={8} alignItems="flex-start">
<Text size={27} weight="semibold">
Pinned Messages
</Text>
<ContextTag
src="https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fi.seadn.io%2Fgae%2FFG0QJ00fN3c_FWuPeUr9-T__iQl63j9hn5d6svW8UqOmia5zp3lKHPkJuHcvhZ0f_Pd6P2COo9tt9zVUvdPxG_9BBw%3Fw%3D500%26auto%3Dformat&f=1&nofb=1&ipt=c177cd71d8d0114080cfc6efd3f9e098ddaeb1b347919bd3089bf0aacb003b3e&ipo=images"
label={['Rarible', '# random']}
/>
</Stack>
</Stack>
<Stack padding={8}>
{messages.map(message => (
<Message key={message.id} {...message} pinned={false} />
))}
</DialogContent>
</Base>
</Stack>
</Dialog.Content>
</Dialog>
) : null
)
}
export { PinnedMessage }
export type { Props as PinnedMessageProps }
const Base = styled(View, {
position: 'relative',
paddingHorizontal: 16,
paddingVertical: 16,
borderRadius: 16,
alignSelf: 'center',
alignItems: 'flex-start',
maxWidth: 480,
backgroundColor: '$neutral-5',
zIndex: 100,
variants: {
active: {
true: {
backgroundColor: '$neutral-5',
},
},
pinned: {
true: {
backgroundColor: '$blue-50-opa-5',
},
},
} as const,
})
const DialogHeader = styled(View, {
alignItems: 'flex-start',
justifyContent: 'flex-start',
paddingVertical: 16,
space: 11,
})
const DialogContent = styled(View, {
alignItems: 'stretch',
justifyContent: 'flex-start',
})

View File

@ -6,62 +6,24 @@ 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" />
<ReactButton {...args} type="laugh" />
<ReactButton {...args} type="love" />
<ReactButton {...args} type="sad" />
<ReactButton {...args} type="thumbs-up" />
<ReactButton {...args} type="thumbs-down" />
<ReactButton {...args} type="angry" />
<ReactButton {...args} type="add" />
</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 const Default: Story = {}
export default meta

View File

@ -9,18 +9,16 @@ import {
ThumbsDownIcon,
ThumbsUpIcon,
} from '@status-im/icons/reactions'
import { Stack, styled } from '@tamagui/core'
import { styled } from '@tamagui/core'
import { Stack } from '@tamagui/web'
import { Text } from '../text'
import type { GetVariants } from '../types'
import type { StackProps } from '@tamagui/core'
import type { ReactionType } from '../messages/types'
import type { PressableProps } from '../types'
import type { Ref } from 'react'
import type { PressableProps } from 'react-native'
type Variants = GetVariants<typeof Button>
export const REACTIONS = {
export const REACTIONS_ICONS = {
love: LoveIcon,
laugh: LaughIcon,
'thumbs-up': ThumbsUpIcon,
@ -31,10 +29,7 @@ export const REACTIONS = {
} as const
type Props = PressableProps & {
icon: keyof typeof REACTIONS
variant?: Variants['variant']
size?: Variants['size']
// FIXME: use aria-selected
type: ReactionType
selected?: boolean
count?: number
// FIXME: update to latest RN
@ -43,25 +38,17 @@ type Props = PressableProps & {
}
const ReactButton = (props: Props, ref: Ref<HTMLButtonElement>) => {
const {
icon,
variant = 'outline',
size = 40,
count,
...pressableProps
} = props
const { type, count, ...pressableProps } = props
const Icon = REACTIONS[icon]
const Icon = REACTIONS_ICONS[type]
const selected =
props.selected || props['aria-expanded'] || props['aria-selected']
return (
<Button
{...(pressableProps as unknown as StackProps)} // TODO: Tamagui has incorrect types for PressableProps
{...(pressableProps as unknown as object)}
ref={ref}
variant={variant}
size={size}
selected={selected}
>
<Icon color="$neutral-100" />
@ -81,6 +68,7 @@ export type { Props as ReactButtonProps }
const Button = styled(Stack, {
name: 'ReactButton',
tag: 'button',
accessibilityRole: 'button',
cursor: 'pointer',
@ -93,50 +81,24 @@ const Button = styled(Stack, {
animation: 'fast',
space: 4,
borderRadius: 8,
minWidth: 36,
height: 24,
paddingHorizontal: 8,
borderColor: '$neutral-20',
hoverStyle: { borderColor: '$neutral-30' },
pressStyle: {
backgroundColor: '$neutral-10',
borderColor: '$neutral-20',
},
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,
})

View File

@ -3,11 +3,11 @@ import { Stack } from '@tamagui/core'
import { DividerLabel } from '../dividers'
import { UserList } from '../user-list'
import type { USerListProps } from '../user-list'
import type { UserListProps } from '../user-list'
type GroupProps = {
label: string
users: USerListProps['users']
users: UserListProps['users']
}
const Group = (props: GroupProps) => {

View File

@ -0,0 +1 @@
export { Tabs, type TabsProps } from './tabs'

View File

@ -0,0 +1,72 @@
import { PlaceholderIcon } from '@status-im/icons/20'
import { Text } from '../text'
import { Tabs } from './tabs'
import type { Meta, StoryObj } from '@storybook/react'
import type { ComponentProps } from 'react'
const meta: Meta<typeof Tabs> = {
component: Tabs,
argTypes: {},
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/IBmFKgGL1B4GzqD8LQTw6n/Design-System-for-Desktop%2FWeb?node-id=57-13214&t=q5DFi3jlBAcdghLy-11',
},
},
}
type Story = StoryObj<{ size: 24 | 32; icon: boolean; count: boolean }>
export const Default: Story = {
name: 'Default',
args: {
size: 24,
icon: false,
},
argTypes: {
size: {
control: 'select',
options: [24, 32] satisfies ComponentProps<typeof Tabs.List>['size'][],
},
icon: {
control: 'boolean',
},
count: {
control: 'boolean',
},
},
render(args) {
const icon = args.icon ? <PlaceholderIcon /> : undefined
const count = args.count ? 8 : undefined
return (
<Tabs defaultValue="1">
<Tabs.List size={args.size}>
<Tabs.Trigger value="1" icon={icon} count={count}>
Tab 1
</Tabs.Trigger>
<Tabs.Trigger value="2" icon={icon}>
Tab 2
</Tabs.Trigger>
<Tabs.Trigger value="3" icon={icon}>
Tab 3
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="1">
<Text size={15}>Content 1</Text>
</Tabs.Content>
<Tabs.Content value="2">
<Text size={15}>Content 2</Text>
</Tabs.Content>
<Tabs.Content value="3">
<Text size={15}>Content 3</Text>
</Tabs.Content>
</Tabs>
)
},
}
export default meta

View File

@ -0,0 +1,164 @@
import { Children, cloneElement, forwardRef } from 'react'
import { Content, List, Root, Trigger } from '@radix-ui/react-tabs'
import { Stack } from '@tamagui/web'
import { styled } from 'tamagui'
import { Counter } from '../counter'
import { usePressableColors } from '../hooks/use-pressable-colors'
import { Text } from '../text'
import type { TextProps } from '../text'
import type { GetVariants } from '../types'
import type { Ref } from 'react'
import type { View } from 'react-native'
type Variants = GetVariants<typeof TriggerBase>
type Props = {
children: React.ReactNode[]
defaultValue: string
value?: string
onValueChange?: (value: string) => void
}
const Tabs = (props: Props) => {
const { children, defaultValue, value, onValueChange } = props
return (
<Root
defaultValue={defaultValue}
value={value}
onValueChange={onValueChange}
>
{children}
</Root>
)
}
type ListProps = {
children: React.ReactElement[]
size: Variants['size']
}
const TabsList = (props: ListProps) => {
const { children } = props
return (
<List asChild>
<Stack flexDirection="row" gap={8}>
{Children.map(children, child => (
<Trigger asChild value={child.props.value}>
{cloneElement(child, { size: props.size })}
</Trigger>
))}
</Stack>
</List>
)
}
type TriggerProps = {
value: string
children: string
icon?: React.ReactElement
count?: number
}
// TODO: Add counter
const TabsTrigger = (props: TriggerProps, ref: Ref<View>) => {
const { icon = null, children, count, ...triggerProps } = props
// props coming from parent List and Trigger, not passed by the user (line 52)
const providedProps = props as TriggerProps & {
size: 24 | 32
'aria-selected': boolean
}
const { color, pressableProps } = usePressableColors(
{
default: '$neutral-100',
hover: '$neutral-100',
press: '$neutral-100',
active: '$white-100',
},
providedProps
)
const { size, 'aria-selected': selected } = providedProps
const textSize = triggerTextSizes[size]
return (
<TriggerBase
{...triggerProps}
{...pressableProps}
ref={ref}
size={size}
active={selected}
>
{icon && cloneElement(icon, { size: iconSizes[size] })}
<Text size={textSize} weight="medium" color={color}>
{children}
</Text>
{count && (
<Stack marginRight={-4}>
<Counter type="secondary" value={count} />
</Stack>
)}
</TriggerBase>
)
}
Tabs.List = TabsList
Tabs.Trigger = forwardRef(TabsTrigger)
Tabs.Content = Content
export { Tabs }
export type { Props as TabsProps }
const TriggerBase = styled(Stack, {
tag: 'button',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
variants: {
size: {
32: {
height: 32,
borderRadius: 10,
paddingHorizontal: 12,
gap: 6,
},
24: {
height: 24,
borderRadius: 8,
paddingHorizontal: 8,
gap: 4,
},
},
active: {
true: {
backgroundColor: '$neutral-50',
},
false: {
backgroundColor: '$neutral-10',
hoverStyle: {
backgroundColor: '$neutral-20',
},
},
},
},
})
const triggerTextSizes: Record<Variants['size'], TextProps['size']> = {
'32': 15,
'24': 13,
}
// FIXME: icons will accept size as number
const iconSizes: Record<Variants['size'], number> = {
'32': 16,
'24': 12,
}

View File

@ -5,7 +5,8 @@ import { Text } from './text'
import type { Meta, StoryObj } from '@storybook/react'
const meta: Meta = {
title: 'Text',
title: 'text',
args: {
children: 'The quick brown fox jumped over the lazy dog.',
},
@ -19,8 +20,7 @@ const meta: Meta = {
},
}
export const TextStory: StoryObj<typeof Text> = {
name: 'TextNew',
export const Default: StoryObj<typeof Text> = {
render: args => (
<Stack gap={24}>
<Stack gap={8}>

View File

@ -8,8 +8,9 @@ import {
TooltipProvider,
Trigger,
} from '@radix-ui/react-tooltip'
import { Stack } from 'tamagui'
import { useTheme } from 'tamagui'
import { Shadow } from '../shadow'
import { Text } from '../text'
import type { TooltipContentProps } from '@radix-ui/react-tooltip'
@ -37,6 +38,8 @@ const Tooltip = (props: Props, ref: Ref<HTMLButtonElement>) => {
...triggerProps
} = props
const theme = useTheme() // not ideal
return (
<TooltipProvider>
<Root delayDuration={delayDuration}>
@ -52,24 +55,22 @@ const Tooltip = (props: Props, ref: Ref<HTMLButtonElement>) => {
align={align}
alignOffset={alignOffset}
>
<Stack
backgroundColor="$neutral-95"
<Shadow
variant="$3"
backgroundColor="$white-100"
paddingVertical={6}
paddingHorizontal={12}
borderRadius={8}
shadowRadius={30}
shadowOffset="0px 8px"
shadowColor="rgba(9, 16, 28, 0.12)"
>
{typeof content === 'string' ? (
<Text size={13} weight="medium" color="$white-100">
<Text size={13} weight="medium" color="$neutral-100">
{content}
</Text>
) : (
content
)}
<Arrow width={11} height={5} />
</Stack>
<Arrow width={11} height={5} fill={theme.background.val} />
</Shadow>
</Content>
</Portal>
</Root>

View File

@ -17,7 +17,7 @@ type PressableProps = {
delayHoverIn?: NativePressableProps['delayHoverIn']
delayHoverOut?: NativePressableProps['delayHoverOut']
delayLongPress?: NativePressableProps['delayLongPress']
disabled?: NativePressableProps['disabled']
disabled?: boolean
}
export type MapVariant<

View File

@ -49,4 +49,4 @@ const UserList = (props: Props) => {
}
export { UserList }
export type { Props as USerListProps }
export type { Props as UserListProps }

View File

@ -3541,6 +3541,21 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-tabs@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.3.tgz#8b4158160a7c6633c893c74641e929d2708e709a"
integrity sha512-4CkF/Rx1GcrusI/JZ1Rvyx4okGUs6wEenWA0RG/N+CwkRhTy7t54y7BLsWUXrAz/GRbBfHQg/Odfs/RoW0CiRA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "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-presence" "1.0.0"
"@radix-ui/react-primitive" "1.0.2"
"@radix-ui/react-roving-focus" "1.0.3"
"@radix-ui/react-use-controllable-state" "1.0.0"
"@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"