@classic-homes/slides 0.2.0
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/README.md +129 -0
- package/dist/chunk-CKVKX2ZD.js +76 -0
- package/dist/chunk-CKVKX2ZD.js.map +1 -0
- package/dist/chunk-TM3TNVHB.js +1520 -0
- package/dist/chunk-TM3TNVHB.js.map +1 -0
- package/dist/chunk-TXNQKBWJ.js +69 -0
- package/dist/chunk-TXNQKBWJ.js.map +1 -0
- package/dist/chunk-VONVGYRL.js +52 -0
- package/dist/chunk-VONVGYRL.js.map +1 -0
- package/dist/cli.js +167 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +149 -0
- package/dist/index.js +190 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-5R6YD3QW.js +320 -0
- package/dist/mcp-5R6YD3QW.js.map +1 -0
- package/dist/node.d.ts +25 -0
- package/dist/node.js +19 -0
- package/dist/node.js.map +1 -0
- package/dist/render-CZ4g2fa9.d.ts +136 -0
- package/package.json +69 -0
- package/starters/basic.yaml +69 -0
- package/starters/journey.yaml +85 -0
package/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# @classic-homes/slides
|
|
2
|
+
|
|
3
|
+
Branded Classic Homes presentations from a simple deck file. Pick a layout per
|
|
4
|
+
slide, fill in the words and photos, and get one self-contained HTML file that
|
|
5
|
+
opens in any browser: brand fonts, colours and presenter tools included, no
|
|
6
|
+
internet needed.
|
|
7
|
+
|
|
8
|
+
Decks are data, layouts own the styling. There is no way to set colours, fonts
|
|
9
|
+
or sizes on a slide, which is what keeps every deck on-brand whether a person
|
|
10
|
+
or an AI agent wrote it.
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx classic-slides new decks/closing-day.yaml # basic starter
|
|
16
|
+
npx classic-slides new decks/orientation.yaml --starter journey
|
|
17
|
+
npx classic-slides dev decks/closing-day.yaml # live preview, reloads on save
|
|
18
|
+
npx classic-slides build decks/closing-day.yaml # → decks/closing-day.html
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Present: open the HTML, use arrow keys or Space. **N** shows speaker notes,
|
|
22
|
+
**F** goes fullscreen, `#slide-7` in the URL jumps to a slide. Print to PDF
|
|
23
|
+
gives one 16:9 page per slide.
|
|
24
|
+
|
|
25
|
+
## A deck file
|
|
26
|
+
|
|
27
|
+
```yaml
|
|
28
|
+
title: Closing Day
|
|
29
|
+
description: What to bring and expect at your closing appointment.
|
|
30
|
+
|
|
31
|
+
slides:
|
|
32
|
+
- layout: title
|
|
33
|
+
kicker: New Homeowner
|
|
34
|
+
title: Closing day, *simplified*
|
|
35
|
+
lead: Everything you need for a smooth signing.
|
|
36
|
+
|
|
37
|
+
- layout: bullets
|
|
38
|
+
title: Bring these with you
|
|
39
|
+
marker: check
|
|
40
|
+
items:
|
|
41
|
+
- Photo ID for everyone on the loan
|
|
42
|
+
- text: Certified funds
|
|
43
|
+
sub: Never wire money based on an email — call to confirm
|
|
44
|
+
notes: |
|
|
45
|
+
Stress wire fraud. Title companies never change wiring instructions by email.
|
|
46
|
+
|
|
47
|
+
- layout: split
|
|
48
|
+
title: Where to go
|
|
49
|
+
groups:
|
|
50
|
+
- items: [Arrive 10 minutes early, Plan for about an hour]
|
|
51
|
+
images:
|
|
52
|
+
- { src: images/title-office.jpg, alt: Title company front entrance }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Images are paths relative to the deck file (or https URLs); they are inlined
|
|
56
|
+
at build time. Keep photos around 2000px wide — `build` warns about large ones.
|
|
57
|
+
|
|
58
|
+
Inline text supports `**bold**`, `*highlight*` (brand accent), `` `code` `` and
|
|
59
|
+
`[links](https://…)`. HTML is always escaped.
|
|
60
|
+
|
|
61
|
+
## Layouts
|
|
62
|
+
|
|
63
|
+
Run `classic-slides layouts` for the list, `classic-slides layouts <name>` for
|
|
64
|
+
every field plus an example.
|
|
65
|
+
|
|
66
|
+
| Layout | Use for |
|
|
67
|
+
| ------------- | ----------------------------------------------------- |
|
|
68
|
+
| `title` | First slide |
|
|
69
|
+
| `section` | Chapter divider on a solid colour, optional photo row |
|
|
70
|
+
| `statement` | One big idea, a transition, a closing line |
|
|
71
|
+
| `bullets` | Short checklist, one or two columns |
|
|
72
|
+
| `split` | Text beside 1–4 photos, optional callout badge |
|
|
73
|
+
| `cards` | 3–8 peers: services, options, people |
|
|
74
|
+
| `stats` | 2–6 numbers worth remembering |
|
|
75
|
+
| `comparison` | 2–3 columns side by side |
|
|
76
|
+
| `journey` | Numbered agenda map (uses `journey.steps`) |
|
|
77
|
+
| `photos` | A grid of photos: team, products, homes |
|
|
78
|
+
| `image` | One big image or diagram |
|
|
79
|
+
| `walkthrough` | Ordered stages, one slide each with a progress rail |
|
|
80
|
+
| `quote` | A testimonial or pull quote |
|
|
81
|
+
| `qr` | Send people to a URL with a scannable code |
|
|
82
|
+
| `contacts` | Who to call |
|
|
83
|
+
|
|
84
|
+
Every slide also accepts `track` (value/red, build/teal, neutral/navy),
|
|
85
|
+
`background` (paper or a solid brand colour), `notes`, `time`, `id`, and — in
|
|
86
|
+
decks with `journey.steps` — `step`, `stepDone` and `showJourney`.
|
|
87
|
+
|
|
88
|
+
`classic-slides guide` prints the writing rules; `classic-slides validate`
|
|
89
|
+
flags errors and crowded slides.
|
|
90
|
+
|
|
91
|
+
## With an AI agent (MCP)
|
|
92
|
+
|
|
93
|
+
The package ships an MCP server that edits deck files slide by slide and
|
|
94
|
+
validates before every save, so an agent cannot leave a deck broken.
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
claude mcp add classic-slides -- npx -y @classic-homes/slides mcp --root ./decks
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Then ask: _"Build a 10-slide deck in closing-day.yaml for first-time buyers
|
|
101
|
+
about closing day, then build it."_
|
|
102
|
+
|
|
103
|
+
Tools: `list_layouts`, `get_layout`, `get_authoring_guide`, `create_deck`,
|
|
104
|
+
`read_deck`, `update_deck`, `add_slide`, `add_slides`, `update_slide`,
|
|
105
|
+
`move_slide`, `delete_slide`, `validate_deck`, `build_deck`. Prompt:
|
|
106
|
+
`build_deck_from_outline`. File access is confined to `--root` (default: the
|
|
107
|
+
working directory).
|
|
108
|
+
|
|
109
|
+
## As a library
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { renderDeck, validateDeck, listLayouts } from '@classic-homes/slides';
|
|
113
|
+
import { buildDeckFile, fileImageResolver } from '@classic-homes/slides/node';
|
|
114
|
+
|
|
115
|
+
const { html } = await renderDeck(deck, { resolveImage: (src) => lookUp(src) });
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The core entry has no Node dependencies (it runs in Workers); `/node` adds
|
|
119
|
+
file-based image loading and `buildDeckFile`.
|
|
120
|
+
|
|
121
|
+
Rendered HTML embeds the deck source (minus image data), so
|
|
122
|
+
`extractDeckSource(html)` recovers the editable deck.
|
|
123
|
+
|
|
124
|
+
## Brand
|
|
125
|
+
|
|
126
|
+
Colours come from `@classic-homes/theme-tokens`. Fonts (Figtree, Spectral) are
|
|
127
|
+
embedded from `assets/fonts`. Styling lives in `src/styles.ts`; add a layout in
|
|
128
|
+
`src/layouts/` and register it in `src/layouts/index.ts` — its example is
|
|
129
|
+
tested automatically.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseDeckSource,
|
|
3
|
+
renderDeck,
|
|
4
|
+
stringifyDeck
|
|
5
|
+
} from "./chunk-TM3TNVHB.js";
|
|
6
|
+
|
|
7
|
+
// src/node/index.ts
|
|
8
|
+
import { readFile, stat, writeFile } from "fs/promises";
|
|
9
|
+
import { dirname, extname, isAbsolute, join, resolve } from "path";
|
|
10
|
+
var MIME = {
|
|
11
|
+
".jpg": "image/jpeg",
|
|
12
|
+
".jpeg": "image/jpeg",
|
|
13
|
+
".png": "image/png",
|
|
14
|
+
".webp": "image/webp",
|
|
15
|
+
".gif": "image/gif",
|
|
16
|
+
".svg": "image/svg+xml",
|
|
17
|
+
".avif": "image/avif"
|
|
18
|
+
};
|
|
19
|
+
var IMAGE_WARN_BYTES = 800 * 1024;
|
|
20
|
+
function fileImageResolver(baseDir, options = {}) {
|
|
21
|
+
const warn = options.onWarning ?? (() => {
|
|
22
|
+
});
|
|
23
|
+
return async (src) => {
|
|
24
|
+
if (src.startsWith("data:")) return src;
|
|
25
|
+
if (/^https?:/.test(src)) {
|
|
26
|
+
if (options.inlineRemote === false) return src;
|
|
27
|
+
const res = await fetch(src);
|
|
28
|
+
if (!res.ok) throw new Error(`Image ${src} returned HTTP ${res.status}`);
|
|
29
|
+
const type2 = res.headers.get("content-type") ?? "application/octet-stream";
|
|
30
|
+
const buf2 = Buffer.from(await res.arrayBuffer());
|
|
31
|
+
if (buf2.length > IMAGE_WARN_BYTES)
|
|
32
|
+
warn(`${src} is ${(buf2.length / 1024).toFixed(0)} KB; consider a smaller copy.`);
|
|
33
|
+
return `data:${type2};base64,${buf2.toString("base64")}`;
|
|
34
|
+
}
|
|
35
|
+
const path = isAbsolute(src) ? src : join(baseDir, src);
|
|
36
|
+
const type = MIME[extname(path).toLowerCase()];
|
|
37
|
+
if (!type)
|
|
38
|
+
throw new Error(`Unsupported image type for "${src}". Use jpg, png, webp, gif, svg or avif.`);
|
|
39
|
+
let buf;
|
|
40
|
+
try {
|
|
41
|
+
buf = await readFile(path);
|
|
42
|
+
} catch {
|
|
43
|
+
throw new Error(`Image not found: "${src}" (looked in ${path})`);
|
|
44
|
+
}
|
|
45
|
+
if (buf.length > IMAGE_WARN_BYTES)
|
|
46
|
+
warn(`${src} is ${(buf.length / 1024).toFixed(0)} KB; consider resizing to ~2000px wide.`);
|
|
47
|
+
return `data:${type};base64,${buf.toString("base64")}`;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async function loadDeckFile(path) {
|
|
51
|
+
return parseDeckSource(await readFile(path, "utf8"));
|
|
52
|
+
}
|
|
53
|
+
async function saveDeckFile(path, deck) {
|
|
54
|
+
const text = path.endsWith(".json") ? JSON.stringify(deck, null, 2) + "\n" : stringifyDeck(deck);
|
|
55
|
+
await writeFile(path, text);
|
|
56
|
+
}
|
|
57
|
+
async function buildDeckFile(path, options = {}) {
|
|
58
|
+
const abs = resolve(path);
|
|
59
|
+
const deck = await loadDeckFile(abs);
|
|
60
|
+
const result = await renderDeck(deck, {
|
|
61
|
+
...options,
|
|
62
|
+
resolveImage: fileImageResolver(dirname(abs), options)
|
|
63
|
+
});
|
|
64
|
+
const out = options.out ? resolve(options.out) : abs.replace(/\.(ya?ml|json)$/i, "") + ".html";
|
|
65
|
+
await writeFile(out, result.html);
|
|
66
|
+
return { ...result, out, bytes: (await stat(out)).size };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export {
|
|
70
|
+
IMAGE_WARN_BYTES,
|
|
71
|
+
fileImageResolver,
|
|
72
|
+
loadDeckFile,
|
|
73
|
+
saveDeckFile,
|
|
74
|
+
buildDeckFile
|
|
75
|
+
};
|
|
76
|
+
//# sourceMappingURL=chunk-CKVKX2ZD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/node/index.ts"],"sourcesContent":["/** Node-only helpers: read deck files, inline local images, write HTML. */\nimport { readFile, stat, writeFile } from 'node:fs/promises';\nimport { dirname, extname, isAbsolute, join, resolve } from 'node:path';\nimport { parseDeckSource, stringifyDeck } from '../parse.js';\nimport {\n renderDeck,\n type ImageResolver,\n type RenderOptions,\n type RenderResult,\n} from '../render.js';\n\nconst MIME: Record<string, string> = {\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.png': 'image/png',\n '.webp': 'image/webp',\n '.gif': 'image/gif',\n '.svg': 'image/svg+xml',\n '.avif': 'image/avif',\n};\n\n/** Largest single image we inline without warning. Big photos bloat the deck file. */\nexport const IMAGE_WARN_BYTES = 800 * 1024;\n\nexport interface FileResolverOptions {\n /** Fetch https images and inline them so the deck works offline. Default true. */\n inlineRemote?: boolean;\n onWarning?: (message: string) => void;\n}\n\n/** Resolves image paths relative to the deck file and inlines them as data URIs. */\nexport function fileImageResolver(\n baseDir: string,\n options: FileResolverOptions = {}\n): ImageResolver {\n const warn = options.onWarning ?? (() => {});\n return async (src) => {\n if (src.startsWith('data:')) return src;\n if (/^https?:/.test(src)) {\n if (options.inlineRemote === false) return src;\n const res = await fetch(src);\n if (!res.ok) throw new Error(`Image ${src} returned HTTP ${res.status}`);\n const type = res.headers.get('content-type') ?? 'application/octet-stream';\n const buf = Buffer.from(await res.arrayBuffer());\n if (buf.length > IMAGE_WARN_BYTES)\n warn(`${src} is ${(buf.length / 1024).toFixed(0)} KB; consider a smaller copy.`);\n return `data:${type};base64,${buf.toString('base64')}`;\n }\n const path = isAbsolute(src) ? src : join(baseDir, src);\n const type = MIME[extname(path).toLowerCase()];\n if (!type)\n throw new Error(`Unsupported image type for \"${src}\". Use jpg, png, webp, gif, svg or avif.`);\n let buf: Buffer;\n try {\n buf = await readFile(path);\n } catch {\n throw new Error(`Image not found: \"${src}\" (looked in ${path})`);\n }\n if (buf.length > IMAGE_WARN_BYTES)\n warn(`${src} is ${(buf.length / 1024).toFixed(0)} KB; consider resizing to ~2000px wide.`);\n return `data:${type};base64,${buf.toString('base64')}`;\n };\n}\n\nexport async function loadDeckFile(path: string): Promise<unknown> {\n return parseDeckSource(await readFile(path, 'utf8'));\n}\n\nexport async function saveDeckFile(path: string, deck: unknown): Promise<void> {\n const text = path.endsWith('.json') ? JSON.stringify(deck, null, 2) + '\\n' : stringifyDeck(deck);\n await writeFile(path, text);\n}\n\nexport interface BuildOptions extends Omit<RenderOptions, 'resolveImage'>, FileResolverOptions {\n out?: string;\n}\n\n/** Renders a deck file to a self-contained HTML file next to it (or at `out`). */\nexport async function buildDeckFile(\n path: string,\n options: BuildOptions = {}\n): Promise<RenderResult & { out: string; bytes: number }> {\n const abs = resolve(path);\n const deck = await loadDeckFile(abs);\n const result = await renderDeck(deck, {\n ...options,\n resolveImage: fileImageResolver(dirname(abs), options),\n });\n const out = options.out ? resolve(options.out) : abs.replace(/\\.(ya?ml|json)$/i, '') + '.html';\n await writeFile(out, result.html);\n return { ...result, out, bytes: (await stat(out)).size };\n}\n\nexport { renderDeck };\n"],"mappings":";;;;;;;AACA,SAAS,UAAU,MAAM,iBAAiB;AAC1C,SAAS,SAAS,SAAS,YAAY,MAAM,eAAe;AAS5D,IAAM,OAA+B;AAAA,EACnC,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,SAAS;AACX;AAGO,IAAM,mBAAmB,MAAM;AAS/B,SAAS,kBACd,SACA,UAA+B,CAAC,GACjB;AACf,QAAM,OAAO,QAAQ,cAAc,MAAM;AAAA,EAAC;AAC1C,SAAO,OAAO,QAAQ;AACpB,QAAI,IAAI,WAAW,OAAO,EAAG,QAAO;AACpC,QAAI,WAAW,KAAK,GAAG,GAAG;AACxB,UAAI,QAAQ,iBAAiB,MAAO,QAAO;AAC3C,YAAM,MAAM,MAAM,MAAM,GAAG;AAC3B,UAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,SAAS,GAAG,kBAAkB,IAAI,MAAM,EAAE;AACvE,YAAMA,QAAO,IAAI,QAAQ,IAAI,cAAc,KAAK;AAChD,YAAMC,OAAM,OAAO,KAAK,MAAM,IAAI,YAAY,CAAC;AAC/C,UAAIA,KAAI,SAAS;AACf,aAAK,GAAG,GAAG,QAAQA,KAAI,SAAS,MAAM,QAAQ,CAAC,CAAC,+BAA+B;AACjF,aAAO,QAAQD,KAAI,WAAWC,KAAI,SAAS,QAAQ,CAAC;AAAA,IACtD;AACA,UAAM,OAAO,WAAW,GAAG,IAAI,MAAM,KAAK,SAAS,GAAG;AACtD,UAAM,OAAO,KAAK,QAAQ,IAAI,EAAE,YAAY,CAAC;AAC7C,QAAI,CAAC;AACH,YAAM,IAAI,MAAM,+BAA+B,GAAG,0CAA0C;AAC9F,QAAI;AACJ,QAAI;AACF,YAAM,MAAM,SAAS,IAAI;AAAA,IAC3B,QAAQ;AACN,YAAM,IAAI,MAAM,qBAAqB,GAAG,gBAAgB,IAAI,GAAG;AAAA,IACjE;AACA,QAAI,IAAI,SAAS;AACf,WAAK,GAAG,GAAG,QAAQ,IAAI,SAAS,MAAM,QAAQ,CAAC,CAAC,yCAAyC;AAC3F,WAAO,QAAQ,IAAI,WAAW,IAAI,SAAS,QAAQ,CAAC;AAAA,EACtD;AACF;AAEA,eAAsB,aAAa,MAAgC;AACjE,SAAO,gBAAgB,MAAM,SAAS,MAAM,MAAM,CAAC;AACrD;AAEA,eAAsB,aAAa,MAAc,MAA8B;AAC7E,QAAM,OAAO,KAAK,SAAS,OAAO,IAAI,KAAK,UAAU,MAAM,MAAM,CAAC,IAAI,OAAO,cAAc,IAAI;AAC/F,QAAM,UAAU,MAAM,IAAI;AAC5B;AAOA,eAAsB,cACpB,MACA,UAAwB,CAAC,GAC+B;AACxD,QAAM,MAAM,QAAQ,IAAI;AACxB,QAAM,OAAO,MAAM,aAAa,GAAG;AACnC,QAAM,SAAS,MAAM,WAAW,MAAM;AAAA,IACpC,GAAG;AAAA,IACH,cAAc,kBAAkB,QAAQ,GAAG,GAAG,OAAO;AAAA,EACvD,CAAC;AACD,QAAM,MAAM,QAAQ,MAAM,QAAQ,QAAQ,GAAG,IAAI,IAAI,QAAQ,oBAAoB,EAAE,IAAI;AACvF,QAAM,UAAU,KAAK,OAAO,IAAI;AAChC,SAAO,EAAE,GAAG,QAAQ,KAAK,QAAQ,MAAM,KAAK,GAAG,GAAG,KAAK;AACzD;","names":["type","buf"]}
|