V

VivaBox UI

Style guide

Consumer guide · v0.1.0

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
Peersreact ^19, react-dom ^19
Host CSSTailwind CSS v4 (required)
Primary hostNext.js App Router (React 19)
Style guideapps/docs — components, patterns, Theme Studio, this guide

Tech stack

What the package is built with (and what a host app should expect):

LayerChoice
LanguageTypeScript
UI runtimeReact 19 (peer)
StylingTailwind CSS v4 in the host + package CSS tokens (@vivabox/ui/styles.css)
Design tokensOKLCH CSS variables, [data-theme='…'] + .dark
PrimitivesRadix UI (Dialog, Select, Dropdown, Tooltip, Scroll Area, Label, Separator, Slot, Collapsible, …)
Variantsclass-variance-authority + clsx + tailwind-merge (cn)
Light / darknext-themes (re-exported as useTheme)
TablesTanStack Table v8 (ManagedDataTable)
Command palettekbar + cmdk
Docs / style guideNext.js 15 App Router (apps/docs)
Monorepopnpm 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
  • ProvidersVivaBoxProvider for 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 palettedata-theme on <html> (useThemeConfig)
  • Light / dark / system.dark class via next-themes (useTheme)

Bundled themes

NameSlug
Claudeclaude
Neobrutualismneobrutualism
Supabasesupabase
Vercel (default)vercel
Monomono
Notebooknotebook
Light Greenlight-green
Zenzen
Astro Vistaastro-vista
WhatsAppwhatsapp

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

ExportRole
VivaBoxProviderRoot: light/dark + named theme
useThemeConfig{ activeTheme, setActiveTheme }
useThemeFrom next-themes (mode)
THEMES, DEFAULT_THEME, ACTIVE_THEME_COOKIECatalog + SSR cookie
cnclsx + 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

ExportRole
AppShellLogged-in sidebar + header slots
Header, AppSidebar, SchoolSidebar, SchoolSwitcher, UserAvatarProfile, DashboardShellLower-level copied Academia chrome exports
Sidebar* / useSidebarLower-level sidebar primitives
ManagedDataTableList-page table kit
DataTable* helpersPagination, column header, toolbar, filters, skeleton
ListPageHeaderList page title/actions
CommandSearch, CommandSearchTriggerGlobal command palette

Full export list: packages/ui/src/index.ts. Prefer importing only what you need from @vivabox/ui.


Rules of engagement

  1. 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.
  2. Do not copy Academia screens into products — extract through this package or open a PR here.
  3. Theme via tokens — prefer CSS variables (bg-primary, text-muted-foreground) over one-off hex.
  4. Pass i18n from the host — use labels props; the library does not hard-depend on next-intl.
  5. Pass behaviour as props — nav, search actions, export handlers, form submit.
  6. Verify @source in a fresh app before calling an install “done”.

Style guide & Theme Studio

SurfaceURL (local docs app)
Consumer guide/guide
Home/
/components/buttonLoading: first block (isLoading)
/components/accordionRadix accordion (single / multiple)
/components/mapLeaflet pin picker (dynamic, ssr: false)
/components/audit-trailAcademia audit panels with mock data — host fetches real APIs
/components/multi-selectFaculty / Department / Level scope bar (Academia MultiSelectFilter)
/components/drawerClick Open sheet (right panel) / Open drawer
/patterns/app-shellAcademia SchoolSidebar (structure real; nav labels are demo placeholders). Help / Ctrl⌘+I = infobar. Host passes real items.
/patterns/managed-data-tableToolbar Filters/Columns; bottom Rows per page (10/25/50/100)
/studioAcademia 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

SymptomLikely fix
Components unstyled / missing utilitiesFix @source path to @vivabox/ui (or workspace packages/ui)
Theme flash on loadSet data-theme on <html> from cookie before paint; pass same value to VivaBoxProvider
Dark mode not applyingEnsure VivaBoxProvider is mounted; check .dark on <html>
Select / Dialog / Sidebar brokenConfirm you import from @vivabox/ui (client components) and do not strip 'use client' boundaries
Table export does nothingImplement onExportExcel / onExportPdf in the host

Versioning & support

  • Current package version: 0.1.0 (packages/ui; still private: true until 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

FieldValue
Canonical fileplanning/docs/HOW-TO-USE-PACKAGE.md
Implementation SoTplanning/docs/VivaBox-UI-Library-Proposal.md (update as work lands)
Hosted atStyle guide /guide (same markdown)
Package READMEShort pointer + setup; full detail here
Last aligned with code0.1.0 — NO-REWRITE recopy: buttons/inputs/search/dropdowns/drawers/tables/header/sidebar/Theme Studio + catalog pages