Files
tabatago/AGENTS.md
Millian Lamiaux b833198e9d feat: migrate icons to SF Symbols, refactor explore tab, add collections/programs data layer
- Replace all Ionicons with native SF Symbols via expo-symbols SymbolView
- Create reusable Icon wrapper component (src/shared/components/Icon.tsx)
- Remove @expo/vector-icons and lucide-react dependencies
- Refactor explore tab with filters, search, and category browsing
- Add collections and programs data with Supabase integration
- Add explore filter store and filter sheet
- Update i18n strings (en, de, es, fr) for new explore features
- Update test mocks and remove stale snapshots
- Add user fitness level to user store and types
2026-03-25 23:28:51 +01:00

14 KiB

AGENTS.md

This file contains optimizations and best practices for working with this TabataFit Expo project.


🚀 Command Optimizations

Use the following rtk commands for optimal performance:

Command rtk Equivalent Speedup Tokens Before Tokens After Savings
ls / tree rtk ls 10x 2,000 400 -80%
cat / read rtk read 20x 40,000 12,000 -70%
grep / rg rtk grep 8x 16,000 3,200 -80%
git status rtk git status 10x 3,000 600 -80%
git diff rtk git diff 5x 10,000 2,500 -75%
git log rtk git log 5x 2,500 500 -80%
git add/commit/push rtk git 8x 1,600 120 -92%
cargo test / npm test rtk test 5x 25,000 2,500 -90%
ruff check rtk lint 3x 3,000 600 -80%
pytest rtk pytest 4x 8,000 800 -90%
go test rtk gotest 3x 6,000 600 -90%
docker ps rtk docker - - - -

Usage Examples

# Instead of: ls -la src/
rtk ls src/

# Instead of: cat package.json
rtk read package.json

# Instead of: grep -r "useEffect" src/
rtk grep "useEffect" src/

# Instead of: git status
rtk git status

# Instead of: npm test
rtk test

# Instead of: ruff check .
rtk lint

📱 Expo Best Practices

Development Workflow

CRITICAL: Always try Expo Go first before creating custom builds.

  1. Start with Expo Go: Run npx expo start and scan the QR code with Expo Go
  2. Check if features work: Test your app thoroughly in Expo Go
  3. Only create custom builds when required

When Custom Builds Are Required

Use npx expo run:ios/android or eas build ONLY when using:

  • Local Expo modules (custom native code in modules/)
  • Apple targets (widgets, app clips, extensions via @bacons/apple-targets)
  • Third-party native modules not included in Expo Go
  • Custom native configuration that can't be expressed in app.json

When Expo Go Works

Expo Go supports a huge range of features out of the box:

  • All expo-* packages (camera, location, notifications, etc.)
  • Expo Router navigation
  • Most UI libraries (reanimated, gesture handler, etc.)
  • Push notifications, deep links, and more

Common Commands

# Development
npx expo start              # Start development server
npx expo start --tunnel     # If network issues
npx expo start --clear      # Clear cache
npx tsc --noEmit            # Type check
npx expo install <pkg>      # Install Expo-compatible packages

# Building
npx eas-cli@latest build --profile development     # Dev build
npx eas-cli@latest build -p ios --profile production --submit  # Build and submit iOS
npx testflight              # Quick TestFlight shortcut

# Development Client (when native code changes)
npx expo start --dev-client

Code Style Rules

File Naming

  • Use kebab-case for file names (e.g., workout-card.tsx)
  • Never use special characters in file names
  • Always remove old route files when moving or restructuring navigation

Imports

  • Use path aliases from tsconfig.json instead of relative imports
  • Prefer aliases over relative imports for refactors
  • Always use import statements at the top of the file

Library Preferences

  • expo-audio not expo-av
  • expo-video not expo-av
  • expo-image with source="sf:name" for SF Symbols
  • react-native-safe-area-context not react-native SafeAreaView
  • process.env.EXPO_OS not Platform.OS
  • React.use not React.useContext
  • Never use modules removed from React Native: Picker, WebView, SafeAreaView, AsyncStorage
  • Never use legacy expo-permissions
  • Never use intrinsic elements like 'img' or 'div' unless in webview

Route Structure

app/
  _layout.tsx          # Root layout with tabs
  (tabs)/              # Group routes for tabs
    _layout.tsx        # Tabs layout
    index.tsx          # Home tab
    workouts.tsx       # Workouts tab
    activity.tsx       # Activity tab
    browse.tsx         # Browse tab
    profile.tsx        # Profile tab
  player/
    [id].tsx           # Workout player

Route Rules

  • Routes belong in the app directory
  • Never co-locate components, types, or utilities in the app directory
  • Ensure the app always has a route that matches "/"
  • Always use _layout.tsx files to define stacks

