@christophervr/pptx-viewer 1.1.46 → 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 CHANGED
@@ -4,6 +4,12 @@ All notable changes to this project are documented here.
4
4
  This file is generated from [Conventional Commits](https://www.conventionalcommits.org)
5
5
  by [git-cliff](https://git-cliff.org); do not edit it by hand.
6
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
+
7
13
  ## [1.1.45](https://github.com/ChristopherVR/pptx-viewer/releases/tag/@christophervr/pptx-viewer@1.1.45) - 2026-07-02
8
14
 
9
15
  ### Features
package/dist/index.mjs CHANGED
@@ -66,14 +66,13 @@ var blue = wrap(34, 39);
66
66
  var magenta = wrap(35, 39);
67
67
  var cyan = wrap(36, 39);
68
68
  var gray = wrap(90, 39);
69
- var symbols = {
70
- pointer: "\u276F",
71
- check: "\u2714",
72
- cross: "\u2718",
73
- radioOn: "\u25C9",
74
- radioOff: "\u25EF",
75
- bullet: "\xB7"
76
- };
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: "*" };
77
76
 
78
77
  // src/project-deps.ts
79
78
  import { existsSync, readFileSync } from "fs";
@@ -179,15 +178,26 @@ import { emitKeypressEvents } from "readline";
179
178
  var HIDE_CURSOR = "\x1B[?25l";
180
179
  var SHOW_CURSOR = "\x1B[?25h";
181
180
  var CLEAR_LINE = "\x1B[2K";
181
+ var ERASE_DOWN = "\x1B[0J";
182
182
  function moveUp(lines) {
183
183
  return lines > 0 ? `\x1B[${lines}A` : "";
184
184
  }
185
+ function isEnterKey(str, key) {
186
+ return key?.name === "return" || key?.name === "enter" || str === "\r" || str === "\n";
187
+ }
185
188
  function renderChoice(choice, isCursor, checked) {
186
189
  const pointer = isCursor ? cyan(symbols.pointer) : " ";
187
190
  const box = checked === null ? "" : `${checked ? green(symbols.radioOn) : gray(symbols.radioOff)} `;
188
191
  const label = isCursor ? bold(choice.label) : choice.label;
189
192
  return `${pointer} ${box}${label} ${dim(`- ${choice.description}`)}`;
190
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
+ }
191
201
  function runMenu(choices, multi) {
192
202
  return new Promise((resolve) => {
193
203
  if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== "function") {
@@ -196,24 +206,34 @@ function runMenu(choices, multi) {
196
206
  }
197
207
  let cursor = 0;
198
208
  const checked = /* @__PURE__ */ new Set();
209
+ let statusMessage = "";
199
210
  let settled = false;
200
211
  const hint = multi ? dim("(\u2191/\u2193 move, space toggle, a select all, enter confirm)") : dim("(\u2191/\u2193 move, enter confirm)");
201
- console.log(hint);
212
+ const totalLines = choices.length + 2;
202
213
  process.stdout.write(HIDE_CURSOR);
203
214
  function draw(first) {
204
215
  if (!first) {
205
- process.stdout.write(moveUp(choices.length));
216
+ process.stdout.write(moveUp(totalLines));
206
217
  }
218
+ process.stdout.write(`${CLEAR_LINE}${hint}
219
+ `);
207
220
  for (const [i, choice] of choices.entries()) {
208
221
  const checkedState = multi ? checked.has(i) : null;
209
222
  process.stdout.write(`${CLEAR_LINE}${renderChoice(choice, i === cursor, checkedState)}
210
223
  `);
211
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);
212
231
  }
213
232
  function cleanup() {
214
233
  process.stdin.setRawMode?.(false);
215
234
  process.stdin.removeListener("keypress", onKeypress);
216
235
  process.stdin.pause();
236
+ eraseWidget();
217
237
  process.stdout.write(SHOW_CURSOR);
218
238
  }
219
239
  function finish(result) {
@@ -224,12 +244,30 @@ function runMenu(choices, multi) {
224
244
  cleanup();
225
245
  resolve(result);
226
246
  }
227
- function onKeypress(_str, key) {
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) {
228
265
  if (key?.ctrl && key.name === "c") {
229
266
  finish(null);
230
267
  process.exit(130);
231
268
  return;
232
269
  }
270
+ statusMessage = "";
233
271
  if (key?.name === "up") {
234
272
  cursor = (cursor - 1 + choices.length) % choices.length;
235
273
  draw(false);
@@ -240,20 +278,19 @@ function runMenu(choices, multi) {
240
278
  if (checked.has(cursor)) {
241
279
  checked.delete(cursor);
242
280
  } else {
243
- checked.add(cursor);
281
+ check(cursor);
244
282
  }
245
283
  draw(false);
246
284
  } else if (multi && key?.name === "a") {
247
- if (checked.size === choices.length) {
248
- checked.clear();
249
- } else {
250
- choices.forEach((_, i) => checked.add(i));
251
- }
285
+ toggleSelectAll();
252
286
  draw(false);
253
- } else if (key?.name === "return") {
287
+ } else if (isEnterKey(str, key)) {
254
288
  if (multi) {
255
289
  if (checked.size > 0) {
256
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);
257
294
  }
258
295
  } else {
259
296
  finish([cursor]);
@@ -374,7 +411,7 @@ async function input(question, defaultValue) {
374
411
  var REACT_APP_TSX = `import { useCallback, useState } from 'react';
375
412
  import { PptxHandler } from 'pptx-viewer-core';
376
413
  import { PowerPointViewer } from 'pptx-react-viewer';
377
- import 'pptx-react-viewer/styles';
414
+ import 'pptx-react-viewer/styles.css';
378
415
 
379
416
  export default function App() {
380
417
  const [content, setContent] = useState<Uint8Array | null>(null);
@@ -420,7 +457,7 @@ var VUE_APP_VUE = `<script setup lang="ts">
420
457
  import { ref } from 'vue';
421
458
  import { PptxHandler } from 'pptx-viewer-core';
422
459
  import { PowerPointViewer } from 'pptx-vue-viewer';
423
- import 'pptx-vue-viewer/styles';
460
+ import 'pptx-vue-viewer/styles.css';
424
461
 
425
462
  const content = ref<Uint8Array>();
426
463
 
@@ -500,6 +537,7 @@ var TARGETS = [
500
537
  label: "React",
501
538
  description: "pptx-react-viewer - viewer/editor component for a React 19 app",
502
539
  mode: "install",
540
+ group: "framework",
503
541
  packages: [
504
542
  "pptx-react-viewer",
505
543
  "react",
@@ -514,7 +552,7 @@ var TARGETS = [
514
552
  "react-i18next"
515
553
  ],
516
554
  nextSteps: `import { PowerPointViewer } from 'pptx-react-viewer';
517
- import 'pptx-react-viewer/styles';
555
+ import 'pptx-react-viewer/styles.css';
518
556
 
519
557
  <PowerPointViewer content={arrayBuffer} canEdit />
520
558
 
@@ -522,7 +560,11 @@ Docs: https://www.npmjs.com/package/pptx-react-viewer`,
522
560
  compat: { peerPackage: "react", requiredMajor: 19 },
523
561
  scaffold: {
524
562
  command: "create-vite@latest",
525
- 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"],
526
568
  extraPackages: [
527
569
  "pptx-react-viewer",
528
570
  "pptx-viewer-core",
@@ -544,10 +586,11 @@ Docs: https://www.npmjs.com/package/pptx-react-viewer`,
544
586
  label: "Vue",
545
587
  description: "pptx-vue-viewer - viewer/editor component for a Vue 3.5+ app",
546
588
  mode: "install",
589
+ group: "framework",
547
590
  packages: ["pptx-vue-viewer", "vue", "jszip", "fast-xml-parser"],
548
591
  nextSteps: `<script setup lang="ts">
549
592
  import { PowerPointViewer } from 'pptx-vue-viewer';
550
- import 'pptx-vue-viewer/styles';
593
+ import 'pptx-vue-viewer/styles.css';
551
594
  </script>
552
595
 
553
596
  <template>
@@ -558,7 +601,7 @@ Docs: https://www.npmjs.com/package/pptx-vue-viewer`,
558
601
  compat: { peerPackage: "vue", requiredMajor: 3 },
559
602
  scaffold: {
560
603
  command: "create-vite@latest",
561
- args: (dir) => [dir, "--template", "vue-ts"],
604
+ args: (dir) => [dir, "--template", "vue-ts", "--no-interactive", "--no-immediate"],
562
605
  extraPackages: ["pptx-vue-viewer", "pptx-viewer-core", "jszip", "fast-xml-parser"],
563
606
  entryCandidates: ["src/App.vue"],
564
607
  entryContent: VUE_APP_VUE
@@ -569,9 +612,10 @@ Docs: https://www.npmjs.com/package/pptx-vue-viewer`,
569
612
  label: "Angular",
570
613
  description: "pptx-angular-viewer - viewer/editor component for an Angular 22+ app",
571
614
  mode: "install",
615
+ group: "framework",
572
616
  packages: ["pptx-angular-viewer", "@angular/core", "@angular/common", "rxjs"],
573
617
  nextSteps: `import { PowerPointViewerComponent } from 'pptx-angular-viewer';
574
- import 'pptx-angular-viewer/styles';
618
+ import 'pptx-angular-viewer/styles.css';
575
619
 
576
620
  <pptx-power-point-viewer [content]="content" />
577
621
 
@@ -579,7 +623,19 @@ Docs: https://www.npmjs.com/package/pptx-angular-viewer`,
579
623
  compat: { peerPackage: "@angular/core", requiredMajor: 22 },
580
624
  scaffold: {
581
625
  command: "@angular/cli@latest",
582
- args: (dir) => ["new", dir, "--standalone", "--skip-git", "--style=css", "--skip-install"],
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
+ ],
583
639
  extraPackages: ["pptx-angular-viewer", "pptx-viewer-core"],
584
640
  // Angular v20+ generates `app.ts`; older schematics generate `app.component.ts`.
585
641
  entryCandidates: ["src/app/app.ts", "src/app/app.component.ts"],
@@ -645,6 +701,24 @@ function findTargetsByIds(ids) {
645
701
  return match;
646
702
  });
647
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
+ }
648
722
  function mergePackages(targets) {
649
723
  const seen = /* @__PURE__ */ new Set();
650
724
  const merged = [];
@@ -721,6 +795,7 @@ ${bold("Usage:")} npx @christophervr/pptx-viewer [options]
721
795
 
722
796
  ${bold("Options:")}
723
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)
724
799
  ${cyan("--scaffold")} Bootstrap a brand-new starter project instead of installing here
725
800
  ${cyan("--dir <name>")} Project directory name for --scaffold
726
801
  ${cyan("--pm <manager>")} Package manager to use: bun, pnpm, yarn, npm (default: auto-detected)
@@ -772,13 +847,6 @@ async function resolveScaffoldChoice(installTargets, args) {
772
847
  }
773
848
  return { useScaffold: true, scaffoldTarget: scaffoldable[0] };
774
849
  }
775
- if (scaffoldable.length > 1) {
776
- console.log(
777
- `
778
- ${dim("Scaffolding a new project only supports one UI framework at a time; installing instead.")}`
779
- );
780
- return { useScaffold: false };
781
- }
782
850
  if (scaffoldable.length === 1 && process.stdin.isTTY) {
783
851
  const choice = await selectOption("Install into the current project, or scaffold a new one?", [
784
852
  { label: "Install here", description: "Add the package(s) to the project in this directory" },
@@ -895,6 +963,7 @@ async function main() {
895
963
  }
896
964
  printBanner();
897
965
  const targets = await resolveTargets(args.target);
966
+ assertSingleFramework(targets);
898
967
  const installTargets = targets.filter((t) => t.mode === "install");
899
968
  const configTargets = targets.filter((t) => t.mode === "print-config");
900
969
  const cwd = process.cwd();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@christophervr/pptx-viewer",
3
- "version": "1.1.46",
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",