fix: add React Flow handles, fix keyboard, NodeUse active, beat captions
This commit is contained in:
@@ -194,6 +194,8 @@ export const SceneBody = ({ location, demo, selectedNodeId, selectNode, openEvid
|
|||||||
case "architecture":
|
case "architecture":
|
||||||
return (
|
return (
|
||||||
<ArchitectureScene
|
<ArchitectureScene
|
||||||
|
scene={scene}
|
||||||
|
beat={beat}
|
||||||
focusPath={location.kind === "main" ? location.focusPath : []}
|
focusPath={location.kind === "main" ? location.focusPath : []}
|
||||||
activeNodeId={beat.figure?.activeNodeId ?? null}
|
activeNodeId={beat.figure?.activeNodeId ?? null}
|
||||||
onFocusPathChange={onFocusPathChange}
|
onFocusPathChange={onFocusPathChange}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState, type KeyboardEvent } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState, type KeyboardEvent } from "react";
|
||||||
import { ReactFlow, ReactFlowProvider, useReactFlow, type Node, type Edge, type NodeTypes } from "@xyflow/react";
|
import { ReactFlow, ReactFlowProvider, Handle, Position, useReactFlow, type Node, type Edge, type NodeTypes } from "@xyflow/react";
|
||||||
import "@xyflow/react/dist/style.css";
|
import "@xyflow/react/dist/style.css";
|
||||||
import type { FigureCatalogDefinition, FigureNodeKind } from "./model.js";
|
import type { FigureCatalogDefinition, FigureNodeKind } from "./model.js";
|
||||||
import { layoutFigure, NODE_WIDTH, NODE_HEIGHT, type PositionedFigure } from "./layout.js";
|
import { layoutFigure, NODE_WIDTH, NODE_HEIGHT, type PositionedFigure } from "./layout.js";
|
||||||
@@ -37,32 +37,36 @@ const FigureFlowNode = ({ data }: { data: FigureNodeData }) => {
|
|||||||
const accessibleName = expandable ? `${data.label}, expand` : data.label;
|
const accessibleName = expandable ? `${data.label}, expand` : data.label;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<>
|
||||||
type="button"
|
<Handle type="target" position={Position.Top} id="target" />
|
||||||
className="figure-node"
|
<button
|
||||||
data-figure-node-kind={data.kind}
|
type="button"
|
||||||
data-active={data.isActive}
|
className="figure-node"
|
||||||
data-expandable={expandable}
|
data-figure-node-kind={data.kind}
|
||||||
data-testid={`figure-node-${data.nodeId}`}
|
data-active={data.isActive}
|
||||||
aria-label={accessibleName}
|
data-expandable={expandable}
|
||||||
tabIndex={-1}
|
data-testid={`figure-node-${data.nodeId}`}
|
||||||
onClick={() => {
|
aria-label={accessibleName}
|
||||||
data.onActivate(data.nodeId);
|
tabIndex={data.isActive ? 0 : -1}
|
||||||
if (expandable) data.onExpand(data.nodeId);
|
onClick={() => {
|
||||||
}}
|
data.onActivate(data.nodeId);
|
||||||
onKeyDown={(event) => {
|
if (expandable) data.onExpand(data.nodeId);
|
||||||
if (event.key === "Enter" && expandable) {
|
}}
|
||||||
event.preventDefault();
|
onKeyDown={(event) => {
|
||||||
data.onExpand(data.nodeId);
|
if (event.key === "Enter" && expandable) {
|
||||||
}
|
event.preventDefault();
|
||||||
}}
|
data.onExpand(data.nodeId);
|
||||||
>
|
}
|
||||||
<span className="figure-node__kind">{data.kind}</span>
|
}}
|
||||||
<strong className="figure-node__label">{data.label}</strong>
|
>
|
||||||
<span className="figure-node__summary">{data.summary}</span>
|
<span className="figure-node__kind">{data.kind}</span>
|
||||||
{expandable && <span className="figure-node__expand-affance" aria-hidden="true">▸</span>}
|
<strong className="figure-node__label">{data.label}</strong>
|
||||||
{data.isActive && <span className="figure-node__current-marker">Current</span>}
|
<span className="figure-node__summary">{data.summary}</span>
|
||||||
</button>
|
{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" />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,16 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.interactive-figure .react-flow__handle {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
min-width: 6px;
|
||||||
|
min-height: 6px;
|
||||||
|
background: var(--color-editorial-muted, oklch(0.48 0.025 65));
|
||||||
|
border: none;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
.figure-node {
|
.figure-node {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -31,12 +31,35 @@ afterAll(() => {
|
|||||||
delete (globalThis as Record<string, unknown>).DOMRect;
|
delete (globalThis as Record<string, unknown>).DOMRect;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mockScene = {
|
||||||
|
id: "architecture",
|
||||||
|
number: 6,
|
||||||
|
title: "Architecture Zoom",
|
||||||
|
claimClass: "implemented" as const,
|
||||||
|
evidencePointer: "Thesis System Architecture",
|
||||||
|
stageTheme: "night" as const,
|
||||||
|
view: "architecture" as const,
|
||||||
|
beats: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockBeat = {
|
||||||
|
id: "client",
|
||||||
|
title: "Client operations",
|
||||||
|
caption: "Human and agent clients use the same public lifecycle surface.",
|
||||||
|
chatMode: "rail" as const,
|
||||||
|
chatTheme: "light" as const,
|
||||||
|
evidenceMode: "hidden" as const,
|
||||||
|
figure: { catalogId: "system-architecture", focusPath: [] as readonly string[], activeNodeId: "client-operations" },
|
||||||
|
};
|
||||||
|
|
||||||
const renderArchitecture = (overrides: Partial<React.ComponentProps<typeof ArchitectureScene>> = {}) => {
|
const renderArchitecture = (overrides: Partial<React.ComponentProps<typeof ArchitectureScene>> = {}) => {
|
||||||
const onFocusPathChange = overrides.onFocusPathChange ?? vi.fn();
|
const onFocusPathChange = overrides.onFocusPathChange ?? vi.fn();
|
||||||
return {
|
return {
|
||||||
onFocusPathChange,
|
onFocusPathChange,
|
||||||
...render(
|
...render(
|
||||||
<ArchitectureScene
|
<ArchitectureScene
|
||||||
|
scene={overrides.scene ?? mockScene}
|
||||||
|
beat={overrides.beat ?? mockBeat}
|
||||||
focusPath={overrides.focusPath ?? []}
|
focusPath={overrides.focusPath ?? []}
|
||||||
activeNodeId={overrides.activeNodeId ?? null}
|
activeNodeId={overrides.activeNodeId ?? null}
|
||||||
onFocusPathChange={onFocusPathChange}
|
onFocusPathChange={onFocusPathChange}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { StageCaption } from "../StageCaption.js";
|
import { StageCaption } from "../StageCaption.js";
|
||||||
import { InteractiveFigure } from "../figures/InteractiveFigure.js";
|
import { InteractiveFigure } from "../figures/InteractiveFigure.js";
|
||||||
import { architectureCatalog } from "../figures/architecture-catalog.js";
|
import { architectureCatalog } from "../figures/architecture-catalog.js";
|
||||||
|
import type { SceneDefinition, SceneBeatDefinition } from "../storyboard.js";
|
||||||
|
|
||||||
type ArchitectureSceneProps = {
|
type ArchitectureSceneProps = {
|
||||||
|
readonly scene: SceneDefinition;
|
||||||
|
readonly beat: SceneBeatDefinition;
|
||||||
readonly focusPath: readonly string[];
|
readonly focusPath: readonly string[];
|
||||||
readonly activeNodeId: string | null;
|
readonly activeNodeId: string | null;
|
||||||
readonly onFocusPathChange: (path: readonly string[]) => void;
|
readonly onFocusPathChange: (path: readonly string[]) => void;
|
||||||
@@ -10,14 +13,16 @@ type ArchitectureSceneProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ArchitectureScene = ({
|
export const ArchitectureScene = ({
|
||||||
|
scene,
|
||||||
|
beat,
|
||||||
focusPath,
|
focusPath,
|
||||||
activeNodeId,
|
activeNodeId,
|
||||||
onFocusPathChange,
|
onFocusPathChange,
|
||||||
motionDisabled,
|
motionDisabled,
|
||||||
}: ArchitectureSceneProps) => (
|
}: ArchitectureSceneProps) => (
|
||||||
<>
|
<>
|
||||||
<StageCaption eyebrow="Act II · implemented" title="Architecture Zoom">
|
<StageCaption eyebrow={`Act II · ${scene.claimClass}`} title={scene.title}>
|
||||||
<p>The system exposes one public lifecycle surface across all client types.</p>
|
<p>{beat.caption}</p>
|
||||||
</StageCaption>
|
</StageCaption>
|
||||||
<InteractiveFigure
|
<InteractiveFigure
|
||||||
catalog={architectureCatalog}
|
catalog={architectureCatalog}
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export const mainScenes = defineScenes([
|
|||||||
sceneBeat("client", "Client operations", "Human and agent clients use the same public lifecycle surface.", { figure: { catalogId: "system-architecture", focusPath: [], activeNodeId: "client-operations" } }),
|
sceneBeat("client", "Client operations", "Human and agent clients use the same public lifecycle surface.", { figure: { catalogId: "system-architecture", focusPath: [], activeNodeId: "client-operations" } }),
|
||||||
sceneBeat("api", "Transport and API", "JSON-RPC reaches WorkflowApi without owning domain behavior.", { figure: { catalogId: "system-architecture", focusPath: [], activeNodeId: "application-lifecycle" } }),
|
sceneBeat("api", "Transport and API", "JSON-RPC reaches WorkflowApi without owning domain behavior.", { figure: { catalogId: "system-architecture", focusPath: [], activeNodeId: "application-lifecycle" } }),
|
||||||
sceneBeat("runtime", "Runtime and providers", "The runtime resolves provider-neutral capabilities and stores lifecycle records.", { figure: { catalogId: "system-architecture", focusPath: ["runtime-providers"], activeNodeId: "configured-providers" } }),
|
sceneBeat("runtime", "Runtime and providers", "The runtime resolves provider-neutral capabilities and stores lifecycle records.", { figure: { catalogId: "system-architecture", focusPath: ["runtime-providers"], activeNodeId: "configured-providers" } }),
|
||||||
sceneBeat("node-use", "NodeUse", "One callable node validates input, invokes a capability, and reduces output into state.", { evidenceMode: "peek", figure: { catalogId: "system-architecture", focusPath: ["node-use"], activeNodeId: "node-use" } }),
|
sceneBeat("node-use", "NodeUse", "One callable node validates input, invokes a capability, and reduces output into state.", { evidenceMode: "peek", figure: { catalogId: "system-architecture", focusPath: ["node-use"], activeNodeId: "invoke-handler" } }),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user