pptx-vanilla-viewer 0.1.2 → 0.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/dist/index.d.cts CHANGED
@@ -1368,9 +1368,144 @@ interface CanvasSize {
1368
1368
  width: number;
1369
1369
  height: number;
1370
1370
  }
1371
+ /** Collaboration role within a session. */
1372
+ type CollaborationRole = 'owner' | 'collaborator' | 'viewer';
1373
+ /**
1374
+ * Collaboration transport.
1375
+ *
1376
+ * - `'websocket'` (default): y-websocket against `serverUrl`.
1377
+ * - `'webrtc'`: y-webrtc peer-to-peer; needs no document server. Peers meet
1378
+ * through the `signaling` servers (WebRTC signaling only, no document data)
1379
+ * and same-browser tabs additionally sync via BroadcastChannel even without
1380
+ * any signaling server, which makes this mode usable from static hosting.
1381
+ */
1382
+ type CollaborationTransport = 'websocket' | 'webrtc';
1383
+ /**
1384
+ * Real-time collaboration configuration.
1385
+ *
1386
+ * The same shape is accepted by the React, Vue, and Angular bindings.
1387
+ */
1388
+ interface CollaborationConfig {
1389
+ /** Unique identifier for the collaboration room (alphanumeric, hyphens, underscores). */
1390
+ roomId: string;
1391
+ /**
1392
+ * WebSocket server URL for the Yjs provider (e.g. "wss://collab.example.com").
1393
+ * Ignored (may be empty) when `transport` is `'webrtc'`.
1394
+ */
1395
+ serverUrl: string;
1396
+ /** Transport to use. Defaults to `'websocket'`. */
1397
+ transport?: CollaborationTransport;
1398
+ /**
1399
+ * WebRTC signaling server URLs (only used when `transport` is `'webrtc'`).
1400
+ * Defaults to y-webrtc's built-in public signaling list. Same-browser tabs
1401
+ * sync via BroadcastChannel regardless of signaling availability.
1402
+ */
1403
+ signaling?: string[];
1404
+ /** Display name for the local user. */
1405
+ userName: string;
1406
+ /** Avatar URL for the local user (optional). */
1407
+ userAvatar?: string;
1408
+ /** Hex colour for the local user's cursor/presence indicator. */
1409
+ userColor?: string;
1410
+ /** Optional authentication token sent with the WebSocket handshake. */
1411
+ authToken?: string;
1412
+ /** Role in the session; defaults to `'collaborator'`. */
1413
+ role?: CollaborationRole;
1414
+ /**
1415
+ * Elected-writer write-back callback (Area 3 of the C3 hardening plan).
1416
+ *
1417
+ * When the local user has `role: 'owner'`, the binding debounces changes and
1418
+ * serializes the current Y.Doc state to a PPTX byte array, then calls this
1419
+ * callback so the host can persist the snapshot. Only one writer (the owner)
1420
+ * does this; other collaborators never trigger write-back, eliminating the
1421
+ * last-save-wins problem.
1422
+ */
1423
+ onWriteBack?: (bytes: Uint8Array) => void;
1424
+ /**
1425
+ * Debounce delay (ms) between the last Y.Doc change and the write-back
1426
+ * invocation. Defaults to 5000 ms. Set to 0 to write back on every change
1427
+ * (not recommended for large documents).
1428
+ */
1429
+ writeBackDebounceMs?: number;
1430
+ }
1371
1431
  /** A neutral CSS map (framework `CSSProperties` are structurally compatible). */
1372
1432
  type CssStyleMap = Record<string, string | number>;
1373
1433
 
1434
+ /**
1435
+ * collaboration-presence.ts: Pure, framework-agnostic logic for the real-time
1436
+ * collaboration subsystem (Yjs-backed presence + remote cursors).
1437
+ *
1438
+ * Everything here is a plain function with no framework / Yjs dependency, so it
1439
+ * can be unit-tested in isolation and shared across the React, Vue, and Angular
1440
+ * bindings. The bindings own the stateful provider lifecycle (creating the
1441
+ * Y.Doc / WebsocketProvider, wiring awareness listeners) and call into these
1442
+ * helpers to validate config, sanitise inbound awareness data, and project it
1443
+ * into render-ready view-models.
1444
+ *
1445
+ * Responsibilities:
1446
+ * - Validate the room id and detect mixed-content (ws:// from https) up front.
1447
+ * - Sanitise inbound awareness data (XSS, bounds, colour, avatar, room id).
1448
+ * - Map awareness state into a `RemoteCursor` view-model for rendering.
1449
+ * - Derive the presence list (remote users only, stale entries dropped).
1450
+ * - Deterministic per-user colour assignment + cursor label formatting.
1451
+ *
1452
+ * This mirrors the React `sanitize.ts` / `usePresenceTracking.ts` helpers and
1453
+ * the Angular `collaboration-helpers.ts` (which predates this shared copy).
1454
+ */
1455
+
1456
+ /** Connection lifecycle states for the Yjs WebSocket provider. */
1457
+ type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error';
1458
+ interface AutosaveRecord {
1459
+ key: string;
1460
+ data: Uint8Array;
1461
+ timestamp: number;
1462
+ size: number;
1463
+ }
1464
+
1465
+ /**
1466
+ * Pure handout layout calculations, shared by every binding's print path.
1467
+ *
1468
+ * Handles distributing slides across pages, computing grid dimensions, and
1469
+ * positioning cells within A4 page space. No DOM/framework dependency: callers
1470
+ * render the resulting rectangles however their view layer prefers.
1471
+ */
1472
+ /** Supported slides-per-page values. */
1473
+ type HandoutSlidesPerPage = 1 | 2 | 3 | 4 | 6 | 9;
1474
+
1475
+ /**
1476
+ * Pure print helpers shared by every binding's print path: settings validation,
1477
+ * slide-range / colour-filter resolution, page-count estimation, HTML markup
1478
+ * builders, escaping, and the full print-document assembler.
1479
+ *
1480
+ * No DOM side effects and no `window.print()`: everything is deterministic and
1481
+ * the binding writes the returned HTML string into a print window. The handout
1482
+ * grid geometry lives in `handout-layout`; this module reuses it.
1483
+ *
1484
+ * ng-packagr constraint honoured here (the Angular binding inlines this source
1485
+ * and compiles it through ng-packagr): NO `String.prototype.replaceAll`
1486
+ * (`escapeHtml` uses `.split(x).join(y)` instead).
1487
+ */
1488
+
1489
+ /** What to print. */
1490
+ type PrintWhat = 'slides' | 'handouts' | 'notes' | 'outline';
1491
+ /** Page orientation for the printed output. */
1492
+ type PrintOrientation = 'portrait' | 'landscape';
1493
+ /** Colour mode for the printed output. */
1494
+ type PrintColorMode = 'color' | 'grayscale' | 'blackAndWhite';
1495
+ /** Slide range mode. */
1496
+ type PrintSlideRange = 'all' | 'current' | 'custom';
1497
+ /** Resolved print settings emitted on confirm. */
1498
+ interface PrintSettings {
1499
+ printWhat: PrintWhat;
1500
+ orientation: PrintOrientation;
1501
+ colorMode: PrintColorMode;
1502
+ frameSlides: boolean;
1503
+ slidesPerPage: HandoutSlidesPerPage;
1504
+ slideRange: PrintSlideRange;
1505
+ customRangeFrom: number;
1506
+ customRangeTo: number;
1507
+ }
1508
+
1374
1509
  /**
1375
1510
  * Minimal i18n for the vanilla binding.
1376
1511
  *
@@ -1394,6 +1529,154 @@ type TranslationMessages = Record<string, Record<string, string>>;
1394
1529
  */
