@a3s-lab/office 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -150,12 +150,18 @@ round trip preserves supported and untouched unsupported OOXML.
150
150
 
151
151
  ### Phase 3: Presentation
152
152
 
153
- Status: browser collaboration foundation implemented; rich-text CRDT and PPTX
154
- round-trip concurrency coverage are pending.
153
+ Status: browser collaboration foundation and native conflict-local
154
+ scene-element content and z-order mutations implemented; rich-text CRDT,
155
+ remaining structural native operations, and PPTX round-trip concurrency
156
+ coverage are pending.
155
157
 
156
158
  - Stable slide/master/layout order arrays and ID-keyed record maps avoid a
157
159
  serialized whole-deck root. Scene elements and slide comments are ID-keyed
158
160
  child records.
161
+ - Append-only element claims make identical same-ID creation idempotent and
162
+ reject different records assigned to one ID. Deletion writes a durable
163
+ tombstone, removes the element from visible order, and permanently reserves
164
+ the identity; element ID and type cannot drift.
159
165
  - Snapshot writes are translated into `previous -> next` record patches, so an
160
166
  unrelated stale snapshot does not remove remotely added slides or objects.
161
167
  - Typed validation rejects duplicate identities and invalid references before
@@ -166,13 +172,25 @@ round-trip concurrency coverage are pending.
166
172
  - Tests cover bootstrap, identity validation, separate-record convergence,
167
173
  stale snapshots, concurrent comments, local-only undo, permissions, mounted
168
174
  projection, and framework adapters.
175
+ - Rust, CLI, MCP, and A3S Code can create, optimistically update, move, or
176
+ tombstone one element in a slide, master, or layout. Creation can insert
177
+ after a stable active element. Update compares complete
178
+ expected/current/next records and writes only changed top-level fields, so
179
+ unrelated concurrent changes merge while a stale same-field edit fails
180
+ atomically. Move compares stable observed and requested predecessor IDs,
181
+ treats an already-satisfied destination as idempotent, and changes only the
182
+ moved element's order entry; `null` means the first order position. Delete
183
+ requires an exact complete-element match. Browser/Yrs fixtures, native
184
+ restart, duplicate and reordered delivery, real CLI/MCP subprocesses, and
185
+ the Playground cover update/create/move/delete interoperability.
169
186
 
170
187
  Remaining:
171
188
 
172
189
  - Bind editable scene text to collaborative XML fragments instead of scalar
173
190
  text/run replacement.
174
- - Add creation claims and explicit conflict handling for concurrent same-ID
175
- object creation, delete-vs-edit, z-order, grouping, and theme changes.
191
+ - Add structural native operations and explicit conflict handling for slide
192
+ order, grouping, and theme changes; extend z-order beyond stable scene-element
193
+ moves where a concrete browser workflow requires it.
176
194
  - Keep derived thumbnails and layout measurements local and prove this in UI
177
195
  tests.
178
196
  - Add offline/reordered-update property tests plus merged PPTX export/reopen.
@@ -322,9 +340,10 @@ destructive actions remain attributable, reviewable, and non-retryable.
322
340
 
323
341
  Status: native Yrs replica store, resumable CLI/MCP event streams, `a3s code`
324
342
  projection, a host-injected live CLI transport session, and typed Markdown,
325
- Document, Spreadsheet cell, PDF annotation/form-value, and PDF
326
- redaction/page-operation review mutation surfaces are implemented; the
327
- remaining format mutations and native presence projection are pending.
343
+ Document, Spreadsheet cell, Presentation scene-element, PDF
344
+ annotation/form-value, and PDF redaction/page-operation review mutation
345
+ surfaces are implemented; the remaining format mutations and native presence
346
+ projection are pending.
328
347
 
329
348
  - Yrs `0.27.3` now uses 53-bit Yjs-compatible client IDs and exchanges standard
330
349
  Yjs v1 updates, state vectors, and y-sync `SyncStep1`/`Update` messages with
@@ -375,6 +394,12 @@ remaining format mutations and native presence projection are pending.
375
394
  an exact complete-cell match. They preserve the browser's field-addressed
376
395
  representation, dense/sparse projection mode, and atomic fail-closed
377
396
  semantics without replacing a worksheet or workbook.
397
+ - Presentation scene-element mutations create, update, move, or tombstone one
398
+ stable object inside a slide, master, or layout. Canonical creation claims
399
+ prevent conflicting same-ID reuse, optimistic top-level field guards merge
400
+ unrelated concurrent edits, stable predecessor guards move one order entry
401
+ without array indexes, and exact deletion guards prevent a stale destructive
402
+ write without replacing the deck or container.
378
403
  - PDF form-value mutations write the browser-compatible conflict-local record
379
404
  roots by fully-qualified field name, retain idempotent receipts and optional
380
405
  state-vector preconditions, and never synchronize source bytes.
@@ -402,20 +427,22 @@ remaining format mutations and native presence projection are pending.
402
427
  source browser operation, and source attribution is audit data rather than
403
428
  an authorization token.
404
429
  - Cross-language tests apply deterministic native UTF-16 Markdown, ProseMirror
405
- Document text/options/paragraph, Spreadsheet cell, and PDF
406
- annotation/form-value updates in browser Yjs, including concurrent
407
- browser/native paragraph, cell-leaf, annotation-leaf, and PDF form edits, in
408
- addition to importing browser Yjs fixtures in Yrs.
430
+ Document text/options/paragraph, Spreadsheet cell, Presentation
431
+ scene-element content/z-order, and PDF annotation/form-value updates in browser Yjs,
432
+ including concurrent browser/native paragraph, cell-leaf, element-field,
433
+ annotation-leaf, and PDF form edits, in addition to importing browser Yjs
434
+ fixtures in Yrs.
409
435
  - Standard MCP exposes the durable replica lifecycle and bounded event stream;
410
436
  the same seven collaboration tools are explicitly available to the dedicated
411
437
  `a3s code` Use worker.
412
438
 
413
439
  Remaining:
414
440
 
415
- - Extend typed format-model mutations to Spreadsheet structural operations and
416
- Presentation plus PDF signatures; deepen Document mutations to additional
417
- nested structures and review operations, and add durable comment/suggest
418
- operations before enabling those modes for local mutation.
441
+ - Extend typed format-model mutations to Spreadsheet and remaining Presentation
442
+ structural/rich-text operations plus PDF signatures; deepen Document
443
+ mutations to additional nested structures and review operations, and add
444
+ durable comment/suggest operations before enabling those modes for local
445
+ mutation.
419
446
  - Project editor-visible presence and selection state while keeping Awareness
