Install this example’s dependencies, load the package stylesheet from Setup, and add Example CSS where shown. The snippet includes its local helpers.
Copy JSX "use client";
import { Plus } from "lucide-react";
import { type ReactNode, useState } from "react";
import { Badge } from "@kongmy-dev/sapphire-ui/badge";
import { Button } from "@kongmy-dev/sapphire-ui/button";
import { Card } from "@kongmy-dev/sapphire-ui/card";
import { Dialog, DialogBody, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@kongmy-dev/sapphire-ui/dialog";
import { Empty } from "@kongmy-dev/sapphire-ui/empty";
import { FormField } from "@kongmy-dev/sapphire-ui/form-field";
import { Input } from "@kongmy-dev/sapphire-ui/input";
import { NativeSelect } from "@kongmy-dev/sapphire-ui/native-select";
import { PageHeader } from "@kongmy-dev/sapphire-ui/page-header";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@kongmy-dev/sapphire-ui/table";
import { Archive, ChevronDown, FolderKanban, LayoutDashboard, Settings } from "lucide-react";
import { useId } from "react";
import { Breadcrumb } from "@kongmy-dev/sapphire-ui/breadcrumb";
import { Checkbox } from "@kongmy-dev/sapphire-ui/checkbox";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@kongmy-dev/sapphire-ui/dropdown-menu";
import { FormSection } from "@kongmy-dev/sapphire-ui/form-section";
import { Stat } from "@kongmy-dev/sapphire-ui/stat";
import { StatGroup } from "@kongmy-dev/sapphire-ui/stat-group";
import { ThemeToggle } from "@kongmy-dev/sapphire-ui/theme-toggle";
import { AdminHeader, AdminLayout, AdminSidebar, AdminSidebarGroup, AdminSidebarLink, AdminSidebarSection } from "@kongmy-dev/sapphire-ui/admin-layout";
export const initialProjects = [
{
name: 'Customer portal',
client: 'Northstar',
status: 'Active',
updated: 'Today',
owner: 'AR',
},
{
name: 'Inventory refresh',
client: 'Fieldwork',
status: 'Active',
updated: 'Today',
owner: 'JL',
},
{
name: 'Operations dashboard',
client: 'Meridian',
status: 'Review',
updated: 'Yesterday',
owner: 'AR',
},
{
name: 'Brand website',
client: 'Northstar',
status: 'Active',
updated: '22 Oct',
owner: 'SK',
},
{
name: 'Billing integration',
client: 'Fieldwork',
status: 'Review',
updated: '21 Oct',
owner: 'JL',
},
{
name: 'Member directory',
client: 'Meridian',
status: 'Archived',
updated: '18 Oct',
owner: 'SK',
},
] as const;
export type Project = {
name: string;
client: string;
status: string;
updated: string;
owner: string;
};
export function NewProject({
onCreate,
}: {
onCreate: (project: Project) => void;
}) {
const [open, setOpen] = useState(false);
const [name, setName] = useState('');
const [error, setError] = useState('');
function changeOpen(next: boolean) {
setOpen(next);
if (!next) {
setName('');
setError('');
}
}
return (
<Dialog open={open} onOpenChange={changeOpen}>
<DialogTrigger asChild>
<Button icon={<Plus className="sp-icon" aria-hidden="true" />}>
New project
</Button>
</DialogTrigger>
<DialogContent>
<form
noValidate
onSubmit={(event) => {
event.preventDefault();
if (!name.trim()) {
setError('Enter a project name.');
return;
}
onCreate({
name: name.trim(),
client: 'Your workspace',
status: 'Active',
updated: 'Just now',
owner: 'AR',
});
changeOpen(false);
}}
>
<DialogHeader>
<DialogTitle>Create project</DialogTitle>
<DialogDescription>
Start a project for your team.
</DialogDescription>
</DialogHeader>
<DialogBody>
<FormField label="Project name" required error={error}>
<Input
value={name}
onChange={(event) => {
setName(event.target.value);
setError('');
}}
/>
</FormField>
</DialogBody>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => changeOpen(false)}
>
Cancel
</Button>
<Button type="submit">Create project</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
export function ProjectTable({ projects }: { projects: readonly Project[] }) {
return (
<Table aria-label="Projects">
<TableHeader>
<TableRow>
<TableHead scope="col">Project</TableHead>
<TableHead scope="col">Status</TableHead>
<TableHead scope="col">Owner</TableHead>
<TableHead scope="col">Updated</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{projects.map((project) => (
<TableRow key={project.name}>
<TableCell>
<strong>{project.name}</strong>
<span className="demo-project-client">{project.client}</span>
</TableCell>
<TableCell>
<Badge
variant={project.status === 'Active' ? 'success' : 'default'}
>
{project.status}
</Badge>
</TableCell>
<TableCell>
<span
className="demo-workspace-avatar"
role="img"
aria-label={`Owner ${project.owner}`}
>
{project.owner}
</span>
</TableCell>
<TableCell className="demo-project-updated">
{project.updated}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}
export function WorkspaceProjects({
projects,
actions,
archived,
}: {
projects: readonly Project[];
actions: ReactNode;
archived: boolean;
}) {
const [search, setSearch] = useState('');
const [status, setStatus] = useState(archived ? 'Archived' : 'All');
const [page, setPage] = useState(0);
const filtered = projects.filter(
(project) =>
`${project.name} ${project.client}`
.toLowerCase()
.includes(search.toLowerCase()) &&
(status === 'All' || project.status === status),
);
const pageCount = Math.max(1, Math.ceil(filtered.length / 4));
const currentPage = Math.min(page, pageCount - 1);
return (
<>
<PageHeader
title={archived ? 'Archived projects' : 'Projects'}
description="Find a project and see what needs attention."
actions={actions}
/>
<Card padding="none">
<div className="demo-workspace-filters">
<Input
type="search"
aria-label="Search projects"
placeholder="Search projects or clients…"
value={search}
onChange={(event) => {
setSearch(event.target.value);
setPage(0);
}}
/>
<NativeSelect
aria-label="Project status"
value={status}
onChange={(event) => {
setStatus(event.target.value);
setPage(0);
}}
>
<option value="All">All statuses</option>
<option>Active</option>
<option>Review</option>
<option>Archived</option>
</NativeSelect>
</div>
{filtered.length ? (
<ProjectTable
projects={filtered.slice(currentPage * 4, currentPage * 4 + 4)}
/>
) : (
<Empty
title="No projects found"
description="Try another search or clear your filters."
action={
<Button
variant="outline"
onClick={() => {
setSearch('');
setStatus('All');
}}
>
Clear filters
</Button>
}
/>
)}
<ProjectPagination
currentPage={currentPage}
pageCount={pageCount}
total={filtered.length}
onPageChange={setPage}
/>
</Card>
</>
);
}
function ProjectPagination({
currentPage,
pageCount,
total,
onPageChange,
}: {
currentPage: number;
pageCount: number;
total: number;
onPageChange: (page: number) => void;
}) {
return (
<div className="demo-workspace-pagination">
<span role="status">
{total} projects · Page {currentPage + 1} of {pageCount}
</span>
<div className="sp-cluster">
<Button
variant="outline"
size="sm"
disabled={currentPage === 0}
onClick={() => onPageChange(currentPage - 1)}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
disabled={currentPage + 1 === pageCount}
onClick={() => onPageChange(currentPage + 1)}
>
Next
</Button>
</div>
</div>
);
}
type Page = 'overview' | 'projects' | 'archived' | 'settings';
const titles = {
overview: 'Overview',
projects: 'Projects',
archived: 'Archived projects',
settings: 'Workspace settings',
} as const;
function WorkspaceBrand({ compact }: { compact: boolean }) {
return (
<a
className="demo-workspace-identity"
href="#workspace"
aria-label={compact ? 'Sapphire workspace' : 'S Sapphire Team workspace'}
>
<span className="demo-workspace-mark" aria-hidden="true">
S
</span>{' '}
{!compact && (
<span>
<strong>Sapphire</strong> <small>Team workspace</small>
</span>
)}
</a>
);
}
function WorkspaceAccount({
compact,
onSettings,
}: {
compact: boolean;
onSettings: () => void;
}) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="demo-workspace-identity demo-workspace-account"
aria-label={
compact
? 'AR Aisha Rahman account'
: 'AR Aisha Rahman Workspace admin account'
}
>
<span className="demo-workspace-avatar" aria-hidden="true">
AR
</span>{' '}
{!compact && (
<>
<span>
<strong>Aisha Rahman</strong> <small>Workspace admin</small>
</span>
<ChevronDown className="sp-icon" aria-hidden="true" />
</>
)}
</button>
</DropdownMenuTrigger>
<DropdownMenuContent side="right" align="end">
<DropdownMenuItem onSelect={onSettings}>
Workspace settings
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
function WorkspaceSidebar({
page,
navigate,
projectCount,
helpId,
}: {
page: Page;
navigate: (page: Page) => void;
projectCount: number;
helpId: string;
}) {
return (
<AdminSidebar
aria-label="Workspace navigation"
brand={WorkspaceBrand}
footer={({ compact }) => (
<WorkspaceAccount
compact={compact}
onSettings={() => navigate('settings')}
/>
)}
>
<AdminSidebarSection title="Workspace">
<AdminSidebarLink
href="#overview"
active={page === 'overview'}
icon={<LayoutDashboard className="sp-icon" />}
onClick={() => navigate('overview')}
>
Overview
</AdminSidebarLink>
<AdminSidebarLink
href="#projects"
active={page === 'projects'}
badge={projectCount}
icon={<FolderKanban className="sp-icon" />}
onClick={() => navigate('projects')}
>
Projects
</AdminSidebarLink>
<AdminSidebarGroup
label="Library"
icon={<Archive className="sp-icon" />}
active={page === 'archived'}
>
<AdminSidebarLink
href="#all-projects"
onClick={() => navigate('projects')}
>
All projects
</AdminSidebarLink>
<AdminSidebarLink
href="#archived-projects"
active={page === 'archived'}
onClick={() => navigate('archived')}
>
Archived projects
</AdminSidebarLink>
</AdminSidebarGroup>
</AdminSidebarSection>
<AdminSidebarSection title="Manage">
<AdminSidebarLink
href="#settings"
active={page === 'settings'}
icon={<Settings className="sp-icon" />}
onClick={() => navigate('settings')}
>
Settings
</AdminSidebarLink>
<AdminSidebarLink href={`#${helpId}`}>Help</AdminSidebarLink>
</AdminSidebarSection>
</AdminSidebar>
);
}
function WorkspaceOverview({
projects,
actions,
navigate,
}: {
projects: readonly Project[];
actions: ReactNode;
navigate: (page: Page) => void;
}) {
return (
<>
<PageHeader
title="Workspace overview"
description="Your projects, priorities and recent activity in one place."
actions={actions}
/>
<StatGroup cols={3}>
<Stat
label="Active projects"
value={
projects.filter((project) => project.status === 'Active').length
}
description="Across three clients"
/>
<Stat
label="Awaiting review"
value="2"
description="Ready for your feedback"
/>
<Stat
label="Team members"
value="8"
description="3 collaborators online"
/>
</StatGroup>
<div className="demo-workspace-overview">
<Card padding="none">
<div className="demo-workspace-card-heading">
<h2>Recent projects</h2>
<Button
variant="ghost"
size="sm"
onClick={() => navigate('projects')}
>
View all projects
</Button>
</div>
<ProjectTable projects={projects.slice(0, 4)} />
</Card>
<Card>
<h2>Recent activity</h2>
<ol className="demo-workspace-activity">
<li>
<strong>Customer portal updated</strong>
<span>Aisha added the onboarding flow.</span>
<small>12 minutes ago</small>
</li>
<li>
<strong>Dashboard ready for review</strong>
<span>James shared the latest version.</span>
<small>1 hour ago</small>
</li>
<li>
<strong>New teammate joined</strong>
<span>Sarah joined the design team.</span>
<small>Today, 09:15</small>
</li>
</ol>
</Card>
</div>
</>
);
}
function WorkspaceSettings() {
const [name, setName] = useState('Sapphire');
const [error, setError] = useState('');
const [saved, setSaved] = useState(false);
return (
<>
<PageHeader
title="Workspace settings"
description="Manage the details your team shares."
/>
<Card className="demo-workspace-settings">
<form
noValidate
onSubmit={(event) => {
event.preventDefault();
setError(name.trim() ? '' : 'Enter a workspace name.');
setSaved(Boolean(name.trim()));
}}
>
<FormSection
legend="General"
description="These details appear throughout your workspace."
>
<FormField
label="Workspace name"
required
error={error}
hint="Choose a name your team will recognize."
>
<Input
value={name}
onChange={(event) => {
setName(event.target.value);
setSaved(false);
}}
/>
</FormField>
<FormField label="Timezone">
<NativeSelect
defaultValue="Asia/Kuala_Lumpur"
name="timezone"
onChange={() => setSaved(false)}
>
<option value="Asia/Kuala_Lumpur">Kuala Lumpur (UTC+8)</option>
<option value="Europe/London">London</option>
</NativeSelect>
</FormField>
</FormSection>
<FormSection
legend="Notifications"
description="Choose what reaches your inbox."
>
<Checkbox
label="Weekly project summary"
description="A digest of progress and upcoming reviews."
defaultChecked
name="weekly"
onCheckedChange={() => setSaved(false)}
/>
<Checkbox
label="Project review requests"
description="An email when a teammate needs your feedback."
defaultChecked
name="reviews"
onCheckedChange={() => setSaved(false)}
/>
</FormSection>
<div className="demo-workspace-save">
<Button type="submit">Save changes</Button>
<span role="status">
{saved ? 'Workspace settings saved.' : ''}
</span>
</div>
</form>
</Card>
</>
);
}
function WorkspaceDemo({
initialPage = 'overview',
compact = false,
}: {
initialPage?: Page;
compact?: boolean;
}) {
const [page, setPage] = useState(initialPage);
const helpId = useId();
const [projects, setProjects] = useState<readonly Project[]>(initialProjects);
const actions = (
<NewProject
onCreate={(project) => {
setProjects((current) => [project, ...current]);
setPage('projects');
}}
/>
);
return (
<AdminLayout
defaultDesktopState={compact ? 'compact' : 'expanded'}
sidebar={
<WorkspaceSidebar
page={page}
navigate={setPage}
projectCount={projects.length}
helpId={helpId}
/>
}
header={
<AdminHeader
leftSlot={
<strong className="demo-workspace-app-name">Sapphire</strong>
}
breadcrumbs={<Breadcrumb items={[{ label: titles[page] }]} />}
rightSlot={<ThemeToggle />}
/>
}
>
{page === 'overview' && (
<WorkspaceOverview
projects={projects}
actions={actions}
navigate={setPage}
/>
)}
{(page === 'projects' || page === 'archived') && (
<WorkspaceProjects
key={page}
projects={projects}
actions={actions}
archived={page === 'archived'}
/>
)}
{page === 'settings' && <WorkspaceSettings />}
<p id={helpId} className="demo-workspace-help">
Need a hand? Contact your workspace administrator.
</p>
</AdminLayout>
);
}
function AdminWorkspace() {
return <WorkspaceDemo />;
}
export default AdminWorkspace;