This is the React Native + Expo mobile app for PearPass, written in JavaScript + TypeScript. UI is built on the shared component library @tetherto/pearpass-lib-ui-kit.
This document is for anyone contributing UI to the repo — new hires, current engineers, and AI coding assistants (Claude Code, Cursor, Codex, etc.). It captures the component catalog, styling conventions, and patterns we use when building UI in this app. Read it once before your first UI change; keep it open when you're in doubt.
- Check the catalog below before creating any component. If it exists in the kit, use it — never wrap or reimplement.
- All new UI goes through
@tetherto/pearpass-lib-ui-kit. New.tsx/.jsxfiles must import from the kit, not from src/libComponents/. That tree is legacy and is being phased out — migrate callers to the kit equivalents when you touch them. - Never add variants under src/libComponents/ (
ButtonThin,ButtonPrimary,ButtonLittle,InputPasswordPearPass, etc.). The kit'sButtontakes variants — use it. - Style with tokens.
useTheme()+rawTokens. No hardcoded hex colors or design-system spacing. - Icons come from the kit.
@tetherto/pearpass-lib-ui-kit/iconshas 530 icons. Do not add new SVGs undersrc/. - If the kit lacks something you need, stop and ask. Don't silently roll a custom component.
Import pattern: import { ComponentName } from '@tetherto/pearpass-lib-ui-kit'
Button— all CTAs. Takes variants; use instead ofButtonThin,ButtonPrimary,ButtonSecondary,ButtonFilter,ButtonLittle,ButtonCreate,ButtonUniversal.Pressable— low-level pressable wrapper for custom interactive elements.Link— text links.
Form— form wrapper; pair withuseFormfrom@tetherto/pear-apps-lib-ui-react-hooks.InputField— text input. Use instead of the legacyInputField/PearPassInputField.PasswordField— password input with strength indicator. Use instead of legacyPasswordField/InputPasswordPearPass.SearchField,SelectField,Dropdown,TextAreaCheckbox,Radio,ToggleSwitch,SliderDateField,AttachmentField,UploadFieldMultiSlotInput— split inputs for OTP / recovery codes.FieldError— inline field validation error.
Title— headings.Text— body text.
Dialog— modals.NativeBottomSheet— bottom sheets.PageHeader,ItemScreenHeader,Breadcrumb,ListItem,NavbarListItem,ContextMenu.
AlertMessage,Snackbar,PasswordIndicator,RingSpinner.
ThemeColors,Theme,ThemeType,RawTokensPasswordIndicatorVariant—'vulnerable' | 'decent' | 'strong'ButtonVariant—'primary' | 'secondary' | 'tertiary' | 'destructive'ButtonSize—'small' | 'medium'
Import types with import type { ... } from '@tetherto/pearpass-lib-ui-kit'.
For components not listed, open node_modules/@tetherto/pearpass-lib-ui-kit/dist/components/<Name>/types.d.ts.
Required props have no ?. Always include a testID on interactive components (buttons, fields, toggles, dialogs).
- Button —
variant: 'primary' | 'secondary' | 'tertiary' | 'destructive',size?: 'small' | 'medium',onClick,children,type?: 'button' | 'submit',disabled?,isLoading?,fullWidth?,iconBefore?,iconAfter?,testID?. Icon-only buttons needaria-label. - Dialog —
title(ReactNode),onClose?,open?,footer?,children?,closeOnOutsideClick?,hideCloseButton?,trapFocus?,initialFocusRef?,testID?,closeButtonTestID?. Put action buttons infooter. - InputField —
label,value,onChangeText?: (v: string) => void,placeholder?,error?: string,inputType?: 'text' | 'password',disabled?,readOnly?,copyable?,onCopy?,leftSlot?,rightSlot?,testID?. - PasswordField —
label,value,onChangeText?,placeholder?,error?,passwordIndicator?: 'vulnerable' | 'decent' | 'strong' | 'match',infoBox?: string,copyable?,testID?. - SearchField —
value,onChangeText,placeholderText?,size?: 'small' | 'medium',testID?. - Form —
children,onSubmit?,noValidate?,testID?. - Text —
children,variant?: 'label' | 'labelEmphasized' | 'body' | 'bodyEmphasized' | 'caption',color?,numberOfLines?. - Title —
children,as?: 'h1' | 'h2' | ... | 'h6'. - AlertMessage —
variant: 'info' | 'warning' | 'error',size: 'small' | 'medium' | 'big',title,description(ReactNode),actionText?,onAction?,backgroundColor?,color?,testID?,actionTestId?. - ToggleSwitch, Checkbox —
checked?,onChange?: (b: boolean) => void,label?,description?,disabled?,testID?. - Radio —
options: Array<{value, label?, description?, disabled?}>,value?,onChange?: (v: string) => void,testID?. - SelectField —
label,value?,placeholder?,onClick?(opens dropdown),error?,disabled?,leftSlot?,rightSlot?,testID?. - TextArea —
value,onChange?,label?,placeholder?,error?,disabled?,testID?. - Link —
children,href?,isExternal?,onClick?,testID?.
The kit's TypeScript defs mark onChangeText / placeholderText / errorMessage as @deprecated in favour of onChange / placeholder / error. That's a web-centric note. The onChange signature the kit declares is (e: React.ChangeEvent<HTMLInputElement>) => void, which React Native does not produce.
For this mobile repo: keep using onChangeText (receives a string) on InputField, PasswordField, SearchField, and TextArea. That's what the kit's .native implementations actually call, and it's what every existing file in this repo does. placeholder and error (string) are fine to use directly. testID is current everywhere.
No styled-components. No createStyles(colors) factory. The mobile convention is:
StyleSheet.create({ ... })defined once per file (usually at the bottom), usingrawTokensfor numeric values.- Color tokens applied inline via
style={[styles.x, { borderColor: theme.colors.xxx }]}— because colors depend on the active theme.
Example (from src/containers/Modal/ModifyVaultModalContent/index.jsx):
import { rawTokens, useTheme } from '@tetherto/pearpass-lib-ui-kit'
import { StyleSheet, View } from 'react-native'
export const Component = () => {
const { theme } = useTheme()
return (
<View
style={[
styles.container,
{
backgroundColor: theme.colors.colorSurfacePrimary,
borderColor: theme.colors.colorBorderPrimary,
},
]}
>
…
</View>
)
}
const styles = StyleSheet.create({
container: {
borderWidth: 1,
borderRadius: rawTokens.spacing20,
padding: rawTokens.spacing16,
gap: rawTokens.spacing16,
},
})Factoring styles into a sibling styles.ts module (e.g. src/screens/CreateFolder/styles.ts) is also fine for larger screens — keep the StyleSheet.create + rawTokens pattern.
In React Native, spacing / radius / font-size values are numbers — no px suffix, no template strings.
- Spacing:
spacing2,spacing4,spacing6,spacing8,spacing10,spacing12,spacing16,spacing20,spacing24,spacing32,spacing40,spacing48 - Radius:
radius8,radius16,radius20,radius26 - Font size:
fontSize12,fontSize14,fontSize16,fontSize24,fontSize28 - Font family:
fontPrimary("Inter"),fontDisplay("Humble Nostalgia") - Weight:
weightRegular("400"),weightMedium("500")
Usage: borderRadius: rawTokens.radius8, padding: rawTokens.spacing16, gap: rawTokens.spacing12. No `${n}px`.
colorSurfacePrimary, colorSurfaceHover, colorBorderPrimary, colorBorderSecondary, colorTextPrimary, colorTextSecondary, colorTextTertiary, colorLinkText. If you need one you haven't seen, inspect the ThemeColors type from @tetherto/pearpass-lib-ui-kit.
Tokens cover the design-system primitives. Feature-specific layout values (a card's maxWidth: 480, a one-off paddingBottom: 55) are fine as numeric literals — these aren't design tokens. Rule of thumb: if the value corresponds to a semantic design decision (spacing step, brand color, radius), it must come from a token.
import { Add, Download, Folder, OpenInNew } from '@tetherto/pearpass-lib-ui-kit/icons'530 icons, mostly Material Design, with style variants as suffixes: Filled, Outlined, Round, Sharp, Tone (e.g. LockFilled, InfoOutlined, KeyboardArrowRightRound). If a name has no suffix, it exists as a single variant.
Commonly used in this repo (check these first before browsing):
- Actions:
Add,Download,ContentCopy,Share,Send - Folder / organization:
Folder,FolderOpen,CreateNewFolder,LayerFilled - Navigation / arrows:
KeyboardArrowRightFilled,KeyboardArrowLeftFilled,KeyboardArrowBottom,ExpandMore - Status / feedback:
InfoOutlined,ReportProblem,ErrorFilled,Check - Security:
LockOutlined,Key,SecurityFilled,Fingerprint - Settings:
SettingsApplicationsFilled,PaletteOutlined,Translate,Sync,Devices,SystemSecurityUpdateFilled - Misc:
Logout,Login,HubFilled,BugReportFilled,OpenInNew
Discovering others: ls node_modules/@tetherto/pearpass-lib-ui-kit/dist/icons/components/ | grep -i <keyword> — names are PascalCase, grep is case-insensitive friendly.
When creating or editing UI, do not:
- Add a new file under src/libComponents/ for a Button/Input variant.
- Import from src/libComponents/ in a new file — swap to the kit equivalents.
- Use
TouchableOpacity,TouchableHighlight, or a raw<Text>fromreact-nativefor interactive UI when the kit'sButton/Pressable/Textfit. RN primitives for layout (View,StyleSheet,ScrollView,FlatList) are fine and expected. - Hardcode hex colors, brand radii, or design-system spacing — use
rawTokensandtheme.colors. (Feature-specific layout literals likemaxWidth: 480are fine.) - Add
pxsuffixes or template-string units to token values — React Native takes numbers. - Add new SVG files under
src/when the kit's icons subpath covers them. - Introduce
styled-components— the convention isStyleSheet.create+rawTokenswith colors applied inline.
If you encounter a file still importing from src/libComponents/, migrate it to the kit when the change is in scope; don't do drive-by rewrites outside your task.
- src/screens/Settings/Vaults/index.jsx — kit imports,
Layout+PageHeader+Buttonfooter,StyleSheet.createat the bottom. - src/containers/Modal/ModifyVaultModalContent/index.jsx — form with
InputField/PasswordField,AlertMessage, fulltestIDdiscipline, inline color theming. - src/screens/CreateRecord/CreatePasswordItem.tsx — password generator,
Radio/Slider/ToggleSwitchusage.
- Confirm by grepping
node_modules/@tetherto/pearpass-lib-ui-kit/dist/components/for the concept. - Check if a composition of existing kit primitives covers it (e.g.
Pressable+Text+ tokens). - If still missing, surface it to the user: "The kit doesn't export X — options are (a) compose from Y + Z, (b) request X be added upstream, (c) temporary local component. Which?" Do not silently create (c).