UI Components (@numa/ui)
Shared UI component library built with shadcn/ui, Radix UI, Tailwind CSS v4, and class-variance-authority (cva).
Stack
| Library | Role |
|---|---|
| Tailwind CSS v4 | Utility-first styling (CSS-first) |
| shadcn/ui | UI primitives (Radix-based) |
| Radix UI | Accessible headless components |
| cva | Variant-based component styling |
| lucide-react | Icon library |
| vaul | Drawer component |
| react-day-picker | Calendar/date picker |
| sonner | Toast notifications |
| tw-animate-css | Animations |
Package structure
packages/ui/
├── globals.css # Tailwind v4 theme, design tokens, CSS variables
├── primitives/ # shadcn base components
│ ├── button.tsx
│ ├── calendar.tsx
│ ├── dialog.tsx
│ ├── drawer.tsx
│ ├── popover.tsx
│ ├── select.tsx
│ └── sheet.tsx
├── shared/
│ ├── ui/ # G-prefixed shared components
│ │ ├── typography/ # GText
│ │ ├── button/ # GButton
│ │ ├── input/ # GInput, GPassword, GEmail, GSelect, etc.
│ │ ├── icon/ # GIcon
│ │ ├── table/ # GTable
│ │ ├── modal/ # Modal components
│ │ ├── form/ # GForm (declarative form builder)
│ │ ├── sonner.tsx # Toast re-export
│ │ └── otp/ # OTP input
│ ├── layout/ # Layout shells
│ │ ├── energy/ # GLayoutEnergy, GEnergyPage
│ │ ├── auth/ # Auth layout
│ │ ├── dashboard/ # Dashboard layout
│ │ └── containers/ # GContainer
│ ├── loaders/ # Loading states
│ ├── error/ # Error page components
│ └── private/ # Permission guard components
└── global.d.tsG-prefixed components
Numa uses a G prefix convention for shared components to distinguish them from shadcn primitives:
| Component | Purpose |
|---|---|
GButton | Button with variants, loading state, AppleSpinner |
GText | Typography with size/weight variants |
GIcon | Typed icon component using iconSrc from config |
GInput | Text input with label, error, prefix/suffix icons |
GPassword | Password input with visibility toggle |
GEmail | Email input with validation |
GSelect | Select dropdown |
GPhoneNumber | Phone number input |
GTextarea | Multi-line text input |
GForm | Declarative form builder (see Forms) |
GTable | Data table component |
GContainer | Page container wrapper |
Importing components
import { GLayoutEnergy } from "@numa/ui/shared/layout";
import GIcon from "@numa/ui/shared/ui/icon";
import { GText } from "@numa/ui/shared/ui/typography";
import { GButton } from "@numa/ui/shared/ui/button";
import { Toaster, toast } from "@numa/ui/shared/ui/sonner";Layouts
| Layout | Route group | Description |
|---|---|---|
GLayoutEnergy | (energy)/ | Sidebar + top bar for energy dashboard |
GLayoutAuth | (auth)/ | Auth pages shell |
GContainer | All | Main content container |
GEnergyPage
A layout component for energy list screens providing consistent header, search, actions, and query loading/error/empty states:
<GEnergyPage
title="Clients"
searchPlaceholder="Search clients..."
actions={<Button>Add Client</Button>}
query={{ isLoading, error }}
>
{/* Page content */}
</GEnergyPage>Adding shadcn components
Use the gc script:
pnpm gc -- inputThis runs npx shadcn@latest add input and moves the result into the nested folder structure (shared/ui/input/index.tsx).
For manual addition:
npx shadcn@latest add button -p components/shared/uiThen move the file to shared/ui/button/index.tsx.
Sub-path exports
Every import path used by consumers must be explicitly listed in packages/ui/package.json exports:
{
"exports": {
"./shared/layout/auth": "./shared/layout/auth/index.tsx",
"./shared/ui/button": "./shared/ui/button/index.tsx"
}
}If you add a new component, add its export path to package.json.
Theming
See the Theming page for how CSS variables and design tokens work.