@beyondwork/docx-react-component 1.0.78 → 1.0.80

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.
@@ -276,10 +276,15 @@ export type CreateScopeFromAnchorResult =
276
276
  readonly reason:
277
277
  | "from-negative"
278
278
  | "to-less-than-from"
279
- | "range-exceeds-story-length";
279
+ | "range-exceeds-story-length"
280
+ | "non-paragraph-target"
281
+ | "empty-document";
280
282
  readonly from: number;
281
283
  readonly to: number;
282
284
  readonly storyLength: number;
285
+ /** Non-paragraph target only — the offending block's index and kind. */
286
+ readonly blockIndex?: number;
287
+ readonly blockKind?: string;
283
288
  /**
284
289
  * Single-sentence, agent-actionable explanation. Tells the caller
285
290
  * what the failure was and the concrete next step — no guesswork
@@ -291,7 +296,8 @@ export type CreateScopeFromAnchorResult =
291
296
  * Short machine-routable next-step hint for thin consumers that
292
297
  * don't want to pattern-match on `reason`. Examples:
293
298
  * "clamp-from-to-zero", "swap-from-and-to",
294
- * "clamp-to-to-storyLength-or-pick-a-different-range".
299
+ * "clamp-to-to-storyLength-or-pick-a-different-range",
300
+ * "pick-a-paragraph-target".
295
301
  */
296
302
  readonly nextStep: string;
297
303
  };
@@ -420,5 +426,66 @@ export function createScopeFromAnchor(
420
426
  ...(scopeMetadataFields.length > 0 ? { scopeMetadataFields } : {}),
421
427
  });
422
428
 
429
+ // Pre-2026-04-24 the coordinator silently returned a minted scopeId
430
+ // even when insertScopeMarkers refused to plant (non-paragraph
431
+ // target, out-of-bounds after the story-length check passed).
432
+ // Cross-paragraph ranges now plant successfully (2026-04-24
433
+ // multi-paragraph-scopes slice), so that refusal variant is retired.
434
+ // Remaining reasons translate into the same `range-invalid` shape
435
+ // used by the bounds checks above so the caller gets one uniform
436
+ // discriminator to branch on.
437
+ if (result.plantStatus && result.plantStatus.planted === false) {
438
+ const ps = result.plantStatus;
439
+ if (ps.reason === "non-paragraph-target") {
440
+ return {
441
+ status: "range-invalid",
442
+ reason: "non-paragraph-target",
443
+ from,
444
+ to,
445
+ storyLength,
446
+ blockIndex: ps.blockIndex ?? -1,
447
+ blockKind: ps.blockKind ?? "unknown",
448
+ message:
449
+ `createScopeFromAnchor refused: range [${from}, ${to}] targets a ` +
450
+ `${ps.blockKind ?? "non-paragraph"} block (index ${ps.blockIndex}). ` +
451
+ `Marker scopes only plant inside paragraphs today. Pick a paragraph ` +
452
+ `target, or use runtime.workflow.createScope({blockId}) for ` +
453
+ `whole-block scopes on the containing structure.`,
454
+ nextStep: "pick-a-paragraph-target",
455
+ };
456
+ }
457
+ if (ps.reason === "range-out-of-bounds") {
458
+ // Shouldn't happen — storyLength was checked above — but surface
459
+ // it as a first-class failure in case the underlying length math
460
+ // drifts from our bounds check.
461
+ return {
462
+ status: "range-invalid",
463
+ reason: "range-exceeds-story-length",
464
+ from,
465
+ to,
466
+ storyLength: ps.storyLength ?? storyLength,
467
+ message:
468
+ `createScopeFromAnchor refused: coordinator reports range [${from}, ${to}] ` +
469
+ `is out of bounds (storyLength=${ps.storyLength}). This is usually a ` +
470
+ `stale-offset bug (KI-P9) — re-derive positions from the current ` +
471
+ `document and retry.`,
472
+ nextStep: "clamp-to-to-storyLength-or-pick-a-different-range",
473
+ };
474
+ }
475
+ // empty-document — target has no canonical blocks.
476
+ return {
477
+ status: "range-invalid",
478
+ reason: "empty-document",
479
+ from,
480
+ to,
481
+ storyLength,
482
+ message:
483
+ `createScopeFromAnchor refused: the target document has no blocks; ` +
484
+ `cannot plant scope markers. Open or initialize a document before ` +
485
+ `creating sub-block scopes.`,
486
+ nextStep: "initialize-document-before-creating-scopes",
487
+ };
488
+ }
489
+
423
490
  return { status: "created", scopeId: result.scopeId, anchor: result.anchor };
424
491
  }
