@assassin1717/aifelib
v1.5.0
Published
Private UI component library — React + TypeScript + Tailwind + Lucide
Readme
@assassin1717/aifelib
Private UI component library — React + TypeScript + Tailwind CSS + Lucide Icons.
Built to standardize frontend across all internal apps: consistent visuals, mobile-first, accessible, and easy for AI agents to use.
Install
npm install @assassin1717/aifelibPeer dependencies (must be installed in the consuming app):
npm install react react-domTailwind CSS must be configured in the consuming app. Add the library path to your content array so Tailwind scans its classes:
// tailwind.config.js / tailwind.config.ts
content: [
"./src/**/*.{ts,tsx}",
"./node_modules/@assassin1717/aifelib/dist/**/*.js",
]Setup
Wrap your app with ToastProvider (required for useToast to work):
import { ToastProvider } from "@assassin1717/aifelib";
function App() {
return (
<ToastProvider>
<YourApp />
</ToastProvider>
);
}Dark Mode
All components support dark mode via Tailwind's class strategy. Add the dark class to your <html> element (or any wrapper) to activate it:
<!-- entire app in dark mode -->
<html class="dark">// controlled dark mode (e.g. with a toggle)
<div className={isDark ? "dark" : ""}>
<YourApp />
</div>Make sure your Tailwind config also has darkMode: "class":
// tailwind.config.ts
export default {
darkMode: "class",
content: [
"./src/**/*.{ts,tsx}",
"./node_modules/@assassin1717/aifelib/dist/**/*.js",
],
};Components
Phase 1 — Form
Button
import { Button } from "@assassin1717/aifelib";
<Button>Save</Button>
<Button variant="destructive" size="lg">Delete</Button>
<Button loading>Saving…</Button>
<Button fullWidth>Submit</Button>Props:
variant:"primary"|"secondary"|"ghost"|"destructive"|"outline"— default"primary"size:"sm"|"md"|"lg"— default"md"loading:boolean— shows spinner, disables buttonfullWidth:boolean—w-full- All native
<button>props
Input
import { Input } from "@assassin1717/aifelib";
import { Search } from "lucide-react";
<Input placeholder="Email" type="email" />
<Input error placeholder="Invalid value" />
<Input startIcon={<Search size={16} />} placeholder="Search…" />Props:
error:boolean— red border +aria-invalidstartIcon:ReactNode— icon on the leftendIcon:ReactNode— icon on the right- All native
<input>props
Textarea
import { Textarea } from "@assassin1717/aifelib";
<Textarea placeholder="Description" rows={4} />
<Textarea error />Props:
error:boolean- All native
<textarea>props
Select
import { Select } from "@assassin1717/aifelib";
<Select
options={[
{ value: "admin", label: "Admin" },
{ value: "user", label: "User" },
]}
placeholder="Choose role…"
/>Props:
options:{ value: string; label: string; disabled?: boolean }[]placeholder:string— disabled first optionerror:boolean- All native
<select>props
Checkbox
import { Checkbox } from "@assassin1717/aifelib";
<Checkbox label="Accept terms" />
<Checkbox label="Required" error />Props:
label:string— inline label (renders its own<label>)error:boolean- All native
<input type="checkbox">props excepttype
Label
import { Label } from "@assassin1717/aifelib";
<Label htmlFor="email" required>Email</Label>Props:
required:boolean— appends a red*- All native
<label>props
FormField
Composes Label + any input child. Automatically injects id, error, and aria-describedby into the child.
import { FormField, Input } from "@assassin1717/aifelib";
<FormField label="Email" htmlFor="email" required error="Invalid email">
<Input id="email" type="email" />
</FormField>
<FormField label="Bio" htmlFor="bio" hint="Max 200 characters">
<Textarea id="bio" />
</FormField>Props:
label:stringhtmlFor:string— links label to inputrequired:booleanhint:string— helper text shown below input (hidden when error is set)error:string— error message shown below input withrole="alert"
Phase 2 — Feedback & Overlays
Spinner
import { Spinner } from "@assassin1717/aifelib";
<Spinner />
<Spinner size="lg" label="Loading users…" />Props:
size:"sm"|"md"|"lg"— default"md"label:string— aria-label for screen readers — default"Loading…"
Badge
import { Badge } from "@assassin1717/aifelib";
<Badge>Default</Badge>
<Badge variant="success">Active</Badge>
<Badge variant="destructive">Error</Badge>Props:
variant:"default"|"success"|"warning"|"destructive"|"info"|"outline"— default"default"- All native
<span>props
Alert
import { Alert } from "@assassin1717/aifelib";
<Alert variant="success" title="Saved" onDismiss={() => {}}>
Your changes have been saved.
</Alert>Props:
variant:"info"|"success"|"warning"|"destructive"— default"info"title:stringonDismiss:() => void— shows dismiss buttonchildren: message body
Card
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "@assassin1717/aifelib";
<Card>
<CardHeader>
<CardTitle>Users</CardTitle>
<CardDescription>Manage your team members.</CardDescription>
</CardHeader>
<CardContent>Content here</CardContent>
<CardFooter>
<Button>Save</Button>
</CardFooter>
</Card>Card props:
padding:"none"|"sm"|"md"|"lg"— default"md"
Modal
import { Modal, Button } from "@assassin1717/aifelib";
const [open, setOpen] = useState(false);
<Modal
open={open}
onClose={() => setOpen(false)}
title="Edit user"
description="Update the user details below."
size="md"
footer={
<>
<Button variant="ghost" onClick={() => setOpen(false)}>Cancel</Button>
<Button onClick={handleSave}>Save</Button>
</>
}
>
<FormField label="Name" htmlFor="name">
<Input id="name" />
</FormField>
</Modal>Props:
open:booleanonClose:() => void— called on backdrop click and ESC keytitle:stringdescription:stringsize:"sm"|"md"|"lg"|"xl"|"full"— default"md"footer:ReactNode— action buttons slot (rendered inside the modal)children: modal body
Behaviour: focus trap, ESC to close, body scroll lock, bottom sheet on mobile / centered dialog on sm+.
ConfirmDialog
import { ConfirmDialog } from "@assassin1717/aifelib";
<ConfirmDialog
open={open}
onClose={() => setOpen(false)}
onConfirm={handleDelete}
title="Delete user"
description="This action cannot be undone."
variant="destructive"
confirmLabel="Delete"
loading={isDeleting}
/>Props:
open:booleanonClose:() => voidonConfirm:() => voidtitle:stringdescription:stringvariant:"primary"|"destructive"— default"primary"confirmLabel:string— default"Confirm"cancelLabel:string— default"Cancel"loading:boolean
ToastMessage
import { useToast } from "@assassin1717/aifelib";
function MyComponent() {
const { addToast } = useToast();
return (
<Button onClick={() => addToast({ type: "success", title: "Saved", description: "Changes saved." })}>
Save
</Button>
);
}addToast options:
type:"success"|"error"|"warning"|"info"title:stringdescription:stringduration:number— ms before auto-dismiss, default5000. Pass0to disable auto-dismiss.
Requires <ToastProvider> at the root of the app.
Phase 3 — Data Display
Table
Mobile: each row renders as a card with label: value pairs. Desktop: standard table with horizontal scroll.
import {
Table, TableHeader, TableBody, TableRow,
TableHead, TableCell, TableToolbar, TableEmptyState
} from "@assassin1717/aifelib";
<TableToolbar
filters={<Input placeholder="Search…" />}
actions={<Button>Add user</Button>}
/>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Status</TableHead>
<TableHead align="right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.length === 0 ? (
<TableEmptyState colSpan={3} title="No users found" description="Add your first user." />
) : (
users.map(user => (
<TableRow key={user.id}>
<TableCell label="Name">{user.name}</TableCell>
<TableCell label="Status"><Badge variant="success">Active</Badge></TableCell>
<TableCell label="Actions" align="right"><Button size="sm">Edit</Button></TableCell>
</TableRow>
))
)}
</TableBody>
</Table>Important: Always pass label to TableCell — it's the column header shown on mobile cards.
TableHead / TableCell props:
align:"left"|"center"|"right"— default"left"
TableCell extra props:
label:string— column name shown on mobile
TableEmptyState props:
colSpan:number— must match number of columnstitle:stringdescription:stringicon:ReactNodeaction:ReactNode— action button
TableToolbar props:
filters:ReactNode— left slot (search, selects)actions:ReactNode— right slot (buttons)
PageHeader
import { PageHeader, Button } from "@assassin1717/aifelib";
<PageHeader
title="Users"
description="Manage your team members."
actions={
<>
<Button variant="outline">Export</Button>
<Button>Add user</Button>
</>
}
/>Props:
title:stringdescription:stringprefix:ReactNode— breadcrumb or back linkactions:ReactNode— right slot, stacks on mobile
EmptyState
import { EmptyState, Button } from "@assassin1717/aifelib";
import { Users } from "lucide-react";
<EmptyState
icon={<Users size={48} />}
title="No users yet"
description="Add your first team member to get started."
action={<Button>Add user</Button>}
secondaryAction={<Button variant="ghost">Learn more</Button>}
/>Props:
icon:ReactNodetitle:stringdescription:stringaction:ReactNode— primary buttonsecondaryAction:ReactNode— secondary button
Phase 4 — Navigation & Extras
Tabs
import { Tabs, TabPanel } from "@assassin1717/aifelib";
const [tab, setTab] = useState("overview");
<Tabs
tabs={[
{ value: "overview", label: "Overview" },
{ value: "members", label: "Members" },
{ value: "settings", label: "Settings", disabled: true },
]}
value={tab}
onChange={setTab}
>
<TabPanel value="overview" activeValue={tab}>Overview content</TabPanel>
<TabPanel value="members" activeValue={tab}>Members content</TabPanel>
</Tabs>Tabs props:
tabs:{ value: string; label: string; disabled?: boolean }[]value:string— controlled active tabonChange:(value: string) => void
TabPanel props:
value:string— this panel's tab valueactiveValue:string— currently active tab value
Behaviour: horizontal scroll when tabs overflow on mobile, arrow key navigation.
DropdownMenu
import { DropdownMenu, Button } from "@assassin1717/aifelib";
import { Edit, Trash2, MoreHorizontal } from "lucide-react";
<DropdownMenu
trigger={<Button variant="ghost" size="sm"><MoreHorizontal size={16} /></Button>}
align="right"
items={[
{ label: "Edit", icon: <Edit size={14} />, onClick: handleEdit },
{ label: "Delete", icon: <Trash2 size={14} />, destructive: true, divider: true, onClick: handleDelete },
]}
/>Props:
trigger:ReactNode— the button that opens the menuitems:DropdownMenuItem[]align:"left"|"right"— default"right"
DropdownMenuItem:
label:stringonClick:() => voidicon:ReactNodedisabled:booleandestructive:boolean— red stylingdivider:boolean— visual separator above item
Behaviour: closes on outside click, ESC, or item selection. Focus returns to trigger on ESC.
Pagination
import { Pagination } from "@assassin1717/aifelib";
const [page, setPage] = useState(1);
<Pagination page={page} totalPages={20} onChange={setPage} />Props:
page:numbertotalPages:numberonChange:(page: number) => voidsiblingCount:number— page buttons around current, default1
Mobile: shows "X / Y" compact label instead of page buttons. Returns null when totalPages <= 1.
Drawer
import { Drawer, Button } from "@assassin1717/aifelib";
<Drawer
open={open}
onClose={() => setOpen(false)}
title="Filters"
side="right"
footer={
<>
<Button variant="ghost" onClick={() => setOpen(false)}>Cancel</Button>
<Button onClick={applyFilters}>Apply</Button>
</>
}
>
<FormField label="Status" htmlFor="status">
<Select id="status" options={statusOptions} />
</FormField>
</Drawer>Props:
open:booleanonClose:() => voidtitle:stringdescription:stringside:"right"|"left"— default"right"widthClass:string— Tailwind width class for desktop, default"sm:w-96"footer:ReactNode— action buttons slot
Behaviour: same as Modal (focus trap, ESC, scroll lock). Bottom sheet on mobile, side panel on sm+.
Tooltip
import { Tooltip, Button } from "@assassin1717/aifelib";
<Tooltip content="Save your changes" side="top">
<Button>Save</Button>
</Tooltip>Props:
content:stringside:"top"|"bottom"|"left"|"right"— default"top"children: singleReactElement— must acceptonMouseEnter,onMouseLeave,onFocus,onBlur
Behaviour: visible on hover and keyboard focus.
Phase 5 — App Shell
AppShell
Full-page layout with fixed sidebar (desktop) + Drawer sidebar (mobile).
import { AppShell } from "@assassin1717/aifelib";
import { LayoutDashboard, Users, Settings } from "lucide-react";
<AppShell
sidebar={{
logo: <span className="font-bold text-blue-600">MyApp</span>,
groups: [
{
items: [
{ label: "Dashboard", href: "/", icon: <LayoutDashboard size={18} />, active: true },
{ label: "Users", href: "/users", icon: <Users size={18} /> },
],
},
{
title: "Config",
items: [
{ label: "Settings", href: "/settings", icon: <Settings size={18} /> },
],
},
],
footer: <div className="text-sm text-gray-500">v1.0.0</div>,
}}
topbar={{ title: "Dashboard" }}
>
<PageHeader title="Dashboard" />
{/* page content */}
</AppShell>AppShell props:
sidebar:SidebarProps— see Sidebar belowtopbar:TopbarProps(withoutonMenuOpen) — omit to hide topbar entirelychildren: page content rendered in the scrollable main area
Sidebar
Can be used standalone (e.g. inside a custom layout).
import { Sidebar } from "@assassin1717/aifelib";
<Sidebar
logo={<img src="/logo.svg" alt="MyApp" className="h-8" />}
groups={[
{
items: [
{ label: "Dashboard", href: "/", active: true },
{ label: "Users", href: "/users", badge: 3 },
{ label: "Archived", href: "/archived", disabled: true },
],
},
]}
footer={<Button variant="ghost" fullWidth>Logout</Button>}
/>SidebarNavItem:
label:stringhref:string— renders as<a>, omit to render as<button>icon:ReactNodeactive:boolean— highlights item, setsaria-current="page"disabled:booleanonClick:() => voidbadge:string | number— shown on the right
SidebarNavGroup:
title:string— optional group headingitems:SidebarNavItem[]
Topbar
import { Topbar } from "@assassin1717/aifelib";
<Topbar
onMenuOpen={() => setSidebarOpen(true)}
title="Users"
actions={<Avatar name="Tiago" />}
/>Props:
onMenuOpen:() => void— hamburger button handler (button hidden onsm+)title:ReactNodeactions:ReactNode— right slothideMenuButton:boolean— hides the hamburger
Phase 5 — Extras (continued)
StatsCard
Metric card with label, value and optional trend indicator.
import { StatsCard } from "@assassin1717/aifelib";
import { Users } from "lucide-react";
<StatsCard
label="Total Users"
value="1,284"
trend={{ direction: "up", value: "+12%" }}
icon={<Users size={20} />}
/>Props:
label:stringvalue:string | numbertrend:{ direction: "up" | "down" | "neutral"; value: string }— optional trend badgeicon:ReactNode— optional icon in the top-right cornersize:"sm"|"md"— default"md"
Grid
Responsive CSS grid wrapper.
import { Grid } from "@assassin1717/aifelib";
<Grid cols={3} gap="md">
<StatsCard label="Users" value="1,284" />
<StatsCard label="Revenue" value="$42k" />
<StatsCard label="Uptime" value="99.9%" />
</Grid>Props:
cols:1|2|3|4|6|12— default1gap:"none"|"sm"|"md"|"lg"— default"md"responsive:{ sm?: GridCols; md?: GridCols; lg?: GridCols }— breakpoint overrides
New Components
Skeleton
Pulsing placeholder for loading states. Prevents layout shift while content loads.
import { Skeleton, SkeletonCard } from "@assassin1717/aifelib";
// custom shapes
<Skeleton className="h-4 w-48" />
<Skeleton className="h-32 w-full" />
// pre-built card preset
<SkeletonCard />
<SkeletonCard className="w-80" />Skeleton props:
className— Tailwind width/height classes (required to give it dimensions)animate:boolean— pulse animation, defaulttrue
SkeletonCard props:
className— additional Tailwind classes
SectionHeader
Title + description + actions slot for sections within a page. Distinct from PageHeader which is used at page level.
import { SectionHeader, Button } from "@assassin1717/aifelib";
<SectionHeader
title="Recent Ideas"
description="Ideas submitted in the last 7 days."
actions={<Button size="sm">View all</Button>}
/>Props:
title:stringdescription:stringactions:ReactNode— right slotclassName:string
DateInput
Native <input type="date"> with consistent styling matching Input. Supports dark mode date picker icon via color-scheme.
import { DateInput } from "@assassin1717/aifelib";
const [date, setDate] = useState("");
<DateInput value={date} onChange={setDate} />
<DateInput value={date} onChange={setDate} error min="2024-01-01" max="2026-12-31" />Props:
value:string— ISO date string (YYYY-MM-DD)onChange:(value: string) => voiderror:boolean— red bordermin/max:string— ISO date bounds- All native
<input type="date">props excepttypeandonChange
Compose with FormField for labels and error messages:
<FormField label="Retention date" htmlFor="ret" error={errors.date}>
<DateInput id="ret" value={date} onChange={setDate} error={!!errors.date} />
</FormField>FileUpload
Drag-and-drop file input with click-to-upload fallback.
import { FileUpload } from "@assassin1717/aifelib";
<FileUpload
accept=".pdf,.jpg,.png"
multiple
onFiles={(files) => handleUpload(files)}
hint="PDF, JPG or PNG up to 10MB"
error={uploadError}
loading={isUploading}
/>Props:
onFiles:(files: File[]) => void— called with selected/dropped filesaccept:string— MIME types or extensions (e.g.".pdf,.jpg")multiple:boolean— allow selecting multiple fileshint:string— helper text shown inside the drop zoneerror:string— error message shown below the drop zoneloading:boolean— shows "Uploading…" state, disables interactiondisabled:booleanclassName:string
Slider
Range input with styled track, thumb, and optional value display.
import { Slider } from "@assassin1717/aifelib";
const [volume, setVolume] = useState(40);
<Slider value={volume} onChange={setVolume} />
<Slider
label="Price range"
value={price}
onChange={setPrice}
min={0}
max={1000}
step={10}
showValue
formatValue={(v) => `$${v}`}
hint="Set your maximum budget"
/>Props:
value:number— controlled valueonChange:(value: number) => voidmin:number— default0max:number— default100step:number— default1showValue:boolean— shows current value on the right of the labelformatValue:(value: number) => string— custom value formatter, defaultStringlabel:stringhint:string— helper texterror:string— error messagedisabled:boolean
Utilities
cn
Merges Tailwind classes safely (clsx + tailwind-merge).
import { cn } from "@assassin1717/aifelib";
<div className={cn("px-4 py-2", isActive && "bg-blue-50", className)} />Development
# Install dependencies
npm install
# Run Storybook (component explorer)
npm run dev
# Run tests
npm test
npm run test:watch
# Type check
npm run type-check
# Lint
npm run lint
# Build library
npm run buildRelease process
- Work on a feature branch (
dev/<name>) - Merge to
staging— pipeline runs lint + type-check + test + build - Bump version in
package.json(npm version patch|minor|major) - Merge
staging→main— pipeline publishes to npm automatically
CI/CD
Bitbucket Pipelines (bitbucket-pipelines.yml):
| Branch | Steps |
|---|---|
| Any push | install → lint + type-check + test (parallel) → build |
| staging | Same as above |
| main | Above + publish to npm |
Required pipeline variable (set in Bitbucket repository Settings → Pipelines → Variables):
NPM_TOKEN— npm access token with read+write on@assassin1717scope. Mark as Secured.
Design decisions
- No CSS-in-JS — Tailwind only. No runtime style injection.
- No headless UI library — all components are hand-rolled to keep the bundle small and predictable.
- Composition over configuration —
FormFieldinjects props into children viacloneElement.Modal/Drawer/ConfirmDialogdefine their own action buttons internally — callers pass callbacks, not buttons. - Mobile-first — touch targets minimum 44px, bottom sheets on mobile, horizontal scroll on tables replaced by card layout.
- Accessibility —
aria-invalid,aria-describedby,aria-current,role="alert",role="status", focus traps in overlays, keyboard navigation in Tabs and DropdownMenu. exactOptionalPropertyTypes: true— optional props are spread conditionally, never passed asundefinedexplicitly.