420
447
  ephemeral and outside native replica persistence.
421
448
 
package/README.md CHANGED
@@ -138,9 +138,10 @@ The images below are committed visual-regression baselines from the real
138
138
  - **Automation outside the browser** — The native Rust CLI, standard MCP
139
139
  server, and Office Skill share bounded mutation contracts. Coding agents can
140
140
  also keep a durable Yrs replica, exchange standard Yjs v1 updates and state
141
- vectors, perform authorized typed Markdown, Document, Spreadsheet cell, and
142
- PDF annotation, form-value, and review changes, retain browser/native actor
143
- attribution, and checkpoint without replacing a whole Office file.
141
+ vectors, perform authorized typed Markdown, Document, Spreadsheet cell,
142
+ Presentation scene-element and z-order, and PDF annotation, form-value, and
143
+ review changes, retain browser/native actor attribution, and checkpoint
144
+ without replacing a whole Office file.
144
145
 
145
146
  ## Quick start
146
147
 
@@ -1030,6 +1031,21 @@ cargo run -p a3s-office-cli -- collab mutate .a3s/plan.replica \
1030
1031
  --operation-id edit-45 \
1031
1032
  --mutation '{"type":"spreadsheet-set-cell","sheetId":"sheet-data","row":1,"column":0,"expectedCell":{"v":10,"m":"10"},"nextCell":{"v":12,"m":"12","f":"=6*2"}}' --json
1032
1033
 
1034
+ # Update one Presentation scene element after matching the complete observed
1035
+ # record. Create/delete use presentation-create/delete-element.
1036
+ cargo run -p a3s-office-cli -- collab mutate .a3s/deck.replica \
1037
+ --artifact-id deck --kind presentation --actor-id agent-7 --mode edit \
1038
+ --operation-id edit-46 \
1039
+ --mutation '{"type":"presentation-update-element","containerKind":"slide","containerId":"slide-1","elementId":"title-1","expectedElement":{"id":"title-1","type":"text","x":10,"y":10,"width":80,"height":20,"text":"Draft"},"nextElement":{"id":"title-1","type":"text","x":16,"y":10,"width":80,"height":20,"text":"Final"}}' --json
1040
+
1041
+ # Move that element to the first order position after verifying its currently
1042
+ # observed predecessor. Destination and source positions use stable IDs, not
1043
+ # array indexes; null means no predecessor.
1044
+ cargo run -p a3s-office-cli -- collab mutate .a3s/deck.replica \
1045
+ --artifact-id deck --kind presentation --actor-id agent-7 --mode edit \
1046
+ --operation-id move-46 \
1047
+ --mutation '{"type":"presentation-move-element","containerKind":"slide","containerId":"slide-1","elementId":"title-1","expectedAfterElementId":"background-1","afterElementId":null}' --json
1048
+
1033
1049
  # Set one PDF form value through its stable fully-qualified field name.
1034
1050
  cargo run -p a3s-office-cli -- collab mutate .a3s/application.replica \
1035
1051
  --artifact-id application --kind pdf --actor-id agent-7 --mode edit \
@@ -1093,7 +1109,18 @@ write only changed leaves, so unrelated concurrent value, formula, style,
1093
1109
  hyperlink, note, and metadata edits merge. Use `expectedCell: null` only when
1094
1110
  creating an observed blank coordinate. Spreadsheet deletion requires the exact
1095
1111
  complete current cell, dense projections keep their dimensions, and sparse or
1096
- empty sheets remain sparse. PDF annotation creation accepts the
1112
+ empty sheets remain sparse. Presentation creation writes one complete element
1113
+ plus a canonical immutable claim inside a slide, master, or layout and can
1114
+ place it after a stable active element. Presentation update compares complete
1115
+ expected/current/next elements and writes only changed top-level fields, so
1116
+ unrelated concurrent geometry, text, or style changes merge while a stale
1117
+ same-field edit fails. Presentation move compares the stable observed and
1118
+ requested predecessor IDs instead of array indexes; `null` means the first
1119
+ element-order position. An already-satisfied destination is idempotent, while
1120
+ a stale source position or unavailable anchor fails before a durable update.
1121
+ Only the moved element's order entry changes; no object fields, container, or
1122
+ deck are replaced. Exact deletion writes a durable tombstone, and element IDs
1123
+ and types never drift or become reusable. PDF annotation creation accepts the
1097
1124
  portable browser record for FreeText, Highlight, Underline, StrikeOut, or Ink,
1098
1125
  writes `source: created`, and commits its immutable claim atomically. An update
1099
1126
  supplies complete `expectedAnnotation` and `nextAnnotation` values; recursive
@@ -1171,6 +1198,8 @@ without hard-coded return URLs.
1171
1198
 
