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)}
-
-
- ); -}