feat(website): refine casts — trim shell, autoplay+loop landing, cap size

Address cast review feedback:

- Trim every cast to the in-app region (generate.mjs): the recording now
  starts with the app already running and ends on the last in-app frame —
  drops the `$ rdbms-playground` launch and the return-to-shell frame (the
  latter was the stray cursor-under-$ artifact). Opt out per cast with
  `keepShell: true` for demos that document the CLI launch.
- Landing quickstart cast: autoPlay + loop, with a 2.5s hold on the final
  frame so it pauses before restarting.
- Cap the demo at max-width 46rem and centre it, so the player (fit:'width')
  no longer scales its font up to the full splash column.

Casts re-recorded via `pnpm casts`. Build clean (25 pages).

Tab-keypress visibility deferred to an in-app overlay primitive (filed as
issue #22 — also serves the planned guided-lesson system); the cast notes
Tab in its caption for now.
This commit is contained in:
claude@clouddev1
2026-06-10 13:56:39 +00:00
parent 1f82fb2c79
commit a8f84c9d17
6 changed files with 458 additions and 441 deletions
+1
View File
@@ -45,6 +45,7 @@ export const casts = [
width: 90,
height: 26,
typeSpeed: '45ms',
holdEnd: 2.5, // landing cast loops — pause on the final frame before restart
steps: [
{ wait: 1100 },
{ type: 'create table authors with pk', after: 1000 },
+44 -1
View File
@@ -15,7 +15,7 @@
* recording shows a clean `$ rdbms-playground` launch line.
*/
import { spawnSync } from 'node:child_process';
import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import os from 'node:os';
@@ -67,6 +67,45 @@ function keysFor(steps) {
return keys;
}
/**
* Trim a recorded cast to just the in-app portion and (optionally) hold the
* final frame. The app enters the alternate screen (`?1049h`) right after
* launch and leaves it (`?1049l`) on quit, so keeping the events between those
* drops the shell prompt, the `$ rdbms-playground` launch line, and the
* return-to-shell frame — the cast starts with the app already running and
* ends on the last in-app frame (no stray cursor-under-prompt frame).
* `holdEnd` re-emits the final frame after a pause so a looping cast lingers
* before restarting.
*/
function trimCast(outPath, { holdEnd = 1.5 } = {}) {
const lines = readFileSync(outPath, 'utf8').split('\n').filter(Boolean);
const header = JSON.parse(lines[0]);
const events = lines.slice(1).map((l) => JSON.parse(l));
const isOut = (e) => Array.isArray(e) && e[1] === 'o' && typeof e[2] === 'string';
const start = events.findIndex((e) => isOut(e) && e[2].includes('?1049h'));
let end = -1;
for (let i = events.length - 1; i >= 0; i--) {
if (isOut(events[i]) && events[i][2].includes('?1049l')) {
end = i;
break;
}
}
const kept = events.slice(start < 0 ? 0 : start, end < 0 ? events.length : end);
if (kept.length === 0) return; // nothing matched — leave the cast untouched
const t0 = kept[0][0];
const rebased = kept.map((e) => [Number((e[0] - t0).toFixed(6)), e[1], e[2]]);
if (holdEnd > 0) {
const lastT = rebased[rebased.length - 1][0];
rebased.push([Number((lastT + holdEnd).toFixed(6)), 'o', '']); // hold final frame
}
delete header.duration; // let the player recompute from events
writeFileSync(
outPath,
[JSON.stringify(header), ...rebased.map((e) => JSON.stringify(e))].join('\n') + '\n'
);
}
function yamlFor(cast) {
const keys = keysFor(cast.steps)
.map((k) => ` - ${k}`)
@@ -113,6 +152,10 @@ for (const cast of selected) {
console.error(`${cast.name} failed (autocast exit ${res.status})`);
failures += 1;
} else {
// Trim the shell launch/quit so the cast starts with the app already
// running (the default; opt out with `keepShell: true` for casts that
// deliberately document the CLI launch).
if (!cast.keepShell) trimCast(outPath, { holdEnd: cast.holdEnd ?? 1.5 });
console.log(`${cast.name}`);
}
}