{
  "version": 3,
  "sources": ["../../src/canvas/thread-preview.tsx"],
  "sourcesContent": ["import { useEffect, useMemo, useRef, type CSSProperties } from 'react'\nimport {\n\tEditor,\n\tEditorAtom,\n\tEditorPortal,\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 is unchanged.\n *\n * The panel is live, not a passive tooltip: the pointer can travel into it and click a card to open\n * that thread. Two pieces make that work \u2014 the close delay survives the trip across the gap, and\n * the panel carries an invisible bridge over it (see `.tlui-cmt-canvas-preview::before`).\n *\n * Opening a thread from a card needs nothing more than setting `openThreadId`, even for one folded\n * inside a badge: `collectClusterLeaves` skips the open thread, so it drops out and renders itself.\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 \u2014 long enough to cross the gap\n * between them, which the bridge element covers geometrically.\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. Those are\n * dropped rather than rendered blank, and don't count toward the overflow tally.\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: the close delay can leave an outgoing marker's timer pending when the next\n * 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. `markerId` must be stable and unique per marker across the\n * layer \u2014 prefix by kind, or a stack collides with its oldest member'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\" line.\n * Mounted only while hovering, so its store subscription costs nothing at rest.\n */\nexport function ThreadPreview({\n\teditor,\n\tthreads,\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\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 (\n\t\t<EditorPortal>\n\t\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\t<div\n\t\t\t\tref={ref}\n\t\t\t\tclassName={`tlui-cmt-canvas-preview tlui-cmt-canvas-preview--${variant}`}\n\t\t\t\tstyle={\n\t\t\t\t\t{\n\t\t\t\t\t\tleft: point.x + offset.x,\n\t\t\t\t\t\ttop: point.y + offset.y,\n\t\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\t// the offset rather than being restated as a second magic number.\n\t\t\t\t\t\t'--tlui-cmt-preview-offset': `${offset.x}px`,\n\t\t\t\t\t} as CSSProperties\n\t\t\t\t}\n\t\t\t\tonPointerEnter={onPointerEnter}\n\t\t\t\tonPointerLeave={onPointerLeave}\n\t\t\t\t// The panel sits over the canvas; a press on it is not a canvas press.\n\t\t\t\tonPointerDown={(e) => e.stopPropagation()}\n\t\t\t>\n\t\t\t\t<div className={panelClass}>\n\t\t\t\t\t{cards.map(({ thread, first }) => {\n\t\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\t// reader the thread continues past what they see.\n\t\t\t\t\t\tconst replies = replyCountLabel(msg, (countByThread.get(thread.id) ?? 1) - 1)\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tkey={thread.id}\n\t\t\t\t\t\t\t\tclassName={\n\t\t\t\t\t\t\t\t\tisThread\n\t\t\t\t\t\t\t\t\t\t? undefined\n\t\t\t\t\t\t\t\t\t\t: onSelectThread\n\t\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\t: 'tlui-cmt-preview-card'\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tonClick={\n\t\t\t\t\t\t\t\t\tonSelectThread\n\t\t\t\t\t\t\t\t\t\t? (e) => {\n\t\t\t\t\t\t\t\t\t\t\t\te.stopPropagation()\n\t\t\t\t\t\t\t\t\t\t\t\tonSelectThread(thread)\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t: 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\t\t<CommentCard\n\t\t\t\t\t\t\t\t\t{...toCardProps(first, props, options.components, resolveName)}\n\t\t\t\t\t\t\t\t\tfooter={\n\t\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\t}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)\n\t\t\t\t\t})}\n\t\t\t\t\t{overflow > 0 && (\n\t\t\t\t\t\t<div className=\"tlui-cmt-preview-more\">\n\t\t\t\t\t\t\t{msg('comments.preview-more').replace('{count}', String(overflow))}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</EditorPortal>\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"],
  "mappings": "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",
  "names": []
}
