@blocksdiy/react-common 1.70.2 → 1.71.1
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/dist/comments/anchor.d.ts +96 -0
- package/dist/comments/anchor.d.ts.map +1 -0
- package/dist/comments/anchor.js +346 -0
- package/dist/comments/api.d.ts +38 -0
- package/dist/comments/api.d.ts.map +1 -0
- package/dist/comments/api.js +62 -0
- package/dist/comments/comment-agents.d.ts +28 -0
- package/dist/comments/comment-agents.d.ts.map +1 -0
- package/dist/comments/comment-agents.js +32 -0
- package/dist/comments/comment-mode.d.ts +115 -0
- package/dist/comments/comment-mode.d.ts.map +1 -0
- package/dist/comments/comment-mode.js +176 -0
- package/dist/comments/composer.d.ts +133 -0
- package/dist/comments/composer.d.ts.map +1 -0
- package/dist/comments/composer.js +273 -0
- package/dist/comments/index.d.ts +19 -0
- package/dist/comments/index.d.ts.map +1 -0
- package/dist/comments/index.js +23 -0
- package/dist/comments/layout.d.ts +25 -0
- package/dist/comments/layout.d.ts.map +1 -0
- package/dist/comments/layout.js +83 -0
- package/dist/comments/mentions.d.ts +32 -0
- package/dist/comments/mentions.d.ts.map +1 -0
- package/dist/comments/mentions.js +30 -0
- package/dist/comments/menu.d.ts +33 -0
- package/dist/comments/menu.d.ts.map +1 -0
- package/dist/comments/menu.js +47 -0
- package/dist/comments/open-comment.d.ts +28 -0
- package/dist/comments/open-comment.d.ts.map +1 -0
- package/dist/comments/open-comment.js +9 -0
- package/dist/comments/page-pick.d.ts +76 -0
- package/dist/comments/page-pick.d.ts.map +1 -0
- package/dist/comments/page-pick.js +283 -0
- package/dist/comments/pick-message.d.ts +11 -0
- package/dist/comments/pick-message.d.ts.map +1 -0
- package/dist/comments/pick-message.js +33 -0
- package/dist/comments/pin-view.d.ts +107 -0
- package/dist/comments/pin-view.d.ts.map +1 -0
- package/dist/comments/pin-view.js +248 -0
- package/dist/comments/pins.d.ts +126 -0
- package/dist/comments/pins.d.ts.map +1 -0
- package/dist/comments/pins.js +218 -0
- package/dist/comments/text-pick.d.ts +69 -0
- package/dist/comments/text-pick.d.ts.map +1 -0
- package/dist/comments/text-pick.js +307 -0
- package/dist/comments/threads-store.d.ts +119 -0
- package/dist/comments/threads-store.d.ts.map +1 -0
- package/dist/comments/threads-store.js +286 -0
- package/dist/comments/types.d.ts +235 -0
- package/dist/comments/types.d.ts.map +1 -0
- package/dist/comments/types.js +109 -0
- package/dist/comments/use-comment-menu.d.ts +69 -0
- package/dist/comments/use-comment-menu.d.ts.map +1 -0
- package/dist/comments/use-comment-menu.js +140 -0
- package/dist/comments/use-held.d.ts +3 -0
- package/dist/comments/use-held.d.ts.map +1 -0
- package/dist/comments/use-held.js +16 -0
- package/dist/comments/use-thread-scroll.d.ts +23 -0
- package/dist/comments/use-thread-scroll.d.ts.map +1 -0
- package/dist/comments/use-thread-scroll.js +89 -0
- package/dist/hooks/agent-conversation-context.d.ts +12 -1
- package/dist/hooks/agent-conversation-context.d.ts.map +1 -1
- package/dist/hooks/agent-conversation-context.js +12 -5
- package/dist/hooks/agent-thread-live-update.d.ts +5 -2
- package/dist/hooks/agent-thread-live-update.d.ts.map +1 -1
- package/dist/hooks/agent-thread-live-update.js +14 -0
- package/dist/hooks/use-agent-conversation.d.ts +8 -0
- package/dist/hooks/use-agent-conversation.d.ts.map +1 -1
- package/dist/hooks/use-agent-conversation.js +10 -0
- package/dist/hooks/use-agent-threads.d.ts +1 -0
- package/dist/hooks/use-agent-threads.d.ts.map +1 -1
- package/dist/hooks/use-agent-threads.js +4 -1
- package/dist/iframe-communication/types.d.ts +25 -0
- package/dist/iframe-communication/types.d.ts.map +1 -1
- package/package.json +5 -2
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type Rect } from "./layout.js";
|
|
2
|
+
import { type TextPoint } from "./text-pick.js";
|
|
3
|
+
import type { CommentAnchor } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Pointing at something on the page: what comment mode and the agent chat's pointer share. While
|
|
6
|
+
* active, hovering outlines the element under the pointer and a click (or the first tap on a touch
|
|
7
|
+
* screen) picks it, with any text selected inside it as the quote. Nothing in the app reacts to
|
|
8
|
+
* that click: no link opens, no handler runs. UI marked with `PICK_IGNORE_ATTRIBUTE` (the comments'
|
|
9
|
+
* own, the chat) is neither picked nor blocked.
|
|
10
|
+
*
|
|
11
|
+
* Over text, the pointer is a text cursor on the word under it, and a drag points at just the text
|
|
12
|
+
* it covers — never past the row it started in — picking its element with that text as the quote,
|
|
13
|
+
* on buttons and links too (the picker's own range, not the browser's selection). A press without
|
|
14
|
+
* a drag still picks the whole element.
|
|
15
|
+
*/
|
|
16
|
+
export interface PagePick {
|
|
17
|
+
anchor: CommentAnchor;
|
|
18
|
+
element: HTMLElement;
|
|
19
|
+
}
|
|
20
|
+
export interface UsePagePickOptions {
|
|
21
|
+
isActive: boolean;
|
|
22
|
+
/** The page's pathname, stored on the anchor. */
|
|
23
|
+
routePath: string;
|
|
24
|
+
onPick: (pick: PagePick) => void;
|
|
25
|
+
/** Esc while pointing. */
|
|
26
|
+
onCancel?: () => void;
|
|
27
|
+
}
|
|
28
|
+
/** Pointing at text: the word under the pointer, or the text a drag covers. */
|
|
29
|
+
export interface PagePickText {
|
|
30
|
+
range: Range;
|
|
31
|
+
/** The caret position under the pointer; null while dragging. */
|
|
32
|
+
caret: TextPoint | null;
|
|
33
|
+
isSelecting: boolean;
|
|
34
|
+
}
|
|
35
|
+
export interface PagePickHover {
|
|
36
|
+
element: HTMLElement;
|
|
37
|
+
rect: Rect;
|
|
38
|
+
/** What the element is called, as its pick would be (`pickLabel`): its text, else its component's name, else the page. */
|
|
39
|
+
label: string;
|
|
40
|
+
/** Over text: what to tint (on screen, clipped) and where the caret bar goes. */
|
|
41
|
+
text?: {
|
|
42
|
+
rects: Rect[];
|
|
43
|
+
caret: {
|
|
44
|
+
x: number;
|
|
45
|
+
top: number;
|
|
46
|
+
bottom: number;
|
|
47
|
+
} | null;
|
|
48
|
+
isSelecting: boolean;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* What the hover label shows for an element: what picking it would be labelled (`anchorLabel`) —
|
|
53
|
+
* its text, else its component's name, else the page; never its tag.
|
|
54
|
+
*/
|
|
55
|
+
export declare const pickLabel: (element: HTMLElement, routePath: string) => string;
|
|
56
|
+
export interface PagePickListeners {
|
|
57
|
+
/** The page's pathname at the moment of the pick. */
|
|
58
|
+
routePath: () => string;
|
|
59
|
+
onPick: (pick: PagePick) => void;
|
|
60
|
+
onCancel: () => void;
|
|
61
|
+
/** The element the pointer is over (null when it left the page or is over ignored UI). */
|
|
62
|
+
onHover: (element: HTMLElement | null) => void;
|
|
63
|
+
/** The text the pointer is on, or a drag covers; null off text. */
|
|
64
|
+
onText?: (text: PagePickText | null) => void;
|
|
65
|
+
}
|
|
66
|
+
/** How the picker finds text under the pointer: the browser's caret, unless a test gives its own. */
|
|
67
|
+
export interface PagePickTextLookup {
|
|
68
|
+
/** The text position under the pointer, only when it is on a word. */
|
|
69
|
+
textPointAt: (x: number, y: number) => TextPoint | null;
|
|
70
|
+
/** The nearest text position, wherever the pointer is (where a drag ends). */
|
|
71
|
+
caretAt: (x: number, y: number) => TextPoint | null;
|
|
72
|
+
}
|
|
73
|
+
/** Starts listening on `doc` for a pick; returns the stop. What `usePagePick` runs while active. */
|
|
74
|
+
export declare const listenForPagePick: (doc: Document, listeners: PagePickListeners, lookup?: PagePickTextLookup) => (() => void);
|
|
75
|
+
export declare const usePagePick: ({ isActive, routePath, onPick, onCancel }: UsePagePickOptions) => PagePickHover | null;
|
|
76
|
+
//# sourceMappingURL=page-pick.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-pick.d.ts","sourceRoot":"","sources":["../../src/comments/page-pick.ts"],"names":[],"mappings":"AAGA,OAAO,EAAe,KAAK,IAAI,EAA0C,MAAM,UAAU,CAAC;AAC1F,OAAO,EAOL,KAAK,SAAS,EAGf,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,aAAa,CAAC;IACtB,OAAO,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,OAAO,CAAC;IAClB,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAC;IACjC,0BAA0B;IAC1B,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;CACvB;AAED,+EAA+E;AAC/E,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,KAAK,CAAC;IACb,iEAAiE;IACjE,KAAK,EAAE,SAAS,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,WAAW,CAAC;IACrB,IAAI,EAAE,IAAI,CAAC;IACX,0HAA0H;IAC1H,KAAK,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,IAAI,CAAC,EAAE;QACL,KAAK,EAAE,IAAI,EAAE,CAAC;QACd,KAAK,EAAE;YAAE,CAAC,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAC;QACzD,WAAW,EAAE,OAAO,CAAC;KACtB,CAAC;CACH;AAED;;;GAGG;AACH,eAAO,MAAM,SAAS,GAAI,SAAS,WAAW,EAAE,WAAW,MAAM,KAAG,MAQhE,CAAC;AAeL,MAAM,WAAW,iBAAiB;IAChC,qDAAqD;IACrD,SAAS,EAAE,MAAM,MAAM,CAAC;IACxB,MAAM,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAC;IACjC,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,0FAA0F;IAC1F,OAAO,EAAE,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;IAC/C,mEAAmE;IACnE,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,KAAK,IAAI,CAAC;CAC9C;AAED,qGAAqG;AACrG,MAAM,WAAW,kBAAkB;IACjC,sEAAsE;IACtE,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,SAAS,GAAG,IAAI,CAAC;IACxD,8EAA8E;IAC9E,OAAO,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,SAAS,GAAG,IAAI,CAAC;CACrD;AAQD,oGAAoG;AACpG,eAAO,MAAM,iBAAiB,GAC5B,KAAK,QAAQ,EACb,WAAW,iBAAiB,EAC5B,SAAQ,kBAGP,KACA,CAAC,MAAM,IAAI,CAkMb,CAAC;AAcF,eAAO,MAAM,WAAW,GAAI,2CAA2C,kBAAkB,KAAG,aAAa,GAAG,IA4C3G,CAAC"}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { anchorLabel, captureAnchor, isPickIgnored, pickTarget, textOf } from "./anchor.js";
|
|
3
|
+
import { clipRectsOf, toRect, usePageLayoutTick, visiblePart } from "./layout.js";
|
|
4
|
+
import { caretFromPoint, caretRectAt, clampToRoot, DRAG_THRESHOLD_PX, dragRootOf, rangeBetween, textPointAt, wordRangeAt, } from "./text-pick.js";
|
|
5
|
+
/**
|
|
6
|
+
* What the hover label shows for an element: what picking it would be labelled (`anchorLabel`) —
|
|
7
|
+
* its text, else its component's name, else the page; never its tag.
|
|
8
|
+
*/
|
|
9
|
+
export const pickLabel = (element, routePath) => anchorLabel({
|
|
10
|
+
v: 1,
|
|
11
|
+
routePath,
|
|
12
|
+
element: {
|
|
13
|
+
devName: element.getAttribute("data-dev-name") ?? undefined,
|
|
14
|
+
textSnapshot: textOf(element) || undefined,
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
/** The page's text selection, if any: `captureAnchor` quotes it when it is inside the picked element. */
|
|
18
|
+
const selectedRange = () => {
|
|
19
|
+
const selection = typeof window.getSelection === "function" ? window.getSelection() : null;
|
|
20
|
+
return selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
|
|
21
|
+
};
|
|
22
|
+
/** Stops the app hearing a press that is part of picking; leaves native text selection alone. */
|
|
23
|
+
const swallow = (event) => {
|
|
24
|
+
if (!isPickIgnored(event.target)) {
|
|
25
|
+
event.stopPropagation();
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* The pickers listening now, oldest first. Comment mode and the chat's pointer can both be on:
|
|
30
|
+
* only the one started last hears the page, and the earlier one resumes when it stops.
|
|
31
|
+
*/
|
|
32
|
+
const listeningPicks = [];
|
|
33
|
+
/** Starts listening on `doc` for a pick; returns the stop. What `usePagePick` runs while active. */
|
|
34
|
+
export const listenForPagePick = (doc, listeners, lookup = {
|
|
35
|
+
textPointAt: (x, y) => textPointAt(doc, x, y),
|
|
36
|
+
caretAt: (x, y) => caretFromPoint(doc, x, y),
|
|
37
|
+
}) => {
|
|
38
|
+
const self = {};
|
|
39
|
+
listeningPicks.push(self);
|
|
40
|
+
const isPaused = () => listeningPicks[listeningPicks.length - 1] !== self;
|
|
41
|
+
// A press on text, until its release: where it started, what it may cover, what it covers.
|
|
42
|
+
let drag = null;
|
|
43
|
+
// The click a drag's release makes: the drag already picked.
|
|
44
|
+
let isSwallowingClick = false;
|
|
45
|
+
// What was last reported: a move that changes nothing reports nothing (no re-render per pixel).
|
|
46
|
+
let shown = [null, 0, null, 0];
|
|
47
|
+
const showText = (text) => {
|
|
48
|
+
const next = text
|
|
49
|
+
? text.isSelecting
|
|
50
|
+
? [text.range.startContainer, text.range.startOffset, text.range.endContainer, text.range.endOffset]
|
|
51
|
+
: [text.caret?.node ?? null, text.caret?.offset ?? 0, null, -1]
|
|
52
|
+
: [null, 0, null, 0];
|
|
53
|
+
if (next.every((value, index) => value === shown[index])) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
shown = next;
|
|
57
|
+
listeners.onText?.(text);
|
|
58
|
+
};
|
|
59
|
+
const pick = (element, range, event) => listeners.onPick({
|
|
60
|
+
element,
|
|
61
|
+
anchor: captureAnchor({
|
|
62
|
+
routePath: listeners.routePath(),
|
|
63
|
+
element,
|
|
64
|
+
range,
|
|
65
|
+
clientX: event.clientX,
|
|
66
|
+
clientY: event.clientY,
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
69
|
+
/** A collapsed range at a text position: where a drag starts, the caret between words. */
|
|
70
|
+
const rangeAt = (point) => {
|
|
71
|
+
const range = doc.createRange();
|
|
72
|
+
range.setStart(point.node, point.offset);
|
|
73
|
+
return range;
|
|
74
|
+
};
|
|
75
|
+
const onOver = (event) => {
|
|
76
|
+
listeners.onHover(isPaused() ? null : pickTarget(event.target));
|
|
77
|
+
};
|
|
78
|
+
const onOut = (event) => {
|
|
79
|
+
if (!event.relatedTarget && !drag) {
|
|
80
|
+
listeners.onHover(null);
|
|
81
|
+
showText(null);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const onMove = (event) => {
|
|
85
|
+
if (drag) {
|
|
86
|
+
if (!drag.isMoved && Math.hypot(event.clientX - drag.x, event.clientY - drag.y) < DRAG_THRESHOLD_PX) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
drag.isMoved = true;
|
|
90
|
+
const end = lookup.caretAt(event.clientX, event.clientY);
|
|
91
|
+
if (end) {
|
|
92
|
+
drag.range = rangeBetween(doc, drag.start, clampToRoot(drag.root, end));
|
|
93
|
+
}
|
|
94
|
+
showText({ range: drag.range, caret: null, isSelecting: true });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (isPaused() || isPickIgnored(event.target)) {
|
|
98
|
+
showText(null);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const point = lookup.textPointAt(event.clientX, event.clientY);
|
|
102
|
+
if (!point) {
|
|
103
|
+
showText(null);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// Between words: the caret alone, nothing tinted.
|
|
107
|
+
showText({ range: wordRangeAt(doc, point) ?? rangeAt(point), caret: point, isSelecting: false });
|
|
108
|
+
};
|
|
109
|
+
const onPress = (event) => {
|
|
110
|
+
isSwallowingClick = false;
|
|
111
|
+
if (isPaused()) {
|
|
112
|
+
// The picker in front takes this press. Should it stop before the click the press ends in
|
|
113
|
+
// (a drag's pick, Esc during a drag), that click is still part of its pointing, not a pick
|
|
114
|
+
// of this one's.
|
|
115
|
+
isSwallowingClick = event.button === 0 && !isPickIgnored(event.target);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (event.button !== 0 || isPickIgnored(event.target)) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const point = lookup.textPointAt(event.clientX, event.clientY);
|
|
122
|
+
const root = point ? dragRootOf(point.node) : null;
|
|
123
|
+
if (!point || !root) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
// The picker's own range, not a native selection (which buttons and links don't make).
|
|
127
|
+
event.preventDefault();
|
|
128
|
+
drag = { root, start: point, x: event.clientX, y: event.clientY, isMoved: false, range: rangeAt(point) };
|
|
129
|
+
};
|
|
130
|
+
const onRelease = (event) => {
|
|
131
|
+
if (isPaused()) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const finished = drag;
|
|
135
|
+
drag = null;
|
|
136
|
+
if (!finished?.isMoved) {
|
|
137
|
+
// A press without a drag: the click that follows picks the element.
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
event.stopPropagation();
|
|
141
|
+
showText(null);
|
|
142
|
+
const { range, root } = finished;
|
|
143
|
+
if (range.collapsed || !range.toString().trim()) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const target = pickTarget(range.commonAncestorContainer);
|
|
147
|
+
isSwallowingClick = true;
|
|
148
|
+
pick(target && root.contains(target) ? target : root, range, event);
|
|
149
|
+
};
|
|
150
|
+
const onClick = (event) => {
|
|
151
|
+
if (isPaused()) {
|
|
152
|
+
isSwallowingClick = false;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (isSwallowingClick) {
|
|
156
|
+
isSwallowingClick = false;
|
|
157
|
+
event.preventDefault();
|
|
158
|
+
event.stopPropagation();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const target = pickTarget(event.target);
|
|
162
|
+
if (!target) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
event.preventDefault();
|
|
166
|
+
event.stopPropagation();
|
|
167
|
+
pick(target, selectedRange(), event);
|
|
168
|
+
};
|
|
169
|
+
const onKeyDown = (event) => {
|
|
170
|
+
if (event.key === "Escape" && !isPaused() && !isPickIgnored(event.target)) {
|
|
171
|
+
event.stopPropagation();
|
|
172
|
+
if (drag) {
|
|
173
|
+
// Esc mid-drag drops the drag, not the mode, and the click its release makes picks nothing.
|
|
174
|
+
drag = null;
|
|
175
|
+
isSwallowingClick = true;
|
|
176
|
+
showText(null);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
listeners.onCancel();
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
// Menus and drags often act on press, not click: the app doesn't hear those either.
|
|
183
|
+
const presses = ["pointerdown", "mousedown", "touchstart"];
|
|
184
|
+
doc.addEventListener("mouseover", onOver);
|
|
185
|
+
doc.addEventListener("mouseout", onOut);
|
|
186
|
+
doc.addEventListener("mousemove", onMove, true);
|
|
187
|
+
doc.addEventListener("mousedown", onPress, true);
|
|
188
|
+
doc.addEventListener("mouseup", onRelease, true);
|
|
189
|
+
doc.addEventListener("click", onClick, true);
|
|
190
|
+
presses.forEach((type) => doc.addEventListener(type, swallow, true));
|
|
191
|
+
doc.addEventListener("keydown", onKeyDown, true);
|
|
192
|
+
return () => {
|
|
193
|
+
const index = listeningPicks.indexOf(self);
|
|
194
|
+
if (index >= 0) {
|
|
195
|
+
listeningPicks.splice(index, 1);
|
|
196
|
+
}
|
|
197
|
+
doc.removeEventListener("mouseover", onOver);
|
|
198
|
+
doc.removeEventListener("mouseout", onOut);
|
|
199
|
+
doc.removeEventListener("mousemove", onMove, true);
|
|
200
|
+
doc.removeEventListener("mousedown", onPress, true);
|
|
201
|
+
doc.removeEventListener("mouseup", onRelease, true);
|
|
202
|
+
doc.removeEventListener("click", onClick, true);
|
|
203
|
+
presses.forEach((type) => doc.removeEventListener(type, swallow, true));
|
|
204
|
+
doc.removeEventListener("keydown", onKeyDown, true);
|
|
205
|
+
// The click a release makes comes right after it, and still isn't the app's when the press
|
|
206
|
+
// was part of picking. Only that click: the listener is gone a moment later.
|
|
207
|
+
const swallowNextClick = () => {
|
|
208
|
+
const swallowClick = (event) => {
|
|
209
|
+
event.preventDefault();
|
|
210
|
+
event.stopPropagation();
|
|
211
|
+
};
|
|
212
|
+
doc.addEventListener("click", swallowClick, { capture: true, once: true });
|
|
213
|
+
setTimeout(() => doc.removeEventListener("click", swallowClick, true), 0);
|
|
214
|
+
};
|
|
215
|
+
if (isSwallowingClick) {
|
|
216
|
+
// Stopped by its own drag's pick (the chat's pointer ends on a pick), before the click.
|
|
217
|
+
isSwallowingClick = false;
|
|
218
|
+
swallowNextClick();
|
|
219
|
+
}
|
|
220
|
+
else if (drag) {
|
|
221
|
+
// Stopped during a press (Esc ends the chat's pointing mid-drag): the click comes after the
|
|
222
|
+
// release. A new press first means that release never came here: nothing to swallow.
|
|
223
|
+
const forget = () => doc.removeEventListener("mouseup", swallowNextClick, true);
|
|
224
|
+
doc.addEventListener("mouseup", swallowNextClick, { capture: true, once: true });
|
|
225
|
+
doc.addEventListener("mousedown", forget, { capture: true, once: true });
|
|
226
|
+
drag = null;
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
};
|
|
230
|
+
/** A text pick's tint and caret, on screen: each line's box, clipped by what scrolls it. */
|
|
231
|
+
const textGeometry = (text) => {
|
|
232
|
+
const container = text.range.commonAncestorContainer;
|
|
233
|
+
const element = container instanceof Element ? container : container.parentElement;
|
|
234
|
+
const clips = element ? clipRectsOf(element) : [];
|
|
235
|
+
const rects = Array.from(text.range.getClientRects())
|
|
236
|
+
.map((rect) => visiblePart(toRect(rect), clips))
|
|
237
|
+
.filter((rect) => rect !== null && rect.right > rect.left);
|
|
238
|
+
const doc = element?.ownerDocument ?? document;
|
|
239
|
+
return { rects, caret: text.caret ? caretRectAt(doc, text.caret) : null, isSelecting: text.isSelecting };
|
|
240
|
+
};
|
|
241
|
+
export const usePagePick = ({ isActive, routePath, onPick, onCancel }) => {
|
|
242
|
+
// Not for the React Compiler (app-render compiles this module): the memo's input is the page's
|
|
243
|
+
// layout, read through `tick`, which the compiler would drop from the deps as unused.
|
|
244
|
+
"use no memo";
|
|
245
|
+
const [hovered, setHovered] = useState(null);
|
|
246
|
+
const [text, setText] = useState(null);
|
|
247
|
+
const latest = useRef({ routePath, onPick, onCancel });
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
latest.current = { routePath, onPick, onCancel };
|
|
250
|
+
});
|
|
251
|
+
useEffect(() => {
|
|
252
|
+
if (!isActive || typeof document === "undefined") {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const stop = listenForPagePick(document, {
|
|
256
|
+
routePath: () => latest.current.routePath,
|
|
257
|
+
onPick: (pick) => latest.current.onPick(pick),
|
|
258
|
+
onCancel: () => latest.current.onCancel?.(),
|
|
259
|
+
onHover: setHovered,
|
|
260
|
+
onText: setText,
|
|
261
|
+
});
|
|
262
|
+
return () => {
|
|
263
|
+
stop();
|
|
264
|
+
setHovered(null);
|
|
265
|
+
setText(null);
|
|
266
|
+
};
|
|
267
|
+
}, [isActive]);
|
|
268
|
+
// Per element, not per layout tick: reading a large element's text isn't free.
|
|
269
|
+
const label = useMemo(() => (hovered ? pickLabel(hovered, routePath) : ""), [hovered, routePath]);
|
|
270
|
+
const tick = usePageLayoutTick(isActive && hovered !== null);
|
|
271
|
+
return useMemo(() => {
|
|
272
|
+
void tick;
|
|
273
|
+
if (!isActive || !hovered?.isConnected) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
element: hovered,
|
|
278
|
+
rect: toRect(hovered.getBoundingClientRect()),
|
|
279
|
+
label,
|
|
280
|
+
...(text ? { text: textGeometry(text) } : {}),
|
|
281
|
+
};
|
|
282
|
+
}, [hovered, isActive, label, text, tick]);
|
|
283
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ComposerMessageMeta } from "../utils/composer-message.js";
|
|
2
|
+
import { type CommentAnchor } from "./types.js";
|
|
3
|
+
/** The text part a pick sends: the marker line, then the place described for the agent. */
|
|
4
|
+
export declare const pagePickText: (anchor: CommentAnchor) => string;
|
|
5
|
+
/** Arms the composer with a pick. */
|
|
6
|
+
export declare const pagePickComposerMeta: (anchor: CommentAnchor) => ComposerMessageMeta;
|
|
7
|
+
/** The pill's label for a sent or armed pick's text part, or null when the part isn't one. */
|
|
8
|
+
export declare const parsePagePickPart: (text: string) => string | null;
|
|
9
|
+
/** The label of the pick the composer is armed with, or null. */
|
|
10
|
+
export declare const pagePickOf: (meta: ComposerMessageMeta | null | undefined) => string | null;
|
|
11
|
+
//# sourceMappingURL=pick-message.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pick-message.d.ts","sourceRoot":"","sources":["../../src/comments/pick-message.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAErE,OAAO,EAAE,KAAK,aAAa,EAA0B,MAAM,SAAS,CAAC;AAiBrE,2FAA2F;AAC3F,eAAO,MAAM,YAAY,GAAI,QAAQ,aAAa,KAAG,MAC+B,CAAC;AAErF,qCAAqC;AACrC,eAAO,MAAM,oBAAoB,GAAI,QAAQ,aAAa,KAAG,mBAE3D,CAAC;AAEH,8FAA8F;AAC9F,eAAO,MAAM,iBAAiB,GAAI,MAAM,MAAM,KAAG,MAAM,GAAG,IAMzD,CAAC;AAEF,iEAAiE;AACjE,eAAO,MAAM,UAAU,GAAI,MAAM,mBAAmB,GAAG,IAAI,GAAG,SAAS,KAAG,MAAM,GAAG,IAGlF,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { anchorLabel } from "./anchor.js";
|
|
2
|
+
import { describeAnchorForAgent } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Pointing at something from the agent chat (BLK-1636): the pick rides with the next message as
|
|
5
|
+
* one leading text part — a first line the chat shows as a pill (`[Pointing at: <label>]`), then
|
|
6
|
+
* what the agent is told about the place, in the same words a comment gives it. Nothing is saved
|
|
7
|
+
* and no pin is made. The composer has one meta slot: a pick replaces armed feedback and back.
|
|
8
|
+
*/
|
|
9
|
+
const PREFIX = "[Pointing at: ";
|
|
10
|
+
/**
|
|
11
|
+
* The label as the marker line carries it: as written, on one line. A "]" inside is safe — the
|
|
12
|
+
* readers find the line's end by "]\n", and the label holds no line break.
|
|
13
|
+
*/
|
|
14
|
+
const markerLabel = (label) => label.replace(/[\r\n]+/g, " ");
|
|
15
|
+
/** The text part a pick sends: the marker line, then the place described for the agent. */
|
|
16
|
+
export const pagePickText = (anchor) => `${PREFIX}${markerLabel(anchorLabel(anchor))}]\n${describeAnchorForAgent(anchor)}`;
|
|
17
|
+
/** Arms the composer with a pick. */
|
|
18
|
+
export const pagePickComposerMeta = (anchor) => ({
|
|
19
|
+
leadingParts: [{ type: "text", text: pagePickText(anchor) }],
|
|
20
|
+
});
|
|
21
|
+
/** The pill's label for a sent or armed pick's text part, or null when the part isn't one. */
|
|
22
|
+
export const parsePagePickPart = (text) => {
|
|
23
|
+
if (!text.startsWith(PREFIX)) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const end = text.indexOf("]\n", PREFIX.length);
|
|
27
|
+
return end < 0 ? null : text.slice(PREFIX.length, end);
|
|
28
|
+
};
|
|
29
|
+
/** The label of the pick the composer is armed with, or null. */
|
|
30
|
+
export const pagePickOf = (meta) => {
|
|
31
|
+
const first = meta?.leadingParts?.[0]?.text;
|
|
32
|
+
return meta && !meta.name && first ? parsePagePickPart(first) : null;
|
|
33
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
import { type Rect } from "./layout.js";
|
|
3
|
+
import type { PinPoint } from "./pins.js";
|
|
4
|
+
import type { CommentParticipantDto } from "./types.js";
|
|
5
|
+
/** The window as a placement sees it: its layer's width (less a classic scrollbar), its height, its direction. */
|
|
6
|
+
export interface PlacementViewport {
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
isRtl: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface PanelPositionOptions {
|
|
12
|
+
/**
|
|
13
|
+
* The window, read where the caller renders on each frame: a call the React Compiler caches on
|
|
14
|
+
* its arguments then never keeps a stale place after a resize, a zoom or a change of direction.
|
|
15
|
+
*/
|
|
16
|
+
viewport: PlacementViewport;
|
|
17
|
+
/**
|
|
18
|
+
* How far the pin's element has gone past the window's edge (`ThreadLayout.hidden.shift`): the
|
|
19
|
+
* panel is placed as for `point`, where the pin stood as the element left, and moves with it.
|
|
20
|
+
*/
|
|
21
|
+
shift?: PinPoint;
|
|
22
|
+
/**
|
|
23
|
+
* Where to stand (`panelSide`, kept per open panel): without it, as `panelSide` opens one.
|
|
24
|
+
*/
|
|
25
|
+
side?: PanelSide;
|
|
26
|
+
/**
|
|
27
|
+
* What the comment is on (`ThreadLayout.target`, a new comment's `pickPlacement`), in the same
|
|
28
|
+
* coordinates as the point, before any shift: the panel keeps clear of it where it can
|
|
29
|
+
* (`panelSide`). Without it the panel goes beside the pin only.
|
|
30
|
+
*/
|
|
31
|
+
target?: Rect;
|
|
32
|
+
/**
|
|
33
|
+
* The panel's height as last drawn, when the caller measured it (and its `maxHeight` didn't cut
|
|
34
|
+
* it): the room it wants. Without it, `PANEL_MIN_ROOM`.
|
|
35
|
+
*/
|
|
36
|
+
panelHeight?: number;
|
|
37
|
+
}
|
|
38
|
+
/** A pin's width: one face fills it, and each more face (or "+N") adds what shows past the overlap. */
|
|
39
|
+
export declare const pinWidth: (faceCount: number) => number;
|
|
40
|
+
/**
|
|
41
|
+
* Where the panel goes by a pin `width` wide whose pointed corner is on `point` (a new comment's:
|
|
42
|
+
* the one-face pin it becomes), keeping clear of `target` where it can. The pin's box is worked out
|
|
43
|
+
* as `Pin` places it: above the point and toward the line's end, slid back inside the window at its
|
|
44
|
+
* edges. Where it stands is `side` (else as `panelSide` opens one): past the pin's end side or
|
|
45
|
+
* before its start, level with the pin's top (`down`) or with its bottom (`up`), or further along
|
|
46
|
+
* when that leaves it less than `PANEL_MIN_ROOM`; or below or above the target, its end edge level
|
|
47
|
+
* with the pin's. It is never taller than the room it has, so its replies scroll and its composer
|
|
48
|
+
* stays on screen. With a `shift` it then moves by it, keeping its height: it goes out of the
|
|
49
|
+
* window with its pin.
|
|
50
|
+
*/
|
|
51
|
+
export declare const panelPosition: (point: PinPoint, width: number, panelWidth: number, { viewport: frame, shift, side, target, panelHeight }: PanelPositionOptions) => React.CSSProperties;
|
|
52
|
+
/**
|
|
53
|
+
* Where a panel stands by its pin. `place`, in the order a panel tries them:
|
|
54
|
+
* - `end`: past the pin's end side (the right, or the left on a right-to-left page), level with it;
|
|
55
|
+
* - `below`, `above`: below or above what the comment is on (and its pin), its end edge level with
|
|
56
|
+
* the pin's end edge;
|
|
57
|
+
* - `start`: before the pin's start side, level with it: over what the comment is on, the last
|
|
58
|
+
* resort.
|
|
59
|
+
*
|
|
60
|
+
* `block`, beside the pin (`end`, `start`): `down` hangs it from the pin's top (its top level with
|
|
61
|
+
* the pin's), `up` above the pin's bottom (its bottom level with the pin's).
|
|
62
|
+
*
|
|
63
|
+
* `isMeasured`: chosen knowing the panel's own height (`panelHeight`). One chosen without it is
|
|
64
|
+
* chosen afresh once the height is known, so a panel's first, unmeasured frame doesn't decide
|
|
65
|
+
* where it stays.
|
|
66
|
+
*/
|
|
67
|
+
export interface PanelSide {
|
|
68
|
+
block: "up" | "down";
|
|
69
|
+
place: "end" | "below" | "above" | "start";
|
|
70
|
+
isMeasured?: boolean;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Where a panel stands by its pin (`panelPosition`'s `side`), sticky. As it opens: level with the
|
|
74
|
+
* pin's top, hanging down, when it fits between there and the window's bottom margin; hanging up
|
|
75
|
+
* from the pin's bottom only when it doesn't and does fit above (`block`); and the first place
|
|
76
|
+
* (`PanelSide`) that fits inside the window's margins (a panel below or above the target with the
|
|
77
|
+
* room it wants) without covering `target` (`ThreadLayout.target`), else before the pin's start.
|
|
78
|
+
* The room it wants is `panelHeight` when known, else `PANEL_MIN_ROOM`. Then, given where it
|
|
79
|
+
* stood (`previous`): hanging down and standing past the pin's end side are where it rests, kept
|
|
80
|
+
* while they fit within `PANEL_SIDE_SLACK` (a panel kept within the slack slides along for room,
|
|
81
|
+
* still beside the pin); any other side or place is held only until the resting one fits again
|
|
82
|
+
* with no slack, then kept while it fits within the slack. The two switch points are the slack
|
|
83
|
+
* apart, so a page scrolled back and forth by a few pixels never makes it flap. A `previous`
|
|
84
|
+
* chosen before the height was known is not kept once it is (`isMeasured`). Without a target only
|
|
85
|
+
* the places beside the pin are taken.
|
|
86
|
+
*/
|
|
87
|
+
export declare const panelSide: (point: PinPoint, width: number, panelWidth: number, previous: PanelSide | undefined, viewport: PlacementViewport, target?: Rect, panelHeight?: number) => PanelSide;
|
|
88
|
+
export declare const participantKey: (participant: CommentParticipantDto) => string;
|
|
89
|
+
/**
|
|
90
|
+
* Everyone in a pin's threads, by first comment, each once: up to three as faces, the working
|
|
91
|
+
* agent's last (on top, so its spinner badge shows), and how many more there are, as "+N".
|
|
92
|
+
*/
|
|
93
|
+
export declare const pinParticipants: (threads: readonly {
|
|
94
|
+
participants: readonly CommentParticipantDto[];
|
|
95
|
+
createdByUserId: number;
|
|
96
|
+
}[], workingAgentId: string | null) => {
|
|
97
|
+
shown: CommentParticipantDto[];
|
|
98
|
+
moreCount: number;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Where a pin `width` wide goes for a panel with nothing on the page to stand beside (a thread
|
|
102
|
+
* opened from its link whose element isn't there): so that `panelPosition` from it puts the panel
|
|
103
|
+
* in the window's bottom end corner, 16px in, with the pin just past the panel's start side, its
|
|
104
|
+
* bottom level with the panel's.
|
|
105
|
+
*/
|
|
106
|
+
export declare const cornerPoint: (viewport: PlacementViewport, width: number, panelWidth: number) => PinPoint;
|
|
107
|
+
//# sourceMappingURL=pin-view.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pin-view.d.ts","sourceRoot":"","sources":["../../src/comments/pin-view.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AAEpC,OAAO,EAAkB,KAAK,IAAI,EAAE,MAAM,UAAU,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAQrD,kHAAkH;AAClH,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,QAAQ,EAAE,iBAAiB,CAAC;IAC5B;;;OAGG;IACH,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB;;OAEG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB;;;;OAIG;IACH,MAAM,CAAC,EAAE,IAAI,CAAC;IACd;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAqBD,uGAAuG;AACvG,eAAO,MAAM,QAAQ,GAAI,WAAW,MAAM,WAA0D,CAAC;AAKrG;;;;;;;;;;GAUG;AACH,eAAO,MAAM,aAAa,GACxB,OAAO,QAAQ,EACf,OAAO,MAAM,EACb,YAAY,MAAM,EAClB,uDAAuD,oBAAoB,KAC1E,KAAK,CAAC,aAqBR,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,IAAI,GAAG,MAAM,CAAC;IACrB,KAAK,EAAE,KAAK,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;IAC3C,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AASD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,SAAS,GACpB,OAAO,QAAQ,EACf,OAAO,MAAM,EACb,YAAY,MAAM,EAClB,UAAU,SAAS,GAAG,SAAS,EAC/B,UAAU,iBAAiB,EAC3B,SAAS,IAAI,EACb,cAAc,MAAM,KACnB,SA2CF,CAAC;AA6GF,eAAO,MAAM,cAAc,GAAI,aAAa,qBAAqB,WACmB,CAAC;AAErF;;;GAGG;AACH,eAAO,MAAM,eAAe,GAC1B,SAAS,SAAS;IAChB,YAAY,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC/C,eAAe,EAAE,MAAM,CAAC;CACzB,EAAE,EACH,gBAAgB,MAAM,GAAG,IAAI,KAC5B;IAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAqBrD,CAAC;AAKF;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,UAAU,iBAAiB,EAAE,OAAO,MAAM,EAAE,YAAY,MAAM,KAAG,QAM5F,CAAC"}
|