@blocksdiy/react-common 1.70.2 → 1.71.0
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 +74 -0
- package/dist/comments/anchor.d.ts.map +1 -0
- package/dist/comments/anchor.js +279 -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 +97 -0
- package/dist/comments/comment-mode.d.ts.map +1 -0
- package/dist/comments/comment-mode.js +159 -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 +13 -0
- package/dist/comments/index.d.ts.map +1 -0
- package/dist/comments/index.js +17 -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/page-pick.d.ts +76 -0
- package/dist/comments/page-pick.d.ts.map +1 -0
- package/dist/comments/page-pick.js +267 -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/pins.d.ts +72 -0
- package/dist/comments/pins.d.ts.map +1 -0
- package/dist/comments/pins.js +125 -0
- package/dist/comments/text-pick.d.ts +59 -0
- package/dist/comments/text-pick.d.ts.map +1 -0
- package/dist/comments/text-pick.js +165 -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 +232 -0
- package/dist/comments/types.d.ts.map +1 -0
- package/dist/comments/types.js +109 -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 +24 -0
- package/dist/iframe-communication/types.d.ts.map +1 -1
- package/package.json +5 -2
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { isPickIgnored } from "./anchor.js";
|
|
3
|
+
/**
|
|
4
|
+
* A counter that moves at most once per animation frame whenever the page may have moved: a
|
|
5
|
+
* scroll anywhere (capture phase, so scroll containers count), a window resize, a change to the
|
|
6
|
+
* app's DOM, or a reflow of the body (a panel opening beside the app moves everything with no DOM
|
|
7
|
+
* change and no window resize). Changes inside the comments' own UI don't count, so drawing pins
|
|
8
|
+
* never feeds itself.
|
|
9
|
+
*
|
|
10
|
+
* Positions are read from the DOM, which is not a React input: pass the tick to whatever reads them
|
|
11
|
+
* so it recomputes.
|
|
12
|
+
*/
|
|
13
|
+
export const usePageLayoutTick = (isActive) => {
|
|
14
|
+
const [tick, setTick] = useState(0);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (!isActive || typeof window === "undefined") {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
let frame = 0;
|
|
20
|
+
const schedule = () => {
|
|
21
|
+
if (!frame) {
|
|
22
|
+
frame = window.requestAnimationFrame(() => {
|
|
23
|
+
frame = 0;
|
|
24
|
+
setTick((value) => value + 1);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const mutations = new MutationObserver((records) => {
|
|
29
|
+
if (records.some((record) => !isPickIgnored(record.target))) {
|
|
30
|
+
schedule();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
mutations.observe(document.body, { subtree: true, childList: true, characterData: true });
|
|
34
|
+
const resizes = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(schedule);
|
|
35
|
+
resizes?.observe(document.body);
|
|
36
|
+
document.addEventListener("scroll", schedule, true);
|
|
37
|
+
window.addEventListener("resize", schedule);
|
|
38
|
+
return () => {
|
|
39
|
+
window.cancelAnimationFrame(frame);
|
|
40
|
+
mutations.disconnect();
|
|
41
|
+
resizes?.disconnect();
|
|
42
|
+
document.removeEventListener("scroll", schedule, true);
|
|
43
|
+
window.removeEventListener("resize", schedule);
|
|
44
|
+
};
|
|
45
|
+
}, [isActive]);
|
|
46
|
+
return tick;
|
|
47
|
+
};
|
|
48
|
+
export const toRect = (rect) => ({
|
|
49
|
+
top: rect.top,
|
|
50
|
+
left: rect.left,
|
|
51
|
+
right: rect.right,
|
|
52
|
+
bottom: rect.bottom,
|
|
53
|
+
});
|
|
54
|
+
/** The overlap of two rects, or null when they don't overlap. */
|
|
55
|
+
export const intersectRects = (a, b) => {
|
|
56
|
+
const top = Math.max(a.top, b.top);
|
|
57
|
+
const left = Math.max(a.left, b.left);
|
|
58
|
+
const right = Math.min(a.right, b.right);
|
|
59
|
+
const bottom = Math.min(a.bottom, b.bottom);
|
|
60
|
+
return right > left && bottom > top ? { top, left, right, bottom } : null;
|
|
61
|
+
};
|
|
62
|
+
/** The part of `rect` inside every clip (the viewport, each scrolling ancestor), or null. */
|
|
63
|
+
export const visiblePart = (rect, clips) => {
|
|
64
|
+
let visible = rect.right > rect.left && rect.bottom > rect.top ? rect : null;
|
|
65
|
+
for (const clip of clips) {
|
|
66
|
+
if (!visible) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
visible = intersectRects(visible, clip);
|
|
70
|
+
}
|
|
71
|
+
return visible;
|
|
72
|
+
};
|
|
73
|
+
const clipsOverflow = (style) => /(auto|scroll|hidden|clip)/.test(`${style.overflow}${style.overflowX}${style.overflowY}`);
|
|
74
|
+
/** The boxes that clip `element`: the viewport and every ancestor that clips its overflow. */
|
|
75
|
+
export const clipRectsOf = (element) => {
|
|
76
|
+
const clips = [{ top: 0, left: 0, right: window.innerWidth, bottom: window.innerHeight }];
|
|
77
|
+
for (let ancestor = element.parentElement; ancestor && ancestor !== document.body; ancestor = ancestor.parentElement) {
|
|
78
|
+
if (clipsOverflow(window.getComputedStyle(ancestor))) {
|
|
79
|
+
clips.push(toRect(ancestor.getBoundingClientRect()));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return clips;
|
|
83
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mentions in a comment body. Stored, a person is `@[42]` and an agent `@[agent:SupportAgent]`;
|
|
3
|
+
* in the composer both read as `@Name`. The server parses the stored tokens and never trusts a list
|
|
4
|
+
* the client sends.
|
|
5
|
+
*/
|
|
6
|
+
export type Mention = {
|
|
7
|
+
kind: "user";
|
|
8
|
+
userId: number;
|
|
9
|
+
name: string;
|
|
10
|
+
} | {
|
|
11
|
+
kind: "agent";
|
|
12
|
+
agentId: string;
|
|
13
|
+
name: string;
|
|
14
|
+
};
|
|
15
|
+
/** Names for the ids a body mentions: the thread's people, the app's agents. */
|
|
16
|
+
export interface MentionNames {
|
|
17
|
+
users: ReadonlyMap<number, string>;
|
|
18
|
+
agents: ReadonlyMap<string, string>;
|
|
19
|
+
}
|
|
20
|
+
export declare const mentionToken: (mention: Mention) => string;
|
|
21
|
+
/** The `@query` being typed at the caret, if any: an `@` at a word start, no line break after it. */
|
|
22
|
+
export declare const mentionQueryAt: (text: string, caret: number) => {
|
|
23
|
+
start: number;
|
|
24
|
+
query: string;
|
|
25
|
+
} | null;
|
|
26
|
+
/** Whether a mention candidate matches what was typed after `@`: its name contains it, or its email starts with it. */
|
|
27
|
+
export declare const matchesMentionQuery: (candidate: {
|
|
28
|
+
name: string;
|
|
29
|
+
email?: string | null;
|
|
30
|
+
}, query: string) => boolean;
|
|
31
|
+
export declare const initials: (name: string) => string;
|
|
32
|
+
//# sourceMappingURL=mentions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mentions.d.ts","sourceRoot":"","sources":["../../src/comments/mentions.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,MAAM,OAAO,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAExH,gFAAgF;AAChF,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACrC;AAED,eAAO,MAAM,YAAY,GAAI,SAAS,OAAO,KAAG,MACkC,CAAC;AAEnF,qGAAqG;AACrG,eAAO,MAAM,cAAc,GAAI,MAAM,MAAM,EAAE,OAAO,MAAM,KAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAW/F,CAAC;AAEF,uHAAuH;AACvH,eAAO,MAAM,mBAAmB,GAAI,WAAW;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EAAE,OAAO,MAAM,KAAG,OAKvG,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,MAAM,MAAM,KAAG,MAMpB,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mentions in a comment body. Stored, a person is `@[42]` and an agent `@[agent:SupportAgent]`;
|
|
3
|
+
* in the composer both read as `@Name`. The server parses the stored tokens and never trusts a list
|
|
4
|
+
* the client sends.
|
|
5
|
+
*/
|
|
6
|
+
export const mentionToken = (mention) => mention.kind === "user" ? `@[${mention.userId}]` : `@[agent:${mention.agentId}]`;
|
|
7
|
+
/** The `@query` being typed at the caret, if any: an `@` at a word start, no line break after it. */
|
|
8
|
+
export const mentionQueryAt = (text, caret) => {
|
|
9
|
+
const before = text.slice(0, caret);
|
|
10
|
+
const at = before.lastIndexOf("@");
|
|
11
|
+
if (at < 0 || (at > 0 && !/\s/.test(before[at - 1]))) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const query = before.slice(at + 1);
|
|
15
|
+
if (query.length > 30 || /[\n@]/.test(query) || /\s{2}/.test(query)) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return { start: at, query };
|
|
19
|
+
};
|
|
20
|
+
/** Whether a mention candidate matches what was typed after `@`: its name contains it, or its email starts with it. */
|
|
21
|
+
export const matchesMentionQuery = (candidate, query) => {
|
|
22
|
+
const needle = query.trim().toLowerCase();
|
|
23
|
+
return (!needle || candidate.name.toLowerCase().includes(needle) || (candidate.email ?? "").toLowerCase().startsWith(needle));
|
|
24
|
+
};
|
|
25
|
+
export const initials = (name) => name
|
|
26
|
+
.split(/\s+/)
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
.slice(0, 2)
|
|
29
|
+
.map((part) => part[0].toUpperCase())
|
|
30
|
+
.join("") || "?";
|
|
@@ -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,CAmLb,CAAC;AAcF,eAAO,MAAM,WAAW,GAAI,2CAA2C,kBAAkB,KAAG,aAAa,GAAG,IA4C3G,CAAC"}
|
|
@@ -0,0 +1,267 @@
|
|
|
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() || event.button !== 0 || isPickIgnored(event.target)) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const point = lookup.textPointAt(event.clientX, event.clientY);
|
|
115
|
+
const root = point ? dragRootOf(point.node) : null;
|
|
116
|
+
if (!point || !root) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// The picker's own range, not a native selection (which buttons and links don't make).
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
drag = { root, start: point, x: event.clientX, y: event.clientY, isMoved: false, range: rangeAt(point) };
|
|
122
|
+
};
|
|
123
|
+
const onRelease = (event) => {
|
|
124
|
+
if (isPaused()) {
|
|
125
|
+
// The picker in front may pick on this release (a drag) and stop before the click it makes:
|
|
126
|
+
// that click is part of its pick, not a pick of this one's.
|
|
127
|
+
isSwallowingClick = !isPickIgnored(event.target);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const finished = drag;
|
|
131
|
+
drag = null;
|
|
132
|
+
if (!finished?.isMoved) {
|
|
133
|
+
// A press without a drag: the click that follows picks the element.
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
event.stopPropagation();
|
|
137
|
+
showText(null);
|
|
138
|
+
const { range, root } = finished;
|
|
139
|
+
if (range.collapsed || !range.toString().trim()) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const target = pickTarget(range.commonAncestorContainer);
|
|
143
|
+
isSwallowingClick = true;
|
|
144
|
+
pick(target && root.contains(target) ? target : root, range, event);
|
|
145
|
+
};
|
|
146
|
+
const onClick = (event) => {
|
|
147
|
+
if (isPaused()) {
|
|
148
|
+
isSwallowingClick = false;
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (isSwallowingClick) {
|
|
152
|
+
isSwallowingClick = false;
|
|
153
|
+
event.preventDefault();
|
|
154
|
+
event.stopPropagation();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const target = pickTarget(event.target);
|
|
158
|
+
if (!target) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
event.preventDefault();
|
|
162
|
+
event.stopPropagation();
|
|
163
|
+
pick(target, selectedRange(), event);
|
|
164
|
+
};
|
|
165
|
+
const onKeyDown = (event) => {
|
|
166
|
+
if (event.key === "Escape" && !isPaused() && !isPickIgnored(event.target)) {
|
|
167
|
+
event.stopPropagation();
|
|
168
|
+
if (drag) {
|
|
169
|
+
// Esc mid-drag drops the drag, not the mode, and the click its release makes picks nothing.
|
|
170
|
+
drag = null;
|
|
171
|
+
isSwallowingClick = true;
|
|
172
|
+
showText(null);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
listeners.onCancel();
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
// Menus and drags often act on press, not click: the app doesn't hear those either.
|
|
179
|
+
const presses = ["pointerdown", "mousedown", "touchstart"];
|
|
180
|
+
doc.addEventListener("mouseover", onOver);
|
|
181
|
+
doc.addEventListener("mouseout", onOut);
|
|
182
|
+
doc.addEventListener("mousemove", onMove, true);
|
|
183
|
+
doc.addEventListener("mousedown", onPress, true);
|
|
184
|
+
doc.addEventListener("mouseup", onRelease, true);
|
|
185
|
+
doc.addEventListener("click", onClick, true);
|
|
186
|
+
presses.forEach((type) => doc.addEventListener(type, swallow, true));
|
|
187
|
+
doc.addEventListener("keydown", onKeyDown, true);
|
|
188
|
+
return () => {
|
|
189
|
+
const index = listeningPicks.indexOf(self);
|
|
190
|
+
if (index >= 0) {
|
|
191
|
+
listeningPicks.splice(index, 1);
|
|
192
|
+
}
|
|
193
|
+
doc.removeEventListener("mouseover", onOver);
|
|
194
|
+
doc.removeEventListener("mouseout", onOut);
|
|
195
|
+
doc.removeEventListener("mousemove", onMove, true);
|
|
196
|
+
doc.removeEventListener("mousedown", onPress, true);
|
|
197
|
+
doc.removeEventListener("mouseup", onRelease, true);
|
|
198
|
+
doc.removeEventListener("click", onClick, true);
|
|
199
|
+
presses.forEach((type) => doc.removeEventListener(type, swallow, true));
|
|
200
|
+
doc.removeEventListener("keydown", onKeyDown, true);
|
|
201
|
+
if (isSwallowingClick) {
|
|
202
|
+
// Stopped by its own drag's pick (the chat's pointer ends on a pick): the click that release
|
|
203
|
+
// makes comes right after, and still isn't the app's. Only that click: gone a moment later.
|
|
204
|
+
const swallowClick = (event) => {
|
|
205
|
+
event.preventDefault();
|
|
206
|
+
event.stopPropagation();
|
|
207
|
+
};
|
|
208
|
+
doc.addEventListener("click", swallowClick, { capture: true, once: true });
|
|
209
|
+
setTimeout(() => doc.removeEventListener("click", swallowClick, true), 0);
|
|
210
|
+
isSwallowingClick = false;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
/** A text pick's tint and caret, on screen: each line's box, clipped by what scrolls it. */
|
|
215
|
+
const textGeometry = (text) => {
|
|
216
|
+
const container = text.range.commonAncestorContainer;
|
|
217
|
+
const element = container instanceof Element ? container : container.parentElement;
|
|
218
|
+
const clips = element ? clipRectsOf(element) : [];
|
|
219
|
+
const rects = Array.from(text.range.getClientRects())
|
|
220
|
+
.map((rect) => visiblePart(toRect(rect), clips))
|
|
221
|
+
.filter((rect) => rect !== null && rect.right > rect.left);
|
|
222
|
+
const doc = element?.ownerDocument ?? document;
|
|
223
|
+
return { rects, caret: text.caret ? caretRectAt(doc, text.caret) : null, isSelecting: text.isSelecting };
|
|
224
|
+
};
|
|
225
|
+
export const usePagePick = ({ isActive, routePath, onPick, onCancel }) => {
|
|
226
|
+
// Not for the React Compiler (app-render compiles this module): the memo's input is the page's
|
|
227
|
+
// layout, read through `tick`, which the compiler would drop from the deps as unused.
|
|
228
|
+
"use no memo";
|
|
229
|
+
const [hovered, setHovered] = useState(null);
|
|
230
|
+
const [text, setText] = useState(null);
|
|
231
|
+
const latest = useRef({ routePath, onPick, onCancel });
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
latest.current = { routePath, onPick, onCancel };
|
|
234
|
+
});
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
if (!isActive || typeof document === "undefined") {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const stop = listenForPagePick(document, {
|
|
240
|
+
routePath: () => latest.current.routePath,
|
|
241
|
+
onPick: (pick) => latest.current.onPick(pick),
|
|
242
|
+
onCancel: () => latest.current.onCancel?.(),
|
|
243
|
+
onHover: setHovered,
|
|
244
|
+
onText: setText,
|
|
245
|
+
});
|
|
246
|
+
return () => {
|
|
247
|
+
stop();
|
|
248
|
+
setHovered(null);
|
|
249
|
+
setText(null);
|
|
250
|
+
};
|
|
251
|
+
}, [isActive]);
|
|
252
|
+
// Per element, not per layout tick: reading a large element's text isn't free.
|
|
253
|
+
const label = useMemo(() => (hovered ? pickLabel(hovered, routePath) : ""), [hovered, routePath]);
|
|
254
|
+
const tick = usePageLayoutTick(isActive && hovered !== null);
|
|
255
|
+
return useMemo(() => {
|
|
256
|
+
void tick;
|
|
257
|
+
if (!isActive || !hovered?.isConnected) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
element: hovered,
|
|
262
|
+
rect: toRect(hovered.getBoundingClientRect()),
|
|
263
|
+
label,
|
|
264
|
+
...(text ? { text: textGeometry(text) } : {}),
|
|
265
|
+
};
|
|
266
|
+
}, [hovered, isActive, label, text, tick]);
|
|
267
|
+
};
|
|
@@ -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,72 @@
|
|
|
1
|
+
import { type AnchorState, resolveAnchor } from "./anchor.js";
|
|
2
|
+
import { type Rect } from "./layout.js";
|
|
3
|
+
import type { CommentAnchor } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Where each thread's pin goes on the page as it is now. Never stored: worked out from the anchor
|
|
6
|
+
* every time the page moves, so a pin follows its element across screen sizes, re-sorted lists and
|
|
7
|
+
* scrolling.
|
|
8
|
+
*/
|
|
9
|
+
/** How far a pin keeps from the viewport's edges. */
|
|
10
|
+
export declare const PIN_EDGE_MARGIN = 12;
|
|
11
|
+
export interface PinPoint {
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The pin's point, in viewport coordinates.
|
|
17
|
+
* - A quote: the end of its last line.
|
|
18
|
+
* - An element: where it was clicked, scaled to the element's box now; else its top-right corner.
|
|
19
|
+
*/
|
|
20
|
+
export declare const pinPoint: (target: {
|
|
21
|
+
kind: "quote";
|
|
22
|
+
lastLine: Rect;
|
|
23
|
+
} | {
|
|
24
|
+
kind: "element";
|
|
25
|
+
rect: Rect;
|
|
26
|
+
point?: {
|
|
27
|
+
x: number;
|
|
28
|
+
y: number;
|
|
29
|
+
};
|
|
30
|
+
}) => PinPoint;
|
|
31
|
+
/** `point` kept `margin` inside the viewport. */
|
|
32
|
+
export declare const keepInViewport: (point: PinPoint, viewport: {
|
|
33
|
+
width: number;
|
|
34
|
+
height: number;
|
|
35
|
+
}, margin?: number) => {
|
|
36
|
+
x: number;
|
|
37
|
+
y: number;
|
|
38
|
+
};
|
|
39
|
+
export interface PinGroup<T> {
|
|
40
|
+
key: string;
|
|
41
|
+
point: PinPoint;
|
|
42
|
+
items: T[];
|
|
43
|
+
}
|
|
44
|
+
/** Pins on the same element become one group, at the first one's point (items in input order). */
|
|
45
|
+
export declare const groupPins: <T>(pins: readonly {
|
|
46
|
+
key: string;
|
|
47
|
+
point: PinPoint;
|
|
48
|
+
item: T;
|
|
49
|
+
}[]) => PinGroup<T>[];
|
|
50
|
+
export interface ThreadLayout {
|
|
51
|
+
state: AnchorState;
|
|
52
|
+
element: HTMLElement | null;
|
|
53
|
+
/** Where the pin goes; null when there is nothing on screen to pin to (scrolled away, covered, missing). */
|
|
54
|
+
point: PinPoint | null;
|
|
55
|
+
/** Threads with the same key point at the same element. */
|
|
56
|
+
groupKey: string | null;
|
|
57
|
+
/** What the anchored text says now, when it changed. */
|
|
58
|
+
currentText?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The comments' own layer (pins, threads) marks its root with this attribute: what it draws over an
|
|
62
|
+
* element doesn't hide the element. Other ignored UI (the agent chat's panel) does.
|
|
63
|
+
*/
|
|
64
|
+
export declare const COMMENTS_LAYER_ATTRIBUTE = "data-blocks-comments-layer";
|
|
65
|
+
/** Lays out one anchor whose element is known. */
|
|
66
|
+
export declare const layoutAnchor: (anchor: CommentAnchor, resolved: ReturnType<typeof resolveAnchor>) => ThreadLayout;
|
|
67
|
+
/** Where each thread is on the page now, re-measured at most once per frame while `isActive`. */
|
|
68
|
+
export declare const useCommentPins: (anchors: readonly {
|
|
69
|
+
id: string | number;
|
|
70
|
+
anchor: CommentAnchor;
|
|
71
|
+
}[], isActive: boolean) => ReadonlyMap<string | number, ThreadLayout>;
|
|
72
|
+
//# sourceMappingURL=pins.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pins.d.ts","sourceRoot":"","sources":["../../src/comments/pins.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,WAAW,EAAoC,aAAa,EAAE,MAAM,UAAU,CAAC;AAC7F,OAAO,EAAe,KAAK,IAAI,EAA0C,MAAM,UAAU,CAAC;AAC1F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C;;;;GAIG;AAEH,qDAAqD;AACrD,eAAO,MAAM,eAAe,KAAK,CAAC;AAElC,MAAM,WAAW,QAAQ;IACvB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED;;;;GAIG;AACH,eAAO,MAAM,QAAQ,GACnB,QAAQ;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,KAC5G,QAYF,CAAC;AAEF,iDAAiD;AACjD,eAAO,MAAM,cAAc,GACzB,OAAO,QAAQ,EACf,UAAU;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAC3C,eAAwB;;;CAIxB,CAAC;AAEH,MAAM,WAAW,QAAQ,CAAC,CAAC;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,QAAQ,CAAC;IAChB,KAAK,EAAE,CAAC,EAAE,CAAC;CACZ;AAED,kGAAkG;AAClG,eAAO,MAAM,SAAS,GAAI,CAAC,EAAE,MAAM,SAAS;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,CAAC,CAAA;CAAE,EAAE,KAAG,QAAQ,CAAC,CAAC,CAAC,EAWpG,CAAC;AAEF,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,WAAW,GAAG,IAAI,CAAC;IAC5B,4GAA4G;IAC5G,KAAK,EAAE,QAAQ,GAAG,IAAI,CAAC;IACvB,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAID;;;GAGG;AACH,eAAO,MAAM,wBAAwB,+BAA+B,CAAC;AAmBrE,kDAAkD;AAClD,eAAO,MAAM,YAAY,GAAI,QAAQ,aAAa,EAAE,UAAU,UAAU,CAAC,OAAO,aAAa,CAAC,KAAG,YAsBhG,CAAC;AAkBF,iGAAiG;AACjG,eAAO,MAAM,cAAc,GACzB,SAAS,SAAS;IAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,MAAM,EAAE,aAAa,CAAA;CAAE,EAAE,EAClE,UAAU,OAAO,KAChB,WAAW,CAAC,MAAM,GAAG,MAAM,EAAE,YAAY,CAgB3C,CAAC"}
|