@burdenoff/microfe-planmagnet 2026.1001.8 → 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 (49) 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/package.json +3 -2
@@ -0,0 +1,16 @@
1
+ export interface CommentAttachmentUploadState {
2
+ /** Successfully-uploaded file ids, in attach order — pass straight through as `attachmentFileIds`. */
3
+ fileIds: string[];
4
+ /** True while an attachment is queued, uploading, finalizing, failed, or a selection was rejected.
5
+ * The composer must not post until the author retries/removes failures or dismisses a rejected selection. */
6
+ pending: boolean;
7
+ }
8
+ export interface CommentAttachmentUploadProps {
9
+ onChange: (state: CommentAttachmentUploadState) => void;
10
+ onInsertImage?: (fileId: string, filename: string) => void;
11
+ onRemoveImage?: (fileId: string) => void;
12
+ disabled?: boolean;
13
+ idPrefix: string;
14
+ }
15
+ export declare function CommentAttachmentUpload({ onChange, onInsertImage, onRemoveImage, disabled, idPrefix, }: CommentAttachmentUploadProps): import("react/jsx-runtime").JSX.Element;
16
+ //# sourceMappingURL=CommentAttachmentUpload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CommentAttachmentUpload.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/comments/CommentAttachmentUpload.tsx"],"names":[],"mappings":"AA0CA,MAAM,WAAW,4BAA4B;IAC3C,sGAAsG;IACtG,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB;iHAC6G;IAC7G,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,EAAE,CAAC,KAAK,EAAE,4BAA4B,KAAK,IAAI,CAAC;IACxD,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3D,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,uBAAuB,CAAC,EACtC,QAAQ,EACR,aAAa,EACb,aAAa,EACb,QAAQ,EACR,QAAQ,GACT,EAAE,4BAA4B,2CA2P9B"}
@@ -0,0 +1,218 @@
1
+ import { formatBytes as e, mimeTypeIcon as t } from "./fileDisplay.js";
2
+ import { MAX_COMMENT_ATTACHMENT_BYTES as n, UploadCompletionUncertainError as r, commentUploadErrorMessage as i, createCommentAttachmentUploadTransport as a, retryCommentAttachmentCompletion as o, uploadCommentAttachment as s } from "./commentAttachmentUploadClient.js";
3
+ import { useEffect as c, useMemo as l, useRef as u, useState as d } from "react";
4
+ import { Loader2 as f, Paperclip as p, X as m } from "lucide-react";
5
+ import { useApolloClient as h } from "@apollo/client/react";
6
+ import { jsx as g, jsxs as _ } from "react/jsx-runtime";
7
+ import { Button as v } from "@burdenoff/fe-libs/ui";
8
+ //#region src/planmagnet/comments/CommentAttachmentUpload.tsx
9
+ var y = 10, b = 0;
10
+ function x() {
11
+ return b += 1, `pending-attachment-${Date.now()}-${b}`;
12
+ }
13
+ function S({ onChange: t, onInsertImage: f, onRemoveImage: m, disabled: b, idPrefix: S }) {
14
+ let w = h(), T = l(() => a(w), [w]), E = u(null), [D, O] = d([]), [k, A] = d(null), j = u(null), M = u([]), N = u(!1), P = u(!1), F = u(/* @__PURE__ */ new Map());
15
+ c(() => {
16
+ P.current = !0;
17
+ let e = F.current;
18
+ return () => {
19
+ P.current = !1, e.forEach((e) => e.abort()), e.clear();
20
+ };
21
+ }, []);
22
+ let I = (e) => {
23
+ P.current && (M.current = e, O(e), t({
24
+ fileIds: e.flatMap((e) => e.status === "ready" && e.fileId ? [e.fileId] : []),
25
+ pending: j.current !== null || e.some((e) => e.status !== "ready")
26
+ }));
27
+ }, L = async () => {
28
+ if (!N.current) {
29
+ N.current = !0;
30
+ try {
31
+ for (; P.current;) {
32
+ let e = M.current.find((e) => e.status === "queued");
33
+ if (!e) break;
34
+ let t = new AbortController();
35
+ F.current.set(e.tempId, t), I(M.current.map((t) => t.tempId === e.tempId ? {
36
+ ...t,
37
+ status: e.completionSession ? "finalizing" : "uploading"
38
+ } : t));
39
+ try {
40
+ let n = e.completionSession ? await o(e.completionSession, T, t.signal) : await s(e.file, T, t.signal, () => {
41
+ I(M.current.map((t) => t.tempId === e.tempId ? {
42
+ ...t,
43
+ status: "finalizing"
44
+ } : t));
45
+ });
46
+ I(M.current.map((t) => t.tempId === e.tempId ? {
47
+ ...t,
48
+ status: "ready",
49
+ fileId: n,
50
+ error: null,
51
+ completionSession: null
52
+ } : t));
53
+ } catch (n) {
54
+ (!t.signal.aborted || n instanceof r) && I(M.current.map((t) => t.tempId === e.tempId ? {
55
+ ...t,
56
+ status: "error",
57
+ error: i(n),
58
+ completionSession: n instanceof r ? n.session : t.completionSession
59
+ } : t));
60
+ } finally {
61
+ F.current.delete(e.tempId);
62
+ }
63
+ }
64
+ } finally {
65
+ N.current = !1;
66
+ }
67
+ }
68
+ }, R = D.filter((e) => e.status !== "error").length, z = R >= y, B = (t) => {
69
+ if (!t || t.length === 0) return;
70
+ let r = y - M.current.filter((e) => e.status !== "error").length, i = Array.from(t), a = i.filter((e) => e.size <= n), o = a.slice(0, Math.max(0, r)).map((e) => ({
71
+ tempId: x(),
72
+ file: e,
73
+ status: "queued",
74
+ error: null,
75
+ fileId: null,
76
+ completionSession: null
77
+ })), s = i.length !== a.length, c = a.length > o.length, l = [s ? `Each attachment must be at most ${e(104857600)}. The larger files were not attached.` : null, c ? `Only ${y} attachments can be added to one comment. The extra files were not attached.` : null].filter(Boolean).join(" ") || null;
78
+ if (l) {
79
+ let e = j.current;
80
+ j.current = e && !e.includes(l) ? `${e} ${l}` : e ?? l, A(j.current);
81
+ }
82
+ I([...M.current, ...o]), o.length > 0 && L(), E.current && (E.current.value = "");
83
+ }, V = (e) => {
84
+ M.current.filter((e) => e.status !== "error").length >= y || (I(M.current.map((t) => t.tempId === e && t.status === "error" ? {
85
+ ...t,
86
+ status: "queued",
87
+ error: null
88
+ } : t)), L());
89
+ }, H = (e) => {
90
+ let t = M.current.find((t) => t.tempId === e);
91
+ t?.status !== "finalizing" && (F.current.get(e)?.abort(), t?.fileId && t.file.type.startsWith("image/") && m?.(t.fileId), I(M.current.filter((t) => t.tempId !== e)));
92
+ };
93
+ return /* @__PURE__ */ _("div", {
94
+ className: "space-y-1.5",
95
+ children: [
96
+ /* @__PURE__ */ _("div", {
97
+ className: "flex items-center gap-2",
98
+ children: [
99
+ /* @__PURE__ */ g("input", {
100
+ ref: E,
101
+ id: `${S}-attach-input`,
102
+ type: "file",
103
+ multiple: !0,
104
+ className: "sr-only",
105
+ disabled: b || z,
106
+ onChange: (e) => B(e.target.files)
107
+ }),
108
+ /* @__PURE__ */ _(v, {
109
+ type: "button",
110
+ size: "sm",
111
+ variant: "outline",
112
+ className: "h-7 px-2 text-xs",
113
+ disabled: b || z,
114
+ onClick: () => E.current?.click(),
115
+ "data-testid": `${S}-attach-files`,
116
+ children: [/* @__PURE__ */ g(p, {
117
+ className: "mr-1 h-3.5 w-3.5",
118
+ "aria-hidden": "true"
119
+ }), "Attach files"]
120
+ }),
121
+ /* @__PURE__ */ g("span", {
122
+ className: "text-xs text-muted-foreground",
123
+ children: D.length > 0 ? `${R}/${y} attached` : `Up to ${y} files`
124
+ })
125
+ ]
126
+ }),
127
+ k && /* @__PURE__ */ _("p", {
128
+ role: "alert",
129
+ className: "text-xs text-destructive",
130
+ children: [
131
+ k,
132
+ " ",
133
+ /* @__PURE__ */ g("button", {
134
+ type: "button",
135
+ className: "underline",
136
+ onClick: () => {
137
+ j.current = null, A(null), I(M.current);
138
+ },
139
+ children: "Dismiss"
140
+ })
141
+ ]
142
+ }),
143
+ D.length > 0 && /* @__PURE__ */ g("ul", {
144
+ className: "flex flex-wrap gap-1.5",
145
+ "data-testid": `${S}-pending-attachments`,
146
+ "aria-label": "Files attached to this comment",
147
+ children: D.map((e) => /* @__PURE__ */ g("li", { children: /* @__PURE__ */ g(C, {
148
+ item: e,
149
+ disabled: b,
150
+ onInsertImage: f,
151
+ onRetry: () => V(e.tempId),
152
+ retryDisabled: z,
153
+ onRemove: () => H(e.tempId)
154
+ }) }, e.tempId))
155
+ })
156
+ ]
157
+ });
158
+ }
159
+ function C({ item: n, disabled: r, onInsertImage: i, onRetry: a, retryDisabled: o, onRemove: s }) {
160
+ let c = n.status === "error", l = n.status === "ready" && n.file.type.startsWith("image/") ? n.fileId : null;
161
+ return /* @__PURE__ */ _("span", {
162
+ className: `inline-flex max-w-[16rem] items-center gap-1.5 rounded-md border px-2 py-1 text-xs ${c ? "border-destructive/50 text-destructive" : "border-border-default"}`,
163
+ "data-testid": "pending-attachment",
164
+ children: [
165
+ n.status === "uploading" || n.status === "finalizing" ? /* @__PURE__ */ g(f, {
166
+ className: "h-3.5 w-3.5 shrink-0 animate-spin",
167
+ "aria-hidden": "true"
168
+ }) : t(n.file.type || "application/octet-stream"),
169
+ /* @__PURE__ */ g("span", {
170
+ className: "truncate",
171
+ children: n.file.name
172
+ }),
173
+ /* @__PURE__ */ g("span", {
174
+ className: "shrink-0 text-muted-foreground",
175
+ children: e(n.file.size)
176
+ }),
177
+ n.status === "finalizing" && /* @__PURE__ */ g("span", {
178
+ className: "shrink-0",
179
+ children: "Finalizing…"
180
+ }),
181
+ c && /* @__PURE__ */ _("span", {
182
+ className: "shrink-0 truncate",
183
+ role: "alert",
184
+ children: ["· ", n.error ?? "Upload failed"]
185
+ }),
186
+ c && /* @__PURE__ */ g("button", {
187
+ type: "button",
188
+ className: "shrink-0 text-primary hover:underline disabled:opacity-50",
189
+ disabled: r || o,
190
+ onClick: a,
191
+ title: o ? "Remove another attachment to retry" : void 0,
192
+ children: n.completionSession ? "Retry finalizing" : "Retry upload"
193
+ }),
194
+ l && i && /* @__PURE__ */ g("button", {
195
+ type: "button",
196
+ className: "shrink-0 text-primary hover:underline",
197
+ disabled: r,
198
+ onClick: () => i(l, n.file.name),
199
+ children: "Insert image"
200
+ }),
201
+ /* @__PURE__ */ g("button", {
202
+ type: "button",
203
+ "aria-label": `Remove ${n.file.name}`,
204
+ className: "shrink-0 rounded-sm text-muted-foreground hover:text-foreground",
205
+ disabled: r || n.status === "finalizing",
206
+ onClick: s,
207
+ children: /* @__PURE__ */ g(m, {
208
+ className: "h-3 w-3",
209
+ "aria-hidden": "true"
210
+ })
211
+ })
212
+ ]
213
+ });
214
+ }
215
+ //#endregion
216
+ export { S as CommentAttachmentUpload };
217
+
218
+ //# sourceMappingURL=CommentAttachmentUpload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CommentAttachmentUpload.js","names":[],"sources":["../../../src/planmagnet/comments/CommentAttachmentUpload.tsx"],"sourcesContent":["import { useEffect, useMemo, useRef, useState } from 'react';\nimport { useApolloClient } from '@apollo/client/react';\nimport { Loader2, Paperclip, X } from 'lucide-react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { formatBytes, mimeTypeIcon } from './fileDisplay';\nimport {\n MAX_COMMENT_ATTACHMENT_BYTES,\n UploadCompletionUncertainError,\n commentUploadErrorMessage,\n createCommentAttachmentUploadTransport,\n retryCommentAttachmentCompletion,\n uploadCommentAttachment,\n type CompletionSession,\n} from './commentAttachmentUploadClient';\n\n/**\n * CWF-12: Upload add/reply attachments into the creator's PRIVATE Files root, not a shared\n * path-based folder. Files chooses direct or multipart at 5 MiB; completion retries only\n * against the original session. Errors remain visible and block posting until resolved.\n */\n\n/** Mirrors `PlanmagnetAdd*CommentInput`'s server-side ten-file cap. */\nconst MAX_ATTACHMENTS = 10;\n\ntype AttachmentStatus = 'queued' | 'uploading' | 'finalizing' | 'ready' | 'error';\n\ninterface PendingAttachment {\n tempId: string;\n file: File;\n status: AttachmentStatus;\n error: string | null;\n fileId: string | null;\n /** Only retained after an ambiguous completion; retry must never re-upload the bytes. */\n completionSession: CompletionSession | null;\n}\n\nlet tempSeq = 0;\nfunction nextTempId(): string {\n tempSeq += 1;\n return `pending-attachment-${Date.now()}-${tempSeq}`;\n}\n\nexport interface CommentAttachmentUploadState {\n /** Successfully-uploaded file ids, in attach order — pass straight through as `attachmentFileIds`. */\n fileIds: string[];\n /** True while an attachment is queued, uploading, finalizing, failed, or a selection was rejected.\n * The composer must not post until the author retries/removes failures or dismisses a rejected selection. */\n pending: boolean;\n}\n\nexport interface CommentAttachmentUploadProps {\n onChange: (state: CommentAttachmentUploadState) => void;\n onInsertImage?: (fileId: string, filename: string) => void;\n onRemoveImage?: (fileId: string) => void;\n disabled?: boolean;\n idPrefix: string;\n}\n\nexport function CommentAttachmentUpload({\n onChange,\n onInsertImage,\n onRemoveImage,\n disabled,\n idPrefix,\n}: CommentAttachmentUploadProps) {\n const client = useApolloClient();\n const transport = useMemo(() => createCommentAttachmentUploadTransport(client), [client]);\n const inputRef = useRef<HTMLInputElement>(null);\n // `items` is what renders. `itemsRef` mirrors it in lockstep (every `commit` below writes both) purely so the\n // sequential upload loop — running across `await` boundaries, outside any render — always reads what is\n // actually attached right now (including files attached mid-upload, or removed while their own upload is\n // still in flight) instead of a stale closure over the array from when the loop began. `itemsRef` is never\n // read from the render body: `react-hooks/refs` (this repo's full React Compiler lint set) rejects that, and\n // correctly — a ref read during render isn't reactive, so React would have no reason to re-render on it.\n const [items, setItems] = useState<PendingAttachment[]>([]);\n const [selectionError, setSelectionError] = useState<string | null>(null);\n const selectionErrorRef = useRef<string | null>(null);\n const itemsRef = useRef<PendingAttachment[]>([]);\n const uploadingRef = useRef(false);\n const mountedRef = useRef(false);\n const controllersRef = useRef(new Map<string, AbortController>());\n\n useEffect(() => {\n mountedRef.current = true;\n const controllers = controllersRef.current;\n return () => {\n mountedRef.current = false;\n controllers.forEach((controller) => controller.abort());\n controllers.clear();\n };\n }, []);\n\n const commit = (next: PendingAttachment[]) => {\n if (!mountedRef.current) return;\n itemsRef.current = next;\n setItems(next);\n onChange({\n fileIds: next.flatMap((item) =>\n item.status === 'ready' && item.fileId ? [item.fileId] : []\n ),\n pending: selectionErrorRef.current !== null || next.some((item) => item.status !== 'ready'),\n });\n };\n\n const runQueue = async () => {\n if (uploadingRef.current) return; // Already draining — that loop will pick up anything just added.\n uploadingRef.current = true;\n try {\n for (;;) {\n if (!mountedRef.current) break;\n const next = itemsRef.current.find((item) => item.status === 'queued');\n if (!next) break;\n const controller = new AbortController();\n controllersRef.current.set(next.tempId, controller);\n commit(\n itemsRef.current.map(\n (item): PendingAttachment =>\n item.tempId === next.tempId\n ? { ...item, status: next.completionSession ? 'finalizing' : 'uploading' }\n : item\n )\n );\n try {\n const fileId = next.completionSession\n ? await retryCommentAttachmentCompletion(\n next.completionSession,\n transport,\n controller.signal\n )\n : await uploadCommentAttachment(next.file, transport, controller.signal, () => {\n commit(\n itemsRef.current.map(\n (item): PendingAttachment =>\n item.tempId === next.tempId ? { ...item, status: 'finalizing' } : item\n )\n );\n });\n // No-op if the attachment was removed during its upload. Finalization cannot be safely\n // canceled after the mutation has been sent, so Remove is disabled while finalizing.\n commit(\n itemsRef.current.map(\n (item): PendingAttachment =>\n item.tempId === next.tempId\n ? { ...item, status: 'ready', fileId, error: null, completionSession: null }\n : item\n )\n );\n } catch (error) {\n if (!controller.signal.aborted || error instanceof UploadCompletionUncertainError) {\n commit(\n itemsRef.current.map(\n (item): PendingAttachment =>\n item.tempId === next.tempId\n ? {\n ...item,\n status: 'error',\n error: commentUploadErrorMessage(error),\n completionSession:\n error instanceof UploadCompletionUncertainError\n ? error.session\n : item.completionSession,\n }\n : item\n )\n );\n }\n } finally {\n controllersRef.current.delete(next.tempId);\n }\n }\n } finally {\n uploadingRef.current = false;\n }\n };\n\n // Render-time derivation from `items` (state), never from `itemsRef` — see the comment on `itemsRef` above.\n const countedTowardCap = items.filter((i) => i.status !== 'error').length;\n const atCap = countedTowardCap >= MAX_ATTACHMENTS;\n\n const handleFiles = (files: FileList | null) => {\n if (!files || files.length === 0) return;\n // Reads itemsRef (not `items`/`countedTowardCap`) deliberately: this handler can fire again before a\n // previous commit's re-render has landed, and itemsRef is always the latest value, synchronously.\n const roomLeft =\n MAX_ATTACHMENTS - itemsRef.current.filter((item) => item.status !== 'error').length;\n const selected = Array.from(files);\n const allowed = selected.filter((file) => file.size <= MAX_COMMENT_ATTACHMENT_BYTES);\n const queued: PendingAttachment[] = allowed.slice(0, Math.max(0, roomLeft)).map((file) => ({\n tempId: nextTempId(),\n file,\n status: 'queued',\n error: null,\n fileId: null,\n completionSession: null,\n }));\n const rejectedForSize = selected.length !== allowed.length;\n const rejectedForCount = allowed.length > queued.length;\n const rejection =\n [\n rejectedForSize\n ? `Each attachment must be at most ${formatBytes(MAX_COMMENT_ATTACHMENT_BYTES)}. The larger files were not attached.`\n : null,\n rejectedForCount\n ? `Only ${MAX_ATTACHMENTS} attachments can be added to one comment. The extra files were not attached.`\n : null,\n ]\n .filter(Boolean)\n .join(' ') || null;\n // A rejected selection remains visible (and blocks posting) even if a later\n // selection succeeds. The author must explicitly dismiss the omitted files.\n if (rejection) {\n const previous = selectionErrorRef.current;\n selectionErrorRef.current =\n previous && !previous.includes(rejection)\n ? `${previous} ${rejection}`\n : (previous ?? rejection);\n setSelectionError(selectionErrorRef.current);\n }\n commit([...itemsRef.current, ...queued]);\n if (queued.length > 0) void runQueue();\n if (inputRef.current) inputRef.current.value = '';\n };\n\n const retryItem = (tempId: string) => {\n // Errors do not count towards the selection cap, but retrying one must not exceed it.\n if (itemsRef.current.filter((item) => item.status !== 'error').length >= MAX_ATTACHMENTS)\n return;\n commit(\n itemsRef.current.map(\n (item): PendingAttachment =>\n item.tempId === tempId && item.status === 'error'\n ? { ...item, status: 'queued', error: null }\n : item\n )\n );\n void runQueue();\n };\n\n const removeItem = (tempId: string) => {\n const item = itemsRef.current.find((entry) => entry.tempId === tempId);\n if (item?.status === 'finalizing') return; // Finalization may already have committed: never abort it.\n controllersRef.current.get(tempId)?.abort();\n if (item?.fileId && item.file.type.startsWith('image/')) onRemoveImage?.(item.fileId);\n commit(itemsRef.current.filter((entry) => entry.tempId !== tempId));\n };\n\n return (\n <div className=\"space-y-1.5\">\n <div className=\"flex items-center gap-2\">\n <input\n ref={inputRef}\n id={`${idPrefix}-attach-input`}\n type=\"file\"\n multiple\n className=\"sr-only\"\n disabled={disabled || atCap}\n onChange={(e) => handleFiles(e.target.files)}\n />\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n className=\"h-7 px-2 text-xs\"\n disabled={disabled || atCap}\n onClick={() => inputRef.current?.click()}\n data-testid={`${idPrefix}-attach-files`}\n >\n <Paperclip className=\"mr-1 h-3.5 w-3.5\" aria-hidden=\"true\" />\n Attach files\n </Button>\n <span className=\"text-xs text-muted-foreground\">\n {items.length > 0\n ? `${countedTowardCap}/${MAX_ATTACHMENTS} attached`\n : `Up to ${MAX_ATTACHMENTS} files`}\n </span>\n </div>\n\n {selectionError && (\n <p role=\"alert\" className=\"text-xs text-destructive\">\n {selectionError}{' '}\n <button\n type=\"button\"\n className=\"underline\"\n onClick={() => {\n selectionErrorRef.current = null;\n setSelectionError(null);\n commit(itemsRef.current);\n }}\n >\n Dismiss\n </button>\n </p>\n )}\n {items.length > 0 && (\n <ul\n className=\"flex flex-wrap gap-1.5\"\n data-testid={`${idPrefix}-pending-attachments`}\n aria-label=\"Files attached to this comment\"\n >\n {items.map((item) => (\n <li key={item.tempId}>\n <PendingAttachmentChip\n item={item}\n disabled={disabled}\n onInsertImage={onInsertImage}\n onRetry={() => retryItem(item.tempId)}\n retryDisabled={atCap}\n onRemove={() => removeItem(item.tempId)}\n />\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n}\n\nfunction PendingAttachmentChip({\n item,\n disabled,\n onInsertImage,\n onRetry,\n retryDisabled,\n onRemove,\n}: {\n item: PendingAttachment;\n disabled?: boolean;\n onInsertImage?: (fileId: string, filename: string) => void;\n onRetry: () => void;\n retryDisabled: boolean;\n onRemove: () => void;\n}) {\n const errored = item.status === 'error';\n const imageFileId =\n item.status === 'ready' && item.file.type.startsWith('image/') ? item.fileId : null;\n return (\n <span\n className={`inline-flex max-w-[16rem] items-center gap-1.5 rounded-md border px-2 py-1 text-xs ${\n errored ? 'border-destructive/50 text-destructive' : 'border-border-default'\n }`}\n data-testid=\"pending-attachment\"\n >\n {item.status === 'uploading' || item.status === 'finalizing' ? (\n <Loader2 className=\"h-3.5 w-3.5 shrink-0 animate-spin\" aria-hidden=\"true\" />\n ) : (\n mimeTypeIcon(item.file.type || 'application/octet-stream')\n )}\n <span className=\"truncate\">{item.file.name}</span>\n <span className=\"shrink-0 text-muted-foreground\">{formatBytes(item.file.size)}</span>\n {item.status === 'finalizing' && <span className=\"shrink-0\">Finalizing…</span>}\n {errored && (\n <span className=\"shrink-0 truncate\" role=\"alert\">\n · {item.error ?? 'Upload failed'}\n </span>\n )}\n {errored && (\n <button\n type=\"button\"\n className=\"shrink-0 text-primary hover:underline disabled:opacity-50\"\n disabled={disabled || retryDisabled}\n onClick={onRetry}\n title={retryDisabled ? 'Remove another attachment to retry' : undefined}\n >\n {item.completionSession ? 'Retry finalizing' : 'Retry upload'}\n </button>\n )}\n {imageFileId && onInsertImage && (\n <button\n type=\"button\"\n className=\"shrink-0 text-primary hover:underline\"\n disabled={disabled}\n onClick={() => onInsertImage(imageFileId, item.file.name)}\n >\n Insert image\n </button>\n )}\n <button\n type=\"button\"\n aria-label={`Remove ${item.file.name}`}\n className=\"shrink-0 rounded-sm text-muted-foreground hover:text-foreground\"\n disabled={disabled || item.status === 'finalizing'}\n onClick={onRemove}\n >\n <X className=\"h-3 w-3\" aria-hidden=\"true\" />\n </button>\n </span>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import { CommentFileTarget } from './useCommentFileUrl';
2
+
3
+ /** Server-snapshotted metadata; the attachment ID, not its raw Files ID, authorizes each download. */
4
+ export interface CommentAttachmentSummary {
5
+ id: string;
6
+ fileId: string;
7
+ name: string;
8
+ mimeType: string;
9
+ size: number;
10
+ }
11
+ export declare function CommentAttachments({ attachments, target, }: {
12
+ attachments: CommentAttachmentSummary[];
13
+ target: CommentFileTarget;
14
+ }): import("react/jsx-runtime").JSX.Element | null;
15
+ //# sourceMappingURL=CommentAttachments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CommentAttachments.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/comments/CommentAttachments.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAqB,KAAK,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAEhF,sGAAsG;AACtG,MAAM,WAAW,wBAAwB;IACvC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,kBAAkB,CAAC,EACjC,WAAW,EACX,MAAM,GACP,EAAE;IACD,WAAW,EAAE,wBAAwB,EAAE,CAAC;IACxC,MAAM,EAAE,iBAAiB,CAAC;CAC3B,kDAWA"}
@@ -0,0 +1,74 @@
1
+ import { useCommentFileUrl as e } from "./useCommentFileUrl.js";
2
+ import { formatBytes as t, mimeTypeIcon as n } from "./fileDisplay.js";
3
+ import { useEffect as r, useRef as i, useState as a } from "react";
4
+ import { jsx as o, jsxs as s } from "react/jsx-runtime";
5
+ //#region src/planmagnet/comments/CommentAttachments.tsx
6
+ function c({ attachments: e, target: t }) {
7
+ return e.length === 0 ? null : /* @__PURE__ */ o("ul", {
8
+ className: "flex flex-wrap gap-1.5",
9
+ "data-testid": "comment-attachments",
10
+ children: e.map((e) => /* @__PURE__ */ o("li", { children: /* @__PURE__ */ o(l, {
11
+ attachment: e,
12
+ target: t
13
+ }) }, e.id))
14
+ });
15
+ }
16
+ function l({ attachment: c, target: l }) {
17
+ let u = e(), [d, f] = a(!1), [p, m] = a(!1), h = i(null);
18
+ r(() => () => {
19
+ let e = h.current;
20
+ h.current = null, e?.close();
21
+ }, []);
22
+ let g = async () => {
23
+ if (d) return;
24
+ m(!1);
25
+ let e = window.open("about:blank", "_blank");
26
+ if (!e) {
27
+ m(!0);
28
+ return;
29
+ }
30
+ e.opener = null, h.current = e, f(!0);
31
+ try {
32
+ let t = await u(l, c.id);
33
+ h.current === e && !e.closed && e.location.replace(t);
34
+ } catch {
35
+ h.current === e && (e.close(), m(!0));
36
+ } finally {
37
+ h.current === e && (h.current = null, f(!1));
38
+ }
39
+ };
40
+ return /* @__PURE__ */ s("div", {
41
+ className: "space-y-0.5",
42
+ children: [/* @__PURE__ */ s("button", {
43
+ type: "button",
44
+ disabled: d,
45
+ title: c.name,
46
+ "data-testid": "comment-attachment",
47
+ className: "inline-flex max-w-[14rem] items-center gap-1.5 rounded-md border border-border-default px-2 py-1 text-xs hover:bg-muted disabled:opacity-70",
48
+ onClick: () => void g(),
49
+ children: [
50
+ n(c.mimeType),
51
+ /* @__PURE__ */ o("span", {
52
+ className: "truncate",
53
+ children: c.name
54
+ }),
55
+ /* @__PURE__ */ o("span", {
56
+ className: "shrink-0 text-muted-foreground",
57
+ children: t(c.size)
58
+ }),
59
+ d && /* @__PURE__ */ o("span", {
60
+ className: "sr-only",
61
+ children: "Opening…"
62
+ })
63
+ ]
64
+ }), p && /* @__PURE__ */ o("p", {
65
+ role: "alert",
66
+ className: "max-w-[14rem] text-xs text-destructive",
67
+ children: "Attachment unavailable. Check your access and pop-up settings."
68
+ })]
69
+ });
70
+ }
71
+ //#endregion
72
+ export { c as CommentAttachments };
73
+
74
+ //# sourceMappingURL=CommentAttachments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CommentAttachments.js","names":[],"sources":["../../../src/planmagnet/comments/CommentAttachments.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from 'react';\nimport { formatBytes, mimeTypeIcon } from './fileDisplay';\nimport { useCommentFileUrl, type CommentFileTarget } from './useCommentFileUrl';\n\n/** Server-snapshotted metadata; the attachment ID, not its raw Files ID, authorizes each download. */\nexport interface CommentAttachmentSummary {\n id: string;\n fileId: string;\n name: string;\n mimeType: string;\n size: number;\n}\n\nexport function CommentAttachments({\n attachments,\n target,\n}: {\n attachments: CommentAttachmentSummary[];\n target: CommentFileTarget;\n}) {\n if (attachments.length === 0) return null;\n return (\n <ul className=\"flex flex-wrap gap-1.5\" data-testid=\"comment-attachments\">\n {attachments.map((attachment) => (\n <li key={attachment.id}>\n <CommentAttachmentLink attachment={attachment} target={target} />\n </li>\n ))}\n </ul>\n );\n}\n\nfunction CommentAttachmentLink({\n attachment,\n target,\n}: {\n attachment: CommentAttachmentSummary;\n target: CommentFileTarget;\n}) {\n const getUrl = useCommentFileUrl();\n const [opening, setOpening] = useState(false);\n const [error, setError] = useState(false);\n const pendingTabRef = useRef<Window | null>(null);\n\n useEffect(() => {\n return () => {\n // Navigating away must not leave a blank tab that a finished request could still populate.\n const tab = pendingTabRef.current;\n pendingTabRef.current = null;\n tab?.close();\n };\n }, []);\n\n const open = async () => {\n if (opening) return;\n setError(false);\n // Open in the user's click gesture; browsers often block window.open after an awaited GraphQL request.\n // Sever the opener before making a network request, then navigate only after fresh authorization succeeds.\n const tab = window.open('about:blank', '_blank');\n if (!tab) {\n setError(true);\n return;\n }\n tab.opener = null;\n pendingTabRef.current = tab;\n setOpening(true);\n try {\n const url = await getUrl(target, attachment.id);\n if (pendingTabRef.current === tab && !tab.closed) tab.location.replace(url);\n } catch {\n if (pendingTabRef.current === tab) {\n tab.close();\n setError(true);\n }\n } finally {\n if (pendingTabRef.current === tab) {\n pendingTabRef.current = null;\n setOpening(false);\n }\n }\n };\n\n return (\n <div className=\"space-y-0.5\">\n <button\n type=\"button\"\n disabled={opening}\n title={attachment.name}\n data-testid=\"comment-attachment\"\n className=\"inline-flex max-w-[14rem] items-center gap-1.5 rounded-md border border-border-default px-2 py-1 text-xs hover:bg-muted disabled:opacity-70\"\n onClick={() => void open()}\n >\n {mimeTypeIcon(attachment.mimeType)}\n <span className=\"truncate\">{attachment.name}</span>\n <span className=\"shrink-0 text-muted-foreground\">{formatBytes(attachment.size)}</span>\n {opening && <span className=\"sr-only\">Opening…</span>}\n </button>\n {error && (\n <p role=\"alert\" className=\"max-w-[14rem] text-xs text-destructive\">\n Attachment unavailable. Check your access and pop-up settings.\n </p>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;AAaA,SAAgB,EAAmB,EACjC,gBACA,aAIC;AAED,QADI,EAAY,WAAW,IAAU,OAEnC,kBAAC,MAAD;EAAI,WAAU;EAAyB,eAAY;YAChD,EAAY,KAAK,MAChB,kBAAC,MAAD,EAAA,UACE,kBAAC,GAAD;GAAmC;GAAoB;GAAU,CAAA,EAC9D,EAFI,EAAW,GAEf,CACL;EACC,CAAA;;AAIT,SAAS,EAAsB,EAC7B,eACA,aAIC;CACD,IAAM,IAAS,GAAmB,EAC5B,CAAC,GAAS,KAAc,EAAS,GAAM,EACvC,CAAC,GAAO,KAAY,EAAS,GAAM,EACnC,IAAgB,EAAsB,KAAK;AAEjD,eACe;EAEX,IAAM,IAAM,EAAc;AAE1B,EADA,EAAc,UAAU,MACxB,GAAK,OAAO;IAEb,EAAE,CAAC;CAEN,IAAM,IAAO,YAAY;AACvB,MAAI,EAAS;AACb,IAAS,GAAM;EAGf,IAAM,IAAM,OAAO,KAAK,eAAe,SAAS;AAChD,MAAI,CAAC,GAAK;AACR,KAAS,GAAK;AACd;;AAIF,EAFA,EAAI,SAAS,MACb,EAAc,UAAU,GACxB,EAAW,GAAK;AAChB,MAAI;GACF,IAAM,IAAM,MAAM,EAAO,GAAQ,EAAW,GAAG;AAC/C,GAAI,EAAc,YAAY,KAAO,CAAC,EAAI,UAAQ,EAAI,SAAS,QAAQ,EAAI;UACrE;AACN,GAAI,EAAc,YAAY,MAC5B,EAAI,OAAO,EACX,EAAS,GAAK;YAER;AACR,GAAI,EAAc,YAAY,MAC5B,EAAc,UAAU,MACxB,EAAW,GAAM;;;AAKvB,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,UAAD;GACE,MAAK;GACL,UAAU;GACV,OAAO,EAAW;GAClB,eAAY;GACZ,WAAU;GACV,eAAe,KAAK,GAAM;aAN5B;IAQG,EAAa,EAAW,SAAS;IAClC,kBAAC,QAAD;KAAM,WAAU;eAAY,EAAW;KAAY,CAAA;IACnD,kBAAC,QAAD;KAAM,WAAU;eAAkC,EAAY,EAAW,KAAK;KAAQ,CAAA;IACrF,KAAW,kBAAC,QAAD;KAAM,WAAU;eAAU;KAAe,CAAA;IAC9C;MACR,KACC,kBAAC,KAAD;GAAG,MAAK;GAAQ,WAAU;aAAyC;GAE/D,CAAA,CAEF"}
@@ -0,0 +1,11 @@
1
+ import { CommentFileTarget } from './useCommentFileUrl';
2
+ import { CommentAttachmentSummary } from './CommentAttachments';
3
+
4
+ export interface CommentBodyProps {
5
+ text: string;
6
+ attachments: CommentAttachmentSummary[];
7
+ target: CommentFileTarget;
8
+ className?: string;
9
+ }
10
+ export declare function CommentBody({ text, attachments, target, className }: CommentBodyProps): import("react/jsx-runtime").JSX.Element;
11
+ //# sourceMappingURL=CommentBody.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CommentBody.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/comments/CommentBody.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAqB,KAAK,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAChF,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAkKrE,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,wBAAwB,EAAE,CAAC;IACxC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,gBAAgB,2CAsBrF"}
@@ -0,0 +1,130 @@
1
+ import { PersonName as e } from "../people/People.js";
2
+ import { tokenizeCommentBody as t } from "./commentTokens.js";
3
+ import { useCommentFileUrl as n } from "./useCommentFileUrl.js";
4
+ import { Fragment as r, useEffect as i, useRef as a, useState as o } from "react";
5
+ import { ImageOff as s } from "lucide-react";
6
+ import { Fragment as c, jsx as l, jsxs as u } from "react/jsx-runtime";
7
+ //#region src/planmagnet/comments/CommentBody.tsx
8
+ function d({ token: t, attachment: n, target: r }) {
9
+ switch (t.kind) {
10
+ case "text": return /* @__PURE__ */ l(c, { children: t.value });
11
+ case "mention": return /* @__PURE__ */ u("span", {
12
+ className: "rounded bg-primary/10 px-1 py-0.5 font-medium text-primary",
13
+ children: ["@", /* @__PURE__ */ l(e, {
14
+ id: t.actorId,
15
+ className: "text-primary"
16
+ })]
17
+ });
18
+ case "link": return /* @__PURE__ */ l("a", {
19
+ href: t.url,
20
+ target: "_blank",
21
+ rel: "noopener noreferrer nofollow",
22
+ className: "text-primary underline underline-offset-2 hover:no-underline",
23
+ children: t.text
24
+ });
25
+ case "image": return n?.mimeType.startsWith("image/") ? /* @__PURE__ */ l(p, {
26
+ attachment: n,
27
+ target: r,
28
+ alt: t.alt
29
+ }, n.id) : /* @__PURE__ */ l(f, { alt: t.alt });
30
+ case "inline-code": return /* @__PURE__ */ l("code", {
31
+ className: "rounded bg-muted px-1 py-0.5 font-mono text-[0.85em]",
32
+ children: t.value
33
+ });
34
+ case "code-block": return /* @__PURE__ */ l("pre", {
35
+ className: "my-1 overflow-x-auto rounded-md bg-muted p-2 font-mono text-[0.85em] whitespace-pre",
36
+ children: /* @__PURE__ */ l("code", { children: t.value })
37
+ });
38
+ case "bold": return /* @__PURE__ */ l("strong", {
39
+ className: "font-semibold",
40
+ children: t.value
41
+ });
42
+ case "italic": return /* @__PURE__ */ l("em", { children: t.value });
43
+ }
44
+ }
45
+ function f({ alt: e }) {
46
+ return /* @__PURE__ */ u("span", {
47
+ className: "my-1 inline-flex items-center gap-1 rounded-md border border-border-default px-2 py-1 align-bottom text-xs text-muted-foreground",
48
+ children: [/* @__PURE__ */ l(s, {
49
+ className: "h-3.5 w-3.5",
50
+ "aria-hidden": "true"
51
+ }), e || "Image unavailable"]
52
+ });
53
+ }
54
+ function p({ attachment: e, target: t, alt: r }) {
55
+ let s = n(), c = t.type, u = t.id, d = a(null), [p, m] = o(!1), [h, g] = o(null), [_, v] = o(!1), [y, b] = o(0);
56
+ return i(() => {
57
+ let e = d.current;
58
+ if (!e) return;
59
+ if (typeof IntersectionObserver > "u") {
60
+ let e = !0;
61
+ return queueMicrotask(() => {
62
+ e && m(!0);
63
+ }), () => {
64
+ e = !1;
65
+ };
66
+ }
67
+ let t = new IntersectionObserver((e) => {
68
+ e.some((e) => e.isIntersecting) && (m(!0), t.disconnect());
69
+ }, { rootMargin: "256px" });
70
+ return t.observe(e), () => t.disconnect();
71
+ }, []), i(() => {
72
+ if (!p) return;
73
+ let t = !0;
74
+ return s({
75
+ type: c,
76
+ id: u
77
+ }, e.id).then((e) => {
78
+ t && g(e);
79
+ }, () => {
80
+ t && v(!0);
81
+ }), () => {
82
+ t = !1;
83
+ };
84
+ }, [
85
+ p,
86
+ s,
87
+ c,
88
+ u,
89
+ e.id,
90
+ y
91
+ ]), /* @__PURE__ */ l("span", {
92
+ ref: d,
93
+ children: _ ? /* @__PURE__ */ l(f, { alt: r }) : h ? /* @__PURE__ */ l("img", {
94
+ src: h,
95
+ alt: r,
96
+ className: "my-1 max-h-64 max-w-full rounded-md border border-border-default object-contain align-bottom",
97
+ loading: "eager",
98
+ onError: () => {
99
+ y === 0 ? (g(null), b(1)) : v(!0);
100
+ }
101
+ }) : /* @__PURE__ */ l("span", {
102
+ className: "my-1 inline-flex h-20 w-32 animate-pulse items-center justify-center rounded-md bg-muted align-bottom text-xs text-muted-foreground",
103
+ "aria-hidden": "true"
104
+ })
105
+ });
106
+ }
107
+ function m({ text: e, attachments: n, target: i, className: a }) {
108
+ let o;
109
+ try {
110
+ o = t(e);
111
+ } catch {
112
+ o = [{
113
+ kind: "text",
114
+ value: e
115
+ }];
116
+ }
117
+ let s = new Map(n.map((e) => [e.fileId.toLowerCase(), e]));
118
+ return /* @__PURE__ */ l("div", {
119
+ className: a ?? "text-sm whitespace-pre-wrap break-words",
120
+ children: o.map((e, t) => /* @__PURE__ */ l(r, { children: /* @__PURE__ */ l(d, {
121
+ token: e,
122
+ attachment: e.kind === "image" ? s.get(e.fileId.toLowerCase()) : void 0,
123
+ target: i
124
+ }) }, t))
125
+ });
126
+ }
127
+ //#endregion
128
+ export { m as CommentBody };
129
+
130
+ //# sourceMappingURL=CommentBody.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CommentBody.js","names":[],"sources":["../../../src/planmagnet/comments/CommentBody.tsx"],"sourcesContent":["import { Fragment, useEffect, useRef, useState } from 'react';\nimport { ImageOff } from 'lucide-react';\nimport { PersonName } from '../people/People';\nimport { tokenizeCommentBody, type CommentToken } from './commentTokens';\nimport { useCommentFileUrl, type CommentFileTarget } from './useCommentFileUrl';\nimport type { CommentAttachmentSummary } from './CommentAttachments';\n\n/** Render only the server's constrained plain-text grammar; raw HTML is always inert React text. */\nfunction TokenView({\n token,\n attachment,\n target,\n}: {\n token: CommentToken;\n attachment?: CommentAttachmentSummary;\n target: CommentFileTarget;\n}) {\n switch (token.kind) {\n case 'text':\n return <>{token.value}</>;\n case 'mention':\n return (\n <span className=\"rounded bg-primary/10 px-1 py-0.5 font-medium text-primary\">\n @<PersonName id={token.actorId} className=\"text-primary\" />\n </span>\n );\n case 'link':\n return (\n <a\n href={token.url}\n target=\"_blank\"\n rel=\"noopener noreferrer nofollow\"\n className=\"text-primary underline underline-offset-2 hover:no-underline\"\n >\n {token.text}\n </a>\n );\n case 'image':\n // A text token is not itself a grant to read any Files ID. Only this comment's attachment row\n // permits a download; an unbound fileId in a hand-edited body remains inert.\n return attachment?.mimeType.startsWith('image/') ? (\n <CommentBodyImage\n key={attachment.id}\n attachment={attachment}\n target={target}\n alt={token.alt}\n />\n ) : (\n <UnavailableImage alt={token.alt} />\n );\n case 'inline-code':\n return (\n <code className=\"rounded bg-muted px-1 py-0.5 font-mono text-[0.85em]\">{token.value}</code>\n );\n case 'code-block':\n return (\n <pre className=\"my-1 overflow-x-auto rounded-md bg-muted p-2 font-mono text-[0.85em] whitespace-pre\">\n <code>{token.value}</code>\n </pre>\n );\n case 'bold':\n return <strong className=\"font-semibold\">{token.value}</strong>;\n case 'italic':\n return <em>{token.value}</em>;\n }\n}\n\nfunction UnavailableImage({ alt }: { alt: string }) {\n return (\n <span className=\"my-1 inline-flex items-center gap-1 rounded-md border border-border-default px-2 py-1 align-bottom text-xs text-muted-foreground\">\n <ImageOff className=\"h-3.5 w-3.5\" aria-hidden=\"true\" />\n {alt || 'Image unavailable'}\n </span>\n );\n}\n\n/** Fetch a fresh, short-lived URL only as an attached image enters the viewport. */\nfunction CommentBodyImage({\n attachment,\n target,\n alt,\n}: {\n attachment: CommentAttachmentSummary;\n target: CommentFileTarget;\n alt: string;\n}) {\n const getUrl = useCommentFileUrl();\n const targetType = target.type;\n const targetId = target.id;\n const containerRef = useRef<HTMLSpanElement>(null);\n const [visible, setVisible] = useState(false);\n const [url, setUrl] = useState<string | null>(null);\n const [failed, setFailed] = useState(false);\n const [retry, setRetry] = useState(0);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n if (typeof IntersectionObserver === 'undefined') {\n let active = true;\n queueMicrotask(() => {\n if (active) setVisible(true);\n });\n return () => {\n active = false;\n };\n }\n const observer = new IntersectionObserver(\n (entries) => {\n if (entries.some((entry) => entry.isIntersecting)) {\n setVisible(true);\n observer.disconnect();\n }\n },\n { rootMargin: '256px' }\n );\n observer.observe(container);\n return () => observer.disconnect();\n }, []);\n\n useEffect(() => {\n if (!visible) return;\n let active = true;\n void getUrl({ type: targetType, id: targetId }, attachment.id).then(\n (next) => {\n if (active) setUrl(next);\n },\n () => {\n if (active) setFailed(true);\n }\n );\n return () => {\n active = false;\n };\n }, [visible, getUrl, targetType, targetId, attachment.id, retry]);\n\n return (\n <span ref={containerRef}>\n {failed ? (\n <UnavailableImage alt={alt} />\n ) : url ? (\n <img\n src={url}\n alt={alt}\n className=\"my-1 max-h-64 max-w-full rounded-md border border-border-default object-contain align-bottom\"\n loading=\"eager\"\n onError={() => {\n if (retry === 0) {\n // The browser may have delayed an image request until its presign expired. Retry once via\n // the comment-reader query, never by reusing a cached URL or looping forever on failure.\n setUrl(null);\n setRetry(1);\n } else {\n setFailed(true);\n }\n }}\n />\n ) : (\n <span\n className=\"my-1 inline-flex h-20 w-32 animate-pulse items-center justify-center rounded-md bg-muted align-bottom text-xs text-muted-foreground\"\n aria-hidden=\"true\"\n />\n )}\n </span>\n );\n}\n\nexport interface CommentBodyProps {\n text: string;\n attachments: CommentAttachmentSummary[];\n target: CommentFileTarget;\n className?: string;\n}\n\nexport function CommentBody({ text, attachments, target, className }: CommentBodyProps) {\n let tokens: CommentToken[];\n try {\n tokens = tokenizeCommentBody(text);\n } catch {\n tokens = [{ kind: 'text', value: text }];\n }\n const files = new Map(attachments.map((item) => [item.fileId.toLowerCase(), item]));\n\n return (\n <div className={className ?? 'text-sm whitespace-pre-wrap break-words'}>\n {tokens.map((token, index) => (\n <Fragment key={index}>\n <TokenView\n token={token}\n attachment={token.kind === 'image' ? files.get(token.fileId.toLowerCase()) : undefined}\n target={target}\n />\n </Fragment>\n ))}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1,4 +1,6 @@
1
1
  import { DocumentNode } from 'graphql';
