@christophervr/pptx-viewer 1.5.0 → 1.5.1

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.5.0](https://github.com/ChristopherVR/pptx-viewer/releases/tag/@christophervr/pptx-viewer@1.5.0) - 2026-07-11
8
+
9
+ ### Features
10
+
11
+ - **cli:** Add svelte and vanilla js install/scaffold targets (by @ChristopherVR) ([768aafe](https://github.com/ChristopherVR/pptx-viewer/commit/768aafe14f57b75cc3d91a00c62be261c4044789))
12
+
7
13
  ## [1.4.2](https://github.com/ChristopherVR/pptx-viewer/releases/tag/@christophervr/pptx-viewer@1.4.2) - 2026-07-07
8
14
 
9
15
  ## [1.4.1](https://github.com/ChristopherVR/pptx-viewer/releases/tag/@christophervr/pptx-viewer@1.4.1) - 2026-07-05
package/dist/index.mjs CHANGED
@@ -410,38 +410,76 @@ async function input(question, defaultValue) {
410
410
  // src/templates/angular.ts
411
411
  var ANGULAR_APP_TS = `import { Component, signal } from '@angular/core';
412
412
  import { PptxHandler } from 'pptx-viewer-core';
413
+ import type { CollaborationConfig } from 'pptx-angular-viewer';
413
414
  import { PowerPointViewerComponent } from 'pptx-angular-viewer';
414
415
 
415
416
  @Component({
416
417
  selector: 'app-root',
417
418
  standalone: true,
418
419
  imports: [PowerPointViewerComponent],
420
+ styles: [\`
421
+ :host { display: block; height: 100dvh; }
422
+ .stage { display: flex; align-items: center; justify-content: center; height: 100dvh; padding: 2rem; cursor: default; }
423
+ .dropzone { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.75rem; max-width: 520px; width: 100%; padding: 3rem; text-align: center; border: 2px dashed var(--pptx-border, #374151); border-radius: 0.75rem; cursor: pointer; transition: border-color 0.15s, background 0.15s; }
424
+ .dropzone.over, .dropzone:hover { border-color: var(--pptx-primary, #6366f1); background: var(--pptx-muted, rgba(255,255,255,0.04)); }
425
+ h1 { margin: 0; font-size: 1.5rem; font-weight: 500; }
426
+ p { margin: 0; font-size: 0.875rem; color: var(--pptx-muted-foreground, #9ca3af); }
427
+ .pick-label { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.5rem 1.25rem; border-radius: 0.5rem; border: 1px solid var(--pptx-border, #374151); background: var(--pptx-muted, #1f2937); color: var(--pptx-foreground, #f3f4f6); cursor: pointer; font-size: 0.875rem; transition: background 0.15s; }
428
+ .pick-label:hover { background: var(--pptx-accent, #374151); }
429
+ .or-sep { font-size: 0.8rem; color: var(--pptx-muted-foreground, #6b7280); }
430
+ .new-btn { padding: 0.5rem 1.25rem; border-radius: 0.5rem; border: none; background: var(--pptx-primary, #6366f1); color: #fff; cursor: pointer; font-size: 0.875rem; font-weight: 500; transition: opacity 0.15s; }
431
+ .new-btn:hover { opacity: 0.9; }
432
+ \`],
419
433
  template: \`
420
434
  @if (content(); as c) {
421
- <div style="height: 100vh">
422
- <pptx-power-point-viewer [content]="c" [canEdit]="true" style="height: 100%" />
435
+ <div style="height: 100dvh">
436
+ <pptx-power-point-viewer
437
+ [content]="c"
438
+ [canEdit]="true"
439
+ style="height: 100%"
440
+ [collaboration]="collab()"
441
+ (startCollaboration)="collab.set($event)"
442
+ (stopCollaboration)="collab.set(undefined)"
443
+ />
423
444
  </div>
424
445
  } @else {
425
- <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 24px; height: 100vh; font-family: system-ui, sans-serif">
426
- <h1 style="margin: 0; font-size: 24px; font-weight: 500; color: #e5e7eb">Open a Presentation</h1>
427
- <label style="display: inline-flex; align-items: center; gap: 8px; padding: 10px 20px; border-radius: 8px; border: 1px solid #4b5563; background: #1f2937; color: #f3f4f6; cursor: pointer; font-size: 14px">
428
- Choose .pptx file
429
- <input type="file" accept=".pptx" style="display: none" (change)="onPick($event)" />
430
- </label>
431
- <span style="color: #6b7280; font-size: 13px">or</span>
432
- <button style="padding: 10px 20px; border-radius: 8px; border: none; background: #2563eb; color: #fff; cursor: pointer; font-size: 14px; font-weight: 500" (click)="newPresentation()">New Presentation</button>
446
+ <div
447
+ class="stage"
448
+ [class.over]="over()"
449
+ (dragover)="$event.preventDefault(); over.set(true)"
450
+ (dragleave)="over.set(false)"
451
+ (drop)="onDrop($event)"
452
+ (click)="fileInput.click()"
453
+ >
454
+ <div class="dropzone">
455
+ <h1>Open a Presentation</h1>
456
+ <p>Drag &amp; drop a .pptx file here, or</p>
457
+ <label class="pick-label" (click)="$event.stopPropagation()">
458
+ Choose .pptx file
459
+ <input #fileInput type="file" accept=".pptx" style="display: none" (change)="onPick($event)" />
460
+ </label>
461
+ <span class="or-sep">or</span>
462
+ <button class="new-btn" (click)="$event.stopPropagation(); newPresentation()">New Presentation</button>
463
+ </div>
433
464
  </div>
434
465
  }
435
466
  \`,
436
467
  })
437
468
  export class App {
438
469
  content = signal<ArrayBuffer | Uint8Array | null>(null);
470
+ collab = signal<CollaborationConfig | undefined>(undefined);
471
+ over = signal(false);
472
+
473
+ async onDrop(e: DragEvent) {
474
+ e.preventDefault();
475
+ this.over.set(false);
476
+ const file = e.dataTransfer?.files?.[0];
477
+ if (file?.name.endsWith('.pptx')) this.content.set(await file.arrayBuffer());
478
+ }
439
479
 
440
480
  async onPick(e: Event) {
441
481
  const file = (e.target as HTMLInputElement).files?.[0];
442
- if (file) {
443
- this.content.set(await file.arrayBuffer());
444
- }
482
+ if (file) this.content.set(await file.arrayBuffer());
445
483
  }
446
484
 
447
485
  async newPresentation() {
@@ -487,17 +525,18 @@ bootstrapApplication(App, {
487
525
  // src/templates/react.ts
488
526
  var REACT_APP_TSX = `import { useCallback, useState } from 'react';
489
527
  import { PptxHandler } from 'pptx-viewer-core';
528
+ import type { CollaborationConfig } from 'pptx-react-viewer';
490
529
  import { PowerPointViewer } from 'pptx-react-viewer';
491
530
  import 'pptx-react-viewer/styles.css';
492
531
  import './i18n';
493
532
 
494
533
  export default function App() {
495
534
  const [content, setContent] = useState<Uint8Array | null>(null);
535
+ const [over, setOver] = useState(false);
536
+ const [collab, setCollab] = useState<CollaborationConfig | undefined>();
496
537
 
497
- const loadFile = useCallback((file: File) => {
498
- const reader = new FileReader();
499
- reader.onload = () => setContent(new Uint8Array(reader.result as ArrayBuffer));
500
- reader.readAsArrayBuffer(file);
538
+ const loadFile = useCallback(async (file: File) => {
539
+ setContent(new Uint8Array(await file.arrayBuffer()));
501
540
  }, []);
502
541
 
503
542
  const newPresentation = useCallback(async () => {
@@ -510,34 +549,55 @@ export default function App() {
510
549
 
511
550
  if (content) {
512
551
  return (
513
- <div style={{ height: '100vh' }}>
514
- <PowerPointViewer content={content} canEdit />
552
+ <div style={{ height: '100dvh' }}>
553
+ <PowerPointViewer
554
+ content={content}
555
+ canEdit
556
+ collaboration={collab}
557
+ onStartCollaboration={setCollab}
558
+ onStopCollaboration={() => setCollab(undefined)}
559
+ />
515
560
  </div>
516
561
  );
517
562
  }
518
563
 
519
564
  return (
520
- <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 24, height: '100vh', fontFamily: 'system-ui, sans-serif' }}>
521
- <h1 style={{ margin: 0, fontSize: 24, fontWeight: 500, color: '#e5e7eb' }}>Open a Presentation</h1>
522
- <label style={{ display: 'inline-flex', alignItems: 'center', gap: 8, padding: '10px 20px', borderRadius: 8, border: '1px solid #4b5563', background: '#1f2937', color: '#f3f4f6', cursor: 'pointer', fontSize: 14, transition: 'background 0.15s' }}>
523
- Choose .pptx file
524
- <input
525
- type="file"
526
- accept=".pptx"
527
- style={{ display: 'none' }}
528
- onChange={(e) => {
529
- const file = e.target.files?.[0];
530
- if (file) loadFile(file);
531
- }}
532
- />
533
- </label>
534
- <span style={{ color: '#6b7280', fontSize: 13 }}>or</span>
535
- <button
536
- onClick={() => void newPresentation()}
537
- style={{ padding: '10px 20px', borderRadius: 8, border: 'none', background: '#2563eb', color: '#fff', cursor: 'pointer', fontSize: 14, fontWeight: 500 }}
565
+ <div className="stage">
566
+ <div
567
+ className={\`dropzone\${over ? ' over' : ''}\`}
568
+ onDragOver={(e) => { e.preventDefault(); setOver(true); }}
569
+ onDragLeave={() => setOver(false)}
570
+ onDrop={(e) => {
571
+ e.preventDefault();
572
+ setOver(false);
573
+ const file = e.dataTransfer.files[0];
574
+ if (file?.name.endsWith('.pptx')) void loadFile(file);
575
+ }}
576
+ onClick={() => document.getElementById('file-input')?.click()}
538
577
  >
539
- New Presentation
540
- </button>
578
+ <h1>Open a Presentation</h1>
579
+ <p>Drag &amp; drop a .pptx file here, or</p>
580
+ <label className="pick-label" onClick={(e) => e.stopPropagation()}>
581
+ Choose .pptx file
582
+ <input
583
+ id="file-input"
584
+ type="file"
585
+ accept=".pptx"
586
+ style={{ display: 'none' }}
587
+ onChange={(e) => {
588
+ const file = e.target.files?.[0];
589
+ if (file) void loadFile(file);
590
+ }}
591
+ />
592
+ </label>
593
+ <span className="or-sep">or</span>
594
+ <button
595
+ className="new-btn"
596
+ onClick={(e) => { e.stopPropagation(); void newPresentation(); }}
597
+ >
598
+ New Presentation
599
+ </button>
600
+ </div>
541
601
  </div>
542
602
  );
543
603
  }
@@ -564,27 +624,157 @@ export default i18n;
564
624
 
565
625
  // src/templates/shared.ts
566
626
  var MINIMAL_APP_CSS = `:root {
567
- color-scheme: light dark;
627
+ color-scheme: dark;
628
+ }
629
+
630
+ *,
631
+ *::before,
632
+ *::after {
633
+ box-sizing: border-box;
634
+ }
635
+
636
+ body {
637
+ margin: 0;
638
+ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
639
+ overflow-x: hidden;
640
+ background: var(--pptx-background, #030712);
641
+ color: var(--pptx-foreground, #f3f4f6);
642
+ }
643
+
644
+ #app,
645
+ #root {
646
+ height: 100dvh;
647
+ }
648
+
649
+ .stage {
650
+ display: flex;
651
+ align-items: center;
652
+ justify-content: center;
653
+ height: 100dvh;
654
+ padding: 2rem;
655
+ }
656
+
657
+ .dropzone {
658
+ display: flex;
659
+ flex-direction: column;
660
+ align-items: center;
661
+ justify-content: center;
662
+ gap: 0.75rem;
663
+ max-width: 520px;
664
+ width: 100%;
665
+ padding: 3rem;
666
+ text-align: center;
667
+ border: 2px dashed var(--pptx-border, #374151);
668
+ border-radius: 0.75rem;
669
+ cursor: pointer;
670
+ transition:
671
+ border-color 0.15s,
672
+ background 0.15s;
673
+ }
674
+
675
+ .dropzone.over,
676
+ .dropzone:hover {
677
+ border-color: var(--pptx-primary, #6366f1);
678
+ background: var(--pptx-muted, rgba(255, 255, 255, 0.04));
679
+ }
680
+
681
+ .dropzone h1 {
682
+ margin: 0;
683
+ font-size: 1.5rem;
684
+ font-weight: 500;
685
+ }
686
+
687
+ .dropzone p {
688
+ margin: 0;
689
+ font-size: 0.875rem;
690
+ color: var(--pptx-muted-foreground, #9ca3af);
691
+ }
692
+
693
+ .pick-label {
694
+ display: inline-flex;
695
+ align-items: center;
696
+ gap: 0.5rem;
697
+ padding: 0.5rem 1.25rem;
698
+ border-radius: 0.5rem;
699
+ border: 1px solid var(--pptx-border, #374151);
700
+ background: var(--pptx-muted, #1f2937);
701
+ color: var(--pptx-foreground, #f3f4f6);
702
+ cursor: pointer;
703
+ font-size: 0.875rem;
704
+ transition: background 0.15s;
705
+ }
706
+
707
+ .pick-label:hover {
708
+ background: var(--pptx-accent, #374151);
709
+ }
710
+
711
+ .or-sep {
712
+ font-size: 0.8rem;
713
+ color: var(--pptx-muted-foreground, #6b7280);
714
+ }
715
+
716
+ .new-btn {
717
+ padding: 0.5rem 1.25rem;
718
+ border-radius: 0.5rem;
719
+ border: none;
720
+ background: var(--pptx-primary, #6366f1);
721
+ color: #fff;
722
+ cursor: pointer;
723
+ font-size: 0.875rem;
724
+ font-weight: 500;
725
+ transition: opacity 0.15s;
726
+ }
727
+
728
+ .new-btn:hover {
729
+ opacity: 0.9;
730
+ }
731
+ `;
732
+ var ANGULAR_GLOBAL_CSS = `:root {
733
+ color-scheme: dark;
734
+ }
735
+
736
+ *,
737
+ *::before,
738
+ *::after {
739
+ box-sizing: border-box;
568
740
  }
569
741
 
570
742
  body {
571
743
  margin: 0;
744
+ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
745
+ background: var(--pptx-background, #030712);
746
+ color: var(--pptx-foreground, #f3f4f6);
747
+ }
748
+
749
+ app-root {
750
+ display: block;
751
+ height: 100dvh;
572
752
  }
573
753
  `;
574
754
 
575
755
  // src/templates/svelte.ts
576
756
  var SVELTE_APP_SVELTE = `<script lang="ts">
577
757
  import { PptxHandler } from 'pptx-viewer-core';
758
+ import type { CollaborationConfig } from 'pptx-svelte-viewer';
578
759
  import { PowerPointViewer } from 'pptx-svelte-viewer';
579
760
 
580
761
  let content = $state<Uint8Array | null>(null);
762
+ let over = $state(false);
763
+ let collab = $state<CollaborationConfig | undefined>();
764
+
765
+ async function loadFile(file: File) {
766
+ content = new Uint8Array(await file.arrayBuffer());
767
+ }
768
+
769
+ function onDrop(e: DragEvent) {
770
+ over = false;
771
+ const file = e.dataTransfer?.files?.[0];
772
+ if (file?.name.endsWith('.pptx')) void loadFile(file);
773
+ }
581
774
 
582
775
  function onPick(e: Event) {
583
776
  const file = (e.target as HTMLInputElement).files?.[0];
584
- if (!file) return;
585
- const reader = new FileReader();
586
- reader.onload = () => (content = new Uint8Array(reader.result as ArrayBuffer));
587
- reader.readAsArrayBuffer(file);
777
+ if (file) void loadFile(file);
588
778
  }
589
779
 
590
780
  async function newPresentation() {
@@ -597,88 +787,154 @@ var SVELTE_APP_SVELTE = `<script lang="ts">
597
787
  </script>
598
788
 
599
789
  {#if content}
600
- <div style="height: 100vh">
601
- <PowerPointViewer source={content} editable />
790
+ <div style="height: 100dvh">
791
+ <PowerPointViewer
792
+ source={content}
793
+ editable
794
+ collaboration={collab}
795
+ onstartcollaboration={(cfg) => { collab = cfg; }}
796
+ onstopcollaboration={() => { collab = undefined; }}
797
+ />
602
798
  </div>
603
799
  {:else}
604
- <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 24px; height: 100vh; font-family: system-ui, sans-serif">
605
- <h1 style="margin: 0; font-size: 24px; font-weight: 500; color: #e5e7eb">Open a Presentation</h1>
606
- <label style="display: inline-flex; align-items: center; gap: 8px; padding: 10px 20px; border-radius: 8px; border: 1px solid #4b5563; background: #1f2937; color: #f3f4f6; cursor: pointer; font-size: 14px">
607
- Choose .pptx file
608
- <input type="file" accept=".pptx" style="display: none" onchange={onPick} />
609
- </label>
610
- <span style="color: #6b7280; font-size: 13px">or</span>
611
- <button style="padding: 10px 20px; border-radius: 8px; border: none; background: #2563eb; color: #fff; cursor: pointer; font-size: 14px; font-weight: 500" onclick={() => void newPresentation()}>New Presentation</button>
800
+ <div
801
+ class="stage"
802
+ ondragover={(e) => { e.preventDefault(); over = true; }}
803
+ ondragleave={() => { over = false; }}
804
+ ondrop={(e) => { e.preventDefault(); onDrop(e); }}
805
+ onclick={() => document.getElementById('file-input')?.click()}
806
+ role="button"
807
+ tabindex="0"
808
+ >
809
+ <div class="dropzone" class:over>
810
+ <h1>Open a Presentation</h1>
811
+ <p>Drag &amp; drop a .pptx file here, or</p>
812
+ <label class="pick-label" onclick={(e) => e.stopPropagation()}>
813
+ Choose .pptx file
814
+ <input id="file-input" type="file" accept=".pptx" style="display: none" onchange={onPick} />
815
+ </label>
816
+ <span class="or-sep">or</span>
817
+ <button class="new-btn" onclick={(e) => { e.stopPropagation(); void newPresentation(); }}>
818
+ New Presentation
819
+ </button>
820
+ </div>
612
821
  </div>
613
822
  {/if}
614
823
  `;
615
824
 
616
825
  // src/templates/vanilla.ts
617
- var VANILLA_MAIN_TS = `import { PptxHandler } from 'pptx-viewer-core';
618
- import { createPptxViewer } from 'pptx-vanilla-viewer';
826
+ var VANILLA_MAIN_TS = `import { createPptxViewer } from 'pptx-vanilla-viewer';
827
+ import { PptxHandler } from 'pptx-viewer-core';
619
828
 
620
829
  import './style.css';
621
830
 
622
831
  const app = document.querySelector<HTMLDivElement>('#app')!;
623
832
 
624
- const picker = document.createElement('div');
625
- picker.setAttribute(
626
- 'style',
627
- 'display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 24px; height: 100vh; font-family: system-ui, sans-serif',
628
- );
629
- picker.innerHTML = \`
630
- <h1 style="margin: 0; font-size: 24px; font-weight: 500; color: #e5e7eb">Open a Presentation</h1>
631
- <label style="display: inline-flex; align-items: center; gap: 8px; padding: 10px 20px; border-radius: 8px; border: 1px solid #4b5563; background: #1f2937; color: #f3f4f6; cursor: pointer; font-size: 14px">
632
- Choose .pptx file
633
- <input id="file-input" type="file" accept=".pptx" style="display: none" />
634
- </label>
635
- <span style="color: #6b7280; font-size: 13px">or</span>
636
- <button id="new-presentation" style="padding: 10px 20px; border-radius: 8px; border: none; background: #2563eb; color: #fff; cursor: pointer; font-size: 14px; font-weight: 500">New Presentation</button>
637
- \`;
638
- app.append(picker);
639
-
640
- function show(content: ArrayBuffer | Uint8Array) {
641
- picker.remove();
642
- app.style.height = '100vh';
643
- createPptxViewer(app, { source: content, editable: true });
644
- }
645
-
646
- picker.querySelector<HTMLInputElement>('#file-input')!.addEventListener('change', (e) => {
647
- const file = (e.target as HTMLInputElement).files?.[0];
648
- if (file) {
649
- void file.arrayBuffer().then(show);
650
- }
651
- });
833
+ function show(source: ArrayBuffer | Uint8Array): void {
834
+ app.innerHTML = '';
835
+ app.style.height = '100dvh';
836
+ createPptxViewer(app, { source, editable: true });
837
+ }
838
+
839
+ function showLanding(): void {
840
+ app.style.height = '';
841
+ app.innerHTML = '';
842
+
843
+ const stage = document.createElement('div');
844
+ stage.className = 'stage';
845
+
846
+ const zone = document.createElement('div');
847
+ zone.className = 'dropzone';
848
+
849
+ const h1 = document.createElement('h1');
850
+ h1.textContent = 'Open a Presentation';
851
+
852
+ const hint = document.createElement('p');
853
+ hint.textContent = 'Drag & drop a .pptx file here, or';
854
+
855
+ const label = document.createElement('label');
856
+ label.className = 'pick-label';
857
+ label.textContent = 'Choose .pptx file';
858
+
859
+ const input = document.createElement('input');
860
+ input.type = 'file';
861
+ input.accept = '.pptx';
862
+ input.style.display = 'none';
863
+ label.append(input);
864
+
865
+ const orSep = document.createElement('span');
866
+ orSep.className = 'or-sep';
867
+ orSep.textContent = 'or';
868
+
869
+ const newBtn = document.createElement('button');
870
+ newBtn.className = 'new-btn';
871
+ newBtn.textContent = 'New Presentation';
872
+
873
+ zone.append(h1, hint, label, orSep, newBtn);
874
+ stage.append(zone);
875
+ app.append(stage);
876
+
877
+ zone.addEventListener('dragover', (e) => {
878
+ e.preventDefault();
879
+ zone.classList.add('over');
880
+ });
881
+ zone.addEventListener('dragleave', () => zone.classList.remove('over'));
882
+ zone.addEventListener('drop', (e) => {
883
+ e.preventDefault();
884
+ zone.classList.remove('over');
885
+ const file = e.dataTransfer?.files?.[0];
886
+ if (file?.name.endsWith('.pptx')) void file.arrayBuffer().then(show);
887
+ });
652
888
 
653
- picker.querySelector<HTMLButtonElement>('#new-presentation')!.addEventListener('click', () => {
654
- void (async () => {
889
+ // Click the zone to open the file picker (but not if the button was clicked).
890
+ zone.addEventListener('click', () => input.click());
891
+ label.addEventListener('click', (e) => e.stopPropagation());
892
+ input.addEventListener('click', (e) => e.stopPropagation());
893
+ input.addEventListener('change', () => {
894
+ const file = input.files?.[0];
895
+ if (file) void file.arrayBuffer().then(show);
896
+ });
897
+
898
+ newBtn.addEventListener('click', async (e) => {
899
+ e.stopPropagation();
900
+ newBtn.textContent = 'Creating...';
901
+ newBtn.disabled = true;
655
902
  const { handler, data } = await PptxHandler.createBlank({
656
903
  title: 'Untitled Presentation',
657
904
  initialSlideCount: 1,
658
905
  });
659
906
  show(await handler.save(data.slides));
660
- })();
661
- });
907
+ });
908
+ }
909
+
910
+ showLanding();
662
911
  `;
663
912
 
664
913
  // src/templates/vue.ts
665
914
  var VUE_APP_VUE = `<script setup lang="ts">
666
915
  import { ref } from 'vue';
667
916
  import { PptxHandler } from 'pptx-viewer-core';
917
+ import type { CollaborationConfig } from 'pptx-vue-viewer';
668
918
  import { PowerPointViewer } from 'pptx-vue-viewer';
669
919
  import 'pptx-vue-viewer/styles.css';
670
920
 
671
921
  const content = ref<Uint8Array>();
922
+ const over = ref(false);
923
+ const collab = ref<CollaborationConfig | undefined>();
672
924
 
673
- function loadFile(file: File) {
674
- const reader = new FileReader();
675
- reader.onload = () => (content.value = new Uint8Array(reader.result as ArrayBuffer));
676
- reader.readAsArrayBuffer(file);
925
+ async function loadFile(file: File) {
926
+ content.value = new Uint8Array(await file.arrayBuffer());
927
+ }
928
+
929
+ function onDrop(e: DragEvent) {
930
+ over.value = false;
931
+ const file = e.dataTransfer?.files?.[0];
932
+ if (file?.name.endsWith('.pptx')) void loadFile(file);
677
933
  }
678
934
 
679
935
  function onPick(e: Event) {
680
936
  const file = (e.target as HTMLInputElement).files?.[0];
681
- if (file) loadFile(file);
937
+ if (file) void loadFile(file);
682
938
  }
683
939
 
684
940
  async function newPresentation() {
@@ -691,19 +947,53 @@ async function newPresentation() {
691
947
  </script>
692
948
 
693
949
  <template>
694
- <div v-if="content" style="height: 100vh">
695
- <PowerPointViewer :content="content" can-edit style="height: 100%" />
950
+ <div v-if="content" style="height: 100dvh">
951
+ <PowerPointViewer
952
+ :content="content"
953
+ can-edit
954
+ style="height: 100%"
955
+ :collaboration="collab"
956
+ @start-collaboration="collab = $event"
957
+ @stop-collaboration="collab = undefined"
958
+ />
696
959
  </div>
697
- <div v-else style="display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 24px; height: 100vh; font-family: system-ui, sans-serif">
698
- <h1 style="margin: 0; font-size: 24px; font-weight: 500; color: #e5e7eb">Open a Presentation</h1>
699
- <label style="display: inline-flex; align-items: center; gap: 8px; padding: 10px 20px; border-radius: 8px; border: 1px solid #4b5563; background: #1f2937; color: #f3f4f6; cursor: pointer; font-size: 14px">
700
- Choose .pptx file
701
- <input type="file" accept=".pptx" style="display: none" @change="onPick" />
702
- </label>
703
- <span style="color: #6b7280; font-size: 13px">or</span>
704
- <button style="padding: 10px 20px; border-radius: 8px; border: none; background: #2563eb; color: #fff; cursor: pointer; font-size: 14px; font-weight: 500" @click="newPresentation">New Presentation</button>
960
+ <div
961
+ v-else
962
+ class="stage"
963
+ @dragover.prevent="over = true"
964
+ @dragleave="over = false"
965
+ @drop.prevent="onDrop($event as DragEvent)"
966
+ @click="($refs.input as HTMLInputElement).click()"
967
+ >
968
+ <div :class="['dropzone', { over }]">
969
+ <h1>Open a Presentation</h1>
970
+ <p>Drag &amp; drop a .pptx file here, or</p>
971
+ <label class="pick-label" @click.stop>
972
+ Choose .pptx file
973
+ <input ref="input" type="file" accept=".pptx" style="display: none" @change="onPick" />
974
+ </label>
975
+ <span class="or-sep">or</span>
976
+ <button class="new-btn" @click.stop="newPresentation">New Presentation</button>
977
+ </div>
705
978
  </div>
706
979
  </template>
980
+
981
+ <style>
982
+ :root { color-scheme: dark; }
983
+ *, *::before, *::after { box-sizing: border-box; }
984
+ body { margin: 0; font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; background: var(--pptx-background, #030712); color: var(--pptx-foreground, #f3f4f6); }
985
+ #app { height: 100dvh; }
986
+ .stage { display: flex; align-items: center; justify-content: center; height: 100dvh; padding: 2rem; }
987
+ .dropzone { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.75rem; max-width: 520px; width: 100%; padding: 3rem; text-align: center; border: 2px dashed var(--pptx-border, #374151); border-radius: 0.75rem; cursor: pointer; transition: border-color 0.15s, background 0.15s; }
988
+ .dropzone.over, .dropzone:hover { border-color: var(--pptx-primary, #6366f1); background: var(--pptx-muted, rgba(255, 255, 255, 0.04)); }
989
+ .dropzone h1 { margin: 0; font-size: 1.5rem; font-weight: 500; }
990
+ .dropzone p { margin: 0; font-size: 0.875rem; color: var(--pptx-muted-foreground, #9ca3af); }
991
+ .pick-label { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.5rem 1.25rem; border-radius: 0.5rem; border: 1px solid var(--pptx-border, #374151); background: var(--pptx-muted, #1f2937); color: var(--pptx-foreground, #f3f4f6); cursor: pointer; font-size: 0.875rem; transition: background 0.15s; }
992
+ .pick-label:hover { background: var(--pptx-accent, #374151); }
993
+ .or-sep { font-size: 0.8rem; color: var(--pptx-muted-foreground, #6b7280); }
994
+ .new-btn { padding: 0.5rem 1.25rem; border-radius: 0.5rem; border: none; background: var(--pptx-primary, #6366f1); color: #fff; cursor: pointer; font-size: 0.875rem; font-weight: 500; transition: opacity 0.15s; }
995
+ .new-btn:hover { opacity: 0.9; }
996
+ </style>
707
997
  `;
708
998
  var VUE_MAIN_TS = `import { createApp } from 'vue';
709
999
  import { createI18n } from 'vue-i18n';
@@ -724,6 +1014,13 @@ createApp(App).use(i18n).mount('#app');
724
1014
  `;
725
1015
 
726
1016
  // src/targets.ts
1017
+ var COLLAB_EXTRAS = [
1018
+ {
1019
+ prompt: "Include real-time collaboration? (adds yjs, y-websocket, y-webrtc)",
1020
+ packages: ["yjs", "y-websocket", "y-webrtc"]
1021
+ // defaultInclude is true (the default) - demo apps ship with collab packages.
1022
+ }
1023
+ ];
727
1024
  var TARGETS = [
728
1025
  {
729
1026
  id: "react",
@@ -775,7 +1072,8 @@ Docs: https://www.npmjs.com/package/pptx-react-viewer`,
775
1072
  extraFiles: {
776
1073
  "src/i18n.ts": REACT_I18N_TS,
777
1074
  "src/index.css": MINIMAL_APP_CSS
778
- }
1075
+ },
1076
+ optionalExtras: COLLAB_EXTRAS
779
1077
  }
780
1078
  },
781
1079
  {
@@ -808,7 +1106,8 @@ Docs: https://www.npmjs.com/package/pptx-vue-viewer`,
808
1106
  ],
809
1107
  entryCandidates: ["src/App.vue"],
810
1108
  entryContent: VUE_APP_VUE,
811
- extraFiles: { "src/main.ts": VUE_MAIN_TS }
1109
+ extraFiles: { "src/main.ts": VUE_MAIN_TS },
1110
+ optionalExtras: COLLAB_EXTRAS
812
1111
  }
813
1112
  },
814
1113
  {
@@ -844,7 +1143,22 @@ Docs: https://www.npmjs.com/package/pptx-angular-viewer`,
844
1143
  // Angular v20+ generates `app.ts`; older schematics generate `app.component.ts`.
845
1144
  entryCandidates: ["src/app/app.ts", "src/app/app.component.ts"],
846
1145
  entryContent: ANGULAR_APP_TS,
847
- extraFiles: { "src/main.ts": ANGULAR_MAIN_TS }
1146
+ extraFiles: { "src/main.ts": ANGULAR_MAIN_TS, "src/styles.css": ANGULAR_GLOBAL_CSS },
1147
+ // @angular/cli@22 requires Node.js >=22.22.0, >=24.13.1, or >=26.0.0. Check
1148
+ // BEFORE the project-name prompt so the user sees the real reason immediately.
1149
+ preflight: () => {
1150
+ const node = process.versions.node;
1151
+ const [maj, min, pat] = node.split(".").map(Number);
1152
+ const ok = maj === 22 && min >= 22 || maj === 24 && (min > 13 || min === 13 && pat >= 1) || maj >= 26;
1153
+ if (!ok) {
1154
+ throw new Error(
1155
+ `@angular/cli@latest requires Node.js v22.22.0+, v24.13.1+, or v26.0.0+.
1156
+ You are running Node.js v${node}.
1157
+ Update Node.js at: https://nodejs.org`
1158
+ );
1159
+ }
1160
+ },
1161
+ optionalExtras: COLLAB_EXTRAS
848
1162
  }
849
1163
  },
850
1164
  {
@@ -872,7 +1186,8 @@ Docs: https://www.npmjs.com/package/pptx-svelte-viewer`,
872
1186
  entryContent: SVELTE_APP_SVELTE,
873
1187
  // The starter's main.ts imports ./app.css; replace the Vite demo
874
1188
  // styles (centred #app with padding) with a full-viewport reset.
875
- extraFiles: { "src/app.css": MINIMAL_APP_CSS }
1189
+ extraFiles: { "src/app.css": MINIMAL_APP_CSS },
1190
+ optionalExtras: COLLAB_EXTRAS
876
1191
  }
877
1192
  },
878
1193
  {
@@ -895,12 +1210,19 @@ Docs: https://www.npmjs.com/package/pptx-vanilla-viewer`,
895
1210
  scaffold: {
896
1211
  command: "create-vite@latest",
897
1212
  args: (dir) => [dir, "--template", "vanilla-ts", "--no-interactive", "--no-immediate"],
898
- extraPackages: ["pptx-vanilla-viewer", "pptx-viewer-core", "jszip", "fast-xml-parser"],
1213
+ extraPackages: [
1214
+ "pptx-vanilla-viewer",
1215
+ "pptx-viewer-core",
1216
+ "three",
1217
+ "jszip",
1218
+ "fast-xml-parser"
1219
+ ],
899
1220
  entryCandidates: ["src/main.ts"],
900
1221
  entryContent: VANILLA_MAIN_TS,
901
1222
  // main.ts imports ./style.css; replace the Vite demo styles with a
902
1223
  // full-viewport reset.
903
- extraFiles: { "src/style.css": MINIMAL_APP_CSS }
1224
+ extraFiles: { "src/style.css": MINIMAL_APP_CSS },
1225
+ optionalExtras: COLLAB_EXTRAS
904
1226
  }
905
1227
  },
906
1228
  {
@@ -999,7 +1321,7 @@ import { spawn } from "child_process";
999
1321
  function runCommand(command, args, cwd, options) {
1000
1322
  return new Promise((resolve, reject) => {
1001
1323
  const isWindows = process.platform === "win32";
1002
- const stdio = options?.silent ? "ignore" : "inherit";
1324
+ const stdio = options?.silent ? ["inherit", "ignore", "inherit"] : "inherit";
1003
1325
  const child = isWindows ? spawn([command, ...args].join(" "), { cwd, stdio, shell: true }) : spawn(command, args, { cwd, stdio });
1004
1326
  child.on("error", reject);
1005
1327
  child.on("close", (code) => resolve(code ?? 1));
@@ -1138,6 +1460,18 @@ async function runScaffoldMode(target, args, configTargets, cwd) {
1138
1460
  if (!recipe) {
1139
1461
  throw new Error(`${target.label} has no scaffold recipe.`);
1140
1462
  }
1463
+ recipe.preflight?.();
1464
+ const optionalPkgs = [];
1465
+ if (recipe.optionalExtras) {
1466
+ for (const extra of recipe.optionalExtras) {
1467
+ const defaultChoice = extra.defaultInclude !== false;
1468
+ const include = args.yes || !process.stdin.isTTY ? defaultChoice : await confirm(extra.prompt);
1469
+ if (include) {
1470
+ optionalPkgs.push(...extra.packages);
1471
+ }
1472
+ }
1473
+ }
1474
+ const effectiveRecipe = optionalPkgs.length > 0 ? { ...recipe, extraPackages: [...recipe.extraPackages, ...optionalPkgs] } : recipe;
1141
1475
  const defaultName = `pptx-${target.id}-app`;
1142
1476
  const rawName = args.dir ?? (process.stdin.isTTY ? await input("Project directory name", defaultName) : defaultName);
1143
1477
  const projectName = sanitizeProjectName(rawName);
@@ -1155,7 +1489,7 @@ ${dim("Skipped.")}`);
1155
1489
  return;
1156
1490
  }
1157
1491
  }
1158
- const result = await scaffoldProject(recipe, projectName, pm, cwd);
1492
+ const result = await scaffoldProject(effectiveRecipe, projectName, pm, cwd);
1159
1493
  if (!result.patchedFile) {
1160
1494
  console.log(
1161
1495
  `
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@christophervr/pptx-viewer",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
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
  "angular",