/**
* pages.ts — the shell pages: landing (all classrooms), classroom (its lessons and
* documents), and the markdown document view.
*
* Pure string builders — they take already-read data and return HTML, so they are
* testable without a server or a filesystem.
*/
import { escapeHtml, renderMarkdown } from "./markdown.js";
import type { Classroom, Lesson } from "./store.js";
// ── Shell ─────────────────────────────────────────────────────────────────────
interface ShellOptions {
title: string;
/** Rendered into the sticky header, left of the theme toggle. */
breadcrumb: string;
body: string;
/** Page-level class applied to
, for layout variants. */
bodyClass?: string;
}
/**
* Wrap page content in the common document shell.
*
* The theme bootstrap runs inline in deliberately: reading localStorage after
* the stylesheet has painted would flash the light theme at dark-mode readers.
*/
export function shell(options: ShellOptions): string {
return `
${escapeHtml(options.title)}
${options.body}
`;
}
// ── Landing page ──────────────────────────────────────────────────────────────
export function landingPage(classrooms: Classroom[]): string {
const body =
classrooms.length === 0
? emptyState(
"No classrooms yet",
"Run /teach <topic> in your Pi session and your first classroom will appear here.",
)
: `
Classrooms
Pick up where you left off, or start something new with /teach.
`;
}
/**
* Pull the "## Why" paragraph out of MISSION.md for the card subtitle.
*
* The mission is the reason the learner cares about the topic, so it is the single
* most useful line to show on a card — far better than a truncated lesson title.
*/
export function missionWhy(mission: string | null): string | null {
if (!mission) return null;
const lines = mission.split("\n");
const start = lines.findIndex((line) => /^##\s+why\s*$/i.test(line.trim()));
if (start === -1) return null;
const body: string[] = [];
for (const line of lines.slice(start + 1)) {
if (/^##\s/.test(line)) break;
body.push(line);
}
const firstParagraph = body
.join("\n")
.trim()
.split(/\n\s*\n/)[0]
?.replace(/\s+/g, " ")
.trim();
if (!firstParagraph) return null;
return firstParagraph.length > 220
? firstParagraph.slice(0, 217).trimEnd() + "…"
: firstParagraph;
}
// ── Classroom page ────────────────────────────────────────────────────────────
export interface ClassroomPageData {
classroom: Classroom;
lessons: Lesson[];
docs: string[];
learningRecords: string[];
referenceDocs: string[];
}
export function classroomPage(data: ClassroomPageData): string {
const { classroom, lessons } = data;
const lessonList =
lessons.length === 0
? emptyState(
"No lessons yet",
"Ask your teacher for the next lesson in your Pi session — it will show up here.",
)
: `
${lessons.map((lesson, i) => lessonRow(lesson, i)).join("\n")}
`;
const body = `