From 7ea33ab68eaf9404fc781bde2d65b327c9269eb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Tue, 7 Apr 2026 11:05:00 +0400 Subject: [PATCH] Change naming rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Émile Ré --- contrib/claude/ui.md | 71 ++++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/contrib/claude/ui.md b/contrib/claude/ui.md index 849833432..98dc04861 100644 --- a/contrib/claude/ui.md +++ b/contrib/claude/ui.md @@ -2,7 +2,7 @@ Shared React UI for Probo apps lives in the **`@probo/ui`** workspace package ([`packages/ui`](../../packages/ui)). This document describes **target** conventions for building and styling those components. -**Today’s codebase does not fully match these rules.** The tree still uses layouts like `Atoms/`, `Molecules/`, and `Layouts/`, and many files mix ad-hoc Tailwind on `className` with `tailwind-variants`. Treat this guide as the direction for new work and refactors, not as a description of the current tree. +**Today's codebase does not fully match these rules.** The tree still uses layouts like `Atoms/`, `Molecules/`, and `Layouts/`, and many files mix ad-hoc Tailwind on `className` with `tailwind-variants`. Treat this guide as the direction for new work and refactors, not as a description of the current tree. For data loading and GraphQL on the console, see [`contrib/claude/relay.md`](relay.md). @@ -48,7 +48,7 @@ In a **single component file**, do **not** mix arbitrary Tailwind utility string For **compound / multi-slot** components, define `tv` in a **dedicated module** (see [Variants file](#variants-file)) so loading-only code paths can import styles without pulling the full interactive implementation. -### Do / don’t: `tv` vs raw `className` +### Do / don't: `tv` vs raw `className` ```tsx // Bad — same file mixes tv() output with ad-hoc Tailwind on className (clsx shown for the anti-pattern) @@ -99,7 +99,7 @@ export function Row({ bordered, children }: { bordered?: boolean; children: Reac **Other components** live in a folder **named after the component** (e.g. `ImageCard/`), with optional split files for subparts. -### Do / don’t: folder placement +### Do / don't: folder placement ```text // Good — target layout (usage folders for primitives, component folder for composites) @@ -111,7 +111,8 @@ packages/ui/src/ form/Field.tsx layouts/CenteredLayout.tsx ImageCard/variants.ts - ImageCard/ImageCard.tsx + ImageCard/ImageCardRoot.tsx + ImageCard/ImageCardShell.tsx ImageCard/ImageCardSkeleton.tsx // Bad — ad-hoc placement for a simple primitive (should live under typography / form / layouts) @@ -126,7 +127,7 @@ For each meaningful component, provide a paired loading UI: A partial precedent today: [`CenteredLayoutSkeleton`](../../packages/ui/src/Layouts/CenteredLayout.tsx) alongside the layout component. -### Do / don’t: skeleton naming +### Do / don't: skeleton naming ```tsx // Good — paired names @@ -138,19 +139,18 @@ export function Text(props: TextProps) { /* … */ } export function LoadingText() { /* … */ } // use TextSkeleton instead ``` -## Compound modules (e.g. `ImageCard`) +## Compound components (e.g. `ImageCard`) -Multi-region UI (card shell, media, text column, etc.) is exported as a **compound module** with static properties on one object. +Multi-region UI (card shell, media, text column, etc.) is exported as **individual named exports** — one per sub-component — all prefixed with the feature name (e.g. `ImageCardRoot`, `ImageCardShell`, `ImageCardSkeleton`). **Do not** group sub-components as static properties on a single namespace object (`ImageCard.Root`, `ImageCard.Shell`, …); flat named exports enable proper tree shaking and keep unwanted third-party dependencies out of loading-time bundles. ### Folder and exports -- One directory per feature component (e.g. `ImageCard/`). Heavy logic may live in **separate files**; the public surface remains **`ImageCard`** with attached properties. -- **`ImageCard`** — the compound module (named/default export as established in the package). -- **`ImageCard.Root`** — top-level container **when it may hold business logic** (state, effects, data wiring, etc.). -- **`ImageCard.Shell`** — **pure layout shell**: takes **`image`** and **`text`** (and other region) **props**—each a `ReactNode`—and places them in the matching **`tv` slots**. **No children** for layout regions on `Shell`; **no state or logic** in `Shell`. If the outer wrapper is layout-only, expose it as **`Shell`**, not **`Root`**. -- **`Image`** and **`Text`** — **shared primitives** from **`@probo/ui`** (e.g. typography / media folders), not namespaced under **`ImageCard`**. **`Root`** composes them into **`Shell`**’s **`image`** / **`text`** props; apps import the same **`Image`** / **`Text`** everywhere. +- One directory per feature component (e.g. `ImageCard/`). Heavy logic may live in **separate files**; each public part is a **standalone named export**. +- **`ImageCardRoot`** — top-level container **when it may hold business logic** (state, effects, data wiring, etc.). +- **`ImageCardShell`** — **pure layout shell**: takes **`image`** and **`text`** (and other region) **props**—each a `ReactNode`—and places them in the matching **`tv` slots**. **No children** for layout regions on the shell; **no state or logic** in the shell. If the outer wrapper is layout-only, expose it as **`ImageCardShell`**, not **`ImageCardRoot`**. +- **`Image`** and **`Text`** — **shared primitives** from **`@probo/ui`** (e.g. typography / media folders), not prefixed under `ImageCard`. **`ImageCardRoot`** composes them into **`ImageCardShell`**'s **`image`** / **`text`** props; apps import the same **`Image`** / **`Text`** everywhere. -**`Root` vs `Shell`:** use **`Root`** when the container owns logic; use **`Shell`** for a presentational outer frame. **`Root` may render `Shell`** inside when logic sits outside the styled layout. +**Root vs Shell:** use **`ImageCardRoot`** when the container owns logic; use **`ImageCardShell`** for a presentational outer frame. **`ImageCardRoot` may render `ImageCardShell`** inside when logic sits outside the styled layout. ### `tailwind-variants` slots @@ -160,11 +160,11 @@ For this pattern, model regions with **`tv` `slots`** named consistently with th - `image` - `text` -Add or rename slots when the layout has more or different regions. **`Shell`** applies the matching slot output on its wrappers; **`Image`** / **`Text`** stay free of **`ImageCard`**-specific layout—keep the [no-mixing rule](#tailwind-variants-and-classname) in each file. +Add or rename slots when the layout has more or different regions. **`ImageCardShell`** applies the matching slot output on its wrappers; **`Image`** / **`Text`** stay free of **`ImageCard`**-specific layout—keep the [no-mixing rule](#tailwind-variants-and-classname) in each file. -### Do / don’t: compound API and slots +### Do / don't: compound API and slots -`variants.ts` holds `tv`; **`Shell`** applies slot class names on its wrapping tags only (no duplicate Tailwind strings for those regions in the same file). +`variants.ts` holds `tv`; **`ImageCardShell`** applies slot class names on its wrapping tags only (no duplicate Tailwind strings for those regions in the same file). ```ts // ImageCard/variants.ts — Good @@ -179,7 +179,7 @@ export const imageCard = tv({ }); ``` -`Shell` calls **`imageCard()`** (or **`imageCard({ … })`** when the layout has variants), destructures **`shell`**, **`image`**, and **`text`**, and mounts each slot’s class name on a **wrapper element** around the prop node. **`Image`** and **`Text`** supply semantics and styling for media and copy; **`Shell`** only owns the **card layout slot wrappers**. +**`ImageCardShell`** calls **`imageCard()`** (or **`imageCard({ … })`** when the layout has variants), destructures **`shell`**, **`image`**, and **`text`**, and mounts each slot's class name on a **wrapper element** around the prop node. **`Image`** and **`Text`** supply semantics and styling for media and copy; **`ImageCardShell`** only owns the **card layout slot wrappers**. ```tsx // ImageCard/ImageCardShell.tsx — Good — slot class names on wrapping tags @@ -197,14 +197,15 @@ export function ImageCardShell({ image, text }: { image: React.ReactNode; text: ``` ```tsx -// ImageCard/ImageCard.tsx — Good — Root owns logic; Shell receives region nodes as props +// ImageCard/ImageCardRoot.tsx — Good — Root owns logic; Shell receives region nodes as props import { Image, Text } from "@probo/ui"; +import { ImageCardShell } from "./ImageCardShell"; function ImageCardRoot({ image, text }: { image: React.ReactNode; text: React.ReactNode }) { const id = useId(); // state, effects, data wiring … return ( - {image}} text={{text}} /> @@ -212,10 +213,10 @@ function ImageCardRoot({ image, text }: { image: React.ReactNode; text: React.Re } // Bad — Shell takes regions as children instead of image / text props -// +// // … // … -// +// // Bad — data hooks or state live on Shell function ImageCardShellWithData({ image, text }: { image: React.ReactNode; text: React.ReactNode }) { @@ -233,31 +234,31 @@ function ImageCardShellWithData({ image, text }: { image: React.ReactNode; text: ## Skeleton placement and composition -For compound components, the **card-level skeleton is not nested** on the module as `ImageCard.Skeleton`. Export **`ImageCardSkeleton`** as a **separate top-level symbol** (e.g. `ImageCardSkeleton.tsx` or the folder barrel) so routes can depend on **loading UI + shell layout** without importing the full `ImageCard` graph—smaller initial bundles for skeleton-first views. That also avoids pulling in **Radix UI** and other dependencies that are **not needed at load time** for the skeleton-only path. +For compound components, export **`ImageCardSkeleton`** as a **separate named export** (e.g. `ImageCardSkeleton.tsx` or the folder barrel) so routes can depend on **loading UI + shell layout** without importing the full `ImageCardRoot` graph—smaller initial bundles for skeleton-first views. That also avoids pulling in **Radix UI** and other dependencies that are **not needed at load time** for the skeleton-only path. -**Implementation:** `ImageCardSkeleton` should **reuse the same layout as the real card** by rendering **`ImageCard.Shell`** with the same **`image` / `text` props** as **`ImageCard.Root`**, but passing **skeleton primitives** instead of **`Image`** / **`Text`**: +**Implementation:** `ImageCardSkeleton` should **reuse the same layout as the real card** by rendering **`ImageCardShell`** with the same **`image` / `text` props** as **`ImageCardRoot`**, but passing **skeleton primitives** instead of **`Image`** / **`Text`**: - **`image`** → **`ImageSkeleton`** - **`text`** → **`TextSkeleton`** -`Root` composes real content with **`Image`** and **`Text`** (same imports as elsewhere in the app). The skeleton passes **`ImageSkeleton`** and **`TextSkeleton`** directly into **`Shell`** so loading views avoid **`Image`** / **`Text`** when that keeps bundles or behavior simpler. +**`ImageCardRoot`** composes real content with **`Image`** and **`Text`** (same imports as elsewhere in the app). The skeleton passes **`ImageSkeleton`** and **`TextSkeleton`** directly into **`ImageCardShell`** so loading views avoid **`Image`** / **`Text`** when that keeps bundles or behavior simpler. Reuse existing **`ImageSkeleton`** / **`TextSkeleton`** from typography or media primitives when available; avoid duplicate one-off pulse blocks. -### Do / don’t: skeleton imports and composition +### Do / don't: skeleton imports and composition ```tsx -// Bad — skeleton nested on the compound object (pulls full card module into the route) +// Bad — skeleton nested on a namespace object (pulls full card module into the route) import { ImageCard } from "@probo/ui"; -// Good — top-level skeleton export; reuse Shell + ImageSkeleton / TextSkeleton -import { ImageCard, ImageCardSkeleton } from "@probo/ui"; +// Good — each sub-component is a standalone named export +import { ImageCardShell, ImageCardSkeleton } from "@probo/ui"; // Inside ImageCardSkeleton.tsx (conceptually): export function ImageCardSkeleton() { return ( - } text={} /> @@ -265,20 +266,20 @@ export function ImageCardSkeleton() { } ``` -The important part is **separate `ImageCardSkeleton` export**, **one `Shell` API** (`image` / `text` props), **shared shell layout**, and **reused `ImageSkeleton` / `TextSkeleton`**. +The important part is **separate `ImageCardSkeleton` export**, **one `ImageCardShell` API** (`image` / `text` props), **shared shell layout**, and **reused `ImageSkeleton` / `TextSkeleton`**. ## Variants file -Keep the **`tv({ slots: { … } })` definition** (and derived slot functions) in a **standalone file**, conventionally **`variants.ts`** next to the component folder. Import it from **`Shell`** and **skeleton** modules so skeleton entry points can pull **variants + shell** without the rest of the compound module’s business logic. +Keep the **`tv({ slots: { … } })` definition** (and derived slot functions) in a **standalone file**, conventionally **`variants.ts`** next to the component folder. Import it from **`ImageCardShell`** and **skeleton** modules so skeleton entry points can pull **variants + shell** without the rest of the compound component's business logic. -### Do / don’t: colocating `tv` with the heavy module +### Do / don't: colocating `tv` with the heavy module ```tsx -// Bad — variants defined only inside ImageCard.tsx; ImageCardSkeleton imports ImageCard and drags Root / hooks -// ImageCard.tsx +// Bad — variants defined only inside ImageCardRoot.tsx; ImageCardSkeleton imports it and drags Root / hooks +// ImageCardRoot.tsx const imageCard = tv({ slots: { shell: "...", image: "...", text: "..." } }); -// Good — shared variants module imported by Shell and ImageCardSkeleton only +// Good — shared variants module imported by ImageCardShell and ImageCardSkeleton only // variants.ts — export imageCard (or slot helpers) // ImageCardShell.tsx — import { imageCard } from "./variants" // ImageCardSkeleton.tsx — import { imageCard } from "./variants"