@ai-matrx/records-ui 0.102.0 → 0.102.4

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.
package/dist/pickers.cjs CHANGED
@@ -561,29 +561,121 @@ var import_design_system4 = require("@ai-matrx/design-system");
561
561
  var import_react2 = require("react");
562
562
  var import_design_system2 = require("@ai-matrx/design-system");
563
563
  var import_jsx_runtime2 = require("react/jsx-runtime");
564
+ function Glyph({ d, className }) {
565
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, className, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d }) });
566
+ }
567
+ var TICK = "m5 12 4 4L19 6";
568
+ var CROSS = "M18 6 6 18M6 6l12 12";
569
+ var PLUS = "M12 5v14M5 12h14";
564
570
  function useListKeys(args) {
565
- const { count, resetOn, onPick, onNothing, onClose } = args;
571
+ const { count, resetOn, onPick, onNothing, onClose, onFinish, onBackspaceEmpty, searchEmpty } = args;
566
572
  const [highlight, setHighlight] = (0, import_react2.useState)(0);
567
- (0, import_react2.useEffect)(() => setHighlight(0), [resetOn]);
573
+ const [moved, setMoved] = (0, import_react2.useState)(false);
574
+ (0, import_react2.useEffect)(() => {
575
+ setHighlight(0);
576
+ setMoved(false);
577
+ }, [resetOn]);
568
578
  const at = count === 0 ? -1 : Math.min(highlight, count - 1);
569
579
  const onKeyDown = (event) => {
580
+ if (event.key === "Tab") return;
570
581
  event.stopPropagation();
571
582
  if (event.key === "ArrowDown") {
572
583
  event.preventDefault();
584
+ setMoved(true);
573
585
  if (count > 0) setHighlight((h) => (Math.min(h, count - 1) + 1) % count);
574
586
  } else if (event.key === "ArrowUp") {
575
587
  event.preventDefault();
588
+ setMoved(true);
576
589
  if (count > 0) setHighlight((h) => (Math.min(h, count - 1) - 1 + count) % count);
577
590
  } else if (event.key === "Enter") {
578
591
  event.preventDefault();
579
- if (at >= 0) onPick(at);
592
+ if (onFinish && searchEmpty && !moved) onFinish();
593
+ else if (at >= 0) onPick(at);
580
594
  else onNothing?.();
581
595
  } else if (event.key === "Escape") {
582
596
  event.preventDefault();
583
597
  onClose();
598
+ } else if (event.key === "Backspace" && searchEmpty && onBackspaceEmpty) {
599
+ event.preventDefault();
600
+ onBackspaceEmpty();
584
601
  }
585
602
  };
586
- return { highlight: at, onKeyDown };
603
+ return { highlight: at, setHighlight, onKeyDown };
604
+ }
605
+ function PickRow({
606
+ label,
607
+ detail,
608
+ title,
609
+ chip,
610
+ picked,
611
+ highlighted,
612
+ disabled,
613
+ attrs,
614
+ icon,
615
+ onPick,
616
+ onPoint
617
+ }) {
618
+ const ref = (0, import_react2.useRef)(null);
619
+ (0, import_react2.useEffect)(() => {
620
+ if (highlighted) ref.current?.scrollIntoView?.({ block: "nearest" });
621
+ }, [highlighted]);
622
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
623
+ "button",
624
+ {
625
+ ref,
626
+ type: "button",
627
+ role: "option",
628
+ "aria-selected": picked,
629
+ disabled: disabled ?? false,
630
+ onClick: onPick,
631
+ onMouseMove: onPoint,
632
+ title: title ?? label,
633
+ ...attrs ?? {},
634
+ "data-pick-highlighted": highlighted ? "" : void 0,
635
+ className: (0, import_design_system2.cn)(
636
+ "flex w-full min-w-0 items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm outline-none disabled:opacity-50",
637
+ highlighted ? "bg-accent text-accent-foreground" : ""
638
+ ),
639
+ children: [
640
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "flex size-3.5 shrink-0 items-center justify-center", children: [
641
+ picked ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { "data-pick-tick": "", className: "contents", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Glyph, { d: TICK, className: "size-3.5" }) }) : null,
642
+ icon === "plus" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Glyph, { d: PLUS, className: "size-3.5 text-muted-foreground" }) : null
643
+ ] }),
644
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "min-w-0 flex-1", children: [
645
+ chip ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: (0, import_design_system2.cn)("inline-flex max-w-full truncate rounded-md border px-1.5 py-0.5 text-xs font-medium", chip), "data-records-choice-chip": "", children: label }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "block truncate", children: label }),
646
+ detail ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "block truncate text-xs text-muted-foreground", "data-tell-apart": "", children: detail }) : null
647
+ ] })
648
+ ]
649
+ }
650
+ );
651
+ }
652
+ function PickChips({ chips }) {
653
+ if (chips.length === 0) return null;
654
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex flex-wrap gap-1 px-1 pb-1", "data-pick-chips": "", children: chips.map((c) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
655
+ "span",
656
+ {
657
+ "data-pick-chip": "",
658
+ title: c.label,
659
+ className: (0, import_design_system2.cn)(
660
+ "inline-flex max-w-full items-center gap-1 rounded-md border px-1.5 py-0.5 text-xs font-medium",
661
+ c.chip ?? "bg-secondary text-secondary-foreground"
662
+ ),
663
+ children: [
664
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "min-w-0 truncate", children: c.label }),
665
+ c.onRemove ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
666
+ "button",
667
+ {
668
+ type: "button",
669
+ "aria-label": `Remove ${c.label}`,
670
+ onClick: c.onRemove,
671
+ className: "shrink-0 rounded-sm opacity-60 hover:opacity-100",
672
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Glyph, { d: CROSS, className: "size-3" })
673
+ }
674
+ ) : null
675
+ ]
676
+ },
677
+ c.id
678
+ )) });
587
679
  }
