68 lines
2.4 KiB
Plaintext
68 lines
2.4 KiB
Plaintext
---
|
|
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/<Name>/
|
|
<Name>.types.ts // Public Props + internal ViewProps
|
|
<Name>.view.tsx // "use client"; pure render; exports memo(view)
|
|
<Name>.container.tsx // "use client"; memo; prop normalization & logic
|
|
index.tsx // re-exports default + public types
|
|
```
|
|
|
|
**Container** (`<Name>.container.tsx`):
|
|
|
|
- Marked `"use client"`.
|
|
- Receives `<Name>Props`; computes derived state (clamps, ids, bounds, prop
|
|
defaults) and bound event handlers.
|
|
- Renders `<<Name>View />`. Containers do **not** translate prop casing —
|
|
enum props are lowercase end-to-end (see `component-props.mdc`).
|
|
- Default export: `memo(<Name>Container)` with `.displayName = "<Name>"`.
|
|
- Carries the Figma docstring (`Figma: "<Path>" (<node-id>)`).
|
|
|
|
**View** (`<Name>.view.tsx`):
|
|
|
|
- Marked `"use client"`.
|
|
- Pure render of `<Name>ViewProps`. No data fetching, no derived business
|
|
logic, no enum casing translation.
|
|
- Default export: `memo(<Name>View)` with `.displayName = "<Name>View"`.
|
|
|
|
**Types** (`<Name>.types.ts`):
|
|
|
|
- Export `<Name>Props` (consumer-facing).
|
|
- Export `<Name>ViewProps` (the shape the view consumes — typically a
|
|
resolved superset of `<Name>Props`).
|
|
- Export any locally-defined value types (`<Name>SizeValue`, etc.) sourced
|
|
from the matching `*_OPTIONS` array in `lib/propNormalization.ts`.
|
|
|
|
**Index** (`index.tsx`):
|
|
|
|
```typescript
|
|
export { default } from "./<Name>.container";
|
|
export type { <Name>Props } from "./<Name>.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`.
|