Component Library
The @cruzjs/ui package provides a set of composable components for building dashboard pages, settings views, and organization interfaces. All components use Tailwind CSS for styling and Chakra UI where interactive behavior (modals, spinners) is needed.
import { StatCard, PageHeader, SectionCard, DetailRow, ConfirmModal, LoadingState, EmptyState, PermissionDenied,} from '@cruzjs/ui';StatCard
Section titled “StatCard”Displays a single metric with an icon, label, and value. Supports color variants for visual grouping.
| Prop | Type | Default | Description |
|---|---|---|---|
icon | React.ReactNode | required | Icon element rendered inside a colored square |
label | string | required | Short uppercase label above the value |
value | React.ReactNode | required | The metric value (number, string, or JSX) |
color | ColorVariant | 'primary' | Color theme: 'primary', 'emerald', 'cyan', 'amber', 'red', 'purple', 'slate', 'blue', 'green', 'orange', 'gray' |
valueClassName | string | 'text-slate-900' | Custom class for the value text |
import { StatCard } from '@cruzjs/ui';
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> <StatCard icon={<UsersIcon className="w-5 h-5 text-white" />} label="Total Users" value={1_284} color="primary" /> <StatCard icon={<CheckCircleIcon className="w-5 h-5 text-white" />} label="Active" value="98.2%" color="emerald" /> <StatCard icon={<AlertIcon className="w-5 h-5 text-white" />} label="Errors (24h)" value={3} color="red" /> <StatCard icon={<DollarIcon className="w-5 h-5 text-white" />} label="MRR" value="$12,400" color="cyan" /></div>PageHeader
Section titled “PageHeader”Renders a page title with an optional description and action element (typically a button) aligned to the right.
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | required | Page heading |
description | string | — | Subtitle text below the title |
action | React.ReactNode | — | Right-aligned action element |
import { PageHeader } from '@cruzjs/ui';
<PageHeader title="Team Members" description="Manage who has access to this organization." action={ <button onClick={onInvite} className="px-4 py-2 bg-[#003DCC] text-white font-medium rounded-lg hover:bg-[#0031A3] transition-colors" > Invite Member </button> }/>SectionCard
Section titled “SectionCard”A container for grouping related content, with an optional title header and action element.
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | — | Section heading (uppercase, small text) |
children | React.ReactNode | required | Card content |
headerAction | React.ReactNode | — | Element rendered to the right of the title |
variant | 'default' | 'danger' | 'default' | Visual style; 'danger' adds a red border |
className | string | '' | Additional CSS classes |
import { SectionCard } from '@cruzjs/ui';
{/* Standard section */}<SectionCard title="General Settings"> <div className="space-y-4"> <label className="block text-sm text-slate-600"> Organization Name <input className="mt-1 block w-full rounded-lg border border-slate-300 px-3 py-2" /> </label> </div></SectionCard>
{/* Danger section for destructive actions */}<SectionCard title="Danger Zone" variant="danger" headerAction={ <button className="text-sm text-red-600 hover:underline"> Delete Organization </button> }> <p className="text-sm text-slate-600"> Deleting this organization will permanently remove all data, members, and resources. </p></SectionCard>DetailRow
Section titled “DetailRow”A horizontal label-value row with a leading icon. Use it inside SectionCard to display structured details.
| Prop | Type | Default | Description |
|---|---|---|---|
icon | React.ReactNode | required | Icon in a bordered square |
label | string | required | Uppercase label |
value | React.ReactNode | required | Display value |
mono | boolean | false | Use monospace font for the value (IDs, codes) |
import { SectionCard, DetailRow } from '@cruzjs/ui';
<SectionCard title="Organization Details"> <div className="space-y-4"> <DetailRow icon={<BuildingIcon className="w-5 h-5 text-slate-500" />} label="Organization" value="Acme Corp" /> <DetailRow icon={<HashIcon className="w-5 h-5 text-slate-500" />} label="ID" value="org_a1b2c3d4" mono /> <DetailRow icon={<CalendarIcon className="w-5 h-5 text-slate-500" />} label="Created" value="January 15, 2025" /> <DetailRow icon={<UsersIcon className="w-5 h-5 text-slate-500" />} label="Members" value="12 members" /> </div></SectionCard>ConfirmModal
Section titled “ConfirmModal”A Chakra UI modal for confirming destructive or important actions. Renders a title, body content, and two buttons (cancel and confirm).
| Prop | Type | Default | Description |
|---|---|---|---|
isOpen | boolean | required | Whether the modal is visible |
onClose | () => void | required | Called when the modal is dismissed |
onConfirm | () => void | required | Called when the confirm button is clicked |
title | string | required | Modal heading |
children | React.ReactNode | required | Modal body content |
confirmLabel | string | 'Confirm' | Text for the confirm button |
cancelLabel | string | 'Cancel' | Text for the cancel button |
variant | 'primary' | 'danger' | 'primary' | Button color scheme |
isLoading | boolean | false | Show loading spinner on confirm button |
import { useState } from 'react';import { ConfirmModal } from '@cruzjs/ui';import { trpc } from '~/trpc/client';
function DeleteMemberButton({ memberId }: { memberId: string }) { const [isOpen, setIsOpen] = useState(false); const removeMember = trpc.member.remove.useMutation({ onSuccess: () => setIsOpen(false), });
return ( <> <button onClick={() => setIsOpen(true)} className="text-red-600 hover:underline"> Remove </button> <ConfirmModal isOpen={isOpen} onClose={() => setIsOpen(false)} onConfirm={() => removeMember.mutate({ memberId })} title="Remove Member" confirmLabel="Remove" variant="danger" isLoading={removeMember.isPending} > <p>Are you sure you want to remove this member? They will lose access to all organization resources.</p> </ConfirmModal> </> );}LoadingState
Section titled “LoadingState”A centered spinner with an optional text label. Uses the Chakra UI Spinner component.
| Prop | Type | Default | Description |
|---|---|---|---|
size | 'sm' | 'md' | 'lg' | 'xl' | 'xl' | Spinner size |
text | string | — | Loading message below the spinner |
import { LoadingState } from '@cruzjs/ui';
// Full-page loadingif (isLoading) { return <LoadingState size="xl" text="Loading organization..." />;}
// Inline loading within a section<SectionCard title="Recent Activity"> {isLoading ? ( <LoadingState size="md" /> ) : ( <ActivityList items={data.activities} /> )}</SectionCard>EmptyState
Section titled “EmptyState”A centered message for when a list or section has no data. Supports an optional icon and action element.
| Prop | Type | Default | Description |
|---|---|---|---|
message | string | required | Descriptive text |
icon | React.ReactNode | — | Icon rendered in a circular background |
action | React.ReactNode | — | Call-to-action element (button, link) |
import { EmptyState } from '@cruzjs/ui';
<EmptyState message="No team members yet. Invite someone to get started." icon={<UsersIcon className="w-8 h-8 text-slate-400" />} action={ <button onClick={onInvite} className="px-4 py-2 bg-[#003DCC] text-white font-medium rounded-lg hover:bg-[#0031A3] transition-colors" > Invite Member </button> }/>PermissionDenied
Section titled “PermissionDenied”A warning state for when the current user lacks permission to view or interact with a resource.
| Prop | Type | Default | Description |
|---|---|---|---|
message | string | required | Explanation of why access is denied |
actionLabel | string | — | Optional button text |
onAction | () => void | — | Click handler for the action button |
import { PermissionDenied } from '@cruzjs/ui';import { useNavigate } from 'react-router';
function SettingsPage({ userRole }: { userRole: string }) { const navigate = useNavigate();
if (userRole !== 'OWNER' && userRole !== 'ADMIN') { return ( <PermissionDenied message="You need admin access to manage organization settings." actionLabel="Back to Overview" onAction={() => navigate('..')} /> ); }
return <SettingsForm />;}Composing Components
Section titled “Composing Components”These components are designed to work together. A typical page combines several of them:
import { trpc } from '~/trpc/client';import { PageHeader, SectionCard, StatCard, DetailRow, LoadingState, EmptyState,} from '@cruzjs/ui';
export default function ProjectOverviewPage() { const { data, isLoading } = trpc.project.overview.useQuery();
if (isLoading) return <LoadingState size="xl" text="Loading project..." />;
return ( <div className="space-y-6"> <PageHeader title={data.name} description={data.description} />
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <StatCard icon={<TaskIcon />} label="Tasks" value={data.taskCount} color="primary" /> <StatCard icon={<CheckIcon />} label="Completed" value={data.completedCount} color="emerald" /> <StatCard icon={<ClockIcon />} label="In Progress" value={data.inProgressCount} color="amber" /> </div>
<SectionCard title="Project Details"> <div className="space-y-4"> <DetailRow icon={<CalendarIcon />} label="Created" value={data.createdAt} /> <DetailRow icon={<HashIcon />} label="Project ID" value={data.id} mono /> </div> </SectionCard>
<SectionCard title="Recent Activity"> {data.activities.length === 0 ? ( <EmptyState message="No activity yet." /> ) : ( <ul className="divide-y divide-slate-100"> {data.activities.map((a) => ( <li key={a.id} className="py-3 text-sm text-slate-600">{a.description}</li> ))} </ul> )} </SectionCard> </div> );}