slides-grab 1.0.0 → 1.1.3

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
@@ -9,7 +9,7 @@ Simple things like text, size, or bold can still be edited manually, just like i
9
9
 
10
10
  <p align="center">
11
11
  The whole slides are HTML & CSS, the programming langauge (which is not) that outperformed by AI agents.<br>
12
- So the slides are beautiful, easily editable by AI agents, and can be converted to pdf and pptx.
12
+ So the slides are beautiful, easily editable by AI agents, and can be converted to PDF or to experimental / unstable PPTX formats.
13
13
  </p>
14
14
 
15
15
  <p align="center">
@@ -56,31 +56,66 @@ There are many AI tools that generate slide HTML. Almost none let you **visually
56
56
  - **Plan** — Agent creates a structured slide outline from your topic/files
57
57
  - **Design** — Agent generates each slide as a self-contained HTML file
58
58
  - **Edit** — Browser-based editor with bbox selection, direct text editing, and agent-powered rewrites
59
- - **Export** — One command to PPTX or PDF
59
+ - **Export** — One command to PDF, plus experimental / unstable PPTX or Figma-export flows
60
60
 
61
61
  ## CLI Commands
62
62
 
63
63
  All commands support `--slides-dir <path>` (default: `slides`).
64
64
 
65
+ On a fresh clone, only `--help`, `list-templates`, and `list-themes` work without a deck. `edit`, `build-viewer`, `validate`, `convert`, and `pdf` require an existing slides workspace containing `slide-*.html`.
66
+
65
67
  ```bash
66
68
  slides-grab edit # Launch visual slide editor
67
69
  slides-grab build-viewer # Build single-file viewer.html
68
70
  slides-grab validate # Validate slide HTML (Playwright-based)
69
- slides-grab convert # Export to PPTX
70
- slides-grab pdf # Export to PDF
71
+ slides-grab convert # Export to experimental / unstable PPTX
72
+ slides-grab convert --resolution 2160p # Higher-resolution raster PPTX export
73
+ slides-grab figma # Export an experimental / unstable Figma Slides importable PPTX
74
+ slides-grab pdf # Export PDF in capture mode (default)
75
+ slides-grab pdf --resolution 2160p # Higher-resolution image-backed PDF export
76
+ slides-grab pdf --mode print # Export searchable/selectable text PDF
71
77
  slides-grab list-templates # Show available slide templates
72
78
  slides-grab list-themes # Show available color themes
73
79
  ```
74
80
 
81
+ ## Image Contract
82
+
83
+ Slides should store local image files in `<slides-dir>/assets/` and reference them as `./assets/<file>` from each `slide-XX.html`.
84
+
85
+ - Preferred: `<img src="./assets/example.png" alt="...">`
86
+ - Allowed: `data:` URLs for fully self-contained slides
87
+ - Allowed with warnings: remote `https://` images
88
+ - Unsupported: absolute filesystem paths such as `/Users/...` or `C:\\...`
89
+
90
+ Run `slides-grab validate --slides-dir <path>` before export to catch missing local assets and discouraged path forms.
91
+
92
+ `slides-grab pdf` now defaults to `--mode capture`, which rasterizes each rendered slide into the PDF for better visual fidelity. Use `--mode print` when searchable/selectable browser text matters more than pixel-perfect parity.
93
+
94
+ `slides-grab pdf` and `slides-grab convert` now default to `2160p` / `4k` raster output for sharper exports. You can still override with `--resolution <preset>` using `720p`, `1080p`, `1440p`, `2160p`, or `4k` when you want smaller or faster artifacts.
95
+
75
96
  ### Multi-Deck Workflow
76
97
 
98
+ Prerequisite: create or generate a deck in `decks/my-deck/` first.
99
+
77
100
  ```bash
78
101
  slides-grab edit --slides-dir decks/my-deck
79
102
  slides-grab validate --slides-dir decks/my-deck
80
103
  slides-grab pdf --slides-dir decks/my-deck --output decks/my-deck.pdf
104
+ slides-grab pdf --slides-dir decks/my-deck --mode print --output decks/my-deck-searchable.pdf
81
105
  slides-grab convert --slides-dir decks/my-deck --output decks/my-deck.pptx
106
+ slides-grab figma --slides-dir decks/my-deck --output decks/my-deck-figma.pptx
82
107
  ```
