@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.
@@ -13,6 +13,7 @@ function AssociationsProvider({
13
13
  capture,
14
14
  pickerOverrides,
15
15
  entityDoors,
16
+ authorDisplay,
16
17
  children
17
18
  }) {
18
19
  const notifierWarned = useRef(false);
@@ -26,7 +27,8 @@ function AssociationsProvider({
26
27
  ...windowShell !== void 0 ? { windowShell } : {},
27
28
  ...capture !== void 0 ? { capture } : {},
28
29
  ...pickerOverrides !== void 0 ? { pickerOverrides } : {},
29
- ...entityDoors !== void 0 ? { entityDoors } : {}
30
+ ...entityDoors !== void 0 ? { entityDoors } : {},
31
+ ...authorDisplay !== void 0 ? { authorDisplay } : {}
30
32
  },
31
33
  children
32
34
  }
@@ -503,6 +505,7 @@ var ENTITY_TYPE_TOKENS = [
503
505
  "platform_actor_session",
504
506
  "platform_actor_token",
505
507
  "platform_actor_token_event",
508
+ "platform_continued_access",
506
509
  "platform_outcome_event",
507
510
  "platform_outsider_consumer",
508
511
  "platform_saved_view",
@@ -531,6 +534,7 @@ var ENTITY_TYPE_TOKENS = [
531
534
  "research_tag",
532
535
  "research_template",
533
536
  "research_topic",
537
+ "route_manifest_entry",
534
538
  "rulebook",
535
539
  "sandbox_instance",
536
540
  "sch_agent_task",
@@ -609,6 +613,7 @@ var ENTITY_TYPE_TOKENS = [
609
613
  "seo_topic",
610
614
  "seo_web_analytics_daily",
611
615
  "shared_canvas_item",
616
+ "short_link",
612
617
  "skill",
613
618
  "skill_render_definition",
614
619
  "sms_consent",
@@ -763,6 +768,12 @@ var IDLE_CATEGORIES = Object.freeze({
763
768
  fetchedAt: null,
764
769
  error: null
765
770
  });
771
+ var IDLE_COMMENTS = Object.freeze({
772
+ status: "idle",
773
+ comments: Object.freeze([]),
774
+ fetchedAt: null,
775
+ error: null
776
+ });
766
777
 
767
778
  // src/react/hooks/useAssociations.ts
768
779
  var noopSubscribe = () => () => {
@@ -1247,6 +1258,72 @@ function currentUserIdOrNull(store) {
1247
1258
  }
1248
1259
  }
1249
1260
 
1261
+ // src/react/hooks/useComments.ts
1262
+ import { useEffect as useEffect7, useRef as useRef6, useSyncExternalStore as useSyncExternalStore3 } from "react";
1263
+ var noopSubscribe3 = () => () => {
1264
+ };
1265
+ function useComments(args) {
1266
+ const { token, id, autoLoad = true } = args;
1267
+ const store = useAssociationsStore();
1268
+ const key = token && id ? associationsKey(token, id) : null;
1269
+ const entry = useSyncExternalStore3(
1270
+ key ? (cb) => store.subscribeComments(key, cb) : noopSubscribe3,
1271
+ () => store.getComments(token, id ?? ""),
1272
+ () => store.getComments(token, id ?? "")
1273
+ );
1274
+ const loadedKey = useRef6(null);
1275
+ useEffect7(() => {
1276
+ if (!autoLoad || !key || !id) return;
1277
+ if (loadedKey.current === key) return;
1278
+ loadedKey.current = key;
1279
+ void store.loadComments(token, id);
1280
+ }, [autoLoad, store, key, token, id]);
1281
+ let currentUserId = null;
1282
+ try {
1283
+ currentUserId = store.identity.requireUserId();
1284
+ } catch {
1285
+ currentUserId = null;
1286
+ }
1287
+ return {
1288
+ comments: entry.comments,
1289
+ status: entry.status,
1290
+ error: entry.error,
1291
+ fetchedAt: entry.fetchedAt,
1292
+ add: async (body, opts) => {
1293
+ if (!id) return { ok: false, error: "Missing entity id" };
1294
+ return store.addComment({
1295
+ entityType: token,
1296
+ entityId: id,
1297
+ body,
1298
+ ...opts?.parentId !== void 0 ? { parentId: opts.parentId } : {},
1299
+ ...opts?.orgId !== void 0 ? { orgId: opts.orgId } : {}
1300
+ });
1301
+ },
1302
+ edit: async (commentId, body) => {
1303
+ if (!id) return { ok: false, error: "Missing entity id" };
1304
+ return store.editComment({
1305
+ entityType: token,
1306
+ entityId: id,
1307
+ id: commentId,
1308
+ body
1309
+ });
1310
+ },
1311
+ remove: async (commentId) => {
1312
+ if (!id) return { ok: false, error: "Missing entity id" };
1313
+ return store.deleteComment({
1314
+ entityType: token,
1315
+ entityId: id,
1316
+ id: commentId
1317
+ });
1318
+ },
1319
+ reload: async () => {
1320
+ if (!id) return;
1321
+ await store.loadComments(token, id, { force: true });
1322
+ },
1323
+ currentUserId
1324
+ };
1325
+ }
1326
+
1250
1327
  // src/react/hooks/useAssociationEntitySelect.ts
1251
1328
  import { useState as useState5 } from "react";
1252
1329
  function useAssociationEntitySelectAdapter(args) {
@@ -1418,6 +1495,27 @@ function ExternalLinkIcon(props) {
1418
1495
  /* @__PURE__ */ jsx3("path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" })
1419
1496
  ] });
1420
1497
  }
1498
+ function ChevronDownIcon(props) {
1499
+ return /* @__PURE__ */ jsx3("svg", { ...base(props), children: /* @__PURE__ */ jsx3("path", { d: "m6 9 6 6 6-6" }) });
1500
+ }
1501
+ function ChevronsUpDownIcon(props) {
1502
+ return /* @__PURE__ */ jsxs("svg", { ...base(props), children: [
1503
+ /* @__PURE__ */ jsx3("path", { d: "m7 15 5 5 5-5" }),
1504
+ /* @__PURE__ */ jsx3("path", { d: "m7 9 5-5 5 5" })
1505
+ ] });
1506
+ }
1507
+ function CornerDownRightIcon(props) {
1508
+ return /* @__PURE__ */ jsxs("svg", { ...base(props), children: [
1509
+ /* @__PURE__ */ jsx3("polyline", { points: "15 10 20 15 15 20" }),
1510
+ /* @__PURE__ */ jsx3("path", { d: "M4 4v7a4 4 0 0 0 4 4h12" })
1511
+ ] });
1512
+ }
1513
+ function TagIcon(props) {
1514
+ return /* @__PURE__ */ jsxs("svg", { ...base(props), children: [
1515
+ /* @__PURE__ */ jsx3("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" }),
1516
+ /* @__PURE__ */ jsx3("circle", { cx: "7.5", cy: "7.5", r: ".5", fill: "currentColor" })
1517
+ ] });
1518
+ }
1421
1519
  function DefaultEntityIcon(props) {
1422
1520
  return /* @__PURE__ */ jsxs("svg", { ...base(props), children: [
1423
1521
  /* @__PURE__ */ jsx3("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" }),
@@ -1969,7 +2067,7 @@ function UniversalAssociationPicker(props) {
1969
2067
  }
1970
2068
 
1971
2069
  // src/react/components/AttachedItemsSheet.tsx
1972
- import { useEffect as useEffect7, useState as useState8 } from "react";
2070
+ import { useEffect as useEffect8, useState as useState8 } from "react";
1973
2071
  import { cn as cn5 } from "@ai-matrx/design-system";
1974
2072
 
1975
2073
  // src/react/components/entityDoors.tsx
@@ -2089,7 +2187,7 @@ function AttachedItemsBody({
2089
2187
  const [titleError, setTitleError] = useState8(null);
2090
2188
  const [busyId, setBusyId] = useState8(null);
2091
2189
  const idKey = links.map((l) => l.resourceId).sort().join(",");
2092
- useEffect7(() => {
2190
+ useEffect8(() => {
2093
2191
  if (!enabled) return;
2094
2192
  const ids = idKey ? idKey.split(",") : [];
2095
2193
  if (ids.length === 0) {
@@ -2774,7 +2872,7 @@ function titleize(token) {
2774
2872
  }
2775
2873
 
2776
2874
  // src/react/components/AssociationCaptureToolbar.tsx
2777
- import { useRef as useRef6, useState as useState11 } from "react";
2875
+ import { useRef as useRef7, useState as useState11 } from "react";
2778
2876
  import { Button, cn as cn9, Input as Input3 } from "@ai-matrx/design-system";
2779
2877
  import { Fragment as Fragment4, jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
2780
2878
  function AssociationCaptureToolbar({
@@ -2794,7 +2892,7 @@ function AssociationCaptureToolbar({
2794
2892
  const store = useAssociationsStore();
2795
2893
  const { capture } = useAssociationsUiPorts();
2796
2894
  const notifier = useNotifier();
2797
- const fileInputRef = useRef6(null);
2895
+ const fileInputRef = useRef7(null);
2798
2896
  const [isUploading, setIsUploading] = useState11(false);
2799
2897
  const [isDragOver, setIsDragOver] = useState11(false);
2800
2898
  const [isPicking, setIsPicking] = useState11(false);
@@ -3106,11 +3204,1174 @@ function CaptureToolbarAction({
3106
3204
  }
3107
3205
  );
3108
3206
  }
3207
+
3208
+ // src/react/components/AssociationEntitySelect.tsx
3209
+ import { useState as useState12 } from "react";
3210
+ import {
3211
+ cn as cn10,
3212
+ Command,
3213
+ CommandEmpty,
3214
+ CommandGroup,
3215
+ CommandInput,
3216
+ CommandItem,
3217
+ CommandList,
3218
+ EditableLabel,
3219
+ Popover,
3220
+ PopoverContent,
3221
+ PopoverTrigger
3222
+ } from "@ai-matrx/design-system";
3223
+ import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
3224
+ function AssociationEntitySelect({
3225
+ token,
3226
+ adapter,
3227
+ className,
3228
+ labelClassName,
3229
+ align = "end",
3230
+ showIcon = true,
3231
+ renameActivation = "click",
3232
+ emptyLabel,
3233
+ iconClassName,
3234
+ createSlot
3235
+ }) {
3236
+ const store = useAssociationsStore();
3237
+ const { entityDoors } = useAssociationsUiPorts();
3238
+ const notifier = useNotifier();
3239
+ const info = store.registry.getEntityInfo(token);
3240
+ const InfoIcon = info.Icon ?? DefaultEntityIcon;
3241
+ const DoorControls = entityDoors?.DoorControls;
3242
+ const { items, activeId } = adapter;
3243
+ const active = items.find((i) => i.id === activeId) ?? null;
3244
+ const activeIndex = active ? items.indexOf(active) : -1;
3245
+ const [open, setOpen] = useState12(false);
3246
+ const [query, setQuery] = useState12("");
3247
+ const [creating, setCreating] = useState12(false);
3248
+ const [draftName, setDraftName] = useState12("");
3249
+ const [busy, setBusy] = useState12(false);
3250
+ const entityLabel = info.label.toLowerCase();
3251
+ const close = () => {
3252
+ setOpen(false);
3253
+ setQuery("");
3254
+ setCreating(false);
3255
+ setDraftName("");
3256
+ };
3257
+ const create = async (title) => {
3258
+ const name = title.trim();
3259
+ if (!name || busy || !adapter.createAndAttach) return;
3260
+ setBusy(true);
3261
+ const id = await adapter.createAndAttach(name);
3262
+ setBusy(false);
3263
+ if (id) close();
3264
+ else notifier.error(`Couldn't create the ${entityLabel}`);
3265
+ };
3266
+ return /* @__PURE__ */ jsxs11(
3267
+ "div",
3268
+ {
3269
+ className: cn10(
3270
+ "group/entity-ref flex min-w-0 items-center gap-0.5",
3271
+ className
3272
+ ),
3273
+ children: [
3274
+ showIcon ? /* @__PURE__ */ jsx13(
3275
+ InfoIcon,
3276
+ {
3277
+ className: cn10(
3278
+ "size-3.5 shrink-0",
3279
+ iconClassName ?? "text-muted-foreground"
3280
+ ),
3281
+ "aria-hidden": true
3282
+ }
3283
+ ) : null,
3284
+ active ? /* @__PURE__ */ jsx13(
3285
+ EditableLabel,
3286
+ {
3287
+ value: active.title,
3288
+ onCommit: async (next) => {
3289
+ const ok = await adapter.rename(active.id, next);
3290
+ if (!ok) {
3291
+ notifier.error(`Couldn't rename the ${entityLabel}`);
3292
+ throw new Error("rename failed");
3293
+ }
3294
+ },
3295
+ commitMode: "await",
3296
+ activation: renameActivation,
3297
+ ariaLabel: info.label,
3298
+ className: cn10("max-w-[14rem]", labelClassName),
3299
+ displayClassName: "text-xs font-medium text-foreground",
3300
+ inputClassName: "text-xs font-medium"
3301
+ }
3302
+ ) : null,
3303
+ active && DoorControls ? /* @__PURE__ */ jsx13(DoorControls, { token, id: active.id, name: active.title }) : null,
3304
+ !active ? /* @__PURE__ */ jsx13(
3305
+ "span",
3306
+ {
3307
+ className: cn10(
3308
+ "truncate px-1 text-xs font-medium text-muted-foreground",
3309
+ labelClassName
3310
+ ),
3311
+ children: adapter.loading ? "\u2026" : emptyLabel ?? info.labelPlural
3312
+ }
3313
+ ) : null,
3314
+ /* @__PURE__ */ jsxs11(
3315
+ Popover,
3316
+ {
3317
+ open,
3318
+ onOpenChange: (next) => next ? setOpen(true) : close(),
3319
+ children: [
3320
+ /* @__PURE__ */ jsx13(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs11(
3321
+ "button",
3322
+ {
3323
+ type: "button",
3324
+ 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",
3325
+ title: `Switch or add ${info.labelPlural.toLowerCase()}`,
3326
+ "aria-label": `Switch or add ${info.labelPlural.toLowerCase()}`,
3327
+ children: [
3328
+ items.length > 1 ? /* @__PURE__ */ jsxs11("span", { className: "tabular-nums", children: [
3329
+ activeIndex >= 0 ? activeIndex + 1 : "\u2014",
3330
+ "/",
3331
+ items.length
3332
+ ] }) : null,
3333
+ /* @__PURE__ */ jsx13(ChevronDownIcon, { className: "size-3 opacity-60" })
3334
+ ]
3335
+ }
3336
+ ) }),
3337
+ /* @__PURE__ */ jsx13(PopoverContent, { className: "w-60 p-0", align, children: /* @__PURE__ */ jsxs11(Command, { children: [
3338
+ items.length > 5 ? /* @__PURE__ */ jsx13(
3339
+ CommandInput,
3340
+ {
3341
+ value: query,
3342
+ onValueChange: setQuery,
3343
+ placeholder: `Search ${info.labelPlural.toLowerCase()}\u2026`
3344
+ }
3345
+ ) : null,
3346
+ /* @__PURE__ */ jsxs11(CommandList, { children: [
3347
+ /* @__PURE__ */ jsx13(CommandEmpty, { children: "No match." }),
3348
+ items.length > 0 ? /* @__PURE__ */ jsx13(CommandGroup, { children: items.map((item) => /* @__PURE__ */ jsxs11(
3349
+ CommandItem,
3350
+ {
3351
+ value: `${item.title} ${item.id}`,
3352
+ onSelect: () => {
3353
+ if (item.id !== activeId)
3354
+ void adapter.setActive(item.id);
3355
+ close();
3356
+ },
3357
+ className: "group/entity-ref group gap-2",
3358
+ children: [
3359
+ /* @__PURE__ */ jsx13(
3360
+ CheckIcon,
3361
+ {
3362
+ className: cn10(
3363
+ "size-3.5 shrink-0",
3364
+ item.id === activeId ? "opacity-100" : "opacity-0"
3365
+ )
3366
+ }
3367
+ ),
3368
+ /* @__PURE__ */ jsx13("span", { className: "min-w-0 flex-1 truncate", children: item.title }),
3369
+ DoorControls ? /* @__PURE__ */ jsx13(
3370
+ "span",
3371
+ {
3372
+ className: "inline-flex shrink-0 items-center",
3373
+ onPointerDown: (e) => {
3374
+ e.preventDefault();
3375
+ e.stopPropagation();
3376
+ },
3377
+ children: /* @__PURE__ */ jsx13(
3378
+ DoorControls,
3379
+ {
3380
+ token,
3381
+ id: item.id,
3382
+ name: item.title
3383
+ }
3384
+ )
3385
+ }
3386
+ ) : null,
3387
+ adapter.detach && item.id !== activeId ? /* @__PURE__ */ jsx13(
3388
+ "button",
3389
+ {
3390
+ type: "button",
3391
+ onPointerDown: (e) => {
3392
+ e.preventDefault();
3393
+ e.stopPropagation();
3394
+ },
3395
+ onClick: (e) => {
3396
+ e.stopPropagation();
3397
+ void adapter.detach?.(item.id);
3398
+ },
3399
+ 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",
3400
+ title: `Remove this ${entityLabel} from here (does not delete it)`,
3401
+ "aria-label": `Remove ${item.title}`,
3402
+ children: /* @__PURE__ */ jsx13(XIcon, { className: "size-3" })
3403
+ }
3404
+ ) : null
3405
+ ]
3406
+ },
3407
+ item.id
3408
+ )) }) : null
3409
+ ] }),
3410
+ createSlot !== void 0 ? /* @__PURE__ */ jsx13("div", { className: "border-t border-border p-1", children: typeof createSlot === "function" ? createSlot(close) : createSlot }) : null,
3411
+ createSlot === void 0 && adapter.createAndAttach ? /* @__PURE__ */ jsx13("div", { className: "border-t border-border p-1", children: creating ? /* @__PURE__ */ jsxs11("div", { className: "flex items-center gap-1 px-1 py-0.5", children: [
3412
+ /* @__PURE__ */ jsx13(
3413
+ "input",
3414
+ {
3415
+ autoFocus: true,
3416
+ type: "text",
3417
+ value: draftName,
3418
+ disabled: busy,
3419
+ placeholder: `New ${entityLabel} name\u2026`,
3420
+ onChange: (e) => setDraftName(e.target.value),
3421
+ onKeyDown: (e) => {
3422
+ if (e.key === "Enter") {
3423
+ e.preventDefault();
3424
+ void create(draftName);
3425
+ } else if (e.key === "Escape") {
3426
+ e.preventDefault();
3427
+ e.stopPropagation();
3428
+ setCreating(false);
3429
+ setDraftName("");
3430
+ }
3431
+ },
3432
+ 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",
3433
+ "aria-label": `New ${entityLabel} name`
3434
+ }
3435
+ ),
3436
+ /* @__PURE__ */ jsx13(
3437
+ "button",
3438
+ {
3439
+ type: "button",
3440
+ disabled: busy || !draftName.trim(),
3441
+ onClick: () => void create(draftName),
3442
+ 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",
3443
+ children: busy ? /* @__PURE__ */ jsx13(SpinnerIcon, { className: "size-3 animate-spin" }) : "Create"
3444
+ }
3445
+ )
3446
+ ] }) : /* @__PURE__ */ jsxs11(
3447
+ "button",
3448
+ {
3449
+ type: "button",
3450
+ disabled: busy,
3451
+ onClick: () => {
3452
+ if (query.trim()) void create(query);
3453
+ else setCreating(true);
3454
+ },
3455
+ 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",
3456
+ children: [
3457
+ busy ? /* @__PURE__ */ jsx13(SpinnerIcon, { className: "size-3.5 animate-spin" }) : /* @__PURE__ */ jsx13(PlusIcon, { className: "size-3.5" }),
3458
+ query.trim() ? `Create "${query.trim()}"` : `New ${info.label}`
3459
+ ]
3460
+ }
3461
+ ) }) : null
3462
+ ] }) })
3463
+ ]
3464
+ }
3465
+ )
3466
+ ]
3467
+ }
3468
+ );
3469
+ }
3470
+
3471
+ // src/react/components/CategorySelect.tsx
3472
+ import { useState as useState13 } from "react";
3473
+ import {
3474
+ CreatablePicker,
3475
+ Select,
3476
+ SelectContent,
3477
+ SelectItem,
3478
+ SelectTrigger,
3479
+ SelectValue
3480
+ } from "@ai-matrx/design-system";
3481
+
3482
+ // src/core/categoryHierarchy.ts
3483
+ function buildCategoryHierarchy(categories) {
3484
+ const byId = new Map(categories.map((category) => [category.id, category]));
3485
+ const hasHierarchy = categories.some(
3486
+ (category) => category.parentId !== null && byId.has(category.parentId)
3487
+ );
3488
+ if (!hasHierarchy) {
3489
+ return {
3490
+ items: categories.map((category) => ({
3491
+ category,
3492
+ depth: 0,
3493
+ parent: null,
3494
+ displayName: category.name
3495
+ })),
3496
+ roots: categories.filter((category) => category.parentId === null),
3497
+ hasHierarchy: false
3498
+ };
3499
+ }
3500
+ const roots = categories.filter(
3501
+ (category) => category.parentId === null || !byId.has(category.parentId)
3502
+ );
3503
+ const childrenByParent = /* @__PURE__ */ new Map();
3504
+ for (const category of categories) {
3505
+ if (!category.parentId || !byId.has(category.parentId)) continue;
3506
+ const children = childrenByParent.get(category.parentId) ?? [];
3507
+ children.push(category);
3508
+ childrenByParent.set(category.parentId, children);
3509
+ }
3510
+ const items = [];
3511
+ const seen = /* @__PURE__ */ new Set();
3512
+ for (const root of roots) {
3513
+ items.push({
3514
+ category: root,
3515
+ depth: 0,
3516
+ parent: null,
3517
+ displayName: root.name
3518
+ });
3519
+ seen.add(root.id);
3520
+ for (const child of childrenByParent.get(root.id) ?? []) {
3521
+ items.push({
3522
+ category: child,
3523
+ depth: 1,
3524
+ parent: root,
3525
+ displayName: `${root.name} / ${child.name}`
3526
+ });
3527
+ seen.add(child.id);
3528
+ }
3529
+ }
3530
+ for (const category of categories) {
3531
+ if (seen.has(category.id)) continue;
3532
+ items.push({
3533
+ category,
3534
+ depth: 0,
3535
+ parent: null,
3536
+ displayName: category.name
3537
+ });
3538
+ }
3539
+ return {
3540
+ items,
3541
+ roots: categories.filter((category) => category.parentId === null),
3542
+ hasHierarchy: true
3543
+ };
3544
+ }
3545
+
3546
+ // src/react/components/CategorySelect.tsx
3547
+ import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
3548
+ var NONE = "__none__";
3549
+ var ROOT_PARENT = "__root__";
3550
+ function plural(noun) {
3551
+ return /[^aeiou]y$/i.test(noun) ? `${noun.slice(0, -1)}ies` : `${noun}s`;
3552
+ }
3553
+ function CategorySelect({
3554
+ dimension,
3555
+ value,
3556
+ onChange,
3557
+ placeholder,
3558
+ allowNone = true,
3559
+ disabled,
3560
+ allowCreate = true,
3561
+ orgId,
3562
+ noun = "category"
3563
+ }) {
3564
+ const store = useAssociationsStore();
3565
+ const notifier = useNotifier();
3566
+ const {
3567
+ categories,
3568
+ status,
3569
+ error,
3570
+ create: createCategory,
3571
+ reload
3572
+ } = useCategories({ dimension });
3573
+ const hierarchy = buildCategoryHierarchy(categories);
3574
+ const [createParentId, setCreateParentId] = useState13(ROOT_PARENT);
3575
+ const options = [
3576
+ ...allowNone ? [{ value: NONE, label: "None", keywords: "none clear empty" }] : [],
3577
+ ...hierarchy.hasHierarchy ? hierarchy.items.map(({ category, depth, parent }) => ({
3578
+ value: category.id,
3579
+ label: category.name,
3580
+ // The type-ahead matches the parent's name too, so someone who knows
3581
+ // the branch but not the leaf still finds it.
3582
+ keywords: parent ? parent.name : "",
3583
+ render: depth === 0 ? /* @__PURE__ */ jsx14("span", { className: "font-medium", children: category.name }) : /* @__PURE__ */ jsxs12("span", { className: "flex items-center gap-1.5 pl-3 text-muted-foreground", children: [
3584
+ /* @__PURE__ */ jsx14(CornerDownRightIcon, { className: "h-3.5 w-3.5 shrink-0" }),
3585
+ /* @__PURE__ */ jsx14("span", { className: "text-foreground", children: category.name })
3586
+ ] })
3587
+ })) : categories.map((category) => ({
3588
+ value: category.id,
3589
+ label: category.name
3590
+ }))
3591
+ ];
3592
+ const selected = value ? hierarchy.items.find((item) => item.category.id === value)?.displayName ?? categories.find((category) => category.id === value)?.name ?? null : null;
3593
+ const create = async (typed) => {
3594
+ const name = typed.trim();
3595
+ if (!name) return null;
3596
+ const existing = categories.find(
3597
+ (category) => category.name.toLowerCase() === name.toLowerCase()
3598
+ );
3599
+ if (existing) return existing.id;
3600
+ let resolvedOrgId = orgId ?? null;
3601
+ if (!resolvedOrgId && store.identity.ensureOrgId) {
3602
+ try {
3603
+ resolvedOrgId = await store.identity.ensureOrgId();
3604
+ } catch (e) {
3605
+ store.errorSink({
3606
+ code: "ensure_org_failed",
3607
+ message: "[CategorySelect] identity.ensureOrgId threw",
3608
+ context: { error: e instanceof Error ? e.message : String(e) }
3609
+ });
3610
+ }
3611
+ }
3612
+ if (!resolvedOrgId) {
3613
+ notifier.error(`Select an organization before creating a ${noun}.`);
3614
+ return null;
3615
+ }
3616
+ const parentId = hierarchy.hasHierarchy && createParentId !== ROOT_PARENT ? createParentId : null;
3617
+ const result = await createCategory({ name, orgId: resolvedOrgId, parentId });
3618
+ if (!result.ok || !result.id) {
3619
+ notifier.error(`Couldn't create that ${noun}`, {
3620
+ ...result.error != null ? { description: result.error } : {}
3621
+ });
3622
+ return null;
3623
+ }
3624
+ await reload();
3625
+ setCreateParentId(ROOT_PARENT);
3626
+ const under = parentId ? categories.find((category) => category.id === parentId)?.name : null;
3627
+ notifier.success(
3628
+ `\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.`}`
3629
+ );
3630
+ return result.id;
3631
+ };
3632
+ return /* @__PURE__ */ jsxs12("div", { children: [
3633
+ /* @__PURE__ */ jsx14(
3634
+ CreatablePicker,
3635
+ {
3636
+ value: value ?? (allowNone ? NONE : null),
3637
+ onSelect: (next) => onChange(next === NONE ? null : next),
3638
+ options,
3639
+ placeholder,
3640
+ searchPlaceholder: allowCreate ? `Search or add a ${noun}\u2026` : `Search ${plural(noun)}\u2026`,
3641
+ noun,
3642
+ ariaLabel: placeholder,
3643
+ className: "text-sm",
3644
+ ...disabled !== void 0 ? { disabled } : {},
3645
+ loading: status === "loading",
3646
+ emptyLabel: `No ${noun} matches that.`,
3647
+ ...allowCreate ? { onCreate: create } : {},
3648
+ ...allowCreate && hierarchy.hasHierarchy ? {
3649
+ createExtra: /* @__PURE__ */ jsxs12(Select, { value: createParentId, onValueChange: setCreateParentId, children: [
3650
+ /* @__PURE__ */ jsx14(
3651
+ SelectTrigger,
3652
+ {
3653
+ className: "h-7 text-xs",
3654
+ "aria-label": `Put the new ${noun} under`,
3655
+ children: /* @__PURE__ */ jsx14(SelectValue, {})
3656
+ }
3657
+ ),
3658
+ /* @__PURE__ */ jsxs12(SelectContent, { children: [
3659
+ /* @__PURE__ */ jsx14(SelectItem, { value: ROOT_PARENT, className: "text-xs", children: "Top level" }),
3660
+ hierarchy.items.filter((item) => item.depth === 0).map((item) => /* @__PURE__ */ jsxs12(
3661
+ SelectItem,
3662
+ {
3663
+ value: item.category.id,
3664
+ className: "text-xs",
3665
+ children: [
3666
+ "Under ",
3667
+ item.category.name
3668
+ ]
3669
+ },
3670
+ item.category.id
3671
+ ))
3672
+ ] })
3673
+ ] })
3674
+ } : {}
3675
+ }
3676
+ ),
3677
+ selected === null && value !== null ? (
3678
+ // A stored id whose row this reader cannot see is not "nothing chosen":
3679
+ // saying so beats a blank box that looks like an unsaved field.
3680
+ /* @__PURE__ */ jsxs12("p", { className: "mt-1 text-xs text-muted-foreground", children: [
3681
+ "This ",
3682
+ noun,
3683
+ " is no longer available to you."
3684
+ ] })
3685
+ ) : null,
3686
+ error ? /* @__PURE__ */ jsx14("p", { className: "mt-1 text-xs text-destructive", children: error }) : null
3687
+ ] });
3688
+ }
3689
+
3690
+ // src/react/components/CategoryTagPicker.tsx
3691
+ import { useState as useState14 } from "react";
3692
+ import {
3693
+ Badge,
3694
+ Button as Button2,
3695
+ cn as cn11,
3696
+ Command as Command2,
3697
+ CommandEmpty as CommandEmpty2,
3698
+ CommandGroup as CommandGroup2,
3699
+ CommandInput as CommandInput2,
3700
+ CommandItem as CommandItem2,
3701
+ CommandList as CommandList2,
3702
+ Popover as Popover2,
3703
+ PopoverContent as PopoverContent2,
3704
+ PopoverTrigger as PopoverTrigger2,
3705
+ Select as Select2,
3706
+ SelectContent as SelectContent2,
3707
+ SelectItem as SelectItem2,
3708
+ SelectTrigger as SelectTrigger2,
3709
+ SelectValue as SelectValue2
3710
+ } from "@ai-matrx/design-system";
3711
+ import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
3712
+ var ROOT_PARENT2 = "__root__";
3713
+ function CategoryTagPicker({
3714
+ entityType,
3715
+ entityId,
3716
+ dimension,
3717
+ edgeRole,
3718
+ orgId,
3719
+ addLabel = "Add tag",
3720
+ icon: Icon = TagIcon,
3721
+ allowCreate = true,
3722
+ hierarchical,
3723
+ emptyText = "No categories yet."
3724
+ }) {
3725
+ const store = useAssociationsStore();
3726
+ const notifier = useNotifier();
3727
+ const [open, setOpen] = useState14(false);
3728
+ const [creating, setCreating] = useState14(false);
3729
+ const [writing, setWriting] = useState14(false);
3730
+ const [search, setSearch] = useState14("");
3731
+ const [createParentId, setCreateParentId] = useState14(ROOT_PARENT2);
3732
+ const {
3733
+ categories,
3734
+ create: createCategory,
3735
+ reload: reloadCategories
3736
+ } = useCategories({ dimension });
3737
+ const { edges, setTargets } = useAssociations({
3738
+ type: entityType,
3739
+ id: entityId
3740
+ });
3741
+ const hierarchy = buildCategoryHierarchy(categories);
3742
+ const showHierarchy = hierarchical ?? hierarchy.hasHierarchy;
3743
+ const hierarchyById = new Map(
3744
+ hierarchy.items.map((item) => [item.category.id, item])
3745
+ );
3746
+ const selectedIds = new Set(
3747
+ edges.filter(
3748
+ (e) => e.direction === "outgoing" && e.otherType === "category" && e.role === edgeRole
3749
+ ).map((e) => e.otherId)
3750
+ );
3751
+ const selected = categories.filter((c) => selectedIds.has(c.id));
3752
+ const toggle = async (categoryId) => {
3753
+ if (writing) return;
3754
+ setWriting(true);
3755
+ try {
3756
+ const next = selectedIds.has(categoryId) ? [...selectedIds].filter((id) => id !== categoryId) : [...selectedIds, categoryId];
3757
+ const res = await setTargets({
3758
+ targetType: "category",
3759
+ targetIds: next,
3760
+ ...orgId != null ? { orgId } : {},
3761
+ role: edgeRole
3762
+ });
3763
+ if (!res.ok) {
3764
+ store.errorSink({
3765
+ code: "category_tag_set_targets_failed",
3766
+ message: "[CategoryTagPicker] setTargets failed",
3767
+ context: { entityType, entityId, edgeRole, error: res.error }
3768
+ });
3769
+ notifier.error(res.error ?? "Could not update categories");
3770
+ }
3771
+ } finally {
3772
+ setWriting(false);
3773
+ }
3774
+ };
3775
+ const createAndAttach = async () => {
3776
+ const name = search.trim();
3777
+ if (!name) return;
3778
+ setCreating(true);
3779
+ try {
3780
+ let resolvedOrgId = orgId ?? null;
3781
+ if (!resolvedOrgId && store.identity.ensureOrgId) {
3782
+ try {
3783
+ resolvedOrgId = await store.identity.ensureOrgId();
3784
+ } catch (e) {
3785
+ store.errorSink({
3786
+ code: "ensure_org_failed",
3787
+ message: "[CategoryTagPicker] identity.ensureOrgId threw",
3788
+ context: { error: e instanceof Error ? e.message : String(e) }
3789
+ });
3790
+ }
3791
+ }
3792
+ if (!resolvedOrgId) {
3793
+ store.errorSink({
3794
+ code: "ensure_org_missing",
3795
+ message: "[CategoryTagPicker] no org for the created category \u2014 pass `orgId` or bind `identity.ensureOrgId` on the store config",
3796
+ context: { entityType, entityId, dimension }
3797
+ });
3798
+ notifier.error(
3799
+ "Could not create the category \u2014 no organization is selected"
3800
+ );
3801
+ return;
3802
+ }
3803
+ const parentId = showHierarchy && createParentId !== ROOT_PARENT2 ? createParentId : null;
3804
+ const createRes = await createCategory({
3805
+ name,
3806
+ orgId: resolvedOrgId,
3807
+ parentId
3808
+ });
3809
+ if (!createRes.ok || !createRes.id) {
3810
+ store.errorSink({
3811
+ code: "category_tag_create_failed",
3812
+ message: "[CategoryTagPicker] create failed",
3813
+ context: { dimension, error: createRes.error }
3814
+ });
3815
+ notifier.error(createRes.error ?? "Could not create the category");
3816
+ return;
3817
+ }
3818
+ await reloadCategories();
3819
+ await toggle(createRes.id);
3820
+ setSearch("");
3821
+ setCreateParentId(ROOT_PARENT2);
3822
+ } finally {
3823
+ setCreating(false);
3824
+ }
3825
+ };
3826
+ const filtered = categories.filter(
3827
+ (c) => c.name.toLowerCase().includes(search.trim().toLowerCase())
3828
+ );
3829
+ const filteredHierarchy = hierarchy.items.filter(
3830
+ (item) => item.displayName.toLowerCase().includes(search.trim().toLowerCase())
3831
+ );
3832
+ const exactMatch = categories.some(
3833
+ (c) => c.name.toLowerCase() === search.trim().toLowerCase()
3834
+ );
3835
+ return /* @__PURE__ */ jsxs13("div", { className: "flex flex-wrap items-center gap-1.5", children: [
3836
+ selected.map((c) => /* @__PURE__ */ jsxs13(
3837
+ Badge,
3838
+ {
3839
+ variant: "secondary",
3840
+ className: "gap-1 pr-1 text-xs font-medium",
3841
+ children: [
3842
+ showHierarchy ? hierarchyById.get(c.id)?.displayName ?? c.name : c.name,
3843
+ /* @__PURE__ */ jsx15(
3844
+ "button",
3845
+ {
3846
+ type: "button",
3847
+ onClick: () => void toggle(c.id),
3848
+ className: "ml-0.5 rounded-full p-0.5 hover:bg-muted-foreground/20",
3849
+ "aria-label": `Remove ${c.name}`,
3850
+ children: /* @__PURE__ */ jsx15(XIcon, { className: "h-2.5 w-2.5" })
3851
+ }
3852
+ )
3853
+ ]
3854
+ },
3855
+ c.id
3856
+ )),
3857
+ /* @__PURE__ */ jsxs13(Popover2, { open, onOpenChange: setOpen, children: [
3858
+ /* @__PURE__ */ jsx15(PopoverTrigger2, { asChild: true, children: /* @__PURE__ */ jsxs13(
3859
+ Button2,
3860
+ {
3861
+ variant: "outline",
3862
+ size: "sm",
3863
+ className: "h-7 gap-1 px-2 text-xs",
3864
+ children: [
3865
+ /* @__PURE__ */ jsx15(Icon, { className: "h-3.5 w-3.5" }),
3866
+ selected.length === 0 ? addLabel : "Edit",
3867
+ /* @__PURE__ */ jsx15(ChevronsUpDownIcon, { className: "h-3 w-3 opacity-50" })
3868
+ ]
3869
+ }
3870
+ ) }),
3871
+ /* @__PURE__ */ jsx15(PopoverContent2, { className: "w-56 p-0", align: "start", children: /* @__PURE__ */ jsxs13(Command2, { shouldFilter: false, children: [
3872
+ /* @__PURE__ */ jsx15(
3873
+ CommandInput2,
3874
+ {
3875
+ placeholder: allowCreate ? "Search or create..." : "Search...",
3876
+ value: search,
3877
+ onValueChange: setSearch
3878
+ }
3879
+ ),
3880
+ /* @__PURE__ */ jsxs13(CommandList2, { children: [
3881
+ /* @__PURE__ */ jsx15(CommandEmpty2, { className: "px-2 py-3 text-xs text-muted-foreground", children: search.trim() ? "No match." : emptyText }),
3882
+ /* @__PURE__ */ jsx15(CommandGroup2, { children: showHierarchy ? filteredHierarchy.map(({ category, depth, parent }) => /* @__PURE__ */ jsxs13(
3883
+ CommandItem2,
3884
+ {
3885
+ value: category.id,
3886
+ keywords: parent ? [parent.name, category.name] : [],
3887
+ onSelect: () => void toggle(category.id),
3888
+ className: cn11("text-xs", depth === 1 && "pl-6"),
3889
+ children: [
3890
+ /* @__PURE__ */ jsx15(
3891
+ CheckIcon,
3892
+ {
3893
+ className: cn11(
3894
+ "mr-2 h-3.5 w-3.5",
3895
+ selectedIds.has(category.id) ? "opacity-100" : "opacity-0"
3896
+ )
3897
+ }
3898
+ ),
3899
+ depth === 1 ? /* @__PURE__ */ jsx15(CornerDownRightIcon, { className: "mr-1.5 h-3.5 w-3.5 shrink-0 text-muted-foreground" }) : null,
3900
+ /* @__PURE__ */ jsx15("span", { className: cn11(depth === 0 && "font-medium"), children: category.name })
3901
+ ]
3902
+ },
3903
+ category.id
3904
+ )) : filtered.map((c) => /* @__PURE__ */ jsxs13(
3905
+ CommandItem2,
3906
+ {
3907
+ value: c.id,
3908
+ onSelect: () => void toggle(c.id),
3909
+ className: "text-xs",
3910
+ children: [
3911
+ /* @__PURE__ */ jsx15(
3912
+ CheckIcon,
3913
+ {
3914
+ className: cn11(
3915
+ "mr-2 h-3.5 w-3.5",
3916
+ selectedIds.has(c.id) ? "opacity-100" : "opacity-0"
3917
+ )
3918
+ }
3919
+ ),
3920
+ c.name
3921
+ ]
3922
+ },
3923
+ c.id
3924
+ )) }),
3925
+ allowCreate && search.trim() && !exactMatch ? /* @__PURE__ */ jsxs13(CommandGroup2, { children: [
3926
+ showHierarchy ? /* @__PURE__ */ jsxs13("div", { className: "space-y-1.5 px-2 pb-2", children: [
3927
+ /* @__PURE__ */ jsx15("p", { className: "text-[11px] font-medium text-muted-foreground", children: "Place in" }),
3928
+ /* @__PURE__ */ jsxs13(
3929
+ Select2,
3930
+ {
3931
+ value: createParentId,
3932
+ onValueChange: setCreateParentId,
3933
+ children: [
3934
+ /* @__PURE__ */ jsx15(SelectTrigger2, { size: "sm", className: "bg-background", children: /* @__PURE__ */ jsx15(SelectValue2, {}) }),
3935
+ /* @__PURE__ */ jsxs13(SelectContent2, { children: [
3936
+ /* @__PURE__ */ jsx15(SelectItem2, { value: ROOT_PARENT2, children: "Top level" }),
3937
+ hierarchy.roots.map((root) => /* @__PURE__ */ jsx15(SelectItem2, { value: root.id, children: root.name }, root.id))
3938
+ ] })
3939
+ ]
3940
+ }
3941
+ )
3942
+ ] }) : null,
3943
+ /* @__PURE__ */ jsxs13(
3944
+ CommandItem2,
3945
+ {
3946
+ value: `__create__${search}`,
3947
+ onSelect: () => void createAndAttach(),
3948
+ disabled: creating,
3949
+ className: "text-xs text-primary",
3950
+ children: [
3951
+ creating ? /* @__PURE__ */ jsx15(SpinnerIcon, { className: "mr-2 h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx15(Icon, { className: "mr-2 h-3.5 w-3.5" }),
3952
+ "Create \u201C",
3953
+ search.trim(),
3954
+ "\u201D"
3955
+ ]
3956
+ }
3957
+ )
3958
+ ] }) : null
3959
+ ] })
3960
+ ] }) })
3961
+ ] })
3962
+ ] });
3963
+ }
3964
+
3965
+ // src/react/components/CommentThread.tsx
3966
+ import { useState as useState15 } from "react";
3967
+ import { Button as Button3, Skeleton as Skeleton2, cn as cn12 } from "@ai-matrx/design-system";
3968
+
3969
+ // src/react/relativeTime.ts
3970
+ var UNITS = [
3971
+ { unit: "year", ms: 365 * 24 * 60 * 60 * 1e3 },
3972
+ { unit: "month", ms: 30 * 24 * 60 * 60 * 1e3 },
3973
+ { unit: "week", ms: 7 * 24 * 60 * 60 * 1e3 },
3974
+ { unit: "day", ms: 24 * 60 * 60 * 1e3 },
3975
+ { unit: "hour", ms: 60 * 60 * 1e3 },
3976
+ { unit: "minute", ms: 60 * 1e3 }
3977
+ ];
3978
+ function formatRelativeTime(iso, now = Date.now()) {
3979
+ const t = Date.parse(iso);
3980
+ if (Number.isNaN(t)) return iso;
3981
+ const delta = t - now;
3982
+ const magnitude = Math.abs(delta);
3983
+ if (magnitude < 60 * 1e3) return "just now";
3984
+ const rtf = new Intl.RelativeTimeFormat(void 0, { numeric: "auto" });
3985
+ for (const { unit, ms } of UNITS) {
3986
+ if (magnitude >= ms) {
3987
+ return rtf.format(Math.trunc(delta / ms), unit);
3988
+ }
3989
+ }
3990
+ return "just now";
3991
+ }
3992
+
3993
+ // src/react/components/CommentThread.tsx
3994
+ import { Fragment as Fragment5, jsx as jsx16, jsxs as jsxs14 } from "react/jsx-runtime";
3995
+ var AUTHOR_DOOR_TOKEN = "user";
3996
+ function resolveAuthor(comment, authorDisplay) {
3997
+ const base2 = {
3998
+ userId: comment.createdBy,
3999
+ email: comment.author.email,
4000
+ displayName: comment.author.displayName,
4001
+ avatarUrl: comment.author.avatarUrl
4002
+ };
4003
+ const enriched = authorDisplay ? authorDisplay(base2) : null;
4004
+ const name = enriched?.displayName ?? base2.displayName ?? base2.email ?? "Unknown user";
4005
+ return {
4006
+ name,
4007
+ avatarUrl: enriched?.avatarUrl !== void 0 && enriched?.avatarUrl !== null ? enriched.avatarUrl : base2.avatarUrl,
4008
+ userId: comment.createdBy
4009
+ };
4010
+ }
4011
+ function AuthorAvatar({ author }) {
4012
+ if (author.avatarUrl) {
4013
+ return /* @__PURE__ */ jsx16(
4014
+ "img",
4015
+ {
4016
+ src: author.avatarUrl,
4017
+ alt: "",
4018
+ className: "h-6 w-6 shrink-0 rounded-full object-cover"
4019
+ }
4020
+ );
4021
+ }
4022
+ return /* @__PURE__ */ jsx16(
4023
+ "span",
4024
+ {
4025
+ "aria-hidden": true,
4026
+ className: "flex h-6 w-6 shrink-0 select-none items-center justify-center rounded-full bg-muted text-[11px] font-medium uppercase text-muted-foreground",
4027
+ children: author.name.trim().charAt(0) || "?"
4028
+ }
4029
+ );
4030
+ }
4031
+ function AuthorName({
4032
+ author,
4033
+ entityDoors
4034
+ }) {
4035
+ const DoorRef2 = entityDoors?.EntityRef;
4036
+ if (DoorRef2 && author.userId) {
4037
+ return /* @__PURE__ */ jsx16(
4038
+ DoorRef2,
4039
+ {
4040
+ token: AUTHOR_DOOR_TOKEN,
4041
+ id: author.userId,
4042
+ name: author.name,
4043
+ showIcon: false,
4044
+ className: "text-sm font-medium text-foreground"
4045
+ }
4046
+ );
4047
+ }
4048
+ return /* @__PURE__ */ jsx16("span", { className: "text-sm font-medium text-foreground", children: author.name });
4049
+ }
4050
+ function CommentComposer({
4051
+ onSubmit,
4052
+ placeholder = "Write a comment\u2026",
4053
+ submitLabel = "Comment",
4054
+ initialValue = "",
4055
+ trailing,
4056
+ autoFocus = false
4057
+ }) {
4058
+ const notifier = useNotifier();
4059
+ const [body, setBody] = useState15(initialValue);
4060
+ const [submitting, setSubmitting] = useState15(false);
4061
+ const [error, setError] = useState15(null);
4062
+ async function submit(e) {
4063
+ e?.preventDefault();
4064
+ const trimmed = body.trim();
4065
+ if (!trimmed || submitting) return;
4066
+ setSubmitting(true);
4067
+ setError(null);
4068
+ const res = await onSubmit(trimmed);
4069
+ setSubmitting(false);
4070
+ if (!res.ok) {
4071
+ const msg = res.error ?? "Could not save the comment";
4072
+ setError(msg);
4073
+ notifier.error(msg);
4074
+ return;
4075
+ }
4076
+ setBody("");
4077
+ }
4078
+ return /* @__PURE__ */ jsxs14("form", { onSubmit: submit, className: "flex flex-col gap-1.5", children: [
4079
+ /* @__PURE__ */ jsx16(
4080
+ "textarea",
4081
+ {
4082
+ value: body,
4083
+ onChange: (e) => setBody(e.target.value),
4084
+ onKeyDown: (e) => {
4085
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void submit();
4086
+ },
4087
+ placeholder,
4088
+ rows: 2,
4089
+ autoFocus,
4090
+ "aria-label": placeholder,
4091
+ className: "w-full resize-y rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
4092
+ }
4093
+ ),
4094
+ error && /* @__PURE__ */ jsx16("p", { role: "alert", className: "text-xs text-destructive", children: error }),
4095
+ /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-2", children: [
4096
+ /* @__PURE__ */ jsxs14(Button3, { type: "submit", size: "sm", disabled: !body.trim() || submitting, children: [
4097
+ submitting && /* @__PURE__ */ jsx16(SpinnerIcon, { className: "mr-1 h-3 w-3 animate-spin" }),
4098
+ submitLabel
4099
+ ] }),
4100
+ trailing
4101
+ ] })
4102
+ ] });
4103
+ }
4104
+ function CommentItem({
4105
+ comment,
4106
+ childrenOf,
4107
+ depth,
4108
+ thread,
4109
+ orgId
4110
+ }) {
4111
+ const { authorDisplay, entityDoors } = useAssociationsUiPorts();
4112
+ const notifier = useNotifier();
4113
+ const [replying, setReplying] = useState15(false);
4114
+ const [editing, setEditing] = useState15(false);
4115
+ const [confirmingDelete, setConfirmingDelete] = useState15(false);
4116
+ const [deleting, setDeleting] = useState15(false);
4117
+ const author = resolveAuthor(comment, authorDisplay);
4118
+ const isOwn = thread.currentUserId !== null && comment.createdBy === thread.currentUserId;
4119
+ const edited = comment.updatedAt !== comment.createdAt;
4120
+ const replies = childrenOf.get(comment.id) ?? [];
4121
+ async function confirmDelete() {
4122
+ setDeleting(true);
4123
+ const res = await thread.remove(comment.id);
4124
+ setDeleting(false);
4125
+ setConfirmingDelete(false);
4126
+ if (!res.ok) {
4127
+ notifier.error(res.error ?? "Could not delete the comment");
4128
+ }
4129
+ }
4130
+ return /* @__PURE__ */ jsxs14(
4131
+ "div",
4132
+ {
4133
+ "data-comment-id": comment.id,
4134
+ className: cn12(
4135
+ "flex flex-col gap-1",
4136
+ depth > 0 && "border-l border-border pl-3"
4137
+ ),
4138
+ children: [
4139
+ /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-2", children: [
4140
+ /* @__PURE__ */ jsx16(AuthorAvatar, { author }),
4141
+ /* @__PURE__ */ jsx16(AuthorName, { author, entityDoors }),
4142
+ /* @__PURE__ */ jsx16(
4143
+ "span",
4144
+ {
4145
+ className: "text-xs text-muted-foreground",
4146
+ title: comment.createdAt,
4147
+ children: formatRelativeTime(comment.createdAt)
4148
+ }
4149
+ ),
4150
+ edited && /* @__PURE__ */ jsx16(
4151
+ "span",
4152
+ {
4153
+ className: "text-xs text-muted-foreground",
4154
+ title: comment.updatedAt,
4155
+ children: "(edited)"
4156
+ }
4157
+ )
4158
+ ] }),
4159
+ editing ? /* @__PURE__ */ jsx16("div", { className: "pl-8", children: /* @__PURE__ */ jsx16(
4160
+ CommentComposer,
4161
+ {
4162
+ initialValue: comment.body,
4163
+ submitLabel: "Save",
4164
+ placeholder: "Edit your comment\u2026",
4165
+ autoFocus: true,
4166
+ onSubmit: async (body) => {
4167
+ const res = await thread.edit(comment.id, body);
4168
+ if (res.ok) setEditing(false);
4169
+ return res;
4170
+ },
4171
+ trailing: /* @__PURE__ */ jsx16(
4172
+ Button3,
4173
+ {
4174
+ type: "button",
4175
+ size: "sm",
4176
+ variant: "ghost",
4177
+ onClick: () => setEditing(false),
4178
+ children: "Cancel"
4179
+ }
4180
+ )
4181
+ }
4182
+ ) }) : /* @__PURE__ */ jsx16("p", { className: "whitespace-pre-wrap pl-8 text-sm text-foreground", children: comment.body }),
4183
+ !editing && /* @__PURE__ */ jsx16("div", { className: "flex items-center gap-1 pl-7", children: confirmingDelete ? /* @__PURE__ */ jsxs14(Fragment5, { children: [
4184
+ /* @__PURE__ */ jsx16("span", { className: "text-xs text-destructive", children: "Deletes this comment for everyone." }),
4185
+ /* @__PURE__ */ jsxs14(
4186
+ Button3,
4187
+ {
4188
+ type: "button",
4189
+ size: "sm",
4190
+ variant: "destructive",
4191
+ disabled: deleting,
4192
+ onClick: () => void confirmDelete(),
4193
+ children: [
4194
+ deleting && /* @__PURE__ */ jsx16(SpinnerIcon, { className: "mr-1 h-3 w-3 animate-spin" }),
4195
+ "Delete"
4196
+ ]
4197
+ }
4198
+ ),
4199
+ /* @__PURE__ */ jsx16(
4200
+ Button3,
4201
+ {
4202
+ type: "button",
4203
+ size: "sm",
4204
+ variant: "ghost",
4205
+ disabled: deleting,
4206
+ onClick: () => setConfirmingDelete(false),
4207
+ children: "Cancel"
4208
+ }
4209
+ )
4210
+ ] }) : /* @__PURE__ */ jsxs14(Fragment5, { children: [
4211
+ /* @__PURE__ */ jsx16(
4212
+ "button",
4213
+ {
4214
+ type: "button",
4215
+ onClick: () => setReplying((v) => !v),
4216
+ className: "rounded px-1.5 py-0.5 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
4217
+ children: "Reply"
4218
+ }
4219
+ ),
4220
+ isOwn && /* @__PURE__ */ jsxs14(Fragment5, { children: [
4221
+ /* @__PURE__ */ jsx16(
4222
+ "button",
4223
+ {
4224
+ type: "button",
4225
+ onClick: () => setEditing(true),
4226
+ className: "rounded px-1.5 py-0.5 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
4227
+ children: "Edit"
4228
+ }
4229
+ ),
4230
+ /* @__PURE__ */ jsx16(
4231
+ "button",
4232
+ {
4233
+ type: "button",
4234
+ onClick: () => setConfirmingDelete(true),
4235
+ className: "rounded px-1.5 py-0.5 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-destructive",
4236
+ children: "Delete"
4237
+ }
4238
+ )
4239
+ ] })
4240
+ ] }) }),
4241
+ replying && /* @__PURE__ */ jsxs14("div", { className: "flex items-start gap-2 pl-8 pt-1", children: [
4242
+ /* @__PURE__ */ jsx16(CornerDownRightIcon, { className: "mt-2 h-3.5 w-3.5 shrink-0 text-muted-foreground" }),
4243
+ /* @__PURE__ */ jsx16("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx16(
4244
+ CommentComposer,
4245
+ {
4246
+ submitLabel: "Reply",
4247
+ placeholder: `Reply to ${author.name}\u2026`,
4248
+ autoFocus: true,
4249
+ onSubmit: async (body) => {
4250
+ const res = await thread.add(body, {
4251
+ parentId: comment.id,
4252
+ orgId: orgId ?? null
4253
+ });
4254
+ if (res.ok) setReplying(false);
4255
+ return res;
4256
+ },
4257
+ trailing: /* @__PURE__ */ jsx16(
4258
+ Button3,
4259
+ {
4260
+ type: "button",
4261
+ size: "sm",
4262
+ variant: "ghost",
4263
+ onClick: () => setReplying(false),
4264
+ children: "Cancel"
4265
+ }
4266
+ )
4267
+ }
4268
+ ) })
4269
+ ] }),
4270
+ replies.length > 0 && /* @__PURE__ */ jsx16("div", { className: "flex flex-col gap-3 pl-8 pt-1", children: replies.map((reply) => /* @__PURE__ */ jsx16(
4271
+ CommentItem,
4272
+ {
4273
+ comment: reply,
4274
+ childrenOf,
4275
+ depth: depth + 1,
4276
+ thread,
4277
+ orgId
4278
+ },
4279
+ reply.id
4280
+ )) })
4281
+ ]
4282
+ }
4283
+ );
4284
+ }
4285
+ function buildCommentTree(comments) {
4286
+ const byId = new Set(comments.map((c) => c.id));
4287
+ const roots = [];
4288
+ const childrenOf = /* @__PURE__ */ new Map();
4289
+ for (const c of comments) {
4290
+ if (c.parentId && byId.has(c.parentId)) {
4291
+ const bucket = childrenOf.get(c.parentId);
4292
+ if (bucket) bucket.push(c);
4293
+ else childrenOf.set(c.parentId, [c]);
4294
+ } else {
4295
+ roots.push(c);
4296
+ }
4297
+ }
4298
+ return { roots, childrenOf };
4299
+ }
4300
+ function CommentThread({
4301
+ token,
4302
+ id,
4303
+ orgId,
4304
+ showHeader = true,
4305
+ className
4306
+ }) {
4307
+ const thread = useComments({ token, id });
4308
+ const { roots, childrenOf } = buildCommentTree(thread.comments);
4309
+ const loading = (thread.status === "loading" || thread.status === "idle") && thread.comments.length === 0;
4310
+ return /* @__PURE__ */ jsxs14("section", { className: cn12("flex flex-col gap-3", className), children: [
4311
+ showHeader && /* @__PURE__ */ jsxs14("header", { className: "flex items-center gap-2", children: [
4312
+ /* @__PURE__ */ jsx16("h3", { className: "text-sm font-semibold text-foreground", children: "Comments" }),
4313
+ /* @__PURE__ */ jsx16("span", { className: "text-xs text-muted-foreground tabular-nums", children: thread.comments.length }),
4314
+ /* @__PURE__ */ jsx16(
4315
+ "button",
4316
+ {
4317
+ type: "button",
4318
+ onClick: () => void thread.reload(),
4319
+ "aria-label": "Refresh comments",
4320
+ title: "Refresh comments",
4321
+ className: "ml-auto flex h-6 w-6 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
4322
+ children: thread.status === "loading" ? /* @__PURE__ */ jsx16(SpinnerIcon, { className: "h-3.5 w-3.5 animate-spin" }) : /* @__PURE__ */ jsx16(RefreshIcon, { className: "h-3.5 w-3.5" })
4323
+ }
4324
+ )
4325
+ ] }),
4326
+ thread.error && /* @__PURE__ */ jsxs14(
4327
+ "div",
4328
+ {
4329
+ role: "alert",
4330
+ className: "flex items-center gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive",
4331
+ children: [
4332
+ /* @__PURE__ */ jsx16("span", { className: "min-w-0 flex-1", children: thread.error }),
4333
+ /* @__PURE__ */ jsx16(
4334
+ Button3,
4335
+ {
4336
+ type: "button",
4337
+ size: "sm",
4338
+ variant: "outline",
4339
+ disabled: thread.status === "loading",
4340
+ onClick: () => void thread.reload(),
4341
+ children: "Retry"
4342
+ }
4343
+ )
4344
+ ]
4345
+ }
4346
+ ),
4347
+ loading ? /* @__PURE__ */ jsxs14("div", { className: "flex flex-col gap-3", "aria-hidden": true, "data-testid": "comments-loading", children: [
4348
+ /* @__PURE__ */ jsx16(Skeleton2, { className: "h-12 w-full" }),
4349
+ /* @__PURE__ */ jsx16(Skeleton2, { className: "h-12 w-4/5" })
4350
+ ] }) : roots.length === 0 && !thread.error ? /* @__PURE__ */ jsx16("p", { className: "text-sm text-muted-foreground", children: "No comments yet." }) : /* @__PURE__ */ jsx16("div", { className: "flex flex-col gap-4", children: roots.map((comment) => /* @__PURE__ */ jsx16(
4351
+ CommentItem,
4352
+ {
4353
+ comment,
4354
+ childrenOf,
4355
+ depth: 0,
4356
+ thread,
4357
+ orgId
4358
+ },
4359
+ comment.id
4360
+ )) }),
4361
+ /* @__PURE__ */ jsx16(
4362
+ CommentComposer,
4363
+ {
4364
+ onSubmit: (body) => thread.add(body, { orgId: orgId ?? null })
4365
+ }
4366
+ )
4367
+ ] });
4368
+ }
3109
4369
  export {
3110
4370
  AssociationCandidateBody,
3111
4371
  AssociationCaptureToolbar,
3112
4372
  AssociationCard,
3113
4373
  AssociationCardGrid,
4374
+ AssociationEntitySelect,
3114
4375
  AssociationList,
3115
4376
  AssociationPicker,
3116
4377
  AssociationWindow,
@@ -3118,12 +4379,18 @@ export {
3118
4379
  AttachedItemsSheet,
3119
4380
  CONTENT_ROLES,
3120
4381
  CaptureToolbarAction,
4382
+ CategorySelect,
4383
+ CategoryTagPicker,
4384
+ CommentComposer,
4385
+ CommentThread,
3121
4386
  DefaultEntityIcon,
3122
4387
  DoorRef,
3123
4388
  PrimaryEntityProvider,
3124
4389
  UniversalAssociationPicker,
3125
4390
  UnresolvedRef,
3126
4391
  attachedKey,
4392
+ buildCommentTree,
4393
+ formatRelativeTime,
3127
4394
  getContentRoleMeta,
3128
4395
  useAssociationCandidates,
3129
4396
  useAssociationEntitySelectAdapter,
@@ -3131,6 +4398,7 @@ export {
3131
4398
  useAssociationsStore,
3132
4399
  useAssociationsUiPorts,
3133
4400
  useCategories,
4401
+ useComments,
3134
4402
  useContainerLinks,
3135
4403
  useContainerLinksAdapter,
3136
4404
  useEntityRelationships,