Threads Carousel Generator
Converts a text post into a set of visual carousel slides for Threads, Instagram, LinkedIn, TikTok, Stories, YouTube, or a standalone presentation deck. Composable design system with three independent style axes (font × color × purpose) and 12 slide types.
Invocation
/threads-carousel <post text>
/threads-carousel path/to/post.md
Brand setup with design.md (optional, do this once)
If the user has a design.md (their brand system, e.g. exported from styles.refero.design or built from a site they love), read it and make the carousel match their exact brand before generating slides:
- Open
design.md and pull out the background, text, and accent colors, plus the font.
- In
src/lib/presets.ts, set a surface to those colors (edit the ember "Dark Claude" surface, or add a new one): bg, textColor, accentColor, and a bgGradient for the optional glow. Point the matching ACCENTS entry at the brand's pop color.
- In
src/app/layout.tsx, add the brand font via next/font/google and point a FONT_STYLES entry at it.
- In
src/slides.ts, set DEFAULT_SURFACE, DEFAULT_ACCENT, and DEFAULT_FONT to the brand.
Now every carousel renders in the user's exact brand. If no design.md is provided, use the defaults.
Format presets (choose target platform)
| Preset |
Size |
Platforms |
threads-4x5 (default) |
1080×1350 |
Threads, Instagram feed (portrait) |
instagram-square |
1080×1080 |
Instagram, Facebook, LinkedIn feed |
linkedin-square |
1080×1080 |
LinkedIn document post (PDF) |
tiktok-9x16 |
1080×1920 |
TikTok Photo Mode, Reels, Shorts |
story-9x16 |
1080×1920 |
Instagram Stories, Threads Stories |
wide-16x9 |
1920×1080 |
Presentations, YouTube, desktop decks |
Slide types (12)
| Type |
Purpose |
Required fields |
hook |
Opening slide — the catchiest line |
text |
body |
Title + paragraph |
title, text |
body (points) |
Pros/cons list with ✓/✗ SVG icons |
title, points[] (instead of text) |
list |
Numbered items (ordered list) |
title, items[] |
stats |
Big numbers with labels |
title, stats[] |
quote |
Large pulled quote |
text, author |
checklist |
Checkmark bullets |
title, items[] |
process |
Numbered steps with connector line |
title, steps[] |
comparison |
Two-column VS / before-after |
leftLabel, leftItems[], rightLabel, rightItems[] |
cta |
Final call to action |
text, handle |
image |
Title + screenshot/photo + optional caption |
imageSrc, optional title, imageCaption |
emoji |
Giant emoji illustration + title + text |
emoji, optional title, text |
number |
Huge hero number/string + title + text |
bigNumber, optional title, text |
points shape: Array<{ type: "plus" | "minus"; text: string }> — green ✓ for plus, muted ✗ for minus. Adaptive sizing (44–62px) based on item count and longest line.
All types also support optional:
badge — small outlined tag above title (e.g. "01", "TIP")
highlight — a word or phrase within text/title that will be colored in the preset's highlight color (yellow for dark themes)
highlightStyle: "italic-box" — renders the highlighted word in Playfair italic on a colored rectangle (instead of plain color)
Images in slides
image slides expect imageSrc as a path served by Next.js under /public/. Workflow when a user gives you a local file:
- User drops a file path like
/Users/me/Desktop/screenshot.png (or passes a file via the chat).
- Copy it into
template/public/images/ with a safe, lowercase filename — e.g. cp "$USER_PATH" template/public/images/screenshot.png.
- In
slides.ts reference it as imageSrc: "/images/screenshot.png" (absolute from /public/).
Same-origin serving avoids CORS errors in the PNG export pipeline. Do not use external URLs — html-to-image will often blank them out.
Background decorations (8 types)
Switchable via toolbar in preview. Default: glow.
| Type |
What it is |
none |
Solid background |
blobs |
Organic colored shapes |
grid |
Dotted grid pattern |
lines |
Diagonal line pattern |
paper |
Ruled notebook lines + left margin (use with paper/light/white colors for a literary feel) |
noise |
SVG grain overlay (overlay blend) |
bignumber |
Giant slide index as watermark (01, 02…) |
glow (default) |
Soft radial gradient in alternating corners |
Style system (3 independent axes)
The final style is composed at runtime from three axes via composePreset(font, color, purpose):
Font axis (DEFAULT_FONT), 5 typefaces:
| Id |
Font |
Feel |
minimal (default) |
Unbounded (body + hook) |
Geometric display, bold, distinctive |
editorial |
Playfair Display |
Classic serif, literary |
clean |
Inter |
Neutral sans-serif, most standard |
mono |
JetBrains Mono |
Monospace, tech/dev feel |
condensed |
Oswald |
Narrow + tall, editorial poster |
Surface axis (DEFAULT_SURFACE) — bg + text neutrals, 8 options:
| Id |
Bg |
Text |
Feel |
dark (default) |
#0A0A0A |
white |
default bold |
white |
pure #FFFFFF |
near-black |
sharp clinical, Apple-style |
light |
cool zinc #F4F4F5 |
near-black |
neutral cool grey, Vercel-style |
paper |
cream #ECE2C8 |
warm brown |
notebook / literary warm |
gradient |
purple→pink→amber |
white |
bright gradient |
pastel |
lilac #EDE9FE |
indigo |
soft |
neon |
dark gradient |
light cyan |
tech dark |
ember |
black→red radial |
near-white |
dramatic |
Accent axis (DEFAULT_ACCENT) — pop color for highlighted words, 8 options:
| Id |
Hex |
Matches surfaces |
yellow (default) |
#FACC15 |
dark, ember |
red |
#DC2626 |
pure red — white, light, paper |
teal |
#14B8A6 |
versatile — dark, white, light, paper |
coral |
#FB7185 |
pink-family — paper, light, white |
orange |
#F97316 |
pumpkin — paper, light, dark |
violet |
#A78BFA |
dark, neon, ember |
lime |
#D9F056 |
ember, dark |
blue |
#3B82F6 |
white, light, paper, dark |
fuchsia |
#C026D3 |
pastel, white, dark |
pink |
#EC4899 |
pastel, dark, light |
amber |
#F59E0B |
gradient, dark, paper |
Total combinations: 8 surfaces × 8 accents × 5 fonts × 2 purposes = 640 valid styles. Pick surface for neutral base, accent for pop color — independently.
Reference combos (inspired by design refs):
dark + teal — noir / minimalist tech
paper + orange — literary warm
ember + lime — dramatic announcement
white + coral — sharp editorial
light + teal — calm informational
Purpose axis (DEFAULT_PURPOSE):
| Id |
Title |
Body |
Divider |
carousel (default) |
44px, weight 800, UPPERCASE |
weight 600, textColor, line-height 1.2 |
visible (96×4px accent) |
presentation |
72px, weight 700, sentence case |
weight 400, textSecondary, line-height 1.45 |
hidden |
Pick purpose: "presentation" + format: "wide-16x9" for a desktop / YouTube presentation deck. Any 3 × 8 × 2 = 48 combinations are valid.
Workflow
Step 1 — Get the text
- If passed inline — use it.
- If a file path (
.md, .txt) — read it.
- If text is long (>500 chars) — confirm it's complete before planning.
Step 2 — Clarify parameters (optional, can assume defaults)
Ask once, combined:
- Number of slides (3–10, default 6)
- Format (see table above, default
threads-4x5; use wide-16x9 for a presentation deck)
- Purpose (
carousel or presentation, default carousel)
- Font (
minimal / editorial / clean, default minimal)
- Color (8 palettes, default
dark)
- Handle for CTA slide (e.g.
@username)
Shortcut: if user says "presentation" / "презентация" / "slide deck" → default to purpose: presentation, format: wide-16x9, font: clean, color: white.
If user says "your call" — apply defaults, do not block.
Step 3 — Plan the slide breakdown
Show the user a preview list before generation:
Slide 1 (hook): "Headline..." [highlight: "word"]
Slide 2 (body): badge 01 — "Title" / "Text..."
Slide 3 (list): badge 02 — "iOS stack" / 3 items
Slide 4 (stats): badge 03 — "Growth" / 3 stats
Slide 5 (quote): "Quote..." — Author
Slide 6 (cta): "Final message" @username
Rules for splitting text into slides
- Hook = single most intriguing line from the post. 1–3 short lines. Works as a standalone thumbnail.
- Body slides = one idea each. Max 40 words. Max 5 lines. Never join two ideas with "and".
- Mix slide types for visual variety: prefer
list for enumerations, stats for numbers, quote for direct speech, comparison for VS/before-after. Don't make every slide a body.
- CTA = conclusion + follow handle. Centered, short.
Step 4 — Generate
Prepare working copy
WORK_DIR="/tmp/carousel-$(date +%s)"
rsync -a --exclude=node_modules ~/.claude/skills/threads-carousel/template/ "$WORK_DIR/"
ln -s ~/.claude/skills/threads-carousel/template/node_modules "$WORK_DIR/node_modules"
cd "$WORK_DIR"
Symlinking node_modules avoids the 350MB copy per run.
Inject content into src/slides.ts
All content + defaults live in src/slides.ts — never touch the engine (src/app/CarouselApp.tsx, src/lib/*). Edit the SLIDES array and the 5 default constants.
Full injection example:
import type { SlideData, BgType, FormatId, FontId, ColorThemeId, PurposeId } from "./lib/types";
export const SLIDES: SlideData[] = [
{ type: "hook", text: "Line one\nline two", highlight: "two" },
{ type: "body", badge: "01", title: "Title", text: "Body text...", highlight: "key" },
{ type: "body", badge: "02", title: "Pros & Cons", points: [
{ type: "plus", text: "One click to register" },
{ type: "plus", text: "Works on any background" },
{ type: "minus", text: "Requires provider setup" },
]},
{ type: "list", badge: "03", title: "Steps", items: ["First", "Second", "Third"] },
{ type: "stats", title: "Numbers", stats: [
{ value: "10×", label: "Faster" },
{ value: "50%", label: "Smaller" },
]},
{ type: "quote", text: "Big idea\nin few words", author: "Someone", role: "2026" },
{ type: "checklist", title: "Pre-flight", items: ["One", "Two", "Three"] },
{ type: "cta", text: "Last word", handle: "@username" },
];
export const DEFAULT_FONT: FontId = "minimal";
export const DEFAULT_COLOR: ColorThemeId = "dark";
export const DEFAULT_PURPOSE: PurposeId = "carousel";
export const DEFAULT_BG: BgType = "glow";
export const DEFAULT_FORMAT: FormatId = "threads-4x5";
Launch preview and export
bun dev --port 3333
Tell the user to open http://localhost:3333. They can:
- Switch Format / Mode / Font / Color / Background live via toolbar rows
- Toggle UI language RU / EN in the top-right
- Click PDF to download all slides in one file (JPEG-compressed, ~5–8 MB for 10 slides)
- Click PNG (a.k.a. "Export All") to download every slide as
01-hook.png, 02-body.png, …
- Click an individual slide thumbnail to export just that one as PNG
After export, stop the dev server.
Parallel carousels
If making multiple carousels at once: create multiple work dirs and launch on ports 3333, 3334, 3335 — they run side by side without conflict.
Design system
Canonical look (font: minimal, color: dark, purpose: carousel):
- Display typeface: Unbounded (Google Fonts), for hooks — applied only when the font axis provides
hookFontFamily
- Body typeface: Space Grotesk (minimal) / Playfair (editorial) / Inter (clean) — everything else uses
preset.fontFamily
- Palette (dark):
#0A0A0A bg, #FFFFFF text, #FACC15 highlight
- Layout: 80px padding, left-aligned, slide counter bottom-center
- Title discipline: carousel purpose — title → a 96×4px accent divider → body, with ≥64px breathing room above body. Presentation purpose — no divider, sentence case, 72px.
- Hook size: 104–140px, adaptive by character/line count
- Body text size: 48–88px, adaptive
- Text balance:
textWrap: "balance" on hook + title (no orphan words)
Typography table (carousel purpose)
The presentation purpose overrides titles to 72px / 700 / sentence case and body to 400 / textSecondary / line-height 1.45.
| Element |
Size |
Weight |
Font source |
| Hook |
104–140px |
800 |
hookFontFamily ?? fontFamily |
| Title |
44px |
800 uppercase |
fontFamily |
| Body |
48–88px |
600 |
fontFamily |
| Points (pros/cons) |
44–62px |
600 |
fontFamily |
| Badge |
26px |
800 uppercase |
fontFamily |
| Stats value |
140–170px |
900 |
fontFamily |
| Stats label |
32px |
500 uppercase |
fontFamily |
| Quote |
62px |
600 |
fontFamily |
| List item |
46px |
600 |
fontFamily (numbers 48px) |
| Checklist item |
44px |
600 |
fontFamily |
| Process step title |
36px |
700 |
fontFamily |
| Handle |
36px |
500 |
fontFamily |
Common mistakes
| Mistake |
Fix |
| Too much text on a slide |
Max 40 words, max 5 lines |
| Two ideas on one slide |
Split into two slides |
All slides are body type |
Mix in list, stats, quote, checklist for visual variety |
| No hook on first slide |
Slide 1 must be the catchiest line |
| No CTA on last slide |
Slide N must end with a handle or call to action |
| Highlight word too long |
Keep highlight to 1–2 words, not whole phrase |
| Badge has too many characters |
Max 2–4 characters (01, TIP, NEW) |
Future work / TODOs
- Satori server-side export — replace browser-based
html-to-image with Satori + Resvg for CLI export (bun run export → out/*.png). Enables headless runs. See Slashgear/linkedin-carousel-gen for reference.
- Per-slide background override — currently
DEFAULT_BG is global; could accept a per-slide bg field to mix decorations across a deck.
- Cyrillic-optimized adaptive sizing — current thresholds are calibrated for Latin; Russian copy tends to be 20–30% longer at the same font size.
- Pencil MCP mode — previous skill version had a manual design mode via Pencil; not currently implemented in the template.
1---2name: threads-carousel3description: Convert text posts into visual carousel images or presentations for Threads, Instagram, LinkedIn, TikTok, YouTube. 12 slide types (incl. image/emoji/number), 6 format presets (incl. 1920×1080 wide), 8 background styles (incl. ruled paper), 3-axis style system (font × color × purpose), highlighted keywords with optional italic-box style. Generates PNG or single-file PDF via Next.js preview + browser export. Triggers: threads carousel, instagram carousel, linkedin carousel, tiktok carousel, slides, carousel images, presentation deck, presentation pdf.4---56# Threads Carousel Generator78Converts a text post into a set of visual carousel slides for Threads, Instagram, LinkedIn, TikTok, Stories, YouTube, or a standalone presentation deck. Composable design system with three independent style axes (font × color × purpose) and 12 slide types.910## Invocation1112```13/threads-carousel <post text>14/threads-carousel path/to/post.md15```1617## Brand setup with design.md (optional, do this once)1819If the user has a `design.md` (their brand system, e.g. exported from styles.refero.design or built from a site they love), read it and make the carousel match their exact brand before generating slides:20211. Open `design.md` and pull out the background, text, and accent colors, plus the font.222. In `src/lib/presets.ts`, set a surface to those colors (edit the `ember` "Dark Claude" surface, or add a new one): `bg`, `textColor`, `accentColor`, and a `bgGradient` for the optional glow. Point the matching `ACCENTS` entry at the brand's pop color.233. In `src/app/layout.tsx`, add the brand font via `next/font/google` and point a `FONT_STYLES` entry at it.244. In `src/slides.ts`, set `DEFAULT_SURFACE`, `DEFAULT_ACCENT`, and `DEFAULT_FONT` to the brand.2526Now every carousel renders in the user's exact brand. If no `design.md` is provided, use the defaults.2728## Format presets (choose target platform)2930| Preset | Size | Platforms |31|---|---|---|32| `threads-4x5` *(default)* | 1080×1350 | Threads, Instagram feed (portrait) |33| `instagram-square` | 1080×1080 | Instagram, Facebook, LinkedIn feed |34| `linkedin-square` | 1080×1080 | LinkedIn document post (PDF) |35| `tiktok-9x16` | 1080×1920 | TikTok Photo Mode, Reels, Shorts |36| `story-9x16` | 1080×1920 | Instagram Stories, Threads Stories |37| `wide-16x9` | 1920×1080 | Presentations, YouTube, desktop decks |3839## Slide types (12)4041| Type | Purpose | Required fields |42|---|---|---|43| `hook` | Opening slide — the catchiest line | `text` |44| `body` | Title + paragraph | `title`, `text` |45| `body` (points) | Pros/cons list with ✓/✗ SVG icons | `title`, `points[]` (instead of `text`) |46| `list` | Numbered items (ordered list) | `title`, `items[]` |47| `stats` | Big numbers with labels | `title`, `stats[]` |48| `quote` | Large pulled quote | `text`, `author` |49| `checklist` | Checkmark bullets | `title`, `items[]` |50| `process` | Numbered steps with connector line | `title`, `steps[]` |51| `comparison` | Two-column VS / before-after | `leftLabel`, `leftItems[]`, `rightLabel`, `rightItems[]` |52| `cta` | Final call to action | `text`, `handle` |53| `image` | Title + screenshot/photo + optional caption | `imageSrc`, optional `title`, `imageCaption` |54| `emoji` | Giant emoji illustration + title + text | `emoji`, optional `title`, `text` |55| `number` | Huge hero number/string + title + text | `bigNumber`, optional `title`, `text` |5657`points` shape: `Array<{ type: "plus" | "minus"; text: string }>` — green ✓ for plus, muted ✗ for minus. Adaptive sizing (44–62px) based on item count and longest line.5859All types also support optional:60- `badge` — small outlined tag above title (e.g. `"01"`, `"TIP"`)61- `highlight` — a word or phrase within `text`/`title` that will be colored in the preset's highlight color (yellow for dark themes)62- `highlightStyle: "italic-box"` — renders the highlighted word in Playfair italic on a colored rectangle (instead of plain color)6364### Images in slides6566`image` slides expect `imageSrc` as a path served by Next.js under `/public/`. Workflow when a user gives you a local file:67681. User drops a file path like `/Users/me/Desktop/screenshot.png` (or passes a file via the chat).692. Copy it into `template/public/images/` with a safe, lowercase filename — e.g. `cp "$USER_PATH" template/public/images/screenshot.png`.703. In `slides.ts` reference it as `imageSrc: "/images/screenshot.png"` (absolute from `/public/`).7172Same-origin serving avoids CORS errors in the PNG export pipeline. Do not use external URLs — `html-to-image` will often blank them out.7374## Background decorations (8 types)7576Switchable via toolbar in preview. Default: `glow`.7778| Type | What it is |79|---|---|80| `none` | Solid background |81| `blobs` | Organic colored shapes |82| `grid` | Dotted grid pattern |83| `lines` | Diagonal line pattern |84| `paper` | Ruled notebook lines + left margin (use with `paper`/`light`/`white` colors for a literary feel) |85| `noise` | SVG grain overlay (overlay blend) |86| `bignumber` | Giant slide index as watermark (01, 02…) |87| `glow` *(default)* | Soft radial gradient in alternating corners |8889## Style system (3 independent axes)9091The final style is composed at runtime from three axes via `composePreset(font, color, purpose)`:9293**Font axis** (`DEFAULT_FONT`), 5 typefaces:9495| Id | Font | Feel |96|---|---|---|97| `minimal` *(default)* | Unbounded (body + hook) | Geometric display, bold, distinctive |98| `editorial` | Playfair Display | Classic serif, literary |99| `clean` | Inter | Neutral sans-serif, most standard |100| `mono` | JetBrains Mono | Monospace, tech/dev feel |101| `condensed` | Oswald | Narrow + tall, editorial poster |102103**Surface axis** (`DEFAULT_SURFACE`) — bg + text neutrals, 8 options:104105| Id | Bg | Text | Feel |106|---|---|---|---|107| `dark` *(default)* | `#0A0A0A` | white | default bold |108| `white` | pure `#FFFFFF` | near-black | sharp clinical, Apple-style |109| `light` | cool zinc `#F4F4F5` | near-black | neutral cool grey, Vercel-style |110| `paper` | cream `#ECE2C8` | warm brown | notebook / literary warm |111| `gradient` | purple→pink→amber | white | bright gradient |112| `pastel` | lilac `#EDE9FE` | indigo | soft |113| `neon` | dark gradient | light cyan | tech dark |114| `ember` | black→red radial | near-white | dramatic |115116**Accent axis** (`DEFAULT_ACCENT`) — pop color for highlighted words, 8 options:117118| Id | Hex | Matches surfaces |119|---|---|---|120| `yellow` *(default)* | `#FACC15` | dark, ember |121| `red` | `#DC2626` | pure red — white, light, paper |122| `teal` | `#14B8A6` | versatile — dark, white, light, paper |123| `coral` | `#FB7185` | pink-family — paper, light, white |124| `orange` | `#F97316` | pumpkin — paper, light, dark |125| `violet` | `#A78BFA` | dark, neon, ember |126| `lime` | `#D9F056` | ember, dark |127| `blue` | `#3B82F6` | white, light, paper, dark |128| `fuchsia` | `#C026D3` | pastel, white, dark |129| `pink` | `#EC4899` | pastel, dark, light |130| `amber` | `#F59E0B` | gradient, dark, paper |131132Total combinations: 8 surfaces × 8 accents × 5 fonts × 2 purposes = **640 valid styles**. Pick surface for neutral base, accent for pop color — independently.133134**Reference combos** (inspired by design refs):135- `dark + teal` — noir / minimalist tech136- `paper + orange` — literary warm137- `ember + lime` — dramatic announcement138- `white + coral` — sharp editorial139- `light + teal` — calm informational140141**Purpose axis** (`DEFAULT_PURPOSE`):142143| Id | Title | Body | Divider |144|---|---|---|---|145| `carousel` *(default)* | 44px, weight 800, UPPERCASE | weight 600, `textColor`, line-height 1.2 | visible (96×4px accent) |146| `presentation` | 72px, weight 700, sentence case | weight 400, `textSecondary`, line-height 1.45 | hidden |147148Pick `purpose: "presentation"` + `format: "wide-16x9"` for a desktop / YouTube presentation deck. Any 3 × 8 × 2 = 48 combinations are valid.149150---151152## Workflow153154### Step 1 — Get the text155156- If passed inline — use it.157- If a file path (`.md`, `.txt`) — read it.158- If text is long (>500 chars) — confirm it's complete before planning.159160### Step 2 — Clarify parameters (optional, can assume defaults)161162Ask once, combined:1631641. **Number of slides** (3–10, default 6)1652. **Format** (see table above, default `threads-4x5`; use `wide-16x9` for a presentation deck)1663. **Purpose** (`carousel` or `presentation`, default `carousel`)1674. **Font** (`minimal` / `editorial` / `clean`, default `minimal`)1685. **Color** (8 palettes, default `dark`)1696. **Handle** for CTA slide (e.g. `@username`)170171Shortcut: if user says "presentation" / "презентация" / "slide deck" → default to `purpose: presentation`, `format: wide-16x9`, `font: clean`, `color: white`.172173If user says "your call" — apply defaults, do not block.174175### Step 3 — Plan the slide breakdown176177Show the user a preview list before generation:178179```180Slide 1 (hook): "Headline..." [highlight: "word"]181Slide 2 (body): badge 01 — "Title" / "Text..."182Slide 3 (list): badge 02 — "iOS stack" / 3 items183Slide 4 (stats): badge 03 — "Growth" / 3 stats184Slide 5 (quote): "Quote..." — Author185Slide 6 (cta): "Final message" @username186```187188### Rules for splitting text into slides1891901. **Hook** = single most intriguing line from the post. 1–3 short lines. Works as a standalone thumbnail.1912. **Body slides** = one idea each. Max 40 words. Max 5 lines. Never join two ideas with "and".1923. **Mix slide types** for visual variety: prefer `list` for enumerations, `stats` for numbers, `quote` for direct speech, `comparison` for VS/before-after. Don't make every slide a `body`.1934. **CTA** = conclusion + follow handle. Centered, short.194195### Step 4 — Generate196197#### Prepare working copy198199```bash200WORK_DIR="/tmp/carousel-$(date +%s)"201rsync -a --exclude=node_modules ~/.claude/skills/threads-carousel/template/ "$WORK_DIR/"202ln -s ~/.claude/skills/threads-carousel/template/node_modules "$WORK_DIR/node_modules"203cd "$WORK_DIR"204```205206Symlinking `node_modules` avoids the 350MB copy per run.207208#### Inject content into `src/slides.ts`209210All content + defaults live in `src/slides.ts` — never touch the engine (`src/app/CarouselApp.tsx`, `src/lib/*`). Edit the `SLIDES` array and the 5 default constants.211212Full injection example:213214```ts215import type { SlideData, BgType, FormatId, FontId, ColorThemeId, PurposeId } from "./lib/types";216217export const SLIDES: SlideData[] = [218 { type: "hook", text: "Line one\nline two", highlight: "two" },219 { type: "body", badge: "01", title: "Title", text: "Body text...", highlight: "key" },220 { type: "body", badge: "02", title: "Pros & Cons", points: [221 { type: "plus", text: "One click to register" },222 { type: "plus", text: "Works on any background" },223 { type: "minus", text: "Requires provider setup" },224 ]},225 { type: "list", badge: "03", title: "Steps", items: ["First", "Second", "Third"] },226 { type: "stats", title: "Numbers", stats: [227 { value: "10×", label: "Faster" },228 { value: "50%", label: "Smaller" },229 ]},230 { type: "quote", text: "Big idea\nin few words", author: "Someone", role: "2026" },231 { type: "checklist", title: "Pre-flight", items: ["One", "Two", "Three"] },232 { type: "cta", text: "Last word", handle: "@username" },233];234235export const DEFAULT_FONT: FontId = "minimal";236export const DEFAULT_COLOR: ColorThemeId = "dark";237export const DEFAULT_PURPOSE: PurposeId = "carousel";238export const DEFAULT_BG: BgType = "glow";239export const DEFAULT_FORMAT: FormatId = "threads-4x5";240```241242#### Launch preview and export243244```bash245bun dev --port 3333246```247248Tell the user to open `http://localhost:3333`. They can:249- Switch **Format / Mode / Font / Color / Background** live via toolbar rows250- Toggle UI language **RU / EN** in the top-right251- Click **PDF** to download all slides in one file (JPEG-compressed, ~5–8 MB for 10 slides)252- Click **PNG** (a.k.a. "Export All") to download every slide as `01-hook.png`, `02-body.png`, …253- Click an individual slide thumbnail to export just that one as PNG254255After export, stop the dev server.256257#### Parallel carousels258259If making multiple carousels at once: create multiple work dirs and launch on ports 3333, 3334, 3335 — they run side by side without conflict.260261---262263## Design system264265Canonical look (`font: minimal`, `color: dark`, `purpose: carousel`):266267- **Display typeface:** Unbounded (Google Fonts), for hooks — applied only when the font axis provides `hookFontFamily`268- **Body typeface:** Space Grotesk (minimal) / Playfair (editorial) / Inter (clean) — everything else uses `preset.fontFamily`269- **Palette (dark):** `#0A0A0A` bg, `#FFFFFF` text, `#FACC15` highlight270- **Layout:** 80px padding, left-aligned, slide counter bottom-center271- **Title discipline:** carousel purpose — title → a 96×4px accent divider → body, with ≥64px breathing room above body. Presentation purpose — no divider, sentence case, 72px.272- **Hook size:** 104–140px, adaptive by character/line count273- **Body text size:** 48–88px, adaptive274- **Text balance:** `textWrap: "balance"` on hook + title (no orphan words)275276### Typography table (carousel purpose)277278The `presentation` purpose overrides titles to 72px / 700 / sentence case and body to 400 / `textSecondary` / line-height 1.45.279280| Element | Size | Weight | Font source |281|---|---|---|---|282| Hook | 104–140px | 800 | `hookFontFamily` ?? `fontFamily` |283| Title | 44px | 800 uppercase | `fontFamily` |284| Body | 48–88px | 600 | `fontFamily` |285| Points (pros/cons) | 44–62px | 600 | `fontFamily` |286| Badge | 26px | 800 uppercase | `fontFamily` |287| Stats value | 140–170px | 900 | `fontFamily` |288| Stats label | 32px | 500 uppercase | `fontFamily` |289| Quote | 62px | 600 | `fontFamily` |290| List item | 46px | 600 | `fontFamily` (numbers 48px) |291| Checklist item | 44px | 600 | `fontFamily` |292| Process step title | 36px | 700 | `fontFamily` |293| Handle | 36px | 500 | `fontFamily` |294295## Common mistakes296297| Mistake | Fix |298|---|---|299| Too much text on a slide | Max 40 words, max 5 lines |300| Two ideas on one slide | Split into two slides |301| All slides are `body` type | Mix in `list`, `stats`, `quote`, `checklist` for visual variety |302| No hook on first slide | Slide 1 must be the catchiest line |303| No CTA on last slide | Slide N must end with a handle or call to action |304| Highlight word too long | Keep highlight to 1–2 words, not whole phrase |305| Badge has too many characters | Max 2–4 characters (`01`, `TIP`, `NEW`) |306307## Future work / TODOs308309- **Satori server-side export** — replace browser-based `html-to-image` with Satori + Resvg for CLI export (`bun run export → out/*.png`). Enables headless runs. See `Slashgear/linkedin-carousel-gen` for reference.310- **Per-slide background override** — currently `DEFAULT_BG` is global; could accept a per-slide `bg` field to mix decorations across a deck.311- **Cyrillic-optimized adaptive sizing** — current thresholds are calibrated for Latin; Russian copy tends to be 20–30% longer at the same font size.312- **Pencil MCP mode** — previous skill version had a manual design mode via Pencil; not currently implemented in the template.