refactor: use one editorial presentation canvas

This commit is contained in:
lda
2026-07-11 18:53:03 +07:00 Verified
parent d715fb68ee
commit cbbb000a44
14 changed files with 30 additions and 82 deletions
@@ -17,17 +17,11 @@ describe("PresentationCanvas", () => {
const canvas = screen.getByTestId("presentation-canvas");
expect(canvas).toHaveClass("presentation-canvas");
expect(canvas).toHaveAttribute("data-stage-theme", "paper");
expect(canvas).not.toHaveAttribute("data-stage-theme");
expect(canvas.style.transform).toBe("");
expect(canvas).toHaveTextContent("Scene");
});
it("applies the authored night stage theme without changing canvas geometry", () => {
render(<PresentationCanvas stageTheme="night"><div>Scene</div></PresentationCanvas>);
expect(screen.getByTestId("presentation-canvas")).toHaveAttribute("data-stage-theme", "night");
});
it("uses CSS ratio bounds instead of JavaScript scale calculations", () => {
expect(editorialCss).toContain("width: min(100dvw, calc(100dvh * 16 / 9))");
expect(editorialCss).toContain("height: min(100dvh, calc(100dvw * 9 / 12))");
@@ -1,12 +1,10 @@
import type { ReactNode } from "react";
import type { StageTheme } from "./storyboard.js";
type PresentationCanvasProps = {
readonly children: ReactNode;
readonly stageTheme?: StageTheme;
};
export const PresentationCanvas = ({ children, stageTheme = "paper" }: PresentationCanvasProps) => (
export const PresentationCanvas = ({ children }: PresentationCanvasProps) => (
<div className="presentation-viewport">
{/*
Keep the presentation as normal responsive DOM instead of scaling the
@@ -17,7 +15,6 @@ export const PresentationCanvas = ({ children, stageTheme = "paper" }: Presentat
<div
className="presentation-canvas"
data-testid="presentation-canvas"
data-stage-theme={stageTheme}
>
{children}
</div>
@@ -13,7 +13,7 @@ import {
presentationReducer,
} from "./presentation-state.js";
import { hashForLocation } from "./storyboard-navigation.js";
import { findScene, type MainLocation } from "./storyboard.js";
import type { MainLocation } from "./storyboard.js";
import type { DemoApprovalActions, DemoApprovalUiState } from "./demo-approval-actions.js";
import "./presentation.css";
import "./styles/demo-workflow.css";
@@ -224,13 +224,9 @@ export const PresentationRoute = () => {
[],
);
const stageTheme = state.location.kind === "main"
? findScene(state.location.sceneId)?.stageTheme ?? "paper"
: "paper";
return (
<main className="presentation-route" aria-label="lda.chat presentation" data-motion={state.motionDisabled ? "disabled" : "enabled"}>
<PresentationCanvas stageTheme={stageTheme}>
<PresentationCanvas>
<PresentationStage
state={state}
demo={demo}
@@ -62,9 +62,16 @@ const DiscussionLinks = ({
);
};
const actForSceneNumber = (sceneNumber: number): string => {
if (sceneNumber <= 3) return "I";
if (sceneNumber <= 12) return "II";
if (sceneNumber === 13) return "III";
return "IV";
};
const NarrativeScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBeatDefinition }) => (
<>
<StageCaption eyebrow={`Act ${scene.stageTheme === "paper" ? "I" : "II"} · ${scene.claimClass}`} title={scene.title}>
<StageCaption eyebrow={`Act ${actForSceneNumber(scene.number)} · ${scene.claimClass}`} title={scene.title}>
<p>{beat.caption}</p>
</StageCaption>
<p className="scene-body__evidence">{scene.evidencePointer}</p>
@@ -8,7 +8,6 @@ const scene = {
title: "Contribution and future work",
claimClass: "future-work" as const,
evidencePointer: "Thesis conclusion",
stageTheme: "night" as const,
view: "conclusion" as const,
beats: [],
};
@@ -8,7 +8,6 @@ const scene = {
title: "Evaluation",
claimClass: "evaluated" as const,
evidencePointer: "Thesis Evaluation and Appendix C",
stageTheme: "paper" as const,
view: "evaluation" as const,
beats: [],
};
@@ -193,7 +193,7 @@
/* Semantic kind colors */
.figure-node[data-figure-node-kind="actor"],
.figure-node[data-figure-node-kind="operation"] {
border-color: var(--color-intent, oklch(0.53 0.17 250));
border-color: var(--color-editorial-ink, oklch(0.19 0.015 65));
}
.figure-node[data-figure-node-kind="runtime"] {
@@ -245,14 +245,6 @@
font-weight: 600;
}
.presentation-canvas[data-stage-theme="night"] .figure-breadcrumbs {
color: var(--text-secondary, oklch(0.78 0.035 250));
}
.presentation-canvas[data-stage-theme="night"] .figure-breadcrumbs__crumb[aria-current="page"] {
color: var(--text-primary, oklch(0.96 0.01 250));
}
.figure-breadcrumbs__crumb + .figure-breadcrumbs__crumb::before {
content: "\203A";
margin-right: 4px;
@@ -19,31 +19,20 @@
--chat-width: 18rem;
}
.presentation-canvas[data-stage-theme="night"] .presentation-stage__primary {
background: var(--stage-canvas);
color: var(--text-primary);
}
.presentation-canvas[data-stage-theme="paper"] .presentation-stage__primary {
.presentation-stage__primary {
background: var(--color-editorial-paper, oklch(0.975 0.012 82));
color: var(--color-editorial-ink, oklch(0.19 0.015 65));
}
/* The console's global section rule must not turn night-mode slide roots white. */
.presentation-canvas[data-stage-theme="night"] .presentation-stage__primary > .stage-caption,
.presentation-canvas[data-stage-theme="night"] .architecture-scene > .stage-caption,
.presentation-canvas[data-stage-theme="night"] .presentation-stage__primary > .architecture-scene,
.presentation-canvas[data-stage-theme="night"] .presentation-stage__primary > .scene-body__authoring-composition {
/* Presentation roots own their visual surfaces; do not inherit console cards. */
.presentation-stage__primary > .architecture-scene,
.presentation-stage__primary > .scene-body__authoring-composition,
.presentation-stage__primary > .prepared-lifecycle-scene {
margin: 0;
border: 0;
border-radius: 0;
padding: 0;
background: transparent;
color: var(--text-primary);
border-color: var(--stage-line);
}
.presentation-canvas[data-stage-theme="night"] .presentation-stage__primary > .prepared-lifecycle-scene {
background: var(--stage-canvas);
color: var(--text-primary);
border-color: var(--stage-line);
margin-bottom: 0;
}
.presentation-stage {
@@ -295,7 +284,7 @@
}
.stage-caption__eyebrow {
color: oklch(0.78 0.035 250);
color: var(--color-editorial-muted, oklch(0.48 0.025 65));
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.08em;
@@ -326,7 +315,7 @@
.scene-body__evidence {
flex-shrink: 0;
margin: 0.55rem 0 0;
color: oklch(0.62 0.05 250);
color: var(--color-editorial-muted, oklch(0.48 0.025 65));
font-size: 0.8rem;
}
@@ -37,7 +37,6 @@ const mockScene = {
title: "Architecture Zoom",
claimClass: "implemented" as const,
evidencePointer: "Thesis System Architecture",
stageTheme: "night" as const,
view: "architecture" as const,
beats: [],
};
@@ -42,10 +42,7 @@ describe("defense storyboard catalog", () => {
expect(findBeat("conclusion", "questions")?.chatMode).toBe("hidden");
});
it("uses act-level stage themes and independent chat composition", () => {
expect(mainScenes.slice(0, 3).every((scene) => scene.stageTheme === "paper")).toBe(true);
expect(mainScenes.slice(3, 12).every((scene) => scene.stageTheme === "night")).toBe(true);
expect(mainScenes.slice(12).every((scene) => scene.stageTheme === "paper")).toBe(true);
it("uses one editorial canvas theme and independent chat composition", () => {
expect(findBeat("agent-handoff", "request")?.chatMode).toBe("hidden");
expect(findBeat("resume-output-evidence", "trace")?.chatMode).toBe("hidden");
});
@@ -1,5 +1,4 @@
export type ClaimClass = "motivation" | "implemented" | "evaluated" | "external-context" | "future-work";
export type StageTheme = "paper" | "night";
export type ChatTheme = "light" | "dark";
export type ChatMode = "hidden" | "full" | "rail" | "dock";
export type EvidencePresentation = "hidden" | "receipt" | "inspector";
@@ -39,7 +38,6 @@ export type SceneDefinition = {
readonly title: string;
readonly claimClass: ClaimClass;
readonly evidencePointer: string;
readonly stageTheme: StageTheme;
readonly view: SceneView;
readonly beats: readonly SceneBeatDefinition[];
};
@@ -68,7 +66,6 @@ export const mainScenes = defineScenes([
title: "Thesis",
claimClass: "implemented",
evidencePointer: "Thesis Abstract and Introduction",
stageTheme: "paper",
view: "narrative",
beats: [
sceneBeat("title", "Design and Implementation of lda.chat", "The project began as a pursuit of an AI agent for workspace automation."),
@@ -81,7 +78,6 @@ export const mainScenes = defineScenes([
title: "The Problem",
claimClass: "motivation",
evidencePointer: "Thesis Problem Statement and Requirements",
stageTheme: "paper",
view: "narrative",
beats: [
sceneBeat("direct-actions", "Direct actions are not reusable automation", "A tool loop can act without owning durable lifecycle records."),
@@ -94,7 +90,6 @@ export const mainScenes = defineScenes([
title: "Positioning and Related Systems",
claimClass: "motivation",
evidencePointer: "Thesis Positioning and Related Systems",
stageTheme: "paper",
view: "positioning",
beats: [
sceneBeat("landscape", "Different centers of gravity", "Tool loops, scripts, hosted automation, agent graphs, and MCP solve different parts of the problem."),
@@ -107,7 +102,6 @@ export const mainScenes = defineScenes([
title: "Planner and Runtime",
claimClass: "implemented",
evidencePointer: "Thesis Architecture Overview",
stageTheme: "night",
view: "boundary",
beats: [
sceneBeat("planner", "External planner", "The planner proposes and revises workflow structure."),
@@ -121,7 +115,6 @@ export const mainScenes = defineScenes([
title: "Workflow Lifecycle",
claimClass: "implemented",
evidencePointer: "Thesis Workflow Lifecycle",
stageTheme: "night",
view: "lifecycle",
beats: [
sceneBeat("draft", "Draft", "Mutable iterative authoring state."),
@@ -136,7 +129,6 @@ export const mainScenes = defineScenes([
title: "Architecture Zoom",
claimClass: "implemented",
evidencePointer: "Thesis System Architecture; docs/project_map.md; docs/source_architecture.md",
stageTheme: "night",
view: "architecture",
beats: [
sceneBeat("client", "Client operations", "Human and agent clients use the same public lifecycle surface.", { figure: { catalogId: "system-architecture", focusPath: [], activeNodeId: "client-operations" } }),
@@ -151,7 +143,6 @@ export const mainScenes = defineScenes([
title: "Author, Validate, Repair",
claimClass: "implemented",
evidencePointer: "CLI documentation; draft authoring API; challenge UX findings",
stageTheme: "night",
view: "authoring",
beats: [
sceneBeat("discover", "Discover", "Inspect capabilities and schemas before authoring."),
@@ -166,7 +157,6 @@ export const mainScenes = defineScenes([
title: "Agent Handoff",
claimClass: "implemented",
evidencePointer: "Constrained demo agent and prepared replay recipe",
stageTheme: "night",
view: "agent",
beats: [
// Scene 8 renders its own full-screen prepared transcript in the primary
@@ -181,7 +171,6 @@ export const mainScenes = defineScenes([
title: "Prepared Workflow Lifecycle",
claimClass: "implemented",
evidencePointer: "examples/lda_report_workflow; deployment inspect replay evidence",
stageTheme: "night",
view: "demo-lifecycle",
beats: [
sceneBeat("discover", "Discover capabilities", "Inspect available sources, capabilities, and schemas before authoring.", { chatMode: "hidden", chatTheme: "light" }),
@@ -197,7 +186,6 @@ export const mainScenes = defineScenes([
title: "Run From Deployment",
claimClass: "implemented",
evidencePointer: "workflow.runs.start replay evidence",
stageTheme: "night",
view: "demo",
beats: [
sceneBeat("input", "Workflow input", "The run starts from selected documents and an issue board path.", { chatMode: "hidden", chatTheme: "light" }),
@@ -211,7 +199,6 @@ export const mainScenes = defineScenes([
title: "Typed Human Boundary",
claimClass: "implemented",
evidencePointer: "typed interrupt payload and resume contract",
stageTheme: "night",
view: "demo",
beats: [
sceneBeat("interrupt", "Interrupt payload", "Execution reaches a declared issue-review boundary.", { chatMode: "hidden", chatTheme: "light" }),
@@ -225,7 +212,6 @@ export const mainScenes = defineScenes([
title: "Resume, Output, Evidence",
claimClass: "implemented",
evidencePointer: "workflow.runs.resume, workflow output, and trace replay evidence",
stageTheme: "night",
view: "demo",
beats: [
sceneBeat("resume", "Resume", "The submitted payload resumes the same persisted run.", { chatMode: "hidden", chatTheme: "light" }),
@@ -239,7 +225,6 @@ export const mainScenes = defineScenes([
title: "Evaluation",
claimClass: "evaluated",
evidencePointer: "Thesis Evaluation and Appendix C",
stageTheme: "paper",
view: "evaluation",
beats: [
sceneBeat("cohort", "36-trial cohort", "Two challenges, two hosted models, three profiles, and three waves.", { chatMode: "hidden" }),
@@ -253,7 +238,6 @@ export const mainScenes = defineScenes([
title: "Limits and Conclusion",
claimClass: "future-work",
evidencePointer: "Thesis Limitations, Future Work, and Conclusion",
stageTheme: "paper",
view: "conclusion",
beats: [
sceneBeat("limits", "Implemented boundary", "The prototype is not a production sandbox, scheduler, or broad agent benchmark.", { chatMode: "hidden" }),
@@ -40,8 +40,3 @@
container-name: presentation-canvas;
container-type: inline-size;
}
.presentation-canvas[data-stage-theme="night"] {
background: oklch(0.12 0.025 250);
color: oklch(0.96 0.01 250);
}