1172
1199
  - [Live Playground](https://a3s-lab.github.io/Office/)
1173
1200
  - [Documentation center](https://a3s-lab.github.io/Office/docs/)
1201
+ - [A3S Office 0.7.0 documentation](https://a3s-lab.github.io/Office/docs/0.7.0/)
1202
+ - [A3S Office 0.6.0 documentation](https://a3s-lab.github.io/Office/docs/0.6.0/)
1174
1203
  - [A3S Office 0.5.0 documentation](https://a3s-lab.github.io/Office/docs/0.5.0/)
1175
1204
  - [A3S Office 0.4.0 documentation](https://a3s-lab.github.io/Office/docs/0.4.0/)
1176
1205
  - [A3S Office 0.3.0 documentation](https://a3s-lab.github.io/Office/docs/0.3.0/)
package/dist/4560.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { initializeWorkOfficeCollaborationMetadata, assertWorkOfficeCollaborationEditable, OfficeCollaborationError as WorkOfficeCollaborationError, readOfficeCollaborationMetadata as readWorkOfficeCollaborationMetadata, registerWorkOfficeCollaborationInitializer, markWorkOfficeCollaborationInitialized, assertWorkOfficeCollaborationOrigin } from "./9787.js";
2
- import { workOfficeCollaborationJsonEqual, isWorkOfficeCollaborationRecord, cloneWorkOfficeCollaborationJson } from "./4650.js";
2
+ import { workOfficeCollaborationJsonEqual, canonicalWorkOfficeCollaborationJson, isWorkOfficeCollaborationRecord, cloneWorkOfficeCollaborationJson } from "./4650.js";
3
3
  import * as __rspack_external_yjs from "yjs";
4
4
  function validateWorkOfficePresentationContent(content) {
5
5
  if (!content || 'presentation' !== content.type) invalidWorkOfficePresentationInput('a Presentation content value');
@@ -66,6 +66,7 @@ function validateLayout(value) {
66
66
  }
67
67
  function validateElement(value) {
68
68
  const record = requiredInputRecord(value, 'presentation element');
69
+ if (Object.hasOwn(record, 'tombstone')) invalidWorkOfficePresentationInput("a presentation element without the reserved 'tombstone' field");
69
70
  const element = validateJsonRecord(record, 'presentation element');
70
71
  element.id = requiredIdentifier(record.id, 'presentation element');
71
72
  element.type = requiredString(record.type, 'presentation element type');
@@ -131,11 +132,115 @@ function requiredFiniteNumber(value, label) {
131
132
  function invalidWorkOfficePresentationInput(expected) {
132
133
  throw new WorkOfficeCollaborationError('office.collaboration.content_invalid', `Presentation collaboration requires ${expected}.`);
133
134
  }
135
+ function appendWorkOfficePresentationRecordClaims(claims, previous, next) {
136
+ appendContainerClaims(claims, 'slide', previous?.slides ?? [], next.slides);
137
+ appendContainerClaims(claims, 'master', previous?.masters ?? [], next.masters ?? []);
138
+ appendContainerClaims(claims, 'layout', previous?.layouts ?? [], next.layouts ?? []);
139
+ }
140
+ function assertWorkOfficePresentationRecordClaims(claims, content) {
141
+ const fingerprints = new Map();
142
+ for (const raw of claims.toArray()){
143
+ const claim = parsedClaim(raw);
144
+ const identity = claimIdentity(claim.containerKind, claim.containerId, claim.id);
145
+ const existing = fingerprints.get(identity);
146
+ if (void 0 !== existing && existing !== claim.fingerprint) throw new WorkOfficeCollaborationError('office.collaboration.content_invalid', `The scene element in ${claim.containerKind} '${claim.containerId}' ID '${claim.id}' was concurrently assigned to different records.`);
147
+ fingerprints.set(identity, claim.fingerprint);
148
+ }
149
+ assertClaimedElementIdentities(fingerprints, 'slide', content.slides);
150
+ assertClaimedElementIdentities(fingerprints, 'master', content.masters ?? []);
151
+ assertClaimedElementIdentities(fingerprints, 'layout', content.layouts ?? []);
152
+ }
153
+ function appendContainerClaims(claims, containerKind, previous, next) {
154
+ const previousById = new Map(previous.map((container)=>[
155
+ container.id,
156
+ container
157
+ ]));
158
+ for (const container of next){
159
+ const previousElementIds = new Set((previousById.get(container.id)?.elements ?? []).map(({ id })=>id));
160
+ for (const element of container.elements)if (!previousElementIds.has(element.id)) appendClaim(claims, containerKind, container.id, element);
161
+ }
162
+ }
163
+ function appendClaim(claims, containerKind, containerId, element) {
164
+ const serialized = canonicalWorkOfficeCollaborationJson({
165
+ containerId,
166
+ containerKind,
167
+ fingerprint: canonicalWorkOfficeCollaborationJson(element),
168
+ id: element.id,
169
+ kind: 'element'
170
+ });
171
+ if (!claims.toArray().includes(serialized)) claims.push([
172
+ serialized
173
+ ]);
174
+ }
175
+ function parsedClaim(raw) {
176
+ if ('string' != typeof raw) invalidWorkOfficePresentationShared('record claim');
177
+ let value;
178
+ try {
179
+ value = JSON.parse(raw);
180
+ } catch {
181
+ invalidWorkOfficePresentationShared('record claim');
182
+ }
183
+ if (!isWorkOfficeCollaborationRecord(value) || canonicalWorkOfficeCollaborationJson(value) !== raw) invalidWorkOfficePresentationShared('record claim');
184
+ if (Object.keys(value).some((key)=>'containerId' !== key && 'containerKind' !== key && 'fingerprint' !== key && 'id' !== key && 'kind' !== key)) invalidWorkOfficePresentationShared('record claim');
185
+ if ('element' !== value.kind) invalidWorkOfficePresentationShared('record claim kind');
186
+ const containerKind = requiredContainerKind(value.containerKind);
187
+ const containerId = office_presentation_collaboration_claims_requiredIdentifier(value.containerId, 'record claim container');
188
+ const id = office_presentation_collaboration_claims_requiredIdentifier(value.id, 'record claim');
189
+ const fingerprint = office_presentation_collaboration_claims_requiredString(value.fingerprint, 'record claim fingerprint');
190
+ let claimed;
191
+ try {
192
+ claimed = JSON.parse(fingerprint);
193
+ } catch {
194
+ invalidWorkOfficePresentationShared('record claim fingerprint');
195
+ }
196
+ if (!isWorkOfficeCollaborationRecord(claimed) || canonicalWorkOfficeCollaborationJson(claimed) !== fingerprint || claimed.id !== id || 'string' != typeof claimed.type) invalidWorkOfficePresentationShared('record claim fingerprint');
197
+ return {
198
+ containerId,
199
+ containerKind,
200
+ fingerprint,
201
+ id,
202
+ kind: 'element'
203
+ };
204
+ }
205
+ function assertClaimedElementIdentities(claims, containerKind, containers) {
206
+ for (const container of containers)for (const element of container.elements){
207
+ const fingerprint = claims.get(claimIdentity(containerKind, container.id, element.id));
208
+ if (void 0 === fingerprint) continue;
209
+ let claimed;
210
+ try {
211
+ claimed = JSON.parse(fingerprint);
212
+ } catch {
213
+ invalidWorkOfficePresentationShared('record claim fingerprint');
214
+ }
215
+ if (!isWorkOfficeCollaborationRecord(claimed) || claimed.id !== element.id || claimed.type !== element.type) invalidWorkOfficePresentationShared('scene element immutable identity claim');
216
+ }
217
+ }
218
+ function requiredContainerKind(value) {
219
+ if ('layout' === value || 'master' === value || 'slide' === value) return value;
220
+ invalidWorkOfficePresentationShared('record claim container kind');
221
+ }
222
+ function office_presentation_collaboration_claims_requiredIdentifier(value, label) {
223
+ const result = office_presentation_collaboration_claims_requiredString(value, label);
224
+ if (!result || result !== result.trim() || result.length > 256) invalidWorkOfficePresentationShared(label);
225
+ return result;
226
+ }
227
+ function office_presentation_collaboration_claims_requiredString(value, label) {
228
+ if ('string' != typeof value) invalidWorkOfficePresentationShared(label);
229
+ return value;
230
+ }
231
+ function claimIdentity(containerKind, containerId, id) {
232
+ return canonicalWorkOfficeCollaborationJson([
233
+ containerKind,
234
+ containerId,
235
+ id
236
+ ]);
237
+ }
134
238
  const PRESENTATION_RECORD_ELEMENTS = 'elements';
135
239
  const PRESENTATION_RECORD_ELEMENT_ORDER = 'elementOrder';
136
240
  const PRESENTATION_RECORD_COMMENTS = 'comments';
137
241
  const PRESENTATION_RECORD_COMMENT_ORDER = 'commentOrder';
138
242
  const PRESENTATION_RECORD_COMMENTS_PRESENT = 'commentsPresent';
243
+ const PRESENTATION_RECORD_TOMBSTONE = 'tombstone';
139
244
  function nestedPresentationMap(parent, key, label) {
140
245
  const existing = parent.get(key);
141
246
  if (existing instanceof __rspack_external_yjs.Map) return existing;
@@ -162,7 +267,7 @@ function requiredPresentationArray(parent, key, label) {
162
267
  if (!(value instanceof __rspack_external_yjs.Array)) invalidWorkOfficePresentationShared(label);
163
268
  return value;
164
269
  }
165
- function validatedPresentationOrder(order, records, label) {
270
+ function validatedPresentationOrder(order, records, label, allowTombstones = false) {
166
271
  const seen = new Set();
167
272
  const values = [];
168
273
  for (const value of order.toArray()){
@@ -172,7 +277,20 @@ function validatedPresentationOrder(order, records, label) {
172
277
  values.push(value);
173
278
  }
174
279
  }
175
- if (seen.size !== records.size || Array.from(records.keys()).some((id)=>!seen.has(id))) invalidWorkOfficePresentationShared(`${label} order and record set`);
280
+ for (const id of seen){
281
+ const record = records.get(id);
282
+ if (!(record instanceof __rspack_external_yjs.Map)) invalidWorkOfficePresentationShared(`${label} order and record set`);
283
+ if (void 0 !== record.get(PRESENTATION_RECORD_TOMBSTONE)) invalidWorkOfficePresentationShared(`${label} order and record set`);
284
+ }
285
+ for (const [id, value] of records.entries()){
286
+ if (!(value instanceof __rspack_external_yjs.Map)) invalidWorkOfficePresentationShared(`${label} order and record set`);
287
+ const tombstone = value.get(PRESENTATION_RECORD_TOMBSTONE);
288
+ if (void 0 === tombstone) {
289
+ if (!seen.has(id)) invalidWorkOfficePresentationShared(`${label} order and record set`);
290
+ continue;
291
+ }
292
+ if (!allowTombstones || true !== tombstone || seen.has(id)) invalidWorkOfficePresentationShared(`${label} order and record set`);
293
+ }
176
294
  return values;
177
295
  }
178
296
  function readPresentationCollection(records, order, label) {
@@ -191,15 +309,15 @@ function readPresentationCollection(records, order, label) {
191
309
  }
192
310
  }
193
311
  if (value.id !== id) invalidWorkOfficePresentationShared(`${label} identity`);
194
- value.elements = readIdRecords(requiredPresentationMap(record, PRESENTATION_RECORD_ELEMENTS, `${label} elements`), requiredPresentationArray(record, PRESENTATION_RECORD_ELEMENT_ORDER, `${label} element order`), `${label} element`);
312
+ value.elements = readIdRecords(requiredPresentationMap(record, PRESENTATION_RECORD_ELEMENTS, `${label} elements`), requiredPresentationArray(record, PRESENTATION_RECORD_ELEMENT_ORDER, `${label} element order`), `${label} element`, true);
195
313
  if ('slide' === label) readSlideComments(record, value);
196
314
  return value;
197
315
  });
198
316
  }
199
317
  function patchPresentationElements(records, order, previous, next, parentLabel) {
200
- patchPresentationIdRecords(records, order, previous, next, `${parentLabel} element`);
318
+ patchPresentationIdRecords(records, order, previous, next, `${parentLabel} element`, true);
201
319
  }
202
- function patchPresentationIdRecords(records, order, previous, next, label) {
320
+ function patchPresentationIdRecords(records, order, previous, next, label, durableTombstones = false) {
203
321
  const previousById = new Map(previous.map((value)=>[
204
322
  value.id,
205
323
  value
@@ -208,7 +326,19 @@ function patchPresentationIdRecords(records, order, previous, next, label) {
208
326
  value.id,
209
327
  value
210
328
  ]));
211
- for (const value of previous)if (!nextById.has(value.id)) records.delete(value.id);
329
+ for (const value of previous){
330
+ if (nextById.has(value.id)) continue;
331
+ if (!durableTombstones) {
332
+ records.delete(value.id);
333
+ continue;
334
+ }
335
+ const record = records.get(value.id);
336
+ if (void 0 === record) continue;
337
+ if (!(record instanceof __rspack_external_yjs.Map)) invalidWorkOfficePresentationShared(label);
338
+ const tombstone = record.get(PRESENTATION_RECORD_TOMBSTONE);
339
+ if (void 0 !== tombstone && true !== tombstone) invalidWorkOfficePresentationShared(`${label} tombstone`);
340
+ record.set(PRESENTATION_RECORD_TOMBSTONE, true);
341
+ }
212
342
  for (const value of next){
213
343
  const before = previousById.get(value.id);
214
344
  if (before && workOfficeCollaborationJsonEqual(before, value)) continue;
@@ -218,6 +348,13 @@ function patchPresentationIdRecords(records, order, previous, next, label) {
218
348
  records.set(value.id, record);
219
349
  }
220
350
  if (!(record instanceof __rspack_external_yjs.Map)) invalidWorkOfficePresentationShared(label);
351
+ const tombstone = record.get(PRESENTATION_RECORD_TOMBSTONE);
352
+ if (void 0 !== tombstone) {
353
+ if (true !== tombstone) invalidWorkOfficePresentationShared(`${label} tombstone`);
354
+ if (!before) throw new WorkOfficeCollaborationError('office.collaboration.content_invalid', `The deleted scene element ID '${value.id}' cannot be reused.`);
355
+ continue;
356
+ }
357
+ if (durableTombstones && before && before.type !== value.type) throw new WorkOfficeCollaborationError('office.collaboration.content_invalid', `The scene element ID '${value.id}' cannot change type.`);
221
358
  patchPresentationJsonMap(record, before ?? {}, value);
222
359
  }
223
360
  patchPresentationOrder(order, previous.map((value)=>value.id), next.map((value)=>value.id));
@@ -246,12 +383,21 @@ function readSlideComments(record, value) {
246
383
  if (commentRecords.size > 0 || commentOrder.length > 0) value.comments = readIdRecords(commentRecords, commentOrder, 'slide comment');
247
384
  else if (true === record.get(PRESENTATION_RECORD_COMMENTS_PRESENT)) value.comments = [];
248
385
  }
249
- function readIdRecords(records, order, label) {
250
- return validatedPresentationOrder(order, records, label).map((id)=>readJsonMap(requiredPresentationMap(records, id, label), id, label));
386
+ function readIdRecords(records, order, label, allowTombstones = false) {
387
+ const activeIds = validatedPresentationOrder(order, records, label, allowTombstones);
388
+ if (allowTombstones) for (const [id, value] of records.entries()){
389
+ if (!(value instanceof __rspack_external_yjs.Map)) invalidWorkOfficePresentationShared(label);
390
+ if (true === value.get(PRESENTATION_RECORD_TOMBSTONE)) readJsonMap(value, id, label, true);
391
+ }
392
+ return activeIds.map((id)=>readJsonMap(requiredPresentationMap(records, id, label), id, label));
251
393
  }
252
- function readJsonMap(record, expectedId, label) {
394
+ function readJsonMap(record, expectedId, label, tombstoned = false) {
253
395
  const value = {};
254
396
  for (const [key, item] of record.entries()){
397
+ if (key === PRESENTATION_RECORD_TOMBSTONE) {
398
+ if (!tombstoned || true !== item) invalidWorkOfficePresentationShared(`${label} tombstone`);
399
+ continue;
400
+ }
255
401
  if (item instanceof __rspack_external_yjs.AbstractType) invalidWorkOfficePresentationShared(`${label} field '${key}'`);
256
402
  value[key] = cloneWorkOfficeCollaborationJson(item);
257
403
  }
@@ -299,6 +445,7 @@ const PRESENTATION_MASTERS_ROOT = 'presentation.masters';
299
445
  const PRESENTATION_MASTER_ORDER_ROOT = 'presentation.master-order';
300
446
  const PRESENTATION_LAYOUTS_ROOT = 'presentation.layouts';
301
447
  const PRESENTATION_LAYOUT_ORDER_ROOT = 'presentation.layout-order';
448
+ const PRESENTATION_RECORD_CLAIMS_ROOT = 'presentation.record-claims';
302
449
  function workOfficePresentationRoots(document, rootName) {
303
450
  return {
304
451
  options: document.getMap(rootName(PRESENTATION_OPTIONS_ROOT)),
@@ -307,10 +454,12 @@ function workOfficePresentationRoots(document, rootName) {
307
454
  masters: document.getMap(rootName(PRESENTATION_MASTERS_ROOT)),
308
455
  masterOrder: document.getArray(rootName(PRESENTATION_MASTER_ORDER_ROOT)),
309
456
  layouts: document.getMap(rootName(PRESENTATION_LAYOUTS_ROOT)),
310
- layoutOrder: document.getArray(rootName(PRESENTATION_LAYOUT_ORDER_ROOT))
457
+ layoutOrder: document.getArray(rootName(PRESENTATION_LAYOUT_ORDER_ROOT)),
458
+ recordClaims: document.getArray(rootName(PRESENTATION_RECORD_CLAIMS_ROOT))
311
459
  };
312
460
  }
313
461
  function initializeWorkOfficePresentationRoots(roots, content) {
462
+ appendWorkOfficePresentationRecordClaims(roots.recordClaims, void 0, content);
314
463
  patchOptionalJson(roots.options, 'width', void 0, content.width);
315
464
  patchOptionalJson(roots.options, 'height', void 0, content.height);
316
465
  patchCollection({
@@ -348,11 +497,14 @@ function readWorkOfficePresentationRoots(roots) {
348
497
  const layouts = readPresentationCollection(roots.layouts, roots.layoutOrder, 'presentation layout');
349
498
  if (masters.length > 0) result.masters = masters;
350
499
  if (layouts.length > 0) result.layouts = layouts;
351
- return validateSharedWorkOfficePresentationContent(result);
500
+ const validated = validateSharedWorkOfficePresentationContent(result);
501
+ assertWorkOfficePresentationRecordClaims(roots.recordClaims, validated);
502
+ return validated;
352
503
  }
353
504
  function patchWorkOfficePresentationRoots(roots, previous, next) {
354
505
  const shared = readWorkOfficePresentationRoots(roots);
355
506
  assertChangedRecordsStillExist(previous, next, shared);
507
+ appendWorkOfficePresentationRecordClaims(roots.recordClaims, previous, next);
356
508
  patchOptionalJson(roots.options, 'width', previous.width, next.width);
357
509
  patchOptionalJson(roots.options, 'height', previous.height, next.height);
358
510
  patchCollection({
@@ -413,7 +565,7 @@ function assertChangedChildRecordsExist(previous, next, shared, label) {
413
565
  }
414
566
  }
415
567
  function assertWorkOfficePresentationRootsEmpty(roots) {
416
- if (roots.options.size > 0 || roots.slides.size > 0 || roots.slideOrder.length > 0 || roots.masters.size > 0 || roots.masterOrder.length > 0 || roots.layouts.size > 0 || roots.layoutOrder.length > 0) throw new WorkOfficeCollaborationError('office.collaboration.bootstrap_ambiguous', 'The Presentation collaboration roots contain data without initialized metadata.');
568
+ if (roots.options.size > 0 || roots.slides.size > 0 || roots.slideOrder.length > 0 || roots.masters.size > 0 || roots.masterOrder.length > 0 || roots.layouts.size > 0 || roots.layoutOrder.length > 0 || roots.recordClaims.length > 0) throw new WorkOfficeCollaborationError('office.collaboration.bootstrap_ambiguous', 'The Presentation collaboration roots contain data without initialized metadata.');
417
569
  }
418
570
  function workOfficePresentationUndoScope(roots) {
419
571
  return [
@@ -423,7 +575,8 @@ function workOfficePresentationUndoScope(roots) {
423
575
  roots.masters,
424
576
  roots.masterOrder,
425
577
  roots.layouts,
426
- roots.layoutOrder
578
+ roots.layoutOrder,
579
+ roots.recordClaims
427
580
  ];
428
581
  }
429
582
  function patchCollection({ label, order, previous, records, next }) {
@@ -0,0 +1,5 @@
1
+ import type * as Y from 'yjs';
2
+ import type { WorkPresentationContent } from '../features/work/work-types';
3
+ export type WorkOfficePresentationContainerKind = 'layout' | 'master' | 'slide';
4
+ export declare function appendWorkOfficePresentationRecordClaims(claims: Y.Array<string>, previous: WorkPresentationContent | undefined, next: WorkPresentationContent): void;
5
+ export declare function assertWorkOfficePresentationRecordClaims(claims: Y.Array<string>, content: WorkPresentationContent): void;
@@ -8,6 +8,7 @@ export declare const PRESENTATION_MASTERS_ROOT = "presentation.masters";
8
8
  export declare const PRESENTATION_MASTER_ORDER_ROOT = "presentation.master-order";
9
9
  export declare const PRESENTATION_LAYOUTS_ROOT = "presentation.layouts";
10
10
  export declare const PRESENTATION_LAYOUT_ORDER_ROOT = "presentation.layout-order";
11
+ export declare const PRESENTATION_RECORD_CLAIMS_ROOT = "presentation.record-claims";
11
12
  export interface WorkOfficePresentationRoots {
12
13
  options: Y.Map<unknown>;
13
14
  slides: Y.Map<unknown>;
@@ -16,6 +17,7 @@ export interface WorkOfficePresentationRoots {
16
17
  masterOrder: Y.Array<string>;
17
18
  layouts: Y.Map<unknown>;
18
19
  layoutOrder: Y.Array<string>;
20
+ recordClaims: Y.Array<string>;
19
21
  }
20
22
  export declare function workOfficePresentationRoots(document: Y.Doc, rootName: (suffix: string) => string): WorkOfficePresentationRoots;
21
23
  export declare function initializeWorkOfficePresentationRoots(roots: WorkOfficePresentationRoots, content: WorkPresentationContent): void;
@@ -5,6 +5,7 @@ export declare const PRESENTATION_RECORD_ELEMENT_ORDER = "elementOrder";
5
5
  export declare const PRESENTATION_RECORD_COMMENTS = "comments";
6
6
  export declare const PRESENTATION_RECORD_COMMENT_ORDER = "commentOrder";
7
7
  export declare const PRESENTATION_RECORD_COMMENTS_PRESENT = "commentsPresent";
8
+ export declare const PRESENTATION_RECORD_TOMBSTONE = "tombstone";
8
9
  export type WorkOfficePresentationRecord = {
9
10
  id: string;
10
11
  elements: WorkSlideElement[];
@@ -13,10 +14,10 @@ export declare function nestedPresentationMap(parent: Y.Map<unknown>, key: strin
13
14
  export declare function nestedPresentationArray(parent: Y.Map<unknown>, key: string, label: string): Y.Array<string>;
14
15
  export declare function requiredPresentationMap(parent: Y.Map<unknown>, key: string, label: string): Y.Map<unknown>;
15
16
  export declare function requiredPresentationArray(parent: Y.Map<unknown>, key: string, label: string): Y.Array<string>;
16
- export declare function validatedPresentationOrder(order: Y.Array<string>, records: Y.Map<unknown>, label: string): string[];
17
+ export declare function validatedPresentationOrder(order: Y.Array<string>, records: Y.Map<unknown>, label: string, allowTombstones?: boolean): string[];
17
18
  export declare function readPresentationCollection(records: Y.Map<unknown>, order: Y.Array<string>, label: string): WorkOfficePresentationRecord[];
18
19
  export declare function patchPresentationElements(records: Y.Map<unknown>, order: Y.Array<string>, previous: WorkSlideElement[], next: WorkSlideElement[], parentLabel: string): void;
19
20
  export declare function patchPresentationIdRecords<T extends {
20
21
  id: string;
21
- }>(records: Y.Map<unknown>, order: Y.Array<string>, previous: T[], next: T[], label: string): void;
22
+ }>(records: Y.Map<unknown>, order: Y.Array<string>, previous: T[], next: T[], label: string, durableTombstones?: boolean): void;
22
23
  export declare function patchPresentationOrder(order: Y.Array<string>, previous: string[], next: string[]): void;
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a3s-lab/office",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Open-source browser editors for documents, Markdown, spreadsheets, presentations, and PDFs.",
5
5
  "keywords": [
6
6
  "office",
@@ -87,8 +87,8 @@
87
87
  "playground:visual:spreadsheet-ribbon": "playwright test visual-tests/spreadsheet-ribbon.functional.spec.ts --config playwright.config.ts",
88
88
  "playground:visual:update": "playwright test --config playwright.config.ts --update-snapshots",
89
89
  "test": "rstest",
90
- "test:e2e": "bun run test:e2e:fixtures && a3s-test run tests/e2e/word-page-color.acl --json && a3s-test run tests/e2e/word-page-setup-phone.acl --json && a3s-test run tests/e2e/word-paragraph-layout-phone.acl --json && a3s-test run tests/e2e/word-list-formatting-phone.acl --json && a3s-test run tests/e2e/word-font-picker-phone.acl --json && a3s-test run tests/e2e/word-comments-drawer.acl --json && a3s-test run tests/e2e/word-comment-windowing.acl --json && a3s-test run tests/e2e/word-citations-phone.acl --json && a3s-test run tests/e2e/word-navigation-search.acl --json && a3s-test run tests/e2e/word-navigation-windowing.acl --json && a3s-test run tests/e2e/word-find-replace-phone.acl --json && a3s-test run tests/e2e/word-page-navigation.acl --json && a3s-test run tests/e2e/word-page-windowing.acl --json && a3s-test run tests/e2e/word-ai-question.acl --json && a3s-test run tests/e2e/word-picture-insert.acl --json && a3s-test run tests/e2e/word-picture-alt-text-phone.acl --json && a3s-test run tests/e2e/word-track-changes-phone.acl --json && a3s-test run tests/e2e/word-review-conflict-phone.acl --json && a3s-test run tests/e2e/word-revision-windowing.acl --json && a3s-test run tests/e2e/word-table-phone.acl --json && a3s-test run tests/e2e/word-table-borders.acl --json && a3s-test run tests/e2e/word-theme-table.acl --json && a3s-test run tests/e2e/word-styled-table.acl --json && a3s-test run tests/e2e/word-multi-page-table.acl --json && a3s-test run tests/e2e/word-cross-reference-phone.acl --json && a3s-test run tests/e2e/word-bookmark-links-phone.acl --json && a3s-test run tests/e2e/word-notes-phone.acl --json && a3s-test run tests/e2e/word-fields-phone.acl --json && a3s-test run tests/e2e/spreadsheet-phone-rename.acl --json && a3s-test run tests/e2e/spreadsheet-phone-find.acl --json && a3s-test run tests/e2e/spreadsheet-phone-context-menu.acl --json && a3s-test run tests/e2e/spreadsheet-phone-task-pane.acl --json && a3s-test run tests/e2e/presentation-cut-paste-focus.acl --json && a3s-test run tests/e2e/presentation-presenter-view.acl --json && a3s-test run tests/e2e/presentation-phone-chart-pane.acl --json && a3s-test run tests/e2e/presentation-phone-comments.acl --json && a3s-test run tests/e2e/pdf-thumbnail-keyboard.acl --json && a3s-test run tests/e2e/pdf-page-drawer-phone.acl --json && a3s-test run tests/e2e/markdown-phone-modes.acl --json && a3s-test run tests/e2e/office-sidebar-breakpoint.acl --json && a3s-test run tests/e2e/office-docs-navigation.acl --json && a3s-test run tests/e2e/collaboration-participants.acl --json && a3s-test run tests/e2e/collaboration-pdf-annotations.acl --json && a3s-test run tests/e2e/collaboration-spreadsheet-cells.acl --json",
91
- "test:e2e:check": "bun run test:e2e:fixtures && a3s-test check tests/e2e/word-page-color.acl --json && a3s-test check tests/e2e/word-page-setup-phone.acl --json && a3s-test check tests/e2e/word-paragraph-layout-phone.acl --json && a3s-test check tests/e2e/word-list-formatting-phone.acl --json && a3s-test check tests/e2e/word-font-picker-phone.acl --json && a3s-test check tests/e2e/word-comments-drawer.acl --json && a3s-test check tests/e2e/word-comment-windowing.acl --json && a3s-test check tests/e2e/word-citations-phone.acl --json && a3s-test check tests/e2e/word-navigation-search.acl --json && a3s-test check tests/e2e/word-navigation-windowing.acl --json && a3s-test check tests/e2e/word-find-replace-phone.acl --json && a3s-test check tests/e2e/word-page-navigation.acl --json && a3s-test check tests/e2e/word-page-windowing.acl --json && a3s-test check tests/e2e/word-ai-question.acl --json && a3s-test check tests/e2e/word-picture-insert.acl --json && a3s-test check tests/e2e/word-picture-alt-text-phone.acl --json && a3s-test check tests/e2e/word-track-changes-phone.acl --json && a3s-test check tests/e2e/word-review-conflict-phone.acl --json && a3s-test check tests/e2e/word-revision-windowing.acl --json && a3s-test check tests/e2e/word-table-phone.acl --json && a3s-test check tests/e2e/word-table-borders.acl --json && a3s-test check tests/e2e/word-theme-table.acl --json && a3s-test check tests/e2e/word-styled-table.acl --json && a3s-test check tests/e2e/word-multi-page-table.acl --json && a3s-test check tests/e2e/word-cross-reference-phone.acl --json && a3s-test check tests/e2e/word-bookmark-links-phone.acl --json && a3s-test check tests/e2e/word-notes-phone.acl --json && a3s-test check tests/e2e/word-fields-phone.acl --json && a3s-test check tests/e2e/spreadsheet-phone-rename.acl --json && a3s-test check tests/e2e/spreadsheet-phone-find.acl --json && a3s-test check tests/e2e/spreadsheet-phone-context-menu.acl --json && a3s-test check tests/e2e/spreadsheet-phone-task-pane.acl --json && a3s-test check tests/e2e/presentation-cut-paste-focus.acl --json && a3s-test check tests/e2e/presentation-presenter-view.acl --json && a3s-test check tests/e2e/presentation-phone-chart-pane.acl --json && a3s-test check tests/e2e/presentation-phone-comments.acl --json && a3s-test check tests/e2e/pdf-thumbnail-keyboard.acl --json && a3s-test check tests/e2e/pdf-page-drawer-phone.acl --json && a3s-test check tests/e2e/markdown-phone-modes.acl --json && a3s-test check tests/e2e/office-sidebar-breakpoint.acl --json && a3s-test check tests/e2e/office-docs-navigation.acl --json && a3s-test check tests/e2e/collaboration-participants.acl --json && a3s-test check tests/e2e/collaboration-pdf-annotations.acl --json && a3s-test check tests/e2e/collaboration-spreadsheet-cells.acl --json",
90
+ "test:e2e": "bun run test:e2e:fixtures && a3s-test run tests/e2e/word-page-color.acl --json && a3s-test run tests/e2e/word-page-setup-phone.acl --json && a3s-test run tests/e2e/word-paragraph-layout-phone.acl --json && a3s-test run tests/e2e/word-list-formatting-phone.acl --json && a3s-test run tests/e2e/word-font-picker-phone.acl --json && a3s-test run tests/e2e/word-comments-drawer.acl --json && a3s-test run tests/e2e/word-comment-windowing.acl --json && a3s-test run tests/e2e/word-citations-phone.acl --json && a3s-test run tests/e2e/word-navigation-search.acl --json && a3s-test run tests/e2e/word-navigation-windowing.acl --json && a3s-test run tests/e2e/word-find-replace-phone.acl --json && a3s-test run tests/e2e/word-page-navigation.acl --json && a3s-test run tests/e2e/word-page-windowing.acl --json && a3s-test run tests/e2e/word-ai-question.acl --json && a3s-test run tests/e2e/word-picture-insert.acl --json && a3s-test run tests/e2e/word-picture-alt-text-phone.acl --json && a3s-test run tests/e2e/word-track-changes-phone.acl --json && a3s-test run tests/e2e/word-review-conflict-phone.acl --json && a3s-test run tests/e2e/word-revision-windowing.acl --json && a3s-test run tests/e2e/word-table-phone.acl --json && a3s-test run tests/e2e/word-table-borders.acl --json && a3s-test run tests/e2e/word-theme-table.acl --json && a3s-test run tests/e2e/word-styled-table.acl --json && a3s-test run tests/e2e/word-multi-page-table.acl --json && a3s-test run tests/e2e/word-cross-reference-phone.acl --json && a3s-test run tests/e2e/word-bookmark-links-phone.acl --json && a3s-test run tests/e2e/word-notes-phone.acl --json && a3s-test run tests/e2e/word-fields-phone.acl --json && a3s-test run tests/e2e/spreadsheet-phone-rename.acl --json && a3s-test run tests/e2e/spreadsheet-phone-find.acl --json && a3s-test run tests/e2e/spreadsheet-phone-context-menu.acl --json && a3s-test run tests/e2e/spreadsheet-phone-task-pane.acl --json && a3s-test run tests/e2e/presentation-cut-paste-focus.acl --json && a3s-test run tests/e2e/presentation-presenter-view.acl --json && a3s-test run tests/e2e/presentation-phone-chart-pane.acl --json && a3s-test run tests/e2e/presentation-phone-comments.acl --json && a3s-test run tests/e2e/pdf-thumbnail-keyboard.acl --json && a3s-test run tests/e2e/pdf-page-drawer-phone.acl --json && a3s-test run tests/e2e/markdown-phone-modes.acl --json && a3s-test run tests/e2e/office-sidebar-breakpoint.acl --json && a3s-test run tests/e2e/office-docs-navigation.acl --json && a3s-test run tests/e2e/collaboration-participants.acl --json && a3s-test run tests/e2e/collaboration-pdf-annotations.acl --json && a3s-test run tests/e2e/collaboration-spreadsheet-cells.acl --json && a3s-test run tests/e2e/collaboration-presentation-elements.acl --json",
91
+ "test:e2e:check": "bun run test:e2e:fixtures && a3s-test check tests/e2e/word-page-color.acl --json && a3s-test check tests/e2e/word-page-setup-phone.acl --json && a3s-test check tests/e2e/word-paragraph-layout-phone.acl --json && a3s-test check tests/e2e/word-list-formatting-phone.acl --json && a3s-test check tests/e2e/word-font-picker-phone.acl --json && a3s-test check tests/e2e/word-comments-drawer.acl --json && a3s-test check tests/e2e/word-comment-windowing.acl --json && a3s-test check tests/e2e/word-citations-phone.acl --json && a3s-test check tests/e2e/word-navigation-search.acl --json && a3s-test check tests/e2e/word-navigation-windowing.acl --json && a3s-test check tests/e2e/word-find-replace-phone.acl --json && a3s-test check tests/e2e/word-page-navigation.acl --json && a3s-test check tests/e2e/word-page-windowing.acl --json && a3s-test check tests/e2e/word-ai-question.acl --json && a3s-test check tests/e2e/word-picture-insert.acl --json && a3s-test check tests/e2e/word-picture-alt-text-phone.acl --json && a3s-test check tests/e2e/word-track-changes-phone.acl --json && a3s-test check tests/e2e/word-review-conflict-phone.acl --json && a3s-test check tests/e2e/word-revision-windowing.acl --json && a3s-test check tests/e2e/word-table-phone.acl --json && a3s-test check tests/e2e/word-table-borders.acl --json && a3s-test check tests/e2e/word-theme-table.acl --json && a3s-test check tests/e2e/word-styled-table.acl --json && a3s-test check tests/e2e/word-multi-page-table.acl --json && a3s-test check tests/e2e/word-cross-reference-phone.acl --json && a3s-test check tests/e2e/word-bookmark-links-phone.acl --json && a3s-test check tests/e2e/word-notes-phone.acl --json && a3s-test check tests/e2e/word-fields-phone.acl --json && a3s-test check tests/e2e/spreadsheet-phone-rename.acl --json && a3s-test check tests/e2e/spreadsheet-phone-find.acl --json && a3s-test check tests/e2e/spreadsheet-phone-context-menu.acl --json && a3s-test check tests/e2e/spreadsheet-phone-task-pane.acl --json && a3s-test check tests/e2e/presentation-cut-paste-focus.acl --json && a3s-test check tests/e2e/presentation-presenter-view.acl --json && a3s-test check tests/e2e/presentation-phone-chart-pane.acl --json && a3s-test check tests/e2e/presentation-phone-comments.acl --json && a3s-test check tests/e2e/pdf-thumbnail-keyboard.acl --json && a3s-test check tests/e2e/pdf-page-drawer-phone.acl --json && a3s-test check tests/e2e/markdown-phone-modes.acl --json && a3s-test check tests/e2e/office-sidebar-breakpoint.acl --json && a3s-test check tests/e2e/office-docs-navigation.acl --json && a3s-test check tests/e2e/collaboration-participants.acl --json && a3s-test check tests/e2e/collaboration-pdf-annotations.acl --json && a3s-test check tests/e2e/collaboration-spreadsheet-cells.acl --json && a3s-test check tests/e2e/collaboration-presentation-elements.acl --json",
92
92
  "test:e2e:fixtures": "bun scripts/create-e2e-fixtures.ts",
93
93
  "test:e2e:initial-focus": "a3s-test run tests/e2e/office-editor-initial-focus.acl --json",
94
94
  "test:e2e:initial-focus:check": "a3s-test check tests/e2e/office-editor-initial-focus.acl --json",
@@ -98,6 +98,8 @@
98
98
  "test:e2e:collaboration-pdf-annotations:check": "a3s-test check tests/e2e/collaboration-pdf-annotations.acl --json",
99
99
  "test:e2e:collaboration-spreadsheet-cells": "a3s-test run tests/e2e/collaboration-spreadsheet-cells.acl --json",
100
100
  "test:e2e:collaboration-spreadsheet-cells:check": "a3s-test check tests/e2e/collaboration-spreadsheet-cells.acl --json",
101
+ "test:e2e:collaboration-presentation-elements": "a3s-test run tests/e2e/collaboration-presentation-elements.acl --json",
102
+ "test:e2e:collaboration-presentation-elements:check": "a3s-test check tests/e2e/collaboration-presentation-elements.acl --json",
101
103
  "test:e2e:wps-layout": "bun run test:e2e:fixtures && a3s-test run tests/e2e/word-wps-layout-parity.acl --json && a3s-test run tests/e2e/word-wps-font-grid-parity.acl --json",
102
104
  "test:e2e:wps-layout:check": "bun run test:e2e:fixtures && a3s-test check tests/e2e/word-wps-layout-parity.acl --json && a3s-test check tests/e2e/word-wps-font-grid-parity.acl --json",
103
105
  "test:e2e:writer-ribbon": "a3s-test run tests/e2e/word-ribbon-collapse.acl --json",