How to use @vivabox/ui
Consumer guide for the VivaBox component library. Canonical source: planning/docs/HOW-TO-USE-PACKAGE.md. Hosted in the style guide at /guide.
| Package | @vivabox/ui 0.1.0 |
| Peers | react ^19, react-dom ^19 |
| Host CSS | Tailwind CSS v4 (required) |
| Primary host | Next.js App Router (React 19) |
| Style guide | apps/docs — components, patterns, Theme Studio, this guide |
Tech stack
What the package is built with (and what a host app should expect):
| Layer | Choice |
|---|---|
| Language | TypeScript |
| UI runtime | React 19 (peer) |
| Styling | Tailwind CSS v4 in the host + package CSS tokens (@vivabox/ui/styles.css) |
| Design tokens | OKLCH CSS variables, [data-theme='…'] + .dark |
| Primitives | Radix UI (Dialog, Select, Dropdown, Tooltip, Scroll Area, Label, Separator, Slot, Collapsible, …) |
| Variants | class-variance-authority + clsx + tailwind-merge (cn) |
| Light / dark | next-themes (re-exported as useTheme) |
| Tables | TanStack Table v8 (ManagedDataTable) |
| Command palette | kbar + cmdk |
| Docs / style guide | Next.js 15 App Router (apps/docs) |
| Monorepo | pnpm workspaces (packages/ui, apps/docs) |
The library ships source + CSS (not a pre-bundled sealed CSS bundle) so Tailwind can scan component classes via @source. It is not locked to next/font; fonts stay host-owned.
Style guide fonts: apps/docs loads Academia’s font.config.ts (copied to apps/docs/src/lib/font.config.ts) so theme --font-* variables resolve. Product hosts should load the same (or their own) font variables on <body>.
Not part of the package: Academia APIs, auth IdPs, next-intl, product routing.
What you get
- Design tokens — OKLCH CSS variables (colour, radius, sidebar, charts, shadows)
- 10 bundled themes — switchable named palettes + light/dark
- React components — primitives, tables, command search, app shell
- Providers —
VivaBoxProviderfor theme + mode
Products should install the package, import styles once, wrap with VivaBoxProvider, and build UI from library exports — not by forking Academia screens.
Out of scope for consumers: Academia APIs, tenants, next-auth/Authentik, next-intl as a hard dependency, product-specific nav. Wire those in the host app; pass data and callbacks into the library.
Install
Workspace (current)
Until the package is published to the approved registry (private: true for now):
{
"dependencies": {
"@vivabox/ui": "workspace:*"
}
}
Or path / git dependency against this monorepo.
After registry publish
pnpm add @vivabox/ui
# or: npm install @vivabox/ui / bun add @vivabox/ui
Also install Tailwind CSS v4 in the host app if it is not already present.
Optional host dependency for tables (if you define columns in the app):
pnpm add @tanstack/react-table
(ManagedDataTable already depends on TanStack Table inside the package; the host only needs the types/helpers when authoring ColumnDefs.)
Setup (required)
1. Global CSS
In your app’s global stylesheet (e.g. app/globals.css or src/styles/globals.css):
@import "tailwindcss";
@import "@vivabox/ui/styles.css";
/*
Tailwind v4 must scan the package source so utility classes
used inside @vivabox/ui components are generated.
*/
@source "../node_modules/@vivabox/ui";
Monorepo workspace (path relative to the CSS file — count ../ from the file’s directory to the repo root, then into packages/ui):
/* apps/docs/src/app/globals.css → ../../../../packages/ui */
@source "../../../../packages/ui";
If utilities from the library are missing at runtime (sidebar stays invisible, drawers open off-screen, overlays block clicks), the @source path is wrong — fix that before anything else.
Verify (monorepo): a minimal consumer lives at apps/smoke and uses the same @source "../node_modules/@vivabox/ui" shape. From the repo root:
pnpm verify:consumer
# or: pnpm --filter @vivabox/smoke build
2. Root layout + provider
Set data-theme on <html> for SSR (avoids a flash), then wrap the tree:
// app/layout.tsx — Next.js App Router
import { cookies } from 'next/headers';
import {
VivaBoxProvider,
DEFAULT_THEME,
THEMES
} from '@vivabox/ui';
// Optional but recommended for theme font stacks (Inter, Outfit, Geist, …):
// copy Academia/docs font.config and apply `fontVariables` on <body>.
export default async function RootLayout({
children
}: {
children: React.ReactNode;
}) {
const cookieStore = await cookies();
const raw = cookieStore.get('active_theme')?.value;
const themeToApply = THEMES.some((t) => t.value === raw)
? raw!
: DEFAULT_THEME;
return (
<html lang="en" suppressHydrationWarning data-theme={themeToApply}>
<body className="min-h-screen bg-background font-sans text-foreground antialiased">
<VivaBoxProvider initialTheme={themeToApply}>
{children}
</VivaBoxProvider>
</body>
</html>
);
}
For the style-guide parity path used in this monorepo:
import { fontVariables } from '@/lib/font.config'; // copied Academia font.config
<body className={`… font-sans antialiased ${fontVariables}`}>
Cookie name constant (if you persist the palette yourself): ACTIVE_THEME_COOKIE from @vivabox/ui.
3. Theme controls (optional UI)
'use client';
import { useThemeConfig, useTheme, THEMES } from '@vivabox/ui';
export function ThemeControls() {
const { activeTheme, setActiveTheme } = useThemeConfig();
const { setTheme, resolvedTheme } = useTheme();
return (
<>
<select
value={activeTheme}
onChange={(e) => setActiveTheme(e.target.value)}
>
{THEMES.map((t) => (
<option key={t.value} value={t.value}>
{t.name}
</option>
))}
</select>
<button
type="button"
onClick={() =>
setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')
}
>
Toggle light / dark
</button>
</>
);
}
- Named palette →
data-themeon<html>(useThemeConfig) - Light / dark / system →
.darkclass vianext-themes(useTheme)
Bundled themes
| Name | Slug |
|---|---|
| Claude | claude |
| Neobrutualism | neobrutualism |
| Supabase | supabase |
| Vercel (default) | vercel |
| Mono | mono |
| Notebook | notebook |
| Light Green | light-green |
| Zen | zen |
| Astro Vista | astro-vista |
whatsapp |
Custom presets: edit/export CSS in the style guide Theme Studio (/studio), then drop the file into styles/themes/, import it, and register it in THEMES. Products that only consume the package can ship a local CSS overlay with the same [data-theme='…'] / .dark shape if they do not fork the package.
Using components
Import from the package root:
import { Button, Input, Dialog, DialogContent, DialogTrigger } from '@vivabox/ui';
Browse live examples in the style guide:
/guide— this document (hosted)/components/*— primitives/patterns/*— App shell, Managed data table, Command search/studio— Theme Studio
Primitives (examples)
import {
Button,
Input,
Field,
FieldLabel,
FieldDescription,
FieldError,
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
Badge
} from '@vivabox/ui';
export function ExampleForm() {
return (
<Field>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input id="email" type="email" placeholder="you@company.com" />
<FieldDescription>We never share your email.</FieldDescription>
<FieldError>Required</FieldError>
<div className="flex gap-2 pt-2">
<Button type="submit">Save</Button>
<Button type="button" variant="outline">
Cancel
</Button>
<Badge variant="secondary">Draft</Badge>
</div>
</Field>
);
}
App shell (logged-in chrome)
Pass nav as data. Pass your router Link as linkComponent. No product routes or auth are baked in. (AuthShell is deferred.)
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import {
AppShell,
IconHome,
IconSettings,
type AppShellNavGroup
} from '@vivabox/ui';
const nav: AppShellNavGroup[] = [
{
label: 'Platform',
items: [
{ title: 'Home', href: '/', icon: <IconHome className="size-4" /> },
{
title: 'Settings',
href: '/settings',
icon: <IconSettings className="size-4" />
}
]
}
];
export function ProductChrome({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
return (
<AppShell
nav={nav}
pathname={pathname}
linkComponent={Link}
sidebarHeader={<span className="px-2 font-semibold">My product</span>}
headerStart={<span className="text-sm text-muted-foreground">Overview</span>}
>
{children}
</AppShell>
);
}
Managed data table
Server/client-owned pagination and sorting; UI persistence via persistenceKey. Export is callbacks only (host implements Excel/PDF).
'use client';
import { useState } from 'react';
import type { ColumnDef, PaginationState } from '@tanstack/react-table';
import { ManagedDataTable, ListPageHeader, Button } from '@vivabox/ui';
type Row = { id: string; name: string };
const columns: ColumnDef<Row>[] = [
{ accessorKey: 'name', header: 'Name' }
];
export function UsersTable({ rows, rowCount }: { rows: Row[]; rowCount: number }) {
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10
});
return (
<ManagedDataTable
persistenceKey="users-table"
columns={columns}
data={rows}
rowCount={rowCount}
pagination={pagination}
onPaginationChange={setPagination}
heading={
<ListPageHeader
title="Users"
description="Directory"
actions={<Button size="sm">Invite</Button>}
/>
}
onExportExcel={() => {
/* host export */
}}
/>
);
}
Useful props: isLoading, emptyMessage, filterBar, filterToggles, enableRowSelection, labels / paginationLabels (no next-intl — pass strings from the host), tableMinHeight ('auto' for content-sized demos).
Command search
Host owns the action list (routes, mutations, shortcuts):
'use client';
import { useRouter } from 'next/navigation';
import {
CommandSearch,
CommandSearchTrigger,
type Action
} from '@vivabox/ui';
export function SearchRoot({ children }: { children: React.ReactNode }) {
const router = useRouter();
const actions: Action[] = [
{
id: 'home',
name: 'Go home',
shortcut: ['g', 'h'],
perform: () => router.push('/')
}
];
return (
<CommandSearch actions={actions} searchPlaceholder="Search…">
<CommandSearchTrigger placeholder="Search…" />
{children}
</CommandSearch>
);
}
Public API map (0.1 surface)
Providers & utilities
| Export | Role |
|---|---|
VivaBoxProvider | Root: light/dark + named theme |
useThemeConfig | { activeTheme, setActiveTheme } |
useTheme | From next-themes (mode) |
THEMES, DEFAULT_THEME, ACTIVE_THEME_COOKIE | Catalog + SSR cookie |
cn | clsx + tailwind-merge |
Primitives
Button, ButtonGroup*, Badge, Input, InputGroup*, InputOTP*, Textarea, Field*, Label, Separator, Spinner, Select*, MultiSelectFilter, Accordion*, MapPin, AuditTrails* (heatmap, table, geography, charts), Dialog*, Drawer*, Sheet*, Popover*, Tooltip*, DropdownMenu*, Command*, ScrollArea, Skeleton, Sidebar*, Infobar*, SearchInput, Icons
Patterns
| Export | Role |
|---|---|
AppShell | Logged-in sidebar + header slots |
Header, AppSidebar, SchoolSidebar, SchoolSwitcher, UserAvatarProfile, DashboardShell | Lower-level copied Academia chrome exports |
Sidebar* / useSidebar | Lower-level sidebar primitives |
ManagedDataTable | List-page table kit |
DataTable* helpers | Pagination, column header, toolbar, filters, skeleton |
ListPageHeader | List page title/actions |
CommandSearch, CommandSearchTrigger | Global command palette |
Full export list: packages/ui/src/index.ts. Prefer importing only what you need from @vivabox/ui.
Rules of engagement
- NO REWRITE of Academia UI — library work is copy → paste → sanitize only (see proposal §7.2). Do not redesign components that already exist in Academia.
- Do not copy Academia screens into products — extract through this package or open a PR here.
- Theme via tokens — prefer CSS variables (
bg-primary,text-muted-foreground) over one-off hex. - Pass i18n from the host — use
labelsprops; the library does not hard-depend on next-intl. - Pass behaviour as props — nav, search actions, export handlers, form submit.
- Verify
@sourcein a fresh app before calling an install “done”.
Style guide & Theme Studio
| Surface | URL (local docs app) |
|---|---|
| Consumer guide | /guide |
| Home | / |
/components/button | Loading: first block (isLoading) |
/components/accordion | Radix accordion (single / multiple) |
/components/map | Leaflet pin picker (dynamic, ssr: false) |
/components/audit-trail | Academia audit panels with mock data — host fetches real APIs |
/components/multi-select | Faculty / Department / Level scope bar (Academia MultiSelectFilter) |
/components/drawer | Click Open sheet (right panel) / Open drawer |
/patterns/app-shell | Academia SchoolSidebar (structure real; nav labels are demo placeholders). Help / Ctrl⌘+I = infobar. Host passes real items. |
/patterns/managed-data-table | Toolbar Filters/Columns; bottom Rows per page (10/25/50/100) |
/studio | Academia Theme Studio (local seed) |
Theme Studio is local only: edit tokens, preview, Copy CSS / Export CSS. It does not call an Academia themes API.
# from monorepo root
pnpm install
pnpm dev
Troubleshooting
| Symptom | Likely fix |
|---|---|
| Components unstyled / missing utilities | Fix @source path to @vivabox/ui (or workspace packages/ui) |
| Theme flash on load | Set data-theme on <html> from cookie before paint; pass same value to VivaBoxProvider |
| Dark mode not applying | Ensure VivaBoxProvider is mounted; check .dark on <html> |
| Select / Dialog / Sidebar broken | Confirm you import from @vivabox/ui (client components) and do not strip 'use client' boundaries |
| Table export does nothing | Implement onExportExcel / onExportPdf in the host |
Versioning & support
- Current package version:
0.1.0(packages/ui; stillprivate: trueuntil registry publish). - Changelog:
packages/ui/CHANGELOG.md. - Fresh-consumer verify:
apps/smoke+pnpm verify:consumer. - Registry steps:
planning/docs/PUBLISH-CHECKLIST.md. - After registry publish, treat the public export surface as stable toward 1.0; breaking changes need a major bump and a migration note.
Questions and new components: add a style-guide page in apps/docs before treating a component as shipped.
Document status
| Field | Value |
|---|---|
| Canonical file | planning/docs/HOW-TO-USE-PACKAGE.md |
| Implementation SoT | planning/docs/VivaBox-UI-Library-Proposal.md (update as work lands) |
| Hosted at | Style guide /guide (same markdown) |
| Package README | Short pointer + setup; full detail here |
| Last aligned with code | 0.1.0 — NO-REWRITE recopy: buttons/inputs/search/dropdowns/drawers/tables/header/sidebar/Theme Studio + catalog pages |