docxodus 12.2.0 → 12.4.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.
Files changed (58) hide show
  1. package/README.md +11 -4
  2. package/dist/editor.bundle.js +1208 -28
  3. package/dist/editor.d.ts +10 -1
  4. package/dist/editor.d.ts.map +1 -1
  5. package/dist/editor.js +48 -16
  6. package/dist/editor.js.map +1 -1
  7. package/dist/embed.bundle.js +1212 -31
  8. package/dist/embed.d.ts +5 -2
  9. package/dist/embed.d.ts.map +1 -1
  10. package/dist/embed.iife.js +1212 -31
  11. package/dist/embed.js +10 -3
  12. package/dist/embed.js.map +1 -1
  13. package/dist/export-assets.json +9 -9
  14. package/dist/history-checkpoints.d.ts +50 -0
  15. package/dist/history-checkpoints.d.ts.map +1 -0
  16. package/dist/history-checkpoints.js +102 -0
  17. package/dist/history-checkpoints.js.map +1 -0
  18. package/dist/history-controls.d.ts +33 -0
  19. package/dist/history-controls.d.ts.map +1 -0
  20. package/dist/history-controls.js +375 -0
  21. package/dist/history-controls.js.map +1 -0
  22. package/dist/history-indexeddb.d.ts +12 -0
  23. package/dist/history-indexeddb.d.ts.map +1 -0
  24. package/dist/history-indexeddb.js +154 -0
  25. package/dist/history-indexeddb.js.map +1 -0
  26. package/dist/index.d.ts +4 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +4 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/ribbon-chrome.d.ts +1 -1
  31. package/dist/ribbon-chrome.d.ts.map +1 -1
  32. package/dist/ribbon-chrome.js +6 -3
  33. package/dist/ribbon-chrome.js.map +1 -1
  34. package/dist/ribbon-history.d.ts +85 -0
  35. package/dist/ribbon-history.d.ts.map +1 -0
  36. package/dist/ribbon-history.js +508 -0
  37. package/dist/ribbon-history.js.map +1 -0
  38. package/dist/ribbon.d.ts +6 -0
  39. package/dist/ribbon.d.ts.map +1 -1
  40. package/dist/ribbon.js +50 -19
  41. package/dist/ribbon.js.map +1 -1
  42. package/dist/viewport.d.ts +3 -1
  43. package/dist/viewport.d.ts.map +1 -1
  44. package/dist/viewport.js +14 -0
  45. package/dist/viewport.js.map +1 -1
  46. package/dist/wasm/_framework/Docxodus.wasm +0 -0
  47. package/dist/wasm/_framework/Docxodus.wasm.br +0 -0
  48. package/dist/wasm/_framework/DocxodusWasm.wasm +0 -0
  49. package/dist/wasm/_framework/DocxodusWasm.wasm.br +0 -0
  50. package/dist/wasm/_framework/System.Private.CoreLib.wasm +0 -0
  51. package/dist/wasm/_framework/System.Private.CoreLib.wasm.br +0 -0
  52. package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm +0 -0
  53. package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm.br +0 -0
  54. package/dist/wasm/_framework/dotnet.boot.js +6 -6
  55. package/dist/wasm/_framework/dotnet.boot.js.br +0 -0
  56. package/dist/wasm/_framework/dotnet.native.wasm +0 -0
  57. package/dist/wasm/_framework/dotnet.native.wasm.br +0 -0
  58. package/package.json +8 -7
@@ -2278,7 +2278,7 @@ function createMemoryHistoryStorage(maxBlobBytes = 256 * 1024 * 1024) {
2278
2278
  return `${reference.digest.value}:${reference.length}`;
2279
2279
  };
2280
2280
  const equalReference = (a, b) => a.length === b.length && a.digest.algorithm === b.digest.algorithm && a.digest.value === b.digest.value;
