fix: align prepared lifecycle discussion rail
This commit is contained in:
@@ -20,3 +20,12 @@ Implemented in shared `main`; ready for commit as `refactor: remove duplicate au
|
|||||||
- `PresentationStage.test.tsx` named in the brief is not present in this checkout, so the focused run used the existing PresentationStage implementation coverage through route tests.
|
- `PresentationStage.test.tsx` named in the brief is not present in this checkout, so the focused run used the existing PresentationStage implementation coverage through route tests.
|
||||||
|
|
||||||
The broader presenter-note catalog test remains outside Task 4 scope and still expects the later Task 5 `architecture/node-use` note and timing ledger.
|
The broader presenter-note catalog test remains outside Task 4 scope and still expects the later Task 5 `architecture/node-use` note and timing ledger.
|
||||||
|
|
||||||
|
## Task 4 Follow-Up: Prepared Lifecycle Composition
|
||||||
|
|
||||||
|
- RED: focused seam tests failed because the discussion rail was a sibling below the full prepared scene, with no presentation-column wrapper or grid-area contract.
|
||||||
|
- GREEN: the existing discussion rail is now passed into Prepared Lifecycle as a slot; the assistant spans the left column, presentation occupies the upper right, and questions occupy the lower right.
|
||||||
|
- Mobile order is explicit: presentation, questions, assistant.
|
||||||
|
- Focused tests passed: `4` files, `162/162` tests.
|
||||||
|
- Console typecheck passed: `pnpm --dir web --filter @lda/console typecheck`.
|
||||||
|
- React Doctor passed: `100/100`, no issues in changed files.
|
||||||
|
|||||||
@@ -376,6 +376,10 @@ describe("SceneBody", () => {
|
|||||||
expect(within(rail).getByRole("button", { name: /Raw plan import/i })).toBeInTheDocument();
|
expect(within(rail).getByRole("button", { name: /Raw plan import/i })).toBeInTheDocument();
|
||||||
expect(within(rail).getByRole("button", { name: /Validation and diagnostics/i })).toBeInTheDocument();
|
expect(within(rail).getByRole("button", { name: /Validation and diagnostics/i })).toBeInTheDocument();
|
||||||
expect(within(rail).getByRole("button", { name: /Why schemas matter/i })).toBeInTheDocument();
|
expect(within(rail).getByRole("button", { name: /Why schemas matter/i })).toBeInTheDocument();
|
||||||
|
expect(rail.closest(".prepared-lifecycle-scene__discussion")).toHaveAttribute(
|
||||||
|
"data-discussion-placement",
|
||||||
|
"presentation-column",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders discussion branches as a labelled presenter rail", () => {
|
it("renders discussion branches as a labelled presenter rail", () => {
|
||||||
|
|||||||
@@ -326,7 +326,14 @@ export const SceneBody = ({ location, demo, selectedNodeId, selectNode, openEvid
|
|||||||
case "agent":
|
case "agent":
|
||||||
return <AgentHandoffScene scene={scene} beat={beat} />;
|
return <AgentHandoffScene scene={scene} beat={beat} />;
|
||||||
case "demo-lifecycle":
|
case "demo-lifecycle":
|
||||||
return <PreparedAuthoringLifecycleScene scene={scene} beat={beat} onAdvance={onPreparedLifecycleAdvance} />;
|
return (
|
||||||
|
<PreparedAuthoringLifecycleScene
|
||||||
|
scene={scene}
|
||||||
|
beat={beat}
|
||||||
|
onAdvance={onPreparedLifecycleAdvance}
|
||||||
|
discussionRail={scene.id === "prepared-lifecycle" ? discussionLinks : undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case "demo":
|
case "demo":
|
||||||
return (
|
return (
|
||||||
<DemoWorkflowScene
|
<DemoWorkflowScene
|
||||||
@@ -353,7 +360,7 @@ export const SceneBody = ({ location, demo, selectedNodeId, selectNode, openEvid
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{content}
|
{content}
|
||||||
{discussionLinks}
|
{scene.id === "prepared-lifecycle" ? null : discussionLinks}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+27
-2
@@ -1,16 +1,24 @@
|
|||||||
import { cleanup, render, screen, within } from "@testing-library/react";
|
import { cleanup, render, screen, within } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
import { findBeat, findScene } from "../storyboard.js";
|
import { findBeat, findScene } from "../storyboard.js";
|
||||||
import { PreparedAuthoringLifecycleScene } from "./PreparedAuthoringLifecycleScene.js";
|
import { PreparedAuthoringLifecycleScene } from "./PreparedAuthoringLifecycleScene.js";
|
||||||
|
|
||||||
afterEach(() => cleanup());
|
afterEach(() => cleanup());
|
||||||
|
|
||||||
const renderBeat = (beatId: string, onAdvance?: () => void) => {
|
const renderBeat = (beatId: string, onAdvance?: () => void, discussionRail?: ReactNode) => {
|
||||||
const scene = findScene("prepared-lifecycle");
|
const scene = findScene("prepared-lifecycle");
|
||||||
const beat = findBeat("prepared-lifecycle", beatId);
|
const beat = findBeat("prepared-lifecycle", beatId);
|
||||||
if (!scene || !beat) throw new Error(`missing prepared-lifecycle/${beatId}`);
|
if (!scene || !beat) throw new Error(`missing prepared-lifecycle/${beatId}`);
|
||||||
return render(<PreparedAuthoringLifecycleScene scene={scene} beat={beat} onAdvance={onAdvance} />);
|
return render(
|
||||||
|
<PreparedAuthoringLifecycleScene
|
||||||
|
scene={scene}
|
||||||
|
beat={beat}
|
||||||
|
onAdvance={onAdvance}
|
||||||
|
discussionRail={discussionRail}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("PreparedAuthoringLifecycleScene", () => {
|
describe("PreparedAuthoringLifecycleScene", () => {
|
||||||
@@ -206,6 +214,23 @@ describe("PreparedAuthoringLifecycleScene", () => {
|
|||||||
expect(within(rail).getByText("Sources, capabilities, schemas")).toBeInTheDocument();
|
expect(within(rail).getByText("Sources, capabilities, schemas")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("places defense questions in a presentation-only grid row", () => {
|
||||||
|
renderBeat(
|
||||||
|
"diagnose",
|
||||||
|
undefined,
|
||||||
|
<aside aria-label="defense discussion topics">Defense questions</aside>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const workspace = screen.getByRole("region", { name: "prepared workflow authoring lifecycle" });
|
||||||
|
const discussion = workspace.querySelector(".prepared-lifecycle-scene__discussion");
|
||||||
|
|
||||||
|
expect(discussion?.tagName).toBe("SECTION");
|
||||||
|
expect(discussion).toHaveAttribute("data-discussion-placement", "presentation-column");
|
||||||
|
expect(discussion?.parentElement).toBe(workspace);
|
||||||
|
expect(discussion).toContainElement(screen.getByLabelText("defense discussion topics"));
|
||||||
|
expect(workspace.querySelector(".presentation-assistant-pane")?.parentElement).toBe(workspace);
|
||||||
|
});
|
||||||
|
|
||||||
it("highlights the active phase in the rail", () => {
|
it("highlights the active phase in the rail", () => {
|
||||||
renderBeat("deployment");
|
renderBeat("deployment");
|
||||||
const rail = screen.getByRole("list", { name: /prepared authoring lifecycle/i });
|
const rail = screen.getByRole("list", { name: /prepared authoring lifecycle/i });
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useReducer } from "react";
|
import { useReducer } from "react";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
import {
|
import {
|
||||||
projectPreparedLifecycleStep,
|
projectPreparedLifecycleStep,
|
||||||
type PreparedLifecycleStepId,
|
type PreparedLifecycleStepId,
|
||||||
@@ -17,6 +18,7 @@ type PreparedAuthoringLifecycleSceneProps = {
|
|||||||
readonly scene: SceneDefinition;
|
readonly scene: SceneDefinition;
|
||||||
readonly beat: SceneBeatDefinition;
|
readonly beat: SceneBeatDefinition;
|
||||||
readonly onAdvance?: (() => void) | undefined;
|
readonly onAdvance?: (() => void) | undefined;
|
||||||
|
readonly discussionRail?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const steps = [
|
const steps = [
|
||||||
@@ -38,7 +40,7 @@ const steps = [
|
|||||||
* Each beat shows a persistent prepared assistant beside one dominant phase
|
* Each beat shows a persistent prepared assistant beside one dominant phase
|
||||||
* projection sourced from the prepared authoring recording.
|
* projection sourced from the prepared authoring recording.
|
||||||
*/
|
*/
|
||||||
export const PreparedAuthoringLifecycleScene = ({ scene, beat, onAdvance }: PreparedAuthoringLifecycleSceneProps) => {
|
export const PreparedAuthoringLifecycleScene = ({ scene, beat, onAdvance, discussionRail }: PreparedAuthoringLifecycleSceneProps) => {
|
||||||
const [messageState, dispatch] = useReducer(
|
const [messageState, dispatch] = useReducer(
|
||||||
preparedLifecycleMessageReducer,
|
preparedLifecycleMessageReducer,
|
||||||
initialPreparedLifecycleMessageState,
|
initialPreparedLifecycleMessageState,
|
||||||
@@ -128,6 +130,15 @@ export const PreparedAuthoringLifecycleScene = ({ scene, beat, onAdvance }: Prep
|
|||||||
<AuthoringPhaseVisual projection={projection} focus={projection.focus} />
|
<AuthoringPhaseVisual projection={projection} focus={projection.focus} />
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
{discussionRail && (
|
||||||
|
<section
|
||||||
|
className="prepared-lifecycle-scene__discussion"
|
||||||
|
data-discussion-placement="presentation-column"
|
||||||
|
aria-label="prepared lifecycle defense questions"
|
||||||
|
>
|
||||||
|
{discussionRail}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ describe("presentation.css", () => {
|
|||||||
expect(boardBlock).toContain("flex-shrink: 0");
|
expect(boardBlock).toContain("flex-shrink: 0");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps the prepared lifecycle as a bounded 26/74 split without a lower dock row", () => {
|
it("keeps the prepared lifecycle as a bounded 26/74 split with an explicit discussion row", () => {
|
||||||
const sceneBlock = css.match(
|
const sceneBlock = css.match(
|
||||||
/^\.prepared-lifecycle-scene\s*\{(?<body>[\s\S]*?)\n\}/m,
|
/^\.prepared-lifecycle-scene\s*\{(?<body>[\s\S]*?)\n\}/m,
|
||||||
)?.groups?.body;
|
)?.groups?.body;
|
||||||
@@ -93,11 +93,29 @@ describe("presentation.css", () => {
|
|||||||
expect(sceneBlock).toMatch(/minmax\(12rem, 0\.26fr\).*minmax\(0, 0\.74fr\)/);
|
expect(sceneBlock).toMatch(/minmax\(12rem, 0\.26fr\).*minmax\(0, 0\.74fr\)/);
|
||||||
expect(sceneBlock).toContain("min-height: 0");
|
expect(sceneBlock).toContain("min-height: 0");
|
||||||
expect(sceneBlock).toContain("overflow: hidden");
|
expect(sceneBlock).toContain("overflow: hidden");
|
||||||
expect(sceneBlock).not.toContain("grid-template-rows:");
|
expect(sceneBlock).toContain("grid-template-rows: minmax(0, 1fr) auto;");
|
||||||
|
expect(sceneBlock).toContain('grid-template-areas: "assistant presentation" "assistant discussion";');
|
||||||
expect(css).not.toContain(".prepared-lifecycle-scene__dock");
|
expect(css).not.toContain(".prepared-lifecycle-scene__dock");
|
||||||
expect(css).not.toMatch(/prepared-lifecycle-scene__dock[\s\S]*position:\s*(absolute|fixed)/);
|
expect(css).not.toMatch(/prepared-lifecycle-scene__dock[\s\S]*position:\s*(absolute|fixed)/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps the discussion row in the presentation column and stacks it before chat on mobile", () => {
|
||||||
|
const discussion = cssBlocks(css, ".prepared-lifecycle-scene__discussion")
|
||||||
|
.find((body) => body.includes("grid-area: discussion;"));
|
||||||
|
const assistant = cssBlocks(css, ".prepared-lifecycle-scene > .presentation-assistant-pane")
|
||||||
|
.find((body) => body.includes("grid-area: assistant;"));
|
||||||
|
const presentation = cssBlocks(css, ".prepared-lifecycle-scene__presentation")
|
||||||
|
.find((body) => body.includes("grid-area: presentation;"));
|
||||||
|
const narrowContainer = cssBlock(css, "@container presentation-canvas (max-width: 600px)") ?? "";
|
||||||
|
const narrowScene = cssBlocks(narrowContainer, ".prepared-lifecycle-scene")
|
||||||
|
.find((body) => body.includes('grid-template-areas: "presentation" "discussion" "assistant";'));
|
||||||
|
|
||||||
|
expect(discussion).toContain("grid-area: discussion;");
|
||||||
|
expect(assistant).toContain("grid-area: assistant;");
|
||||||
|
expect(presentation).toContain("grid-area: presentation;");
|
||||||
|
expect(narrowScene).toContain('grid-template-areas: "presentation" "discussion" "assistant";');
|
||||||
|
});
|
||||||
|
|
||||||
it("asserts the winning editorial 26/74 split", () => {
|
it("asserts the winning editorial 26/74 split", () => {
|
||||||
const editorialScene = cssBlocks(
|
const editorialScene = cssBlocks(
|
||||||
css,
|
css,
|
||||||
@@ -197,8 +215,8 @@ describe("presentation.css", () => {
|
|||||||
expect(compactRepair).toContain("grid-template-columns: minmax(0, 1fr);");
|
expect(compactRepair).toContain("grid-template-columns: minmax(0, 1fr);");
|
||||||
expect(compactRepair).toContain("min-width: 0;");
|
expect(compactRepair).toContain("min-width: 0;");
|
||||||
expect(narrowScene).toContain("grid-template-columns: minmax(0, 1fr);");
|
expect(narrowScene).toContain("grid-template-columns: minmax(0, 1fr);");
|
||||||
expect(narrowScene).toContain('grid-template-areas: "presentation" "assistant";');
|
expect(narrowScene).toContain('grid-template-areas: "presentation" "discussion" "assistant";');
|
||||||
expect(narrowScene).toContain("grid-template-rows: max-content max-content;");
|
expect(narrowScene).toContain("grid-template-rows: max-content max-content max-content;");
|
||||||
expect(narrowScene).toContain("align-content: start;");
|
expect(narrowScene).toContain("align-content: start;");
|
||||||
expect(narrowScene).toContain("min-height: max-content;");
|
expect(narrowScene).toContain("min-height: max-content;");
|
||||||
expect(narrowScene).toContain("height: max-content;");
|
expect(narrowScene).toContain("height: max-content;");
|
||||||
@@ -206,6 +224,8 @@ describe("presentation.css", () => {
|
|||||||
expect(narrowPresentation).toContain("grid-template-rows: auto max-content;");
|
expect(narrowPresentation).toContain("grid-template-rows: auto max-content;");
|
||||||
expect(narrowPresentation).toContain("overflow: visible;");
|
expect(narrowPresentation).toContain("overflow: visible;");
|
||||||
expect(narrowPresentation).not.toContain("overflow: auto;");
|
expect(narrowPresentation).not.toContain("overflow: auto;");
|
||||||
|
expect(cssBlocks(narrowContainer, '.prepared-lifecycle-scene[data-presentation-surface="editorial"] > .prepared-lifecycle-scene__discussion')
|
||||||
|
.some((body) => body.includes("align-self: start;"))).toBe(true);
|
||||||
expect(narrowFrame).toContain("height: max-content;");
|
expect(narrowFrame).toContain("height: max-content;");
|
||||||
expect(narrowFrame).toContain("overflow: visible;");
|
expect(narrowFrame).toContain("overflow: visible;");
|
||||||
expect(narrowFrame).not.toContain("overflow: auto;");
|
expect(narrowFrame).not.toContain("overflow: auto;");
|
||||||
|
|||||||
@@ -3454,6 +3454,8 @@
|
|||||||
.prepared-lifecycle-scene {
|
.prepared-lifecycle-scene {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(12rem, 0.26fr) minmax(0, 0.74fr);
|
grid-template-columns: minmax(12rem, 0.26fr) minmax(0, 0.74fr);
|
||||||
|
grid-template-rows: minmax(0, 1fr) auto;
|
||||||
|
grid-template-areas: "assistant presentation" "assistant discussion";
|
||||||
gap: 0.55rem;
|
gap: 0.55rem;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
@@ -3462,6 +3464,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.prepared-lifecycle-scene__presentation {
|
.prepared-lifecycle-scene__presentation {
|
||||||
|
grid-area: presentation;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto minmax(0, 1fr);
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -3469,6 +3472,21 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.prepared-lifecycle-scene > .presentation-assistant-pane {
|
||||||
|
grid-area: assistant;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prepared-lifecycle-scene__discussion {
|
||||||
|
grid-area: discussion;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prepared-lifecycle-scene__discussion > .scene-body__discussion-links {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
.prepared-lifecycle-scene__rail {
|
.prepared-lifecycle-scene__rail {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||||
@@ -3482,11 +3500,6 @@
|
|||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.presentation-stage__primary > .prepared-lifecycle-scene + .scene-body__discussion-links {
|
|
||||||
margin-top: 0;
|
|
||||||
padding-top: 0.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prepared-lifecycle-scene__rail::-webkit-scrollbar {
|
.prepared-lifecycle-scene__rail::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -4009,8 +4022,8 @@
|
|||||||
@container presentation-canvas (max-width: 600px) {
|
@container presentation-canvas (max-width: 600px) {
|
||||||
.prepared-lifecycle-scene[data-presentation-surface="editorial"] {
|
.prepared-lifecycle-scene[data-presentation-surface="editorial"] {
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
grid-template-rows: max-content max-content;
|
grid-template-rows: max-content max-content max-content;
|
||||||
grid-template-areas: "presentation" "assistant";
|
grid-template-areas: "presentation" "discussion" "assistant";
|
||||||
align-content: start;
|
align-content: start;
|
||||||
min-height: max-content;
|
min-height: max-content;
|
||||||
height: max-content;
|
height: max-content;
|
||||||
@@ -4027,6 +4040,12 @@
|
|||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.prepared-lifecycle-scene[data-presentation-surface="editorial"] > .prepared-lifecycle-scene__discussion {
|
||||||
|
grid-area: discussion;
|
||||||
|
align-self: start;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.prepared-lifecycle-scene[data-presentation-surface="editorial"] .prepared-lifecycle-scene__frame {
|
.prepared-lifecycle-scene[data-presentation-surface="editorial"] .prepared-lifecycle-scene__frame {
|
||||||
grid-template-rows: auto max-content;
|
grid-template-rows: auto max-content;
|
||||||
align-self: start;
|
align-self: start;
|
||||||
|
|||||||
Reference in New Issue
Block a user