fix: structure narrow conclusion flow

This commit is contained in:
lda
2026-07-10 23:25:45 +07:00 Verified
parent 93b6edae4e
commit b89befbe89
5 changed files with 92 additions and 47 deletions
+23
View File
@@ -0,0 +1,23 @@
# Narrow Conclusion Flow Fix Report
## Scope
- Refactored the conclusion contribution flow into planner, substrate stack, and runtime semantic grid units.
- Kept the typed substrate and persisted evidence vertically attached within the substrate stack.
- Assigned wide and narrow outer connectors to the planner and substrate-stack units so narrow flow is planner, substrate stack, runtime.
## TDD Record
- RED: `pnpm --dir web\\apps\\console test -- src/presentation/conclusion/ConclusionScene.test.tsx src/presentation/presentation-css.test.ts`
- Result: 2 files failed, 3 tests failed. The component still rendered four direct flow children and CSS still owned outer connectors at individual nodes.
- GREEN: `pnpm --dir web\\apps\\console test -- src/presentation/conclusion/conclusion-model.test.ts src/presentation/conclusion/ConclusionScene.test.tsx src/presentation/presentation-css.test.ts src/presentation/SceneBody.test.tsx; pnpm --dir web\\apps\\console typecheck`
- Result: 4 files passed, 38 tests passed; typecheck passed.
## Verification
- `git diff --check` passed.
- No unrelated files changed.
## Concerns
- None.
@@ -79,14 +79,15 @@ describe("ConclusionScene", () => {
it("attaches evidence vertically beneath the typed substrate rather than extending the contribution line", () => { it("attaches evidence vertically beneath the typed substrate rather than extending the contribution line", () => {
render(<ConclusionScene scene={scene} beat={beat("future")} />); render(<ConclusionScene scene={scene} beat={beat("future")} />);
const executionNodes = [...screen.getByLabelText("contribution flow").children] const flow = screen.getByLabelText("contribution flow");
.filter((node) => node.getAttribute("data-evidence-attachment") !== "vertical"); expect([...flow.children].map((unit) => unit.getAttribute("data-flow-unit"))).toEqual([
expect(executionNodes.map((node) => node.getAttribute("data-node-id"))).toEqual([
"planner", "planner",
"substrate", "substrate-stack",
"runtime", "runtime",
]); ]);
expect(screen.getByText("Persisted, inspectable evidence").closest("[data-node-id='evidence']")) expect(flow.querySelector('[data-flow-unit="substrate-stack"] [data-node-id="substrate"]'))
.toHaveTextContent("Typed workflow substrate");
expect(flow.querySelector('[data-flow-unit="substrate-stack"] [data-node-id="evidence"]'))
.toHaveAttribute("data-evidence-attachment", "vertical"); .toHaveAttribute("data-evidence-attachment", "vertical");
}); });
@@ -35,25 +35,44 @@ export const ConclusionScene: FC<ConclusionSceneProps> = ({ scene, beat }) => {
</StageCaption> </StageCaption>
<section className="conclusion-map" aria-label="thesis contribution boundary" data-conclusion-beat={beatId}> <section className="conclusion-map" aria-label="thesis contribution boundary" data-conclusion-beat={beatId}>
<div className="conclusion-map__flow" aria-label="contribution flow"> <div className="conclusion-map__flow" aria-label="contribution flow">
{contributionNodes.map((node, index) => ( <div className="conclusion-map__flow-unit conclusion-map__flow-unit--planner" data-flow-unit="planner">
<div <div
className={`conclusion-map__node conclusion-map__node--${node.id}`} className="conclusion-map__node conclusion-map__node--planner"
data-node-id={node.id} data-node-id={contributionNodes[0].id}
data-emphasis={node.id === "substrate" ? "substrate" : "neutral"} data-emphasis="neutral"
key={node.id}
> >
<span className="conclusion-map__node-index">0{index + 1}</span> <span className="conclusion-map__node-index">01</span>
<strong>{node.label}</strong> <strong>{contributionNodes[0].label}</strong>
</div>
</div>
<div className="conclusion-map__flow-unit conclusion-map__flow-unit--substrate-stack" data-flow-unit="substrate-stack">
<div
className="conclusion-map__node conclusion-map__node--substrate"
data-node-id={contributionNodes[1].id}
data-emphasis="substrate"
>
<span className="conclusion-map__node-index">02</span>
<strong>{contributionNodes[1].label}</strong>
</div>
<div
className={`conclusion-map__node conclusion-map__node--${evidenceNode.id}`}
data-evidence-attachment="vertical"
data-node-id={evidenceNode.id}
>
<span className="conclusion-map__node-index">04</span>
<strong>{evidenceNode.label}</strong>
<small>saved traces and receipts</small>
</div>
</div>
<div className="conclusion-map__flow-unit conclusion-map__flow-unit--runtime" data-flow-unit="runtime">
<div
className="conclusion-map__node conclusion-map__node--runtime"
data-node-id={contributionNodes[2].id}
data-emphasis="neutral"
>
<span className="conclusion-map__node-index">03</span>
<strong>{contributionNodes[2].label}</strong>
</div> </div>
))}
<div
className={`conclusion-map__node conclusion-map__node--${evidenceNode.id}`}
data-evidence-attachment="vertical"
data-node-id={evidenceNode.id}
>
<span className="conclusion-map__node-index">04</span>
<strong>{evidenceNode.label}</strong>
<small>saved traces and receipts</small>
</div> </div>
</div> </div>
@@ -31,12 +31,10 @@ describe("presentation.css", () => {
expect(boardBlock).toContain("flex-shrink: 0"); expect(boardBlock).toContain("flex-shrink: 0");
}); });
it("keeps evidence vertically attached beneath substrate from wide desktop through the 1080px breakpoint", () => { it("keeps evidence inside a substrate stack from wide desktop through the 1080px breakpoint", () => {
expect(css).toMatch(/\.conclusion-map__flow\s*\{\s*display: grid;\s*grid-template-columns: repeat\(3, minmax\(0, 1fr\)\);\s*grid-template-rows: auto auto;/); expect(css).toMatch(/\.conclusion-map__flow\s*\{\s*display: grid;\s*grid-template-columns: repeat\(3, minmax\(0, 1fr\)\);/);
expect(css).toMatch(/\.conclusion-map__node--planner\s*\{\s*grid-column: 1;\s*grid-row: 1;/); expect(css).toMatch(/\.conclusion-map__flow-unit--substrate-stack\s*\{[\s\S]*?grid-template-rows: auto auto;/);
expect(css).toMatch(/\.conclusion-map__node--substrate\s*\{\s*grid-column: 2;\s*grid-row: 1;/); expect(css).toMatch(/\.conclusion-map__flow-unit--planner::after,\s*\.conclusion-map__flow-unit--substrate-stack::after/);
expect(css).toMatch(/\.conclusion-map__node--runtime\s*\{\s*grid-column: 3;\s*grid-row: 1;/);
expect(css).toMatch(/\.conclusion-map__node--evidence\s*\{\s*grid-column: 2;\s*grid-row: 2;/);
expect(css).not.toMatch(/\.conclusion-map__node--runtime::after/); expect(css).not.toMatch(/\.conclusion-map__node--runtime::after/);
expect(css).toMatch(/\.conclusion-map__node--evidence::before\s*\{[\s\S]*?content: "↓";/); expect(css).toMatch(/\.conclusion-map__node--evidence::before\s*\{[\s\S]*?content: "↓";/);
}); });
@@ -52,10 +50,11 @@ describe("presentation.css", () => {
expect(css).not.toMatch(/\.conclusion-map__future svg\s*\{[^}]*color: var\(--accent-cyan\);/s); expect(css).not.toMatch(/\.conclusion-map__future svg\s*\{[^}]*color: var\(--accent-cyan\);/s);
}); });
it("resets conclusion node placement and keeps connectors node-specific on narrow screens", () => { it("stacks the same three flow units on narrow screens while keeping the evidence connector internal", () => {
expect(css).toMatch( expect(css).toMatch(
/@media \(max-width: 640px\) \{[\s\S]*?\.conclusion-map__node--planner,\s*\.conclusion-map__node--substrate,\s*\.conclusion-map__node--runtime,\s*\.conclusion-map__node--evidence\s*\{\s*grid-column: auto;\s*grid-row: auto;/, /@media \(max-width: 640px\) \{[\s\S]*?\.conclusion-map__flow\s*\{\s*grid-template-columns: 1fr;[\s\S]*?\.conclusion-map__flow-unit--planner::after,\s*\.conclusion-map__flow-unit--substrate-stack::after\s*\{[\s\S]*?content: "↓";/,
); );
expect(css).toMatch(/@media \(max-width: 640px\) \{[\s\S]*?\.conclusion-map__node--evidence::before\s*\{[\s\S]*?content: "↓";/);
expect(css).not.toContain(".conclusion-map__node:not(:last-child)::after"); expect(css).not.toContain(".conclusion-map__node:not(:last-child)::after");
}); });
}); });
@@ -276,11 +276,22 @@
.conclusion-map__flow { .conclusion-map__flow {
display: grid; display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-rows: auto auto;
gap: 0.8rem 1.7rem; gap: 0.8rem 1.7rem;
align-items: center; align-items: center;
} }
.conclusion-map__flow-unit {
position: relative;
min-width: 0;
}
/* Evidence stays inside the substrate unit at every breakpoint. */
.conclusion-map__flow-unit--substrate-stack {
display: grid;
grid-template-rows: auto auto;
gap: 0.8rem;
}
.conclusion-map__node { .conclusion-map__node {
position: relative; position: relative;
display: grid; display: grid;
@@ -292,10 +303,11 @@
background: oklch(0.19 0.03 250 / 88%); background: oklch(0.19 0.03 250 / 88%);
} }
.conclusion-map__node--planner::after, .conclusion-map__flow-unit--planner::after,
.conclusion-map__node--substrate::after { .conclusion-map__flow-unit--substrate-stack::after {
position: absolute; position: absolute;
top: 50%; /* Align wide connectors with the substrate node, not the full two-node stack. */
top: 2.75rem;
right: -0.85rem; right: -0.85rem;
z-index: 1; z-index: 1;
width: 1.7rem; width: 1.7rem;
@@ -321,11 +333,6 @@
line-height: 0; line-height: 0;
} }
.conclusion-map__node--planner { grid-column: 1; grid-row: 1; }
.conclusion-map__node--substrate { grid-column: 2; grid-row: 1; }
.conclusion-map__node--runtime { grid-column: 3; grid-row: 1; }
.conclusion-map__node--evidence { grid-column: 2; grid-row: 2; }
.conclusion-map__node[data-emphasis="substrate"] { .conclusion-map__node[data-emphasis="substrate"] {
border-color: var(--accent-cyan); border-color: var(--accent-cyan);
box-shadow: inset 0 0 0 1px oklch(0.72 0.17 195 / 20%); box-shadow: inset 0 0 0 1px oklch(0.72 0.17 195 / 20%);
@@ -409,16 +416,9 @@
@media (max-width: 640px) { @media (max-width: 640px) {
.conclusion-map__flow { grid-template-columns: 1fr; gap: 0.85rem; } .conclusion-map__flow { grid-template-columns: 1fr; gap: 0.85rem; }
.conclusion-map__node--planner,
.conclusion-map__node--substrate,
.conclusion-map__node--runtime,
.conclusion-map__node--evidence {
grid-column: auto;
grid-row: auto;
}
.conclusion-map__node--planner::after, .conclusion-map__flow-unit--planner::after,
.conclusion-map__node--substrate::after { .conclusion-map__flow-unit--substrate-stack::after {
top: auto; top: auto;
right: 50%; right: 50%;
bottom: -0.85rem; bottom: -0.85rem;
@@ -426,6 +426,9 @@
border-top: 0; border-top: 0;
content: "↓"; content: "↓";
} }
/* Reassert the internal attachment without turning evidence into a flow unit. */
.conclusion-map__node--evidence::before { content: "↓"; }
.conclusion-map__non-claims, .conclusion-map__non-claims,
.conclusion-map__future { grid-template-columns: 1fr; } .conclusion-map__future { grid-template-columns: 1fr; }
} }