React Native UI conventions for this repo. Add component, styling, navigation, animation, and Expo Router guidance here when it becomes stable enough to apply across the app.
For visual design principles (colors, layout, typography, when to use cards), see docs/design.md.
Every color must come from the theme object in src/constants/theme.ts. Never hardcode a hex or rgba value directly in a component.
Use theme.neutral for the white/read-only canvas, separators, and neutral copy. Use theme.control for interactive surfaces and their foregrounds. Use theme.palette.<hue> for adaptive accent text, icons, borders, and small tinted states rendered by React Native. Metric visuals use theme.telemetry. Raw theme.palette.slate values are fixed dark swatches for map JSON and other non-adaptive assets.
The adaptive neutral, control, accent, and telemetry tokens are native color objects at runtime. Mapbox, Skia, Reanimated worklets, and string-valued state/options cannot consume them. Use the corresponding hooks from src/hooks/useTheme.ts: useResolvedNeutralColors(), useResolvedControlColors(), useResolvedAccentColors(), and useResolvedTelemetryColors(). Pass only their plain string values into the renderer or data structure.
Filled actions use the resolved hue's solid background and onSolid content pair. Do not infer the foreground from color, text, or a fixed black/white value; the pair is contrast-checked separately for each appearance.
import { theme } from '@/constants/theme'
// ✅ Good
backgroundColor: theme.control.background,
color: theme.telemetry.speed,
// ❌ Bad
backgroundColor: '#1e293b',
color: '#38bdf8',Do not create index.ts barrel files under src/components/ or any of its subdirectories.
- Import components directly from their source file:
import { Foo } from '@/components/Foo'orimport { Foo } from '@/components/settings/Foo'. - Barrel files add indirection, slow down TypeScript resolution, and create merge conflicts when multiple agents touch the same index file.
When creating or significantly changing a reusable UI component, add or update its showcase in
src/app/settings/components.tsx in the same change.
- When asked to create a component, create a real component file under
src/components/or the appropriate subdirectory such assrc/components/settings/. Do not hide reusable UI as a function at the top of a screen file. - Use existing
ShowcaseCardand controls from@/components/dev/ShowcaseControls. - Include useful variants, states, and props that future agents/design checks need to see.
- Keep showcase data local and deterministic enough for quick visual inspection.
- Skip only components that are route-specific screens or tiny private sub-components with no reuse surface.
Use phosphor-react-native for all icons. Do not use emoji or unicode characters as icon substitutes.
import { LightningIcon, WarningCircleIcon } from 'phosphor-react-native'
import { theme } from '@/constants/theme'
;<LightningIcon size={16} color={theme.gps.text} weight="fill" />- Always use the
Icon-suffixed export, for exampleLightningIcon, notLightning. The un-suffixed names are deprecated and will produce warnings. - The
type Iconexport for typing icon props is not suffixed; import it astype Iconas-is. sizeis typically10-16for inline or label icons, larger for standalone UI elements.
Use IconButton (@/components/IconButton) for all circular icon-only pressables. Do not build ad-hoc Pressable + icon combinations.
import { IconButton } from '@/components/IconButton'
import { ArrowLeftIcon, TrashIcon } from 'phosphor-react-native'
<IconButton icon={ArrowLeftIcon} onPress={handleBack} />
<IconButton icon={TrashIcon} destructive onPress={handleDelete} disabled={!canDelete} />
<IconButton icon={ArrowsClockwiseIcon} size="lg" loading={syncing} onPress={handleSync} />size:'sm'(default, 38×38 — headers, overlays) |'lg'(54×54 — bottom/content area)destructiveshifts border to red-tinted and auto-tints icon totheme.error.text— no manual color neededloadingdisables the button and shows anActivityIndicatorin the icon colorstyleaccepts layout-levelViewStyle(position, margin, bottom/top/left/right)- The default surface uses
theme.control.background,theme.control.border, andtheme.control.icon; destructive state changes the accent without abandoning the navy interaction language.
Use Button (@/components/Button) for all tappable button actions. Do not build ad-hoc Pressable + Text combinations for buttons.
import { Button } from '@/components/Button'
import { TrashIcon } from 'phosphor-react-native'
<Button label="Save" onPress={handleSave} />
<Button label="Cancel" variant="secondary" onPress={handleCancel} />
<Button label="Delete" variant="destructive" icon={TrashIcon} onPress={handleDelete} />
<Button label="Saving…" loading={isSaving} onPress={handleSave} />variant:'primary'(default, blue fill) |'secondary'(ghost/outline) |'destructive'(red fill)size:'md'(default, h40) |'sm'(h32)icon: phosphorIcontype — rendered left of the labelloadingdisables the button and shows anActivityIndicatorstyleaccepts layout-levelViewStyle(e.g.flex: 1, margins) — do not use it for visual overrides
Use ConfirmModal (@/components/ConfirmModal) instead of Alert.alert for all confirmation prompts. Alert.alert renders a plain OS dialog that looks out of place in the dark-themed UI.
import { ConfirmModal } from '@/components/ConfirmModal'
const [confirmVisible, setConfirmVisible] = useState(false)
<ConfirmModal
visible={confirmVisible}
title="Delete item"
message="This cannot be undone."
confirmLabel="Delete"
destructive
onConfirm={() => { deleteItem(); setConfirmVisible(false) }}
onCancel={() => setConfirmVisible(false)}
/>- Drive visibility with state (
useState<boolean>oruseState<T | null>when you need to remember what to confirm). - Set
destructivefor irreversible actions — it renders the confirm button in red. confirmLabelandcancelLabeldefault to "Confirm" / "Cancel"; override when a specific verb is clearer.