Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
ceda428
fix(slideshow): address code-review findings #1580-1584
vanceingalls Jun 19, 2026
cc75257
feat(player): slideshow fullscreen + presenter-view rework
vanceingalls Jun 19, 2026
2e5e63c
fix(player): slideshow no auto-progress + presenter slide fits/pins
vanceingalls Jun 19, 2026
0f229fe
fix(slideshow): presenter nav flash, slide-1 boundary, branch buttons
vanceingalls Jun 19, 2026
f2ad1dd
fix(slideshow): stop presenter nav buttons flickering / dropping clicks
vanceingalls Jun 19, 2026
0d208db
fix(slideshow): CSP-safe nav hover, UUID editor ids, manifest version
vanceingalls Jun 19, 2026
32996ed
chore(ci): fix format + fallow gates for slideshow stack
vanceingalls Jun 19, 2026
8b6f09e
fix(slideshow): address PR review + CodeQL findings
vanceingalls Jun 19, 2026
5ee7b2f
feat(cli): add 'present' command — serve a deck in presenter mode
vanceingalls Jun 19, 2026
54a4460
fix(cli): present renders the deck (player sizing + self-driving serve)
vanceingalls Jun 19, 2026
5c70074
fix(cli): present plays slideshow sound effects
vanceingalls Jun 19, 2026
8830a3c
feat(examples): softer mellow slideshow sfx for airbnb-deck
vanceingalls Jun 19, 2026
366c310
feat(examples): whoosh + sparkle slideshow sfx for airbnb-deck
vanceingalls Jun 19, 2026
87e0f73
feat(examples): directional whoosh + richer branch-enter cue (airbnb-…
vanceingalls Jun 19, 2026
d7bb879
fix(cli): harden present sfx handler + mute-hover affordance (R2 review)
vanceingalls Jun 19, 2026
e1e08f7
fix(slideshow): address remaining R2 items (14/18/22) + re-remove har…
vanceingalls Jun 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .fallowrc.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,21 @@
// that naturally converges and is unlikely to diverge; extraction would
// require intrusive middleware changes beyond this PR's scope.
"minLines": 6,
"ignore": [
// slideshowPanelHelpers.ts: setSlideNotes/addFragment/addHotspot share an
// intentional parallel shape (signature + mapSlidesIn → exists-check →
// map/append); the per-slide mutation differs, so a shared abstraction
// would obscure more than it dedupes.
"packages/studio/src/components/panels/slideshowPanelHelpers.ts",
// SlideshowPanel.test.ts: parallel arrange/act/assert test cases — collapsing
// them would hurt readability of what each case verifies.
"packages/studio/src/components/panels/SlideshowPanel.test.ts",
// present.ts mirrors play.ts's server startup + console-output block. The
// shared low-level pieces (resolve*/injectRuntime/listenOnFreePort) are in
// utils/compositionServer.ts; the remaining clone is per-command logging text
// (different labels/help lines) — extracting it would over-abstract.
"packages/cli/src/commands/present.ts",
],
},
"health": {
// executeGsapMutation (introduced by Phase 3b / acorn-parser stack, already
Expand All @@ -260,6 +275,18 @@
"ignore": [
"packages/core/src/studio-api/routes/files.ts",
"packages/core/src/parsers/gsapParser.ts",
// SlideshowPanel.tsx: top-level editor panel that wires several independent
// sections (slides/inspector/branches/hotspot). Its cyclomatic count comes
// from that fan-out; splitting it would scatter shared state without
// reducing real complexity. File-level exemption (not an inline comment)
// avoids the line-shift fingerprint problem noted above.
"packages/studio/src/components/panels/SlideshowPanel.tsx",
// play.ts / present.ts: CLI command entrypoints whose cyclomatic count is
// browser/arg validation + server wiring (same shape as preview.ts). The
// serving logic is factored into utils/compositionServer.ts; the remaining
// body is linear validation that reads clearly inline.
"packages/cli/src/commands/play.ts",
"packages/cli/src/commands/present.ts",
],
},
}
11 changes: 11 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,14 @@ skills/**/assets/vendor/
skills/**/*.min.js
# reference snippets with intentional pseudo-markup (literal "..." attributes)
skills/graphic-overlays/references/frames/polaroid.html

# Generated demo compositions — large video-pipeline output (GSAP/Three/WebGL
# embedded), not hand-authored source; reformatting them is churn + risk. Listed
# explicitly (not registry/examples/**/*.html) so hand-authored example HTML still
# gets formatted.
registry/examples/airbnb-deck/index.html
registry/examples/airbnb-deck/demo.html
registry/examples/startup-pitch/index.html
registry/examples/startup-pitch/demo.html
registry/examples/slideshow-demo/index.html
registry/examples/warm-grain/compositions/captions.html
1 change: 1 addition & 0 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ const commandLoaders = {
add: () => import("./commands/add.js").then((m) => m.default),
catalog: () => import("./commands/catalog.js").then((m) => m.default),
play: () => import("./commands/play.js").then((m) => m.default),
present: () => import("./commands/present.js").then((m) => m.default),
preview: () => import("./commands/preview.js").then((m) => m.default),
publish: () => import("./commands/publish.js").then((m) => m.default),
render: () => import("./commands/render.js").then((m) => m.default),
Expand Down
108 changes: 13 additions & 95 deletions packages/cli/src/commands/play.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const examples: Example[] = [
"hyperframes play --browser-path /usr/bin/chromium --user-data-dir /tmp/hf-profile --remote-debugging-port 9222",
],
];
import { resolve, dirname } from "node:path";
import { resolve } from "node:path";
import * as clack from "@clack/prompts";
import { c } from "../ui/colors.js";
import { resolveProject } from "../utils/project.js";
Expand All @@ -22,6 +22,13 @@ import {
parseRemoteDebuggingPort,
validateRemoteDebuggingPortDeps,
} from "../utils/openBrowser.js";
import {
resolveRuntimePath,
resolvePlayerPath,
listenOnFreePort,
injectRuntime,
assetContentType,
} from "../utils/compositionServer.js";