2281
- const equalHead = (a, b) => a === null || b === null ? a === b : a.revision === b.revision && equalReference(a.state, b.state);
2281
+ const equalHead2 = (a, b) => a === null || b === null ? a === b : a.revision === b.revision && equalReference(a.state, b.state);
2282
2282
  return {
2283
2283
  async readBlob(reference) {
2284
2284
  return blobs.get(key(reference))?.slice() ?? null;
@@ -2300,7 +2300,7 @@ function createMemoryHistoryStorage(maxBlobBytes = 256 * 1024 * 1024) {
2300
2300
  async advanceHead(documentId, expected, state) {
2301
2301
  key(state);
2302
2302
  const current = heads.get(documentId) ?? null;
2303
- if (!equalHead(current, expected)) return null;
2303
+ if (!equalHead2(current, expected)) return null;
2304
2304
  const revision = BigInt(current?.revision ?? "0") + 1n;
2305
2305
  if (revision > 9223372036854775807n) throw new RangeError("History revision exhausted.");
2306
2306
  const head = { revision: String(revision), state: copy(state) };
@@ -2320,6 +2320,627 @@ function createMemoryHistoryStorage(maxBlobBytes = 256 * 1024 * 1024) {
2320
2320
  };
2321
2321
  }
2322
2322
 
2323
+ // src/history-checkpoints.ts
2324
+ var HistoryCheckpoints = class _HistoryCheckpoints {
2325
+ constructor(document2, journal) {
2326
+ this.document = document2;
2327
+ this.journal = journal;
2328
+ this.current = null;
2329
+ this.request = null;
2330
+ this.running = false;
2331
+ this.stale = false;
2332
+ }
2333
+ /** Pass an already captured view when opening its exact version in an editor (for example, after import). */
2334
+ static async open(document2, journal, view) {
2335
+ const controls = new _HistoryCheckpoints(document2, journal);
2336
+ controls.request = structuredClone(await journal.read());
2337
+ if (controls.request && controls.request.documentId !== document2.documentId)
2338
+ throw new DocxHistoryError("InvalidRequest", "The pending checkpoint belongs to another document.");
2339
+ controls.current = view === void 0 ? await document2.read() : structuredClone(view);
2340
+ if (controls.current && controls.current.state.documentId !== document2.documentId)
2341
+ throw new DocxHistoryError("InvalidRequest", "The captured view belongs to another document.");
2342
+ return controls;
2343
+ }
2344
+ get view() {
2345
+ return structuredClone(this.current);
2346
+ }
2347
+ get hasPending() {
2348
+ return this.request !== null;
2349
+ }
2350
+ /** Snapshot of the exact request a retry will recover, including its original operation kind. */
2351
+ get pendingRequest() {
2352
+ return structuredClone(this.request);
2353
+ }
2354
+ get needsRefresh() {
2355
+ return this.stale;
2356
+ }
2357
+ async refresh() {
2358
+ return this.exclusive(async () => {
2359
+ this.current = await this.document.read();
2360
+ this.stale = false;
2361
+ return this.view;
2362
+ });
2363
+ }
2364
+ async save(bytes, metadata) {
2365
+ return this.start({
2366
+ kind: "save",
2367
+ head: this.current?.head ?? null,
2368
+ bytes,
2369
+ id: crypto.randomUUID(),
2370
+ documentId: this.document.documentId,
2371
+ metadata
2372
+ });
2373
+ }
2374
+ async restore(target, metadata) {
2375
+ if (!this.current) throw new DocxHistoryError("NotFound", "Save a checkpoint before restoring a version.");
2376
+ return this.start({
2377
+ kind: "restore",
2378
+ head: this.current.head,
2379
+ target,
2380
+ id: crypto.randomUUID(),
2381
+ documentId: this.document.documentId,
2382
+ metadata
2383
+ });
2384
+ }
2385
+ async retry() {
2386
+ return this.exclusive(async () => {
2387
+ if (!this.request) throw new DocxHistoryError("InvalidRequest", "There is no pending checkpoint.");
2388
+ return this.publish();
2389
+ });
2390
+ }
2391
+ async start(request) {
2392
+ return this.exclusive(async () => {
2393
+ if (this.request) throw new DocxHistoryError("PendingRequest", "Retry the pending checkpoint first.");
2394
+ if (this.stale) throw new DocxHistoryError("StaleHead", "Refresh history before saving again.");
2395
+ this.request = structuredClone(request);
2396
+ return this.publish();
2397
+ });
2398
+ }
2399
+ async publish() {
2400
+ const intended = this.request;
2401
+ const request = await this.journal.put(structuredClone(intended));
2402
+ this.request = structuredClone(request);
2403
+ if (request.id !== intended.id)
2404
+ throw new DocxHistoryError("PendingRequest", "Another tab has a pending checkpoint. Retry it before saving your draft.");
2405
+ let view;
2406
+ try {
2407
+ view = request.kind === "save" ? await this.document.createVersion(request.head, request.bytes, request.metadata, request.id) : await this.document.restoreVersion(request.head, request.target, request.metadata, request.id);
2408
+ } catch (error) {
2409
+ if (error instanceof DocxHistoryError && error.code === "StaleHead") {
2410
+ this.stale = true;
2411
+ await this.journal.remove(request.id);
2412
+ this.request = null;
2413
+ }
2414
+ throw error;
2415
+ }
2416
+ this.current = view;
2417
+ await this.journal.remove(request.id);
2418
+ this.request = null;
2419
+ return this.view;
2420
+ }
2421
+ async exclusive(action) {
2422
+ if (this.running) throw new DocxHistoryError("Busy", "A history command is still running.");
2423
+ this.running = true;
2424
+ try {
2425
+ return await action();
2426
+ } finally {
2427
+ this.running = false;
2428
+ }
2429
+ }
2430
+ };
2431
+
2432
+ // src/history-indexeddb.ts
2433
+ async function openIndexedDbHistoryStore(name) {
2434
+ const db = await new Promise((resolve, reject) => {
2435
+ let blocked = false;
2436
+ const request = indexedDB.open(name, 1);
2437
+ request.onupgradeneeded = () => {
2438
+ for (const store of ["blobs", "heads", "requests"]) request.result.createObjectStore(store);
2439
+ };
2440
+ request.onsuccess = () => {
2441
+ if (blocked) request.result.close();
2442
+ else resolve(request.result);
2443
+ };
2444
+ request.onerror = () => reject(request.error);
2445
+ request.onblocked = () => {
2446
+ blocked = true;
2447
+ reject(new Error("Close other tabs using this history store, then try again."));
2448
+ };
2449
+ });
2450
+ db.onversionchange = () => db.close();
2451
+ function transaction(name2, mode, action) {
2452
+ return new Promise((resolve, reject) => {
2453
+ const tx = db.transaction(name2, mode);
2454
+ let value;
2455
+ tx.oncomplete = () => resolve(value);
2456
+ tx.onabort = () => reject(tx.error ?? new Error("History storage transaction was aborted."));
2457
+ try {
2458
+ action(tx.objectStore(name2), (result) => {
2459
+ value = result;
2460
+ });
2461
+ } catch (error) {
2462
+ tx.abort();
2463
+ reject(error);
2464
+ }
2465
+ });
2466
+ }
2467
+ const storage = {
2468
+ readBlob(reference) {
2469
+ const key = referenceKey(reference);
2470
+ return transaction("blobs", "readonly", (store, result) => {
2471
+ store.get(key).onsuccess = (event) => result(event.target.result ?? null);
2472
+ });
2473
+ },
2474
+ async putBlob(reference, bytes) {
2475
+ const key = referenceKey(reference);
2476
+ const captured = bytes.slice();
2477
+ if (captured.length !== reference.length) throw new DocxHistoryError("PayloadMismatch", "Stored document length does not match its reference.");
2478
+ const hash = Array.from(
2479
+ new Uint8Array(await crypto.subtle.digest("SHA-256", captured)),
2480
+ (byte) => byte.toString(16).padStart(2, "0")
2481
+ ).join("");
2482
+ if (hash !== key.split(":")[0])
2483
+ throw new DocxHistoryError("PayloadMismatch", "Stored document bytes do not match their reference.");
2484
+ await transaction("blobs", "readwrite", (store, result) => {
2485
+ store.put(captured, key);
2486
+ result();
2487
+ });
2488
+ },
2489
+ readHead(documentId) {
2490
+ return transaction("heads", "readonly", (store, result) => {
2491
+ store.get(documentId).onsuccess = (event) => result(event.target.result ?? null);
2492
+ });
2493
+ },
2494
+ advanceHead(documentId, expected, state) {
2495
+ referenceKey(state);
2496
+ const captured = structuredClone({ expected, state });
2497
+ if (expected) validateHead(expected);
2498
+ const revision = BigInt(expected?.revision ?? "0") + 1n;
2499
+ if (revision > 9223372036854775807n) throw new RangeError("History revision exhausted.");
2500
+ return transaction("heads", "readwrite", (store, result) => {
2501
+ store.get(documentId).onsuccess = (event) => {
2502
+ const current = event.target.result ?? null;
2503
+ if (!equalHead(current, captured.expected)) {
2504
+ result(null);
2505
+ return;
2506
+ }
2507
+ const head = { revision: String(revision), state: captured.state };
2508
+ store.put(head, documentId);
2509
+ result(head);
2510
+ };
2511
+ });
2512
+ },
2513
+ initializeHead(documentId, head) {
2514
+ validateHead(head);
2515
+ const captured = structuredClone(head);
2516
+ return transaction("heads", "readwrite", (store, result) => {
2517
+ store.get(documentId).onsuccess = (event) => {
2518
+ const existing = event.target.result;
2519
+ if (existing) {
2520
+ result({ initialized: false, head: existing });
2521
+ return;
2522
+ }
2523
+ store.put(captured, documentId);
2524
+ result({ initialized: true, head: captured });
2525
+ };
2526
+ });
2527
+ }
2528
+ };
2529
+ return {
2530
+ storage,
2531
+ journal(documentId) {
2532
+ return {
2533
+ read: () => transaction("requests", "readonly", (store, result) => {
2534
+ store.get(documentId).onsuccess = (event) => result(event.target.result ?? null);
2535
+ }),
2536
+ put(request) {
2537
+ if (request.documentId !== documentId) throw new Error("Checkpoint document identity does not match.");
2538
+ const captured = structuredClone(request);
2539
+ return transaction("requests", "readwrite", (store, result) => {
2540
+ store.get(documentId).onsuccess = (event) => {
2541
+ const existing = event.target.result;
2542
+ if (existing) {
2543
+ result(existing);
2544
+ return;
2545
+ }
2546
+ store.put(captured, documentId);
2547
+ result(captured);
2548
+ };
2549
+ });
2550
+ },
2551
+ remove(requestId) {
2552
+ return transaction("requests", "readwrite", (store, result) => {
2553
+ store.get(documentId).onsuccess = (event) => {
2554
+ if (event.target.result?.id === requestId) store.delete(documentId);
2555
+ result();
2556
+ };
2557
+ });
2558
+ }
2559
+ };
2560
+ },
2561
+ close: () => db.close()
2562
+ };
2563
+ }
2564
+ function referenceKey(reference) {
2565
+ if (reference.digest.algorithm !== "SHA-256" || !/^[a-f0-9]{64}$/.test(reference.digest.value) || !Number.isSafeInteger(reference.length) || reference.length < 0)
2566
+ throw new DocxHistoryError("InvalidRequest", "Invalid history blob reference.");
2567
+ return `${reference.digest.value}:${reference.length}`;
2568
+ }
2569
+ function validateHead(head) {
2570
+ referenceKey(head.state);
2571
+ if (typeof head.revision !== "string" || !/^[1-9][0-9]*$/.test(head.revision) || BigInt(head.revision) > 9223372036854775807n)
2572
+ throw new DocxHistoryError("InvalidRequest", "Invalid history revision.");
2573
+ }
2574
+ function equalHead(a, b) {
2575
+ return a === null || b === null ? a === b : a.revision === b.revision && a.state.length === b.state.length && a.state.digest.algorithm === b.state.digest.algorithm && a.state.digest.value === b.state.digest.value;
2576
+ }
2577
+
2578
+ // src/history-controls.ts
2579
+ function mountHistoryControls(container, options) {
2580
+ const pageSize = options.pageSize ?? 25;
2581
+ if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 100) throw new RangeError("History page size must be 1\u2013100.");
2582
+ if (options.checkpoints && options.checkpoints.document !== options.reader)
2583
+ throw new Error("History controls and checkpoints must use the same document.");
2584
+ return new HistoryPanel(container, options, pageSize);
2585
+ }
2586
+ var HistoryPanel = class {
2587
+ constructor(container, options, pageSize) {
2588
+ this.options = options;
2589
+ this.pageSize = pageSize;
2590
+ this.actions = /* @__PURE__ */ new Map();
2591
+ this.events = new AbortController();
2592
+ this.records = [];
2593
+ this.next = null;
2594
+ this.view = null;
2595
+ this.active = null;
2596
+ this.destroyed = false;
2597
+ const doc = container.ownerDocument;
2598
+ this.element = doc.createElement("section");
2599
+ this.element.className = "dx-history";
2600
+ this.element.setAttribute("aria-label", "Version history");
2601
+ const style = doc.createElement("style");
2602
+ style.textContent = HISTORY_CSS;
2603
+ const title = doc.createElement("h2");
2604
+ title.textContent = "Version history";
2605
+ this.status = doc.createElement("p");
2606
+ this.status.setAttribute("role", "status");
2607
+ this.status.setAttribute("aria-live", "polite");
2608
+ this.fieldset = doc.createElement("fieldset");
2609
+ const legend = doc.createElement("legend");
2610
+ legend.textContent = "Saved versions";
2611
+ this.fieldset.append(legend);
2612
+ this.element.append(style, title, this.status, this.fieldset);
2613
+ container.append(this.element);
2614
+ const note = doc.createElement("p");
2615
+ note.dataset.historyExisting = "";
2616
+ note.textContent = options.checkpoints ? "Browse saved versions without changing your draft." : "Read-only history. Preview or download any saved version.";
2617
+ this.fieldset.append(note);
2618
+ this.button("refresh", "Refresh history", () => this.load(true));
2619
+ this.button("latest", "Preview latest", async () => this.preview(await options.reader.exportDocx(), "Latest saved version"));
2620
+ this.versions = this.select("Version");
2621
+ this.versions.size = 6;
2622
+ this.detail = doc.createElement("p");
2623
+ this.fieldset.append(this.detail);
2624
+ this.versions.addEventListener("change", () => this.update(), { signal: this.events.signal });
2625
+ this.button("preview", "Preview selected", async () => this.preview(await options.reader.exportDocx(this.selected().id), versionTitle(this.selected())));
2626
+ this.button("download", "Download selected", async () => this.download(await options.reader.exportDocx(this.selected().id), `version-${this.selected().record.sequence}.docx`));
2627
+ const comparison = this.disclosure("Compare versions");
2628
+ this.before = this.select("Compare from", comparison);
2629
+ this.before.addEventListener("change", () => this.update(), { signal: this.events.signal });
2630
+ const compareNote = doc.createElement("p");
2631
+ compareNote.textContent = "Compare from this version to the selected version above.";
2632
+ comparison.append(compareNote);
2633
+ this.button("compare", "Compare versions", async () => {
2634
+ const before = this.records[Number(this.before.value)];
2635
+ await this.preview(await options.reader.compareVersions(before.id, this.selected().id), "Comparison with tracked changes");
2636
+ }, comparison);
2637
+ this.button("more", "Load older versions", async () => {
2638
+ if (this.next) await this.page(this.next, false);
2639
+ });
2640
+ const saveGroup = doc.createElement("div");
2641
+ saveGroup.className = "dx-history-save";
2642
+ this.fieldset.insertBefore(saveGroup, note);
2643
+ const attribution = doc.createElement("details");
2644
+ const attributionTitle = doc.createElement("summary");
2645
+ attributionTitle.textContent = "Saved by";
2646
+ attribution.append(attributionTitle);
2647
+ saveGroup.append(attribution);
2648
+ this.author = this.input("Your name", "text", options.author ?? "You", attribution);
2649
+ this.label = this.input("Version name (optional)", "text", "", saveGroup);
2650
+ saveGroup.hidden = !options.checkpoints;
2651
+ this.author.parentElement.hidden = this.label.parentElement.hidden = !options.checkpoints;
2652
+ this.button("save", "Save version", async () => {
2653
+ const view = await options.checkpoints.save(await options.capture(), this.metadata());
2654
+ await options.onCheckpoint?.(view, "save");
2655
+ await this.load(false);
2656
+ this.label.value = "";
2657
+ this.status.textContent = "Version saved. Your draft remains open.";
2658
+ }, saveGroup);
2659
+ saveGroup.append(attribution);
2660
+ this.button("restore", "Restore selected", async () => {
2661
+ const version = this.selected();
2662
+ const confirmed = options.confirmRestore ? await options.confirmRestore(versionTitle(version)) : doc.defaultView?.confirm(`Restore ${versionTitle(version)} as a new saved version?
2663
+
2664
+ Your current draft and all later versions will be kept.`);
2665
+ if (!confirmed) {
2666
+ this.status.textContent = "Restore canceled. Your draft and history are unchanged.";
2667
+ return;
2668
+ }
2669
+ const view = await options.checkpoints.restore(version.id, this.metadata());
2670
+ await options.onCheckpoint?.(view, "restore");
2671
+ await this.load(false);
2672
+ this.status.textContent = options.restoreUpdatesDraft ? "Version restored. All saved versions are kept." : "Restored as a new saved version. Your draft and later versions are kept.";
2673
+ });
2674
+ const restoreNote = doc.createElement("p");
2675
+ restoreNote.hidden = !options.checkpoints;
2676
+ if (options.checkpoints) restoreNote.dataset.historyExisting = "";
2677
+ restoreNote.textContent = options.restoreUpdatesDraft ? "Restore returns your document to this version and keeps every saved version." : "Restore creates a new saved version. Preview latest to view it; your draft stays open.";
2678
+ this.fieldset.append(restoreNote);
2679
+ this.button("retry", "Retry save", async () => {
2680
+ const request = options.checkpoints.pendingRequest;
2681
+ if (request?.kind === "restore" && options.confirmRestore) {
2682
+ const target = await options.reader.getVersion(request.target);
2683
+ if (!await options.confirmRestore(versionTitle(target))) {
2684
+ this.status.textContent = "Restore retry canceled. Your draft is unchanged; the saved action still needs recovery.";
2685
+ return;
2686
+ }
2687
+ }
2688
+ const view = await options.checkpoints.retry();
2689
+ await options.onCheckpoint?.(view, "retry", request ?? void 0);
2690
+ await this.load(false);
2691
+ this.status.textContent = request?.kind === "restore" && options.restoreUpdatesDraft ? "Version restored. All saved versions are kept. Refresh history to check for newer versions." : "Saved version recovered. Your draft remains open. Refresh history to check for newer versions.";
2692
+ });
2693
+ const sharing = this.disclosure("Download with history");
2694
+ const sharingNote = doc.createElement("p");
2695
+ sharingNote.textContent = "Includes retained drafts and collaboration proposals. Share this file only when you want to include that history. A DOCX download keeps existing Word comments and revisions, without external history.";
2696
+ sharing.append(sharingNote);
2697
+ this.button("archive", "Download with version history", async () => this.download(await options.reader.exportHistoryArchive(), "docxhistory"), sharing);
2698
+ const portability = doc.createElement("p");
2699
+ portability.textContent = "Saved versions stay on this device. Download a history file to keep a portable copy; clearing browser data removes local versions.";
2700
+ sharing.append(portability);
2701
+ const time = this.disclosure("Find a version by time");
2702
+ const cutoff = this.input("Saved at or before (local time)", "datetime-local", "", time);
2703
+ this.button("time", "Preview at time", async () => {
2704
+ if (!cutoff.value) throw new DocxHistoryError("InvalidRequest", "Choose a date and time first.");
2705
+ const sequence = await options.reader.resolveSequenceAtTime(new Date(cutoff.value).toISOString());
2706
+ await this.preview(await options.reader.materialize(sequence), "Version at selected time");
2707
+ }, time);
2708
+ const activity = this.disclosure("Recorded collaboration");
2709
+ const decisions = doc.createElement("ol");
2710
+ let showOlderActivity = () => {
2711
+ };
2712
+ this.button("activity", "Load activity", async () => {
2713
+ const { operations } = await options.reader.readOperationsSince(null);
2714
+ const resolved = new Set(operations.filter((op) => op.record.status === "accepted").map((op) => op.input.request.resolves?.digest.value));
2715
+ let shown = 0;
2716
+ showOlderActivity = () => {
2717
+ const page = operations.slice(Math.max(0, operations.length - shown - this.pageSize), operations.length - shown).reverse();
2718
+ decisions.append(...page.map((op) => this.activityItem(op, resolved.has(op.id.digest.value))));
2719
+ shown += page.length;
2720
+ this.actions.get("activity-more").hidden = shown >= operations.length;
2721
+ };
2722
+ decisions.replaceChildren();
2723
+ showOlderActivity();
2724
+ this.status.textContent = operations.length ? "Recorded activity loaded." : "No recorded collaboration.";
2725
+ }, activity);
2726
+ activity.append(decisions);
2727
+ this.button("activity-more", "Load older activity", async () => showOlderActivity(), activity);
2728
+ this.actions.get("activity-more").hidden = true;
2729
+ this.ready = this.run("Loading history", () => this.load(false));
2730
+ }
2731
+ refresh() {
2732
+ return this.run("Loading history", () => this.load(true));
2733
+ }
2734
+ async destroy() {
2735
+ this.destroyed = true;
2736
+ this.events.abort();
2737
+ this.element.remove();
2738
+ await this.active?.catch(() => {
2739
+ });
2740
+ }
2741
+ async load(refresh) {
2742
+ this.view = this.options.checkpoints ? refresh ? await this.options.checkpoints.refresh() : this.options.checkpoints.view : await this.options.reader.read();
2743
+ if (this.view) await this.page(this.view.version.id, true);
2744
+ else {
2745
+ this.records = [];
2746
+ this.next = null;
2747
+ this.versions.replaceChildren();
2748
+ this.before.replaceChildren();
2749
+ }
2750
+ this.status.textContent = this.options.checkpoints?.hasPending ? "A save needs recovery. Retry it before saving more changes." : this.view ? "Select a version to preview, download or restore." : "No saved versions yet. Save your first version when you are ready.";
2751
+ }
2752
+ async page(cursor, reset) {
2753
+ const page = await this.options.reader.listVersions(cursor, this.pageSize);
2754
+ if (reset) {
2755
+ this.records = [];
2756
+ this.versions.replaceChildren();
2757
+ this.before.replaceChildren();
2758
+ }
2759
+ const start2 = this.records.length;
2760
+ this.records.push(...page.versions);
2761
+ this.next = page.next;
2762
+ for (let index = start2; index < this.records.length; index++) {
2763
+ for (const select of [this.versions, this.before]) {
2764
+ const option = select.ownerDocument.createElement("option");
2765
+ option.value = String(index);
2766
+ option.textContent = option.title = versionTitle(this.records[index]);
2767
+ select.append(option);
2768
+ }
2769
+ }
2770
+ if (reset) {
2771
+ this.versions.value = "0";
2772
+ this.before.value = this.records.length > 1 ? "1" : "0";
2773
+ }
2774
+ }
2775
+ update() {
2776
+ const hasVersion = this.records.length > 0;
2777
+ const pending = this.options.checkpoints?.hasPending ?? false;
2778
+ const stale = this.options.checkpoints?.needsRefresh ?? false;
2779
+ for (const key of ["latest", "preview", "download", "archive", "time", "activity"]) this.actions.get(key).disabled = !hasVersion;
2780
+ this.actions.get("more").hidden = !this.next;
2781
+ this.actions.get("compare").disabled = !hasVersion || this.before.value === this.versions.value;
2782
+ this.actions.get("save").hidden = !this.options.checkpoints || !this.options.capture;
2783
+ this.actions.get("save").disabled = pending || stale;
2784
+ this.actions.get("restore").hidden = !this.options.checkpoints;
2785
+ this.actions.get("restore").disabled = !hasVersion || pending || stale;
2786
+ this.actions.get("retry").hidden = !pending;
2787
+ this.versions.parentElement.hidden = this.detail.hidden = !hasVersion;
2788
+ this.versions.size = Math.max(2, Math.min(6, this.records.length));
2789
+ for (const key of ["latest", "preview", "download"]) this.actions.get(key).hidden = !hasVersion;
2790
+ this.actions.get("restore").hidden = !hasVersion || !this.options.checkpoints;
2791
+ for (const element of Array.from(this.element.querySelectorAll("[data-history-existing]"))) element.hidden = !hasVersion;
2792
+ this.detail.textContent = hasVersion ? [
2793
+ versionTitle(this.selected()),
2794
+ this.selected().record.metadata.message,
2795
+ this.selected().record.restoredFrom ? "Restored from an earlier version." : ""
2796
+ ].filter(Boolean).join(" \u2014 ") : "";
2797
+ }
2798
+ selected() {
2799
+ return this.records[Number(this.versions.value)];
2800
+ }
2801
+ activityItem(op, resolved) {
2802
+ const doc = this.element.ownerDocument;
2803
+ const item = doc.createElement("li");
2804
+ const state = op.record.status === "accepted" ? "Accepted" : resolved ? "Conflict resolved" : "Conflict needs review";
2805
+ const label = doc.createElement("p");
2806
+ label.textContent = `${op.input.request.metadata.author} \xB7 ${state} \xB7 ${formatTime(op.input.request.metadata.createdAt)}`;
2807
+ item.append(label);
2808
+ const description = doc.createElement("p");
2809
+ this.commandButton("View decision", async () => {
2810
+ const decision = await this.options.reader.getOperation(op.id);
2811
+ const { kind, metadata } = decision.input.request;
2812
+ description.textContent = [
2813
+ { text: "Text edit", package: "Document edit", discard: "Discarded proposal" }[kind],
2814
+ metadata.label,
2815
+ metadata.message,
2816
+ decision.record.conflict
2817
+ ].filter(Boolean).join(" \u2014 ");
2818
+ }, item);
2819
+ item.append(description);
2820
+ this.commandButton("Download proposal", async () => {
2821
+ await this.download(await this.options.reader.exportOperationProposal(op.id), `proposal-${op.record.revision}.docx`);
2822
+ }, item);
2823
+ return item;
2824
+ }
2825
+ metadata() {
2826
+ return { author: this.author.value.trim() || "You", createdAt: (/* @__PURE__ */ new Date()).toISOString(), label: this.label.value.trim() || void 0 };
2827
+ }
2828
+ async preview(bytes, title) {
2829
+ if (!this.destroyed) await this.options.preview(bytes, title);
2830
+ }
2831
+ async download(bytes, suffix) {
2832
+ if (this.destroyed) return;
2833
+ const stem = (this.options.documentName ?? "document").replace(/\.(docx|docxhistory)$/i, "");
2834
+ const name = suffix === "docxhistory" ? `${stem}.docxhistory` : `${stem}-${suffix}`;
2835
+ if (this.options.download) {
2836
+ await this.options.download(bytes, name);
2837
+ return;
2838
+ }
2839
+ const url = URL.createObjectURL(new Blob([bytes.slice()], { type: suffix === "docxhistory" ? "application/octet-stream" : "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }));
2840
+ const link = this.element.ownerDocument.createElement("a");
2841
+ link.href = url;
2842
+ link.download = name;
2843
+ link.click();
2844
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
2845
+ }
2846
+ async run(label, action) {
2847
+ if (this.destroyed) throw new DocxHistoryError("Closed", "History controls are closed.");
2848
+ if (this.active) throw new DocxHistoryError("Busy", "A history command is still running.");
2849
+ this.fieldset.disabled = true;
2850
+ this.element.setAttribute("aria-busy", "true");
2851
+ this.status.textContent = `${label}\u2026`;
2852
+ const work = Promise.resolve().then(action);
2853
+ this.active = work;
2854
+ try {
2855
+ await work;
2856
+ if (this.status.textContent === `${label}\u2026`) this.status.textContent = "Ready.";
2857
+ } catch (error) {
2858
+ if (!this.destroyed) this.status.textContent = historyControlError(error, this.options.checkpoints?.hasPending);
2859
+ throw error;
2860
+ } finally {
2861
+ this.active = null;
2862
+ if (!this.destroyed) {
2863
+ this.fieldset.disabled = false;
2864
+ this.element.setAttribute("aria-busy", "false");
2865
+ this.update();
2866
+ }
2867
+ }
2868
+ }
2869
+ button(key, title, action, parent = this.fieldset) {
2870
+ const button = this.commandButton(title, action, parent);
2871
+ button.dataset.historyAction = key;
2872
+ this.actions.set(key, button);
2873
+ }
2874
+ commandButton(title, action, parent) {
2875
+ const button = parent.ownerDocument.createElement("button");
2876
+ button.type = "button";
2877
+ button.textContent = title;
2878
+ button.addEventListener("click", () => {
2879
+ void this.run(title, action).catch(() => {
2880
+ });
2881
+ }, { signal: this.events.signal });
2882
+ parent.append(button);
2883
+ return button;
2884
+ }
2885
+ select(title, parent = this.fieldset) {
2886
+ const label = this.fieldset.ownerDocument.createElement("label");
2887
+ label.textContent = title;
2888
+ const select = label.ownerDocument.createElement("select");
2889
+ select.setAttribute("aria-label", title);
2890
+ label.append(select);
2891
+ parent.append(label);
2892
+ return select;
2893
+ }
2894
+ input(title, type, value = "", parent = this.fieldset) {
2895
+ const label = parent.ownerDocument.createElement("label");
2896
+ label.textContent = title;
2897
+ const input = label.ownerDocument.createElement("input");
2898
+ input.type = type;
2899
+ input.value = value;
2900
+ label.append(input);
2901
+ parent.append(label);
2902
+ return input;
2903
+ }
2904
+ disclosure(title) {
2905
+ const details = this.fieldset.ownerDocument.createElement("details");
2906
+ details.dataset.historyExisting = "";
2907
+ const summary = details.ownerDocument.createElement("summary");
2908
+ summary.textContent = title;
2909
+ details.append(summary);
2910
+ this.fieldset.append(details);
2911
+ return details;
2912
+ }
2913
+ };
2914
+ function historyControlError(error, pending = false) {
2915
+ const code = error instanceof DocxHistoryError ? error.code : "";
2916
+ if (code === "StaleHead") return "A newer saved version exists. Your draft is safe. Refresh history, review the newer version, then save again.";
2917
+ if (code === "ImportConflict") return "A different local history already exists. Open this file read-only to explore it.";
2918
+ if (code === "InitializationUnsupported") return "This storage cannot import history. Open read-only or choose storage that supports importing.";
2919
+ if (pending) return "The save could not be confirmed. Your draft is safe. Retry save to recover your saved version.";
2920
+ if (code === "ResourceLimit") return "This history file exceeds browser processing limits, which can apply even below 64 MiB. Your document is unchanged.";
2921
+ if (code === "UnsupportedVersion") return "This history file uses an unsupported version. Open it with a newer app. Your document is unchanged.";
2922
+ if (code === "InvalidManifest") return "This history file is damaged or incomplete. Choose another copy. Your document is unchanged.";
2923
+ return `History could not be loaded. Your document is unchanged. ${error instanceof Error ? error.message : "Please try again."}`;
2924
+ }
2925
+ function versionTitle(version) {
2926
+ const { metadata } = version.record;
2927
+ return `${metadata.label || "Saved version"} \xB7 ${metadata.author} \xB7 ${formatTime(metadata.createdAt)}`;
2928
+ }
2929
+ function formatTime(value) {
2930
+ const time = new Date(value);
2931
+ return Number.isNaN(time.getTime()) ? value : new Intl.DateTimeFormat(void 0, { dateStyle: "medium", timeStyle: "short" }).format(time);
2932
+ }
2933
+ var HISTORY_CSS = `
2934
+ .dx-history{font:14px/1.5 system-ui,sans-serif;color:#203047;background:#fff;border:1px solid #cbd5e1;border-radius:12px;padding:18px;min-width:0}
2935
+ .dx-history *{box-sizing:border-box}.dx-history h2{font-size:20px;margin:0 0 8px}.dx-history p{margin:8px 0;overflow-wrap:anywhere}
2936
+ .dx-history fieldset{border:0;padding:0;margin:0;min-width:0}.dx-history legend{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
2937
+ .dx-history label{display:block;font-weight:600;margin:12px 0 6px}.dx-history input,.dx-history select{display:block;width:100%;max-width:100%;margin-top:4px;font:inherit;color:inherit;border:1px solid #94a3b8;border-radius:6px;padding:8px;background:#fff}
2938
+ .dx-history button{min-height:40px;margin:4px 6px 4px 0;padding:7px 12px;border:1px solid #94a3b8;border-radius:6px;background:#f8fafc;color:inherit;font:inherit;cursor:pointer}
2939
+ .dx-history button:disabled{opacity:.5;cursor:wait}.dx-history :focus-visible{outline:3px solid #2563eb;outline-offset:2px}.dx-history [hidden]{display:none}
2940
+ .dx-history details{margin-top:16px;border-top:1px solid #e2e8f0;padding-top:12px}.dx-history summary{cursor:pointer;font-weight:600;padding:4px 0}
2941
+ .dx-history [role=status]{min-height:42px}.dx-history ol{padding-left:22px}.dx-history option{padding:6px}
2942
+ `;
2943
+
2323
2944
  // src/page-number-format.ts
2324
2945
  var ROMAN_ONES = ["", "i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix"];
2325
2946
  var ROMAN_TENS = ["", "x", "xx", "xxx", "xl", "l", "lx", "lxx", "lxxx", "xc"];
@@ -5546,6 +6167,19 @@ var DocumentViewport = class {
5546
6167
  scale: options.scale ?? 1
5547
6168
  };
5548
6169
  }
6170
+ /** Retarget the mounted document to an equivalent host after its DOM is adopted. */
6171
+ adoptHost(host) {
6172
+ if (host === this.host) return;
6173
+ this.observer?.disconnect();
6174
+ this.observer = null;
6175
+ this.host.style.removeProperty("--docx-sheet-width");
6176
+ this.host = host;
6177
+ this.refresh();
6178
+ if (this.root && typeof ResizeObserver !== "undefined") {
6179
+ this.observer = new ResizeObserver(() => this.refresh());
6180
+ this.observer.observe(this.host);
6181
+ }
6182
+ }
5549
6183
  /**
5550
6184
  * Adopt a freshly mounted document root (a continuous flow, or the paginated page stack).
5551
6185
  * Safe to call on every remount; the previous root is released first.
@@ -10479,15 +11113,20 @@ var DocxEditor = class _DocxEditor {
10479
11113
  revisionAuthor: opts.revisionAuthor
10480
11114
  }));
10481
11115
  const editor = new _DocxEditor(container, exports, handle, opts);
10482
- editor.refreshAnchorMap();
10483
- if (opts.headerFooter) editor.createRegion();
10484
- const fullHtml = editor.renderFullHtml(bytes);
10485
- if (opts.paginated) editor.mountPaginated(fullHtml);
10486
- else editor.mountHtml(fullHtml);
10487
- editor.syncRegionToBody();
10488
- editor.setupBlockDrag();
10489
- if (opts.comments) editor.createGutter();
10490
- return editor;
11116
+ try {
11117
+ editor.refreshAnchorMap();
11118
+ if (opts.headerFooter) editor.createRegion();
11119
+ const fullHtml = editor.renderFullHtml(bytes);
11120
+ if (opts.paginated) editor.mountPaginated(fullHtml);
11121
+ else editor.mountHtml(fullHtml);
11122
+ editor.syncRegionToBody();
11123
+ editor.setupBlockDrag();
11124
+ if (opts.comments) editor.createGutter();
11125
+ return editor;
11126
+ } catch (error) {
11127
+ editor.close();
11128
+ throw error;
11129
+ }
10491
11130
  }
10492
11131
  /**
10493
11132
  * Open a fresh, blank document (a "New document" — single empty paragraph, Normal style,
@@ -10502,6 +11141,12 @@ var DocxEditor = class _DocxEditor {
10502
11141
  this.assertOpen();
10503
11142
  return this.exports.DocxSessionBridge.Save(this.handle);
10504
11143
  }
11144
+ /** Monotonic committed document version, when supported by the loaded engine. */
11145
+ get version() {
11146
+ this.assertOpen();
11147
+ const value = this.exports.DocxSessionBridge.GetVersion?.(this.handle);
11148
+ return value ? JSON.parse(value).version : null;
11149
+ }
10505
11150
  /** Release the underlying WASM session. The editor is unusable afterward. */
10506
11151
  close() {
10507
11152
  if (this.closed) return;
@@ -10536,6 +11181,24 @@ var DocxEditor = class _DocxEditor {
10536
11181
  get root() {
10537
11182
  return this.container;
10538
11183
  }
11184
+ /**
11185
+ * Move a fully rendered candidate into the host's stable public surface.
11186
+ * Internal editor chrome that binds directly to the container is recreated there;
11187
+ * document blocks and their listeners move with the DOM nodes.
11188
+ */
11189
+ adoptContainer(container) {
11190
+ this.assertOpen();
11191
+ if (container === this.container) return;
11192
+ this.teardownBlockDrag();
11193
+ this.gutter?.dispose();
11194
+ this.gutter = null;
11195
+ const previous = this.container;
11196
+ container.replaceChildren(...Array.from(previous.childNodes));
11197
+ this.container = container;
11198
+ this.viewport.adoptHost(container);
11199
+ this.setupBlockDrag();
11200
+ if (this.options.comments) this.createGutter();
11201
+ }
10539
11202
  /**
10540
11203
  * The zoom the viewport is currently applying (1 = 100%). Below 1 the page is wider than the
10541
11204
  * host and has been scaled to fit rather than reflowed — the honest thing to show a user who
@@ -13695,6 +14358,481 @@ function ensureCommentGutterStyles(doc) {
13695
14358
  (doc.head ?? doc.documentElement).appendChild(style);
13696
14359
  }
13697
14360
 
14361
+ // src/ribbon-history.ts
14362
+ var RibbonHistory = class {
14363
+ constructor(ribbon, options) {
14364
+ this.ribbon = ribbon;
14365
+ this.options = options;
14366
+ this.events = new AbortController();
14367
+ this.identity = { id: crypto.randomUUID(), name: "Untitled.docx" };
14368
+ this.destroyed = false;
14369
+ this.installing = false;
14370
+ this.dirty = false;
14371
+ this.generation = 0;
14372
+ this.captured = -1;
14373
+ this.savedVersion = null;
14374
+ this.observedVersion = null;
14375
+ this.capturedVersion = null;
14376
+ const doc = ribbon.element.ownerDocument;
14377
+ this.prefix = `docxodus:versions:${options.storageName ?? "docxodus-editor"}:document:`;
14378
+ this.lastKey = `docxodus:versions:${options.storageName ?? "docxodus-editor"}:workspace:${options.workspaceId ?? crypto.randomUUID()}`;
14379
+ const style = doc.createElement("style");
14380
+ style.textContent = HISTORY_DRAWER_CSS;
14381
+ this.dialog = doc.createElement("dialog");
14382
+ this.dialog.className = "dxr-history-dialog";
14383
+ this.dialog.setAttribute("aria-label", "Version history");
14384
+ const header = doc.createElement("div");
14385
+ header.className = "dxr-history-header";
14386
+ this.dialog.append(header);
14387
+ const close = this.button("Close version history", () => this.dialog.close(), header);
14388
+ close.className = "dxr-history-close";
14389
+ close.textContent = "Close \u2715";
14390
+ this.source = doc.createElement("p");
14391
+ this.source.className = "dxr-history-source";
14392
+ this.status = doc.createElement("p");
14393
+ this.status.setAttribute("role", "status");
14394
+ this.status.className = "dxr-history-status";
14395
+ const recentLabel = doc.createElement("label");
14396
+ recentLabel.textContent = "Saved documents on this device";
14397
+ this.recent = doc.createElement("select");
14398
+ this.recent.setAttribute("aria-label", recentLabel.textContent);
14399
+ recentLabel.append(this.recent);
14400
+ this.recent.addEventListener("change", () => this.command(async () => {
14401
+ const selectedId = this.recent.value;
14402
+ this.recent.value = this.identity.id;
14403
+ const selected = this.readIdentity(localStorage.getItem(this.prefix + selectedId));
14404
+ if (!selected || !this.confirmReplace()) return;
14405
+ await this.loadSaved(selected);
14406
+ await this.mountPanel();
14407
+ this.updateRecent();
14408
+ }), { signal: this.events.signal });
14409
+ this.dialog.append(this.source, this.status, recentLabel);
14410
+ this.resumeButton = this.button("Continue editing this document", () => this.command(() => this.importArchive()), this.dialog);
14411
+ this.back = this.button("Back to my document", () => this.command(async () => {
14412
+ await this.closeArchive();
14413
+ await this.mountPanel();
14414
+ }), this.dialog);
14415
+ this.body = doc.createElement("div");
14416
+ this.dialog.append(this.body);
14417
+ this.previewDialog = doc.createElement("dialog");
14418
+ this.previewDialog.className = "dxr-version-preview";
14419
+ this.previewDialog.setAttribute("aria-label", "Version preview");
14420
+ this.panelObserver = new MutationObserver(() => this.syncBusy());
14421
+ ribbon.element.append(style, this.dialog, this.previewDialog);
14422
+ for (const dialog of [this.dialog, this.previewDialog]) {
14423
+ dialog.addEventListener("keydown", (event) => event.stopPropagation(), { signal: this.events.signal });
14424
+ dialog.addEventListener("click", (event) => {
14425
+ if (event.target === dialog) {
14426
+ const rect = dialog.getBoundingClientRect();
14427
+ if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) dialog.close();
14428
+ }
14429
+ }, { signal: this.events.signal });
14430
+ }
14431
+ this.dialog.addEventListener("close", () => ribbon.control("history")?.focus(), { signal: this.events.signal });
14432
+ ribbon.control("history")?.addEventListener("click", () => {
14433
+ void this.show();
14434
+ }, { signal: this.events.signal });
14435
+ doc.defaultView?.addEventListener("beforeunload", (event) => {
14436
+ if (this.hasUnsavedChanges || this.busy) event.preventDefault();
14437
+ }, { signal: this.events.signal });
14438
+ doc.defaultView?.addEventListener("pagehide", (event) => {
14439
+ if (!event.persisted) void this.destroy();
14440
+ }, { signal: this.events.signal });
14441
+ }
14442
+ get busy() {
14443
+ return !!this.active || this.panel?.element.getAttribute("aria-busy") === "true";
14444
+ }
14445
+ get hasUnsavedChanges() {
14446
+ return this.dirty || (this.ribbon.editor?.version ?? null) !== this.savedVersion;
14447
+ }
14448
+ /** Called by the ribbon before a programmatic replacement. */
14449
+ beforeOpen() {
14450
+ if (!this.installing && this.busy) throw new DocxHistoryError("Busy", "Finish the version action before opening another document.");
14451
+ }
14452
+ /** A new DOCX is a new document identity, even if its filename matches another document. */
14453
+ documentOpened(name) {
14454
+ if (this.installing) return;
14455
+ this.identity = { id: crypto.randomUUID(), name };
14456
+ this.draft = void 0;
14457
+ this.dirty = false;
14458
+ this.generation++;
14459
+ this.savedVersion = this.observedVersion = this.ribbon.editor?.version ?? null;
14460
+ this.dialog.close();
14461
+ this.previewDialog.close();
14462
+ void this.closeArchive();
14463
+ }
14464
+ edited() {
14465
+ const version = this.ribbon.editor?.version ?? null;
14466
+ if (version !== null && version === this.observedVersion) return;
14467
+ this.observedVersion = version;
14468
+ this.dirty = true;
14469
+ this.generation++;
14470
+ }
14471
+ /** Restores a saved workspace only when requested by the host, before exposing the editor. */
14472
+ async resume() {
14473
+ if (!this.options.workspaceId) return;
14474
+ try {
14475
+ const previous = this.readIdentity(localStorage.getItem(this.lastKey));
14476
+ if (previous) await this.run(() => this.loadSaved(previous));
14477
+ } catch (error) {
14478
+ this.ribbon.setStatus("Your document is open. Version history is unavailable on this device.");
14479
+ this.status.textContent = this.explain(error);
14480
+ }
14481
+ }
14482
+ async show() {
14483
+ if (this.destroyed) return;
14484
+ if (!this.dialog.open) this.dialog.showModal();
14485
+ if (this.busy) return;
14486
+ await this.run(async () => {
14487
+ await this.mountPanel();
14488
+ this.updateRecent();
14489
+ }).catch(() => {
14490
+ });
14491
+ }
14492
+ async openFile(file) {
14493
+ await this.run(async () => {
14494
+ if (/\.docxhistory$/i.test(file.name)) {
14495
+ if (file.size > MAX_HISTORY_ARCHIVE_BYTES) throw new DocxHistoryError("ResourceLimit", "History file is too large.");
14496
+ const bytes = new Uint8Array(await file.arrayBuffer());
14497
+ const reader = await this.options.openArchive(bytes);
14498
+ try {
14499
+ await reader.read();
14500
+ } catch (error) {
14501
+ reader.close();
14502
+ throw error;
14503
+ }
14504
+ await this.closeArchive();
14505
+ this.archive = { reader, bytes, name: file.name };
14506
+ if (!this.dialog.open) this.dialog.showModal();
14507
+ await this.mountPanel();
14508
+ this.updateRecent();
14509
+ } else {
14510
+ if (!/\.docx$/i.test(file.name)) throw new Error("Choose a Word document or a document with version history.");
14511
+ if (!this.confirmReplace()) return;
14512
+ const bytes = new Uint8Array(await file.arrayBuffer());
14513
+ this.install(bytes, { id: crypto.randomUUID(), name: file.name });
14514
+ this.draft = void 0;
14515
+ this.dirty = true;
14516
+ await this.closeArchive();
14517
+ this.dialog.close();
14518
+ }
14519
+ }).catch(() => {
14520
+ });
14521
+ }
14522
+ async newDocument() {
14523
+ await this.run(async () => {
14524
+ if (!this.confirmReplace()) return;
14525
+ this.installing = true;
14526
+ try {
14527
+ this.ribbon.openBlank("Untitled.docx");
14528
+ } finally {
14529
+ this.installing = false;
14530
+ }
14531
+ this.identity = { id: crypto.randomUUID(), name: "Untitled.docx" };
14532
+ this.draft = void 0;
14533
+ this.dirty = false;
14534
+ this.generation++;
14535
+ this.savedVersion = this.observedVersion = this.ribbon.editor?.version ?? null;
14536
+ try {
14537
+ localStorage.removeItem(this.lastKey);
14538
+ } catch {
14539
+ }
14540
+ await this.closeArchive();
14541
+ this.dialog.close();
14542
+ }).catch(() => {
14543
+ });
14544
+ }
14545
+ async destroy() {
14546
+ if (this.destroyed) return;
14547
+ this.destroyed = true;
14548
+ this.events.abort();
14549
+ this.dialog.remove();
14550
+ this.previewDialog.remove();
14551
+ this.panelObserver.disconnect();
14552
+ await this.active?.catch(() => {
14553
+ });
14554
+ await this.closeArchive();
14555
+ this.preview?.destroy();
14556
+ this.client?.close();
14557
+ this.store?.close();
14558
+ }
14559
+ async ensureStorage() {
14560
+ if (this.client) return;
14561
+ const store = await openIndexedDbHistoryStore(this.options.storageName ?? "docxodus-editor");
14562
+ try {
14563
+ this.client = this.options.openHistory(store.storage);
14564
+ this.store = store;
14565
+ } catch (error) {
14566
+ store.close();
14567
+ throw error;
14568
+ }
14569
+ }
14570
+ async ensureDraft() {
14571
+ await this.ensureStorage();
14572
+ if (!this.draft) {
14573
+ const document2 = this.client.document(this.identity.id);
14574
+ this.draft = { ...this.identity, checkpoints: await HistoryCheckpoints.open(document2, this.store.journal(this.identity.id)) };
14575
+ }
14576
+ return this.draft;
14577
+ }
14578
+ async loadSaved(identity) {
14579
+ await this.ensureStorage();
14580
+ const document2 = this.client.document(identity.id);
14581
+ const checkpoints = await HistoryCheckpoints.open(document2, this.store.journal(identity.id));
14582
+ const pending = checkpoints.pendingRequest;
14583
+ const view = checkpoints.view;
14584
+ if (!view && pending?.kind !== "save") throw new Error("This saved document is no longer on this device.");
14585
+ const bytes = pending?.kind === "save" ? pending.bytes : await document2.exportDocx(view.version.id);
14586
+ this.install(bytes, identity);
14587
+ this.draft = { ...identity, checkpoints };
14588
+ this.dirty = pending?.kind === "save";
14589
+ if (pending?.kind === "save") this.captureState(pending.bytes);
14590
+ await this.closeArchive();
14591
+ this.remember();
14592
+ }
14593
+ install(bytes, identity) {
14594
+ if (this.destroyed) throw new DocxHistoryError("Closed", "The editor is closed.");
14595
+ this.installing = true;
14596
+ try {
14597
+ this.ribbon.open(bytes, identity.name);
14598
+ } finally {
14599
+ this.installing = false;
14600
+ }
14601
+ this.identity = identity;
14602
+ this.dirty = false;
14603
+ this.generation++;
14604
+ this.savedVersion = this.observedVersion = this.ribbon.editor?.version ?? null;
14605
+ }
14606
+ async mountPanel() {
14607
+ const current = this.archive ?? await this.ensureDraft();
14608
+ const writable = "checkpoints" in current;
14609
+ const holder = this.body.ownerDocument.createElement("div");
14610
+ const panel = mountHistoryControls(holder, {
14611
+ reader: writable ? current.checkpoints.document : current.reader,
14612
+ checkpoints: writable ? current.checkpoints : void 0,
14613
+ author: this.options.author,
14614
+ documentName: current.name,
14615
+ capture: writable ? () => {
14616
+ const bytes = this.ribbon.save();
14617
+ if (!bytes) throw new Error("Open a document first.");
14618
+ this.remember();
14619
+ this.captureState(bytes);
14620
+ return bytes;
14621
+ } : void 0,
14622
+ confirmRestore: writable ? (title) => this.confirm(`Restore ${title}?${this.hasUnsavedChanges ? "\n\nYour unsaved changes will be replaced." : ""}
14623
+
14624
+ All saved versions will be kept.`) : void 0,
14625
+ onCheckpoint: async (view, action, request) => {
14626
+ if (this.destroyed) return;
14627
+ const savedCapture = action === "save" || action === "retry" && request?.kind === "save" && this.capturedBytes && sameBytes(this.capturedBytes, request.bytes);
14628
+ if (savedCapture && this.captured === this.generation && this.capturedVersion === (this.ribbon.editor?.version ?? null)) {
14629
+ this.dirty = false;
14630
+ this.savedVersion = this.capturedVersion;
14631
+ }
14632
+ this.captured = -1;
14633
+ this.capturedBytes = void 0;
14634
+ if ((action === "restore" || action === "retry" && request?.kind === "restore") && writable)
14635
+ this.install(await current.checkpoints.document.exportDocx(view.version.id), this.identity);
14636
+ this.remember();
14637
+ this.updateRecent();
14638
+ },
14639
+ restoreUpdatesDraft: true,
14640
+ preview: (bytes, title) => this.showPreview(bytes, title)
14641
+ });
14642
+ try {
14643
+ await panel.ready;
14644
+ } catch (error) {
14645
+ await panel.destroy();
14646
+ throw error;
14647
+ }
14648
+ if (this.destroyed) {
14649
+ await panel.destroy();
14650
+ return;
14651
+ }
14652
+ await this.panel?.destroy();
14653
+ this.panel = panel;
14654
+ this.body.replaceChildren(holder);
14655
+ this.panelObserver.disconnect();
14656
+ this.panelObserver.observe(panel.element, { attributes: true, attributeFilter: ["aria-busy"] });
14657
+ this.source.textContent = current.name;
14658
+ this.resumeButton.hidden = this.back.hidden = writable;
14659
+ this.status.textContent = writable ? "Your saved versions stay on this device." : "You\u2019re browsing a history file. Your open document is safe.";
14660
+ }
14661
+ async showPreview(bytes, title) {
14662
+ const doc = this.previewDialog.ownerDocument;
14663
+ const holder = doc.createElement("div");
14664
+ holder.className = "dxr-version-paper";
14665
+ const preview = await this.options.preview(holder, bytes);
14666
+ if (this.destroyed) {
14667
+ preview.destroy();
14668
+ return;
14669
+ }
14670
+ const heading = doc.createElement("h2");
14671
+ heading.textContent = title;
14672
+ const actions = doc.createElement("div");
14673
+ actions.className = "dxr-version-actions";
14674
+ this.button("Back to version history", () => this.previewDialog.close(), actions);
14675
+ if (!this.archive) this.button("Use as draft", () => this.command(async () => {
14676
+ if (!this.confirmReplace()) return;
14677
+ this.install(bytes, this.identity);
14678
+ this.dirty = true;
14679
+ this.previewDialog.close();
14680
+ this.dialog.close();
14681
+ }), actions);
14682
+ this.button("Download this version", () => download(bytes, "version.docx", doc), actions);
14683
+ this.preview?.destroy();
14684
+ this.preview = preview;
14685
+ this.previewDialog.replaceChildren(heading, actions, holder);
14686
+ if (!this.previewDialog.open) this.previewDialog.showModal();
14687
+ }
14688
+ async importArchive() {
14689
+ if (!this.archive || !this.confirmReplace()) return;
14690
+ await this.ensureStorage();
14691
+ const imported = await this.client.importHistoryArchive(this.archive.bytes);
14692
+ const identity = { id: imported.archive.documentId, name: this.archive.name.replace(/\.docxhistory$/i, ".docx") };
14693
+ const document2 = this.client.document(identity.id);
14694
+ const checkpoints = await HistoryCheckpoints.open(document2, this.store.journal(identity.id), imported.view);
14695
+ this.install(await document2.exportDocx(imported.view.version.id), identity);
14696
+ this.draft = { ...identity, checkpoints };
14697
+ this.remember();
14698
+ await this.closeArchive();
14699
+ await this.mountPanel();
14700
+ this.updateRecent();
14701
+ }
14702
+ async closeArchive() {
14703
+ this.panelObserver.disconnect();
14704
+ const panel = this.panel;
14705
+ this.panel = void 0;
14706
+ const archive = this.archive;
14707
+ this.archive = void 0;
14708
+ await panel?.destroy();
14709
+ archive?.reader.close();
14710
+ }
14711
+ remember() {
14712
+ localStorage.setItem(this.prefix + this.identity.id, JSON.stringify(this.identity));
14713
+ localStorage.setItem(this.lastKey, JSON.stringify(this.identity));
14714
+ }
14715
+ readIdentity(value) {
14716
+ if (!value) return null;
14717
+ const identity = JSON.parse(value);
14718
+ if (typeof identity?.id !== "string" || !identity.id || typeof identity?.name !== "string") return null;
14719
+ return { id: identity.id, name: identity.name };
14720
+ }
14721
+ updateRecent() {
14722
+ this.recent.replaceChildren(new Option("Choose a saved document\u2026", ""));
14723
+ for (let i = 0; i < localStorage.length; i++) {
14724
+ const key = localStorage.key(i);
14725
+ if (!key.startsWith(this.prefix)) continue;
14726
+ try {
14727
+ const identity = this.readIdentity(localStorage.getItem(key));
14728
+ if (identity) this.recent.append(new Option(identity.name, identity.id));
14729
+ } catch {
14730
+ }
14731
+ }
14732
+ this.recent.value = this.identity.id;
14733
+ this.recent.parentElement.hidden = this.recent.options.length <= 1;
14734
+ }
14735
+ confirm(message) {
14736
+ return this.dialog.ownerDocument.defaultView?.confirm(message) ?? false;
14737
+ }
14738
+ captureState(bytes) {
14739
+ this.captured = this.generation;
14740
+ this.capturedVersion = this.ribbon.editor?.version ?? null;
14741
+ this.capturedBytes = bytes.slice();
14742
+ }
14743
+ confirmReplace() {
14744
+ return !this.hasUnsavedChanges || this.confirm("Replace your open document? Save a version or download it first to keep your unsaved changes.");
14745
+ }
14746
+ explain(error) {
14747
+ return historyControlError(error, this.draft?.checkpoints.hasPending);
14748
+ }
14749
+ command(action) {
14750
+ void this.run(action).catch(() => {
14751
+ });
14752
+ }
14753
+ syncBusy() {
14754
+ const busy = this.busy;
14755
+ this.ribbon.surface.inert = busy;
14756
+ const chrome = this.ribbon.element.querySelector(".dxr-chrome");
14757
+ if (chrome) chrome.inert = busy;
14758
+ this.recent.disabled = this.resumeButton.disabled = this.back.disabled = busy;
14759
+ const file = this.ribbon.control("file");
14760
+ if (file) file.disabled = busy;
14761
+ const create = this.ribbon.control("new");
14762
+ if (create) create.disabled = busy;
14763
+ }
14764
+ async run(action) {
14765
+ if (this.destroyed || this.busy) return;
14766
+ const work = Promise.resolve().then(action);
14767
+ this.active = work;
14768
+ this.dialog.setAttribute("aria-busy", "true");
14769
+ this.body.inert = true;
14770
+ this.syncBusy();
14771
+ this.ribbon.element.querySelector(".dxr-chrome").inert = true;
14772
+ this.ribbon.surface.inert = true;
14773
+ try {
14774
+ await work;
14775
+ } catch (error) {
14776
+ this.status.textContent = this.explain(error);
14777
+ this.ribbon.setStatus(this.explain(error));
14778
+ throw error;
14779
+ } finally {
14780
+ this.active = void 0;
14781
+ this.body.inert = false;
14782
+ this.dialog.setAttribute("aria-busy", "false");
14783
+ this.syncBusy();
14784
+ this.ribbon.element.querySelector(".dxr-chrome")?.removeAttribute("inert");
14785
+ this.ribbon.surface.inert = false;
14786
+ }
14787
+ }
14788
+ button(label, action, parent) {
14789
+ const button = parent.ownerDocument.createElement("button");
14790
+ button.type = "button";
14791
+ button.textContent = label;
14792
+ button.setAttribute("aria-label", label);
14793
+ button.addEventListener("click", action, { signal: this.events.signal });
14794
+ parent.append(button);
14795
+ return button;
14796
+ }
14797
+ };
14798
+ function download(bytes, name, doc) {
14799
+ const url = URL.createObjectURL(new Blob([bytes.slice()], { type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }));
14800
+ const link = doc.createElement("a");
14801
+ link.href = url;
14802
+ link.download = name;
14803
+ link.click();
14804
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
14805
+ }
14806
+ function sameBytes(left2, right2) {
14807
+ return left2.length === right2.length && left2.every((value, index) => value === right2[index]);
14808
+ }
14809
+ var HISTORY_DRAWER_CSS = `
14810
+ .dxr-history-dialog,.dxr-version-preview{box-sizing:border-box;color:#243b42;background:#fff;border:1px solid #dce5e5;box-shadow:0 24px 80px #163f4033;font:14px/1.5 system-ui,sans-serif;padding:24px;overscroll-behavior:contain}
14811
+ .dxr-history-dialog{width:min(420px,calc(100vw - 24px));max-height:calc(100dvh - 24px);margin:12px 12px 12px auto;border-radius:18px}
14812
+ .dxr-history-dialog::backdrop,.dxr-version-preview::backdrop{background:#16323333;backdrop-filter:blur(2px)}
14813
+ .dxr-history-dialog [hidden],.dxr-version-preview [hidden]{display:none!important}
14814
+ .dxr-history-dialog button,.dxr-version-preview button{font:inherit;color:inherit;border:1px solid #cbd9d9;background:#fff;border-radius:9px;min-height:40px;padding:8px 12px;cursor:pointer}
14815
+ .dxr-history-dialog button:hover,.dxr-version-preview button:hover{background:#eff8f6}
14816
+ .dxr-history-dialog :focus-visible,.dxr-version-preview :focus-visible{outline:3px solid #0f766e;outline-offset:3px}
14817
+ .dxr-history-header{display:flex;justify-content:flex-end;position:sticky;top:0;z-index:2;background:#fff;box-shadow:0 0 0 6px #fff}
14818
+ .dxr-history-dialog .dxr-history-close{border:0;font-size:13px;background:#fff}
14819
+ .dxr-history-source{font-weight:600;overflow-wrap:anywhere;margin:12px 0 4px}
14820
+ .dxr-history-status{color:#597273;margin:4px 0 18px}
14821
+ .dxr-history-dialog>label{display:block;font-size:12px;color:#597273;margin:12px 0}
14822
+ .dxr-history-dialog>label select{display:block;width:100%;font:inherit;padding:8px;border:1px solid #cbd9d9;border-radius:8px;background:#fff;color:#243b42}
14823
+ .dxr-history-dialog .dx-history{padding:0;border:0;border-radius:0;background:transparent}
14824
+ .dxr-history-dialog .dx-history h2{font-size:24px;letter-spacing:-.6px;margin:12px 0}
14825
+ .dxr-history-dialog .dx-history button[data-history-action=save]{background:#0f766e;border-color:#0f766e;color:#fff;font-weight:600}
14826
+ .dxr-history-dialog .dx-history select{border-radius:9px;background:#f8fbfa}
14827
+ .dxr-history-dialog .dx-history option{padding:10px;font-size:13px}
14828
+ .dxr-version-preview{width:min(1080px,calc(100vw - 24px));max-height:calc(100dvh - 24px);border-radius:18px}
14829
+ .dxr-version-preview h2{font-size:20px;margin:0 0 12px;overflow-wrap:anywhere}
14830
+ .dxr-version-actions{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:16px}
14831
+ .dxr-version-paper{max-height:70dvh;overflow:auto;background:#f2f6f5;padding:20px;border-radius:10px}
14832
+ @media(max-width:520px){.dxr-history-dialog,.dxr-version-preview{padding:16px}.dxr-version-paper{padding:8px}}
14833
+ @media(prefers-reduced-motion:no-preference){.dxr-history-dialog[open]{animation:dxr-history-in .16s ease-out}@keyframes dxr-history-in{from{opacity:0;transform:translateX(16px)}to{opacity:1;transform:translateX(0)}}}
14834
+ `;
14835
+
13698
14836
  // src/ribbon-chrome.ts
13699
14837
  var RIBBON_STYLE_VERSION = "11";
13700
14838
  var RIBBON_STYLE_ATTR = "data-docxodus-ribbon-styles";
@@ -14145,9 +15283,9 @@ var RIBBON_CSS = `
14145
15283
  Word draws a header that is being edited \u2014 a dashed rule with a small tag in the margin. */
14146
15284
  .dxr-surface .docx-hf-band {
14147
15285
  /* Docked outside the zoomed sheet, so it takes the page's on-screen width from the
14148
- custom property the viewport publishes rather than stretching to the whole surface. */
15286
+ custom property the viewport publishes, including when the page exceeds the surface. */
14149
15287
  position: relative;
14150
- width: min(100%, var(--docx-sheet-width, 100%));
15288
+ width: var(--docx-sheet-width, 100%);
14151
15289
  margin: 0 auto;
14152
15290
  padding: 34px 72px 12px;
14153
15291
  background: var(--dxr-sheet);
@@ -14206,7 +15344,9 @@ var RIBBON_CSS = `
14206
15344
  vertical breathing room is ours. Centering is left to margin:auto so a page the viewport
14207
15345
  has zoomed to fit stays centered at its scaled width. */
14208
15346
  .dxr[data-chrome] .dxr-surface[data-view="continuous"] .docx-body-flow {
14209
- max-width: 100%;
15347
+ /* CSS zoom scales the authored page width. A percentage cap would shrink only the
15348
+ paper to the host while its fixed-width section and content keep magnifying. */
15349
+ max-width: none;
14210
15350
  margin: 0 auto;
14211
15351
  padding: 56px 0;
14212
15352
  border-radius: 3px;
@@ -14573,6 +15713,7 @@ var RIBBON_HTML = `
14573
15713
  <label class="dxr-btn" tabindex="0">Open<input data-dxr="file" type="file" accept=".docx" hidden /></label>
14574
15714
  <button type="button" data-dxr="save" disabled>Save</button>
14575
15715
  </div>
15716
+ <button type="button" class="dxr-btn" data-dxr="history" title="Browse and save document versions" aria-haspopup="dialog">Version history</button>
14576
15717
  <div class="dxr-quick">
14577
15718
  <button type="button" class="dxr-icon" data-dxr="undo" title="Undo (Ctrl+Z)" aria-label="Undo">&#8630;</button>
14578
15719
  <button type="button" class="dxr-icon" data-dxr="redo" title="Redo (Ctrl+Shift+Z)" aria-label="Redo">&#8631;</button>
@@ -15407,6 +16548,7 @@ var RibbonSurface = class {
15407
16548
  this.resizeObserver = new ResizeObserver(() => this.applyChrome());
15408
16549
  this.resizeObserver.observe(root);
15409
16550
  }
16551
+ this.history = options.history ? new RibbonHistory(this, options.history) : null;
15410
16552
  }
15411
16553
  // ── element lookup ──────────────────────────────────────────────────────────
15412
16554
  control(name) {
@@ -15431,6 +16573,14 @@ var RibbonSurface = class {
15431
16573
  this.element.querySelector("[data-dxr-files]")?.remove();
15432
16574
  }
15433
16575
  if (!this.loaderOptions) this.control("loader")?.remove();
16576
+ if (!this.options.history) this.control("history")?.remove();
16577
+ else {
16578
+ const file = this.control("file");
16579
+ if (file) {
16580
+ file.accept = ".docx,.docxhistory";
16581
+ file.setAttribute("aria-label", "Open a document or history file");
16582
+ }
16583
+ }
15434
16584
  this.require("docname").textContent = this.documentName;
15435
16585
  this.require("paginated").checked = this.options.paginated ?? false;
15436
16586
  this.require("headerfooter").checked = this.headerFooter;
@@ -15612,22 +16762,13 @@ var RibbonSurface = class {
15612
16762
  }
15613
16763
  open(bytes, name) {
15614
16764
  if (!this.exports) throw new Error("Docxodus ribbon: WASM exports are not set yet");
15615
- if (this.live) {
15616
- try {
15617
- this.live.close();
15618
- } catch {
15619
- }
15620
- this.live = null;
15621
- }
15622
- if (name) this.documentName = name;
15623
- this.require("docname").textContent = this.documentName;
16765
+ this.history?.beforeOpen();
15624
16766
  const paginated = this.require("paginated").checked;
15625
- this.surface.dataset.view = paginated ? "paginated" : "continuous";
15626
- this.surface.replaceChildren();
15627
- this.closeFindBar();
16767
+ const candidateSurface = this.surface.cloneNode(false);
16768
+ candidateSurface.dataset.view = paginated ? "paginated" : "continuous";
15628
16769
  const started = performance.now();
15629
16770
  const tracked = this.require("trackchanges").checked ? 1 /* RenderInline */ : this.options.trackedChanges ?? 0 /* Accept */;
15630
- this.live = DocxEditor.open(this.surface, bytes, this.exports, {
16771
+ const candidate = DocxEditor.open(candidateSurface, bytes, this.exports, {
15631
16772
  cssPrefix: this.options.cssPrefix,
15632
16773
  fabricateClasses: this.options.fabricateClasses,
15633
16774
  editable: this.options.editable,
@@ -15635,10 +16776,14 @@ var RibbonSurface = class {
15635
16776
  columnWidth: this.options.columnWidth,
15636
16777
  fitToWidth: this.options.fitToWidth,
15637
16778
  onEdit: (info) => {
16779
+ this.history?.edited();
15638
16780
  this.options.onEdit?.(info);
15639
16781
  this.scheduleStats();
15640
16782
  },
15641
- onMove: this.options.onMove,
16783
+ onMove: (info) => {
16784
+ this.history?.edited();
16785
+ this.options.onMove?.(info);
16786
+ },
15642
16787
  onStoryChange: (which) => this.onStoryChange(which),
15643
16788
  onCommentsChange: (info) => this.onCommentsChange(info),
15644
16789
  paginated,
@@ -15649,7 +16794,16 @@ var RibbonSurface = class {
15649
16794
  comments: this.options.comments,
15650
16795
  commentAuthor: this.author
15651
16796
  });
15652
- this.require("save").disabled = false;
16797
+ this.live?.close();
16798
+ candidate.adoptContainer(this.surface);
16799
+ this.surface.dataset.view = paginated ? "paginated" : "continuous";
16800
+ this.live = candidate;
16801
+ if (name) this.documentName = name;
16802
+ this.require("docname").textContent = this.documentName;
16803
+ this.closeFindBar();
16804
+ this.history?.documentOpened(this.documentName);
16805
+ const saveButton = this.control("save");
16806
+ if (saveButton) saveButton.disabled = false;
15653
16807
  this.require("ribbon").setAttribute("aria-disabled", "false");
15654
16808
  this.setState("ready");
15655
16809
  this.setStatus(`Rendered in ${Math.round(performance.now() - started)} ms`);
@@ -15695,6 +16849,7 @@ var RibbonSurface = class {
15695
16849
  destroy() {
15696
16850
  if (this.destroyed) return;
15697
16851
  this.destroyed = true;
16852
+ void this.history?.destroy();
15698
16853
  const doc = this.element.ownerDocument ?? document;
15699
16854
  doc.removeEventListener("selectionchange", this.onSelectionChange);
15700
16855
  doc.removeEventListener("mousedown", this.onDocumentMouseDown);
@@ -15731,6 +16886,7 @@ var RibbonSurface = class {
15731
16886
  const el = this.control("railOp");
15732
16887
  if (el) el.textContent = `${label} ${ms >= 1e3 ? `${(ms / 1e3).toFixed(2)} s` : `${Math.round(ms)} ms`}`;
15733
16888
  this.options.onCommand?.(label, ms);
16889
+ this.history?.edited();
15734
16890
  this.refreshRailCounts();
15735
16891
  this.refreshRailAnchor();
15736
16892
  this.scheduleStats();
@@ -15930,11 +17086,24 @@ var RibbonSurface = class {
15930
17086
  file?.addEventListener("change", async () => {
15931
17087
  const chosen = file.files?.[0];
15932
17088
  if (!chosen) return;
17089
+ file.value = "";
17090
+ if (this.history) {
17091
+ await this.history.openFile(chosen);
17092
+ return;
17093
+ }
15933
17094
  this.setStatus(`Loading ${chosen.name}\u2026`);
15934
- this.open(new Uint8Array(await chosen.arrayBuffer()), chosen.name);
17095
+ try {
17096
+ this.open(new Uint8Array(await chosen.arrayBuffer()), chosen.name);
17097
+ } catch (error) {
17098
+ this.setStatus(`Could not open this document. ${String(error)}`);
17099
+ }
15935
17100
  file.value = "";
15936
17101
  });
15937
17102
  this.control("new")?.addEventListener("click", () => {
17103
+ if (this.history) {
17104
+ void this.history.newDocument();
17105
+ return;
17106
+ }
15938
17107
  if (this.exports) this.openBlank("untitled.docx");
15939
17108
  });
15940
17109
  this.control("save")?.addEventListener("click", () => this.download());
@@ -15945,6 +17114,7 @@ var RibbonSurface = class {
15945
17114
  if (!chosen || !this.live) return;
15946
17115
  const started = performance.now();
15947
17116
  const ok = await this.live.insertImageFile(chosen, { altText: chosen.name });
17117
+ if (ok) this.history?.edited();
15948
17118
  const ms = performance.now() - started;
15949
17119
  const el = this.control("railOp");
15950
17120
  if (el) el.textContent = `picture ${Math.round(ms)} ms`;
@@ -18407,7 +19577,7 @@ function nameFromSource(source) {
18407
19577
  }
18408
19578
  async function createRibbonEditor(container, source, options = {}) {
18409
19579
  const el = resolveContainer2(container);
18410
- const { wasmBasePath: wasmBasePath2, ...ribbonOptions } = options;
19580
+ const { wasmBasePath: wasmBasePath2, history, ...ribbonOptions } = options;
18411
19581
  const mount2 = createScopedMount(el);
18412
19582
  mount2.root.style.height = "100%";
18413
19583
  mount2.root.style.minHeight = "0";
@@ -18417,6 +19587,12 @@ async function createRibbonEditor(container, source, options = {}) {
18417
19587
  // boundary (rounded card + shadow). Full-bleed hosts pass frame: "flush".
18418
19588
  frame: "card",
18419
19589
  ...ribbonOptions,
19590
+ history: history ? {
19591
+ ...typeof history === "object" ? history : {},
19592
+ openHistory: openDocxHistory,
19593
+ openArchive: openDocxHistoryArchive,
19594
+ preview: (container2, bytes) => createViewer(container2, bytes, { wasmBasePath: wasmBasePath2, renderTrackedChanges: true })
19595
+ } : void 0,
18420
19596
  // Exports arrive after the runtime boots; the loader covers that gap.
18421
19597
  exports: void 0
18422
19598
  });
@@ -18434,6 +19610,7 @@ async function createRibbonEditor(container, source, options = {}) {
18434
19610
  ribbon.loader.stage(2);
18435
19611
  if (bytes == null) ribbon.openBlank(ribbonOptions.documentName);
18436
19612
  else ribbon.open(bytes, ribbonOptions.documentName ?? nameFromSource(source));
19613
+ await ribbon.history?.resume();
18437
19614
  ribbon.loader.stage(3);
18438
19615
  ribbon.loader.done();
18439
19616
  return ribbon;
@@ -18468,6 +19645,7 @@ export {
18468
19645
  DocxHistoryReader,
18469
19646
  DocxSession,
18470
19647
  EmptyParagraphMode,
19648
+ HistoryCheckpoints,
18471
19649
  MAX_HISTORY_ARCHIVE_BYTES,
18472
19650
  PaginationEngine,
18473
19651
  PaginationMode,
@@ -18527,6 +19705,7 @@ export {
18527
19705
  getVersion,
18528
19706
  getWasmExports,
18529
19707
  hasAnnotations,
19708
+ historyControlError,
18530
19709
  initialize,
18531
19710
  installHistoryStorageImports,
18532
19711
  isDeletion,
@@ -18534,11 +19713,13 @@ export {
18534
19713
  isInitialized,
18535
19714
  isInsertion,
18536
19715
  isMove,
19716
+ mountHistoryControls,
18537
19717
  mountRibbon,
18538
19718
  navigateToPageCitation,
18539
19719
  openDocxHistory,
18540
19720
  openDocxHistoryArchive,
18541
19721
  openDocxSession2 as openDocxSession,
19722
+ openIndexedDbHistoryStore,
18542
19723
  paginateHtml,
18543
19724
  parseSectionDimensions,
18544
19725
  projectAnnotationsOntoHtml,