Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
beef2049ed | ||
|
|
c30592e6b6 | ||
|
|
abae81a0ce | ||
|
|
66a568818a | ||
|
|
f3d6146884 | ||
|
|
35ae01db93 |
@@ -2916,17 +2916,16 @@ async fn mark_session_dead_with_worktree_settlement(
|
|||||||
// Release anyone attached; the scrollback survives for a later
|
// Release anyone attached; the scrollback survives for a later
|
||||||
// attach.
|
// attach.
|
||||||
record.bridge.seal();
|
record.bridge.seal();
|
||||||
let session = record.session.clone();
|
|
||||||
let worktree = record
|
let worktree = record
|
||||||
.worktree_base_repo
|
.worktree_base_repo
|
||||||
.clone()
|
.clone()
|
||||||
.zip(record.worktree_path.clone());
|
.zip(record.worktree_path.clone());
|
||||||
Some((session, worktree))
|
Some(worktree)
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let Some((mut session, worktree)) = flipped else {
|
let Some(worktree) = flipped else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let stream = inner
|
let stream = inner
|
||||||
@@ -2943,9 +2942,6 @@ async fn mark_session_dead_with_worktree_settlement(
|
|||||||
if settle_worktree {
|
if settle_worktree {
|
||||||
if let Some(wt) = &worktree {
|
if let Some(wt) = &worktree {
|
||||||
if settle_worktree_on_exit(inner, id, wt).await {
|
if settle_worktree_on_exit(inner, id, wt).await {
|
||||||
// The tree is settled (removed); a stale "in the way" would
|
|
||||||
// describe something the session no longer holds.
|
|
||||||
session.blocked_work = None;
|
|
||||||
let mut sessions = inner.sessions.lock().await;
|
let mut sessions = inner.sessions.lock().await;
|
||||||
if let Some(record) = sessions.get_mut(&id) {
|
if let Some(record) = sessions.get_mut(&id) {
|
||||||
record.session.worktree = None;
|
record.session.worktree = None;
|
||||||
@@ -2957,13 +2953,17 @@ async fn mark_session_dead_with_worktree_settlement(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Settle has now been applied or refused either way (#115): from here on
|
// Settle has now been applied or refused either way (#115): from here on
|
||||||
// its outcomes are safe to observe.
|
// its outcomes are safe to observe. Re-read the session from the record
|
||||||
{
|
// so the sidecar write includes any updates (e.g. a haiku rename) that
|
||||||
|
// landed between the initial clone and now.
|
||||||
|
let session = {
|
||||||
let mut sessions = inner.sessions.lock().await;
|
let mut sessions = inner.sessions.lock().await;
|
||||||
if let Some(record) = sessions.get_mut(&id) {
|
let Some(record) = sessions.get_mut(&id) else {
|
||||||
record.settled = true;
|
return false;
|
||||||
}
|
};
|
||||||
}
|
record.settled = true;
|
||||||
|
record.session.clone()
|
||||||
|
};
|
||||||
|
|
||||||
let _ = inner.sidecar.write(&session);
|
let _ = inner.sidecar.write(&session);
|
||||||
let _ = inner.events.send(CatalogEvent::SessionUpdated { session });
|
let _ = inner.events.send(CatalogEvent::SessionUpdated { session });
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ use common::TmuxFixture;
|
|||||||
use maestro_daemon::sidecar::SidecarStore;
|
use maestro_daemon::sidecar::SidecarStore;
|
||||||
use maestro_daemon::worktree::WorktreeManager;
|
use maestro_daemon::worktree::WorktreeManager;
|
||||||
use maestro_daemon::SessionManager;
|
use maestro_daemon::SessionManager;
|
||||||
use maestro_protocol::{display, CatalogEvent, CreateSessionRequest, NameSource};
|
use maestro_protocol::{display, CatalogEvent, CreateSessionRequest, NameSource, SessionStatus};
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
struct Fixture {
|
struct Fixture {
|
||||||
manager: SessionManager,
|
manager: SessionManager,
|
||||||
sidecar: SidecarStore,
|
sidecar: SidecarStore,
|
||||||
_tmux: TmuxFixture,
|
tmux: TmuxFixture,
|
||||||
_state_dir: TempDir,
|
_state_dir: TempDir,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ fn fixture() -> Fixture {
|
|||||||
Fixture {
|
Fixture {
|
||||||
manager,
|
manager,
|
||||||
sidecar,
|
sidecar,
|
||||||
_tmux: tmux,
|
tmux,
|
||||||
_state_dir: state_dir,
|
_state_dir: state_dir,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,3 +171,37 @@ async fn a_blank_name_changes_nothing() {
|
|||||||
let live = fx.sidecar.read(id).unwrap().unwrap();
|
let live = fx.sidecar.read(id).unwrap().unwrap();
|
||||||
assert_eq!(display::name_source(&live), NameSource::Generated);
|
assert_eq!(display::name_source(&live), NameSource::Generated);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A session that dies naturally (liveness watcher marks it dead) must not
|
||||||
|
/// overwrite a haiku rename that landed before the death. Regression test for
|
||||||
|
/// the stale-clone sidecar write in `mark_session_dead`.
|
||||||
|
#[tokio::test]
|
||||||
|
async fn session_death_preserves_the_haiku_name() {
|
||||||
|
let fx = fixture();
|
||||||
|
let id = spawn(&fx.manager, None).await;
|
||||||
|
|
||||||
|
fx.manager.claim_autoname(id).await.unwrap();
|
||||||
|
fx.manager.apply_autoname(id, "haiku-name").await.unwrap();
|
||||||
|
|
||||||
|
// Verify the sidecar has Haiku before the session dies.
|
||||||
|
let before = fx.sidecar.read(id).unwrap().unwrap();
|
||||||
|
assert_eq!(display::name_source(&before), NameSource::Haiku);
|
||||||
|
|
||||||
|
// Kill the tmux session directly so the liveness watcher picks it up.
|
||||||
|
fx.tmux.tmux.kill_session(id).expect("kill tmux directly");
|
||||||
|
fx.manager
|
||||||
|
.start_liveness_watcher(std::time::Duration::from_millis(25));
|
||||||
|
|
||||||
|
common::wait_for_async(std::time::Duration::from_secs(3), || async {
|
||||||
|
fx.manager.status(id).await == Some(SessionStatus::Dead)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let after = fx.sidecar.read(id).unwrap().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
display::name_source(&after),
|
||||||
|
NameSource::Haiku,
|
||||||
|
"mark_session_dead must not overwrite a haiku rename in the sidecar"
|
||||||
|
);
|
||||||
|
assert_eq!(display::custom_name(&after), Some("haiku-name"));
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ async fn embedded_index_is_real_spa_not_placeholder() {
|
|||||||
let html = String::from_utf8(body.to_vec()).unwrap();
|
let html = String::from_utf8(body.to_vec()).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
html.contains(r#"id="app""#),
|
html.contains(r#"id="root""#),
|
||||||
"served index is missing the SPA mount point; got:\n{html}"
|
"served index is missing the SPA mount point; got:\n{html}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!html.contains("maestro is running"),
|
!html.contains("maestro is running"),
|
||||||
"served index is the placeholder, not the built bundle"
|
"served index is the placeholder, not the built bundle"
|
||||||
);
|
);
|
||||||
// Stronger signal than `id="app"` (which appears in both source and built
|
// Stronger signal than `id="root"` (which appears in both source and built
|
||||||
// HTML): the vite build rewrites the entry to a hashed `/assets/index-<hash>`
|
// HTML): the vite build rewrites the entry to a hashed `/assets/index-<hash>`
|
||||||
// bundle, whereas the un-built source references `/src/main.ts`. This catches
|
// bundle, whereas the un-built source references `/src/main.ts`. This catches
|
||||||
// both an empty/placeholder embed and the staleness case.
|
// both an empty/placeholder embed and the staleness case.
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ async fn root_serves_the_spa_shell() {
|
|||||||
.map(|v| v.starts_with("text/html")),
|
.map(|v| v.starts_with("text/html")),
|
||||||
Some(true)
|
Some(true)
|
||||||
);
|
);
|
||||||
assert!(resp.text().await.unwrap().contains(r#"id="app""#));
|
assert!(resp.text().await.unwrap().contains(r#"id="root""#));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -69,7 +69,7 @@ async fn client_side_route_falls_back_to_the_shell() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(resp.status(), 200);
|
assert_eq!(resp.status(), 200);
|
||||||
assert!(resp.text().await.unwrap().contains(r#"id="app""#));
|
assert!(resp.text().await.unwrap().contains(r#"id="root""#));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An unmatched API path must 404 rather than fall through to the SPA shell:
|
/// An unmatched API path must 404 rather than fall through to the SPA shell:
|
||||||
|
|||||||
@@ -0,0 +1,382 @@
|
|||||||
|
// Dev-only design kit (#254), React port of the deleted kit.ts + kit.css.
|
||||||
|
// Specimen page for the Tailwind token mapping (tailwind.config.ts ->
|
||||||
|
// src/styles.css) so the visual system can be reviewed as a whole. Loads on
|
||||||
|
// `?kit` (see index.tsx) and never ships to a production bundle.
|
||||||
|
|
||||||
|
import { useState, type ReactNode } from "react";
|
||||||
|
|
||||||
|
import { ActivityRail, STATE_WORDS } from "../sidebar/ActivityRail.js";
|
||||||
|
import { SessionCard, type SessionCardProps } from "../sidebar/SessionCard.js";
|
||||||
|
import { PaneTitle } from "../layout/PaneTitle.js";
|
||||||
|
import { PALETTES, useTheme, type Palette } from "../../contexts/ThemeContext.js";
|
||||||
|
import type { Session } from "../../types.js";
|
||||||
|
|
||||||
|
/** The accent each palette lands on, read from styles.css — shown so all six
|
||||||
|
* are comparable at a glance without switching the live one one at a time. */
|
||||||
|
const PALETTE_ACCENT: Record<Palette, string> = {
|
||||||
|
graphite: "oklch(0.92 0.005 256)",
|
||||||
|
"ice-cyan": "oklch(0.85 0.115 205)",
|
||||||
|
periwinkle: "oklch(0.75 0.125 275)",
|
||||||
|
"amber-phosphor": "oklch(0.85 0.16 85)",
|
||||||
|
ember: "oklch(0.78 0.16 40)",
|
||||||
|
orchid: "oklch(0.78 0.13 320)",
|
||||||
|
};
|
||||||
|
|
||||||
|
const COLOR_TOKENS = [
|
||||||
|
"--bg",
|
||||||
|
"--bg-grid",
|
||||||
|
"--surface",
|
||||||
|
"--surface-2",
|
||||||
|
"--surface-3",
|
||||||
|
"--border",
|
||||||
|
"--border-strong",
|
||||||
|
"--ink",
|
||||||
|
"--muted",
|
||||||
|
"--faint",
|
||||||
|
"--accent",
|
||||||
|
"--accent-strong",
|
||||||
|
"--accent-ink",
|
||||||
|
"--accent-tint",
|
||||||
|
"--accent-fill",
|
||||||
|
"--live",
|
||||||
|
"--danger",
|
||||||
|
"--danger-tint",
|
||||||
|
"--warn",
|
||||||
|
"--term-bg",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
// Tailwind's content scanner needs each utility to appear as a literal,
|
||||||
|
// contiguous string in source — a template-built `w-${token}` is invisible
|
||||||
|
// to it, so every scale below pairs the token label with its whole class name.
|
||||||
|
const SPACING_TOKENS = [
|
||||||
|
{ token: "sp-1", widthClass: "w-sp-1" },
|
||||||
|
{ token: "sp-2", widthClass: "w-sp-2" },
|
||||||
|
{ token: "sp-3", widthClass: "w-sp-3" },
|
||||||
|
{ token: "sp-4", widthClass: "w-sp-4" },
|
||||||
|
{ token: "sp-5", widthClass: "w-sp-5" },
|
||||||
|
{ token: "sp-6", widthClass: "w-sp-6" },
|
||||||
|
{ token: "sp-8", widthClass: "w-sp-8" },
|
||||||
|
] as const;
|
||||||
|
const RADIUS_TOKENS = [
|
||||||
|
{ token: "r-1", radiusClass: "rounded-r-1" },
|
||||||
|
{ token: "r-2", radiusClass: "rounded-r-2" },
|
||||||
|
{ token: "r-3", radiusClass: "rounded-r-3" },
|
||||||
|
{ token: "r-pill", radiusClass: "rounded-r-pill" },
|
||||||
|
] as const;
|
||||||
|
const FONT_SIZE_TOKENS = [
|
||||||
|
{ token: "fs-11", sizeClass: "text-fs-11" },
|
||||||
|
{ token: "fs-12", sizeClass: "text-fs-12" },
|
||||||
|
{ token: "fs-13", sizeClass: "text-fs-13" },
|
||||||
|
{ token: "fs-14", sizeClass: "text-fs-14" },
|
||||||
|
{ token: "fs-15", sizeClass: "text-fs-15" },
|
||||||
|
{ token: "fs-17", sizeClass: "text-fs-17" },
|
||||||
|
{ token: "fs-20", sizeClass: "text-fs-20" },
|
||||||
|
] as const;
|
||||||
|
const Z_INDEX_TOKENS = [
|
||||||
|
{ name: "z-sticky", value: 10 },
|
||||||
|
{ name: "z-backdrop", value: 40 },
|
||||||
|
{ name: "z-modal", value: 50 },
|
||||||
|
{ name: "z-toast", value: 60 },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
function Section({
|
||||||
|
title,
|
||||||
|
note,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
note?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<section className="border-t border-border py-sp-6 first:border-t-0 first:pt-0">
|
||||||
|
<h2 className="text-fs-17 font-semibold text-ink">{title}</h2>
|
||||||
|
{note && <p className="mt-sp-1 max-w-[70ch] text-fs-12 text-muted">{note}</p>}
|
||||||
|
<div className="mt-sp-4 flex flex-wrap gap-sp-4">{children}</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Specimen({ label, children }: { label: string; children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-start gap-sp-2">
|
||||||
|
<div className="flex min-h-[48px] items-center rounded-r-2 border border-border bg-surface p-sp-3">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
<span className="font-mono text-fs-11 text-faint">{label}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fakeSession(overrides: Partial<Session>): Session {
|
||||||
|
return {
|
||||||
|
id: "00000000-0000-0000-0000-000000000000",
|
||||||
|
cwd: "/home/dev/maestro",
|
||||||
|
command: "claude",
|
||||||
|
env: {},
|
||||||
|
metadata: {},
|
||||||
|
status: "Running",
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
activity_since: new Date(Date.now() - 4 * 60_000).toISOString(),
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const NOOP_CARD_HANDLERS: Omit<SessionCardProps, "session" | "depth" | "now" | "highlighted" | "active" | "open" | "previewing" | "renaming"> = {
|
||||||
|
onSelect: () => {},
|
||||||
|
onOpenMenu: () => {},
|
||||||
|
onSpawnSimilar: () => {},
|
||||||
|
onKill: () => {},
|
||||||
|
onRenameCommit: () => {},
|
||||||
|
onRenameCancel: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const CARD_SPECIMENS: Array<{ label: string; props: Omit<SessionCardProps, keyof typeof NOOP_CARD_HANDLERS> }> = [
|
||||||
|
{
|
||||||
|
label: "idle",
|
||||||
|
props: {
|
||||||
|
session: fakeSession({ command: "bash" }),
|
||||||
|
depth: 0,
|
||||||
|
now: Date.now(),
|
||||||
|
highlighted: false,
|
||||||
|
active: false,
|
||||||
|
open: false,
|
||||||
|
previewing: false,
|
||||||
|
renaming: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "active, focused",
|
||||||
|
props: {
|
||||||
|
session: fakeSession({ activity: "Active", command: "vite dev" }),
|
||||||
|
depth: 0,
|
||||||
|
now: Date.now(),
|
||||||
|
highlighted: false,
|
||||||
|
active: true,
|
||||||
|
open: true,
|
||||||
|
previewing: false,
|
||||||
|
renaming: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "awaiting-input",
|
||||||
|
props: {
|
||||||
|
session: fakeSession({ activity: "AwaitingInput", activity_since: new Date(Date.now() - 12_000).toISOString() }),
|
||||||
|
depth: 0,
|
||||||
|
now: Date.now(),
|
||||||
|
highlighted: false,
|
||||||
|
active: false,
|
||||||
|
open: false,
|
||||||
|
previewing: false,
|
||||||
|
renaming: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "error",
|
||||||
|
props: {
|
||||||
|
session: fakeSession({ activity: "Error", activity_detail: "tests failing", activity_since: new Date(Date.now() - 7_200_000).toISOString() }),
|
||||||
|
depth: 0,
|
||||||
|
now: Date.now(),
|
||||||
|
highlighted: false,
|
||||||
|
active: false,
|
||||||
|
open: false,
|
||||||
|
previewing: false,
|
||||||
|
renaming: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "previewing",
|
||||||
|
props: {
|
||||||
|
session: fakeSession({ command: "opencode" }),
|
||||||
|
depth: 0,
|
||||||
|
now: Date.now(),
|
||||||
|
highlighted: false,
|
||||||
|
active: false,
|
||||||
|
open: false,
|
||||||
|
previewing: true,
|
||||||
|
renaming: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "exited",
|
||||||
|
props: {
|
||||||
|
session: fakeSession({ status: "Dead", command: "btop" }),
|
||||||
|
depth: 0,
|
||||||
|
now: Date.now(),
|
||||||
|
highlighted: false,
|
||||||
|
active: false,
|
||||||
|
open: false,
|
||||||
|
previewing: false,
|
||||||
|
renaming: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const ACTIVITY_STATES = Object.keys(STATE_WORDS) as Array<keyof typeof STATE_WORDS>;
|
||||||
|
|
||||||
|
export function DesignKit() {
|
||||||
|
const { theme, palette, toggleTheme, setPalette } = useTheme();
|
||||||
|
// "open" is deliberately display:none in the real app (.mv-pane's rule,
|
||||||
|
// styles.css) — a streaming pane already shows it's connected — so start on
|
||||||
|
// a state the kit page can actually show.
|
||||||
|
const [attachState, setAttachState] = useState<"connecting" | "open" | "closed">("connecting");
|
||||||
|
|
||||||
|
return (
|
||||||
|
// #root is a fixed-height, overflow-hidden flex column (main.css) built for
|
||||||
|
// the app shell's own internal scrolling — this page needs its own scroll
|
||||||
|
// container to match, or everything past the first viewport is clipped.
|
||||||
|
<div className="h-full overflow-y-auto bg-bg text-ink">
|
||||||
|
<header className="flex items-center justify-between border-b border-border px-sp-4 py-sp-3">
|
||||||
|
<h1 className="text-fs-20 font-semibold">
|
||||||
|
maestro<span className="text-accent">_</span> · design kit
|
||||||
|
</h1>
|
||||||
|
<div className="flex items-center gap-sp-2">
|
||||||
|
<button type="button" className="btn btn-ghost" onClick={toggleTheme}>
|
||||||
|
{theme === "dark" ? "☀ light" : "☾ dark"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="mx-auto max-w-5xl px-sp-4 py-sp-4">
|
||||||
|
<Section
|
||||||
|
title="Colour palettes"
|
||||||
|
note="6 palettes, picked via data-palette on <html> through ThemeContext. Switching one recolours every token below live."
|
||||||
|
>
|
||||||
|
{PALETTES.map((p) => (
|
||||||
|
<button
|
||||||
|
key={p}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setPalette(p)}
|
||||||
|
className={`flex flex-col items-center gap-sp-1 rounded-r-2 border p-sp-2 ${
|
||||||
|
p === palette ? "border-accent" : "border-border"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="h-sp-6 w-sp-8 rounded-r-1"
|
||||||
|
style={{ background: PALETTE_ACCENT[p] }}
|
||||||
|
/>
|
||||||
|
<span className="font-mono text-fs-11 text-muted">{p}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title="Colour tokens" note="Current palette + theme, applied through var(--token).">
|
||||||
|
{COLOR_TOKENS.map((t) => (
|
||||||
|
<div key={t} className="flex flex-col items-start gap-sp-1">
|
||||||
|
<div
|
||||||
|
className="h-sp-8 w-sp-8 rounded-r-2 border border-border"
|
||||||
|
style={{ background: `var(${t})` }}
|
||||||
|
/>
|
||||||
|
<span className="font-mono text-fs-11 text-faint">{t}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title="Typography" note="fontSize -> var(--fs-N), from tailwind.config.ts.">
|
||||||
|
{FONT_SIZE_TOKENS.map(({ token, sizeClass }) => (
|
||||||
|
<Specimen key={token} label={token}>
|
||||||
|
<span className={`font-mono ${sizeClass}`}>Sessions</span>
|
||||||
|
</Specimen>
|
||||||
|
))}
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title="Spacing scale" note="spacing -> var(--sp-N).">
|
||||||
|
{SPACING_TOKENS.map(({ token, widthClass }) => (
|
||||||
|
<div key={token} className="flex flex-col items-start gap-sp-1">
|
||||||
|
<div className={`h-sp-3 bg-accent ${widthClass}`} />
|
||||||
|
<span className="font-mono text-fs-11 text-faint">{token}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title="Border radii" note="borderRadius -> var(--r-N).">
|
||||||
|
{RADIUS_TOKENS.map(({ token, radiusClass }) => (
|
||||||
|
<div key={token} className="flex flex-col items-start gap-sp-1">
|
||||||
|
<div className={`h-sp-6 w-sp-8 border-2 border-accent ${radiusClass}`} />
|
||||||
|
<span className="font-mono text-fs-11 text-faint">{token}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title="Z-index scale" note="zIndex -> var(--z-*).">
|
||||||
|
{Z_INDEX_TOKENS.map((z) => (
|
||||||
|
<div key={z.name} className="flex items-baseline gap-sp-2 font-mono text-fs-12">
|
||||||
|
<span className="text-muted">{z.name}</span>
|
||||||
|
<span className="text-ink">{z.value}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title="Buttons" note="Terminal-native bracket chrome — .btn / .btn-primary / .btn-ghost / .btn-icon / .btn-danger.">
|
||||||
|
<Specimen label="btn">
|
||||||
|
<button type="button" className="btn">Cancel</button>
|
||||||
|
</Specimen>
|
||||||
|
<Specimen label="btn-primary">
|
||||||
|
<button type="button" className="btn btn-primary">+ Spawn</button>
|
||||||
|
</Specimen>
|
||||||
|
<Specimen label="btn-ghost">
|
||||||
|
<button type="button" className="btn btn-ghost">Dismiss</button>
|
||||||
|
</Specimen>
|
||||||
|
<Specimen label="btn-icon">
|
||||||
|
<button type="button" className="btn btn-icon">✕</button>
|
||||||
|
</Specimen>
|
||||||
|
<Specimen label="btn-danger">
|
||||||
|
<button type="button" className="btn btn-danger">Kill</button>
|
||||||
|
</Specimen>
|
||||||
|
<Specimen label="btn:disabled">
|
||||||
|
<button type="button" className="btn btn-primary" disabled>+ Spawn</button>
|
||||||
|
</Specimen>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title="Activity rail" note="ActivityRail.tsx — one state per session card, colour-coded and never colour-only (title/aria-label carry the word).">
|
||||||
|
{ACTIVITY_STATES.map((slug) => (
|
||||||
|
<Specimen key={slug} label={`${slug} · ${STATE_WORDS[slug]}`}>
|
||||||
|
<div className="relative h-[32px] w-[24px] overflow-hidden rounded-r-1 bg-surface-2">
|
||||||
|
<ActivityRail activity={slug} />
|
||||||
|
</div>
|
||||||
|
</Specimen>
|
||||||
|
))}
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title="Session cards" note="SessionCard.tsx, docs/SIDEBAR.md states.">
|
||||||
|
<div className="w-full max-w-sm divide-y divide-border rounded-r-2 border border-border bg-surface-2">
|
||||||
|
{CARD_SPECIMENS.map(({ label, props }) => (
|
||||||
|
<div key={label} className="relative">
|
||||||
|
<SessionCard {...props} {...NOOP_CARD_HANDLERS} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title="Terminal chrome" note="attach-header + attach-body, as worn by TerminalPane/StructuredPane.">
|
||||||
|
<div className="w-full max-w-lg overflow-hidden rounded-r-2 border border-border">
|
||||||
|
<div className="attach-header">
|
||||||
|
<PaneTitle
|
||||||
|
sessionId="11111111-2222-3333-4444-555555555555"
|
||||||
|
session={fakeSession({ command: "claude" })}
|
||||||
|
/>
|
||||||
|
<span className="flex-1" />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="attach-state"
|
||||||
|
data-state={attachState}
|
||||||
|
onClick={() =>
|
||||||
|
setAttachState((s) => (s === "open" ? "connecting" : s === "connecting" ? "closed" : "open"))
|
||||||
|
}
|
||||||
|
title="Click to cycle"
|
||||||
|
>
|
||||||
|
{attachState}
|
||||||
|
</button>
|
||||||
|
<button type="button" className="btn btn-icon disconnect" title="Disconnect">
|
||||||
|
⏻
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="attach-body h-[120px] items-start">
|
||||||
|
<span className="font-mono text-fs-13 text-muted">$ pnpm test</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+16
-1
@@ -1,5 +1,6 @@
|
|||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import { App } from "./App.js";
|
import { App } from "./App.js";
|
||||||
|
import { ThemeProvider } from "./contexts/ThemeContext.js";
|
||||||
import "./main.css";
|
import "./main.css";
|
||||||
|
|
||||||
const container = document.getElementById("root");
|
const container = document.getElementById("root");
|
||||||
@@ -7,4 +8,18 @@ if (!container) {
|
|||||||
throw new Error("missing #root element");
|
throw new Error("missing #root element");
|
||||||
}
|
}
|
||||||
|
|
||||||
createRoot(container).render(<App />);
|
const root = createRoot(container);
|
||||||
|
|
||||||
|
// Dev-only: `?kit` renders the standalone design kit instead of the app
|
||||||
|
// shell (#254). Dynamic import keeps it out of the production bundle.
|
||||||
|
if (import.meta.env.DEV && new URLSearchParams(location.search).has("kit")) {
|
||||||
|
void import("./components/kit/DesignKit.js").then(({ DesignKit }) => {
|
||||||
|
root.render(
|
||||||
|
<ThemeProvider>
|
||||||
|
<DesignKit />
|
||||||
|
</ThemeProvider>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
root.render(<App />);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user