Refine vellum and terminal preview surfaces

This commit is contained in:
mvschwarz committed 2026-05-07 02:48:23 -07:00
1 parent 7d4334b1de
commit 15115fdc04
15 files changed
+114 -25

No files matched your search

@@ -2,6 +2,7 @@ import { Link } from "@tanstack/react-router";
import { Sparkles } from "lucide-react";
import { VellumCard } from "../../ui/vellum-card.js";
import { SectionHeader } from "../../ui/section-header.js";
import { dashboardCardSurfaceClass } from "./card-surface.js";
export function ForYouCard() {
return (
@@ -10,7 +11,7 @@ export function ForYouCard() {
data-testid="dashboard-card-for-you"
className="block focus-visible:outline focus-visible:outline-2 focus-visible:outline-stone-900 focus-visible:outline-offset-2"
>
<VellumCard className="h-full hover:hard-shadow-hover" accentClass="border-l-4 border-l-tertiary">
<VellumCard className={dashboardCardSurfaceClass} accentClass="border-l-4 border-l-tertiary">
<div className="bg-stone-900 text-white px-4 py-1.5 flex items-center gap-2">
<Sparkles className="h-3 w-3" />
<SectionHeader tone="default" className="text-stone-50">
@@ -2,6 +2,7 @@ import { Link } from "@tanstack/react-router";
import { Folder } from "lucide-react";
import { VellumCard } from "../../ui/vellum-card.js";
import { SectionHeader } from "../../ui/section-header.js";
import { dashboardCardSurfaceClass } from "./card-surface.js";
export function ProjectCard() {
return (
@@ -10,7 +11,7 @@ export function ProjectCard() {
data-testid="dashboard-card-project"
className="block focus-visible:outline focus-visible:outline-2 focus-visible:outline-stone-900 focus-visible:outline-offset-2"
>
<VellumCard className="h-full hover:hard-shadow-hover">
<VellumCard className={dashboardCardSurfaceClass}>
<div className="bg-stone-900 text-white px-4 py-1.5 flex items-center gap-2">
<Folder className="h-3 w-3" />
<SectionHeader tone="default" className="text-stone-50">
@@ -2,6 +2,7 @@ import { Link } from "@tanstack/react-router";
import { Search } from "lucide-react";
import { VellumCard } from "../../ui/vellum-card.js";
import { SectionHeader } from "../../ui/section-header.js";
import { dashboardCardSurfaceClass } from "./card-surface.js";
export function SearchCard() {
return (
@@ -10,7 +11,7 @@ export function SearchCard() {
data-testid="dashboard-card-search"
className="block focus-visible:outline focus-visible:outline-2 focus-visible:outline-stone-900 focus-visible:outline-offset-2"
>
<VellumCard className="h-full hover:hard-shadow-hover" variant="ghost" elevation="flat">
<VellumCard className={dashboardCardSurfaceClass} variant="ghost" elevation="flat">
<div className="border-b border-outline-variant px-4 py-1.5 flex items-center gap-2">
<Search className="h-3 w-3 text-stone-700" />
<SectionHeader tone="muted">Search & Audit</SectionHeader>
@@ -2,6 +2,7 @@ import { Link } from "@tanstack/react-router";
import { Cog } from "lucide-react";
import { VellumCard } from "../../ui/vellum-card.js";
import { SectionHeader } from "../../ui/section-header.js";
import { dashboardCardSurfaceClass } from "./card-surface.js";
export function SettingsCard() {
return (
@@ -10,7 +11,7 @@ export function SettingsCard() {
data-testid="dashboard-card-settings"
className="block focus-visible:outline focus-visible:outline-2 focus-visible:outline-stone-900 focus-visible:outline-offset-2"
>
<VellumCard className="h-full hover:hard-shadow-hover" variant="ghost" elevation="flat">
<VellumCard className={dashboardCardSurfaceClass} variant="ghost" elevation="flat">
<div className="border-b border-outline-variant px-4 py-1.5 flex items-center gap-2">
<Cog className="h-3 w-3 text-stone-700" />
<SectionHeader tone="muted">Settings</SectionHeader>
@@ -3,6 +3,7 @@ import { FileText } from "lucide-react";
import { VellumCard } from "../../ui/vellum-card.js";
import { SectionHeader } from "../../ui/section-header.js";
import { useSpecLibrary } from "../../../hooks/useSpecLibrary.js";
import { dashboardCardSurfaceClass } from "./card-surface.js";
export function SpecsCard() {
const { data: library } = useSpecLibrary();
@@ -14,7 +15,7 @@ export function SpecsCard() {
data-testid="dashboard-card-specs"
className="block focus-visible:outline focus-visible:outline-2 focus-visible:outline-stone-900 focus-visible:outline-offset-2"
>
<VellumCard className="h-full hover:hard-shadow-hover">
<VellumCard className={dashboardCardSurfaceClass}>
<div className="bg-stone-900 text-white px-4 py-1.5 flex items-center gap-2">
<FileText className="h-3 w-3" />
<SectionHeader tone="default" className="text-stone-50">
@@ -11,6 +11,7 @@ import { VellumCard } from "../../ui/vellum-card.js";
import { SectionHeader } from "../../ui/section-header.js";
import { useRigSummary } from "../../../hooks/useRigSummary.js";
import { usePsEntries } from "../../../hooks/usePsEntries.js";
import { dashboardCardSurfaceClass } from "./card-surface.js";
export function TopologyCard() {
const { data: rigs } = useRigSummary();
@@ -25,7 +26,7 @@ export function TopologyCard() {
data-testid="dashboard-card-topology"
className="block focus-visible:outline focus-visible:outline-2 focus-visible:outline-stone-900 focus-visible:outline-offset-2"
>
<VellumCard className="h-full hover:hard-shadow-hover">
<VellumCard className={dashboardCardSurfaceClass}>
<div className="bg-stone-900 text-white px-4 py-1.5 flex items-center gap-2">
<Network className="h-3 w-3" />
<SectionHeader tone="default" className="text-stone-50">
@@ -0,0 +1 @@
export const dashboardCardSurfaceClass = "h-full bg-white/30 hover:bg-white/40 hover:hard-shadow-hover";
@@ -60,7 +60,7 @@ export function SessionPreviewPane({
className={cn(
"space-y-1",
compactTerminal
? "border-0 bg-transparent p-0 text-stone-100"
? "border-0 bg-transparent p-0 text-stone-50"
: "border border-stone-300/40 bg-white/8 px-3 py-2",
)}
>
@@ -75,7 +75,7 @@ export function SessionPreviewPane({
{preview.isError && (
<div
data-testid={`${testIdPrefix}-error`}
className={cn("font-mono text-[9px]", compactTerminal ? "text-red-300" : "text-red-600")}
className={cn("font-mono text-[9px]", compactTerminal ? "text-red-200" : "text-red-600")}
>
{(preview.error as Error)?.message ?? "Preview failed."}
</div>
@@ -85,16 +85,25 @@ export function SessionPreviewPane({
data-testid={`${testIdPrefix}-unavailable`}
className={cn(
"font-mono space-y-0.5",
compactTerminal ? "text-[10px] text-stone-300" : "text-[9px] text-stone-500",
compactTerminal ? "text-[10px] text-stone-50" : "text-[9px] text-stone-500",
)}
>
<div>Preview unavailable: {preview.data.reason}.</div>
{preview.data.hint && (
<div className={compactTerminal ? "text-stone-500" : "text-stone-400"}>{preview.data.hint}</div>
{compactTerminal ? (
<>
<div>Preview unavailable.</div>
<div className="text-stone-400">$ waiting for terminal output</div>
</>
) : (
<>
<div>Preview unavailable: {preview.data.reason}.</div>
{preview.data.hint && (
<div className="text-stone-400">{preview.data.hint}</div>
)}
<div className="text-stone-400">
Use <code>rig capture {sessionName}</code> from terminal as a fallback.
</div>
</>
)}
<div className={compactTerminal ? "text-stone-500" : "text-stone-400"}>
Use <code>rig capture {sessionName}</code> from terminal as a fallback.
</div>
</div>
)}
{!isNodePreviewUnavailable(preview.data) && preview.data && (
@@ -106,7 +115,7 @@ export function SessionPreviewPane({
className={cn(
"font-mono overflow-y-auto whitespace-pre-wrap break-all",
compactTerminal
? "max-h-72 bg-transparent px-1 py-0.5 text-[10px] leading-[1.25] text-stone-100"
? "max-h-72 bg-transparent px-1 py-0.5 text-[10px] leading-[1.25] text-stone-50"
: "max-h-32 bg-stone-50 px-2 py-1 text-[9px] text-stone-800",
)}
>
@@ -71,7 +71,7 @@ export function HybridPodGroupNode({ data }: { data: HybridPodGroupNodeData }) {
return (
<div
data-testid="hybrid-pod-group-node"
className="relative h-full w-full border border-dashed border-stone-400/55 bg-stone-50/35"
className="relative h-full w-full border border-dashed border-stone-400/55 bg-stone-50/25"
>
<div className="absolute left-2 top-2 flex items-center gap-2 font-mono text-[9px] lowercase tracking-[0.02em] text-stone-600">
<span>{label}</span>
@@ -114,7 +114,7 @@ export function HybridAgentNode({ data }: { data: HybridAgentNodeData }) {
data-activity-card-state={activityCard.state}
data-activity-card-flash={activityCard.flash ?? "none"}
className={cn(
"group relative h-full w-full select-none border hard-shadow transition-[background-color,border-color,box-shadow] duration-300",
"group relative h-full w-full select-none border bg-white/30 hard-shadow transition-[background-color,border-color,box-shadow] duration-300",
getActivityCardClasses({
state: activityCard.state,
flash: activityCard.flash,
@@ -54,8 +54,8 @@ export function RigGroupNode({ data }: { data: RigGroupNodeData }) {
className={cn(
"w-full h-full relative flex flex-col cursor-pointer select-none overflow-visible",
collapsed
? "border border-outline-variant bg-white/40 backdrop-blur-[8px] hard-shadow hover:bg-white/60"
: "border border-outline-variant/70 bg-white/[0.14] backdrop-blur-[2px] shadow-[0_0_0_1px_rgba(84,96,115,0.06)]",
? "border border-outline-variant bg-white/30 backdrop-blur-[8px] hard-shadow hover:bg-white/40"
: "border border-outline-variant/70 bg-white/[0.10] backdrop-blur-[2px] shadow-[0_0_0_1px_rgba(84,96,115,0.06)]",
recentActivity && "rig-activity-frame-pulse",
)}
>
@@ -127,8 +127,8 @@ export function TerminalPreviewPopover({
data-testid={`${testIdPrefix}-terminal-popover`}
data-reduced-motion={reducedMotion ? "true" : "false"}
className={cn(
"nodrag nopan fixed z-[1000] w-80 border border-outline-variant bg-stone-950 p-1.5 hard-shadow",
"cursor-default select-text text-stone-100",
"nodrag nopan fixed z-[1000] w-80 bg-stone-950/65 p-1.5 backdrop-blur-sm",
"cursor-default select-text text-stone-50",
popoverClassName,
)}
style={{ left: position.left, top: position.top }}
+1 -1
View File
@@ -364,7 +364,7 @@
}
}
.activity-card-surface {
background-color: rgba(255, 255, 255, 0.96);
background-color: rgba(255, 255, 255, 0.72);
}
.activity-card-active {
background-color: rgba(220, 252, 231, 0.88);
@@ -0,0 +1,28 @@
import { describe, expect, it } from "vitest";
import { readFileSync } from "node:fs";
import path from "node:path";
import { dashboardCardSurfaceClass } from "../src/components/dashboard/cards/card-surface.js";
describe("Dashboard card vellum surface", () => {
it("uses a shared translucent vellum body across all six dashboard cards", () => {
const srcRoot = path.resolve(__dirname, "../src/components/dashboard/cards");
const cardFiles = [
"TopologyCard.tsx",
"ProjectCard.tsx",
"ForYouCard.tsx",
"SpecsCard.tsx",
"SearchCard.tsx",
"SettingsCard.tsx",
];
expect(dashboardCardSurfaceClass).toContain("bg-white/30");
expect(dashboardCardSurfaceClass).toContain("hover:bg-white/40");
expect(dashboardCardSurfaceClass).toContain("hover:hard-shadow-hover");
for (const file of cardFiles) {
const source = readFileSync(path.join(srcRoot, file), "utf8");
expect(source).toContain("dashboardCardSurfaceClass");
expect(source).not.toContain('className="h-full hover:hard-shadow-hover"');
}
});
});
@@ -206,6 +206,11 @@ describe("P5.3 ActivityRing and HotPotatoEdge", () => {
expect(driverPopover.parentElement).toBe(document.body);
expect(driverPopover.className).toContain("fixed");
expect(driverPopover.className).toContain("z-[1000]");
expect(driverPopover.className).toContain("bg-stone-950/65");
expect(driverPopover.className).toContain("text-stone-50");
expect(driverPopover.className).toContain("backdrop-blur-sm");
expect(driverPopover.className).not.toContain("border");
expect(driverPopover.className).not.toContain("hard-shadow");
expect(driverPopover.style.left).not.toBe("");
expect(driverPopover.style.top).not.toBe("");
@@ -226,6 +231,7 @@ describe("P5.3 ActivityRing and HotPotatoEdge", () => {
const rigGraph = readFileSync(path.join(srcRoot, "components/RigGraph.tsx"), "utf8");
const rigNode = readFileSync(path.join(srcRoot, "components/RigNode.tsx"), "utf8");
const hybridNodes = readFileSync(path.join(srcRoot, "components/topology/HybridTopologyNodes.tsx"), "utf8");
const rigGroupNode = readFileSync(path.join(srcRoot, "components/topology/RigGroupNode.tsx"), "utf8");
const terminalPopover = readFileSync(path.join(srcRoot, "components/topology/TerminalPreviewPopover.tsx"), "utf8");
const activityCards = readFileSync(path.join(srcRoot, "components/topology/activity-card-visuals.ts"), "utf8");
const table = readFileSync(path.join(srcRoot, "components/topology/TopologyTableView.tsx"), "utf8");
@@ -250,12 +256,25 @@ describe("P5.3 ActivityRing and HotPotatoEdge", () => {
expect(terminalPopover).toMatch(/createPortal/);
expect(terminalPopover).toMatch(/document\.body/);
expect(terminalPopover).toMatch(/variant="compact-terminal"/);
expect(terminalPopover).toMatch(/bg-stone-950/);
expect(terminalPopover).toMatch(/bg-stone-950\/65/);
expect(terminalPopover).toMatch(/text-stone-50/);
expect(terminalPopover).toMatch(/backdrop-blur-sm/);
expect(terminalPopover).not.toMatch(/border-outline-variant/);
expect(terminalPopover).not.toMatch(/hard-shadow/);
expect(terminalPopover).toMatch(/z-\[1000\]/);
expect(terminalPopover).not.toMatch(/absolute left-full/);
expect(terminalPopover).not.toMatch(/terminal preview/);
expect(terminalPopover).not.toMatch(/terminal-close/);
expect(rigGroupNode).toMatch(/bg-white\/30/);
expect(rigGroupNode).toMatch(/hover:bg-white\/40/);
expect(rigGroupNode).toMatch(/bg-white\/\[0\.10\]/);
expect(rigGroupNode).not.toMatch(/bg-white\/\[0\.14\]/);
expect(rigGroupNode).not.toMatch(/hover:bg-white\/60/);
expect(hybridNodes).toMatch(/bg-white\/30/);
expect(hybridNodes).toMatch(/bg-stone-50\/25/);
expect(activityCards).toMatch(/activity-card-active/);
expect(css).toMatch(/background-color: rgba\(255, 255, 255, 0\.72\)/);
expect(css).not.toMatch(/background-color: rgba\(255, 255, 255, 0\.96\)/);
expect(rigGraph).toMatch(/activityRing/);
expect(table).toMatch(/ActivityRing/);
expect(table).toMatch(/group-hover:opacity-100/);
+27 -1
View File
@@ -90,9 +90,35 @@ describe("SessionPreviewPane", () => {
expect(pane.getAttribute("data-variant")).toBe("compact-terminal");
expect(content.className).toContain("text-[10px]");
expect(content.className).toContain("text-stone-100");
expect(content.className).toContain("text-stone-50");
expect(screen.queryByText(/live preview/i)).toBeNull();
expect(screen.queryByText(/captured/i)).toBeNull();
expect(screen.queryByText(/1 lines/i)).toBeNull();
});
it("renders compact unavailable state without machine labels or fallback chrome", async () => {
mockFetch.mockImplementation(async (url: string) => {
if (url === "/api/config") return new Response(JSON.stringify(settingsResponse()));
if (url.includes("/api/sessions/")) {
return new Response(JSON.stringify({ error: "preview_unavailable" }), { status: 404 });
}
return new Response(JSON.stringify({}), { status: 404 });
});
withQueryClient(
<SessionPreviewPane
sessionName="driver@test-rig"
testIdPrefix="compact-terminal-test"
variant="compact-terminal"
/>,
);
const unavailable = await screen.findByTestId("compact-terminal-test-unavailable");
expect(unavailable.className).toContain("text-stone-50");
expect(unavailable.textContent).toContain("Preview unavailable.");
expect(unavailable.textContent).toContain("$ waiting for terminal output");
expect(unavailable.textContent).not.toContain("preview_unavailable");
expect(unavailable.textContent).not.toContain("rig capture");
});
});