fix: improve presentation evidence and architecture figure
This commit is contained in:
@@ -245,8 +245,9 @@ legible without hiding raw evidence.
|
||||
|
||||
### Do:
|
||||
|
||||
- **Do** make the workflow graph, operation block, evidence drawer, lifecycle
|
||||
explorer, typed interrupt/resume panel, and trace the main visual surfaces.
|
||||
- **Do** make the workflow graph, operation block, presentation evidence
|
||||
receipt/inspector, lifecycle explorer, typed interrupt/resume panel, and trace
|
||||
the main visual surfaces.
|
||||
- **Do** keep `/console` familiar and product-like: readable tables, standard
|
||||
buttons, clear labels, predictable focus states.
|
||||
- **Do** let `/present` be cinematic with staged panels, larger text, and graph
|
||||
|
||||
@@ -32,8 +32,8 @@ the resulting runs remain auditable through traces and evidence records.
|
||||
|
||||
Chat is a framing device, not the core product. It may introduce or narrate a
|
||||
prepared workflow, but the main surfaces are the workflow graph, operation
|
||||
blocks, evidence drawer, lifecycle explorer, typed interrupt/resume panel, and
|
||||
run trace.
|
||||
blocks, presentation evidence receipt/inspector, lifecycle explorer, typed
|
||||
interrupt/resume panel, and run trace.
|
||||
|
||||
Success means a viewer can answer three questions quickly:
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { act, cleanup, render, screen } from "@testing-library/react";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeAll, describe, expect, it } from "vitest";
|
||||
|
||||
@@ -112,7 +112,7 @@ describe("PresentationRoute", () => {
|
||||
expect(screen.getByRole("dialog", { name: /evidence inspector/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("closes the inspector when navigation moves to another beat", async () => {
|
||||
it("closes the inspector from the explicit close action", async () => {
|
||||
const user = userEvent.setup();
|
||||
window.location.hash = "#scene/workflow-demo/operation";
|
||||
const { PresentationRoute } = await import("./PresentationRoute.js");
|
||||
@@ -122,4 +122,16 @@ describe("PresentationRoute", () => {
|
||||
await user.click(screen.getByRole("button", { name: /close evidence/i }));
|
||||
expect(screen.queryByRole("dialog", { name: /evidence inspector/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("returns to the receipt after closing the inspector on a receipt beat", async () => {
|
||||
const user = userEvent.setup();
|
||||
window.location.hash = "#scene/interrupt-evidence/trace";
|
||||
const { PresentationRoute } = await import("./PresentationRoute.js");
|
||||
render(<PresentationRoute />);
|
||||
await user.click(await screen.findByRole("button", { name: /inspect evidence/i }));
|
||||
expect(screen.getByRole("dialog", { name: /evidence inspector/i })).toBeInTheDocument();
|
||||
await user.click(screen.getByRole("button", { name: /close evidence/i }));
|
||||
expect(screen.queryByRole("dialog", { name: /evidence inspector/i })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /inspect evidence/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -91,10 +91,10 @@ export const EvidenceInspector = ({
|
||||
</label>
|
||||
)}
|
||||
<div role="tablist" aria-label="Evidence view">
|
||||
<button role="tab" aria-selected={view === "interpreted"} onClick={() => setView("interpreted")}>
|
||||
<button type="button" role="tab" aria-selected={view === "interpreted"} onClick={() => setView("interpreted")}>
|
||||
Interpreted
|
||||
</button>
|
||||
<button role="tab" aria-selected={view === "raw"} onClick={() => setView("raw")}>
|
||||
<button type="button" role="tab" aria-selected={view === "raw"} onClick={() => setView("raw")}>
|
||||
Raw
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -78,7 +78,7 @@ const renderFigure = (overrides: Partial<React.ComponentProps<typeof Interactive
|
||||
onFocusPathChange,
|
||||
...render(
|
||||
<InteractiveFigure
|
||||
catalog={validCatalog}
|
||||
catalog={restOverrides.catalog ?? validCatalog}
|
||||
focusPath={restOverrides.focusPath ?? []}
|
||||
activeNodeId={restOverrides.activeNodeId ?? null}
|
||||
onFocusPathChange={onFocusPathChange}
|
||||
@@ -99,10 +99,26 @@ describe("InteractiveFigure", () => {
|
||||
});
|
||||
|
||||
it("renders within a React Flow container for edge support", () => {
|
||||
renderFigure({ focusPath: [] });
|
||||
const { container } = renderFigure({ focusPath: [] });
|
||||
const rfWrapper = screen.getByTestId("rf__wrapper");
|
||||
expect(rfWrapper).toBeInTheDocument();
|
||||
expect(rfWrapper.querySelector("[class*='react-flow']")).toBeInTheDocument();
|
||||
expect(container.querySelector(".react-flow__handle-top")).toBeInTheDocument();
|
||||
expect(container.querySelector(".react-flow__handle-bottom")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("uses left and right handles for flow figures", () => {
|
||||
const flowCatalog: FigureCatalogDefinition = {
|
||||
...validCatalog,
|
||||
figures: validCatalog.figures.map((figure) =>
|
||||
figure.id === validCatalog.rootFigureId
|
||||
? { ...figure, layout: { kind: "flow" as const } }
|
||||
: figure,
|
||||
),
|
||||
};
|
||||
const { container } = renderFigure({ catalog: flowCatalog });
|
||||
expect(container.querySelector(".react-flow__handle-left")).toBeInTheDocument();
|
||||
expect(container.querySelector(".react-flow__handle-right")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("expands a child figure by click and Enter", async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type KeyboardEvent } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, type KeyboardEvent } from "react";
|
||||
import { ReactFlow, ReactFlowProvider, Handle, Position, useReactFlow, type Node, type Edge, type NodeTypes } from "@xyflow/react";
|
||||
import "@xyflow/react/dist/style.css";
|
||||
import type { FigureCatalogDefinition, FigureNodeKind } from "./model.js";
|
||||
@@ -19,6 +19,7 @@ type InteractiveFigureProps = {
|
||||
readonly activeNodeId: string | null;
|
||||
readonly onFocusPathChange: (path: readonly string[]) => void;
|
||||
readonly motionDisabled: boolean;
|
||||
readonly size?: "standard" | "wide";
|
||||
};
|
||||
|
||||
type FigureNodeData = {
|
||||
@@ -26,6 +27,7 @@ type FigureNodeData = {
|
||||
readonly label: string;
|
||||
readonly summary: string;
|
||||
readonly kind: FigureNodeKind;
|
||||
readonly orientation: "horizontal" | "vertical";
|
||||
readonly isActive: boolean;
|
||||
readonly isExpandable: boolean;
|
||||
readonly onActivate: (nodeId: string) => void;
|
||||
@@ -35,10 +37,12 @@ type FigureNodeData = {
|
||||
const FigureFlowNode = ({ data }: { data: FigureNodeData }) => {
|
||||
const expandable = data.isExpandable;
|
||||
const accessibleName = expandable ? `${data.label}, expand` : data.label;
|
||||
const targetPosition = data.orientation === "horizontal" ? Position.Left : Position.Top;
|
||||
const sourcePosition = data.orientation === "horizontal" ? Position.Right : Position.Bottom;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Handle type="target" position={Position.Top} id="target" />
|
||||
<Handle type="target" position={targetPosition} id="target" />
|
||||
<button
|
||||
type="button"
|
||||
className="figure-node"
|
||||
@@ -65,7 +69,7 @@ const FigureFlowNode = ({ data }: { data: FigureNodeData }) => {
|
||||
{expandable && <span className="figure-node__expand-affance" aria-hidden="true">▸</span>}
|
||||
{data.isActive && <span className="figure-node__current-marker">Current</span>}
|
||||
</button>
|
||||
<Handle type="source" position={Position.Bottom} id="source" />
|
||||
<Handle type="source" position={sourcePosition} id="source" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -74,11 +78,11 @@ const nodeTypes: NodeTypes = {
|
||||
figure: FigureFlowNode,
|
||||
};
|
||||
|
||||
const FitViewOnLayoutChange = ({ layoutVersion }: { layoutVersion: number }) => {
|
||||
const FitViewOnLayoutChange = ({ layoutKey }: { layoutKey: string }) => {
|
||||
const { fitView } = useReactFlow();
|
||||
useEffect(() => {
|
||||
void fitView({ padding: 0.15, duration: 0 });
|
||||
}, [fitView, layoutVersion]);
|
||||
}, [fitView, layoutKey]);
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -88,24 +92,20 @@ const InteractiveFigureInner = ({
|
||||
activeNodeId,
|
||||
onFocusPathChange,
|
||||
motionDisabled,
|
||||
size = "standard",
|
||||
}: InteractiveFigureProps) => {
|
||||
const focus = resolveFigureFocus(catalog, focusPath);
|
||||
const layout = layoutFigure(focus.figure);
|
||||
const [focusedNodeId, setFocusedNodeId] = useState<string>(
|
||||
activeNodeId ?? focus.figure.nodes[0]?.id ?? "",
|
||||
const focus = useMemo(
|
||||
() => resolveFigureFocus(catalog, focusPath),
|
||||
[catalog, focusPath],
|
||||
);
|
||||
const layout = useMemo(() => layoutFigure(focus.figure), [focus.figure]);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [layoutVersion, setLayoutVersion] = useState(0);
|
||||
const focusedNodeIdRef = useRef(activeNodeId ?? focus.figure.nodes[0]?.id ?? "");
|
||||
|
||||
useEffect(() => {
|
||||
if (activeNodeId) setFocusedNodeId(activeNodeId);
|
||||
}, [activeNodeId]);
|
||||
|
||||
useEffect(() => {
|
||||
const firstNode = focus.figure.nodes[0];
|
||||
if (firstNode) setFocusedNodeId(firstNode.id);
|
||||
setLayoutVersion((v) => v + 1);
|
||||
}, [focus.figure.id]);
|
||||
const fallbackFocusedNodeId = activeNodeId ?? focus.figure.nodes[0]?.id ?? "";
|
||||
if (activeNodeId || !layout.nodes.some((node) => node.id === focusedNodeIdRef.current)) {
|
||||
focusedNodeIdRef.current = fallbackFocusedNodeId;
|
||||
}
|
||||
|
||||
const handleExpand = useCallback(
|
||||
(nodeId: string) => {
|
||||
@@ -148,17 +148,21 @@ const InteractiveFigureInner = ({
|
||||
if (direction) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const nextId = nextFigureNodeId(layout, focusedNodeId, direction);
|
||||
setFocusedNodeId(nextId);
|
||||
const nextId = nextFigureNodeId(layout, focusedNodeIdRef.current, direction);
|
||||
focusedNodeIdRef.current = nextId;
|
||||
const nextNode = containerRef.current?.querySelector(
|
||||
`[data-testid="figure-node-${nextId}"]`,
|
||||
);
|
||||
if (nextNode instanceof HTMLElement) nextNode.focus();
|
||||
}
|
||||
},
|
||||
[catalog, focus, focusedNodeId, layout, onFocusPathChange],
|
||||
[catalog, focus, layout, onFocusPathChange],
|
||||
);
|
||||
|
||||
const handleActivateNode = useCallback((nodeId: string) => {
|
||||
focusedNodeIdRef.current = nodeId;
|
||||
}, []);
|
||||
|
||||
const rfNodes: Node[] = useMemo(
|
||||
() =>
|
||||
layout.nodes.map((node) => ({
|
||||
@@ -170,13 +174,14 @@ const InteractiveFigureInner = ({
|
||||
label: node.label,
|
||||
summary: node.summary,
|
||||
kind: node.kind,
|
||||
orientation: layout.definition.layout.kind === "flow" ? "horizontal" : "vertical",
|
||||
isActive: node.id === activeNodeId,
|
||||
isExpandable: node.childFigureId !== undefined,
|
||||
onActivate: setFocusedNodeId,
|
||||
onActivate: handleActivateNode,
|
||||
onExpand: handleExpand,
|
||||
},
|
||||
})),
|
||||
[layout.nodes, activeNodeId, handleExpand],
|
||||
[layout.definition.layout.kind, layout.nodes, activeNodeId, handleActivateNode, handleExpand],
|
||||
);
|
||||
|
||||
const rfEdges: Edge[] = useMemo(
|
||||
@@ -194,7 +199,7 @@ const InteractiveFigureInner = ({
|
||||
const handleNodeClick = useCallback(
|
||||
(_event: React.MouseEvent, node: Node) => {
|
||||
const data = node.data as FigureNodeData;
|
||||
setFocusedNodeId(data.nodeId);
|
||||
focusedNodeIdRef.current = data.nodeId;
|
||||
if (data.isExpandable) handleExpand(data.nodeId);
|
||||
},
|
||||
[handleExpand],
|
||||
@@ -207,6 +212,7 @@ const InteractiveFigureInner = ({
|
||||
aria-label={focus.figure.title}
|
||||
data-motion={motionDisabled ? "disabled" : "enabled"}
|
||||
data-figure-id={focus.figure.id}
|
||||
data-figure-size={size}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<FigureBreadcrumbs
|
||||
@@ -232,7 +238,7 @@ const InteractiveFigureInner = ({
|
||||
preventScrolling={false}
|
||||
onNodeClick={handleNodeClick}
|
||||
>
|
||||
<FitViewOnLayoutChange layoutVersion={layoutVersion} />
|
||||
<FitViewOnLayoutChange layoutKey={focus.figure.id} />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveFigureFocus } from "./focus.js";
|
||||
import { architectureCatalog } from "./architecture-catalog.js";
|
||||
import { layoutFigure } from "./layout.js";
|
||||
|
||||
describe("architectureCatalog", () => {
|
||||
it("contains the conceptual architecture overview", () => {
|
||||
const root = resolveFigureFocus(architectureCatalog, []).figure;
|
||||
expect(root.layout.kind).toBe("flow");
|
||||
expect(root.nodes.map((node) => node.label)).toEqual([
|
||||
"Client operations",
|
||||
"Application lifecycle",
|
||||
@@ -22,6 +24,14 @@ describe("architectureCatalog", () => {
|
||||
).figure.id).toBe("configured-provider-detail");
|
||||
});
|
||||
|
||||
it("lays out the architecture path horizontally for presentation", () => {
|
||||
const root = resolveFigureFocus(architectureCatalog, []).figure;
|
||||
const layout = layoutFigure(root);
|
||||
const client = layout.nodes.find((node) => node.id === "client-operations");
|
||||
const nodeUse = layout.nodes.find((node) => node.id === "node-use");
|
||||
expect(client?.position.x).toBeLessThan(nodeUse?.position.x ?? 0);
|
||||
});
|
||||
|
||||
it("gives every factual node an evidence pointer", () => {
|
||||
for (const figure of architectureCatalog.figures) {
|
||||
for (const node of figure.nodes) {
|
||||
|
||||
@@ -9,7 +9,7 @@ export const architectureCatalog: FigureCatalogDefinition = defineFigureCatalog(
|
||||
{
|
||||
id: "architecture-overview",
|
||||
title: "Architecture",
|
||||
layout: { kind: "layered" },
|
||||
layout: { kind: "flow" },
|
||||
nodes: [
|
||||
{
|
||||
id: "client-operations",
|
||||
@@ -84,7 +84,7 @@ export const architectureCatalog: FigureCatalogDefinition = defineFigureCatalog(
|
||||
{
|
||||
id: "runtime-provider-detail",
|
||||
title: "Runtime and providers",
|
||||
layout: { kind: "layered" },
|
||||
layout: { kind: "flow" },
|
||||
nodes: [
|
||||
{
|
||||
id: "workflow-server",
|
||||
@@ -133,7 +133,7 @@ export const architectureCatalog: FigureCatalogDefinition = defineFigureCatalog(
|
||||
{
|
||||
id: "configured-provider-detail",
|
||||
title: "Configured providers",
|
||||
layout: { kind: "layered" },
|
||||
layout: { kind: "flow" },
|
||||
nodes: [
|
||||
{
|
||||
id: "builtin-sources",
|
||||
@@ -172,7 +172,7 @@ export const architectureCatalog: FigureCatalogDefinition = defineFigureCatalog(
|
||||
{
|
||||
id: "node-use-detail",
|
||||
title: "NodeUse execution",
|
||||
layout: { kind: "layered" },
|
||||
layout: { kind: "flow" },
|
||||
nodes: [
|
||||
{
|
||||
id: "resolve-bindings",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.interactive-figure__canvas {
|
||||
@@ -11,6 +12,24 @@
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.interactive-figure[data-figure-size="wide"] {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding-bottom: 0.35rem;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.interactive-figure[data-figure-size="wide"] .interactive-figure__canvas {
|
||||
min-width: 1280px;
|
||||
min-height: 420px;
|
||||
}
|
||||
|
||||
.interactive-figure[data-figure-size="wide"] .react-flow {
|
||||
border: 1px solid color-mix(in oklch, var(--color-editorial-muted, oklch(0.48 0.025 65)) 45%, transparent);
|
||||
border-radius: 0.75rem;
|
||||
background: color-mix(in oklch, var(--color-editorial-surface, oklch(0.96 0.012 82)) 92%, white);
|
||||
}
|
||||
|
||||
.interactive-figure .react-flow__pane {
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -50,9 +69,9 @@
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 2px;
|
||||
width: 196px;
|
||||
height: 84px;
|
||||
padding: 8px 12px;
|
||||
width: 236px;
|
||||
height: 102px;
|
||||
padding: 10px 14px;
|
||||
border: 2px solid oklch(0.75 0.01 65);
|
||||
border-radius: 4px;
|
||||
background: oklch(0.975 0.012 82);
|
||||
@@ -81,17 +100,19 @@
|
||||
}
|
||||
|
||||
.figure-node__label {
|
||||
font-size: 14px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.figure-node__summary {
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
line-height: 1.18;
|
||||
color: var(--color-editorial-muted, oklch(0.48 0.025 65));
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ export type PositionedFigure = {
|
||||
readonly edges: readonly FigureEdgeDefinition[];
|
||||
};
|
||||
|
||||
export const NODE_WIDTH = 196;
|
||||
export const NODE_HEIGHT = 84;
|
||||
export const NODE_WIDTH = 236;
|
||||
export const NODE_HEIGHT = 102;
|
||||
const NODESEP = 56;
|
||||
const RANKSEP = 88;
|
||||
|
||||
|
||||
@@ -86,16 +86,16 @@ describe("presentationReducer", () => {
|
||||
branchId: "hosted-automation",
|
||||
});
|
||||
|
||||
expect(opened.evidencePresentationOverride).toBeNull();
|
||||
expect(opened.selectedNodeId).toBe("review_issues");
|
||||
expect(opened.location.kind).toBe("discussion");
|
||||
|
||||
const closed1 = presentationReducer(opened, { type: "close_overlay" });
|
||||
expect(closed1.evidencePresentationOverride).toBe("hidden");
|
||||
expect(closed1.selectedNodeId).toBe("review_issues");
|
||||
expect(closed1.selectedNodeId).toBeNull();
|
||||
expect(closed1.location.kind).toBe("discussion");
|
||||
|
||||
const closed2 = presentationReducer(closed1, { type: "close_overlay" });
|
||||
expect(closed2.selectedNodeId).toBeNull();
|
||||
|
||||
const closed3 = presentationReducer(closed2, { type: "close_overlay" });
|
||||
expect(closed3.location.kind).toBe("main");
|
||||
expect(closed2.location.kind).toBe("main");
|
||||
});
|
||||
|
||||
it("does nothing on next while a discussion branch is open", () => {
|
||||
@@ -127,7 +127,7 @@ describe("presentationReducer", () => {
|
||||
presentation: "inspector",
|
||||
});
|
||||
const closed = presentationReducer(opened, { type: "close_overlay" });
|
||||
expect(closed.evidencePresentationOverride).toBe("hidden");
|
||||
expect(closed.evidencePresentationOverride).toBeNull();
|
||||
expect(closed.location).toEqual(initialPresentationState.location);
|
||||
});
|
||||
|
||||
@@ -204,12 +204,29 @@ describe("presentationReducer", () => {
|
||||
presentation: "inspector",
|
||||
});
|
||||
const firstEscape = presentationReducer(withInspector, { type: "close_overlay" });
|
||||
expect(firstEscape.evidencePresentationOverride).toBe("hidden");
|
||||
expect(firstEscape.evidencePresentationOverride).toBeNull();
|
||||
expect(firstEscape.selectedNodeId).toBe("review_issues");
|
||||
const secondEscape = presentationReducer(firstEscape, { type: "close_overlay" });
|
||||
expect(secondEscape.selectedNodeId).toBeNull();
|
||||
});
|
||||
|
||||
it("returns to the beat receipt after closing an explicit inspector", () => {
|
||||
const atReceiptBeat = presentationReducer(initialPresentationState, {
|
||||
type: "jump",
|
||||
location: { kind: "main", sceneId: "interrupt-evidence", beatId: "trace", focusPath: [] },
|
||||
});
|
||||
expect(compositionForState(atReceiptBeat).evidencePresentation).toBe("receipt");
|
||||
const opened = presentationReducer(atReceiptBeat, {
|
||||
type: "set_evidence_presentation",
|
||||
presentation: "inspector",
|
||||
});
|
||||
expect(compositionForState(opened).evidencePresentation).toBe("inspector");
|
||||
|
||||
const closed = presentationReducer(opened, { type: "close_overlay" });
|
||||
expect(closed.evidencePresentationOverride).toBeNull();
|
||||
expect(compositionForState(closed).evidencePresentation).toBe("receipt");
|
||||
});
|
||||
|
||||
it("closes the inspector and recomputes receipt state when the beat changes", () => {
|
||||
const atReceiptBeat = presentationReducer(initialPresentationState, {
|
||||
type: "jump",
|
||||
|
||||
@@ -142,6 +142,7 @@ export const presentationReducer = (
|
||||
...state,
|
||||
location: { kind: "discussion", branchId: action.branchId as DiscussionBranchId },
|
||||
discussionReturn: returnLocation,
|
||||
evidencePresentationOverride: null,
|
||||
};
|
||||
}
|
||||
case "close_discussion": {
|
||||
@@ -158,7 +159,9 @@ export const presentationReducer = (
|
||||
case "set_evidence_presentation":
|
||||
return { ...state, evidencePresentationOverride: action.presentation };
|
||||
case "close_overlay": {
|
||||
if (state.evidencePresentationOverride === "inspector") return { ...state, evidencePresentationOverride: "hidden" };
|
||||
// The inspector is transient. Closing it should reveal the current beat's
|
||||
// default evidence presentation again, which may be a receipt.
|
||||
if (state.evidencePresentationOverride === "inspector") return { ...state, evidencePresentationOverride: null };
|
||||
if (state.selectedNodeId !== null) return { ...state, selectedNodeId: null };
|
||||
if (state.location.kind === "discussion") {
|
||||
return {
|
||||
|
||||
@@ -81,6 +81,22 @@
|
||||
margin: 0.25rem 0 0.5rem;
|
||||
}
|
||||
|
||||
.architecture-scene {
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.architecture-scene .stage-caption h1 {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.architecture-scene .stage-caption p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.scene-body__evidence {
|
||||
font-size: 0.8rem;
|
||||
color: oklch(0.72 0.03 250);
|
||||
|
||||
@@ -81,6 +81,12 @@ describe("ArchitectureScene", () => {
|
||||
expect(onFocusPathChange).toHaveBeenCalledWith(["runtime-providers"]);
|
||||
});
|
||||
|
||||
it("uses the wide figure presentation for the defense architecture scene", () => {
|
||||
renderArchitecture({ focusPath: [] });
|
||||
expect(screen.getByRole("group", { name: /architecture/i })).toHaveAttribute("data-figure-size", "wide");
|
||||
expect(screen.getByTestId("architecture-scene")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a directly linked nested provider view", () => {
|
||||
renderArchitecture({
|
||||
focusPath: ["runtime-providers", "configured-providers"],
|
||||
|
||||
@@ -20,7 +20,7 @@ export const ArchitectureScene = ({
|
||||
onFocusPathChange,
|
||||
motionDisabled,
|
||||
}: ArchitectureSceneProps) => (
|
||||
<>
|
||||
<section className="architecture-scene" data-testid="architecture-scene">
|
||||
<StageCaption eyebrow={`Act II · ${scene.claimClass}`} title={scene.title}>
|
||||
<p>{beat.caption}</p>
|
||||
</StageCaption>
|
||||
@@ -30,6 +30,7 @@ export const ArchitectureScene = ({
|
||||
activeNodeId={activeNodeId}
|
||||
onFocusPathChange={onFocusPathChange}
|
||||
motionDisabled={motionDisabled}
|
||||
size="wide"
|
||||
/>
|
||||
</>
|
||||
</section>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user