@proteos/ui 0.54.1 → 0.56.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.
Files changed (33) hide show
  1. package/dist/index.cjs +679 -321
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +84 -9
  4. package/dist/index.d.ts +84 -9
  5. package/dist/index.js +677 -324
  6. package/dist/index.js.map +1 -1
  7. package/package.json +4 -4
  8. package/src/components/badge/badge.stories.tsx +37 -1
  9. package/src/components/badge/badge.test.tsx +39 -2
  10. package/src/components/badge/badge.tsx +65 -1
  11. package/src/components/badge/index.ts +2 -2
  12. package/src/components/badge-group/badge-group.stories.tsx +75 -0
  13. package/src/components/badge-group/badge-group.test.tsx +121 -0
  14. package/src/components/badge-group/badge-group.tsx +208 -0
  15. package/src/components/badge-group/index.ts +7 -0
  16. package/src/components/badge-multi-select/badge-multi-select.stories.tsx +25 -0
  17. package/src/components/badge-multi-select/badge-multi-select.test.tsx +110 -1
  18. package/src/components/badge-multi-select/badge-multi-select.tsx +190 -128
  19. package/src/components/data-table/data-table.stories.tsx +53 -2
  20. package/src/components/data-table/data-table.tsx +45 -25
  21. package/src/components/field/listbox-check.tsx +44 -0
  22. package/src/components/field/use-chip-row-wrap.ts +45 -0
  23. package/src/components/multi-select/multi-select.tsx +8 -39
  24. package/src/components/record-filter/chip-value.tsx +6 -5
  25. package/src/components/record-filter/filter-value-input.tsx +11 -9
  26. package/src/components/record-filter/operators.test.ts +67 -12
  27. package/src/components/record-filter/operators.ts +43 -2
  28. package/src/components/record-filter/types.ts +6 -1
  29. package/src/components/table/table.test.tsx +88 -9
  30. package/src/components/table/table.tsx +139 -13
  31. package/src/components/tag-input/tag-input.test.tsx +67 -1
  32. package/src/components/tag-input/tag-input.tsx +98 -64
  33. package/src/index.ts +1 -0
package/dist/index.cjs CHANGED
@@ -873,6 +873,7 @@ function Badge({
873
873
  hasDot,
874
874
  color,
875
875
  icon,
876
+ actions,
876
877
  className,
877
878
  style,
878
879
  children,
@@ -894,11 +895,249 @@ function Badge({
894
895
  ),
895
896
  children: [
896
897
  Icon ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { "aria-hidden": true, className: chunkMWAXK66H_cjs.cn("size-3 shrink-0", ICON_TONE[register2]) }) : null,
897
- children
898
+ children,
899
+ actions ? (
900
+ // Pulled flush with the trailing edge so the chip still reads
901
+ // symmetric around its leading dot / icon.
902
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex items-center shrink-0 -mr-1", children: actions })
903
+ ) : null
898
904
  ]
899
905
  }
900
906
  );
901
907
  }
