diff --git a/contrib/claude/app-arborescence.md b/contrib/claude/app-arborescence.md
index b6499f465..f1cbd4177 100644
--- a/contrib/claude/app-arborescence.md
+++ b/contrib/claude/app-arborescence.md
@@ -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 ``. 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 `` and exists to provide shared UI (breadcrumbs, tabs, page header) around child routes. It is a **page** when it renders final content with no ``.
+
+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 , wraps child routes
+CookieBannerConfigPage.tsx # renders tabs +
+
+// 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 `` 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(createOrganizationMutation);
+ // …
+}
+
+export default function NewOrganizationPage() {
+ return (
+
+
+
+ );
+}
+```
+
+### Example: no data at all
+
+```tsx
+// pages/organizations/cookie-banners/CookieBannerLayout.tsx
+export default function CookieBannerLayout() {
+ const { __ } = useTranslate();
+ usePageTitle(__("Cookie Banners"));
+
+ return (
+
+ );
+}
+```
+
+`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,
+ 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
```
diff --git a/contrib/claude/ui.md b/contrib/claude/ui.md
index c4db06575..54bd8cbbe 100644
--- a/contrib/claude/ui.md
+++ b/contrib/claude/ui.md
@@ -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
+🍪
+```
+
+```tsx
+// Good — phosphor icon as the default choice
+import { CookieIcon } from "@phosphor-icons/react";
+
+
+```
+
+```tsx
+// Good — @probo/ui icon when phosphor has no suitable equivalent
+import { IconShield } from "@probo/ui";
+
+
+```
+
## Folder layout
**Simple and layout primitives** belong in **usage-oriented** folders: