pptx-angular-viewer 1.1.58 → 1.1.60

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.
@@ -22215,6 +22215,14 @@ function getDraggedShapeAdjustmentValue(state, deltaX) {
22215
22215
  */
22216
22216
  /** Maximum time (ms) to wait for an initial WebSocket connection before giving up. */
22217
22217
  const CONNECTION_TIMEOUT_MS = 30_000;
22218
+ /**
22219
+ * Grace period (ms) before the first local doc write when the provider has not
22220
+ * signalled initial sync. Websocket providers emit 'synced' reliably; webrtc
22221
+ * only syncs once a peer is present, so a lone (fresh-room) peer seeds the doc
22222
+ * after this delay instead. Gating the first write prevents a late joiner's
22223
+ * bootstrap deck from merging into a room whose real content has not arrived.
22224
+ */
22225
+ const INITIAL_SYNC_GRACE_MS = 3_000;
22218
22226
  /** Heartbeat interval (ms): re-publish presence so peers don't time us out. */
22219
22227
  const PRESENCE_HEARTBEAT_MS = 10_000;
22220
22228
  /** Minimum interval (ms) between outgoing presence broadcasts (rate limiting). */
@@ -22501,117 +22509,22 @@ function asSelectionIds(value) {
22501
22509
  }
22502
22510
 
22503
22511
  /**
22504
- * collaboration-sync.ts: Framework-agnostic CRDT sync utilities for the
22505
- * pptx-viewer collaboration stack (Yjs backend).
22512
+ * collaboration-text-codec.ts: TextSegment[] <-> Y.Text delta codec used by the
22513
+ * collaboration sync layer. Split out of collaboration-sync.ts to keep both
22514
+ * modules focused.
22506
22515
  *
22507
22516
  * Exports:
22508
- * - Structural Yjs interfaces (no hard yjs import - bindings pass live instances)
22509
- * - YjsFactories: factory interface bindings implement using `new Y.Map()` etc.
22510
- * - encodeTextBody / decodeTextBody: TextSegment[] <-> YTextLike delta
22511
- * - writeElementToYMap / readElementFromYMap: PptxElement <-> YMapLike
22512
- * - writeSlideToYMap / readSlideFromYMap: PptxSlide <-> YMapLike
22513
- * - writeSlidesToYDoc / readSlidesFromYDoc: PptxSlide[] <-> Y.Doc
22514
- * - observeYDocSlides: register a change listener on the pptx:slides array
22515
- *
22516
- * Y.Doc schema (matches pptx-codec.ts in packages/tools):
22517
- * pptx:slides - Y.Array of slide Y.Maps
22518
- * Each slide Y.Map has scalar keys + `_`-prefixed JSON blobs + `elements`
22519
- * Each element Y.Map has scalar keys + `_`-prefixed JSON blobs + `textBody`
22520
- * textBody is a Y.Text with one delta-op per TextSegment
22517
+ * - DeltaOp / YTextLike: structural Yjs text interfaces (no yjs import)
22518
+ * - encodeTextBody: write TextSegment[] into a live YTextLike
22519
+ * - encodeSegmentsToDelta: pure simulation of the delta Y.Text would produce
22520
+ * - decodeDelta / decodeTextBody: delta -> TextSegment[]
22521
+ * - isYTextLike: runtime guard
22521
22522
  */
22522
- // ---------------------------------------------------------------------------
22523
- // Y.Doc schema constants (mirror of pptx-codec.ts)
22524
- // ---------------------------------------------------------------------------
22525
- const YDOC_SLIDES_KEY = 'pptx:slides';
22526
- const YDOC_META_KEY = 'pptx:meta';
22527
- const SCALAR_ELEMENT_KEYS = new Set([
22528
- 'id',
22529
- 'type',
22530
- 'x',
22531
- 'y',
22532
- 'width',
22533
- 'height',
22534
- 'rotation',
22535
- 'flipHorizontal',
22536
- 'flipVertical',
22537
- 'hidden',
22538
- 'opacity',
22539
- 'text',
22540
- 'name',
22541
- 'altText',
22542
- 'shapeType',
22543
- 'placeholder',
22544
- 'imagePath',
22545
- 'imageData',
22546
- 'svgContent',
22547
- 'inkSvg',
22548
- 'sourceSlideId',
22549
- 'mediaType',
22550
- 'mediaPath',
22551
- 'linkedTxbxId',
22552
- 'linkedTxbxSeq',
22553
- 'promptText',
22554
- ]);
22555
- const COMPLEX_ELEMENT_FIELDS = {
22556
- textStyle: '_ts',
22557
- shapeStyle: '_ss',
22558
- shapeAdjustments: '_sa',
22559
- adjustmentHandles: '_ah',
22560
- tableData: '_td',
22561
- chartData: '_cd',
22562
- smartArtData: '_smad',
22563
- connectionStart: '_cs',
22564
- connectionEnd: '_ce',
22565
- animations: '_an',
22566
- nativeAnimations: '_na',
22567
- children: '_ch',
22568
- paragraphIndents: '_pi',
22569
- rawXml: '_rx',
22570
- actionClick: '_ac',
22571
- actionHover: '_av',
22572
- locks: '_lk',
22573
- imageEffects: '_ie',
22574
- cropShape: '_cr',
22575
- mediaBookmarks: '_mb',
22576
- captionTracks: '_ct',
22577
- };
22578
- const REV_COMPLEX_ELEMENT = Object.fromEntries(Object.entries(COMPLEX_ELEMENT_FIELDS).map(([k, v]) => [v, k]));
22579
- const SCALAR_SLIDE_KEYS = new Set([
22580
- 'id',
22581
- 'rId',
22582
- 'sourceSlideId',
22583
- 'layoutPath',
22584
- 'layoutName',
22585
- 'slideNumber',
22586
- 'hidden',
22587
- 'sectionName',
22588
- 'sectionId',
22589
- 'backgroundColor',
22590
- 'backgroundImage',
22591
- 'backgroundGradient',
22592
- 'notes',
22593
- 'backgroundShowAnimation',
22594
- 'showMasterShapes',
22595
- 'isDirty',
22596
- ]);
22597
- const COMPLEX_SLIDE_FIELDS = {
22598
- transition: '_tr',
22599
- animations: '_an',
22600
- nativeAnimations: '_na',
22601
- rawTiming: '_rt',
22602
- notesSegments: '_ns',
22603
- comments: '_cm',
22604
- warnings: '_wa',
22605
- rawXml: '_rx',
22606
- clrMapOverride: '_cm2',
22607
- guides: '_gu',
22608
- customerData: '_cu',
22609
- activeXControls: '_ax',
22610
- };
22611
- const REV_COMPLEX_SLIDE = Object.fromEntries(Object.entries(COMPLEX_SLIDE_FIELDS).map(([k, v]) => [v, k]));
22612
- // ---------------------------------------------------------------------------
22613
- // Text-body encode/decode (TextSegment[] <-> YTextLike delta)
22614
- // ---------------------------------------------------------------------------
22523
+ function isYTextLike(value) {
22524
+ return (typeof value === 'object' &&
22525
+ value !== null &&
22526
+ typeof value.toDelta === 'function');
22527
+ }
22615
22528
  function buildSegmentAttrs(seg) {
22616
22529
  const a = {};
22617
22530
  const style = seg.style;
@@ -22668,29 +22581,56 @@ function buildSegmentAttrs(seg) {
22668
22581
  }
22669
22582
  return a;
22670
22583
  }
22584
+ /** Resolve the literal text a segment contributes to the Y.Text document. */
22585
+ function segmentInsertText(seg) {
22586
+ if (seg.isParagraphBreak === true || seg.isLineBreak === true) {
22587
+ return '\n';
22588
+ }
22589
+ if (typeof seg.text === 'string' && seg.text.length > 0) {
22590
+ return seg.text;
22591
+ }
22592
+ // Empty non-break run: zero-width space holds the attributes.
22593
+ return '​';
22594
+ }
22671
22595
  function encodeTextBody(segments, ytext) {
22672
22596
  let offset = 0;
22673
22597
  for (const raw of segments) {
22674
22598
  const seg = raw;
22675
22599
  const attrs = buildSegmentAttrs(seg);
22676
- const hasAttrs = Object.keys(attrs).length > 0;
22677
- if (seg.isParagraphBreak === true || seg.isLineBreak === true) {
22678
- ytext.insert(offset, '\n', hasAttrs ? attrs : undefined);
22679
- offset += 1;
22680
- }
22681
- else if (typeof seg.text === 'string' && seg.text.length > 0) {
22682
- ytext.insert(offset, seg.text, hasAttrs ? attrs : undefined);
22683
- offset += seg.text.length;
22600
+ const text = segmentInsertText(seg);
22601
+ // Always pass an explicit attrs object: Yjs makes attribute-less inserts
22602
+ // inherit the preceding character's formatting, which bled styles and
22603
+ // paragraph-break markers into unstyled runs.
22604
+ ytext.insert(offset, text, attrs);
22605
+ offset += text.length;
22606
+ }
22607
+ }
22608
+ /**
22609
+ * Pure simulation of the delta a Y.Text produces after `encodeTextBody`:
22610
+ * adjacent runs with identical attribute maps merge into one op, matching
22611
+ * Yjs run-merging. Used to compare desired segments against a live Y.Text
22612
+ * without instantiating one.
22613
+ */
22614
+ function encodeSegmentsToDelta(segments) {
22615
+ const ops = [];
22616
+ for (const raw of segments) {
22617
+ const seg = raw;
22618
+ const attrs = buildSegmentAttrs(seg);
22619
+ const attributes = Object.keys(attrs).length > 0 ? attrs : undefined;
22620
+ const text = segmentInsertText(seg);
22621
+ const prev = ops[ops.length - 1];
22622
+ if (prev &&
22623
+ typeof prev.insert === 'string' &&
22624
+ JSON.stringify(prev.attributes ?? null) === JSON.stringify(attributes ?? null)) {
22625
+ prev.insert += text;
22684
22626
  }
22685
22627
  else {
22686
- // Empty non-break run: use zero-width space to hold attributes
22687
- ytext.insert(offset, '​', hasAttrs ? attrs : undefined);
22688
- offset += 1;
22628
+ ops.push(attributes ? { insert: text, attributes } : { insert: text });
22689
22629
  }
22690
22630
  }
22631
+ return ops;
22691
22632
  }
22692
- function decodeTextBody(ytext) {
22693
- const delta = ytext.toDelta();
22633
+ function decodeDelta(delta) {
22694
22634
  const segments = [];
22695
22635
  for (const op of delta) {
22696
22636
  if (typeof op.insert !== 'string' || op.insert === '') {
@@ -22791,6 +22731,128 @@ function decodeTextBody(ytext) {
22791
22731
  }
22792
22732
  return segments;
22793
22733
  }
22734
+ function decodeTextBody(ytext) {
22735
+ return decodeDelta(ytext.toDelta());
22736
+ }
22737
+
22738
+ /**
22739
+ * collaboration-sync.ts: Framework-agnostic CRDT sync utilities for the
22740
+ * pptx-viewer collaboration stack (Yjs backend).
22741
+ *
22742
+ * Exports:
22743
+ * - Structural Yjs interfaces (no hard yjs import - bindings pass live instances)
22744
+ * - YjsFactories: factory interface bindings implement using `new Y.Map()` etc.
22745
+ * - writeElementToYMap / readElementFromYMap: PptxElement <-> YMapLike
22746
+ * - writeSlideToYMap / readSlideFromYMap: PptxSlide <-> YMapLike
22747
+ * - writeSlidesToYDoc / readSlidesFromYDoc: PptxSlide[] <-> Y.Doc
22748
+ * - observeYDocSlides: register a change listener on the pptx:slides array
22749
+ * - re-exports of the text codec (collaboration-text-codec.ts)
22750
+ *
22751
+ * Y.Doc schema:
22752
+ * pptx:slides - Y.Array of slide Y.Maps
22753
+ * Each slide Y.Map has scalar keys + `_`-prefixed JSON blobs + `elements`
22754
+ * Each element Y.Map has scalar keys + `_`-prefixed JSON blobs + `textBody`
22755
+ * textBody is a Y.Text with one delta-op per TextSegment
22756
+ *
22757
+ * NOTE: packages/tools' pptx-codec.ts implements a similar schema but with
22758
+ * different complex-field key prefixes (e.g. `_textStyle` vs `_ts` here); the
22759
+ * two doc layouts are NOT interchangeable on the same Y.Doc.
22760
+ *
22761
+ * Prefer `reconcileSlidesInYDoc` (collaboration-reconcile.ts) over
22762
+ * `writeSlidesToYDoc` for live editing: it updates only what changed instead
22763
+ * of replacing the whole slides array, so concurrent edits merge per
22764
+ * slide/element/field rather than colliding at document granularity.
22765
+ */
22766
+ // ---------------------------------------------------------------------------
22767
+ // Y.Doc schema constants
22768
+ // ---------------------------------------------------------------------------
22769
+ const YDOC_SLIDES_KEY = 'pptx:slides';
22770
+ const YDOC_META_KEY = 'pptx:meta';
22771
+ const SCALAR_ELEMENT_KEYS = new Set([
22772
+ 'id',
22773
+ 'type',
22774
+ 'x',
22775
+ 'y',
22776
+ 'width',
22777
+ 'height',
22778
+ 'rotation',
22779
+ 'flipHorizontal',
22780
+ 'flipVertical',
22781
+ 'hidden',
22782
+ 'opacity',
22783
+ 'text',
22784
+ 'name',
22785
+ 'altText',
22786
+ 'shapeType',
22787
+ 'placeholder',
22788
+ 'imagePath',
22789
+ 'imageData',
22790
+ 'svgContent',
22791
+ 'inkSvg',
22792
+ 'sourceSlideId',
22793
+ 'mediaType',
22794
+ 'mediaPath',
22795
+ 'linkedTxbxId',
22796
+ 'linkedTxbxSeq',
22797
+ 'promptText',
22798
+ ]);
22799
+ const COMPLEX_ELEMENT_FIELDS = {
22800
+ textStyle: '_ts',
22801
+ shapeStyle: '_ss',
22802
+ shapeAdjustments: '_sa',
22803
+ adjustmentHandles: '_ah',
22804
+ tableData: '_td',
22805
+ chartData: '_cd',
22806
+ smartArtData: '_smad',
22807
+ connectionStart: '_cs',
22808
+ connectionEnd: '_ce',
22809
+ animations: '_an',
22810
+ nativeAnimations: '_na',
22811
+ children: '_ch',
22812
+ paragraphIndents: '_pi',
22813
+ rawXml: '_rx',
22814
+ actionClick: '_ac',
22815
+ actionHover: '_av',
22816
+ locks: '_lk',
22817
+ imageEffects: '_ie',
22818
+ cropShape: '_cr',
22819
+ mediaBookmarks: '_mb',
22820
+ captionTracks: '_ct',
22821
+ };
22822
+ const REV_COMPLEX_ELEMENT = Object.fromEntries(Object.entries(COMPLEX_ELEMENT_FIELDS).map(([k, v]) => [v, k]));
22823
+ const SCALAR_SLIDE_KEYS = new Set([
22824
+ 'id',
22825
+ 'rId',
22826
+ 'sourceSlideId',
22827
+ 'layoutPath',
22828
+ 'layoutName',
22829
+ 'slideNumber',
22830
+ 'hidden',
22831
+ 'sectionName',
22832
+ 'sectionId',
22833
+ 'backgroundColor',
22834
+ 'backgroundImage',
22835
+ 'backgroundGradient',
22836
+ 'notes',
22837
+ 'backgroundShowAnimation',
22838
+ 'showMasterShapes',
22839
+ 'isDirty',
22840
+ ]);
22841
+ const COMPLEX_SLIDE_FIELDS = {
22842
+ transition: '_tr',
22843
+ animations: '_an',
22844
+ nativeAnimations: '_na',
22845
+ rawTiming: '_rt',
22846
+ notesSegments: '_ns',
22847
+ comments: '_cm',
22848
+ warnings: '_wa',
22849
+ rawXml: '_rx',
22850
+ clrMapOverride: '_cm2',
22851
+ guides: '_gu',
22852
+ customerData: '_cu',
22853
+ activeXControls: '_ax',
22854
+ };
22855
+ const REV_COMPLEX_SLIDE = Object.fromEntries(Object.entries(COMPLEX_SLIDE_FIELDS).map(([k, v]) => [v, k]));
22794
22856
  // ---------------------------------------------------------------------------
22795
22857
  // Element serialization
22796
22858
  // ---------------------------------------------------------------------------
@@ -22815,11 +22877,6 @@ function writeElementToYMap(element, ymap, factories) {
22815
22877
  }
22816
22878
  }
22817
22879
  }
22818
- function isYTextLike(value) {
22819
- return (typeof value === 'object' &&
22820
- value !== null &&
22821
- typeof value.toDelta === 'function');
22822
- }
22823
22880
  function readElementFromYMap(ymap) {
22824
22881
  const element = {};
22825
22882
  ymap.forEach((value, key) => {
@@ -22897,6 +22954,11 @@ function readSlideFromYMap(ymap) {
22897
22954
  // ---------------------------------------------------------------------------
22898
22955
  // Y.Doc-level helpers
22899
22956
  // ---------------------------------------------------------------------------
22957
+ /**
22958
+ * Replace the full slides array in the Y.Doc. Coarse: prefer
22959
+ * `reconcileSlidesInYDoc` for live editing; this remains suitable for
22960
+ * one-shot seeding of an empty document.
22961
+ */
22900
22962
  function writeSlidesToYDoc(slides, ydoc, factories, origin) {
22901
22963
  ydoc.transact(() => {
22902
22964
  const arr = ydoc.getArray(YDOC_SLIDES_KEY);
@@ -22918,12 +22980,260 @@ function readSlidesFromYDoc(ydoc) {
22918
22980
  }
22919
22981
  return slides;
22920
22982
  }
22983
+ /**
22984
+ * Observe (deeply) the pptx:slides array. The handler receives the Yjs
22985
+ * events plus the transaction, so callers can skip their own writes by
22986
+ * checking `transaction.origin` (see LOCAL_SYNC_ORIGIN in
22987
+ * collaboration-reconcile.ts).
22988
+ */
22921
22989
  function observeYDocSlides(ydoc, onChange) {
22922
22990
  const arr = ydoc.getArray(YDOC_SLIDES_KEY);
22923
22991
  arr.observeDeep(onChange);
22924
22992
  return () => arr.unobserveDeep(onChange);
22925
22993
  }
22926
22994
 
22995
+ /**
22996
+ * collaboration-reconcile.ts: Granular Y.Doc reconciliation for collaborative
22997
+ * editing.
22998
+ *
22999
+ * `writeSlidesToYDoc` replaces the entire pptx:slides array on every write,
23000
+ * which makes concurrent edits collide at document granularity (last writer
23001
+ * wins for the whole deck). `reconcileSlidesInYDoc` instead diffs the desired
23002
+ * slide state against the live Y.Doc and only mutates what changed:
23003
+ *
23004
+ * - slides and elements are matched by `id`; unchanged ones keep their Y.Map
23005
+ * instance so concurrent field edits merge via Yjs
23006
+ * - scalar / complex fields are compared and only set when different
23007
+ * - textBody is only replaced when its canonical decoded form differs
23008
+ * - removed items are deleted, new ones inserted at their position; moves
23009
+ * are delete+reinsert (Yjs has no move primitive)
23010
+ *
23011
+ * All mutations run in a single transaction tagged with LOCAL_SYNC_ORIGIN (or
23012
+ * a caller-supplied origin) so observers can ignore their own writes.
23013
+ */
23014
+ /** Transaction origin used for local reconcile writes. */
23015
+ const LOCAL_SYNC_ORIGIN = 'pptx-viewer:local-sync';
23016
+ function jsonEqual(a, b) {
23017
+ return a === b || JSON.stringify(a) === JSON.stringify(b);
23018
+ }
23019
+ function reconcileScalars(ymap, rec, keys) {
23020
+ for (const key of keys) {
23021
+ const next = rec[key];
23022
+ const current = ymap.get(key);
23023
+ if (next === undefined) {
23024
+ if (current !== undefined) {
23025
+ ymap.delete(key);
23026
+ }
23027
+ }
23028
+ else if (!jsonEqual(current, next)) {
23029
+ ymap.set(key, next);
23030
+ }
23031
+ }
23032
+ }
23033
+ function reconcileComplexFields(ymap, rec, fields) {
23034
+ for (const [original, prefixed] of Object.entries(fields)) {
23035
+ const next = rec[original] === undefined ? undefined : JSON.stringify(rec[original]);
23036
+ const current = ymap.get(prefixed);
23037
+ if (next === undefined) {
23038
+ if (current !== undefined) {
23039
+ ymap.delete(prefixed);
23040
+ }
23041
+ }
23042
+ else if (current !== next) {
23043
+ ymap.set(prefixed, next);
23044
+ }
23045
+ }
23046
+ }
23047
+ function reconcileTextBody(ymap, rec, factories) {
23048
+ const segments = rec.textSegments;
23049
+ const current = ymap.get('textBody');
23050
+ if (!Array.isArray(segments)) {
23051
+ if (current !== undefined) {
23052
+ ymap.delete('textBody');
23053
+ }
23054
+ return;
23055
+ }
23056
+ const desired = decodeDelta(encodeSegmentsToDelta(segments));
23057
+ const existing = isYTextLike(current) ? decodeDelta(current.toDelta()) : undefined;
23058
+ if (existing !== undefined && jsonEqual(existing, desired)) {
23059
+ return;
23060
+ }
23061
+ const ytext = factories.createText();
23062
+ encodeTextBody(segments, ytext);
23063
+ ymap.set('textBody', ytext);
23064
+ }
23065
+ function reconcileElementYMap(ymap, element, factories) {
23066
+ const rec = element;
23067
+ reconcileScalars(ymap, rec, SCALAR_ELEMENT_KEYS);
23068
+ reconcileComplexFields(ymap, rec, COMPLEX_ELEMENT_FIELDS);
23069
+ reconcileTextBody(ymap, rec, factories);
23070
+ }
23071
+ function mapIdAt(arr, index) {
23072
+ const entry = arr.get(index);
23073
+ if (!entry || typeof entry.get !== 'function') {
23074
+ return undefined;
23075
+ }
23076
+ const id = entry.get('id');
23077
+ return typeof id === 'string' ? id : undefined;
23078
+ }
23079
+ /**
23080
+ * Reconcile a Y.Array of Y.Maps against a desired item list, matching by id.
23081
+ * Items without a string id fall back to positional matching.
23082
+ */
23083
+ function reconcileYArrayById(arr, items, adapter) {
23084
+ const desiredIds = new Set();
23085
+ for (const item of items) {
23086
+ const id = adapter.idOf(item);
23087
+ if (typeof id === 'string') {
23088
+ desiredIds.add(id);
23089
+ }
23090
+ }
23091
+ // Pass 1: delete maps whose id is gone (or duplicated); keep the last dup.
23092
+ const seen = new Set();
23093
+ for (let i = arr.length - 1; i >= 0; i--) {
23094
+ const id = mapIdAt(arr, i);
23095
+ if (id === undefined) {
23096
+ continue;
23097
+ }
23098
+ if (!desiredIds.has(id) || seen.has(id)) {
23099
+ arr.delete(i, 1);
23100
+ }
23101
+ else {
23102
+ seen.add(id);
23103
+ }
23104
+ }
23105
+ // Pass 2: walk desired order; update in place, move, or insert.
23106
+ for (let pos = 0; pos < items.length; pos++) {
23107
+ const item = items[pos];
23108
+ const id = adapter.idOf(item);
23109
+ const idAtPos = pos < arr.length ? mapIdAt(arr, pos) : undefined;
23110
+ if (id === undefined) {
23111
+ // Positional fallback for id-less items.
23112
+ if (pos < arr.length && idAtPos === undefined) {
23113
+ adapter.update(arr.get(pos), item);
23114
+ }
23115
+ else {
23116
+ arr.insert(pos, [adapter.create(item)]);
23117
+ }
23118
+ continue;
23119
+ }
23120
+ if (idAtPos === id) {
23121
+ adapter.update(arr.get(pos), item);
23122
+ continue;
23123
+ }
23124
+ let foundAt = -1;
23125
+ for (let j = pos + 1; j < arr.length; j++) {
23126
+ if (mapIdAt(arr, j) === id) {
23127
+ foundAt = j;
23128
+ break;
23129
+ }
23130
+ }
23131
+ if (foundAt >= 0) {
23132
+ // Move: Yjs cannot re-insert an integrated type, so rebuild at pos.
23133
+ arr.delete(foundAt, 1);
23134
+ }
23135
+ arr.insert(pos, [adapter.create(item)]);
23136
+ }
23137
+ // Pass 3: trim trailing leftovers.
23138
+ if (arr.length > items.length) {
23139
+ arr.delete(items.length, arr.length - items.length);
23140
+ }
23141
+ }
23142
+ const isYArrayLike = (value) => typeof value === 'object' &&
23143
+ value !== null &&
23144
+ typeof value.insert === 'function' &&
23145
+ typeof value.toArray === 'function';
23146
+ function reconcileSlideYMap(ymap, slide, factories) {
23147
+ const rec = slide;
23148
+ reconcileScalars(ymap, rec, SCALAR_SLIDE_KEYS);
23149
+ reconcileComplexFields(ymap, rec, COMPLEX_SLIDE_FIELDS);
23150
+ let elements = ymap.get('elements');
23151
+ if (!isYArrayLike(elements)) {
23152
+ elements = factories.createArray();
23153
+ ymap.set('elements', elements);
23154
+ }
23155
+ reconcileYArrayById(elements, slide.elements, {
23156
+ idOf: (el) => (typeof el.id === 'string' ? el.id : undefined),
23157
+ create: (el) => {
23158
+ const map = factories.createMap();
23159
+ writeElementToYMap(el, map, factories);
23160
+ return map;
23161
+ },
23162
+ update: (map, el) => reconcileElementYMap(map, el, factories),
23163
+ });
23164
+ }
23165
+ /**
23166
+ * Granular local -> Y.Doc sync: mutate only what changed, inside one
23167
+ * transaction tagged with `origin` (default LOCAL_SYNC_ORIGIN) so the
23168
+ * caller's own observer can skip the resulting events.
23169
+ */
23170
+ function reconcileSlidesInYDoc(slides, ydoc, factories, origin = LOCAL_SYNC_ORIGIN) {
23171
+ ydoc.transact(() => {
23172
+ const arr = ydoc.getArray(YDOC_SLIDES_KEY);
23173
+ reconcileYArrayById(arr, slides, {
23174
+ idOf: (slide) => (typeof slide.id === 'string' ? slide.id : undefined),
23175
+ create: (slide) => {
23176
+ const map = factories.createMap();
23177
+ writeSlideToYMap(slide, map, factories);
23178
+ return map;
23179
+ },
23180
+ update: (map, slide) => reconcileSlideYMap(map, slide, factories),
23181
+ });
23182
+ }, origin);
23183
+ }
23184
+
23185
+ /**
23186
+ * collaboration-sync-gate.ts: first-write gate for collaborative sessions.
23187
+ *
23188
+ * Until a provider confirms its initial document sync, local state must not
23189
+ * be written into the shared doc: a late joiner bootstrapped with a
23190
+ * placeholder deck would otherwise merge that placeholder into the room's
23191
+ * real content. Websocket providers emit a reliable 'synced' event; y-webrtc
23192
+ * only syncs when a peer is present, so a lone fresh-room peer opens the gate
23193
+ * after {@link INITIAL_SYNC_GRACE_MS} instead and seeds the empty doc.
23194
+ *
23195
+ * Usage per binding: create the gate with an `onOpen` callback that performs
23196
+ * the deferred first write, call `arm()` when the provider is created (and on
23197
+ * (re)connect), `open()` from the provider's sync event, gate local writes on
23198
+ * `isOpen()`, and `reset()` on session teardown.
23199
+ */
23200
+ /**
23201
+ * Create a first-write gate. `onOpen` fires exactly once per session (until
23202
+ * `reset()`), from either the provider sync signal or the grace timer.
23203
+ */
23204
+ function createSyncGate(onOpen, graceMs = INITIAL_SYNC_GRACE_MS) {
23205
+ let opened = false;
23206
+ let timer = null;
23207
+ const clear = () => {
23208
+ if (timer !== null) {
23209
+ clearTimeout(timer);
23210
+ timer = null;
23211
+ }
23212
+ };
23213
+ const open = () => {
23214
+ clear();
23215
+ if (opened) {
23216
+ return;
23217
+ }
23218
+ opened = true;
23219
+ onOpen();
23220
+ };
23221
+ return {
23222
+ isOpen: () => opened,
23223
+ arm: () => {
23224
+ clear();
23225
+ if (!opened) {
23226
+ timer = setTimeout(open, graceMs);
23227
+ }
23228
+ },
23229
+ open,
23230
+ reset: () => {
23231
+ clear();
23232
+ opened = false;
23233
+ },
23234
+ };
23235
+ }
23236
+
22927
23237
  // ---------------------------------------------------------------------------
22928
23238
  // Tuning constants (px tolerances for position / size changes)
22929
23239
  // ---------------------------------------------------------------------------
@@ -28777,6 +29087,15 @@ function formatRelativeTime(ts) {
28777
29087
  */
28778
29088
  /** Default y-websocket server URL used when no default is supplied. */
28779
29089
  const DEFAULT_BROADCAST_SERVER_URL = 'ws://localhost:1234';
29090
+ /**
29091
+ * Resolve the transport implied by a server-URL form field: a blank server
29092
+ * URL selects the serverless y-webrtc transport (peers meet via WebRTC
29093
+ * signaling plus same-browser BroadcastChannel); anything else is a
29094
+ * y-websocket server URL.
29095
+ */
29096
+ function resolveTransportForServerUrl(serverUrl) {
29097
+ return serverUrl.trim().length === 0 ? 'webrtc' : 'websocket';
29098
+ }
28780
29099
  /** Generate a fresh, broadcast-scoped room id (`broadcast-<suffix>`). */
28781
29100
  function generateBroadcastRoomId() {
28782
29101
  const suffix = Math.random().toString(36).slice(2, 10);
@@ -28792,30 +29111,44 @@ function seedBroadcastFields(defaults) {
28792
29111
  serverUrl: defaults?.serverUrl ?? DEFAULT_BROADCAST_SERVER_URL,
28793
29112
  };
28794
29113
  }
28795
- /** Whether both required fields are non-blank (after trimming). */
29114
+ /**
29115
+ * Whether the form can start: the room id is required; the server URL may be
29116
+ * blank, which selects the serverless webrtc transport.
29117
+ */
28796
29118
  function canStartBroadcast(fields) {
28797
- return fields.roomId.trim().length > 0 && fields.serverUrl.trim().length > 0;
29119
+ return fields.roomId.trim().length > 0;
28798
29120
  }
28799
29121
  /**
28800
29122
  * Assemble a {@link BroadcastConfig} from the (trimmed) form fields, or `null`
28801
- * when incomplete.
29123
+ * when incomplete. A blank server URL yields `transport: 'webrtc'`.
28802
29124
  */
28803
29125
  function buildBroadcastConfig(fields) {
28804
29126
  if (!canStartBroadcast(fields)) {
28805
29127
  return null;
28806
29128
  }
28807
- return { roomId: fields.roomId.trim(), serverUrl: fields.serverUrl.trim() };
29129
+ const serverUrl = fields.serverUrl.trim();
29130
+ return {
29131
+ roomId: fields.roomId.trim(),
29132
+ serverUrl,
29133
+ transport: resolveTransportForServerUrl(serverUrl),
29134
+ };
28808
29135
  }
28809
29136
  /**
28810
29137
  * Build the shareable viewer follow-link for a broadcast. Returns just the
28811
29138
  * room id when no `origin`/`pathname` are available (non-browser environments).
29139
+ * A blank server URL produces a `transport=webrtc` link instead of a
29140
+ * `server=` parameter.
28812
29141
  */
28813
29142
  function buildBroadcastViewerUrl(roomId, serverUrl, location) {
28814
29143
  if (!location) {
28815
29144
  return roomId;
28816
29145
  }
28817
29146
  const room = encodeURIComponent(roomId);
28818
- const server = encodeURIComponent(serverUrl);
29147
+ const trimmed = serverUrl.trim();
29148
+ if (trimmed.length === 0) {
29149
+ return `${location.origin}${location.pathname}?broadcast=${room}&transport=webrtc`;
29150
+ }
29151
+ const server = encodeURIComponent(trimmed);
28819
29152
  return `${location.origin}${location.pathname}?broadcast=${room}&server=${server}`;
28820
29153
  }
28821
29154
  /** Whether the runtime exposes a usable async clipboard write API. */
@@ -34830,6 +35163,9 @@ class BroadcastDialogComponent {
34830
35163
  /** The shareable follow link (shown while `active`). */
34831
35164
  viewerUrl = input(/* @ts-ignore */
34832
35165
  ...(ngDevMode ? [undefined, { debugName: "viewerUrl" }] : /* istanbul ignore next */ []));
35166
+ /** Whether the active broadcast is peer-to-peer (serverless webrtc). */
35167
+ p2p = input(false, /* @ts-ignore */
35168
+ ...(ngDevMode ? [{ debugName: "p2p" }] : /* istanbul ignore next */ []));
34833
35169
  /** Fired when the presenter starts a broadcast. */
34834
35170
  start = output();
34835
35171
  /** Fired when the presenter stops the active broadcast. */
@@ -34844,6 +35180,9 @@ class BroadcastDialogComponent {
34844
35180
  ...(ngDevMode ? [{ debugName: "copied" }] : /* istanbul ignore next */ []));
34845
35181
  canStart = computed(() => canStartBroadcast({ roomId: this.roomId(), serverUrl: this.serverUrl() }), /* @ts-ignore */
34846
35182
  ...(ngDevMode ? [{ debugName: "canStart" }] : /* istanbul ignore next */ []));
35183
+ /** Blank server URL selects the serverless peer-to-peer (webrtc) transport. */
35184
+ isP2p = computed(() => this.serverUrl().trim().length === 0, /* @ts-ignore */
35185
+ ...(ngDevMode ? [{ debugName: "isP2p" }] : /* istanbul ignore next */ []));
34847
35186
  broadcastingTitle = translate('pptx.broadcast.broadcastingTitle');
34848
35187
  startTitle = translate('pptx.broadcast.startTitle');
34849
35188
  dialogTitle = computed(() => this.active() ? this.broadcastingTitle() : this.startTitle(), /* @ts-ignore */
@@ -34893,7 +35232,7 @@ class BroadcastDialogComponent {
34893
35232
  });
34894
35233
  }
34895
35234
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: BroadcastDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
34896
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: BroadcastDialogComponent, isStandalone: true, selector: "pptx-broadcast-dialog", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, defaults: { classPropertyName: "defaults", publicName: "defaults", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, connected: { classPropertyName: "connected", publicName: "connected", isSignal: true, isRequired: false, transformFunction: null }, viewerCount: { classPropertyName: "viewerCount", publicName: "viewerCount", isSignal: true, isRequired: false, transformFunction: null }, viewerUrl: { classPropertyName: "viewerUrl", publicName: "viewerUrl", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { start: "start", stop: "stop", close: "close" }, ngImport: i0, template: `
35235
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: BroadcastDialogComponent, isStandalone: true, selector: "pptx-broadcast-dialog", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, defaults: { classPropertyName: "defaults", publicName: "defaults", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, connected: { classPropertyName: "connected", publicName: "connected", isSignal: true, isRequired: false, transformFunction: null }, viewerCount: { classPropertyName: "viewerCount", publicName: "viewerCount", isSignal: true, isRequired: false, transformFunction: null }, viewerUrl: { classPropertyName: "viewerUrl", publicName: "viewerUrl", isSignal: true, isRequired: false, transformFunction: null }, p2p: { classPropertyName: "p2p", publicName: "p2p", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { start: "start", stop: "stop", close: "close" }, ngImport: i0, template: `
34897
35236
  <pptx-modal-dialog [open]="open()" [title]="dialogTitle()" (close)="onClose()">
34898
35237
  @if (active()) {
34899
35238
  <!-- Active: share the follow link + stop control -->
@@ -34913,6 +35252,10 @@ class BroadcastDialogComponent {
34913
35252
  </span>
34914
35253
  </div>
34915
35254
 
35255
+ @if (p2p()) {
35256
+ <p class="pptx-ng-broadcast-hint">{{ 'pptx.broadcast.p2pServerValue' | translate }}</p>
35257
+ }
35258
+
34916
35259
  <p class="pptx-ng-broadcast-desc">
34917
35260
  {{ 'pptx.broadcast.liveDesc' | translate }}
34918
35261
  </p>
@@ -34979,6 +35322,9 @@ class BroadcastDialogComponent {
34979
35322
  [value]="serverUrl()"
34980
35323
  (input)="serverUrl.set(asValue($event))"
34981
35324
  />
35325
+ @if (isP2p()) {
35326
+ <p class="pptx-ng-broadcast-hint">{{ 'pptx.broadcast.p2pHint' | translate }}</p>
35327
+ }
34982
35328
  </div>
34983
35329
  </div>
34984
35330
  }
@@ -35023,6 +35369,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
35023
35369
  </span>
35024
35370
  </div>
35025
35371
 
35372
+ @if (p2p()) {
35373
+ <p class="pptx-ng-broadcast-hint">{{ 'pptx.broadcast.p2pServerValue' | translate }}</p>
35374
+ }
35375
+
35026
35376
  <p class="pptx-ng-broadcast-desc">
35027
35377
  {{ 'pptx.broadcast.liveDesc' | translate }}
35028
35378
  </p>
@@ -35089,6 +35439,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
35089
35439
  [value]="serverUrl()"
35090
35440
  (input)="serverUrl.set(asValue($event))"
35091
35441
  />
35442
+ @if (isP2p()) {
35443
+ <p class="pptx-ng-broadcast-hint">{{ 'pptx.broadcast.p2pHint' | translate }}</p>
35444
+ }
35092
35445
  </div>
35093
35446
  </div>
35094
35447
  }
@@ -35110,7 +35463,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
35110
35463
  </div>
35111
35464
  </pptx-modal-dialog>
35112
35465
  `, styles: [".pptx-ng-broadcast{display:flex;flex-direction:column;gap:1rem}.pptx-ng-broadcast-desc{margin:0;font-size:.8125rem;line-height:1.5;color:var(--pptx-muted-foreground, #9ca3af)}.pptx-ng-broadcast-field{display:flex;flex-direction:column;gap:.375rem}.pptx-ng-broadcast-label{font-size:.75rem;font-weight:500;color:var(--pptx-foreground, #f3f4f6)}.pptx-ng-broadcast-input{width:100%;padding:.375rem .75rem;border:1px solid var(--pptx-border, #374151);border-radius:.375rem;background:var(--pptx-background, #030712);color:var(--pptx-foreground, #f3f4f6);font-size:.8125rem}.pptx-ng-broadcast-input:focus{outline:none;border-color:var(--pptx-primary, #6366f1)}.pptx-ng-broadcast-link-row{display:flex;align-items:center;gap:.5rem}.pptx-ng-broadcast-hint{margin:0;font-size:.6875rem;color:var(--pptx-muted-foreground, #9ca3af)}.pptx-ng-broadcast-btn{padding:.375rem .75rem;border:1px solid var(--pptx-border, #374151);border-radius:.375rem;background:var(--pptx-card, #111827);color:var(--pptx-foreground, #f3f4f6);font-size:.75rem;cursor:pointer;white-space:nowrap;transition:background .15s ease}.pptx-ng-broadcast-btn:hover:not(:disabled){background:var(--pptx-border, #374151)}.pptx-ng-broadcast-btn:disabled{opacity:.4;cursor:not-allowed}.pptx-ng-broadcast-btn-primary{border-color:var(--pptx-primary, #6366f1);background:var(--pptx-primary, #6366f1);color:#fff}.pptx-ng-broadcast-btn-primary:hover:not(:disabled){background:var(--pptx-primary, #6366f1);filter:brightness(1.1)}.pptx-ng-broadcast-stop{width:100%;padding:.5rem .75rem;border:1px solid rgba(239,68,68,.3);border-radius:.375rem;background:#ef44441a;color:#f87171;font-size:.75rem;font-weight:500;cursor:pointer;transition:background .15s ease}.pptx-ng-broadcast-stop:hover{background:#ef444433}.pptx-ng-broadcast-status-row{display:flex;align-items:center;gap:.5rem;font-size:.8125rem}.pptx-ng-broadcast-status-dot{width:.5rem;height:.5rem;border-radius:9999px;background:var(--pptx-muted-foreground, #9ca3af)}.pptx-ng-broadcast-status-dot.is-on{background:#22c55e}.pptx-ng-broadcast-status-text{font-weight:500;color:var(--pptx-foreground, #f3f4f6)}.pptx-ng-broadcast-count{margin-left:auto;color:var(--pptx-muted-foreground, #9ca3af)}\n"] }]
35113
- }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], defaults: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaults", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], connected: [{ type: i0.Input, args: [{ isSignal: true, alias: "connected", required: false }] }], viewerCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewerCount", required: false }] }], viewerUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewerUrl", required: false }] }], start: [{ type: i0.Output, args: ["start"] }], stop: [{ type: i0.Output, args: ["stop"] }], close: [{ type: i0.Output, args: ["close"] }] } });
35466
+ }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], defaults: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaults", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], connected: [{ type: i0.Input, args: [{ isSignal: true, alias: "connected", required: false }] }], viewerCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewerCount", required: false }] }], viewerUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "viewerUrl", required: false }] }], p2p: [{ type: i0.Input, args: [{ isSignal: true, alias: "p2p", required: false }] }], start: [{ type: i0.Output, args: ["start"] }], stop: [{ type: i0.Output, args: ["stop"] }], close: [{ type: i0.Output, args: ["close"] }] } });
35114
35467
 
35115
35468
  /**
35116
35469
  * Thin re-export shim → vendored `pptx-viewer-shared`
@@ -35246,6 +35599,107 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
35246
35599
  `, styles: [":host{position:absolute;inset:0;pointer-events:none;overflow:visible;z-index:9999}.pptx-ng-collab-cursor{position:absolute;top:0;left:0;pointer-events:none;will-change:transform;transition:transform 90ms linear}.pptx-ng-collab-pointer{display:block;filter:drop-shadow(0 1px 1px rgba(0,0,0,.35))}.pptx-ng-collab-label{position:absolute;top:16px;left:12px;max-width:150px;padding:2px 6px;border-radius:4px;color:#fff;font-family:system-ui,sans-serif;font-size:10px;font-weight:500;line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;box-shadow:0 1px 2px #0000004d}\n"] }]
35247
35600
  }], propDecorators: { cursors: [{ type: i0.Input, args: [{ isSignal: true, alias: "cursors", required: false }] }], zoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoom", required: false }] }] } });
35248
35601
 
35602
+ /**
35603
+ * collaboration-local-presence.ts: publishes the local user's presence into a
35604
+ * single awareness `presence` field.
35605
+ *
35606
+ * Cursor, selection, and active-slide updates all merge into one record so a
35607
+ * later cursor move never clobbers the selection (or vice versa). The remote
35608
+ * side reads this shape via the shared `derivePresenceList`.
35609
+ */
35610
+ class LocalPresencePublisher {
35611
+ awareness;
35612
+ identity;
35613
+ activeSlide = 0;
35614
+ cursor = { x: 0, y: 0 };
35615
+ selection;
35616
+ constructor(awareness, identity) {
35617
+ this.awareness = awareness;
35618
+ this.identity = identity;
35619
+ }
35620
+ /** Re-emit the merged presence record (also used as a heartbeat). */
35621
+ publish() {
35622
+ this.awareness.setLocalStateField('presence', {
35623
+ userName: this.identity.userName,
35624
+ userColor: this.identity.userColor,
35625
+ userAvatar: this.identity.userAvatar,
35626
+ role: this.identity.role,
35627
+ activeSlideIndex: this.activeSlide,
35628
+ cursorX: this.cursor.x,
35629
+ cursorY: this.cursor.y,
35630
+ selectedElementId: this.selection,
35631
+ lastUpdated: new Date().toISOString(),
35632
+ });
35633
+ }
35634
+ setCursor(x, y, activeSlideIndex = this.activeSlide) {
35635
+ this.cursor = { x, y };
35636
+ this.activeSlide = activeSlideIndex;
35637
+ // A bare `cursor` field mirrors the flat awareness shape some peers read.
35638
+ this.awareness.setLocalStateField('cursor', { x, y });
35639
+ this.publish();
35640
+ }
35641
+ setSelection(selectedElementId, activeSlideIndex = this.activeSlide) {
35642
+ this.selection = selectedElementId;
35643
+ this.activeSlide = activeSlideIndex;
35644
+ this.publish();
35645
+ }
35646
+ setActiveSlide(index) {
35647
+ this.activeSlide = Math.max(0, Math.floor(index));
35648
+ this.publish();
35649
+ }
35650
+ }
35651
+
35652
+ /**
35653
+ * collaboration-providers.ts: transport factories for the Angular
35654
+ * collaboration service.
35655
+ *
35656
+ * Isolates the dynamic `yjs` / `y-websocket` / `y-webrtc` imports and provider
35657
+ * construction so the service focuses on lifecycle + reactive state. The Yjs
35658
+ * packages are dynamically imported so they are fully tree-shaken when
35659
+ * collaboration is unused; each specifier is read from a variable so bundlers
35660
+ * do not eagerly follow it (mirrors the historical `/* @vite-ignore *\/`
35661
+ * pattern).
35662
+ */
35663
+ async function createDoc() {
35664
+ const yModule = 'yjs';
35665
+ const Y = (await import(/* @vite-ignore */ yModule));
35666
+ const doc = new Y.Doc();
35667
+ const factories = {
35668
+ createMap: () => new Y.Map(),
35669
+ createArray: () => new Y.Array(),
35670
+ createText: () => new Y.Text(),
35671
+ };
35672
+ return { doc, factories, Y };
35673
+ }
35674
+ /** Create a y-websocket transport bundle for `config`. */
35675
+ async function createWebsocketBundle(config) {
35676
+ const wsSpecifier = 'y-websocket';
35677
+ const [{ doc, factories }, yws] = await Promise.all([
35678
+ createDoc(),
35679
+ import(/* @vite-ignore */ wsSpecifier),
35680
+ ]);
35681
+ const provider = new yws.WebsocketProvider(config.serverUrl, config.roomId, doc, config.authToken ? { params: { token: config.authToken } } : undefined);
35682
+ return { doc, provider, awareness: provider.awareness, factories };
35683
+ }
35684
+ /**
35685
+ * Create a y-webrtc (peer-to-peer, serverless) transport bundle for `config`.
35686
+ * Peers rendezvous through the configured `signaling` servers (or y-webrtc's
35687
+ * public default) and same-browser tabs additionally sync via BroadcastChannel.
35688
+ * `authToken` is passed as the room `password`.
35689
+ */
35690
+ async function createWebrtcBundle(config) {
35691
+ const rtcSpecifier = 'y-webrtc';
35692
+ const [{ doc, factories }, yrtc] = await Promise.all([
35693
+ createDoc(),
35694
+ import(/* @vite-ignore */ rtcSpecifier),
35695
+ ]);
35696
+ const provider = new yrtc.WebrtcProvider(config.roomId, doc, {
35697
+ signaling: config.signaling?.length ? config.signaling : undefined,
35698
+ password: config.authToken || undefined,
35699
+ });
35700
+ return { doc, provider, awareness: provider.awareness, factories };
35701
+ }
35702
+
35249
35703
  /**
35250
35704
  * Split inherited template (master/layout) elements out of each loaded slide.
35251
35705
  *
@@ -35309,31 +35763,110 @@ function showsTemplateAffordance(element, editTemplateMode) {
35309
35763
  }
35310
35764
 
35311
35765
  /**
35312
- * CollaborationService: Angular port of the Vue `useCollaboration` composable.
35766
+ * collaboration-writeback.ts: elected-writer serialization helper for the
35767
+ * Angular collaboration service.
35313
35768
  *
35314
- * Changes from the original:
35315
- * - Slide sync uses the granular `pptx:slides` Y.Array (structural CRDT) via
35316
- * the shared `writeSlidesToYDoc` / `readSlidesFromYDoc` / `observeYDocSlides`
35317
- * helpers, replacing the monolithic JSON blob in 'presentation'.
35318
- * - Elected-writer write-back: when role === 'owner', changes are debounced
35319
- * and `config.onWriteBack` receives serialized PPTX bytes for persistence.
35769
+ * When the local user is the session `owner`, the service debounces Y.Doc
35770
+ * changes and calls this to serialize the live document (templates re-merged)
35771
+ * into `.pptx` bytes for `config.onWriteBack`. Kept out of the service so the
35772
+ * service stays within the repo's per-file size budget.
35773
+ */
35774
+ const WRITE_BACK_DEBOUNCE_MS = 5_000;
35775
+ /**
35776
+ * Serialize the current Y.Doc slide state (with the separated master/layout
35777
+ * template elements merged back) into `.pptx` bytes. `sourceBytes` seeds the
35778
+ * handler so package parts the CRDT does not carry (media, fonts, rels) survive.
35779
+ */
35780
+ async function serializeWriteBack(ydoc, sourceBytes, templateElements) {
35781
+ const { PptxHandler } = await import('pptx-viewer-core');
35782
+ const handler = new PptxHandler();
35783
+ await handler.load(sourceBytes.buffer);
35784
+ const slides = buildSaveSlides(readSlidesFromYDoc(ydoc), templateElements);
35785
+ return handler.save(slides);
35786
+ }
35787
+ /**
35788
+ * Debounced elected-writer write-back. Only the session `owner` with an
35789
+ * `onWriteBack` callback schedules anything; each new change resets the timer,
35790
+ * and on fire it serializes the live Y.Doc and hands the bytes to the host.
35791
+ */
35792
+ class WriteBackScheduler {
35793
+ timer = null;
35794
+ schedule(config, ydoc, getSourceBytes, getTemplateElements) {
35795
+ if (!config?.onWriteBack || config.role !== 'owner' || !ydoc) {
35796
+ return;
35797
+ }
35798
+ this.cancel();
35799
+ const ms = config.writeBackDebounceMs ?? WRITE_BACK_DEBOUNCE_MS;
35800
+ this.timer = setTimeout(() => {
35801
+ this.timer = null;
35802
+ const bytes = getSourceBytes?.();
35803
+ if (!bytes || !config.onWriteBack) {
35804
+ return;
35805
+ }
35806
+ void serializeWriteBack(ydoc, bytes, getTemplateElements?.() ?? {})
35807
+ .then((out) => config.onWriteBack?.(out))
35808
+ .catch(() => {
35809
+ /* non-fatal */
35810
+ });
35811
+ }, ms);
35812
+ }
35813
+ cancel() {
35814
+ if (this.timer !== null) {
35815
+ clearTimeout(this.timer);
35816
+ this.timer = null;
35817
+ }
35818
+ }
35819
+ }
35820
+
35821
+ /**
35822
+ * CollaborationService: Angular real-time collaboration (Yjs) service.
35823
+ *
35824
+ * Owns the provider lifecycle and the reactive collaboration state consumed by
35825
+ * the viewer component:
35826
+ * - Transport is `y-websocket` (default) or serverless `y-webrtc`
35827
+ * (`config.transport === 'webrtc'`), created via `./collaboration-providers`.
35828
+ * - Local edits sync via the granular `reconcileSlidesInYDoc` (only changed
35829
+ * slides/elements/fields mutate) in a transaction tagged `LOCAL_SYNC_ORIGIN`;
35830
+ * the remote observer skips its own local-origin writes.
35831
+ * - Websocket connections fail fast on mixed content and time out to `'error'`
35832
+ * after `CONNECTION_TIMEOUT_MS`; `retry()` reconnects with the last config.
35833
+ * - Elected-writer write-back (role === 'owner') via `WriteBackScheduler`.
35320
35834
  *
35321
35835
  * Provide at the component level: `@Component({ providers: [CollaborationService] })`.
35322
35836
  */
35323
35837
  const DEFAULT_CANVAS_BOUND = 100_000;
35324
- const WRITE_BACK_DEBOUNCE_MS = 5_000;
35325
35838
  class CollaborationService {
35326
35839
  // Reactive state
35327
- connected = signal(false, /* @ts-ignore */
35840
+ status = signal('disconnected', /* @ts-ignore */
35841
+ ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
35842
+ connected = computed(() => this.status() === 'connected', /* @ts-ignore */
35328
35843
  ...(ngDevMode ? [{ debugName: "connected" }] : /* istanbul ignore next */ []));
35329
35844
  active = signal(false, /* @ts-ignore */
35330
35845
  ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
35846
+ /** Role of the local user in the active session, or undefined when idle. */
35847
+ activeRole = signal(undefined, /* @ts-ignore */
35848
+ ...(ngDevMode ? [{ debugName: "activeRole" }] : /* istanbul ignore next */ []));
35331
35849
  presence = signal([], /* @ts-ignore */
35332
35850
  ...(ngDevMode ? [{ debugName: "presence" }] : /* istanbul ignore next */ []));
35333
35851
  cursors = computed(() => presenceToCursors(this.presence()), /* @ts-ignore */
35334
35852
  ...(ngDevMode ? [{ debugName: "cursors" }] : /* istanbul ignore next */ []));
35335
35853
  connectedCount = computed(() => this.presence().length + (this.active() ? 1 : 0), /* @ts-ignore */
35336
35854
  ...(ngDevMode ? [{ debugName: "connectedCount" }] : /* istanbul ignore next */ []));
35855
+ /** The client id the local user is currently following (null when free). */
35856
+ followedClientId = signal(null, /* @ts-ignore */
35857
+ ...(ngDevMode ? [{ debugName: "followedClientId" }] : /* istanbul ignore next */ []));
35858
+ /** Active-slide index of the followed peer, or null when not following. */
35859
+ followedSlideIndex = computed(() => {
35860
+ const id = this.followedClientId();
35861
+ if (id === null) {
35862
+ return null;
35863
+ }
35864
+ return this.presence().find((p) => p.clientId === id)?.activeSlideIndex ?? null;
35865
+ }, /* @ts-ignore */
35866
+ ...(ngDevMode ? [{ debugName: "followedSlideIndex" }] : /* istanbul ignore next */ []));
35867
+ /** Active-slide index of the first `owner` peer (the broadcaster), or null. */
35868
+ broadcasterSlideIndex = computed(() => this.presence().find((p) => p.role === 'owner')?.activeSlideIndex ?? null, /* @ts-ignore */
35869
+ ...(ngDevMode ? [{ debugName: "broadcasterSlideIndex" }] : /* istanbul ignore next */ []));
35337
35870
  // Internal handles
35338
35871
  ydoc = null;
35339
35872
  provider = null;
@@ -35342,18 +35875,26 @@ class CollaborationService {
35342
35875
  applyingRemote = false;
35343
35876
  yFactories = null;
35344
35877
  lastSynced = '';
35345
- writeBackTimer = null;
35878
+ connectTimer = null;
35346
35879
  unobserveSlides = null;
35880
+ writeBack = new WriteBackScheduler();
35881
+ /**
35882
+ * First-write gate: local broadcasts are suppressed (captured as pending)
35883
+ * until the provider confirms its initial sync or the grace period lifts
35884
+ * the gate, so a late joiner never seeds its placeholder deck into a room
35885
+ * whose real content has not arrived yet.
35886
+ */
35887
+ syncGate = createSyncGate(() => this.flushPendingBroadcast());
35888
+ pendingBroadcast = null;
35347
35889
  onRemoteSlides = null;
35348
35890
  canvasWidth = DEFAULT_CANVAS_BOUND;
35349
35891
  canvasHeight = DEFAULT_CANVAS_BOUND;
35350
35892
  getSourceBytes = null;
35351
35893
  getTemplateElements = null;
35352
- userName = 'Anonymous';
35353
- userColor = DEFAULT_CURSOR_COLOR;
35354
- userAvatar;
35355
- role;
35356
35894
  currentConfig = null;
35895
+ lastConfig = null;
35896
+ lastOptions = {};
35897
+ localPresence = null;
35357
35898
  refreshPresence = () => {
35358
35899
  if (!this.awareness) {
35359
35900
  this.presence.set([]);
@@ -35366,10 +35907,19 @@ class CollaborationService {
35366
35907
  }
35367
35908
  async connect(config, options = {}) {
35368
35909
  this.disconnect();
35910
+ this.lastConfig = config;
35911
+ this.lastOptions = options;
35369
35912
  try {
35370
35913
  validateRoomId(config.roomId);
35371
35914
  }
35372
35915
  catch {
35916
+ this.status.set('error');
35917
+ return;
35918
+ }
35919
+ // Fail fast on mixed content (websocket only): an https page cannot open a
35920
+ // ws:// socket, so surface the error rather than hanging until the timeout.
35921
+ if (config.transport !== 'webrtc' && isMixedContentBlocked(config.serverUrl)) {
35922
+ this.status.set('error');
35373
35923
  return;
35374
35924
  }
35375
35925
  this.onRemoteSlides = options.onRemoteSlides ?? null;
@@ -35377,60 +35927,152 @@ class CollaborationService {
35377
35927
  this.canvasHeight = options.canvasHeight ?? DEFAULT_CANVAS_BOUND;
35378
35928
  this.getSourceBytes = options.getSourceBytes ?? null;
35379
35929
  this.getTemplateElements = options.getTemplateElements ?? null;
35380
- this.userName = config.userName;
35381
- this.userColor = config.userColor ?? DEFAULT_CURSOR_COLOR;
35382
- this.userAvatar = config.userAvatar;
35383
- this.role = config.role;
35384
35930
  this.currentConfig = config;
35931
+ this.activeRole.set(config.role);
35932
+ this.status.set('connecting');
35385
35933
  try {
35386
- const wsModule = 'y-websocket';
35387
- const [Y, yws] = await Promise.all([
35388
- import('yjs'),
35389
- import(/* @vite-ignore */ wsModule),
35390
- ]);
35391
- const doc = new Y.Doc();
35392
- this.ydoc = doc;
35393
- this.yFactories = {
35394
- createMap: () => new Y.Map(),
35395
- createArray: () => new Y.Array(),
35396
- createText: () => new Y.Text(),
35397
- };
35398
- const wsProvider = new yws.WebsocketProvider(config.serverUrl, config.roomId, doc, config.authToken ? { params: { token: config.authToken } } : undefined);
35399
- this.provider = wsProvider;
35400
- this.awareness = wsProvider.awareness;
35934
+ const bundle = config.transport === 'webrtc'
35935
+ ? await createWebrtcBundle(config)
35936
+ : await createWebsocketBundle(config);
35937
+ this.ydoc = bundle.doc;
35938
+ this.yFactories = bundle.factories;
35939
+ this.provider = bundle.provider;
35940
+ this.awareness = bundle.awareness;
35401
35941
  this.selfId = this.awareness.clientID ?? -1;
35402
- this._publishAwareness();
35942
+ this.localPresence = new LocalPresencePublisher(this.awareness, {
35943
+ userName: config.userName,
35944
+ userColor: config.userColor ?? DEFAULT_CURSOR_COLOR,
35945
+ userAvatar: config.userAvatar,
35946
+ role: config.role,
35947
+ });
35948
+ this.localPresence.publish();
35403
35949
  this.awareness.on('change', this.refreshPresence);
35404
35950
  this.awareness.on('update', this.refreshPresence);
35405
- wsProvider.on('status', (payload) => {
35406
- this.connected.set(payload.status === 'connected');
35407
- });
35408
- // Observe remote slide changes
35409
- this.unobserveSlides = observeYDocSlides(doc, () => {
35410
- if (this.applyingRemote || !this.ydoc) {
35411
- return;
35412
- }
35413
- const remote = readSlidesFromYDoc(this.ydoc);
35414
- if (remote.length === 0) {
35415
- return;
35416
- }
35417
- this.applyingRemote = true;
35418
- this.onRemoteSlides?.(remote);
35419
- this.applyingRemote = false;
35420
- this._scheduleWriteBack();
35421
- });
35951
+ this.wireStatus(config);
35952
+ this.syncGate.reset();
35953
+ this.wireSynced();
35954
+ this.unobserveSlides = observeYDocSlides(this.ydoc, (_events, transaction) => this.onRemoteChange(transaction));
35422
35955
  this.active.set(true);
35423
35956
  this.refreshPresence();
35424
35957
  }
35425
35958
  catch {
35426
35959
  this.disconnect();
35960
+ this.status.set('error');
35427
35961
  }
35428
35962
  }
35429
- disconnect() {
35430
- if (this.writeBackTimer !== null) {
35431
- clearTimeout(this.writeBackTimer);
35432
- this.writeBackTimer = null;
35963
+ /** Reconnect using the configuration from the most recent {@link connect}. */
35964
+ async retry() {
35965
+ if (this.lastConfig) {
35966
+ await this.connect(this.lastConfig, this.lastOptions);
35967
+ }
35968
+ }
35969
+ /** Wire the provider status events + (websocket-only) connection timeout. */
35970
+ wireStatus(config) {
35971
+ const provider = this.provider;
35972
+ if (!provider) {
35973
+ return;
35974
+ }
35975
+ if (config.transport === 'webrtc') {
35976
+ // P2P: no server round-trip to wait on. Treat "created" as connected,
35977
+ // and reflect explicit disconnect events.
35978
+ this.status.set('connected');
35979
+ provider.on('status', (payload) => {
35980
+ if (payload.connected === false && this.active()) {
35981
+ this.status.set('disconnected');
35982
+ }
35983
+ else if (payload.connected === true) {
35984
+ this.status.set('connected');
35985
+ }
35986
+ });
35987
+ return;
35988
+ }
35989
+ provider.on('status', (payload) => {
35990
+ if (payload.status === 'connected') {
35991
+ this.clearConnectTimer();
35992
+ this.status.set('connected');
35993
+ }
35994
+ else if (payload.status === 'disconnected' && this.active()) {
35995
+ this.status.set('disconnected');
35996
+ }
35997
+ });
35998
+ if (provider.wsconnected) {
35999
+ this.status.set('connected');
36000
+ return;
36001
+ }
36002
+ this.connectTimer = setTimeout(() => {
36003
+ this.connectTimer = null;
36004
+ if (this.status() !== 'connected') {
36005
+ this.disconnect();
36006
+ this.status.set('error');
36007
+ }
36008
+ }, CONNECTION_TIMEOUT_MS);
36009
+ }
36010
+ /**
36011
+ * Open the first-write gate on the provider's initial-sync confirmation.
36012
+ * y-websocket emits 'sync' with a boolean; y-webrtc emits 'synced' with an
36013
+ * object carrying a `synced` flag (and only once a peer syncs, hence the
36014
+ * grace timer). Listen to both; opening is idempotent.
36015
+ */
36016
+ wireSynced() {
36017
+ const provider = this.provider;
36018
+ if (!provider) {
36019
+ return;
36020
+ }
36021
+ const handle = (payload) => {
36022
+ const flag = payload;
36023
+ const isSynced = typeof flag === 'boolean' ? flag : flag?.synced !== false;
36024
+ if (isSynced) {
36025
+ this.syncGate.open();
36026
+ }
36027
+ };
36028
+ provider.on('sync', handle);
36029
+ provider.on('synced', handle);
36030
+ if (provider.synced === true) {
36031
+ this.syncGate.open();
36032
+ }
36033
+ else {
36034
+ this.syncGate.arm();
36035
+ }
36036
+ }
36037
+ /**
36038
+ * Perform the deferred first broadcast once the gate opens. When the doc is
36039
+ * still empty (fresh room, or nobody else present), clear the baseline so
36040
+ * the pending deck actually seeds it; when remote content already arrived,
36041
+ * the pending deck matches the applied baseline and the write is a no-op.
36042
+ */
36043
+ flushPendingBroadcast() {
36044
+ const pending = this.pendingBroadcast;
36045
+ this.pendingBroadcast = null;
36046
+ if (!pending || !this.ydoc || !this.yFactories) {
36047
+ return;
36048
+ }
36049
+ if (this.ydoc.getArray(YDOC_SLIDES_KEY).length === 0) {
36050
+ this.lastSynced = '';
35433
36051
  }
36052
+ this.broadcastSlides(pending);
36053
+ }
36054
+ /** Handle a remote Y.Doc change, skipping our own local-origin transactions. */
36055
+ onRemoteChange(transaction) {
36056
+ if (transaction?.origin === LOCAL_SYNC_ORIGIN || this.applyingRemote || !this.ydoc) {
36057
+ return;
36058
+ }
36059
+ const remote = readSlidesFromYDoc(this.ydoc);
36060
+ if (remote.length === 0) {
36061
+ return;
36062
+ }
36063
+ // Suppress the echo: record what we just applied so the subsequent local
36064
+ // broadcast (driven by the editor signal) is a no-op.
36065
+ this.lastSynced = JSON.stringify(remote);
36066
+ this.applyingRemote = true;
36067
+ this.onRemoteSlides?.(remote);
36068
+ this.applyingRemote = false;
36069
+ this.scheduleWriteBack();
36070
+ }
36071
+ disconnect() {
36072
+ this.clearConnectTimer();
36073
+ this.syncGate.reset();
36074
+ this.pendingBroadcast = null;
36075
+ this.writeBack.cancel();
35434
36076
  this.unobserveSlides?.();
35435
36077
  this.unobserveSlides = null;
35436
36078
  this.awareness?.off?.('change', this.refreshPresence);
@@ -35441,19 +36083,42 @@ class CollaborationService {
35441
36083
  this.provider = null;
35442
36084
  this.ydoc = null;
35443
36085
  this.awareness = null;
36086
+ this.localPresence = null;
35444
36087
  this.selfId = -1;
35445
36088
  this.applyingRemote = false;
35446
36089
  this.yFactories = null;
35447
36090
  this.lastSynced = '';
35448
36091
  this.onRemoteSlides = null;
35449
36092
  this.currentConfig = null;
35450
- this.connected.set(false);
36093
+ this.status.set('disconnected');
35451
36094
  this.active.set(false);
36095
+ this.activeRole.set(undefined);
35452
36096
  this.presence.set([]);
36097
+ this.followedClientId.set(null);
36098
+ }
36099
+ /**
36100
+ * Broadcast the local slide set to peers, reconciling only what changed into
36101
+ * the pptx:slides Y.Array. An empty deck is never written (so a late-joiner
36102
+ * that has not yet received the doc cannot clobber it), and an unchanged deck
36103
+ * is skipped.
36104
+ */
36105
+ /**
36106
+ * Record the current local deck as the sync baseline so the first (unchanged)
36107
+ * broadcast after connecting is suppressed. Call right after {@link connect}
36108
+ * for a joiner whose local deck is a placeholder awaiting remote sync, so it
36109
+ * never overwrites the shared document before receiving it.
36110
+ */
36111
+ seedBaseline(slides) {
36112
+ this.lastSynced = JSON.stringify(slides);
35453
36113
  }
35454
- /** Broadcast the local slide set to peers via the pptx:slides Y.Array. */
35455
36114
  broadcastSlides(slides) {
35456
- if (!this.ydoc || !this.yFactories || this.applyingRemote) {
36115
+ if (!this.ydoc || !this.yFactories || this.applyingRemote || slides.length === 0) {
36116
+ return;
36117
+ }
36118
+ if (!this.syncGate.isOpen()) {
36119
+ // Defer until the initial sync confirms; the gate flushes the latest
36120
+ // pending deck when it opens.
36121
+ this.pendingBroadcast = slides;
35457
36122
  return;
35458
36123
  }
35459
36124
  const s = JSON.stringify(slides);
@@ -35461,68 +36126,31 @@ class CollaborationService {
35461
36126
  return;
35462
36127
  }
35463
36128
  this.lastSynced = s;
35464
- writeSlidesToYDoc([...slides], this.ydoc, this.yFactories);
35465
- this._scheduleWriteBack();
36129
+ reconcileSlidesInYDoc([...slides], this.ydoc, this.yFactories, LOCAL_SYNC_ORIGIN);
36130
+ this.scheduleWriteBack();
35466
36131
  }
35467
- setCursor(x, y, activeSlideIndex = 0) {
35468
- if (!this.awareness) {
35469
- return;
35470
- }
35471
- this.awareness.setLocalStateField('cursor', { x, y });
35472
- this._publishAwareness(activeSlideIndex, x, y);
36132
+ setCursor(x, y, activeSlideIndex) {
36133
+ this.localPresence?.setCursor(x, y, activeSlideIndex);
35473
36134
  }
35474
- setSelection(selectedElementId, activeSlideIndex = 0) {
35475
- if (!this.awareness) {
35476
- return;
35477
- }
35478
- this._publishAwareness(activeSlideIndex, undefined, undefined, selectedElementId);
35479
- }
35480
- _publishAwareness(activeSlideIndex = 0, cursorX = 0, cursorY = 0, selectedElementId) {
35481
- this.awareness?.setLocalStateField('presence', {
35482
- userName: this.userName,
35483
- userColor: this.userColor,
35484
- userAvatar: this.userAvatar,
35485
- role: this.role,
35486
- activeSlideIndex,
35487
- cursorX,
35488
- cursorY,
35489
- selectedElementId,
35490
- lastUpdated: new Date().toISOString(),
35491
- });
35492
- this.awareness?.setLocalStateField('user', { name: this.userName, color: this.userColor });
36135
+ setSelection(selectedElementId, activeSlideIndex) {
36136
+ this.localPresence?.setSelection(selectedElementId, activeSlideIndex);
35493
36137
  }
35494
- _scheduleWriteBack() {
35495
- const config = this.currentConfig;
35496
- if (!config?.onWriteBack || config.role !== 'owner' || !this.ydoc) {
35497
- return;
35498
- }
35499
- if (this.writeBackTimer !== null) {
35500
- clearTimeout(this.writeBackTimer);
36138
+ /** Publish the local active-slide index (drives follow-along). */
36139
+ setActiveSlide(index) {
36140
+ this.localPresence?.setActiveSlide(index);
36141
+ }
36142
+ /** Follow the given peer's active slide, or `null` to stop following. */
36143
+ followUser(clientId) {
36144
+ this.followedClientId.set(clientId);
36145
+ }
36146
+ clearConnectTimer() {
36147
+ if (this.connectTimer !== null) {
36148
+ clearTimeout(this.connectTimer);
36149
+ this.connectTimer = null;
35501
36150
  }
35502
- const ms = config.writeBackDebounceMs ?? WRITE_BACK_DEBOUNCE_MS;
35503
- this.writeBackTimer = setTimeout(async () => {
35504
- this.writeBackTimer = null;
35505
- if (!this.ydoc || !config.onWriteBack) {
35506
- return;
35507
- }
35508
- const sourceBytes = this.getSourceBytes?.();
35509
- if (!sourceBytes) {
35510
- return;
35511
- }
35512
- try {
35513
- const { PptxHandler } = await import('pptx-viewer-core');
35514
- const handler = new PptxHandler();
35515
- await handler.load(sourceBytes.buffer);
35516
- // Merge the editor's separated template (master/layout) elements back
35517
- // into the broadcast (template-free) slides so template edits persist.
35518
- const slides = buildSaveSlides(readSlidesFromYDoc(this.ydoc), this.getTemplateElements?.() ?? {});
35519
- const bytes = await handler.save(slides);
35520
- config.onWriteBack(bytes);
35521
- }
35522
- catch {
35523
- /* non-fatal */
35524
- }
35525
- }, ms);
36151
+ }
36152
+ scheduleWriteBack() {
36153
+ this.writeBack.schedule(this.currentConfig, this.ydoc, this.getSourceBytes, this.getTemplateElements);
35526
36154
  }
35527
36155
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CollaborationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
35528
36156
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CollaborationService });
@@ -36388,6 +37016,16 @@ class EditorStateService {
36388
37016
  this.dirty.set(true);
36389
37017
  this.syncHistory();
36390
37018
  }
37019
+ /**
37020
+ * Apply a remote collaborator's slide set (already template-free, as broadcast
37021
+ * over the CRDT) to the editable deck. Selection, history, and this peer's own
37022
+ * separated template store are left untouched; remote edits are not local undo
37023
+ * steps.
37024
+ */
37025
+ applyRemoteSlides(slides) {
37026
+ this.slides.set(this.clone(slides));
37027
+ this.dirty.set(true);
37028
+ }
36391
37029
  // ── Snapshot (deck + template store) ─────────────────────────────────────
36392
37030
  /** Capture the current deck + template store as one undo/redo snapshot. */
36393
37031
  captureSnapshot() {
@@ -39728,6 +40366,145 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
39728
40366
  args: ['document:keydown', ['$event']]
39729
40367
  }] } });
39730
40368
 
40369
+ /**
40370
+ * follow-mode-bar.component.ts: Angular port of the Vue `FollowModeBar.vue`.
40371
+ *
40372
+ * Selector: `pptx-follow-mode-bar`
40373
+ *
40374
+ * Lists the active remote peers and lets the local user follow one of them
40375
+ * (mirroring that peer's active slide) or stop following. Purely presentational:
40376
+ * the host supplies the reactive presence list and the currently-followed
40377
+ * clientId (from `CollaborationService`) and reacts to the `follow` event to
40378
+ * drive `followUser(clientId | null)`. Each peer chip shows an initials avatar
40379
+ * in the peer's colour; the followed peer is highlighted with a "Stop" affordance.
40380
+ */
40381
+ class FollowModeBarComponent {
40382
+ /** Active remote collaborators (excludes self). */
40383
+ presences = input([], /* @ts-ignore */
40384
+ ...(ngDevMode ? [{ debugName: "presences" }] : /* istanbul ignore next */ []));
40385
+ /** The clientId currently being followed, or null. */
40386
+ followedClientId = input(null, /* @ts-ignore */
40387
+ ...(ngDevMode ? [{ debugName: "followedClientId" }] : /* istanbul ignore next */ []));
40388
+ /** Follow the given peer, or `null` to stop following. */
40389
+ follow = output();
40390
+ chips = computed(() => {
40391
+ const followed = this.followedClientId();
40392
+ return this.presences().map((peer) => ({
40393
+ clientId: peer.clientId,
40394
+ userName: peer.userName,
40395
+ color: peer.userColor,
40396
+ initials: initialsOf(peer.userName),
40397
+ following: peer.clientId === followed,
40398
+ }));
40399
+ }, /* @ts-ignore */
40400
+ ...(ngDevMode ? [{ debugName: "chips" }] : /* istanbul ignore next */ []));
40401
+ followedName = computed(() => {
40402
+ const id = this.followedClientId();
40403
+ if (id === null) {
40404
+ return null;
40405
+ }
40406
+ return this.presences().find((p) => p.clientId === id)?.userName ?? null;
40407
+ }, /* @ts-ignore */
40408
+ ...(ngDevMode ? [{ debugName: "followedName" }] : /* istanbul ignore next */ []));
40409
+ toggle(clientId) {
40410
+ this.follow.emit(this.followedClientId() === clientId ? null : clientId);
40411
+ }
40412
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: FollowModeBarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
40413
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: FollowModeBarComponent, isStandalone: true, selector: "pptx-follow-mode-bar", inputs: { presences: { classPropertyName: "presences", publicName: "presences", isSignal: true, isRequired: false, transformFunction: null }, followedClientId: { classPropertyName: "followedClientId", publicName: "followedClientId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { follow: "follow" }, ngImport: i0, template: `
40414
+ @if (chips().length > 0) {
40415
+ <div class="pptx-ng-follow-bar" data-export-ignore="true">
40416
+ <span class="pptx-ng-follow-status">
40417
+ @if (followedName(); as name) {
40418
+ {{ 'pptx.followMode.following' | translate }}
40419
+ <strong>{{ name }}</strong>
40420
+ <button
40421
+ type="button"
40422
+ class="pptx-ng-follow-stop"
40423
+ [title]="'pptx.followMode.stopFollowing' | translate"
40424
+ (click)="follow.emit(null)"
40425
+ >
40426
+ {{ 'pptx.followMode.stop' | translate }}
40427
+ </button>
40428
+ } @else {
40429
+ {{ 'pptx.followMode.followCollaborator' | translate }}
40430
+ }
40431
+ </span>
40432
+ <ul class="pptx-ng-follow-list">
40433
+ @for (peer of chips(); track peer.clientId) {
40434
+ <li>
40435
+ <button
40436
+ type="button"
40437
+ class="pptx-ng-follow-peer"
40438
+ [class.is-following]="peer.following"
40439
+ [attr.data-client-id]="peer.clientId"
40440
+ [attr.aria-pressed]="peer.following"
40441
+ (click)="toggle(peer.clientId)"
40442
+ >
40443
+ <span class="pptx-ng-follow-avatar" [style.background-color]="peer.color">
40444
+ {{ peer.initials }}
40445
+ </span>
40446
+ <span class="pptx-ng-follow-name">{{ peer.userName }}</span>
40447
+ </button>
40448
+ </li>
40449
+ }
40450
+ </ul>
40451
+ </div>
40452
+ }
40453
+ `, isInline: true, styles: [":host{display:block}.pptx-ng-follow-bar{display:flex;flex-wrap:wrap;align-items:center;gap:.75rem;border-radius:.5rem;background:var(--pptx-card, rgba(17, 24, 39, .95));padding:.375rem .625rem;font-size:.75rem;color:var(--pptx-foreground, #f3f4f6)}.pptx-ng-follow-status{display:inline-flex;align-items:center;gap:.375rem;white-space:nowrap;color:var(--pptx-muted-foreground, #9ca3af)}.pptx-ng-follow-stop{cursor:pointer;border-radius:.375rem;border:1px solid var(--pptx-border, #374151);background:transparent;padding:.125rem .5rem;font-size:11px;color:var(--pptx-foreground, #f3f4f6)}.pptx-ng-follow-list{display:flex;align-items:center;gap:.375rem;margin:0;padding:0;list-style:none}.pptx-ng-follow-peer{display:inline-flex;cursor:pointer;align-items:center;gap:.375rem;border-radius:9999px;border:1px solid transparent;background:var(--pptx-muted, rgba(55, 65, 81, .6));padding:.125rem .5rem .125rem .125rem;color:var(--pptx-foreground, #f3f4f6)}.pptx-ng-follow-peer.is-following{border-color:var(--pptx-primary, #6366f1);background:color-mix(in srgb,var(--pptx-primary, #6366f1) 30%,transparent)}.pptx-ng-follow-avatar{display:inline-flex;height:22px;width:22px;align-items:center;justify-content:center;border-radius:9999px;font-size:10px;font-weight:600;line-height:1;color:#fff}.pptx-ng-follow-name{max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n"], dependencies: [{ kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
40454
+ }
40455
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: FollowModeBarComponent, decorators: [{
40456
+ type: Component,
40457
+ args: [{ selector: 'pptx-follow-mode-bar', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [TranslatePipe], template: `
40458
+ @if (chips().length > 0) {
40459
+ <div class="pptx-ng-follow-bar" data-export-ignore="true">
40460
+ <span class="pptx-ng-follow-status">
40461
+ @if (followedName(); as name) {
40462
+ {{ 'pptx.followMode.following' | translate }}
40463
+ <strong>{{ name }}</strong>
40464
+ <button
40465
+ type="button"
40466
+ class="pptx-ng-follow-stop"
40467
+ [title]="'pptx.followMode.stopFollowing' | translate"
40468
+ (click)="follow.emit(null)"
40469
+ >
40470
+ {{ 'pptx.followMode.stop' | translate }}
40471
+ </button>
40472
+ } @else {
40473
+ {{ 'pptx.followMode.followCollaborator' | translate }}
40474
+ }
40475
+ </span>
40476
+ <ul class="pptx-ng-follow-list">
40477
+ @for (peer of chips(); track peer.clientId) {
40478
+ <li>
40479
+ <button
40480
+ type="button"
40481
+ class="pptx-ng-follow-peer"
40482
+ [class.is-following]="peer.following"
40483
+ [attr.data-client-id]="peer.clientId"
40484
+ [attr.aria-pressed]="peer.following"
40485
+ (click)="toggle(peer.clientId)"
40486
+ >
40487
+ <span class="pptx-ng-follow-avatar" [style.background-color]="peer.color">
40488
+ {{ peer.initials }}
40489
+ </span>
40490
+ <span class="pptx-ng-follow-name">{{ peer.userName }}</span>
40491
+ </button>
40492
+ </li>
40493
+ }
40494
+ </ul>
40495
+ </div>
40496
+ }
40497
+ `, styles: [":host{display:block}.pptx-ng-follow-bar{display:flex;flex-wrap:wrap;align-items:center;gap:.75rem;border-radius:.5rem;background:var(--pptx-card, rgba(17, 24, 39, .95));padding:.375rem .625rem;font-size:.75rem;color:var(--pptx-foreground, #f3f4f6)}.pptx-ng-follow-status{display:inline-flex;align-items:center;gap:.375rem;white-space:nowrap;color:var(--pptx-muted-foreground, #9ca3af)}.pptx-ng-follow-stop{cursor:pointer;border-radius:.375rem;border:1px solid var(--pptx-border, #374151);background:transparent;padding:.125rem .5rem;font-size:11px;color:var(--pptx-foreground, #f3f4f6)}.pptx-ng-follow-list{display:flex;align-items:center;gap:.375rem;margin:0;padding:0;list-style:none}.pptx-ng-follow-peer{display:inline-flex;cursor:pointer;align-items:center;gap:.375rem;border-radius:9999px;border:1px solid transparent;background:var(--pptx-muted, rgba(55, 65, 81, .6));padding:.125rem .5rem .125rem .125rem;color:var(--pptx-foreground, #f3f4f6)}.pptx-ng-follow-peer.is-following{border-color:var(--pptx-primary, #6366f1);background:color-mix(in srgb,var(--pptx-primary, #6366f1) 30%,transparent)}.pptx-ng-follow-avatar{display:inline-flex;height:22px;width:22px;align-items:center;justify-content:center;border-radius:9999px;font-size:10px;font-weight:600;line-height:1;color:#fff}.pptx-ng-follow-name{max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n"] }]
40498
+ }], propDecorators: { presences: [{ type: i0.Input, args: [{ isSignal: true, alias: "presences", required: false }] }], followedClientId: [{ type: i0.Input, args: [{ isSignal: true, alias: "followedClientId", required: false }] }], follow: [{ type: i0.Output, args: ["follow"] }] } });
40499
+ /** First-letter / two-char initials for the avatar chip. */
40500
+ function initialsOf(name) {
40501
+ const parts = name.trim().split(/\s+/u);
40502
+ if (parts.length >= 2 && parts[0] && parts[parts.length - 1]) {
40503
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
40504
+ }
40505
+ return name.slice(0, 2).toUpperCase() || '?';
40506
+ }
40507
+
39731
40508
  /**
39732
40509
  * hyperlink-dialog.component.ts: Set or clear an element's click hyperlink.
39733
40510
  *
@@ -56588,7 +57365,7 @@ class ElementRendererComponent {
56588
57365
  }, /* @ts-ignore */
56589
57366
  ...(ngDevMode ? [{ debugName: "placeholderLabel" }] : /* istanbul ignore next */ []));
56590
57367
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ElementRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
56591
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: ElementRendererComponent, isStandalone: true, selector: "pptx-element-renderer", inputs: { element: { classPropertyName: "element", publicName: "element", isSignal: true, isRequired: true, transformFunction: null }, mediaDataUrls: { classPropertyName: "mediaDataUrls", publicName: "mediaDataUrls", isSignal: true, isRequired: false, transformFunction: null }, zIndex: { classPropertyName: "zIndex", publicName: "zIndex", isSignal: true, isRequired: false, transformFunction: null }, obstacles: { classPropertyName: "obstacles", publicName: "obstacles", isSignal: true, isRequired: false, transformFunction: null }, canvasWidth: { classPropertyName: "canvasWidth", publicName: "canvasWidth", isSignal: true, isRequired: false, transformFunction: null }, canvasHeight: { classPropertyName: "canvasHeight", publicName: "canvasHeight", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, fieldContext: { classPropertyName: "fieldContext", publicName: "fieldContext", isSignal: true, isRequired: false, transformFunction: null }, editTemplateMode: { classPropertyName: "editTemplateMode", publicName: "editTemplateMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { cellCommit: "cellCommit", tableChange: "tableChange" }, ngImport: i0, template: `
57368
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: ElementRendererComponent, isStandalone: true, selector: "pptx-element-renderer", inputs: { element: { classPropertyName: "element", publicName: "element", isSignal: true, isRequired: true, transformFunction: null }, mediaDataUrls: { classPropertyName: "mediaDataUrls", publicName: "mediaDataUrls", isSignal: true, isRequired: false, transformFunction: null }, zIndex: { classPropertyName: "zIndex", publicName: "zIndex", isSignal: true, isRequired: false, transformFunction: null }, obstacles: { classPropertyName: "obstacles", publicName: "obstacles", isSignal: true, isRequired: false, transformFunction: null }, canvasWidth: { classPropertyName: "canvasWidth", publicName: "canvasWidth", isSignal: true, isRequired: false, transformFunction: null }, canvasHeight: { classPropertyName: "canvasHeight", publicName: "canvasHeight", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, fieldContext: { classPropertyName: "fieldContext", publicName: "fieldContext", isSignal: true, isRequired: false, transformFunction: null }, editTemplateMode: { classPropertyName: "editTemplateMode", publicName: "editTemplateMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { cellCommit: "cellCommit", tableChange: "tableChange" }, host: { classAttribute: "contents" }, ngImport: i0, template: `
56592
57369
  @switch (true) {
56593
57370
  @case (element().type === 'connector') {
56594
57371
  <pptx-connector-renderer
@@ -56862,6 +57639,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
56862
57639
  selector: 'pptx-element-renderer',
56863
57640
  standalone: true,
56864
57641
  changeDetection: ChangeDetectionStrategy.OnPush,
57642
+ host: { class: 'contents' },
56865
57643
  imports: [
56866
57644
  NgStyle,
56867
57645
  ConnectorRendererComponent,
@@ -64622,6 +65400,112 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
64622
65400
  `, styles: [".pptx-ng-props-form{display:flex;flex-direction:column;gap:.75rem}.pptx-ng-props-field{display:flex;flex-direction:column;gap:.375rem}.pptx-ng-props-label{font-size:.75rem;font-weight:500;color:var(--pptx-foreground, #e5e5e5)}.pptx-ng-props-input{width:100%;padding:.375rem .75rem;border-radius:.375rem;border:1px solid var(--pptx-border, #2a2a2a);background:var(--pptx-background, #111);color:var(--pptx-foreground, #e5e5e5);font-size:.8125rem}.pptx-ng-props-input:focus{outline:none;border-color:var(--pptx-primary, #6366f1);box-shadow:0 0 0 1px var(--pptx-primary, #6366f1)}.pptx-ng-props-meta{display:flex;flex-direction:column;gap:.375rem;padding-top:.5rem;border-top:1px solid var(--pptx-border, #2a2a2a)}.pptx-ng-props-meta-row{display:flex;justify-content:space-between;font-size:.75rem}.pptx-ng-props-meta-label{color:var(--pptx-muted-foreground, #9a9a9a)}.pptx-ng-props-meta-value{color:var(--pptx-foreground, #e5e5e5)}.pptx-ng-props-btn{padding:.375rem .75rem;border:none;border-radius:.375rem;background:var(--pptx-muted, #2a2a2a);color:var(--pptx-foreground, #e5e5e5);font-size:.75rem;cursor:pointer}.pptx-ng-props-btn-primary{background:var(--pptx-primary, #6366f1);color:var(--pptx-primary-foreground, #fff)}\n"] }]
64623
65401
  }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], properties: [{ type: i0.Input, args: [{ isSignal: true, alias: "properties", required: true }] }], save: [{ type: i0.Output, args: ["save"] }], close: [{ type: i0.Output, args: ["close"] }] } });
64624
65402
 
65403
+ /**
65404
+ * remote-selection-overlay.component.ts: Angular port of the Vue
65405
+ * `RemoteSelectionOverlay.vue` / React `RemoteSelectionOverlay.tsx`.
65406
+ *
65407
+ * Selector: `pptx-remote-selection-overlay`
65408
+ *
65409
+ * Draws a coloured rectangle around each element a remote collaborator has
65410
+ * selected, labelled with that peer's name in their colour (Google-Slides-style
65411
+ * presence). Purely presentational: the host supplies the reactive presence
65412
+ * list (from `CollaborationService.presence`), the elements on the active slide,
65413
+ * the active slide index, and the current `zoom`. Only peers whose
65414
+ * `activeSlideIndex` matches are drawn, and only for a selected id that resolves
65415
+ * to an element on the slide.
65416
+ *
65417
+ * Element geometry is in unscaled slide coordinates (px); this component
65418
+ * multiplies by `zoom` so it can mount inside the scaled slide stage. It sets
65419
+ * `pointer-events: none` so it never intercepts canvas input.
65420
+ */
65421
+ class RemoteSelectionOverlayComponent {
65422
+ /** Remote collaborators' presence (cursor + selection + active slide). */
65423
+ presences = input([], /* @ts-ignore */
65424
+ ...(ngDevMode ? [{ debugName: "presences" }] : /* istanbul ignore next */ []));
65425
+ /** Elements on the active slide (used to resolve selected ids -> geometry). */
65426
+ elements = input([], /* @ts-ignore */
65427
+ ...(ngDevMode ? [{ debugName: "elements" }] : /* istanbul ignore next */ []));
65428
+ /** The current slide index: only peers on this slide are drawn. */
65429
+ activeSlideIndex = input(0, /* @ts-ignore */
65430
+ ...(ngDevMode ? [{ debugName: "activeSlideIndex" }] : /* istanbul ignore next */ []));
65431
+ /** Current canvas zoom factor; geometry scales by this. */
65432
+ zoom = input(1, /* @ts-ignore */
65433
+ ...(ngDevMode ? [{ debugName: "zoom" }] : /* istanbul ignore next */ []));
65434
+ elementMap = computed(() => {
65435
+ const map = new Map();
65436
+ for (const el of this.elements()) {
65437
+ map.set(el.id, el);
65438
+ }
65439
+ return map;
65440
+ }, /* @ts-ignore */
65441
+ ...(ngDevMode ? [{ debugName: "elementMap" }] : /* istanbul ignore next */ []));
65442
+ boxes = computed(() => {
65443
+ const slide = this.activeSlideIndex();
65444
+ const z = this.zoom();
65445
+ const lookup = this.elementMap();
65446
+ const result = [];
65447
+ for (const peer of this.presences()) {
65448
+ if (peer.activeSlideIndex !== slide || !peer.selectedElementId) {
65449
+ continue;
65450
+ }
65451
+ const el = lookup.get(peer.selectedElementId);
65452
+ if (!el) {
65453
+ continue;
65454
+ }
65455
+ result.push({
65456
+ key: `${peer.clientId}-${peer.selectedElementId}`,
65457
+ label: formatCursorLabel(peer.userName, MAX_LABEL_CHARS),
65458
+ color: peer.userColor,
65459
+ transform: `translate(${el.x * z}px, ${el.y * z}px)`,
65460
+ width: el.width * z,
65461
+ height: el.height * z,
65462
+ });
65463
+ }
65464
+ return result;
65465
+ }, /* @ts-ignore */
65466
+ ...(ngDevMode ? [{ debugName: "boxes" }] : /* istanbul ignore next */ []));
65467
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: RemoteSelectionOverlayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
65468
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: RemoteSelectionOverlayComponent, isStandalone: true, selector: "pptx-remote-selection-overlay", inputs: { presences: { classPropertyName: "presences", publicName: "presences", isSignal: true, isRequired: false, transformFunction: null }, elements: { classPropertyName: "elements", publicName: "elements", isSignal: true, isRequired: false, transformFunction: null }, activeSlideIndex: { classPropertyName: "activeSlideIndex", publicName: "activeSlideIndex", isSignal: true, isRequired: false, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
65469
+ <div aria-hidden="true" data-export-ignore="true">
65470
+ @for (box of boxes(); track box.key) {
65471
+ <div
65472
+ class="pptx-ng-remote-selection"
65473
+ [attr.data-element-id]="box.key"
65474
+ [style.transform]="box.transform"
65475
+ [style.width.px]="box.width"
65476
+ [style.height.px]="box.height"
65477
+ [style.color]="box.color"
65478
+ >
65479
+ <span class="pptx-ng-remote-selection-label" [style.background-color]="box.color">
65480
+ {{ box.label }}
65481
+ </span>
65482
+ </div>
65483
+ }
65484
+ </div>
65485
+ `, isInline: true, styles: [":host{position:absolute;inset:0;pointer-events:none;overflow:visible;z-index:9997}.pptx-ng-remote-selection{position:absolute;top:0;left:0;box-sizing:border-box;border:2px solid currentcolor;border-radius:2px;pointer-events:none;will-change:transform;transition:transform 90ms linear}.pptx-ng-remote-selection-label{position:absolute;top:-18px;left:-2px;max-width:150px;padding:1px 5px;border-radius:3px;color:#fff;font-family:system-ui,sans-serif;font-size:9px;font-weight:500;line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
65486
+ }
65487
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: RemoteSelectionOverlayComponent, decorators: [{
65488
+ type: Component,
65489
+ args: [{ selector: 'pptx-remote-selection-overlay', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
65490
+ <div aria-hidden="true" data-export-ignore="true">
65491
+ @for (box of boxes(); track box.key) {
65492
+ <div
65493
+ class="pptx-ng-remote-selection"
65494
+ [attr.data-element-id]="box.key"
65495
+ [style.transform]="box.transform"
65496
+ [style.width.px]="box.width"
65497
+ [style.height.px]="box.height"
65498
+ [style.color]="box.color"
65499
+ >
65500
+ <span class="pptx-ng-remote-selection-label" [style.background-color]="box.color">
65501
+ {{ box.label }}
65502
+ </span>
65503
+ </div>
65504
+ }
65505
+ </div>
65506
+ `, styles: [":host{position:absolute;inset:0;pointer-events:none;overflow:visible;z-index:9997}.pptx-ng-remote-selection{position:absolute;top:0;left:0;box-sizing:border-box;border:2px solid currentcolor;border-radius:2px;pointer-events:none;will-change:transform;transition:transform 90ms linear}.pptx-ng-remote-selection-label{position:absolute;top:-18px;left:-2px;max-width:150px;padding:1px 5px;border-radius:3px;color:#fff;font-family:system-ui,sans-serif;font-size:9px;font-weight:500;line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"] }]
65507
+ }], propDecorators: { presences: [{ type: i0.Input, args: [{ isSignal: true, alias: "presences", required: false }] }], elements: [{ type: i0.Input, args: [{ isSignal: true, alias: "elements", required: false }] }], activeSlideIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeSlideIndex", required: false }] }], zoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoom", required: false }] }] } });
65508
+
64625
65509
  /**
64626
65510
  * ribbon-animations-section.component.ts: the Animations ribbon tab (preview, the
64627
65511
  * Add Animation entrance/emphasis/exit gallery, Remove Animation, Animation
@@ -69658,24 +70542,29 @@ function seedShareFields(defaults) {
69658
70542
  serverUrl: defaults?.serverUrl ?? '',
69659
70543
  };
69660
70544
  }
69661
- /** Whether all three required fields are non-blank (after trimming). */
70545
+ /**
70546
+ * Whether the required fields are non-blank (after trimming). The server URL is
70547
+ * optional: a blank server selects the serverless (webrtc) peer-to-peer
70548
+ * transport, so only the room id and display name are required.
70549
+ */
69662
70550
  function canStartShare(fields) {
69663
- return (fields.roomId.trim().length > 0 &&
69664
- fields.userName.trim().length > 0 &&
69665
- fields.serverUrl.trim().length > 0);
70551
+ return fields.roomId.trim().length > 0 && fields.userName.trim().length > 0;
69666
70552
  }
69667
70553
  /**
69668
70554
  * Assemble a {@link CollaborationConfig} from the (trimmed) form fields, or
69669
- * `null` when the form is incomplete.
70555
+ * `null` when the form is incomplete. A blank server URL yields
70556
+ * `transport: 'webrtc'`; otherwise the default websocket transport is used.
69670
70557
  */
69671
70558
  function buildCollaborationConfig(fields) {
69672
70559
  if (!canStartShare(fields)) {
69673
70560
  return null;
69674
70561
  }
70562
+ const serverUrl = fields.serverUrl.trim();
69675
70563
  return {
69676
70564
  roomId: fields.roomId.trim(),
69677
70565
  userName: fields.userName.trim(),
69678
- serverUrl: fields.serverUrl.trim(),
70566
+ serverUrl,
70567
+ transport: resolveTransportForServerUrl(serverUrl),
69679
70568
  };
69680
70569
  }
69681
70570
  /**
@@ -69687,7 +70576,11 @@ function buildShareUrl(roomId, serverUrl, location) {
69687
70576
  return roomId;
69688
70577
  }
69689
70578
  const room = encodeURIComponent(roomId);
69690
- const server = encodeURIComponent(serverUrl);
70579
+ const trimmed = serverUrl.trim();
70580
+ if (trimmed.length === 0) {
70581
+ return `${location.origin}${location.pathname}?room=${room}&transport=webrtc`;
70582
+ }
70583
+ const server = encodeURIComponent(trimmed);
69691
70584
  return `${location.origin}${location.pathname}?room=${room}&server=${server}`;
69692
70585
  }
69693
70586
 
@@ -69723,6 +70616,9 @@ class ShareDialogComponent {
69723
70616
  /** Shareable join link surfaced while the session is active. */
69724
70617
  shareUrl = input('', /* @ts-ignore */
69725
70618
  ...(ngDevMode ? [{ debugName: "shareUrl" }] : /* istanbul ignore next */ []));
70619
+ /** Whether the active session is peer-to-peer (serverless webrtc). */
70620
+ p2p = input(false, /* @ts-ignore */
70621
+ ...(ngDevMode ? [{ debugName: "p2p" }] : /* istanbul ignore next */ []));
69726
70622
  /** Fired with the assembled config when the user starts sharing. */
69727
70623
  start = output();
69728
70624
  /** Fired when the user stops an active session. */
@@ -69743,6 +70639,9 @@ class ShareDialogComponent {
69743
70639
  serverUrl: this.serverUrl(),
69744
70640
  }), /* @ts-ignore */
69745
70641
  ...(ngDevMode ? [{ debugName: "canStart" }] : /* istanbul ignore next */ []));
70642
+ /** Blank server URL selects the serverless peer-to-peer (webrtc) transport. */
70643
+ isP2p = computed(() => this.serverUrl().trim().length === 0, /* @ts-ignore */
70644
+ ...(ngDevMode ? [{ debugName: "isP2p" }] : /* istanbul ignore next */ []));
69746
70645
  canCopy = computed(() => canUseClipboard(typeof navigator !== 'undefined' ? navigator : undefined), /* @ts-ignore */
69747
70646
  ...(ngDevMode ? [{ debugName: "canCopy" }] : /* istanbul ignore next */ []));
69748
70647
  constructor() {
@@ -69789,7 +70688,7 @@ class ShareDialogComponent {
69789
70688
  this.stop.emit();
69790
70689
  }
69791
70690
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ShareDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
69792
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: ShareDialogComponent, isStandalone: true, selector: "pptx-share-dialog", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, defaults: { classPropertyName: "defaults", publicName: "defaults", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, connected: { classPropertyName: "connected", publicName: "connected", isSignal: true, isRequired: false, transformFunction: null }, userCount: { classPropertyName: "userCount", publicName: "userCount", isSignal: true, isRequired: false, transformFunction: null }, shareUrl: { classPropertyName: "shareUrl", publicName: "shareUrl", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { start: "start", stop: "stop", close: "close" }, ngImport: i0, template: `
70691
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: ShareDialogComponent, isStandalone: true, selector: "pptx-share-dialog", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, defaults: { classPropertyName: "defaults", publicName: "defaults", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, connected: { classPropertyName: "connected", publicName: "connected", isSignal: true, isRequired: false, transformFunction: null }, userCount: { classPropertyName: "userCount", publicName: "userCount", isSignal: true, isRequired: false, transformFunction: null }, shareUrl: { classPropertyName: "shareUrl", publicName: "shareUrl", isSignal: true, isRequired: false, transformFunction: null }, p2p: { classPropertyName: "p2p", publicName: "p2p", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { start: "start", stop: "stop", close: "close" }, ngImport: i0, template: `
69793
70692
  <pptx-modal-dialog
69794
70693
  [open]="open()"
69795
70694
  [title]="(active() ? 'pptx.share.activeTitle' : 'pptx.toolbar.share') | translate"
@@ -69816,6 +70715,14 @@ class ShareDialogComponent {
69816
70715
  </span>
69817
70716
  </div>
69818
70717
 
70718
+ @if (p2p()) {
70719
+ <div class="pptx-ng-share-status-row">
70720
+ <span class="pptx-ng-share-count" style="margin-left: 0">
70721
+ {{ 'pptx.share.p2pServerValue' | translate }}
70722
+ </span>
70723
+ </div>
70724
+ }
70725
+
69819
70726
  @if (shareUrl()) {
69820
70727
  <div class="pptx-ng-share-field">
69821
70728
  <label class="pptx-ng-share-label">{{
@@ -69892,6 +70799,9 @@ class ShareDialogComponent {
69892
70799
  [value]="serverUrl()"
69893
70800
  (input)="serverUrl.set(asValue($event))"
69894
70801
  />
70802
+ @if (isP2p()) {
70803
+ <p class="pptx-ng-share-hint">{{ 'pptx.share.p2pHint' | translate }}</p>
70804
+ }
69895
70805
  </div>
69896
70806
  </div>
69897
70807
  }
@@ -69943,6 +70853,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
69943
70853
  </span>
69944
70854
  </div>
69945
70855
 
70856
+ @if (p2p()) {
70857
+ <div class="pptx-ng-share-status-row">
70858
+ <span class="pptx-ng-share-count" style="margin-left: 0">
70859
+ {{ 'pptx.share.p2pServerValue' | translate }}
70860
+ </span>
70861
+ </div>
70862
+ }
70863
+
69946
70864
  @if (shareUrl()) {
69947
70865
  <div class="pptx-ng-share-field">
69948
70866
  <label class="pptx-ng-share-label">{{
@@ -70019,6 +70937,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
70019
70937
  [value]="serverUrl()"
70020
70938
  (input)="serverUrl.set(asValue($event))"
70021
70939
  />
70940
+ @if (isP2p()) {
70941
+ <p class="pptx-ng-share-hint">{{ 'pptx.share.p2pHint' | translate }}</p>
70942
+ }
70022
70943
  </div>
70023
70944
  </div>
70024
70945
  }
@@ -70040,7 +70961,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
70040
70961
  </div>
70041
70962
  </pptx-modal-dialog>
70042
70963
  `, styles: [".pptx-ng-share-form,.pptx-ng-share-active{display:flex;flex-direction:column;gap:1rem}.pptx-ng-share-desc{margin:0;font-size:.8125rem;color:var(--pptx-muted-foreground, #9a9a9a)}.pptx-ng-share-field{display:flex;flex-direction:column;gap:.375rem}.pptx-ng-share-label{font-size:.75rem;font-weight:500;color:var(--pptx-foreground, #e5e5e5)}.pptx-ng-share-input{width:100%;padding:.375rem .75rem;border-radius:.375rem;border:1px solid var(--pptx-border, #2a2a2a);background:var(--pptx-background, #111);color:var(--pptx-foreground, #e5e5e5);font-size:.8125rem}.pptx-ng-share-input:focus{outline:none;border-color:var(--pptx-primary, #6366f1);box-shadow:0 0 0 1px var(--pptx-primary, #6366f1)}.pptx-ng-share-btn{padding:.375rem .75rem;border:none;border-radius:.375rem;background:var(--pptx-muted, #2a2a2a);color:var(--pptx-foreground, #e5e5e5);font-size:.75rem;cursor:pointer}.pptx-ng-share-btn-primary{background:var(--pptx-primary, #6366f1);color:var(--pptx-primary-foreground, #fff)}.pptx-ng-share-btn-primary:disabled{opacity:.4;cursor:not-allowed}.pptx-ng-share-stop{width:100%;padding:.5rem .75rem;border:1px solid rgba(239,68,68,.3);border-radius:.375rem;background:#ef44441a;color:#f87171;font-size:.75rem;font-weight:500;cursor:pointer}.pptx-ng-share-stop:hover{background:#ef444433}.pptx-ng-share-status-row{display:flex;align-items:center;gap:.5rem;font-size:.8125rem}.pptx-ng-share-status-dot{width:.5rem;height:.5rem;border-radius:9999px;background:var(--pptx-muted-foreground, #9a9a9a)}.pptx-ng-share-status-dot.is-on{background:#22c55e}.pptx-ng-share-status-text{font-weight:500;color:var(--pptx-foreground, #e5e5e5)}.pptx-ng-share-count{margin-left:auto;color:var(--pptx-muted-foreground, #9a9a9a)}.pptx-ng-share-link-row{display:flex;align-items:center;gap:.5rem}.pptx-ng-share-hint{margin:0;font-size:.6875rem;color:var(--pptx-muted-foreground, #9a9a9a)}\n"] }]
70043
- }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], defaults: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaults", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], connected: [{ type: i0.Input, args: [{ isSignal: true, alias: "connected", required: false }] }], userCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "userCount", required: false }] }], shareUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "shareUrl", required: false }] }], start: [{ type: i0.Output, args: ["start"] }], stop: [{ type: i0.Output, args: ["stop"] }], close: [{ type: i0.Output, args: ["close"] }] } });
70964
+ }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], defaults: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaults", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], connected: [{ type: i0.Input, args: [{ isSignal: true, alias: "connected", required: false }] }], userCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "userCount", required: false }] }], shareUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "shareUrl", required: false }] }], p2p: [{ type: i0.Input, args: [{ isSignal: true, alias: "p2p", required: false }] }], start: [{ type: i0.Output, args: ["start"] }], stop: [{ type: i0.Output, args: ["stop"] }], close: [{ type: i0.Output, args: ["close"] }] } });
70044
70965
 
70045
70966
  /**
70046
70967
  * signatures-helpers.ts: Pure functions for digital-signature status
@@ -71562,6 +72483,34 @@ class ViewerCollaborationSessionService {
71562
72483
  : '';
71563
72484
  }, /* @ts-ignore */
71564
72485
  ...(ngDevMode ? [{ debugName: "broadcastViewerUrl" }] : /* istanbul ignore next */ []));
72486
+ /** Whether the active session is serverless (peer-to-peer / webrtc). */
72487
+ activeSessionP2p = computed(() => (this.activeSession()?.serverUrl ?? '').trim().length === 0 && this.collab.active(), /* @ts-ignore */
72488
+ ...(ngDevMode ? [{ debugName: "activeSessionP2p" }] : /* istanbul ignore next */ []));
72489
+ /**
72490
+ * Assemble the {@link ConnectOptions} shared by every connect call site: apply
72491
+ * remote slides to the editor, size the presence bounds to the canvas, and
72492
+ * expose the source bytes + separated template elements for write-back.
72493
+ */
72494
+ connectOptions() {
72495
+ const host = this.requireHost();
72496
+ const size = host.canvasSize();
72497
+ return {
72498
+ onRemoteSlides: (slides) => host.applyRemoteSlides(slides),
72499
+ canvasWidth: size.width,
72500
+ canvasHeight: size.height,
72501
+ getSourceBytes: () => host.getSourceBytes(),
72502
+ getTemplateElements: () => host.getTemplateElements(),
72503
+ };
72504
+ }
72505
+ /**
72506
+ * Connect and immediately seed the sync baseline with the current deck, so a
72507
+ * joiner whose deck is still a placeholder never broadcasts (and overwrites)
72508
+ * the shared document before the first remote sync arrives.
72509
+ */
72510
+ connectWithBaseline(config) {
72511
+ void this.collab.connect(config, this.connectOptions());
72512
+ this.collab.seedBaseline(this.requireHost().currentSlides());
72513
+ }
71565
72514
  /**
71566
72515
  * Seed values for the Share dialog: the host-supplied `shareDefaults`, with
71567
72516
  * `userName` falling back to `authorName` (then "You") so the local user's
@@ -71583,9 +72532,7 @@ class ViewerCollaborationSessionService {
71583
72532
  syncHostConfig(config) {
71584
72533
  if (config) {
71585
72534
  this.activeSession.set({ roomId: config.roomId, serverUrl: config.serverUrl });
71586
- void this.collab.connect(config, {
71587
- getTemplateElements: () => this.requireHost().getTemplateElements(),
71588
- });
72535
+ this.connectWithBaseline(config);
71589
72536
  }
71590
72537
  else {
71591
72538
  this.collab.disconnect();
@@ -71605,9 +72552,7 @@ class ViewerCollaborationSessionService {
71605
72552
  roomId: collaboratorConfig.roomId,
71606
72553
  serverUrl: collaboratorConfig.serverUrl,
71607
72554
  });
71608
- void this.collab.connect(collaboratorConfig, {
71609
- getTemplateElements: () => host.getTemplateElements(),
71610
- });
72555
+ this.connectWithBaseline(collaboratorConfig);
71611
72556
  host.emitStart(collaboratorConfig);
71612
72557
  }
71613
72558
  onShareStop() {
@@ -71621,13 +72566,12 @@ class ViewerCollaborationSessionService {
71621
72566
  const collabConfig = {
71622
72567
  roomId: config.roomId,
71623
72568
  serverUrl: config.serverUrl,
72569
+ transport: config.transport,
71624
72570
  userName: host.authorName() ?? 'Presenter',
71625
72571
  role: 'owner',
71626
72572
  };
71627
72573
  this.activeSession.set({ roomId: config.roomId, serverUrl: config.serverUrl });
71628
- void this.collab.connect(collabConfig, {
71629
- getTemplateElements: () => host.getTemplateElements(),
71630
- });
72574
+ this.connectWithBaseline(collabConfig);
71631
72575
  host.emitStart(collabConfig);
71632
72576
  }
71633
72577
  onBroadcastStop() {
@@ -73298,7 +74242,7 @@ class FontEmbeddingListComponent {
73298
74242
  missingCount = input(0, /* @ts-ignore */
73299
74243
  ...(ngDevMode ? [{ debugName: "missingCount" }] : /* istanbul ignore next */ []));
73300
74244
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: FontEmbeddingListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
73301
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: FontEmbeddingListComponent, isStandalone: true, selector: "pptx-font-embedding-list", inputs: { usedFontFamilies: { classPropertyName: "usedFontFamilies", publicName: "usedFontFamilies", isSignal: true, isRequired: false, transformFunction: null }, availableFamilies: { classPropertyName: "availableFamilies", publicName: "availableFamilies", isSignal: true, isRequired: false, transformFunction: null }, embeddedSet: { classPropertyName: "embeddedSet", publicName: "embeddedSet", isSignal: true, isRequired: false, transformFunction: null }, scanning: { classPropertyName: "scanning", publicName: "scanning", isSignal: true, isRequired: false, transformFunction: null }, missingCount: { classPropertyName: "missingCount", publicName: "missingCount", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
74245
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: FontEmbeddingListComponent, isStandalone: true, selector: "pptx-font-embedding-list", inputs: { usedFontFamilies: { classPropertyName: "usedFontFamilies", publicName: "usedFontFamilies", isSignal: true, isRequired: false, transformFunction: null }, availableFamilies: { classPropertyName: "availableFamilies", publicName: "availableFamilies", isSignal: true, isRequired: false, transformFunction: null }, embeddedSet: { classPropertyName: "embeddedSet", publicName: "embeddedSet", isSignal: true, isRequired: false, transformFunction: null }, scanning: { classPropertyName: "scanning", publicName: "scanning", isSignal: true, isRequired: false, transformFunction: null }, missingCount: { classPropertyName: "missingCount", publicName: "missingCount", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "contents" }, ngImport: i0, template: `
73302
74246
  <div class="pptx-ng-fonts-section">
73303
74247
  <h3 class="pptx-ng-fonts-section-title">
73304
74248
  {{ 'pptx.fontEmbedding.usedFonts' | translate: { count: usedFontFamilies().length } }}
@@ -73347,7 +74291,7 @@ class FontEmbeddingListComponent {
73347
74291
  }
73348
74292
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: FontEmbeddingListComponent, decorators: [{
73349
74293
  type: Component,
73350
- args: [{ selector: 'pptx-font-embedding-list', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: `
74294
+ args: [{ selector: 'pptx-font-embedding-list', standalone: true, imports: [TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'contents' }, template: `
73351
74295
  <div class="pptx-ng-fonts-section">
73352
74296
  <h3 class="pptx-ng-fonts-section-title">
73353
74297
  {{ 'pptx.fontEmbedding.usedFonts' | translate: { count: usedFontFamilies().length } }}
@@ -75218,7 +76162,7 @@ class ViewerExtraDialogsComponent {
75218
76162
  this.svc.showPassword.set(false);
75219
76163
  }
75220
76164
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: ViewerExtraDialogsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
75221
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.5", type: ViewerExtraDialogsComponent, isStandalone: true, selector: "pptx-viewer-extra-dialogs", inputs: { activeSlideIndex: { classPropertyName: "activeSlideIndex", publicName: "activeSlideIndex", isSignal: true, isRequired: false, transformFunction: null }, selectedElementId: { classPropertyName: "selectedElementId", publicName: "selectedElementId", isSignal: true, isRequired: false, transformFunction: null }, filePath: { classPropertyName: "filePath", publicName: "filePath", isSignal: true, isRequired: false, transformFunction: null }, customShows: { classPropertyName: "customShows", publicName: "customShows", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { restoreContent: "restoreContent" }, ngImport: i0, template: `
76165
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.5", type: ViewerExtraDialogsComponent, isStandalone: true, selector: "pptx-viewer-extra-dialogs", inputs: { activeSlideIndex: { classPropertyName: "activeSlideIndex", publicName: "activeSlideIndex", isSignal: true, isRequired: false, transformFunction: null }, selectedElementId: { classPropertyName: "selectedElementId", publicName: "selectedElementId", isSignal: true, isRequired: false, transformFunction: null }, filePath: { classPropertyName: "filePath", publicName: "filePath", isSignal: true, isRequired: false, transformFunction: null }, customShows: { classPropertyName: "customShows", publicName: "customShows", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { restoreContent: "restoreContent" }, host: { classAttribute: "contents" }, ngImport: i0, template: `
75222
76166
  <pptx-equation-editor-dialog
75223
76167
  [open]="svc.showEquation()"
75224
76168
  [existingOmml]="svc.editingEquationOmml()"
@@ -75299,6 +76243,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
75299
76243
  selector: 'pptx-viewer-extra-dialogs',
75300
76244
  standalone: true,
75301
76245
  changeDetection: ChangeDetectionStrategy.OnPush,
76246
+ host: { class: 'contents' },
75302
76247
  imports: [
75303
76248
  EquationEditorDialogComponent,
75304
76249
  SetUpSlideShowDialogComponent,
@@ -76033,6 +76978,14 @@ class PowerPointViewerComponent {
76033
76978
  ...(ngDevMode ? [{ debugName: "zoom" }] : /* istanbul ignore next */ []));
76034
76979
  zoomPercent = computed(() => Math.round(this.zoom() * 100), /* @ts-ignore */
76035
76980
  ...(ngDevMode ? [{ debugName: "zoomPercent" }] : /* istanbul ignore next */ []));
76981
+ /**
76982
+ * Remote cursors filtered to the slide the local user is viewing, so peers'
76983
+ * cursors only appear on the shared slide (mirrors React/Vue).
76984
+ */
76985
+ collabCursors = computed(() => presenceToCursors(this.collab.presence(), this.activeSlideIndex()), /* @ts-ignore */
76986
+ ...(ngDevMode ? [{ debugName: "collabCursors" }] : /* istanbul ignore next */ []));
76987
+ /** Timestamp of the last cursor broadcast (throttle gate). */
76988
+ lastCursorBroadcast = 0;
76036
76989
  /** Fullscreen presentation-mode overlay visibility. */
76037
76990
  presenting = signal(false, /* @ts-ignore */
76038
76991
  ...(ngDevMode ? [{ debugName: "presenting" }] : /* istanbul ignore next */ []));
@@ -76297,6 +77250,47 @@ class PowerPointViewerComponent {
76297
77250
  effect(() => {
76298
77251
  this.session.syncHostConfig(this.collaboration());
76299
77252
  });
77253
+ // Push local slide edits into the shared Y.Doc (reconcile-based; the
77254
+ // service guards against echoing remote-applied changes and against
77255
+ // clobbering with an empty deck). A broadcast `viewer` never writes, so a
77256
+ // follow-along joiner cannot overwrite the presenter's deck.
77257
+ effect(() => {
77258
+ const slides = this.editor.slides();
77259
+ if (this.collab.active() && this.collab.activeRole() !== 'viewer') {
77260
+ this.collab.broadcastSlides(slides);
77261
+ }
77262
+ });
77263
+ // Publish the local selection so peers can draw remote selection boxes.
77264
+ effect(() => {
77265
+ const ids = this.editor.selectedIds();
77266
+ if (this.collab.active()) {
77267
+ this.collab.setSelection(ids[0], this.activeSlideIndex());
77268
+ }
77269
+ });
77270
+ // Publish the local active slide so followers navigate with us.
77271
+ effect(() => {
77272
+ const index = this.activeSlideIndex();
77273
+ if (this.collab.active()) {
77274
+ this.collab.setActiveSlide(index);
77275
+ }
77276
+ });
77277
+ // Follow mode: mirror the followed peer's active slide.
77278
+ effect(() => {
77279
+ const target = this.collab.followedSlideIndex();
77280
+ if (target !== null) {
77281
+ this.goTo(target);
77282
+ }
77283
+ });
77284
+ // Broadcast auto-follow: a `viewer` tracks the broadcaster (owner) peer.
77285
+ effect(() => {
77286
+ if (this.collab.activeRole() !== 'viewer') {
77287
+ return;
77288
+ }
77289
+ const target = this.collab.broadcasterSlideIndex();
77290
+ if (target !== null) {
77291
+ this.goTo(target);
77292
+ }
77293
+ });
76300
77294
  // Hand the export/print orchestrator the live navigation signal + deck
76301
77295
  // accessors + stage resolver so it can flip the stage and capture slides.
76302
77296
  this.xport.bind({
@@ -76318,6 +77312,10 @@ class PowerPointViewerComponent {
76318
77312
  authorName: () => this.authorName(),
76319
77313
  shareDefaults: () => this.shareDefaults(),
76320
77314
  getTemplateElements: () => this.editor.templateElementsBySlideId(),
77315
+ applyRemoteSlides: (slides) => this.editor.applyRemoteSlides(slides),
77316
+ canvasSize: () => this.loader.canvasSize(),
77317
+ getSourceBytes: () => this.currentSourceBytes(),
77318
+ currentSlides: () => this.editor.slides(),
76321
77319
  emitStart: (config) => this.startCollaboration.emit(config),
76322
77320
  emitStop: () => this.stopCollaboration.emit(),
76323
77321
  });
@@ -76373,6 +77371,40 @@ class PowerPointViewerComponent {
76373
77371
  goNext() {
76374
77372
  this.goTo(this.activeSlideIndex() + 1);
76375
77373
  }
77374
+ /**
77375
+ * Publish the local cursor while the pointer moves over the canvas. Throttled
77376
+ * to {@link BROADCAST_THROTTLE_MS}; coordinates are mapped from client space
77377
+ * into unscaled slide space (dividing by zoom, matching the cursor overlay)
77378
+ * and clamped to the canvas bounds.
77379
+ */
77380
+ onCollabPointerMove(event) {
77381
+ if (!this.collab.active()) {
77382
+ return;
77383
+ }
77384
+ const now = Date.now();
77385
+ if (now - this.lastCursorBroadcast < BROADCAST_THROTTLE_MS) {
77386
+ return;
77387
+ }
77388
+ this.lastCursorBroadcast = now;
77389
+ const host = this.mainEl()?.nativeElement;
77390
+ if (!host) {
77391
+ return;
77392
+ }
77393
+ const rect = host.getBoundingClientRect();
77394
+ const zoom = this.zoom() || 1;
77395
+ const size = this.loader.canvasSize();
77396
+ const x = clampCursorPosition((event.clientX - rect.left) / zoom, 0, size.width);
77397
+ const y = clampCursorPosition((event.clientY - rect.top) / zoom, 0, size.height);
77398
+ this.collab.setCursor(x, y, this.activeSlideIndex());
77399
+ }
77400
+ /** The loaded source `.pptx` bytes (for elected-writer write-back), if any. */
77401
+ currentSourceBytes() {
77402
+ const content = this.contentOverride() ?? this.content();
77403
+ if (!content) {
77404
+ return null;
77405
+ }
77406
+ return content instanceof Uint8Array ? content : new Uint8Array(content);
77407
+ }
76376
77408
  // ── Theme gallery (Design tab) ─────────────────────────────────────────────
76377
77409
  /**
76378
77410
  * Apply a built-in theme preset to the whole deck.
@@ -77010,7 +78042,7 @@ class PowerPointViewerComponent {
77010
78042
  </nav>
77011
78043
  }
77012
78044
 
77013
- <main class="pptx-ng-main" #mainEl>
78045
+ <main class="pptx-ng-main" #mainEl (pointermove)="onCollabPointerMove($event)">
77014
78046
  <pptx-slide-canvas
77015
78047
  [slide]="activeSlide()"
77016
78048
  [canvasSize]="loader.canvasSize()"
@@ -77048,7 +78080,22 @@ class PowerPointViewerComponent {
77048
78080
  (tableChange)="onTableChange($event)"
77049
78081
  />
77050
78082
  @if (collab.connected()) {
77051
- <pptx-collaboration-cursors [cursors]="collab.cursors()" [zoom]="zoom()" />
78083
+ <pptx-collaboration-cursors [cursors]="collabCursors()" [zoom]="zoom()" />
78084
+ <pptx-remote-selection-overlay
78085
+ [presences]="collab.presence()"
78086
+ [elements]="activeSlide()?.elements ?? []"
78087
+ [activeSlideIndex]="activeSlideIndex()"
78088
+ [zoom]="zoom()"
78089
+ />
78090
+ }
78091
+ @if (collab.active() && collab.presence().length > 0) {
78092
+ <div class="pptx-ng-collab-follow">
78093
+ <pptx-follow-mode-bar
78094
+ [presences]="collab.presence()"
78095
+ [followedClientId]="collab.followedClientId()"
78096
+ (follow)="collab.followUser($event)"
78097
+ />
78098
+ </div>
77052
78099
  }
77053
78100
  @if (showNotes() && !mobile.isMobile()) {
77054
78101
  <aside class="pptx-ng-notes" [attr.aria-label]="'pptx.notes.speakerNotes' | translate">
@@ -77314,6 +78361,7 @@ class PowerPointViewerComponent {
77314
78361
  [connected]="collab.connected()"
77315
78362
  [userCount]="collab.connectedCount()"
77316
78363
  [shareUrl]="session.shareUrl()"
78364
+ [p2p]="session.activeSessionP2p()"
77317
78365
  [defaults]="session.shareDialogDefaults()"
77318
78366
  (start)="session.onShareStart($event)"
77319
78367
  (stop)="session.onShareStop()"
@@ -77326,6 +78374,7 @@ class PowerPointViewerComponent {
77326
78374
  [connected]="collab.connected()"
77327
78375
  [viewerCount]="collab.presence().length"
77328
78376
  [viewerUrl]="session.broadcastViewerUrl()"
78377
+ [p2p]="session.activeSessionP2p()"
77329
78378
  [defaults]="{ serverUrl: shareDefaults()?.serverUrl }"
77330
78379
  (start)="session.onBroadcastStart($event)"
77331
78380
  (stop)="session.onBroadcastStop()"
@@ -77433,7 +78482,7 @@ class PowerPointViewerComponent {
77433
78482
  />
77434
78483
  }
77435
78484
  </div>
77436
- `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: SlideCanvasComponent, selector: "pptx-slide-canvas", inputs: ["slide", "canvasSize", "mediaDataUrls", "zoom", "editable", "showGrid", "showRulers", "showGuides", "snapToGrid", "snapToGuides", "autoFit", "interactive", "selectedIds", "editingId", "editTemplateMode", "templateElements", "drawTool", "drawColor", "drawWidth"], outputs: ["elementSelect", "backgroundClick", "transformStart", "transformUpdate", "contextMenu", "textEditStart", "textCommit", "textCancel", "textFormat", "rotateUpdate", "marqueeSelect", "inkStrokeComplete", "eraserHit", "cellCommit", "tableChange"] }, { kind: "component", type: PresentationOverlayComponent, selector: "pptx-presentation-overlay", inputs: ["slides", "canvasSize", "mediaDataUrls", "startIndex"], outputs: ["indexChange", "closed", "annotationsExit"] }, { kind: "component", type: PresenterViewComponent, selector: "pptx-presenter-view", inputs: ["slides", "currentSlideIndex", "canvasSize", "templateElements", "mediaDataUrls", "presentationStartTime", "isAudienceWindowOpen"], outputs: ["movePresentationSlide", "exit", "openAudienceWindow", "closeAudienceWindow"] }, { kind: "component", type: MobilePresenterViewComponent, selector: "pptx-mobile-presenter-view", inputs: ["slides", "currentSlideIndex", "canvasSize", "templateElements", "mediaDataUrls", "presentationStartTime"], outputs: ["movePresentationSlide", "exit"] }, { kind: "component", type: SlideSorterOverlayComponent, selector: "pptx-slide-sorter-overlay", inputs: ["slides", "canvasSize", "mediaDataUrls", "activeIndex"], outputs: ["select", "closed"] }, { kind: "component", type: FindBarComponent, selector: "pptx-find-bar", inputs: ["slides"], outputs: ["navigate", "closed"] }, { kind: "component", type: FindReplaceBarComponent, selector: "pptx-find-replace-bar", inputs: ["matchCount", "matchIndex"], outputs: ["find", "navigate", "replaceOne", "replaceAll", "close"] }, { kind: "component", type: InspectorPanelComponent, selector: "pptx-inspector-panel", inputs: ["element", "slideIndex"] }, { kind: "component", type: SlidesPanelComponent, selector: "pptx-slides-panel", inputs: ["canvasSize", "mediaDataUrls", "activeIndex"], outputs: ["select"] }, { kind: "component", type: StatusBarComponent, selector: "pptx-status-bar", inputs: ["slideIndex", "slideCount", "canEdit", "dirty", "notesOpen", "zoomPercent", "sorterActive", "presenting"], outputs: ["toggleNotes", "normalView", "openSorter", "slideShow", "zoomIn", "zoomOut", "zoomReset"] }, { kind: "component", type: EditorContextMenuComponent, selector: "pptx-editor-context-menu", inputs: ["x", "y", "slideIndex"], outputs: ["closed"] }, { kind: "component", type: ExportProgressModalComponent, selector: "pptx-export-progress-modal", inputs: ["open", "title", "progress", "statusMessage"], outputs: ["cancel"] }, { kind: "component", type: CommentsPanelComponent, selector: "pptx-comments-panel", inputs: ["comments", "authorName"], outputs: ["add", "remove", "resolve"] }, { kind: "component", type: SignaturesPanelComponent, selector: "pptx-signatures-panel", inputs: ["signatures"] }, { kind: "component", type: AccessibilityPanelComponent, selector: "pptx-accessibility-panel", inputs: ["issues"], outputs: ["selectSlide"] }, { kind: "component", type: CollaborationCursorsComponent, selector: "pptx-collaboration-cursors", inputs: ["cursors", "zoom"] }, { kind: "component", type: PropertiesDialogComponent, selector: "pptx-properties-dialog", inputs: ["open", "properties"], outputs: ["save", "close"] }, { kind: "component", type: HyperlinkDialogComponent, selector: "pptx-hyperlink-dialog", inputs: ["open", "element"], outputs: ["save", "close"] }, { kind: "component", type: PrintDialogComponent, selector: "pptx-print-dialog", inputs: ["slides", "activeSlideIndex", "defaultSlidesPerPage", "defaultFrameSlides"], outputs: ["print", "cancel"] }, { kind: "component", type: ShareDialogComponent, selector: "pptx-share-dialog", inputs: ["open", "defaults", "active", "connected", "userCount", "shareUrl"], outputs: ["start", "stop", "close"] }, { kind: "component", type: BroadcastDialogComponent, selector: "pptx-broadcast-dialog", inputs: ["open", "defaults", "active", "connected", "viewerCount", "viewerUrl"], outputs: ["start", "stop", "close"] }, { kind: "component", type: MobileBottomBarComponent, selector: "pptx-mobile-bottom-bar", inputs: ["slideCount", "commentCount", "activeSheet"], outputs: ["openSlides", "insert", "openFormat", "openComments", "notes"] }, { kind: "component", type: MobileMenuSheetComponent, selector: "pptx-mobile-menu-sheet", inputs: ["open", "slideCount", "exporting", "showNotes", "canEdit"], outputs: ["closed", "openFind", "openSorter", "toggleNotes", "insertText", "present", "openFile", "savePptx", "exportPng", "exportPdf", "exportGif", "exportVideo", "print"] }, { kind: "component", type: MobileSlidesSheetComponent, selector: "pptx-mobile-slides-sheet", inputs: ["open", "slides", "canvasSize", "mediaDataUrls", "activeIndex"], outputs: ["closed", "jumpToSlide"] }, { kind: "component", type: MobileToolbarComponent, selector: "pptx-mobile-toolbar", inputs: ["canUndo", "canRedo", "canPresent", "canEdit", "menuOpen"], outputs: ["toggleMenu", "undo", "redo", "save", "present"] }, { kind: "component", type: NotesPanelComponent, selector: "pptx-notes-panel", inputs: ["slide"], outputs: ["update"] }, { kind: "component", type: RibbonComponent, selector: "pptx-ribbon", inputs: ["slideIndex", "slideCount", "canEdit", "selectedElement", "zoomPercent", "formatPainterActive", "canActivateFormatPainter", "exporting", "showGrid", "showRulers", "showGuides", "snapToGrid", "eyedropperActive", "themeGalleryOpen", "sidebarCollapsed", "inspectorOpen", "commentsOpen", "commentCount", "findOpen", "collabConnected", "connectedCount"], outputs: ["prev", "next", "zoomIn", "zoomOut", "zoomReset", "find", "present", "presenter", "share", "broadcast", "openFile", "save", "toggleSidebar", "signatures", "info", "print", "comments", "a11y", "link", "openSorter", "toggleNotes", "toggleFormatPainter", "exportPng", "exportPdf", "exportGif", "exportVideo", "replace", "toggleInspector", "drawToolChange", "toggleThemeGallery", "toggleGrid", "toggleRulers", "toggleGuides", "toggleSelectionPane", "openCustomShows", "toggleSnapToGrid", "toggleEyedropper", "openSmartArtDialog", "openEquationDialog", "openSetUpSlideShow", "openCompare", "openPassword", "openFontEmbedding", "openVersionHistory", "openShortcuts"] }, { kind: "component", type: ThemeGalleryComponent, selector: "pptx-theme-gallery", inputs: ["open", "activeName"], outputs: ["applyTheme", "close"] }, { kind: "component", type: SelectionPaneComponent, selector: "pptx-selection-pane", inputs: ["elements", "selectedIds"], outputs: ["selectElement", "bringForward", "sendBackward", "toggleHidden"] }, { kind: "component", type: CustomShowsComponent, selector: "pptx-custom-shows", inputs: ["open", "slides", "customShows", "activeCustomShowId"], outputs: ["create", "remove", "update", "setActive", "close"] }, { kind: "component", type: InsertSmartArtDialogComponent, selector: "pptx-insert-smart-art-dialog", inputs: ["open"], outputs: ["close", "insert"] }, { kind: "component", type: ViewerExtraDialogsComponent, selector: "pptx-viewer-extra-dialogs", inputs: ["activeSlideIndex", "selectedElementId", "filePath", "customShows"], outputs: ["restoreContent"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
78485
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: SlideCanvasComponent, selector: "pptx-slide-canvas", inputs: ["slide", "canvasSize", "mediaDataUrls", "zoom", "editable", "showGrid", "showRulers", "showGuides", "snapToGrid", "snapToGuides", "autoFit", "interactive", "selectedIds", "editingId", "editTemplateMode", "templateElements", "drawTool", "drawColor", "drawWidth"], outputs: ["elementSelect", "backgroundClick", "transformStart", "transformUpdate", "contextMenu", "textEditStart", "textCommit", "textCancel", "textFormat", "rotateUpdate", "marqueeSelect", "inkStrokeComplete", "eraserHit", "cellCommit", "tableChange"] }, { kind: "component", type: PresentationOverlayComponent, selector: "pptx-presentation-overlay", inputs: ["slides", "canvasSize", "mediaDataUrls", "startIndex"], outputs: ["indexChange", "closed", "annotationsExit"] }, { kind: "component", type: PresenterViewComponent, selector: "pptx-presenter-view", inputs: ["slides", "currentSlideIndex", "canvasSize", "templateElements", "mediaDataUrls", "presentationStartTime", "isAudienceWindowOpen"], outputs: ["movePresentationSlide", "exit", "openAudienceWindow", "closeAudienceWindow"] }, { kind: "component", type: MobilePresenterViewComponent, selector: "pptx-mobile-presenter-view", inputs: ["slides", "currentSlideIndex", "canvasSize", "templateElements", "mediaDataUrls", "presentationStartTime"], outputs: ["movePresentationSlide", "exit"] }, { kind: "component", type: SlideSorterOverlayComponent, selector: "pptx-slide-sorter-overlay", inputs: ["slides", "canvasSize", "mediaDataUrls", "activeIndex"], outputs: ["select", "closed"] }, { kind: "component", type: FindBarComponent, selector: "pptx-find-bar", inputs: ["slides"], outputs: ["navigate", "closed"] }, { kind: "component", type: FindReplaceBarComponent, selector: "pptx-find-replace-bar", inputs: ["matchCount", "matchIndex"], outputs: ["find", "navigate", "replaceOne", "replaceAll", "close"] }, { kind: "component", type: InspectorPanelComponent, selector: "pptx-inspector-panel", inputs: ["element", "slideIndex"] }, { kind: "component", type: SlidesPanelComponent, selector: "pptx-slides-panel", inputs: ["canvasSize", "mediaDataUrls", "activeIndex"], outputs: ["select"] }, { kind: "component", type: StatusBarComponent, selector: "pptx-status-bar", inputs: ["slideIndex", "slideCount", "canEdit", "dirty", "notesOpen", "zoomPercent", "sorterActive", "presenting"], outputs: ["toggleNotes", "normalView", "openSorter", "slideShow", "zoomIn", "zoomOut", "zoomReset"] }, { kind: "component", type: EditorContextMenuComponent, selector: "pptx-editor-context-menu", inputs: ["x", "y", "slideIndex"], outputs: ["closed"] }, { kind: "component", type: ExportProgressModalComponent, selector: "pptx-export-progress-modal", inputs: ["open", "title", "progress", "statusMessage"], outputs: ["cancel"] }, { kind: "component", type: CommentsPanelComponent, selector: "pptx-comments-panel", inputs: ["comments", "authorName"], outputs: ["add", "remove", "resolve"] }, { kind: "component", type: SignaturesPanelComponent, selector: "pptx-signatures-panel", inputs: ["signatures"] }, { kind: "component", type: AccessibilityPanelComponent, selector: "pptx-accessibility-panel", inputs: ["issues"], outputs: ["selectSlide"] }, { kind: "component", type: CollaborationCursorsComponent, selector: "pptx-collaboration-cursors", inputs: ["cursors", "zoom"] }, { kind: "component", type: RemoteSelectionOverlayComponent, selector: "pptx-remote-selection-overlay", inputs: ["presences", "elements", "activeSlideIndex", "zoom"] }, { kind: "component", type: FollowModeBarComponent, selector: "pptx-follow-mode-bar", inputs: ["presences", "followedClientId"], outputs: ["follow"] }, { kind: "component", type: PropertiesDialogComponent, selector: "pptx-properties-dialog", inputs: ["open", "properties"], outputs: ["save", "close"] }, { kind: "component", type: HyperlinkDialogComponent, selector: "pptx-hyperlink-dialog", inputs: ["open", "element"], outputs: ["save", "close"] }, { kind: "component", type: PrintDialogComponent, selector: "pptx-print-dialog", inputs: ["slides", "activeSlideIndex", "defaultSlidesPerPage", "defaultFrameSlides"], outputs: ["print", "cancel"] }, { kind: "component", type: ShareDialogComponent, selector: "pptx-share-dialog", inputs: ["open", "defaults", "active", "connected", "userCount", "shareUrl", "p2p"], outputs: ["start", "stop", "close"] }, { kind: "component", type: BroadcastDialogComponent, selector: "pptx-broadcast-dialog", inputs: ["open", "defaults", "active", "connected", "viewerCount", "viewerUrl", "p2p"], outputs: ["start", "stop", "close"] }, { kind: "component", type: MobileBottomBarComponent, selector: "pptx-mobile-bottom-bar", inputs: ["slideCount", "commentCount", "activeSheet"], outputs: ["openSlides", "insert", "openFormat", "openComments", "notes"] }, { kind: "component", type: MobileMenuSheetComponent, selector: "pptx-mobile-menu-sheet", inputs: ["open", "slideCount", "exporting", "showNotes", "canEdit"], outputs: ["closed", "openFind", "openSorter", "toggleNotes", "insertText", "present", "openFile", "savePptx", "exportPng", "exportPdf", "exportGif", "exportVideo", "print"] }, { kind: "component", type: MobileSlidesSheetComponent, selector: "pptx-mobile-slides-sheet", inputs: ["open", "slides", "canvasSize", "mediaDataUrls", "activeIndex"], outputs: ["closed", "jumpToSlide"] }, { kind: "component", type: MobileToolbarComponent, selector: "pptx-mobile-toolbar", inputs: ["canUndo", "canRedo", "canPresent", "canEdit", "menuOpen"], outputs: ["toggleMenu", "undo", "redo", "save", "present"] }, { kind: "component", type: NotesPanelComponent, selector: "pptx-notes-panel", inputs: ["slide"], outputs: ["update"] }, { kind: "component", type: RibbonComponent, selector: "pptx-ribbon", inputs: ["slideIndex", "slideCount", "canEdit", "selectedElement", "zoomPercent", "formatPainterActive", "canActivateFormatPainter", "exporting", "showGrid", "showRulers", "showGuides", "snapToGrid", "eyedropperActive", "themeGalleryOpen", "sidebarCollapsed", "inspectorOpen", "commentsOpen", "commentCount", "findOpen", "collabConnected", "connectedCount"], outputs: ["prev", "next", "zoomIn", "zoomOut", "zoomReset", "find", "present", "presenter", "share", "broadcast", "openFile", "save", "toggleSidebar", "signatures", "info", "print", "comments", "a11y", "link", "openSorter", "toggleNotes", "toggleFormatPainter", "exportPng", "exportPdf", "exportGif", "exportVideo", "replace", "toggleInspector", "drawToolChange", "toggleThemeGallery", "toggleGrid", "toggleRulers", "toggleGuides", "toggleSelectionPane", "openCustomShows", "toggleSnapToGrid", "toggleEyedropper", "openSmartArtDialog", "openEquationDialog", "openSetUpSlideShow", "openCompare", "openPassword", "openFontEmbedding", "openVersionHistory", "openShortcuts"] }, { kind: "component", type: ThemeGalleryComponent, selector: "pptx-theme-gallery", inputs: ["open", "activeName"], outputs: ["applyTheme", "close"] }, { kind: "component", type: SelectionPaneComponent, selector: "pptx-selection-pane", inputs: ["elements", "selectedIds"], outputs: ["selectElement", "bringForward", "sendBackward", "toggleHidden"] }, { kind: "component", type: CustomShowsComponent, selector: "pptx-custom-shows", inputs: ["open", "slides", "customShows", "activeCustomShowId"], outputs: ["create", "remove", "update", "setActive", "close"] }, { kind: "component", type: InsertSmartArtDialogComponent, selector: "pptx-insert-smart-art-dialog", inputs: ["open"], outputs: ["close", "insert"] }, { kind: "component", type: ViewerExtraDialogsComponent, selector: "pptx-viewer-extra-dialogs", inputs: ["activeSlideIndex", "selectedElementId", "filePath", "customShows"], outputs: ["restoreContent"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
77437
78486
  }
77438
78487
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: PowerPointViewerComponent, decorators: [{
77439
78488
  type: Component,
@@ -77483,6 +78532,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
77483
78532
  SignaturesPanelComponent,
77484
78533
  AccessibilityPanelComponent,
77485
78534
  CollaborationCursorsComponent,
78535
+ RemoteSelectionOverlayComponent,
78536
+ FollowModeBarComponent,
77486
78537
  PropertiesDialogComponent,
77487
78538
  HyperlinkDialogComponent,
77488
78539
  PrintDialogComponent,
@@ -77627,7 +78678,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
77627
78678
  </nav>
77628
78679
  }
77629
78680
 
77630
- <main class="pptx-ng-main" #mainEl>
78681
+ <main class="pptx-ng-main" #mainEl (pointermove)="onCollabPointerMove($event)">
77631
78682
  <pptx-slide-canvas
77632
78683
  [slide]="activeSlide()"
77633
78684
  [canvasSize]="loader.canvasSize()"
@@ -77665,7 +78716,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
77665
78716
  (tableChange)="onTableChange($event)"
77666
78717
  />
77667
78718
  @if (collab.connected()) {
77668
- <pptx-collaboration-cursors [cursors]="collab.cursors()" [zoom]="zoom()" />
78719
+ <pptx-collaboration-cursors [cursors]="collabCursors()" [zoom]="zoom()" />
78720
+ <pptx-remote-selection-overlay
78721
+ [presences]="collab.presence()"
78722
+ [elements]="activeSlide()?.elements ?? []"
78723
+ [activeSlideIndex]="activeSlideIndex()"
78724
+ [zoom]="zoom()"
78725
+ />
78726
+ }
78727
+ @if (collab.active() && collab.presence().length > 0) {
78728
+ <div class="pptx-ng-collab-follow">
78729
+ <pptx-follow-mode-bar
78730
+ [presences]="collab.presence()"
78731
+ [followedClientId]="collab.followedClientId()"
78732
+ (follow)="collab.followUser($event)"
78733
+ />
78734
+ </div>
77669
78735
  }
77670
78736
  @if (showNotes() && !mobile.isMobile()) {
77671
78737
  <aside class="pptx-ng-notes" [attr.aria-label]="'pptx.notes.speakerNotes' | translate">
@@ -77931,6 +78997,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
77931
78997
  [connected]="collab.connected()"
77932
78998
  [userCount]="collab.connectedCount()"
77933
78999
  [shareUrl]="session.shareUrl()"
79000
+ [p2p]="session.activeSessionP2p()"
77934
79001
  [defaults]="session.shareDialogDefaults()"
77935
79002
  (start)="session.onShareStart($event)"
77936
79003
  (stop)="session.onShareStop()"
@@ -77943,6 +79010,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
77943
79010
  [connected]="collab.connected()"
77944
79011
  [viewerCount]="collab.presence().length"
77945
79012
  [viewerUrl]="session.broadcastViewerUrl()"
79013
+ [p2p]="session.activeSessionP2p()"
77946
79014
  [defaults]="{ serverUrl: shareDefaults()?.serverUrl }"
77947
79015
  (start)="session.onBroadcastStart($event)"
77948
79016
  (stop)="session.onBroadcastStop()"
@@ -78479,6 +79547,9 @@ const translationsEn = {
78479
79547
  'pptx.autosave.justNow': 'just now',
78480
79548
  'pptx.autosave.oneMinAgo': '1 min ago',
78481
79549
  'pptx.autosave.minutesAgo': '{{count}} min ago',
79550
+ 'pptx.autosave.saveFailed': 'Save failed',
79551
+ 'pptx.autosave.savedShort': 'Saved',
79552
+ 'pptx.autosave.allChangesSaved': 'All changes saved',
78482
79553
  // Toolbar
78483
79554
  'pptx.toolbar.toggleSlidesPanel': 'Toggle slides panel',
78484
79555
  'pptx.toolbar.undo': 'Undo',
@@ -78597,7 +79668,17 @@ const translationsEn = {
78597
79668
  'pptx.collaboration.youLabel': '{{name}} (you)',
78598
79669
  'pptx.collaboration.usersConnected': '{{count}} user(s) connected',
78599
79670
  'pptx.collaboration.moreUsers': '{{count}} more user(s)',
79671
+ 'pptx.collaboration.onePersonHere': '1 person here',
79672
+ 'pptx.collaboration.peopleHere': '{{count}} people here',
78600
79673
  'pptx.collaboration.retry': 'Retry',
79674
+ 'pptx.collaboration.retryConnection': 'Retry connection',
79675
+ // Follow mode
79676
+ 'pptx.followMode.following': 'Following',
79677
+ 'pptx.followMode.stop': 'Stop',
79678
+ 'pptx.followMode.stopFollowing': 'Stop following',
79679
+ 'pptx.followMode.followCollaborator': 'Follow a collaborator',
79680
+ 'pptx.followMode.followUser': 'Follow {{name}}',
79681
+ 'pptx.followMode.stopFollowingUser': 'Stop following {{name}}',
78601
79682
  // Share dialog
78602
79683
  'pptx.share.title': 'Share Presentation',
78603
79684
  'pptx.share.collaborationActive': 'Collaboration Active',