@tldraw/commenting 0.0.0-bootstrap → 5.3.0-canary.31036673d3f8
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/README.md +0 -2
- package/commenting.css +1139 -329
- package/dist-cjs/canvas/anchor-lifecycle.js +148 -0
- package/dist-cjs/canvas/anchor-lifecycle.js.map +7 -0
- package/dist-cjs/canvas/cluster-input.js +2 -2
- package/dist-cjs/canvas/cluster-input.js.map +2 -2
- package/dist-cjs/canvas/comment-body.js +1 -1
- package/dist-cjs/canvas/comment-body.js.map +2 -2
- package/dist-cjs/canvas/comment-drafts.js +54 -0
- package/dist-cjs/canvas/comment-drafts.js.map +7 -0
- package/dist-cjs/canvas/comment-mutations.js +123 -0
- package/dist-cjs/canvas/comment-mutations.js.map +7 -0
- package/dist-cjs/canvas/comment-reactions.js +163 -0
- package/dist-cjs/canvas/comment-reactions.js.map +7 -0
- package/dist-cjs/canvas/comment-render.js +5 -3
- package/dist-cjs/canvas/comment-render.js.map +2 -2
- package/dist-cjs/canvas/comment-store.js +19 -9
- package/dist-cjs/canvas/comment-store.js.map +2 -2
- package/dist-cjs/canvas/comment-tool.js +64 -14
- package/dist-cjs/canvas/comment-tool.js.map +2 -2
- package/dist-cjs/canvas/comments-filter-menu.js +20 -38
- package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
- package/dist-cjs/canvas/comments-overflow-menu.js +22 -27
- package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
- package/dist-cjs/canvas/comments-overlay.js +429 -474
- package/dist-cjs/canvas/comments-overlay.js.map +2 -2
- package/dist-cjs/canvas/comments-sidebar.js +35 -32
- package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
- package/dist-cjs/canvas/comments-visibility-toggle.js +65 -0
- package/dist-cjs/canvas/comments-visibility-toggle.js.map +7 -0
- package/dist-cjs/canvas/context.js +17 -0
- package/dist-cjs/canvas/context.js.map +7 -0
- package/dist-cjs/canvas/hooks.js +22 -3
- package/dist-cjs/canvas/hooks.js.map +2 -2
- package/dist-cjs/canvas/options.js +19 -2
- package/dist-cjs/canvas/options.js.map +2 -2
- package/dist-cjs/canvas/pin-stacking.js +56 -0
- package/dist-cjs/canvas/pin-stacking.js.map +7 -0
- package/dist-cjs/canvas/sidebar-filters.js +2 -2
- package/dist-cjs/canvas/sidebar-filters.js.map +2 -2
- package/dist-cjs/canvas/state.js +28 -14
- package/dist-cjs/canvas/state.js.map +2 -2
- package/dist-cjs/canvas/thread-preview.js +185 -0
- package/dist-cjs/canvas/thread-preview.js.map +7 -0
- package/dist-cjs/canvas/thread-stack.js +199 -0
- package/dist-cjs/canvas/thread-stack.js.map +7 -0
- package/dist-cjs/canvas/thread-state.js +67 -21
- package/dist-cjs/canvas/thread-state.js.map +2 -2
- package/dist-cjs/canvas/thread-view.js +397 -0
- package/dist-cjs/canvas/thread-view.js.map +7 -0
- package/dist-cjs/clustering/computeClusterTable.js +4 -4
- package/dist-cjs/clustering/computeClusterTable.js.map +2 -2
- package/dist-cjs/clustering/runtime.js +119 -7
- package/dist-cjs/clustering/runtime.js.map +2 -2
- package/dist-cjs/clustering/types.js.map +1 -1
- package/dist-cjs/index.d.ts +763 -333
- package/dist-cjs/index.js +46 -28
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/ui/byline.js +6 -4
- package/dist-cjs/ui/byline.js.map +2 -2
- package/dist-cjs/ui/comment-card.js +15 -8
- package/dist-cjs/ui/comment-card.js.map +2 -2
- package/dist-cjs/ui/comment-composer.js +137 -36
- package/dist-cjs/ui/comment-composer.js.map +2 -2
- package/dist-cjs/ui/comment-pin.js +24 -2
- package/dist-cjs/ui/comment-pin.js.map +2 -2
- package/dist-cjs/ui/comment-thread.js +9 -7
- package/dist-cjs/ui/comment-thread.js.map +2 -2
- package/dist-cjs/ui/comments-list.js +30 -18
- package/dist-cjs/ui/comments-list.js.map +2 -2
- package/dist-cjs/ui/count-badge.js +3 -2
- package/dist-cjs/ui/count-badge.js.map +2 -2
- package/dist-cjs/ui/emoji-picker.js +54 -0
- package/dist-cjs/ui/emoji-picker.js.map +7 -0
- package/dist-cjs/ui/empty-state.js +3 -3
- package/dist-cjs/ui/empty-state.js.map +2 -2
- package/dist-cjs/ui/format-time.js +4 -1
- package/dist-cjs/ui/format-time.js.map +2 -2
- package/dist-cjs/ui/reaction-picker.js +73 -0
- package/dist-cjs/ui/reaction-picker.js.map +7 -0
- package/dist-cjs/ui/reaction.js +59 -6
- package/dist-cjs/ui/reaction.js.map +2 -2
- package/dist-cjs/ui/reactions.js +20 -7
- package/dist-cjs/ui/reactions.js.map +2 -2
- package/dist-cjs/ui/{mention.js → reply-count.js} +9 -11
- package/dist-cjs/ui/reply-count.js.map +7 -0
- package/dist-cjs/ui/send-button.js +20 -1
- package/dist-cjs/ui/send-button.js.map +2 -2
- package/dist-cjs/ui/{comment-text.js → tooltip-button.js} +12 -9
- package/dist-cjs/ui/tooltip-button.js.map +7 -0
- package/dist-esm/canvas/anchor-lifecycle.mjs +128 -0
- package/dist-esm/canvas/anchor-lifecycle.mjs.map +7 -0
- package/dist-esm/canvas/cluster-input.mjs +2 -2
- package/dist-esm/canvas/cluster-input.mjs.map +2 -2
- package/dist-esm/canvas/comment-body.mjs +1 -1
- package/dist-esm/canvas/comment-body.mjs.map +2 -2
- package/dist-esm/canvas/comment-drafts.mjs +34 -0
- package/dist-esm/canvas/comment-drafts.mjs.map +7 -0
- package/dist-esm/canvas/comment-mutations.mjs +103 -0
- package/dist-esm/canvas/comment-mutations.mjs.map +7 -0
- package/dist-esm/canvas/comment-reactions.mjs +152 -0
- package/dist-esm/canvas/comment-reactions.mjs.map +7 -0
- package/dist-esm/canvas/comment-render.mjs +5 -3
- package/dist-esm/canvas/comment-render.mjs.map +2 -2
- package/dist-esm/canvas/comment-store.mjs +19 -9
- package/dist-esm/canvas/comment-store.mjs.map +2 -2
- package/dist-esm/canvas/comment-tool.mjs +67 -17
- package/dist-esm/canvas/comment-tool.mjs.map +2 -2
- package/dist-esm/canvas/comments-filter-menu.mjs +20 -38
- package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
- package/dist-esm/canvas/comments-overflow-menu.mjs +23 -27
- package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
- package/dist-esm/canvas/comments-overlay.mjs +450 -479
- package/dist-esm/canvas/comments-overlay.mjs.map +3 -3
- package/dist-esm/canvas/comments-sidebar.mjs +38 -35
- package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
- package/dist-esm/canvas/comments-visibility-toggle.mjs +45 -0
- package/dist-esm/canvas/comments-visibility-toggle.mjs.map +7 -0
- package/dist-esm/canvas/context.mjs +1 -0
- package/dist-esm/canvas/context.mjs.map +7 -0
- package/dist-esm/canvas/hooks.mjs +28 -5
- package/dist-esm/canvas/hooks.mjs.map +2 -2
- package/dist-esm/canvas/options.mjs +21 -3
- package/dist-esm/canvas/options.mjs.map +2 -2
- package/dist-esm/canvas/pin-stacking.mjs +36 -0
- package/dist-esm/canvas/pin-stacking.mjs.map +7 -0
- package/dist-esm/canvas/sidebar-filters.mjs +2 -2
- package/dist-esm/canvas/sidebar-filters.mjs.map +2 -2
- package/dist-esm/canvas/state.mjs +29 -19
- package/dist-esm/canvas/state.mjs.map +2 -2
- package/dist-esm/canvas/thread-preview.mjs +170 -0
- package/dist-esm/canvas/thread-preview.mjs.map +7 -0
- package/dist-esm/canvas/thread-stack.mjs +190 -0
- package/dist-esm/canvas/thread-stack.mjs.map +7 -0
- package/dist-esm/canvas/thread-state.mjs +71 -22
- package/dist-esm/canvas/thread-state.mjs.map +2 -2
- package/dist-esm/canvas/thread-view.mjs +402 -0
- package/dist-esm/canvas/thread-view.mjs.map +7 -0
- package/dist-esm/clustering/computeClusterTable.mjs +4 -4
- package/dist-esm/clustering/computeClusterTable.mjs.map +2 -2
- package/dist-esm/clustering/runtime.mjs +119 -7
- package/dist-esm/clustering/runtime.mjs.map +2 -2
- package/dist-esm/index.d.mts +763 -333
- package/dist-esm/index.mjs +87 -41
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/ui/byline.mjs +6 -4
- package/dist-esm/ui/byline.mjs.map +2 -2
- package/dist-esm/ui/comment-card.mjs +15 -8
- package/dist-esm/ui/comment-card.mjs.map +2 -2
- package/dist-esm/ui/comment-composer.mjs +148 -36
- package/dist-esm/ui/comment-composer.mjs.map +2 -2
- package/dist-esm/ui/comment-pin.mjs +24 -2
- package/dist-esm/ui/comment-pin.mjs.map +2 -2
- package/dist-esm/ui/comment-thread.mjs +9 -7
- package/dist-esm/ui/comment-thread.mjs.map +2 -2
- package/dist-esm/ui/comments-list.mjs +43 -29
- package/dist-esm/ui/comments-list.mjs.map +2 -2
- package/dist-esm/ui/count-badge.mjs +3 -2
- package/dist-esm/ui/count-badge.mjs.map +2 -2
- package/dist-esm/ui/emoji-picker.mjs +34 -0
- package/dist-esm/ui/emoji-picker.mjs.map +7 -0
- package/dist-esm/ui/empty-state.mjs +3 -3
- package/dist-esm/ui/empty-state.mjs.map +2 -2
- package/dist-esm/ui/format-time.mjs +4 -1
- package/dist-esm/ui/format-time.mjs.map +2 -2
- package/dist-esm/ui/reaction-picker.mjs +60 -0
- package/dist-esm/ui/reaction-picker.mjs.map +7 -0
- package/dist-esm/ui/reaction.mjs +60 -7
- package/dist-esm/ui/reaction.mjs.map +2 -2
- package/dist-esm/ui/reactions.mjs +21 -8
- package/dist-esm/ui/reactions.mjs.map +2 -2
- package/dist-esm/ui/reply-count.mjs +9 -0
- package/dist-esm/ui/reply-count.mjs.map +7 -0
- package/dist-esm/ui/send-button.mjs +20 -1
- package/dist-esm/ui/send-button.mjs.map +2 -2
- package/dist-esm/ui/tooltip-button.mjs +12 -0
- package/dist-esm/ui/tooltip-button.mjs.map +7 -0
- package/package.json +6 -6
- package/src/canvas/anchor-lifecycle.test.ts +290 -0
- package/src/canvas/anchor-lifecycle.ts +205 -0
- package/src/canvas/canvas.css +461 -46
- package/src/canvas/cluster-input.test.ts +31 -23
- package/src/canvas/cluster-input.ts +3 -4
- package/src/canvas/comment-body.tsx +1 -1
- package/src/canvas/comment-drafts.test.ts +55 -0
- package/src/canvas/comment-drafts.ts +46 -0
- package/src/canvas/comment-mutations.test.ts +410 -0
- package/src/canvas/comment-mutations.ts +288 -0
- package/src/canvas/comment-reactions.test.ts +132 -0
- package/src/canvas/comment-reactions.tsx +245 -0
- package/src/canvas/comment-render.test.ts +1 -1
- package/src/canvas/comment-render.ts +9 -5
- package/src/canvas/comment-store.ts +67 -27
- package/src/canvas/comment-tool.test.ts +162 -0
- package/src/canvas/comment-tool.tsx +105 -23
- package/src/canvas/comments-filter-menu.tsx +23 -32
- package/src/canvas/comments-overflow-menu.tsx +14 -21
- package/src/canvas/comments-overlay.tsx +695 -622
- package/src/canvas/comments-sidebar.test.ts +45 -0
- package/src/canvas/comments-sidebar.tsx +76 -58
- package/src/canvas/comments-visibility-toggle.tsx +49 -0
- package/src/canvas/context.ts +47 -0
- package/src/canvas/hooks.ts +52 -11
- package/src/canvas/options.test.ts +52 -2
- package/src/canvas/options.ts +133 -9
- package/src/canvas/pin-stacking.test.ts +114 -0
- package/src/canvas/pin-stacking.ts +57 -0
- package/src/canvas/sidebar-filters.ts +4 -3
- package/src/canvas/state.ts +92 -35
- package/src/canvas/thread-preview.test.ts +91 -0
- package/src/canvas/thread-preview.tsx +304 -0
- package/src/canvas/thread-stack.tsx +252 -0
- package/src/canvas/thread-state.test.ts +209 -0
- package/src/canvas/thread-state.ts +137 -36
- package/src/canvas/thread-view.tsx +516 -0
- package/src/clustering/computeClusterTable.test.ts +24 -23
- package/src/clustering/computeClusterTable.ts +5 -5
- package/src/clustering/runtime.test.ts +217 -7
- package/src/clustering/runtime.ts +171 -17
- package/src/clustering/types.ts +9 -9
- package/src/index.ts +76 -40
- package/src/ui/byline.tsx +7 -5
- package/src/ui/comment-card.tsx +20 -10
- package/src/ui/comment-composer.tsx +206 -40
- package/src/ui/comment-pin.tsx +26 -2
- package/src/ui/comment-thread.tsx +13 -6
- package/src/ui/comments-list.test.ts +31 -0
- package/src/ui/comments-list.tsx +45 -21
- package/src/ui/comments.css +566 -281
- package/src/ui/count-badge.tsx +9 -2
- package/src/ui/emoji-picker.test.ts +25 -0
- package/src/ui/emoji-picker.tsx +76 -0
- package/src/ui/empty-state.tsx +3 -3
- package/src/ui/format-time.ts +7 -2
- package/src/ui/reaction-picker.tsx +109 -0
- package/src/ui/reaction.tsx +121 -6
- package/src/ui/reactions.tsx +69 -10
- package/src/ui/reply-count.ts +16 -0
- package/src/ui/send-button.tsx +18 -3
- package/src/ui/tooltip-button.tsx +20 -0
- package/dist-cjs/canvas/region-options.js +0 -49
- package/dist-cjs/canvas/region-options.js.map +0 -7
- package/dist-cjs/ui/avatar.js +0 -43
- package/dist-cjs/ui/avatar.js.map +0 -7
- package/dist-cjs/ui/comment-mention.js +0 -42
- package/dist-cjs/ui/comment-mention.js.map +0 -7
- package/dist-cjs/ui/comment-text.js.map +0 -7
- package/dist-cjs/ui/mention-list.js +0 -70
- package/dist-cjs/ui/mention-list.js.map +0 -7
- package/dist-cjs/ui/mention-suggestion.js +0 -206
- package/dist-cjs/ui/mention-suggestion.js.map +0 -7
- package/dist-cjs/ui/mention.js.map +0 -7
- package/dist-cjs/ui/render-markdown.js +0 -63
- package/dist-cjs/ui/render-markdown.js.map +0 -7
- package/dist-esm/canvas/region-options.mjs +0 -29
- package/dist-esm/canvas/region-options.mjs.map +0 -7
- package/dist-esm/ui/avatar.mjs +0 -23
- package/dist-esm/ui/avatar.mjs.map +0 -7
- package/dist-esm/ui/comment-mention.mjs +0 -22
- package/dist-esm/ui/comment-mention.mjs.map +0 -7
- package/dist-esm/ui/comment-text.mjs +0 -9
- package/dist-esm/ui/comment-text.mjs.map +0 -7
- package/dist-esm/ui/mention-list.mjs +0 -50
- package/dist-esm/ui/mention-list.mjs.map +0 -7
- package/dist-esm/ui/mention-suggestion.mjs +0 -186
- package/dist-esm/ui/mention-suggestion.mjs.map +0 -7
- package/dist-esm/ui/mention.mjs +0 -11
- package/dist-esm/ui/mention.mjs.map +0 -7
- package/dist-esm/ui/render-markdown.mjs +0 -45
- package/dist-esm/ui/render-markdown.mjs.map +0 -7
- package/src/canvas/region-options.ts +0 -57
- package/src/ui/avatar.tsx +0 -31
- package/src/ui/comment-mention.ts +0 -47
- package/src/ui/comment-text.tsx +0 -12
- package/src/ui/mention-list.tsx +0 -106
- package/src/ui/mention-suggestion.test.ts +0 -18
- package/src/ui/mention-suggestion.tsx +0 -285
- package/src/ui/mention.tsx +0 -9
- package/src/ui/render-markdown.tsx +0 -72
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
3
|
+
import { createPortal } from "react-dom";
|
|
4
|
+
import {
|
|
5
|
+
EditorAtom,
|
|
6
|
+
usePassThroughWheelEvents,
|
|
7
|
+
useTranslation,
|
|
8
|
+
useValue
|
|
9
|
+
} from "tldraw";
|
|
10
|
+
import { CommentCard } from "../ui/comment-card.mjs";
|
|
11
|
+
import { replyCountLabel } from "../ui/reply-count.mjs";
|
|
12
|
+
import { useComments } from "./hooks.mjs";
|
|
13
|
+
import { useCommentingOptions } from "./options.mjs";
|
|
14
|
+
import { openStackId, openThreadId } from "./state.mjs";
|
|
15
|
+
import { POPOVER_OFFSET, toCardProps, useResolveName } from "./thread-view.mjs";
|
|
16
|
+
const PREVIEW_OPEN_DELAY_MS = 180;
|
|
17
|
+
const PREVIEW_CLOSE_DELAY_MS = 220;
|
|
18
|
+
const PREVIEW_MAX_THREADS = 5;
|
|
19
|
+
function selectPreviewCards(threads, firstCommentOf, max = PREVIEW_MAX_THREADS) {
|
|
20
|
+
const readable = [];
|
|
21
|
+
for (const thread of threads) {
|
|
22
|
+
const first = firstCommentOf(thread);
|
|
23
|
+
if (first) readable.push({ thread, first });
|
|
24
|
+
}
|
|
25
|
+
return { cards: readable.slice(0, max), overflow: Math.max(0, readable.length - max) };
|
|
26
|
+
}
|
|
27
|
+
const hoveredMarkerId = new EditorAtom("commentHoveredMarkerId", () => null);
|
|
28
|
+
function useMarkerPreview(editor, markerId) {
|
|
29
|
+
const openTimer = useRef(0);
|
|
30
|
+
const closeTimer = useRef(0);
|
|
31
|
+
const suppressed = useValue(
|
|
32
|
+
"marker preview suppressed",
|
|
33
|
+
() => openThreadId.get(editor) !== null || openStackId.get(editor) !== null,
|
|
34
|
+
[editor]
|
|
35
|
+
);
|
|
36
|
+
const shown = useValue("marker preview shown", () => hoveredMarkerId.get(editor) === markerId, [
|
|
37
|
+
editor,
|
|
38
|
+
markerId
|
|
39
|
+
]);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const open = openTimer;
|
|
42
|
+
const close = closeTimer;
|
|
43
|
+
return () => {
|
|
44
|
+
window.clearTimeout(open.current);
|
|
45
|
+
window.clearTimeout(close.current);
|
|
46
|
+
if (hoveredMarkerId.get(editor) === markerId) hoveredMarkerId.set(editor, null);
|
|
47
|
+
};
|
|
48
|
+
}, [editor, markerId]);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (suppressed && hoveredMarkerId.get(editor) === markerId) {
|
|
51
|
+
hoveredMarkerId.set(editor, null);
|
|
52
|
+
}
|
|
53
|
+
}, [suppressed, editor, markerId]);
|
|
54
|
+
const onPointerEnter = () => {
|
|
55
|
+
window.clearTimeout(openTimer.current);
|
|
56
|
+
window.clearTimeout(closeTimer.current);
|
|
57
|
+
if (suppressed) return;
|
|
58
|
+
openTimer.current = window.setTimeout(
|
|
59
|
+
() => hoveredMarkerId.set(editor, markerId),
|
|
60
|
+
PREVIEW_OPEN_DELAY_MS
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
const onPointerLeave = () => {
|
|
64
|
+
window.clearTimeout(openTimer.current);
|
|
65
|
+
window.clearTimeout(closeTimer.current);
|
|
66
|
+
closeTimer.current = window.setTimeout(() => {
|
|
67
|
+
if (hoveredMarkerId.get(editor) === markerId) hoveredMarkerId.set(editor, null);
|
|
68
|
+
}, PREVIEW_CLOSE_DELAY_MS);
|
|
69
|
+
};
|
|
70
|
+
return {
|
|
71
|
+
previewShown: shown && !suppressed,
|
|
72
|
+
previewHandlers: { onPointerEnter, onPointerLeave }
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function ThreadPreview({
|
|
76
|
+
editor,
|
|
77
|
+
threads,
|
|
78
|
+
container,
|
|
79
|
+
variant,
|
|
80
|
+
point,
|
|
81
|
+
onSelectThread,
|
|
82
|
+
onPointerEnter,
|
|
83
|
+
onPointerLeave,
|
|
84
|
+
...props
|
|
85
|
+
}) {
|
|
86
|
+
const options = useCommentingOptions();
|
|
87
|
+
const msg = useTranslation();
|
|
88
|
+
const comments = useComments(editor);
|
|
89
|
+
const resolveName = useResolveName(props.resolveAuthor);
|
|
90
|
+
const ref = useRef(null);
|
|
91
|
+
usePassThroughWheelEvents(ref);
|
|
92
|
+
const { firstByThread, countByThread } = useMemo(() => {
|
|
93
|
+
const first = /* @__PURE__ */ new Map();
|
|
94
|
+
const count = /* @__PURE__ */ new Map();
|
|
95
|
+
for (const comment of comments) {
|
|
96
|
+
if (!first.has(comment.threadId)) first.set(comment.threadId, comment);
|
|
97
|
+
count.set(comment.threadId, (count.get(comment.threadId) ?? 0) + 1);
|
|
98
|
+
}
|
|
99
|
+
return { firstByThread: first, countByThread: count };
|
|
100
|
+
}, [comments]);
|
|
101
|
+
const { cards, overflow } = useMemo(
|
|
102
|
+
() => selectPreviewCards(threads, (thread) => firstByThread.get(thread.id)),
|
|
103
|
+
[threads, firstByThread]
|
|
104
|
+
);
|
|
105
|
+
if (cards.length === 0) return null;
|
|
106
|
+
const offset = POPOVER_OFFSET[variant];
|
|
107
|
+
const isThread = variant === "thread";
|
|
108
|
+
const panelClass = [
|
|
109
|
+
"tlui-cmt-canvas-preview__panel",
|
|
110
|
+
`tlui-cmt-canvas-preview__panel--${variant}`,
|
|
111
|
+
isThread && onSelectThread ? "tlui-cmt-canvas-preview__panel--selectable" : ""
|
|
112
|
+
].filter(Boolean).join(" ");
|
|
113
|
+
return createPortal(
|
|
114
|
+
// The root is the hover region — it carries the bridge back to the marker — while the panel
|
|
115
|
+
// inside it is the visible surface. Keeping them apart is what lets the surface light up on
|
|
116
|
+
// its own hover without the bridge (which reaches back over the marker) lighting it up too.
|
|
117
|
+
/* @__PURE__ */ jsx(
|
|
118
|
+
"div",
|
|
119
|
+
{
|
|
120
|
+
ref,
|
|
121
|
+
className: `tlui-cmt-canvas-preview tlui-cmt-canvas-preview--${variant}`,
|
|
122
|
+
style: {
|
|
123
|
+
left: point.x + offset.x,
|
|
124
|
+
top: point.y + offset.y,
|
|
125
|
+
// The stylesheet sizes the hover bridge against this, so the gap it spans follows
|
|
126
|
+
// the offset rather than being restated as a second magic number.
|
|
127
|
+
"--tlui-cmt-preview-offset": `${offset.x}px`
|
|
128
|
+
},
|
|
129
|
+
onPointerEnter,
|
|
130
|
+
onPointerLeave,
|
|
131
|
+
onPointerDown: (e) => e.stopPropagation(),
|
|
132
|
+
children: /* @__PURE__ */ jsxs("div", { className: panelClass, children: [
|
|
133
|
+
cards.map(({ thread, first }) => {
|
|
134
|
+
const replies = replyCountLabel(msg, (countByThread.get(thread.id) ?? 1) - 1);
|
|
135
|
+
return /* @__PURE__ */ jsx(
|
|
136
|
+
"div",
|
|
137
|
+
{
|
|
138
|
+
className: isThread ? void 0 : onSelectThread ? "tlui-cmt-preview-card tlui-cmt-preview-card--selectable" : "tlui-cmt-preview-card",
|
|
139
|
+
onClick: onSelectThread ? (e) => {
|
|
140
|
+
e.stopPropagation();
|
|
141
|
+
onSelectThread(thread);
|
|
142
|
+
} : void 0,
|
|
143
|
+
children: /* @__PURE__ */ jsx(
|
|
144
|
+
CommentCard,
|
|
145
|
+
{
|
|
146
|
+
...toCardProps(first, props, options.components, resolveName),
|
|
147
|
+
footer: replies ? /* @__PURE__ */ jsx("span", { className: "tlui-cmt-card__replies", children: replies }) : void 0
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
},
|
|
151
|
+
thread.id
|
|
152
|
+
);
|
|
153
|
+
}),
|
|
154
|
+
overflow > 0 && /* @__PURE__ */ jsx("div", { className: "tlui-cmt-preview-more", children: msg("comments.preview-more").replace("{count}", String(overflow)) })
|
|
155
|
+
] })
|
|
156
|
+
}
|
|
157
|
+
),
|
|
158
|
+
container
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
function sortThreadsForPreview(threads) {
|
|
162
|
+
return [...threads].sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : 1));
|
|
163
|
+
}
|
|
164
|
+
export {
|
|
165
|
+
ThreadPreview,
|
|
166
|
+
selectPreviewCards,
|
|
167
|
+
sortThreadsForPreview,
|
|
168
|
+
useMarkerPreview
|
|
169
|
+
};
|
|
170
|
+
//# sourceMappingURL=thread-preview.mjs.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/canvas/thread-preview.tsx"],
|
|
4
|
+
"sourcesContent": ["import { useEffect, useMemo, useRef, type CSSProperties } from 'react'\nimport { createPortal } from 'react-dom'\nimport {\n\tEditor,\n\tEditorAtom,\n\tTLComment,\n\tTLCommentThread,\n\tusePassThroughWheelEvents,\n\tuseTranslation,\n\tuseValue,\n} from 'tldraw'\nimport { CommentCard } from '../ui/comment-card'\nimport { replyCountLabel } from '../ui/reply-count'\nimport { type CommentingContext } from './context'\nimport { useComments } from './hooks'\nimport { useCommentingOptions } from './options'\nimport { openStackId, openThreadId } from './state'\nimport { POPOVER_OFFSET, toCardProps, useResolveName } from './thread-view'\n\n/**\n * Hover previews for every canvas marker \u2014 a single pin, a coincident stack, or a cluster badge.\n * Hovering shows the thread(s) behind the marker as cards; the marker's own click keeps whatever\n * it already did (open the thread, open the stack list, zoom to the cluster's split).\n *\n * The panel is live, not a passive tooltip: the pointer can travel right into it and hover each\n * card, and clicking one opens that thread \u2014 the same affordance the stack list gives its cards.\n * Two pieces make that work. The close delay survives the trip across the gap, and the panel\n * carries an invisible bridge over that gap (see `.tlui-cmt-canvas-preview::before`) so the\n * journey never crosses dead space and retracts the panel mid-move.\n *\n * Opening a thread from a card needs nothing more than setting `openThreadId`, including for a\n * thread currently folded inside a cluster badge: `collectClusterLeaves` skips the open thread, so\n * it drops out of its badge and renders its own pin and popover.\n */\n\n/** How long the pointer must rest on a marker before its preview appears. */\nconst PREVIEW_OPEN_DELAY_MS = 180\n/**\n * Grace period after the pointer leaves the marker *or* the panel. Long enough to cross the gap\n * between them by hand \u2014 the bridge element covers that gap geometrically, and this covers the\n * moment of transit between the two elements' enter/leave events.\n */\nconst PREVIEW_CLOSE_DELAY_MS = 220\n/** Cards shown before the panel falls back to a \"+N more\" line. */\nconst PREVIEW_MAX_THREADS = 5\n\n/** A thread paired with the comment that opens it \u2014 one card of a preview. */\nexport interface ThreadPreviewCard {\n\tthread: TLCommentThread\n\tfirst: TLComment\n}\n\n/**\n * The cards a marker's preview will show, and how many threads it will summarise as \"+N more\".\n *\n * A thread can exist before its opening comment does \u2014 a collaborator's, mid-sync \u2014 and has nothing\n * to preview until it arrives. Those are dropped here rather than rendered as blank cards, and they\n * don't count toward the overflow tally either, so \"+2 more\" always means two readable threads.\n */\nexport function selectPreviewCards(\n\tthreads: readonly TLCommentThread[],\n\tfirstCommentOf: (thread: TLCommentThread) => TLComment | undefined,\n\tmax = PREVIEW_MAX_THREADS\n): { cards: ThreadPreviewCard[]; overflow: number } {\n\tconst readable: ThreadPreviewCard[] = []\n\tfor (const thread of threads) {\n\t\tconst first = firstCommentOf(thread)\n\t\tif (first) readable.push({ thread, first })\n\t}\n\treturn { cards: readable.slice(0, max), overflow: Math.max(0, readable.length - max) }\n}\n\n/**\n * What a marker's click opens, which is what its preview imitates \u2014 the panel's surface, its\n * width, and how it lays a comment out.\n *\n * `'thread'` previews a single pin: a thread panel without the header. `'list'` previews a\n * coincident stack or a cluster: one card per thread, like the stack list.\n */\nexport type ThreadPreviewVariant = 'thread' | 'list'\n\n/**\n * Which marker's preview is showing, or null. One atom for the whole layer, so previews are\n * mutually exclusive by construction: the close delay means an outgoing marker's timer can still\n * be pending when the next marker opens, and per-component state would briefly show both.\n */\nconst hoveredMarkerId = new EditorAtom<string | null>('commentHoveredMarkerId', () => null)\n\n/**\n * Hover state for one marker. Returns whether its preview should render, plus the pointer handlers\n * to spread onto the marker element.\n *\n * `markerId` must be stable and unique per marker across the layer \u2014 prefix by kind, since a stack\n * is keyed by its oldest member's thread id and would otherwise collide with that thread's own pin.\n */\nexport function useMarkerPreview(editor: Editor, markerId: string) {\n\tconst openTimer = useRef(0)\n\tconst closeTimer = useRef(0)\n\n\t// Previews are a resting-state affordance. While a thread or stack list is open, that view is\n\t// the thing being read \u2014 a preview floating over it would just compete with it.\n\tconst suppressed = useValue(\n\t\t'marker preview suppressed',\n\t\t() => openThreadId.get(editor) !== null || openStackId.get(editor) !== null,\n\t\t[editor]\n\t)\n\tconst shown = useValue('marker preview shown', () => hoveredMarkerId.get(editor) === markerId, [\n\t\teditor,\n\t\tmarkerId,\n\t])\n\n\tuseEffect(() => {\n\t\tconst open = openTimer\n\t\tconst close = closeTimer\n\t\treturn () => {\n\t\t\twindow.clearTimeout(open.current)\n\t\t\twindow.clearTimeout(close.current)\n\t\t\t// Don't strand the atom on a marker that has unmounted \u2014 zoomed into a cluster, deleted,\n\t\t\t// or folded away \u2014 or no other marker's preview could ever show.\n\t\t\tif (hoveredMarkerId.get(editor) === markerId) hoveredMarkerId.set(editor, null)\n\t\t}\n\t}, [editor, markerId])\n\n\t// Suppression can start *while* a preview is up: clicking the hovered pin opens its thread.\n\t// Retract the preview rather than leaving it stranded behind the popover that just opened.\n\tuseEffect(() => {\n\t\tif (suppressed && hoveredMarkerId.get(editor) === markerId) {\n\t\t\thoveredMarkerId.set(editor, null)\n\t\t}\n\t}, [suppressed, editor, markerId])\n\n\tconst onPointerEnter = () => {\n\t\twindow.clearTimeout(openTimer.current)\n\t\twindow.clearTimeout(closeTimer.current)\n\t\tif (suppressed) return\n\t\topenTimer.current = window.setTimeout(\n\t\t\t() => hoveredMarkerId.set(editor, markerId),\n\t\t\tPREVIEW_OPEN_DELAY_MS\n\t\t)\n\t}\n\n\tconst onPointerLeave = () => {\n\t\twindow.clearTimeout(openTimer.current)\n\t\twindow.clearTimeout(closeTimer.current)\n\t\tcloseTimer.current = window.setTimeout(() => {\n\t\t\tif (hoveredMarkerId.get(editor) === markerId) hoveredMarkerId.set(editor, null)\n\t\t}, PREVIEW_CLOSE_DELAY_MS)\n\t}\n\n\treturn {\n\t\tpreviewShown: shown && !suppressed,\n\t\tpreviewHandlers: { onPointerEnter, onPointerLeave },\n\t}\n}\n\n/**\n * The hover panel: each thread's opening comment as a read-only card, capped with a \"+N more\"\n * line. Mounted only while hovering, so the store subscription it needs to find those comments\n * costs nothing at rest.\n */\nexport function ThreadPreview({\n\teditor,\n\tthreads,\n\tcontainer,\n\tvariant,\n\tpoint,\n\tonSelectThread,\n\tonPointerEnter,\n\tonPointerLeave,\n\t...props\n}: Pick<CommentingContext, 'currentUserId' | 'resolveAuthor'> & {\n\teditor: Editor\n\t/** The marker's threads, in the order they should read (oldest first). */\n\tthreads: readonly TLCommentThread[]\n\tcontainer: HTMLElement\n\t/** What the marker's click opens, which this panel imitates. */\n\tvariant: ThreadPreviewVariant\n\t/** The marker's anchor point in viewport space \u2014 the same origin its popover is placed from. */\n\tpoint: { x: number; y: number }\n\t/** Open a thread from its card. Omit to leave the cards inert. */\n\tonSelectThread?(thread: TLCommentThread): void\n\t/** The owning marker's hover handlers, so the panel counts as part of its hover region. */\n\tonPointerEnter?(): void\n\tonPointerLeave?(): void\n}) {\n\tconst options = useCommentingOptions()\n\tconst msg = useTranslation()\n\tconst comments = useComments(editor)\n\tconst resolveName = useResolveName(props.resolveAuthor)\n\n\t// The panel floats over the canvas and scrolls nothing of its own, so a wheel on it should zoom\n\t// and pan the canvas underneath \u2014 like every other tldraw panel, and like the popover it\n\t// previews. Without this the canvas would freeze wherever the preview happened to be.\n\tconst ref = useRef<HTMLDivElement>(null)\n\tusePassThroughWheelEvents(ref)\n\n\t// Each thread's opening comment and its total comment count. `useComments` is oldest-first, so\n\t// the first hit per thread is that thread's first comment. One pass over every comment beats a\n\t// per-thread hook \u2014 the thread count here is driven by cluster size, which has no fixed bound.\n\tconst { firstByThread, countByThread } = useMemo(() => {\n\t\tconst first = new Map<string, TLComment>()\n\t\tconst count = new Map<string, number>()\n\t\tfor (const comment of comments) {\n\t\t\tif (!first.has(comment.threadId)) first.set(comment.threadId, comment)\n\t\t\tcount.set(comment.threadId, (count.get(comment.threadId) ?? 0) + 1)\n\t\t}\n\t\treturn { firstByThread: first, countByThread: count }\n\t}, [comments])\n\n\tconst { cards, overflow } = useMemo(\n\t\t() => selectPreviewCards(threads, (thread) => firstByThread.get(thread.id)),\n\t\t[threads, firstByThread]\n\t)\n\n\t// Nothing readable yet \u2014 render no panel rather than an empty one. It matters most for the\n\t// thread variant, where the panel *is* the card's surface: an empty one would paint as a blank\n\t// box floating on the canvas.\n\tif (cards.length === 0) return null\n\n\t// Placed at the same origin as the popover this previews, so the two only differ by what their\n\t// stylesheets do \u2014 see `.tlui-cmt-canvas-preview__panel--thread` for the header compensation.\n\tconst offset = POPOVER_OFFSET[variant]\n\t// A thread preview is one comment on the panel itself, so the panel carries the hover and the\n\t// click. A list preview puts each thread on its own card, as the stack list does.\n\tconst isThread = variant === 'thread'\n\tconst panelClass = [\n\t\t'tlui-cmt-canvas-preview__panel',\n\t\t`tlui-cmt-canvas-preview__panel--${variant}`,\n\t\tisThread && onSelectThread ? 'tlui-cmt-canvas-preview__panel--selectable' : '',\n\t]\n\t\t.filter(Boolean)\n\t\t.join(' ')\n\n\treturn createPortal(\n\t\t// The root is the hover region \u2014 it carries the bridge back to the marker \u2014 while the panel\n\t\t// inside it is the visible surface. Keeping them apart is what lets the surface light up on\n\t\t// its own hover without the bridge (which reaches back over the marker) lighting it up too.\n\t\t<div\n\t\t\tref={ref}\n\t\t\tclassName={`tlui-cmt-canvas-preview tlui-cmt-canvas-preview--${variant}`}\n\t\t\tstyle={\n\t\t\t\t{\n\t\t\t\t\tleft: point.x + offset.x,\n\t\t\t\t\ttop: point.y + offset.y,\n\t\t\t\t\t// The stylesheet sizes the hover bridge against this, so the gap it spans follows\n\t\t\t\t\t// the offset rather than being restated as a second magic number.\n\t\t\t\t\t'--tlui-cmt-preview-offset': `${offset.x}px`,\n\t\t\t\t} as CSSProperties\n\t\t\t}\n\t\t\tonPointerEnter={onPointerEnter}\n\t\t\tonPointerLeave={onPointerLeave}\n\t\t\t// The panel sits over the canvas; a press on it is not a canvas press.\n\t\t\tonPointerDown={(e) => e.stopPropagation()}\n\t\t>\n\t\t\t<div className={panelClass}>\n\t\t\t\t{cards.map(({ thread, first }) => {\n\t\t\t\t\t// The preview shows only the opening comment, so its reply count is what tells the\n\t\t\t\t\t// reader the thread continues past what they see.\n\t\t\t\t\tconst replies = replyCountLabel(msg, (countByThread.get(thread.id) ?? 1) - 1)\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tkey={thread.id}\n\t\t\t\t\t\t\tclassName={\n\t\t\t\t\t\t\t\tisThread\n\t\t\t\t\t\t\t\t\t? undefined\n\t\t\t\t\t\t\t\t\t: onSelectThread\n\t\t\t\t\t\t\t\t\t\t? 'tlui-cmt-preview-card tlui-cmt-preview-card--selectable'\n\t\t\t\t\t\t\t\t\t\t: 'tlui-cmt-preview-card'\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tonClick={\n\t\t\t\t\t\t\t\tonSelectThread\n\t\t\t\t\t\t\t\t\t? (e) => {\n\t\t\t\t\t\t\t\t\t\t\te.stopPropagation()\n\t\t\t\t\t\t\t\t\t\t\tonSelectThread(thread)\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t: undefined\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<CommentCard\n\t\t\t\t\t\t\t\t{...toCardProps(first, props, options.components, resolveName)}\n\t\t\t\t\t\t\t\tfooter={\n\t\t\t\t\t\t\t\t\treplies ? <span className=\"tlui-cmt-card__replies\">{replies}</span> : undefined\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)\n\t\t\t\t})}\n\t\t\t\t{overflow > 0 && (\n\t\t\t\t\t<div className=\"tlui-cmt-preview-more\">\n\t\t\t\t\t\t{msg('comments.preview-more').replace('{count}', String(overflow))}\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t</div>,\n\t\tcontainer\n\t)\n}\n\n/** Order a marker's threads for reading: oldest first, id as the tiebreak so it's stable. */\nexport function sortThreadsForPreview(\n\tthreads: readonly TLCommentThread[]\n): readonly TLCommentThread[] {\n\treturn [...threads].sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : 1))\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { memo, useEffect, useRef } from "react";
|
|
3
|
+
import {
|
|
4
|
+
useContainer,
|
|
5
|
+
usePassThroughWheelEvents,
|
|
6
|
+
useTranslation,
|
|
7
|
+
useValue
|
|
8
|
+
} from "tldraw";
|
|
9
|
+
import { CommentCard } from "../ui/comment-card.mjs";
|
|
10
|
+
import { CountBadge } from "../ui/count-badge.mjs";
|
|
11
|
+
import { UNKNOWN_AUTHOR } from "./comment-render.mjs";
|
|
12
|
+
import { useThreadComments } from "./hooks.mjs";
|
|
13
|
+
import { useCommentingOptions } from "./options.mjs";
|
|
14
|
+
import { pinStackKey } from "./pin-stacking.mjs";
|
|
15
|
+
import { openStackId, openThreadId } from "./state.mjs";
|
|
16
|
+
import { ThreadPreview, useMarkerPreview } from "./thread-preview.mjs";
|
|
17
|
+
import { anchorPagePoint } from "./thread-state.mjs";
|
|
18
|
+
import {
|
|
19
|
+
POPOVER_OFFSET,
|
|
20
|
+
ThreadPopover,
|
|
21
|
+
ThreadView,
|
|
22
|
+
toCardProps,
|
|
23
|
+
useResolveName
|
|
24
|
+
} from "./thread-view.mjs";
|
|
25
|
+
const ThreadStackPin = memo(function ThreadStackPin2({
|
|
26
|
+
editor,
|
|
27
|
+
threads,
|
|
28
|
+
...props
|
|
29
|
+
}) {
|
|
30
|
+
const container = useContainer();
|
|
31
|
+
const msg = useTranslation();
|
|
32
|
+
const badgeRef = useRef(null);
|
|
33
|
+
usePassThroughWheelEvents(badgeRef);
|
|
34
|
+
const { previewShown, previewHandlers } = useMarkerPreview(editor, `stack:${threads[0].id}`);
|
|
35
|
+
const stackId = useValue(
|
|
36
|
+
"stack id",
|
|
37
|
+
() => {
|
|
38
|
+
const pagePoint = anchorPagePoint(editor, threads[0].anchor);
|
|
39
|
+
return pagePoint ? pinStackKey(pagePoint) : threads[0].id;
|
|
40
|
+
},
|
|
41
|
+
[editor, threads]
|
|
42
|
+
);
|
|
43
|
+
const listOpen = useValue("stack list open", () => openStackId.get(editor) === stackId, [
|
|
44
|
+
editor,
|
|
45
|
+
stackId
|
|
46
|
+
]);
|
|
47
|
+
const openId = useValue("open thread id", () => openThreadId.get(editor), [editor]);
|
|
48
|
+
const openMember = threads.find((thread) => thread.id === openId);
|
|
49
|
+
const open = listOpen || openMember !== void 0;
|
|
50
|
+
const point = useValue(
|
|
51
|
+
"stack point",
|
|
52
|
+
() => {
|
|
53
|
+
const first = threads[0];
|
|
54
|
+
if (first.pageId !== editor.getCurrentPageId()) return null;
|
|
55
|
+
const pagePoint = anchorPagePoint(editor, first.anchor);
|
|
56
|
+
return pagePoint ? editor.pageToViewport(pagePoint) : null;
|
|
57
|
+
},
|
|
58
|
+
[editor, threads]
|
|
59
|
+
);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!open) return;
|
|
62
|
+
const onPointerDown = (e) => {
|
|
63
|
+
const target = e.target;
|
|
64
|
+
if (!target) return;
|
|
65
|
+
if (target.closest(".tlui-cmt-canvas-popover")) return;
|
|
66
|
+
const badge = badgeRef.current;
|
|
67
|
+
if (badge && badge.contains(target)) return;
|
|
68
|
+
if (target.closest(".tlui-menu, [data-radix-popper-content-wrapper], .tlui-cmt-mention-popup"))
|
|
69
|
+
return;
|
|
70
|
+
openStackId.set(editor, null);
|
|
71
|
+
openThreadId.set(editor, null);
|
|
72
|
+
};
|
|
73
|
+
document.addEventListener("pointerdown", onPointerDown, true);
|
|
74
|
+
return () => document.removeEventListener("pointerdown", onPointerDown, true);
|
|
75
|
+
}, [open, editor]);
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (!listOpen) return;
|
|
78
|
+
const onKeyDown = (e) => {
|
|
79
|
+
if (e.key !== "Escape" || e.defaultPrevented) return;
|
|
80
|
+
openStackId.set(editor, null);
|
|
81
|
+
};
|
|
82
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
83
|
+
return () => document.removeEventListener("keydown", onKeyDown, true);
|
|
84
|
+
}, [listOpen, editor]);
|
|
85
|
+
if (!point) return null;
|
|
86
|
+
const toggle = () => {
|
|
87
|
+
if (open) {
|
|
88
|
+
openStackId.set(editor, null);
|
|
89
|
+
openThreadId.set(editor, null);
|
|
90
|
+
} else {
|
|
91
|
+
openStackId.set(editor, stackId);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const liftForHeader = threads[0]?.id === openId ? 42 : 0;
|
|
95
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
96
|
+
/* @__PURE__ */ jsx("div", { className: "tlui-cmt-canvas-pin", style: { left: point.x, top: point.y }, children: /* @__PURE__ */ jsx(
|
|
97
|
+
"button",
|
|
98
|
+
{
|
|
99
|
+
ref: badgeRef,
|
|
100
|
+
type: "button",
|
|
101
|
+
className: "tlui-cmt-button tlui-cmt-canvas-stack-badge",
|
|
102
|
+
"aria-label": msg("comments.stack-label").replace("{count}", String(threads.length)),
|
|
103
|
+
"aria-expanded": open,
|
|
104
|
+
onPointerDown: (e) => e.stopPropagation(),
|
|
105
|
+
onClick: (e) => {
|
|
106
|
+
e.stopPropagation();
|
|
107
|
+
toggle();
|
|
108
|
+
},
|
|
109
|
+
...previewHandlers,
|
|
110
|
+
onFocus: previewHandlers.onPointerEnter,
|
|
111
|
+
onBlur: previewHandlers.onPointerLeave,
|
|
112
|
+
children: /* @__PURE__ */ jsx(CountBadge, { count: threads.length, open })
|
|
113
|
+
}
|
|
114
|
+
) }),
|
|
115
|
+
previewShown && !open && /* @__PURE__ */ jsx(
|
|
116
|
+
ThreadPreview,
|
|
117
|
+
{
|
|
118
|
+
editor,
|
|
119
|
+
threads,
|
|
120
|
+
container,
|
|
121
|
+
variant: "list",
|
|
122
|
+
point,
|
|
123
|
+
onSelectThread: (thread) => {
|
|
124
|
+
openStackId.set(editor, stackId);
|
|
125
|
+
openThreadId.set(editor, thread.id);
|
|
126
|
+
},
|
|
127
|
+
...previewHandlers,
|
|
128
|
+
currentUserId: props.currentUserId,
|
|
129
|
+
resolveAuthor: props.resolveAuthor
|
|
130
|
+
}
|
|
131
|
+
),
|
|
132
|
+
open && /* @__PURE__ */ jsx(
|
|
133
|
+
ThreadPopover,
|
|
134
|
+
{
|
|
135
|
+
container,
|
|
136
|
+
style: {
|
|
137
|
+
left: point.x + POPOVER_OFFSET.list.x,
|
|
138
|
+
top: point.y + POPOVER_OFFSET.list.y - liftForHeader
|
|
139
|
+
},
|
|
140
|
+
children: /* @__PURE__ */ jsx("div", { className: "tlui-cmt-stack-list", children: threads.map(
|
|
141
|
+
(thread) => thread.id === openId ? /* @__PURE__ */ jsx("div", { className: "tlui-cmt-stack-list__thread", children: /* @__PURE__ */ jsx(ThreadView, { editor, thread, ...props }) }, thread.id) : /* @__PURE__ */ jsx(
|
|
142
|
+
StackThreadCard,
|
|
143
|
+
{
|
|
144
|
+
editor,
|
|
145
|
+
thread,
|
|
146
|
+
...props,
|
|
147
|
+
onOpen: () => openThreadId.set(editor, thread.id)
|
|
148
|
+
},
|
|
149
|
+
thread.id
|
|
150
|
+
)
|
|
151
|
+
) })
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
] });
|
|
155
|
+
});
|
|
156
|
+
function StackThreadCard({
|
|
157
|
+
editor,
|
|
158
|
+
thread,
|
|
159
|
+
onOpen,
|
|
160
|
+
...props
|
|
161
|
+
}) {
|
|
162
|
+
const msg = useTranslation();
|
|
163
|
+
const options = useCommentingOptions();
|
|
164
|
+
const comments = useThreadComments(editor, thread.id);
|
|
165
|
+
const resolveName = useResolveName(props.resolveAuthor);
|
|
166
|
+
const first = comments[0];
|
|
167
|
+
if (!first) return null;
|
|
168
|
+
const open = (e) => {
|
|
169
|
+
e.stopPropagation();
|
|
170
|
+
onOpen();
|
|
171
|
+
};
|
|
172
|
+
return /* @__PURE__ */ jsxs("div", { className: "tlui-cmt-stack-list__card", onClick: open, children: [
|
|
173
|
+
/* @__PURE__ */ jsx(
|
|
174
|
+
"button",
|
|
175
|
+
{
|
|
176
|
+
type: "button",
|
|
177
|
+
className: "tlui-cmt-button tlui-cmt-stack-list__card-action",
|
|
178
|
+
"aria-label": msg(
|
|
179
|
+
thread.resolved ? "comments.pin-label-resolved" : "comments.pin-label"
|
|
180
|
+
).replace("{name}", props.resolveAuthor(thread.createdBy)?.name ?? UNKNOWN_AUTHOR),
|
|
181
|
+
onClick: open
|
|
182
|
+
}
|
|
183
|
+
),
|
|
184
|
+
/* @__PURE__ */ jsx(CommentCard, { ...toCardProps(first, props, options.components, resolveName) })
|
|
185
|
+
] });
|
|
186
|
+
}
|
|
187
|
+
export {
|
|
188
|
+
ThreadStackPin
|
|
189
|
+
};
|
|
190
|
+
//# sourceMappingURL=thread-stack.mjs.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/canvas/thread-stack.tsx"],
|
|
4
|
+
"sourcesContent": ["import { type MouseEvent as ReactMouseEvent, memo, useEffect, useRef } from 'react'\nimport {\n\tEditor,\n\tTLCommentThread,\n\tuseContainer,\n\tusePassThroughWheelEvents,\n\tuseTranslation,\n\tuseValue,\n} from 'tldraw'\nimport { CommentCard } from '../ui/comment-card'\nimport { CountBadge } from '../ui/count-badge'\nimport { UNKNOWN_AUTHOR } from './comment-render'\nimport { type CommentingContext } from './context'\nimport { useThreadComments } from './hooks'\nimport { useCommentingOptions } from './options'\nimport { pinStackKey } from './pin-stacking'\nimport { openStackId, openThreadId } from './state'\nimport { ThreadPreview, useMarkerPreview } from './thread-preview'\nimport { anchorPagePoint } from './thread-state'\nimport {\n\tPOPOVER_OFFSET,\n\tThreadPopover,\n\tThreadView,\n\ttoCardProps,\n\tuseResolveName,\n} from './thread-view'\n\n/**\n * The pin for threads whose anchors resolve to the same page point \u2014 pins zooming can never\n * separate, so instead of stacked markers they share one count badge. Clicking it opens a\n * popover listing each thread as a card; clicking a card expands that thread in place (via the\n * single open-thread state, so expanding one collapses another).\n */\nexport const ThreadStackPin = memo(function ThreadStackPin({\n\teditor,\n\tthreads,\n\t...props\n}: CommentingContext & {\n\teditor: Editor\n\t/** The stack's threads, oldest first. All resolve to the same anchor point. */\n\tthreads: readonly TLCommentThread[]\n}) {\n\tconst container = useContainer()\n\tconst msg = useTranslation()\n\tconst badgeRef = useRef<HTMLButtonElement>(null)\n\t// The badge takes pointer events (to open on click), so wheel input over it would otherwise be\n\t// swallowed instead of zooming \u2014 pass it through to the canvas, as the pin and cluster badge do.\n\tusePassThroughWheelEvents(badgeRef)\n\t// Hovering the badge previews its threads; clicking still opens them as the interactive list.\n\t// The preview is what makes the badge legible before you commit to opening it.\n\tconst { previewShown, previewHandlers } = useMarkerPreview(editor, `stack:${threads[0].id}`)\n\t// The list stays open while a member thread is expanded, and on its own after the member\n\t// collapses \u2014 so Escape steps back: expanded thread \u2192 card list \u2192 closed. Held in editor\n\t// state (not component state) because this pin remounts as its owning render path changes.\n\t// Keyed by the coincident page point, not a member id, so the open state survives losing a\n\t// member (including the oldest): the survivors keep the same key. Falls back to a thread id\n\t// only when the anchor can't resolve (off page), where the list isn't shown anyway.\n\tconst stackId = useValue(\n\t\t'stack id',\n\t\t() => {\n\t\t\tconst pagePoint = anchorPagePoint(editor, threads[0].anchor)\n\t\t\treturn pagePoint ? pinStackKey(pagePoint) : threads[0].id\n\t\t},\n\t\t[editor, threads]\n\t)\n\tconst listOpen = useValue('stack list open', () => openStackId.get(editor) === stackId, [\n\t\teditor,\n\t\tstackId,\n\t])\n\tconst openId = useValue('open thread id', () => openThreadId.get(editor), [editor])\n\tconst openMember = threads.find((thread) => thread.id === openId)\n\tconst open = listOpen || openMember !== undefined\n\n\tconst point = useValue(\n\t\t'stack point',\n\t\t() => {\n\t\t\tconst first = threads[0]\n\t\t\tif (first.pageId !== editor.getCurrentPageId()) return null\n\t\t\t// The badge hangs off its anchor point bottom-left (transform: translate(0, -100%)),\n\t\t\t// like a pin and like the cluster badge \u2014 but it sits at the raw page point with no pin\n\t\t\t// inset. The inset only tucks an imprecise single pin inside its shape; applying it here\n\t\t\t// would offset the badge from where the cluster badge sits (cluster centroids average\n\t\t\t// raw anchor points) and make it hop as pins flip between them.\n\t\t\tconst pagePoint = anchorPagePoint(editor, first.anchor)\n\t\t\treturn pagePoint ? editor.pageToViewport(pagePoint) : null\n\t\t},\n\t\t[editor, threads]\n\t)\n\n\t// Clicking outside the popover (and off the badge) closes the whole stack \u2014 mirrors the\n\t// single pin's dismiss. Capture phase + class checks, since the popover portals elsewhere.\n\tuseEffect(() => {\n\t\tif (!open) return\n\t\tconst onPointerDown = (e: PointerEvent) => {\n\t\t\tconst target = e.target as HTMLElement | null\n\t\t\tif (!target) return\n\t\t\tif (target.closest('.tlui-cmt-canvas-popover')) return\n\t\t\tconst badge = badgeRef.current\n\t\t\tif (badge && badge.contains(target)) return\n\t\t\t// A click inside a menu/popover layered above us belongs to that layer; defer to its\n\t\t\t// own dismissal instead of closing the stack out from under it.\n\t\t\tif (\n\t\t\t\ttarget.closest('.tlui-menu, [data-radix-popper-content-wrapper], .tlui-cmt-mention-popup')\n\t\t\t)\n\t\t\t\treturn\n\t\t\topenStackId.set(editor, null)\n\t\t\topenThreadId.set(editor, null)\n\t\t}\n\t\tdocument.addEventListener('pointerdown', onPointerDown, true)\n\t\treturn () => document.removeEventListener('pointerdown', onPointerDown, true)\n\t}, [open, editor])\n\n\t// Escape with only the card list showing closes it. When a member thread is expanded, the\n\t// layer's Escape handler collapses that first and marks the event consumed \u2014 stepping back\n\t// to the list rather than closing everything at once.\n\tuseEffect(() => {\n\t\tif (!listOpen) return\n\t\tconst onKeyDown = (e: KeyboardEvent) => {\n\t\t\tif (e.key !== 'Escape' || e.defaultPrevented) return\n\t\t\topenStackId.set(editor, null)\n\t\t}\n\t\tdocument.addEventListener('keydown', onKeyDown, true)\n\t\treturn () => document.removeEventListener('keydown', onKeyDown, true)\n\t}, [listOpen, editor])\n\n\tif (!point) return null\n\n\tconst toggle = () => {\n\t\tif (open) {\n\t\t\topenStackId.set(editor, null)\n\t\t\topenThreadId.set(editor, null)\n\t\t} else {\n\t\t\topenStackId.set(editor, stackId)\n\t\t}\n\t}\n\n\t// The expanded thread gains a \"Comment\" header that pushes its first comment down from where the\n\t// hover preview showed it. When that thread is the list's first, lift the whole list by the\n\t// header block so its \"You\" holds position across hover -> open: the single thread's\n\t// THREAD_HEADER_BLOCK (36) plus the 8px margin above the expanded entry, less the 2px top the\n\t// preview card sits its \"You\" down by. Only the first entry \u2014 lifting the list can't also hold a\n\t// lower thread's neighbours in place.\n\tconst liftForHeader = threads[0]?.id === openId ? 42 : 0\n\n\treturn (\n\t\t<>\n\t\t\t<div className=\"tlui-cmt-canvas-pin\" style={{ left: point.x, top: point.y }}>\n\t\t\t\t<button\n\t\t\t\t\tref={badgeRef}\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclassName=\"tlui-cmt-button tlui-cmt-canvas-stack-badge\"\n\t\t\t\t\taria-label={msg('comments.stack-label').replace('{count}', String(threads.length))}\n\t\t\t\t\taria-expanded={open}\n\t\t\t\t\tonPointerDown={(e) => e.stopPropagation()}\n\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\te.stopPropagation()\n\t\t\t\t\t\ttoggle()\n\t\t\t\t\t}}\n\t\t\t\t\t{...previewHandlers}\n\t\t\t\t\tonFocus={previewHandlers.onPointerEnter}\n\t\t\t\t\tonBlur={previewHandlers.onPointerLeave}\n\t\t\t\t>\n\t\t\t\t\t<CountBadge count={threads.length} open={open} />\n\t\t\t\t</button>\n\t\t\t</div>\n\t\t\t{previewShown && !open && (\n\t\t\t\t<ThreadPreview\n\t\t\t\t\teditor={editor}\n\t\t\t\t\tthreads={threads}\n\t\t\t\t\tcontainer={container}\n\t\t\t\t\t// Lines the preview up with the stack list itself, so opening it leaves the cards\n\t\t\t\t\t// exactly where the preview had them.\n\t\t\t\t\tvariant=\"list\"\n\t\t\t\t\tpoint={point}\n\t\t\t\t\t// Picking a card from the preview lands in the same place clicking the badge and\n\t\t\t\t\t// then the card would: the list open, that thread expanded within it.\n\t\t\t\t\tonSelectThread={(thread) => {\n\t\t\t\t\t\topenStackId.set(editor, stackId)\n\t\t\t\t\t\topenThreadId.set(editor, thread.id)\n\t\t\t\t\t}}\n\t\t\t\t\t{...previewHandlers}\n\t\t\t\t\tcurrentUserId={props.currentUserId}\n\t\t\t\t\tresolveAuthor={props.resolveAuthor}\n\t\t\t\t/>\n\t\t\t)}\n\t\t\t{open && (\n\t\t\t\t<ThreadPopover\n\t\t\t\t\tcontainer={container}\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tleft: point.x + POPOVER_OFFSET.list.x,\n\t\t\t\t\t\ttop: point.y + POPOVER_OFFSET.list.y - liftForHeader,\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<div className=\"tlui-cmt-stack-list\">\n\t\t\t\t\t\t{threads.map((thread) =>\n\t\t\t\t\t\t\tthread.id === openId ? (\n\t\t\t\t\t\t\t\t<div key={thread.id} className=\"tlui-cmt-stack-list__thread\">\n\t\t\t\t\t\t\t\t\t<ThreadView editor={editor} thread={thread} {...props} />\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t<StackThreadCard\n\t\t\t\t\t\t\t\t\tkey={thread.id}\n\t\t\t\t\t\t\t\t\teditor={editor}\n\t\t\t\t\t\t\t\t\tthread={thread}\n\t\t\t\t\t\t\t\t\t{...props}\n\t\t\t\t\t\t\t\t\tonOpen={() => openThreadId.set(editor, thread.id)}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t)}\n\t\t\t\t\t</div>\n\t\t\t\t</ThreadPopover>\n\t\t\t)}\n\t\t</>\n\t)\n})\n\n/** A collapsed stack entry: the thread's first comment as a card; clicking expands the thread. */\nfunction StackThreadCard({\n\teditor,\n\tthread,\n\tonOpen,\n\t...props\n}: CommentingContext & { editor: Editor; thread: TLCommentThread; onOpen(): void }) {\n\tconst msg = useTranslation()\n\tconst options = useCommentingOptions()\n\tconst comments = useThreadComments(editor, thread.id)\n\tconst resolveName = useResolveName(props.resolveAuthor)\n\tconst first = comments[0]\n\tif (!first) return null\n\tconst open = (e: ReactMouseEvent) => {\n\t\te.stopPropagation()\n\t\tonOpen()\n\t}\n\treturn (\n\t\t<div className=\"tlui-cmt-stack-list__card\" onClick={open}>\n\t\t\t{/* The card's keyboard affordance. A button *wrapping* the card would put the comment\n\t\t\t body inside it, and a body renders its links as real anchors \u2014 interactive content\n\t\t\t nested in a button, which is an invalid content model and reads to assistive tech as a\n\t\t\t broken control. So the button covers the card as a sibling instead, leaving the body's\n\t\t\t own links above it and still reachable. */}\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tclassName=\"tlui-cmt-button tlui-cmt-stack-list__card-action\"\n\t\t\t\taria-label={msg(\n\t\t\t\t\tthread.resolved ? 'comments.pin-label-resolved' : 'comments.pin-label'\n\t\t\t\t).replace('{name}', props.resolveAuthor(thread.createdBy)?.name ?? UNKNOWN_AUTHOR)}\n\t\t\t\tonClick={open}\n\t\t\t/>\n\t\t\t<CommentCard {...toCardProps(first, props, options.components, resolveName)} />\n\t\t</div>\n\t)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["ThreadStackPin"]
|
|
7
|
+
}
|
|
@@ -1,60 +1,109 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
Mat,
|
|
3
|
+
Vec
|
|
4
|
+
} from "tldraw";
|
|
5
|
+
import { getCommentingOptions } from "./options.mjs";
|
|
6
|
+
import { commentsSidebarOpen, openThreadId } from "./state.mjs";
|
|
7
|
+
import { POPOVER_OFFSET } from "./thread-view.mjs";
|
|
8
|
+
const IMPRECISE_PIN_INSET_PX = 20;
|
|
9
|
+
function impreciseShapePinInset(editor, anchor) {
|
|
10
|
+
if (anchor.type !== "shape" || anchor.isPrecise) return null;
|
|
11
|
+
const spot = getCommentingOptions(editor).impreciseShapeAnchor;
|
|
12
|
+
const inset = {
|
|
13
|
+
x: Math.sign(0.5 - spot.x) * IMPRECISE_PIN_INSET_PX,
|
|
14
|
+
y: Math.sign(0.5 - spot.y) * IMPRECISE_PIN_INSET_PX
|
|
15
|
+
};
|
|
16
|
+
const rotation = editor.getShapePageTransform(anchor.shapeId)?.rotation() ?? 0;
|
|
17
|
+
return rotation === 0 ? inset : Vec.Rot(inset, rotation);
|
|
18
|
+
}
|
|
4
19
|
const REGION_PIN_CORNER = { x: 1, y: 1 };
|
|
20
|
+
function regionAnchorPinCorner(anchor) {
|
|
21
|
+
if (anchor.pinX !== void 0 && anchor.pinY !== void 0) {
|
|
22
|
+
return { x: anchor.pinX, y: anchor.pinY };
|
|
23
|
+
}
|
|
24
|
+
return REGION_PIN_CORNER;
|
|
25
|
+
}
|
|
5
26
|
function regionPinPoint(region, corner = REGION_PIN_CORNER) {
|
|
6
27
|
return {
|
|
7
28
|
x: region.x + corner.x * region.w,
|
|
8
29
|
y: region.y + corner.y * region.h
|
|
9
30
|
};
|
|
10
31
|
}
|
|
11
|
-
function anchorPagePoint(editor, anchor
|
|
32
|
+
function anchorPagePoint(editor, anchor) {
|
|
12
33
|
switch (anchor.type) {
|
|
13
34
|
case "shape": {
|
|
14
|
-
const
|
|
15
|
-
if (!
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (!bounds) return null;
|
|
22
|
-
return { x: bounds.maxX, y: bounds.minY };
|
|
35
|
+
const shape = editor.getShape(anchor.shapeId);
|
|
36
|
+
if (!shape) return null;
|
|
37
|
+
const transform = editor.getShapePageTransform(shape);
|
|
38
|
+
if (!transform) return null;
|
|
39
|
+
const { point, size } = editor.getShapeGeometry(shape).bounds;
|
|
40
|
+
const spot = anchor.isPrecise ? anchor : getCommentingOptions(editor).impreciseShapeAnchor;
|
|
41
|
+
return Mat.applyToPoint(transform, Vec.Add(point, Vec.MulV(spot, size)));
|
|
23
42
|
}
|
|
24
43
|
case "point":
|
|
25
44
|
return { x: anchor.x, y: anchor.y };
|
|
26
45
|
case "region":
|
|
27
|
-
return regionPinPoint(anchor,
|
|
46
|
+
return regionPinPoint(anchor, regionAnchorPinCorner(anchor));
|
|
28
47
|
case "page":
|
|
29
48
|
return null;
|
|
30
49
|
}
|
|
31
50
|
}
|
|
51
|
+
function commentTargetShapeAt(editor, page) {
|
|
52
|
+
return editor.getShapeAtPoint(page, {
|
|
53
|
+
hitInside: true,
|
|
54
|
+
hitFrameInside: true,
|
|
55
|
+
margin: editor.getHitTestMargin()
|
|
56
|
+
});
|
|
57
|
+
}
|
|
32
58
|
function shapeAnchorAt(editor, shapeId, page, precise) {
|
|
33
|
-
const
|
|
34
|
-
|
|
59
|
+
const shape = editor.getShape(shapeId);
|
|
60
|
+
const bounds = shape && editor.getShapeGeometry(shape).bounds;
|
|
61
|
+
if (!shape || !bounds || bounds.w === 0 || bounds.h === 0) {
|
|
35
62
|
return { type: "shape", shapeId, x: 0.5, y: 0.5, isPrecise: precise };
|
|
36
63
|
}
|
|
64
|
+
const local = editor.getPointInShapeSpace(shape, page);
|
|
37
65
|
return {
|
|
38
66
|
type: "shape",
|
|
39
67
|
shapeId,
|
|
40
|
-
x: (
|
|
41
|
-
y: (
|
|
68
|
+
x: (local.x - bounds.minX) / bounds.w,
|
|
69
|
+
y: (local.y - bounds.minY) / bounds.h,
|
|
42
70
|
isPrecise: precise
|
|
43
71
|
};
|
|
44
72
|
}
|
|
45
|
-
function focusThread(editor, thread
|
|
73
|
+
function focusThread(editor, thread) {
|
|
46
74
|
if (thread.pageId !== editor.getCurrentPageId()) {
|
|
47
75
|
editor.setCurrentPage(thread.pageId);
|
|
48
76
|
}
|
|
49
77
|
openThreadId.set(editor, thread.id);
|
|
50
|
-
const point = anchorPagePoint(editor, thread.anchor
|
|
51
|
-
if (point)
|
|
78
|
+
const point = anchorPagePoint(editor, thread.anchor);
|
|
79
|
+
if (!point) return;
|
|
80
|
+
const offset = commentCenterScreenOffset(editor) / editor.getZoomLevel();
|
|
81
|
+
editor.centerOnPoint({ x: point.x + offset, y: point.y }, { animation: { duration: 200 } });
|
|
82
|
+
}
|
|
83
|
+
const CENTER_MARGIN_PX = 8;
|
|
84
|
+
const THREAD_UI_EXTENT_PX = POPOVER_OFFSET.thread.x + 300;
|
|
85
|
+
function commentCenterScreenOffset(editor) {
|
|
86
|
+
if (!commentsSidebarOpen.get(editor)) return 0;
|
|
87
|
+
const sidebar = editor.getContainer().querySelector(".tlui-cmt-canvas-sidebar");
|
|
88
|
+
if (!sidebar) return 0;
|
|
89
|
+
const viewport = editor.getViewportScreenBounds();
|
|
90
|
+
const sidebarLeft = sidebar.getBoundingClientRect().left - viewport.x;
|
|
91
|
+
if (sidebarLeft >= viewport.w) return 0;
|
|
92
|
+
const pinX = Math.max(
|
|
93
|
+
Math.min(sidebarLeft / 2, sidebarLeft - CENTER_MARGIN_PX - THREAD_UI_EXTENT_PX),
|
|
94
|
+
CENTER_MARGIN_PX
|
|
95
|
+
);
|
|
96
|
+
return viewport.w / 2 - pinX;
|
|
52
97
|
}
|
|
53
98
|
export {
|
|
54
|
-
|
|
99
|
+
IMPRECISE_PIN_INSET_PX,
|
|
55
100
|
REGION_PIN_CORNER,
|
|
56
101
|
anchorPagePoint,
|
|
102
|
+
commentCenterScreenOffset,
|
|
103
|
+
commentTargetShapeAt,
|
|
57
104
|
focusThread,
|
|
105
|
+
impreciseShapePinInset,
|
|
106
|
+
regionAnchorPinCorner,
|
|
58
107
|
regionPinPoint,
|
|
59
108
|
shapeAnchorAt
|
|
60
109
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/canvas/thread-state.ts"],
|
|
4
|
-
"sourcesContent": ["import {
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import {\n\tBoxModel,\n\tEditor,\n\tMat,\n\tTLCommentAnchor,\n\tTLCommentThread,\n\tTLShape,\n\tTLShapeId,\n\tVec,\n\tVecLike,\n} from 'tldraw'\nimport { getCommentingOptions } from './options'\nimport { commentsSidebarOpen, openThreadId } from './state'\nimport { POPOVER_OFFSET } from './thread-view'\n\n/** How far an imprecise shape pin steps inside the shape from its anchor spot, in screen px \u2014\n * most of the marker sits within the shape, with a small overhang past the corner. */\nexport const IMPRECISE_PIN_INSET_PX = 20\n\n/** Imprecise shape pins tuck inside the shape rather than hanging off its edge: the marker\n * extends up-right of its anchor point, so step it toward the shape's centre. Screen px \u2014 the\n * pin is screen-fixed while the shape scales with zoom. Null for anchors that need no inset. */\nexport function impreciseShapePinInset(\n\teditor: Editor,\n\tanchor: TLCommentThread['anchor']\n): { x: number; y: number } | null {\n\tif (anchor.type !== 'shape' || anchor.isPrecise) return null\n\tconst spot = getCommentingOptions(editor).impreciseShapeAnchor\n\tconst inset = {\n\t\tx: Math.sign(0.5 - spot.x) * IMPRECISE_PIN_INSET_PX,\n\t\ty: Math.sign(0.5 - spot.y) * IMPRECISE_PIN_INSET_PX,\n\t}\n\t// The spot is a corner of the shape's own bounds, so which way \"toward the centre\" points turns\n\t// with the shape: without this a rotated shape's pin would step out of the shape, not into it.\n\tconst rotation = editor.getShapePageTransform(anchor.shapeId as TLShapeId)?.rotation() ?? 0\n\treturn rotation === 0 ? inset : Vec.Rot(inset, rotation)\n}\n\n/** The corner a region's pin and composer sit on, as a normalized 0\u20131 offset (bottom-right). Pin\n * position, composer placement, region move, and which corner has no resize handle all derive\n * from it. */\nexport const REGION_PIN_CORNER: VecLike = { x: 1, y: 1 }\n\n/** A region anchor's pin corner: the corner its creating drag released on, when recorded, else\n * {@link REGION_PIN_CORNER}. @internal */\nexport function regionAnchorPinCorner(\n\tanchor: Extract<TLCommentAnchor, { type: 'region' }>\n): VecLike {\n\tif (anchor.pinX !== undefined && anchor.pinY !== undefined) {\n\t\treturn { x: anchor.pinX, y: anchor.pinY }\n\t}\n\treturn REGION_PIN_CORNER\n}\n\n/** The page point of a region's pin corner. */\nexport function regionPinPoint(region: BoxModel, corner: VecLike = REGION_PIN_CORNER): VecLike {\n\treturn {\n\t\tx: region.x + corner.x * region.w,\n\t\ty: region.y + corner.y * region.h,\n\t}\n}\n\n/**\n * Where a thread's pin sits on the page, for each anchor kind. Null hides the pin. For imprecise\n * shape anchors the pin uses the editor's {@link CommentingOptions.impreciseShapeAnchor} (a\n * normalized 0\u20131 spot, top-right by default) rather than the stored `x`/`y`.\n *\n * A shape anchor's `x`/`y` are normalized within the shape's own bounds and resolved through the\n * shape's page transform, so the pin rides every part of that transform \u2014 rotating the shape\n * carries the pin around with it instead of leaving it behind in the bounding box.\n * @public\n */\nexport function anchorPagePoint(\n\teditor: Editor,\n\tanchor: TLCommentAnchor\n): { x: number; y: number } | null {\n\tswitch (anchor.type) {\n\t\tcase 'shape': {\n\t\t\tconst shape = editor.getShape(anchor.shapeId as TLShapeId)\n\t\t\tif (!shape) return null\n\t\t\tconst transform = editor.getShapePageTransform(shape)\n\t\t\tif (!transform) return null\n\t\t\tconst { point, size } = editor.getShapeGeometry(shape).bounds\n\t\t\t// Precise pins sit at their stored x/y; imprecise ones at the editor's configured spot.\n\t\t\tconst spot = anchor.isPrecise ? anchor : getCommentingOptions(editor).impreciseShapeAnchor\n\t\t\treturn Mat.applyToPoint(transform, Vec.Add(point, Vec.MulV(spot, size)))\n\t\t}\n\t\tcase 'point':\n\t\t\treturn { x: anchor.x, y: anchor.y }\n\t\tcase 'region':\n\t\t\treturn regionPinPoint(anchor, regionAnchorPinCorner(anchor))\n\t\tcase 'page':\n\t\t\treturn null\n\t}\n}\n\n/**\n * The shape a comment placed at a page point should anchor to, or undefined for empty canvas.\n *\n * Uses the editor's hit-test margin, the same slack select and hover use. Without it, shapes whose\n * geometry is an open path \u2014 arrows, lines, draw strokes \u2014 are unhittable in practice: their\n * geometry reports a positive distance for every point off the stroke, so a zero margin only\n * matches a pixel-perfect click right on the line.\n *\n * `hitFrameInside` lets a click inside a frame's body anchor to the frame \u2014 without it a frame is\n * hit only on its edge/label (the select-tool convention), so the frame's interior would fall\n * through to a bare point. A child shape under the pointer still wins (children sort above the\n * frame), so only a frame's empty interior anchors the frame.\n *\n * @internal\n */\nexport function commentTargetShapeAt(editor: Editor, page: VecLike): TLShape | undefined {\n\treturn editor.getShapeAtPoint(page, {\n\t\thitInside: true,\n\t\thitFrameInside: true,\n\t\tmargin: editor.getHitTestMargin(),\n\t})\n}\n\n/**\n * A shape anchor for a page point. `x`/`y` are the point's normalized (0\u20131) offset within the\n * shape's own bounds \u2014 taken in the shape's own space, so a pin placed on a rotated shape records\n * the spot it was dropped on rather than a spot in the bounding box. Remembered either way: when\n * `precise` the pin sits at exactly `x`/`y`; otherwise it sits at the consumer's imprecise default\n * (top-right out of the box). Placement gestures get `precise` from the `shouldBePrecise`\n * commenting option (always precise, by default).\n * @public\n */\nexport function shapeAnchorAt(\n\teditor: Editor,\n\tshapeId: TLShapeId,\n\tpage: { x: number; y: number },\n\tprecise: boolean\n): TLCommentAnchor {\n\tconst shape = editor.getShape(shapeId)\n\tconst bounds = shape && editor.getShapeGeometry(shape).bounds\n\tif (!shape || !bounds || bounds.w === 0 || bounds.h === 0) {\n\t\treturn { type: 'shape', shapeId, x: 0.5, y: 0.5, isPrecise: precise }\n\t}\n\tconst local = editor.getPointInShapeSpace(shape, page)\n\treturn {\n\t\ttype: 'shape',\n\t\tshapeId,\n\t\tx: (local.x - bounds.minX) / bounds.w,\n\t\ty: (local.y - bounds.minY) / bounds.h,\n\t\tisPrecise: precise,\n\t}\n}\n\n/** Open a thread and bring it into view \u2014 switch to its page if needed, then center its pin. @public */\nexport function focusThread(editor: Editor, thread: TLCommentThread): void {\n\tif (thread.pageId !== editor.getCurrentPageId()) {\n\t\teditor.setCurrentPage(thread.pageId as any)\n\t}\n\topenThreadId.set(editor, thread.id)\n\tconst point = anchorPagePoint(editor, thread.anchor)\n\tif (!point) return\n\tconst offset = commentCenterScreenOffset(editor) / editor.getZoomLevel()\n\teditor.centerOnPoint({ x: point.x + offset, y: point.y }, { animation: { duration: 200 } })\n}\n\n/** The left inset a nudged pin never crosses, and the gap kept between thread UI and sidebar. */\nconst CENTER_MARGIN_PX = 8\n\n/** How far the open thread UI reaches right of its pin, in screen px: the popover's offset plus\n * the thread panel's fixed width (300px, `.tlui-cmt-thread`). */\nconst THREAD_UI_EXTENT_PX = POPOVER_OFFSET.thread.x + 300\n\n/**\n * How far right of a centered-on pin the true viewport center should sit, in screen px. While the\n * comments sidebar covers the viewport's right edge, centering a pin dead-center puts the thread\n * popover that opens on it under the sidebar \u2014 so centering aims the pin at the middle of the\n * uncovered area instead, nudged further left if the thread UI would still reach the sidebar, but\n * never past the viewport's left edge. Zero when the sidebar is closed or not on screen.\n * @internal\n */\nexport function commentCenterScreenOffset(editor: Editor): number {\n\tif (!commentsSidebarOpen.get(editor)) return 0\n\tconst sidebar = editor.getContainer().querySelector('.tlui-cmt-canvas-sidebar')\n\tif (!sidebar) return 0\n\tconst viewport = editor.getViewportScreenBounds()\n\t// Viewport screen bounds are the container's client rect, so this is container-local.\n\tconst sidebarLeft = sidebar.getBoundingClientRect().left - viewport.x\n\tif (sidebarLeft >= viewport.w) return 0\n\t// Aim the pin at the middle of the uncovered area, nudged left until the thread UI clears the\n\t// sidebar \u2014 but never past the left edge (the edge wins when there's no room for both).\n\tconst pinX = Math.max(\n\t\tMath.min(sidebarLeft / 2, sidebarLeft - CENTER_MARGIN_PX - THREAD_UI_EXTENT_PX),\n\t\tCENTER_MARGIN_PX\n\t)\n\treturn viewport.w / 2 - pinX\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|