@ai-matrx/associations 0.3.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.
@@ -25,6 +25,7 @@ __export(react_exports, {
25
25
  AssociationCaptureToolbar: () => AssociationCaptureToolbar,
26
26
  AssociationCard: () => AssociationCard,
27
27
  AssociationCardGrid: () => AssociationCardGrid,
28
+ AssociationEntitySelect: () => AssociationEntitySelect,
28
29
  AssociationList: () => AssociationList,
29
30
  AssociationPicker: () => AssociationPicker,
30
31
  AssociationWindow: () => AssociationWindow,
@@ -32,12 +33,18 @@ __export(react_exports, {
32
33
  AttachedItemsSheet: () => AttachedItemsSheet,
33
34
  CONTENT_ROLES: () => CONTENT_ROLES,
34
35
  CaptureToolbarAction: () => CaptureToolbarAction,
36
+ CategorySelect: () => CategorySelect,
37
+ CategoryTagPicker: () => CategoryTagPicker,
38
+ CommentComposer: () => CommentComposer,
39
+ CommentThread: () => CommentThread,
35
40
  DefaultEntityIcon: () => DefaultEntityIcon,
36
41
  DoorRef: () => DoorRef,
37
42
  PrimaryEntityProvider: () => PrimaryEntityProvider,
38
43
  UniversalAssociationPicker: () => UniversalAssociationPicker,
39
44
  UnresolvedRef: () => UnresolvedRef,
40
45
  attachedKey: () => attachedKey,
46
+ buildCommentTree: () => buildCommentTree,
47
+ formatRelativeTime: () => formatRelativeTime,
41
48
  getContentRoleMeta: () => getContentRoleMeta,
42
49
  useAssociationCandidates: () => useAssociationCandidates,
43
50
  useAssociationEntitySelectAdapter: () => useAssociationEntitySelectAdapter,
@@ -45,6 +52,7 @@ __export(react_exports, {
45
52
  useAssociationsStore: () => useAssociationsStore,
46
53
  useAssociationsUiPorts: () => useAssociationsUiPorts,
47
54
  useCategories: () => useCategories,
55
+ useComments: () => useComments,
48
56
  useContainerLinks: () => useContainerLinks,
49
57
  useContainerLinksAdapter: () => useContainerLinksAdapter,
50
58
  useEntityRelationships: () => useEntityRelationships,
@@ -68,6 +76,7 @@ function AssociationsProvider({
68
76
  capture,
69
77
  pickerOverrides,
70
78
  entityDoors,
79
+ authorDisplay,
71
80
  children
72
81
  }) {
73
82
  const notifierWarned = (0, import_react.useRef)(false);
@@ -81,7 +90,8 @@ function AssociationsProvider({
81
90
  ...windowShell !== void 0 ? { windowShell } : {},
82
91
  ...capture !== void 0 ? { capture } : {},
83
92
  ...pickerOverrides !== void 0 ? { pickerOverrides } : {},
84
- ...entityDoors !== void 0 ? { entityDoors } : {}
93
+ ...entityDoors !== void 0 ? { entityDoors } : {},
94
+ ...authorDisplay !== void 0 ? { authorDisplay } : {}
85
95
  },
86
96
  children
87
97
  }
@@ -558,6 +568,7 @@ var ENTITY_TYPE_TOKENS = [
558
568
  "platform_actor_session",
559
569
  "platform_actor_token",
560
570
  "platform_actor_token_event",
571
+ "platform_continued_access",
561
572
  "platform_outcome_event",
562
573
  "platform_outsider_consumer",
563
574
  "platform_saved_view",
@@ -586,6 +597,7 @@ var ENTITY_TYPE_TOKENS = [
586
597
  "research_tag",
587
598
  "research_template",
588
599
  "research_topic",
600
+ "route_manifest_entry",
589
601
  "rulebook",
590
602
  "sandbox_instance",
591
603
  "sch_agent_task",
@@ -664,6 +676,7 @@ var ENTITY_TYPE_TOKENS = [
664
676
  "seo_topic",
665
677
  "seo_web_analytics_daily",
666
678
  "shared_canvas_item",
679
+ "short_link",
667
680
  "skill",
668
681
  "skill_render_definition",
669
682
  "sms_consent",
@@ -818,6 +831,12 @@ var IDLE_CATEGORIES = Object.freeze({
818
831
  fetchedAt: null,
819
832
  error: null
820
833
  });
834
+ var IDLE_COMMENTS = Object.freeze({
835
+ status: "idle",
836
+ comments: Object.freeze([]),
837
+ fetchedAt: null,
838
+ error: null
839
+ });
821
840
 
822
841
  // src/react/hooks/useAssociations.ts
823
842
  var noopSubscribe = () => () => {
@@ -1296,8 +1315,74 @@ function currentUserIdOrNull(store) {
1296
1315
  }
1297
1316
  }
1298
1317
 
1299
- // src/react/hooks/useAssociationEntitySelect.ts
1318
+ // src/react/hooks/useComments.ts
1300
1319
  var import_react8 = require("react");
1320
+ var noopSubscribe3 = () => () => {
1321
+ };
1322
+ function useComments(args) {
1323
+ const { token, id, autoLoad = true } = args;
1324
+ const store = useAssociationsStore();
1325
+ const key = token && id ? associationsKey(token, id) : null;
1326
+ const entry = (0, import_react8.useSyncExternalStore)(
1327
+ key ? (cb) => store.subscribeComments(key, cb) : noopSubscribe3,
1328
+ () => store.getComments(token, id ?? ""),
1329
+ () => store.getComments(token, id ?? "")
1330
+ );
1331
+ const loadedKey = (0, import_react8.useRef)(null);
1332
+ (0, import_react8.useEffect)(() => {
1333
+ if (!autoLoad || !key || !id) return;
1334
+ if (loadedKey.current === key) return;
1335
+ loadedKey.current = key;
1336
+ void store.loadComments(token, id);
1337
+ }, [autoLoad, store, key, token, id]);
1338
+ let currentUserId = null;
1339
+ try {
1340
+ currentUserId = store.identity.requireUserId();
1341
+ } catch {
1342
+ currentUserId = null;
1343
+ }
1344
+ return {
1345
+ comments: entry.comments,
1346
+ status: entry.status,
1347
+ error: entry.error,
1348
+ fetchedAt: entry.fetchedAt,
1349
+ add: async (body, opts) => {
1350
+ if (!id) return { ok: false, error: "Missing entity id" };
1351
+ return store.addComment({
1352
+ entityType: token,
1353
+ entityId: id,
1354
+ body,
1355
+ ...opts?.parentId !== void 0 ? { parentId: opts.parentId } : {},
1356
+ ...opts?.orgId !== void 0 ? { orgId: opts.orgId } : {}
1357
+ });
1358
+ },
1359
+ edit: async (commentId, body) => {
1360
+ if (!id) return { ok: false, error: "Missing entity id" };
1361
+ return store.editComment({
1362
+ entityType: token,
1363
+ entityId: id,
1364
+ id: commentId,
1365
+ body
1366
+ });
1367
+ },
1368
+ remove: async (commentId) => {
1369
+ if (!id) return { ok: false, error: "Missing entity id" };
1370
+ return store.deleteComment({
1371
+ entityType: token,
1372
+ entityId: id,
1373
+ id: commentId
1374
+ });
1375
+ },
1376
+ reload: async () => {
1377
+ if (!id) return;
1378
+ await store.loadComments(token, id, { force: true });
1379
+ },
1380
+ currentUserId
1381
+ };
1382
+ }
1383
+
1384
+ // src/react/hooks/useAssociationEntitySelect.ts
1385
+ var import_react9 = require("react");
1301
1386
  function useAssociationEntitySelectAdapter(args) {
1302
1387
  const { token, container, activeId, onActiveChange, createColumns } = args;
1303
1388
  const store = useAssociationsStore();
@@ -1311,10 +1396,10 @@ function useAssociationEntitySelectAdapter(args) {
1311
1396
  const { titleFor } = useEntityTitles(
1312
1397
  rows.map((r) => ({ token, id: r.resourceId, label: r.label }))
1313
1398
  );
1314
- const [titleOverrides, setTitleOverrides] = (0, import_react8.useState)(
1399
+ const [titleOverrides, setTitleOverrides] = (0, import_react9.useState)(
1315
1400
  {}
1316
1401
  );
1317
- const [localActiveId, setLocalActiveId] = (0, import_react8.useState)(null);
1402
+ const [localActiveId, setLocalActiveId] = (0, import_react9.useState)(null);
1318
1403
  const items = rows.map((r) => ({
1319
1404
  id: r.resourceId,
1320
1405
  title: titleOverrides[r.resourceId] ?? titleFor({ token, id: r.resourceId, label: r.label })
@@ -1371,9 +1456,9 @@ function useAssociationEntitySelectAdapter(args) {
1371
1456
  }
1372
1457
 
1373
1458
  // src/react/components/PrimaryEntityContext.tsx
1374
- var import_react9 = require("react");
1459
+ var import_react10 = require("react");
1375
1460
  var import_jsx_runtime2 = require("react/jsx-runtime");
1376
- var PrimaryEntityCtx = (0, import_react9.createContext)(null);
1461
+ var PrimaryEntityCtx = (0, import_react10.createContext)(null);
1377
1462
  function PrimaryEntityProvider({
1378
1463
  value,
1379
1464
  children
@@ -1381,11 +1466,11 @@ function PrimaryEntityProvider({
1381
1466
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PrimaryEntityCtx.Provider, { value, children });
1382
1467
  }
1383
1468
  function usePrimaryEntity() {
1384
- return (0, import_react9.useContext)(PrimaryEntityCtx);
1469
+ return (0, import_react10.useContext)(PrimaryEntityCtx);
1385
1470
  }
1386
1471
 
1387
1472
  // src/react/components/AssociationWindow.tsx
1388
- var import_react10 = require("react");
1473
+ var import_react11 = require("react");
1389
1474
  var import_design_system = require("@ai-matrx/design-system");
1390
1475
 
1391
1476
  // src/react/icons.tsx
@@ -1467,6 +1552,27 @@ function ExternalLinkIcon(props) {
1467
1552
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" })
1468
1553
  ] });
1469
1554
  }
1555
+ function ChevronDownIcon(props) {
1556
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { ...base(props), children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "m6 9 6 6 6-6" }) });
1557
+ }
1558
+ function ChevronsUpDownIcon(props) {
1559
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("svg", { ...base(props), children: [
1560
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "m7 15 5 5 5-5" }),
1561
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "m7 9 5-5 5 5" })
1562
+ ] });
1563
+ }
1564
+ function CornerDownRightIcon(props) {
1565
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("svg", { ...base(props), children: [
1566
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("polyline", { points: "15 10 20 15 15 20" }),
1567
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M4 4v7a4 4 0 0 0 4 4h12" })
1568
+ ] });
1569
+ }
1570
+ function TagIcon(props) {
1571
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("svg", { ...base(props), children: [
1572
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z" }),
1573
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("circle", { cx: "7.5", cy: "7.5", r: ".5", fill: "currentColor" })
1574
+ ] });
1575
+ }
1470
1576
  function DefaultEntityIcon(props) {
1471
1577
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("svg", { ...base(props), children: [
1472
1578
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z" }),
@@ -1495,7 +1601,7 @@ function AssociationWindow({
1495
1601
  subtitle,
1496
1602
  children
1497
1603
  }) {
1498
- const instanceId = (0, import_react10.useId)();
1604
+ const instanceId = (0, import_react11.useId)();
1499
1605
  const { windowShell } = useAssociationsUiPorts();
1500
1606
  if (!open) return null;
1501
1607
  const body = /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex h-full min-h-0 flex-col gap-2 p-3", children: [
@@ -1547,7 +1653,7 @@ function AssociationWindow({
1547
1653
  }
1548
1654
 
1549
1655
  // src/react/components/AssociationPicker.tsx
1550
- var import_react11 = require("react");
1656
+ var import_react12 = require("react");
1551
1657
  var import_design_system2 = require("@ai-matrx/design-system");
1552
1658
  var import_jsx_runtime5 = require("react/jsx-runtime");
1553
1659
  function AssociationPicker(props) {
@@ -1616,8 +1722,8 @@ function EntityCandidateList({
1616
1722
  }) {
1617
1723
  const store = useAssociationsStore();
1618
1724
  const notifier = useNotifier();
1619
- const [search, setSearch] = (0, import_react11.useState)("");
1620
- const [busyId, setBusyId] = (0, import_react11.useState)(null);
1725
+ const [search, setSearch] = (0, import_react12.useState)("");
1726
+ const [busyId, setBusyId] = (0, import_react12.useState)(null);
1621
1727
  const info = store.registry.getEntityInfo(token);
1622
1728
  const Icon = info.Icon ?? DefaultEntityIcon;
1623
1729
  const { candidates, loading, error, reload } = useAssociationCandidates({
@@ -1723,9 +1829,9 @@ function CreateAndAttachFooter({
1723
1829
  const store = useAssociationsStore();
1724
1830
  const notifier = useNotifier();
1725
1831
  const info = store.registry.getEntityInfo(token);
1726
- const [editing, setEditing] = (0, import_react11.useState)(false);
1727
- const [name, setName] = (0, import_react11.useState)("");
1728
- const [busy, setBusy] = (0, import_react11.useState)(false);
1832
+ const [editing, setEditing] = (0, import_react12.useState)(false);
1833
+ const [name, setName] = (0, import_react12.useState)("");
1834
+ const [busy, setBusy] = (0, import_react12.useState)(false);
1729
1835
  const submit = async () => {
1730
1836
  const title = name.trim();
1731
1837
  if (!title || busy) return;
@@ -1833,7 +1939,7 @@ function ListMessage({ children }) {
1833
1939
  }
1834
1940
 
1835
1941
  // src/react/components/UniversalAssociationPicker.tsx
1836
- var import_react12 = require("react");
1942
+ var import_react13 = require("react");
1837
1943
  var import_design_system3 = require("@ai-matrx/design-system");
1838
1944
  var import_jsx_runtime6 = require("react/jsx-runtime");
1839
1945
  function attachedKey(token, id) {
@@ -1845,9 +1951,9 @@ function UniversalAssociationPicker(props) {
1845
1951
  const { entityDoors, pickerOverrides } = useAssociationsUiPorts();
1846
1952
  const notifier = useNotifier();
1847
1953
  const tokens = props.tokens ?? store.registry.curatedTokens();
1848
- const [query, setQuery] = (0, import_react12.useState)("");
1849
- const [browseToken, setBrowseToken] = (0, import_react12.useState)(null);
1850
- const [busyKey, setBusyKey] = (0, import_react12.useState)(null);
1954
+ const [query, setQuery] = (0, import_react13.useState)("");
1955
+ const [browseToken, setBrowseToken] = (0, import_react13.useState)(null);
1956
+ const [busyKey, setBusyKey] = (0, import_react13.useState)(null);
1851
1957
  const { results, loading, isRecents } = useUniversalEntitySearch({
1852
1958
  query,
1853
1959
  tokens,
@@ -2018,7 +2124,7 @@ function UniversalAssociationPicker(props) {
2018
2124
  }
2019
2125
 
2020
2126
  // src/react/components/AttachedItemsSheet.tsx
2021
- var import_react13 = require("react");
2127
+ var import_react14 = require("react");
2022
2128
  var import_design_system5 = require("@ai-matrx/design-system");
2023
2129
 
2024
2130
  // src/react/components/entityDoors.tsx
@@ -2134,11 +2240,11 @@ function AttachedItemsBody({
2134
2240
  const notifier = useNotifier();
2135
2241
  const info = store.registry.getEntityInfo(token);
2136
2242
  const Icon = info.Icon ?? DefaultEntityIcon;
2137
- const [titles, setTitles] = (0, import_react13.useState)(null);
2138
- const [titleError, setTitleError] = (0, import_react13.useState)(null);
2139
- const [busyId, setBusyId] = (0, import_react13.useState)(null);
2243
+ const [titles, setTitles] = (0, import_react14.useState)(null);
2244
+ const [titleError, setTitleError] = (0, import_react14.useState)(null);
2245
+ const [busyId, setBusyId] = (0, import_react14.useState)(null);
2140
2246
  const idKey = links.map((l) => l.resourceId).sort().join(",");
2141
- (0, import_react13.useEffect)(() => {
2247
+ (0, import_react14.useEffect)(() => {
2142
2248
  if (!enabled) return;
2143
2249
  const ids = idKey ? idKey.split(",") : [];
2144
2250
  if (ids.length === 0) {
@@ -2251,7 +2357,7 @@ function AttachedItemsBody({
2251
2357
  }
2252
2358
 
2253
2359
  // src/react/components/AssociationCard.tsx
2254
- var import_react14 = require("react");
2360
+ var import_react15 = require("react");
2255
2361
  var import_design_system6 = require("@ai-matrx/design-system");
2256
2362
 
2257
2363
  // src/react/contentRoles.ts
@@ -2311,11 +2417,11 @@ function AssociationCard({
2311
2417
  const store = useAssociationsStore();
2312
2418
  const fromCtx = usePrimaryEntity();
2313
2419
  const container = containerProp ?? fromCtx;
2314
- const [open, setOpen] = (0, import_react14.useState)(false);
2420
+ const [open, setOpen] = (0, import_react15.useState)(false);
2315
2421
  const info = store.registry.getEntityInfo(token);
2316
2422
  const role = getContentRoleMeta(info.contentRole);
2317
2423
  const Icon = info.Icon ?? DefaultEntityIcon;
2318
- const [listOpen, setListOpen] = (0, import_react14.useState)(false);
2424
+ const [listOpen, setListOpen] = (0, import_react15.useState)(false);
2319
2425
  const { status, countFor, attachedIdsFor, linksFor, attach, detach } = useContainerLinks({
2320
2426
  containerType: container?.type ?? "organization",
2321
2427
  containerId: container?.id ?? null,
@@ -2469,7 +2575,7 @@ function AssociationCardGrid({
2469
2575
  }
2470
2576
 
2471
2577
  // src/react/components/AssociationList.tsx
2472
- var import_react15 = require("react");
2578
+ var import_react16 = require("react");
2473
2579
  var import_design_system8 = require("@ai-matrx/design-system");
2474
2580
  var import_jsx_runtime11 = require("react/jsx-runtime");
2475
2581
  function useContainerLinksAdapter(container, tokens) {
@@ -2509,9 +2615,9 @@ function AssociationList(props) {
2509
2615
  const adapter = props.adapter ?? defaultAdapter;
2510
2616
  const variant = props.variant ?? "full";
2511
2617
  const tokenFilter = props.tokens ?? null;
2512
- const [pickerToken, setPickerToken] = (0, import_react15.useState)(null);
2513
- const [showUniversal, setShowUniversal] = (0, import_react15.useState)(false);
2514
- const [removingKeys, setRemovingKeys] = (0, import_react15.useState)(/* @__PURE__ */ new Set());
2618
+ const [pickerToken, setPickerToken] = (0, import_react16.useState)(null);
2619
+ const [showUniversal, setShowUniversal] = (0, import_react16.useState)(false);
2620
+ const [removingKeys, setRemovingKeys] = (0, import_react16.useState)(/* @__PURE__ */ new Set());
2515
2621
  const rows = tokenFilter ? adapter.rows.filter((r) => tokenFilter.includes(r.token)) : adapter.rows;
2516
2622
  const visibleRows = rows.filter((r) => !removingKeys.has(r.key));
2517
2623
  const { titleFor } = useEntityTitles(
@@ -2823,7 +2929,7 @@ function titleize(token) {
2823
2929
  }
2824
2930
 
2825
2931
  // src/react/components/AssociationCaptureToolbar.tsx
2826
- var import_react16 = require("react");
2932
+ var import_react17 = require("react");
2827
2933
  var import_design_system9 = require("@ai-matrx/design-system");
2828
2934
  var import_jsx_runtime12 = require("react/jsx-runtime");
2829
2935
  function AssociationCaptureToolbar({
@@ -2843,13 +2949,13 @@ function AssociationCaptureToolbar({
2843
2949
  const store = useAssociationsStore();
2844
2950
  const { capture } = useAssociationsUiPorts();
2845
2951
  const notifier = useNotifier();
2846
- const fileInputRef = (0, import_react16.useRef)(null);
2847
- const [isUploading, setIsUploading] = (0, import_react16.useState)(false);
2848
- const [isDragOver, setIsDragOver] = (0, import_react16.useState)(false);
2849
- const [isPicking, setIsPicking] = (0, import_react16.useState)(false);
2850
- const [namingDoc, setNamingDoc] = (0, import_react16.useState)(false);
2851
- const [docName, setDocName] = (0, import_react16.useState)("");
2852
- const [creatingDoc, setCreatingDoc] = (0, import_react16.useState)(false);
2952
+ const fileInputRef = (0, import_react17.useRef)(null);
2953
+ const [isUploading, setIsUploading] = (0, import_react17.useState)(false);
2954
+ const [isDragOver, setIsDragOver] = (0, import_react17.useState)(false);
2955
+ const [isPicking, setIsPicking] = (0, import_react17.useState)(false);
2956
+ const [namingDoc, setNamingDoc] = (0, import_react17.useState)(false);
2957
+ const [docName, setDocName] = (0, import_react17.useState)("");
2958
+ const [creatingDoc, setCreatingDoc] = (0, import_react17.useState)(false);
2853
2959
  const actions = {
2854
2960
  upload: (showActions?.upload ?? true) && Boolean(capture?.requestUpload),
2855
2961
  addFile: (showActions?.addFile ?? true) && Boolean(capture?.openFilePicker),
@@ -3155,4 +3261,1129 @@ function CaptureToolbarAction({
3155
3261
  }
3156
3262
  );
3157
3263
  }
3264
+
3265
+ // src/react/components/AssociationEntitySelect.tsx
3266
+ var import_react18 = require("react");
3267
+ var import_design_system10 = require("@ai-matrx/design-system");
3268
+ var import_jsx_runtime13 = require("react/jsx-runtime");
3269
+ function AssociationEntitySelect({
3270
+ token,
3271
+ adapter,
3272
+ className,
3273
+ labelClassName,
3274
+ align = "end",
3275
+ showIcon = true,
3276
+ renameActivation = "click",
3277
+ emptyLabel,
3278
+ iconClassName,
3279
+ createSlot
3280
+ }) {
3281
+ const store = useAssociationsStore();
3282
+ const { entityDoors } = useAssociationsUiPorts();
3283
+ const notifier = useNotifier();
3284
+ const info = store.registry.getEntityInfo(token);
3285
+ const InfoIcon = info.Icon ?? DefaultEntityIcon;
3286
+ const DoorControls = entityDoors?.DoorControls;
3287
+ const { items, activeId } = adapter;
3288
+ const active = items.find((i) => i.id === activeId) ?? null;
3289
+ const activeIndex = active ? items.indexOf(active) : -1;
3290
+ const [open, setOpen] = (0, import_react18.useState)(false);
3291
+ const [query, setQuery] = (0, import_react18.useState)("");
3292
+ const [creating, setCreating] = (0, import_react18.useState)(false);
3293
+ const [draftName, setDraftName] = (0, import_react18.useState)("");
3294
+ const [busy, setBusy] = (0, import_react18.useState)(false);
3295
+ const entityLabel = info.label.toLowerCase();
3296
+ const close = () => {
3297
+ setOpen(false);
3298
+ setQuery("");
3299
+ setCreating(false);
3300
+ setDraftName("");
3301
+ };
3302
+ const create = async (title) => {
3303
+ const name = title.trim();
3304
+ if (!name || busy || !adapter.createAndAttach) return;
3305
+ setBusy(true);
3306
+ const id = await adapter.createAndAttach(name);
3307
+ setBusy(false);
3308
+ if (id) close();
3309
+ else notifier.error(`Couldn't create the ${entityLabel}`);
3310
+ };
3311
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
3312
+ "div",
3313
+ {
3314
+ className: (0, import_design_system10.cn)(
3315
+ "group/entity-ref flex min-w-0 items-center gap-0.5",
3316
+ className
3317
+ ),
3318
+ children: [
3319
+ showIcon ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3320
+ InfoIcon,
3321
+ {
3322
+ className: (0, import_design_system10.cn)(
3323
+ "size-3.5 shrink-0",
3324
+ iconClassName ?? "text-muted-foreground"
3325
+ ),
3326
+ "aria-hidden": true
3327
+ }
3328
+ ) : null,
3329
+ active ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3330
+ import_design_system10.EditableLabel,
3331
+ {
3332
+ value: active.title,
3333
+ onCommit: async (next) => {
3334
+ const ok = await adapter.rename(active.id, next);
3335
+ if (!ok) {
3336
+ notifier.error(`Couldn't rename the ${entityLabel}`);
3337
+ throw new Error("rename failed");
3338
+ }
3339
+ },
3340
+ commitMode: "await",
3341
+ activation: renameActivation,
3342
+ ariaLabel: info.label,
3343
+ className: (0, import_design_system10.cn)("max-w-[14rem]", labelClassName),
3344
+ displayClassName: "text-xs font-medium text-foreground",
3345
+ inputClassName: "text-xs font-medium"
3346
+ }
3347
+ ) : null,
3348
+ active && DoorControls ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(DoorControls, { token, id: active.id, name: active.title }) : null,
3349
+ !active ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3350
+ "span",
3351
+ {
3352
+ className: (0, import_design_system10.cn)(
3353
+ "truncate px-1 text-xs font-medium text-muted-foreground",
3354
+ labelClassName
3355
+ ),
3356
+ children: adapter.loading ? "\u2026" : emptyLabel ?? info.labelPlural
3357
+ }
3358
+ ) : null,
3359
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
3360
+ import_design_system10.Popover,
3361
+ {
3362
+ open,
3363
+ onOpenChange: (next) => next ? setOpen(true) : close(),
3364
+ children: [
3365
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_design_system10.PopoverTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
3366
+ "button",
3367
+ {
3368
+ type: "button",
3369
+ className: "inline-flex h-6 shrink-0 items-center gap-0.5 rounded-md px-1 text-[10px] font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
3370
+ title: `Switch or add ${info.labelPlural.toLowerCase()}`,
3371
+ "aria-label": `Switch or add ${info.labelPlural.toLowerCase()}`,
3372
+ children: [
3373
+ items.length > 1 ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("span", { className: "tabular-nums", children: [
3374
+ activeIndex >= 0 ? activeIndex + 1 : "\u2014",
3375
+ "/",
3376
+ items.length
3377
+ ] }) : null,
3378
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ChevronDownIcon, { className: "size-3 opacity-60" })
3379
+ ]
3380
+ }
3381
+ ) }),
3382
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_design_system10.PopoverContent, { className: "w-60 p-0", align, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_design_system10.Command, { children: [
3383
+ items.length > 5 ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3384
+ import_design_system10.CommandInput,
3385
+ {
3386
+ value: query,
3387
+ onValueChange: setQuery,
3388
+ placeholder: `Search ${info.labelPlural.toLowerCase()}\u2026`
3389
+ }
3390
+ ) : null,
3391
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_design_system10.CommandList, { children: [
3392
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_design_system10.CommandEmpty, { children: "No match." }),
3393
+ items.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_design_system10.CommandGroup, { children: items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
3394
+ import_design_system10.CommandItem,
3395
+ {
3396
+ value: `${item.title} ${item.id}`,
3397
+ onSelect: () => {
3398
+ if (item.id !== activeId)
3399
+ void adapter.setActive(item.id);
3400
+ close();
3401
+ },
3402
+ className: "group/entity-ref group gap-2",
3403
+ children: [
3404
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3405
+ CheckIcon,
3406
+ {
3407
+ className: (0, import_design_system10.cn)(
3408
+ "size-3.5 shrink-0",
3409
+ item.id === activeId ? "opacity-100" : "opacity-0"
3410
+ )
3411
+ }
3412
+ ),
3413
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "min-w-0 flex-1 truncate", children: item.title }),
3414
+ DoorControls ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3415
+ "span",
3416
+ {
3417
+ className: "inline-flex shrink-0 items-center",
3418
+ onPointerDown: (e) => {
3419
+ e.preventDefault();
3420
+ e.stopPropagation();
3421
+ },
3422
+ children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3423
+ DoorControls,
3424
+ {
3425
+ token,
3426
+ id: item.id,
3427
+ name: item.title
3428
+ }
3429
+ )
3430
+ }
3431
+ ) : null,
3432
+ adapter.detach && item.id !== activeId ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3433
+ "button",
3434
+ {
3435
+ type: "button",
3436
+ onPointerDown: (e) => {
3437
+ e.preventDefault();
3438
+ e.stopPropagation();
3439
+ },
3440
+ onClick: (e) => {
3441
+ e.stopPropagation();
3442
+ void adapter.detach?.(item.id);
3443
+ },
3444
+ className: "grid size-5 shrink-0 place-items-center rounded text-muted-foreground opacity-0 transition-opacity hover:bg-accent hover:text-destructive group-hover:opacity-100",
3445
+ title: `Remove this ${entityLabel} from here (does not delete it)`,
3446
+ "aria-label": `Remove ${item.title}`,
3447
+ children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(XIcon, { className: "size-3" })
3448
+ }
3449
+ ) : null
3450
+ ]
3451
+ },
3452
+ item.id
3453
+ )) }) : null
3454
+ ] }),
3455
+ createSlot !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "border-t border-border p-1", children: typeof createSlot === "function" ? createSlot(close) : createSlot }) : null,
3456
+ createSlot === void 0 && adapter.createAndAttach ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "border-t border-border p-1", children: creating ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "flex items-center gap-1 px-1 py-0.5", children: [
3457
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3458
+ "input",
3459
+ {
3460
+ autoFocus: true,
3461
+ type: "text",
3462
+ value: draftName,
3463
+ disabled: busy,
3464
+ placeholder: `New ${entityLabel} name\u2026`,
3465
+ onChange: (e) => setDraftName(e.target.value),
3466
+ onKeyDown: (e) => {
3467
+ if (e.key === "Enter") {
3468
+ e.preventDefault();
3469
+ void create(draftName);
3470
+ } else if (e.key === "Escape") {
3471
+ e.preventDefault();
3472
+ e.stopPropagation();
3473
+ setCreating(false);
3474
+ setDraftName("");
3475
+ }
3476
+ },
3477
+ className: "h-6 w-full min-w-0 rounded-sm bg-transparent px-1 text-base outline-none focus:bg-background focus:ring-1 focus:ring-ring md:text-xs",
3478
+ "aria-label": `New ${entityLabel} name`
3479
+ }
3480
+ ),
3481
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3482
+ "button",
3483
+ {
3484
+ type: "button",
3485
+ disabled: busy || !draftName.trim(),
3486
+ onClick: () => void create(draftName),
3487
+ className: "inline-flex h-6 shrink-0 items-center rounded-md px-1.5 text-[10px] font-medium text-primary transition-colors hover:bg-accent disabled:opacity-50",
3488
+ children: busy ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(SpinnerIcon, { className: "size-3 animate-spin" }) : "Create"
3489
+ }
3490
+ )
3491
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
3492
+ "button",
3493
+ {
3494
+ type: "button",
3495
+ disabled: busy,
3496
+ onClick: () => {
3497
+ if (query.trim()) void create(query);
3498
+ else setCreating(true);
3499
+ },
3500
+ className: "flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
3501
+ children: [
3502
+ busy ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(SpinnerIcon, { className: "size-3.5 animate-spin" }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(PlusIcon, { className: "size-3.5" }),
3503
+ query.trim() ? `Create "${query.trim()}"` : `New ${info.label}`
3504
+ ]
3505
+ }
3506
+ ) }) : null
3507
+ ] }) })
3508
+ ]
3509
+ }
3510
+ )
3511
+ ]
3512
+ }
3513
+ );
3514
+ }
3515
+
3516
+ // src/react/components/CategorySelect.tsx
3517
+ var import_react19 = require("react");
3518
+ var import_design_system11 = require("@ai-matrx/design-system");
3519
+
3520
+ // src/core/categoryHierarchy.ts
3521
+ function buildCategoryHierarchy(categories) {
3522
+ const byId = new Map(categories.map((category) => [category.id, category]));
3523
+ const hasHierarchy = categories.some(
3524
+ (category) => category.parentId !== null && byId.has(category.parentId)
3525
+ );
3526
+ if (!hasHierarchy) {
3527
+ return {
3528
+ items: categories.map((category) => ({
3529
+ category,
3530
+ depth: 0,
3531
+ parent: null,
3532
+ displayName: category.name
3533
+ })),
3534
+ roots: categories.filter((category) => category.parentId === null),
3535
+ hasHierarchy: false
3536
+ };
3537
+ }
3538
+ const roots = categories.filter(
3539
+ (category) => category.parentId === null || !byId.has(category.parentId)
3540
+ );
3541
+ const childrenByParent = /* @__PURE__ */ new Map();
3542
+ for (const category of categories) {
3543
+ if (!category.parentId || !byId.has(category.parentId)) continue;
3544
+ const children = childrenByParent.get(category.parentId) ?? [];
3545
+ children.push(category);
3546
+ childrenByParent.set(category.parentId, children);
3547
+ }
3548
+ const items = [];
3549
+ const seen = /* @__PURE__ */ new Set();
3550
+ for (const root of roots) {
3551
+ items.push({
3552
+ category: root,
3553
+ depth: 0,
3554
+ parent: null,
3555
+ displayName: root.name
3556
+ });
3557
+ seen.add(root.id);
3558
+ for (const child of childrenByParent.get(root.id) ?? []) {
3559
+ items.push({
3560
+ category: child,
3561
+ depth: 1,
3562
+ parent: root,
3563
+ displayName: `${root.name} / ${child.name}`
3564
+ });
3565
+ seen.add(child.id);
3566
+ }
3567
+ }
3568
+ for (const category of categories) {
3569
+ if (seen.has(category.id)) continue;
3570
+ items.push({
3571
+ category,
3572
+ depth: 0,
3573
+ parent: null,
3574
+ displayName: category.name
3575
+ });
3576
+ }
3577
+ return {
3578
+ items,
3579
+ roots: categories.filter((category) => category.parentId === null),
3580
+ hasHierarchy: true
3581
+ };
3582
+ }
3583
+
3584
+ // src/react/components/CategorySelect.tsx
3585
+ var import_jsx_runtime14 = require("react/jsx-runtime");
3586
+ var NONE = "__none__";
3587
+ var ROOT_PARENT = "__root__";
3588
+ function plural(noun) {
3589
+ return /[^aeiou]y$/i.test(noun) ? `${noun.slice(0, -1)}ies` : `${noun}s`;
3590
+ }
3591
+ function CategorySelect({
3592
+ dimension,
3593
+ value,
3594
+ onChange,
3595
+ placeholder,
3596
+ allowNone = true,
3597
+ disabled,
3598
+ allowCreate = true,
3599
+ orgId,
3600
+ noun = "category"
3601
+ }) {
3602
+ const store = useAssociationsStore();
3603
+ const notifier = useNotifier();
3604
+ const {
3605
+ categories,
3606
+ status,
3607
+ error,
3608
+ create: createCategory,
3609
+ reload
3610
+ } = useCategories({ dimension });
3611
+ const hierarchy = buildCategoryHierarchy(categories);
3612
+ const [createParentId, setCreateParentId] = (0, import_react19.useState)(ROOT_PARENT);
3613
+ const options = [
3614
+ ...allowNone ? [{ value: NONE, label: "None", keywords: "none clear empty" }] : [],
3615
+ ...hierarchy.hasHierarchy ? hierarchy.items.map(({ category, depth, parent }) => ({
3616
+ value: category.id,
3617
+ label: category.name,
3618
+ // The type-ahead matches the parent's name too, so someone who knows
3619
+ // the branch but not the leaf still finds it.
3620
+ keywords: parent ? parent.name : "",
3621
+ render: depth === 0 ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "font-medium", children: category.name }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("span", { className: "flex items-center gap-1.5 pl-3 text-muted-foreground", children: [
3622
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(CornerDownRightIcon, { className: "h-3.5 w-3.5 shrink-0" }),
3623
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "text-foreground", children: category.name })
3624
+ ] })
3625
+ })) : categories.map((category) => ({
3626
+ value: category.id,
3627
+ label: category.name
3628
+ }))
3629
+ ];
3630
+ const selected = value ? hierarchy.items.find((item) => item.category.id === value)?.displayName ?? categories.find((category) => category.id === value)?.name ?? null : null;
3631
+ const create = async (typed) => {
3632
+ const name = typed.trim();
3633
+ if (!name) return null;
3634
+ const existing = categories.find(
3635
+ (category) => category.name.toLowerCase() === name.toLowerCase()
3636
+ );
3637
+ if (existing) return existing.id;
3638
+ let resolvedOrgId = orgId ?? null;
3639
+ if (!resolvedOrgId && store.identity.ensureOrgId) {
3640
+ try {
3641
+ resolvedOrgId = await store.identity.ensureOrgId();
3642
+ } catch (e) {
3643
+ store.errorSink({
3644
+ code: "ensure_org_failed",
3645
+ message: "[CategorySelect] identity.ensureOrgId threw",
3646
+ context: { error: e instanceof Error ? e.message : String(e) }
3647
+ });
3648
+ }
3649
+ }
3650
+ if (!resolvedOrgId) {
3651
+ notifier.error(`Select an organization before creating a ${noun}.`);
3652
+ return null;
3653
+ }
3654
+ const parentId = hierarchy.hasHierarchy && createParentId !== ROOT_PARENT ? createParentId : null;
3655
+ const result = await createCategory({ name, orgId: resolvedOrgId, parentId });
3656
+ if (!result.ok || !result.id) {
3657
+ notifier.error(`Couldn't create that ${noun}`, {
3658
+ ...result.error != null ? { description: result.error } : {}
3659
+ });
3660
+ return null;
3661
+ }
3662
+ await reload();
3663
+ setCreateParentId(ROOT_PARENT);
3664
+ const under = parentId ? categories.find((category) => category.id === parentId)?.name : null;
3665
+ notifier.success(
3666
+ `\u201C${name}\u201D is now one of your ${plural(noun)}${under ? ` \u2014 it sits under ${under}, and every screen that offers ${plural(noun)} offers it.` : ` \u2014 every screen that offers ${plural(noun)} offers it from now on.`}`
3667
+ );
3668
+ return result.id;
3669
+ };
3670
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { children: [
3671
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
3672
+ import_design_system11.CreatablePicker,
3673
+ {
3674
+ value: value ?? (allowNone ? NONE : null),
3675
+ onSelect: (next) => onChange(next === NONE ? null : next),
3676
+ options,
3677
+ placeholder,
3678
+ searchPlaceholder: allowCreate ? `Search or add a ${noun}\u2026` : `Search ${plural(noun)}\u2026`,
3679
+ noun,
3680
+ ariaLabel: placeholder,
3681
+ className: "text-sm",
3682
+ ...disabled !== void 0 ? { disabled } : {},
3683
+ loading: status === "loading",
3684
+ emptyLabel: `No ${noun} matches that.`,
3685
+ ...allowCreate ? { onCreate: create } : {},
3686
+ ...allowCreate && hierarchy.hasHierarchy ? {
3687
+ createExtra: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_design_system11.Select, { value: createParentId, onValueChange: setCreateParentId, children: [
3688
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
3689
+ import_design_system11.SelectTrigger,
3690
+ {
3691
+ className: "h-7 text-xs",
3692
+ "aria-label": `Put the new ${noun} under`,
3693
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_design_system11.SelectValue, {})
3694
+ }
3695
+ ),
3696
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(import_design_system11.SelectContent, { children: [
3697
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_design_system11.SelectItem, { value: ROOT_PARENT, className: "text-xs", children: "Top level" }),
3698
+ hierarchy.items.filter((item) => item.depth === 0).map((item) => /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
3699
+ import_design_system11.SelectItem,
3700
+ {
3701
+ value: item.category.id,
3702
+ className: "text-xs",
3703
+ children: [
3704
+ "Under ",
3705
+ item.category.name
3706
+ ]
3707
+ },
3708
+ item.category.id
3709
+ ))
3710
+ ] })
3711
+ ] })
3712
+ } : {}
3713
+ }
3714
+ ),
3715
+ selected === null && value !== null ? (
3716
+ // A stored id whose row this reader cannot see is not "nothing chosen":
3717
+ // saying so beats a blank box that looks like an unsaved field.
3718
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("p", { className: "mt-1 text-xs text-muted-foreground", children: [
3719
+ "This ",
3720
+ noun,
3721
+ " is no longer available to you."
3722
+ ] })
3723
+ ) : null,
3724
+ error ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "mt-1 text-xs text-destructive", children: error }) : null
3725
+ ] });
3726
+ }
3727
+
3728
+ // src/react/components/CategoryTagPicker.tsx
3729
+ var import_react20 = require("react");
3730
+ var import_design_system12 = require("@ai-matrx/design-system");
3731
+ var import_jsx_runtime15 = require("react/jsx-runtime");
3732
+ var ROOT_PARENT2 = "__root__";
3733
+ function CategoryTagPicker({
3734
+ entityType,
3735
+ entityId,
3736
+ dimension,
3737
+ edgeRole,
3738
+ orgId,
3739
+ addLabel = "Add tag",
3740
+ icon: Icon = TagIcon,
3741
+ allowCreate = true,
3742
+ hierarchical,
3743
+ emptyText = "No categories yet."
3744
+ }) {
3745
+ const store = useAssociationsStore();
3746
+ const notifier = useNotifier();
3747
+ const [open, setOpen] = (0, import_react20.useState)(false);
3748
+ const [creating, setCreating] = (0, import_react20.useState)(false);
3749
+ const [writing, setWriting] = (0, import_react20.useState)(false);
3750
+ const [search, setSearch] = (0, import_react20.useState)("");
3751
+ const [createParentId, setCreateParentId] = (0, import_react20.useState)(ROOT_PARENT2);
3752
+ const {
3753
+ categories,
3754
+ create: createCategory,
3755
+ reload: reloadCategories
3756
+ } = useCategories({ dimension });
3757
+ const { edges, setTargets } = useAssociations({
3758
+ type: entityType,
3759
+ id: entityId
3760
+ });
3761
+ const hierarchy = buildCategoryHierarchy(categories);
3762
+ const showHierarchy = hierarchical ?? hierarchy.hasHierarchy;
3763
+ const hierarchyById = new Map(
3764
+ hierarchy.items.map((item) => [item.category.id, item])
3765
+ );
3766
+ const selectedIds = new Set(
3767
+ edges.filter(
3768
+ (e) => e.direction === "outgoing" && e.otherType === "category" && e.role === edgeRole
3769
+ ).map((e) => e.otherId)
3770
+ );
3771
+ const selected = categories.filter((c) => selectedIds.has(c.id));
3772
+ const toggle = async (categoryId) => {
3773
+ if (writing) return;
3774
+ setWriting(true);
3775
+ try {
3776
+ const next = selectedIds.has(categoryId) ? [...selectedIds].filter((id) => id !== categoryId) : [...selectedIds, categoryId];
3777
+ const res = await setTargets({
3778
+ targetType: "category",
3779
+ targetIds: next,
3780
+ ...orgId != null ? { orgId } : {},
3781
+ role: edgeRole
3782
+ });
3783
+ if (!res.ok) {
3784
+ store.errorSink({
3785
+ code: "category_tag_set_targets_failed",
3786
+ message: "[CategoryTagPicker] setTargets failed",
3787
+ context: { entityType, entityId, edgeRole, error: res.error }
3788
+ });
3789
+ notifier.error(res.error ?? "Could not update categories");
3790
+ }
3791
+ } finally {
3792
+ setWriting(false);
3793
+ }
3794
+ };
3795
+ const createAndAttach = async () => {
3796
+ const name = search.trim();
3797
+ if (!name) return;
3798
+ setCreating(true);
3799
+ try {
3800
+ let resolvedOrgId = orgId ?? null;
3801
+ if (!resolvedOrgId && store.identity.ensureOrgId) {
3802
+ try {
3803
+ resolvedOrgId = await store.identity.ensureOrgId();
3804
+ } catch (e) {
3805
+ store.errorSink({
3806
+ code: "ensure_org_failed",
3807
+ message: "[CategoryTagPicker] identity.ensureOrgId threw",
3808
+ context: { error: e instanceof Error ? e.message : String(e) }
3809
+ });
3810
+ }
3811
+ }
3812
+ if (!resolvedOrgId) {
3813
+ store.errorSink({
3814
+ code: "ensure_org_missing",
3815
+ message: "[CategoryTagPicker] no org for the created category \u2014 pass `orgId` or bind `identity.ensureOrgId` on the store config",
3816
+ context: { entityType, entityId, dimension }
3817
+ });
3818
+ notifier.error(
3819
+ "Could not create the category \u2014 no organization is selected"
3820
+ );
3821
+ return;
3822
+ }
3823
+ const parentId = showHierarchy && createParentId !== ROOT_PARENT2 ? createParentId : null;
3824
+ const createRes = await createCategory({
3825
+ name,
3826
+ orgId: resolvedOrgId,
3827
+ parentId
3828
+ });
3829
+ if (!createRes.ok || !createRes.id) {
3830
+ store.errorSink({
3831
+ code: "category_tag_create_failed",
3832
+ message: "[CategoryTagPicker] create failed",
3833
+ context: { dimension, error: createRes.error }
3834
+ });
3835
+ notifier.error(createRes.error ?? "Could not create the category");
3836
+ return;
3837
+ }
3838
+ await reloadCategories();
3839
+ await toggle(createRes.id);
3840
+ setSearch("");
3841
+ setCreateParentId(ROOT_PARENT2);
3842
+ } finally {
3843
+ setCreating(false);
3844
+ }
3845
+ };
3846
+ const filtered = categories.filter(
3847
+ (c) => c.name.toLowerCase().includes(search.trim().toLowerCase())
3848
+ );
3849
+ const filteredHierarchy = hierarchy.items.filter(
3850
+ (item) => item.displayName.toLowerCase().includes(search.trim().toLowerCase())
3851
+ );
3852
+ const exactMatch = categories.some(
3853
+ (c) => c.name.toLowerCase() === search.trim().toLowerCase()
3854
+ );
3855
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex flex-wrap items-center gap-1.5", children: [
3856
+ selected.map((c) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
3857
+ import_design_system12.Badge,
3858
+ {
3859
+ variant: "secondary",
3860
+ className: "gap-1 pr-1 text-xs font-medium",
3861
+ children: [
3862
+ showHierarchy ? hierarchyById.get(c.id)?.displayName ?? c.name : c.name,
3863
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3864
+ "button",
3865
+ {
3866
+ type: "button",
3867
+ onClick: () => void toggle(c.id),
3868
+ className: "ml-0.5 rounded-full p-0.5 hover:bg-muted-foreground/20",
3869
+ "aria-label": `Remove ${c.name}`,
3870
+ children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(XIcon, { className: "h-2.5 w-2.5" })
3871
+ }
3872
+ )
3873
+ ]
3874
+ },
3875
+ c.id
3876
+ )),
3877
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_design_system12.Popover, { open, onOpenChange: setOpen, children: [
3878
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_design_system12.PopoverTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
3879
+ import_design_system12.Button,
3880
+ {
3881
+ variant: "outline",
3882
+ size: "sm",
3883
+ className: "h-7 gap-1 px-2 text-xs",
3884
+ children: [
3885
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Icon, { className: "h-3.5 w-3.5" }),
3886
+ selected.length === 0 ? addLabel : "Edit",
3887
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ChevronsUpDownIcon, { className: "h-3 w-3 opacity-50" })
3888
+ ]
3889
+ }
3890
+ ) }),
3891
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_design_system12.PopoverContent, { className: "w-56 p-0", align: "start", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_design_system12.Command, { shouldFilter: false, children: [
3892
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3893
+ import_design_system12.CommandInput,
3894
+ {
3895
+ placeholder: allowCreate ? "Search or create..." : "Search...",
3896
+ value: search,
3897
+ onValueChange: setSearch
3898
+ }
3899
+ ),
3900
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_design_system12.CommandList, { children: [
3901
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_design_system12.CommandEmpty, { className: "px-2 py-3 text-xs text-muted-foreground", children: search.trim() ? "No match." : emptyText }),
3902
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_design_system12.CommandGroup, { children: showHierarchy ? filteredHierarchy.map(({ category, depth, parent }) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
3903
+ import_design_system12.CommandItem,
3904
+ {
3905
+ value: category.id,
3906
+ keywords: parent ? [parent.name, category.name] : [],
3907
+ onSelect: () => void toggle(category.id),
3908
+ className: (0, import_design_system12.cn)("text-xs", depth === 1 && "pl-6"),
3909
+ children: [
3910
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3911
+ CheckIcon,
3912
+ {
3913
+ className: (0, import_design_system12.cn)(
3914
+ "mr-2 h-3.5 w-3.5",
3915
+ selectedIds.has(category.id) ? "opacity-100" : "opacity-0"
3916
+ )
3917
+ }
3918
+ ),
3919
+ depth === 1 ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(CornerDownRightIcon, { className: "mr-1.5 h-3.5 w-3.5 shrink-0 text-muted-foreground" }) : null,
3920
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: (0, import_design_system12.cn)(depth === 0 && "font-medium"), children: category.name })
3921
+ ]
3922
+ },
3923
+ category.id
3924
+ )) : filtered.map((c) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
3925
+ import_design_system12.CommandItem,
3926
+ {
3927
+ value: c.id,
3928
+ onSelect: () => void toggle(c.id),
3929
+ className: "text-xs",
3930
+ children: [
3931
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3932
+ CheckIcon,
3933
+ {
3934
+ className: (0, import_design_system12.cn)(
3935
+ "mr-2 h-3.5 w-3.5",
3936
+ selectedIds.has(c.id) ? "opacity-100" : "opacity-0"
3937
+ )
3938
+ }
3939
+ ),
3940
+ c.name
3941
+ ]
3942
+ },
3943
+ c.id
3944
+ )) }),
3945
+ allowCreate && search.trim() && !exactMatch ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_design_system12.CommandGroup, { children: [
3946
+ showHierarchy ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "space-y-1.5 px-2 pb-2", children: [
3947
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-[11px] font-medium text-muted-foreground", children: "Place in" }),
3948
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
3949
+ import_design_system12.Select,
3950
+ {
3951
+ value: createParentId,
3952
+ onValueChange: setCreateParentId,
3953
+ children: [
3954
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_design_system12.SelectTrigger, { size: "sm", className: "bg-background", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_design_system12.SelectValue, {}) }),
3955
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_design_system12.SelectContent, { children: [
3956
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_design_system12.SelectItem, { value: ROOT_PARENT2, children: "Top level" }),
3957
+ hierarchy.roots.map((root) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_design_system12.SelectItem, { value: root.id, children: root.name }, root.id))
3958
+ ] })
3959
+ ]
3960
+ }
3961
+ )
3962
+ ] }) : null,
3963
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
3964
+ import_design_system12.CommandItem,
3965
+ {
3966
+ value: `__create__${search}`,
3967
+ onSelect: () => void createAndAttach(),
3968
+ disabled: creating,
3969
+ className: "text-xs text-primary",
3970
+ children: [
3971
+ creating ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(SpinnerIcon, { className: "mr-2 h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Icon, { className: "mr-2 h-3.5 w-3.5" }),
3972
+ "Create \u201C",
3973
+ search.trim(),
3974
+ "\u201D"
3975
+ ]
3976
+ }
3977
+ )
3978
+ ] }) : null
3979
+ ] })
3980
+ ] }) })
3981
+ ] })
3982
+ ] });
3983
+ }
3984
+
3985
+ // src/react/components/CommentThread.tsx
3986
+ var import_react21 = require("react");
3987
+ var import_design_system13 = require("@ai-matrx/design-system");
3988
+
3989
+ // src/react/relativeTime.ts
3990
+ var UNITS = [
3991
+ { unit: "year", ms: 365 * 24 * 60 * 60 * 1e3 },
3992
+ { unit: "month", ms: 30 * 24 * 60 * 60 * 1e3 },
3993
+ { unit: "week", ms: 7 * 24 * 60 * 60 * 1e3 },
3994
+ { unit: "day", ms: 24 * 60 * 60 * 1e3 },
3995
+ { unit: "hour", ms: 60 * 60 * 1e3 },
3996
+ { unit: "minute", ms: 60 * 1e3 }
3997
+ ];
3998
+ function formatRelativeTime(iso, now = Date.now()) {
3999
+ const t = Date.parse(iso);
4000
+ if (Number.isNaN(t)) return iso;
4001
+ const delta = t - now;
4002
+ const magnitude = Math.abs(delta);
4003
+ if (magnitude < 60 * 1e3) return "just now";
4004
+ const rtf = new Intl.RelativeTimeFormat(void 0, { numeric: "auto" });
4005
+ for (const { unit, ms } of UNITS) {
4006
+ if (magnitude >= ms) {
4007
+ return rtf.format(Math.trunc(delta / ms), unit);
4008
+ }
4009
+ }
4010
+ return "just now";
4011
+ }
4012
+
4013
+ // src/react/components/CommentThread.tsx
4014
+ var import_jsx_runtime16 = require("react/jsx-runtime");
4015
+ var AUTHOR_DOOR_TOKEN = "user";
4016
+ function resolveAuthor(comment, authorDisplay) {
4017
+ const base2 = {
4018
+ userId: comment.createdBy,
4019
+ email: comment.author.email,
4020
+ displayName: comment.author.displayName,
4021
+ avatarUrl: comment.author.avatarUrl
4022
+ };
4023
+ const enriched = authorDisplay ? authorDisplay(base2) : null;
4024
+ const name = enriched?.displayName ?? base2.displayName ?? base2.email ?? "Unknown user";
4025
+ return {
4026
+ name,
4027
+ avatarUrl: enriched?.avatarUrl !== void 0 && enriched?.avatarUrl !== null ? enriched.avatarUrl : base2.avatarUrl,
4028
+ userId: comment.createdBy
4029
+ };
4030
+ }
4031
+ function AuthorAvatar({ author }) {
4032
+ if (author.avatarUrl) {
4033
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4034
+ "img",
4035
+ {
4036
+ src: author.avatarUrl,
4037
+ alt: "",
4038
+ className: "h-6 w-6 shrink-0 rounded-full object-cover"
4039
+ }
4040
+ );
4041
+ }
4042
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4043
+ "span",
4044
+ {
4045
+ "aria-hidden": true,
4046
+ 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",
4047
+ children: author.name.trim().charAt(0) || "?"
4048
+ }
4049
+ );
4050
+ }
4051
+ function AuthorName({
4052
+ author,
4053
+ entityDoors
4054
+ }) {
4055
+ const DoorRef2 = entityDoors?.EntityRef;
4056
+ if (DoorRef2 && author.userId) {
4057
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4058
+ DoorRef2,
4059
+ {
4060
+ token: AUTHOR_DOOR_TOKEN,
4061
+ id: author.userId,
4062
+ name: author.name,
4063
+ showIcon: false,
4064
+ className: "text-sm font-medium text-foreground"
4065
+ }
4066
+ );
4067
+ }
4068
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-sm font-medium text-foreground", children: author.name });
4069
+ }
4070
+ function CommentComposer({
4071
+ onSubmit,
4072
+ placeholder = "Write a comment\u2026",
4073
+ submitLabel = "Comment",
4074
+ initialValue = "",
4075
+ trailing,
4076
+ autoFocus = false
4077
+ }) {
4078
+ const notifier = useNotifier();
4079
+ const [body, setBody] = (0, import_react21.useState)(initialValue);
4080
+ const [submitting, setSubmitting] = (0, import_react21.useState)(false);
4081
+ const [error, setError] = (0, import_react21.useState)(null);
4082
+ async function submit(e) {
4083
+ e?.preventDefault();
4084
+ const trimmed = body.trim();
4085
+ if (!trimmed || submitting) return;
4086
+ setSubmitting(true);
4087
+ setError(null);
4088
+ const res = await onSubmit(trimmed);
4089
+ setSubmitting(false);
4090
+ if (!res.ok) {
4091
+ const msg = res.error ?? "Could not save the comment";
4092
+ setError(msg);
4093
+ notifier.error(msg);
4094
+ return;
4095
+ }
4096
+ setBody("");
4097
+ }
4098
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("form", { onSubmit: submit, className: "flex flex-col gap-1.5", children: [
4099
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4100
+ "textarea",
4101
+ {
4102
+ value: body,
4103
+ onChange: (e) => setBody(e.target.value),
4104
+ onKeyDown: (e) => {
4105
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void submit();
4106
+ },
4107
+ placeholder,
4108
+ rows: 2,
4109
+ autoFocus,
4110
+ "aria-label": placeholder,
4111
+ 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"
4112
+ }
4113
+ ),
4114
+ error && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { role: "alert", className: "text-xs text-destructive", children: error }),
4115
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex items-center gap-2", children: [
4116
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_design_system13.Button, { type: "submit", size: "sm", disabled: !body.trim() || submitting, children: [
4117
+ submitting && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(SpinnerIcon, { className: "mr-1 h-3 w-3 animate-spin" }),
4118
+ submitLabel
4119
+ ] }),
4120
+ trailing
4121
+ ] })
4122
+ ] });
4123
+ }
4124
+ function CommentItem({
4125
+ comment,
4126
+ childrenOf,
4127
+ depth,
4128
+ thread,
4129
+ orgId
4130
+ }) {
4131
+ const { authorDisplay, entityDoors } = useAssociationsUiPorts();
4132
+ const notifier = useNotifier();
4133
+ const [replying, setReplying] = (0, import_react21.useState)(false);
4134
+ const [editing, setEditing] = (0, import_react21.useState)(false);
4135
+ const [confirmingDelete, setConfirmingDelete] = (0, import_react21.useState)(false);
4136
+ const [deleting, setDeleting] = (0, import_react21.useState)(false);
4137
+ const author = resolveAuthor(comment, authorDisplay);
4138
+ const isOwn = thread.currentUserId !== null && comment.createdBy === thread.currentUserId;
4139
+ const edited = comment.updatedAt !== comment.createdAt;
4140
+ const replies = childrenOf.get(comment.id) ?? [];
4141
+ async function confirmDelete() {
4142
+ setDeleting(true);
4143
+ const res = await thread.remove(comment.id);
4144
+ setDeleting(false);
4145
+ setConfirmingDelete(false);
4146
+ if (!res.ok) {
4147
+ notifier.error(res.error ?? "Could not delete the comment");
4148
+ }
4149
+ }
4150
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
4151
+ "div",
4152
+ {
4153
+ "data-comment-id": comment.id,
4154
+ className: (0, import_design_system13.cn)(
4155
+ "flex flex-col gap-1",
4156
+ depth > 0 && "border-l border-border pl-3"
4157
+ ),
4158
+ children: [
4159
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex items-center gap-2", children: [
4160
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(AuthorAvatar, { author }),
4161
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(AuthorName, { author, entityDoors }),
4162
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4163
+ "span",
4164
+ {
4165
+ className: "text-xs text-muted-foreground",
4166
+ title: comment.createdAt,
4167
+ children: formatRelativeTime(comment.createdAt)
4168
+ }
4169
+ ),
4170
+ edited && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4171
+ "span",
4172
+ {
4173
+ className: "text-xs text-muted-foreground",
4174
+ title: comment.updatedAt,
4175
+ children: "(edited)"
4176
+ }
4177
+ )
4178
+ ] }),
4179
+ editing ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "pl-8", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4180
+ CommentComposer,
4181
+ {
4182
+ initialValue: comment.body,
4183
+ submitLabel: "Save",
4184
+ placeholder: "Edit your comment\u2026",
4185
+ autoFocus: true,
4186
+ onSubmit: async (body) => {
4187
+ const res = await thread.edit(comment.id, body);
4188
+ if (res.ok) setEditing(false);
4189
+ return res;
4190
+ },
4191
+ trailing: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4192
+ import_design_system13.Button,
4193
+ {
4194
+ type: "button",
4195
+ size: "sm",
4196
+ variant: "ghost",
4197
+ onClick: () => setEditing(false),
4198
+ children: "Cancel"
4199
+ }
4200
+ )
4201
+ }
4202
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "whitespace-pre-wrap pl-8 text-sm text-foreground", children: comment.body }),
4203
+ !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: [
4204
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-xs text-destructive", children: "Deletes this comment for everyone." }),
4205
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
4206
+ import_design_system13.Button,
4207
+ {
4208
+ type: "button",
4209
+ size: "sm",
4210
+ variant: "destructive",
4211
+ disabled: deleting,
4212
+ onClick: () => void confirmDelete(),
4213
+ children: [
4214
+ deleting && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(SpinnerIcon, { className: "mr-1 h-3 w-3 animate-spin" }),
4215
+ "Delete"
4216
+ ]
4217
+ }
4218
+ ),
4219
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4220
+ import_design_system13.Button,
4221
+ {
4222
+ type: "button",
4223
+ size: "sm",
4224
+ variant: "ghost",
4225
+ disabled: deleting,
4226
+ onClick: () => setConfirmingDelete(false),
4227
+ children: "Cancel"
4228
+ }
4229
+ )
4230
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
4231
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4232
+ "button",
4233
+ {
4234
+ type: "button",
4235
+ onClick: () => setReplying((v) => !v),
4236
+ className: "rounded px-1.5 py-0.5 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
4237
+ children: "Reply"
4238
+ }
4239
+ ),
4240
+ isOwn && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
4241
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4242
+ "button",
4243
+ {
4244
+ type: "button",
4245
+ onClick: () => setEditing(true),
4246
+ className: "rounded px-1.5 py-0.5 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
4247
+ children: "Edit"
4248
+ }
4249
+ ),
4250
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4251
+ "button",
4252
+ {
4253
+ type: "button",
4254
+ onClick: () => setConfirmingDelete(true),
4255
+ className: "rounded px-1.5 py-0.5 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-destructive",
4256
+ children: "Delete"
4257
+ }
4258
+ )
4259
+ ] })
4260
+ ] }) }),
4261
+ replying && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex items-start gap-2 pl-8 pt-1", children: [
4262
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(CornerDownRightIcon, { className: "mt-2 h-3.5 w-3.5 shrink-0 text-muted-foreground" }),
4263
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4264
+ CommentComposer,
4265
+ {
4266
+ submitLabel: "Reply",
4267
+ placeholder: `Reply to ${author.name}\u2026`,
4268
+ autoFocus: true,
4269
+ onSubmit: async (body) => {
4270
+ const res = await thread.add(body, {
4271
+ parentId: comment.id,
4272
+ orgId: orgId ?? null
4273
+ });
4274
+ if (res.ok) setReplying(false);
4275
+ return res;
4276
+ },
4277
+ trailing: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4278
+ import_design_system13.Button,
4279
+ {
4280
+ type: "button",
4281
+ size: "sm",
4282
+ variant: "ghost",
4283
+ onClick: () => setReplying(false),
4284
+ children: "Cancel"
4285
+ }
4286
+ )
4287
+ }
4288
+ ) })
4289
+ ] }),
4290
+ 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)(
4291
+ CommentItem,
4292
+ {
4293
+ comment: reply,
4294
+ childrenOf,
4295
+ depth: depth + 1,
4296
+ thread,
4297
+ orgId
4298
+ },
4299
+ reply.id
4300
+ )) })
4301
+ ]
4302
+ }
4303
+ );
4304
+ }
4305
+ function buildCommentTree(comments) {
4306
+ const byId = new Set(comments.map((c) => c.id));
4307
+ const roots = [];
4308
+ const childrenOf = /* @__PURE__ */ new Map();
4309
+ for (const c of comments) {
4310
+ if (c.parentId && byId.has(c.parentId)) {
4311
+ const bucket = childrenOf.get(c.parentId);
4312
+ if (bucket) bucket.push(c);
4313
+ else childrenOf.set(c.parentId, [c]);
4314
+ } else {
4315
+ roots.push(c);
4316
+ }
4317
+ }
4318
+ return { roots, childrenOf };
4319
+ }
4320
+ function CommentThread({
4321
+ token,
4322
+ id,
4323
+ orgId,
4324
+ showHeader = true,
4325
+ className
4326
+ }) {
4327
+ const thread = useComments({ token, id });
4328
+ const { roots, childrenOf } = buildCommentTree(thread.comments);
4329
+ const loading = (thread.status === "loading" || thread.status === "idle") && thread.comments.length === 0;
4330
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("section", { className: (0, import_design_system13.cn)("flex flex-col gap-3", className), children: [
4331
+ showHeader && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("header", { className: "flex items-center gap-2", children: [
4332
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("h3", { className: "text-sm font-semibold text-foreground", children: "Comments" }),
4333
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-xs text-muted-foreground tabular-nums", children: thread.comments.length }),
4334
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4335
+ "button",
4336
+ {
4337
+ type: "button",
4338
+ onClick: () => void thread.reload(),
4339
+ "aria-label": "Refresh comments",
4340
+ title: "Refresh comments",
4341
+ className: "ml-auto flex h-6 w-6 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
4342
+ 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" })
4343
+ }
4344
+ )
4345
+ ] }),
4346
+ thread.error && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
4347
+ "div",
4348
+ {
4349
+ role: "alert",
4350
+ className: "flex items-center gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive",
4351
+ children: [
4352
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "min-w-0 flex-1", children: thread.error }),
4353
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4354
+ import_design_system13.Button,
4355
+ {
4356
+ type: "button",
4357
+ size: "sm",
4358
+ variant: "outline",
4359
+ disabled: thread.status === "loading",
4360
+ onClick: () => void thread.reload(),
4361
+ children: "Retry"
4362
+ }
4363
+ )
4364
+ ]
4365
+ }
4366
+ ),
4367
+ loading ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex flex-col gap-3", "aria-hidden": true, "data-testid": "comments-loading", children: [
4368
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_design_system13.Skeleton, { className: "h-12 w-full" }),
4369
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_design_system13.Skeleton, { className: "h-12 w-4/5" })
4370
+ ] }) : 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)(
4371
+ CommentItem,
4372
+ {
4373
+ comment,
4374
+ childrenOf,
4375
+ depth: 0,
4376
+ thread,
4377
+ orgId
4378
+ },
4379
+ comment.id
4380
+ )) }),
4381
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4382
+ CommentComposer,
4383
+ {
4384
+ onSubmit: (body) => thread.add(body, { orgId: orgId ?? null })
4385
+ }
4386
+ )
4387
+ ] });
4388
+ }
3158
4389
  //# sourceMappingURL=index.cjs.map