@burdenoff/microfe-planmagnet 2026.1001.7 → 2026.1002.1

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.
Files changed (62) hide show
  1. package/dist/generated/global-types.d.ts +5 -0
  2. package/dist/generated/global-types.d.ts.map +1 -1
  3. package/dist/planmagnet/comments/CommentAttachmentUpload.d.ts +16 -0
  4. package/dist/planmagnet/comments/CommentAttachmentUpload.d.ts.map +1 -0
  5. package/dist/planmagnet/comments/CommentAttachmentUpload.js +218 -0
  6. package/dist/planmagnet/comments/CommentAttachmentUpload.js.map +1 -0
  7. package/dist/planmagnet/comments/CommentAttachments.d.ts +15 -0
  8. package/dist/planmagnet/comments/CommentAttachments.d.ts.map +1 -0
  9. package/dist/planmagnet/comments/CommentAttachments.js +74 -0
  10. package/dist/planmagnet/comments/CommentAttachments.js.map +1 -0
  11. package/dist/planmagnet/comments/CommentBody.d.ts +11 -0
  12. package/dist/planmagnet/comments/CommentBody.d.ts.map +1 -0
  13. package/dist/planmagnet/comments/CommentBody.js +130 -0
  14. package/dist/planmagnet/comments/CommentBody.js.map +1 -0
  15. package/dist/planmagnet/comments/CommentThread.d.ts +7 -2
  16. package/dist/planmagnet/comments/CommentThread.d.ts.map +1 -1
  17. package/dist/planmagnet/comments/CommentThread.js +288 -233
  18. package/dist/planmagnet/comments/CommentThread.js.map +1 -1
  19. package/dist/planmagnet/comments/MentionComposer.d.ts +0 -7
  20. package/dist/planmagnet/comments/MentionComposer.d.ts.map +1 -1
  21. package/dist/planmagnet/comments/MentionComposer.js +225 -86
  22. package/dist/planmagnet/comments/MentionComposer.js.map +1 -1
  23. package/dist/planmagnet/comments/TargetComments.d.ts.map +1 -1
  24. package/dist/planmagnet/comments/TargetComments.js +44 -6
  25. package/dist/planmagnet/comments/TargetComments.js.map +1 -1
  26. package/dist/planmagnet/comments/WorkItemComments.d.ts.map +1 -1
  27. package/dist/planmagnet/comments/WorkItemComments.js +22 -3
  28. package/dist/planmagnet/comments/WorkItemComments.js.map +1 -1
  29. package/dist/planmagnet/comments/commentAttachmentUploadClient.d.ts +102 -0
  30. package/dist/planmagnet/comments/commentAttachmentUploadClient.d.ts.map +1 -0
  31. package/dist/planmagnet/comments/commentAttachmentUploadClient.js +298 -0
  32. package/dist/planmagnet/comments/commentAttachmentUploadClient.js.map +1 -0
  33. package/dist/planmagnet/comments/commentImageTokens.d.ts +5 -0
  34. package/dist/planmagnet/comments/commentImageTokens.d.ts.map +1 -0
  35. package/dist/planmagnet/comments/commentImageTokens.js +14 -0
  36. package/dist/planmagnet/comments/commentImageTokens.js.map +1 -0
  37. package/dist/planmagnet/comments/commentTokens.d.ts +31 -0
  38. package/dist/planmagnet/comments/commentTokens.d.ts.map +1 -0
  39. package/dist/planmagnet/comments/commentTokens.js +142 -0
  40. package/dist/planmagnet/comments/commentTokens.js.map +1 -0
  41. package/dist/planmagnet/comments/fileDisplay.d.ts +22 -0
  42. package/dist/planmagnet/comments/fileDisplay.d.ts.map +1 -0
  43. package/dist/planmagnet/comments/fileDisplay.js +48 -0
  44. package/dist/planmagnet/comments/fileDisplay.js.map +1 -0
  45. package/dist/planmagnet/comments/useCommentFileUrl.d.ts +22 -0
  46. package/dist/planmagnet/comments/useCommentFileUrl.d.ts.map +1 -0
  47. package/dist/planmagnet/comments/useCommentFileUrl.js +54 -0
  48. package/dist/planmagnet/comments/useCommentFileUrl.js.map +1 -0
  49. package/dist/planmagnet/hooks/useLiveRefresh.d.ts +33 -2
  50. package/dist/planmagnet/hooks/useLiveRefresh.d.ts.map +1 -1
  51. package/dist/planmagnet/hooks/useLiveRefresh.js +71 -42
  52. package/dist/planmagnet/hooks/useLiveRefresh.js.map +1 -1
  53. package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.d.ts.map +1 -1
  54. package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.js +153 -117
  55. package/dist/planmagnet/pages/ProductsRoadmapAnalyticsPagePage.js.map +1 -1
  56. package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.d.ts.map +1 -1
  57. package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js +173 -138
  58. package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js.map +1 -1
  59. package/dist/planmagnet/roadmaps/RoadmapNowNextLater.d.ts.map +1 -1
  60. package/dist/planmagnet/roadmaps/RoadmapNowNextLater.js +171 -153
  61. package/dist/planmagnet/roadmaps/RoadmapNowNextLater.js.map +1 -1
  62. package/package.json +3 -2
