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.1.68

Published

Portal UI design system — components, Tailwind preset, auth hooks for Next.js

Readme

@lucifer91299/ui

Next.js 15 portal design system — animated login, dashboard layout, JWT auth hooks, full theming, and 65+ production-ready components. 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' | 'both' | '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 table with sorting, global search, per-column filters, row selection, pagination, and a toolbar slot.

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} />,
  },
  {
    key: 'actions',
    header: '',
    align: 'right',
    render: (row) => (
      <ActionButtons
        showEdit
        showDelete
        onEdit={() => openEdit(row)}
        onDelete={() => deleteRow(row.id)}
      />
    ),
  },
]

<DataTable
  title="Members"
  description="All registered users"
  columns={columns}
  data={rows}
  keyExtractor={(r) => r.id}
  searchable
  searchPlaceholder="Search by name…"
  pagination
  defaultPageSize={10}
  pageSizeOptions={[5, 10, 25, 50]}
  selectable
  onSelectionChange={(selected) => console.log(selected)}
  striped
  toolbar={<Button size="sm">Export CSV</Button>}
/>

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.


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

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' | 'full'
  footer={
    <>
      <Button variant="ghost" onClick={() => setOpen(false)}>Cancel</Button>
      <Button variant="primary" onClick={save}>Save</Button>
    </>
  }
>
  <Input label="Full name" />
</Dialog>

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 |


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={[
    { label: 'Approve', onClick: approve, variant: 'success' },
    { label: 'Reject',  onClick: reject,  variant: 'danger'  },
  ]}
/>

| Prop | Type | Description | |------|------|-------------| | items | DropdownMenuItem[] | Menu items array | | trigger | ReactNode | Custom trigger element (defaults to MoreVertical icon button) |

DropdownMenuItem fields:

| Field | Type | Description | |-------|------|-------------| | label | string | Menu item text | | icon | ReactNode | Optional leading icon | | onClick | () => void | Click handler | | variant | 'default' \| 'success' \| 'danger' \| 'warning' | Color variant | | disabled | boolean | Disable the item |


PhoneInput

Country flag picker + international dial code + phone number input. 250+ countries, India pinned at top, auto-formats number for storage as +<dialCode><nationalNumber>.

import { PhoneInput } from '@lucifer91299/ui'

<PhoneInput
  label="Mobile number"
  value={phone}
  onChange={(e) => setPhone(e.target.value)}  // value is '+919876543210'
/>

// Pre-select a country
<PhoneInput
  label="Mobile number"
  defaultCountryIso="US"
  value={phone}
  onChange={(e) => setPhone(e.target.value)}
/>

// With error + helper
<PhoneInput
  label="WhatsApp number"
  value={phone}
  onChange={(e) => setPhone(e.target.value)}
  error="Invalid phone number"
  helperText="Include country code"
/>

// Listen for country changes
<PhoneInput
  label="Phone"
  value={phone}
  onChange={(e) => setPhone(e.target.value)}
  onCountryChange={(country) => console.log(country.name, country.dialCode)}
/>

The onChange value is always a full E.164-style string: +919876543210. Store this directly; it round-trips safely through splitPhoneNumber if you need to display parts separately.

| Prop | Type | Default | Description | |------|------|---------|-------------| | value | string | — | Full phone string e.g. '+919876543210' | | onChange | (e: ChangeEvent) => void | — | e.target.value is the formatted E.164 string | | defaultCountryIso | string | 'IN' | ISO2 code for initial country selection | | onCountryChange | (country: PhoneCountry) => void | — | Fires when the user switches country | | label | string | — | Field label | | error | string | — | Red border + error message | | helperText | string | — | Helper text (hidden when error is set) |

Phone utilities (also exported directly):

import { splitPhoneNumber, validatePhoneNumber, formatPhoneForStorage, PHONE_COUNTRIES } from '@lucifer91299/ui'

const { country, nationalNumber } = splitPhoneNumber('+919876543210')
// country.name → 'India', country.dialCode → '+91', nationalNumber → '9876543210'

const ok = validatePhoneNumber('+919876543210')  // true / false

ProfilePhotoInput

Square drag-drop photo picker with live preview and remove button. Validates file type and size client-side.

import { ProfilePhotoInput } from '@lucifer91299/ui'

const [photo, setPhoto] = useState<File | null>(null)

