Dashboards, pages and widgets
A dashboard is a product for one kind of business: a brand, a sidebar, and pages that read one sample store. Install all of it by one name, one page of it, or one card from a page — and mix them, because every page wears the same shell and reads the same data.
Install a dashboard
The commands here use the @vibra namespace: register it once, as Installation shows. The CLI then asks before it replaces shadcn’s button, card and the rest with Vibra’s; answer yes, or pass --overwrite.
One name installs a dashboard whole. E-commerce is 12 pages under one sidebar: each page at its own route, the sidebar as lib/dashboards/ecommerce/nav.ts, and the sample data the pages read.
There are 13, each installed by its name: ecommerce, saas, crm, support, engineering, people, finance, projects, hotel, clinic, real-estate, academy and ai.
Install a page
A page installs alone, at its route, and still wears its dashboard’s sidebar: Sales lands at app/ecommerce/sales/page.tsx and brings nav-ecommerce with it. Its links to the rest of E-commerce point at routes you can install next, or take out of the nav file.
Install a widget
A widget is one card cut out of a page, with the selector it reads. It installs beside where its page would be, so the same import works with the page or without it: Recent signups, from SaaS’s overview, lands as app/saas/components/recent-signups.tsx and app/saas/data.ts.
The card and the selectors it reads are named exports. Render it from a server component, the way its page does:
Every widget is on Widgets, each with the page it comes from.
Put two dashboards under one sidebar
Install E-commerce and SaaS side by side and their routes never collide — /ecommerce and /saas. mergeNav joins their sidebars: the first brand leads, every group of both follows in order, and the footers join so Settings appears once. Hand the result to every page through NavOverrideProvider; each page still passes its own activeHref, so the merged sidebar marks the page you are on.
Swap the sample data for your API
Every page reads its rows through db, from @/lib/sample-data: db.orders.list(query) for a table, db.orders.all() for a chart. Every entity on it is a Repository — list, get, create, update, remove and all — kept in memory over seeded rows; the exchange rates, read whole and never written, are the one plain record.
Swap one entity at a time, behind the same interface. In lib/sample-data/orders.ts, keep the Order type and the ORDERS seed, and replace only the exported orders repository with one over your API:
api stands for your own client, and loadedOrders for rows you have already fetched. list resolves to a Page, and create, update and remove to a Result — the shapes in lib/sample-data/core.ts, where the in-memory repository shows what each one returns. Before you swap, look at what else reads orders:
reviews.tsandshipments.tsbuild their own rows fromORDERS, which is why it stays: they go on describing the sample orders until you swap them too.all()answers synchronously, and every page calls it inside the selector that needs it, as the page renders — none as its data module is imported — so the rows have to be in memory by the time a page renders, and a write reaches every page on its next render.- No page reaches the store from its client code: every island is handed its rows by the server component that renders it, so a repository that can only run on the server works for all of them.