--- description: File-structure conventions for design-system components globs: app/components/**/*.{ts,tsx} alwaysApply: false --- # Component file structure ## Split-file pattern (default) Anything in `app/components/controls/**` and `app/components/utility/**` uses a **4-file split**, one folder per component: ``` app/components/controls// .types.ts // Public Props + internal ViewProps .view.tsx // "use client"; pure render; exports memo(view) .container.tsx // "use client"; memo; prop normalization & logic index.tsx // re-exports default + public types ``` **Container** (`.container.tsx`): - Marked `"use client"`. - Receives `Props`; computes derived state (clamps, ids, bounds, prop defaults) and bound event handlers. - Renders `<View />`. Containers do **not** translate prop casing — enum props are lowercase end-to-end (see `component-props.mdc`). - Default export: `memo(Container)` with `.displayName = ""`. - Carries the Figma docstring (`Figma: "" ()`). **View** (`.view.tsx`): - Marked `"use client"`. - Pure render of `ViewProps`. No data fetching, no derived business logic, no enum casing translation. - Default export: `memo(View)` with `.displayName = "View"`. **Types** (`.types.ts`): - Export `Props` (consumer-facing). - Export `ViewProps` (the shape the view consumes — typically a resolved superset of `Props`). - Export any locally-defined value types (`SizeValue`, etc.) sourced from the matching `*_OPTIONS` array in `lib/propNormalization.ts`. **Index** (`index.tsx`): ```typescript export { default } from "./.container"; export type { Props } from "./.types"; ``` ## Single-file pattern (exception) `app/components/buttons/*.tsx` and other trivially-presentational components can stay as a single file when they have **no derived state and only a handful of props** (e.g. `Button.tsx`, `InlineTextButton.tsx`). If you find yourself adding state, side effects, or enum logic, promote it to the split pattern. ## Wrapper / group components Related composites live in a **sibling folder**, not inside the base component's folder — mirror `CheckboxGroup/` ↔ `Checkbox/`, `IncrementerBlock/` ↔ `Incrementer/`, etc. Each gets its own 4-file split. Consumers import from the folder's `index.tsx`.