lazyslides 0.2.2 → 0.3.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 +3 -2
- package/_includes/slides/diagram.njk +34 -0
- package/index.js +91 -0
- package/lib/validate.js +11 -0
- package/package.json +7 -4
- package/scaffold/CLAUDE.md +9 -0
- package/scaffold/claude-commands/add-slide.md +1 -0
- package/scaffold/claude-commands/add-template.md +1 -1
- package/scaffold/claude-commands/new-presentation.md +1 -0
- package/scaffold/claude-commands/validate.md +1 -1
- package/src/styles.css +64 -0
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ LazySlides separates them. You write slides as structured YAML — the format AI
|
|
|
26
26
|
|
|
27
27
|
## Features
|
|
28
28
|
|
|
29
|
-
- **
|
|
29
|
+
- **19 slide templates** — title, content, metrics, comparison, diagram, timeline, funnel, code, and more
|
|
30
30
|
- **Speaker view** — press S for notes, timer, and next-slide preview
|
|
31
31
|
- **Section progress bar** — visual progress dots with section markers; click to jump between sections
|
|
32
32
|
- **Source references** — attach citations to any slide, rendered as linked footnotes
|
|
@@ -128,7 +128,7 @@ Supporting files (outlines, notes, PDFs, images) go alongside `index.md` in the
|
|
|
128
128
|
|
|
129
129
|
## Templates
|
|
130
130
|
|
|
131
|
-
|
|
131
|
+
19 built-in templates cover common slide patterns:
|
|
132
132
|
|
|
133
133
|
| Template | Description |
|
|
134
134
|
|----------|-------------|
|
|
@@ -149,6 +149,7 @@ Supporting files (outlines, notes, PDFs, images) go alongside `index.md` in the
|
|
|
149
149
|
| `code` | Code snippet with syntax highlighting |
|
|
150
150
|
| `image-overlay` | Full image with positioned text box |
|
|
151
151
|
| `agenda` | Clickable table of contents (supports `auto_generate: true`) |
|
|
152
|
+
| `diagram` | D2 diagram compiled to inline SVG at build time |
|
|
152
153
|
|
|
153
154
|
See `CLAUDE.md` for the full field reference for each template.
|
|
154
155
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{# TEMPLATE: diagram — D2 diagram rendered to inline SVG at build time.
|
|
2
|
+
Fields:
|
|
3
|
+
title* — slide title
|
|
4
|
+
d2 — inline D2 source (multiline with |)
|
|
5
|
+
d2_file — path to .d2 file (alternative to inline d2)
|
|
6
|
+
caption — optional description below diagram
|
|
7
|
+
notes — speaker notes
|
|
8
|
+
reference / reference_link / references — source attribution
|
|
9
|
+
#}
|
|
10
|
+
{% from "slides/_section-attrs.njk" import sectionAttrs %}
|
|
11
|
+
<section class="slide-diagram"{{ sectionAttrs(slide) }}>
|
|
12
|
+
<div class="slide-body">
|
|
13
|
+
<div class="slide-header">
|
|
14
|
+
<h2>{{ slide.title }}</h2>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="diagram-container">
|
|
17
|
+
{% if slide.d2 %}
|
|
18
|
+
{{ slide.d2 | compileD2 | safe }}
|
|
19
|
+
{% elif slide.d2_file %}
|
|
20
|
+
<div class="d2-error">d2_file is not yet supported. Use inline d2 instead.</div>
|
|
21
|
+
{% else %}
|
|
22
|
+
<div class="d2-error">No d2 source provided. Add a 'd2' or 'd2_file' field.</div>
|
|
23
|
+
{% endif %}
|
|
24
|
+
</div>
|
|
25
|
+
{% if slide.caption %}<p class="diagram-caption">{{ slide.caption }}</p>{% endif %}
|
|
26
|
+
{% if slide.notes %}<aside class="notes">{{ slide.notes }}</aside>{% endif %}
|
|
27
|
+
</div>
|
|
28
|
+
{% if global_footer and not slide.hide_footer %}
|
|
29
|
+
{% set reference = slide.reference %}
|
|
30
|
+
{% set reference_link = slide.reference_link %}
|
|
31
|
+
{% set references = slide.references %}
|
|
32
|
+
{% include "slides/footer.njk" %}
|
|
33
|
+
{% endif %}
|
|
34
|
+
</section>
|
package/index.js
CHANGED
|
@@ -23,6 +23,97 @@ export default function lazyslides(eleventyConfig, options = {}) {
|
|
|
23
23
|
return val !== null && typeof val === "object" && !Array.isArray(val);
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
+
// ---------------------------------------------------------------
|
|
27
|
+
// 1b. D2 diagram compilation (async filter)
|
|
28
|
+
// ---------------------------------------------------------------
|
|
29
|
+
let d2Instance = null;
|
|
30
|
+
let d2Available = null; // null = unknown, true/false after check
|
|
31
|
+
async function getD2() {
|
|
32
|
+
if (d2Available === false) return null;
|
|
33
|
+
if (!d2Instance) {
|
|
34
|
+
try {
|
|
35
|
+
const { D2 } = await import("@terrastruct/d2");
|
|
36
|
+
d2Instance = new D2();
|
|
37
|
+
d2Available = true;
|
|
38
|
+
} catch {
|
|
39
|
+
d2Available = false;
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return d2Instance;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Pre-compile D2 diagrams before Nunjucks renders, because Nunjucks
|
|
47
|
+
// async filters/shortcodes are unreliable inside {% for %} loops.
|
|
48
|
+
// We store compiled SVGs in a map keyed by source hash, then look them
|
|
49
|
+
// up synchronously via a regular filter during template rendering.
|
|
50
|
+
const d2Cache = new Map();
|
|
51
|
+
|
|
52
|
+
eleventyConfig.addFilter("compileD2", (source) => {
|
|
53
|
+
if (!source) return "";
|
|
54
|
+
const cached = d2Cache.get(source);
|
|
55
|
+
if (cached) return cached;
|
|
56
|
+
return `<div class="d2-error">D2 diagram was not pre-compiled.</div>`;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
eleventyConfig.on("eleventy.before", async ({ directories }) => {
|
|
60
|
+
// Scan all presentation files for D2 source blocks and pre-compile them
|
|
61
|
+
const presentationsDir = path.join(directories.input || ".", "presentations");
|
|
62
|
+
if (!fs.existsSync(presentationsDir)) return;
|
|
63
|
+
|
|
64
|
+
const { default: yaml } = await import("js-yaml");
|
|
65
|
+
const entries = fs.readdirSync(presentationsDir, { withFileTypes: true });
|
|
66
|
+
const d2Sources = [];
|
|
67
|
+
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
if (!entry.isDirectory() || entry.name === "_template") continue;
|
|
70
|
+
const indexPath = path.join(presentationsDir, entry.name, "index.md");
|
|
71
|
+
if (!fs.existsSync(indexPath)) continue;
|
|
72
|
+
|
|
73
|
+
const content = fs.readFileSync(indexPath, "utf-8");
|
|
74
|
+
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
75
|
+
if (!fmMatch) continue;
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const data = yaml.load(fmMatch[1]);
|
|
79
|
+
if (!data?.slides) continue;
|
|
80
|
+
for (const slide of data.slides) {
|
|
81
|
+
if (slide.template === "diagram" && slide.d2) {
|
|
82
|
+
d2Sources.push(slide.d2);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} catch { /* skip invalid YAML */ }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (d2Sources.length === 0) return;
|
|
89
|
+
|
|
90
|
+
const d2 = await getD2();
|
|
91
|
+
if (!d2) {
|
|
92
|
+
console.log("[LazySlides] @terrastruct/d2 not installed — diagram slides will show placeholders. Install it with: pnpm add @terrastruct/d2");
|
|
93
|
+
for (const source of d2Sources) {
|
|
94
|
+
d2Cache.set(source, `<div class="d2-error">@terrastruct/d2 is not installed. Run: pnpm add @terrastruct/d2</div>`);
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
for (const source of d2Sources) {
|
|
99
|
+
try {
|
|
100
|
+
const result = await d2.compile(source);
|
|
101
|
+
const svg = await d2.render(result.diagram, {
|
|
102
|
+
...result.renderOptions,
|
|
103
|
+
noXMLTag: true,
|
|
104
|
+
pad: 20,
|
|
105
|
+
});
|
|
106
|
+
d2Cache.set(source, svg);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
const safeMsg = (err.message || String(err))
|
|
109
|
+
.replace(/&/g, "&")
|
|
110
|
+
.replace(/</g, "<")
|
|
111
|
+
.replace(/>/g, ">");
|
|
112
|
+
d2Cache.set(source, `<div class="d2-error">${safeMsg}</div>`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
26
117
|
// ---------------------------------------------------------------
|
|
27
118
|
// 2. Nunjucks search paths — makes {% include "slides/…" %} and
|
|
28
119
|
// layout: presentation.njk resolve to files inside the package.
|
package/lib/validate.js
CHANGED
|
@@ -7,6 +7,7 @@ const VALID_TEMPLATES = [
|
|
|
7
7
|
"title", "section", "content", "metrics", "comparison", "columns",
|
|
8
8
|
"three-columns", "quote", "center", "hero", "image-overlay", "code",
|
|
9
9
|
"timeline", "funnel", "split", "split-wide", "table", "agenda",
|
|
10
|
+
"diagram",
|
|
10
11
|
];
|
|
11
12
|
const VALID_TRANSITIONS = [
|
|
12
13
|
"none", "fade", "slide", "convex", "concave", "zoom",
|
|
@@ -128,6 +129,16 @@ export async function run(opts = {}) {
|
|
|
128
129
|
}
|
|
129
130
|
}
|
|
130
131
|
|
|
132
|
+
// Diagram template validation
|
|
133
|
+
if (slide.template === "diagram") {
|
|
134
|
+
if (!slide.d2 && !slide.d2_file) {
|
|
135
|
+
errors.push(`${file}: Slide ${slideNum} (diagram) missing 'd2' or 'd2_file' — one is required`);
|
|
136
|
+
}
|
|
137
|
+
if (slide.d2 && slide.d2_file) {
|
|
138
|
+
errors.push(`${file}: Slide ${slideNum} (diagram) has both 'd2' and 'd2_file' — use only one`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
131
142
|
// Image path validation
|
|
132
143
|
if (slide.image) {
|
|
133
144
|
const imagePath = slide.image;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lazyslides",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Slide decks with native agentic AI integration",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Chris Tietz",
|
|
@@ -52,13 +52,16 @@
|
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"js-yaml": "^4.1.0"
|
|
54
54
|
},
|
|
55
|
+
"optionalDependencies": {
|
|
56
|
+
"@terrastruct/d2": "^0.1.33"
|
|
57
|
+
},
|
|
55
58
|
"devDependencies": {
|
|
56
59
|
"@11ty/eleventy": "^3.0.0",
|
|
57
|
-
"@tailwindcss/cli": "
|
|
60
|
+
"@tailwindcss/cli": "4.1.18",
|
|
58
61
|
"concurrently": "^9.0.0",
|
|
59
62
|
"decktape": "^3.14.0",
|
|
60
|
-
"tailwindcss": "
|
|
61
|
-
"vitest": "^4.
|
|
63
|
+
"tailwindcss": "4.1.18",
|
|
64
|
+
"vitest": "^4.1.2"
|
|
62
65
|
},
|
|
63
66
|
"scripts": {
|
|
64
67
|
"dev": "node cli.js validate && concurrently \"pnpm:watch:*\"",
|
package/scaffold/CLAUDE.md
CHANGED
|
@@ -190,6 +190,15 @@ Fields marked with `*` are required.
|
|
|
190
190
|
- `slide`* — 0-based slide index to link to
|
|
191
191
|
- `reference` / `reference_link` / `references`
|
|
192
192
|
|
|
193
|
+
#### `diagram` — D2 diagram (compiled to inline SVG at build time)
|
|
194
|
+
- `title`* — slide title
|
|
195
|
+
- `d2` — inline D2 source string (use `|` for multiline YAML)
|
|
196
|
+
- `d2_file` — path to a `.d2` file (alternative to inline `d2`; not yet supported)
|
|
197
|
+
- `caption` — optional description below diagram
|
|
198
|
+
- `reference` / `reference_link` / `references`
|
|
199
|
+
|
|
200
|
+
Note: requires `@terrastruct/d2` npm dependency (included in the package). Exactly one of `d2` or `d2_file` must be provided.
|
|
201
|
+
|
|
193
202
|
## Common Patterns
|
|
194
203
|
|
|
195
204
|
### Nested lists
|
|
@@ -41,6 +41,7 @@ Ask the user which template to use. Show this reference:
|
|
|
41
41
|
| `code` | Code snippet with syntax highlighting |
|
|
42
42
|
| `image-overlay` | Full image with positioned text box |
|
|
43
43
|
| `agenda` | Clickable table of contents |
|
|
44
|
+
| `diagram` | D2 diagram compiled to inline SVG |
|
|
44
45
|
|
|
45
46
|
## Step 4: Gather Content
|
|
46
47
|
|
|
@@ -11,7 +11,7 @@ Ask the user:
|
|
|
11
11
|
|
|
12
12
|
## Step 2: Choose a Name
|
|
13
13
|
|
|
14
|
-
Help the user pick a kebab-case name for the template (e.g. `icon-grid`, `two-images`, `photo-grid`). The name must not conflict with the
|
|
14
|
+
Help the user pick a kebab-case name for the template (e.g. `icon-grid`, `two-images`, `photo-grid`). The name must not conflict with the 19 built-in templates: `title`, `section`, `content`, `center`, `hero`, `metrics`, `comparison`, `columns`, `quote`, `image-overlay`, `code`, `timeline`, `funnel`, `split`, `split-wide`, `table`, `agenda`, `diagram`.
|
|
15
15
|
|
|
16
16
|
## Step 3: Pick a Starting Point
|
|
17
17
|
|
|
@@ -90,6 +90,7 @@ Review each slide in the outline and propose a template. The available templates
|
|
|
90
90
|
| `table` | Data table with headers |
|
|
91
91
|
| `code` | Code snippet with syntax highlighting |
|
|
92
92
|
| `image-overlay` | Image with text overlay |
|
|
93
|
+
| `diagram` | D2 diagram compiled to inline SVG |
|
|
93
94
|
|
|
94
95
|
Read each template file's docblock comment for exact YAML structure and options.
|
|
95
96
|
|
|
@@ -19,7 +19,7 @@ Review the output and categorize issues:
|
|
|
19
19
|
- **Wrong comparison format** — comparison slides must use `rows:` with `left`/`right` pairs
|
|
20
20
|
|
|
21
21
|
### Warnings (should fix)
|
|
22
|
-
- **Unknown template** — template name doesn't match one of the
|
|
22
|
+
- **Unknown template** — template name doesn't match one of the 19 valid types
|
|
23
23
|
- **Missing title** — slide has no title (except `quote` which doesn't need one)
|
|
24
24
|
- **Missing image** — image path in YAML doesn't exist on disk
|
|
25
25
|
- **Hidden slide** — possible slide definition commented out
|
package/src/styles.css
CHANGED
|
@@ -2235,3 +2235,67 @@ html, body {
|
|
|
2235
2235
|
.section-progress-dots .section-dot:hover::after {
|
|
2236
2236
|
opacity: 1;
|
|
2237
2237
|
}
|
|
2238
|
+
|
|
2239
|
+
/* ============================================
|
|
2240
|
+
SLIDE: DIAGRAM
|
|
2241
|
+
D2 diagrams rendered to inline SVG
|
|
2242
|
+
============================================ */
|
|
2243
|
+
|
|
2244
|
+
.reveal .slides section.slide-diagram {
|
|
2245
|
+
display: flex !important;
|
|
2246
|
+
flex-direction: column;
|
|
2247
|
+
justify-content: flex-start;
|
|
2248
|
+
align-items: stretch;
|
|
2249
|
+
height: 100%;
|
|
2250
|
+
padding: 0;
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
.reveal .slides section.slide-diagram .slide-body {
|
|
2254
|
+
flex: 1;
|
|
2255
|
+
padding: var(--slide-padding);
|
|
2256
|
+
padding-bottom: 12px;
|
|
2257
|
+
display: flex;
|
|
2258
|
+
flex-direction: column;
|
|
2259
|
+
min-height: 0;
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
.reveal .slides section.slide-diagram .slide-header {
|
|
2263
|
+
margin-bottom: 12px;
|
|
2264
|
+
padding-bottom: 10px;
|
|
2265
|
+
border-bottom: 2px solid var(--color-primary-500);
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
.reveal .slides section.slide-diagram .diagram-container {
|
|
2269
|
+
display: flex;
|
|
2270
|
+
justify-content: center;
|
|
2271
|
+
align-items: center;
|
|
2272
|
+
flex: 1;
|
|
2273
|
+
overflow: hidden;
|
|
2274
|
+
min-height: 0;
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
.reveal .slides section.slide-diagram .diagram-container svg {
|
|
2278
|
+
max-width: 100%;
|
|
2279
|
+
max-height: 100%;
|
|
2280
|
+
height: auto;
|
|
2281
|
+
width: auto;
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
.reveal .slides section.slide-diagram .diagram-caption {
|
|
2285
|
+
text-align: center;
|
|
2286
|
+
font-size: 0.75em;
|
|
2287
|
+
color: var(--color-text-muted);
|
|
2288
|
+
margin-top: 0.5em;
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
.reveal .slides section.slide-diagram .d2-error {
|
|
2292
|
+
color: #dc2626;
|
|
2293
|
+
background: #fef2f2;
|
|
2294
|
+
border: 1px solid #fecaca;
|
|
2295
|
+
border-radius: 8px;
|
|
2296
|
+
padding: 1em;
|
|
2297
|
+
font-family: var(--font-mono);
|
|
2298
|
+
font-size: 0.7em;
|
|
2299
|
+
white-space: pre-wrap;
|
|
2300
|
+
text-align: left;
|
|
2301
|
+
}
|