Migrates all studio-related Cursor rules to Claude skills and adds a top-level `.claude/CLAUDE.md` for project context. Docs rules left in place. **Decisions:** - Only studio + testing rules migrated — docs rules intentionally left in `.cursor/rules/docs/` - Vitest skill already shared via symlink (`.claude/skills/vitest` → `.agents/skills/vitest`) — nothing to migrate - Grouped ~21 granular cursor rules into 5 new skills + 1 updated skill by topic - `studio-architecture` skill fully merged into `CLAUDE.md` and deleted to avoid overlap - Skills are self-contained (content inlined, not relying on sub-files) since Claude reads SKILL.md first - Skills cross-reference each other inline where relevant (e.g. best-practices → testing, error-handling, queries) - No `paths` frontmatter — would auto-inject full skill content on every matching file. Current description-based matching is more selective and token-efficient. **Removed:** - `.cursor/rules/studio/` (21 rule files covering architecture, best practices, UI patterns, queries, styling, etc.) - `.cursor/rules/testing/` (e2e-studio + unit-integration rules) - `.cursor/rules/studio-useStaticEffectEvent.mdc` - `.claude/skills/studio-architecture/` — fully merged into CLAUDE.md to avoid duplication - `.claude/skills/studio-testing/rules/` — orphaned sub-files after inlining content into SKILL.md **Added:** - `.claude/CLAUDE.md` — concise monorepo overview with structure, commands, and conventions. Absorbs studio-architecture content. References `studio-*` skills for detail. - `.claude/skills/studio-best-practices/` — boolean naming, component structure, loading/error/success patterns, state management, hooks, TypeScript conventions. Cross-references `vercel-composition-patterns`, `studio-ui-patterns`, `studio-queries`, `studio-error-handling`, and `studio-testing` inline where relevant. - `.claude/skills/studio-ui-patterns/` — layout, forms, tables, charts, empty states, navigation, cards, alerts, sheets. Grouped from ~10 separate cursor rules into one cohesive skill. - `.claude/skills/studio-queries/` — React Query `queryOptions` pattern, `keys.ts` structure, mutation hook template, imperative fetching. - `.claude/skills/use-static-effect-event/` — the `useStaticEffectEvent` hook: when to use, when not to, patterns, implementation. **Changed:** - `.claude/skills/studio-e2e-tests/` — renamed from `e2e-studio-tests` for `studio-*` naming consistency. Merged race condition, waiting strategy, test structure, assertion, and cleanup patterns from the cursor e2e rule. - `.claude/skills/studio-testing/` — inlined key content from sub-rule files directly into SKILL.md so it's self-contained. Removed broken `AGENTS.md` reference. Deleted orphaned `rules/` sub-files. - `.claude/skills/vercel-composition-patterns/` — added note that Studio uses React 18, so React 19 patterns should be skipped. - `.gitignore` — added `!.claude/CLAUDE.md` exception so it's tracked. ## To test - Open Claude Code in the repo, verify `.claude/CLAUDE.md` loads as project context - Ask Claude about Studio conventions and verify it references the right skills - Check that `studio-*` skills appear in the skill list --------- Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4.7 KiB
name, description
| name | description |
|---|---|
| studio-best-practices | React and TypeScript best practices for Supabase Studio. Use when writing or reviewing Studio components — covers boolean naming, component structure, loading/error states, state management, custom hooks, event handlers, conditional rendering, performance, and TypeScript conventions. |
Studio Best Practices
Applies to apps/studio/**/*.{ts,tsx}.
Boolean Naming
Use descriptive prefixes — derive from existing state rather than storing separately:
is— state/identity:isLoading,isPaused,isNewRecordhas— possession:hasPermission,hasDatacan— capability:canUpdateColumns,canDeleteshould— conditional behavior:shouldFetch,shouldRender
Extract complex conditions into named variables:
// ❌ inline multi-condition
{
!isSchemaLocked && isTableLike(selectedTable) && canUpdateColumns && !isLoading && <Button />
}
// ✅ named variable
const canShowAddButton =
!isSchemaLocked && isTableLike(selectedTable) && canUpdateColumns && !isLoading
{
canShowAddButton && <Button />
}
Derive booleans — don't store them:
// ❌ stored derived state
const [isFormValid, setIsFormValid] = useState(false)
useEffect(() => {
setIsFormValid(name.length > 0 && email.includes('@'))
}, [name, email])
// ✅ derived
const isFormValid = name.length > 0 && email.includes('@')
Component Structure
See vercel-composition-patterns skill for compound component and composition patterns.
Keep components under 200–300 lines. Split when you see:
- Multiple distinct UI sections
- Complex conditional rendering
- Multiple unrelated
useStatecalls - Hard to understand at a glance
Co-locate sub-components in the same directory as the parent. Avoid barrel re-export files.
Extract repeated JSX patterns into small components.
Data Fetching
All data fetching uses TanStack Query (React Query). See studio-queries skill for query/mutation patterns and studio-error-handling skill for error display conventions.
Loading / Error / Success Pattern
Top level:
const { data, error, isLoading, isError, isSuccess } = useQuery(...)
if (isLoading) return <GenericSkeletonLoader />
if (isError) return <AlertError error={error} subject="Failed to load data" />
if (isSuccess && data.length === 0) return <EmptyState />
return <DataDisplay data={data} />
Use early returns — avoid deeply nested conditionals.
Inline:
<div>
{isLoading && <InlineLoader />}
{isError && <InlineError error={error} />}
{isSuccess && data.length === 0 && <EmptyState />}
{isSuccess && data.length > 0 && <DataDisplay data={data} />}
</div>
State Management
Keep state as local as possible; lift only when needed.
Group related form state with react-hook-form rather than multiple useState calls. See studio-ui-patterns skill for form layout and component conventions.
// ❌ multiple related useState
const [name, setName] = useState('')
const [email, setEmail] = useState('')
// ✅ grouped with react-hook-form
const form = useForm<FormValues>({ defaultValues: { name: '', email: '' } })
Custom Hooks
Extract complex or reusable logic into hooks. Return objects, not arrays:
// ❌ array return (hard to extend)
return [value, toggle]
// ✅ object return
return { value, toggle, setTrue, setFalse }
Event Handlers
- Prop callbacks:
onprefix (onClose,onSave) - Internal handlers:
handleprefix (handleSubmit,handleCancel)
Use useCallback for handlers passed to memoized children; avoid unnecessary inline arrow functions.
Conditional Rendering
// Simple show/hide
<>{isVisible && <Component />}</>
// Binary choice
<>{isLoading ? <Spinner /> : <Content />}</>
// Multiple conditions — use early returns, not nested ternaries
if (isLoading) return <Spinner />
if (isError) return <Error />
return <Content />
Performance
useMemo for genuinely expensive computations (measured, not assumed). Don't wrap everything — only optimize when you have a measured problem or are passing values to memoized children.
TypeScript
Define prop interfaces explicitly. Use discriminated unions for complex state:
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error }
Avoid as any / as Type casts. Validate at boundaries with zod:
// ❌ type cast
const user = apiResponse as User
// ✅ zod parse
const user = userSchema.parse(apiResponse)
// or safe:
const result = userSchema.safeParse(apiResponse)
Testing
Extract logic into .utils.ts pure functions and test exhaustively. See the studio-testing skill for the full testing strategy and decision tree.