export default defineCommand({
meta: { name: "play", description: "Play a composition in a lightweight browser player" },
Expand Down Expand Up @@ -121,7 +128,7 @@ export default defineCommand({
});

// Serve composition files (HTML + assets)
app.get("/composition/*", async (ctx) => {
app.get("/composition/*", (ctx) => {
const reqPath = ctx.req.path.replace("/composition/", "");
const filePath = resolve(project.dir, reqPath);

Expand All @@ -131,33 +138,11 @@ export default defineCommand({
// shares the project-dir prefix (e.g. `<dir>-evil`) can escape.
if (!isSafePath(project.dir, filePath)) return ctx.text("Forbidden", 403);
if (!existsSync(filePath)) return ctx.text("Not found", 404);

const content = readFileSync(filePath, "utf-8");

// For the main HTML, inject the runtime script before </body>
// HTML gets the runtime injected; other assets pass through with a guessed type.
if (filePath.endsWith(".html")) {
const injected = injectRuntime(content);
return ctx.html(injected);
return ctx.html(injectRuntime(readFileSync(filePath, "utf-8")));
}

// Guess content type for other files
const ext = filePath.split(".").pop() ?? "";
const types: Record<string, string> = {
js: "application/javascript",
css: "text/css",
json: "application/json",
png: "image/png",
jpg: "image/jpeg",
jpeg: "image/jpeg",
svg: "image/svg+xml",
mp4: "video/mp4",
webm: "video/webm",
mp3: "audio/mpeg",
wav: "audio/wav",
};
return ctx.body(readFileSync(filePath), 200, {
"Content-Type": types[ext] ?? "application/octet-stream",
});
return ctx.body(readFileSync(filePath), 200, { "Content-Type": assetContentType(filePath) });
});

// Main page — the player wrapper
Expand All @@ -170,31 +155,7 @@ export default defineCommand({
s.start("Starting player...");

const server = createAdaptorServer({ fetch: app.fetch });
let actualPort = startPort;

for (let attempt = 0; attempt < 10; attempt++) {
const port = startPort + attempt;
try {
await new Promise<void>((res, rej) => {
const onErr = (err: NodeJS.ErrnoException) => {
server.removeListener("listening", onOk);
rej(err);
};
const onOk = () => {
server.removeListener("error", onErr);
res();
};
server.once("error", onErr);
server.once("listening", onOk);
server.listen(port);
});
actualPort = port;
break;
} catch (err: unknown) {
if ((err as NodeJS.ErrnoException).code === "EADDRINUSE") continue;
throw err;
}
}
const actualPort = await listenOnFreePort(server, startPort);

const url = `http://localhost:${actualPort}`;
s.stop(c.success("Player running"));
Expand All @@ -220,49 +181,6 @@ export default defineCommand({
},
});

function commandDir(): string {
return dirname(new URL(import.meta.url).pathname);
}

function resolveRuntimePath(): string | null {
const d = commandDir();
const candidates = [
// Bundled with CLI dist
resolve(d, "hyperframe-runtime.js"),
resolve(d, "..", "hyperframe-runtime.js"),
// Monorepo dev: commands/ → src/ → cli/ → packages/ then into core/dist/
resolve(d, "..", "..", "..", "core", "dist", "hyperframe.runtime.iife.js"),
];
for (const p of candidates) {
if (existsSync(p)) return p;
}
return null;
}

function resolvePlayerPath(): string | null {
const d = commandDir();
const candidates = [
// Monorepo dev: commands/ → src/ → cli/ → packages/ then into player/dist/
resolve(d, "..", "..", "..", "player", "dist", "hyperframes-player.global.js"),
// Bundled with CLI dist
resolve(d, "hyperframes-player.global.js"),
resolve(d, "..", "hyperframes-player.global.js"),
];
for (const p of candidates) {
if (existsSync(p)) return p;
}
return null;
}

function injectRuntime(html: string): string {
// Inject runtime script before closing </body> or at the end
const runtimeTag = `<script src="/runtime.js"></script>`;
if (html.includes("</body>")) {
return html.replace("</body>", `${runtimeTag}\n</body>`);
}
return html + `\n${runtimeTag}`;
}

function buildPlayerPage(projectName: string): string {
return `<!doctype html>
<html lang="en">
Expand Down
Loading
Loading