588
680
  function PickOrAdd({
589
681
  triggerLabel,
@@ -600,30 +692,51 @@ function PickOrAdd({
600
692
  add,
601
693
  foot,
602
694
  footNote,
603
- onEnterWithNoMatch
695
+ onEnterWithNoMatch,
696
+ anchored,
697
+ children,
698
+ chips,
699
+ multi
604
700
  }) {
605
- const hasFoot = Boolean(add) || Boolean(foot) || Boolean(footNote);
701
+ const addRow = add && !foot ? add : null;
702
+ const count = items.length + (addRow ? 1 : 0);
703
+ const removable = (chips ?? []).filter((c) => c.onRemove);
606
704
  const keys = useListKeys({
607
- count: items.length,
705
+ count,
608
706
  resetOn: search,
609
707
  onPick: (index) => {
610
708
  const item = items[index];
611
- if (item) onPick(item.id);
709
+ if (item) {
710
+ if (!item.disabled) onPick(item.id);
711
+ } else if (addRow && !addRow.disabled) addRow.onAdd();
612
712
  },
613
713
  onNothing: onEnterWithNoMatch ?? (add && !add.disabled ? add.onAdd : void 0),
614
- onClose: () => onOpenChange(false)
714
+ onClose: () => onOpenChange(false),
715
+ ...multi ? { onFinish: () => onOpenChange(false) } : {},
716
+ ...removable.length > 0 ? { onBackspaceEmpty: () => removable[removable.length - 1]?.onRemove?.() } : {},
717
+ searchEmpty: search === ""
615
718
  });
719
+ const hasFoot = Boolean(foot) || Boolean(footNote);
616
720
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_design_system2.Popover, { open, onOpenChange, children: [
617
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_design_system2.PopoverTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_design_system2.Button, { id: triggerId, type: "button", size: "sm", variant: "outline", "aria-label": triggerLabel, children: triggerText }) }),
721
+ anchored ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_design_system2.PopoverAnchor, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex min-h-6 w-full min-w-0 flex-wrap items-center gap-1", "aria-label": triggerLabel, children }) }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_design_system2.PopoverTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_design_system2.Button, { id: triggerId, type: "button", size: "sm", variant: "outline", "aria-label": triggerLabel, children: triggerText }) }),
618
722
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
619
723
  import_design_system2.PopoverContent,
620
724
  {
621
725
  sizing: "content",
622
- className: "p-1",
726
+ padding: "xs",
623
727
  align: "start",
728
+ side: "bottom",
729
+ sideOffset: anchored ? 2 : 4,
730
+ animated: false,
624
731
  "data-matrx-cell-editor": "",
625
- onKeyDown: (event) => event.stopPropagation(),
732
+ onKeyDown: (event) => {
733
+ if (event.key !== "Tab") event.stopPropagation();
734
+ },
735
+ onCloseAutoFocus: (event) => {
736
+ if (anchored) event.preventDefault();
737
+ },
626
738
  children: [
739
+ chips ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PickChips, { chips }) : null,
627
740
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
628
741
  import_design_system2.BasicInput,
629
742
  {
@@ -634,53 +747,46 @@ function PickOrAdd({
634
747
  "aria-label": `Search \u2014 ${triggerLabel}`,
635
748
  autoFocus: true,
636
749
  autoComplete: "off",
637
- className: "mb-1 h-7 text-xs"
750
+ className: "mb-1 h-8 text-sm"
638
751
  }
639
752
  ),
640
753
  notice,
641
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_design_system2.ScrollArea, { className: "max-h-56", children: [
642
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ul", { role: "listbox", "aria-label": triggerLabel, children: items.map((item, index) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
643
- "button",
644
- {
645
- type: "button",
646
- onClick: () => onPick(item.id),
647
- title: item.title ?? item.label,
648
- ...item.attrs ?? {},
649
- "data-pick-highlighted": index === keys.highlight ? "" : void 0,
650
- className: (0, import_design_system2.cn)(
651
- "w-full truncate rounded px-2 py-1 text-left text-xs hover:bg-muted",
652
- item.picked ? "bg-accent text-accent-foreground" : "",
653
- index === keys.highlight ? "ring-1 ring-ring" : ""
654
- ),
655
- children: [
656
- item.chip ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
657
- "span",
658
- {
659
- className: (0, import_design_system2.cn)("inline-flex max-w-full truncate rounded-md border px-1.5 py-0.5 text-[11px] font-medium", item.chip),
660
- "data-records-choice-chip": "",
661
- children: item.label
662
- }
663
- ) : item.label,
664
- item.detail ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "block truncate text-[11px] text-muted-foreground", "data-tell-apart": "", children: item.detail }) : null
665
- ]
666
- }
667
- ) }, item.id)) }),
754
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_design_system2.ScrollArea, { className: "max-h-64", children: [
755
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { role: "listbox", "aria-label": triggerLabel, "aria-multiselectable": multi ? true : void 0, children: [
756
+ items.map((item, index) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
757
+ PickRow,
758
+ {
759
+ label: item.label,
760
+ detail: item.detail,
761
+ title: item.title,
762
+ chip: item.chip,
763
+ picked: item.picked,
764
+ disabled: item.disabled,
765
+ attrs: item.attrs,
766
+ highlighted: index === keys.highlight,
767
+ onPick: () => onPick(item.id),
768
+ onPoint: () => keys.setHighlight(index)
769
+ },
770
+ item.id
771
+ )),
772
+ addRow ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
773
+ PickRow,
774
+ {
775
+ label: addRow.label,
776
+ picked: false,
777
+ icon: "plus",
778
+ disabled: addRow.disabled,
779
+ attrs: { "data-pick-add": "" },
780
+ highlighted: items.length === keys.highlight,
781
+ onPick: addRow.onAdd,
782
+ onPoint: () => keys.setHighlight(items.length)
783
+ }
784
+ ) : null
785
+ ] }),
668
786
  items.length === 0 && empty ? empty : null
