From 12ad61d98ec23005b7f58e4063569d64914a4c26 Mon Sep 17 00:00:00 2001 From: UltimaPhoenix <2079222+UltimaPhoenix@users.noreply.github.com> Date: Sun, 8 Nov 2026 20:04:14 +0100 Subject: [PATCH] fix(dashboard): /courses and /settings read in a narrow window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Below sm (640 px) the courses table cut every title ("From a cache miss to a…") and laid the status badge over the date, and the settings form's paired fields (lessons per day + gap, interactions + scope) cut their selects ("Per chat sessi…"). - /courses below sm is a list: each course is a block with the full title (no clamp) and its lesson, then progress · status · updated on one line. From sm up it is the table, unchanged. - /settings below sm stacks the paired fields one per row. From sm up, two per row as before. Classes only, in views.ts. The nav is untouched: how Settings and the theme toggle fit a narrow window is a separate decision. tests/web.test.ts: the courses list carries its narrow layout (hidden header, wrapping rows, unclamped title); settings has no fixed two-column row left. --- src/web/views.ts | 28 +++++++++++++++------------- tests/web.test.ts | 24 ++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/src/web/views.ts b/src/web/views.ts index 545a9c2..6150e0b 100644 --- a/src/web/views.ts +++ b/src/web/views.ts @@ -1240,27 +1240,29 @@ export function coursesPage(d: CoursesData): Html { ${ d.courses.length ? html`
- - - + +
+ + - + ${d.courses.map((c) => { const done = c.steps.filter((s) => s.status !== "todo").length; const pct = c.steps.length ? Math.round((done / c.steps.length) * 100) : 0; - return html` - + - - - + + `; })} @@ -1435,7 +1437,7 @@ ${

Coaching

-
+
@@ -1449,7 +1451,7 @@ ${

Minimum gap between lessons.

-
+
diff --git a/tests/web.test.ts b/tests/web.test.ts index f5cbe38..bb1d501 100644 --- a/tests/web.test.ts +++ b/tests/web.test.ts @@ -131,6 +131,15 @@ describe("web app", () => { expect(await (await get("/settings?notebook_saved=1")).text()).toContain("Coaching Notebook"); }); + it("settings stacks its paired fields in a narrow window", async () => { + const page = await (await get("/settings")).text(); + // two rows of paired fields (lessons per day + gap, interactions + scope): one column below sm + expect( + page.match(/
/g)?.length, + ).toBeGreaterThanOrEqual(2); + expect(page).not.toContain('
'); + }); + it("multipart import (lessons.json + backup.zip)", async () => { const fd = new FormData(); fd.append( @@ -971,6 +980,21 @@ describe("web courses", () => { expect(list).toContain("From sums to powers"); expect(list).toContain("0/2"); expect(list).toContain("Webify"); // seeded-from lesson title + // Below sm the table is a list: the header hides, each row wraps, the title is never clamped. + expect(list).toContain('
- ${c.title} - ${c.lesson_id ? html`from ${c.lesson_title ?? c.lesson_id}` : ""} + return html`
+ ${c.title} + ${c.lesson_id ? html`from ${c.lesson_title ?? c.lesson_id}` : ""} +
${done}/${c.steps.length}
${statusBadge(c.status)}${c.updated_at.slice(0, 10)}${statusBadge(c.status)}${c.updated_at.slice(0, 10)}
'); + expect(list).toMatch(/From sums to powers + courses.createCourse(c, { title: "Reading a flame graph", topic_id: "profiling" }), + ); + const bareRow = (await (await get("/courses")).text()) + .split("Reading a flame graph")[1] + ?.split("")[0]; + expect(bareRow).toBeDefined(); + expect(bareRow).not.toContain("from courses.deleteCourse(c, bare.id)); const detail = await (await get(`/courses/${course.id}`)).text(); expect(detail).toContain(`src="/courses/${course.id}/index.html#step-1"`);