@ai-matrx/associations 0.3.0 → 0.4.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.
@@ -503,6 +503,7 @@ var ENTITY_TYPE_TOKENS = [
503
503
  "platform_actor_session",
504
504
  "platform_actor_token",
505
505
  "platform_actor_token_event",
506
+ "platform_continued_access",
506
507
  "platform_outcome_event",
507
508
  "platform_outsider_consumer",
508
509
  "platform_saved_view",
@@ -531,6 +532,7 @@ var ENTITY_TYPE_TOKENS = [
531
532
  "research_tag",
532
533
  "research_template",
533
534
  "research_topic",
535
+ "route_manifest_entry",
534
536
  "rulebook",
535
537
  "sandbox_instance",
536
538
  "sch_agent_task",
@@ -609,6 +611,7 @@ var ENTITY_TYPE_TOKENS = [
609
611
  "seo_topic",
610
612
  "seo_web_analytics_daily",
611
613
  "shared_canvas_item",
614
+ "short_link",
612
615
  "skill",
613
616
  "skill_render_definition",
614
617
  "sms_consent",
@@ -1418,6 +1421,27 @@ function ExternalLinkIcon(props) {
1418
1421
  /* @__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
1422
  ] });
1420
1423
  }
1424
+ function ChevronDownIcon(props) {
1425
+ return /* @__PURE__ */ jsx3("svg", { ...base(props), children: /* @__PURE__ */ jsx3("path", { d: "m6 9 6 6 6-6" }) });
1426
+ }
1427
+ function ChevronsUpDownIcon(props) {
1428
+ return /* @__PURE__ */ jsxs("svg", { ...base(props), children: [
1429
+ /* @__PURE__ */ jsx3("path", { d: "m7 15 5 5 5-5" }),
1430
+ /* @__PURE__ */ jsx3("path", { d: "m7 9 5-5 5 5" })
1431
+ ] });
1432
+ }
1433
+ function CornerDownRightIcon(props) {
1434
+ return /* @__PURE__ */ jsxs("svg", { ...base(props), children: [
1435
+ /* @__PURE__ */ jsx3("polyline", { points: "15 10 20 15 15 20" }),
1436
+ /* @__PURE__ */ jsx3("path", { d: "M4 4v7a4 4 0 0 0 4 4h12" })
1437
+ ] });
1438
+ }
1439
+ function TagIcon(props) {
1440
+ return /* @__PURE__ */ jsxs("svg", { ...base(props), children: [
1441
+ /* @__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" }),
1442
+ /* @__PURE__ */ jsx3("circle", { cx: "7.5", cy: "7.5", r: ".5", fill: "currentColor" })
1443
+ ] });
1444
+ }
1421
1445
  function DefaultEntityIcon(props) {
1422
1446
  return /* @__PURE__ */ jsxs("svg", { ...base(props), children: [
1423
1447
  /* @__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" }),
@@ -3106,11 +3130,769 @@ function CaptureToolbarAction({
3106
3130
  }
3107
3131
  );
3108
3132
  }
3133
+
3134
+ // src/react/components/AssociationEntitySelect.tsx
3135
+ import { useState as useState12 } from "react";
3136
+ import {
3137
+ cn as cn10,
3138
+ Command,
3139
+ CommandEmpty,
3140
+ CommandGroup,
3141
+ CommandInput,
3142
+ CommandItem,
3143
+ CommandList,
3144
+ EditableLabel,
3145
+ Popover,
3146
+ PopoverContent,
3147
+ PopoverTrigger
3148
+ } from "@ai-matrx/design-system";
3149
+ import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
3150
+ function AssociationEntitySelect({
3151
+ token,
3152
+ adapter,
3153
+ className,
3154
+ labelClassName,
3155
+ align = "end",
3156
+ showIcon = true,
3157
+ renameActivation = "click",
3158
+ emptyLabel,
3159
+ iconClassName,
3160
+ createSlot
3161
+ }) {
3162
+ const store = useAssociationsStore();
3163
+ const { entityDoors } = useAssociationsUiPorts();
3164
+ const notifier = useNotifier();
3165
+ const info = store.registry.getEntityInfo(token);
3166
+ const InfoIcon = info.Icon ?? DefaultEntityIcon;
3167
+ const DoorControls = entityDoors?.DoorControls;
3168
+ const { items, activeId } = adapter;
3169
+ const active = items.find((i) => i.id === activeId) ?? null;
3170
+ const activeIndex = active ? items.indexOf(active) : -1;
3171
+ const [open, setOpen] = useState12(false);
3172
+ const [query, setQuery] = useState12("");
3173
+ const [creating, setCreating] = useState12(false);
3174
+ const [draftName, setDraftName] = useState12("");
3175
+ const [busy, setBusy] = useState12(false);
3176
+ const entityLabel = info.label.toLowerCase();
3177
+ const close = () => {
3178
+ setOpen(false);
3179
+ setQuery("");
3180
+ setCreating(false);
3181
+ setDraftName("");
3182
+ };
3183
+ const create = async (title) => {
3184
+ const name = title.trim();
3185
+ if (!name || busy || !adapter.createAndAttach) return;
3186
+ setBusy(true);
3187
+ const id = await adapter.createAndAttach(name);
3188
+ setBusy(false);
3189
+ if (id) close();
3190
+ else notifier.error(`Couldn't create the ${entityLabel}`);
3191
+ };
3192
+ return /* @__PURE__ */ jsxs11(
3193
+ "div",
3194
+ {
3195
+ className: cn10(
3196
+ "group/entity-ref flex min-w-0 items-center gap-0.5",
3197
+ className
3198
+ ),
3199
+ children: [
3200
+ showIcon ? /* @__PURE__ */ jsx13(
3201
+ InfoIcon,
3202
+ {
3203
+ className: cn10(
3204
+ "size-3.5 shrink-0",
3205
+ iconClassName ?? "text-muted-foreground"
3206
+ ),
3207
+ "aria-hidden": true
3208
+ }
3209
+ ) : null,
3210
+ active ? /* @__PURE__ */ jsx13(
3211
+ EditableLabel,
3212
+ {
3213
+ value: active.title,
3214
+ onCommit: async (next) => {
3215
+ const ok = await adapter.rename(active.id, next);
3216
+ if (!ok) {
3217
+ notifier.error(`Couldn't rename the ${entityLabel}`);
3218
+ throw new Error("rename failed");
3219
+ }
3220
+ },
3221
+ commitMode: "await",
3222
+ activation: renameActivation,
3223
+ ariaLabel: info.label,
3224
+ className: cn10("max-w-[14rem]", labelClassName),
3225
+ displayClassName: "text-xs font-medium text-foreground",
3226
+ inputClassName: "text-xs font-medium"
3227
+ }
3228
+ ) : null,
3229
+ active && DoorControls ? /* @__PURE__ */ jsx13(DoorControls, { token, id: active.id, name: active.title }) : null,
3230
+ !active ? /* @__PURE__ */ jsx13(
3231
+ "span",
3232
+ {
3233
+ className: cn10(
3234
+ "truncate px-1 text-xs font-medium text-muted-foreground",
3235
+ labelClassName
3236
+ ),
3237
+ children: adapter.loading ? "\u2026" : emptyLabel ?? info.labelPlural
3238
+ }
3239
+ ) : null,
3240
+ /* @__PURE__ */ jsxs11(
3241
+ Popover,
3242
+ {
3243
+ open,
3244
+ onOpenChange: (next) => next ? setOpen(true) : close(),
3245
+ children: [
3246
+ /* @__PURE__ */ jsx13(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs11(
3247
+ "button",
3248
+ {
3249
+ type: "button",
3250
+ 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",
3251
+ title: `Switch or add ${info.labelPlural.toLowerCase()}`,
3252
+ "aria-label": `Switch or add ${info.labelPlural.toLowerCase()}`,
3253
+ children: [
3254
+ items.length > 1 ? /* @__PURE__ */ jsxs11("span", { className: "tabular-nums", children: [
3255
+ activeIndex >= 0 ? activeIndex + 1 : "\u2014",
3256
+ "/",
3257
+ items.length
3258
+ ] }) : null,
3259
+ /* @__PURE__ */ jsx13(ChevronDownIcon, { className: "size-3 opacity-60" })
3260
+ ]
3261
+ }
3262
+ ) }),
3263
+ /* @__PURE__ */ jsx13(PopoverContent, { className: "w-60 p-0", align, children: /* @__PURE__ */ jsxs11(Command, { children: [
3264
+ items.length > 5 ? /* @__PURE__ */ jsx13(
3265
+ CommandInput,
3266
+ {
3267
+ value: query,
3268
+ onValueChange: setQuery,
3269
+ placeholder: `Search ${info.labelPlural.toLowerCase()}\u2026`
3270
+ }
3271
+ ) : null,
3272
+ /* @__PURE__ */ jsxs11(CommandList, { children: [
3273
+ /* @__PURE__ */ jsx13(CommandEmpty, { children: "No match." }),
3274
+ items.length > 0 ? /* @__PURE__ */ jsx13(CommandGroup, { children: items.map((item) => /* @__PURE__ */ jsxs11(
3275
+ CommandItem,
3276
+ {
3277
+ value: `${item.title} ${item.id}`,
3278
+ onSelect: () => {
3279
+ if (item.id !== activeId)
3280
+ void adapter.setActive(item.id);
3281
+ close();
3282
+ },
3283
+ className: "group/entity-ref group gap-2",
3284
+ children: [
3285
+ /* @__PURE__ */ jsx13(
3286
+ CheckIcon,
3287
+ {
3288
+ className: cn10(
3289
+ "size-3.5 shrink-0",
3290
+ item.id === activeId ? "opacity-100" : "opacity-0"
3291
+ )
3292
+ }
3293
+ ),
3294
+ /* @__PURE__ */ jsx13("span", { className: "min-w-0 flex-1 truncate", children: item.title }),
3295
+ DoorControls ? /* @__PURE__ */ jsx13(
3296
+ "span",
3297
+ {
3298
+ className: "inline-flex shrink-0 items-center",
3299
+ onPointerDown: (e) => {
3300
+ e.preventDefault();
3301
+ e.stopPropagation();
3302
+ },
3303
+ children: /* @__PURE__ */ jsx13(
3304
+ DoorControls,
3305
+ {
3306
+ token,
3307
+ id: item.id,
3308
+ name: item.title
3309
+ }
3310
+ )
3311
+ }
3312
+ ) : null,
3313
+ adapter.detach && item.id !== activeId ? /* @__PURE__ */ jsx13(
3314
+ "button",
3315
+ {
3316
+ type: "button",
3317
+ onPointerDown: (e) => {
3318
+ e.preventDefault();
3319
+ e.stopPropagation();
3320
+ },
3321
+ onClick: (e) => {
3322
+ e.stopPropagation();
3323
+ void adapter.detach?.(item.id);
3324
+ },
3325
+ 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",
3326
+ title: `Remove this ${entityLabel} from here (does not delete it)`,
3327
+ "aria-label": `Remove ${item.title}`,
3328
+ children: /* @__PURE__ */ jsx13(XIcon, { className: "size-3" })
3329
+ }
3330
+ ) : null
3331
+ ]
3332
+ },
3333
+ item.id
3334
+ )) }) : null
3335
+ ] }),
3336
+ createSlot !== void 0 ? /* @__PURE__ */ jsx13("div", { className: "border-t border-border p-1", children: typeof createSlot === "function" ? createSlot(close) : createSlot }) : null,
3337
+ 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: [
3338
+ /* @__PURE__ */ jsx13(
3339
+ "input",
3340
+ {
3341
+ autoFocus: true,
3342
+ type: "text",
3343
+ value: draftName,
3344
+ disabled: busy,
3345
+ placeholder: `New ${entityLabel} name\u2026`,
3346
+ onChange: (e) => setDraftName(e.target.value),
3347
+ onKeyDown: (e) => {
3348
+ if (e.key === "Enter") {
3349
+ e.preventDefault();
3350
+ void create(draftName);
3351
+ } else if (e.key === "Escape") {
3352
+ e.preventDefault();
3353
+ e.stopPropagation();
3354
+ setCreating(false);
3355
+ setDraftName("");
3356
+ }
3357
+ },
3358
+ 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",
3359
+ "aria-label": `New ${entityLabel} name`
3360
+ }
3361
+ ),
3362
+ /* @__PURE__ */ jsx13(
3363
+ "button",
3364
+ {
3365
+ type: "button",
3366
+ disabled: busy || !draftName.trim(),
3367
+ onClick: () => void create(draftName),
3368
+ 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",
3369
+ children: busy ? /* @__PURE__ */ jsx13(SpinnerIcon, { className: "size-3 animate-spin" }) : "Create"
3370
+ }
3371
+ )
3372
+ ] }) : /* @__PURE__ */ jsxs11(
3373
+ "button",
3374
+ {
3375
+ type: "button",
3376
+ disabled: busy,
3377
+ onClick: () => {
3378
+ if (query.trim()) void create(query);
3379
+ else setCreating(true);
3380
+ },
3381
+ 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",
3382
+ children: [
3383
+ busy ? /* @__PURE__ */ jsx13(SpinnerIcon, { className: "size-3.5 animate-spin" }) : /* @__PURE__ */ jsx13(PlusIcon, { className: "size-3.5" }),
3384
+ query.trim() ? `Create "${query.trim()}"` : `New ${info.label}`
3385
+ ]
3386
+ }
3387
+ ) }) : null
3388
+ ] }) })
3389
+ ]
3390
+ }
3391
+ )
3392
+ ]
3393
+ }
3394
+ );
3395
+ }
3396
+
3397
+ // src/react/components/CategorySelect.tsx
3398
+ import { useState as useState13 } from "react";
3399
+ import {
3400
+ CreatablePicker,
3401
+ Select,
3402
+ SelectContent,
3403
+ SelectItem,
3404
+ SelectTrigger,
3405
+ SelectValue
3406
+ } from "@ai-matrx/design-system";
3407
+
3408
+ // src/core/categoryHierarchy.ts
3409
+ function buildCategoryHierarchy(categories) {
3410
+ const byId = new Map(categories.map((category) => [category.id, category]));
3411
+ const hasHierarchy = categories.some(
3412
+ (category) => category.parentId !== null && byId.has(category.parentId)
3413
+ );
3414
+ if (!hasHierarchy) {
3415
+ return {
3416
+ items: categories.map((category) => ({
3417
+ category,
3418
+ depth: 0,
3419
+ parent: null,
3420
+ displayName: category.name
3421
+ })),
3422
+ roots: categories.filter((category) => category.parentId === null),
3423
+ hasHierarchy: false
3424
+ };
3425
+ }
3426
+ const roots = categories.filter(
3427
+ (category) => category.parentId === null || !byId.has(category.parentId)
3428
+ );
3429
+ const childrenByParent = /* @__PURE__ */ new Map();
3430
+ for (const category of categories) {
3431
+ if (!category.parentId || !byId.has(category.parentId)) continue;
3432
+ const children = childrenByParent.get(category.parentId) ?? [];
3433
+ children.push(category);
3434
+ childrenByParent.set(category.parentId, children);
3435
+ }
3436
+ const items = [];
3437
+ const seen = /* @__PURE__ */ new Set();
3438
+ for (const root of roots) {
3439
+ items.push({
3440
+ category: root,
3441
+ depth: 0,
3442
+ parent: null,
3443
+ displayName: root.name
3444
+ });
3445
+ seen.add(root.id);
3446
+ for (const child of childrenByParent.get(root.id) ?? []) {
3447
+ items.push({
3448
+ category: child,
3449
+ depth: 1,
3450
+ parent: root,
3451
+ displayName: `${root.name} / ${child.name}`
3452
+ });
3453
+ seen.add(child.id);
3454
+ }
3455
+ }
3456
+ for (const category of categories) {
3457
+ if (seen.has(category.id)) continue;
3458
+ items.push({
3459
+ category,
3460
+ depth: 0,
3461
+ parent: null,
3462
+ displayName: category.name
3463
+ });
3464
+ }
3465
+ return {
3466
+ items,
3467
+ roots: categories.filter((category) => category.parentId === null),
3468
+ hasHierarchy: true
3469
+ };
3470
+ }
3471
+
3472
+ // src/react/components/CategorySelect.tsx
3473
+ import { jsx as jsx14, jsxs as jsxs12 } from "react/jsx-runtime";
3474
+ var NONE = "__none__";
3475
+ var ROOT_PARENT = "__root__";
3476
+ function plural(noun) {
3477
+ return /[^aeiou]y$/i.test(noun) ? `${noun.slice(0, -1)}ies` : `${noun}s`;
3478
+ }
3479
+ function CategorySelect({
3480
+ dimension,
3481
+ value,
3482
+ onChange,
3483
+ placeholder,
3484
+ allowNone = true,
3485
+ disabled,
3486
+ allowCreate = true,
3487
+ orgId,
3488
+ noun = "category"
3489
+ }) {
3490
+ const store = useAssociationsStore();
3491
+ const notifier = useNotifier();
3492
+ const {
3493
+ categories,
3494
+ status,
3495
+ error,
3496
+ create: createCategory,
3497
+ reload
3498
+ } = useCategories({ dimension });
3499
+ const hierarchy = buildCategoryHierarchy(categories);
3500
+ const [createParentId, setCreateParentId] = useState13(ROOT_PARENT);
3501
+ const options = [
3502
+ ...allowNone ? [{ value: NONE, label: "None", keywords: "none clear empty" }] : [],
3503
+ ...hierarchy.hasHierarchy ? hierarchy.items.map(({ category, depth, parent }) => ({
3504
+ value: category.id,
3505
+ label: category.name,
3506
+ // The type-ahead matches the parent's name too, so someone who knows
3507
+ // the branch but not the leaf still finds it.
3508
+ keywords: parent ? parent.name : "",
3509
+ 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: [
3510
+ /* @__PURE__ */ jsx14(CornerDownRightIcon, { className: "h-3.5 w-3.5 shrink-0" }),
3511
+ /* @__PURE__ */ jsx14("span", { className: "text-foreground", children: category.name })
3512
+ ] })
3513
+ })) : categories.map((category) => ({
3514
+ value: category.id,
3515
+ label: category.name
3516
+ }))
3517
+ ];
3518
+ const selected = value ? hierarchy.items.find((item) => item.category.id === value)?.displayName ?? categories.find((category) => category.id === value)?.name ?? null : null;
3519
+ const create = async (typed) => {
3520
+ const name = typed.trim();
3521
+ if (!name) return null;
3522
+ const existing = categories.find(
3523
+ (category) => category.name.toLowerCase() === name.toLowerCase()
3524
+ );
3525
+ if (existing) return existing.id;
3526
+ let resolvedOrgId = orgId ?? null;
3527
+ if (!resolvedOrgId && store.identity.ensureOrgId) {
3528
+ try {
3529
+ resolvedOrgId = await store.identity.ensureOrgId();
3530
+ } catch (e) {
3531
+ store.errorSink({
3532
+ code: "ensure_org_failed",
3533
+ message: "[CategorySelect] identity.ensureOrgId threw",
3534
+ context: { error: e instanceof Error ? e.message : String(e) }
3535
+ });
3536
+ }
3537
+ }
3538
+ if (!resolvedOrgId) {
3539
+ notifier.error(`Select an organization before creating a ${noun}.`);
3540
+ return null;
3541
+ }
3542
+ const parentId = hierarchy.hasHierarchy && createParentId !== ROOT_PARENT ? createParentId : null;
3543
+ const result = await createCategory({ name, orgId: resolvedOrgId, parentId });
3544
+ if (!result.ok || !result.id) {
3545
+ notifier.error(`Couldn't create that ${noun}`, {
3546
+ ...result.error != null ? { description: result.error } : {}
3547
+ });
3548
+ return null;
3549
+ }
3550
+ await reload();
3551
+ setCreateParentId(ROOT_PARENT);
3552
+ const under = parentId ? categories.find((category) => category.id === parentId)?.name : null;
3553
+ notifier.success(
3554
+ `\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.`}`
3555
+ );
3556
+ return result.id;
3557
+ };
3558
+ return /* @__PURE__ */ jsxs12("div", { children: [
3559
+ /* @__PURE__ */ jsx14(
3560
+ CreatablePicker,
3561
+ {
3562
+ value: value ?? (allowNone ? NONE : null),
3563
+ onSelect: (next) => onChange(next === NONE ? null : next),
3564
+ options,
3565
+ placeholder,
3566
+ searchPlaceholder: allowCreate ? `Search or add a ${noun}\u2026` : `Search ${plural(noun)}\u2026`,
3567
+ noun,
3568
+ ariaLabel: placeholder,
3569
+ className: "text-sm",
3570
+ ...disabled !== void 0 ? { disabled } : {},
3571
+ loading: status === "loading",
3572
+ emptyLabel: `No ${noun} matches that.`,
3573
+ ...allowCreate ? { onCreate: create } : {},
3574
+ ...allowCreate && hierarchy.hasHierarchy ? {
3575
+ createExtra: /* @__PURE__ */ jsxs12(Select, { value: createParentId, onValueChange: setCreateParentId, children: [
3576
+ /* @__PURE__ */ jsx14(
3577
+ SelectTrigger,
3578
+ {
3579
+ className: "h-7 text-xs",
3580
+ "aria-label": `Put the new ${noun} under`,
3581
+ children: /* @__PURE__ */ jsx14(SelectValue, {})
3582
+ }
3583
+ ),
3584
+ /* @__PURE__ */ jsxs12(SelectContent, { children: [
3585
+ /* @__PURE__ */ jsx14(SelectItem, { value: ROOT_PARENT, className: "text-xs", children: "Top level" }),
3586
+ hierarchy.items.filter((item) => item.depth === 0).map((item) => /* @__PURE__ */ jsxs12(
3587
+ SelectItem,
3588
+ {
3589
+ value: item.category.id,
3590
+ className: "text-xs",
3591
+ children: [
3592
+ "Under ",
3593
+ item.category.name
3594
+ ]
3595
+ },
3596
+ item.category.id
3597
+ ))
3598
+ ] })
3599
+ ] })
3600
+ } : {}
3601
+ }
3602
+ ),
3603
+ selected === null && value !== null ? (
3604
+ // A stored id whose row this reader cannot see is not "nothing chosen":
3605
+ // saying so beats a blank box that looks like an unsaved field.
3606
+ /* @__PURE__ */ jsxs12("p", { className: "mt-1 text-xs text-muted-foreground", children: [
3607
+ "This ",
3608
+ noun,
3609
+ " is no longer available to you."
3610
+ ] })
3611
+ ) : null,
3612
+ error ? /* @__PURE__ */ jsx14("p", { className: "mt-1 text-xs text-destructive", children: error }) : null
3613
+ ] });
3614
+ }
3615
+
3616
+ // src/react/components/CategoryTagPicker.tsx
3617
+ import { useState as useState14 } from "react";
3618
+ import {
3619
+ Badge,
3620
+ Button as Button2,
3621
+ cn as cn11,
3622
+ Command as Command2,
3623
+ CommandEmpty as CommandEmpty2,
3624
+ CommandGroup as CommandGroup2,
3625
+ CommandInput as CommandInput2,
3626
+ CommandItem as CommandItem2,
3627
+ CommandList as CommandList2,
3628
+ Popover as Popover2,
3629
+ PopoverContent as PopoverContent2,
3630
+ PopoverTrigger as PopoverTrigger2,
3631
+ Select as Select2,
3632
+ SelectContent as SelectContent2,
3633
+ SelectItem as SelectItem2,
3634
+ SelectTrigger as SelectTrigger2,
3635
+ SelectValue as SelectValue2
3636
+ } from "@ai-matrx/design-system";
3637
+ import { jsx as jsx15, jsxs as jsxs13 } from "react/jsx-runtime";
3638
+ var ROOT_PARENT2 = "__root__";
3639
+ function CategoryTagPicker({
3640
+ entityType,
3641
+ entityId,
3642
+ dimension,
3643
+ edgeRole,
3644
+ orgId,
3645
+ addLabel = "Add tag",
3646
+ icon: Icon = TagIcon,
3647
+ allowCreate = true,
3648
+ hierarchical,
3649
+ emptyText = "No categories yet."
3650
+ }) {
3651
+ const store = useAssociationsStore();
3652
+ const notifier = useNotifier();
3653
+ const [open, setOpen] = useState14(false);
3654
+ const [creating, setCreating] = useState14(false);
3655
+ const [writing, setWriting] = useState14(false);
3656
+ const [search, setSearch] = useState14("");
3657
+ const [createParentId, setCreateParentId] = useState14(ROOT_PARENT2);
3658
+ const {
3659
+ categories,
3660
+ create: createCategory,
3661
+ reload: reloadCategories
3662
+ } = useCategories({ dimension });
3663
+ const { edges, setTargets } = useAssociations({
3664
+ type: entityType,
3665
+ id: entityId
3666
+ });
3667
+ const hierarchy = buildCategoryHierarchy(categories);
3668
+ const showHierarchy = hierarchical ?? hierarchy.hasHierarchy;
3669
+ const hierarchyById = new Map(
3670
+ hierarchy.items.map((item) => [item.category.id, item])
3671
+ );
3672
+ const selectedIds = new Set(
3673
+ edges.filter(
3674
+ (e) => e.direction === "outgoing" && e.otherType === "category" && e.role === edgeRole
3675
+ ).map((e) => e.otherId)
3676
+ );
3677
+ const selected = categories.filter((c) => selectedIds.has(c.id));
3678
+ const toggle = async (categoryId) => {
3679
+ if (writing) return;
3680
+ setWriting(true);
3681
+ try {
3682
+ const next = selectedIds.has(categoryId) ? [...selectedIds].filter((id) => id !== categoryId) : [...selectedIds, categoryId];
3683
+ const res = await setTargets({
3684
+ targetType: "category",
3685
+ targetIds: next,
3686
+ ...orgId != null ? { orgId } : {},
3687
+ role: edgeRole
3688
+ });
3689
+ if (!res.ok) {
3690
+ store.errorSink({
3691
+ code: "category_tag_set_targets_failed",
3692
+ message: "[CategoryTagPicker] setTargets failed",
3693
+ context: { entityType, entityId, edgeRole, error: res.error }
3694
+ });
3695
+ notifier.error(res.error ?? "Could not update categories");
3696
+ }
3697
+ } finally {
3698
+ setWriting(false);
3699
+ }
3700
+ };
3701
+ const createAndAttach = async () => {
3702
+ const name = search.trim();
3703
+ if (!name) return;
3704
+ setCreating(true);
3705
+ try {
3706
+ let resolvedOrgId = orgId ?? null;
3707
+ if (!resolvedOrgId && store.identity.ensureOrgId) {
3708
+ try {
3709
+ resolvedOrgId = await store.identity.ensureOrgId();
3710
+ } catch (e) {
3711
+ store.errorSink({
3712
+ code: "ensure_org_failed",
3713
+ message: "[CategoryTagPicker] identity.ensureOrgId threw",
3714
+ context: { error: e instanceof Error ? e.message : String(e) }
3715
+ });
3716
+ }
3717
+ }
3718
+ if (!resolvedOrgId) {
3719
+ store.errorSink({
3720
+ code: "ensure_org_missing",
3721
+ message: "[CategoryTagPicker] no org for the created category \u2014 pass `orgId` or bind `identity.ensureOrgId` on the store config",
3722
+ context: { entityType, entityId, dimension }
3723
+ });
3724
+ notifier.error(
3725
+ "Could not create the category \u2014 no organization is selected"
3726
+ );
3727
+ return;
3728
+ }
3729
+ const parentId = showHierarchy && createParentId !== ROOT_PARENT2 ? createParentId : null;
3730
+ const createRes = await createCategory({
3731
+ name,
3732
+ orgId: resolvedOrgId,
3733
+ parentId
3734
+ });
3735
+ if (!createRes.ok || !createRes.id) {
3736
+ store.errorSink({
3737
+ code: "category_tag_create_failed",
3738
+ message: "[CategoryTagPicker] create failed",
3739
+ context: { dimension, error: createRes.error }
3740
+ });
3741
+ notifier.error(createRes.error ?? "Could not create the category");
3742
+ return;
3743
+ }
3744
+ await reloadCategories();
3745
+ await toggle(createRes.id);
3746
+ setSearch("");
3747
+ setCreateParentId(ROOT_PARENT2);
3748
+ } finally {
3749
+ setCreating(false);
3750
+ }
3751
+ };
3752
+ const filtered = categories.filter(
3753
+ (c) => c.name.toLowerCase().includes(search.trim().toLowerCase())
3754
+ );
3755
+ const filteredHierarchy = hierarchy.items.filter(
3756
+ (item) => item.displayName.toLowerCase().includes(search.trim().toLowerCase())
3757
+ );
3758
+ const exactMatch = categories.some(
3759
+ (c) => c.name.toLowerCase() === search.trim().toLowerCase()
3760
+ );
3761
+ return /* @__PURE__ */ jsxs13("div", { className: "flex flex-wrap items-center gap-1.5", children: [
3762
+ selected.map((c) => /* @__PURE__ */ jsxs13(
3763
+ Badge,
3764
+ {
3765
+ variant: "secondary",
3766
+ className: "gap-1 pr-1 text-xs font-medium",
3767
+ children: [
3768
+ showHierarchy ? hierarchyById.get(c.id)?.displayName ?? c.name : c.name,
3769
+ /* @__PURE__ */ jsx15(
3770
+ "button",
3771
+ {
3772
+ type: "button",
3773
+ onClick: () => void toggle(c.id),
3774
+ className: "ml-0.5 rounded-full p-0.5 hover:bg-muted-foreground/20",
3775
+ "aria-label": `Remove ${c.name}`,
3776
+ children: /* @__PURE__ */ jsx15(XIcon, { className: "h-2.5 w-2.5" })
3777
+ }
3778
+ )
3779
+ ]
3780
+ },
3781
+ c.id
3782
+ )),
3783
+ /* @__PURE__ */ jsxs13(Popover2, { open, onOpenChange: setOpen, children: [
3784
+ /* @__PURE__ */ jsx15(PopoverTrigger2, { asChild: true, children: /* @__PURE__ */ jsxs13(
3785
+ Button2,
3786
+ {
3787
+ variant: "outline",
3788
+ size: "sm",
3789
+ className: "h-7 gap-1 px-2 text-xs",
3790
+ children: [
3791
+ /* @__PURE__ */ jsx15(Icon, { className: "h-3.5 w-3.5" }),
3792
+ selected.length === 0 ? addLabel : "Edit",
3793
+ /* @__PURE__ */ jsx15(ChevronsUpDownIcon, { className: "h-3 w-3 opacity-50" })
3794
+ ]
3795
+ }
3796
+ ) }),
3797
+ /* @__PURE__ */ jsx15(PopoverContent2, { className: "w-56 p-0", align: "start", children: /* @__PURE__ */ jsxs13(Command2, { shouldFilter: false, children: [
3798
+ /* @__PURE__ */ jsx15(
3799
+ CommandInput2,
3800
+ {
3801
+ placeholder: allowCreate ? "Search or create..." : "Search...",
3802
+ value: search,
3803
+ onValueChange: setSearch
3804
+ }
3805
+ ),
3806
+ /* @__PURE__ */ jsxs13(CommandList2, { children: [
3807
+ /* @__PURE__ */ jsx15(CommandEmpty2, { className: "px-2 py-3 text-xs text-muted-foreground", children: search.trim() ? "No match." : emptyText }),
3808
+ /* @__PURE__ */ jsx15(CommandGroup2, { children: showHierarchy ? filteredHierarchy.map(({ category, depth, parent }) => /* @__PURE__ */ jsxs13(
3809
+ CommandItem2,
3810
+ {
3811
+ value: category.id,
3812
+ keywords: parent ? [parent.name, category.name] : [],
3813
+ onSelect: () => void toggle(category.id),
3814
+ className: cn11("text-xs", depth === 1 && "pl-6"),
3815
+ children: [
3816
+ /* @__PURE__ */ jsx15(
3817
+ CheckIcon,
3818
+ {
3819
+ className: cn11(
3820
+ "mr-2 h-3.5 w-3.5",
3821
+ selectedIds.has(category.id) ? "opacity-100" : "opacity-0"
3822
+ )
3823
+ }
3824
+ ),
3825
+ depth === 1 ? /* @__PURE__ */ jsx15(CornerDownRightIcon, { className: "mr-1.5 h-3.5 w-3.5 shrink-0 text-muted-foreground" }) : null,
3826
+ /* @__PURE__ */ jsx15("span", { className: cn11(depth === 0 && "font-medium"), children: category.name })
3827
+ ]
3828
+ },
3829
+ category.id
3830
+ )) : filtered.map((c) => /* @__PURE__ */ jsxs13(
3831
+ CommandItem2,
3832
+ {
3833
+ value: c.id,
3834
+ onSelect: () => void toggle(c.id),
3835
+ className: "text-xs",
3836
+ children: [
3837
+ /* @__PURE__ */ jsx15(
3838
+ CheckIcon,
3839
+ {
3840
+ className: cn11(
3841
+ "mr-2 h-3.5 w-3.5",
3842
+ selectedIds.has(c.id) ? "opacity-100" : "opacity-0"
3843
+ )
3844
+ }
3845
+ ),
3846
+ c.name
3847
+ ]
3848
+ },
3849
+ c.id
3850
+ )) }),
3851
+ allowCreate && search.trim() && !exactMatch ? /* @__PURE__ */ jsxs13(CommandGroup2, { children: [
3852
+ showHierarchy ? /* @__PURE__ */ jsxs13("div", { className: "space-y-1.5 px-2 pb-2", children: [
3853
+ /* @__PURE__ */ jsx15("p", { className: "text-[11px] font-medium text-muted-foreground", children: "Place in" }),
3854
+ /* @__PURE__ */ jsxs13(
3855
+ Select2,
3856
+ {
3857
+ value: createParentId,
3858
+ onValueChange: setCreateParentId,
3859
+ children: [
3860
+ /* @__PURE__ */ jsx15(SelectTrigger2, { size: "sm", className: "bg-background", children: /* @__PURE__ */ jsx15(SelectValue2, {}) }),
3861
+ /* @__PURE__ */ jsxs13(SelectContent2, { children: [
3862
+ /* @__PURE__ */ jsx15(SelectItem2, { value: ROOT_PARENT2, children: "Top level" }),
3863
+ hierarchy.roots.map((root) => /* @__PURE__ */ jsx15(SelectItem2, { value: root.id, children: root.name }, root.id))
3864
+ ] })
3865
+ ]
3866
+ }
3867
+ )
3868
+ ] }) : null,
3869
+ /* @__PURE__ */ jsxs13(
3870
+ CommandItem2,
3871
+ {
3872
+ value: `__create__${search}`,
3873
+ onSelect: () => void createAndAttach(),
3874
+ disabled: creating,
3875
+ className: "text-xs text-primary",
3876
+ children: [
3877
+ 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" }),
3878
+ "Create \u201C",
3879
+ search.trim(),
3880
+ "\u201D"
3881
+ ]
3882
+ }
3883
+ )
3884
+ ] }) : null
3885
+ ] })
3886
+ ] }) })
3887
+ ] })
3888
+ ] });
3889
+ }
3109
3890
  export {
3110
3891
  AssociationCandidateBody,
3111
3892
  AssociationCaptureToolbar,
3112
3893
  AssociationCard,
3113
3894
  AssociationCardGrid,
3895
+ AssociationEntitySelect,
3114
3896
  AssociationList,
3115
3897
  AssociationPicker,
3116
3898
  AssociationWindow,
@@ -3118,6 +3900,8 @@ export {
3118
3900
  AttachedItemsSheet,
3119
3901
  CONTENT_ROLES,
3120
3902
  CaptureToolbarAction,
3903
+ CategorySelect,
3904
+ CategoryTagPicker,
3121
3905
  DefaultEntityIcon,
3122
3906
  DoorRef,
3123
3907
  PrimaryEntityProvider,