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 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
- - **17 slide templates** — title, content, metrics, comparison, timeline, funnel, code, and more
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
- 17 built-in templates cover common slide patterns:
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, "&amp;")
110
+ .replace(/</g, "&lt;")
111
+ .replace(/>/g, "&gt;");
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.2.2",
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": "^4.0.0",
60
+ "@tailwindcss/cli": "4.1.18",
58
61
  "concurrently": "^9.0.0",
59
62
  "decktape": "^3.14.0",
60
- "tailwindcss": "^4.1.18",
61
- "vitest": "^4.0.18"
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:*\"",
@@ -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 17 built-in templates: `title`, `section`, `content`, `center`, `hero`, `metrics`, `comparison`, `columns`, `quote`, `image-overlay`, `code`, `timeline`, `funnel`, `split`, `split-wide`, `table`, `agenda`.
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 17 valid types
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
+ }