669
787
  ] }),
670
788
  hasFoot ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mt-1 border-t pt-1", children: [
671
- foot ?? (add ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
672
- import_design_system2.Button,
673
- {
674
- type: "button",
675
- size: "sm",
676
- variant: "ghost",
677
- title: add.label,
678
- className: "w-full justify-start text-xs",
679
- disabled: add.disabled ?? false,
680
- onClick: add.onAdd,
681
- children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "block w-full truncate text-left", children: add.label })
682
- }
683
- ) : null),
789
+ foot,
684
790
  footNote
685
791
  ] }) : null
686
792
  ]
@@ -1000,13 +1106,19 @@ var import_jsx_runtime4 = (
1000
1106
  require("react/jsx-runtime")
1001
1107
  );
1002
1108
  var NO_STORE_ON_THIS_SCREEN_REASON = "This question points at a record in somebody's own data, and a page answered without an account is never allowed to search that. Leave it blank \u2014 whoever receives your answer will fill it in.";
1003
- function RelationPicker({ field, value, onChange, disabled, id, className, allowCreate = true, onPicked }) {
1109
+ function RelationPicker({ field, value, onChange, disabled, id, className, allowCreate = true, onPicked, autoOpen, onClose }) {
1004
1110
  const client = (0, import_react4.useOptionalRecordsClient)();
1005
1111
  const [rows, setRows] = (0, import_react3.useState)(null);
1006
1112
  const [words, setWords] = (0, import_react3.useState)({});
1007
1113
  const [refusal2, setRefusal] = (0, import_react3.useState)(null);
1008
1114
  const [search, setSearch] = (0, import_react3.useState)("");
1009
- const [open, setOpen] = (0, import_react3.useState)(false);
1115
+ const [open, setOpenState] = (0, import_react3.useState)(autoOpen === true);
1116
+ const onCloseRef = (0, import_react3.useRef)(onClose);
1117
+ onCloseRef.current = onClose;
1118
+ const setOpen = (0, import_react3.useCallback)((next) => {
1119
+ setOpenState(next);
1120
+ if (!next) onCloseRef.current?.();
1121
+ }, []);
1010
1122
  const [target, setTarget] = (0, import_react3.useState)(null);
1011
1123
  const [targetRefusal, setTargetRefusal] = (0, import_react3.useState)(null);
1012
1124
  const [creating, setCreating] = (0, import_react3.useState)(false);
@@ -1138,22 +1250,28 @@ function RelationPicker({ field, value, onChange, disabled, id, className, allow
1138
1250
  };
1139
1251
  const apart = tellApart(rows ?? [], titleOf);
1140
1252
  const visible = (0, import_records3.rankTypedMatches)(rows ?? [], search, (row) => [titleOf(row.id), apart.get(row.id) ?? ""]);
1253
+ const anchored = autoOpen === true;
1254
+ const cellChips = picked.map((recordId) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1255
+ RecordChip,
1256
+ {
1257
+ title: titleOf(recordId),
1258
+ ...disabled || anchored ? {} : { onRemove: () => toggle(recordId) }
1259
+ },
1260
+ recordId
1261
+ ));
1141
1262
  return (
1142
1263
  // Never wider than where it is drawn (a grid cell beside a sticky column: B3-07) — chips truncate, the button wraps.
1143
1264
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: (0, import_design_system4.cn)("flex min-w-0 max-w-full flex-wrap items-center gap-1", className), children: [
1144
- picked.map((recordId) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1145
- RecordChip,
1146
- {
1147
- title: titleOf(recordId),
1148
- ...disabled ? {} : { onRemove: () => toggle(recordId) }
1149
- },
1150
- recordId
1151
- )),
1265
+ anchored && !disabled ? null : cellChips,
1152
1266
  disabled ? null : (
1153
1267
  // THE ONE "PICK OR ADD" BUILDER (PickOrAdd.tsx) — the choice editor draws the same one.
1268
+ // In a grid cell it hangs from the cell's own chips; what is picked heads the list with its ×.
1154
1269
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1155
1270
  PickOrAdd,
1156
1271
  {
1272
+ anchored,
1273
+ multi: field.multi,
1274
+ chips: picked.map((recordId) => ({ id: recordId, label: titleOf(recordId), onRemove: () => toggle(recordId) })),
1157
1275
  triggerId: id,
1158
1276
  triggerLabel: `Pick for ${fieldName(field)}`,
1159
1277
  triggerText: picked.length === 0 ? "Pick" : "Change",
@@ -1240,7 +1358,8 @@ function RelationPicker({ field, value, onChange, disabled, id, className, allow
1240
1358
  }
1241
1359
  }
1242
1360
  )
1243
- ) : null
1361
+ ) : null,
1362
+ children: anchored ? cellChips : null
1244
1363
  }
1245
1364
  )