1395
1530
  declare function createTranslator(locale?: string, messages?: TranslationMessages): Translator;
1396
1531
 
1532
+ /**
1533
+ * A tiny framework-free reactive store: `get` / `set(patch)` / `subscribe`.
1534
+ *
1535
+ * This is deliberately minimal (a few dozen lines, no external deps). It only
1536
+ * carries the vanilla binding's *view state*; all domain logic (parsing,
1537
+ * styles, geometry, text building) stays in `pptx-viewer-core` and
1538
+ * `pptx-viewer-shared`.
1539
+ */
1540
+ /** Listener invoked after every state change with the next and previous state. */
1541
+ type StoreListener<T> = (state: T, previous: T) => void;
1542
+ interface Store<T extends object> {
1543
+ /** Current state snapshot (do not mutate). */
1544
+ get(): T;
1545
+ /** Shallow-merge a partial patch into the state and notify subscribers. */
1546
+ set(patch: Partial<T>): void;
1547
+ /** Subscribe to state changes; returns an unsubscribe function. */
1548
+ subscribe(listener: StoreListener<T>): () => void;
1549
+ }
1550
+
1551
+ /** `zoom` is either an explicit scale factor (1 = 100%) or fit-to-viewport. */
1552
+ type ZoomLevel = number | 'fit';
1553
+ /**
1554
+ * The vanilla viewer's reactive view state. Kept intentionally flat and small;
1555
+ * everything here is what the DOM render layer consumes.
1556
+ */
1557
+ interface ViewerState {
1558
+ /** Parsed slides (image/media URLs already patched in by the load pipeline). */
1559
+ slides: P[];
1560
+ /** Slide canvas size in CSS pixels. */
1561
+ canvasSize: CanvasSize;
1562
+ /** Archive-path to displayable URL map for media + poster frames. */
1563
+ mediaDataUrls: Map<string, string>;
1564
+ /** Zero-based index of the visible slide. */
1565
+ currentSlide: number;
1566
+ /** Requested zoom (explicit factor or fit-to-viewport). */
1567
+ zoom: ZoomLevel;
1568
+ /** True while a load is in flight. */
1569
+ loading: boolean;
1570
+ /** Error message from the last failed load, or null. */
1571
+ error: string | null;
1572
+ /** True while presentation (fullscreen) mode is active. */
1573
+ presenting: boolean;
1574
+ /** True when editing interactions (select/move/resize/...) are enabled. */
1575
+ editable: boolean;
1576
+ /** Id of the selected element on the current slide, or null. */
1577
+ selectedElementId: string | null;
1578
+ /** True when the document has unsaved edits (cleared by a save). */
1579
+ dirty: boolean;
1580
+ /**
1581
+ * True while a pointer gesture (drag/resize/rotate) is in flight. Thumbnail
1582
+ * re-renders are deferred until the gesture ends.
1583
+ */
1584
+ interactionActive: boolean;
1585
+ /**
1586
+ * True when the speaker-notes panel body is expanded. Persists across slide
1587
+ * navigation for the life of the viewer instance (in-memory only).
1588
+ */
1589
+ notesExpanded: boolean;
1590
+ }
1591
+
1592
+ /**
1593
+ * Insert-element factories for the vanilla editor.
1594
+ *
1595
+ * The pure builders wrap the framework-agnostic core/shared factories
1596
+ * (`createTextElement`, `createShapeElement`, `createConnectorElement`,
1597
+ * `newTableElement`) and centre the new element on the slide canvas. The one
1598
+ * async helper (`pickImageElement`) owns the file-picker + `FileReader` DOM
1599
+ * side effects needed to turn a chosen image file into a data-URL image
1600
+ * element; it too returns a centred, ready-to-insert element.
1601
+ */
1602
+ /** The shape/text/table element kinds the insert menu can create directly. */
1603
+ type InsertKind = 'text' | 'rect' | 'ellipse' | 'line' | 'table';
1604
+
1605
+ /** The selection-derived state the toolbar reflects (enable/disable + values). */
1606
+ interface FormatSelectionState {
1607
+ hasSelection: boolean;
1608
+ canText: boolean;
1609
+ canShape: boolean;
1610
+ bold: boolean;
1611
+ italic: boolean;
1612
+ underline: boolean;
1613
+ fontSize: number;
1614
+ textColor: string | undefined;
1615
+ highlightColor: string | undefined;
1616
+ fillColor: string | undefined;
1617
+ strokeColor: string | undefined;
1618
+ }
1619
+ interface FormatToolbar {
1620
+ el: HTMLElement;
1621
+ /** Reflect the current selection (enable/disable controls + seed values). */
1622
+ update(state: FormatSelectionState): void;
1623
+ /** Show/hide the whole row (editing mode on/off). */
1624
+ setEditable(editable: boolean): void;
1625
+ }
1626
+
1627
+ /** A geometry patch from the inspector (all fields optional). */
1628
+ interface GeometryPatch {
1629
+ x?: number;
1630
+ y?: number;
1631
+ width?: number;
1632
+ height?: number;
1633
+ rotation?: number;
1634
+ }
1635
+ /**
1636
+ * The formatting / insert / arrange actions exposed to the editing chrome
1637
+ * (format toolbar, inspector, insert menu). Every mutating action is
1638
+ * history-integrated (push -> mutate -> commit) via the shared {@link EditorOps}.
1639
+ */
1640
+ interface EditActions {
1641
+ toggleBold(): void;
1642
+ toggleItalic(): void;
1643
+ toggleUnderline(): void;
1644
+ changeFontSize(delta: number): void;
1645
+ setFontSize(size: number): void;
1646
+ setTextColor(color: string): void;
1647
+ setHighlightColor(color: string): void;
1648
+ setShapeFill(color: string): void;
1649
+ setShapeStroke(color: string): void;
1650
+ setShapeStrokeWidth(width: number): void;
1651
+ /** Commit an inspector geometry edit (X/Y/W/H/rotation). */
1652
+ setGeometry(patch: GeometryPatch): void;
1653
+ insert(kind: InsertKind): void;
1654
+ insertImage(): Promise<void>;
1655
+ bringForward(): void;
1656
+ sendBackward(): void;
1657
+ bringToFront(): void;
1658
+ sendToBack(): void;
1659
+ }
1660
+
1661
+ /** Selection-derived state the inspector reflects. */
1662
+ interface InspectorState {
1663
+ hasSelection: boolean;
1664
+ canShape: boolean;
1665
+ x: number;
1666
+ y: number;
1667
+ width: number;
1668
+ height: number;
1669
+ rotation: number;
1670
+ fillColor: string | undefined;
1671
+ strokeColor: string | undefined;
1672
+ strokeWidth: number;
1673
+ }
1674
+ interface Inspector {
1675
+ el: HTMLElement;
1676
+ update(state: InspectorState): void;
1677
+ setEditable(editable: boolean): void;
1678
+ }
1679
+
1397
1680
  /** State the panel needs to reflect: the slide to read notes from, and whether edits are allowed. */
