@opendirectory.dev/skills 0.1.57 → 0.1.59
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.
- package/package.json +1 -1
- package/registry.json +18 -0
- package/skills/graphic-ebook/README.md +123 -0
- package/skills/graphic-ebook/SKILL.md +396 -0
- package/skills/graphic-ebook/assets/viewport-base.css +153 -0
- package/skills/graphic-ebook/evals/evals.json +35 -0
- package/skills/graphic-ebook/references/design-system.md +241 -0
- package/skills/graphic-ebook/references/page-library.md +461 -0
- package/skills/graphic-ebook/references/style-presets.md +346 -0
- package/skills/graphic-ebook/scripts/export-pdf.sh +361 -0
- package/skills/graphic-gif/README.md +99 -0
- package/skills/graphic-gif/SKILL.md +313 -0
- package/skills/graphic-gif/evals/evals.json +30 -0
- package/skills/graphic-gif/references/animation-library.md +446 -0
- package/skills/graphic-gif/references/style-presets.md +194 -0
- package/skills/graphic-gif/scripts/capture-and-encode.mjs +201 -0
- package/skills/graphic-gif/scripts/export-gif.sh +274 -0
|
@@ -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
|