@christophervr/pptx-viewer 1.1.45 → 1.2.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/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+ This file is generated from [Conventional Commits](https://www.conventionalcommits.org)
5
+ by [git-cliff](https://git-cliff.org); do not edit it by hand.
6
+
7
+ ## [1.1.46](https://github.com/ChristopherVR/pptx-viewer/releases/tag/@christophervr/pptx-viewer@1.1.46) - 2026-07-03
8
+
9
+ ### Features
10
+
11
+ - **cli:** Arrow-key colour prompts and PowerPoint-ready scaffolds (by @ChristopherVR) ([8de03c9](https://github.com/ChristopherVR/pptx-viewer/commit/8de03c9da8c8d20e28cca253ff6d7083de65a0d8))
12
+
13
+ ## [1.1.45](https://github.com/ChristopherVR/pptx-viewer/releases/tag/@christophervr/pptx-viewer@1.1.45) - 2026-07-02
14
+
15
+ ### Features
16
+
17
+ - **cli:** Add interactive @christophervr/pptx-viewer installer (by @ChristopherVR) ([4df680d](https://github.com/ChristopherVR/pptx-viewer/commit/4df680d9791d18e38c0f413420e8e1e5f9f2907e))
package/README.md CHANGED
@@ -19,18 +19,21 @@ This is what you get one `npx` away: a working `.pptx` viewer/editor, wired up i
19
19
  npx @christophervr/pptx-viewer@latest
20
20
  ```
21
21
 
22
- It first asks what you're building (multiple choice, comma-separated):
22
+ It first asks what you're building with an arrow-key checklist (`↑`/`↓` to move, `space` to toggle, `a` to select all, `enter` to confirm):
23
23
 
24
24
  ```
25
25
  What are you building with pptx-viewer? (you can pick more than one)
26
+ (↑/↓ move, space toggle, a select all, enter confirm)
26
27
 
27
- 1) React - pptx-react-viewer, a viewer/editor component for a React 19 app
28
- 2) Vue - pptx-vue-viewer, a viewer/editor component for a Vue 3.5+ app
29
- 3) Angular - pptx-angular-viewer, a viewer/editor component for an Angular 22+ app
30
- 4) Core engine only - pptx-viewer-core, the framework-agnostic SDK, no UI
31
- 5) MCP server - pptx-viewer-mcp, PowerPoint editing tools for AI agents
28
+ React - pptx-react-viewer, a viewer/editor component for a React 19 app
29
+ Vue - pptx-vue-viewer, a viewer/editor component for a Vue 3.5+ app
30
+ Angular - pptx-angular-viewer, a viewer/editor component for an Angular 22+ app
31
+ Core engine only - pptx-viewer-core, the framework-agnostic SDK, no UI
32
+ MCP server - pptx-viewer-mcp, PowerPoint editing tools for AI agents
32
33
  ```
33
34
 
35
+ The whole flow is colour-highlighted (current row, confirmations, warnings, errors) and falls back to a plain numbered prompt in shells without raw keyboard input (piped stdin, some CI runners) or when `NO_COLOR`/a non-TTY output disables colour.
36
+
34
37
  Picking more than one is fine, for example React plus the MCP server to get both a viewer and AI-agent tooling in the same repo. `pptx-viewer-mcp` never gets installed as a dependency: since it's meant to be launched by an MCP client via `npx`, this just prints the client config to paste in.
35
38
 
36
39
  ### Compatibility check
@@ -42,7 +45,7 @@ If you picked React, Vue, or Angular and a `package.json` already exists in the
42
45
  When exactly one UI framework is selected, you're asked how to set it up:
43
46
 
44
47
  - **Install here** adds the package(s) to the project in the current directory (a `package.json` must already exist; run `npm init -y` first if not).
45
- - **Scaffold a new project** bootstraps a brand-new starter app in its own folder, using the framework's own official scaffolding tool ([`create-vite`](https://www.npmjs.com/package/create-vite) for React/Vue, [`@angular/cli`](https://www.npmjs.com/package/@angular/cli) for Angular), then wires in a minimal working `PowerPointViewer` example and installs the viewer package on top.
48
+ - **Scaffold a new project** bootstraps a brand-new starter app in its own folder, using the framework's own official scaffolding tool ([`create-vite`](https://www.npmjs.com/package/create-vite) for React/Vue, [`@angular/cli`](https://www.npmjs.com/package/@angular/cli) for Angular), then wires in a working `PowerPointViewer` example (same pattern as the [live demos](https://christophervr.github.io/pptx-viewer/demo/): open an existing `.pptx`, or click "New Presentation" to build a blank deck with `PptxHandler.createBlank` and start editing right away) and installs the viewer package plus `pptx-viewer-core` on top.
46
49
 
47
50
  Scaffolding is only offered for a single framework at a time; if you select more than one UI framework together, the CLI installs into the current project instead.
48
51
 
package/dist/index.mjs CHANGED
@@ -52,6 +52,28 @@ function parseArgs(args) {
52
52
  return parsed;
53
53
  }
54
54
 
55
+ // src/colors.ts
56
+ var isColorEnabled = process.env.NO_COLOR === void 0 && (process.env.FORCE_COLOR !== void 0 || Boolean(process.stdout.isTTY));
57
+ function wrap(open, close) {
58
+ return (text) => isColorEnabled ? `\x1B[${open}m${text}\x1B[${close}m` : text;
59
+ }
60
+ var bold = wrap(1, 22);
61
+ var dim = wrap(2, 22);
62
+ var red = wrap(31, 39);
63
+ var green = wrap(32, 39);
64
+ var yellow = wrap(33, 39);
65
+ var blue = wrap(34, 39);
66
+ var magenta = wrap(35, 39);
67
+ var cyan = wrap(36, 39);
68
+ var gray = wrap(90, 39);
69
+ function isUnicodeSupported() {
70
+ if (process.platform !== "win32") {
71
+ return true;
72
+ }
73
+ return Boolean(process.env.CI) || Boolean(process.env.WT_SESSION) || Boolean(process.env.ConEmuTask) || process.env.TERM_PROGRAM === "vscode" || process.env.TERM === "xterm-256color";
74
+ }
75
+ var symbols = isUnicodeSupported() ? { pointer: "\u276F", check: "\u2714", cross: "\u2718", radioOn: "\u25C9", radioOff: "\u25EF", bullet: "\xB7" } : { pointer: ">", check: "\u221A", cross: "\xD7", radioOn: "(*)", radioOff: "( )", bullet: "*" };
76
+
55
77
  // src/project-deps.ts
56
78
  import { existsSync, readFileSync } from "fs";
57
79
  import { join } from "path";
@@ -150,6 +172,140 @@ function installCommand(pm, packages) {
150
172
 
151
173
  // src/prompt.ts
152
174
  import { createInterface } from "readline/promises";
175
+
176
+ // src/interactive-menu.ts
177
+ import { emitKeypressEvents } from "readline";
178
+ var HIDE_CURSOR = "\x1B[?25l";
179
+ var SHOW_CURSOR = "\x1B[?25h";
180
+ var CLEAR_LINE = "\x1B[2K";
181
+ var ERASE_DOWN = "\x1B[0J";
182
+ function moveUp(lines) {
183
+ return lines > 0 ? `\x1B[${lines}A` : "";
184
+ }
185
+ function isEnterKey(str, key) {
186
+ return key?.name === "return" || key?.name === "enter" || str === "\r" || str === "\n";
187
+ }
188
+ function renderChoice(choice, isCursor, checked) {
189
+ const pointer = isCursor ? cyan(symbols.pointer) : " ";
190
+ const box = checked === null ? "" : `${checked ? green(symbols.radioOn) : gray(symbols.radioOff)} `;
191
+ const label = isCursor ? bold(choice.label) : choice.label;
192
+ return `${pointer} ${box}${label} ${dim(`- ${choice.description}`)}`;
193
+ }
194
+ function groupMatesOf(choices, index) {
195
+ const group = choices[index].group;
196
+ if (!group) {
197
+ return [];
198
+ }
199
+ return choices.flatMap((c, i) => i !== index && c.group === group ? [i] : []);
200
+ }
201
+ function runMenu(choices, multi) {
202
+ return new Promise((resolve) => {
203
+ if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== "function") {
204
+ resolve(null);
205
+ return;
206
+ }
207
+ let cursor = 0;
208
+ const checked = /* @__PURE__ */ new Set();
209
+ let statusMessage = "";
210
+ let settled = false;
211
+ const hint = multi ? dim("(\u2191/\u2193 move, space toggle, a select all, enter confirm)") : dim("(\u2191/\u2193 move, enter confirm)");
212
+ const totalLines = choices.length + 2;
213
+ process.stdout.write(HIDE_CURSOR);
214
+ function draw(first) {
215
+ if (!first) {
216
+ process.stdout.write(moveUp(totalLines));
217
+ }
218
+ process.stdout.write(`${CLEAR_LINE}${hint}
219
+ `);
220
+ for (const [i, choice] of choices.entries()) {
221
+ const checkedState = multi ? checked.has(i) : null;
222
+ process.stdout.write(`${CLEAR_LINE}${renderChoice(choice, i === cursor, checkedState)}
223
+ `);
224
+ }
225
+ process.stdout.write(`${CLEAR_LINE}${statusMessage}
226
+ `);
227
+ }
228
+ function eraseWidget() {
229
+ process.stdout.write(moveUp(totalLines));
230
+ process.stdout.write(ERASE_DOWN);
231
+ }
232
+ function cleanup() {
233
+ process.stdin.setRawMode?.(false);
234
+ process.stdin.removeListener("keypress", onKeypress);
235
+ process.stdin.pause();
236
+ eraseWidget();
237
+ process.stdout.write(SHOW_CURSOR);
238
+ }
239
+ function finish(result) {
240
+ if (settled) {
241
+ return;
242
+ }
243
+ settled = true;
244
+ cleanup();
245
+ resolve(result);
246
+ }
247
+ function check(index) {
248
+ for (const mate of groupMatesOf(choices, index)) {
249
+ checked.delete(mate);
250
+ }
251
+ checked.add(index);
252
+ }
253
+ function toggleSelectAll() {
254
+ const selectable = choices.flatMap((c, i) => c.group ? [] : [i]);
255
+ const allSelected = selectable.every((i) => checked.has(i));
256
+ for (const i of selectable) {
257
+ if (allSelected) {
258
+ checked.delete(i);
259
+ } else {
260
+ checked.add(i);
261
+ }
262
+ }
263
+ }
264
+ function onKeypress(str, key) {
265
+ if (key?.ctrl && key.name === "c") {
266
+ finish(null);
267
+ process.exit(130);
268
+ return;
269
+ }
270
+ statusMessage = "";
271
+ if (key?.name === "up") {
272
+ cursor = (cursor - 1 + choices.length) % choices.length;
273
+ draw(false);
274
+ } else if (key?.name === "down") {
275
+ cursor = (cursor + 1) % choices.length;
276
+ draw(false);
277
+ } else if (multi && key?.name === "space") {
278
+ if (checked.has(cursor)) {
279
+ checked.delete(cursor);
280
+ } else {
281
+ check(cursor);
282
+ }
283
+ draw(false);
284
+ } else if (multi && key?.name === "a") {
285
+ toggleSelectAll();
286
+ draw(false);
287
+ } else if (isEnterKey(str, key)) {
288
+ if (multi) {
289
+ if (checked.size > 0) {
290
+ finish([...checked].sort((a, b) => a - b));
291
+ } else {
292
+ statusMessage = dim("Select at least one option with space, then press enter.");
293
+ draw(false);
294
+ }
295
+ } else {
296
+ finish([cursor]);
297
+ }
298
+ }
299
+ }
300
+ emitKeypressEvents(process.stdin);
301
+ process.stdin.setRawMode(true);
302
+ process.stdin.on("keypress", onKeypress);
303
+ process.stdin.resume();
304
+ draw(true);
305
+ });
306
+ }
307
+
308
+ // src/prompt.ts
153
309
  function parseSelection(answer, count) {
154
310
  const trimmed = answer.trim().toLowerCase();
155
311
  if (trimmed === "all" || trimmed === "a") {
@@ -171,15 +327,12 @@ function parseSelection(answer, count) {
171
327
  }
172
328
  function printOptions(options) {
173
329
  options.forEach((opt, i) => {
174
- console.log(` ${i + 1}) ${opt.label} - ${opt.description}`);
330
+ console.log(` ${cyan(`${i + 1})`)} ${bold(opt.label)} ${dim(`- ${opt.description}`)}`);
175
331
  });
176
332
  }
177
- async function selectOption(question, options) {
333
+ async function selectByNumber(options) {
178
334
  const rl = createInterface({ input: process.stdin, output: process.stdout });
179
335
  try {
180
- console.log(`
181
- ${question}
182
- `);
183
336
  printOptions(options);
184
337
  for (; ; ) {
185
338
  const answer = (await rl.question(`
@@ -194,12 +347,9 @@ Enter a number (1-${options.length}): `)).trim();
194
347
  rl.close();
195
348
  }
196
349
  }
197
- async function multiSelect(question, options) {
350
+ async function selectManyByNumber(options) {
198
351
  const rl = createInterface({ input: process.stdin, output: process.stdout });
199
352
  try {
200
- console.log(`
201
- ${question}
202
- `);
203
353
  printOptions(options);
204
354
  for (; ; ) {
205
355
  const answer = await rl.question(
@@ -216,10 +366,32 @@ Enter one or more numbers, comma-separated (e.g. "1,3"), or "all": `
216
366
  rl.close();
217
367
  }
218
368
  }
369
+ async function selectOption(question, options) {
370
+ console.log(`
371
+ ${bold(question)}`);
372
+ const picked = await runMenu(options, false);
373
+ if (!picked) {
374
+ return selectByNumber(options);
375
+ }
376
+ const choice = options[picked[0]];
377
+ console.log(`${green("\u2714")} ${choice.label}`);
378
+ return choice;
379
+ }
380
+ async function multiSelect(question, options) {
381
+ console.log(`
382
+ ${bold(question)}`);
383
+ const picked = await runMenu(options, true);
384
+ if (!picked) {
385
+ return selectManyByNumber(options);
386
+ }
387
+ const choices = picked.map((i) => options[i]);
388
+ console.log(`${green("\u2714")} ${choices.map((c) => c.label).join(", ")}`);
389
+ return choices;
390
+ }
219
391
  async function confirm(question) {
220
392
  const rl = createInterface({ input: process.stdin, output: process.stdout });
221
393
  try {
222
- const answer = (await rl.question(`${question} (Y/n): `)).trim().toLowerCase();
394
+ const answer = (await rl.question(`${bold(question)} ${dim("(Y/n)")} `)).trim().toLowerCase();
223
395
  return answer === "" || answer === "y" || answer === "yes";
224
396
  } finally {
225
397
  rl.close();
@@ -228,7 +400,7 @@ async function confirm(question) {
228
400
  async function input(question, defaultValue) {
229
401
  const rl = createInterface({ input: process.stdin, output: process.stdout });
230
402
  try {
231
- const answer = (await rl.question(`${question} (${defaultValue}): `)).trim();
403
+ const answer = (await rl.question(`${bold(question)} ${dim(`(${defaultValue})`)} `)).trim();
232
404
  return answer === "" ? defaultValue : answer;
233
405
  } finally {
234
406
  rl.close();
@@ -236,48 +408,91 @@ async function input(question, defaultValue) {
236
408
  }
237
409
 
238
410
  // src/targets.ts
239
- var REACT_APP_TSX = `import { useState } from 'react';
411
+ var REACT_APP_TSX = `import { useCallback, useState } from 'react';
412
+ import { PptxHandler } from 'pptx-viewer-core';
240
413
  import { PowerPointViewer } from 'pptx-react-viewer';
241
- import 'pptx-react-viewer/styles';
414
+ import 'pptx-react-viewer/styles.css';
242
415
 
243
416
  export default function App() {
244
- const [content, setContent] = useState<ArrayBuffer | null>(null);
245
-
246
- const onPick = (e: React.ChangeEvent<HTMLInputElement>) =>
247
- e.target.files?.[0]?.arrayBuffer().then(setContent);
417
+ const [content, setContent] = useState<Uint8Array | null>(null);
418
+
419
+ const loadFile = useCallback((file: File) => {
420
+ const reader = new FileReader();
421
+ reader.onload = () => setContent(new Uint8Array(reader.result as ArrayBuffer));
422
+ reader.readAsArrayBuffer(file);
423
+ }, []);
424
+
425
+ const newPresentation = useCallback(async () => {
426
+ const { handler, data } = await PptxHandler.createBlank({
427
+ title: 'Untitled Presentation',
428
+ initialSlideCount: 1,
429
+ });
430
+ setContent(await handler.save(data.slides));
431
+ }, []);
432
+
433
+ if (content) {
434
+ return (
435
+ <div style={{ height: '100vh' }}>
436
+ <PowerPointViewer content={content} canEdit />
437
+ </div>
438
+ );
439
+ }
248
440
 
249
441
  return (
250
- <div style={{ height: '100vh' }}>
251
- {content ? (
252
- <PowerPointViewer content={content} canEdit />
253
- ) : (
254
- <input type="file" accept=".pptx" onChange={onPick} />
255
- )}
442
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 12, height: '100vh' }}>
443
+ <input
444
+ type="file"
445
+ accept=".pptx"
446
+ onChange={(e) => {
447
+ const file = e.target.files?.[0];
448
+ if (file) loadFile(file);
449
+ }}
450
+ />
451
+ <button onClick={() => void newPresentation()}>or create a New Presentation</button>
256
452
  </div>
257
453
  );
258
454
  }
259
455
  `;
260
456
  var VUE_APP_VUE = `<script setup lang="ts">
261
457
  import { ref } from 'vue';
458
+ import { PptxHandler } from 'pptx-viewer-core';
262
459
  import { PowerPointViewer } from 'pptx-vue-viewer';
263
- import 'pptx-vue-viewer/styles';
460
+ import 'pptx-vue-viewer/styles.css';
264
461
 
265
462
  const content = ref<Uint8Array>();
266
463
 
464
+ function loadFile(file: File) {
465
+ const reader = new FileReader();
466
+ reader.onload = () => (content.value = new Uint8Array(reader.result as ArrayBuffer));
467
+ reader.readAsArrayBuffer(file);
468
+ }
469
+
267
470
  function onPick(e: Event) {
268
471
  const file = (e.target as HTMLInputElement).files?.[0];
269
- file?.arrayBuffer().then((buf) => (content.value = new Uint8Array(buf)));
472
+ if (file) loadFile(file);
473
+ }
474
+
475
+ async function newPresentation() {
476
+ const { handler, data } = await PptxHandler.createBlank({
477
+ title: 'Untitled Presentation',
478
+ initialSlideCount: 1,
479
+ });
480
+ content.value = await handler.save(data.slides);
270
481
  }
271
482
  </script>
272
483
 
273
484
  <template>
274
- <div style="height: 100vh">
275
- <PowerPointViewer v-if="content" :content="content" style="height: 100%" />
276
- <input v-else type="file" accept=".pptx" @change="onPick" />
485
+ <div v-if="content" style="height: 100vh">
486
+ <PowerPointViewer :content="content" can-edit style="height: 100%" />
487
+ </div>
488
+ <div v-else style="display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; height: 100vh">
489
+ <input type="file" accept=".pptx" @change="onPick" />
490
+ <button @click="newPresentation">or create a New Presentation</button>
277
491
  </div>
278
492
  </template>
279
493
  `;
280
494
  var ANGULAR_APP_TS = `import { Component, signal } from '@angular/core';
495
+ import { PptxHandler } from 'pptx-viewer-core';
281
496
  import { PowerPointViewerComponent } from 'pptx-angular-viewer';
282
497
 
283
498
  @Component({
@@ -285,17 +500,20 @@ import { PowerPointViewerComponent } from 'pptx-angular-viewer';
285
500
  standalone: true,
286
501
  imports: [PowerPointViewerComponent],
287
502
  template: \`
288
- <div style="height: 100vh">
289
- @if (content(); as c) {
290
- <pptx-power-point-viewer [content]="c" style="height: 100%" />
291
- } @else {
503
+ @if (content(); as c) {
504
+ <div style="height: 100vh">
505
+ <pptx-power-point-viewer [content]="c" [canEdit]="true" style="height: 100%" />
506
+ </div>
507
+ } @else {
508
+ <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; height: 100vh">
292
509
  <input type="file" accept=".pptx" (change)="onPick($event)" />
293
- }
294
- </div>
510
+ <button (click)="newPresentation()">or create a New Presentation</button>
511
+ </div>
512
+ }
295
513
  \`,
296
514
  })
297
515
  export class App {
298
- content = signal<ArrayBuffer | null>(null);
516
+ content = signal<ArrayBuffer | Uint8Array | null>(null);
299
517
 
300
518
  async onPick(e: Event) {
301
519
  const file = (e.target as HTMLInputElement).files?.[0];
@@ -303,6 +521,14 @@ export class App {
303
521
  this.content.set(await file.arrayBuffer());
304
522
  }
305
523
  }
524
+
525
+ async newPresentation() {
526
+ const { handler, data } = await PptxHandler.createBlank({
527
+ title: 'Untitled Presentation',
528
+ initialSlideCount: 1,
529
+ });
530
+ this.content.set(await handler.save(data.slides));
531
+ }
306
532
  }
307
533
  `;
308
534
  var TARGETS = [
@@ -311,6 +537,7 @@ var TARGETS = [
311
537
  label: "React",
312
538
  description: "pptx-react-viewer - viewer/editor component for a React 19 app",
313
539
  mode: "install",
540
+ group: "framework",
314
541
  packages: [
315
542
  "pptx-react-viewer",
316
543
  "react",
@@ -325,7 +552,7 @@ var TARGETS = [
325
552
  "react-i18next"
326
553
  ],
327
554
  nextSteps: `import { PowerPointViewer } from 'pptx-react-viewer';
328
- import 'pptx-react-viewer/styles';
555
+ import 'pptx-react-viewer/styles.css';
329
556
 
330
557
  <PowerPointViewer content={arrayBuffer} canEdit />
331
558
 
@@ -333,9 +560,14 @@ Docs: https://www.npmjs.com/package/pptx-react-viewer`,
333
560
  compat: { peerPackage: "react", requiredMajor: 19 },
334
561
  scaffold: {
335
562
  command: "create-vite@latest",
336
- args: (dir) => [dir, "--template", "react-ts"],
563
+ // --no-interactive/--no-immediate stop create-vite from prompting for a linter
564
+ // choice and then auto-installing + auto-starting its own dev server; if it did,
565
+ // that dev server would block forever and our own entry-file patch + extra
566
+ // package install below would never run, leaving the default Vite template in place.
567
+ args: (dir) => [dir, "--template", "react-ts", "--no-interactive", "--no-immediate"],
337
568
  extraPackages: [
338
569
  "pptx-react-viewer",
570
+ "pptx-viewer-core",
339
571
  "framer-motion",
340
572
  "lucide-react",
341
573
  "react-icons",
@@ -354,10 +586,11 @@ Docs: https://www.npmjs.com/package/pptx-react-viewer`,
354
586
  label: "Vue",
355
587
  description: "pptx-vue-viewer - viewer/editor component for a Vue 3.5+ app",
356
588
  mode: "install",
589
+ group: "framework",
357
590
  packages: ["pptx-vue-viewer", "vue", "jszip", "fast-xml-parser"],
358
591
  nextSteps: `<script setup lang="ts">
359
592
  import { PowerPointViewer } from 'pptx-vue-viewer';
360
- import 'pptx-vue-viewer/styles';
593
+ import 'pptx-vue-viewer/styles.css';
361
594
  </script>
362
595
 
363
596
  <template>
@@ -368,8 +601,8 @@ Docs: https://www.npmjs.com/package/pptx-vue-viewer`,
368
601
  compat: { peerPackage: "vue", requiredMajor: 3 },
369
602
  scaffold: {
370
603
  command: "create-vite@latest",
371
- args: (dir) => [dir, "--template", "vue-ts"],
372
- extraPackages: ["pptx-vue-viewer", "jszip", "fast-xml-parser"],
604
+ args: (dir) => [dir, "--template", "vue-ts", "--no-interactive", "--no-immediate"],
605
+ extraPackages: ["pptx-vue-viewer", "pptx-viewer-core", "jszip", "fast-xml-parser"],
373
606
  entryCandidates: ["src/App.vue"],
374
607
  entryContent: VUE_APP_VUE
375
608
  }
@@ -379,9 +612,10 @@ Docs: https://www.npmjs.com/package/pptx-vue-viewer`,
379
612
  label: "Angular",
380
613
  description: "pptx-angular-viewer - viewer/editor component for an Angular 22+ app",
381
614
  mode: "install",
615
+ group: "framework",
382
616
  packages: ["pptx-angular-viewer", "@angular/core", "@angular/common", "rxjs"],
383
617
  nextSteps: `import { PowerPointViewerComponent } from 'pptx-angular-viewer';
384
- import 'pptx-angular-viewer/styles';
618
+ import 'pptx-angular-viewer/styles.css';
385
619
 
386
620
  <pptx-power-point-viewer [content]="content" />
387
621
 
@@ -389,8 +623,20 @@ Docs: https://www.npmjs.com/package/pptx-angular-viewer`,
389
623
  compat: { peerPackage: "@angular/core", requiredMajor: 22 },
390
624
  scaffold: {
391
625
  command: "@angular/cli@latest",
392
- args: (dir) => ["new", dir, "--standalone", "--skip-git", "--style=css", "--skip-install"],
393
- extraPackages: ["pptx-angular-viewer"],
626
+ // --no-interactive matters even with the flags above supplied: the
627
+ // `application` schematic's `ssr` option has an `x-prompt`, and `ng new`
628
+ // prompts for it (plus anything else not already given a value) whenever
629
+ // stdin is a TTY, which ours is (we inherit the real user's terminal).
630
+ args: (dir) => [
631
+ "new",
632
+ dir,
633
+ "--standalone",
634
+ "--skip-git",
635
+ "--style=css",
636
+ "--skip-install",
637
+ "--no-interactive"
638
+ ],
639
+ extraPackages: ["pptx-angular-viewer", "pptx-viewer-core"],
394
640
  // Angular v20+ generates `app.ts`; older schematics generate `app.component.ts`.
395
641
  entryCandidates: ["src/app/app.ts", "src/app/app.component.ts"],
396
642
  entryContent: ANGULAR_APP_TS
@@ -455,6 +701,24 @@ function findTargetsByIds(ids) {
455
701
  return match;
456
702
  });
457
703
  }
704
+ function assertSingleFramework(targets) {
705
+ const grouped = /* @__PURE__ */ new Map();
706
+ for (const target of targets) {
707
+ if (!target.group) {
708
+ continue;
709
+ }
710
+ const mates = grouped.get(target.group) ?? [];
711
+ mates.push(target);
712
+ grouped.set(target.group, mates);
713
+ }
714
+ for (const mates of grouped.values()) {
715
+ if (mates.length > 1) {
716
+ throw new Error(
717
+ `${mates.map((t) => t.label).join(", ")} can't be selected together; pick a single UI framework.`
718
+ );
719
+ }
720
+ }
721
+ }
458
722
  function mergePackages(targets) {
459
723
  const seen = /* @__PURE__ */ new Set();
460
724
  const merged = [];
@@ -520,29 +784,35 @@ async function scaffoldProject(recipe, projectName, pm, cwd) {
520
784
  }
521
785
 
522
786
  // src/index.ts
787
+ function printBanner() {
788
+ console.log(`
789
+ ${bold(cyan("pptx-viewer"))} ${dim("\xB7 interactive installer")}`);
790
+ }
523
791
  function printUsage() {
792
+ printBanner();
524
793
  console.log(`
525
- @christophervr/pptx-viewer - interactive installer for the pptx-viewer packages
526
-
527
- Usage: npx @christophervr/pptx-viewer [options]
528
-
529
- Options:
530
- --target <ids> Skip the picker; comma-separated, any of: ${TARGETS.map((t) => t.id).join(", ")}
531
- --scaffold Bootstrap a brand-new starter project instead of installing here
532
- --dir <name> Project directory name for --scaffold
533
- --pm <manager> Package manager to use: bun, pnpm, yarn, npm (default: auto-detected)
534
- --yes, -y Skip confirmation prompts
535
- --help, -h Show this help
536
-
537
- Examples:
538
- npx @christophervr/pptx-viewer
539
- npx @christophervr/pptx-viewer --target react,mcp --yes
540
- npx @christophervr/pptx-viewer --target react --scaffold --dir my-app --yes
794
+ ${bold("Usage:")} npx @christophervr/pptx-viewer [options]
795
+
796
+ ${bold("Options:")}
797
+ ${cyan("--target <ids>")} Skip the picker; comma-separated, any of: ${TARGETS.map((t) => t.id).join(", ")}
798
+ (react, vue, and angular are mutually exclusive; pick at most one)
799
+ ${cyan("--scaffold")} Bootstrap a brand-new starter project instead of installing here
800
+ ${cyan("--dir <name>")} Project directory name for --scaffold
801
+ ${cyan("--pm <manager>")} Package manager to use: bun, pnpm, yarn, npm (default: auto-detected)
802
+ ${cyan("--yes, -y")} Skip confirmation prompts
803
+ ${cyan("--help, -h")} Show this help
804
+
805
+ ${bold("Examples:")}
806
+ ${gray("npx @christophervr/pptx-viewer")}
807
+ ${gray("npx @christophervr/pptx-viewer --target react,mcp --yes")}
808
+ ${gray("npx @christophervr/pptx-viewer --target react --scaffold --dir my-app --yes")}
541
809
  `);
542
810
  }
543
811
  async function resolveTargets(requested) {
544
812
  if (requested) {
545
- return findTargetsByIds(parseTargetIds(requested));
813
+ const targets = findTargetsByIds(parseTargetIds(requested));
814
+ console.log(`${green("\u2714")} ${targets.map((t) => t.label).join(", ")}`);
815
+ return targets;
546
816
  }
547
817
  if (!process.stdin.isTTY) {
548
818
  throw new Error("Not running in a terminal: pass --target explicitly (see --help).");
@@ -559,7 +829,7 @@ async function confirmCompat(cwd, targets) {
559
829
  continue;
560
830
  }
561
831
  console.log(`
562
- Warning: ${result.message}`);
832
+ ${yellow("Warning:")} ${result.message}`);
563
833
  if (process.stdin.isTTY) {
564
834
  const proceed = await confirm("Continue anyway?");
565
835
  if (!proceed) {
@@ -577,12 +847,6 @@ async function resolveScaffoldChoice(installTargets, args) {
577
847
  }
578
848
  return { useScaffold: true, scaffoldTarget: scaffoldable[0] };
579
849
  }
580
- if (scaffoldable.length > 1) {
581
- console.log(
582
- "\nScaffolding a new project only supports one UI framework at a time; installing instead."
583
- );
584
- return { useScaffold: false };
585
- }
586
850
  if (scaffoldable.length === 1 && process.stdin.isTTY) {
587
851
  const choice = await selectOption("Install into the current project, or scaffold a new one?", [
588
852
  { label: "Install here", description: "Add the package(s) to the project in this directory" },
@@ -609,13 +873,14 @@ async function runScaffoldMode(target, args, configTargets, cwd) {
609
873
  const pm = args.pm ?? detectPackageManager(cwd);
610
874
  console.log(
611
875
  `
612
- About to scaffold "${projectName}" with ${recipe.command} (${target.label}), then install with ${pm}.
876
+ ${bold("About to scaffold")} "${cyan(projectName)}" with ${recipe.command} (${target.label}), then install with ${pm}.
613
877
  `
614
878
  );
615
879
  if (!args.yes && process.stdin.isTTY) {
616
880
  const proceed = await confirm("Continue?");
617
881
  if (!proceed) {
618
- console.log("\nSkipped.");
882
+ console.log(`
883
+ ${dim("Skipped.")}`);
619
884
  return;
620
885
  }
621
886
  }
@@ -623,15 +888,15 @@ About to scaffold "${projectName}" with ${recipe.command} (${target.label}), the
623
888
  if (!result.patchedFile) {
624
889
  console.log(
625
890
  `
626
- Scaffolded the project, but could not find an entry file to wire up automatically. See the quick-start snippet below and add it yourself.`
891
+ ${yellow("Scaffolded the project, but could not find an entry file to wire up automatically.")} See the quick-start snippet below and add it yourself.`
627
892
  );
628
893
  }
629
894
  console.log(
630
895
  `
631
- Done. Next steps:
896
+ ${green("\u2714")} ${bold("Done.")} Next steps:
632
897
 
633
- cd ${projectName}
634
- ${pm} run dev
898
+ ${cyan(`cd ${projectName}`)}
899
+ ${cyan(`${pm} run dev`)}
635
900
 
636
901
  ${target.nextSteps}
637
902
  `
@@ -650,22 +915,23 @@ async function runInstallMode(installTargets, configTargets, args, cwd) {
650
915
  }
651
916
  const proceedPastCompat = await confirmCompat(cwd, installTargets);
652
917
  if (!proceedPastCompat) {
653
- console.log("\nAborted.");
918
+ console.log(`
919
+ ${red("Aborted.")}`);
654
920
  return;
655
921
  }
656
922
  const packages = mergePackages(installTargets);
657
923
  const pm = args.pm ?? detectPackageManager(cwd);
658
924
  const [command, cmdArgs] = installCommand(pm, packages);
659
925
  console.log(`
660
- About to run: ${command} ${cmdArgs.join(" ")}
926
+ ${bold("About to run:")} ${cyan(`${command} ${cmdArgs.join(" ")}`)}
661
927
  `);
662
928
  if (!args.yes && process.stdin.isTTY) {
663
929
  const proceed = await confirm("Install now?");
664
930
  if (!proceed) {
665
931
  console.log(
666
932
  `
667
- Skipped. Run this yourself when ready:
668
- ${command} ${cmdArgs.join(" ")}
933
+ ${dim("Skipped.")} Run this yourself when ready:
934
+ ${cyan(`${command} ${cmdArgs.join(" ")}`)}
669
935
  `
670
936
  );
671
937
  return;
@@ -676,7 +942,7 @@ Skipped. Run this yourself when ready:
676
942
  throw new Error(`${command} exited with code ${exitCode}`);
677
943
  }
678
944
  console.log(`
679
- Done. Next steps:`);
945
+ ${green("\u2714")} ${bold("Done.")} Next steps:`);
680
946
  for (const target of installTargets) {
681
947
  console.log(`
682
948
  ${target.nextSteps}
@@ -695,9 +961,9 @@ async function main() {
695
961
  printUsage();
696
962
  return;
697
963
  }
964
+ printBanner();
698
965
  const targets = await resolveTargets(args.target);
699
- console.log(`
700
- Selected: ${targets.map((t) => t.label).join(", ")}`);
966
+ assertSingleFramework(targets);
701
967
  const installTargets = targets.filter((t) => t.mode === "install");
702
968
  const configTargets = targets.filter((t) => t.mode === "print-config");
703
969
  const cwd = process.cwd();
@@ -710,6 +976,6 @@ Selected: ${targets.map((t) => t.label).join(", ")}`);
710
976
  }
711
977
  main().catch((err) => {
712
978
  const message = err instanceof Error ? err.message : String(err);
713
- console.error(`Error: ${message}`);
979
+ console.error(`${red("\u2718 Error:")} ${message}`);
714
980
  process.exit(1);
715
981
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@christophervr/pptx-viewer",
3
- "version": "1.1.45",
3
+ "version": "1.2.0",
4
4
  "description": "Interactive installer for the pptx-viewer family of packages: React, Vue, and Angular viewer components, the framework-agnostic core engine, and the MCP server.",
5
5
  "keywords": [
6
6
  "cli",