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:
marcelines 2023-04-05 13:48:38 +01:00 committed by GitHub
parent fc580590ab
commit 91fe20549c
No known key found for this signature in database
GPG Key ID: 0EB8D75C775AB6F1
26 changed files with 953 additions and 14 deletions

View File

@ -77,3 +77,15 @@ body,
display: none; display: none;
} }
} }
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}

View File

@ -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%;
}
}

View File

@ -1,7 +1,9 @@
import React from 'react'
import { Provider, ToastContainer } from '../src' import { Provider, ToastContainer } from '../src'
import { Parameters, Decorator } from '@storybook/react' import { Parameters, Decorator } from '@storybook/react'
import './reset.css' import './reset.css'
import './components.css'
export const parameters: Parameters = { export const parameters: Parameters = {
actions: { argTypesRegex: '^on[A-Z].*' }, actions: { argTypesRegex: '^on[A-Z].*' },

View File

@ -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 { Stack } from '@tamagui/core'
import { Banner } from './banner' 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 = { export const AllVariants: Story = {
args: {}, args: {},
render: () => ( render: () => (
@ -46,6 +62,12 @@ export const AllVariants: Story = {
Banner message Banner message
</Banner> </Banner>
<Banner count={5}>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> <Banner icon={<PinIcon />}>Banner message</Banner>
</Stack> </Stack>
), ),

View File

@ -4,17 +4,25 @@ import { View } from 'react-native'
import { Counter } from '../counter' import { Counter } from '../counter'
import { Text } from '../text' import { Text } from '../text'
import type { ColorTokens } from '@tamagui/core'
type Props = { type Props = {
children: React.ReactNode children: React.ReactNode
icon?: React.ReactNode icon?: React.ReactNode
count?: number count?: number
backgroundColor?: ColorTokens
} }
const Banner = (props: Props) => { const Banner = (props: Props) => {
const { icon, children, count } = props const {
icon,
children,
count,
backgroundColor = '$primary-50-opa-20',
} = props
return ( return (
<Base> <Base backgroundColor={backgroundColor}>
<Content> <Content>
{icon} {icon}
<Text size={13} color="$textPrimary"> <Text size={13} color="$textPrimary">
@ -30,7 +38,6 @@ export { Banner }
export type { Props as BannerProps } export type { Props as BannerProps }
const Base = styled(View, { const Base = styled(View, {
backgroundColor: '$primary-50-opa-20',
padding: 12, padding: 12,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',

View File

@ -55,7 +55,7 @@ const ContextTag = (props: Props) => {
src, src,
icon, icon,
label, label,
type = 'default', // type = 'default', // this is commented because it's not being used
size = 24, size = 24,
blur = false, blur = false,
outline, outline,

View File

@ -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

View File

@ -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',
})

View File

@ -0,0 +1 @@
export { GapMessages } from './gap-messages'

View File

@ -3,14 +3,17 @@ export * from './button'
export * from './composer' export * from './composer'
export * from './dividers' export * from './dividers'
export * from './dynamic-button' export * from './dynamic-button'
export * from './gap-messages'
export * from './icon-button' export * from './icon-button'
export * from './image' export * from './image'
export * from './information-box'
export * from './input' export * from './input'
export * from './messages' export * from './messages'
export * from './pinned-message' export * from './pinned-message'
export * from './provider' export * from './provider'
export * from './sidebar' export * from './sidebar'
export * from './sidebar-members' export * from './sidebar-members'
export * from './skeleton'
export * from './text' export * from './text'
export * from './toast' export * from './toast'
export * from './topbar' export * from './topbar'

View File

@ -0,0 +1 @@
export { InformationBox } from './information-box'

View File

@ -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

View File

@ -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',
},
},
},
})

View File

@ -1,7 +1,19 @@
import { Stack } from '@tamagui/core'
import { CHANNEL_GROUPS } from './mock-data' import { CHANNEL_GROUPS } from './mock-data'
import { Sidebar } from './sidebar' 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 // More on how to set up stories at: https://storybook.js.org/docs/7.0/react/writing-stories/introduction
const meta: Meta<typeof Sidebar> = { const meta: Meta<typeof Sidebar> = {
@ -9,6 +21,7 @@ const meta: Meta<typeof Sidebar> = {
component: Sidebar, component: Sidebar,
args: { args: {
channels: CHANNEL_GROUPS, channels: CHANNEL_GROUPS,
community: COMMUNITY,
}, },
argTypes: {}, argTypes: {},
parameters: { 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 LoadingSidebar = {
export const Default: Story = { render: (args: SidebarProps) => (
args: {}, <Stack width={352} height="100vh">
<Sidebar {...args} isLoading />
</Stack>
),
} }
export default meta export default meta

View File

@ -6,12 +6,13 @@ import { AccordionItem } from '../accordion/accordionItem'
import { Avatar } from '../avatar' import { Avatar } from '../avatar'
import { Button } from '../button' import { Button } from '../button'
import { Image } from '../image' import { Image } from '../image'
import { SidebarSkeleton } from '../skeleton/sidebar-skeleton'
import { Text } from '../text' import { Text } from '../text'
import { CHANNEL_GROUPS } from './mock-data' import { CHANNEL_GROUPS } from './mock-data'
import type { ChannelGroup } from './mock-data' import type { ChannelGroup } from './mock-data'
type Props = { export type SidebarProps = {
community: { community: {
name: string name: string
description: string description: string
@ -21,18 +22,24 @@ type Props = {
channels?: ChannelGroup[] channels?: ChannelGroup[]
selectedChannelId?: string selectedChannelId?: string
onChannelPress: (channelId: string) => void onChannelPress: (channelId: string) => void
isLoading?: boolean
} }
const Sidebar = (props: Props) => { const Sidebar = (props: SidebarProps) => {
const { const {
community, community,
channels = CHANNEL_GROUPS, channels = CHANNEL_GROUPS,
selectedChannelId, selectedChannelId,
onChannelPress, onChannelPress,
isLoading,
} = props } = props
const { name, description, membersCount, imageUrl } = community const { name, description, membersCount, imageUrl } = community
if (isLoading) {
return <SidebarSkeleton />
}
return ( return (
<Stack <Stack
backgroundColor="$background" backgroundColor="$background"

View File

@ -0,0 +1,2 @@
export * from './message-skeleton'
export * from './skeleton'

View File

@ -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

View File

@ -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 }

View File

@ -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 }

View File

@ -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

View File

@ -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 }

View File

@ -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 }

View File

@ -35,6 +35,13 @@ export const Default: Story = {
args: {}, args: {},
} }
export const isLoading: Story = {
args: {
...Default.args,
isLoading: true,
},
}
export const WithMembersSelected: Story = { export const WithMembersSelected: Story = {
args: { args: {
...Default.args, ...Default.args,

View File

@ -16,6 +16,7 @@ import { BlurView } from 'expo-blur'
import { DropdownMenu } from '../dropdown-menu' import { DropdownMenu } from '../dropdown-menu'
import { IconButton } from '../icon-button' import { IconButton } from '../icon-button'
import { PinnedMessage } from '../pinned-message' import { PinnedMessage } from '../pinned-message'
import { TopbarSkeleton } from '../skeleton/topbar-skeleton'
import { Text } from '../text' import { Text } from '../text'
import type { Channel } from '../sidebar/mock-data' import type { Channel } from '../sidebar/mock-data'
@ -41,13 +42,19 @@ type Props = {
goBack?: () => void goBack?: () => void
channel: Channel channel: Channel
blur?: boolean blur?: boolean
isLoading?: boolean
} }
const Topbar = (props: Props) => { const Topbar = (props: Props) => {
const { showMembers, onMembersPress, goBack, blur, channel } = props const { showMembers, onMembersPress, goBack, blur, channel, isLoading } =
props
const { title, description, emoji } = channel const { title, description, emoji } = channel
if (isLoading) {
return <TopbarSkeleton />
}
return ( return (
<BlurView intensity={40} style={{ zIndex: 100 }}> <BlurView intensity={40} style={{ zIndex: 100 }}>
<Stack flexDirection="column" width="100%" height={96}> <Stack flexDirection="column" width="100%" height={96}>

View File

@ -28,6 +28,10 @@ export type MapVariant<
[key in V[K] & string]: ColorTokens [key in V[K] & string]: ColorTokens
} }
export type MapColorToken<V> = {
[key in V & string]: ColorTokens
}
export type GetVariants<A extends TamaguiComponent> = Required< export type GetVariants<A extends TamaguiComponent> = Required<
GetStyledVariants<A> GetStyledVariants<A>
> >

View File

@ -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: 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" version "2.6.7"
uid "1b5d62978f2ed07b99444f64f0df39f960a6d34d"
resolved "https://registry.npmjs.org/@achingbrain/node-fetch/-/node-fetch-2.6.7.tgz#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: node-forge@^1.1.0, node-forge@^1.2.1, node-forge@^1.3.1: