Search for pages, actions, and quick links.
Headers, toolbars, tab strips and the bar that appears on selection. 8 blocks.
Title, record count and the actions, wrapping rather than shrinking.
src/components/blocks/layout/page-header-actions.tsx"use client";
import { Button } from "@dashboardpack/core/components/ui/button";
import { Badge } from "@dashboardpack/core/components/ui/badge";
import { Download, Plus, SlidersHorizontal } from "lucide-react";
/**
* The header every index page needs: title, context, actions.
*
* The actions wrap onto their own line below the title on small screens rather than shrinking,
* because a truncated "Create order" button is worse than a stacked one. `flex-wrap` plus
* `justify-between` does that with no breakpoint: the row splits when it no longer fits.
*
* `me-auto` on the title group rather than `justify-between` on the parent — that keeps the
* buttons together as a unit when they wrap, instead of pushing them to opposite edges.
*/
export function PageHeaderActions() {
return (
<div className="flex flex-wrap items-start gap-4">
<div className="me-auto space-y-1">
<div className="flex items-center gap-2">
<h1 className="text-2xl font-bold tracking-tight">Orders</h1>
<Badge variant="secondary" className="tabular-nums">
1,284
</Badge>
</div>
<p className="text-sm text-muted-foreground">
Every order placed across all channels, updated live.
</p>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm">
<SlidersHorizontal className="h-4 w-4" />
Filters
</Button>
<Button variant="outline" size="sm">
<Download className="h-4 w-4" />
Export
</Button>
<Button size="sm">
<Plus className="h-4 w-4" />
New order
</Button>
</div>
</div>
);
}Active filters shown and individually clearable, not hidden behind a count.
src/components/blocks/layout/filter-toolbar.tsx"use client";
import { useState } from "react";
import { Button } from "@dashboardpack/core/components/ui/button";
import { Badge } from "@dashboardpack/core/components/ui/badge";
import { Input } from "@dashboardpack/core/components/ui/input";
import { Search, X } from "lucide-react";
/**
* A filter bar whose active filters are visible and individually removable.
*
* The pattern this replaces is a "Filters (3)" button that hides which three. Users then cannot
* tell why a table looks empty, and the only way out is a reset that throws away the filters they
* did want.
*
* Each chip is a `<button>` with its own accessible name — "Remove filter: Status is Paid" — so a
* screen-reader user can clear one filter without discovering the set by trial and error. The × is
* `aria-hidden`; the name carries the meaning.
*/
export function FilterToolbar() {
const [filters, setFilters] = useState([
{ id: "status", label: "Status", value: "Paid" },
{ id: "channel", label: "Channel", value: "Online" },
{ id: "range", label: "Date", value: "Last 30 days" },
]);
return (
<div className="space-y-3">
<div className="flex flex-wrap items-center gap-2">
<div className="relative min-w-[200px] flex-1">
<Search className="pointer-events-none absolute top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground ltr:left-3 rtl:right-3" />
<Input placeholder="Search orders…" className="ps-9" />
</div>
<Button variant="outline" size="sm">
Status
</Button>
<Button variant="outline" size="sm">
Channel
</Button>
<Button variant="outline" size="sm">
Date range
</Button>
</div>
{filters.length > 0 && (
<div className="flex flex-wrap items-center gap-2">
<span className="text-xs text-muted-foreground">Active:</span>
{filters.map((filter) => (
<Badge key={filter.id} variant="secondary" className="gap-1 ps-2 pe-1">
<span className="text-xs">
{filter.label} is {filter.value}
</span>
<button
type="button"
onClick={() => setFilters((prev) => prev.filter((f) => f.id !== filter.id))}
aria-label={`Remove filter: ${filter.label} is ${filter.value}`}
className="rounded-full p-0.5 transition-colors hover:bg-background/60"
>
<X className="h-3 w-3" aria-hidden="true" />
</button>
</Badge>
))}
<Button
variant="ghost"
size="sm"
className="h-6 text-xs"
onClick={() => setFilters([])}
>
Clear all
</Button>
</div>
)}
</div>
);
}Status tabs with totals and real arrow-key navigation, one tab stop.
src/components/blocks/layout/section-tabs.tsx"use client";
import { useState } from "react";
import { Badge } from "@dashboardpack/core/components/ui/badge";
import { cn } from "@dashboardpack/core/lib/utils";
/**
* Section tabs with counts, driven by real roving-tabindex keyboard behaviour.
*
* Hand-rolled tab strips usually put every tab in the tab order and respond only to clicks, which
* means a keyboard user tabs through all five to reach the content. The correct pattern — and what
* this does — is one stop for the whole group, with arrow keys moving between tabs.
*
* `aria-controls` is deliberately omitted rather than pointed at a panel that is not in this
* block: a dangling idref is worse than none, because assistive tech announces a relationship the
* user cannot follow. Wire it up when you pair this with panels.
*/
export function SectionTabs() {
const tabs = [
{ id: "all", label: "All", count: 1284 },
{ id: "open", label: "Open", count: 42 },
{ id: "paid", label: "Paid", count: 1180 },
{ id: "refunded", label: "Refunded", count: 38 },
{ id: "failed", label: "Failed", count: 24 },
];
const [active, setActive] = useState("open");
function onKeyDown(event: React.KeyboardEvent, index: number) {
const delta = event.key === "ArrowRight" ? 1 : event.key === "ArrowLeft" ? -1 : 0;
if (!delta) return;
event.preventDefault();
// Wraps at both ends, which is what a tablist does. Note the arrow keys are NOT flipped for
// RTL: the browser already mirrors ArrowRight/ArrowLeft semantics for the user's writing mode
// in a horizontal tablist, so remapping them here would double the flip.
const next = (index + delta + tabs.length) % tabs.length;
setActive(tabs[next]!.id);
}
return (
<div className="border-b">
<div role="tablist" aria-label="Order status" className="flex gap-1 overflow-x-auto">
{tabs.map((tab, index) => {
const selected = tab.id === active;
return (
<button
key={tab.id}
role="tab"
type="button"
aria-selected={selected}
// One tab stop for the group: only the selected tab is reachable by Tab.
tabIndex={selected ? 0 : -1}
onClick={() => setActive(tab.id)}
onKeyDown={(event) => onKeyDown(event, index)}
className={cn(
"flex shrink-0 items-center gap-2 border-b-2 px-3 py-2 text-sm font-medium transition-colors",
selected
? "border-primary text-foreground"
: "border-transparent text-muted-foreground hover:text-foreground",
)}
>
{tab.label}
<Badge
variant={selected ? "default" : "secondary"}
className="h-5 px-1.5 text-[10px] tabular-nums"
>
{tab.count.toLocaleString("en-US")}
</Badge>
</button>
);
})}
</div>
</div>
);
}Appears on selection, announces the count, keeps delete away from the rest.
src/components/blocks/layout/selection-action-bar.tsx"use client";
import { useState } from "react";
import { Button } from "@dashboardpack/core/components/ui/button";
import { Archive, Tag, Trash2, X } from "lucide-react";
/**
* The bar that appears when rows are selected.
*
* Two details make this usable rather than decorative.
*
* It is a `role="status"` region, so the selection count is announced as it changes. A silent
* bulk-action bar is invisible to a screen-reader user, who then has no way to know a destructive
* action now applies to 12 records.
*
* The destructive action sits apart from the others with a separator. Putting "Delete" adjacent to
* "Archive" in a bar that appears under the cursor is how people delete twelve rows by accident.
*/
export function SelectionActionBar() {
const [count, setCount] = useState(12);
if (count === 0) {
return (
<div className="rounded-lg border border-dashed p-4 text-center text-sm text-muted-foreground">
Nothing selected. Select rows in a table to reveal the bar.
<Button variant="link" size="sm" onClick={() => setCount(12)}>
Select 12
</Button>
</div>
);
}
return (
<div
role="status"
className="flex flex-wrap items-center gap-3 rounded-lg border bg-card p-3 shadow-sm"
>
<p className="text-sm font-medium tabular-nums">{count} selected</p>
<div className="flex flex-wrap items-center gap-2 ms-auto">
<Button variant="outline" size="sm">
<Tag className="h-4 w-4" />
Tag
</Button>
<Button variant="outline" size="sm">
<Archive className="h-4 w-4" />
Archive
</Button>
<span aria-hidden="true" className="h-5 w-px bg-border" />
<Button variant="outline" size="sm" className="text-destructive hover:text-destructive">
<Trash2 className="h-4 w-4" />
Delete
</Button>
<Button variant="ghost" size="sm" aria-label="Clear selection" onClick={() => setCount(0)}>
<X className="h-4 w-4" />
</Button>
</div>
</div>
);
}Status, owner and the four numbers you would otherwise scroll to find.
src/components/blocks/layout/detail-summary-header.tsx"use client";
import { Badge } from "@dashboardpack/core/components/ui/badge";
import { Button } from "@dashboardpack/core/components/ui/button";
import { Card, CardContent } from "@dashboardpack/core/components/ui/card";
import { MoreHorizontal, Printer } from "lucide-react";
/**
* A record header that carries the four facts you would otherwise scroll for.
*
* The summary row is a `<dl>`, not a grid of divs. Label/value pairs are exactly what a definition
* list is for, and it means a screen reader reads "Total, $1,248.00" as a pair instead of two
* unrelated strings — for free, with no ARIA.
*
* Values are `tabular-nums` so the columns stay aligned when the numbers change width; without it
* a live-updating total visibly jitters.
*/
export function DetailSummaryHeader() {
const facts = [
{ label: "Total", value: "$1,248.00" },
{ label: "Placed", value: "12 Mar 2026" },
{ label: "Channel", value: "Online" },
{ label: "Items", value: "7" },
];
return (
<Card>
<CardContent className="space-y-4 p-4 sm:p-6">
<div className="flex flex-wrap items-start gap-3">
<div className="me-auto space-y-1">
<div className="flex flex-wrap items-center gap-2">
<h2 className="text-lg font-semibold tabular-nums">ORD-7891</h2>
<Badge variant="success">Paid</Badge>
<Badge variant="outline">Fulfilled</Badge>
</div>
<p className="text-sm text-muted-foreground">Ana Whitfield · [email protected]</p>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm">
<Printer className="h-4 w-4" />
Print
</Button>
<Button variant="outline" size="sm" aria-label="More actions">
<MoreHorizontal className="h-4 w-4" />
</Button>
</div>
</div>
<dl className="grid grid-cols-2 gap-4 border-t pt-4 sm:grid-cols-4">
{facts.map((fact) => (
<div key={fact.label} className="space-y-1">
<dt className="text-xs text-muted-foreground">{fact.label}</dt>
<dd className="text-sm font-semibold tabular-nums">{fact.value}</dd>
</div>
))}
</dl>
</CardContent>
</Card>
);
}