docxodus 9.6.0 → 9.8.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 (44) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +0 -1
  3. package/dist/editor.bundle.js +462 -173
  4. package/dist/editor.d.ts +4 -4
  5. package/dist/editor.d.ts.map +1 -1
  6. package/dist/editor.js +55 -8
  7. package/dist/editor.js.map +1 -1
  8. package/dist/embed.bundle.js +466 -174
  9. package/dist/embed.d.ts.map +1 -1
  10. package/dist/embed.iife.js +465 -173
  11. package/dist/embed.js +3 -0
  12. package/dist/embed.js.map +1 -1
  13. package/dist/page-geometry.d.ts +50 -3
  14. package/dist/page-geometry.d.ts.map +1 -1
  15. package/dist/page-geometry.js +63 -6
  16. package/dist/page-geometry.js.map +1 -1
  17. package/dist/pagination.bundle.js +253 -68
  18. package/dist/pagination.d.ts +33 -8
  19. package/dist/pagination.d.ts.map +1 -1
  20. package/dist/pagination.js +198 -78
  21. package/dist/pagination.js.map +1 -1
  22. package/dist/ribbon-chrome.d.ts +2 -2
  23. package/dist/ribbon-chrome.d.ts.map +1 -1
  24. package/dist/ribbon-chrome.js +171 -97
  25. package/dist/ribbon-chrome.js.map +1 -1
  26. package/dist/ribbon.d.ts +8 -0
  27. package/dist/ribbon.d.ts.map +1 -1
  28. package/dist/ribbon.js +1 -0
  29. package/dist/ribbon.js.map +1 -1
  30. package/dist/wasm/_framework/Docxodus.wasm +0 -0
  31. package/dist/wasm/_framework/Docxodus.wasm.br +0 -0
  32. package/dist/wasm/_framework/DocxodusWasm.wasm +0 -0
  33. package/dist/wasm/_framework/DocxodusWasm.wasm.br +0 -0
  34. package/dist/wasm/_framework/System.Linq.wasm +0 -0
  35. package/dist/wasm/_framework/System.Linq.wasm.br +0 -0
  36. package/dist/wasm/_framework/System.Private.CoreLib.wasm +0 -0
  37. package/dist/wasm/_framework/System.Private.CoreLib.wasm.br +0 -0
  38. package/dist/wasm/_framework/dotnet.boot.js +6 -6
  39. package/dist/wasm/_framework/dotnet.boot.js.br +0 -0
  40. package/dist/wasm/_framework/dotnet.native.wasm +0 -0
  41. package/dist/wasm/_framework/dotnet.native.wasm.br +0 -0
  42. package/package.json +13 -5
  43. package/dist/wasm/index.html +0 -238
  44. package/dist/wasm/main.js +0 -26
