Update community sidebars, topbar & author (#370)

* add ChannelAvatar component

* add Channel component

* add expandable state to DividerLabel

* change counter story name

* Finalize SidebarCommunity component

* complete Author component

* update UserList component

* finalize SidebarMembers component

* Finalize Topbar component

* fix Banner truncate

* update dropdown menu props

* update app styles

* make mono font work

* render author part optionally

* rename css IDs

* remove console.log

* imageUrl -> imageSrc

* upgrade expo-blur

* add loading to messages

* fix  ¯\_(ツ)_/¯  comment
This commit is contained in:
Pavel 2023-04-05 16:31:35 +02:00 committed by GitHub
parent 91fe20549c
commit e7b6aa090d
No known key found for this signature in database
GPG Key ID: 0EB8D75C775AB6F1
39 changed files with 1027 additions and 695 deletions

View File

@ -5,7 +5,7 @@ import {
CHANNEL_GROUPS, CHANNEL_GROUPS,
Composer, Composer,
Messages, Messages,
Sidebar, SidebarCommunity,
SidebarMembers, SidebarMembers,
Topbar, Topbar,
useAppDispatch, useAppDispatch,
@ -20,8 +20,8 @@ const COMMUNITY = {
description: description:
'Multichain community-centric NFT marketplace. Create, buy and sell your NFTs.', 'Multichain community-centric NFT marketplace. Create, buy and sell your NFTs.',
membersCount: 123, membersCount: 123,
imageUrl: imageSrc:
'https://images.unsplash.com/photo-1574786527860-f2e274867c91?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1764&q=80', 'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2264&q=80',
} }
const updateProperty = (property: string, value: number) => { const updateProperty = (property: string, value: number) => {
@ -29,8 +29,17 @@ const updateProperty = (property: string, value: number) => {
} }
function App() { function App() {
const [loading, setLoading] = useState(false)
const [showMembers, setShowMembers] = useState(false) const [showMembers, setShowMembers] = useState(false)
// TODO: Use it to simulate loading
// useEffect(() => {
// setLoading(true)
// setTimeout(() => {
// setLoading(false)
// }, 2000)
// }, [])
const appState = useAppState() const appState = useAppState()
const appDispatch = useAppDispatch() const appDispatch = useAppDispatch()
@ -73,13 +82,14 @@ function App() {
return ( return (
<div id="app"> <div id="app">
<div id="sidebar" style={{ zIndex: 200 }}> <div id="sidebar-community" style={{ zIndex: 200 }}>
<Sidebar <SidebarCommunity
community={COMMUNITY} community={COMMUNITY}
selectedChannelId={appState.channelId} selectedChannelId={appState.channelId}
onChannelPress={channelId => onChannelPress={channelId =>
appDispatch({ type: 'set-channel', channelId }) appDispatch({ type: 'set-channel', channelId })
} }
loading={loading}
/> />
</div> </div>
@ -90,15 +100,31 @@ function App() {
channel={selectedChannel} channel={selectedChannel}
showMembers={showMembers} showMembers={showMembers}
onMembersPress={() => setShowMembers(show => !show)} onMembersPress={() => setShowMembers(show => !show)}
pinnedMessages={[
{
text: 'Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam a, posuere eu, velit.',
reactions: {},
pinned: true,
id: '1234-1234',
},
{
text: 'Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam.',
reactions: {},
pinned: true,
id: '4321-4321',
},
]}
loading={loading}
/> />
</div> </div>
<div id="content" ref={contentRef}> <div id="content" ref={contentRef}>
<div id="messages"> <div id="messages">
<Messages /> <Messages loading={loading} />
</div> </div>
</div> </div>
{loading === false && (
<div id="composer" ref={composerRef}> <div id="composer" ref={composerRef}>
{scrollPosition !== 'bottom' && ( {scrollPosition !== 'bottom' && (
<div id="anchor-actions"> <div id="anchor-actions">
@ -107,10 +133,11 @@ function App() {
)} )}
<Composer blur={scrollPosition !== 'bottom'} /> <Composer blur={scrollPosition !== 'bottom'} />
</div> </div>
)}
</main> </main>
{showMembers && ( {showMembers && (
<div id="members"> <div id="sidebar-members">
<SidebarMembers /> <SidebarMembers />
</div> </div>
)} )}

View File

@ -18,19 +18,25 @@ body,
#app { #app {
isolation: isolate; isolation: isolate;
height: 100%; height: 100vh;
display: grid; display: grid;
grid-template-columns: 352px 1fr auto; grid-template-columns: 352px 1fr auto;
} }
#main { #sidebar-community {
position: relative; overflow: auto;
height: 100vh; height: 100vh;
} }
#sidebar { #sidebar-members {
width: 352px;
overflow: auto; overflow: auto;
height: 100vh; background-color: #fff;
z-index: 2;
}
#main {
position: relative;
} }
#topbar { #topbar {
@ -45,6 +51,7 @@ body,
padding-top: var(--topbar-height); padding-top: var(--topbar-height);
padding-bottom: var(--composer-height); padding-bottom: var(--composer-height);
height: 100vh; height: 100vh;
isolation: isolate;
} }
#messages { #messages {
@ -60,12 +67,7 @@ body,
#composer { #composer {
position: absolute; position: absolute;
inset: auto 0 0; inset: auto 0 0;
z-index: 100; z-index: 1;
}
#members {
width: 352px;
overflow: auto;
} }
@media screen and (max-width: 768px) { @media screen and (max-width: 768px) {

View File

@ -92,3 +92,9 @@ h6 {
#__next { #__next {
isolation: isolate; isolation: isolate;
} }
/* Temporary testing purposes of keyboard navigation */
button:focus-visible {
outline: 2px solid crimson;
border-radius: 3px;
}

View File

@ -29,6 +29,7 @@
"react-native-web": "^0.18.0" "react-native-web": "^0.18.0"
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-accordion": "^1.1.1",
"@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",
@ -43,7 +44,7 @@
"@tamagui/react-native-media-driver": "1.7.7", "@tamagui/react-native-media-driver": "1.7.7",
"@tamagui/shorthands": "1.7.7", "@tamagui/shorthands": "1.7.7",
"@tamagui/theme-base": "1.7.7", "@tamagui/theme-base": "1.7.7",
"expo-blur": "~12.0.1", "expo-blur": "^12.2.2",
"expo-linear-gradient": "^12.1.2", "expo-linear-gradient": "^12.1.2",
"tamagui": "1.7.7", "tamagui": "1.7.7",
"zustand": "^4.3.6" "zustand": "^4.3.6"

View File

@ -1,41 +0,0 @@
import { CHANNEL_GROUPS } from '../sidebar/mock-data'
import { Accordion } from './accordion'
import { AccordionItem } from './accordionItem'
import type { Meta, StoryObj } from '@storybook/react'
const meta: Meta<typeof Accordion> = {
component: Accordion,
argTypes: {},
args: {
unreadCount: 3,
title: 'Welcome',
},
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/IBmFKgGL1B4GzqD8LQTw6n/Design-System-for-Web?node-id=14849%3A172544&t=4BeIzudVkio0c6Px-4',
},
},
}
type Story = StoryObj<typeof Accordion>
export const Default: Story = {
args: {
children: (
<>
<AccordionItem
key="welcome"
selected
channel={CHANNEL_GROUPS[0].channels[0]}
/>
<AccordionItem key="general" channel={CHANNEL_GROUPS[0].channels[0]} />
<AccordionItem key="lounge" channel={CHANNEL_GROUPS[0].channels[0]} />
<AccordionItem key="random" channel={CHANNEL_GROUPS[0].channels[0]} />
</>
),
},
}
export default meta

View File