@@ -1 +1 @@
1
- {"version":3,"file":"CommentThread.js","names":[],"sources":["../../../src/planmagnet/comments/CommentThread.tsx"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport type { DocumentNode } from 'graphql';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { CheckCircle2, ChevronDown, ChevronRight, Reply, RotateCcw, Trash2 } from 'lucide-react';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { PersonName, usePersonProfile } from '../people/People';\nimport { ConfirmButton, ErrorLine } from '../controls/ConfirmButton';\nimport { throwOnError } from '../utils/mutationResult';\nimport { isForbidden, mutationErrorMessage } from '../utils/graphqlErrors';\nimport { MentionComposer, MentionText } from './MentionComposer';\nimport { TypingLine } from '../presence/PresenceAvatars';\n\n/**\n * The comment thread engine shared by every commentable surface in this MFE: a work item (CWF-02, via\n * `WorkItemComments.tsx`) and a project/doc (CWF-12, via `TargetComments.tsx`). Modeled on\n * `comments/RoadmapComments.tsx`'s list/add/author-only-edit/delete structure, extended with:\n *\n * - Threading (CWF-02): `PlanmagnetWorkItemComment`/`PlanmagnetComment.replies` are ONE level deep by contract —\n * the server reparents \"a reply to a reply\" onto ITS OWN top-level parent. So a Reply action lives on both a\n * top-level comment and on one of its replies, but both always post with `parentId = <the top-level id>`\n * (`replyTargetId` below), matching what the server would do anyway — never a reply's own id.\n * - Resolution (CWF-14): a top-level-only OPEN/RESOLVED toggle, not restricted to the comment's author. A\n * resolved thread collapses to a one-line summary that can be expanded back out.\n * - Mentions (CWF-13): every composer here (add / reply / edit) is a `MentionComposer`; every rendered body is\n * `MentionText`, so a `@[Name](actorId)` token always shows as a highlighted name, never raw markup.\n * - A caller-agnostic core: `query`/`add`/`edit`/`remove`/`resolve` are passed in per surface (different\n * operation names, same field shapes — see `graphql/wspace.graphql`'s `PlanmagnetWorkItemComment` vs\n * `PlanmagnetComment` doc comments: \"everything here means exactly what it means on\" the other type).\n *\n * The status filter (Open/Resolved/All) is applied CLIENT-SIDE over the loaded page: none of\n * `planmagnetWorkItemComments`/`planmagnetProjectComments`/`planmagnetDocComments` take a status argument, only\n * `pagination` — same constraint the pre-existing pagination already lived with.\n */\n\nconst PAGE = 30;\nconst MAX_LENGTH = 10_000; // Every Add*CommentInput: \"1 to 10,000 characters after trimming\"\n\ntype CommentStatus = 'OPEN' | 'RESOLVED';\n\nexport interface ThreadComment {\n id: string;\n authorId: string;\n body: string;\n createdAt: string;\n editedAt: string | null;\n parentId: string | null;\n status: CommentStatus;\n resolvedBy: string | null;\n resolvedAt: string | null;\n mentions: string[];\n replies: ThreadComment[];\n}\n\ninterface ThreadList {\n totalCount: number;\n pageInfo: { hasNextPage: boolean; endCursor: string | null };\n items: ThreadComment[];\n}\n\ntype ThreadQueryData = Record<string, ThreadList>;\n\nexport interface CommentThreadProps {\n /** What this thread is attached to, used only in accessible copy (\"Add a comment on this task…\"). */\n targetNoun: string;\n /** Seeds every form-control id this instance renders, so two threads on the same page never collide. */\n idPrefix: string;\n query: {\n document: DocumentNode;\n /** The response field this query's data lands under (`planmagnetWorkItemComments`, …). */\n dataKey: string;\n variables: Record<string, unknown>;\n skip?: boolean;\n };\n add: {\n document: DocumentNode;\n buildVariables: (body: string, parentId: string | null) => Record<string, unknown>;\n };\n edit: DocumentNode;\n remove: DocumentNode;\n resolve: DocumentNode;\n /** Apollo operation names to refetch after any write here (this thread's own query, plus e.g. an Activity\n * panel on the same page that also shows COMMENTED events). */\n refetchQueries: string[];\n /** Reports the thread's live top-level count up to the caller, for a \"Comments (n)\" title. */\n onTotalCount?: (count: number) => void;\n /** CWF-24 typing presence, owned by the page (one `usePresence` call shared with its `PresenceAvatars`) and\n * threaded down here so the composers can signal typing and the thread can show who else is mid-reply. Omitted\n * entirely on a surface that has not wired presence up (renders nothing extra, same as before). */\n presence?: {\n typingActorIds: readonly string[];\n notifyTyping: () => void;\n };\n}\n\nconst when = (iso: string) => new Date(iso).toLocaleString();\n\nexport function CommentThread({\n targetNoun,\n idPrefix,\n query,\n add,\n edit,\n remove,\n resolve,\n refetchQueries,\n onTotalCount,\n presence,\n}: CommentThreadProps) {\n const { currentUser, workspaceId, authToken, apiGatewayUrl } = usePlanMagnet();\n const [draft, setDraft] = useState('');\n const [error, setError] = useState<string | null>(null);\n const [loadingMore, setLoadingMore] = useState(false);\n const [filter, setFilter] = useState<'OPEN' | 'RESOLVED' | 'ALL'>('ALL');\n\n const mutationRefetch = { refetchQueries, awaitRefetchQueries: true };\n\n const {\n data,\n loading,\n error: loadError,\n fetchMore,\n } = useQuery<ThreadQueryData>(query.document, {\n variables: { ...query.variables, pagination: { first: PAGE } },\n skip: query.skip,\n });\n const [addComment, addState] = useMutation(add.document, mutationRefetch);\n\n const thread = data?.[query.dataKey];\n // The server returns TOP-LEVEL comments here; each carries its own (bounded, unpaginated) `replies`.\n const topLevel = thread?.items ?? [];\n const visible = filter === 'ALL' ? topLevel : topLevel.filter((c) => c.status === filter);\n const openCount = topLevel.filter((c) => c.status === 'OPEN').length;\n const resolvedCount = topLevel.length - openCount;\n\n const trimmed = draft.trim();\n const tooLong = trimmed.length > MAX_LENGTH;\n\n useEffect(() => {\n if (thread) onTotalCount?.(thread.totalCount);\n }, [thread, onTotalCount]);\n\n const post = async (body: string, parentId: string | null): Promise<boolean> => {\n setError(null);\n try {\n await throwOnError(addComment({ variables: add.buildVariables(body, parentId) }));\n return true;\n } catch (e) {\n setError(\n isForbidden(e)\n ? 'Your role can read but not comment.'\n : mutationErrorMessage(e, 'The comment could not be posted.')\n );\n return false;\n }\n };\n\n const submitTop = async () => {\n if (await post(trimmed, null)) setDraft('');\n };\n\n const loadMore = async () => {\n const after = thread?.pageInfo.endCursor;\n if (!after) return;\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { ...query.variables, pagination: { first: PAGE, after } },\n updateQuery: (prev, { fetchMoreResult }) => ({\n [query.dataKey]: {\n ...fetchMoreResult[query.dataKey],\n items: [...prev[query.dataKey].items, ...fetchMoreResult[query.dataKey].items],\n },\n }),\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n return (\n <div className=\"space-y-4\" data-testid=\"comment-thread\">\n {error && <ErrorLine message={error} />}\n {loading && !data && <p className=\"text-sm text-muted-foreground\">Loading comments…</p>}\n {loadError && <ErrorLine message={`Comments could not be loaded: ${loadError.message}`} />}\n\n {thread && topLevel.length > 0 && (\n <div\n role=\"group\"\n aria-label=\"Filter comments by resolution\"\n data-testid=\"comment-filter\"\n className=\"flex flex-wrap gap-1\"\n >\n {(\n [\n ['OPEN', `Open (${openCount})`],\n ['RESOLVED', `Resolved (${resolvedCount})`],\n ['ALL', `All (${topLevel.length})`],\n ] as const\n ).map(([mode, label]) => (\n <Button\n key={mode}\n type=\"button\"\n size=\"sm\"\n variant={filter === mode ? 'default' : 'outline'}\n aria-pressed={filter === mode}\n className=\"h-7 px-2.5 text-xs\"\n onClick={() => setFilter(mode)}\n >\n {label}\n </Button>\n ))}\n </div>\n )}\n\n {thread && visible.length === 0 && (\n <p className=\"text-sm text-muted-foreground\">\n {topLevel.length === 0 ? 'No comments yet.' : 'No comments match this filter.'}\n </p>\n )}\n\n <ol className=\"space-y-3\">\n {visible.map((comment) => (\n <TopLevelThread\n key={comment.id}\n comment={comment}\n currentUserId={currentUser?.id}\n editMutation={edit}\n removeMutation={remove}\n resolveMutation={resolve}\n addMutation={add.document}\n buildReplyVariables={(body) => add.buildVariables(body, comment.id)}\n mutationRefetch={mutationRefetch}\n onError={setError}\n idPrefix={idPrefix}\n workspaceId={workspaceId}\n authToken={authToken}\n apiGatewayUrl={apiGatewayUrl}\n onTyping={presence?.notifyTyping}\n />\n ))}\n </ol>\n\n {thread?.pageInfo.hasNextPage && (\n <Button size=\"sm\" variant=\"outline\" onClick={() => void loadMore()} disabled={loadingMore}>\n {loadingMore ? 'Loading…' : `Show more (${topLevel.length} of ${thread.totalCount})`}\n </Button>\n )}\n\n {presence && <TypingLine typingActorIds={presence.typingActorIds} />}\n\n <div className=\"space-y-2 border-t border-border-default pt-4\">\n <label htmlFor={`${idPrefix}-add`} className=\"sr-only\">\n Add a comment\n </label>\n <MentionComposer\n id={`${idPrefix}-add`}\n ariaLabel={`Add a comment on this ${targetNoun}`}\n placeholder=\"Add a comment…\"\n className=\"min-h-[72px]\"\n value={draft}\n onChange={(value) => {\n setDraft(value);\n presence?.notifyTyping();\n }}\n workspaceId={workspaceId}\n authToken={authToken}\n apiGatewayUrl={apiGatewayUrl}\n />\n {tooLong && <ErrorLine message={`A comment can be at most ${MAX_LENGTH} characters.`} />}\n <div className=\"flex justify-end\">\n <Button\n size=\"sm\"\n onClick={() => void submitTop()}\n disabled={!trimmed || tooLong || addState.loading}\n data-testid=\"comment-post\"\n >\n {addState.loading ? 'Posting…' : 'Comment'}\n </Button>\n </div>\n </div>\n </div>\n );\n}\n\ninterface RowMutationProps {\n editMutation: DocumentNode;\n removeMutation: DocumentNode;\n mutationRefetch: { refetchQueries: string[]; awaitRefetchQueries: boolean };\n onError: (message: string | null) => void;\n idPrefix: string;\n workspaceId?: string;\n authToken?: string;\n apiGatewayUrl?: string;\n}\n\n/**\n * One top-level comment: header, body, its replies (indented), a Reply action, and — top-level only — the\n * Resolve/Reopen toggle. A resolved thread starts collapsed to a one-line summary; expand/collapse is local UI\n * state that does not reset when Resolve/Reopen flips (so reopening mid-review doesn't yank the view around).\n */\nfunction TopLevelThread({\n comment,\n currentUserId,\n editMutation,\n removeMutation,\n resolveMutation,\n addMutation,\n buildReplyVariables,\n mutationRefetch,\n onError,\n idPrefix,\n workspaceId,\n authToken,\n apiGatewayUrl,\n onTyping,\n}: RowMutationProps & {\n comment: ThreadComment;\n currentUserId: string | undefined;\n resolveMutation: DocumentNode;\n addMutation: DocumentNode;\n buildReplyVariables: (body: string) => Record<string, unknown>;\n /** CWF-24: signals typing while the reply box (the only composer a `TopLevelThread` instance owns) is active. */\n onTyping?: () => void;\n}) {\n const [editing, setEditing] = useState(false);\n const [text, setText] = useState(comment.body);\n const [expanded, setExpanded] = useState(comment.status !== 'RESOLVED');\n const [replyOpen, setReplyOpen] = useState(false);\n const [replyDraft, setReplyDraft] = useState('');\n const [replyingToName, setReplyingToName] = useState<string | null>(null);\n\n const authorProfile = usePersonProfile(comment.authorId);\n const [edit, editState] = useMutation(editMutation, mutationRefetch);\n const [remove, removeState] = useMutation(removeMutation, mutationRefetch);\n const [resolveComment, resolveState] = useMutation(resolveMutation, mutationRefetch);\n const [reply, replyState] = useMutation(addMutation, mutationRefetch);\n\n const mine = comment.authorId === currentUserId;\n const resolved = comment.status === 'RESOLVED';\n const editTooLong = text.trim().length > MAX_LENGTH;\n const replyTooLong = replyDraft.trim().length > MAX_LENGTH;\n\n const save = async () => {\n onError(null);\n try {\n await throwOnError(edit({ variables: { id: comment.id, body: text.trim() } }));\n setEditing(false);\n } catch (e) {\n onError(mutationErrorMessage(e, 'The comment could not be saved.'));\n }\n };\n\n const onRemove = async () => {\n onError(null);\n try {\n await throwOnError(remove({ variables: { id: comment.id } }));\n } catch (e) {\n onError(mutationErrorMessage(e, 'The comment could not be deleted.'));\n }\n };\n\n const toggleResolve = async () => {\n onError(null);\n try {\n await throwOnError(resolveComment({ variables: { id: comment.id, resolved: !resolved } }));\n } catch (e) {\n onError(\n isForbidden(e)\n ? 'Your role can read but not resolve comments.'\n : mutationErrorMessage(e, 'The resolution could not be changed.')\n );\n }\n };\n\n const openReply = (name: string, mentionId?: string, mentionName?: string) => {\n setReplyingToName(name);\n setReplyDraft(mentionId && mentionName ? `@[${mentionName}](${mentionId}) ` : '');\n setReplyOpen(true);\n setExpanded(true);\n };\n\n const submitReply = async () => {\n const body = replyDraft.trim();\n if (!body) return;\n onError(null);\n try {\n await throwOnError(reply({ variables: buildReplyVariables(body) }));\n setReplyDraft('');\n setReplyOpen(false);\n setReplyingToName(null);\n } catch (e) {\n onError(\n isForbidden(e)\n ? 'Your role can read but not comment.'\n : mutationErrorMessage(e, 'The reply could not be posted.')\n );\n }\n };\n\n const rowId = `${idPrefix}-comment-${comment.id}`;\n\n return (\n <li\n className=\"rounded-md border border-border-default p-3 space-y-2\"\n data-testid=\"comment-thread-item\"\n >\n {resolved && !expanded ? (\n <div className=\"flex items-center justify-between gap-2\">\n <p className=\"min-w-0 truncate text-sm text-muted-foreground\">\n <CheckCircle2\n className=\"mr-1 inline h-3.5 w-3.5 text-status-success-text\"\n aria-hidden=\"true\"\n />\n Resolved by <PersonName id={comment.resolvedBy} />\n {comment.resolvedAt ? ` · ${when(comment.resolvedAt)}` : ''}\n {comment.replies.length > 0\n ? ` · ${comment.replies.length} repl${comment.replies.length === 1 ? 'y' : 'ies'}`\n : ''}\n </p>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 shrink-0 px-2 text-xs\"\n aria-expanded={false}\n aria-controls={rowId}\n onClick={() => setExpanded(true)}\n >\n <ChevronRight className=\"mr-1 h-3.5 w-3.5\" aria-hidden=\"true\" />\n Expand\n </Button>\n </div>\n ) : (\n <div id={rowId} className=\"space-y-2\">\n <div className=\"flex items-start justify-between gap-2 text-xs text-muted-foreground\">\n <span>\n <span className=\"font-medium text-foreground\">\n <PersonName id={comment.authorId} />\n </span>{' '}\n · {when(comment.createdAt)}\n {comment.editedAt && (\n <span title={`Edited ${when(comment.editedAt)}`}> · edited</span>\n )}\n {resolved && (\n <span className=\"ml-1 inline-flex items-center gap-1 text-status-success-text\">\n <CheckCircle2 className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n Resolved\n </span>\n )}\n </span>\n <span className=\"flex shrink-0 items-center gap-1\">\n {mine && !editing && (\n <>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 px-2 text-xs\"\n onClick={() => setEditing(true)}\n >\n Edit\n </Button>\n <ConfirmButton\n label=\"\"\n ariaLabel=\"Delete comment\"\n variant=\"outline\"\n icon={<Trash2 className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />}\n title=\"Delete this comment?\"\n description=\"It is removed from the discussion for everyone, replies included. This cannot be undone here.\"\n confirmLabel=\"Delete\"\n destructive\n disabled={removeState.loading}\n onConfirm={() => void onRemove()}\n />\n </>\n )}\n <Button\n size=\"sm\"\n variant={resolved ? 'outline' : 'ghost'}\n className=\"h-6 px-2 text-xs\"\n data-testid=\"comment-resolve\"\n disabled={resolveState.loading}\n onClick={() => void toggleResolve()}\n >\n {resolved ? (\n <>\n <RotateCcw className=\"mr-1 h-3.5 w-3.5\" aria-hidden=\"true\" />\n Reopen\n </>\n ) : (\n <>\n <CheckCircle2 className=\"mr-1 h-3.5 w-3.5\" aria-hidden=\"true\" />\n Resolve\n </>\n )}\n </Button>\n {resolved && (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 px-2 text-xs\"\n aria-expanded\n aria-controls={rowId}\n onClick={() => setExpanded(false)}\n >\n <ChevronDown className=\"mr-1 h-3.5 w-3.5\" aria-hidden=\"true\" />\n Collapse\n </Button>\n )}\n </span>\n </div>\n\n {editing ? (\n <div className=\"space-y-2\">\n <MentionComposer\n id={`${rowId}-edit`}\n ariaLabel=\"Edit comment\"\n className=\"min-h-[60px]\"\n value={text}\n onChange={setText}\n workspaceId={workspaceId}\n authToken={authToken}\n apiGatewayUrl={apiGatewayUrl}\n />\n {editTooLong && (\n <ErrorLine message={`A comment can be at most ${MAX_LENGTH} characters.`} />\n )}\n <div className=\"flex justify-end gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => {\n setEditing(false);\n setText(comment.body);\n onError(null);\n }}\n >\n Cancel\n </Button>\n <Button\n size=\"sm\"\n onClick={() => void save()}\n disabled={!text.trim() || editTooLong || editState.loading}\n >\n {editState.loading ? 'Saving…' : 'Save'}\n </Button>\n </div>\n </div>\n ) : (\n <MentionText text={comment.body} />\n )}\n\n {!editing && (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 px-2 text-xs\"\n data-testid=\"comment-reply\"\n onClick={() =>\n openReply(\n authorProfile?.displayName ?? 'this comment',\n comment.authorId,\n authorProfile?.displayName\n )\n }\n >\n <Reply className=\"mr-1 h-3.5 w-3.5\" aria-hidden=\"true\" />\n Reply\n </Button>\n )}\n\n {comment.replies.length > 0 && (\n <ol className=\"ml-5 space-y-2 border-l border-border-default pl-3\">\n {comment.replies.map((r) => (\n <ReplyItem\n key={r.id}\n reply={r}\n currentUserId={currentUserId}\n editMutation={editMutation}\n removeMutation={removeMutation}\n mutationRefetch={mutationRefetch}\n onError={onError}\n idPrefix={idPrefix}\n workspaceId={workspaceId}\n authToken={authToken}\n apiGatewayUrl={apiGatewayUrl}\n onReply={openReply}\n />\n ))}\n </ol>\n )}\n\n {replyOpen && (\n <div className=\"ml-5 space-y-2 border-l border-border-default pl-3\">\n {replyingToName && (\n <p className=\"text-xs text-muted-foreground\">Replying to {replyingToName}</p>\n )}\n <MentionComposer\n id={`${rowId}-reply`}\n ariaLabel=\"Write a reply\"\n className=\"min-h-[60px]\"\n value={replyDraft}\n onChange={(value) => {\n setReplyDraft(value);\n onTyping?.();\n }}\n autoFocus\n workspaceId={workspaceId}\n authToken={authToken}\n apiGatewayUrl={apiGatewayUrl}\n />\n {replyTooLong && (\n <ErrorLine message={`A comment can be at most ${MAX_LENGTH} characters.`} />\n )}\n <div className=\"flex justify-end gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => {\n setReplyOpen(false);\n setReplyDraft('');\n setReplyingToName(null);\n }}\n >\n Cancel\n </Button>\n <Button\n size=\"sm\"\n onClick={() => void submitReply()}\n disabled={!replyDraft.trim() || replyTooLong || replyState.loading}\n >\n {replyState.loading ? 'Posting…' : 'Reply'}\n </Button>\n </div>\n </div>\n )}\n </div>\n )}\n </li>\n );\n}\n\n/** One reply, indented under its top-level parent. Replies carry no resolution state of their own (only a\n * top-level comment does — CWF-14) and their own Reply action still targets the TOP-LEVEL thread\n * (`onReply` is the parent's `openReply`, called with this reply's own author as the \"replying to\" context). */\nfunction ReplyItem({\n reply,\n currentUserId,\n editMutation,\n removeMutation,\n mutationRefetch,\n onError,\n idPrefix,\n workspaceId,\n authToken,\n apiGatewayUrl,\n onReply,\n}: RowMutationProps & {\n reply: ThreadComment;\n currentUserId: string | undefined;\n onReply: (name: string, mentionId?: string, mentionName?: string) => void;\n}) {\n const [editing, setEditing] = useState(false);\n const [text, setText] = useState(reply.body);\n const profile = usePersonProfile(reply.authorId);\n const [edit, editState] = useMutation(editMutation, mutationRefetch);\n const [remove, removeState] = useMutation(removeMutation, mutationRefetch);\n\n const mine = reply.authorId === currentUserId;\n const editTooLong = text.trim().length > MAX_LENGTH;\n\n const save = async () => {\n onError(null);\n try {\n await throwOnError(edit({ variables: { id: reply.id, body: text.trim() } }));\n setEditing(false);\n } catch (e) {\n onError(mutationErrorMessage(e, 'The reply could not be saved.'));\n }\n };\n\n const onRemove = async () => {\n onError(null);\n try {\n await throwOnError(remove({ variables: { id: reply.id } }));\n } catch (e) {\n onError(mutationErrorMessage(e, 'The reply could not be deleted.'));\n }\n };\n\n return (\n <li className=\"space-y-1\" data-testid=\"comment-reply-item\">\n <div className=\"flex items-start justify-between gap-2 text-xs text-muted-foreground\">\n <span>\n <span className=\"font-medium text-foreground\">\n <PersonName id={reply.authorId} />\n </span>{' '}\n · {when(reply.createdAt)}\n {reply.editedAt && <span title={`Edited ${when(reply.editedAt)}`}> · edited</span>}\n </span>\n {mine && !editing && (\n <span className=\"flex shrink-0 items-center gap-1\">\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 px-2 text-xs\"\n onClick={() => setEditing(true)}\n >\n Edit\n </Button>\n <ConfirmButton\n label=\"\"\n ariaLabel=\"Delete reply\"\n variant=\"outline\"\n icon={<Trash2 className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />}\n title=\"Delete this reply?\"\n description=\"It is removed from the discussion for everyone. This cannot be undone here.\"\n confirmLabel=\"Delete\"\n destructive\n disabled={removeState.loading}\n onConfirm={() => void onRemove()}\n />\n </span>\n )}\n </div>\n\n {editing ? (\n <div className=\"space-y-2\">\n <MentionComposer\n id={`${idPrefix}-reply-${reply.id}-edit`}\n ariaLabel=\"Edit reply\"\n className=\"min-h-[56px]\"\n value={text}\n onChange={setText}\n workspaceId={workspaceId}\n authToken={authToken}\n apiGatewayUrl={apiGatewayUrl}\n />\n {editTooLong && (\n <ErrorLine message={`A comment can be at most ${MAX_LENGTH} characters.`} />\n )}\n <div className=\"flex justify-end gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => {\n setEditing(false);\n setText(reply.body);\n onError(null);\n }}\n >\n Cancel\n </Button>\n <Button\n size=\"sm\"\n onClick={() => void save()}\n disabled={!text.trim() || editTooLong || editState.loading}\n >\n {editState.loading ? 'Saving…' : 'Save'}\n </Button>\n </div>\n </div>\n ) : (\n <MentionText text={reply.body} />\n )}\n\n {!editing && (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 px-2 text-xs\"\n data-testid=\"comment-reply\"\n onClick={() =>\n onReply(profile?.displayName ?? 'this reply', reply.authorId, profile?.displayName)\n }\n >\n <Reply className=\"mr-1 h-3.5 w-3.5\" aria-hidden=\"true\" />\n Reply\n </Button>\n )}\n </li>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"CommentThread.js","names":[],"sources":["../../../src/planmagnet/comments/CommentThread.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from 'react';\nimport type { DocumentNode } from 'graphql';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { CheckCircle2, ChevronDown, ChevronRight, Reply, RotateCcw, Trash2 } from 'lucide-react';\nimport { usePlanMagnet } from '../providers/PlanMagnetProvider';\nimport { PersonName, usePersonProfile } from '../people/People';\nimport { ConfirmButton, ErrorLine } from '../controls/ConfirmButton';\nimport { throwOnError } from '../utils/mutationResult';\nimport { isForbidden, mutationErrorMessage } from '../utils/graphqlErrors';\nimport { MentionComposer } from './MentionComposer';\nimport { CommentBody } from './CommentBody';\nimport { appendImageEmbed, removeImageEmbed } from './commentImageTokens';\nimport { CommentAttachments, type CommentAttachmentSummary } from './CommentAttachments';\nimport type { CommentFileTarget } from './useCommentFileUrl';\nimport {\n CommentAttachmentUpload,\n type CommentAttachmentUploadState,\n} from './CommentAttachmentUpload';\nimport { TypingLine } from '../presence/PresenceAvatars';\n\n/**\n * The comment thread engine shared by every commentable surface in this MFE: a work item (CWF-02, via\n * `WorkItemComments.tsx`) and a project/doc (CWF-12, via `TargetComments.tsx`). Modeled on\n * `comments/RoadmapComments.tsx`'s list/add/author-only-edit/delete structure, extended with:\n *\n * - Threading (CWF-02): `PlanmagnetWorkItemComment`/`PlanmagnetComment.replies` are ONE level deep by contract —\n * the server reparents \"a reply to a reply\" onto ITS OWN top-level parent. So a Reply action lives on both a\n * top-level comment and on one of its replies, but both always post with `parentId = <the top-level id>`\n * (`replyTargetId` below), matching what the server would do anyway — never a reply's own id.\n * - Resolution (CWF-14): a top-level-only OPEN/RESOLVED toggle, not restricted to the comment's author. A\n * resolved thread collapses to a one-line summary that can be expanded back out.\n * - Mentions (CWF-13): every composer here (add / reply / edit) is a `MentionComposer`; every rendered body is\n * `CommentBody`, so a `@[Name](actorId)` token always shows as a highlighted name, never raw markup.\n * - Rich text + attachments (CWF-12): the body also carries a small server-sanitized markdown grammar (bold,\n * italic, code, links, mentions, fileId-referenced images — `CommentBody.tsx` renders it, `MentionComposer`'s\n * toolbar writes it) and up to 10 file attachments, addable only when POSTING (add/reply) — none of the Edit\n * mutations take `attachmentFileIds`, so an existing comment's attachments can never change.\n * - A caller-agnostic core: `query`/`add`/`edit`/`remove`/`resolve` are passed in per surface (different\n * operation names, same field shapes — see `graphql/wspace.graphql`'s `PlanmagnetWorkItemComment` vs\n * `PlanmagnetComment` doc comments: \"everything here means exactly what it means on\" the other type).\n *\n * The status filter (Open/Resolved/All) is applied CLIENT-SIDE over the loaded page: none of\n * `planmagnetWorkItemComments`/`planmagnetProjectComments`/`planmagnetDocComments` take a status argument, only\n * `pagination` — same constraint the pre-existing pagination already lived with.\n */\n\nconst PAGE = 30;\nconst MAX_LENGTH = 10_000; // Every Add*CommentInput: \"1 to 10,000 characters after trimming\"\n\ntype CommentStatus = 'OPEN' | 'RESOLVED';\n\nexport interface ThreadComment {\n id: string;\n authorId: string;\n body: string;\n createdAt: string;\n editedAt: string | null;\n parentId: string | null;\n status: CommentStatus;\n resolvedBy: string | null;\n resolvedAt: string | null;\n mentions: string[];\n attachments: CommentAttachmentSummary[];\n replies: ThreadComment[];\n}\n\ninterface ThreadList {\n totalCount: number;\n pageInfo: { hasNextPage: boolean; endCursor: string | null };\n items: ThreadComment[];\n}\n\ntype ThreadQueryData = Record<string, ThreadList>;\n\nexport interface CommentThreadProps {\n /** Verified by the matching product-read resource before an attachment can be downloaded. */\n target: CommentFileTarget;\n /** What this thread is attached to, used only in accessible copy (\"Add a comment on this task…\"). */\n targetNoun: string;\n /** Seeds every form-control id this instance renders, so two threads on the same page never collide. */\n idPrefix: string;\n query: {\n document: DocumentNode;\n /** The response field this query's data lands under (`planmagnetWorkItemComments`, …). */\n dataKey: string;\n variables: Record<string, unknown>;\n skip?: boolean;\n };\n add: {\n document: DocumentNode;\n buildVariables: (\n body: string,\n parentId: string | null,\n attachmentFileIds: string[]\n ) => Record<string, unknown>;\n };\n edit: DocumentNode;\n remove: DocumentNode;\n resolve: DocumentNode;\n /** Apollo operation names to refetch after any write here (this thread's own query, plus e.g. an Activity\n * panel on the same page that also shows COMMENTED events). */\n refetchQueries: string[];\n /** Reports the thread's live top-level count up to the caller, for a \"Comments (n)\" title. */\n onTotalCount?: (count: number) => void;\n /** CWF-24 typing presence, owned by the page (one `usePresence` call shared with its `PresenceAvatars`) and\n * threaded down here so the composers can signal typing and the thread can show who else is mid-reply. Omitted\n * entirely on a surface that has not wired presence up (renders nothing extra, same as before). */\n presence?: {\n typingActorIds: readonly string[];\n notifyTyping: () => void;\n };\n}\n\nconst when = (iso: string) => new Date(iso).toLocaleString();\n\nexport function CommentThread({\n target,\n targetNoun,\n idPrefix,\n query,\n add,\n edit,\n remove,\n resolve,\n refetchQueries,\n onTotalCount,\n presence,\n}: CommentThreadProps) {\n const { currentUser, workspaceId, authToken, apiGatewayUrl } = usePlanMagnet();\n const [draft, setDraft] = useState('');\n const [error, setError] = useState<string | null>(null);\n const [loadingMore, setLoadingMore] = useState(false);\n const [filter, setFilter] = useState<'OPEN' | 'RESOLVED' | 'ALL'>('ALL');\n const [topAttach, setTopAttach] = useState<CommentAttachmentUploadState>({\n fileIds: [],\n pending: false,\n });\n // Remounts CommentAttachmentUpload (clearing its queue) after a successful top-level post — simpler and more\n // robust than an imperative reset API, same \"change the key to reset\" approach React itself recommends.\n const [topAttachResetKey, setTopAttachResetKey] = useState(0);\n\n const mutationRefetch = { refetchQueries, awaitRefetchQueries: true };\n\n const {\n data,\n loading,\n error: loadError,\n fetchMore,\n } = useQuery<ThreadQueryData>(query.document, {\n variables: { ...query.variables, pagination: { first: PAGE } },\n skip: query.skip,\n });\n const [addComment, addState] = useMutation(add.document, mutationRefetch);\n\n const thread = data?.[query.dataKey];\n // The server returns TOP-LEVEL comments here; each carries its own (bounded, unpaginated) `replies`.\n const topLevel = thread?.items ?? [];\n const visible = filter === 'ALL' ? topLevel : topLevel.filter((c) => c.status === filter);\n const openCount = topLevel.filter((c) => c.status === 'OPEN').length;\n const resolvedCount = topLevel.length - openCount;\n\n const trimmed = draft.trim();\n const tooLong = trimmed.length > MAX_LENGTH;\n\n useEffect(() => {\n if (thread) onTotalCount?.(thread.totalCount);\n }, [thread, onTotalCount]);\n\n const post = async (\n body: string,\n parentId: string | null,\n attachmentFileIds: string[]\n ): Promise<boolean> => {\n setError(null);\n try {\n await throwOnError(\n addComment({ variables: add.buildVariables(body, parentId, attachmentFileIds) })\n );\n return true;\n } catch (e) {\n setError(\n isForbidden(e)\n ? 'Your role can read but not comment.'\n : mutationErrorMessage(e, 'The comment could not be posted.')\n );\n return false;\n }\n };\n\n const submitTop = async () => {\n if (await post(trimmed, null, topAttach.fileIds)) {\n setDraft('');\n setTopAttach({ fileIds: [], pending: false });\n setTopAttachResetKey((k) => k + 1);\n }\n };\n\n const loadMore = async () => {\n const after = thread?.pageInfo.endCursor;\n if (!after) return;\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { ...query.variables, pagination: { first: PAGE, after } },\n updateQuery: (prev, { fetchMoreResult }) => ({\n [query.dataKey]: {\n ...fetchMoreResult[query.dataKey],\n items: [...prev[query.dataKey].items, ...fetchMoreResult[query.dataKey].items],\n },\n }),\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n return (\n <div className=\"space-y-4\" data-testid=\"comment-thread\">\n {error && <ErrorLine message={error} />}\n {loading && !data && <p className=\"text-sm text-muted-foreground\">Loading comments…</p>}\n {loadError && <ErrorLine message={`Comments could not be loaded: ${loadError.message}`} />}\n\n {thread && topLevel.length > 0 && (\n <div\n role=\"group\"\n aria-label=\"Filter comments by resolution\"\n data-testid=\"comment-filter\"\n className=\"flex flex-wrap gap-1\"\n >\n {(\n [\n ['OPEN', `Open (${openCount})`],\n ['RESOLVED', `Resolved (${resolvedCount})`],\n ['ALL', `All (${topLevel.length})`],\n ] as const\n ).map(([mode, label]) => (\n <Button\n key={mode}\n type=\"button\"\n size=\"sm\"\n variant={filter === mode ? 'default' : 'outline'}\n aria-pressed={filter === mode}\n className=\"h-7 px-2.5 text-xs\"\n onClick={() => setFilter(mode)}\n >\n {label}\n </Button>\n ))}\n </div>\n )}\n\n {thread && visible.length === 0 && (\n <p className=\"text-sm text-muted-foreground\">\n {topLevel.length === 0 ? 'No comments yet.' : 'No comments match this filter.'}\n </p>\n )}\n\n <ol className=\"space-y-3\">\n {visible.map((comment) => (\n <TopLevelThread\n key={comment.id}\n comment={comment}\n target={target}\n currentUserId={currentUser?.id}\n editMutation={edit}\n removeMutation={remove}\n resolveMutation={resolve}\n addMutation={add.document}\n buildReplyVariables={(body, attachmentFileIds) =>\n add.buildVariables(body, comment.id, attachmentFileIds)\n }\n mutationRefetch={mutationRefetch}\n onError={setError}\n idPrefix={idPrefix}\n workspaceId={workspaceId}\n authToken={authToken}\n apiGatewayUrl={apiGatewayUrl}\n onTyping={presence?.notifyTyping}\n />\n ))}\n </ol>\n\n {thread?.pageInfo.hasNextPage && (\n <Button size=\"sm\" variant=\"outline\" onClick={() => void loadMore()} disabled={loadingMore}>\n {loadingMore ? 'Loading…' : `Show more (${topLevel.length} of ${thread.totalCount})`}\n </Button>\n )}\n\n {presence && <TypingLine typingActorIds={presence.typingActorIds} />}\n\n <div className=\"space-y-2 border-t border-border-default pt-4\">\n <label htmlFor={`${idPrefix}-add`} className=\"sr-only\">\n Add a comment\n </label>\n <MentionComposer\n id={`${idPrefix}-add`}\n ariaLabel={`Add a comment on this ${targetNoun}`}\n placeholder=\"Add a comment…\"\n className=\"min-h-[72px]\"\n value={draft}\n disabled={addState.loading}\n onChange={(value) => {\n setDraft(value);\n presence?.notifyTyping();\n }}\n workspaceId={workspaceId}\n authToken={authToken}\n apiGatewayUrl={apiGatewayUrl}\n />\n {tooLong && <ErrorLine message={`A comment can be at most ${MAX_LENGTH} characters.`} />}\n <CommentAttachmentUpload\n key={topAttachResetKey}\n idPrefix={`${idPrefix}-add`}\n disabled={addState.loading}\n onChange={setTopAttach}\n onInsertImage={(fileId, filename) =>\n setDraft((current) => appendImageEmbed(current, fileId, filename))\n }\n onRemoveImage={(fileId) => setDraft((current) => removeImageEmbed(current, fileId))}\n />\n <div className=\"flex justify-end\">\n <Button\n size=\"sm\"\n onClick={() => void submitTop()}\n disabled={!trimmed || tooLong || addState.loading || topAttach.pending}\n data-testid=\"comment-post\"\n >\n {addState.loading\n ? 'Posting…'\n : topAttach.pending\n ? 'Preparing attachments…'\n : 'Comment'}\n </Button>\n </div>\n </div>\n </div>\n );\n}\n\ninterface RowMutationProps {\n target: CommentFileTarget;\n editMutation: DocumentNode;\n removeMutation: DocumentNode;\n mutationRefetch: { refetchQueries: string[]; awaitRefetchQueries: boolean };\n onError: (message: string | null) => void;\n idPrefix: string;\n workspaceId?: string;\n authToken?: string;\n apiGatewayUrl?: string;\n}\n\n/**\n * One top-level comment: header, body, its replies (indented), a Reply action, and — top-level only — the\n * Resolve/Reopen toggle. A resolved thread starts collapsed to a one-line summary; expand/collapse is local UI\n * state that does not reset when Resolve/Reopen flips (so reopening mid-review doesn't yank the view around).\n */\nfunction TopLevelThread({\n comment,\n target,\n currentUserId,\n editMutation,\n removeMutation,\n resolveMutation,\n addMutation,\n buildReplyVariables,\n mutationRefetch,\n onError,\n idPrefix,\n workspaceId,\n authToken,\n apiGatewayUrl,\n onTyping,\n}: RowMutationProps & {\n comment: ThreadComment;\n currentUserId: string | undefined;\n resolveMutation: DocumentNode;\n addMutation: DocumentNode;\n buildReplyVariables: (body: string, attachmentFileIds: string[]) => Record<string, unknown>;\n /** CWF-24: signals typing while the reply box (the only composer a `TopLevelThread` instance owns) is active. */\n onTyping?: () => void;\n}) {\n const [editing, setEditing] = useState(false);\n const [text, setText] = useState(comment.body);\n const [expanded, setExpanded] = useState(comment.status !== 'RESOLVED');\n const [replyOpen, setReplyOpen] = useState(false);\n const [replyDraft, setReplyDraft] = useState('');\n const [replyingToName, setReplyingToName] = useState<string | null>(null);\n const [replyAttach, setReplyAttach] = useState<CommentAttachmentUploadState>({\n fileIds: [],\n pending: false,\n });\n const [replyAttachResetKey, setReplyAttachResetKey] = useState(0);\n const replyUploadEpochRef = useRef(0);\n\n const resetReplyAttachments = () => {\n replyUploadEpochRef.current += 1;\n setReplyAttach({ fileIds: [], pending: false });\n setReplyAttachResetKey(replyUploadEpochRef.current);\n };\n\n const authorProfile = usePersonProfile(comment.authorId);\n const [edit, editState] = useMutation(editMutation, mutationRefetch);\n const [remove, removeState] = useMutation(removeMutation, mutationRefetch);\n const [resolveComment, resolveState] = useMutation(resolveMutation, mutationRefetch);\n const [reply, replyState] = useMutation(addMutation, mutationRefetch);\n\n const mine = comment.authorId === currentUserId;\n const resolved = comment.status === 'RESOLVED';\n const editTooLong = text.trim().length > MAX_LENGTH;\n const replyTooLong = replyDraft.trim().length > MAX_LENGTH;\n\n const save = async () => {\n onError(null);\n try {\n await throwOnError(edit({ variables: { id: comment.id, body: text.trim() } }));\n setEditing(false);\n } catch (e) {\n onError(mutationErrorMessage(e, 'The comment could not be saved.'));\n }\n };\n\n const onRemove = async () => {\n onError(null);\n try {\n await throwOnError(remove({ variables: { id: comment.id } }));\n } catch (e) {\n onError(mutationErrorMessage(e, 'The comment could not be deleted.'));\n }\n };\n\n const toggleResolve = async () => {\n onError(null);\n try {\n await throwOnError(resolveComment({ variables: { id: comment.id, resolved: !resolved } }));\n } catch (e) {\n onError(\n isForbidden(e)\n ? 'Your role can read but not resolve comments.'\n : mutationErrorMessage(e, 'The resolution could not be changed.')\n );\n }\n };\n\n const openReply = (name: string, mentionId?: string, mentionName?: string) => {\n setReplyingToName(name);\n setReplyDraft(mentionId && mentionName ? `@[${mentionName}](${mentionId}) ` : '');\n setReplyOpen(true);\n setExpanded(true);\n };\n\n const submitReply = async () => {\n const body = replyDraft.trim();\n if (!body) return;\n onError(null);\n try {\n await throwOnError(reply({ variables: buildReplyVariables(body, replyAttach.fileIds) }));\n setReplyDraft('');\n setReplyOpen(false);\n setReplyingToName(null);\n resetReplyAttachments();\n } catch (e) {\n onError(\n isForbidden(e)\n ? 'Your role can read but not comment.'\n : mutationErrorMessage(e, 'The reply could not be posted.')\n );\n }\n };\n\n const rowId = `${idPrefix}-comment-${comment.id}`;\n\n return (\n <li\n className=\"rounded-md border border-border-default p-3 space-y-2\"\n data-testid=\"comment-thread-item\"\n >\n {resolved && !expanded ? (\n <div className=\"flex items-center justify-between gap-2\">\n <p className=\"min-w-0 truncate text-sm text-muted-foreground\">\n <CheckCircle2\n className=\"mr-1 inline h-3.5 w-3.5 text-status-success-text\"\n aria-hidden=\"true\"\n />\n Resolved by <PersonName id={comment.resolvedBy} />\n {comment.resolvedAt ? ` · ${when(comment.resolvedAt)}` : ''}\n {comment.replies.length > 0\n ? ` · ${comment.replies.length} repl${comment.replies.length === 1 ? 'y' : 'ies'}`\n : ''}\n </p>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 shrink-0 px-2 text-xs\"\n aria-expanded={false}\n aria-controls={rowId}\n onClick={() => setExpanded(true)}\n >\n <ChevronRight className=\"mr-1 h-3.5 w-3.5\" aria-hidden=\"true\" />\n Expand\n </Button>\n </div>\n ) : (\n <div id={rowId} className=\"space-y-2\">\n <div className=\"flex items-start justify-between gap-2 text-xs text-muted-foreground\">\n <span>\n <span className=\"font-medium text-foreground\">\n <PersonName id={comment.authorId} />\n </span>{' '}\n · {when(comment.createdAt)}\n {comment.editedAt && (\n <span title={`Edited ${when(comment.editedAt)}`}> · edited</span>\n )}\n {resolved && (\n <span className=\"ml-1 inline-flex items-center gap-1 text-status-success-text\">\n <CheckCircle2 className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n Resolved\n </span>\n )}\n </span>\n <span className=\"flex shrink-0 items-center gap-1\">\n {mine && !editing && (\n <>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 px-2 text-xs\"\n onClick={() => setEditing(true)}\n >\n Edit\n </Button>\n <ConfirmButton\n label=\"\"\n ariaLabel=\"Delete comment\"\n variant=\"outline\"\n icon={<Trash2 className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />}\n title=\"Delete this comment?\"\n description=\"It is removed from the discussion for everyone, replies included. This cannot be undone here.\"\n confirmLabel=\"Delete\"\n destructive\n disabled={removeState.loading}\n onConfirm={() => void onRemove()}\n />\n </>\n )}\n <Button\n size=\"sm\"\n variant={resolved ? 'outline' : 'ghost'}\n className=\"h-6 px-2 text-xs\"\n data-testid=\"comment-resolve\"\n disabled={resolveState.loading}\n onClick={() => void toggleResolve()}\n >\n {resolved ? (\n <>\n <RotateCcw className=\"mr-1 h-3.5 w-3.5\" aria-hidden=\"true\" />\n Reopen\n </>\n ) : (\n <>\n <CheckCircle2 className=\"mr-1 h-3.5 w-3.5\" aria-hidden=\"true\" />\n Resolve\n </>\n )}\n </Button>\n {resolved && (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 px-2 text-xs\"\n aria-expanded\n aria-controls={rowId}\n onClick={() => setExpanded(false)}\n >\n <ChevronDown className=\"mr-1 h-3.5 w-3.5\" aria-hidden=\"true\" />\n Collapse\n </Button>\n )}\n </span>\n </div>\n\n {editing ? (\n <div className=\"space-y-2\">\n <MentionComposer\n id={`${rowId}-edit`}\n ariaLabel=\"Edit comment\"\n className=\"min-h-[60px]\"\n value={text}\n onChange={setText}\n workspaceId={workspaceId}\n authToken={authToken}\n apiGatewayUrl={apiGatewayUrl}\n />\n {editTooLong && (\n <ErrorLine message={`A comment can be at most ${MAX_LENGTH} characters.`} />\n )}\n <div className=\"flex justify-end gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => {\n setEditing(false);\n setText(comment.body);\n onError(null);\n }}\n >\n Cancel\n </Button>\n <Button\n size=\"sm\"\n onClick={() => void save()}\n disabled={!text.trim() || editTooLong || editState.loading}\n >\n {editState.loading ? 'Saving…' : 'Save'}\n </Button>\n </div>\n </div>\n ) : (\n <CommentBody text={comment.body} attachments={comment.attachments} target={target} />\n )}\n\n {!editing && comment.attachments.length > 0 && (\n <CommentAttachments attachments={comment.attachments} target={target} />\n )}\n\n {!editing && (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 px-2 text-xs\"\n data-testid=\"comment-reply\"\n onClick={() =>\n openReply(\n authorProfile?.displayName ?? 'this comment',\n comment.authorId,\n authorProfile?.displayName\n )\n }\n >\n <Reply className=\"mr-1 h-3.5 w-3.5\" aria-hidden=\"true\" />\n Reply\n </Button>\n )}\n\n {comment.replies.length > 0 && (\n <ol className=\"ml-5 space-y-2 border-l border-border-default pl-3\">\n {comment.replies.map((r) => (\n <ReplyItem\n key={r.id}\n reply={r}\n target={target}\n currentUserId={currentUserId}\n editMutation={editMutation}\n removeMutation={removeMutation}\n mutationRefetch={mutationRefetch}\n onError={onError}\n idPrefix={idPrefix}\n workspaceId={workspaceId}\n authToken={authToken}\n apiGatewayUrl={apiGatewayUrl}\n onReply={openReply}\n />\n ))}\n </ol>\n )}\n\n {replyOpen && (\n <div className=\"ml-5 space-y-2 border-l border-border-default pl-3\">\n {replyingToName && (\n <p className=\"text-xs text-muted-foreground\">Replying to {replyingToName}</p>\n )}\n <MentionComposer\n id={`${rowId}-reply`}\n ariaLabel=\"Write a reply\"\n className=\"min-h-[60px]\"\n value={replyDraft}\n disabled={replyState.loading}\n onChange={(value) => {\n setReplyDraft(value);\n onTyping?.();\n }}\n autoFocus\n workspaceId={workspaceId}\n authToken={authToken}\n apiGatewayUrl={apiGatewayUrl}\n />\n {replyTooLong && (\n <ErrorLine message={`A comment can be at most ${MAX_LENGTH} characters.`} />\n )}\n <CommentAttachmentUpload\n key={replyAttachResetKey}\n idPrefix={`${rowId}-reply`}\n disabled={replyState.loading}\n onChange={(state) => {\n if (replyAttachResetKey === replyUploadEpochRef.current) setReplyAttach(state);\n }}\n onInsertImage={(fileId, filename) =>\n setReplyDraft((current) => appendImageEmbed(current, fileId, filename))\n }\n onRemoveImage={(fileId) =>\n setReplyDraft((current) => removeImageEmbed(current, fileId))\n }\n />\n <div className=\"flex justify-end gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={replyState.loading}\n onClick={() => {\n setReplyOpen(false);\n setReplyDraft('');\n setReplyingToName(null);\n // Ignore any callback from the old picker even if its in-flight upload resolves before\n // React runs the unmount cleanup. A reopened Reply gets a fresh upload generation.\n resetReplyAttachments();\n }}\n >\n Cancel\n </Button>\n <Button\n size=\"sm\"\n onClick={() => void submitReply()}\n disabled={\n !replyDraft.trim() || replyTooLong || replyState.loading || replyAttach.pending\n }\n >\n {replyState.loading\n ? 'Posting…'\n : replyAttach.pending\n ? 'Preparing attachments…'\n : 'Reply'}\n </Button>\n </div>\n </div>\n )}\n </div>\n )}\n </li>\n );\n}\n\n/** One reply, indented under its top-level parent. Replies carry no resolution state of their own (only a\n * top-level comment does — CWF-14) and their own Reply action still targets the TOP-LEVEL thread\n * (`onReply` is the parent's `openReply`, called with this reply's own author as the \"replying to\" context). */\nfunction ReplyItem({\n reply,\n target,\n currentUserId,\n editMutation,\n removeMutation,\n mutationRefetch,\n onError,\n idPrefix,\n workspaceId,\n authToken,\n apiGatewayUrl,\n onReply,\n}: RowMutationProps & {\n reply: ThreadComment;\n currentUserId: string | undefined;\n onReply: (name: string, mentionId?: string, mentionName?: string) => void;\n}) {\n const [editing, setEditing] = useState(false);\n const [text, setText] = useState(reply.body);\n const profile = usePersonProfile(reply.authorId);\n const [edit, editState] = useMutation(editMutation, mutationRefetch);\n const [remove, removeState] = useMutation(removeMutation, mutationRefetch);\n\n const mine = reply.authorId === currentUserId;\n const editTooLong = text.trim().length > MAX_LENGTH;\n\n const save = async () => {\n onError(null);\n try {\n await throwOnError(edit({ variables: { id: reply.id, body: text.trim() } }));\n setEditing(false);\n } catch (e) {\n onError(mutationErrorMessage(e, 'The reply could not be saved.'));\n }\n };\n\n const onRemove = async () => {\n onError(null);\n try {\n await throwOnError(remove({ variables: { id: reply.id } }));\n } catch (e) {\n onError(mutationErrorMessage(e, 'The reply could not be deleted.'));\n }\n };\n\n return (\n <li className=\"space-y-1\" data-testid=\"comment-reply-item\">\n <div className=\"flex items-start justify-between gap-2 text-xs text-muted-foreground\">\n <span>\n <span className=\"font-medium text-foreground\">\n <PersonName id={reply.authorId} />\n </span>{' '}\n · {when(reply.createdAt)}\n {reply.editedAt && <span title={`Edited ${when(reply.editedAt)}`}> · edited</span>}\n </span>\n {mine && !editing && (\n <span className=\"flex shrink-0 items-center gap-1\">\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 px-2 text-xs\"\n onClick={() => setEditing(true)}\n >\n Edit\n </Button>\n <ConfirmButton\n label=\"\"\n ariaLabel=\"Delete reply\"\n variant=\"outline\"\n icon={<Trash2 className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />}\n title=\"Delete this reply?\"\n description=\"It is removed from the discussion for everyone. This cannot be undone here.\"\n confirmLabel=\"Delete\"\n destructive\n disabled={removeState.loading}\n onConfirm={() => void onRemove()}\n />\n </span>\n )}\n </div>\n\n {editing ? (\n <div className=\"space-y-2\">\n <MentionComposer\n id={`${idPrefix}-reply-${reply.id}-edit`}\n ariaLabel=\"Edit reply\"\n className=\"min-h-[56px]\"\n value={text}\n onChange={setText}\n workspaceId={workspaceId}\n authToken={authToken}\n apiGatewayUrl={apiGatewayUrl}\n />\n {editTooLong && (\n <ErrorLine message={`A comment can be at most ${MAX_LENGTH} characters.`} />\n )}\n <div className=\"flex justify-end gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => {\n setEditing(false);\n setText(reply.body);\n onError(null);\n }}\n >\n Cancel\n </Button>\n <Button\n size=\"sm\"\n onClick={() => void save()}\n disabled={!text.trim() || editTooLong || editState.loading}\n >\n {editState.loading ? 'Saving…' : 'Save'}\n </Button>\n </div>\n </div>\n ) : (\n <CommentBody text={reply.body} attachments={reply.attachments} target={target} />\n )}\n\n {!editing && reply.attachments.length > 0 && (\n <CommentAttachments attachments={reply.attachments} target={target} />\n )}\n\n {!editing && (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-6 px-2 text-xs\"\n data-testid=\"comment-reply\"\n onClick={() =>\n onReply(profile?.displayName ?? 'this reply', reply.authorId, profile?.displayName)\n }\n >\n <Reply className=\"mr-1 h-3.5 w-3.5\" aria-hidden=\"true\" />\n Reply\n </Button>\n )}\n </li>\n );\n}\n"],"mappings":"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"}
@@ -15,11 +15,4 @@ export interface MentionComposerProps {
15
15
  /** A plain multi-line composer with an @-mention autocomplete overlay. Controlled, like every other composer in
16
16
  * this MFE (`value`/`onChange`) — the mention token is spliced into that same string, never a side channel. */
17
17
  export declare function MentionComposer({ id, value, onChange, placeholder, ariaLabel, disabled, className, autoFocus, workspaceId, authToken, apiGatewayUrl, }: MentionComposerProps): import("react/jsx-runtime").JSX.Element;
18
- /** A comment body as displayed text, with `@[Name](actorId)` tokens rendered as highlighted, live-resolved names
19
- * instead of the raw markup (CWF-13). `actorId` drives the rendering via `PersonName` so a since-renamed member
20
- * shows their current name, not the one captured when the mention was typed. */
21
- export declare function MentionText({ text, className }: {
22
- text: string;
23
- className?: string;
24
- }): import("react/jsx-runtime").JSX.Element;
25
18
  //# sourceMappingURL=MentionComposer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"MentionComposer.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/comments/MentionComposer.tsx"],"names":[],"mappings":"AAwCA,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2FAA2F;IAC3F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;+GAC+G;AAC/G,wBAAgB,eAAe,CAAC,EAC9B,EAAE,EACF,KAAK,EACL,QAAQ,EACR,WAAW,EACX,SAAS,EACT,QAAgB,EAChB,SAAS,EACT,SAAiB,EACjB,WAAW,EACX,SAAS,EACT,aAAa,GACd,EAAE,oBAAoB,2CAiLtB;AAID;;gFAEgF;AAChF,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,2CAwBpF"}
1
+ {"version":3,"file":"MentionComposer.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/comments/MentionComposer.tsx"],"names":[],"mappings":"AAuDA,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2FAA2F;IAC3F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;+GAC+G;AAC/G,wBAAgB,eAAe,CAAC,EAC9B,EAAE,EACF,KAAK,EACL,QAAQ,EACR,WAAW,EACX,SAAS,EACT,QAAgB,EAChB,SAAS,EACT,SAAiB,EACjB,WAAW,EACX,SAAS,EACT,aAAa,GACd,EAAE,oBAAoB,2CAuVtB"}
@@ -1,148 +1,287 @@
1
- import { PersonName as e } from "../people/People.js";
2
- import { Fragment as t, useEffect as n, useMemo as r, useRef as i, useState as a } from "react";
3
- import { jsx as o, jsxs as s } from "react/jsx-runtime";
4
- import { Textarea as c } from "@burdenoff/fe-libs/ui";
5
- import { useWorkspaceDirectory as l } from "@burdenoff/fe-libs/sharing";
1
+ import { useEffect as e, useMemo as t, useRef as n, useState as r } from "react";
2
+ import { Bold as i, Code2 as a, Italic as o, Link2 as s } from "lucide-react";
3
+ import { jsx as c, jsxs as l } from "react/jsx-runtime";
4
+ import { Button as u, Input as d, Label as f, Popover as p, PopoverContent as ee, PopoverTrigger as te, Textarea as m } from "@burdenoff/fe-libs/ui";
5
+ import { useWorkspaceDirectory as h } from "@burdenoff/fe-libs/sharing";
6
6
  //#region src/planmagnet/comments/MentionComposer.tsx
7
- var u = 6, d = /(?:^|\s)@([^\s@]{0,40})$/;
8
- function f(e, t) {
9
- let n = e.slice(0, t), r = d.exec(n);
7
+ var g = 6, _ = /(?:^|\s)@([^\s@]{0,40})$/;
8
+ function v(e, t) {
9
+ let n = e.slice(0, t), r = _.exec(n);
10
10
  return r ? {
11
11
  start: r[0].startsWith("@") ? r.index : r.index + 1,
12
12
  query: r[1]
13
13
  } : null;
14
14
  }
15
- function p({ id: e, value: t, onChange: d, placeholder: p, ariaLabel: m, disabled: h = !1, className: g, autoFocus: _ = !1, workspaceId: v, authToken: y, apiGatewayUrl: b }) {
16
- let x = i(null), S = i(null), [C, w] = a(null), [T, E] = a(0), D = l({
17
- workspaceId: v ?? "",
18
- authToken: y,
19
- apiGatewayUrl: b
20
- }), { search: O, hasMore: k, fetchMore: A } = D;
21
- n(() => {
22
- O(C?.query ?? "");
23
- }, [C?.query, O]), n(() => {
24
- if (S.current !== null && x.current) {
25
- let e = S.current;
26
- S.current = null, x.current.focus(), x.current.setSelectionRange(e, e);
15
+ function y({ id: _, value: y, onChange: x, placeholder: ne, ariaLabel: re, disabled: S = !1, className: C, autoFocus: w = !1, workspaceId: T, authToken: E, apiGatewayUrl: D }) {
16
+ let O = n(null), k = n(null), [A, j] = r(null), [M, N] = r(0), [ie, P] = r(!1), [F, I] = r(""), [L, R] = r(""), [z, B] = r(null), V = h({
17
+ workspaceId: T ?? "",
18
+ authToken: E,
19
+ apiGatewayUrl: D
20
+ }), { search: H, hasMore: U, fetchMore: W } = V;
21
+ e(() => {
22
+ H(A?.query ?? "");
23
+ }, [A?.query, H]), e(() => {
24
+ if (k.current && O.current) {
25
+ let { start: e, end: t } = k.current;
26
+ k.current = null, O.current.focus(), O.current.setSelectionRange(e, t);
27
27
  }
28
- }, [t]);
29
- let j = r(() => C ? D.members.slice(0, u) : [], [C, D.members]), M = C !== null && !h, N = (e, t) => {
30
- w(f(e, t)), E(0);
31
- }, P = (e) => {
32
- if (!C) return;
33
- let n = C.start + 1 + C.query.length, r = `@[${e.name}](${e.id})`, i = `${t.slice(0, C.start)}${r} ${t.slice(n)}`;
34
- S.current = C.start + r.length + 1, w(null), d(i);
35
- }, F = `${e}-mentions`, I = M ? j[T] : void 0;
36
- return /* @__PURE__ */ s("div", {
28
+ }, [y]);
29
+ let G = t(() => A ? V.members.slice(0, g) : [], [A, V.members]), K = A !== null && !S, q = (e, t) => {
30
+ j(v(e, t)), N(0);
31
+ }, J = (e) => {
32
+ if (!A) return;
33
+ let t = A.start + 1 + A.query.length, n = `@[${e.name}](${e.id})`, r = `${y.slice(0, A.start)}${n} ${y.slice(t)}`, i = A.start + n.length + 1;
34
+ k.current = {
35
+ start: i,
36
+ end: i
37
+ }, j(null), x(r);
38
+ }, Y = () => {
39
+ let e = O.current;
40
+ return {
41
+ start: e?.selectionStart ?? y.length,
42
+ end: e?.selectionEnd ?? y.length
43
+ };
44
+ }, X = (e, t, n) => {
45
+ let { start: r, end: i } = Y(), a = i > r ? y.slice(r, i) : n, o = `${y.slice(0, r)}${e}${a}${t}${y.slice(i)}`;
46
+ k.current = {
47
+ start: r + e.length,
48
+ end: r + e.length + a.length
49
+ }, j(null), x(o);
50
+ }, ae = () => {
51
+ let { start: e, end: t } = Y();
52
+ y.slice(e, t).includes("\n") ? X("```\n", "\n```", "code") : X("`", "`", "code");
53
+ }, oe = () => {
54
+ let { start: e, end: t } = Y();
55
+ R(y.slice(e, t)), I(""), B(null), P(!0);
56
+ }, Z = () => {
57
+ let e = F.trim();
58
+ if (!/^https:\/\/[^\s<>"'`]+$/i.test(e)) {
59
+ B("Enter a link starting with https://");
60
+ return;
61
+ }
62
+ let { start: t, end: n } = Y(), r = L.trim() || e;
63
+ if (/[[\]]/.test(r)) {
64
+ B("Link text cannot contain [ or ].");
65
+ return;
66
+ }
67
+ let i = `[${r}](${e})`, a = `${y.slice(0, t)}${i}${y.slice(n)}`, o = t + i.length;
68
+ k.current = {
69
+ start: o,
70
+ end: o
71
+ }, j(null), x(a), P(!1), I(""), R(""), B(null);
72
+ }, Q = `${_}-mentions`, $ = K ? G[M] : void 0;
73
+ return /* @__PURE__ */ l("div", {
37
74
  className: "relative",
38
75
  children: [
39
- /* @__PURE__ */ o(c, {
40
- ref: x,
41
- id: e,
42
- "aria-label": m,
43
- className: g,
44
- placeholder: p,
45
- value: t,
46
- disabled: h,
47
- autoFocus: _,
48
- "aria-expanded": M,
49
- "aria-controls": M ? F : void 0,
76
+ /* @__PURE__ */ l("div", {
77
+ role: "toolbar",
78
+ "aria-label": "Formatting",
79
+ "aria-controls": _,
80
+ className: "mb-1 flex items-center gap-0.5",
81
+ children: [
82
+ /* @__PURE__ */ c(b, {
83
+ icon: /* @__PURE__ */ c(i, {
84
+ className: "h-3.5 w-3.5",
85
+ "aria-hidden": "true"
86
+ }),
87
+ label: "Bold",
88
+ disabled: S,
89
+ onClick: () => X("**", "**", "bold text")
90
+ }),
91
+ /* @__PURE__ */ c(b, {
92
+ icon: /* @__PURE__ */ c(o, {
93
+ className: "h-3.5 w-3.5",
94
+ "aria-hidden": "true"
95
+ }),
96
+ label: "Italic",
97
+ disabled: S,
98
+ onClick: () => X("_", "_", "italic text")
99
+ }),
100
+ /* @__PURE__ */ c(b, {
101
+ icon: /* @__PURE__ */ c(a, {
102
+ className: "h-3.5 w-3.5",
103
+ "aria-hidden": "true"
104
+ }),
105
+ label: "Code",
106
+ disabled: S,
107
+ onClick: ae
108
+ }),
109
+ /* @__PURE__ */ l(p, {
110
+ open: ie,
111
+ onOpenChange: (e) => {
112
+ P(e), e || B(null);
113
+ },
114
+ children: [/* @__PURE__ */ c(te, {
115
+ asChild: !0,
116
+ children: /* @__PURE__ */ c(u, {
117
+ type: "button",
118
+ size: "sm",
119
+ variant: "ghost",
120
+ className: "h-6 w-6 p-0",
121
+ "aria-label": "Link",
122
+ title: "Link",
123
+ disabled: S,
124
+ onClick: oe,
125
+ children: /* @__PURE__ */ c(s, {
126
+ className: "h-3.5 w-3.5",
127
+ "aria-hidden": "true"
128
+ })
129
+ })
130
+ }), /* @__PURE__ */ l(ee, {
131
+ className: "w-72 space-y-2",
132
+ align: "start",
133
+ children: [
134
+ /* @__PURE__ */ l("div", {
135
+ className: "space-y-1",
136
+ children: [/* @__PURE__ */ c(f, {
137
+ htmlFor: `${_}-link-url`,
138
+ children: "Link URL"
139
+ }), /* @__PURE__ */ c(d, {
140
+ id: `${_}-link-url`,
141
+ placeholder: "https://example.com",
142
+ value: F,
143
+ autoFocus: !0,
144
+ onChange: (e) => {
145
+ I(e.target.value), B(null);
146
+ },
147
+ onKeyDown: (e) => {
148
+ e.key === "Enter" && (e.preventDefault(), Z());
149
+ }
150
+ })]
151
+ }),
152
+ /* @__PURE__ */ l("div", {
153
+ className: "space-y-1",
154
+ children: [/* @__PURE__ */ c(f, {
155
+ htmlFor: `${_}-link-label`,
156
+ children: "Link text (optional)"
157
+ }), /* @__PURE__ */ c(d, {
158
+ id: `${_}-link-label`,
159
+ placeholder: F || "Link text",
160
+ value: L,
161
+ onChange: (e) => R(e.target.value),
162
+ onKeyDown: (e) => {
163
+ e.key === "Enter" && (e.preventDefault(), Z());
164
+ }
165
+ })]
166
+ }),
167
+ z && /* @__PURE__ */ c("p", {
168
+ className: "text-xs text-destructive",
169
+ children: z
170
+ }),
171
+ /* @__PURE__ */ c("div", {
172
+ className: "flex justify-end",
173
+ children: /* @__PURE__ */ c(u, {
174
+ type: "button",
175
+ size: "sm",
176
+ onClick: Z,
177
+ disabled: !F.trim(),
178
+ children: "Insert link"
179
+ })
180
+ })
181
+ ]
182
+ })]
183
+ })
184
+ ]
185
+ }),
186
+ /* @__PURE__ */ c(m, {
187
+ ref: O,
188
+ id: _,
189
+ "aria-label": re,
190
+ className: C,
191
+ placeholder: ne,
192
+ value: y,
193
+ disabled: S,
194
+ autoFocus: w,
195
+ "aria-expanded": K,
196
+ "aria-controls": K ? Q : void 0,
50
197
  "aria-haspopup": "listbox",
51
198
  "aria-autocomplete": "list",
52
- "aria-activedescendant": I ? `${F}-${I.id}` : void 0,
199
+ "aria-activedescendant": $ ? `${Q}-${$.id}` : void 0,
53
200
  onChange: (e) => {
54
- d(e.target.value), N(e.target.value, e.target.selectionStart ?? e.target.value.length);
201
+ x(e.target.value), q(e.target.value, e.target.selectionStart ?? e.target.value.length);
55
202
  },
56
203
  onSelect: (e) => {
57
204
  let t = e.currentTarget;
58
- N(t.value, t.selectionStart ?? t.value.length);
205
+ q(t.value, t.selectionStart ?? t.value.length);
59
206
  },
60
- onBlur: () => w(null),
207
+ onBlur: () => j(null),
61
208
  onKeyDown: (e) => {
62
- if (!(!M || j.length === 0)) if (e.key === "ArrowDown") e.preventDefault(), E((e) => (e + 1) % j.length);
63
- else if (e.key === "ArrowUp") e.preventDefault(), E((e) => (e - 1 + j.length) % j.length);
209
+ if (!(!K || G.length === 0)) if (e.key === "ArrowDown") e.preventDefault(), N((e) => (e + 1) % G.length);
210
+ else if (e.key === "ArrowUp") e.preventDefault(), N((e) => (e - 1 + G.length) % G.length);
64
211
  else if (e.key === "Enter" || e.key === "Tab") {
65
- let t = j[T] ?? j[0];
212
+ let t = G[M] ?? G[0];
66
213
  if (!t) return;
67
- e.preventDefault(), P(t);
68
- } else e.key === "Escape" && (e.preventDefault(), w(null));
214
+ e.preventDefault(), J(t);
215
+ } else e.key === "Escape" && (e.preventDefault(), j(null));
69
216
  }
70
217
  }),
71
- M && /* @__PURE__ */ s("div", {
218
+ K && /* @__PURE__ */ l("div", {
72
219
  className: "absolute z-20 mt-1 w-72 overflow-hidden rounded-md border border-border-default bg-bg-surface text-sm shadow-md",
73
- children: [/* @__PURE__ */ s("div", {
220
+ children: [/* @__PURE__ */ l("div", {
74
221
  role: "listbox",
75
- id: F,
222
+ id: Q,
76
223
  "aria-label": "Workspace members",
77
224
  "data-testid": "mention-suggestions",
78
225
  className: "max-h-56 overflow-y-auto p-1",
79
226
  children: [
80
- D.loading && j.length === 0 && /* @__PURE__ */ o("p", {
227
+ V.loading && G.length === 0 && /* @__PURE__ */ c("p", {
81
228
  className: "px-2 py-1.5 text-xs text-muted-foreground",
82
229
  children: "Loading members…"
83
230
  }),
84
- !D.loading && j.length === 0 && /* @__PURE__ */ o("p", {
231
+ !V.loading && G.length === 0 && /* @__PURE__ */ c("p", {
85
232
  className: "px-2 py-1.5 text-xs text-muted-foreground",
86
233
  children: "No matching members."
87
234
  }),
88
- j.map((e, t) => /* @__PURE__ */ s("button", {
235
+ G.map((e, t) => /* @__PURE__ */ l("button", {
89
236
  type: "button",
90
- id: `${F}-${e.id}`,
237
+ id: `${Q}-${e.id}`,
91
238
  role: "option",
92
- "aria-selected": t === T,
93
- className: `flex w-full items-center justify-between gap-2 rounded-sm px-2 py-1.5 text-left ${t === T ? "bg-primary/10 text-primary" : "hover:bg-muted"}`,
239
+ "aria-selected": t === M,
240
+ className: `flex w-full items-center justify-between gap-2 rounded-sm px-2 py-1.5 text-left ${t === M ? "bg-primary/10 text-primary" : "hover:bg-muted"}`,
94
241
  onMouseDown: (t) => {
95
- t.preventDefault(), P(e);
242
+ t.preventDefault(), J(e);
96
243
  },
97
- onMouseEnter: () => E(t),
98
- children: [/* @__PURE__ */ o("span", {
244
+ onMouseEnter: () => N(t),
245
+ children: [/* @__PURE__ */ c("span", {
99
246
  className: "truncate font-medium",
100
247
  children: e.name
101
- }), e.email && /* @__PURE__ */ o("span", {
248
+ }), e.email && /* @__PURE__ */ c("span", {
102
249
  className: "shrink-0 truncate text-xs text-muted-foreground",
103
250
  children: e.email
104
251
  })]
105
252
  }, e.id))
106
253
  ]
107
- }), k && /* @__PURE__ */ o("button", {
254
+ }), U && /* @__PURE__ */ c("button", {
108
255
  type: "button",
109
256
  className: "w-full border-t border-border-default px-2 py-1.5 text-left text-xs text-primary hover:bg-muted hover:underline",
110
257
  onMouseDown: (e) => {
111
- e.preventDefault(), A();
258
+ e.preventDefault(), W();
112
259
  },
113
260
  children: "Show more members"
114
261
  })]
115
262
  }),
116
- /* @__PURE__ */ o("span", {
263
+ /* @__PURE__ */ c("span", {
117
264
  className: "sr-only",
118
265
  "aria-live": "polite",
119
- children: M ? I ? `${I.name} highlighted, ${j.length} member${j.length === 1 ? "" : "s"} matching` : D.loading ? "Loading members" : "No matching members" : ""
266
+ children: K ? $ ? `${$.name} highlighted, ${G.length} member${G.length === 1 ? "" : "s"} matching` : V.loading ? "Loading members" : "No matching members" : ""
120
267
  })
121
268
  ]
122
269
  });
123
270
  }
124
- var m = /@\[([^\]]+)\]\(([^)\s]+)\)/g;
125
- function h({ text: n, className: r }) {
126
- let i = [], a = 0;
127
- for (let e of n.matchAll(m)) {
128
- let t = e.index ?? 0;
129
- t > a && i.push(n.slice(a, t)), i.push({
130
- name: e[1],
131
- actorId: e[2]
132
- }), a = t + e[0].length;
133
- }
134
- return a < n.length && i.push(n.slice(a)), /* @__PURE__ */ o("p", {
135
- className: r ?? "text-sm whitespace-pre-wrap break-words",
136
- children: i.map((n, r) => typeof n == "string" ? /* @__PURE__ */ o(t, { children: n }, r) : /* @__PURE__ */ s("span", {
137
- className: "rounded bg-primary/10 px-1 py-0.5 font-medium text-primary",
138
- children: ["@", /* @__PURE__ */ o(e, {
139
- id: n.actorId,
140
- className: "text-primary"
141
- })]
142
- }, r))
271
+ function b({ icon: e, label: t, onClick: n, disabled: r }) {
272
+ return /* @__PURE__ */ c(u, {
273
+ type: "button",
274
+ size: "sm",
275
+ variant: "ghost",
276
+ className: "h-6 w-6 p-0",
277
+ "aria-label": t,
278
+ title: t,
279
+ disabled: r,
280
+ onClick: n,
281
+ children: e
143
282
  });
144
283
  }
145
284
  //#endregion
146
- export { p as MentionComposer, h as MentionText };
285
+ export { y as MentionComposer };
147
286
 
148
287
  //# sourceMappingURL=MentionComposer.js.map