@opendirectory.dev/skills 0.1.56 → 0.1.58

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.
@@ -0,0 +1,346 @@
1
+ # Style Presets — graphic-case-study
2
+
3
+ 9 business-oriented style presets. Each is fully self-contained -- complete CSS token set, no "derive from" chains.
4
+
5
+ Choose based on the vendor's brand and audience. When in doubt: clean-slate for enterprise B2B, midnight-editorial for tech/AI companies, warm-earth for agencies and services.
6
+
7
+ ---
8
+
9
+ ## 1. clean-slate
10
+
11
+ **Best for:** Enterprise B2B, sales teams, customer-facing materials, any audience that expects professionalism
12
+ **Feeling:** Trustworthy, clear, confident, enterprise-safe
13
+
14
+ ```
15
+ Background: #FFFFFF outer / #FFFFFF page / #F8F8F8 alt sections / #F4F4F4 footer
16
+ Text primary: #111111
17
+ Text secondary: #555555
18
+ Text muted: #999999
19
+ Accent: #0F172A (near-black navy)
20
+ Accent light: #E0F2FE (light blue for pill/badge backgrounds)
21
+ Accent text: #FFFFFF
22
+ Divider: #E8E8E8
23
+ Card border: #E0E0E0
24
+ Card radius: 16px
25
+ Card shadow: 0 2px 8px rgba(0, 0, 0, 0.06)
26
+
27
+ Display font: 'Plus Jakarta Sans', Arial, Helvetica, sans-serif
28
+ Body font: 'Plus Jakarta Sans', Arial, Helvetica, sans-serif
29
+ Google Fonts: https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;800&display=swap
30
+
31
+ Display weight: 800
32
+ Body weight: 400 / 500
33
+ ```
34
+
35
+ **Signature elements:**
36
+ - Rounded card containers (`border-radius: 16px`) on all fact cells, feature cards, stat containers
37
+ - Badge pills for category labels (`border-radius: 100px; background: #E0F2FE; color: #0F172A; padding: 0.2em 0.8em`)
38
+ - Generous whitespace -- push toward maximum clamp values
39
+ - Hero stat in dark `#0F172A` (high contrast, authoritative -- no color distraction)
40
+ - Cover background: `#0F172A` full dark (inversion for premium feel)
41
+ - Closing CTA: `#0F172A` full bg with white headline and white CTA
42
+
43
+ ---
44
+
45
+ ## 2. midnight-editorial
46
+
47
+ **Best for:** Tech, AI, developer-focused companies, premium B2B, thought leadership
48
+ **Feeling:** Editorial authority, premium, considered
49
+
50
+ ```
51
+ Background: #0A0A0A outer / #111111 page / #1A1A1A elevated cards / #080808 footer
52
+ Text primary: #F2F2F2
53
+ Text secondary: #AAAAAA
54
+ Text muted: #555555
55
+ Accent: #D8F90A (yellow-green)
56
+ Accent text: #0A0A0A (dark text on accent)
57
+ Divider: #2A2A2A
58
+ Card border: #222222
59
+
60
+ Display font: 'Instrument Serif', Georgia, 'Times New Roman', serif
61
+ Body font: Inter, Arial, Helvetica, sans-serif
62
+ Google Fonts: https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter:wght@400;500;600;700&display=swap
63
+
64
+ Display weight: 400
65
+ Body weight: 400 / 600
66
+ ```
67
+
68
+ **Signature elements:**
69
+ - Thin `<hr>` separators (`border: none; border-top: 1px solid #2A2A2A; width: 40px; margin: 0`)
70
+ - Oversized section numbers at `opacity: 0.06` as absolute background elements
71
+ - Instrument Serif italic on testimonial quotes (literary weight)
72
+ - Hero stat number in `#D8F90A` accent
73
+ - Cover: `#D8F90A` full-bg inversion with `#0A0A0A` text (unmissable)
74
+ - Closing CTA: `#D8F90A` full bg with `#0A0A0A` text
75
+
76
+ ---
77
+
78
+ ## 3. matt-gray
79
+
80
+ **Best for:** Internal business reviews, board materials, professional presentations to mixed audiences
81
+ **Feeling:** Safe, professional, accessible, clean
82
+
83
+ ```
84
+ Background: #F5F5F5 outer / #FFFFFF page / #EEEEEE section alt / #F8F8F8 footer
85
+ Text primary: #1A1A1A
86
+ Text secondary: #444444
87
+ Text muted: #888888
88
+ Accent: #2563EB (blue)
89
+ Accent text: #FFFFFF
90
+ Divider: #E5E5E5
91
+ Card border: #DDDDDD
92
+ Card shadow: 0 1px 3px rgba(0, 0, 0, 0.08)
93
+
94
+ Display font: 'DM Sans', Arial, Helvetica, sans-serif
95
+ Body font: 'DM Sans', Arial, Helvetica, sans-serif
96
+ Google Fonts: https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700;800&display=swap
97
+
98
+ Display weight: 700
99
+ Body weight: 400 / 500
100
+ ```
101
+
102
+ **Signature elements:**
103
+ - 4px left border on section headings and fact cells (`border-left: 4px solid #2563EB; padding-left: 1rem`)
104
+ - Subtle drop shadow on stat containers
105
+ - Hero stat in accent blue `#2563EB`
106
+ - Section headings use `#EEEEEE` alt background
107
+ - Closing CTA: `#1A1A1A` full dark bg with white text and blue button
108
+
109
+ ---
110
+
111
+ ## 4. brutalist
112
+
113
+ **Best for:** Standout sales materials, design-forward agencies, brands that want to be remembered
114
+ **Feeling:** Direct, raw, confident, uncompromising
115
+
116
+ ```
117
+ Background: #FFFFFF outer / #FFFFFF page
118
+ Text primary: #000000
119
+ Text secondary: #333333
120
+ Accent: #FF3300 (red) or #FFE500 (yellow)
121
+ Accent text: #000000
122
+ Divider: #000000 (solid black)
123
+ Border: 3px solid #000000 (heavy)
124
+ Border-radius: 0 (zero everywhere -- absolute rule)
125
+
126
+ Display font: 'Archivo Black', Arial Black, Arial, sans-serif
127
+ Body font: 'Space Grotesk', Arial, sans-serif
128
+ Google Fonts: https://fonts.googleapis.com/css2?family=Archivo+Black&family=Space+Grotesk:wght@400;500;700&display=swap
129
+
130
+ Display weight: 900
131
+ Body weight: 400 / 700
132
+ ```
133
+
134
+ **Signature elements:**
135
+ - Heavy borders everywhere: `border: 3px solid #000000` on ALL cards, tables, containers, page sections
136
+ - Zero border-radius on every element -- this is a hard rule, never soften
137
+ - Oversized section numbers fully visible (`opacity: 1`) in accent color
138
+ - Cover: accent color full bg (`#FF3300` or `#FFE500`) with `#000000` text
139
+ - Hero stat in accent color, extreme size
140
+ - Closing CTA: `#000000` full bg with white text and accent-color button/border
141
+
142
+ **DO NOT apply rounded corners, gradients, or drop shadows to brutalist case studies.**
143
+
144
+ ---
145
+
146
+ ## 5. mint-pixel-corporate
147
+
148
+ **Best for:** SaaS sales, product-focused companies, tech startups, growth-stage B2B
149
+ **Feeling:** Fresh, modern, startup-professional
150
+
151
+ ```
152
+ Background: #F0FAF7 outer / #FFFFFF page / #E8F5F0 alt sections / #F0FAF7 footer
153
+ Text primary: #1A2E2A
154
+ Text secondary: #4A6B62
155
+ Text muted: #7A9B92
156
+ Accent: #00B894 (mint)
157
+ Accent text: #1A2E2A
158
+ Divider: #D1E8E1
159
+ Card border: #C5DFD7
160
+ Card radius: 10px
161
+
162
+ Display font: 'Manrope', Arial, Helvetica, sans-serif
163
+ Body font: 'Manrope', Arial, Helvetica, sans-serif
164
+ Google Fonts: https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;700;800&display=swap
165
+
166
+ Display weight: 800
167
+ Body weight: 400 / 500
168
+ ```
169
+
170
+ **Signature elements:**
171
+ - Mint accent pills for feature labels and category tags (`border-radius: 100px; background: #00B894; color: #1A2E2A`)
172
+ - CSS `radial-gradient` dot pattern on cover background:
173
+ ```css
174
+ background-image: radial-gradient(circle, #00B894 1px, transparent 1px);
175
+ background-size: 24px 24px;
176
+ background-color: #1A2E2A;
177
+ ```
178
+ - Hero stat in mint `#00B894`
179
+ - Feature cards with mint border (`border: 2px solid #00B894`)
180
+ - Closing CTA: `#1A2E2A` dark bg with dot pattern + mint text and white button
181
+
182
+ ---
183
+
184
+ ## 6. product-minimal
185
+
186
+ **Best for:** Product companies, design-forward B2B, companies presenting to design-savvy buyers
187
+ **Feeling:** Maximum whitespace, purposeful restraint, design system precision
188
+
189
+ ```
190
+ Background: #FAFAFA outer / #FAFAFA page / #F4F4F4 alt / #F0F0F0 footer
191
+ Text primary: #18181B
192
+ Text secondary: #71717A
193
+ Text muted: #A1A1AA
194
+ Accent: #8B5CF6 (violet)
195
+ Accent text: #FFFFFF
196
+ Divider: #E4E4E7
197
+ Card shadow: 0 1px 3px rgba(0, 0, 0, 0.08)
198
+ Card radius: 12px
199
+
200
+ Display font: 'Syne', Arial, Helvetica, sans-serif
201
+ Body font: 'IBM Plex Sans', Arial, Helvetica, sans-serif
202
+ Google Fonts: https://fonts.googleapis.com/css2?family=Syne:wght@400;700;800&family=IBM+Plex+Sans:wght@400;500;600&display=swap
203
+
204
+ Display weight: 700-800
205
+ Body weight: 400 / 500
206
+ ```
207
+
208
+ **Signature elements:**
209
+ - Single accent element per section rule: violet appears in AT MOST ONE element per section
210
+ - Subtle CSS drop shadow on cards (`box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08)`)
211
+ - Very generous padding (use maximum clamp values everywhere)
212
+ - Hero stat in violet `#8B5CF6` with thin violet top-border on stat container
213
+ - Cover: `#18181B` full dark bg with Syne display font in white
214
+ - Closing CTA: violet background (`#8B5CF6`) with white text
215
+
216
+ ---
217
+
218
+ ## 7. magazine-red
219
+
220
+ **Best for:** Marketing agencies, bold companies, brand-forward case studies, attention-commanding materials
221
+ **Feeling:** Authoritative, editorial, high energy
222
+
223
+ ```
224
+ Background: #1A1A1A outer / #111111 page / #1E1E1E alt / #0D0D0D footer
225
+ Text primary: #FFFFFF
226
+ Text secondary: #CCCCCC
227
+ Text muted: #888888
228
+ Accent: #E63329 (red)
229
+ Divider: #2A2A2A
230
+
231
+ Display font: 'Fraunces', Georgia, 'Times New Roman', serif
232
+ Body font: 'Work Sans', Arial, Helvetica, sans-serif
233
+ Google Fonts: https://fonts.googleapis.com/css2?family=Fraunces:ital,wght@0,900;1,900&family=Work+Sans:wght@400;500;600&display=swap
234
+
235
+ Display weight: 900
236
+ Body weight: 400 / 500
237
+ ```
238
+
239
+ **Signature elements:**
240
+ - 8px full-width red band separator: `<div style="width:100%; height:8px; background:#E63329; margin: clamp(1rem, 2vw, 2rem) 0">`
241
+ - Editorial section numbers at `opacity: 0.25` in red
242
+ - Testimonial section inverted to white bg with dark text (only white section in the deck)
243
+ - Fraunces italic for quote text (extreme editorial weight)
244
+ - Hero stat in red `#E63329`, large
245
+ - Closing CTA: full red background (`#E63329`) with white text
246
+
247
+ ---
248
+
249
+ ## 8. soft-cloud
250
+
251
+ **Best for:** Onboarding materials, customer education, HR/people companies, approachable SaaS
252
+ **Feeling:** Friendly, accessible, soft, welcoming
253
+
254
+ ```
255
+ Background: #EEF2FF outer / #FFFFFF page / #F5F3FF alt / #EEF2FF footer
256
+ Text primary: #1E1B4B
257
+ Text secondary: #4C4A7B
258
+ Text muted: #9896C0
259
+ Accent: #6366F1 (indigo)
260
+ Accent light: #E0E7FF (light indigo for card backgrounds)
261
+ Accent text: #FFFFFF
262
+ Divider: #DDD6FE
263
+ Card radius: 24px
264
+ Card shadow: 0 4px 16px rgba(99, 102, 241, 0.08)
265
+
266
+ Display font: 'Outfit', Arial, Helvetica, sans-serif
267
+ Body font: 'Outfit', Arial, Helvetica, sans-serif
268
+ Google Fonts: https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&display=swap
269
+
270
+ Display weight: 700
271
+ Body weight: 400 / 500
272
+ ```
273
+
274
+ **Signature elements:**
275
+ - Generous border-radius (`border-radius: 24px`) on all cards, callout blocks, stat containers
276
+ - Gradient section backgrounds: `linear-gradient(135deg, #EEF2FF 0%, #E0E7FF 50%, #DDD6FE 100%)`
277
+ - Badge pills on key points (`border-radius: 100px; background: #E0E7FF; color: #6366F1`)
278
+ - Hero stat in indigo `#6366F1` on `#F5F3FF` elevated card bg
279
+ - Closing CTA: indigo gradient (`linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%)`) with white text
280
+
281
+ ---
282
+
283
+ ## 9. warm-earth (case-study-exclusive)
284
+
285
+ **Best for:** Agencies, consultancies, service businesses, health/education tech, companies that sell on trust
286
+ **Feeling:** Trusted, grounded, human, warm authority
287
+
288
+ ```
289
+ Background: #FDF6EF outer / #FFFFFF page / #FAF0E4 alt sections / #F5EBD8 footer
290
+ Text primary: #2C1A0E
291
+ Text secondary: #6B4C30
292
+ Text muted: #A08060
293
+ Accent: #D4622A (burnt orange)
294
+ Accent light: #FDE8D8 (peach -- for pill/badge backgrounds)
295
+ Accent text: #FFFFFF
296
+ Divider: #E8D5BF
297
+ Card border: #E0C9A8
298
+ Card radius: 10px
299
+ Card shadow: 0 2px 8px rgba(44, 26, 14, 0.06)
300
+
301
+ Display font: 'Lora', Georgia, 'Times New Roman', serif
302
+ Body font: 'Source Sans 3', Arial, Helvetica, sans-serif
303
+ Google Fonts: https://fonts.googleapis.com/css2?family=Lora:wght@400;600;700&family=Source+Sans+3:wght@400;600&display=swap
304
+
305
+ Display weight: 700
306
+ Body weight: 400 / 600
307
+ ```
308
+
309
+ **Signature elements:**
310
+ - Warm off-white background (`#FDF6EF`) -- not pure white, not gray. The warmth starts with the canvas.
311
+ - Accent pills in peach (`background: #FDE8D8; color: #D4622A; border-radius: 100px`) for category labels
312
+ - **Drop cap on challenge page opening paragraph:**
313
+ ```css
314
+ .challenge-context::first-letter {
315
+ font-family: 'Lora', Georgia, serif;
316
+ font-size: clamp(3rem, 7vw, 5rem);
317
+ float: left;
318
+ margin: 0.05em 0.12em 0 0;
319
+ color: var(--accent);
320
+ line-height: 0.8;
321
+ }
322
+ ```
323
+ - Hero stat in burnt orange `#D4622A`
324
+ - Section headings: `border-left: 4px solid #D4622A; padding-left: 1rem` (no full-background inversion -- subtle accent)
325
+ - Cover: warm-dark background `#2C1A0E` with off-white text `#F5EBD8` and burnt-orange accent label
326
+ - Closing CTA: `#2C1A0E` full dark bg with `#FDE8D8` peach headline, white CTA button with burnt orange border
327
+
328
+ ---
329
+
330
+ ## DO NOT USE -- Style Slop Checklist
331
+
332
+ Before outputting any HTML, verify none of these are present:
333
+
334
+ | Pattern | Why it's wrong |
335
+ |---|---|
336
+ | Inter as display font | Zero typographic character -- reads as default AI output |
337
+ | Purple-to-blue gradient on white background | Most overused AI aesthetic -- immediately signals undesigned |
338
+ | Every section same background color | No visual rhythm -- the PDF reads as one flat block |
339
+ | Identical section layouts throughout | No typographic thinking -- copy-pasted template feel |
340
+ | `box-shadow` on everything | Overused "depth" signal with no real depth |
341
+ | `border-radius: 8px` on everything (especially brutalist) | Softening that fights the style's aesthetic intent |
342
+ | Accent color on 5+ elements per section | Over-branded, destroys scarcity = destroys premium feel |
343
+ | Helvetica Neue / Arial as display font | Generic -- no personality at display size |
344
+ | Stat numbers at body text size | Breaks the entire purpose of the results section |
345
+ | "Thank You" as closing-cta headline | No action, no contact info = completely wasted page |
346
+ | Logo placeholder box with no styling | Unfinished -- use CSS initials at minimum |
@@ -0,0 +1,361 @@
1
+ #!/usr/bin/env bash
2
+ # export-pdf.sh — Export an HTML presentation to PDF
3
+ #
4
+ # Usage:
5
+ # bash scripts/export-pdf.sh <path-to-html> [output.pdf]
6
+ #
7
+ # Examples:
8
+ # bash scripts/export-pdf.sh ./my-deck/index.html
9
+ # bash scripts/export-pdf.sh ./presentation.html ./presentation.pdf
10
+ #
11
+ # What this does:
12
+ # 1. Starts a local server to serve the HTML (fonts and assets need HTTP)
13
+ # 2. Uses Playwright to screenshot each slide at 1920x1080
14
+ # 3. Combines all screenshots into a single PDF
15
+ # 4. Cleans up the server and temp files
16
+ #
17
+ # The PDF preserves colors, fonts, and layout — but not animations.
18
+ # Perfect for email attachments, printing, or embedding in documents.
19
+ set -euo pipefail
20
+
21
+ # ─── Colors ────────────────────────────────────────────────
22
+ RED='\033[0;31m'
23
+ GREEN='\033[0;32m'
24
+ CYAN='\033[0;36m'
25
+ YELLOW='\033[1;33m'
26
+ BOLD='\033[1m'
27
+ NC='\033[0m'
28
+
29
+ info() { echo -e "${CYAN}ℹ${NC} $*"; }
30
+ ok() { echo -e "${GREEN}✓${NC} $*"; }
31
+ warn() { echo -e "${YELLOW}⚠${NC} $*"; }
32
+ err() { echo -e "${RED}✗${NC} $*" >&2; }
33
+
34
+ # ─── Parse flags ──────────────────────────────────────────
35
+
36
+ # Default resolution: 1920x1080 (full HD, ~1-2MB per slide)
37
+ # Compact resolution: 1280x720 (HD, ~50-70% smaller files)
38
+ # Portrait resolution: 1200x1697 (A4 portrait — for case studies and PDFs)
39
+ VIEWPORT_W=1920
40
+ VIEWPORT_H=1080
41
+ COMPACT=false
42
+ PORTRAIT=false
43
+
44
+ POSITIONAL=()
45
+ for arg in "$@"; do
46
+ case $arg in
47
+ --compact)
48
+ COMPACT=true
49
+ VIEWPORT_W=1280
50
+ VIEWPORT_H=720
51
+ ;;
52
+ --portrait)
53
+ PORTRAIT=true
54
+ VIEWPORT_W=1200
55
+ VIEWPORT_H=1697
56
+ ;;
57
+ *)
58
+ POSITIONAL+=("$arg")
59
+ ;;
60
+ esac
61
+ done
62
+ set -- "${POSITIONAL[@]}"
63
+
64
+ # ─── Input validation ─────────────────────────────────────
65
+
66
+ if [[ $# -lt 1 ]]; then
67
+ err "Usage: bash scripts/export-pdf.sh <path-to-html> [output.pdf] [--compact|--portrait]"
68
+ err ""
69
+ err "Examples:"
70
+ err " bash scripts/export-pdf.sh ./my-deck/index.html"
71
+ err " bash scripts/export-pdf.sh ./presentation.html ./slides.pdf"
72
+ err " bash scripts/export-pdf.sh ./presentation.html --compact # smaller file size (1280×720)"
73
+ err " bash scripts/export-pdf.sh ./case-study/index.html --portrait # A4 portrait (1200×1697)"
74
+ exit 1
75
+ fi
76
+
77
+ INPUT_HTML="$1"
78
+ if [[ ! -f "$INPUT_HTML" ]]; then
79
+ err "File not found: $INPUT_HTML"
80
+ exit 1
81
+ fi
82
+
83
+ # Resolve to absolute path
84
+ INPUT_HTML=$(cd "$(dirname "$INPUT_HTML")" && pwd)/$(basename "$INPUT_HTML")
85
+
86
+ # Output PDF path: use second argument or derive from input name
87
+ if [[ $# -ge 2 ]]; then
88
+ OUTPUT_PDF="$2"
89
+ else
90
+ OUTPUT_PDF="$(dirname "$INPUT_HTML")/$(basename "$INPUT_HTML" .html).pdf"
91
+ fi
92
+
93
+ # Resolve output to absolute path
94
+ OUTPUT_DIR=$(dirname "$OUTPUT_PDF")
95
+ mkdir -p "$OUTPUT_DIR"
96
+ OUTPUT_PDF="$OUTPUT_DIR/$(basename "$OUTPUT_PDF")"
97
+
98
+ echo ""
99
+ echo -e "${BOLD}╔══════════════════════════════════════╗${NC}"
100
+ echo -e "${BOLD}║ Export Slides to PDF ║${NC}"
101
+ echo -e "${BOLD}╚══════════════════════════════════════╝${NC}"
102
+ echo ""
103
+
104
+ # ─── Step 1: Check dependencies ───────────────────────────
105
+
106
+ info "Checking dependencies..."
107
+
108
+ if ! command -v npx &>/dev/null; then
109
+ err "Node.js is required but not installed."
110
+ err ""
111
+ err "Install Node.js:"
112
+ err " macOS: brew install node"
113
+ err " or visit https://nodejs.org and download the installer"
114
+ exit 1
115
+ fi
116
+
117
+ ok "Node.js found"
118
+
119
+ # ─── Step 2: Create the export script ─────────────────────
120
+
121
+ # We use a temporary Node.js script with Playwright to:
122
+ # 1. Start a local server (so fonts load correctly)
123
+ # 2. Navigate to each slide
124
+ # 3. Screenshot each slide at 1920x1080 (16:9 landscape)
125
+ # 4. Combine into a single PDF
126
+
127
+ TEMP_DIR=$(mktemp -d)
128
+ TEMP_SCRIPT="$TEMP_DIR/export-slides.mjs"
129
+
130
+ # Figure out which directory to serve (the folder containing the HTML)
131
+ SERVE_DIR=$(dirname "$INPUT_HTML")
132
+ HTML_FILENAME=$(basename "$INPUT_HTML")
133
+
134
+ cat > "$TEMP_SCRIPT" << 'EXPORT_SCRIPT'
135
+ // export-slides.mjs — Vector PDF export via Playwright
136
+ //
137
+ // Uses page.pdf() directly on the live HTML — produces true vector PDF
138
+ // with crisp text, real fonts, and CSS backgrounds. No screenshots.
139
+
140
+ import { chromium } from 'playwright';
141
+ import { createServer } from 'http';
142
+ import { readFileSync } from 'fs';
143
+ import { join, extname } from 'path';
144
+
145
+ const SERVE_DIR = process.argv[2];
146
+ const HTML_FILE = process.argv[3];
147
+ const OUTPUT_PDF = process.argv[4];
148
+ const VP_WIDTH = parseInt(process.argv[5]) || 1920;
149
+ const VP_HEIGHT = parseInt(process.argv[6]) || 1080;
150
+
151
+ // ─── Static file server (needed for Google Fonts + relative assets) ──
152
+
153
+ const MIME_TYPES = {
154
+ '.html': 'text/html',
155
+ '.css': 'text/css',
156
+ '.js': 'application/javascript',
157
+ '.json': 'application/json',
158
+ '.png': 'image/png',
159
+ '.jpg': 'image/jpeg',
160
+ '.jpeg': 'image/jpeg',
161
+ '.gif': 'image/gif',
162
+ '.svg': 'image/svg+xml',
163
+ '.webp': 'image/webp',
164
+ '.woff': 'font/woff',
165
+ '.woff2':'font/woff2',
166
+ '.ttf': 'font/ttf',
167
+ '.eot': 'application/vnd.ms-fontobject',
168
+ };
169
+
170
+ const server = createServer((req, res) => {
171
+ const decodedUrl = decodeURIComponent(req.url);
172
+ const filePath = join(SERVE_DIR, decodedUrl === '/' ? HTML_FILE : decodedUrl);
173
+ try {
174
+ const content = readFileSync(filePath);
175
+ const ext = extname(filePath).toLowerCase();
176
+ res.writeHead(200, { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream' });
177
+ res.end(content);
178
+ } catch {
179
+ res.writeHead(404);
180
+ res.end('Not found');
181
+ }
182
+ });
183
+
184
+ const port = await new Promise((resolve) => {
185
+ server.listen(0, () => resolve(server.address().port));
186
+ });
187
+
188
+ console.log(` Local server on port ${port}`);
189
+
190
+ // ─── Load page ────────────────────────────────────────────
191
+
192
+ const browser = await chromium.launch();
193
+ const page = await browser.newPage({
194
+ viewport: { width: VP_WIDTH, height: VP_HEIGHT },
195
+ });
196
+
197
+ await page.goto(`http://localhost:${port}/`, { waitUntil: 'networkidle' });
198
+ await page.evaluate(() => document.fonts.ready);
199
+ await page.waitForTimeout(1000);
200
+
201
+ // Count slides
202
+ const slideCount = await page.evaluate(() =>
203
+ document.querySelectorAll('.slide').length
204
+ );
205
+
206
+ console.log(` Found ${slideCount} slides`);
207
+
208
+ if (slideCount === 0) {
209
+ console.error(' ERROR: No .slide elements found.');
210
+ console.error(' Make sure your HTML uses <section class="slide"> or <div class="slide">.');
211
+ await browser.close();
212
+ server.close();
213
+ process.exit(1);
214
+ }
215
+
216
+ // ─── Force all animation states to final visible state ────
217
+
218
+ await page.evaluate(() => {
219
+ // Trigger intersection observer targets
220
+ document.querySelectorAll('.slide').forEach(s => s.classList.add('visible'));
221
+ // Force reveal + stat-item animations to completed state
222
+ document.querySelectorAll('.reveal, .stat-item').forEach(el => {
223
+ el.style.setProperty('opacity', '1', 'important');
224
+ el.style.setProperty('transform', 'none', 'important');
225
+ el.style.setProperty('transition', 'none', 'important');
226
+ });
227
+ });
228
+
229
+ // ─── Inject print layout CSS ──────────────────────────────
230
+ // Each .slide becomes exactly one page in the PDF.
231
+ // @page sets the physical page size to match the ebook canvas.
232
+ // overflow: visible on html/body lets Playwright see all slides.
233
+
234
+ await page.addStyleTag({ content: `
235
+ @page {
236
+ size: ${VP_WIDTH}px ${VP_HEIGHT}px;
237
+ margin: 0;
238
+ }
239
+ @media print {
240
+ html, body {
241
+ overflow: visible !important;
242
+ height: auto !important;
243
+ width: ${VP_WIDTH}px !important;
244
+ scroll-snap-type: none !important;
245
+ }
246
+ .slide {
247
+ page-break-after: always !important;
248
+ break-after: page !important;
249
+ width: ${VP_WIDTH}px !important;
250
+ height: ${VP_HEIGHT}px !important;
251
+ min-height: ${VP_HEIGHT}px !important;
252
+ max-height: ${VP_HEIGHT}px !important;
253
+ overflow: hidden !important;
254
+ position: relative !important;
255
+ display: block !important;
256
+ scroll-snap-align: none !important;
257
+ }
258
+ .slide:last-child {
259
+ page-break-after: auto !important;
260
+ break-after: auto !important;
261
+ }
262
+ }
263
+ ` });
264
+
265
+ await page.waitForTimeout(200);
266
+
267
+ // ─── Export as vector PDF ─────────────────────────────────
268
+ // page.pdf() renders the live DOM — text stays as vectors,
269
+ // fonts stay as fonts, backgrounds render via printBackground.
270
+
271
+ console.log(` Generating vector PDF...`);
272
+
273
+ await page.pdf({
274
+ path: OUTPUT_PDF,
275
+ width: `${VP_WIDTH}px`,
276
+ height: `${VP_HEIGHT}px`,
277
+ printBackground: true,
278
+ margin: { top: 0, right: 0, bottom: 0, left: 0 },
279
+ });
280
+
281
+ await browser.close();
282
+ server.close();
283
+
284
+ console.log(` ✓ PDF saved to: ${OUTPUT_PDF}`);
285
+ EXPORT_SCRIPT
286
+
287
+ # ─── Step 3: Install Playwright in temp directory ──────────
288
+ # We install Playwright locally in the temp dir so the Node script can import it.
289
+ # This avoids polluting global packages and ensures the script is self-contained.
290
+
291
+ info "Setting up Playwright (headless browser for screenshots)..."
292
+ info "This may take a moment on first run..."
293
+ echo ""
294
+
295
+ cd "$TEMP_DIR"
296
+
297
+ # Create a minimal package.json so npm install works
298
+ cat > "$TEMP_DIR/package.json" << 'PKG'
299
+ { "name": "slide-export", "private": true, "type": "module" }
300
+ PKG
301
+
302
+ # Install Playwright into the temp directory
303
+ npm install playwright &>/dev/null || {
304
+ err "Failed to install Playwright."
305
+ err "Try running: npm install playwright"
306
+ rm -rf "$TEMP_DIR"
307
+ exit 1
308
+ }
309
+
310
+ # Ensure Chromium browser binary is downloaded
311
+ npx playwright install chromium 2>/dev/null || {
312
+ err "Failed to install Chromium browser for Playwright."
313
+ err "Try running manually: npx playwright install chromium"
314
+ rm -rf "$TEMP_DIR"
315
+ exit 1
316
+ }
317
+ ok "Playwright ready"
318
+ echo ""
319
+
320
+ # ─── Step 4: Run the export ───────────────────────────────
321
+
322
+ info "Exporting slides to PDF..."
323
+ echo ""
324
+
325
+ if [[ "$COMPACT" == "true" ]]; then
326
+ info "Using compact mode (1280×720) for smaller file size"
327
+ fi
328
+ if [[ "$PORTRAIT" == "true" ]]; then
329
+ info "Using portrait mode (1200×1697) — A4 portrait format"
330
+ fi
331
+
332
+ node "$TEMP_SCRIPT" "$SERVE_DIR" "$HTML_FILENAME" "$OUTPUT_PDF" "$VIEWPORT_W" "$VIEWPORT_H" || {
333
+ err "PDF export failed."
334
+ rm -rf "$TEMP_DIR"
335
+ exit 1
336
+ }
337
+
338
+ # ─── Step 5: Cleanup and success ──────────────────────────
339
+
340
+ rm -rf "$TEMP_DIR"
341
+
342
+ echo ""
343
+ echo -e "${BOLD}════════════════════════════════════════${NC}"
344
+ ok "PDF exported successfully!"
345
+ echo ""
346
+ echo -e " ${BOLD}File:${NC} $OUTPUT_PDF"
347
+ echo ""
348
+ FILE_SIZE=$(du -h "$OUTPUT_PDF" | cut -f1 | xargs)
349
+ echo " Size: $FILE_SIZE"
350
+ echo ""
351
+ echo " This PDF works everywhere — email, Slack, Notion, print."
352
+ echo " Note: Animations are not preserved (it's a static export)."
353
+ echo -e "${BOLD}════════════════════════════════════════${NC}"
354
+ echo ""
355
+
356
+ # Open the PDF automatically
357
+ if command -v open &>/dev/null; then
358
+ open "$OUTPUT_PDF"
359
+ elif command -v xdg-open &>/dev/null; then
360
+ xdg-open "$OUTPUT_PDF"
361
+ fi