feat: make interrupt approval the demo hero

This commit is contained in:
lda
2026-07-08 08:08:32 +07:00 Verified
parent 349b84c010
commit 35a7b5cf2e
4 changed files with 72 additions and 2 deletions
@@ -105,6 +105,22 @@ describe("DemoWorkflowScene", () => {
);
});
it("makes the Scene 10 approval contract the primary visual", () => {
renderBeat("approval", "interrupt-evidence");
const stage = screen.getByLabelText("demo workflow stage");
expect(stage).toHaveAttribute("data-demo-layout", "approval");
expect(screen.getByLabelText("typed interrupt contract")).toHaveAttribute("data-hero", "true");
expect(screen.getByLabelText("workflow graph")).toBeInTheDocument();
});
it("marks trace beat as evidence layout", () => {
renderBeat("trace", "interrupt-evidence");
expect(screen.getByLabelText("demo workflow stage")).toHaveAttribute("data-demo-layout", "evidence");
expect(screen.getByLabelText("workflow.runs.trace operation")).toBeInTheDocument();
});
it("renders resume and trace operations as expanded evidence summaries", () => {
const { unmount } = renderBeat("resume", "interrupt-evidence");
expect(screen.getByLabelText("workflow.runs.resume operation")).toBeInTheDocument();
@@ -20,6 +20,16 @@ type DemoWorkflowSceneProps = {
readonly openEvidence: () => void;
};
type DemoWorkflowLayout = "operation" | "graph" | "interrupt" | "approval" | "evidence";
const layoutForBeat = (beatId: string): DemoWorkflowLayout => {
if (beatId === "operation" || beatId === "resume") return "operation";
if (beatId === "interrupt") return "interrupt";
if (beatId === "approval") return "approval";
if (beatId === "trace" || beatId === "output") return "evidence";
return "graph";
};
const operationStageByBeat: Readonly<Record<string, DemoEvent["stage"] | undefined>> = {
operation: "run_start",
resume: "run_resume",
@@ -44,6 +54,7 @@ export const DemoWorkflowScene = ({
const currentEvent = currentStage ? findEvent(demo, currentStage) : null;
const contract = runStart ? projectInterruptContract(runStart) : null;
const execution = graphExecutionForBeat(beat.id);
const layout = layoutForBeat(beat.id);
const showExpandedOperation = beat.id === "operation" || beat.id === "resume" || beat.id === "trace";
const showGraph = beat.id === "graph" || beat.id === "interrupt" || beat.id === "approval" || beat.id === "output";
@@ -60,7 +71,7 @@ export const DemoWorkflowScene = ({
<p>{beat.caption}</p>
</StageCaption>
<div className="demo-workflow-stage" data-beat={beat.id}>
<div className="demo-workflow-stage" data-beat={beat.id} data-demo-layout={layout} aria-label="demo workflow stage">
{showExpandedOperation && currentEvent && (
<OperationBlock
event={currentEvent}
@@ -85,7 +96,11 @@ export const DemoWorkflowScene = ({
selectNode={selectNode}
/>
{contractMode && contract && (
<InterruptContractPreview contract={contract} mode={contractMode} />
<InterruptContractPreview
contract={contract}
mode={contractMode}
hero={layout === "approval"}
/>
)}
</div>
)}
@@ -5,15 +5,18 @@ import { formatJson } from "./format.js";
type InterruptContractPreviewProps = {
readonly contract: InterruptContractPresentation;
readonly mode: "preview" | "approval";
readonly hero?: boolean;
};
export const InterruptContractPreview = ({
contract,
mode,
hero = false,
}: InterruptContractPreviewProps) => (
<m.aside
className="interrupt-contract-preview"
data-mode={mode}
data-hero={hero ? "true" : "false"}
initial={{ opacity: 0, x: 10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.22, ease: [0.16, 1, 0.3, 1] }}
@@ -482,6 +482,42 @@
oklch(0.135 0.025 250);
}
.demo-workflow-stage[data-demo-layout="approval"] .demo-workflow-stage__graph,
.demo-workflow-stage[data-demo-layout="interrupt"] .demo-workflow-stage__graph {
grid-template-columns: minmax(19rem, 34%) minmax(0, 1fr);
gap: 1rem;
}
.demo-workflow-stage[data-demo-layout="approval"] .interrupt-contract-preview,
.demo-workflow-stage[data-demo-layout="interrupt"] .interrupt-contract-preview {
grid-column: 1;
grid-row: 1;
}
.demo-workflow-stage[data-demo-layout="approval"] .workflow-graph-stage,
.demo-workflow-stage[data-demo-layout="interrupt"] .workflow-graph-stage {
grid-column: 2;
grid-row: 1;
}
.interrupt-contract-preview[data-hero="true"] {
border-color: color-mix(in oklch, var(--accent-amber) 72%, white);
background:
linear-gradient(180deg, color-mix(in oklch, var(--accent-amber) 14%, transparent), transparent 48%),
color-mix(in oklch, var(--stage-surface) 92%, black);
}
.interrupt-contract-preview[data-hero="true"] h2 {
font-size: clamp(1.45rem, 2.3vw, 2rem);
}
@media (max-width: 1050px) {
.demo-workflow-stage[data-demo-layout="approval"] .demo-workflow-stage__graph,
.demo-workflow-stage[data-demo-layout="interrupt"] .demo-workflow-stage__graph {
grid-template-columns: minmax(17rem, 40%) minmax(0, 1fr);
}
}
.interrupt-contract-preview[data-mode="approval"] {
box-shadow: inset 0 3px 0 var(--accent-amber);
}