@classic-homes/slides
Branded Classic Homes presentations from a simple deck file. Pick a layout per slide, fill in the words and photos, and get one self-contained HTML file that opens in any browser: brand fonts, colours and presenter tools included, no internet needed.
Decks are data, layouts own the styling. There is no way to set colours, fonts or sizes on a slide, which is what keeps every deck on-brand whether a person or an AI agent wrote it.
Quick start
npx classic-slides new decks/closing-day.yaml # basic starter
npx classic-slides new decks/orientation.yaml --starter journey
npx classic-slides dev decks/closing-day.yaml # live preview, reloads on save
npx classic-slides build decks/closing-day.yaml # → decks/closing-day.html
npx classic-slides export decks/closing-day.yaml # → decks/closing-day.deck.json
Present: open the HTML, use arrow keys or Space. N shows speaker notes,
F goes fullscreen, #slide-7 in the URL jumps to a slide. Print to PDF
gives one 16:9 page per slide.
A deck file
title: Closing Day
description: What to bring and expect at your closing appointment.
slides:
- layout: title
kicker: New Homeowner
title: Closing day, *simplified*
lead: Everything you need for a smooth signing.
- layout: bullets
title: Bring these with you
marker: check
items:
- Photo ID for everyone on the loan
- text: Certified funds
sub: Never wire money based on an email — call to confirm
notes: |
Stress wire fraud. Title companies never change wiring instructions by email.
- layout: split
title: Where to go
groups:
- items: [Arrive 10 minutes early, Plan for about an hour]
images:
- { src: images/title-office.jpg, alt: Title company front entrance }
Images are paths relative to the deck file (or https URLs); they are inlined
at build time. Keep photos around 2000px wide — build warns about large ones.
Inline text supports **bold**, *highlight* (brand accent), `code` and
[links](https://…). HTML is always escaped.
Layouts
Run classic-slides layouts for the list, classic-slides layouts <name> for
every field plus an example.
| Group | Layouts |
|---|---|
| Openers & dividers | title, section, statement, journey |
| Text | bullets, cards, comparison, quote |
| Data | stats (with trends + sparklines), chart, table |
| Diagrams | flow, timeline, cycle, hierarchy, matrix, tiers (funnel / pyramid) |
| Media | split, photos, image (with numbered hotspots), compare (before/after), walkthrough, video (YouTube/Vimeo), embed (any https page) |
| People & places | qr, contacts |
Every slide also accepts track (value/red, build/teal, neutral/navy),
background (paper or a solid brand colour), backgroundImage + overlay,
notes, time, id, hidden, and — in decks with journey.steps — step,
stepDone and showJourney. bullets, cards, flow and timeline take
build: true to reveal one item per click.
Charts
- layout: chart
title: Closings *doubled* since spring
chart:
type: column # bar, stacked-*, percent-*, line, area, combo, pie, donut, treemap, funnel, waterfall, gauge, ring, bullet, dumbbell, heatmap, sankey, pictogram, scatter, radar
labels: [Q1, Q2, Q3, Q4]
series: [{ name: Closings, values: [42, 61, 88, 94] }]
unit: homes
highlight: Q4
insights: [Q4 best quarter on record]
source: Sales ops
Charts are rendered to static SVG with ECharts (server-side rendering, themed
from @classic-homes/charts-core), so they work offline and in print. ECharts
is loaded only when a deck has charts, and /core leaves it out entirely (see
As a library).
Video and embeds
video takes YouTube/Vimeo links (privacy-enhanced players); embed takes any
https page (maps, dashboards, 3D tours, forms) in a sandboxed frame. Both load
only while their slide is shown, and fall back to a poster/screenshot with a QR
code offline, in print and in PDF.
Brand profiles and deck settings
brand:
profile: pentrust # admin-managed profile; default classic-homes
name: PenTrust Lending # optional overrides: name, tagline, logo, logoOnDark, favicon, footer
settings:
slideNumbers: true
logo: all # title | all | none
confidential: Internal
autoAdvance: 15 # kiosk mode (also ?kiosk=15 on the URL)
loop: true
Profiles (BrandProfile) carry name, logos, favicon, footer and an optional
palette; validateBrandProfile() enforces WCAG AA contrast. Pass them to
renderDeck(deck, { brands }) / validateDeck(deck, { brands }). Authors can
override words and images, never colours.
Presenting
Arrow keys / Space advance (and step through reveals) · N notes · P
presenter window (current + next slide, notes, timer, clock) · O overview ·
F fullscreen · #slide-7 deep links · ?kiosk=15 loops unattended. Print
to PDF gives one 16:9 page per slide with media fallbacks.
With an AI agent (MCP)
The package ships an MCP server that edits deck files slide by slide and
validates before every save, so an agent cannot leave a deck broken. It needs
@modelcontextprotocol/sdk (an optional peer, so apps that only render decks
don't install it).
claude mcp add classic-slides -- npx -y -p @classic-homes/slides -p @modelcontextprotocol/sdk classic-slides mcp --root ./decks
Then ask: "Build a 10-slide deck in closing-day.yaml for first-time buyers about closing day, then build it."
Tools: list_layouts, get_layout, get_authoring_guide, create_deck,
read_deck, update_deck, add_slide, add_slides, update_slide,
move_slide, delete_slide, validate_deck, build_deck. Prompt:
build_deck_from_outline. File access is confined to --root (default: the
working directory).
As a library
import { renderDeck, validateDeck, listLayouts } from '@classic-homes/slides';
import { buildDeckFile, fileImageResolver } from '@classic-homes/slides/node';
const { html } = await renderDeck(deck, { resolveImage: (src) => lookUp(src) });
The main entry has no Node dependencies (it runs in Workers and browsers);
/node adds file-based image loading and buildDeckFile.
/core is the main entry without ECharts (about 650 KB minified), for bundles
that must stay small, such as a Cloudflare Worker. Its renderDeck draws charts
with the renderCharts option you pass: a ChartSvgRenderer that gets every
chart in the deck in one call ({ spec, width, height, palette?, title? },
plain data that survives RPC) and returns their SVGs in the same order. Run
renderChartSvgs from the main entry wherever ECharts can live (another Worker,
a server) and return its output. A deck with charts and no renderer throws.
import { renderDeck } from '@classic-homes/slides/core';
const { html } = await renderDeck(deck, {
renderCharts: (charts) => env.CHARTS.renderSlideChartSvgs(charts),
});
``` For editors,
`layoutEditorInfo()` / `deckEditorFields()` describe every field as a widget
(with `hint`s for chart, grid, url, video-url and point pickers),
`blankSlide(layout)` gives a valid starting slide, and `renderDeck(deck,
{ includeHidden: true, initialSlide })` plus the `postMessage` API drive a
live preview. Fields flagged `advanced` (slide id, speaking time, accent
colour) belong behind an "Advanced" section.
Click-to-edit: render the preview with `editorMarkers: true` and, inside an
iframe, clicking a title, bullet or picture posts
`{ type: 'classic-slides:select', index, path }` to the parent (`index` is the
deck slide, `path` the field such as `['items', 2, 'text']`, or `null` for the
slide itself). Send `{ type: 'classic-slides:highlight', index, path }` to
outline the field being edited. Never turn it on for shared or exported decks.
`layoutGalleryDeck()` returns one deck with every layout's starting slide, for
rendering a layout picker's thumbnails in one pass.
Rendered HTML embeds the deck source (minus image data), so
`extractDeckSource(html)` recovers the editable deck.
### Portable decks (import / export)
`classic-slides export` (or `exportDeckFile` from `/node`) writes one JSON file
with every image inlined as a `data:` URI — the format to import into a hosted
deck editor. Keep it under ~10 MB by resizing photos to ~2000px wide.
In code, `inlineDeckImages(deck, resolveImage)` does the export side and
`externalizeDeckImages(deck, upload)` the import side: it hands each `data:`
image to `upload` and puts the returned URL in its place. Both build on
`mapDeckImages(deck, fn)`, which finds images written as plain strings too and
returns a copy in the author's shape (no schema defaults added).
## Brand
Colours come from `@classic-homes/theme-tokens`. Fonts (Figtree, Spectral) are
embedded from `assets/fonts`. Both are licensed under the SIL Open Font License
1.1; their copyright notices and the licence text ship as
`assets/fonts/OFL.txt`, which also covers the copies embedded in built decks. Styling lives in `src/styles.ts`; add a layout in
`src/layouts/` and register it in `src/layouts/index.ts` — its example is
tested automatically.