@beyondwork/docx-react-component 1.0.105 → 1.0.106
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/v3/_create.ts +9 -2
- package/src/api/v3/ai/_audit-reference.ts +28 -0
- package/src/api/v3/ai/_pe2-evidence.ts +272 -6
- package/src/api/v3/ai/attach.ts +22 -2
- package/src/api/v3/ai/bundle.ts +6 -2
- package/src/api/v3/ai/inspect.ts +6 -2
- package/src/api/v3/ai/replacement.ts +11 -0
- package/src/api/v3/index.ts +7 -0
- package/src/api/v3/ui/_types.ts +53 -0
- package/src/api/v3/ui/index.ts +4 -0
- package/src/api/v3/ui/viewport.ts +97 -0
- package/src/model/layout/page-graph-types.ts +29 -0
- package/src/runtime/document-runtime.ts +39 -18
- package/src/runtime/geometry/geometry-index.ts +74 -0
- package/src/runtime/layout/layout-engine-version.ts +8 -1
- package/src/runtime/layout/page-graph.ts +2 -0
- package/src/runtime/layout/paginated-layout-engine.ts +10 -0
- package/src/runtime/layout/project-block-fragments.ts +86 -7
- package/src/runtime/layout/public-facet.ts +84 -0
- package/src/runtime/workflow/index.ts +1 -0
- package/src/runtime/workflow/overlay-lanes.ts +228 -0
- package/src/ui/presence-overlay-lane.ts +131 -0
- package/src/ui/ui-controller-factory.ts +10 -0
|
@@ -34,6 +34,7 @@ import type {
|
|
|
34
34
|
RuntimePageNode,
|
|
35
35
|
RuntimePageRegion,
|
|
36
36
|
RuntimePageRegions,
|
|
37
|
+
RuntimeStoryAnchoredObject,
|
|
37
38
|
RuntimeTwipsRect,
|
|
38
39
|
} from "./page-graph.ts";
|
|
39
40
|
import type {
|
|
@@ -168,6 +169,33 @@ export interface PublicPageFrame {
|
|
|
168
169
|
rectTwips?: PublicTwipsRect;
|
|
169
170
|
fragmentIds: readonly string[];
|
|
170
171
|
}[];
|
|
172
|
+
pageLocalStories: readonly PublicPageLocalStoryInstance[];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface PublicPageLocalStoryInstance {
|
|
176
|
+
instanceId: string;
|
|
177
|
+
storyKey: string;
|
|
178
|
+
pageId: string;
|
|
179
|
+
kind: "header" | "footer";
|
|
180
|
+
variant: "default" | "first" | "even" | "odd";
|
|
181
|
+
relationshipId: string;
|
|
182
|
+
sectionIndex?: number;
|
|
183
|
+
anchoredObjects: readonly PublicStoryAnchoredObject[];
|
|
184
|
+
measuredFrameHeightTwips: number;
|
|
185
|
+
signature: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface PublicStoryAnchoredObject {
|
|
189
|
+
objectId: string;
|
|
190
|
+
sourceType: RuntimeStoryAnchoredObject["sourceType"];
|
|
191
|
+
display: RuntimeStoryAnchoredObject["display"];
|
|
192
|
+
extentTwips?: {
|
|
193
|
+
widthTwips: number;
|
|
194
|
+
heightTwips: number;
|
|
195
|
+
};
|
|
196
|
+
relationshipIds?: readonly string[];
|
|
197
|
+
preserveOnly: boolean;
|
|
198
|
+
divergenceIds: readonly string[];
|
|
171
199
|
}
|
|
172
200
|
|
|
173
201
|
export interface PublicPageNode {
|
|
@@ -265,9 +293,11 @@ export interface PublicBlockFragment {
|
|
|
265
293
|
};
|
|
266
294
|
columnIndex?: number;
|
|
267
295
|
continuation?: PublicLayoutContinuationCursor;
|
|
296
|
+
layoutObject?: PublicFragmentLayoutObject;
|
|
268
297
|
}
|
|
269
298
|
|
|
270
299
|
export type PublicLayoutContinuationCursor = RuntimeLayoutContinuationCursor;
|
|
300
|
+
export type PublicFragmentLayoutObject = NonNullable<RuntimeBlockFragment["layoutObject"]>;
|
|
271
301
|
|
|
272
302
|
/**
|
|
273
303
|
* P8 — One block snapshot rendered into a region of a page. Returned by
|
|
@@ -1478,6 +1508,45 @@ function toPublicPageFrame(frame: RuntimePageFrame): PublicPageFrame {
|
|
|
1478
1508
|
signature: frame.signature,
|
|
1479
1509
|
exclusionZoneCount: frame.regions.exclusionZones.length,
|
|
1480
1510
|
regionFrames: frameRegionEntries(frame),
|
|
1511
|
+
pageLocalStories: frame.pageLocalStories.map(toPublicPageLocalStory),
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
function toPublicPageLocalStory(
|
|
1516
|
+
story: RuntimePageFrame["pageLocalStories"][number],
|
|
1517
|
+
): PublicPageLocalStoryInstance {
|
|
1518
|
+
return {
|
|
1519
|
+
instanceId: story.instanceId,
|
|
1520
|
+
storyKey: story.storyKey,
|
|
1521
|
+
pageId: story.pageId,
|
|
1522
|
+
kind: story.kind,
|
|
1523
|
+
variant: story.variant,
|
|
1524
|
+
relationshipId: story.relationshipId,
|
|
1525
|
+
...(story.sectionIndex !== undefined ? { sectionIndex: story.sectionIndex } : {}),
|
|
1526
|
+
anchoredObjects: story.anchoredObjects.map(toPublicStoryAnchoredObject),
|
|
1527
|
+
measuredFrameHeightTwips: story.measuredFrameHeightTwips,
|
|
1528
|
+
signature: story.signature,
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
function toPublicStoryAnchoredObject(
|
|
1533
|
+
object: RuntimeStoryAnchoredObject,
|
|
1534
|
+
): PublicStoryAnchoredObject {
|
|
1535
|
+
return {
|
|
1536
|
+
objectId: object.objectId,
|
|
1537
|
+
sourceType: object.sourceType,
|
|
1538
|
+
display: object.display,
|
|
1539
|
+
...(object.extentTwips
|
|
1540
|
+
? {
|
|
1541
|
+
extentTwips: {
|
|
1542
|
+
widthTwips: object.extentTwips.widthTwips,
|
|
1543
|
+
heightTwips: object.extentTwips.heightTwips,
|
|
1544
|
+
},
|
|
1545
|
+
}
|
|
1546
|
+
: {}),
|
|
1547
|
+
...(object.relationshipIds ? { relationshipIds: [...object.relationshipIds] } : {}),
|
|
1548
|
+
preserveOnly: object.preserveOnly,
|
|
1549
|
+
divergenceIds: [...object.divergenceIds],
|
|
1481
1550
|
};
|
|
1482
1551
|
}
|
|
1483
1552
|
|
|
@@ -1556,6 +1625,9 @@ function toPublicBlockFragment(
|
|
|
1556
1625
|
...(fragment.continuation !== undefined
|
|
1557
1626
|
? { continuation: cloneContinuationCursor(fragment.continuation) }
|
|
1558
1627
|
: {}),
|
|
1628
|
+
...(fragment.layoutObject !== undefined
|
|
1629
|
+
? { layoutObject: cloneFragmentLayoutObject(fragment.layoutObject) }
|
|
1630
|
+
: {}),
|
|
1559
1631
|
};
|
|
1560
1632
|
}
|
|
1561
1633
|
|
|
@@ -1576,6 +1648,18 @@ function cloneContinuationCursor(
|
|
|
1576
1648
|
};
|
|
1577
1649
|
}
|
|
1578
1650
|
|
|
1651
|
+
function cloneFragmentLayoutObject(
|
|
1652
|
+
layoutObject: NonNullable<RuntimeBlockFragment["layoutObject"]>,
|
|
1653
|
+
): NonNullable<RuntimeBlockFragment["layoutObject"]> {
|
|
1654
|
+
return {
|
|
1655
|
+
...layoutObject,
|
|
1656
|
+
measuredExtentTwips: { ...layoutObject.measuredExtentTwips },
|
|
1657
|
+
...(layoutObject.fieldFamilies !== undefined
|
|
1658
|
+
? { fieldFamilies: [...layoutObject.fieldFamilies] }
|
|
1659
|
+
: {}),
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1579
1663
|
function toPublicLineBox(box: RuntimeLineBox): PublicLineBox {
|
|
1580
1664
|
return {
|
|
1581
1665
|
fragmentId: box.fragmentId,
|
|
@@ -19,6 +19,7 @@ export * from "./scope-writer.ts";
|
|
|
19
19
|
export * from "./metadata-writer.ts";
|
|
20
20
|
export * from "./projector.ts";
|
|
21
21
|
export * from "./overlay-store.ts";
|
|
22
|
+
export * from "./overlay-lanes.ts";
|
|
22
23
|
export * from "./coordinator.ts";
|
|
23
24
|
export * from "./visibility-policy.ts";
|
|
24
25
|
export * from "./markup-mode-policy.ts";
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CommentSidebarSnapshot,
|
|
3
|
+
SuggestionsSnapshot,
|
|
4
|
+
TrackedChangesSnapshot,
|
|
5
|
+
WorkflowMarkupSnapshot,
|
|
6
|
+
WorkflowScopeSnapshot,
|
|
7
|
+
} from "../../api/public-types.ts";
|
|
8
|
+
import { ISSUE_METADATA_ID } from "../../api/public-types.ts";
|
|
9
|
+
import type {
|
|
10
|
+
UiOverlayLaneEntry,
|
|
11
|
+
UiOverlayLaneKind,
|
|
12
|
+
UiOverlayLaneSnapshot,
|
|
13
|
+
UiOverlayLaneStatus,
|
|
14
|
+
} from "../../api/v3/ui/index.ts";
|
|
15
|
+
|
|
16
|
+
export type WorkflowReviewOverlayLaneKind = Extract<
|
|
17
|
+
UiOverlayLaneKind,
|
|
18
|
+
"redlines" | "comments" | "issues" | "field-scopes" | "broad-scopes" | "presence"
|
|
19
|
+
>;
|
|
20
|
+
|
|
21
|
+
export interface WorkflowReviewOverlayLaneInput {
|
|
22
|
+
readonly comments?: CommentSidebarSnapshot;
|
|
23
|
+
readonly trackedChanges?: TrackedChangesSnapshot;
|
|
24
|
+
readonly suggestions?: SuggestionsSnapshot | null;
|
|
25
|
+
readonly workflowScope?: WorkflowScopeSnapshot;
|
|
26
|
+
readonly workflowMarkup?: WorkflowMarkupSnapshot;
|
|
27
|
+
readonly revision?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Layer-06 data handoff for L10/L11 PE2 overlay lanes.
|
|
32
|
+
*
|
|
33
|
+
* This projector emits plain lane snapshots only. It does not resolve geometry,
|
|
34
|
+
* read DOM state, subscribe to awareness, dispatch PM transactions, or wake
|
|
35
|
+
* layout. Presence is intentionally unavailable here because live cursor/
|
|
36
|
+
* collaborator state is sourced from Yjs awareness, not durable workflow truth.
|
|
37
|
+
*/
|
|
38
|
+
export function projectWorkflowReviewOverlayLane(
|
|
39
|
+
kind: WorkflowReviewOverlayLaneKind,
|
|
40
|
+
input: WorkflowReviewOverlayLaneInput,
|
|
41
|
+
): UiOverlayLaneSnapshot {
|
|
42
|
+
if (kind === "presence") {
|
|
43
|
+
return {
|
|
44
|
+
kind,
|
|
45
|
+
status: "unavailable",
|
|
46
|
+
entries: [],
|
|
47
|
+
revision: input.revision ?? 0,
|
|
48
|
+
source: "awareness",
|
|
49
|
+
reason: "presence lane is sourced from Yjs awareness, not durable workflow/review state",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const entries = projectEntries(kind, input);
|
|
54
|
+
return {
|
|
55
|
+
kind,
|
|
56
|
+
status: summarizeStatus(entries),
|
|
57
|
+
entries,
|
|
58
|
+
revision: input.revision ?? 0,
|
|
59
|
+
source: "workflow",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function projectEntries(
|
|
64
|
+
kind: Exclude<WorkflowReviewOverlayLaneKind, "presence">,
|
|
65
|
+
input: WorkflowReviewOverlayLaneInput,
|
|
66
|
+
): UiOverlayLaneEntry[] {
|
|
67
|
+
switch (kind) {
|
|
68
|
+
case "comments":
|
|
69
|
+
return (input.comments?.threads ?? []).map((thread) => ({
|
|
70
|
+
id: thread.commentId,
|
|
71
|
+
status: thread.status === "detached" ? "requires-rehydration" : "resolved",
|
|
72
|
+
anchor: { kind: "comment", value: thread.commentId },
|
|
73
|
+
reason: thread.detachedReason,
|
|
74
|
+
data: compactData({
|
|
75
|
+
laneItemKind: "comment",
|
|
76
|
+
commentId: thread.commentId,
|
|
77
|
+
editorAnchor: thread.anchor,
|
|
78
|
+
status: thread.status,
|
|
79
|
+
createdAt: thread.createdAt,
|
|
80
|
+
createdBy: thread.createdBy,
|
|
81
|
+
entryCount: thread.entryCount,
|
|
82
|
+
warningCount: thread.warningCount,
|
|
83
|
+
linkedRevisionId: thread.linkedRevisionId,
|
|
84
|
+
excerpt: thread.excerpt,
|
|
85
|
+
anchorLabel: thread.anchorLabel,
|
|
86
|
+
}),
|
|
87
|
+
}));
|
|
88
|
+
case "redlines":
|
|
89
|
+
return (input.trackedChanges?.revisions ?? []).map((revision) => ({
|
|
90
|
+
id: revision.revisionId,
|
|
91
|
+
status: revision.status === "detached" ? "requires-rehydration" : "resolved",
|
|
92
|
+
anchor: { kind: "revision", value: revision.revisionId },
|
|
93
|
+
reason: revision.preserveOnlyReason,
|
|
94
|
+
data: compactData({
|
|
95
|
+
laneItemKind: "revision",
|
|
96
|
+
revisionId: revision.revisionId,
|
|
97
|
+
editorAnchor: revision.anchor,
|
|
98
|
+
revisionKind: revision.kind,
|
|
99
|
+
semanticKind: revision.semanticKind,
|
|
100
|
+
suggestionId: revision.suggestionId,
|
|
101
|
+
status: revision.status,
|
|
102
|
+
actionability: revision.actionability,
|
|
103
|
+
authorId: revision.authorId,
|
|
104
|
+
createdAt: revision.createdAt,
|
|
105
|
+
label: revision.label,
|
|
106
|
+
excerpt: revision.excerpt,
|
|
107
|
+
storyTarget: revision.storyTarget,
|
|
108
|
+
canAccept: revision.canAccept,
|
|
109
|
+
canReject: revision.canReject,
|
|
110
|
+
warningCount: revision.warningCount,
|
|
111
|
+
commentThreadIds: revision.commentThreadIds,
|
|
112
|
+
}),
|
|
113
|
+
}));
|
|
114
|
+
case "issues":
|
|
115
|
+
return projectIssueEntries(input);
|
|
116
|
+
case "field-scopes":
|
|
117
|
+
return (input.workflowMarkup?.fields ?? []).map((field) => ({
|
|
118
|
+
id: `field:${field.fieldIndex}`,
|
|
119
|
+
status: "resolved",
|
|
120
|
+
data: compactData({
|
|
121
|
+
laneItemKind: "field",
|
|
122
|
+
fieldIndex: field.fieldIndex,
|
|
123
|
+
editorAnchor: field.anchor,
|
|
124
|
+
fieldFamily: field.fieldFamily,
|
|
125
|
+
fieldTarget: field.fieldTarget,
|
|
126
|
+
refreshStatus: field.refreshStatus,
|
|
127
|
+
displayText: field.displayText,
|
|
128
|
+
label: field.label,
|
|
129
|
+
storyTarget: field.storyTarget,
|
|
130
|
+
}),
|
|
131
|
+
}));
|
|
132
|
+
case "broad-scopes":
|
|
133
|
+
return [
|
|
134
|
+
...(input.workflowScope?.scopes ?? []).map((scope) => ({
|
|
135
|
+
id: scope.scopeId,
|
|
136
|
+
status: "resolved" as const,
|
|
137
|
+
anchor: { kind: "scope" as const, value: scope.scopeId },
|
|
138
|
+
data: compactData({
|
|
139
|
+
laneItemKind: "workflow-scope",
|
|
140
|
+
scopeId: scope.scopeId,
|
|
141
|
+
editorAnchor: scope.anchor,
|
|
142
|
+
mode: scope.mode,
|
|
143
|
+
label: scope.label,
|
|
144
|
+
domain: scope.domain,
|
|
145
|
+
workItemId: scope.workItemId,
|
|
146
|
+
visibility: scope.visibility,
|
|
147
|
+
guardPolicy: scope.guardPolicy,
|
|
148
|
+
storyTarget: scope.storyTarget,
|
|
149
|
+
metadataRefs: scope.metadataRefs,
|
|
150
|
+
}),
|
|
151
|
+
})),
|
|
152
|
+
...(input.workflowScope?.candidates ?? []).map((candidate) => ({
|
|
153
|
+
id: candidate.candidateId,
|
|
154
|
+
status: "resolved" as const,
|
|
155
|
+
data: compactData({
|
|
156
|
+
laneItemKind: "workflow-candidate",
|
|
157
|
+
candidateId: candidate.candidateId,
|
|
158
|
+
editorAnchor: candidate.anchor,
|
|
159
|
+
label: candidate.label,
|
|
160
|
+
source: candidate.source,
|
|
161
|
+
storyTarget: candidate.storyTarget,
|
|
162
|
+
}),
|
|
163
|
+
})),
|
|
164
|
+
];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function projectIssueEntries(input: WorkflowReviewOverlayLaneInput): UiOverlayLaneEntry[] {
|
|
169
|
+
const suggestionGroupsByIssueId = new Map<string, string[]>();
|
|
170
|
+
for (const group of input.suggestions?.groups ?? []) {
|
|
171
|
+
if (!group.issueId) continue;
|
|
172
|
+
const ids = suggestionGroupsByIssueId.get(group.issueId) ?? [];
|
|
173
|
+
ids.push(group.groupId);
|
|
174
|
+
suggestionGroupsByIssueId.set(group.issueId, ids);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return (input.workflowMarkup?.metadata ?? [])
|
|
178
|
+
.filter((entry) => entry.metadataId === ISSUE_METADATA_ID)
|
|
179
|
+
.map((entry) => {
|
|
180
|
+
const issueValue = isRecord(entry.value) ? entry.value : undefined;
|
|
181
|
+
const issueId = typeof issueValue?.issueId === "string" ? issueValue.issueId : undefined;
|
|
182
|
+
return {
|
|
183
|
+
id: issueId ?? entry.entryId,
|
|
184
|
+
status: "resolved",
|
|
185
|
+
anchor: entry.scopeId ? { kind: "scope", value: entry.scopeId } : undefined,
|
|
186
|
+
data: compactData({
|
|
187
|
+
laneItemKind: "issue",
|
|
188
|
+
entryId: entry.entryId,
|
|
189
|
+
metadataId: entry.metadataId,
|
|
190
|
+
issueId,
|
|
191
|
+
editorAnchor: entry.anchor,
|
|
192
|
+
scopeId: entry.scopeId,
|
|
193
|
+
workItemId: entry.workItemId,
|
|
194
|
+
label: entry.label,
|
|
195
|
+
title: typeof issueValue?.title === "string" ? issueValue.title : undefined,
|
|
196
|
+
topic: typeof issueValue?.topic === "string" ? issueValue.topic : undefined,
|
|
197
|
+
severity: typeof issueValue?.severity === "string" ? issueValue.severity : undefined,
|
|
198
|
+
checklistState:
|
|
199
|
+
typeof issueValue?.checklistState === "string" ? issueValue.checklistState : undefined,
|
|
200
|
+
suggestionGroupIds: issueId ? suggestionGroupsByIssueId.get(issueId) ?? [] : [],
|
|
201
|
+
persistence: entry.persistence,
|
|
202
|
+
metadataPersistence: entry.metadataPersistence,
|
|
203
|
+
storageRef: entry.storageRef,
|
|
204
|
+
metadataVersion: entry.metadataVersion,
|
|
205
|
+
}),
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function summarizeStatus(entries: readonly UiOverlayLaneEntry[]): UiOverlayLaneStatus {
|
|
211
|
+
return entries.some((entry) => entry.status === "requires-rehydration")
|
|
212
|
+
? "requires-rehydration"
|
|
213
|
+
: "resolved";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function compactData(input: Record<string, unknown>): Readonly<Record<string, unknown>> {
|
|
217
|
+
const output: Record<string, unknown> = {};
|
|
218
|
+
for (const [key, value] of Object.entries(input)) {
|
|
219
|
+
if (value !== undefined) {
|
|
220
|
+
output[key] = value;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return output;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
227
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
228
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getRemoteCursorStates,
|
|
3
|
+
type RemoteCursorState,
|
|
4
|
+
} from "../api/v3/runtime/collab.ts";
|
|
5
|
+
import type {
|
|
6
|
+
GeometryRect,
|
|
7
|
+
UiListener,
|
|
8
|
+
UiOverlayLaneKind,
|
|
9
|
+
UiOverlayLaneSnapshot,
|
|
10
|
+
UiOverlayLaneStatus,
|
|
11
|
+
UiUnsubscribe,
|
|
12
|
+
} from "../api/v3/ui/_types.ts";
|
|
13
|
+
|
|
14
|
+
type AwarenessChangeListener = () => void;
|
|
15
|
+
|
|
16
|
+
export interface PresenceAwarenessSource {
|
|
17
|
+
readonly clientID: number;
|
|
18
|
+
getStates(): Map<number, Record<string, unknown>>;
|
|
19
|
+
on(event: "change", listener: AwarenessChangeListener): void;
|
|
20
|
+
off(event: "change", listener: AwarenessChangeListener): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type PresenceCursorProjection =
|
|
24
|
+
| { readonly status: "resolved"; readonly rects: readonly GeometryRect[]; readonly reason?: string }
|
|
25
|
+
| { readonly status: "requires-rehydration" | "unavailable"; readonly rects?: undefined; readonly reason?: string };
|
|
26
|
+
|
|
27
|
+
export interface PresenceOverlayLaneSourceOptions {
|
|
28
|
+
readonly awareness: PresenceAwarenessSource;
|
|
29
|
+
readonly localClientId?: number;
|
|
30
|
+
readonly getRevision?: () => number;
|
|
31
|
+
readonly projectCursor?: (cursor: RemoteCursorState) => PresenceCursorProjection;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PresenceOverlayLaneSource {
|
|
35
|
+
getLane(kind: UiOverlayLaneKind): UiOverlayLaneSnapshot;
|
|
36
|
+
subscribeLane(
|
|
37
|
+
kind: UiOverlayLaneKind,
|
|
38
|
+
listener: UiListener<UiOverlayLaneSnapshot>,
|
|
39
|
+
): UiUnsubscribe;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function unavailable(kind: UiOverlayLaneKind, reason: string): UiOverlayLaneSnapshot {
|
|
43
|
+
return Object.freeze({
|
|
44
|
+
__mock: true,
|
|
45
|
+
kind,
|
|
46
|
+
status: "unavailable",
|
|
47
|
+
entries: Object.freeze([]),
|
|
48
|
+
revision: 0,
|
|
49
|
+
source: "unavailable",
|
|
50
|
+
reason,
|
|
51
|
+
} as const);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function coerceLaneStatus(
|
|
55
|
+
current: UiOverlayLaneStatus,
|
|
56
|
+
next: UiOverlayLaneStatus,
|
|
57
|
+
): UiOverlayLaneStatus {
|
|
58
|
+
if (current === "unavailable" || next === "unavailable") return "unavailable";
|
|
59
|
+
if (current === "requires-rehydration" || next === "requires-rehydration") {
|
|
60
|
+
return "requires-rehydration";
|
|
61
|
+
}
|
|
62
|
+
return "resolved";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createPresenceOverlayLaneSource(
|
|
66
|
+
options: PresenceOverlayLaneSourceOptions,
|
|
67
|
+
): PresenceOverlayLaneSource {
|
|
68
|
+
const localClientId = options.localClientId ?? options.awareness.clientID;
|
|
69
|
+
|
|
70
|
+
function readPresenceLane(): UiOverlayLaneSnapshot {
|
|
71
|
+
const cursors = getRemoteCursorStates(
|
|
72
|
+
options.awareness as Parameters<typeof getRemoteCursorStates>[0],
|
|
73
|
+
localClientId,
|
|
74
|
+
);
|
|
75
|
+
let laneStatus: UiOverlayLaneStatus = "resolved";
|
|
76
|
+
const entries = cursors.map((cursor) => {
|
|
77
|
+
const projection = options.projectCursor?.(cursor) ?? {
|
|
78
|
+
status: "requires-rehydration" as const,
|
|
79
|
+
reason: "presence cursor geometry projection is not wired",
|
|
80
|
+
};
|
|
81
|
+
laneStatus = coerceLaneStatus(laneStatus, projection.status);
|
|
82
|
+
return {
|
|
83
|
+
id: `presence:${cursor.userId}`,
|
|
84
|
+
status: projection.status,
|
|
85
|
+
...(projection.rects ? { rects: projection.rects } : {}),
|
|
86
|
+
...(projection.reason ? { reason: projection.reason } : {}),
|
|
87
|
+
data: {
|
|
88
|
+
kind: "remote-cursor",
|
|
89
|
+
userId: cursor.userId,
|
|
90
|
+
displayName: cursor.displayName,
|
|
91
|
+
color: cursor.color,
|
|
92
|
+
anchor: cursor.anchor,
|
|
93
|
+
head: cursor.head,
|
|
94
|
+
storyTarget: cursor.storyTarget,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
kind: "presence",
|
|
101
|
+
status: entries.length === 0 ? "resolved" : laneStatus,
|
|
102
|
+
entries,
|
|
103
|
+
revision: options.getRevision?.() ?? 0,
|
|
104
|
+
source: "awareness",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
getLane(kind: UiOverlayLaneKind): UiOverlayLaneSnapshot {
|
|
110
|
+
if (kind !== "presence") {
|
|
111
|
+
return unavailable(kind, "presence lane source only handles kind='presence'");
|
|
112
|
+
}
|
|
113
|
+
return readPresenceLane();
|
|
114
|
+
},
|
|
115
|
+
subscribeLane(
|
|
116
|
+
kind: UiOverlayLaneKind,
|
|
117
|
+
listener: UiListener<UiOverlayLaneSnapshot>,
|
|
118
|
+
): UiUnsubscribe {
|
|
119
|
+
if (kind !== "presence") {
|
|
120
|
+
throw new Error(`presence lane source only handles kind='presence'; got "${kind}"`);
|
|
121
|
+
}
|
|
122
|
+
const onChange = (): void => {
|
|
123
|
+
listener(readPresenceLane());
|
|
124
|
+
};
|
|
125
|
+
options.awareness.on("change", onChange);
|
|
126
|
+
return () => {
|
|
127
|
+
options.awareness.off("change", onChange);
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -54,6 +54,7 @@ import type {
|
|
|
54
54
|
ChromePosture,
|
|
55
55
|
GeometryRect,
|
|
56
56
|
OverlayAnchorQuery,
|
|
57
|
+
PageResidencySnapshot,
|
|
57
58
|
UiOverlayLaneKind,
|
|
58
59
|
UiOverlayLaneSnapshot,
|
|
59
60
|
UiController,
|
|
@@ -113,6 +114,13 @@ export interface ShellUiControllerDeps {
|
|
|
113
114
|
* dpr / container-resize. Returns unsubscribe.
|
|
114
115
|
*/
|
|
115
116
|
readonly subscribeViewport?: (listener: UiListener<ViewportState>) => UiUnsubscribe;
|
|
117
|
+
/** PE2 page-residency policy read. */
|
|
118
|
+
readonly getPageResidency?: (pageIndex: number) => PageResidencySnapshot;
|
|
119
|
+
/** PE2 page-residency subscription channel. */
|
|
120
|
+
readonly subscribePageResidency?: (
|
|
121
|
+
pageIndex: number,
|
|
122
|
+
listener: UiListener<PageResidencySnapshot>,
|
|
123
|
+
) => UiUnsubscribe;
|
|
116
124
|
/**
|
|
117
125
|
* Overlay invalidation stream — fires when geometry invalidation overlaps
|
|
118
126
|
* an attached overlay query (U7). Typically delegated to the render
|
|
@@ -154,6 +162,8 @@ export function makeShellUiControllerFactory(
|
|
|
154
162
|
...(deps.subscribeChrome ? { subscribeChrome: deps.subscribeChrome } : {}),
|
|
155
163
|
...(deps.getViewport ? { getViewport: deps.getViewport } : {}),
|
|
156
164
|
...(deps.subscribeViewport ? { subscribeViewport: deps.subscribeViewport } : {}),
|
|
165
|
+
...(deps.getPageResidency ? { getPageResidency: deps.getPageResidency } : {}),
|
|
166
|
+
...(deps.subscribePageResidency ? { subscribePageResidency: deps.subscribePageResidency } : {}),
|
|
157
167
|
...(deps.subscribeOverlays ? { subscribeOverlays: deps.subscribeOverlays } : {}),
|
|
158
168
|
...(deps.getOverlayLane ? { getOverlayLane: deps.getOverlayLane } : {}),
|
|
159
169
|
...(deps.subscribeOverlayLane ? { subscribeOverlayLane: deps.subscribeOverlayLane } : {}),
|