@ai-matrx/associations 0.4.0 → 0.5.0

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.
@@ -13,6 +13,7 @@ function AssociationsProvider({
13
13
  capture,
14
14
  pickerOverrides,
15
15
  entityDoors,
16
+ authorDisplay,
16
17
  children
17
18
  }) {
18
19
  const notifierWarned = useRef(false);
@@ -26,7 +27,8 @@ function AssociationsProvider({
26
27
  ...windowShell !== void 0 ? { windowShell } : {},
27
28
  ...capture !== void 0 ? { capture } : {},
28
29
  ...pickerOverrides !== void 0 ? { pickerOverrides } : {},
29
- ...entityDoors !== void 0 ? { entityDoors } : {}
30
+ ...entityDoors !== void 0 ? { entityDoors } : {},
31
+ ...authorDisplay !== void 0 ? { authorDisplay } : {}
30
32
  },
31
33
  children
32
34
  }
@@ -766,6 +768,12 @@ var IDLE_CATEGORIES = Object.freeze({
766
768
  fetchedAt: null,
767
769
  error: null
768
770
  });
771
+ var IDLE_COMMENTS = Object.freeze({
772
+ status: "idle",
773
+ comments: Object.freeze([]),
774
+ fetchedAt: null,
775
+ error: null
776
+ });
769
777
 
770
778
  // src/react/hooks/useAssociations.ts
771
779
  var noopSubscribe = () => () => {
@@ -1250,6 +1258,72 @@ function currentUserIdOrNull(store) {
1250
1258
  }
1251
1259
  }
1252
1260
 
1261
+ // src/react/hooks/useComments.ts
1262
+ import { useEffect as useEffect7, useRef as useRef6, useSyncExternalStore as useSyncExternalStore3 } from "react";
1263
+ var noopSubscribe3 = () => () => {
1264
+ };
1265
+ function useComments(args) {
1266
+ const { token, id, autoLoad = true } = args;
1267
+ const store = useAssociationsStore();
1268
+ const key = token && id ? associationsKey(token, id) : null;
1269
+ const entry = useSyncExternalStore3(
1270
+ key ? (cb) => store.subscribeComments(key, cb) : noopSubscribe3,
1271
+ () => store.getComments(token, id ?? ""),
1272
+ () => store.getComments(token, id ?? "")
1273
+ );
1274
+ const loadedKey = useRef6(null);
1275
+ useEffect7(() => {
1276
+ if (!autoLoad || !key || !id) return;
1277
+ if (loadedKey.current === key) return;
1278
+ loadedKey.current = key;
1279
+ void store.loadComments(token, id);
1280
+ }, [autoLoad, store, key, token, id]);
1281
+ let currentUserId = null;
1282
+ try {
1283
+ currentUserId = store.identity.requireUserId();
1284
+ } catch {
1285
+ currentUserId = null;
1286
+ }
1287
+ return {
1288
+ comments: entry.comments,
1289
+ status: entry.status,
1290
+ error: entry.error,
1291
+ fetchedAt: entry.fetchedAt,
1292
+ add: async (body, opts) => {
1293
+ if (!id) return { ok: false, error: "Missing entity id" };
1294
+ return store.addComment({
1295
+ entityType: token,
1296
+ entityId: id,
1297
+ body,
1298
+ ...opts?.parentId !== void 0 ? { parentId: opts.parentId } : {},
1299
+ ...opts?.orgId !== void 0 ? { orgId: opts.orgId } : {}
1300
+ });
1301
+ },
1302
+ edit: async (commentId, body) => {
1303
+ if (!id) return { ok: false, error: "Missing entity id" };
1304
+ return store.editComment({
1305
+ entityType: token,
1306
+ entityId: id,
1307
+ id: commentId,
1308
+ body
1309
+ });
1310
+ },
1311
+ remove: async (commentId) => {
1312
+ if (!id) return { ok: false, error: "Missing entity id" };
1313
+ return store.deleteComment({
1314
+ entityType: token,
1315
+ entityId: id,
1316
+ id: commentId
1317
+ });
1318
+ },
1319
+ reload: async () => {
1320
+ if (!id) return;
1321
+ await store.loadComments(token, id, { force: true });
1322
+ },
1323
+ currentUserId
1324
+ };
1325
+ }
1326
+
1253
1327
  // src/react/hooks/useAssociationEntitySelect.ts
1254
1328
  import { useState as useState5 } from "react";
1255
1329
  function useAssociationEntitySelectAdapter(args) {
@@ -1993,7 +2067,7 @@ function UniversalAssociationPicker(props) {
1993
2067
  }
1994
2068
 
1995
2069
  // src/react/components/AttachedItemsSheet.tsx
1996
- import { useEffect as useEffect7, useState as useState8 } from "react";
2070
+ import { useEffect as useEffect8, useState as useState8 } from "react";
1997
2071
  import { cn as cn5 } from "@ai-matrx/design-system";
1998
2072
 
1999
2073
  // src/react/components/entityDoors.tsx
@@ -2113,7 +2187,7 @@ function AttachedItemsBody({
2113
2187
  const [titleError, setTitleError] = useState8(null);
2114
2188
  const [busyId, setBusyId] = useState8(null);
2115
2189
  const idKey = links.map((l) => l.resourceId).sort().join(",");
2116
- useEffect7(() => {
2190
+ useEffect8(() => {
2117
2191
  if (!enabled) return;
2118
2192
  const ids = idKey ? idKey.split(",") : [];
2119
2193
  if (ids.length === 0) {
@@ -2798,7 +2872,7 @@ function titleize(token) {
2798
2872
  }
2799
2873
 
2800
2874
  // src/react/components/AssociationCaptureToolbar.tsx
2801
- import { useRef as useRef6, useState as useState11 } from "react";
2875
+ import { useRef as useRef7, useState as useState11 } from "react";
2802
2876
  import { Button, cn as cn9, Input as Input3 } from "@ai-matrx/design-system";
2803
2877
  import { Fragment as Fragment4, jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
2804
2878
  function AssociationCaptureToolbar({
@@ -2818,7 +2892,7 @@ function AssociationCaptureToolbar({
2818
2892
  const store = useAssociationsStore();
2819
2893
  const { capture } = useAssociationsUiPorts();
2820
2894
  const notifier = useNotifier();
2821
- const fileInputRef = useRef6(null);
2895
+ const fileInputRef = useRef7(null);
2822
2896
  const [isUploading, setIsUploading] = useState11(false);
2823
2897
  const [isDragOver, setIsDragOver] = useState11(false);
2824
2898
  const [isPicking, setIsPicking] = useState11(false);
@@ -3887,6 +3961,411 @@ function CategoryTagPicker({
3887
3961
  ] })
3888
3962
  ] });
3889
3963
  }
3964
+
3965
+ // src/react/components/CommentThread.tsx
3966
+ import { useState as useState15 } from "react";
3967
+ import { Button as Button3, Skeleton as Skeleton2, cn as cn12 } from "@ai-matrx/design-system";
3968
+
3969
+ // src/react/relativeTime.ts
3970
+ var UNITS = [
3971
+ { unit: "year", ms: 365 * 24 * 60 * 60 * 1e3 },
3972
+ { unit: "month", ms: 30 * 24 * 60 * 60 * 1e3 },
3973
+ { unit: "week", ms: 7 * 24 * 60 * 60 * 1e3 },
3974
+ { unit: "day", ms: 24 * 60 * 60 * 1e3 },
3975
+ { unit: "hour", ms: 60 * 60 * 1e3 },
3976
+ { unit: "minute", ms: 60 * 1e3 }
3977
+ ];
3978
+ function formatRelativeTime(iso, now = Date.now()) {
3979
+ const t = Date.parse(iso);
3980
+ if (Number.isNaN(t)) return iso;
3981
+ const delta = t - now;
3982
+ const magnitude = Math.abs(delta);
3983
+ if (magnitude < 60 * 1e3) return "just now";
3984
+ const rtf = new Intl.RelativeTimeFormat(void 0, { numeric: "auto" });
3985
+ for (const { unit, ms } of UNITS) {
3986
+ if (magnitude >= ms) {
3987
+ return rtf.format(Math.trunc(delta / ms), unit);
3988
+ }
3989
+ }
3990
+ return "just now";
3991
+ }
3992
+
3993
+ // src/react/components/CommentThread.tsx
3994
+ import { Fragment as Fragment5, jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
3995
+ var AUTHOR_DOOR_TOKEN = "user";
3996
+ function resolveAuthor(comment, authorDisplay) {
3997
+ const base2 = {
3998
+ userId: comment.createdBy,
3999
+ email: comment.author.email,
4000
+ displayName: comment.author.displayName,
4001
+ avatarUrl: comment.author.avatarUrl
4002
+ };
4003
+ const enriched = authorDisplay ? authorDisplay(base2) : null;
4004
+ const name = enriched?.displayName ?? base2.displayName ?? base2.email ?? "Unknown user";
4005
+ return {
4006
+ name,
4007
+ avatarUrl: enriched?.avatarUrl !== void 0 && enriched?.avatarUrl !== null ? enriched.avatarUrl : base2.avatarUrl,
4008
+ userId: comment.createdBy
4009
+ };
4010
+ }
4011
+ function AuthorAvatar({ author }) {
4012
+ if (author.avatarUrl) {
4013
+ return /* @__PURE__ */ jsx16(
4014
+ "img",
4015
+ {
4016
+ src: author.avatarUrl,
4017
+ alt: "",
4018
+ className: "h-6 w-6 shrink-0 rounded-full object-cover"
4019
+ }
4020
+ );
4021
+ }
4022
+ return /* @__PURE__ */ jsx16(
4023
+ "span",
4024
+ {
4025
+ "aria-hidden": true,
4026
+ className: "flex h-6 w-6 shrink-0 select-none items-center justify-center rounded-full bg-muted text-[11px] font-medium uppercase text-muted-foreground",
4027
+ children: author.name.trim().charAt(0) || "?"
4028
+ }
4029
+ );
4030
+ }
4031
+ function AuthorName({
4032
+ author,
4033
+ entityDoors
4034
+ }) {
4035
+ const DoorRef2 = entityDoors?.EntityRef;
4036
+ if (DoorRef2 && author.userId) {
4037
+ return /* @__PURE__ */ jsx16(
4038
+ DoorRef2,
4039
+ {
4040
+ token: AUTHOR_DOOR_TOKEN,
4041
+ id: author.userId,
4042
+ name: author.name,
4043
+ showIcon: false,
4044
+ className: "text-sm font-medium text-foreground"
4045
+ }
4046
+ );
4047
+ }
4048
+ return /* @__PURE__ */ jsx16("span", { className: "text-sm font-medium text-foreground", children: author.name });
4049
+ }
4050
+ function CommentComposer({
4051
+ onSubmit,
4052
+ placeholder = "Write a comment\u2026",
4053
+ submitLabel = "Comment",
4054
+ initialValue = "",
4055
+ trailing,
4056
+ autoFocus = false
4057
+ }) {
4058
+ const notifier = useNotifier();
4059
+ const [body, setBody] = useState15(initialValue);
4060
+ const [submitting, setSubmitting] = useState15(false);
4061
+ const [error, setError] = useState15(null);
4062
+ async function submit(e) {
4063
+ e?.preventDefault();
4064
+ const trimmed = body.trim();
4065
+ if (!trimmed || submitting) return;
4066
+ setSubmitting(true);
4067
+ setError(null);
4068
+ const res = await onSubmit(trimmed);
4069
+ setSubmitting(false);
4070
+ if (!res.ok) {
4071
+ const msg = res.error ?? "Could not save the comment";
4072
+ setError(msg);
4073
+ notifier.error(msg);
4074
+ return;
4075
+ }
4076
+ setBody("");
4077
+ }
4078
+ return /* @__PURE__ */ jsxs14("form", { onSubmit: submit, className: "flex flex-col gap-1.5", children: [
4079
+ /* @__PURE__ */ jsx16(
4080
+ "textarea",
4081
+ {
4082
+ value: body,
4083
+ onChange: (e) => setBody(e.target.value),
4084
+ onKeyDown: (e) => {
4085
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void submit();
4086
+ },
4087
+ placeholder,
4088
+ rows: 2,
4089
+ autoFocus,
4090
+ "aria-label": placeholder,
4091
+ className: "w-full resize-y rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
4092
+ }
4093
+ ),
4094
+ error && /* @__PURE__ */ jsx16("p", { role: "alert", className: "text-xs text-destructive", children: error }),
4095
+ /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-2", children: [
4096
+ /* @__PURE__ */ jsxs14(Button3, { type: "submit", size: "sm", disabled: !body.trim() || submitting, children: [
4097
+ submitting && /* @__PURE__ */ jsx16(SpinnerIcon, { className: "mr-1 h-3 w-3 animate-spin" }),
4098
+ submitLabel
4099
+ ] }),
4100
+ trailing
4101
+ ] })
4102
+ ] });
4103
+ }
4104
+ function CommentItem({
4105
+ comment,
4106
+ childrenOf,
4107
+ depth,
4108
+ thread,
4109
+ orgId
4110
+ }) {
4111
+ const { authorDisplay, entityDoors } = useAssociationsUiPorts();
4112
+ const notifier = useNotifier();
4113
+ const [replying, setReplying] = useState15(false);
4114
+ const [editing, setEditing] = useState15(false);
4115
+ const [confirmingDelete, setConfirmingDelete] = useState15(false);
4116
+ const [deleting, setDeleting] = useState15(false);
4117
+ const author = resolveAuthor(comment, authorDisplay);
4118
+ const isOwn = thread.currentUserId !== null && comment.createdBy === thread.currentUserId;
4119
+ const edited = comment.updatedAt !== comment.createdAt;
4120
+ const replies = childrenOf.get(comment.id) ?? [];
4121
+ async function confirmDelete() {
4122
+ setDeleting(true);
4123
+ const res = await thread.remove(comment.id);
4124
+ setDeleting(false);
4125
+ setConfirmingDelete(false);
4126
+ if (!res.ok) {
4127
+ notifier.error(res.error ?? "Could not delete the comment");
4128
+ }
4129
+ }
4130
+ return /* @__PURE__ */ jsxs14(
4131
+ "div",
4132
+ {
4133
+ "data-comment-id": comment.id,
4134
+ className: cn12(
4135
+ "flex flex-col gap-1",
4136
+ depth > 0 && "border-l border-border pl-3"
4137
+ ),
4138
+ children: [
4139
+ /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-2", children: [
4140
+ /* @__PURE__ */ jsx16(AuthorAvatar, { author }),
4141
+ /* @__PURE__ */ jsx16(AuthorName, { author, entityDoors }),
4142
+ /* @__PURE__ */ jsx16(
4143
+ "span",
4144
+ {
4145
+ className: "text-xs text-muted-foreground",
4146
+ title: comment.createdAt,
4147
+ children: formatRelativeTime(comment.createdAt)
4148
+ }
4149
+ ),
4150
+ edited && /* @__PURE__ */ jsx16(
4151
+ "span",
4152
+ {
4153
+ className: "text-xs text-muted-foreground",
4154
+ title: comment.updatedAt,
4155
+ children: "(edited)"
4156
+ }
4157
+ )
4158
+ ] }),
4159
+ editing ? /* @__PURE__ */ jsx16("div", { className: "pl-8", children: /* @__PURE__ */ jsx16(
4160
+ CommentComposer,
4161
+ {
4162
+ initialValue: comment.body,
4163
+ submitLabel: "Save",
4164
+ placeholder: "Edit your comment\u2026",
4165
+ autoFocus: true,
4166
+ onSubmit: async (body) => {
4167
+ const res = await thread.edit(comment.id, body);
4168
+ if (res.ok) setEditing(false);
4169
+ return res;
4170
+ },
4171
+ trailing: /* @__PURE__ */ jsx16(
4172
+ Button3,
4173
+ {
4174
+ type: "button",
4175
+ size: "sm",
4176
+ variant: "ghost",
4177
+ onClick: () => setEditing(false),
4178
+ children: "Cancel"
4179
+ }
4180
+ )
4181
+ }
4182
+ ) }) : /* @__PURE__ */ jsx16("p", { className: "whitespace-pre-wrap pl-8 text-sm text-foreground", children: comment.body }),
4183
+ !editing && /* @__PURE__ */ jsx16("div", { className: "flex items-center gap-1 pl-7", children: confirmingDelete ? /* @__PURE__ */ jsxs14(Fragment5, { children: [
4184
+ /* @__PURE__ */ jsx16("span", { className: "text-xs text-destructive", children: "Deletes this comment for everyone." }),
4185
+ /* @__PURE__ */ jsxs14(
4186
+ Button3,
4187
+ {
4188
+ type: "button",
4189
+ size: "sm",
4190
+ variant: "destructive",
4191
+ disabled: deleting,
4192
+ onClick: () => void confirmDelete(),
4193
+ children: [
4194
+ deleting && /* @__PURE__ */ jsx16(SpinnerIcon, { className: "mr-1 h-3 w-3 animate-spin" }),
4195
+ "Delete"
4196
+ ]
4197
+ }
4198
+ ),
4199
+ /* @__PURE__ */ jsx16(
4200
+ Button3,
4201
+ {
4202
+ type: "button",
4203
+ size: "sm",
4204
+ variant: "ghost",
4205
+ disabled: deleting,
4206
+ onClick: () => setConfirmingDelete(false),
4207
+ children: "Cancel"
4208
+ }
4209
+ )
4210
+ ] }) : /* @__PURE__ */ jsxs14(Fragment5, { children: [
4211
+ /* @__PURE__ */ jsx16(
4212
+ "button",
4213
+ {
4214
+ type: "button",
4215
+ onClick: () => setReplying((v) => !v),
4216
+ className: "rounded px-1.5 py-0.5 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
4217
+ children: "Reply"
4218
+ }
4219
+ ),
4220
+ isOwn && /* @__PURE__ */ jsxs14(Fragment5, { children: [
4221
+ /* @__PURE__ */ jsx16(
4222
+ "button",
4223
+ {
4224
+ type: "button",
4225
+ onClick: () => setEditing(true),
4226
+ className: "rounded px-1.5 py-0.5 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
4227
+ children: "Edit"
4228
+ }
4229
+ ),
4230
+ /* @__PURE__ */ jsx16(
4231
+ "button",
4232
+ {
4233
+ type: "button",
4234
+ onClick: () => setConfirmingDelete(true),
4235
+ className: "rounded px-1.5 py-0.5 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-destructive",
4236
+ children: "Delete"
4237
+ }
4238
+ )
4239
+ ] })
4240
+ ] }) }),
4241
+ replying && /* @__PURE__ */ jsxs14("div", { className: "flex items-start gap-2 pl-8 pt-1", children: [
4242
+ /* @__PURE__ */ jsx16(CornerDownRightIcon, { className: "mt-2 h-3.5 w-3.5 shrink-0 text-muted-foreground" }),
4243
+ /* @__PURE__ */ jsx16("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx16(
4244
+ CommentComposer,
4245
+ {
4246
+ submitLabel: "Reply",
4247
+ placeholder: `Reply to ${author.name}\u2026`,
4248
+ autoFocus: true,
4249
+ onSubmit: async (body) => {
4250
+ const res = await thread.add(body, {
4251
+ parentId: comment.id,
4252
+ orgId: orgId ?? null
4253
+ });
4254
+ if (res.ok) setReplying(false);
4255
+ return res;
4256
+ },
4257
+ trailing: /* @__PURE__ */ jsx16(
4258
+ Button3,
4259
+ {
4260
+ type: "button",
4261
+ size: "sm",
4262
+ variant: "ghost",
4263
+ onClick: () => setReplying(false),
4264
+ children: "Cancel"
4265
+ }
4266
+ )
4267
+ }
4268
+ ) })
4269
+ ] }),
4270
+ replies.length > 0 && /* @__PURE__ */ jsx16("div", { className: "flex flex-col gap-3 pl-8 pt-1", children: replies.map((reply) => /* @__PURE__ */ jsx16(
4271
+ CommentItem,
4272
+ {
4273
+ comment: reply,
4274
+ childrenOf,
4275
+ depth: depth + 1,
4276
+ thread,
4277
+ orgId
4278
+ },
4279
+ reply.id
4280
+ )) })
4281
+ ]
4282
+ }
4283
+ );
4284
+ }
4285
+ function buildCommentTree(comments) {
4286
+ const byId = new Set(comments.map((c) => c.id));
4287
+ const roots = [];
4288
+ const childrenOf = /* @__PURE__ */ new Map();
4289
+ for (const c of comments) {
4290
+ if (c.parentId && byId.has(c.parentId)) {
4291
+ const bucket = childrenOf.get(c.parentId);
4292
+ if (bucket) bucket.push(c);
4293
+ else childrenOf.set(c.parentId, [c]);
4294
+ } else {
4295
+ roots.push(c);
4296
+ }
4297
+ }
4298
+ return { roots, childrenOf };
4299
+ }
4300
+ function CommentThread({
4301
+ token,
4302
+ id,
4303
+ orgId,
4304
+ showHeader = true,
4305
+ className
4306
+ }) {
4307
+ const thread = useComments({ token, id });
4308
+ const { roots, childrenOf } = buildCommentTree(thread.comments);
4309
+ const loading = (thread.status === "loading" || thread.status === "idle") && thread.comments.length === 0;
4310
+ return /* @__PURE__ */ jsxs14("section", { className: cn12("flex flex-col gap-3", className), children: [
4311
+ showHeader && /* @__PURE__ */ jsxs14("header", { className: "flex items-center gap-2", children: [
4312
+ /* @__PURE__ */ jsx16("h3", { className: "text-sm font-semibold text-foreground", children: "Comments" }),
4313
+ /* @__PURE__ */ jsx16("span", { className: "text-xs text-muted-foreground tabular-nums", children: thread.comments.length }),
4314
+ /* @__PURE__ */ jsx16(
4315
+ "button",
4316
+ {
4317
+ type: "button",
4318
+ onClick: () => void thread.reload(),
4319
+ "aria-label": "Refresh comments",
4320
+ title: "Refresh comments",
4321
+ className: "ml-auto flex h-6 w-6 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
4322
+ children: thread.status === "loading" ? /* @__PURE__ */ jsx16(SpinnerIcon, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx16(RefreshIcon, { className: "h-3.5 w-3.5" })
4323
+ }
4324
+ )
4325
+ ] }),
4326
+ thread.error && /* @__PURE__ */ jsxs14(
4327
+ "div",
4328
+ {
4329
+ role: "alert",
4330
+ className: "flex items-center gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive",
4331
+ children: [
4332
+ /* @__PURE__ */ jsx16("span", { className: "min-w-0 flex-1", children: thread.error }),
4333
+ /* @__PURE__ */ jsx16(
4334
+ Button3,
4335
+ {
4336
+ type: "button",
4337
+ size: "sm",
4338
+ variant: "outline",
4339
+ disabled: thread.status === "loading",
4340
+ onClick: () => void thread.reload(),
4341
+ children: "Retry"
4342
+ }
4343
+ )
4344
+ ]
4345
+ }
4346
+ ),
4347
+ loading ? /* @__PURE__ */ jsxs14("div", { className: "flex flex-col gap-3", "aria-hidden": true, "data-testid": "comments-loading", children: [
4348
+ /* @__PURE__ */ jsx16(Skeleton2, { className: "h-12 w-full" }),
4349
+ /* @__PURE__ */ jsx16(Skeleton2, { className: "h-12 w-4/5" })
4350
+ ] }) : roots.length === 0 && !thread.error ? /* @__PURE__ */ jsx16("p", { className: "text-sm text-muted-foreground", children: "No comments yet." }) : /* @__PURE__ */ jsx16("div", { className: "flex flex-col gap-4", children: roots.map((comment) => /* @__PURE__ */ jsx16(
4351
+ CommentItem,
4352
+ {
4353
+ comment,
4354
+ childrenOf,
4355
+ depth: 0,
4356
+ thread,
4357
+ orgId
4358
+ },
4359
+ comment.id
4360
+ )) }),
4361
+ /* @__PURE__ */ jsx16(
4362
+ CommentComposer,
4363
+ {
4364
+ onSubmit: (body) => thread.add(body, { orgId: orgId ?? null })
4365
+ }
4366
+ )
4367
+ ] });
4368
+ }
3890
4369
  export {
3891
4370
  AssociationCandidateBody,
3892
4371
  AssociationCaptureToolbar,
@@ -3902,12 +4381,16 @@ export {
3902
4381
  CaptureToolbarAction,
3903
4382
  CategorySelect,
3904
4383
  CategoryTagPicker,
4384
+ CommentComposer,
4385
+ CommentThread,
3905
4386
  DefaultEntityIcon,
3906
4387
  DoorRef,
3907
4388
  PrimaryEntityProvider,
3908
4389
  UniversalAssociationPicker,
3909
4390
  UnresolvedRef,
3910
4391
  attachedKey,
4392
+ buildCommentTree,
4393
+ formatRelativeTime,
3911
4394
  getContentRoleMeta,
3912
4395
  useAssociationCandidates,
3913
4396
  useAssociationEntitySelectAdapter,
@@ -3915,6 +4398,7 @@ export {
3915
4398
  useAssociationsStore,
3916
4399
  useAssociationsUiPorts,
3917
4400
  useCategories,
4401
+ useComments,
3918
4402
  useContainerLinks,
3919
4403
  useContainerLinksAdapter,
3920
4404
  useEntityRelationships,