@tldraw/commenting 0.0.0-bootstrap → 5.3.0-canary.04044ed9e96d
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 +1152 -350
- package/dist-cjs/canvas/anchor-lifecycle.js +148 -0
- package/dist-cjs/canvas/anchor-lifecycle.js.map +7 -0
- package/dist-cjs/{ui/avatar.js → canvas/canvas-events.js} +14 -22
- package/dist-cjs/canvas/canvas-events.js.map +7 -0
- package/dist-cjs/canvas/cluster-badge.js +121 -0
- package/dist-cjs/canvas/cluster-badge.js.map +7 -0
- package/dist-cjs/canvas/cluster-fade.js +85 -0
- package/dist-cjs/canvas/cluster-fade.js.map +7 -0
- package/dist-cjs/canvas/cluster-input.js +53 -3
- package/dist-cjs/canvas/cluster-input.js.map +2 -2
- package/dist-cjs/canvas/cluster-model.js +252 -0
- package/dist-cjs/canvas/cluster-model.js.map +7 -0
- 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 +152 -0
- package/dist-cjs/canvas/comment-mutations.js.map +7 -0
- package/dist-cjs/canvas/comment-reactions.js +160 -0
- package/dist-cjs/canvas/comment-reactions.js.map +7 -0
- package/dist-cjs/canvas/comment-render.js +9 -6
- 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 +63 -14
- package/dist-cjs/canvas/comment-tool.js.map +2 -2
- package/dist-cjs/canvas/comments-filter-menu.js +17 -37
- package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
- package/dist-cjs/canvas/comments-overflow-menu.js +25 -22
- package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
- package/dist-cjs/canvas/comments-overlay.js +151 -989
- package/dist-cjs/canvas/comments-overlay.js.map +3 -3
- package/dist-cjs/canvas/comments-sidebar.js +49 -41
- package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
- package/dist-cjs/canvas/comments-visibility-toggle.js +45 -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 +28 -3
- package/dist-cjs/canvas/hooks.js.map +2 -2
- package/dist-cjs/canvas/license.js.map +1 -1
- package/dist-cjs/canvas/mobile-placement.js +91 -0
- package/dist-cjs/canvas/mobile-placement.js.map +7 -0
- package/dist-cjs/canvas/options.js +57 -2
- package/dist-cjs/canvas/options.js.map +2 -2
- package/dist-cjs/canvas/pending-composer.js +134 -0
- package/dist-cjs/canvas/pending-composer.js.map +7 -0
- package/dist-cjs/canvas/pin-stacking.js +81 -0
- package/dist-cjs/canvas/pin-stacking.js.map +7 -0
- package/dist-cjs/canvas/region-box.js +113 -0
- package/dist-cjs/canvas/region-box.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-pin.js +304 -0
- package/dist-cjs/canvas/thread-pin.js.map +7 -0
- package/dist-cjs/canvas/thread-preview.js +177 -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 +82 -21
- package/dist-cjs/canvas/thread-state.js.map +2 -2
- package/dist-cjs/canvas/thread-view.js +450 -0
- package/dist-cjs/canvas/thread-view.js.map +7 -0
- package/dist-cjs/clustering/computeClusterTable.js +6 -6
- package/dist-cjs/clustering/computeClusterTable.js.map +2 -2
- package/dist-cjs/clustering/replay.js +89 -22
- package/dist-cjs/clustering/replay.js.map +3 -3
- package/dist-cjs/clustering/runtime.js +126 -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 +904 -353
- package/dist-cjs/index.js +52 -28
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/ui/byline.js +10 -6
- 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 +124 -39
- package/dist-cjs/ui/comment-composer.js.map +2 -2
- package/dist-cjs/ui/comment-pin.js +11 -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 +39 -40
- 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 +16 -1
- package/dist-cjs/ui/format-time.js.map +2 -2
- package/dist-cjs/ui/icons.js +115 -0
- package/dist-cjs/ui/icons.js.map +7 -0
- package/dist-cjs/ui/reaction-picker.js +71 -0
- package/dist-cjs/ui/reaction-picker.js.map +7 -0
- package/dist-cjs/ui/reaction.js +58 -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 +13 -1
- package/dist-cjs/ui/send-button.js.map +2 -2
- package/dist-cjs/ui/{comment-text.js → visual-viewport.js} +12 -9
- package/dist-cjs/ui/visual-viewport.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/canvas-events.mjs +15 -0
- package/dist-esm/canvas/canvas-events.mjs.map +7 -0
- package/dist-esm/canvas/cluster-badge.mjs +106 -0
- package/dist-esm/canvas/cluster-badge.mjs.map +7 -0
- package/dist-esm/canvas/cluster-fade.mjs +65 -0
- package/dist-esm/canvas/cluster-fade.mjs.map +7 -0
- package/dist-esm/canvas/cluster-input.mjs +54 -4
- package/dist-esm/canvas/cluster-input.mjs.map +2 -2
- package/dist-esm/canvas/cluster-model.mjs +236 -0
- package/dist-esm/canvas/cluster-model.mjs.map +7 -0
- 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 +132 -0
- package/dist-esm/canvas/comment-mutations.mjs.map +7 -0
- package/dist-esm/canvas/comment-reactions.mjs +145 -0
- package/dist-esm/canvas/comment-reactions.mjs.map +7 -0
- package/dist-esm/canvas/comment-render.mjs +10 -7
- 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 +66 -17
- package/dist-esm/canvas/comment-tool.mjs.map +2 -2
- package/dist-esm/canvas/comments-filter-menu.mjs +17 -36
- package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
- package/dist-esm/canvas/comments-overflow-menu.mjs +27 -22
- package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
- package/dist-esm/canvas/comments-overlay.mjs +157 -1010
- package/dist-esm/canvas/comments-overlay.mjs.map +3 -3
- package/dist-esm/canvas/comments-sidebar.mjs +53 -46
- package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
- package/dist-esm/canvas/comments-visibility-toggle.mjs +25 -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 +35 -5
- package/dist-esm/canvas/hooks.mjs.map +2 -2
- package/dist-esm/canvas/license.mjs.map +1 -1
- package/dist-esm/canvas/mobile-placement.mjs +71 -0
- package/dist-esm/canvas/mobile-placement.mjs.map +7 -0
- package/dist-esm/canvas/options.mjs +59 -3
- package/dist-esm/canvas/options.mjs.map +2 -2
- package/dist-esm/canvas/pending-composer.mjs +126 -0
- package/dist-esm/canvas/pending-composer.mjs.map +7 -0
- package/dist-esm/canvas/pin-stacking.mjs +61 -0
- package/dist-esm/canvas/pin-stacking.mjs.map +7 -0
- package/dist-esm/canvas/region-box.mjs +93 -0
- package/dist-esm/canvas/region-box.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-pin.mjs +310 -0
- package/dist-esm/canvas/thread-pin.mjs.map +7 -0
- package/dist-esm/canvas/thread-preview.mjs +163 -0
- package/dist-esm/canvas/thread-preview.mjs.map +7 -0
- package/dist-esm/canvas/thread-stack.mjs +189 -0
- package/dist-esm/canvas/thread-stack.mjs.map +7 -0
- package/dist-esm/canvas/thread-state.mjs +86 -22
- package/dist-esm/canvas/thread-state.mjs.map +2 -2
- package/dist-esm/canvas/thread-view.mjs +461 -0
- package/dist-esm/canvas/thread-view.mjs.map +7 -0
- package/dist-esm/clustering/computeClusterTable.mjs +6 -6
- package/dist-esm/clustering/computeClusterTable.mjs.map +2 -2
- package/dist-esm/clustering/replay.mjs +89 -22
- package/dist-esm/clustering/replay.mjs.map +3 -3
- package/dist-esm/clustering/runtime.mjs +126 -7
- package/dist-esm/clustering/runtime.mjs.map +2 -2
- package/dist-esm/index.d.mts +904 -353
- package/dist-esm/index.mjs +102 -43
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/ui/byline.mjs +11 -7
- 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 +135 -39
- package/dist-esm/ui/comment-composer.mjs.map +2 -2
- package/dist-esm/ui/comment-pin.mjs +11 -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 +51 -50
- 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 +16 -1
- package/dist-esm/ui/format-time.mjs.map +2 -2
- package/dist-esm/ui/icons.mjs +95 -0
- package/dist-esm/ui/icons.mjs.map +7 -0
- package/dist-esm/ui/reaction-picker.mjs +59 -0
- package/dist-esm/ui/reaction-picker.mjs.map +7 -0
- package/dist-esm/ui/reaction.mjs +59 -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 +13 -1
- package/dist-esm/ui/send-button.mjs.map +2 -2
- package/dist-esm/ui/visual-viewport.mjs +12 -0
- package/dist-esm/ui/visual-viewport.mjs.map +7 -0
- package/package.json +6 -6
- package/src/canvas/anchor-lifecycle.test.ts +290 -0
- package/src/canvas/anchor-lifecycle.ts +191 -0
- package/src/canvas/canvas-events.ts +18 -0
- package/src/canvas/canvas.css +464 -55
- package/src/canvas/cluster-badge.tsx +133 -0
- package/src/canvas/cluster-fade.ts +102 -0
- package/src/canvas/cluster-input.test.ts +178 -36
- package/src/canvas/cluster-input.ts +95 -9
- package/src/canvas/cluster-model.ts +378 -0
- 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 +452 -0
- package/src/canvas/comment-mutations.ts +299 -0
- package/src/canvas/comment-reactions.test.ts +154 -0
- package/src/canvas/comment-reactions.tsx +246 -0
- package/src/canvas/comment-render.test.ts +1 -1
- package/src/canvas/comment-render.ts +17 -13
- package/src/canvas/comment-store.ts +68 -33
- package/src/canvas/comment-tool.test.ts +162 -0
- package/src/canvas/comment-tool.tsx +103 -25
- package/src/canvas/comments-filter-menu.tsx +20 -36
- package/src/canvas/comments-overflow-menu.tsx +17 -19
- package/src/canvas/comments-overlay.tsx +252 -1369
- package/src/canvas/comments-sidebar.test.ts +45 -0
- package/src/canvas/comments-sidebar.tsx +112 -73
- package/src/canvas/comments-visibility-toggle.tsx +25 -0
- package/src/canvas/context.ts +48 -0
- package/src/canvas/hooks.test.ts +94 -0
- package/src/canvas/hooks.ts +66 -11
- package/src/canvas/license.ts +1 -1
- package/src/canvas/mobile-placement.ts +115 -0
- package/src/canvas/options.test.ts +176 -4
- package/src/canvas/options.ts +276 -18
- package/src/canvas/pending-composer.tsx +157 -0
- package/src/canvas/pin-stacking.test.ts +224 -0
- package/src/canvas/pin-stacking.ts +103 -0
- package/src/canvas/region-box.tsx +124 -0
- package/src/canvas/sidebar-filters.ts +4 -3
- package/src/canvas/state.ts +88 -39
- package/src/canvas/thread-pin.tsx +418 -0
- package/src/canvas/thread-preview.test.ts +91 -0
- package/src/canvas/thread-preview.tsx +294 -0
- package/src/canvas/thread-stack.tsx +242 -0
- package/src/canvas/thread-state.test.ts +260 -0
- package/src/canvas/thread-state.ts +171 -37
- package/src/canvas/thread-view.test.ts +72 -0
- package/src/canvas/thread-view.tsx +614 -0
- package/src/clustering/computeClusterTable.test.ts +24 -23
- package/src/clustering/computeClusterTable.ts +17 -8
- package/src/clustering/replay.test.ts +0 -7
- package/src/clustering/replay.ts +131 -32
- package/src/clustering/runtime.test.ts +267 -13
- package/src/clustering/runtime.ts +176 -19
- package/src/clustering/schedule.test.ts +0 -6
- package/src/clustering/screen-offsets.test.ts +171 -0
- package/src/clustering/types.ts +19 -9
- package/src/index.ts +91 -42
- package/src/ui/byline.tsx +21 -9
- package/src/ui/comment-card.tsx +20 -10
- package/src/ui/comment-composer.tsx +178 -56
- package/src/ui/comment-pin.tsx +13 -2
- package/src/ui/comment-thread.tsx +13 -6
- package/src/ui/comments-list.test.ts +31 -0
- package/src/ui/comments-list.tsx +82 -47
- package/src/ui/comments.css +577 -294
- 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.test.ts +69 -0
- package/src/ui/format-time.ts +27 -2
- package/src/ui/icons.tsx +116 -0
- package/src/ui/reaction-picker.tsx +102 -0
- package/src/ui/reaction.tsx +125 -6
- package/src/ui/reactions.tsx +69 -10
- package/src/ui/reply-count.ts +16 -0
- package/src/ui/send-button.tsx +13 -3
- package/src/ui/visual-viewport.test.ts +36 -0
- package/src/ui/visual-viewport.ts +27 -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.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,294 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, type CSSProperties } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Editor,
|
|
4
|
+
EditorAtom,
|
|
5
|
+
EditorPortal,
|
|
6
|
+
TLComment,
|
|
7
|
+
TLCommentThread,
|
|
8
|
+
usePassThroughWheelEvents,
|
|
9
|
+
useTranslation,
|
|
10
|
+
useValue,
|
|
11
|
+
} from 'tldraw'
|
|
12
|
+
import { CommentCard } from '../ui/comment-card'
|
|
13
|
+
import { replyCountLabel } from '../ui/reply-count'
|
|
14
|
+
import { type CommentingContext } from './context'
|
|
15
|
+
import { useComments } from './hooks'
|
|
16
|
+
import { useCommentingOptions } from './options'
|
|
17
|
+
import { openStackId, openThreadId } from './state'
|
|
18
|
+
import { POPOVER_OFFSET, toCardProps, useResolveName } from './thread-view'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Hover previews for every canvas marker — a single pin, a coincident stack, or a cluster badge.
|
|
22
|
+
* Hovering shows the thread(s) behind the marker as cards; the marker's own click is unchanged.
|
|
23
|
+
*
|
|
24
|
+
* The panel is live, not a passive tooltip: the pointer can travel into it and click a card to open
|
|
25
|
+
* that thread. Two pieces make that work — the close delay survives the trip across the gap, and
|
|
26
|
+
* the panel carries an invisible bridge over it (see `.tlui-cmt-canvas-preview::before`).
|
|
27
|
+
*
|
|
28
|
+
* Opening a thread from a card needs nothing more than setting `openThreadId`, even for one folded
|
|
29
|
+
* inside a badge: `collectClusterLeaves` skips the open thread, so it drops out and renders itself.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** How long the pointer must rest on a marker before its preview appears. */
|
|
33
|
+
const PREVIEW_OPEN_DELAY_MS = 180
|
|
34
|
+
/**
|
|
35
|
+
* Grace period after the pointer leaves the marker *or* the panel — long enough to cross the gap
|
|
36
|
+
* between them, which the bridge element covers geometrically.
|
|
37
|
+
*/
|
|
38
|
+
const PREVIEW_CLOSE_DELAY_MS = 220
|
|
39
|
+
/** Cards shown before the panel falls back to a "+N more" line. */
|
|
40
|
+
const PREVIEW_MAX_THREADS = 5
|
|
41
|
+
|
|
42
|
+
/** A thread paired with the comment that opens it — one card of a preview. */
|
|
43
|
+
export interface ThreadPreviewCard {
|
|
44
|
+
thread: TLCommentThread
|
|
45
|
+
first: TLComment
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The cards a marker's preview will show, and how many threads it will summarise as "+N more".
|
|
50
|
+
*
|
|
51
|
+
* A thread can exist before its opening comment does — a collaborator's, mid-sync. Those are
|
|
52
|
+
* dropped rather than rendered blank, and don't count toward the overflow tally.
|
|
53
|
+
*/
|
|
54
|
+
export function selectPreviewCards(
|
|
55
|
+
threads: readonly TLCommentThread[],
|
|
56
|
+
firstCommentOf: (thread: TLCommentThread) => TLComment | undefined,
|
|
57
|
+
max = PREVIEW_MAX_THREADS
|
|
58
|
+
): { cards: ThreadPreviewCard[]; overflow: number } {
|
|
59
|
+
const readable: ThreadPreviewCard[] = []
|
|
60
|
+
for (const thread of threads) {
|
|
61
|
+
const first = firstCommentOf(thread)
|
|
62
|
+
if (first) readable.push({ thread, first })
|
|
63
|
+
}
|
|
64
|
+
return { cards: readable.slice(0, max), overflow: Math.max(0, readable.length - max) }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* What a marker's click opens, which is what its preview imitates — the panel's surface, its
|
|
69
|
+
* width, and how it lays a comment out.
|
|
70
|
+
*
|
|
71
|
+
* `'thread'` previews a single pin: a thread panel without the header. `'list'` previews a
|
|
72
|
+
* coincident stack or a cluster: one card per thread, like the stack list.
|
|
73
|
+
*/
|
|
74
|
+
export type ThreadPreviewVariant = 'thread' | 'list'
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Which marker's preview is showing, or null. One atom for the whole layer, so previews are
|
|
78
|
+
* mutually exclusive: the close delay can leave an outgoing marker's timer pending when the next
|
|
79
|
+
* opens, and per-component state would briefly show both.
|
|
80
|
+
*/
|
|
81
|
+
const hoveredMarkerId = new EditorAtom<string | null>('commentHoveredMarkerId', () => null)
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Hover state for one marker. Returns whether its preview should render, plus the pointer handlers
|
|
85
|
+
* to spread onto the marker element. `markerId` must be stable and unique per marker across the
|
|
86
|
+
* layer — prefix by kind, or a stack collides with its oldest member's own pin.
|
|
87
|
+
*/
|
|
88
|
+
export function useMarkerPreview(editor: Editor, markerId: string) {
|
|
89
|
+
const openTimer = useRef(0)
|
|
90
|
+
const closeTimer = useRef(0)
|
|
91
|
+
|
|
92
|
+
// Previews are a resting-state affordance. While a thread or stack list is open, that view is
|
|
93
|
+
// the thing being read — a preview floating over it would just compete with it.
|
|
94
|
+
const suppressed = useValue(
|
|
95
|
+
'marker preview suppressed',
|
|
96
|
+
() => openThreadId.get(editor) !== null || openStackId.get(editor) !== null,
|
|
97
|
+
[editor]
|
|
98
|
+
)
|
|
99
|
+
const shown = useValue('marker preview shown', () => hoveredMarkerId.get(editor) === markerId, [
|
|
100
|
+
editor,
|
|
101
|
+
markerId,
|
|
102
|
+
])
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
const open = openTimer
|
|
106
|
+
const close = closeTimer
|
|
107
|
+
return () => {
|
|
108
|
+
window.clearTimeout(open.current)
|
|
109
|
+
window.clearTimeout(close.current)
|
|
110
|
+
// Don't strand the atom on a marker that has unmounted — zoomed into a cluster, deleted,
|
|
111
|
+
// or folded away — or no other marker's preview could ever show.
|
|
112
|
+
if (hoveredMarkerId.get(editor) === markerId) hoveredMarkerId.set(editor, null)
|
|
113
|
+
}
|
|
114
|
+
}, [editor, markerId])
|
|
115
|
+
|
|
116
|
+
// Suppression can start *while* a preview is up: clicking the hovered pin opens its thread.
|
|
117
|
+
// Retract the preview rather than leaving it stranded behind the popover that just opened.
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (suppressed && hoveredMarkerId.get(editor) === markerId) {
|
|
120
|
+
hoveredMarkerId.set(editor, null)
|
|
121
|
+
}
|
|
122
|
+
}, [suppressed, editor, markerId])
|
|
123
|
+
|
|
124
|
+
const onPointerEnter = () => {
|
|
125
|
+
window.clearTimeout(openTimer.current)
|
|
126
|
+
window.clearTimeout(closeTimer.current)
|
|
127
|
+
if (suppressed) return
|
|
128
|
+
openTimer.current = window.setTimeout(
|
|
129
|
+
() => hoveredMarkerId.set(editor, markerId),
|
|
130
|
+
PREVIEW_OPEN_DELAY_MS
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const onPointerLeave = () => {
|
|
135
|
+
window.clearTimeout(openTimer.current)
|
|
136
|
+
window.clearTimeout(closeTimer.current)
|
|
137
|
+
closeTimer.current = window.setTimeout(() => {
|
|
138
|
+
if (hoveredMarkerId.get(editor) === markerId) hoveredMarkerId.set(editor, null)
|
|
139
|
+
}, PREVIEW_CLOSE_DELAY_MS)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
previewShown: shown && !suppressed,
|
|
144
|
+
previewHandlers: { onPointerEnter, onPointerLeave },
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The hover panel: each thread's opening comment as a read-only card, capped with a "+N more" line.
|
|
150
|
+
* Mounted only while hovering, so its store subscription costs nothing at rest.
|
|
151
|
+
*/
|
|
152
|
+
export function ThreadPreview({
|
|
153
|
+
editor,
|
|
154
|
+
threads,
|
|
155
|
+
variant,
|
|
156
|
+
point,
|
|
157
|
+
onSelectThread,
|
|
158
|
+
onPointerEnter,
|
|
159
|
+
onPointerLeave,
|
|
160
|
+
...props
|
|
161
|
+
}: Pick<CommentingContext, 'currentUserId' | 'resolveAuthor'> & {
|
|
162
|
+
editor: Editor
|
|
163
|
+
/** The marker's threads, in the order they should read (oldest first). */
|
|
164
|
+
threads: readonly TLCommentThread[]
|
|
165
|
+
/** What the marker's click opens, which this panel imitates. */
|
|
166
|
+
variant: ThreadPreviewVariant
|
|
167
|
+
/** The marker's anchor point in viewport space — the same origin its popover is placed from. */
|
|
168
|
+
point: { x: number; y: number }
|
|
169
|
+
/** Open a thread from its card. Omit to leave the cards inert. */
|
|
170
|
+
onSelectThread?(thread: TLCommentThread): void
|
|
171
|
+
/** The owning marker's hover handlers, so the panel counts as part of its hover region. */
|
|
172
|
+
onPointerEnter?(): void
|
|
173
|
+
onPointerLeave?(): void
|
|
174
|
+
}) {
|
|
175
|
+
const options = useCommentingOptions()
|
|
176
|
+
const msg = useTranslation()
|
|
177
|
+
const comments = useComments(editor)
|
|
178
|
+
const resolveName = useResolveName(props.resolveAuthor)
|
|
179
|
+
|
|
180
|
+
// The panel floats over the canvas and scrolls nothing of its own, so a wheel on it should zoom
|
|
181
|
+
// and pan the canvas underneath — like every other tldraw panel, and like the popover it
|
|
182
|
+
// previews. Without this the canvas would freeze wherever the preview happened to be.
|
|
183
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
184
|
+
usePassThroughWheelEvents(ref)
|
|
185
|
+
|
|
186
|
+
// Each thread's opening comment and its total comment count. `useComments` is oldest-first, so
|
|
187
|
+
// the first hit per thread is that thread's first comment. One pass over every comment beats a
|
|
188
|
+
// per-thread hook — the thread count here is driven by cluster size, which has no fixed bound.
|
|
189
|
+
const { firstByThread, countByThread } = useMemo(() => {
|
|
190
|
+
const first = new Map<string, TLComment>()
|
|
191
|
+
const count = new Map<string, number>()
|
|
192
|
+
for (const comment of comments) {
|
|
193
|
+
if (!first.has(comment.threadId)) first.set(comment.threadId, comment)
|
|
194
|
+
count.set(comment.threadId, (count.get(comment.threadId) ?? 0) + 1)
|
|
195
|
+
}
|
|
196
|
+
return { firstByThread: first, countByThread: count }
|
|
197
|
+
}, [comments])
|
|
198
|
+
|
|
199
|
+
const { cards, overflow } = useMemo(
|
|
200
|
+
() => selectPreviewCards(threads, (thread) => firstByThread.get(thread.id)),
|
|
201
|
+
[threads, firstByThread]
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
// Nothing readable yet — render no panel rather than an empty one. It matters most for the
|
|
205
|
+
// thread variant, where the panel *is* the card's surface: an empty one would paint as a blank
|
|
206
|
+
// box floating on the canvas.
|
|
207
|
+
if (cards.length === 0) return null
|
|
208
|
+
|
|
209
|
+
// Placed at the same origin as the popover this previews, so the two only differ by what their
|
|
210
|
+
// stylesheets do — see `.tlui-cmt-canvas-preview__panel--thread` for the header compensation.
|
|
211
|
+
const offset = POPOVER_OFFSET[variant]
|
|
212
|
+
// A thread preview is one comment on the panel itself, so the panel carries the hover and the
|
|
213
|
+
// click. A list preview puts each thread on its own card, as the stack list does.
|
|
214
|
+
const isThread = variant === 'thread'
|
|
215
|
+
const panelClass = [
|
|
216
|
+
'tlui-cmt-canvas-preview__panel',
|
|
217
|
+
`tlui-cmt-canvas-preview__panel--${variant}`,
|
|
218
|
+
isThread && onSelectThread ? 'tlui-cmt-canvas-preview__panel--selectable' : '',
|
|
219
|
+
]
|
|
220
|
+
.filter(Boolean)
|
|
221
|
+
.join(' ')
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<EditorPortal>
|
|
225
|
+
{/* The root is the hover region — it carries the bridge back to the marker — while the panel
|
|
226
|
+
inside it is the visible surface. Keeping them apart is what lets the surface light up on
|
|
227
|
+
its own hover without the bridge (which reaches back over the marker) lighting it up too. */}
|
|
228
|
+
<div
|
|
229
|
+
ref={ref}
|
|
230
|
+
className={`tlui-cmt-canvas-preview tlui-cmt-canvas-preview--${variant}`}
|
|
231
|
+
style={
|
|
232
|
+
{
|
|
233
|
+
left: point.x + offset.x,
|
|
234
|
+
top: point.y + offset.y,
|
|
235
|
+
// The stylesheet sizes the hover bridge against this, so the gap it spans follows
|
|
236
|
+
// the offset rather than being restated as a second magic number.
|
|
237
|
+
'--tlui-cmt-preview-offset': `${offset.x}px`,
|
|
238
|
+
} as CSSProperties
|
|
239
|
+
}
|
|
240
|
+
onPointerEnter={onPointerEnter}
|
|
241
|
+
onPointerLeave={onPointerLeave}
|
|
242
|
+
// The panel sits over the canvas; a press on it is not a canvas press.
|
|
243
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
244
|
+
>
|
|
245
|
+
<div className={panelClass}>
|
|
246
|
+
{cards.map(({ thread, first }) => {
|
|
247
|
+
// The preview shows only the opening comment, so its reply count is what tells the
|
|
248
|
+
// reader the thread continues past what they see.
|
|
249
|
+
const replies = replyCountLabel(msg, (countByThread.get(thread.id) ?? 1) - 1)
|
|
250
|
+
return (
|
|
251
|
+
<div
|
|
252
|
+
key={thread.id}
|
|
253
|
+
className={
|
|
254
|
+
isThread
|
|
255
|
+
? undefined
|
|
256
|
+
: onSelectThread
|
|
257
|
+
? 'tlui-cmt-preview-card tlui-cmt-preview-card--selectable'
|
|
258
|
+
: 'tlui-cmt-preview-card'
|
|
259
|
+
}
|
|
260
|
+
onClick={
|
|
261
|
+
onSelectThread
|
|
262
|
+
? (e) => {
|
|
263
|
+
e.stopPropagation()
|
|
264
|
+
onSelectThread(thread)
|
|
265
|
+
}
|
|
266
|
+
: undefined
|
|
267
|
+
}
|
|
268
|
+
>
|
|
269
|
+
<CommentCard
|
|
270
|
+
{...toCardProps(first, props, options.components, resolveName)}
|
|
271
|
+
footer={
|
|
272
|
+
replies ? <span className="tlui-cmt-card__replies">{replies}</span> : undefined
|
|
273
|
+
}
|
|
274
|
+
/>
|
|
275
|
+
</div>
|
|
276
|
+
)
|
|
277
|
+
})}
|
|
278
|
+
{overflow > 0 && (
|
|
279
|
+
<div className="tlui-cmt-preview-more">
|
|
280
|
+
{msg('comments.preview-more').replace('{count}', String(overflow))}
|
|
281
|
+
</div>
|
|
282
|
+
)}
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
</EditorPortal>
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Order a marker's threads for reading: oldest first, id as the tiebreak so it's stable. */
|
|
290
|
+
export function sortThreadsForPreview(
|
|
291
|
+
threads: readonly TLCommentThread[]
|
|
292
|
+
): readonly TLCommentThread[] {
|
|
293
|
+
return [...threads].sort((a, b) => a.createdAt - b.createdAt || (a.id < b.id ? -1 : 1))
|
|
294
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { type MouseEvent as ReactMouseEvent, memo, useEffect, useRef } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Editor,
|
|
4
|
+
TLCommentThread,
|
|
5
|
+
usePassThroughWheelEvents,
|
|
6
|
+
useTranslation,
|
|
7
|
+
useValue,
|
|
8
|
+
} from 'tldraw'
|
|
9
|
+
import { CommentCard } from '../ui/comment-card'
|
|
10
|
+
import { CountBadge } from '../ui/count-badge'
|
|
11
|
+
import { UNKNOWN_AUTHOR } from './comment-render'
|
|
12
|
+
import { type CommentingContext } from './context'
|
|
13
|
+
import { useThreadComments } from './hooks'
|
|
14
|
+
import { useCommentingOptions } from './options'
|
|
15
|
+
import { pinStackKey } from './pin-stacking'
|
|
16
|
+
import { openStackId, openThreadId } from './state'
|
|
17
|
+
import { ThreadPreview, useMarkerPreview } from './thread-preview'
|
|
18
|
+
import { anchorPagePoint, impreciseShapePinInset } from './thread-state'
|
|
19
|
+
import {
|
|
20
|
+
POPOVER_OFFSET,
|
|
21
|
+
ThreadPopover,
|
|
22
|
+
ThreadView,
|
|
23
|
+
toCardProps,
|
|
24
|
+
useResolveName,
|
|
25
|
+
} from './thread-view'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The pin for threads whose anchors resolve to the same page point — pins zooming can never
|
|
29
|
+
* separate, so they share one count badge. Clicking it lists each thread as a card; clicking a card
|
|
30
|
+
* expands that thread in place, via the single open-thread state.
|
|
31
|
+
*/
|
|
32
|
+
export const ThreadStackPin = memo(function ThreadStackPin({
|
|
33
|
+
editor,
|
|
34
|
+
threads,
|
|
35
|
+
...props
|
|
36
|
+
}: CommentingContext & {
|
|
37
|
+
editor: Editor
|
|
38
|
+
/** The stack's threads, oldest first. All resolve to the same anchor point. */
|
|
39
|
+
threads: readonly TLCommentThread[]
|
|
40
|
+
}) {
|
|
41
|
+
const msg = useTranslation()
|
|
42
|
+
const badgeRef = useRef<HTMLButtonElement>(null)
|
|
43
|
+
// The badge takes pointer events (to open on click), so wheel input over it would otherwise be
|
|
44
|
+
// swallowed instead of zooming — pass it through to the canvas, as the pin and cluster badge do.
|
|
45
|
+
usePassThroughWheelEvents(badgeRef)
|
|
46
|
+
// Hovering the badge previews its threads; clicking still opens them as the interactive list.
|
|
47
|
+
// The preview is what makes the badge legible before you commit to opening it.
|
|
48
|
+
const { previewShown, previewHandlers } = useMarkerPreview(editor, `stack:${threads[0].id}`)
|
|
49
|
+
// The list stays open while a member thread is expanded, so Escape steps back: expanded thread ->
|
|
50
|
+
// card list -> closed. Held in editor state because this pin remounts as its owning render path
|
|
51
|
+
// changes, and keyed by the coincident page point so the open state survives losing a member.
|
|
52
|
+
const stackId = useValue(
|
|
53
|
+
'stack id',
|
|
54
|
+
() => {
|
|
55
|
+
const pagePoint = anchorPagePoint(editor, threads[0].anchor)
|
|
56
|
+
return pagePoint ? pinStackKey(pagePoint) : threads[0].id
|
|
57
|
+
},
|
|
58
|
+
[editor, threads]
|
|
59
|
+
)
|
|
60
|
+
const listOpen = useValue('stack list open', () => openStackId.get(editor) === stackId, [
|
|
61
|
+
editor,
|
|
62
|
+
stackId,
|
|
63
|
+
])
|
|
64
|
+
const openId = useValue('open thread id', () => openThreadId.get(editor), [editor])
|
|
65
|
+
const openMember = threads.find((thread) => thread.id === openId)
|
|
66
|
+
const open = listOpen || openMember !== undefined
|
|
67
|
+
|
|
68
|
+
const point = useValue(
|
|
69
|
+
'stack point',
|
|
70
|
+
() => {
|
|
71
|
+
const first = threads[0]
|
|
72
|
+
if (first.pageId !== editor.getCurrentPageId()) return null
|
|
73
|
+
// The badge hangs off its anchor point bottom-left like a pin, and applies the same imprecise-shape
|
|
74
|
+
// inset the pins it stands in for would (see ThreadPin) — otherwise the marker would snap from tucked
|
|
75
|
+
// inside the shape to the raw corner the moment a second comment turns a pin into a stack.
|
|
76
|
+
const pagePoint = anchorPagePoint(editor, first.anchor)
|
|
77
|
+
if (!pagePoint) return null
|
|
78
|
+
const viewportPoint = editor.pageToViewport(pagePoint)
|
|
79
|
+
const inset = impreciseShapePinInset(editor, first.anchor)
|
|
80
|
+
return inset ? { x: viewportPoint.x + inset.x, y: viewportPoint.y + inset.y } : viewportPoint
|
|
81
|
+
},
|
|
82
|
+
[editor, threads]
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
// Clicking outside the popover (and off the badge) closes the whole stack — mirrors the
|
|
86
|
+
// single pin's dismiss. Capture phase + class checks, since the popover portals elsewhere.
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (!open) return
|
|
89
|
+
const onPointerDown = (e: PointerEvent) => {
|
|
90
|
+
const target = e.target as HTMLElement | null
|
|
91
|
+
if (!target) return
|
|
92
|
+
if (target.closest('.tlui-cmt-canvas-popover')) return
|
|
93
|
+
const badge = badgeRef.current
|
|
94
|
+
if (badge && badge.contains(target)) return
|
|
95
|
+
// A click inside a menu/popover layered above us belongs to that layer; defer to its
|
|
96
|
+
// own dismissal instead of closing the stack out from under it.
|
|
97
|
+
if (
|
|
98
|
+
target.closest('.tlui-menu, [data-radix-popper-content-wrapper], .tlui-cmt-mention-popup')
|
|
99
|
+
)
|
|
100
|
+
return
|
|
101
|
+
openStackId.set(editor, null)
|
|
102
|
+
openThreadId.set(editor, null)
|
|
103
|
+
}
|
|
104
|
+
document.addEventListener('pointerdown', onPointerDown, true)
|
|
105
|
+
return () => document.removeEventListener('pointerdown', onPointerDown, true)
|
|
106
|
+
}, [open, editor])
|
|
107
|
+
|
|
108
|
+
// Escape with only the card list showing closes it. When a member thread is expanded, the
|
|
109
|
+
// layer's Escape handler collapses that first and marks the event consumed — stepping back
|
|
110
|
+
// to the list rather than closing everything at once.
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (!listOpen) return
|
|
113
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
114
|
+
if (e.key !== 'Escape' || e.defaultPrevented) return
|
|
115
|
+
openStackId.set(editor, null)
|
|
116
|
+
}
|
|
117
|
+
document.addEventListener('keydown', onKeyDown, true)
|
|
118
|
+
return () => document.removeEventListener('keydown', onKeyDown, true)
|
|
119
|
+
}, [listOpen, editor])
|
|
120
|
+
|
|
121
|
+
if (!point) return null
|
|
122
|
+
|
|
123
|
+
const toggle = () => {
|
|
124
|
+
if (open) {
|
|
125
|
+
openStackId.set(editor, null)
|
|
126
|
+
openThreadId.set(editor, null)
|
|
127
|
+
} else {
|
|
128
|
+
openStackId.set(editor, stackId)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// The expanded thread gains a header that pushes its first comment down from where the hover preview
|
|
133
|
+
// showed it. When that thread is the list's first, lift the whole list so its "You" holds position
|
|
134
|
+
// across hover -> open. Only the first entry — lifting can't also hold a lower thread's neighbours.
|
|
135
|
+
const liftForHeader = threads[0]?.id === openId ? 42 : 0
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<>
|
|
139
|
+
<div className="tlui-cmt-canvas-pin" style={{ left: point.x, top: point.y }}>
|
|
140
|
+
<button
|
|
141
|
+
ref={badgeRef}
|
|
142
|
+
type="button"
|
|
143
|
+
className="tlui-cmt-button tlui-cmt-canvas-stack-badge"
|
|
144
|
+
aria-label={msg('comments.stack-label').replace('{count}', String(threads.length))}
|
|
145
|
+
aria-expanded={open}
|
|
146
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
147
|
+
onClick={(e) => {
|
|
148
|
+
e.stopPropagation()
|
|
149
|
+
toggle()
|
|
150
|
+
}}
|
|
151
|
+
{...previewHandlers}
|
|
152
|
+
onFocus={previewHandlers.onPointerEnter}
|
|
153
|
+
onBlur={previewHandlers.onPointerLeave}
|
|
154
|
+
>
|
|
155
|
+
<CountBadge count={threads.length} open={open} />
|
|
156
|
+
</button>
|
|
157
|
+
</div>
|
|
158
|
+
{previewShown && !open && (
|
|
159
|
+
<ThreadPreview
|
|
160
|
+
editor={editor}
|
|
161
|
+
threads={threads}
|
|
162
|
+
// Lines the preview up with the stack list itself, so opening it leaves the cards
|
|
163
|
+
// exactly where the preview had them.
|
|
164
|
+
variant="list"
|
|
165
|
+
point={point}
|
|
166
|
+
// Picking a card from the preview lands in the same place clicking the badge and
|
|
167
|
+
// then the card would: the list open, that thread expanded within it.
|
|
168
|
+
onSelectThread={(thread) => {
|
|
169
|
+
openStackId.set(editor, stackId)
|
|
170
|
+
openThreadId.set(editor, thread.id)
|
|
171
|
+
}}
|
|
172
|
+
{...previewHandlers}
|
|
173
|
+
currentUserId={props.currentUserId}
|
|
174
|
+
resolveAuthor={props.resolveAuthor}
|
|
175
|
+
/>
|
|
176
|
+
)}
|
|
177
|
+
{open && (
|
|
178
|
+
<ThreadPopover
|
|
179
|
+
base={{
|
|
180
|
+
x: point.x + POPOVER_OFFSET.list.x,
|
|
181
|
+
y: point.y + POPOVER_OFFSET.list.y - liftForHeader,
|
|
182
|
+
}}
|
|
183
|
+
>
|
|
184
|
+
<div className="tlui-cmt-stack-list">
|
|
185
|
+
{threads.map((thread) =>
|
|
186
|
+
thread.id === openId ? (
|
|
187
|
+
<div key={thread.id} className="tlui-cmt-stack-list__thread">
|
|
188
|
+
<ThreadView editor={editor} thread={thread} {...props} />
|
|
189
|
+
</div>
|
|
190
|
+
) : (
|
|
191
|
+
<StackThreadCard
|
|
192
|
+
key={thread.id}
|
|
193
|
+
editor={editor}
|
|
194
|
+
thread={thread}
|
|
195
|
+
{...props}
|
|
196
|
+
onOpen={() => openThreadId.set(editor, thread.id)}
|
|
197
|
+
/>
|
|
198
|
+
)
|
|
199
|
+
)}
|
|
200
|
+
</div>
|
|
201
|
+
</ThreadPopover>
|
|
202
|
+
)}
|
|
203
|
+
</>
|
|
204
|
+
)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
/** A collapsed stack entry: the thread's first comment as a card; clicking expands the thread. */
|
|
208
|
+
function StackThreadCard({
|
|
209
|
+
editor,
|
|
210
|
+
thread,
|
|
211
|
+
onOpen,
|
|
212
|
+
...props
|
|
213
|
+
}: CommentingContext & { editor: Editor; thread: TLCommentThread; onOpen(): void }) {
|
|
214
|
+
const msg = useTranslation()
|
|
215
|
+
const options = useCommentingOptions()
|
|
216
|
+
const comments = useThreadComments(editor, thread.id)
|
|
217
|
+
const resolveName = useResolveName(props.resolveAuthor)
|
|
218
|
+
const first = comments[0]
|
|
219
|
+
if (!first) return null
|
|
220
|
+
const open = (e: ReactMouseEvent) => {
|
|
221
|
+
e.stopPropagation()
|
|
222
|
+
onOpen()
|
|
223
|
+
}
|
|
224
|
+
return (
|
|
225
|
+
<div className="tlui-cmt-stack-list__card" onClick={open}>
|
|
226
|
+
{/* The card's keyboard affordance. A button *wrapping* the card would put the comment
|
|
227
|
+
body inside it, and a body renders its links as real anchors — interactive content
|
|
228
|
+
nested in a button, which is an invalid content model and reads to assistive tech as a
|
|
229
|
+
broken control. So the button covers the card as a sibling instead, leaving the body's
|
|
230
|
+
own links above it and still reachable. */}
|
|
231
|
+
<button
|
|
232
|
+
type="button"
|
|
233
|
+
className="tlui-cmt-button tlui-cmt-stack-list__card-action"
|
|
234
|
+
aria-label={msg(
|
|
235
|
+
thread.resolved ? 'comments.pin-label-resolved' : 'comments.pin-label'
|
|
236
|
+
).replace('{name}', props.resolveAuthor(thread.createdBy)?.name ?? UNKNOWN_AUTHOR)}
|
|
237
|
+
onClick={open}
|
|
238
|
+
/>
|
|
239
|
+
<CommentCard {...toCardProps(first, props, options.components, resolveName)} />
|
|
240
|
+
</div>
|
|
241
|
+
)
|
|
242
|
+
}
|