Skip to main content

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/ui at packages/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.tsx files 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 implementation
  • Component.test.tsx — Co-located tests
  • index.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>
);
}
warning

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