<ProfilePhotoInput
  value={photo}
  onChange={setPhoto}
/>

// With constraints + error forwarding
<ProfilePhotoInput
  value={photo}
  onChange={setPhoto}
  maxSizeMb={2}
  accept="image/jpeg,image/png"
  error={errors.photo}
/>

| Prop | Type | Default | Description | |------|------|---------|-------------| | value | File \| null | — | Currently selected file | | onChange | (file: File \| null) => void | — | Called when file is selected or removed | | maxSizeMb | number | 5 | Max file size in MB | | accept | string | 'image/jpeg,image/png,image/webp' | Accepted MIME types | | error | string | — | Error message shown below the picker |


AttendanceCalendar

Two-month side-by-side attendance calendar for tracking daily present/absent records. Manages pending (unsaved) changes locally with an amber ring indicator, then flushes them via an onSave callback. Supports a "not started" empty state, a "completed" read-only state, a progress bar, and a confirm dialog for completing the period.

import { AttendanceCalendar } from '@lucifer91299/ui'
import type { AttendanceRecord } from '@lucifer91299/ui'

// Not started — shows empty state + Start button
<AttendanceCalendar
  status="not_started"
  onStart={async () => {
    await api.startInternship()
    // after this resolves the parent should re-fetch and pass status="active"
  }}
/>

// Active — two-month calendar with attendance marking
<AttendanceCalendar
  status="active"
  startDate="2025-01-15"
  attendanceRecords={records}   // AttendanceRecord[]
  presentDaysCount={42}
  requiredDays={60}
  onSave={async (changes) => {
    // changes: Map<string, 'present' | 'absent' | undefined>
    for (const [date, status] of changes) {
      await api.markAttendance({ date, status })
    }
  }}
  onComplete={async (remark) => {
    await api.completeInternship({ remark })
  }}
/>

// Completed — read-only view with progress
<AttendanceCalendar
  status="completed"
  startDate="2025-01-15"
  completedAt="2025-04-20"
  attendanceRecords={records}
  presentDaysCount={60}
  requiredDays={60}
  notes="Excellent performance throughout the internship."
/>

| Prop | Type | Default | Description | |------|------|---------|-------------| | status | 'not_started' \| 'active' \| 'completed' | — | Controls which view is rendered | | startDate | string | — | 'YYYY-MM-DD' — required for active/completed | | attendanceRecords | AttendanceRecord[] | [] | Saved records from the server | | presentDaysCount | number | 0 | Count shown in the progress bar | | requiredDays | number | 60 | Target days for completion | | completedAt | string | — | 'YYYY-MM-DD' — shown when completed | | notes | string | — | Admin notes shown below the calendar | | onSave | (changes: Map<string, 'present' \| 'absent' \| undefined>) => Promise<void> | — | Save pending changes; component shows loading state until promise resolves | | onComplete | (remark?: string) => Promise<void> | — | Confirm-complete callback | | onStart | () => Promise<void> | — | Start button callback (not_started state only) |

Day click cycle: unmarked → present (✓ green) → absent (✕ red) → unmarked. Days before startDate or in the future are non-interactive. All clicks accumulate as pending changes (amber ring) until the user clicks Save Attendance.


LoginPage (Animated)

Full-screen login with floating parallax orbs, particle canvas, and an animated tricolor stripe. Best for institutional portals.

import { LoginPage } from '@lucifer91299/ui'

<LoginPage
  projectName="My Portal"
  logoSrc="/brand/logo.svg"
  onSubmit={async ({ identifier, password }) => {
    const res = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email: identifier, password }),
    })
    if (!res.ok) throw new Error('Invalid credentials')
  }}
  isLoading={false}
  error={null}
  forgotPasswordHref="/forgot-password"
  poweredBy={{ logoSrc: '/brand/powered-by.svg', text: 'Powered by STSPL', href: 'https://xyz.com' }}
/>

The field is called identifier in onSubmit — not email. It accepts both email and username.


LoginPageSimple (Clean)

Minimal gradient card login. Optionally shows a role-select splash after login.

import { LoginPageSimple } from '@lucifer91299/ui'
import { User, Shield } from 'lucide-react'

