Components
Complete reference for all @elcto/ui components.
ApiHealthBanner
Display API health status with automatic health checks and exponential backoff.
import {
ApiHealthBanner,
useApiHealth,
useShowApiHealthBanner,
} from "@elcto/ui/components";
function App() {
const { state: apiHealth, refresh: refreshApiHealth } = useApiHealth();
const showBanner = useShowApiHealthBanner(apiHealth);
const labels = {
unreachable: "Unable to connect to the API server.",
serviceIssues: "Service issues:",
database: "Database",
redis: "Redis",
websocket: "WebSocket",
checking: "Checking...",
refreshNow: "Click to refresh now",
};
return (
<div>
<ApiHealthBanner
apiHealth={apiHealth}
show={showBanner}
labels={labels}
fixedTop="4rem"
sidebarOffset="16rem"
onRefresh={refreshApiHealth}
/>
{/* Your app content */}
</div>
);
}
ApiHealthBanner Props:
| Prop | Type | Default | Description |
|---|---|---|---|
apiHealth | ApiHealthState | required | Health state from useApiHealth |
show | boolean | required | Whether to show the banner |
labels | ApiHealthBannerLabels | required | Translated label strings |
fixedTop | string | '0' | Fixed position top offset |
sidebarOffset | string | - | Sidebar offset for positioning |
onRefresh | () => void | - | Manual refresh callback |
useApiHealth Options:
const { state, refresh } = useApiHealth({
baseInterval: 30000, // 30 seconds
maxInterval: 600000, // 10 minutes (max with backoff)
apiUrl: process.env.NEXT_PUBLIC_API_URL,
});
| Option | Type | Default | Description |
|---|---|---|---|
baseInterval | number | 30000 | Base check interval (ms) |
maxInterval | number | 600000 | Max interval with backoff (ms) |
apiUrl | string | NEXT_PUBLIC_API_URL | API URL to check |
ApiHealthState:
interface ApiHealthState {
status: "healthy" | "unhealthy" | "unreachable";
services: {
database: { status: string };
redis: { status: string };
websocket: { status: string };
};
nextCheckIn: number | null; // seconds until next check
isChecking: boolean;
}
ApiHealthBannerLabels:
interface ApiHealthBannerLabels {
unreachable: string;
serviceIssues: string;
database: string;
redis: string;
websocket: string;
checking: string;
refreshNow: string;
}
AuditIcon
Renders appropriate icons for audit events based on event type.
import {
AuditIcon,
AuditIconElements,
getAuditIconComponent,
getIconByName,
renderAuditIcon,
renderIconByName,
} from "@elcto/ui/components";
// Component usage
<AuditIcon eventType="login" className="w-4 h-4" />
<AuditIcon eventType="2fa_enabled" className="w-5 h-5 text-amber-500" />
// Pre-built size variants
{AuditIconElements.sm("login")} // w-3 h-3
{AuditIconElements.md("login")} // w-4 h-4
{AuditIconElements.lg("login")} // w-5 h-5
// Function usage
const Icon = getAuditIconComponent("login");
<Icon className="w-4 h-4" />
// Render helper
{renderAuditIcon("2fa_enabled", "w-5 h-5")}
AuditIcon Props:
| Prop | Type | Default | Description |
|---|---|---|---|
eventType | string | required | Audit event type (e.g., "login", "2fa_enabled") |
className | string | "w-4 h-4" | CSS classes for sizing and color |
Exported Functions:
| Function | Description |
|---|---|
getAuditIconComponent(eventType) | Returns the Lucide icon component for an event type |
getIconByName(iconName) | Returns icon component by icon name |
renderAuditIcon(eventType, className) | Renders icon as React element |
renderIconByName(iconName, className) | Renders icon by name as React element |
AuditIconElements:
| Key | Size |
|---|---|
sm | w-3 h-3 |
md | w-4 h-4 |
lg | w-5 h-5 |
Alert
Display status messages with semantic styling.
import { Alert } from "@elcto/ui/components";
<Alert variant="success" title="Success!">
Your changes have been saved.
</Alert>
<Alert variant="error" title="Error" onClose={() => setShow(false)}>
Something went wrong. Please try again.
</Alert>
<Alert variant="warning">
This action cannot be undone.
</Alert>
<Alert variant="info">
Your session will expire in 5 minutes.
</Alert>
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'error' | 'warning' | 'success' | 'info' | required | Alert style |
children | ReactNode | required | Alert content |
title | string | - | Optional title |
onClose | () => void | - | Close callback (shows X button) |
className | string | - | Additional CSS classes |
AppSwitcher
Navigation dropdown for switching between apps.
import { AppSwitcher, LayoutGridIcon } from "@elcto/ui/components";
import { Settings, User, FileText, Shield } from "lucide-react";
const apps = [
{
key: "backend",
label: "Backend",
description: "Admin console",
icon: <Settings className="w-5 h-5" />,
url: "https://console.elcto.com",
current: true,
},
{
key: "id",
label: "ID",
description: "Account settings",
icon: <User className="w-5 h-5" />,
url: "https://id.elcto.com",
},
{
key: "docs",
label: "Docs",
icon: <FileText className="w-5 h-5" />,
url: "https://docs.elcto.com",
},
{
key: "policies",
label: "Policies",
icon: <Shield className="w-5 h-5" />,
url: "https://policies.elcto.com",
},
];
<AppSwitcher apps={apps} headerLabel="Apps" columns={2} />
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
apps | AppItem[] | required | List of apps to display |
headerLabel | string | 'Apps' | Header text |
columns | 2 | 3 | 4 | 3 | Grid columns |
className | string | - | Additional CSS classes |
AppItem:
| Prop | Type | Description |
|---|---|---|
key | string | Unique identifier |
label | string | Display name |
description | string | Optional description |
icon | ReactNode | Icon element |
url | string | Navigation URL |
current | boolean | Whether this is the current app |
Avatar
User avatar with image fallback to initials.
import { Avatar } from "@elcto/ui/components";
<Avatar src="/user.jpg" name="John Doe" size={40} />
<Avatar name="Jane Smith" size={32} />
<Avatar size={48} /> // Shows default icon
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
src | string | null | - | Image URL |
name | string | null | - | User name (for initials) |
size | number | 40 | Size in pixels |
className | string | - | Additional CSS classes |
Badge
Status badges and labels.
import { Badge } from "@elcto/ui/components";
<Badge variant="success">Active</Badge>
<Badge variant="warning">Pending</Badge>
<Badge variant="error">Failed</Badge>
<Badge variant="info">New</Badge>
<Badge variant="neutral">Draft</Badge>
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'success' | 'warning' | 'error' | 'info' | 'neutral' | 'neutral' | Badge style |
children | ReactNode | required | Badge content |
className | string | - | Additional CSS classes |
Banner
Full-width notification banners.
import { Banner } from "@elcto/ui/components";
<Banner variant="warning" onClose={() => setShow(false)}>
System maintenance scheduled for tonight.
</Banner>
<Banner variant="error">
Connection lost. Reconnecting...
</Banner>
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'info' | 'warning' | 'error' | 'success' | 'info' | Banner style |
children | ReactNode | required | Banner content |
onClose | () => void | - | Close callback |
className | string | - | Additional CSS classes |
BlurredText
Privacy-aware text display with blur effect.
import { BlurredText } from "@elcto/ui/components";
// Controlled component
<BlurredText
text="secret-api-key-12345"
isBlurred={isPrivacyMode}
onToggle={() => setIsPrivacyMode(!isPrivacyMode)}
blurIntensity="md"
/>
// Without toggle button
<BlurredText
text="hidden-value"
isBlurred={true}
showToggle={false}
/>
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
text | string | required | Text to display |
isBlurred | boolean | required | Whether text is blurred |
onToggle | () => void | - | Toggle callback |
showToggle | boolean | true | Show toggle button |
blurIntensity | 'xs' | 'sm' | 'md' | 'lg' | 'md' | Blur intensity |
className | string | - | Additional CSS classes |
Button
Interactive buttons with variants and states.
import { Button } from "@elcto/ui/components";
import { Plus, ArrowRight } from "lucide-react";
// Variants
<Button variant="brand">Primary Action</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="danger">Delete</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="outline">Outline</Button>
// Sizes
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
// With icons
<Button leftIcon={<Plus className="w-4 h-4" />}>Add Item</Button>
<Button rightIcon={<ArrowRight className="w-4 h-4" />}>Next</Button>
// Loading state
<Button isLoading>Saving...</Button>
// Disabled
<Button disabled>Disabled</Button>
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'brand' | 'secondary' | 'danger' | 'ghost' | 'outline' | 'brand' | Button style |
size | 'sm' | 'md' | 'lg' | 'md' | Button size |
isLoading | boolean | false | Loading state |
leftIcon | ReactNode | - | Icon before text |
rightIcon | ReactNode | - | Icon after text |
disabled | boolean | false | Disabled state |
Also accepts all standard <button> HTML attributes.
Card
Content container components.
import {
Card,
CardHeader,
CardContent,
CardFooter,
ActionCard,
StatusCard,
} from "@elcto/ui/components";
// Basic card
<Card>
<CardHeader>
<h3>Card Title</h3>
</CardHeader>
<CardContent>
Card content goes here.
</CardContent>
<CardFooter>
<Button>Action</Button>
</CardFooter>
</Card>
// Action card
<ActionCard
title="Quick Action"
description="Click to perform action"
onClick={() => handleAction()}
/>
// Status card
<StatusCard
variant="success"
title="Connected"
description="All services operational"
/>
Card Props:
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | required | Card content |
className | string | - | Additional CSS classes |
variant | 'default' | 'flat' | 'default' | Card style |
ActionCard Props:
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | required | Card title |
description | string | - | Card description |
icon | ReactNode | - | Icon element |
onClick | () => void | - | Click handler |
StatusCard Props:
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'success' | 'warning' | 'error' | 'info' | 'info' | Status style |
title | string | required | Card title |
description | string | - | Card description |
Checkbox
Styled checkbox input with label and description support.
import { Checkbox } from "@elcto/ui/components";
// Basic checkbox
<Checkbox
label="Accept terms and conditions"
checked={accepted}
onChange={(e) => setAccepted(e.target.checked)}
/>
// With description
<Checkbox
label="Email notifications"
description="Receive email updates about your account"
checked={emailEnabled}
onChange={(e) => setEmailEnabled(e.target.checked)}
/>
// Size variants
<Checkbox label="Small" size="sm" />
<Checkbox label="Medium" size="md" />
<Checkbox label="Large" size="lg" />
// Error state
<Checkbox
label="Required field"
error
errorMessage="You must accept the terms"
/>
// Disabled
<Checkbox label="Disabled option" disabled />
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
label | ReactNode | - | Label text |
description | string | - | Description text below label |
size | 'sm' | 'md' | 'lg' | 'md' | Size variant |
error | boolean | false | Error state |
errorMessage | string | - | Error message (also sets error state) |
wrapperClassName | string | - | Class for the wrapper div |
Also accepts all standard <input type="checkbox"> HTML attributes.
CodeInput
OTP/verification code input with auto-focus.
import { CodeInput } from "@elcto/ui/components";
<CodeInput
length={6}
value={code}
onChange={setCode}
onComplete={(code) => verifyCode(code)}
/>
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
length | number | 6 | Number of digits |
value | string | - | Controlled value |
onChange | (value: string) => void | - | Change handler |
onComplete | (code: string) => void | - | Called when all digits entered |
error | boolean | false | Error state |
disabled | boolean | false | Disabled state |
Dropdown
Accessible dropdown menu with animations.
import {
Dropdown,
DropdownTrigger,
DropdownMenu,
DropdownItem,
DropdownDivider,
DropdownHeader,
DropdownLabel,
} from "@elcto/ui/components";
import { Settings, LogOut, User } from "lucide-react";
<Dropdown>
<DropdownTrigger>
Open Menu
</DropdownTrigger>
<DropdownMenu align="right" width={200}>
<DropdownHeader>
<p className="font-medium">John Doe</p>
<p className="text-sm text-muted">john@example.com</p>
</DropdownHeader>
<DropdownDivider />
<DropdownLabel>Account</DropdownLabel>
<DropdownItem icon={<User className="w-4 h-4" />} as="a" href="/profile">
Profile
</DropdownItem>
<DropdownItem icon={<Settings className="w-4 h-4" />}>
Settings
</DropdownItem>
<DropdownDivider />
<DropdownItem icon={<LogOut className="w-4 h-4" />} variant="danger">
Sign Out
</DropdownItem>
</DropdownMenu>
</Dropdown>
Dropdown Props:
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | required | Trigger and menu |
open | boolean | - | Controlled open state |
onOpenChange | (open: boolean) => void | - | Open state callback |
DropdownTrigger Props:
| Prop | Type | Default | Description |
|---|---|---|---|
showChevron | boolean | true | Show chevron icon |
Also accepts all standard <button> HTML attributes.
DropdownMenu Props:
| Prop | Type | Default | Description |
|---|---|---|---|
align | 'left' | 'right' | 'right' | Menu alignment |
width | number | string | 224 | Menu width |
DropdownItem Props:
| Prop | Type | Default | Description |
|---|---|---|---|
icon | ReactNode | - | Item icon |
as | 'button' | 'a' | 'button' | Element type |
href | string | - | Link href (when as="a") |
variant | 'default' | 'danger' | 'default' | Item style |
closeOnClick | boolean | true | Close menu on click |
DateTimePicker
Date, time, or datetime picker with calendar dropdown.
import { DateTimePicker } from "@elcto/ui/components";
// Date only
<DateTimePicker
mode="date"
value={date}
onChange={setDate}
placeholder="Select date..."
/>
// Date and time
<DateTimePicker
mode="datetime"
value={datetime}
onChange={setDatetime}
placeholder="Select date and time..."
/>
// Time only
<DateTimePicker
mode="time"
value={time}
onChange={setTime}
placeholder="Select time..."
/>
// With min/max constraints
<DateTimePicker
mode="date"
value={date}
onChange={setDate}
min={new Date("2024-01-01")}
max={new Date("2024-12-31")}
/>
// Custom labels for i18n
<DateTimePicker
mode="datetime"
value={date}
onChange={setDate}
labels={{
days: ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"],
months: ["Januar", "Februar", "März", ...],
hour: "Stunde",
minute: "Minute",
apply: "Anwenden",
selectDate: "Datum wählen...",
}}
/>
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | Date | null | - | Current value (ISO 8601 string or Date) |
onChange | (value: string | null) => void | - | Change callback |
mode | 'date' | 'datetime' | 'time' | 'date' | Picker mode |
placeholder | string | - | Placeholder text |
size | 'sm' | 'md' | 'lg' | 'md' | Size variant |
error | boolean | false | Error state |
disabled | boolean | false | Disabled state |
min | string | Date | - | Minimum date |
max | string | Date | - | Maximum date |
clearable | boolean | true | Show clear button |
locale | string | Browser locale | Locale for formatting |
name | string | - | Name for form submission |
leftIcon | ReactNode | - | Custom left icon |
dropdownAlign | 'left' | 'right' | 'left' | Dropdown alignment |
labels | DateTimePickerLabels | - | Labels for translations |
DateTimePickerLabels:
| Key | Type | Description |
|---|---|---|
days | [string x 7] | Day abbreviations (Su, Mo, Tu...) |
months | [string x 12] | Month names |
hour | string | Hour label |
minute | string | Minute label |
apply | string | Apply button text |
selectDate | string | Date placeholder |
selectTime | string | Time placeholder |
FloatingInput
Input with floating label animation.
import { FloatingInput, FloatingTextarea } from "@elcto/ui/components";
import { Mail, Lock } from "lucide-react";
<FloatingInput
label="Email Address"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
icon={<Mail className="w-4 h-4" />}
error={errors.email}
/>
<FloatingInput
label="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
icon={<Lock className="w-4 h-4" />}
/>
<FloatingTextarea
label="Description"
value={description}
onChange={(e) => setDescription(e.target.value)}
rows={4}
/>
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | required | Input label |
icon | ReactNode | - | Left icon |
error | string | - | Error message |
value | string | - | Input value |
onChange | (e) => void | - | Change handler |
Also accepts all standard <input> HTML attributes.
Input
Basic text input with icon support and error states.
import { Input } from "@elcto/ui/components";
import { Search, Mail, Lock } from "lucide-react";
// Basic input
<Input
placeholder="Enter your name..."
value={name}
onChange={(e) => setName(e.target.value)}
/>
// With left icon
<Input
leftIcon={<Search className="w-4 h-4" />}
placeholder="Search..."
/>
// With right icon
<Input
rightIcon={<Mail className="w-4 h-4" />}
placeholder="Email address..."
/>
// Size variants
<Input placeholder="Small" inputSize="sm" />
<Input placeholder="Medium" inputSize="md" />
<Input placeholder="Large" inputSize="lg" />
// Error state
<Input
error
errorMessage="This field is required"
placeholder="Required field"
/>
// Disabled
<Input placeholder="Disabled" disabled />
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
leftIcon | ReactNode | - | Left icon element |
rightIcon | ReactNode | - | Right icon element |
inputSize | 'sm' | 'md' | 'lg' | 'md' | Size variant |
error | boolean | false | Error state |
errorMessage | string | - | Error message (also sets error state) |
Also accepts all standard <input> HTML attributes.
LanguageSwitcher
Locale selection dropdown with flag icons.
import { LanguageSwitcher, GBFlag, DEFlag } from "@elcto/ui/components";
import { useTranslations } from "next-intl";
const t = useTranslations("common");
// Basic usage
<LanguageSwitcher
locale={locale}
onLocaleChange={(newLocale) => router.push(pathname, { locale: newLocale })}
/>
// Custom languages and translations
const languages = [
{ code: "en", nativeLabel: "English", Flag: GBFlag },
{ code: "de", nativeLabel: "Deutsch", Flag: DEFlag },
];
<LanguageSwitcher
locale={locale}
onLocaleChange={handleLocaleChange}
languages={languages}
headerLabel={t("language")}
getLanguageLabel={(code) => t(`languages.${code}`)}
/>
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
locale | string | required | Current locale code |
onLocaleChange | (locale: string) => void | required | Locale change callback |
languages | Language[] | [en, de] | Available languages |
headerLabel | string | 'Language' | Header text |
getLanguageLabel | (code: string) => string | - | Translation function |
className | string | - | Additional CSS classes |
Language:
| Prop | Type | Description |
|---|---|---|
code | string | Locale code (e.g., "en", "de") |
nativeLabel | string | Native language name |
Flag | React.ComponentType | Flag icon component |
Exported Icons:
GBFlag- British flagDEFlag- German flagGlobeIcon- Globe icon
LoadingSpinner & Spinner
Loading indicator components.
import { LoadingSpinner, Spinner } from "@elcto/ui/components";
// LoadingSpinner - Branded loading with logo animation
<LoadingSpinner />
<LoadingSpinner size="sm" />
<LoadingSpinner size="lg" />
// Full page loading
<div className="flex items-center justify-center min-h-screen">
<LoadingSpinner size="lg" />
</div>
// Spinner - Simple animated spinner
<Spinner />
<Spinner size="sm" />
<Spinner size="lg" />
<Spinner className="text-brand" />
LoadingSpinner Props:
| Prop | Type | Default | Description |
|---|---|---|---|
size | 'sm' | 'md' | 'lg' | 'md' | Spinner size |
className | string | - | Additional CSS classes |
Spinner Props:
| Prop | Type | Default | Description |
|---|---|---|---|
size | 'xs' | 'sm' | 'md' | 'lg' | 'md' | Spinner size |
className | string | - | Additional CSS classes |
Size Reference:
| Size | LoadingSpinner | Spinner |
|---|---|---|
xs | - | w-3 h-3 |
sm | small | w-4 h-4 |
md | medium | w-5 h-5 |
lg | large | w-6 h-6 |
Modal
Dialog windows with composable structure.
import {
Modal,
ModalHeader,
ModalBody,
ModalFooter,
InfoModal,
ConfirmModal,
} from "@elcto/ui/components";
// Basic modal
<Modal isOpen={isOpen} onClose={() => setIsOpen(false)} title="Edit Profile">
<ModalBody>
<p>Modal content goes here.</p>
</ModalBody>
<ModalFooter>
<Button variant="secondary" onClick={() => setIsOpen(false)}>Cancel</Button>
<Button onClick={handleSave}>Save</Button>
</ModalFooter>
</Modal>
// Info modal
<InfoModal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
title="Success!"
description="Your changes have been saved."
icon={<CheckCircle className="w-8 h-8 text-brand" />}
buttonText="Done"
/>
// Confirm modal
<ConfirmModal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
onConfirm={handleDelete}
title="Delete Item"
message="Are you sure you want to delete this item? This action cannot be undone."
confirmText="Delete"
cancelText="Cancel"
variant="danger"
icon={<Trash2 className="w-6 h-6 text-[var(--error)]" />}
/>
Modal Props:
| Prop | Type | Default | Description |
|---|---|---|---|
isOpen | boolean | required | Open state |
onClose | () => void | required | Close callback |
title | string | - | Modal title |
icon | ReactNode | - | Header icon |
maxWidth | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'md' | Max width |
showCloseButton | boolean | true | Show X button |
closeOnBackdrop | boolean | true | Close on backdrop click |
closeOnEscape | boolean | true | Close on Escape key |
ConfirmModal Props:
| Prop | Type | Default | Description |
|---|---|---|---|
isOpen | boolean | required | Open state |
onClose | () => void | required | Close callback |
onConfirm | () => void | required | Confirm callback |
title | string | required | Modal title |
message | ReactNode | required | Confirmation message |
confirmText | string | 'Confirm' | Confirm button text |
cancelText | string | 'Cancel' | Cancel button text |
variant | 'danger' | 'warning' | 'info' | 'default' | 'default' | Button style |
isLoading | boolean | false | Loading state |
Table
Comprehensive data table system with container, loading states, pagination, and list layout support.
Basic Table
import {
Table,
TableHeader,
TableBody,
TableRow,
TableHead,
TableCell,
} from "@elcto/ui/components";
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map((user) => (
<TableRow key={user.id}>
<TableCell>{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>
<Badge variant={user.active ? "success" : "neutral"}>
{user.active ? "Active" : "Inactive"}
</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
Full-Featured Table with Container
import {
TableContainer,
TableHeaderBar,
TableFooterBar,
TableWrapper,
Table,
TableHeader,
TableBody,
TableRow,
TableHead,
TableCell,
TableEmpty,
TableCenteredLoading,
TablePagination,
} from "@elcto/ui/components";
import { History, Loader2, ChevronLeft, ChevronRight, ArrowUp, ArrowDown, ArrowUpDown } from "lucide-react";
<TableContainer>
{/* Header with title */}
<TableHeaderBar>
<div className="flex items-center gap-3">
<History className="w-5 h-5 text-brand" />
<span className="font-semibold text-primary">Activity Log</span>
</div>
</TableHeaderBar>
{/* Loading state */}
{isLoading ? (
<TableCenteredLoading
icon={<Loader2 className="w-5 h-5 animate-spin text-muted" />}
text="Loading..."
/>
) : events.length === 0 ? (
/* Empty state */
<TableEmpty
icon={<History className="w-6 h-6 text-muted" />}
title="No events found"
description="Try adjusting your filters"
/>
) : (
/* Table content */
<TableWrapper>
<Table>
<TableHeader>
<TableRow>
<TableHead
sortable
sorted={sortField === "event" ? sortDirection : null}
onSort={() => handleSort("event")}
sortIcon={<ArrowUpDown className="w-3 h-3 opacity-40" />}
sortAscIcon={<ArrowUp className="w-3 h-3" />}
sortDescIcon={<ArrowDown className="w-3 h-3" />}
>
Event
</TableHead>
<TableHead>User</TableHead>
<TableHead
sortable
sorted={sortField === "date" ? sortDirection : null}
onSort={() => handleSort("date")}
sortIcon={<ArrowUpDown className="w-3 h-3 opacity-40" />}
sortAscIcon={<ArrowUp className="w-3 h-3" />}
sortDescIcon={<ArrowDown className="w-3 h-3" />}
>
Date
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{events.map((event) => (
<TableRow
key={event.id}
clickable
onClick={() => router.push(`/audit/${event.id}`)}
>
<TableCell>{event.type}</TableCell>
<TableCell>{event.user}</TableCell>
<TableCell>{event.date}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableWrapper>
)}
{/* Pagination footer */}
{totalPages > 1 && (
<TableFooterBar>
<TablePagination
page={page}
totalPages={totalPages}
totalCount={totalCount}
limit={limit}
onPageChange={setPage}
showingText="Showing {start}-{end} of {total}"
pageText="Page {current} of {total}"
prevIcon={<ChevronLeft className="w-4 h-4" />}
nextIcon={<ChevronRight className="w-4 h-4" />}
/>
</TableFooterBar>
)}
</TableContainer>
List Layout (Card-Based)
For activity feeds and card-based lists:
import {
TableContainer,
TableHeaderBar,
TableList,
TableListItem,
TableListGroupHeader,
TableEmpty,
} from "@elcto/ui/components";
<TableContainer>
<TableHeaderBar>
<h2 className="font-semibold">Recent Activity</h2>
</TableHeaderBar>
<TableList>
{groupedEvents.map((group) => (
<div key={group.date}>
{/* Sticky date header */}
<TableListGroupHeader>
{group.label}
</TableListGroupHeader>
{/* Events in group */}
{group.events.map((event) => (
<TableListItem
key={event.id}
clickable
onClick={() => toggleExpand(event.id)}
>
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-lg bg-[var(--bg-3)]" />
<div>
<p className="font-medium">{event.title}</p>
<p className="text-sm text-muted">{event.description}</p>
</div>
</div>
</TableListItem>
))}
</div>
))}
</TableList>
</TableContainer>
Component Reference
Container Components
TableContainer - Card-styled wrapper with border and rounded corners.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | required | Table content |
className | string | - | Additional CSS classes |
TableHeaderBar - Header section above the table (for titles, filters).
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | required | Header content |
sticky | boolean | false | Sticky positioning |
className | string | - | Additional CSS classes |
TableFooterBar - Footer section below the table (for pagination).
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | required | Footer content |
className | string | - | Additional CSS classes |
TableWrapper - Scrollable container for horizontal overflow.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | required | Table element |
className | string | - | Additional CSS classes |
Core Table Components
Table - Main table element.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | required | Table content |
className | string | - | Additional CSS classes |
TableHeader - Table thead with background styling.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | required | Header rows |
className | string | - | Additional CSS classes |
TableBody - Table tbody with row dividers.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | required | Body rows |
className | string | - | Additional CSS classes |
TableRow - Table row with hover and click states.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | required | Row cells |
hoverable | boolean | true | Enable hover effect |
clickable | boolean | false | Enable cursor pointer |
className | string | - | Additional CSS classes |
TableHead - Header cell with sortable support.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | required | Cell content |
sortable | boolean | false | Enable sorting |
sorted | 'asc' | 'desc' | null | - | Current sort state |
onSort | () => void | - | Sort click handler |
sortIcon | ReactNode | - | Default sort icon |
sortAscIcon | ReactNode | - | Ascending sort icon |
sortDescIcon | ReactNode | - | Descending sort icon |
className | string | - | Additional CSS classes |
TableCell - Data cell.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | required | Cell content |
className | string | - | Additional CSS classes |
State Components
TableEmpty - Empty state display.
| Prop | Type | Default | Description |
|---|---|---|---|
icon | ReactNode | - | Icon element |
title | string | required | Title text |
description | string | - | Description text |
className | string | - | Additional CSS classes |
TableLoading - Skeleton loading rows.
| Prop | Type | Default | Description |
|---|---|---|---|
rows | number | 5 | Number of skeleton rows |
columns | number | 4 | Number of columns |
className | string | - | Additional CSS classes |
TableCenteredLoading - Centered spinner loading.
| Prop | Type | Default | Description |
|---|---|---|---|
icon | ReactNode | - | Loading spinner icon |
text | string | - | Loading text |
className | string | - | Additional CSS classes |
TablePagination - Pagination controls.
| Prop | Type | Default | Description |
|---|---|---|---|
page | number | required | Current page (1-indexed) |
totalPages | number | required | Total number of pages |
totalCount | number | required | Total number of items |
limit | number | required | Items per page |
onPageChange | (page: number) => void | required | Page change callback |
showingText | string | - | Text with {start}, {end}, {total} placeholders |
pageText | string | - | Text with {current}, {total} placeholders |
prevIcon | ReactNode | - | Previous button icon |
nextIcon | ReactNode | - | Next button icon |
className | string | - | Additional CSS classes |
List Layout Components
TableList - Divider-separated list container.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | required | List items |
className | string | - | Additional CSS classes |
TableListItem - List item with hover/click states.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | required | Item content |
clickable | boolean | false | Enable cursor pointer |
hoverable | boolean | true | Enable hover effect |
className | string | - | Additional CSS classes |
TableListGroupHeader - Sticky group header (for date groupings).
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | required | Header content |
sticky | boolean | true | Sticky positioning |
className | string | - | Additional CSS classes |
Toast
Toast notification system with auto-dismiss and slide animations.
import { ToastProvider, useToast } from "@elcto/ui/components";
// Wrap your app with ToastProvider
function App() {
return (
<ToastProvider
position="top-right"
autoDismissMs={5000}
animationDurationMs={200}
topOffset="4rem"
>
<MyApp />
</ToastProvider>
);
}
// Use the hook in any component
function MyComponent() {
const { showSuccess, showError, showWarning, showInfo, showToast } = useToast();
const handleSave = async () => {
try {
await saveData();
showSuccess("Changes saved successfully!", "Success");
} catch (error) {
showError("Failed to save changes. Please try again.");
}
};
return (
<div>
<button onClick={() => showInfo("This is an info message")}>
Show Info
</button>
<button onClick={() => showWarning("This is a warning", "Warning")}>
Show Warning
</button>
<button onClick={handleSave}>Save</button>
</div>
);
}
Features:
- Auto-dismiss after configurable duration
- Slide-in/slide-out animations based on position
- Backdrop blur effect
- Manual dismiss with close button
- Multiple toast variants (success, error, warning, info)
ToastProvider Props:
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | required | App content |
position | 'top-center' | 'top-right' | 'bottom-center' | 'bottom-right' | 'top-center' | Toast position |
autoDismissMs | number | 5000 | Auto-dismiss duration in milliseconds |
animationDurationMs | number | 200 | Slide animation duration in milliseconds |
topOffset | string | - | Top offset (e.g., "4rem" for header height) |
className | string | - | Additional CSS classes for the container |
useToast Hook:
| Method | Signature | Description |
|---|---|---|
showToast | (message, variant?, title?) => void | Show any variant |
showSuccess | (message, title?) => void | Show success toast |
showError | (message, title?) => void | Show error toast |
showWarning | (message, title?) => void | Show warning toast |
showInfo | (message, title?) => void | Show info toast |
Animation Behavior:
top-center: Slides in from top, slides out to toptop-right: Slides in from right, slides out to rightbottom-center: Slides in from bottom, slides out to bottombottom-right: Slides in from right, slides out to right
Tooltip
Hover tooltips with positioning.
import { Tooltip } from "@elcto/ui/components";
<Tooltip content="Click to edit" position="top">
<button>Edit</button>
</Tooltip>
<Tooltip content="More info here" position="right" variant="info">
<InfoIcon />
</Tooltip>
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
content | ReactNode | required | Tooltip content |
position | 'top' | 'right' | 'bottom' | 'left' | 'top' | Tooltip position |
variant | 'default' | 'info' | 'default' | Tooltip style |
children | ReactNode | required | Trigger element |
className | string | - | Additional CSS classes |
Logo
Brand logo components.
import { ElctoLogo, ElcapitanoLogo } from "@elcto/ui/components";
<ElctoLogo />
<ElctoLogo className="w-32" />
<ElcapitanoLogo />
<ElcapitanoLogo className="w-40" />
Props (both components):
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes |
Platform Icons
SVG icon components for various platforms.
import {
TwitchIcon,
DiscordIcon,
GitHubIcon,
YouTubeIcon,
KickIcon,
SteamIcon,
TrovoIcon,
TwitterIcon,
} from "@elcto/ui/components";
// All icons accept className for sizing and color
<TwitchIcon className="w-5 h-5" />
<DiscordIcon className="w-5 h-5 text-[#5865F2]" />
<GitHubIcon className="w-5 h-5" />
<YouTubeIcon className="w-5 h-5 text-[#FF0000]" />
<KickIcon className="w-5 h-5 text-[#53FC18]" />
<SteamIcon className="w-5 h-5" />
<TrovoIcon className="w-5 h-5" />
<TwitterIcon className="w-5 h-5" />
Available Icons:
| Component | Platform |
|---|---|
TwitchIcon | Twitch |
DiscordIcon | Discord |
GitHubIcon | GitHub |
YouTubeIcon | YouTube |
KickIcon | Kick |
SteamIcon | Steam |
TrovoIcon | Trovo |
TwitterIcon | Twitter/X |
Props (all icons):
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | CSS classes for sizing and color |
Select
Custom dropdown select component with keyboard navigation and auto-sizing.
import { Select } from "@elcto/ui/components";
import { Globe } from "lucide-react";
const options = [
{ value: "en", label: "English" },
{ value: "de", label: "German" },
{ value: "fr", label: "French", disabled: true },
];
// Basic usage
<Select
options={options}
value={selected}
onChange={setSelected}
placeholder="Select language..."
/>
// With icon and size variant
<Select
options={options}
value={selected}
onChange={setSelected}
leftIcon={<Globe className="w-4 h-4" />}
size="lg"
/>
// Error state
<Select
options={options}
value={selected}
onChange={setSelected}
error={true}
/>
// Fixed width (disable auto-sizing)
<Select
options={options}
value={selected}
onChange={setSelected}
autoWidth={false}
containerClassName="w-48"
/>
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
options | SelectOption[] | required | Options to display |
value | string | - | Currently selected value |
onChange | (value: string) => void | - | Change callback |
placeholder | string | 'Select...' | Placeholder text |
size | 'sm' | 'md' | 'lg' | 'md' | Size variant |
error | boolean | false | Error state |
disabled | boolean | false | Disabled state |
leftIcon | ReactNode | - | Icon on the left |
autoWidth | boolean | true | Auto-size to fit longest option |
name | string | - | Name for form submission |
className | string | - | Classes for trigger button |
containerClassName | string | - | Classes for container |
SelectOption:
| Prop | Type | Description |
|---|---|---|
value | string | Option value |
label | string | Display label |
disabled | boolean | Whether option is disabled |
Features:
- Full keyboard navigation (Arrow keys, Enter, Escape)
- Auto-sizing to fit longest option label
- Smooth open/close animations
- Accessible with ARIA attributes
- Form submission support with hidden input
SubNav
Nested navigation component for secondary navigation within pages.
import { SubNav } from "@elcto/ui/components";
import { Settings, Shield, Link2 } from "lucide-react";
const items = [
{ key: "settings", label: "Settings", icon: <Settings className="w-4 h-4" /> },
{ key: "security", label: "Security", icon: <Shield className="w-4 h-4" /> },
{ key: "connections", label: "Connections", icon: <Link2 className="w-4 h-4" /> },
];
// Basic usage
<SubNav
items={items}
activeKey={currentTab}
onItemClick={setCurrentTab}
/>
// Small size
<SubNav
items={items}
activeKey={currentTab}
onItemClick={setCurrentTab}
size="sm"
/>
// With custom Link component (Next.js)
import { Link } from "next/link";
<SubNav
items={items}
activeKey={currentPath}
renderItem={(item, isActive, className) => (
<Link key={item.key} href={`/account/${item.key}`} className={className}>
{item.icon}
{item.label}
</Link>
)}
/>
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
items | SubNavItem[] | required | Navigation items |
activeKey | string | required | Currently active item key |
onItemClick | (key: string) => void | - | Click callback |
renderItem | (item, isActive, className) => ReactNode | - | Custom render function |
size | 'sm' | 'md' | 'md' | Size variant |
className | string | - | Additional CSS classes |
SubNavItem:
| Prop | Type | Description |
|---|---|---|
key | string | Unique identifier |
label | string | Display label |
icon | ReactNode | Optional icon |
exact | boolean | Use exact matching for active state |
SubMenuBar
A horizontal sub-navigation bar that appears below the header. Respects sidebar width (collapsed/expanded) in dashboard layouts. On mobile, shows a dropdown menu or horizontal scroll.
import { SubMenuBar, type SubMenuBarItem } from "@elcto/ui/components";
import { History, Flag } from "lucide-react";
const menuItems: SubMenuBarItem[] = [
{
key: "overview",
label: "Overview",
href: "/system/audit",
icon: <History className="w-4 h-4" />,
},
{
key: "reports",
label: "Reports",
href: "/system/audit/reports",
icon: <Flag className="w-4 h-4" />,
},
];
// Basic usage
<SubMenuBar
items={menuItems}
activeHref={pathname}
sidebarCollapsed={sidebarCollapsed}
hasBanner={showBanner}
/>
// With Next.js Link
import Link from "next/link";
<SubMenuBar
items={menuItems}
activeHref={pathname}
sidebarCollapsed={sidebarCollapsed}
hasBanner={hasBanner}
renderItem={(item, isActive, className) => (
<Link key={item.key} href={item.href} className={className}>
{item.icon}
{item.label}
</Link>
)}
/>
// Horizontal scroll variant on mobile (instead of dropdown)
<SubMenuBar
items={menuItems}
activeHref={pathname}
mobileVariant="scroll"
/>
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
items | SubMenuBarItem[] | required | Navigation items |
activeKey | string | - | Active item by key |
activeHref | string | - | Active item by href (alternative to activeKey) |
renderItem | (item, isActive, className) => ReactNode | - | Custom render function for items |
className | string | - | Additional CSS classes |
sidebarCollapsed | boolean | false | Whether sidebar is collapsed (affects left offset on desktop) |
hasBanner | boolean | false | Whether a banner is showing above (affects top position) |
height | string | '3.5rem' | Custom height for the bar |
mobileVariant | 'dropdown' | 'scroll' | 'dropdown' | Mobile menu variant |
SubMenuBarItem:
| Prop | Type | Description |
|---|---|---|
key | string | Unique identifier |
label | string | Display label |
href | string | URL/href for the item |
icon | ReactNode | Optional icon |
Mobile Behavior:
- Dropdown (default): Shows active item with chevron, clicking opens a dropdown with all items
- Scroll: Shows all items in a horizontal scrollable row
Desktop Behavior:
- Respects sidebar width:
16remwhen expanded,4remwhen collapsed - Smooth 200ms transition when sidebar state changes
- Fixed position below header (
4remfrom top, or6.5remwith banner)
Layout Integration Example:
// In your layout component
import { SubMenuBar } from "@elcto/ui/components";
import { useWorkspace } from "@/contexts/WorkspaceContext";
import { useApiHealth, useShowApiHealthBanner } from "@elcto/ui/components";
export default function AuditLayout({ children }) {
const { sidebarCollapsed } = useWorkspace();
const { state: apiHealth } = useApiHealth();
const hasBanner = useShowApiHealthBanner(apiHealth);
return (
<>
<SubMenuBar
items={menuItems}
activeHref={pathname}
sidebarCollapsed={sidebarCollapsed}
hasBanner={hasBanner}
renderItem={(item, isActive, className) => (
<Link href={item.href} className={className}>
{item.icon}
{item.label}
</Link>
)}
/>
{/* Add padding for the fixed SubMenuBar */}
<div className="pt-14">
{children}
</div>
</>
);
}
AnimatedBackground
Animated background component with gradient circles and floating particles. Ideal for auth pages, error pages, or any page needing visual interest.
import {
AnimatedBackground,
AnimatedBackgroundProvider,
SetAnimatedBackground,
useAnimatedBackground,
} from "@elcto/ui/components";
// Basic usage
<AnimatedBackground variant="brand">
<div className="p-8">
<h1>Welcome</h1>
</div>
</AnimatedBackground>
// Different color variants
<AnimatedBackground variant="error">
<ErrorPage />
</AnimatedBackground>
<AnimatedBackground variant="warning">
<WarningPage />
</AnimatedBackground>
<AnimatedBackground variant="success">
<SuccessPage />
</AnimatedBackground>
// Custom color
<AnimatedBackground variant="custom" customColor="#ff6b6b">
<CustomPage />
</AnimatedBackground>
// Disable certain effects
<AnimatedBackground
variant="brand"
showParticles={false}
showSpinningGradient={false}
particleCount={6}
>
<MinimalPage />
</AnimatedBackground>
// Fixed overlay
<AnimatedBackground variant="error" fixed zIndex={100}>
<FullScreenOverlay />
</AnimatedBackground>
AnimatedBackground Props:
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'brand' | 'warning' | 'error' | 'success' | 'subtle' | 'custom' | 'brand' | Color variant |
customColor | string | - | Custom CSS color (when variant="custom") |
particleCount | number | 12 | Number of floating particles |
showParticles | boolean | true | Show floating particles |
showGradients | boolean | true | Show gradient circles |
showSpinningGradient | boolean | true | Show spinning gradient |
fixed | boolean | false | Use fixed positioning (for overlays) |
zIndex | number | 50 | Z-index when fixed |
useContextConfig | boolean | false | Read config from context |
className | string | - | Additional CSS classes |
Context Provider Pattern
For layouts where child pages need to override the background:
// In your layout.tsx
import {
AnimatedBackground,
AnimatedBackgroundProvider,
} from "@elcto/ui/components";
export default function AuthLayout({ children }) {
return (
<AnimatedBackgroundProvider defaultVariant="brand">
<AnimatedBackground useContextConfig>
{children}
</AnimatedBackground>
</AnimatedBackgroundProvider>
);
}
// In a child page (e.g., banned/layout.tsx)
import { SetAnimatedBackground } from "@elcto/ui/components";
export default function BannedLayout({ children }) {
return (
<SetAnimatedBackground variant="error">
{children}
</SetAnimatedBackground>
);
}
AnimatedBackgroundProvider Props:
| Prop | Type | Default | Description |
|---|---|---|---|
defaultVariant | string | 'brand' | Default color variant |
defaults | Partial<AnimatedBackgroundConfig> | - | Default configuration |
useAnimatedBackground Hook:
const { config, setConfig, resetConfig } = useAnimatedBackground();
// Update background dynamically
useEffect(() => {
setConfig({ variant: "error", showParticles: false });
return () => resetConfig();
}, []);
Turnstile
Cloudflare Turnstile CAPTCHA integration for bot protection.
import { Turnstile, useTurnstile } from "@elcto/ui/components";
// Using the hook (recommended)
function LoginForm() {
const turnstile = useTurnstile();
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
if (turnstile.isWaiting) {
setError("Please wait for verification...");
return;
}
setIsLoading(true);
await fetch("/api/login", {
method: "POST",
body: JSON.stringify({
email,
password,
turnstile_token: turnstile.getTokenForApi(),
}),
});
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
disabled={turnstile.disabledWhileWaiting}
/>
<input
type="password"
disabled={turnstile.disabledWhileWaiting}
/>
{/* Render the invisible widget */}
<turnstile.Widget />
<button disabled={isLoading || turnstile.disabledWhileWaiting}>
{turnstile.isWaiting ? "Verifying..." : "Sign In"}
</button>
</form>
);
}
// Direct component usage
<Turnstile
siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY}
onVerify={(token) => setToken(token)}
onError={() => setError("Verification failed")}
onExpire={() => setToken(null)}
theme="dark"
/>
Turnstile Props:
| Prop | Type | Default | Description |
|---|---|---|---|
siteKey | string | required | Cloudflare Turnstile site key |
onVerify | (token: string) => void | required | Success callback |
onError | () => void | - | Error callback |
onExpire | () => void | - | Token expiry callback |
theme | 'light' | 'dark' | 'auto' | 'auto' | Widget theme |
size | 'normal' | 'compact' | 'normal' | Widget size |
className | string | - | Additional CSS classes |
useTurnstile Hook:
const turnstile = useTurnstile({
theme: "dark",
siteKey: "your-site-key", // or use NEXT_PUBLIC_TURNSTILE_SITE_KEY env var
});
Hook Options:
| Option | Type | Default | Description |
|---|---|---|---|
theme | 'light' | 'dark' | 'auto' | 'auto' | Widget theme |
siteKey | string | env var | Turnstile site key |
Hook Return Value:
| Property | Type | Description |
|---|---|---|
token | string | null | Current verification token |
isEnabled | boolean | Whether Turnstile is configured |
isWaiting | boolean | Waiting for verification |
isVerified | boolean | Verification complete |
reset | () => void | Reset token state |
disabledWhileWaiting | boolean | Use for disabling inputs |
Widget | React.FC | Widget component to render |
getTokenForApi | () => string | undefined | Get token for API calls |
Environment Variable:
Set NEXT_PUBLIC_TURNSTILE_SITE_KEY in your .env file:
NEXT_PUBLIC_TURNSTILE_SITE_KEY=your-cloudflare-site-key
If the environment variable is not set, Turnstile is automatically disabled (isEnabled = false).
FileUpload
Complete file upload component with drag-and-drop, presigned URL support, progress tracking, and file previews.
import { FileUpload } from "@elcto/ui/components";
// With presigned URLs for direct S3 upload
<FileUpload
accept="image/*,video/*"
maxSize={5 * 1024 * 1024} // 5MB
maxFiles={5}
directUpload
presignedUrlFetcher={async (file) => {
const res = await fetch("/api/storage/presigned-upload", {
method: "POST",
body: JSON.stringify({
filename: file.name,
contentType: file.type,
category: file.type.startsWith("image/") ? "images" : "videos",
}),
});
return res.json();
}}
onUpload={(files) => console.log("Uploaded:", files)}
onError={(error) => showToast(error.message, "error")}
label="Drop files here"
description="or click to browse"
/>
// Without direct upload (for form handling)
<FileUpload
accept="image/*"
maxFiles={1}
onUpload={(files) => setSelectedFile(files[0])}
variant="compact"
/>
// Controlled mode
<FileUpload
files={files}
onFilesChange={setFiles}
directUpload
presignedUrlFetcher={fetchPresignedUrl}
onUpload={handleUpload}
/>
FileUpload Props:
| Prop | Type | Default | Description |
|---|---|---|---|
onUpload | (files: UploadedFile[]) => void | required | Upload success callback |
onError | (error: UploadError) => void | - | Error callback |
presignedUrlFetcher | (file: File) => Promise<PresignedUploadUrl> | - | Function to get presigned URL |
directUpload | boolean | false | Enable direct browser upload to S3 |
accept | string | - | MIME types to accept (e.g., "image/,video/") |
maxSize | number | - | Max file size in bytes |
maxFiles | number | 10 | Max number of files |
variant | 'default' | 'compact' | 'inline' | 'default' | Dropzone style |
size | 'sm' | 'md' | 'lg' | 'md' | Size variant |
showPreview | boolean | true | Show file previews |
previewType | 'grid' | 'list' | 'grid' | Preview layout |
label | string | - | Dropzone label |
description | string | - | Dropzone description |
icon | ReactNode | - | Custom dropzone icon |
children | ReactNode | - | Custom dropzone content |
disabled | boolean | false | Disable upload |
loading | boolean | false | Loading state |
files | UploadedFile[] | - | Controlled files (for controlled mode) |
onFilesChange | (files: UploadedFile[]) => void | - | Files change callback (controlled mode) |
className | string | - | Additional CSS classes |
PresignedUploadUrl:
interface PresignedUploadUrl {
url: string; // The presigned PUT URL
key: string; // The S3 object key
expiresAt: string; // Expiration timestamp
}
UploadedFile:
interface UploadedFile {
key: string; // File key (S3 key or temp ID)
url: string; // Public URL after upload
name: string; // Original filename
size: number; // File size in bytes
type: string; // MIME type
progress?: number; // Upload progress 0-100
status: "pending" | "uploading" | "complete" | "error";
error?: string; // Error message if status is "error"
}
UploadError:
interface UploadError {
file: File;
code: "SIZE_EXCEEDED" | "TYPE_NOT_ALLOWED" | "MAX_FILES" | "UPLOAD_FAILED";
message: string;
}
Sub-components:
| Component | Description |
|---|---|
FileDropzone | Drag-and-drop zone with validation |
FilePreview | Grid/list preview of uploaded files |
UploadProgress | Upload progress bar |
ImagePreview
Display images with loading states, error handling, and optional lightbox support.
import { ImagePreview } from "@elcto/ui/components";
// Basic usage
<ImagePreview
src="https://storage.example.com/images/photo.jpg"
alt="User photo"
aspectRatio="16:9"
objectFit="cover"
rounded="md"
/>
// With lightbox (click to view fullscreen)
<ImagePreview
src={imageUrl}
alt="Gallery image"
aspectRatio="1:1"
lightbox
zoom
/>
// Custom fallback on error
<ImagePreview
src={userAvatar}
alt={userName}
fallback={<Avatar name={userName} size={120} />}
rounded="full"
/>
// Lazy loading disabled
<ImagePreview
src={heroImage}
alt="Hero"
lazy={false}
aspectRatio="21:9"
/>
ImagePreview Props:
| Prop | Type | Default | Description |
|---|---|---|---|
src | string | required | Image source URL |
alt | string | required | Alt text for accessibility |
width | number | string | - | Image width |
height | number | string | - | Image height |
aspectRatio | '1:1' | '4:3' | '16:9' | 'auto' | 'auto' | Aspect ratio |
objectFit | 'cover' | 'contain' | 'fill' | 'cover' | How image fills container |
lightbox | boolean | false | Enable fullscreen view on click |
zoom | boolean | false | Enable zoom on hover/click |
lazy | boolean | true | Enable lazy loading |
fallback | string | ReactNode | - | Error fallback (URL or component) |
placeholder | ReactNode | - | Loading placeholder |
rounded | 'none' | 'sm' | 'md' | 'lg' | 'full' | 'md' | Border radius |
border | boolean | false | Show border |
shadow | boolean | false | Add shadow |
onClick | () => void | - | Click handler (overrides lightbox) |
className | string | - | Additional CSS classes |
Loading State:
The component shows a shimmer skeleton while loading. Customize with the placeholder prop.
Error State:
When image fails to load, shows a fallback. Can be a URL string or React component.
Lightbox:
When lightbox is true, clicking the image opens a fullscreen overlay with close button and optional zoom controls.