Resolved: light

Navigation & panes

Multi-pane shells. SideTabPage pairs a route-driven rail with content + an optional inspector; WorkspacePage is the resizable start | primary | end split. TriplePanePage, TreeExplorerPage and InboxPage are configured presets of WorkspacePage / SplitViewPage.

The pane shells own the layout (rail widths, collapse, persisted sizes) and take ReactNode slots; any content needing handlers (tree selection, list↔detail wiring) rides its own client island.

TabbedPage

A report/analytics shell: a PageTabs rail below the header and the active tab's panel as children (composes the @phauna/ds ReportLayout).

Analytics

Last 30 days
Signups
1,284+8.1%
vs prior period
Activation
62%
completed profile
Churn
2.4%-0.3
vs prior period

Overview

Signups are up week over week, driven by the Bay Area meet-up campaign. Activation held steady; churn ticked down after the new onboarding checklist shipped.

<TabbedPage> — TitleBar + status · PageTabs rail in the toolbar · active tab's panel as children
tabs (required)
The tab rail rendered into the toolbar — typically a thin client island that resolves the active tab from usePathname; the TabbedPage shell stays server-safe.
children / toolbarExtra
children is the active tab's panel; toolbarExtra adds controls beside the tabs (e.g. a date range).
maxWidth / footer
maxWidth centers the body to a reading column (true → var(--page-max), or a CSS width); footer is the page footer slot.

SideTabPage

A 'BeagleBot console': a route-driven <SideTabs> rail | the active section's content | an optional right <InspectorPanel> — the shape for settings-style and section-switcher consoles.

BeagleBot console

Online
SideTabPage — TitleBar · SideTabs rail (grouped) | Commands content | InspectorPanel
tabs / children
tabs is the grouped SideTabGroup[] rail model (route-driven highlight); children is the active section's content pane.
inspector / inspectorWidth
Optional right InspectorPanel rail for the selected item; the body splits when set.
SideTabs is client
The rail resolves the active tab from the current route (DS Tabs state), so wrap selection in a client island; the SideTabPage shell stays server-safe.

WorkspacePage

The resizable, persisted split shell: a primary work region with optional leading (nav/tree) and trailing (inspector) panes that drag, collapse and remember their sizes.

Mission control

Sprint 3

Objectives

Foster network
Recruit 25 fostersIn progress
Onboarding flow v2To do
Intake backlog
Clear 40 pending intakesIn progress
Triage rotaDone
Adoption drive
Spring adoption eventTo do
Partner shelter outreachDone
To do2
In progress2
Done2
<WorkspacePage> — TitleBar + "Sprint 3" status · resizable Objectives tree | Board | Inspector · drag/arrow-key the handles · double-click or Enter a side handle to collapse · persistKey restores the layout · folds to mobile drawers below md
primary (required)
The main work region — never collapsible, fills the remainder (PaneLayout.primary). primaryTitle is its a11y label / mobile-stack header.
start / end
Leading (left/top) and trailing (right/bottom) PaneRegion auxiliary panes — a nav/tree/files rail and an inspector/details rail.
direction / persistKey
direction lays panes 'horizontal' (default) or 'vertical'; persistKey forwards to SplitView to persist sizes + collapse across reloads. Server-safe wrapper over the client SplitView.

TriplePanePage

This is WorkspacePage configured as a three-pane IDE workspace — start navigator + primary canvas + end inspector, all resizable/persisted via PaneLayout.

tsx
import { WorkspacePage } from "@/components/ds/recipes";
import { TreeView } from "@/components/ds/tree";
import { InspectorPanel } from "@/components/ds/surface";

<WorkspacePage
  title="Workspace"
  start={{ title: "Navigator", content: <TreeView framed nodes={nodes} />, defaultSize: 22 }}
  primary={<EditorCanvas doc={doc} />}
  end={{ title: "Inspector", content: <InspectorPanel title={sel?.name}>{props}</InspectorPanel>, defaultSize: 26 }}
  persistKey="workspace:panes"
/>

SplitViewPage

The master/detail (inbox) shell: a list pane beside a detail pane that collapses to one column on narrow screens (composes the @phauna/ds ListDetailLayout). TreeExplorerPage and InboxPage below are configured presets of it.

Inbox

3 threads
<SplitViewPage> — TitleBar + status · message list pane | detail InspectorPanel · folds to one column below md
list / detail (required)
list is the index pane; detail is the selected item's pane. Selection wiring rides the caller's own client island.
listWidth / listEnd / fullHeight
listWidth sizes the list pane; listEnd moves it to the trailing edge; fullHeight fills the viewport below the 44px admin nav (inbox-style).
maxWidth / detailMaxWidth
Cap the whole two-pane block, or just the detail content, to a centered reading column (true → var(--page-max), or a CSS width).

TriageQueuePage