<LoginPageSimple
  projectName="My Portal"
  logoSrc="/brand/logo.svg"
  onSubmit={async ({ email, password }) => {
    const data = await login(email, password)
    return { role: data.role } // return 'both' to show RoleSelectSplash
  }}
  roles={[
    { key: 'coach', label: 'Continue as Coach', description: 'Manage athletes', icon: <User /> },
    { key: 'judge', label: 'Continue as Judge', description: 'Score events',   icon: <Shield /> },
  ]}
  onRoleSelect={(role) => router.push(`/dashboard/${role}`)}
/>

DashboardLayout

Responsive layout supporting four sidebar variants. Handles navigation, user info, logout, and mobile drawer automatically.

'use client'

import { DashboardLayout, useJwtAuth } from '@lucifer91299/ui'
import { usePathname } from 'next/navigation'
import { LayoutDashboard, Users, Settings } from 'lucide-react'

const navGroups = [
  {
    heading: 'Main',
    groupIcon: <LayoutDashboard className="w-3.5 h-3.5" />,
    items: [
      { label: 'Dashboard', href: '/dashboard',          icon: <LayoutDashboard className="w-4 h-4" /> },
      { label: 'Users',     href: '/dashboard/users',    icon: <Users className="w-4 h-4" /> },
      { label: 'Settings',  href: '/dashboard/settings', icon: <Settings className="w-4 h-4" /> },
    ],
  },
]

export default function Layout({ children }: { children: React.ReactNode }) {
  const pathname = usePathname()
  const { user, loading, logout } = useJwtAuth()

  if (loading) return <div className="min-h-screen flex items-center justify-center">Loading…</div>

  return (
    <DashboardLayout
      navGroups={navGroups}
      sidebar="full"   // 'full' | 'rail' | 'both' | 'header'
      projectName="My Portal"
      logoSrc="/brand/logo.svg"
      user={{ name: String(user?.name ?? 'User'), role: String(user?.role ?? '') }}
      pathname={pathname}
      onLogout={logout}
    >
      {children}
    </DashboardLayout>
  )
}

Sidebar variants:

| Value | Description | |-------|-------------| | full | Wide sidebar with group headings, nav labels, and collapsible sections | | rail | Icon-only narrow sidebar | | both | Full on desktop, rail on mobile/tablet | | header | Horizontal top nav bar — see HeaderNav |

icon and groupIcon accept pre-rendered JSX (ReactNode), not component types. Always pass <Icon className="w-4 h-4" />, not Icon.


HeaderNav

Horizontal top navigation bar — logo + brand name, scrollable pill links, dropdown groups, a profile menu, and an optional settings gear icon. Used automatically when sidebar="header" is passed to DashboardLayout.

Now matches the sales frontend style: gradient background, TricolorBar shimmer at the bottom of the bar, rounded-full nav pills, polished profile button with name + role visible.

import { HeaderNav } from '@lucifer91299/ui'

<HeaderNav
  navGroups={navGroups}
  projectName="My Portal"
  logoSrc="/brand/logo.svg"
  user={{ name: 'Admin User', role: 'Admin' }}
  pathname={pathname}
  onLogout={logout}
  configHref="/dashboard/settings"  // optional — shows settings gear icon
  configLabel="Settings"
/>

Layout behaviour:

  • Desktop (lg+): Logo | brand name | divider | scrollable pill links (groups with one item → direct pill; groups with multiple items → dropdown button with portaled menu) | profile avatar + dropdown
  • Mobile (< lg): Compact top bar with hamburger → slide-in drawer with collapsible group sections

NavGroup and NavItem types:

type NavItem = {
  label: string
  href: string
  icon?: ReactNode        // pre-rendered JSX, e.g. <LayoutDashboard className="w-4 h-4" />
}

type NavGroup = {
  heading?: string
  groupIcon?: ReactNode   // pre-rendered JSX
  items: NavItem[]
}

Combobox

Free-text input with a filtered suggestion dropdown. The user can type freely (autocomplete) or pick from the filtered list.

import { Combobox } from '@lucifer91299/ui'

const [sport, setSport] = useState('')

<Combobox
  label="Sport"
  value={sport}
  onChange={setSport}
  placeholder="Type or select…"
  options={[
    { value: 'shooting',  label: 'Shooting' },
    { value: 'archery',   label: 'Archery' },
    { value: 'boxing',    label: 'Boxing' },
    { value: 'wrestling', label: 'Wrestling' },
  ]}
  helperText="Type to filter or enter a custom value"
/>