@ -1,105 +0,0 @@
import { Fragment, useState } from 'react'
import { ChevronRightIcon } from '@status-im/icons/20'
import { Stack } from '@tamagui/core'
import { AnimatePresence } from 'tamagui'
import { Text } from '../text'
type Props = {
children: React.ReactElement[] | React.ReactElement
initialExpanded: boolean
title: string
unreadCount?: number
}
const Accordion = (props: Props) => {
const { children, initialExpanded, title, unreadCount } = props
const [isExpanded, setIsExpanded] = useState(initialExpanded)
return (
<Stack
accessibilityRole="button"
width="100%"
borderRadius="$0"
borderTopWidth={1}
borderTopColor="$neutral-10"
paddingHorizontal={8}
paddingBottom={8}
>
<Stack justifyContent="flex-start">
<Stack width="100%">
<Stack
width="100%"
flexDirection="row"
justifyContent={'space-between'}
onPress={() => setIsExpanded(prev => !prev)}
cursor="pointer"
py={8}
>
<Stack flexDirection="row" alignItems="center" gap={4}>
<Stack
animation="fast"
justifyContent="center"
transform={[
{
rotateZ: isExpanded ? '90deg' : '0deg',
},
{
translateY: isExpanded ? -4 : 0,
},
]}
>
<ChevronRightIcon color="$neutral-50" />
</Stack>
<Text size={13} color="$neutral-50" weight="medium">
{title}
</Text>
</Stack>
<AnimatePresence>
{!isExpanded && unreadCount !== 0 && (
<Stack
key={`notifications-${title}}`}
width={20}
justifyContent="center"
alignItems="center"
mr={8}
animation={[
'fast',
{
opacity: {
overshootClamping: true,
},
},
]}
enterStyle={{ opacity: 0 }}
exitStyle={{ opacity: 0 }}
opacity={1}
>
<Stack
backgroundColor="$turquoise-50"
borderRadius="$4"
width={16}
height={16}
justifyContent="center"
alignItems="center"
>
<Text size={11} color="$white-100" weight="medium">
{unreadCount}
</Text>
</Stack>
</Stack>
)}
</AnimatePresence>
</Stack>
<AnimatePresence>
{isExpanded && <Fragment key={title}>{children}</Fragment>}
</AnimatePresence>
</Stack>
</Stack>
</Stack>
)
}
export { Accordion }

View File

@ -1,120 +0,0 @@
import { MutedIcon } from '@status-im/icons/20'
import { Stack, Text as RNText } from '@tamagui/core'
import { Text } from '../text'
import type { Channel } from '../sidebar/mock-data'
import type { ColorTokens } from '@tamagui/core'
type Props = {
selected?: boolean
onPress?: () => void
channel: Channel
}
const textColors: Record<NonNullable<Channel['channelStatus']>, ColorTokens> = {
muted: '$neutral-40',
normal: '$neutral-50',
withMessages: '$neutral-100',
withMentions: '$neutral-100',
}
const AccordionItem = (props: Props) => {
const { channel, selected, onPress } = props
const { emoji, title, channelStatus = 'normal', unreadCount } = channel
return (
<Stack
accessibilityRole="button"
animation={[
'fast',
{
opacity: {
overshootClamping: true,
},
},
]}
backgroundColor={selected ? '$primary-50-opa-10' : 'transparent'}
hoverStyle={{
backgroundColor: '$primary-50-opa-5',
}}
borderRadius="$4"
padding={8}
width="100%"
enterStyle={{ opacity: 0 }}
exitStyle={{ opacity: 0 }}
opacity={1}
justifyContent={
channelStatus === 'normal' ? 'flex-start' : 'space-between'
}
alignItems="center"
flexDirection="row"
cursor="pointer"
onPress={onPress}
>
<Stack
justifyContent="flex-start"
alignItems="center"
flexDirection="row"
>
{emoji && (
<Stack
width={24}
height={24}
borderRadius={24 / 2}
backgroundColor="$turquoise-50-opa-10"
justifyContent="center"
alignItems="center"
marginRight={8}
>
<RNText>{emoji}</RNText>
</Stack>
)}
<Text size={15} color={textColors[channelStatus]} weight="medium">
{title}
</Text>
</Stack>
{channelStatus !== 'normal' && (
<Stack>
{channelStatus === 'withMentions' && (
<Stack width={20} justifyContent="center" alignItems="center">
<Stack
backgroundColor="$turquoise-50"
borderRadius="$4"
width={16}
height={16}
justifyContent="center"
alignItems="center"
>
<Text size={11} color="$white-100" weight="medium">
{unreadCount}
</Text>
</Stack>
</Stack>
)}
{channelStatus === 'withMessages' && (
<Stack
width={20}
height={20}
justifyContent="center"
alignItems="center"
>
<Stack
backgroundColor="$neutral-40"
borderRadius="$4"
width={8}
height={8}
justifyContent="center"
alignItems="center"
/>
</Stack>
)}
{channelStatus === 'muted' && <MutedIcon color="$neutral-40" />}
</Stack>
)}
</Stack>
)
}
export { AccordionItem }

View File

@ -1,3 +1,5 @@
import { Stack } from 'tamagui'
import { Author } from './author' import { Author } from './author'
import type { Meta, StoryObj } from '@storybook/react' import type { Meta, StoryObj } from '@storybook/react'
@ -14,30 +16,125 @@ const meta: Meta<typeof Author> = {
url: 'https://www.figma.com/file/IBmFKgGL1B4GzqD8LQTw6n/Design-System-for-Web?node-id=3155%3A49848&t=87Ziud3PyYYSvsRg-4', url: 'https://www.figma.com/file/IBmFKgGL1B4GzqD8LQTw6n/Design-System-for-Web?node-id=3155%3A49848&t=87Ziud3PyYYSvsRg-4',
}, },
}, },
render: args => (
<Stack gap={8}>
<Author {...args} />
<Author {...args} status="verified" />
<Author {...args} status="untrustworthy" />
<Author {...args} status="contact" />
</Stack>
),
} }
type Story = StoryObj<typeof Author> type Story = StoryObj<typeof Author>
export const Default: Story = { export const AllVariants: Story = {
args: {}, args: {},
} render: args => (
<Stack gap={20}>
<Stack gap={8}>
<Author {...args} />
<Author {...args} status="verified" />
<Author {...args} status="untrustworthy" />
<Author {...args} status="contact" />
</Stack>
export const Contact: Story = { <Stack gap={8}>
args: { <Author {...args} address="zQ3...9d4Gs0" />
status: 'contact', <Author {...args} status="verified" address="zQ3...9d4Gs0" />
}, <Author {...args} status="untrustworthy" address="zQ3...9d4Gs0" />
} <Author {...args} status="contact" address="zQ3...9d4Gs0" />
</Stack>
export const Verified: Story = { <Stack gap={8}>
args: { <Author {...args} address="zQ3...9d4Gs0" time="09:30" />
status: 'verified', <Author
}, {...args}
} status="verified"
address="zQ3...9d4Gs0"
time="09:30"
/>
<Author
{...args}
status="untrustworthy"
address="zQ3...9d4Gs0"
time="09:30"
/>
<Author
{...args}
status="contact"
nickname="alisher.eth"
address="zQ3...9d4Gs0"
time="09:30"
/>
</Stack>
export const Untrustworthy: Story = { <Stack gap={8}>
args: { <Author
status: 'untrustworthy', {...args}
}, nickname="alisher.eth"
address="zQ3...9d4Gs0"
time="09:30"
/>
<Author
{...args}
status="verified"
nickname="alisher.eth"
address="zQ3...9d4Gs0"
time="09:30"
/>
<Author
{...args}
status="untrustworthy"
nickname="alisher.eth"
address="zQ3...9d4Gs0"
time="09:30"
/>
<Author
{...args}
status="contact"
nickname="alisher.eth"
address="zQ3...9d4Gs0"
time="09:30"
/>
</Stack>
<Stack gap={8}>
<Author
{...args}
size={15}
nickname="alisher.eth"
address="zQ3...9d4Gs0"
time="09:30"
/>
<Author
{...args}
status="verified"
size={15}
nickname="alisher.eth"
address="zQ3...9d4Gs0"
time="09:30"
/>
<Author
{...args}
status="untrustworthy"
size={15}
nickname="alisher.eth"
address="zQ3...9d4Gs0"
time="09:30"
/>
<Author
{...args}
status="contact"
size={15}
nickname="alisher.eth"
address="zQ3...9d4Gs0"
time="09:30"
/>
</Stack>
</Stack>
),
} }
export default meta export default meta

