dashboard-01
A ready-to-pull AI dashboard with sidebar navigation, stat cards, a usage chart, and recent activity built from VegaStack components.
Last updated
What it is
dashboard-01 is a registry block, not a component — a starter dashboard PAGE you pull once
with shadcn add and then own (edit freely; it is not hash-tracked/re-pulled the way
app-shell is). It composes AppShell + AppShellSidebar +
AppShellHeader + AppShellContent (the hash-tracked shared layout) with an AI-platform-flavored
sample page: a nav rail (Dashboard / Agents / Tasks / Usage / Settings + a user menu), a four-up
stat-card row, a "usage over time" chart, and a recent-activity list — plus a full-page empty
state and per-region loading/error handling.
This mirrors shadcn/ui's own sidebar (component) ↔ dashboard-01 (block) split: the shell
mechanics live in app-shell/sidebar (pulled and updated forever), the page-specific nav items,
stat cards, and sample data live here (expected to diverge per app from day one).
Install
npx shadcn@latest add @vegastack/dashboard-01This installs the block's own files (below) plus every registryDependencies component it
composes (app-shell, sidebar, breadcrumb, card, badge, animated-number, chart,
data-list, truncated-text, status-icon, relative-time, empty, avatar,
dropdown-menu, button) that your project doesn't already have.
Usage
import { DashboardPage } from "@/app/dashboard/page"; // after shadcn add, this is your page
export default function Page() {
return <DashboardPage />;
}DashboardPage accepts optional stats / usage / activity (each defaults to the bundled
data.json), plus loading / error (per-region) and isEmpty (full-page). Wire these to your
real data fetching once the block is installed:
<DashboardPage
stats={realStats}
usage={realUsage}
activity={realActivity}
loading={{ activity: isActivityLoading }}
error={{ chart: chartError?.message }}
isEmpty={workspace.agentCount === 0}
/>Examples
Mobile navigation
Switch the preview's width toggle (the toolbar's phone icon) to mobile to send the block through the same AppShell breakpoint branch a narrow viewport triggers. The rail collapses; the persistent header trigger opens navigation as a focus-trapped Sheet over the page while the dashboard content stays mounted underneath. It's viewport-driven in a real app — the toggle constrains a container, so this demo forces the branch to make it visible without resizing your window.
Full-page empty state
Rendered when the workspace genuinely has no agents/tasks yet (isEmpty prop) — a standard SaaS
zero-state, in place of the stat/chart/activity regions (audit §e item 5). This is the block's own
responsibility, not AppShell's — AppShell has no opinion on page content.
Per-region loading
Each region — the stat row, the chart, and the recent-activity list — carries its OWN loading
flag, so one slow region never blocks the others from rendering. RecentActivity reuses
DataList's built-in skeleton rows; StatCards/DashboardChart render their own Skeleton
placeholders shaped like the real content.
File manifest
| File | Registry type | Target |
|---|---|---|
page.tsx | registry:page | app/dashboard/page.tsx |
loading.tsx | registry:page | app/dashboard/loading.tsx |
data.json | registry:file | app/dashboard/data.json |
components/app-sidebar.tsx | registry:component | — (resolves to your configured components dir) |
components/stat-cards.tsx | registry:component | — |
components/dashboard-chart.tsx | registry:component | — |
components/recent-activity.tsx | registry:component | — |
page.tsx is server-safe (no hooks, no 'use client') — the interactive pieces
(AppSidebar's nav-user menu, StatCards' AnimatedNumber, DashboardChart's Recharts
composition, RecentActivity's DataList/RelativeTime) are client leaves it imports as JSX,
per the same Server/Client Component boundary rule AppShellSkeleton
documents for SidebarMenuSkeleton. The block carries zero Next.js imports — nav links and
the breadcrumb render as plain <a>; swap in your router's Link via each render prop at the
call site without touching the block's structure.
View transitions
What was verified. Next.js 16.2.9 (the version installed in this workspace) documents a single
opt-in mechanism for animating <Link> navigations with the browser's native View Transitions API:
the experimental.viewTransition flag in next.config.js
(source),
which wraps every <Link> navigation in document.startViewTransition and unlocks React's
<ViewTransition> component + transitionTypes prop on next/link. This is a purely
client-side mechanism (it calls a browser API in response to a link click) — nothing about it
requires server rendering, so it is compatible with this workspace's output: 'export' static
export (source
confirms static export has no such restriction).
What this block wires. The flag itself is apps/docs/next.config.mjs config this block's
files cannot carry (out of a registry block's file scope — and, in THIS repository, editing that
file was explicitly out of scope for this change), so per the brief this is documented, not
hacked in. What the block DOES wire, inside its own files: AppShellHeader and AppSidebar's
AppShellSidebar each carry a [view-transition-name:dashboard-shell-header] /
[view-transition-name:dashboard-shell-sidebar] arbitrary CSS property (Tailwind v4 native
arbitrary-property syntax — a plain CSS custom-ident, not a hex/px literal, so it stays clean
under design-lint's arbitrary-value contract). Once a consuming app enables the flag, this is
the MINIMAL, honest mechanism for "shell stays stable while content crossfades": giving the
header/sidebar a view-transition-name pulls them out of the default whole-page
::view-transition-old(root)/::view-transition-new(root) crossfade and into their OWN stable
group — since their content is pixel-identical before/after a same-shell navigation, that group
reads as "not animating" while the rest of the page (still under the root pseudo) crossfades.
This works with pure CSS + the Next config flag alone; it does not require React's
<ViewTransition> component.
Consumer setup required (this block cannot do this for you):
-
Enable the flag:
// next.config.ts const nextConfig: NextConfig = { experimental: { viewTransition: true } }; -
Hoist the shell into a layout, not a page. This block's file manifest mirrors shadcn's
dashboard-01shape — a singleapp/dashboard/page.tsxthat renders the ENTIREAppShell(sidebar + header + content) itself. That is correct for a single-route starter, but it means the sidebar/header literally unmount and remount on every navigation to a DIFFERENT route, same as any other page-scoped tree. For the "shell stays visually stable across route changes" effect to read as true continuity (not just an inert same-position crossfade), move theAppShell+AppSidebar+AppShellHeadercomposition into a sharedapp/dashboard/layout.tsxthat wraps{children}insideAppShellContent, and keep each route'spage.tsxto just its own content (the stat/chart/activity regions here, for example). This is a real, deliberate restructuring the block does not do for you — the file manifest above is intentionally the single-page shadcn-mirroring shape; layout-hoisting is an app-specific decision left to the consumer once there is more than one route to navigate between. -
Keep the token base stylesheet installed.
@vegastack/design-tokens/base.cssincludes a dedicated reduced-motion kill switch for::view-transition-group(*),::view-transition-old(*), and::view-transition-new(*), because the normal universal reset cannot reach the browser's snapshot tree. Consumers following the standard installation path already have this protection; if an app intentionally omits the base stylesheet, it must provide an equivalent rule before enabling route transitions.
This workspace's own docs app does not have the flag enabled (apps/docs/next.config.mjs was out
of scope for this change), so the crossfade cannot be interactively demonstrated on this page —
the view-transition-name styling above is inert CSS until a consuming app opts in.
Determinism (VRT)
Every value this block renders is static (data.json) or derived from it — no Date.now() or
Math.random() anywhere in the render path, so a screenshot is pixel-identical on every run.
The one place this needed real investigation: RecentActivity's "started" column uses
RelativeTime, which defaults to the LIVE clock. Its API
exposes a documented deterministic mode — pass now (a fixed epoch-ms reference instant) together
with refresh={false} (which also suppresses its internal re-render timer) — and
recent-activity.tsx uses exactly that, pinned to data.json's own generatedAt timestamp.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
activity | ActivityRow[] | bundled sample `data.json` | Recent-activity rows. |
className | string | — | Class name forwarded to the root AppShell (useful when embedding the block in a bounded preview). |
error | { stats?: string; chart?: string; activity?: string; } | {} | Per-region error messages — each region shows an inline error Empty instead of its content. |
isEmpty | boolean | false | True when the workspace genuinely has no agents/tasks yet — renders a full-page Empty
zero-state instead of the stat/chart/activity regions (audit §e item 5, the block's own
responsibility, not AppShell's). |
loading | { stats?: boolean; chart?: boolean; activity?: boolean; } | {} | Per-region loading flags — each region shows its own skeleton independently. |
mobileBreakpoint | number | 768 | Viewport width below which navigation uses AppShell's modal Sheet. |
stats | StatCardDatum[] | bundled sample `data.json` | Stat-card row data. |
usage | UsagePoint[] | bundled sample `data.json` | "Usage over time" chart series. |
| Prop | Type | Default | Description |
|---|---|---|---|
activeKey | "agents" | "dashboard" | "settings" | "tasks" | "usage" | 'dashboard' | Which nav item is current — highlights it and sets aria-current="page". |
onLogout | (() => void) | — | Called when "Log out" is selected — presentational only, no session logic here. |
user | AppSidebarUser | bundled sample user | The signed-in user shown in the footer menu. |
Accessibility
- The block composes
AppShell's skip link and banner/navigation/main landmark trio; preserve those landmarks when adapting the starter into a shared layout. - Sidebar links, the user menu, chart summary, activity table, empty state, loading states, and error regions retain the accessibility contracts of their underlying VegaStack components.
- Navigation labels and status text remain visible; icons and chart decoration never carry meaning alone.
- At 320px, the responsive sidebar collapses while data regions contain their own overflow rather than forcing page-level horizontal scrolling.
| Key | Action |
|---|---|
| Tab | Move through the skip link, navigation, user menu, and dashboard controls. |
| Enter / Space | Activate the focused link, menu trigger, or button. |
| Escape | Close the open user menu and restore focus to its trigger. |