@@ -1332,7 +1332,7 @@ function formatPageNumber(value, format) {
1332
1332
  var DEFAULT_PAGE_WIDTH = 612;
1333
1333
  var DEFAULT_PAGE_HEIGHT = 792;
1334
1334
  var DEFAULT_MARGIN = 72;
1335
- var DEFAULT_HEADER_FOOTER_HEIGHT = 36;
1335
+ var DEFAULT_HEADER_FOOTER_DISTANCE = 36;
1336
1336
  function pxToPt(px) {
1337
1337
  return px * 0.75;
1338
1338
  }
@@ -1348,8 +1348,8 @@ function parseSectionDimensions(section) {
1348
1348
  const marginRight = parseFloat(section.dataset.marginRight || "") || DEFAULT_MARGIN;
1349
1349
  const marginBottom = parseFloat(section.dataset.marginBottom || "") || DEFAULT_MARGIN;
1350
1350
  const marginLeft = parseFloat(section.dataset.marginLeft || "") || DEFAULT_MARGIN;
1351
- const headerHeight = parseFloat(section.dataset.headerHeight || "") || DEFAULT_HEADER_FOOTER_HEIGHT;
1352
- const footerHeight = parseFloat(section.dataset.footerHeight || "") || DEFAULT_HEADER_FOOTER_HEIGHT;
1351
+ const headerDistance = parseFloat(section.dataset.headerHeight || "") || DEFAULT_HEADER_FOOTER_DISTANCE;
1352
+ const footerDistance = parseFloat(section.dataset.footerHeight || "") || DEFAULT_HEADER_FOOTER_DISTANCE;
1353
1353
  return {
1354
1354
  pageWidth,
1355
1355
  pageHeight,
@@ -1359,8 +1359,37 @@ function parseSectionDimensions(section) {
1359
1359
  marginRight,
1360
1360
  marginBottom,
1361
1361
  marginLeft,
1362
- headerHeight,
1363
- footerHeight
1362
+ headerDistance,
1363
+ footerDistance,
1364
+ headerHeight: headerDistance,
1365
+ footerHeight: footerDistance
1366
+ };
1367
+ }
1368
+ var MIN_BODY_BAND_PT = 12;
1369
+ function resolvePageBands(dims, headerContentHeight, footerContentHeight) {
1370
+ const headerTop = dims.headerDistance;
1371
+ const footerBottom = dims.pageHeight - dims.footerDistance;
1372
+ const headerReach = headerContentHeight > 0 ? headerTop + headerContentHeight : 0;
1373
+ const footerReach = footerContentHeight > 0 ? footerBottom - footerContentHeight : dims.pageHeight;
1374
+ let bodyTop = Math.max(dims.marginTop, headerReach);
1375
+ let bodyBottom = Math.min(dims.pageHeight - dims.marginBottom, footerReach);
1376
+ const shortfall = MIN_BODY_BAND_PT - (bodyBottom - bodyTop);
1377
+ if (shortfall > 0) {
1378
+ const headerGrowth = bodyTop - dims.marginTop;
1379
+ const footerGrowth = dims.pageHeight - dims.marginBottom - bodyBottom;
1380
+ const growth = headerGrowth + footerGrowth;
1381
+ if (growth > 0) {
1382
+ bodyTop -= shortfall * headerGrowth / growth;
1383
+ bodyBottom += shortfall * footerGrowth / growth;
1384
+ }
1385
+ }
1386
+ return {
1387
+ headerTop,
1388
+ headerHeight: Math.max(0, Math.min(headerContentHeight, bodyTop - headerTop)),
1389
+ bodyTop,
1390
+ bodyHeight: Math.max(0, bodyBottom - bodyTop),
1391
+ footerTop: footerBottom - Math.max(0, Math.min(footerContentHeight, footerBottom - bodyBottom)),
1392
+ footerHeight: Math.max(0, Math.min(footerContentHeight, footerBottom - bodyBottom))
1364
1393
  };
1365
1394
  }
1366
1395
  function sectionWrappers(root) {
@@ -1637,9 +1666,9 @@ var PaginationEngine = class {
1637
1666
  */
1638
1667
  smallestEffectiveContentHeight(dims, sectionIndex) {
1639
1668
  return Math.min(
1640
- this.getEffectiveHeights(dims, sectionIndex, 1, 1).contentHeight,
1641
- this.getEffectiveHeights(dims, sectionIndex, 2, 1).contentHeight,
1642
- this.getEffectiveHeights(dims, sectionIndex, 2, 2).contentHeight
1669
+ this.getPageBands(dims, sectionIndex, 1, 1).bodyHeight,
1670
+ this.getPageBands(dims, sectionIndex, 2, 1).bodyHeight,
1671
+ this.getPageBands(dims, sectionIndex, 2, 2).bodyHeight
1643
1672
  );
1644
1673
  }
1645
1674
  /**
@@ -2163,7 +2192,7 @@ var PaginationEngine = class {
2163
2192
  /**
2164
2193
  * Adds footnotes to a page container, including continuation content.
2165
2194
  */
2166
- addPageFootnotes(pageBox, footnoteIds, dims, footnoteHeight, continuation, partialFootnotes) {
2195
+ addPageFootnotes(pageBox, footnoteIds, dims, bands, footnoteHeight, continuation, partialFootnotes) {
2167
2196
  const hasContinuation = continuation && continuation.remainingElements.length > 0;
2168
2197
  if (footnoteIds.length === 0 && !hasContinuation) {
2169
2198
  return;
@@ -2174,12 +2203,12 @@ var PaginationEngine = class {
2174
2203
  const partialFootnoteIds = new Set(partialFootnotes?.map((p) => p.footnoteId) || []);
2175
2204
  const maxFootnoteHeight = Math.min(
2176
2205
  footnoteHeight,
2177
- dims.contentHeight * MAX_FOOTNOTE_AREA_RATIO
2206
+ bands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO
2178
2207
  );
2179
2208
  const footnotesDiv = document.createElement("div");
2180
2209
  footnotesDiv.className = `${this.cssPrefix}footnotes`;
2181
2210
  footnotesDiv.style.position = "absolute";
2182
- footnotesDiv.style.bottom = `${dims.marginBottom}pt`;
2211
+ footnotesDiv.style.bottom = `${dims.pageHeight - (bands.bodyTop + bands.bodyHeight)}pt`;
2183
2212
  footnotesDiv.style.left = `${dims.marginLeft}pt`;
2184
2213
  footnotesDiv.style.width = `${dims.contentWidth}pt`;
2185
2214
  footnotesDiv.style.boxSizing = "border-box";
@@ -2253,50 +2282,51 @@ var PaginationEngine = class {
2253
2282
  return sectionHf.footerDefault;
2254
2283
  }
2255
2284
  /**
2256
- * Computes effective header, footer, and content heights for a specific page position.
2257
- * Uses pre-measured header/footer heights from the registry.
2258
- * This method is deterministic - same inputs always produce same outputs.
2259
- * This enables lazy loading compatibility since available height can be computed
2260
- * for any page position without knowing the page's content.
2285
+ * The header, body, and footer bands for one page position.
2286
+ *
2287
+ * The single owner of "where does anything sit vertically on this page" — placement in
2288
+ * {@link createPage}, the body budget the flow loop spends, and the note area's anchor all
2289
+ * read it, so those three cannot disagree about where the body ends.
2290
+ *
2291
+ * Deterministic: it depends only on the section's page setup and the registry's pre-measured
2292
+ * story heights, never on the page's content, which is what keeps it lazy-loading compatible.
2261
2293
  */
2262
- getEffectiveHeights(dims, sectionIndex, pageInSection, globalPageNumber) {
2294
+ getPageBands(dims, sectionIndex, pageInSection, globalPageNumber) {
2263
2295
  const sectionHf = this.hfRegistry.get(sectionIndex);
2264
- let headerHeight = dims.marginTop;
2265
- if (sectionHf) {
2266
- let measuredHeaderHeight;
2267
- if (pageInSection === 1 && sectionHf.headerFirstHeight != null) {
2268
- measuredHeaderHeight = sectionHf.headerFirstHeight;
2269
- } else if (globalPageNumber % 2 === 0 && sectionHf.headerEvenHeight != null) {
2270
- measuredHeaderHeight = sectionHf.headerEvenHeight;
2271
- } else if (sectionHf.headerDefaultHeight != null) {
2272
- measuredHeaderHeight = sectionHf.headerDefaultHeight;
2273
- }
2274
- if (measuredHeaderHeight != null) {
2275
- headerHeight = Math.max(dims.marginTop, measuredHeaderHeight);
2276
- }
2277
- }
2278
- let footerHeight = dims.marginBottom;
2279
- if (sectionHf) {
2280
- let measuredFooterHeight;
2281
- if (pageInSection === 1 && sectionHf.footerFirstHeight != null) {
2282
- measuredFooterHeight = sectionHf.footerFirstHeight;
2283
- } else if (globalPageNumber % 2 === 0 && sectionHf.footerEvenHeight != null) {
2284
- measuredFooterHeight = sectionHf.footerEvenHeight;
2285
- } else if (sectionHf.footerDefaultHeight != null) {
2286
- measuredFooterHeight = sectionHf.footerDefaultHeight;
2287
- }
2288
- if (measuredFooterHeight != null) {
2289
- footerHeight = Math.max(dims.marginBottom, measuredFooterHeight);
2290
- }
2291
- }
2292
- const headerExpansion = headerHeight - dims.marginTop;
2293
- const footerExpansion = footerHeight - dims.marginBottom;
2294
- const contentHeight = dims.contentHeight - headerExpansion - footerExpansion;
2295
- return { headerHeight, footerHeight, contentHeight };
2296
+ return resolvePageBands(
2297
+ dims,
2298
+ this.selectStoryHeight(
2299
+ sectionHf?.headerFirstHeight,
2300
+ sectionHf?.headerEvenHeight,
2301
+ sectionHf?.headerDefaultHeight,
2302
+ pageInSection,
2303
+ globalPageNumber
2304
+ ),
2305
+ this.selectStoryHeight(
2306
+ sectionHf?.footerFirstHeight,
2307
+ sectionHf?.footerEvenHeight,
2308
+ sectionHf?.footerDefaultHeight,
2309
+ pageInSection,
2310
+ globalPageNumber
2311
+ )
2312
+ );
2313
+ }
2314
+ /**
2315
+ * The measured height of the running story this page position selects, mirroring
2316
+ * {@link selectHeader}/{@link selectFooter}. Zero when the page has no such story.
2317
+ */
2318
+ selectStoryHeight(first, even, fallback, pageInSection, globalPageNumber) {
2319
+ if (pageInSection === 1 && first != null) return first;
2320
+ if (globalPageNumber % 2 === 0 && even != null) return even;
2321
+ return fallback ?? 0;
2296
2322
  }
2297
2323
  /**
2298
2324
  * Measures the content height of a header or footer element.
2299
- * This is needed because headers/footers can contain more content than fits in the margin area.
2325
+ *
2326
+ * This is what tells {@link resolvePageBands} whether the story stays inside its margin or
2327
+ * pushes the body, so it must measure the story ALONE — any padding added here would have to
2328
+ * be added to the rendered band too, and the two drifting apart is exactly how a header
2329
+ * silently starts overlapping body text.
2300
2330
  */
2301
2331
  measureHeaderFooterHeight(source, contentWidth) {
2302
2332
  const measureContainer = document.createElement("div");
@@ -2304,7 +2334,6 @@ var PaginationEngine = class {
2304
2334
  measureContainer.style.visibility = "hidden";
2305
2335
  measureContainer.style.width = `${contentWidth}pt`;
2306
2336
  measureContainer.style.left = "-9999px";
2307
- measureContainer.style.paddingBottom = "4pt";
2308
2337
  for (const child of Array.from(source.childNodes)) {
2309
2338
  measureContainer.appendChild(child.cloneNode(true));
2310
2339
  }
@@ -2324,7 +2353,7 @@ var PaginationEngine = class {
2324
2353
  let currentContent = [];
2325
2354
  let pageNumber = startPageNumber;
2326
2355
  let pageInSection = 1;
2327
- let { contentHeight: effectiveContentHeight } = this.getEffectiveHeights(
2356
+ let { bodyHeight: effectiveContentHeight } = this.getPageBands(
2328
2357
  dims,
2329
2358
  sectionIndex,
2330
2359
  pageInSection,
@@ -2359,8 +2388,8 @@ var PaginationEngine = class {
2359
2388
  pageNumber++;
2360
2389
  pageInSection++;
2361
2390
  currentContent = [];
2362
- const newHeights = this.getEffectiveHeights(dims, sectionIndex, pageInSection, pageNumber);
2363
- effectiveContentHeight = newHeights.contentHeight;
2391
+ const newBands = this.getPageBands(dims, sectionIndex, pageInSection, pageNumber);
2392
+ effectiveContentHeight = newBands.bodyHeight;
2364
2393
  remainingHeight = effectiveContentHeight;
2365
2394
  prevMarginBottomPt = 0;
2366
2395
  currentFootnoteIds = [];
@@ -2422,7 +2451,7 @@ var PaginationEngine = class {
2422
2451
  ) + additionalChainFootnoteHeight;
2423
2452
  const currentAvailableHeight = remainingHeight - currentFootnoteHeight;
2424
2453
  if (currentChainHeight > currentAvailableHeight) {
2425
- const nextPageHeights = this.getEffectiveHeights(
2454
+ const nextPageBands = this.getPageBands(
2426
2455
  dims,
2427
2456
  sectionIndex,
2428
2457
  pageInSection + 1,
@@ -2438,7 +2467,7 @@ var PaginationEngine = class {
2438
2467
  dims.contentWidth,
2439
2468
  nextPageContinuation
2440
2469
  );
2441
- if (freshChainBodyHeight + freshChainFootnoteHeight <= nextPageHeights.contentHeight) {
2470
+ if (freshChainBodyHeight + freshChainFootnoteHeight <= nextPageBands.bodyHeight) {
2442
2471
  finishPage();
2443
2472
  }
2444
2473
  }
@@ -2607,6 +2636,161 @@ var PaginationEngine = class {
2607
2636
  if (el.getAttribute("contenteditable") !== null) el.setAttribute("contenteditable", "false");
2608
2637
  }
2609
2638
  }
2639
+ /**
2640
+ * Resolves floating DrawingML objects after their anchor paragraphs have landed on a page.
2641
+ *
2642
+ * The converter deliberately carries the OOXML bases and offsets as data instead of flattening
2643
+ * them into CSS: page/margin/column bases belong to the page, while paragraph/line/character
2644
+ * bases belong to the laid-out anchor. Once both coordinate systems exist, promote the object
2645
+ * from the clipped text column into the page box and position it in one shared point space.
2646
+ */
2647
+ positionDrawingAnchors(pageBox, contentArea, dims, pageNumber) {
2648
+ const anchors = Array.from(
2649
+ contentArea.querySelectorAll('[data-docx-drawing-anchor="true"]')
2650
+ );
2651
+ if (anchors.length === 0) return;
2652
+ const pageRect = pageBox.getBoundingClientRect();
2653
+ const pixelsPerPoint = pageRect.width / dims.pageWidth;
2654
+ if (!(pixelsPerPoint > 0)) return;
2655
+ for (const anchor of anchors) {
2656
+ const staticRect = anchor.getBoundingClientRect();
2657
+ const paragraph = anchor.closest("p, h1, h2, h3, h4, h5, h6");
2658
+ const paragraphRect = paragraph?.getBoundingClientRect() ?? staticRect;
2659
+ const toPageX = (x) => (x - pageRect.left) / pixelsPerPoint;
2660
+ const toPageY = (y) => (y - pageRect.top) / pixelsPerPoint;
2661
+ const context = {
2662
+ staticLeft: toPageX(staticRect.left),
2663
+ staticTop: toPageY(staticRect.top),
2664
+ lineHeight: paragraph ? (parseFloat(getComputedStyle(paragraph).lineHeight) || staticRect.height) / pixelsPerPoint : staticRect.height / pixelsPerPoint,
2665
+ paragraphLeft: toPageX(paragraphRect.left),
2666
+ paragraphTop: toPageY(paragraphRect.top),
2667
+ paragraphWidth: paragraphRect.width / pixelsPerPoint,
2668
+ paragraphHeight: paragraphRect.height / pixelsPerPoint
2669
+ };
2670
+ const widthReference = this.horizontalAnchorReference(
2671
+ anchor.getAttribute("data-docx-anchor-width-relative") ?? "margin",
2672
+ dims,
2673
+ pageNumber,
2674
+ context
2675
+ );
2676
+ const widthPercent = this.anchorNumber(anchor, "width-percent");
2677
+ if (widthPercent !== void 0) {
2678
+ anchor.style.width = `${widthReference.size * widthPercent / 100}pt`;
2679
+ }
2680
+ const heightReference = this.verticalAnchorReference(
2681
+ anchor.getAttribute("data-docx-anchor-height-relative") ?? "margin",
2682
+ dims,
2683
+ pageNumber,
2684
+ context
2685
+ );
2686
+ const heightPercent = this.anchorNumber(anchor, "height-percent");
2687
+ if (heightPercent !== void 0 && anchor.dataset.docxAnchorAutofit !== "true") {
2688
+ anchor.style.height = `${heightReference.size * heightPercent / 100}pt`;
2689
+ }
2690
+ const sizedRect = anchor.getBoundingClientRect();
2691
+ const width = sizedRect.width / pixelsPerPoint;
2692
+ const height = sizedRect.height / pixelsPerPoint;
2693
+ const horizontal2 = this.horizontalAnchorReference(
2694
+ anchor.getAttribute("data-docx-anchor-h-relative") ?? "column",
2695
+ dims,
2696
+ pageNumber,
2697
+ context
2698
+ );
2699
+ const vertical2 = this.verticalAnchorReference(
2700
+ anchor.getAttribute("data-docx-anchor-v-relative") ?? "paragraph",
2701
+ dims,
2702
+ pageNumber,
2703
+ context
2704
+ );
2705
+ const left2 = this.resolveAnchorAxis(
2706
+ horizontal2,
2707
+ width,
2708
+ anchor.getAttribute("data-docx-anchor-h-align"),
2709
+ this.anchorNumber(anchor, "h-offset"),
2710
+ pageNumber
2711
+ );
2712
+ const top2 = this.resolveAnchorAxis(
2713
+ vertical2,
2714
+ height,
2715
+ anchor.getAttribute("data-docx-anchor-v-align"),
2716
+ this.anchorNumber(anchor, "v-offset"),
2717
+ pageNumber
2718
+ );
2719
+ pageBox.appendChild(anchor);
2720
+ anchor.style.position = "absolute";
2721
+ anchor.style.left = `${left2}pt`;
2722
+ anchor.style.top = `${top2}pt`;
2723
+ anchor.style.margin = "0";
2724
+ }
2725
+ }
2726
+ anchorNumber(anchor, suffix) {
2727
+ const raw = anchor.getAttribute(`data-docx-anchor-${suffix}`);
2728
+ if (raw === null) return void 0;
2729
+ const value = Number(raw);
2730
+ return Number.isFinite(value) ? value : void 0;
2731
+ }
2732
+ horizontalAnchorReference(relativeFrom, dims, pageNumber, context) {
2733
+ const leftMargin = { start: 0, size: dims.marginLeft };
2734
+ const rightMargin = {
2735
+ start: dims.marginLeft + dims.contentWidth,
2736
+ size: dims.marginRight
2737
+ };
2738
+ switch (relativeFrom) {
2739
+ case "page":
2740
+ return { start: 0, size: dims.pageWidth };
2741
+ case "character":
2742
+ return { start: context.staticLeft, size: 0 };
2743
+ case "leftMargin":
2744
+ return leftMargin;
2745
+ case "rightMargin":
2746
+ return rightMargin;
2747
+ case "insideMargin":
2748
+ return pageNumber % 2 === 1 ? leftMargin : rightMargin;
2749
+ case "outsideMargin":
2750
+ return pageNumber % 2 === 1 ? rightMargin : leftMargin;
2751
+ case "paragraph":
2752
+ return { start: context.paragraphLeft, size: context.paragraphWidth };
2753
+ case "column":
2754
+ case "margin":
2755
+ default:
2756
+ return { start: dims.marginLeft, size: dims.contentWidth };
2757
+ }
2758
+ }
2759
+ verticalAnchorReference(relativeFrom, dims, pageNumber, context) {
2760
+ const topMargin = { start: 0, size: dims.marginTop };
2761
+ const bottomMargin = {
2762
+ start: dims.marginTop + dims.contentHeight,
2763
+ size: dims.marginBottom
2764
+ };
2765
+ switch (relativeFrom) {
2766
+ case "page":
2767
+ return { start: 0, size: dims.pageHeight };
2768
+ case "paragraph":
2769
+ return { start: context.paragraphTop, size: context.paragraphHeight };
2770
+ case "line":
2771
+ return { start: context.staticTop, size: context.lineHeight };
2772
+ case "topMargin":
2773
+ return topMargin;
2774
+ case "bottomMargin":
2775
+ return bottomMargin;
2776
+ case "insideMargin":
2777
+ return pageNumber % 2 === 1 ? topMargin : bottomMargin;
2778
+ case "outsideMargin":
2779
+ return pageNumber % 2 === 1 ? bottomMargin : topMargin;
2780
+ case "margin":
2781
+ default:
2782
+ return { start: dims.marginTop, size: dims.contentHeight };
2783
+ }
2784
+ }
2785
+ resolveAnchorAxis(reference, objectSize, alignment, offset, pageNumber) {
2786
+ if (offset !== void 0) return reference.start + offset;
2787
+ let fraction = 0;
2788
+ if (alignment === "center") fraction = 0.5;
2789
+ else if (alignment === "right" || alignment === "bottom") fraction = 1;
2790
+ else if (alignment === "inside") fraction = pageNumber % 2 === 1 ? 0 : 1;
2791
+ else if (alignment === "outside") fraction = pageNumber % 2 === 1 ? 1 : 0;
2792
+ return reference.start + (reference.size - objectSize) * fraction;
2793
+ }
2610
2794
  /**
2611
2795
  * Creates a page container element.
2612
2796
  */
@@ -2633,22 +2817,22 @@ var PaginationEngine = class {
2633
2817
  pageBox.dataset.pageNumber = String(pageNumber);
2634
2818
  pageBox.dataset.sectionIndex = String(sectionIndex);
2635
2819
  pageBox.dataset.pageInSection = String(pageInSection);
2636
- const effectiveHeights = this.getEffectiveHeights(dims, sectionIndex, pageInSection, pageNumber);
2820
+ const bands = this.getPageBands(dims, sectionIndex, pageInSection, pageNumber);
2637
2821
  const headerSource = this.selectHeader(sectionIndex, pageInSection, pageNumber);
2638
2822
  if (headerSource) {
2639
2823
  const headerDiv = document.createElement("div");
2640
2824
  headerDiv.className = `${this.cssPrefix}header`;
2641
2825
  headerDiv.style.position = "absolute";
2642
- headerDiv.style.top = "0";
2826
+ headerDiv.style.top = `${bands.headerTop}pt`;
2827
+ headerDiv.style.bottom = "auto";
2643
2828
  headerDiv.style.left = `${dims.marginLeft}pt`;
2644
2829
  headerDiv.style.width = `${dims.contentWidth}pt`;
2645
- headerDiv.style.height = `${effectiveHeights.headerHeight}pt`;
2830
+ headerDiv.style.height = `${bands.headerHeight}pt`;
2646
2831
  headerDiv.style.overflow = "hidden";
2647
2832
  headerDiv.style.boxSizing = "border-box";
2648
2833
  headerDiv.style.display = "flex";
2649
2834
  headerDiv.style.flexDirection = "column";
2650
- headerDiv.style.justifyContent = "flex-end";
2651
- headerDiv.style.paddingBottom = "4pt";
2835
+ headerDiv.style.justifyContent = "flex-start";
2652
2836
  for (const child of Array.from(headerSource.childNodes)) {
2653
2837
  const clonedheaderDiv = child.cloneNode(true);
2654
2838
  if (clonedheaderDiv.nodeType === 1) this.makeClonedStoryInert(clonedheaderDiv);
@@ -2656,8 +2840,8 @@ var PaginationEngine = class {
2656
2840
  }
2657
2841
  pageBox.appendChild(headerDiv);
2658
2842
  }
2659
- const contentAreaTop = effectiveHeights.headerHeight;
2660
- const contentAreaHeight = effectiveHeights.contentHeight;
2843
+ const contentAreaTop = bands.bodyTop;
2844
+ const contentAreaHeight = bands.bodyHeight;
2661
2845
  const contentArea = document.createElement("div");
2662
2846
  contentArea.className = `${this.cssPrefix}content`;
2663
2847
  contentArea.style.position = "absolute";
@@ -2672,23 +2856,23 @@ var PaginationEngine = class {
2672
2856
  pageBox.appendChild(contentArea);
2673
2857
  const hasContinuation = continuation && continuation.remainingElements.length > 0;
2674
2858
  if (footnoteIds.length > 0 || hasContinuation) {
2675
- this.addPageFootnotes(pageBox, footnoteIds, dims, footnoteHeight, continuation, partialFootnotes);
2859
+ this.addPageFootnotes(pageBox, footnoteIds, dims, bands, footnoteHeight, continuation, partialFootnotes);
2676
2860
  }
2677
2861
  const footerSource = this.selectFooter(sectionIndex, pageInSection, pageNumber);
2678
2862
  if (footerSource) {
2679
2863
  const footerDiv = document.createElement("div");
2680
2864
  footerDiv.className = `${this.cssPrefix}footer`;
2681
2865
  footerDiv.style.position = "absolute";
2682
- footerDiv.style.bottom = "0";
2866
+ footerDiv.style.top = "auto";
2867
+ footerDiv.style.bottom = `${dims.footerDistance}pt`;
2683
2868
  footerDiv.style.left = `${dims.marginLeft}pt`;
2684
2869
  footerDiv.style.width = `${dims.contentWidth}pt`;
2685
- footerDiv.style.height = `${effectiveHeights.footerHeight}pt`;
2870
+ footerDiv.style.height = `${bands.footerHeight}pt`;
2686
2871
  footerDiv.style.overflow = "hidden";
2687
2872
  footerDiv.style.boxSizing = "border-box";
2688
2873
  footerDiv.style.display = "flex";
2689
2874
  footerDiv.style.flexDirection = "column";
2690
- footerDiv.style.justifyContent = "flex-start";
2691
- footerDiv.style.paddingTop = "4pt";
2875
+ footerDiv.style.justifyContent = "flex-end";
2692
2876
  for (const child of Array.from(footerSource.childNodes)) {
2693
2877
  const clonedfooterDiv = child.cloneNode(true);
2694
2878
  if (clonedfooterDiv.nodeType === 1) this.makeClonedStoryInert(clonedfooterDiv);
@@ -2703,6 +2887,7 @@ var PaginationEngine = class {
2703
2887
  pageBox.appendChild(pageNum);
2704
2888
  }
2705
2889
  this.containerElement.appendChild(pageBox);
2890
+ this.positionDrawingAnchors(pageBox, contentArea, dims, pageNumber);
2706
2891
  const notesEl = pageBox.querySelector(`.${this.cssPrefix}footnotes`);
2707
2892
  if (notesEl) {
2708
2893
  const notesHeightPt = pxToPt(notesEl.getBoundingClientRect().height);
@@ -6077,6 +6262,19 @@ function serializeInlineMarkdown(block) {
6077
6262
  function isListBlock(block) {
6078
6263
  return !!block.querySelector(":scope > [data-list-marker]");
6079
6264
  }
6265
+ function ensureListMarkerSeparator(block) {
6266
+ const marker = block.querySelector(":scope > [data-list-marker]");
6267
+ if (!marker) return;
6268
+ const next = marker.nextElementSibling;
6269
+ if (next?.hasAttribute("data-editor-list-separator")) return;
6270
+ const separator = block.ownerDocument.createElement("span");
6271
+ separator.setAttribute("data-editor-list-separator", "");
6272
+ separator.setAttribute("data-list-marker", "true");
6273
+ separator.setAttribute("aria-hidden", "true");
6274
+ separator.style.display = "none";
6275
+ separator.textContent = " ";
6276
+ marker.after(separator);
6277
+ }
6080
6278
  var GENERATED_CHROME_SELECTOR = '[data-list-marker], a.footnote-ref, a.endnote-ref, a[class$="-backref"]';
6081
6279
  function isGeneratedChrome(node) {
6082
6280
  return node?.nodeType === 1 && !!node.matches?.(GENERATED_CHROME_SELECTOR);
@@ -7367,6 +7565,7 @@ var DocxEditor = class _DocxEditor {
7367
7565
  const unid = el.getAttribute("data-anchor");
7368
7566
  if (!unid || !this.anchorIdOf(el)) return;
7369
7567
  el.setAttribute("contenteditable", "true");
7568
+ ensureListMarkerSeparator(el);
7370
7569
  el.querySelectorAll(GENERATED_CHROME_SELECTOR).forEach((m) => m.setAttribute("contenteditable", "false"));
7371
7570
  el.dataset.committedText = blockContentText(el);
7372
7571
  el.addEventListener("focus", () => {
@@ -8229,8 +8428,8 @@ var DocxEditor = class _DocxEditor {
8229
8428
  * editable `data-anchor` blocks in the notes section, so editing it afterwards needs no new op.
8230
8429
  *
8231
8430
  * Body blocks only: Word disallows a note reference inside a header/footer story or inside
8232
- * another note, and the session rejects those with `AnchorWrongKind`. Remounts, because a new
8233
- * note renumbers the citations after it and can add a whole part.
8431
+ * another note, and the session rejects those with `AnchorWrongKind`. Reconciliation creates
8432
+ * the first notes section when needed and renumbers every affected citation in place.
8234
8433
  */
8235
8434
  insertFootnote(markdown = "New footnote.") {
8236
8435
  this.insertNote("footnote", markdown);
@@ -8622,7 +8821,7 @@ var DocxEditor = class _DocxEditor {
8622
8821
  * the current node — never a section div (multi-child) or the edit root. */
8623
8822
  unitWrapperOf(el) {
8624
8823
  let n = el;
8625
- while (n.parentElement && n.parentElement !== this.editRoot && n.parentElement.tagName === "DIV" && !n.parentElement.hasAttribute("data-anchor") && n.parentElement.childElementCount === 1) {
8824
+ while (n.parentElement && n.parentElement !== this.editRoot && n.parentElement.tagName === "DIV" && !n.parentElement.hasAttribute("data-section-index") && !n.parentElement.hasAttribute("data-anchor") && n.parentElement.childElementCount === 1) {
8626
8825
  n = n.parentElement;
8627
8826
  }
8628
8827
  return n;
@@ -8658,6 +8857,7 @@ var DocxEditor = class _DocxEditor {
8658
8857
  const pureRemoved = diff.removed.filter(
8659
8858
  (i) => !diff.substituted.some((s) => s.oldIndex === i) && !movedOld.has(i)
8660
8859
  );
8860
+ const appliedFresh = /* @__PURE__ */ new Set();
8661
8861
  const nodeAt = (j) => {
8662
8862
  const oi = diff.keep.get(j);
8663
8863
  if (oi !== void 0) return oldNodes[oi];
@@ -8665,7 +8865,7 @@ var DocxEditor = class _DocxEditor {
8665
8865
  if (movedOi !== void 0) return oldNodes[movedOi];
8666
8866
  if (subOldByNew.has(j)) return fresh.get(j);
8667
8867
  const f = fresh.get(j);
8668
- return f && f.isConnected ? f : null;
8868
+ return f && appliedFresh.has(j) ? f : null;
8669
8869
  };
8670
8870
  for (const j of pureAdded) {
8671
8871
  const el = fresh.get(j);
@@ -8680,6 +8880,7 @@ var DocxEditor = class _DocxEditor {
8680
8880
  if (prevW) prevW.after(el);
8681
8881
  else if (nextW) nextW.before(el);
8682
8882
  else return "insert with no anchored neighbor";
8883
+ appliedFresh.add(j);
8683
8884
  this.wireUnit(el, units[j]);
8684
8885
  }
8685
8886
  for (const i of pureRemoved) {
@@ -8718,13 +8919,13 @@ var DocxEditor = class _DocxEditor {
8718
8919
  if (anchorEl) this.wireBlock(anchorEl);
8719
8920
  root.querySelectorAll("[data-anchor]").forEach((b) => this.wireBlock(b));
8720
8921
  }
8721
- /** Old-sequence diff state for one notes section. `null` requests remount (DOM not
8722
- * stampable/consistent). */
8922
+ /** Old-sequence diff state for one notes section. `null` requests remount when an
8923
+ * existing list is not stampable; an absent list is the valid first-note case. */
8723
8924
  notesDiff(sectionClass, units) {
8724
8925
  const ol = this.editRoot.querySelector(`section.${sectionClass} > ol`);
8725
8926
  const lis = ol ? Array.from(ol.children).filter((c) => c.tagName === "LI") : [];
8726
8927
  if (units.length === 0 && lis.length === 0) return { lis, diff: diffUnits([], []) };
8727
- if (!ol) return null;
8928
+ if (!ol) return { lis, diff: diffUnits([], units) };
8728
8929
  const tokens = [];
8729
8930
  for (const li of lis) {
8730
8931
  const unid = li.getAttribute("data-note-anchor");
@@ -8741,7 +8942,20 @@ var DocxEditor = class _DocxEditor {
8741
8942
  this.editRoot.querySelector(`section.${sectionClass}`)?.remove();
8742
8943
  return;
8743
8944
  }
8744
- const ol = this.editRoot.querySelector(`section.${sectionClass} > ol`);
8945
+ let section = this.editRoot.querySelector(`:scope > section.${sectionClass}`);
8946
+ let ol = section?.querySelector(`:scope > ol`) ?? null;
8947
+ if (!ol) {
8948
+ if (!section) {
8949
+ section = document.createElement("section");
8950
+ section.className = sectionClass;
8951
+ const following = sectionClass === "footnotes" ? this.editRoot.querySelector(`:scope > section.endnotes`) : null;
8952
+ if (following) following.before(section);
8953
+ else this.editRoot.append(section);
8954
+ }
8955
+ if (!section.querySelector(":scope > hr")) section.prepend(document.createElement("hr"));
8956
+ ol = document.createElement("ol");
8957
+ section.append(ol);
8958
+ }
8745
8959
  const prefix = sectionClass === "footnotes" ? "fn" : "en";
8746
8960
  const nodes = [];
8747
8961
  for (let j = 0; j < units.length; j++) {
@@ -8878,23 +9092,38 @@ var DocxEditor = class _DocxEditor {
8878
9092
  };
8879
9093
 
8880
9094
  // src/ribbon-chrome.ts
8881
- var RIBBON_STYLE_VERSION = "5";
9095
+ var RIBBON_STYLE_VERSION = "8";
8882
9096
  var RIBBON_STYLE_ATTR = "data-docxodus-ribbon-styles";
8883
9097
  var RIBBON_CSS = `
8884
9098
  .dxr {
8885
- --dxr-ink: #101418;
9099
+ /* OS-Legal design tokens (github.com/Open-Source-Legal/OS-Legal-Style):
9100
+ deep-teal accent, slate foreground scale, warm neutral surfaces, and
9101
+ "light touch" shadows at 0.03-0.06 opacity \u2014 structure over decoration. */
9102
+ --dxr-ink: #1e293b;
8886
9103
  --dxr-sheet: #ffffff;
8887
- --dxr-chrome: #eef1f6;
8888
- --dxr-chrome-sunk: #e3e8f0;
8889
- --dxr-rule: #d3dae4;
8890
- --dxr-accent: #1f5fd0;
8891
- --dxr-wash: #dde8fb;
8892
- --dxr-muted: #5d6975;
8893
- --dxr-data: #0b6f6a;
8894
- --dxr-danger: #a3341f;
8895
- --dxr-desk: #dfe3ea;
8896
- --dxr-ui: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
9104
+ --dxr-chrome: #ffffff;
9105
+ --dxr-chrome-sunk: #f8fafc;
9106
+ --dxr-rule: #e2e8f0;
9107
+ --dxr-rule-strong: #cbd5e1;
9108
+ --dxr-accent: #0f766e;
9109
+ --dxr-accent-hover: #0d9488;
9110
+ --dxr-accent-active: #115e59;
9111
+ --dxr-wash: #f0fdfa;
9112
+ --dxr-wash-on: #ccfbf1;
9113
+ --dxr-muted: #475569;
9114
+ --dxr-faint: #94a3b8;
9115
+ --dxr-data: #0f766e;
9116
+ --dxr-danger: #dc2626;
9117
+ --dxr-danger-wash: #fef2f2;
9118
+ --dxr-desk: #f1f5f9;
9119
+ --dxr-ui: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
9120
+ --dxr-serif: Georgia, "Times New Roman", serif;
8897
9121
  --dxr-mono: ui-monospace, SFMono-Regular, "Cascadia Mono", Consolas, monospace;
9122
+ --dxr-shadow-sm: 0 1px 2px rgba(15, 23, 42, .04);
9123
+ --dxr-shadow-md: 0 1px 3px rgba(15, 23, 42, .06), 0 4px 6px rgba(15, 23, 42, .03);
9124
+ --dxr-shadow-lg: 0 4px 6px rgba(15, 23, 42, .05), 0 10px 15px rgba(15, 23, 42, .04);
9125
+ --dxr-shadow-xl: 0 8px 16px rgba(15, 23, 42, .06), 0 20px 25px rgba(15, 23, 42, .05);
9126
+ --dxr-ease: cubic-bezier(.4, 0, .2, 1);
8898
9127
  --dxr-tap: 30px;
8899
9128
 
8900
9129
  position: relative;
@@ -8910,6 +9139,18 @@ var RIBBON_CSS = `
8910
9139
  }
8911
9140
  .dxr *, .dxr *::before, .dxr *::after { box-sizing: border-box; }
8912
9141
 
9142
+ /* \u2500\u2500 Card frame \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
9143
+ A drop-in embed carries its OWN boundary instead of hoping the host clips it:
9144
+ rounded corners, a hairline, and the house elevation. overflow: clip keeps
9145
+ the sticky chrome, scrollbars, and the loading overlay inside the curve.
9146
+ Full-bleed hosts mount with data-frame="flush" and stay edge-to-edge. */
9147
+ .dxr[data-frame="card"] {
9148
+ border: 1px solid var(--dxr-rule);
9149
+ border-radius: 14px;
9150
+ box-shadow: var(--dxr-shadow-xl);
9151
+ overflow: clip;
9152
+ }
9153
+
8913
9154
  /* Touch devices get bigger hit targets everywhere the token is used. */
8914
9155
  @media (pointer: coarse) { .dxr { --dxr-tap: 40px; } }
8915
9156
 
@@ -8938,12 +9179,13 @@ var RIBBON_CSS = `
8938
9179
  font-weight: 600;
8939
9180
  letter-spacing: -.01em;
8940
9181
  }
9182
+ /* A filled circle, echoing the OS-Legal "Node" motif rather than a diamond. */
8941
9183
  .dxr-brand .dxr-mark {
8942
9184
  flex: 0 0 auto;
8943
9185
  width: 9px;
8944
9186
  height: 9px;
9187
+ border-radius: 50%;
8945
9188
  background: var(--dxr-accent);
8946
- clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
8947
9189
  }
8948
9190
  .dxr-brand .dxr-docname {
8949
9191
  overflow: hidden;
@@ -8975,25 +9217,25 @@ var RIBBON_CSS = `
8975
9217
  scrollbar-width: none;
8976
9218
  }
8977
9219
  .dxr-tabs::-webkit-scrollbar { display: none; }
9220
+ /* Line-variant tabs (the house Tabs default): no boxes, no fills \u2014 the selected
9221
+ tab is a 2px accent underline sitting on the panel's own hairline. */
8978
9222
  .dxr-tab {
8979
9223
  flex: 0 0 auto;
8980
- padding: 6px 15px 7px;
8981
- border: 1px solid transparent;
8982
- border-bottom: none;
8983
- border-radius: 5px 5px 0 0;
9224
+ padding: 7px 14px 8px;
9225
+ border: none;
8984
9226
  background: none;
8985
9227
  color: var(--dxr-muted);
8986
9228
  font: inherit;
8987
9229
  font-size: 12.5px;
9230
+ font-weight: 500;
8988
9231
  cursor: pointer;
9232
+ transition: color .15s var(--dxr-ease), box-shadow .15s var(--dxr-ease);
8989
9233
  }
8990
- .dxr-tab:hover { color: var(--dxr-ink); background: #e6ebf3; }
9234
+ .dxr-tab:hover { color: var(--dxr-ink); }
8991
9235
  .dxr-tab[aria-selected="true"] {
8992
9236
  color: var(--dxr-ink);
8993
9237
  font-weight: 600;
8994
- background: var(--dxr-chrome-sunk);
8995
- border-color: var(--dxr-rule);
8996
- box-shadow: inset 0 2px 0 var(--dxr-accent);
9238
+ box-shadow: inset 0 -2px 0 var(--dxr-accent);
8997
9239
  }
8998
9240
  /* Contextual tab \u2014 present only while the caret is inside a table. */
8999
9241
  .dxr-tab[data-contextual] { color: var(--dxr-accent); }
@@ -9023,7 +9265,7 @@ var RIBBON_CSS = `
9023
9265
  .dxr-group:last-child { border-right: none; }
9024
9266
  .dxr-group:first-child { padding-left: 0; }
9025
9267
  .dxr-glabel {
9026
- color: var(--dxr-muted);
9268
+ color: var(--dxr-faint);
9027
9269
  font-size: 9.5px;
9028
9270
  font-weight: 600;
9029
9271
  letter-spacing: .11em;
@@ -9032,48 +9274,69 @@ var RIBBON_CSS = `
9032
9274
  .dxr-row { display: flex; gap: 3px; align-items: center; }
9033
9275
 
9034
9276
  /* \u2500\u2500 Controls \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
9277
+ /* One button family, house-style: every control is a ghost \u2014 no borders, no
9278
+ fills, no per-button boxes. Hover paints a quiet slate wash; the active
9279
+ (toggled) state paints a teal one. Save alone is the flat primary, so the
9280
+ surface has exactly one accent-colored action. */
9035
9281
  .dxr button, .dxr label.dxr-btn {
9036
9282
  min-height: var(--dxr-tap);
9037
9283
  padding: 5px 9px;
9038
- border: 1px solid transparent;
9039
- border-radius: 4px;
9284
+ border: none;
9285
+ border-radius: 6px;
9040
9286
  background: none;
9041
9287
  color: var(--dxr-ink);
9042
9288
  font: inherit;
9043
9289
  font-size: 13px;
9290
+ font-weight: 500;
9044
9291
  line-height: 1.15;
9045
9292
  cursor: pointer;
9293
+ transition: background-color .15s var(--dxr-ease), color .15s var(--dxr-ease),
9294
+ box-shadow .15s var(--dxr-ease), transform .15s var(--dxr-ease);
9046
9295
  }
9047
9296
  .dxr label.dxr-btn { display: inline-flex; align-items: center; }
9048
- .dxr button:hover, .dxr label.dxr-btn:hover { background: #d6deea; }
9049
- .dxr button:active { background: #c8d3e3; }
9297
+ .dxr button:hover, .dxr label.dxr-btn:hover { background: #f1f5f9; }
9298
+ .dxr button:active { background: #e2e8f0; }
9050
9299
  .dxr button:disabled { opacity: .38; cursor: default; background: none; }
9051
- .dxr button.dxr-on { color: var(--dxr-accent); background: var(--dxr-wash); border-color: #b3ccf2; }
9052
- .dxr-quick button, .dxr-quick label.dxr-btn {
9053
- padding: 4px 10px;
9054
- border-color: var(--dxr-rule);
9055
- background: #f7f9fc;
9056
- font-size: 12.5px;
9057
- }
9058
- .dxr-quick button:hover, .dxr-quick label.dxr-btn:hover { background: #fff; border-color: #b9c4d4; }
9059
- .dxr-quick button:disabled:hover { background: #f7f9fc; border-color: var(--dxr-rule); }
9060
- .dxr button.dxr-icon { min-width: var(--dxr-tap); text-align: center; }
9300
+ .dxr button.dxr-on { color: var(--dxr-accent); background: var(--dxr-wash-on); }
9301
+ .dxr-quick button, .dxr-quick label.dxr-btn { padding: 4px 10px; font-size: 12.5px; }
9302
+ /* Save \u2014 the house primary: flat accent, sm shadow, the 1px lift on hover. */
9303
+ .dxr-quick button[data-dxr="save"]:not(:disabled) {
9304
+ background: var(--dxr-accent);
9305
+ color: #fff;
9306
+ font-weight: 600;
9307
+ box-shadow: var(--dxr-shadow-sm);
9308
+ }
9309
+ .dxr-quick button[data-dxr="save"]:not(:disabled):hover {
9310
+ background: var(--dxr-accent-hover);
9311
+ box-shadow: var(--dxr-shadow-md);
9312
+ transform: translateY(-1px);
9313
+ }
9314
+ .dxr-quick button[data-dxr="save"]:not(:disabled):active {
9315
+ background: var(--dxr-accent-active);
9316
+ box-shadow: var(--dxr-shadow-sm);
9317
+ transform: translateY(0);
9318
+ }
9319
+ /* Icon controls sit quieter than text ones until touched (house IconButton). */
9320
+ .dxr button.dxr-icon { min-width: var(--dxr-tap); text-align: center; color: var(--dxr-muted); }
9321
+ .dxr button.dxr-icon:hover:not(:disabled) { color: var(--dxr-ink); }
9322
+ .dxr button.dxr-icon.dxr-on { color: var(--dxr-accent); }
9061
9323
  /* Icons are inline SVG drawn from the same shapes the control acts on (text lines,
9062
9324
  rules), so alignment and indent read at a glance instead of relying on arrow
9063
9325
  glyphs that collide with undo/redo. currentColor keeps them in step with state. */
9064
9326
  .dxr button svg { display: block; margin: 0 auto; fill: currentColor; }
9065
9327
  .dxr button.dxr-wide { display: inline-flex; align-items: center; justify-content: center; gap: 6px; }
9066
- .dxr button.dxr-danger:hover { color: var(--dxr-danger); background: #f6dcd6; }
9328
+ .dxr button.dxr-danger:hover { color: var(--dxr-danger); background: var(--dxr-danger-wash); }
9067
9329
  .dxr select, .dxr input[type="number"], .dxr input[type="text"] {
9068
9330
  min-height: var(--dxr-tap);
9069
9331
  padding: 4px 6px;
9070
9332
  border: 1px solid var(--dxr-rule);
9071
- border-radius: 4px;
9072
- background: #f7f9fc;
9333
+ border-radius: 6px;
9334
+ background: var(--dxr-sheet);
9073
9335
  color: var(--dxr-ink);
9074
9336
  font: inherit;
9075
9337
  font-size: 12.5px;
9076
9338
  }
9339
+ .dxr select:hover, .dxr input:hover { border-color: var(--dxr-rule-strong); }
9077
9340
  .dxr select:focus-visible, .dxr input:focus-visible, .dxr button:focus-visible,
9078
9341
  .dxr .dxr-tab:focus-visible, .dxr label.dxr-btn:focus-within {
9079
9342
  outline: 2px solid var(--dxr-accent);
@@ -9104,7 +9367,7 @@ var RIBBON_CSS = `
9104
9367
  height: 25px;
9105
9368
  padding: 0 10px;
9106
9369
  overflow-x: auto;
9107
- background: #f7f9fc;
9370
+ background: #fafafa;
9108
9371
  border-top: 1px solid var(--dxr-rule);
9109
9372
  color: var(--dxr-muted);
9110
9373
  font-family: var(--dxr-mono);
@@ -9123,7 +9386,7 @@ var RIBBON_CSS = `
9123
9386
  .dxr-rail .dxr-cell:first-child { padding-left: 0; }
9124
9387
  .dxr-rail .dxr-cell:last-child { border-right: none; }
9125
9388
  .dxr-rail .dxr-k {
9126
- color: #8b96a3;
9389
+ color: var(--dxr-faint);
9127
9390
  font-family: var(--dxr-ui);
9128
9391
  font-size: 9.5px;
9129
9392
  letter-spacing: .1em;
@@ -9131,7 +9394,7 @@ var RIBBON_CSS = `
9131
9394
  }
9132
9395
  .dxr-rail .dxr-v { color: var(--dxr-data); }
9133
9396
  .dxr-rail .dxr-v.dxr-flash { animation: dxr-railflash .45s ease-out; }
9134
- @keyframes dxr-railflash { from { background: #b9ecdf; } to { background: transparent; } }
9397
+ @keyframes dxr-railflash { from { background: #ccfbf1; } to { background: transparent; } }
9135
9398
 
9136
9399
  /* \u2500\u2500 Hint \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
9137
9400
  .dxr-hint {
@@ -9146,8 +9409,8 @@ var RIBBON_CSS = `
9146
9409
  .dxr-hint kbd {
9147
9410
  padding: 0 4px;
9148
9411
  border: 1px solid var(--dxr-rule);
9149
- border-radius: 3px;
9150
- background: #e8ecf3;
9412
+ border-radius: 4px;
9413
+ background: #f1f5f9;
9151
9414
  font-family: var(--dxr-mono);
9152
9415
  font-size: 11px;
9153
9416
  }
@@ -9162,13 +9425,34 @@ var RIBBON_CSS = `
9162
9425
  SHOULD win.
9163
9426
  (No backticks in this file's comments: the stylesheet is a template literal.) */
9164
9427
  .dxr-scroll { flex: 1 1 auto; min-height: 0; overflow: auto; -webkit-overflow-scrolling: touch; }
9428
+ /* Soft scroll boundaries: content dissolves at the chrome and at the bottom edge
9429
+ instead of hard-clipping. Sticky gradient veils cost one paint layer each and
9430
+ never intercept input. */
9431
+ .dxr-scroll::before, .dxr-scroll::after {
9432
+ content: "";
9433
+ position: sticky;
9434
+ z-index: 3;
9435
+ display: block;
9436
+ height: 26px;
9437
+ pointer-events: none;
9438
+ }
9439
+ .dxr-scroll::before {
9440
+ top: 0;
9441
+ margin-bottom: -26px;
9442
+ background: linear-gradient(to bottom, var(--dxr-desk), transparent);
9443
+ }
9444
+ .dxr-scroll::after {
9445
+ bottom: 0;
9446
+ margin-top: -26px;
9447
+ background: linear-gradient(to top, var(--dxr-desk), transparent);
9448
+ }
9165
9449
  .dxr[data-chrome] .dxr-surface { margin: 26px auto; padding: 0 16px 96px; }
9166
9450
  .dxr-surface [contenteditable="true"]:focus {
9167
9451
  outline: 2px solid var(--dxr-accent);
9168
9452
  outline-offset: 2px;
9169
9453
  border-radius: 2px;
9170
9454
  }
9171
- @media (hover: hover) { .dxr-surface [contenteditable="true"]:hover { background: #f2f7ff; } }
9455
+ @media (hover: hover) { .dxr-surface [contenteditable="true"]:hover { background: var(--dxr-wash); } }
9172
9456
 
9173
9457
  /* Header/footer bands: stories live in their own OOXML parts outside the body,
9174
9458
  so they dock as their own regions. Styled from the same tokens as the ribbon
@@ -9180,9 +9464,10 @@ var RIBBON_CSS = `
9180
9464
  margin: 0 auto 14px;
9181
9465
  padding: 9px 72px 13px;
9182
9466
  border: 1px solid var(--dxr-rule);
9183
- border-left: 2px solid #c2ccd9;
9184
- border-radius: 3px;
9467
+ border-left: 2px solid #5eead4;
9468
+ border-radius: 6px;
9185
9469
  background: var(--dxr-sheet);
9470
+ box-shadow: var(--dxr-shadow-sm);
9186
9471
  }
9187
9472
  .dxr-surface .docx-hf-band + .docx-body-flow { margin-top: 0; }
9188
9473
  .dxr-surface .docx-hf-band[data-hf-band="footer"] { margin: 14px auto 0; }
@@ -9201,8 +9486,8 @@ var RIBBON_CSS = `
9201
9486
  .dxr-surface .docx-hf-chrome select, .dxr-surface .docx-hf-chrome input {
9202
9487
  padding: 3px 5px;
9203
9488
  border: 1px solid var(--dxr-rule);
9204
- border-radius: 4px;
9205
- background: #f7f9fc;
9489
+ border-radius: 6px;
9490
+ background: var(--dxr-sheet);
9206
9491
  color: var(--dxr-ink);
9207
9492
  font: inherit;
9208
9493
  font-family: var(--dxr-ui);
@@ -9216,22 +9501,22 @@ var RIBBON_CSS = `
9216
9501
  .dxr-surface .docx-hf-warning {
9217
9502
  margin: 0 0 8px -60px;
9218
9503
  padding: 7px 9px;
9219
- border: 1px solid #e8d9a8;
9220
- border-left: 2px solid #c9a227;
9221
- border-radius: 3px;
9222
- background: #fdf6e3;
9223
- color: #6b5310;
9504
+ border: 1px solid #fde68a;
9505
+ border-left: 2px solid #d97706;
9506
+ border-radius: 6px;
9507
+ background: #fffbeb;
9508
+ color: #92400e;
9224
9509
  font-size: 12px;
9225
9510
  line-height: 1.45;
9226
9511
  }
9227
9512
  .dxr-surface .docx-hf-warning button {
9228
9513
  margin-left: 6px;
9229
9514
  padding: 2px 8px;
9230
- border-color: #e8d9a8;
9515
+ border-color: #fde68a;
9231
9516
  background: #fff;
9232
9517
  font-size: 12px;
9233
9518
  }
9234
- .dxr-surface .docx-hf-placeholder { color: #9aa4b1; font-style: italic; }
9519
+ .dxr-surface .docx-hf-placeholder { color: var(--dxr-faint); font-style: italic; }
9235
9520
  .dxr-surface .docx-hf-inherited {
9236
9521
  color: var(--dxr-muted);
9237
9522
  font-style: italic;
@@ -9251,7 +9536,7 @@ var RIBBON_CSS = `
9251
9536
  padding: 56px 0;
9252
9537
  border-radius: 3px;
9253
9538
  background: var(--dxr-sheet);
9254
- box-shadow: 0 1px 3px rgba(16, 20, 24, .14), 0 8px 24px rgba(16, 20, 24, .05);
9539
+ box-shadow: 0 1px 3px rgba(15, 23, 42, .08), 0 12px 24px rgba(15, 23, 42, .06);
9255
9540
  }
9256
9541
 
9257
9542
  /* \u2500\u2500 Table size picker \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
@@ -9261,9 +9546,9 @@ var RIBBON_CSS = `
9261
9546
  z-index: 30;
9262
9547
  padding: 9px;
9263
9548
  border: 1px solid var(--dxr-rule);
9264
- border-radius: 6px;
9549
+ border-radius: 12px;
9265
9550
  background: #fff;
9266
- box-shadow: 0 4px 16px rgba(16, 20, 24, .18);
9551
+ box-shadow: var(--dxr-shadow-xl);
9267
9552
  }
9268
9553
  .dxr-pop[data-open] { display: block; }
9269
9554
  .dxr-gridcells { display: grid; grid-template-columns: repeat(10, 16px); gap: 2px; }
@@ -9271,11 +9556,11 @@ var RIBBON_CSS = `
9271
9556
  width: 16px;
9272
9557
  height: 16px;
9273
9558
  border: 1px solid var(--dxr-rule);
9274
- border-radius: 2px;
9275
- background: #f2f5f9;
9559
+ border-radius: 3px;
9560
+ background: var(--dxr-chrome-sunk);
9276
9561
  cursor: pointer;
9277
9562
  }
9278
- .dxr-gridcells div[data-on] { background: var(--dxr-wash); border-color: var(--dxr-accent); }
9563
+ .dxr-gridcells div[data-on] { background: #ccfbf1; border-color: var(--dxr-accent); }
9279
9564
  .dxr-popfoot {
9280
9565
  display: flex;
9281
9566
  justify-content: space-between;
@@ -9358,11 +9643,11 @@ var RIBBON_CSS = `
9358
9643
  display: grid;
9359
9644
  place-items: center;
9360
9645
  padding: 24px;
9361
- color: #f4f9ff;
9646
+ color: #f8fafc;
9362
9647
  background:
9363
- radial-gradient(circle at 20% 20%, rgba(63, 128, 255, .24), transparent 22rem),
9364
- radial-gradient(circle at 82% 80%, rgba(176, 91, 255, .22), transparent 24rem),
9365
- linear-gradient(145deg, #071221 0%, #0b1930 52%, #101426 100%);
9648
+ radial-gradient(circle at 20% 20%, rgba(13, 148, 136, .2), transparent 22rem),
9649
+ radial-gradient(circle at 82% 80%, rgba(45, 212, 191, .1), transparent 24rem),
9650
+ linear-gradient(150deg, #0b1220 0%, #0f172a 55%, #0f2723 100%);
9366
9651
  transition: opacity .55s ease, visibility .55s ease;
9367
9652
  }
9368
9653
  .dxr-loader[hidden] { display: none; }
@@ -9381,14 +9666,14 @@ var RIBBON_CSS = `
9381
9666
  .dxr-orbit {
9382
9667
  position: absolute;
9383
9668
  inset: 7%;
9384
- border: 1px solid rgba(115, 204, 255, .22);
9669
+ border: 1px solid rgba(45, 212, 191, .24);
9385
9670
  border-radius: 50%;
9386
9671
  animation: dxr-spin 9s linear infinite;
9387
9672
  }
9388
9673
  .dxr-orbit.dxr-two {
9389
9674
  inset: 20%;
9390
9675
  border-style: dashed;
9391
- border-color: rgba(196, 128, 255, .32);
9676
+ border-color: rgba(94, 234, 212, .3);
9392
9677
  animation-duration: 6s;
9393
9678
  animation-direction: reverse;
9394
9679
  }
@@ -9398,19 +9683,19 @@ var RIBBON_CSS = `
9398
9683
  height: 10px;
9399
9684
  border-radius: 50%;
9400
9685
  content: "";
9401
- background: #52e5ff;
9402
- box-shadow: 0 0 18px #52e5ff;
9686
+ background: #2dd4bf;
9687
+ box-shadow: 0 0 18px rgba(45, 212, 191, .8);
9403
9688
  }
9404
9689
  .dxr-orbit::before { top: -5px; left: 50%; }
9405
- .dxr-orbit::after { right: 7%; bottom: 12%; background: #b26cff; box-shadow: 0 0 18px #b26cff; }
9690
+ .dxr-orbit::after { right: 7%; bottom: 12%; background: #5eead4; box-shadow: 0 0 18px rgba(94, 234, 212, .7); }
9406
9691
  .dxr-card {
9407
9692
  position: absolute;
9408
9693
  inset: 24% 28%;
9409
9694
  padding: 20px 15px;
9410
- border: 1px solid rgba(255, 255, 255, .33);
9695
+ border: 1px solid rgba(255, 255, 255, .3);
9411
9696
  border-radius: 14px;
9412
- background: linear-gradient(150deg, rgba(255, 255, 255, .18), rgba(255, 255, 255, .06));
9413
- box-shadow: 0 22px 50px rgba(0, 0, 0, .35), 0 0 45px rgba(71, 150, 255, .13);
9697
+ background: linear-gradient(150deg, rgba(255, 255, 255, .16), rgba(255, 255, 255, .05));
9698
+ box-shadow: 0 22px 50px rgba(0, 0, 0, .35), 0 0 45px rgba(13, 148, 136, .15);
9414
9699
  backdrop-filter: blur(10px);
9415
9700
  animation: dxr-float 3.6s ease-in-out infinite;
9416
9701
  }
@@ -9420,8 +9705,8 @@ var RIBBON_CSS = `
9420
9705
  right: -9px;
9421
9706
  padding: 4px 7px;
9422
9707
  border-radius: 6px;
9423
- background: #52e5ff;
9424
- color: #06111d;
9708
+ background: #2dd4bf;
9709
+ color: #042f2e;
9425
9710
  content: "DOCX";
9426
9711
  font-size: 8px;
9427
9712
  font-weight: 900;
@@ -9438,46 +9723,49 @@ var RIBBON_CSS = `
9438
9723
  }
9439
9724
  .dxr-card i:nth-child(2) { width: 76%; animation-delay: -.45s; }
9440
9725
  .dxr-card i:nth-child(3) { width: 88%; animation-delay: -.85s; }
9441
- .dxr-card i:nth-child(4) { width: 58%; background: rgba(255, 111, 139, .75); animation-delay: -1.2s; }
9726
+ .dxr-card i:nth-child(4) { width: 58%; background: rgba(248, 113, 113, .75); animation-delay: -1.2s; }
9442
9727
  .dxr-chip {
9443
9728
  position: absolute;
9444
9729
  display: grid;
9445
9730
  width: 34px;
9446
9731
  height: 34px;
9447
9732
  place-items: center;
9448
- border: 1px solid rgba(255, 255, 255, .17);
9733
+ border: 1px solid rgba(255, 255, 255, .16);
9449
9734
  border-radius: 11px;
9450
- background: rgba(12, 28, 49, .88);
9735
+ background: rgba(15, 30, 46, .88);
9451
9736
  box-shadow: 0 12px 30px rgba(0, 0, 0, .28);
9452
9737
  font-size: 11px;
9453
9738
  font-weight: 850;
9454
9739
  }
9455
- .dxr-chip.dxr-b { left: 2%; top: 30%; color: #52e5ff; animation: dxr-chip 3s ease-in-out infinite; }
9456
- .dxr-chip.dxr-r { right: -2%; bottom: 24%; color: #ff8da0; animation: dxr-chip 3s -1.4s ease-in-out infinite; }
9457
- .dxr-chip.dxr-s { left: 20%; bottom: 0; color: #52e0a2; animation: dxr-chip 3s -.7s ease-in-out infinite; }
9458
- .dxr-eyebrow { color: #52e5ff; font-size: 10px; font-weight: 850; letter-spacing: .16em; text-transform: uppercase; }
9740
+ .dxr-chip.dxr-b { left: 2%; top: 30%; color: #5eead4; animation: dxr-chip 3s ease-in-out infinite; }
9741
+ .dxr-chip.dxr-r { right: -2%; bottom: 24%; color: #fca5a5; animation: dxr-chip 3s -1.4s ease-in-out infinite; }
9742
+ .dxr-chip.dxr-s { left: 20%; bottom: 0; color: #6ee7b7; animation: dxr-chip 3s -.7s ease-in-out infinite; }
9743
+ .dxr-eyebrow { color: #5eead4; font-size: 10px; font-weight: 850; letter-spacing: .16em; text-transform: uppercase; }
9744
+ /* Georgia headline \u2014 the house pairing of a serif headline over Inter UI copy. */
9459
9745
  .dxr-loader h2 {
9460
9746
  margin: 10px auto 10px;
9461
9747
  max-width: 520px;
9462
- font-size: clamp(23px, 5vw, 40px);
9463
- line-height: 1.05;
9464
- letter-spacing: -.045em;
9748
+ font-family: var(--dxr-serif);
9749
+ font-size: clamp(23px, 5vw, 38px);
9750
+ font-weight: 500;
9751
+ line-height: 1.08;
9752
+ letter-spacing: -.02em;
9465
9753
  }
9466
- .dxr-loader-copy > p { margin: 0 auto; max-width: 46ch; color: #9eb2c9; font-size: 13.5px; line-height: 1.6; }
9754
+ .dxr-loader-copy > p { margin: 0 auto; max-width: 46ch; color: #94a3b8; font-size: 13.5px; line-height: 1.6; }
9467
9755
  .dxr-ad {
9468
9756
  display: flex;
9469
9757
  gap: 12px;
9470
9758
  margin: 20px auto 0;
9471
9759
  max-width: 420px;
9472
9760
  padding: 13px;
9473
- border: 1px solid rgba(126, 160, 200, .16);
9474
- border-radius: 13px;
9761
+ border: 1px solid rgba(148, 163, 184, .16);
9762
+ border-radius: 12px;
9475
9763
  background: rgba(255, 255, 255, .04);
9476
9764
  text-align: left;
9477
9765
  }
9478
- .dxr-ad .dxr-num { color: #b26cff; font: 800 10px/1.4 var(--dxr-mono); }
9766
+ .dxr-ad .dxr-num { color: #2dd4bf; font: 800 10px/1.4 var(--dxr-mono); }
9479
9767
  .dxr-ad strong { display: block; font-size: 12.5px; }
9480
- .dxr-ad .dxr-adcopy { display: block; margin-top: 4px; color: #849bb5; font-size: 11.5px; line-height: 1.45; }
9768
+ .dxr-ad .dxr-adcopy { display: block; margin-top: 4px; color: #8ba0b9; font-size: 11.5px; line-height: 1.45; }
9481
9769
  .dxr-ad[data-swap] { animation: dxr-swap .4s ease; }
9482
9770
  .dxr-track {
9483
9771
  height: 3px;
@@ -9491,8 +9779,8 @@ var RIBBON_CSS = `
9491
9779
  width: 12%;
9492
9780
  height: 100%;
9493
9781
  border-radius: inherit;
9494
- background: linear-gradient(90deg, #52e5ff, #5c7cff, #b26cff);
9495
- box-shadow: 0 0 16px rgba(82, 229, 255, .55);
9782
+ background: linear-gradient(90deg, #14b8a6, #2dd4bf, #5eead4);
9783
+ box-shadow: 0 0 16px rgba(45, 212, 191, .5);
9496
9784
  transition: width .65s cubic-bezier(.22, .8, .28, 1);
9497
9785
  }
9498
9786
  .dxr-meta {
@@ -9501,7 +9789,7 @@ var RIBBON_CSS = `
9501
9789
  gap: 14px;
9502
9790
  margin: 9px auto 0;
9503
9791
  max-width: 420px;
9504
- color: #647e9c;
9792
+ color: #64748b;
9505
9793
  font: 700 9px/1 var(--dxr-mono);
9506
9794
  letter-spacing: .09em;
9507
9795
  text-transform: uppercase;
@@ -9510,9 +9798,9 @@ var RIBBON_CSS = `
9510
9798
  display: none;
9511
9799
  margin-top: 18px;
9512
9800
  padding: 9px 14px;
9513
- border: 1px solid rgba(255, 127, 145, .4);
9801
+ border: 1px solid rgba(248, 113, 113, .4);
9514
9802
  border-radius: 9px;
9515
- background: rgba(255, 127, 145, .12);
9803
+ background: rgba(248, 113, 113, .12);
9516
9804
  color: #fff;
9517
9805
  cursor: pointer;
9518
9806
  }
@@ -9627,8 +9915,8 @@ var RIBBON_HTML = `
9627
9915
  )}</button>
9628
9916
  </div>
9629
9917
  <div class="dxr-row">
9630
- <button type="button" data-list="bullet" title="Bullet list">&#8226; List</button>
9631
- <button type="button" data-list="decimal" title="Numbered list">1. List</button>
9918
+ <button type="button" data-list="bullet" title="Bullet list">Bullets</button>
9919
+ <button type="button" data-list="decimal" title="Numbered list">Numbered</button>
9632
9920
  <button type="button" data-pagebreak title="Start this block on a new page">Page break</button>
9633
9921
  </div>
9634
9922
  </div>
@@ -9951,6 +10239,7 @@ var RibbonSurface = class {
9951
10239
  const root = doc.createElement("div");
9952
10240
  root.className = "dxr";
9953
10241
  root.dataset.state = "idle";
10242
+ root.dataset.frame = this.options.frame ?? "flush";
9954
10243
  root.innerHTML = RIBBON_HTML;
9955
10244
  for (const el of Array.from(root.querySelectorAll("[data-dxr]"))) {
9956
10245
  el.id = this.idPrefix + el.dataset.dxr;
@@ -12018,6 +12307,9 @@ async function createRibbonEditor(container, source, options = {}) {
12018
12307
  mount2.root.style.minHeight = "0";
12019
12308
  const ribbon = mountRibbon(mount2.root, {
12020
12309
  documentName: ribbonOptions.documentName ?? nameFromSource(source),
12310
+ // A drop-in embed sits in the middle of someone's page, so it carries its own
12311
+ // boundary (rounded card + shadow). Full-bleed hosts pass frame: "flush".
12312
+ frame: "card",
12021
12313
  ...ribbonOptions,
12022
12314
  // Exports arrive after the runtime boots; the loader covers that gap.
12023
12315
  exports: void 0