fix: move audience pairing above deck

This commit is contained in:
lda
2026-07-14 15:29:35 +07:00 Verified
parent b87a6abcde
commit f5eddae5a4
5 changed files with 19 additions and 23 deletions
@@ -2,20 +2,10 @@ import { cleanup, render, screen, within } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import type { EvidenceRecord } from "../app/state.js"; import type { EvidenceRecord } from "../app/state.js";
import type { DemoChromePresentation } from "./presentation-demo-chrome.js"; import type { DemoChromePresentation } from "./presentation-demo-chrome.js";
import type { PresentationSyncController } from "./sync/presentation-sync-state.js";
import { PresentationFooter } from "./PresentationFooter.js"; import { PresentationFooter } from "./PresentationFooter.js";
afterEach(() => cleanup()); afterEach(() => cleanup());
const standaloneSyncController = (): PresentationSyncController => ({
state: { kind: "standalone" },
startSession: vi.fn(async () => {}),
joinSession: vi.fn(async () => {}),
retry: vi.fn(),
leaveSession: vi.fn(),
endSession: vi.fn(),
});
describe("PresentationFooter", () => { describe("PresentationFooter", () => {
it("combines scene progress and evidence provenance", () => { it("combines scene progress and evidence provenance", () => {
const evidence: EvidenceRecord = { const evidence: EvidenceRecord = {
@@ -40,7 +30,6 @@ describe("PresentationFooter", () => {
}} }}
evidence={[evidence]} evidence={[evidence]}
demoRail={hiddenRail} demoRail={hiddenRail}
syncController={standaloneSyncController()}
retryHealth={vi.fn()} retryHealth={vi.fn()}
showEvidenceReceipt showEvidenceReceipt
inspectEvidence={vi.fn()} inspectEvidence={vi.fn()}
@@ -63,7 +52,6 @@ describe("PresentationFooter", () => {
}} }}
evidence={[]} evidence={[]}
demoRail={{ kind: "hidden" }} demoRail={{ kind: "hidden" }}
syncController={standaloneSyncController()}
retryHealth={vi.fn()} retryHealth={vi.fn()}
showEvidenceReceipt={false} showEvidenceReceipt={false}
inspectEvidence={vi.fn()} inspectEvidence={vi.fn()}
@@ -92,7 +80,6 @@ describe("PresentationFooter", () => {
canRun: true, canRun: true,
canRetry: true, canRetry: true,
}} }}
syncController={standaloneSyncController()}
retryHealth={vi.fn()} retryHealth={vi.fn()}
showEvidenceReceipt={false} showEvidenceReceipt={false}
inspectEvidence={vi.fn()} inspectEvidence={vi.fn()}
@@ -103,7 +90,7 @@ describe("PresentationFooter", () => {
expect(screen.getByText("Live target ready")).toBeInTheDocument(); expect(screen.getByText("Live target ready")).toBeInTheDocument();
}); });
it("keeps audience pairing in the non-demo utility area beside one demo rail", () => { it("keeps the footer focused on one demo rail without the pairing surface", () => {
render( render(
<PresentationFooter <PresentationFooter
location={{ kind: "main", sceneId: "agent-handoff", beatId: "request", focusPath: [] }} location={{ kind: "main", sceneId: "agent-handoff", beatId: "request", focusPath: [] }}
@@ -121,7 +108,6 @@ describe("PresentationFooter", () => {
canRun: true, canRun: true,
canRetry: true, canRetry: true,
}} }}
syncController={standaloneSyncController()}
retryHealth={vi.fn()} retryHealth={vi.fn()}
showEvidenceReceipt={false} showEvidenceReceipt={false}
inspectEvidence={vi.fn()} inspectEvidence={vi.fn()}
@@ -132,8 +118,8 @@ describe("PresentationFooter", () => {
const utility = footer.querySelector<HTMLElement>(".presentation-footer__utility"); const utility = footer.querySelector<HTMLElement>(".presentation-footer__utility");
expect(utility).not.toBeNull(); expect(utility).not.toBeNull();
if (utility === null) throw new Error("presentation utility area is missing"); if (utility === null) throw new Error("presentation utility area is missing");
expect(within(utility).getByRole("complementary", { name: "Presentation pairing" })) expect(within(utility).queryByRole("complementary", { name: "Presentation pairing" }))
.toHaveAttribute("data-role", "audience"); .not.toBeInTheDocument();
expect(screen.getAllByTestId("presentation-demo-rail")).toHaveLength(1); expect(screen.getAllByTestId("presentation-demo-rail")).toHaveLength(1);
}); });
}); });
@@ -5,8 +5,6 @@ import { EvidenceReceipt } from "./evidence/EvidenceReceipt.js";
import type { DemoChromePresentation } from "./presentation-demo-chrome.js"; import type { DemoChromePresentation } from "./presentation-demo-chrome.js";
import { PresentationDemoRail } from "./PresentationDemoRail.js"; import { PresentationDemoRail } from "./PresentationDemoRail.js";
import type { MainLocation } from "./storyboard.js"; import type { MainLocation } from "./storyboard.js";
import { PresentationPairingPanel } from "./sync/PresentationPairingPanel.js";
import type { PresentationSyncController } from "./sync/presentation-sync-state.js";
type PresentationFooterProps = { type PresentationFooterProps = {
readonly location: MainLocation; readonly location: MainLocation;
@@ -16,7 +14,6 @@ type PresentationFooterProps = {
readonly retryHealth: () => void; readonly retryHealth: () => void;
readonly showEvidenceReceipt: boolean; readonly showEvidenceReceipt: boolean;
readonly inspectEvidence: () => void; readonly inspectEvidence: () => void;
readonly syncController: PresentationSyncController;
}; };
export const PresentationFooter = ({ export const PresentationFooter = ({
@@ -27,7 +24,6 @@ export const PresentationFooter = ({
retryHealth, retryHealth,
showEvidenceReceipt, showEvidenceReceipt,
inspectEvidence, inspectEvidence,
syncController,
}: PresentationFooterProps) => ( }: PresentationFooterProps) => (
<footer className="presentation-footer" aria-label="presentation footer"> <footer className="presentation-footer" aria-label="presentation footer">
<SceneProgress location={location} /> <SceneProgress location={location} />
@@ -37,7 +33,6 @@ export const PresentationFooter = ({
retryHealth={retryHealth} retryHealth={retryHealth}
/> />
<div className="presentation-footer__utility"> <div className="presentation-footer__utility">
<PresentationPairingPanel role="audience" controller={syncController} />
<EvidenceReceipt <EvidenceReceipt
records={evidence} records={evidence}
visible={showEvidenceReceipt} visible={showEvidenceReceipt}
@@ -178,6 +178,8 @@ describe("PresentationRoute", () => {
const panel = screen.getByRole("complementary", { name: "Presentation pairing" }); const panel = screen.getByRole("complementary", { name: "Presentation pairing" });
expect(panel).toHaveAttribute("data-role", "audience"); expect(panel).toHaveAttribute("data-role", "audience");
expect(panel.closest(".presentation-stage__sync")).not.toBeNull();
expect(panel.closest(".presentation-footer")).toBeNull();
expect(within(panel).getByRole("button", { name: "Pair presentation" })).toBeInTheDocument(); expect(within(panel).getByRole("button", { name: "Pair presentation" })).toBeInTheDocument();
}); });
@@ -12,6 +12,7 @@ import type { PresentationState } from "./presentation-state.js";
import { compositionForState } from "./presentation-state.js"; import { compositionForState } from "./presentation-state.js";
import type { PresentationTargetHealth } from "./presentation-target-status.js"; import type { PresentationTargetHealth } from "./presentation-target-status.js";
import type { PresentationSyncController } from "./sync/presentation-sync-state.js"; import type { PresentationSyncController } from "./sync/presentation-sync-state.js";
import { PresentationPairingPanel } from "./sync/PresentationPairingPanel.js";
import { demoChromeFor } from "./presentation-demo-chrome.js"; import { demoChromeFor } from "./presentation-demo-chrome.js";
import type { DemoTimelineController } from "../demo/useDemoTimeline.js"; import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
import { findScene, type MainLocation } from "./storyboard.js"; import { findScene, type MainLocation } from "./storyboard.js";
@@ -89,6 +90,9 @@ export const PresentationStage = ({
data-evidence-presentation={composition.evidencePresentation} data-evidence-presentation={composition.evidencePresentation}
data-scene-view={activeSceneView} data-scene-view={activeSceneView}
> >
<div className="presentation-stage__sync">
<PresentationPairingPanel role="audience" controller={syncController} />
</div>
<aside className="presentation-stage__chat" aria-label="agent chat region"> <aside className="presentation-stage__chat" aria-label="agent chat region">
<OperatorChat state={state} messages={messages} timelineAgent={timelineAgent} onApprove={onApprove} onRequestRevision={onRequestRevision} /> <OperatorChat state={state} messages={messages} timelineAgent={timelineAgent} onApprove={onApprove} onRequestRevision={onRequestRevision} />
</aside> </aside>
@@ -129,7 +133,6 @@ export const PresentationStage = ({
retryHealth={retryHealth} retryHealth={retryHealth}
showEvidenceReceipt={composition.evidencePresentation !== "hidden"} showEvidenceReceipt={composition.evidencePresentation !== "hidden"}
inspectEvidence={openEvidence} inspectEvidence={openEvidence}
syncController={syncController}
/> />
)} )}
<EvidenceInspector <EvidenceInspector
@@ -1077,6 +1077,12 @@
.presentation-stage__chat { grid-area: chat; } .presentation-stage__chat { grid-area: chat; }
.presentation-stage__primary { grid-area: primary; } .presentation-stage__primary { grid-area: primary; }
/* The audience pairing control floats above the deck instead of competing with demo footer controls. */
.presentation-stage__sync .presentation-pairing {
top: 1rem;
bottom: auto;
}
.presentation-footer { .presentation-footer {
grid-area: footer; grid-area: footer;
min-height: var(--presentation-footer-height); min-height: var(--presentation-footer-height);
@@ -1225,6 +1231,10 @@
} }
@media (max-width: 640px) { @media (max-width: 640px) {
.presentation-stage__sync .presentation-pairing {
top: 0.5rem;
}
.presentation-footer__utility { .presentation-footer__utility {
flex: 0 1 auto; flex: 0 1 auto;
max-width: 52%; max-width: 52%;