feat: make architecture figure expandable

- Create validated architecture catalog with 5 recursive figures
- ArchitectureScene delegates to InteractiveFigure with catalog
- SceneBody routes architecture view to ArchitectureScene
- Architecture beats carry canonical figure state (focusPath, activeNodeId)
- Remove old inline architectureLayers constant
This commit is contained in:
lda
2026-07-05 18:19:58 +07:00 Verified
parent 196abe4b49
commit 20f99a5034
8 changed files with 379 additions and 34 deletions
@@ -0,0 +1,69 @@
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeAll, afterAll, describe, expect, it, vi } from "vitest";
import { ArchitectureScene } from "./ArchitectureScene.js";
class MockResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
}
beforeAll(() => {
globalThis.ResizeObserver = MockResizeObserver as unknown as typeof ResizeObserver;
globalThis.DOMRect = {
fromRect: () => ({
x: 0,
y: 0,
width: 0,
height: 0,
top: 0,
right: 0,
bottom: 0,
left: 0,
toJSON() {},
}),
} as unknown as typeof DOMRect;
});
afterAll(() => {
delete (globalThis as Record<string, unknown>).ResizeObserver;
delete (globalThis as Record<string, unknown>).DOMRect;
});
const renderArchitecture = (overrides: Partial<React.ComponentProps<typeof ArchitectureScene>> = {}) => {
const onFocusPathChange = overrides.onFocusPathChange ?? vi.fn();
return {
onFocusPathChange,
...render(
<ArchitectureScene
focusPath={overrides.focusPath ?? []}
activeNodeId={overrides.activeNodeId ?? null}
onFocusPathChange={onFocusPathChange}
motionDisabled={overrides.motionDisabled ?? false}
/>,
),
};
};
afterEach(() => cleanup());
describe("ArchitectureScene", () => {
it("renders the overview and expands Runtime & providers", async () => {
const user = userEvent.setup();
const onFocusPathChange = vi.fn();
renderArchitecture({ focusPath: [], onFocusPathChange });
expect(screen.getByRole("heading", { name: /architecture/i })).toBeInTheDocument();
fireEvent.click(screen.getByTestId("figure-node-runtime-providers"));
expect(onFocusPathChange).toHaveBeenCalledWith(["runtime-providers"]);
});
it("renders a directly linked nested provider view", () => {
renderArchitecture({
focusPath: ["runtime-providers", "configured-providers"],
});
expect(screen.getByRole("group", { name: /configured providers/i })).toBeInTheDocument();
expect(screen.getByText("MCP sources")).toBeInTheDocument();
expect(screen.getByText("Python sources")).toBeInTheDocument();
});
});
@@ -0,0 +1,30 @@
import { StageCaption } from "../StageCaption.js";
import { InteractiveFigure } from "../figures/InteractiveFigure.js";
import { architectureCatalog } from "../figures/architecture-catalog.js";
type ArchitectureSceneProps = {
readonly focusPath: readonly string[];
readonly activeNodeId: string | null;
readonly onFocusPathChange: (path: readonly string[]) => void;
readonly motionDisabled: boolean;
};
export const ArchitectureScene = ({
focusPath,
activeNodeId,
onFocusPathChange,
motionDisabled,
}: ArchitectureSceneProps) => (
<>
<StageCaption eyebrow="Act II · implemented" title="Architecture Zoom">
<p>The system exposes one public lifecycle surface across all client types.</p>
</StageCaption>
<InteractiveFigure
catalog={architectureCatalog}
focusPath={focusPath}
activeNodeId={activeNodeId}
onFocusPathChange={onFocusPathChange}
motionDisabled={motionDisabled}
/>
</>
);