|
| 1 | +--- |
| 2 | +name: typst-cards |
| 3 | +description: Generate PNG images for online communication — social media, carousels, infographics, posts — using Typst. Use this skill whenever the user wants to create slides, cards, visual posts or any digital graphic content, even if they don't explicitly mention Typst. The skill drives an interview about brand materials (logo, palette, fonts, DESIGN.md), proposes the formats best suited to the context (Instagram 1:1, Stories 9:16, LinkedIn 16:9, etc.) and produces ready-to-use PNGs. |
| 4 | +--- |
| 5 | + |
| 6 | +## Purpose |
| 7 | + |
| 8 | +Turn a textual context and optional brand materials into professional PNG images for online communication, using Typst as the rendering engine. The skill manages the full flow: interview → theme → generation → review. |
| 9 | + |
| 10 | +## Prerequisites — verify Typst |
| 11 | + |
| 12 | +Before any other step, check that Typst is installed: |
| 13 | + |
| 14 | +```bash |
| 15 | +typst --version |
| 16 | +``` |
| 17 | + |
| 18 | +If not available, install it like this (Linux/WSL x86_64): |
| 19 | + |
| 20 | +```bash |
| 21 | +curl -fsSL https://github.com/typst/typst/releases/latest/download/typst-x86_64-unknown-linux-musl.tar.xz -o /tmp/typst.tar.xz && tar -xf /tmp/typst.tar.xz -C /tmp/ && mv /tmp/typst-x86_64-unknown-linux-musl/typst ~/.local/bin/ |
| 22 | +``` |
| 23 | + |
| 24 | +--- |
| 25 | + |
| 26 | +## Phase 1 — Interview (single message, mandatory) |
| 27 | + |
| 28 | +Before creating any file, ask these questions to the user **in a single message**: |
| 29 | + |
| 30 | +**Visual materials available?** |
| 31 | +- Do you have a logo? If yes, where is the file? (PNG, SVG, or other) |
| 32 | +- Do you have a `DESIGN.md` or brand guidelines to share? |
| 33 | +- Do you have color preferences? (specific palette, or a mood description: "dark and technical", "colorful and lively", "institutional", etc.) |
| 34 | +- Do you have specific fonts to use? |
| 35 | + |
| 36 | +**Desired output:** |
| 37 | +- What is the topic or content of the cards? |
| 38 | +- How many slides or cards? (a single image or a carousel?) |
| 39 | +- Which formats do you need? (propose those best suited to the context, see table below) |
| 40 | + |
| 41 | +If the user has no brand materials, propose a palette consistent with the context and ask for a quick confirmation before proceeding. |
| 42 | + |
| 43 | +--- |
| 44 | + |
| 45 | +## Supported formats |
| 46 | + |
| 47 | +| Name | Typical use | Pixels | width | height | PPI | |
| 48 | +|------|-------------|--------|-------|--------|-----| |
| 49 | +| Square 1:1 | Instagram post, carousels | 1080×1080 | 7.5in | 7.5in | 144 | |
| 50 | +| Portrait 4:5 | Instagram portrait | 1080×1350 | 7.5in | 9.375in | 144 | |
| 51 | +| Story 9:16 | Instagram/TikTok Stories, Reels | 1080×1920 | 7.5in | 13.33in | 144 | |
| 52 | +| Landscape 16:9 | Twitter/X, YouTube thumb, LinkedIn header | 1920×1080 | 13.33in | 7.5in | 144 | |
| 53 | +| LinkedIn/OG 1.91:1 | LinkedIn post, Open Graph meta | 1200×628 | 8.33in | 4.36in | 144 | |
| 54 | + |
| 55 | +**Math**: `pixels = inches × PPI` → e.g. 7.5in × 144ppi = 1080px. |
| 56 | + |
| 57 | +For carousels, always use the same format for every slide. |
| 58 | + |
| 59 | +--- |
| 60 | + |
| 61 | +## Phase 2 — Material analysis |
| 62 | + |
| 63 | +**With DESIGN.md**: read it and extract — and **apply in theme.typ**: |
| 64 | +- **Colors**: every token (primary, secondary, accent, surface, neutral…) |
| 65 | +- **Fonts**: if the DESIGN.md specifies different fonts for different roles (e.g. display vs body), use them all in theme.typ as separate variables (`DISPLAY`, `SANS`, `MONO`). For example: a serif display face for headlines, a sans-serif for body. |
| 66 | +- **Visual style**: restrained? bold? editorial? Steer the layout accordingly. |
| 67 | +- **Explicit rules**: if the DESIGN.md says "do not use X on Y", honor it. |
| 68 | + |
| 69 | +Do not collapse everything into a single font — the display/body distinction is part of the brand. |
| 70 | + |
| 71 | +**With logo**: note the path. You'll include it in Typst with: |
| 72 | +```typst |
| 73 | +#image("path/to/logo.png", height: 0.5in) // fixed height |
| 74 | +#image("path/to/logo.svg", width: 2in) // fixed width |
| 75 | +``` |
| 76 | + |
| 77 | +**Without materials**: pick a palette based on context: |
| 78 | +- Tech/data topic → dark background `#0d1117`, accent blue `#58a6ff` |
| 79 | +- Consumer/lifestyle topic → white background, vivid colors |
| 80 | +- Institutional/PA topic → sober palette, neutral tones with a measured accent |
| 81 | +- Editorial/journalism topic → strong typography, high contrast |
| 82 | + |
| 83 | +--- |
| 84 | + |
| 85 | +## Phase 3 — File structure |
| 86 | + |
| 87 | +Always create this structure in the project directory: |
| 88 | + |
| 89 | +``` |
| 90 | +<project>/carousel/ |
| 91 | +├── theme.typ # design tokens and helper functions |
| 92 | +├── slides.typ # content (imports theme.typ) |
| 93 | +└── output/ # generated PNGs |
| 94 | + ├── slide-1.png |
| 95 | + ├── slide-2.png |
| 96 | + └── ... |
| 97 | +``` |
| 98 | + |
| 99 | +### Recommended starting point |
| 100 | + |
| 101 | +Instead of writing from scratch, **copy one of the reference templates** from the skill folder and adapt it: |
| 102 | + |
| 103 | +- `references/theme-starter.typ` → palette, fonts, helpers (`lbl`, `ctr`, `codebox`, `divider`) |
| 104 | +- `references/slides-1x1-starter.typ` → 5 Instagram 1:1 slides (cover + 3 content + outro) |
| 105 | +- `references/slides-16x9-starter.typ` → 3 LinkedIn 16:9 slides (cover + two-column + 3 stat) |
| 106 | + |
| 107 | +The templates are **already tested and compile cleanly**. They give you a proven structure; you change colors, fonts, content. |
| 108 | + |
| 109 | +```bash |
| 110 | +# example: |
| 111 | +cp <SKILL_DIR>/references/theme-starter.typ <project>/carousel/theme.typ |
| 112 | +cp <SKILL_DIR>/references/slides-1x1-starter.typ <project>/carousel/slides.typ |
| 113 | +# then edit content in slides.typ and tokens in theme.typ |
| 114 | +``` |
| 115 | + |
| 116 | +### theme.typ — design tokens |
| 117 | + |
| 118 | +Adapt colors to the materials gathered. This is a starting point: |
| 119 | + |
| 120 | +```typst |
| 121 | +// — palette — |
| 122 | +#let BG-DARK = rgb("#0d1117") |
| 123 | +#let BG-LIGHT = rgb("#ffffff") |
| 124 | +#let ACC-DARK = rgb("#58a6ff") // accent on dark background |
| 125 | +#let ACC-LIGHT = rgb("#0969da") // accent on light background |
| 126 | +#let FG-DARK = rgb("#e6edf3") // text on dark |
| 127 | +#let FG-LIGHT = rgb("#1c2128") // text on light |
| 128 | +#let MUTED-D = rgb("#8b949e") // secondary on dark |
| 129 | +#let MUTED-L = rgb("#656d76") // secondary on light |
| 130 | +#let CODE-BG = rgb("#161b22") |
| 131 | +#let CODE-BR = rgb("#30363d") |
| 132 | +
|
| 133 | +// — fonts — |
| 134 | +// Safe fonts on Linux/WSL: DejaVu Sans, DejaVu Sans Mono |
| 135 | +// Check availability: fc-list | grep -i "Font Name" |
| 136 | +#let SANS = ("DejaVu Sans", "Liberation Sans", "Arial") |
| 137 | +#let MONO = ("DejaVu Sans Mono", "Liberation Mono", "Courier New") |
| 138 | +
|
| 139 | +// — helper: section label — |
| 140 | +#let lbl(body, dark: false) = text( |
| 141 | + size: 9pt, weight: "bold", tracking: 2pt, |
| 142 | + fill: if dark { ACC-DARK } else { ACC-LIGHT }, |
| 143 | +)[#upper(body)] |
| 144 | +
|
| 145 | +// — helper: slide counter (for carousels) — |
| 146 | +#let ctr(n, total, dark: false) = align(right)[ |
| 147 | + #text(size: 9pt, fill: if dark { MUTED-D } else { MUTED-L })[#n / #total] |
| 148 | +] |
| 149 | +
|
| 150 | +// — helper: code block — |
| 151 | +#let codebox(body) = block( |
| 152 | + fill: CODE-BG, stroke: 0.5pt + CODE-BR, |
| 153 | + radius: 4pt, inset: (x: 14pt, y: 11pt), width: 100%, |
| 154 | +)[ |
| 155 | + #set text(font: MONO, size: 10.5pt, fill: FG-DARK) |
| 156 | + #body |
| 157 | +] |
| 158 | +``` |
| 159 | + |
| 160 | +### slides.typ — base structure |
| 161 | + |
| 162 | +```typst |
| 163 | +#import "theme.typ": * |
| 164 | +
|
| 165 | +// Pick the format (only one active line): |
| 166 | +#set page(width: 7.5in, height: 7.5in, margin: (x: 0.62in, y: 0.58in)) // 1:1 |
| 167 | +// #set page(width: 7.5in, height: 9.375in, margin: (x: 0.62in, y: 0.58in)) // 4:5 |
| 168 | +// #set page(width: 7.5in, height: 13.33in, margin: (x: 0.62in, y: 0.70in)) // 9:16 |
| 169 | +// #set page(width: 13.33in, height: 7.5in, margin: (x: 0.80in, y: 0.58in)) // 16:9 |
| 170 | +// #set page(width: 8.33in, height: 4.36in, margin: (x: 0.62in, y: 0.45in)) // 1.91:1 |
| 171 | +
|
| 172 | +#set text(font: SANS, size: 15pt, fill: FG-LIGHT) |
| 173 | +
|
| 174 | +// — SLIDE 1 (dark background) — |
| 175 | +#set page(fill: BG-DARK) |
| 176 | +#v(1fr) |
| 177 | +#lbl(dark: true)[tag · topic] |
| 178 | +#v(0.15in) |
| 179 | +#text(size: 44pt, weight: 900, fill: FG-DARK)[ |
| 180 | + Main title\ |
| 181 | + of the slide |
| 182 | +] |
| 183 | +#v(0.2in) |
| 184 | +#text(size: 14pt, fill: MUTED-D)[Short subtitle or description] |
| 185 | +#v(1fr) |
| 186 | +#ctr(1, 6, dark: true) |
| 187 | +
|
| 188 | +// — SLIDE 2 (light background) — different settings → automatic page break |
| 189 | +#set page(fill: BG-LIGHT) |
| 190 | +#lbl[02 · section] |
| 191 | +#v(0.2in) |
| 192 | +#text(size: 31pt, weight: 900)[Section title] |
| 193 | +#v(0.2in) |
| 194 | +#text(size: 14pt, fill: MUTED-L)[Slide body text.] |
| 195 | +#v(1fr) |
| 196 | +#ctr(2, 6) |
| 197 | +
|
| 198 | +// — SLIDE 3 (same background as slide 2 → explicit pagebreak) — |
| 199 | +#pagebreak() |
| 200 | +// ... content ... |
| 201 | +``` |
| 202 | + |
| 203 | +### Compilation |
| 204 | + |
| 205 | +```bash |
| 206 | +cd <project>/carousel |
| 207 | +typst compile slides.typ "output/slide-{p}.png" --ppi 144 |
| 208 | +``` |
| 209 | + |
| 210 | +The `{p}` is replaced by the page number → one PNG per slide. |
| 211 | + |
| 212 | +--- |
| 213 | + |
| 214 | +## Design principles for social cards |
| 215 | + |
| 216 | +**Text hierarchy**: large title → subtitle → body. Max 3 levels. Don't crowd. |
| 217 | + |
| 218 | +**White space**: generous margins (0.5–0.7in). Let the content breathe. |
| 219 | + |
| 220 | +**Colors**: 2–3 max. A vivid accent on a neutral background always works. |
| 221 | + |
| 222 | +**Font sizes on a 1080px canvas** (1pt Typst ≈ 2px output): |
| 223 | +- Cover title: 42–50pt |
| 224 | +- Section title: 28–34pt |
| 225 | +- Body: 14–16pt |
| 226 | +- Label/tag: 9–10pt bold uppercase with tracking |
| 227 | + |
| 228 | +**Carousels**: counter at bottom right of every slide (`1 / 6`, `2 / 6`, ...). |
| 229 | + |
| 230 | +**Story 9:16**: center the content vertically, use larger fonts, avoid corners. |
| 231 | + |
| 232 | +--- |
| 233 | + |
| 234 | +## Known pitfalls |
| 235 | + |
| 236 | +**Grid with wide numbers/text**: `columns: (1fr, 1fr, 1fr)` causes overflow if values are wide. Use a vertical layout or `columns: (auto, 1fr)` with a generous gutter. |
| 237 | + |
| 238 | +**Unavailable fonts**: always specify fallbacks (`("Chosen Font", "DejaVu Sans", "Arial")`). Check with `fc-list | grep -i "name"`. A warning is not an error: Typst uses the fallback. |
| 239 | + |
| 240 | +**`#set page(fill: X)` does not create a new page if X doesn't change**: between slides with the same fill, use an explicit `#pagebreak()`. |
| 241 | + |
| 242 | +**`v(1fr)` works at page level**: it splits the leftover space. If you place two of them, the space is split evenly between the two points. |
| 243 | + |
| 244 | +**Logo with transparent background**: prefer SVG when possible. PNG with alpha works but requires that the slide background does not contrast badly with it. |
| 245 | + |
| 246 | +**The `<` symbol in Typst content**: it is interpreted as a label opening and causes an error. Replace with words ("less than", "lower than") or use `$lt$` in math mode. |
| 247 | + |
| 248 | +**`leading` is not a parameter of `text()`**: it belongs to `par()`. To control line height of a text block: |
| 249 | +```typst |
| 250 | +// WRONG — error "unexpected argument: leading" |
| 251 | +#text(size: 14pt, leading: 1.4em)[...] |
| 252 | +
|
| 253 | +// CORRECT — use par leading inside a block |
| 254 | +#block[ |
| 255 | + #set par(leading: 0.7em) |
| 256 | + #text(size: 14pt)[...] |
| 257 | +] |
| 258 | +``` |
| 259 | + |
| 260 | +**`align(center + horizon)` with long text causes incorrect wordwrap**: words can fuse. Prefer to handle vertical and horizontal alignment separately, or use `block(width: 100%)` to contain the text. |
| 261 | + |
| 262 | +**Editorial pattern (kicker + huge title + footer)**: |
| 263 | +```typst |
| 264 | +// magazine/newspaper style — battle-tested on 1:1 |
| 265 | +#text(size: 9pt, weight: "bold", tracking: 3pt, fill: ACC)[#upper("category · section")] |
| 266 | +#v(0.08in) |
| 267 | +#line(length: 100%, stroke: 1pt + ACC) |
| 268 | +#v(0.25in) |
| 269 | +#text(size: 14pt, fill: DIM)[Eyebrow] |
| 270 | +#v(0.05in) |
| 271 | +#text(size: 60pt, weight: 900)[Big title] |
| 272 | +#v(0.05in) |
| 273 | +#text(size: 18pt, weight: "bold", fill: ACC)[Accent subtitle] |
| 274 | +#v(1fr) |
| 275 | +#line(length: 100%, stroke: 0.5pt + rgb("#333333")) |
| 276 | +#v(0.1in) |
| 277 | +#grid(columns: (1fr, 1fr, 1fr), |
| 278 | + text(size: 11pt)[Date], |
| 279 | + align(center, text(size: 11pt)[Place]), |
| 280 | + align(right, text(size: 11pt, fill: ACC)[CTA]), |
| 281 | +) |
| 282 | +``` |
| 283 | + |
| 284 | +--- |
| 285 | + |
| 286 | +## Phase 4 — Review and iteration |
| 287 | + |
| 288 | +1. Compile with `typst compile` and read the resulting PNGs with the Read tool |
| 289 | +2. Show the generated slides to the user |
| 290 | +3. Ask for feedback: colors, text sizes, layout, content |
| 291 | +4. Typst recompiles in <1s: iterate quickly until satisfied |
0 commit comments