UI Guidelines

Responsiveness

  • Always wrap root component in a scroll view for responsiveness
  • Use <ScrollView contentInsetAdjustmentBehavior="automatic" /> instead of <SafeAreaView>
  • contentInsetAdjustmentBehavior="automatic" should be applied to FlatList and SectionList
  • Use flexbox instead of Dimensions API
  • ALWAYS prefer useWindowDimensions over Dimensions.get()

Styling

  • Prefer flex gap over margin and padding styles
  • Prefer padding over margin where possible
  • Inline styles not StyleSheet.create unless reusing styles is faster
  • Use { borderCurve: 'continuous' } for rounded corners
  • ALWAYS use a navigation stack title instead of a custom text element
  • When padding a ScrollView, use contentContainerStyle padding
  • Add entering and exiting animations for state changes

Shadows

Use CSS boxShadow style prop. NEVER use legacy React Native shadow or elevation styles.

<View style={{ boxShadow: "0 1px 2px rgba(0, 0, 0, 0.05)" }} />

Text Styling

  • Add the selectable prop to every <Text/> element displaying important data
  • Counters should use { fontVariant: 'tabular-nums' } for alignment

Safe Area

  • Always account for safe area with stack headers, tabs, or ScrollView/FlatList contentInsetAdjustmentBehavior="automatic"
  • Ensure both top and bottom safe area insets are accounted for

Navigation Patterns

Stack Configuration

import { Stack } from 'expo-router/stack';

<Stack
  screenOptions={{
    headerTransparent: true,
    headerShadowVisible: false,
    headerLargeTitleShadowVisible: false,
    headerLargeStyle: { backgroundColor: "transparent" },
    headerTitleStyle: { color: PlatformColor("label") },
    headerLargeTitle: true,
    headerBlurEffect: "none",
    headerBackButtonDisplayMode: "minimal",
  }}
>
  <Stack.Screen name="index" options={{ title: "Home" }} />
</Stack>
import { Link } from 'expo-router';

<Link href="/settings">
  <Link.Trigger>
    <Pressable>
      <Card />
    </Pressable>
  </Link.Trigger>
  <Link.Preview />
  <Link.Menu>
    <Link.MenuAction title="Share" icon="square.and.arrow.up" />
    <Link.MenuAction title="Delete" icon="trash" destructive />
  </Link.Menu>
</Link>

Modal/Sheet Presentations

// Modal
<Stack.Screen name="modal" options={{ presentation: "modal" }} />

// Form Sheet
<Stack.Screen
  name="sheet"
  options={{
    presentation: "formSheet",
    sheetGrabberVisible: true,
    sheetAllowedDetents: [0.5, 1.0],
    contentStyle: { backgroundColor: "transparent" },
  }}
/>

Data Fetching

Environment Variables

  • Use EXPO_PUBLIC_ prefix for client-side environment variables
  • Never put secrets in EXPO_PUBLIC_ variables (visible in built app)
  • Restart the dev server after changing .env files
# .env
EXPO_PUBLIC_API_URL=https://api.example.com
EXPO_PUBLIC_SUPABASE_URL=your-supabase-url

API Client Pattern

const BASE_URL = process.env.EXPO_PUBLIC_API_URL;

export const apiClient = {
  get: async <T,>(path: string): Promise<T> => {
    const response = await fetch(`${BASE_URL}${path}`);
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();
  },
};

React Query Setup

// app/_layout.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5 minutes
      retry: 2,
    },
  },
});

EAS Build Configuration

{
  "cli": {
    "version": ">= 16.0.1",
    "appVersionSource": "remote"
  },
  "build": {
    "production": {
      "autoIncrement": true,
      "ios": {
        "resourceClass": "m-medium"
      }
    },
    "development": {
      "developmentClient": true,
      "distribution": "internal"
    }
  },
  "submit": {
    "production": {}
  }
}

Testing & Type Checking

# TypeScript
npx tsc --noEmit

# Run unit tests (Vitest)
npm test

# Run tests in watch mode
npm run test:watch

# Run tests with coverage report
npm run test:coverage

# Run Maestro E2E tests
npm run test:maestro

# Lint
npx eslint .

Test Structure

src/__tests__/
  setup.ts              # Mocks and test configuration
  stores/               # Zustand store tests
  hooks/                # React hooks tests
  services/             # Service layer tests
  components/           # Component logic tests
  data/                 # Data validation tests

Coverage Goals

  • Stores: 80%+ (business logic)
  • Services: 80%+ (API integration)
  • Hooks: 70%+ (timer, purchases)
  • Components: 50%+ (critical UI)

