Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 45 additions & 35 deletions content/docs/guide/layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,59 +194,69 @@ Available values:

## PageHeader Component

The `PageHeader` provides consistent page headers with title, breadcrumbs, and actions.
The `PageHeader` provides consistent page headers with a title, an optional subtitle,
an icon chip, and an action row.

### Usage

```json
{
"type": "page-header",
"title": "Customer Details",
"description": "View and edit customer information",
"breadcrumbs": [
{ "label": "Home", "href": "/" },
{ "label": "Customers", "href": "/customers" },
{ "label": "John Doe" }
],
"actions": [
{
"type": "button",
"text": "Edit",
"variant": "default",
"icon": "pencil"
},
{
"type": "button",
"text": "Delete",
"variant": "destructive",
"icon": "trash"
}
]
"subtitle": "View and edit customer information",
"icon": "users",
"actions": ["edit", "delete"]
}
```

`title` and `subtitle` both interpolate `{field.path}` tokens against the surrounding
record context, so `"title": "{first_name} {last_name}"` resolves on a record page.
Unresolvable tokens collapse to an empty string rather than leaking the raw template.

### Schema API

```typescript
{
type: 'page-header',

title: string,
description?: string,
icon?: string,

breadcrumbs?: Array<{
label: string,
href?: string,
icon?: string
}>,

actions?: ComponentSchema[],

className?: string

title: string, // required; {field.path} tokens interpolated
subtitle?: string, // secondary line; {field.path} tokens interpolated
icon?: string, // Lucide icon name, rendered in a chip left of the title
actions?: Array<string | ActionDef>, // action ids, or inline ActionDef objects
showBack?: boolean, // back arrow; inferred from record context when omitted
children?: ComponentSchema[], // rendered into the right-aligned slot; `actions` takes precedence
className?: string,

description?: string // legacy alias of `subtitle` — do not author, see below
}
```

`showBack` defaults to `true` when a record context carrying a `recordId` is in scope and
the header is not rendered inside embedded chrome (drawer / modal, which already provide
their own Close control), and `false` otherwise. Pass it explicitly to override.

`actions` is handed to the `record:quick_actions` widget with
`location: 'record_header'`. Its entries are **action ids** — resolved from the object's
own `actions` metadata, which keeps the definitions in one place — or inline `ActionDef`
objects. They are **not** `ComponentSchema` nodes: a `{ "type": "button", … }` entry
renders nothing here.

