wordinweb 0.1.8 → 0.1.10

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.
package/dist/index.js CHANGED
@@ -3955,10 +3955,12 @@ function parseRun(r, ctx, field, suppressedObjects) {
3955
3955
  case "endnoteReference": {
3956
3956
  const id = intAttr(el16, "id");
3957
3957
  if (id !== void 0) {
3958
+ const customMark = attr(el16, "customMarkFollows");
3958
3959
  run.content.push({
3959
3960
  kind: "noteRef",
3960
3961
  noteType: ln === "footnoteReference" ? "footnote" : "endnote",
3961
- id
3962
+ id,
3963
+ customMarkFollows: customMark !== void 0 && customMark !== "0" && customMark !== "false" && customMark !== "off"
3962
3964
  });
3963
3965
  }
3964
3966
  break;
@@ -4361,6 +4363,7 @@ function parseDrawing(drawing, ctx) {
4361
4363
  const rot = intAttr(xfrm, "rot");
4362
4364
  const lnEl = child(spPr, "ln");
4363
4365
  const strokeColor = lnEl && !child(lnEl, "noFill") ? fillColorOf(lnEl) : void 0;
4366
+ const fill = child(spPr, "noFill") ? void 0 : fillColorOf(spPr) ?? styleFillOf(textboxEl);
4364
4367
  const bodyPr = child(textboxEl, "bodyPr");
4365
4368
  const insetOf = (name, dflt) => {
4366
4369
  const v = bodyPr ? intAttr(bodyPr, name) : void 0;
@@ -4404,7 +4407,7 @@ function parseDrawing(drawing, ctx) {
4404
4407
  vAlign: vAlignOf(posV),
4405
4408
  blocks: txbxContent ? parseBlocks(txbxContent, ctx) : [],
4406
4409
  ...textboxChainId !== void 0 ? { chainId: textboxChainId, chainSeq: textboxChainSeq ?? 0 } : {},
4407
- ...fillColorOf(spPr) ? { fill: fillColorOf(spPr) } : {},
4410
+ ...fill ? { fill } : {},
4408
4411
  ...strokeColor ? { stroke: { color: strokeColor, weight: Math.max(emuToPx(intAttr(lnEl, "w") ?? 0), 0.75) } } : {},
4409
4412
  textAnchor: anchorAttr === "ctr" ? "middle" : anchorAttr === "b" ? "bottom" : anchorAttr === "t" ? "top" : void 0,
4410
4413
  insets: {
@@ -6111,23 +6114,49 @@ var DocxDocument = class _DocxDocument {
6111
6114
  }
6112
6115
  this._modelVersion++;
6113
6116
  }
6114
- /**
6115
- * Reparse the two direct body paragraphs created by Enter without rebuilding
6116
- * the complete document model. This deliberately covers only the simple
6117
- * single-section body case used by incremental pagination; structural
6118
- * containers, revisions, bookmarks, and section breaks use refresh().
6119
- */
6117
+ /** Reparse the two sibling body-story paragraphs created by Enter without
6118
+ * rebuilding the complete document model. Paragraphs nested in table cells
6119
+ * are included because legal documents spend most of their body inside
6120
+ * tables; revisions, bookmarks, fields, and section breaks use refresh(). */
6120
6121
  reparseDirectBodyParagraphSplit(beforeSource, afterSource) {
6121
- if (this.sections.length !== 1) return null;
6122
- const body = child(this.docRoot, "body");
6123
- if (!body) return null;
6124
- const beforeIndex = body.children.indexOf(beforeSource);
6125
- if (beforeIndex < 0 || body.children[beforeIndex + 1] !== afterSource) return null;
6126
- if (localName(beforeSource.name) !== "p" || localName(afterSource.name) !== "p") return null;
6127
- const blocks = this.sections[0].blocks;
6128
- if (blocks.some((block) => block.src === afterSource)) return null;
6129
- const blockIndex = blocks.findIndex((block) => block.type === "paragraph" && block.src === beforeSource);
6130
- if (blockIndex < 0) return null;
6122
+ const parsed = this.reparseDirectBodyParagraphSplits(beforeSource, [afterSource]);
6123
+ return parsed ? { before: parsed[0], after: parsed[1] } : null;
6124
+ }
6125
+ /** Reparse a body paragraph plus several new siblings created by
6126
+ * click-and-type without rebuilding the complete document model. */
6127
+ reparseDirectBodyParagraphSplits(beforeSource, afterSources) {
6128
+ if (afterSources.length === 0) return null;
6129
+ const parent = this.findParentOf(beforeSource);
6130
+ if (!parent) return null;
6131
+ const beforeIndex = parent.children.indexOf(beforeSource);
6132
+ if (beforeIndex < 0 || afterSources.some((source, i) => parent.children[beforeIndex + i + 1] !== source)) {
6133
+ return null;
6134
+ }
6135
+ if (localName(beforeSource.name) !== "p" || afterSources.some((source) => localName(source.name) !== "p")) {
6136
+ return null;
6137
+ }
6138
+ const findBlockList = (blocks2) => {
6139
+ const index = blocks2.findIndex((block) => block.type === "paragraph" && block.src === beforeSource);
6140
+ if (index >= 0) return { blocks: blocks2, index };
6141
+ for (const block of blocks2) {
6142
+ if (block.type !== "table") continue;
6143
+ for (const row of block.rows) {
6144
+ for (const cell of row.cells) {
6145
+ const found = findBlockList(cell.blocks);
6146
+ if (found) return found;
6147
+ }
6148
+ }
6149
+ }
6150
+ return null;
6151
+ };
6152
+ let location = null;
6153
+ for (const section of this.sections) {
6154
+ location = findBlockList(section.blocks);
6155
+ if (location) break;
6156
+ }
6157
+ if (!location) return null;
6158
+ const { blocks, index: blockIndex } = location;
6159
+ if (afterSources.some((source) => blocks.some((block) => block.src === source))) return null;
6131
6160
  const old = blocks[blockIndex];
6132
6161
  if (old.type !== "paragraph" || old.sectionBreak) return null;
6133
6162
  const unsafe = (element) => {
@@ -6135,7 +6164,7 @@ var DocxDocument = class _DocxDocument {
6135
6164
  if (name === "sectPr" || name === "bookmarkStart" || name === "bookmarkEnd" || name === "fldChar" || name === "instrText" || name === "fldSimple" || name === "sdt" || name === "ins" || name === "del" || name.startsWith("move") || name.endsWith("PrChange")) return true;
6136
6165
  return element.children.some(unsafe);
6137
6166
  };
6138
- if (unsafe(beforeSource) || unsafe(afterSource)) return null;
6167
+ if (unsafe(beforeSource) || afterSources.some(unsafe)) return null;
6139
6168
  const readPart = (part) => this.readXmlOptional(part);
6140
6169
  const ctx = {
6141
6170
  ...this.ctxBase,
@@ -6143,11 +6172,103 @@ var DocxDocument = class _DocxDocument {
6143
6172
  readPart,
6144
6173
  independentTextboxStories: true
6145
6174
  };
6146
- const before = parseParagraph(beforeSource, ctx);
6147
- const after = parseParagraph(afterSource, ctx);
6148
- if (before.revisionHidden || after.revisionHidden || before.sectionBreak || after.sectionBreak) return null;
6149
- blocks.splice(blockIndex, 1, before, after);
6150
- return { before, after };
6175
+ const parsed = [beforeSource, ...afterSources].map((source) => parseParagraph(source, ctx));
6176
+ if (parsed.some((paragraph) => paragraph.revisionHidden || paragraph.sectionBreak)) return null;
6177
+ blocks.splice(blockIndex, 1, ...parsed);
6178
+ return parsed;
6179
+ }
6180
+ /** Reparse one retained body-story paragraph after a structural edit that
6181
+ * leaves its surrounding block list unchanged, such as inserting ink. */
6182
+ reparseBodyParagraph(source) {
6183
+ if (localName(source.name) !== "p") return null;
6184
+ const findBlockList = (blocks) => {
6185
+ const index = blocks.findIndex((block) => block.type === "paragraph" && block.src === source);
6186
+ if (index >= 0) return { blocks, index };
6187
+ for (const block of blocks) {
6188
+ if (block.type !== "table") continue;
6189
+ for (const row of block.rows) {
6190
+ for (const cell of row.cells) {
6191
+ const found = findBlockList(cell.blocks);
6192
+ if (found) return found;
6193
+ }
6194
+ }
6195
+ }
6196
+ return null;
6197
+ };
6198
+ let location = null;
6199
+ for (const section of this.sections) {
6200
+ location = findBlockList(section.blocks);
6201
+ if (location) break;
6202
+ }
6203
+ if (!location) return null;
6204
+ const old = location.blocks[location.index];
6205
+ if (old.type !== "paragraph" || old.sectionBreak) return null;
6206
+ const unsafe = (element) => {
6207
+ const name = localName(element.name);
6208
+ if (name === "sectPr" || name === "bookmarkStart" || name === "bookmarkEnd") return true;
6209
+ return element.children.some(unsafe);
6210
+ };
6211
+ if (unsafe(source)) return null;
6212
+ const paragraph = parseParagraph(source, {
6213
+ ...this.ctxBase,
6214
+ rels: this.documentRels,
6215
+ readPart: (part) => this.readXmlOptional(part),
6216
+ independentTextboxStories: true
6217
+ });
6218
+ if (paragraph.revisionHidden || paragraph.sectionBreak) return null;
6219
+ location.blocks[location.index] = paragraph;
6220
+ return paragraph;
6221
+ }
6222
+ /** Reparse two sibling body-story paragraphs after Backspace/Delete merged
6223
+ * their XML into one. Keeps the parsed model generation stable so long
6224
+ * documents can use incremental layout instead of repaginating in full. */
6225
+ reparseDirectBodyParagraphMerge(beforeSource, afterSource, survivorSource) {
6226
+ if (survivorSource !== beforeSource && survivorSource !== afterSource) return null;
6227
+ if (localName(beforeSource.name) !== "p" || localName(afterSource.name) !== "p") return null;
6228
+ const parent = this.findParentOf(survivorSource);
6229
+ if (!parent || parent.children.includes(survivorSource === beforeSource ? afterSource : beforeSource)) return null;
6230
+ const findBlockList = (blocks2) => {
6231
+ const index2 = blocks2.findIndex(
6232
+ (block, i) => block.type === "paragraph" && block.src === beforeSource && blocks2[i + 1]?.type === "paragraph" && blocks2[i + 1].src === afterSource
6233
+ );
6234
+ if (index2 >= 0) return { blocks: blocks2, index: index2 };
6235
+ for (const block of blocks2) {
6236
+ if (block.type !== "table") continue;
6237
+ for (const row of block.rows) {
6238
+ for (const cell of row.cells) {
6239
+ const found = findBlockList(cell.blocks);
6240
+ if (found) return found;
6241
+ }
6242
+ }
6243
+ }
6244
+ return null;
6245
+ };
6246
+ let location = null;
6247
+ for (const section of this.sections) {
6248
+ location = findBlockList(section.blocks);
6249
+ if (location) break;
6250
+ }
6251
+ if (!location) return null;
6252
+ const { blocks, index } = location;
6253
+ const before = blocks[index];
6254
+ const after = blocks[index + 1];
6255
+ if (before.type !== "paragraph" || after.type !== "paragraph" || before.sectionBreak || after.sectionBreak) return null;
6256
+ const unsafe = (element) => {
6257
+ const name = localName(element.name);
6258
+ if (name === "sectPr" || name === "bookmarkStart" || name === "bookmarkEnd" || name === "fldChar" || name === "instrText" || name === "fldSimple" || name === "sdt" || name === "ins" || name === "del" || name.startsWith("move") || name.endsWith("PrChange")) return true;
6259
+ return element.children.some(unsafe);
6260
+ };
6261
+ if (unsafe(beforeSource) || unsafe(afterSource)) return null;
6262
+ const readPart = (part) => this.readXmlOptional(part);
6263
+ const merged = parseParagraph(survivorSource, {
6264
+ ...this.ctxBase,
6265
+ rels: this.documentRels,
6266
+ readPart,
6267
+ independentTextboxStories: true
6268
+ });
6269
+ if (merged.revisionHidden || merged.sectionBreak) return null;
6270
+ blocks.splice(index, 2, merged);
6271
+ return merged;
6151
6272
  }
6152
6273
  deriveComments() {
6153
6274
  const out = [];
@@ -7974,7 +8095,10 @@ var WORD_FONT_METRICS = {
7974
8095
  // baselines 118.80 + n x 20.00; first attempt baked the multiple into the
7975
8096
  // profile and over-pitched every line by 1.08x).
7976
8097
  mangal: { asc: 1.245, desc: 0.4399, gap: 0 },
7977
- latha: { asc: 0.9266, desc: 0.3369, gap: 0 }
8098
+ latha: { asc: 0.9266, desc: 0.3369, gap: 0 },
8099
+ // Word substitutes Lao text to DokChampa. Its hhea box is 2850/-1117 at
8100
+ // 2048 UPEM; this is also the line profile Word uses in the reference PDF.
8101
+ "noto sans lao looped": { asc: 1.391602, desc: 0.54541, gap: 0 }
7978
8102
  };
7979
8103
  var QUARTER_PT_PX = 1 / 3;
7980
8104
  function quantizeQuarterPt(px) {
@@ -9066,6 +9190,7 @@ var JUSTIFY_MAX_COMPRESS = 0.25;
9066
9190
  var JUSTIFY_STRETCH_FACTOR = 0.5;
9067
9191
  var TAMIL_GLYPH_SCALE = 0.735;
9068
9192
  var TAMIL_BASELINE_DY_EM = 0.136;
9193
+ var LAO_GLYPH_SCALE = 1.048;
9069
9194
  var KASHIDA_LOW_EM = 0;
9070
9195
  var KASHIDA_MEDIUM_EM = 0.032;
9071
9196
  var KASHIDA_HIGH_EM = 0.04;
@@ -9420,6 +9545,7 @@ function breakParagraphImpl(doc, measurer, para, contentWidth, fields, numbering
9420
9545
  href: atom.href,
9421
9546
  src: atom.src ? { ...atom.src, offset: atom.src.offset + sliceOff } : void 0,
9422
9547
  metricsFont: atom.metricsFont,
9548
+ metricsStrut: atom.metricsStrut,
9423
9549
  rtl: atom.rtl,
9424
9550
  rtlLevel: levelOf(atom.rtl)
9425
9551
  });
@@ -9543,6 +9669,44 @@ function breakParagraphImpl(doc, measurer, para, contentWidth, fields, numbering
9543
9669
  if (found) target = stop.pos - pre;
9544
9670
  }
9545
9671
  }
9672
+ if (doc.compatibilityMode < 15 && stop.align === "left" && !bidiPara && curLineWidth > 0) {
9673
+ let nextAtom = ai + 1;
9674
+ let probeX = Math.max(target, x + 2);
9675
+ while (atoms[nextAtom]?.kind === "tab") {
9676
+ let probeStop = nextTabStop(
9677
+ probeX + offset,
9678
+ props.tabs,
9679
+ contentWidth - indentRight,
9680
+ doc.defaultTabStop
9681
+ );
9682
+ if (opts?.inTableCell) {
9683
+ let guard = 0;
9684
+ while (probeStop.align === "decimal" && guard++ < 8) {
9685
+ probeStop = nextTabStop(
9686
+ probeStop.pos,
9687
+ props.tabs,
9688
+ contentWidth - indentRight,
9689
+ doc.defaultTabStop
9690
+ );
9691
+ }
9692
+ }
9693
+ if (probeStop.align !== "left") break;
9694
+ probeX = Math.max(probeStop.pos - offset, probeX + 2);
9695
+ nextAtom++;
9696
+ }
9697
+ let firstWordWidth = 0;
9698
+ for (let j = nextAtom; j < atoms.length; j++) {
9699
+ const a = atoms[j];
9700
+ if (a.kind !== "frag") break;
9701
+ firstWordWidth += a.width;
9702
+ }
9703
+ const lineEnd2 = lineStartX(lineIndex) + availFor(lineIndex);
9704
+ if (firstWordWidth > 0 && probeX + firstWordWidth > lineEnd2 + 0.01) {
9705
+ flush(false, false);
9706
+ ai--;
9707
+ continue;
9708
+ }
9709
+ }
9546
9710
  const aligned = (stop.align === "right" || stop.align === "decimal") && !bidiPara;
9547
9711
  if (aligned && curLineWidth > 0 && x > target + 0.75) {
9548
9712
  flush(false, false);
@@ -9808,7 +9972,7 @@ function breakParagraphImpl(doc, measurer, para, contentWidth, fields, numbering
9808
9972
  }
9809
9973
  const padL = bdrPad && (!sameBdrAtom(atoms[ai - 1], atom) || curLineWidth === 0) ? bdrPad : 0;
9810
9974
  x += padL;
9811
- cur.push({ x, width: atom.width, text: atom.text, props: atom.props, font: atom.font, href: atom.href, src: atom.src, noteId: atom.noteId, metricsFont: atom.metricsFont, breakAfter: atom.breakAfter, pageRef: atom.pageRef, rtl: atom.rtl, rtlLevel: levelOf(atom.rtl), ruby: atom.ruby });
9975
+ cur.push({ x, width: atom.width, text: atom.text, props: atom.props, font: atom.font, href: atom.href, src: atom.src, noteId: atom.noteId, metricsFont: atom.metricsFont, metricsStrut: atom.metricsStrut, breakAfter: atom.breakAfter, pageRef: atom.pageRef, rtl: atom.rtl, rtlLevel: levelOf(atom.rtl), ruby: atom.ruby });
9812
9976
  curLineWidth += padL + atom.width + bdrPadR;
9813
9977
  x += atom.width + bdrPadR;
9814
9978
  if (atom.text.trim().length > 0) prevContentCJK = fragIsCJK;
@@ -9965,6 +10129,7 @@ function finishLine(spans, width, props, measurer, fallbackFamily, para, doc, is
9965
10129
  let maxBorderedAscent = 0;
9966
10130
  let maxBorderedDescent = 0;
9967
10131
  let tallestTextIsEa = false;
10132
+ let metricsStrutHeight = 0;
9968
10133
  const consider = (font, imageHeight, objRaise = 0, descShift = 0) => {
9969
10134
  if (imageHeight !== void 0) {
9970
10135
  maxAscent = Math.max(maxAscent, imageHeight + objRaise);
@@ -10005,7 +10170,7 @@ function finishLine(spans, width, props, measurer, fallbackFamily, para, doc, is
10005
10170
  };
10006
10171
  let metricSpans = spans.filter((s) => !(s.text === " " && !s.leader));
10007
10172
  const solidSpans = metricSpans.filter(
10008
- (s) => s.image || s.drawing || s.math || s.leader || s.text === void 0 || !/^[ \t]*$/.test(s.text)
10173
+ (s) => s.metricsStrut || s.image || s.drawing || s.math || s.leader || s.text === void 0 || !/^[ \t]*$/.test(s.text)
10009
10174
  );
10010
10175
  if (solidSpans.length > 0) metricSpans = solidSpans;
10011
10176
  if (metricSpans.some((s) => s.numLabel) && metricSpans.some((s) => !s.numLabel)) {
@@ -10038,7 +10203,13 @@ function finishLine(spans, width, props, measurer, fallbackFamily, para, doc, is
10038
10203
  const lineHasObject = metricSpans.some((s) => s.image || s.drawing);
10039
10204
  for (const s of metricSpans) {
10040
10205
  let textMetrics;
10041
- if (s.image || s.drawing) {
10206
+ if (s.metricsStrut) {
10207
+ metricsStrutHeight = Math.max(
10208
+ metricsStrutHeight,
10209
+ measurer.metrics(s.metricsFont ?? s.font).lineHeight
10210
+ );
10211
+ consider(s.metricsFont ?? s.font);
10212
+ } else if (s.image || s.drawing) {
10042
10213
  const objectMetrics = measurer.metrics(s.metricsFont ?? s.font);
10043
10214
  const objectHeight = s.image?.height ?? s.drawing.height;
10044
10215
  const objRaise = s.props.raise ?? 0;
@@ -10093,6 +10264,14 @@ function finishLine(spans, width, props, measurer, fallbackFamily, para, doc, is
10093
10264
  const commonLower = metricSpans.length > 0 && metricSpans.every(
10094
10265
  (s) => !s.image && !s.drawing && !s.math && (s.props.raise ?? 0) < 0
10095
10266
  ) ? Math.min(...metricSpans.map((s) => -s.props.raise)) : 0;
10267
+ if (metricsStrutHeight > 0) {
10268
+ for (const s of spans) {
10269
+ if (s.metricsStrut || s.image || s.drawing || s.math) continue;
10270
+ const spanHeight = measurer.metrics(s.font).lineHeight;
10271
+ const paintDY = Math.max(0, (metricsStrutHeight - spanHeight) / 2);
10272
+ if (paintDY > 0) s.font = { ...s.font, paintDY: (s.font.paintDY ?? 0) + paintDY };
10273
+ }
10274
+ }
10096
10275
  const natural = Math.max(maxNatural, maxAscent + maxDescent, minLineHeight ?? 0);
10097
10276
  let height = natural;
10098
10277
  let baselineH;
@@ -10253,11 +10432,22 @@ function resolveBidiRtl(text) {
10253
10432
  function buildAtoms(doc, para, measurer, fields, fallbackFamily, gridPitch) {
10254
10433
  const atoms = [];
10255
10434
  const anchors = [];
10435
+ const stylelessOleParagraph = doc.styles.defaultRPr.font === void 0 && doc.styles.defaultRPr.size === void 0 && para.children.some((child4) => {
10436
+ const runs = child4.type === "run" ? [child4] : child4.runs;
10437
+ return runs.some(
10438
+ (run) => run.content.some(
10439
+ (content) => content.kind === "image" && content.embeddedObject !== void 0
10440
+ )
10441
+ );
10442
+ });
10256
10443
  const bidiPara = doc.effectiveParaProps(para).bidi === true;
10257
10444
  const kashidaKind = doc.effectiveParaProps(para).justifyKind;
10258
10445
  const kashidaEm = kashidaKind === "highKashida" ? KASHIDA_HIGH_EM : kashidaKind === "mediumKashida" ? KASHIDA_MEDIUM_EM : kashidaKind === "lowKashida" ? KASHIDA_LOW_EM : 0;
10259
10446
  const pushRun = (run, href) => {
10260
- const baseProps = doc.effectiveRunProps(para, run.props);
10447
+ let baseProps = doc.effectiveRunProps(para, run.props);
10448
+ if (stylelessOleParagraph && baseProps.size === void 0) {
10449
+ baseProps = { ...baseProps, size: 16 };
10450
+ }
10261
10451
  if (baseProps.vanish) return;
10262
10452
  const font = fontOf(baseProps, fallbackFamily);
10263
10453
  let props = kashidaEm > 0 && baseProps.rtl ? { ...baseProps, letterSpacing: (baseProps.letterSpacing ?? 0) + kashidaEm * font.size } : baseProps;
@@ -10327,6 +10517,20 @@ function buildAtoms(doc, para, measurer, fields, fallbackFamily, gridPitch) {
10327
10517
  });
10328
10518
  break;
10329
10519
  }
10520
+ if (!text && /^\s*SEQ\b/i.test(content.instruction) && /\\h(\s|$)/i.test(content.instruction)) {
10521
+ atoms.push({
10522
+ kind: "frag",
10523
+ text: "",
10524
+ props,
10525
+ font,
10526
+ width: 0,
10527
+ href,
10528
+ src: { run, t: null, offset: 0 },
10529
+ metricsFont: vertMetricsFont,
10530
+ metricsStrut: true
10531
+ });
10532
+ break;
10533
+ }
10330
10534
  if (text) pushStyled(displayText(text, props), props, font, href, { run, t: null, offset: 0 }, vertMetricsFont);
10331
10535
  break;
10332
10536
  }
@@ -10371,7 +10575,7 @@ function buildAtoms(doc, para, measurer, fields, fallbackFamily, gridPitch) {
10371
10575
  break;
10372
10576
  }
10373
10577
  case "noteRef": {
10374
- const text = content.self ? fields.selfNoteMark?.() ?? "" : fields.noteMark?.(content.noteType, content.id) ?? "";
10578
+ const text = content.self ? fields.selfNoteMark?.() ?? "" : content.customMarkFollows ? "" : fields.noteMark?.(content.noteType, content.id) ?? "";
10375
10579
  if (!text) break;
10376
10580
  const markProps = props.verticalAlign ? props : { ...props, verticalAlign: "superscript" };
10377
10581
  const markFont = fontOf(markProps, fallbackFamily);
@@ -10427,6 +10631,7 @@ function buildAtoms(doc, para, measurer, fields, fallbackFamily, gridPitch) {
10427
10631
  const hasLao = /[຀-໿]/.test(text);
10428
10632
  const indicFace = hasDeva && !hasTamil && !hasLao ? "Mangal" : hasTamil && !hasDeva && !hasLao ? "Latha" : hasLao && !hasDeva && !hasTamil ? "Noto Sans Lao Looped" : null;
10429
10633
  if (indicFace && font.family.toLowerCase() !== indicFace.toLowerCase()) {
10634
+ const scriptProps = indicFace === "Noto Sans Lao Looped" ? { ...props, textScale: (props.textScale ?? 1) * LAO_GLYPH_SCALE } : props;
10430
10635
  const indicScale = indicFace === "Latha" ? TAMIL_GLYPH_SCALE : 1;
10431
10636
  const indicFont = indicScale === 1 ? { ...font, family: indicFace } : {
10432
10637
  ...font,
@@ -10446,7 +10651,7 @@ function buildAtoms(doc, para, measurer, fields, fallbackFamily, gridPitch) {
10446
10651
  const seg = text.slice(i2, j);
10447
10652
  const src = srcBase ? { ...srcBase, offset: srcBase.offset + i2 } : void 0;
10448
10653
  if (isSpace) pushText(seg, props, font, href, src, metricsFont);
10449
- else pushText(seg, props, indicFont, href, src, indicMetrics);
10654
+ else pushText(seg, scriptProps, indicFont, href, src, indicMetrics);
10450
10655
  i2 = j;
10451
10656
  }
10452
10657
  return;
@@ -10669,8 +10874,8 @@ function resolveField(instruction, cachedResult, ctx, fieldKey) {
10669
10874
  }
10670
10875
  case "SEQ": {
10671
10876
  const ident = instr.split(/\s+/)[1];
10672
- if (ident && ctx.seq && fieldKey) return ctx.seq(ident, fieldKey, instr);
10673
- return cachedResult || "";
10877
+ const value = ident && ctx.seq && fieldKey ? ctx.seq(ident, fieldKey, instr) : cachedResult || "";
10878
+ return /\\h(\s|$)/i.test(instr) ? "" : value;
10674
10879
  }
10675
10880
  case "REF": {
10676
10881
  const bookmark = instr.split(/\s+/)[1];
@@ -10833,6 +11038,7 @@ var __incrStats = {
10833
11038
  resumePage: -1,
10834
11039
  convergedBlock: -1,
10835
11040
  convergedPage: -1,
11041
+ pageShift: 0,
10836
11042
  blocksLaid: 0,
10837
11043
  fallbackReason: ""
10838
11044
  };
@@ -10892,6 +11098,7 @@ var MULTI_COL_NOTE_SEP_RESERVE = 26;
10892
11098
  var NOTE_SEP_LEN = 192;
10893
11099
  var NOTE_SEP_GAP = 4.4;
10894
11100
  var ROW_OVERHANG_TOL = 3;
11101
+ var CUSTOM_NOTE_BANNER_OVERHANG = 3;
10895
11102
  var CHICAGO = ["*", "\u2020", "\u2021", "\xA7"];
10896
11103
  var AUTO_PARA_SPACING_PX = 14 * (96 / 72);
10897
11104
  var FRAME_WRAP_HSPACE_PX = 6 * (96 / 72);
@@ -10933,6 +11140,7 @@ var Engine = class {
10933
11140
  this.trailingSectionBreakMarkGap = 0;
10934
11141
  /** Bookmark name -> formatted display page number (PAGEREF rewrite). */
10935
11142
  this.bookmarkPages = /* @__PURE__ */ new Map();
11143
+ this.bookmarkPageIndices = /* @__PURE__ */ new Map();
10936
11144
  /** STYLEREF page-awareness: styleIds whose paragraphs a header/footer STYLEREF
10937
11145
  * references (null until precomputed; empty set = no STYLEREF, tracking off). */
10938
11146
  this.styleRefTrack = null;
@@ -10998,6 +11206,7 @@ var Engine = class {
10998
11206
  /** Vertical flow already consumed in the current later-column pre-banner
10999
11207
  * slot. It reduces the below-banner capacity by the same amount. */
11000
11208
  this.bannerSlotUsed = 0;
11209
+ this.customNoteBannerFit = false;
11001
11210
  /** True while a section-level tbRl vertical flow is being laid: paragraphs
11002
11211
  * re-establish the vertical grid after embedded Western runs (see
11003
11212
  * breakParagraph's verticalGridResync). */
@@ -11025,6 +11234,8 @@ var Engine = class {
11025
11234
  this.incrConvergeBlockIdx = -1;
11026
11235
  this.incrConvergePageIdx = -1;
11027
11236
  this.incrConvergeItemDelta = 0;
11237
+ this.incrConvergePrevPointPageIdx = -1;
11238
+ this.incrPageShift = 0;
11028
11239
  /** New block indices are old indices plus this value after a structural
11029
11240
  * split, so convergence compares the same semantic suffix boundary. */
11030
11241
  this.incrBlockDelta = 0;
@@ -11113,7 +11324,7 @@ var Engine = class {
11113
11324
  const contentWidth = page.sp.pageWidth - page.sp.marginLeft - page.sp.marginRight - page.sp.gutter;
11114
11325
  const header = this.doc.headers.get(page.headerRel ?? "");
11115
11326
  const footer = this.doc.footers.get(page.footerRel ?? "");
11116
- const headerHeight = this.measureHeaderFooter(header, page, contentWidth, this.pageFieldFrameOverlay(header));
11327
+ const headerHeight = this.measureHeaderFooter(header, page, contentWidth, this.pageFieldFrameOverlay(header), true);
11117
11328
  const footerHeight = this.measureHeaderFooter(footer, page, contentWidth, this.pageFieldFrameOverlay(footer));
11118
11329
  if (headerHeight !== page.headerHeight || footerHeight !== page.footerHeight) return null;
11119
11330
  }
@@ -11213,6 +11424,7 @@ var Engine = class {
11213
11424
  if (this.pages.length === 0) {
11214
11425
  this.sp = sections[0]?.props ?? {};
11215
11426
  }
11427
+ this.applyOpeningFlowOverlap();
11216
11428
  this.placeEndnotes();
11217
11429
  this.emitFootnoteAreas();
11218
11430
  this.emitColumnSeparators();
@@ -11231,6 +11443,7 @@ var Engine = class {
11231
11443
  points: this.incrPoints,
11232
11444
  pages: this.pages,
11233
11445
  bookmarks: this.bookmarkPages,
11446
+ bookmarkPageIndices: this.bookmarkPageIndices,
11234
11447
  modelVersion: this.doc.modelVersion,
11235
11448
  seqCounters: this.seqCounters,
11236
11449
  seqAssigned: this.seqAssigned,
@@ -11240,6 +11453,43 @@ var Engine = class {
11240
11453
  }
11241
11454
  return result;
11242
11455
  }
11456
+ /** Some document-opening empty paragraphs reserve a second mark line for
11457
+ * pagination while the following content paints over it on page one. Keep
11458
+ * the reservation in flow so later page breaks stay unchanged, then lift
11459
+ * only the painted first-page body. */
11460
+ applyOpeningFlowOverlap() {
11461
+ const opening = this.doc.sections[0]?.blocks[0];
11462
+ const next = this.doc.sections[0]?.blocks[1];
11463
+ const page = this.pages[0];
11464
+ if (!page || page.openingFlowOverlapApplied || opening?.type !== "paragraph" || opening.sectionBreak || paragraphHasContent(opening)) {
11465
+ return;
11466
+ }
11467
+ const beforeNegativeTable = next?.type === "table" && (next.props.indent ?? 0) < 0;
11468
+ const header = this.doc.headers.get(page.headerRel ?? "");
11469
+ const headerAnchors = header?.blocks.flatMap(
11470
+ (block) => block.type === "paragraph" ? this.collectAnchors(block) : []
11471
+ ) ?? [];
11472
+ const beforeParagraphUnderUnwrappedHeader = next?.type === "paragraph" && page.headerGrown === true && headerAnchors.length > 0 && headerAnchors.every(
11473
+ (shape) => !("wrap" in shape) || shape.wrap === void 0 || shape.wrap === "none"
11474
+ );
11475
+ if (!beforeNegativeTable && !beforeParagraphUnderUnwrappedHeader) return;
11476
+ const firstBodyItem = page.items.findIndex(
11477
+ (item) => item.kind !== "text" || item.text.length > 0
11478
+ );
11479
+ if (firstBodyItem < 0) return;
11480
+ const paraProps = this.doc.effectiveParaProps(opening);
11481
+ const markProps = this.doc.effectiveRunProps(opening, paraProps.markRunProps ?? {});
11482
+ let overlap = this.measurer.metrics(
11483
+ fontOf(markProps, this.doc.styles.defaultRPr.font ?? "Calibri")
11484
+ ).lineHeight;
11485
+ if (beforeParagraphUnderUnwrappedHeader) {
11486
+ overlap += paraProps.spacingAfter ?? 0;
11487
+ }
11488
+ for (let i = firstBodyItem; i < page.items.length; i++) {
11489
+ offsetItem(page.items[i], 0, -overlap);
11490
+ }
11491
+ page.openingFlowOverlapApplied = true;
11492
+ }
11243
11493
  /** Whether this document is in the envelope the incremental path handles:
11244
11494
  * one section, no notes, single column, upright flow, no line numbering /
11245
11495
  * mirror margins / vertical alignment, and no positioned/floating content or
@@ -11367,9 +11617,13 @@ var Engine = class {
11367
11617
  });
11368
11618
  if (this.incrPrevPoints && blockIdx > this.incrFirstDirty) {
11369
11619
  const pp = this.incrPrevPoints.get(blockIdx);
11370
- if (pp && pp.pageCount === globalPageIdx && this.statesMatch(pp.state)) {
11620
+ const pageShift = pp ? globalPageIdx - pp.pageCount : 0;
11621
+ if (pp && this.statesMatch(pp.state, pageShift)) {
11371
11622
  __incrStats.convergedBlock = blockIdx;
11372
11623
  __incrStats.convergedPage = globalPageIdx;
11624
+ __incrStats.pageShift = pageShift;
11625
+ this.incrPageShift = pageShift;
11626
+ this.incrConvergePrevPointPageIdx = pp.pageCount;
11373
11627
  this.incrPoints.push(snapshot());
11374
11628
  const reuseWholePage = p.items.length === 0 && pp.pageItemCount === 0 && Math.abs(this.y - p.bodyTop) < 1e-6;
11375
11629
  if (reuseWholePage) {
@@ -11394,9 +11648,9 @@ var Engine = class {
11394
11648
  /** True when the current engine state at a clean page top exactly matches a
11395
11649
  * prev-layout capture point — i.e. the relaid tail has re-converged and the
11396
11650
  * rest of the layout is guaranteed identical. */
11397
- statesMatch(s) {
11651
+ statesMatch(s, pageShift = 0) {
11398
11652
  const p = this.cur;
11399
- if (this.col !== s.col || this.y !== s.y || this.sectionFirstPagePhys !== s.sectionFirstPagePhys || this.lastParaSpacingAfter !== s.lastParaSpacingAfter || this.lastParaAfterPad !== s.lastParaAfterPad || this.lastParaWasEmpty !== s.lastParaWasEmpty || this.trailingSectionBreakMarkGap !== s.trailingSectionBreakMarkGap || this.suppressNextSpaceBefore !== s.suppressNextSpaceBefore || this.docGridDropBefore !== s.docGridDropBefore || this.gridResyncPending !== s.gridResyncPending || this.verticalGridFlow !== s.verticalGridFlow || this.bannerSlotUsed !== s.bannerSlotUsed || p.physIndex !== s.page.physIndex || p.displayNumber !== s.page.displayNumber || p.bodyTop !== s.page.bodyTop || p.bodyBottom !== s.page.bodyBottom) {
11653
+ if (this.col !== s.col || this.y !== s.y || this.sectionFirstPagePhys !== s.sectionFirstPagePhys || this.lastParaSpacingAfter !== s.lastParaSpacingAfter || this.lastParaAfterPad !== s.lastParaAfterPad || this.lastParaWasEmpty !== s.lastParaWasEmpty || this.trailingSectionBreakMarkGap !== s.trailingSectionBreakMarkGap || this.suppressNextSpaceBefore !== s.suppressNextSpaceBefore || this.docGridDropBefore !== s.docGridDropBefore || this.gridResyncPending !== s.gridResyncPending || this.verticalGridFlow !== s.verticalGridFlow || this.bannerSlotUsed !== s.bannerSlotUsed || p.physIndex !== s.page.physIndex + pageShift || p.displayNumber !== s.page.displayNumber + pageShift || p.bodyTop !== s.page.bodyTop || p.bodyBottom !== s.page.bodyBottom) {
11400
11654
  return false;
11401
11655
  }
11402
11656
  if (this.seenNumIds.size !== s.seenNumIds.length) return false;
@@ -11453,11 +11707,60 @@ var Engine = class {
11453
11707
  this.counters = new Map(s.counters.map(([k, v]) => [k, [...v]]));
11454
11708
  this.seenNumIds = new Set(s.seenNumIds);
11455
11709
  }
11710
+ bodyHasPageField(el16 = this.doc.docRoot) {
11711
+ const name = el16.name.includes(":") ? el16.name.slice(el16.name.indexOf(":") + 1) : el16.name;
11712
+ const instruction = name === "instrText" ? el16.text : name === "fldSimple" ? el16.attrs["w:instr"] ?? "" : "";
11713
+ if (/\bPAGE\b/i.test(instruction)) return true;
11714
+ return el16.children.some((child4) => this.bodyHasPageField(child4));
11715
+ }
11716
+ /** Clone a retained suffix page onto its new physical/display number. Body
11717
+ * items and page-border items stay identical; headers and footers are removed
11718
+ * so the final pass can rebuild PAGE fields against the shifted number. */
11719
+ shiftRetainedPage(page, pageShift) {
11720
+ if (page.hfStart === void 0) return null;
11721
+ const physIndex = page.physIndex + pageShift;
11722
+ const displayNumber = page.displayNumber + pageShift;
11723
+ const isFirstOfSection = physIndex === this.sectionFirstPagePhys;
11724
+ const useEven = this.doc.evenAndOddHeaders && displayNumber % 2 === 0;
11725
+ const headerRel = page.sp.titlePage && isFirstOfSection ? page.sp.headerRefs.first : useEven ? page.sp.headerRefs.even : page.sp.headerRefs.default;
11726
+ const footerRel = page.sp.titlePage && isFirstOfSection ? page.sp.footerRefs.first : useEven ? page.sp.footerRefs.even : page.sp.footerRefs.default;
11727
+ const shifted = {
11728
+ ...page,
11729
+ items: page.items.slice(0, page.hfStart),
11730
+ physIndex,
11731
+ displayNumber,
11732
+ headerRel,
11733
+ footerRel,
11734
+ hfStart: void 0,
11735
+ colXs: [...page.colXs],
11736
+ colWidths: [...page.colWidths],
11737
+ footnotes: page.footnotes.map((note) => ({ ...note, items: [...note.items] })),
11738
+ footnoteH: [...page.footnoteH],
11739
+ bands: page.bands.map((band) => ({
11740
+ ...band,
11741
+ colXs: [...band.colXs],
11742
+ colWidths: [...band.colWidths],
11743
+ bottoms: [...band.bottoms]
11744
+ }))
11745
+ };
11746
+ if (headerRel === page.headerRel && footerRel === page.footerRel) return shifted;
11747
+ this.sp = shifted.sp;
11748
+ const contentWidth = shifted.sp.pageWidth - shifted.sp.marginLeft - shifted.sp.marginRight - shifted.sp.gutter;
11749
+ const header = this.doc.headers.get(headerRel ?? "");
11750
+ const footer = this.doc.footers.get(footerRel ?? "");
11751
+ const headerHeight = this.measureHeaderFooter(header, shifted, contentWidth, this.pageFieldFrameOverlay(header), true);
11752
+ const footerHeight = this.measureHeaderFooter(footer, shifted, contentWidth, this.pageFieldFrameOverlay(footer));
11753
+ if (headerHeight !== page.headerHeight || footerHeight !== page.footerHeight) return null;
11754
+ shifted.headerHeight = headerHeight;
11755
+ shifted.footerHeight = footerHeight;
11756
+ return shifted;
11757
+ }
11456
11758
  /** Attempt an incremental layout; return null (and leave a discarded, dirty
11457
11759
  * engine) if reuse can't be proven safe, so the caller does a full layout in a
11458
11760
  * fresh engine. */
11459
11761
  runIncremental(prev, dirtyHint, dirtySource) {
11460
11762
  __incrStats.fallbackReason = "";
11763
+ __incrStats.pageShift = 0;
11461
11764
  const fallback = (reason) => {
11462
11765
  __incrStats.fallbackReason = reason;
11463
11766
  const perf2 = globalThis.__dxwPerf;
@@ -11472,11 +11775,12 @@ var Engine = class {
11472
11775
  const blocks = this.doc.sections[0].blocks;
11473
11776
  let newSigs = null;
11474
11777
  let firstDirty = 0;
11475
- if (dirtyHint && blocks.length === inc.sigs.length + 1) {
11778
+ if (dirtyHint && blocks.length > inc.sigs.length) {
11779
+ const delta = blocks.length - inc.sigs.length;
11476
11780
  const inserted = blocks.findIndex((block) => block.src === dirtyHint);
11477
- const changed = inserted - 1;
11781
+ const changed = inserted - delta;
11478
11782
  if (changed >= 0) {
11479
- let hashed = 2;
11783
+ let hashed = delta + 1;
11480
11784
  let neighborsOk = true;
11481
11785
  if (changed > 0) {
11482
11786
  hashed++;
@@ -11484,21 +11788,47 @@ var Engine = class {
11484
11788
  }
11485
11789
  if (neighborsOk && inserted < blocks.length - 1) {
11486
11790
  hashed++;
11487
- neighborsOk = this.blockSig(blocks[inserted + 1]) === inc.sigs[inserted];
11791
+ neighborsOk = this.blockSig(blocks[inserted + 1]) === inc.sigs[changed + 1];
11488
11792
  }
11489
11793
  if (neighborsOk) {
11490
11794
  newSigs = inc.sigs.slice();
11491
11795
  newSigs[changed] = this.blockSig(blocks[changed]);
11492
- newSigs.splice(inserted, 0, this.blockSig(blocks[inserted]));
11796
+ newSigs.splice(changed + 1, 0, ...blocks.slice(changed + 1, inserted + 1).map((block) => this.blockSig(block)));
11493
11797
  firstDirty = changed;
11494
11798
  __incrStats.hintFastPath = true;
11495
11799
  __incrStats.blocksHashed = hashed;
11496
11800
  __incrStats.firstDirty = changed;
11497
- this.incrBlockDelta = 1;
11801
+ this.incrBlockDelta = delta;
11498
11802
  this.incrBlockShiftAfter = changed;
11499
11803
  }
11500
11804
  }
11501
11805
  }
11806
+ if (newSigs === null && dirtyHint && blocks.length === inc.sigs.length - 1) {
11807
+ const changed = blocks.findIndex((block) => block.src === dirtyHint);
11808
+ if (changed >= 0) {
11809
+ let hashed = 1;
11810
+ let neighborsOk = true;
11811
+ if (changed > 0) {
11812
+ hashed++;
11813
+ neighborsOk = this.blockSig(blocks[changed - 1]) === inc.sigs[changed - 1];
11814
+ }
11815
+ if (neighborsOk && changed < blocks.length - 1) {
11816
+ hashed++;
11817
+ neighborsOk = this.blockSig(blocks[changed + 1]) === inc.sigs[changed + 2];
11818
+ }
11819
+ if (neighborsOk) {
11820
+ newSigs = inc.sigs.slice();
11821
+ newSigs[changed] = this.blockSig(blocks[changed]);
11822
+ newSigs.splice(changed + 1, 1);
11823
+ firstDirty = changed;
11824
+ __incrStats.hintFastPath = true;
11825
+ __incrStats.blocksHashed = hashed;
11826
+ __incrStats.firstDirty = changed;
11827
+ this.incrBlockDelta = -1;
11828
+ this.incrBlockShiftAfter = changed + 1;
11829
+ }
11830
+ }
11831
+ }
11502
11832
  if (newSigs === null && dirtyHint && blocks.length === inc.sigs.length) {
11503
11833
  const d = blocks.findIndex((b) => b.src === dirtyHint);
11504
11834
  if (d >= 0) {
@@ -11555,19 +11885,47 @@ var Engine = class {
11555
11885
  __incrStats.resumePage = rp.pageCount;
11556
11886
  __incrStats.convergedBlock = -1;
11557
11887
  __incrStats.convergedPage = -1;
11888
+ __incrStats.pageShift = 0;
11558
11889
  __incrStats.blocksLaid = 0;
11559
11890
  this.restoreIncrState(rp.state, inc.pages[rp.pageCount].items.slice(0, rp.pageItemCount));
11560
11891
  this.layoutBlocks(blocks, rp.blockIdx);
11561
11892
  if (this.incrAbort) return fallback("layout-abort");
11562
11893
  const suffixStart = this.incrConvergePrevPageIdx >= 0 ? this.incrConvergePrevPageIdx : inc.pages.length;
11563
- const totalPages = prefixCount + this.pages.length + (inc.pages.length - suffixStart);
11564
- if (totalPages !== prev.totalPages) return fallback("page-count");
11894
+ const middleCount = this.pages.length;
11895
+ const hasRetainedSuffix = suffixStart < inc.pages.length;
11896
+ const pageShift = hasRetainedSuffix ? prefixCount + middleCount - suffixStart : 0;
11897
+ if (hasRetainedSuffix && pageShift !== this.incrPageShift) return fallback("page-shift");
11898
+ if (pageShift !== 0 && this.bodyHasPageField()) return fallback("body-page-field-shift");
11899
+ const shiftedSuffix = [];
11900
+ if (pageShift !== 0) {
11901
+ for (const page of inc.pages.slice(suffixStart)) {
11902
+ const shifted = this.shiftRetainedPage(page, pageShift);
11903
+ if (!shifted) return fallback("shifted-header-footer");
11904
+ shiftedSuffix.push(shifted);
11905
+ }
11906
+ }
11565
11907
  const mergedBookmarks = new Map(inc.bookmarks);
11566
- const movedBookmarks = /* @__PURE__ */ new Set();
11908
+ const mergedBookmarkPageIndices = new Map(inc.bookmarkPageIndices);
11909
+ if (pageShift !== 0) {
11910
+ for (const [name, oldPageIndex] of inc.bookmarkPageIndices) {
11911
+ if (oldPageIndex < suffixStart) continue;
11912
+ const page = inc.pages[oldPageIndex];
11913
+ if (!page) continue;
11914
+ mergedBookmarkPageIndices.set(name, oldPageIndex + pageShift);
11915
+ mergedBookmarks.set(
11916
+ name,
11917
+ formatNumber(page.displayNumber + pageShift, PAGE_FMT[page.sp.pageNumberFormat ?? "decimal"] ?? "decimal")
11918
+ );
11919
+ }
11920
+ }
11567
11921
  for (const [name, page] of this.bookmarkPages) {
11568
- if (inc.bookmarks.get(name) !== page) movedBookmarks.add(name);
11569
11922
  mergedBookmarks.set(name, page);
11570
11923
  }
11924
+ for (const [name, pageIndex] of this.bookmarkPageIndices) mergedBookmarkPageIndices.set(name, pageIndex);
11925
+ const movedBookmarks = /* @__PURE__ */ new Set();
11926
+ for (const [name, page] of mergedBookmarks) {
11927
+ if (inc.bookmarks.get(name) !== page) movedBookmarks.add(name);
11928
+ }
11571
11929
  const pageRefUpdates = [];
11572
11930
  if (movedBookmarks.size > 0) {
11573
11931
  for (const page of inc.pages) {
@@ -11585,32 +11943,53 @@ var Engine = class {
11585
11943
  this.placeEndnotes();
11586
11944
  this.emitFootnoteAreas();
11587
11945
  this.emitColumnSeparators();
11588
- this.finalizeHeadersFooters();
11589
11946
  this.bookmarkPages = mergedBookmarks;
11590
- this.rewritePageRefs(this.pages);
11947
+ this.bookmarkPageIndices = mergedBookmarkPageIndices;
11591
11948
  this.applySectionVAlign();
11949
+ if (pageShift !== 0) {
11950
+ for (let i = 0; i < middleCount; i++) this.emitPageBorders(this.pages[i]);
11951
+ this.pages.push(...shiftedSuffix);
11952
+ this.finalizeHeadersFooters(false);
11953
+ } else {
11954
+ this.finalizeHeadersFooters();
11955
+ }
11956
+ this.rewritePageRefs(this.pages);
11592
11957
  if (this.incrAbort) return fallback("postpass-abort");
11593
- const middle = this.pages.map((p) => laidOutPage(p));
11958
+ const middle = this.pages.slice(0, middleCount).map((p) => laidOutPage(p));
11594
11959
  let structuralPrefixEnd = prefixCount;
11595
11960
  if (dirtySource) {
11596
- const dirtyMiddleIdx = this.pages.findIndex(
11961
+ const dirtyMiddleIdx = this.pages.slice(0, middleCount).findIndex(
11597
11962
  (page) => page.items.some((item) => item.kind === "text" && item.src?.t === dirtySource)
11598
11963
  );
11599
11964
  if (dirtyMiddleIdx > 0) structuralPrefixEnd += dirtyMiddleIdx;
11600
11965
  }
11601
- const outPages = prev.pages.slice(0, prefixCount).concat(middle, prev.pages.slice(suffixStart));
11602
- const outInternal = inc.pages.slice(0, prefixCount).concat(this.pages, inc.pages.slice(suffixStart));
11603
- const samePageTail = this.incrConvergeBlockIdx >= 0 ? inc.points.filter(
11604
- (pt) => pt.pageCount === this.incrConvergePageIdx && shiftedBlockIdx(pt.blockIdx) > this.incrConvergeBlockIdx
11605
- ).map((pt) => ({
11966
+ const outPages = prev.pages.slice(0, prefixCount).concat(
11967
+ pageShift !== 0 ? this.pages.map((page) => laidOutPage(page)) : middle.concat(prev.pages.slice(suffixStart))
11968
+ );
11969
+ const outInternal = inc.pages.slice(0, prefixCount).concat(
11970
+ pageShift !== 0 ? this.pages : this.pages.concat(inc.pages.slice(suffixStart))
11971
+ );
11972
+ const shiftPoint = (pt, itemDelta = 0) => ({
11606
11973
  ...pt,
11607
11974
  blockIdx: shiftedBlockIdx(pt.blockIdx),
11608
- pageItemCount: pt.pageItemCount + this.incrConvergeItemDelta
11609
- })) : [];
11975
+ pageCount: pt.pageCount + pageShift,
11976
+ pageItemCount: pt.pageItemCount + itemDelta,
11977
+ state: {
11978
+ ...pt.state,
11979
+ page: {
11980
+ ...pt.state.page,
11981
+ physIndex: pt.state.page.physIndex + pageShift,
11982
+ displayNumber: pt.state.page.displayNumber + pageShift
11983
+ }
11984
+ }
11985
+ });
11986
+ const samePageTail = this.incrConvergeBlockIdx >= 0 ? inc.points.filter(
11987
+ (pt) => pt.pageCount === this.incrConvergePrevPointPageIdx && shiftedBlockIdx(pt.blockIdx) > this.incrConvergeBlockIdx
11988
+ ).map((pt) => shiftPoint(pt, this.incrConvergeItemDelta)) : [];
11610
11989
  const points = inc.points.filter((pt) => pt.pageCount < prefixCount).concat(
11611
11990
  this.incrPoints,
11612
11991
  samePageTail,
11613
- inc.points.filter((pt) => pt.pageCount >= suffixStart).map((pt) => ({ ...pt, blockIdx: shiftedBlockIdx(pt.blockIdx) }))
11992
+ inc.points.filter((pt) => pt.pageCount >= suffixStart).map((pt) => shiftPoint(pt))
11614
11993
  );
11615
11994
  const perf = globalThis.__dxwPerf;
11616
11995
  if (perf) perf.incr = { ...__incrStats };
@@ -11623,6 +12002,7 @@ var Engine = class {
11623
12002
  points,
11624
12003
  pages: outInternal,
11625
12004
  bookmarks: mergedBookmarks,
12005
+ bookmarkPageIndices: mergedBookmarkPageIndices,
11626
12006
  modelVersion: this.doc.modelVersion,
11627
12007
  seqCounters: this.seqCounters,
11628
12008
  seqAssigned: this.seqAssigned,
@@ -11746,8 +12126,8 @@ var Engine = class {
11746
12126
  displayNumber,
11747
12127
  headerHeight: 0,
11748
12128
  footerHeight: 0,
11749
- bodyTop: Math.abs(sp.marginTop),
11750
- bandTop: Math.abs(sp.marginTop),
12129
+ bodyTop: sp.marginTop,
12130
+ bandTop: sp.marginTop,
11751
12131
  softTop: !sectionStart,
11752
12132
  bodyBottom: sp.pageHeight - Math.abs(sp.marginBottom),
11753
12133
  colXs,
@@ -11771,13 +12151,17 @@ var Engine = class {
11771
12151
  }
11772
12152
  const header = this.doc.headers.get(page.headerRel ?? "");
11773
12153
  const footer = this.doc.footers.get(page.footerRel ?? "");
11774
- const headerH = this.measureHeaderFooter(header, page, contentWidth, this.pageFieldFrameOverlay(header));
12154
+ const headerH = this.measureHeaderFooter(header, page, contentWidth, this.pageFieldFrameOverlay(header), true);
11775
12155
  const footerOverlay = this.pageFieldFrameOverlay(footer);
11776
12156
  const footerH = this.measureHeaderFooter(footer, page, contentWidth, footerOverlay);
11777
12157
  page.headerHeight = headerH;
11778
12158
  page.footerHeight = footerH;
11779
12159
  if (sp.marginTop >= 0) {
11780
- page.bodyTop = Math.max(sp.marginTop, headerH > 0 ? sp.headerDistance + headerH : 0);
12160
+ const headerBodyHeight = this.doc.compatibilityMode < 15 && header?.blocks.every((block) => block.type === "paragraph" && !paragraphHasContent(block)) ? ptToPx(7) : headerH;
12161
+ page.bodyTop = Math.max(
12162
+ sp.marginTop,
12163
+ headerBodyHeight > 0 ? sp.headerDistance + headerBodyHeight : 0
12164
+ );
11781
12165
  page.bandTop = page.bodyTop;
11782
12166
  page.headerGrown = page.bodyTop > sp.marginTop;
11783
12167
  }
@@ -11964,6 +12348,7 @@ var Engine = class {
11964
12348
  counters: new Map(Array.from(this.counters, ([k, v]) => [k, [...v]])),
11965
12349
  seenNumIds: new Set(this.seenNumIds),
11966
12350
  bookmarkPages: new Map(this.bookmarkPages),
12351
+ bookmarkPageIndices: new Map(this.bookmarkPageIndices),
11967
12352
  placedFootnotes: new Set(this.placedFootnotes),
11968
12353
  lnCounter: this.lnCounter,
11969
12354
  lnLastPage: this.lnLastPage,
@@ -12003,6 +12388,7 @@ var Engine = class {
12003
12388
  this.counters = s.counters;
12004
12389
  this.seenNumIds = s.seenNumIds;
12005
12390
  this.bookmarkPages = s.bookmarkPages;
12391
+ this.bookmarkPageIndices = s.bookmarkPageIndices;
12006
12392
  this.placedFootnotes = s.placedFootnotes;
12007
12393
  this.lnCounter = s.lnCounter;
12008
12394
  this.lnLastPage = s.lnLastPage;
@@ -12015,7 +12401,7 @@ var Engine = class {
12015
12401
  if (this.col + 1 < this.cur.colXs.length) {
12016
12402
  this.col++;
12017
12403
  this.y = this.columnStartY(this.col);
12018
- this.bannerSlotUsed = 0;
12404
+ this.bannerSlotUsed = this.cur.bannerTop !== void 0 ? this.cur.openingColumnReserve ?? 0 : 0;
12019
12405
  this.lastParaSpacingAfter = 0;
12020
12406
  this.lastParaAfterPad = 0;
12021
12407
  this.lastParaAfterPad = 0;
@@ -12352,7 +12738,7 @@ var Engine = class {
12352
12738
  const runs = c.type === "run" ? [c] : c.runs;
12353
12739
  for (const r of runs) {
12354
12740
  for (const rc of r.content) {
12355
- if (rc.kind !== "noteRef" || rc.self) continue;
12741
+ if (rc.kind !== "noteRef" || rc.self || rc.customMarkFollows) continue;
12356
12742
  if (rc.noteType === "footnote" && this.doc.footnotes.has(rc.id) && !this.footnoteNumbers.has(rc.id)) {
12357
12743
  this.footnoteNumbers.set(rc.id, fnStart + fn++);
12358
12744
  } else if (rc.noteType === "endnote" && this.doc.endnotes.has(rc.id) && !this.endnoteNumbers.has(rc.id)) {
@@ -12865,6 +13251,20 @@ var Engine = class {
12865
13251
  }
12866
13252
  placeParagraph(para, prev, next, siblings, index) {
12867
13253
  const props = this.doc.effectiveParaProps(para);
13254
+ const detachedFootnotes = customFootnoteAnchorIds(para);
13255
+ if (detachedFootnotes && next?.type === "paragraph") {
13256
+ const nextProps2 = this.doc.effectiveParaProps(next);
13257
+ const nextFrame = nextProps2.frame ? this.resolveFrame(nextProps2.frame) : void 0;
13258
+ if (nextFrame?.wrap === "notBeside" && nextFrame.w !== void 0 && this.cur.colXs.length > 1 && nextFrame.w > this.colWidth + 1) {
13259
+ for (const id of detachedFootnotes) this.registerFootnote(id, this.cur);
13260
+ const markProps = this.doc.effectiveRunProps(para, props.markRunProps ?? {});
13261
+ this.cur.openingColumnReserve = this.measurer.metrics(
13262
+ fontOf(markProps, this.doc.styles.defaultRPr.font ?? "Calibri")
13263
+ ).lineHeight;
13264
+ this.customNoteBannerFit = true;
13265
+ return;
13266
+ }
13267
+ }
12868
13268
  if (this.consumedFrames.has(para)) return;
12869
13269
  if (props.frame && !props.dropCap && this.cur.physIndex !== -1) {
12870
13270
  const fr = this.resolveFrame(props.frame);
@@ -12997,12 +13397,18 @@ var Engine = class {
12997
13397
  if (breakBeforeForced && !(isLeadingPageBreak && keepSpBeforeAtPageTop)) dropSpaceBefore = true;
12998
13398
  if (this.docGridDropBefore) {
12999
13399
  if (props.snapToGrid === false && this.sp.docGridLinePitch) {
13000
- const reduction = 2 * this.sp.docGridLinePitch;
13400
+ const reduction = (props.spacingBefore ?? 0) > 0 ? 4 * this.sp.docGridLinePitch : 2 * this.sp.docGridLinePitch;
13401
+ this.cur.bodyTop -= reduction;
13402
+ this.y -= reduction;
13403
+ } else if (para.props.styleId === "Heading1" && this.sp.docGridLinePitch) {
13404
+ const reduction = 3 * this.sp.docGridLinePitch - ptToPx(1.5);
13001
13405
  this.cur.bodyTop -= reduction;
13002
13406
  this.y -= reduction;
13407
+ dropSpaceBefore = true;
13408
+ } else {
13409
+ dropSpaceBefore = true;
13003
13410
  }
13004
13411
  this.docGridDropBefore = false;
13005
- dropSpaceBefore = true;
13006
13412
  }
13007
13413
  const rawSpacingBefore = dropSpaceBefore ? 0 : props.spacingBefore ?? 0;
13008
13414
  let paraTopEstimate = this.y + rawSpacingBefore;
@@ -13176,7 +13582,7 @@ var Engine = class {
13176
13582
  const nextProps = nextPara ? this.doc.effectiveParaProps(nextPara) : void 0;
13177
13583
  const nextLeadBreak = nextPara ? leadingBreakOf(nextPara) : void 0;
13178
13584
  let legacyBreakChain = false;
13179
- if (lines.length > 0 && this.pageIsEmptyAtCursor() && this.doc.compatibilityMode < 15 && this.sp.docGridType === "default" && props.keepNext && nextPara && nextProps?.keepNext && nextLeadBreak?.type === "page") {
13585
+ if (lines.length > 0 && this.pageIsEmptyAtCursor() && this.doc.compatibilityMode < 15 && this.sp.docGridType === "default" && !this.cur.footerRel && props.keepNext && nextPara && nextProps?.keepNext && nextLeadBreak?.type === "page") {
13180
13586
  const breakProps = this.doc.effectiveRunProps(nextPara, nextLeadBreak.run.props);
13181
13587
  const breakFont = fontOf(breakProps, this.doc.styles.defaultRPr.font ?? "Calibri");
13182
13588
  let breakHeight = this.measurer.metrics(breakFont).lineHeight;
@@ -13347,6 +13753,7 @@ var Engine = class {
13347
13753
  let simCol = this.col;
13348
13754
  let simOnCurrentPage = true;
13349
13755
  let simBannerUsed = this.bannerSlotUsed;
13756
+ const paragraphOverhang = this.customNoteBannerFit ? CUSTOM_NOTE_BANNER_OVERHANG : 0;
13350
13757
  const updateBottom = () => {
13351
13758
  bottom = simOnCurrentPage && this.balanceBottom !== void 0 && simCol + 1 < this.cur.colXs.length ? this.balanceBottom - simBannerUsed : this.cur.bodyBottom - (simOnCurrentPage ? this.footnoteReserve(this.cur, simCol) + simBannerUsed : 0);
13352
13759
  };
@@ -13369,16 +13776,17 @@ var Engine = class {
13369
13776
  }
13370
13777
  };
13371
13778
  const nextSimColumn = () => {
13372
- simBannerUsed = 0;
13373
13779
  simNotes = 0;
13374
13780
  simNoted.clear();
13375
13781
  if (simCol + 1 < this.cur.colXs.length) {
13376
13782
  simCol++;
13377
13783
  simY = simOnCurrentPage ? this.columnStartY(simCol) : this.cur.bodyTop;
13784
+ simBannerUsed = simOnCurrentPage && this.cur.bannerTop !== void 0 ? this.cur.openingColumnReserve ?? 0 : 0;
13378
13785
  } else {
13379
13786
  simCol = 0;
13380
13787
  simOnCurrentPage = false;
13381
13788
  simY = this.cur.bodyTop;
13789
+ simBannerUsed = 0;
13382
13790
  }
13383
13791
  updateBottom();
13384
13792
  };
@@ -13397,7 +13805,7 @@ var Engine = class {
13397
13805
  const noteAdd = lineNoteHeights(lines[li]);
13398
13806
  const baseNotes = simOnCurrentPage ? this.cur.footnoteH[simCol] ?? 0 : 0;
13399
13807
  const simSep = simNotes + noteAdd > 0 && baseNotes === 0 ? this.noteSeparatorReserve(this.cur) : 0;
13400
- const overflowsHere = !postTablePageBreak && (simBalancing ? simY > bottom + 0.01 : simY + lines[li].fitHeight + (li === lines.length - 1 ? keepNextTail : 0) > bottom - simNotes - noteAdd - simSep + 0.01);
13808
+ const overflowsHere = !postTablePageBreak && (simBalancing ? simY > bottom + 0.01 : simY + lines[li].fitHeight + (li === lines.length - 1 ? keepNextTail : 0) > bottom - simNotes - noteAdd - simSep + paragraphOverhang + 0.01);
13401
13809
  if (overflowsHere && li === segStart && segStart === 0 && onPartialPage && !simBalancing) {
13402
13810
  breaks2.add(0);
13403
13811
  segStart = 0;
@@ -13483,7 +13891,7 @@ var Engine = class {
13483
13891
  const pendingNotes = this.pendingNoteHeight(line2);
13484
13892
  const balancing = this.balanceBottom !== void 0 && this.col + 1 < this.cur.colXs.length;
13485
13893
  const balBottomBased = balancing && this.colWidth < 40;
13486
- const overflow = !postTablePageBreak && (balancing ? (balBottomBased ? this.y + line2.fitHeight : this.y) > this.bodyBottom + 0.01 : this.y + line2.fitHeight > this.bodyBottom - pendingNotes + 0.01) && !this.pageIsEmptyAtCursor();
13894
+ const overflow = !postTablePageBreak && (balancing ? (balBottomBased ? this.y + line2.fitHeight : this.y) > this.bodyBottom + 0.01 : this.y + line2.fitHeight > this.bodyBottom - pendingNotes + (this.customNoteBannerFit ? CUSTOM_NOTE_BANNER_OVERHANG : 0) + 0.01) && !this.pageIsEmptyAtCursor();
13487
13895
  if ((planned || overflow) && li > fragStartLine) {
13488
13896
  closeFragment(li, false);
13489
13897
  this.nextColumn();
@@ -13523,6 +13931,7 @@ var Engine = class {
13523
13931
  for (const bm of para.bookmarks) {
13524
13932
  if (!this.bookmarkPages.has(bm)) {
13525
13933
  this.bookmarkPages.set(bm, formatNumber(pg.displayNumber, PAGE_FMT[pg.sp.pageNumberFormat ?? "decimal"] ?? "decimal"));
13934
+ this.bookmarkPageIndices.set(bm, pg.physIndex - 1);
13526
13935
  }
13527
13936
  }
13528
13937
  }
@@ -14150,13 +14559,35 @@ var Engine = class {
14150
14559
  y = Math.max(y, pendingPageFrame.bottom);
14151
14560
  pendingPageFrame = null;
14152
14561
  }
14153
- y = this.layoutTableInFrame(block, fake, 0, y, width, fields);
14562
+ y = this.layoutTableInFrame(block, fake, 0, y, width, fields, inCell === true);
14154
14563
  framePrevAfter = 0;
14155
14564
  }
14156
14565
  }
14157
14566
  if (pendingPageFrame) y = Math.max(y, pendingPageFrame.bottom);
14567
+ const itemBottom = (item) => {
14568
+ if (item.kind === "text") return (item.glyphTop ?? item.lineTop) + item.lineHeight;
14569
+ if (item.kind === "rect" || item.kind === "image" || item.kind === "path" || item.kind === "drawingHit") {
14570
+ return item.y + item.height;
14571
+ }
14572
+ if (item.kind === "edge") return Math.max(item.y1, item.y2);
14573
+ if (item.kind === "wordart" || item.kind === "warptext") return item.y + item.height;
14574
+ return 0;
14575
+ };
14576
+ const headerBandBottom = this.sp.marginTop - (origin?.y ?? 0);
14577
+ const paintedHeaderBottom = items.reduce((bottom, item) => {
14578
+ const itemEnd = itemBottom(item);
14579
+ return "behind" in item && item.behind && itemEnd <= headerBandBottom + 0.01 ? Math.max(bottom, itemEnd) : bottom;
14580
+ }, 0);
14581
+ const wrappedBottom = (this.floats.get(fake) ?? []).reduce(
14582
+ (bottom, float) => Math.max(bottom, float.y1 - (origin?.y ?? 0)),
14583
+ 0
14584
+ );
14158
14585
  this.floats.delete(fake);
14159
- return { items, height: y + pageFramePhantomH };
14586
+ return {
14587
+ items,
14588
+ height: y + pageFramePhantomH,
14589
+ contentBottom: Math.max(y + pageFramePhantomH, paintedHeaderBottom, wrappedBottom)
14590
+ };
14160
14591
  }
14161
14592
  /**
14162
14593
  * Emit floating shapes anchored at (textX, textY). Coordinates in the shape
@@ -14562,7 +14993,7 @@ var Engine = class {
14562
14993
  page.items.push(it);
14563
14994
  }
14564
14995
  }
14565
- measureHeaderFooter(hf, page, contentWidth, overlayPageFrame = false) {
14996
+ measureHeaderFooter(hf, page, contentWidth, overlayPageFrame = false, reserveBodyClearance = false) {
14566
14997
  if (!hf || hf.blocks.length === 0) return 0;
14567
14998
  const fields = {
14568
14999
  pageNumber: () => page.displayNumber,
@@ -14571,10 +15002,25 @@ var Engine = class {
14571
15002
  };
14572
15003
  const snapshot = new Map(Array.from(this.counters, ([k, v]) => [k, [...v]]));
14573
15004
  const seenSnapshot = new Set(this.seenNumIds);
14574
- const { height } = this.layoutFrame(hf.blocks, contentWidth, fields, void 0, false, overlayPageFrame);
15005
+ const origin = reserveBodyClearance ? { x: page.sp.marginLeft + page.sp.gutter, y: page.sp.headerDistance } : void 0;
15006
+ const { height, contentBottom } = this.layoutFrame(
15007
+ hf.blocks,
15008
+ contentWidth,
15009
+ fields,
15010
+ origin,
15011
+ false,
15012
+ overlayPageFrame
15013
+ );
14575
15014
  this.counters = snapshot;
14576
15015
  this.seenNumIds = seenSnapshot;
14577
- return height;
15016
+ const anchors = hf.blocks.flatMap(
15017
+ (block) => block.type === "paragraph" ? this.collectAnchors(block) : []
15018
+ );
15019
+ const hasOnlyUnwrappedAnchors = anchors.length > 0 && anchors.every(
15020
+ (shape) => !("wrap" in shape) || shape.wrap === void 0 || shape.wrap === "none"
15021
+ );
15022
+ const complexHeader = anchors.length > 0 || hf.blocks.some((block) => block.type === "table");
15023
+ return reserveBodyClearance ? Math.max(height, contentBottom) + (!hasOnlyUnwrappedAnchors && complexHeader ? ptToPx(22.5) : 0) : height;
14578
15024
  }
14579
15025
  pageFieldFrameOverlay(hf) {
14580
15026
  if (!hf) return false;
@@ -14636,7 +15082,10 @@ var Engine = class {
14636
15082
  const measured = this.layoutFrame(footer.blocks, contentWidth, fields, void 0, false, overlayPageFrame);
14637
15083
  this.counters = snapshot;
14638
15084
  this.seenNumIds = seenSnapshot;
14639
- const top = sp.pageHeight - sp.footerDistance - measured.height;
15085
+ const legacyPageFrame = this.doc.compatibilityMode < 15 && footer.blocks.some(
15086
+ (block) => block.type === "paragraph" && this.doc.effectiveParaProps(block).frame?.vAnchor === "page"
15087
+ );
15088
+ const top = sp.pageHeight - sp.footerDistance - (legacyPageFrame ? 0 : measured.height);
14640
15089
  snapshot = new Map(Array.from(this.counters, ([k, v]) => [k, [...v]]));
14641
15090
  seenSnapshot = new Set(this.seenNumIds);
14642
15091
  this.hfMarginVTop = page.bodyTop;
@@ -15015,7 +15464,14 @@ var Engine = class {
15015
15464
  if (cell.props.margins?.top !== void 0) topPad = Math.max(topPad, cell.props.margins.top);
15016
15465
  if (cell.props.margins?.bottom !== void 0) bottomPad = Math.max(bottomPad, cell.props.margins.bottom);
15017
15466
  }
15018
- if (row.props.heightRule === "exact") return trHeight + topPad;
15467
+ if (row.props.heightRule === "exact") {
15468
+ return this.doc.compatibilityMode < 15 ? trHeight : trHeight + topPad;
15469
+ }
15470
+ if (this.doc.compatibilityMode < 15) {
15471
+ const legacyBottomPad = bottomPad >= ptToPx(1) ? bottomPad : 0;
15472
+ const legacyTopPad = row.props.heightRule === "atLeast" && (row.props.height ?? 0) >= ptToPx(30) ? Math.max(topPad - ptToPx(0.25), 0) : topPad;
15473
+ return Math.max(contentHeight, trHeight + legacyTopPad + legacyBottomPad);
15474
+ }
15019
15475
  const borderPad = this.rowBorderShare(tbl, ri);
15020
15476
  return Math.max(contentHeight, trHeight + topPad + bottomPad + borderPad);
15021
15477
  }
@@ -15235,7 +15691,7 @@ var Engine = class {
15235
15691
  };
15236
15692
  let guard = 0;
15237
15693
  const noteReserve = this.rowNoteHeight(laid) + this.footnoteReserve(this.cur, this.col);
15238
- const overhang = noteReserve > 0 || row.props.cantSplit || row.props.heightRule === "exact" ? 0 : ROW_OVERHANG_TOL;
15694
+ const overhang = noteReserve > 0 || row.props.heightRule === "exact" ? 0 : row.props.cantSplit ? this.doc.compatibilityMode < 15 && (row.props.heightRule !== "atLeast" || (row.props.height ?? 0) >= ptToPx(2)) ? this.cur.footerHeight : 0 : ROW_OVERHANG_TOL;
15239
15695
  while (this.y + rowHeight > this.bodyBottom - this.rowNoteHeight(laid) + overhang + 0.01 && guard++ < 50) {
15240
15696
  const atColumnTop = this.pageIsEmptyAtCursor() || this.y <= this.cur.bodyTop + this.rowBorderWidths(tbl, ri).top / 2 + 0.01;
15241
15697
  const cantSplitHolds = row.props.cantSplit === true && (rowHeight <= this.cur.bodyBottom - this.cur.bodyTop + 0.01 || !atColumnTop);
@@ -15454,9 +15910,9 @@ var Engine = class {
15454
15910
  });
15455
15911
  }
15456
15912
  }
15457
- layoutTableInFrame(tbl, fake, x0, y, width, fields) {
15913
+ layoutTableInFrame(tbl, fake, x0, y, width, fields, nested) {
15458
15914
  this.ensureTableBorders(tbl);
15459
- const widths = this.resolveGridWidths(tbl, width, true);
15915
+ const widths = this.resolveGridWidths(tbl, width, nested);
15460
15916
  const saveY = this.y;
15461
15917
  const saveCur = this.cur;
15462
15918
  const saveCol = this.col;
@@ -15722,6 +16178,7 @@ var Engine = class {
15722
16178
  }
15723
16179
  let maxH = 0;
15724
16180
  let measuredContentH = 0;
16181
+ const effectiveBottomPad = (bottom) => this.doc.compatibilityMode < 15 && row.props.heightRule === "atLeast" && (row.props.height ?? 0) < ptToPx(2) && (bottom ?? 0) >= ptToPx(2) ? (bottom ?? 0) / 2 : bottom ?? 0;
15725
16182
  for (let ci = 0; ci < row.cells.length; ci++) {
15726
16183
  const cell = row.cells[ci];
15727
16184
  const { x, width: w, margins: m } = geometry[ci];
@@ -15741,7 +16198,7 @@ var Engine = class {
15741
16198
  true
15742
16199
  );
15743
16200
  for (const it of items) offsetItem(it, m.left ?? 0, m.top ?? 0);
15744
- const cellHeight = height + (m.top ?? 0) + (m.bottom ?? 0);
16201
+ const cellHeight = height + (m.top ?? 0) + effectiveBottomPad(m.bottom);
15745
16202
  cells[ci] = { items, height: cellHeight, x, width: w, cellIdx: ci };
15746
16203
  measuredContentH = Math.max(measuredContentH, height);
15747
16204
  maxH = Math.max(maxH, cellHeight);
@@ -15785,7 +16242,7 @@ var Engine = class {
15785
16242
  it.rotate = { deg, ox: centerX - it.x, oy: centerY - top };
15786
16243
  }
15787
16244
  }
15788
- const cellHeight = frameWidth + (m.top ?? 0) + (m.bottom ?? 0);
16245
+ const cellHeight = frameWidth + (m.top ?? 0) + effectiveBottomPad(m.bottom);
15789
16246
  cells[ci] = { items, height: cellHeight, x, width: w, cellIdx: ci, rotated: true };
15790
16247
  maxH = Math.max(maxH, cellHeight);
15791
16248
  }
@@ -15934,6 +16391,24 @@ var Engine = class {
15934
16391
  }
15935
16392
  }
15936
16393
  };
16394
+ function customFootnoteAnchorIds(p) {
16395
+ const ids = [];
16396
+ for (const child4 of p.children) {
16397
+ const runs = child4.type === "run" ? [child4] : child4.runs;
16398
+ for (const run of runs) {
16399
+ for (const content of run.content) {
16400
+ if (content.kind === "text") {
16401
+ if (content.text.trim().length > 0) return void 0;
16402
+ } else if (content.kind === "noteRef" && content.noteType === "footnote" && !content.self && content.customMarkFollows) {
16403
+ ids.push(content.id);
16404
+ } else {
16405
+ return void 0;
16406
+ }
16407
+ }
16408
+ }
16409
+ }
16410
+ return ids.length > 0 ? ids : void 0;
16411
+ }
15937
16412
  function isEmptyParagraph(p) {
15938
16413
  for (const child4 of p.children) {
15939
16414
  const runs = child4.type === "run" ? [child4] : child4.runs;
@@ -16905,7 +17380,7 @@ function renderToDom(doc, layout, container, options = {}, prev) {
16905
17380
  renderComments(doc, root, handle.bindings, zoom, options.onDeleteComment, options.onReplyComment);
16906
17381
  }
16907
17382
  };
16908
- const wantedPages = () => {
17383
+ const wantedPages = (overscan = 2) => {
16909
17384
  const wanted = /* @__PURE__ */ new Set();
16910
17385
  const top = container.scrollTop;
16911
17386
  const bottom = top + Math.max(container.clientHeight, 1);
@@ -16921,15 +17396,15 @@ function renderToDom(doc, layout, container, options = {}, prev) {
16921
17396
  y = pageBottom + gap;
16922
17397
  }
16923
17398
  if (first < 0) first = last = Math.max(0, pages.length - 1);
16924
- for (let i = Math.max(0, first - 2); i <= Math.min(pages.length - 1, last + 2); i++) wanted.add(i);
17399
+ for (let i = Math.max(0, first - overscan); i <= Math.min(pages.length - 1, last + overscan); i++) wanted.add(i);
16925
17400
  for (let i = 0; i < pages.length; i++) {
16926
17401
  if (pages[i].mounted && pages[i].el.querySelector("[data-dxw-caret], .dxw-sel")) wanted.add(i);
16927
17402
  }
16928
17403
  return wanted;
16929
17404
  };
16930
- const syncViewport = (notify, force = false) => {
17405
+ const syncViewport = (notify, force = false, overscan = 2) => {
16931
17406
  if (!virtualized) return;
16932
- const wanted = wantedPages();
17407
+ const wanted = wantedPages(overscan);
16933
17408
  let changed = false;
16934
17409
  for (const i of wanted) {
16935
17410
  if (pages[i].mounted) continue;
@@ -16944,14 +17419,30 @@ function renderToDom(doc, layout, container, options = {}, prev) {
16944
17419
  }
16945
17420
  if (!changed && !force) return;
16946
17421
  refreshBindings();
16947
- redrawComments();
17422
+ if (drawComments || force) redrawComments();
16948
17423
  if (notify) options.onViewportChange?.();
16949
17424
  };
17425
+ let settleTimer;
17426
+ let syncedScrollTop = container.scrollTop;
17427
+ let pendingOverscan = 2;
16950
17428
  const scheduleViewport = () => {
17429
+ const fast = Math.abs(container.scrollTop - syncedScrollTop) > Math.max(container.clientHeight, 1) * 2;
17430
+ if (fast) {
17431
+ pendingOverscan = 0;
17432
+ if (settleTimer !== void 0) clearTimeout(settleTimer);
17433
+ settleTimer = setTimeout(() => {
17434
+ settleTimer = void 0;
17435
+ syncedScrollTop = container.scrollTop;
17436
+ syncViewport(true);
17437
+ }, 80);
17438
+ }
16951
17439
  if (frame) return;
16952
17440
  frame = requestAnimationFrame(() => {
16953
17441
  frame = 0;
16954
- syncViewport(true);
17442
+ const overscan = pendingOverscan;
17443
+ pendingOverscan = 2;
17444
+ syncedScrollTop = container.scrollTop;
17445
+ syncViewport(true, false, overscan);
16955
17446
  });
16956
17447
  };
16957
17448
  const stopVirtualizer = () => {
@@ -16960,6 +17451,8 @@ function renderToDom(doc, layout, container, options = {}, prev) {
16960
17451
  window.removeEventListener("resize", scheduleViewport);
16961
17452
  if (frame) cancelAnimationFrame(frame);
16962
17453
  frame = 0;
17454
+ if (settleTimer !== void 0) clearTimeout(settleTimer);
17455
+ settleTimer = void 0;
16963
17456
  };
16964
17457
  handle = {
16965
17458
  root,
@@ -17683,6 +18176,11 @@ function renderItem(doc, item, urls) {
17683
18176
  hit.dataset.dxwDrawing = "1";
17684
18177
  if (item.ink) hit.dataset.dxwInk = "1";
17685
18178
  if (item.smartArt) hit.dataset.dxwSmartArt = "1";
18179
+ if (item.height < 12) {
18180
+ const target = document.createElement("div");
18181
+ target.style.cssText = `position:absolute;left:0;top:${-(12 - item.height) / 2}px;width:100%;height:12px;cursor:move;`;
18182
+ hit.appendChild(target);
18183
+ }
17686
18184
  return hit;
17687
18185
  }
17688
18186
  case "edge": {
@@ -18142,6 +18640,10 @@ function renderText(item) {
18142
18640
  el16.style.alignItems = "flex-end";
18143
18641
  el16.style.whiteSpace = "pre";
18144
18642
  el16.style.font = cssFont(item.font);
18643
+ if (item.font.paintDY) {
18644
+ const transform = el16.style.transform;
18645
+ el16.style.transform = `${transform ? `${transform} ` : ""}translateY(${item.font.paintDY}px)`;
18646
+ }
18145
18647
  }
18146
18648
  if (item.rtl) {
18147
18649
  el16.style.direction = "rtl";
@@ -18205,7 +18707,8 @@ function renderEdge(x1, y1, x2, y2, border, rotate) {
18205
18707
  }
18206
18708
  const horizontal = Math.abs(y2 - y1) < 0.01;
18207
18709
  const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
18208
- const w = Math.max(1 / dpr, Math.round(border.width * dpr) / dpr);
18710
+ const declaredWidth = border.rawWidth !== void 0 && border.rawWidth < 0.25 ? border.rawWidth : border.width;
18711
+ const w = Math.max(1 / dpr, Math.round(declaredWidth * dpr) / dpr);
18209
18712
  const snap = (v) => Math.round(v * dpr) / dpr;
18210
18713
  const place = border.space === 0 ? snap : (v) => v;
18211
18714
  const dashPattern = border.style === "dashed" || border.style === "dotDash" || border.style === "dotDotDash" ? [3, 1] : border.style === "dotted" ? [1, 1] : null;
@@ -18475,6 +18978,10 @@ function mergeParagraphBackward(doc, pEl) {
18475
18978
  if (idx <= 0) return false;
18476
18979
  const prev = parent.children[idx - 1];
18477
18980
  if (localName(prev.name) !== "p") return false;
18981
+ const finishMerge = (survivor) => {
18982
+ if (!doc.reparseDirectBodyParagraphMerge(prev, pEl, survivor)) doc.refresh();
18983
+ return true;
18984
+ };
18478
18985
  const visible = [];
18479
18986
  const collect = (el16) => {
18480
18987
  for (const c of el16.children) {
@@ -18492,6 +18999,7 @@ function mergeParagraphBackward(doc, pEl) {
18492
18999
  if (type === "page" || type === "column") {
18493
19000
  if (visible.length === 1) {
18494
19001
  parent.children.splice(idx - 1, 1);
19002
+ return finishMerge(pEl);
18495
19003
  } else {
18496
19004
  last.parent.children.splice(last.parent.children.indexOf(last.el), 1);
18497
19005
  }
@@ -18501,14 +19009,12 @@ function mergeParagraphBackward(doc, pEl) {
18501
19009
  }
18502
19010
  if (visible.length === 0) {
18503
19011
  parent.children.splice(idx - 1, 1);
18504
- doc.refresh();
18505
- return true;
19012
+ return finishMerge(pEl);
18506
19013
  }
18507
19014
  const moved = pEl.children.filter((c) => localName(c.name) !== "pPr");
18508
19015
  prev.children.push(...moved);
18509
19016
  parent.children.splice(idx, 1);
18510
- doc.refresh();
18511
- return true;
19017
+ return finishMerge(prev);
18512
19018
  }
18513
19019
  function firstTextOf(pEl) {
18514
19020
  const walk2 = (el16) => {
@@ -20423,23 +20929,44 @@ function insertShapeAt(doc, caretT, preset, text = "") {
20423
20929
  if (!caretRun || !parent || localName(caretRun.name) !== "r") return null;
20424
20930
  const w = prefixOf8(caretRun);
20425
20931
  const id = String(doc.nextDrawingId());
20426
- const width = preset === "textBox" ? 240 : 192;
20427
- const height = preset === "textBox" ? 72 : 96;
20932
+ const isLine = preset === "line";
20933
+ const width = isLine ? 240 : preset === "textBox" ? 240 : 192;
20934
+ const height = isLine ? 2 : preset === "textBox" ? 72 : 96;
20428
20935
  const cx = String(Math.round(width * EMU_PER_PX4));
20429
20936
  const cy = String(Math.round(height * EMU_PER_PX4));
20430
20937
  const geometry = preset === "roundedRectangle" ? "roundRect" : preset === "rectangle" || preset === "textBox" ? "rect" : preset;
20431
20938
  const isTextBox = preset === "textBox";
20432
- const shapeName = isTextBox ? `Text Box ${id}` : `Shape ${id}`;
20939
+ const shapeName = isLine ? `Line ${id}` : isTextBox ? `Text Box ${id}` : `Shape ${id}`;
20433
20940
  const spPr = [
20434
- el6("a:xfrm", {}, [el6("a:off", { x: "0", y: "0" }), el6("a:ext", { cx, cy })]),
20941
+ el6("a:xfrm", {}, [
20942
+ el6("a:off", { x: "0", y: "0" }),
20943
+ el6("a:ext", { cx, cy })
20944
+ ]),
20435
20945
  el6("a:prstGeom", { prst: geometry }, [el6("a:avLst")]),
20436
- isTextBox ? el6("a:noFill") : el6("a:solidFill", {}, [el6("a:srgbClr", { val: "4472C4" })]),
20437
- el6("a:ln", { w: "12700" }, [el6("a:solidFill", {}, [el6("a:srgbClr", { val: isTextBox ? "404040" : "2F5597" })])])
20946
+ isTextBox || isLine ? el6("a:noFill") : el6("a:solidFill", {}, [el6("a:srgbClr", { val: "4472C4" })]),
20947
+ el6("a:ln", { w: "12700" }, [el6("a:solidFill", {}, [el6("a:srgbClr", { val: isTextBox || isLine ? "404040" : "2F5597" })])])
20438
20948
  ];
20439
20949
  const textRun = el6(`${w}r`, {}, [
20440
20950
  el6(`${w}rPr`, {}, [el6(`${w}color`, { [`${w}val`]: isTextBox ? "202124" : "FFFFFF" }), el6(`${w}sz`, { [`${w}val`]: "22" })]),
20441
20951
  el6(`${w}t`, { "xml:space": "preserve" }, [], text)
20442
20952
  ]);
20953
+ const shapeChildren = isLine ? [
20954
+ el6("wps:cNvCnPr", {}, [el6("a:cxnSpLocks", { noChangeShapeType: "1" })]),
20955
+ el6("wps:spPr", { bwMode: "auto" }, spPr),
20956
+ el6("wps:bodyPr")
20957
+ ] : [
20958
+ el6("wps:cNvSpPr", isTextBox ? { txBox: "1" } : {}),
20959
+ el6("wps:spPr", {}, spPr),
20960
+ el6("wps:txbx", {}, [
20961
+ el6(`${w}txbxContent`, {}, [
20962
+ el6(`${w}p`, {}, [
20963
+ el6(`${w}pPr`, {}, [el6(`${w}jc`, { [`${w}val`]: isTextBox ? "left" : "center" })]),
20964
+ textRun
20965
+ ])
20966
+ ])
20967
+ ]),
20968
+ el6("wps:bodyPr", { rot: "0", anchor: isTextBox ? "t" : "ctr" }, [el6("a:noAutofit")])
20969
+ ];
20443
20970
  const drawing = el6(`${w}drawing`, {}, [
20444
20971
  el6("wp:anchor", {
20445
20972
  "xmlns:wp": NS_WP3,
@@ -20459,24 +20986,12 @@ function insertShapeAt(doc, caretT, preset, text = "") {
20459
20986
  el6("wp:positionV", { relativeFrom: "paragraph" }, [el6("wp:posOffset", {}, [], "0")]),
20460
20987
  el6("wp:extent", { cx, cy }),
20461
20988
  el6("wp:effectExtent", { l: "0", t: "0", r: "0", b: "0" }),
20462
- el6("wp:wrapSquare", { wrapText: "bothSides" }),
20989
+ isLine ? el6("wp:wrapNone") : el6("wp:wrapSquare", { wrapText: "bothSides" }),
20463
20990
  el6("wp:docPr", { id, name: shapeName }),
20464
20991
  el6("wp:cNvGraphicFramePr"),
20465
20992
  el6("a:graphic", { "xmlns:a": NS_A3 }, [
20466
20993
  el6("a:graphicData", { uri: NS_WPS }, [
20467
- el6("wps:wsp", { "xmlns:wps": NS_WPS }, [
20468
- el6("wps:cNvSpPr", { txBox: "1" }),
20469
- el6("wps:spPr", {}, spPr),
20470
- el6("wps:txbx", {}, [
20471
- el6(`${w}txbxContent`, {}, [
20472
- el6(`${w}p`, {}, [
20473
- el6(`${w}pPr`, {}, [el6(`${w}jc`, { [`${w}val`]: isTextBox ? "left" : "center" })]),
20474
- textRun
20475
- ])
20476
- ])
20477
- ]),
20478
- el6("wps:bodyPr", { rot: "0", anchor: isTextBox ? "t" : "ctr" }, [el6("a:noAutofit")])
20479
- ])
20994
+ el6("wps:wsp", { "xmlns:wps": NS_WPS }, shapeChildren)
20480
20995
  ])
20481
20996
  ])
20482
20997
  ])
@@ -20538,7 +21053,26 @@ function setDrawingWordArtText(doc, drawing, text) {
20538
21053
  doc.refresh();
20539
21054
  return true;
20540
21055
  }
20541
- function insertInkAt(doc, caretT, points, color = "#202124", widthPx = 2, opacity = 1) {
21056
+ function drawingLineStyle(drawing) {
21057
+ const line2 = descendant3(drawing, "ln");
21058
+ if (!line2) return null;
21059
+ const color = descendant3(line2, "srgbClr")?.attrs.val ?? "000000";
21060
+ const width = Math.max((parseInt(line2.attrs.w ?? "0", 10) || 0) / EMU_PER_PX4, 0.75);
21061
+ return { color: `#${color.toUpperCase()}`, width };
21062
+ }
21063
+ function setDrawingLineStyle(doc, drawing, color, widthPx) {
21064
+ const line2 = descendant3(drawing, "ln");
21065
+ if (!line2) return false;
21066
+ line2.attrs.w = String(Math.max(1, Math.round(widthPx * EMU_PER_PX4)));
21067
+ line2.children = line2.children.filter((child4) => {
21068
+ const name = localName(child4.name);
21069
+ return name !== "solidFill" && name !== "noFill";
21070
+ });
21071
+ line2.children.unshift(el6("a:solidFill", {}, [el6("a:srgbClr", { val: color.replace(/^#/, "").toUpperCase() })]));
21072
+ doc.refresh();
21073
+ return true;
21074
+ }
21075
+ function insertInkAt(doc, caretT, points, color = "#202124", widthPx = 2, opacity = 1, refreshModel = true) {
20542
21076
  if (points.length < 2) return null;
20543
21077
  const caretRun = doc.findParentOf(caretT);
20544
21078
  const parent = caretRun && doc.findParentOf(caretRun);
@@ -20615,7 +21149,7 @@ function insertInkAt(doc, caretT, points, color = "#202124", widthPx = 2, opacit
20615
21149
  ])
20616
21150
  ]);
20617
21151
  parent.children.splice(parent.children.indexOf(caretRun) + 1, 0, el6(`${w}r`, {}, [drawing]));
20618
- doc.refresh();
21152
+ if (refreshModel) doc.refresh();
20619
21153
  return drawing;
20620
21154
  }
20621
21155
 
@@ -20946,6 +21480,111 @@ function deleteWatermark(doc, shapeEl) {
20946
21480
  return true;
20947
21481
  }
20948
21482
 
21483
+ // ../core/src/edit/dialog.ts
21484
+ var dialogSequence = 0;
21485
+ function requestTextInputDialog(anchor, options) {
21486
+ const doc = anchor.ownerDocument;
21487
+ const win = doc.defaultView;
21488
+ return new Promise((resolve) => {
21489
+ const id = `dxw-input-dialog-${++dialogSequence}`;
21490
+ const previousFocus = doc.activeElement instanceof HTMLElement ? doc.activeElement : null;
21491
+ const backdrop = doc.createElement("div");
21492
+ backdrop.className = "dxw-input-dialog-backdrop";
21493
+ backdrop.dataset.dxwInputDialog = "";
21494
+ backdrop.style.cssText = "position:fixed;inset:0;z-index:var(--dxw-dialog-z-index,1000);display:grid;place-items:center;padding:16px;box-sizing:border-box;background:var(--dxw-dialog-backdrop,rgba(32,33,36,.38));";
21495
+ if (win) {
21496
+ const computed = win.getComputedStyle(anchor);
21497
+ for (const property of [
21498
+ "--dxw-toolbar-fg",
21499
+ "--dxw-toolbar-border",
21500
+ "--dxw-toolbar-muted",
21501
+ "--dxw-accent",
21502
+ "--dxw-accent-fg",
21503
+ "--dxw-popover-bg",
21504
+ "--dxw-popover-shadow",
21505
+ "--dxw-dialog-backdrop",
21506
+ "--dxw-dialog-z-index",
21507
+ "--dxw-dialog-width",
21508
+ "--dxw-dialog-radius",
21509
+ "--dxw-dialog-field-bg"
21510
+ ]) {
21511
+ const value = computed.getPropertyValue(property);
21512
+ if (value) backdrop.style.setProperty(property, value);
21513
+ }
21514
+ }
21515
+ const form = doc.createElement("form");
21516
+ form.className = "dxw-input-dialog";
21517
+ form.setAttribute("role", "dialog");
21518
+ form.setAttribute("aria-modal", "true");
21519
+ form.setAttribute("aria-labelledby", id);
21520
+ form.style.cssText = "width:min(var(--dxw-dialog-width,420px),calc(100vw - 32px));box-sizing:border-box;display:grid;gap:10px;padding:16px;border:1px solid var(--dxw-toolbar-border,#dadce0);border-radius:var(--dxw-dialog-radius,10px);color:var(--dxw-toolbar-fg,#3c4043);background:var(--dxw-popover-bg,#fff);box-shadow:var(--dxw-popover-shadow,0 8px 28px rgba(0,0,0,.24));font:13px system-ui,sans-serif;";
21521
+ const title = doc.createElement("strong");
21522
+ title.id = id;
21523
+ title.className = "dxw-input-dialog-title";
21524
+ title.textContent = options.title;
21525
+ title.style.cssText = "font-size:15px;line-height:1.35;";
21526
+ form.appendChild(title);
21527
+ const label = doc.createElement("label");
21528
+ label.className = "dxw-input-dialog-label";
21529
+ label.textContent = options.label ?? options.title;
21530
+ label.style.cssText = "display:grid;gap:5px;color:var(--dxw-toolbar-muted,#5f6368);font-size:12px;";
21531
+ const field = options.multiline ? doc.createElement("textarea") : doc.createElement("input");
21532
+ field.className = "dxw-input-dialog-field";
21533
+ field.value = options.value ?? "";
21534
+ field.placeholder = options.placeholder ?? "";
21535
+ field.setAttribute("aria-label", options.label ?? options.title);
21536
+ if (field instanceof HTMLTextAreaElement) field.rows = 4;
21537
+ field.style.cssText = "width:100%;min-height:34px;box-sizing:border-box;padding:7px 9px;border:1px solid var(--dxw-toolbar-border,#dadce0);border-radius:6px;outline:none;color:var(--dxw-toolbar-fg,#3c4043);background:var(--dxw-dialog-field-bg,var(--dxw-popover-bg,#fff));font:13px system-ui,sans-serif;resize:vertical;";
21538
+ label.appendChild(field);
21539
+ form.appendChild(label);
21540
+ const actions = doc.createElement("div");
21541
+ actions.className = "dxw-input-dialog-actions";
21542
+ actions.style.cssText = "display:flex;justify-content:flex-end;gap:7px;";
21543
+ const cancel = doc.createElement("button");
21544
+ cancel.type = "button";
21545
+ cancel.className = "dxw-input-dialog-cancel";
21546
+ cancel.textContent = "Cancel";
21547
+ cancel.style.cssText = "min-height:32px;padding:0 12px;border:1px solid var(--dxw-toolbar-border,#dadce0);border-radius:7px;color:var(--dxw-toolbar-fg,#3c4043);background:var(--dxw-popover-bg,#fff);cursor:pointer;font:600 12px system-ui,sans-serif;";
21548
+ const submit = doc.createElement("button");
21549
+ submit.type = "submit";
21550
+ submit.className = "dxw-input-dialog-submit";
21551
+ submit.textContent = options.submitLabel ?? "Apply";
21552
+ submit.style.cssText = "min-height:32px;padding:0 14px;border:1px solid transparent;border-radius:7px;color:var(--dxw-accent-fg,#fff);background:var(--dxw-accent,#1a73e8);cursor:pointer;font:600 12px system-ui,sans-serif;";
21553
+ actions.append(cancel, submit);
21554
+ form.appendChild(actions);
21555
+ backdrop.appendChild(form);
21556
+ doc.body.appendChild(backdrop);
21557
+ let finished = false;
21558
+ const finish = (value) => {
21559
+ if (finished) return;
21560
+ finished = true;
21561
+ doc.removeEventListener("keydown", onKeyDown, true);
21562
+ backdrop.remove();
21563
+ previousFocus?.focus({ preventScroll: true });
21564
+ resolve(value);
21565
+ };
21566
+ const onKeyDown = (event) => {
21567
+ if (event.key === "Escape") {
21568
+ event.preventDefault();
21569
+ finish(null);
21570
+ }
21571
+ };
21572
+ doc.addEventListener("keydown", onKeyDown, true);
21573
+ backdrop.addEventListener("mousedown", (event) => {
21574
+ if (event.target === backdrop) finish(null);
21575
+ });
21576
+ cancel.addEventListener("click", () => finish(null));
21577
+ form.addEventListener("submit", (event) => {
21578
+ event.preventDefault();
21579
+ finish(field.value);
21580
+ });
21581
+ requestAnimationFrame(() => {
21582
+ field.focus({ preventScroll: true });
21583
+ field.select();
21584
+ });
21585
+ });
21586
+ }
21587
+
20949
21588
  // ../core/src/edit/grapheme.ts
20950
21589
  var segmenter = null;
20951
21590
  var segmenterTried = false;
@@ -21651,10 +22290,15 @@ var _DocxEditor = class _DocxEditor {
21651
22290
  this.selection = null;
21652
22291
  this.selectionRects = [];
21653
22292
  this.selectedAll = false;
21654
- /** A click-and-type placement below a float has already recorded the undo
22293
+ /** Header/footer membership for hit-testing and caret movement. Rebuilt only
22294
+ * when refresh() replaces the parsed model; header/footer trees are tiny. */
22295
+ this.regionModelVersion = -1;
22296
+ this.headerFooterElements = /* @__PURE__ */ new WeakSet();
22297
+ /** A click-and-type placement in body whitespace has already recorded the undo
21655
22298
  * snapshot that the following text insertion belongs to. */
21656
22299
  this.pendingClickTypeCheckpoint = false;
21657
22300
  this.clickTypeCheckpointUntil = 0;
22301
+ this.textContextMenu = null;
21658
22302
  /** True while a drag-selection is in progress (checked by onMouseUp, which
21659
22303
  * bubbles from the container BEFORE the document-level drag-end listener). */
21660
22304
  this.dragSelecting = false;
@@ -22009,7 +22653,7 @@ var _DocxEditor = class _DocxEditor {
22009
22653
  this.hfPage = null;
22010
22654
  }
22011
22655
  if (caret && region === "body" && !onGlyph && e.detail === 1 && !e.shiftKey) {
22012
- caret = this.extendCaretBelowFloatingTable(caret, e.clientX, e.clientY) ?? caret;
22656
+ caret = this.extendCaretBelowFloatingTable(caret, e.clientX, e.clientY) ?? this.extendCaretIntoBodyWhitespace(caret, e.clientX, e.clientY) ?? caret;
22013
22657
  }
22014
22658
  if (caret && e.shiftKey) {
22015
22659
  const anchor = this.anchorPoint();
@@ -22044,6 +22688,102 @@ var _DocxEditor = class _DocxEditor {
22044
22688
  };
22045
22689
  /** Which part tree the element lives in: document body or header/footer. */
22046
22690
  this.mathEditorEl = null;
22691
+ this.dismissTextContextMenu = () => {
22692
+ this.textContextMenu?.remove();
22693
+ this.textContextMenu = null;
22694
+ document.removeEventListener("mousedown", this.onContextMenuDocumentMouseDown);
22695
+ };
22696
+ this.onContextMenuDocumentMouseDown = (event) => {
22697
+ if (!this.textContextMenu?.contains(event.target)) this.dismissTextContextMenu();
22698
+ };
22699
+ this.onContextMenu = (event) => {
22700
+ const target = event.target;
22701
+ if (!target?.closest('[data-dxw-item-kind="text"]')) return;
22702
+ const caret = this.caretFromPoint(event.clientX, event.clientY);
22703
+ if (!caret || this.regionOf(caret.t) === "hf" !== this.inHeaderFooter) return;
22704
+ event.preventDefault();
22705
+ event.stopPropagation();
22706
+ this.dismissTextContextMenu();
22707
+ if (!this.hasSelection()) {
22708
+ this.caret = caret;
22709
+ this.selectWordAt(caret);
22710
+ }
22711
+ const menu = document.createElement("div");
22712
+ menu.dataset.dxwTextContextMenu = "1";
22713
+ menu.setAttribute("role", "menu");
22714
+ menu.style.cssText = `position:fixed;left:${event.clientX}px;top:${event.clientY}px;z-index:2147483647;min-width:210px;padding:5px;background:#fff;border:1px solid #dadce0;border-radius:8px;box-shadow:0 4px 18px rgba(0,0,0,.2);font:13px system-ui,sans-serif;color:#202124;`;
22715
+ const isApplePlatform = /Mac|iPhone|iPad/.test(navigator.platform);
22716
+ const mod = isApplePlatform ? "\u2318" : "Ctrl+";
22717
+ const item = (label, shortcut, disabled, action) => {
22718
+ const button = document.createElement("button");
22719
+ button.type = "button";
22720
+ button.disabled = disabled;
22721
+ button.setAttribute("role", "menuitem");
22722
+ button.style.cssText = `display:flex;width:100%;align-items:center;justify-content:space-between;gap:24px;border:0;border-radius:5px;padding:7px 9px;background:transparent;color:${disabled ? "#9aa0a6" : "#202124"};cursor:${disabled ? "default" : "pointer"};`;
22723
+ const name = document.createElement("span");
22724
+ name.textContent = label;
22725
+ const keys = document.createElement("span");
22726
+ keys.textContent = shortcut;
22727
+ keys.style.color = "#5f6368";
22728
+ keys.style.fontSize = "11px";
22729
+ button.append(name, keys);
22730
+ button.addEventListener("mouseenter", () => {
22731
+ if (!button.disabled) button.style.background = "#f1f3f4";
22732
+ });
22733
+ button.addEventListener("mouseleave", () => {
22734
+ button.style.background = "transparent";
22735
+ });
22736
+ button.addEventListener("click", () => {
22737
+ if (button.disabled) return;
22738
+ this.dismissTextContextMenu();
22739
+ action();
22740
+ });
22741
+ menu.appendChild(button);
22742
+ };
22743
+ const separator = () => {
22744
+ const line2 = document.createElement("div");
22745
+ line2.style.cssText = "height:1px;background:#e8eaed;margin:4px 2px;";
22746
+ menu.appendChild(line2);
22747
+ };
22748
+ const selectedText = selectionTextLogical(this.host.doc, this.getSelectionSegments());
22749
+ const clipboard = navigator.clipboard;
22750
+ const copy = () => clipboard.writeText(selectedText);
22751
+ item("Copy", `${mod}C`, !selectedText || !clipboard?.writeText, () => void copy());
22752
+ item("Cut", `${mod}X`, !selectedText || !clipboard?.writeText, () => {
22753
+ void copy().then(() => {
22754
+ this.host.history?.checkpoint();
22755
+ this.removeSelectedText();
22756
+ this.commit();
22757
+ });
22758
+ });
22759
+ item("Paste", `${mod}V`, !clipboard?.readText, () => {
22760
+ void clipboard.readText().then((text) => {
22761
+ if (text) this.insertText(text);
22762
+ });
22763
+ });
22764
+ separator();
22765
+ item("Bold", `${mod}B`, !selectedText, () => this.host.onFormatShortcut?.("bold"));
22766
+ item("Italic", `${mod}I`, !selectedText, () => this.host.onFormatShortcut?.("italic"));
22767
+ item("Underline", `${mod}U`, !selectedText, () => this.host.onFormatShortcut?.("underline"));
22768
+ separator();
22769
+ item("Link\u2026", `${mod}K`, !selectedText, () => this.host.onTextCommand?.("link"));
22770
+ item("New comment\u2026", isApplePlatform ? "\u2318\u2325A" : "Ctrl+Alt+M", !selectedText, () => this.host.onTextCommand?.("comment"));
22771
+ item("Bulleted list", `${mod}Shift+L`, false, () => this.host.onTextCommand?.("bullet"));
22772
+ item("Numbered list", `${mod}Shift+7`, false, () => this.host.onTextCommand?.("number"));
22773
+ separator();
22774
+ item("Align left", `${mod}L`, false, () => this.host.onTextCommand?.("alignLeft"));
22775
+ item("Center", `${mod}E`, false, () => this.host.onTextCommand?.("alignCenter"));
22776
+ item("Align right", `${mod}R`, false, () => this.host.onTextCommand?.("alignRight"));
22777
+ item("Justify", `${mod}J`, false, () => this.host.onTextCommand?.("justify"));
22778
+ separator();
22779
+ item("Select all", `${mod}A`, false, () => this.selectAll());
22780
+ document.body.appendChild(menu);
22781
+ const rect = menu.getBoundingClientRect();
22782
+ menu.style.left = `${Math.max(6, Math.min(event.clientX, window.innerWidth - rect.width - 6))}px`;
22783
+ menu.style.top = `${Math.max(6, Math.min(event.clientY, window.innerHeight - rect.height - 6))}px`;
22784
+ this.textContextMenu = menu;
22785
+ setTimeout(() => document.addEventListener("mousedown", this.onContextMenuDocumentMouseDown), 0);
22786
+ };
22047
22787
  // ---------- keyboard ----------
22048
22788
  this.onKeyDown = (e) => {
22049
22789
  if (this.composing || e.isComposing || e.keyCode === 229) return;
@@ -22051,6 +22791,11 @@ var _DocxEditor = class _DocxEditor {
22051
22791
  this.pendingClickTypeCheckpoint = false;
22052
22792
  this.clickTypeCheckpointUntil = 0;
22053
22793
  }
22794
+ if (this.textContextMenu) {
22795
+ if (e.key === "Escape") e.preventDefault();
22796
+ this.dismissTextContextMenu();
22797
+ if (e.key === "Escape") return;
22798
+ }
22054
22799
  if (e.key === "Escape" && this.drawingTool) {
22055
22800
  e.preventDefault();
22056
22801
  this.setDrawingTool(null);
@@ -22165,6 +22910,12 @@ var _DocxEditor = class _DocxEditor {
22165
22910
  }
22166
22911
  }
22167
22912
  const meta = e.metaKey || e.ctrlKey;
22913
+ const isApplePlatform = /Mac|iPhone|iPad/.test(navigator.platform);
22914
+ if (isApplePlatform ? e.metaKey && e.altKey && e.key.toLowerCase() === "a" : e.ctrlKey && e.altKey && e.key.toLowerCase() === "m") {
22915
+ e.preventDefault();
22916
+ if (this.hasSelection()) this.host.onTextCommand?.("comment");
22917
+ return;
22918
+ }
22168
22919
  if (meta && e.altKey && /^[0-6]$/.test(e.key)) {
22169
22920
  e.preventDefault();
22170
22921
  this.host.onStyleShortcut?.(e.key === "0" ? null : `Heading${e.key}`);
@@ -22184,14 +22935,36 @@ var _DocxEditor = class _DocxEditor {
22184
22935
  }
22185
22936
  return;
22186
22937
  }
22187
- if (e.key.startsWith("Arrow")) {
22938
+ if (!e.shiftKey && k === "k") {
22188
22939
  e.preventDefault();
22189
- const extend = e.shiftKey;
22190
- if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
22191
- this.moveFocus((pt) => this.lineEdgePoint(pt, e.key === "ArrowLeft" ? "start" : "end"), extend);
22192
- } else {
22193
- this.moveFocus((pt) => this.paragraphPoint(pt, e.key === "ArrowUp" ? -1 : 1), extend);
22194
- }
22940
+ if (this.hasSelection()) this.host.onTextCommand?.("link");
22941
+ return;
22942
+ }
22943
+ if (e.shiftKey && (k === "l" || k === "8")) {
22944
+ e.preventDefault();
22945
+ this.host.onTextCommand?.("bullet");
22946
+ return;
22947
+ }
22948
+ if (e.shiftKey && k === "7") {
22949
+ e.preventDefault();
22950
+ this.host.onTextCommand?.("number");
22951
+ return;
22952
+ }
22953
+ if (!e.shiftKey && (k === "l" || k === "e" || k === "r" || k === "j")) {
22954
+ e.preventDefault();
22955
+ this.host.onTextCommand?.(
22956
+ k === "l" ? "alignLeft" : k === "e" ? "alignCenter" : k === "r" ? "alignRight" : "justify"
22957
+ );
22958
+ return;
22959
+ }
22960
+ if (e.key.startsWith("Arrow")) {
22961
+ e.preventDefault();
22962
+ const extend = e.shiftKey;
22963
+ if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
22964
+ this.moveFocus((pt) => this.lineEdgePoint(pt, e.key === "ArrowLeft" ? "start" : "end"), extend);
22965
+ } else {
22966
+ this.moveFocus((pt) => this.paragraphPoint(pt, e.key === "ArrowUp" ? -1 : 1), extend);
22967
+ }
22195
22968
  this.revealCaret();
22196
22969
  return;
22197
22970
  }
@@ -22270,6 +23043,7 @@ var _DocxEditor = class _DocxEditor {
22270
23043
  /** Focus the hidden text target (keeps IME working); falls back to the
22271
23044
  * container if the textarea is not attached yet. */
22272
23045
  focusText() {
23046
+ if (!this.imeEl.isConnected) this.host.container.appendChild(this.imeEl);
22273
23047
  if (this.imeEl.isConnected) this.imeEl.focus({ preventScroll: true });
22274
23048
  else this.host.container.focus({ preventScroll: true });
22275
23049
  }
@@ -22278,6 +23052,14 @@ var _DocxEditor = class _DocxEditor {
22278
23052
  this.applyHfChrome();
22279
23053
  this.paintSelection();
22280
23054
  this.positionCaret();
23055
+ if (this.caret || this.selection) this.focusText();
23056
+ }
23057
+ /** Repaint only state affected by virtualized page mounts. Scrolling keeps
23058
+ * the current focus and skips header/footer mode notifications in body mode. */
23059
+ afterViewportChange() {
23060
+ if (this.inHeaderFooter) this.applyHfChrome();
23061
+ if (this.selection) this.paintSelection();
23062
+ if (this.caret) this.positionCaret();
22281
23063
  }
22282
23064
  // ---------- owned selection ----------
22283
23065
  hasSelection() {
@@ -22810,6 +23592,7 @@ var _DocxEditor = class _DocxEditor {
22810
23592
  c.addEventListener("copy", this.onCopy);
22811
23593
  c.addEventListener("cut", this.onCut);
22812
23594
  c.addEventListener("paste", this.onPaste);
23595
+ c.addEventListener("contextmenu", this.onContextMenu);
22813
23596
  c.addEventListener("dragover", this.onDragOver);
22814
23597
  c.addEventListener("drop", this.onDrop);
22815
23598
  this.applyHfChrome();
@@ -22829,8 +23612,10 @@ var _DocxEditor = class _DocxEditor {
22829
23612
  c.removeEventListener("copy", this.onCopy);
22830
23613
  c.removeEventListener("cut", this.onCut);
22831
23614
  c.removeEventListener("paste", this.onPaste);
23615
+ c.removeEventListener("contextmenu", this.onContextMenu);
22832
23616
  c.removeEventListener("dragover", this.onDragOver);
22833
23617
  c.removeEventListener("drop", this.onDrop);
23618
+ this.dismissTextContextMenu();
22834
23619
  this.dismissSuggestionPopover();
22835
23620
  this.setDrawingTool(null);
22836
23621
  this.deselectInkGroup();
@@ -23034,6 +23819,12 @@ var _DocxEditor = class _DocxEditor {
23034
23819
  const page = target.closest?.(".dxw-page");
23035
23820
  const surface = page?.firstElementChild;
23036
23821
  if (!page || !surface || !surface.contains(target)) return false;
23822
+ const zoom = this.host.zoom ?? 1;
23823
+ const rect = surface.getBoundingClientRect();
23824
+ const localY = (e.clientY - rect.top) / zoom;
23825
+ const bodyTop = parseFloat(page.dataset.bodyTop ?? "0");
23826
+ const bodyBottom = parseFloat(page.dataset.bodyBottom ?? "0");
23827
+ if (localY < bodyTop || localY > bodyBottom) return false;
23037
23828
  this.deselectInkGroup();
23038
23829
  if (tool.kind === "eraser") return this.startInkEraser(e, page, tool.size);
23039
23830
  if (tool.kind === "lasso") return this.startInkLasso(e, page, surface);
@@ -23043,8 +23834,6 @@ var _DocxEditor = class _DocxEditor {
23043
23834
  this.deselectImage();
23044
23835
  this.deselectWordArt();
23045
23836
  this.hideCaret();
23046
- const zoom = this.host.zoom ?? 1;
23047
- const rect = surface.getBoundingClientRect();
23048
23837
  const pointAt = (clientX, clientY) => ({
23049
23838
  x: Math.max(0, Math.min((clientX - rect.left) / zoom, rect.width / zoom)),
23050
23839
  y: Math.max(0, Math.min((clientY - rect.top) / zoom, rect.height / zoom))
@@ -23081,9 +23870,18 @@ var _DocxEditor = class _DocxEditor {
23081
23870
  if (points.length < 2) return;
23082
23871
  const anchor = this.nearestCaret(e.clientX, e.clientY, "body") ?? this.nearestCaret(up.clientX, up.clientY, "body");
23083
23872
  if (!anchor) return;
23873
+ const paragraph = paragraphOf(this.host.doc, anchor.t);
23874
+ const dirtyBlock = topLevelBlockOf(this.host.doc, anchor.t) ?? paragraph ?? void 0;
23084
23875
  this.host.history?.checkpoint();
23085
- if (insertInkAt(this.host.doc, anchor.t, points, tool.color, tool.width, tool.kind === "highlighter" ? 0.45 : 1)) {
23086
- this.host.rerender(void 0, "global");
23876
+ if (insertInkAt(this.host.doc, anchor.t, points, tool.color, tool.width, tool.kind === "highlighter" ? 0.45 : 1, false)) {
23877
+ const reparsed = paragraph ? this.host.doc.reparseBodyParagraph(paragraph) : null;
23878
+ if (reparsed) {
23879
+ invalidateParagraphSignature(paragraph);
23880
+ this.host.rerender(dirtyBlock, "local", anchor.t);
23881
+ } else {
23882
+ this.host.doc.refresh();
23883
+ this.host.rerender(void 0, "global");
23884
+ }
23087
23885
  }
23088
23886
  };
23089
23887
  document.addEventListener("mousemove", onMove);
@@ -23687,23 +24485,73 @@ var _DocxEditor = class _DocxEditor {
23687
24485
  };
23688
24486
  extra("Alt", "Alternative text", () => {
23689
24487
  const cur = imageAltText(src);
23690
- const next = window.prompt("Alternative text", cur);
23691
- if (next === null) return;
23692
- this.host.history?.checkpoint();
23693
- if (setImageAltText(this.host.doc, src, next)) this.host.rerender();
23694
- this.deselectImage();
24488
+ void requestTextInputDialog(this.host.container, {
24489
+ title: "Alternative text",
24490
+ label: "Describe this object for screen readers",
24491
+ value: cur
24492
+ }).then((next) => {
24493
+ if (next === null) return;
24494
+ this.host.history?.checkpoint();
24495
+ if (setImageAltText(this.host.doc, src, next)) this.host.rerender();
24496
+ this.deselectImage();
24497
+ });
23695
24498
  });
23696
24499
  extra("Size", "Exact size (px)", () => {
23697
24500
  const curW = parseFloat(el16.style.width) || 0;
23698
24501
  const curH = parseFloat(el16.style.height) || 0;
23699
24502
  const cur = `${Math.round(curW)}x${Math.round(curH)}`;
23700
- const next = window.prompt("Size in px (width x height)", cur);
23701
- const m = next && /^\s*(\d+)\s*[x×]\s*(\d+)\s*$/i.exec(next);
23702
- if (!m) return;
23703
- this.host.history?.checkpoint();
23704
- if (resizeDrawing(this.host.doc, src, parseInt(m[1], 10), parseInt(m[2], 10))) this.host.rerender();
23705
- this.deselectImage();
24503
+ void requestTextInputDialog(this.host.container, {
24504
+ title: "Exact size",
24505
+ label: "Width \xD7 height in pixels",
24506
+ value: cur,
24507
+ placeholder: "640 \xD7 360"
24508
+ }).then((next) => {
24509
+ const match = next && /^\s*(\d+)\s*[x×]\s*(\d+)\s*$/i.exec(next);
24510
+ if (!match) return;
24511
+ this.host.history?.checkpoint();
24512
+ if (resizeDrawing(this.host.doc, src, parseInt(match[1], 10), parseInt(match[2], 10))) this.host.rerender();
24513
+ this.deselectImage();
24514
+ });
24515
+ });
24516
+ extra("Position", "Exact page position (px)", () => {
24517
+ const curX = parseFloat(el16.style.left) || 0;
24518
+ const curY = parseFloat(el16.style.top) || 0;
24519
+ void requestTextInputDialog(this.host.container, {
24520
+ title: "Page position",
24521
+ label: "X, Y in pixels",
24522
+ value: `${Math.round(curX)}, ${Math.round(curY)}`,
24523
+ placeholder: "120, 80"
24524
+ }).then((next) => {
24525
+ const match = next && /^\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*$/.exec(next);
24526
+ if (!match) return;
24527
+ this.host.history?.checkpoint();
24528
+ if (!isFloatingDrawing(src)) setImageWrap(this.host.doc, src, "square", { x: 0, y: 0 });
24529
+ if (setFloatingPagePosition(this.host.doc, src, parseFloat(match[1]), parseFloat(match[2]))) {
24530
+ this.host.rerender();
24531
+ this.reselectImage(src);
24532
+ }
24533
+ });
23706
24534
  });
24535
+ if (kind === "drawing") {
24536
+ extra("Outline", "Outline color and weight", () => {
24537
+ const style = drawingLineStyle(src);
24538
+ if (!style) return;
24539
+ void requestTextInputDialog(this.host.container, {
24540
+ title: "Outline",
24541
+ label: "Color and width in pixels",
24542
+ value: `${style.color}, ${Number(style.width.toFixed(2))}`,
24543
+ placeholder: "#1a73e8, 1.5"
24544
+ }).then((next) => {
24545
+ const match = next && /^\s*#?([0-9a-f]{6})\s*,\s*(\d+(?:\.\d+)?)\s*$/i.exec(next);
24546
+ if (!match) return;
24547
+ this.host.history?.checkpoint();
24548
+ if (setDrawingLineStyle(this.host.doc, src, `#${match[1]}`, parseFloat(match[2]))) {
24549
+ this.host.rerender();
24550
+ this.reselectImage(src);
24551
+ }
24552
+ });
24553
+ });
24554
+ }
23707
24555
  if (kind === "drawing" && isDrawingWordArt(src)) {
23708
24556
  extra("Edit text", "Edit WordArt text", () => this.editDrawingWordArt(src));
23709
24557
  }
@@ -23857,13 +24705,17 @@ var _DocxEditor = class _DocxEditor {
23857
24705
  };
23858
24706
  button("Edit text", "Edit watermark text", () => {
23859
24707
  const cur = wordArtText(src);
23860
- const next = window.prompt("Watermark text", cur);
23861
- if (next === null || next === cur) return;
23862
- this.host.history?.checkpoint();
23863
- if (setWordArtText(this.host.doc, src, next)) {
23864
- this.host.rerender();
23865
- this.reselectWordArt(src);
23866
- }
24708
+ void requestTextInputDialog(this.host.container, {
24709
+ title: "Watermark text",
24710
+ value: cur
24711
+ }).then((next) => {
24712
+ if (next === null || next === cur) return;
24713
+ this.host.history?.checkpoint();
24714
+ if (setWordArtText(this.host.doc, src, next)) {
24715
+ this.host.rerender();
24716
+ this.reselectWordArt(src);
24717
+ }
24718
+ });
23867
24719
  });
23868
24720
  const sep1 = document.createElement("span");
23869
24721
  sep1.style.cssText = "width:1px;background:#dadce0;margin:2px 2px;";
@@ -23875,15 +24727,20 @@ var _DocxEditor = class _DocxEditor {
23875
24727
  bar.appendChild(sep2);
23876
24728
  button("Rotate", "Set rotation (degrees)", () => {
23877
24729
  const cur = wordArtRotation(src);
23878
- const next = window.prompt("Rotation (degrees clockwise)", String(cur));
23879
- if (next === null) return;
23880
- const deg = parseFloat(next);
23881
- if (!Number.isFinite(deg)) return;
23882
- this.host.history?.checkpoint();
23883
- if (setWordArtRotation(this.host.doc, src, deg)) {
23884
- this.host.rerender();
23885
- this.reselectWordArt(src);
23886
- }
24730
+ void requestTextInputDialog(this.host.container, {
24731
+ title: "Watermark rotation",
24732
+ label: "Degrees clockwise",
24733
+ value: String(cur)
24734
+ }).then((next) => {
24735
+ if (next === null) return;
24736
+ const degrees = parseFloat(next);
24737
+ if (!Number.isFinite(degrees)) return;
24738
+ this.host.history?.checkpoint();
24739
+ if (setWordArtRotation(this.host.doc, src, degrees)) {
24740
+ this.host.rerender();
24741
+ this.reselectWordArt(src);
24742
+ }
24743
+ });
23887
24744
  });
23888
24745
  const sep3 = document.createElement("span");
23889
24746
  sep3.style.cssText = "width:1px;background:#dadce0;margin:2px 2px;";
@@ -24518,14 +25375,19 @@ var _DocxEditor = class _DocxEditor {
24518
25375
  return caretAt(t, 0);
24519
25376
  }
24520
25377
  regionOf(t) {
24521
- let cur = t;
24522
- let root;
24523
- while (cur) {
24524
- root = cur;
24525
- cur = this.host.doc.findParentOf(cur);
25378
+ if (this.regionModelVersion !== this.host.doc.modelVersion) {
25379
+ this.headerFooterElements = /* @__PURE__ */ new WeakSet();
25380
+ const visit = (element) => {
25381
+ this.headerFooterElements.add(element);
25382
+ for (const child4 of element.children) visit(child4);
25383
+ };
25384
+ for (const root of this.host.doc.revisionRoots()) {
25385
+ const name = localName(root.name);
25386
+ if (name === "hdr" || name === "ftr") visit(root);
25387
+ }
25388
+ this.regionModelVersion = this.host.doc.modelVersion;
24526
25389
  }
24527
- const ln = root ? localName(root.name) : "";
24528
- return ln === "hdr" || ln === "ftr" ? "hf" : "body";
25390
+ return this.headerFooterElements.has(t) ? "hf" : "body";
24529
25391
  }
24530
25392
  /**
24531
25393
  * Snap to the closest character boundary on the clicked line — makes
@@ -24689,6 +25551,103 @@ var _DocxEditor = class _DocxEditor {
24689
25551
  this.host.rerender(void 0, "global");
24690
25552
  return caret;
24691
25553
  }
25554
+ /** Word-style click-and-type: clicking below the final body line on a page
25555
+ * creates the intervening empty paragraphs so vertical arrows visit every
25556
+ * visible line instead of jumping across synthetic paragraph spacing. */
25557
+ extendCaretIntoBodyWhitespace(caret, x, y) {
25558
+ const handle = this.host.getHandle();
25559
+ const page = document.elementFromPoint(x, y)?.closest(".dxw-page");
25560
+ const surface = page?.firstElementChild;
25561
+ if (!handle || !page || !surface) return null;
25562
+ const rect = surface.getBoundingClientRect();
25563
+ const zoom = this.host.zoom ?? 1;
25564
+ const ly = (y - rect.top) / zoom;
25565
+ const bodyTop = parseFloat(page.dataset.bodyTop ?? "0");
25566
+ const bodyBottom = parseFloat(page.dataset.bodyBottom ?? "0");
25567
+ if (ly < bodyTop || ly > bodyBottom) return null;
25568
+ const paragraph = paragraphOf(this.host.doc, caret.t);
25569
+ const parent = paragraph ? this.host.doc.findParentOf(paragraph) : void 0;
25570
+ if (!paragraph || !parent || localName(parent.name) !== "body") return null;
25571
+ const endText = lastTextOf(paragraph);
25572
+ if (!endText) return null;
25573
+ const binding = (handle.bindingsByText.get(endText) ?? []).filter((candidate) => candidate.el.parentElement === surface && candidate.item.src).sort((a, b) => b.item.lineTop - a.item.lineTop)[0];
25574
+ if (!binding) return null;
25575
+ const lineHeight = Math.max(1, binding.item.lineHeight);
25576
+ if (ly <= binding.item.lineTop + lineHeight / 2) return null;
25577
+ const hasLaterLine = handle.bindings.some(
25578
+ (candidate) => candidate.el.parentElement === surface && candidate.item.src?.t && this.regionOf(candidate.item.src.t) === "body" && candidate.item.lineTop > binding.item.lineTop + lineHeight / 2
25579
+ );
25580
+ if (hasLaterLine) return null;
25581
+ const lineCount = Math.max(1, Math.round((ly - binding.item.lineTop) / lineHeight));
25582
+ this.host.history?.checkpoint();
25583
+ this.caret = {
25584
+ t: endText,
25585
+ run: binding.item.src.run,
25586
+ offset: endText.text.length,
25587
+ bias: endText.text.length > 0 ? "end" : void 0
25588
+ };
25589
+ const split = this.splitParagraphCore();
25590
+ if (!split) return null;
25591
+ const hasPageBreak = (element) => localName(element.name) === "br" && attr(element, "type") === "page" || element.children.some(hasPageBreak);
25592
+ const insertBeforeTrailingBreak = hasPageBreak(split.after);
25593
+ const afterSources = insertBeforeTrailingBreak ? [] : [split.after];
25594
+ const w = split.after.name.includes(":") ? split.after.name.slice(0, split.after.name.indexOf(":") + 1) : "";
25595
+ const templatePPr = split.after.children.find((child4) => localName(child4.name) === "pPr");
25596
+ const templateRun = split.after.children.find(
25597
+ (child4) => localName(child4.name) === "r" && child4.children.includes(this.caret.t)
25598
+ );
25599
+ const templateRPr = templateRun?.children.find((child4) => localName(child4.name) === "rPr");
25600
+ let previous = insertBeforeTrailingBreak ? split.before : split.after;
25601
+ let caretSource = split.after;
25602
+ for (let i = 1; i < lineCount; i++) {
25603
+ if (this.suggesting && previous !== split.before) markParagraphGlyph(previous, "ins", this.revMeta());
25604
+ const t = {
25605
+ name: this.caret.t.name,
25606
+ attrs: { ...this.caret.t.attrs, "xml:space": "preserve" },
25607
+ children: [],
25608
+ text: ""
25609
+ };
25610
+ const run = {
25611
+ name: templateRun?.name ?? `${w}r`,
25612
+ attrs: { ...templateRun?.attrs ?? {} },
25613
+ children: [...templateRPr ? [cloneXml(templateRPr)] : [], t],
25614
+ text: ""
25615
+ };
25616
+ const paragraph2 = {
25617
+ name: split.after.name,
25618
+ attrs: {},
25619
+ children: [...templatePPr ? [cloneXml(templatePPr)] : [], run],
25620
+ text: ""
25621
+ };
25622
+ const previousIndex = parent.children.indexOf(previous);
25623
+ parent.children.splice(previousIndex + 1, 0, paragraph2);
25624
+ afterSources.push(paragraph2);
25625
+ previous = paragraph2;
25626
+ caretSource = paragraph2;
25627
+ this.caret = { t, run: caret.run, offset: 0 };
25628
+ }
25629
+ if (insertBeforeTrailingBreak) afterSources.push(split.after);
25630
+ const reparsed = this.host.doc.reparseDirectBodyParagraphSplits(split.before, afterSources);
25631
+ if (reparsed) {
25632
+ const caretParagraph = reparsed[afterSources.indexOf(caretSource) + 1];
25633
+ for (const child4 of caretParagraph.children) {
25634
+ const runs = child4.type === "run" ? [child4] : child4.runs;
25635
+ const run = runs.find(
25636
+ (candidate) => candidate.content.some((content) => content.kind === "text" && content.srcT === this.caret.t)
25637
+ );
25638
+ if (run) {
25639
+ this.caret.run = run;
25640
+ break;
25641
+ }
25642
+ }
25643
+ invalidateParagraphSignature(split.before);
25644
+ for (const source of afterSources) invalidateParagraphSignature(source);
25645
+ }
25646
+ this.pendingClickTypeCheckpoint = true;
25647
+ this.commit(false, "local", !!reparsed);
25648
+ this.focusText();
25649
+ return this.caret;
25650
+ }
24692
25651
  caretFromPoint(x, y) {
24693
25652
  const doc = document;
24694
25653
  let node = null;
@@ -24991,7 +25950,8 @@ var _DocxEditor = class _DocxEditor {
24991
25950
  if (mergeParagraphBackward(this.host.doc, pEl)) {
24992
25951
  const target = junction && paragraphOf(this.host.doc, junction) ? junction : caret.t;
24993
25952
  this.caret = { t: target, run: caret.run, offset: target === junction ? target.text.length : 0 };
24994
- this.commit();
25953
+ invalidateParagraphSignature(paragraphOf(this.host.doc, target) ?? pEl);
25954
+ this.commit(false, "local", true);
24995
25955
  }
24996
25956
  return;
24997
25957
  }
@@ -25011,7 +25971,10 @@ var _DocxEditor = class _DocxEditor {
25011
25971
  const next = siblingParagraph(this.host.doc, pEl, 1);
25012
25972
  if (!next) return;
25013
25973
  this.host.history?.checkpoint();
25014
- if (mergeParagraphBackward(this.host.doc, next)) this.commit();
25974
+ if (mergeParagraphBackward(this.host.doc, next)) {
25975
+ invalidateParagraphSignature(pEl);
25976
+ this.commit(false, "local", true);
25977
+ }
25015
25978
  return;
25016
25979
  }
25017
25980
  if (!this.stepToNeighbor(1)) return;
@@ -25241,6 +26204,7 @@ var _DocxEditor = class _DocxEditor {
25241
26204
  invalidateParagraphSignature(split.after);
25242
26205
  }
25243
26206
  this.commit(false, "local", !!reparsed);
26207
+ this.focusText();
25244
26208
  }
25245
26209
  /** Split the paragraph at the caret without history or commit — shared by
25246
26210
  * Enter and multi-line paste. */
@@ -26983,7 +27947,8 @@ function detectMissingFonts(layout) {
26983
27947
  for (const page of layout.pages) {
26984
27948
  for (const item of page.items) {
26985
27949
  if (item.kind !== "text" || !item.text.trim()) continue;
26986
- const primary = item.font.family.split(",")[0].trim().replace(/^"|"$/g, "");
27950
+ const requested = item.font.family.split(",")[0].trim().replace(/^"|"$/g, "");
27951
+ const primary = normalizeFamily(requested, item.font.bold, item.font.italic).family;
26987
27952
  if (!primary || GENERIC.has(primary.toLowerCase())) continue;
26988
27953
  const existing = samples.get(primary);
26989
27954
  if (!existing || !/[^-ɏ]/.test(existing) && /[^-ɏ]/.test(item.text)) {
@@ -27115,15 +28080,6 @@ var btnStyle = (active) => ({
27115
28080
  padding: "0 5px",
27116
28081
  color: T.fg
27117
28082
  });
27118
- var selectStyle = {
27119
- height: 26,
27120
- border: "1px solid transparent",
27121
- background: "transparent",
27122
- borderRadius: 4,
27123
- fontSize: 13,
27124
- color: T.fg,
27125
- cursor: "pointer"
27126
- };
27127
28083
  var PAGE_SIZES = [
27128
28084
  { value: "letter", label: "Letter", description: '8.5" \xD7 11"', width: 8.5, height: 11 },
27129
28085
  { value: "legal", label: "Legal", description: '8.5" \xD7 14"', width: 8.5, height: 14 },
@@ -27227,6 +28183,264 @@ function HighlightIcon({ color }) {
27227
28183
  /* @__PURE__ */ jsx("rect", { x: "2", y: "13.5", width: "12", height: "2.5", rx: "0.5", fill: color })
27228
28184
  ] });
27229
28185
  }
28186
+ function ToolbarMenuSelect({
28187
+ value,
28188
+ options,
28189
+ onChange,
28190
+ placeholder = "Choose\u2026",
28191
+ title,
28192
+ ariaLabel,
28193
+ triggerAriaLabel,
28194
+ width,
28195
+ menuWidth,
28196
+ className,
28197
+ style
28198
+ }) {
28199
+ const [open, setOpen] = useState(false);
28200
+ const [position, setPosition] = useState({ left: 8, top: 8, width: menuWidth ?? 180, maxHeight: 320 });
28201
+ const rootRef = useRef(null);
28202
+ const triggerRef = useRef(null);
28203
+ const menuRef = useRef(null);
28204
+ const keyboardOpen = useRef(null);
28205
+ const id = useId();
28206
+ const selected = options.find((option) => option.value === value);
28207
+ useLayoutEffect(() => {
28208
+ if (!open) return;
28209
+ const update = () => {
28210
+ const trigger = triggerRef.current;
28211
+ if (!trigger) return;
28212
+ const rect = trigger.getBoundingClientRect();
28213
+ const nextWidth = Math.min(
28214
+ window.innerWidth - 16,
28215
+ menuWidth ?? Math.max(rect.width, menuRef.current?.scrollWidth ?? 180)
28216
+ );
28217
+ const below = window.innerHeight - rect.bottom - 8;
28218
+ const above = rect.top - 8;
28219
+ const placeAbove = below < 140 && above > below;
28220
+ const maxHeight = Math.max(96, Math.min(320, placeAbove ? above : below));
28221
+ const shownHeight = Math.min(menuRef.current?.scrollHeight ?? maxHeight, maxHeight);
28222
+ setPosition({
28223
+ left: Math.max(8, Math.min(rect.left, window.innerWidth - nextWidth - 8)),
28224
+ top: placeAbove ? Math.max(8, rect.top - shownHeight - 4) : rect.bottom + 4,
28225
+ width: nextWidth,
28226
+ maxHeight
28227
+ });
28228
+ };
28229
+ update();
28230
+ const frame = requestAnimationFrame(() => {
28231
+ update();
28232
+ if (!keyboardOpen.current) return;
28233
+ const items = menuRef.current?.querySelectorAll('[role="option"]');
28234
+ const item = keyboardOpen.current === "last" ? items?.[items.length - 1] : items?.[0];
28235
+ item?.focus({ preventScroll: true });
28236
+ keyboardOpen.current = null;
28237
+ });
28238
+ const close = (event) => {
28239
+ if (!rootRef.current?.contains(event.target)) setOpen(false);
28240
+ };
28241
+ const keydown = (event) => {
28242
+ if (event.key === "Escape") {
28243
+ setOpen(false);
28244
+ requestAnimationFrame(() => triggerRef.current?.focus({ preventScroll: true }));
28245
+ }
28246
+ };
28247
+ document.addEventListener("mousedown", close);
28248
+ document.addEventListener("keydown", keydown);
28249
+ window.addEventListener("resize", update);
28250
+ window.addEventListener("scroll", update, true);
28251
+ return () => {
28252
+ cancelAnimationFrame(frame);
28253
+ document.removeEventListener("mousedown", close);
28254
+ document.removeEventListener("keydown", keydown);
28255
+ window.removeEventListener("resize", update);
28256
+ window.removeEventListener("scroll", update, true);
28257
+ };
28258
+ }, [open, menuWidth]);
28259
+ const pick = (next) => {
28260
+ const option = options.find((item) => item.value === next);
28261
+ if (!option || option.disabled) return;
28262
+ onChange(next);
28263
+ setOpen(false);
28264
+ };
28265
+ const onMenuKeyDown = (event) => {
28266
+ const items = menuRef.current ? Array.from(menuRef.current.querySelectorAll('[role="option"]:not(:disabled)')) : [];
28267
+ const current = items.indexOf(document.activeElement);
28268
+ let next = current;
28269
+ if (event.key === "ArrowDown") next = current < 0 ? 0 : (current + 1) % items.length;
28270
+ else if (event.key === "ArrowUp") next = current < 0 ? items.length - 1 : (current - 1 + items.length) % items.length;
28271
+ else if (event.key === "Home") next = 0;
28272
+ else if (event.key === "End") next = items.length - 1;
28273
+ else if ((event.key === "Enter" || event.key === " ") && current >= 0) {
28274
+ event.preventDefault();
28275
+ items[current].click();
28276
+ return;
28277
+ } else if (event.key === "Escape" || event.key === "Tab") {
28278
+ event.preventDefault();
28279
+ setOpen(false);
28280
+ requestAnimationFrame(() => triggerRef.current?.focus({ preventScroll: true }));
28281
+ return;
28282
+ } else return;
28283
+ event.preventDefault();
28284
+ items[next]?.focus({ preventScroll: true });
28285
+ };
28286
+ let previousGroup;
28287
+ return /* @__PURE__ */ jsxs(
28288
+ "span",
28289
+ {
28290
+ ref: rootRef,
28291
+ className: `dxw-menu-select${className ? ` ${className}` : ""}`,
28292
+ "data-dxw-menu-select": "",
28293
+ style: { position: "relative", display: "inline-flex", width },
28294
+ children: [
28295
+ /* @__PURE__ */ jsxs(
28296
+ "select",
28297
+ {
28298
+ tabIndex: -1,
28299
+ title,
28300
+ "aria-label": ariaLabel,
28301
+ "aria-hidden": "true",
28302
+ value,
28303
+ onChange: (event) => pick(event.target.value),
28304
+ "data-dxw-native-bridge": "",
28305
+ style: {
28306
+ position: "absolute",
28307
+ width: 1,
28308
+ height: 1,
28309
+ inset: 0,
28310
+ opacity: 0,
28311
+ pointerEvents: "none"
28312
+ },
28313
+ children: [
28314
+ !options.some((option) => option.value === value) && /* @__PURE__ */ jsx("option", { value: "", children: String(placeholder) }),
28315
+ options.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, disabled: option.disabled, children: String(option.label) }, option.value))
28316
+ ]
28317
+ }
28318
+ ),
28319
+ /* @__PURE__ */ jsxs(
28320
+ "button",
28321
+ {
28322
+ ref: triggerRef,
28323
+ type: "button",
28324
+ "aria-haspopup": "listbox",
28325
+ "aria-expanded": open,
28326
+ "aria-controls": open ? id : void 0,
28327
+ "aria-label": triggerAriaLabel,
28328
+ "data-tip": title,
28329
+ className: "dxw-menu-select-trigger",
28330
+ "data-dxw-menu-select-trigger": "",
28331
+ onMouseDown: (event) => event.preventDefault(),
28332
+ onClick: () => setOpen(!open),
28333
+ onKeyDown: (event) => {
28334
+ if (!open && (event.key === "Enter" || event.key === " " || event.key === "ArrowDown" || event.key === "ArrowUp")) {
28335
+ event.preventDefault();
28336
+ keyboardOpen.current = event.key === "ArrowUp" ? "last" : "first";
28337
+ setOpen(true);
28338
+ }
28339
+ },
28340
+ style: {
28341
+ width: "100%",
28342
+ minWidth: 0,
28343
+ height: "var(--dxw-select-height, 26px)",
28344
+ border: "1px solid var(--dxw-select-border, transparent)",
28345
+ borderRadius: "var(--dxw-select-radius, 4px)",
28346
+ background: "var(--dxw-select-bg, transparent)",
28347
+ color: "var(--dxw-select-fg, var(--dxw-toolbar-fg, #3c4043))",
28348
+ padding: "var(--dxw-select-padding, 0 6px)",
28349
+ display: "inline-flex",
28350
+ alignItems: "center",
28351
+ justifyContent: "space-between",
28352
+ gap: 6,
28353
+ cursor: "pointer",
28354
+ font: "var(--dxw-select-font, 13px system-ui, sans-serif)",
28355
+ ...style
28356
+ },
28357
+ children: [
28358
+ /* @__PURE__ */ jsx("span", { style: { minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontFamily: selected?.fontFamily }, children: selected?.label ?? placeholder }),
28359
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "dxw-menu-select-chevron", style: { flexShrink: 0, fontSize: 10 }, children: "\u2304" })
28360
+ ]
28361
+ }
28362
+ ),
28363
+ open && /* @__PURE__ */ jsx(
28364
+ "div",
28365
+ {
28366
+ ref: menuRef,
28367
+ id,
28368
+ role: "listbox",
28369
+ "aria-label": ariaLabel ?? title,
28370
+ className: "dxw-menu-select-menu",
28371
+ "data-dxw-menu-select-menu": "",
28372
+ onMouseDown: (event) => event.preventDefault(),
28373
+ onKeyDown: onMenuKeyDown,
28374
+ style: {
28375
+ position: "fixed",
28376
+ left: position.left,
28377
+ top: position.top,
28378
+ zIndex: "var(--dxw-toolbar-z-index, 100)",
28379
+ width: position.width,
28380
+ maxHeight: `min(var(--dxw-select-menu-max-height, 320px), ${position.maxHeight}px)`,
28381
+ overflowY: "auto",
28382
+ overscrollBehavior: "contain",
28383
+ boxSizing: "border-box",
28384
+ padding: "var(--dxw-select-menu-padding, 5px)",
28385
+ border: "1px solid var(--dxw-select-menu-border, var(--dxw-toolbar-border, #dadce0))",
28386
+ borderRadius: "var(--dxw-select-menu-radius, 8px)",
28387
+ background: "var(--dxw-select-menu-bg, var(--dxw-popover-bg, #fff))",
28388
+ boxShadow: "var(--dxw-select-menu-shadow, var(--dxw-popover-shadow, 0 4px 16px rgba(0,0,0,.15)))"
28389
+ },
28390
+ children: options.map((option) => {
28391
+ const groupChanged = option.group !== previousGroup;
28392
+ previousGroup = option.group;
28393
+ const active = option.value === value;
28394
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
28395
+ groupChanged && option.group && /* @__PURE__ */ jsx("div", { className: "dxw-menu-select-group", style: { padding: "6px 8px 3px", color: T.muted, font: "600 10.5px system-ui, sans-serif" }, children: option.group }),
28396
+ /* @__PURE__ */ jsxs(
28397
+ "button",
28398
+ {
28399
+ type: "button",
28400
+ role: "option",
28401
+ "aria-selected": active,
28402
+ disabled: option.disabled,
28403
+ tabIndex: -1,
28404
+ className: "dxw-menu-select-option",
28405
+ "data-dxw-menu-select-option": option.value,
28406
+ onMouseDown: (event) => event.preventDefault(),
28407
+ onClick: () => pick(option.value),
28408
+ style: {
28409
+ width: "100%",
28410
+ minHeight: 30,
28411
+ border: 0,
28412
+ borderRadius: 6,
28413
+ padding: "5px 8px",
28414
+ display: "flex",
28415
+ alignItems: "center",
28416
+ gap: 8,
28417
+ background: active ? T.activeBg : "transparent",
28418
+ color: option.disabled ? T.muted : T.fg,
28419
+ textAlign: "left",
28420
+ cursor: option.disabled ? "default" : "pointer",
28421
+ font: "13px system-ui, sans-serif",
28422
+ fontFamily: option.fontFamily
28423
+ },
28424
+ onMouseEnter: (event) => {
28425
+ if (!active && !option.disabled) event.currentTarget.style.background = T.hoverBg;
28426
+ },
28427
+ onMouseLeave: (event) => {
28428
+ if (!active) event.currentTarget.style.background = "transparent";
28429
+ },
28430
+ children: [
28431
+ /* @__PURE__ */ jsx("span", { style: { flex: 1 }, children: option.label }),
28432
+ active && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", style: { color: T.accent, fontWeight: 700 }, children: "\u2713" })
28433
+ ]
28434
+ }
28435
+ )
28436
+ ] }, option.value);
28437
+ })
28438
+ }
28439
+ )
28440
+ ]
28441
+ }
28442
+ );
28443
+ }
27230
28444
  function ActionMenu({
27231
28445
  label,
27232
28446
  title,
@@ -27234,22 +28448,21 @@ function ActionMenu({
27234
28448
  onPick,
27235
28449
  width
27236
28450
  }) {
27237
- return /* @__PURE__ */ jsxs(
27238
- "select",
28451
+ return /* @__PURE__ */ jsx(
28452
+ ToolbarMenuSelect,
27239
28453
  {
27240
28454
  title,
28455
+ triggerAriaLabel: title,
27241
28456
  value: "",
27242
- onMouseDown: (e) => e.stopPropagation(),
27243
- onChange: (e) => {
27244
- if (e.target.value) onPick(e.target.value);
27245
- },
27246
- style: { ...selectStyle, width },
27247
- children: [
27248
- /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: label }),
27249
- groups.map(
27250
- (g, i) => g.label ? /* @__PURE__ */ jsx("optgroup", { label: g.label, children: g.items.map(([v, t]) => /* @__PURE__ */ jsx("option", { value: v, children: t }, v)) }, i) : g.items.map(([v, t]) => /* @__PURE__ */ jsx("option", { value: v, children: t }, v))
27251
- )
27252
- ]
28457
+ placeholder: label,
28458
+ width,
28459
+ menuWidth: Math.max(width ?? 0, 190),
28460
+ options: groups.flatMap((group) => group.items.map(([value, text]) => ({
28461
+ value,
28462
+ label: text,
28463
+ group: group.label
28464
+ }))),
28465
+ onChange: onPick
27253
28466
  }
27254
28467
  );
27255
28468
  }
@@ -27329,6 +28542,183 @@ function HighlightMenu({ current, onPick }) {
27329
28542
  )
27330
28543
  ] });
27331
28544
  }
28545
+ var COLOR_SWATCHES = [
28546
+ "#000000",
28547
+ "#434343",
28548
+ "#666666",
28549
+ "#999999",
28550
+ "#b7b7b7",
28551
+ "#cccccc",
28552
+ "#d9d9d9",
28553
+ "#ffffff",
28554
+ "#980000",
28555
+ "#ff0000",
28556
+ "#ff9900",
28557
+ "#ffff00",
28558
+ "#00ff00",
28559
+ "#00ffff",
28560
+ "#4a86e8",
28561
+ "#0000ff",
28562
+ "#9900ff",
28563
+ "#ff00ff",
28564
+ "#e6b8af",
28565
+ "#f4cccc",
28566
+ "#fce5cd",
28567
+ "#fff2cc",
28568
+ "#d9ead3",
28569
+ "#d0e0e3",
28570
+ "#c9daf8",
28571
+ "#cfe2f3",
28572
+ "#d9d2e9",
28573
+ "#ead1dc",
28574
+ "#a61c00",
28575
+ "#cc0000",
28576
+ "#e69138",
28577
+ "#6aa84f"
28578
+ ];
28579
+ function normalizedColor(value) {
28580
+ const match = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(value.trim());
28581
+ if (!match) return null;
28582
+ const digits = match[1].length === 3 ? match[1].split("").map((digit) => digit + digit).join("") : match[1];
28583
+ return `#${digits.toLowerCase()}`;
28584
+ }
28585
+ function ColorMenu({
28586
+ current,
28587
+ title,
28588
+ trigger,
28589
+ onPick
28590
+ }) {
28591
+ const [open, setOpen] = useState(false);
28592
+ const [custom, setCustom] = useState(current);
28593
+ const [position, setPosition] = useState({ left: 8, top: 8 });
28594
+ const rootRef = useRef(null);
28595
+ const triggerRef = useRef(null);
28596
+ const menuRef = useRef(null);
28597
+ const valid = normalizedColor(custom);
28598
+ useLayoutEffect(() => {
28599
+ if (!open) return;
28600
+ setCustom(current);
28601
+ const update = () => {
28602
+ const rect = triggerRef.current?.getBoundingClientRect();
28603
+ if (!rect) return;
28604
+ const width = Math.min(236, window.innerWidth - 16);
28605
+ const menuHeight = menuRef.current?.offsetHeight ?? 188;
28606
+ const top = window.innerHeight - rect.bottom >= menuHeight + 8 ? rect.bottom + 4 : Math.max(8, rect.top - menuHeight - 4);
28607
+ setPosition({ left: Math.max(8, Math.min(rect.left, window.innerWidth - width - 8)), top });
28608
+ };
28609
+ update();
28610
+ const frame = requestAnimationFrame(update);
28611
+ const close = (event) => {
28612
+ if (!rootRef.current?.contains(event.target)) setOpen(false);
28613
+ };
28614
+ const keydown = (event) => {
28615
+ if (event.key === "Escape") {
28616
+ setOpen(false);
28617
+ requestAnimationFrame(() => triggerRef.current?.focus({ preventScroll: true }));
28618
+ }
28619
+ };
28620
+ document.addEventListener("mousedown", close);
28621
+ document.addEventListener("keydown", keydown);
28622
+ window.addEventListener("resize", update);
28623
+ window.addEventListener("scroll", update, true);
28624
+ return () => {
28625
+ cancelAnimationFrame(frame);
28626
+ document.removeEventListener("mousedown", close);
28627
+ document.removeEventListener("keydown", keydown);
28628
+ window.removeEventListener("resize", update);
28629
+ window.removeEventListener("scroll", update, true);
28630
+ };
28631
+ }, [open, current]);
28632
+ const pick = (value) => {
28633
+ onPick(value);
28634
+ setOpen(false);
28635
+ };
28636
+ return /* @__PURE__ */ jsxs("span", { ref: rootRef, className: "dxw-color-control", style: { position: "relative", display: "inline-flex" }, children: [
28637
+ /* @__PURE__ */ jsx(
28638
+ "button",
28639
+ {
28640
+ ref: triggerRef,
28641
+ type: "button",
28642
+ title,
28643
+ "aria-haspopup": "dialog",
28644
+ "aria-expanded": open,
28645
+ className: "dxw-color-trigger",
28646
+ "data-dxw-color-trigger": "",
28647
+ style: { ...btnStyle(open), display: "inline-flex", alignItems: "center", gap: 5 },
28648
+ onMouseDown: (event) => event.preventDefault(),
28649
+ onClick: () => setOpen(!open),
28650
+ children: trigger
28651
+ }
28652
+ ),
28653
+ open && /* @__PURE__ */ jsxs(
28654
+ "div",
28655
+ {
28656
+ ref: menuRef,
28657
+ role: "dialog",
28658
+ "aria-label": title,
28659
+ className: "dxw-color-menu",
28660
+ "data-dxw-color-menu": "",
28661
+ onMouseDown: (event) => event.stopPropagation(),
28662
+ style: {
28663
+ position: "fixed",
28664
+ left: position.left,
28665
+ top: position.top,
28666
+ zIndex: "var(--dxw-toolbar-z-index, 100)",
28667
+ width: "min(var(--dxw-color-menu-width, 236px), calc(100vw - 16px))",
28668
+ boxSizing: "border-box",
28669
+ padding: 8,
28670
+ border: `1px solid ${T.border}`,
28671
+ borderRadius: 8,
28672
+ background: T.popoverBg,
28673
+ boxShadow: T.popoverShadow
28674
+ },
28675
+ children: [
28676
+ /* @__PURE__ */ jsx("div", { className: "dxw-color-swatches", style: { display: "grid", gridTemplateColumns: "repeat(8, 1fr)", gap: 4 }, children: COLOR_SWATCHES.map((color) => /* @__PURE__ */ jsx(
28677
+ "button",
28678
+ {
28679
+ type: "button",
28680
+ title: color,
28681
+ "aria-label": `Choose ${color}`,
28682
+ className: "dxw-color-swatch",
28683
+ "data-dxw-color": color,
28684
+ onMouseDown: (event) => event.preventDefault(),
28685
+ onClick: () => pick(color),
28686
+ style: {
28687
+ width: 23,
28688
+ height: 23,
28689
+ padding: 0,
28690
+ borderRadius: 4,
28691
+ cursor: "pointer",
28692
+ border: color.toLowerCase() === current.toLowerCase() ? `2px solid ${T.accent}` : `1px solid ${T.border}`,
28693
+ background: color
28694
+ }
28695
+ },
28696
+ color
28697
+ )) }),
28698
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6, marginTop: 8 }, children: [
28699
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", style: { width: 24, height: 24, borderRadius: 4, border: `1px solid ${T.border}`, background: valid ?? current, flexShrink: 0 } }),
28700
+ /* @__PURE__ */ jsx(
28701
+ "input",
28702
+ {
28703
+ "aria-label": "Custom hex color",
28704
+ className: "dxw-color-value",
28705
+ value: custom,
28706
+ onChange: (event) => setCustom(event.target.value),
28707
+ onKeyDown: (event) => {
28708
+ if (event.key === "Enter" && valid) pick(valid);
28709
+ },
28710
+ spellCheck: false,
28711
+ placeholder: "#1a73e8",
28712
+ style: { minWidth: 0, flex: 1, height: 28, boxSizing: "border-box", border: `1px solid ${T.border}`, borderRadius: 5, padding: "3px 6px", color: T.fg, background: T.popoverBg }
28713
+ }
28714
+ ),
28715
+ /* @__PURE__ */ jsx("button", { type: "button", disabled: !valid, onClick: () => valid && pick(valid), style: pillBtn, children: "Apply" })
28716
+ ] })
28717
+ ]
28718
+ }
28719
+ )
28720
+ ] });
28721
+ }
27332
28722
  function BulletListIcon() {
27333
28723
  return /* @__PURE__ */ jsxs("svg", { style: icon, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.4", children: [
27334
28724
  /* @__PURE__ */ jsx("circle", { cx: "3", cy: "4", r: "1.1", fill: "currentColor", stroke: "none" }),
@@ -27691,7 +29081,18 @@ function CrossReferenceMenu({ api }) {
27691
29081
  return /* @__PURE__ */ jsxs("span", { ref: rootRef, style: { position: "relative", display: "inline-block" }, children: [
27692
29082
  /* @__PURE__ */ jsx("button", { title: "Insert cross-reference", style: btnStyle(open), onMouseDown: (event) => event.preventDefault(), onClick: () => setOpen(!open), children: "Cross-reference" }),
27693
29083
  open && /* @__PURE__ */ jsx("div", { style: { position: "absolute", top: 28, left: 0, zIndex: 100, width: 260, padding: 10, background: T.popoverBg, border: `1px solid ${T.border}`, borderRadius: 8, boxShadow: T.popoverShadow }, children: bookmarks.length === 0 ? /* @__PURE__ */ jsx("div", { style: { color: T.muted, fontSize: 12 }, children: "Add a bookmark first, then reference its text or page." }) : /* @__PURE__ */ jsxs(Fragment2, { children: [
27694
- /* @__PURE__ */ jsx("select", { value: selected, onChange: (event) => setBookmark(event.target.value), style: { ...selectStyle, width: "100%", borderColor: T.border }, children: bookmarks.map((name) => /* @__PURE__ */ jsx("option", { value: name, children: name }, name)) }),
29084
+ /* @__PURE__ */ jsx(
29085
+ ToolbarMenuSelect,
29086
+ {
29087
+ value: selected,
29088
+ ariaLabel: "Bookmark to reference",
29089
+ width: "100%",
29090
+ menuWidth: 240,
29091
+ options: bookmarks.map((name) => ({ value: name, label: name })),
29092
+ onChange: setBookmark,
29093
+ style: { borderColor: T.border }
29094
+ }
29095
+ ),
27695
29096
  /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: 6, marginTop: 8 }, children: [
27696
29097
  /* @__PURE__ */ jsx("button", { style: { ...pillBtn, background: T.popoverBg, color: T.fg }, onClick: () => insert("page"), children: "Page number" }),
27697
29098
  /* @__PURE__ */ jsx("button", { style: pillBtn, onClick: () => insert("text"), children: "Bookmark text" })
@@ -27798,6 +29199,7 @@ function SymbolMenu({ api }) {
27798
29199
  ] });
27799
29200
  }
27800
29201
  var SHAPES = [
29202
+ ["line", "Line", "\u2015"],
27801
29203
  ["rectangle", "Rectangle", "\u25AD"],
27802
29204
  ["roundedRectangle", "Rounded rectangle", "\u25A2"],
27803
29205
  ["ellipse", "Ellipse", "\u25EF"],
@@ -27969,12 +29371,23 @@ function ChartMenu({ api }) {
27969
29371
  return /* @__PURE__ */ jsxs("span", { ref: rootRef, style: { position: "relative", display: "inline-block" }, children: [
27970
29372
  /* @__PURE__ */ jsx("button", { title: "Insert or edit chart", style: btnStyle(open), onMouseDown: (event) => event.preventDefault(), onClick: () => setOpen(!open), children: "Chart" }),
27971
29373
  open && /* @__PURE__ */ jsxs("div", { style: { position: "absolute", top: 28, right: 0, zIndex: 100, width: 320, padding: 10, display: "grid", gap: 7, background: T.popoverBg, border: `1px solid ${T.border}`, borderRadius: 8, boxShadow: T.popoverShadow }, children: [
27972
- /* @__PURE__ */ jsxs("select", { "aria-label": "Chart type", value: type, onChange: (event) => setType(event.target.value), style: fieldStyle, children: [
27973
- /* @__PURE__ */ jsx("option", { value: "column", children: "Column" }),
27974
- /* @__PURE__ */ jsx("option", { value: "bar", children: "Bar" }),
27975
- /* @__PURE__ */ jsx("option", { value: "line", children: "Line" }),
27976
- /* @__PURE__ */ jsx("option", { value: "pie", children: "Pie" })
27977
- ] }),
29374
+ /* @__PURE__ */ jsx(
29375
+ ToolbarMenuSelect,
29376
+ {
29377
+ value: type,
29378
+ ariaLabel: "Chart type",
29379
+ width: "100%",
29380
+ menuWidth: 300,
29381
+ options: [
29382
+ { value: "column", label: "Column" },
29383
+ { value: "bar", label: "Bar" },
29384
+ { value: "line", label: "Line" },
29385
+ { value: "pie", label: "Pie" }
29386
+ ],
29387
+ onChange: (value) => setType(value),
29388
+ style: fieldStyle
29389
+ }
29390
+ ),
27978
29391
  /* @__PURE__ */ jsx("input", { "aria-label": "Chart title", value: title, onChange: (event) => setTitle(event.target.value), placeholder: "Chart title", style: fieldStyle }),
27979
29392
  /* @__PURE__ */ jsx("input", { "aria-label": "Chart categories", value: categories, onChange: (event) => setCategories(event.target.value), placeholder: "Q1, Q2, Q3, Q4", style: fieldStyle }),
27980
29393
  /* @__PURE__ */ jsx("textarea", { "aria-label": "Chart series", rows: 3, value: series, onChange: (event) => setSeries(event.target.value), style: { ...fieldStyle, resize: "vertical" } }),
@@ -28007,12 +29420,23 @@ function SmartArtMenu({ api }) {
28007
29420
  return /* @__PURE__ */ jsxs("span", { ref: rootRef, style: { position: "relative", display: "inline-block" }, children: [
28008
29421
  /* @__PURE__ */ jsx("button", { title: "Insert or edit SmartArt", style: btnStyle(open), onMouseDown: (event) => event.preventDefault(), onClick: () => setOpen(!open), children: "SmartArt" }),
28009
29422
  open && /* @__PURE__ */ jsxs("div", { style: { position: "absolute", top: 28, right: 0, zIndex: 100, width: 280, padding: 10, display: "grid", gap: 7, background: T.popoverBg, border: `1px solid ${T.border}`, borderRadius: 8, boxShadow: T.popoverShadow }, children: [
28010
- /* @__PURE__ */ jsxs("select", { "aria-label": "SmartArt layout", value: layout, onChange: (event) => setLayout(event.target.value), style: fieldStyle, children: [
28011
- /* @__PURE__ */ jsx("option", { value: "process", children: "Process" }),
28012
- /* @__PURE__ */ jsx("option", { value: "cycle", children: "Cycle" }),
28013
- /* @__PURE__ */ jsx("option", { value: "hierarchy", children: "Hierarchy" }),
28014
- /* @__PURE__ */ jsx("option", { value: "list", children: "List" })
28015
- ] }),
29423
+ /* @__PURE__ */ jsx(
29424
+ ToolbarMenuSelect,
29425
+ {
29426
+ value: layout,
29427
+ ariaLabel: "SmartArt layout",
29428
+ width: "100%",
29429
+ menuWidth: 260,
29430
+ options: [
29431
+ { value: "process", label: "Process" },
29432
+ { value: "cycle", label: "Cycle" },
29433
+ { value: "hierarchy", label: "Hierarchy" },
29434
+ { value: "list", label: "List" }
29435
+ ],
29436
+ onChange: (value) => setLayout(value),
29437
+ style: fieldStyle
29438
+ }
29439
+ ),
28016
29440
  /* @__PURE__ */ jsx("textarea", { "aria-label": "SmartArt items", rows: 5, value: items, onChange: (event) => setItems(event.target.value), style: { ...fieldStyle, resize: "vertical" } }),
28017
29441
  /* @__PURE__ */ jsx("div", { style: { color: T.muted, font: "11.5px system-ui, sans-serif" }, children: "One diagram item per line." }),
28018
29442
  /* @__PURE__ */ jsx("button", { disabled: !items.trim(), onClick: submit, style: { border: 0, borderRadius: 6, padding: "7px 10px", background: items.trim() ? T.accent : T.border, color: T.accentFg, cursor: items.trim() ? "pointer" : "default", font: "600 12px system-ui, sans-serif" }, children: "Insert or update SmartArt" })
@@ -28070,19 +29494,18 @@ function DrawTab({ api }) {
28070
29494
  /* @__PURE__ */ jsx(Btn, { label: "Eraser", title: "Stroke eraser", active: active === "eraser", onClick: () => api?.setDrawingTool({ kind: "eraser", size: 14 }) }),
28071
29495
  /* @__PURE__ */ jsx(Btn, { label: "Lasso", title: "Lasso ink", active: active === "lasso", onClick: () => api?.setDrawingTool({ kind: "lasso" }) }),
28072
29496
  /* @__PURE__ */ jsx(Sep, {}),
28073
- /* @__PURE__ */ jsxs("label", { title: "Pen color", style: { display: "inline-flex", alignItems: "center", gap: 5, color: T.fg, font: "12px system-ui, sans-serif" }, children: [
28074
- "Color",
28075
- /* @__PURE__ */ jsx(
28076
- "input",
28077
- {
28078
- "aria-label": "Pen color",
28079
- type: "color",
28080
- value: color,
28081
- onChange: (event) => activate(active === "highlighter" ? "highlighter" : "pen", event.target.value, width),
28082
- style: { width: 28, height: 24, padding: 1, border: `1px solid ${T.border}`, borderRadius: 4, background: "transparent" }
28083
- }
28084
- )
28085
- ] }),
29497
+ /* @__PURE__ */ jsx(
29498
+ ColorMenu,
29499
+ {
29500
+ current: color,
29501
+ title: "Pen color",
29502
+ trigger: /* @__PURE__ */ jsxs(Fragment2, { children: [
29503
+ /* @__PURE__ */ jsx("span", { style: { font: "12px system-ui, sans-serif" }, children: "Color" }),
29504
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", style: { width: 18, height: 18, borderRadius: 4, border: `1px solid ${T.border}`, background: color } })
29505
+ ] }),
29506
+ onPick: (value) => activate(active === "highlighter" ? "highlighter" : "pen", value, width)
29507
+ }
29508
+ ),
28086
29509
  /* @__PURE__ */ jsx(
28087
29510
  ActionMenu,
28088
29511
  {
@@ -28660,9 +30083,8 @@ function MarginMenu({
28660
30083
  "input",
28661
30084
  {
28662
30085
  "aria-label": `${label} margin (inches)`,
28663
- type: "number",
28664
- min: "0",
28665
- step: "0.01",
30086
+ type: "text",
30087
+ inputMode: "decimal",
28666
30088
  required: true,
28667
30089
  autoFocus: side === "top",
28668
30090
  value: values[side],
@@ -28787,9 +30209,8 @@ function PageSizeMenu({
28787
30209
  "input",
28788
30210
  {
28789
30211
  "aria-label": `Page ${side} (inches)`,
28790
- type: "number",
28791
- min: "0.01",
28792
- step: "0.01",
30212
+ type: "text",
30213
+ inputMode: "decimal",
28793
30214
  required: true,
28794
30215
  autoFocus: side === "width",
28795
30216
  value: values[side],
@@ -28887,19 +30308,20 @@ function LayoutTab({ api, showArrange }) {
28887
30308
  "data-dxw-layout-ribbon": "",
28888
30309
  style: { display: "flex", flex: "1 1 640px", minWidth: 0, alignItems: "center", flexWrap: "wrap", gap: 2 },
28889
30310
  children: [
28890
- /* @__PURE__ */ jsxs(
28891
- "select",
30311
+ /* @__PURE__ */ jsx(
30312
+ ToolbarMenuSelect,
28892
30313
  {
28893
30314
  title: "Apply layout changes to",
28894
- "aria-label": "Apply layout changes to",
30315
+ ariaLabel: "Apply layout changes to",
28895
30316
  value: scope,
28896
- onMouseDown: (e) => e.stopPropagation(),
28897
- onChange: (e) => setScope(e.target.value),
28898
- style: { ...selectStyle, width: 118, maxWidth: "100%" },
28899
- children: [
28900
- /* @__PURE__ */ jsx("option", { value: "document", children: "Whole document" }),
28901
- /* @__PURE__ */ jsx("option", { value: "section", children: "This section" })
28902
- ]
30317
+ width: 118,
30318
+ menuWidth: 170,
30319
+ options: [
30320
+ { value: "document", label: "Whole document" },
30321
+ { value: "section", label: "This section" }
30322
+ ],
30323
+ onChange: (value) => setScope(value),
30324
+ style: { maxWidth: "100%" }
28903
30325
  }
28904
30326
  ),
28905
30327
  /* @__PURE__ */ jsx(Sep, {}),
@@ -29126,42 +30548,44 @@ function DocxToolbar({
29126
30548
  if (on("styles"))
29127
30549
  groups.push({
29128
30550
  key: "styles",
29129
- node: /* @__PURE__ */ jsxs(
29130
- "select",
30551
+ node: /* @__PURE__ */ jsx(
30552
+ ToolbarMenuSelect,
29131
30553
  {
29132
30554
  title: "Paragraph style",
29133
30555
  value: curStyle ?? "__normal",
29134
- onMouseDown: (e) => e.stopPropagation(),
29135
- onChange: (e) => {
29136
- if (e.target.value) {
29137
- api?.setParagraphStyle(e.target.value === "__normal" ? null : e.target.value);
30556
+ width: 92,
30557
+ menuWidth: 190,
30558
+ options: [
30559
+ { value: "__normal", label: "Normal" },
30560
+ ...(api?.listParagraphStyles() ?? []).filter((style2) => !/^normal$/i.test(style2.name)).map((style2) => ({ value: style2.id, label: style2.name })),
30561
+ ...curStyle !== null && !(api?.listParagraphStyles() ?? []).some((style2) => style2.id === curStyle) ? [{ value: curStyle, label: api?.document.styles.byId.get(curStyle)?.name ?? curStyle }] : []
30562
+ ],
30563
+ onChange: (value) => {
30564
+ if (value) {
30565
+ api?.setParagraphStyle(value === "__normal" ? null : value);
29138
30566
  setCurStyle(api?.getParagraphStyleId?.() ?? null);
29139
30567
  }
29140
- },
29141
- style: { ...selectStyle, width: 92 },
29142
- children: [
29143
- /* @__PURE__ */ jsx("option", { value: "__normal", children: "Normal" }),
29144
- (api?.listParagraphStyles() ?? []).filter((s) => !/^normal$/i.test(s.name)).map((s) => /* @__PURE__ */ jsx("option", { value: s.id, children: s.name }, s.id)),
29145
- curStyle !== null && !(api?.listParagraphStyles() ?? []).some((s) => s.id === curStyle) && /* @__PURE__ */ jsx("option", { value: curStyle, children: api?.document.styles.byId.get(curStyle)?.name ?? curStyle })
29146
- ]
30568
+ }
29147
30569
  }
29148
30570
  )
29149
30571
  });
29150
30572
  if (on("font"))
29151
30573
  groups.push({
29152
30574
  key: "font",
29153
- node: /* @__PURE__ */ jsxs(
29154
- "select",
30575
+ node: /* @__PURE__ */ jsx(
30576
+ ToolbarMenuSelect,
29155
30577
  {
29156
30578
  title: "Font",
29157
30579
  value: fmt?.fontFamily ?? "",
29158
- onMouseDown: (e) => e.stopPropagation(),
29159
- onChange: (e) => e.target.value && apply({ fontFamily: e.target.value }),
29160
- style: { ...selectStyle, width: 130 },
29161
- children: [
29162
- /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: "Font" }),
29163
- (fmt?.fontFamily && !detectFonts().includes(fmt.fontFamily) ? [fmt.fontFamily, ...detectFonts()] : detectFonts()).map((f) => /* @__PURE__ */ jsx("option", { value: f, style: { fontFamily: f }, children: f }, f))
29164
- ]
30580
+ placeholder: "Font",
30581
+ width: 130,
30582
+ menuWidth: 210,
30583
+ options: (fmt?.fontFamily && !detectFonts().includes(fmt.fontFamily) ? [fmt.fontFamily, ...detectFonts()] : detectFonts()).map((font) => ({
30584
+ value: font,
30585
+ label: font,
30586
+ fontFamily: font
30587
+ })),
30588
+ onChange: (value) => value && apply({ fontFamily: value })
29165
30589
  }
29166
30590
  )
29167
30591
  });
@@ -29169,18 +30593,16 @@ function DocxToolbar({
29169
30593
  groups.push({
29170
30594
  key: "size",
29171
30595
  node: /* @__PURE__ */ jsxs(Fragment2, { children: [
29172
- /* @__PURE__ */ jsxs(
29173
- "select",
30596
+ /* @__PURE__ */ jsx(
30597
+ ToolbarMenuSelect,
29174
30598
  {
29175
30599
  title: "Font size",
29176
- value: fmt?.fontSizePt ?? "",
29177
- onMouseDown: (e) => e.stopPropagation(),
29178
- onChange: (e) => e.target.value && apply({ fontSizePt: parseFloat(e.target.value) }),
29179
- style: { ...selectStyle, width: 58 },
29180
- children: [
29181
- /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: "Size" }),
29182
- SIZES.map((s) => /* @__PURE__ */ jsx("option", { value: s, children: s }, s))
29183
- ]
30600
+ value: fmt?.fontSizePt === void 0 ? "" : String(fmt.fontSizePt),
30601
+ placeholder: "Size",
30602
+ width: 58,
30603
+ menuWidth: 92,
30604
+ options: SIZES.map((size) => ({ value: String(size), label: String(size) })),
30605
+ onChange: (value) => value && apply({ fontSizePt: parseFloat(value) })
29184
30606
  }
29185
30607
  ),
29186
30608
  /* @__PURE__ */ jsx(Sep, {})
@@ -29237,19 +30659,15 @@ function DocxToolbar({
29237
30659
  if (on("color"))
29238
30660
  groups.push({
29239
30661
  key: "color",
29240
- node: /* @__PURE__ */ jsxs("label", { title: "Text color", style: { display: "inline-flex", alignItems: "center", cursor: "pointer" }, children: [
29241
- /* @__PURE__ */ jsx("span", { style: { fontSize: 13, borderBottom: `3px solid ${fmt?.color && fmt.color !== "auto" ? fmt.color : "#000"}`, padding: "0 3px", color: T.fg }, children: "A" }),
29242
- /* @__PURE__ */ jsx(
29243
- "input",
29244
- {
29245
- type: "color",
29246
- value: fmt?.color && fmt.color !== "auto" ? fmt.color : "#000000",
29247
- onMouseDown: (e) => e.stopPropagation(),
29248
- onChange: (e) => apply({ color: e.target.value }),
29249
- style: { width: 0, height: 0, opacity: 0, border: "none", padding: 0 }
29250
- }
29251
- )
29252
- ] })
30662
+ node: /* @__PURE__ */ jsx(
30663
+ ColorMenu,
30664
+ {
30665
+ current: fmt?.color && fmt.color !== "auto" ? fmt.color : "#000000",
30666
+ title: "Text color",
30667
+ trigger: /* @__PURE__ */ jsx("span", { style: { fontSize: 13, borderBottom: `3px solid ${fmt?.color && fmt.color !== "auto" ? fmt.color : "#000"}`, padding: "0 3px", color: T.fg }, children: "A" }),
30668
+ onPick: (value) => apply({ color: value })
30669
+ }
30670
+ )
29253
30671
  });
29254
30672
  if (on("highlight"))
29255
30673
  groups.push({
@@ -29822,7 +31240,7 @@ function DocxView({
29822
31240
  comments: showComments,
29823
31241
  onDeleteComment,
29824
31242
  onReplyComment,
29825
- onViewportChange: () => editor?.afterRender()
31243
+ onViewportChange: () => editor?.afterViewportChange()
29826
31244
  }, prev ?? void 0);
29827
31245
  container.scrollTop = scrollTop;
29828
31246
  container.scrollLeft = scrollLeft;
@@ -29921,19 +31339,23 @@ function DocxView({
29921
31339
  "Calibri",
29922
31340
  "Calibri Light",
29923
31341
  "Tahoma",
29924
- "Franklin Gothic Medium",
29925
- // Complex-script faces gate layout too (advances must be real
29926
- // before line breaking): Mangal (Devanagari), Latha (Tamil),
29927
- // Noto Sans Lao Looped (Lao — bundled OFL webfont).
29928
- "Mangal",
29929
- "Latha",
29930
- "Noto Sans Lao Looped"
31342
+ "Franklin Gothic Medium"
29931
31343
  ];
29932
31344
  for (const fam of latin) {
29933
31345
  for (const variant of ["", "italic ", "bold ", "bold italic "]) {
29934
31346
  loads.push(document.fonts.load(`${variant}16px "${fam}"`).catch(() => []));
29935
31347
  }
29936
31348
  }
31349
+ const complex = [
31350
+ ["Mangal", "\u0905"],
31351
+ ["Latha", "\u0B85"],
31352
+ ["Noto Sans Lao Looped", "\u0E81"]
31353
+ ];
31354
+ for (const [fam, sample] of complex) {
31355
+ for (const variant of ["", "bold "]) {
31356
+ loads.push(document.fonts.load(`${variant}16px "${fam}"`, sample).catch(() => []));
31357
+ }
31358
+ }
29937
31359
  loads.push(document.fonts.load('16px "Cambria Math"').catch(() => []));
29938
31360
  const cjk = [
29939
31361
  "MS Mincho",
@@ -29949,7 +31371,7 @@ function DocxView({
29949
31371
  ];
29950
31372
  for (const fam of cjk) {
29951
31373
  for (const variant of ["", "bold "]) {
29952
- loads.push(document.fonts.load(`${variant}16px "${fam}"`).catch(() => []));
31374
+ loads.push(document.fonts.load(`${variant}16px "${fam}"`, "A\u6F22").catch(() => []));
29953
31375
  }
29954
31376
  }
29955
31377
  await Promise.all(loads);
@@ -29996,7 +31418,39 @@ function DocxView({
29996
31418
  else if (formatted.length > 0) editor?.selectRanges(formatted);
29997
31419
  document.dispatchEvent(new CustomEvent("dxw-selection"));
29998
31420
  },
29999
- onStyleShortcut: (styleId) => applyStyleShortcut?.(styleId)
31421
+ onStyleShortcut: (styleId) => applyStyleShortcut?.(styleId),
31422
+ onTextCommand: (command) => {
31423
+ const current = apiRef.current;
31424
+ if (!current) return;
31425
+ if (command === "link") {
31426
+ const anchor = containerRef.current;
31427
+ if (!anchor) return;
31428
+ void requestTextInputDialog(anchor, {
31429
+ title: "Link address",
31430
+ label: "URL",
31431
+ value: current.getLinkAt() ?? "https://"
31432
+ }).then((next) => {
31433
+ if (next !== null) current.setLink(next.trim() || null);
31434
+ });
31435
+ } else if (command === "comment") {
31436
+ const anchor = containerRef.current;
31437
+ if (!anchor) return;
31438
+ void requestTextInputDialog(anchor, {
31439
+ title: "New comment",
31440
+ label: "Comment",
31441
+ submitLabel: "Comment",
31442
+ multiline: true
31443
+ }).then((text) => {
31444
+ if (text?.trim()) current.addComment(text.trim());
31445
+ });
31446
+ } else if (command === "bullet" || command === "number") {
31447
+ current.toggleList(command);
31448
+ } else {
31449
+ current.setAlignment(
31450
+ command === "alignLeft" ? "left" : command === "alignCenter" ? "center" : command === "alignRight" ? "right" : "justify"
31451
+ );
31452
+ }
31453
+ }
30000
31454
  };
30001
31455
  editor = new DocxEditor(editorConfig);
30002
31456
  editor.attach();
@@ -30615,7 +32069,8 @@ function DocxView({
30615
32069
  alignItems: "center"
30616
32070
  },
30617
32071
  children: [
30618
- /* @__PURE__ */ jsx2("span", { style: { font: "600 11.5px system-ui, sans-serif", color: "#5f6368", padding: "0 4px" }, children: "Header & footer" }),
32072
+ /* @__PURE__ */ jsx2("span", { style: { font: "600 11.5px system-ui, sans-serif", color: "#5f6368", padding: "0 4px" }, children: "Header & footer \xB7 repeats on pages" }),
32073
+ hotBtn("Line", "Insert a reusable line in the repeating page layer", () => apiRef.current?.insertShape("line")),
30619
32074
  hotBtn("Page number", "Insert a dynamic page number at the caret", () => apiRef.current?.insertPageNumber("page")),
30620
32075
  hotBtn("Page X of Y", "Insert 'Page X of Y' at the caret", () => apiRef.current?.insertPageNumber("pageOfTotal")),
30621
32076
  hotBtn("Close", "Return to the document body", () => {
@@ -30631,6 +32086,7 @@ export {
30631
32086
  DocxDocument,
30632
32087
  DocxToolbar,
30633
32088
  DocxView,
32089
+ ToolbarMenuSelect,
30634
32090
  layoutDocument,
30635
32091
  printPages,
30636
32092
  renderToDom