wordinweb 0.1.9 → 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: {
@@ -8092,7 +8095,10 @@ var WORD_FONT_METRICS = {
8092
8095
  // baselines 118.80 + n x 20.00; first attempt baked the multiple into the
8093
8096
  // profile and over-pitched every line by 1.08x).
8094
8097
  mangal: { asc: 1.245, desc: 0.4399, gap: 0 },
8095
- 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 }
8096
8102
  };
8097
8103
  var QUARTER_PT_PX = 1 / 3;
8098
8104
  function quantizeQuarterPt(px) {
@@ -9184,6 +9190,7 @@ var JUSTIFY_MAX_COMPRESS = 0.25;
9184
9190
  var JUSTIFY_STRETCH_FACTOR = 0.5;
9185
9191
  var TAMIL_GLYPH_SCALE = 0.735;
9186
9192
  var TAMIL_BASELINE_DY_EM = 0.136;
9193
+ var LAO_GLYPH_SCALE = 1.048;
9187
9194
  var KASHIDA_LOW_EM = 0;
9188
9195
  var KASHIDA_MEDIUM_EM = 0.032;
9189
9196
  var KASHIDA_HIGH_EM = 0.04;
@@ -9538,6 +9545,7 @@ function breakParagraphImpl(doc, measurer, para, contentWidth, fields, numbering
9538
9545
  href: atom.href,
9539
9546
  src: atom.src ? { ...atom.src, offset: atom.src.offset + sliceOff } : void 0,
9540
9547
  metricsFont: atom.metricsFont,
9548
+ metricsStrut: atom.metricsStrut,
9541
9549
  rtl: atom.rtl,
9542
9550
  rtlLevel: levelOf(atom.rtl)
9543
9551
  });
@@ -9661,6 +9669,44 @@ function breakParagraphImpl(doc, measurer, para, contentWidth, fields, numbering
9661
9669
  if (found) target = stop.pos - pre;
9662
9670
  }
9663
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
+ }
9664
9710
  const aligned = (stop.align === "right" || stop.align === "decimal") && !bidiPara;
9665
9711
  if (aligned && curLineWidth > 0 && x > target + 0.75) {
9666
9712
  flush(false, false);
@@ -9926,7 +9972,7 @@ function breakParagraphImpl(doc, measurer, para, contentWidth, fields, numbering
9926
9972
  }
9927
9973
  const padL = bdrPad && (!sameBdrAtom(atoms[ai - 1], atom) || curLineWidth === 0) ? bdrPad : 0;
9928
9974
  x += padL;
9929
- 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 });
9930
9976
  curLineWidth += padL + atom.width + bdrPadR;
9931
9977
  x += atom.width + bdrPadR;
9932
9978
  if (atom.text.trim().length > 0) prevContentCJK = fragIsCJK;
