@proteos/ui 0.54.1 → 0.55.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.
package/dist/index.js CHANGED
@@ -2,9 +2,9 @@ import { cn, Button, Skeleton } from './chunk-IKAX76MB.js';
2
2
  export { Button, Skeleton, buttonStyles, cn, skeletonStyles } from './chunk-IKAX76MB.js';
3
3
  import { icons, Contact, Paperclip, BookText, Banknote, UsersRound, User, Link2, Braces, Brackets, ToggleLeft, CircleDot, Calendar, Hash, Type, File, FileImage, FileText, FileSpreadsheet, FileArchive, Loader2, X, Shapes, Search, Phone, AudioLines, Video, Mic, AtSign, MessagesSquare, Camera, Send, Linkedin, MessageCircle, Mail, Pipette, Pencil, ChevronDown, GripVertical, SendHorizontal, CircleX, Download, ChevronRight as ChevronRight$1, CircleCheck, CircleAlert, Info } from 'lucide-react';
4
4
  import { tv } from 'tailwind-variants';
5
- import { Tabs as Tabs$1, TabPanel, Tab, TabList, MenuTrigger as MenuTrigger$1, SubmenuTrigger as SubmenuTrigger$1, DialogTrigger, Popover as Popover$1, Dialog, DropZone, Text, FileTrigger, Heading, Label, Input as Input$1, ListBox, ListBoxItem, Breadcrumbs as Breadcrumbs$1, Breadcrumb, Link, Button as Button$1, Checkbox as Checkbox$1, CheckboxGroup as CheckboxGroup$1, FieldError, TextField, ComboBox, ComboBoxStateContext, Select as Select$1, SelectValue, ModalOverlay, Modal as Modal$1, DatePicker as DatePicker$1, Group, DateInput, DateSegment, Menu as Menu$1, MenuItem as MenuItem$1, Disclosure, DisclosurePanel, ProgressBar as ProgressBar$1, RadioGroup as RadioGroup$1, Radio as Radio$1, Separator as Separator$1, Switch as Switch$1, TextArea, TimeField as TimeField$1, UNSTABLE_ToastQueue, UNSTABLE_ToastRegion, UNSTABLE_Toast, UNSTABLE_ToastContent, TooltipTrigger as TooltipTrigger$1, Tooltip as Tooltip$1, Calendar as Calendar$1, CalendarGrid, CalendarGridHeader, CalendarHeaderCell, CalendarGridBody, CalendarCell, DatePickerStateContext } from 'react-aria-components';
5
+ import { Tabs as Tabs$1, TabPanel, Tab, TabList, MenuTrigger as MenuTrigger$1, SubmenuTrigger as SubmenuTrigger$1, DialogTrigger, Popover as Popover$1, Dialog, DropZone, Text, FileTrigger, Heading, Label, Input as Input$1, ListBox, ListBoxItem, TooltipTrigger as TooltipTrigger$1, Tooltip as Tooltip$1, Button as Button$1, Breadcrumbs as Breadcrumbs$1, Breadcrumb, Link, Checkbox as Checkbox$1, CheckboxGroup as CheckboxGroup$1, FieldError, TextField, ComboBox, ComboBoxStateContext, Select as Select$1, SelectValue, ModalOverlay, Modal as Modal$1, DatePicker as DatePicker$1, Group, DateInput, DateSegment, Menu as Menu$1, MenuItem as MenuItem$1, Disclosure, DisclosurePanel, ProgressBar as ProgressBar$1, RadioGroup as RadioGroup$1, Radio as Radio$1, Separator as Separator$1, Switch as Switch$1, TextArea, TimeField as TimeField$1, UNSTABLE_ToastQueue, UNSTABLE_ToastRegion, UNSTABLE_Toast, UNSTABLE_ToastContent, Calendar as Calendar$1, CalendarGrid, CalendarGridHeader, CalendarHeaderCell, CalendarGridBody, CalendarCell, DatePickerStateContext } from 'react-aria-components';
6
6
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
- import { lazy, createContext, useState, useRef, useMemo, useEffect, useContext, useSyncExternalStore, useCallback, useLayoutEffect, Suspense, useId, Fragment as Fragment$1 } from 'react';
7
+ import { lazy, createContext, useState, useRef, useMemo, Children, isValidElement, useLayoutEffect, useEffect, useContext, useSyncExternalStore, useCallback, Suspense, useId, Fragment as Fragment$1 } from 'react';
8
8
  import { formatAmount, parseAmount } from '@proteos/sdk';