2
+ import { CommentAttachmentSummary } from './CommentAttachments';
3
+ import { CommentFileTarget } from './useCommentFileUrl';
2
4
 
3
5
  type CommentStatus = 'OPEN' | 'RESOLVED';
4
6
  export interface ThreadComment {
@@ -12,9 +14,12 @@ export interface ThreadComment {
12
14
  resolvedBy: string | null;
13
15
  resolvedAt: string | null;
14
16
  mentions: string[];
17
+ attachments: CommentAttachmentSummary[];
15
18
  replies: ThreadComment[];
16
19
  }
17
20
  export interface CommentThreadProps {
21
+ /** Verified by the matching product-read resource before an attachment can be downloaded. */
22
+ target: CommentFileTarget;
18
23
  /** What this thread is attached to, used only in accessible copy ("Add a comment on this task…"). */
19
24
  targetNoun: string;
20
25
  /** Seeds every form-control id this instance renders, so two threads on the same page never collide. */
@@ -28,7 +33,7 @@ export interface CommentThreadProps {
28
33
  };
29
34
  add: {
30
35
  document: DocumentNode;
31
- buildVariables: (body: string, parentId: string | null) => Record<string, unknown>;
36
+ buildVariables: (body: string, parentId: string | null, attachmentFileIds: string[]) => Record<string, unknown>;
32
37
  };
33
38
  edit: DocumentNode;
34
39
  remove: DocumentNode;
@@ -46,6 +51,6 @@ export interface CommentThreadProps {
46
51
  notifyTyping: () => void;
47
52
  };
48
53
  }
49
- export declare function CommentThread({ targetNoun, idPrefix, query, add, edit, remove, resolve, refetchQueries, onTotalCount, presence, }: CommentThreadProps): import("react/jsx-runtime").JSX.Element;
54
+ export declare function CommentThread({ target, targetNoun, idPrefix, query, add, edit, remove, resolve, refetchQueries, onTotalCount, presence, }: CommentThreadProps): import("react/jsx-runtime").JSX.Element;
50
55
  export {};
51
56
  //# sourceMappingURL=CommentThread.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"CommentThread.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/comments/CommentThread.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAqC5C,KAAK,aAAa,GAAG,MAAM,GAAG,UAAU,CAAC;AAEzC,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,aAAa,CAAC;IACtB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B;AAUD,MAAM,WAAW,kBAAkB;IACjC,qGAAqG;IACrG,UAAU,EAAE,MAAM,CAAC;IACnB,wGAAwG;IACxG,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE;QACL,QAAQ,EAAE,YAAY,CAAC;QACvB,0FAA0F;QAC1F,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACnC,IAAI,CAAC,EAAE,OAAO,CAAC;KAChB,CAAC;IACF,GAAG,EAAE;QACH,QAAQ,EAAE,YAAY,CAAC;QACvB,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACpF,CAAC;IACF,IAAI,EAAE,YAAY,CAAC;IACnB,MAAM,EAAE,YAAY,CAAC;IACrB,OAAO,EAAE,YAAY,CAAC;IACtB;mEAC+D;IAC/D,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,8FAA8F;IAC9F,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC;;uGAEmG;IACnG,QAAQ,CAAC,EAAE;QACT,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;QAClC,YAAY,EAAE,MAAM,IAAI,CAAC;KAC1B,CAAC;CACH;AAID,wBAAgB,aAAa,CAAC,EAC5B,UAAU,EACV,QAAQ,EACR,KAAK,EACL,GAAG,EACH,IAAI,EACJ,MAAM,EACN,OAAO,EACP,cAAc,EACd,YAAY,EACZ,QAAQ,GACT,EAAE,kBAAkB,2CA+KpB"}
1
+ {"version":3,"file":"CommentThread.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/comments/CommentThread.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAY5C,OAAO,EAAsB,KAAK,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AACzF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAoC7D,KAAK,aAAa,GAAG,MAAM,GAAG,UAAU,CAAC;AAEzC,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,aAAa,CAAC;IACtB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,WAAW,EAAE,wBAAwB,EAAE,CAAC;IACxC,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B;AAUD,MAAM,WAAW,kBAAkB;IACjC,6FAA6F;IAC7F,MAAM,EAAE,iBAAiB,CAAC;IAC1B,qGAAqG;IACrG,UAAU,EAAE,MAAM,CAAC;IACnB,wGAAwG;IACxG,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE;QACL,QAAQ,EAAE,YAAY,CAAC;QACvB,0FAA0F;QAC1F,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACnC,IAAI,CAAC,EAAE,OAAO,CAAC;KAChB,CAAC;IACF,GAAG,EAAE;QACH,QAAQ,EAAE,YAAY,CAAC;QACvB,cAAc,EAAE,CACd,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,iBAAiB,EAAE,MAAM,EAAE,KACxB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KAC9B,CAAC;IACF,IAAI,EAAE,YAAY,CAAC;IACnB,MAAM,EAAE,YAAY,CAAC;IACrB,OAAO,EAAE,YAAY,CAAC;IACtB;mEAC+D;IAC/D,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,8FAA8F;IAC9F,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC;;uGAEmG;IACnG,QAAQ,CAAC,EAAE;QACT,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;QAClC,YAAY,EAAE,MAAM,IAAI,CAAC;KAC1B,CAAC;CACH;AAID,wBAAgB,aAAa,CAAC,EAC5B,MAAM,EACN,UAAU,EACV,QAAQ,EACR,KAAK,EACL,GAAG,EACH,IAAI,EACJ,MAAM,EACN,OAAO,EACP,cAAc,EACd,YAAY,EACZ,QAAQ,GACT,EAAE,kBAAkB,2CAkNpB"}