View File

@ -7,35 +7,51 @@ import { XStack } from 'tamagui'
import { Text } from '../text' import { Text } from '../text'
interface Props { import type { TextProps } from '../text'
type Props = {
name: string name: string
size?: Extract<TextProps['size'], 13 | 15>
nickname?: string nickname?: string
address?: string
status?: 'verified' | 'untrustworthy' | 'contact' status?: 'verified' | 'untrustworthy' | 'contact'
address?: string
time?: string time?: string
orientation?: 'horizontal' | 'vertical'
} }
const Author = (props: Props) => { const Author = (props: Props) => {
const { name, status, address, time } = props const { name, size = 13, nickname, status, address, time } = props
return ( return (
<XStack space={8} alignItems="center"> <XStack space={8} alignItems="center">
<XStack space={4} alignItems="center"> <XStack gap={4} alignItems="center">
<Text size={13} weight="semibold"> <Text size={size} weight="semibold">
{name} {name}
</Text> </Text>
{nickname && (
<Text size={11} color="$neutral-60">
· {nickname}
</Text>
)}
{status === 'contact' && <ContactIcon />} {status === 'contact' && <ContactIcon />}
{status === 'verified' && <VerifiedIcon />} {status === 'verified' && <VerifiedIcon />}
{status === 'untrustworthy' && <UntrustworthyIcon />} {status === 'untrustworthy' && <UntrustworthyIcon />}
</XStack> </XStack>
{(address || time) && (
<XStack gap={4} alignItems="center">
{address && ( {address && (
<Text size={11} color="$neutral-50"> <Text size={11} color="$neutral-50" type="monospace">
{address} {address}
{time && ` · ${time}`}
</Text> </Text>
)} )}
{time && (
<Text size={11} color="$neutral-50">
· {time}
</Text>
)}
</XStack>
)}
</XStack> </XStack>
) )
} }

View File

@ -0,0 +1,81 @@
import { LockedIcon, UnlockedIcon } from '@status-im/icons/12'
import { type ColorTokens, Stack, styled, Text } from '@tamagui/core'
type Props = {
emoji: string
color?: ColorTokens
background?: ColorTokens
size: 32 | 24 | 20
lock?: 'locked' | 'unlocked' | 'none'
}
const emojiSizes: Record<Props['size'], number> = {
32: 14,
24: 13,
20: 11,
}
// https://www.figma.com/file/IBmFKgGL1B4GzqD8LQTw6n/Design-System-for-Desktop%2FWeb?node-id=399-20709&t=kX5LC5OYFnSF8BiZ-11
const ChannelAvatar = (props: Props) => {
const { emoji, background = '$blue-50-opa-20', size, lock = 'none' } = props
return (
<Base size={size} backgroundColor={background}>
{lock !== 'none' && (
<LockBase variant={size}>
{lock === 'locked' ? <LockedIcon /> : <UnlockedIcon />}
</LockBase>
)}
<Text fontSize={emojiSizes[size]}>{emoji}</Text>
</Base>
)
}
export { ChannelAvatar }
export type { Props as ChannelAvatarProps }
const Base = styled(Stack, {
position: 'relative',
justifyContent: 'center',
alignItems: 'center',
variants: {
size: {
'...': (size: number) => {
return {
width: size,
height: size,
borderRadius: size / 2,
}
},
},
},
})
const LockBase = styled(Stack, {
justifyContent: 'center',
alignItems: 'center',
width: 16,
height: 16,
backgroundColor: '$white-100',
position: 'absolute',
borderRadius: 16,
variants: {
variant: {
32: {
right: -4,
bottom: -4,
},
24: {
right: -4,
bottom: -4,
},
20: {
right: -6,
bottom: -6,
},
},
},
})

View File

@ -1,2 +1,3 @@
export * from './avatar' export * from './avatar'
export * from './channel-avatar'
export * from './icon-avatar' export * from './icon-avatar'

View File

