Template: Add a new feature screen
Status: Reference
Task
Add a new feature screen for <purpose> (e.g. "club members admin", "court directory").
Pre-implementation checks
- Read
docs/architecture/screen-blueprint.md— does this screen already have a blueprint entry? If yes, follow it. If no, you'll need to add one. - Identify the feature package: which
packages/features/<name>/does it belong to? If none fits, escalate to user before creating a new package. - Grep for related primitives:bash
grep -rn "DetailShell\|MainTabShell" packages/features/<feature>/src/ # see existing shells - Confirm queries exist: the data the screen needs should be available via existing query hooks. If not, write those FIRST (separate prior commit, see
add-query.md). - Confirm permission gating: does this screen require admin role? Use
useClubRole(clubId)for club-scoped screens.
Files in scope
- New:
packages/features/<feature>/src/<screen-name>-screen.tsx - Modified:
packages/features/<feature>/src/index.ts(add export) - New:
apps/mobile/app/<route-path>.tsx(mobile route) - New:
apps/web/app/<route-path>/page.tsx(web route) - Modified:
packages/app/src/navigation/routes.ts(addroutes.<noun>helper if missing) - Modified:
packages/app/src/config/i18n/{ko,en}/<file>.ts(add new strings)
Files OUT of scope
- Mutation/query hooks — those go in
@twomore/app, separate commits - New shared primitives — only if this screen requires a new pattern (escalate first)
Implementation pattern
tsx
import React, { useState } from 'react';
import { ActivityIndicator } from 'react-native';
import {
appRouter,
useRouteParams,
useAuth,
use<Entity>, // query hooks
use<Mutation>, // mutation hooks
t,
} from '@twomore/app';
import {
DetailShell, // or MainTabShell for tab pages
Card,
SectionBlock,
Button,
Badge,
Text,
XStack,
YStack,
EmptyState,
FeedList,
SkeletonCard,
} from '@twomore/ui';
import { <Icon> } from '@tamagui/lucide-icons';
export function <Name>Screen(): React.JSX.Element {
const { <param> } = useRouteParams<{ <param>: string }>();
const { userId } = useAuth();
const { data, isLoading } = use<Entity>(<param>);
const labels = t().<scope>;
if (isLoading && !data) {
return (
<DetailShell title={labels.pageTitle} onBack={() => appRouter.back()}>
<YStack padding="$4" gap="$3">
<SkeletonCard />
<SkeletonCard />
</YStack>
</DetailShell>
);
}
return (
<DetailShell title={labels.pageTitle} onBack={() => appRouter.back()}>
{/* SectionBlock for titled groups, FeedList for lists */}
</DetailShell>
);
}Hard constraints to follow (from AGENTS.md)
- ARCH-2: import only from
@twomore/appand@twomore/ui. Neverexpo-router,next/*,@/, or sibling features. - COMP-3: page CTAs in
BottomCtaBand; cards never contain<Button>JSX subtrees - COMP-4:
MainTabShellfor tab pages,DetailShellfor detail screens - COMP-5:
FeedList/GroupedFeedListfor growable lists, never bareFlatList/SectionList/ScrollView - DATA-7: all UI strings via
t(), code/comments in English, Korean only in i18n files - Reviewer checklist:
CHK-COMP-1(skeletons during loading) — seedocs/best-practices/review-checklist.md
Post-implementation verification
bash
git status # confirm changes persisted
yarn typecheck # exit 0
# Workspace-specific
yarn workspace @twomore/<feature> typecheck
yarn workspace @twomore/app typecheck
# Architecture contracts
yarn workspace @twomore/app jest --runInBand src/config/__tests__/architecture-archunit.test.ts
# Build smoke tests
yarn workspace @twomore/web build # web compiles
yarn workspace @twomore/mobile preflight # mobile preflight passesWhat to report
- Files created/modified
- Permission gating decision
- Skeleton/empty state handling
- Typecheck + arch test results
- Web build + mobile preflight results
- Whether
screen-blueprint.mdwas updated (required for new screens)