@gtmi/slides-deck
v0.3.0
Published
Framework-agnostic builder for Google Slides batchUpdate requests: units, palette, reusable slide layouts (title, section, feature+screenshot, detail cards, architecture, workflow). Emits chunked request batches you feed to any Slides client (e.g. gws CLI
Keywords
Readme
@gtmi/slides-deck
Framework-agnostic builder for Google Slides presentations.batchUpdate requests.
It knows nothing about how you talk to Google — you hand the emitted request chunks to any
Slides client (the gws CLI, googleapis, etc.). It gives you:
- Units —
inch(),pt(),hexToRgb(), page constants (EMU helpers). - Primitives —
rect,text,bullets(plain strings or bold-lead-in{ lead, body }items),image,RequestSink,chunkRequests. - Layouts —
screenshotCard(faux browser frame),pill,footer,infoCardGrid,compareCards,logoMark. Deck— a high-level, chainable builder with ready-made slide templates:titleSlide,sectionSlide,featureSlide,detailSlide,contentSlide,cardsSlide,contrastSlide,workflowSlide,architectureSlide,compareSlide,quoteSlide,closingSlide.
Why
The Slides API is verbose (every shape = create + style + text requests), uses EMU units,
and batchUpdate is all-or-nothing. This package encapsulates a consistent, professional
design system so decks come out clean instead of default-ugly, and keeps object IDs
deterministic and batches sized safely.
Usage
import { Deck } from '@gtmi/slides-deck';
// manifest maps a screenshot key -> a fetchable image URL (see note below)
const manifest: Record<string, string> = JSON.parse(readFileSync('manifest.json', 'utf8'));
const deck = new Deck({
brand: 'RAMP Architecture & Details · SE Enablement',
resolveImage: (key) => manifest[key],
});
deck
.titleSlide({
kicker: 'TWILIO GTM INNOVATION',
title: 'RAMP',
subtitle: '…',
keywords: ['Voice', 'SMS'],
})
.featureSlide({
eyebrow: 'LAUNCH',
title: 'Launch on Any Channel',
summary: '…',
points: ['…'],
img: '07-launch-demo',
note: 'SE tip',
})
.closingSlide({ kicker: 'START', title: 'Build your demo', points: ['…'] });
// feed each chunk to your Slides client:
for (const requests of deck.build()) {
await slides.presentations.batchUpdate({ presentationId, requestBody: { requests } });
}Images must be publicly fetchable at insert time
Slides fetches createImage.url server-side and needs anonymous access. In orgs that block
public Drive/GCS sharing, use short-lived GCS signed URLs — see the global pi skill
slides-image-upload (~/.pi/agent/skills/slides-image-upload) for the full workaround and
helper scripts.
Design system
16:9 page (10 × 5.625 in). Consistent eyebrow pill + title + summary + bullets, dark footer with brand + page number, and screenshot cards rendered inside a drawn browser frame (never bake chrome into the PNG).
Branding
Deck defaults to TWILIO_2026_PALETTE and DEFAULT_FONT ('Space Grotesk') —
sourced from the authoritative Twilio 2026 corporate slide brand contract (accent red
#EF223A, dark bg #000D25, light bg #FFFFFF, footer blue #058DC7, card/surface fill
#F5F5F5) documented in the twilio-html-slide-builder skill
(twilio-internal/twilio-deck-studio, skills/twilio-html-slide-builder/references/
corporate-template-spec-2026-{dark,light}.md). That skill targets HTML/reveal.js decks;
this package independently implements the same brand contract for the Google Slides API.
Override either per deck:
import { Deck, GENERIC_1_PALETTE } from '@gtmi/slides-deck';
// the original hand-picked palette, if you don't want the real Twilio colors
const deck = new Deck({ brand: '…', palette: GENERIC_1_PALETTE, font: 'Arial' });
// or override a subset of the default palette
const deck2 = new Deck({ brand: '…', palette: { red: '#123456' } });Set brandMarkUrl to a fetchable image URL to draw a brand mark top-right on titleSlide,
sectionSlide, and closingSlide (matches the Twilio theme's "dots" mark placement and
slide-type scope — the brand spec says never on plain content slides). This package ships no
logo asset itself, only the placement convention: createImage needs a fetchable raster URL,
and the only official mark assets are SVG (twilio-dots-white.svg for dark slides,
twilio-dots-dark.svg for light slides — no PNG exists upstream either). If you need the
mark rendered, rasterize one of those SVGs yourself and host it; all of Deck's
mark-eligible slides are dark-background, so the white variant is the one you want.
Richer bullets, comparisons, and quotes
bullets() (and any points/leftPoints slide spec field) accepts either plain strings or
{ lead, body } objects — the lead renders bold, the rest doesn't:
deck.featureSlide({
// ...
points: [{ lead: 'Fast setup —', body: 'go live in minutes' }, 'Plain bullet still works'],
});compareSlide renders side-by-side comparison panels (e.g. "the old way" vs. "with RAMP"),
and quoteSlide renders a large centered testimonial with attribution:
deck
.compareSlide({
eyebrow: 'COMPARE',
title: 'Old way vs. new way',
columns: [
{ heading: 'Before', points: ['Manual setup', 'Days to demo'] },
{ heading: 'With RAMP', points: ['One command', 'Minutes to demo'] },
],
})
.quoteSlide({
quote: 'This changed how we ship demos.',
attributionName: 'Jane Doe',
attributionTitle: 'VP Engineering',
});Text-only content slides (no screenshots)
featureSlide pairs copy with a screenshot; for text-only decks (e.g. a customer
pitch generated from a brief), use contentSlide, cardsSlide, and contrastSlide.
All three are light-theme content slides matching detailSlide/compareSlide:
deck
// full-width bulleted content, with an optional closing footnote
.contentSlide({
eyebrow: 'SITUATION',
title: 'Where you stand today',
summary: 'The context the rest of the deck builds on.',
points: [
{ lead: 'Fragmented stack', body: 'six tools, no shared profile' },
'Manual escalations',
],
footnote: 'You sit one integration away from a unified view.',
})
// 2–3 feature cards, each heading + blurb + capability bullets + tie-in footnote
.cardsSlide({
eyebrow: 'EXPAND',
title: 'The expand path',
cards: [
{
heading: 'Twilio Verify',
blurb: 'Know every caller before they speak.',
points: ['Silent network auth', 'Fraud scoring'],
footnote: 'For Acme, ties directly to their loyalty program.',
},
],
})
// two verdict panels (today vs. could-be); later columns get the red accent
.contrastSlide({
eyebrow: 'THE GAP',
title: 'Anonymous vs. recognised',
columns: [
{ tag: 'Today', verdict: 'Anonymous', note: 'Callers are strangers to your agents.' },
{ tag: 'Could be', verdict: 'Recognised', note: 'You greet them by name, in context.' },
],
});Scripts
pnpm --filter @gtmi/slides-deck check-types
pnpm --filter @gtmi/slides-deck test
pnpm --filter @gtmi/slides-deck lint