feat: expose defense qna in presenter sidebar

This commit is contained in:
lda
2026-07-13 10:06:09 +07:00 Verified
parent c36542413e
commit f43853e63f
5 changed files with 37 additions and 5 deletions
@@ -43,5 +43,7 @@ describe("PresenterRoute", () => {
expect(screen.getByRole("heading", { name: /Where is the AI agent/i })).toBeInTheDocument(); expect(screen.getByRole("heading", { name: /Where is the AI agent/i })).toBeInTheDocument();
expect(screen.getByText(/Answer directly first/i)).toBeInTheDocument(); expect(screen.getByText(/Answer directly first/i)).toBeInTheDocument();
expect(screen.getByText(/Abstract; Chapter 1 framing/i)).toBeInTheDocument(); expect(screen.getByText(/Abstract; Chapter 1 framing/i)).toBeInTheDocument();
expect(screen.getByText(/Defense Q&A/i).closest("details")).toHaveAttribute("open");
expect(screen.getByRole("link", { name: /Where is the AI agent in this thesis/i })).toHaveAttribute("aria-current", "page");
}); });
}); });
@@ -35,7 +35,11 @@ export const PresenterRoute = () => {
const discussion = navigation.location.kind === "discussion" ? findDiscussionBranch(navigation.location.branchId) : undefined; const discussion = navigation.location.kind === "discussion" ? findDiscussionBranch(navigation.location.branchId) : undefined;
return ( return (
<PresenterShell current={navigation.note} covered={covered}> <PresenterShell
current={navigation.note}
covered={covered}
activeDiscussionId={navigation.location.kind === "discussion" ? navigation.location.branchId : null}
>
{navigation.note && ( {navigation.note && (
<PresenterNavigationBar <PresenterNavigationBar
currentIndex={navigation.index} currentIndex={navigation.index}
@@ -1,16 +1,18 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import type { PresenterBeatNote } from "./presenter-notes.js"; import type { PresenterBeatNote } from "./presenter-notes.js";
import type { DiscussionBranchId } from "../storyboard.js";
import { PresenterSidebar } from "./PresenterSidebar.js"; import { PresenterSidebar } from "./PresenterSidebar.js";
type PresenterShellProps = { type PresenterShellProps = {
readonly current: PresenterBeatNote | null; readonly current: PresenterBeatNote | null;
readonly covered: ReadonlySet<string>; readonly covered: ReadonlySet<string>;
readonly activeDiscussionId: DiscussionBranchId | null;
readonly children: ReactNode; readonly children: ReactNode;
}; };
export const PresenterShell = ({ current, covered, children }: PresenterShellProps) => ( export const PresenterShell = ({ current, covered, activeDiscussionId, children }: PresenterShellProps) => (
<main className="presenter-route" aria-label="lda.chat presenter notes"> <main className="presenter-route" aria-label="lda.chat presenter notes">
<PresenterSidebar current={current} covered={covered} /> <PresenterSidebar current={current} covered={covered} activeDiscussionId={activeDiscussionId} />
<div className="presenter-route__reader">{children}</div> <div className="presenter-route__reader">{children}</div>
</main> </main>
); );
@@ -1,15 +1,31 @@
import { mainScenes } from "../storyboard.js"; import { discussionBranches, mainScenes, type DiscussionBranchId } from "../storyboard.js";
import { presenterSceneNotes, type PresenterBeatNote } from "./presenter-notes.js"; import { presenterSceneNotes, type PresenterBeatNote } from "./presenter-notes.js";
import { presenterHashForNote } from "./presenter-navigation.js"; import { presenterHashForNote } from "./presenter-navigation.js";
type PresenterSidebarProps = { type PresenterSidebarProps = {
readonly current: PresenterBeatNote | null; readonly current: PresenterBeatNote | null;
readonly covered: ReadonlySet<string>; readonly covered: ReadonlySet<string>;
readonly activeDiscussionId: DiscussionBranchId | null;
}; };
export const PresenterSidebar = ({ current, covered }: PresenterSidebarProps) => ( export const PresenterSidebar = ({ current, covered, activeDiscussionId }: PresenterSidebarProps) => (
<nav className="presenter-sidebar" aria-label="Presenter scene index"> <nav className="presenter-sidebar" aria-label="Presenter scene index">
<a className="presenter-sidebar__brand" href="#scene/thesis/title">lda.chat defense</a> <a className="presenter-sidebar__brand" href="#scene/thesis/title">lda.chat defense</a>
<details className="presenter-sidebar__qna" open={activeDiscussionId !== null}>
<summary>Defense Q&amp;A <span>{discussionBranches.length}</span></summary>
<ul>
{discussionBranches.map((branch) => (
<li key={branch.id}>
<a
href={`#discuss/${branch.id}`}
aria-current={activeDiscussionId === branch.id ? "page" : undefined}
>
{"question" in branch && branch.question ? branch.question : branch.title}
</a>
</li>
))}
</ul>
</details>
<ol> <ol>
{mainScenes.map((scene, sceneIndex) => { {mainScenes.map((scene, sceneIndex) => {
const notes = presenterSceneNotes(scene.id); const notes = presenterSceneNotes(scene.id);
@@ -27,6 +27,14 @@
.presenter-sidebar__beats a { width: 1.55rem; height: 1.55rem; display: grid; place-items: center; border: 1px solid #c8c5bf; color: inherit; font: 0.7rem "IBM Plex Mono", monospace; text-decoration: none; } .presenter-sidebar__beats a { width: 1.55rem; height: 1.55rem; display: grid; place-items: center; border: 1px solid #c8c5bf; color: inherit; font: 0.7rem "IBM Plex Mono", monospace; text-decoration: none; }
.presenter-sidebar__beats a[aria-current="page"] { border-color: #1e6b55; background: #1e6b55; color: white; } .presenter-sidebar__beats a[aria-current="page"] { border-color: #1e6b55; background: #1e6b55; color: white; }
.presenter-sidebar__beats a[data-covered="true"]:not([aria-current="page"]) { background: #dce9e2; } .presenter-sidebar__beats a[data-covered="true"]:not([aria-current="page"]) { background: #dce9e2; }
.presenter-sidebar__qna { margin: 0 0 1.15rem; padding: 0.8rem 0; border-block: 1px solid #aaa79f; }
.presenter-sidebar__qna summary { cursor: pointer; font: 700 1rem "Barlow Condensed", sans-serif; text-transform: uppercase; }
.presenter-sidebar__qna summary span { margin-left: 0.3rem; color: #67655f; font: 0.68rem "IBM Plex Mono", monospace; }
.presenter-sidebar__qna ul { display: grid; gap: 0.4rem; margin: 0.7rem 0 0; padding: 0; list-style: none; }
.presenter-sidebar__qna li { display: block; opacity: 1; }
.presenter-sidebar__qna a { display: block; color: #56544f; font-size: 0.82rem; line-height: 1.2; text-decoration: none; }
.presenter-sidebar__qna a:hover, .presenter-sidebar__qna a:focus-visible { color: #155b49; text-decoration: underline; text-underline-offset: 0.15em; }
.presenter-sidebar__qna a[aria-current="page"] { color: #155b49; font-weight: 700; }
.presenter-route__reader { min-width: 0; padding: 1.25rem clamp(1.5rem, 5vw, 6rem) 5rem; } .presenter-route__reader { min-width: 0; padding: 1.25rem clamp(1.5rem, 5vw, 6rem) 5rem; }
.presenter-navigation { max-width: 72ch; min-height: 2.5rem; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 1rem; margin: 0 auto 2rem; border-bottom: 1px solid #d7d5d0; color: #67655f; font: 0.78rem "IBM Plex Mono", monospace; } .presenter-navigation { max-width: 72ch; min-height: 2.5rem; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 1rem; margin: 0 auto 2rem; border-bottom: 1px solid #d7d5d0; color: #67655f; font: 0.78rem "IBM Plex Mono", monospace; }