slide-cli 1.0.0
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/README.md +184 -0
- package/TEMPLATE_GUIDE.md +661 -0
- package/dist/TEMPLATE_GUIDE.md +661 -0
- package/dist/index.js +277058 -0
- package/dist/scripts/build.js +41 -0
- package/dist/scripts/download-fonts.js +123 -0
- package/dist/templates/bold-title/sample.json +57 -0
- package/dist/templates/bold-title/template.html +212 -0
- package/dist/templates/bold-title/template.json +76 -0
- package/dist/templates/bold-title-wide/sample.json +58 -0
- package/dist/templates/bold-title-wide/template.html +224 -0
- package/dist/templates/bold-title-wide/template.json +76 -0
- package/dist/templates/minimal/sample.json +53 -0
- package/dist/templates/minimal/template.html +183 -0
- package/dist/templates/minimal/template.json +76 -0
- package/dist/templates/minimal-wide/sample.json +53 -0
- package/dist/templates/minimal-wide/template.html +208 -0
- package/dist/templates/minimal-wide/template.json +76 -0
- package/dist/templates/quote-card/sample.json +57 -0
- package/dist/templates/quote-card/template.html +203 -0
- package/dist/templates/quote-card/template.json +76 -0
- package/dist/templates/quote-card-wide/sample.json +58 -0
- package/dist/templates/quote-card-wide/template.html +215 -0
- package/dist/templates/quote-card-wide/template.json +76 -0
- package/package.json +66 -0
- package/scripts/build.js +41 -0
- package/scripts/download-fonts.js +123 -0
- package/templates/bold-title/sample.json +57 -0
- package/templates/bold-title/template.html +212 -0
- package/templates/bold-title/template.json +76 -0
- package/templates/bold-title-wide/sample.json +58 -0
- package/templates/bold-title-wide/template.html +224 -0
- package/templates/bold-title-wide/template.json +76 -0
- package/templates/minimal/sample.json +53 -0
- package/templates/minimal/template.html +183 -0
- package/templates/minimal/template.json +76 -0
- package/templates/minimal-wide/sample.json +53 -0
- package/templates/minimal-wide/template.html +208 -0
- package/templates/minimal-wide/template.json +76 -0
- package/templates/quote-card/sample.json +57 -0
- package/templates/quote-card/template.html +203 -0
- package/templates/quote-card/template.json +76 -0
- package/templates/quote-card-wide/sample.json +58 -0
- package/templates/quote-card-wide/template.html +215 -0
- package/templates/quote-card-wide/template.json +76 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<style>
|
|
6
|
+
|
|
7
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
8
|
+
|
|
9
|
+
html, body {
|
|
10
|
+
width: 1920px;
|
|
11
|
+
height: 1080px;
|
|
12
|
+
overflow: hidden;
|
|
13
|
+
background: {{bg}};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
body {
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
position: relative;
|
|
20
|
+
font-family: 'Lato', 'Helvetica Neue', Arial, sans-serif;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Paper texture */
|
|
24
|
+
body::before {
|
|
25
|
+
content: '';
|
|
26
|
+
position: absolute; inset: 0;
|
|
27
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='400'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='400' height='400' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E");
|
|
28
|
+
pointer-events: none; mix-blend-mode: multiply;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.header {
|
|
32
|
+
padding: 52px 96px 0;
|
|
33
|
+
display: flex;
|
|
34
|
+
align-items: center;
|
|
35
|
+
justify-content: space-between;
|
|
36
|
+
position: relative; z-index: 1;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.topic {
|
|
40
|
+
font-size: 22px; font-weight: 700;
|
|
41
|
+
letter-spacing: 0.16em; text-transform: uppercase;
|
|
42
|
+
color: {{accent}};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.header-num {
|
|
46
|
+
font-size: 22px; font-weight: 300;
|
|
47
|
+
color: {{ink}}; opacity: 0.35; letter-spacing: 0.1em;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.top-rule {
|
|
51
|
+
margin: 36px 96px 0;
|
|
52
|
+
height: 1px;
|
|
53
|
+
background: linear-gradient(90deg, {{accent}}, transparent);
|
|
54
|
+
opacity: 0.4;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* ── Main: two-column layout for wide ── */
|
|
58
|
+
.main {
|
|
59
|
+
flex: 1;
|
|
60
|
+
display: flex;
|
|
61
|
+
flex-direction: row;
|
|
62
|
+
align-items: stretch;
|
|
63
|
+
padding: 0 96px;
|
|
64
|
+
position: relative; z-index: 1;
|
|
65
|
+
gap: 80px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* Left column: the quote itself */
|
|
69
|
+
.quote-col {
|
|
70
|
+
flex: 1 1 0;
|
|
71
|
+
display: flex;
|
|
72
|
+
flex-direction: column;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
position: relative;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Right column: attribution */
|
|
78
|
+
.attr-col {
|
|
79
|
+
flex: 0 0 480px;
|
|
80
|
+
display: flex;
|
|
81
|
+
flex-direction: column;
|
|
82
|
+
justify-content: center;
|
|
83
|
+
border-left: 1px solid {{ink}};
|
|
84
|
+
border-left-color: {{ink}};
|
|
85
|
+
opacity-reference: 0.08;
|
|
86
|
+
padding-left: 64px;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* SVG quotation mark */
|
|
90
|
+
.open-quote {
|
|
91
|
+
position: absolute;
|
|
92
|
+
top: -10px; left: -24px;
|
|
93
|
+
pointer-events: none;
|
|
94
|
+
opacity: 0.10;
|
|
95
|
+
width: 200px; height: 170px;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.quote-text {
|
|
99
|
+
font-family: 'Playfair Display', 'Noto Serif CJK', Georgia, 'Times New Roman', serif;
|
|
100
|
+
font-size: 64px; font-weight: 700; font-style: italic;
|
|
101
|
+
line-height: 1.3; letter-spacing: -0.01em;
|
|
102
|
+
color: {{ink}};
|
|
103
|
+
position: relative; z-index: 1;
|
|
104
|
+
text-wrap: balance;
|
|
105
|
+
word-break: keep-all;
|
|
106
|
+
overflow-wrap: break-word;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* ── Attribution ── */
|
|
110
|
+
.portrait {
|
|
111
|
+
flex: 0 0 110px;
|
|
112
|
+
width: 110px; height: 110px;
|
|
113
|
+
border-radius: 50%;
|
|
114
|
+
overflow: hidden;
|
|
115
|
+
border: 3px solid {{accent}};
|
|
116
|
+
opacity: 0.92;
|
|
117
|
+
margin-bottom: 28px;
|
|
118
|
+
}
|
|
119
|
+
.portrait img {
|
|
120
|
+
width: 100%; height: 100%;
|
|
121
|
+
object-fit: cover; object-position: center top;
|
|
122
|
+
display: block;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.attribution-text { display: flex; flex-direction: column; gap: 10px; }
|
|
126
|
+
|
|
127
|
+
.attr-rule { width: 52px; height: 3px; background: {{accent}}; margin-bottom: 4px; }
|
|
128
|
+
|
|
129
|
+
.author-name {
|
|
130
|
+
font-family: 'Playfair Display', 'Noto Serif CJK', Georgia, 'Times New Roman', serif;
|
|
131
|
+
font-size: 36px; font-weight: 700;
|
|
132
|
+
color: {{ink}}; letter-spacing: 0.01em;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.author-role {
|
|
136
|
+
font-size: 26px; font-weight: 400;
|
|
137
|
+
color: {{ink}}; opacity: 0.6; letter-spacing: 0.04em;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.footer {
|
|
141
|
+
padding: 0 96px 44px;
|
|
142
|
+
display: flex;
|
|
143
|
+
align-items: flex-end;
|
|
144
|
+
justify-content: space-between;
|
|
145
|
+
position: relative; z-index: 1;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.bottom-rule {
|
|
149
|
+
flex: 1; height: 1px;
|
|
150
|
+
background: {{ink}}; opacity: 0.1;
|
|
151
|
+
margin-right: 40px; margin-bottom: 8px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.slide-info {
|
|
155
|
+
font-size: 22px; font-weight: 400;
|
|
156
|
+
color: {{ink}}; opacity: 0.38;
|
|
157
|
+
letter-spacing: 0.1em; white-space: nowrap;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.corner-br {
|
|
161
|
+
position: absolute; bottom: 52px; right: 80px;
|
|
162
|
+
width: 48px; height: 48px;
|
|
163
|
+
border-right: 2px solid {{accent}};
|
|
164
|
+
border-bottom: 2px solid {{accent}};
|
|
165
|
+
opacity: 0.25;
|
|
166
|
+
}
|
|
167
|
+
.corner-tl {
|
|
168
|
+
position: absolute; top: 52px; left: 80px;
|
|
169
|
+
width: 48px; height: 48px;
|
|
170
|
+
border-left: 2px solid {{accent}};
|
|
171
|
+
border-top: 2px solid {{accent}};
|
|
172
|
+
opacity: 0.25;
|
|
173
|
+
}
|
|
174
|
+
</style>
|
|
175
|
+
</head>
|
|
176
|
+
<body>
|
|
177
|
+
<div class="corner-tl"></div>
|
|
178
|
+
<div class="corner-br"></div>
|
|
179
|
+
|
|
180
|
+
<div class="header">
|
|
181
|
+
{{#if topic}}<span class="topic">{{topic}}</span>{{else}}<span></span>{{/if}}
|
|
182
|
+
<span class="header-num">{{slideIndex}}</span>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<div class="top-rule"></div>
|
|
186
|
+
|
|
187
|
+
<div class="main">
|
|
188
|
+
<div class="quote-col">
|
|
189
|
+
<svg class="open-quote" viewBox="0 0 200 160" xmlns="http://www.w3.org/2000/svg">
|
|
190
|
+
<path d="M20 130 C20 90 45 55 80 30 L70 15 C25 42 0 82 0 130 C0 148 12 160 28 160 C44 160 55 148 55 132 C55 116 44 104 28 104 C24 104 22 104 20 105 Z" fill="{{accent}}"/>
|
|
191
|
+
<path d="M110 130 C110 90 135 55 170 30 L160 15 C115 42 90 82 90 130 C90 148 102 160 118 160 C134 160 145 148 145 132 C145 116 134 104 118 104 C114 104 112 104 110 105 Z" fill="{{accent}}"/>
|
|
192
|
+
</svg>
|
|
193
|
+
<blockquote class="quote-text">{{quote}}</blockquote>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
{{#if author}}
|
|
197
|
+
<div class="attr-col">
|
|
198
|
+
{{#if image}}
|
|
199
|
+
<div class="portrait"><img src="{{image}}" alt="{{author}}"></div>
|
|
200
|
+
{{/if}}
|
|
201
|
+
<div class="attribution-text">
|
|
202
|
+
<div class="attr-rule"></div>
|
|
203
|
+
<div class="author-name">{{author}}</div>
|
|
204
|
+
{{#if role}}<div class="author-role">{{role}}</div>{{/if}}
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
{{/if}}
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<div class="footer">
|
|
211
|
+
<div class="bottom-rule"></div>
|
|
212
|
+
<div class="slide-info">{{slideIndex}} / {{totalSlides}}</div>
|
|
213
|
+
</div>
|
|
214
|
+
</body>
|
|
215
|
+
</html>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Quote Card Wide",
|
|
3
|
+
"id": "quote-card-wide",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Elegant pull-quote card with large quotation marks and attribution — 16:9 widescreen",
|
|
6
|
+
"author": "slide-cli",
|
|
7
|
+
"aspectRatio": "16:9",
|
|
8
|
+
"width": 1920,
|
|
9
|
+
"height": 1080,
|
|
10
|
+
"tags": ["quote", "elegant", "serif", "wide", "16:9"],
|
|
11
|
+
"slots": [
|
|
12
|
+
{
|
|
13
|
+
"id": "quote",
|
|
14
|
+
"type": "text",
|
|
15
|
+
"label": "Quote text",
|
|
16
|
+
"required": true,
|
|
17
|
+
"description": "The main quote (without quotation marks)"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": "author",
|
|
21
|
+
"type": "text",
|
|
22
|
+
"label": "Author name",
|
|
23
|
+
"required": false,
|
|
24
|
+
"default": "",
|
|
25
|
+
"description": "Who said it"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "role",
|
|
29
|
+
"type": "text",
|
|
30
|
+
"label": "Author role / context",
|
|
31
|
+
"required": false,
|
|
32
|
+
"default": "",
|
|
33
|
+
"description": "e.g. CEO, Philosopher, 1984"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "image",
|
|
37
|
+
"type": "image",
|
|
38
|
+
"label": "Author portrait",
|
|
39
|
+
"required": false,
|
|
40
|
+
"default": "",
|
|
41
|
+
"description": "Optional author portrait. Shown as a circle beside the author name. File path or https:// URL."
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "topic",
|
|
45
|
+
"type": "text",
|
|
46
|
+
"label": "Topic tag",
|
|
47
|
+
"required": false,
|
|
48
|
+
"default": "",
|
|
49
|
+
"description": "Short category label shown at top"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "bg",
|
|
53
|
+
"type": "color",
|
|
54
|
+
"label": "Background",
|
|
55
|
+
"required": false,
|
|
56
|
+
"default": "#faf7f2",
|
|
57
|
+
"description": "Card background"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "ink",
|
|
61
|
+
"type": "color",
|
|
62
|
+
"label": "Text color",
|
|
63
|
+
"required": false,
|
|
64
|
+
"default": "#1a1714",
|
|
65
|
+
"description": "Main text color"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"id": "accent",
|
|
69
|
+
"type": "color",
|
|
70
|
+
"label": "Accent color",
|
|
71
|
+
"required": false,
|
|
72
|
+
"default": "#c0392b",
|
|
73
|
+
"description": "Color for quotation marks and rule"
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "slide-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI to create beautiful slide cards (9:16, 16:9, 1:1) from JSON + HTML templates",
|
|
5
|
+
"module": "index.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": { "slide": "./dist/index.js" },
|
|
8
|
+
"scripts": {
|
|
9
|
+
"postinstall": "node scripts/download-fonts.js",
|
|
10
|
+
"start": "bun src/index.ts",
|
|
11
|
+
"dev": "bun --watch src/index.ts",
|
|
12
|
+
"build": "bun scripts/build.js",
|
|
13
|
+
"prepublishOnly": "bun scripts/build.js",
|
|
14
|
+
"test": "bun test",
|
|
15
|
+
"test:watch": "bun test --watch",
|
|
16
|
+
"test:coverage": "bun test --coverage"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"slides",
|
|
20
|
+
"presentation",
|
|
21
|
+
"cli",
|
|
22
|
+
"card",
|
|
23
|
+
"template",
|
|
24
|
+
"instagram",
|
|
25
|
+
"stories",
|
|
26
|
+
"reels",
|
|
27
|
+
"youtube",
|
|
28
|
+
"thumbnail",
|
|
29
|
+
"json",
|
|
30
|
+
"html",
|
|
31
|
+
"puppeteer",
|
|
32
|
+
"bun"
|
|
33
|
+
],
|
|
34
|
+
"author": "",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist/",
|
|
41
|
+
"templates/",
|
|
42
|
+
"scripts/",
|
|
43
|
+
"TEMPLATE_GUIDE.md",
|
|
44
|
+
"README.md"
|
|
45
|
+
],
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/bun": "latest"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"typescript": "^5"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@fontsource-variable/fraunces": "^5.2.9",
|
|
54
|
+
"@fontsource-variable/outfit": "^5.2.8",
|
|
55
|
+
"@fontsource/bebas-neue": "^5.2.7",
|
|
56
|
+
"@fontsource/dm-mono": "^5.2.7",
|
|
57
|
+
"@fontsource/lato": "^5.2.7",
|
|
58
|
+
"@fontsource/playfair-display": "^5.2.8",
|
|
59
|
+
"chalk": "^5.6.2",
|
|
60
|
+
"commander": "^14.0.3",
|
|
61
|
+
"handlebars": "^4.7.8",
|
|
62
|
+
"ora": "^9.3.0",
|
|
63
|
+
"puppeteer": "^24.40.0",
|
|
64
|
+
"zod": "^4.3.6"
|
|
65
|
+
}
|
|
66
|
+
}
|
package/scripts/build.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Cross-platform build script (works on Windows, macOS, Linux)
|
|
3
|
+
// Run with: bun scripts/build.js OR node scripts/build.js
|
|
4
|
+
|
|
5
|
+
import { cpSync, mkdirSync, copyFileSync, rmSync, existsSync } from "fs";
|
|
6
|
+
import { join, dirname } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import { execSync } from "child_process";
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const root = join(__dirname, "..");
|
|
12
|
+
const dist = join(root, "dist");
|
|
13
|
+
|
|
14
|
+
// Clean dist
|
|
15
|
+
if (existsSync(dist)) {
|
|
16
|
+
rmSync(dist, { recursive: true, force: true });
|
|
17
|
+
console.log("🗑 Cleaned dist/");
|
|
18
|
+
}
|
|
19
|
+
mkdirSync(dist, { recursive: true });
|
|
20
|
+
|
|
21
|
+
// Bundle TypeScript entry point
|
|
22
|
+
console.log("⚙️ Bundling src/index.ts …");
|
|
23
|
+
execSync(
|
|
24
|
+
"bun build ./src/index.ts --outdir ./dist --target node --format esm",
|
|
25
|
+
{ cwd: root, stdio: "inherit" }
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// Copy static assets
|
|
29
|
+
const copies = [
|
|
30
|
+
["templates", "dist/templates"],
|
|
31
|
+
["scripts", "dist/scripts"],
|
|
32
|
+
];
|
|
33
|
+
for (const [src, dest] of copies) {
|
|
34
|
+
cpSync(join(root, src), join(root, dest), { recursive: true });
|
|
35
|
+
console.log(`📁 Copied ${src}/ → ${dest}/`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
copyFileSync(join(root, "TEMPLATE_GUIDE.md"), join(root, "dist", "TEMPLATE_GUIDE.md"));
|
|
39
|
+
console.log("📄 Copied TEMPLATE_GUIDE.md → dist/");
|
|
40
|
+
|
|
41
|
+
console.log("\n✅ Build complete → dist/");
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* scripts/download-fonts.js
|
|
4
|
+
*
|
|
5
|
+
* Downloads Noto CJK fonts for unicode support (CJK, Korean, Japanese, French, etc.)
|
|
6
|
+
* These fonts are too large for npm packages (~20MB each) so we fetch them directly
|
|
7
|
+
* from the Noto project's GitHub releases.
|
|
8
|
+
*
|
|
9
|
+
* Fonts are stored in ./fonts/ and are gitignored.
|
|
10
|
+
* Re-run manually with: node scripts/download-fonts.js
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createWriteStream, mkdirSync, existsSync, statSync } from "fs";
|
|
14
|
+
import { get as httpsGet } from "https";
|
|
15
|
+
import { get as httpGet } from "http";
|
|
16
|
+
import { join, dirname } from "path";
|
|
17
|
+
import { fileURLToPath } from "url";
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const FONTS_DIR = join(__dirname, "..", "fonts");
|
|
21
|
+
|
|
22
|
+
const FONTS = [
|
|
23
|
+
{
|
|
24
|
+
name: "NotoSansCJK-Bold",
|
|
25
|
+
filename: "NotoSansCJK-Bold.ttc",
|
|
26
|
+
// Google Fonts CDN via GitHub Noto CJK releases
|
|
27
|
+
url: "https://github.com/notofonts/noto-cjk/raw/main/Sans/OTC/NotoSansCJK-Bold.ttc",
|
|
28
|
+
minSize: 15_000_000, // ~20MB — if smaller, download failed
|
|
29
|
+
description: "Noto Sans CJK Bold (Latin, French, CJK, Korean, Japanese)",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "NotoSansCJK-Black",
|
|
33
|
+
filename: "NotoSansCJK-Black.ttc",
|
|
34
|
+
url: "https://github.com/notofonts/noto-cjk/raw/main/Sans/OTC/NotoSansCJK-Black.ttc",
|
|
35
|
+
minSize: 15_000_000,
|
|
36
|
+
description: "Noto Sans CJK Black (display titles, weight 900)",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "NotoSerifCJK-Bold",
|
|
40
|
+
filename: "NotoSerifCJK-Bold.ttc",
|
|
41
|
+
url: "https://github.com/notofonts/noto-cjk/raw/main/Serif/OTC/NotoSerifCJK-Bold.ttc",
|
|
42
|
+
minSize: 15_000_000,
|
|
43
|
+
description: "Noto Serif CJK Bold (serif quotes, headings)",
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
mkdirSync(FONTS_DIR, { recursive: true });
|
|
48
|
+
|
|
49
|
+
function download(url, destPath, description) {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
// Skip if already present and large enough
|
|
52
|
+
if (existsSync(destPath)) {
|
|
53
|
+
const size = statSync(destPath).size;
|
|
54
|
+
const font = FONTS.find((f) => destPath.endsWith(f.filename));
|
|
55
|
+
if (font && size >= font.minSize) {
|
|
56
|
+
console.log(` ✓ ${description} (already downloaded)`);
|
|
57
|
+
resolve();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log(` ↓ Downloading ${description}...`);
|
|
63
|
+
const file = createWriteStream(destPath);
|
|
64
|
+
const getter = url.startsWith("https") ? httpsGet : httpGet;
|
|
65
|
+
|
|
66
|
+
const request = getter(url, { headers: { "User-Agent": "slide-cli/1.0" } }, (res) => {
|
|
67
|
+
// Follow redirects (GitHub uses 302)
|
|
68
|
+
if (res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307) {
|
|
69
|
+
file.close();
|
|
70
|
+
download(res.headers.location, destPath, description).then(resolve).catch(reject);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (res.statusCode !== 200) {
|
|
74
|
+
file.close();
|
|
75
|
+
reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
res.pipe(file);
|
|
79
|
+
file.on("finish", () => {
|
|
80
|
+
file.close();
|
|
81
|
+
const size = statSync(destPath).size;
|
|
82
|
+
console.log(` ✓ ${description} (${(size / 1_000_000).toFixed(1)}MB)`);
|
|
83
|
+
resolve();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
request.on("error", (err) => {
|
|
88
|
+
file.close();
|
|
89
|
+
reject(err);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function main() {
|
|
95
|
+
console.log("\nslide-cli: Setting up unicode fonts (Noto CJK)...");
|
|
96
|
+
console.log(` Destination: ${FONTS_DIR}\n`);
|
|
97
|
+
|
|
98
|
+
const failures = [];
|
|
99
|
+
|
|
100
|
+
for (const font of FONTS) {
|
|
101
|
+
const destPath = join(FONTS_DIR, font.filename);
|
|
102
|
+
try {
|
|
103
|
+
await download(font.url, destPath, font.description);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
failures.push({ font, err });
|
|
106
|
+
console.warn(` ⚠ Could not download ${font.name}: ${err.message}`);
|
|
107
|
+
console.warn(` Slides with CJK/accented text will fall back to system fonts.`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (failures.length === 0) {
|
|
112
|
+
console.log("\n ✓ All unicode fonts ready.\n");
|
|
113
|
+
} else {
|
|
114
|
+
console.log(`\n ⚠ ${failures.length} font(s) could not be downloaded.`);
|
|
115
|
+
console.log(` Re-run: node scripts/download-fonts.js\n`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
main().catch((err) => {
|
|
120
|
+
console.error("Font setup failed:", err.message);
|
|
121
|
+
// Non-zero exit would break bun install, so we warn and continue
|
|
122
|
+
process.exit(0);
|
|
123
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_template": "bold-title",
|
|
3
|
+
"_description": "High-impact editorial card with a gradient background, subtle grid overlay, and a giant Bebas Neue display title. Best for short punchy titles (1–3 words). Each slide can have its own gradient colors and highlight accent.",
|
|
4
|
+
"_slots": {
|
|
5
|
+
"title": "REQUIRED — the bold display title. Shown at 192px. Use text naturally — text-wrap:balance distributes lines evenly so any length looks intentional. Put context or explanation in subtitle, not in the title itself.",
|
|
6
|
+
"subtitle": "optional — one sentence of supporting text below the title",
|
|
7
|
+
"eyebrow": "optional — small uppercase label with a decorative line shown above the title",
|
|
8
|
+
"colorA": "optional — hex color for top of the gradient background (default: #1a0533)",
|
|
9
|
+
"colorB": "optional — hex color for bottom of the gradient background (default: #0d1f3c)",
|
|
10
|
+
"highlight": "optional — hex accent color used for the eyebrow text, dot, and rule (default: #ff6b35)",
|
|
11
|
+
"image": "optional — path or https:// URL. Used as a full-bleed background image behind the gradient. Dark, atmospheric photos work best (landscapes, textures, cityscapes)."
|
|
12
|
+
},
|
|
13
|
+
"title": "The Future Deck",
|
|
14
|
+
"slides": [
|
|
15
|
+
{
|
|
16
|
+
"layout": "bold-title",
|
|
17
|
+
"eyebrow": "The future is",
|
|
18
|
+
"title": "NOW",
|
|
19
|
+
"subtitle": "Everything you thought would take a decade is happening this year.",
|
|
20
|
+
"colorA": "#0d0221",
|
|
21
|
+
"colorB": "#1a0f2e",
|
|
22
|
+
"highlight": "#ff6b35",
|
|
23
|
+
"cta": "Stay curious →"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"layout": "bold-title",
|
|
27
|
+
"eyebrow": "Rule No. 2",
|
|
28
|
+
"title": "SHIP FAST",
|
|
29
|
+
"subtitle": "A good product shipped today beats a perfect product shipped never.",
|
|
30
|
+
"colorA": "#0a1628",
|
|
31
|
+
"colorB": "#051020",
|
|
32
|
+
"highlight": "#00d4ff",
|
|
33
|
+
"cta": "Move fast, learn faster"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"layout": "bold-title",
|
|
37
|
+
"eyebrow": "The bottom line",
|
|
38
|
+
"title": "BUILD",
|
|
39
|
+
"subtitle": "Ideas are worthless without execution. Start before you're ready.",
|
|
40
|
+
"colorA": "#1a0a00",
|
|
41
|
+
"colorB": "#0d1a00",
|
|
42
|
+
"highlight": "#f5c400",
|
|
43
|
+
"cta": "Start today"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"layout": "bold-title",
|
|
47
|
+
"eyebrow": "With image",
|
|
48
|
+
"title": "RISE",
|
|
49
|
+
"subtitle": "Great things are built one day at a time.",
|
|
50
|
+
"colorA": "#0a0a14",
|
|
51
|
+
"colorB": "#141428",
|
|
52
|
+
"highlight": "#a78bfa",
|
|
53
|
+
"image": "https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?w=1080",
|
|
54
|
+
"cta": "Keep climbing"
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|