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