908
+ function BadgeAction({ label, onPress, isDisabled = false, children }) {
909
+ return /* @__PURE__ */ jsxRuntime.jsx(
910
+ "button",
911
+ {
912
+ type: "button",
913
+ "aria-label": label,
914
+ disabled: isDisabled,
915
+ onClick: (event) => {
916
+ event.stopPropagation();
917
+ onPress();
918
+ },
919
+ className: chunkMWAXK66H_cjs.cn(
920
+ "inline-flex items-center justify-center shrink-0",
921
+ "size-3.5 rounded-full",
922
+ "text-current/70 hover:text-current hover:bg-current/10",
923
+ "outline-none cursor-pointer",
924
+ "focus-visible:shadow-[var(--shadow-focus)]",
925
+ "disabled:opacity-45 disabled:cursor-not-allowed"
926
+ ),
927
+ children: children ?? /* @__PURE__ */ jsxRuntime.jsx("svg", { "aria-hidden": true, viewBox: "0 0 10 10", className: "size-2.5", fill: "none", children: /* @__PURE__ */ jsxRuntime.jsx(
928
+ "path",
929
+ {
930
+ d: "M2 2l6 6M8 2l-6 6",
931
+ stroke: "currentColor",
932
+ strokeWidth: "1.4",
933
+ strokeLinecap: "round"
934
+ }
935
+ ) })
936
+ }
937
+ );
938
+ }
939
+ function TooltipTrigger({
940
+ delay = 500,
941
+ closeDelay = 0,
942
+ ...props
943
+ }) {
944
+ return /* @__PURE__ */ jsxRuntime.jsx(reactAriaComponents.TooltipTrigger, { delay, closeDelay, ...props });
945
+ }
946
+ function Tooltip({ className, offset = 6, ...props }) {
947
+ return /* @__PURE__ */ jsxRuntime.jsx(
948
+ reactAriaComponents.Tooltip,
949
+ {
950
+ ...props,
951
+ offset,
952
+ className: (renderProps) => chunkMWAXK66H_cjs.cn(
953
+ "z-50 max-w-xs",
954
+ "rounded-md border border-rule bg-surface shadow-[var(--shadow-raised)]",
955
+ "px-2.5 py-1.5",
956
+ "font-mono text-[11.5px] text-ink-2",
957
+ "entering:animate-in entering:fade-in-0 entering:slide-in-from-top-1 entering:duration-150",
958
+ "exiting:animate-out exiting:fade-out-0 exiting:duration-100",
959
+ typeof className === "function" ? className(renderProps) : className
960
+ )
961
+ }
962
+ );
963
+ }
964
+ var badgeGroupStyles = tailwindVariants.tv({
965
+ base: "flex flex-nowrap items-center gap-1.5 w-full min-w-0",
966
+ variants: {
967
+ // The measuring pass renders every chip plus the overflow chip so their
968
+ // widths can be read; it is hidden so that pass never paints.
969
+ isMeasuring: { true: "invisible", false: "" }
970
+ },
971
+ defaultVariants: { isMeasuring: false }
972
+ });
973
+ function fitBadgeCount(metrics, available) {
974
+ const { itemWidths, overflowWidth, gap } = metrics;
975
+ const total = itemWidths.length;
976
+ if (available <= 0) return total;
977
+ let fullWidth = 0;
978
+ for (let index = 0; index < total; index += 1) {
979
+ fullWidth += (itemWidths[index] ?? 0) + (index > 0 ? gap : 0);
980
+ }
981
+ if (fullWidth <= available) return total;
982
+ let used = 0;
983
+ let count = 0;
984
+ for (let index = 0; index < total; index += 1) {
985
+ const next = used + (itemWidths[index] ?? 0) + gap;
986
+ if (next + overflowWidth > available) break;
987
+ used = next;
988
+ count = index + 1;
989
+ }
990
+ return count;
991
+ }
992
+ var badgeGroupOverflowStyles = tailwindVariants.tv({
993
+ base: [
994
+ badgeStyles({ variant: "neutral", shape: "rounded", hasDot: false }),
995
+ // badgeStyles' register still materializes an empty ::before — hide it,
996
+ // as Badge itself does when no dot should show.
997
+ "before:hidden shrink-0 cursor-pointer whitespace-nowrap",
998
+ "hover:bg-surface-2 hover:text-ink",
999
+ "outline-none focus-visible:shadow-[var(--shadow-focus)]"
1000
+ ]
1001
+ });
1002
+ function BadgeGroup({ children, className, ...props }) {
1003
+ const items = react.Children.toArray(children);
1004
+ const signature = items.map((item, index) => react.isValidElement(item) ? String(item.key ?? index) : String(item)).join("");
1005
+ const rootRef = react.useRef(null);
1006
+ const metricsRef = react.useRef(null);
1007
+ const [layout, setLayout] = react.useState({ signature, visibleCount: null });
1008
+ if (layout.signature !== signature) {
1009
+ metricsRef.current = null;
1010
+ setLayout({ signature, visibleCount: null });
1011
+ }
1012
+ const measuring = layout.signature !== signature || layout.visibleCount === null;
1013
+ const visibleCount = measuring ? items.length : layout.visibleCount ?? items.length;
1014
+ const shown = items.slice(0, visibleCount);
1015
+ const hidden = measuring ? items.length : items.length - visibleCount;
1016
+ react.useLayoutEffect(() => {
1017
+ if (!measuring) return;
1018
+ const root = rootRef.current;
1019
+ if (!root) return;
1020
+ const nodes = Array.from(root.children);
1021
+ const metrics = {
1022
+ itemWidths: nodes.slice(0, items.length).map((node) => node.getBoundingClientRect().width),
1023
+ overflowWidth: nodes[items.length]?.getBoundingClientRect().width ?? 0,
1024
+ gap: Number.parseFloat(getComputedStyle(root).columnGap) || 0
1025
+ };
1026
+ metricsRef.current = metrics;
1027
+ setLayout({ signature, visibleCount: fitBadgeCount(metrics, root.clientWidth) });
1028
+ }, [measuring, signature, items.length]);
1029
+ react.useEffect(() => {
1030
+ const root = rootRef.current;
1031
+ if (!root || typeof ResizeObserver === "undefined") return;
1032
+ const observer = new ResizeObserver(() => {
1033
+ const metrics = metricsRef.current;
1034
+ if (!metrics) return;
1035
+ const next = fitBadgeCount(metrics, root.clientWidth);
1036
+ setLayout((prev) => prev.visibleCount === next ? prev : { ...prev, visibleCount: next });
1037
+ });
1038
+ observer.observe(root);
1039
+ return () => observer.disconnect();
1040
+ }, []);
1041
+ react.useEffect(() => {
1042
+ const fonts = typeof document === "undefined" ? void 0 : document.fonts;
1043
+ if (!fonts || fonts.status === "loaded") return;
1044
+ let cancelled = false;
1045
+ void fonts.ready.then(() => {
1046
+ if (cancelled) return;
1047
+ metricsRef.current = null;
1048
+ setLayout((prev) => ({ ...prev, visibleCount: null }));
1049
+ });
1050
+ return () => {
1051
+ cancelled = true;
1052
+ };
1053
+ }, []);
1054
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1055
+ "div",
1056
+ {
1057
+ ...props,
1058
+ ref: rootRef,
1059
+ className: chunkMWAXK66H_cjs.cn(badgeGroupStyles({ isMeasuring: measuring }), className),
1060
+ children: [
1061
+ shown.map((item, index) => /* @__PURE__ */ jsxRuntime.jsx(
1062
+ "span",
1063
+ {
1064
+ className: "inline-flex shrink-0",
1065
+ children: item
1066
+ },
1067
+ (react.isValidElement(item) ? item.key : null) ?? index
1068
+ )),
1069
+ hidden > 0 ? (
1070
+ // The span swallows the click before it reaches a clickable host row
1071
+ // (React propagation), so opening the overflow never opens the record.
1072
+ // biome-ignore lint/a11y/noStaticElementInteractions: click fence only — the button inside is the interactive element
1073
+ // biome-ignore lint/a11y/useKeyWithClickEvents: keyboard handling lives on the button inside
1074
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex shrink-0", onClick: (event) => event.stopPropagation(), children: /* @__PURE__ */ jsxRuntime.jsxs(Popover, { children: [
1075
+ /* @__PURE__ */ jsxRuntime.jsxs(TooltipTrigger, { children: [
1076
+ /* @__PURE__ */ jsxRuntime.jsxs(reactAriaComponents.Button, { className: badgeGroupOverflowStyles(), children: [
1077
+ "+",
1078
+ hidden,
1079
+ " more"
1080
+ ] }),
1081
+ /* @__PURE__ */ jsxRuntime.jsx(Tooltip, { className: "max-w-sm", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex flex-wrap items-center gap-1.5", children: items }) })
1082
+ ] }),
1083
+ /* @__PURE__ */ jsxRuntime.jsx(PopoverContent, { placement: "bottom start", panelClassName: "max-w-sm", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap items-center gap-1.5 p-2.5", children: items }) })
1084
+ ] }) })
1085
+ ) : null
1086
+ ]
1087
+ }
1088
+ );
1089
+ }
1090
+ var listboxCheckStyles = tailwindVariants.tv({
1091
+ slots: {
1092
+ box: [
1093
+ "relative inline-flex items-center justify-center shrink-0",
1094
+ "size-[16px] rounded-[3px] border border-ink-4 bg-surface",
1095
+ "transition-[background-color,border-color] duration-150",
1096
+ "group-data-[selected]:bg-ink group-data-[selected]:border-ink"
1097
+ ],
1098
+ tick: [
1099
+ "size-3 text-surface transition-opacity duration-150",
1100
+ "opacity-0 group-data-[selected]:opacity-100"
1101
+ ]
1102
+ }
1103
+ });
1104
+ function ListboxCheck() {
1105
+ const { box, tick } = listboxCheckStyles();
1106
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": true, className: box(), children: /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 12 12", className: tick(), "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx(
1107
+ "path",
1108
+ {
1109
+ d: "M2.5 6.25 5 8.75 9.5 3.75",
1110
+ stroke: "currentColor",
1111
+ strokeWidth: "1.6",
1112
+ strokeLinecap: "round",
1113
+ strokeLinejoin: "round",
1114
+ fill: "none"
1115
+ }
1116
+ ) }) });
1117
+ }
1118
+ function useChipRowWrap(ref, isEnabled, signature) {
1119
+ const [hasWrapped, setHasWrapped] = react.useState(false);
1120
+ react.useLayoutEffect(() => {
1121
+ if (!isEnabled) return;
1122
+ const element = ref.current;
1123
+ if (!element) return;
1124
+ const measure = () => {
1125
+ const children = Array.from(element.children);
1126
+ const first = children[0]?.getBoundingClientRect();
1127
+ if (!first || first.height === 0) {
1128
+ setHasWrapped(false);
1129
+ return;
1130
+ }
1131
+ setHasWrapped(children.some((child) => child.getBoundingClientRect().top >= first.bottom - 1));
1132
+ };
1133
+ measure();
1134
+ if (typeof ResizeObserver === "undefined") return;
1135
+ const observer = new ResizeObserver(measure);
1136
+ observer.observe(element);
1137
+ return () => observer.disconnect();
1138
+ }, [ref, isEnabled, signature]);
1139
+ return hasWrapped;
1140
+ }
902
1141
  function badgeChrome(option) {
903
1142
  return {
904
1143
  ...option.variant ? { variant: option.variant } : {},
@@ -922,18 +1161,51 @@ function BadgeMultiSelect({
922
1161
  size = "md",
923
1162
  appearance,
924
1163
  autoFocus = false,
1164
+ defaultOpen = false,
1165
+ "aria-label": ariaLabel,
925
1166
  className
926
1167
  }) {
1168
+ const isCell = appearance === "cell";
927
1169
  const styles4 = fieldControlStyles({ size, invalid: isInvalid, appearance });
928
- const focusWithin = appearance === "cell" ? "focus-within:border-accent focus-within:shadow-none" : "focus-within:border-accent focus-within:shadow-[var(--shadow-focus)]";
929
- const minHeight = appearance === "cell" ? "min-h-[29px]" : fieldMinHeight[size];
1170
+ const focusWithin = isCell ? "focus-within:border-accent focus-within:shadow-none" : "focus-within:border-accent focus-within:shadow-[var(--shadow-focus)]";
930
1171
  const [isOpen, setIsOpen] = react.useState(false);
931
1172
  const triggerRef = react.useRef(null);
1173
+ const chipsRef = react.useRef(null);
1174
+ const popoverRef = react.useRef(null);
1175
+ const closedByPointerRef = react.useRef(false);
932
1176
  react.useEffect(() => {
933
1177
  if (autoFocus) triggerRef.current?.focus();
1178
+ if (defaultOpen && !isDisabled) setIsOpen(true);
934
1179
  }, []);
1180
+ const wasOpenRef = react.useRef(false);
1181
+ react.useEffect(() => {
1182
+ if (wasOpenRef.current && !isOpen && !closedByPointerRef.current) {
1183
+ const active = document.activeElement;
1184
+ const strandedInList = !active || active === document.body || !!active.closest('[data-rac][role="listbox"]');
1185
+ if (strandedInList) triggerRef.current?.focus();
1186
+ }
1187
+ wasOpenRef.current = isOpen;
1188
+ closedByPointerRef.current = false;
1189
+ }, [isOpen]);
935
1190
  const selectedSet = new Set(selectedKeys);
936
1191
  const selectedOptions = options.filter((o) => selectedSet.has(o.id));
1192
+ const hasWrapped = useChipRowWrap(
1193
+ chipsRef,
1194
+ isCell,
1195
+ JSON.stringify(selectedOptions.map((o) => o.id))
1196
+ );
1197
+ react.useEffect(() => {
1198
+ if (!isOpen) return;
1199
+ const onPointerDown = (event) => {
1200
+ const target = event.target;
1201
+ if (!(target instanceof Node)) return;
1202
+ if (triggerRef.current?.contains(target) || popoverRef.current?.contains(target)) return;
1203
+ closedByPointerRef.current = true;
1204
+ setIsOpen(false);
1205
+ };
1206
+ document.addEventListener("pointerdown", onPointerDown, true);
1207
+ return () => document.removeEventListener("pointerdown", onPointerDown, true);
1208
+ }, [isOpen]);
937
1209
  const listboxOptions = hideSelectedOptions ? options.filter((o) => !selectedSet.has(o.id)) : options;
938
1210
  const remove = (id) => {
939
1211
  const next = new Set(selectedSet);
@@ -945,6 +1217,12 @@ function BadgeMultiSelect({
945
1217
  if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") {
946
1218
  e.preventDefault();
947
1219
  setIsOpen(true);
1220
+ return;
1221
+ }
1222
+ if (e.key === "Escape" && isOpen) {
1223
+ e.preventDefault();
1224
+ e.stopPropagation();
1225
+ setIsOpen(false);
948
1226
  }
949
1227
  };
950
1228
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: chunkMWAXK66H_cjs.cn("grid grid-cols-1 gap-1.5", className), children: [
@@ -954,6 +1232,7 @@ function BadgeMultiSelect({
954
1232
  {
955
1233
  ref: triggerRef,
956
1234
  role: "combobox",
1235
+ "aria-label": ariaLabel,
957
1236
  "aria-expanded": isOpen,
958
1237
  "aria-haspopup": "listbox",
959
1238
  "aria-disabled": isDisabled || void 0,
@@ -965,34 +1244,53 @@ function BadgeMultiSelect({
965
1244
  "data-disabled": isDisabled || void 0,
966
1245
  className: chunkMWAXK66H_cjs.cn(
967
1246
  styles4.box(),
968
- "flex flex-wrap items-center gap-1.5 cursor-pointer",
969
- minHeight,
970
- fieldChipPadding[size],
971
- "pr-2",
1247
+ "flex items-stretch cursor-pointer",
1248
+ // Cell editor: one table row (29px under a 32px row) while the chips
1249
+ // fit on a line; two rows (64px), popping out of the row, once they
1250
+ // wrap — the chips then scroll inside. Field mode grows with its
1251
+ // content like every other chip field.
1252
+ isCell ? hasWrapped ? "h-[64px] p-1" : "h-[29px] px-1 py-0" : [fieldChipPadding[size], fieldMinHeight[size]],
972
1253
  focusWithin
973
1254
  ),
974
1255
  children: [
975
- selectedOptions.length === 0 ? (
976
- // No vertical padding — wrapper's p-1.5 + min-h-[size] govern the
977
- // overall height so it matches the Input/Select trigger at the same
978
- // size.
979
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: chunkMWAXK66H_cjs.cn("text-ink-4 font-sans px-1", fieldTextStyles[size]), children: placeholder })
980
- ) : selectedOptions.map((o) => /* @__PURE__ */ jsxRuntime.jsx(
981
- RemovableBadge,
1256
+ /* @__PURE__ */ jsxRuntime.jsx(
1257
+ "div",
982
1258
  {
983
- ...badgeChrome(o),
984
- onRemove: () => remove(o.id),
985
- isDisabled,
986
- children: o.label
987
- },
988
- o.id
989
- )),
990
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-auto pr-1", children: /* @__PURE__ */ jsxRuntime.jsx(FieldCaret, {}) })
1259
+ ref: chipsRef,
1260
+ className: chunkMWAXK66H_cjs.cn(
1261
+ "flex flex-1 flex-wrap items-center gap-1.5 min-w-0",
1262
+ isCell && (hasWrapped ? "content-start overflow-y-auto" : "content-center overflow-hidden")
1263
+ ),
1264
+ children: selectedOptions.length === 0 ? (
1265
+ // No vertical padding — wrapper's p-1.5 + min-h-[size] govern the
1266
+ // overall height so it matches the Input/Select trigger at the same
1267
+ // size.
1268
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: chunkMWAXK66H_cjs.cn("text-ink-4 font-sans px-1", fieldTextStyles[size]), children: placeholder })
1269
+ ) : selectedOptions.map((o) => /* @__PURE__ */ jsxRuntime.jsx(
1270
+ Badge,
1271
+ {
1272
+ ...badgeChrome(o),
1273
+ className: "shrink-0",
1274
+ actions: /* @__PURE__ */ jsxRuntime.jsx(
1275
+ BadgeAction,
1276
+ {
1277
+ label: "Remove",
1278
+ onPress: () => remove(o.id),
1279
+ isDisabled
1280
+ }
1281
+ ),
1282
+ children: o.label
1283
+ },
1284
+ o.id
1285
+ ))
1286
+ }
1287
+ ),
1288
+ isCell ? null : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex items-center pl-1.5 pr-1", children: /* @__PURE__ */ jsxRuntime.jsx(FieldCaret, {}) })
991
1289
  ]
992
1290
  }
993
1291
  ),
994
1292
  description ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: fieldHintStyles, children: description }) : null,
995
- errorMessage && appearance !== "cell" ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: fieldErrorStyles, children: errorMessage }) : null,
1293
+ errorMessage && !isCell ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: fieldErrorStyles, children: errorMessage }) : null,
996
1294
  /* @__PURE__ */ jsxRuntime.jsx(
997
1295
  reactAriaComponents.Popover,
998
1296
  {
@@ -1001,18 +1299,27 @@ function BadgeMultiSelect({
1001
1299
  triggerRef,
1002
1300
  offset: 6,
1003
1301
  placement: "bottom start",
1302
+ isNonModal: true,
1303
+ shouldCloseOnInteractOutside: (element) => !triggerRef.current?.contains(element),
1004
1304
  className: chunkMWAXK66H_cjs.cn(
1005
- "w-[var(--trigger-width)]",
1006
- "rounded-md border border-rule bg-surface p-1",
1305
+ // At least as wide as the trigger, wider when a badge needs it —
1306
+ // a narrow table column must not clip its own options.
1307
+ "min-w-[var(--trigger-width)] w-max max-w-[min(28rem,calc(100vw-2rem))]",
1308
+ // Float above modals + full-screen editors, matching Select.
1309
+ "z-[1300]",
1310
+ // The popover element itself takes focus when opened programmatically
1311
+ // (defaultOpen) — no browser outline around the whole panel.
1312
+ "outline-none rounded-md border border-rule bg-surface p-1",
1007
1313
  "shadow-[var(--shadow-raised)]",
1008
1314
  "entering:animate-in entering:fade-in-0 entering:slide-in-from-top-1",
1009
1315
  "exiting:animate-out exiting:fade-out-0 exiting:slide-out-to-top-1"
1010
1316
  ),
1011
- children: /* @__PURE__ */ jsxRuntime.jsx(
1317
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { ref: popoverRef, children: /* @__PURE__ */ jsxRuntime.jsx(
1012
1318
  reactAriaComponents.ListBox,
1013
1319
  {
1014
- "aria-label": typeof label === "string" ? label : "Options",
1320
+ "aria-label": typeof label === "string" ? label : ariaLabel ?? "Options",
1015
1321
  selectionMode: "multiple",
1322
+ escapeKeyBehavior: "none",
1016
1323
  selectedKeys: selectedSet,
1017
1324
  onSelectionChange: (keys) => {
1018
1325
  if (keys === "all") {
@@ -1021,70 +1328,32 @@ function BadgeMultiSelect({
1021
1328
  }
1022
1329
  onSelectionChange(keys);
1023
1330
  },
1331
+ autoFocus: "first",
1024
1332
  className: "outline-none max-h-72 overflow-auto",
1025
1333
  renderEmptyState: () => /* @__PURE__ */ jsxRuntime.jsx("div", { className: chunkMWAXK66H_cjs.cn("px-3 py-2", fieldHintStyles), children: "All options selected" }),
1026
- children: listboxOptions.map((option) => /* @__PURE__ */ jsxRuntime.jsx(
1334
+ children: listboxOptions.map((option) => /* @__PURE__ */ jsxRuntime.jsxs(
1027
1335
  reactAriaComponents.ListBoxItem,
1028
1336
  {
1029
1337
  id: option.id,
1030
1338
  textValue: option.label,
1031
1339
  ...option.isDisabled ? { isDisabled: true } : {},
1032
1340
  className: chunkMWAXK66H_cjs.cn(
1033
- "group flex items-center",
1034
- "rounded-[6px] px-3 py-2",
1341
+ "group flex items-center gap-2.5",
1342
+ "rounded-[6px] px-2.5 py-1.5",
1035
1343
  "cursor-pointer outline-none select-none",
1344
+ // Selection is carried by the check box, not a row fill.
1036
1345
  "data-[hovered]:bg-surface-2",
1037
1346
  "data-[focused]:bg-surface-2",
1038
- "data-[selected]:bg-surface-3",
1039
1347
  "data-[disabled]:opacity-45 data-[disabled]:cursor-not-allowed"
1040
1348
  ),
1041
- children: /* @__PURE__ */ jsxRuntime.jsx(Badge, { ...badgeChrome(option), children: option.label })
1349
+ children: [
1350
+ /* @__PURE__ */ jsxRuntime.jsx(ListboxCheck, {}),
1351
+ /* @__PURE__ */ jsxRuntime.jsx(Badge, { ...badgeChrome(option), children: option.label })
1352
+ ]
1042
1353
  },
1043
1354
  option.id
1044
1355
  ))
1045
1356
  }
1046
- )
1047
- }
1048
- )
1049
- ] });
1050
- }
1051
- function RemovableBadge({
1052
- children,
1053
- onRemove,
1054
- isDisabled,
1055
- ...chrome
1056
- }) {
1057
- return /* @__PURE__ */ jsxRuntime.jsxs(Badge, { ...badgeChrome(chrome), children: [
1058
- /* @__PURE__ */ jsxRuntime.jsx("span", { children }),
1059
- /* @__PURE__ */ jsxRuntime.jsx(
1060
- "button",
1061
- {
1062
- type: "button",
1063
- "aria-label": "Remove",
1064
- disabled: isDisabled,
1065
- onClick: (e) => {
1066
- e.stopPropagation();
1067
- onRemove();
1068
- },
1069
- className: chunkMWAXK66H_cjs.cn(
1070
- "inline-flex items-center justify-center shrink-0",
1071
- "size-3.5 rounded-full",
1072
- "text-current/70 hover:text-current hover:bg-current/10",
1073
- "outline-none cursor-pointer",
1074
- "focus-visible:shadow-[var(--shadow-focus)]",
1075
- "disabled:opacity-45 disabled:cursor-not-allowed",
1076
- // Pull the button flush with the right edge so the pill still feels
1077
- // symmetric around the leading dot.
1078
- "-mr-1"
1079
- ),
1080
- children: /* @__PURE__ */ jsxRuntime.jsx("svg", { "aria-hidden": true, viewBox: "0 0 10 10", className: "size-2.5", fill: "none", children: /* @__PURE__ */ jsxRuntime.jsx(
1081
- "path",
1082
- {
1083
- d: "M2 2l6 6M8 2l-6 6",
1084
- stroke: "currentColor",
1085
- strokeWidth: "1.4",
1086
- strokeLinecap: "round"
1087
- }
1088
1357
  ) })
1089
1358
  }
1090
1359
  )
@@ -2688,7 +2957,9 @@ var tableHeaderStyles = tailwindVariants.tv({
2688
2957
  "border-b border-rule"
2689
2958
  ],
2690
2959
  variants: {
2691
- sticky: { true: "sticky top-0 z-[2]", false: "" }
2960
+ // z-[4]: above an editing row (z-[3]) — sticky chrome stays on top of
2961
+ // rows scrolling underneath, popped-out editors included.
2962
+ sticky: { true: "sticky top-0 z-[4]", false: "" }
2692
2963
  },
2693
2964
  defaultVariants: { sticky: false }
2694
2965
  });
@@ -2753,11 +3024,57 @@ function TableHeaderCell({
2753
3024
  onRemove,
2754
3025
  ariaLabel,
2755
3026
  sticky = false,
3027
+ onResize,
3028
+ minWidth = 64,
2756
3029
  ...rest
2757
3030
  }) {
2758
3031
  const cellRef = react.useRef(null);
2759
3032
  const handleRef = react.useRef(null);
2760
3033
  const [dragState, setDragState] = react.useState({ kind: "idle" });
3034
+ const [isResizing, setIsResizing] = react.useState(false);
3035
+ const startResize = (event) => {
3036
+ if (!onResize || event.button !== 0) return;
3037
+ const cell = cellRef.current;
3038
+ if (!cell) return;
3039
+ event.preventDefault();
3040
+ event.stopPropagation();
3041
+ const handle = event.currentTarget;
3042
+ const startX = event.clientX;
3043
+ const startWidth = cell.getBoundingClientRect().width;
3044
+ if (typeof handle.setPointerCapture === "function") handle.setPointerCapture(event.pointerId);
3045
+ setIsResizing(true);
3046
+ document.body.style.cursor = "col-resize";
3047
+ let frame = 0;
3048
+ const report = (clientX) => {
3049
+ const next = Math.max(minWidth, Math.round(startWidth + clientX - startX));
3050
+ if (typeof requestAnimationFrame !== "function") {
3051
+ onResize(next);
3052
+ return;
3053
+ }
3054
+ cancelAnimationFrame(frame);
3055
+ frame = requestAnimationFrame(() => onResize(next));
3056
+ };
3057
+ const onMove = (move) => report(move.clientX);
3058
+ const onEnd = () => {
3059
+ handle.removeEventListener("pointermove", onMove);
3060
+ handle.removeEventListener("pointerup", onEnd);
3061
+ handle.removeEventListener("pointercancel", onEnd);
3062
+ document.body.style.cursor = "";
3063
+ setIsResizing(false);
3064
+ };
3065
+ handle.addEventListener("pointermove", onMove);
3066
+ handle.addEventListener("pointerup", onEnd);
3067
+ handle.addEventListener("pointercancel", onEnd);
3068
+ };
3069
+ const resizeByKey = (event) => {
3070
+ if (!onResize) return;
3071
+ const step = event.key === "ArrowRight" ? 8 : event.key === "ArrowLeft" ? -8 : 0;
3072
+ if (step === 0) return;
3073
+ event.preventDefault();
3074
+ event.stopPropagation();
3075
+ const current = cellRef.current?.getBoundingClientRect().width ?? minWidth;
3076
+ onResize(Math.max(minWidth, Math.round(current + step)));
3077
+ };
2761
3078
  react.useEffect(() => {
2762
3079
  if (!isDraggable || !columnId) return;
2763
3080
  const cell = cellRef.current;
@@ -2775,10 +3092,7 @@ function TableHeaderCell({
2775
3092
  adapter.dropTargetForElements({
2776
3093
  element: cell,
2777
3094
  canDrop: ({ source }) => isColumnDragData(source.data) && source.data.columnId !== columnId,
2778
- getData: ({ input, element }) => closestEdge.attachClosestEdge(
2779
- { ...data },
2780
- { input, element, allowedEdges: ["left", "right"] }
2781
- ),
3095
+ getData: ({ input, element }) => closestEdge.attachClosestEdge({ ...data }, { input, element, allowedEdges: ["left", "right"] }),
2782
3096
  getIsSticky: () => true,
2783
3097
  onDragEnter: ({ self }) => {
2784
3098
  const edge = closestEdge.extractClosestEdge(self.data);
@@ -2805,6 +3119,7 @@ function TableHeaderCell({
2805
3119
  const target = e.target;
2806
3120
  if (target.closest('[data-dnd-handle="true"]')) return;
2807
3121
  if (target.closest('[data-column-remove="true"]')) return;
3122
+ if (target.closest('[data-column-resize="true"]')) return;
2808
3123
  onSortToggle?.();
2809
3124
  };
2810
3125
  const handleKeyDown = (e) => {
@@ -2888,6 +3203,42 @@ function TableHeaderCell({
2888
3203
  dragState.edge === "left" ? "-left-px" : "-right-px"
2889
3204
  )
2890
3205
  }
3206
+ ) : null,
3207
+ onResize ? (
3208
+ // Sits astride the trailing separator: an 8px grab zone with a 2px
3209
+ // bar that lights up on hover, focus and while dragging. A focusable
3210
+ // separator is the ARIA pattern for a resizer (no semantic element
3211
+ // exists for it); its value is the column width when one is known.
3212
+ // biome-ignore lint/a11y/useSemanticElements: interactive vertical resizer — <hr> cannot be one
3213
+ /* @__PURE__ */ jsxRuntime.jsx(
3214
+ "span",
3215
+ {
3216
+ role: "separator",
3217
+ "aria-orientation": "vertical",
3218
+ "aria-label": `Resize column ${ariaLabel ?? "column"}`,
3219
+ "aria-valuemin": minWidth,
3220
+ "aria-valuenow": typeof width === "number" ? width : void 0,
3221
+ tabIndex: 0,
3222
+ "data-column-resize": "true",
3223
+ onPointerDown: startResize,
3224
+ onKeyDown: resizeByKey,
3225
+ onClick: (e) => e.stopPropagation(),
3226
+ className: chunkMWAXK66H_cjs.cn(
3227
+ "group/resize absolute inset-y-0 -right-1 z-10 w-2 cursor-col-resize outline-none",
3228
+ "flex justify-center"
3229
+ ),
3230
+ children: /* @__PURE__ */ jsxRuntime.jsx(
3231
+ "span",
3232
+ {
3233
+ "aria-hidden": "true",
3234
+ className: chunkMWAXK66H_cjs.cn(
3235
+ "h-full w-0.5 transition-colors",
3236
+ isResizing ? "bg-accent" : "bg-transparent group-hover/resize:bg-accent group-focus-visible/resize:bg-accent"
3237
+ )
3238
+ }
3239
+ )
3240
+ }
3241
+ )
2891
3242
  ) : null
2892
3243
  ]
2893
3244
  }
@@ -2941,12 +3292,27 @@ var tableScrollerStyles = tailwindVariants.tv({
2941
3292
  base: ["relative flex-1 min-h-0 overflow-auto", ...scrollbarChrome]
2942
3293
  });
2943
3294
  function TableScroller({ className, ref, ...props }) {
2944
- return /* @__PURE__ */ jsxRuntime.jsx("div", { role: "presentation", ref, ...props, className: chunkMWAXK66H_cjs.cn(tableScrollerStyles(), className) });
3295
+ return /* @__PURE__ */ jsxRuntime.jsx(
3296
+ "div",
3297
+ {
3298
+ role: "presentation",
3299
+ ref,
3300
+ ...props,
3301
+ className: chunkMWAXK66H_cjs.cn(tableScrollerStyles(), className)
3302
+ }
3303
+ );
2945
3304
  }
2946
3305
  var rowStripeBg = "bg-[color-mix(in_oklab,var(--color-ink)_var(--row-stripe),var(--color-bg))]";
2947
3306
  var rowHoverBg = "hover:bg-[color-mix(in_oklab,var(--color-ink)_var(--row-hover),var(--color-bg))]";
3307
+ var cellEditingRow = "has-[[data-cell-editing]]:z-[3]";
3308
+ var cellEditingCell = "has-[[data-cell-editing]]:z-[3] has-[[data-cell-editing]]:overflow-visible";
2948
3309
  var tableRowStyles = tailwindVariants.tv({
2949
- base: ["flex items-stretch", "border-b border-rule-2 last:border-b-0", "bg-bg"],
3310
+ base: [
3311
+ "relative flex items-stretch",
3312
+ "border-b border-rule-2 last:border-b-0",
3313
+ "bg-bg",
3314
+ cellEditingRow
3315
+ ],
2950
3316
  variants: {
2951
3317
  alt: {
2952
3318
  true: rowStripeBg,
@@ -2957,7 +3323,9 @@ var tableRowStyles = tailwindVariants.tv({
2957
3323
  false: rowHoverBg
2958
3324
  },
2959
3325
  absolute: {
2960
- true: "absolute left-0 right-0",
3326
+ // `top-0` pins the static position: a virtualized row must never
3327
+ // inherit a flow offset from a sibling that is in normal flow.
3328
+ true: "absolute top-0 left-0 right-0",
2961
3329
  false: ""
2962
3330
  }
2963
3331
  },
@@ -2988,11 +3356,12 @@ function TableRow({
2988
3356
  }
2989
3357
  var tableCellStyles = tailwindVariants.tv({
2990
3358
  base: [
2991
- "flex items-center shrink-0 min-w-0",
3359
+ "relative flex items-center shrink-0 min-w-0",
2992
3360
  "px-[14px]",
2993
3361
  "whitespace-nowrap overflow-hidden text-ellipsis",
2994
3362
  "font-sans text-[13px] tracking-[-0.005em]",
2995
- "text-ink-2"
3363
+ "text-ink-2",
3364
+ cellEditingCell
2996
3365
  ],
2997
3366
  variants: {
2998
3367
  align: {
@@ -3153,6 +3522,8 @@ function DataTable({
3153
3522
  enableColumnRemoval = false,
3154
3523
  onReorderColumn,
3155
3524
  onRemoveColumn,
3525
+ enableColumnResizing = false,
3526
+ onResizeColumn,
3156
3527
  variant = "panel",
3157
3528
  rowWrapper,
3158
3529
  extraRows,
@@ -3366,7 +3737,6 @@ function DataTable({
3366
3737
  const showEmpty = !query.isLoading && !query.error && tableRowCount === 0 && !query.isFetchingNextPage;
3367
3738
  const showInitialLoading = query.isLoading && tableRowCount === 0;
3368
3739
  const showError = !!query.error && tableRowCount === 0;
3369
- const showEndMarker = !query.isLoading && !query.isFetchingNextPage && !query.hasNextPage && tableRowCount > 0;
3370
3740
  const totalSize = virtualizer.getTotalSize();
3371
3741
  const virtualItems = virtualizer.getVirtualItems();
3372
3742
  const headerGroups = table.getHeaderGroups();
@@ -3377,84 +3747,108 @@ function DataTable({
3377
3747
  ...ariaLabel !== void 0 ? { "aria-label": ariaLabel } : {},
3378
3748
  variant,
3379
3749
  className: chunkMWAXK66H_cjs.cn("flex-1 min-h-0", className),
3380
- children: /* @__PURE__ */ jsxRuntime.jsx(TableScroller, { ref: scrollerRef, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col", style: { minWidth: minPanelWidth }, children: [
3381
- headerGroups.map((group) => /* @__PURE__ */ jsxRuntime.jsxs(TableHeader, { sticky: true, children: [
3382
- group.headers.map((header) => {
3383
- const meta = header.column.columnDef.meta;
3384
- const align = meta?.align ?? "left";
3385
- const sticky = meta?.sticky ?? false;
3386
- const headerSize = header.getSize() || void 0;
3387
- const headerNode = header.isPlaceholder ? null : reactTable.flexRender(header.column.columnDef.header, header.getContext());
3388
- const sorted = header.column.getIsSorted();
3389
- const direction = sorted === false ? null : sorted;
3390
- const canSort = header.column.getCanSort();
3391
- const handleSort = header.column.getToggleSortingHandler();
3392
- const ariaLabel2 = typeof header.column.columnDef.header === "string" ? header.column.columnDef.header : header.column.id;
3393
- return /* @__PURE__ */ jsxRuntime.jsx(
3394
- TableHeaderCell,
3395
- {
3396
- align,
3397
- ariaLabel: ariaLabel2,
3398
- draggable: enableColumnReordering && !!header.column.id,
3399
- ...header.column.id ? { columnId: header.column.id } : {},
3400
- ...headerSize !== void 0 ? { width: headerSize } : {},
3401
- ...sticky !== false ? { sticky } : {},
3402
- ...meta?.cellClassName ? { className: meta.cellClassName } : {},
3403
- ...canSort ? {
3404
- sortDirection: direction,
3405
- onSortToggle: () => handleSort?.({})
3406
- } : {},
3407
- ...enableColumnRemoval && onRemoveColumn && header.column.id ? { onRemove: () => onRemoveColumn(header.column.id) } : {},
3408
- children: headerNode
3409
- },
3410
- header.id
3411
- );
3412
- }),
3413
- /* @__PURE__ */ jsxRuntime.jsx(TableHeaderSpacer, {})
3414
- ] }, group.id)),
3415
- bodyBefore,
3416
- showInitialLoading ? loadingState ?? /* @__PURE__ */ jsxRuntime.jsx(DataTableSkeleton, { rowHeight }) : showError ? /* @__PURE__ */ jsxRuntime.jsx("div", { role: "rowgroup", children: errorState ?? (query.error ? /* @__PURE__ */ jsxRuntime.jsx(DataTableError, { error: query.error }) : null) }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "rowgroup", children: [
3417
- showEmpty ? emptyState ?? /* @__PURE__ */ jsxRuntime.jsx(DataTableEmpty, {}) : /* @__PURE__ */ jsxRuntime.jsx(
3418
- "div",
3419
- {
3420
- ref: listRef,
3421
- role: "presentation",
3422
- style: { height: totalSize, position: "relative", width: "100%" },
3423
- children: virtualItems.map((vRow) => {
3424
- const item = listItems[vRow.index];
3425
- if (!item) return null;
3426
- const rowStyle = {
3427
- // Items' `start` includes the virtualizer's scrollMargin
3428
- // (the list's offset below the sticky header); subtract
3429
- // it to position relative to the list container.
3430
- transform: `translateY(${vRow.start - listOffset}px)`,
3431
- height: vRow.size
3432
- };
3433
- if (item.kind === "group") {
3434
- return /* @__PURE__ */ jsxRuntime.jsx(GroupHeaderRow, { style: rowStyle, children: item.label }, `group:${item.id}`);
3435
- }
3750
+ children: /* @__PURE__ */ jsxRuntime.jsx(
3751
+ TableScroller,
3752
+ {
3753
+ ref: scrollerRef,
3754
+ className: chunkMWAXK66H_cjs.cn("scroll-pt-[36px]", bodyAfter ? "scroll-pb-8" : void 0),
3755
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col", style: { minWidth: minPanelWidth }, children: [
3756
+ headerGroups.map((group) => /* @__PURE__ */ jsxRuntime.jsxs(TableHeader, { sticky: true, children: [
3757
+ group.headers.map((header) => {
3758
+ const meta = header.column.columnDef.meta;
3759
+ const align = meta?.align ?? "left";
3760
+ const sticky = meta?.sticky ?? false;
3761
+ const headerSize = header.getSize() || void 0;
3762
+ const headerNode = header.isPlaceholder ? null : reactTable.flexRender(header.column.columnDef.header, header.getContext());
3763
+ const sorted = header.column.getIsSorted();
3764
+ const direction = sorted === false ? null : sorted;
3765
+ const canSort = header.column.getCanSort();
3766
+ const handleSort = header.column.getToggleSortingHandler();
3767
+ const ariaLabel2 = typeof header.column.columnDef.header === "string" ? header.column.columnDef.header : header.column.id;
3436
3768
  return /* @__PURE__ */ jsxRuntime.jsx(
3437
- DataRow,
3769
+ TableHeaderCell,
3438
3770
  {
3439
- row: item.row,
3440
- index: item.dataIndex,
3441
- zebra,
3442
- onRowClick,
3443
- style: rowStyle,
3444
- columnBorders,
3445
- ...rowWrapper ? { rowWrapper } : {}
3771
+ align,
3772
+ ariaLabel: ariaLabel2,
3773
+ draggable: enableColumnReordering && !!header.column.id,
3774
+ ...header.column.id ? { columnId: header.column.id } : {},
3775
+ ...headerSize !== void 0 ? { width: headerSize } : {},
3776
+ ...sticky !== false ? { sticky } : {},
3777
+ ...meta?.cellClassName ? { className: meta.cellClassName } : {},
3778
+ ...canSort ? {
3779
+ sortDirection: direction,
3780
+ onSortToggle: () => handleSort?.({})
3781
+ } : {},
3782
+ ...enableColumnRemoval && onRemoveColumn && header.column.id ? { onRemove: () => onRemoveColumn(header.column.id) } : {},
3783
+ ...enableColumnResizing && onResizeColumn && header.column.id ? {
3784
+ onResize: (width) => onResizeColumn({ id: header.column.id, width })
3785
+ } : {},
3786
+ children: headerNode
3446
3787
  },
3447
- item.row.id
3788
+ header.id
3448
3789
  );
3449
- })
3450
- }
3451
- ),
3452
- !showEmpty && /* @__PURE__ */ jsxRuntime.jsx("div", { ref: sentinelRef, "aria-hidden": "true", style: { height: 1, width: "100%" } }),
3453
- query.isFetchingNextPage ? /* @__PURE__ */ jsxRuntime.jsx(LoaderRow, {}) : null,
3454
- showEndMarker ? /* @__PURE__ */ jsxRuntime.jsx(EndMarker, { count: tableRowCount }) : null
3455
- ] }),
3456
- bodyAfter
3457
- ] }) })
3790
+ }),
3791
+ /* @__PURE__ */ jsxRuntime.jsx(TableHeaderSpacer, {})
3792
+ ] }, group.id)),
3793
+ bodyBefore,
3794
+ showInitialLoading ? loadingState ?? /* @__PURE__ */ jsxRuntime.jsx(DataTableSkeleton, { rowHeight }) : showError ? /* @__PURE__ */ jsxRuntime.jsx("div", { role: "rowgroup", children: errorState ?? (query.error ? /* @__PURE__ */ jsxRuntime.jsx(DataTableError, { error: query.error }) : null) }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "rowgroup", children: [
3795
+ showEmpty ? emptyState ?? /* @__PURE__ */ jsxRuntime.jsx(DataTableEmpty, {}) : /* @__PURE__ */ jsxRuntime.jsx(
3796
+ "div",
3797
+ {
3798
+ ref: listRef,
3799
+ role: "presentation",
3800
+ style: { height: totalSize, position: "relative", width: "100%" },
3801
+ children: virtualItems.map((vRow) => {
3802
+ const item = listItems[vRow.index];
3803
+ if (!item) return null;
3804
+ const rowStyle = {
3805
+ // Items' `start` includes the virtualizer's scrollMargin
3806
+ // (the list's offset below the sticky header); subtract
3807
+ // it to position relative to the list container.
3808
+ transform: `translateY(${vRow.start - listOffset}px)`,
3809
+ height: vRow.size
3810
+ };
3811
+ if (item.kind === "group") {
3812
+ return /* @__PURE__ */ jsxRuntime.jsx(GroupHeaderRow, { style: rowStyle, children: item.label }, `group:${item.id}`);
3813
+ }
3814
+ return /* @__PURE__ */ jsxRuntime.jsx(
3815
+ DataRow,
3816
+ {
3817
+ row: item.row,
3818
+ index: item.dataIndex,
3819
+ zebra,
3820
+ onRowClick,
3821
+ style: rowStyle,
3822
+ columnBorders,
3823
+ ...rowWrapper ? { rowWrapper } : {}
3824
+ },
3825
+ item.row.id
3826
+ );
3827
+ })
3828
+ }
3829
+ ),
3830
+ !showEmpty && /* @__PURE__ */ jsxRuntime.jsx("div", { ref: sentinelRef, "aria-hidden": "true", style: { height: 1, width: "100%" } }),
3831
+ query.isFetchingNextPage ? /* @__PURE__ */ jsxRuntime.jsx(LoaderRow, {}) : null
3832
+ ] }),
3833
+ bodyAfter ? (
3834
+ // Sticky to the scroller's bottom edge; `bg-bg` covers the rows
3835
+ // sliding underneath. z-[4] — like the sticky header — stays above an
3836
+ // editing row (z-[3]), so an editor scrolled under the strip is
3837
+ // covered rather than drawn over it.
3838
+ /* @__PURE__ */ jsxRuntime.jsx(
3839
+ "div",
3840
+ {
3841
+ className: chunkMWAXK66H_cjs.cn(
3842
+ "sticky bottom-0 z-[4] flex items-center h-8 px-0.5",
3843
+ "border-t border-rule-2 bg-bg"
3844
+ ),
3845
+ children: bodyAfter
3846
+ }
3847
+ )
3848
+ ) : null
3849
+ ] })
3850
+ }
3851
+ )
3458
3852
  }
3459
3853
  );
3460
3854
  }
@@ -3554,23 +3948,6 @@ function LoaderRow() {
3554
3948
  }
3555
3949
  );
3556
3950
  }
3557
- function EndMarker({ count }) {
3558
- return /* @__PURE__ */ jsxRuntime.jsxs(
3559
- "div",
3560
- {
3561
- className: chunkMWAXK66H_cjs.cn(
3562
- "flex items-center justify-center",
3563
- "h-8 font-mono text-[10.5px] uppercase tracking-[0.08em]",
3564
- "text-ink-4"
3565
- ),
3566
- children: [
3567
- "end \xB7 ",
3568
- count.toLocaleString(),
3569
- " rows"
3570
- ]
3571
- }
3572
- );
3573
- }
3574
3951
 
3575
3952
  // src/components/data-table/make-actions-column.ts
3576
3953
  function makeActionsColumn({
@@ -4158,7 +4535,9 @@ function MultiSelect({
4158
4535
  placement: "bottom start",
4159
4536
  className: chunkMWAXK66H_cjs.cn(
4160
4537
  "w-[var(--trigger-width)]",
4161
- "rounded-md border border-rule bg-surface p-1",
4538
+ // The popover element itself takes focus when opened programmatically
4539
+ // (defaultOpen) — no browser outline around the whole panel.
4540
+ "outline-none rounded-md border border-rule bg-surface p-1",
4162
4541
  "shadow-[var(--shadow-raised)]",
4163
4542
  "entering:animate-in entering:fade-in-0 entering:slide-in-from-top-1",
4164
4543
  "exiting:animate-out exiting:fade-out-0 exiting:slide-out-to-top-1"
@@ -4183,6 +4562,7 @@ function MultiSelect({
4183
4562
  {
4184
4563
  "aria-label": typeof label === "string" ? label : "Options",
4185
4564
  selectionMode: "multiple",
4565
+ escapeKeyBehavior: "none",
4186
4566
  selectedKeys: selectedSet,
4187
4567
  onSelectionChange: (keys) => {
4188
4568
  if (keys === "all") {
@@ -4205,11 +4585,10 @@ function MultiSelect({
4205
4585
  "cursor-pointer outline-none select-none",
4206
4586
  "data-[hovered]:bg-surface-2 data-[hovered]:text-ink",
4207
4587
  "data-[focused]:bg-surface-2 data-[focused]:text-ink",
4208
- "data-[selected]:bg-surface-3 data-[selected]:text-ink",
4209
4588
  "data-[disabled]:opacity-45 data-[disabled]:cursor-not-allowed"
4210
4589
  ),
4211
4590
  children: [
4212
- /* @__PURE__ */ jsxRuntime.jsx(Checkmark, {}),
4591
+ /* @__PURE__ */ jsxRuntime.jsx(ListboxCheck, {}),
4213
4592
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 truncate", children: option.content ?? option.label })
4214
4593
  ]
4215
4594
  },
@@ -4270,42 +4649,6 @@ function Chip({
4270
4649
  }
4271
4650
  );
4272
4651
  }
4273
- function Checkmark() {
4274
- return /* @__PURE__ */ jsxRuntime.jsx(
4275
- "span",
4276
- {
4277
- "aria-hidden": true,
4278
- className: chunkMWAXK66H_cjs.cn(
4279
- "relative inline-flex items-center justify-center shrink-0",
4280
- "size-[16px] rounded-[3px] border border-ink-4 bg-surface",
4281
- "transition-[background-color,border-color] duration-150",
4282
- "group-data-[selected]:bg-ink group-data-[selected]:border-ink"
4283
- ),
4284
- children: /* @__PURE__ */ jsxRuntime.jsx(
4285
- "svg",
4286
- {
4287
- viewBox: "0 0 12 12",
4288
- className: chunkMWAXK66H_cjs.cn(
4289
- "size-3 text-surface transition-opacity duration-150",
4290
- "opacity-0 group-data-[selected]:opacity-100"
4291
- ),
4292
- "aria-hidden": true,
4293
- children: /* @__PURE__ */ jsxRuntime.jsx(
4294
- "path",
4295
- {
4296
- d: "M2.5 6.25 5 8.75 9.5 3.75",
4297
- stroke: "currentColor",
4298
- strokeWidth: "1.6",
4299
- strokeLinecap: "round",
4300
- strokeLinejoin: "round",
4301
- fill: "none"
4302
- }
4303
- )
4304
- }
4305
- )
4306
- }
4307
- );
4308
- }
4309
4652
  var SEARCH_DEBOUNCE_MS = 250;
4310
4653
  function RecipientSelect({
4311
4654
  connectionId,
@@ -5875,7 +6218,8 @@ var OPERATORS = [
5875
6218
  "relation",
5876
6219
  "user",
5877
6220
  "principal",
5878
- "contact-address"
6221
+ "contact-address",
6222
+ "array"
5879
6223
  ]
5880
6224
  },
5881
6225
  {
@@ -5891,10 +6235,19 @@ var OPERATORS = [
5891
6235
  "relation",
5892
6236
  "user",
5893
6237
  "principal",
5894
- "contact-address"
6238
+ "contact-address",
6239
+ "array"
5895
6240
  ]
5896
6241
  },
5897
6242
  { id: "contains", label: "contains", arity: 1, types: ["string", "array", "contact-address"] },
6243
+ {
6244
+ id: "not_contains",
6245
+ label: "does not contain",
6246
+ arity: 1,
6247
+ types: ["string", "array", "contact-address"]
6248
+ },
6249
+ // Array-only: "has every one of these". On a scalar the backend rejects it.
6250
+ { id: "contains_all", label: "has all of", arity: "n", types: ["array"] },
5898
6251
  { id: "starts_with", label: "starts with", arity: 1, types: ["string", "contact-address"] },
5899
6252
  { id: "ends_with", label: "ends with", arity: 1, types: ["string", "contact-address"] },
5900
6253
  {
@@ -5957,14 +6310,33 @@ var OP_LABEL_OVERRIDES = {
5957
6310
  integer: { gt: "is greater than", lt: "is less than", eq: "equals", ne: "does not equal" },
5958
6311
  currency: { gt: "is greater than", lt: "is less than", eq: "equals", ne: "does not equal" },
5959
6312
  datetime: { eq: "is on", ne: "is not on", gt: "is after", lt: "is before" },
5960
- array: { contains: "contains", empty: "is empty", not_empty: "is not empty" },
6313
+ // Arrays filter by set membership, in the words every CRM uses for it.
6314
+ array: {
6315
+ contains: "has",
6316
+ not_contains: "does not have",
6317
+ in: "has any of",
6318
+ not_in: "has none of",
6319
+ contains_all: "has all of",
6320
+ empty: "is empty",
6321
+ not_empty: "is not empty"
6322
+ },
5961
6323
  boolean: { eq: "is", ne: "is not" }
5962
6324
  };
6325
+ var OP_ORDER_OVERRIDES = {
6326
+ array: ["contains", "not_contains", "in", "contains_all", "not_in", "empty", "not_empty"]
6327
+ };
5963
6328
  function operatorsForType(type) {
5964
- return OPERATORS.filter((op) => op.types.includes(type)).map((op) => {
6329
+ const ops = OPERATORS.filter((op) => op.types.includes(type)).map((op) => {
5965
6330
  const override = OP_LABEL_OVERRIDES[type]?.[op.id];
5966
6331
  return override ? { ...op, label: override } : op;
5967
6332
  });
6333
+ const order = OP_ORDER_OVERRIDES[type];
6334
+ if (!order) return ops;
6335
+ return [...ops].sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id));
6336
+ }
6337
+ function isEnumLike(field) {
6338
+ if (field.type === "enum") return true;
6339
+ return field.type === "array" && (field.options?.length ?? 0) > 0;
5968
6340
  }
5969
6341
  function operatorById(id) {
5970
6342
  return OPERATORS.find((o) => o.id === id);
@@ -6036,7 +6408,7 @@ function ChipItem({
6036
6408
  const custom = effectiveRenderer({ field, operator, value });
6037
6409
  if (custom !== void 0) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: custom });
6038
6410
  }
6039
- if (field.type === "enum") {
6411
+ if (isEnumLike(field)) {
6040
6412
  const option = findEnumOption(field, value);
6041
6413
  return /* @__PURE__ */ jsxRuntime.jsx(
6042
6414
  Badge,
@@ -6064,7 +6436,7 @@ function formatValue(field, operator, value) {
6064
6436
  if (arity === "n") {
6065
6437
  const arr = Array.isArray(value) ? value : typeof value === "string" && value.length > 0 ? value.split(",") : [];
6066
6438
  if (arr.length === 0) return "\u2014";
6067
- if (field.type === "enum") {
6439
+ if (isEnumLike(field)) {
6068
6440
  const labels = arr.map((v) => findEnumOption(field, v)?.label ?? v);
6069
6441
  return labels.length <= 2 ? labels.join(", ") : `${labels[0]}, ${labels[1]} +${labels.length - 2}`;
6070
6442
  }
@@ -6072,7 +6444,7 @@ function formatValue(field, operator, value) {
6072
6444
  }
6073
6445
  if (field.type === "boolean") return value === true || value === "true" ? "true" : "false";
6074
6446
  if (value === "" || value == null) return "\u2014";
6075
- if (field.type === "enum") {
6447
+ if (isEnumLike(field)) {
6076
6448
  return findEnumOption(field, String(value))?.label ?? String(value);
6077
6449
  }
6078
6450
  if (field.type === "number" || field.type === "integer") {
@@ -6160,13 +6532,21 @@ function TagInput({
6160
6532
  }) {
6161
6533
  const styles4 = fieldControlStyles({ size, invalid: isInvalid, appearance });
6162
6534
  const focusWithin = appearance === "cell" ? "focus-within:border-accent focus-within:shadow-none" : "focus-within:border-accent focus-within:shadow-[var(--shadow-focus)]";
6163
- const minHeight = appearance === "cell" ? "min-h-[29px]" : fieldMinHeight[size];
6535
+ const isCell = appearance === "cell";
6164
6536
  const inputRef = react.useRef(null);
6537
+ const chipsRef = react.useRef(null);
6538
+ const hasWrapped = useChipRowWrap(chipsRef, isCell, JSON.stringify(value));
6165
6539
  react.useEffect(() => {
6166
6540
  if (autoFocus) inputRef.current?.focus();
6167
6541
  }, []);
6168
6542
  const inputId = react.useId();
6169
6543
  const [draft, setDraft] = react.useState("");
6544
+ react.useLayoutEffect(() => {
6545
+ const input = inputRef.current;
6546
+ if (!isCell || !input || document.activeElement !== input) return;
6547
+ if (typeof input.scrollIntoView !== "function") return;
6548
+ input.scrollIntoView({ block: "nearest" });
6549
+ }, [isCell, value]);
6170
6550
  const commit = () => {
6171
6551
  const v = draft.trim();
6172
6552
  if (!v) return;
@@ -6183,7 +6563,10 @@ function TagInput({
6183
6563
  const onKeyDown2 = (e) => {
6184
6564
  if (e.key === "Enter" || e.key === ",") {
6185
6565
  e.preventDefault();
6186
- commit();
6566
+ if (draft.trim()) {
6567
+ e.stopPropagation();
6568
+ commit();
6569
+ }
6187
6570
  return;
6188
6571
  }
6189
6572
  if (e.key === "Backspace" && !draft && value.length > 0) {
@@ -6193,85 +6576,78 @@ function TagInput({
6193
6576
  };
6194
6577
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: chunkMWAXK66H_cjs.cn("grid grid-cols-1 gap-1.5", className), children: [
6195
6578
  label ? /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: inputId, className: fieldLabelStyles, children: label }) : null,
6196
- /* @__PURE__ */ jsxRuntime.jsxs(
6579
+ /* @__PURE__ */ jsxRuntime.jsx(
6197
6580
  "div",
6198
6581
  {
6199
6582
  "data-invalid": isInvalid || void 0,
6200
6583
  "data-disabled": isDisabled || void 0,
6201
6584
  className: chunkMWAXK66H_cjs.cn(
6202
6585
  styles4.box(),
6203
- "flex flex-wrap items-center gap-1",
6204
- minHeight,
6205
- fieldChipPadding[size],
6586
+ "flex",
6587
+ // Cell editor: one table row (29px under a 32px row) while the chips
6588
+ // fit on a line; two rows (64px), popping out of the row, once they
6589
+ // wrap — the chips then scroll inside. Field mode grows with its
6590
+ // content.
6591
+ isCell ? hasWrapped ? "h-[64px] p-1" : "h-[29px] px-1 py-0" : [fieldChipPadding[size], fieldMinHeight[size]],
6206
6592
  focusWithin
6207
6593
  ),
6208
- children: [
6209
- value.map((tag, idx) => (
6210
- // tags are kept unique on commit, so the value alone is a stable key
6211
- /* @__PURE__ */ jsxRuntime.jsxs(
6212
- "span",
6213
- {
6214
- className: chunkMWAXK66H_cjs.cn(
6215
- "inline-flex items-center gap-1",
6216
- "px-1.5 py-0.5 rounded-sm bg-surface-3",
6217
- "font-mono text-[12px] text-ink-2"
6218
- ),
6219
- children: [
6220
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "max-w-[10rem] truncate", children: tag }),
6221
- /* @__PURE__ */ jsxRuntime.jsx(
6222
- "button",
6223
- {
6224
- type: "button",
6225
- "aria-label": `Remove ${tag}`,
6226
- disabled: isDisabled,
6227
- onClick: () => remove(idx),
6228
- className: chunkMWAXK66H_cjs.cn(
6229
- "inline-flex items-center justify-center",
6230
- "size-3.5 rounded-full text-ink-3 hover:text-ink hover:bg-surface-2",
6231
- "cursor-pointer outline-none",
6232
- "focus-visible:shadow-[var(--shadow-focus)]",
6233
- "disabled:opacity-45 disabled:cursor-not-allowed"
6234
- ),
6235
- children: /* @__PURE__ */ jsxRuntime.jsx("svg", { "aria-hidden": true, viewBox: "0 0 10 10", className: "size-2", fill: "none", children: /* @__PURE__ */ jsxRuntime.jsx(
6236
- "path",
6237
- {
6238
- d: "M2 2l6 6M8 2l-6 6",
6239
- stroke: "currentColor",
6240
- strokeWidth: "1.4",
6241
- strokeLinecap: "round"
6242
- }
6243
- ) })
6244
- }
6594
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
6595
+ "div",
6596
+ {
6597
+ ref: chipsRef,
6598
+ className: chunkMWAXK66H_cjs.cn(
6599
+ "flex flex-1 flex-wrap items-center gap-1.5 min-w-0",
6600
+ isCell && (hasWrapped ? "content-start overflow-y-auto" : "content-center overflow-hidden")
6601
+ ),
6602
+ children: [
6603
+ value.map((tag, idx) => (
6604
+ // tags are kept unique on commit, so the value alone is a stable key
6605
+ /* @__PURE__ */ jsxRuntime.jsx(
6606
+ Badge,
6607
+ {
6608
+ variant: "active",
6609
+ shape: "rounded",
6610
+ hasDot: false,
6611
+ className: "shrink-0",
6612
+ actions: /* @__PURE__ */ jsxRuntime.jsx(
6613
+ BadgeAction,
6614
+ {
6615
+ label: `Remove ${tag}`,
6616
+ onPress: () => remove(idx),
6617
+ isDisabled
6618
+ }
6619
+ ),
6620
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "max-w-[10rem] truncate", children: tag })
6621
+ },
6622
+ tag
6623
+ )
6624
+ )),
6625
+ /* @__PURE__ */ jsxRuntime.jsx(
6626
+ "input",
6627
+ {
6628
+ ref: inputRef,
6629
+ id: inputId,
6630
+ type: inputType,
6631
+ value: draft,
6632
+ onChange: (e) => setDraft(e.target.value),
6633
+ onKeyDown: onKeyDown2,
6634
+ onBlur: commit,
6635
+ disabled: isDisabled,
6636
+ placeholder: value.length === 0 ? placeholder : "",
6637
+ className: chunkMWAXK66H_cjs.cn(
6638
+ "flex-1 min-w-[6rem] bg-transparent outline-none",
6639
+ "font-sans text-ink placeholder:text-ink-4",
6640
+ fieldTextStyles[size],
6641
+ // No vertical padding — wrapper's p-1.5 + min-h-[size] govern the
6642
+ // overall height so it matches Input/Select at the same size.
6643
+ "px-1.5",
6644
+ "disabled:cursor-not-allowed disabled:opacity-45"
6245
6645
  )
6246
- ]
6247
- },
6248
- tag
6249
- )
6250
- )),
6251
- /* @__PURE__ */ jsxRuntime.jsx(
6252
- "input",
6253
- {
6254
- ref: inputRef,
6255
- id: inputId,
6256
- type: inputType,
6257
- value: draft,
6258
- onChange: (e) => setDraft(e.target.value),
6259
- onKeyDown: onKeyDown2,
6260
- onBlur: commit,
6261
- disabled: isDisabled,
6262
- placeholder: value.length === 0 ? placeholder : "",
6263
- className: chunkMWAXK66H_cjs.cn(
6264
- "flex-1 min-w-[6rem] bg-transparent outline-none",
6265
- "font-sans text-ink placeholder:text-ink-4",
6266
- fieldTextStyles[size],
6267
- // No vertical padding — wrapper's p-1.5 + min-h-[size] govern the
6268
- // overall height so it matches Input/Select at the same size.
6269
- "px-1.5",
6270
- "disabled:cursor-not-allowed disabled:opacity-45"
6646
+ }
6271
6647
  )
6272
- }
6273
- )
6274
- ]
6648
+ ]
6649
+ }
6650
+ )
6275
6651
  }
6276
6652
  ),
6277
6653
  description ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: fieldHintStyles, children: description }) : null,
@@ -6316,7 +6692,7 @@ function FilterValueInput({
6316
6692
  }
6317
6693
  const leaf = (() => {
6318
6694
  if (arity === "n") {
6319
- if (field.type === "enum") {
6695
+ if (isEnumLike(field)) {
6320
6696
  return /* @__PURE__ */ jsxRuntime.jsx(
6321
6697
  EnumMultiValue,
6322
6698
  {
@@ -6343,7 +6719,7 @@ function FilterValueInput({
6343
6719
  }
6344
6720
  );
6345
6721
  }
6346
- if (field.type === "enum") {
6722
+ if (isEnumLike(field)) {
6347
6723
  return /* @__PURE__ */ jsxRuntime.jsx(
6348
6724
  EnumSingleValue,
6349
6725
  {
@@ -6504,7 +6880,9 @@ function placeholderFor(field, operator) {
6504
6880
  if (field.type === "number" || field.type === "integer" || field.type === "currency") return "0";
6505
6881
  if (operator === "starts_with") return "starts with\u2026";
6506
6882
  if (operator === "ends_with") return "ends with\u2026";
6507
- if (operator === "contains") return "contains\u2026";
6883
+ if (operator === "contains") return field.type === "array" ? "has\u2026" : "contains\u2026";
6884
+ if (operator === "not_contains")
6885
+ return field.type === "array" ? "does not have\u2026" : "does not contain\u2026";
6508
6886
  return "value";
6509
6887
  }
6510
6888
  function OperatorPicker({
@@ -8171,31 +8549,6 @@ function ToastRegion({ queue, className }) {
8171
8549
  }
8172
8550
  );
8173
8551
  }
8174
- function TooltipTrigger({
8175
- delay = 500,
8176
- closeDelay = 0,
8177
- ...props
8178
- }) {
8179
- return /* @__PURE__ */ jsxRuntime.jsx(reactAriaComponents.TooltipTrigger, { delay, closeDelay, ...props });
8180
- }
8181
- function Tooltip({ className, offset = 6, ...props }) {
8182
- return /* @__PURE__ */ jsxRuntime.jsx(
8183
- reactAriaComponents.Tooltip,
8184
- {
8185
- ...props,
8186
- offset,
8187
- className: (renderProps) => chunkMWAXK66H_cjs.cn(
8188
- "z-50 max-w-xs",
8189
- "rounded-md border border-rule bg-surface shadow-[var(--shadow-raised)]",
8190
- "px-2.5 py-1.5",
8191
- "font-mono text-[11.5px] text-ink-2",
8192
- "entering:animate-in entering:fade-in-0 entering:slide-in-from-top-1 entering:duration-150",
8193
- "exiting:animate-out exiting:fade-out-0 exiting:duration-100",
8194
- typeof className === "function" ? className(renderProps) : className
8195
- )
8196
- }
8197
- );
8198
- }
8199
8552
 
8200
8553
  Object.defineProperty(exports, "Button", {
8201
8554
  enumerable: true,
@@ -8226,6 +8579,8 @@ exports.AttributeTypeIcon = AttributeTypeIcon;
8226
8579
  exports.Avatar = Avatar;
8227
8580
  exports.AvatarGroup = AvatarGroup;
8228
8581
  exports.Badge = Badge;
8582
+ exports.BadgeAction = BadgeAction;
8583
+ exports.BadgeGroup = BadgeGroup;
8229
8584
  exports.BadgeMultiSelect = BadgeMultiSelect;
8230
8585
  exports.BooleanCell = BooleanCell;
8231
8586
  exports.Breadcrumbs = Breadcrumbs;
@@ -8347,6 +8702,8 @@ exports.arityOf = arityOf;
8347
8702
  exports.attachmentChipStyles = attachmentChipStyles;
8348
8703
  exports.avatarGroupStyles = avatarGroupStyles;
8349
8704
  exports.avatarStyles = avatarStyles;
8705
+ exports.badgeGroupOverflowStyles = badgeGroupOverflowStyles;
8706
+ exports.badgeGroupStyles = badgeGroupStyles;
8350
8707
  exports.badgeStyles = badgeStyles;
8351
8708
  exports.cardStyles = cardStyles2;
8352
8709
  exports.channelIcon = channelIcon;
@@ -8368,6 +8725,7 @@ exports.fieldHintStyles = fieldHintStyles;
8368
8725
  exports.fieldLabelStyles = fieldLabelStyles;
8369
8726
  exports.fileViewerFallbackCardStyles = fileViewerFallbackCardStyles;
8370
8727
  exports.fileViewerStyles = fileViewerStyles;
8728
+ exports.fitBadgeCount = fitBadgeCount;
8371
8729
  exports.formatFileSize = formatFileSize;
8372
8730
  exports.formatValue = formatValue;
8373
8731
  exports.iconForContentType = iconForContentType;