/** * Comment controls painted over the document, never into it. * * The collaborative tree is prose alone. Thread chrome belongs to one reader, * so this adapter resolves its anchors into DOM rectangles and portals ordinary * React controls into the document page. */ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; import { DraftCard, ThreadCard } from "./comments"; import { gutterPoint, popoverPoint } from "./comment-geometry"; import { containsHit, passageHits } from "./comment-hits"; import { $rangeOf } from "./marks"; import { blockElement } from "./scroll"; import { useThreads } from "./threads"; import type { CSSProperties } from "react"; import type { Point, Rect } from "./comment-geometry"; import type { PassageHit } from "./comment-hits"; import type { ThreadStore, ThreadView } from "./threads"; type PlacedThread = { view: ThreadView; button: Point; hits: PassageHit[] }; type PassagePress = { id: string; left: number; pointer: number; top: number; moved: boolean }; function rect(value: DOMRect): Rect { return { top: value.top, right: value.right, bottom: value.bottom, left: value.left, width: value.width, height: value.height, }; } /** A small thread summary while a reader is deciding whether to open it. */ function Preview({ id, onMeasure, style, view, }: { id: string; onMeasure: (element: HTMLDivElement | null) => void; style: CSSProperties; view: ThreadView; }) { let replies = Math.max(0, view.thread.notes.length - 1); return (
{view.quote}
{replies > 0 && {replies} {replies === 1 ? "reply" : "replies"}}