1246
1365
  )
@@ -2402,16 +2521,6 @@ function PersonPicker({ field, value, onChange, disabled, id, className, autoOpe
2402
2521
  };
2403
2522
  }, [client, personTable, picked.length, people.length]);
2404
2523
  const listed = (0, import_records7.rankTypedMatches)(members ?? [], search, memberName);
2405
- const keys = useListKeys({
2406
- count: listed.length,
2407
- resetOn: search,
2408
- onPick: (index) => {
2409
- const member = listed[index];
2410
- if (member && !busy) void pickRef.current?.(member);
2411
- },
2412
- onClose: () => setOpen(false)
2413
- });
2414
- const pickRef = (0, import_react17.useRef)(null);
2415
2524
  if (!membersPort || !client) {
2416
2525
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: (0, import_design_system10.cn)("text-xs text-muted-foreground", className), children: NO_MEMBERS_REASON });
2417
2526
  }
@@ -2450,7 +2559,6 @@ function PersonPicker({ field, value, onChange, disabled, id, className, autoOpe
2450
2559
  else next.add(resolved.data);
2451
2560
  onChange(next.size === 0 ? null : [...next]);
2452
2561
  };
2453
- pickRef.current = pick;
2454
2562
  const drop = (recordId) => {
2455
2563
  const next = picked.filter((id2) => id2 !== recordId);
2456
2564
  onChange(next.length === 0 ? null : field.multi ? next : next[0] ?? null);
@@ -2459,79 +2567,56 @@ function PersonPicker({ field, value, onChange, disabled, id, className, autoOpe
2459
2567
  const pickedUserIds = new Set(
2460
2568
  picked.map((recordId) => people.find((p) => p.id === recordId)?.userId).filter(Boolean)
2461
2569
  );
2570
+ const anchored = autoOpen === true;
2571
+ const cellChips = picked.map((recordId) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2572
+ RecordChip,
2573
+ {
2574
+ title: nameOfRecord(recordId),
2575
+ ...disabled || anchored ? {} : { onRemove: () => drop(recordId) }
2576
+ },
2577
+ recordId
2578
+ ));
2462
2579
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: (0, import_design_system10.cn)("flex flex-wrap items-center gap-1", className), children: [
2463
- picked.map((recordId) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2464
- RecordChip,
2465
- {
2466
- title: nameOfRecord(recordId),
2467
- ...disabled ? {} : { onRemove: () => drop(recordId) }
2468
- },
2469
- recordId
2470
- )),
2471
- disabled ? null : /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_design_system10.Popover, { open, onOpenChange: setOpen, children: [
2472
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_design_system10.PopoverTrigger, { asChild: true, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2473
- import_design_system10.Button,
2474
- {
2475
- id,
2476
- type: "button",
2477
- size: "sm",
2478
- variant: "outline",
2479
- "aria-label": `Pick a person for ${fieldName(field)}`,
2480
- children: picked.length === 0 ? "Pick a person" : "Change"
2481
- }
2482
- ) }),
2483
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
2484
- import_design_system10.PopoverContent,
2580
+ anchored && !disabled ? null : cellChips,
2581
+ disabled ? null : (
2582
+ // THE ONE "PICK OR ADD" BUILDER (lane CELL-EDITORS, 2026-10-06): the person list used to carry
2583
+ // its own copy of the list's layout and keys; it now draws the same rows, ticks, highlight and
2584
+ // head chips as every other picker, and in a grid cell hangs from the cell.
2585
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2586
+ PickOrAdd,
2485
2587
  {
2486
- sizing: "content",
2487
- className: "p-1",
2488
- align: "start",
2489
- "data-matrx-cell-editor": "",
2490
- onKeyDown: (event) => event.stopPropagation(),
2491
- children: [
2492
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2493
- import_design_system10.BasicInput,
2494
- {
2495
- value: search,
2496
- onChange: (e) => setSearch(e.target.value),
2497
- onKeyDown: keys.onKeyDown,
2498
- autoFocus: true,
2499
- autoComplete: "off",
2500
- "aria-label": `Search people for ${fieldName(field)}`,
2501
- placeholder: "Search people",
2502
- className: "mb-1 h-7 text-xs"
2503
- }
2504
- ),
2588
+ anchored,
2589
+ multi: field.multi,
2590
+ triggerId: id,
2591
+ triggerLabel: `Pick a person for ${fieldName(field)}`,
2592
+ triggerText: picked.length === 0 ? "Pick a person" : "Change",
2593
+ open,
2594
+ onOpenChange: setOpen,
2595
+ search,
2596
+ onSearch: setSearch,
2597
+ chips: picked.map((recordId) => ({ id: recordId, label: nameOfRecord(recordId), onRemove: () => drop(recordId) })),
2598
+ items: visible.map((member) => ({
2599
+ id: member.userId,
2600
+ label: memberName(member),
2601
+ // A name and an email with nothing between them read as one run-on string (verdict,
2602
+ // 19 September). They are two facts, so they are drawn as two.
2603
+ detail: member.email && member.email !== memberName(member) ? member.email : null,
2604
+ picked: pickedUserIds.has(member.userId),
2605
+ disabled: busy
2606
+ })),
2607
+ onPick: (userId) => {
2608
+ const member = visible.find((m) => m.userId === userId);
2609
+ if (member && !busy) void pick(member);
2610
+ },
2611
+ notice: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
2505
2612
  error ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(RefusalLine, { error, className: "p-2" }) : null,
2506
- members === null ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "p-2 text-xs text-muted-foreground", children: "Reading the members\u2026" }) : null,
2507
- /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_design_system10.ScrollArea, { className: "max-h-56", children: [
2508
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("ul", { role: "listbox", "aria-label": `People for ${fieldName(field)}`, children: visible.map((member, index) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
2509
- "button",
2510
- {
2511
- type: "button",
2512
- disabled: busy,
2513
- "data-pick-highlighted": index === keys.highlight ? "" : void 0,
2514
- onClick: () => void pick(member),
2515
- className: (0, import_design_system10.cn)(
2516
- "w-full truncate rounded px-2 py-1 text-left text-xs hover:bg-muted",
2517
- pickedUserIds.has(member.userId) ? "bg-accent text-accent-foreground" : "",
2518
- index === keys.highlight ? "ring-1 ring-ring" : ""
2519
- ),
2520
- children: [
2521
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { children: memberName(member) }),
2522
- member.email && member.email !== memberName(member) ? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { className: "ml-1.5 opacity-60", children: [
2523
- "\xB7 ",
2524
- member.email
2525
- ] }) : null
2526
- ]
2527
- }
2528
- ) }, member.userId)) }),
2529
- members !== null && visible.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "p-2 text-xs text-muted-foreground", children: members.length === 0 ? "This organization has no other members yet, so there is nobody to assign." : "Nobody in this organization matches." }) : null
2530
- ] })
2531
- ]
2613
+ members === null ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "p-2 text-xs text-muted-foreground", children: "Reading the members\u2026" }) : null
2614
+ ] }),
2615
+ empty: members !== null ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "p-2 text-xs text-muted-foreground", children: members.length === 0 ? "This organization has no other members yet, so there is nobody to assign." : "Nobody in this organization matches." }) : null,
2616
+ children: anchored ? cellChips : null
2532
2617
  }
