lazyslides 0.1.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.
Files changed (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +207 -0
  3. package/_includes/slides/_nested-list.njk +26 -0
  4. package/_includes/slides/agenda.njk +17 -0
  5. package/_includes/slides/center.njk +16 -0
  6. package/_includes/slides/code.njk +15 -0
  7. package/_includes/slides/columns.njk +35 -0
  8. package/_includes/slides/comparison.njk +29 -0
  9. package/_includes/slides/content.njk +22 -0
  10. package/_includes/slides/footer.njk +58 -0
  11. package/_includes/slides/funnel.njk +30 -0
  12. package/_includes/slides/hero.njk +27 -0
  13. package/_includes/slides/image-overlay.njk +21 -0
  14. package/_includes/slides/metrics.njk +27 -0
  15. package/_includes/slides/quote.njk +17 -0
  16. package/_includes/slides/section.njk +6 -0
  17. package/_includes/slides/split-wide.njk +30 -0
  18. package/_includes/slides/split.njk +30 -0
  19. package/_includes/slides/table.njk +31 -0
  20. package/_includes/slides/timeline.njk +30 -0
  21. package/_includes/slides/title.njk +17 -0
  22. package/_layouts/default.njk +20 -0
  23. package/_layouts/presentation.njk +240 -0
  24. package/assets/css/themes/default.css +62 -0
  25. package/assets/css/vendor/glightbox.min.css +1 -0
  26. package/assets/js/vendor/glightbox.min.js +1 -0
  27. package/assets/reveal.js/LICENSE +19 -0
  28. package/assets/reveal.js/dist/reset.css +30 -0
  29. package/assets/reveal.js/dist/reveal.css +8 -0
  30. package/assets/reveal.js/dist/reveal.esm.js +9 -0
  31. package/assets/reveal.js/dist/reveal.esm.js.map +1 -0
  32. package/assets/reveal.js/dist/reveal.js +9 -0
  33. package/assets/reveal.js/dist/reveal.js.map +1 -0
  34. package/assets/reveal.js/dist/theme/beige.css +366 -0
  35. package/assets/reveal.js/dist/theme/black-contrast.css +362 -0
  36. package/assets/reveal.js/dist/theme/black.css +359 -0
  37. package/assets/reveal.js/dist/theme/blood.css +392 -0
  38. package/assets/reveal.js/dist/theme/dracula.css +385 -0
  39. package/assets/reveal.js/dist/theme/fonts/league-gothic/LICENSE +2 -0
  40. package/assets/reveal.js/dist/theme/fonts/league-gothic/league-gothic.css +10 -0
  41. package/assets/reveal.js/dist/theme/fonts/league-gothic/league-gothic.eot +0 -0
  42. package/assets/reveal.js/dist/theme/fonts/league-gothic/league-gothic.ttf +0 -0
  43. package/assets/reveal.js/dist/theme/fonts/league-gothic/league-gothic.woff +0 -0
  44. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/LICENSE +45 -0
  45. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-italic.eot +0 -0
  46. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-italic.ttf +0 -0
  47. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-italic.woff +0 -0
  48. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-regular.eot +0 -0
  49. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-regular.ttf +0 -0
  50. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-regular.woff +0 -0
  51. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-semibold.eot +0 -0
  52. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-semibold.ttf +0 -0
  53. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-semibold.woff +0 -0
  54. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-semibolditalic.eot +0 -0
  55. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-semibolditalic.ttf +0 -0
  56. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro-semibolditalic.woff +0 -0
  57. package/assets/reveal.js/dist/theme/fonts/source-sans-pro/source-sans-pro.css +39 -0
  58. package/assets/reveal.js/dist/theme/league.css +368 -0
  59. package/assets/reveal.js/dist/theme/moon.css +362 -0
  60. package/assets/reveal.js/dist/theme/night.css +360 -0
  61. package/assets/reveal.js/dist/theme/serif.css +363 -0
  62. package/assets/reveal.js/dist/theme/simple.css +362 -0
  63. package/assets/reveal.js/dist/theme/sky.css +370 -0
  64. package/assets/reveal.js/dist/theme/solarized.css +363 -0
  65. package/assets/reveal.js/dist/theme/white-contrast.css +362 -0
  66. package/assets/reveal.js/dist/theme/white.css +359 -0
  67. package/assets/reveal.js/dist/theme/white_contrast_compact_verbatim_headers.css +360 -0
  68. package/assets/reveal.js/plugin/highlight/highlight.esm.js +5 -0
  69. package/assets/reveal.js/plugin/highlight/highlight.js +5 -0
  70. package/assets/reveal.js/plugin/highlight/monokai.css +71 -0
  71. package/assets/reveal.js/plugin/highlight/plugin.js +439 -0
  72. package/assets/reveal.js/plugin/highlight/zenburn.css +80 -0
  73. package/assets/reveal.js/plugin/markdown/markdown.esm.js +7 -0
  74. package/assets/reveal.js/plugin/markdown/markdown.js +7 -0
  75. package/assets/reveal.js/plugin/markdown/plugin.js +491 -0
  76. package/assets/reveal.js/plugin/notes/notes.esm.js +1 -0
  77. package/assets/reveal.js/plugin/notes/notes.js +1 -0
  78. package/assets/reveal.js/plugin/notes/plugin.js +267 -0
  79. package/assets/reveal.js/plugin/notes/speaker-view.html +898 -0
  80. package/cli.js +80 -0
  81. package/data/site.json +3 -0
  82. package/index.js +153 -0
  83. package/lib/export-pdf.js +137 -0
  84. package/lib/init.js +154 -0
  85. package/lib/renumber.js +181 -0
  86. package/lib/validate.js +196 -0
  87. package/package.json +76 -0
  88. package/scaffold/CLAUDE.md +283 -0
  89. package/scaffold/README.md +29 -0
  90. package/scaffold/_template/index.md +78 -0
  91. package/scaffold/_template/outline.md +32 -0
  92. package/scaffold/claude-commands/add-slide.md +61 -0
  93. package/scaffold/claude-commands/create-outline.md +75 -0
  94. package/scaffold/claude-commands/new-presentation.md +218 -0
  95. package/scaffold/claude-commands/refine-slides.md +62 -0
  96. package/scaffold/claude-commands/research-topic.md +66 -0
  97. package/scaffold/claude-commands/validate.md +47 -0
  98. package/scaffold/claude-settings.json +7 -0
  99. package/scaffold/eleventy.config.js +11 -0
  100. package/scaffold/gitignore +5 -0
  101. package/scaffold/my-first-deck/index.md +26 -0
  102. package/scaffold/nvmrc +1 -0
  103. package/scaffold/package.json.tmpl +25 -0
  104. package/scaffold/presentations.11tydata.js +11 -0
  105. package/scaffold/styles.css +3 -0
  106. package/src/styles.css +2077 -0
@@ -0,0 +1,181 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ function extractSlideTitle(lines, templateIdx) {
5
+ let title = null;
6
+ let subtitle = null;
7
+ let templateType = null;
8
+
9
+ const templateMatch = lines[templateIdx].match(/template:\s*(\w+)/);
10
+ if (templateMatch) templateType = templateMatch[1];
11
+
12
+ for (let idx = templateIdx + 1; idx < lines.length; idx++) {
13
+ const line = lines[idx];
14
+ if (/^\s*-\s*template:/.test(line)) break;
15
+
16
+ const titleMatch = line.match(/^\s+title:\s*(.+)$/);
17
+ if (titleMatch) {
18
+ title = titleMatch[1].trim();
19
+ if ((title.startsWith('"') && title.endsWith('"')) || (title.startsWith("'") && title.endsWith("'"))) {
20
+ title = title.slice(1, -1);
21
+ }
22
+ title = title.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
23
+ }
24
+
25
+ const subtitleMatch = line.match(/^\s+subtitle:\s*(.+)$/);
26
+ if (subtitleMatch) {
27
+ subtitle = subtitleMatch[1].trim();
28
+ if ((subtitle.startsWith('"') && subtitle.endsWith('"')) || (subtitle.startsWith("'") && subtitle.endsWith("'"))) {
29
+ subtitle = subtitle.slice(1, -1);
30
+ }
31
+ }
32
+ }
33
+
34
+ if (templateType === "section" && title && subtitle) {
35
+ return `${title} \u2014 ${subtitle}`;
36
+ }
37
+ return title || "Untitled";
38
+ }
39
+
40
+ export function renumberFile(file) {
41
+ let lines = fs.readFileSync(file, "utf-8").split("\n");
42
+
43
+ // First pass: find all template line positions and their slide numbers
44
+ let templateLines = {};
45
+ let slideCounter = 0;
46
+
47
+ lines.forEach((line, idx) => {
48
+ if (/^\s*-\s*template:/.test(line)) {
49
+ slideCounter++;
50
+ templateLines[idx] = slideCounter;
51
+ }
52
+ });
53
+
54
+ if (slideCounter === 0) {
55
+ console.log(` No slides found in ${file}`);
56
+ return false;
57
+ }
58
+
59
+ // Second pass: find which slides have comments
60
+ const slidesWithComments = {};
61
+ lines.forEach((line, idx) => {
62
+ if (/^(\s*#\s*Slide)\s*(\d*):\s*(.*)$/.test(line)) {
63
+ for (let futureIdx = idx + 1; futureIdx < lines.length; futureIdx++) {
64
+ if (templateLines[futureIdx] !== undefined) {
65
+ slidesWithComments[futureIdx] = idx;
66
+ break;
67
+ }
68
+ }
69
+ }
70
+ });
71
+
72
+ // Third pass: add missing comments (work backwards to preserve line indices)
73
+ let insertions = 0;
74
+ const sortedKeys = Object.keys(templateLines).map(Number).sort((a, b) => b - a);
75
+
76
+ for (const templateIdx of sortedKeys) {
77
+ const slideNum = templateLines[templateIdx];
78
+ if (slidesWithComments[templateIdx] === undefined) {
79
+ const title = extractSlideTitle(lines, templateIdx);
80
+ const comment = `# Slide ${slideNum}: ${title}`;
81
+ lines.splice(templateIdx, 0, comment);
82
+ insertions++;
83
+ console.log(` Added: # Slide ${slideNum}: ${title}`);
84
+ }
85
+ }
86
+
87
+ // Fourth pass: rebuild templateLines if insertions happened
88
+ if (insertions > 0) {
89
+ templateLines = {};
90
+ slideCounter = 0;
91
+ lines.forEach((line, idx) => {
92
+ if (/^\s*-\s*template:/.test(line)) {
93
+ slideCounter++;
94
+ templateLines[idx] = slideCounter;
95
+ }
96
+ });
97
+ }
98
+
99
+ // Fifth pass: renumber all comments
100
+ let changes = 0;
101
+ lines.forEach((line, idx) => {
102
+ const match = line.match(/^(\s*#\s*Slide)\s*(\d*):\s*(.*)$/);
103
+ if (match) {
104
+ const prefix = match[1];
105
+ const oldNum = match[2];
106
+ const rest = match[3];
107
+
108
+ let nextSlideNum = null;
109
+ for (let futureIdx = idx + 1; futureIdx < lines.length; futureIdx++) {
110
+ if (templateLines[futureIdx] !== undefined) {
111
+ nextSlideNum = templateLines[futureIdx];
112
+ break;
113
+ }
114
+ }
115
+
116
+ if (nextSlideNum !== null) {
117
+ const newLine = `${prefix} ${nextSlideNum}: ${rest}`;
118
+ if (lines[idx] !== newLine) {
119
+ console.log(` Line ${idx + 1}: '# Slide ${oldNum}:' -> '# Slide ${nextSlideNum}:'`);
120
+ lines[idx] = newLine;
121
+ changes++;
122
+ }
123
+ }
124
+ }
125
+ });
126
+
127
+ if (changes > 0 || insertions > 0) {
128
+ fs.writeFileSync(file, lines.join("\n"));
129
+ console.log(` \u2713 Updated ${file}: ${insertions} comment(s) added, ${changes} renumbered`);
130
+ return true;
131
+ } else {
132
+ console.log(` No changes needed in ${file}`);
133
+ return false;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Renumber slide comments in presentation YAML files.
139
+ * @param {object} [opts]
140
+ * @param {string} [opts.cwd] - working directory (defaults to process.cwd())
141
+ * @param {string[]} [opts.files] - specific files to process
142
+ */
143
+ export async function run(opts = {}) {
144
+ const cwd = opts.cwd || process.cwd();
145
+
146
+ if (opts.files && opts.files.length > 0) {
147
+ for (const file of opts.files) {
148
+ if (!fs.existsSync(file)) {
149
+ throw new Error(`File not found: ${file}`);
150
+ }
151
+ console.log(`Renumbering: ${file}`);
152
+ renumberFile(file);
153
+ }
154
+ return;
155
+ }
156
+
157
+ const dir = path.join(cwd, "presentations");
158
+ if (!fs.existsSync(dir)) {
159
+ console.log(`No presentations directory found at ${dir}`);
160
+ return;
161
+ }
162
+
163
+ const files = fs.readdirSync(dir, { withFileTypes: true })
164
+ .filter((e) => e.isDirectory() && e.name !== "_template")
165
+ .map((e) => path.join(dir, e.name, "index.md"))
166
+ .filter((f) => fs.existsSync(f));
167
+
168
+ if (files.length === 0) {
169
+ console.log(`No presentation files found in ${dir}`);
170
+ return;
171
+ }
172
+
173
+ let totalUpdated = 0;
174
+ for (const file of files) {
175
+ console.log(`Renumbering: ${file}`);
176
+ if (renumberFile(file)) totalUpdated++;
177
+ }
178
+
179
+ console.log("\n" + "=".repeat(50));
180
+ console.log(`Renumbering complete: ${files.length} file(s) processed, ${totalUpdated} updated`);
181
+ }
@@ -0,0 +1,196 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import yaml from "js-yaml";
4
+
5
+ const REQUIRED_FIELDS = ["title", "slides"];
6
+ const VALID_TEMPLATES = [
7
+ "title", "section", "content", "metrics", "comparison", "columns",
8
+ "quote", "center", "hero", "image-overlay", "code", "timeline",
9
+ "funnel", "split", "split-wide", "table", "agenda",
10
+ ];
11
+
12
+ function findPresentations(dir) {
13
+ const results = [];
14
+ if (!fs.existsSync(dir)) return results;
15
+
16
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
17
+ for (const entry of entries) {
18
+ if (!entry.isDirectory() || entry.name === "_template") continue;
19
+ const indexPath = path.join(dir, entry.name, "index.md");
20
+ if (fs.existsSync(indexPath)) {
21
+ results.push(indexPath);
22
+ }
23
+ }
24
+ return results;
25
+ }
26
+
27
+ /**
28
+ * Validate all presentation YAML files.
29
+ * @param {object} [opts]
30
+ * @param {string} [opts.cwd] - working directory (defaults to process.cwd())
31
+ * @param {boolean} [opts.renumber] - run renumber before validating
32
+ * @returns {{ errors: string[], warnings: string[], validated: number }}
33
+ */
34
+ export async function run(opts = {}) {
35
+ const cwd = opts.cwd || process.cwd();
36
+ const presentationsDir = path.join(cwd, "presentations");
37
+
38
+ if (opts.renumber) {
39
+ const { run: renumber } = await import("./renumber.js");
40
+ await renumber({ cwd });
41
+ }
42
+
43
+ const errors = [];
44
+ const warnings = [];
45
+ let validated = 0;
46
+
47
+ const files = findPresentations(presentationsDir);
48
+
49
+ for (const file of files) {
50
+ console.log(`Validating: ${file}`);
51
+ validated++;
52
+
53
+ try {
54
+ const content = fs.readFileSync(file, "utf-8");
55
+
56
+ // Check: Comment-hidden slide detection
57
+ content.split("\n").forEach((line, idx) => {
58
+ if (/#.*-\s*template:/.test(line)) {
59
+ warnings.push(`${file}:${idx + 1}: Possible slide definition hidden in comment`);
60
+ }
61
+ });
62
+
63
+ // Extract front matter
64
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
65
+ if (!match) {
66
+ errors.push(`${file}: No valid YAML front matter found`);
67
+ continue;
68
+ }
69
+
70
+ const data = yaml.load(match[1]);
71
+
72
+ // Check required fields
73
+ for (const field of REQUIRED_FIELDS) {
74
+ if (!(field in data)) {
75
+ errors.push(`${file}: Missing required field '${field}'`);
76
+ }
77
+ }
78
+
79
+ // Validate slides array
80
+ if (data.slides) {
81
+ if (!Array.isArray(data.slides)) {
82
+ errors.push(`${file}: 'slides' must be an array`);
83
+ continue;
84
+ }
85
+
86
+ const sections = [];
87
+
88
+ data.slides.forEach((slide, idx) => {
89
+ const slideNum = idx + 1;
90
+
91
+ if (!slide.template) {
92
+ errors.push(`${file}: Slide ${slideNum} missing 'template' field`);
93
+ return;
94
+ }
95
+
96
+ if (!VALID_TEMPLATES.includes(slide.template)) {
97
+ warnings.push(`${file}: Slide ${slideNum} has unknown template '${slide.template}'`);
98
+ }
99
+
100
+ if (slide.template === "section") {
101
+ sections.push({ title: slide.title, start: slideNum });
102
+ }
103
+
104
+ if (!slide.title && !["quote"].includes(slide.template)) {
105
+ warnings.push(`${file}: Slide ${slideNum} (${slide.template}) missing 'title'`);
106
+ }
107
+
108
+ // Comparison template structure validation
109
+ if (slide.template === "comparison") {
110
+ if (!slide.rows) {
111
+ errors.push(`${file}: Slide ${slideNum} (comparison) missing 'rows:' - comparison template requires rows format`);
112
+ }
113
+ if ((slide.left && typeof slide.left === "object") || (slide.right && typeof slide.right === "object")) {
114
+ errors.push(`${file}: Slide ${slideNum} (comparison) uses wrong format - use 'rows:' with left/right pairs, not nested left:/right: objects`);
115
+ }
116
+ }
117
+
118
+ // Image path validation
119
+ if (slide.image) {
120
+ const imagePath = slide.image;
121
+ if (imagePath.includes("../")) {
122
+ warnings.push(`${file}: Slide ${slideNum} image path contains '../'`);
123
+ }
124
+ const presentationDir = path.dirname(file);
125
+ const fullImagePath = path.join(presentationDir, imagePath);
126
+ if (!fs.existsSync(fullImagePath)) {
127
+ warnings.push(`${file}: Slide ${slideNum} missing image '${imagePath}'`);
128
+ }
129
+ }
130
+ });
131
+
132
+ // Expected slide count
133
+ if (data.expected_slides && data.slides.length !== data.expected_slides) {
134
+ warnings.push(`${file}: Expected ${data.expected_slides} slides, found ${data.slides.length}`);
135
+ }
136
+
137
+ // Section index output
138
+ if (sections.length > 0) {
139
+ console.log("");
140
+ console.log(" SECTIONS:");
141
+ console.log(" " + "\u2500".repeat(50));
142
+
143
+ sections.forEach((section, idx) => {
144
+ const endSlide = idx + 1 < sections.length
145
+ ? sections[idx + 1].start - 1
146
+ : data.slides.length;
147
+
148
+ const title = section.title || "(untitled section)";
149
+ const range = `[slides ${section.start}-${endSlide}]`;
150
+ console.log(` ${title.padEnd(42)} ${range}`);
151
+ });
152
+ }
153
+
154
+ // Slide listing output
155
+ console.log("");
156
+ console.log(" SLIDES:");
157
+ console.log(" " + "\u2500".repeat(50));
158
+
159
+ data.slides.forEach((slide, idx) => {
160
+ const slideNum = idx + 1;
161
+ const template = slide.template || "???";
162
+ let title = slide.title || slide.text || slide.headline || "(no title)";
163
+ if (title.length > 48) title = title.substring(0, 45) + "...";
164
+ console.log(` ${String(slideNum).padStart(3)}. [${template}] ${title}`);
165
+ });
166
+
167
+ console.log("");
168
+ console.log(` \u2713 ${data.slides.length} slides validated`);
169
+ }
170
+ } catch (e) {
171
+ if (e.name === "YAMLException") {
172
+ errors.push(`${file}: YAML syntax error - ${e.message}`);
173
+ } else {
174
+ errors.push(`${file}: ${e.constructor.name} - ${e.message}`);
175
+ }
176
+ }
177
+ }
178
+
179
+ console.log("\n" + "=".repeat(50));
180
+ console.log(`Validation complete: ${validated} presentation(s)`);
181
+
182
+ if (warnings.length > 0) {
183
+ console.log(`\n\u26A0\uFE0F Warnings (${warnings.length}):`);
184
+ warnings.forEach((w) => console.log(` ${w}`));
185
+ }
186
+
187
+ if (errors.length > 0) {
188
+ console.log(`\n\u274C Errors (${errors.length}):`);
189
+ errors.forEach((e) => console.log(` ${e}`));
190
+ console.log("\nBuild may fail or skip files with errors!");
191
+ } else {
192
+ console.log("\n\u2705 All presentations valid");
193
+ }
194
+
195
+ return { errors, warnings, validated };
196
+ }
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "lazyslides",
3
+ "version": "0.1.0",
4
+ "description": "AI-native presentations from YAML — Eleventy plugin with Reveal.js",
5
+ "license": "MIT",
6
+ "author": "Chris Tietz",
7
+ "keywords": [
8
+ "presentations",
9
+ "slides",
10
+ "reveal.js",
11
+ "eleventy",
12
+ "yaml",
13
+ "tailwind",
14
+ "ai",
15
+ "claude",
16
+ "slide-deck"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/Chrisissorry/lazyslides.git"
21
+ },
22
+ "homepage": "https://github.com/Chrisissorry/lazyslides",
23
+ "bugs": {
24
+ "url": "https://github.com/Chrisissorry/lazyslides/issues"
25
+ },
26
+ "engines": {
27
+ "node": ">=22"
28
+ },
29
+ "type": "module",
30
+ "main": "index.js",
31
+ "exports": {
32
+ ".": "./index.js",
33
+ "./styles.css": "./src/styles.css"
34
+ },
35
+ "bin": {
36
+ "lazyslides": "./cli.js"
37
+ },
38
+ "files": [
39
+ "index.js",
40
+ "cli.js",
41
+ "lib/",
42
+ "_layouts/",
43
+ "_includes/",
44
+ "assets/",
45
+ "src/",
46
+ "scaffold/",
47
+ "data/"
48
+ ],
49
+ "peerDependencies": {
50
+ "@11ty/eleventy": "^3.0.0"
51
+ },
52
+ "dependencies": {
53
+ "js-yaml": "^4.1.0"
54
+ },
55
+ "devDependencies": {
56
+ "@11ty/eleventy": "^3.0.0",
57
+ "@tailwindcss/cli": "^4.0.0",
58
+ "concurrently": "^9.0.0",
59
+ "decktape": "^3.14.0",
60
+ "tailwindcss": "^4.1.18",
61
+ "vitest": "^4.0.18"
62
+ },
63
+ "scripts": {
64
+ "dev": "node cli.js validate && concurrently \"pnpm:watch:*\"",
65
+ "watch:css": "pnpm exec tailwindcss -i src/styles.css -o _site/assets/css/styles.css --watch",
66
+ "watch:11ty": "pnpm exec eleventy --serve",
67
+ "build": "node cli.js validate && pnpm run build:css && pnpm run build:11ty",
68
+ "build:css": "pnpm exec tailwindcss -i src/styles.css -o _site/assets/css/styles.css",
69
+ "build:11ty": "pnpm exec eleventy",
70
+ "test": "vitest run",
71
+ "test:watch": "vitest",
72
+ "validate": "node cli.js validate",
73
+ "renumber": "node cli.js renumber",
74
+ "pdf": "node cli.js pdf"
75
+ }
76
+ }