Skip to main content

Component Library (Primitives)

ComponentFilePurpose
BaseCardsrc/components/primitives/BaseCard.tsxBase card wrapper with consistent styling
BaseModalsrc/components/primitives/BaseModal.tsxBase modal with backdrop, animation
BasePickerModalsrc/components/primitives/BasePickerModal.tsxModal with picker/selection UI
SettingRowsrc/components/primitives/SettingRow.tsxSettings list row
SettingSectionsrc/components/primitives/SettingSection.tsxSettings group section

Common Components

ComponentPurpose
ContentDetailViewShared detail view layout for recipes/posts
ContentDetailSkeletonLoading skeleton for detail views
DetailViewHelpersShared helper functions (utility module, not a component)
ImageCarousel / PhotoCarouselSwipeable image carousel with pagination dots
ImageLightboxFull-screen zoomable image viewer
ZoomableImagePinch-to-zoom image component
ImageMasonryGridImage-only masonry grid
EmptyStateEmpty state placeholder
ErrorStateError state with retry
LoadingStateLoading spinner/skeleton
ErrorBoundary / ScreenErrorBoundaryError boundary wrappers
RelativeTime”2 hours ago” timestamp display
TruncatedCaptionExpandable text with “more”
PrivacyBadgePublic/private indicator
DismissKeyboardTap-to-dismiss keyboard wrapper
ConfirmDeleteModalDestructive action confirmation
LinkedContentCardCard showing linked recipe/collection
ImagePlaceholder / ImageWithPlaceholderImage loading states
PaginationDotsCarousel page indicators
HeartAnimationAnimated heart for double-tap like
UserCardUser card for follower/following lists and search results
SplashScreenCustom animated splash screen — plays after native splash hides, deep links deferred until complete

Auth Components

AuthGuard (src/components/auth/AuthGuard.tsx) — wraps screens that require authentication.

Theme System

Three-mode theme: light, dark, system (follows device).
PiecePurpose
ThemeProvider (src/providers/ThemeProvider.tsx)Manages theme state
useTheme hookReturns colors, isDark, resolvedTheme, themeMode, setThemeMode, toggleTheme
PersistenceAsyncStorage with key recipe-room-theme-mode
ThemeSelectorSettings component for user selection (light/dark/system)

Color Palette

Mobile uses its own LIGHT_COLORS / DARK_COLORS (slate-based palette) rather than the shared package design tokens. Light Mode:
TokenHexUsage
background#ffffffScreen backgrounds
foreground#0f172aPrimary text, icons
card#ffffffCard backgrounds
primary#1e293bButtons, active states
primaryForeground#f8fafcText on primary
secondary#f1f5f9Metadata pills, secondary buttons
muted#f1f5f9Input backgrounds, skeleton base
mutedForeground#64748bPlaceholder text, inactive icons
destructive#ef4444Delete actions, errors
border#e2e8f0Dividers, card borders
input#f1f5f9Text input backgrounds
Dark Mode:
TokenHexUsage
background#0f1115Deep space grey screen backgrounds
foreground#e8eaedPrimary text, icons
card#1c2127Card backgrounds
primary#e8eaedButtons, active states (inverted)
primaryForeground#0f1115Text on primary
secondary#2d3748Metadata pills, secondary buttons
muted#3d4654Input backgrounds, skeleton base
mutedForeground#94a3b8Placeholder text, inactive icons
destructive#7f1d1dDelete actions, errors
border#1c2127Matches card bg for seamless cards
input#1c2127Text input backgrounds
The shared package (packages/shared/src/constants/theme.ts) exports a complete design token system with lightTheme / darkTheme objects (red primary scale, neutral scale, semantic colors). The mobile ThemeProvider uses its own separate color system. This inconsistency is worth reviewing during the audit.

Iconography

