103 lines
5.3 KiB
Plaintext
103 lines
5.3 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";
|
|
```
|
|
|
|
## Small presentational packages (buttons)
|
|
|
|
`app/components/buttons/<Name>/` holds **`index.tsx`** plus **`<Name>.tsx`**
|
|
(the **`Button`**, **`InlineTextButton`** packages today). Promote to the full
|
|
container/view/types split when state or logic outgrows a single module (like **`controls/TextInput`**).
|
|
|
|
## `cards/` packages
|
|
|
|
Prefer the **container / view / types** layout for **`Selection/`**, **`CardStack/`**, **`Rule/`**,
|
|
**`Icon/`**, **`Mini/`**, **`TemplateReviewCard/`**. **`Step/`** keeps a single
|
|
**`<Name>.tsx`** next to **`index.tsx`** until complexity justifies a split.
|
|
|
|
## `modals/` packages
|
|
|
|
Use the same **container / view / types** split where those files exist (**`Alert`**, **`Create`**, **`Dialog`**, **`Login`**, **`Tooltip`**, **`ModalHeader`**, **`ModalFooter`**).
|
|
|
|
## `navigation/` packages
|
|
|
|
Use the **container / view / types** split + per-package **`index.tsx`** for **`Top/`**, **`CreateFlowTopNav/`**, **`CreateFlowFooter/`**, **`NavigationItem/`**, **`Link/`**, **`MenuItem/`**. **`TopWithPathname.tsx`** lives inside **`Top/`** as the pathname + session wrapper.
|
|
|
|
**Root-level** **`Menu.tsx`**, **`Footer.tsx`**, **`ConditionalNavigation.tsx`**, and **`ConditionalNavigationClient.tsx`** sit beside those folders—no bucket-level barrel. Figma **Navigation / Menu** maps to **`Menu`** + **`MenuItem`** (see **`docs/figma-component-registry.md`**, Navigation conventions) and **`routes.mdc`** for shell behavior.
|
|
|
|
## `progress/` packages
|
|
|
|
Use the **container / view / types** split + **`index.tsx`** for **`Stepper/`** and **`ProportionBar/`** (same shape as **`controls/`**). See **`docs/figma-component-registry.md`** — **Progress conventions** for Figma **Progress** vs **Control / Proportion**.
|
|
|
|
## `sections/` packages
|
|
|
|
Section-level compositions are **mixed**: many folders use **`container` / `view` / `types`** (**`FeatureGrid/`**, **`QuoteBlock/`**, …), while **`ContentBanner.tsx`** and **`SectionNumber.tsx`** are **single modules** at the bucket root. Prefer the **split** for **new** composites; see **`docs/figma-component-registry.md`** — **Sections conventions**. **`SectionHeader/`** lives under **`type/`** (Figma Type / SectionHeader). Published rule typography body **`CommunityRule/`** lives under **`type/`** (see **Type conventions**).
|
|
|
|
## `type/` packages
|
|
|
|
**`type/`** is mostly **`container` / `view` / `types`** + **`index.tsx`** (**`HeaderLockup/`**, **`ContentLockup/`**, **`NumberedList/`**, **`InputLabel/`**). **`SectionHeader/`** is a small presentational package (**`SectionHeader.tsx`** + **`index.tsx`**) for the Figma Type SectionHeader lockup. **`CommunityRule/`**, **`Section/`**, and **`TextBlock/`** are **view + types** packages (Community Rule document tree). See **`docs/figma-component-registry.md`** — **Type conventions**.
|
|
|
|
## No package-level barrels
|
|
|
|
Do **not** add **`app/components/<bucket>/index.tsx`** that re-exports every
|
|
sibling under that bucket (there is no `buttons/index.tsx` or `asset/index.tsx`).
|
|
Import **`…/buttons/Button`**, **`…/asset/icon`**, **`…/asset/Logo`**, etc.—same
|
|
mental model everywhere.
|
|
|
|
**Per-component** **`index.tsx`** entrypoints (**`Logo/index.tsx`**, **`controls/TextInput/index.tsx`**, …) stay as documented above—aggregating an entire **`buttons/`** or **`asset/`** tier in one file does not.
|
|
|
|
## 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`.
|