pptx-angular-viewer 1.15.0 → 1.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22867,7 +22867,7 @@ function assignUserColor(seed, palette = CURSOR_PALETTE) {
22867
22867
  * truncating, the result is exactly `maxChars` characters including a trailing
22868
22868
  * `...` ellipsis.
22869
22869
  */
22870
- function formatCursorLabel$1(userName, maxChars = MAX_LABEL_CHARS) {
22870
+ function formatCursorLabel(userName, maxChars = MAX_LABEL_CHARS) {
22871
22871
  if (userName.length <= maxChars) {
22872
22872
  return userName;
22873
22873
  }
@@ -22998,6 +22998,175 @@ function asSelectionIds(value) {
22998
22998
  return value.filter((entry) => typeof entry === 'string');
22999
22999
  }
23000
23000
 
23001
+ /**
23002
+ * Create a throttled presence publisher bound to an awareness instance. The
23003
+ * initial presence is announced synchronously so peers learn of us at once.
23004
+ */
23005
+ function createPresencePublisher(awareness, identity) {
23006
+ const local = { activeSlideIndex: 0, cursorX: 0, cursorY: 0 };
23007
+ // Starts at 0 so the first real update publishes immediately (the initial
23008
+ // announce below does not consume the throttle budget).
23009
+ let lastBroadcast = 0;
23010
+ let pending = null;
23011
+ function publish() {
23012
+ awareness.setLocalStateField('presence', {
23013
+ userName: identity.userName,
23014
+ userColor: identity.userColor,
23015
+ userAvatar: identity.userAvatar,
23016
+ role: identity.role,
23017
+ activeSlideIndex: local.activeSlideIndex,
23018
+ cursorX: local.cursorX,
23019
+ cursorY: local.cursorY,
23020
+ selectedElementId: local.selectedElementId,
23021
+ lastUpdated: new Date().toISOString(),
23022
+ });
23023
+ }
23024
+ function update(patch) {
23025
+ Object.assign(local, patch);
23026
+ const elapsed = Date.now() - lastBroadcast;
23027
+ if (elapsed >= BROADCAST_THROTTLE_MS) {
23028
+ if (pending !== null) {
23029
+ clearTimeout(pending);
23030
+ pending = null;
23031
+ }
23032
+ lastBroadcast = Date.now();
23033
+ publish();
23034
+ }
23035
+ else if (pending === null) {
23036
+ pending = setTimeout(() => {
23037
+ pending = null;
23038
+ lastBroadcast = Date.now();
23039
+ publish();
23040
+ }, BROADCAST_THROTTLE_MS - elapsed);
23041
+ }
23042
+ }
23043
+ function flush() {
23044
+ if (pending !== null) {
23045
+ clearTimeout(pending);
23046
+ pending = null;
23047
+ }
23048
+ lastBroadcast = Date.now();
23049
+ publish();
23050
+ }
23051
+ function dispose() {
23052
+ if (pending !== null) {
23053
+ clearTimeout(pending);
23054
+ pending = null;
23055
+ }
23056
+ }
23057
+ // Announce initial presence immediately (without consuming the throttle).
23058
+ publish();
23059
+ return { update, flush, dispose };
23060
+ }
23061
+
23062
+ /**
23063
+ * collaboration-assets.ts: binary payload sync for the pptx-viewer
23064
+ * collaboration stack.
23065
+ *
23066
+ * Large binary element fields (media/OLE/3D-model payloads) are synced once
23067
+ * via a separate `pptx:assets` Y.Map keyed by `${elementId}:${fieldName}`,
23068
+ * instead of being re-embedded as base64 inside every element write. This
23069
+ * avoids re-transmitting multi-MB payloads on every unrelated edit to the
23070
+ * same element: `writeAssetFields` only touches the assets map when the
23071
+ * stored payload actually differs from the incoming value.
23072
+ *
23073
+ * Known, accepted cost: asset entries are NOT garbage-collected when the
23074
+ * element that referenced them is deleted outright (only an explicit
23075
+ * field-clear removes its entry, via the `else` branch below). A long-lived
23076
+ * room's `pptx:assets` map can only grow. No sweep is implemented in this
23077
+ * pass; correct GC across concurrent/offline peers is a separate problem.
23078
+ */
23079
+ const YDOC_ASSETS_KEY = 'pptx:assets';
23080
+ /**
23081
+ * Element fields large/binary enough to route through the asset map instead
23082
+ * of being embedded inline as a scalar or complex JSON-blob field.
23083
+ */
23084
+ const ASSET_ELEMENT_FIELDS = new Set([
23085
+ 'mediaData',
23086
+ 'posterFrameData',
23087
+ 'oleEmbeddedData',
23088
+ 'previewImageData',
23089
+ 'modelData',
23090
+ ]);
23091
+ const ASSET_REF_KEYS = {
23092
+ mediaData: '_mdRef',
23093
+ posterFrameData: '_pfdRef',
23094
+ oleEmbeddedData: '_oedRef',
23095
+ previewImageData: '_pidRef',
23096
+ modelData: '_moRef',
23097
+ };
23098
+ const REV_ASSET_REF_KEYS = Object.fromEntries(Object.entries(ASSET_REF_KEYS).map(([field, ref]) => [ref, field]));
23099
+ function assetKey(elementId, fieldName) {
23100
+ return `${elementId}:${fieldName}`;
23101
+ }
23102
+ function getAssetsMap(ydoc) {
23103
+ return ydoc.getMap(YDOC_ASSETS_KEY);
23104
+ }
23105
+ /** True when `key` is one of the element-map ref-pointer keys this module owns. */
23106
+ function isAssetRefKey(key) {
23107
+ return key in REV_ASSET_REF_KEYS;
23108
+ }
23109
+ /**
23110
+ * Writes any present asset-routed fields from `rec` into `assets`, storing
23111
+ * only a small ref pointer (not the payload) on the element's own `ymap`.
23112
+ * Never reads from `ymap` itself, so it's safe to call on a brand-new,
23113
+ * not-yet-integrated map (Yjs throws on reading an unintegrated type) - use
23114
+ * this from a create/write path. `assets` is always a root-level (already
23115
+ * integrated) map, so the identical-payload skip-write check is still safe.
23116
+ */
23117
+ function writeAssetFields(elementId, rec, ymap, assets) {
23118
+ for (const field of ASSET_ELEMENT_FIELDS) {
23119
+ const value = rec[field];
23120
+ if (typeof value === 'string' && value.length > 0) {
23121
+ const key = assetKey(elementId, field);
23122
+ if (assets.get(key) !== value) {
23123
+ assets.set(key, value);
23124
+ }
23125
+ ymap.set(ASSET_REF_KEYS[field], key);
23126
+ }
23127
+ }
23128
+ }
23129
+ /**
23130
+ * Reconcile variant of {@link writeAssetFields} for an `ymap` that is
23131
+ * already integrated (an existing live element being updated in place):
23132
+ * additionally reads the current ref to skip a redundant set, and clears
23133
+ * the ref + its `assets` entry when the field was cleared.
23134
+ */
23135
+ function reconcileAssetFields(elementId, rec, ymap, assets) {
23136
+ for (const field of ASSET_ELEMENT_FIELDS) {
23137
+ const refKey = ASSET_REF_KEYS[field];
23138
+ const value = rec[field];
23139
+ if (typeof value === 'string' && value.length > 0) {
23140
+ const key = assetKey(elementId, field);
23141
+ if (assets.get(key) !== value) {
23142
+ assets.set(key, value);
23143
+ }
23144
+ if (ymap.get(refKey) !== key) {
23145
+ ymap.set(refKey, key);
23146
+ }
23147
+ }
23148
+ else {
23149
+ const existingRef = ymap.get(refKey);
23150
+ if (typeof existingRef === 'string') {
23151
+ ymap.delete(refKey);
23152
+ assets.delete(existingRef);
23153
+ }
23154
+ }
23155
+ }
23156
+ }
23157
+ /** Rehydrates any asset-routed fields referenced by `ymap` onto `element`. */
23158
+ function readAssetFields(ymap, assets, element) {
23159
+ for (const [refKey, field] of Object.entries(REV_ASSET_REF_KEYS)) {
23160
+ const ref = ymap.get(refKey);
23161
+ if (typeof ref === 'string') {
23162
+ const value = assets.get(ref);
23163
+ if (typeof value === 'string') {
23164
+ element[field] = value;
23165
+ }
23166
+ }
23167
+ }
23168
+ }
23169
+
23001
23170
  /**
23002
23171
  * collaboration-text-codec.ts: TextSegment[] <-> Y.Text delta codec used by the
23003
23172
  * collaboration sync layer. Split out of collaboration-sync.ts to keep both
@@ -23266,6 +23435,9 @@ const SCALAR_ELEMENT_KEYS = new Set([
23266
23435
  'width',
23267
23436
  'height',
23268
23437
  'rotation',
23438
+ 'shapeId',
23439
+ 'skewX',
23440
+ 'skewY',
23269
23441
  'flipHorizontal',
23270
23442
  'flipVertical',
23271
23443
  'hidden',
@@ -23274,14 +23446,66 @@ const SCALAR_ELEMENT_KEYS = new Set([
23274
23446
  'name',
23275
23447
  'altText',
23276
23448
  'shapeType',
23277
- 'placeholder',
23278
23449
  'imagePath',
23279
23450
  'imageData',
23280
- 'svgContent',
23281
- 'inkSvg',
23282
- 'sourceSlideId',
23451
+ 'svgData',
23452
+ 'svgPath',
23453
+ 'cropLeft',
23454
+ 'cropTop',
23455
+ 'cropRight',
23456
+ 'cropBottom',
23457
+ 'tileOffsetX',
23458
+ 'tileOffsetY',
23459
+ 'tileScaleX',
23460
+ 'tileScaleY',
23461
+ 'tileFlip',
23462
+ 'tileAlignment',
23463
+ 'pathData',
23464
+ 'pathWidth',
23465
+ 'pathHeight',
23283
23466
  'mediaType',
23284
23467
  'mediaPath',
23468
+ 'mediaMimeType',
23469
+ 'trimStartMs',
23470
+ 'trimEndMs',
23471
+ 'posterFramePath',
23472
+ 'fullScreen',
23473
+ 'loop',
23474
+ 'fadeInDuration',
23475
+ 'fadeOutDuration',
23476
+ 'volume',
23477
+ 'autoPlay',
23478
+ 'playAcrossSlides',
23479
+ 'hideWhenNotPlaying',
23480
+ 'playbackSpeed',
23481
+ 'mediaMissing',
23482
+ 'isLinked',
23483
+ 'oleTarget',
23484
+ 'oleProgId',
23485
+ 'oleName',
23486
+ 'oleClsId',
23487
+ 'oleObjectType',
23488
+ 'oleFileExtension',
23489
+ 'fileName',
23490
+ 'externalPath',
23491
+ 'previewImage',
23492
+ 'oleShowAsIcon',
23493
+ 'oleImgW',
23494
+ 'oleImgH',
23495
+ 'oleEmbeddedFileName',
23496
+ 'oleEmbeddedMimeType',
23497
+ 'oleEmbeddedByteSize',
23498
+ 'inkPaths',
23499
+ 'inkColors',
23500
+ 'inkWidths',
23501
+ 'inkOpacities',
23502
+ 'inkTool',
23503
+ 'zoomType',
23504
+ 'targetSlideIndex',
23505
+ 'targetSectionId',
23506
+ 'modelPath',
23507
+ 'modelMimeType',
23508
+ 'posterImage',
23285
23509
  'linkedTxbxId',
23286
23510
  'linkedTxbxSeq',
23287
23511
  'promptText',
@@ -23294,26 +23518,35 @@ const COMPLEX_ELEMENT_FIELDS = {
23294
23518
  tableData: '_td',
23295
23519
  chartData: '_cd',
23296
23520
  smartArtData: '_smad',
23297
- connectionStart: '_cs',
23298
- connectionEnd: '_ce',
23299
- animations: '_an',
23300
- nativeAnimations: '_na',
23301
23521
  children: '_ch',
23302
23522
  paragraphIndents: '_pi',
23303
23523
  rawXml: '_rx',
23524
+ extLstXml: '_elx',
23304
23525
  actionClick: '_ac',
23305
23526
  actionHover: '_av',
23306
23527
  locks: '_lk',
23307
23528
  imageEffects: '_ie',
23308
23529
  cropShape: '_cr',
23309
- mediaBookmarks: '_mb',
23530
+ bookmarks: '_mb',
23310
23531
  captionTracks: '_ct',
23532
+ metadata: '_md',
23533
+ groupFill: '_gf',
23534
+ inkPointPressures: '_ipp',
23535
+ inkStrokes: '_cis',
23536
+ extensionXml: '_ext',
23537
+ customGeometryPaths: '_cgp',
23538
+ customGeometryRawData: '_cgr',
23539
+ customGeometryAdjustHandlesXY: '_cgx',
23540
+ customGeometryAdjustHandlesPolar: '_cgo',
23541
+ customGeometryConnectionSites: '_cgc',
23542
+ customGeometryTextRect: '_cgt',
23311
23543
  };
23312
23544
  const REV_COMPLEX_ELEMENT = Object.fromEntries(Object.entries(COMPLEX_ELEMENT_FIELDS).map(([k, v]) => [v, k]));
23313
23545
  const SCALAR_SLIDE_KEYS = new Set([
23314
23546
  'id',
23315
23547
  'rId',
23316
23548
  'sourceSlideId',
23549
+ 'name',
23317
23550
  'layoutPath',
23318
23551
  'layoutName',
23319
23552
  'slideNumber',
@@ -23323,7 +23556,9 @@ const SCALAR_SLIDE_KEYS = new Set([
23323
23556
  'backgroundColor',
23324
23557
  'backgroundImage',
23325
23558
  'backgroundGradient',
23559
+ 'backgroundShadeToTitle',
23326
23560
  'notes',
23561
+ 'notesCSldName',
23327
23562
  'backgroundShowAnimation',
23328
23563
  'showMasterShapes',
23329
23564
  'isDirty',
@@ -23334,6 +23569,8 @@ const COMPLEX_SLIDE_FIELDS = {
23334
23569
  nativeAnimations: '_na',
23335
23570
  rawTiming: '_rt',
23336
23571
  notesSegments: '_ns',
23572
+ notesShapes: '_nsh',
23573
+ notesClrMapOverride: '_ncm',
23337
23574
  comments: '_cm',
23338
23575
  warnings: '_wa',
23339
23576
  rawXml: '_rx',
@@ -23341,15 +23578,17 @@ const COMPLEX_SLIDE_FIELDS = {
23341
23578
  guides: '_gu',
23342
23579
  customerData: '_cu',
23343
23580
  activeXControls: '_ax',
23581
+ backgroundPattern: '_bp',
23582
+ headerFooterFlags: '_hff',
23344
23583
  };
23345
23584
  const REV_COMPLEX_SLIDE = Object.fromEntries(Object.entries(COMPLEX_SLIDE_FIELDS).map(([k, v]) => [v, k]));
23346
23585
  // ---------------------------------------------------------------------------
23347
23586
  // Element serialization
23348
23587
  // ---------------------------------------------------------------------------
23349
- function writeElementToYMap(element, ymap, factories) {
23588
+ function writeElementToYMap(element, ymap, factories, assets) {
23350
23589
  const rec = element;
23351
23590
  for (const [key, value] of Object.entries(rec)) {
23352
- if (value === undefined) {
23591
+ if (value === undefined || ASSET_ELEMENT_FIELDS.has(key)) {
23353
23592
  continue;
23354
23593
  }
23355
23594
  if (SCALAR_ELEMENT_KEYS.has(key)) {
@@ -23366,8 +23605,9 @@ function writeElementToYMap(element, ymap, factories) {
23366
23605
  ymap.set(COMPLEX_ELEMENT_FIELDS[key], JSON.stringify(value));
23367
23606
  }
23368
23607
  }
23608
+ writeAssetFields(rec.id, rec, ymap, assets);
23369
23609
  }
23370
- function readElementFromYMap(ymap) {
23610
+ function readElementFromYMap(ymap, assets) {
23371
23611
  const element = {};
23372
23612
  ymap.forEach((value, key) => {
23373
23613
  if (key === 'textBody') {
@@ -23375,6 +23615,9 @@ function readElementFromYMap(ymap) {
23375
23615
  element.textSegments = decodeTextBody(value);
23376
23616
  }
23377
23617
  }
23618
+ else if (isAssetRefKey(key)) {
23619
+ // handled by readAssetFields below; not a literal PptxElement field
23620
+ }
23378
23621
  else if (REV_COMPLEX_ELEMENT[key]) {
23379
23622
  try {
23380
23623
  element[REV_COMPLEX_ELEMENT[key]] = JSON.parse(value);
@@ -23387,12 +23630,13 @@ function readElementFromYMap(ymap) {
23387
23630
  element[key] = value;
23388
23631
  }
23389
23632
  });
23633
+ readAssetFields(ymap, assets, element);
23390
23634
  return element;
23391
23635
  }
23392
23636
  // ---------------------------------------------------------------------------
23393
23637
  // Slide serialization
23394
23638
  // ---------------------------------------------------------------------------
23395
- function writeSlideToYMap(slide, ymap, factories) {
23639
+ function writeSlideToYMap(slide, ymap, factories, assets) {
23396
23640
  const rec = slide;
23397
23641
  for (const key of SCALAR_SLIDE_KEYS) {
23398
23642
  if (rec[key] !== undefined) {
@@ -23407,12 +23651,12 @@ function writeSlideToYMap(slide, ymap, factories) {
23407
23651
  const elemArr = factories.createArray();
23408
23652
  for (const el of slide.elements) {
23409
23653
  const elemMap = factories.createMap();
23410
- writeElementToYMap(el, elemMap, factories);
23654
+ writeElementToYMap(el, elemMap, factories, assets);
23411
23655
  elemArr.push([elemMap]);
23412
23656
  }
23413
23657
  ymap.set('elements', elemArr);
23414
23658
  }
23415
- function readSlideFromYMap(ymap) {
23659
+ function readSlideFromYMap(ymap, assets) {
23416
23660
  const slide = {};
23417
23661
  for (const key of SCALAR_SLIDE_KEYS) {
23418
23662
  const v = ymap.get(key);
@@ -23435,7 +23679,7 @@ function readSlideFromYMap(ymap) {
23435
23679
  const elements = [];
23436
23680
  if (elemArr) {
23437
23681
  for (let i = 0; i < elemArr.length; i++) {
23438
- elements.push(readElementFromYMap(elemArr.get(i)));
23682
+ elements.push(readElementFromYMap(elemArr.get(i), assets));
23439
23683
  }
23440
23684
  }
23441
23685
  slide.elements = elements;
@@ -23450,6 +23694,7 @@ function readSlideFromYMap(ymap) {
23450
23694
  * one-shot seeding of an empty document.
23451
23695
  */
23452
23696
  function writeSlidesToYDoc(slides, ydoc, factories, origin) {
23697
+ const assets = getAssetsMap(ydoc);
23453
23698
  ydoc.transact(() => {
23454
23699
  const arr = ydoc.getArray(YDOC_SLIDES_KEY);
23455
23700
  if (arr.length > 0) {
@@ -23457,16 +23702,17 @@ function writeSlidesToYDoc(slides, ydoc, factories, origin) {
23457
23702
  }
23458
23703
  for (const slide of slides) {
23459
23704
  const ymap = factories.createMap();
23460
- writeSlideToYMap(slide, ymap, factories);
23705
+ writeSlideToYMap(slide, ymap, factories, assets);
23461
23706
  arr.push([ymap]);
23462
23707
  }
23463
23708
  }, origin);
23464
23709
  }
23465
23710
  function readSlidesFromYDoc(ydoc) {
23711
+ const assets = getAssetsMap(ydoc);
23466
23712
  const arr = ydoc.getArray(YDOC_SLIDES_KEY);
23467
23713
  const slides = [];
23468
23714
  for (let i = 0; i < arr.length; i++) {
23469
- slides.push(readSlideFromYMap(arr.get(i)));
23715
+ slides.push(readSlideFromYMap(arr.get(i), assets));
23470
23716
  }
23471
23717
  return slides;
23472
23718
  }
@@ -23718,11 +23964,12 @@ function reconcileTextBody(ymap, rec, factories) {
23718
23964
  encodeTextBody(segments, ytext);
23719
23965
  ymap.set('textBody', ytext);
23720
23966
  }
23721
- function reconcileElementYMap(ymap, element, factories) {
23967
+ function reconcileElementYMap(ymap, element, factories, assets) {
23722
23968
  const rec = element;
23723
23969
  reconcileScalars(ymap, rec, SCALAR_ELEMENT_KEYS);
23724
23970
  reconcileComplexFields(ymap, rec, COMPLEX_ELEMENT_FIELDS);
23725
23971
  reconcileTextBody(ymap, rec, factories);
23972
+ reconcileAssetFields(rec.id, rec, ymap, assets);
23726
23973
  }
23727
23974
  function mapIdAt(arr, index) {
23728
23975
  const entry = arr.get(index);
@@ -23799,7 +24046,7 @@ const isYArrayLike = (value) => typeof value === 'object' &&
23799
24046
  value !== null &&
23800
24047
  typeof value.insert === 'function' &&
23801
24048
  typeof value.toArray === 'function';
23802
- function reconcileSlideYMap(ymap, slide, factories) {
24049
+ function reconcileSlideYMap(ymap, slide, factories, assets) {
23803
24050
  const rec = slide;
23804
24051
  reconcileScalars(ymap, rec, SCALAR_SLIDE_KEYS);
23805
24052
  reconcileComplexFields(ymap, rec, COMPLEX_SLIDE_FIELDS);
@@ -23812,10 +24059,10 @@ function reconcileSlideYMap(ymap, slide, factories) {
23812
24059
  idOf: (el) => (typeof el.id === 'string' ? el.id : undefined),
23813
24060
  create: (el) => {
23814
24061
  const map = factories.createMap();
23815
- writeElementToYMap(el, map, factories);
24062
+ writeElementToYMap(el, map, factories, assets);
23816
24063
  return map;
23817
24064
  },
23818
- update: (map, el) => reconcileElementYMap(map, el, factories),
24065
+ update: (map, el) => reconcileElementYMap(map, el, factories, assets),
23819
24066
  });
23820
24067
  }
23821
24068
  /**
@@ -23824,16 +24071,17 @@ function reconcileSlideYMap(ymap, slide, factories) {
23824
24071
  * caller's own observer can skip the resulting events.
23825
24072
  */
23826
24073
  function reconcileSlidesInYDoc(slides, ydoc, factories, origin = LOCAL_SYNC_ORIGIN) {
24074
+ const assets = getAssetsMap(ydoc);
23827
24075
  ydoc.transact(() => {
23828
24076
  const arr = ydoc.getArray(YDOC_SLIDES_KEY);
23829
24077
  reconcileYArrayById(arr, slides, {
23830
24078
  idOf: (slide) => (typeof slide.id === 'string' ? slide.id : undefined),
23831
24079
  create: (slide) => {
23832
24080
  const map = factories.createMap();
23833
- writeSlideToYMap(slide, map, factories);
24081
+ writeSlideToYMap(slide, map, factories, assets);
23834
24082
  return map;
23835
24083
  },
23836
- update: (map, slide) => reconcileSlideYMap(map, slide, factories),
24084
+ update: (map, slide) => reconcileSlideYMap(map, slide, factories, assets),
23837
24085
  });
23838
24086
  }, origin);
23839
24087
  }
@@ -23890,6 +24138,50 @@ function createSyncGate(onOpen, graceMs = INITIAL_SYNC_GRACE_MS) {
23890
24138
  };
23891
24139
  }
23892
24140
 
24141
+ const DEFAULT_DEBOUNCE_MS = 5_000;
24142
+ function createWriteBackScheduler(deps) {
24143
+ let timer = null;
24144
+ function cancel() {
24145
+ if (timer !== null) {
24146
+ clearTimeout(timer);
24147
+ timer = null;
24148
+ }
24149
+ }
24150
+ function schedule(config) {
24151
+ if (!config.onWriteBack || config.role !== 'owner' || !deps.getYDoc()) {
24152
+ return;
24153
+ }
24154
+ cancel();
24155
+ const debounceMs = config.writeBackDebounceMs ?? DEFAULT_DEBOUNCE_MS;
24156
+ timer = setTimeout(async () => {
24157
+ timer = null;
24158
+ const ydoc = deps.getYDoc();
24159
+ if (!ydoc || !config.onWriteBack) {
24160
+ return;
24161
+ }
24162
+ const sourceBytes = deps.getSourceBytes?.();
24163
+ if (!sourceBytes) {
24164
+ return;
24165
+ }
24166
+ try {
24167
+ const { PptxHandler } = await import('pptx-viewer-core');
24168
+ const handler = new PptxHandler();
24169
+ await handler.load(sourceBytes.buffer);
24170
+ const slides = readSlidesFromYDoc(ydoc);
24171
+ const merged = deps.mergeTemplateElements
24172
+ ? deps.mergeTemplateElements(slides, deps.getTemplateElements?.() ?? {})
24173
+ : slides;
24174
+ const bytes = await handler.save(merged);
24175
+ config.onWriteBack(bytes);
24176
+ }
24177
+ catch {
24178
+ /* non-fatal */
24179
+ }
24180
+ }, debounceMs);
24181
+ }
24182
+ return { schedule, cancel };
24183
+ }
24184
+
23893
24185
  // ---------------------------------------------------------------------------
23894
24186
  // Tuning constants (px tolerances for position / size changes)
23895
24187
  // ---------------------------------------------------------------------------
@@ -33282,6 +33574,51 @@ const CHANGE_CASE_OPTIONS$1 = [
33282
33574
  { value: 'toggle', i18nKey: 'pptx.text.changeCaseToggle' },
33283
33575
  ];
33284
33576
 
33577
+ /**
33578
+ * color-swatches.ts: the canonical "Office Standard Colors" swatch catalogue
33579
+ * shared by every binding's colour pickers (font colour, highlight colour,
33580
+ * and any future fill/line colour picker that wants the same 10-swatch grid).
33581
+ *
33582
+ * Mirrors the "Standard Colors" row PowerPoint itself ships. Before this
33583
+ * module existed, the VanillaJS (`swatch-picker.ts`) and Svelte
33584
+ * (`SwatchColorPicker.svelte`) bindings each independently hardcoded an
33585
+ * identical 10-colour list; this module is the single source of truth going
33586
+ * forward so no binding drifts from the others.
33587
+ *
33588
+ * There is no shared-i18n dictionary key for individual colour names yet
33589
+ * (only the `pptx.ribbon.customColour` "Custom colour..." row label exists),
33590
+ * so {@link OfficeColorSwatch.label} is a plain English fallback rather than
33591
+ * an `i18nKey`, matching the optional-label convention used elsewhere in this
33592
+ * module family (e.g. `LineSpacingOption` in `text-format-presets.ts`).
33593
+ *
33594
+ * @module render/color-swatches
33595
+ */
33596
+ /**
33597
+ * The canonical 10-swatch "Office Standard Colors" catalogue, in PowerPoint's
33598
+ * on-screen order. Consumed by the font-colour and highlight-colour pickers
33599
+ * (and any other picker that wants the same standard set) across every
33600
+ * binding.
33601
+ */
33602
+ const OFFICE_COLOR_SWATCHES = [
33603
+ { hex: '#000000', label: 'Black' },
33604
+ { hex: '#ffffff', label: 'White' },
33605
+ { hex: '#ff0000', label: 'Red' },
33606
+ { hex: '#00aa00', label: 'Green' },
33607
+ { hex: '#0000ff', label: 'Blue' },
33608
+ { hex: '#ff8800', label: 'Orange' },
33609
+ { hex: '#8800cc', label: 'Purple' },
33610
+ { hex: '#00cccc', label: 'Cyan' },
33611
+ { hex: '#ff69b4', label: 'Pink' },
33612
+ { hex: '#808080', label: 'Gray' },
33613
+ ];
33614
+ /**
33615
+ * The same catalogue flattened to bare `#rrggbb` hex strings, in the same
33616
+ * order as {@link OFFICE_COLOR_SWATCHES}. A drop-in replacement for the
33617
+ * `readonly string[]` swatch lists the vanilla and Svelte bindings each
33618
+ * hardcoded locally before this module existed.
33619
+ */
33620
+ const OFFICE_COLOR_SWATCH_HEXES = OFFICE_COLOR_SWATCHES.map((swatch) => swatch.hex);
33621
+
33285
33622
  /**
33286
33623
  * PowerPoint-style title bar: shared pure logic + Tailwind class tokens.
33287
33624
  *
@@ -37702,25 +38039,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImpor
37702
38039
  * surface so `collaboration.service.ts`, `collaboration-cursors.component.ts`,
37703
38040
  * the viewer barrel, and the colocated tests are unchanged.
37704
38041
  *
37705
- * Two pieces stay Angular-local because they diverge from the shared copy:
37706
- * - `RemotePresence` is the Angular name for shared's `SanitizedPresence`
37707
- * (re-aliased here).
37708
- * - `formatCursorLabel` keeps the historical single-character ellipsis (`…`)
37709
- * truncation, which differs from shared's three-dot (`...`) variant that the
37710
- * React binding expects. Kept local so the rendered label and the colocated
37711
- * test are unchanged.
37712
- *
37713
- * `mapAwarenessCursors` (the foundational bare-`{ cursor, user }` mapping) now
37714
- * lives in shared and is re-exported here so existing Angular imports are
37715
- * unchanged.
37716
- */
37717
- /**
37718
- * Clamp/format a cursor label so long names don't overflow the chip. Uses a
37719
- * single-character ellipsis (`…`); the total length is exactly `maxChars`.
38042
+ * `RemotePresence` is the Angular name for shared's `SanitizedPresence`
38043
+ * (re-aliased here). `formatCursorLabel` and `mapAwarenessCursors` are now
38044
+ * re-exported as-is from shared: Angular's `formatCursorLabel` used to keep
38045
+ * its own single-character ellipsis (`…`) truncation that differed from
38046
+ * shared's three-dot (`...`) variant, so two peers viewing the same presence
38047
+ * record saw differently-truncated names depending on which binding hosted
38048
+ * the session - a real cross-binding consistency bug, not a style choice.
37720
38049
  */
37721
- function formatCursorLabel(userName, maxChars = MAX_LABEL_CHARS) {
37722
- return userName.length > maxChars ? `${userName.slice(0, maxChars - 1)}…` : userName;
37723
- }
37724
38050
 
37725
38051
  /**
37726
38052
  * collaboration-cursors.component.ts: Angular port of the Vue
@@ -37862,8 +38188,6 @@ class LocalPresencePublisher {
37862
38188
  setCursor(x, y, activeSlideIndex = this.activeSlide) {
37863
38189
  this.cursor = { x, y };
37864
38190
  this.activeSlide = activeSlideIndex;
37865
- // A bare `cursor` field mirrors the flat awareness shape some peers read.
37866
- this.awareness.setLocalStateField('cursor', { x, y });
37867
38191
  this.publish();
37868
38192
  }
37869
38193
  setSelection(selectedElementId, activeSlideIndex = this.activeSlide) {
@@ -37883,14 +38207,14 @@ class LocalPresencePublisher {
37883
38207
  *
37884
38208
  * Isolates the dynamic `yjs` / `y-websocket` / `y-webrtc` imports and provider
37885
38209
  * construction so the service focuses on lifecycle + reactive state. The Yjs
37886
- * packages are dynamically imported so they are fully tree-shaken when
37887
- * collaboration is unused; each specifier is read from a variable so bundlers
37888
- * do not eagerly follow it (mirrors the historical `/* @vite-ignore *\/`
37889
- * pattern).
38210
+ * packages are dynamically imported (plain string-literal specifiers, matching
38211
+ * React/Vue) so they are fully tree-shaken when collaboration is unused, while
38212
+ * still resolving correctly under a consuming app's Vite dev server: an
38213
+ * `@vite-ignore`d or variable specifier skips Vite's dev-time bare-specifier
38214
+ * rewrite, which then fails to resolve natively in the browser.
37890
38215
  */
37891
38216
  async function createDoc() {
37892
- const yModule = 'yjs';
37893
- const Y = (await import(/* @vite-ignore */ yModule));
38217
+ const Y = (await import('yjs'));
37894
38218
  const doc = new Y.Doc();
37895
38219
  const factories = {
37896
38220
  createMap: () => new Y.Map(),
@@ -37901,10 +38225,9 @@ async function createDoc() {
37901
38225
  }
37902
38226
  /** Create a y-websocket transport bundle for `config`. */
37903
38227
  async function createWebsocketBundle(config) {
37904
- const wsSpecifier = 'y-websocket';
37905
38228
  const [{ doc, factories }, yws] = await Promise.all([
37906
38229
  createDoc(),
37907
- import(/* @vite-ignore */ wsSpecifier),
38230
+ import('y-websocket'),
37908
38231
  ]);
37909
38232
  const provider = new yws.WebsocketProvider(config.serverUrl, config.roomId, doc, config.authToken ? { params: { token: config.authToken } } : undefined);
37910
38233
  return { doc, provider, awareness: provider.awareness, factories };
@@ -37916,10 +38239,9 @@ async function createWebsocketBundle(config) {
37916
38239
  * `authToken` is passed as the room `password`.
37917
38240
  */
37918
38241
  async function createWebrtcBundle(config) {
37919
- const rtcSpecifier = 'y-webrtc';
37920
38242
  const [{ doc, factories }, yrtc] = await Promise.all([
37921
38243
  createDoc(),
37922
- import(/* @vite-ignore */ rtcSpecifier),
38244
+ import('y-webrtc'),
37923
38245
  ]);
37924
38246
  const provider = new yrtc.WebrtcProvider(config.roomId, doc, {
37925
38247
  signaling: config.signaling?.length ? config.signaling : undefined,
@@ -38144,9 +38466,14 @@ class CollaborationService {
38144
38466
  this.status.set('error');
38145
38467
  return;
38146
38468
  }
38469
+ // Falls back from a blank serverUrl the same way Vue's session layer
38470
+ // already does, so a bare CollaborationConfig behaves identically
38471
+ // regardless of which binding's session layer receives it directly (not
38472
+ // just via the Share/Broadcast dialogs, which already pre-resolve it).
38473
+ const transport = config.transport ?? resolveTransportForServerUrl(config.serverUrl);
38147
38474
  // Fail fast on mixed content (websocket only): an https page cannot open a
38148
38475
  // ws:// socket, so surface the error rather than hanging until the timeout.
38149
- if (config.transport !== 'webrtc' && isMixedContentBlocked(config.serverUrl)) {
38476
+ if (transport !== 'webrtc' && isMixedContentBlocked(config.serverUrl)) {
38150
38477
  this.status.set('error');
38151
38478
  return;
38152
38479
  }
@@ -38159,7 +38486,7 @@ class CollaborationService {
38159
38486
  this.activeRole.set(config.role);
38160
38487
  this.status.set('connecting');
38161
38488
  try {
38162
- const bundle = config.transport === 'webrtc'
38489
+ const bundle = transport === 'webrtc'
38163
38490
  ? await createWebrtcBundle(config)
38164
38491
  : await createWebsocketBundle(config);
38165
38492
  this.ydoc = bundle.doc;
@@ -38176,7 +38503,7 @@ class CollaborationService {
38176
38503
  this.localPresence.publish();
38177
38504
  this.awareness.on('change', this.refreshPresence);
38178
38505
  this.awareness.on('update', this.refreshPresence);
38179
- this.wireStatus(config);
38506
+ this.wireStatus(transport);
38180
38507
  this.syncGate.reset();
38181
38508
  this.wireSynced();
38182
38509
  this.unobserveSlides = observeYDocSlides(this.ydoc, (_events, transaction) => this.onRemoteChange(transaction));
@@ -38195,18 +38522,23 @@ class CollaborationService {
38195
38522
  }
38196
38523
  }
38197
38524
  /** Wire the provider status events + (websocket-only) connection timeout. */
38198
- wireStatus(config) {
38525
+ wireStatus(transport) {
38199
38526
  const provider = this.provider;
38200
38527
  if (!provider) {
38201
38528
  return;
38202
38529
  }
38203
- if (config.transport === 'webrtc') {
38530
+ if (transport === 'webrtc') {
38204
38531
  // P2P: no server round-trip to wait on. Treat "created" as connected,
38205
38532
  // and reflect explicit disconnect events.
38206
38533
  this.status.set('connected');
38207
38534
  provider.on('status', (payload) => {
38208
38535
  if (payload.connected === false && this.active()) {
38209
38536
  this.status.set('disconnected');
38537
+ // Re-arm on (re)connect: without this, a peer that drops and
38538
+ // rejoins keeps the gate permanently open from the first
38539
+ // connection and can clobber the room with a stale local doc.
38540
+ this.syncGate.reset();
38541
+ this.syncGate.arm();
38210
38542
  }
38211
38543
  else if (payload.connected === true) {
38212
38544
  this.status.set('connected');
@@ -38221,6 +38553,8 @@ class CollaborationService {
38221
38553
  }
38222
38554
  else if (payload.status === 'disconnected' && this.active()) {
38223
38555
  this.status.set('disconnected');
38556
+ this.syncGate.reset();
38557
+ this.syncGate.arm();
38224
38558
  }
38225
38559
  });
38226
38560
  if (provider.wsconnected) {