Add skeleton and loading states (#366)
* feat: add skeleton placeholder components and stories * feat: adds gap messages component and stories * feat: add information box component and stories (WIP) * feat: add dismiss prop and more stories * fix: changes onDismiss existing function to onClose * feat: add sidebar skeleton loader * feat: makes the banner component more flexible * update information box * fix: changes from review * feat: add topbar-skeleton component * Fix Skeleton typo --------- Co-authored-by: Pavel Prichodko <14926950+prichodko@users.noreply.github.com>
This commit is contained in:
parent
fc580590ab
commit
91fe20549c
|
@ -77,3 +77,15 @@ body,
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes gradient {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
/* Animation for skeleton placeholder */
|
||||
@keyframes gradient {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
import React from 'react'
|
||||
import { Provider, ToastContainer } from '../src'
|
||||
import { Parameters, Decorator } from '@storybook/react'
|
||||
|
||||
import './reset.css'
|
||||
import './components.css'
|
||||
|
||||
export const parameters: Parameters = {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { PinIcon } from '@status-im/icons/20'
|
||||
import { AlertIcon, PinIcon, RecentIcon } from '@status-im/icons/20'
|
||||
import { Stack } from '@tamagui/core'
|
||||
|
||||
import { Banner } from './banner'
|
||||
|
@ -38,6 +38,22 @@ export const NoCount: Story = {
|
|||
},
|
||||
}
|
||||
|
||||
export const NetworkStateConnecting: Story = {
|
||||
args: {
|
||||
backgroundColor: '$neutral-80-opa-5',
|
||||
icon: <RecentIcon />,
|
||||
children: 'Connecting...',
|
||||
},
|
||||
}
|
||||
|
||||
export const NetworkStateError: Story = {
|
||||
args: {
|
||||
backgroundColor: '$danger-50-opa-20',
|
||||
icon: <AlertIcon />,
|
||||
children: 'Network is down',
|
||||
},
|
||||
}
|
||||
|
||||
export const AllVariants: Story = {
|
||||
args: {},
|
||||
render: () => (
|
||||
|
@ -46,6 +62,12 @@ export const AllVariants: Story = {
|
|||
Banner message
|
||||
</Banner>
|
||||
<Banner count={5}>Banner message</Banner>
|
||||
<Banner backgroundColor="$neutral-80-opa-5" icon={<RecentIcon />}>
|
||||
Connecting...
|
||||
</Banner>
|
||||
<Banner backgroundColor="$danger-50-opa-20" icon={<AlertIcon />}>
|
||||
Network is down
|
||||
</Banner>
|
||||
<Banner icon={<PinIcon />}>Banner message</Banner>
|
||||
</Stack>
|
||||
),
|
||||
|
|
|
@ -4,17 +4,25 @@ import { View } from 'react-native'
|
|||
import { Counter } from '../counter'
|
||||
import { Text } from '../text'
|
||||
|
||||
import type { ColorTokens } from '@tamagui/core'
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
icon?: React.ReactNode
|
||||
count?: number
|
||||
backgroundColor?: ColorTokens
|
||||
}
|
||||
|
||||
const Banner = (props: Props) => {
|
||||
const { icon, children, count } = props
|
||||
const {
|
||||
icon,
|
||||
children,
|
||||
count,
|
||||
backgroundColor = '$primary-50-opa-20',
|
||||
} = props
|
||||
|
||||
return (
|
||||
<Base>
|
||||
<Base backgroundColor={backgroundColor}>
|
||||
<Content>
|
||||
{icon}
|
||||
<Text size={13} color="$textPrimary">
|
||||
|
@ -30,7 +38,6 @@ export { Banner }
|
|||
export type { Props as BannerProps }
|
||||
|
||||
const Base = styled(View, {
|
||||
backgroundColor: '$primary-50-opa-20',
|
||||
padding: 12,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
|
|
@ -55,7 +55,7 @@ const ContextTag = (props: Props) => {
|
|||
src,
|
||||
icon,
|
||||
label,
|
||||
type = 'default',
|
||||
// type = 'default', // this is commented because it's not being used
|
||||
size = 24,
|
||||
blur = false,
|
||||
outline,
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import { GapMessages } from './gap-messages'
|
||||
|
||||
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 GapMessages> = {
|
||||
title: 'gap-messages',
|
||||
component: GapMessages,
|
||||
argTypes: {},
|
||||
parameters: {
|
||||
design: {
|
||||
type: 'figma',
|
||||
url: 'https://www.figma.com/file/IBmFKgGL1B4GzqD8LQTw6n/Design-System-for-Desktop%2FWeb?node-id=5187-181408&t=5dgANDld90Qfd00V-0',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type Story = StoryObj<typeof GapMessages>
|
||||
|
||||
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
message: 'This is a simple message.',
|
||||
startDate: 'Jan 8 · 09:12',
|
||||
endDate: 'Mar 8 · 22:42',
|
||||
tooltipMessage: 'This is some tooltip message.',
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
|
@ -0,0 +1,110 @@
|
|||
import { InfoIcon } from '@status-im/icons/16'
|
||||
import { Stack, styled } from '@tamagui/core'
|
||||
|
||||
import { Text } from '../text'
|
||||
import { Tooltip } from '../tooltip'
|
||||
|
||||
const NUM_CIRCLES = 200
|
||||
|
||||
type Props = {
|
||||
startDate: string
|
||||
endDate: string
|
||||
message: string
|
||||
tooltipMessage: string
|
||||
}
|
||||
|
||||
// TODO try to find a solution for the inset shadow
|
||||
const GapMessages = (props: Props) => {
|
||||
const { startDate, endDate, message, tooltipMessage } = props
|
||||
|
||||
return (
|
||||
<Stack backgroundColor="$neutral-5" width="100%">
|
||||
<Stack>
|
||||
<Stack flexDirection="row" width="100%" overflow="hidden" mt={-4}>
|
||||
<Circles />
|
||||
</Stack>
|
||||
<Stack py={20} flexDirection="row">
|
||||
<Stack
|
||||
height="auto"
|
||||
minHeight="100%"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
py={5}
|
||||
pr={16}
|
||||
pl={28}
|
||||
>
|
||||
<EmptyCircle />
|
||||
<Divider flex={1} />
|
||||
<EmptyCircle />
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Text size={11} color="$neutral-50">
|
||||
{startDate}
|
||||
</Text>
|
||||
<Stack py={16}>
|
||||
<Text size={15} truncate>
|
||||
{message}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Text size={11} color="$neutral-50">
|
||||
{endDate}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack position="absolute" right={26} top={20}>
|
||||
<Tooltip side="bottom" sideOffset={4} content={<>{tooltipMessage}</>}>
|
||||
<Stack width={20}>
|
||||
<InfoIcon color="$neutral-50" />
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
<Stack flexDirection="row" width="100%" overflow="hidden" mb={-4}>
|
||||
<Circles />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
export { GapMessages }
|
||||
export type { Props as GapMessageProps }
|
||||
|
||||
// TODO try to find a responsive solution if we need to keep the circles in the future
|
||||
const Circles = () => {
|
||||
return (
|
||||
<>
|
||||
{[...Array(NUM_CIRCLES)].map((_, i) => (
|
||||
<Circle key={i} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Circle = styled(Stack, {
|
||||
name: 'Circle',
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: '$neutral-5',
|
||||
marginRight: 7,
|
||||
})
|
||||
|
||||
const EmptyCircle = styled(Stack, {
|
||||
name: 'EmptyCircle',
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: 'transparent',
|
||||
borderWidth: 1,
|
||||
borderColor: '$neutral-40',
|
||||
})
|
||||
|
||||
const Divider = styled(Stack, {
|
||||
name: 'Divider',
|
||||
backgroundColor: '$neutral-40',
|
||||
borderWidth: 1,
|
||||
borderColor: '$neutral-5',
|
||||
borderStyle: 'dashed',
|
||||
width: 1,
|
||||
height: 'auto',
|
||||
})
|
|
@ -0,0 +1 @@
|
|||
export { GapMessages } from './gap-messages'
|
|
@ -3,14 +3,17 @@ export * from './button'
|
|||
export * from './composer'
|
||||
export * from './dividers'
|
||||
export * from './dynamic-button'
|
||||
export * from './gap-messages'
|
||||
export * from './icon-button'
|
||||
export * from './image'
|
||||
export * from './information-box'
|
||||
export * from './input'
|
||||
export * from './messages'
|
||||
export * from './pinned-message'
|
||||
export * from './provider'
|
||||
export * from './sidebar'
|
||||
export * from './sidebar-members'
|
||||
export * from './skeleton'
|
||||
export * from './text'
|
||||
export * from './toast'
|
||||
export * from './topbar'
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export { InformationBox } from './information-box'
|
|
@ -0,0 +1,167 @@
|
|||
import { InfoIcon } from '@status-im/icons/16'
|
||||
|
||||
import { InformationBox } from './information-box'
|
||||
|
||||
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 InformationBox> = {
|
||||
title: 'information-box',
|
||||
component: InformationBox,
|
||||
argTypes: {},
|
||||
parameters: {
|
||||
design: {
|
||||
type: 'figma',
|
||||
url: 'https://www.figma.com/file/IBmFKgGL1B4GzqD8LQTw6n/Design-System-for-Desktop%2FWeb?node-id=5187-181408&t=5dgANDld90Qfd00V-0',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type Story = StoryObj<typeof InformationBox>
|
||||
|
||||
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
message: 'This is a simple message.',
|
||||
},
|
||||
}
|
||||
|
||||
export const Information: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
variant: 'information',
|
||||
},
|
||||
}
|
||||
|
||||
export const Error: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
variant: 'error',
|
||||
},
|
||||
}
|
||||
|
||||
export const DefaultWithIcon: Story = {
|
||||
args: {
|
||||
message: 'This is a simple message with an info icon.',
|
||||
icon: <InfoIcon />,
|
||||
},
|
||||
}
|
||||
|
||||
export const InformationWithIcon: Story = {
|
||||
args: {
|
||||
...DefaultWithIcon.args,
|
||||
variant: 'information',
|
||||
},
|
||||
}
|
||||
|
||||
export const ErrorWithIcon: Story = {
|
||||
args: {
|
||||
...DefaultWithIcon.args,
|
||||
variant: 'error',
|
||||
},
|
||||
}
|
||||
|
||||
export const WithMaxWidth: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
},
|
||||
}
|
||||
|
||||
export const WithIconAndTwoLines: Story = {
|
||||
args: {
|
||||
...DefaultWithIcon.args,
|
||||
message: 'This is a message with an icon and two lines.',
|
||||
},
|
||||
}
|
||||
|
||||
export const WithButtonAndIconDefault: Story = {
|
||||
args: {
|
||||
...DefaultWithIcon.args,
|
||||
message: 'This is a message with an icon and a button.',
|
||||
buttonText: 'Button',
|
||||
onButtonPress: () => alert('clicked'),
|
||||
},
|
||||
}
|
||||
|
||||
export const WithButtonAndIconInformation: Story = {
|
||||
args: {
|
||||
...WithButtonAndIconDefault.args,
|
||||
variant: 'information',
|
||||
},
|
||||
}
|
||||
|
||||
export const WithButtonAndIconError: Story = {
|
||||
args: {
|
||||
...WithButtonAndIconDefault.args,
|
||||
variant: 'error',
|
||||
},
|
||||
}
|
||||
|
||||
export const DefaultWithDismiss: Story = {
|
||||
args: {
|
||||
message: 'This is a simple message.',
|
||||
onClosePress: () => alert('dismissed'),
|
||||
},
|
||||
}
|
||||
|
||||
export const InformationWithDismiss: Story = {
|
||||
args: {
|
||||
...DefaultWithDismiss.args,
|
||||
variant: 'information',
|
||||
},
|
||||
}
|
||||
|
||||
export const ErrorWithDismiss: Story = {
|
||||
args: {
|
||||
...DefaultWithDismiss.args,
|
||||
variant: 'error',
|
||||
},
|
||||
}
|
||||
|
||||
export const DefaultWithIconAndDismiss: Story = {
|
||||
args: {
|
||||
message: 'This is a simple message with an info icon.',
|
||||
icon: <InfoIcon />,
|
||||
onClosePress: () => alert('dismissed'),
|
||||
},
|
||||
}
|
||||
|
||||
export const InformationWithIconAndDismiss: Story = {
|
||||
args: {
|
||||
...DefaultWithIconAndDismiss.args,
|
||||
variant: 'information',
|
||||
},
|
||||
}
|
||||
|
||||
export const ErrorWithIconAndDismiss: Story = {
|
||||
args: {
|
||||
...DefaultWithIconAndDismiss.args,
|
||||
variant: 'error',
|
||||
},
|
||||
}
|
||||
|
||||
export const WithButtonAndIconAndDismiss: Story = {
|
||||
args: {
|
||||
...WithButtonAndIconDefault.args,
|
||||
message: 'This is a message with an icon and a button.',
|
||||
buttonText: 'Button',
|
||||
onButtonPress: () => alert('clicked'),
|
||||
onClosePress: () => alert('dismissed'),
|
||||
},
|
||||
}
|
||||
|
||||
export const WithButtonAndIconAndDismissInformation: Story = {
|
||||
args: {
|
||||
...WithButtonAndIconAndDismiss.args,
|
||||
variant: 'information',
|
||||
},
|
||||
}
|
||||
|
||||
export const WithButtonAndIconAndDismissError: Story = {
|
||||
args: {
|
||||
...WithButtonAndIconAndDismiss.args,
|
||||
variant: 'error',
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
|
@ -0,0 +1,134 @@
|
|||
import { cloneElement } from 'react'
|
||||
|
||||
import { CloseIcon } from '@status-im/icons/12'
|
||||
import { Stack, styled } from '@tamagui/core'
|
||||
|
||||
import { Button } from '../button'
|
||||
import { Text } from '../text'
|
||||
|
||||
import type { GetVariants, MapColorToken } from '../types'
|
||||
|
||||
type Variants = GetVariants<typeof Base>
|
||||
|
||||
type Props = {
|
||||
message: string
|
||||
variant?: Variants['variant']
|
||||
icon?: React.ReactElement
|
||||
buttonText?: string
|
||||
onButtonPress?: () => void
|
||||
onClosePress?: () => void
|
||||
}
|
||||
|
||||
type Variant = Props['variant']
|
||||
|
||||
const textColors: MapColorToken<Variant> = {
|
||||
default: '$neutral-100',
|
||||
information: '$neutral-100',
|
||||
error: '$danger-50',
|
||||
}
|
||||
|
||||
const iconColors: MapColorToken<Variant> = {
|
||||
default: '$neutral-50',
|
||||
information: '$neutral-50',
|
||||
error: '$danger-50',
|
||||
}
|
||||
|
||||
const buttonVariants: Record<NonNullable<Variant>, 'primary' | 'danger'> = {
|
||||
default: 'primary',
|
||||
information: 'primary',
|
||||
error: 'danger',
|
||||
}
|
||||
|
||||
const InformationBox = (props: Props) => {
|
||||
const {
|
||||
message,
|
||||
variant = 'default',
|
||||
icon,
|
||||
buttonText,
|
||||
onButtonPress,
|
||||
onClosePress,
|
||||
} = props
|
||||
|
||||
const textColor = textColors[variant]
|
||||
const iconColor = iconColors[variant]
|
||||
const buttonVariant = buttonVariants[variant]
|
||||
|
||||
return (
|
||||
<Base variant={variant} {...props}>
|
||||
<Stack
|
||||
flexDirection="row"
|
||||
alignItems="center"
|
||||
justifyContent="flex-start"
|
||||
width="100%"
|
||||
>
|
||||
{icon ? (
|
||||
<Stack pr={8} pt={1} alignSelf="flex-start">
|
||||
{cloneElement(icon, { color: iconColor })}
|
||||
</Stack>
|
||||
) : null}
|
||||
<Stack flexShrink={1} width="100%">
|
||||
<Text size={13} color={textColor}>
|
||||
{message}
|
||||
</Text>
|
||||
{buttonText ? (
|
||||
<Stack pt={8} width="fit-content">
|
||||
<Button
|
||||
onPress={() => onButtonPress?.()}
|
||||
size={24}
|
||||
variant={buttonVariant}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
</Stack>
|
||||
) : null}
|
||||
</Stack>
|
||||
{onClosePress ? (
|
||||
<Stack
|
||||
pl={8}
|
||||
pt={4}
|
||||
onPress={() => onClosePress()}
|
||||
cursor="pointer"
|
||||
alignSelf="flex-start"
|
||||
>
|
||||
<CloseIcon />
|
||||
</Stack>
|
||||
) : null}
|
||||
</Stack>
|
||||
</Base>
|
||||
)
|
||||
}
|
||||
|
||||
export { InformationBox }
|
||||
export type { Props as InformationBoxProps }
|
||||
|
||||
const Base = styled(Stack, {
|
||||
name: 'InformationBox',
|
||||
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'center',
|
||||
|
||||
userSelect: 'none',
|
||||
borderWidth: 1,
|
||||
|
||||
py: 11,
|
||||
px: 16,
|
||||
borderRadius: 12,
|
||||
|
||||
variants: {
|
||||
variant: {
|
||||
default: {
|
||||
backgroundColor: '$white-100',
|
||||
borderColor: '$neutral-20',
|
||||
},
|
||||
information: {
|
||||
backgroundColor: '$blue-50-opa-5',
|
||||
borderColor: '$blue-50-opa-10',
|
||||
},
|
||||
error: {
|
||||
backgroundColor: '$danger-50-opa-5',
|
||||
borderColor: '$danger-50-opa-10',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
|
@ -1,7 +1,19 @@
|
|||
import { Stack } from '@tamagui/core'
|
||||
|
||||
import { CHANNEL_GROUPS } from './mock-data'
|
||||
import { Sidebar } from './sidebar'
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
import type { SidebarProps } from './sidebar'
|
||||
import type { Meta } from '@storybook/react'
|
||||
|
||||
const COMMUNITY = {
|
||||
name: 'Rarible',
|
||||
description:
|
||||
'Multichain community-centric NFT marketplace. Create, buy and sell your NFTs.',
|
||||
membersCount: 123,
|
||||
imageUrl:
|
||||
'https://images.unsplash.com/photo-1574786527860-f2e274867c91?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1764&q=80',
|
||||
}
|
||||
|
||||
// More on how to set up stories at: https://storybook.js.org/docs/7.0/react/writing-stories/introduction
|
||||
const meta: Meta<typeof Sidebar> = {
|
||||
|
@ -9,6 +21,7 @@ const meta: Meta<typeof Sidebar> = {
|
|||
component: Sidebar,
|
||||
args: {
|
||||
channels: CHANNEL_GROUPS,
|
||||
community: COMMUNITY,
|
||||
},
|
||||
argTypes: {},
|
||||
parameters: {
|
||||
|
@ -19,11 +32,20 @@ const meta: Meta<typeof Sidebar> = {
|
|||
},
|
||||
}
|
||||
|
||||
type Story = StoryObj<typeof Sidebar>
|
||||
export const Default = {
|
||||
render: (args: SidebarProps) => (
|
||||
<Stack width={352} height="100vh">
|
||||
<Sidebar {...args} />
|
||||
</Stack>
|
||||
),
|
||||
}
|
||||
|
||||
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
export const LoadingSidebar = {
|
||||
render: (args: SidebarProps) => (
|
||||
<Stack width={352} height="100vh">
|
||||
<Sidebar {...args} isLoading />
|
||||
</Stack>
|
||||
),
|
||||
}
|
||||
|
||||
export default meta
|
||||
|
|
|
@ -6,12 +6,13 @@ import { AccordionItem } from '../accordion/accordionItem'
|
|||
import { Avatar } from '../avatar'
|
||||
import { Button } from '../button'
|
||||
import { Image } from '../image'
|
||||
import { SidebarSkeleton } from '../skeleton/sidebar-skeleton'
|
||||
import { Text } from '../text'
|
||||
import { CHANNEL_GROUPS } from './mock-data'
|
||||
|
||||
import type { ChannelGroup } from './mock-data'
|
||||
|
||||
type Props = {
|
||||
export type SidebarProps = {
|
||||
community: {
|
||||
name: string
|
||||
description: string
|
||||
|
@ -21,18 +22,24 @@ type Props = {
|
|||
channels?: ChannelGroup[]
|
||||
selectedChannelId?: string
|
||||
onChannelPress: (channelId: string) => void
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
const Sidebar = (props: Props) => {
|
||||
const Sidebar = (props: SidebarProps) => {
|
||||
const {
|
||||
community,
|
||||
channels = CHANNEL_GROUPS,
|
||||
selectedChannelId,
|
||||
onChannelPress,
|
||||
isLoading,
|
||||
} = props
|
||||
|
||||
const { name, description, membersCount, imageUrl } = community
|
||||
|
||||
if (isLoading) {
|
||||
return <SidebarSkeleton />
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
backgroundColor="$background"
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
export * from './message-skeleton'
|
||||
export * from './skeleton'
|
|
@ -0,0 +1,43 @@
|
|||
import { MessageSkeleton } from './message-skeleton'
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
const meta: Meta<typeof MessageSkeleton> = {
|
||||
component: MessageSkeleton,
|
||||
|
||||
parameters: {
|
||||
design: {
|
||||
type: 'figma',
|
||||
url: 'https://www.figma.com/file/1RN1MFwfSqA6jNFJBeNdEu/Posts-%26-Attachments-for-Web?t=1Xf5496ymHeazodw-0',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type Story = StoryObj<typeof MessageSkeleton>
|
||||
|
||||
export const MessageSkeletonSmallest: Story = {
|
||||
name: 'Smallest',
|
||||
args: {
|
||||
size: 'smallest',
|
||||
},
|
||||
}
|
||||
export const MessageSkeletonSmall: Story = {
|
||||
name: 'Small',
|
||||
args: {
|
||||
size: 'small',
|
||||
},
|
||||
}
|
||||
export const MessageSkeletonMedium: Story = {
|
||||
name: 'Medium',
|
||||
args: {
|
||||
size: 'medium',
|
||||
},
|
||||
}
|
||||
export const MessageSkeletonLarge: Story = {
|
||||
name: 'Large',
|
||||
args: {
|
||||
size: 'large',
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
|
@ -0,0 +1,52 @@
|
|||
import { Stack } from '@tamagui/core'
|
||||
|
||||
import { Skeleton } from './skeleton'
|
||||
|
||||
import type { StackProps } from '@tamagui/core'
|
||||
|
||||
type SizeVariant = 'smallest' | 'small' | 'medium' | 'large'
|
||||
|
||||
type Props = Omit<StackProps, 'size'> & {
|
||||
size?: SizeVariant
|
||||
}
|
||||
|
||||
const skeletonTopSize = {
|
||||
smallest: 80,
|
||||
small: 96,
|
||||
medium: 112,
|
||||
large: 124,
|
||||
}
|
||||
|
||||
const skeletonBottomSizes = {
|
||||
smallest: 144,
|
||||
small: 156,
|
||||
medium: 212,
|
||||
large: 249,
|
||||
}
|
||||
|
||||
const MessageSkeleton = (props: Props) => {
|
||||
const { size, ...rest } = props
|
||||
return (
|
||||
<Stack flexDirection="row" p={8} width="100%" {...rest}>
|
||||
{/* Avatar */}
|
||||
<Skeleton />
|
||||
<Stack flex={1} ml={8}>
|
||||
{/* Text placeholders */}
|
||||
<Skeleton
|
||||
width={skeletonTopSize[size || 'medium']}
|
||||
br={6}
|
||||
height={8}
|
||||
mb={8}
|
||||
/>
|
||||
<Skeleton
|
||||
width={skeletonBottomSizes[size || 'medium']}
|
||||
br={6}
|
||||
height={16}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
export { MessageSkeleton }
|
||||
export type { Props as MessageSkeletonProps }
|
|
@ -0,0 +1,152 @@
|
|||
import { Stack } from '@tamagui/core'
|
||||
|
||||
import { Skeleton } from './skeleton'
|
||||
|
||||
const SidebarSkeleton = () => {
|
||||
// Eventually we can in the future abstract some of these components to be reusable if we need to
|
||||
return (
|
||||
<Stack
|
||||
backgroundColor="$background"
|
||||
borderRightWidth={1}
|
||||
borderColor="$neutral-10"
|
||||
height="100%"
|
||||
overflow="scroll"
|
||||
>
|
||||
<Stack height={135} width="100%" backgroundColor="$neutral-10" />
|
||||
<Stack
|
||||
paddingBottom={16}
|
||||
marginTop={-16}
|
||||
backgroundColor="$background"
|
||||
borderTopLeftRadius={20}
|
||||
borderTopRightRadius={20}
|
||||
zIndex={10}
|
||||
>
|
||||
<Stack paddingHorizontal={16} paddingBottom={16}>
|
||||
<Stack marginTop={-40} marginBottom={12}>
|
||||
<Skeleton
|
||||
height={80}
|
||||
width={80}
|
||||
borderRadius={40}
|
||||
borderWidth={2}
|
||||
borderColor="$white-100"
|
||||
variant="secondary"
|
||||
/>
|
||||
</Stack>
|
||||
<Skeleton
|
||||
height={24}
|
||||
width={104}
|
||||
borderRadius={8}
|
||||
mb={14}
|
||||
variant="secondary"
|
||||
/>
|
||||
<Skeleton
|
||||
height={16}
|
||||
width={312}
|
||||
borderRadius={8}
|
||||
mb={8}
|
||||
variant="secondary"
|
||||
/>
|
||||
<Skeleton
|
||||
height={16}
|
||||
width={272}
|
||||
borderRadius={8}
|
||||
mb={12}
|
||||
variant="secondary"
|
||||
/>
|
||||
<Stack flexDirection="row" alignItems="center" mb={18}>
|
||||
<Skeleton height={14} width={14} mr={4} />
|
||||
<Skeleton
|
||||
height={12}
|
||||
width={50}
|
||||
borderRadius={5}
|
||||
variant="secondary"
|
||||
/>
|
||||
<Skeleton height={14} width={14} ml={12} mr={4} />
|
||||
<Skeleton
|
||||
height={12}
|
||||
width={50}
|
||||
borderRadius={5}
|
||||
variant="secondary"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack flexDirection="row" alignItems="center" mb={27} gap={8}>
|
||||
<Skeleton height={24} width={76} borderRadius={20} />
|
||||
<Skeleton height={24} width={76} borderRadius={20} />
|
||||
<Skeleton height={24} width={76} borderRadius={20} />
|
||||
</Stack>
|
||||
<Stack mb={27}>
|
||||
<Skeleton height={12} width={50} borderRadius={5} mb={19} />
|
||||
<Stack flexDirection="row" alignItems="center" mb={16}>
|
||||
<Skeleton height={24} width={24} mr={8} />
|
||||
<Skeleton
|
||||
height={12}
|
||||
width={80}
|
||||
borderRadius={5}
|
||||
variant="secondary"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack flexDirection="row" alignItems="center" mb={16}>
|
||||
<Skeleton height={24} width={24} mr={8} />
|
||||
<Skeleton
|
||||
height={12}
|
||||
width={100}
|
||||
borderRadius={5}
|
||||
variant="secondary"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack flexDirection="row" alignItems="center" mb={16}>
|
||||
<Skeleton height={24} width={24} mr={8} />
|
||||
<Skeleton
|
||||
height={12}
|
||||
width={70}
|
||||
borderRadius={5}
|
||||
variant="secondary"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack flexDirection="row" alignItems="center">
|
||||
<Skeleton height={24} width={24} mr={8} />
|
||||
<Skeleton
|
||||
height={12}
|
||||
width={90}
|
||||
borderRadius={5}
|
||||
variant="secondary"
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Skeleton height={12} width={50} borderRadius={5} mb={19} />
|
||||
<Stack flexDirection="row" alignItems="center" mb={16}>
|
||||
<Skeleton height={24} width={24} mr={8} />
|
||||
<Skeleton
|
||||
height={12}
|
||||
width={80}
|
||||
borderRadius={5}
|
||||
variant="secondary"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack flexDirection="row" alignItems="center" mb={16}>
|
||||
<Skeleton height={24} width={24} mr={8} />
|
||||
<Skeleton
|
||||
height={12}
|
||||
width={100}
|
||||
borderRadius={5}
|
||||
variant="secondary"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack flexDirection="row" alignItems="center" mb={16}>
|
||||
<Skeleton height={24} width={24} mr={8} />
|
||||
<Skeleton
|
||||
height={12}
|
||||
width={70}
|
||||
borderRadius={5}
|
||||
variant="secondary"
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
export { SidebarSkeleton }
|
|
@ -0,0 +1,32 @@
|
|||
import { Skeleton } from './skeleton'
|
||||
|
||||
import type { Meta, StoryObj } from '@storybook/react'
|
||||
|
||||
const meta: Meta<typeof Skeleton> = {
|
||||
component: Skeleton,
|
||||
argTypes: {},
|
||||
parameters: {
|
||||
design: {
|
||||
type: 'figma',
|
||||
url: 'https://www.figma.com/file/1RN1MFwfSqA6jNFJBeNdEu/Posts-%26-Attachments-for-Web?t=1Xf5496ymHeazodw-0',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type Story = StoryObj<typeof Skeleton>
|
||||
|
||||
export const Avatar: Story = {
|
||||
name: 'Avatar',
|
||||
args: {},
|
||||
}
|
||||
|
||||
export const Text: Story = {
|
||||
name: 'Text',
|
||||
args: {
|
||||
width: 249,
|
||||
br: 6,
|
||||
height: 8,
|
||||
},
|
||||
}
|
||||
|
||||
export default meta
|
|
@ -0,0 +1,57 @@
|
|||
import { Stack, useTheme } from '@tamagui/core'
|
||||
|
||||
import type { ColorTokens, StackProps } from '@tamagui/core'
|
||||
|
||||
type Props = StackProps & {
|
||||
width?: number | string
|
||||
height?: number | string
|
||||
borderRadius?: number
|
||||
variant?: 'primary' | 'secondary'
|
||||
}
|
||||
|
||||
const skeletonColor: Record<NonNullable<Props['variant']>, ColorTokens> = {
|
||||
primary: '$neutral-10',
|
||||
secondary: '$neutral-20',
|
||||
}
|
||||
|
||||
const Skeleton = (props: Props) => {
|
||||
const {
|
||||
width = 32,
|
||||
height = 32,
|
||||
borderRadius = 16,
|
||||
variant = 'primary',
|
||||
...rest
|
||||
} = props
|
||||
|
||||
const theme = useTheme()
|
||||
/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const color = theme[skeletonColor[variant]]?.val
|
||||
|
||||
return (
|
||||
<Stack
|
||||
height={height}
|
||||
maxWidth={width}
|
||||
width="100%"
|
||||
borderRadius={borderRadius}
|
||||
overflow="hidden"
|
||||
{...rest}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: width,
|
||||
width: '100%',
|
||||
height,
|
||||
borderRadius,
|
||||
background: `linear-gradient(-90deg, ${color}, #FCFCFC, ${color})`,
|
||||
backgroundSize: '400% 400%',
|
||||
animation: 'gradient 1.5s ease infinite',
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
export type { Props as SkeletonProps }
|
|
@ -0,0 +1,54 @@
|
|||
import { Stack } from '@tamagui/core'
|
||||
import { BlurView } from 'expo-blur'
|
||||
|
||||
import { Skeleton } from './skeleton'
|
||||
|
||||
const TopbarSkeleton = () => {
|
||||
return (
|
||||
<BlurView intensity={40} style={{ zIndex: 100 }}>
|
||||
<Stack flexDirection="column" width="100%" height={96}>
|
||||
<Stack
|
||||
flexDirection="row"
|
||||
height={56}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
py={12}
|
||||
px={16}
|
||||
backgroundColor={'$blurBackground'}
|
||||
borderBottomWidth={1}
|
||||
borderColor={'$neutral-80-opa-10'}
|
||||
width="100%"
|
||||
>
|
||||
<Stack
|
||||
flexDirection="row"
|
||||
alignItems="center"
|
||||
flexWrap="wrap"
|
||||
flexGrow={1}
|
||||
flexShrink={1}
|
||||
>
|
||||
<Skeleton height={24} width={24} mr={8} />
|
||||
<Skeleton
|
||||
height={16}
|
||||
width={92}
|
||||
borderRadius={5}
|
||||
variant="secondary"
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
space={12}
|
||||
flexDirection="row"
|
||||
alignItems="center"
|
||||
justifyContent="flex-end"
|
||||
flexGrow={1}
|
||||
flexShrink={1}
|
||||
>
|
||||
<Skeleton height={32} width={32} borderRadius={10} />
|
||||
<Skeleton height={32} width={32} borderRadius={10} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</BlurView>
|
||||
)
|
||||
}
|
||||
|
||||
export { TopbarSkeleton }
|
|
@ -35,6 +35,13 @@ export const Default: Story = {
|
|||
args: {},
|
||||
}
|
||||
|
||||
export const isLoading: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
isLoading: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const WithMembersSelected: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
|
|
|
@ -16,6 +16,7 @@ import { BlurView } from 'expo-blur'
|
|||
import { DropdownMenu } from '../dropdown-menu'
|
||||
import { IconButton } from '../icon-button'
|
||||
import { PinnedMessage } from '../pinned-message'
|
||||
import { TopbarSkeleton } from '../skeleton/topbar-skeleton'
|
||||
import { Text } from '../text'
|
||||
|
||||
import type { Channel } from '../sidebar/mock-data'
|
||||
|
@ -41,13 +42,19 @@ type Props = {
|
|||
goBack?: () => void
|
||||
channel: Channel
|
||||
blur?: boolean
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
const Topbar = (props: Props) => {
|
||||
const { showMembers, onMembersPress, goBack, blur, channel } = props
|
||||
const { showMembers, onMembersPress, goBack, blur, channel, isLoading } =
|
||||
props
|
||||
|
||||
const { title, description, emoji } = channel
|
||||
|
||||
if (isLoading) {
|
||||
return <TopbarSkeleton />
|
||||
}
|
||||
|
||||
return (
|
||||
<BlurView intensity={40} style={{ zIndex: 100 }}>
|
||||
<Stack flexDirection="column" width="100%" height={96}>
|
||||
|
|
|
@ -28,6 +28,10 @@ export type MapVariant<
|
|||
[key in V[K] & string]: ColorTokens
|
||||
}
|
||||
|
||||
export type MapColorToken<V> = {
|
||||
[key in V & string]: ColorTokens
|
||||
}
|
||||
|
||||
export type GetVariants<A extends TamaguiComponent> = Required<
|
||||
GetStyledVariants<A>
|
||||
>
|
||||
|
|
|
@ -13232,7 +13232,6 @@ 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:
|
||||
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:
|
||||
|
|
Loading…
Reference in New Issue