commit 1b45aae6c049a62ac9c3e0c3e2f2759be8010a5d
parent 78379b24091fe0fcb1e5ac64117d11edce3dad8f
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Thu, 1 Oct 2026 15:34:24 -0400
umtool: a teaser entry is a card row named by its lines; the teaser's supporting lines a size up
The report page's timeline row and the export's fallback chapter name a
`teaser` by its lines joined with " — " (teaserTitle); the On-screen table
already does, through deckText. onscreen-fixture carries a teaser, and
onscreen.spec checks the table's placeholder, the preview's node and the
report page's row. The overline, second tier and kicker are drawn a size
larger, so the card holds the frame.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
5 files changed, 24 insertions(+), 8 deletions(-)
diff --git a/umtool/e2e/fixtures/make-fixture.mjs b/umtool/e2e/fixtures/make-fixture.mjs
@@ -1483,13 +1483,19 @@ const deckManifest = (slug, title, timeline) => {
// onscreen-fixture: the On-screen section and the bench's fields WRITE here --
// the switch, the settings, the table, a stale token. Never built: its
// schedule is the estimate, which is the state a report is in when titles are
-// first written. A card, because a row is any entry and not only a clip.
+// first written. A card, because a row is any entry and not only a clip; a
+// teaser last, because the page and the table must name one by its lines.
const ONSCREEN = writeProject(
"onscreen-fixture",
deckManifest("onscreen-fixture", "The On-screen Fixture", [
{ type: "clip", id: "c01", video: "vid1", start: 3.0, end: 6.0, cite: 3, section: 0, lock: true, quote: "and because" },
{ type: "clip", id: "c02", video: "vid1", start: 9.0, end: 12.0, cite: 9, section: 0, lock: true, quote: "another whole sentence" },
{ type: "card", id: "k01", style: "chapter", seconds: 3, heading: "A card" },
+ {
+ type: "teaser", id: "t01", seconds: 6,
+ lines: ["Next Season", { text: "The Big Build in the Valley", break: "in the Valley" }, "Spring 2027"],
+ tail: "?",
+ },
]),
);
diff --git a/umtool/e2e/onscreen.spec.ts b/umtool/e2e/onscreen.spec.ts
@@ -268,6 +268,11 @@ test("the preview is the composition: it reports ready, and typing reaches it be
const cardAuto = await row(page, "k01").getByTestId("onscreen-title").getAttribute("placeholder");
expect(cardAuto).toBe("A card");
await expect(frame.locator('[data-seg="k01"] .deck-title')).toHaveText(cardAuto!);
+ // A teaser is a card row named by its lines, here and on the report page's timeline.
+ const teaserTitle = "Next Season — The Big Build in the Valley — Spring 2027 ?";
+ await expect(frame.locator('[data-seg="t01"]')).toHaveCount(1);
+ expect(await row(page, "t01").getByTestId("onscreen-title").getAttribute("placeholder")).toBe(teaserTitle);
+ await expect(page.locator('li[data-entry="t01"][data-kind="teaser"]')).toContainText(teaserTitle);
// Typing is patched into the frame by postMessage: nothing is saved.
await fillSure(row(page, "c02").getByTestId("onscreen-title"), "Typed, not saved");
diff --git a/umtool/lib/projects/report.mjs b/umtool/lib/projects/report.mjs
@@ -11,6 +11,7 @@ import { DEFAULT_VARIANT, cachedWindowsFor } from "umtool-report-to-video/build-
import { rawCacheOf } from "../report/raw-cache.mjs";
import { CHANNELS_DIR } from "../paths.mjs";
import { channelName, cleanTitle } from "umtool-report-to-video/attribution";
+import { teaserTitle } from "umtool-report-to-video/deck";
/**
* Where a build's per-entry segments live.
@@ -1110,7 +1111,8 @@ export async function readClipDetail(dir, { manifest = null } = {}) {
// and 500'd the whole project page. Every other real manifest is clips only,
// which is exactly why this survived testing.
if (e.type !== "clip") {
- entries.push({ ...e, kind: e.type ?? "entry" });
+ // A teaser has no heading or title of its own: its row names its lines.
+ entries.push({ ...e, kind: e.type ?? "entry", ...(e.type === "teaser" ? { label: teaserTitle(e) } : {}) });
continue;
}
const chan = channelFor(m, e);
diff --git a/umtool/lib/report/export.mjs b/umtool/lib/report/export.mjs
@@ -18,6 +18,7 @@ import { readFile, readdir, stat } from "node:fs/promises";
import path from "node:path";
import { DEFAULT_VARIANT, selectVariant, segmentOffsets } from "umtool-report-to-video/build-video";
import { citeUrlFor, channelFor, readAvailability, readManifest } from "../projects/report.mjs";
+import { teaserTitle } from "umtool-report-to-video/deck";
export const EXPORT_FORMATS = ["toc-bbcode", "toc-markdown", "description", "chapters"];
@@ -60,7 +61,9 @@ export function parseFfmeta(text) {
}
/** The chapter title the build would print: `chapter`, else the entry's own words. */
-const titleOf = (e, i) => e.chapter ?? (e.type === "clip" ? `${i + 1}. ${e.video}` : e.title ?? e.heading ?? `Card ${i + 1}`);
+const titleOf = (e, i) =>
+ e.chapter ??
+ (e.type === "clip" ? `${i + 1}. ${e.video}` : e.type === "teaser" ? teaserTitle(e) : e.title ?? e.heading ?? `Card ${i + 1}`);
/**
* Deliverable-second offsets for every entry of the chosen variant.
diff --git a/umtool/report-to-video/chrome-teaser.mjs b/umtool/report-to-video/chrome-teaser.mjs
@@ -177,10 +177,10 @@ export function teaserCues({ lines, tail = "", seconds, motion = TEASER_MOTION }
/** Each role's type: size (px, the most it may be), weight, width (%), tracking (em), and the fit floor. */
export const TEASER_TYPE = Object.freeze({
- overline: Object.freeze({ size: 30, weight: 600, stretch: 125, tracking: 0.48, floor: 18 }),
+ overline: Object.freeze({ size: 34, weight: 600, stretch: 125, tracking: 0.48, floor: 18 }),
title: Object.freeze({ size: 148, weight: 900, stretch: 112, tracking: -0.006, floor: 56 }),
- sub: Object.freeze({ size: 34, weight: 600, stretch: 125, tracking: 0.4, floor: 18 }),
- kicker: Object.freeze({ size: 76, weight: 800, stretch: 118, tracking: 0.04, floor: 32 }),
+ sub: Object.freeze({ size: 38, weight: 600, stretch: 125, tracking: 0.4, floor: 18 }),
+ kicker: Object.freeze({ size: 84, weight: 800, stretch: 118, tracking: 0.04, floor: 32 }),
});
/**
@@ -294,10 +294,10 @@ export function teaserHtml(entry, render, opts = {}) {
${typeCss(".kicker > .pop > .row", ty.kicker)}
.kicker > .pop > .row { line-height: 1.1; }
.kicker .txt { color: ${pal.fg}; }
- .overline { margin-bottom: 34px; }
+ .overline { margin-bottom: 38px; }
.title + .title { margin-top: 10px; }
.title .sub { margin-top: 14px; }
- .kicker { margin-top: 64px; }
+ .kicker { margin-top: 70px; }
/* An overline between two hairline rules in the accent. */
.rules { position: absolute; left: -132px; right: -132px; top: 50%; height: 2px; transform-origin: 50% 50%; }
.rule { position: absolute; top: 0; width: 96px; height: 2px; border-radius: 1px; }