@beyondwork/docx-react-component 1.0.58 → 1.0.59
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/README.md +2 -2
- package/package.json +2 -1
- package/src/api/awareness-identity-types.ts +4 -2
- package/src/api/comment-negotiation-types.ts +4 -1
- package/src/api/external-custody-types.ts +16 -0
- package/src/api/internal/build-ref-projections.ts +108 -0
- package/src/api/package-version.ts +1 -1
- package/src/api/participants-types.ts +11 -1
- package/src/api/public-types.ts +978 -10
- package/src/api/scope-metadata-resolver-types.ts +6 -0
- package/src/compare/diff-engine.ts +3 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/index.ts +225 -16
- package/src/core/commands/legacy-form-field-commands.ts +181 -0
- package/src/core/commands/table-structure-commands.ts +149 -31
- package/src/core/selection/mapping.ts +20 -0
- package/src/core/state/editor-state.ts +2 -1
- package/src/index.ts +28 -0
- package/src/io/docx-session.ts +22 -3
- package/src/io/export/export-session.ts +11 -7
- package/src/io/export/ooxml-namespaces.ts +47 -0
- package/src/io/export/reattach-preserved-parts.ts +4 -16
- package/src/io/export/serialize-comments.ts +3 -131
- package/src/io/export/serialize-ffdata.ts +89 -0
- package/src/io/export/serialize-headers-footers.ts +5 -0
- package/src/io/export/serialize-main-document.ts +224 -34
- package/src/io/export/serialize-numbering.ts +22 -2
- package/src/io/export/serialize-revisions.ts +99 -0
- package/src/io/export/serialize-tables.ts +9 -0
- package/src/io/export/split-review-boundaries.ts +1 -0
- package/src/io/export/table-properties-xml.ts +14 -0
- package/src/io/load-scheduler.ts +70 -28
- package/src/io/normalize/normalize-text.ts +13 -0
- package/src/io/ooxml/_mini-xml.ts +198 -0
- package/src/io/ooxml/canonicalize-payload.ts +1 -4
- package/src/io/ooxml/chart/chart-style-table.ts +4 -3
- package/src/io/ooxml/chart/parse-chart-space.ts +2 -4
- package/src/io/ooxml/chart/parse-series.ts +2 -1
- package/src/io/ooxml/chart/resolve-color.ts +2 -2
- package/src/io/ooxml/chart/types.ts +6 -434
- package/src/io/ooxml/comment-presentation-payload.ts +6 -5
- package/src/io/ooxml/highlight-colors.ts +8 -5
- package/src/io/ooxml/parse-anchor.ts +68 -53
- package/src/io/ooxml/parse-comments.ts +14 -142
- package/src/io/ooxml/parse-complex-content.ts +3 -106
- package/src/io/ooxml/parse-drawing.ts +100 -195
- package/src/io/ooxml/parse-ffdata.ts +93 -0
- package/src/io/ooxml/parse-fields.ts +7 -146
- package/src/io/ooxml/parse-fill.ts +88 -8
- package/src/io/ooxml/parse-font-table.ts +5 -105
- package/src/io/ooxml/parse-footnotes.ts +28 -152
- package/src/io/ooxml/parse-headers-footers.ts +106 -212
- package/src/io/ooxml/parse-inline-media.ts +3 -200
- package/src/io/ooxml/parse-main-document.ts +180 -217
- package/src/io/ooxml/parse-numbering.ts +154 -335
- package/src/io/ooxml/parse-object.ts +147 -0
- package/src/io/ooxml/parse-ole-relationship.ts +82 -0
- package/src/io/ooxml/parse-paragraph-formatting.ts +7 -10
- package/src/io/ooxml/parse-picture-sdt.ts +85 -0
- package/src/io/ooxml/parse-picture.ts +72 -42
- package/src/io/ooxml/parse-revisions.ts +285 -51
- package/src/io/ooxml/parse-settings.ts +6 -99
- package/src/io/ooxml/parse-shapes.ts +25 -140
- package/src/io/ooxml/parse-styles.ts +3 -218
- package/src/io/ooxml/parse-tables.ts +76 -256
- package/src/io/ooxml/parse-theme.ts +1 -4
- package/src/io/ooxml/property-grab-bag.ts +5 -47
- package/src/io/ooxml/xml-element-serialize.ts +32 -0
- package/src/io/ooxml/xml-parser.ts +183 -0
- package/src/legal/bookmarks.ts +1 -1
- package/src/legal/cross-references.ts +1 -1
- package/src/legal/defined-terms.ts +1 -1
- package/src/legal/{_document-root.ts → document-root.ts} +8 -0
- package/src/legal/signature-blocks.ts +1 -1
- package/src/model/canonical-document.ts +159 -6
- package/src/model/chart-types.ts +439 -0
- package/src/model/snapshot.ts +3 -1
- package/src/review/store/comment-remapping.ts +24 -11
- package/src/review/store/revision-actions.ts +482 -2
- package/src/review/store/revision-store.ts +15 -0
- package/src/review/store/revision-types.ts +76 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +24 -0
- package/src/runtime/collab/runtime-collab-sync.ts +33 -0
- package/src/runtime/diagnostics/build-diagnostic.ts +151 -0
- package/src/runtime/diagnostics/code-metadata-table.ts +221 -0
- package/src/runtime/document-runtime.ts +476 -34
- package/src/runtime/document-search.ts +115 -0
- package/src/runtime/edit-ops/index.ts +18 -2
- package/src/runtime/footnote-resolver.ts +130 -0
- package/src/runtime/layout/layout-engine-instance.ts +31 -4
- package/src/runtime/layout/layout-engine-version.ts +37 -1
- package/src/runtime/layout/page-graph.ts +14 -1
- package/src/runtime/layout/resolved-formatting-state.ts +21 -0
- package/src/runtime/numbering-prefix.ts +17 -0
- package/src/runtime/query-scopes.ts +5 -8
- package/src/runtime/resolved-numbering-geometry.ts +37 -6
- package/src/runtime/revision-runtime.ts +27 -1
- package/src/runtime/selection/post-edit-validator.ts +60 -6
- package/src/runtime/structure-ops/index.ts +20 -4
- package/src/runtime/surface-projection.ts +290 -21
- package/src/runtime/table-schema.ts +6 -0
- package/src/runtime/theme-color-resolver.ts +2 -2
- package/src/runtime/units.ts +9 -0
- package/src/runtime/workflow-rail-segments.ts +4 -0
- package/src/ui/WordReviewEditor.tsx +187 -43
- package/src/ui/editor-runtime-boundary.ts +10 -0
- package/src/ui/editor-shell-view.tsx +4 -1
- package/src/ui/headless/chrome-registry.ts +53 -0
- package/src/ui/headless/selection-tool-resolver.ts +11 -1
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +13 -0
- package/src/ui-tailwind/chrome/tw-command-palette-mount.tsx +96 -0
- package/src/ui-tailwind/chrome/tw-context-menu.tsx +2 -1
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +5 -4
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +6 -2
- package/src/ui-tailwind/chrome/use-container-breakpoint.ts +111 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +0 -9
- package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +1 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +6 -7
- package/src/ui-tailwind/editor-surface/pm-schema.ts +87 -25
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +9 -0
- package/src/ui-tailwind/editor-surface/shape-renderer.ts +76 -14
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +18 -1
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +2 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +18 -2
- package/src/ui-tailwind/index.ts +9 -0
- package/src/ui-tailwind/page-chrome-model.ts +77 -5
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +56 -1
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +2 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +116 -113
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +2 -2
- package/src/ui-tailwind/theme/tokens.ts +14 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +5 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +29 -87
- package/src/validation/diagnostics.ts +1 -0
|
@@ -387,15 +387,18 @@ export function attachScopeCardModel(
|
|
|
387
387
|
}
|
|
388
388
|
|
|
389
389
|
const workItemByScope = new Map<string, string>();
|
|
390
|
+
const anchorByScope = new Map<string, EditorAnchorProjection>();
|
|
390
391
|
for (const scope of input.scopes ?? []) {
|
|
391
392
|
if (scope.workItemId) {
|
|
392
393
|
workItemByScope.set(scope.scopeId, scope.workItemId);
|
|
393
394
|
}
|
|
395
|
+
anchorByScope.set(scope.scopeId, scope.anchor);
|
|
394
396
|
}
|
|
395
397
|
|
|
396
398
|
const models: ScopeCardModel[] = [];
|
|
397
399
|
for (const segment of firstByScope.values()) {
|
|
398
400
|
const workItemId = workItemByScope.get(segment.scopeId);
|
|
401
|
+
const anchor = anchorByScope.get(segment.scopeId);
|
|
399
402
|
const issue = resolveIssueForScope(
|
|
400
403
|
segment.scopeId,
|
|
401
404
|
workItemId,
|
|
@@ -423,6 +426,7 @@ export function attachScopeCardModel(
|
|
|
423
426
|
label: segment.label ?? "",
|
|
424
427
|
posture: segment.posture,
|
|
425
428
|
primaryAnchorRect,
|
|
429
|
+
...(anchor ? { anchor } : {}),
|
|
426
430
|
...(issue ? { issue } : {}),
|
|
427
431
|
suggestionGroupIds: suggestionGroups.map((group) => group.groupId),
|
|
428
432
|
suggestionGroups,
|
|
@@ -44,6 +44,7 @@ import type {
|
|
|
44
44
|
SectionLayoutPatch,
|
|
45
45
|
SectionPageNumberingPatch,
|
|
46
46
|
SearchOptions,
|
|
47
|
+
SetSelectionOptions,
|
|
47
48
|
SearchResultSnapshot,
|
|
48
49
|
SelectionSnapshot as PublicSelectionSnapshot,
|
|
49
50
|
SuggestionEntrySnapshot,
|
|
@@ -76,6 +77,7 @@ import type {
|
|
|
76
77
|
ZoomLevel,
|
|
77
78
|
} from "../api/public-types";
|
|
78
79
|
import { MetadataResolverMissingError } from "../api/public-types";
|
|
80
|
+
import { buildRefProjections } from "../api/internal/build-ref-projections.ts";
|
|
79
81
|
import { readHarnessDebugPortsFlag } from "../internal/harness-debug-ports.ts";
|
|
80
82
|
import type { ScopeMetadataResolver } from "../api/scope-metadata-resolver-types.ts";
|
|
81
83
|
import {
|
|
@@ -189,6 +191,7 @@ import type {
|
|
|
189
191
|
SuggestionCardModel,
|
|
190
192
|
} from "./headless/selection-toolbar-model";
|
|
191
193
|
import { resolveActiveSelectionTool } from "./headless/selection-tool-resolver";
|
|
194
|
+
import { resolveSelectionToolRegistry } from "./headless/chrome-registry";
|
|
192
195
|
import { type EditorCommandBag, useCommandBag } from "./editor-command-bag.ts";
|
|
193
196
|
import {
|
|
194
197
|
resolveHeadingShortcutStyleId,
|
|
@@ -566,7 +569,16 @@ export function __createWordReviewEditorRefBridge(
|
|
|
566
569
|
// Pending-conflict queue for this bridge instance. Keyed by conflictKey(...).
|
|
567
570
|
const pendingConflicts = new Map<string, PendingConflict>();
|
|
568
571
|
|
|
569
|
-
|
|
572
|
+
// v2.0.0 Track D — purpose-grouped projections dispatch via this holder
|
|
573
|
+
// so they stay in lockstep with the ref even if a rebuild is triggered.
|
|
574
|
+
const refHolder: { current: WordReviewEditorRef | null } = { current: null };
|
|
575
|
+
const projections = buildRefProjections(() => {
|
|
576
|
+
const r = refHolder.current;
|
|
577
|
+
if (!r) throw new Error("ref projection used before ref initialization");
|
|
578
|
+
return r;
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
const refValue: WordReviewEditorRef = {
|
|
570
582
|
focus: () => runtime.focus(),
|
|
571
583
|
blur: () => runtime.blur(),
|
|
572
584
|
undo: () => runtime.undo(),
|
|
@@ -595,6 +607,12 @@ export function __createWordReviewEditorRefBridge(
|
|
|
595
607
|
addScope: (params) => runtime.addScope(params),
|
|
596
608
|
getScope: (scopeId) => runtime.getScope(scopeId),
|
|
597
609
|
removeScope: (scopeId) => runtime.removeScope(scopeId),
|
|
610
|
+
addInvisibleScope: (params) => runtime.addInvisibleScope(params),
|
|
611
|
+
setScopeVisibility: (scopeId, visibility) => runtime.setScopeVisibility(scopeId, visibility),
|
|
612
|
+
getScopeVisibility: (scopeId) => runtime.getScopeVisibility(scopeId),
|
|
613
|
+
setScopeChromeVisibility: (state) => runtime.setScopeChromeVisibility(state),
|
|
614
|
+
getScopeChromeVisibility: () => runtime.getScopeChromeVisibility(),
|
|
615
|
+
subscribeToScopeQuery: (filter, callback) => runtime.subscribeToScopeQuery(filter, callback),
|
|
598
616
|
acceptChange: (changeId) => runtime.acceptChange(changeId),
|
|
599
617
|
rejectChange: (changeId) => runtime.rejectChange(changeId),
|
|
600
618
|
acceptAllChanges: () => runtime.acceptAllChanges(),
|
|
@@ -760,7 +778,7 @@ export function __createWordReviewEditorRefBridge(
|
|
|
760
778
|
clearSearch: () => {
|
|
761
779
|
mountedSurface?.clearSearch();
|
|
762
780
|
},
|
|
763
|
-
setSelection: (selection) => {
|
|
781
|
+
setSelection: (selection, _options?: SetSelectionOptions) => {
|
|
764
782
|
applyRuntimeSelection(
|
|
765
783
|
runtime,
|
|
766
784
|
normalizeRequestedSelection(runtime.getRenderSnapshot(), selection),
|
|
@@ -937,6 +955,8 @@ export function __createWordReviewEditorRefBridge(
|
|
|
937
955
|
applyRuntimeSelection(runtime, createSelectionFromAnchor(hits[0]!));
|
|
938
956
|
return hits.length;
|
|
939
957
|
},
|
|
958
|
+
findTextWithStyle: (query, filter, opts) => runtime.findTextWithStyle(query, filter, opts),
|
|
959
|
+
selectTextWithStyle: (query, filter, opts) => runtime.selectTextWithStyle(query, filter, opts),
|
|
940
960
|
// P17 — metadata persistence toggle + convert methods.
|
|
941
961
|
setMetadataPersistenceMode: (mode) => {
|
|
942
962
|
if (mode === "external" && !(options?.resolverRef?.current ?? null)) {
|
|
@@ -1140,7 +1160,10 @@ export function __createWordReviewEditorRefBridge(
|
|
|
1140
1160
|
replaceWorkflowMarkupText: (markupId, text) => {
|
|
1141
1161
|
runtime.replaceWorkflowMarkupText(markupId, text);
|
|
1142
1162
|
},
|
|
1163
|
+
...projections,
|
|
1143
1164
|
};
|
|
1165
|
+
refHolder.current = refValue;
|
|
1166
|
+
return refValue;
|
|
1144
1167
|
}
|
|
1145
1168
|
|
|
1146
1169
|
export function __applyRuntimeTextCommand(
|
|
@@ -1154,6 +1177,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1154
1177
|
function WordReviewEditor(props, ref) {
|
|
1155
1178
|
const {
|
|
1156
1179
|
currentUser,
|
|
1180
|
+
shellHeader,
|
|
1157
1181
|
ydoc,
|
|
1158
1182
|
awareness,
|
|
1159
1183
|
hostAdapter,
|
|
@@ -1191,6 +1215,8 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1191
1215
|
suggestionsEnabled = false,
|
|
1192
1216
|
showReviewPanel = true,
|
|
1193
1217
|
chromeVisibility,
|
|
1218
|
+
density,
|
|
1219
|
+
customSelectionTools,
|
|
1194
1220
|
} = props;
|
|
1195
1221
|
|
|
1196
1222
|
const [activeRailTab, setActiveRailTab] = useState<ReviewRailTab>("comments");
|
|
@@ -1218,6 +1244,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1218
1244
|
new Map<string, { choice: string; mergedValue?: Record<string, unknown> }>(),
|
|
1219
1245
|
);
|
|
1220
1246
|
const metadataConflictPendingRef = useRef(new Map<string, PendingConflict>());
|
|
1247
|
+
const suppressNextAwarenessPublishRef = useRef(false);
|
|
1221
1248
|
const {
|
|
1222
1249
|
runtime,
|
|
1223
1250
|
loadError,
|
|
@@ -1472,6 +1499,21 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1472
1499
|
activeRuntime.setDocumentMode(suggestionsEnabled ? "suggesting" : "editing");
|
|
1473
1500
|
}, [activeRuntime, suggestionsEnabled]);
|
|
1474
1501
|
|
|
1502
|
+
// design-close-chrome Phase 2 — density contract (designsystem §4.2).
|
|
1503
|
+
// When the `density` prop is supplied, drive the root `data-density`
|
|
1504
|
+
// attribute that powers the `--space-density-multiplier` cascade in
|
|
1505
|
+
// `tokens.css`. When the prop is omitted, leave the attribute alone
|
|
1506
|
+
// so the host (or `useDensity()`) retains control — the prop is
|
|
1507
|
+
// strictly an opt-in override, mirroring React's controlled vs.
|
|
1508
|
+
// uncontrolled input pattern. On unmount the attribute is left in
|
|
1509
|
+
// place so a subsequent host-managed setter (e.g. the density picker
|
|
1510
|
+
// in the shell view menu) can persist without a flicker.
|
|
1511
|
+
useEffect(() => {
|
|
1512
|
+
if (density === undefined) return;
|
|
1513
|
+
if (typeof document === "undefined") return;
|
|
1514
|
+
document.documentElement.dataset.density = density;
|
|
1515
|
+
}, [density]);
|
|
1516
|
+
|
|
1475
1517
|
const markCurrentSectionForReview = useCallback((input?: {
|
|
1476
1518
|
sectionIndex?: number;
|
|
1477
1519
|
label?: string;
|
|
@@ -1568,14 +1610,18 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1568
1610
|
return;
|
|
1569
1611
|
}
|
|
1570
1612
|
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1613
|
+
if (suppressNextAwarenessPublishRef.current) {
|
|
1614
|
+
suppressNextAwarenessPublishRef.current = false;
|
|
1615
|
+
} else {
|
|
1616
|
+
setLocalCursorState(awareness, {
|
|
1617
|
+
userId: currentUser.userId,
|
|
1618
|
+
displayName: currentUser.displayName,
|
|
1619
|
+
color: getCursorColorForUser(currentUser.userId),
|
|
1620
|
+
anchor: snapshot.selection.anchor,
|
|
1621
|
+
head: snapshot.selection.head,
|
|
1622
|
+
storyTarget: snapshot.activeStory,
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1579
1625
|
}, [
|
|
1580
1626
|
awareness,
|
|
1581
1627
|
currentUser.displayName,
|
|
@@ -1600,7 +1646,17 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1600
1646
|
|
|
1601
1647
|
useImperativeHandle(
|
|
1602
1648
|
ref,
|
|
1603
|
-
() =>
|
|
1649
|
+
() => {
|
|
1650
|
+
// v2.0.0 Track D — projections dispatch through a holder so they
|
|
1651
|
+
// always point at the freshly-built ref on every
|
|
1652
|
+
// useImperativeHandle re-run.
|
|
1653
|
+
const refHolder: { current: WordReviewEditorRef | null } = { current: null };
|
|
1654
|
+
const projections = buildRefProjections(() => {
|
|
1655
|
+
const r = refHolder.current;
|
|
1656
|
+
if (!r) throw new Error("ref projection used before ref initialization");
|
|
1657
|
+
return r;
|
|
1658
|
+
});
|
|
1659
|
+
const refValue: WordReviewEditorRef = ({
|
|
1604
1660
|
focus: () => activeRuntime.focus(),
|
|
1605
1661
|
blur: () => activeRuntime.blur(),
|
|
1606
1662
|
undo: () => activeRuntime.undo(),
|
|
@@ -1635,6 +1691,12 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1635
1691
|
addScope: (params) => activeRuntime.addScope(params),
|
|
1636
1692
|
getScope: (scopeId) => activeRuntime.getScope(scopeId),
|
|
1637
1693
|
removeScope: (scopeId) => activeRuntime.removeScope(scopeId),
|
|
1694
|
+
addInvisibleScope: (params) => activeRuntime.addInvisibleScope(params),
|
|
1695
|
+
setScopeVisibility: (scopeId, visibility) => activeRuntime.setScopeVisibility(scopeId, visibility),
|
|
1696
|
+
getScopeVisibility: (scopeId) => activeRuntime.getScopeVisibility(scopeId),
|
|
1697
|
+
setScopeChromeVisibility: (state) => activeRuntime.setScopeChromeVisibility(state),
|
|
1698
|
+
getScopeChromeVisibility: () => activeRuntime.getScopeChromeVisibility(),
|
|
1699
|
+
subscribeToScopeQuery: (filter, callback) => activeRuntime.subscribeToScopeQuery(filter, callback),
|
|
1638
1700
|
acceptChange: (changeId) => activeRuntime.acceptChange(changeId),
|
|
1639
1701
|
rejectChange: (changeId) => activeRuntime.rejectChange(changeId),
|
|
1640
1702
|
acceptAllChanges: () => activeRuntime.acceptAllChanges(),
|
|
@@ -1849,11 +1911,22 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1849
1911
|
clearSearch: () => {
|
|
1850
1912
|
surfaceRef.current?.clearSearch();
|
|
1851
1913
|
},
|
|
1852
|
-
setSelection: (selection) => {
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1914
|
+
setSelection: (selection, options?: SetSelectionOptions) => {
|
|
1915
|
+
const snap = activeRuntime.getRenderSnapshot();
|
|
1916
|
+
const normalized = normalizeRequestedSelection(snap, selection);
|
|
1917
|
+
if (options?.silent) {
|
|
1918
|
+
// Only suppress awareness publish if the selection will actually change.
|
|
1919
|
+
// If positions are identical the cursor-state useEffect won't re-run,
|
|
1920
|
+
// which would leave the flag set and silently suppress the next real
|
|
1921
|
+
// selection change (stale-flag bug).
|
|
1922
|
+
if (
|
|
1923
|
+
normalized.anchor !== snap.selection.anchor ||
|
|
1924
|
+
normalized.head !== snap.selection.head
|
|
1925
|
+
) {
|
|
1926
|
+
suppressNextAwarenessPublishRef.current = true;
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
applyRuntimeSelection(activeRuntime, normalized);
|
|
1857
1930
|
},
|
|
1858
1931
|
scrollToRevision: (revisionId: string) => {
|
|
1859
1932
|
const revision = activeRuntime.getRenderSnapshot().trackedChanges.revisions.find(
|
|
@@ -2032,6 +2105,8 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2032
2105
|
applyRuntimeSelection(activeRuntime, createSelectionFromAnchor(hits[0]!));
|
|
2033
2106
|
return hits.length;
|
|
2034
2107
|
},
|
|
2108
|
+
findTextWithStyle: (query, filter, opts) => activeRuntime.findTextWithStyle(query, filter, opts),
|
|
2109
|
+
selectTextWithStyle: (query, filter, opts) => activeRuntime.selectTextWithStyle(query, filter, opts),
|
|
2035
2110
|
// P17 — metadata persistence toggle + convert methods.
|
|
2036
2111
|
setMetadataPersistenceMode: (mode) => {
|
|
2037
2112
|
if (mode === "external" && scopeMetadataResolverRef.current === null) {
|
|
@@ -2226,7 +2301,11 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2226
2301
|
replaceWorkflowMarkupText: (markupId, text) => {
|
|
2227
2302
|
activeRuntime.replaceWorkflowMarkupText(markupId, text);
|
|
2228
2303
|
},
|
|
2229
|
-
|
|
2304
|
+
...projections,
|
|
2305
|
+
}) as WordReviewEditorRef;
|
|
2306
|
+
refHolder.current = refValue;
|
|
2307
|
+
return refValue;
|
|
2308
|
+
},
|
|
2230
2309
|
[
|
|
2231
2310
|
activeRuntime,
|
|
2232
2311
|
clearReviewSectionMarkById,
|
|
@@ -2540,29 +2619,40 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2540
2619
|
suggestionsSnapshot,
|
|
2541
2620
|
viewState.activeStory,
|
|
2542
2621
|
]);
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2622
|
+
// R9 — host may supply custom selection-tool registry entries; merge
|
|
2623
|
+
// with the default before resolving. The merger is a pure function
|
|
2624
|
+
// returning the canonical default array by reference when `custom`
|
|
2625
|
+
// is empty, so the typical no-custom case is a single identity call.
|
|
2626
|
+
const selectionToolRegistry = useMemo(
|
|
2627
|
+
() => resolveSelectionToolRegistry(customSelectionTools),
|
|
2628
|
+
[customSelectionTools],
|
|
2629
|
+
);
|
|
2630
|
+
const activeSelectionTool = resolveActiveSelectionTool(
|
|
2631
|
+
{
|
|
2632
|
+
snapshot,
|
|
2633
|
+
viewState,
|
|
2634
|
+
capabilities,
|
|
2635
|
+
documentNavigation,
|
|
2636
|
+
styleCatalog,
|
|
2637
|
+
formattingState,
|
|
2638
|
+
workflowScopeSnapshot,
|
|
2639
|
+
interactionGuardSnapshot,
|
|
2640
|
+
workflowMarkupSnapshot: workflowMarkupSnapshot ?? undefined,
|
|
2641
|
+
suggestionsSnapshot,
|
|
2642
|
+
activeRevisionId,
|
|
2643
|
+
activeCommentId: snapshot.comments.activeCommentId,
|
|
2644
|
+
activeCommentThread,
|
|
2645
|
+
activeTableContext,
|
|
2646
|
+
activeImageContext,
|
|
2647
|
+
activeObjectContext,
|
|
2648
|
+
activeListContext: viewState.activeListContext,
|
|
2649
|
+
preferListStructureContext: viewState.workspaceMode === "page",
|
|
2650
|
+
addCommentDisabledReason,
|
|
2651
|
+
suppressedSuggestionRevisionId,
|
|
2652
|
+
scopedChromePolicy,
|
|
2653
|
+
},
|
|
2654
|
+
selectionToolRegistry,
|
|
2655
|
+
);
|
|
2566
2656
|
const selectionToolbarSelectionKey = useMemo(
|
|
2567
2657
|
() =>
|
|
2568
2658
|
createSelectionToolbarSelectionKey(
|
|
@@ -3293,6 +3383,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
3293
3383
|
)}
|
|
3294
3384
|
visuallyHiddenStyles={VISUALLY_HIDDEN_STYLES}
|
|
3295
3385
|
onShellKeyDownCapture={handleShellKeyDownCapture}
|
|
3386
|
+
shellHeader={shellHeader}
|
|
3296
3387
|
viewState={viewState}
|
|
3297
3388
|
markupDisplay={liveMarkupDisplay}
|
|
3298
3389
|
currentUserId={currentUser.userId}
|
|
@@ -3395,6 +3486,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
3395
3486
|
onScopeRejectSuggestionGroup={(payload) => {
|
|
3396
3487
|
applySuggestionGroupAction(activeRuntime, payload.groupId, "reject");
|
|
3397
3488
|
}}
|
|
3489
|
+
onDeselectObject={() => activeRuntime.deselectObject()}
|
|
3398
3490
|
onScopeAskAgent={(payload) => {
|
|
3399
3491
|
// Resolve the scope's anchor + story from the facet's card
|
|
3400
3492
|
// model so the agent request carries the canonical range.
|
|
@@ -3449,13 +3541,28 @@ function applySuggestionGroupAction(
|
|
|
3449
3541
|
): void {
|
|
3450
3542
|
const snapshot = runtime.getSuggestionsSnapshot();
|
|
3451
3543
|
const group = snapshot.groups?.find((entry) => entry.groupId === groupId);
|
|
3452
|
-
|
|
3544
|
+
const op = action === "accept" ? "acceptSuggestionGroup" : "rejectSuggestionGroup";
|
|
3545
|
+
if (!group) {
|
|
3546
|
+
runtime.emitTransientWarning({
|
|
3547
|
+
warningId: `suggestion-group-unknown-${groupId}-${Date.now()}`,
|
|
3548
|
+
code: "review_target_not_found",
|
|
3549
|
+
severity: "info",
|
|
3550
|
+
message: `${op}("${groupId}") skipped: unknown groupId.`,
|
|
3551
|
+
source: "review",
|
|
3552
|
+
details: { op, targetId: groupId, reason: "group_unknown" },
|
|
3553
|
+
});
|
|
3554
|
+
return;
|
|
3555
|
+
}
|
|
3453
3556
|
const byId = new Map(
|
|
3454
3557
|
snapshot.suggestions.map((entry) => [entry.suggestionId, entry]),
|
|
3455
3558
|
);
|
|
3559
|
+
const skippedSuggestions: string[] = [];
|
|
3456
3560
|
for (const suggestionId of group.suggestionIds) {
|
|
3457
3561
|
const suggestion = byId.get(suggestionId);
|
|
3458
|
-
if (!suggestion)
|
|
3562
|
+
if (!suggestion) {
|
|
3563
|
+
skippedSuggestions.push(suggestionId);
|
|
3564
|
+
continue;
|
|
3565
|
+
}
|
|
3459
3566
|
for (const changeId of suggestion.changeIds) {
|
|
3460
3567
|
if (action === "accept") {
|
|
3461
3568
|
runtime.acceptChange(changeId);
|
|
@@ -3464,6 +3571,21 @@ function applySuggestionGroupAction(
|
|
|
3464
3571
|
}
|
|
3465
3572
|
}
|
|
3466
3573
|
}
|
|
3574
|
+
if (skippedSuggestions.length > 0) {
|
|
3575
|
+
runtime.emitTransientWarning({
|
|
3576
|
+
warningId: `suggestion-group-stale-${groupId}-${Date.now()}`,
|
|
3577
|
+
code: "review_target_not_found",
|
|
3578
|
+
severity: "info",
|
|
3579
|
+
message: `${op}("${groupId}") partially skipped: ${skippedSuggestions.length} suggestion(s) no longer in snapshot.`,
|
|
3580
|
+
source: "review",
|
|
3581
|
+
details: {
|
|
3582
|
+
op,
|
|
3583
|
+
targetId: groupId,
|
|
3584
|
+
reason: "suggestion_stale",
|
|
3585
|
+
skippedSuggestionIds: skippedSuggestions,
|
|
3586
|
+
},
|
|
3587
|
+
});
|
|
3588
|
+
}
|
|
3467
3589
|
}
|
|
3468
3590
|
|
|
3469
3591
|
function applyRuntimeFormattingOperation(
|
|
@@ -5036,12 +5158,34 @@ function applyRuntimeDeleteComment(
|
|
|
5036
5158
|
commentId: string,
|
|
5037
5159
|
): void {
|
|
5038
5160
|
const snapshot = runtime.getRenderSnapshot();
|
|
5039
|
-
|
|
5161
|
+
// Pre-ready / fatal states stay silent: the host called too early, and
|
|
5162
|
+
// there is no meaningful document yet to signal against. Emitting a
|
|
5163
|
+
// warning here would only add noise to load-time error handling.
|
|
5164
|
+
if (!snapshot.isReady || snapshot.fatalError) {
|
|
5165
|
+
return;
|
|
5166
|
+
}
|
|
5167
|
+
if (snapshot.readOnly) {
|
|
5168
|
+
runtime.emitTransientWarning({
|
|
5169
|
+
warningId: `delete-comment-readonly-${commentId}-${Date.now()}`,
|
|
5170
|
+
code: "review_target_not_found",
|
|
5171
|
+
severity: "info",
|
|
5172
|
+
message: `deleteComment("${commentId}") skipped: editor is read-only.`,
|
|
5173
|
+
source: "review",
|
|
5174
|
+
details: { op: "deleteComment", targetId: commentId, reason: "read_only" },
|
|
5175
|
+
});
|
|
5040
5176
|
return;
|
|
5041
5177
|
}
|
|
5042
5178
|
|
|
5043
5179
|
const sessionState = runtime.getSessionState();
|
|
5044
5180
|
if (!sessionState.canonicalDocument.review.comments[commentId]) {
|
|
5181
|
+
runtime.emitTransientWarning({
|
|
5182
|
+
warningId: `delete-comment-unknown-${commentId}-${Date.now()}`,
|
|
5183
|
+
code: "review_target_not_found",
|
|
5184
|
+
severity: "info",
|
|
5185
|
+
message: `deleteComment("${commentId}") skipped: unknown commentId.`,
|
|
5186
|
+
source: "review",
|
|
5187
|
+
details: { op: "deleteComment", targetId: commentId, reason: "comment_unknown" },
|
|
5188
|
+
});
|
|
5045
5189
|
return;
|
|
5046
5190
|
}
|
|
5047
5191
|
|
|
@@ -1015,6 +1015,7 @@ function createLoadingRuntimeBridge(input: {
|
|
|
1015
1015
|
subscribe: () => () => undefined,
|
|
1016
1016
|
subscribeToEvents: () => () => undefined,
|
|
1017
1017
|
emitBlockedCommand: () => undefined,
|
|
1018
|
+
emitTransientWarning: () => undefined,
|
|
1018
1019
|
getRenderSnapshot: () => {
|
|
1019
1020
|
const progressive = input.progressiveSurfaceRef?.current;
|
|
1020
1021
|
if (progressive == null) return input.snapshot;
|
|
@@ -1186,6 +1187,15 @@ function createLoadingRuntimeBridge(input: {
|
|
|
1186
1187
|
resetPerfCounters: () => undefined,
|
|
1187
1188
|
setVisibleBlockRange: () => undefined,
|
|
1188
1189
|
requestViewportRefresh: () => undefined,
|
|
1190
|
+
addInvisibleScope: () => ({ scopeId: "", anchor: { kind: "range", from: 0, to: 0, assoc: { start: -1, end: 1 } } }),
|
|
1191
|
+
setScopeVisibility: () => undefined,
|
|
1192
|
+
getScopeVisibility: () => "visible" as const,
|
|
1193
|
+
setScopeChromeVisibility: () => undefined,
|
|
1194
|
+
getScopeChromeVisibility: () => ({ mode: "all" as const }),
|
|
1195
|
+
subscribeToScopeQuery: (_filter, _callback) => () => undefined,
|
|
1196
|
+
findAllText: () => [],
|
|
1197
|
+
findTextWithStyle: () => [],
|
|
1198
|
+
selectTextWithStyle: () => 0,
|
|
1189
1199
|
};
|
|
1190
1200
|
}
|
|
1191
1201
|
|
|
@@ -82,6 +82,7 @@ export interface EditorShellViewProps {
|
|
|
82
82
|
selectionToolAnchor?: SelectionToolAnchor | null;
|
|
83
83
|
documentNavigation?: DocumentNavigationSnapshot;
|
|
84
84
|
commands: EditorCommandBag;
|
|
85
|
+
shellHeader?: ReactNode;
|
|
85
86
|
document: ReactNode;
|
|
86
87
|
onAddCommentFromSelection?: () => void;
|
|
87
88
|
onAddCommentFromSuggestion?: () => void;
|
|
@@ -126,6 +127,8 @@ export interface EditorShellViewProps {
|
|
|
126
127
|
}) => void;
|
|
127
128
|
/** K2 — forwarded from workspace to WordReviewEditor. */
|
|
128
129
|
onScopeAskAgent?: (payload: { scopeId: string }) => void;
|
|
130
|
+
/** N6 — deselects the currently grabbed object; wired to runtime.deselectObject(). */
|
|
131
|
+
onDeselectObject?: () => void;
|
|
129
132
|
}
|
|
130
133
|
|
|
131
134
|
export function EditorShellView(props: EditorShellViewProps) {
|
|
@@ -154,7 +157,7 @@ export function EditorShellView(props: EditorShellViewProps) {
|
|
|
154
157
|
aria-describedby={`${accessibilityInstructionsId} ${accessibilityStatusId}${
|
|
155
158
|
diagnosticsModeMessage ? ` ${accessibilityAlertId}` : ""
|
|
156
159
|
}`}
|
|
157
|
-
className="wre-editor relative h-full"
|
|
160
|
+
className="wre-editor wre-editor-root relative h-full"
|
|
158
161
|
onKeyDownCapture={onShellKeyDownCapture}
|
|
159
162
|
>
|
|
160
163
|
<p id={accessibilityInstructionsId} style={visuallyHiddenStyles}>
|
|
@@ -102,6 +102,59 @@ export const SELECTION_TOOL_REGISTRY: ReadonlyArray<SelectionToolRegistryEntry>
|
|
|
102
102
|
{ id: "blocked-explainer", surfaces: ["selection-tool"], group: "blocked", precedence: 60 },
|
|
103
103
|
];
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* R10 — options for `resolveSelectionToolRegistry`.
|
|
107
|
+
*
|
|
108
|
+
* `remove` lets a host suppress a default kind entirely (the dedicated
|
|
109
|
+
* builder for that kind is skipped at resolve time). Used together
|
|
110
|
+
* with `custom` when the host wants to both add a new precedence and
|
|
111
|
+
* silence an existing one.
|
|
112
|
+
*/
|
|
113
|
+
export interface ResolveSelectionToolRegistryOptions {
|
|
114
|
+
remove?: ReadonlyArray<SelectionToolRegistryEntry["id"]>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Merge host-supplied selection-tool entries with the default registry.
|
|
119
|
+
*
|
|
120
|
+
* design-close-chrome Phase 4 / designsystem §6 extensibility. Pure
|
|
121
|
+
* function — callers supply `custom` as a stable array (memoize in
|
|
122
|
+
* React), this returns a fresh, precedence-sorted copy that the
|
|
123
|
+
* resolver can iterate. Dedupe rule: host entries win on id collision
|
|
124
|
+
* (they override default precedence for that id). When `options.remove`
|
|
125
|
+
* lists ids, those are filtered out of the default set before merging.
|
|
126
|
+
*
|
|
127
|
+
* Note: as of this Phase, the resolver builder matrix (see
|
|
128
|
+
* `buildSelectionToolFromRegistryEntry` in `selection-tool-resolver.ts`)
|
|
129
|
+
* still only understands the built-in `SelectionToolKind` union.
|
|
130
|
+
* Widening that union to accept host render functions is a downstream
|
|
131
|
+
* change coordinated with Lane 8 API ergonomics; the merger is shipped
|
|
132
|
+
* first so callers can depend on the final shape while the renderer
|
|
133
|
+
* catches up.
|
|
134
|
+
*/
|
|
135
|
+
export function resolveSelectionToolRegistry(
|
|
136
|
+
custom?: ReadonlyArray<SelectionToolRegistryEntry>,
|
|
137
|
+
options?: ResolveSelectionToolRegistryOptions,
|
|
138
|
+
): ReadonlyArray<SelectionToolRegistryEntry> {
|
|
139
|
+
const remove = options?.remove;
|
|
140
|
+
const hasCustom = custom && custom.length > 0;
|
|
141
|
+
const hasRemove = remove && remove.length > 0;
|
|
142
|
+
if (!hasCustom && !hasRemove) return SELECTION_TOOL_REGISTRY;
|
|
143
|
+
const removed = new Set(remove ?? []);
|
|
144
|
+
const byId = new Map<SelectionToolRegistryEntry["id"], SelectionToolRegistryEntry>();
|
|
145
|
+
for (const entry of SELECTION_TOOL_REGISTRY) {
|
|
146
|
+
if (removed.has(entry.id)) continue;
|
|
147
|
+
byId.set(entry.id, entry);
|
|
148
|
+
}
|
|
149
|
+
for (const entry of custom ?? []) {
|
|
150
|
+
// Host entries are NOT filtered by `remove` — callers can always
|
|
151
|
+
// install an entry they meant to keep even when a matching default
|
|
152
|
+
// would otherwise have been suppressed.
|
|
153
|
+
byId.set(entry.id, entry);
|
|
154
|
+
}
|
|
155
|
+
return [...byId.values()].sort((a, b) => a.precedence - b.precedence);
|
|
156
|
+
}
|
|
157
|
+
|
|
105
158
|
export const TOOLBAR_CHROME_REGISTRY: ReadonlyArray<ToolbarChromeRegistryEntry> = [
|
|
106
159
|
{
|
|
107
160
|
id: "history",
|
|
@@ -23,10 +23,20 @@ import {
|
|
|
23
23
|
} from "./chrome-registry";
|
|
24
24
|
import { shouldRenderSelectionToolKind } from "./scoped-chrome-policy";
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the active selection tool for `input`.
|
|
28
|
+
*
|
|
29
|
+
* `registry` defaults to the canonical `SELECTION_TOOL_REGISTRY`. Hosts
|
|
30
|
+
* that want to reorder or shadow default precedences pass in the result
|
|
31
|
+
* of `resolveSelectionToolRegistry(props.customSelectionTools, options)`.
|
|
32
|
+
* Entries iterate in the order provided — callers are responsible for
|
|
33
|
+
* supplying a precedence-sorted array.
|
|
34
|
+
*/
|
|
26
35
|
export function resolveActiveSelectionTool(
|
|
27
36
|
input: SelectionToolResolverInput,
|
|
37
|
+
registry: ReadonlyArray<SelectionToolRegistryEntry> = SELECTION_TOOL_REGISTRY,
|
|
28
38
|
): ActiveSelectionToolModel | null {
|
|
29
|
-
for (const entry of
|
|
39
|
+
for (const entry of registry) {
|
|
30
40
|
if (!shouldRenderSelectionToolKind(input.scopedChromePolicy, entry.id)) {
|
|
31
41
|
continue;
|
|
32
42
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
EditorRole,
|
|
2
3
|
WordReviewEditorChromeOptions,
|
|
3
4
|
WordReviewEditorChromePreset,
|
|
4
5
|
WordReviewEditorChromeVisibility,
|
|
@@ -21,6 +22,7 @@ export function resolveChromePreset(
|
|
|
21
22
|
export function resolveChromePresetOptions(
|
|
22
23
|
chromePreset: WordReviewEditorChromePreset,
|
|
23
24
|
overrides?: Partial<WordReviewEditorChromeOptions>,
|
|
25
|
+
editorRole?: EditorRole,
|
|
24
26
|
): WordReviewEditorChromeOptions {
|
|
25
27
|
const defaults: Record<WordReviewEditorChromePreset, WordReviewEditorChromeOptions> = {
|
|
26
28
|
selection: {
|
|
@@ -73,10 +75,21 @@ export function resolveChromePresetOptions(
|
|
|
73
75
|
|
|
74
76
|
return {
|
|
75
77
|
...defaults[chromePreset],
|
|
78
|
+
...resolveRoleAdjustments(editorRole),
|
|
76
79
|
...overrides,
|
|
77
80
|
};
|
|
78
81
|
}
|
|
79
82
|
|
|
83
|
+
function resolveRoleAdjustments(role: EditorRole | undefined): Partial<WordReviewEditorChromeOptions> {
|
|
84
|
+
if (role === "editor") {
|
|
85
|
+
return { showReviewRail: false, showReviewQueueBar: false };
|
|
86
|
+
}
|
|
87
|
+
if (role === "workflow") {
|
|
88
|
+
return { showReviewQueueBar: false };
|
|
89
|
+
}
|
|
90
|
+
return {};
|
|
91
|
+
}
|
|
92
|
+
|
|
80
93
|
export function resolveChromeVisibilityForPreset(input: {
|
|
81
94
|
chromePreset: WordReviewEditorChromePreset;
|
|
82
95
|
chromeOptions?: Partial<WordReviewEditorChromeOptions>;
|