import type { IconName } from '@fortawesome/fontawesome-svg-core'
import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'
import React, { useState } from 'react'
import { Alert, Pressable, ScrollView, StyleSheet, View } from 'react-native'
import type { ViewStyle } from 'react-native'
import {
Avatar,
AvatarGroup,
Badge,
Banner,
BannerCollapsible,
Button,
ConversationAvatar,
EmojiAvatar,
FilterDropdown,
IconAvatar,
ToggleButton,
Heading,
Icon,
IconButton,
Image,
Person,
Spinner,
Switch,
Text,
TextButton,
TextInlineButton,
ImageAttachmentPreview,
} from '../components/display'
import { VideoAttachmentPreview } from '../components/display/video_attachment_preview'
import BannerPrimitive from '../components/primitive/banner_primitive'
import BlankState from '../components/primitive/blank_state_primitive'
import { useSelectedChipBackgroundColor, useTheme } from '../hooks'
import {
MAX_FONT_SIZE_MULTIPLIER,
platformPressedOpacityStyle,
platformFontWeightMedium,
Haptic,
} from '../utils'
// =================================
// ====== Docs Utils ===============
// =================================
const URL = {
image: 'https://picsum.photos/seed/picsum/200',
broken: 'https://broken.url',
avatar: 'https://i.pravatar.cc/200?img=22',
avatar_fallback: 'https://avatars.planningcenteronline.com/uploads/initials/PR.png',
two_avatars: ['https://i.pravatar.cc/200?img=22', 'https://i.pravatar.cc/200?img=23'],
three_avatars: [
'https://i.pravatar.cc/200?img=22',
'https://i.pravatar.cc/200?img=23',
'https://i.pravatar.cc/200?img=24',
],
four_avatars: [
'https://i.pravatar.cc/200?img=30',
'https://i.pravatar.cc/200?img=29',
'https://i.pravatar.cc/200?img=28',
'https://i.pravatar.cc/200?img=27',
],
}
const personAdult = {
id: 1,
type: 'Member' as const,
name: 'John Doe',
avatar: URL.avatar,
badges: [{ title: 'Conversation owner' }, { title: 'Leader' }],
child: false,
gender: null,
}
const personChild = {
id: 2,
type: 'Member' as const,
name: 'Kid McChild',
avatar: URL.two_avatars[1],
badges: [],
child: true,
gender: null,
}
const buttonPress = () => Alert.alert('Button clicked')
const filterDropdownOptions = [
{ id: 'all', label: 'All conversations' },
{ id: 'groups', label: 'Groups' },
{ id: 'teams', label: 'Teams' },
{ id: 'dms', label: 'DMs' },
{ id: 'unread', label: 'Unread' },
]
// =================================
// ====== Component ================
// =================================
export function DesignSystemScreen() {
const styles = useStyles()
return (
)
}
// =================================
// ====== Sections =================
// =================================
interface SectionProps {
isLast?: boolean
}
function ThemeSection({ isLast }: SectionProps) {
return (
There are four main parts to our theming system…
Default theme
Start at `src/utils/theme` when adding new theme values or checking what target apps
have access to. The file has more instructions and examples.
At a high level, it provides…
Access to consuming app targets of what theme values that they can be customized.
Fallback values for our components to use if the values weren't overriden.
Customizing the theme
Apps can override any default theme value by passing a `theme` object to our
`ChatProvider` that holds a `theme` and a `colorScheme`.
Currently types can be enforced by setting the parent theme object to
`CreateChatThemeProps`.
Example setup: `apps/sandbox/src/context/chat_context_provider.tsx`
Merged theme
In `src/contexts/chat_context.tsx` we merge the default theme and any custom values
coming from a product target with the `useCreateChatTheme` hook. It creates a single
`ChatTheme` type.
Using theme values
Inside of our own `chat-react-native` components we can access the merged `ChatTheme`
object via our own `useTheme` hook.
Example setup: `src/components/display/button.tsx`
)
}
function IndicatorsSection({ isLast }: SectionProps) {
return (
)
}
function HeadingTextSection({ isLast }: SectionProps) {
const styles = useStyles()
return (
Heading 1
Heading 2
Heading 3
Heading 4
Plain text
Secondary
Tertiary
Footnote
)
}
function PressablesSection({ isLast }: SectionProps) {
const styles = useStyles()
const [filterSelected, setFilterSelected] = useState('Groups')
const [dropdownSelected, setDropdownSelected] = useState('all')
return (
setFilterSelected('Groups')}
title="Groups"
active={filterSelected === 'Groups'}
accessibilityLabel="Filter to group conversations"
/>
setFilterSelected('Teams')}
title="Teams"
active={filterSelected === 'Teams'}
accessibilityLabel="Filter to team conversations"
/>
setFilterSelected('More')}
title="More"
active={filterSelected === 'More'}
iconNameRight="general.threeReducingHorizontalBars"
accessibilityLabel="View more filters"
/>
Alert.alert('More filters')}
triggerLabel={
filterDropdownOptions.find(option => option.id === dropdownSelected)?.label ?? ''
}
/>
Default
Default
Default
Default
Danger
Danger
Danger
Danger
Disabled
Disabled
Disabled
Disabled
This text is next to{' '}
default button text Lorem
ipsum dolor{' '}
danger button text
{' '}
consectetur{' '}
disabled button text
{' '}
elit.
This text is next to{' '}
default button text
{' '}
Lorem ipsum dolor{' '}
danger button text
{' '}
consectetur{' '}
disabled button text
{' '}
elit.
This text is next to{' '}
default button text
{' '}
Lorem ipsum dolor{' '}
danger button text
{' '}
consectetur{' '}
disabled button text
{' '}
elit.
This text is next to{' '}
default button text
{' '}
Lorem ipsum dolor{' '}
danger button text
{' '}
consectetur{' '}
disabled button text
{' '}
elit.
)
}
function HapticsSection({ isLast }: SectionProps) {
return (
Haptics help communicate changes and interactions in the UI by making them feel physical
and "weighty". They are a progressive enhancement and shouldn't replace visual feedback.
Haptic.impactLight()} />
Haptic.impactMedium()} />
Haptic.impactHeavy()} />
Haptic.rigid()} />
Haptic.soft()} />
Haptic.notificationSuccess()} />
Haptic.notificationWarning()} />
Haptic.notificationError()} />
)
}
function ImageIconsSection({ isLast }: SectionProps) {
const styles = useStyles()
const { colors } = useTheme()
return (
{/* @ts-expect-error - Testing missing icon fallback */}
)
}
function FormControlsSection({ isLast }: SectionProps) {
const [switchOn, setSwitchOn] = useState(true)
const [switchOff, setSwitchOff] = useState(false)
return (
setSwitchOn(value)} />
setSwitchOff(value)} />
)
}
function StatusComponentsSection({ isLast }: SectionProps) {
return (
Et sint Quis non excepturi{' '}
enim et conseq atur porro est
{' '}
galisum labore ea volupt.
}
/>
Et sint Quis non excepturi enim et conseq atur porro est galisum labore ea volupt.
Et sint Quis non excepturi enim et conseq atur porro est galisum labore ea volupt.
Et sint Quis non excepturi{' '}
enim et conseq atur porro est
{' '}
galisum labore ea volupt.
More hdden content that can be revealed on a tap. Lorem ipsum dolor sit amet,
consectetur
Et sint Quis non excepturi enim et conseq atur porro est galisum labore ea volupt. Et
sint Quis non excepturi enim et conseq atur porro est galisum labore ea volupt. Et
sint Quis non excepturi enim et .
)
}
const FA_ICONS: { category: string; icons: IconName[] }[] = [
{
category: 'Church',
icons: [
'book-bible',
'church',
'cross',
'dove',
'hands-heart',
'person-rays',
'praying-hands',
'podium',
'droplet',
'person-drowning',
'lighthouse',
],
},
{
category: 'Services',
icons: [
'guitar',
'guitar-electric',
'violin',
'calendar-heart',
'hand-heart',
'hand-wave',
'baby',
'children',
'mug-hot',
'video-camera',
'music',
'piano-keyboard',
'drum',
'user-graduate',
'comments-question',
'presentation',
'microphone-stand',
'id-badge',
'book-user',
'hand-holding-heart',
'guitars',
'amp-guitar',
'school-flag',
'shield-check',
'projector',
],
},
{
category: 'Groups',
icons: [
'people',
'people-group',
'person-dress',
'person',
'seedling',
'comments-alt',
'globe',
'book-open-reader',
'running',
'leaf',
],
},
]
function FontAwesomeIconsSection({ isLast }: SectionProps) {
const { colors } = useTheme()
return (
{FA_ICONS.map(({ category, icons }) => (
{icons.map(name => (
{name}
))}
))}
)
}
const faStyles = StyleSheet.create({
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
},
cell: {
width: '25%',
alignItems: 'center',
gap: 4,
paddingVertical: 12,
},
})
function MiscComponentsSection({ isLast }: SectionProps) {
return (
No messages
Conversation is hidden from everyone until you send a message.
)
}
// =================================
// ====== Docs UI ==================
// =================================
interface CollapsableSectionProps {
children: React.ReactNode
title: string
isLast?: boolean
}
function CollapsableSection({ children, title, isLast = false }: CollapsableSectionProps) {
const styles = useStyles()
const { colors } = useTheme()
const [collapsed, setCollapsed] = useState(true)
return (
setCollapsed(!collapsed)}
style={({ pressed }) => [styles.sectionHeader, pressed && platformPressedOpacityStyle]}
android_ripple={{ color: colors.androidRippleNeutral, borderless: false, foreground: true }}
>
{title}
{!collapsed && {children}}
)
}
interface GroupProps {
title: string
description?: string
children: React.ReactNode
}
function Group({ title, description, children }: GroupProps) {
const styles = useStyles()
return (
{title}
{description && {description}}
{children}
)
}
interface RowProps {
children: React.ReactNode
style?: ViewStyle
}
function Row({ children, style }: RowProps) {
const styles = useStyles()
return {children}
}
interface ColumnProps {
children: React.ReactNode
style?: ViewStyle
}
function Column({ children, style }: ColumnProps) {
const styles = useStyles()
return {children}
}
function TextGroup({ children }: { children: React.ReactNode }) {
const styles = useStyles()
return {children}
}
interface TextRowProps {
children: React.ReactNode
style?: ViewStyle
}
function TextRow({ children, style }: TextRowProps) {
const styles = useStyles()
return {children}
}
interface TextListItemProps {
label: string
children: React.ReactNode
}
function TextListItem({ label, children }: TextListItemProps) {
const styles = useStyles()
return (
{label}
{' '}
{children}
)
}
function HapticButton({ title, onPress }: { title: string; onPress: () => void }) {
const styles = useStyles()
return (
({
...styles.hapticButton,
...{ opacity: pressed ? 0.5 : 1 },
})}
>
{title}
)
}
// =================================
// ====== Styles ===================
// =================================
const useStyles = () => {
const { colors } = useTheme()
const selectedChipBackgroundColor = useSelectedChipBackgroundColor()
return StyleSheet.create({
hapticButton: {
flexGrow: 1,
height: 100,
padding: 16,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: selectedChipBackgroundColor,
borderWidth: 1,
borderColor: colors.interaction,
borderRadius: 8,
},
hapticButtonText: {
textAlign: 'center',
color: colors.interaction,
},
scrollView: {
flex: 1,
backgroundColor: colors.fillColorNeutral100Inverted,
},
container: {
padding: 24,
},
listItem: { color: colors.fillColorNeutral020 },
section: {
gap: 8,
borderColor: colors.fillColorNeutral020,
},
sectionBorderTop: {
borderTopWidth: 1,
},
sectionBorderBottom: {
borderBottomWidth: 1,
},
sectionHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 24,
},
sectionChildren: {
paddingBottom: 24,
gap: 48,
},
row: {
gap: 16,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
flexWrap: 'wrap',
},
column: {
gap: 16,
},
columnCenter: {
alignItems: 'center',
},
textRow: {
gap: 12,
},
textGroup: {
gap: 20,
},
mediumWeight: {
fontWeight: platformFontWeightMedium,
},
alignRowLeft: {
justifyContent: 'flex-start',
},
group: {
gap: 24,
},
groupHeading: {
gap: 8,
},
image: {
width: 100,
height: 100,
},
icon: {
fontSize: 20,
color: colors.iconColorDefaultSecondary,
},
customIconButtonColor: {
color: 'purple',
},
})
}