Skip to content

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';

Displays a single metric with an icon, label, and value. Supports color variants for visual grouping.

PropTypeDefaultDescription
iconReact.ReactNoderequiredIcon element rendered inside a colored square
labelstringrequiredShort uppercase label above the value
valueReact.ReactNoderequiredThe metric value (number, string, or JSX)
colorColorVariant'primary'Color theme: 'primary', 'emerald', 'cyan', 'amber', 'red', 'purple', 'slate', 'blue', 'green', 'orange', 'gray'
valueClassNamestring'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>

Renders a page title with an optional description and action element (typically a button) aligned to the right.

PropTypeDefaultDescription
titlestringrequiredPage heading
descriptionstringSubtitle text below the title
actionReact.ReactNodeRight-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>
}
/>

A container for grouping related content, with an optional title header and action element.

PropTypeDefaultDescription
titlestringSection heading (uppercase, small text)
childrenReact.ReactNoderequiredCard content
headerActionReact.ReactNodeElement rendered to the right of the title
variant'default' | 'danger''default'Visual style; 'danger' adds a red border
classNamestring''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>

A horizontal label-value row with a leading icon. Use it inside SectionCard to display structured details.

PropTypeDefaultDescription
iconReact.ReactNoderequiredIcon in a bordered square
labelstringrequiredUppercase label
valueReact.ReactNoderequiredDisplay value
monobooleanfalseUse 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>

A Chakra UI modal for confirming destructive or important actions. Renders a title, body content, and two buttons (cancel and confirm).

PropTypeDefaultDescription
isOpenbooleanrequiredWhether the modal is visible
onClose() => voidrequiredCalled when the modal is dismissed
onConfirm() => voidrequiredCalled when the confirm button is clicked
titlestringrequiredModal heading
childrenReact.ReactNoderequiredModal body content
confirmLabelstring'Confirm'Text for the confirm button
cancelLabelstring'Cancel'Text for the cancel button
variant'primary' | 'danger''primary'Button color scheme
isLoadingbooleanfalseShow 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>
</>
);
}

A centered spinner with an optional text label. Uses the Chakra UI Spinner component.

PropTypeDefaultDescription
size'sm' | 'md' | 'lg' | 'xl''xl'Spinner size
textstringLoading message below the spinner
import { LoadingState } from '@cruzjs/ui';
// Full-page loading
if (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>

A centered message for when a list or section has no data. Supports an optional icon and action element.

PropTypeDefaultDescription
messagestringrequiredDescriptive text
iconReact.ReactNodeIcon rendered in a circular background
actionReact.ReactNodeCall-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>
}
/>

A warning state for when the current user lacks permission to view or interact with a resource.

PropTypeDefaultDescription
messagestringrequiredExplanation of why access is denied
actionLabelstringOptional button text
onAction() => voidClick 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 />;
}

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>
);
}