A list/triage SPLIT preset over SplitViewPage: a selectable queue rail (a sticky filter bar + a stats band + the list of work items) beside an InspectorPanel detail whose footer is the action rail (assign / decline). Selection is route-driven; the rapid-review shape for onboarding / organizers / memberships.

Onboarding

3 pending
<TriageQueuePage> — TitleBar · filter bar + stats + selectable queue rail | detail InspectorPanel with a sticky action footer · route-driven selection
queue / detail (required)
queue is the selectable list (rows link to a per-item subroute); detail is the InspectorPanel for the routed selection. Selection is route-driven, so the recipe holds no state.
filterBar / stats
filterBar pins a search/facets/sort strip atop the rail; stats is an at-a-glance counts band (SummaryBar/StatGrid) beneath it.
listWidth / fullHeight
listWidth sizes the queue rail (default 380px); fullHeight fills the viewport below the 44px admin nav (default on).

Triage session

The shared behavior layer that rides TriageQueuePage (Admin Hi-fi 6c): keyboard j/k/a/h with auto-advance to the next unhandled row, the session-local handled ledger (approved/held/declined rows stay visible, dimmed with a status pill, instead of vanishing on revalidate), and the queue-clear panel once every row is handled. One recipe — pages register rows and a primary action; they never fork the keyboard/triage semantics.

Live session — focus the panel, then a approves, h holds (keys scoped to the panel; real queues use the window-global listener).

  • Dana Kerrigan3d ago
    Organizing
  • Priya Natarajan5d ago
    Political action
  • Marcus Webb1w ago
    Marches

j / k move · a approve · h hold · 0/0 handled

Dana Kerrigan

Organizing · requested 3d ago

Approve / hold / decline below — or focus this panel and press a / h. Each action dims the row with its status pill and auto-advances to the next unhandled one.

<TriageSessionProvider> + useTriageRows / useTriagePrimaryAction / mergeHandledRows + <TriageQueueClear> — handled rows dim with a status pill instead of vanishing; the dashed queue-clear panel takes the detail pane once everything is handled.
  • Alma ReyesOrganizing
  • Jon ParkMarches
  • Lena FisherPolitical action
  • Omar HaddadOrganizing

j / k move · a approve · h hold

The pure layer (triage-session.ts) — keyToTriageIntent → moveSelection / nextPendingIndex, shouldSuppressTriageKeys, allHandled. Focus the panel and press j / k / a / h (keys scoped to this panel).
TriageSessionProvider
Mounts in the queue's LAYOUT (not the page) so the ledger survives index ↔ [id] navigation. Owns the one window keydown listener (j/k move, a = primary, h = hold; dead in inputs / contenteditable / open modals) and the handled ledger.
useTriageRows / useTriagePrimaryAction
The queue island registers its VISIBLE ordered rows ({key, href, data}) + the routed selection + the index href; the inspector registers the fn that `a` fires. Selection stays route-driven — moving/advancing is a router.push to the row's href.
mergeHandledRows / statusOf
Merge the server's still-pending rows with the session ledger so approved/declined rows re-insert from their snapshot after revalidate (a hold is session-local — no server write). Style handled rows dimmed with the TRIAGE_STATUS_LABELS / TRIAGE_STATUS_TONES pill.
TriageQueueClear
Wraps the empty-detail placeholder; once every visible row is handled (or nothing was pending — serverRowCount settles the pre-hydration frame) it renders the dashed ✓ queue-clear panel with the session tally and a link to the next queue with pending items.

TreeExplorerPage

This is SplitViewPage configured as a tree explorer — list is a framed TreeView hierarchy, detail shows the selected node (an InspectorPanel).

tsx
import { SplitViewPage } from "@/components/ds/recipes";
import { TreeView } from "@/components/ds/tree";
import { InspectorPanel } from "@/components/ds/surface";

<SplitViewPage
  title="Territory explorer"
  fullHeight
  listWidth="20rem"
  list={<TreeView framed nodes={nodes} selectedId={sel} onSelect={(n) => setSel(String(n.id))} defaultExpanded={["us"]} />}
  detail={<InspectorPanel eyebrow="Team" title={node?.label}>{detailBody}</InspectorPanel>}
/>

InboxPage

This is SplitViewPage configured as a mail/notification inbox — list is a message Feed, detail is an InspectorPanel reader, fullHeight on (mirrors the email-triage route SplitViewPage already serves).

tsx
import { SplitViewPage } from "@/components/ds/recipes";
import { InspectorPanel } from "@/components/ds/surface";

<SplitViewPage
  title="Inbox"
  fullHeight
  listWidth="22rem"
  list={<MessageList items={messages} selectedId={sel} onSelect={setSel} />}
  detail={<InspectorPanel title={open.subject} subtitle={open.from} actions={replyActions} footer={<ReplyBox />}>{open.body}</InspectorPanel>}
/>