portable-agent-layer 0.29.1 → 0.30.1
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/assets/skills/consulting-report/tools/generate-pdf.ts +4 -5
- package/assets/skills/create-skill/SKILL.md +55 -18
- package/assets/skills/presentation/README.md +11 -5
- package/assets/skills/presentation/SKILL.md +93 -17
- package/assets/skills/presentation/demo/slides/011a-big-stat.md +6 -0
- package/assets/skills/presentation/demo/slides/011b-metric-grid.md +22 -0
- package/assets/skills/presentation/demo/slides/011c-pull-quote.md +6 -0
- package/assets/skills/presentation/demo/slides/012-closing.md +2 -2
- package/assets/skills/presentation/template/README.md +4 -3
- package/assets/skills/presentation/theme-base/base.css +193 -35
- package/assets/skills/presentation/theme-base/layouts.css +224 -42
- package/assets/skills/presentation/tools/build.ts +70 -26
- package/assets/skills/presentation/tools/doctor.ts +406 -0
- package/assets/skills/presentation/tools/new-deck.ts +9 -2
- package/package.json +3 -1
- package/src/targets/lib.ts +1 -4
- package/assets/skills/presentation/tools/present.ts +0 -70
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/* presentation skill — theme-base/layouts.css
|
|
2
2
|
* Per-layout styling via [data-layout="..."] attribute selectors.
|
|
3
|
-
*
|
|
3
|
+
* 15 layouts. Brand-neutral — every color references --brand-* / --neutral-* tokens.
|
|
4
|
+
*/
|
|
4
5
|
|
|
5
6
|
/* Reveal default is centered text; we want left for content slides. */
|
|
6
7
|
.reveal .slides > section { text-align: left; }
|
|
7
8
|
|
|
8
|
-
/* ── 1. title ── Cover slide */
|
|
9
|
+
/* ── 1. title ── Cover slide (centered, brand mark + accent rule under title) */
|
|
9
10
|
.reveal section[data-layout="title"] {
|
|
10
11
|
text-align: center !important;
|
|
11
12
|
display: flex !important;
|
|
@@ -14,29 +15,40 @@
|
|
|
14
15
|
align-items: center;
|
|
15
16
|
padding: var(--space-5) var(--space-4);
|
|
16
17
|
}
|
|
18
|
+
.reveal section[data-layout="title"]::before {
|
|
19
|
+
content: '';
|
|
20
|
+
background: var(--brand-logo) no-repeat center / contain;
|
|
21
|
+
width: 320px;
|
|
22
|
+
height: 96px;
|
|
23
|
+
margin-bottom: var(--space-4);
|
|
24
|
+
display: block;
|
|
25
|
+
}
|
|
17
26
|
.reveal section[data-layout="title"] h1 {
|
|
18
|
-
font-size:
|
|
27
|
+
font-size: var(--text-4xl);
|
|
19
28
|
margin-bottom: var(--space-2);
|
|
20
29
|
color: var(--brand-primary);
|
|
30
|
+
letter-spacing: var(--tracking-tighter);
|
|
31
|
+
}
|
|
32
|
+
.reveal section[data-layout="title"] h1::after {
|
|
33
|
+
content: '';
|
|
34
|
+
display: block;
|
|
35
|
+
width: 64px;
|
|
36
|
+
height: 4px;
|
|
37
|
+
background: var(--brand-accent);
|
|
38
|
+
margin: var(--space-2) auto 0;
|
|
39
|
+
border-radius: var(--radius-full);
|
|
21
40
|
}
|
|
22
41
|
.reveal section[data-layout="title"] h2 {
|
|
23
|
-
font-size:
|
|
42
|
+
font-size: var(--text-lg);
|
|
24
43
|
font-weight: 400;
|
|
25
44
|
color: var(--brand-muted);
|
|
26
45
|
margin-bottom: var(--space-4);
|
|
27
|
-
|
|
28
|
-
.reveal section[data-layout="title"]::before {
|
|
29
|
-
content: '';
|
|
30
|
-
background: var(--brand-logo) no-repeat center / contain;
|
|
31
|
-
width: 320px;
|
|
32
|
-
height: 96px;
|
|
33
|
-
margin-bottom: var(--space-4);
|
|
34
|
-
display: block;
|
|
46
|
+
letter-spacing: var(--tracking-normal);
|
|
35
47
|
}
|
|
36
48
|
|
|
37
|
-
/* ── 2. section ── Section divider,
|
|
49
|
+
/* ── 2. section ── Section divider, gradient bg, accent rule */
|
|
38
50
|
.reveal section[data-layout="section"] {
|
|
39
|
-
background: var(--brand-primary);
|
|
51
|
+
background: linear-gradient(135deg, var(--brand-primary) 0%, var(--brand-primary-800) 100%);
|
|
40
52
|
color: #fff;
|
|
41
53
|
text-align: left !important;
|
|
42
54
|
display: flex !important;
|
|
@@ -44,26 +56,35 @@
|
|
|
44
56
|
justify-content: center;
|
|
45
57
|
padding: var(--space-5);
|
|
46
58
|
}
|
|
59
|
+
.reveal section[data-layout="section"]::before {
|
|
60
|
+
content: '';
|
|
61
|
+
display: block;
|
|
62
|
+
width: 64px;
|
|
63
|
+
height: 4px;
|
|
64
|
+
background: var(--brand-accent);
|
|
65
|
+
margin-bottom: var(--space-3);
|
|
66
|
+
border-radius: var(--radius-full);
|
|
67
|
+
}
|
|
47
68
|
.reveal section[data-layout="section"] h1 {
|
|
48
69
|
color: #fff;
|
|
49
|
-
font-size:
|
|
70
|
+
font-size: var(--text-3xl);
|
|
50
71
|
font-weight: 700;
|
|
51
72
|
margin-bottom: var(--space-2);
|
|
73
|
+
letter-spacing: var(--tracking-tighter);
|
|
52
74
|
}
|
|
53
75
|
.reveal section[data-layout="section"] h2,
|
|
54
76
|
.reveal section[data-layout="section"] h3 {
|
|
55
77
|
color: rgba(255,255,255,0.7);
|
|
56
|
-
font-size:
|
|
78
|
+
font-size: var(--text-lg);
|
|
57
79
|
font-weight: 400;
|
|
58
80
|
margin-bottom: var(--space-2);
|
|
59
81
|
}
|
|
60
82
|
.reveal section[data-layout="section"] p {
|
|
61
83
|
color: rgba(255,255,255,0.85);
|
|
62
|
-
font-size:
|
|
84
|
+
font-size: var(--text-lg);
|
|
63
85
|
}
|
|
64
|
-
.reveal section[data-layout="section"]::after { display: none !important; }
|
|
65
86
|
|
|
66
|
-
/* ── 3. content ── default;
|
|
87
|
+
/* ── 3. content ── default; styled in base.css */
|
|
67
88
|
|
|
68
89
|
/* ── 4. two-column ── */
|
|
69
90
|
.reveal section[data-layout="two-column"] .col-left,
|
|
@@ -90,13 +111,12 @@
|
|
|
90
111
|
.reveal section[data-layout="image-text"] .image img,
|
|
91
112
|
.reveal section[data-layout="image-text"] .image svg {
|
|
92
113
|
width: 100%; height: auto; max-height: 60vh; object-fit: contain;
|
|
93
|
-
border-radius:
|
|
94
|
-
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
|
|
114
|
+
border-radius: var(--radius-md);
|
|
95
115
|
display: block;
|
|
96
116
|
}
|
|
97
117
|
.reveal section[data-layout="image-text"] .text { padding-left: var(--space-3); }
|
|
98
118
|
|
|
99
|
-
/* ── 6. quote ── */
|
|
119
|
+
/* ── 6. quote ── Centered quote with oversized accent glyph */
|
|
100
120
|
.reveal section[data-layout="quote"] {
|
|
101
121
|
text-align: center !important;
|
|
102
122
|
display: flex !important;
|
|
@@ -107,26 +127,30 @@
|
|
|
107
127
|
.reveal section[data-layout="quote"] blockquote {
|
|
108
128
|
border-left: none;
|
|
109
129
|
background: transparent;
|
|
110
|
-
font-size:
|
|
130
|
+
font-size: var(--text-xl);
|
|
111
131
|
font-style: italic;
|
|
112
132
|
color: var(--brand-fg);
|
|
113
133
|
padding: 0;
|
|
114
134
|
margin: 0 auto;
|
|
115
135
|
max-width: 80%;
|
|
116
136
|
position: relative;
|
|
137
|
+
line-height: var(--leading-snug);
|
|
138
|
+
border-radius: 0;
|
|
117
139
|
}
|
|
118
140
|
.reveal section[data-layout="quote"] blockquote::before {
|
|
119
141
|
content: '"';
|
|
120
142
|
font-family: Georgia, serif;
|
|
121
|
-
font-size:
|
|
122
|
-
color: var(--brand-accent);
|
|
143
|
+
font-size: 5em;
|
|
144
|
+
color: var(--brand-accent-300);
|
|
123
145
|
position: absolute;
|
|
124
146
|
top: -0.5em;
|
|
125
|
-
left: -0.
|
|
147
|
+
left: -0.5em;
|
|
126
148
|
line-height: 1;
|
|
149
|
+
z-index: 0;
|
|
127
150
|
}
|
|
151
|
+
.reveal section[data-layout="quote"] blockquote > * { position: relative; z-index: 1; }
|
|
128
152
|
|
|
129
|
-
/* ── 7. closing ── */
|
|
153
|
+
/* ── 7. closing ── Thank-you / Q&A on gradient brand BG */
|
|
130
154
|
.reveal section[data-layout="closing"] {
|
|
131
155
|
text-align: center !important;
|
|
132
156
|
display: flex !important;
|
|
@@ -134,23 +158,33 @@
|
|
|
134
158
|
justify-content: center;
|
|
135
159
|
align-items: center;
|
|
136
160
|
padding: var(--space-5) var(--space-4);
|
|
137
|
-
background: var(--brand-primary);
|
|
161
|
+
background: linear-gradient(135deg, var(--brand-primary) 0%, var(--brand-primary-800) 100%);
|
|
138
162
|
color: #fff;
|
|
139
163
|
}
|
|
140
164
|
.reveal section[data-layout="closing"] h1 {
|
|
141
165
|
color: #fff;
|
|
142
|
-
font-size:
|
|
166
|
+
font-size: var(--text-4xl);
|
|
143
167
|
margin-bottom: var(--space-2);
|
|
168
|
+
letter-spacing: var(--tracking-tighter);
|
|
169
|
+
}
|
|
170
|
+
.reveal section[data-layout="closing"] h1::after {
|
|
171
|
+
content: '';
|
|
172
|
+
display: block;
|
|
173
|
+
width: 64px;
|
|
174
|
+
height: 4px;
|
|
175
|
+
background: var(--brand-accent);
|
|
176
|
+
margin: var(--space-2) auto 0;
|
|
177
|
+
border-radius: var(--radius-full);
|
|
144
178
|
}
|
|
145
179
|
.reveal section[data-layout="closing"] h2 {
|
|
146
180
|
color: rgba(255,255,255,0.85);
|
|
147
181
|
font-weight: 400;
|
|
148
|
-
font-size:
|
|
182
|
+
font-size: var(--text-xl);
|
|
149
183
|
}
|
|
150
184
|
.reveal section[data-layout="closing"] p { color: rgba(255,255,255,0.85); }
|
|
151
|
-
.reveal section[data-layout="closing"]::after { display: none !important; }
|
|
152
185
|
|
|
153
|
-
/* ── 8. agenda ──
|
|
186
|
+
/* ── 8. agenda ── Numbered ol, generous whitespace.
|
|
187
|
+
* Sized so 10 items fit comfortably; if you have more, split into two slides. */
|
|
154
188
|
.reveal section[data-layout="agenda"] ol {
|
|
155
189
|
margin: var(--space-2) 0 0 0;
|
|
156
190
|
padding-left: 0;
|
|
@@ -159,8 +193,9 @@
|
|
|
159
193
|
}
|
|
160
194
|
.reveal section[data-layout="agenda"] ol li {
|
|
161
195
|
counter-increment: agenda;
|
|
162
|
-
font-size:
|
|
163
|
-
|
|
196
|
+
font-size: var(--text-sm);
|
|
197
|
+
line-height: var(--leading-snug);
|
|
198
|
+
padding: 0.5rem 0;
|
|
164
199
|
border-bottom: 1px solid var(--brand-divider);
|
|
165
200
|
position: relative;
|
|
166
201
|
padding-left: 2.6em;
|
|
@@ -170,12 +205,12 @@
|
|
|
170
205
|
content: counter(agenda, decimal-leading-zero);
|
|
171
206
|
position: absolute;
|
|
172
207
|
left: 0;
|
|
173
|
-
top: 0.
|
|
208
|
+
top: 0.5rem;
|
|
174
209
|
color: var(--brand-accent);
|
|
175
210
|
font-weight: 700;
|
|
176
|
-
font-size:
|
|
211
|
+
font-size: var(--text-sm);
|
|
177
212
|
font-family: var(--font-display);
|
|
178
|
-
letter-spacing:
|
|
213
|
+
letter-spacing: var(--tracking-wide);
|
|
179
214
|
}
|
|
180
215
|
|
|
181
216
|
/* ── 9. table ── styling already in base; just ensure breathing room */
|
|
@@ -183,34 +218,181 @@
|
|
|
183
218
|
margin-top: var(--space-3);
|
|
184
219
|
}
|
|
185
220
|
|
|
186
|
-
/* ── 10. comparison ── */
|
|
221
|
+
/* ── 10. comparison ── 2-3 option boxes with numbered badge + top stripe */
|
|
187
222
|
.reveal section[data-layout="comparison"] .compare {
|
|
188
223
|
display: flex;
|
|
189
224
|
gap: var(--space-3);
|
|
190
225
|
margin-top: var(--space-3);
|
|
191
226
|
align-items: stretch;
|
|
227
|
+
counter-reset: cmp;
|
|
192
228
|
}
|
|
193
229
|
.reveal section[data-layout="comparison"] .option {
|
|
194
230
|
flex: 1;
|
|
195
231
|
background: var(--brand-surface);
|
|
196
232
|
padding: var(--space-3);
|
|
197
|
-
border-radius:
|
|
233
|
+
border-radius: var(--radius-md);
|
|
198
234
|
border-top: 4px solid var(--brand-accent);
|
|
199
|
-
font-size:
|
|
235
|
+
font-size: var(--text-sm);
|
|
236
|
+
position: relative;
|
|
237
|
+
counter-increment: cmp;
|
|
238
|
+
box-shadow: var(--shadow-sm);
|
|
239
|
+
}
|
|
240
|
+
.reveal section[data-layout="comparison"] .option::before {
|
|
241
|
+
content: counter(cmp, decimal-leading-zero);
|
|
242
|
+
position: absolute;
|
|
243
|
+
top: var(--space-1);
|
|
244
|
+
right: var(--space-2);
|
|
245
|
+
font-family: var(--font-display);
|
|
246
|
+
font-size: var(--text-sm);
|
|
247
|
+
font-weight: 700;
|
|
248
|
+
color: var(--brand-accent);
|
|
249
|
+
letter-spacing: var(--tracking-wide);
|
|
250
|
+
opacity: 0.6;
|
|
200
251
|
}
|
|
201
252
|
.reveal section[data-layout="comparison"] .option:nth-child(1) { border-top-color: var(--brand-primary); }
|
|
253
|
+
.reveal section[data-layout="comparison"] .option:nth-child(1)::before { color: var(--brand-primary); }
|
|
202
254
|
.reveal section[data-layout="comparison"] .option p:first-child strong:first-child,
|
|
203
255
|
.reveal section[data-layout="comparison"] .option > strong:first-child {
|
|
204
256
|
display: block;
|
|
205
|
-
font-size:
|
|
257
|
+
font-size: var(--text-lg);
|
|
206
258
|
margin-bottom: var(--space-2);
|
|
207
259
|
color: var(--brand-primary);
|
|
208
260
|
}
|
|
209
261
|
|
|
210
|
-
/* ── 11. code ──
|
|
262
|
+
/* ── 11. code ── Code-focused; pre fills more vertical space */
|
|
211
263
|
.reveal section[data-layout="code"] pre {
|
|
212
264
|
width: 100%;
|
|
213
265
|
max-height: 65vh;
|
|
214
266
|
margin: var(--space-2) 0;
|
|
215
267
|
}
|
|
216
268
|
.reveal section[data-layout="code"] pre code { font-size: 0.9em; }
|
|
269
|
+
|
|
270
|
+
/* ── 12. big-stat ── One giant number + a muted caption beneath. Use sparingly.
|
|
271
|
+
* Author surface:
|
|
272
|
+
* <!-- .slide: data-layout="big-stat" -->
|
|
273
|
+
* # 87%
|
|
274
|
+
* ## of customers retained YoY
|
|
275
|
+
*/
|
|
276
|
+
.reveal section[data-layout="big-stat"] {
|
|
277
|
+
text-align: center !important;
|
|
278
|
+
display: flex !important;
|
|
279
|
+
flex-direction: column;
|
|
280
|
+
justify-content: center;
|
|
281
|
+
align-items: center;
|
|
282
|
+
padding: var(--space-5);
|
|
283
|
+
}
|
|
284
|
+
.reveal section[data-layout="big-stat"] h1 {
|
|
285
|
+
font-family: var(--font-display);
|
|
286
|
+
font-size: var(--text-display);
|
|
287
|
+
font-weight: 800;
|
|
288
|
+
line-height: 0.9;
|
|
289
|
+
letter-spacing: var(--tracking-tighter);
|
|
290
|
+
color: var(--brand-primary);
|
|
291
|
+
margin: 0 0 var(--space-2) 0;
|
|
292
|
+
font-feature-settings: "tnum";
|
|
293
|
+
}
|
|
294
|
+
.reveal section[data-layout="big-stat"] h2,
|
|
295
|
+
.reveal section[data-layout="big-stat"] p {
|
|
296
|
+
font-size: var(--text-lg);
|
|
297
|
+
color: var(--brand-muted);
|
|
298
|
+
font-weight: 400;
|
|
299
|
+
max-width: 70%;
|
|
300
|
+
margin: 0 0 var(--space-1) 0;
|
|
301
|
+
line-height: var(--leading-snug);
|
|
302
|
+
text-align: center;
|
|
303
|
+
}
|
|
304
|
+
.reveal section[data-layout="big-stat"] em.unit {
|
|
305
|
+
font-size: 0.4em;
|
|
306
|
+
color: var(--brand-accent);
|
|
307
|
+
font-weight: 600;
|
|
308
|
+
font-style: normal;
|
|
309
|
+
margin-left: 0.1em;
|
|
310
|
+
letter-spacing: var(--tracking-normal);
|
|
311
|
+
vertical-align: top;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/* ── 13. metric-grid ── 3-up KPI cards. Wrap in <div class="metrics"> with .metric children.
|
|
315
|
+
* Author surface (HTML inside markdown):
|
|
316
|
+
* <div class="metrics">
|
|
317
|
+
* <div class="metric"><p class="label">MRR</p><p class="value">$48k</p><p class="delta up">+12%</p></div>
|
|
318
|
+
* ...
|
|
319
|
+
* </div>
|
|
320
|
+
*/
|
|
321
|
+
.reveal section[data-layout="metric-grid"] .metrics {
|
|
322
|
+
display: grid;
|
|
323
|
+
grid-template-columns: repeat(3, 1fr);
|
|
324
|
+
gap: var(--space-3);
|
|
325
|
+
margin-top: var(--space-3);
|
|
326
|
+
}
|
|
327
|
+
.reveal section[data-layout="metric-grid"] .metric {
|
|
328
|
+
background: var(--brand-surface);
|
|
329
|
+
border-radius: var(--radius-md);
|
|
330
|
+
padding: var(--space-3);
|
|
331
|
+
box-shadow: var(--shadow-sm);
|
|
332
|
+
border-top: 3px solid var(--brand-accent);
|
|
333
|
+
}
|
|
334
|
+
.reveal section[data-layout="metric-grid"] .metric:nth-child(3n+1) { border-top-color: var(--brand-primary); }
|
|
335
|
+
.reveal section[data-layout="metric-grid"] .metric:nth-child(3n+2) { border-top-color: var(--brand-accent); }
|
|
336
|
+
.reveal section[data-layout="metric-grid"] .metric:nth-child(3n) { border-top-color: var(--brand-primary-700); }
|
|
337
|
+
.reveal section[data-layout="metric-grid"] .metric .value {
|
|
338
|
+
font-family: var(--font-display);
|
|
339
|
+
font-size: var(--text-3xl);
|
|
340
|
+
font-weight: 700;
|
|
341
|
+
line-height: 1;
|
|
342
|
+
color: var(--brand-primary);
|
|
343
|
+
margin: 0 0 var(--space-1) 0;
|
|
344
|
+
letter-spacing: var(--tracking-tight);
|
|
345
|
+
}
|
|
346
|
+
.reveal section[data-layout="metric-grid"] .metric .label {
|
|
347
|
+
font-size: var(--text-xs);
|
|
348
|
+
color: var(--brand-muted);
|
|
349
|
+
text-transform: uppercase;
|
|
350
|
+
letter-spacing: var(--tracking-wide);
|
|
351
|
+
margin: 0 0 var(--space-1) 0;
|
|
352
|
+
font-weight: 600;
|
|
353
|
+
}
|
|
354
|
+
.reveal section[data-layout="metric-grid"] .metric .delta {
|
|
355
|
+
font-size: var(--text-sm);
|
|
356
|
+
font-weight: 600;
|
|
357
|
+
display: inline-block;
|
|
358
|
+
margin: 0;
|
|
359
|
+
}
|
|
360
|
+
.reveal section[data-layout="metric-grid"] .metric .delta.up { color: #16A34A; }
|
|
361
|
+
.reveal section[data-layout="metric-grid"] .metric .delta.down { color: #DC2626; }
|
|
362
|
+
|
|
363
|
+
/* ── 14. pull-quote ── Oversized italic with accent rule + attribution
|
|
364
|
+
* Author surface:
|
|
365
|
+
* <!-- .slide: data-layout="pull-quote" -->
|
|
366
|
+
* > Big sentence.
|
|
367
|
+
*
|
|
368
|
+
* — Attribution name, role
|
|
369
|
+
*/
|
|
370
|
+
.reveal section[data-layout="pull-quote"] {
|
|
371
|
+
display: flex !important;
|
|
372
|
+
flex-direction: column;
|
|
373
|
+
justify-content: center;
|
|
374
|
+
padding: var(--space-5);
|
|
375
|
+
}
|
|
376
|
+
.reveal section[data-layout="pull-quote"] blockquote {
|
|
377
|
+
font-size: var(--text-2xl);
|
|
378
|
+
font-style: italic;
|
|
379
|
+
font-weight: 300;
|
|
380
|
+
font-family: var(--font-display);
|
|
381
|
+
color: var(--brand-fg);
|
|
382
|
+
line-height: var(--leading-snug);
|
|
383
|
+
letter-spacing: var(--tracking-tight);
|
|
384
|
+
border-left: 4px solid var(--brand-accent);
|
|
385
|
+
background: transparent;
|
|
386
|
+
padding: 0 0 0 var(--space-3);
|
|
387
|
+
margin: 0;
|
|
388
|
+
border-radius: 0;
|
|
389
|
+
max-width: 90%;
|
|
390
|
+
}
|
|
391
|
+
.reveal section[data-layout="pull-quote"] blockquote + p {
|
|
392
|
+
margin-top: var(--space-3);
|
|
393
|
+
margin-left: calc(4px + var(--space-3));
|
|
394
|
+
font-size: var(--text-base);
|
|
395
|
+
font-style: normal;
|
|
396
|
+
color: var(--brand-muted);
|
|
397
|
+
font-weight: 500;
|
|
398
|
+
}
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
// presentation skill — build a deck folder to a
|
|
2
|
+
// presentation skill — build a deck folder to a self-contained HTML.
|
|
3
3
|
//
|
|
4
4
|
// Usage:
|
|
5
|
-
// bun build.ts <deck-dir>
|
|
5
|
+
// bun build.ts <deck-dir> [--out <dir>] [--force]
|
|
6
|
+
//
|
|
7
|
+
// Output:
|
|
8
|
+
// <out>/<deck-name>/<deck-name>.md concatenated slides (written first)
|
|
9
|
+
// <out>/<deck-name>/<deck-name>.html self-contained presentation
|
|
10
|
+
//
|
|
11
|
+
// --out defaults to process.cwd(). The deck-name subdir is always created
|
|
12
|
+
// inside --out, even when --out is explicitly provided. Existing files in
|
|
13
|
+
// the subdir are preserved unless --force is passed.
|
|
6
14
|
|
|
7
15
|
import { constants as fsConst } from "node:fs";
|
|
8
|
-
import { access, mkdir, readdir, writeFile } from "node:fs/promises";
|
|
9
|
-
import { join, resolve } from "node:path";
|
|
16
|
+
import { access, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
17
|
+
import { basename, join, resolve } from "node:path";
|
|
10
18
|
import { dataUri, escapeForTextarea, readText } from "./lib/inline";
|
|
11
19
|
import { THEME_BASE, VENDOR_REVEAL } from "./lib/paths";
|
|
12
20
|
import { getTemplate } from "./lib/registry";
|
|
@@ -50,14 +58,45 @@ async function exists(p: string): Promise<boolean> {
|
|
|
50
58
|
}
|
|
51
59
|
}
|
|
52
60
|
|
|
61
|
+
function deckSlug(deckDir: string): string {
|
|
62
|
+
// Filesystem-safe name. Falls back to "deck" if basename is empty (shouldn't happen).
|
|
63
|
+
const raw = basename(resolve(deckDir));
|
|
64
|
+
const slug = raw.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
65
|
+
return slug || "deck";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function buildConcat(deckDir: string): Promise<string> {
|
|
69
|
+
const slidesDir = join(deckDir, "slides");
|
|
70
|
+
if (await exists(slidesDir)) {
|
|
71
|
+
const files = (await readdir(slidesDir)).filter((f) => f.endsWith(".md")).sort();
|
|
72
|
+
if (files.length === 0) {
|
|
73
|
+
throw new Error(`slides/ is empty at ${slidesDir}`);
|
|
74
|
+
}
|
|
75
|
+
const parts = await Promise.all(files.map((f) => readText(join(slidesDir, f))));
|
|
76
|
+
return `${parts.map((p) => p.trim()).join("\n\n---\n\n")}\n`;
|
|
77
|
+
}
|
|
78
|
+
const legacy = join(deckDir, "content.md");
|
|
79
|
+
if (await exists(legacy)) {
|
|
80
|
+
return await readText(legacy);
|
|
81
|
+
}
|
|
82
|
+
throw new Error(`no slides/ directory or content.md found in ${deckDir}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
53
85
|
async function main() {
|
|
54
86
|
const argv = process.argv.slice(2);
|
|
55
87
|
if (argv.length === 0) {
|
|
56
|
-
console.error("usage: build.ts <deck-dir>");
|
|
88
|
+
console.error("usage: build.ts <deck-dir> [--out <dir>] [--force]");
|
|
57
89
|
process.exit(1);
|
|
58
90
|
}
|
|
59
91
|
const deckDir = resolve(argv[0]);
|
|
60
92
|
|
|
93
|
+
let outRoot = process.cwd();
|
|
94
|
+
let force = false;
|
|
95
|
+
for (let i = 1; i < argv.length; i++) {
|
|
96
|
+
if (argv[i] === "--out") outRoot = resolve(argv[++i]);
|
|
97
|
+
else if (argv[i] === "--force") force = true;
|
|
98
|
+
}
|
|
99
|
+
|
|
61
100
|
const cfgPath = join(deckDir, "slides.config.yml");
|
|
62
101
|
if (!(await exists(cfgPath))) {
|
|
63
102
|
console.error(`slides.config.yml not found at ${cfgPath}`);
|
|
@@ -70,6 +109,28 @@ async function main() {
|
|
|
70
109
|
process.exit(1);
|
|
71
110
|
}
|
|
72
111
|
|
|
112
|
+
const slug = deckSlug(deckDir);
|
|
113
|
+
const outDir = join(outRoot, slug);
|
|
114
|
+
const concatPath = join(outDir, `${slug}.md`);
|
|
115
|
+
const htmlPath = join(outDir, `${slug}.html`);
|
|
116
|
+
|
|
117
|
+
if (!force) {
|
|
118
|
+
const collisions: string[] = [];
|
|
119
|
+
if (await exists(concatPath)) collisions.push(concatPath);
|
|
120
|
+
if (await exists(htmlPath)) collisions.push(htmlPath);
|
|
121
|
+
if (collisions.length > 0) {
|
|
122
|
+
console.error("refusing to overwrite existing output (pass --force to replace):");
|
|
123
|
+
for (const p of collisions) console.error(` - ${p}`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Step 1 — concatenate slides to a single on-disk markdown artifact.
|
|
129
|
+
const concatMd = await buildConcat(deckDir);
|
|
130
|
+
await mkdir(outDir, { recursive: true });
|
|
131
|
+
await writeFile(concatPath, concatMd, "utf8");
|
|
132
|
+
|
|
133
|
+
// Step 2 — build self-contained HTML, sourcing content from the on-disk concat.
|
|
73
134
|
const template = await getTemplate(templateName);
|
|
74
135
|
const tplYml = parseSimpleYaml(await readText(join(template.path, "template.yml")));
|
|
75
136
|
const tplCss = await readText(join(template.path, "template.css"));
|
|
@@ -95,21 +156,7 @@ async function main() {
|
|
|
95
156
|
);
|
|
96
157
|
const notesJs = await readText(join(VENDOR_REVEAL, "plugin", "notes", "notes.js"));
|
|
97
158
|
|
|
98
|
-
|
|
99
|
-
// slides/ wins if present. Files inside are sorted by filename, then joined with the slide separator.
|
|
100
|
-
const slidesDir = join(deckDir, "slides");
|
|
101
|
-
let contentMd: string;
|
|
102
|
-
if (await exists(slidesDir)) {
|
|
103
|
-
const files = (await readdir(slidesDir)).filter((f) => f.endsWith(".md")).sort();
|
|
104
|
-
if (files.length === 0) {
|
|
105
|
-
console.error(`slides/ is empty at ${slidesDir}`);
|
|
106
|
-
process.exit(1);
|
|
107
|
-
}
|
|
108
|
-
const parts = await Promise.all(files.map((f) => readText(join(slidesDir, f))));
|
|
109
|
-
contentMd = parts.map((p) => p.trim()).join("\n\n---\n\n") + "\n";
|
|
110
|
-
} else {
|
|
111
|
-
contentMd = await readText(join(deckDir, "content.md"));
|
|
112
|
-
}
|
|
159
|
+
const contentMd = await readFile(concatPath, "utf8");
|
|
113
160
|
|
|
114
161
|
let deckOverridesCss = "";
|
|
115
162
|
const overridesPath = join(deckDir, "overrides.css");
|
|
@@ -144,14 +191,11 @@ async function main() {
|
|
|
144
191
|
};
|
|
145
192
|
|
|
146
193
|
const html = skeleton.replace(/\{\{(\w+)\}\}/g, (_, k) => subs[k] ?? "");
|
|
147
|
-
|
|
148
|
-
const distDir = join(deckDir, "dist");
|
|
149
|
-
await mkdir(distDir, { recursive: true });
|
|
150
|
-
const outPath = join(distDir, "index.html");
|
|
151
|
-
await writeFile(outPath, html, "utf8");
|
|
194
|
+
await writeFile(htmlPath, html, "utf8");
|
|
152
195
|
|
|
153
196
|
const sizeMb = (Buffer.byteLength(html) / 1024 / 1024).toFixed(2);
|
|
154
|
-
console.log(`✓
|
|
197
|
+
console.log(`✓ concat ${concatPath}`);
|
|
198
|
+
console.log(`✓ html ${htmlPath} (${sizeMb} MB self-contained)`);
|
|
155
199
|
}
|
|
156
200
|
|
|
157
201
|
main().catch((e) => {
|