@ -1,4 +1,4 @@
import { styled } from '@tamagui/core' import { Stack, styled } from '@tamagui/core'
import { View } from 'react-native' import { View } from 'react-native'
import { Counter } from '../counter' import { Counter } from '../counter'
@ -15,7 +15,7 @@ type Props = {
const Banner = (props: Props) => { const Banner = (props: Props) => {
const { const {
icon, icon = null,
children, children,
count, count,
backgroundColor = '$primary-50-opa-20', backgroundColor = '$primary-50-opa-20',
@ -25,9 +25,11 @@ const Banner = (props: Props) => {
<Base backgroundColor={backgroundColor}> <Base backgroundColor={backgroundColor}>
<Content> <Content>
{icon} {icon}
<Text size={13} color="$textPrimary"> <Stack flexGrow={1} flexShrink={1}>
<Text size={13} color="$textPrimary" truncate>
{children} {children}
</Text> </Text>
</Stack>
</Content> </Content>
{count ? <Counter value={count} /> : null} {count ? <Counter value={count} /> : null}
</Base> </Base>
@ -43,10 +45,12 @@ const Base = styled(View, {
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
maxHeight: '40px', maxHeight: '40px',
gap: 10,
}) })
const Content = styled(View, { const Content = styled(View, {
flexDirection: 'row', flexDirection: 'row',
gap: 10, gap: 10,
alignItems: 'center', alignItems: 'center',
width: '90%', // truncate does not work without this ¯\_(ツ)_/¯
}) })

View File

@ -0,0 +1,64 @@
import { Stack } from '@tamagui/core'
import { Channel } from './channel'
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 Channel> = {
component: Channel,
args: {
emoji: '🍑',
children: 'channel',
},
argTypes: {},
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/IBmFKgGL1B4GzqD8LQTw6n/Design-System-for-Desktop%2FWeb?node-id=411-18564&t=kX5LC5OYFnSF8BiZ-11',
},
},
render: args => (
<Stack space flexDirection="row">
<Stack space width={336}>
<Channel {...args} type="default" />
<Channel {...args} type="default" selected />
<Channel {...args} type="notification" />
<Channel {...args} type="notification" selected />
<Channel {...args} type="mention" mentionCount={10} />
<Channel {...args} type="mention" mentionCount={10} selected />
<Channel {...args} type="muted" />
<Channel {...args} type="muted" selected />
</Stack>
</Stack>
),
}
type Story = StoryObj<typeof Channel>
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
export const Default: Story = {
args: {
lock: 'none',
},
}
export const Locked: Story = {
args: {
lock: 'locked',
},
}
export const Unlocked: Story = {
args: {
lock: 'unlocked',
},
}
export default meta

View File

@ -0,0 +1,130 @@
import { useState } from 'react'
import { MutedIcon, NotificationIcon, OptionsIcon } from '@status-im/icons/20'
import { Stack, styled } from 'tamagui'
import { ChannelAvatar } from '../avatar'
import { Counter } from '../counter'
import { DropdownMenu } from '../dropdown-menu'
import { Text } from '../text'
import type { ChannelAvatarProps } from '../avatar'
import type { ColorTokens } from 'tamagui'
type Props = {
children: string
selected: boolean
emoji: ChannelAvatarProps['emoji']
lock?: ChannelAvatarProps['lock']
mentionCount?: number
} & (
| {
type: 'default' | 'notification' | 'muted'
}
| {
type: 'mention'
mentionCount: number
}
)
const textColors: Record<Props['type'], ColorTokens> = {
default: '$neutral-50',
notification: '$neutral-100',
mention: '$neutral-100',
muted: '$neutral-40',
}
const Channel = (props: Props) => {
const { type, children, selected, emoji, lock } = props
const [hovered, setHovered] = useState(false)
const [menuOpen, setMenuOpen] = useState(false)
const active = hovered || menuOpen
const renderContent = () => {
if (active) {
return (
<DropdownMenu onOpenChange={setMenuOpen}>
<Stack tag="button" width={20} height={20}>
<OptionsIcon color="$neutral-50" />
</Stack>
{/* TODO: Find all options */}
<DropdownMenu.Content align="start">
<DropdownMenu.Item
icon={<MutedIcon />}
label="Mute channel"
onSelect={() => {
console.log('Mute channel')
}}
/>
<DropdownMenu.Item
icon={<MutedIcon />}
label="Mark as read"
onSelect={() => {
console.log('Mark as read')
}}
/>
</DropdownMenu.Content>
</DropdownMenu>
)
}
switch (type) {
case 'default':
return null
case 'mention': {
const { mentionCount } = props
return <Counter value={mentionCount} />
}
case 'notification':
return <NotificationIcon color="$neutral-40" />
case 'muted':
return <MutedIcon color="$neutral-40" />
}
}
const textColor: ColorTokens =
selected || active ? '$neutral-100' : textColors[type]
return (
<Base
onHoverIn={() => setHovered(true)}
onHoverOut={() => setHovered(false)}
state={active ? 'active' : selected ? 'selected' : undefined}
>
<Stack flexDirection="row" gap={8} alignItems="center">
<ChannelAvatar emoji={emoji} size={24} lock={lock} />
<Text size={15} weight="medium" color={textColor}>
# {children}
</Text>
</Stack>
{renderContent()}
</Base>
)
}
export { Channel }
export type { Props as ChannelProps }
const Base = styled(Stack, {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 8,
borderRadius: 12,
userSelect: 'none',
variants: {
state: {
active: {
backgroundColor: '$primary-50-opa-5',
},
selected: {
backgroundColor: '$primary-50-opa-10',
},
},
},
})

View File

@ -0,0 +1 @@
export { Channel, type ChannelProps } from './channel'

View File

@ -0,0 +1,4 @@
export { CHANNEL_GROUPS } from './mock-data'
export { SidebarCommunity } from './sidebar-community'
export { SidebarMembers } from './sidebar-members'
export { Topbar } from './topbar'

View File

@ -1,17 +1,17 @@
export interface Channel { export interface ChannelType {
id: string id: string
title: string title: string
description: string description: string
emoji: string emoji: string
channelStatus?: 'muted' | 'normal' | 'withMessages' | 'withMentions' channelStatus?: 'default' | 'notification' | 'muted'
unreadCount?: number mentionCount?: number
} }
export interface ChannelGroup { export interface ChannelGroupType {
id: string id: string
title: string title: string
unreadCount?: number unreadCount?: number
channels: Channel[] channels: ChannelType[]
} }
const emojis = ['👋', '🔥', '🦄', '🍑', '🤫', '🫣', '🏀', '🤝'] const emojis = ['👋', '🔥', '🦄', '🍑', '🤫', '🫣', '🏀', '🤝']
@ -19,7 +19,7 @@ const emojis = ['👋', '🔥', '🦄', '🍑', '🤫', '🫣', '🏀', '🤝']
const randomEmoji = () => emojis[Math.floor(Math.random() * emojis.length)] const randomEmoji = () => emojis[Math.floor(Math.random() * emojis.length)]
// MOCK DATA // MOCK DATA
export const CHANNEL_GROUPS: ChannelGroup[] = [ export const CHANNEL_GROUPS: ChannelGroupType[] = [
{ {
id: 'welcome', id: 'welcome',
title: 'Welcome', title: 'Welcome',
@ -27,29 +27,28 @@ export const CHANNEL_GROUPS: ChannelGroup[] = [
channels: [ channels: [
{ {
id: 'welcome', id: 'welcome',
title: '# welcome', title: 'welcome',
emoji: randomEmoji(), emoji: randomEmoji(),
description: 'Share random funny stuff with the community. Play nice.', description: 'Share random funny stuff with the community. Play nice.',
}, },
{ {
id: 'general-welcome', id: 'general-welcome',
title: '# general', title: 'general',
emoji: randomEmoji(), emoji: randomEmoji(),
description: 'Share random funny stuff with the community. Play nice.', description: 'Share random funny stuff with the community. Play nice.',
}, },
{ {
id: 'random', id: 'random',
title: '# random', title: 'random',
emoji: randomEmoji(), emoji: randomEmoji(),
description: 'Share random funny stuff with the community. Play nice.', description: 'Share random funny stuff with the community. Play nice.',
}, },
{ {
id: 'onboarding', id: 'onboarding',
title: '# onboarding', title: 'onboarding',
emoji: randomEmoji(), emoji: randomEmoji(),
description: 'Share random funny stuff with the community. Play nice.', description: 'Share random funny stuff with the community. Play nice.',
channelStatus: 'withMentions', mentionCount: 3,
unreadCount: 3,
}, },
], ],
}, },
@ -60,29 +59,27 @@ export const CHANNEL_GROUPS: ChannelGroup[] = [
channels: [ channels: [
{ {
id: 'announcements', id: 'announcements',
title: '# announcements', title: 'announcements',
emoji: randomEmoji(), emoji: randomEmoji(),
description: 'Share random funny stuff with the community. Play nice.', description: 'Share random funny stuff with the community. Play nice.',
}, },
{ {
id: 'jobs', id: 'jobs',
title: '# jobs', title: 'jobs',
channelStatus: 'withMentions', mentionCount: 3,
unreadCount: 3,
emoji: randomEmoji(), emoji: randomEmoji(),
description: 'Share random funny stuff with the community. Play nice.', description: 'Share random funny stuff with the community. Play nice.',
}, },
{ {
id: 'events', id: 'events',
title: '# events', title: 'events',
channelStatus: 'withMentions', mentionCount: 2,
unreadCount: 2,
emoji: randomEmoji(), emoji: randomEmoji(),
description: 'Share random funny stuff with the community. Play nice.', description: 'Share random funny stuff with the community. Play nice.',
}, },
{ {
id: 'meetups', id: 'meetups',
title: '# meetups', title: 'meetups',
emoji: randomEmoji(), emoji: randomEmoji(),
description: 'Share random funny stuff with the community. Play nice.', description: 'Share random funny stuff with the community. Play nice.',
}, },
@ -94,25 +91,25 @@ export const CHANNEL_GROUPS: ChannelGroup[] = [
channels: [ channels: [
{ {
id: 'design', id: 'design',
title: '# design', title: 'design',
emoji: randomEmoji(), emoji: randomEmoji(),
description: 'Share random funny stuff with the community. Play nice.', description: 'Share random funny stuff with the community. Play nice.',
}, },
{ {
id: 'ux', id: 'ux',
title: '# ux', title: 'ux',
emoji: randomEmoji(), emoji: randomEmoji(),
description: 'Share random funny stuff with the community. Play nice.', description: 'Share random funny stuff with the community. Play nice.',
}, },
{ {
id: 'ui', id: 'ui',
title: '# ui', title: 'ui',
emoji: randomEmoji(), emoji: randomEmoji(),
description: 'Share random funny stuff with the community. Play nice.', description: 'Share random funny stuff with the community. Play nice.',
}, },
{ {
id: 'figma', id: 'figma',
title: '# figma', title: 'figma',
emoji: randomEmoji(), emoji: randomEmoji(),
description: 'Share random funny stuff with the community. Play nice.', description: 'Share random funny stuff with the community. Play nice.',
}, },
@ -124,13 +121,13 @@ export const CHANNEL_GROUPS: ChannelGroup[] = [
channels: [ channels: [
{ {
id: 'general', id: 'general',
title: '# general', title: 'general',
emoji: randomEmoji(), emoji: randomEmoji(),
description: 'Share random funny stuff with the community. Play nice.', description: 'Share random funny stuff with the community. Play nice.',
}, },
{ {
id: 'people-ops', id: 'people-ops',
title: '# people-ops', title: 'people-ops',
emoji: randomEmoji(), emoji: randomEmoji(),
description: 'Share random funny stuff with the community. Play nice.', description: 'Share random funny stuff with the community. Play nice.',
}, },
@ -142,27 +139,27 @@ export const CHANNEL_GROUPS: ChannelGroup[] = [
channels: [ channels: [
{ {
id: 'react', id: 'react',
title: '# react', title: 'react',
emoji: randomEmoji(), emoji: randomEmoji(),
description: 'Share random funny stuff with the community. Play nice.', description: 'Share random funny stuff with the community. Play nice.',
channelStatus: 'withMessages', channelStatus: 'notification',
}, },
{ {
id: 'vue', id: 'vue',
title: '# vue', title: 'vue',
emoji: randomEmoji(), emoji: randomEmoji(),
description: 'Share random funny stuff with the community. Play nice.', description: 'Share random funny stuff with the community. Play nice.',
}, },
{ {
id: 'angular', id: 'angular',
title: '# angular', title: 'angular',
channelStatus: 'muted', channelStatus: 'muted',
emoji: randomEmoji(), emoji: randomEmoji(),
description: 'Share random funny stuff with the community. Play nice.', description: 'Share random funny stuff with the community. Play nice.',
}, },
{ {
id: 'svelte', id: 'svelte',
title: '# svelte', title: 'svelte',
emoji: randomEmoji(), emoji: randomEmoji(),
description: 'Share random funny stuff with the community. Play nice.', description: 'Share random funny stuff with the community. Play nice.',
}, },
@ -174,25 +171,25 @@ export const CHANNEL_GROUPS: ChannelGroup[] = [
channels: [ channels: [
{ {
id: 'node', id: 'node',
title: '# node', title: 'node',
emoji: randomEmoji(), emoji: randomEmoji(),
description: 'Share random funny stuff with the community. Play nice.', description: 'Share random funny stuff with the community. Play nice.',
}, },
{ {
id: 'python', id: 'python',
title: '# python', title: 'python',
emoji: randomEmoji(), emoji: randomEmoji(),
description: 'Share random funny stuff with the community. Play nice.', description: 'Share random funny stuff with the community. Play nice.',
}, },
{ {
id: 'ruby', id: 'ruby',
title: '# ruby', title: 'ruby',
emoji: randomEmoji(), emoji: randomEmoji(),
description: 'Share random funny stuff with the community. Play nice.', description: 'Share random funny stuff with the community. Play nice.',
}, },
{ {
id: 'php', id: 'php',
title: '# php', title: 'php',
channelStatus: 'muted', channelStatus: 'muted',
emoji: randomEmoji(), emoji: randomEmoji(),
description: 'Share random funny stuff with the community. Play nice.', description: 'Share random funny stuff with the community. Play nice.',

View File

@ -0,0 +1,74 @@
import * as Accordion from '@radix-ui/react-accordion'
import { Stack } from 'tamagui'
import { Channel } from '../../../channel'
import { DividerLabel } from '../../../dividers'
import type { ChannelType } from '../../mock-data'
type Props = {
name: string
channels: ChannelType[]
unreadCount?: number
selectedChannelId?: string
expanded: boolean
}
const ChannelGroup = (props: Props) => {
const { name, channels, selectedChannelId, expanded } = props
const totalMentionsCount = channels.reduce(
(acc, channel) => acc + (channel.mentionCount || 0),
0
)
return (
<Accordion.Item value={name}>
<Stack>
<Accordion.Trigger>
<DividerLabel
type="expandable"
expanded={expanded}
label={name}
counterType="default"
count={
totalMentionsCount > 0 && expanded === false
? totalMentionsCount
: undefined
}
/>
</Accordion.Trigger>
<Stack paddingHorizontal={8} paddingBottom={expanded ? 8 : 0}>
{channels.map(channel => {
const {
emoji,
title,
//This will work differently with the live data
channelStatus: type = 'default',
mentionCount = 0,
} = channel
const selected = selectedChannelId === channel.id
return (
<Accordion.Content key={channel.title}>
<Channel
emoji={emoji}
selected={!!selected}
{...(mentionCount > 0
? { type: 'mention', mentionCount }
: { type })}
>
{title}
</Channel>
</Accordion.Content>
)
})}
</Stack>
</Stack>
</Accordion.Item>
)
}
export { ChannelGroup }

View File

@ -0,0 +1 @@
export { SidebarCommunity } from './sidebar-community'

View File

@ -0,0 +1,47 @@
import { CHANNEL_GROUPS } from '../mock-data'
import { SidebarCommunity } from './sidebar-community'
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 SidebarCommunity> = {
title: 'Community/Community Sidebar',
component: SidebarCommunity,
args: {
community: {
name: 'Rarible',
description:
'Multichain community-centric NFT marketplace. Create, buy and sell your NFTs.',
membersCount: 123,
imageSrc:
'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2264&q=80',
},
channels: CHANNEL_GROUPS,
},
argTypes: {},
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/IBmFKgGL1B4GzqD8LQTw6n/Design-System-for-Web?node-id=14692%3A148489&t=NfQkS7CPSrZknAGF-4',
},
},
render: args => (
<div style={{ maxWidth: 360 }}>
<SidebarCommunity {...args} />
</div>
),
}
type Story = StoryObj<typeof SidebarCommunity>
export const Default: Story = {
args: {},
}
export const Loading: Story = {
args: {
loading:true
},
}
export default meta

View File

@ -1,42 +1,47 @@
import { useState } from 'react'
import * as Accordion from '@radix-ui/react-accordion'
import { GroupIcon } from '@status-im/icons/16' import { GroupIcon } from '@status-im/icons/16'
import { CommunitiesIcon } from '@status-im/icons/20'
import { Stack } from '@tamagui/core' import { Stack } from '@tamagui/core'
import { Accordion } from '../accordion/accordion' import { Avatar } from '../../avatar'
import { AccordionItem } from '../accordion/accordionItem' import { Button } from '../../button'
import { Avatar } from '../avatar' import { Image } from '../../image'
import { Button } from '../button' import { SidebarSkeleton } from '../../skeleton/sidebar-skeleton'
import { Image } from '../image' import { Text } from '../../text'
import { SidebarSkeleton } from '../skeleton/sidebar-skeleton' import { CHANNEL_GROUPS } from '../mock-data'
import { Text } from '../text' import { ChannelGroup } from './components/channel-group'
import { CHANNEL_GROUPS } from './mock-data'
import type { ChannelGroup } from './mock-data' import type { ChannelGroupType } from '../mock-data'
export type SidebarProps = { type Props = {
community: { community: {
name: string name: string
description: string description: string
membersCount: number membersCount: number
imageUrl: string imageSrc: string
} }
channels?: ChannelGroup[] channels?: ChannelGroupType[]
selectedChannelId?: string selectedChannelId?: string
onChannelPress: (channelId: string) => void onChannelPress: (channelId: string) => void
isLoading?: boolean loading?: boolean
} }
const Sidebar = (props: SidebarProps) => { const SidebarCommunity = (props: Props) => {
const { const {
community, community,
channels = CHANNEL_GROUPS, channels = CHANNEL_GROUPS,
selectedChannelId, selectedChannelId,
onChannelPress, loading,
isLoading, // onChannelPress,
} = props } = props
const { name, description, membersCount, imageUrl } = community const { name, description, membersCount, imageSrc } = community
if (isLoading) { const [value, setValue] = useState(['Welcome'])
if (loading) {
return <SidebarSkeleton /> return <SidebarSkeleton />
} }
@ -48,7 +53,7 @@ const Sidebar = (props: SidebarProps) => {
height="100%" height="100%"
overflow="scroll" overflow="scroll"
> >
<Image src={imageUrl} width="full" aspectRatio={2.6} /> <Image src={imageSrc} width="full" aspectRatio={2.6} />
<Stack <Stack
paddingBottom={16} paddingBottom={16}
marginTop={-16} marginTop={-16}
@ -76,31 +81,26 @@ const Sidebar = (props: SidebarProps) => {
<Text size={15}>{membersCount}</Text> <Text size={15}>{membersCount}</Text>
</Stack> </Stack>
<Button>Join community</Button> <Button icon={<CommunitiesIcon />}>Request to join community</Button>
</Stack> </Stack>
{channels.map(group => (
<Accordion <Accordion.Root type="multiple" value={value} onValueChange={setValue}>
key={group.id} {channels.map(group => {
initialExpanded={group.id === 'welcome'}
title={group.title}
unreadCount={group.unreadCount}
>
{group.channels.map(channel => {
return ( return (
<AccordionItem <ChannelGroup
key={channel.id} key={group.id}
channel={channel} name={group.title}
selected={selectedChannelId === channel.id} unreadCount={group.unreadCount}
onPress={() => onChannelPress(channel.id)} channels={group.channels}
expanded={value.includes(group.title)}
selectedChannelId={selectedChannelId}
/> />
) )
})} })}
</Accordion> </Accordion.Root>
))}
<Stack borderBottomColor="$neutral-10" borderBottomWidth={1} />
</Stack> </Stack>
</Stack> </Stack>
) )
} }
export { Sidebar } export { SidebarCommunity }

View File

@ -4,9 +4,15 @@ 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 SidebarMembers> = { const meta: Meta<typeof SidebarMembers> = {
title: 'Sidebar/Members', title: 'Community/Members Sidebar',
component: SidebarMembers, component: SidebarMembers,
args: {},
argTypes: {}, argTypes: {},
render: () => (
<div style={{ maxWidth: 360 }}>
<SidebarMembers />
</div>
),
} }
type Story = StoryObj<typeof SidebarMembers> type Story = StoryObj<typeof SidebarMembers>

View File

@ -1,32 +1,18 @@
import { Stack } from '@tamagui/core' 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 Props = {
label: string // users: []
users: UserListProps['users'] // }
}
const Group = (props: GroupProps) => {
const { label, users } = props
return (
<Stack paddingBottom={8}>
<DividerLabel label={label} tight={false} />
<Stack paddingHorizontal={8}>
<UserList users={users} />
</Stack>
</Stack>
)
}
const SidebarMembers = () => { const SidebarMembers = () => {
return ( return (
<Stack <Stack
backgroundColor="$background" backgroundColor="$white-100"
borderLeftWidth={1} borderLeftWidth={1}
borderColor="$neutral-10" borderColor="$neutral-10"
overflow="scroll" overflow="scroll"
@ -51,9 +37,11 @@ const SidebarMembers = () => {
src: 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1760&q=80', 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', address: 'zQ3...9d4Gs0',
indicator: 'online', indicator: 'online',
status: 'untrustworthy',
}, },
{ {
name: 'Pedro', name: 'Pedro',
nickname: 'pedro',
src: 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1760&q=80', 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', address: 'zQ3...9d4Gs0',
indicator: 'online', indicator: 'online',
@ -63,12 +51,14 @@ const SidebarMembers = () => {
src: 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1760&q=80', 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', address: 'zQ3...9d4Gs0',
indicator: 'online', indicator: 'online',
status: 'contact',
}, },
{ {
name: 'Pedro', 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', 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', address: 'zQ3...9d4Gs0',
indicator: 'online', indicator: 'online',
status: 'verified',
}, },
{ {
name: 'Pedro', name: 'Pedro',
@ -92,6 +82,7 @@ const SidebarMembers = () => {
src: 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1760&q=80', 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', address: 'zQ3...9d4Gs0',
indicator: 'offline', indicator: 'offline',
status: 'verified',
}, },
{ {
name: 'Pedro', name: 'Pedro',
@ -110,6 +101,7 @@ const SidebarMembers = () => {
src: 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1760&q=80', 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', address: 'zQ3...9d4Gs0',
indicator: 'offline', indicator: 'offline',
status: 'verified',
}, },
{ {
name: 'Pedro', name: 'Pedro',
@ -122,6 +114,7 @@ const SidebarMembers = () => {
src: 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1760&q=80', 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', address: 'zQ3...9d4Gs0',
indicator: 'offline', indicator: 'offline',
status: 'contact',
}, },
]} ]}
/> />
@ -130,3 +123,21 @@ const SidebarMembers = () => {
} }
export { SidebarMembers } export { SidebarMembers }
type GroupProps = {
label: string
users: UserListProps['users']
}
const Group = (props: GroupProps) => {
const { label, users } = props
return (
<Stack paddingBottom={8}>
<DividerLabel label={label} tight={false} />
<Stack paddingHorizontal={8}>
<UserList users={users} />
</Stack>
</Stack>
)
}

View File

@ -6,13 +6,13 @@ 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 Topbar> = { const meta: Meta<typeof Topbar> = {
title: 'Navigation/Topbar', title: 'Community/Topbar',
component: Topbar, component: Topbar,
args: { args: {
channel: { channel: {
id: '1', id: '1',
emoji: '👋', emoji: '👋',
title: '# channel', title: 'channel',
description: 'This is a channel description', description: 'This is a channel description',
}, },
}, },
@ -35,16 +35,34 @@ export const Default: Story = {
args: {}, args: {},
} }
export const isLoading: Story = { export const Loading: Story = {
args: { args: {
...Default.args, loading: true,
isLoading: true, },
}
export const WithPinnedMessages: Story = {
args: {
pinnedMessages: [
{
text: 'Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam a, posuere eu, velit.',
reactions: {},
pinned: true,
id: '1234-1234',
},
{
text: 'Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam.',
reactions: {},
pinned: true,
id: '4321-4321',
},
],
showMembers: true,
}, },
} }
export const WithMembersSelected: Story = { export const WithMembersSelected: Story = {
args: { args: {
...Default.args,
showMembers: true, showMembers: true,
}, },
} }

View File

@ -0,0 +1,161 @@
import {
ArrowLeftIcon,
CommunitiesIcon,
DeleteIcon,
DownloadIcon,
LockedIcon,
MembersIcon,
MutedIcon,
OptionsIcon,
ShareIcon,
UpToDateIcon,
} from '@status-im/icons/20'
import { Stack, Text as RNText } from '@tamagui/core'
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 { MessageProps } from '../../messages'
import type { ChannelType } from '../mock-data'
type Props = {
showMembers: boolean
onMembersPress: () => void
goBack?: () => void
channel: ChannelType
blur?: boolean
pinnedMessages?: MessageProps[]
loading?: boolean
}
const Topbar = (props: Props) => {
const {
showMembers,
onMembersPress,
goBack,
blur,
channel,
pinnedMessages,
loading,
} = props
if (loading) {
return <TopbarSkeleton />
}
const { title, description, emoji } = channel
return (
<BlurView intensity={40} style={{ zIndex: 100 }}>
<Stack
flexDirection="row"
height={56}
alignItems="center"
justifyContent="space-between"
padding={16}
backgroundColor={'$blurBackground'}
borderBottomWidth={1}
borderColor={blur ? 'transparent' : '$neutral-80-opa-10'}
>
<Stack flexDirection="row" alignItems="center" flexWrap="wrap">
<Stack marginRight={12} $gtSm={{ display: 'none' }}>
<IconButton
icon={<ArrowLeftIcon />}
onPress={() => goBack?.()}
blur={blur}
/>
</Stack>
<Stack marginRight={12}>
<RNText>{emoji}</RNText>
</Stack>
<Text size={15} weight="semibold">
{title}
</Text>
<Stack marginLeft={4}>
<LockedIcon color="$neutral-80-opa-40" />
</Stack>
<Stack
backgroundColor="$neutral-80-opa-10"
marginHorizontal={12}
height={16}
width={1}
$sm={{ display: 'none' }}
/>
</Stack>
<Stack flexGrow={1} flexShrink={1} $sm={{ display: 'none' }}>
<Text size={13} weight="medium" color="$neutral-80-opa-50" truncate>
{description}
</Text>
</Stack>
<Stack
flexDirection="row"
alignItems="center"
justifyContent="flex-end"
gap={12}
>
<Stack $sm={{ display: 'none' }}>
<IconButton
icon={<MembersIcon />}
selected={showMembers}
onPress={onMembersPress}
blur={blur}
/>
</Stack>
<DropdownMenu>
<IconButton icon={<OptionsIcon />} />
<DropdownMenu.Content align="end" sideOffset={4}>
<DropdownMenu.Item
icon={<CommunitiesIcon />}
label="View channel details"
onSelect={() => console.log('click')}
/>
<DropdownMenu.Item
icon={<MutedIcon />}
label="Mute channel"
onSelect={() => console.log('click')}
/>
<DropdownMenu.Item
icon={<UpToDateIcon />}
label="Mark as read"
onSelect={() => console.log('click')}
/>
<DropdownMenu.Item
icon={<DownloadIcon />}
label="Fetch messages"
onSelect={() => console.log('click')}
/>
<DropdownMenu.Item
icon={<ShareIcon />}
label="Share link to the channel"
onSelect={() => console.log('click')}
/>
<DropdownMenu.Separator />
<DropdownMenu.Item
icon={<DeleteIcon />}
label="Clear history"
onSelect={() => console.log('click')}
danger
/>
</DropdownMenu.Content>
</DropdownMenu>
</Stack>
</Stack>
{pinnedMessages && pinnedMessages.length > 0 && (
<PinnedMessage messages={pinnedMessages} />
)}
</BlurView>
)
}
export { Topbar }

View File

@ -5,6 +5,7 @@ import { Counter } from './counter'
import type { Meta, StoryObj } from '@storybook/react' import type { Meta, StoryObj } from '@storybook/react'
const meta: Meta<typeof Counter> = { const meta: Meta<typeof Counter> = {
title: 'Components/Counter',
component: Counter, component: Counter,
argTypes: { argTypes: {
value: { value: {

View File

@ -1,26 +1,59 @@
import { ChevronRightIcon } from '@status-im/icons/20'
import { Stack } from '@tamagui/core' import { Stack } from '@tamagui/core'
import { Counter } from '../../counter'
import { Text } from '../../text' import { Text } from '../../text'
import { DividerLine } from '../divider-line' import { DividerLine } from '../divider-line'
import type { CounterProps } from '../../counter'
type Props = { type Props = {
label: string label: string
tight?: boolean tight?: boolean
count?: number count?: CounterProps['value']
} counterType?: CounterProps['type']
} & (
| {
type?: 'default'
}
| {
type?: 'expandable'
expanded: boolean
// ?chevronPosition?: 'left' | 'right'
}
)
// TODO: Add counter after PR #355 lands
const DividerLabel = (props: Props) => { const DividerLabel = (props: Props) => {
const { label, tight = true } = props const { label, tight = true, counterType = 'secondary', count } = props
return ( return (
<Stack paddingBottom={8} gap={tight ? 8 : 16}> <Stack paddingBottom={8} gap={tight ? 8 : 16}>
<DividerLine /> <DividerLine />
<Stack paddingHorizontal={16}> <Stack
paddingHorizontal={16}
flexDirection="row"
justifyContent="space-between"
alignItems="center"
>
<Stack flexDirection="row" alignItems="center" gap={2}>
{props.type === 'expandable' && (
<Stack
marginLeft={-2}
transform={[
{
rotate: props.expanded ? '90deg' : '0deg',
},
]}
>
<ChevronRightIcon color="$neutral-50" />
</Stack>
)}
<Text size={13} color="$neutral-50" weight="medium"> <Text size={13} color="$neutral-50" weight="medium">
{label} {label}
</Text> </Text>
</Stack> </Stack>
{count && count > 0 && <Counter type={counterType} value={count} />}
</Stack>
</Stack> </Stack>
) )
} }

View File

@ -106,4 +106,4 @@ DropdownMenu.Item = MenuItem
DropdownMenu.Separator = Separator DropdownMenu.Separator = Separator
export { DropdownMenu } export { DropdownMenu }
export type { Props as DropdownMenuProps } export type DropdownMenuProps = Omit<Props, 'children'>

View File

@ -1,5 +1,7 @@
export * from './anchor-actions' export * from './anchor-actions'
export * from './avatar'
export * from './button' export * from './button'
export * from './community'
export * from './composer' export * from './composer'
export * from './dividers' export * from './dividers'
export * from './dynamic-button' export * from './dynamic-button'
@ -11,16 +13,10 @@ 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-members'
export * from './skeleton' export * from './skeleton'
export * from './text' export * from './text'
export * from './toast' export * from './toast'
export * from './topbar'
export * from './user-list' export * from './user-list'
// MOCK DATA
export { CHANNEL_GROUPS } from './sidebar/mock-data'
// eslint-disable-next-line simple-import-sort/exports // eslint-disable-next-line simple-import-sort/exports
export { config } from './tamagui.config' export { config } from './tamagui.config'

View File

@ -1,6 +1,7 @@
import { Stack } from '@tamagui/core' import { Stack } from '@tamagui/core'
import { DividerDate, DividerNewMessages } from '../dividers' import { DividerDate, DividerNewMessages } from '../dividers'
import { MessageSkeleton } from '../skeleton'
import { PinAnnouncement } from '../system-messages' import { PinAnnouncement } from '../system-messages'
import { Message } from './message' import { Message } from './message'
@ -14,7 +15,29 @@ const reactions: ReactionsType = {
'thumbs-down': new Set(['me', '1', '2', '3']), 'thumbs-down': new Set(['me', '1', '2', '3']),
} }
export const Messages = () => { type Props = {
loading?: boolean
}
export const Messages = (props: Props) => {
const { loading } = props
if (loading) {
return (
<>
<MessageSkeleton size="large" />
<MessageSkeleton size="small" />
<MessageSkeleton size="medium" />
<MessageSkeleton size="large" />
<MessageSkeleton size="medium" />
<MessageSkeleton size="small" />
<MessageSkeleton size="large" />
<MessageSkeleton size="small" />
<MessageSkeleton size="medium" />
</>
)
}
return ( return (
<> <>
<Message <Message

View File

@ -1 +0,0 @@
export { Sidebar } from './sidebar'

View File

@ -1,51 +0,0 @@
import { Stack } from '@tamagui/core'
import { CHANNEL_GROUPS } from './mock-data'
import { Sidebar } from './sidebar'
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> = {
title: 'Sidebar/Community',
component: Sidebar,
args: {
channels: CHANNEL_GROUPS,
community: COMMUNITY,
},
argTypes: {},
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/IBmFKgGL1B4GzqD8LQTw6n/Design-System-for-Web?node-id=14692%3A148489&t=NfQkS7CPSrZknAGF-4',
},
},
}
export const Default = {
render: (args: SidebarProps) => (
<Stack width={352} height="100vh">
<Sidebar {...args} />
</Stack>
),
}
export const LoadingSidebar = {
render: (args: SidebarProps) => (
<Stack width={352} height="100vh">
<Sidebar {...args} isLoading />
</Stack>
),
}
export default meta

View File

@ -33,7 +33,8 @@ export const config = createTamagui({
}, },
}), }),
mono: createFont({ mono: createFont({
family: 'UbuntuMono', family:
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;',
weight: {}, weight: {},
letterSpacing: {}, letterSpacing: {},
size: {}, size: {},

View File

@ -1,181 +0,0 @@
import {
ArrowLeftIcon,
CommunitiesIcon,
DeleteIcon,
DownloadIcon,
LockedIcon,
MembersIcon,
MutedIcon,
OptionsIcon,
ShareIcon,
UpToDateIcon,
} from '@status-im/icons/20'
import { Stack, Text as RNText } from '@tamagui/core'
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'
const mockMessages = [
{
text: 'Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam a, posuere eu, velit.',
reactions: {},
pinned: true,
id: '1234-1234',
},
{
text: 'Morbi a metus. Phasellus enim erat, vestibulum vel, aliquam.',
reactions: {},
pinned: true,
id: '4321-4321',
},
]
type Props = {
showMembers: boolean
onMembersPress: () => void
goBack?: () => void
channel: Channel
blur?: boolean
isLoading?: boolean
}
const Topbar = (props: 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}>
<Stack
flexDirection="row"
height={56}
alignItems="center"
justifyContent="space-between"
padding={16}
backgroundColor={'$blurBackground'}
borderBottomWidth={1}
borderColor={blur ? 'transparent' : '$neutral-80-opa-10'}
width="100%"
>
<Stack flexDirection="row" alignItems="center" flexWrap="wrap">
<Stack mr={12} $gtSm={{ display: 'none' }}>
<IconButton
icon={<ArrowLeftIcon />}
onPress={() => goBack?.()}
blur={blur}
/>
</Stack>
{emoji && (
<Stack marginRight={12}>
<RNText>{emoji}</RNText>
</Stack>
)}
{title && (
<Text size={15} weight="semibold">
{title}
</Text>
)}
<LockedIcon color="$neutral-80-opa-40" />
<Stack
backgroundColor="$neutral-80-opa-10"
marginHorizontal={12}
height={16}
width={1}
$sm={{ display: 'none' }}
/>
</Stack>
<Stack
space={12}
flexDirection="row"
alignItems="center"
justifyContent={description ? 'space-between' : 'flex-end'}
flexGrow={1}
flexShrink={1}
$sm={{ justifyContent: 'flex-end' }}
>
{description && (
<Stack flexGrow={1} flexShrink={1} $sm={{ display: 'none' }}>
<Text
weight="medium"
color="$neutral-80-opa-50"
size={13}
truncate
>
{description}
</Text>
</Stack>
)}
<Stack $sm={{ display: 'none' }}>
<IconButton
icon={<MembersIcon />}
selected={showMembers}
onPress={onMembersPress}
blur={blur}
/>
</Stack>
<DropdownMenu>
<IconButton icon={<OptionsIcon />} />
<DropdownMenu.Content align="end" sideOffset={4}>
<DropdownMenu.Item
icon={<CommunitiesIcon />}
label="View channel details"
onSelect={() => console.log('click')}
/>
<DropdownMenu.Item
icon={<MutedIcon />}
label="Mute channel"
onSelect={() => console.log('click')}
/>
<DropdownMenu.Item
icon={<UpToDateIcon />}
label="Mark as read"
onSelect={() => console.log('click')}
/>
<DropdownMenu.Item
icon={<DownloadIcon />}
label="Fetch messages"
onSelect={() => console.log('click')}
/>
<DropdownMenu.Item
icon={<ShareIcon />}
label="Share link to the channel"
onSelect={() => console.log('click')}
/>
<DropdownMenu.Separator />
<DropdownMenu.Item
icon={<DeleteIcon />}
label="Clear history"
onSelect={() => console.log('click')}
danger
/>
</DropdownMenu.Content>
</DropdownMenu>
</Stack>
</Stack>
<PinnedMessage messages={mockMessages} />
</Stack>
</BlurView>
)
}
export { Topbar }

View File

@ -17,11 +17,13 @@ const UserList = (props: Props) => {
return ( return (
<YStack> <YStack>
{users.map((user, index) => { {users.map((user, index) => {
const { src, indicator, ...authorProps } = user
return ( return (
<XStack <XStack
key={user.address! + index} key={user.address! + index}
padding={8} padding={8}
space={8} gap={8}
borderRadius={12} borderRadius={12}
alignItems="center" alignItems="center"
cursor="pointer" cursor="pointer"
@ -29,15 +31,10 @@ const UserList = (props: Props) => {
backgroundColor: '$primary-50-opa-5', backgroundColor: '$primary-50-opa-5',
}} }}
> >
<Avatar size={32} src={user.src} indicator={user.indicator} /> <Avatar size={32} src={src} indicator={indicator} />
<YStack> <YStack>
<Author <Author {...authorProps} />
name={user.name} <Text size={13} color="$neutral-50" type="monospace">
nickname={user.nickname}
status={user.status}
orientation="vertical"
/>
<Text size={13} color="$neutral-50">
{user.address} {user.address}
</Text> </Text>
</YStack> </YStack>

View File

@ -18,7 +18,7 @@ const SvgNotificationIcon = (props: IconProps) => {
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
{...props} {...props}
> >
<Circle cx={10} cy={10} r={4} fill="#4360DF" /> <Circle cx={10} cy={10} r={4} fill={color} />
</Svg> </Svg>
) )
} }

View File

@ -3321,6 +3321,22 @@
dependencies: dependencies:
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-accordion@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-accordion/-/react-accordion-1.1.1.tgz#fa1ab1b5c6a29aa75aefaf306a9e72fe3a482dbc"
integrity sha512-TQtyyRubYe8DD6DYCovNLTjd2D+TFrNCpr99T5M3cYUbR7BsRxWsxfInjbQ1nHsdy2uPTcnJS5npyXPVfP0piw==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.0"
"@radix-ui/react-collapsible" "1.0.2"
"@radix-ui/react-collection" "1.0.2"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-context" "1.0.0"
"@radix-ui/react-direction" "1.0.0"
"@radix-ui/react-id" "1.0.0"
"@radix-ui/react-primitive" "1.0.2"
"@radix-ui/react-use-controllable-state" "1.0.0"
"@radix-ui/react-arrow@1.0.2": "@radix-ui/react-arrow@1.0.2":
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.0.2.tgz#93b0ff95f65e2264a05b14ef1031ec798243dd6f" resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.0.2.tgz#93b0ff95f65e2264a05b14ef1031ec798243dd6f"
@ -3329,6 +3345,21 @@
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.2" "@radix-ui/react-primitive" "1.0.2"
"@radix-ui/react-collapsible@1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-collapsible/-/react-collapsible-1.0.2.tgz#0583470c7caa8cd1ab6f606416288d19b3baf777"
integrity sha512-QNiDT6Au8jUU0K1WV+HEd4loH7C5CKQjeXxskwqyiyAkyCmW7qlQM5vSSJCIoQC+OVPyhgafSmGudRP8Qm1/gA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.0"
"@radix-ui/react-compose-refs" "1.0.0"
"@radix-ui/react-context" "1.0.0"
"@radix-ui/react-id" "1.0.0"
"@radix-ui/react-presence" "1.0.0"
"@radix-ui/react-primitive" "1.0.2"
"@radix-ui/react-use-controllable-state" "1.0.0"
"@radix-ui/react-use-layout-effect" "1.0.0"
"@radix-ui/react-collection@1.0.2": "@radix-ui/react-collection@1.0.2":
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.0.2.tgz#d50da00bfa2ac14585319efdbbb081d4c5a29a97" resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.0.2.tgz#d50da00bfa2ac14585319efdbbb081d4c5a29a97"
@ -6383,7 +6414,7 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
"@types/react-dom@18.0.11", "@types/react-dom@^18.0.11": "@types/react-dom@^18.0.11":
version "18.0.11" version "18.0.11"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.11.tgz#321351c1459bc9ca3d216aefc8a167beec334e33" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.11.tgz#321351c1459bc9ca3d216aefc8a167beec334e33"
integrity sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw== integrity sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==
@ -6397,7 +6428,7 @@
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react@*", "@types/react@18.0.28", "@types/react@>=16", "@types/react@^18.0.28": "@types/react@*", "@types/react@>=16", "@types/react@^18.0.28":
version "18.0.28" version "18.0.28"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.28.tgz#accaeb8b86f4908057ad629a26635fe641480065" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.28.tgz#accaeb8b86f4908057ad629a26635fe641480065"
integrity sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew== integrity sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==
@ -9504,10 +9535,10 @@ expo-asset@~8.7.0:
path-browserify "^1.0.0" path-browserify "^1.0.0"
url-parse "^1.5.9" url-parse "^1.5.9"
expo-blur@~12.0.1: expo-blur@^12.2.2:
version "12.0.1" version "12.2.2"
resolved "https://registry.yarnpkg.com/expo-blur/-/expo-blur-12.0.1.tgz#7aa4186620359acfa976dda84360070b634ffe3d" resolved "https://registry.yarnpkg.com/expo-blur/-/expo-blur-12.2.2.tgz#b7f94499255afbd3468302d02f3c4e39a0e562d5"
integrity sha512-7oF/xRIFJukM4/qL6ejZ4Z/4YcVExvBPsBrz7rGYz6PtgAkWwYFR62+ExZOzTEG4hgoPPmlnt1ncimsk/MYUgQ== integrity sha512-SvGbEZbB0VFNGqCW7FcqzWOEb3lrRgBnQKGrsKo49KwhMyHTYjYVYWnmrk9l8Tr7lIaNnd55QD6dPAzcXjZYMg==
expo-constants@^14.0.2, expo-constants@~14.0.0, expo-constants@~14.0.2: expo-constants@^14.0.2, expo-constants@~14.0.0, expo-constants@~14.0.2:
version "14.0.2" version "14.0.2"