{/* With error */}
<Combobox
  label="Category"
  value={cat}
  onChange={setCat}
  options={options}
  error
  errorText="This field is required"
/>

| Prop | Type | Default | Description | |------|------|---------|-------------| | value | string | — | Current text (free text or selected label) | | onChange | (v: string) => void | — | Called on every keystroke or option click | | options | ComboboxOption[] | — | { value, label, disabled? } | | label | string | — | Field label | | placeholder | string | 'Type or select…' | | | error | boolean | false | Red border | | errorText | string | — | Error message below | | helperText | string | — | Helper text (hidden when errorText is set) | | maxDropdownHeight | number | 260 | Max px height of dropdown list |


ConfirmModal

Opinionated confirm dialog with 4 variants, multi-line message support, optional summary table, and a loading state on the confirm button.

import { ConfirmModal } from '@lucifer91299/ui'

const [open, setOpen] = useState(false)

<Button variant="danger" onClick={() => setOpen(true)}>Delete</Button>

<ConfirmModal
  isOpen={open}
  onClose={() => setOpen(false)}
  onConfirm={async () => { await deleteRecord(); setOpen(false) }}
  variant="danger"         // 'danger' | 'warning' | 'info' | 'success'
  title="Delete record?"
  message={[
    'This action cannot be undone.',
    '• All associated data will be removed.',
  ]}
  confirmText="Delete"
  cancelText="Cancel"
  isLoading={deleting}
/>

{/* With optional summary table */}
<ConfirmModal
  isOpen={open}
  onClose={() => setOpen(false)}
  onConfirm={confirm}
  variant="warning"
  title="Process renewal?"
  message="The following members will be renewed:"
  tableData={{
    headers: ['Member', 'Fee'],
    rows: [['Priya Mehta', '₹2,500'], ['Arjun Sharma', '₹2,500']],
  }}
  confirmText="Proceed"
/>

| Prop | Type | Default | Description | |------|------|---------|-------------| | isOpen | boolean | — | Controls visibility | | onClose | () => void | — | Backdrop click / cancel button | | onConfirm | () => void | — | Confirm button click | | variant | 'danger' \| 'warning' \| 'info' \| 'success' | 'warning' | Icon + confirm button colour | | title | string | — | Modal heading | | message | string \| string[] | — | Body text. Array items separated by lines; items starting with are indented | | tableData | { headers: string[], rows: (string\|number)[][] } | — | Optional summary table below message | | confirmText | string | 'Confirm' | | | cancelText | string | 'Cancel' | | | isLoading | boolean | false | Shows spinner + "Processing…" on confirm button |


AlertModal

Single-button acknowledgment dialog. Use when no cancel action is needed — just an "Okay" to dismiss.

import { AlertModal } from '@lucifer91299/ui'

<AlertModal
  isOpen={alertOpen}
  onClose={() => setAlertOpen(false)}
  variant="error"   // 'error' | 'warning' | 'info' | 'success'
  title="Something went wrong"
  message="The server returned a 500 error. Please try again later."
/>

{/* Multi-line message */}
<AlertModal
  isOpen={alertOpen}
  onClose={() => setAlertOpen(false)}
  variant="success"
  title="Done!"
  message={['Your changes have been saved.', 'An email confirmation has been sent.']}
  okText="Got it"
/>

| Prop | Type | Default | Description | |------|------|---------|-------------| | isOpen | boolean | — | Controls visibility | | onClose | () => void | — | Dismiss / backdrop click | | variant | 'error' \| 'warning' \| 'info' \| 'success' | 'info' | Icon + button colour | | title | string | — | Modal heading | | message | string \| string[] | — | Body text | | okText | string | 'Okay' | Dismiss button label |


DashboardFullPage

Full-bleed gradient surface for add / edit / detail flows. Bleeds edge-to-edge inside the dashboard content area.

import { DashboardFullPage, dashboardFullPageSurfaceClass } from '@lucifer91299/ui'

{/* Component */}
<DashboardFullPage>
  <PageShell title="Add Product" actions={<Button>Save</Button>} />
  {/* form content */}
</DashboardFullPage>

{/* Class string — apply to an existing element */}
<div className={dashboardFullPageSurfaceClass}>
  ...
</div>

