Update CLAUDE.md with additional context and instructions (#10385)

This commit is contained in:
DS Boyce
2026-04-29 03:53:07 -07:00
committed by GitHub
parent 8bfce09628
commit b97cd413e3
+165 -81
View File
@@ -1,4 +1,4 @@
# CLAUDE.md - Bluesky Social App Development Guide # CLAUDE.md  Bluesky Social App Development Guide
This document provides guidance for working effectively in the Bluesky Social app codebase. This document provides guidance for working effectively in the Bluesky Social app codebase.
@@ -7,13 +7,17 @@ This document provides guidance for working effectively in the Bluesky Social ap
Bluesky Social is a cross-platform social media application built with React Native and Expo. It runs on iOS, Android, and Web, connecting to the AT Protocol (atproto) decentralized social network. Bluesky Social is a cross-platform social media application built with React Native and Expo. It runs on iOS, Android, and Web, connecting to the AT Protocol (atproto) decentralized social network.
**Tech Stack:** **Tech Stack:**
- React 19.1
- React Native 0.81 with Expo 54 - React Native 0.81 with Expo 54
- TypeScript - TypeScript 6
- React Navigation for routing - React Navigation 7 for routing
- TanStack Query (React Query) for data fetching - TanStack Query (React Query) for data fetching
- Lingui for internationalization - Lingui 5 for internationalization
- Custom design system called ALF (Application Layout Framework) - Custom design system called ALF (Application Layout Framework)
Prefer using the latest features available for each of these libraries (exact versions are found in `package.json`). For example, prefer `@lingui/react/macro` over `@lingui/react`. Suggest refactoring legacy or deprecated uses.
## Essential Commands ## Essential Commands
```bash ```bash
@@ -162,6 +166,10 @@ its own looks more like a single component file.
### Documentation and Tests Within Features ### Documentation and Tests Within Features
Comment code when necessary to explain the “why” behind something; avoid
comments that simply describe the code. Avoid Unicode characters in comments,
e.g., use `-` not `—`.
For larger features or components, it's helpful to include a README.md file For larger features or components, it's helpful to include a README.md file
within the directory that explains the purpose of the feature, how it works, and within the directory that explains the purpose of the feature, how it works, and
any important implementation details. The `/Component/index.tsx` pattern lends any important implementation details. The `/Component/index.tsx` pattern lends
@@ -182,6 +190,14 @@ ALF is the custom design system. It uses Tailwind-inspired naming with underscor
### Basic Usage ### Basic Usage
Generally, order atoms by:
- Flexbox configuration, e.g., `a.flex_row`
- Spacing, e.g., `a.px_md`
- Text styles, e.g., `a.font_bold`
- Themes, e.g., `t.atoms.text`,
- Raw styles, e.g., `{backgroundColor: t.palette.primary_500}`
```tsx ```tsx
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
@@ -190,9 +206,7 @@ function MyComponent() {
return ( return (
<View style={[a.flex_row, a.gap_md, a.p_lg, t.atoms.bg]}> <View style={[a.flex_row, a.gap_md, a.p_lg, t.atoms.bg]}>
<Text style={[a.text_md, a.font_bold, t.atoms.text]}> <Text style={[a.text_md, a.font_bold, t.atoms.text]}>Hello</Text>
Hello
</Text>
</View> </View>
) )
} }
@@ -200,20 +214,23 @@ function MyComponent() {
### Key Concepts ### Key Concepts
**Static Atoms** - Theme-independent styles imported from `atoms`: **Static Atoms**  Theme-independent styles imported from `atoms`:
```tsx ```tsx
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
// a.flex_row, a.p_md, a.gap_sm, a.rounded_md, a.text_lg, etc. // a.flex_row, a.p_md, a.gap_sm, a.rounded_md, a.text_lg, etc.
``` ```
**Theme Atoms** - Theme-dependent colors from `useTheme()`: **Theme Atoms**  Theme-dependent colors from `useTheme()`:
```tsx ```tsx
const t = useTheme() const t = useTheme()
// t.atoms.bg, t.atoms.text, t.atoms.border_contrast_low, etc. // t.atoms.bg, t.atoms.text, t.atoms.border_contrast_low, etc.
// t.palette.primary_500, t.palette.negative_400, etc. // t.palette.primary_500, t.palette.negative_400, etc.
``` ```
**Platform Utilities** - For platform-specific styles: **Platform Utilities**  For platform-specific styles:
```tsx ```tsx
import {web, native, ios, android, platform} from '#/alf' import {web, native, ios, android, platform} from '#/alf'
@@ -225,7 +242,8 @@ const styles = [
] ]
``` ```
**Breakpoints** - Responsive design: **Breakpoints**  Responsive design:
```tsx ```tsx
import {useBreakpoints} from '#/alf' import {useBreakpoints} from '#/alf'
@@ -245,6 +263,38 @@ if (gtMobile) {
## Component Patterns ## Component Patterns
- Prefer fragment shorthand over `Fragment` unless a `key` is needed.
- Prefer functions over arrow functions for component declarations.
- Prefer prop destructuring via parameters over a const within the component.
- Prefer inline types over `Props` types or interfaces.
- Set reasonable defaults for optional props.
```tsx
import {Fragment} from 'react'
import {View} from 'react-native'
import {Trans, useLingui} from '@lingui/react/macro'
import {Text} from '#/components/Typography'
function MyComponent({foo = []}: {foo?: string[]}) {
const {t: l} = useLingui()
return (
<>
<View><Text><Trans>Example</Trans><Text></View>
<View>
{foo.map((foo, index) => (
<Fragment key={foo}>
<Text>{index}</Text>
<Text>{foo}</Text>
</Fragment>
))}
</View>
</>
);
}
```
### Dialog Component ### Dialog Component
Dialogs use a bottom sheet on native and a modal on web. Use `useDialogControl()` hook to manage state. Dialogs use a bottom sheet on native and a modal on web. Use `useDialogControl()` hook to manage state.
@@ -263,23 +313,29 @@ function MyFeature() {
<Dialog.Outer control={control}> <Dialog.Outer control={control}>
{/* Typically the inner part is in its own component */} {/* Typically the inner part is in its own component */}
<Dialog.Handle /> {/* Native-only drag handle */} <DialogInner />
<Dialog.ScrollableInner label={_(msg`My Dialog`)}>
<Dialog.Header>
<Dialog.HeaderText>Title</Dialog.HeaderText>
</Dialog.Header>
<Text>Dialog content here</Text>
<Button label="Done" onPress={() => control.close()}>
<ButtonText>Done</ButtonText>
</Button>
<Dialog.Close /> {/* Web-only X button in top left */}
</Dialog.ScrollableInner>
</Dialog.Outer> </Dialog.Outer>
</> </>
) )
} }
function DialogInner() {
return (
<>
<Dialog.Handle /> {/* Native-only drag handle */}
<Dialog.ScrollableInner label={l`My Dialog`}>
<Dialog.Header>
<Dialog.HeaderText>Title</Dialog.HeaderText>
</Dialog.Header>
<Text>Dialog content here</Text>
<Button label="Done" onPress={() => control.close()}>
<ButtonText>Done</ButtonText>
</Button>
<Dialog.Close /> {/* Web-only X button in top left */}
</Dialog.ScrollableInner>
</>
)
}
``` ```
### Menu Component ### Menu Component
@@ -345,6 +401,7 @@ import {Button, ButtonText, ButtonIcon} from '#/components/Button'
``` ```
**Button Props:** **Button Props:**
- `color`: `'primary'` | `'secondary'` | `'negative'` | `'primary_subtle'` | `'negative_subtle'` | `'secondary_inverted'` - `color`: `'primary'` | `'secondary'` | `'negative'` | `'primary_subtle'` | `'negative_subtle'` | `'secondary_inverted'`
- `size`: `'tiny'` | `'small'` | `'large'` - `size`: `'tiny'` | `'small'` | `'large'`
- `shape`: `'default'` (pill) | `'round'` | `'square'` | `'rectangular'` - `shape`: `'default'` (pill) | `'round'` | `'square'` | `'rectangular'`
@@ -384,39 +441,66 @@ import * as TextField from '#/components/forms/TextField'
## Internationalization (i18n) ## Internationalization (i18n)
All user-facing strings must be wrapped for translation using Lingui. All user-facing strings must be wrapped for translation using Lingui. Include `comment` and/or `context` props when necessary to avoid ambiguity, e.g., “Post” as a noun vs a verb.
Prefer using `t` via `import {useLingui} '@lingui/react/macro'` vs `_` via `import {useLingui} from '@lingui/react'`. Alias `t` to `l` to avoid collisions with `const t = useTheme()`. Refactor existing uses of ``_(msg`foo`)`` to use `` l`foo` ``.
Prefer Unicode punctuation over keyboard punctuation, e.g., `“quote”` over `"quote"`. Prefer en dashes preceded by a non-breaking space over em dashes, e.g., `one  two` over `one—two`.
```tsx ```tsx
import {msg, plural} from '@lingui/core/macro' import {plural} from '@lingui/core/macro'
import {Trans} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {useLingui} from '@lingui/react'
function MyComponent() { function MyComponent() {
const {_} = useLingui() const {t: l} = useLingui()
// Simple strings - use msg() with _() function // Simple strings - use the l macro
const title = _(msg`Settings`) const title = l`Settings`
const errorMessage = _(msg`Something went wrong`) const errorMessage = l({
message: 'Something went wrong',
comment: 'Generic error message for unknown/unhandled errors.',
context: 'Toast',
})
// Strings with variables // Strings with variables
const greeting = _(msg`Hello, ${name}!`) const greeting = l`Hello, ${name}!`
// Pluralization // Pluralization
const countLabel = _(plural(count, { const countLabel = plural(count, {
one: '# item', one: '# item',
other: '# items', other: '# items',
})) })
// JSX content - use Trans component // JSX content - use Trans component
return ( return (
<Text> <Text>
<Trans>Welcome to <Text style={a.font_bold}>Bluesky</Text></Trans> <Trans>
Welcome to <Text style={a.font_bold}>Bluesky</Text>, {name}!
</Trans>
</Text> </Text>
) )
} }
``` ```
Prefer `i18n.date` for date and time formatting. This ensures formatting is re-applied when the language changes at runtime. Refactor existing uses of `Intl.DateTimeFormat` to use `i18n.date`.
```tsx
import {useLingui} from '@lingui/react/macro'
function MyComponent() {
const {i18n} = useLingui()
const createdAt = new Date()
return i18n.date(createdAt, {
dateStyle: 'medium',
timeStyle: 'medium',
})
}
```
**Commands:** **Commands:**
```bash ```bash
# DO NOT run these commands - extraction and compilation are handled by a nightly CI job # DO NOT run these commands - extraction and compilation are handled by a nightly CI job
yarn intl:extract # Extract new strings to locale files yarn intl:extract # Extract new strings to locale files
@@ -473,7 +557,7 @@ export function useProfileMutation() {
const queryClient = useQueryClient() const queryClient = useQueryClient()
return useMutation({ return useMutation({
mutationFn: async (data) => { mutationFn: async data => {
// Update logic // Update logic
}, },
onSuccess: (_, variables) => { onSuccess: (_, variables) => {
@@ -481,7 +565,7 @@ export function useProfileMutation() {
queryKey: createProfileQueryKey({did: variables.did}), queryKey: createProfileQueryKey({did: variables.did}),
}) })
}, },
onError: (error) => { onError: error => {
if (isNetworkError(error)) { if (isNetworkError(error)) {
// don't log, but inform user // don't log, but inform user
} else if (error instanceof AppBskyExampleProcedure.ExampleError) { } else if (error instanceof AppBskyExampleProcedure.ExampleError) {
@@ -490,7 +574,7 @@ export function useProfileMutation() {
// Log unexpected errors to Sentry // Log unexpected errors to Sentry
logger.error('Error updating profile', {safeMessage: error}) logger.error('Error updating profile', {safeMessage: error})
} }
} },
}) })
} }
@@ -505,21 +589,25 @@ export function useProfileCacheMutation() {
const queryClient = useQueryClient() const queryClient = useQueryClient()
return (data: Partial<Profile>) => { return (data: Partial<Profile>) => {
queryClient.setQueryData(createProfileQueryKey({did: data.did}), oldData => { queryClient.setQueryData(
if (!oldData) return oldData createProfileQueryKey({did: data.did}),
return {...oldData, ...data} oldData => {
}) if (!oldData) return oldData
return {...oldData, ...data}
},
)
} }
} }
``` ```
**Stale Time Constants** (from `src/state/queries/index.ts`): **Stale Time Constants** (from `src/state/queries/index.ts`):
```tsx ```tsx
STALE.SECONDS.FIFTEEN // 15 seconds STALE.SECONDS.FIFTEEN // 15 seconds
STALE.MINUTES.ONE // 1 minute STALE.MINUTES.ONE // 1 minute
STALE.MINUTES.FIVE // 5 minutes STALE.MINUTES.FIVE // 5 minutes
STALE.HOURS.ONE // 1 hour STALE.HOURS.ONE // 1 hour
STALE.INFINITY // Never stale STALE.INFINITY // Never stale
``` ```
**Paginated APIs:** Many atproto APIs return paginated results with a `cursor`. Use `useInfiniteQuery` for these: **Paginated APIs:** Many atproto APIs return paginated results with a `cursor`. Use `useInfiniteQuery` for these:
@@ -565,12 +653,7 @@ function SettingsScreen() {
const autoplayDisabled = useAutoplayDisabled() const autoplayDisabled = useAutoplayDisabled()
const setAutoplayDisabled = useSetAutoplayDisabled() const setAutoplayDisabled = useSetAutoplayDisabled()
return ( return <Toggle value={autoplayDisabled} onValueChange={setAutoplayDisabled} />
<Toggle
value={autoplayDisabled}
onValueChange={setAutoplayDisabled}
/>
)
} }
``` ```
@@ -604,13 +687,9 @@ import {type CommonNavigatorParams} from '#/lib/routes/types'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'>
export function ProfileScreen({route, navigation}: Props) { export function ProfileScreen({route, navigation}: Props) {
const {name} = route.params // Type-safe params const {name} = route.params // Type-safe params
return ( return <Layout.Screen>{/* Screen content */}</Layout.Screen>
<Layout.Screen>
{/* Screen content */}
</Layout.Screen>
)
} }
// Programmatic navigation // Programmatic navigation
@@ -637,8 +716,9 @@ Component.android.tsx # Android-only
``` ```
Example from Dialog: Example from Dialog:
- `src/components/Dialog/index.tsx` - Native (uses BottomSheet)
- `src/components/Dialog/index.web.tsx` - Web (uses modal with Radix primitives) - `src/components/Dialog/index.tsx Native (uses BottomSheet)
- `src/components/Dialog/index.web.tsx Web (uses modal with Radix primitives)
**Important:** The bundler automatically resolves platform-specific files. Just import normally: **Important:** The bundler automatically resolves platform-specific files. Just import normally:
@@ -653,6 +733,7 @@ const storage = IS_NATIVE
``` ```
Platform detection (for runtime logic, not imports): Platform detection (for runtime logic, not imports):
```tsx ```tsx
import {IS_WEB, IS_NATIVE, IS_IOS, IS_ANDROID} from '#/env' import {IS_WEB, IS_NATIVE, IS_IOS, IS_ANDROID} from '#/env'
@@ -687,13 +768,13 @@ Common pitfalls to avoid in this codebase:
// WRONG - causes bugs with state updates, navigation, opening other dialogs // WRONG - causes bugs with state updates, navigation, opening other dialogs
const onConfirm = () => { const onConfirm = () => {
control.close() control.close()
navigation.navigate('Home') // May race with dialog animation navigation.navigate('Home') // May race with dialog animation
} }
// WRONG - same problem // WRONG - same problem
const onConfirm = () => { const onConfirm = () => {
control.close() control.close()
otherDialogControl.open() // Will likely fail or cause visual glitches otherDialogControl.open() // Will likely fail or cause visual glitches
} }
// CORRECT - action runs after dialog fully closes // CORRECT - action runs after dialog fully closes
@@ -720,12 +801,13 @@ const onConfirm = () => {
``` ```
This applies to: This applies to:
- Navigation (`navigation.navigate()`, `navigation.push()`) - Navigation (`navigation.navigate()`, `navigation.push()`)
- Opening other dialogs or menus - Opening other dialogs or menus
- State updates that affect UI (`setState`, `queryClient.invalidateQueries`) - State updates that affect UI (`setState`, `queryClient.invalidateQueries`)
- Callbacks passed from parent components - Callbacks passed from parent components
The Menu component on iOS specifically uses this pattern - see `src/components/Menu/index.tsx:151`. The Menu component on iOS specifically uses this pattern  see `src/components/Menu/index.tsx:151`.
### Controlled vs Uncontrolled Inputs ### Controlled vs Uncontrolled Inputs
@@ -748,10 +830,11 @@ Prefer `defaultValue` over `value` for TextInput on the old architecture:
### Platform-Specific Behavior ### Platform-Specific Behavior
Some components behave differently across platforms: Some components behave differently across platforms:
- `Dialog.Handle` - Only renders on native (drag handle for bottom sheet)
- `Dialog.Close` - Only renders on web (X button) - `Dialog.Handle Only renders on native (drag handle for bottom sheet)
- `Menu.Divider` - Only renders on web - `Dialog.Close Only renders on web (X button)
- `Menu.ContainerItem` - Only works on native - `Menu.Divider Only renders on web
- `Menu.ContainerItem Only works on native
Always test on multiple platforms when using these components. Always test on multiple platforms when using these components.
@@ -772,6 +855,7 @@ const handlePress = () => {
``` ```
Only use `useMemo`/`useCallback` when you have a specific reason, such as: Only use `useMemo`/`useCallback` when you have a specific reason, such as:
- The value is immediately used in an effect's dependency array - The value is immediately used in an effect's dependency array
- You're passing a callback to a non-React library that needs referential stability - You're passing a callback to a non-React library that needs referential stability
@@ -779,7 +863,7 @@ Only use `useMemo`/`useCallback` when you have a specific reason, such as:
1. **Accessibility**: Always provide `label` prop for interactive elements, use `accessibilityHint` where helpful 1. **Accessibility**: Always provide `label` prop for interactive elements, use `accessibilityHint` where helpful
2. **Translations**: Wrap ALL user-facing strings with `msg()` or `<Trans>` 2. **Translations**: Wrap ALL user-facing strings with ` `l` `` or `<Trans>`
3. **Styling**: Combine static atoms with theme atoms, use platform utilities for platform-specific styles 3. **Styling**: Combine static atoms with theme atoms, use platform utilities for platform-specific styles
@@ -793,14 +877,14 @@ Only use `useMemo`/`useCallback` when you have a specific reason, such as:
## Key Files Reference ## Key Files Reference
| Purpose | Location | | Purpose | Location |
|---------|----------| | ----------------- | -------------------------------------------- |
| Theme definitions | `src/alf/themes.ts` | | Theme definitions | `src/alf/themes.ts` |
| Design tokens | `src/alf/tokens.ts` | | Design tokens | `src/alf/tokens.ts` |
| Static atoms | `src/alf/atoms.ts` (extends `@bsky.app/alf`) | | Static atoms | `src/alf/atoms.ts` (extends `@bsky.app/alf`) |
| Navigation config | `src/Navigation.tsx` | | Navigation config | `src/Navigation.tsx` |
| Route definitions | `src/routes.ts` | | Route definitions | `src/routes.ts` |
| Route types | `src/lib/routes/types.ts` | | Route types | `src/lib/routes/types.ts` |
| Query hooks | `src/state/queries/*.ts` | | Query hooks | `src/state/queries/*.ts` |
| Session state | `src/state/session/index.tsx` | | Session state | `src/state/session/index.tsx` |
| i18n setup | `src/locale/i18n.ts` | | i18n setup | `src/locale/i18n.ts` |