Shared UI Library
The @gospelib/ui package is a cross-platform React Native component library shared across the web, admin, and mobile apps. This guide covers the design system, core components, and how to add new components.
Architecture
- Package:
@gospelib/uiatpackages/ui/ - Primitives: React Native components (
View,Text,Pressable) - Cross-platform: Works on mobile (React Native) and web (React Native Web)
- Design tokens: Centralized in
theme/tokens.ts - Testing: Co-located
*.test.tsxfiles with Vitest
Package Structure
packages/ui/src/
├── components/
│ ├── ScriptureText/
│ │ ├── ScriptureText.tsx
│ │ ├── ScriptureText.test.tsx
│ │ └── index.ts
│ ├── WordToken/
│ │ ├── WordToken.tsx
│ │ ├── WordToken.test.tsx
│ │ └── index.ts
│ ├── ManuscriptView/
│ ├── GraphNode/
│ ├── PassageCard/
│ └── ...
├── primitives/ # Base design system (Button, Text, etc.)
├── theme/
│ └── tokens.ts # Design tokens (colors, spacing, typography)
└── index.ts # Barrel export
Each component follows the same structure:
Component.tsx— The component implementationComponent.test.tsx— Co-located testsindex.ts— Barrel export
Design Tokens
All visual constants are defined in theme/tokens.ts:
Colors
export const colors = {
// Brand
brand: {
blue: '#2C5F8A',
gold: '#8B6914',
green: '#3D6B4F',
},
// Testament-specific
testament: {
ot: '#8B6914', // Old Testament — gold
nt: '#2C5F8A', // New Testament — blue
bom: '#3D6B4F', // Book of Mormon — green
dc: '#6B4F8A', // D&C — purple
pgp: '#8B6947', // Pearl of Great Price — brown
},
// Semantic
text: {
primary: '#1a1a1a',
secondary: '#666666',
muted: '#999999',
},
};
Typography
export const typography = {
scripture: {
fontFamily: 'Georgia, serif',
fontSize: 18,
lineHeight: 28,
},
heading: {
fontFamily: 'Inter, system-ui, sans-serif',
fontWeight: '700' as const,
},
body: {
fontFamily: 'Inter, system-ui, sans-serif',
fontSize: 16,
lineHeight: 24,
},
};
Spacing
export const spacing = {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32,
xxl: 48,
};
Core Components
ScriptureText
Renders a scripture verse with optional word-level interactivity:
import { ScriptureText } from '@gospelib/ui';
<ScriptureText
text="In the beginning God created the heaven and the earth."
words={wordAlignments}
onWordPress={(word) => showLexiconPopover(word.strongs)}
/>;
When words are provided, each token becomes tappable, showing the original-language form and Strong's number.
WordToken
A single tappable word token with original-language popup:
import { WordToken } from '@gospelib/ui';
<WordToken
gloss="God"
token="אֱלֹהִ֑ים"
strongs="H0430"
onPress={() => navigateToLexicon('H0430')}
/>;
ManuscriptView
Side-by-side manuscript witness comparison:
import { ManuscriptView } from '@gospelib/ui';
<ManuscriptView
witnesses={[
{ language: 'ethiopic', text: '...', witness: 'Tana 9' },
{ language: 'aramaic', text: '...', witness: '4QEn^a' },
{ language: 'greek', text: '...', witness: 'Synkellos' },
]}
/>;
PassageCard
A compact card displaying a passage reference and preview:
import { PassageCard } from '@gospelib/ui';
<PassageCard
reference="Genesis 1:1"
text="In the beginning God created the heaven and the earth."
corpus="ot"
onPress={() => router.push('/passage/gen.1.1')}
/>;
The corpus prop applies testament-specific accent colors.
Adding a New Component
1. Create the component directory
packages/ui/src/components/NewComponent/
├── NewComponent.tsx
├── NewComponent.test.tsx
└── index.ts
2. Implement the component
Use React Native primitives for cross-platform compatibility:
// NewComponent.tsx
import { View, Text, Pressable, type ViewProps } from 'react-native';
import { colors, spacing } from '../../theme/tokens';
interface NewComponentProps extends ViewProps {
title: string;
onPress?: () => void;
}
export function NewComponent({ title, onPress, style, ...props }: NewComponentProps) {
return (
<Pressable onPress={onPress} style={[{ padding: spacing.md }, style]} {...props}>
<Text style={{ color: colors.text.primary }}>{title}</Text>
</Pressable>
);
}
Do not use web-specific elements (div, span, p) or web-specific CSS (className, Tailwind classes). Use React Native primitives (View, Text, Pressable) and inline styles or StyleSheet for cross-platform compatibility.
3. Write tests
// NewComponent.test.tsx
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react-native';
import { NewComponent } from './NewComponent';
describe('NewComponent', () => {
it('renders the title', () => {
render(<NewComponent title="Hello" />);
expect(screen.getByText('Hello')).toBeTruthy();
});
});
4. Export from the barrel
// NewComponent/index.ts
export { NewComponent } from './NewComponent';
// packages/ui/src/index.ts
export { NewComponent } from './components/NewComponent';
Consuming in Apps
Web / Admin (Next.js)
Add @gospelib/ui to transpilePackages in next.config.ts:
const nextConfig: NextConfig = {
transpilePackages: ['@gospelib/ui'],
};
React Native components work on the web through React Native Web, which is included as a dependency of @gospelib/ui.
Mobile (Expo)
Add the package to metro.config.js watch folders:
config.watchFolders = [path.resolve(__dirname, '../../packages/ui')];
React Version Compatibility
The UI library must work with both:
- React 19 (web and admin apps)
- React 18.3 (mobile app, constrained by Expo 52)
Avoid React 19-only APIs (e.g., use(), useActionState()). Stick to hooks and patterns that work in both versions.
Verify It Works
# Run UI library tests
cd packages/ui && pnpm test
# Type check
cd packages/ui && pnpm typecheck
# Test in web app
cd apps/web && pnpm dev
# Visit http://localhost:3002 and verify component renders
# Test in mobile app
cd apps/mobile && npx expo start