@@ -10083,6 +10129,7 @@ function finishLine(spans, width, props, measurer, fallbackFamily, para, doc, is
10083
10129
  let maxBorderedAscent = 0;
10084
10130
  let maxBorderedDescent = 0;
10085
10131
  let tallestTextIsEa = false;
10132
+ let metricsStrutHeight = 0;
10086
10133
  const consider = (font, imageHeight, objRaise = 0, descShift = 0) => {
10087
10134
  if (imageHeight !== void 0) {
10088
10135
  maxAscent = Math.max(maxAscent, imageHeight + objRaise);
@@ -10123,7 +10170,7 @@ function finishLine(spans, width, props, measurer, fallbackFamily, para, doc, is
10123
10170
  };
10124
10171
  let metricSpans = spans.filter((s) => !(s.text === " " && !s.leader));
10125
10172
  const solidSpans = metricSpans.filter(
10126
- (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)
10127
10174
  );
10128
10175
  if (solidSpans.length > 0) metricSpans = solidSpans;
10129
10176
  if (metricSpans.some((s) => s.numLabel) && metricSpans.some((s) => !s.numLabel)) {
@@ -10156,7 +10203,13 @@ function finishLine(spans, width, props, measurer, fallbackFamily, para, doc, is
10156
10203
  const lineHasObject = metricSpans.some((s) => s.image || s.drawing);
10157
10204
  for (const s of metricSpans) {
10158
10205
  let textMetrics;
10159
- 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) {
10160
10213
  const objectMetrics = measurer.metrics(s.metricsFont ?? s.font);
10161
10214
  const objectHeight = s.image?.height ?? s.drawing.height;
10162
10215
  const objRaise = s.props.raise ?? 0;
@@ -10211,6 +10264,14 @@ function finishLine(spans, width, props, measurer, fallbackFamily, para, doc, is
10211
10264
  const commonLower = metricSpans.length > 0 && metricSpans.every(
10212
10265
  (s) => !s.image && !s.drawing && !s.math && (s.props.raise ?? 0) < 0
10213
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
+ }
10214
10275
  const natural = Math.max(maxNatural, maxAscent + maxDescent, minLineHeight ?? 0);
10215
10276
  let height = natural;
10216
10277
  let baselineH;
@@ -10371,11 +10432,22 @@ function resolveBidiRtl(text) {
10371
10432
  function buildAtoms(doc, para, measurer, fields, fallbackFamily, gridPitch) {
10372
10433
  const atoms = [];
10373
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
+ });
10374
10443
  const bidiPara = doc.effectiveParaProps(para).bidi === true;
10375
10444
  const kashidaKind = doc.effectiveParaProps(para).justifyKind;
10376
10445
  const kashidaEm = kashidaKind === "highKashida" ? KASHIDA_HIGH_EM : kashidaKind === "mediumKashida" ? KASHIDA_MEDIUM_EM : kashidaKind === "lowKashida" ? KASHIDA_LOW_EM : 0;
10377
10446
  const pushRun = (run, href) => {
10378
- 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
+ }
10379
10451
  if (baseProps.vanish) return;
10380
10452
  const font = fontOf(baseProps, fallbackFamily);
10381
10453
  let props = kashidaEm > 0 && baseProps.rtl ? { ...baseProps, letterSpacing: (baseProps.letterSpacing ?? 0) + kashidaEm * font.size } : baseProps;
@@ -10445,6 +10517,20 @@ function buildAtoms(doc, para, measurer, fields, fallbackFamily, gridPitch) {
10445
10517
  });
10446
10518
  break;
10447
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
+ }
10448
10534
  if (text) pushStyled(displayText(text, props), props, font, href, { run, t: null, offset: 0 }, vertMetricsFont);
10449
10535
  break;
10450
10536
  }
@@ -10489,7 +10575,7 @@ function buildAtoms(doc, para, measurer, fields, fallbackFamily, gridPitch) {
10489
10575
  break;
10490
10576
  }
10491
10577
  case "noteRef": {
10492
- 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) ?? "";
10493
10579
  if (!text) break;
10494
10580
  const markProps = props.verticalAlign ? props : { ...props, verticalAlign: "superscript" };
10495
10581
  const markFont = fontOf(markProps, fallbackFamily);
@@ -10545,6 +10631,7 @@ function buildAtoms(doc, para, measurer, fields, fallbackFamily, gridPitch) {
10545
10631
  const hasLao = /[຀-໿]/.test(text);
10546
10632
  const indicFace = hasDeva && !hasTamil && !hasLao ? "Mangal" : hasTamil && !hasDeva && !hasLao ? "Latha" : hasLao && !hasDeva && !hasTamil ? "Noto Sans Lao Looped" : null;
10547
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;
10548
10635
  const indicScale = indicFace === "Latha" ? TAMIL_GLYPH_SCALE : 1;
10549
10636
  const indicFont = indicScale === 1 ? { ...font, family: indicFace } : {
10550
10637
  ...font,
@@ -10564,7 +10651,7 @@ function buildAtoms(doc, para, measurer, fields, fallbackFamily, gridPitch) {
10564
10651
  const seg = text.slice(i2, j);
10565
10652
  const src = srcBase ? { ...srcBase, offset: srcBase.offset + i2 } : void 0;
10566
10653
  if (isSpace) pushText(seg, props, font, href, src, metricsFont);
10567
- else pushText(seg, props, indicFont, href, src, indicMetrics);
10654
+ else pushText(seg, scriptProps, indicFont, href, src, indicMetrics);
10568
10655
  i2 = j;
10569
10656
  }
10570
10657
  return;
@@ -10787,8 +10874,8 @@ function resolveField(instruction, cachedResult, ctx, fieldKey) {
10787
10874
  }
10788
10875
  case "SEQ": {
10789
10876
  const ident = instr.split(/\s+/)[1];
10790
- if (ident && ctx.seq && fieldKey) return ctx.seq(ident, fieldKey, instr);
10791
- return cachedResult || "";
10877
+ const value = ident && ctx.seq && fieldKey ? ctx.seq(ident, fieldKey, instr) : cachedResult || "";
10878
+ return /\\h(\s|$)/i.test(instr) ? "" : value;
10792
10879
  }
10793
10880
  case "REF": {
10794
10881
  const bookmark = instr.split(/\s+/)[1];
@@ -11011,6 +11098,7 @@ var MULTI_COL_NOTE_SEP_RESERVE = 26;
11011
11098
  var NOTE_SEP_LEN = 192;
11012
11099
  var NOTE_SEP_GAP = 4.4;
11013
11100
  var ROW_OVERHANG_TOL = 3;
11101
+ var CUSTOM_NOTE_BANNER_OVERHANG = 3;
11014
11102
  var CHICAGO = ["*", "\u2020", "\u2021", "\xA7"];
11015
11103
  var AUTO_PARA_SPACING_PX = 14 * (96 / 72);
11016
11104
  var FRAME_WRAP_HSPACE_PX = 6 * (96 / 72);
@@ -11118,6 +11206,7 @@ var Engine = class {
11118
11206
  /** Vertical flow already consumed in the current later-column pre-banner
11119
11207
  * slot. It reduces the below-banner capacity by the same amount. */
11120
11208
  this.bannerSlotUsed = 0;
11209
+ this.customNoteBannerFit = false;
11121
11210
  /** True while a section-level tbRl vertical flow is being laid: paragraphs
11122
11211
  * re-establish the vertical grid after embedded Western runs (see
11123
11212
  * breakParagraph's verticalGridResync). */
@@ -11235,7 +11324,7 @@ var Engine = class {
11235
11324
  const contentWidth = page.sp.pageWidth - page.sp.marginLeft - page.sp.marginRight - page.sp.gutter;
11236
11325
  const header = this.doc.headers.get(page.headerRel ?? "");
11237
11326
  const footer = this.doc.footers.get(page.footerRel ?? "");
11238
- const headerHeight = this.measureHeaderFooter(header, page, contentWidth, this.pageFieldFrameOverlay(header));
11327
+ const headerHeight = this.measureHeaderFooter(header, page, contentWidth, this.pageFieldFrameOverlay(header), true);
11239
11328
  const footerHeight = this.measureHeaderFooter(footer, page, contentWidth, this.pageFieldFrameOverlay(footer));
11240
11329
  if (headerHeight !== page.headerHeight || footerHeight !== page.footerHeight) return null;
11241
11330
  }
@@ -11335,6 +11424,7 @@ var Engine = class {
11335
11424
  if (this.pages.length === 0) {
11336
11425
  this.sp = sections[0]?.props ?? {};
11337
11426
  }
11427
+ this.applyOpeningFlowOverlap();
11338
11428
  this.placeEndnotes();
11339
11429
  this.emitFootnoteAreas();
11340
11430
  this.emitColumnSeparators();
@@ -11363,6 +11453,43 @@ var Engine = class {
11363
11453
  }
11364
11454
  return result;
11365
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
+ }
11366
11493
  /** Whether this document is in the envelope the incremental path handles:
11367
11494
  * one section, no notes, single column, upright flow, no line numbering /
11368
11495
  * mirror margins / vertical alignment, and no positioned/floating content or
@@ -11621,7 +11748,7 @@ var Engine = class {
11621
11748
  const contentWidth = shifted.sp.pageWidth - shifted.sp.marginLeft - shifted.sp.marginRight - shifted.sp.gutter;
11622
11749
  const header = this.doc.headers.get(headerRel ?? "");
11623
11750
  const footer = this.doc.footers.get(footerRel ?? "");
11624
- const headerHeight = this.measureHeaderFooter(header, shifted, contentWidth, this.pageFieldFrameOverlay(header));
11751
+ const headerHeight = this.measureHeaderFooter(header, shifted, contentWidth, this.pageFieldFrameOverlay(header), true);
11625
11752
  const footerHeight = this.measureHeaderFooter(footer, shifted, contentWidth, this.pageFieldFrameOverlay(footer));
11626
11753
  if (headerHeight !== page.headerHeight || footerHeight !== page.footerHeight) return null;
11627
11754
  shifted.headerHeight = headerHeight;
@@ -11999,8 +12126,8 @@ var Engine = class {
11999
12126
  displayNumber,
12000
12127
  headerHeight: 0,
12001
12128
  footerHeight: 0,
12002
- bodyTop: Math.abs(sp.marginTop),
12003
- bandTop: Math.abs(sp.marginTop),
12129
+ bodyTop: sp.marginTop,
12130
+ bandTop: sp.marginTop,
12004
12131
  softTop: !sectionStart,
12005
12132
  bodyBottom: sp.pageHeight - Math.abs(sp.marginBottom),
12006
12133
  colXs,
@@ -12024,13 +12151,17 @@ var Engine = class {
12024
12151
  }
12025
12152
  const header = this.doc.headers.get(page.headerRel ?? "");
12026
12153
  const footer = this.doc.footers.get(page.footerRel ?? "");
12027
- const headerH = this.measureHeaderFooter(header, page, contentWidth, this.pageFieldFrameOverlay(header));
12154
+ const headerH = this.measureHeaderFooter(header, page, contentWidth, this.pageFieldFrameOverlay(header), true);
12028
12155
  const footerOverlay = this.pageFieldFrameOverlay(footer);
12029
12156
  const footerH = this.measureHeaderFooter(footer, page, contentWidth, footerOverlay);
12030
12157
  page.headerHeight = headerH;
12031
12158
  page.footerHeight = footerH;
12032
12159
  if (sp.marginTop >= 0) {
12033
- 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
+ );
12034
12165
  page.bandTop = page.bodyTop;
12035
12166
  page.headerGrown = page.bodyTop > sp.marginTop;
12036
12167
  }
@@ -12270,7 +12401,7 @@ var Engine = class {
12270
12401
  if (this.col + 1 < this.cur.colXs.length) {
12271
12402
  this.col++;
12272
12403
  this.y = this.columnStartY(this.col);
12273
- this.bannerSlotUsed = 0;
12404
+ this.bannerSlotUsed = this.cur.bannerTop !== void 0 ? this.cur.openingColumnReserve ?? 0 : 0;
12274
12405
  this.lastParaSpacingAfter = 0;
12275
12406
  this.lastParaAfterPad = 0;
12276
12407
  this.lastParaAfterPad = 0;
@@ -12607,7 +12738,7 @@ var Engine = class {
12607
12738
  const runs = c.type === "run" ? [c] : c.runs;
12608
12739
  for (const r of runs) {
12609
12740
  for (const rc of r.content) {
12610
- if (rc.kind !== "noteRef" || rc.self) continue;
12741
+ if (rc.kind !== "noteRef" || rc.self || rc.customMarkFollows) continue;
12611
12742
  if (rc.noteType === "footnote" && this.doc.footnotes.has(rc.id) && !this.footnoteNumbers.has(rc.id)) {
12612
12743
  this.footnoteNumbers.set(rc.id, fnStart + fn++);
12613
12744
  } else if (rc.noteType === "endnote" && this.doc.endnotes.has(rc.id) && !this.endnoteNumbers.has(rc.id)) {
@@ -13120,6 +13251,20 @@ var Engine = class {
13120
13251
  }
13121
13252
  placeParagraph(para, prev, next, siblings, index) {
13122
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
+ }
13123
13268
  if (this.consumedFrames.has(para)) return;
13124
13269
  if (props.frame && !props.dropCap && this.cur.physIndex !== -1) {
13125
13270
  const fr = this.resolveFrame(props.frame);
@@ -13252,12 +13397,18 @@ var Engine = class {
13252
13397
  if (breakBeforeForced && !(isLeadingPageBreak && keepSpBeforeAtPageTop)) dropSpaceBefore = true;
13253
13398
  if (this.docGridDropBefore) {
13254
13399
  if (props.snapToGrid === false && this.sp.docGridLinePitch) {
13255
- const reduction = 2 * this.sp.docGridLinePitch;
13400
+ const reduction = (props.spacingBefore ?? 0) > 0 ? 4 * this.sp.docGridLinePitch : 2 * this.sp.docGridLinePitch;
13256
13401
  this.cur.bodyTop -= reduction;
13257
13402
  this.y -= reduction;
13403
+ } else if (para.props.styleId === "Heading1" && this.sp.docGridLinePitch) {
13404
+ const reduction = 3 * this.sp.docGridLinePitch - ptToPx(1.5);
13405
+ this.cur.bodyTop -= reduction;
13406
+ this.y -= reduction;
13407
+ dropSpaceBefore = true;
13408
+ } else {
13409
+ dropSpaceBefore = true;
13258
13410
  }
13259
13411
  this.docGridDropBefore = false;
13260
- dropSpaceBefore = true;
13261
13412
  }
13262
13413
  const rawSpacingBefore = dropSpaceBefore ? 0 : props.spacingBefore ?? 0;
13263
13414
  let paraTopEstimate = this.y + rawSpacingBefore;
@@ -13431,7 +13582,7 @@ var Engine = class {
13431
13582
  const nextProps = nextPara ? this.doc.effectiveParaProps(nextPara) : void 0;
13432
13583
  const nextLeadBreak = nextPara ? leadingBreakOf(nextPara) : void 0;
13433
13584
  let legacyBreakChain = false;
13434
- 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") {
13435
13586
  const breakProps = this.doc.effectiveRunProps(nextPara, nextLeadBreak.run.props);
13436
13587
  const breakFont = fontOf(breakProps, this.doc.styles.defaultRPr.font ?? "Calibri");
13437
13588
  let breakHeight = this.measurer.metrics(breakFont).lineHeight;
@@ -13602,6 +13753,7 @@ var Engine = class {
13602
13753
  let simCol = this.col;
13603
13754
  let simOnCurrentPage = true;
13604
13755
  let simBannerUsed = this.bannerSlotUsed;
13756
+ const paragraphOverhang = this.customNoteBannerFit ? CUSTOM_NOTE_BANNER_OVERHANG : 0;
13605
13757
  const updateBottom = () => {
13606
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);
13607
13759
  };
@@ -13624,16 +13776,17 @@ var Engine = class {
13624
13776
  }
13625
13777
  };
13626
13778
  const nextSimColumn = () => {
13627
- simBannerUsed = 0;
13628
13779
  simNotes = 0;
13629
13780
  simNoted.clear();
13630
13781
  if (simCol + 1 < this.cur.colXs.length) {
13631
13782
  simCol++;
13632
13783
  simY = simOnCurrentPage ? this.columnStartY(simCol) : this.cur.bodyTop;
13784
+ simBannerUsed = simOnCurrentPage && this.cur.bannerTop !== void 0 ? this.cur.openingColumnReserve ?? 0 : 0;
13633
13785
  } else {
13634
13786
  simCol = 0;
13635
13787
  simOnCurrentPage = false;
13636
13788
  simY = this.cur.bodyTop;
13789
+ simBannerUsed = 0;
13637
13790
  }
13638
13791
  updateBottom();
13639
13792
  };
@@ -13652,7 +13805,7 @@ var Engine = class {
13652
13805
  const noteAdd = lineNoteHeights(lines[li]);
13653
13806
  const baseNotes = simOnCurrentPage ? this.cur.footnoteH[simCol] ?? 0 : 0;
13654
13807
  const simSep = simNotes + noteAdd > 0 && baseNotes === 0 ? this.noteSeparatorReserve(this.cur) : 0;
13655
- 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);
13656
13809
  if (overflowsHere && li === segStart && segStart === 0 && onPartialPage && !simBalancing) {
13657
13810
  breaks2.add(0);
13658
13811
  segStart = 0;
@@ -13738,7 +13891,7 @@ var Engine = class {
13738
13891
  const pendingNotes = this.pendingNoteHeight(line2);
13739
13892
  const balancing = this.balanceBottom !== void 0 && this.col + 1 < this.cur.colXs.length;
13740
13893
  const balBottomBased = balancing && this.colWidth < 40;
13741
- 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();
13742
13895
  if ((planned || overflow) && li > fragStartLine) {
13743
13896
  closeFragment(li, false);
13744
13897
  this.nextColumn();
@@ -14406,13 +14559,35 @@ var Engine = class {
14406
14559
  y = Math.max(y, pendingPageFrame.bottom);
14407
14560
  pendingPageFrame = null;
14408
14561
  }
14409
- y = this.layoutTableInFrame(block, fake, 0, y, width, fields);
14562
+ y = this.layoutTableInFrame(block, fake, 0, y, width, fields, inCell === true);
14410
14563
  framePrevAfter = 0;
14411
14564
  }
14412
14565
  }
14413
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
+ );
14414
14585
  this.floats.delete(fake);
14415
- return { items, height: y + pageFramePhantomH };
14586
+ return {
14587
+ items,
14588
+ height: y + pageFramePhantomH,
14589
+ contentBottom: Math.max(y + pageFramePhantomH, paintedHeaderBottom, wrappedBottom)
14590
+ };
14416
14591
  }
14417
14592
  /**
14418
14593
  * Emit floating shapes anchored at (textX, textY). Coordinates in the shape
@@ -14818,7 +14993,7 @@ var Engine = class {
14818
14993
  page.items.push(it);
14819
14994
  }
14820
14995
  }
14821
- measureHeaderFooter(hf, page, contentWidth, overlayPageFrame = false) {
14996
+ measureHeaderFooter(hf, page, contentWidth, overlayPageFrame = false, reserveBodyClearance = false) {
14822
14997
  if (!hf || hf.blocks.length === 0) return 0;
14823
14998
  const fields = {
14824
14999
  pageNumber: () => page.displayNumber,
@@ -14827,10 +15002,25 @@ var Engine = class {
14827
15002
  };
14828
15003
  const snapshot = new Map(Array.from(this.counters, ([k, v]) => [k, [...v]]));
14829
15004
  const seenSnapshot = new Set(this.seenNumIds);
14830
- 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
+ );
14831
15014
  this.counters = snapshot;
14832
15015
  this.seenNumIds = seenSnapshot;
14833
- 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;
14834
15024
  }
14835
15025
  pageFieldFrameOverlay(hf) {
14836
15026
  if (!hf) return false;
@@ -14892,7 +15082,10 @@ var Engine = class {
14892
15082
  const measured = this.layoutFrame(footer.blocks, contentWidth, fields, void 0, false, overlayPageFrame);
14893
15083
  this.counters = snapshot;
14894
15084
  this.seenNumIds = seenSnapshot;
14895
- 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);
14896
15089
  snapshot = new Map(Array.from(this.counters, ([k, v]) => [k, [...v]]));
14897
15090
  seenSnapshot = new Set(this.seenNumIds);
14898
15091
  this.hfMarginVTop = page.bodyTop;
@@ -15271,7 +15464,14 @@ var Engine = class {
15271
15464
  if (cell.props.margins?.top !== void 0) topPad = Math.max(topPad, cell.props.margins.top);
15272
15465
  if (cell.props.margins?.bottom !== void 0) bottomPad = Math.max(bottomPad, cell.props.margins.bottom);
15273
15466
  }
15274
- 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
+ }
15275
15475
  const borderPad = this.rowBorderShare(tbl, ri);
15276
15476
  return Math.max(contentHeight, trHeight + topPad + bottomPad + borderPad);
15277
15477
  }
@@ -15491,7 +15691,7 @@ var Engine = class {
15491
15691
  };
15492
15692
  let guard = 0;
15493
15693
  const noteReserve = this.rowNoteHeight(laid) + this.footnoteReserve(this.cur, this.col);
15494
- 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;
15495
15695
  while (this.y + rowHeight > this.bodyBottom - this.rowNoteHeight(laid) + overhang + 0.01 && guard++ < 50) {
15496
15696
  const atColumnTop = this.pageIsEmptyAtCursor() || this.y <= this.cur.bodyTop + this.rowBorderWidths(tbl, ri).top / 2 + 0.01;
15497
15697
  const cantSplitHolds = row.props.cantSplit === true && (rowHeight <= this.cur.bodyBottom - this.cur.bodyTop + 0.01 || !atColumnTop);
@@ -15710,9 +15910,9 @@ var Engine = class {
15710
15910
  });
15711
15911
  }
15712
15912
  }
15713
- layoutTableInFrame(tbl, fake, x0, y, width, fields) {
15913
+ layoutTableInFrame(tbl, fake, x0, y, width, fields, nested) {
15714
15914
  this.ensureTableBorders(tbl);
15715
- const widths = this.resolveGridWidths(tbl, width, true);
15915
+ const widths = this.resolveGridWidths(tbl, width, nested);
15716
15916
  const saveY = this.y;
15717
15917
  const saveCur = this.cur;
15718
15918
  const saveCol = this.col;
@@ -15978,6 +16178,7 @@ var Engine = class {
15978
16178
  }
15979
16179
  let maxH = 0;
15980
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;
15981
16182
  for (let ci = 0; ci < row.cells.length; ci++) {
15982
16183
  const cell = row.cells[ci];
15983
16184
  const { x, width: w, margins: m } = geometry[ci];
@@ -15997,7 +16198,7 @@ var Engine = class {
15997
16198
  true
15998
16199
  );
15999
16200
  for (const it of items) offsetItem(it, m.left ?? 0, m.top ?? 0);
16000
- const cellHeight = height + (m.top ?? 0) + (m.bottom ?? 0);
16201
+ const cellHeight = height + (m.top ?? 0) + effectiveBottomPad(m.bottom);
16001
16202
  cells[ci] = { items, height: cellHeight, x, width: w, cellIdx: ci };
16002
16203
  measuredContentH = Math.max(measuredContentH, height);
16003
16204
  maxH = Math.max(maxH, cellHeight);
@@ -16041,7 +16242,7 @@ var Engine = class {
16041
16242
  it.rotate = { deg, ox: centerX - it.x, oy: centerY - top };
16042
16243
  }
16043
16244
  }
16044
- const cellHeight = frameWidth + (m.top ?? 0) + (m.bottom ?? 0);
16245
+ const cellHeight = frameWidth + (m.top ?? 0) + effectiveBottomPad(m.bottom);
16045
16246
  cells[ci] = { items, height: cellHeight, x, width: w, cellIdx: ci, rotated: true };
16046
16247
  maxH = Math.max(maxH, cellHeight);
16047
16248
  }
@@ -16190,6 +16391,24 @@ var Engine = class {
16190
16391
  }
16191
16392
  }
16192
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
+ }
16193
16412
  function isEmptyParagraph(p) {
16194
16413
  for (const child4 of p.children) {
16195
16414
  const runs = child4.type === "run" ? [child4] : child4.runs;
@@ -17957,6 +18176,11 @@ function renderItem(doc, item, urls) {
17957
18176
  hit.dataset.dxwDrawing = "1";
17958
18177
  if (item.ink) hit.dataset.dxwInk = "1";
17959
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
+ }
17960
18184
  return hit;
17961
18185
  }
17962
18186
  case "edge": {
@@ -18416,6 +18640,10 @@ function renderText(item) {
18416
18640
  el16.style.alignItems = "flex-end";
18417
18641
  el16.style.whiteSpace = "pre";
18418
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
+ }
18419
18647
  }
18420
18648
  if (item.rtl) {
18421
18649
  el16.style.direction = "rtl";
@@ -18479,7 +18707,8 @@ function renderEdge(x1, y1, x2, y2, border, rotate) {
18479
18707
  }
18480
18708
  const horizontal = Math.abs(y2 - y1) < 0.01;
18481
18709
  const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
18482
- 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);
18483
18712
  const snap = (v) => Math.round(v * dpr) / dpr;
18484
18713
  const place = border.space === 0 ? snap : (v) => v;
18485
18714
  const dashPattern = border.style === "dashed" || border.style === "dotDash" || border.style === "dotDotDash" ? [3, 1] : border.style === "dotted" ? [1, 1] : null;
@@ -20700,23 +20929,44 @@ function insertShapeAt(doc, caretT, preset, text = "") {
20700
20929
  if (!caretRun || !parent || localName(caretRun.name) !== "r") return null;
20701
20930
  const w = prefixOf8(caretRun);
20702
20931
  const id = String(doc.nextDrawingId());
20703
- const width = preset === "textBox" ? 240 : 192;
20704
- 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;
20705
20935
  const cx = String(Math.round(width * EMU_PER_PX4));
20706
20936
  const cy = String(Math.round(height * EMU_PER_PX4));
20707
20937
  const geometry = preset === "roundedRectangle" ? "roundRect" : preset === "rectangle" || preset === "textBox" ? "rect" : preset;
20708
20938
  const isTextBox = preset === "textBox";
20709
- const shapeName = isTextBox ? `Text Box ${id}` : `Shape ${id}`;
20939
+ const shapeName = isLine ? `Line ${id}` : isTextBox ? `Text Box ${id}` : `Shape ${id}`;
20710
20940
  const spPr = [
20711
- 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
+ ]),
20712
20945
  el6("a:prstGeom", { prst: geometry }, [el6("a:avLst")]),
20713
- isTextBox ? el6("a:noFill") : el6("a:solidFill", {}, [el6("a:srgbClr", { val: "4472C4" })]),
20714
- 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" })])])
20715
20948
  ];
20716
20949
  const textRun = el6(`${w}r`, {}, [
20717
20950
  el6(`${w}rPr`, {}, [el6(`${w}color`, { [`${w}val`]: isTextBox ? "202124" : "FFFFFF" }), el6(`${w}sz`, { [`${w}val`]: "22" })]),
20718
20951
  el6(`${w}t`, { "xml:space": "preserve" }, [], text)
20719
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
+ ];
20720
20970
  const drawing = el6(`${w}drawing`, {}, [
20721
20971
  el6("wp:anchor", {
20722
20972
  "xmlns:wp": NS_WP3,
@@ -20736,24 +20986,12 @@ function insertShapeAt(doc, caretT, preset, text = "") {
20736
20986
  el6("wp:positionV", { relativeFrom: "paragraph" }, [el6("wp:posOffset", {}, [], "0")]),
20737
20987
  el6("wp:extent", { cx, cy }),
20738
20988
  el6("wp:effectExtent", { l: "0", t: "0", r: "0", b: "0" }),
20739
- el6("wp:wrapSquare", { wrapText: "bothSides" }),
20989
+ isLine ? el6("wp:wrapNone") : el6("wp:wrapSquare", { wrapText: "bothSides" }),
20740
20990
  el6("wp:docPr", { id, name: shapeName }),
20741
20991
  el6("wp:cNvGraphicFramePr"),
20742
20992
  el6("a:graphic", { "xmlns:a": NS_A3 }, [
20743
20993
  el6("a:graphicData", { uri: NS_WPS }, [
20744
- el6("wps:wsp", { "xmlns:wps": NS_WPS }, [
20745
- el6("wps:cNvSpPr", { txBox: "1" }),
20746
- el6("wps:spPr", {}, spPr),
20747
- el6("wps:txbx", {}, [
20748
- el6(`${w}txbxContent`, {}, [
20749
- el6(`${w}p`, {}, [
20750
- el6(`${w}pPr`, {}, [el6(`${w}jc`, { [`${w}val`]: isTextBox ? "left" : "center" })]),
20751
- textRun
20752
- ])
20753
- ])
20754
- ]),
20755
- el6("wps:bodyPr", { rot: "0", anchor: isTextBox ? "t" : "ctr" }, [el6("a:noAutofit")])
20756
- ])
20994
+ el6("wps:wsp", { "xmlns:wps": NS_WPS }, shapeChildren)
20757
20995
  ])
20758
20996
  ])
20759
20997
  ])
@@ -20815,6 +21053,25 @@ function setDrawingWordArtText(doc, drawing, text) {
20815
21053
  doc.refresh();
20816
21054
  return true;
20817
21055
  }
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
+ }
20818
21075
  function insertInkAt(doc, caretT, points, color = "#202124", widthPx = 2, opacity = 1, refreshModel = true) {
20819
21076
  if (points.length < 2) return null;
20820
21077
  const caretRun = doc.findParentOf(caretT);
@@ -21223,6 +21480,111 @@ function deleteWatermark(doc, shapeEl) {
21223
21480
  return true;
21224
21481
  }
21225
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
+
21226
21588
  // ../core/src/edit/grapheme.ts
21227
21589
  var segmenter = null;
21228
21590
  var segmenterTried = false;
@@ -21936,6 +22298,7 @@ var _DocxEditor = class _DocxEditor {
21936
22298
  * snapshot that the following text insertion belongs to. */
21937
22299
  this.pendingClickTypeCheckpoint = false;
21938
22300
  this.clickTypeCheckpointUntil = 0;
22301
+ this.textContextMenu = null;
21939
22302
  /** True while a drag-selection is in progress (checked by onMouseUp, which
21940
22303
  * bubbles from the container BEFORE the document-level drag-end listener). */
21941
22304
  this.dragSelecting = false;
@@ -22325,6 +22688,102 @@ var _DocxEditor = class _DocxEditor {
22325
22688
  };
22326
22689
  /** Which part tree the element lives in: document body or header/footer. */
22327
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
+ };
22328
22787
  // ---------- keyboard ----------
22329
22788
  this.onKeyDown = (e) => {
22330
22789
  if (this.composing || e.isComposing || e.keyCode === 229) return;
@@ -22332,6 +22791,11 @@ var _DocxEditor = class _DocxEditor {
22332
22791
  this.pendingClickTypeCheckpoint = false;
22333
22792
  this.clickTypeCheckpointUntil = 0;
22334
22793
  }
22794
+ if (this.textContextMenu) {
22795
+ if (e.key === "Escape") e.preventDefault();
22796
+ this.dismissTextContextMenu();
22797
+ if (e.key === "Escape") return;
22798
+ }
22335
22799
  if (e.key === "Escape" && this.drawingTool) {
22336
22800
  e.preventDefault();
22337
22801
  this.setDrawingTool(null);
@@ -22446,6 +22910,12 @@ var _DocxEditor = class _DocxEditor {
22446
22910
  }
22447
22911
  }
22448
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
+ }
22449
22919
  if (meta && e.altKey && /^[0-6]$/.test(e.key)) {
22450
22920
  e.preventDefault();
22451
22921
  this.host.onStyleShortcut?.(e.key === "0" ? null : `Heading${e.key}`);
@@ -22465,6 +22935,28 @@ var _DocxEditor = class _DocxEditor {
22465
22935
  }
22466
22936
  return;
22467
22937
  }
22938
+ if (!e.shiftKey && k === "k") {
22939
+ e.preventDefault();
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
+ }
22468
22960
  if (e.key.startsWith("Arrow")) {
22469
22961
  e.preventDefault();
22470
22962
  const extend = e.shiftKey;
@@ -23100,6 +23592,7 @@ var _DocxEditor = class _DocxEditor {
23100
23592
  c.addEventListener("copy", this.onCopy);
23101
23593
  c.addEventListener("cut", this.onCut);
23102
23594
  c.addEventListener("paste", this.onPaste);
23595
+ c.addEventListener("contextmenu", this.onContextMenu);
23103
23596
  c.addEventListener("dragover", this.onDragOver);
23104
23597
  c.addEventListener("drop", this.onDrop);
23105
23598
  this.applyHfChrome();
@@ -23119,8 +23612,10 @@ var _DocxEditor = class _DocxEditor {
23119
23612
  c.removeEventListener("copy", this.onCopy);
23120
23613
  c.removeEventListener("cut", this.onCut);
23121
23614
  c.removeEventListener("paste", this.onPaste);
23615
+ c.removeEventListener("contextmenu", this.onContextMenu);
23122
23616
  c.removeEventListener("dragover", this.onDragOver);
23123
23617
  c.removeEventListener("drop", this.onDrop);
23618
+ this.dismissTextContextMenu();
23124
23619
  this.dismissSuggestionPopover();
23125
23620
  this.setDrawingTool(null);
23126
23621
  this.deselectInkGroup();
@@ -23990,23 +24485,73 @@ var _DocxEditor = class _DocxEditor {
23990
24485
  };
23991
24486
  extra("Alt", "Alternative text", () => {
23992
24487
  const cur = imageAltText(src);
23993
- const next = window.prompt("Alternative text", cur);
23994
- if (next === null) return;
23995
- this.host.history?.checkpoint();
23996
- if (setImageAltText(this.host.doc, src, next)) this.host.rerender();
23997
- 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
+ });
23998
24498
  });
23999
24499
  extra("Size", "Exact size (px)", () => {
24000
24500
  const curW = parseFloat(el16.style.width) || 0;
24001
24501
  const curH = parseFloat(el16.style.height) || 0;
24002
24502
  const cur = `${Math.round(curW)}x${Math.round(curH)}`;
24003
- const next = window.prompt("Size in px (width x height)", cur);
24004
- const m = next && /^\s*(\d+)\s*[x×]\s*(\d+)\s*$/i.exec(next);
24005
- if (!m) return;
24006
- this.host.history?.checkpoint();
24007
- if (resizeDrawing(this.host.doc, src, parseInt(m[1], 10), parseInt(m[2], 10))) this.host.rerender();
24008
- 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
+ });
24009
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
+ }
24010
24555
  if (kind === "drawing" && isDrawingWordArt(src)) {
24011
24556
  extra("Edit text", "Edit WordArt text", () => this.editDrawingWordArt(src));
24012
24557
  }
@@ -24160,13 +24705,17 @@ var _DocxEditor = class _DocxEditor {
24160
24705
  };
24161
24706
  button("Edit text", "Edit watermark text", () => {
24162
24707
  const cur = wordArtText(src);
24163
- const next = window.prompt("Watermark text", cur);
24164
- if (next === null || next === cur) return;
24165
- this.host.history?.checkpoint();
24166
- if (setWordArtText(this.host.doc, src, next)) {
24167
- this.host.rerender();
24168
- this.reselectWordArt(src);
24169
- }
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
+ });
24170
24719
  });
24171
24720
  const sep1 = document.createElement("span");
24172
24721
  sep1.style.cssText = "width:1px;background:#dadce0;margin:2px 2px;";
@@ -24178,15 +24727,20 @@ var _DocxEditor = class _DocxEditor {
24178
24727
  bar.appendChild(sep2);
24179
24728
  button("Rotate", "Set rotation (degrees)", () => {
24180
24729
  const cur = wordArtRotation(src);
24181
- const next = window.prompt("Rotation (degrees clockwise)", String(cur));
24182
- if (next === null) return;
24183
- const deg = parseFloat(next);
24184
- if (!Number.isFinite(deg)) return;
24185
- this.host.history?.checkpoint();
24186
- if (setWordArtRotation(this.host.doc, src, deg)) {
24187
- this.host.rerender();
24188
- this.reselectWordArt(src);
24189
- }
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
+ });
24190
24744
  });
24191
24745
  const sep3 = document.createElement("span");
24192
24746
  sep3.style.cssText = "width:1px;background:#dadce0;margin:2px 2px;";
@@ -25034,16 +25588,19 @@ var _DocxEditor = class _DocxEditor {
25034
25588
  };
25035
25589
  const split = this.splitParagraphCore();
25036
25590
  if (!split) return null;
25037
- const afterSources = [split.after];
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];
25038
25594
  const w = split.after.name.includes(":") ? split.after.name.slice(0, split.after.name.indexOf(":") + 1) : "";
25039
25595
  const templatePPr = split.after.children.find((child4) => localName(child4.name) === "pPr");
25040
25596
  const templateRun = split.after.children.find(
25041
25597
  (child4) => localName(child4.name) === "r" && child4.children.includes(this.caret.t)
25042
25598
  );
25043
25599
  const templateRPr = templateRun?.children.find((child4) => localName(child4.name) === "rPr");
25044
- let previous = split.after;
25600
+ let previous = insertBeforeTrailingBreak ? split.before : split.after;
25601
+ let caretSource = split.after;
25045
25602
  for (let i = 1; i < lineCount; i++) {
25046
- if (this.suggesting) markParagraphGlyph(previous, "ins", this.revMeta());
25603
+ if (this.suggesting && previous !== split.before) markParagraphGlyph(previous, "ins", this.revMeta());
25047
25604
  const t = {
25048
25605
  name: this.caret.t.name,
25049
25606
  attrs: { ...this.caret.t.attrs, "xml:space": "preserve" },
@@ -25066,11 +25623,14 @@ var _DocxEditor = class _DocxEditor {
25066
25623
  parent.children.splice(previousIndex + 1, 0, paragraph2);
25067
25624
  afterSources.push(paragraph2);
25068
25625
  previous = paragraph2;
25626
+ caretSource = paragraph2;
25069
25627
  this.caret = { t, run: caret.run, offset: 0 };
25070
25628
  }
25629
+ if (insertBeforeTrailingBreak) afterSources.push(split.after);
25071
25630
  const reparsed = this.host.doc.reparseDirectBodyParagraphSplits(split.before, afterSources);
25072
25631
  if (reparsed) {
25073
- for (const child4 of reparsed[reparsed.length - 1].children) {
25632
+ const caretParagraph = reparsed[afterSources.indexOf(caretSource) + 1];
25633
+ for (const child4 of caretParagraph.children) {
25074
25634
  const runs = child4.type === "run" ? [child4] : child4.runs;
25075
25635
  const run = runs.find(
25076
25636
  (candidate) => candidate.content.some((content) => content.kind === "text" && content.srcT === this.caret.t)
@@ -27387,7 +27947,8 @@ function detectMissingFonts(layout) {
27387
27947
  for (const page of layout.pages) {
27388
27948
  for (const item of page.items) {
27389
27949
  if (item.kind !== "text" || !item.text.trim()) continue;
27390
- 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;
27391
27952
  if (!primary || GENERIC.has(primary.toLowerCase())) continue;
27392
27953
  const existing = samples.get(primary);
27393
27954
  if (!existing || !/[^-ɏ]/.test(existing) && /[^-ɏ]/.test(item.text)) {
@@ -27519,15 +28080,6 @@ var btnStyle = (active) => ({
27519
28080
  padding: "0 5px",
27520
28081
  color: T.fg
27521
28082
  });
27522
- var selectStyle = {
27523
- height: 26,
27524
- border: "1px solid transparent",
27525
- background: "transparent",
27526
- borderRadius: 4,
27527
- fontSize: 13,
27528
- color: T.fg,
27529
- cursor: "pointer"
27530
- };
27531
28083
  var PAGE_SIZES = [
27532
28084
  { value: "letter", label: "Letter", description: '8.5" \xD7 11"', width: 8.5, height: 11 },
27533
28085
  { value: "legal", label: "Legal", description: '8.5" \xD7 14"', width: 8.5, height: 14 },
@@ -27631,6 +28183,264 @@ function HighlightIcon({ color }) {
27631
28183
  /* @__PURE__ */ jsx("rect", { x: "2", y: "13.5", width: "12", height: "2.5", rx: "0.5", fill: color })
27632
28184
  ] });
27633
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
+ }
27634
28444
  function ActionMenu({
27635
28445
  label,
27636
28446
  title,
@@ -27638,22 +28448,21 @@ function ActionMenu({
27638
28448
  onPick,
27639
28449
  width
27640
28450
  }) {
27641
- return /* @__PURE__ */ jsxs(
27642
- "select",
28451
+ return /* @__PURE__ */ jsx(
28452
+ ToolbarMenuSelect,
27643
28453
  {
27644
28454
  title,
28455
+ triggerAriaLabel: title,
27645
28456
  value: "",
27646
- onMouseDown: (e) => e.stopPropagation(),
27647
- onChange: (e) => {
27648
- if (e.target.value) onPick(e.target.value);
27649
- },
27650
- style: { ...selectStyle, width },
27651
- children: [
27652
- /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: label }),
27653
- groups.map(
27654
- (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))
27655
- )
27656
- ]
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
27657
28466
  }
27658
28467
  );
27659
28468
  }
@@ -27733,6 +28542,183 @@ function HighlightMenu({ current, onPick }) {
27733
28542
  )
27734
28543
  ] });
27735
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
+ }
27736
28722
  function BulletListIcon() {
27737
28723
  return /* @__PURE__ */ jsxs("svg", { style: icon, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.4", children: [
27738
28724
  /* @__PURE__ */ jsx("circle", { cx: "3", cy: "4", r: "1.1", fill: "currentColor", stroke: "none" }),
@@ -28095,7 +29081,18 @@ function CrossReferenceMenu({ api }) {
28095
29081
  return /* @__PURE__ */ jsxs("span", { ref: rootRef, style: { position: "relative", display: "inline-block" }, children: [
28096
29082
  /* @__PURE__ */ jsx("button", { title: "Insert cross-reference", style: btnStyle(open), onMouseDown: (event) => event.preventDefault(), onClick: () => setOpen(!open), children: "Cross-reference" }),
28097
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: [
28098
- /* @__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
+ ),
28099
29096
  /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "flex-end", gap: 6, marginTop: 8 }, children: [
28100
29097
  /* @__PURE__ */ jsx("button", { style: { ...pillBtn, background: T.popoverBg, color: T.fg }, onClick: () => insert("page"), children: "Page number" }),
28101
29098
  /* @__PURE__ */ jsx("button", { style: pillBtn, onClick: () => insert("text"), children: "Bookmark text" })
@@ -28202,6 +29199,7 @@ function SymbolMenu({ api }) {
28202
29199
  ] });
28203
29200
  }
28204
29201
  var SHAPES = [
29202
+ ["line", "Line", "\u2015"],
28205
29203
  ["rectangle", "Rectangle", "\u25AD"],
28206
29204
  ["roundedRectangle", "Rounded rectangle", "\u25A2"],
28207
29205
  ["ellipse", "Ellipse", "\u25EF"],
@@ -28373,12 +29371,23 @@ function ChartMenu({ api }) {
28373
29371
  return /* @__PURE__ */ jsxs("span", { ref: rootRef, style: { position: "relative", display: "inline-block" }, children: [
28374
29372
  /* @__PURE__ */ jsx("button", { title: "Insert or edit chart", style: btnStyle(open), onMouseDown: (event) => event.preventDefault(), onClick: () => setOpen(!open), children: "Chart" }),
28375
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: [
28376
- /* @__PURE__ */ jsxs("select", { "aria-label": "Chart type", value: type, onChange: (event) => setType(event.target.value), style: fieldStyle, children: [
28377
- /* @__PURE__ */ jsx("option", { value: "column", children: "Column" }),
28378
- /* @__PURE__ */ jsx("option", { value: "bar", children: "Bar" }),
28379
- /* @__PURE__ */ jsx("option", { value: "line", children: "Line" }),
28380
- /* @__PURE__ */ jsx("option", { value: "pie", children: "Pie" })
28381
- ] }),
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
+ ),
28382
29391
  /* @__PURE__ */ jsx("input", { "aria-label": "Chart title", value: title, onChange: (event) => setTitle(event.target.value), placeholder: "Chart title", style: fieldStyle }),
28383
29392
  /* @__PURE__ */ jsx("input", { "aria-label": "Chart categories", value: categories, onChange: (event) => setCategories(event.target.value), placeholder: "Q1, Q2, Q3, Q4", style: fieldStyle }),
28384
29393
  /* @__PURE__ */ jsx("textarea", { "aria-label": "Chart series", rows: 3, value: series, onChange: (event) => setSeries(event.target.value), style: { ...fieldStyle, resize: "vertical" } }),
@@ -28411,12 +29420,23 @@ function SmartArtMenu({ api }) {
28411
29420
  return /* @__PURE__ */ jsxs("span", { ref: rootRef, style: { position: "relative", display: "inline-block" }, children: [
28412
29421
  /* @__PURE__ */ jsx("button", { title: "Insert or edit SmartArt", style: btnStyle(open), onMouseDown: (event) => event.preventDefault(), onClick: () => setOpen(!open), children: "SmartArt" }),
28413
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: [
28414
- /* @__PURE__ */ jsxs("select", { "aria-label": "SmartArt layout", value: layout, onChange: (event) => setLayout(event.target.value), style: fieldStyle, children: [
28415
- /* @__PURE__ */ jsx("option", { value: "process", children: "Process" }),
28416
- /* @__PURE__ */ jsx("option", { value: "cycle", children: "Cycle" }),
28417
- /* @__PURE__ */ jsx("option", { value: "hierarchy", children: "Hierarchy" }),
28418
- /* @__PURE__ */ jsx("option", { value: "list", children: "List" })
28419
- ] }),
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
+ ),
28420
29440
  /* @__PURE__ */ jsx("textarea", { "aria-label": "SmartArt items", rows: 5, value: items, onChange: (event) => setItems(event.target.value), style: { ...fieldStyle, resize: "vertical" } }),
28421
29441
  /* @__PURE__ */ jsx("div", { style: { color: T.muted, font: "11.5px system-ui, sans-serif" }, children: "One diagram item per line." }),
28422
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" })
@@ -28474,19 +29494,18 @@ function DrawTab({ api }) {
28474
29494
  /* @__PURE__ */ jsx(Btn, { label: "Eraser", title: "Stroke eraser", active: active === "eraser", onClick: () => api?.setDrawingTool({ kind: "eraser", size: 14 }) }),
28475
29495
  /* @__PURE__ */ jsx(Btn, { label: "Lasso", title: "Lasso ink", active: active === "lasso", onClick: () => api?.setDrawingTool({ kind: "lasso" }) }),
28476
29496
  /* @__PURE__ */ jsx(Sep, {}),
28477
- /* @__PURE__ */ jsxs("label", { title: "Pen color", style: { display: "inline-flex", alignItems: "center", gap: 5, color: T.fg, font: "12px system-ui, sans-serif" }, children: [
28478
- "Color",
28479
- /* @__PURE__ */ jsx(
28480
- "input",
28481
- {
28482
- "aria-label": "Pen color",
28483
- type: "color",
28484
- value: color,
28485
- onChange: (event) => activate(active === "highlighter" ? "highlighter" : "pen", event.target.value, width),
28486
- style: { width: 28, height: 24, padding: 1, border: `1px solid ${T.border}`, borderRadius: 4, background: "transparent" }
28487
- }
28488
- )
28489
- ] }),
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
+ ),
28490
29509
  /* @__PURE__ */ jsx(
28491
29510
  ActionMenu,
28492
29511
  {
@@ -29064,9 +30083,8 @@ function MarginMenu({
29064
30083
  "input",
29065
30084
  {
29066
30085
  "aria-label": `${label} margin (inches)`,
29067
- type: "number",
29068
- min: "0",
29069
- step: "0.01",
30086
+ type: "text",
30087
+ inputMode: "decimal",
29070
30088
  required: true,
29071
30089
  autoFocus: side === "top",
29072
30090
  value: values[side],
@@ -29191,9 +30209,8 @@ function PageSizeMenu({
29191
30209
  "input",
29192
30210
  {
29193
30211
  "aria-label": `Page ${side} (inches)`,
29194
- type: "number",
29195
- min: "0.01",
29196
- step: "0.01",
30212
+ type: "text",
30213
+ inputMode: "decimal",
29197
30214
  required: true,
29198
30215
  autoFocus: side === "width",
29199
30216
  value: values[side],
@@ -29291,19 +30308,20 @@ function LayoutTab({ api, showArrange }) {
29291
30308
  "data-dxw-layout-ribbon": "",
29292
30309
  style: { display: "flex", flex: "1 1 640px", minWidth: 0, alignItems: "center", flexWrap: "wrap", gap: 2 },
29293
30310
  children: [
29294
- /* @__PURE__ */ jsxs(
29295
- "select",
30311
+ /* @__PURE__ */ jsx(
30312
+ ToolbarMenuSelect,
29296
30313
  {
29297
30314
  title: "Apply layout changes to",
29298
- "aria-label": "Apply layout changes to",
30315
+ ariaLabel: "Apply layout changes to",
29299
30316
  value: scope,
29300
- onMouseDown: (e) => e.stopPropagation(),
29301
- onChange: (e) => setScope(e.target.value),
29302
- style: { ...selectStyle, width: 118, maxWidth: "100%" },
29303
- children: [
29304
- /* @__PURE__ */ jsx("option", { value: "document", children: "Whole document" }),
29305
- /* @__PURE__ */ jsx("option", { value: "section", children: "This section" })
29306
- ]
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%" }
29307
30325
  }
29308
30326
  ),
29309
30327
  /* @__PURE__ */ jsx(Sep, {}),
@@ -29530,42 +30548,44 @@ function DocxToolbar({
29530
30548
  if (on("styles"))
29531
30549
  groups.push({
29532
30550
  key: "styles",
29533
- node: /* @__PURE__ */ jsxs(
29534
- "select",
30551
+ node: /* @__PURE__ */ jsx(
30552
+ ToolbarMenuSelect,
29535
30553
  {
29536
30554
  title: "Paragraph style",
29537
30555
  value: curStyle ?? "__normal",
29538
- onMouseDown: (e) => e.stopPropagation(),
29539
- onChange: (e) => {
29540
- if (e.target.value) {
29541
- 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);
29542
30566
  setCurStyle(api?.getParagraphStyleId?.() ?? null);
29543
30567
  }
29544
- },
29545
- style: { ...selectStyle, width: 92 },
29546
- children: [
29547
- /* @__PURE__ */ jsx("option", { value: "__normal", children: "Normal" }),
29548
- (api?.listParagraphStyles() ?? []).filter((s) => !/^normal$/i.test(s.name)).map((s) => /* @__PURE__ */ jsx("option", { value: s.id, children: s.name }, s.id)),
29549
- curStyle !== null && !(api?.listParagraphStyles() ?? []).some((s) => s.id === curStyle) && /* @__PURE__ */ jsx("option", { value: curStyle, children: api?.document.styles.byId.get(curStyle)?.name ?? curStyle })
29550
- ]
30568
+ }
29551
30569
  }
29552
30570
  )
29553
30571
  });
29554
30572
  if (on("font"))
29555
30573
  groups.push({
29556
30574
  key: "font",
29557
- node: /* @__PURE__ */ jsxs(
29558
- "select",
30575
+ node: /* @__PURE__ */ jsx(
30576
+ ToolbarMenuSelect,
29559
30577
  {
29560
30578
  title: "Font",
29561
30579
  value: fmt?.fontFamily ?? "",
29562
- onMouseDown: (e) => e.stopPropagation(),
29563
- onChange: (e) => e.target.value && apply({ fontFamily: e.target.value }),
29564
- style: { ...selectStyle, width: 130 },
29565
- children: [
29566
- /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: "Font" }),
29567
- (fmt?.fontFamily && !detectFonts().includes(fmt.fontFamily) ? [fmt.fontFamily, ...detectFonts()] : detectFonts()).map((f) => /* @__PURE__ */ jsx("option", { value: f, style: { fontFamily: f }, children: f }, f))
29568
- ]
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 })
29569
30589
  }
29570
30590
  )
29571
30591
  });
@@ -29573,18 +30593,16 @@ function DocxToolbar({
29573
30593
  groups.push({
29574
30594
  key: "size",
29575
30595
  node: /* @__PURE__ */ jsxs(Fragment2, { children: [
29576
- /* @__PURE__ */ jsxs(
29577
- "select",
30596
+ /* @__PURE__ */ jsx(
30597
+ ToolbarMenuSelect,
29578
30598
  {
29579
30599
  title: "Font size",
29580
- value: fmt?.fontSizePt ?? "",
29581
- onMouseDown: (e) => e.stopPropagation(),
29582
- onChange: (e) => e.target.value && apply({ fontSizePt: parseFloat(e.target.value) }),
29583
- style: { ...selectStyle, width: 58 },
29584
- children: [
29585
- /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: "Size" }),
29586
- SIZES.map((s) => /* @__PURE__ */ jsx("option", { value: s, children: s }, s))
29587
- ]
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) })
29588
30606
  }
29589
30607
  ),
29590
30608
  /* @__PURE__ */ jsx(Sep, {})
@@ -29641,19 +30659,15 @@ function DocxToolbar({
29641
30659
  if (on("color"))
29642
30660
  groups.push({
29643
30661
  key: "color",
29644
- node: /* @__PURE__ */ jsxs("label", { title: "Text color", style: { display: "inline-flex", alignItems: "center", cursor: "pointer" }, children: [
29645
- /* @__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" }),
29646
- /* @__PURE__ */ jsx(
29647
- "input",
29648
- {
29649
- type: "color",
29650
- value: fmt?.color && fmt.color !== "auto" ? fmt.color : "#000000",
29651
- onMouseDown: (e) => e.stopPropagation(),
29652
- onChange: (e) => apply({ color: e.target.value }),
29653
- style: { width: 0, height: 0, opacity: 0, border: "none", padding: 0 }
29654
- }
29655
- )
29656
- ] })
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
+ )
29657
30671
  });
29658
30672
  if (on("highlight"))
29659
30673
  groups.push({
@@ -30325,19 +31339,23 @@ function DocxView({
30325
31339
  "Calibri",
30326
31340
  "Calibri Light",
30327
31341
  "Tahoma",
30328
- "Franklin Gothic Medium",
30329
- // Complex-script faces gate layout too (advances must be real
30330
- // before line breaking): Mangal (Devanagari), Latha (Tamil),
30331
- // Noto Sans Lao Looped (Lao — bundled OFL webfont).
30332
- "Mangal",
30333
- "Latha",
30334
- "Noto Sans Lao Looped"
31342
+ "Franklin Gothic Medium"
30335
31343
  ];
30336
31344
  for (const fam of latin) {
30337
31345
  for (const variant of ["", "italic ", "bold ", "bold italic "]) {
30338
31346
  loads.push(document.fonts.load(`${variant}16px "${fam}"`).catch(() => []));
30339
31347
  }
30340
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
+ }
30341
31359
  loads.push(document.fonts.load('16px "Cambria Math"').catch(() => []));
30342
31360
  const cjk = [
30343
31361
  "MS Mincho",
@@ -30353,7 +31371,7 @@ function DocxView({
30353
31371
  ];
30354
31372
  for (const fam of cjk) {
30355
31373
  for (const variant of ["", "bold "]) {
30356
- loads.push(document.fonts.load(`${variant}16px "${fam}"`).catch(() => []));
31374
+ loads.push(document.fonts.load(`${variant}16px "${fam}"`, "A\u6F22").catch(() => []));
30357
31375
  }
30358
31376
  }
30359
31377
  await Promise.all(loads);
@@ -30400,7 +31418,39 @@ function DocxView({
30400
31418
  else if (formatted.length > 0) editor?.selectRanges(formatted);
30401
31419
  document.dispatchEvent(new CustomEvent("dxw-selection"));
30402
31420
  },
30403
- 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
+ }
30404
31454
  };
30405
31455
  editor = new DocxEditor(editorConfig);
30406
31456
  editor.attach();
@@ -31019,7 +32069,8 @@ function DocxView({
31019
32069
  alignItems: "center"
31020
32070
  },
31021
32071
  children: [
31022
- /* @__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")),
31023
32074
  hotBtn("Page number", "Insert a dynamic page number at the caret", () => apiRef.current?.insertPageNumber("page")),
31024
32075
  hotBtn("Page X of Y", "Insert 'Page X of Y' at the caret", () => apiRef.current?.insertPageNumber("pageOfTotal")),
31025
32076
  hotBtn("Close", "Return to the document body", () => {
@@ -31035,6 +32086,7 @@ export {
31035
32086
  DocxDocument,
31036
32087
  DocxToolbar,
31037
32088
  DocxView,
32089
+ ToolbarMenuSelect,
31038
32090
  layoutDocument,
31039
32091
  printPages,
31040
32092
  renderToDom