Parcel: productivity workspace
A five-app productivity suite in one shell: a Today page with agenda, tasks and a focus timer, plus a full mail client, week calendar, drag-and-drop board and settings. A tab bar on phones.
// Ballmac UI: Parcel workspace template fonts. https://ui.ballmac.com/templates/template-workspace
import { Albert_Sans } from "next/font/google"
export const workspaceSans = Albert_Sans({ variable: "--workspace-sans", subsets: ["latin"], display: "swap", preload: false })// Ballmac UI: Parcel workspace template shell. https://ui.ballmac.com/templates/template-workspace
"use client"
import * as React from "react"
import { CalendarDays, CheckSquare, Command as CommandIcon, Inbox, LayoutGrid, Plus, Search, Settings } from "lucide-react"
import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ballmac/command"
import { workspaceSans } from "@/components/ballmac/templates/workspace/workspace-fonts"
import { cn } from "@/lib/utils"
type WorkspacePage = "today" | "mail" | "calendar" | "board" | "settings"
type WorkspaceHrefs = Record<WorkspacePage, string>
const defaultHrefs: WorkspaceHrefs = { today: "/workspace", mail: "/workspace/mail", calendar: "/workspace/calendar", board: "/workspace/board", settings: "/workspace/settings" }
/** Parcel's palette: cool paper and navy ink with one blue and a touch of amber. */
const workspaceCss = `
.workspace-theme,body:not(:has([data-gallery])):has(.workspace-theme){--background:oklch(0.982 0.004 250);--foreground:oklch(0.21 0.03 262);--card:oklch(1 0 0);--card-foreground:oklch(0.21 0.03 262);--popover:oklch(1 0 0);--popover-foreground:oklch(0.21 0.03 262);--primary:oklch(0.28 0.05 262);--primary-foreground:oklch(0.985 0.004 250);--secondary:oklch(0.958 0.008 250);--secondary-foreground:oklch(0.21 0.03 262);--muted:oklch(0.958 0.008 250);--muted-foreground:oklch(0.48 0.03 258);--accent:oklch(0.94 0.012 250);--accent-foreground:oklch(0.21 0.03 262);--border:oklch(0.21 0.03 262 / 11%);--input:oklch(0.21 0.03 262 / 16%);--ring:oklch(0.5 0.17 258);--surface:oklch(0.966 0.007 250);--destructive:oklch(0.54 0.21 25);--chart-1:oklch(0.5 0.17 258);--chart-2:oklch(0.5 0.12 165);--chart-3:oklch(0.62 0.14 72);--chart-4:oklch(0.5 0.16 310);--chart-5:oklch(0.52 0.15 25);--radius:0.75rem}
.dark .workspace-theme,.dark body:not(:has([data-gallery])):has(.workspace-theme){--background:oklch(0.17 0.02 262);--foreground:oklch(0.95 0.008 250);--card:oklch(0.21 0.024 262);--card-foreground:oklch(0.95 0.008 250);--popover:oklch(0.23 0.026 262);--popover-foreground:oklch(0.95 0.008 250);--primary:oklch(0.95 0.008 250);--primary-foreground:oklch(0.21 0.03 262);--secondary:oklch(0.26 0.026 262);--secondary-foreground:oklch(0.95 0.008 250);--muted:oklch(0.25 0.026 262);--muted-foreground:oklch(0.73 0.025 255);--accent:oklch(0.28 0.03 262);--accent-foreground:oklch(0.95 0.008 250);--border:oklch(1 0 0 / 10%);--input:oklch(1 0 0 / 14%);--ring:oklch(0.72 0.14 258);--surface:oklch(0.19 0.022 262);--destructive:oklch(0.7 0.19 25);--chart-1:oklch(0.74 0.13 258);--chart-2:oklch(0.78 0.13 165);--chart-3:oklch(0.82 0.13 80);--chart-4:oklch(0.75 0.13 310);--chart-5:oklch(0.76 0.13 25)}
body:not(:has([data-gallery])):has(.workspace-theme){font-family:var(--workspace-sans),ui-sans-serif,system-ui,sans-serif}
`
const apps: { key: WorkspacePage; label: string; icon: typeof Inbox }[] = [
{ key: "today", label: "Today", icon: LayoutGrid },
{ key: "mail", label: "Mail", icon: Inbox },
{ key: "calendar", label: "Calendar", icon: CalendarDays },
{ key: "board", label: "Board", icon: CheckSquare },
{ key: "settings", label: "Settings", icon: Settings },
]
function ParcelMark({ className }: { className?: string }) {
return (
<svg viewBox="0 0 28 28" aria-hidden="true" className={cn("size-7", className)} fill="none">
<rect width="28" height="28" rx="8" fill="var(--chart-1)" />
<path d="M14 6 21 10v8l-7 4-7-4v-8z" stroke="var(--card)" strokeWidth="2" strokeLinejoin="round" />
<path d="M7 10l7 4 7-4M14 14v8" stroke="var(--card)" strokeWidth="2" strokeLinejoin="round" />
</svg>
)
}
type WorkspaceShellProps = Omit<React.ComponentProps<"div">, "title"> & {
/** The app being shown, so its icon is marked current. */
page: WorkspacePage
/** Override where pages live (used by previews). */
hrefs?: Partial<WorkspaceHrefs>
/** Let the content fill the window height (mail, calendar and board do). */
fill?: boolean
}
/** Parcel's frame: an app rail (a tab bar on phones), a search and quick-create bar, and a command menu on ⌘K. */
function WorkspaceShell({ page, hrefs: overrides, fill, className, style, children, ...props }: WorkspaceShellProps) {
const hrefs = { ...defaultHrefs, ...overrides }
const [palette, setPalette] = React.useState(false)
React.useEffect(() => {
const cls = workspaceSans.variable
if (!cls) return
document.body.classList.add(cls)
return () => document.body.classList.remove(cls)
}, [])
const current = apps.find((a) => a.key === page)!
return (
<div
data-slot="workspace"
className={cn("workspace-theme bg-background text-foreground flex min-h-dvh flex-col md:flex-row", fill && "h-dvh overflow-hidden", workspaceSans.variable, className)}
style={{ fontFamily: "var(--workspace-sans), ui-sans-serif, system-ui, sans-serif", ...style }}
{...props}
>
<style>{workspaceCss}</style>
<nav aria-label="Apps" className="bg-surface order-last flex shrink-0 items-center justify-around border-t px-2 py-1.5 md:order-none md:w-[4.5rem] md:flex-col md:justify-start md:gap-1 md:border-t-0 md:border-e md:py-3">
<a href={hrefs.today} aria-label="Parcel home" className="focus-visible:ring-ring/50 mb-3 hidden rounded-xl outline-none focus-visible:ring-[3px] md:block"><ParcelMark /></a>
{apps.map((a) => (
<a key={a.key} href={hrefs[a.key]} aria-current={page === a.key ? "page" : undefined} className="text-muted-foreground hover:text-foreground hover:bg-accent aria-[current=page]:bg-card aria-[current=page]:text-foreground aria-[current=page]:shadow-sm focus-visible:ring-ring/50 flex min-w-14 flex-col items-center gap-1 rounded-xl px-2 py-1.5 text-[11px] font-medium outline-none transition-colors focus-visible:ring-[3px] md:w-14">
<a.icon className="size-5" aria-hidden="true" />
{a.label}
</a>
))}
</nav>
<div className={cn("flex min-w-0 flex-1 flex-col", fill && "min-h-0")}>
<header className="flex h-14 shrink-0 items-center gap-3 border-b px-4 sm:px-6">
<h1 className="text-[15px] font-semibold tracking-tight">{current.label}</h1>
<button type="button" onClick={() => setPalette(true)} className="text-muted-foreground bg-card hover:bg-accent focus-visible:ring-ring/50 mx-auto flex h-9 w-full max-w-md items-center gap-2.5 rounded-xl border px-3 text-sm outline-none transition-colors focus-visible:ring-[3px]">
<Search className="size-4" aria-hidden="true" />
<span className="flex-1 text-start">Search or jump to…</span>
<kbd className="bg-muted hidden items-center gap-0.5 rounded px-1.5 py-0.5 text-[11px] sm:flex"><CommandIcon className="size-3" aria-hidden="true" />K</kbd>
</button>
<a href={hrefs.mail} className="bg-primary text-primary-foreground focus-visible:ring-ring/50 inline-flex h-9 shrink-0 items-center gap-1.5 rounded-xl px-3 text-sm font-semibold outline-none transition-opacity hover:opacity-90 focus-visible:ring-[3px]"><Plus className="size-4" aria-hidden="true" /><span className="hidden sm:inline">Compose</span><span className="sr-only sm:hidden">Compose</span></a>
<span className="bg-chart-4/25 flex size-9 shrink-0 items-center justify-center rounded-full text-xs font-bold" aria-hidden="true">MK</span>
</header>
<div className={cn("flex-1", fill && "min-h-0 overflow-hidden")}>{children}</div>
</div>
<CommandDialog open={palette} onOpenChange={setPalette} title="Search or jump to">
<CommandInput placeholder="Search mail, events and tasks…" />
<CommandList>
<CommandEmpty>No results.</CommandEmpty>
<CommandGroup heading="Apps">
{apps.map((a) => <CommandItem key={a.key} value={a.label} onSelect={() => { setPalette(false); window.location.assign(hrefs[a.key]) }}><a.icon aria-hidden="true" /> {a.label}</CommandItem>)}
</CommandGroup>
</CommandList>
</CommandDialog>
</div>
)
}
export { ParcelMark, WorkspaceShell, defaultHrefs as workspaceDefaultHrefs, type WorkspaceHrefs, type WorkspacePage, type WorkspaceShellProps }// Ballmac UI: Parcel today page. https://ui.ballmac.com/templates/template-workspace
"use client"
import * as React from "react"
import { Archive, Check, Pause, Play, Plus, Reply, RotateCcw } from "lucide-react"
import { WorkspaceShell, type WorkspaceHrefs } from "@/components/ballmac/templates/workspace/workspace-theme"
import { cn } from "@/lib/utils"
type Task = { id: string; text: string; done: boolean }
const startTasks: Task[] = [
{ id: "t1", text: "Send the Q4 plan to Priya", done: true },
{ id: "t2", text: "Review Dev’s onboarding mockups", done: false },
{ id: "t3", text: "Book the Lisbon flights", done: false },
{ id: "t4", text: "Write Friday’s team update", done: false },
]
const agenda = [
{ time: "09:00", end: "09:30", title: "Design standup", where: "Meet", tone: "bg-chart-1" },
{ time: "10:30", end: "11:30", title: "Onboarding review", where: "Room 4", tone: "bg-chart-2" },
{ time: "13:00", end: "13:45", title: "Lunch with Amara", where: "Bar Lucia", tone: "bg-chart-3" },
{ time: "15:00", end: "16:00", title: "1:1 with Dev", where: "Meet", tone: "bg-chart-4" },
]
const inbox = [
{ id: "m1", from: "Priya Raman", subject: "Re: Q4 plan — two small edits", preview: "Love it. Can we add owners to the risks?", time: "8:12 AM" },
{ id: "m2", from: "Amara Okafor", subject: "Lunch today?", preview: "Still on for 13:00 at Bar Lucia?", time: "7:48 AM" },
{ id: "m3", from: "Billing", subject: "Your October invoice", preview: "Your invoice for $48.00 is ready.", time: "Yesterday" },
]
const NOW = "09:41"
const toMin = (t: string) => Number(t.slice(0, 2)) * 60 + Number(t.slice(3))
const card = "bg-card rounded-2xl border"
type WorkspaceTodayProps = React.ComponentProps<"div"> & { hrefs?: Partial<WorkspaceHrefs> }
/** The Parcel today page: your agenda with a now line, tasks you can tick and add, inbox highlights you can archive, and a focus timer. */
function WorkspaceToday({ hrefs, ...props }: WorkspaceTodayProps) {
const [tasks, setTasks] = React.useState(startTasks)
const [draft, setDraft] = React.useState("")
const [mail, setMail] = React.useState(inbox)
const [secs, setSecs] = React.useState(25 * 60)
const [running, setRunning] = React.useState(false)
const [note, setNote] = React.useState("")
React.useEffect(() => {
if (!running) return
const id = setInterval(() => setSecs((s) => (s <= 1 ? 0 : s - 1)), 1000)
return () => clearInterval(id)
}, [running])
const finished = secs === 0
const done = tasks.filter((t) => t.done).length
const pct = tasks.length ? (done / tasks.length) * 100 : 0
const mm = String(Math.floor(secs / 60)).padStart(2, "0")
const ss = String(secs % 60).padStart(2, "0")
function add(e: React.FormEvent) {
e.preventDefault()
const text = draft.trim()
if (!text) return
setTasks((all) => [...all, { id: `n${all.length}`, text, done: false }])
setDraft("")
}
return (
<WorkspaceShell page="today" hrefs={hrefs} {...props}>
<main className="mx-auto max-w-6xl px-4 py-6 pb-10 sm:px-6">
<p className="text-muted-foreground text-sm font-medium">Thursday, October 1</p>
<h2 className="mt-1 text-3xl font-extrabold tracking-[-0.03em] sm:text-4xl">Good morning, Mina.</h2>
<p className="text-muted-foreground mt-2 text-pretty">You have {agenda.length} meetings, {tasks.length - done} tasks left and {mail.length} messages worth a look.</p>
<div className="mt-8 grid grid-cols-[minmax(0,1fr)] gap-4 lg:grid-cols-3">
<section aria-labelledby="wt-agenda" className={cn(card, "p-5 lg:row-span-2")}>
<h3 id="wt-agenda" className="font-bold">Agenda</h3>
<ol className="mt-4 space-y-1">
{agenda.map((e, i) => {
const past = toMin(e.end) <= toMin(NOW)
const live = toMin(e.time) <= toMin(NOW) && toMin(NOW) < toMin(e.end)
const showNow = !past && !live && (i === 0 || toMin(agenda[i - 1].end) <= toMin(NOW))
return (
<React.Fragment key={e.title}>
{showNow && (
<li aria-label={`Now, ${NOW}`} className="flex items-center gap-2 py-1.5"><span className="bg-destructive size-2 rounded-full" aria-hidden="true" /><span className="bg-destructive/50 h-px flex-1" aria-hidden="true" /><span className="text-destructive text-xs font-semibold tabular-nums">{NOW}</span></li>
)}
<li className={cn("flex gap-3 rounded-xl p-2.5", live && "bg-accent", past && "text-muted-foreground")}>
<span className={cn("w-1 shrink-0 rounded-full", e.tone)} aria-hidden="true" />
<div className="min-w-0 flex-1"><p className="text-sm font-semibold">{e.title}</p><p className="text-muted-foreground text-xs">{e.time}–{e.end} · {e.where}</p></div>
{past && <Check className="text-chart-2 size-4 shrink-0" aria-label="Done" />}
</li>
</React.Fragment>
)
})}
</ol>
</section>
<section aria-labelledby="wt-tasks" className={cn(card, "p-5")}>
<div className="flex items-center justify-between"><h3 id="wt-tasks" className="font-bold">Tasks</h3><p className="text-muted-foreground text-sm tabular-nums" role="status">{done} of {tasks.length} done</p></div>
<div className="bg-muted mt-3 h-1.5 rounded-full" aria-hidden="true"><div className="bg-chart-2 h-full rounded-full transition-[width] duration-500 motion-reduce:transition-none" style={{ width: `${pct}%` }} /></div>
<ul className="mt-4 space-y-1">
{tasks.map((t) => (
<li key={t.id}>
<label className="hover:bg-accent flex cursor-pointer items-center gap-3 rounded-lg px-2 py-2 text-sm">
<input type="checkbox" checked={t.done} onChange={() => setTasks((all) => all.map((x) => (x.id === t.id ? { ...x, done: !x.done } : x)))} className="accent-chart-1 size-4" />
<span className={cn(t.done && "text-muted-foreground line-through")}>{t.text}</span>
</label>
</li>
))}
</ul>
<form onSubmit={add} className="mt-3 flex gap-2">
<label htmlFor="wt-new" className="sr-only">Add a task</label>
<input id="wt-new" value={draft} onChange={(e) => setDraft(e.target.value)} placeholder="Add a task" className="bg-background focus-visible:ring-ring/50 placeholder:text-muted-foreground h-9 min-w-0 flex-1 rounded-lg border px-3 text-sm outline-none focus-visible:ring-[3px]" />
<button type="submit" aria-label="Add task" className="bg-primary text-primary-foreground focus-visible:ring-ring/50 inline-flex size-9 items-center justify-center rounded-lg outline-none transition-opacity hover:opacity-90 focus-visible:ring-[3px]"><Plus className="size-4" aria-hidden="true" /></button>
</form>
</section>
<section aria-labelledby="wt-focus" className={cn(card, "flex flex-col items-center p-5 text-center")}>
<h3 id="wt-focus" className="self-start font-bold">Focus</h3>
<p className="mt-6 text-6xl font-extrabold tracking-[-0.04em] tabular-nums" role="timer" aria-label={`${mm} minutes ${ss} seconds left`}>{mm}:{ss}</p>
<p className="text-muted-foreground mt-2 text-sm" aria-live="polite">{finished ? "Nice work. Take five." : running ? "Stay with one thing." : "Pick a task and begin."}</p>
<div className="mt-5 flex gap-2">
<button type="button" onClick={() => { if (finished) setSecs(25 * 60); setRunning((r) => !r) }} className="bg-primary text-primary-foreground focus-visible:ring-ring/50 inline-flex h-10 items-center gap-2 rounded-xl px-5 text-sm font-semibold outline-none transition-opacity hover:opacity-90 focus-visible:ring-[3px]">
{running ? <><Pause className="size-4" aria-hidden="true" />Pause</> : <><Play className="size-4" aria-hidden="true" />{secs === 25 * 60 ? "Start" : "Resume"}</>}
</button>
<button type="button" aria-label="Reset timer" onClick={() => { setRunning(false); setSecs(25 * 60) }} className="hover:bg-accent focus-visible:ring-ring/50 inline-flex size-10 items-center justify-center rounded-xl border outline-none focus-visible:ring-[3px]"><RotateCcw className="size-4" aria-hidden="true" /></button>
</div>
</section>
<section aria-labelledby="wt-inbox" className={cn(card, "p-5 lg:col-span-2")}>
<div className="flex items-center justify-between"><h3 id="wt-inbox" className="font-bold">Worth a look</h3><a href={hrefs?.mail ?? "/workspace/mail"} className="text-chart-1 rounded text-sm font-semibold underline underline-offset-4">Open inbox</a></div>
<p className="sr-only" role="status">{note}</p>
{mail.length === 0 ? (
<p className="text-muted-foreground mt-6 rounded-xl border border-dashed p-8 text-center text-sm">Inbox zero. Enjoy the quiet.</p>
) : (
<ul className="mt-3 divide-y">
{mail.map((m) => (
<li key={m.id} className="flex items-center gap-3 py-3">
<span className="bg-chart-1/15 flex size-9 shrink-0 items-center justify-center rounded-full text-xs font-bold" aria-hidden="true">{m.from.split(" ").map((w) => w[0]).join("")}</span>
<div className="min-w-0 flex-1"><p className="truncate text-sm"><span className="font-semibold">{m.from}</span> <span className="text-muted-foreground">· {m.time}</span></p><p className="truncate text-sm font-medium">{m.subject}</p><p className="text-muted-foreground truncate text-xs">{m.preview}</p></div>
<div className="flex shrink-0 gap-1">
<a href={hrefs?.mail ?? "/workspace/mail"} aria-label={`Reply to ${m.from}`} className="hover:bg-accent focus-visible:ring-ring/50 inline-flex size-8 items-center justify-center rounded-lg outline-none focus-visible:ring-[3px]"><Reply className="size-4 rtl:-scale-x-100" aria-hidden="true" /></a>
<button type="button" aria-label={`Archive: ${m.subject}`} onClick={() => { setMail((all) => all.filter((x) => x.id !== m.id)); setNote(`Archived ${m.subject}`) }} className="hover:bg-accent focus-visible:ring-ring/50 inline-flex size-8 items-center justify-center rounded-lg outline-none focus-visible:ring-[3px]"><Archive className="size-4" aria-hidden="true" /></button>
</div>
</li>
))}
</ul>
)}
</section>
</div>
</main>
</WorkspaceShell>
)
}
export { WorkspaceToday, type WorkspaceTodayProps }// Ballmac UI: Parcel mail page. https://ui.ballmac.com/templates/template-workspace
import * as React from "react"
import { Mail1 } from "@/components/ballmac/blocks/mail-1/mail-1"
import { WorkspaceShell, type WorkspaceHrefs } from "@/components/ballmac/templates/workspace/workspace-theme"
type WorkspaceMailProps = React.ComponentProps<"div"> & { hrefs?: Partial<WorkspaceHrefs> }
/** Parcel mail: the three-pane inbox with search, compose and keyboard shortcuts, filling the window. */
function WorkspaceMail({ hrefs, ...props }: WorkspaceMailProps) {
return (
<WorkspaceShell page="mail" hrefs={hrefs} fill {...props}>
<Mail1 height="100%" className="rounded-none border-0" />
</WorkspaceShell>
)
}
export { WorkspaceMail, type WorkspaceMailProps }// Ballmac UI: Parcel calendar page. https://ui.ballmac.com/templates/template-workspace
import * as React from "react"
import { Calendar1 } from "@/components/ballmac/blocks/calendar-1/calendar-1"
import { WorkspaceShell, type WorkspaceHrefs } from "@/components/ballmac/templates/workspace/workspace-theme"
type WorkspaceCalendarProps = React.ComponentProps<"div"> & { hrefs?: Partial<WorkspaceHrefs> }
/** Parcel calendar: the week view with overlapping events, a now line and a new-event dialog, filling the window. */
function WorkspaceCalendar({ hrefs, ...props }: WorkspaceCalendarProps) {
return (
<WorkspaceShell page="calendar" hrefs={hrefs} fill {...props}>
<Calendar1 height="100%" today="2026-10-01" now="09:41" className="rounded-none border-0" />
</WorkspaceShell>
)
}
export { WorkspaceCalendar, type WorkspaceCalendarProps }// Ballmac UI: Parcel board page. https://ui.ballmac.com/templates/template-workspace
import * as React from "react"
import { Kanban1 } from "@/components/ballmac/blocks/kanban-1/kanban-1"
import { WorkspaceShell, type WorkspaceHrefs } from "@/components/ballmac/templates/workspace/workspace-theme"
type WorkspaceBoardProps = React.ComponentProps<"div"> & { hrefs?: Partial<WorkspaceHrefs> }
/** Parcel board: drag cards between columns, move with the keyboard, add and filter by person. */
function WorkspaceBoard({ hrefs, ...props }: WorkspaceBoardProps) {
return (
<WorkspaceShell page="board" hrefs={hrefs} {...props}>
<Kanban1 title="Q4 launch" description="Drag cards between columns, or use the Move to menu on any card." height="calc(100dvh - 15rem)" />
</WorkspaceShell>
)
}
export { WorkspaceBoard, type WorkspaceBoardProps }// Ballmac UI: Parcel settings page. https://ui.ballmac.com/templates/template-workspace
import * as React from "react"
import { Settings1 } from "@/components/ballmac/blocks/settings-1/settings-1"
import { WorkspaceShell, type WorkspaceHrefs } from "@/components/ballmac/templates/workspace/workspace-theme"
type WorkspaceSettingsProps = React.ComponentProps<"div"> & { hrefs?: Partial<WorkspaceHrefs> }
/** Parcel settings: profile, preferences and account actions with a save bar that appears only when something changed. */
function WorkspaceSettings({ hrefs, ...props }: WorkspaceSettingsProps) {
return (
<WorkspaceShell page="settings" hrefs={hrefs} {...props}>
<Settings1 title="Your account" description="Manage your profile and how Parcel works for you." />
</WorkspaceShell>
)
}
export { WorkspaceSettings, type WorkspaceSettingsProps }// Ballmac UI: Workspace template route. https://ui.ballmac.com/templates/template-workspace
import type { Metadata } from "next"
import { WorkspaceToday } from "@/components/ballmac/templates/workspace/workspace-today"
export const metadata: Metadata = {
title: "Parcel",
description: "Mail, calendar and tasks in one calm place.",
}
export default function Page() {
return <WorkspaceToday />
}// Ballmac UI: Workspace template route. https://ui.ballmac.com/templates/template-workspace
import type { Metadata } from "next"
import { WorkspaceMail } from "@/components/ballmac/templates/workspace/workspace-mail"
export const metadata: Metadata = {
title: "Mail · Parcel",
description: "A fast three-pane inbox.",
}
export default function Page() {
return <WorkspaceMail />
}// Ballmac UI: Workspace template route. https://ui.ballmac.com/templates/template-workspace
import type { Metadata } from "next"
import { WorkspaceCalendar } from "@/components/ballmac/templates/workspace/workspace-calendar"
export const metadata: Metadata = {
title: "Calendar · Parcel",
description: "Your week at a glance.",
}
export default function Page() {
return <WorkspaceCalendar />
}// Ballmac UI: Workspace template route. https://ui.ballmac.com/templates/template-workspace
import type { Metadata } from "next"
import { WorkspaceBoard } from "@/components/ballmac/templates/workspace/workspace-board"
export const metadata: Metadata = {
title: "Board · Parcel",
description: "Tasks on a board you can drag.",
}
export default function Page() {
return <WorkspaceBoard />
}// Ballmac UI: Workspace template route. https://ui.ballmac.com/templates/template-workspace
import type { Metadata } from "next"
import { WorkspaceSettings } from "@/components/ballmac/templates/workspace/workspace-settings"
export const metadata: Metadata = {
title: "Settings · Parcel",
description: "Profile and preferences.",
}
export default function Page() {
return <WorkspaceSettings />
}Installation
$ pnpm dlx shadcn@latest add @ballmac/template-workspaceInstall the dependencies.
$ pnpm add lucide-reactAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/calendar-1 @ballmac/command @ballmac/kanban-1 @ballmac/mail-1 @ballmac/settings-1Copy the source into your project.
components/ballmac/templates/workspace/workspace-fonts.ts// Ballmac UI: Parcel workspace template fonts. https://ui.ballmac.com/templates/template-workspace import { Albert_Sans } from "next/font/google" export const workspaceSans = Albert_Sans({ variable: "--workspace-sans", subsets: ["latin"], display: "swap", preload: false })components/ballmac/templates/workspace/workspace-theme.tsx// Ballmac UI: Parcel workspace template shell. https://ui.ballmac.com/templates/template-workspace "use client" import * as React from "react" import { CalendarDays, CheckSquare, Command as CommandIcon, Inbox, LayoutGrid, Plus, Search, Settings } from "lucide-react" import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ballmac/command" import { workspaceSans } from "@/components/ballmac/templates/workspace/workspace-fonts" import { cn } from "@/lib/utils" type WorkspacePage = "today" | "mail" | "calendar" | "board" | "settings" type WorkspaceHrefs = Record<WorkspacePage, string> const defaultHrefs: WorkspaceHrefs = { today: "/workspace", mail: "/workspace/mail", calendar: "/workspace/calendar", board: "/workspace/board", settings: "/workspace/settings" } /** Parcel's palette: cool paper and navy ink with one blue and a touch of amber. */ const workspaceCss = ` .workspace-theme,body:not(:has([data-gallery])):has(.workspace-theme){--background:oklch(0.982 0.004 250);--foreground:oklch(0.21 0.03 262);--card:oklch(1 0 0);--card-foreground:oklch(0.21 0.03 262);--popover:oklch(1 0 0);--popover-foreground:oklch(0.21 0.03 262);--primary:oklch(0.28 0.05 262);--primary-foreground:oklch(0.985 0.004 250);--secondary:oklch(0.958 0.008 250);--secondary-foreground:oklch(0.21 0.03 262);--muted:oklch(0.958 0.008 250);--muted-foreground:oklch(0.48 0.03 258);--accent:oklch(0.94 0.012 250);--accent-foreground:oklch(0.21 0.03 262);--border:oklch(0.21 0.03 262 / 11%);--input:oklch(0.21 0.03 262 / 16%);--ring:oklch(0.5 0.17 258);--surface:oklch(0.966 0.007 250);--destructive:oklch(0.54 0.21 25);--chart-1:oklch(0.5 0.17 258);--chart-2:oklch(0.5 0.12 165);--chart-3:oklch(0.62 0.14 72);--chart-4:oklch(0.5 0.16 310);--chart-5:oklch(0.52 0.15 25);--radius:0.75rem} .dark .workspace-theme,.dark body:not(:has([data-gallery])):has(.workspace-theme){--background:oklch(0.17 0.02 262);--foreground:oklch(0.95 0.008 250);--card:oklch(0.21 0.024 262);--card-foreground:oklch(0.95 0.008 250);--popover:oklch(0.23 0.026 262);--popover-foreground:oklch(0.95 0.008 250);--primary:oklch(0.95 0.008 250);--primary-foreground:oklch(0.21 0.03 262);--secondary:oklch(0.26 0.026 262);--secondary-foreground:oklch(0.95 0.008 250);--muted:oklch(0.25 0.026 262);--muted-foreground:oklch(0.73 0.025 255);--accent:oklch(0.28 0.03 262);--accent-foreground:oklch(0.95 0.008 250);--border:oklch(1 0 0 / 10%);--input:oklch(1 0 0 / 14%);--ring:oklch(0.72 0.14 258);--surface:oklch(0.19 0.022 262);--destructive:oklch(0.7 0.19 25);--chart-1:oklch(0.74 0.13 258);--chart-2:oklch(0.78 0.13 165);--chart-3:oklch(0.82 0.13 80);--chart-4:oklch(0.75 0.13 310);--chart-5:oklch(0.76 0.13 25)} body:not(:has([data-gallery])):has(.workspace-theme){font-family:var(--workspace-sans),ui-sans-serif,system-ui,sans-serif} ` const apps: { key: WorkspacePage; label: string; icon: typeof Inbox }[] = [ { key: "today", label: "Today", icon: LayoutGrid }, { key: "mail", label: "Mail", icon: Inbox }, { key: "calendar", label: "Calendar", icon: CalendarDays }, { key: "board", label: "Board", icon: CheckSquare }, { key: "settings", label: "Settings", icon: Settings }, ] function ParcelMark({ className }: { className?: string }) { return ( <svg viewBox="0 0 28 28" aria-hidden="true" className={cn("size-7", className)} fill="none"> <rect width="28" height="28" rx="8" fill="var(--chart-1)" /> <path d="M14 6 21 10v8l-7 4-7-4v-8z" stroke="var(--card)" strokeWidth="2" strokeLinejoin="round" /> <path d="M7 10l7 4 7-4M14 14v8" stroke="var(--card)" strokeWidth="2" strokeLinejoin="round" /> </svg> ) } type WorkspaceShellProps = Omit<React.ComponentProps<"div">, "title"> & { /** The app being shown, so its icon is marked current. */ page: WorkspacePage /** Override where pages live (used by previews). */ hrefs?: Partial<WorkspaceHrefs> /** Let the content fill the window height (mail, calendar and board do). */ fill?: boolean } /** Parcel's frame: an app rail (a tab bar on phones), a search and quick-create bar, and a command menu on ⌘K. */ function WorkspaceShell({ page, hrefs: overrides, fill, className, style, children, ...props }: WorkspaceShellProps) { const hrefs = { ...defaultHrefs, ...overrides } const [palette, setPalette] = React.useState(false) React.useEffect(() => { const cls = workspaceSans.variable if (!cls) return document.body.classList.add(cls) return () => document.body.classList.remove(cls) }, []) const current = apps.find((a) => a.key === page)! return ( <div data-slot="workspace" className={cn("workspace-theme bg-background text-foreground flex min-h-dvh flex-col md:flex-row", fill && "h-dvh overflow-hidden", workspaceSans.variable, className)} style={{ fontFamily: "var(--workspace-sans), ui-sans-serif, system-ui, sans-serif", ...style }} {...props} > <style>{workspaceCss}</style> <nav aria-label="Apps" className="bg-surface order-last flex shrink-0 items-center justify-around border-t px-2 py-1.5 md:order-none md:w-[4.5rem] md:flex-col md:justify-start md:gap-1 md:border-t-0 md:border-e md:py-3"> <a href={hrefs.today} aria-label="Parcel home" className="focus-visible:ring-ring/50 mb-3 hidden rounded-xl outline-none focus-visible:ring-[3px] md:block"><ParcelMark /></a> {apps.map((a) => ( <a key={a.key} href={hrefs[a.key]} aria-current={page === a.key ? "page" : undefined} className="text-muted-foreground hover:text-foreground hover:bg-accent aria-[current=page]:bg-card aria-[current=page]:text-foreground aria-[current=page]:shadow-sm focus-visible:ring-ring/50 flex min-w-14 flex-col items-center gap-1 rounded-xl px-2 py-1.5 text-[11px] font-medium outline-none transition-colors focus-visible:ring-[3px] md:w-14"> <a.icon className="size-5" aria-hidden="true" /> {a.label} </a> ))} </nav> <div className={cn("flex min-w-0 flex-1 flex-col", fill && "min-h-0")}> <header className="flex h-14 shrink-0 items-center gap-3 border-b px-4 sm:px-6"> <h1 className="text-[15px] font-semibold tracking-tight">{current.label}</h1> <button type="button" onClick={() => setPalette(true)} className="text-muted-foreground bg-card hover:bg-accent focus-visible:ring-ring/50 mx-auto flex h-9 w-full max-w-md items-center gap-2.5 rounded-xl border px-3 text-sm outline-none transition-colors focus-visible:ring-[3px]"> <Search className="size-4" aria-hidden="true" /> <span className="flex-1 text-start">Search or jump to…</span> <kbd className="bg-muted hidden items-center gap-0.5 rounded px-1.5 py-0.5 text-[11px] sm:flex"><CommandIcon className="size-3" aria-hidden="true" />K</kbd> </button> <a href={hrefs.mail} className="bg-primary text-primary-foreground focus-visible:ring-ring/50 inline-flex h-9 shrink-0 items-center gap-1.5 rounded-xl px-3 text-sm font-semibold outline-none transition-opacity hover:opacity-90 focus-visible:ring-[3px]"><Plus className="size-4" aria-hidden="true" /><span className="hidden sm:inline">Compose</span><span className="sr-only sm:hidden">Compose</span></a> <span className="bg-chart-4/25 flex size-9 shrink-0 items-center justify-center rounded-full text-xs font-bold" aria-hidden="true">MK</span> </header> <div className={cn("flex-1", fill && "min-h-0 overflow-hidden")}>{children}</div> </div> <CommandDialog open={palette} onOpenChange={setPalette} title="Search or jump to"> <CommandInput placeholder="Search mail, events and tasks…" /> <CommandList> <CommandEmpty>No results.</CommandEmpty> <CommandGroup heading="Apps"> {apps.map((a) => <CommandItem key={a.key} value={a.label} onSelect={() => { setPalette(false); window.location.assign(hrefs[a.key]) }}><a.icon aria-hidden="true" /> {a.label}</CommandItem>)} </CommandGroup> </CommandList> </CommandDialog> </div> ) } export { ParcelMark, WorkspaceShell, defaultHrefs as workspaceDefaultHrefs, type WorkspaceHrefs, type WorkspacePage, type WorkspaceShellProps }components/ballmac/templates/workspace/workspace-today.tsx// Ballmac UI: Parcel today page. https://ui.ballmac.com/templates/template-workspace "use client" import * as React from "react" import { Archive, Check, Pause, Play, Plus, Reply, RotateCcw } from "lucide-react" import { WorkspaceShell, type WorkspaceHrefs } from "@/components/ballmac/templates/workspace/workspace-theme" import { cn } from "@/lib/utils" type Task = { id: string; text: string; done: boolean } const startTasks: Task[] = [ { id: "t1", text: "Send the Q4 plan to Priya", done: true }, { id: "t2", text: "Review Dev’s onboarding mockups", done: false }, { id: "t3", text: "Book the Lisbon flights", done: false }, { id: "t4", text: "Write Friday’s team update", done: false }, ] const agenda = [ { time: "09:00", end: "09:30", title: "Design standup", where: "Meet", tone: "bg-chart-1" }, { time: "10:30", end: "11:30", title: "Onboarding review", where: "Room 4", tone: "bg-chart-2" }, { time: "13:00", end: "13:45", title: "Lunch with Amara", where: "Bar Lucia", tone: "bg-chart-3" }, { time: "15:00", end: "16:00", title: "1:1 with Dev", where: "Meet", tone: "bg-chart-4" }, ] const inbox = [ { id: "m1", from: "Priya Raman", subject: "Re: Q4 plan — two small edits", preview: "Love it. Can we add owners to the risks?", time: "8:12 AM" }, { id: "m2", from: "Amara Okafor", subject: "Lunch today?", preview: "Still on for 13:00 at Bar Lucia?", time: "7:48 AM" }, { id: "m3", from: "Billing", subject: "Your October invoice", preview: "Your invoice for $48.00 is ready.", time: "Yesterday" }, ] const NOW = "09:41" const toMin = (t: string) => Number(t.slice(0, 2)) * 60 + Number(t.slice(3)) const card = "bg-card rounded-2xl border" type WorkspaceTodayProps = React.ComponentProps<"div"> & { hrefs?: Partial<WorkspaceHrefs> } /** The Parcel today page: your agenda with a now line, tasks you can tick and add, inbox highlights you can archive, and a focus timer. */ function WorkspaceToday({ hrefs, ...props }: WorkspaceTodayProps) { const [tasks, setTasks] = React.useState(startTasks) const [draft, setDraft] = React.useState("") const [mail, setMail] = React.useState(inbox) const [secs, setSecs] = React.useState(25 * 60) const [running, setRunning] = React.useState(false) const [note, setNote] = React.useState("") React.useEffect(() => { if (!running) return const id = setInterval(() => setSecs((s) => (s <= 1 ? 0 : s - 1)), 1000) return () => clearInterval(id) }, [running]) const finished = secs === 0 const done = tasks.filter((t) => t.done).length const pct = tasks.length ? (done / tasks.length) * 100 : 0 const mm = String(Math.floor(secs / 60)).padStart(2, "0") const ss = String(secs % 60).padStart(2, "0") function add(e: React.FormEvent) { e.preventDefault() const text = draft.trim() if (!text) return setTasks((all) => [...all, { id: `n${all.length}`, text, done: false }]) setDraft("") } return ( <WorkspaceShell page="today" hrefs={hrefs} {...props}> <main className="mx-auto max-w-6xl px-4 py-6 pb-10 sm:px-6"> <p className="text-muted-foreground text-sm font-medium">Thursday, October 1</p> <h2 className="mt-1 text-3xl font-extrabold tracking-[-0.03em] sm:text-4xl">Good morning, Mina.</h2> <p className="text-muted-foreground mt-2 text-pretty">You have {agenda.length} meetings, {tasks.length - done} tasks left and {mail.length} messages worth a look.</p> <div className="mt-8 grid grid-cols-[minmax(0,1fr)] gap-4 lg:grid-cols-3"> <section aria-labelledby="wt-agenda" className={cn(card, "p-5 lg:row-span-2")}> <h3 id="wt-agenda" className="font-bold">Agenda</h3> <ol className="mt-4 space-y-1"> {agenda.map((e, i) => { const past = toMin(e.end) <= toMin(NOW) const live = toMin(e.time) <= toMin(NOW) && toMin(NOW) < toMin(e.end) const showNow = !past && !live && (i === 0 || toMin(agenda[i - 1].end) <= toMin(NOW)) return ( <React.Fragment key={e.title}> {showNow && ( <li aria-label={`Now, ${NOW}`} className="flex items-center gap-2 py-1.5"><span className="bg-destructive size-2 rounded-full" aria-hidden="true" /><span className="bg-destructive/50 h-px flex-1" aria-hidden="true" /><span className="text-destructive text-xs font-semibold tabular-nums">{NOW}</span></li> )} <li className={cn("flex gap-3 rounded-xl p-2.5", live && "bg-accent", past && "text-muted-foreground")}> <span className={cn("w-1 shrink-0 rounded-full", e.tone)} aria-hidden="true" /> <div className="min-w-0 flex-1"><p className="text-sm font-semibold">{e.title}</p><p className="text-muted-foreground text-xs">{e.time}–{e.end} · {e.where}</p></div> {past && <Check className="text-chart-2 size-4 shrink-0" aria-label="Done" />} </li> </React.Fragment> ) })} </ol> </section> <section aria-labelledby="wt-tasks" className={cn(card, "p-5")}> <div className="flex items-center justify-between"><h3 id="wt-tasks" className="font-bold">Tasks</h3><p className="text-muted-foreground text-sm tabular-nums" role="status">{done} of {tasks.length} done</p></div> <div className="bg-muted mt-3 h-1.5 rounded-full" aria-hidden="true"><div className="bg-chart-2 h-full rounded-full transition-[width] duration-500 motion-reduce:transition-none" style={{ width: `${pct}%` }} /></div> <ul className="mt-4 space-y-1"> {tasks.map((t) => ( <li key={t.id}> <label className="hover:bg-accent flex cursor-pointer items-center gap-3 rounded-lg px-2 py-2 text-sm"> <input type="checkbox" checked={t.done} onChange={() => setTasks((all) => all.map((x) => (x.id === t.id ? { ...x, done: !x.done } : x)))} className="accent-chart-1 size-4" /> <span className={cn(t.done && "text-muted-foreground line-through")}>{t.text}</span> </label> </li> ))} </ul> <form onSubmit={add} className="mt-3 flex gap-2"> <label htmlFor="wt-new" className="sr-only">Add a task</label> <input id="wt-new" value={draft} onChange={(e) => setDraft(e.target.value)} placeholder="Add a task" className="bg-background focus-visible:ring-ring/50 placeholder:text-muted-foreground h-9 min-w-0 flex-1 rounded-lg border px-3 text-sm outline-none focus-visible:ring-[3px]" /> <button type="submit" aria-label="Add task" className="bg-primary text-primary-foreground focus-visible:ring-ring/50 inline-flex size-9 items-center justify-center rounded-lg outline-none transition-opacity hover:opacity-90 focus-visible:ring-[3px]"><Plus className="size-4" aria-hidden="true" /></button> </form> </section> <section aria-labelledby="wt-focus" className={cn(card, "flex flex-col items-center p-5 text-center")}> <h3 id="wt-focus" className="self-start font-bold">Focus</h3> <p className="mt-6 text-6xl font-extrabold tracking-[-0.04em] tabular-nums" role="timer" aria-label={`${mm} minutes ${ss} seconds left`}>{mm}:{ss}</p> <p className="text-muted-foreground mt-2 text-sm" aria-live="polite">{finished ? "Nice work. Take five." : running ? "Stay with one thing." : "Pick a task and begin."}</p> <div className="mt-5 flex gap-2"> <button type="button" onClick={() => { if (finished) setSecs(25 * 60); setRunning((r) => !r) }} className="bg-primary text-primary-foreground focus-visible:ring-ring/50 inline-flex h-10 items-center gap-2 rounded-xl px-5 text-sm font-semibold outline-none transition-opacity hover:opacity-90 focus-visible:ring-[3px]"> {running ? <><Pause className="size-4" aria-hidden="true" />Pause</> : <><Play className="size-4" aria-hidden="true" />{secs === 25 * 60 ? "Start" : "Resume"}</>} </button> <button type="button" aria-label="Reset timer" onClick={() => { setRunning(false); setSecs(25 * 60) }} className="hover:bg-accent focus-visible:ring-ring/50 inline-flex size-10 items-center justify-center rounded-xl border outline-none focus-visible:ring-[3px]"><RotateCcw className="size-4" aria-hidden="true" /></button> </div> </section> <section aria-labelledby="wt-inbox" className={cn(card, "p-5 lg:col-span-2")}> <div className="flex items-center justify-between"><h3 id="wt-inbox" className="font-bold">Worth a look</h3><a href={hrefs?.mail ?? "/workspace/mail"} className="text-chart-1 rounded text-sm font-semibold underline underline-offset-4">Open inbox</a></div> <p className="sr-only" role="status">{note}</p> {mail.length === 0 ? ( <p className="text-muted-foreground mt-6 rounded-xl border border-dashed p-8 text-center text-sm">Inbox zero. Enjoy the quiet.</p> ) : ( <ul className="mt-3 divide-y"> {mail.map((m) => ( <li key={m.id} className="flex items-center gap-3 py-3"> <span className="bg-chart-1/15 flex size-9 shrink-0 items-center justify-center rounded-full text-xs font-bold" aria-hidden="true">{m.from.split(" ").map((w) => w[0]).join("")}</span> <div className="min-w-0 flex-1"><p className="truncate text-sm"><span className="font-semibold">{m.from}</span> <span className="text-muted-foreground">· {m.time}</span></p><p className="truncate text-sm font-medium">{m.subject}</p><p className="text-muted-foreground truncate text-xs">{m.preview}</p></div> <div className="flex shrink-0 gap-1"> <a href={hrefs?.mail ?? "/workspace/mail"} aria-label={`Reply to ${m.from}`} className="hover:bg-accent focus-visible:ring-ring/50 inline-flex size-8 items-center justify-center rounded-lg outline-none focus-visible:ring-[3px]"><Reply className="size-4 rtl:-scale-x-100" aria-hidden="true" /></a> <button type="button" aria-label={`Archive: ${m.subject}`} onClick={() => { setMail((all) => all.filter((x) => x.id !== m.id)); setNote(`Archived ${m.subject}`) }} className="hover:bg-accent focus-visible:ring-ring/50 inline-flex size-8 items-center justify-center rounded-lg outline-none focus-visible:ring-[3px]"><Archive className="size-4" aria-hidden="true" /></button> </div> </li> ))} </ul> )} </section> </div> </main> </WorkspaceShell> ) } export { WorkspaceToday, type WorkspaceTodayProps }components/ballmac/templates/workspace/workspace-mail.tsx// Ballmac UI: Parcel mail page. https://ui.ballmac.com/templates/template-workspace import * as React from "react" import { Mail1 } from "@/components/ballmac/blocks/mail-1/mail-1" import { WorkspaceShell, type WorkspaceHrefs } from "@/components/ballmac/templates/workspace/workspace-theme" type WorkspaceMailProps = React.ComponentProps<"div"> & { hrefs?: Partial<WorkspaceHrefs> } /** Parcel mail: the three-pane inbox with search, compose and keyboard shortcuts, filling the window. */ function WorkspaceMail({ hrefs, ...props }: WorkspaceMailProps) { return ( <WorkspaceShell page="mail" hrefs={hrefs} fill {...props}> <Mail1 height="100%" className="rounded-none border-0" /> </WorkspaceShell> ) } export { WorkspaceMail, type WorkspaceMailProps }components/ballmac/templates/workspace/workspace-calendar.tsx// Ballmac UI: Parcel calendar page. https://ui.ballmac.com/templates/template-workspace import * as React from "react" import { Calendar1 } from "@/components/ballmac/blocks/calendar-1/calendar-1" import { WorkspaceShell, type WorkspaceHrefs } from "@/components/ballmac/templates/workspace/workspace-theme" type WorkspaceCalendarProps = React.ComponentProps<"div"> & { hrefs?: Partial<WorkspaceHrefs> } /** Parcel calendar: the week view with overlapping events, a now line and a new-event dialog, filling the window. */ function WorkspaceCalendar({ hrefs, ...props }: WorkspaceCalendarProps) { return ( <WorkspaceShell page="calendar" hrefs={hrefs} fill {...props}> <Calendar1 height="100%" today="2026-10-01" now="09:41" className="rounded-none border-0" /> </WorkspaceShell> ) } export { WorkspaceCalendar, type WorkspaceCalendarProps }components/ballmac/templates/workspace/workspace-board.tsx// Ballmac UI: Parcel board page. https://ui.ballmac.com/templates/template-workspace import * as React from "react" import { Kanban1 } from "@/components/ballmac/blocks/kanban-1/kanban-1" import { WorkspaceShell, type WorkspaceHrefs } from "@/components/ballmac/templates/workspace/workspace-theme" type WorkspaceBoardProps = React.ComponentProps<"div"> & { hrefs?: Partial<WorkspaceHrefs> } /** Parcel board: drag cards between columns, move with the keyboard, add and filter by person. */ function WorkspaceBoard({ hrefs, ...props }: WorkspaceBoardProps) { return ( <WorkspaceShell page="board" hrefs={hrefs} {...props}> <Kanban1 title="Q4 launch" description="Drag cards between columns, or use the Move to menu on any card." height="calc(100dvh - 15rem)" /> </WorkspaceShell> ) } export { WorkspaceBoard, type WorkspaceBoardProps }components/ballmac/templates/workspace/workspace-settings.tsx// Ballmac UI: Parcel settings page. https://ui.ballmac.com/templates/template-workspace import * as React from "react" import { Settings1 } from "@/components/ballmac/blocks/settings-1/settings-1" import { WorkspaceShell, type WorkspaceHrefs } from "@/components/ballmac/templates/workspace/workspace-theme" type WorkspaceSettingsProps = React.ComponentProps<"div"> & { hrefs?: Partial<WorkspaceHrefs> } /** Parcel settings: profile, preferences and account actions with a save bar that appears only when something changed. */ function WorkspaceSettings({ hrefs, ...props }: WorkspaceSettingsProps) { return ( <WorkspaceShell page="settings" hrefs={hrefs} {...props}> <Settings1 title="Your account" description="Manage your profile and how Parcel works for you." /> </WorkspaceShell> ) } export { WorkspaceSettings, type WorkspaceSettingsProps }app/workspace/page.tsx// Ballmac UI: Workspace template route. https://ui.ballmac.com/templates/template-workspace import type { Metadata } from "next" import { WorkspaceToday } from "@/components/ballmac/templates/workspace/workspace-today" export const metadata: Metadata = { title: "Parcel", description: "Mail, calendar and tasks in one calm place.", } export default function Page() { return <WorkspaceToday /> }app/workspace/mail/page.tsx// Ballmac UI: Workspace template route. https://ui.ballmac.com/templates/template-workspace import type { Metadata } from "next" import { WorkspaceMail } from "@/components/ballmac/templates/workspace/workspace-mail" export const metadata: Metadata = { title: "Mail · Parcel", description: "A fast three-pane inbox.", } export default function Page() { return <WorkspaceMail /> }app/workspace/calendar/page.tsx// Ballmac UI: Workspace template route. https://ui.ballmac.com/templates/template-workspace import type { Metadata } from "next" import { WorkspaceCalendar } from "@/components/ballmac/templates/workspace/workspace-calendar" export const metadata: Metadata = { title: "Calendar · Parcel", description: "Your week at a glance.", } export default function Page() { return <WorkspaceCalendar /> }app/workspace/board/page.tsx// Ballmac UI: Workspace template route. https://ui.ballmac.com/templates/template-workspace import type { Metadata } from "next" import { WorkspaceBoard } from "@/components/ballmac/templates/workspace/workspace-board" export const metadata: Metadata = { title: "Board · Parcel", description: "Tasks on a board you can drag.", } export default function Page() { return <WorkspaceBoard /> }app/workspace/settings/page.tsx// Ballmac UI: Workspace template route. https://ui.ballmac.com/templates/template-workspace import type { Metadata } from "next" import { WorkspaceSettings } from "@/components/ballmac/templates/workspace/workspace-settings" export const metadata: Metadata = { title: "Settings · Parcel", description: "Profile and preferences.", } export default function Page() { return <WorkspaceSettings /> }Update the import paths to match your project setup.
The page is added as a route in your app, with every block it uses.
What's inside
- Pages
- Today/workspace
- Mail/workspace/mail
- Calendar/workspace/calendar
- Board/workspace/board
- Settings/workspace/settings
- Fonts
- Albert Sans (Google Fonts, loaded with next/font)
- Files
- 12 files · 5 Ballmac blocks and components
Built with
Dependencies
- npm
- lucide-react
Use with AI
Installs a multi-app productivity shell. WorkspaceShell gives every page an app rail (a tab bar on phones) and a command menu; each app page wraps a Ballmac block you can feed with your own data. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Parcel: productivity workspace (@ballmac/template-workspace) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Suites that combine several tools under one navigation
- Starting point for team inboxes, planners and internal tools
Not for
- A single dashboard (use template-atlas)
Registry JSON: https://ui.ballmac.com/r/template-workspace.json
Free to use in personal and commercial projects.