npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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 TricolorBar with sweep and infinite shimmer animations.

npm version npm downloads license

Scaffold a full portal in seconds using the CLI:

npx @lucifer91299/create-portal-app my-portal

Table of Contents


Install

npm install @lucifer91299/ui framer-motion jose
# Charts (optional)
npm install recharts

Required 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 nextConfig

2. 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 Config

3. 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.css in layout.tsx, not in globals.css. Importing SDK CSS from node_modules can 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:

  1. series[i].color (or data[i].color for DonutChart) — explicit override
  2. CSS variables --primary, --accent, --success — from your ThemeProvider
  3. 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, AreaRange
  • EChartsBar.tsx — Column, Bar, StackedColumn, StackedBar, ColumnRange, Waterfall, BarLabelRotation, DataZoomColumn, BrushColumn
  • EChartsMixed.tsx — Finance (Candlestick + Volume + MA + DataZoom)
  • EChartsPie.tsx — Pie, Donut, Nightingale, Funnel, Polar/Radar
  • EChartsScatter.tsx — Scatter, EffectScatter, Bubble, BoxPlot, Candlestick
  • EChartsGaugeHeatmap.tsx — Gauge, SolidGauge, Heatmap, CalendarHeatmap
  • EChartsHierarchy.tsx — Treemap, Tree, Sunburst
  • EChartsFlowAdvanced.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