UI patterns
Primebrick standardizes detail pages (any non-table page: form pages, settings sub-pages, entity edit/create pages) around two layouts. Following these layouts keeps validation UX, button hierarchy, and card wrapping consistent across the app.
What is a detail page?
A detail page is any page that is not a table/list page. Examples:
/system/settings/profile— profile form/system/settings/credentials— change password + passkeys + MFA/system/settings/organizations/create— org create form/system/settings/organizations/{uuid}— org edit form
Table pages (entity lists) use EntityListTable and are covered in
Entity list table. This page
only covers detail pages.
The two layouts
Layout 1 — Pure form (TUTTO FORM)
The entire content is a single 2-column form (grid grid-cols-2 gap-6)
with no other sections (no in-card lists, no extra cards, no info boxes).
Rules:
- Use
FormPageLayout— it provides the outer card wrapper (rounded-md border bg-background) and the audit footer. - The DEFAULT primary button lives in the footer via the
footerActionssnippet. - No other primary button may appear inside the content — the footer already holds the primary action.
- The footer MAY contain multiple CTAs (primary + secondary) if needed (e.g. Cancel + Save, Delete + Save).
- The form is NOT wrapped in an extra
<Card>—FormPageLayoutalready provides the wrapper. - The form keeps the 2-column grid, validation (
use:enhance,FormField/FormLabel/FormControl), andPasswordChecklist/custom components as needed.
Example — the Profile settings page:
Code
Layout 2 — Mixed content (UN PO' FORM + UN PO' ALTRO)
The page mixes a form with other content — in-card lists, info boxes, multiple cards, etc.
Rules:
- Use
AppPageScaffold(NOTFormPageLayout) — there is no single footer primary to render. - No primary button in the footer. The footer (if any) may hold only secondary/tertiary actions.
- Each card that needs an action puts its own DEFAULT primary button
inside the card content (
variant="default"with defaulttone="primary"— the full sky-to-indigo gradient, white text). - The form keeps ALL its characteristics: 2-column grid, validation,
use:enhance,FormField/FormLabel/FormControlblocks, etc. - The form MUST be wrapped in a
<Card>—AppPageScaffolddoes not provide a per-section card wrapper, only the outer page shell.
Example — the Credentials settings page (3 cards: Change Password, Passkeys, MFA):
Code
Button variants recap
The Button component (src/lib/components/ui/button/) supports these
variants relevant to detail pages:
| Variant | Tone | Look | Use on detail pages |
|---|---|---|---|
default | primary (default) | Full sky→indigo gradient, white text | DEFAULT PRIMARY — footer CTA in Layout 1, in-card CTA in Layout 2 |
soft | primary | Gradient border + subtle background, dark text | In-card CTA only in Layout 1 when a footer primary already exists |
outline | — | Border with gradient, hover background | Secondary action (Cancel, etc.) |
destructive | — | Rose→red gradient | Delete / irreversible action |
ghost | — | Transparent, hover background | Icon-only actions (trash, settings) |
Soft primary vs default primary
variant="soft" tone="primary" (gradient border, subtle background, dark text)
is used for in-card CTAs only when a DEFAULT primary already exists in the
footer (Layout 1 with extra in-card actions). When there is no footer primary
(Layout 2), in-card CTAs are DEFAULT primary — never soft primary.
In-card list pattern
When a card contains a list of records (e.g. enrolled passkeys, MFA factors), use this shared structure so all in-card lists look consistent:
- Card title with an icon (
CardTitle class="flex items-center gap-2"+ a Lucide™ icon atsize-5). - Card subtitle via
CardDescription. - List items with
rounded-md border-primary-gradient px-3 py-2(the gradient border utility fromsrc/app.css). - Item icon at
size-5 text-muted-foreground shrink-0 mt-0.5. - Item title at
text-sm font-medium truncate. - Item meta lines at
text-xs text-muted-foreground. - Delete button at
variant="ghost" size="sm"with aTrash2icon atsize-4and ansr-onlylabel for accessibility. - Empty state using the huge-icon pattern (see below).
- Card-level CTA as a DEFAULT primary button rendered after the list (works both when the list is empty and when it has items).
Empty state (huge icon)
When the list is empty, render the moduli-style empty state instead of a plain text paragraph:
Code
The pb-watermark-empty class (defined in src/app.css) applies a bounce +
opacity-pulse animation to the icon. The empty-state text uses two lines:
a primary line (emptyTitle, e.g. "No passkey found") and a hint line
(emptyHint, e.g. "Add a passkey to sign in faster without a password.").
Both are distinct in weight and size from the card subtitle.
The card-level CTA renders below the empty state, so the user sees the "no record found" message and the primary action to add one in the same card.
Decision checklist
Before adding any button to a detail page, answer these questions:
-
Is the entire content a single 2-col form with no other sections?
- Yes → Layout 1. Put the DEFAULT primary in
footerActions. Do not add any other primary inside the content. Do not wrap the form in a<Card>. - No → Layout 2. Use
AppPageScaffold. Do not put a primary in the footer. Put a DEFAULT primary inside each card that needs an action. Wrap any form in a<Card>.
- Yes → Layout 1. Put the DEFAULT primary in
-
Do I need a secondary action (Cancel, Delete) alongside the primary?
- Layout 1 → put both in
footerActions(primary + secondary). - Layout 2 → put the secondary inside its own card or next to the primary inside the relevant card.
- Layout 1 → put both in
-
Is the in-card action in a Layout 1 page (extra action inside a card while the footer already has the primary)?
- Use
variant="soft" tone="primary"for that in-card action. - Never use DEFAULT primary inside a Layout 1 card.
- Use
-
Is the in-card action in a Layout 2 page (no footer primary)?
- Use DEFAULT primary (
variant="default", defaulttone="primary"). - Never use soft primary as the main card CTA in Layout 2.
- Use DEFAULT primary (
Real examples in the codebase
| Page | Layout | Scaffold | Primary CTA location |
|---|---|---|---|
/system/settings/profile | 1 (pure form) | FormPageLayout | Footer (Save) |
/system/settings/credentials | 2 (mixed) | AppPageScaffold | Inside each of the 3 cards |
/system/settings/organizations/create | 1 (pure form) | FormPageLayout | Footer (Save) |
/system/settings/organizations/{uuid} | 1 (pure form) | FormPageLayout | Footer (Save) |
/system/settings/security | 2 (mixed) | AppPageScaffold | Footer (Save OIDC) + Delete Account |
Next steps
- UI components — custom components built on Shadcn-Svelte™
- FormPageLayout — the Layout 1 scaffold with audit footer
- AppPageScaffold — the Layout 2 scaffold for mixed-content pages
- Entity list table — table pages (not covered by this page)
- Settings — the settings module overview