@hypersocial/decks-cli 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/ops.mjs CHANGED
@@ -36,7 +36,8 @@ export async function activityOverview(opts = {}) {
36
36
  return (await client(opts)).query(api.activity.overview, {});
37
37
  }
38
38
 
39
- // Generate an image via fal (GPT Image 2 t2i/i2i, optional BiRefNet transparency),
39
+ // Generate an image via fal (GPT Image 2.5 by default, or Nano Banana 2.1 / GPT
40
+ // Image 2 with `model`; t2i/i2i, optional BiRefNet transparency),
40
41
  // store it in Convex, and return the hosted URL. Runs the imagegen action as the
41
42
  // authenticated user (injected client supported via opts.client).
42
43
  export async function generateImage(opts, fields = {}) {
@@ -51,6 +52,8 @@ export async function generateImage(opts, fields = {}) {
51
52
  if (fields.quality) args.quality = String(fields.quality);
52
53
  if (fields.count != null && fields.count !== "") args.count = Number(fields.count);
53
54
  if (fields.format) args.format = String(fields.format);
55
+ if (fields.model) args.model = String(fields.model);
56
+ if (fields.seed != null && fields.seed !== "") args.seed = Number(fields.seed);
54
57
  // Returns { images } for fast requests, or { jobId, status, count } for slow ones
55
58
  // (high/auto quality, transparent, count>1). The MCP tool returns this as-is so it
56
59
  // never blocks; the CLI polls with pollImageJob to stay one-command (see commands).
@@ -99,8 +102,9 @@ export async function pollImageJob(opts, jobId, { intervalMs = 3000, timeoutMs =
99
102
 
100
103
  // ---- video (convex/videogen.ts) ------------------------------------------
101
104
 
102
- // Generate a short clip via fal (Kling v3: text→video, or image→video when an image
103
- // URL is given as the first frame). Always a background job: returns
105
+ // Generate a short clip via fal (MiniMax H3 Max by default, Kling v3 with
106
+ // model "kling-v3": text→video, or image→video when an image URL is given as the
107
+ // first frame). Always a background job: returns
104
108
  // { jobId, status, seconds, aspect?, creditsRemaining? } without waiting, so the MCP
105
109
  // tool never blocks; the CLI polls with pollVideoJob.
106
110
  export async function generateVideo(opts, fields = {}) {
@@ -110,11 +114,99 @@ export async function generateVideo(opts, fields = {}) {
110
114
  if (img) args.imageUrl = String(img);
111
115
  if (fields.seconds != null && fields.seconds !== "") args.seconds = Number(fields.seconds);
112
116
  if (fields.aspect) args.aspect = String(fields.aspect);
117
+ if (fields.model) args.model = String(fields.model);
113
118
  if (fields.slug) args.slug = String(fields.slug);
114
119
  if (fields.name) args.name = String(fields.name);
115
120
  return (await client(opts)).action(api.videogen.generateVideo, args);
116
121
  }
117
122
 
123
+ // ---- deck assets: screenshots, charts, icon sets --------------------------
124
+
125
+ // Screenshot a public URL (convex/screenshots.ts) and store it as a PNG asset.
126
+ // Returns { url, storageId, contentType, name?, width, height, creditsRemaining? }.
127
+ export async function captureUrl(opts, fields = {}) {
128
+ if (!fields.url) throw new Error("url is required");
129
+ const args = { url: String(fields.url) };
130
+ if (fields.width != null && fields.width !== "") args.width = Number(fields.width);
131
+ if (fields.height != null && fields.height !== "") args.height = Number(fields.height);
132
+ if (fields.fullPage) args.fullPage = true;
133
+ if (fields.slug) args.slug = String(fields.slug);
134
+ if (fields.name) args.name = String(fields.name);
135
+ return (await client(opts)).action(api.screenshots.captureUrl, args);
136
+ }
137
+
138
+ const CHART_FIELDS = ["kind", "title", "subtitle", "labels", "series", "values", "value", "label", "delta", "prefix", "suffix", "palette", "width", "height", "slug", "name"];
139
+
140
+ // Render a chart spec (convex/charts.ts) to a branded SVG asset. Unknown keys are
141
+ // dropped so a spec file may carry notes of its own.
142
+ export async function renderChart(opts, spec = {}) {
143
+ if (!spec.kind) throw new Error("kind is required (bar, line, donut or number)");
144
+ const args = {};
145
+ for (const k of CHART_FIELDS) if (spec[k] !== undefined && spec[k] !== null) args[k] = spec[k];
146
+ return (await client(opts)).action(api.charts.renderChart, args);
147
+ }
148
+
149
+ // Queue a style-consistent transparent icon set (convex/imagegen.ts
150
+ // generateIconSet). Returns { jobId, status, count, subjects }; poll it like any
151
+ // image job (checkImageJob / pollImageJob).
152
+ export async function generateIconSet(opts, fields = {}) {
153
+ const subjects = (fields.subjects || []).map(String).filter(Boolean);
154
+ if (!subjects.length) throw new Error("subjects is required (one or more things to draw)");
155
+ if (!fields.style) throw new Error("style is required");
156
+ const args = { subjects, style: String(fields.style) };
157
+ if (fields.palette?.length) args.palette = fields.palette.map(String);
158
+ if (fields.slug) args.slug = String(fields.slug);
159
+ if (fields.name) args.name = String(fields.name);
160
+ if (fields.model) args.model = String(fields.model);
161
+ return (await client(opts)).action(api.imagegen.generateIconSet, args);
162
+ }
163
+
164
+ // ---- share-link poster (convex/posters.ts) -------------------------------
165
+
166
+ // Regenerate the deck's share-link poster (release and import already make
167
+ // one). Returns { slug, url, unchanged }: unchanged when the poster already
168
+ // shows the deck's current HTML, unless `force`.
169
+ export async function generatePoster(opts, fields = {}) {
170
+ if (!fields.slug) throw new Error("slug is required");
171
+ const args = { slug: String(fields.slug) };
172
+ if (fields.force) args.force = true;
173
+ return (await client(opts)).action(api.posters.generatePoster, args);
174
+ }
175
+
176
+ // ---- deck review (convex/review.ts) --------------------------------------
177
+
178
+ // Queue a GPT-6 Luna review of every slide (desktop + phone screenshots of the
179
+ // deck's QA share). Returns { jobId, status, slides, shareToken, creditsRequired }.
180
+ export async function reviewDeck(opts, fields = {}) {
181
+ if (!fields.slug) throw new Error("slug is required");
182
+ const args = { slug: String(fields.slug) };
183
+ if (fields.script) args.script = String(fields.script);
184
+ return (await client(opts)).action(api.review.reviewDeck, args);
185
+ }
186
+
187
+ export async function checkReviewJob(opts, jobId) {
188
+ if (!jobId) throw new Error("jobId is required");
189
+ return (await client(opts)).query(api.review.checkReviewJob, { jobId: String(jobId) });
190
+ }
191
+
192
+ // Poll a review job until it finishes; returns the job (findings and all).
193
+ // Throws on job error or timeout.
194
+ export async function pollReviewJob(opts, jobId, { intervalMs = 4000, timeoutMs = 1_800_000, onProgress } = {}) {
195
+ const c = await client(opts);
196
+ const start = Date.now();
197
+ for (;;) {
198
+ const job = await c.query(api.review.checkReviewJob, { jobId: String(jobId) });
199
+ if (job.status === "done") return job;
200
+ if (job.status === "error") {
201
+ const done = job.findings?.length ? ` (${job.progress} slides reviewed, ${job.findings.length} finding(s) so far: review-job ${jobId})` : "";
202
+ throw new Error(`${job.error || "review failed"}${done}`);
203
+ }
204
+ if (Date.now() - start > timeoutMs) throw new Error(`review ${jobId} is still running; check it later with: review-job ${jobId}`);
205
+ if (onProgress) onProgress(job);
206
+ await new Promise((r) => setTimeout(r, intervalMs));
207
+ }
208
+ }
209
+
118
210
  // Read one video job's status (and the hosted MP4 once done). Shared by the MCP
119
211
  // check_video_job tool and the CLI poller.
120
212
  export async function checkVideoJob(opts, jobId) {
@@ -613,7 +705,7 @@ function designServeUrl(projectId, token, path) {
613
705
  }
614
706
 
615
707
  // Pull every local asset path a deck file references (HTML or CSS): the
616
- // ext-resource-dependency manifest, src/href/poster, srcset, <script>/<link>,
708
+ // ext-resource-dependency manifest, src/href/poster/data-presenter-image, srcset, <script>/<link>,
617
709
  // and CSS url(). Mirrors the importer's rewrite surface so we fetch the same set
618
710
  // it will later rewrite. External/data/blob refs are skipped.
619
711
  function extractDesignRefs(text) {
@@ -625,7 +717,7 @@ function extractDesignRefs(text) {
625
717
  if (clean) out.add(clean);
626
718
  };
627
719
  for (const m of text.matchAll(/ext-resource-dependency"\s+content="([^"]+)"/gi)) add(m[1]);
628
- for (const m of text.matchAll(/\b(?:src|href|poster)\s*=\s*["']([^"']+)["']/gi)) add(m[1]);
720
+ for (const m of text.matchAll(/\b(?:src|href|poster|data-presenter-image)\s*=\s*["']([^"']+)["']/gi)) add(m[1]);
629
721
  for (const m of text.matchAll(/\bsrcset\s*=\s*["']([^"']+)["']/gi)) {
630
722
  for (const part of m[1].split(",")) add(part.trim().split(/\s+/)[0]);
631
723
  }
package/src/runtime.mjs CHANGED
@@ -8,7 +8,7 @@ import { fileURLToPath } from "node:url";
8
8
  import { ConvexHttpClient } from "convex/browser";
9
9
  import { makeFunctionReference } from "convex/server";
10
10
 
11
- export const VERSION = "0.1.12";
11
+ export const VERSION = "0.2.2";
12
12
  const UPLOAD_TIMEOUT_MS = 125_000;
13
13
 
14
14
  export const PROD = {
@@ -50,6 +50,20 @@ export const api = {
50
50
  imagegen: {
51
51
  generateImage: makeFunctionReference("imagegen:generateImage"),
52
52
  checkImageJob: makeFunctionReference("imagegen:checkImageJob"),
53
+ generateIconSet: makeFunctionReference("imagegen:generateIconSet"),
54
+ },
55
+ screenshots: {
56
+ captureUrl: makeFunctionReference("screenshots:captureUrl"),
57
+ },
58
+ charts: {
59
+ renderChart: makeFunctionReference("charts:renderChart"),
60
+ },
61
+ posters: {
62
+ generatePoster: makeFunctionReference("posters:generatePoster"),
63
+ },
64
+ review: {
65
+ reviewDeck: makeFunctionReference("review:reviewDeck"),
66
+ checkReviewJob: makeFunctionReference("review:checkReviewJob"),
53
67
  },
54
68
  videogen: {
55
69
  generateVideo: makeFunctionReference("videogen:generateVideo"),
@@ -75,7 +89,7 @@ export const api = {
75
89
  },
76
90
  };
77
91
 
78
- const BOOL = new Set(["prod", "dev", "admin", "json", "require-email", "qa", "no-notify", "help", "version", "all-agents", "global", "project", "transparent", "upload", "dry-run", "narrated"]);
92
+ const BOOL = new Set(["prod", "dev", "admin", "json", "require-email", "qa", "no-notify", "help", "version", "all-agents", "global", "project", "transparent", "upload", "dry-run", "narrated", "layout", "full-page", "force"]);
79
93
  // release's recipients, in order: each --to starts one, and an --email right
80
94
  // after it belongs to it (`--to A --email a --to B`). Collected as opts.recipients;
81
95
  // opts.email stays the last --email for the other commands (share new).
package/src/scaffold.mjs CHANGED
@@ -1,35 +1,101 @@
1
- // hyperdecks deck new <slug> "<title>" --narrated [--dir <path>]
1
+ // hyperdecks deck new <slug> "<title>" --narrated [--layouts hero,product,…] [--presenter-style stage|bubble|glass|corner] [--dir <path>]
2
2
  //
3
3
  // Writes decks/<slug>/ (or --dir) from the packaged templates: index.html (eight
4
- // slides for the hosted player/v1), script.md (the narration) and evidence.md
5
- // (where each fact comes from), then registers the deck as a draft you own so
6
- // narrate can render into it.
4
+ // slides for the hosted player/v1, or one slide per --layouts entry from
5
+ // templates/layouts/), script.md (the narration) and evidence.md (where each
6
+ // fact comes from), then registers the deck as a draft you own so narrate can
7
+ // render into it.
7
8
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
8
9
  import { dirname, join, resolve } from "node:path";
9
10
  import { fileURLToPath } from "node:url";
10
11
  import * as ops from "./ops.mjs";
12
+ import { PRESENTER_STYLES } from "./check.mjs";
11
13
 
12
14
  export const TEMPLATE_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "templates");
13
15
  export const NARRATED_FILES = { "index.html": "narrated-deck.html", "script.md": "script.md", "evidence.md": "evidence.md" };
14
16
  const SLUG = /^[a-z0-9][a-z0-9-]{0,63}$/;
17
+ export const LAYOUT_DIR = join(TEMPLATE_DIR, "layouts");
18
+ // The library, in a sensible deck order; `--layouts` takes any order and repeats.
19
+ export const LAYOUTS = ["hero", "number", "product", "quote", "compare", "timeline", "features", "chart", "team", "closer"];
20
+ const LAYOUT_ALIASES = { "big-number": "number", "product-shot": "product", comparison: "compare", "three-up": "features" };
21
+ const USAGE = 'usage: deck new <slug> "<title>" --narrated [--layouts hero,product,number,closer] [--presenter-style <stage|bubble|glass|corner>] [--dir <path>]';
15
22
 
16
23
  const escapeHtml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
17
24
 
18
25
  // Template text with __SLUG__ / __TITLE__ filled; HTML gets an escaped title.
19
- export function fillTemplate(file, { slug, title }) {
26
+ // Without a presenter style the player tag carries none (the player's default).
27
+ export function fillTemplate(file, { slug, title, presenterStyle }) {
20
28
  const text = readFileSync(join(TEMPLATE_DIR, file), "utf8");
21
29
  const t = /\.html$/.test(file) ? escapeHtml(title) : title;
22
- return text.split("__SLUG__").join(slug).split("__TITLE__").join(t);
30
+ const styled = presenterStyle ? text.split("__PRESENTER_STYLE__").join(presenterStyle) : text.replace(/\s*data-presenter-style="__PRESENTER_STYLE__"/g, "");
31
+ return styled.split("__SLUG__").join(slug).split("__TITLE__").join(t);
32
+ }
33
+
34
+ // One layout file: a leading comment of `key: value` lines (layout, summary,
35
+ // use, slot, presenter, cues, script), its <style> and its <section>.
36
+ export function readLayout(name) {
37
+ const text = readFileSync(join(LAYOUT_DIR, `${name}.html`), "utf8");
38
+ const head = /^<!--([\s\S]*?)-->/.exec(text);
39
+ const style = /<style>[\s\S]*?<\/style>/.exec(text);
40
+ const section = /<section\b[\s\S]*<\/section>/.exec(text);
41
+ if (!head || !style || !section) throw new Error(`layout ${name}: needs a leading comment, a <style> and a <section>`);
42
+ const meta = {};
43
+ for (const line of head[1].split("\n")) {
44
+ const m = /^([a-z]+):\s*(.*)$/.exec(line.trim());
45
+ if (m) meta[m[1]] = m[2];
46
+ }
47
+ return { name, meta, style: style[0], section: section[0] };
48
+ }
49
+
50
+ export function parseLayouts(list) {
51
+ const names = String(list).split(",").map((n) => n.trim().toLowerCase()).filter(Boolean).map((n) => LAYOUT_ALIASES[n] ?? n);
52
+ const unknown = names.filter((n) => !LAYOUTS.includes(n));
53
+ if (unknown.length) throw new Error(`unknown layout${unknown.length > 1 ? "s" : ""} ${unknown.join(", ")}: pick from ${LAYOUTS.join(", ")}`);
54
+ if (!names.length) throw new Error(`--layouts needs at least one of ${LAYOUTS.join(", ")}`);
55
+ return names;
56
+ }
57
+
58
+ // index.html, script.md and evidence.md for a deck of `layouts`, one slide
59
+ // each, in order. A layout used twice gets ids hero, hero-2, …; ids inside a
60
+ // slide start with its id, so its cues stay unique.
61
+ export function assembleLayoutDeck({ slug, title, presenterStyle, layouts }) {
62
+ const names = parseLayouts(layouts);
63
+ const used = new Map();
64
+ const slides = names.map((name) => {
65
+ const n = (used.get(name) ?? 0) + 1;
66
+ used.set(name, n);
67
+ return { layout: readLayout(name), id: n === 1 ? name : `${name}-${n}` };
68
+ });
69
+ const fill = (text, id, t) => text.split("__ID__").join(id).split("__SLUG__").join(slug).split("__TITLE__").join(t);
70
+ const sections = slides.map(({ layout, id }, i) => {
71
+ let html = fill(layout.section, id, escapeHtml(title));
72
+ if (i === 0 && !/\sdata-intro=/.test(html.slice(0, html.indexOf(">")))) html = html.replace("<section ", '<section data-intro="Hi! Press play." ');
73
+ return `<!-- layout: ${layout.name}. ${layout.meta.summary ?? ""} -->\n${html}`;
74
+ });
75
+ const styles = [...new Set(names)].map((name) => `<!-- layout: ${name} -->\n${readLayout(name).style}`);
76
+ const shell = fillTemplate("layouts/_deck.html", { slug, title, presenterStyle });
77
+ // Function replacements: a "$" in the title or a layout is text, not a replacement pattern.
78
+ const html = shell.replace("<!-- LAYOUT STYLES -->", () => styles.join("\n")).replace("<!-- SLIDES -->", () => sections.join("\n\n"));
79
+ const intro = fillTemplate("script.md", { slug, title }).split(/\n## /)[0].trimEnd();
80
+ const script = `${intro}\n\n${slides.map(({ layout, id }) => `## ${id}\n${fill(layout.meta.script ?? "", id, title)}`).join("\n\n")}\n`;
81
+ return { "index.html": html, "script.md": script, "evidence.md": fillTemplate("evidence.md", { slug, title }) };
23
82
  }
24
83
 
25
84
  export async function deckNew(pos, opts = {}, { log = (line) => console.log(line), cwd = process.cwd() } = {}) {
26
85
  const [slug, title] = pos;
27
- if (!slug || !title) throw new Error('usage: deck new <slug> "<title>" --narrated [--dir <path>]');
86
+ if (!slug || !title) throw new Error(USAGE);
28
87
  if (!opts.narrated) throw new Error("the published CLI scaffolds narrated decks only: add --narrated");
88
+ const presenterStyle = opts["presenter-style"];
89
+ if (presenterStyle !== undefined && !PRESENTER_STYLES.includes(presenterStyle)) {
90
+ throw new Error(`--presenter-style must be one of ${PRESENTER_STYLES.join(", ")}`);
91
+ }
29
92
  if (!SLUG.test(slug)) throw new Error("slug must be lowercase letters, digits and dashes (max 64)");
30
93
  const dir = resolve(cwd, typeof opts.dir === "string" ? opts.dir : join("decks", slug));
31
94
  if (existsSync(dir)) throw new Error(`${dir} already exists; pick another slug or --dir`);
32
- const files = Object.entries(NARRATED_FILES).map(([out, file]) => [out, fillTemplate(file, { slug, title })]);
95
+ if (opts.layouts === true) throw new Error(`--layouts needs a list, e.g. --layouts ${LAYOUTS.slice(0, 3).join(",")},closer`);
96
+ const files = opts.layouts !== undefined
97
+ ? Object.entries(assembleLayoutDeck({ slug, title, presenterStyle, layouts: opts.layouts }))
98
+ : Object.entries(NARRATED_FILES).map(([out, file]) => [out, fillTemplate(file, { slug, title, presenterStyle })]);
33
99
  await ops.createDeck(opts, { slug, title });
34
100
  try {
35
101
  mkdirSync(dir, { recursive: true });
@@ -0,0 +1,102 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>__TITLE__</title>
7
+ <!-- Loading state (keep verbatim; `hyperdecks check` requires it). Until the
8
+ player marks body[data-hd-ready] the page shows only the slide card, at its
9
+ final size, in the deck's --hd-ground (its first slide's colour) on the
10
+ page's brand colour, with a quiet loader in --hd-ground-ink; then the
11
+ slides fade in. If the player never arrives, the slides show after 10 s
12
+ rather than never. Put data-layout="overlay" on <html> for the overlay. -->
13
+ <link rel="modulepreload" href="https://decks.hypersocial.ai/player/v1/player.js">
14
+ <link rel="preload" as="style" href="https://decks.hypersocial.ai/player/v1/player.css">
15
+ <script>if (self !== top || /[?&]thumb\b/.test(location.search)) document.documentElement.dataset.hdFrame = "";</script>
16
+ <style data-hd-loading>
17
+ body:not([data-hd-ready]) { overflow: hidden; background: var(--hd-frame, var(--hd-ground, var(--hd-accent, #ffda1b))); animation: hd-fail-open 0s 10s forwards; }
18
+ body:not([data-hd-ready]) > * { visibility: hidden; animation: hd-fail-open 0s 10s forwards; }
19
+ body[data-hd-ready] > * { animation: hd-in .2s ease-out; }
20
+ body::before, body::after { content: ""; position: fixed; z-index: -1; pointer-events: none; transition: opacity .2s ease-out; }
21
+ /* The card as the player draws it (player.css, framed): 16:9, scaled to fit
22
+ between the 64px masthead and the 56px dock, 24px in from the sides. */
23
+ body::before { inset: 64px 24px 56px; margin: auto; width: min(100vw - 48px, (100dvh - 120px) * 16 / 9); height: min(100dvh - 120px, (100vw - 48px) * 9 / 16); border-radius: calc(min(100vw - 48px, (100dvh - 120px) * 16 / 9) * .01125); background: var(--hd-ground, #fff); box-shadow: 0 0 0 1px color-mix(in srgb, var(--hd-ground-ink, #0b0b0f) 16%, transparent); }
24
+ body::after { left: 50%; top: 50%; width: 12px; height: 12px; margin: -6px 0 0 -6px; border-radius: 50%; color: var(--hd-ground-ink, #0b0b0f); background: currentColor; box-shadow: -24px 0 currentColor, 24px 0 currentColor; opacity: 0; animation: hd-dots .7s ease-in-out .6s infinite alternate; }
25
+ body[data-hd-ready]::before, body[data-hd-ready]::after { opacity: 0; animation: none; }
26
+ /* The overlay (embedded, ?thumb, or data-layout="overlay"): the stage fills the window. */
27
+ :is([data-hd-frame], [data-layout="overlay"]) body::before { inset: 0; width: min(100vw, 100dvh * 16 / 9); height: min(100dvh, 100vw * 9 / 16); border-radius: calc(min(100vw, 100dvh * 16 / 9) * .01125); box-shadow: none; }
28
+ [data-layout="overlay"] body:not([data-hd-ready]) { background: var(--hd-backdrop, #16161c); }
29
+ [data-hd-frame] body:not([data-hd-ready]) { background: transparent; }
30
+ [data-hd-frame] body::before { border-radius: 0; }
31
+ /* Phones: the card fills the space between a 52px masthead and the dock, 8px in. */
32
+ @media (max-width: 650px) {
33
+ body::before { inset: 52px 8px calc(56px + env(safe-area-inset-bottom)); width: auto; height: auto; border-radius: 14px; }
34
+ :is([data-hd-frame], [data-layout="overlay"]) body::before { inset: 0; width: auto; height: auto; border-radius: 0; }
35
+ }
36
+ @media (prefers-reduced-motion: reduce) {
37
+ body::before, body::after { transition: none; }
38
+ body:not([data-hd-ready])::after { animation: none; opacity: .4; }
39
+ body[data-hd-ready] > * { animation: none; }
40
+ }
41
+ @keyframes hd-in { from { opacity: 0; } }
42
+ @keyframes hd-dots { from { opacity: .15; } to { opacity: .6; } }
43
+ @keyframes hd-fail-open { to { visibility: visible; overflow: auto; } }
44
+ </style>
45
+ <link rel="preconnect" href="https://fonts.googleapis.com">
46
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
47
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;700&family=Space+Mono:wght@400;700&display=swap">
48
+ <style>
49
+ /* A narrated deck assembled from the layout library (`deck new --layouts`).
50
+ The hosted player (player/v1) scales the 1600×900 stage, draws the chrome
51
+ and the presenter, and plays the narration. Change the palette and fonts
52
+ here; every layout reads them. */
53
+ :root {
54
+ --ink: #0b0b0f; --paper: #fafaf7; --accent: #ffda1b; --accent-2: #27ceff; --card: #ffffff;
55
+ --font: "Space Grotesk", system-ui, sans-serif; --mono: "Space Mono", ui-monospace, monospace;
56
+ /* The type scale: one display size per slide at most, then title, sub (a title beside a visual), lead, body, label. */
57
+ --t-display: 128px; --t-title: 84px; --t-sub: 56px; --t-lead: 30px; --t-body: 24px; --t-label: 17px;
58
+ --hd-ground: var(--accent); --hd-ground-ink: var(--ink); /* the loading state: the first slide's colours */
59
+ }
60
+ * { box-sizing: border-box; margin: 0; padding: 0; }
61
+ body { color: var(--ink); font-family: var(--font); font-weight: 500; -webkit-font-smoothing: antialiased; --hd-font: var(--font); --hd-mono: var(--mono); }
62
+ .slide { display: flex; flex-direction: column; gap: 24px; padding: 64px 72px 56px; background: var(--paper); color: var(--ink); --hd-speech: #f3c9dc; }
63
+ .slide.on-accent { background: var(--accent); --hd-speech: var(--card); }
64
+ .slide.on-ink { background: var(--ink); color: var(--paper); --hd-speech: #d9ccff; }
65
+ .kicker { font: var(--t-label) var(--mono); letter-spacing: .16em; text-transform: uppercase; }
66
+ .tt { font-weight: 700; font-size: var(--t-title); line-height: .95; letter-spacing: -.045em; text-wrap: balance; }
67
+ .lead { font-size: var(--t-lead); line-height: 1.3; max-width: 28em; text-wrap: pretty; }
68
+ .muted { opacity: .65; }
69
+ /* The presenter glides to each slide's slot: bottom-right by default,
70
+ bottom-left on .slot-left, top-right on .slot-top. */
71
+ .narration-slot { position: absolute; right: 56px; bottom: 64px; }
72
+ .slot-left .narration-slot { right: auto; left: 56px; }
73
+ .slot-top .narration-slot { top: 96px; bottom: auto; }
74
+ .reveal { opacity: 0; transform: translateY(12px); transition: opacity .5s, transform .5s; }
75
+ .reveal.is-revealed { opacity: 1; transform: none; }
76
+ /* Phones: the slide is the 374×736 card; the presenter is a small head in
77
+ the corner, so keep the bottom 170px clear. */
78
+ @media (max-width: 650px) {
79
+ :root { --t-display: 54px; --t-title: 38px; --t-sub: 28px; --t-lead: 17px; --t-body: 15px; --t-label: 12px; }
80
+ .slide { padding: 28px 20px 170px; gap: 14px; }
81
+ .narration-slot { right: 12px; left: 12px; bottom: 64px; }
82
+ }
83
+ </style>
84
+ <!-- LAYOUT STYLES -->
85
+ </head>
86
+ <body>
87
+
88
+ <!-- One <section class="slide" id> per slide, one idea each, from
89
+ packages/decks-cli/templates/layouts/. The id joins the slide to its
90
+ narration (`## <id>` in script.md); ids inside a slide start with its id
91
+ so the [go]/[look] cues in script.md stay unique. Every slide needs a
92
+ data-note (where its facts come from) and, over 28 characters, a
93
+ data-title-mobile on its title. -->
94
+ <!-- SLIDES -->
95
+
96
+ <!-- `hyperdecks narrate decks/__SLUG__` fills data-manifest with the manifest
97
+ URL it writes. data-presenter: head (live Minimoji, needs data-avatar),
98
+ static (data-presenter-image) or none (captions only). -->
99
+ <script type="module" src="https://decks.hypersocial.ai/player/v1/player.js"
100
+ data-manifest="" data-stage="1600x900" data-presenter="none" data-presenter-style="__PRESENTER_STYLE__"></script>
101
+ </body>
102
+ </html>
@@ -0,0 +1,39 @@
1
+ <!--
2
+ layout: chart
3
+ summary: A chart of real numbers with its takeaway in one line; the presenter beside it.
4
+ use: a trend or a comparison in numbers. Draw it with `hyperdecks chart` (never a generated picture of a chart) and name the data's source.
5
+ slot: bottom-right
6
+ presenter: stage
7
+ cues: [look: #__ID__-chart] when the narration turns to it; [reveal: #__ID__-takeaway] at the point.
8
+ script: Here are the numbers. [look: #__ID__-chart] The last bar is the one to see. [reveal: #__ID__-takeaway]
9
+ -->
10
+ <style>
11
+ .l-chart { display: grid; grid-template-columns: 960px 1fr; grid-template-rows: auto auto 1fr; column-gap: 56px; row-gap: 20px; }
12
+ .l-chart .kicker, .l-chart .tt { grid-column: 1; }
13
+ .l-chart .tt { font-size: var(--t-sub); line-height: 1.02; }
14
+ /* Swap the svg for an <img class="chart"> of your file (hyperdecks chart). */
15
+ .l-chart .chart { grid-column: 1; width: 100%; height: 100%; min-height: 0; object-fit: contain; object-position: left bottom; }
16
+ .l-chart .chart .bar { fill: color-mix(in srgb, var(--ink) 18%, transparent); }
17
+ .l-chart .chart .bar.hi { fill: var(--accent); stroke: var(--ink); stroke-width: 3; }
18
+ .l-chart .chart text { font: 22px var(--mono); fill: currentColor; }
19
+ .l-chart .chart .v { font: 700 30px var(--font); }
20
+ .l-chart .takeaway { grid-column: 2; grid-row: 1 / 3; align-self: start; font-size: var(--t-lead); line-height: 1.25; font-weight: 700; padding-left: 20px; border-left: 6px solid var(--accent); }
21
+ @media (max-width: 650px) {
22
+ .l-chart { display: flex; flex-direction: column; }
23
+ .l-chart .chart { height: 210px; flex: none; }
24
+ .l-chart .takeaway { padding-left: 12px; border-left-width: 4px; }
25
+ }
26
+ </style>
27
+ <section class="slide l-chart" id="__ID__" data-chapter="The numbers" data-note="Data: name the source of the numbers and the period they cover.">
28
+ <div class="kicker">The numbers</div>
29
+ <h2 class="tt" data-title-mobile="The trend.">The trend, in one line.</h2>
30
+ <svg class="chart" id="__ID__-chart" viewBox="0 0 960 470" role="img" aria-label="Bar chart placeholder: four periods, the last highest">
31
+ <line x1="0" y1="420" x2="960" y2="420" stroke="currentColor" stroke-opacity=".25" stroke-width="2"/>
32
+ <rect class="bar" x="20" y="300" width="180" height="120" rx="10"/><text class="v" x="110" y="285" text-anchor="middle">12</text><text x="110" y="458" text-anchor="middle">Q1</text>
33
+ <rect class="bar" x="260" y="250" width="180" height="170" rx="10"/><text class="v" x="350" y="235" text-anchor="middle">17</text><text x="350" y="458" text-anchor="middle">Q2</text>
34
+ <rect class="bar" x="500" y="190" width="180" height="230" rx="10"/><text class="v" x="590" y="175" text-anchor="middle">23</text><text x="590" y="458" text-anchor="middle">Q3</text>
35
+ <rect class="bar hi" x="740" y="60" width="180" height="360" rx="10"/><text class="v" x="830" y="45" text-anchor="middle">36</text><text x="830" y="458" text-anchor="middle">Q4</text>
36
+ </svg>
37
+ <p class="takeaway reveal" id="__ID__-takeaway">The takeaway, in one sentence a listener could repeat.</p>
38
+ <div class="narration-slot"></div>
39
+ </section>
@@ -0,0 +1,28 @@
1
+ <!--
2
+ layout: closer
3
+ summary: The last slide. One line, the one action to take (a command, a link) and the presenter's goodbye.
4
+ use: the end. One call to action, never three. Label an ask that is not agreed as Proposed.
5
+ slot: bottom-left
6
+ presenter: stage
7
+ cues: [go: #__ID__-cta] at the action, [go: slot] and [wave] at the goodbye.
8
+ script: That's it. [go: #__ID__-cta] Here is the one thing to do next. Thanks [go: slot] for watching! [wave]
9
+ -->
10
+ <style>
11
+ .l-closer .content { flex: 1; display: flex; flex-direction: column; justify-content: center; gap: 32px; padding-left: 500px; }
12
+ .l-closer .tt { font-size: var(--t-display); line-height: .9; }
13
+ .l-closer .cta { display: inline-flex; align-items: center; gap: 16px; width: fit-content; max-width: 100%; padding: 22px 34px; border-radius: 22px; background: var(--accent); color: var(--ink); font: 700 34px var(--mono); letter-spacing: -.01em; text-decoration: none; }
14
+ .l-closer .contact { font: var(--t-label) var(--mono); letter-spacing: .12em; text-transform: uppercase; }
15
+ @media (max-width: 650px) {
16
+ .l-closer .content { padding-left: 0; justify-content: flex-start; gap: 16px; }
17
+ .l-closer .cta { padding: 12px 16px; border-radius: 14px; font-size: 15px; }
18
+ }
19
+ </style>
20
+ <section class="slide on-ink l-closer slot-left" id="__ID__" data-chapter="Next" data-note="Closing slide: the link or command is the deck's own call to action.">
21
+ <div class="content">
22
+ <h2 class="tt" data-title-mobile="Thanks.">Thanks.</h2>
23
+ <p class="lead">One sentence on the one thing to do next.</p>
24
+ <a class="cta" id="__ID__-cta" href="https://example.com" target="_blank" rel="noopener">example.com →</a>
25
+ <div class="contact muted">your@email · your.site</div>
26
+ </div>
27
+ <div class="narration-slot"></div>
28
+ </section>
@@ -0,0 +1,33 @@
1
+ <!--
2
+ layout: compare
3
+ summary: Before and after (or them and us) side by side, three short lines each.
4
+ use: the change your idea makes. Keep the rows parallel so each line has its counterpart.
5
+ slot: bottom-right
6
+ presenter: stage
7
+ cues: [look: #__ID__-before] on the old way, [go: #__ID__-after] on the new; [go: slot] to come home.
8
+ script: Today it looks like this. [look: #__ID__-before] With us, [go: #__ID__-after] it looks like this. [go: slot]
9
+ -->
10
+ <style>
11
+ .l-compare .cols { flex: 1; display: grid; grid-template-columns: 1fr 1fr; gap: 24px; max-width: 960px; align-content: center; }
12
+ .l-compare .col { border-radius: 28px; padding: 32px 34px; background: color-mix(in srgb, var(--ink) 6%, transparent); }
13
+ .l-compare .col.is-after { background: var(--ink); color: var(--paper); }
14
+ .l-compare .label { font: 700 var(--t-label) var(--mono); letter-spacing: .16em; text-transform: uppercase; margin-bottom: 20px; }
15
+ .l-compare .is-after .label { color: var(--accent); }
16
+ .l-compare li { list-style: none; font-size: var(--t-body); line-height: 1.3; padding: 14px 0; border-top: 1px solid color-mix(in srgb, currentColor 18%, transparent); }
17
+ .l-compare .tt { max-width: 11em; }
18
+ @media (max-width: 650px) {
19
+ .l-compare .cols { flex: none; grid-template-columns: 1fr; gap: 10px; }
20
+ .l-compare .col { border-radius: 16px; padding: 12px 16px; }
21
+ .l-compare .label { margin-bottom: 4px; }
22
+ .l-compare li { padding: 5px 0; }
23
+ }
24
+ </style>
25
+ <section class="slide l-compare" id="__ID__" data-chapter="Before and after" data-note="Source: name where each side's facts come from.">
26
+ <div class="kicker">Before and after</div>
27
+ <h2 class="tt" data-title-mobile="Before, after.">What changes.</h2>
28
+ <div class="cols">
29
+ <div class="col" id="__ID__-before"><div class="label">Before</div><ul><li>The old way, step one</li><li>Where it hurts</li><li>What it costs</li></ul></div>
30
+ <div class="col is-after" id="__ID__-after"><div class="label">After</div><ul><li>The new way, step one</li><li>What stops hurting</li><li>What it saves</li></ul></div>
31
+ </div>
32
+ <div class="narration-slot"></div>
33
+ </section>
@@ -0,0 +1,35 @@
1
+ <!--
2
+ layout: features
3
+ summary: Three features side by side, an icon, a name and one line each; the presenter flies under each card.
4
+ use: what it does, the three reasons, the three parts. Fill the icons with `hyperdecks icons` in the deck's palette.
5
+ slot: bottom-right
6
+ presenter: stage
7
+ cues: [go: #__ID__-f1], [go: #__ID__-f2], [go: #__ID__-f3] at the word that starts each, then [go: slot].
8
+ script: It does three things. First, [go: #__ID__-f1] it writes. Then [go: #__ID__-f2] it speaks. Last, [go: #__ID__-f3] it sends. [go: slot]
9
+ -->
10
+ <style>
11
+ .l-features .cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; margin-top: 20px; }
12
+ .l-features .card { list-style: none; display: flex; flex-direction: column; gap: 10px; padding: 28px 30px; border-radius: 28px; background: var(--card); box-shadow: 0 0 0 1px color-mix(in srgb, var(--ink) 10%, transparent); }
13
+ /* Swap each svg for an <img class="icon"> of your file (hyperdecks icons). */
14
+ .l-features .icon { width: 64px; height: 64px; object-fit: contain; color: var(--ink); }
15
+ .l-features .icon .fill { fill: var(--accent); }
16
+ .l-features b { font-size: 32px; letter-spacing: -.02em; line-height: 1.05; }
17
+ .l-features p { font-size: 20px; line-height: 1.3; opacity: .7; }
18
+ @media (max-width: 650px) {
19
+ .l-features .cards { grid-template-columns: 1fr; gap: 8px; margin-top: 4px; }
20
+ .l-features .card { display: grid; grid-template-columns: 40px 1fr; column-gap: 12px; row-gap: 2px; padding: 12px 14px; border-radius: 16px; }
21
+ .l-features .icon { width: 40px; height: 40px; grid-row: span 2; }
22
+ .l-features b { font-size: 18px; }
23
+ .l-features p { font-size: 13px; }
24
+ }
25
+ </style>
26
+ <section class="slide l-features" id="__ID__" data-chapter="What it does" data-note="Source: name the spec, docs or repo each feature is described in.">
27
+ <div class="kicker">What it does</div>
28
+ <h2 class="tt" data-title-mobile="Three things.">It does three things.</h2>
29
+ <ul class="cards">
30
+ <li class="card" id="__ID__-f1"><svg class="icon" viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect class="fill" x="10" y="10" width="44" height="44" rx="12" stroke="none"/><path d="M22 42l4-12 16-16 8 8-16 16-12 4z"/></svg><b>It writes</b><p>One line on the first feature.</p></li>
31
+ <li class="card" id="__ID__-f2"><svg class="icon" viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle class="fill" cx="32" cy="32" r="24" stroke="none"/><rect x="26" y="14" width="12" height="24" rx="6"/><path d="M18 32a14 14 0 0 0 28 0M32 46v6"/></svg><b>It speaks</b><p>One line on the second feature.</p></li>
32
+ <li class="card" id="__ID__-f3"><svg class="icon" viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path class="fill" d="M10 54L32 8l22 46z" stroke="none"/><path d="M14 32l38-16-12 36-8-14z"/></svg><b>It sends</b><p>One line on the third feature.</p></li>
33
+ </ul>
34
+ <div class="narration-slot"></div>
35
+ </section>
@@ -0,0 +1,35 @@
1
+ <!--
2
+ layout: hero
3
+ summary: The opening. One display-size line, a visual on the right, the presenter's hello.
4
+ use: the first slide; a section opener. Pair with a generated illustration (gen-image) in .art.
5
+ slot: bottom-right
6
+ presenter: stage
7
+ cues: [wave] at the hello, [look: h1] when the narration says the title.
8
+ script: Hi, [wave] this is __TITLE__. [look: h1] One idea, in a few minutes.
9
+ -->
10
+ <style>
11
+ .l-hero { overflow: hidden; }
12
+ .l-hero .content { flex: 1; display: flex; flex-direction: column; justify-content: center; gap: 28px; max-width: 980px; position: relative; z-index: 1; }
13
+ .l-hero .tt { font-size: var(--t-display); line-height: .9; }
14
+ /* Swap the div for an <img class="art"> of your file (gen-image, wide). */
15
+ .l-hero .art { position: absolute; top: 0; right: 0; width: 820px; height: 100%; object-fit: cover; -webkit-mask-image: linear-gradient(to right, transparent, #000 32%); mask-image: linear-gradient(to right, transparent, #000 32%); }
16
+ .l-hero div.art { background:
17
+ radial-gradient(circle at 64% 36%, var(--paper) 0 168px, transparent 169px),
18
+ radial-gradient(circle at 64% 36%, var(--ink) 0 176px, transparent 177px),
19
+ radial-gradient(circle at 34% 78%, var(--accent-2) 0 92px, transparent 93px),
20
+ repeating-linear-gradient(135deg, color-mix(in srgb, var(--ink) 10%, transparent) 0 2px, transparent 2px 22px); }
21
+ @media (max-width: 650px) {
22
+ .l-hero .content { justify-content: flex-start; gap: 16px; padding-top: 200px; }
23
+ .l-hero .art { width: 100%; height: 230px; -webkit-mask-image: linear-gradient(to bottom, #000 60%, transparent); mask-image: linear-gradient(to bottom, #000 60%, transparent); }
24
+ .l-hero div.art { background-size: 180% 180%; background-position: 70% 30%; }
25
+ }
26
+ </style>
27
+ <section class="slide on-accent l-hero" id="__ID__" data-chapter="Hello" data-note="Title slide: no facts.">
28
+ <div class="art" role="presentation"></div>
29
+ <div class="content">
30
+ <div class="kicker">__SLUG__</div>
31
+ <h1 class="tt" data-title-mobile="__TITLE__">__TITLE__</h1>
32
+ <p class="lead">One sentence on what this deck is about.</p>
33
+ </div>
34
+ <div class="narration-slot"></div>
35
+ </section>
@@ -0,0 +1,29 @@
1
+ <!--
2
+ layout: number
3
+ summary: One big number, what it measures and its source. The number is the slide.
4
+ use: proof, traction, a before/after delta. Never an estimate shown as fact: the source line is required.
5
+ slot: bottom-right
6
+ presenter: stage
7
+ cues: [look: #__ID__-figure] as the narration says the number; [reveal: #__ID__-source] optional.
8
+ script: Here is the number that matters. [look: #__ID__-figure] Three times faster.
9
+ -->
10
+ <style>
11
+ .l-number .content { flex: 1; display: flex; flex-direction: column; justify-content: center; gap: 20px; max-width: 980px; }
12
+ .l-number .figure { font-weight: 700; font-size: 300px; line-height: .82; letter-spacing: -.06em; color: var(--accent); }
13
+ .l-number .figure small { font-size: .45em; letter-spacing: -.02em; margin-left: 8px; }
14
+ .l-number .tt { font-size: var(--t-sub); line-height: 1.02; max-width: 15em; }
15
+ .l-number .source { font: var(--t-label)/1.4 var(--mono); }
16
+ @media (max-width: 650px) {
17
+ .l-number .content { justify-content: flex-start; gap: 12px; }
18
+ .l-number .figure { font-size: 120px; }
19
+ }
20
+ </style>
21
+ <section class="slide on-ink l-number" id="__ID__" data-chapter="The number" data-note="Source: name the dashboard, report or email the number comes from, with its date.">
22
+ <div class="content">
23
+ <div class="kicker">Proof</div>
24
+ <div class="figure" id="__ID__-figure">3<small>×</small></div>
25
+ <h2 class="tt" data-title-mobile="What it measures.">What the number measures, in one line.</h2>
26
+ <p class="source muted" id="__ID__-source">Source: the report, with its date.</p>
27
+ </div>
28
+ <div class="narration-slot"></div>
29
+ </section>