@lucifer91299/ui
v1.2.8
Published
Portal UI design system — components, Tailwind preset, auth hooks for Next.js
Readme
@lucifer91299/ui
Next.js portal design system — animated login, dashboard layout, JWT auth hooks, full theming, and 90+ production-ready components. Full shadcn/ui-compatible composable API. 40 Apache ECharts chart types (open-source). Includes
TricolorBarwith sweep and infinite shimmer animations.
Scaffold a full portal in seconds using the CLI:
npx @lucifer91299/create-portal-app my-portalTable of Contents
- Install
- Setup (5 steps)
- Styling Components
- Components
- Button
- Input & Textarea
- Select
- DatePicker
- DateTimePicker
- Switch, Checkbox, RadioGroup
- Badge & StatusBadge
- DataTable
- Alert
- AlertDialog
- Card, Separator, AlertBanner
- Dialog (composable)
- Drawer
- Label
- Table
- Pagination
- ScrollArea
- Toggle & ToggleGroup
- Collapsible
- Tabs
- Accordion
- Tooltip & Popover
- Avatar & AvatarGroup
- Progress, Skeleton, LoadingSpinner, PageLoader
- Combobox
- ConfirmModal
- AlertModal
- Skeleton Presets
- Toast
- StatsCard & EmptyState
- FileUpload
- OTPInput
- NumberInput
- Slider
- TagInput
- Timeline
- Charts (Recharts)
- Apache ECharts — 40 chart types
- ImageViewer
- DropdownMenu
- PhoneInput
- ProfilePhotoInput
- AttendanceCalendar
- LoginPage (Animated)
- LoginPageSimple (Clean)
- DashboardLayout
- HeaderNav
- DashboardFullPage
- LanguageSwitcher
- Layout Primitives
- Auth Hooks
- Auth API routes
- Middleware / proxy.ts
- Theming
- Server exports
- Local development
- Changelog
Install
npm install @lucifer91299/ui framer-motion jose
# Charts (optional)
npm install rechartsRequired peer deps: react >=18, next >=14, framer-motion >=10, tailwindcss >=3
Setup (5 steps)
1. next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
transpilePackages: ['@lucifer91299/ui'],
}
export default nextConfig2. tailwind.config.ts
import type { Config } from 'tailwindcss'
import preset from '@lucifer91299/ui/tailwind/preset'
export default {
presets: [preset],
content: [
'./src/**/*.{ts,tsx}',
'./node_modules/@lucifer91299/ui/dist/index.js',
],
} satisfies Config3. src/theme.config.ts
import { createTheme } from '@lucifer91299/ui'
export default createTheme({
primary: '#000080',
accent: '#FF9933',
success: '#138808',
projectName: 'My Portal',
logoSrc: '/brand/logo.svg',
sidebar: 'full', // 'full' | 'rail' | 'header'
loginStyle: 'animated', // 'animated' | 'simple'
})4. src/app/layout.tsx
import '@lucifer91299/ui/styles/components.css'
import './globals.css'
import { ThemeProvider } from '@lucifer91299/ui'
import theme from '@/theme.config'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<ThemeProvider theme={theme}>{children}</ThemeProvider>
</body>
</html>
)
}5. src/app/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;Import
@lucifer91299/ui/styles/components.cssinlayout.tsx, not inglobals.css. Importing SDK CSS fromnode_modulescan trigger Tailwind directive errors.
Styling Components
Every component in this library supports both className and style props on its root element. This makes it trivial to override defaults using Tailwind classes or inline CSS without fighting specificity.
className — Tailwind / CSS classes
<StatsCard className="border-2 border-blue-500 shadow-xl" ... />
<Dialog className="max-w-2xl" ... />
<PortalBarChart className="rounded-2xl bg-white p-4" ... />style — Inline CSS (React.CSSProperties)
<LoginPage style={{ background: 'linear-gradient(to bottom, #001, #003)' }} ... />
<DashboardLayout style={{ '--primary': '#7c3aed' } as React.CSSProperties} ... />
<Tooltip style={{ background: '#333', borderRadius: 8 }} ... />CSS Variable overrides via style
Because the theme system uses CSS variables, you can override individual token values per-component:
<StatsCard
style={{ '--primary': '#e11d48', '--primary-soft': 'rgba(225,29,72,0.1)' } as React.CSSProperties}
variant="primary"
...
/>Component className / style coverage
| Component | className | style | Notes |
|-----------|:-----------:|:-------:|-------|
| Button | ✓ | ✓ | via ButtonHTMLAttributes |
| Input | ✓ | ✓ | via InputHTMLAttributes; wraps <input> |
| Textarea | ✓ | ✓ | via TextareaHTMLAttributes |
| Card | ✓ | ✓ | via HTMLAttributes<HTMLDivElement> |
| Select | ✓ | ✓ | applied to trigger button |
| MultiSelect | ✓ | ✓ | applied to trigger button |
| DatePicker | ✓ | ✓ | applied to trigger button |
| DateTimePicker | ✓ | ✓ | applied to trigger button |
| Switch | ✓ | ✓ | applied to root wrapper |
| Checkbox | ✓ | ✓ | applied to root wrapper |
| RadioGroup | ✓ | ✓ | applied to root wrapper |
| Badge | ✓ | ✓ | |
| AlertBanner | ✓ | ✓ | |
| Separator | ✓ | ✓ | all 3 orientation branches |
| Dialog | ✓ | ✓ | merged with default box-shadow |
| Drawer | ✓ | ✓ | merged with slide transform |
| Tabs / TabsList / TabsTrigger / TabsContent | ✓ | ✓ | |
| Accordion / AccordionItem | ✓ | ✓ | |
| Tooltip | ✓ | ✓ | merged with positioning + background |
| Popover | ✓ | ✓ | applied to content panel |
| Avatar / AvatarGroup | ✓ | ✓ | merged with initials background |
| Progress | ✓ | ✓ | |
| Skeleton / SkeletonText / SkeletonCard | ✓ | ✓ | merged with width/height |
| LoadingSpinner | ✓ | ✓ | |
| StatsCard | ✓ | ✓ | |
| EmptyState | ✓ | ✓ | |
| FileUpload | ✓ | ✓ | |
| OTPInput | ✓ | ✓ | |
| NumberInput | ✓ | ✓ | |
| Slider | ✓ | ✓ | |
| TagInput | ✓ | ✓ | |
| Timeline | ✓ | ✓ | |
| DataTable | ✓ | ✓ | |
| Stepper | ✓ | ✓ | |
| PortalBarChart | ✓ | ✓ | merged with width/height |
| PortalLineChart | ✓ | ✓ | merged with width/height |
| PortalAreaChart | ✓ | ✓ | merged with width/height |
| PortalDonutChart | ✓ | ✓ | merged with width/height |
| ImageViewer | ✓ | ✓ | applied to root overlay |
| DropdownMenu | — | — | portal-rendered, no root element |
| PhoneInput | ✓ | — | applied to number <input> |
| ProfilePhotoInput | ✓ | ✓ | applied to root wrapper |
| AttendanceCalendar | ✓ | ✓ | applied to root wrapper |
| LoginPage | ✓ | ✓ | merged with gradient background |
| LoginPageSimple | ✓ | ✓ | |
| RoleSelectSplash | ✓ | ✓ | |
| DashboardLayout | ✓ | ✓ | |
| Sidebar | ✓ | ✓ | |
| SidebarRail | ✓ | ✓ | |
| HeaderNav | ✓ | ✓ | applied to desktop sticky <header> |
| PageShell | ✓ | ✓ | |
| PageFooter | ✓ | ✓ | |
| BrandLogo | ✓ | ✓ | |
| TricolorBar | ✓ | ✓ | merged with bar gradient/height; shimmer uses pseudo-element |
| SocialLinks | ✓ | ✓ | |
| PoweredBy | ✓ | ✓ | |
Components
Button
import { Button } from '@lucifer91299/ui'
<Button variant="primary">Save</Button>
<Button variant="accent">Highlight</Button>
<Button variant="tinted">Tinted</Button>
<Button variant="secondary">Secondary</Button> {/* bordered, primary colour */}
<Button variant="gray">Gray</Button> {/* gray fill */}
<Button variant="outline">Cancel</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="plain">Plain / link</Button> {/* text-only, hover underline */}
<Button variant="danger">Delete</Button>
<Button size="sm">Small</Button>
<Button size="md">Medium</Button> {/* default */}
<Button size="lg">Large</Button>
<Button isLoading>Saving…</Button>
<Button disabled>Disabled</Button>| Variant | Appearance |
|---|---|
| primary | Solid — CSS-variable primary colour |
| accent | Solid — CSS-variable accent colour |
| tinted | Soft primary-colour fill |
| secondary | White background, primary-colour border & text |
| gray | Gray-100 fill, secondary label text |
| outline | White with opaque separator border |
| ghost | Transparent, secondary label text |
| plain | Fully transparent, hover underline |
| danger | Red-500 fill |
Input & Textarea
type="password" automatically renders an Eye / EyeOff toggle button — no extra props needed.
import { Input, Textarea } from '@lucifer91299/ui'
<Input label="Full name" placeholder="Priya Mehta" />
<Input label="Email" type="email" placeholder="[email protected]" />
{/* Password — toggle button appears automatically */}
<Input label="Password" type="password" />
<Input label="Confirm password" type="password" />
<Input label="With error" error="This field is required" />
<Input label="Disabled" disabled defaultValue="Read-only" />
<Input label="With right label" labelRight={<a href="#">Forgot?</a>} />
<Input label="With suffix icon" suffix={<SearchIcon className="w-4 h-4" />} />
<Textarea label="Message" placeholder="Type here…" helperText="Max 500 chars" />
<Textarea label="With error" error="Message is required" />| Prop | Type | Description |
|------|------|-------------|
| type | string | "password" auto-shows Eye/EyeOff toggle |
| suffix | ReactNode | Icon/element rendered on the right (ignored when type="password") |
| label | ReactNode | Field label |
| labelRight | ReactNode | Right-aligned label slot (e.g. "Forgot password?") |
| error | string | Red border + error message below |
| helperText | string | Helper text (hidden when error is set) |
Select
Supports single select and multi-select with pill tags, search, grouped options, select-all, and clear.
import { Select } from '@lucifer91299/ui'
const options = [
{ value: 'admin', label: 'Administrator' },
{ value: 'manager', label: 'Manager' },
{ value: 'viewer', label: 'Viewer' },
]
{/* Single */}
<Select
label="Role"
options={options}
value={value}
onChange={setValue}
searchable
clearable
/>
{/* Multi-select — pill tags, select-all, Done button */}
<Select
label="Roles"
multiple
options={options}
value={values} // string[]
onChange={setValues} // (values: string[]) => void
placeholder="Pick roles…"
clearable
helperText={values.length ? `${values.length} selected` : ''}
/>
{/* Grouped */}
<Select
label="Team member"
multiple
options={[
{ value: 'admin', label: 'Admin', group: 'Management' },
{ value: 'manager', label: 'Manager', group: 'Management' },
{ value: 'editor', label: 'Editor', group: 'Content' },
]}
value={values}
onChange={setValues}
searchable
/>| Prop | Type | Description |
|------|------|-------------|
| options | SelectOption[] | { value, label, disabled?, group? } |
| multiple | true | Enable multi-select mode |
| value | string or string[] | Controlled value |
| onChange | (v) => void | string for single, string[] for multi |
| searchable | boolean | Show search input in dropdown |
| clearable | boolean | Show clear button |
| error | string | Red border + error message below |
| onAddNew | () => void | Show "Add new…" footer row |
| maxTagsShown | number | Max pill tags before "+N more" (default 3) |
DatePicker
3-level calendar (days → months → years). Supports uncontrolled mode, past/future/weekend/specific date constraints.
import { DatePicker } from '@lucifer91299/ui'
{/* Uncontrolled — no value/onChange needed */}
<DatePicker label="Pick a date" />
{/* Controlled */}
<DatePicker
label="Start date"
value={date} // 'yyyy-MM-dd'
onChange={setDate}
/>
{/* Constraints */}
<DatePicker label="No future dates" disableFuture />
<DatePicker label="No past dates" disablePast />
<DatePicker label="Weekdays only" excludeWeekends />
<DatePicker label="Range" minDate="2024-01-01" maxDate="2024-12-31" />
{/* Specific dates disabled */}
<DatePicker
label="Blocked dates"
excludeDates={['2024-12-25', '2024-12-26', '2025-01-01']}
helperText="Holidays disabled"
/>| Prop | Type | Description |
|------|------|-------------|
| value | string | 'yyyy-MM-dd'. Omit for uncontrolled mode |
| onChange | (iso: string) => void | Called on day select or Clear |
| disableFuture | boolean | Block all dates after today |
| disablePast | boolean | Block all dates before today |
| minDate / maxDate | string | 'yyyy-MM-dd' range bounds |
| excludeWeekends | boolean | Disable Sat + Sun |
| excludeDates | string[] | Specific 'yyyy-MM-dd' dates to block |
| error | string | Red border + error message below |
Disabled dates render with strikethrough, ash background, and muted colour.
DateTimePicker
All DatePicker features plus a time spinner — 12h/24h format, minute step, optional seconds, minTime/maxTime, and a Now button.
import { DateTimePicker } from '@lucifer91299/ui'
{/* 24-hour, 5-minute steps (uncontrolled) */}
<DateTimePicker
label="Schedule"
minuteStep={5}
/>
{/* 12-hour format with AM/PM toggle */}
<DateTimePicker
label="Meeting time"
value={dt} // 'yyyy-MM-dd HH:mm'
onChange={setDt}
timeFormat="12h"
/>
{/* With seconds */}
<DateTimePicker
label="Exact time"
value={dt}
onChange={setDt}
showSeconds
helperText={dt} // shows 'yyyy-MM-dd HH:mm:ss'
/>
{/* All date constraints work identically to DatePicker */}
<DateTimePicker
label="Workday only"
disableFuture
excludeWeekends
excludeDates={['2025-05-01']}
minTime="09:00"
maxTime="18:00"
minuteStep={15}
/>| Prop | Type | Default | Description |
|------|------|---------|-------------|
| value | string | — | 'yyyy-MM-dd HH:mm' or 'yyyy-MM-dd HH:mm:ss'. Omit for uncontrolled |
| onChange | (v: string) => void | — | |
| timeFormat | '12h' \| '24h' | '24h' | 12h shows AM/PM toggle |
| minuteStep | number | 1 | Step size for minute spinner (e.g. 5, 10, 15, 30) |
| showSeconds | boolean | false | Add seconds spinner; value format becomes HH:mm:ss |
| minTime / maxTime | string | — | 'HH:mm' allowed time range |
| disableFuture | boolean | — | Same as DatePicker |
| disablePast | boolean | — | Same as DatePicker |
| minDate / maxDate | string | — | Same as DatePicker |
| excludeWeekends | boolean | — | Same as DatePicker |
| excludeDates | string[] | — | Same as DatePicker |
| error | string | — | Red border + error message below |
UI flow: Click trigger → pick date → adjust time spinners with ▲/▼ → press Done. Now sets both to current moment. Clear resets.
Switch, Checkbox, RadioGroup
All three support an error prop — renders the label/indicator in red with an error message below.
import { Switch, Checkbox, RadioGroup } from '@lucifer91299/ui'
<Switch label="Email notifications" description="Daily digest" checked={on} onChange={setOn} />
<Switch label="Disabled" disabled />
<Switch label="Required" error="You must enable notifications" />
<Checkbox label="Accept terms" description="I agree" checked={checked} onChange={setChecked} />
<Checkbox label="Indeterminate" indeterminate />
<Checkbox label="Required" error="You must accept the terms" />
<RadioGroup
label="Billing cycle"
options={[
{ value: 'monthly', label: 'Monthly', description: 'Billed every month' },
{ value: 'quarterly', label: 'Quarterly', description: 'Save 10%' },
{ value: 'annual', label: 'Annual', description: 'Save 25%' },
]}
value={cycle}
onChange={setCycle}
orientation="vertical" // 'vertical' | 'horizontal'
/>
{/* RadioGroup with validation error */}
<RadioGroup
options={options}
value=""
onChange={setCycle}
error="Please select a billing cycle"
/>Badge & StatusBadge
import { Badge, StatusBadge } from '@lucifer91299/ui'
{/* Original variants */}
<Badge variant="primary">Primary</Badge>
<Badge variant="active">Active</Badge>
<Badge variant="pending">Pending</Badge>
<Badge variant="inactive">Inactive</Badge>
<Badge variant="rejected">Rejected</Badge>
{/* Extended variants (sales frontend parity) */}
<Badge variant="expired">Expired</Badge> {/* neutral ring */}
<Badge variant="dead">Dead</Badge> {/* dark/filled */}
<Badge variant="navy">Navy</Badge> {/* primary soft */}
<Badge variant="saffron">Saffron</Badge> {/* accent soft */}
<Badge variant="green">Green</Badge> {/* success soft */}
{/* Auto-styled workflow states */}
<StatusBadge status="active" />
<StatusBadge status="pending" />
<StatusBadge status="approved" />
<StatusBadge status="rejected" />
<StatusBadge status="completed" />
<StatusBadge status="paid" />
<StatusBadge status="scheduled" />
<StatusBadge status="cancelled" />| Variant | Style |
|---------|-------|
| active | Green |
| pending | Amber |
| inactive | Gray |
| rejected | Red |
| primary | Primary color |
| expired | Neutral + ring |
| dead | Dark filled |
| navy | Primary soft bg |
| saffron | Accent soft bg |
| green | Success soft bg |
DataTable
Fully-featured data grid with sorting, global search, per-column filters, pagination, row actions, and multi-row checkbox selection. Selection supports both uncontrolled (internal state) and controlled (external state) modes, plus a selectionActions slot for bulk-action buttons.
import { DataTable, StatusBadge, ActionButtons } from '@lucifer91299/ui'
const columns = [
{
key: 'name',
header: 'Name',
sortable: true,
searchable: true,
render: (row) => <span className="font-medium">{row.name}</span>,
},
{
key: 'status',
header: 'Status',
sortable: true,
filterOptions: [
{ value: 'active', label: 'Active' },
{ value: 'inactive', label: 'Inactive' },
],
render: (row) => <StatusBadge status={row.status} />,
},
]
// ── Uncontrolled selection (internal state) ───────────────────────────────────
<DataTable
title="Members"
columns={columns}
data={rows}
keyExtractor={(r) => r.id}
searchable
pagination
defaultPageSize={10}
selectable
onSelectionChange={(items, keys) => console.log(items, keys)}
striped
toolbar={<Button size="sm">Export CSV</Button>}
/>
// ── Controlled selection + bulk-action bar ────────────────────────────────────
const [selectedIds, setSelectedIds] = useState<string[]>([])
<DataTable
columns={columns}
data={rows}
keyExtractor={(r) => r.id}
selectable
selectedKeys={selectedIds}
onSelectionChange={(_, keys) => setSelectedIds(keys as string[])}
selectionActions={
<button
onClick={() => handleBulkDelete(selectedIds)}
className="text-white/80 hover:text-white underline text-xs"
>
Delete {selectedIds.length}
</button>
}
/>DataTable props:
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| columns | TableColumn[] | — | Column definitions |
| data | T[] | — | Row data |
| keyExtractor | (item) => string \| number | — | Unique row key |
| isLoading | boolean | false | Show skeleton |
| loadingRows | number | 6 | Skeleton row count |
| searchable | boolean | false | Global search bar |
| searchPlaceholder | string | 'Search…' | |
| pagination | boolean | false | Enable pagination |
| defaultPageSize | number | 10 | |
| pageSizeOptions | number[] | [10,25,50,100] | |
| selectable | boolean | false | Checkbox column for multi-select |
| selectedKeys | (string \| number)[] | — | Controlled — overrides internal selection state |
| onSelectionChange | (items, keys) => void | — | Fires on every selection change |
| selectionActions | ReactNode | — | Slot rendered inside the blue selection bar (bulk-action buttons) |
| striped | boolean | false | Alternating row shading |
| compact | boolean | false | Reduced cell padding |
| stickyHeader | boolean | false | Freeze header on scroll |
| title | string | — | Card heading |
| description | string | — | Card sub-heading |
| toolbar | ReactNode | — | Top-right toolbar slot |
| rowActions | (item, index) => ReactNode | — | Per-row actions column |
| actionsHeader | string | '' | Header label for actions column |
TableColumn props:
| Prop | Type | Description |
|------|------|-------------|
| key | string | Unique identifier, used for sort |
| header | string | Column header text |
| render | (row) => ReactNode | Cell renderer |
| sortable | boolean | Enable sort on this column |
| searchable | boolean | Include in global search |
| filterOptions | { value, label }[] | Per-column dropdown filter |
| width | string | e.g. '80px' |
| align | 'left' \| 'center' \| 'right' | |
ActionButtons props: showView, showEdit, showDelete, showApprove, showReject — each paired with an on* handler.
Alert
Inline contextual alert with 5 variants, optional title, dismiss button, and custom icon.
import { Alert, AlertTitle, AlertDescription } from '@lucifer91299/ui'
<Alert variant="info" title="Heads up" dismissible>
Your session will expire in 30 minutes.
</Alert>
<Alert variant="success">
<AlertTitle>Saved!</AlertTitle>
<AlertDescription>Your changes have been applied successfully.</AlertDescription>
</Alert>
<Alert variant="warning" title="Almost full" dismissible>
You have used 90% of your storage quota.
</Alert>
<Alert variant="destructive" icon={null}>
Payment failed — please update your billing details.
</Alert>| Variant | Appearance |
|---|---|
| default | Neutral surface |
| info | Blue |
| success | Green |
| warning | Amber |
| destructive | Red |
| Prop | Type | Description |
|---|---|---|
| variant | AlertVariant | Color variant (default 'default') |
| title | string | Bold heading inside the alert |
| dismissible | boolean | Show × dismiss button — unmounts on click |
| icon | ReactNode \| null | Override icon. Pass null to hide icon entirely |
AlertDialog
Composable destructive-confirmation dialog — identical API to shadcn/ui AlertDialog. Built on DialogRoot so theming, animation, and portal behaviour are inherited.
import {
AlertDialog, AlertDialogTrigger, AlertDialogContent,
AlertDialogHeader, AlertDialogTitle, AlertDialogDescription,
AlertDialogFooter, AlertDialogAction, AlertDialogCancel,
} from '@lucifer91299/ui'
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="danger">Delete account</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. Your account and all data will be permanently deleted.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleDelete}>Delete account</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>| Component | Role |
|---|---|
| AlertDialog | Context root (controlled: open + onOpenChange, or uncontrolled: defaultOpen) |
| AlertDialogTrigger | Opens the dialog. asChild forwards click to child |
| AlertDialogContent | Modal panel (always size="sm", no built-in close button) |
| AlertDialogHeader | Wrapper for title + description |
| AlertDialogTitle | Accessible heading |
| AlertDialogDescription | Body copy |
| AlertDialogFooter | Button row |
| AlertDialogAction | Red confirm button — does not auto-close; wire your handler directly |
| AlertDialogCancel | Cancel button — auto-closes the dialog |
Card, Separator, AlertBanner
Card now accepts a hoverable prop for cursor-pointer + hover lift + primary-color border highlight.
<Card hoverable>
<CardContent>Click me!</CardContent>
</Card>
<Card hoverable variant="elevated">
<CardContent>Elevated + hoverable</CardContent>
</Card>Card, Separator, AlertBanner (original)
import { Card, Separator, AlertBanner } from '@lucifer91299/ui'
<Card className="p-6">Content</Card>
<Separator />
<Separator label="OR" />
<Separator orientation="vertical" /> {/* use in a flex row */}
<AlertBanner variant="info">Your session expires in 30 minutes.</AlertBanner>
<AlertBanner variant="success">Changes saved.</AlertBanner>
<AlertBanner variant="warning">This action cannot be undone.</AlertBanner>
<AlertBanner variant="error">Failed to connect to the server.</AlertBanner>Dialog (composable)
Full shadcn/ui-compatible composable API plus the original all-in-one Dialog (100% backward compatible).
Composable (shadcn-style)
import {
DialogRoot, DialogTrigger, DialogContent,
DialogHeader, DialogTitle, DialogDescription,
DialogBody, DialogFooter, DialogClose,
} from '@lucifer91299/ui'
{/* Uncontrolled — trigger handles open/close automatically */}
<DialogRoot>
<DialogTrigger asChild>
<Button variant="outline">Edit profile</Button>
</DialogTrigger>
<DialogContent size="lg">
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>Update your name and role below.</DialogDescription>
</DialogHeader>
<DialogBody>
<Input label="Full name" />
</DialogBody>
<DialogFooter>
<DialogClose asChild>
<Button variant="ghost">Cancel</Button>
</DialogClose>
<Button variant="primary" onClick={save}>Save</Button>
</DialogFooter>
</DialogContent>
</DialogRoot>
{/* Full-screen — covers entire viewport, slide-up animation */}
<DialogContent fullScreen>
<DialogHeader>
<DialogTitle>Full-screen panel</DialogTitle>
</DialogHeader>
<DialogBody noPadding>
...edge-to-edge content...
</DialogBody>
</DialogContent>
{/* Controlled */}
<DialogRoot open={open} onOpenChange={setOpen}>
...
</DialogRoot>| Component | Role |
|---|---|
| DialogRoot | Context root. open / defaultOpen / onOpenChange |
| DialogTrigger | Opens dialog. asChild forwards click to child element |
| DialogPortal | Renders children into document.body (or custom container) |
| DialogOverlay | Semi-transparent backdrop |
| DialogContent | Modal panel — size, fullScreen, hideCloseButton |
| DialogHeader | Groups title + description with bottom border |
| DialogTitle | <h2> with design-system typography |
| DialogDescription | Subtitle below title |
| DialogBody | Scrollable content area. noPadding for edge-to-edge |
| DialogFooter | Sticky footer. align="left \| right \| between" |
| DialogClose | Closes dialog. asChild to wrap any element |
DialogContent props:
| Prop | Type | Default | Description |
|---|---|---|---|
| size | 'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl' \| 'full' | 'md' | Max-width preset |
| fullScreen | boolean | false | Covers entire viewport, rounded-none |
| hideCloseButton | boolean | false | Hide built-in × button |
| onEscapeKeyDown | (e: KeyboardEvent) => void | — | Call e.preventDefault() to prevent close |
| onPointerDownOutside | () => void | — | Called when backdrop is clicked |
All-in-one (original API — still works)
import { Dialog } from '@lucifer91299/ui'
<Dialog
open={open}
onClose={() => setOpen(false)}
title="Edit profile"
description="Update your name and role."
size="md" // 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full'
fullScreen={false}
footer={
<>
<Button variant="ghost" onClick={() => setOpen(false)}>Cancel</Button>
<Button variant="primary" onClick={save}>Save</Button>
</>
}
>
<Input label="Full name" />
</Dialog>Label
Accessible form label with optional required asterisk and disabled state.
import { Label } from '@lucifer91299/ui'
<Label htmlFor="email">Email address</Label>
<Label htmlFor="name" required>Full name</Label>
<Label disabled>Disabled field</Label>| Prop | Type | Description |
|---|---|---|
| required | boolean | Appends a red * asterisk |
| disabled | boolean | Reduces opacity to 60% |
Table
Raw semantic table primitives — styled for the design system. Use these when you need full control over layout (e.g. comparison tables, invoices). For data grids with sorting/filtering/pagination, use DataTable.
import {
Table, TableHeader, TableBody, TableFooter,
TableRow, TableHead, TableCell, TableCaption,
} from '@lucifer91299/ui'
<Table>
<TableCaption>Recent invoices</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invoices.map((inv) => (
<TableRow key={inv.id}>
<TableCell className="font-medium">{inv.id}</TableCell>
<TableCell><StatusBadge status={inv.status} /></TableCell>
<TableCell className="text-right tabular-nums">{inv.amount}</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={2}>Total</TableCell>
<TableCell className="text-right font-semibold">₹12,500</TableCell>
</TableRow>
</TableFooter>
</Table>Table wraps itself in overflow-auto by default. Pass scrollable={false} to disable.
Multi-row selection in Table
Use TableCheckboxHead, TableCheckboxCell, and the useTableSelection hook to add checkbox multi-select to any raw Table.
import {
Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
TableCheckboxHead, TableCheckboxCell,
useTableSelection,
} from '@lucifer91299/ui'
function InvoiceTable({ invoices }: { invoices: Invoice[] }) {
const {
isSelected, isAllSelected, isIndeterminate,
toggleRow, toggleAll,
selectedItems, selectedCount,
} = useTableSelection(invoices, (inv) => inv.id)
return (
<>
{selectedCount > 0 && (
<p className="text-sm mb-2">{selectedCount} selected</p>
)}
<Table>
<TableHeader>
<TableRow>
{/* Select-all checkbox */}
<TableCheckboxHead
checked={isAllSelected}
indeterminate={isIndeterminate}
onChange={toggleAll}
/>
<TableHead>Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invoices.map((inv) => (
<TableRow
key={inv.id}
data-state={isSelected(inv) ? 'selected' : undefined}
>
{/* Per-row checkbox */}
<TableCheckboxCell
checked={isSelected(inv)}
onChange={() => toggleRow(inv)}
/>
<TableCell>{inv.id}</TableCell>
<TableCell><StatusBadge status={inv.status} /></TableCell>
<TableCell className="text-right tabular-nums">{inv.amount}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</>
)
}useTableSelection return values:
| Field | Type | Description |
|---|---|---|
| selectedKeys | Set<string \| number> | Raw set of selected row keys |
| selectedItems | T[] | Selected row objects |
| selectedCount | number | Count of selected rows |
| isSelected | (item) => boolean | Check if a row is selected |
| isAllSelected | boolean | All rows in items are selected |
| isIndeterminate | boolean | Some (but not all) rows selected — for header checkbox |
| toggleRow | (item) => void | Toggle a single row |
| toggleAll | () => void | Select all / deselect all |
| clearSelection | () => void | Clear all selections |
TableCheckboxHead props: checked, indeterminate?, onChange — plus all native <th> attributes.
TableCheckboxCell props: checked, onChange — plus all native <td> attributes.
Pagination
Standalone pagination for any list or infinite scroll. Also works as low-level composable primitives.
High-level controlled (PaginationBar)
import { PaginationBar } from '@lucifer91299/ui'
<PaginationBar
page={currentPage}
total={totalItems}
pageSize={10}
onPageChange={setCurrentPage}
siblingCount={1}
showInfo // shows "Showing 1–10 of 87"
/>| Prop | Type | Default | Description |
|---|---|---|---|
| page | number | — | Current page (1-based) |
| total | number | — | Total number of items |
| pageSize | number | — | Items per page |
| onPageChange | (page: number) => void | — | Called on page change |
| siblingCount | number | 1 | Page numbers shown each side of current |
| showInfo | boolean | true | Show "Showing X–Y of Z" |
Low-level composable
import {
Pagination, PaginationContent, PaginationItem,
PaginationPrevious, PaginationLink, PaginationEllipsis, PaginationNext,
} from '@lucifer91299/ui'
<Pagination>
<PaginationContent>
<PaginationItem><PaginationPrevious href="#" /></PaginationItem>
<PaginationItem><PaginationLink href="#" isActive>1</PaginationLink></PaginationItem>
<PaginationItem><PaginationLink href="#">2</PaginationLink></PaginationItem>
<PaginationItem><PaginationEllipsis /></PaginationItem>
<PaginationItem><PaginationNext href="#" /></PaginationItem>
</PaginationContent>
</Pagination>ScrollArea
Custom scroll container with a design-system-styled thin scrollbar track/thumb.
import { ScrollArea } from '@lucifer91299/ui'
{/* Vertical scroll (default) */}
<ScrollArea className="h-72 rounded-xl border border-separator-opaque p-4">
{longContent}
</ScrollArea>
{/* Horizontal scroll */}
<ScrollArea orientation="horizontal" className="w-full">
<div className="flex gap-4 w-max">{items}</div>
</ScrollArea>
{/* Both axes */}
<ScrollArea orientation="both" className="h-96 w-full">
{largeContent}
</ScrollArea>
{/* Hidden scrollbar (scroll still works) */}
<ScrollArea hideScrollbar className="h-48">
{content}
</ScrollArea>| Prop | Type | Default | Description |
|---|---|---|---|
| orientation | 'vertical' \| 'horizontal' \| 'both' | 'vertical' | Scroll axis |
| hideScrollbar | boolean | false | Hide scrollbar track while preserving scroll |
Toggle & ToggleGroup
Two-state toggle button and grouped toggles (single-select or multi-select). Identical API to shadcn/ui.
import { Toggle, ToggleGroup, ToggleGroupItem } from '@lucifer91299/ui'
import { Bold, Italic, Underline, AlignLeft, AlignCenter, AlignRight } from 'lucide-react'
{/* Single toggle — uncontrolled */}
<Toggle aria-label="Bold" defaultPressed>
<Bold className="h-4 w-4" />
</Toggle>
{/* Single toggle — controlled */}
<Toggle pressed={bold} onPressedChange={setBold} variant="outline">
<Bold className="h-4 w-4" />
</Toggle>
{/* ToggleGroup — single select */}
<ToggleGroup type="single" value={align} onValueChange={setAlign}>
<ToggleGroupItem value="left" aria-label="Align left"> <AlignLeft className="h-4 w-4" /></ToggleGroupItem>
<ToggleGroupItem value="center" aria-label="Align center"><AlignCenter className="h-4 w-4" /></ToggleGroupItem>
<ToggleGroupItem value="right" aria-label="Align right"> <AlignRight className="h-4 w-4" /></ToggleGroupItem>
</ToggleGroup>
{/* ToggleGroup — multi select */}
<ToggleGroup type="multiple" value={formats} onValueChange={setFormats} variant="outline">
<ToggleGroupItem value="bold"><Bold className="h-4 w-4" /></ToggleGroupItem>
<ToggleGroupItem value="italic"><Italic className="h-4 w-4" /></ToggleGroupItem>
<ToggleGroupItem value="underline"><Underline className="h-4 w-4" /></ToggleGroupItem>
</ToggleGroup>Toggle props:
| Prop | Type | Default | Description |
|---|---|---|---|
| pressed | boolean | — | Controlled pressed state |
| defaultPressed | boolean | false | Initial uncontrolled state |
| onPressedChange | (v: boolean) => void | — | |
| variant | 'default' \| 'outline' | 'default' | Visual style |
| size | 'sm' \| 'md' \| 'lg' | 'md' | |
ToggleGroup props: Same variant + size inherited by items. type="single" → string value. type="multiple" → string[] value.
Collapsible
Expand / collapse content area with smooth height animation. Identical API to shadcn/ui.
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@lucifer91299/ui'
import { ChevronsUpDown } from 'lucide-react'
{/* Uncontrolled */}
<Collapsible defaultOpen>
<CollapsibleTrigger asChild>
<Button variant="ghost" className="w-full justify-between">
Repositories <ChevronsUpDown className="h-4 w-4" />
</Button>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="space-y-1 pt-2">
<p>@radix-ui/react-collapsible</p>
<p>@radix-ui/react-dialog</p>
</div>
</CollapsibleContent>
</Collapsible>
{/* Controlled */}
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<CollapsibleTrigger>Toggle</CollapsibleTrigger>
<CollapsibleContent>Hidden content</CollapsibleContent>
</Collapsible>| Prop | Type | Description |
|---|---|---|
| open | boolean | Controlled open state |
| defaultOpen | boolean | Initial uncontrolled state |
| onOpenChange | (open: boolean) => void | |
| disabled | boolean | Prevents toggling |
CollapsibleTrigger accepts asChild to forward the click to any child element.
Drawer
Side-panel overlay with smooth slide-in/out animation. Opens from left or right, supports header, scrollable body, and sticky footer.
import { Drawer } from '@lucifer91299/ui'
const [open, setOpen] = useState(false)
<Button onClick={() => setOpen(true)}>Open drawer</Button>
<Drawer
open={open}
onClose={() => setOpen(false)}
title="Edit user"
description="Update details and save."
side="right" // 'left' | 'right'
size="md" // 'sm' | 'md' | 'lg' | 'full'
footer={
<>
<Button variant="ghost" onClick={() => setOpen(false)}>Cancel</Button>
<Button variant="primary" onClick={save}>Save</Button>
</>
}
>
<Input label="Full name" />
<Input label="Email" type="email" />
</Drawer>| Prop | Type | Default | Description |
|------|------|---------|-------------|
| open | boolean | — | Controls visibility |
| onClose | () => void | — | Called on backdrop click or Escape |
| title | string | — | Panel header title |
| description | string | — | Subtitle below title |
| side | 'left' \| 'right' | 'right' | Which edge the panel slides from |
| size | 'sm' \| 'md' \| 'lg' \| 'full' | 'md' | Panel width (w-72 / w-96 / w-[32rem] / w-screen) |
| footer | ReactNode | — | Sticky footer content |
| className | string | — | Extra classes on the panel |
Escape key closes the drawer. Backdrop click also closes.
Tabs
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@lucifer91299/ui'
<Tabs defaultValue="overview" variant="line"> {/* 'line' | 'pill' | 'card' */}
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="analytics">Analytics</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
<TabsTrigger value="disabled" disabled>Disabled</TabsTrigger>
</TabsList>
<TabsContent value="overview">…</TabsContent>
<TabsContent value="analytics">…</TabsContent>
<TabsContent value="settings">…</TabsContent>
</Tabs>Accordion
import { Accordion, AccordionItem } from '@lucifer91299/ui'
<Accordion type="single" defaultValue="q1">
<AccordionItem value="q1" trigger="What's included?">
Buttons, inputs, selects, date pickers, charts, tables, and more.
</AccordionItem>
<AccordionItem value="q2" trigger="How do I theme it?">
Wrap your app in ThemeProvider with createTheme().
</AccordionItem>
</Accordion>Tooltip & Popover
import { Tooltip, Popover } from '@lucifer91299/ui'
<Tooltip content="Helpful hint" placement="top">
<Button variant="outline">Hover me</Button>
</Tooltip>
{/* Popover — click-triggered, outside-click dismiss */}
<Popover
placement="bottom" // 'top' | 'bottom' | 'left' | 'right'
trigger={<Button variant="outline" size="sm">More info</Button>}
content={
<div className="space-y-1">
<p className="text-callout font-medium">Details</p>
<p className="text-footnote text-label-tertiary">Some extra context here.</p>
</div>
}
/>placement: 'top' | 'bottom' | 'left' | 'right'
Avatar & AvatarGroup
import { Avatar, AvatarGroup } from '@lucifer91299/ui'
<Avatar name="Priya Mehta" size="md" /> {/* xs | sm | md | lg */}
<Avatar src="/priya.jpg" name="Priya Mehta" />
<AvatarGroup
avatars={[{ name: 'Priya' }, { name: 'Arjun' }, { name: 'Neha' }]}
max={3}
/>Progress, Skeleton, LoadingSpinner, PageLoader
import {
Progress, Skeleton, SkeletonCard, SkeletonText,
TableSkeleton, GridSkeleton, ProfileSkeleton, SettingsSkeleton,
LoadingSpinner, PageLoader,
} from '@lucifer91299/ui'
<Progress label="Upload" value={68} showValue />
<Progress value={90} variant="success" size="lg" />
<Progress value={45} variant="warning" />
<Progress value={15} variant="danger" size="sm" />
{/* Base skeletons */}
<SkeletonCard />
<SkeletonText lines={3} />
<Skeleton className="h-12 w-12" rounded="full" />
{/* Spinner variants */}
<LoadingSpinner size="md" variant="default" /> {/* single ring (original) */}
<LoadingSpinner size="md" variant="dual" /> {/* primary outer + accent inner (reverse) */}
<LoadingSpinner size="md" variant="white" /> {/* white — for dark backgrounds */}
{/* Full-screen loading gate */}
<PageLoader label="Loading…" />Skeleton Presets
Full-page skeleton layouts for common dashboard patterns.
import { TableSkeleton, GridSkeleton, ProfileSkeleton, SettingsSkeleton } from '@lucifer91299/ui'
{/* Configurable rows × cols table */}
<TableSkeleton rows={5} cols={5} />
{/* Grid of card skeletons */}
<GridSkeleton count={6} />
{/* Profile: avatar + info + detail grid */}
<ProfileSkeleton />
{/* Settings: sidebar tabs + content panel */}
<SettingsSkeleton />| Component | Props |
|---|---|
| TableSkeleton | rows (default 5), cols (default 5), className |
| GridSkeleton | count (default 6), className |
| ProfileSkeleton | className |
| SettingsSkeleton | className |
Toast
import { ToastProvider, useToast } from '@lucifer91299/ui'
// In root layout:
<ToastProvider>{children}</ToastProvider>
// In any component:
const { toast } = useToast()
toast({ title: 'Saved!', variant: 'success' })
toast({ title: 'Error', variant: 'error', description: 'Something went wrong' })
toast({ title: 'Heads up', variant: 'warning' })
toast({ title: 'FYI', variant: 'info' })StatsCard & EmptyState
import { StatsCard, EmptyState } from '@lucifer91299/ui'
import { Users, TrendingUp } from 'lucide-react'
<StatsCard
title="Total Users"
value="1,284"
subtitle="Registered accounts"
trend={{ direction: 'up', value: '+12%', label: 'vs last month' }}
icon={<Users className="w-5 h-5" />}
variant="primary" // 'default' | 'primary' | 'success' | 'warning' | 'danger'
/>
<EmptyState
icon={<Users className="w-8 h-8" />}
title="No users yet"
description="Invite team members to get started."
action={<Button variant="primary">Invite user</Button>}
/>FileUpload
Drag-and-drop file picker with size validation, file list with remove buttons, accept filter, and error state.
import { FileUpload } from '@lucifer91299/ui'
<FileUpload
label="Profile photo"
accept="image/*"
maxSizeMB={2}
helperText="PNG or JPG, max 2 MB"
onChange={(files) => setFiles(files)}
/>
<FileUpload
label="Documents"
multiple
accept=".pdf,.doc,.docx"
maxSizeMB={10}
error="File too large"
/>| Prop | Type | Description |
|------|------|-------------|
| accept | string | MIME types or extensions (e.g. "image/*", ".pdf") |
| multiple | boolean | Allow multiple files |
| maxSizeMB | number | Max file size in MB — shows error if exceeded |
| onChange | (files: File[]) => void | Called when file list changes |
| error | string | Red border + error message |
OTPInput
4 or 6-digit code boxes with auto-advance, backspace navigation, paste support, and error state.
import { OTPInput } from '@lucifer91299/ui'
{/* 6-digit (default) */}
<OTPInput
label="Verification code"
length={6}
value={otp}
onChange={setOtp}
helperText="Enter the code sent to your email"
/>
{/* 4-digit with error */}
<OTPInput
label="PIN"
length={4}
value={pin}
onChange={setPin}
error="Incorrect PIN — try again"
/>| Prop | Type | Default | Description |
|------|------|---------|-------------|
| length | 4 \| 6 | 6 | Number of digit boxes |
| value | string | — | Controlled value |
| onChange | (v: string) => void | — | Called on each digit change |
| error | string | — | Red boxes + error message |
NumberInput
+/− stepper input with min/max/step constraints, controlled and uncontrolled modes, error state.
import { NumberInput } from '@lucifer91299/ui'
<NumberInput
label="Quantity"
value={qty}
onChange={setQty}
min={1}
max={100}
step={1}
/>
<NumberInput
label="Budget (₹ thousands)"
value={budget}
onChange={setBudget}
min={0}
max={500}
step={10}
helperText="0 – 500"
/>
<NumberInput label="Disabled" value={5} disabled />
<NumberInput label="With error" value={0} error="Must be at least 1" />Slider
Range slider with track fill, custom thumb, value format callback, min/max labels, and show-value toggle.
import { Slider } from '@lucifer91299/ui'
<Slider
label="Volume"
value={volume}
onChange={setVolume}
min={0}
max={100}
step={1}
showValue
/>
<Slider
label="Price range"
value={price}
onChange={setPrice}
min={0}
max={1000}
step={50}
valueFormat={(v) => `₹${v.toLocaleString()}`}
showValue
/>| Prop | Type | Description |
|------|------|-------------|
| value | number | Controlled value |
| onChange | (v: number) => void | |
| min / max | number | Range bounds |
| step | number | Increment size |
| showValue | boolean | Show current value above thumb |
| valueFormat | (v: number) => string | Custom value display (e.g. currency) |
TagInput
Free-text tag entry — press Enter or comma to add, Backspace to remove last, optional maxTags limit.
import { TagInput } from '@lucifer91299/ui'
<TagInput
label="Skills"
value={tags}
onChange={setTags}
placeholder="Add a skill…"
helperText="Press Enter or comma to add"
/>
<TagInput
label="Keywords"
value={keywords}
onChange={setKeywords}
maxTags={5}
helperText="Max 5 keywords"
error={keywords.length === 0 ? 'Add at least one keyword' : undefined}
/>Timeline
Activity feed with dot/icon, 5 colour variants, timestamps, and descriptions.
import { Timeline, TimelineItem } from '@lucifer91299/ui'
<Timeline>
<TimelineItem
title="Account created"
description="Welcome to the portal"
time="2025-01-15 09:00"
variant="success" // 'default' | 'primary' | 'success' | 'warning' | 'danger'
/>
<TimelineItem
title="Profile updated"
description="Name and role changed"
time="2025-01-16 14:30"
variant="primary"
/>
<TimelineItem
title="Password reset"
time="2025-01-18 11:00"
variant="warning"
/>
</Timeline>Charts
Requires recharts peer dependency (npm install recharts).
import { PortalBarChart, PortalLineChart, PortalAreaChart, PortalDonutChart } from '@lucifer91299/ui'
const data = [
{ month: 'Jan', revenue: 42, expenses: 28 },
{ month: 'Feb', revenue: 55, expenses: 31 },
]
<PortalBarChart
data={data}
xKey="month"
series={[
{ key: 'revenue', name: 'Revenue', color: '#7c3aed' },
{ key: 'expenses', name: 'Expenses', color: '#e11d48' },
]}
height={240}
legendTextColor="#444"
/>
<PortalLineChart data={data} xKey="month" series={[{ key: 'revenue', name: 'Revenue' }]} height={240} legendTextColor="#666" />
<PortalAreaChart data={data} xKey="month" series={[{ key: 'revenue', name: 'Revenue' }]} height={240} />
{/* DonutChart — fully customisable indication colors */}
<PortalDonutChart
data={[
{ label: 'Active', value: 58, color: '#138808' },
{ label: 'Pending', value: 22, color: '#FF9933' },
{ label: 'Inactive', value: 20, color: '#000080' },
]}
centerLabel="Total"
centerValue={100}
centerValueColor="#1a1a1a"
centerLabelColor="#888"
legendTextColor="#555"
height={240}
/>Chart series color resolution order:
series[i].color(ordata[i].colorfor DonutChart) — explicit override- CSS variables
--primary,--accent,--success— from yourThemeProvider - Built-in fallback palette (
#000080,#FF9933,#138808,#6366f1, …)
Chart props reference:
| Prop | Bar | Line | Area | Donut | Description |
|------|:---:|:----:|:----:|:-----:|-------------|
| height | ✓ | ✓ | ✓ | ✓ | Chart height in px (default 280) |
| showGrid | ✓ | ✓ | ✓ | — | Show grid lines |
| showLegend | ✓ | ✓ | ✓ | ✓ | Show legend |
| legendTextColor | ✓ | ✓ | ✓ | ✓ | Legend label text color (default #555) |
| rounded | ✓ | — | — | — | Rounded bar tops |
| showDots | — | ✓ | — | — | Show data dots |
| curved | — | ✓ | — | — | Smooth curve |
| stacked | — | — | ✓ | — | Stack areas |
| centerLabel | — | — | — | ✓ | Text inside donut |
| centerValue | — | — | — | ✓ | Number inside donut |
| centerValueColor | — | — | — | ✓ | Center value text color (default #1a1a1a) |
| centerLabelColor | — | — | — | ✓ | Center label text color (default #888) |
| innerRadius | — | — | — | ✓ | Inner ring radius (default 58%) |
| outerRadius | — | — | — | ✓ | Outer ring radius (default 78%) |
| className | ✓ | ✓ | ✓ | ✓ | CSS class on wrapper div |
| style | ✓ | ✓ | ✓ | ✓ | Inline style on wrapper div |
Apache ECharts — 40 chart types
Production-grade interactive charts powered by Apache ECharts (Apache-2.0 licence — 100% open source, free for commercial use). All charts are themed to the design system using CSS variables (--primary, --accent, --success), load asynchronously (skeleton shown until ready), and accept an options prop for full ECharts control. Every chart supports a watermark prop (diagonal overlay text) and a showDownload prop (save-as-PNG button).
Install peer deps first:
npm install echarts echarts-for-react
Source files (each ≤400 lines):
EChartsBase.tsx— shared theme utils, types, skeleton, renderer (watermark + download)EChartsLine.tsx— Line, Spline, StepLine, StackedLine, Area, AreaSpline, StackedArea, AreaRangeEChartsBar.tsx— Column, Bar, StackedColumn, StackedBar, ColumnRange, Waterfall, BarLabelRotation, DataZoomColumn, BrushColumnEChartsMixed.tsx— Finance (Candlestick + Volume + MA + DataZoom)EChartsPie.tsx— Pie, Donut, Nightingale, Funnel, Polar/RadarEChartsScatter.tsx— Scatter, EffectScatter, Bubble, BoxPlot, CandlestickEChartsGaugeHeatmap.tsx— Gauge, SolidGauge, Heatmap, CalendarHeatmapEChartsHierarchy.tsx— Treemap, Tree, SunburstEChartsFlowAdvanced.tsx— Graph, Sankey, Parallel, ThemeRiver, PictorialBar
All 40 chart types
| # | Component | Type | Use case |
|---|-----------|------|----------|
| 1 | HCLineChart | Line | Trends over time |
| 2 | HCSplineChart | Spline (smooth) | Smooth trend lines |
| 3 | HCStepLineChart | Step Line | Discrete state changes |
| 4 | HCStackedLineChart | Stacked Line | Cumulative trends |
| 5 | HCAreaChart | Area | Volume over time |
| 6 | HCAreaSplineChart | Area Spline | Smooth area fill |
| 7 | HCStackedAreaChart | Stacked Area | Cumulative area fill |
| 8 | HCAreaRangeChart | Area Range | High/low bands |
| 9 | HCColumnChart | Column (vertical) | Category comparison |
| 10 | HCBarChart | Bar (horizontal) | Ranked comparison |
| 11 | HCStackedColumnChart | Stacked Column | Part-to-whole comparison |
| 12 | HCStackedBarChart | Stacked Bar (horizontal) | Horizontal part-to-whole |
| 13 | HCColumnRangeChart | Column Range | Min/max per category |
| 14 | HCWaterfallChart | Waterfall | Running total changes |
| 15 | HCBarLabelRotationChart | Bar Label Rotation | Many categories with angled labels |
| 16 | HCDataZoomColumnChart | DataZoom Column | Large-scale data with scroll/zoom |
| 17 | HCBrushColumnChart | Brush Select Column | Drag-to-select region |
| 18 | HCFinanceChart | Finance / Stock | Candlestick + Volume + MA + DataZoom |
| 19 | HCPieChart | Pie | Part-to-whole |
| 20 | HCDonutChart | Donut | Part-to-whole with center |
| 21 | HCNightingaleChart | Nightingale / Rose | Radial bar comparison |
| 22 | HCFunnelChart | Funnel | Conversion pipeline |
| 23 | HCScatterChart | Scatter | Correlation / distribution |
| 24 | HCEffectScatterChart | Effect Scatter (ripple) | Highlighted data points |
| 25 | HCBubbleChart | Bubble | 3-variable comparison |
| 26 | HCBoxPlotChart | Box Plot | Statistical distribution |
| 27 | HCCandlestickChart | Candlestick / K-Line | OHLC financial data |
| 28 | HCGaugeChart | Angular Gauge | KPI dial / speedometer |
| 29 | HCSolidGaugeChart | Solid Gauge (arc) | Percentage / progress |
| 30 | HCHeatmapChart | Heatmap (cartesian) | Density / matrix |
| 31 | HCCalendarHeatmapChart | Calendar Heatmap | Activity over a year |
| 32 | HCTreemapChart | Treemap | Hierarchical proportion |
| 33 | HCTreeChart | Tree Diagram | Org chart / hierarchy |
| 34 | HCSunburstChart | Sunburst | Drill-down hierarchy |
| 35 | HCGraphChart | Graph / Network | Force-directed network |
| 36 | HCSankeyChart | Sankey | Flow / energy diagram |
| 37 | HCParallelChart | Parallel Coordinates | Multi-dimensional data |
| 38 | HCThemeRiverChart | Theme River | Stream / flow over time |
| 39 | HCPictorialBarChart | Pictorial Bar | Icon-based bar chart |
| 40 | HCPolarChart | Polar / Radar / Spider | Multi-axis comparison |
Common props (all charts)
| Prop | Type | Default | Description |
|---|---|---|---|
| title | string | — | Chart title |
| subtitle | string | — | Subtitle below title |
| height | number | 280 | Chart height in px |
| className | string | — | CSS class on wrapper |
| style | React.CSSProperties | — | Inline style on wrapper |
| options | EChartsOption | — | Deep-merged override — full Apache ECharts API |
1–6. Line / Spline / Area / Area Spline / Column / Bar
import { HCLineChart, HCColumnChart, HCBarChart, HCAreaChart } from '@lucifer91299/ui'
const categories = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
const series = [
{ name: 'Revenue', data: [420, 550, 490, 610, 730, 680] },
{ name: 'Expenses', data: [280, 310, 270, 340, 390, 360] },
]
<HCLineChart categories={categories} series={series} title="Monthly Revenue" height={300} />
<HCSplineChart categories={categories} series={series} title="Smooth Trend" />
<HCAreaChart categories={categories} series={series} title="Volume" />
<HCAreaSplineChart categories={categories} series={series} title="Smooth Area" />
<HCColumnChart categories={categories} series={series} title="Comparison" stacked />
<HCBarChart categories={categories} series={series} title="Horizontal" showDataLabels />Shared props for these 6:
| Prop | Type | Default | Description |
|---|---|---|---|
| categories | string[] | — | X-axis labels |
| series | HCSeries[] | — | { name, data, color? } |
| xAxisTitle / yAxisTitle | string | — | Axis titles |
| stacked | boolean | false | Stack series |
| showLegend | boolean | true | |
| showDataLabels | boolean | false | Show value labels on bars/points |
| showGrid | boolean | true | Show horizontal grid lines |
7. Pie Chart
import { HCPieChart } from '@lucifer91299/ui'
<HCPieChart
title="Revenue split"
data={[
{ name: 'Memberships', y: 58, color: '#000080' },
{ name: 'Sessions', y: 28 },
{ name: 'Merchandise', y: 14 },
]}
showDataLabels
showLegend
/>8. Donut Chart
import { HCDonutChart } from '@lucifer91299/ui'
<HCDonutChart
title="Status breakdown"
innerSize="55%"
data={[
{ name: 'Active', y: 63 },
{ name: 'Pending', y: 21 },
{ name: 'Inactive', y: 16 },
]}
/>9. Scatter Chart
import { HCScatterChart } from '@lucifer91299/ui'
<HCScatterChart
title="Height vs Weight"
xAxisTitle="Height (cm)"
yAxisTitle="Weight (kg)"
series={[
{ name: 'Male', data: [[170, 70], [175, 78], [180, 82], [165, 65]] },
{ name: 'Female', data: [[160, 55], [165, 60], [158, 52], [172, 68]] },
]}
/>10. Bubble Chart
import { HCBubbleChart } from '@lucifer91299/ui'
<HCBubbleChart
title="Market analysis"
xAxisTitle="Value"
yAxisTitle="Growth"
series={[
{
name: 'Products',
data: [
{ x: 95, y: 95, z: 13.8, name: 'A' },
{ x: 86, y: 76, z: 14.7, name: 'B' },
{ x: 80, y: 102, z: 23.5, name: 'C' },
],
},
]}
/>11. Angular Gauge
import { HCGaugeChart } from '@lucifer91299/ui'
<HCGaugeChart
value={72}
min={0}
max={100}
label="Performance"
suffix="%"
height={240}
/>12. Solid Gauge
import { HCSolidGaugeChart } from '@lucifer91299/ui'
<HCSolidGaugeChart
value={68}
min={0}
max={100}
label="Completion"
suffix="%"
color="#000080"
height={200}
/>13. Heatmap
import { HCHeatmapChart } from '@lucifer91299/ui'
<HCHeatmapChart
title="Sales by Day & Hour"
xCategories={['Mon', 'Tue', 'Wed', 'Thu', 'Fri']}
yCategories={['Morning', 'Afternoon', 'Evening']}
data={[
[0, 0, 10], [1, 0, 19], [2, 0, 8], [3, 0, 24], [4, 0, 67],
[0, 1, 92], [1, 1, 58], [2, 1, 78], [3, 1, 117],[4, 1, 48],
[0, 2, 35], [1, 2, 15], [2, 2, 123],[3, 2, 64], [4, 2, 52],
]}
showDataLabels
/>14. Treemap
import { HCTreemapChart } from '@lucifer91299/ui'
<HCTreemapChart
title="Budget allocation"
data={[
{ name: 'Marketing', value: 6 },
{ name: 'Engineering', value: 15 },
{ name: 'Sales', value: 10 },
{ name: 'Support', value: 4 },
{ name: 'Operations', value: 8 },
]}
/>15. Waterfall Chart
import { HCWaterfallChart } from '@lucifer91299/ui'
<HCWaterfallChart
title="Revenue Bridge"
categories={['Start', 'Sales', 'Refunds', 'Expenses', 'Net']}
data={[
{ y: 120000 },
{ y: 32000 },
{ y: -12000 },
{ y: -25000 },
{ isSum: true, y: 0 },
]}
yAxisTitle="₹"
/>16. Funnel Chart
import { HCFunnelChart } from '@lucifer91299/ui'
<HCFunnelChart
title="Sales Funnel"
data={[
{ name: 'Leads', y: 5000 },
{ name: 'Qualified', y: 3200 },
{ name: 'Proposal', y: 1800 },
{ name: 'Negotiation',y: 900 },
{ name: 'Won', y: 450 },
]}
/>17. Polar / Radar / Spider Chart
import { HCPolarChart } from '@lucifer91299/ui'
<HCPolarChart
title="Athlete Profile"
polarType="area" // 'line' | 'column' | 'area'
categories={['Speed', 'Strength', 'Endurance', 'Agility', 'Technique']}
series={[
{ name: 'Athlete A', data: [90, 75, 80, 85, 70] },
{ name: 'Athlete B', data: [65, 88, 72, 60, 95] },
]}
/>18. Area Range Chart
import { HCAreaRangeChart } from '@lucifer91299/ui'
<HCAreaRangeChart
title="Temperature Range"
yAxisTitle="°C"
categories={['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']}
series={[
{ name: 'Temperature', data: [[7, 14], [4, 12], [8, 17], [12, 22], [17, 28], [21, 33]] },
]}
/>19. Column Range Chart
import { HCColumnRangeChart } from '@lucifer91299/ui'
<HCColumnRangeChart
title="Working Hours"
yAxisTitle="Hour"
categories={['Mon', 'Tue', 'Wed', 'Thu', 'Fri']}
series={[
{ name: 'Shift A', data: [[8, 16], [8, 14], [9, 17], [8, 13], [9, 15]] },
{ name: 'Shift B', data: [[16, 0], [14, 0], [17, 0], [13, 0], [15, 0]] },
]}
/>20. Box Plot Chart
import { HCBoxPlotChart } from '@lucifer91299/ui'
<HCBoxPlotChart
title="Score Distribution"
yAxisTitle="Score"
categories={['Q1', 'Q2', 'Q3', 'Q4']}
series={[
{
name: 'Scores',
// [low, q1, median, q3, high]
data: [
[52, 65, 70, 78, 95],
[55, 68, 74, 83, 98],
[48, 60, 68, 77, 92],
[58, 72, 80, 88, 99],
],
},
]}
/>Full ECharts override
Every component accepts an options prop that is deep-merged on top of the generated options — giving full access to the Apache ECharts API:
<HCColumnChart
categories={cats}
series={series}
options={{
chart: { backgroundColor: '#1a1a2e' },
xAxis: { labels: { style: { color: '#ccc' } } },
yAxis: { gridLineColor: '#2a2a4e' },
plotOptions:{ column: { borderRadius: 8, groupPadding: 0.1 } },
}}
/>ImageViewer
Full-screen portal overlay for viewing images and PDFs. Supports zoom, rotate, download, keyboard shortcuts, and optional authenticated fetching via useCredentials.
import { ImageViewer, useImageViewer } from '@lucifer91299/ui'
// Standalone — pass src directly
const [open, setOpen] = useState(false)
<button onClick={() => setOpen(true)}>View photo</button>
<ImageViewer
src="/uploads/athlete-photo.jpg"
alt="Athlete photo"
open={open}
onClose={() => setOpen(false)}
/>
// Hook — convenient open/close helper
const { open, src, alt, openViewer, closeViewer } = useImageViewer()
<button onClick={() => openViewer('/uploads/cert.pdf', 'Certificate')}>View PDF</button>
<ImageViewer src={src} alt={alt} open={open} onClose={closeViewer} />
// Authenticated fetch — loads image via blob URL using credentials cookie
<ImageViewer
src="/api/private/photo.jpg"
alt="Private photo"
open={open}
onClose={closeViewer}
useCredentials
/>| Prop | Type | Default | Description |
|------|------|---------|-------------|
| src | string | — | Image or PDF URL |
| alt | string | '' | Alt text / aria-label |
| open | boolean | — | Controls visibility |
| onClose | () => void | — | Called on Escape or backdrop click |
| useCredentials | boolean | false | Fetch via credentials: 'include' and display as blob URL |
Keyboard shortcuts: Escape = close · + = zoom in · - = zoom out
DropdownMenu
Portal-based action menu triggered by a MoreVertical icon (or custom trigger). Auto-flips up/down based on viewport space. Closes on outside click, scroll, resize, and Escape.
import { DropdownMenu } from '@lucifer91299/ui'
import { Edit, Trash2, Eye } from 'lucide-react'
<DropdownMenu
items={[
{ label: 'View', icon: <Eye className="w-4 h-4" />, onClick: () => view(row) },
{ label: 'Edit', icon: <Edit className="w-4 h-4" />, onClick: () => edit(row) },
{ label: 'Delete', icon: <Trash2 className="w-4 h-4" />, onClick: () => del(row), variant: 'danger' },
]}
/>
// Custom trigger
<DropdownMenu
trigger={<Button size="sm" variant="outline">Actions</Button>}
items={[
{ lab