mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-08 17:31:56 -10:00
Refine vellum and terminal preview surfaces
This commit is contained in:
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 }}
|
||||
|
||||
@@ -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/);
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user