@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.
@@ -35,12 +35,16 @@ __export(react_exports, {
35
35
  CaptureToolbarAction: () => CaptureToolbarAction,
36
36
  CategorySelect: () => CategorySelect,
37
37
  CategoryTagPicker: () => CategoryTagPicker,
38
+ CommentComposer: () => CommentComposer,
39
+ CommentThread: () => CommentThread,
38
40
  DefaultEntityIcon: () => DefaultEntityIcon,
39
41
  DoorRef: () => DoorRef,
40
42
  PrimaryEntityProvider: () => PrimaryEntityProvider,
41
43
  UniversalAssociationPicker: () => UniversalAssociationPicker,
42
44
  UnresolvedRef: () => UnresolvedRef,
43
45
  attachedKey: () => attachedKey,
46
+ buildCommentTree: () => buildCommentTree,
47
+ formatRelativeTime: () => formatRelativeTime,
44
48
  getContentRoleMeta: () => getContentRoleMeta,
45
49
  useAssociationCandidates: () => useAssociationCandidates,
46
50
  useAssociationEntitySelectAdapter: () => useAssociationEntitySelectAdapter,
@@ -48,6 +52,7 @@ __export(react_exports, {
48
52
  useAssociationsStore: () => useAssociationsStore,
49
53
  useAssociationsUiPorts: () => useAssociationsUiPorts,
50
54
  useCategories: () => useCategories,
55
+ useComments: () => useComments,
51
56
  useContainerLinks: () => useContainerLinks,
52
57
  useContainerLinksAdapter: () => useContainerLinksAdapter,
53
58
  useEntityRelationships: () => useEntityRelationships,
@@ -71,6 +76,7 @@ function AssociationsProvider({
71
76
  capture,
72
77
  pickerOverrides,
73
78
  entityDoors,
79
+ authorDisplay,
74
80
  children
75
81
  }) {
76
82
  const notifierWarned = (0, import_react.useRef)(false);
@@ -84,7 +90,8 @@ function AssociationsProvider({
84
90
  ...windowShell !== void 0 ? { windowShell } : {},
85
91
  ...capture !== void 0 ? { capture } : {},
86
92
  ...pickerOverrides !== void 0 ? { pickerOverrides } : {},
87
- ...entityDoors !== void 0 ? { entityDoors } : {}
93
+ ...entityDoors !== void 0 ? { entityDoors } : {},
94
+ ...authorDisplay !== void 0 ? { authorDisplay } : {}
88
95
  },
89
96
  children
90
97
  }
@@ -779,6 +786,7 @@ var ENTITY_TYPE_TOKENS = [
779
786
  "workflow",
780
787
  "workflow_card",
781
788
  "workflow_checkpoint",
789
+ "workflow_comparison",
782
790
  "workflow_definition_version",
783
791
  "workflow_idempotency",
784
792
  "workflow_job",
@@ -824,6 +832,12 @@ var IDLE_CATEGORIES = Object.freeze({
824
832
  fetchedAt: null,
825
833
  error: null
826
834
  });
835
+ var IDLE_COMMENTS = Object.freeze({
836
+ status: "idle",
837
+ comments: Object.freeze([]),
838
+ fetchedAt: null,
839
+ error: null
840
+ });
827
841
 
828
842
  // src/react/hooks/useAssociations.ts
829
843
  var noopSubscribe = () => () => {
@@ -1302,8 +1316,74 @@ function currentUserIdOrNull(store) {
1302
1316
  }
1303
1317
  }
1304
1318
 
1305
- // src/react/hooks/useAssociationEntitySelect.ts
1319
+ // src/react/hooks/useComments.ts
1306
1320
  var import_react8 = require("react");
1321
+ var noopSubscribe3 = () => () => {
1322
+ };
1323
+ function useComments(args) {
1324
+ const { token, id, autoLoad = true } = args;
1325
+ const store = useAssociationsStore();
1326
+ const key = token && id ? associationsKey(token, id) : null;
1327
+ const entry = (0, import_react8.useSyncExternalStore)(
1328
+ key ? (cb) => store.subscribeComments(key, cb) : noopSubscribe3,
1329
+ () => store.getComments(token, id ?? ""),
1330
+ () => store.getComments(token, id ?? "")
1331
+ );
1332
+ const loadedKey = (0, import_react8.useRef)(null);
1333
+ (0, import_react8.useEffect)(() => {
1334
+ if (!autoLoad || !key || !id) return;
1335
+ if (loadedKey.current === key) return;
1336
+ loadedKey.current = key;
1337
+ void store.loadComments(token, id);
1338
+ }, [autoLoad, store, key, token, id]);
1339
+ let currentUserId = null;
1340
+ try {
1341
+ currentUserId = store.identity.requireUserId();
1342
+ } catch {
1343
+ currentUserId = null;
1344
+ }
1345
+ return {
1346
+ comments: entry.comments,
1347
+ status: entry.status,
1348
+ error: entry.error,
1349
+ fetchedAt: entry.fetchedAt,
1350
+ add: async (body, opts) => {
1351
+ if (!id) return { ok: false, error: "Missing entity id" };
1352
+ return store.addComment({
1353
+ entityType: token,
1354
+ entityId: id,
1355
+ body,
1356
+ ...opts?.parentId !== void 0 ? { parentId: opts.parentId } : {},
1357
+ ...opts?.orgId !== void 0 ? { orgId: opts.orgId } : {}
1358
+ });
1359
+ },
1360
+ edit: async (commentId, body) => {
1361
+ if (!id) return { ok: false, error: "Missing entity id" };
1362
+ return store.editComment({
1363
+ entityType: token,
1364
+ entityId: id,
1365
+ id: commentId,
1366
+ body
1367
+ });
1368
+ },
1369
+ remove: async (commentId) => {
1370
+ if (!id) return { ok: false, error: "Missing entity id" };
1371
+ return store.deleteComment({
1372
+ entityType: token,
1373
+ entityId: id,
1374
+ id: commentId
1375
+ });
1376
+ },
1377
+ reload: async () => {
1378
+ if (!id) return;
1379
+ await store.loadComments(token, id, { force: true });
1380
+ },
1381
+ currentUserId
1382
+ };
1383
+ }
1384
+
1385
+ // src/react/hooks/useAssociationEntitySelect.ts
1386
+ var import_react9 = require("react");
1307
1387
  function useAssociationEntitySelectAdapter(args) {
1308
1388
  const { token, container, activeId, onActiveChange, createColumns } = args;
1309
1389
  const store = useAssociationsStore();
@@ -1317,10 +1397,10 @@ function useAssociationEntitySelectAdapter(args) {
1317
1397
  const { titleFor } = useEntityTitles(
1318
1398
  rows.map((r) => ({ token, id: r.resourceId, label: r.label }))
1319
1399
  );
1320
- const [titleOverrides, setTitleOverrides] = (0, import_react8.useState)(
1400
+ const [titleOverrides, setTitleOverrides] = (0, import_react9.useState)(
1321
1401
  {}
1322
1402
  );
1323
- const [localActiveId, setLocalActiveId] = (0, import_react8.useState)(null);
1403
+ const [localActiveId, setLocalActiveId] = (0, import_react9.useState)(null);
1324
1404
  const items = rows.map((r) => ({
1325
1405
  id: r.resourceId,
1326
1406
  title: titleOverrides[r.resourceId] ?? titleFor({ token, id: r.resourceId, label: r.label })
@@ -1377,9 +1457,9 @@ function useAssociationEntitySelectAdapter(args) {
1377
1457
  }
1378
1458
 
1379
1459
  // src/react/components/PrimaryEntityContext.tsx
1380
- var import_react9 = require("react");
1460
+ var import_react10 = require("react");
1381
1461
  var import_jsx_runtime2 = require("react/jsx-runtime");
1382
- var PrimaryEntityCtx = (0, import_react9.createContext)(null);
1462
+ var PrimaryEntityCtx = (0, import_react10.createContext)(null);
1383
1463
  function PrimaryEntityProvider({
1384
1464
  value,
1385
1465
  children
@@ -1387,11 +1467,11 @@ function PrimaryEntityProvider({
1387
1467
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PrimaryEntityCtx.Provider, { value, children });
1388
1468
  }
1389
1469
  function usePrimaryEntity() {
1390
- return (0, import_react9.useContext)(PrimaryEntityCtx);
1470
+ return (0, import_react10.useContext)(PrimaryEntityCtx);
1391
1471
  }
1392
1472
 
1393
1473
  // src/react/components/AssociationWindow.tsx
1394
- var import_react10 = require("react");
1474
+ var import_react11 = require("react");
1395
1475
  var import_design_system = require("@ai-matrx/design-system");
1396
1476
 
1397
1477
  // src/react/icons.tsx
@@ -1522,7 +1602,7 @@ function AssociationWindow({
1522
1602
  subtitle,
1523
1603
  children
1524
1604
  }) {
1525
- const instanceId = (0, import_react10.useId)();
1605
+ const instanceId = (0, import_react11.useId)();
1526
1606
  const { windowShell } = useAssociationsUiPorts();
1527
1607
  if (!open) return null;
1528
1608
  const body = /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex h-full min-h-0 flex-col gap-2 p-3", children: [
@@ -1574,7 +1654,7 @@ function AssociationWindow({
1574
1654
  }
1575
1655
 
1576
1656
  // src/react/components/AssociationPicker.tsx
1577
- var import_react11 = require("react");
1657
+ var import_react12 = require("react");
1578
1658
  var import_design_system2 = require("@ai-matrx/design-system");
1579
1659
  var import_jsx_runtime5 = require("react/jsx-runtime");
1580
1660
  function AssociationPicker(props) {
@@ -1643,8 +1723,8 @@ function EntityCandidateList({
1643
1723
  }) {
1644
1724
  const store = useAssociationsStore();
1645
1725
  const notifier = useNotifier();
1646
- const [search, setSearch] = (0, import_react11.useState)("");
1647
- const [busyId, setBusyId] = (0, import_react11.useState)(null);
1726
+ const [search, setSearch] = (0, import_react12.useState)("");
1727
+ const [busyId, setBusyId] = (0, import_react12.useState)(null);
1648
1728
  const info = store.registry.getEntityInfo(token);
1649
1729
  const Icon = info.Icon ?? DefaultEntityIcon;
1650
1730
  const { candidates, loading, error, reload } = useAssociationCandidates({
@@ -1750,9 +1830,9 @@ function CreateAndAttachFooter({
1750
1830
  const store = useAssociationsStore();
1751
1831
  const notifier = useNotifier();
1752
1832
  const info = store.registry.getEntityInfo(token);
1753
- const [editing, setEditing] = (0, import_react11.useState)(false);
1754
- const [name, setName] = (0, import_react11.useState)("");
1755
- const [busy, setBusy] = (0, import_react11.useState)(false);
1833
+ const [editing, setEditing] = (0, import_react12.useState)(false);
1834
+ const [name, setName] = (0, import_react12.useState)("");
1835
+ const [busy, setBusy] = (0, import_react12.useState)(false);
1756
1836
  const submit = async () => {
1757
1837
  const title = name.trim();
1758
1838
  if (!title || busy) return;
@@ -1860,7 +1940,7 @@ function ListMessage({ children }) {
1860
1940
  }
1861
1941
 
1862
1942
  // src/react/components/UniversalAssociationPicker.tsx
1863
- var import_react12 = require("react");
1943
+ var import_react13 = require("react");
1864
1944
  var import_design_system3 = require("@ai-matrx/design-system");
1865
1945
  var import_jsx_runtime6 = require("react/jsx-runtime");
1866
1946
  function attachedKey(token, id) {
@@ -1872,9 +1952,9 @@ function UniversalAssociationPicker(props) {
1872
1952
  const { entityDoors, pickerOverrides } = useAssociationsUiPorts();
1873
1953
  const notifier = useNotifier();
1874
1954
  const tokens = props.tokens ?? store.registry.curatedTokens();
1875
- const [query, setQuery] = (0, import_react12.useState)("");
1876
- const [browseToken, setBrowseToken] = (0, import_react12.useState)(null);
1877
- const [busyKey, setBusyKey] = (0, import_react12.useState)(null);
1955
+ const [query, setQuery] = (0, import_react13.useState)("");
1956
+ const [browseToken, setBrowseToken] = (0, import_react13.useState)(null);
1957
+ const [busyKey, setBusyKey] = (0, import_react13.useState)(null);
1878
1958
  const { results, loading, isRecents } = useUniversalEntitySearch({
1879
1959
  query,
1880
1960
  tokens,
@@ -2045,7 +2125,7 @@ function UniversalAssociationPicker(props) {
2045
2125
  }
2046
2126
 
2047
2127
  // src/react/components/AttachedItemsSheet.tsx
2048
- var import_react13 = require("react");
2128
+ var import_react14 = require("react");
2049
2129
  var import_design_system5 = require("@ai-matrx/design-system");
2050
2130
 
2051
2131
  // src/react/components/entityDoors.tsx
@@ -2161,11 +2241,11 @@ function AttachedItemsBody({
2161
2241
  const notifier = useNotifier();
2162
2242
  const info = store.registry.getEntityInfo(token);
2163
2243
  const Icon = info.Icon ?? DefaultEntityIcon;
2164
- const [titles, setTitles] = (0, import_react13.useState)(null);
2165
- const [titleError, setTitleError] = (0, import_react13.useState)(null);
2166
- const [busyId, setBusyId] = (0, import_react13.useState)(null);
2244
+ const [titles, setTitles] = (0, import_react14.useState)(null);
2245
+ const [titleError, setTitleError] = (0, import_react14.useState)(null);
2246
+ const [busyId, setBusyId] = (0, import_react14.useState)(null);
2167
2247
  const idKey = links.map((l) => l.resourceId).sort().join(",");
2168
- (0, import_react13.useEffect)(() => {
2248
+ (0, import_react14.useEffect)(() => {
2169
2249
  if (!enabled) return;
2170
2250
  const ids = idKey ? idKey.split(",") : [];
2171
2251
  if (ids.length === 0) {
@@ -2278,7 +2358,7 @@ function AttachedItemsBody({
2278
2358
  }
2279
2359
 
2280
2360
  // src/react/components/AssociationCard.tsx
2281
- var import_react14 = require("react");
2361
+ var import_react15 = require("react");
2282
2362
  var import_design_system6 = require("@ai-matrx/design-system");
2283
2363
 
2284
2364
  // src/react/contentRoles.ts
@@ -2338,11 +2418,11 @@ function AssociationCard({
2338
2418
  const store = useAssociationsStore();
2339
2419
  const fromCtx = usePrimaryEntity();
2340
2420
  const container = containerProp ?? fromCtx;
2341
- const [open, setOpen] = (0, import_react14.useState)(false);
2421
+ const [open, setOpen] = (0, import_react15.useState)(false);
2342
2422
  const info = store.registry.getEntityInfo(token);
2343
2423
  const role = getContentRoleMeta(info.contentRole);
2344
2424
  const Icon = info.Icon ?? DefaultEntityIcon;
2345
- const [listOpen, setListOpen] = (0, import_react14.useState)(false);
2425
+ const [listOpen, setListOpen] = (0, import_react15.useState)(false);
2346
2426
  const { status, countFor, attachedIdsFor, linksFor, attach, detach } = useContainerLinks({
2347
2427
  containerType: container?.type ?? "organization",
2348
2428
  containerId: container?.id ?? null,
@@ -2496,7 +2576,7 @@ function AssociationCardGrid({
2496
2576
  }
2497
2577
 
2498
2578
  // src/react/components/AssociationList.tsx
2499
- var import_react15 = require("react");
2579
+ var import_react16 = require("react");
2500
2580
  var import_design_system8 = require("@ai-matrx/design-system");
2501
2581
  var import_jsx_runtime11 = require("react/jsx-runtime");
2502
2582
  function useContainerLinksAdapter(container, tokens) {
@@ -2536,9 +2616,9 @@ function AssociationList(props) {
2536
2616
  const adapter = props.adapter ?? defaultAdapter;
2537
2617
  const variant = props.variant ?? "full";
2538
2618
  const tokenFilter = props.tokens ?? null;
2539
- const [pickerToken, setPickerToken] = (0, import_react15.useState)(null);
2540
- const [showUniversal, setShowUniversal] = (0, import_react15.useState)(false);
2541
- const [removingKeys, setRemovingKeys] = (0, import_react15.useState)(/* @__PURE__ */ new Set());
2619
+ const [pickerToken, setPickerToken] = (0, import_react16.useState)(null);
2620
+ const [showUniversal, setShowUniversal] = (0, import_react16.useState)(false);
2621
+ const [removingKeys, setRemovingKeys] = (0, import_react16.useState)(/* @__PURE__ */ new Set());
2542
2622
  const rows = tokenFilter ? adapter.rows.filter((r) => tokenFilter.includes(r.token)) : adapter.rows;
2543
2623
  const visibleRows = rows.filter((r) => !removingKeys.has(r.key));
2544
2624
  const { titleFor } = useEntityTitles(
@@ -2850,7 +2930,7 @@ function titleize(token) {
2850
2930
  }
2851
2931
 
2852
2932
  // src/react/components/AssociationCaptureToolbar.tsx
2853
- var import_react16 = require("react");
2933
+ var import_react17 = require("react");
2854
2934
  var import_design_system9 = require("@ai-matrx/design-system");
2855
2935
  var import_jsx_runtime12 = require("react/jsx-runtime");
2856
2936
  function AssociationCaptureToolbar({
@@ -2870,13 +2950,13 @@ function AssociationCaptureToolbar({
2870
2950
  const store = useAssociationsStore();
2871
2951
  const { capture } = useAssociationsUiPorts();
2872
2952
  const notifier = useNotifier();
2873
- const fileInputRef = (0, import_react16.useRef)(null);
2874
- const [isUploading, setIsUploading] = (0, import_react16.useState)(false);
2875
- const [isDragOver, setIsDragOver] = (0, import_react16.useState)(false);
2876
- const [isPicking, setIsPicking] = (0, import_react16.useState)(false);
2877
- const [namingDoc, setNamingDoc] = (0, import_react16.useState)(false);
2878
- const [docName, setDocName] = (0, import_react16.useState)("");
2879
- const [creatingDoc, setCreatingDoc] = (0, import_react16.useState)(false);
2953
+ const fileInputRef = (0, import_react17.useRef)(null);
2954
+ const [isUploading, setIsUploading] = (0, import_react17.useState)(false);
2955
+ const [isDragOver, setIsDragOver] = (0, import_react17.useState)(false);
2956
+ const [isPicking, setIsPicking] = (0, import_react17.useState)(false);
2957
+ const [namingDoc, setNamingDoc] = (0, import_react17.useState)(false);
2958
+ const [docName, setDocName] = (0, import_react17.useState)("");
2959
+ const [creatingDoc, setCreatingDoc] = (0, import_react17.useState)(false);
2880
2960
  const actions = {
2881
2961
  upload: (showActions?.upload ?? true) && Boolean(capture?.requestUpload),
2882
2962
  addFile: (showActions?.addFile ?? true) && Boolean(capture?.openFilePicker),
@@ -3184,7 +3264,7 @@ function CaptureToolbarAction({
3184
3264
  }
3185
3265
 
3186
3266
  // src/react/components/AssociationEntitySelect.tsx
3187
- var import_react17 = require("react");
3267
+ var import_react18 = require("react");
3188
3268
  var import_design_system10 = require("@ai-matrx/design-system");
3189
3269
  var import_jsx_runtime13 = require("react/jsx-runtime");
3190
3270
  function AssociationEntitySelect({
@@ -3208,11 +3288,11 @@ function AssociationEntitySelect({
3208
3288
  const { items, activeId } = adapter;
3209
3289
  const active = items.find((i) => i.id === activeId) ?? null;
3210
3290
  const activeIndex = active ? items.indexOf(active) : -1;
3211
- const [open, setOpen] = (0, import_react17.useState)(false);
3212
- const [query, setQuery] = (0, import_react17.useState)("");
3213
- const [creating, setCreating] = (0, import_react17.useState)(false);
3214
- const [draftName, setDraftName] = (0, import_react17.useState)("");
3215
- const [busy, setBusy] = (0, import_react17.useState)(false);
3291
+ const [open, setOpen] = (0, import_react18.useState)(false);
3292
+ const [query, setQuery] = (0, import_react18.useState)("");
3293
+ const [creating, setCreating] = (0, import_react18.useState)(false);
3294
+ const [draftName, setDraftName] = (0, import_react18.useState)("");
3295
+ const [busy, setBusy] = (0, import_react18.useState)(false);
3216
3296
  const entityLabel = info.label.toLowerCase();
3217
3297
  const close = () => {
3218
3298
  setOpen(false);
@@ -3435,7 +3515,7 @@ function AssociationEntitySelect({
3435
3515
  }
3436
3516
 
3437
3517
  // src/react/components/CategorySelect.tsx
3438
- var import_react18 = require("react");
3518
+ var import_react19 = require("react");
3439
3519
  var import_design_system11 = require("@ai-matrx/design-system");
3440
3520
 
3441
3521
  // src/core/categoryHierarchy.ts
@@ -3530,7 +3610,7 @@ function CategorySelect({
3530
3610
  reload
3531
3611
  } = useCategories({ dimension });
3532
3612
  const hierarchy = buildCategoryHierarchy(categories);
3533
- const [createParentId, setCreateParentId] = (0, import_react18.useState)(ROOT_PARENT);
3613
+ const [createParentId, setCreateParentId] = (0, import_react19.useState)(ROOT_PARENT);
3534
3614
  const options = [
3535
3615
  ...allowNone ? [{ value: NONE, label: "None", keywords: "none clear empty" }] : [],
3536
3616
  ...hierarchy.hasHierarchy ? hierarchy.items.map(({ category, depth, parent }) => ({
@@ -3647,7 +3727,7 @@ function CategorySelect({
3647
3727
  }
3648
3728
 
3649
3729
  // src/react/components/CategoryTagPicker.tsx
3650
- var import_react19 = require("react");
3730
+ var import_react20 = require("react");
3651
3731
  var import_design_system12 = require("@ai-matrx/design-system");
3652
3732
  var import_jsx_runtime15 = require("react/jsx-runtime");
3653
3733
  var ROOT_PARENT2 = "__root__";
@@ -3665,11 +3745,11 @@ function CategoryTagPicker({
3665
3745
  }) {
3666
3746
  const store = useAssociationsStore();
3667
3747
  const notifier = useNotifier();
3668
- const [open, setOpen] = (0, import_react19.useState)(false);
3669
- const [creating, setCreating] = (0, import_react19.useState)(false);
3670
- const [writing, setWriting] = (0, import_react19.useState)(false);
3671
- const [search, setSearch] = (0, import_react19.useState)("");
3672
- const [createParentId, setCreateParentId] = (0, import_react19.useState)(ROOT_PARENT2);
3748
+ const [open, setOpen] = (0, import_react20.useState)(false);
3749
+ const [creating, setCreating] = (0, import_react20.useState)(false);
3750
+ const [writing, setWriting] = (0, import_react20.useState)(false);
3751
+ const [search, setSearch] = (0, import_react20.useState)("");
3752
+ const [createParentId, setCreateParentId] = (0, import_react20.useState)(ROOT_PARENT2);
3673
3753
  const {
3674
3754
  categories,
3675
3755
  create: createCategory,
@@ -3902,4 +3982,409 @@ function CategoryTagPicker({
3902
3982
  ] })
3903
3983
  ] });
3904
3984
  }
3985
+
3986
+ // src/react/components/CommentThread.tsx
3987
+ var import_react21 = require("react");
3988
+ var import_design_system13 = require("@ai-matrx/design-system");
3989
+
3990
+ // src/react/relativeTime.ts
3991
+ var UNITS = [
3992
+ { unit: "year", ms: 365 * 24 * 60 * 60 * 1e3 },
3993
+ { unit: "month", ms: 30 * 24 * 60 * 60 * 1e3 },
3994
+ { unit: "week", ms: 7 * 24 * 60 * 60 * 1e3 },
3995
+ { unit: "day", ms: 24 * 60 * 60 * 1e3 },
3996
+ { unit: "hour", ms: 60 * 60 * 1e3 },
3997
+ { unit: "minute", ms: 60 * 1e3 }
3998
+ ];
3999
+ function formatRelativeTime(iso, now = Date.now()) {
4000
+ const t = Date.parse(iso);
4001
+ if (Number.isNaN(t)) return iso;
4002
+ const delta = t - now;
4003
+ const magnitude = Math.abs(delta);
4004
+ if (magnitude < 60 * 1e3) return "just now";
4005
+ const rtf = new Intl.RelativeTimeFormat(void 0, { numeric: "auto" });
4006
+ for (const { unit, ms } of UNITS) {
4007
+ if (magnitude >= ms) {
4008
+ return rtf.format(Math.trunc(delta / ms), unit);
4009
+ }
4010
+ }
4011
+ return "just now";
4012
+ }
4013
+
4014
+ // src/react/components/CommentThread.tsx
4015
+ var import_jsx_runtime16 = require("react/jsx-runtime");
4016
+ var AUTHOR_DOOR_TOKEN = "user";
4017
+ function resolveAuthor(comment, authorDisplay) {
4018
+ const base2 = {
4019
+ userId: comment.createdBy,
4020
+ email: comment.author.email,
4021
+ displayName: comment.author.displayName,
4022
+ avatarUrl: comment.author.avatarUrl
4023
+ };
4024
+ const enriched = authorDisplay ? authorDisplay(base2) : null;
4025
+ const name = enriched?.displayName ?? base2.displayName ?? base2.email ?? "Unknown user";
4026
+ return {
4027
+ name,
4028
+ avatarUrl: enriched?.avatarUrl !== void 0 && enriched?.avatarUrl !== null ? enriched.avatarUrl : base2.avatarUrl,
4029
+ userId: comment.createdBy
4030
+ };
4031
+ }
4032
+ function AuthorAvatar({ author }) {
4033
+ if (author.avatarUrl) {
4034
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4035
+ "img",
4036
+ {
4037
+ src: author.avatarUrl,
4038
+ alt: "",
4039
+ className: "h-6 w-6 shrink-0 rounded-full object-cover"
4040
+ }
4041
+ );
4042
+ }
4043
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4044
+ "span",
4045
+ {
4046
+ "aria-hidden": true,
4047
+ 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",
4048
+ children: author.name.trim().charAt(0) || "?"
4049
+ }
4050
+ );
4051
+ }
4052
+ function AuthorName({
4053
+ author,
4054
+ entityDoors
4055
+ }) {
4056
+ const DoorRef2 = entityDoors?.EntityRef;
4057
+ if (DoorRef2 && author.userId) {
4058
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4059
+ DoorRef2,
4060
+ {
4061
+ token: AUTHOR_DOOR_TOKEN,
4062
+ id: author.userId,
4063
+ name: author.name,
4064
+ showIcon: false,
4065
+ className: "text-sm font-medium text-foreground"
4066
+ }
4067
+ );
4068
+ }
4069
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-sm font-medium text-foreground", children: author.name });
4070
+ }
4071
+ function CommentComposer({
4072
+ onSubmit,
4073
+ placeholder = "Write a comment\u2026",
4074
+ submitLabel = "Comment",
4075
+ initialValue = "",
4076
+ trailing,
4077
+ autoFocus = false
4078
+ }) {
4079
+ const notifier = useNotifier();
4080
+ const [body, setBody] = (0, import_react21.useState)(initialValue);
4081
+ const [submitting, setSubmitting] = (0, import_react21.useState)(false);
4082
+ const [error, setError] = (0, import_react21.useState)(null);
4083
+ async function submit(e) {
4084
+ e?.preventDefault();
4085
+ const trimmed = body.trim();
4086
+ if (!trimmed || submitting) return;
4087
+ setSubmitting(true);
4088
+ setError(null);
4089
+ const res = await onSubmit(trimmed);
4090
+ setSubmitting(false);
4091
+ if (!res.ok) {
4092
+ const msg = res.error ?? "Could not save the comment";
4093
+ setError(msg);
4094
+ notifier.error(msg);
4095
+ return;
4096
+ }
4097
+ setBody("");
4098
+ }
4099
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("form", { onSubmit: submit, className: "flex flex-col gap-1.5", children: [
4100
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4101
+ "textarea",
4102
+ {
4103
+ value: body,
4104
+ onChange: (e) => setBody(e.target.value),
4105
+ onKeyDown: (e) => {
4106
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void submit();
4107
+ },
4108
+ placeholder,
4109
+ rows: 2,
4110
+ autoFocus,
4111
+ "aria-label": placeholder,
4112
+ 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"
4113
+ }
4114
+ ),
4115
+ error && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { role: "alert", className: "text-xs text-destructive", children: error }),
4116
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex items-center gap-2", children: [
4117
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_design_system13.Button, { type: "submit", size: "sm", disabled: !body.trim() || submitting, children: [
4118
+ submitting && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(SpinnerIcon, { className: "mr-1 h-3 w-3 animate-spin" }),
4119
+ submitLabel
4120
+ ] }),
4121
+ trailing
4122
+ ] })
4123
+ ] });
4124
+ }
4125
+ function CommentItem({
4126
+ comment,
4127
+ childrenOf,
4128
+ depth,
4129
+ thread,
4130
+ orgId
4131
+ }) {
4132
+ const { authorDisplay, entityDoors } = useAssociationsUiPorts();
4133
+ const notifier = useNotifier();
4134
+ const [replying, setReplying] = (0, import_react21.useState)(false);
4135
+ const [editing, setEditing] = (0, import_react21.useState)(false);
4136
+ const [confirmingDelete, setConfirmingDelete] = (0, import_react21.useState)(false);
4137
+ const [deleting, setDeleting] = (0, import_react21.useState)(false);
4138
+ const author = resolveAuthor(comment, authorDisplay);
4139
+ const isOwn = thread.currentUserId !== null && comment.createdBy === thread.currentUserId;
4140
+ const edited = comment.updatedAt !== comment.createdAt;
4141
+ const replies = childrenOf.get(comment.id) ?? [];
4142
+ async function confirmDelete() {
4143
+ setDeleting(true);
4144
+ const res = await thread.remove(comment.id);
4145
+ setDeleting(false);
4146
+ setConfirmingDelete(false);
4147
+ if (!res.ok) {
4148
+ notifier.error(res.error ?? "Could not delete the comment");
4149
+ }
4150
+ }
4151
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
4152
+ "div",
4153
+ {
4154
+ "data-comment-id": comment.id,
4155
+ className: (0, import_design_system13.cn)(
4156
+ "flex flex-col gap-1",
4157
+ depth > 0 && "border-l border-border pl-3"
4158
+ ),
4159
+ children: [
4160
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex items-center gap-2", children: [
4161
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(AuthorAvatar, { author }),
4162
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(AuthorName, { author, entityDoors }),
4163
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4164
+ "span",
4165
+ {
4166
+ className: "text-xs text-muted-foreground",
4167
+ title: comment.createdAt,
4168
+ children: formatRelativeTime(comment.createdAt)
4169
+ }
4170
+ ),
4171
+ edited && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4172
+ "span",
4173
+ {
4174
+ className: "text-xs text-muted-foreground",
4175
+ title: comment.updatedAt,
4176
+ children: "(edited)"
4177
+ }
4178
+ )
4179
+ ] }),
4180
+ editing ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "pl-8", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4181
+ CommentComposer,
4182
+ {
4183
+ initialValue: comment.body,
4184
+ submitLabel: "Save",
4185
+ placeholder: "Edit your comment\u2026",
4186
+ autoFocus: true,
4187
+ onSubmit: async (body) => {
4188
+ const res = await thread.edit(comment.id, body);
4189
+ if (res.ok) setEditing(false);
4190
+ return res;
4191
+ },
4192
+ trailing: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4193
+ import_design_system13.Button,
4194
+ {
4195
+ type: "button",
4196
+ size: "sm",
4197
+ variant: "ghost",
4198
+ onClick: () => setEditing(false),
4199
+ children: "Cancel"
4200
+ }
4201
+ )
4202
+ }
4203
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "whitespace-pre-wrap pl-8 text-sm text-foreground", children: comment.body }),
4204
+ !editing && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "flex items-center gap-1 pl-7", children: confirmingDelete ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
4205
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-xs text-destructive", children: "Deletes this comment for everyone." }),
4206
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
4207
+ import_design_system13.Button,
4208
+ {
4209
+ type: "button",
4210
+ size: "sm",
4211
+ variant: "destructive",
4212
+ disabled: deleting,
4213
+ onClick: () => void confirmDelete(),
4214
+ children: [
4215
+ deleting && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(SpinnerIcon, { className: "mr-1 h-3 w-3 animate-spin" }),
4216
+ "Delete"
4217
+ ]
4218
+ }
4219
+ ),
4220
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4221
+ import_design_system13.Button,
4222
+ {
4223
+ type: "button",
4224
+ size: "sm",
4225
+ variant: "ghost",
4226
+ disabled: deleting,
4227
+ onClick: () => setConfirmingDelete(false),
4228
+ children: "Cancel"
4229
+ }
4230
+ )
4231
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
4232
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4233
+ "button",
4234
+ {
4235
+ type: "button",
4236
+ onClick: () => setReplying((v) => !v),
4237
+ className: "rounded px-1.5 py-0.5 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
4238
+ children: "Reply"
4239
+ }
4240
+ ),
4241
+ isOwn && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
4242
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4243
+ "button",
4244
+ {
4245
+ type: "button",
4246
+ onClick: () => setEditing(true),
4247
+ className: "rounded px-1.5 py-0.5 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
4248
+ children: "Edit"
4249
+ }
4250
+ ),
4251
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4252
+ "button",
4253
+ {
4254
+ type: "button",
4255
+ onClick: () => setConfirmingDelete(true),
4256
+ className: "rounded px-1.5 py-0.5 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-destructive",
4257
+ children: "Delete"
4258
+ }
4259
+ )
4260
+ ] })
4261
+ ] }) }),
4262
+ replying && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex items-start gap-2 pl-8 pt-1", children: [
4263
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(CornerDownRightIcon, { className: "mt-2 h-3.5 w-3.5 shrink-0 text-muted-foreground" }),
4264
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4265
+ CommentComposer,
4266
+ {
4267
+ submitLabel: "Reply",
4268
+ placeholder: `Reply to ${author.name}\u2026`,
4269
+ autoFocus: true,
4270
+ onSubmit: async (body) => {
4271
+ const res = await thread.add(body, {
4272
+ parentId: comment.id,
4273
+ orgId: orgId ?? null
4274
+ });
4275
+ if (res.ok) setReplying(false);
4276
+ return res;
4277
+ },
4278
+ trailing: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4279
+ import_design_system13.Button,
4280
+ {
4281
+ type: "button",
4282
+ size: "sm",
4283
+ variant: "ghost",
4284
+ onClick: () => setReplying(false),
4285
+ children: "Cancel"
4286
+ }
4287
+ )
4288
+ }
4289
+ ) })
4290
+ ] }),
4291
+ replies.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "flex flex-col gap-3 pl-8 pt-1", children: replies.map((reply) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4292
+ CommentItem,
4293
+ {
4294
+ comment: reply,
4295
+ childrenOf,
4296
+ depth: depth + 1,
4297
+ thread,
4298
+ orgId
4299
+ },
4300
+ reply.id
4301
+ )) })
4302
+ ]
4303
+ }
4304
+ );
4305
+ }
4306
+ function buildCommentTree(comments) {
4307
+ const byId = new Set(comments.map((c) => c.id));
4308
+ const roots = [];
4309
+ const childrenOf = /* @__PURE__ */ new Map();
4310
+ for (const c of comments) {
4311
+ if (c.parentId && byId.has(c.parentId)) {
4312
+ const bucket = childrenOf.get(c.parentId);
4313
+ if (bucket) bucket.push(c);
4314
+ else childrenOf.set(c.parentId, [c]);
4315
+ } else {
4316
+ roots.push(c);
4317
+ }
4318
+ }
4319
+ return { roots, childrenOf };
4320
+ }
4321
+ function CommentThread({
4322
+ token,
4323
+ id,
4324
+ orgId,
4325
+ showHeader = true,
4326
+ className
4327
+ }) {
4328
+ const thread = useComments({ token, id });
4329
+ const { roots, childrenOf } = buildCommentTree(thread.comments);
4330
+ const loading = (thread.status === "loading" || thread.status === "idle") && thread.comments.length === 0;
4331
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("section", { className: (0, import_design_system13.cn)("flex flex-col gap-3", className), children: [
4332
+ showHeader && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("header", { className: "flex items-center gap-2", children: [
4333
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("h3", { className: "text-sm font-semibold text-foreground", children: "Comments" }),
4334
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-xs text-muted-foreground tabular-nums", children: thread.comments.length }),
4335
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4336
+ "button",
4337
+ {
4338
+ type: "button",
4339
+ onClick: () => void thread.reload(),
4340
+ "aria-label": "Refresh comments",
4341
+ title: "Refresh comments",
4342
+ className: "ml-auto flex h-6 w-6 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
4343
+ children: thread.status === "loading" ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(SpinnerIcon, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(RefreshIcon, { className: "h-3.5 w-3.5" })
4344
+ }
4345
+ )
4346
+ ] }),
4347
+ thread.error && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
4348
+ "div",
4349
+ {
4350
+ role: "alert",
4351
+ className: "flex items-center gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive",
4352
+ children: [
4353
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "min-w-0 flex-1", children: thread.error }),
4354
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4355
+ import_design_system13.Button,
4356
+ {
4357
+ type: "button",
4358
+ size: "sm",
4359
+ variant: "outline",
4360
+ disabled: thread.status === "loading",
4361
+ onClick: () => void thread.reload(),
4362
+ children: "Retry"
4363
+ }
4364
+ )
4365
+ ]
4366
+ }
4367
+ ),
4368
+ loading ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex flex-col gap-3", "aria-hidden": true, "data-testid": "comments-loading", children: [
4369
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_design_system13.Skeleton, { className: "h-12 w-full" }),
4370
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_design_system13.Skeleton, { className: "h-12 w-4/5" })
4371
+ ] }) : roots.length === 0 && !thread.error ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "text-sm text-muted-foreground", children: "No comments yet." }) : /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "flex flex-col gap-4", children: roots.map((comment) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4372
+ CommentItem,
4373
+ {
4374
+ comment,
4375
+ childrenOf,
4376
+ depth: 0,
4377
+ thread,
4378
+ orgId
4379
+ },
4380
+ comment.id
4381
+ )) }),
4382
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4383
+ CommentComposer,
4384
+ {
4385
+ onSubmit: (body) => thread.add(body, { orgId: orgId ?? null })
4386
+ }
4387
+ )
4388
+ ] });
4389
+ }
3905
4390
  //# sourceMappingURL=index.cjs.map