1398
1681
  interface NotesPanelUpdate {
1399
1682
  slide: P | undefined;
@@ -1442,6 +1725,12 @@ interface Toolbar {
1442
1725
  setEditState(state: ToolbarEditState): void;
1443
1726
  /** Reflect the notes panel's expanded/collapsed state on the Notes button. */
1444
1727
  setNotesExpanded(expanded: boolean): void;
1728
+ /**
1729
+ * Show the autosave status pill. `label` is the (already localized) text;
1730
+ * `kind` drives the styling hook (`is-saving` / `is-error`). An empty label
1731
+ * hides the pill.
1732
+ */
1733
+ setAutosaveStatus(label: string, kind: 'idle' | 'saving' | 'saved' | 'error'): void;
1445
1734
  }
1446
1735
 
1447
1736
  /** The viewer's static DOM skeleton plus the mutable overlay controls. */
@@ -1449,6 +1738,10 @@ interface ViewerChrome {
1449
1738
  /** `.pptxv` root (focusable; keyboard navigation attaches here). */
1450
1739
  root: HTMLElement;
1451
1740
  toolbar: Toolbar | null;
1741
+ /** Editing format toolbar (bold/fill/insert/z-order); null when disabled. */
1742
+ formatToolbar: FormatToolbar | null;
1743
+ /** Property inspector panel; null when disabled. */
1744
+ inspector: Inspector | null;
1452
1745
  thumbnails: ThumbnailRail | null;
1453
1746
  /** Scrollable centring viewport around the stage. */
1454
1747
  viewport: HTMLElement;
@@ -1473,6 +1766,78 @@ interface PresentationController {
1473
1766
  dispose(): void;
1474
1767
  }
1475
1768
 
1769
+ interface EditorController {
1770
+ /** (Re)wire listeners + overlay into the current chrome (after mount). */
1771
+ attachChrome(): void;
1772
+ detachChrome(): void;
1773
+ /** Called by the render controller after every stage render. */
1774
+ onStageRendered(): void;
1775
+ /** True while editing owns the keyboard (selection or inline editing). */
1776
+ capturesKeyboard(): boolean;
1777
+ /** Drop history/selection/dirty state (new content loaded). */
1778
+ reset(): void;
1779
+ setEditable(editable: boolean): void;
1780
+ undo(): void;
1781
+ redo(): void;
1782
+ canUndo(): boolean;
1783
+ canRedo(): boolean;
1784
+ deleteSelected(): void;
1785
+ duplicateSelected(): string | null;
1786
+ getSelectedElementId(): string | null;
1787
+ /** The formatting / insert / arrange actions for the editing chrome. */
1788
+ getEditActions(): EditActions;
1789
+ /** Commit the speaker-notes textarea's plain text onto the current slide. */
1790
+ commitNotes(notes: string): void;
1791
+ save(): Promise<Uint8Array>;
1792
+ downloadPptx(fileName?: string): Promise<void>;
1793
+ destroy(): void;
1794
+ }
1795
+
1796
+ /** Everything the controller needs after a stage (re)render. */
1797
+ interface SyncStageParams {
1798
+ doc: Document;
1799
+ /** The stage host (position: relative), for layering a transition overlay. */
1800
+ stageWrap: HTMLElement;
1801
+ /** The freshly rendered, fully-visible main stage node. */
1802
+ stage: HTMLElement;
1803
+ /** The slide the stage renders, or `undefined` when empty. */
1804
+ slide: P | undefined;
1805
+ /** Zero-based index of the rendered slide. */
1806
+ slideIndex: number;
1807
+ /** True only when the live (fullscreen) presentation stage is active. */
1808
+ presenting: boolean;
1809
+ }
1810
+ /**
1811
+ * The presentation-mode playback state machine for the vanilla binding.
1812
+ *
1813
+ * It owns the click-stepped animation cursor and the slide-transition overlay,
1814
+ * both driven by the shared framework-agnostic helpers. It is consulted from
1815
+ * two seams in the rebuild-per-change render flow:
1816
+ *
1817
+ * - {@link PresentationPlayback.syncStage} runs after every stage render:
1818
+ * on a slide entry it rebuilds the click groups, resets the step, hides
1819
+ * pending entrances, and (when the slide changed mid-show) plays the
1820
+ * incoming slide's transition over a snapshot of the outgoing stage.
1821
+ * - {@link PresentationPlayback.advance} runs on each forward navigation
1822
+ * key/tap while presenting: it reveals the next animation build in place
1823
+ * (no rebuild) and reports whether a build remained, so the caller only
1824
+ * advances the slide once the timeline is exhausted.
1825
+ */
1826
+ interface PresentationPlayback {
1827
+ /**
1828
+ * Reveal the next on-click animation build for the current slide. Returns
1829
+ * `true` if a build was revealed (stay on the slide); `false` when the
1830
+ * slide's builds are exhausted (the caller should advance to the next slide).
1831
+ */
1832
+ advance(): boolean;
1833
+ /** True when every click group on the current slide has been revealed. */
1834
+ isComplete(): boolean;
1835
+ /** Sync playback + transitions after a stage (re)render. */
1836
+ syncStage(params: SyncStageParams): void;
1837
+ /** Cancel any running transition and forget all per-slide state. */
1838
+ reset(): void;
1839
+ }
1840
+
1476
1841
  /** Discriminant values of the {@link PptxElement} union (`'text'`, `'chart'`, ...). */
1477
1842
  type PptxElementType = a['type'];
1478
1843
  /**
@@ -1620,79 +1985,137 @@ declare function renderSlideStage(options: SlideStageOptions): HTMLElement;
1620
1985
  */
1621
1986
  declare function createDefaultRegistry(): ElementRendererRegistry;
1622
1987
 
1988
+ interface RenderController {
1989
+ /** Re-render everything (used after a chrome rebuild). */
1990
+ renderAll(): void;
1991
+ /** Re-render the main stage + toolbar counters at the current state. */
1992
+ renderStage(): void;
1993
+ /** Rebuild the thumbnail rail from the current slide list. */
1994
+ renderThumbnails(): void;
1995
+ /** Resolve the requested zoom into a concrete scale factor. */
1996
+ effectiveScale(): number;
1997
+ /**
1998
+ * Presentation-mode animation/transition playback, driven by the stage
1999
+ * rebuild flow. Navigation (`viewer-controls`) consults `advance()` so a
2000
+ * "next" first steps the on-click animation timeline before advancing slides.
2001
+ */
2002
+ readonly presentationPlayback: PresentationPlayback;
2003
+ }
2004
+
1623
2005
  /**
1624
- * A tiny framework-free reactive store: `get` / `set(patch)` / `subscribe`.
2006
+ * Debounced autosave for the vanilla viewer, layered on the shared IndexedDB
2007
+ * recovery store (`pptx-viewer-shared/autosave-store`).
1625
2008
  *
1626
- * This is deliberately minimal (a few dozen lines, no external deps). It only
1627
- * carries the vanilla binding's *view state*; all domain logic (parsing,
1628
- * styles, geometry, text building) stays in `pptx-viewer-core` and
1629
- * `pptx-viewer-shared`.
2009
+ * Semantics mirror the React/Vue bindings: a local edit marks the document
2010
+ * dirty; a debounce timer then re-serializes the deck through
2011
+ * `PptxHandler.save` and persists it under `filePath` via
2012
+ * `saveAutosaveSnapshot` (evicting the oldest record on quota exhaustion). The
2013
+ * recovery blob is a crash-safety net only; it never clears the editor's dirty
2014
+ * flag or stands in for the user's real Save.
2015
+ *
2016
+ * On construction it probes `getAutosaveSnapshot(filePath)` and, when a snapshot
2017
+ * from a previous session exists, offers it back through `onRecovery` so the
2018
+ * host can decide whether to restore it (e.g. via `loadFile`).
1630
2019
  */
1631
- /** Listener invoked after every state change with the next and previous state. */
1632
- type StoreListener<T> = (state: T, previous: T) => void;
1633
- interface Store<T extends object> {
1634
- /** Current state snapshot (do not mutate). */
1635
- get(): T;
1636
- /** Shallow-merge a partial patch into the state and notify subscribers. */
1637
- set(patch: Partial<T>): void;
1638
- /** Subscribe to state changes; returns an unsubscribe function. */
1639
- subscribe(listener: StoreListener<T>): () => void;
1640
- }
2020
+ type AutosaveStatus = 'idle' | 'saving' | 'saved' | 'error';
2021
+
2022
+ /** Per-slide progress callback: `(currentSlideIndex, totalSlides)`. */
2023
+ type ExportProgress = (current: number, total: number) => void;
1641
2024
 
1642
- /** `zoom` is either an explicit scale factor (1 = 100%) or fit-to-viewport. */
1643
- type ZoomLevel = number | 'fit';
1644
2025
  /**
1645
- * The vanilla viewer's reactive view state. Kept intentionally flat and small;
1646
- * everything here is what the DOM render layer consumes.
2026
+ * Animated-GIF export for the vanilla binding. All slides are captured (one
2027
+ * frame per slide) via the injected `rasterizeSlide`, then encoded with the
2028
+ * shared pure-JS GIF89a encoder (`pptx-viewer-shared` `gif-encoder`: median-cut
2029
+ * quantisation + LZW). Frame timing comes from the shared `planGifFrames`
2030
+ * planner and oversized captures are downscaled via `clampGifDimensions`; only
2031
+ * the DOM capture / canvas scaling / Blob download driver lives here.
1647
2032
  */
1648
- interface ViewerState {
1649
- /** Parsed slides (image/media URLs already patched in by the load pipeline). */
1650
- slides: P[];
1651
- /** Slide canvas size in CSS pixels. */
1652
- canvasSize: CanvasSize;
1653
- /** Archive-path to displayable URL map for media + poster frames. */
1654
- mediaDataUrls: Map<string, string>;
1655
- /** Zero-based index of the visible slide. */
1656
- currentSlide: number;
1657
- /** Requested zoom (explicit factor or fit-to-viewport). */
1658
- zoom: ZoomLevel;
1659
- /** True while a load is in flight. */
1660
- loading: boolean;
1661
- /** Error message from the last failed load, or null. */
1662
- error: string | null;
1663
- /** True while presentation (fullscreen) mode is active. */
1664
- presenting: boolean;
1665
- /** True when editing interactions (select/move/resize/...) are enabled. */
1666
- editable: boolean;
1667
- /** Id of the selected element on the current slide, or null. */
1668
- selectedElementId: string | null;
1669
- /** True when the document has unsaved edits (cleared by a save). */
1670
- dirty: boolean;
2033
+ /** Options for the animated-GIF export (all slides, one frame per slide). */
2034
+ interface ExportGifOptions {
1671
2035
  /**
1672
- * True while a pointer gesture (drag/resize/rotate) is in flight. Thumbnail
1673
- * re-renders are deferred until the gesture ends.
2036
+ * Duration each slide is shown, in milliseconds (default 2000). Per-slide
2037
+ * overrides can be supplied via {@link ExportGifOptions.slideTimingsMs}.
1674
2038
  */
1675
- interactionActive: boolean;
2039
+ slideDurationMs?: number;
1676
2040
  /**
1677
- * True when the speaker-notes panel body is expanded. Persists across slide
1678
- * navigation for the life of the viewer instance (in-memory only).
2041
+ * Per-slide duration overrides in milliseconds (index maps to slide index,
2042
+ * e.g. rehearsed timings). Flows through the shared `planGifFrames` plan
2043
+ * into per-frame GIF delays.
1679
2044
  */
1680
- notesExpanded: boolean;
2045
+ slideTimingsMs?: number[];
2046
+ /**
2047
+ * Cap on the longer side of the encoded frames, in pixels (default 1920).
2048
+ * Captured canvases larger than this are downscaled before quantisation,
2049
+ * keeping encode time and file size manageable.
2050
+ */
2051
+ maxDimension?: number;
2052
+ /** Capture-phase progress callback: `(currentSlide, totalSlides)`. */
2053
+ onProgress?: ExportProgress;
2054
+ /** Abort the export early; the capture loop checks this between slides. */
2055
+ signal?: AbortSignal;
1681
2056
  }
1682
2057
 
1683
- interface RenderController {
1684
- /** Re-render everything (used after a chrome rebuild). */
1685
- renderAll(): void;
1686
- /** Re-render the main stage + toolbar counters at the current state. */
1687
- renderStage(): void;
1688
- /** Rebuild the thumbnail rail from the current slide list. */
1689
- renderThumbnails(): void;
1690
- /** Resolve the requested zoom into a concrete scale factor. */
1691
- effectiveScale(): number;
2058
+ /**
2059
+ * Print for the vanilla binding, assembled entirely from the shared print
2060
+ * module (`pptx-viewer-shared` `print-document`): `validatePrintSettings`,
2061
+ * `computeSlideIndices` / `computeColorFilter`, the `build*Html` body markup
2062
+ * builders, and the DOMPurify-hardened `buildPrintHtmlDocument` assembler.
2063
+ * Only the drivers live here: rasterising the selected slides to data URLs and
2064
+ * writing the document into a print window. Vanilla port of Vue's `usePrint`
2065
+ * raster path (slides / notes / handouts / outline).
2066
+ */
2067
+ /**
2068
+ * Open a print window for a complete HTML document and trigger printing.
2069
+ * Returns `false` when the window could not be opened (popup blocker).
2070
+ */
2071
+ type OpenPrintWindow = (htmlDocument: string) => boolean;
2072
+ /**
2073
+ * Options for `print`: any subset of the shared `PrintSettings` (unspecified
2074
+ * fields fall back to `DEFAULT_PRINT_SETTINGS`, i.e. all slides, landscape,
2075
+ * full colour) plus progress/abort and a print-window override.
2076
+ */
2077
+ interface PrintOptions extends Partial<PrintSettings> {
2078
+ /** Rasterisation progress callback: `(currentSlide, totalSlidesToPrint)`. */
2079
+ onProgress?: ExportProgress;
2080
+ /** Abort before the window opens; checked between slide captures. */
2081
+ signal?: AbortSignal;
2082
+ /**
2083
+ * Override how the assembled document is opened (e.g. write it into a
2084
+ * hidden iframe). Popup-blocker caveat: the default opener uses
2085
+ * `window.open`, which browsers typically only allow inside a user
2086
+ * gesture, so call `print()` from a click handler; when the popup is
2087
+ * blocked the returned promise resolves `false`.
2088
+ */
2089
+ openPrintWindow?: OpenPrintWindow;
2090
+ }
2091
+
2092
+ /**
2093
+ * WebM video export for the vanilla binding, driven by the shared `video-plan`
2094
+ * module: `planVideoSegments` (per-slide segment timing + frame counts),
2095
+ * `fpsToFrameIntervalMs` (draw-loop pacing), and `pickSupportedMimeType` over
2096
+ * `WEBM_MIME_CANDIDATES` (MediaRecorder codec selection). Only the browser
2097
+ * driver lives here: capture each slide to a canvas, replay the canvases onto
2098
+ * a recording canvas fed to `captureStream()` + `MediaRecorder`, and download
2099
+ * the resulting Blob. Vanilla port of React's `exportAllSlidesAsVideo`.
2100
+ */
2101
+ /** Options for the WebM video export (all slides). */
2102
+ interface ExportVideoOptions {
2103
+ /** Duration each slide is held, in milliseconds (default 3000). */
2104
+ slideDurationMs?: number;
2105
+ /** Per-slide duration overrides in milliseconds (index maps to slide index). */
2106
+ slideTimingsMs?: number[];
2107
+ /** Recording frame rate in frames per second (default 30). */
2108
+ fps?: number;
2109
+ /** MediaRecorder video bitrate in bits per second (default 5,000,000). */
2110
+ videoBitsPerSecond?: number;
2111
+ /** Capture-phase progress callback: `(currentSlide, totalSlides)`. */
2112
+ onProgress?: ExportProgress;
2113
+ /** Recording-phase progress callback: `(currentSlide, totalSlides)`. */
2114
+ onRecordProgress?: ExportProgress;
2115
+ /** Abort the export early; checked between slides and between frames. */
2116
+ signal?: AbortSignal;
1692
2117
  }
1693
2118
 
1694
- /** Per-slide progress callback: `(currentSlideIndex, totalSlides)`. */
1695
- type ExportProgress = (current: number, total: number) => void;
1696
2119
  /** Options for the multi-slide PDF export (progress + cooperative cancel). */
1697
2120
  interface ExportPdfOptions {
1698
2121
  /** Capture-phase progress callback: `(currentSlide, totalSlides)`. */
@@ -1707,13 +2130,6 @@ interface ExportPdfOptions {
1707
2130
  */
1708
2131
  type PptxViewerSource = ArrayBuffer | Uint8Array | Blob | string;
1709
2132
 
1710
- /**
1711
- * Public API types for the vanilla (zero-framework) PowerPoint viewer.
1712
- *
1713
- * Derived from the Vue binding's `PowerPointViewerProps` / emits, translated
1714
- * to plain options + callback functions (there is no framework event system
1715
- * to emit through).
1716
- */
1717
2133
  /** Callbacks mirroring the Vue component's emits. */
1718
2134
  interface PptxViewerCallbacks {
1719
2135
  /** Fired after a presentation loads successfully. */
@@ -1735,6 +2151,16 @@ interface PptxViewerCallbacks {
1735
2151
  onDirtyChange?: (dirty: boolean) => void;
1736
2152
  /** Fired when the selected element changes (`null` = no selection). */
1737
2153
  onSelectionChange?: (elementId: string | null) => void;
2154
+ /** Fired on every autosave lifecycle transition (`saving`/`saved`/`error`). */
2155
+ onAutosaveStatus?: (status: AutosaveStatus) => void;
2156
+ /**
2157
+ * Offered a recovery snapshot found in the shared IndexedDB store on start
2158
+ * (a previous session's autosave for the same `autosaveFilePath`). The host
2159
+ * decides whether to restore it, e.g. `viewer.loadFile(record.data)`.
2160
+ */
2161
+ onAutosaveRecovery?: (record: AutosaveRecord) => void;
2162
+ /** Fired on every collaboration connection-status transition. */
2163
+ onCollaborationStatus?: (status: ConnectionStatus) => void;
1738
2164
  }
1739
2165
  interface PptxViewerOptions extends PptxViewerCallbacks {
1740
2166
  /**
@@ -1769,6 +2195,16 @@ interface PptxViewerOptions extends PptxViewerCallbacks {
1769
2195
  showToolbar?: boolean;
1770
2196
  /** Show the thumbnail sidebar (default `true`). */
1771
2197
  showThumbnails?: boolean;
2198
+ /**
2199
+ * Build the editing format toolbar row (bold/fill/insert/z-order); default
2200
+ * `true`. The row is only *visible* while editing is enabled.
2201
+ */
2202
+ showFormatToolbar?: boolean;
2203
+ /**
2204
+ * Build the property inspector panel (position/size/fill/line); default
2205
+ * `true`. Only *visible* while editing is enabled.
2206
+ */
2207
+ showInspector?: boolean;
1772
2208
  /**
1773
2209
  * Custom element-renderer registry. Defaults to `createDefaultRegistry()`;
1774
2210
  * pass your own (or mutate the default via `getRegistry()`) to add or
@@ -1784,6 +2220,30 @@ interface PptxViewerOptions extends PptxViewerCallbacks {
1784
2220
  * mirroring the Vue/React/Angular bindings).
1785
2221
  */
1786
2222
  smartArt3D?: boolean;
2223
+ /**
2224
+ * Enable debounced autosave (default `false`): after each local edit the deck
2225
+ * is re-serialized and stashed in the shared IndexedDB recovery store as a
2226
+ * crash-safety net (it never replaces the user's real Save). The toolbar shows
2227
+ * a small status pill; a snapshot from a prior session is offered through
2228
+ * {@link PptxViewerCallbacks.onAutosaveRecovery}.
2229
+ */
2230
+ autosave?: boolean;
2231
+ /** Debounce window (ms) between an edit and the persisted snapshot (default 2000). */
2232
+ autosaveIntervalMs?: number;
2233
+ /** IndexedDB recovery key for autosave (default `'presentation.pptx'`). */
2234
+ autosaveFilePath?: string;
2235
+ /**
2236
+ * Start a real-time collaboration session immediately (Yjs over y-websocket
2237
+ * or serverless y-webrtc). Local edits publish to peers and remote edits
2238
+ * merge in granularly; a `role: 'viewer'` config forces read-only. Start or
2239
+ * stop a session later with {@link PptxViewerInstance.startCollaboration} /
2240
+ * {@link PptxViewerInstance.stopCollaboration}.
2241
+ *
2242
+ * Note: media/OLE/3D/ink binary payloads are not carried over the wire (a
2243
+ * shared codec limitation), and a remote update replaces the whole local
2244
+ * slide array, so a joiner's host-provided media can degrade.
2245
+ */
2246
+ collaboration?: CollaborationConfig;
1787
2247
  }
1788
2248
  /** The viewer handle returned by `createPptxViewer`. */
1789
2249
  interface PptxViewerInstance {
@@ -1843,6 +2303,26 @@ interface PptxViewerInstance {
1843
2303
  * `jspdf` is dynamically imported on first use.
1844
2304
  */
1845
2305
  exportPdf(options?: ExportPdfOptions): Promise<void>;
2306
+ /**
2307
+ * Export every slide as an animated GIF download (one frame per slide,
2308
+ * `slideDurationMs` per frame). Slides are captured off-screen like
2309
+ * `exportSlidePng` and encoded with the shared pure-JS GIF89a encoder.
2310
+ */
2311
+ exportGif(options?: ExportGifOptions): Promise<void>;
2312
+ /**
2313
+ * Export every slide as a WebM video download: each captured slide is held
2314
+ * for its configured duration on a canvas stream recorded by
2315
+ * `MediaRecorder` (codec picked from the shared WebM candidates).
2316
+ */
2317
+ exportVideo(options?: ExportVideoOptions): Promise<void>;
2318
+ /**
2319
+ * Assemble the printable document (slides / notes / handouts / outline)
2320
+ * and open it in a new print window. Resolves `false` when the popup was
2321
+ * blocked: browsers typically only allow `window.open` inside a user
2322
+ * gesture, so call this from a click handler (or pass a custom
2323
+ * `openPrintWindow` that writes into an iframe you own).
2324
+ */
2325
+ print(options?: PrintOptions): Promise<boolean>;
1846
2326
  /** The element-renderer registry in effect (extension point). */
1847
2327
  getRegistry(): ElementRendererRegistry;
1848
2328
  /**
@@ -1851,6 +2331,18 @@ interface PptxViewerInstance {
1851
2331
  * archive access) without extra APIs here.
1852
2332
  */
1853
2333
  getHandler(): PptxHandler | null;
2334
+ /**
2335
+ * Start (or restart) a real-time collaboration session. Resolves once the
2336
+ * transport is created; connection status arrives via
2337
+ * {@link PptxViewerCallbacks.onCollaborationStatus}.
2338
+ */
2339
+ startCollaboration(config: CollaborationConfig): Promise<void>;
2340
+ /** Stop the active collaboration session (no-op when none is running). */
2341
+ stopCollaboration(): void;
2342
+ /** Current collaboration connection status (`'disconnected'` when inactive). */
2343
+ getCollaborationStatus(): ConnectionStatus;
2344
+ /** Force an immediate autosave snapshot (no-op when autosave is disabled). */
2345
+ autosaveNow(): Promise<void>;
1854
2346
  /** Tear down DOM, listeners, Blob URLs, and the core handler. */
1855
2347
  destroy(): void;
1856
2348
  }
@@ -1874,6 +2366,7 @@ interface ChromeHost {
1874
2366
  lifecycle: ChromeLifecycle;
1875
2367
  editor: {
1876
2368
  commitNotes(notes: string): void;
2369
+ getEditActions(): EditActions;
1877
2370
  };
1878
2371
  prev(): void;
1879
2372
  next(): void;
@@ -1890,39 +2383,44 @@ interface ChromeHost {
1890
2383
  exitPresentation(): Promise<void>;
1891
2384
  }
1892
2385
 
1893
- interface EditorController {
1894
- /** (Re)wire listeners + overlay into the current chrome (after mount). */
1895
- attachChrome(): void;
1896
- detachChrome(): void;
1897
- /** Called by the render controller after every stage render. */
1898
- onStageRendered(): void;
1899
- /** True while editing owns the keyboard (selection or inline editing). */
1900
- capturesKeyboard(): boolean;
1901
- /** Drop history/selection/dirty state (new content loaded). */
1902
- reset(): void;
1903
- setEditable(editable: boolean): void;
1904
- undo(): void;
1905
- redo(): void;
1906
- canUndo(): boolean;
1907
- canRedo(): boolean;
1908
- deleteSelected(): void;
1909
- duplicateSelected(): string | null;
1910
- getSelectedElementId(): string | null;
1911
- /** Commit the speaker-notes textarea's plain text onto the current slide. */
1912
- commitNotes(notes: string): void;
1913
- save(): Promise<Uint8Array>;
1914
- downloadPptx(fileName?: string): Promise<void>;
2386
+ /** The export slice of the public viewer API (see `PptxViewerInstance`). */
2387
+ interface ViewerExportApi {
2388
+ /** Export a single slide as a PNG download. Defaults to the current slide. */
2389
+ exportSlidePng(index?: number): Promise<void>;
2390
+ /** Export every slide as a multi-page PDF download (one slide per page). */
2391
+ exportPdf(options?: ExportPdfOptions): Promise<void>;
2392
+ /** Export every slide as an animated GIF download (one frame per slide). */
2393
+ exportGif(options?: ExportGifOptions): Promise<void>;
2394
+ /** Export every slide as a WebM video download (MediaRecorder). */
2395
+ exportVideo(options?: ExportVideoOptions): Promise<void>;
2396
+ /** Open the assembled print document in a print window (`false` = blocked). */
2397
+ print(options?: PrintOptions): Promise<boolean>;
2398
+ }
2399
+ interface ExportLifecycle extends ViewerExportApi {
2400
+ /** Remove the off-screen capture stage from the DOM. */
1915
2401
  destroy(): void;
1916
2402
  }
2403
+ /**
2404
+ * Base class implementing the export slice of `PptxViewerInstance` by
2405
+ * delegating to the instance's {@link ExportLifecycle}. `PptxViewer` extends
2406
+ * this, so new export formats are wired here (and in the export controller)
2407
+ * without growing the size-capped `PptxViewer.ts`.
2408
+ */
2409
+ declare abstract class ViewerExportHost implements ViewerExportApi {
2410
+ protected abstract readonly exporter: ExportLifecycle;
2411
+ exportSlidePng(index?: number): Promise<void>;
2412
+ exportPdf(options?: ExportPdfOptions): Promise<void>;
2413
+ exportGif(options?: ExportGifOptions): Promise<void>;
2414
+ exportVideo(options?: ExportVideoOptions): Promise<void>;
2415
+ print(options?: PrintOptions): Promise<boolean>;
2416
+ }
1917
2417
 
1918
2418
  /**
1919
- * The zero-framework PowerPoint viewer. Construct via {@link createPptxViewer}
1920
- * (or `new PptxViewer(container, options)`): builds its chrome inside
1921
- * `container`, loads `options.source` when given, and re-renders through a
1922
- * tiny reactive store. All parsing lives in `pptx-viewer-core`; all pure
1923
- * render logic in `pptx-viewer-shared`.
2419
+ * The zero-framework PowerPoint viewer. Construct via {@link createPptxViewer}:
2420
+ * builds chrome inside `container`, loads `options.source` when given, and
2421
+ * re-renders through a tiny reactive store.
1924
2422
  */
1925
- declare class PptxViewer implements PptxViewerInstance, ChromeHost {
2423
+ declare class PptxViewer extends ViewerExportHost implements PptxViewerInstance, ChromeHost {
1926
2424
  readonly container: HTMLElement;
1927
2425
  readonly doc: Document;
1928
2426
  readonly options: PptxViewerOptions;
@@ -1931,9 +2429,11 @@ declare class PptxViewer implements PptxViewerInstance, ChromeHost {
1931
2429
  t: Translator;
1932
2430
  lifecycle: ChromeLifecycle;
1933
2431
  editor: EditorController;
2432
+ protected readonly exporter: ExportLifecycle;
1934
2433
  private readonly loading;
1935
- private readonly exportLifecycle;
1936
2434
  private readonly registry;
2435
+ private readonly sessions;
2436
+ private readonly controls;
1937
2437
  private destroyed;
1938
2438
  constructor(container: HTMLElement, options?: PptxViewerOptions);
1939
2439
  loadFile(file: Blob | ArrayBuffer | Uint8Array): Promise<void>;
@@ -1960,30 +2460,22 @@ declare class PptxViewer implements PptxViewerInstance, ChromeHost {
1960
2460
  save(): Promise<Uint8Array>;
1961
2461
  downloadPptx(fileName?: string): Promise<void>;
1962
2462
  deleteSelected(): void;
1963
- exportSlidePng(index?: number): Promise<void>;
1964
- exportPdf(options?: ExportPdfOptions): Promise<void>;
1965
2463
  getSelectedElementId(): string | null;
1966
2464
  enterPresentation(): Promise<void>;
1967
2465
  exitPresentation(): Promise<void>;
1968
2466
  getRegistry(): ElementRendererRegistry;
1969
2467
  getHandler(): PptxHandler | null;
2468
+ startCollaboration(config: CollaborationConfig): Promise<void>;
2469
+ stopCollaboration(): void;
2470
+ getCollaborationStatus(): ConnectionStatus;
2471
+ autosaveNow(): Promise<void>;
1970
2472
  destroy(): void;
1971
2473
  private remountChrome;
1972
2474
  }
1973
- /**
1974
- * Create a PowerPoint viewer inside `container`.
1975
- *
1976
- * ```ts
1977
- * import { createPptxViewer } from 'pptx-vanilla-viewer';
1978
- * const viewer = createPptxViewer(document.querySelector('#host')!, {
1979
- * source: '/deck.pptx',
1980
- * onSlideChange: (i) => console.log('slide', i + 1),
1981
- * });
1982
- * ```
1983
- */
2475
+ /** Create a PowerPoint viewer inside `container` (see {@link PptxViewerOptions}). */
1984
2476
  declare function createPptxViewer(container: HTMLElement, options?: PptxViewerOptions): PptxViewerInstance;
1985
2477
 
1986
2478
  /** The complete viewer stylesheet text (for hosts that self-manage CSS). */
1987
2479
  declare function getViewerCss(): string;
1988
2480
 
1989
- export { type CssStyleMap, type ElementRenderContext, type ElementRenderer, type ElementRendererRegistry, type ExportPdfOptions, type ExportProgress, type PptxElementType, PptxHandler, PptxViewer, type PptxViewerCallbacks, type PptxViewerInstance, type PptxViewerOptions, type PptxViewerSource, type SlideStageOptions, type TranslationMessages, type Translator, type ViewerState, type ZoomLevel, applyStyleMap, createDefaultRegistry, createEl, createElementRendererRegistry, createPptxViewer, createSvgEl, createTranslator, getViewerCss, renderSlideStage };
2481
+ export { type AutosaveRecord, type AutosaveStatus, type CollaborationConfig, type CollaborationRole, type CollaborationTransport, type ConnectionStatus, type CssStyleMap, type ElementRenderContext, type ElementRenderer, type ElementRendererRegistry, type ExportGifOptions, type ExportPdfOptions, type ExportProgress, type ExportVideoOptions, type OpenPrintWindow, type PptxElementType, PptxHandler, PptxViewer, type PptxViewerCallbacks, type PptxViewerInstance, type PptxViewerOptions, type PptxViewerSource, type PrintOptions, type SlideStageOptions, type TranslationMessages, type Translator, type ViewerState, type ZoomLevel, applyStyleMap, createDefaultRegistry, createEl, createElementRendererRegistry, createPptxViewer, createSvgEl, createTranslator, getViewerCss, renderSlideStage };