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:
parent
6856d27e4f
commit
1e5e10eadd
|
@ -71,6 +71,7 @@ button,
|
||||||
textarea,
|
textarea,
|
||||||
select {
|
select {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
|
all: unset;
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
8. Avoid text overflows
|
8. Avoid text overflows
|
||||||
|
|
|
@ -71,6 +71,7 @@ button,
|
||||||
textarea,
|
textarea,
|
||||||
select {
|
select {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
|
all: unset;
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
8. Avoid text overflows
|
8. Avoid text overflows
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
"@radix-ui/react-dialog": "^1.0.3",
|
"@radix-ui/react-dialog": "^1.0.3",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.4",
|
"@radix-ui/react-dropdown-menu": "^2.0.4",
|
||||||
"@radix-ui/react-popover": "^1.0.5",
|
"@radix-ui/react-popover": "^1.0.5",
|
||||||
|
"@radix-ui/react-tabs": "^1.0.3",
|
||||||
"@radix-ui/react-tooltip": "^1.0.5",
|
"@radix-ui/react-tooltip": "^1.0.5",
|
||||||
"@status-im/icons": "*",
|
"@status-im/icons": "*",
|
||||||
"@tamagui/animations-css": "1.7.7",
|
"@tamagui/animations-css": "1.7.7",
|
||||||
|
|
|
@ -17,7 +17,7 @@ const Counter = (props: Props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Base type={type}>
|
<Base type={type}>
|
||||||
<Text size={11} color={textColor[type]}>
|
<Text size={11} weight="medium" color={textColors[type]}>
|
||||||
{value > 99 ? '99+' : value}
|
{value > 99 ? '99+' : value}
|
||||||
</Text>
|
</Text>
|
||||||
</Base>
|
</Base>
|
||||||
|
@ -31,7 +31,7 @@ const Base = styled(View, {
|
||||||
backgroundColor: '$primary-50',
|
backgroundColor: '$primary-50',
|
||||||
paddingHorizontal: 3,
|
paddingHorizontal: 3,
|
||||||
paddingVertical: 0,
|
paddingVertical: 0,
|
||||||
borderRadius: '6px', // TODO: use tokens when fixed its definition
|
borderRadius: 6, // TODO: use tokens when fixed its definition
|
||||||
height: 16,
|
height: 16,
|
||||||
minWidth: 16,
|
minWidth: 16,
|
||||||
maxWidth: 28,
|
maxWidth: 28,
|
||||||
|
@ -39,6 +39,8 @@ const Base = styled(View, {
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
flexBasis: 'fit-content',
|
flexBasis: 'fit-content',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'transparent',
|
||||||
|
|
||||||
variants: {
|
variants: {
|
||||||
type: {
|
type: {
|
||||||
|
@ -54,13 +56,12 @@ const Base = styled(View, {
|
||||||
outline: {
|
outline: {
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
borderColor: '$neutral-20',
|
borderColor: '$neutral-20',
|
||||||
borderWidth: '1px',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const textColor: Record<NonNullable<Props['type']>, ColorTokens> = {
|
const textColors: Record<NonNullable<Props['type']>, ColorTokens> = {
|
||||||
default: '$white-100',
|
default: '$white-100',
|
||||||
secondary: '$neutral-100',
|
secondary: '$neutral-100',
|
||||||
outline: '$neutral-100',
|
outline: '$neutral-100',
|
||||||
|
|
|
@ -24,7 +24,10 @@ export const Default: Story = {
|
||||||
render: () => (
|
render: () => (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<Button>Trigger</Button>
|
<Button>Trigger</Button>
|
||||||
<Dialog.Content>test</Dialog.Content>
|
|
||||||
|
<Dialog.Content borderRadius={16} width={400}>
|
||||||
|
test
|
||||||
|
</Dialog.Content>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
import { Stack, styled, useMedia } from 'tamagui'
|
Close,
|
||||||
|
Content,
|
||||||
import { Sheet } from '../sheet'
|
Overlay,
|
||||||
|
Portal,
|
||||||
|
Root,
|
||||||
|
Trigger,
|
||||||
|
} from '@radix-ui/react-dialog'
|
||||||
|
|
||||||
|
import type { DialogTriggerProps } from '@radix-ui/react-dialog'
|
||||||
import type { Ref } from 'react'
|
import type { Ref } from 'react'
|
||||||
import type React from 'react'
|
import type React from 'react'
|
||||||
|
|
||||||
|
@ -15,53 +20,31 @@ interface Props {
|
||||||
press?: 'normal' | 'long'
|
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 Dialog = (props: Props) => {
|
||||||
const { children, open, onOpenChange /* press = 'normal' */ } = props
|
const { children, open, onOpenChange, press = 'normal' } = props
|
||||||
|
|
||||||
const [trigger, content] = children
|
const [trigger, content] = children
|
||||||
|
|
||||||
const media = useMedia()
|
// const media = useMedia()
|
||||||
|
|
||||||
if (media.sm) {
|
// if (media.sm) {
|
||||||
return (
|
// return (
|
||||||
<Sheet>
|
// <Sheet>
|
||||||
{trigger}
|
// {trigger}
|
||||||
{content}
|
// {content}
|
||||||
</Sheet>
|
// </Sheet>
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Root open={open} onOpenChange={onOpenChange}>
|
<Root open={open} onOpenChange={onOpenChange}>
|
||||||
{/* TRIGGER */}
|
{/* TRIGGER */}
|
||||||
<Trigger asChild>{trigger}</Trigger>
|
<Trigger asChild>
|
||||||
|
<PressableTrigger press={press}>{trigger}</PressableTrigger>
|
||||||
|
</Trigger>
|
||||||
|
|
||||||
{/* CONTENT */}
|
{/* CONTENT */}
|
||||||
<Portal>
|
<Portal>
|
||||||
<Wrapper>
|
|
||||||
<Overlay
|
<Overlay
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
|
@ -70,19 +53,28 @@ const Dialog = (props: Props) => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{content}
|
{content}
|
||||||
</Wrapper>
|
|
||||||
</Portal>
|
</Portal>
|
||||||
</Root>
|
</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 {
|
interface DialogContentProps {
|
||||||
// title: string
|
|
||||||
// description?: string
|
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
// action: string
|
borderRadius: 8 | 12 | 16
|
||||||
// onAction: (close: VoidFunction) => void
|
width: number
|
||||||
// onCancel?: () => void
|
|
||||||
initialFocusRef?: React.RefObject<HTMLElement>
|
initialFocusRef?: React.RefObject<HTMLElement>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,25 +88,25 @@ const DialogContent = (props: DialogContentProps, ref: Ref<HTMLDivElement>) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const media = useMedia()
|
// const media = useMedia()
|
||||||
|
|
||||||
if (media.sm) {
|
// if (media.sm) {
|
||||||
return <Sheet.Content>{children}</Sheet.Content>
|
// return <Sheet.Content>{children}</Sheet.Content>
|
||||||
}
|
// }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Content
|
<Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onOpenAutoFocus={handleOpenAutoFocus}
|
onOpenAutoFocus={handleOpenAutoFocus}
|
||||||
|
// TODO: use tamagui components
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'white',
|
|
||||||
padding: 8,
|
|
||||||
width: 400,
|
|
||||||
borderRadius: 8,
|
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
top: '50%',
|
top: '50%',
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translate(-50%, -50%)',
|
transform: 'translate(-50%, -50%)',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
borderRadius: props.borderRadius,
|
||||||
|
width: props.width,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
@ -124,4 +116,4 @@ const DialogContent = (props: DialogContentProps, ref: Ref<HTMLDivElement>) => {
|
||||||
|
|
||||||
Dialog.Content = forwardRef(DialogContent)
|
Dialog.Content = forwardRef(DialogContent)
|
||||||
|
|
||||||
export { Dialog }
|
export { Close, Dialog }
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export { Dialog } from './dialog'
|
export { Close, Dialog } from './dialog'
|
||||||
|
|
|
@ -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
|
// More on how to set up stories at: https://storybook.js.org/docs/7.0/react/writing-stories/introduction
|
||||||
const meta: Meta<typeof DynamicButton> = {
|
const meta: Meta<typeof DynamicButton> = {
|
||||||
title: 'DynamicButton',
|
|
||||||
component: DynamicButton,
|
component: DynamicButton,
|
||||||
args: {},
|
args: {},
|
||||||
argTypes: {},
|
argTypes: {},
|
||||||
|
|
|
@ -6,10 +6,9 @@ import { Stack, styled } from '@tamagui/core'
|
||||||
import { Shadow } from '../shadow'
|
import { Shadow } from '../shadow'
|
||||||
import { Text } from '../text'
|
import { Text } from '../text'
|
||||||
|
|
||||||
import type { GetVariants } from '../types'
|
import type { GetVariants, PressableProps } from '../types'
|
||||||
import type { ColorTokens, StackProps } from '@tamagui/core'
|
import type { ColorTokens } from '@tamagui/core'
|
||||||
import type { Ref } from 'react'
|
import type { Ref } from 'react'
|
||||||
import type { PressableProps } from 'react-native'
|
|
||||||
|
|
||||||
type Variants = GetVariants<typeof Button>
|
type Variants = GetVariants<typeof Button>
|
||||||
|
|
||||||
|
@ -27,7 +26,7 @@ const DynamicButton = (props: Props, ref: Ref<HTMLButtonElement>) => {
|
||||||
return (
|
return (
|
||||||
<Shadow variant="$2" borderRadius={999}>
|
<Shadow variant="$2" borderRadius={999}>
|
||||||
<Button
|
<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}
|
ref={ref}
|
||||||
type={type}
|
type={type}
|
||||||
iconOnly={showCount === false}
|
iconOnly={showCount === false}
|
||||||
|
@ -51,8 +50,8 @@ export type { Props as DynamicButtonProps }
|
||||||
|
|
||||||
const Button = styled(Stack, {
|
const Button = styled(Stack, {
|
||||||
name: 'DynamicButton',
|
name: 'DynamicButton',
|
||||||
accessibilityRole: 'button',
|
|
||||||
tag: 'button',
|
tag: 'button',
|
||||||
|
accessibilityRole: 'button',
|
||||||
|
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
import type { PressableProps } from '../types'
|
import type { PressableProps } from '../types'
|
||||||
import type { ColorTokens } from 'tamagui'
|
import type { MouseEvent } from 'react-native'
|
||||||
|
import type { ColorTokens, TamaguiComponentPropsBase } from 'tamagui'
|
||||||
|
|
||||||
type Config = {
|
type Config = {
|
||||||
default: ColorTokens
|
default: ColorTokens
|
||||||
|
@ -12,8 +13,9 @@ type Config = {
|
||||||
|
|
||||||
type Return = {
|
type Return = {
|
||||||
color: ColorTokens
|
color: ColorTokens
|
||||||
|
// FIXME: use PressableProps instead TamaguiComponentPropsBase, fix necessary in Tamagui
|
||||||
pressableProps: Pick<
|
pressableProps: Pick<
|
||||||
PressableProps,
|
TamaguiComponentPropsBase,
|
||||||
'onHoverIn' | 'onHoverOut' | 'onPressIn' | 'onPressOut'
|
'onHoverIn' | 'onHoverOut' | 'onPressIn' | 'onPressOut'
|
||||||
>
|
>
|
||||||
}
|
}
|
||||||
|
@ -49,11 +51,11 @@ export const usePressableColors = (
|
||||||
color: styles[key],
|
color: styles[key],
|
||||||
pressableProps: {
|
pressableProps: {
|
||||||
onHoverIn: event => {
|
onHoverIn: event => {
|
||||||
props.onHoverIn?.(event)
|
props.onHoverIn?.(event as unknown as MouseEvent)
|
||||||
setHovered(true)
|
setHovered(true)
|
||||||
},
|
},
|
||||||
onHoverOut: event => {
|
onHoverOut: event => {
|
||||||
props.onHoverOut?.(event)
|
props.onHoverOut?.(event as unknown as MouseEvent)
|
||||||
setHovered(false)
|
setHovered(false)
|
||||||
},
|
},
|
||||||
onPressIn: event => {
|
onPressIn: event => {
|
||||||
|
|
|
@ -5,7 +5,6 @@ import { Stack, styled } from 'tamagui'
|
||||||
import { usePressableColors } from '../hooks/use-pressable-colors'
|
import { usePressableColors } from '../hooks/use-pressable-colors'
|
||||||
|
|
||||||
import type { GetVariants, PressableProps } from '../types'
|
import type { GetVariants, PressableProps } from '../types'
|
||||||
import type { StackProps } from '@tamagui/core'
|
|
||||||
import type { Ref } from 'react'
|
import type { Ref } from 'react'
|
||||||
|
|
||||||
type Variants = GetVariants<typeof Base>
|
type Variants = GetVariants<typeof Base>
|
||||||
|
@ -41,8 +40,8 @@ const IconButton = (props: Props, ref: Ref<HTMLButtonElement>) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Base
|
<Base
|
||||||
{...(buttonProps as unknown as StackProps)} // TODO: Tamagui has incorrect types for PressableProps
|
{...(buttonProps as unknown as object)} // TODO: Tamagui has incorrect types for PressableProps
|
||||||
{...(pressableProps as unknown as StackProps)} // TODO: Tamagui has incorrect types for PressableProps
|
{...pressableProps}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
variant={blur ? undefined : variant}
|
variant={blur ? undefined : variant}
|
||||||
active={blur ? undefined : selected ? variant : undefined}
|
active={blur ? undefined : selected ? variant : undefined}
|
||||||
|
|
|
@ -1,7 +1,15 @@
|
||||||
|
import {
|
||||||
|
AngryIcon,
|
||||||
|
LaughIcon,
|
||||||
|
LoveIcon,
|
||||||
|
SadIcon,
|
||||||
|
ThumbsDownIcon,
|
||||||
|
ThumbsUpIcon,
|
||||||
|
} from '@status-im/icons/reactions'
|
||||||
import { XStack } from 'tamagui'
|
import { XStack } from 'tamagui'
|
||||||
|
|
||||||
|
import { IconButton } from '../../icon-button'
|
||||||
import { Popover } from '../../popover'
|
import { Popover } from '../../popover'
|
||||||
import { ReactButton } from '../../react-button'
|
|
||||||
|
|
||||||
import type { PopoverProps } from '../../popover'
|
import type { PopoverProps } from '../../popover'
|
||||||
import type { ReactionsType } from '../types'
|
import type { ReactionsType } from '../types'
|
||||||
|
@ -12,6 +20,15 @@ type Props = Omit<PopoverProps, 'children'> & {
|
||||||
onOpenChange?: PopoverProps['onOpenChange']
|
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) => {
|
export const ReactionPopover = (props: Props) => {
|
||||||
const { children, reactions, onOpenChange, ...popoverProps } = props
|
const { children, reactions, onOpenChange, ...popoverProps } = props
|
||||||
|
|
||||||
|
@ -21,40 +38,34 @@ export const ReactionPopover = (props: Props) => {
|
||||||
|
|
||||||
<Popover.Content>
|
<Popover.Content>
|
||||||
<XStack space={2} padding={2}>
|
<XStack space={2} padding={2}>
|
||||||
<ReactButton
|
<IconButton
|
||||||
|
icon={REACTIONS_ICONS['love']}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size={32}
|
|
||||||
icon="love"
|
|
||||||
selected={reactions['love']?.has('me')}
|
selected={reactions['love']?.has('me')}
|
||||||
/>
|
/>
|
||||||
<ReactButton
|
<IconButton
|
||||||
|
icon={REACTIONS_ICONS['thumbs-up']}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size={32}
|
|
||||||
icon="thumbs-up"
|
|
||||||
selected={reactions['thumbs-up']?.has('me')}
|
selected={reactions['thumbs-up']?.has('me')}
|
||||||
/>
|
/>
|
||||||
<ReactButton
|
<IconButton
|
||||||
|
icon={REACTIONS_ICONS['thumbs-down']}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size={32}
|
|
||||||
icon="thumbs-down"
|
|
||||||
selected={reactions['thumbs-down']?.has('me')}
|
selected={reactions['thumbs-down']?.has('me')}
|
||||||
/>
|
/>
|
||||||
<ReactButton
|
<IconButton
|
||||||
|
icon={REACTIONS_ICONS['laugh']}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size={32}
|
|
||||||
icon="laugh"
|
|
||||||
selected={reactions.laugh?.has('me')}
|
selected={reactions.laugh?.has('me')}
|
||||||
/>
|
/>
|
||||||
<ReactButton
|
<IconButton
|
||||||
|
icon={REACTIONS_ICONS['sad']}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size={32}
|
|
||||||
icon="sad"
|
|
||||||
selected={reactions.sad?.has('me')}
|
selected={reactions.sad?.has('me')}
|
||||||
/>
|
/>
|
||||||
<ReactButton
|
<IconButton
|
||||||
|
icon={REACTIONS_ICONS['angry']}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size={32}
|
|
||||||
icon="angry"
|
|
||||||
selected={reactions.angry?.has('me')}
|
selected={reactions.angry?.has('me')}
|
||||||
/>
|
/>
|
||||||
</XStack>
|
</XStack>
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,12 +1,15 @@
|
||||||
import { XStack } from 'tamagui'
|
import { createElement, useState } from 'react'
|
||||||
|
|
||||||
|
import { Stack, XStack } from 'tamagui'
|
||||||
|
|
||||||
import { Dialog } from '../../dialog'
|
import { Dialog } from '../../dialog'
|
||||||
import { ReactButton } from '../../react-button'
|
import { ReactButton } from '../../react-button'
|
||||||
|
import { REACTIONS_ICONS } from '../../react-button/react-button'
|
||||||
|
import { Text } from '../../text'
|
||||||
import { Tooltip } from '../../tooltip/tooltip'
|
import { Tooltip } from '../../tooltip/tooltip'
|
||||||
import { UserList } from '../../user-list'
|
|
||||||
import { ReactionPopover } from './reaction-popover'
|
import { ReactionPopover } from './reaction-popover'
|
||||||
|
import { ReactionsDialog } from './reactions-dialog'
|
||||||
|
|
||||||
import type { ReactButtonProps } from '../../react-button'
|
|
||||||
import type { ReactionsType, ReactionType } from '../types'
|
import type { ReactionsType, ReactionType } from '../types'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -24,13 +27,11 @@ export const Reactions = (props: Props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<XStack space={6} flexWrap="wrap">
|
<XStack space={6} flexWrap="wrap">
|
||||||
{Object.entries(reactions).map(([type, value]) => (
|
{Object.keys(reactions).map(type => (
|
||||||
<Reaction
|
<Reaction
|
||||||
key={type}
|
key={type}
|
||||||
size="compact"
|
type={type as ReactionType}
|
||||||
icon={type as ReactionType}
|
reactions={reactions}
|
||||||
count={value.size}
|
|
||||||
selected={value.has('me')}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
@ -40,70 +41,67 @@ export const Reactions = (props: Props) => {
|
||||||
align="start"
|
align="start"
|
||||||
sideOffset={8}
|
sideOffset={8}
|
||||||
>
|
>
|
||||||
<ReactButton size="compact" icon="add" selected={false} />
|
<ReactButton type="add" />
|
||||||
</ReactionPopover>
|
</ReactionPopover>
|
||||||
</XStack>
|
</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 (
|
return (
|
||||||
<Dialog press="long">
|
<Dialog press="long" open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
side="bottom"
|
side="bottom"
|
||||||
sideOffset={4}
|
sideOffset={4}
|
||||||
content={
|
content={
|
||||||
<>
|
<Stack
|
||||||
You, Mr Gandalf, Ariana Perlona and
|
tag="button"
|
||||||
<button>3 more</button> reacted with {'[ICON]'}
|
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} />
|
<ReactButton
|
||||||
</Tooltip>
|
type={type as ReactionType}
|
||||||
|
count={value.size}
|
||||||
<Dialog.Content>
|
selected={value.has('me')}
|
||||||
<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>
|
</Tooltip>
|
||||||
|
<ReactionsDialog initialReactionType={type} reactions={reactions} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
import { useState } from 'react'
|
|
||||||
|
|
||||||
import { CloseIcon, PinIcon } from '@status-im/icons/20'
|
import { CloseIcon, PinIcon } from '@status-im/icons/20'
|
||||||
import { styled } from '@tamagui/core'
|
import { Stack } from '@tamagui/core'
|
||||||
import { Pressable, View } from 'react-native'
|
import { Pressable } from 'react-native'
|
||||||
|
|
||||||
import { Banner } from '../banner'
|
import { Banner } from '../banner'
|
||||||
import { Button } from '../button'
|
import { Button } from '../button'
|
||||||
import { ContextTag } from '../context-tag'
|
import { ContextTag } from '../context-tag'
|
||||||
import { Dialog } from '../dialog'
|
import { Close, Dialog } from '../dialog'
|
||||||
import { Message } from '../messages'
|
import { Message } from '../messages'
|
||||||
import { Text } from '../text'
|
import { Text } from '../text'
|
||||||
|
|
||||||
|
@ -19,24 +17,21 @@ type Props = {
|
||||||
|
|
||||||
const PinnedMessage = (props: Props) => {
|
const PinnedMessage = (props: Props) => {
|
||||||
const { messages } = props
|
const { messages } = props
|
||||||
const [isDetailVisible, setIsDetailVisible] = useState(false)
|
|
||||||
|
|
||||||
return messages.length > 0 ? (
|
return (
|
||||||
<Dialog open={isDetailVisible}>
|
<Dialog>
|
||||||
<Pressable onPress={() => setIsDetailVisible(true)}>
|
<Pressable>
|
||||||
<Banner count={messages.length} icon={<PinIcon />}>
|
<Banner count={messages.length} icon={<PinIcon />}>
|
||||||
{messages[0].text}
|
{messages[0].text}
|
||||||
</Banner>
|
</Banner>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
<Base>
|
<Dialog.Content width={480} borderRadius={16}>
|
||||||
<Button
|
<Stack padding={16} alignItems="flex-start">
|
||||||
variant="grey"
|
<Close asChild>
|
||||||
onPress={() => setIsDetailVisible(false)}
|
<Button variant="grey" size={32} icon={<CloseIcon />} />
|
||||||
size={32}
|
</Close>
|
||||||
icon={<CloseIcon />}
|
<Stack paddingTop={24} gap={8} alignItems="flex-start">
|
||||||
/>
|
|
||||||
<DialogHeader>
|
|
||||||
<Text size={27} weight="semibold">
|
<Text size={27} weight="semibold">
|
||||||
Pinned Messages
|
Pinned Messages
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -44,53 +39,17 @@ const PinnedMessage = (props: Props) => {
|
||||||
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"
|
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']}
|
label={['Rarible', '# random']}
|
||||||
/>
|
/>
|
||||||
</DialogHeader>
|
</Stack>
|
||||||
<DialogContent>
|
</Stack>
|
||||||
|
<Stack padding={8}>
|
||||||
{messages.map(message => (
|
{messages.map(message => (
|
||||||
<Message key={message.id} {...message} pinned={false} />
|
<Message key={message.id} {...message} pinned={false} />
|
||||||
))}
|
))}
|
||||||
</DialogContent>
|
</Stack>
|
||||||
</Base>
|
</Dialog.Content>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
) : null
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export { PinnedMessage }
|
export { PinnedMessage }
|
||||||
export type { Props as PinnedMessageProps }
|
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',
|
|
||||||
})
|
|
||||||
|
|
|
@ -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
|
// More on how to set up stories at: https://storybook.js.org/docs/7.0/react/writing-stories/introduction
|
||||||
const meta: Meta<typeof ReactButton> = {
|
const meta: Meta<typeof ReactButton> = {
|
||||||
title: 'ReactButton',
|
|
||||||
component: ReactButton,
|
component: ReactButton,
|
||||||
args: {},
|
args: {},
|
||||||
argTypes: {},
|
argTypes: {},
|
||||||
render: args => (
|
render: args => (
|
||||||
<XStack space={4}>
|
<XStack space={4}>
|
||||||
<ReactButton {...args} icon="laugh" />
|
<ReactButton {...args} type="laugh" />
|
||||||
<ReactButton {...args} icon="love" />
|
<ReactButton {...args} type="love" />
|
||||||
<ReactButton {...args} icon="sad" />
|
<ReactButton {...args} type="sad" />
|
||||||
<ReactButton {...args} icon="thumbs-up" />
|
<ReactButton {...args} type="thumbs-up" />
|
||||||
<ReactButton {...args} icon="thumbs-down" />
|
<ReactButton {...args} type="thumbs-down" />
|
||||||
<ReactButton {...args} icon="angry" />
|
<ReactButton {...args} type="angry" />
|
||||||
|
<ReactButton {...args} type="add" />
|
||||||
</XStack>
|
</XStack>
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
type Story = StoryObj<typeof ReactButton>
|
type Story = StoryObj<typeof ReactButton>
|
||||||
|
|
||||||
export const Outline: Story = {
|
export const Default: 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
|
export default meta
|
||||||
|
|
|
@ -9,18 +9,16 @@ import {
|
||||||
ThumbsDownIcon,
|
ThumbsDownIcon,
|
||||||
ThumbsUpIcon,
|
ThumbsUpIcon,
|
||||||
} from '@status-im/icons/reactions'
|
} 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 { Text } from '../text'
|
||||||
|
|
||||||
import type { GetVariants } from '../types'
|
import type { ReactionType } from '../messages/types'
|
||||||
import type { StackProps } from '@tamagui/core'
|
import type { PressableProps } from '../types'
|
||||||
import type { Ref } from 'react'
|
import type { Ref } from 'react'
|
||||||
import type { PressableProps } from 'react-native'
|
|
||||||
|
|
||||||
type Variants = GetVariants<typeof Button>
|
export const REACTIONS_ICONS = {
|
||||||
|
|
||||||
export const REACTIONS = {
|
|
||||||
love: LoveIcon,
|
love: LoveIcon,
|
||||||
laugh: LaughIcon,
|
laugh: LaughIcon,
|
||||||
'thumbs-up': ThumbsUpIcon,
|
'thumbs-up': ThumbsUpIcon,
|
||||||
|
@ -31,10 +29,7 @@ export const REACTIONS = {
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
type Props = PressableProps & {
|
type Props = PressableProps & {
|
||||||
icon: keyof typeof REACTIONS
|
type: ReactionType
|
||||||
variant?: Variants['variant']
|
|
||||||
size?: Variants['size']
|
|
||||||
// FIXME: use aria-selected
|
|
||||||
selected?: boolean
|
selected?: boolean
|
||||||
count?: number
|
count?: number
|
||||||
// FIXME: update to latest RN
|
// FIXME: update to latest RN
|
||||||
|
@ -43,25 +38,17 @@ type Props = PressableProps & {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReactButton = (props: Props, ref: Ref<HTMLButtonElement>) => {
|
const ReactButton = (props: Props, ref: Ref<HTMLButtonElement>) => {
|
||||||
const {
|
const { type, count, ...pressableProps } = props
|
||||||
icon,
|
|
||||||
variant = 'outline',
|
|
||||||
size = 40,
|
|
||||||
count,
|
|
||||||
...pressableProps
|
|
||||||
} = props
|
|
||||||
|
|
||||||
const Icon = REACTIONS[icon]
|
const Icon = REACTIONS_ICONS[type]
|
||||||
|
|
||||||
const selected =
|
const selected =
|
||||||
props.selected || props['aria-expanded'] || props['aria-selected']
|
props.selected || props['aria-expanded'] || props['aria-selected']
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
{...(pressableProps as unknown as StackProps)} // TODO: Tamagui has incorrect types for PressableProps
|
{...(pressableProps as unknown as object)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
variant={variant}
|
|
||||||
size={size}
|
|
||||||
selected={selected}
|
selected={selected}
|
||||||
>
|
>
|
||||||
<Icon color="$neutral-100" />
|
<Icon color="$neutral-100" />
|
||||||
|
@ -81,6 +68,7 @@ export type { Props as ReactButtonProps }
|
||||||
|
|
||||||
const Button = styled(Stack, {
|
const Button = styled(Stack, {
|
||||||
name: 'ReactButton',
|
name: 'ReactButton',
|
||||||
|
tag: 'button',
|
||||||
accessibilityRole: 'button',
|
accessibilityRole: 'button',
|
||||||
|
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
|
@ -93,50 +81,24 @@ const Button = styled(Stack, {
|
||||||
animation: 'fast',
|
animation: 'fast',
|
||||||
space: 4,
|
space: 4,
|
||||||
|
|
||||||
variants: {
|
borderRadius: 8,
|
||||||
variant: {
|
minWidth: 36,
|
||||||
outline: {
|
height: 24,
|
||||||
borderColor: '$neutral-10',
|
paddingHorizontal: 8,
|
||||||
|
|
||||||
|
borderColor: '$neutral-20',
|
||||||
hoverStyle: { borderColor: '$neutral-30' },
|
hoverStyle: { borderColor: '$neutral-30' },
|
||||||
pressStyle: {
|
pressStyle: {
|
||||||
backgroundColor: '$neutral-10',
|
backgroundColor: '$neutral-10',
|
||||||
borderColor: '$neutral-20',
|
borderColor: '$neutral-20',
|
||||||
},
|
},
|
||||||
},
|
|
||||||
|
|
||||||
ghost: {
|
|
||||||
borderColor: 'transparent',
|
|
||||||
hoverStyle: { backgroundColor: '$neutral-10' },
|
|
||||||
pressStyle: { backgroundColor: '$neutral-20' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
|
variants: {
|
||||||
selected: {
|
selected: {
|
||||||
true: {
|
true: {
|
||||||
backgroundColor: '$neutral-10',
|
backgroundColor: '$neutral-10',
|
||||||
borderColor: '$neutral-30',
|
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,
|
} as const,
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,11 +3,11 @@ import { Stack } from '@tamagui/core'
|
||||||
import { DividerLabel } from '../dividers'
|
import { DividerLabel } from '../dividers'
|
||||||
import { UserList } from '../user-list'
|
import { UserList } from '../user-list'
|
||||||
|
|
||||||
import type { USerListProps } from '../user-list'
|
import type { UserListProps } from '../user-list'
|
||||||
|
|
||||||
type GroupProps = {
|
type GroupProps = {
|
||||||
label: string
|
label: string
|
||||||
users: USerListProps['users']
|
users: UserListProps['users']
|
||||||
}
|
}
|
||||||
|
|
||||||
const Group = (props: GroupProps) => {
|
const Group = (props: GroupProps) => {
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export { Tabs, type TabsProps } from './tabs'
|
|
@ -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
|
|
@ -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,
|
||||||
|
}
|
|
@ -5,7 +5,8 @@ import { Text } from './text'
|
||||||
import type { Meta, StoryObj } from '@storybook/react'
|
import type { Meta, StoryObj } from '@storybook/react'
|
||||||
|
|
||||||
const meta: Meta = {
|
const meta: Meta = {
|
||||||
title: 'Text',
|
title: 'text',
|
||||||
|
|
||||||
args: {
|
args: {
|
||||||
children: 'The quick brown fox jumped over the lazy dog.',
|
children: 'The quick brown fox jumped over the lazy dog.',
|
||||||
},
|
},
|
||||||
|
@ -19,8 +20,7 @@ const meta: Meta = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TextStory: StoryObj<typeof Text> = {
|
export const Default: StoryObj<typeof Text> = {
|
||||||
name: 'TextNew',
|
|
||||||
render: args => (
|
render: args => (
|
||||||
<Stack gap={24}>
|
<Stack gap={24}>
|
||||||
<Stack gap={8}>
|
<Stack gap={8}>
|
||||||
|
|
|
@ -8,8 +8,9 @@ import {
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
Trigger,
|
Trigger,
|
||||||
} from '@radix-ui/react-tooltip'
|
} from '@radix-ui/react-tooltip'
|
||||||
import { Stack } from 'tamagui'
|
import { useTheme } from 'tamagui'
|
||||||
|
|
||||||
|
import { Shadow } from '../shadow'
|
||||||
import { Text } from '../text'
|
import { Text } from '../text'
|
||||||
|
|
||||||
import type { TooltipContentProps } from '@radix-ui/react-tooltip'
|
import type { TooltipContentProps } from '@radix-ui/react-tooltip'
|
||||||
|
@ -37,6 +38,8 @@ const Tooltip = (props: Props, ref: Ref<HTMLButtonElement>) => {
|
||||||
...triggerProps
|
...triggerProps
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
|
const theme = useTheme() // not ideal
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Root delayDuration={delayDuration}>
|
<Root delayDuration={delayDuration}>
|
||||||
|
@ -52,24 +55,22 @@ const Tooltip = (props: Props, ref: Ref<HTMLButtonElement>) => {
|
||||||
align={align}
|
align={align}
|
||||||
alignOffset={alignOffset}
|
alignOffset={alignOffset}
|
||||||
>
|
>
|
||||||
<Stack
|
<Shadow
|
||||||
backgroundColor="$neutral-95"
|
variant="$3"
|
||||||
|
backgroundColor="$white-100"
|
||||||
paddingVertical={6}
|
paddingVertical={6}
|
||||||
paddingHorizontal={12}
|
paddingHorizontal={12}
|
||||||
borderRadius={8}
|
borderRadius={8}
|
||||||
shadowRadius={30}
|
|
||||||
shadowOffset="0px 8px"
|
|
||||||
shadowColor="rgba(9, 16, 28, 0.12)"
|
|
||||||
>
|
>
|
||||||
{typeof content === 'string' ? (
|
{typeof content === 'string' ? (
|
||||||
<Text size={13} weight="medium" color="$white-100">
|
<Text size={13} weight="medium" color="$neutral-100">
|
||||||
{content}
|
{content}
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
content
|
content
|
||||||
)}
|
)}
|
||||||
<Arrow width={11} height={5} />
|
<Arrow width={11} height={5} fill={theme.background.val} />
|
||||||
</Stack>
|
</Shadow>
|
||||||
</Content>
|
</Content>
|
||||||
</Portal>
|
</Portal>
|
||||||
</Root>
|
</Root>
|
||||||
|
|
|
@ -17,7 +17,7 @@ type PressableProps = {
|
||||||
delayHoverIn?: NativePressableProps['delayHoverIn']
|
delayHoverIn?: NativePressableProps['delayHoverIn']
|
||||||
delayHoverOut?: NativePressableProps['delayHoverOut']
|
delayHoverOut?: NativePressableProps['delayHoverOut']
|
||||||
delayLongPress?: NativePressableProps['delayLongPress']
|
delayLongPress?: NativePressableProps['delayLongPress']
|
||||||
disabled?: NativePressableProps['disabled']
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MapVariant<
|
export type MapVariant<
|
||||||
|
|
|
@ -49,4 +49,4 @@ const UserList = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export { UserList }
|
export { UserList }
|
||||||
export type { Props as USerListProps }
|
export type { Props as UserListProps }
|
||||||
|
|
15
yarn.lock
15
yarn.lock
|
@ -3541,6 +3541,21 @@
|
||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
"@radix-ui/react-compose-refs" "1.0.0"
|
"@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":
|
"@radix-ui/react-tooltip@^1.0.5":
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.0.5.tgz#fe20274aeac874db643717fc7761d5a8abdd62d1"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.0.5.tgz#fe20274aeac874db643717fc7761d5a8abdd62d1"
|
||||||
|
|
Loading…
Reference in New Issue