Application
Outline drawer
A collapsible right-edge navigator: groups navigate, items jump to anchors, one group is open at a time. The same tree is reused on mobile inside a sheet.
import { OutlineDrawer, useScrollSpy, type OutlineNavGroup } from "@luc/ui/workspace";Desktop: groups, items and the reading position
Collapse to a rail of group ordinals; expand to read the report contents. Scroll the page and the outline follows the section you are reading, until you choose one yourself.
Market performance
Sample section: Market performance. Scroll the page and the outline highlights the section you are reading.
Trend & momentum
Sample section: Trend & momentum. Scroll the page and the outline highlights the section you are reading.
Trend stack
Momentum
Weekly trend
Volatility & risk
Sample section: Volatility & risk. Scroll the page and the outline highlights the section you are reading.
Volume & liquidity
Sample section: Volume & liquidity. Scroll the page and the outline highlights the section you are reading.
const GROUPS: OutlineNavGroup[] = [
{ id: "market", label: "Market performance", index: "01" },
{ id: "trend", label: "Trend & momentum", index: "02", items: [
{ id: "od-trend-stack", label: "Trend stack" },
{ id: "od-momentum", label: "Momentum" },
{ id: "od-weekly", label: "Weekly trend" },
]},
{ id: "risk", label: "Volatility & risk", index: "03" },
{ id: "volume", label: "Volume & liquidity", index: "04" },
];
/* Every section the outline points at carries the same attribute. */
const anchorIds = GROUPS.flatMap((group) => [
group.id,
...(group.items ?? []).map((item) => item.id),
]);
const spy = useScrollSpy(anchorIds, { attribute: "data-anchor-id" });
<OutlineDrawer
label="Contents"
groups={GROUPS}
open={open}
onOpenChange={setOpen}
expandedGroupId={groupId} // one group is open at a time
activeItemId={spy.activeId} // the section currently being read
onSelectGroup={(group) => {
setGroupId(group.id);
return false; // false: the caller handled navigation
}}
onSelectItem={(id) => spy.lock(id)} // a chosen item outranks the spy
/>
<section id="trend" data-anchor-id="trend" className="scroll-mt-24">…</section>Mobile: the same tree in a sheet
The same hierarchy, rendered without the collapsible rail.
<Sheet>
<SheetTrigger asChild>
<Button variant="outline">Open contents</Button>
</SheetTrigger>
<SheetContent side="right">
<SheetHeader>
<SheetTitle>Contents</SheetTitle>
</SheetHeader>
<OutlineDrawer
label="Contents"
groups={GROUPS}
collapsible={false} // a sheet has no room for a rail
className="static w-full"
expandedGroupId="trend"
/>
</SheetContent>
</Sheet>Exports
Read from the published TypeScript API, so a prop that is not listed here does not exist and a default that is listed is the one the compiler sees.
| Export | Kind | Signature |
|---|---|---|
OutlineDrawer | 15 documented props | ({ label, groups, open: openProp, defaultOpen, onOpenChange, expandedGroupId, pendingGroupId, activeItemId, activeSectionId, anchorHref, onSelectGroup, onSelectItem, collapsible, footer, className, }… |
OutlineDrawerToggle | 3 documented props | ({ open, onOpenChange, label, }: { open: boolean; onOpenChange: (open: boolean) => void; label: string; }) => import("@types/react").JSX.Element |
OutlineDrawer props
Type: OutlineDrawerProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
activeItemId? | string | null | undefined | — | The active item id (usually from `useScrollSpy`). |
activeSectionId? | string | null | undefined | — | The section the active item belongs to, when the caller tracks both levels. The reading position is a *chart*; its section is the context around it. So a report outline marks the … |
anchorHref? | ((id: string) => string) | undefined | — | Anchor id → href. Defaults to `#<id>`. |
className? | string | undefined | — | |
collapsible? | boolean | undefined | — | Whether the drawer can collapse. Default true. |
defaultOpen? | boolean | undefined | — | |
expandedGroupId? | string | null | undefined | — | The group shown expanded. Defaults to the first group with items. |
footer? | ReactNode | — | Extra content under the groups (a footer action row). |
groups | readonly OutlineNavGroup[] | — | The groups, in document order. |
label | string | — | Accessible name of the whole navigator (e.g. "Contents"). |
onOpenChange? | ((open: boolean) => void) | undefined | — | |
onSelectGroup? | ((group: OutlineNavGroup) => boolean | undefined) | undefined | — | A group click; return `false` to let the browser follow the link. |
onSelectItem? | ((id: string) => boolean | void) | undefined | — | An item click, e.g. to lock the scrollspy before the jump lands. Return `false` to take the jump over (the caller scrolls with the right offset and acknowledges the landing); anyt… |
open? | boolean | undefined | — | Controlled open (expanded) state. |
pendingGroupId? | string | null | undefined | — | The group the reader just picked, before its page has loaded. A domain switch is a server navigation; the navigator highlights the pick optimistically (`aria-current` + `data-pend… |
OutlineDrawerToggle props
Type: { open: boolean; onOpenChange: (open: boolean) => void; label: string; }
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
label | string | — | |
onOpenChange | (open: boolean) => void | — | |
open | boolean | — |
Related