refactor: simplify presentation audience surface
This commit is contained in:
@@ -1,15 +0,0 @@
|
||||
type ChatDockProps = {
|
||||
readonly openChat: () => void;
|
||||
};
|
||||
|
||||
export const ChatDock = ({ openChat }: ChatDockProps) => (
|
||||
<button
|
||||
type="button"
|
||||
className="chat-dock"
|
||||
aria-label="open agent chat"
|
||||
onClick={openChat}
|
||||
>
|
||||
<span className="chat-dock__icon">💬</span>
|
||||
<span className="chat-dock__label">Chat</span>
|
||||
</button>
|
||||
);
|
||||
@@ -1,52 +0,0 @@
|
||||
import { discussionBranches, findScene, type MainSceneId } from "./storyboard.js";
|
||||
|
||||
type DiscussionIndexProps = {
|
||||
readonly onSelect: (branchId: string) => void;
|
||||
};
|
||||
|
||||
type BranchGroup = {
|
||||
readonly parentSceneId: MainSceneId;
|
||||
readonly sceneTitle: string;
|
||||
readonly branches: typeof discussionBranches[number][];
|
||||
};
|
||||
|
||||
const groupByParentScene = (): readonly BranchGroup[] => {
|
||||
const groups = new Map<string, BranchGroup>();
|
||||
for (const branch of discussionBranches) {
|
||||
let group = groups.get(branch.parentSceneId);
|
||||
if (!group) {
|
||||
const scene = findScene(branch.parentSceneId);
|
||||
group = {
|
||||
parentSceneId: branch.parentSceneId,
|
||||
sceneTitle: scene?.title ?? branch.parentSceneId,
|
||||
branches: [],
|
||||
};
|
||||
groups.set(branch.parentSceneId, group);
|
||||
}
|
||||
group.branches.push(branch);
|
||||
}
|
||||
return Array.from(groups.values());
|
||||
};
|
||||
|
||||
export const DiscussionIndex = ({ onSelect }: DiscussionIndexProps) => {
|
||||
const groups = groupByParentScene();
|
||||
return (
|
||||
<div className="discussion-index" role="dialog" aria-label="discussion topics">
|
||||
<h2>Discussion Topics</h2>
|
||||
{groups.map((group) => (
|
||||
<section key={group.parentSceneId}>
|
||||
<h3>{group.sceneTitle}</h3>
|
||||
<ul>
|
||||
{group.branches.map((branch) => (
|
||||
<li key={branch.id}>
|
||||
<button type="button" onClick={() => onSelect(branch.id)}>
|
||||
{branch.title}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -112,7 +112,7 @@ export const OperatorChat = ({ state, messages, onApprove, onDeny }: OperatorCha
|
||||
const visibleMessages = messages && messages.length > 0 ? messages : fallbackMessages(state);
|
||||
const composition = compositionForState(state);
|
||||
return (
|
||||
<aside className="operator-chat" data-mode={composition.chatMode} data-chat-theme={composition.chatTheme} aria-label="scripted operator chat">
|
||||
<aside className="operator-chat" data-mode={composition.chatMode} aria-label="scripted operator chat">
|
||||
{visibleMessages.map((message) => (
|
||||
<div key={message.id} className={`chat-message chat-message--${message.role === "user" ? "operator" : "system"}`}>
|
||||
<strong>{message.role === "user" ? "Operator" : "lda.chat"}</strong>
|
||||
|
||||
@@ -23,12 +23,12 @@ describe("PresentationRoute", () => {
|
||||
expect(await screen.findByText(/The interface delegates durable work to lda\.chat/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders replay-first chat, beat rail, and stage caption", () => {
|
||||
it("renders audience progress chrome without rail or mode label", () => {
|
||||
render(<PresentationRoute />);
|
||||
|
||||
expect(screen.getByText("Prepare the thesis readiness report.")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/presentation scene rail/i)).toBeInTheDocument();
|
||||
expect(screen.getByText("Replay · running")).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText(/presentation scene rail/i)).not.toBeInTheDocument();
|
||||
expect(screen.getByLabelText("scene position")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows node spotlight when a graph node is selected", async () => {
|
||||
@@ -63,16 +63,13 @@ describe("PresentationRoute", () => {
|
||||
expect(await screen.findByRole("dialog", { name: /issue review/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens a positioning branch and returns to the exact originating beat", async () => {
|
||||
window.location.hash = "#scene/positioning/lda-position";
|
||||
it("opens a positioning branch via hash and returns to the parent scene first beat", async () => {
|
||||
window.location.hash = "#discuss/hosted-automation";
|
||||
render(<PresentationRoute />);
|
||||
|
||||
await userEvent.click(screen.getByRole("button", { name: /discussion topics/i }));
|
||||
await userEvent.click(screen.getByRole("button", { name: /hosted automation/i }));
|
||||
expect(window.location.hash).toBe("#discuss/hosted-automation");
|
||||
|
||||
expect(await screen.findByRole("button", { name: /return to positioning/i })).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByRole("button", { name: /return to positioning/i }));
|
||||
expect(window.location.hash).toBe("#scene/positioning/lda-position");
|
||||
expect(window.location.hash).toBe("#scene/positioning/landscape");
|
||||
});
|
||||
|
||||
it("uses the parent scene as return location for a directly linked branch", async () => {
|
||||
@@ -82,11 +79,11 @@ describe("PresentationRoute", () => {
|
||||
expect(window.location.hash).toBe("#scene/positioning/landscape");
|
||||
});
|
||||
|
||||
it("renders stable chat, primary, evidence, and navigation regions", () => {
|
||||
it("renders stable chat, primary, evidence, and progress regions", () => {
|
||||
render(<PresentationRoute />);
|
||||
expect(screen.getByLabelText(/agent chat region/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/primary presentation region/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/evidence region/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/presentation scene rail/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("scene position")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,12 +6,9 @@ import { loadCanonicalDemoRecording } from "../demo/timeline/replay.js";
|
||||
import { useDemoTimeline } from "../demo/useDemoTimeline.js";
|
||||
import { PresentationCanvas } from "./PresentationCanvas.js";
|
||||
import { PresentationStage } from "./PresentationStage.js";
|
||||
import { PresenterControls } from "./PresenterControls.js";
|
||||
import { ChatDock } from "./ChatDock.js";
|
||||
import {
|
||||
initialPresentationState,
|
||||
presentationReducer,
|
||||
compositionForState,
|
||||
} from "./presentation-state.js";
|
||||
import { hashForLocation } from "./storyboard-navigation.js";
|
||||
import { findScene } from "./storyboard.js";
|
||||
@@ -93,9 +90,6 @@ export const PresentationRoute = () => {
|
||||
dispatch({ type: "previous" });
|
||||
} else if (event.key === "Escape") {
|
||||
dispatch({ type: "close_overlay" });
|
||||
} else if (event.key === "p" || event.key === "P") {
|
||||
if (!isBodyEvent) return;
|
||||
dispatch({ type: "toggle_controls" });
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
@@ -164,12 +158,6 @@ export const PresentationRoute = () => {
|
||||
setEvidence(replayEvidence);
|
||||
}, [demo, replayEvidence]);
|
||||
|
||||
const handleResetOverrides = useCallback(() => {
|
||||
dispatch({ type: "set_stage_theme", theme: null });
|
||||
dispatch({ type: "set_chat_theme", theme: null });
|
||||
dispatch({ type: "set_chat_mode", mode: null });
|
||||
}, []);
|
||||
|
||||
const handleResetScene = useCallback(() => {
|
||||
if (state.location.kind !== "main") return;
|
||||
const scene = findScene(state.location.sceneId);
|
||||
@@ -181,9 +169,6 @@ export const PresentationRoute = () => {
|
||||
dispatch({ type: "toggle_motion" });
|
||||
}, []);
|
||||
|
||||
const composition = compositionForState(state);
|
||||
const showChatDock = composition.chatMode === "dock" && state.location.kind !== "discussion";
|
||||
|
||||
return (
|
||||
<main className="presentation-route" aria-label="lda.chat presentation" data-motion={state.motionDisabled ? "disabled" : "enabled"}>
|
||||
<PresentationCanvas>
|
||||
@@ -200,25 +185,8 @@ export const PresentationRoute = () => {
|
||||
closeOverlay={() => dispatch({ type: "close_overlay" })}
|
||||
openDiscussion={handleOpenDiscussion}
|
||||
closeDiscussion={handleCloseDiscussion}
|
||||
toggleDiscussionIndex={() => dispatch({ type: "toggle_discussion_index" })}
|
||||
/>
|
||||
</PresentationCanvas>
|
||||
{showChatDock && <ChatDock openChat={() => dispatch({ type: "set_chat_mode", mode: "rail" })} />}
|
||||
{state.controlsOpen && (
|
||||
<PresenterControls
|
||||
state={state}
|
||||
next={() => dispatch({ type: "next" })}
|
||||
previous={() => dispatch({ type: "previous" })}
|
||||
jump={handleJump}
|
||||
setStageTheme={(theme) => dispatch({ type: "set_stage_theme", theme })}
|
||||
setChatTheme={(theme) => dispatch({ type: "set_chat_theme", theme })}
|
||||
setChatMode={(mode) => dispatch({ type: "set_chat_mode", mode })}
|
||||
forceReplay={handleForceReplay}
|
||||
resetOverrides={handleResetOverrides}
|
||||
resetScene={handleResetScene}
|
||||
toggleMotion={handleToggleMotion}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => agent.startPreparedReplay()}
|
||||
|
||||
@@ -2,15 +2,14 @@ import { domAnimation, LayoutGroup, LazyMotion } from "motion/react";
|
||||
import type { EvidenceRecord } from "../app/state.js";
|
||||
import type { AgentMessage } from "../demo/agent/events.js";
|
||||
import { SceneBody } from "./SceneBody.js";
|
||||
import { SceneRail } from "./SceneRail.js";
|
||||
import { DiscussionIndex } from "./DiscussionIndex.js";
|
||||
import { DiscussionPanel } from "./DiscussionPanel.js";
|
||||
import { EvidenceDrawer } from "./EvidenceDrawer.js";
|
||||
import { OperatorChat } from "./OperatorChat.js";
|
||||
import { SceneProgress } from "./SceneProgress.js";
|
||||
import type { PresentationState } from "./presentation-state.js";
|
||||
import { compositionForState } from "./presentation-state.js";
|
||||
import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
|
||||
import { findScene, type PresentationLocation } from "./storyboard.js";
|
||||
import { findScene, type MainLocation, type PresentationLocation } from "./storyboard.js";
|
||||
|
||||
type PresentationStageProps = {
|
||||
readonly state: PresentationState;
|
||||
@@ -25,7 +24,6 @@ type PresentationStageProps = {
|
||||
readonly closeOverlay: () => void;
|
||||
readonly openDiscussion: (branchId: string) => void;
|
||||
readonly closeDiscussion: () => void;
|
||||
readonly toggleDiscussionIndex: () => void;
|
||||
};
|
||||
|
||||
export const PresentationStage = ({
|
||||
@@ -41,7 +39,6 @@ export const PresentationStage = ({
|
||||
closeOverlay,
|
||||
openDiscussion,
|
||||
closeDiscussion,
|
||||
toggleDiscussionIndex,
|
||||
}: PresentationStageProps) => {
|
||||
const composition = compositionForState(state);
|
||||
|
||||
@@ -55,8 +52,6 @@ export const PresentationStage = ({
|
||||
<LayoutGroup id="presentation-stage">
|
||||
<div
|
||||
className="presentation-stage"
|
||||
data-stage-theme={composition.stageTheme}
|
||||
data-chat-theme={composition.chatTheme}
|
||||
data-chat-mode={composition.chatMode}
|
||||
data-evidence-mode={composition.evidenceMode}
|
||||
data-scene-view={activeSceneView}
|
||||
@@ -68,45 +63,27 @@ export const PresentationStage = ({
|
||||
{state.location.kind === "discussion" ? (
|
||||
<DiscussionPanel branchId={state.location.branchId} onClose={closeDiscussion} />
|
||||
) : (
|
||||
<>
|
||||
<SceneBody
|
||||
location={state.location}
|
||||
demo={demo}
|
||||
selectedNodeId={state.selectedNodeId}
|
||||
selectNode={selectNode}
|
||||
openEvidence={openEvidence}
|
||||
onFocusPathChange={(path) => {
|
||||
if (state.location.kind === "main") {
|
||||
jump({ ...state.location, focusPath: path });
|
||||
}
|
||||
}}
|
||||
motionDisabled={false}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="presentation-stage__discussion-toggle"
|
||||
onClick={toggleDiscussionIndex}
|
||||
>
|
||||
Discussion topics
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{state.discussionIndexOpen && (
|
||||
<DiscussionIndex
|
||||
onSelect={(branchId) => {
|
||||
toggleDiscussionIndex();
|
||||
openDiscussion(branchId);
|
||||
<SceneBody
|
||||
location={state.location}
|
||||
demo={demo}
|
||||
selectedNodeId={state.selectedNodeId}
|
||||
selectNode={selectNode}
|
||||
openEvidence={openEvidence}
|
||||
onFocusPathChange={(path) => {
|
||||
if (state.location.kind === "main") {
|
||||
jump({ ...state.location, focusPath: path });
|
||||
}
|
||||
}}
|
||||
motionDisabled={false}
|
||||
/>
|
||||
)}
|
||||
<p className="presentation-stage__mode">
|
||||
{demo.state.mode === "replay" ? "Replay" : "Live"} · {demo.state.phase}
|
||||
</p>
|
||||
</section>
|
||||
<aside className="presentation-stage__evidence" aria-label="evidence region">
|
||||
<EvidenceDrawer records={evidence} mode={composition.evidenceMode} close={closeOverlay} />
|
||||
</aside>
|
||||
<SceneRail location={state.location} jump={jump} />
|
||||
{state.location.kind === "main" && (
|
||||
<SceneProgress location={state.location} />
|
||||
)}
|
||||
</div>
|
||||
</LayoutGroup>
|
||||
</LazyMotion>
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { PresenterControls } from "./PresenterControls.js";
|
||||
import { ChatDock } from "./ChatDock.js";
|
||||
import { initialPresentationState } from "./presentation-state.js";
|
||||
|
||||
afterEach(() => cleanup());
|
||||
|
||||
describe("PresenterControls", () => {
|
||||
const props = {
|
||||
state: initialPresentationState,
|
||||
next: vi.fn(),
|
||||
previous: vi.fn(),
|
||||
jump: vi.fn(),
|
||||
setStageTheme: vi.fn(),
|
||||
setChatTheme: vi.fn(),
|
||||
setChatMode: vi.fn(),
|
||||
forceReplay: vi.fn(),
|
||||
resetOverrides: vi.fn(),
|
||||
resetScene: vi.fn(),
|
||||
toggleMotion: vi.fn(),
|
||||
};
|
||||
|
||||
it("changes stage and chat themes independently", async () => {
|
||||
render(<PresenterControls {...props} />);
|
||||
await userEvent.selectOptions(screen.getByLabelText(/stage theme/i), "night");
|
||||
await userEvent.selectOptions(screen.getByLabelText(/chat theme/i), "light");
|
||||
expect(props.setStageTheme).toHaveBeenCalledWith("night");
|
||||
expect(props.setChatTheme).toHaveBeenCalledWith("light");
|
||||
});
|
||||
|
||||
it("forces replay without changing the current presentation location", async () => {
|
||||
render(<PresenterControls {...props} />);
|
||||
await userEvent.click(screen.getByRole("button", { name: /force replay fallback/i }));
|
||||
expect(props.forceReplay).toHaveBeenCalledTimes(1);
|
||||
expect(props.jump).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ChatDock", () => {
|
||||
it("opens docked chat by click and keyboard", async () => {
|
||||
const openChat = vi.fn();
|
||||
render(<ChatDock openChat={openChat} />);
|
||||
await userEvent.click(screen.getByRole("button", { name: /open agent chat/i }));
|
||||
expect(openChat).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -1,134 +0,0 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { PresentationState } from "./presentation-state.js";
|
||||
import { compositionForState } from "./presentation-state.js";
|
||||
import type { ChatMode, ChatTheme, MainLocation, PresentationLocation, StageTheme } from "./storyboard.js";
|
||||
import { findScene, mainScenes } from "./storyboard.js";
|
||||
|
||||
type PresenterControlsProps = {
|
||||
readonly state: PresentationState;
|
||||
readonly next: () => void;
|
||||
readonly previous: () => void;
|
||||
readonly jump: (location: PresentationLocation) => void;
|
||||
readonly setStageTheme: (theme: StageTheme | null) => void;
|
||||
readonly setChatTheme: (theme: ChatTheme | null) => void;
|
||||
readonly setChatMode: (mode: ChatMode | null) => void;
|
||||
readonly forceReplay: () => void;
|
||||
readonly resetOverrides: () => void;
|
||||
readonly resetScene: () => void;
|
||||
readonly toggleMotion: () => void;
|
||||
};
|
||||
|
||||
export const PresenterControls = ({
|
||||
state,
|
||||
next,
|
||||
previous,
|
||||
jump,
|
||||
setStageTheme,
|
||||
setChatTheme,
|
||||
setChatMode,
|
||||
forceReplay,
|
||||
resetOverrides,
|
||||
resetScene,
|
||||
toggleMotion,
|
||||
}: PresenterControlsProps) => {
|
||||
const composition = compositionForState(state);
|
||||
const isMain = state.location.kind === "main";
|
||||
const currentScene = isMain ? findScene(state.location.sceneId) : null;
|
||||
const currentBeat = currentScene?.beats.find((b) => b.id === (state.location as MainLocation).beatId);
|
||||
const beatIndex = currentScene && isMain
|
||||
? currentScene.beats.findIndex((b) => b.id === (state.location as MainLocation).beatId)
|
||||
: -1;
|
||||
const totalBeats = currentScene?.beats.length ?? 0;
|
||||
const sceneNumber = currentScene?.number ?? 0;
|
||||
const totalScenes = mainScenes.length;
|
||||
|
||||
const [elapsed, setElapsed] = useState(0);
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setElapsed(Math.floor((Date.now() - state.startedAt) / 1000));
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [state.startedAt]);
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const m = Math.floor(seconds / 60);
|
||||
const s = seconds % 60;
|
||||
return `${m}:${s.toString().padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
const plannedMinutes = 15;
|
||||
|
||||
return (
|
||||
<div className="presenter-controls" role="dialog" aria-label="presenter controls">
|
||||
<div className="presenter-controls__nav">
|
||||
<button type="button" onClick={previous}>Previous</button>
|
||||
<button type="button" onClick={next}>Next</button>
|
||||
</div>
|
||||
<div className="presenter-controls__info">
|
||||
<span>Scene {sceneNumber}/{totalScenes}: {currentScene?.title ?? "Discussion"}</span>
|
||||
{currentBeat && <span> · {beatIndex + 1}/{totalBeats} {currentBeat.title}</span>}
|
||||
</div>
|
||||
<div className="presenter-controls__timer">
|
||||
<span>Elapsed: {formatTime(elapsed)}</span>
|
||||
<span> / {plannedMinutes}:00 planned</span>
|
||||
</div>
|
||||
<div className="presenter-controls__notes">
|
||||
<label>
|
||||
Speaker notes
|
||||
<textarea
|
||||
key={currentBeat?.id ?? "none"}
|
||||
defaultValue={currentBeat?.caption ?? ""}
|
||||
rows={3}
|
||||
placeholder="Add notes for this beat..."
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="presenter-controls__over">
|
||||
<label>
|
||||
Stage theme
|
||||
<select
|
||||
value={state.stageThemeOverride ?? "scene default"}
|
||||
onChange={(e) => setStageTheme(e.target.value === "scene default" ? null : e.target.value as StageTheme)}
|
||||
>
|
||||
<option value="scene default">scene default</option>
|
||||
<option value="paper">paper</option>
|
||||
<option value="night">night</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Chat theme
|
||||
<select
|
||||
value={state.chatThemeOverride ?? "scene default"}
|
||||
onChange={(e) => setChatTheme(e.target.value === "scene default" ? null : e.target.value as ChatTheme)}
|
||||
>
|
||||
<option value="scene default">scene default</option>
|
||||
<option value="light">light</option>
|
||||
<option value="dark">dark</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Chat mode
|
||||
<select
|
||||
value={state.chatModeOverride ?? "scene default"}
|
||||
onChange={(e) => setChatMode(e.target.value === "scene default" ? null : e.target.value as ChatMode)}
|
||||
>
|
||||
<option value="scene default">scene default</option>
|
||||
<option value="hidden">hidden</option>
|
||||
<option value="full">full</option>
|
||||
<option value="rail">rail</option>
|
||||
<option value="dock">dock</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div className="presenter-controls__actions">
|
||||
<span>{state.playbackMode === "replay" ? "Replay" : "Live"}</span>
|
||||
<button type="button" onClick={forceReplay}>Force replay fallback</button>
|
||||
<button type="button" onClick={resetScene}>Reset scene</button>
|
||||
<button type="button" onClick={toggleMotion}>
|
||||
Motion: {state.motionDisabled ? "off" : "on"}
|
||||
</button>
|
||||
<button type="button" onClick={resetOverrides}>Reset overrides</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { SceneProgress } from "./SceneProgress.js";
|
||||
import type { MainLocation } from "./storyboard.js";
|
||||
|
||||
afterEach(() => cleanup());
|
||||
|
||||
describe("SceneProgress", () => {
|
||||
it("shows scene and beat position for architecture/runtime", () => {
|
||||
const location: MainLocation = {
|
||||
kind: "main",
|
||||
sceneId: "architecture",
|
||||
beatId: "runtime",
|
||||
focusPath: [],
|
||||
};
|
||||
|
||||
render(<SceneProgress location={location} />);
|
||||
|
||||
expect(screen.getByText("6 / 12")).toBeInTheDocument();
|
||||
expect(screen.getByText("3 / 4")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows scene position for workflow-demo/graph", () => {
|
||||
const location: MainLocation = {
|
||||
kind: "main",
|
||||
sceneId: "workflow-demo",
|
||||
beatId: "graph",
|
||||
focusPath: [],
|
||||
};
|
||||
|
||||
render(<SceneProgress location={location} />);
|
||||
|
||||
expect(screen.getByText("9 / 12")).toBeInTheDocument();
|
||||
expect(screen.getByText("2 / 3")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("has an accessible label", () => {
|
||||
const location: MainLocation = {
|
||||
kind: "main",
|
||||
sceneId: "architecture",
|
||||
beatId: "runtime",
|
||||
focusPath: [],
|
||||
};
|
||||
|
||||
render(<SceneProgress location={location} />);
|
||||
|
||||
expect(screen.getByLabelText("scene position")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { findScene, mainScenes, type MainLocation } from "./storyboard.js";
|
||||
|
||||
type SceneProgressProps = {
|
||||
readonly location: MainLocation;
|
||||
};
|
||||
|
||||
export const SceneProgress = ({ location }: SceneProgressProps) => {
|
||||
const scene = findScene(location.sceneId);
|
||||
const sceneIndex = mainScenes.findIndex((s) => s.id === location.sceneId);
|
||||
const totalScenes = mainScenes.length;
|
||||
const beatIndex = scene?.beats.findIndex((b) => b.id === location.beatId) ?? -1;
|
||||
const totalBeats = scene?.beats.length ?? 0;
|
||||
|
||||
return (
|
||||
<p className="scene-progress" aria-label="scene position">
|
||||
{sceneIndex >= 0 && (
|
||||
<span className="scene-progress__scene">
|
||||
{sceneIndex + 1} / {totalScenes}
|
||||
</span>
|
||||
)}
|
||||
{totalBeats > 1 && (
|
||||
<span className="scene-progress__beat">
|
||||
{beatIndex >= 0 ? beatIndex + 1 : 1} / {totalBeats}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
import { mainScenes, type MainLocation, type PresentationLocation } from "./storyboard.js";
|
||||
|
||||
type SceneRailProps = {
|
||||
readonly location: PresentationLocation;
|
||||
readonly jump: (location: MainLocation) => void;
|
||||
};
|
||||
|
||||
export const SceneRail = ({ location, jump }: SceneRailProps) => {
|
||||
const activeSceneId = location.kind === "main" ? location.sceneId : "positioning";
|
||||
return (
|
||||
<nav className="scene-rail" aria-label="presentation scene rail">
|
||||
{mainScenes.map((scene) => {
|
||||
const isActive = scene.id === activeSceneId;
|
||||
const activeBeatId =
|
||||
isActive && location.kind === "main" ? location.beatId : scene.beats[0]!.id;
|
||||
return (
|
||||
<button
|
||||
key={scene.id}
|
||||
type="button"
|
||||
data-active={isActive}
|
||||
aria-current={isActive ? "step" : undefined}
|
||||
onClick={() => jump({ kind: "main", sceneId: scene.id as MainLocation["sceneId"], beatId: scene.beats[0]!.id, focusPath: scene.beats[0]!.figure?.focusPath ?? [] })}
|
||||
className="scene-rail__scene"
|
||||
>
|
||||
<span className="scene-rail__number">{scene.number}</span>
|
||||
<small className="scene-rail__title">{scene.title}</small>
|
||||
<span className="scene-rail__progress">
|
||||
{scene.beats.map((beat) => (
|
||||
<span
|
||||
key={beat.id}
|
||||
className={`scene-rail__beat ${beat.id === activeBeatId ? "scene-rail__beat--active" : ""}`}
|
||||
/>
|
||||
))}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
@@ -111,14 +111,12 @@ describe("presentationReducer", () => {
|
||||
expect(nexted.location).toEqual(opened.location);
|
||||
});
|
||||
|
||||
it("derives act and chat composition from the current beat", () => {
|
||||
it("derives chatMode from the current beat", () => {
|
||||
const state = presentationReducer(initialPresentationState, {
|
||||
type: "jump",
|
||||
location: { kind: "main", sceneId: "workflow-demo", beatId: "graph", focusPath: [] },
|
||||
});
|
||||
expect(compositionForState(state)).toMatchObject({
|
||||
stageTheme: "night",
|
||||
chatTheme: "light",
|
||||
chatMode: "rail",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,12 +3,10 @@ import {
|
||||
findDiscussionBranch,
|
||||
findScene,
|
||||
type ChatMode,
|
||||
type ChatTheme,
|
||||
type DiscussionBranchId,
|
||||
type EvidenceMode,
|
||||
type MainLocation,
|
||||
type PresentationLocation,
|
||||
type StageTheme,
|
||||
} from "./storyboard.js";
|
||||
import {
|
||||
hashForLocation,
|
||||
@@ -23,11 +21,6 @@ export type PresentationState = {
|
||||
readonly selectedNodeId: string | null;
|
||||
readonly evidenceModeOverride: EvidenceMode | null;
|
||||
readonly playbackMode: "replay" | "live";
|
||||
readonly stageThemeOverride: StageTheme | null;
|
||||
readonly chatThemeOverride: ChatTheme | null;
|
||||
readonly chatModeOverride: ChatMode | null;
|
||||
readonly controlsOpen: boolean;
|
||||
readonly discussionIndexOpen: boolean;
|
||||
readonly motionDisabled: boolean;
|
||||
readonly startedAt: number;
|
||||
};
|
||||
@@ -44,12 +37,7 @@ export type PresentationAction =
|
||||
| { readonly type: "set_evidence_mode"; readonly mode: EvidenceMode }
|
||||
| { readonly type: "close_overlay" }
|
||||
| { readonly type: "set_playback_mode"; readonly mode: PresentationState["playbackMode"] }
|
||||
| { readonly type: "set_stage_theme"; readonly theme: StageTheme | null }
|
||||
| { readonly type: "set_chat_theme"; readonly theme: ChatTheme | null }
|
||||
| { readonly type: "set_chat_mode"; readonly mode: ChatMode | null }
|
||||
| { readonly type: "set_focus_path"; readonly path: readonly string[] }
|
||||
| { readonly type: "toggle_controls" }
|
||||
| { readonly type: "toggle_discussion_index" }
|
||||
| { readonly type: "toggle_motion" };
|
||||
|
||||
export const initialPresentationState: PresentationState = {
|
||||
@@ -58,11 +46,6 @@ export const initialPresentationState: PresentationState = {
|
||||
selectedNodeId: null,
|
||||
evidenceModeOverride: null,
|
||||
playbackMode: "replay",
|
||||
stageThemeOverride: null,
|
||||
chatThemeOverride: null,
|
||||
chatModeOverride: null,
|
||||
controlsOpen: false,
|
||||
discussionIndexOpen: false,
|
||||
motionDisabled: false,
|
||||
startedAt: Date.now(),
|
||||
};
|
||||
@@ -70,31 +53,20 @@ export const initialPresentationState: PresentationState = {
|
||||
const compositionForLocation = (
|
||||
location: PresentationLocation,
|
||||
evidenceOverride: EvidenceMode | null,
|
||||
stageThemeOverride: StageTheme | null,
|
||||
chatThemeOverride: ChatTheme | null,
|
||||
chatModeOverride: ChatMode | null,
|
||||
): {
|
||||
readonly stageTheme: StageTheme;
|
||||
readonly chatTheme: ChatTheme;
|
||||
readonly chatMode: ChatMode;
|
||||
readonly evidenceMode: EvidenceMode;
|
||||
} => {
|
||||
if (location.kind === "discussion") {
|
||||
const branch = findDiscussionBranch(location.branchId);
|
||||
const parentScene = branch ? findScene(branch.parentSceneId) : findScene("positioning");
|
||||
return {
|
||||
stageTheme: stageThemeOverride ?? parentScene?.stageTheme ?? "paper",
|
||||
chatTheme: chatThemeOverride ?? "dark",
|
||||
chatMode: chatModeOverride ?? "hidden",
|
||||
chatMode: "hidden",
|
||||
evidenceMode: evidenceOverride ?? "hidden",
|
||||
};
|
||||
}
|
||||
const scene = findScene(location.sceneId);
|
||||
const beat = scene?.beats.find((b) => b.id === location.beatId);
|
||||
return {
|
||||
stageTheme: stageThemeOverride ?? scene?.stageTheme ?? "paper",
|
||||
chatTheme: chatThemeOverride ?? beat?.chatTheme ?? "dark",
|
||||
chatMode: chatModeOverride ?? beat?.chatMode ?? "hidden",
|
||||
chatMode: beat?.chatMode ?? "hidden",
|
||||
evidenceMode: evidenceOverride ?? beat?.evidenceMode ?? "hidden",
|
||||
};
|
||||
};
|
||||
@@ -103,9 +75,6 @@ export const compositionForState = (state: PresentationState) =>
|
||||
compositionForLocation(
|
||||
state.location,
|
||||
state.evidenceModeOverride,
|
||||
state.stageThemeOverride,
|
||||
state.chatThemeOverride,
|
||||
state.chatModeOverride,
|
||||
);
|
||||
|
||||
const isValidMainLocation = (location: PresentationLocation): location is MainLocation =>
|
||||
@@ -190,8 +159,6 @@ export const presentationReducer = (
|
||||
return beat?.evidenceMode === "open" || beat?.evidenceMode === "peek";
|
||||
})();
|
||||
if (isEvidenceVisible) return { ...state, evidenceModeOverride: "hidden" };
|
||||
if (state.discussionIndexOpen) return { ...state, discussionIndexOpen: false };
|
||||
if (state.controlsOpen) return { ...state, controlsOpen: false };
|
||||
if (state.location.kind === "discussion") {
|
||||
return {
|
||||
...state,
|
||||
@@ -203,22 +170,12 @@ export const presentationReducer = (
|
||||
}
|
||||
case "set_playback_mode":
|
||||
return { ...state, playbackMode: action.mode };
|
||||
case "set_stage_theme":
|
||||
return { ...state, stageThemeOverride: action.theme };
|
||||
case "set_chat_theme":
|
||||
return { ...state, chatThemeOverride: action.theme };
|
||||
case "set_chat_mode":
|
||||
return { ...state, chatModeOverride: action.mode };
|
||||
case "set_focus_path":
|
||||
if (state.location.kind !== "main") return state;
|
||||
return {
|
||||
...state,
|
||||
location: { ...state.location, focusPath: action.path },
|
||||
};
|
||||
case "toggle_controls":
|
||||
return { ...state, controlsOpen: !state.controlsOpen };
|
||||
case "toggle_discussion_index":
|
||||
return { ...state, discussionIndexOpen: !state.discussionIndexOpen };
|
||||
case "toggle_motion":
|
||||
return { ...state, motionDisabled: !state.motionDisabled };
|
||||
}
|
||||
|
||||
@@ -18,13 +18,6 @@
|
||||
--failure: oklch(0.65 0.2 25);
|
||||
--chat-width: 18rem;
|
||||
--evidence-width: 0rem;
|
||||
/* Paper theme tokens */
|
||||
--paper-bg: oklch(0.97 0.005 80);
|
||||
--paper-text: oklch(0.15 0.01 80);
|
||||
--paper-muted: oklch(0.5 0.03 80);
|
||||
--paper-accent: oklch(0.45 0.14 80);
|
||||
--paper-border: oklch(0.75 0.04 80);
|
||||
--paper-surface: oklch(0.92 0.01 80);
|
||||
}
|
||||
|
||||
.presentation-stage {
|
||||
@@ -32,193 +25,14 @@
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template:
|
||||
"chat primary evidence" minmax(0, 1fr)
|
||||
"rail rail rail" auto /
|
||||
"chat primary evidence" minmax(0, 1fr) /
|
||||
var(--chat-width) minmax(0, 1fr) var(--evidence-width);
|
||||
}
|
||||
|
||||
/* Stage themes */
|
||||
.presentation-stage[data-stage-theme="paper"] {
|
||||
background: var(--paper-bg);
|
||||
color: var(--paper-text);
|
||||
}
|
||||
|
||||
.presentation-stage[data-stage-theme="night"] {
|
||||
background: var(--stage-canvas);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Paper theme child-element overrides */
|
||||
.presentation-stage[data-stage-theme="paper"] .stage-caption__eyebrow {
|
||||
color: var(--paper-muted);
|
||||
}
|
||||
|
||||
.presentation-stage[data-stage-theme="paper"] .stage-caption h1 {
|
||||
color: var(--paper-text);
|
||||
}
|
||||
|
||||
.presentation-stage[data-stage-theme="paper"] .scene-body__evidence {
|
||||
color: var(--paper-muted);
|
||||
}
|
||||
|
||||
.presentation-stage[data-stage-theme="paper"] .scene-rail__scene {
|
||||
border-color: oklch(0.75 0.04 80);
|
||||
background: oklch(0.92 0.01 80);
|
||||
}
|
||||
|
||||
.presentation-stage[data-stage-theme="paper"] .scene-rail__scene[data-active="true"] {
|
||||
background: oklch(0.35 0.12 80);
|
||||
color: oklch(0.97 0.005 80);
|
||||
}
|
||||
|
||||
.presentation-stage[data-stage-theme="paper"] .scene-rail__beat {
|
||||
background: oklch(0.7 0.04 80);
|
||||
}
|
||||
|
||||
.presentation-stage[data-stage-theme="paper"] .scene-rail__beat--active {
|
||||
background: oklch(0.3 0.08 80);
|
||||
}
|
||||
|
||||
.presentation-stage[data-stage-theme="paper"] .presentation-stage__mode {
|
||||
color: oklch(0.5 0.03 80);
|
||||
}
|
||||
|
||||
.presentation-stage[data-stage-theme="paper"] .presentation-stage__discussion-toggle {
|
||||
border-color: oklch(0.7 0.05 80);
|
||||
background: oklch(0.9 0.01 80);
|
||||
color: oklch(0.2 0.02 80);
|
||||
}
|
||||
|
||||
/* Paper theme scene-body element overrides */
|
||||
.presentation-stage[data-stage-theme="paper"] .scene-body__positioning-card {
|
||||
border-color: oklch(0.75 0.04 80);
|
||||
background: oklch(0.92 0.01 80);
|
||||
color: oklch(0.15 0.01 80);
|
||||
}
|
||||
|
||||
.presentation-stage[data-stage-theme="paper"] .scene-body__positioning-card span {
|
||||
color: oklch(0.4 0.03 80);
|
||||
}
|
||||
|
||||
.presentation-stage[data-stage-theme="paper"] .scene-body__positioning-card--active {
|
||||
border-color: oklch(0.45 0.14 80);
|
||||
background: oklch(0.85 0.04 80);
|
||||
color: oklch(0.12 0.02 80);
|
||||
}
|
||||
|
||||
.presentation-stage[data-stage-theme="paper"] .scene-body__boundary-side {
|
||||
border-color: oklch(0.75 0.04 80);
|
||||
background: oklch(0.92 0.01 80);
|
||||
color: oklch(0.15 0.01 80);
|
||||
}
|
||||
|
||||
.presentation-stage[data-stage-theme="paper"] .scene-body__boundary-side--dim {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.presentation-stage[data-stage-theme="paper"] .scene-body__boundary-divider {
|
||||
background: oklch(0.5 0.08 80);
|
||||
}
|
||||
|
||||
.presentation-stage[data-stage-theme="paper"] .scene-body__lifecycle-stage,
|
||||
.presentation-stage[data-stage-theme="paper"] .scene-body__authoring-step {
|
||||
border-color: oklch(0.75 0.04 80);
|
||||
background: oklch(0.92 0.01 80);
|
||||
color: oklch(0.15 0.01 80);
|
||||
}
|
||||
|
||||
.presentation-stage[data-stage-theme="paper"] .scene-body__lifecycle-number,
|
||||
.presentation-stage[data-stage-theme="paper"] .scene-body__authoring-number {
|
||||
background: oklch(0.45 0.14 80);
|
||||
color: oklch(0.97 0.005 80);
|
||||
}
|
||||
|
||||
.presentation-stage[data-stage-theme="paper"] .scene-body__lifecycle-arrow,
|
||||
.presentation-stage[data-stage-theme="paper"] .scene-body__authoring-arrow {
|
||||
color: oklch(0.5 0.06 80);
|
||||
}
|
||||
|
||||
.presentation-stage[data-stage-theme="paper"] .scene-body__lifecycle-stage--active,
|
||||
.presentation-stage[data-stage-theme="paper"] .scene-body__authoring-step--active {
|
||||
border-color: oklch(0.45 0.14 80);
|
||||
background: oklch(0.85 0.04 80);
|
||||
}
|
||||
|
||||
.presentation-stage[data-stage-theme="paper"] .scene-body__architecture-layer {
|
||||
border-color: oklch(0.75 0.04 80);
|
||||
background: oklch(0.92 0.01 80);
|
||||
color: oklch(0.15 0.01 80);
|
||||
}
|
||||
|
||||
.presentation-stage[data-stage-theme="paper"] .scene-body__architecture-layer--node {
|
||||
border-color: oklch(0.45 0.14 80);
|
||||
background: oklch(0.85 0.04 80);
|
||||
}
|
||||
|
||||
.presentation-stage[data-stage-theme="paper"] .scene-body__architecture-layer--active {
|
||||
border-color: oklch(0.45 0.14 80);
|
||||
background: oklch(0.85 0.04 80);
|
||||
}
|
||||
|
||||
.presentation-stage[data-stage-theme="paper"] .scene-body__architecture-arrow {
|
||||
color: oklch(0.5 0.06 80);
|
||||
}
|
||||
|
||||
.presentation-stage[data-stage-theme="paper"] .scene-body__evaluation-stat {
|
||||
border-color: oklch(0.75 0.04 80);
|
||||
background: oklch(0.92 0.01 80);
|
||||
}
|
||||
|
||||
.presentation-stage[data-stage-theme="paper"] .scene-body__evaluation-stat strong {
|
||||
color: oklch(0.45 0.14 80);
|
||||
}
|
||||
|
||||
.presentation-stage[data-stage-theme="paper"] .scene-body__evaluation-stat span {
|
||||
color: oklch(0.4 0.03 80);
|
||||
}
|
||||
|
||||
.presentation-stage[data-stage-theme="paper"] .scene-body__evaluation-stat--active {
|
||||
border-color: oklch(0.45 0.14 80);
|
||||
background: oklch(0.85 0.04 80);
|
||||
}
|
||||
|
||||
/* Chat themes */
|
||||
.presentation-stage[data-chat-theme="light"] .presentation-stage__chat,
|
||||
.operator-chat[data-chat-theme="light"] {
|
||||
background: oklch(0.96 0.005 80);
|
||||
color: oklch(0.18 0.01 80);
|
||||
}
|
||||
|
||||
.presentation-stage[data-chat-theme="dark"] .presentation-stage__chat,
|
||||
.operator-chat[data-chat-theme="dark"] {
|
||||
background: oklch(0.14 0.025 250);
|
||||
color: oklch(0.92 0.01 250);
|
||||
}
|
||||
|
||||
/* Light chat message overrides */
|
||||
.presentation-stage[data-chat-theme="light"] .chat-message,
|
||||
.operator-chat[data-chat-theme="light"] .chat-message {
|
||||
border-color: oklch(0.75 0.04 80);
|
||||
background: oklch(0.92 0.01 80);
|
||||
color: oklch(0.18 0.01 80);
|
||||
}
|
||||
|
||||
/* Light chat tool-part overrides */
|
||||
.presentation-stage[data-chat-theme="light"] .chat-tool-part,
|
||||
.operator-chat[data-chat-theme="light"] .chat-tool-part {
|
||||
border-color: oklch(0.75 0.04 80);
|
||||
}
|
||||
|
||||
.presentation-stage[data-chat-theme="light"] .chat-tool-part span,
|
||||
.operator-chat[data-chat-theme="light"] .chat-tool-part span {
|
||||
color: oklch(0.45 0.03 80);
|
||||
}
|
||||
|
||||
/* Chat modes */
|
||||
.presentation-stage[data-chat-mode="hidden"] {
|
||||
grid-template:
|
||||
"primary evidence" minmax(0, 1fr)
|
||||
"rail rail" auto /
|
||||
"primary evidence" minmax(0, 1fr) /
|
||||
minmax(0, 1fr) var(--evidence-width);
|
||||
}
|
||||
|
||||
@@ -230,17 +44,6 @@
|
||||
--chat-width: 16rem;
|
||||
}
|
||||
|
||||
.presentation-stage[data-chat-mode="dock"] {
|
||||
grid-template:
|
||||
"primary evidence" minmax(0, 1fr)
|
||||
"rail rail" auto /
|
||||
minmax(0, 1fr) var(--evidence-width);
|
||||
}
|
||||
|
||||
.presentation-stage[data-chat-mode="dock"] .presentation-stage__chat {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Evidence modes */
|
||||
.presentation-stage[data-evidence-mode="peek"] {
|
||||
--evidence-width: 20rem;
|
||||
@@ -313,59 +116,6 @@
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.scene-rail {
|
||||
grid-area: rail;
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
padding: 0.35rem 0.75rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.scene-rail__scene {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.15rem;
|
||||
min-width: 4.5rem;
|
||||
text-align: center;
|
||||
border: 1px solid oklch(0.36 0.04 250);
|
||||
background: oklch(0.16 0.025 250);
|
||||
color: inherit;
|
||||
padding: 0.35rem 0.5rem;
|
||||
border-radius: 0.4rem;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.scene-rail__scene[data-active="true"] {
|
||||
background: oklch(0.7 0.16 195);
|
||||
color: oklch(0.12 0.025 250);
|
||||
}
|
||||
|
||||
.scene-rail__number {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.scene-rail__title {
|
||||
font-size: 0.6rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.scene-rail__progress {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.scene-rail__beat {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: oklch(0.5 0.04 250);
|
||||
}
|
||||
|
||||
.scene-rail__beat--active {
|
||||
background: oklch(0.9 0.06 250);
|
||||
}
|
||||
|
||||
.operation-block {
|
||||
border: 1px solid var(--stage-line);
|
||||
background: var(--stage-surface);
|
||||
@@ -601,25 +351,23 @@
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.presentation-stage__mode {
|
||||
flex-shrink: 0;
|
||||
.scene-progress {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: oklch(0.65 0.03 250);
|
||||
margin: 0;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.presentation-stage__discussion-toggle {
|
||||
flex-shrink: 0;
|
||||
align-self: flex-start;
|
||||
font-size: 0.8rem;
|
||||
padding: 0.35rem 0.65rem;
|
||||
border: 1px solid oklch(0.4 0.05 250);
|
||||
background: oklch(0.2 0.03 250);
|
||||
color: inherit;
|
||||
border-radius: 0.4rem;
|
||||
.scene-progress__scene {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.scene-progress__beat {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.discussion-index,
|
||||
.discussion-panel {
|
||||
padding: 1rem;
|
||||
border: 1px solid oklch(0.36 0.04 250);
|
||||
@@ -627,36 +375,11 @@
|
||||
border-radius: 0.65rem;
|
||||
}
|
||||
|
||||
.discussion-index h2,
|
||||
.discussion-panel h2 {
|
||||
font-size: 1.1rem;
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.discussion-index h3 {
|
||||
font-size: 0.9rem;
|
||||
margin: 0.5rem 0 0.25rem;
|
||||
color: oklch(0.78 0.035 250);
|
||||
}
|
||||
|
||||
.discussion-index ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.discussion-index button {
|
||||
padding: 0.3rem 0.6rem;
|
||||
border: 1px solid oklch(0.4 0.05 250);
|
||||
background: oklch(0.2 0.03 250);
|
||||
color: inherit;
|
||||
border-radius: 0.4rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.discussion-panel__badge {
|
||||
display: inline-block;
|
||||
padding: 0.15rem 0.4rem;
|
||||
@@ -694,118 +417,6 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.presenter-controls {
|
||||
position: fixed;
|
||||
top: 0.75rem;
|
||||
left: 0.75rem;
|
||||
width: min(20rem, calc(100vw - 1.5rem));
|
||||
padding: 0.75rem;
|
||||
border: 1px solid oklch(0.4 0.05 250);
|
||||
background: oklch(0.17 0.025 250);
|
||||
z-index: 40;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.presenter-controls__nav {
|
||||
display: flex;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.presenter-controls__nav button {
|
||||
flex: 1;
|
||||
padding: 0.3rem 0.5rem;
|
||||
border: 1px solid oklch(0.4 0.05 250);
|
||||
background: oklch(0.2 0.03 250);
|
||||
color: inherit;
|
||||
border-radius: 0.3rem;
|
||||
}
|
||||
|
||||
.presenter-controls__info {
|
||||
color: oklch(0.78 0.035 250);
|
||||
}
|
||||
|
||||
.presenter-controls__timer {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 0.8rem;
|
||||
color: oklch(0.78 0.035 250);
|
||||
}
|
||||
|
||||
.presenter-controls__notes label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.presenter-controls__notes textarea {
|
||||
width: 100%;
|
||||
padding: 0.35rem;
|
||||
border: 1px solid oklch(0.36 0.04 250);
|
||||
background: oklch(0.18 0.025 250);
|
||||
color: inherit;
|
||||
border-radius: 0.3rem;
|
||||
font-size: 0.75rem;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.presenter-controls__over label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.presenter-controls__over select {
|
||||
padding: 0.25rem;
|
||||
border: 1px solid oklch(0.36 0.04 250);
|
||||
background: oklch(0.18 0.025 250);
|
||||
color: inherit;
|
||||
border-radius: 0.3rem;
|
||||
}
|
||||
|
||||
.presenter-controls__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.presenter-controls__actions button {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border: 1px solid oklch(0.4 0.05 250);
|
||||
background: oklch(0.2 0.03 250);
|
||||
color: inherit;
|
||||
border-radius: 0.3rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.chat-dock {
|
||||
position: fixed;
|
||||
bottom: 2.5rem;
|
||||
left: 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.45rem 0.75rem;
|
||||
border: 1px solid oklch(0.4 0.05 250);
|
||||
background: oklch(0.2 0.03 250);
|
||||
color: inherit;
|
||||
border-radius: 0.5rem;
|
||||
z-index: 25;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chat-dock:hover,
|
||||
.chat-dock:focus-visible {
|
||||
background: oklch(0.25 0.04 250);
|
||||
}
|
||||
|
||||
.presentation-route__agent-button {
|
||||
position: fixed;
|
||||
bottom: 0.75rem;
|
||||
@@ -1083,15 +694,6 @@
|
||||
}
|
||||
|
||||
@media (max-height: 760px) {
|
||||
.scene-rail {
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
.scene-rail__scene {
|
||||
min-width: 3.5rem;
|
||||
padding: 0.25rem 0.35rem;
|
||||
}
|
||||
|
||||
.stage-caption h1 {
|
||||
font-size: clamp(1.4rem, 3vw, 2.2rem);
|
||||
}
|
||||
|
||||
@@ -574,131 +574,6 @@
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* Chat remains an app surface while the workflow stage stays cinematic. */
|
||||
.presentation-stage[data-chat-theme="light"] .presentation-stage__chat {
|
||||
border-right: 1px solid oklch(0.82 0.015 80);
|
||||
background: oklch(0.975 0.004 80);
|
||||
}
|
||||
|
||||
.presentation-stage[data-chat-theme="light"] .operator-chat {
|
||||
gap: 1.05rem;
|
||||
padding: 1rem 0.85rem;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.presentation-stage[data-chat-theme="light"] .chat-message {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
color: oklch(0.22 0.015 80);
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.presentation-stage[data-chat-theme="light"] .chat-message > strong {
|
||||
display: block;
|
||||
margin-bottom: 0.35rem;
|
||||
color: oklch(0.42 0.025 80);
|
||||
font: 650 0.6rem/1 var(--font-mono, monospace);
|
||||
letter-spacing: 0.09em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.presentation-stage[data-chat-theme="light"] .chat-message--operator {
|
||||
margin-left: 1rem;
|
||||
border-radius: 0.85rem;
|
||||
padding: 0.65rem 0.75rem;
|
||||
background: oklch(0.925 0.012 80);
|
||||
}
|
||||
|
||||
.presentation-stage[data-chat-theme="light"] .chat-message p {
|
||||
margin: 0.2rem 0;
|
||||
}
|
||||
|
||||
.presentation-stage[data-chat-theme="light"] .chat-tool-part {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 0.3rem;
|
||||
margin-top: 0.5rem;
|
||||
border: 1px solid oklch(0.82 0.015 80);
|
||||
border-radius: 0.55rem;
|
||||
padding: 0.55rem 0.65rem;
|
||||
background: oklch(0.95 0.006 80);
|
||||
}
|
||||
|
||||
.presentation-stage[data-chat-theme="light"] .chat-tool-part span {
|
||||
grid-column: 1 / -1;
|
||||
color: oklch(0.48 0.04 195);
|
||||
font: 650 0.56rem/1 var(--font-mono, monospace);
|
||||
letter-spacing: 0.09em;
|
||||
}
|
||||
|
||||
.presentation-stage[data-chat-theme="light"] .chat-tool-part code {
|
||||
overflow: hidden;
|
||||
color: oklch(0.26 0.025 250);
|
||||
font-size: 0.68rem;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.presentation-stage[data-chat-theme="light"] .chat-tool-part--handoff {
|
||||
border-color: oklch(0.56 0.13 195 / 0.55);
|
||||
background: oklch(0.94 0.025 195);
|
||||
}
|
||||
|
||||
.presentation-route .presentation-stage__discussion-toggle {
|
||||
position: absolute;
|
||||
right: 1.25rem;
|
||||
bottom: 0.7rem;
|
||||
z-index: 8;
|
||||
border: 0;
|
||||
padding: 0.25rem 0;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
font: 600 0.62rem/1 var(--font-mono, monospace);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.presentation-route .presentation-stage__mode {
|
||||
position: absolute;
|
||||
bottom: 0.7rem;
|
||||
left: 1.25rem;
|
||||
z-index: 8;
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
font: 600 0.6rem/1 var(--font-mono, monospace);
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.presentation-route .scene-rail {
|
||||
gap: 0.25rem;
|
||||
min-height: 2.7rem;
|
||||
border-top: 1px solid oklch(0.36 0.04 250 / 0.7);
|
||||
padding: 0.35rem 0.45rem;
|
||||
background: oklch(0.095 0.02 250);
|
||||
}
|
||||
|
||||
.presentation-route .scene-rail__scene {
|
||||
min-width: 2.5rem;
|
||||
min-height: 1.9rem;
|
||||
border: 0;
|
||||
border-radius: 0.35rem;
|
||||
padding: 0.25rem 0.35rem;
|
||||
background: transparent;
|
||||
color: oklch(0.62 0.025 250);
|
||||
}
|
||||
|
||||
.presentation-route .scene-rail__scene[data-active="true"] {
|
||||
background: oklch(0.72 0.17 195 / 0.13);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.presentation-route .scene-rail__title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 1050px) {
|
||||
.demo-workflow-stage__graph:has(.interrupt-contract-preview) {
|
||||
grid-template-columns: minmax(0, 1fr) 15rem;
|
||||
|
||||
Reference in New Issue
Block a user