Rework frontend rules for the v2 UI kit
Make contrib/claude the single source of truth for v2 frontend work on the compliance-portal app and packages/ui/src/v2, treating console and the legacy @probo/ui tree as non-compliant code to migrate rather than precedent. Rewrite ui.md around the v2 kit: flat folders, Base UI as the headless layer styled thinly (controlled open/onOpenChange, no imperative ref or cloneElement plumbing), tailwind-variants only, separate components over structure-changing variants, and bundle-safe skeletons that never drag Base UI into the loading path. Add a naming/suffix taxonomy to react-components.md, replacing the Table/Row and connection-item Card suffixes with List/ListItem, and add an error/fallback props convention. Document _lib and _locales special folders plus routes.ts placement in app-arborescence.md, with at most one _locales per routes.ts. Add error-handling.md (reusable ErrorBoundary usable at any level plus async try/catch) and i18n.md (i18next key-based catalogs). Update the relay file-organization and fragment examples, the connection-item cursor rule, and the AGENTS.md index to match. Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
@@ -1,16 +1,18 @@
|
||||
# App arborescence (folder and file layout)
|
||||
|
||||
Conventions for organising pages, routes, and supporting files in Probo frontend apps (`apps/console`). The guiding principle is **one arborescence**: the route hierarchy is expressed once, through the `pages/` folder tree, and everything related to a route lives next to it.
|
||||
Conventions for organising pages, routes, and supporting files in Probo frontend apps (`apps/compliance-portal`, `apps/console`). The guiding principle is **one arborescence**: the route hierarchy is expressed once, through the `pages/` folder tree, and everything related to a route lives next to it.
|
||||
|
||||
**The codebase does not fully match these rules yet.** Some route definitions still live in a separate `src/routes/` folder. Treat this guide as the target for new work and refactors.
|
||||
These rules are the **source of truth**. Where existing code disagrees (e.g. `apps/console` still keeps some route definitions in a separate `src/routes/` folder), the code is non-compliant and should be migrated — it is not precedent.
|
||||
|
||||
## Related guides
|
||||
|
||||
| Topic | Guide |
|
||||
| -------------------------------------------------------------------------------- | ----------------------------------------------------------- |
|
||||
| `@probo/ui`, Tailwind, `tailwind-variants`, folders, skeletons, compound modules | [`contrib/claude/ui.md`](ui.md) |
|
||||
| React component shape, props, file/export conventions | [`contrib/claude/react-components.md`](react-components.md) |
|
||||
| React component shape, props, file/export conventions, naming/suffixes | [`contrib/claude/react-components.md`](react-components.md) |
|
||||
| Relay queries, fragments, loaders, `queryRef` | [`contrib/claude/relay.md`](relay.md) |
|
||||
| Error boundaries at any level, error/fallback props | [`contrib/claude/error-handling.md`](error-handling.md) |
|
||||
| i18next translations and `_locales` folders | [`contrib/claude/i18n.md`](i18n.md) |
|
||||
|
||||
## Single arborescence principle
|
||||
|
||||
@@ -66,8 +68,10 @@ Each page folder may contain a subset of these files. Names use PascalCase match
|
||||
| `MyPageLoader.tsx` | Bundle entry point imported by `lazy()` in the route. **Default export.** loads data via Relay, renders a skeleton while loading, then mounts the page with `queryRef`. Only needed when the page reads data. |
|
||||
| `MyPage.tsx` | The actual page component. Receives `queryRef` from the loader (when data is loaded), or is the **default export** directly imported by `lazy()` when no data is needed. |
|
||||
| `MyPageSkeleton.tsx` | `Suspense` fallback rendered while the page is still receiving data. Also used as the route-level `Fallback`. Only needed when the page reads data. |
|
||||
| `MyPageError.tsx` | Error boundary rendering component for this page's error state. |
|
||||
| `MyPageError.tsx` | Error UI rendered by a boundary for this page (see [`error-handling.md`](error-handling.md)). An `*Error` file may exist at **any** level, not just the route root. |
|
||||
| `_components/` | Sub-components scoped to this page (see [below](#_components-folder)). |
|
||||
| `_lib/` | Non-component helpers scoped to this subtree: hooks, utilities, constants, types (see [below](#_lib-folder)). |
|
||||
| `_locales/` | i18next translation catalogs for this route segment, one file per locale (`en-US.json`). Colocated with `routes.ts` (see [below](#_locales-folder)). |
|
||||
|
||||
### Layout vs Page naming
|
||||
|
||||
@@ -244,7 +248,7 @@ export function ThirdPartiesPageError() {
|
||||
|
||||
## File naming
|
||||
|
||||
Component files (`.tsx` that export a React component) use **PascalCase**: `ThirdPartiesPage.tsx`, `ThirdPartyContactRow.tsx`, `ThirdPartiesPageSkeleton.tsx`.
|
||||
Component files (`.tsx` that export a React component) use **PascalCase**: `ThirdPartiesPage.tsx`, `ThirdPartyContactListItem.tsx`, `ThirdPartiesPageSkeleton.tsx`.
|
||||
|
||||
All other helper files (utilities, hooks, constants, configuration) use **camelCase**: `routes.ts`, `useThirdPartyFilters.ts`, `formatCurrency.ts`, `constants.ts`.
|
||||
|
||||
@@ -332,10 +336,53 @@ pages/organizations/_components/StatusBadge.tsx
|
||||
|
||||
```text
|
||||
// Bad — page-specific helper placed in a global folder
|
||||
src/components/ThirdPartyContactRow.tsx # only used by ThirdPartyContactsTab
|
||||
src/components/ThirdPartyContactListItem.tsx # only used by the third-parties feature
|
||||
|
||||
// Good — scoped to the page that uses it
|
||||
pages/organizations/third-parties/_components/ThirdPartyContactRow.tsx
|
||||
pages/organizations/third-parties/_components/ThirdPartyContactListItem.tsx
|
||||
```
|
||||
|
||||
## `_lib` folder
|
||||
|
||||
Non-component code scoped to a subtree — hooks, utilities, constants, types — lives in a `_lib/` folder next to the pages that use it. The same hoisting rule as `_components/` applies: shared helpers move to the nearest common ancestor's `_lib/`; truly global helpers live in `src/lib/`. Files in `_lib/` use camelCase (`useThirdPartyFilters.ts`, `formatCurrency.ts`, `constants.ts`).
|
||||
|
||||
```text
|
||||
// Good — feature-scoped helpers under _lib
|
||||
pages/organizations/third-parties/
|
||||
_lib/
|
||||
useThirdPartyFilters.ts
|
||||
formatThirdPartyStatus.ts
|
||||
_components/
|
||||
ThirdPartyListItem.tsx
|
||||
```
|
||||
|
||||
## `_locales` folder
|
||||
|
||||
Translations are i18next catalogs in a `_locales/` folder, **one file per locale**, named by locale tag: `en-US.json`, `fr-FR.json`. See [`contrib/claude/i18n.md`](i18n.md) for key conventions and setup.
|
||||
|
||||
`_locales/` is colocated with a `routes.ts`. The rule of thumb:
|
||||
|
||||
- A `_locales/` folder belongs at a folder that **names a resource and owns a `routes.ts`** (e.g. `organizations/routes.ts` + `organizations/_locales/`, `organizations/measures/routes.ts` + `organizations/measures/_locales/`).
|
||||
- There must be **no more `_locales/` folders than there are `routes.ts` files.** If a folder has no `routes.ts`, it does not get its own `_locales/` — its strings live in the nearest ancestor that does.
|
||||
|
||||
```text
|
||||
// Good — _locales sits beside routes.ts at each resource boundary
|
||||
pages/organizations/
|
||||
routes.ts
|
||||
_locales/
|
||||
en-US.json
|
||||
fr-FR.json
|
||||
measures/
|
||||
routes.ts
|
||||
_locales/
|
||||
en-US.json
|
||||
fr-FR.json
|
||||
MeasuresPage.tsx
|
||||
|
||||
// Bad — a _locales folder with no sibling routes.ts (strings belong to the parent resource)
|
||||
pages/organizations/measures/_components/
|
||||
_locales/
|
||||
en-US.json
|
||||
```
|
||||
|
||||
## Child-route folder naming
|
||||
@@ -368,16 +415,22 @@ Target layout for a `third-parties` feature under `pages/organizations/`:
|
||||
```text
|
||||
pages/organizations/third-parties/
|
||||
routes.ts # route definitions for third parties
|
||||
_locales/ # i18next catalogs (one _locales per routes.ts)
|
||||
en-US.json
|
||||
fr-FR.json
|
||||
ThirdPartiesPageLoader.tsx # lazy entry — providers + Suspense + query loader
|
||||
ThirdPartiesPage.tsx # page component (usePreloadedQuery)
|
||||
ThirdPartiesPageSkeleton.tsx # loading fallback
|
||||
ThirdPartiesPageError.tsx # error UI for this page's boundary
|
||||
ThirdPartyDetailLayoutLoader.tsx # lazy entry for detail layout
|
||||
ThirdPartyDetailLayout.tsx # layout — breadcrumbs, tabs, <Outlet />
|
||||
ThirdPartyDetailLayoutSkeleton.tsx # detail loading fallback
|
||||
NewThirdPartyPage.tsx # mutation-only page — default export, wraps itself in the Relay provider
|
||||
_lib/ # hooks / utils / constants scoped to third party pages
|
||||
useThirdPartyFilters.ts
|
||||
_components/ # sub-components used only by third party pages
|
||||
ThirdPartyContactRow.tsx
|
||||
ThirdPartyRiskSummary.tsx
|
||||
ThirdPartyContactListItem.tsx
|
||||
ThirdPartyRiskSummarySection.tsx
|
||||
overview/ # child route: /third-parties/:thirdPartyId/overview
|
||||
ThirdPartyOverviewPage.tsx
|
||||
compliance/ # child route: /third-parties/:thirdPartyId/compliance
|
||||
|
||||
Reference in New Issue
Block a user