@@ -6,11 +6,11 @@ Conventions for organising pages, routes, and supporting files in Probo frontend
|
||||
|
||||
## 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) |
|
||||
| Relay queries, fragments, loaders, `queryRef` | [`contrib/claude/relay.md`](relay.md) |
|
||||
| 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) |
|
||||
| Relay queries, fragments, loaders, `queryRef` | [`contrib/claude/relay.md`](relay.md) |
|
||||
|
||||
## Single arborescence principle
|
||||
|
||||
@@ -58,14 +58,84 @@ import { compliancePageRoutes } from "./pages/organizations/compliance-page/rout
|
||||
|
||||
Each page folder may contain a subset of these files. Names use PascalCase matching the feature.
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `routes.ts` | Route definitions for this folder. Exports an array spread into the parent route tree. Uses `lazy()` from `@probo/react-lazy` to point at loaders / pages. |
|
||||
| `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`. |
|
||||
| `MyPage.tsx` | The actual page component. Receives `queryRef` from the loader, calls `usePreloadedQuery`. |
|
||||
| `MyPageSkeleton.tsx` | `Suspense` fallback rendered while the page is still receiving data. Also used as the route-level `Fallback`. |
|
||||
| `MyPageError.tsx` | Error boundary rendering component for this page's error state. |
|
||||
| `_components/` | Sub-components scoped to this page (see [below](#_components-folder)). |
|
||||
| File | Role |
|
||||
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `routes.ts` | Route definitions for this folder. Exports an array spread into the parent route tree. Uses `lazy()` from `@probo/react-lazy` to point at loaders / pages. |
|
||||
| `MyLayout.tsx` | A **layout route** component that renders shared chrome (`Breadcrumb`, `PageHeader`, `Tabs`, …) and an `<Outlet />`. Named with the **`Layout` suffix** — never `Page` — to make its role obvious at a glance. |
|
||||
| `MyLayoutLoader.tsx` | Loader for a layout that needs data (same pattern as `MyPageLoader`). |
|
||||
| `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. |
|
||||
| `_components/` | Sub-components scoped to this page (see [below](#_components-folder)). |
|
||||
|
||||
### Layout vs Page naming
|
||||
|
||||
A component is a **layout** when it renders `<Outlet />` and exists to provide shared UI (breadcrumbs, tabs, page header) around child routes. It is a **page** when it renders final content with no `<Outlet />`.
|
||||
|
||||
Use the correct suffix so the role is clear from the file name alone:
|
||||
|
||||
```text
|
||||
// Bad — a layout route named as a "Page"
|
||||
VendorDetailPage.tsx # renders <Outlet />, wraps child routes
|
||||
CookieBannerConfigPage.tsx # renders tabs + <Outlet />
|
||||
|
||||
// Good — layout routes use the "Layout" suffix
|
||||
VendorDetailLayout.tsx
|
||||
CookieBannerConfigLayout.tsx
|
||||
```
|
||||
|
||||
## When do you need a loader, query, or Relay provider?
|
||||
|
||||
The loader / query / provider scaffolding exists to support Relay — it isn't boilerplate to copy into every page. Match the files to what the page actually does:
|
||||
|
||||
| Page has… | Files needed |
|
||||
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| No Relay data and no mutation | `MyPage.tsx` only. `lazy()` imports it directly. No loader, no skeleton, no provider, no query. |
|
||||
| A mutation only (no query) | `MyPage.tsx` wrapped in the appropriate Relay provider (e.g. `IAMRelayProvider`, `CoreRelayProvider`). No loader, no skeleton, no query. |
|
||||
| A query (with or without a mutation) | Full pattern: `MyPageLoader.tsx` (provider + `useQueryLoader` + skeleton) → `MyPage.tsx` (`usePreloadedQuery`) + `MyPageSkeleton.tsx`. |
|
||||
|
||||
Layouts follow the same rule. A layout that only renders a `PageHeader` and an `<Outlet />` does **not** need a query, loader, skeleton, or provider — just a plain component imported by `lazy()` in `routes.ts`.
|
||||
|
||||
### Example: mutation only, no query
|
||||
|
||||
See [`pages/iam/organizations/NewOrganizationPage.tsx`](../../apps/console/src/pages/iam/organizations/NewOrganizationPage.tsx) for a reference. The page is the default export, wraps its inner component in `IAMRelayProvider` so the mutation has a Relay environment, and has no loader / skeleton / query.
|
||||
|
||||
```tsx
|
||||
// pages/iam/organizations/NewOrganizationPage.tsx
|
||||
function NewOrganizationPageInner() {
|
||||
const [createOrganization, isCreating]
|
||||
= useMutation<NewOrganizationPageMutation>(createOrganizationMutation);
|
||||
// …
|
||||
}
|
||||
|
||||
export default function NewOrganizationPage() {
|
||||
return (
|
||||
<IAMRelayProvider>
|
||||
<NewOrganizationPageInner />
|
||||
</IAMRelayProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Example: no data at all
|
||||
|
||||
```tsx
|
||||
// pages/organizations/cookie-banners/CookieBannerLayout.tsx
|
||||
export default function CookieBannerLayout() {
|
||||
const { __ } = useTranslate();
|
||||
usePageTitle(__("Cookie Banners"));
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader title={__("Cookie Banners")} />
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
`routes.ts` points `lazy()` at `CookieBannerLayout` directly — no `CookieBannerLayoutLoader`, no `cookieBannerLayoutQuery`, no skeleton.
|
||||
|
||||
### `routes.ts`
|
||||
|
||||
@@ -87,11 +157,11 @@ export const vendorRoutes = [
|
||||
{
|
||||
path: "vendors/:vendorId",
|
||||
Fallback: VendorsPageSkeleton,
|
||||
Component: lazy(() => import("./VendorDetailPageLoader")),
|
||||
Component: lazy(() => import("./VendorDetailLayoutLoader")),
|
||||
children: [
|
||||
{
|
||||
path: "overview",
|
||||
Component: lazy(() => import("./tabs/VendorOverviewTab")),
|
||||
Component: lazy(() => import("./overview/VendorOverviewPage")),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -198,11 +268,11 @@ pages/organizations/vendors/VendorsPageSkeleton.tsx
|
||||
|
||||
Sub-components that are used **only** by a single page live in a `_components/` folder next to that page. The underscore prefix visually distinguishes them from route-segment folders.
|
||||
|
||||
| Situation | Where the component lives |
|
||||
|-----------|--------------------------|
|
||||
| Used by one page only | `pages/organizations/vendors/_components/` |
|
||||
| Situation | Where the component lives |
|
||||
| ------------------------------------------ | ---------------------------------------------------------------------------------- |
|
||||
| Used by one page only | `pages/organizations/vendors/_components/` |
|
||||
| Used by multiple pages in the same feature | Nearest common ancestor's `_components/` (e.g. `pages/organizations/_components/`) |
|
||||
| Reusable UI primitive | `@probo/ui` package |
|
||||
| Reusable UI primitive | `@probo/ui` package |
|
||||
|
||||
### Do / don't: component placement
|
||||
|
||||
@@ -223,6 +293,29 @@ src/components/VendorContactRow.tsx # only used by VendorContactsTab
|
||||
pages/organizations/vendors/_components/VendorContactRow.tsx
|
||||
```
|
||||
|
||||
## Child-route folder naming
|
||||
|
||||
Folders that contain child-route pages are named after the **resource or concept** the page represents, not after the UX component that currently renders them. UX patterns change (tabs become pages, drawers become routes, etc.); the resource name stays stable.
|
||||
|
||||
### Do / don't: child-route folders
|
||||
|
||||
```text
|
||||
// Bad — folder named after a UI element
|
||||
configuration/
|
||||
tabs/ # "tabs" is a UI component, not a resource
|
||||
VendorOverviewTab.tsx
|
||||
VendorComplianceTab.tsx
|
||||
|
||||
// Good — folders named after the resource each child route represents
|
||||
configuration/
|
||||
overview/
|
||||
VendorOverviewPage.tsx
|
||||
compliance/
|
||||
VendorCompliancePage.tsx
|
||||
```
|
||||
|
||||
This also means child-route components use the `*Page` suffix (not `*Tab`), because they are pages in their own right — the fact that a tab bar navigates between them is an implementation detail of the parent layout.
|
||||
|
||||
## Full example tree
|
||||
|
||||
Target layout for a `vendors` feature under `pages/organizations/`:
|
||||
@@ -233,14 +326,23 @@ pages/organizations/vendors/
|
||||
VendorsPageLoader.tsx # lazy entry — providers + Suspense + query loader
|
||||
VendorsPage.tsx # page component (usePreloadedQuery)
|
||||
VendorsPageSkeleton.tsx # loading fallback
|
||||
<<<<<<< HEAD
|
||||
VendorDetailPageLoader.tsx # lazy entry for detail view
|
||||
VendorDetailPage.tsx # detail page component
|
||||
VendorDetailPageSkeleton.tsx # detail loading fallback
|
||||
=======
|
||||
VendorDetailLayoutLoader.tsx # lazy entry for detail layout
|
||||
VendorDetailLayout.tsx # layout — breadcrumbs, tabs, <Outlet />
|
||||
VendorDetailLayoutSkeleton.tsx # detail loading fallback
|
||||
NewVendorPage.tsx # mutation-only page — default export, wraps itself in the Relay provider
|
||||
>>>>>>> 4dcc034f7 (Update contrib guides)
|
||||
_components/ # sub-components used only by vendor pages
|
||||
VendorContactRow.tsx
|
||||
VendorRiskSummary.tsx
|
||||
tabs/ # tab content for the detail page
|
||||
VendorOverviewTab.tsx
|
||||
VendorComplianceTab.tsx
|
||||
VendorContactsTab.tsx
|
||||
overview/ # child route: /vendors/:vendorId/overview
|
||||
VendorOverviewPage.tsx
|
||||
compliance/ # child route: /vendors/:vendorId/compliance
|
||||
VendorCompliancePage.tsx
|
||||
contacts/ # child route: /vendors/:vendorId/contacts
|
||||
VendorContactsPage.tsx
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# UI system (`@probo/ui`)
|
||||
|
||||
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.
|
||||
Shared React UI for Probo apps lives in the **`@probo/ui`** workspace package ([`packages/ui`](../../packages/ui)), as well as ad-hoc components created in apps (under `apps/*/src`). This document describes **target** conventions for building and styling those components, whether shared or app-local.
|
||||
|
||||
**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.
|
||||
|
||||
@@ -89,6 +89,36 @@ export function Row({ bordered, children }: { bordered?: boolean; children: Reac
|
||||
}
|
||||
```
|
||||
|
||||
## Icons
|
||||
|
||||
Icons come from two sources, in this order of preference:
|
||||
|
||||
1. **`@phosphor-icons/react`** — the default icon library. Import the specific icon directly: `import { CookieIcon } from "@phosphor-icons/react"`. Prefer phosphor whenever it has the icon you need; it covers the vast majority of use cases and keeps the iconography consistent across the product.
|
||||
2. **`@probo/ui` `Icon*` set** — the curated, in-house icons (`IconBank`, `IconShield`, `IconCircleCheck`, …). Use these only when phosphor doesn't have a suitable equivalent, or when you specifically need a bespoke Probo-branded icon.
|
||||
|
||||
**Never use emoji characters (🍪, ✅, ⚠️, …) as icons in UI.** Emojis render inconsistently across platforms, don't inherit `currentColor`, and can't be sized or styled like an SVG. If neither `@phosphor-icons/react` nor `@probo/ui` has what you need, add the missing icon to `@probo/ui` rather than falling back to emoji.
|
||||
|
||||
### Do / don't: icon source
|
||||
|
||||
```tsx
|
||||
// Bad — emoji used as an icon
|
||||
<div className="mb-2 text-4xl">🍪</div>
|
||||
```
|
||||
|
||||
```tsx
|
||||
// Good — phosphor icon as the default choice
|
||||
import { CookieIcon } from "@phosphor-icons/react";
|
||||
|
||||
<CookieIcon size={48} weight="duotone" className="text-muted-foreground" />
|
||||
```
|
||||
|
||||
```tsx
|
||||
// Good — @probo/ui icon when phosphor has no suitable equivalent
|
||||
import { IconShield } from "@probo/ui";
|
||||
|
||||
<IconShield className="size-6 text-muted-foreground" />
|
||||
```
|
||||
|
||||
## Folder layout
|
||||
|
||||
**Simple and layout primitives** belong in **usage-oriented** folders:
|
||||
|
||||
Reference in New Issue
Block a user