@@ -155,6 +155,16 @@ export function getRevisionRangeState(
155
155
  * by `test/runtime/formatting/production-boundary.test.ts`.
156
156
  */
157
157
  export interface RevisionDisplayFlags {
158
+ /**
159
+ * Identity of the attached revision. Mirrors
160
+ * `SurfaceInlineSegment.revisionDisplay.revisionId`.
161
+ */
162
+ revisionId: string;
163
+ /**
164
+ * Mirrors `RevisionRecord.kind`. Consumers branch on insertion /
165
+ * deletion variants without reading the review store.
166
+ */
167
+ kind: "insertion" | "deletion" | "formatting" | "move" | "property-change";
158
168
  markupMode: "clean" | "simple" | "all";
159
169
  hidden?: boolean;
160
170
  strikethrough?: boolean;
@@ -199,6 +199,17 @@ export const editorSchema = new Schema({
199
199
  pageBreakBefore: { default: null },
200
200
  hiddenTextOnly: { default: null },
201
201
  placeholderCulled: { default: null },
202
+ /**
203
+ * Rendered height (in twips) of the block that this placeholder
204
+ * stands in for, supplied by `DocumentRuntime` from L04's page
205
+ * graph. When present on a `placeholderCulled` paragraph, `toDOM`
206
+ * emits a fixed-height `<div>` (`${twips/20}pt`) instead of the
207
+ * `min-height: 20px` fallback, eliminating the scroll-path
208
+ * "paragraphs jump around pagination gaps" flicker that occurred
209
+ * when blocks realized at real heights larger than one line.
210
+ * Null / undefined preserves the pre-existing 20 px minimum.
211
+ */
212
+ placeholderHeightTwips: { default: null },
202
213
  blockId: { default: null },
203
214
  /**
204
215
  * `<w:framePr>` projection from `SurfaceBlockFragment.frameProperties`
@@ -214,6 +225,11 @@ export const editorSchema = new Schema({
214
225
  toDOM(node) {
215
226
  // Viewport-culled placeholder paragraph — cheap size-preserving leaf.
216
227
  if (node.attrs.placeholderCulled) {
228
+ const heightTwips = node.attrs.placeholderHeightTwips as number | null;
229
+ const heightStyle =
230
+ typeof heightTwips === "number" && heightTwips > 0
231
+ ? `height: ${heightTwips / 20}pt`
232
+ : "min-height: 20px";
217
233
  return [
218
234
  "div",
219
235
  {
@@ -221,7 +237,10 @@ export const editorSchema = new Schema({
221
237
  "data-placeholder-culled": "true",
222
238
  "data-placeholder-size": String(node.nodeSize),
223
239
  "data-placeholder-block-id": node.attrs.blockId ?? "",
224
- style: "min-height: 20px; contain: strict;",
240
+ ...(typeof heightTwips === "number" && heightTwips > 0
241
+ ? { "data-placeholder-height-twips": String(heightTwips) }
242
+ : {}),
243
+ style: `${heightStyle}; contain: strict;`,
225
244
  "aria-hidden": "true",
226
245
  },
227
246
  0,
@@ -867,10 +867,25 @@ function buildOpaqueBlock(
867
867
  const placeholderSize = block.placeholderSize ?? null;
868
868
  if (placeholderSize !== null) {
869
869
  const targetSize = placeholderSize as number;
870
+ // Flicker fix — when DocumentRuntime has enriched the placeholder with
871
+ // the block's known rendered height (from L04's page graph), thread it
872
+ // onto the paragraph node so `pm-schema.ts::toDOM` emits a fixed
873
+ // `height` style matching the real block. Without this, the placeholder
874
+ // renders at `min-height: 20px` and inflates to its real height when
875
+ // the block realizes on scroll, dragging content below the scroll
876
+ // pointer ("paragraphs jump around pagination gaps").
877
+ const placeholderHeightTwips = block.placeholderHeightTwips ?? null;
878
+ const placeholderAttrs: Record<string, unknown> = {
879
+ blockId: block.blockId,
880
+ placeholderCulled: true,
881
+ };
882
+ if (placeholderHeightTwips !== null) {
883
+ placeholderAttrs.placeholderHeightTwips = placeholderHeightTwips;
884
+ }
870
885
  if (targetSize <= 2) {
871
886
  // Edge case: bare empty paragraph claims exactly 2 positions.
872
887
  return editorSchema.nodes.paragraph.create(
873
- { blockId: block.blockId, placeholderCulled: true },
888
+ placeholderAttrs,
874
889
  Fragment.empty,
875
890
  );
876
891
  }
@@ -878,7 +893,7 @@ function buildOpaqueBlock(
878
893
  // total PM positions = 1 (open) + (targetSize - 2) (text) + 1 (close) = targetSize.
879
894
  const filler = "\u200b".repeat(targetSize - 2);
880
895
  return editorSchema.nodes.paragraph.create(
881
- { blockId: block.blockId, placeholderCulled: true },
896
+ placeholderAttrs,
882
897
  editorSchema.text(filler),
883
898
  );
884
899
  }
@@ -1075,7 +1075,16 @@
1075
1075
  }
1076
1076
 
1077
1077
  .wre-page-band:hover {
1078
- background-color: color-mix(in srgb, var(--color-bg-muted) 80%, var(--color-surface));
1078
+ /*
1079
+ * N3 audit (2026-04-24): the prior `color-mix(bg-muted 80%, surface 20%)`
1080
+ * blended two near-adjacent tokens — in dark mode (#17211C vs #182420)
1081
+ * the result moved ≤2 RGB units from the base, leaving no visible
1082
+ * hover affordance. `--color-bg-hover` is the token the design system
1083
+ * already dedicates to this signal (light #EAF6EF vs muted #F7FAF8;
1084
+ * dark #21342A vs muted #17211C) and resolves the dark-mode legibility
1085
+ * gap without regressing light-mode subtlety.
1086
+ */
1087
+ background-color: var(--color-bg-hover);
1079
1088
  }
1080
1089
 
1081
1090
  .wre-page-band[data-active="true"] {