@beyondwork/docx-react-component 1.0.37 → 1.0.38
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/package.json +1 -1
- package/src/api/public-types.ts +319 -1
- package/src/core/commands/section-layout-commands.ts +58 -0
- package/src/core/commands/table-grid.ts +431 -0
- package/src/core/commands/table-structure-commands.ts +815 -55
- package/src/io/export/serialize-main-document.ts +2 -11
- package/src/io/export/serialize-numbering.ts +1 -2
- package/src/io/export/serialize-tables.ts +74 -0
- package/src/io/export/table-properties-xml.ts +139 -4
- package/src/io/normalize/normalize-text.ts +15 -0
- package/src/io/ooxml/parse-footnotes.ts +60 -0
- package/src/io/ooxml/parse-headers-footers.ts +60 -0
- package/src/io/ooxml/parse-main-document.ts +137 -0
- package/src/io/ooxml/parse-tables.ts +249 -0
- package/src/model/canonical-document.ts +34 -0
- package/src/runtime/document-layout.ts +4 -2
- package/src/runtime/document-navigation.ts +1 -1
- package/src/runtime/document-runtime.ts +114 -0
- package/src/runtime/layout/default-page-format.ts +96 -0
- package/src/runtime/layout/index.ts +45 -0
- package/src/runtime/layout/inert-layout-facet.ts +14 -0
- package/src/runtime/layout/layout-engine-instance.ts +33 -23
- package/src/runtime/layout/margin-preset-catalog.ts +178 -0
- package/src/runtime/layout/page-format-catalog.ts +233 -0
- package/src/runtime/layout/page-graph.ts +19 -0
- package/src/runtime/layout/paginated-layout-engine.ts +142 -9
- package/src/runtime/layout/project-block-fragments.ts +91 -0
- package/src/runtime/layout/public-facet.ts +709 -16
- package/src/runtime/layout/table-render-plan.ts +229 -0
- package/src/runtime/render/block-fragment-projection.ts +35 -0
- package/src/runtime/render/decoration-resolver.ts +189 -0
- package/src/runtime/render/index.ts +57 -0
- package/src/runtime/render/pending-op-delta-reader.ts +129 -0
- package/src/runtime/render/render-frame-types.ts +317 -0
- package/src/runtime/render/render-kernel.ts +755 -0
- package/src/runtime/view-state.ts +67 -0
- package/src/runtime/workflow-markup.ts +1 -5
- package/src/runtime/workflow-rail-segments.ts +280 -0
- package/src/ui/WordReviewEditor.tsx +84 -15
- package/src/ui/editor-shell-view.tsx +6 -0
- package/src/ui/headless/chrome-registry.ts +280 -14
- package/src/ui/headless/scoped-chrome-policy.ts +20 -1
- package/src/ui/headless/selection-tool-types.ts +10 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
- package/src/ui-tailwind/chrome/role-action-sets.ts +74 -0
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +163 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +57 -92
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +274 -138
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +90 -0
- package/src/ui-tailwind/chrome-overlay/index.ts +22 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +86 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
- package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +95 -0
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +4 -0
- package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +11 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +7 -1
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +12 -1
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +22 -12
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +3 -0
- package/src/ui-tailwind/index.ts +33 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
- package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
- package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
- package/src/ui-tailwind/theme/editor-theme.css +498 -163
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +559 -0
- package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +69 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +136 -1
|
@@ -19,13 +19,17 @@ import type {
|
|
|
19
19
|
ActiveListContext,
|
|
20
20
|
ActiveNoteContext,
|
|
21
21
|
CaretAffinity,
|
|
22
|
+
ChromePinsState,
|
|
23
|
+
ChromePinSurface,
|
|
22
24
|
DocumentMode,
|
|
25
|
+
EditorRole,
|
|
23
26
|
EditorStoryTarget,
|
|
24
27
|
EditorSurfaceSnapshot,
|
|
25
28
|
EditorViewStateSnapshot,
|
|
26
29
|
LayoutMeasurement,
|
|
27
30
|
PageLayoutSnapshot,
|
|
28
31
|
PageRegionHitTest,
|
|
32
|
+
PinState,
|
|
29
33
|
SelectionSnapshot,
|
|
30
34
|
SurfaceBlockSnapshot,
|
|
31
35
|
SurfaceInlineSegment,
|
|
@@ -44,6 +48,20 @@ export interface ViewState {
|
|
|
44
48
|
caretAffinity: CaretAffinity;
|
|
45
49
|
activePageRegion: PageRegionHitTest | null;
|
|
46
50
|
activeObjectFrame: LayoutMeasurement["objectFrame"] | null;
|
|
51
|
+
/**
|
|
52
|
+
* Role-scoped chrome dimension (spec §6.4). Host apps drive the role via
|
|
53
|
+
* `setEditorRole`; the mounted shell reads this to pick a per-role
|
|
54
|
+
* toolbar action set. Independent of `viewMode` — one user session may
|
|
55
|
+
* start in "review" role and switch to "editor" without changing
|
|
56
|
+
* workspace or document mode.
|
|
57
|
+
*/
|
|
58
|
+
editorRole: EditorRole;
|
|
59
|
+
/**
|
|
60
|
+
* Pin state for detachable chrome surfaces (topnav, selection tier).
|
|
61
|
+
* Lives here so it survives snapshot rebuilds within one session.
|
|
62
|
+
* Absent key ⇒ docked default.
|
|
63
|
+
*/
|
|
64
|
+
chromePins: ChromePinsState;
|
|
47
65
|
}
|
|
48
66
|
|
|
49
67
|
const MIN_ZOOM_PERCENT = 50;
|
|
@@ -58,6 +76,8 @@ const DEFAULT_VIEW_STATE: ViewState = {
|
|
|
58
76
|
caretAffinity: "none",
|
|
59
77
|
activePageRegion: null,
|
|
60
78
|
activeObjectFrame: null,
|
|
79
|
+
editorRole: "editor",
|
|
80
|
+
chromePins: {},
|
|
61
81
|
};
|
|
62
82
|
|
|
63
83
|
export function createViewState(initial?: Partial<ViewState>): ViewState {
|
|
@@ -113,6 +133,37 @@ export function setActiveObjectFrame(
|
|
|
113
133
|
return { ...state, activeObjectFrame: frame };
|
|
114
134
|
}
|
|
115
135
|
|
|
136
|
+
export function setEditorRole(state: ViewState, role: EditorRole): ViewState {
|
|
137
|
+
if (state.editorRole === role) return state;
|
|
138
|
+
return { ...state, editorRole: role };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function setChromePin(
|
|
142
|
+
state: ViewState,
|
|
143
|
+
surface: ChromePinSurface,
|
|
144
|
+
pin: PinState | null,
|
|
145
|
+
): ViewState {
|
|
146
|
+
const next: ChromePinsState = { ...state.chromePins };
|
|
147
|
+
if (pin === null) {
|
|
148
|
+
if (next[surface] === undefined) {
|
|
149
|
+
return state;
|
|
150
|
+
}
|
|
151
|
+
delete next[surface];
|
|
152
|
+
} else {
|
|
153
|
+
const current = next[surface];
|
|
154
|
+
if (
|
|
155
|
+
current &&
|
|
156
|
+
current.detached === pin.detached &&
|
|
157
|
+
current.offset.x === pin.offset.x &&
|
|
158
|
+
current.offset.y === pin.offset.y
|
|
159
|
+
) {
|
|
160
|
+
return state;
|
|
161
|
+
}
|
|
162
|
+
next[surface] = pin;
|
|
163
|
+
}
|
|
164
|
+
return { ...state, chromePins: next };
|
|
165
|
+
}
|
|
166
|
+
|
|
116
167
|
/**
|
|
117
168
|
* Derive list context from the surface block at the current selection head.
|
|
118
169
|
*/
|
|
@@ -219,9 +270,25 @@ export function createEditorViewStateSnapshot(
|
|
|
219
270
|
activeObjectFrame: derivedViewState.activeObjectFrame,
|
|
220
271
|
measurement,
|
|
221
272
|
isFocused: derivedViewState.isFocused,
|
|
273
|
+
editorRole: derivedViewState.editorRole,
|
|
274
|
+
chromePins: cloneChromePins(derivedViewState.chromePins),
|
|
222
275
|
};
|
|
223
276
|
}
|
|
224
277
|
|
|
278
|
+
function cloneChromePins(pins: ChromePinsState): ChromePinsState {
|
|
279
|
+
const out: ChromePinsState = {};
|
|
280
|
+
for (const key of Object.keys(pins) as ChromePinSurface[]) {
|
|
281
|
+
const pin = pins[key];
|
|
282
|
+
if (pin) {
|
|
283
|
+
out[key] = {
|
|
284
|
+
detached: pin.detached,
|
|
285
|
+
offset: { x: pin.offset.x, y: pin.offset.y },
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return out;
|
|
290
|
+
}
|
|
291
|
+
|
|
225
292
|
// ---------------------------------------------------------------------------
|
|
226
293
|
// Internal helpers
|
|
227
294
|
// ---------------------------------------------------------------------------
|
|
@@ -341,11 +341,7 @@ function collectOpaqueFragmentMarkup(
|
|
|
341
341
|
const seen = new Set(existing.map((item) => item.fragmentId));
|
|
342
342
|
|
|
343
343
|
return Object.values(preservation.opaqueFragments)
|
|
344
|
-
.filter(
|
|
345
|
-
(fragment) =>
|
|
346
|
-
!seen.has(fragment.fragmentId)
|
|
347
|
-
&& fragment.packagePartName === "/word/document.xml",
|
|
348
|
-
)
|
|
344
|
+
.filter((fragment) => !seen.has(fragment.fragmentId))
|
|
349
345
|
.map((fragment) => {
|
|
350
346
|
const descriptor = describeOpaqueFragment(fragment);
|
|
351
347
|
const blockedReasonCode = isBlockedImportFeatureKey(descriptor.featureKey)
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow rail-segment projection.
|
|
3
|
+
*
|
|
4
|
+
* Per runtime-rendering-and-chrome-phase.md §5, the action rail v2 renders
|
|
5
|
+
* OUTSIDE the PM NodeView tree as an overlay layer positioned from
|
|
6
|
+
* canonical scope data. This module joins the host-supplied
|
|
7
|
+
* `WorkflowOverlay` (scopes, candidates) + blocked-reason ranges +
|
|
8
|
+
* locked-zone data with the runtime page graph to produce
|
|
9
|
+
* `ScopeRailSegment[]` — the shape chrome consumes to render the
|
|
10
|
+
* left-gutter label column and the flat block-tint.
|
|
11
|
+
*
|
|
12
|
+
* The segments are pure reads over canonical state; no DOM, no PM.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
EditorAnchorProjection,
|
|
17
|
+
EditorStoryTarget,
|
|
18
|
+
WorkflowBlockedCommandReason,
|
|
19
|
+
WorkflowCandidateRange,
|
|
20
|
+
WorkflowLockedZone,
|
|
21
|
+
WorkflowScope,
|
|
22
|
+
} from "../api/public-types";
|
|
23
|
+
import { MAIN_STORY_TARGET, storyTargetsEqual } from "../core/selection/mapping.ts";
|
|
24
|
+
import type { RuntimePageGraph } from "./layout/page-graph.ts";
|
|
25
|
+
import type { RenderFrameRect } from "./render/render-frame-types.ts";
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Public shape
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
export type ScopeRailPosture =
|
|
32
|
+
| "edit"
|
|
33
|
+
| "suggest"
|
|
34
|
+
| "comment"
|
|
35
|
+
| "view"
|
|
36
|
+
| "candidate"
|
|
37
|
+
| "preserve-only"
|
|
38
|
+
| "blocked-import";
|
|
39
|
+
|
|
40
|
+
export interface ScopeRailSegment {
|
|
41
|
+
/** Identifier the chrome uses to sync with the Workflow rail tab. */
|
|
42
|
+
scopeId: string;
|
|
43
|
+
/** Visual+accessibility posture keyed off the scope mode or block reason. */
|
|
44
|
+
posture: ScopeRailPosture;
|
|
45
|
+
/** Human label; empty string when none was supplied by the host. */
|
|
46
|
+
label: string;
|
|
47
|
+
/** Runtime offsets (inclusive from, exclusive to) this segment spans on the active story. */
|
|
48
|
+
fromOffset: number;
|
|
49
|
+
toOffset: number;
|
|
50
|
+
/** Story the segment sits on. */
|
|
51
|
+
storyTarget: EditorStoryTarget;
|
|
52
|
+
/** Page index the segment renders on (may span multiple pages; emitted per page). */
|
|
53
|
+
pageIndex: number;
|
|
54
|
+
/** Section index derived from the page graph. */
|
|
55
|
+
sectionIndex: number;
|
|
56
|
+
/** True when the scope is the active work item. */
|
|
57
|
+
isActiveWorkItem: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Body-tint rect in overlay-space pixels, populated when the render kernel
|
|
60
|
+
* is available. Chrome consumers read this directly instead of
|
|
61
|
+
* re-projecting per render via the overlay projector. `null` when the
|
|
62
|
+
* segment is produced without a kernel (e.g., in tests or before the
|
|
63
|
+
* facet is bound to a page graph) — consumers fall back to per-render
|
|
64
|
+
* anchor resolution in that case.
|
|
65
|
+
*/
|
|
66
|
+
bodyTintRect: RenderFrameRect | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Collector input
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
export interface CollectScopeRailSegmentsInput {
|
|
74
|
+
scopes: readonly WorkflowScope[] | undefined;
|
|
75
|
+
candidates?: readonly WorkflowCandidateRange[];
|
|
76
|
+
blockedReasons?: readonly WorkflowBlockedCommandReason[];
|
|
77
|
+
lockedZones?: readonly WorkflowLockedZone[];
|
|
78
|
+
activeWorkItemScopeIds?: readonly string[];
|
|
79
|
+
/** Active story scopes render on; segments for other stories are skipped. */
|
|
80
|
+
activeStory?: EditorStoryTarget;
|
|
81
|
+
pageGraph: RuntimePageGraph;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Entry point
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Build segments for every page in the graph. Callers that only want a
|
|
90
|
+
* single page can filter by `segment.pageIndex` on the returned list.
|
|
91
|
+
*/
|
|
92
|
+
export function collectScopeRailSegments(
|
|
93
|
+
input: CollectScopeRailSegmentsInput,
|
|
94
|
+
): ScopeRailSegment[] {
|
|
95
|
+
const segments: ScopeRailSegment[] = [];
|
|
96
|
+
const activeStory = input.activeStory ?? MAIN_STORY_TARGET;
|
|
97
|
+
const activeIds = new Set(input.activeWorkItemScopeIds ?? []);
|
|
98
|
+
|
|
99
|
+
for (const scope of input.scopes ?? []) {
|
|
100
|
+
const range = anchorToRuntimeRange(scope.anchor);
|
|
101
|
+
if (!range) continue;
|
|
102
|
+
const storyTarget = scope.storyTarget ?? MAIN_STORY_TARGET;
|
|
103
|
+
if (!storyTargetsEqual(storyTarget, activeStory)) continue;
|
|
104
|
+
|
|
105
|
+
const posture = resolveScopePosture(scope);
|
|
106
|
+
const isActiveWorkItem = activeIds.has(scope.scopeId);
|
|
107
|
+
for (const page of pagesCoveringRange(input.pageGraph, range.from, range.to)) {
|
|
108
|
+
const { from, to } = clipRangeToPage(range.from, range.to, page);
|
|
109
|
+
if (from >= to) continue;
|
|
110
|
+
segments.push({
|
|
111
|
+
scopeId: scope.scopeId,
|
|
112
|
+
posture,
|
|
113
|
+
label: scope.label ?? "",
|
|
114
|
+
fromOffset: from,
|
|
115
|
+
toOffset: to,
|
|
116
|
+
storyTarget,
|
|
117
|
+
pageIndex: page.pageIndex,
|
|
118
|
+
sectionIndex: page.sectionIndex,
|
|
119
|
+
isActiveWorkItem,
|
|
120
|
+
bodyTintRect: null,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Candidates render as a faint "candidate" posture so the reader knows
|
|
126
|
+
// where the host is proposing scopes before they're committed.
|
|
127
|
+
for (const candidate of input.candidates ?? []) {
|
|
128
|
+
const range = anchorToRuntimeRange(candidate.anchor);
|
|
129
|
+
if (!range) continue;
|
|
130
|
+
const storyTarget = candidate.storyTarget ?? MAIN_STORY_TARGET;
|
|
131
|
+
if (!storyTargetsEqual(storyTarget, activeStory)) continue;
|
|
132
|
+
|
|
133
|
+
for (const page of pagesCoveringRange(input.pageGraph, range.from, range.to)) {
|
|
134
|
+
const { from, to } = clipRangeToPage(range.from, range.to, page);
|
|
135
|
+
if (from >= to) continue;
|
|
136
|
+
segments.push({
|
|
137
|
+
scopeId: candidate.candidateId,
|
|
138
|
+
posture: "candidate",
|
|
139
|
+
label: candidate.label ?? "",
|
|
140
|
+
fromOffset: from,
|
|
141
|
+
toOffset: to,
|
|
142
|
+
storyTarget,
|
|
143
|
+
pageIndex: page.pageIndex,
|
|
144
|
+
sectionIndex: page.sectionIndex,
|
|
145
|
+
isActiveWorkItem: false,
|
|
146
|
+
bodyTintRect: null,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Blocked-reason anchors: render a "blocked-import" or "preserve-only"
|
|
152
|
+
// posture so the rail signal matches the chrome message in image copy.png.
|
|
153
|
+
for (const reason of input.blockedReasons ?? []) {
|
|
154
|
+
if (!reason.anchor) continue;
|
|
155
|
+
const range = anchorToRuntimeRange(reason.anchor);
|
|
156
|
+
if (!range) continue;
|
|
157
|
+
const storyTarget = reason.storyTarget ?? MAIN_STORY_TARGET;
|
|
158
|
+
if (!storyTargetsEqual(storyTarget, activeStory)) continue;
|
|
159
|
+
|
|
160
|
+
const posture: ScopeRailPosture =
|
|
161
|
+
reason.code === "workflow_blocked_import"
|
|
162
|
+
? "blocked-import"
|
|
163
|
+
: reason.code === "workflow_preserve_only"
|
|
164
|
+
? "preserve-only"
|
|
165
|
+
: "view";
|
|
166
|
+
for (const page of pagesCoveringRange(input.pageGraph, range.from, range.to)) {
|
|
167
|
+
const { from, to } = clipRangeToPage(range.from, range.to, page);
|
|
168
|
+
if (from >= to) continue;
|
|
169
|
+
segments.push({
|
|
170
|
+
scopeId: `blocked:${reason.code}:${range.from}-${range.to}`,
|
|
171
|
+
posture,
|
|
172
|
+
label: reason.label ?? reason.message ?? "",
|
|
173
|
+
fromOffset: from,
|
|
174
|
+
toOffset: to,
|
|
175
|
+
storyTarget,
|
|
176
|
+
pageIndex: page.pageIndex,
|
|
177
|
+
sectionIndex: page.sectionIndex,
|
|
178
|
+
isActiveWorkItem: false,
|
|
179
|
+
bodyTintRect: null,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Locked zones project as their own preserve-only / blocked-import rails so
|
|
185
|
+
// locked cells, locked images, and locked runs show the lock posture in
|
|
186
|
+
// the gutter even without a workflow scope covering them.
|
|
187
|
+
for (const zone of input.lockedZones ?? []) {
|
|
188
|
+
const range = anchorToRuntimeRange(zone.anchor);
|
|
189
|
+
if (!range) continue;
|
|
190
|
+
const storyTarget = zone.storyTarget ?? MAIN_STORY_TARGET;
|
|
191
|
+
if (!storyTargetsEqual(storyTarget, activeStory)) continue;
|
|
192
|
+
|
|
193
|
+
const posture: ScopeRailPosture =
|
|
194
|
+
zone.code === "workflow_blocked_import"
|
|
195
|
+
? "blocked-import"
|
|
196
|
+
: "preserve-only";
|
|
197
|
+
for (const page of pagesCoveringRange(input.pageGraph, range.from, range.to)) {
|
|
198
|
+
const { from, to } = clipRangeToPage(range.from, range.to, page);
|
|
199
|
+
if (from >= to) continue;
|
|
200
|
+
segments.push({
|
|
201
|
+
scopeId: zone.fragmentId ?? `locked:${range.from}-${range.to}`,
|
|
202
|
+
posture,
|
|
203
|
+
label: zone.label ?? "",
|
|
204
|
+
fromOffset: from,
|
|
205
|
+
toOffset: to,
|
|
206
|
+
storyTarget,
|
|
207
|
+
pageIndex: page.pageIndex,
|
|
208
|
+
sectionIndex: page.sectionIndex,
|
|
209
|
+
isActiveWorkItem: false,
|
|
210
|
+
bodyTintRect: null,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return segments;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// Internals
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
function anchorToRuntimeRange(
|
|
223
|
+
anchor: EditorAnchorProjection,
|
|
224
|
+
): { from: number; to: number } | null {
|
|
225
|
+
if (anchor.kind === "range") {
|
|
226
|
+
const from = Math.min(anchor.from, anchor.to);
|
|
227
|
+
const to = Math.max(anchor.from, anchor.to);
|
|
228
|
+
return from < to ? { from, to } : null;
|
|
229
|
+
}
|
|
230
|
+
if (anchor.kind === "node") {
|
|
231
|
+
return { from: anchor.at, to: anchor.at + 1 };
|
|
232
|
+
}
|
|
233
|
+
// detached anchors cannot be rendered; use the last-known range as a
|
|
234
|
+
// best-effort ghost until the scope is re-anchored or dismissed.
|
|
235
|
+
if (anchor.lastKnownRange) {
|
|
236
|
+
const from = Math.min(anchor.lastKnownRange.from, anchor.lastKnownRange.to);
|
|
237
|
+
const to = Math.max(anchor.lastKnownRange.from, anchor.lastKnownRange.to);
|
|
238
|
+
return from < to ? { from, to } : null;
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function pagesCoveringRange(
|
|
244
|
+
graph: RuntimePageGraph,
|
|
245
|
+
from: number,
|
|
246
|
+
to: number,
|
|
247
|
+
): RuntimePageGraph["pages"] {
|
|
248
|
+
return graph.pages.filter(
|
|
249
|
+
(page) =>
|
|
250
|
+
!page.isBlankFiller &&
|
|
251
|
+
page.endOffset > from &&
|
|
252
|
+
page.startOffset < to,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function clipRangeToPage(
|
|
257
|
+
from: number,
|
|
258
|
+
to: number,
|
|
259
|
+
page: RuntimePageGraph["pages"][number],
|
|
260
|
+
): { from: number; to: number } {
|
|
261
|
+
return {
|
|
262
|
+
from: Math.max(from, page.startOffset),
|
|
263
|
+
to: Math.min(to, page.endOffset),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function resolveScopePosture(scope: WorkflowScope): ScopeRailPosture {
|
|
268
|
+
switch (scope.mode) {
|
|
269
|
+
case "edit":
|
|
270
|
+
return "edit";
|
|
271
|
+
case "suggest":
|
|
272
|
+
return "suggest";
|
|
273
|
+
case "comment":
|
|
274
|
+
return "comment";
|
|
275
|
+
case "view":
|
|
276
|
+
return "view";
|
|
277
|
+
default:
|
|
278
|
+
return "view";
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -48,6 +48,8 @@ import type {
|
|
|
48
48
|
StyleCatalogSnapshot,
|
|
49
49
|
SurfaceBlockSnapshot,
|
|
50
50
|
SurfaceInlineSegment,
|
|
51
|
+
TableOp,
|
|
52
|
+
TableOpResult,
|
|
51
53
|
TrackedChangeEntrySnapshot,
|
|
52
54
|
TocRefreshResult,
|
|
53
55
|
UpdateFieldsResult,
|
|
@@ -113,6 +115,7 @@ import {
|
|
|
113
115
|
import {
|
|
114
116
|
applyTableStructureOperation,
|
|
115
117
|
getTableStructureContext,
|
|
118
|
+
type TableStructureOperation,
|
|
116
119
|
} from "../core/commands/table-structure-commands.ts";
|
|
117
120
|
import {
|
|
118
121
|
deleteSelectionOrBackward,
|
|
@@ -579,6 +582,7 @@ export function __createWordReviewEditorRefBridge(
|
|
|
579
582
|
return clonePublicValue(runtime.getRuntimeContextAnalytics(query));
|
|
580
583
|
},
|
|
581
584
|
layout: runtime.layout,
|
|
585
|
+
tables: buildTablesFacet(runtime, mountedSurface ?? null),
|
|
582
586
|
goToNextReviewItem: () => {
|
|
583
587
|
return clonePublicValue(navigateReviewQueue(runtime, "next"));
|
|
584
588
|
},
|
|
@@ -1394,6 +1398,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1394
1398
|
return clonePublicValue(activeRuntime.getRuntimeContextAnalytics(query));
|
|
1395
1399
|
},
|
|
1396
1400
|
layout: activeRuntime.layout,
|
|
1401
|
+
tables: buildTablesFacet(activeRuntime, surfaceRef.current ?? null),
|
|
1397
1402
|
goToNextReviewItem: () => {
|
|
1398
1403
|
return clonePublicValue(navigateMountedReviewQueue("next"));
|
|
1399
1404
|
},
|
|
@@ -2300,6 +2305,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2300
2305
|
activeRevisionId={activeRevisionId}
|
|
2301
2306
|
showTrackedChanges={showTrackedChanges}
|
|
2302
2307
|
workflowScopeSnapshot={workflowScopeSnapshot}
|
|
2308
|
+
layoutFacet={activeRuntime.layout}
|
|
2303
2309
|
interactionGuardSnapshot={interactionGuardSnapshot}
|
|
2304
2310
|
chromePreset={effectiveChromePreset}
|
|
2305
2311
|
chromeOptions={chromeOptions}
|
|
@@ -3388,28 +3394,19 @@ function applyRuntimeImageReposition(
|
|
|
3388
3394
|
function applyRuntimeTableStructureOperation(
|
|
3389
3395
|
runtime: WordReviewEditorRuntime,
|
|
3390
3396
|
mountedSurface: TwProseMirrorSurfaceRef | null | undefined,
|
|
3391
|
-
operation:
|
|
3392
|
-
|
|
3393
|
-
| { type: "add-row-after" }
|
|
3394
|
-
| { type: "add-column-before" }
|
|
3395
|
-
| { type: "add-column-after" }
|
|
3396
|
-
| { type: "delete-row" }
|
|
3397
|
-
| { type: "delete-column" }
|
|
3398
|
-
| { type: "delete-table" }
|
|
3399
|
-
| { type: "merge-cells" }
|
|
3400
|
-
| { type: "split-cell" }
|
|
3401
|
-
| { type: "set-cell-background"; color: string },
|
|
3402
|
-
): void {
|
|
3397
|
+
operation: TableStructureOperation,
|
|
3398
|
+
): { changed: boolean; coercedReason: string | null } {
|
|
3403
3399
|
if (isSelectionSuggesting(runtime)) {
|
|
3400
|
+
const coercedReason = `Table operation "${operation.type}" is not supported in suggesting mode.`;
|
|
3404
3401
|
runtime.emitBlockedCommand(`table.${operation.type}`, [{
|
|
3405
3402
|
code: "unsupported_surface",
|
|
3406
|
-
message:
|
|
3403
|
+
message: coercedReason,
|
|
3407
3404
|
}]);
|
|
3408
|
-
return;
|
|
3405
|
+
return { changed: false, coercedReason };
|
|
3409
3406
|
}
|
|
3410
3407
|
const context = getStoryMutationContext(runtime, `table.${operation.type}`);
|
|
3411
3408
|
if (!context) {
|
|
3412
|
-
return;
|
|
3409
|
+
return { changed: false, coercedReason: "No active mutation context." };
|
|
3413
3410
|
}
|
|
3414
3411
|
|
|
3415
3412
|
const result = applyTableStructureOperation(
|
|
@@ -3419,6 +3416,78 @@ function applyRuntimeTableStructureOperation(
|
|
|
3419
3416
|
operation,
|
|
3420
3417
|
);
|
|
3421
3418
|
dispatchStoryMutationResult(runtime, context, result, context.timestamp);
|
|
3419
|
+
return {
|
|
3420
|
+
changed: result.changed,
|
|
3421
|
+
coercedReason: result.changed ? null : "Op was a no-op against the active selection.",
|
|
3422
|
+
};
|
|
3423
|
+
}
|
|
3424
|
+
|
|
3425
|
+
/**
|
|
3426
|
+
* Translate a public-API `TableOp` (kebab-case `kind` discriminator) to
|
|
3427
|
+
* the internal `TableStructureOperation` (`type` discriminator). The
|
|
3428
|
+
* shape values are identical aside from the discriminator name.
|
|
3429
|
+
*/
|
|
3430
|
+
export function __publicTableOpToInternal(op: TableOp): TableStructureOperation {
|
|
3431
|
+
return publicTableOpToInternal(op);
|
|
3432
|
+
}
|
|
3433
|
+
|
|
3434
|
+
function publicTableOpToInternal(op: TableOp): TableStructureOperation {
|
|
3435
|
+
const { kind, ...rest } = op as { kind: string } & Record<string, unknown>;
|
|
3436
|
+
if (kind === "insert") {
|
|
3437
|
+
throw new Error(
|
|
3438
|
+
"TableOp kind \"insert\" is not routed through ref.tables.apply; use ref.insertTable(...).",
|
|
3439
|
+
);
|
|
3440
|
+
}
|
|
3441
|
+
return { type: kind, ...rest } as TableStructureOperation;
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
/**
|
|
3445
|
+
* Build the `ref.tables` facet: a typed dispatch boundary + capability
|
|
3446
|
+
* read. Delegates every op through the same `applyRuntimeTableStructureOperation`
|
|
3447
|
+
* helper the flat ref verbs use, so there is exactly one server-side path
|
|
3448
|
+
* for every table mutation.
|
|
3449
|
+
*/
|
|
3450
|
+
function buildTablesFacet(
|
|
3451
|
+
runtime: WordReviewEditorRuntime,
|
|
3452
|
+
mountedSurface: TwProseMirrorSurfaceRef | null,
|
|
3453
|
+
) {
|
|
3454
|
+
const getCapabilities = () => {
|
|
3455
|
+
const snapshot = runtime.getRenderSnapshot();
|
|
3456
|
+
const document = runtime.getSessionState().canonicalDocument;
|
|
3457
|
+
return (
|
|
3458
|
+
clonePublicValue(
|
|
3459
|
+
getTableStructureContext(
|
|
3460
|
+
document,
|
|
3461
|
+
snapshot,
|
|
3462
|
+
mountedSurface?.getTableSelection() ?? null,
|
|
3463
|
+
),
|
|
3464
|
+
) ?? null
|
|
3465
|
+
);
|
|
3466
|
+
};
|
|
3467
|
+
return {
|
|
3468
|
+
apply(op: TableOp): TableOpResult {
|
|
3469
|
+
if (op.kind === "insert") {
|
|
3470
|
+
applyRuntimeInsertTable(runtime, { rows: op.rows, columns: op.columns });
|
|
3471
|
+
return {
|
|
3472
|
+
changed: true,
|
|
3473
|
+
coercedReason: null,
|
|
3474
|
+
capabilities: getCapabilities(),
|
|
3475
|
+
};
|
|
3476
|
+
}
|
|
3477
|
+
const internal = publicTableOpToInternal(op);
|
|
3478
|
+
const outcome = applyRuntimeTableStructureOperation(
|
|
3479
|
+
runtime,
|
|
3480
|
+
mountedSurface,
|
|
3481
|
+
internal,
|
|
3482
|
+
);
|
|
3483
|
+
return {
|
|
3484
|
+
changed: outcome.changed,
|
|
3485
|
+
coercedReason: outcome.coercedReason,
|
|
3486
|
+
capabilities: getCapabilities(),
|
|
3487
|
+
};
|
|
3488
|
+
},
|
|
3489
|
+
getCapabilities,
|
|
3490
|
+
};
|
|
3422
3491
|
}
|
|
3423
3492
|
|
|
3424
3493
|
function applyRuntimeTextCommand(
|
|
@@ -55,6 +55,12 @@ export interface EditorShellViewProps {
|
|
|
55
55
|
activeRevisionId?: string;
|
|
56
56
|
showTrackedChanges: boolean;
|
|
57
57
|
workflowScopeSnapshot?: WorkflowScopeSnapshot | null;
|
|
58
|
+
/**
|
|
59
|
+
* Runtime-owned layout facet passed through to the workspace so the
|
|
60
|
+
* ChromeOverlay (scope rail, workflow dock, etc.) can render over the
|
|
61
|
+
* document column and the review rail's Workflow tab can read segments.
|
|
62
|
+
*/
|
|
63
|
+
layoutFacet?: import("../runtime/layout/index.ts").WordReviewEditorLayoutFacet;
|
|
58
64
|
interactionGuardSnapshot?: InteractionGuardSnapshot;
|
|
59
65
|
chromePreset?: WordReviewEditorChromePreset;
|
|
60
66
|
chromeOptions?: Partial<WordReviewEditorChromeOptions>;
|