From 13f37093cd726c9494f95768647321da92e3122a Mon Sep 17 00:00:00 2001 From: Pouyan Jahangiri Date: Sat, 1 Aug 2026 01:36:02 -0700 Subject: [PATCH] feat(composer): drop the build history so the composer sits centred The recent-builds table is gone from the composer, and with it the reason the column was not centring. `safe center` was working correctly all along: the table made the content taller than the viewport, so it fell back to top alignment exactly as designed. Remove the table and the composer is short enough to sit in the middle of the page. RecentBuildsTable, its stylesheet and its test are deleted rather than left unused. The router wrapper its links needed went with it, since it was the only thing in the composer that navigated. Worth stating plainly: this was the ONLY surface in the app where a cancelled or failed build was visible. Home lists running builds and the library lists finished courses, so a build that was cancelled now shows up nowhere. That is a deliberate trade for the cleaner entry surface, not an oversight, and it is the thing to revisit if failed builds ever need somewhere to live. The `runs` prop stays, read only to tell a first-time visitor from a returning one so the explainer still shows once and only once. 1461 tests green. Typecheck, lint, build clean. --- .../configurator/IdleCourseSetup.test.tsx | 21 ++--- .../configurator/IdleCourseSetup.tsx | 5 +- .../configurator/RecentBuildsTable.module.css | 79 ------------------- .../configurator/RecentBuildsTable.test.tsx | 53 ------------- .../configurator/RecentBuildsTable.tsx | 75 ------------------ 5 files changed, 7 insertions(+), 226 deletions(-) delete mode 100644 apps/web/src/components/configurator/RecentBuildsTable.module.css delete mode 100644 apps/web/src/components/configurator/RecentBuildsTable.test.tsx delete mode 100644 apps/web/src/components/configurator/RecentBuildsTable.tsx diff --git a/apps/web/src/components/configurator/IdleCourseSetup.test.tsx b/apps/web/src/components/configurator/IdleCourseSetup.test.tsx index 9f80739d..be7d6fd2 100644 --- a/apps/web/src/components/configurator/IdleCourseSetup.test.tsx +++ b/apps/web/src/components/configurator/IdleCourseSetup.test.tsx @@ -1,5 +1,4 @@ import { fireEvent, render, screen, within } from "@testing-library/react"; -import { MemoryRouter } from "react-router"; import { afterEach, describe, expect, it, vi } from "vitest"; import { makeBriefResponse, makeRun } from "../../test/fixtures"; @@ -44,16 +43,7 @@ function renderSetup(overrides: Partial, so the - // harness supplies router context rather than the component avoiding it. - return { - props, - ...render( - - - , - ), - }; + return { props, ...render() }; } afterEach(() => vi.unstubAllGlobals()); @@ -149,15 +139,15 @@ describe("IdleCourseSetup", () => { ).toBeInTheDocument(); }); - it("drops the explainer once there are builds, and shows them instead", () => { - // Someone with completed courses does not need to be told what a build does; that space is - // better spent on the thing they came back for. + it("drops the explainer once there are builds", () => { + // Someone with completed courses does not need to be told what a build does. The composer is + // then just the composer: nothing below it, so it can sit centred on the page. renderSetup({ runsLoaded: true, runs: [makeRun()] }); expect( screen.queryByRole("list", { name: /what a lunaris studio build does/i }), ).not.toBeInTheDocument(); - expect(screen.getByRole("heading", { name: /recent builds/i })).toBeInTheDocument(); + expect(screen.getByLabelText("Topic")).toBeInTheDocument(); }); it("shows neither while the build history is still loading", () => { @@ -168,7 +158,6 @@ describe("IdleCourseSetup", () => { expect( screen.queryByRole("list", { name: /what a lunaris studio build does/i }), ).not.toBeInTheDocument(); - expect(screen.queryByRole("heading", { name: /recent builds/i })).not.toBeInTheDocument(); }); it("does not offer to personalize before there is a topic to read", () => { diff --git a/apps/web/src/components/configurator/IdleCourseSetup.tsx b/apps/web/src/components/configurator/IdleCourseSetup.tsx index 7705a256..70ab5ee2 100644 --- a/apps/web/src/components/configurator/IdleCourseSetup.tsx +++ b/apps/web/src/components/configurator/IdleCourseSetup.tsx @@ -14,7 +14,6 @@ import { LiveToggle } from "../gateway/LiveToggle"; import { PersonalizeMenu } from "./PersonalizeMenu"; import { ComposerFeatures } from "./ComposerFeatures"; import { ComposerOptions } from "./ComposerOptions"; -import { RecentBuildsTable } from "./RecentBuildsTable"; import styles from "./IdleCourseSetup.module.css"; interface IdleCourseSetupProps { @@ -27,7 +26,8 @@ interface IdleCourseSetupProps { onStartLive?: ((topic: string) => void) | undefined; /** Open the operator/admin Settings panel (the rail only points there). */ onOpenSettings: () => void; - /** The run history (from the shell's useRuns), which drives the recent-builds table. */ + /** The run history (from the shell's useRuns). Read only to tell a first-time visitor from a + * returning one; the composer no longer lists builds. */ runs?: CourseRun[]; /** Whether that history has actually loaded. The parent collapses an unloaded history to an empty * array, so without this the first-run explainer would flash up and vanish for every returning @@ -169,7 +169,6 @@ export function IdleCourseSetup({ {/* What a build does is first-run material: someone with completed courses does not need to be told, and the space is better spent on what they came back for. */} {runsLoaded && runs.length === 0 && } - ); } diff --git a/apps/web/src/components/configurator/RecentBuildsTable.module.css b/apps/web/src/components/configurator/RecentBuildsTable.module.css deleted file mode 100644 index 619472f6..00000000 --- a/apps/web/src/components/configurator/RecentBuildsTable.module.css +++ /dev/null @@ -1,79 +0,0 @@ -/* Composer recent-builds table: a compact, edge-to-edge, hairline-ruled table (not floating cards). */ -.section { - margin-top: var(--space-10); - width: 100%; - max-width: 720px; -} - -.heading { - margin: 0 0 var(--space-3); -} - -.tableWrap { - overflow-x: auto; - border: var(--border-width) solid var(--border); - border-radius: var(--radius-lg); -} - -.table { - width: 100%; - border-collapse: collapse; - font-size: var(--text-sm); -} - -.table thead th { - position: sticky; - top: 0; - text-align: left; - padding: var(--space-2) var(--space-3); - font-family: var(--font-mono); - font-size: var(--text-xs); - font-weight: 500; - color: var(--text-muted); - background: var(--bg-subtle); - border-bottom: var(--border-width) solid var(--border); - white-space: nowrap; -} - -.table tbody td { - padding: var(--space-3); - border-top: var(--border-width) solid var(--border); - vertical-align: middle; -} - -.table tbody tr:first-child td { - border-top: none; -} - -.row:hover { - background: var(--bg-subtle); -} - -.topic { - color: var(--text); - font-weight: 500; - text-decoration: none; - border-radius: var(--radius-sm); -} - -.topic:hover { - text-decoration: underline; -} - -.topic:focus-visible { - outline: none; - box-shadow: 0 0 0 2px var(--focus-ring); -} - -.structureCol, -.whenCol { - color: var(--text-muted); - white-space: nowrap; -} - -@media (max-width: 560px) { - /* Drop the structure column on the narrowest screens; status + topic + when still tell the story. */ - .structureCol { - display: none; - } -} diff --git a/apps/web/src/components/configurator/RecentBuildsTable.test.tsx b/apps/web/src/components/configurator/RecentBuildsTable.test.tsx deleted file mode 100644 index 19eddf8d..00000000 --- a/apps/web/src/components/configurator/RecentBuildsTable.test.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { render, screen, within } from "@testing-library/react"; -import { MemoryRouter } from "react-router"; -import { describe, expect, it } from "vitest"; - -import { RecentBuildsTable } from "./RecentBuildsTable"; -import { makeRun } from "../../test/fixtures"; - -function renderTable(runs = [makeRun()]) { - render( - - - , - ); -} - -describe("RecentBuildsTable", () => { - it("renders nothing when there are no builds yet", () => { - const { container } = render( - - - , - ); - expect(container).toBeEmptyDOMElement(); - }); - - it("lists each build with a linked topic, structure, status, and time", () => { - renderTable([makeRun({ id: "c-1", topic: "How HTTPS works", kcCount: 15, moduleCount: 4 })]); - - const row = screen.getByRole("row", { name: /how https works/i }); - // The topic is a real link into the course/build canvas (Cmd/middle-click works). - const link = within(row).getByRole("link", { name: "How HTTPS works" }); - expect(link).toHaveAttribute("href", "/courses/c-1"); - // Structure reads from the run summary (KCs · modules) as mono data. - expect(within(row).getByText("15 KCs · 4 modules")).toBeInTheDocument(); - // The house status convention: a dot + uppercase-mono label. - expect(within(row).getByText("COMPLETED")).toBeInTheDocument(); - }); - - it("caps the table at the six most-recent builds", () => { - const runs = Array.from({ length: 8 }, (_, i) => - makeRun({ id: `c-${i}`, runId: `r-${i}`, topic: `Course ${i}` }), - ); - renderTable(runs); - - // Header row + 6 build rows. - expect(screen.getAllByRole("row")).toHaveLength(7); - }); - - it("singularises a one-KC, one-module build", () => { - renderTable([makeRun({ topic: "Tiny", kcCount: 1, moduleCount: 1 })]); - expect(screen.getByText("1 KC · 1 module")).toBeInTheDocument(); - }); -}); diff --git a/apps/web/src/components/configurator/RecentBuildsTable.tsx b/apps/web/src/components/configurator/RecentBuildsTable.tsx deleted file mode 100644 index b410949e..00000000 --- a/apps/web/src/components/configurator/RecentBuildsTable.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { Link } from "react-router"; - -import { StatusDot } from "../primitives/StatusDot"; -import { relativeTime } from "../../lib/relativeTime"; -import { coursePath } from "../../lib/routes"; -import { RUN_STATUS_TONE } from "../../lib/runStatus"; -import type { CourseRun } from "../../types/course"; -import styles from "./RecentBuildsTable.module.css"; - -/** How many recent builds the composer table shows before deferring to the full history. */ -const RECENT_LIMIT = 6; - -interface RecentBuildsTableProps { - /** The run history (newest first, from the shell's useRuns). Empty → the table is hidden. */ - runs: CourseRun[]; -} - -function structureLabel(run: CourseRun): string { - const kcs = `${run.kcCount} ${run.kcCount === 1 ? "KC" : "KCs"}`; - const modules = `${run.moduleCount} ${run.moduleCount === 1 ? "module" : "modules"}`; - return `${kcs} · ${modules}`; -} - -/** The composer's recent-builds table (P5): the last few builds as a compact, edge-to-edge table — - * status dot, topic (a real link into the course/build canvas), structure, and when. Sits under - * the composer alongside the sidebar history (which stays). Renders nothing with no builds yet. */ -export function RecentBuildsTable({ runs }: RecentBuildsTableProps) { - const recent = runs.slice(0, RECENT_LIMIT); - if (recent.length === 0) return null; - - return ( -
-

- Recent builds -

-
- - - - - - - - - - - {recent.map((run) => { - const { tone, live } = RUN_STATUS_TONE[run.status]; - return ( - - - - - - - ); - })} - -
- Status - Topic - Structure - - Built -
- - - - {run.topic} - - {structureLabel(run)}{relativeTime(run.createdAt)}
-
-
- ); -}