83
108
 
109
+ > **Warning:** `slides-grab convert` and `slides-grab figma` are currently **experimental / unstable**. Expect best-effort output, layout shifts, and manual cleanup in PowerPoint or Figma.
110
+
111
+ ### Figma Workflow
112
+
113
+ ```bash
114
+ slides-grab figma --slides-dir decks/my-deck --output decks/my-deck-figma.pptx
115
+ ```
116
+
117
+ This command reuses the HTML to PPTX pipeline and emits a `.pptx` deck intended for manual import into Figma Slides via `Import`. It does not upload to Figma directly. The Figma export path is **experimental / unstable** and should be treated as best-effort only.
118
+
84
119
  ## Installation Guides
85
120
 
86
121
  - [Claude Code setup](docs/prompts/setup-claude.md)
@@ -117,4 +152,3 @@ docs/ Installation & usage guides
117
152
  ## Acknowledgment
118
153
 
119
154
  This project is built based on the [ppt_team_agent](https://github.com/uxjoseph/ppt_team_agent) by Builder Josh. Huge thanks to him!
120
-
package/bin/ppt-agent.js CHANGED
@@ -5,10 +5,25 @@ import { readFileSync } from 'node:fs';
5
5
  import { dirname, resolve } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { Command } from 'commander';
8
+ import {
9
+ getFigmaImportCaveats,
10
+ getFigmaManualImportInstructions,
11
+ } from '../src/figma.js';
8
12
 
9
13
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
14
  const packageRoot = resolve(__dirname, '..');
11
15
  const packageJson = JSON.parse(readFileSync(resolve(packageRoot, 'package.json'), 'utf-8'));
16
+ const figmaHelpText = [
17
+ '',
18
+ 'Creates an experimental / unstable PowerPoint file tuned for Figma Slides manual import.',
19
+ 'Treat both PPTX and Figma export as best-effort only.',
20
+ '',
21
+ 'Manual import:',
22
+ ` ${getFigmaManualImportInstructions()}`,
23
+ '',
24
+ 'Figma import caveats:',
25
+ ...getFigmaImportCaveats().map((caveat) => ` - ${caveat}`),
26
+ ].join('\n');
12
27
 
13
28
  /**
14
29
  * Run a Node.js script from the package, with CWD set to the user's directory.
@@ -49,6 +64,10 @@ async function runCommand(relativePath, args = []) {
49
64
  }
50
65
  }
51
66
 
67
+ function collectRepeatedOption(value, previous = []) {
68
+ return [...previous, value];
69
+ }
70
+
52
71
  const program = new Command();
53
72
 
54
73
  program
@@ -69,23 +88,33 @@ program
69
88
 
70
89
  program
71
90
  .command('validate')
91
+ .alias('lint')
72
92
  .description('Run structured validation on slide HTML files (Playwright-based)')
73
93
  .option('--slides-dir <path>', 'Slide directory', 'slides')
94
+ .option('--format <format>', 'Output format: concise, json, json-full', 'concise')
95
+ .option('--slide <file>', 'Validate only the named slide file (repeatable)', collectRepeatedOption, [])
74
96
  .action(async (options = {}) => {
75
- const args = ['--slides-dir', options.slidesDir];
97
+ const args = ['--slides-dir', options.slidesDir, '--format', options.format];
98
+ for (const slide of options.slide || []) {
99
+ args.push('--slide', String(slide));
100
+ }
76
101
  await runCommand('scripts/validate-slides.js', args);
77
102
  });
78
103
 
79
104
  program
80
105
  .command('convert')
81
- .description('Convert slide HTML files to PPTX')
106
+ .description('Convert slide HTML files to experimental / unstable PPTX')
82
107
  .option('--slides-dir <path>', 'Slide directory', 'slides')
83
108
  .option('--output <path>', 'Output PPTX file')
109
+ .option('--resolution <preset>', 'Raster size preset: 720p, 1080p, 1440p, 2160p, or 4k (default: 2160p)')
84
110
  .action(async (options = {}) => {
85
111
  const args = ['--slides-dir', options.slidesDir];
86
112
  if (options.output) {
87
113
  args.push('--output', String(options.output));
88
114
  }
115
+ if (options.resolution) {
116
+ args.push('--resolution', String(options.resolution));
117
+ }
89
118
  await runCommand('convert.cjs', args);
90
119
  });
91
120
 
@@ -94,14 +123,37 @@ program
94
123
  .description('Convert slide HTML files to PDF')
95
124
  .option('--slides-dir <path>', 'Slide directory', 'slides')
96
125
  .option('--output <path>', 'Output PDF file')
126
+ .option('--mode <mode>', 'PDF export mode: capture for visual fidelity, print for searchable text', 'capture')
127
+ .option('--resolution <preset>', 'Capture raster size preset: 720p, 1080p, 1440p, 2160p, or 4k (default: 2160p in capture mode)')
97
128
  .action(async (options = {}) => {
98
129
  const args = ['--slides-dir', options.slidesDir];
99
130
  if (options.output) {
100
131
  args.push('--output', String(options.output));
101
132
  }
133
+ if (options.mode) {
134
+ args.push('--mode', String(options.mode));
135
+ }
136
+ if (options.resolution) {
137
+ args.push('--resolution', String(options.resolution));
138
+ }
102
139
  await runCommand('scripts/html2pdf.js', args);
103
140
  });
104
141
 
142
+ program
143
+ .command('figma')
144
+ .description('Export an experimental / unstable Figma Slides importable PPTX')
145
+ .helpOption('-h, --help', 'Show this help message')
146
+ .option('--slides-dir <path>', 'Slide directory', 'slides')
147
+ .option('--output <path>', 'Output PPTX file (default: <slides-dir>-figma.pptx)')
148
+ .addHelpText('after', figmaHelpText)
149
+ .action(async (options = {}) => {
150
+ const args = ['--slides-dir', options.slidesDir];
151
+ if (options.output) {
152
+ args.push('--output', String(options.output));
153
+ }
154
+ await runCommand('scripts/figma-export.js', args);
155
+ });
156
+
105
157
  program
106
158
  .command('edit')
107
159
  .description('Start interactive slide editor with Codex image-based edit flow')
package/convert.cjs CHANGED
@@ -3,13 +3,52 @@ const { chromium } = require('playwright');
3
3
  const path = require('path');
4
4
  const fs = require('fs');
5
5
  const sharp = require('sharp');
6
+ const {
7
+ getResolutionChoices,
8
+ getResolutionSize,
9
+ normalizeResolutionPreset,
10
+ } = require('./src/export-resolution.cjs');
6
11
 
7
12
  // Inline a simplified version that uses Playwright Chromium (not Chrome)
8
- const PT_PER_PX = 0.75;
9
- const PX_PER_IN = 96;
10
- const EMU_PER_IN = 914400;
11
13
  const DEFAULT_SLIDES_DIR = 'slides';
12
14
  const DEFAULT_OUTPUT = 'output.pptx';
15
+ const DEFAULT_RESOLUTION = '2160p';
16
+ const DEFAULT_CAPTURE_VIEWPORT = { width: 960, height: 540 };
17
+ const DEFAULT_CAPTURE_DEVICE_SCALE_FACTOR = 2;
18
+ const TARGET_RASTER_DPI = 150;
19
+ const TARGET_SLIDE_SIZE_IN = { width: 13.33, height: 7.5 };
20
+
21
+ function normalizeDimension(value, fallback) {
22
+ if (!Number.isFinite(value) || value <= 0) {
23
+ return fallback;
24
+ }
25
+ return Math.max(1, Math.round(value));
26
+ }
27
+
28
+ function buildPageOptions(resolution = '') {
29
+ const targetResolution = getResolutionSize(resolution);
30
+ return {
31
+ viewport: {
32
+ width: DEFAULT_CAPTURE_VIEWPORT.width,
33
+ height: DEFAULT_CAPTURE_VIEWPORT.height,
34
+ },
35
+ deviceScaleFactor: targetResolution
36
+ ? targetResolution.height / DEFAULT_CAPTURE_VIEWPORT.height
37
+ : DEFAULT_CAPTURE_DEVICE_SCALE_FACTOR,
38
+ };
39
+ }
40
+
41
+ function getTargetRasterSize(resolution = '') {
42
+ const targetResolution = getResolutionSize(resolution);
43
+ if (targetResolution) {
44
+ return targetResolution;
45
+ }
46
+
47
+ return {
48
+ width: Math.round(TARGET_SLIDE_SIZE_IN.width * TARGET_RASTER_DPI),
49
+ height: Math.round(TARGET_SLIDE_SIZE_IN.height * TARGET_RASTER_DPI),
50
+ };
51
+ }
13
52
 
14
53
  function printUsage() {
15
54
  process.stdout.write(
@@ -19,6 +58,7 @@ function printUsage() {
19
58
  'Options:',
20
59
  ` --slides-dir <path> Slide directory (default: ${DEFAULT_SLIDES_DIR})`,
21
60
  ` --output <path> Output pptx path (default: ${DEFAULT_OUTPUT})`,
61
+ ` --resolution <preset> Raster size preset: ${getResolutionChoices().join('|')}|4k (default: ${DEFAULT_RESOLUTION})`,
22
62
  ' -h, --help Show this help message',
23
63
  ].join('\n'),
24
64
  );
@@ -37,6 +77,7 @@ function parseArgs(args) {
37
77
  const options = {
38
78
  slidesDir: DEFAULT_SLIDES_DIR,
39
79
  output: DEFAULT_OUTPUT,
80
+ resolution: DEFAULT_RESOLUTION,
40
81
  help: false,
41
82
  };
42
83
 
@@ -69,6 +110,17 @@ function parseArgs(args) {
69
110
  continue;
70
111
  }
71
112
 
113
+ if (arg === '--resolution') {
114
+ options.resolution = normalizeResolutionPreset(readOptionValue(args, i, '--resolution'));
115
+ i += 1;
116
+ continue;
117
+ }
118
+
119
+ if (arg.startsWith('--resolution=')) {
120
+ options.resolution = normalizeResolutionPreset(arg.slice('--resolution='.length));
121
+ continue;
122
+ }
123
+
72
124
  throw new Error(`Unknown option: ${arg}`);
73
125
  }
74
126
 
@@ -82,13 +134,14 @@ function parseArgs(args) {
82
134
 
83
135
  options.slidesDir = options.slidesDir.trim();
84
136
  options.output = options.output.trim();
137
+ options.resolution = normalizeResolutionPreset(options.resolution);
85
138
  return options;
86
139
  }
87
140
 
88
- async function convertSlide(htmlFile, pres, browser) {
141
+ async function convertSlide(htmlFile, pres, browser, options = {}) {
89
142
  const filePath = path.isAbsolute(htmlFile) ? htmlFile : path.join(process.cwd(), htmlFile);
90
143
 
91
- const page = await browser.newPage();
144
+ const page = await browser.newPage(buildPageOptions(options.resolution));
92
145
  await page.goto(`file://${filePath}`);
93
146
 
94
147
  const bodyDimensions = await page.evaluate(() => {
@@ -101,8 +154,8 @@ async function convertSlide(htmlFile, pres, browser) {
101
154
  });
102
155
 
103
156
  await page.setViewportSize({
104
- width: Math.round(bodyDimensions.width),
105
- height: Math.round(bodyDimensions.height)
157
+ width: normalizeDimension(bodyDimensions.width, DEFAULT_CAPTURE_VIEWPORT.width),
158
+ height: normalizeDimension(bodyDimensions.height, DEFAULT_CAPTURE_VIEWPORT.height)
106
159
  });
107
160
 
108
161
  // Take screenshot and add as full-slide image
@@ -110,11 +163,10 @@ async function convertSlide(htmlFile, pres, browser) {
110
163
  await page.close();
111
164
 
112
165
  // Resize to exact slide dimensions (13.33" x 7.5" at 150 DPI)
113
- const targetWidth = Math.round(13.33 * 150);
114
- const targetHeight = Math.round(7.5 * 150);
166
+ const targetSize = getTargetRasterSize(options.resolution);
115
167
 
116
168
  const resized = await sharp(screenshot)
117
- .resize(targetWidth, targetHeight, { fit: 'fill' })
169
+ .resize(targetSize.width, targetSize.height, { fit: 'fill' })
118
170
  .png()
119
171
  .toBuffer();
120
172
 
@@ -144,6 +196,8 @@ async function main() {
144
196
  pres.layout = 'LAYOUT_WIDE'; // 16:9
145
197
 
146
198
  const slidesDir = path.resolve(process.cwd(), options.slidesDir);
199
+ const { ensureSlidesPassValidation } = await import('./scripts/validate-slides.js');
200
+ await ensureSlidesPassValidation(slidesDir, { exportLabel: 'PPTX export' });
147
201
  const files = fs.readdirSync(slidesDir)
148
202
  .filter(f => f.endsWith('.html'))
149
203
  .sort();
@@ -158,7 +212,7 @@ async function main() {
158
212
  const filePath = path.join(slidesDir, file);
159
213
  console.log(` Processing: ${file}`);
160
214
  try {
161
- const tmpPath = await convertSlide(filePath, pres, browser);
215
+ const tmpPath = await convertSlide(filePath, pres, browser, { resolution: options.resolution });
162
216
  tmpFiles.push(tmpPath);
163
217
  console.log(` ✓ ${file} done`);
164
218
  } catch (err) {
@@ -178,7 +232,15 @@ async function main() {
178
232
  }
179
233
  }
180
234
 
181
- main().catch(err => {
182
- console.error('Fatal error:', err);
183
- process.exit(1);
184
- });
235
+ if (require.main === module) {
236
+ main().catch(err => {
237
+ console.error('Fatal error:', err);
238
+ process.exit(1);
239
+ });
240
+ }
241
+
242
+ module.exports = {
243
+ buildPageOptions,
244
+ getTargetRasterSize,
245
+ parseArgs,
246
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "slides-grab",
3
- "version": "1.0.0",
4
- "description": "Agent-first presentation framework — plan, design, and visually edit HTML slides with Claude Code or Codex, then export to PPTX/PDF",
3
+ "version": "1.1.3",
4
+ "description": "Agent-first presentation framework — plan, design, and visually edit HTML slides with Claude Code or Codex, then export to PDF or experimental/unstable PPTX/Figma formats",
5
5
  "license": "MIT",
6
6
  "author": "vkehfdl1",
7
7
  "repository": {
@@ -30,13 +30,30 @@
30
30
  "bin": {
31
31
  "slides-grab": "./bin/ppt-agent.js"
32
32
  },
33
+ "files": [
34
+ "bin/",
35
+ "convert.cjs",
36
+ "scripts/build-viewer.js",
37
+ "scripts/editor-server.js",
38
+ "scripts/figma-export.js",
39
+ "scripts/html2pdf.js",
40
+ "scripts/html2pptx.js",
41
+ "scripts/install-codex-skills.js",
42
+ "scripts/validate-slides.js",
43
+ "skills/",
44
+ "src/",
45
+ "templates/",
46
+ "themes/",
47
+ "LICENSE",
48
+ "README.md"
49
+ ],
33
50
  "scripts": {
34
- "html2pptx": "node .claude/skills/pptx-skill/scripts/html2pptx.js",
51
+ "html2pptx": "node scripts/html2pptx.js",
35
52
  "build-viewer": "node scripts/build-viewer.js",
36
53
  "validate": "node scripts/validate-slides.js",
37
54
  "convert": "node convert.cjs",
38
55
  "codex:install-skills": "node scripts/install-codex-skills.js --force",
39
- "test": "node --test tests/editor/editor-codex-edit.test.js tests/pdf/html2pdf.test.js",
56
+ "test": "node --test tests/editor/editor-codex-edit.test.js tests/pdf/html2pdf.test.js tests/pdf/html2pdf.e2e.test.js tests/figma/figma-export.test.js tests/image-contract/image-contract.test.js tests/validation/validate-slides.test.js",
40
57
  "test:e2e": "node --test tests/editor/editor-ui.e2e.test.js tests/editor/editor-concurrency.e2e.test.js"
41
58
  },
42
59
  "dependencies": {