9
9
  import { draggable, dropTargetForElements, monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
10
10
  import { extractClosestEdge, attachClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
@@ -872,6 +872,7 @@ function Badge({
872
872
  hasDot,
873
873
  color,
874
874
  icon,
875
+ actions,
875
876
  className,
876
877
  style,
877
878
  children,
@@ -893,11 +894,249 @@ function Badge({
893
894
  ),
894
895
  children: [
895
896
  Icon ? /* @__PURE__ */ jsx(Icon, { "aria-hidden": true, className: cn("size-3 shrink-0", ICON_TONE[register2]) }) : null,
896
- children
897
+ children,
898
+ actions ? (
899
+ // Pulled flush with the trailing edge so the chip still reads
900
+ // symmetric around its leading dot / icon.
901
+ /* @__PURE__ */ jsx("span", { className: "inline-flex items-center shrink-0 -mr-1", children: actions })
902
+ ) : null
897
903
  ]
898
904
  }
899
905
  );
900
906
  }
907
+ function BadgeAction({ label, onPress, isDisabled = false, children }) {
908
+ return /* @__PURE__ */ jsx(
909
+ "button",
910
+ {
911
+ type: "button",
912
+ "aria-label": label,
913
+ disabled: isDisabled,
914
+ onClick: (event) => {
915
+ event.stopPropagation();
916
+ onPress();
917
+ },
918
+ className: cn(
919
+ "inline-flex items-center justify-center shrink-0",
920
+ "size-3.5 rounded-full",
921
+ "text-current/70 hover:text-current hover:bg-current/10",
922
+ "outline-none cursor-pointer",
923
+ "focus-visible:shadow-[var(--shadow-focus)]",
924
+ "disabled:opacity-45 disabled:cursor-not-allowed"
925
+ ),
926
+ children: children ?? /* @__PURE__ */ jsx("svg", { "aria-hidden": true, viewBox: "0 0 10 10", className: "size-2.5", fill: "none", children: /* @__PURE__ */ jsx(
927
+ "path",
928
+ {
929
+ d: "M2 2l6 6M8 2l-6 6",
930
+ stroke: "currentColor",
931
+ strokeWidth: "1.4",
932
+ strokeLinecap: "round"
933
+ }
934
+ ) })
935
+ }
936
+ );
937
+ }
938
+ function TooltipTrigger({
939
+ delay = 500,
940
+ closeDelay = 0,
941
+ ...props
942
+ }) {
943
+ return /* @__PURE__ */ jsx(TooltipTrigger$1, { delay, closeDelay, ...props });
944
+ }
945
+ function Tooltip({ className, offset = 6, ...props }) {
946
+ return /* @__PURE__ */ jsx(
947
+ Tooltip$1,
948
+ {
949
+ ...props,
950
+ offset,
951
+ className: (renderProps) => cn(
952
+ "z-50 max-w-xs",
953
+ "rounded-md border border-rule bg-surface shadow-[var(--shadow-raised)]",
954
+ "px-2.5 py-1.5",
955
+ "font-mono text-[11.5px] text-ink-2",
956
+ "entering:animate-in entering:fade-in-0 entering:slide-in-from-top-1 entering:duration-150",
957
+ "exiting:animate-out exiting:fade-out-0 exiting:duration-100",
958
+ typeof className === "function" ? className(renderProps) : className
959
+ )
960
+ }
961
+ );
962
+ }
963
+ var badgeGroupStyles = tv({
964
+ base: "flex flex-nowrap items-center gap-1.5 w-full min-w-0",
965
+ variants: {
966
+ // The measuring pass renders every chip plus the overflow chip so their
967
+ // widths can be read; it is hidden so that pass never paints.
968
+ isMeasuring: { true: "invisible", false: "" }
969
+ },
970
+ defaultVariants: { isMeasuring: false }
971
+ });
972
+ function fitBadgeCount(metrics, available) {
973
+ const { itemWidths, overflowWidth, gap } = metrics;
974
+ const total = itemWidths.length;
975
+ if (available <= 0) return total;
976
+ let fullWidth = 0;
977
+ for (let index = 0; index < total; index += 1) {
978
+ fullWidth += (itemWidths[index] ?? 0) + (index > 0 ? gap : 0);
979
+ }
980
+ if (fullWidth <= available) return total;
981
+ let used = 0;
982
+ let count = 0;
983
+ for (let index = 0; index < total; index += 1) {
984
+ const next = used + (itemWidths[index] ?? 0) + gap;
985
+ if (next + overflowWidth > available) break;
986
+ used = next;
987
+ count = index + 1;
988
+ }
989
+ return count;
990
+ }
991
+ var badgeGroupOverflowStyles = tv({
992
+ base: [
993
+ badgeStyles({ variant: "neutral", shape: "rounded", hasDot: false }),
994
+ // badgeStyles' register still materializes an empty ::before — hide it,
995
+ // as Badge itself does when no dot should show.
996
+ "before:hidden shrink-0 cursor-pointer whitespace-nowrap",
997
+ "hover:bg-surface-2 hover:text-ink",
998
+ "outline-none focus-visible:shadow-[var(--shadow-focus)]"
999
+ ]
1000
+ });
1001
+ function BadgeGroup({ children, className, ...props }) {
1002
+ const items = Children.toArray(children);
1003
+ const signature = items.map((item, index) => isValidElement(item) ? String(item.key ?? index) : String(item)).join("");
1004
+ const rootRef = useRef(null);
1005
+ const metricsRef = useRef(null);
1006
+ const [layout, setLayout] = useState({ signature, visibleCount: null });
1007
+ if (layout.signature !== signature) {
1008
+ metricsRef.current = null;
1009
+ setLayout({ signature, visibleCount: null });
1010
+ }
1011
+ const measuring = layout.signature !== signature || layout.visibleCount === null;
1012
+ const visibleCount = measuring ? items.length : layout.visibleCount ?? items.length;
1013
+ const shown = items.slice(0, visibleCount);
1014
+ const hidden = measuring ? items.length : items.length - visibleCount;
1015
+ useLayoutEffect(() => {
1016
+ if (!measuring) return;
1017
+ const root = rootRef.current;
1018
+ if (!root) return;
1019
+ const nodes = Array.from(root.children);
1020
+ const metrics = {
1021
+ itemWidths: nodes.slice(0, items.length).map((node) => node.getBoundingClientRect().width),
1022
+ overflowWidth: nodes[items.length]?.getBoundingClientRect().width ?? 0,
1023
+ gap: Number.parseFloat(getComputedStyle(root).columnGap) || 0
1024
+ };
1025
+ metricsRef.current = metrics;
1026
+ setLayout({ signature, visibleCount: fitBadgeCount(metrics, root.clientWidth) });
1027
+ }, [measuring, signature, items.length]);
1028
+ useEffect(() => {
1029
+ const root = rootRef.current;
1030
+ if (!root || typeof ResizeObserver === "undefined") return;
1031
+ const observer = new ResizeObserver(() => {
1032
+ const metrics = metricsRef.current;
1033
+ if (!metrics) return;
1034
+ const next = fitBadgeCount(metrics, root.clientWidth);
1035
+ setLayout((prev) => prev.visibleCount === next ? prev : { ...prev, visibleCount: next });
1036
+ });
1037
+ observer.observe(root);
1038
+ return () => observer.disconnect();
1039
+ }, []);
1040
+ useEffect(() => {
1041
+ const fonts = typeof document === "undefined" ? void 0 : document.fonts;
1042
+ if (!fonts || fonts.status === "loaded") return;
1043
+ let cancelled = false;
1044
+ void fonts.ready.then(() => {
1045
+ if (cancelled) return;
1046
+ metricsRef.current = null;
1047
+ setLayout((prev) => ({ ...prev, visibleCount: null }));
1048
+ });
1049
+ return () => {
1050
+ cancelled = true;
1051
+ };
1052
+ }, []);
1053
+ return /* @__PURE__ */ jsxs(
1054
+ "div",
1055
+ {
1056
+ ...props,
1057
+ ref: rootRef,
1058
+ className: cn(badgeGroupStyles({ isMeasuring: measuring }), className),
1059
+ children: [
1060
+ shown.map((item, index) => /* @__PURE__ */ jsx(
1061
+ "span",
1062
+ {
1063
+ className: "inline-flex shrink-0",
1064
+ children: item
1065
+ },
1066
+ (isValidElement(item) ? item.key : null) ?? index
1067
+ )),
1068
+ hidden > 0 ? (
1069
+ // The span swallows the click before it reaches a clickable host row
1070
+ // (React propagation), so opening the overflow never opens the record.
1071
+ // biome-ignore lint/a11y/noStaticElementInteractions: click fence only — the button inside is the interactive element
1072
+ // biome-ignore lint/a11y/useKeyWithClickEvents: keyboard handling lives on the button inside
1073
+ /* @__PURE__ */ jsx("span", { className: "inline-flex shrink-0", onClick: (event) => event.stopPropagation(), children: /* @__PURE__ */ jsxs(Popover, { children: [
1074
+ /* @__PURE__ */ jsxs(TooltipTrigger, { children: [
1075
+ /* @__PURE__ */ jsxs(Button$1, { className: badgeGroupOverflowStyles(), children: [
1076
+ "+",
1077
+ hidden,
1078
+ " more"
1079
+ ] }),
1080
+ /* @__PURE__ */ jsx(Tooltip, { className: "max-w-sm", children: /* @__PURE__ */ jsx("span", { className: "flex flex-wrap items-center gap-1.5", children: items }) })
1081
+ ] }),
1082
+ /* @__PURE__ */ jsx(PopoverContent, { placement: "bottom start", panelClassName: "max-w-sm", children: /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-1.5 p-2.5", children: items }) })
1083
+ ] }) })
1084
+ ) : null
1085
+ ]
1086
+ }
1087
+ );
1088
+ }
1089
+ var listboxCheckStyles = tv({
1090
+ slots: {
1091
+ box: [
1092
+ "relative inline-flex items-center justify-center shrink-0",
1093
+ "size-[16px] rounded-[3px] border border-ink-4 bg-surface",
1094
+ "transition-[background-color,border-color] duration-150",
1095
+ "group-data-[selected]:bg-ink group-data-[selected]:border-ink"
1096
+ ],
1097
+ tick: [
1098
+ "size-3 text-surface transition-opacity duration-150",
1099
+ "opacity-0 group-data-[selected]:opacity-100"
1100
+ ]
1101
+ }
1102
+ });
1103
+ function ListboxCheck() {
1104
+ const { box, tick } = listboxCheckStyles();
1105
+ return /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: box(), children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 12 12", className: tick(), "aria-hidden": true, children: /* @__PURE__ */ jsx(
1106
+ "path",
1107
+ {
1108
+ d: "M2.5 6.25 5 8.75 9.5 3.75",
1109
+ stroke: "currentColor",
1110
+ strokeWidth: "1.6",
1111
+ strokeLinecap: "round",
1112
+ strokeLinejoin: "round",
1113
+ fill: "none"
1114
+ }
1115
+ ) }) });
1116
+ }
1117
+ function useChipRowWrap(ref, isEnabled, signature) {
1118
+ const [hasWrapped, setHasWrapped] = useState(false);
1119
+ useLayoutEffect(() => {
1120
+ if (!isEnabled) return;
1121
+ const element = ref.current;
1122
+ if (!element) return;
1123
+ const measure = () => {
1124
+ const children = Array.from(element.children);
1125
+ const first = children[0]?.getBoundingClientRect();
1126
+ if (!first || first.height === 0) {
1127
+ setHasWrapped(false);
1128
+ return;
1129
+ }
1130
+ setHasWrapped(children.some((child) => child.getBoundingClientRect().top >= first.bottom - 1));
1131
+ };
1132
+ measure();
1133
+ if (typeof ResizeObserver === "undefined") return;
1134
+ const observer = new ResizeObserver(measure);
1135
+ observer.observe(element);
1136
+ return () => observer.disconnect();
1137
+ }, [ref, isEnabled, signature]);
1138
+ return hasWrapped;
1139
+ }
901
1140
  function badgeChrome(option) {
902
1141
  return {
903
1142
  ...option.variant ? { variant: option.variant } : {},
@@ -921,18 +1160,51 @@ function BadgeMultiSelect({
921
1160
  size = "md",
922
1161
  appearance,
923
1162
  autoFocus = false,
1163
+ defaultOpen = false,
1164
+ "aria-label": ariaLabel,
924
1165
  className
925
1166
  }) {
1167
+ const isCell = appearance === "cell";
926
1168
  const styles4 = fieldControlStyles({ size, invalid: isInvalid, appearance });
927
- const focusWithin = appearance === "cell" ? "focus-within:border-accent focus-within:shadow-none" : "focus-within:border-accent focus-within:shadow-[var(--shadow-focus)]";
928
- const minHeight = appearance === "cell" ? "min-h-[29px]" : fieldMinHeight[size];
1169
+ const focusWithin = isCell ? "focus-within:border-accent focus-within:shadow-none" : "focus-within:border-accent focus-within:shadow-[var(--shadow-focus)]";
929
1170
  const [isOpen, setIsOpen] = useState(false);
930
1171
  const triggerRef = useRef(null);
1172
+ const chipsRef = useRef(null);
1173
+ const popoverRef = useRef(null);
1174
+ const closedByPointerRef = useRef(false);
931
1175
  useEffect(() => {
932
1176
  if (autoFocus) triggerRef.current?.focus();
1177
+ if (defaultOpen && !isDisabled) setIsOpen(true);
933
1178
  }, []);
1179
+ const wasOpenRef = useRef(false);
1180
+ useEffect(() => {
1181
+ if (wasOpenRef.current && !isOpen && !closedByPointerRef.current) {
1182
+ const active = document.activeElement;
1183
+ const strandedInList = !active || active === document.body || !!active.closest('[data-rac][role="listbox"]');
1184
+ if (strandedInList) triggerRef.current?.focus();
1185
+ }
1186
+ wasOpenRef.current = isOpen;
1187
+ closedByPointerRef.current = false;
1188
+ }, [isOpen]);
934
1189
  const selectedSet = new Set(selectedKeys);
935
1190
  const selectedOptions = options.filter((o) => selectedSet.has(o.id));
1191
+ const hasWrapped = useChipRowWrap(
1192
+ chipsRef,
1193
+ isCell,
1194
+ JSON.stringify(selectedOptions.map((o) => o.id))
1195
+ );
1196
+ useEffect(() => {
1197
+ if (!isOpen) return;
1198
+ const onPointerDown = (event) => {
1199
+ const target = event.target;
1200
+ if (!(target instanceof Node)) return;
1201
+ if (triggerRef.current?.contains(target) || popoverRef.current?.contains(target)) return;
1202
+ closedByPointerRef.current = true;
1203
+ setIsOpen(false);
1204
+ };
1205
+ document.addEventListener("pointerdown", onPointerDown, true);
1206
+ return () => document.removeEventListener("pointerdown", onPointerDown, true);
1207
+ }, [isOpen]);
936
1208
  const listboxOptions = hideSelectedOptions ? options.filter((o) => !selectedSet.has(o.id)) : options;
937
1209
  const remove = (id) => {
938
1210
  const next = new Set(selectedSet);
@@ -944,6 +1216,12 @@ function BadgeMultiSelect({
944
1216
  if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") {
945
1217
  e.preventDefault();
946
1218
  setIsOpen(true);
1219
+ return;
1220
+ }
1221
+ if (e.key === "Escape" && isOpen) {
1222
+ e.preventDefault();
1223
+ e.stopPropagation();
1224
+ setIsOpen(false);
947
1225
  }
948
1226
  };
949
1227
  return /* @__PURE__ */ jsxs("div", { className: cn("grid grid-cols-1 gap-1.5", className), children: [
@@ -953,6 +1231,7 @@ function BadgeMultiSelect({
953
1231
  {
954
1232
  ref: triggerRef,
955
1233
  role: "combobox",
1234
+ "aria-label": ariaLabel,
956
1235
  "aria-expanded": isOpen,
957
1236
  "aria-haspopup": "listbox",
958
1237
  "aria-disabled": isDisabled || void 0,
@@ -964,34 +1243,53 @@ function BadgeMultiSelect({
964
1243
  "data-disabled": isDisabled || void 0,
965
1244
  className: cn(
966
1245
  styles4.box(),
967
- "flex flex-wrap items-center gap-1.5 cursor-pointer",
968
- minHeight,
969
- fieldChipPadding[size],
970
- "pr-2",
1246
+ "flex items-stretch cursor-pointer",
1247
+ // Cell editor: one table row (29px under a 32px row) while the chips
1248
+ // fit on a line; two rows (64px), popping out of the row, once they
1249
+ // wrap — the chips then scroll inside. Field mode grows with its
1250
+ // content like every other chip field.
1251
+ isCell ? hasWrapped ? "h-[64px] p-1" : "h-[29px] px-1 py-0" : [fieldChipPadding[size], fieldMinHeight[size]],
971
1252
  focusWithin
972
1253
  ),
973
1254
  children: [
974
- selectedOptions.length === 0 ? (
975
- // No vertical padding — wrapper's p-1.5 + min-h-[size] govern the
976
- // overall height so it matches the Input/Select trigger at the same
977
- // size.
978
- /* @__PURE__ */ jsx("span", { className: cn("text-ink-4 font-sans px-1", fieldTextStyles[size]), children: placeholder })
979
- ) : selectedOptions.map((o) => /* @__PURE__ */ jsx(
980
- RemovableBadge,
1255
+ /* @__PURE__ */ jsx(
1256
+ "div",
981
1257
  {
982
- ...badgeChrome(o),
983
- onRemove: () => remove(o.id),
984
- isDisabled,
985
- children: o.label
986
- },
987
- o.id
988
- )),
989
- /* @__PURE__ */ jsx("span", { className: "ml-auto pr-1", children: /* @__PURE__ */ jsx(FieldCaret, {}) })
1258
+ ref: chipsRef,
1259
+ className: cn(
1260
+ "flex flex-1 flex-wrap items-center gap-1.5 min-w-0",
1261
+ isCell && (hasWrapped ? "content-start overflow-y-auto" : "content-center overflow-hidden")
1262
+ ),
1263
+ children: selectedOptions.length === 0 ? (
1264
+ // No vertical padding — wrapper's p-1.5 + min-h-[size] govern the
1265
+ // overall height so it matches the Input/Select trigger at the same
1266
+ // size.
1267
+ /* @__PURE__ */ jsx("span", { className: cn("text-ink-4 font-sans px-1", fieldTextStyles[size]), children: placeholder })
1268
+ ) : selectedOptions.map((o) => /* @__PURE__ */ jsx(
1269
+ Badge,
1270
+ {
1271
+ ...badgeChrome(o),
1272
+ className: "shrink-0",
1273
+ actions: /* @__PURE__ */ jsx(
1274
+ BadgeAction,
1275
+ {
1276
+ label: "Remove",
1277
+ onPress: () => remove(o.id),
1278
+ isDisabled
1279
+ }
1280
+ ),
1281
+ children: o.label
1282
+ },
1283
+ o.id
1284
+ ))
1285
+ }
1286
+ ),
1287
+ isCell ? null : /* @__PURE__ */ jsx("span", { className: "flex items-center pl-1.5 pr-1", children: /* @__PURE__ */ jsx(FieldCaret, {}) })
990
1288
  ]
991
1289
  }
992
1290
  ),
993
1291
  description ? /* @__PURE__ */ jsx("p", { className: fieldHintStyles, children: description }) : null,
994
- errorMessage && appearance !== "cell" ? /* @__PURE__ */ jsx("p", { className: fieldErrorStyles, children: errorMessage }) : null,
1292
+ errorMessage && !isCell ? /* @__PURE__ */ jsx("p", { className: fieldErrorStyles, children: errorMessage }) : null,
995
1293
  /* @__PURE__ */ jsx(
996
1294
  Popover$1,
997
1295
  {
@@ -1000,18 +1298,27 @@ function BadgeMultiSelect({
1000
1298
  triggerRef,
1001
1299
  offset: 6,
1002
1300
  placement: "bottom start",
1301
+ isNonModal: true,
1302
+ shouldCloseOnInteractOutside: (element) => !triggerRef.current?.contains(element),
1003
1303
  className: cn(
1004
- "w-[var(--trigger-width)]",
1005
- "rounded-md border border-rule bg-surface p-1",
1304
+ // At least as wide as the trigger, wider when a badge needs it —
1305
+ // a narrow table column must not clip its own options.
1306
+ "min-w-[var(--trigger-width)] w-max max-w-[min(28rem,calc(100vw-2rem))]",
1307
+ // Float above modals + full-screen editors, matching Select.
1308
+ "z-[1300]",
1309
+ // The popover element itself takes focus when opened programmatically
1310
+ // (defaultOpen) — no browser outline around the whole panel.
1311
+ "outline-none rounded-md border border-rule bg-surface p-1",
1006
1312
  "shadow-[var(--shadow-raised)]",
1007
1313
  "entering:animate-in entering:fade-in-0 entering:slide-in-from-top-1",
1008
1314
  "exiting:animate-out exiting:fade-out-0 exiting:slide-out-to-top-1"
1009
1315
  ),
1010
- children: /* @__PURE__ */ jsx(
1316
+ children: /* @__PURE__ */ jsx("div", { ref: popoverRef, children: /* @__PURE__ */ jsx(
1011
1317
  ListBox,
1012
1318
  {
1013
- "aria-label": typeof label === "string" ? label : "Options",
1319
+ "aria-label": typeof label === "string" ? label : ariaLabel ?? "Options",
1014
1320
  selectionMode: "multiple",
1321
+ escapeKeyBehavior: "none",
1015
1322
  selectedKeys: selectedSet,
1016
1323
  onSelectionChange: (keys) => {
1017
1324
  if (keys === "all") {
@@ -1020,70 +1327,32 @@ function BadgeMultiSelect({
1020
1327
  }
1021
1328
  onSelectionChange(keys);
1022
1329
  },
1330
+ autoFocus: "first",
1023
1331
  className: "outline-none max-h-72 overflow-auto",
1024
1332
  renderEmptyState: () => /* @__PURE__ */ jsx("div", { className: cn("px-3 py-2", fieldHintStyles), children: "All options selected" }),
1025
- children: listboxOptions.map((option) => /* @__PURE__ */ jsx(
1333
+ children: listboxOptions.map((option) => /* @__PURE__ */ jsxs(
1026
1334
  ListBoxItem,
1027
1335
  {
1028
1336
  id: option.id,
1029
1337
  textValue: option.label,
1030
1338
  ...option.isDisabled ? { isDisabled: true } : {},
1031
1339
  className: cn(
1032
- "group flex items-center",
1033
- "rounded-[6px] px-3 py-2",
1340
+ "group flex items-center gap-2.5",
1341
+ "rounded-[6px] px-2.5 py-1.5",
1034
1342
  "cursor-pointer outline-none select-none",
1343
+ // Selection is carried by the check box, not a row fill.
1035
1344
  "data-[hovered]:bg-surface-2",
1036
1345
  "data-[focused]:bg-surface-2",
1037
- "data-[selected]:bg-surface-3",
1038
1346
  "data-[disabled]:opacity-45 data-[disabled]:cursor-not-allowed"
1039
1347
  ),
1040
- children: /* @__PURE__ */ jsx(Badge, { ...badgeChrome(option), children: option.label })
1348
+ children: [
1349
+ /* @__PURE__ */ jsx(ListboxCheck, {}),
1350
+ /* @__PURE__ */ jsx(Badge, { ...badgeChrome(option), children: option.label })
1351
+ ]
1041
1352
  },
1042
1353
  option.id
1043
1354
  ))
1044
1355
  }
1045
- )
1046
- }
1047
- )
1048
- ] });
1049
- }
1050
- function RemovableBadge({
1051
- children,
1052
- onRemove,
1053
- isDisabled,
1054
- ...chrome
1055
- }) {
1056
- return /* @__PURE__ */ jsxs(Badge, { ...badgeChrome(chrome), children: [
1057
- /* @__PURE__ */ jsx("span", { children }),
1058
- /* @__PURE__ */ jsx(
1059
- "button",
1060
- {
1061
- type: "button",
1062
- "aria-label": "Remove",
1063
- disabled: isDisabled,
1064
- onClick: (e) => {
1065
- e.stopPropagation();
1066
- onRemove();
1067
- },
1068
- className: cn(
1069
- "inline-flex items-center justify-center shrink-0",
1070
- "size-3.5 rounded-full",
1071
- "text-current/70 hover:text-current hover:bg-current/10",
1072
- "outline-none cursor-pointer",
1073
- "focus-visible:shadow-[var(--shadow-focus)]",
1074
- "disabled:opacity-45 disabled:cursor-not-allowed",
1075
- // Pull the button flush with the right edge so the pill still feels
1076
- // symmetric around the leading dot.
1077
- "-mr-1"
1078
- ),
1079
- children: /* @__PURE__ */ jsx("svg", { "aria-hidden": true, viewBox: "0 0 10 10", className: "size-2.5", fill: "none", children: /* @__PURE__ */ jsx(
1080
- "path",
1081
- {
1082
- d: "M2 2l6 6M8 2l-6 6",
1083
- stroke: "currentColor",
1084
- strokeWidth: "1.4",
1085
- strokeLinecap: "round"
1086
- }
1087
1356
  ) })
1088
1357
  }
1089
1358
  )
@@ -2687,7 +2956,9 @@ var tableHeaderStyles = tv({
2687
2956
  "border-b border-rule"
2688
2957
  ],
2689
2958
  variants: {
2690
- sticky: { true: "sticky top-0 z-[2]", false: "" }
2959
+ // z-[4]: above an editing row (z-[3]) — sticky chrome stays on top of
2960
+ // rows scrolling underneath, popped-out editors included.
2961
+ sticky: { true: "sticky top-0 z-[4]", false: "" }
2691
2962
  },
2692
2963
  defaultVariants: { sticky: false }
2693
2964
  });
@@ -2752,11 +3023,57 @@ function TableHeaderCell({
2752
3023
  onRemove,
2753
3024
  ariaLabel,
2754
3025
  sticky = false,
3026
+ onResize,
3027
+ minWidth = 64,
2755
3028
  ...rest
2756
3029
  }) {
2757
3030
  const cellRef = useRef(null);
2758
3031
  const handleRef = useRef(null);
2759
3032
  const [dragState, setDragState] = useState({ kind: "idle" });
3033
+ const [isResizing, setIsResizing] = useState(false);
3034
+ const startResize = (event) => {
3035
+ if (!onResize || event.button !== 0) return;
3036
+ const cell = cellRef.current;
3037
+ if (!cell) return;
3038
+ event.preventDefault();
3039
+ event.stopPropagation();
3040
+ const handle = event.currentTarget;
3041
+ const startX = event.clientX;
3042
+ const startWidth = cell.getBoundingClientRect().width;
3043
+ if (typeof handle.setPointerCapture === "function") handle.setPointerCapture(event.pointerId);
3044
+ setIsResizing(true);
3045
+ document.body.style.cursor = "col-resize";
3046
+ let frame = 0;
3047
+ const report = (clientX) => {
3048
+ const next = Math.max(minWidth, Math.round(startWidth + clientX - startX));
3049
+ if (typeof requestAnimationFrame !== "function") {
3050
+ onResize(next);
3051
+ return;
3052
+ }
3053
+ cancelAnimationFrame(frame);
3054
+ frame = requestAnimationFrame(() => onResize(next));
3055
+ };
3056
+ const onMove = (move) => report(move.clientX);
3057
+ const onEnd = () => {
3058
+ handle.removeEventListener("pointermove", onMove);
3059
+ handle.removeEventListener("pointerup", onEnd);
3060
+ handle.removeEventListener("pointercancel", onEnd);
3061
+ document.body.style.cursor = "";
3062
+ setIsResizing(false);
3063
+ };
3064
+ handle.addEventListener("pointermove", onMove);
3065
+ handle.addEventListener("pointerup", onEnd);
3066
+ handle.addEventListener("pointercancel", onEnd);
3067
+ };
3068
+ const resizeByKey = (event) => {
3069
+ if (!onResize) return;
3070
+ const step = event.key === "ArrowRight" ? 8 : event.key === "ArrowLeft" ? -8 : 0;
3071
+ if (step === 0) return;
3072
+ event.preventDefault();
3073
+ event.stopPropagation();
3074
+ const current = cellRef.current?.getBoundingClientRect().width ?? minWidth;
3075
+ onResize(Math.max(minWidth, Math.round(current + step)));
3076
+ };
2760
3077
  useEffect(() => {
2761
3078
  if (!isDraggable || !columnId) return;
2762
3079
  const cell = cellRef.current;
@@ -2774,10 +3091,7 @@ function TableHeaderCell({
2774
3091
  dropTargetForElements({
2775
3092
  element: cell,
2776
3093
  canDrop: ({ source }) => isColumnDragData(source.data) && source.data.columnId !== columnId,
2777
- getData: ({ input, element }) => attachClosestEdge(
2778
- { ...data },
2779
- { input, element, allowedEdges: ["left", "right"] }
2780
- ),
3094
+ getData: ({ input, element }) => attachClosestEdge({ ...data }, { input, element, allowedEdges: ["left", "right"] }),
2781
3095
  getIsSticky: () => true,
2782
3096
  onDragEnter: ({ self }) => {
2783
3097
  const edge = extractClosestEdge(self.data);
@@ -2804,6 +3118,7 @@ function TableHeaderCell({
2804
3118
  const target = e.target;
2805
3119
  if (target.closest('[data-dnd-handle="true"]')) return;
2806
3120
  if (target.closest('[data-column-remove="true"]')) return;
3121
+ if (target.closest('[data-column-resize="true"]')) return;
2807
3122
  onSortToggle?.();
2808
3123
  };
2809
3124
  const handleKeyDown = (e) => {
@@ -2887,6 +3202,42 @@ function TableHeaderCell({
2887
3202
  dragState.edge === "left" ? "-left-px" : "-right-px"
2888
3203
  )
2889
3204
  }
3205
+ ) : null,
3206
+ onResize ? (
3207
+ // Sits astride the trailing separator: an 8px grab zone with a 2px
3208
+ // bar that lights up on hover, focus and while dragging. A focusable
3209
+ // separator is the ARIA pattern for a resizer (no semantic element
3210
+ // exists for it); its value is the column width when one is known.
3211
+ // biome-ignore lint/a11y/useSemanticElements: interactive vertical resizer — <hr> cannot be one
3212
+ /* @__PURE__ */ jsx(
3213
+ "span",
3214
+ {
3215
+ role: "separator",
3216
+ "aria-orientation": "vertical",
3217
+ "aria-label": `Resize column ${ariaLabel ?? "column"}`,
3218
+ "aria-valuemin": minWidth,
3219
+ "aria-valuenow": typeof width === "number" ? width : void 0,
3220
+ tabIndex: 0,
3221
+ "data-column-resize": "true",
3222
+ onPointerDown: startResize,
3223
+ onKeyDown: resizeByKey,
3224
+ onClick: (e) => e.stopPropagation(),
3225
+ className: cn(
3226
+ "group/resize absolute inset-y-0 -right-1 z-10 w-2 cursor-col-resize outline-none",
3227
+ "flex justify-center"
3228
+ ),
3229
+ children: /* @__PURE__ */ jsx(
3230
+ "span",
3231
+ {
3232
+ "aria-hidden": "true",
3233
+ className: cn(
3234
+ "h-full w-0.5 transition-colors",
3235
+ isResizing ? "bg-accent" : "bg-transparent group-hover/resize:bg-accent group-focus-visible/resize:bg-accent"
3236
+ )
3237
+ }
3238
+ )
3239
+ }
3240
+ )
2890
3241
  ) : null
2891
3242
  ]
2892
3243
  }
@@ -2940,12 +3291,27 @@ var tableScrollerStyles = tv({
2940
3291
  base: ["relative flex-1 min-h-0 overflow-auto", ...scrollbarChrome]
2941
3292
  });
2942
3293
  function TableScroller({ className, ref, ...props }) {
2943
- return /* @__PURE__ */ jsx("div", { role: "presentation", ref, ...props, className: cn(tableScrollerStyles(), className) });
3294
+ return /* @__PURE__ */ jsx(
3295
+ "div",
3296
+ {
3297
+ role: "presentation",
3298
+ ref,
3299
+ ...props,
3300
+ className: cn(tableScrollerStyles(), className)
3301
+ }
3302
+ );
2944
3303
  }
2945
3304
  var rowStripeBg = "bg-[color-mix(in_oklab,var(--color-ink)_var(--row-stripe),var(--color-bg))]";
2946
3305
  var rowHoverBg = "hover:bg-[color-mix(in_oklab,var(--color-ink)_var(--row-hover),var(--color-bg))]";
3306
+ var cellEditingRow = "has-[[data-cell-editing]]:z-[3]";
3307
+ var cellEditingCell = "has-[[data-cell-editing]]:z-[3] has-[[data-cell-editing]]:overflow-visible";
2947
3308
  var tableRowStyles = tv({
2948
- base: ["flex items-stretch", "border-b border-rule-2 last:border-b-0", "bg-bg"],
3309
+ base: [
3310
+ "relative flex items-stretch",
3311
+ "border-b border-rule-2 last:border-b-0",
3312
+ "bg-bg",
3313
+ cellEditingRow
3314
+ ],
2949
3315
  variants: {
2950
3316
  alt: {
2951
3317
  true: rowStripeBg,
@@ -2956,7 +3322,9 @@ var tableRowStyles = tv({
2956
3322
  false: rowHoverBg
2957
3323
  },
2958
3324
  absolute: {
2959
- true: "absolute left-0 right-0",
3325
+ // `top-0` pins the static position: a virtualized row must never
3326
+ // inherit a flow offset from a sibling that is in normal flow.
3327
+ true: "absolute top-0 left-0 right-0",
2960
3328
  false: ""
2961
3329
  }
2962
3330
  },
@@ -2987,11 +3355,12 @@ function TableRow({
2987
3355
  }
2988
3356
  var tableCellStyles = tv({
2989
3357
  base: [
2990
- "flex items-center shrink-0 min-w-0",
3358
+ "relative flex items-center shrink-0 min-w-0",
2991
3359
  "px-[14px]",
2992
3360
  "whitespace-nowrap overflow-hidden text-ellipsis",
2993
3361
  "font-sans text-[13px] tracking-[-0.005em]",
2994
- "text-ink-2"
3362
+ "text-ink-2",
3363
+ cellEditingCell
2995
3364
  ],
2996
3365
  variants: {
2997
3366
  align: {
@@ -3152,6 +3521,8 @@ function DataTable({
3152
3521
  enableColumnRemoval = false,
3153
3522
  onReorderColumn,
3154
3523
  onRemoveColumn,
3524
+ enableColumnResizing = false,
3525
+ onResizeColumn,
3155
3526
  variant = "panel",
3156
3527
  rowWrapper,
3157
3528
  extraRows,
@@ -3365,7 +3736,6 @@ function DataTable({
3365
3736
  const showEmpty = !query.isLoading && !query.error && tableRowCount === 0 && !query.isFetchingNextPage;
3366
3737
  const showInitialLoading = query.isLoading && tableRowCount === 0;
3367
3738
  const showError = !!query.error && tableRowCount === 0;
3368
- const showEndMarker = !query.isLoading && !query.isFetchingNextPage && !query.hasNextPage && tableRowCount > 0;
3369
3739
  const totalSize = virtualizer.getTotalSize();
3370
3740
  const virtualItems = virtualizer.getVirtualItems();
3371
3741
  const headerGroups = table.getHeaderGroups();
@@ -3376,84 +3746,108 @@ function DataTable({
3376
3746
  ...ariaLabel !== void 0 ? { "aria-label": ariaLabel } : {},
3377
3747
  variant,
3378
3748
  className: cn("flex-1 min-h-0", className),
3379
- children: /* @__PURE__ */ jsx(TableScroller, { ref: scrollerRef, children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col", style: { minWidth: minPanelWidth }, children: [
3380
- headerGroups.map((group) => /* @__PURE__ */ jsxs(TableHeader, { sticky: true, children: [
3381
- group.headers.map((header) => {
3382
- const meta = header.column.columnDef.meta;
3383
- const align = meta?.align ?? "left";
3384
- const sticky = meta?.sticky ?? false;
3385
- const headerSize = header.getSize() || void 0;
3386
- const headerNode = header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext());
3387
- const sorted = header.column.getIsSorted();
3388
- const direction = sorted === false ? null : sorted;
3389
- const canSort = header.column.getCanSort();
3390
- const handleSort = header.column.getToggleSortingHandler();
3391
- const ariaLabel2 = typeof header.column.columnDef.header === "string" ? header.column.columnDef.header : header.column.id;
3392
- return /* @__PURE__ */ jsx(
3393
- TableHeaderCell,
3394
- {
3395
- align,
3396
- ariaLabel: ariaLabel2,
3397
- draggable: enableColumnReordering && !!header.column.id,
3398
- ...header.column.id ? { columnId: header.column.id } : {},
3399
- ...headerSize !== void 0 ? { width: headerSize } : {},
3400
- ...sticky !== false ? { sticky } : {},
3401
- ...meta?.cellClassName ? { className: meta.cellClassName } : {},
3402
- ...canSort ? {
3403
- sortDirection: direction,
3404
- onSortToggle: () => handleSort?.({})
3405
- } : {},
3406
- ...enableColumnRemoval && onRemoveColumn && header.column.id ? { onRemove: () => onRemoveColumn(header.column.id) } : {},
3407
- children: headerNode
3408
- },
3409
- header.id
3410
- );
3411
- }),
3412
- /* @__PURE__ */ jsx(TableHeaderSpacer, {})
3413
- ] }, group.id)),
3414
- bodyBefore,
3415
- showInitialLoading ? loadingState ?? /* @__PURE__ */ jsx(DataTableSkeleton, { rowHeight }) : showError ? /* @__PURE__ */ jsx("div", { role: "rowgroup", children: errorState ?? (query.error ? /* @__PURE__ */ jsx(DataTableError, { error: query.error }) : null) }) : /* @__PURE__ */ jsxs("div", { role: "rowgroup", children: [
3416
- showEmpty ? emptyState ?? /* @__PURE__ */ jsx(DataTableEmpty, {}) : /* @__PURE__ */ jsx(
3417
- "div",
3418
- {
3419
- ref: listRef,
3420
- role: "presentation",
3421
- style: { height: totalSize, position: "relative", width: "100%" },
3422
- children: virtualItems.map((vRow) => {
3423
- const item = listItems[vRow.index];
3424
- if (!item) return null;
3425
- const rowStyle = {
3426
- // Items' `start` includes the virtualizer's scrollMargin
3427
- // (the list's offset below the sticky header); subtract
3428
- // it to position relative to the list container.
3429
- transform: `translateY(${vRow.start - listOffset}px)`,
3430
- height: vRow.size
3431
- };
3432
- if (item.kind === "group") {
3433
- return /* @__PURE__ */ jsx(GroupHeaderRow, { style: rowStyle, children: item.label }, `group:${item.id}`);
3434
- }
3749
+ children: /* @__PURE__ */ jsx(
3750
+ TableScroller,
3751
+ {
3752
+ ref: scrollerRef,
3753
+ className: cn("scroll-pt-[36px]", bodyAfter ? "scroll-pb-8" : void 0),
3754
+ children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col", style: { minWidth: minPanelWidth }, children: [
3755
+ headerGroups.map((group) => /* @__PURE__ */ jsxs(TableHeader, { sticky: true, children: [
3756
+ group.headers.map((header) => {
3757
+ const meta = header.column.columnDef.meta;
3758
+ const align = meta?.align ?? "left";
3759
+ const sticky = meta?.sticky ?? false;
3760
+ const headerSize = header.getSize() || void 0;
3761
+ const headerNode = header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext());
3762
+ const sorted = header.column.getIsSorted();
3763
+ const direction = sorted === false ? null : sorted;
3764
+ const canSort = header.column.getCanSort();
3765
+ const handleSort = header.column.getToggleSortingHandler();
3766
+ const ariaLabel2 = typeof header.column.columnDef.header === "string" ? header.column.columnDef.header : header.column.id;
3435
3767
  return /* @__PURE__ */ jsx(
3436
- DataRow,
3768
+ TableHeaderCell,
3437
3769
  {
3438
- row: item.row,
3439
- index: item.dataIndex,
3440
- zebra,
3441
- onRowClick,
3442
- style: rowStyle,
3443
- columnBorders,
3444
- ...rowWrapper ? { rowWrapper } : {}
3770
+ align,
3771
+ ariaLabel: ariaLabel2,
3772
+ draggable: enableColumnReordering && !!header.column.id,
3773
+ ...header.column.id ? { columnId: header.column.id } : {},
3774
+ ...headerSize !== void 0 ? { width: headerSize } : {},
3775
+ ...sticky !== false ? { sticky } : {},
3776
+ ...meta?.cellClassName ? { className: meta.cellClassName } : {},
3777
+ ...canSort ? {
3778
+ sortDirection: direction,
3779
+ onSortToggle: () => handleSort?.({})
3780
+ } : {},
3781
+ ...enableColumnRemoval && onRemoveColumn && header.column.id ? { onRemove: () => onRemoveColumn(header.column.id) } : {},
3782
+ ...enableColumnResizing && onResizeColumn && header.column.id ? {
3783
+ onResize: (width) => onResizeColumn({ id: header.column.id, width })
3784
+ } : {},
3785
+ children: headerNode
3445
3786
  },
3446
- item.row.id
3787
+ header.id
3447
3788
  );
3448
- })
3449
- }
3450
- ),
3451
- !showEmpty && /* @__PURE__ */ jsx("div", { ref: sentinelRef, "aria-hidden": "true", style: { height: 1, width: "100%" } }),
3452
- query.isFetchingNextPage ? /* @__PURE__ */ jsx(LoaderRow, {}) : null,
3453
- showEndMarker ? /* @__PURE__ */ jsx(EndMarker, { count: tableRowCount }) : null
3454
- ] }),
3455
- bodyAfter
3456
- ] }) })
3789
+ }),
3790
+ /* @__PURE__ */ jsx(TableHeaderSpacer, {})
3791
+ ] }, group.id)),
3792
+ bodyBefore,
3793
+ showInitialLoading ? loadingState ?? /* @__PURE__ */ jsx(DataTableSkeleton, { rowHeight }) : showError ? /* @__PURE__ */ jsx("div", { role: "rowgroup", children: errorState ?? (query.error ? /* @__PURE__ */ jsx(DataTableError, { error: query.error }) : null) }) : /* @__PURE__ */ jsxs("div", { role: "rowgroup", children: [
3794
+ showEmpty ? emptyState ?? /* @__PURE__ */ jsx(DataTableEmpty, {}) : /* @__PURE__ */ jsx(
3795
+ "div",
3796
+ {
3797
+ ref: listRef,
3798
+ role: "presentation",
3799
+ style: { height: totalSize, position: "relative", width: "100%" },
3800
+ children: virtualItems.map((vRow) => {
3801
+ const item = listItems[vRow.index];
3802
+ if (!item) return null;
3803
+ const rowStyle = {
3804
+ // Items' `start` includes the virtualizer's scrollMargin
3805
+ // (the list's offset below the sticky header); subtract
3806
+ // it to position relative to the list container.
3807
+ transform: `translateY(${vRow.start - listOffset}px)`,
3808
+ height: vRow.size
3809
+ };
3810
+ if (item.kind === "group") {
3811
+ return /* @__PURE__ */ jsx(GroupHeaderRow, { style: rowStyle, children: item.label }, `group:${item.id}`);
3812
+ }
3813
+ return /* @__PURE__ */ jsx(
3814
+ DataRow,
3815
+ {
3816
+ row: item.row,
3817
+ index: item.dataIndex,
3818
+ zebra,
3819
+ onRowClick,
3820
+ style: rowStyle,
3821
+ columnBorders,
3822
+ ...rowWrapper ? { rowWrapper } : {}
3823
+ },
3824
+ item.row.id
3825
+ );
3826
+ })
3827
+ }
3828
+ ),
3829
+ !showEmpty && /* @__PURE__ */ jsx("div", { ref: sentinelRef, "aria-hidden": "true", style: { height: 1, width: "100%" } }),
3830
+ query.isFetchingNextPage ? /* @__PURE__ */ jsx(LoaderRow, {}) : null
3831
+ ] }),
3832
+ bodyAfter ? (
3833
+ // Sticky to the scroller's bottom edge; `bg-bg` covers the rows
3834
+ // sliding underneath. z-[4] — like the sticky header — stays above an
3835
+ // editing row (z-[3]), so an editor scrolled under the strip is
3836
+ // covered rather than drawn over it.
3837
+ /* @__PURE__ */ jsx(
3838
+ "div",
3839
+ {
3840
+ className: cn(
3841
+ "sticky bottom-0 z-[4] flex items-center h-8 px-0.5",
3842
+ "border-t border-rule-2 bg-bg"
3843
+ ),
3844
+ children: bodyAfter
3845
+ }
3846
+ )
3847
+ ) : null
3848
+ ] })
3849
+ }
3850
+ )
3457
3851
  }
3458
3852
  );
3459
3853
  }
@@ -3553,23 +3947,6 @@ function LoaderRow() {
3553
3947
  }
3554
3948
  );
3555
3949
  }
3556
- function EndMarker({ count }) {
3557
- return /* @__PURE__ */ jsxs(
3558
- "div",
3559
- {
3560
- className: cn(
3561
- "flex items-center justify-center",
3562
- "h-8 font-mono text-[10.5px] uppercase tracking-[0.08em]",
3563
- "text-ink-4"
3564
- ),
3565
- children: [
3566
- "end \xB7 ",
3567
- count.toLocaleString(),
3568
- " rows"
3569
- ]
3570
- }
3571
- );
3572
- }
3573
3950
 
3574
3951
  // src/components/data-table/make-actions-column.ts
3575
3952
  function makeActionsColumn({
@@ -4157,7 +4534,9 @@ function MultiSelect({
4157
4534
  placement: "bottom start",
4158
4535
  className: cn(
4159
4536
  "w-[var(--trigger-width)]",
4160
- "rounded-md border border-rule bg-surface p-1",
4537
+ // The popover element itself takes focus when opened programmatically
4538
+ // (defaultOpen) — no browser outline around the whole panel.
4539
+ "outline-none rounded-md border border-rule bg-surface p-1",
4161
4540
  "shadow-[var(--shadow-raised)]",
4162
4541
  "entering:animate-in entering:fade-in-0 entering:slide-in-from-top-1",
4163
4542
  "exiting:animate-out exiting:fade-out-0 exiting:slide-out-to-top-1"
@@ -4182,6 +4561,7 @@ function MultiSelect({
4182
4561
  {
4183
4562
  "aria-label": typeof label === "string" ? label : "Options",
4184
4563
  selectionMode: "multiple",
4564
+ escapeKeyBehavior: "none",
4185
4565
  selectedKeys: selectedSet,
4186
4566
  onSelectionChange: (keys) => {
4187
4567
  if (keys === "all") {
@@ -4204,11 +4584,10 @@ function MultiSelect({
4204
4584
  "cursor-pointer outline-none select-none",
4205
4585
  "data-[hovered]:bg-surface-2 data-[hovered]:text-ink",
4206
4586
  "data-[focused]:bg-surface-2 data-[focused]:text-ink",
4207
- "data-[selected]:bg-surface-3 data-[selected]:text-ink",
4208
4587
  "data-[disabled]:opacity-45 data-[disabled]:cursor-not-allowed"
4209
4588
  ),
4210
4589
  children: [
4211
- /* @__PURE__ */ jsx(Checkmark, {}),
4590
+ /* @__PURE__ */ jsx(ListboxCheck, {}),
4212
4591
  /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: option.content ?? option.label })
4213
4592
  ]
4214
4593
  },
@@ -4269,42 +4648,6 @@ function Chip({
4269
4648
  }
4270
4649
  );
4271
4650
  }
4272
- function Checkmark() {
4273
- return /* @__PURE__ */ jsx(
4274
- "span",
4275
- {
4276
- "aria-hidden": true,
4277
- className: cn(
4278
- "relative inline-flex items-center justify-center shrink-0",
4279
- "size-[16px] rounded-[3px] border border-ink-4 bg-surface",
4280
- "transition-[background-color,border-color] duration-150",
4281
- "group-data-[selected]:bg-ink group-data-[selected]:border-ink"
4282
- ),
4283
- children: /* @__PURE__ */ jsx(
4284
- "svg",
4285
- {
4286
- viewBox: "0 0 12 12",
4287
- className: cn(
4288
- "size-3 text-surface transition-opacity duration-150",
4289
- "opacity-0 group-data-[selected]:opacity-100"
4290
- ),
4291
- "aria-hidden": true,
4292
- children: /* @__PURE__ */ jsx(
4293
- "path",
4294
- {
4295
- d: "M2.5 6.25 5 8.75 9.5 3.75",
4296
- stroke: "currentColor",
4297
- strokeWidth: "1.6",
4298
- strokeLinecap: "round",
4299
- strokeLinejoin: "round",
4300
- fill: "none"
4301
- }
4302
- )
4303
- }
4304
- )
4305
- }
4306
- );
4307
- }
4308
4651
  var SEARCH_DEBOUNCE_MS = 250;
4309
4652
  function RecipientSelect({
4310
4653
  connectionId,
@@ -6159,13 +6502,21 @@ function TagInput({
6159
6502
  }) {
6160
6503
  const styles4 = fieldControlStyles({ size, invalid: isInvalid, appearance });
6161
6504
  const focusWithin = appearance === "cell" ? "focus-within:border-accent focus-within:shadow-none" : "focus-within:border-accent focus-within:shadow-[var(--shadow-focus)]";
6162
- const minHeight = appearance === "cell" ? "min-h-[29px]" : fieldMinHeight[size];
6505
+ const isCell = appearance === "cell";
6163
6506
  const inputRef = useRef(null);
6507
+ const chipsRef = useRef(null);
6508
+ const hasWrapped = useChipRowWrap(chipsRef, isCell, JSON.stringify(value));
6164
6509
  useEffect(() => {
6165
6510
  if (autoFocus) inputRef.current?.focus();
6166
6511
  }, []);
6167
6512
  const inputId = useId();
6168
6513
  const [draft, setDraft] = useState("");
6514
+ useLayoutEffect(() => {
6515
+ const input = inputRef.current;
6516
+ if (!isCell || !input || document.activeElement !== input) return;
6517
+ if (typeof input.scrollIntoView !== "function") return;
6518
+ input.scrollIntoView({ block: "nearest" });
6519
+ }, [isCell, value]);
6169
6520
  const commit = () => {
6170
6521
  const v = draft.trim();
6171
6522
  if (!v) return;
@@ -6182,7 +6533,10 @@ function TagInput({
6182
6533
  const onKeyDown2 = (e) => {
6183
6534
  if (e.key === "Enter" || e.key === ",") {
6184
6535
  e.preventDefault();
6185
- commit();
6536
+ if (draft.trim()) {
6537
+ e.stopPropagation();
6538
+ commit();
6539
+ }
6186
6540
  return;
6187
6541
  }
6188
6542
  if (e.key === "Backspace" && !draft && value.length > 0) {
@@ -6192,85 +6546,78 @@ function TagInput({
6192
6546
  };
6193
6547
  return /* @__PURE__ */ jsxs("div", { className: cn("grid grid-cols-1 gap-1.5", className), children: [
6194
6548
  label ? /* @__PURE__ */ jsx("label", { htmlFor: inputId, className: fieldLabelStyles, children: label }) : null,
6195
- /* @__PURE__ */ jsxs(
6549
+ /* @__PURE__ */ jsx(
6196
6550
  "div",
6197
6551
  {
6198
6552
  "data-invalid": isInvalid || void 0,
6199
6553
  "data-disabled": isDisabled || void 0,
6200
6554
  className: cn(
6201
6555
  styles4.box(),
6202
- "flex flex-wrap items-center gap-1",
6203
- minHeight,
6204
- fieldChipPadding[size],
6556
+ "flex",
6557
+ // Cell editor: one table row (29px under a 32px row) while the chips
6558
+ // fit on a line; two rows (64px), popping out of the row, once they
6559
+ // wrap — the chips then scroll inside. Field mode grows with its
6560
+ // content.
6561
+ isCell ? hasWrapped ? "h-[64px] p-1" : "h-[29px] px-1 py-0" : [fieldChipPadding[size], fieldMinHeight[size]],
6205
6562
  focusWithin
6206
6563
  ),
6207
- children: [
6208
- value.map((tag, idx) => (
6209
- // tags are kept unique on commit, so the value alone is a stable key
6210
- /* @__PURE__ */ jsxs(
6211
- "span",
6212
- {
6213
- className: cn(
6214
- "inline-flex items-center gap-1",
6215
- "px-1.5 py-0.5 rounded-sm bg-surface-3",
6216
- "font-mono text-[12px] text-ink-2"
6217
- ),
6218
- children: [
6219
- /* @__PURE__ */ jsx("span", { className: "max-w-[10rem] truncate", children: tag }),
6220
- /* @__PURE__ */ jsx(
6221
- "button",
6222
- {
6223
- type: "button",
6224
- "aria-label": `Remove ${tag}`,
6225
- disabled: isDisabled,
6226
- onClick: () => remove(idx),
6227
- className: cn(
6228
- "inline-flex items-center justify-center",
6229
- "size-3.5 rounded-full text-ink-3 hover:text-ink hover:bg-surface-2",
6230
- "cursor-pointer outline-none",
6231
- "focus-visible:shadow-[var(--shadow-focus)]",
6232
- "disabled:opacity-45 disabled:cursor-not-allowed"
6233
- ),
6234
- children: /* @__PURE__ */ jsx("svg", { "aria-hidden": true, viewBox: "0 0 10 10", className: "size-2", fill: "none", children: /* @__PURE__ */ jsx(
6235
- "path",
6236
- {
6237
- d: "M2 2l6 6M8 2l-6 6",
6238
- stroke: "currentColor",
6239
- strokeWidth: "1.4",
6240
- strokeLinecap: "round"
6241
- }
6242
- ) })
6243
- }
6564
+ children: /* @__PURE__ */ jsxs(
6565
+ "div",
6566
+ {
6567
+ ref: chipsRef,
6568
+ className: cn(
6569
+ "flex flex-1 flex-wrap items-center gap-1.5 min-w-0",
6570
+ isCell && (hasWrapped ? "content-start overflow-y-auto" : "content-center overflow-hidden")
6571
+ ),
6572
+ children: [
6573
+ value.map((tag, idx) => (
6574
+ // tags are kept unique on commit, so the value alone is a stable key
6575
+ /* @__PURE__ */ jsx(
6576
+ Badge,
6577
+ {
6578
+ variant: "active",
6579
+ shape: "rounded",
6580
+ hasDot: false,
6581
+ className: "shrink-0",
6582
+ actions: /* @__PURE__ */ jsx(
6583
+ BadgeAction,
6584
+ {
6585
+ label: `Remove ${tag}`,
6586
+ onPress: () => remove(idx),
6587
+ isDisabled
6588
+ }
6589
+ ),
6590
+ children: /* @__PURE__ */ jsx("span", { className: "max-w-[10rem] truncate", children: tag })
6591
+ },
6592
+ tag
6593
+ )
6594
+ )),
6595
+ /* @__PURE__ */ jsx(
6596
+ "input",
6597
+ {
6598
+ ref: inputRef,
6599
+ id: inputId,
6600
+ type: inputType,
6601
+ value: draft,
6602
+ onChange: (e) => setDraft(e.target.value),
6603
+ onKeyDown: onKeyDown2,
6604
+ onBlur: commit,
6605
+ disabled: isDisabled,
6606
+ placeholder: value.length === 0 ? placeholder : "",
6607
+ className: cn(
6608
+ "flex-1 min-w-[6rem] bg-transparent outline-none",
6609
+ "font-sans text-ink placeholder:text-ink-4",
6610
+ fieldTextStyles[size],
6611
+ // No vertical padding — wrapper's p-1.5 + min-h-[size] govern the
6612
+ // overall height so it matches Input/Select at the same size.
6613
+ "px-1.5",
6614
+ "disabled:cursor-not-allowed disabled:opacity-45"
6244
6615
  )
6245
- ]
6246
- },
6247
- tag
6248
- )
6249
- )),
6250
- /* @__PURE__ */ jsx(
6251
- "input",
6252
- {
6253
- ref: inputRef,
6254
- id: inputId,
6255
- type: inputType,
6256
- value: draft,
6257
- onChange: (e) => setDraft(e.target.value),
6258
- onKeyDown: onKeyDown2,
6259
- onBlur: commit,
6260
- disabled: isDisabled,
6261
- placeholder: value.length === 0 ? placeholder : "",
6262
- className: cn(
6263
- "flex-1 min-w-[6rem] bg-transparent outline-none",
6264
- "font-sans text-ink placeholder:text-ink-4",
6265
- fieldTextStyles[size],
6266
- // No vertical padding — wrapper's p-1.5 + min-h-[size] govern the
6267
- // overall height so it matches Input/Select at the same size.
6268
- "px-1.5",
6269
- "disabled:cursor-not-allowed disabled:opacity-45"
6616
+ }
6270
6617
  )
6271
- }
6272
- )
6273
- ]
6618
+ ]
6619
+ }
6620
+ )
6274
6621
  }
6275
6622
  ),
6276
6623
  description ? /* @__PURE__ */ jsx("p", { className: fieldHintStyles, children: description }) : null,
@@ -8170,32 +8517,7 @@ function ToastRegion({ queue, className }) {
8170
8517
  }
8171
8518
  );
8172
8519
  }
8173
- function TooltipTrigger({
8174
- delay = 500,
8175
- closeDelay = 0,
8176
- ...props
8177
- }) {
8178
- return /* @__PURE__ */ jsx(TooltipTrigger$1, { delay, closeDelay, ...props });
8179
- }
8180
- function Tooltip({ className, offset = 6, ...props }) {
8181
- return /* @__PURE__ */ jsx(
8182
- Tooltip$1,
8183
- {
8184
- ...props,
8185
- offset,
8186
- className: (renderProps) => cn(
8187
- "z-50 max-w-xs",
8188
- "rounded-md border border-rule bg-surface shadow-[var(--shadow-raised)]",
8189
- "px-2.5 py-1.5",
8190
- "font-mono text-[11.5px] text-ink-2",
8191
- "entering:animate-in entering:fade-in-0 entering:slide-in-from-top-1 entering:duration-150",
8192
- "exiting:animate-out exiting:fade-out-0 exiting:duration-100",
8193
- typeof className === "function" ? className(renderProps) : className
8194
- )
8195
- }
8196
- );
8197
- }
8198
8520
 
8199
- export { ATTRIBUTE_TYPES, ATTRIBUTE_TYPE_ICONS, AttachmentChip, AttributePicker, AttributeType, AttributeTypeIcon, Avatar, AvatarGroup, Badge, BadgeMultiSelect, BooleanCell, Breadcrumbs, COLUMN_DRAG_KIND, CONVERSATION_BRIEF_OUTCOME_LABEL, Card, CardBody, CardContent, CardEyebrow, CardFeatureTitle, CardFooter, CardHeader, CardTitle, ChannelIcon, Checkbox, CheckboxGroup, ChipValue, ColorInput, Combobox, Command, CommandHint, ConversationBriefCard, ConversationBriefPreview, ConversationBriefReview, CurrencyInput, DRAFT_OUTCOME_LABEL, DataTable, DataTableEmpty, DataTableError, DataTableSkeleton, DateCell, DatePicker, DateTimeCell, DateTimePicker, DraftMessageCard, DraftMessageComposer, DraftMessageReview, EmptyState, ErrorAnimation, FIELD_LABEL_SEPARATOR, FieldCaret, FileField, FileViewer, FileViewerFallbackCard, FilterRow, FilterValueInput, IconPicker, ImageAttachment, ImageLightbox, Input, Kbd, LinkCell, MAX_COMPOSER_ATTACHMENT_BYTES, Menu, MenuItem, MenuPopover, MenuTrigger, MessageComposer, Modal, ModalBody, ModalContent, ModalDescription, ModalFooter, ModalHeader, ModalTitle, MultiSelect, NavGroup, NavItem, NavSection, OPERATORS, OperatorPicker, Orb, PROTECTED_COMBOS, Popover, PopoverContent, PopoverPanelBody, PopoverPanelFooter, PopoverPanelHeader, PressableCard, ProgressBar, Radio, RadioGroup, RecipientSelect, RecordFilterBuilderPanel, RecordFilterButton, RecordFilterChips, RecordFilterEditor, RenderChipItemProvider, RenderValueInputProvider, SELECTION_COLUMN_ID, SegmentedControl, Select, Separator, SubmenuTrigger, SuccessAnimation, Switch, Table, TableBody, TableCell, TableHeader, TableHeaderCell, TableHeaderSpacer, TableRow, TableScroller, Tabs, TagInput, Textarea, TimeField, ToastRegion, Tooltip, TooltipTrigger, TruncateCell, __resetDraftMessageActions, __resetDraftMessageOutcomes, addLeaf as addFilterLeaf, addPendingLeaf, addSubgroup, arityOf, attachmentChipStyles, avatarGroupStyles, avatarStyles, badgeStyles, cardStyles2 as cardStyles, channelIcon, contactPhoneNumbers, conversationBriefCardStyles, conversationBriefOutcomeFor, countLeaves, createConversationBriefClient, createDraftMessageClient, createToastQueue, defaultValueFor, draftMessageCardStyles, draftMessageComposerStyles, draftOutcomeFor, emptyStateStyles, fieldControlStyles, fieldErrorStyles, fieldHintStyles, fieldLabelStyles, fileViewerFallbackCardStyles, fileViewerStyles, formatFileSize, formatValue, iconForContentType, imageAttachmentStyles, imageLightboxStyles, isColumnDragData, isDialableConversationBrief, isDraftReviewOutcome, isEditableTarget, isPreviewableImageName, joinFieldLabel, makeActionsColumn, messageComposerStyles, namesOf, navItemStyles, normalizeHexColor, operatorById, operatorsForType, parseCombo, progressBarStyles, publishDraftMessageOutcome, rebaseRowOnFieldChange, rebaseRowOnOperatorChange, registerDraftMessageActions, removeLeaf, removeSubgroup, replaceLeaf, resolveLucideIcon, resolveLucideIconName, segmentedControlStyles, setJoiner, subscribeDraftMessageOutcome, summaryFor, tableCellStyles, tableHeaderCellStyles, tabsStyles, terminalKindFor, toastStyles, updateLeaf, updateSubgroup, useAttributeTypeIcon, useCommand, useDiscoveryActive, useDraftMessageActions, useDraftMessageOutcome, useEscapeKey, useInfiniteScrollSentinel, useRenderChipItem, useRenderValueInput, useShowDetails, viewerKindForContentType, wrapLeafInGroup };
8521
+ export { ATTRIBUTE_TYPES, ATTRIBUTE_TYPE_ICONS, AttachmentChip, AttributePicker, AttributeType, AttributeTypeIcon, Avatar, AvatarGroup, Badge, BadgeAction, BadgeGroup, BadgeMultiSelect, BooleanCell, Breadcrumbs, COLUMN_DRAG_KIND, CONVERSATION_BRIEF_OUTCOME_LABEL, Card, CardBody, CardContent, CardEyebrow, CardFeatureTitle, CardFooter, CardHeader, CardTitle, ChannelIcon, Checkbox, CheckboxGroup, ChipValue, ColorInput, Combobox, Command, CommandHint, ConversationBriefCard, ConversationBriefPreview, ConversationBriefReview, CurrencyInput, DRAFT_OUTCOME_LABEL, DataTable, DataTableEmpty, DataTableError, DataTableSkeleton, DateCell, DatePicker, DateTimeCell, DateTimePicker, DraftMessageCard, DraftMessageComposer, DraftMessageReview, EmptyState, ErrorAnimation, FIELD_LABEL_SEPARATOR, FieldCaret, FileField, FileViewer, FileViewerFallbackCard, FilterRow, FilterValueInput, IconPicker, ImageAttachment, ImageLightbox, Input, Kbd, LinkCell, MAX_COMPOSER_ATTACHMENT_BYTES, Menu, MenuItem, MenuPopover, MenuTrigger, MessageComposer, Modal, ModalBody, ModalContent, ModalDescription, ModalFooter, ModalHeader, ModalTitle, MultiSelect, NavGroup, NavItem, NavSection, OPERATORS, OperatorPicker, Orb, PROTECTED_COMBOS, Popover, PopoverContent, PopoverPanelBody, PopoverPanelFooter, PopoverPanelHeader, PressableCard, ProgressBar, Radio, RadioGroup, RecipientSelect, RecordFilterBuilderPanel, RecordFilterButton, RecordFilterChips, RecordFilterEditor, RenderChipItemProvider, RenderValueInputProvider, SELECTION_COLUMN_ID, SegmentedControl, Select, Separator, SubmenuTrigger, SuccessAnimation, Switch, Table, TableBody, TableCell, TableHeader, TableHeaderCell, TableHeaderSpacer, TableRow, TableScroller, Tabs, TagInput, Textarea, TimeField, ToastRegion, Tooltip, TooltipTrigger, TruncateCell, __resetDraftMessageActions, __resetDraftMessageOutcomes, addLeaf as addFilterLeaf, addPendingLeaf, addSubgroup, arityOf, attachmentChipStyles, avatarGroupStyles, avatarStyles, badgeGroupOverflowStyles, badgeGroupStyles, badgeStyles, cardStyles2 as cardStyles, channelIcon, contactPhoneNumbers, conversationBriefCardStyles, conversationBriefOutcomeFor, countLeaves, createConversationBriefClient, createDraftMessageClient, createToastQueue, defaultValueFor, draftMessageCardStyles, draftMessageComposerStyles, draftOutcomeFor, emptyStateStyles, fieldControlStyles, fieldErrorStyles, fieldHintStyles, fieldLabelStyles, fileViewerFallbackCardStyles, fileViewerStyles, fitBadgeCount, formatFileSize, formatValue, iconForContentType, imageAttachmentStyles, imageLightboxStyles, isColumnDragData, isDialableConversationBrief, isDraftReviewOutcome, isEditableTarget, isPreviewableImageName, joinFieldLabel, makeActionsColumn, messageComposerStyles, namesOf, navItemStyles, normalizeHexColor, operatorById, operatorsForType, parseCombo, progressBarStyles, publishDraftMessageOutcome, rebaseRowOnFieldChange, rebaseRowOnOperatorChange, registerDraftMessageActions, removeLeaf, removeSubgroup, replaceLeaf, resolveLucideIcon, resolveLucideIconName, segmentedControlStyles, setJoiner, subscribeDraftMessageOutcome, summaryFor, tableCellStyles, tableHeaderCellStyles, tabsStyles, terminalKindFor, toastStyles, updateLeaf, updateSubgroup, useAttributeTypeIcon, useCommand, useDiscoveryActive, useDraftMessageActions, useDraftMessageOutcome, useEscapeKey, useInfiniteScrollSentinel, useRenderChipItem, useRenderValueInput, useShowDetails, viewerKindForContentType, wrapLeafInGroup };
8200
8522
  //# sourceMappingURL=index.js.map
8201
8523
  //# sourceMappingURL=index.js.map