UNPKG

raft-ui

Version:

React UI components for Raft.

159 lines (110 loc) 7.76 kB
--- name: layout description: "Shell and layout: AppShell slots, Panel and the PanelHeaderRoot vs PanelHeader distinction, AppRail vs Sidebar vs MobileNav, Resizable, ScrollArea, task surfaces." --- # Layout and shell ## Contents - Use the shell, do not rebuild it - AppShell vs Panel vs Resizable - AppRail vs Sidebar vs MobileNav - Panel vs a bordered div - ScrollArea vs overflow - Per-component norms --- ## Use the shell, do not rebuild it The application frame is `AppShellRoot` with named slots. Fill the slots. ```tsx <AppShellRoot> <AppShellRailSlot>…</AppShellRailSlot> <AppShellSidebarSlot>…</AppShellSidebarSlot> <AppShellMainSlot>…</AppShellMainSlot> </AppShellRoot> ``` Treat the slots as ownership markers, not required direct children. On desktop, put `AppShellSidebarSlot` and `AppShellMainSlot` inside adjacent `ResizablePanel`s when the split is user-adjustable. On mobile, one `AppShellMainSlot` can switch between the sidebar, main content, and thread from app-owned route state. AppShell slots own placement only. They do not add a divider, surface, width, inset, shadow, or resize-handle offset. Put those details on the component rendered inside the slot. For a bounded region, use `edge="attached"` on `Panel` (or a panel component that exposes the same prop) when it meets the preceding region directly, and `edge="inset"` when it should read as an elevated inner surface. The panel then owns its divider, header continuation, radius, and shadow. Set `overlay` and render an `AppShellOverlaySlot` only to choose overlay placement; the overlay child still owns its surface and edges. **Incorrect** — reassembling the frame: ```tsx <div className="flex h-screen"> <aside className="w-16 border-r">…</aside> <aside className="w-64 border-r">…</aside> <main className="flex-1 overflow-auto">…</main> </div> ``` That loses the responsive behavior, the sidebar collapse protocol, and the theme-family treatments. `AppShellSidebarTrigger` is the paired control for collapsing the sidebar — wire it rather than tracking your own boolean and toggling a width class. --- ## AppShell vs Panel vs Resizable Three different jobs, often confused. | | For | | ---------------- | -------------------------------------------------- | | `AppShellRoot` | the outermost slot layout — rail, sidebar, main | | `Panel` | one bounded region with a header, body, and footer | | `ResizableGroup` | regions whose split the **user** can drag | `Resizable` is only for user-adjustable splits. If the widths are fixed by design, use the shell slots or plain layout — do not wrap everything in a resizable group "just in case". ```tsx <ResizableGroup orientation="horizontal"> <ResizablePanel>…</ResizablePanel> <ResizableHandle /> <ResizablePanel>…</ResizablePanel> </ResizableGroup> ``` Every adjacent pair of `ResizablePanel`s needs a `ResizableHandle` between them. A group without handles is a layout, not a resizable. --- ## AppRail vs Sidebar vs MobileNav | | For | | --------------- | --------------------------------------------- | | `AppRailRoot` | the narrow icon rail — top-level destinations | | `SidebarRoot` | the wide list — channels, DMs, sections | | `MobileNavRoot` | bottom navigation on small screens | `AppRail` composes `AppRailRoot`, `AppRailHeader`, `AppRailNav`, `AppRailFooter`, and items built from `AppRailItem` + `AppRailItemIcon` + `AppRailItemLabel`. Unread and attention signals have dedicated parts — `AppRailItemBadge`, `AppRailItemIndicator`, `AppRailItemAttention`, `AppRailItemAttentionMask`. Do not position a dot yourself. `MobileNavRoot` composes `MobileNavItem` + `MobileNavLabel` for small screens. Render its items separately from the rail, but drive selection and navigation from the same app-owned route or view state. Mobile may expose only a subset of destinations; keep shared destinations aligned. --- ## Panel vs a bordered div `Panel` is the standard bounded region, and it owns the whole header vocabulary. ```tsx <Panel> <PanelHeaderRoot>…</PanelHeaderRoot> <PanelBody>…</PanelBody> <PanelFooter>…</PanelFooter> </Panel> ``` **Two headers, pick by need:** `PanelHeaderRoot` is the bare frame — use it when you lay the header out yourself. `PanelHeader` is the composed identity chrome built on top of it, with `PanelHeaderContent` / `PanelHeaderIcon` / `PanelHeading` / `PanelTitle` / `PanelMeta` / `PanelActions` (`PanelAction`, `PanelToggleAction`) — use it when the surface has an identity row, as `ConversationPanel` does. Both type-check inside a `Panel`, so the wrong pick fails silently. **`PanelStatus` vs `PanelActivity`:** `PanelStatus` is a standalone attribute badge in the header; `PanelActivity` is a live status readout wrapping a `Status`. Do not use one for the other. When the body scrolls, use `PanelScrollViewport` + `PanelScrollContent` instead of putting `overflow-auto` on `PanelBody` — the scroll parts keep the header and footer pinned correctly. `PanelSection` divides the body into sections; `PanelSectionHeader` labels one. Reach for `Panel` before writing `rounded-lg border border-line bg-layer-panel`. Use `ProfilePanel` from `raft-ui` for a profile detail pane built on these parts. --- ## ScrollArea vs overflow - Page-level scrolling → let the browser do it. - A component with a specialized viewport → use that viewport: `PanelScrollViewport`, `TasksPanelViewport`, `MessageList`, or `SidebarContent`. - A bounded custom region with no existing scroll owner → compose `ScrollArea`. Use `ScrollArea` when the region itself owns scrolling and needs the library's scrollbar treatment. Do not replace a specialized viewport just to get a custom scrollbar; the higher-level component already owns the layout and overflow contract. Keep one scroll owner per axis. Do not put `ScrollArea` inside `PanelScrollViewport` or `TasksPanelViewport`, and do not make task columns or list sections independently scroll. --- ## Per-component norms **`Tabs`** — see [forms.md](./forms.md) for `Tabs` vs `SegmentedControl`. Variants are `default` and `underline`. Use `TabsIndicator` and `TabsBackground` for the moving marker. **`Separator`** — for dividing content within a region. Structural borders between shell regions come from the shell components themselves; do not add separators between slots. **Task surfaces**`TasksPanelRoot` is the outer surface (`TasksPanelToolbar`, `TasksPanelViewport`, `TasksPanelEmpty`). Inside it, choose: | Component | For | | --------------- | -------------------------- | | `TaskBoardRoot` | kanban columns | | `TaskListRoot` | grouped list sections | | `TaskCard` | one task in either surface | `TasksPanelViewport` is the shared scroll owner for the complete panel. Put `TaskBoardRoot` or `TaskListRoot` directly under it. A standalone Board or List demo may provide its own `ScrollArea`; do not carry that wrapper into `TasksPanelViewport`. `TaskBoardColumn` and `TaskSection` mirror each other — both have `Heading`, `Count`, `Badge`, `Trigger`, `Chevron`, `Panel`, `Items`, `Empty`. The board additionally distinguishes empty states by drop eligibility: `TaskBoardColumnEmptyIdle`, `TaskBoardColumnEmptyAllowed`, `TaskBoardColumnEmptyBlocked`. Drag and drop is `TaskDndProvider` + `TaskDraggable` + `TaskDropZone` + `TaskDropPlaceholder` + `TaskDragOverlay`. Do not wire `@dnd-kit` directly. `TaskCardLegacy` and the `LegacyTaskPanel*` family exist for old callsites — never use them in new code.