PyroRadius UI Component Library¶
Document: PHASE2_11_UI_Component_Library.md Version: 2.0 Date: 2026-07-13 Author: PyroNet Solutions
Overview¶
PyroRadius uses a custom React component library built on top of Tailwind CSS and Headless UI. All components are located in resources/js/Components/ and are shared across all Inertia.js pages. This document catalogs every reusable component, its props, usage patterns, and design system rules.
Framework: React 18 + Inertia.js Styling: Tailwind CSS v3 with custom theme tokens Accessible components: Headless UI v2 Charts: Recharts Icons: Heroicons + custom SVGs
Design System¶
Color Tokens¶
The design system uses semantic color tokens defined in tailwind.config.js. Do not use raw Tailwind colors (e.g., red-500) — always use semantic tokens.
| Token | Light Mode | Dark Mode | Usage |
|---|---|---|---|
ember / flame |
orange-600 | orange-500 | Brand primary, primary actions |
success |
green-600 | green-400 | Active status, success states |
info |
blue-600 | blue-400 | Informational states |
warning |
amber-500 | amber-400 | Expiring soon, warnings |
danger |
red-600 | red-400 | Destructive actions, error states |
muted |
gray-500 | gray-400 | Secondary text, placeholders |
surface |
white | gray-900 | Page background |
card |
white | gray-800 | Card/panel background |
border |
gray-200 | gray-700 | Borders and dividers |
Typography Scale¶
| Class | Size | Weight | Usage |
|---|---|---|---|
text-2xl font-bold |
24px | 700 | Page titles |
text-xl font-semibold |
20px | 600 | Section headings |
text-lg font-semibold |
18px | 600 | Card titles, sub-headings |
text-base |
16px | 400 | Body text |
text-sm |
14px | 400 | Table content, labels |
text-xs |
12px | 400 | Helper text, badges |
text-muted |
— | — | Muted/secondary text |
Spacing Conventions¶
| Element | Tailwind Classes |
|---|---|
| Page container | max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 |
| Card / panel | p-6 rounded-xl shadow-sm |
| Section gap | gap-6 or space-y-6 |
| Form group | space-y-4 |
| Button padding | px-4 py-2 |
| Input padding | px-3 py-2 |
Dark Mode Strategy¶
All components support dark mode via Tailwind's dark: prefix. Dark mode is activated by:
1. Adding the dark class to the <html> element
2. User preference stored in localStorage key theme
Components follow this pattern:
Core Components¶
ApplicationLogo¶
File: Components/ApplicationLogo.jsx
Used In: Login page header, sidebar top section, email templates
The primary brand logo for PyroRadius/PyroNet. Renders an SVG or image logo. Supports both the PyroNet primary brand and the Indus Broadband white-label variant (configured via app settings).
Props:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| className | string | No | "" |
Tailwind classes for sizing and positioning |
| variant | string | No | "default" |
"default" or "white" for dark backgrounds |
Usage Example:
// In login page
<ApplicationLogo className="w-40 h-auto" />
// White variant for dark sidebar
<ApplicationLogo className="w-32 h-auto" variant="white" />
Notes:
The logo automatically adapts to dark mode if variant="default" is used. On very dark backgrounds (sidebar), use variant="white" to ensure visibility. SVG-based for crisp rendering at all sizes.
StatusPill¶
File: Components/StatusPill.jsx
Used In: Customer list table, customer detail page, dashboard customer cards, reports tables, search results
One of the most widely used components. Renders a colored pill badge showing a customer's current connection/billing status.
Props:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| status | string | Yes | — | Customer status value (see below) |
| size | string | No | "md" |
"sm", "md", "lg" |
| className | string | No | "" |
Additional Tailwind classes |
Status Values and Colors:
| Status Value | Display Text | Color | Meaning |
|---|---|---|---|
active |
Active | Green | Customer is active and connected |
expired |
Expired | Red | Service has expired, PPPoE blocked |
suspended |
Suspended | Amber/Yellow | Manually suspended |
disabled |
Disabled | Gray | Admin-disabled, cannot connect |
temp_extended |
Extended | Blue | Temporary extension applied |
Usage Example:
// In customer table row
<StatusPill status={customer.status} />
// Smaller badge in compact list
<StatusPill status={customer.status} size="sm" />
// Large pill in detail page header
<StatusPill status={customer.status} size="lg" />
Implementation Notes:
// Internal color mapping
const statusConfig = {
active: { bg: 'bg-green-100 dark:bg-green-900/30', text: 'text-green-700 dark:text-green-400', label: 'Active' },
expired: { bg: 'bg-red-100 dark:bg-red-900/30', text: 'text-red-700 dark:text-red-400', label: 'Expired' },
suspended: { bg: 'bg-amber-100 dark:bg-amber-900/30', text: 'text-amber-700 dark:text-amber-400', label: 'Suspended' },
disabled: { bg: 'bg-gray-100 dark:bg-gray-700', text: 'text-gray-600 dark:text-gray-400', label: 'Disabled' },
temp_extended: { bg: 'bg-blue-100 dark:bg-blue-900/30', text: 'text-blue-700 dark:text-blue-400', label: 'Extended' },
};
Dark Mode: Fully supported — uses lighter backgrounds in dark mode with adjusted text colors for contrast.
Accessibility: role="status" with aria-label={status} for screen readers.
Modal¶
File: Components/Modal.jsx
Used In: Confirm delete dialogs, edit forms (customer, package, NAS, dealer), archive confirmation, bulk action confirmations, WhatsApp preview
A Headless UI Dialog wrapper providing accessible modal dialogs with backdrop blur, focus trapping, and keyboard dismissal (Escape key).
Props:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| show | boolean | Yes | — | Controls modal visibility |
| onClose | function | Yes | — | Called when user dismisses modal |
| maxWidth | string | No | "md" |
"sm", "md", "lg", "xl", "2xl", "full" |
| closeable | boolean | No | true |
Whether clicking backdrop closes modal |
| children | ReactNode | Yes | — | Modal content |
Usage Example:
const [showDeleteModal, setShowDeleteModal] = useState(false);
<Modal show={showDeleteModal} onClose={() => setShowDeleteModal(false)} maxWidth="sm">
<div className="p-6">
<h2 className="text-lg font-semibold">Confirm Delete</h2>
<p className="mt-2 text-sm text-gray-600">
Are you sure you want to delete this customer? This action cannot be undone.
</p>
<div className="mt-4 flex gap-3 justify-end">
<SecondaryButton onClick={() => setShowDeleteModal(false)}>
Cancel
</SecondaryButton>
<DangerButton onClick={handleDelete}>
Delete
</DangerButton>
</div>
</div>
</Modal>
Notes:
- Focus is automatically trapped inside the modal when open.
- Escape key calls
onCloseifcloseableis true. - Clicking the backdrop calls
onCloseifcloseableis true. closeable={false}is used for critical operations like bulk recharge where accidental dismissal should be prevented.- Modal stacking (nested modals) is not supported — avoid nested modals.
- The modal renders a portal to
document.bodyto avoid z-index issues.
Dropdown¶
File: Components/Dropdown.jsx
Used In: User profile menu (top-right nav), action menus in customer table rows, bulk action dropdowns, NAS context menus
A Headless UI Menu-based dropdown component with sub-components for composability.
Sub-Components:
Dropdown.Trigger— The element that opens the dropdownDropdown.Content— The dropdown panel containerDropdown.Link— A navigation item inside the dropdown (renders<Link>from Inertia)Dropdown.Button— A non-navigation button item
Props — Dropdown:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| children | ReactNode | Yes | — | Must contain Trigger and Content |
Props — Dropdown.Content:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| align | string | No | "right" |
"left" or "right" — panel alignment |
| width | string | No | "48" |
Tailwind width class (e.g., "48", "64") |
| children | ReactNode | Yes | — | Dropdown items |
Usage Example:
// User menu in navigation bar
<Dropdown>
<Dropdown.Trigger>
<button className="flex items-center gap-2">
{user.name}
<ChevronDownIcon className="w-4 h-4" />
</button>
</Dropdown.Trigger>
<Dropdown.Content>
<Dropdown.Link href={route('profile.edit')}>
Profile
</Dropdown.Link>
<Dropdown.Link href={route('settings.index')}>
Settings
</Dropdown.Link>
<Dropdown.Button onClick={handleLogout}>
Logout
</Dropdown.Button>
</Dropdown.Content>
</Dropdown>
// Action menu in table row
<Dropdown>
<Dropdown.Trigger>
<button>
<EllipsisVerticalIcon className="w-5 h-5" />
</button>
</Dropdown.Trigger>
<Dropdown.Content align="right" width="56">
<Dropdown.Link href={route('customers.show', customer.id)}>
View Details
</Dropdown.Link>
<Dropdown.Button onClick={() => openRechargeModal(customer)}>
Recharge
</Dropdown.Button>
</Dropdown.Content>
</Dropdown>
Notes:
Closes automatically when clicking outside or pressing Escape. Keyboard navigation (Arrow keys, Enter, Escape) supported. Positioning is handled automatically — the panel flips to avoid viewport overflow.
PrimaryButton¶
File: Components/PrimaryButton.jsx
Used In: Form submissions, primary action triggers, login button, save buttons
The main call-to-action button using the brand ember/flame color.
Props:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| type | string | No | "submit" |
HTML button type |
| disabled | boolean | No | false |
Disables button and shows reduced opacity |
| children | ReactNode | Yes | — | Button label/content |
| className | string | No | "" |
Additional Tailwind classes |
| onClick | function | No | — | Click handler |
Usage Example:
<PrimaryButton type="submit" disabled={processing}>
{processing ? 'Saving...' : 'Save Customer'}
</PrimaryButton>
<PrimaryButton onClick={handleRecharge}>
Recharge
</PrimaryButton>
Notes:
When disabled, the button shows opacity-50 cursor-not-allowed. The processing state from Inertia's useForm hook is typically passed as disabled={processing} to prevent double-submit. Uses transition-colors for smooth hover effect.
SecondaryButton¶
File: Components/SecondaryButton.jsx
Used In: Cancel actions, secondary form actions, "Back" buttons, export buttons
A muted button for non-primary actions. Does not draw as much visual attention as PrimaryButton.
Props:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| type | string | No | "button" |
HTML button type |
| disabled | boolean | No | false |
Disables button |
| children | ReactNode | Yes | — | Button label/content |
| className | string | No | "" |
Additional Tailwind classes |
| onClick | function | No | — | Click handler |
Usage Example:
// In a modal footer
<div className="flex gap-3 justify-end">
<SecondaryButton onClick={onClose}>
Cancel
</SecondaryButton>
<PrimaryButton type="submit">
Save
</PrimaryButton>
</div>
DangerButton¶
File: Components/DangerButton.jsx
Used In: Delete confirmations, account suspension, bulk delete actions
A red destructive action button. Always paired with a confirmation step (Modal) before the action is executed.
Props:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| type | string | No | "button" |
HTML button type |
| disabled | boolean | No | false |
Disables button |
| children | ReactNode | Yes | — | Button label/content |
| className | string | No | "" |
Additional Tailwind classes |
| onClick | function | No | — | Click handler |
Usage Example:
<DangerButton onClick={confirmDelete} disabled={processing}>
{processing ? 'Deleting...' : 'Delete Customer'}
</DangerButton>
Design Rule: Never use DangerButton as a primary action without a confirmation modal. Always require an explicit user confirmation before executing irreversible actions.
TextInput¶
File: Components/TextInput.jsx
Used In: All forms throughout the application — customer create/edit, package forms, NAS forms, login, settings, search bars
A consistently styled form text input with proper focus ring and dark mode support.
Props:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| type | string | No | "text" |
HTML input type (text, email, password, number, tel, date) |
| id | string | No | — | Input ID (for label association) |
| name | string | No | — | Input name |
| value | string/number | No | — | Controlled value |
| defaultValue | string/number | No | — | Uncontrolled default value |
| onChange | function | No | — | Change handler |
| onBlur | function | No | — | Blur handler |
| placeholder | string | No | "" |
Placeholder text |
| className | string | No | "" |
Additional classes |
| autoComplete | string | No | — | HTML autocomplete attribute |
| disabled | boolean | No | false |
Disables input |
| required | boolean | No | false |
Marks as required |
| isFocused | boolean | No | false |
Auto-focuses on mount |
Usage Example:
// Controlled input in Inertia form
<TextInput
id="pppoe_username"
name="pppoe_username"
type="text"
value={data.pppoe_username}
onChange={(e) => setData('pppoe_username', e.target.value)}
autoComplete="off"
placeholder="e.g., ali_01"
/>
// Password input
<TextInput
id="password"
type="password"
value={data.password}
onChange={(e) => setData('password', e.target.value)}
autoComplete="current-password"
/>
Notes:
Uses ref forwarding for the isFocused prop to auto-focus on mount. Focus ring uses the brand ember color. Compatible with Inertia.js useForm hook's data and setData. Dark mode: bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600.
InputLabel¶
File: Components/InputLabel.jsx
Used In: All form fields — always paired with TextInput, Checkbox, or select elements
Renders a styled <label> element with consistent typography and spacing.
Props:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| htmlFor | string | No | — | ID of associated input element |
| value | string | No | — | Label text (alternative to children) |
| children | ReactNode | No | — | Label content (if not using value prop) |
| className | string | No | "" |
Additional classes |
Usage Example:
// Using value prop
<InputLabel htmlFor="pppoe_username" value="PPPoE Username" />
<TextInput id="pppoe_username" ... />
// Using children (for labels with embedded elements)
<InputLabel htmlFor="package_id">
Package <span className="text-red-500">*</span>
</InputLabel>
Notes:
Always associate labels with inputs via htmlFor/id for accessibility. text-sm font-medium text-gray-700 dark:text-gray-300.
InputError¶
File: Components/InputError.jsx
Used In: All form fields — renders below the input when there is a validation error
Displays a red validation error message below a form field.
Props:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| message | string | No | — | Error message to display |
| className | string | No | "" |
Additional classes |
Usage Example:
// Typically used with Inertia useForm errors
<TextInput
id="pppoe_username"
value={data.pppoe_username}
onChange={(e) => setData('pppoe_username', e.target.value)}
/>
<InputError message={errors.pppoe_username} className="mt-1" />
Notes:
Renders nothing (returns null) if message is falsy — safe to always include in forms. Uses text-red-600 dark:text-red-400 text-sm. The role="alert" attribute is set for screen reader compatibility.
Checkbox¶
File: Components/Checkbox.jsx
Used In: Bulk selection in customer list, permission toggles in user settings, feature flags in package settings
A styled checkbox input consistent with the design system.
Props:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| id | string | No | — | Input ID |
| name | string | No | — | Input name |
| checked | boolean | Yes | — | Controlled checked state |
| onChange | function | Yes | — | Change handler |
| disabled | boolean | No | false |
Disables the checkbox |
| className | string | No | "" |
Additional classes |
Usage Example:
// Single checkbox
<div className="flex items-center gap-2">
<Checkbox
id="auto_renew"
name="auto_renew"
checked={data.auto_renew}
onChange={(e) => setData('auto_renew', e.target.checked)}
/>
<InputLabel htmlFor="auto_renew" value="Auto-renew on expiry" />
</div>
// Bulk select in table header
<Checkbox
checked={allSelected}
onChange={toggleSelectAll}
/>
Notes:
Uses brand ember color for checked state. The onChange event provides the full event object — use e.target.checked (not e.target.value) for boolean checkboxes.
NavLink¶
File: Components/NavLink.jsx
Used In: Sidebar navigation links, mobile navigation menu
An Inertia Link component styled as a navigation item with active and inactive states.
Props:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| href | string | Yes | — | Navigation URL (Inertia route) |
| active | boolean | No | false |
Whether this link is the current page |
| children | ReactNode | Yes | — | Link content (text + optional icon) |
| className | string | No | "" |
Additional classes |
Usage Example:
// In sidebar layout
<NavLink
href={route('customers.index')}
active={route().current('customers.*')}
>
<UsersIcon className="w-5 h-5" />
<span>Customers</span>
</NavLink>
<NavLink
href={route('dashboard')}
active={route().current('dashboard')}
>
<HomeIcon className="w-5 h-5" />
<span>Dashboard</span>
</NavLink>
Notes:
Active state: bg-ember-50 text-ember-700 dark:bg-ember-900/20 dark:text-ember-400 font-semibold.
Inactive state: text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700.
Uses Inertia Link (not <a>) for SPA navigation without full page reload.
BandwidthChart¶
File: Components/BandwidthChart.jsx
Used In: Customer detail page (usage history tab), NAS detail page (per-interface graph), Network overview page, Reports bandwidth section
A Recharts-based area chart showing download and upload bandwidth over time.
Props:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| data | array | Yes | — | Array of { time, rx, tx } objects |
| height | number | No | 250 |
Chart height in pixels |
| rxColor | string | No | "#10b981" |
Line color for download (RX) |
| txColor | string | No | "#f97316" |
Line color for upload (TX) |
| unit | string | No | "Mbps" |
Y-axis unit label |
| showLegend | boolean | No | true |
Show legend below chart |
| className | string | No | "" |
Container class |
Data Format:
const data = [
{ time: "14:00", rx: 4.5, tx: 1.2 },
{ time: "14:05", rx: 6.2, tx: 0.8 },
{ time: "14:10", rx: 3.8, tx: 2.1 },
// ... more data points
];
Usage Example:
// Customer usage chart (fetched from traffic-history API)
<BandwidthChart
data={trafficData}
height={300}
unit="Mbps"
showLegend={true}
/>
// Compact chart in dashboard widget
<BandwidthChart
data={networkData}
height={120}
showLegend={false}
/>
Notes:
Uses Recharts AreaChart with gradient fills. RX (download) is typically green; TX (upload) is amber/orange. Responsive via Recharts ResponsiveContainer — always fills the parent width. Tooltip shows exact values on hover. Dark mode: background-transparent, axis text uses text-gray-400. Performance: for charts with more than 200 data points, the data is automatically downsampled using the LTTB (Largest Triangle Three Buckets) algorithm.
LiveTrafficChart¶
File: Components/LiveTrafficChart.jsx
Used In: Customer detail page (live traffic tab), NAS real-time monitoring panel
A real-time bandwidth display component showing current download/upload speeds as animated gauges or live line charts.
Props:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| rxBps | number | Yes | — | Current download speed in bits per second |
| txBps | number | Yes | — | Current upload speed in bits per second |
| available | boolean | No | true |
Whether live data is available (customer online) |
| packageSpeedKbps | number | No | — | Package speed limit in Kbps (for gauge max) |
| refreshInterval | number | No | 5000 |
Auto-refresh interval in milliseconds |
| onRefresh | function | No | — | Callback to fetch new data |
Usage Example:
// Customer detail live traffic widget
const [liveData, setLiveData] = useState({ rx: 0, tx: 0 });
useEffect(() => {
const interval = setInterval(async () => {
const response = await axios.get(`/api/mobile/customers/${customerId}/live-traffic`);
setLiveData({ rx: response.data.rx_bps, tx: response.data.tx_bps });
}, 5000);
return () => clearInterval(interval);
}, []);
<LiveTrafficChart
rxBps={liveData.rx}
txBps={liveData.tx}
available={customer.online}
packageSpeedKbps={customer.package.speed_download_kbps}
/>
Notes:
Displays human-readable speeds (auto-converts bps → Kbps → Mbps → Gbps). When available is false, shows "Customer Offline" state with gray gauges. The rolling line chart keeps the last 60 data points (5 minutes at 5-second intervals). Does not self-fetch — the parent component is responsible for polling and passing updated values.
ThemeToggle¶
File: Components/ThemeToggle.jsx
Used In: Navigation bar (top-right area), settings page
A button that toggles between dark and light mode.
Props:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| className | string | No | "" |
Additional classes |
Usage Example:
// In the main navigation bar
<div className="flex items-center gap-4">
<ThemeToggle />
<Dropdown>{/* user menu */}</Dropdown>
</div>
Implementation Notes:
// Theme persistence logic
const [theme, setTheme] = useState(
localStorage.getItem('theme') ?? 'light'
);
const toggleTheme = () => {
const newTheme = theme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
localStorage.setItem('theme', newTheme);
document.documentElement.classList.toggle('dark', newTheme === 'dark');
};
Uses a sun icon for light mode and moon icon for dark mode (Heroicons). Smooth transition on icon swap. The dark class is applied to <html> element to enable Tailwind dark mode for all child components.
WhatsAppIcon¶
File: Components/WhatsAppIcon.jsx
Used In: WhatsApp send buttons, WhatsApp section headers, bulk WhatsApp campaign UI, customer communication panel
The official WhatsApp brand icon as an inline SVG component.
Props:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| className | string | No | "w-5 h-5" |
Tailwind size and color classes |
| color | string | No | "#25D366" |
SVG fill color (use brand green) |
Usage Example:
// Send WhatsApp button
<button className="flex items-center gap-2 text-green-600">
<WhatsAppIcon className="w-5 h-5" />
Send WhatsApp
</button>
// Icon-only button with tooltip
<button title="Send WhatsApp receipt">
<WhatsAppIcon className="w-6 h-6" />
</button>
Notes:
Always use the official WhatsApp green (#25D366) for the icon color. Do not invert or recolor for decorative purposes per WhatsApp brand guidelines. On dark backgrounds, the icon remains green (do not make white).
AreaCrud¶
File: Components/AreaCrud.jsx
Used In: Settings → Areas & Cities management page, NAS assignment forms
A complete CRUD UI component for managing geographic areas (cities and sub-areas/sectors).
Props:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| areas | array | Yes | — | Current list of area objects |
| cities | array | Yes | — | Available cities for assignment |
| onSave | function | Yes | — | Callback when area is saved/edited |
| onDelete | function | Yes | — | Callback when area is deleted |
| canEdit | boolean | No | true |
Whether edit controls are shown |
| canDelete | boolean | No | true |
Whether delete button is shown |
Data Format:
const areas = [
{ id: 1, name: "Johar Town", city_id: 1, city_name: "Lahore", customer_count: 145 },
{ id: 2, name: "DHA Phase 5", city_id: 1, city_name: "Lahore", customer_count: 98 },
];
Usage Example:
<AreaCrud
areas={areas}
cities={cities}
onSave={(areaData) => router.post(route('areas.store'), areaData)}
onDelete={(areaId) => router.delete(route('areas.destroy', areaId))}
/>
Notes:
Includes inline add/edit forms (not separate modals). Prevents deletion of areas with assigned customers (shows warning with customer count). Optimistic UI — shows changes immediately before server confirms.
UI Sub-Components (ui/ folder)¶
The resources/js/Components/ui/ folder contains lower-level primitives that are composed into the main components above.
ui/Badge¶
File: Components/ui/Badge.jsx
Used In: Notification counts, system status indicators, package type labels
A simple badge/chip component for short labels.
Props:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| variant | string | No | "default" |
"default", "success", "warning", "danger", "info" |
| children | ReactNode | Yes | — | Badge text/content |
| className | string | No | "" |
Additional classes |
Usage Example:
<Badge variant="success">Online</Badge>
<Badge variant="danger">3 Alerts</Badge>
<Badge variant="warning">Expiring Soon</Badge>
ui/Card¶
File: Components/ui/Card.jsx
Used In: Dashboard stat widgets, form containers, detail page sections, report panels
A consistent card/panel container with shadow and border.
Props:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| children | ReactNode | Yes | — | Card content |
| className | string | No | "" |
Additional classes |
| padding | string | No | "p-6" |
Inner padding class |
Usage Example:
<Card>
<h3 className="text-lg font-semibold">Total Customers</h3>
<p className="text-3xl font-bold mt-2">1,247</p>
</Card>
<Card padding="p-0">
{/* Full-bleed table inside card */}
<table>...</table>
</Card>
ui/Spinner¶
File: Components/ui/Spinner.jsx
Used In: Loading states in data tables, async button states, page transition overlays
An animated loading spinner.
Props:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| size | string | No | "md" |
"sm", "md", "lg" |
| color | string | No | "ember" |
Tailwind color token |
| className | string | No | "" |
Additional classes |
Usage Example:
// In table loading state
{loading ? (
<div className="flex justify-center py-12">
<Spinner size="lg" />
</div>
) : (
<CustomerTable data={customers} />
)}
// Inline in button
<PrimaryButton disabled={processing}>
{processing && <Spinner size="sm" className="mr-2" />}
Save
</PrimaryButton>
ui/EmptyState¶
File: Components/ui/EmptyState.jsx
Used In: Customer list when no results, ticket list when empty, reports with no data
Displays a friendly empty state when a list or section has no data.
Props:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| icon | ReactNode | No | — | Icon to display (Heroicon component) |
| title | string | Yes | — | Main empty state heading |
| description | string | No | — | Supporting description |
| action | ReactNode | No | — | CTA button (e.g., "Create first customer") |
Usage Example:
<EmptyState
icon={<UsersIcon className="w-12 h-12 text-gray-400" />}
title="No customers found"
description="No customers match your current filters. Try adjusting your search."
action={
<PrimaryButton onClick={() => router.visit(route('customers.create'))}>
Add First Customer
</PrimaryButton>
}
/>
ui/StatCard¶
File: Components/ui/StatCard.jsx
Used In: Dashboard KPI row (Total Customers, Active, Online Now, Revenue)
A pre-built card component for displaying a single metric with an icon, value, and trend.
Props:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| title | string | Yes | — | Metric label |
| value | string/number | Yes | — | Metric value (formatted) |
| icon | ReactNode | No | — | Icon component |
| trend | object | No | — | { direction: 'up'/'down', percent: 8.1 } |
| color | string | No | "ember" |
Brand color token |
| loading | boolean | No | false |
Shows skeleton while loading |
Usage Example:
<StatCard
title="Online Now"
value={stats.online_now}
icon={<WifiIcon className="w-6 h-6" />}
trend={{ direction: 'up', percent: 12 }}
color="success"
/>
<StatCard
title="Monthly Revenue"
value={`PKR ${stats.monthly_revenue.toLocaleString()}`}
icon={<CurrencyRupeeIcon className="w-6 h-6" />}
color="ember"
/>
Component Usage Map¶
Quick reference for which component to use in each context:
| Use Case | Component |
|---|---|
| Show customer account state | StatusPill |
| Confirm destructive action | Modal + DangerButton |
| Navigation link in sidebar | NavLink |
| Show historical bandwidth | BandwidthChart |
| Show real-time speed | LiveTrafficChart |
| Toggle dark/light mode | ThemeToggle |
| Send WhatsApp button | WhatsAppIcon + button |
| Form text field | TextInput + InputLabel + InputError |
| Open dropdown menu | Dropdown |
| Dashboard metric | StatCard |
| Loading state | Spinner |
| No data state | EmptyState |
| Content container | Card |
| Short label | Badge |
Adding New Components¶
When adding a new component to the library:
- File naming: Use PascalCase —
MyComponent.jsx - Location:
resources/js/Components/orresources/js/Components/ui/for primitives - Props documentation: Add JSDoc comment block at the top of the file
- Dark mode: Always add
dark:variants for all color classes - Accessibility: Add appropriate ARIA roles, labels, and keyboard handling
- Responsiveness: Mobile-first — test at 375px width minimum
- Export: Named export preferred (
export function MyComponent)
/**
* MyComponent — Brief description of what it does
*
* @param {string} title - Title to display
* @param {boolean} [disabled=false] - Whether the component is disabled
* @param {ReactNode} children - Child content
*/
export function MyComponent({ title, disabled = false, children }) {
return (
<div className={`... ${disabled ? 'opacity-50 pointer-events-none' : ''}`}>
<h3 className="text-sm font-semibold text-gray-900 dark:text-white">
{title}
</h3>
{children}
</div>
);
}
Last updated: 2026-07-13 | Maintained by: PyroNet Solutions Dev Team