The component applies -mx-6 -mt-6 bleed, a subtle bg-gradient-to-b from-[#eceef2] via-[#e6e8ed] to-[#eef0f4] background, and min-h-[calc(100dvh-3.5rem)] so it fills the viewport.


LanguageSwitcher

Generic i18n dropdown. Accepts any list of language options and fires onChange with the selected code. Framework-agnostic — wire it to any i18n library.

import { LanguageSwitcher } from '@lucifer91299/ui'

const [lang, setLang] = useState('en')

<LanguageSwitcher
  options={[
    { code: 'en', label: 'English',  shortLabel: 'EN' },
    { code: 'hi', label: 'हिन्दी',   shortLabel: 'HI', nativeLabel: 'हिन्दी' },
    { code: 'mr', label: 'Marathi',  shortLabel: 'MR', nativeLabel: 'मराठी' },
  ]}
  value={lang}
  onChange={setLang}
/>

{/* Compact icon-only */}
<LanguageSwitcher options={options} value={lang} onChange={setLang} size="sm" />

{/* For dark headers/footers */}
<LanguageSwitcher options={options} value={lang} onChange={setLang} onDark />

{/* Open upwards (footer placement) */}
<LanguageSwitcher options={options} value={lang} onChange={setLang} dropUp />

| Prop | Type | Default | Description | |------|------|---------|-------------| | options | LanguageOption[] | EN + HI | { code, label, shortLabel?, nativeLabel? } | | value | string | — | Active language code | | onChange | (code: string) => void | — | | | size | 'sm' \| 'md' | 'md' | sm = icon-only, md = shortLabel + chevron | | onDark | boolean | false | Lighter text/hover for dark backgrounds | | dropUp | boolean | false | Open menu upward (for footer placement) |


Layout Primitives

Smaller building-block components you can use inside or outside the dashboard layout.

import {
  BrandLogo,
  TricolorBar,
  SocialLinks,
  PoweredBy,
  PageShell,
  PageFooter,
} from '@lucifer91299/ui'

{/* Logo — sizes: 'sm' (32px) | 'md' (48px) | 'lg' (64px) | 'xl' (80px) */}
<BrandLogo src="/brand/logo.svg" alt="My Portal" size="xl" className="rounded-xl" />

{/* One-time left-to-right entrance sweep */}
<TricolorBar animated height={4} />

{/* Continuous infinite shimmer (matches admin frontend style) */}
<TricolorBar shimmer height={3} />

{/* Override colors */}
<TricolorBar colors={['#e11d48', '#ffffff', '#2563eb']} height={3} />

{/* Shimmer with custom colors */}
<TricolorBar shimmer colors={['#e11d48', '#ffffff', '#2563eb']} height={4} />

{/* Social media links row */}
<SocialLinks
  links={[
    { icon: <Instagram className="w-4 h-4" />, href: 'https://instagram.com/...', label: 'Instagram' },
  ]}
  className="gap-4"
/>

{/* "Powered by" fixed badge */}
<PoweredBy logoSrc="/brand/powered-by.svg" text="Powered by" href="https://xyz.com" />

{/* Page title + subtitle + actions area */}
<PageShell
  title="Athletes"
  subtitle="Manage registered athletes"
  actions={<Button>Add Athlete</Button>}
  breadcrumbs={<Breadcrumbs items={[{ label: 'Dashboard', href: '/dashboard' }, { label: 'Athletes' }]} />}
/>

{/* Footer with org name, logo, and social links */}
<PageFooter
  organizationName="My Portal"
  logoSrc="/brand/logo.svg"
  poweredByText="Powered by"
  poweredByLogoSrc="/brand/powered-by.svg"
  poweredByHref="https://xyz.com"
/>

| Component | Key props | |-----------|-----------| | BrandLogo | src, alt, size (sm/md/lg/xl), className, style | | TricolorBar | colors, animated, shimmer, height, className, style | | SocialLinks | links ({ icon, href, label }), className, style | | PoweredBy | logoSrc, text, href, className, style | | PageShell | title, subtitle, actions, controls, breadcrumbs, backButton, className, style | | PageFooter | organizationName, logoSrc, poweredByText, poweredByLogoSrc, poweredByHref, socialLinks, className, style | | Separator | orientation (horizontal/vertical), label, className, style | | Stepper | steps, current, orientation (horizontal/vertical), className, style | | Timeline | items ({ title, description?, timestamp?, icon?, variant? }), className, style |


Auth Hooks

useJwtAuth

Validates an httpOnly cookie JWT by calling /api/auth/user. Auto-redirects on 401.

import { useJwtAuth } from '@lucifer91299/ui'

const { user, authenticated, loading, logout } = useJwtAuth({
  userApiPath:      '/api/auth/user',  // default
  loginPath:        '/login',          // default
  validateInterval: 5 * 60 * 1000,    // default — re-validates every 5 min
})

useMultiRoleAuth

For portals where a user holds multiple roles simultaneously (e.g. coach + judge), each stored in a separate cookie.

import { useMultiRoleAuth } from '@lucifer91299/ui'

const { activeRoles, currentRole, selectRole, loading } = useMultiRoleAuth({
  roles:        ['coach', 'judge'],
  cookiePrefix: 'portal_',
  loginPath:    '/login',
})

useLaravelSessionAuth

import { useLaravelSessionAuth } from '@lucifer91299/ui'

const { user, authenticated, loading, logout } = useLaravelSessionAuth({
  laravelUrl: process.env.NEXT_PUBLIC_LARAVEL_URL!,
  loginPath:  '/login',
})

Auth API routes

src/app/api/auth/login/route.ts

import { SignJWT } from 'jose'
import { NextResponse } from 'next/server'

export async function POST(request: Request) {
  const { email, password } = await request.json()
  if (email !== '[email protected]' || password !== 'password123') {
    return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 })
  }
  const secret = new TextEncoder().encode(process.env.JWT_SECRET ?? 'dev-secret')
  const token = await new SignJWT({ sub: '1', name: 'Admin', role: 'Admin', email })
    .setProtectedHeader({ alg: 'HS256' })
    .setExpirationTime('7d')
    .sign(secret)
  const res = NextResponse.json({ ok: true })
  res.cookies.set('access_token', token, {
    httpOnly: true, secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax', maxAge: 60 * 60 * 24 * 7, path: '/',
  })
  return res
}