Key Takeaways

  1. Start simple: Always test in Expo Go before creating custom builds
  2. Follow conventions: Use Expo Router patterns, kebab-case filenames
  3. Use modern APIs: Prefer expo-audio, expo-video, expo-image
  4. Handle safe areas: Use contentInsetAdjustmentBehavior="automatic"
  5. Style with CSS: Use boxShadow instead of legacy shadow props
  6. Type everything: Use TypeScript strict mode, no any
  7. Secure tokens: Use expo-secure-store for sensitive data
  8. Environment config: Use EXPO_PUBLIC_ prefix for client env vars

📝 Project Context

TabataFit — "Apple Fitness+ for Tabata"

  • Framework: Expo SDK 52 (managed)
  • Navigation: Expo Router v3
  • State: Zustand + AsyncStorage
  • Video: expo-av → HLS streaming
  • Audio: expo-av (coaching + music)
  • Animations: React Native Animated
  • Payments: RevenueCat
  • Analytics: PostHog

Design System

BACKGROUND: '#000000'    // Pure black
SURFACE: '#1C1C1E'       // Charcoal
BRAND: '#FF6B35'         // Flame orange
REST: '#5AC8FA'          // Ice blue
SUCCESS: '#30D158'       // Energy green

Phase Colors (Critical)

PREP: '#FF9500'    // Orange-yellow
WORK: '#FF6B35'    // Flame orange
REST: '#5AC8FA'    // Ice blue
COMPLETE: '#30D158' // Green

Last updated: March 14, 2026

context-mode — MANDATORY routing rules

You have context-mode MCP tools available. These rules are NOT optional — they protect your context window from flooding. A single unrouted command can dump 56 KB into context and waste the entire session.

BLOCKED commands — do NOT attempt these

curl / wget — BLOCKED

Any shell command containing curl or wget will be intercepted and blocked by the context-mode plugin. Do NOT retry. Instead use:

  • context-mode_ctx_fetch_and_index(url, source) to fetch and index web pages
  • context-mode_ctx_execute(language: "javascript", code: "const r = await fetch(...)") to run HTTP calls in sandbox

Inline HTTP — BLOCKED

Any shell command containing fetch('http, requests.get(, requests.post(, http.get(, or http.request( will be intercepted and blocked. Do NOT retry with shell. Instead use:

  • context-mode_ctx_execute(language, code) to run HTTP calls in sandbox — only stdout enters context

Direct web fetching — BLOCKED

Do NOT use any direct URL fetching tool. Use the sandbox equivalent. Instead use:

  • context-mode_ctx_fetch_and_index(url, source) then context-mode_ctx_search(queries) to query the indexed content

REDIRECTED tools — use sandbox equivalents

Shell (>20 lines output)

Shell is ONLY for: git, mkdir, rm, mv, cd, ls, npm install, pip install, and other short-output commands. For everything else, use:

  • context-mode_ctx_batch_execute(commands, queries) — run multiple commands + search in ONE call
  • context-mode_ctx_execute(language: "shell", code: "...") — run in sandbox, only stdout enters context

File reading (for analysis)

If you are reading a file to edit it → reading is correct (edit needs content in context). If you are reading to analyze, explore, or summarize → use context-mode_ctx_execute_file(path, language, code) instead. Only your printed summary enters context.

grep / search (large results)

Search results can flood context. Use context-mode_ctx_execute(language: "shell", code: "grep ...") to run searches in sandbox. Only your printed summary enters context.

Tool selection hierarchy

  1. GATHER: context-mode_ctx_batch_execute(commands, queries) — Primary tool. Runs all commands, auto-indexes output, returns search results. ONE call replaces 30+ individual calls.
  2. FOLLOW-UP: context-mode_ctx_search(queries: ["q1", "q2", ...]) — Query indexed content. Pass ALL questions as array in ONE call.
  3. PROCESSING: context-mode_ctx_execute(language, code) | context-mode_ctx_execute_file(path, language, code) — Sandbox execution. Only stdout enters context.
  4. WEB: context-mode_ctx_fetch_and_index(url, source) then context-mode_ctx_search(queries) — Fetch, chunk, index, query. Raw HTML never enters context.
  5. INDEX: context-mode_ctx_index(content, source) — Store content in FTS5 knowledge base for later search.

Output constraints

  • Keep responses under 500 words.
  • Write artifacts (code, configs, PRDs) to FILES — never return them as inline text. Return only: file path + 1-line description.
  • When indexing content, use descriptive source labels so others can search(source: "label") later.

ctx commands

Command Action
ctx stats Call the stats MCP tool and display the full output verbatim
ctx doctor Call the doctor MCP tool, run the returned shell command, display as checklist
ctx upgrade Call the upgrade MCP tool, run the returned shell command, display as checklist