2533
2618
  )
2534
- ] })
2619
+ )
2535
2620
  ] });
2536
2621
  }
2537
2622
 
@@ -3320,7 +3405,7 @@ function FieldControl({ field, value, onChange, disabled, id, autoOpen, onClose,
3320
3405
  }
3321
3406
  return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(RelationPicker, { field, value, onChange, disabled, id: inputId });
3322
3407
  case "relation":
3323
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(RelationPicker, { field, value, onChange, disabled, id: inputId, onPicked: onCommit });
3408
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(RelationPicker, { field, value, onChange, disabled, id: inputId, onPicked: onCommit, autoOpen, onClose });
3324
3409
  default:
3325
3410
  if (typeof value === "boolean" || field.config?.["kind"] === "boolean") {
3326
3411
  return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
@@ -3697,7 +3782,9 @@ function OpenChoicePicker({
3697
3782
  });
3698
3783
  };
3699
3784
  const offList = q !== "" && decided.decision !== "chosen" && decided.decision !== "empty";
3700
- const foot = offList && decided.decision === "ask" ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3785
+ const [askingFor, setAskingFor] = (0, import_react24.useState)(null);
3786
+ const asks = offList && decided.decision === "ask" && (visible.length === 0 || askingFor === q);
3787
+ const foot = asks ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3701
3788
  ChoiceAskButtons,
3702
3789
  {
3703
3790
  field,
@@ -3713,17 +3800,36 @@ function OpenChoicePicker({
3713
3800
  }
3714
3801
  }
3715
3802
  ) : null;
3716
- const add = offList && decided.decision === "add" ? { label: `Add \u201C${q}\u201D to the choices`, onAdd: addTyped, disabled: adding } : offList && decided.decision === "keep_as_typed" ? { label: `Keep \u201C${q}\u201D as typed`, onAdd: () => choose(q) } : null;
3803
+ const add = offList && decided.decision === "ask" && !asks ? { label: `Add \u201C${q}\u201D\u2026`, onAdd: () => setAskingFor(q) } : offList && decided.decision === "add" ? { label: `Add \u201C${q}\u201D to the choices`, onAdd: addTyped, disabled: adding } : offList && decided.decision === "keep_as_typed" ? { label: `Keep \u201C${q}\u201D as typed`, onAdd: () => choose(q) } : null;
3804
+ const pickedOptions = current.map((v) => options.find((o) => (0, import_records10.isTheChosen)(o, v)));
3805
+ const cellChips = pickedWords.map((word, i) => {
3806
+ const chip = chipOf(pickedOptions[i], word);
3807
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_design_system14.Badge, { variant: "outline", className: (0, import_design_system14.cn)("text-[11px]", chip.className), "data-records-choice-chip": "", children: word }, word);
3808
+ });
3809
+ const headChips = pickedWords.map((word, i) => {
3810
+ const option = pickedOptions[i];
3811
+ return {
3812
+ id: `${word}-${i}`,
3813
+ label: word,
3814
+ chip: chipOf(option, word).className,
3815
+ onRemove: () => {
3816
+ if (field.multi) choose(option ? (0, import_records10.optionKey)(option) : String(current[i]));
3817
+ else onChange(null);
3818
+ }
3819
+ };
3820
+ });
3821
+ const anchored = autoOpen === true;
3717
3822
  return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { id, className: "flex flex-wrap items-center gap-1", children: [
3718
- pickedWords.map((word, i) => {
3719
- const chip = chipOf(options.find((o) => (0, import_records10.isTheChosen)(o, current[i])), word);
3720
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_design_system14.Badge, { variant: "outline", className: (0, import_design_system14.cn)("text-[11px]", chip.className), "data-records-choice-chip": "", children: word }, word);
3721
- }),
3722
- disabled ? null : (
3823
+ anchored || disabled ? null : cellChips,
3824
+ disabled ? cellChips : (
3723
3825
  // THE ONE "PICK OR ADD" BUILDER (PickOrAdd.tsx) — the relation picker draws the same one.
3826
+ // In a grid cell it hangs from the cell's own chips; no button is drawn in the cell.
3724
3827
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3725
3828
  PickOrAdd,
3726
3829
  {
3830
+ anchored,
3831
+ chips: headChips,
3832
+ multi: field.multi,
3727
3833
  triggerLabel: `Pick for ${fieldName(field)}`,
3728
3834
  triggerText: pickedWords.length === 0 ? "Choose" : "Change",
3729
3835
  open,
@@ -3732,6 +3838,7 @@ function OpenChoicePicker({
3732
3838
  onSearch: (next) => {
3733
3839
  setSearch(next);
3734
3840
  setAddRefused(null);
3841
+ setAskingFor(null);
3735
3842
  },
3736
3843
  items: visible.map((option) => ({
3737
3844
  id: option.id,
@@ -3746,14 +3853,15 @@ function OpenChoicePicker({
3746
3853
  empty: q === "" ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { className: "p-2 text-xs text-muted-foreground", children: "No choices yet \u2014 type one to add it." }) : null,
3747
3854
  add,
3748
3855
  foot,
3749
- ...offList && decided.decision === "ask" && !adding ? { onEnterWithNoMatch: addTyped } : {},
3856
+ ...asks && !adding ? { onEnterWithNoMatch: addTyped } : {},
3750
3857
  footNote: addRefused ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(RefusalLine, { error: addRefused }) : offList && decided.decision === "not_a_choice" ? (
3751
3858
  // never_add on a column that takes only its choices: said, never a silent nothing.
3752
3859
  /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
3753
3860
  /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { "data-records-typed-refusal": "", className: "p-2 text-xs text-muted-foreground", children: notAChoiceSentence(field, [q]) }),
3754
3861
  autoOpen ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { "data-records-choice-ask": "", className: "px-2 pb-1", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(import_design_system14.Button, { type: "button", size: "sm", variant: "ghost", className: "h-6 px-2 text-xs", onClick: () => setOpen(false), children: "Cancel" }) }) : null
3755
3862
  ] })
3756
- ) : null
3863
+ ) : null,
3864
+ children: anchored ? cellChips : null
3757
3865
  }
3758
3866
  )
3759
3867
  )