Complete page · Original
Vellum workspace
A complete operations workspace: sidebar and mobile drawer, chart, searchable sortable table, add/edit drawer, and local toasts on demo data.
Live preview
The whole page
Runnable project
v0.4.1Usage & source
"use client";
import type { CSSProperties } from "react";
import { VellumWorkspace } from "./pages/vellum-workspace/VellumWorkspace";
import "./components/styles.css";
import "./pages/vellum-workspace/vellum-workspace.css";
const themeStyle = {"--stepkit-bg":"#111217","--stepkit-surface":"#1b1c24","--stepkit-surface-muted":"#22232d","--stepkit-fg":"#f6f5fb","--stepkit-muted":"#a4a6b4","--stepkit-border":"#343641","--stepkit-border-strong":"#565866","--stepkit-code":"#20212a","--stepkit-shadow":"0 24px 70px rgba(0, 0, 0, .34)","--stepkit-accent":"#2563eb","--stepkit-accent-strong":"#1d4ed8","--stepkit-accent-soft":"#1f356b","--stepkit-accent-on-soft":"#a9c4ff","--stepkit-accent-text":"#ffffff","--stepkit-error":"#ff9ba9","--stepkit-placeholder":"#b8bac6","--stepkit-radius":"16px","--stepkit-space":"10px","--stepkit-control-height":"44px","--stepkit-font-sans":"Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif"} as CSSProperties;
export default function App() {
return (
<div className="stepkit-theme" data-stepkit-preset="modern" style={themeStyle}>
<VellumWorkspace />
</div>
);
}Dependencies
[email protected] [email protected]
/*
* Stepkit 0.4.1
* Copyright (c) 2026 Vladislav Stepanov
*
* Original Stepkit portions in this release are licensed under the MIT License. Adapted portions identify their upstream licence and attribution in the copied source and in the entry-specific upstream notice files.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
"use client";
import { useEffect, useId, useMemo, useRef, useState, type FormEvent } from "react";
import { Badge } from "../../components/Badge";
import { Button } from "../../components/Button";
import { EmptyState } from "../../components/EmptyState";
import { Input } from "../../components/Input";
import { Sidebar } from "../../components/Sidebar";
import { Switch } from "../../components/Switch";
import { Toast } from "../../components/Toast";
import "./vellum-workspace.css";
type SignalKind = "risk" | "decision" | "win";
type SignalStatus = "open" | "watching" | "closed";
type SortKey = "title" | "owner" | "kind" | "status" | "day";
type ViewId = "overview" | "signals" | "brief" | "settings";
interface Signal {
id: string;
title: string;
owner: string;
kind: SignalKind;
status: SignalStatus;
day: string;
note: string;
}
interface DayPoint {
id: string;
label: string;
count: number;
}
const OWNERS = ["Maya Chen", "Jonah Adeyemi", "Priya Shah", "Sam Okada"];
const DAY_ORDER = ["Mon", "Tue", "Wed", "Thu", "Fri"];
const INITIAL_SIGNALS: Signal[] = [
{ id: "sig-01", title: "Checkout retry storm after the card-network blip", owner: "Maya Chen", kind: "risk", status: "open", day: "Mon", note: "Retries peaked at 14:10. Still above the quiet band." },
{ id: "sig-02", title: "Maya owns the Monday sheet through April", owner: "Maya Chen", kind: "decision", status: "closed", day: "Mon", note: "Written down so the brief does not reopen the debate." },
{ id: "sig-03", title: "Freeze pricing copy until the studio review", owner: "Jonah Adeyemi", kind: "decision", status: "watching", day: "Tue", note: "Review is Thursday. Do not ship the interim headline." },
{ id: "sig-04", title: "Activation mail recovered without a vendor ticket", owner: "Priya Shah", kind: "win", status: "closed", day: "Tue", note: "Local template change. Keep the vendor thread closed." },
{ id: "sig-05", title: "EU data map still missing the digest store", owner: "Maya Chen", kind: "risk", status: "open", day: "Wed", note: "Blocks the Floor plan story. Not a legal review yet." },
{ id: "sig-06", title: "Cut the unused weekly digest, keep the sheet", owner: "Jonah Adeyemi", kind: "decision", status: "open", day: "Thu", note: "Digest had no readers this month. Sheet did." },
{ id: "sig-07", title: "Design ran Monday from the sheet, not Slack", owner: "Priya Shah", kind: "win", status: "watching", day: "Fri", note: "Worth keeping in the brief so it does not look accidental." },
{ id: "sig-08", title: "Vendor token rotation is still on a calendar reminder", owner: "Sam Okada", kind: "risk", status: "watching", day: "Fri", note: "Move this into the board before the next rotation." }
];
const NAV_ITEMS = [
{ id: "overview", label: "Overview" },
{ id: "signals", label: "Signals" },
{ id: "brief", label: "Monday brief" },
{ id: "settings", label: "Settings" }
];
function kindTone(kind: SignalKind) {
if (kind === "risk") return "danger" as const;
if (kind === "win") return "success" as const;
return "accent" as const;
}
function statusTone(status: SignalStatus) {
if (status === "open") return "warning" as const;
if (status === "closed") return "success" as const;
return "neutral" as const;
}
function nextId(signals: Signal[]) {
return `sig-${String(signals.length + 1).padStart(2, "0")}-${Math.floor(Math.random() * 90 + 10)}`;
}
function dialogTabbables(node: HTMLElement) {
return [...node.querySelectorAll<HTMLElement>("a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]):not([type='hidden']), select:not([disabled])")].filter((element) => element.tabIndex >= 0 && !element.hasAttribute("disabled") && element.getClientRects().length > 0);
}
function bindDialogTabTrap(node: HTMLDialogElement | null) {
if (!node) return () => {};
const onKey = (event: KeyboardEvent) => {
if (event.key !== "Tab" || !node.open) return;
const items = dialogTabbables(node);
if (!items.length) return;
const first = items[0];
const last = items[items.length - 1];
const active = document.activeElement;
const inside = Boolean(active && node.contains(active));
if (event.shiftKey && (!inside || active === first)) {
event.preventDefault();
last.focus();
return;
}
if (!event.shiftKey && (!inside || active === last || active === document.body)) {
event.preventDefault();
first.focus();
}
};
document.addEventListener("keydown", onKey, true);
const frame = requestAnimationFrame(() => dialogTabbables(node)[0]?.focus());
return () => {
cancelAnimationFrame(frame);
document.removeEventListener("keydown", onKey, true);
};
}
export function VellumWorkspace() {
const searchId = useId();
const weekId = useId();
const addTitleId = useId();
const drawerTitleId = useId();
const [view, setView] = useState<ViewId>("overview");
const [menuOpen, setMenuOpen] = useState(false);
const [narrow, setNarrow] = useState(false);
const [signals, setSignals] = useState<Signal[]>(INITIAL_SIGNALS);
const [query, setQuery] = useState("");
const [statusFilter, setStatusFilter] = useState<"all" | SignalStatus>("all");
const [sortKey, setSortKey] = useState<SortKey>("day");
const [sortDir, setSortDir] = useState<"asc" | "desc">("asc");
const [selectedId, setSelectedId] = useState<string | null>(null);
const [addOpen, setAddOpen] = useState(false);
const [toast, setToast] = useState("");
const [draftNote, setDraftNote] = useState("");
const [draftStatus, setDraftStatus] = useState<SignalStatus>("open");
const [addError, setAddError] = useState("");
const [weekLabel, setWeekLabel] = useState("Week of 16 March");
const [compactRows, setCompactRows] = useState(false);
const menuButtonRef = useRef<HTMLButtonElement>(null);
const addButtonRef = useRef<HTMLButtonElement>(null);
const addDialogRef = useRef<HTMLDialogElement>(null);
const drawerRef = useRef<HTMLDialogElement>(null);
const openerRef = useRef<HTMLElement | null>(null);
const pendingSelectId = useRef<string | null>(null);
const selected = signals.find((signal) => signal.id === selectedId) ?? null;
const railInert = narrow && !menuOpen;
const series = useMemo<DayPoint[]>(() => DAY_ORDER.map((label) => ({
id: label.toLowerCase(),
label,
count: signals.filter((signal) => signal.day === label).length
})), [signals]);
const filtered = useMemo(() => {
const needle = query.trim().toLowerCase();
const rows = signals.filter((signal) => {
const haystack = `${signal.title} ${signal.owner} ${signal.note} ${signal.kind}`.toLowerCase();
const matchesQuery = !needle || haystack.includes(needle);
const matchesStatus = statusFilter === "all" || signal.status === statusFilter;
return matchesQuery && matchesStatus;
});
const direction = sortDir === "asc" ? 1 : -1;
return [...rows].sort((a, b) => {
if (sortKey === "day") return (DAY_ORDER.indexOf(a.day) - DAY_ORDER.indexOf(b.day)) * direction || a.title.localeCompare(b.title);
return String(a[sortKey]).localeCompare(String(b[sortKey])) * direction;
});
}, [query, signals, sortDir, sortKey, statusFilter]);
useEffect(() => {
const media = window.matchMedia("(max-width: 900px)");
const sync = () => setNarrow(media.matches);
sync();
media.addEventListener("change", sync);
return () => media.removeEventListener("change", sync);
}, []);
useEffect(() => {
if (!selected) return;
setDraftNote(selected.note);
setDraftStatus(selected.status);
}, [selected]);
useEffect(() => {
const node = addDialogRef.current;
if (!node) return;
if (addOpen && !node.open) node.showModal();
if (!addOpen && node.open) node.close();
if (!addOpen) return;
return bindDialogTabTrap(node);
}, [addOpen]);
useEffect(() => {
const node = drawerRef.current;
if (!node) return;
if (selectedId && !node.open) node.showModal();
if (!selectedId && node.open) node.close();
if (!selectedId) return;
return bindDialogTabTrap(node);
}, [selectedId]);
useEffect(() => {
if (!menuOpen) return;
const onKey = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setMenuOpen(false);
menuButtonRef.current?.focus();
}
};
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, [menuOpen]);
const closeDrawer = () => {
setSelectedId(null);
const opener = openerRef.current;
queueMicrotask(() => opener?.focus());
};
const openRow = (id: string, trigger?: HTMLElement | null) => {
openerRef.current = trigger ?? (document.activeElement instanceof HTMLElement ? document.activeElement : null);
setSelectedId(id);
};
const toggleSort = (key: SortKey) => {
if (sortKey === key) setSortDir((value) => (value === "asc" ? "desc" : "asc"));
else {
setSortKey(key);
setSortDir("asc");
}
};
const saveSelected = () => {
if (!selected) return;
setSignals((current) => current.map((signal) => signal.id === selected.id ? { ...signal, note: draftNote, status: draftStatus } : signal));
setToast("Saved on this sheet.");
};
const deleteSelected = () => {
if (!selected) return;
const removedId = selected.id;
setSignals((current) => current.filter((signal) => signal.id !== removedId));
openerRef.current = addButtonRef.current;
setSelectedId(null);
setToast("Removed from this sheet.");
queueMicrotask(() => addButtonRef.current?.focus());
};
const addSignal = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
const title = String(data.get("title") || "").trim();
const owner = String(data.get("owner") || "").trim();
const kind = String(data.get("kind") || "risk") as SignalKind;
const day = String(data.get("day") || "Mon");
if (!title || !owner) {
setAddError("Title and owner are required.");
return;
}
const created: Signal = {
id: nextId(signals),
title,
owner,
kind,
status: "open",
day,
note: "Logged in this session."
};
setSignals((current) => [...current, created]);
pendingSelectId.current = created.id;
openerRef.current = addButtonRef.current;
setAddOpen(false);
setAddError("");
setView("signals");
setToast("Signal added to this sheet.");
event.currentTarget.reset();
};
const openCounts = signals.filter((signal) => signal.status === "open").length;
const riskCounts = signals.filter((signal) => signal.kind === "risk" && signal.status !== "closed").length;
const watchingCounts = signals.filter((signal) => signal.status === "watching").length;
const toastNode = toast ? (
<Toast title="Vellum" message={toast} onOpenChange={(open) => { if (!open) setToast(""); }} />
) : null;
const table = (
<SignalTable
rows={filtered}
sortKey={sortKey}
sortDir={sortDir}
compactRows={compactRows}
onSort={toggleSort}
onSelect={openRow}
/>
);
return (
<div className="vellum-workspace" data-vellum-page="workspace" data-narrow={narrow || undefined}>
{menuOpen ? <button className="vellum-workspace__scrim" type="button" aria-label="Close menu" onClick={() => { setMenuOpen(false); menuButtonRef.current?.focus(); }} /> : null}
<div
className="vellum-workspace__rail"
id="vellum-workspace-rail"
data-open={menuOpen || undefined}
{...(railInert ? { inert: true, "aria-hidden": true } : {})}
>
<Sidebar
title="Vellum"
items={NAV_ITEMS}
activeId={view}
onSelect={(id) => {
setView(id as ViewId);
setMenuOpen(false);
menuButtonRef.current?.focus();
}}
footer={<span>{weekLabel}</span>}
/>
</div>
<div className="vellum-workspace__main" {...(menuOpen ? { inert: true } : {})}>
<header className="vellum-workspace__header">
<button
ref={menuButtonRef}
className="vellum-workspace__menu"
type="button"
aria-expanded={menuOpen}
aria-controls="vellum-workspace-rail"
aria-haspopup="true"
onClick={() => setMenuOpen((open) => !open)}
>
Menu
</button>
<div>
<p className="vellum-workspace__eyebrow">{weekLabel}</p>
<h1>Monday sheet</h1>
</div>
<form className="vellum-workspace__search" role="search" onSubmit={(event) => event.preventDefault()}>
<Input
id={searchId}
label="Search signals"
placeholder="Title, owner, or note"
value={query}
onChange={(event) => {
setQuery(event.target.value);
if (view === "overview") setView("signals");
}}
/>
</form>
<Button ref={addButtonRef} className="vellum-workspace__add" label="Add signal" onClick={() => setAddOpen(true)} />
</header>
<p className="vellum-workspace__banner" data-demo-banner role="note">
Demo · changes reset on refresh
</p>
{view === "overview" ? (
<section className="vellum-workspace__panel" data-view="overview" aria-labelledby="overview-title">
<div className="vellum-workspace__panel-head">
<h2 id="overview-title">The week at a glance</h2>
<p>{signals.length} signals · {openCounts} open</p>
</div>
<div className="vellum-workspace__stats">
<div className="vellum-workspace__stat">
<span>On the sheet</span>
<strong>{signals.length}</strong>
</div>
<div className="vellum-workspace__stat">
<span>Still open</span>
<strong>{openCounts}</strong>
</div>
<div className="vellum-workspace__stat">
<span>Live risks</span>
<strong>{riskCounts}</strong>
</div>
</div>
<SignalChart series={series} />
{table}
</section>
) : null}
{view === "signals" ? (
<section className="vellum-workspace__panel" data-view="signals" aria-labelledby="signals-title">
<div className="vellum-workspace__panel-head">
<h2 id="signals-title">Signals</h2>
<p aria-live="polite">{filtered.length} shown of {signals.length}</p>
</div>
<div className="vellum-workspace__filters" role="group" aria-label="Status filter">
{(["all", "open", "watching", "closed"] as const).map((value) => (
<button
key={value}
type="button"
aria-pressed={statusFilter === value}
onClick={() => setStatusFilter(value)}
>
{value === "all" ? "All statuses" : value}
</button>
))}
</div>
{filtered.length ? table : (
<EmptyState
title="No signals match"
description="Clear search or status filters to see the sheet again."
actionLabel="Clear filters"
onAction={() => { setQuery(""); setStatusFilter("all"); }}
/>
)}
</section>
) : null}
{view === "brief" ? (
<section className="vellum-workspace__panel vellum-workspace__brief" data-view="brief" aria-labelledby="brief-title">
<h2 id="brief-title">Monday brief</h2>
<p className="vellum-workspace__brief-kicker">{weekLabel} · written from the current sheet.</p>
<p>{openCounts} open item{openCounts === 1 ? "" : "s"}, including {riskCounts} live risk{riskCounts === 1 ? "" : "s"} and {watchingCounts} still being watched.</p>
<ul>
{signals.filter((signal) => signal.status === "open").map((signal) => (
<li key={signal.id}><strong>{signal.title}</strong> · {signal.owner}</li>
))}
</ul>
</section>
) : null}
{view === "settings" ? (
<section className="vellum-workspace__panel" data-view="settings" aria-labelledby="settings-title">
<h2 id="settings-title">Sheet preferences</h2>
<div className="vellum-workspace__settings">
<Input
id={weekId}
label="Week label"
value={weekLabel}
onChange={(event) => setWeekLabel(event.target.value)}
/>
<Switch
label="Compact rows"
description="Tighter padding when the sheet is long."
checked={compactRows}
onCheckedChange={setCompactRows}
/>
</div>
</section>
) : null}
</div>
<dialog
ref={drawerRef}
className="vellum-workspace__drawer"
aria-labelledby={drawerTitleId}
onClose={(event) => {
if (event.currentTarget.open) return;
setSelectedId(null);
const opener = openerRef.current;
queueMicrotask(() => {
if (!drawerRef.current?.open) opener?.focus();
});
}}
onCancel={(event) => {
event.preventDefault();
closeDrawer();
}}
>
{selected ? (
<>
<div className="vellum-workspace__drawer-head">
<h2 id={drawerTitleId}>{selected.title}</h2>
<Button label="Close" variant="ghost" onClick={closeDrawer} />
</div>
<p className="vellum-workspace__meta">{selected.owner} · {selected.day}</p>
<div className="vellum-workspace__drawer-badges">
<Badge tone={kindTone(selected.kind)}>{selected.kind}</Badge>
<Badge tone={statusTone(selected.status)}>{selected.status}</Badge>
</div>
<label className="vellum-workspace__field">
Status
<select value={draftStatus} onChange={(event) => setDraftStatus(event.target.value as SignalStatus)}>
<option value="open">open</option>
<option value="watching">watching</option>
<option value="closed">closed</option>
</select>
</label>
<label className="vellum-workspace__field">
Note
<textarea value={draftNote} onChange={(event) => setDraftNote(event.target.value)} rows={6} />
</label>
<div className="vellum-workspace__drawer-actions">
<Button label="Save" onClick={saveSelected} />
<Button label="Remove from demo" variant="danger" onClick={deleteSelected} />
</div>
{toastNode}
</>
) : null}
</dialog>
<dialog
ref={addDialogRef}
className="vellum-workspace__dialog"
aria-labelledby={addTitleId}
onClose={(event) => {
if (event.currentTarget.open) return;
setAddOpen(false);
const next = pendingSelectId.current;
pendingSelectId.current = null;
if (next) setSelectedId(next);
}}
onCancel={(event) => {
event.preventDefault();
pendingSelectId.current = null;
setAddOpen(false);
}}
>
<h2 id={addTitleId}>Add a signal</h2>
<p>Adds a row on this sheet. Refresh clears it.</p>
<form className="vellum-workspace__form" onSubmit={addSignal}>
<label>Title<input name="title" type="text" required /></label>
<label>
Owner
<select name="owner" defaultValue={OWNERS[0]}>
{OWNERS.map((owner) => <option key={owner}>{owner}</option>)}
</select>
</label>
<label>
Kind
<select name="kind" defaultValue="risk">
<option value="risk">risk</option>
<option value="decision">decision</option>
<option value="win">win</option>
</select>
</label>
<label>
Day
<select name="day" defaultValue="Mon">
{DAY_ORDER.map((day) => <option key={day}>{day}</option>)}
</select>
</label>
{addError ? <p className="vellum-workspace__error" role="alert">{addError}</p> : null}
<div className="vellum-workspace__drawer-actions">
<Button type="submit" label="Add to the sheet" />
<Button type="button" label="Cancel" variant="outline" onClick={() => setAddOpen(false)} />
</div>
</form>
{!selected ? toastNode : null}
</dialog>
{!selected && !addOpen ? toastNode : null}
</div>
);
}
function SignalChart({ series }: { series: DayPoint[] }) {
const width = 640;
const height = 148;
const pad = { top: 12, right: 12, bottom: 26, left: 28 };
const max = Math.max(1, ...series.map((point) => point.count));
const innerWidth = width - pad.left - pad.right;
const innerHeight = height - pad.top - pad.bottom;
const points = series.map((point, index) => {
const x = pad.left + (index / Math.max(series.length - 1, 1)) * innerWidth;
const y = pad.top + innerHeight - (point.count / max) * innerHeight;
return { ...point, x, y };
});
const path = points.map((point, index) => `${index === 0 ? "M" : "L"}${point.x} ${point.y}`).join(" ");
const last = points[points.length - 1];
const area = `${path} L${last?.x ?? 0} ${pad.top + innerHeight} L${points[0]?.x ?? 0} ${pad.top + innerHeight} Z`;
return (
<figure className="vellum-chart">
<figcaption>Signals logged per weekday</figcaption>
<svg viewBox={`0 0 ${width} ${height}`} role="img" aria-label="Demo signal counts from Monday to Friday">
{[0, 0.5, 1].map((tick) => {
const y = pad.top + innerHeight - tick * innerHeight;
return <line key={tick} x1={pad.left} x2={width - pad.right} y1={y} y2={y} className="vellum-chart__grid" />;
})}
<path d={area} className="vellum-chart__area" />
<path d={path} className="vellum-chart__line" />
{points.map((point) => (
<g key={point.id}>
<circle cx={point.x} cy={point.y} r="4" className="vellum-chart__point" tabIndex={0} aria-label={`${point.label}: ${point.count} signals`}>
<title>{`${point.label}: ${point.count} signals`}</title>
</circle>
<text x={point.x} y={height - 8} textAnchor="middle">{point.label}</text>
</g>
))}
</svg>
</figure>
);
}
function SignalTable({
rows,
sortKey,
sortDir,
compactRows,
onSort,
onSelect
}: {
rows: Signal[];
sortKey: SortKey;
sortDir: "asc" | "desc";
compactRows: boolean;
onSort: (key: SortKey) => void;
onSelect: (id: string, trigger?: HTMLElement | null) => void;
}) {
const columns: { key: SortKey; label: string }[] = [
{ key: "title", label: "Signal" },
{ key: "owner", label: "Owner" },
{ key: "kind", label: "Kind" },
{ key: "status", label: "Status" },
{ key: "day", label: "Day" }
];
return (
<div
className="vellum-table-wrap"
data-compact-rows={compactRows || undefined}
role="region"
aria-label="Signals"
tabIndex={0}
>
<table className="vellum-table">
<colgroup>
<col className="vellum-table__signal" />
<col className="vellum-table__owner" />
<col className="vellum-table__kind" />
<col className="vellum-table__status" />
<col className="vellum-table__day" />
</colgroup>
<thead>
<tr>
{columns.map((column) => (
<th key={column.key} scope="col" aria-sort={sortKey === column.key ? (sortDir === "asc" ? "ascending" : "descending") : "none"}>
<button type="button" onClick={() => onSort(column.key)}>
{column.label}
{sortKey === column.key ? (sortDir === "asc" ? " ↑" : " ↓") : ""}
</button>
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr key={row.id} data-signal-id={row.id}>
<th scope="row">
<button type="button" data-signal-id={row.id} onClick={(event) => onSelect(row.id, event.currentTarget)}>{row.title}</button>
</th>
<td>{row.owner}</td>
<td><Badge tone={kindTone(row.kind)} size="sm">{row.kind}</Badge></td>
<td><Badge tone={statusTone(row.status)} size="sm">{row.status}</Badge></td>
<td>{row.day}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
export default VellumWorkspace;
pages/vellum-workspace/VellumWorkspace.tsxpages/vellum-workspace/vellum-workspace.csscomponents/Badge.tsxcomponents/Button.tsxcomponents/EmptyState.tsxcomponents/Input.tsxcomponents/Sidebar.tsxcomponents/Switch.tsxcomponents/Toast.tsxcomponents/styles.cssDownload and run
This is a complete React + Vite page. You do not need a pre-existing shadcn setup. Individual Stepkit components remain available from the registry if you only want one control.
unzip vellum-workspace-0.4.1.zip && npm ci && npm run devIntegration prompt
Unzip vellum-workspace-0.4.1.zip into a new folder.
npm ci
npm run dev
This is a complete React + Vite page. You do not need a pre-existing shadcn setup. Individual Stepkit components remain available from the registry if you only want one control.
Keep NOTICE.md with the copied source. Change colours by editing the --stepkit-* tokens on the .stepkit-theme wrapper in src/App.tsx.
Forms, plan choice, search, and edits are local demos until you connect your backend.
Usage:
```tsx
"use client";
import type { CSSProperties } from "react";
import { VellumWorkspace } from "./pages/vellum-workspace/VellumWorkspace";
import "./components/styles.css";
import "./pages/vellum-workspace/vellum-workspace.css";
const themeStyle = {"--stepkit-bg":"#f7f7f8","--stepkit-surface":"#ffffff","--stepkit-surface-muted":"#f0f0f3","--stepkit-fg":"#17181c","--stepkit-muted":"#626572","--stepkit-border":"#e3e4e9","--stepkit-border-strong":"#c8cad3","--stepkit-code":"#f1f1f4","--stepkit-shadow":"0 20px 60px rgba(31, 26, 64, .10)","--stepkit-accent":"#6950df","--stepkit-accent-strong":"#5137c7","--stepkit-accent-soft":"#eeeafd","--stepkit-accent-on-soft":"#5137c7","--stepkit-accent-text":"#ffffff","--stepkit-error":"#b43145","--stepkit-placeholder":"#626572","--stepkit-radius":"16px","--stepkit-space":"16px","--stepkit-control-height":"44px","--stepkit-font-sans":"Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif"} as CSSProperties;
export default function App() {
return (
<div className="stepkit-theme" data-stepkit-preset="modern" style={themeStyle}>
<VellumWorkspace />
</div>
);
}
```Accessibility & limits
- Sidebar selection uses aria-current.
- The table exposes sort direction.
- The detail drawer is a native modal dialog: Escape closes it, Tab stays inside, and the opening row is restored.
Limitations
- Auth, persistence, and notifications are not in this demo. Wire them in your app; this README lists the seams.
- Counts on the chart are derived from the same demo rows as the table.
Provenance
Original source under the MIT licence.
Original Stepkit implementation.
Required notices: NOTICE.md, PROVENANCE-stepkit-vellum-workspace.md