src/app/api/auth/user/route.ts

import { jwtVerify } from 'jose'
import { NextResponse } from 'next/server'
import { cookies } from 'next/headers'

export async function GET() {
  const store = await cookies()
  const token = store.get('access_token')?.value
  if (!token) return NextResponse.json({ error: 'Not authenticated' }, { status: 401 })
  try {
    const secret = new TextEncoder().encode(process.env.JWT_SECRET ?? 'dev-secret')
    const { payload } = await jwtVerify(token, secret)
    return NextResponse.json(payload)
  } catch {
    return NextResponse.json({ error: 'Invalid token' }, { status: 401 })
  }
}

src/app/api/auth/logout/route.ts

import { NextResponse } from 'next/server'

export async function POST() {
  const res = NextResponse.json({ ok: true })
  res.cookies.set('access_token', '', { maxAge: 0, path: '/' })
  return res
}

Middleware / proxy.ts

Protect routes at the edge — no backend round-trip. The file is named proxy.ts (Next.js convention).

// src/proxy.ts
import { jwtMiddleware } from '@lucifer91299/ui/server'

export default jwtMiddleware({
  cookieName:     'access_token',
  jwtSecret:      process.env.JWT_SECRET!,
  protectedPaths: ['/dashboard'],
  loginPath:      '/login',
})

export const config = {
  matcher: ['/((?!_next|public|favicon.ico).*)'],
}

Always import from @lucifer91299/ui/server in proxy.ts — that entry uses only Edge Runtime-compatible APIs (no dynamic await import()).


Theming

createTheme — all options

import { createTheme } from '@lucifer91299/ui'

export default createTheme({
  // Brand colours (hex)
  primary:          '#000080',
  accent:           '#FF9933',
  success:          '#138808',

  // Soft + hover overrides (optional — auto-derived if omitted)
  'primary-soft':   'rgba(0, 0, 128, 0.12)',
  'primary-hover':  'rgba(0, 0, 128, 0.9)',
  'accent-soft':    'rgba(255, 153, 51, 0.12)',
  'accent-hover':   'rgba(255, 153, 51, 0.9)',
  'success-soft':   'rgba(19, 136, 8, 0.12)',
  'success-hover':  'rgba(19, 136, 8, 0.9)',

  // Layout
  sidebar:          'full',      // 'full' | 'rail' | 'both' | 'header'
  loginStyle:       'animated',  // 'animated' | 'simple'

  // Identity
  projectName:      'My Portal',
  projectSubtitle:  '',
  logoSrc:          '/brand/logo.svg',
  logoAlt:          'Portal Logo',

  // Footer powered-by (shown on login page)
  poweredByLogoSrc: '/brand/powered-by.svg',
  poweredByText:    'Powered by',
  poweredByHref:    '#',

  // Typography
  fontFamily:       "'Inter', system-ui, sans-serif",

  // Border radius preset
  borderRadius:     'apple',  // 'apple' | 'rounded' | 'sharp'
})

