mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-08 17:31:56 -10:00
shell-redesign V1 attempt-3 Phase 5 P5-8: HumanSeatCard VellumCard refactor
Phase 1 primitive consumption per code-map AFTER tree (Phase 5 row). HumanSeatCard predates the Phase 1 vellum primitives; the ad-hoc `border border-stone-300 bg-stone-50 p-3` chrome is replaced with the canonical VellumCard (RegistrationMarks at 4 corners + 1px outline- variant border + cream paper background + hard-shadow). SectionHeader + StatusPip primitives consumed for the eyebrow + pending/blocked indicators. Capability pills now use border-outline-variant (1px ghost line per universal-shell.md L43-L48 doctrine) instead of the legacy border-stone-300. Visual delta: card now reads as architectural drafting / paper-layered matching the rest of V1; pending=2 / 1 blocked / capabilities pills all render in tactical aesthetic. Existing testids preserved (mc-human-seat-card / mc-human-seat-session / mc-human-seat-pending / mc-human-seat-blocked) so any consumer testing this card keeps working. NEW packages/ui/test/p5-human-seat-card.test.tsx (5 tests): - VellumCard registration marks composed. - Pending count + StatusPip pending/all-clear branches. - Blocked-count → StatusPip warning-tone with "N blocked" label. - Capability pills carry border-outline-variant class. - Source-assertion (ritual #9): no legacy stone-300/stone-50 chrome literals survive outside historical comments; VellumCard import present. Verification: - pnpm -F @openrig/ui exec tsc --noEmit: clean - vitest p5-human-seat-card: 5/5 pass - pnpm -F @openrig/ui build: clean - Existing HumanSeatCard consumers unaffected (testids preserved) Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
3981ad187e
commit
1fb50d15d4
2 files changed
+166
-30
No files matched your search
@@ -4,7 +4,17 @@
|
||||
// concepts, NOT invisible config-layer convention. Mission Control
|
||||
// renders the operator's human seat (default `human-wrandom@kernel`)
|
||||
// with its own card showing identity + load + capabilities.
|
||||
//
|
||||
// V1 attempt-3 Phase 5 P5-8: refactored to compose VellumCard primitive
|
||||
// (Phase 1) — replaces the ad-hoc `border border-stone-300 bg-stone-50`
|
||||
// chrome with the canonical vellum aesthetic (cream paper background +
|
||||
// 1px outline-variant border + RegistrationMarks + hard-shadow). Aligns
|
||||
// HumanSeatCard with the V1 tactical-dossier visual language for the
|
||||
// design-reviewer "professional grade met" V1 ship gate verdict.
|
||||
|
||||
import { VellumCard } from "../../ui/vellum-card.js";
|
||||
import { SectionHeader } from "../../ui/section-header.js";
|
||||
import { StatusPip } from "../../ui/status-pip.js";
|
||||
import type { CompactStatusRow } from "../hooks/useMissionControlView.js";
|
||||
|
||||
export interface HumanSeatCardProps {
|
||||
@@ -25,45 +35,68 @@ export function HumanSeatCard({
|
||||
(r) => r.state === "idle" || r.state === "attention" || r.state === "blocked",
|
||||
).length;
|
||||
const blockedCount = rows.filter((r) => r.state === "blocked").length;
|
||||
// Reflect attention level via the same StatusPip taxonomy used elsewhere
|
||||
// (warning for blocked, info for plain pending). Keeps the card's
|
||||
// semantic palette aligned with the rest of V1.
|
||||
const pendingTone =
|
||||
blockedCount > 0 ? "warning" : pendingCount > 0 ? "info" : "active";
|
||||
return (
|
||||
<div
|
||||
data-testid="mc-human-seat-card"
|
||||
<VellumCard
|
||||
testId="mc-human-seat-card"
|
||||
data-session={session}
|
||||
className="border border-stone-300 bg-stone-50 p-3"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="font-mono text-[10px] uppercase tracking-[0.14em] text-stone-500">
|
||||
human seat
|
||||
<div className="px-4 py-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<SectionHeader tone="muted">Human seat</SectionHeader>
|
||||
<div
|
||||
data-testid="mc-human-seat-session"
|
||||
className="mt-1 font-mono text-sm text-stone-900 truncate"
|
||||
>
|
||||
{session}
|
||||
</div>
|
||||
</div>
|
||||
<div data-testid="mc-human-seat-session" className="font-mono text-sm text-stone-900">
|
||||
{session}
|
||||
<div className="text-right shrink-0">
|
||||
<div
|
||||
data-testid="mc-human-seat-pending"
|
||||
className="font-mono text-2xl font-bold text-stone-900 leading-none"
|
||||
>
|
||||
{pendingCount}
|
||||
</div>
|
||||
<div className="font-mono text-[9px] uppercase tracking-[0.12em] text-on-surface-variant mt-1">
|
||||
pending
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div data-testid="mc-human-seat-pending" className="font-mono text-2xl text-stone-900">
|
||||
{pendingCount}
|
||||
{blockedCount > 0 ? (
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<StatusPip
|
||||
status="warning"
|
||||
label={`${blockedCount} blocked`}
|
||||
variant="pill"
|
||||
testId="mc-human-seat-blocked"
|
||||
/>
|
||||
</div>
|
||||
<div className="font-mono text-[9px] uppercase tracking-[0.12em] text-stone-500">
|
||||
pending
|
||||
) : (
|
||||
<div className="mt-3">
|
||||
<StatusPip
|
||||
status={pendingTone}
|
||||
label={pendingCount === 0 ? "all clear" : `${pendingCount} pending`}
|
||||
variant="pill"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-3 flex flex-wrap gap-1">
|
||||
{capabilities.map((cap) => (
|
||||
<span
|
||||
key={cap}
|
||||
className="border border-outline-variant px-1.5 py-0.5 font-mono text-[9px] uppercase tracking-[0.1em] text-on-surface-variant bg-white/30"
|
||||
>
|
||||
{cap}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{blockedCount > 0 ? (
|
||||
<div data-testid="mc-human-seat-blocked" className="mt-2 font-mono text-[10px] text-red-800">
|
||||
{blockedCount} blocked
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{capabilities.map((cap) => (
|
||||
<span
|
||||
key={cap}
|
||||
className="border border-stone-300 px-1.5 py-0.5 font-mono text-[9px] uppercase tracking-[0.1em] text-stone-600"
|
||||
>
|
||||
{cap}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</VellumCard>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// V1 attempt-3 Phase 5 P5-8 — HumanSeatCard VellumCard refactor.
|
||||
//
|
||||
// Verifies HumanSeatCard composes the VellumCard primitive (Phase 1) +
|
||||
// renders RegistrationMarks at the canonical 4 corners + uses the
|
||||
// StatusPip primitive for pending/blocked state. Per ritual #6
|
||||
// (named-surface adopts primitive verifies at consumer level).
|
||||
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { render, cleanup } from "@testing-library/react";
|
||||
import { HumanSeatCard } from "../src/components/mission-control/components/HumanSeatCard.js";
|
||||
import type { CompactStatusRow } from "../src/components/mission-control/hooks/useMissionControlView.js";
|
||||
|
||||
beforeEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
function makeRow(state: CompactStatusRow["state"]): CompactStatusRow {
|
||||
return {
|
||||
session: `seat-${state}@test`,
|
||||
role: "qa",
|
||||
pod: "test",
|
||||
rigName: "test",
|
||||
rigId: "rig-1",
|
||||
logicalId: `seat-${state}`,
|
||||
runtime: "claude-code",
|
||||
state,
|
||||
} as CompactStatusRow;
|
||||
}
|
||||
|
||||
describe("HumanSeatCard P5-8 VellumCard composition", () => {
|
||||
it("composes VellumCard chrome (registration marks present)", () => {
|
||||
const { container } = render(
|
||||
<HumanSeatCard
|
||||
session="human-wrandom@kernel"
|
||||
rows={[makeRow("idle"), makeRow("attention")]}
|
||||
/>,
|
||||
);
|
||||
// VellumCard composes RegistrationMarks; reg-mark testids should be
|
||||
// present at all 4 corners.
|
||||
expect(container.querySelector(".reg-mark, [data-testid$='-reg-tl']")).toBeTruthy();
|
||||
// The card root has the canonical testid.
|
||||
expect(container.querySelector("[data-testid='mc-human-seat-card']")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders pending count + StatusPip 'pending' when no blocked rows", () => {
|
||||
const { container, getByTestId } = render(
|
||||
<HumanSeatCard
|
||||
session="human-wrandom@kernel"
|
||||
rows={[makeRow("idle"), makeRow("attention")]}
|
||||
/>,
|
||||
);
|
||||
expect(getByTestId("mc-human-seat-pending").textContent).toBe("2");
|
||||
// No blocked StatusPip when 0 blocked.
|
||||
expect(container.querySelector("[data-testid='mc-human-seat-blocked']")).toBeNull();
|
||||
});
|
||||
|
||||
it("surfaces blocked-count via StatusPip when any row is blocked (warning tone)", () => {
|
||||
const { getByTestId } = render(
|
||||
<HumanSeatCard
|
||||
session="human-wrandom@kernel"
|
||||
rows={[makeRow("blocked"), makeRow("idle")]}
|
||||
/>,
|
||||
);
|
||||
const pip = getByTestId("mc-human-seat-blocked");
|
||||
expect(pip.textContent).toContain("1 blocked");
|
||||
});
|
||||
|
||||
it("renders capability pills with outline-variant border (vellum aesthetic)", () => {
|
||||
const { container } = render(
|
||||
<HumanSeatCard
|
||||
session="human-wrandom@kernel"
|
||||
rows={[]}
|
||||
capabilities={["approve", "deny"]}
|
||||
/>,
|
||||
);
|
||||
const pills = Array.from(container.querySelectorAll("span")).filter((s) =>
|
||||
["approve", "deny"].includes(s.textContent?.trim() ?? ""),
|
||||
);
|
||||
expect(pills.length).toBe(2);
|
||||
// Each pill has the outline-variant border class (1px doctrine).
|
||||
for (const pill of pills) {
|
||||
expect(pill.className).toMatch(/border-outline-variant/);
|
||||
}
|
||||
});
|
||||
|
||||
it("source asserts no legacy stone-50/stone-300 ad-hoc card chrome remains (ritual #9)", async () => {
|
||||
const { readFileSync } = await import("node:fs");
|
||||
const path = await import("node:path");
|
||||
const src = readFileSync(
|
||||
path.resolve(
|
||||
__dirname,
|
||||
"../src/components/mission-control/components/HumanSeatCard.tsx",
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
// The legacy chrome was `border border-stone-300 bg-stone-50 p-3` —
|
||||
// negative-assertion that none of those literal patterns survive
|
||||
// outside the historical comment.
|
||||
const codeOnly = src.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
||||
expect(codeOnly).not.toMatch(/border-stone-300\s+bg-stone-50/);
|
||||
expect(codeOnly).toMatch(/VellumCard/);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user