All icons use lucide-react-native. Key icons across the app:
IconUsage
PlusCreate content (header left button on all main tabs)
SparklesDiscover tab (bottom bar), filled when active
SearchSearch tab (bottom bar), heavier stroke when active
BookmarkSaved tab (bottom bar, filled when active), save button on cards/detail views
UserProfile tab fallback (when no avatar), user-related UI
HeartLike/favorite button (red #dc2626 when active, filled), Popular tab icon
MessageCircleComment button and count
SendShare button on detail views
FilmRecipe type indicator, film simulation pill
ImageIconPost type indicator
LibraryBigCollection type indicator, collections tab on profile
CompassDiscover sub-tab icon
UsersCreator search tab
Globe / LockPublic/private collection indicators
FolderOpenEmpty collection placeholder
ChevronLeftBack navigation (other user’s profile → own profile)
XClose/dismiss (settings header, search clear)
Mail / Lock / LogOut / Trash2Settings account section icons
AlertCircle / CheckUsername availability indicators
CameraCamera model metadata pill

Bottom Tab Bar Icons

TabIconActive State
HomeCustom PNG (home_black.png / home_white.png)Tinted with foreground color
DiscoverSparklesFilled
SearchSearchstrokeWidth: 3 (vs 2 inactive)
SavedBookmarkFilled
ProfileUser’s avatar via ExpoImage (28px circle)Falls back to User icon with strokeWidth: 3
Hidden tabs for unauthenticated users: Home (index), Saved.

ContentCard Variants

ContentCard supports 5 variants:
VariantLayoutUsed In
defaultFull overlay: title, film sim, author avatar+name, like/comment/repost countsGeneral content display
profileTop gradient: title + film sim. Bottom: type icon + labelProfile grids (not currently used — profile uses imageOnly)
feedTop gradient: title + film sim. Bottom-left: author avatar. Bottom-right: like + save buttonsFeed cards (superseded by FeedPostCard)
minimalTop gradient: title + film sim. Bottom: type icon + label. No author/save/likesMinimal display
imageOnlyCover image only. Optional Film icon badge top-right for recipesDiscover grid, search results, profile grids

TypeBadge

Pill-shaped badge with icon + label on semi-transparent black background (rgba(0,0,0,0.5)). Types: recipe (Film), post (ImageIcon), collection (Folder), repost (Repeat2). Two sizes: sm (10px icon) and md (12px icon).

MetadataPills

Horizontal scrollable row of pill badges on detail views. Pill types:
  • Content type: icon + label (not clickable)
  • Film simulation: Film icon + name (clickable → opens search)
  • Camera model: Camera icon + name (clickable → opens search)
  • Tags: #tag text with border outline (clickable → opens search)

FeedPostCard

Instagram-style vertical card used in the Following feed:
SectionDetails
ImageFull-width, dynamic height (60%–125% screen width based on aspect ratio). Multi-image posts use ImageCarousel with pagination dots.
Author overlayTop-left: avatar (32px) + username + type icon (Film/ImageIcon) on gradient background
Double-tapTriggers like with animated white heart (80px, scale up + fade out)
Action rowHeart (with count), MessageCircle (with count), SaveButton — left-aligned
Linked recipeBelow actions: secondary-colored tag with Film icon + recipe title (clickable)
CaptionUsername (bold, clickable) + caption text (truncated at 120 chars)
Comments link”View all N comments” (clickable, opens comment modal)
TimestampRelative time (“2 hours ago”, “3 days ago”, etc.)

DetailActionBar

Action bar on recipe/post detail views:
  • Left side: Heart (with count, red when liked), MessageCircle (with count)
  • Right side: Send (share), SaveButton (authenticated only)
  • Repost button is intentionally hidden (kept in code for future use)
  • Like redirects to login for unauthenticated users

SaveButton

Bookmark icon that opens the save modal:
  • Uses useSyncExternalStore for efficient re-renders (only re-renders when this specific item’s loading state changes)
  • Reads save status from SaveStatusContext cache first, falls back to useSavedItemIds set (populated from DB on cold start)
  • Shows ActivityIndicator while saving
  • Filled bookmark when saved, outline when not
  • Hidden for unauthenticated users on detail views

FollowButton

Managed at the component level (not via hook optimistic updates). The FollowButton component maintains local isFollowing state and reverts on error. See Follows journey for details.

Skeleton & Loading Patterns

ComponentUsed InLayout
SkeletonBase building blockRounded rectangle with muted background at 70% opacity
MasonryGridSkeletonDiscover, search, profile gridsTwo-column grid of 300px-tall grey blocks
ProfilePageSkeletonProfile tab loadingCentered avatar (120px circle), name/username, 3-column stats, bio lines, action button, 3 tab icons, masonry grid
ContentDetailSkeletonRecipe/post/collection detail loadingHeader (avatar + name + close), metadata pills, title/description block, action bar icons, optional tabs, two-column masonry
FeedPostCardSkeletonFollowing feed loadingFull-width image area with author overlay skeleton, action row icons, caption lines, timestamp
FeedSkeletonFollowing feed initial loadTwo FeedPostCardSkeleton stacked
ContentCardSkeletonIndividual card loadingSingle grey block at specified height
ProfileHeaderSkeletonProfile header loadingCover photo, avatar, name, bio, stats row
LoadingStateGeneric full-screen loadingCentered ActivityIndicator
ButtonSpinnerButton loading statesSmall ActivityIndicator (white by default)
InfiniteScrollLoaderPagination loadingCentered small ActivityIndicator with vertical padding

EmptyState Variants

Base EmptyState component: icon (in muted circle) + title + description + optional action button. Pre-configured variants:
  • NoSearchResultsSearch icon, “No results found”
  • NoSavedItemsBookmark icon, optional “Browse recipes” action
  • NoFollowersUsers icon
  • NoFollowingUsers icon, optional “Discover creators” action
  • NoRecipesCamera icon, optional “Create recipe” action
  • NoPostsImageIcon icon, optional “Create post” action
  • NoCollectionsFolder icon, optional “Create collection” action

ErrorState Variants

Base ErrorState component: icon (in destructive-tinted circle) + title + message + optional retry button.
TypeIconTitle
genericAlertCircle”Something went wrong”
networkWifiOff”No connection”
notFoundFileQuestion”Not found”
serverServerCrash”Server error”

Toast System

PiecePurpose
ToastProvider (src/providers/ToastProvider.tsx)Manages toast notification state
useToast hookReturns showToast(message, type?)
Typessuccess (green), error (red), info (blue) — defaults to info
AnimationAnimated slide-in/out with Reanimated
Used across all CRUD operations for success/error feedback.

Stack Navigation Configuration

The root layout (app/_layout.tsx) configures Expo Router’s Stack navigator:
ScreenConfig
Defaultslide_from_right, gestureEnabled: true, fullScreenGestureEnabled: true (iOS full-screen back swipe)
Tabsanimation: 'none', gestureEnabled: false (prevents swiping back from tabs)
Modals (save, create)presentation: 'containedTransparentModal', animation: 'fade', transparent background
SaveModal is rendered outside the Stack (always mounted in root, controlled by SaveProvider).

Common Hooks

HookFilePurpose
useAuthuseAuth.tsAuth state, user, isAuthenticated
useAuthOverlayuseAuthOverlay.tsAuth-gated action handling
useThemeuseTheme.tsTheme colors, isDark, resolvedTheme
useDebounceuseDebounce.tsDebounce values
useHapticsuseHaptics.tsHaptic feedback
useInfiniteScrolluseInfiniteScroll.tsInfinite scroll pagination
useScrollPositionuseScrollPosition.tsScroll position tracking
useReducedMotionuseReducedMotion.tsAccessibility: reduced motion preference
useImageDimensionsuseImageDimensions.tsImage dimension calculation
useImageUploaduseImageUpload.tsImage upload with progress
useCommentModaluseCommentModal.tsComment modal state
useMentionInputuseMentionInput.ts@mention input logic
useProfileUpdateuseProfileUpdate.tsProfile edit mutations
useApiErrorHandleruseApiErrorHandler.tsStandardized API error handling
useUsernameCheckuseUsernameCheck.tsDebounced username availability via RPC
useToastuseToast.tsToast notifications

Context Providers

ProviderFilePurpose
AuthProviderproviders/AuthProvider.tsxSession management, user state
ThemeProviderproviders/ThemeProvider.tsxDark/light/system theme
ToastProviderproviders/ToastProvider.tsxToast notifications
ProfileProvidercontext/ProfileContext.tsxProfile navigation state
SaveProvidercontext/SaveContext.tsxSave modal state, auto-save
SearchProvidercontext/SearchContext.tsxSearch query/results state
AuthOverlayProvidercontext/AuthOverlayContext.tsxAuth-gated action interception
TabBarScrollContextcontext/TabBarScrollContext.tsxTab bar hide/show animation

Utility Modules

UtilityFilePurpose
deepLinkingutils/deepLinking.tsUniversal link + custom scheme handling
imageOptimizationutils/imageOptimization.tsClient-side image resize/compress
imagePreloadingutils/imagePreloading.tsPreload images for smooth UX
flatListOptimizationutils/flatListOptimization.tsFlatList performance tuning
masonry / masonryPairing / imageMasonryGridutils/masonry*.tsMasonry layout algorithms
mentionsutils/mentions.ts@mention parsing/extraction
metadataPillsutils/metadataPills.tsMetadata pill generation
recipeSettingsutils/recipeSettings.tsRecipe settings formatting
textUtilsutils/textUtils.tsText truncation, formatting
accessibilityutils/accessibility.tsAccessibility helpers
connectionFiltersutils/connectionFilters.tsFollower/following list filtering
contentDetailViewutils/contentDetailView.tsDetail view helpers
errorLoggingutils/errorLogging.tsError logging utilities
lruCacheutils/lruCache.tsLRU cache implementation
memoizationutils/memoization.tsMemoization utilities

Constants

  • src/constants/layout.ts — Layout dimensions, spacing
  • src/constants/accessibility.ts — Accessibility constants
  • src/constants/index.ts — Barrel export

Shared Package (packages/shared)

ModulePathContents
Typessrc/types/database.tsDatabase types (snake_case)
src/types/api.tsAPI response types (camelCase)
src/types/forms.tsForm types
Validationsrc/validation/index.tsZod schemas (registerSchema, loginSchema, recipeSchema, postSchema, postUpdateSchema, collectionSchema, etc.)
APIsrc/api/client.tsPlatform-agnostic ApiClient class. Auto-prepends /api. credentials: 'include'. Includes its own ApiError class and buildQueryString().
src/api/queryKeys.tsReact Query key factories. Domains: auth, users, recipes, posts, collections, saved, favorites, reposts, creators, notifications, search.
src/api/supabase.tsPlatform-agnostic Supabase client factory. StorageAdapter interface, MemoryStorage fallback.
Constantssrc/constants/routes.tsWEB_ROUTES (path-based) and MOBILE_ROUTES (Expo Router names), both as const.
src/constants/theme.tsDesign tokens: colors, spacing, borderRadius, fontSize, fontWeight, lineHeight, lightTheme/darkTheme.
src/constants/index.tsFilm simulation options, camera model arrays, dynamic range options, grain effect options, sort options, tag suggestions.
Utilssrc/utils/tagSuggestions.tsTag autocomplete suggestions
Both packages/shared/src/api/client.ts and apps/mobile/src/lib/api.ts define their own ApiError class. The shared version is generic/platform-agnostic. The mobile version has convenience getters (isUnauthorized, isNotFound, isServerError) and is tightly integrated with the mobile auth flow. Import the correct one for your context.

Denormalized Counter System

All engagement counts are denormalized into parent tables for fast reads. Triggers keep them in sync. The validate_all_counters RPC can audit all counters and report mismatches.

Cross-Screen Cache Sync Architecture

This is the most critical pattern in the mobile app. When a user favorites, comments, saves, or follows, the change must appear instantly on every screen — detail views, feed cards, masonry grids, profile stats — without a full refetch that would cause layout jumps.

The Pattern

Every engagement mutation hook follows this structure:
onMutate (BEFORE the API call):
  1. Cancel all outgoing refetches (prevents overwriting optimistic state)
  2. Snapshot previous state for every affected cache entry
  3. Walk every page of every infinite query and update the matching item
  4. Update detail view cache

mutationFn:
  Direct Supabase call (supabase.from().insert/delete or supabase.rpc())

onError:
  Rollback every cache entry to its snapshot

onSettled:
  Varies by action (see below)

Invalidation Strategy Per Action

ActiononSettled BehaviorWhy
Favorite (recipe/post)NO invalidationInvalidating causes masonry grid to re-render and items to jump/disappear
CommentNO invalidation (count updated optimistically)Same reason — list views would flash
Save/unsaveNarrow invalidation: specific item status + saved items listSave state is per-item, doesn’t affect grid layout
Collection membershipNarrow invalidation: item status + collection detailOnly affects the specific collection view
RepostinvalidateQueries({ queryKey: queryKeys.recipes.all })Less latency-sensitive than heart animation
Follow/unfollowInvalidate users.profile, users.followers, users.following, all ['profile', 'data'] queriesProfile data needs fresh counts; no optimistic update
Create/delete contentInvalidate recipes.all, collections.all, saved.allNew content needs to appear in feeds

Which Caches Get Updated Per Action

ActionDetail ViewRecipe ListsPost ListsMixed FeedsSave StatusProfile
Favorite recipeisFavorited, favoriteCount✅ all pages✅ all pages
Favorite postisFavorited, favoriteCount✅ all pages
Add commentcommentCountcomment_count all pagescomment_count all pages
Delete commentcommentCount -1comment_count -1 all pagescomment_count -1 all pages
Save item✅ toggle
Follow user✅ counts

Data Access: Direct Supabase, Not Web API Routes

The mobile app calls Supabase directly via supabase.rpc() and supabase.from(). It does NOT route through the Next.js API routes in apps/web. The web API routes exist for the web client (Phase 2). The mobile hooks in src/hooks/api/ are the source of truth for how mobile data flows.
Mobile HookSupabase Call
useDiscoverContentsupabase.rpc('get_discover_content_list')
usePosts (following)supabase.rpc('get_follow_content_list')
usePostImages(id)supabase.from('post_images').select(...) (lazy-load on carousel swipe)
useRecipe(id)supabase.rpc('get_recipe_detail')
usePost(id)supabase.rpc('get_post_detail')
useCreateRecipesupabase.rpc('create_recipe')
useUpdateRecipe(id)supabase.rpc('update_recipe')
useDeleteRecipesupabase.rpc('delete_recipe') + supabase.storage.from('recipe-images').remove()
useCreatePostsupabase.rpc('create_post')
useUpdatePost(id)supabase.rpc('update_post')
useDeletePostsupabase.rpc('delete_post')
useToggleFavoritesupabase.from('recipe_favorites').insert/delete
useTogglePostLikesupabase.from('post_favorites').insert/delete
useToggleRepostsupabase.from('recipe_reposts').insert/delete
useToggleStandalonesupabase.from('saved_items').insert/delete
useToggleCollectionMembershipsupabase.from('collection_saved_items').insert/delete
useAddCommentsupabase.rpc('create_recipe_comment') / supabase.rpc('create_post_comment')
useDeleteCommentsupabase.rpc('delete_recipe_comment') / supabase.rpc('delete_post_comment')
useCommentssupabase.rpc('get_comments')
useFollowUsersupabase.from('follows').insert
useUnfollowUsersupabase.from('follows').delete
useToggleFollowsupabase.from('follows').insert/delete (convenience wrapper)
useFollowers(userId)supabase.rpc('get_user_followers')
useFollowing(userId)supabase.rpc('get_user_following')
useSaveStatussupabase.from('saved_items').select + supabase.from('collection_saved_items').select
useSavedItemIdssupabase.from('saved_items').select → returns Set<string> for O(1) lookup
useSavedItemssupabase.from('saved_items').select + batch fetch recipes/posts
useSavedPageDatasupabase.rpc('get_saved_page_data')
useUncategorizedSavedItemssupabase.rpc('get_uncategorized_saved_items')
useCollectionssupabase.from('collections').select(...) (direct query, not RPC)
useCollection(id)supabase.rpc('get_collection_detail')
useCreateCollectionsupabase.from('collections').insert(...)
useUpdateCollectionsupabase.from('collections').update(...)
useDeleteCollectionsupabase.from('collections').delete(...) (hard delete)
useUserCollectionssupabase.from('collections').select(...) (for ContentSelector)
useToggleSaveToCollectionsupabase.from('collection_saved_items').insert/delete (in useCollections.ts)
useProfileData(username)supabase.rpc('get_user_profile_data')
useProfileDataById(userId)supabase.rpc('get_user_profile_data_by_id')
useProfileCollectionssupabase.rpc('get_user_profile_collections')
useUserProfile(userId)supabase.from('profiles').select + supabase.from('follows').select
useTabAvatar(userId)supabase.from('profiles').select('avatar_url')
useSearchUsers(query)supabase.from('profiles').select(...) with .or() filter

Denormalized Counter Table

CounterTableMaintained By Trigger On
recipes.favorite_countrecipesrecipe_favorites INSERT/DELETE
recipes.comment_countrecipesrecipe_comments INSERT/DELETE
recipes.repost_countrecipesrecipe_reposts INSERT/DELETE
posts.favorite_countpostspost_favorites INSERT/DELETE
posts.comment_countpostspost_comments INSERT/DELETE
posts.repost_countpostspost_reposts INSERT/DELETE
profiles.recipe_countprofilesrecipes INSERT/UPDATE/DELETE
profiles.post_countprofilesposts INSERT/UPDATE/DELETE
profiles.collection_countprofilescollections INSERT/UPDATE/DELETE
profiles.follower_countprofilesfollows INSERT/DELETE
profiles.following_countprofilesfollows INSERT/DELETE
collections.item_countcollectionscollection_saved_items INSERT/DELETE
The existing docs and project rules reference recipe_with_stats and collection_with_stats views — these do NOT exist in the database. All stats are denormalized directly on the parent tables. RPC functions handle joins inline.