> **Write `subtitle`, not `description`.** `@objectstack/spec/ui`'s `PageHeaderProps` —
> the contract for the canonical `page:header` node — declares
> `title / subtitle / icon / breadcrumb / actions / aria` and has **no** `description`,
> and `page-header`'s registration declares only `title` and `subtitle` as authorable
> inputs. `description` is still accepted in two places: protocol 17's ADR-0087 D2
> conversion `page-header-subtitle-alias` rewrites it to `subtitle` on header nodes as the
> stack loads, and this renderer reads it directly as a legacy alias (`subtitle` wins when
> both are set) for nodes that reach it without passing through that loader. `subtitle` is
> the only spelling that renders on **every** path, and the alias is on its way out
> (objectui#3789) — see the [PageHeader reference](/docs/layout/page-header).

> **There is no `breadcrumbs` array.** The component reads no breadcrumb property of any
> kind, in either spelling. The spec's `breadcrumb` is singular and a **boolean** — a
> display toggle on the canonical `page:header` node (see
> [Slotted pages](/docs/guide/slotted-pages)), not a list of links.

## SidebarNav Component

The `SidebarNav` provides a collapsible navigation sidebar with menu items.
Expand Down
75 changes: 58 additions & 17 deletions content/docs/layout/page-header.mdx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
---
title: "PageHeader"
description: "Page header component with title, description, and actions"
description: "Page header component with title, subtitle, and actions"
---

import { ComponentDemo, DemoGrid } from '@/app/components/ComponentDemo';

The PageHeader component provides a consistent header for pages with title, description, and action buttons.
The PageHeader component provides a consistent header for pages with title, subtitle, and action buttons.

## Basic Usage

Expand All @@ -14,7 +14,7 @@ import { PageHeader } from '@object-ui/layout';

<PageHeader
title="Dashboard"
description="Welcome to your dashboard"
subtitle="Welcome to your dashboard"
/>
```

Expand All @@ -25,15 +25,51 @@ import { PageHeader } from '@object-ui/layout';
## Component Props

```plaintext
interface PageHeaderComponentProps {
title: string; // Page title (required)
description?: string; // Page description
action?: React.ReactNode; // Action buttons or elements
children?: React.ReactNode; // Additional content (actions)
className?: string; // Additional CSS classes
interface PageHeaderComponentProps extends React.HTMLAttributes<HTMLDivElement> {
title: string; // Page title (required); {field.path} tokens interpolated
subtitle?: string; // Secondary line under the title — the canonical key
icon?: React.ReactNode | string; // Lucide icon name, or a pre-rendered node
action?: React.ReactNode; // Action buttons or elements (right-aligned slot)
actions?: unknown[]; // ActionDef list (or action ids) → record:quick_actions
showBack?: boolean; // Back arrow; inferred from record context when omitted
schema?: { children?: unknown[]; actions?: unknown[]; properties?: { actions?: unknown[] } };
children?: React.ReactNode; // Additional content (right-aligned slot)
className?: string; // Additional CSS classes

// DEPRECATED: legacy alias of `subtitle`. Still read, but do not author it.
description?: string;
}
```

`subtitle` is the canonical key: `@objectstack/spec/ui`'s `PageHeaderProps` — the contract
for the authored `page:header` node — declares `title / subtitle / icon / breadcrumb /
actions / aria` and has no `description`. Both `title` and `subtitle` interpolate
`{field.path}` tokens against the surrounding record context.

`description` is a **legacy alias** of `subtitle`, kept only for pages authored before
`subtitle` existed; `subtitle` wins when both are set.

Upstream has already closed its half of this. The ADR-0087 D2 conversion entry
`page-header-subtitle-alias` is **live** in protocol 17: the loader rewrites a header
node's `properties.description` to `properties.subtitle` as the stack is loaded. An
already-canonical `subtitle` still wins, and a `description` it shadows is left exactly
as authored rather than the loader choosing between two different second lines.

Retiring the prop on this side is tracked separately, in objectui#3789. The gate there is
a measurement rather than a date: every path by which a `page-header` node reaches this
renderer has to be shown to go through a loader that actually performs that rewrite. Until
objectui#3789 lands, the prop stays and this renderer keeps reading it — and new pages
write `subtitle`.

`actions` is delegated to the `record:quick_actions` widget with
`location: 'record_header'`, so authors get the standard toolbar (icons, overflow menu,
permission filtering, confirm dialogs) without declaring a sibling node. Precedence for
the right-aligned slot is `action` → React `children` → `actions` → schema children.

`showBack` renders a back arrow that navigates one level up the URL. When omitted it
defaults to `true` on record pages (a `recordId` in scope) that are not embedded in a
drawer or modal, and `false` otherwise.

## Examples

### Simple Header
Expand All @@ -42,12 +78,12 @@ interface PageHeaderComponentProps {
<PageHeader title="Settings" />
```

### With Description
### With Subtitle

```plaintext
<PageHeader
title="Analytics"
description="Track your key metrics and performance indicators"
subtitle="Track your key metrics and performance indicators"
/>
```

Expand All @@ -56,7 +92,7 @@ interface PageHeaderComponentProps {
```plaintext
<PageHeader
title="Projects"
description="Manage your projects and tasks"
subtitle="Manage your projects and tasks"
action={
<div className="flex gap-2">
<Button variant="outline">
Expand All @@ -77,7 +113,7 @@ interface PageHeaderComponentProps {
```plaintext
<PageHeader
title="Team"
description="Manage your team members"
subtitle="Manage your team members"
>
<Button>Invite Member</Button>
<Button variant="outline">Settings</Button>
Expand All @@ -91,11 +127,11 @@ The PageHeader uses a flex layout:
```
┌─────────────────────────────────────┐
│ Title [Actions] │
Description
Subtitle
└─────────────────────────────────────┘
```

- **Left Side**: Title and description stacked vertically
- **Left Side**: Title and subtitle stacked vertically
- **Right Side**: Action buttons or custom content
- **Responsive**: Actions move below title on mobile (if needed)

Expand All @@ -107,7 +143,7 @@ The PageHeader uses a flex layout:
- Font weight: `font-bold`
- Tracking: `tracking-tight`

### Description
### Subtitle

- Font size: `text-sm`
- Color: `text-muted-foreground` (gray)
Expand All @@ -132,6 +168,11 @@ The Page component includes built-in PageHeader support:
/>
```

Note that `description` here is **the Page component's own prop**, not the header's — it
renders the page's prose under the page title and is a declared input of the `page`
renderer. It is unrelated to the legacy `description` alias above; do not rename it to
`subtitle`.

## Complete Example

```plaintext
Expand All @@ -144,7 +185,7 @@ function UsersPage() {
<div>
<PageHeader
title="Team Members"
description="Manage your team members and their roles"
subtitle="Manage your team members and their roles"
action={
<div className="flex items-center gap-2">
<Button variant="outline" size="sm">
Expand Down