This repository has been archived on 2026-10-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
maestro/web/src/kit.ts
T

310 lines
11 KiB
TypeScript

// Dev-only design kit. Lazily imported behind `import.meta.env.DEV` from
// main.ts (`?kit`), so it never ships in a production bundle. Renders every
// component specimen on one scrollable page so the visual system can be
// critiqued as a whole — buttons (including terminal-native variants for
// review), inputs, chips, badges, session rows, and empty states.
import "./kit.css";
import { createThemeToggle } from "./theme.js";
function h(tag: string, className?: string, html?: string): HTMLElement {
const el = document.createElement(tag);
if (className) el.className = className;
if (html != null) el.innerHTML = html;
return el;
}
/** A titled section with an optional note and a content host. */
function section(title: string, note?: string): { el: HTMLElement; body: HTMLElement } {
const el = h("section", "kit-section");
const head = h("div", "kit-section-head");
head.appendChild(h("h2", "kit-section-title", title));
if (note) head.appendChild(h("p", "kit-section-note", note));
el.appendChild(head);
const body = h("div", "kit-row");
el.appendChild(body);
return { el, body };
}
/** A labelled specimen cell so each variant is named for critique. */
function specimen(label: string, node: Node): HTMLElement {
const cell = h("div", "kit-cell");
const slot = h("div", "kit-slot");
slot.appendChild(node);
cell.append(slot, h("span", "kit-label", label));
return cell;
}
function btn(className: string, label: string, disabled = false): HTMLButtonElement {
const b = document.createElement("button");
b.type = "button";
b.className = className;
b.textContent = label;
b.disabled = disabled;
return b;
}
/** A collapsible folder header, matching the real sidebar tree markup. */
function folderHeader(name: string, count: number, depth: number): HTMLElement {
const b = h("button", "tree-folder-header");
(b as HTMLButtonElement).type = "button";
b.style.paddingLeft = `${depth * 16}px`;
b.setAttribute("aria-expanded", "true");
b.innerHTML = `<span class="chevron">▾</span><span class="folder-name">${name}</span><span class="folder-count">${count}</span>`;
return b;
}
function leafRow(opts: {
name: string;
cmd: string;
status: "running" | "dead";
active?: boolean;
indent?: number;
activity?: "active" | "idle" | "awaiting-input" | "error";
duration?: string;
}): HTMLElement {
const row = h("a", "tree-leaf session-card");
(row as HTMLAnchorElement).href = "#/kit";
row.dataset.status = opts.status;
if (opts.active) row.classList.add("is-active");
row.style.paddingLeft = `${(opts.indent ?? 0) * 16 + 8}px`;
const flags = [
opts.status === "dead" ? `<span class="card-flag">exited</span>` : "",
opts.cmd !== opts.name ? `<span class="card-flag leaf-cmd">${opts.cmd}</span>` : "",
].join("");
row.innerHTML = `
<span class="card-line1">
<span class="leaf-name">${opts.name}</span>
<span class="leaf-actions">
<button class="leaf-act leaf-clone" type="button" title="Spawn similar">⧉</button>
<button class="leaf-act leaf-kill" type="button" title="Kill session">✕</button>
</span>
</span>
${opts.activity ? `<span class="card-line2" data-activity="${opts.activity}"><span class="activity-dot" title="${opts.activity}"></span><span class="activity-duration">${opts.duration ?? ""}</span>${flags}</span>` : ""}
`;
return row;
}
export function renderKit(): HTMLElement {
const page = h("div", "kit");
// --- header ---
const header = h("header", "kit-header");
header.appendChild(
h(
"div",
"kit-brand",
`<span class="brand-dot" aria-hidden="true"></span>
<h1>maestro<span class="blink" aria-hidden="true">_</span> · design kit</h1>`,
),
);
const headerActions = h("div", "kit-header-actions");
const back = document.createElement("a");
back.className = "btn btn-ghost";
back.href = "?mock";
back.textContent = "← back to app";
headerActions.append(back, createThemeToggle());
header.appendChild(headerActions);
page.appendChild(header);
const main = h("div", "kit-main");
page.appendChild(main);
// --- color tokens ---
{
const { el, body } = section(
"Color tokens",
"Slate neutrals (hue ~256) + signal-green = alive. Swap by theme toggle.",
);
body.classList.add("kit-swatches");
const tokens = [
"--bg",
"--surface",
"--surface-2",
"--surface-3",
"--border",
"--ink",
"--muted",
"--faint",
"--accent",
"--danger",
"--warn",
];
for (const t of tokens) {
const sw = h("div", "kit-swatch");
const chip = h("div", "kit-swatch-chip");
chip.style.background = `var(${t})`;
sw.append(chip, h("span", "kit-label", t));
body.appendChild(sw);
}
main.appendChild(el);
}
// --- buttons: current system ---
{
const { el, body } = section(
"Buttons — current",
"The shipping button styles — now the bracket terminal style, as used across the header, panes, and empty states.",
);
body.append(
specimen("btn-primary", btn("btn btn-primary", "+ Spawn")),
specimen("btn (default)", btn("btn", "Cancel")),
specimen("btn-ghost", btn("btn btn-ghost", "Dismiss")),
specimen("btn-icon", btn("btn btn-icon", "✕")),
specimen("btn:disabled", btn("btn btn-primary", "+ Spawn", true)),
);
main.appendChild(el);
}
// --- buttons: terminal-native variants for critique ---
{
const { el, body } = section(
"Buttons — other explorations",
"The bracket variant was adopted as the app style (above). These remain as alternates — not used in the app.",
);
body.append(
specimen("bracket", btn("kit-btn-bracket", "+ Spawn")),
specimen("bracket primary", btn("kit-btn-bracket is-primary", "+ Spawn")),
specimen("prompt caret", btn("kit-btn-prompt", "spawn")),
specimen("block", btn("kit-btn-block", "SPAWN")),
specimen("underline", btn("kit-btn-underline", "spawn")),
);
main.appendChild(el);
}
// --- inputs ---
{
const { el, body } = section("Inputs", "Mono inputs with a green focus ring.");
const search = h("input") as HTMLInputElement;
search.type = "search";
search.className = "search";
search.placeholder = "Search sessions…";
const text = h("div", "spawn-form");
const ti = document.createElement("input");
ti.type = "text";
ti.placeholder = "branch-name";
text.appendChild(ti);
const sel = h("div", "spawn-form");
const s = document.createElement("select");
s.innerHTML = `<option>thinkpad</option><option>tower</option><option>pi</option>`;
sel.appendChild(s);
body.append(
specimen("search", search),
specimen("text input", text),
specimen("select", sel),
);
main.appendChild(el);
}
// --- chips & badges ---
{
const { el, body } = section("Chips & badges", "Status-bearing pills and counts.");
const activeLine = h(
"span",
"card-line2",
`<span class="activity-dot"></span><span class="activity-duration">4m</span>`,
);
activeLine.setAttribute("data-activity", "active");
body.append(
specimen("session-tally", h("span", "session-tally", "6 sessions · 5 live")),
specimen("folder-count", h("span", "folder-count", "4")),
specimen("card-line2 · active", activeLine),
specimen("attach-state · open", h("span", "attach-state", "live")),
);
(body.lastElementChild?.querySelector(".attach-state") as HTMLElement)?.setAttribute(
"data-state",
"open",
);
const connecting = specimen("attach-state · connecting", h("span", "attach-state", "connecting"));
(connecting.querySelector(".attach-state") as HTMLElement).setAttribute("data-state", "connecting");
const closed = specimen("attach-state · closed", h("span", "attach-state", "closed"));
(closed.querySelector(".attach-state") as HTMLElement).setAttribute("data-state", "closed");
body.append(connecting, closed);
main.appendChild(el);
}
// --- session rows, nested by working directory ---
{
const { el, body } = section(
"Session rows — nested by host, then directory",
"docs/SIDEBAR.md cards: line 1 is name + top-right actions (the name truncates before it can reach them); line 2 only when there's something to say — dot + duration in the state's colour, then blocked-work / exited fragments.",
);
const list = h("div", "kit-tree");
list.append(
folderHeader("~/git/maestro", 3, 0),
folderHeader("web", 2, 1),
leafRow({ name: "redesign", cmd: "claude", status: "running", indent: 2 }),
leafRow({
name: "dev-server",
cmd: "vite dev",
status: "running",
indent: 2,
active: true,
activity: "active",
duration: "4m",
}),
folderHeader("crates/daemon", 1, 1),
leafRow({
name: "daemon-work",
cmd: "claude",
status: "running",
indent: 2,
activity: "awaiting-input",
duration: "12s",
}),
leafRow({
name: "flaky-agent",
cmd: "claude",
status: "running",
indent: 2,
activity: "error",
duration: "2h",
}),
folderHeader("~/work/acme", 2, 0),
leafRow({
name: "billing-api",
cmd: "claude",
status: "running",
indent: 1,
activity: "idle",
duration: "26m",
}),
leafRow({
name: "scratch",
cmd: "bash",
status: "dead",
indent: 1,
activity: "idle",
duration: "5h",
}),
leafRow({ name: "monitor", cmd: "btop", status: "running", indent: 0 }),
);
body.classList.add("kit-tree-host");
body.appendChild(list);
main.appendChild(el);
}
// --- empty states ---
{
const { el, body } = section("Empty states", "Each one teaches the next action.");
body.classList.add("kit-empties");
const welcome = h("div", "empty");
welcome.innerHTML = `
<div class="empty-glyph" aria-hidden="true">⌘</div>
<div class="empty-title">No session open</div>
<p class="empty-hint">Pick a session from the list, or start a new one.</p>
<div class="empty-actions"></div>`;
welcome.querySelector(".empty-actions")!.appendChild(btn("btn btn-primary", "+ Spawn session"));
const noSessions = h("div", "empty");
noSessions.innerHTML = `
<div class="empty-glyph" aria-hidden="true">+</div>
<div class="empty-title">No sessions yet</div>
<p class="empty-hint">Start your first session — it'll show up here.</p>
<div class="empty-actions"></div>`;
noSessions.querySelector(".empty-actions")!.appendChild(btn("btn btn-primary", "+ Spawn session"));
body.append(welcome, noSessions);
main.appendChild(el);
}
return page;
}