Built-in presets

import { createTheme, builtInThemes } from '@lucifer91299/ui'

createTheme({ ...builtInThemes.dark,    projectName: 'My SaaS' })
createTheme({ ...builtInThemes.minimal, projectName: 'Corp Tool' })

| Preset | Primary | Accent | Success | Sidebar | Login | |--------|---------|--------|---------|---------|-------| | default | #000080 navy | #FF9933 saffron | #138808 green | full | animated | | dark | #6366F1 indigo | #F59E0B amber | #10B981 emerald | rail | simple | | minimal | #0F172A slate | #3B82F6 blue | #22C55E green | both | simple |

Design tokens

Brand (CSS variables):

| Class | Variable | |-------|----------| | bg-primary / text-primary | --primary | | bg-primary-soft | --primary-soft | | bg-accent / text-accent | --accent | | bg-success / text-success | --success |

Surfaces:

| Class | Role | |-------|------| | bg-surface-primary | Page / white | | bg-surface-secondary | Card / panel | | bg-surface-tertiary | Input / divider |

Text:

| Class | Role | |-------|------| | text-label-primary | Main text | | text-label-secondary | Supporting | | text-label-tertiary | Placeholder | | text-label-quaternary | Muted |

Typography:

| Class | Size | |-------|------| | text-display | 32px 700 | | text-title1 | 24px 600 | | text-title2 | 20px 600 | | text-body | 15px 400 | | text-callout | 14px 500 | | text-subhead | 13px 400 | | text-footnote | 12px 400 | | text-caption1 | 11px 400 |

Border radius (borderRadius in theme):

| Class | apple | rounded | sharp | |-------|---------|-----------|---------| | rounded-lg | 16px | 12px | 6px | | rounded-xl | 20px | 14px | 6px | | rounded-2xl | 28px | 16px | 8px |


Server exports

// Only import from this path in proxy.ts (Edge Runtime safe)
import { jwtMiddleware, multiRoleMiddleware } from '@lucifer91299/ui/server'

Local development

cd packages/ui
npm run build

# Patch directly into an app's node_modules (no publish needed)
cp -r dist path/to/my-app/node_modules/@lucifer91299/ui/

# Or scaffold with a local path reference
npx @lucifer91299/create-portal-app my-portal --yes --local-ui=../../packages/ui

GitHub: aakashkanojiya91299/nexportal
CLI: create-portal-app


Changelog

v1.1.54

  • Button — added secondary, gray, plain variants (sales-frontend parity)
  • HeaderNav — exported as a standalone component (import { HeaderNav } from '@lucifer91299/ui')
  • FixgroupIcon rendered as ReactNode correctly (was accidentally typed as component)

v1.1.53

New components (sales frontend parity):

  • PageLoader — full-screen centered loading state with dual-ring spinner + label. Exported alongside LoadingSpinner.
  • LoadingSpinnervariant prop — new variant="dual" (primary outer ring + accent inner ring, reverse spin) and variant="white" (for dark backgrounds). Original single-ring is variant="default".
  • Combobox — free-text input with a portal-positioned filtered suggestion dropdown. Supports label, error, errorText, helperText, disabled, maxDropdownHeight.
  • ConfirmModal — opinionated confirm dialog with danger/warning/info/success variants, multi-line message, optional tableData summary, and isLoading state on confirm button.
  • AlertModal — single-button acknowledgment dialog. Same 4 variants; okText prop.
  • TableSkeleton — configurable rows × cols skeleton table.
  • GridSkeleton — responsive grid of card skeletons.
  • ProfileSkeleton — avatar + info + 6-cell detail grid layout.
  • SettingsSkeleton — sidebar tabs + wide content panel layout.
  • DashboardFullPage — full-bleed gradient surface (-mx-6 -mt-6) for add/edit/detail flows. Also exports `da