@beyondwork/docx-react-component 1.0.39 → 1.0.40
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 +122 -0
- package/src/index.ts +9 -0
- package/src/runtime/document-runtime.ts +7 -0
- package/src/runtime/layout/docx-font-loader.ts +30 -11
- package/src/runtime/layout/inert-layout-facet.ts +1 -0
- package/src/runtime/layout/public-facet.ts +41 -0
- package/src/runtime/workflow-rail-segments.ts +149 -1
- package/src/ui/WordReviewEditor.tsx +17 -0
- package/src/ui/editor-shell-view.tsx +18 -0
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +80 -0
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +89 -20
- package/src/ui-tailwind/chrome-overlay/index.ts +2 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +55 -1
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +133 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +386 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +140 -69
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +7 -2
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +26 -1
- package/src/ui-tailwind/index.ts +5 -0
- package/src/ui-tailwind/theme/editor-theme.css +108 -15
- package/src/ui-tailwind/tw-review-workspace.tsx +75 -0
|
@@ -11,6 +11,14 @@
|
|
|
11
11
|
* `onSetRowHeight` callbacks (which route to `ref.tables.apply(op)`).
|
|
12
12
|
*
|
|
13
13
|
* Drag state lives in `useRef` so moves don't trigger re-renders mid-drag.
|
|
14
|
+
*
|
|
15
|
+
* Click-trap fix: the grip is an invisible 6px-wide (column) / 6px-tall (row)
|
|
16
|
+
* strip with `pointer-events-auto`, so a click landing near a cell edge hits
|
|
17
|
+
* the grip instead of cell text. To keep non-drag clicks from being swallowed,
|
|
18
|
+
* mousedown no longer calls `preventDefault` — that's deferred to the first
|
|
19
|
+
* mousemove that crosses `DRAG_THRESHOLD_PX`. A mouseup without any crossing
|
|
20
|
+
* is forwarded via `document.elementFromPoint` to the element beneath the
|
|
21
|
+
* grip so PM still receives the click and places the caret.
|
|
14
22
|
*/
|
|
15
23
|
|
|
16
24
|
import React, { useCallback, useEffect, useRef } from "react";
|
|
@@ -19,15 +27,53 @@ import type {
|
|
|
19
27
|
TableStructureContextSnapshot,
|
|
20
28
|
WordReviewEditorLayoutFacet,
|
|
21
29
|
} from "../../api/public-types";
|
|
22
|
-
import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection";
|
|
23
30
|
import type { OverlayCoordinateSpace } from "../chrome-overlay/chrome-overlay-projector";
|
|
24
31
|
import { projectRectToOverlay } from "../chrome-overlay/chrome-overlay-projector";
|
|
25
32
|
import { DEFAULT_PX_PER_TWIP } from "../../runtime/render/render-frame-types";
|
|
26
33
|
|
|
27
34
|
const GRIP_PX = 6;
|
|
35
|
+
const DRAG_THRESHOLD_PX = 3;
|
|
28
36
|
const MIN_COLUMN_TWIPS = 720;
|
|
29
37
|
const MIN_ROW_TWIPS = 120;
|
|
30
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Re-dispatch a click that landed on an invisible resize grip to the element
|
|
41
|
+
* beneath it. Called when a mouseup fires without any drag movement, so the
|
|
42
|
+
* user's intended target (typically PM-rendered cell text) still receives
|
|
43
|
+
* mousedown/mouseup/click and can place the caret.
|
|
44
|
+
*/
|
|
45
|
+
function forwardNonDragClick(gripEl: HTMLElement, event: MouseEvent): void {
|
|
46
|
+
const previous = gripEl.style.pointerEvents;
|
|
47
|
+
gripEl.style.pointerEvents = "none";
|
|
48
|
+
try {
|
|
49
|
+
const beneath = gripEl.ownerDocument?.elementFromPoint(
|
|
50
|
+
event.clientX,
|
|
51
|
+
event.clientY,
|
|
52
|
+
);
|
|
53
|
+
if (!beneath || beneath === gripEl) return;
|
|
54
|
+
const init: MouseEventInit = {
|
|
55
|
+
bubbles: true,
|
|
56
|
+
cancelable: true,
|
|
57
|
+
view: gripEl.ownerDocument?.defaultView ?? window,
|
|
58
|
+
clientX: event.clientX,
|
|
59
|
+
clientY: event.clientY,
|
|
60
|
+
screenX: event.screenX,
|
|
61
|
+
screenY: event.screenY,
|
|
62
|
+
button: event.button,
|
|
63
|
+
buttons: event.buttons,
|
|
64
|
+
ctrlKey: event.ctrlKey,
|
|
65
|
+
metaKey: event.metaKey,
|
|
66
|
+
shiftKey: event.shiftKey,
|
|
67
|
+
altKey: event.altKey,
|
|
68
|
+
};
|
|
69
|
+
beneath.dispatchEvent(new MouseEvent("mousedown", init));
|
|
70
|
+
beneath.dispatchEvent(new MouseEvent("mouseup", init));
|
|
71
|
+
beneath.dispatchEvent(new MouseEvent("click", init));
|
|
72
|
+
} finally {
|
|
73
|
+
gripEl.style.pointerEvents = previous;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
31
77
|
export interface TwTableGripLayerProps {
|
|
32
78
|
facet: WordReviewEditorLayoutFacet;
|
|
33
79
|
tableContext: TableStructureContextSnapshot | null;
|
|
@@ -131,31 +177,46 @@ function ColResizeGrip({
|
|
|
131
177
|
const dragRef = useRef<{
|
|
132
178
|
startX: number;
|
|
133
179
|
originalTwips: number;
|
|
180
|
+
dragStarted: boolean;
|
|
181
|
+
gripEl: HTMLElement;
|
|
134
182
|
} | null>(null);
|
|
135
183
|
|
|
136
184
|
const handleMouseDown = useCallback(
|
|
137
185
|
(e: React.MouseEvent<HTMLElement>) => {
|
|
138
186
|
if (disabled) return;
|
|
139
|
-
|
|
140
|
-
|
|
187
|
+
dragRef.current = {
|
|
188
|
+
startX: e.clientX,
|
|
189
|
+
originalTwips,
|
|
190
|
+
dragStarted: false,
|
|
191
|
+
gripEl: e.currentTarget,
|
|
192
|
+
};
|
|
141
193
|
},
|
|
142
194
|
[disabled, originalTwips],
|
|
143
195
|
);
|
|
144
196
|
|
|
145
197
|
useEffect(() => {
|
|
146
198
|
const handleMove = (e: MouseEvent) => {
|
|
147
|
-
|
|
199
|
+
const drag = dragRef.current;
|
|
200
|
+
if (!drag) return;
|
|
201
|
+
if (!drag.dragStarted) {
|
|
202
|
+
if (Math.abs(e.clientX - drag.startX) < DRAG_THRESHOLD_PX) return;
|
|
203
|
+
drag.dragStarted = true;
|
|
204
|
+
}
|
|
148
205
|
e.preventDefault();
|
|
149
206
|
};
|
|
150
207
|
const handleUp = (e: MouseEvent) => {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
208
|
+
const drag = dragRef.current;
|
|
209
|
+
if (!drag) return;
|
|
210
|
+
dragRef.current = null;
|
|
211
|
+
if (!drag.dragStarted) {
|
|
212
|
+
forwardNonDragClick(drag.gripEl, e);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const deltaTwips = (e.clientX - drag.startX) / pxPerTwip;
|
|
154
216
|
const newTwips = Math.max(
|
|
155
217
|
MIN_COLUMN_TWIPS,
|
|
156
|
-
Math.round(
|
|
218
|
+
Math.round(drag.originalTwips + deltaTwips),
|
|
157
219
|
);
|
|
158
|
-
dragRef.current = null;
|
|
159
220
|
onCommit?.(colIndex, newTwips);
|
|
160
221
|
};
|
|
161
222
|
window.addEventListener("mousemove", handleMove);
|
|
@@ -216,38 +277,46 @@ function RowResizeGrip({
|
|
|
216
277
|
}: RowResizeGripProps) {
|
|
217
278
|
const dragRef = useRef<{
|
|
218
279
|
startY: number;
|
|
219
|
-
|
|
280
|
+
dragStarted: boolean;
|
|
281
|
+
gripEl: HTMLElement;
|
|
220
282
|
} | null>(null);
|
|
221
283
|
|
|
222
284
|
const handleMouseDown = useCallback(
|
|
223
285
|
(e: React.MouseEvent<HTMLElement>) => {
|
|
224
286
|
if (disabled) return;
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
287
|
+
dragRef.current = {
|
|
288
|
+
startY: e.clientY,
|
|
289
|
+
dragStarted: false,
|
|
290
|
+
gripEl: e.currentTarget,
|
|
291
|
+
};
|
|
229
292
|
},
|
|
230
293
|
[disabled],
|
|
231
294
|
);
|
|
232
295
|
|
|
233
296
|
useEffect(() => {
|
|
234
297
|
const handleMove = (e: MouseEvent) => {
|
|
235
|
-
|
|
298
|
+
const drag = dragRef.current;
|
|
299
|
+
if (!drag) return;
|
|
300
|
+
if (!drag.dragStarted) {
|
|
301
|
+
if (Math.abs(e.clientY - drag.startY) < DRAG_THRESHOLD_PX) return;
|
|
302
|
+
drag.dragStarted = true;
|
|
303
|
+
}
|
|
236
304
|
e.preventDefault();
|
|
237
305
|
};
|
|
238
306
|
const handleUp = (e: MouseEvent) => {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
307
|
+
const drag = dragRef.current;
|
|
308
|
+
if (!drag) return;
|
|
309
|
+
dragRef.current = null;
|
|
310
|
+
if (!drag.dragStarted) {
|
|
311
|
+
forwardNonDragClick(drag.gripEl, e);
|
|
243
312
|
return;
|
|
244
313
|
}
|
|
314
|
+
const deltaY = e.clientY - drag.startY;
|
|
245
315
|
const deltaTwips = deltaY / pxPerTwip;
|
|
246
316
|
// We don't know the current row height from the grip alone; use atLeast
|
|
247
317
|
// so that a positive drag expands and a negative drag collapses to auto.
|
|
248
318
|
const newTwips = Math.max(MIN_ROW_TWIPS, Math.round(deltaTwips));
|
|
249
319
|
const rule = newTwips > 0 ? "atLeast" : "auto";
|
|
250
|
-
dragRef.current = null;
|
|
251
320
|
onCommit?.(rowIndex, newTwips, rule as "atLeast");
|
|
252
321
|
};
|
|
253
322
|
window.addEventListener("mousemove", handleMove);
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
export { TwChromeOverlay, type TwChromeOverlayProps } from "./tw-chrome-overlay";
|
|
9
9
|
export { TwScopeRailLayer, type TwScopeRailLayerProps } from "./tw-scope-rail-layer";
|
|
10
|
+
export { TwScopeCard, type TwScopeCardProps } from "./tw-scope-card";
|
|
11
|
+
export { TwScopeCardLayer, type TwScopeCardLayerProps } from "./tw-scope-card-layer";
|
|
10
12
|
export {
|
|
11
13
|
inflateRect,
|
|
12
14
|
projectRectToOverlay,
|
|
@@ -16,10 +16,13 @@ import * as React from "react";
|
|
|
16
16
|
import type { OverlayCoordinateSpace } from "./chrome-overlay-projector";
|
|
17
17
|
import type { ScopeRailSegment } from "../../runtime/layout";
|
|
18
18
|
import type {
|
|
19
|
+
ScopeIssueAction,
|
|
19
20
|
TableStructureContextSnapshot,
|
|
20
21
|
WordReviewEditorLayoutFacet,
|
|
22
|
+
WorkflowScopeMode,
|
|
21
23
|
} from "../../api/public-types";
|
|
22
24
|
import { TwScopeRailLayer } from "./tw-scope-rail-layer";
|
|
25
|
+
import { TwScopeCardLayer } from "./tw-scope-card-layer";
|
|
23
26
|
import { TwTableGripLayer } from "../chrome/tw-table-grip-layer";
|
|
24
27
|
|
|
25
28
|
export interface TwChromeOverlayProps {
|
|
@@ -29,8 +32,38 @@ export interface TwChromeOverlayProps {
|
|
|
29
32
|
space?: OverlayCoordinateSpace;
|
|
30
33
|
/** Active scope id (for emphasis + rail tab sync). */
|
|
31
34
|
activeScopeId?: string | null;
|
|
32
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Click handler fired when the user clicks a scope rail stripe.
|
|
37
|
+
* P0 wires this to open the scope card (P1 ships the card layer).
|
|
38
|
+
*/
|
|
39
|
+
onScopeStripeClick?: (segment: ScopeRailSegment) => void;
|
|
40
|
+
/**
|
|
41
|
+
* Legacy click handler kept for compatibility with consumers that
|
|
42
|
+
* subscribed before the stripe affordance existed. Called alongside
|
|
43
|
+
* `onScopeStripeClick` on a stripe click.
|
|
44
|
+
*/
|
|
33
45
|
onScopeSegmentClick?: (segment: ScopeRailSegment) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Fires when the scope card is dismissed (close button, Escape, or
|
|
48
|
+
* click-outside). The owner uses this to clear `activeScopeId`.
|
|
49
|
+
*/
|
|
50
|
+
onScopeCardClose?: () => void;
|
|
51
|
+
/**
|
|
52
|
+
* Fires when a mode button inside the scope card is clicked. The
|
|
53
|
+
* owner relays this into a `scope-mode-change-requested` event for
|
|
54
|
+
* the host (which then drives the overlay-apply path).
|
|
55
|
+
*/
|
|
56
|
+
onScopeCardModeChange?: (scopeId: string, mode: WorkflowScopeMode) => void;
|
|
57
|
+
/**
|
|
58
|
+
* Fires when an issue action button (Resolve/Waive/Escalate) inside
|
|
59
|
+
* the scope card is clicked. The owner relays this into a
|
|
60
|
+
* `scope-issue-action-requested` event for the host.
|
|
61
|
+
*/
|
|
62
|
+
onScopeCardIssueAction?: (
|
|
63
|
+
scopeId: string,
|
|
64
|
+
issueId: string,
|
|
65
|
+
action: ScopeIssueAction,
|
|
66
|
+
) => void;
|
|
34
67
|
/** Test id applied to the overlay root. */
|
|
35
68
|
"data-testid"?: string;
|
|
36
69
|
/** Optional extra children (e.g., future comment balloon layer). */
|
|
@@ -62,7 +95,11 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
|
|
|
62
95
|
facet,
|
|
63
96
|
space,
|
|
64
97
|
activeScopeId,
|
|
98
|
+
onScopeStripeClick,
|
|
65
99
|
onScopeSegmentClick,
|
|
100
|
+
onScopeCardClose,
|
|
101
|
+
onScopeCardModeChange,
|
|
102
|
+
onScopeCardIssueAction,
|
|
66
103
|
"data-testid": testId,
|
|
67
104
|
children,
|
|
68
105
|
tableContext,
|
|
@@ -79,8 +116,17 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
|
|
|
79
116
|
facet={facet}
|
|
80
117
|
space={space}
|
|
81
118
|
activeScopeId={activeScopeId}
|
|
119
|
+
onStripeClick={onScopeStripeClick}
|
|
82
120
|
onSegmentClick={onScopeSegmentClick}
|
|
83
121
|
/>
|
|
122
|
+
<TwScopeCardLayer
|
|
123
|
+
facet={facet}
|
|
124
|
+
activeScopeId={activeScopeId ?? null}
|
|
125
|
+
onClose={onScopeCardClose ?? noop}
|
|
126
|
+
onModeChange={onScopeCardModeChange ?? noopModeChange}
|
|
127
|
+
onIssueAction={onScopeCardIssueAction ?? noopIssueAction}
|
|
128
|
+
space={space}
|
|
129
|
+
/>
|
|
84
130
|
<TwTableGripLayer
|
|
85
131
|
facet={facet}
|
|
86
132
|
tableContext={tableContext ?? null}
|
|
@@ -93,4 +139,12 @@ export const TwChromeOverlay: React.FC<TwChromeOverlayProps> = ({
|
|
|
93
139
|
);
|
|
94
140
|
};
|
|
95
141
|
|
|
142
|
+
const noop = () => undefined;
|
|
143
|
+
const noopModeChange = (_scopeId: string, _mode: WorkflowScopeMode) => undefined;
|
|
144
|
+
const noopIssueAction = (
|
|
145
|
+
_scopeId: string,
|
|
146
|
+
_issueId: string,
|
|
147
|
+
_action: ScopeIssueAction,
|
|
148
|
+
) => undefined;
|
|
149
|
+
|
|
96
150
|
export default TwChromeOverlay;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TwScopeCardLayer — renders at most one scope card at a time, chosen
|
|
3
|
+
* by `activeScopeId`. Consumes `facet.getAllScopeCardModels()` to
|
|
4
|
+
* look up the model, then positions the card 8px above the model's
|
|
5
|
+
* `primaryAnchorRect`.
|
|
6
|
+
*
|
|
7
|
+
* Per docs/plans/scope-card-overlay.md P1, the layer keeps
|
|
8
|
+
* positioning pure: it never calls `getBoundingClientRect` or reads
|
|
9
|
+
* DOM rects. Coordinates come from the render kernel's anchor index
|
|
10
|
+
* through `ScopeCardModel.primaryAnchorRect`, projected into the
|
|
11
|
+
* overlay's own coordinate space via the shared projector.
|
|
12
|
+
*
|
|
13
|
+
* Auto-flip, pin, and overlapping-scope stacking land in P3. P1
|
|
14
|
+
* renders a single card above the scope's first line and falls back
|
|
15
|
+
* to a top-left overlay placement when no rect is resolvable.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import * as React from "react";
|
|
19
|
+
import {
|
|
20
|
+
projectRectToOverlay,
|
|
21
|
+
type OverlayCoordinateSpace,
|
|
22
|
+
} from "./chrome-overlay-projector";
|
|
23
|
+
import { TwScopeCard } from "./tw-scope-card";
|
|
24
|
+
import type {
|
|
25
|
+
ScopeIssueAction,
|
|
26
|
+
WorkflowScopeMode,
|
|
27
|
+
} from "../../api/public-types";
|
|
28
|
+
import type { RenderFrameRect } from "../../runtime/render";
|
|
29
|
+
import type { WordReviewEditorLayoutFacet } from "../../runtime/layout";
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Types
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
export interface TwScopeCardLayerProps {
|
|
36
|
+
facet: WordReviewEditorLayoutFacet;
|
|
37
|
+
activeScopeId: string | null;
|
|
38
|
+
onClose: () => void;
|
|
39
|
+
onModeChange: (scopeId: string, mode: WorkflowScopeMode) => void;
|
|
40
|
+
onIssueAction: (
|
|
41
|
+
scopeId: string,
|
|
42
|
+
issueId: string,
|
|
43
|
+
action: ScopeIssueAction,
|
|
44
|
+
) => void;
|
|
45
|
+
space?: OverlayCoordinateSpace;
|
|
46
|
+
"data-testid"?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Component
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
const CARD_GAP_PX = 8;
|
|
54
|
+
const CARD_ESTIMATED_HEIGHT_PX = 160;
|
|
55
|
+
const CARD_FALLBACK_LEFT_PX = 16;
|
|
56
|
+
const CARD_FALLBACK_TOP_PX = 16;
|
|
57
|
+
|
|
58
|
+
export const TwScopeCardLayer: React.FC<TwScopeCardLayerProps> = ({
|
|
59
|
+
facet,
|
|
60
|
+
activeScopeId,
|
|
61
|
+
onClose,
|
|
62
|
+
onModeChange,
|
|
63
|
+
onIssueAction,
|
|
64
|
+
space,
|
|
65
|
+
"data-testid": testId,
|
|
66
|
+
}) => {
|
|
67
|
+
if (!activeScopeId) return null;
|
|
68
|
+
const models =
|
|
69
|
+
typeof facet.getAllScopeCardModels === "function"
|
|
70
|
+
? facet.getAllScopeCardModels()
|
|
71
|
+
: [];
|
|
72
|
+
const model = models.find((m) => m.scopeId === activeScopeId);
|
|
73
|
+
if (!model) return null;
|
|
74
|
+
|
|
75
|
+
const projectorSpace: OverlayCoordinateSpace =
|
|
76
|
+
space ?? { originLeftPx: 0, originTopPx: 0 };
|
|
77
|
+
const positioned = resolveCardPosition(
|
|
78
|
+
model.primaryAnchorRect,
|
|
79
|
+
projectorSpace,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div
|
|
84
|
+
className="wre-scope-card-layer pointer-events-none absolute inset-0 z-30"
|
|
85
|
+
data-testid={testId ?? "scope-card-layer"}
|
|
86
|
+
data-active-scope-id={activeScopeId}
|
|
87
|
+
>
|
|
88
|
+
<div style={positioned} className="absolute">
|
|
89
|
+
<TwScopeCard
|
|
90
|
+
model={model}
|
|
91
|
+
onClose={onClose}
|
|
92
|
+
onModeChange={(mode) => onModeChange(model.scopeId, mode)}
|
|
93
|
+
onIssueAction={(action) => {
|
|
94
|
+
if (!model.issue) return;
|
|
95
|
+
onIssueAction(model.scopeId, model.issue.issueId, action);
|
|
96
|
+
}}
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Positioning
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
function resolveCardPosition(
|
|
108
|
+
anchor: RenderFrameRect | null,
|
|
109
|
+
space: OverlayCoordinateSpace,
|
|
110
|
+
): React.CSSProperties {
|
|
111
|
+
if (!anchor) {
|
|
112
|
+
return {
|
|
113
|
+
left: `${CARD_FALLBACK_LEFT_PX}px`,
|
|
114
|
+
top: `${CARD_FALLBACK_TOP_PX}px`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
// Position the card above the anchor's first line, leaving
|
|
118
|
+
// `CARD_GAP_PX` between anchor top and card bottom. P3 adds auto-
|
|
119
|
+
// flip based on viewport clipping.
|
|
120
|
+
const targetRect: RenderFrameRect = {
|
|
121
|
+
leftPx: anchor.leftPx,
|
|
122
|
+
topPx: Math.max(0, anchor.topPx - CARD_ESTIMATED_HEIGHT_PX - CARD_GAP_PX),
|
|
123
|
+
widthPx: 1,
|
|
124
|
+
heightPx: 1,
|
|
125
|
+
};
|
|
126
|
+
const projected = projectRectToOverlay(targetRect, space);
|
|
127
|
+
return {
|
|
128
|
+
left: projected.left,
|
|
129
|
+
top: projected.top,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export default TwScopeCardLayer;
|