Overview
Matter Vault uses a system of shared components that enforce consistent design across the entire application. Every authenticated page, panel, modal, badge, and form field must use these components.
If a component doesn’t support what you need, extend it — don’t bypass it.
Page Structure
Used on every authenticated page. Renders the H1 title and gold rule divider. Never build a page header inline.
import { PageHeader } from "@/components/shared/page-header";
<PageHeader
title={<>All <span className="text-gold italic">Matters</span></>}
subtitle="Active and archived matters for this firm."
size="lg"
action={<Button variant="default" size="sm">+ New Matter</Button>}
/>
| Prop | Type | Default | Notes |
|---|
title | ReactNode | required | Wrap emphasis word in <span className="text-gold italic"> |
subtitle | ReactNode | — | Plain prose below the title |
action | ReactNode | — | Right-aligned button |
size | "lg" | "md" | "lg" | lg = top-level pages, md = inner tabs |
Eyebrow labels are not used. The breadcrumb provides page context.
Used on every panel, table, or list section. Companion to PanelFooter.
import { PanelHeader } from "@/components/ui/panel-header";
<PanelHeader
title="Documents"
count={131}
unit="file"
actions={<Button variant="outline" size="sm">Export</Button>}
/>
| Prop | Type | Notes |
|---|
title | ReactNode | Panel heading |
count | number | string | Shown after title in muted mono |
unit | string | Auto-pluralized |
actions | ReactNode | Right-aligned button cluster |
density | "default" | "compact" | py-4 vs py-3 |
Used on every panel with pagination or item counts.
import { PanelFooter } from "@/components/ui/panel-footer";
<PanelFooter
showing={25}
total={131}
unit="item"
/>
Field
Wraps every form label + input pair. Handles label, required indicator, hint text, and error messages.
import { Field, FIELD_INPUT_CLASS } from "@/components/ui/field";
<Field label="First Name" htmlFor="first-name" required>
<input
id="first-name"
autoFocus
className={FIELD_INPUT_CLASS}
/>
</Field>
| Prop | Type | Notes |
|---|
label | string | Required |
htmlFor | string | Must match input id |
required | boolean | Shows red asterisk |
hint | ReactNode | Helper text below input |
error | ReactNode | Error message in red |
Always import — never redeclare locally:
import {
FIELD_INPUT_CLASS, // standard height
FIELD_INPUT_CLASS_COMPACT, // h-9 compact variant
FIELD_LABEL_CLASS, // raw label class (prefer <Field>)
} from "@/components/ui/field";
FieldRow
Used for display-mode label + value pairs in detail cards and sidebars.
import { FieldRow } from "@/components/dashboard/field-row";
<FieldRow label="Firm" value={me.firm.name} />
<FieldRow label="Matter Number" value="MV-2026-011" mono />
<FieldRow label="Notes" value={matter.notes} layout="stacked" />
Modals
Modal
All modals use this wrapper. Never use DialogContent directly.
import { Modal } from "@/components/ui/modal";
<Modal
open={open}
onOpenChange={setOpen}
title="Add Contact"
size="md"
disableOutsideClick={true}
footer={
<>
<button onClick={() => setOpen(false)} className={cancelClass}>
Cancel
</button>
<Button
variant="default"
disabled={!isValid || isPending}
>
Save contact
</Button>
</>
}
>
<Field label="First Name" htmlFor="first-name" required>
<input id="first-name" autoFocus className={FIELD_INPUT_CLASS} />
</Field>
</Modal>
| Size | Max width | Use for |
|---|
sm | max-w-sm | Confirmation modals |
md | max-w-lg | Standard form modals (default) |
lg | max-w-2xl | Large form modals |
xl | max-w-4xl | Wizards and multi-step flows |
Enforced behaviors:
- ESC always closes
- Click outside: blocked on form modals (
disableOutsideClick={true}), allowed on read-only modals
- First input gets
autoFocus
- Submit always has
disabled={!isValid || isPending}
- No eyebrow text inside modals
- Always vertically centered
ConfirmModal
Every destructive action must use this. Never show a delete button without confirmation.
import { ConfirmModal } from "@/components/ui/confirm-modal";
<ConfirmModal
open={confirmOpen}
onOpenChange={setConfirmOpen}
title="Delete document?"
description="This will permanently remove the document from this matter."
confirmLabel="Delete document"
onConfirm={handleDelete}
isPending={isDeleting}
/>
The user must check “I understand this action cannot be undone” before the delete button enables.
Badges
Badge
All colored chips, status labels, and role badges use this component.
import { Badge } from "@/components/ui/badge";
<Badge tone="teal">Active</Badge>
<Badge tone="red">Legal Hold</Badge>
<Badge tone="gold">In Review</Badge>
<Badge tone="muted">Closed</Badge>
<Badge tone="blue">Medical Records</Badge>
<Badge tone="amber">Low Balance</Badge>
| Tone | Use for |
|---|
teal | Active, success, confirmed, clean |
red | Error, critical, hold, adversarial |
gold | Warning, pending, review, draft |
amber | Caution, low balance |
muted | Closed, inactive, terminal stages |
blue | Document categories, informational |
neutral | Custom labels with no semantic color |
Empty States
EmptyState
All empty panel states use this. Never build inline empty states.
import { EmptyState } from "@/components/ui/empty-state";
<EmptyState preset="no-documents" size="md" />
<EmptyState preset="no-contacts" size="sm" />
<EmptyState preset="no-activity" size="row" />
The row size renders a single italic line for in-panel one-liners.
Banned Patterns
Never do these. If you see them in existing code, fix them.
| Pattern | Use instead |
|---|
Local INPUT_CLASS / LABEL_CLASS consts | Import from components/ui/field.tsx |
| Inline page headers | <PageHeader> |
| Eyebrow text inside modals | Remove — never use |
text-[9px] or smaller | Minimum is text-[10px] |
| Hardcoded hex colors | Token classes only (text-gold, bg-teal) |
bg-raised on inputs | bg-surface only |
variant="default" on non-primary actions | variant="outline" or variant="ghost" |
Raw <button> for icon affordances | <Button variant="ghost" size="icon"> |
| Delete without confirmation | <ConfirmModal> |
DialogContent directly | <Modal> wrapper |
font-semibold on serif headings | Remove — no-op on Cormorant Garamond |
| Local badge color maps | <Badge tone="..."> |
text-4xl on inner tab pages | text-2xl via <PageHeader size="md"> |