@crispy-seed/data-table 0.11.0 → 0.11.1

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.cjs CHANGED
@@ -987,15 +987,33 @@ function sameCell$1(a, id, key) {
987
987
  * first captured) nor keeps a ghost entry for one that was removed. Known keys keep
988
988
  * their position from `order`; any key in `keys` missing from `order` is appended, in
989
989
  * `keys`' own order; any entry in `order` no longer in `keys` is dropped.
990
- */ function reconcileColumnOrder(order, keys) {
990
+ *
991
+ * `actionsKey` (the styled layer's `actions`-flagged column, e.g. the row-menu `⋯`
992
+ * column) is kept LAST no matter what -- without this, a brand-new column added to
993
+ * `columns` after the table first mounted got appended past it (the "any key in `keys`
994
+ * missing from `order` is appended" rule above has no notion of "past the end" vs. "at
995
+ * the very end"), stranding it in the middle of the table since `stickyLast`/`actions`
996
+ * only sticks the actual trailing column. A real consumer hit this: their newly-added
997
+ * column landed to the right of the already-pinned-sticky `actions` column, covered by
998
+ * it under horizontal scroll. Column-layout's own `pinnedCellProps` independently
999
+ * targets the `actions` column by FLAG rather than by position (belt and suspenders --
1000
+ * see that file's own doc comment), but this still keeps the VISUAL order sane so nothing
1001
+ * renders hidden behind the sticky cell in the first place.
1002
+ */ function reconcileColumnOrder(order, keys, actionsKey) {
991
1003
  const keySet = new Set(keys);
992
1004
  const known = order.filter((key)=>keySet.has(key));
993
1005
  const knownSet = new Set(known);
994
1006
  const added = keys.filter((key)=>!knownSet.has(key));
995
- return [
1007
+ const merged = [
996
1008
  ...known,
997
1009
  ...added
998
1010
  ];
1011
+ if (actionsKey === undefined || !keySet.has(actionsKey)) return merged;
1012
+ const withoutActions = merged.filter((key)=>key !== actionsKey);
1013
+ return [
1014
+ ...withoutActions,
1015
+ actionsKey
1016
+ ];
999
1017
  }
1000
1018
  /**
1001
1019
  * Moves `key` one step toward `direction` in `order` -- a no-op (same array, well,
@@ -1174,7 +1192,7 @@ const EMPTY_RECORD = {};
1174
1192
  * state this hook already owns, not independent state
1175
1193
  * of their own.
1176
1194
  */ function useDataTableColumns(props) {
1177
- const { columnKeys = EMPTY_STRINGS, columnOrder: columnOrderProp, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys: hiddenProp, defaultHiddenColumnKeys = EMPTY_STRINGS, onHiddenColumnKeysChange, columnWidths: widthsProp, defaultColumnWidths = EMPTY_RECORD, onColumnWidthsChange, columnLabels: labelsProp, defaultColumnLabels = EMPTY_RECORD, onColumnLabelsChange, pinnedColumnKeys: pinnedProp, defaultPinnedColumnKeys = EMPTY_STRINGS, onPinnedColumnKeysChange } = props;
1195
+ const { columnKeys = EMPTY_STRINGS, actionsColumnKey, columnOrder: columnOrderProp, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys: hiddenProp, defaultHiddenColumnKeys = EMPTY_STRINGS, onHiddenColumnKeysChange, columnWidths: widthsProp, defaultColumnWidths = EMPTY_RECORD, onColumnWidthsChange, columnLabels: labelsProp, defaultColumnLabels = EMPTY_RECORD, onColumnLabelsChange, pinnedColumnKeys: pinnedProp, defaultPinnedColumnKeys = EMPTY_STRINGS, onPinnedColumnKeysChange } = props;
1178
1196
  const [rawOrder, setRawOrder] = useControllableState$1({
1179
1197
  prop: columnOrderProp,
1180
1198
  defaultProp: defaultColumnOrder ?? columnKeys,
@@ -1185,9 +1203,10 @@ const EMPTY_RECORD = {};
1185
1203
  // consumer persisted before a new column existed must still show that new column
1186
1204
  // (appended), and one holding a column that got removed from `columns` must not
1187
1205
  // render a ghost header for it.
1188
- const columnOrder = React.useMemo(()=>reconcileColumnOrder(rawOrder ?? columnKeys, columnKeys), [
1206
+ const columnOrder = React.useMemo(()=>reconcileColumnOrder(rawOrder ?? columnKeys, columnKeys, actionsColumnKey), [
1189
1207
  rawOrder,
1190
- columnKeys
1208
+ columnKeys,
1209
+ actionsColumnKey
1191
1210
  ]);
1192
1211
  const [hiddenColumnKeys = EMPTY_STRINGS, setHiddenColumnKeys] = useControllableState$1({
1193
1212
  prop: hiddenProp,
@@ -2169,7 +2188,7 @@ function useDataTableRowContext({ strict = true } = {}) {
2169
2188
  return context;
2170
2189
  }
2171
2190
  const DataTableRoot = /*#__PURE__*/ React.forwardRef((props, ref)=>{
2172
- const { sort, defaultSort, onSortChange, rowIds, selectable, selectedIds, defaultSelectedIds, onSelectionChange, query, defaultQuery, onQueryChange, toolbarSorts, defaultToolbarSorts, onToolbarSortsChange, toolbarFilters, defaultToolbarFilters, onToolbarFiltersChange, columnKeys, columnOrder, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys, defaultHiddenColumnKeys, onHiddenColumnKeysChange, columnWidths, defaultColumnWidths, onColumnWidthsChange, columnLabels, defaultColumnLabels, onColumnLabelsChange, pinnedColumnKeys, defaultPinnedColumnKeys, onPinnedColumnKeysChange, columnCalcOps, defaultColumnCalcOps, onColumnCalcOpsChange, onEdit, onLoadMore, hasMore, loadingMore, ...otherProps } = props;
2191
+ const { sort, defaultSort, onSortChange, rowIds, selectable, selectedIds, defaultSelectedIds, onSelectionChange, query, defaultQuery, onQueryChange, toolbarSorts, defaultToolbarSorts, onToolbarSortsChange, toolbarFilters, defaultToolbarFilters, onToolbarFiltersChange, columnKeys, actionsColumnKey, columnOrder, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys, defaultHiddenColumnKeys, onHiddenColumnKeysChange, columnWidths, defaultColumnWidths, onColumnWidthsChange, columnLabels, defaultColumnLabels, onColumnLabelsChange, pinnedColumnKeys, defaultPinnedColumnKeys, onPinnedColumnKeysChange, columnCalcOps, defaultColumnCalcOps, onColumnCalcOpsChange, onEdit, onLoadMore, hasMore, loadingMore, ...otherProps } = props;
2173
2192
  const api = useDataTable({
2174
2193
  sort,
2175
2194
  defaultSort,
@@ -2189,6 +2208,7 @@ const DataTableRoot = /*#__PURE__*/ React.forwardRef((props, ref)=>{
2189
2208
  defaultToolbarFilters,
2190
2209
  onToolbarFiltersChange,
2191
2210
  columnKeys,
2211
+ actionsColumnKey,
2192
2212
  columnOrder,
2193
2213
  defaultColumnOrder,
2194
2214
  onColumnOrderChange,
@@ -3533,10 +3553,25 @@ function RatingCell({ value, maxRating = 5 }) {
3533
3553
  width,
3534
3554
  pinned,
3535
3555
  pinOffset,
3536
- pinnedEdge: false
3556
+ pinnedEdge: false,
3557
+ stickyLastTarget: false
3537
3558
  };
3538
3559
  });
3539
3560
  if (pinnedCount > 0) entries[pinnedCount - 1].pinnedEdge = true;
3561
+ // `stickyLast` sticks the `actions`-flagged column specifically -- not whichever
3562
+ // column merely landed last in render order. Without this, `reconcileColumnOrder`
3563
+ // appending a brand-new column past `actions` (this file's own `<colgroup>` doc
3564
+ // comment on why order must stay in lockstep) silently moved the sticky-right
3565
+ // treatment onto that new column instead, stranding `actions` in the middle of the
3566
+ // table -- a real consumer hit this. Falls back to the genuine last entry when no
3567
+ // column is flagged `actions` at all, matching `stickyLast`'s original single-
3568
+ // column behavior.
3569
+ if (entries.length > 0) {
3570
+ const actionsColumn = columns.find((column)=>column.actions);
3571
+ const actionsIndex = actionsColumn ? entries.findIndex((entry)=>entry.column.key === actionsColumn.key) : -1;
3572
+ const stickyLastIndex = actionsIndex !== -1 ? actionsIndex : entries.length - 1;
3573
+ entries[stickyLastIndex].stickyLastTarget = true;
3574
+ }
3540
3575
  return entries;
3541
3576
  }, [
3542
3577
  columns,
@@ -3576,9 +3611,12 @@ function RatingCell({ value, maxRating = 5 }) {
3576
3611
  }
3577
3612
  /** Sticky positioning for one DATA cell (header, body, or ColumnCalc footer) -- pinned-
3578
3613
  * left (this column's own `pinOffset`, `data-sticky-edge` only on the group's
3579
- * trailing-most member) takes priority over sticky-right (`stickyLast`, always just
3580
- * the last VISIBLE column, e.g. an `actions` column) since a column can only stick to
3581
- * one edge. */ function pinnedCellProps(entry, stickyLast, isLast) {
3614
+ * trailing-most member) takes priority over sticky-right (`stickyLast`, the
3615
+ * `actions`-flagged column when one exists, else the genuine last VISIBLE column)
3616
+ * since a column can only stick to one edge. Reads `entry.stickyLastTarget`
3617
+ * (`useEffectiveColumns`) rather than an `isLast` index the caller computes itself --
3618
+ * that index is just "last in render order", which is exactly what stranded a
3619
+ * reordered-past `actions` column in the first place. */ function pinnedCellProps(entry, stickyLast) {
3582
3620
  if (entry.pinned) {
3583
3621
  return {
3584
3622
  "data-sticky": "",
@@ -3590,7 +3628,7 @@ function RatingCell({ value, maxRating = 5 }) {
3590
3628
  }
3591
3629
  };
3592
3630
  }
3593
- if (stickyLast && isLast) {
3631
+ if (stickyLast && entry.stickyLastTarget) {
3594
3632
  return {
3595
3633
  "data-sticky": "",
3596
3634
  "data-sticky-edge-end": "",
@@ -4749,26 +4787,87 @@ function ChipListEditor({ column, value, pending, onCommit, classNames }) {
4749
4787
  }));
4750
4788
  }
4751
4789
  ////////////////////////////////////////////////////////////////////////////////////
4752
- // -- Feature C (#12): cell-expand "show full content" overlay -- an anchored box
4753
- // (position:absolute over the cell it belongs to, grows in HEIGHT, elevated)
4754
- // that wraps whatever a truncated cell would otherwise show inline: an editable
4755
- // column's OWN editor node (unchanged, just relocated into this box instead of
4756
- // rendered flush in the 36px row) for the editable case, or the plain full value
4757
- // for a read-only one. `body-cells.tsx` decides WHEN to render this (a click on a
4758
- // cell whose content is actually truncated, `truncate.ts`'s `isCellTruncated`);
4759
- // this component only supplies the box.
4760
- function CellExpandOverlay({ className, children }) {
4761
- return(// `stopPropagation` -- an overlay sits on top of (and past) its own cell/row;
4790
+ // -- Feature C (#12): cell-expand "show full content" overlay -- a box that wraps
4791
+ // whatever a truncated cell would otherwise show inline: an editable column's OWN
4792
+ // editor node (unchanged, just relocated into this box instead of rendered flush in
4793
+ // the 36px row) for the editable case, or the plain full value for a read-only one.
4794
+ // `body-cells.tsx` decides WHEN to render this (a click on a cell whose content is
4795
+ // actually truncated, `truncate.ts`'s `isCellTruncated`); this component only
4796
+ // supplies the box.
4797
+ //
4798
+ // Portals to `document.body` (crispy-seed#1094's own pinned-column follow-up,
4799
+ // ported from the chip-list editor's popover, `body-cells.tsx`'s
4800
+ // `isChipListEditable` -- that one shipped this exact fix in 0.10.3) rather than
4801
+ // positioning `absolute` against its own cell: a `position: sticky` `<td>` clips ANY
4802
+ // descendant that grows past its own box, in every browser tested, regardless of
4803
+ // `overflow`/`z-index` on anything in between, so a pinned column's own truncated
4804
+ // cell used to clip this overlay's grown content instead of showing it in full. A
4805
+ // portal sidesteps that by not being a descendant of the sticky `<td>` at all once
4806
+ // mounted -- position is instead measured live off `anchorRef`'s own
4807
+ // `getBoundingClientRect()`, the same technique the chip-list popover already uses.
4808
+ function CellExpandOverlay({ className, anchorRef, contentRef, children }) {
4809
+ const [style, setStyle] = React__namespace.useState(null);
4810
+ // Grows 1px past the cell's own edges in every direction, matching this box's
4811
+ // pre-portal `top: -1px; insetInlineStart: -1px; insetInlineEnd: -1px` (the recipe's
4812
+ // `expandOverlay` class, `data-table.ts`) -- hides the 1px border seam against the
4813
+ // cell it's expanding out of. `minHeight`/`insetInlineEnd` from that same class no
4814
+ // longer apply once portaled (they resolved against the `<td>` as this box's own
4815
+ // containing block, meaningless once it's a `position: fixed` child of `<body>`
4816
+ // instead -- the same override the chip-list popover's own `measurePopoverPosition`,
4817
+ // `body-cells.tsx`, already makes), so this overrides both explicitly.
4818
+ const measure = React__namespace.useCallback(()=>{
4819
+ const cell = anchorRef.current;
4820
+ if (!cell) return;
4821
+ const rect = cell.getBoundingClientRect();
4822
+ setStyle({
4823
+ position: "fixed",
4824
+ top: rect.top - 1,
4825
+ insetInlineStart: rect.left - 1,
4826
+ insetInlineEnd: "auto",
4827
+ width: rect.width + 2,
4828
+ minHeight: "auto",
4829
+ zIndex: 1000
4830
+ });
4831
+ }, [
4832
+ anchorRef
4833
+ ]);
4834
+ // Measured before paint, on mount -- see this component's own prop doc comment for
4835
+ // why it can't just trust a caller to have measured first.
4836
+ React__namespace.useLayoutEffect(()=>{
4837
+ measure();
4838
+ }, [
4839
+ measure
4840
+ ]);
4841
+ // Same capture-phase listeners as the chip-list popover's own reposition effect
4842
+ // (`body-cells.tsx`) -- `scroll` doesn't bubble, so the table's own horizontal/
4843
+ // vertical scroll container (an arbitrary ancestor, not necessarily `window` itself)
4844
+ // is only observable this way.
4845
+ React__namespace.useEffect(()=>{
4846
+ window.addEventListener("scroll", measure, true);
4847
+ window.addEventListener("resize", measure);
4848
+ return ()=>{
4849
+ window.removeEventListener("scroll", measure, true);
4850
+ window.removeEventListener("resize", measure);
4851
+ };
4852
+ }, [
4853
+ measure
4854
+ ]);
4855
+ // Nothing to portal until the first measurement lands (avoids a one-frame flash at
4856
+ // the origin corner before `useLayoutEffect` runs).
4857
+ if (!style) return null;
4858
+ return /*#__PURE__*/ reactDom.createPortal(// `stopPropagation` -- an overlay sits on top of (and past) its own cell/row;
4762
4859
  // a click anywhere inside it must never ALSO fire the row's own `onRowClick`,
4763
4860
  // the same guard every other interactive cell affordance in this directory
4764
4861
  // already applies (`LinkCell`, `EntityCell`'s preview button, editors.tsx's
4765
4862
  // `TextEditor` root).
4766
4863
  // biome-ignore lint/a11y/noStaticElementInteractions: not a widget -- this div never responds to interaction on ITSELF, only stops propagation of events that already occurred on a real interactive descendant (the editor/text it wraps). No role fits it because it isn't a control.
4767
4864
  /*#__PURE__*/ jsxRuntime.jsx("div", {
4865
+ ref: contentRef,
4768
4866
  className: className,
4867
+ style: style,
4769
4868
  onClick: (event)=>event.stopPropagation(),
4770
4869
  children: children
4771
- }));
4870
+ }), document.body);
4772
4871
  }
4773
4872
 
4774
4873
  // Row-narrowing for the toolbar's search box -- split out of DataTable.tsx the way
@@ -4864,21 +4963,17 @@ function isCellTruncated(cell) {
4864
4963
  * close itself after the first tag commits, see `ChipListEditor`'s own doc
4865
4964
  * comment).
4866
4965
  *
4867
- * Unlike every OTHER open editor in this file, this one does NOT render inside
4868
- * Feature C's `CellExpandOverlay` (an anchored box positioned relative to the
4869
- * `<td>`, `position: absolute` against it) -- it renders through a REAL React
4870
- * portal to `document.body` instead (`DataTableBodyCell`'s own `popoverStyle`).
4871
- * Confirmed live, crispy-seed#1094's pinned-column follow-up: a `position: sticky`
4872
- * `<td>` clips ANY descendant content that grows past its own box to the table's
4873
- * scrollport, in EVERY browser tested, regardless of `overflow: visible` on every
4874
- * element in between and regardless of how high its `z-index` is raised -- neither
4875
- * of which is a stacking-context or overflow bug this file can fix from the
4876
- * INSIDE. A portal sidesteps it entirely by not being a descendant of the sticky
4877
- * `<td>` at all once mounted. The same sticky-clip almost certainly affects the
4878
- * OTHER gated editors' `CellExpandOverlay` too when their own column is pinned,
4879
- * but porting all of Feature C to this same portal model is its own change,
4880
- * out of scope here -- this fix is scoped to the one editor crispy-seed#1094
4881
- * actually reported against a pinned column. */ function isChipListEditable(column) {
4966
+ * This one renders through its OWN REAL React portal to `document.body`
4967
+ * (`DataTableBodyCell`'s own `popoverStyle`) rather than `editors.tsx`'s shared
4968
+ * `CellExpandOverlay`, since its shape is different (Attio's own click-anywhere-
4969
+ * opens gesture, no truncation gate). `CellExpandOverlay` itself ALSO portals to
4970
+ * `document.body` now (ported from this exact fix) -- both exist because a
4971
+ * `position: sticky` `<td>` clips ANY descendant content that grows past its own
4972
+ * box to the table's scrollport, in EVERY browser tested, regardless of
4973
+ * `overflow: visible` on every element in between and regardless of how high its
4974
+ * `z-index` is raised -- neither of which is a stacking-context or overflow bug
4975
+ * fixable from the INSIDE. A portal sidesteps it entirely by not being a
4976
+ * descendant of the sticky `<td>` at all once mounted. */ function isChipListEditable(column) {
4882
4977
  return !!column.editable && column.type === "chip-list";
4883
4978
  }
4884
4979
  function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyFirst, stickyLast, cellContextMenu, onViewEditHistory, cellContextMenuItems }) {
@@ -4887,10 +4982,9 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
4887
4982
  stickyFirst
4888
4983
  });
4889
4984
  return /*#__PURE__*/ jsxRuntime.jsx(jsxRuntime.Fragment, {
4890
- children: effective.map((entry, index)=>{
4985
+ children: effective.map((entry)=>{
4891
4986
  const { column } = entry;
4892
- const isLast = index === effective.length - 1;
4893
- const sticky = pinnedCellProps(entry, stickyLast, isLast);
4987
+ const sticky = pinnedCellProps(entry, stickyLast);
4894
4988
  return /*#__PURE__*/ jsxRuntime.jsx(DataTableBodyCell, {
4895
4989
  column: column,
4896
4990
  row: row,
@@ -5038,6 +5132,16 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
5038
5132
  // mounted, in `document.body` -- see `isChipListEditable`'s doc comment) and by
5039
5133
  // the reposition effect further down.
5040
5134
  const popoverRef = React__namespace.useRef(null);
5135
+ // Feature C's own portaled overlay (`CellExpandOverlay`, editors.tsx) -- its own DOM
5136
+ // node, once mounted, same reasoning as `popoverRef` above: it lives outside
5137
+ // `cellRef`'s subtree (a sibling of the table in `document.body`, not a descendant of
5138
+ // the `<td>`), so the outside-click detector below needs a ref to it too. Without
5139
+ // this, `isInside` saw every click/keystroke INSIDE the portaled editor as "outside"
5140
+ // once the overlay actually escaped the `<td>` (this fix's whole point) -- e.g.
5141
+ // typing into the overlay's own `TextEditor` fires a pointerdown on an input that is
5142
+ // no longer `cellRef.current`'s descendant, so every keystroke closed the overlay and
5143
+ // cancelled the edit before the user finished typing.
5144
+ const expandOverlayRef = React__namespace.useRef(null);
5041
5145
  // Closes the overlay on Escape or an outside click/pointerdown -- a gated
5042
5146
  // editor's OWN Escape/blur handling already ends `isEditing` on its own (see
5043
5147
  // `TextEditor`/`SelectEditor`'s existing onCancel/onOpenChange wiring), which
@@ -5050,7 +5154,7 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
5050
5154
  React__namespace.useEffect(()=>{
5051
5155
  if (!overlayOpen) return;
5052
5156
  function isInside(target) {
5053
- return (cellRef.current?.contains(target) ?? false) || (popoverRef.current?.contains(target) ?? false);
5157
+ return (cellRef.current?.contains(target) ?? false) || (popoverRef.current?.contains(target) ?? false) || (expandOverlayRef.current?.contains(target) ?? false);
5054
5158
  }
5055
5159
  function handlePointerDown(event) {
5056
5160
  if (!isInside(event.target)) {
@@ -5123,7 +5227,9 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
5123
5227
  rowId: row.id,
5124
5228
  classNames: classNames,
5125
5229
  locale: locale,
5126
- overlayActive: overlayActive
5230
+ overlayActive: overlayActive,
5231
+ cellRef: cellRef,
5232
+ expandOverlayRef: expandOverlayRef
5127
5233
  }),
5128
5234
  cellContextMenu && contextMenuOpen && contextMenuPoint && // A `<td>` (unlike its own parent `<tr>`) can validly hold any flow
5129
5235
  // content, so `CellContextMenu`'s own 0x0 `MenuAnchor` -- itself never
@@ -5168,7 +5274,7 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
5168
5274
  * display (a custom `render` wins there, same as before `editable` existed; it never
5169
5275
  * overrides an actual editing/always-live state, matching crisp's own split between
5170
5276
  * `renderTyped`'s rest-state branch and the editing branches above it).
5171
- */ function CellContent({ column, row, rowId, classNames, locale, overlayActive }) {
5277
+ */ function CellContent({ column, row, rowId, classNames, locale, overlayActive, cellRef, expandOverlayRef }) {
5172
5278
  const { isEditing, isPending, getEditError, commitEdit, cancelEdit } = useDataTableContext();
5173
5279
  const rawValue = getColumnValue(column, row);
5174
5280
  const pending = isPending(rowId, column.key);
@@ -5262,6 +5368,8 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
5262
5368
  children: [
5263
5369
  overlayActive ? /*#__PURE__*/ jsxRuntime.jsx(CellExpandOverlay, {
5264
5370
  className: classNames.expandOverlay,
5371
+ anchorRef: cellRef,
5372
+ contentRef: expandOverlayRef,
5265
5373
  children: column.editable ? node : /*#__PURE__*/ jsxRuntime.jsx("span", {
5266
5374
  className: classNames.expandOverlayText,
5267
5375
  children: node
@@ -5425,10 +5533,9 @@ function ColumnCalcCells({ columns, rows, selectable, stickyFirst, stickyLast })
5425
5533
  stickyFirst
5426
5534
  });
5427
5535
  return /*#__PURE__*/ jsxRuntime.jsx(jsxRuntime.Fragment, {
5428
- children: effective.map((entry, index)=>{
5536
+ children: effective.map((entry)=>{
5429
5537
  const { column } = entry;
5430
- const isLast = index === effective.length - 1;
5431
- const sticky = pinnedCellProps(entry, stickyLast, isLast);
5538
+ const sticky = pinnedCellProps(entry, stickyLast);
5432
5539
  return /*#__PURE__*/ jsxRuntime.jsx(DataTableFooterCell, {
5433
5540
  "data-sticky": sticky["data-sticky"],
5434
5541
  "data-sticky-edge": sticky["data-sticky-edge"],
@@ -5651,10 +5758,9 @@ const DEFAULT_SORT_ICON = {
5651
5758
  });
5652
5759
  const [renamingKey, setRenamingKey] = React__namespace.useState(null);
5653
5760
  return /*#__PURE__*/ jsxRuntime.jsx(jsxRuntime.Fragment, {
5654
- children: effective.map((entry, index)=>{
5761
+ children: effective.map((entry)=>{
5655
5762
  const { column, label, width } = entry;
5656
- const isLast = index === effective.length - 1;
5657
- const sticky = pinnedCellProps(entry, stickyLast, isLast);
5763
+ const sticky = pinnedCellProps(entry, stickyLast);
5658
5764
  return /*#__PURE__*/ jsxRuntime.jsxs(DataTableHeaderCell, {
5659
5765
  columnKey: column.key,
5660
5766
  sortable: column.sortable,
@@ -11664,6 +11770,12 @@ function DataTableInner(props, ref) {
11664
11770
  const columnKeys = React__namespace.useMemo(()=>columns.map((column)=>column.key), [
11665
11771
  columns
11666
11772
  ]);
11773
+ // Threaded down to `reconcileColumnOrder` (react-headless's `dom.ts`) so a brand-new
11774
+ // column added to `columns` after mount gets appended BEFORE this one, not past it --
11775
+ // see that function's own doc comment for the bug this prevents.
11776
+ const actionsColumnKey = React__namespace.useMemo(()=>columns.find((column)=>column.actions)?.key, [
11777
+ columns
11778
+ ]);
11667
11779
  const searchable = React__namespace.useMemo(()=>columns.some((column)=>column.searchable), [
11668
11780
  columns
11669
11781
  ]);
@@ -11706,6 +11818,7 @@ function DataTableInner(props, ref) {
11706
11818
  toolbarFilters: resolvedToolbarFilters,
11707
11819
  onToolbarFiltersChange: handleToolbarFiltersChange,
11708
11820
  columnKeys: columnKeys,
11821
+ actionsColumnKey: actionsColumnKey,
11709
11822
  columnOrder: columnOrder,
11710
11823
  defaultColumnOrder: defaultColumnOrder,
11711
11824
  onColumnOrderChange: onColumnOrderChange,
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import * as React from 'react';
1
+ import * as react from 'react';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
 
4
4
  declare interface DataTableVariant {
@@ -151,7 +151,7 @@ interface DataTableColumn<TRow> {
151
151
  */
152
152
  key: string;
153
153
  /** 헤더 셀에 표시할 내용입니다. */
154
- header: React.ReactNode;
154
+ header: react.ReactNode;
155
155
  /** 이 열을 클릭해 정렬할 수 있는지 여부입니다. */
156
156
  sortable?: boolean;
157
157
  /**
@@ -162,7 +162,7 @@ interface DataTableColumn<TRow> {
162
162
  /** 셀 내용을 직접 그립니다. 지정하면 `type`의 기본 렌더러 대신 이것을 사용합니다.
163
163
  * `editable`이어도 편집/always-live 상태에는 영향이 없습니다 -- 이 필드는 REST
164
164
  * 상태의 표시만 바꿉니다. */
165
- render?: (row: TRow) => React.ReactNode;
165
+ render?: (row: TRow) => react.ReactNode;
166
166
  /** `type: "currency"`의 통화 기호입니다. @default "$" */
167
167
  symbol?: string;
168
168
  /** `type: "rating"`의 별 개수입니다. @default 5 */
@@ -198,8 +198,11 @@ interface DataTableColumn<TRow> {
198
198
  * 절대 스크롤되어 사라지지 않도록 오른쪽 끝에 고정합니다 -- 이 플래그가 있는 열은 테이블
199
199
  * 전체의 `stickyLast`를 켜지 않아도 자동으로 그 고정 + 왼쪽 스크롤 그림자
200
200
  * (`stickyProps`/`stickyLast`가 이미 구현한 것과 동일한 메커니즘, `data-sticky-edge-end`
201
- * / `data-scrolled-end`)를 받습니다. 열 목록의 마지막 열이어야 합니다 -- `stickyLast`
202
- * 자체가 이미 "마지막으로 보이는 열"만 고정하는 규칙이기 때문입니다.
201
+ * / `data-scrolled-end`)를 받습니다. `stickyLast`는 이 플래그가 붙은 열을 직접 찾아
202
+ * 고정합니다 (`column-layout.ts`의 `useEffectiveColumns`) -- 더 이상 "마지막으로 보이는
203
+ * 열"이어야만 한다는 제약이 없습니다. 내부적으로도 `reconcileColumnOrder`(react-headless의
204
+ * `dom.ts`)가 이 열을 항상 맨 끝에 유지하므로, 마운트 이후 추가된 새 열이 이 열보다 뒤로
205
+ * 붙어 고정 셀 뒤에 가려지는 일도 없습니다.
203
206
  */
204
207
  actions?: boolean;
205
208
  /**
@@ -289,11 +292,11 @@ interface DataTableColumn<TRow> {
289
292
  * 줄임표 처리되고, 아이콘은 `flex-shrink:0`). `type: "text"`(기본)와 `"url"`/
290
293
  * `"email"` 열에서 동작합니다.
291
294
  */
292
- suffixIcon?: (row: TRow) => React.ReactNode;
295
+ suffixIcon?: (row: TRow) => react.ReactNode;
293
296
  }
294
297
  interface DataTableProps<TRow extends {
295
298
  id: string;
296
- }> extends DataTableVariantProps, Omit<PrimitiveProps, "asChild">, Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
299
+ }> extends DataTableVariantProps, Omit<PrimitiveProps, "asChild">, Omit<react.HTMLAttributes<HTMLDivElement>, "children"> {
297
300
  columns: DataTableColumn<TRow>[];
298
301
  data: TRow[];
299
302
  /** `type`이 있는 열의 number/currency/date/datetime 표시에 쓰는 로캘입니다. @default "en-US" */
@@ -311,9 +314,9 @@ interface DataTableProps<TRow extends {
311
314
  * 씁니다. 생략하면 karrot 아이콘 셋의 chevron을 씁니다.
312
315
  */
313
316
  sortIcon?: {
314
- idle?: React.ReactNode;
315
- asc?: React.ReactNode;
316
- desc?: React.ReactNode;
317
+ idle?: react.ReactNode;
318
+ asc?: react.ReactNode;
319
+ desc?: react.ReactNode;
317
320
  };
318
321
  /** 왼쪽에 40px짜리 체크박스 선택 열을 추가합니다. */
319
322
  selectable?: boolean;
@@ -329,10 +332,10 @@ interface DataTableProps<TRow extends {
329
332
  getRowSelectionLabel?: (row: TRow) => string;
330
333
  /** 선택 체크박스가 checked일 때 표시할 아이콘입니다. 생략하면 karrot의 체크 아이콘을
331
334
  * 씁니다. */
332
- selectionCheckedIcon?: React.ReactNode;
335
+ selectionCheckedIcon?: react.ReactNode;
333
336
  /** 전체 선택 체크박스가 일부만 선택됐을 때(indeterminate) 표시할 아이콘입니다.
334
337
  * 생략하면 karrot의 minus 아이콘을 씁니다. */
335
- selectionIndeterminateIcon?: React.ReactNode;
338
+ selectionIndeterminateIcon?: react.ReactNode;
336
339
  /**
337
340
  * 행이 하나 이상 선택되면 테이블 하단 중앙에 뜨는 벌크 액션 바(Attio 스타일 floating
338
341
  * pill, gap #4)에 넣을 소비자 액션입니다. 현재 선택된 행 id 목록을 받는 render prop --
@@ -341,7 +344,7 @@ interface DataTableProps<TRow extends {
341
344
  * "크롬 vs 소비자 콘텐츠" 분리). 생략하면 바는 크롬만 보여줍니다. `selectable`이 꺼져
342
345
  * 있으면 무시됩니다(선택 자체가 없으므로).
343
346
  */
344
- bulkActions?: (selectedIds: string[]) => React.ReactNode;
347
+ bulkActions?: (selectedIds: string[]) => react.ReactNode;
345
348
  /** 벌크 액션 바의 지우기(x) 버튼 접근성 이름입니다. @default "Clear selection" */
346
349
  clearSelectionLabel?: string;
347
350
  /** 제어할 검색어입니다. */
@@ -380,7 +383,7 @@ interface DataTableProps<TRow extends {
380
383
  * 행 개수 텍스트를 직접 만듭니다. 생략하면 crisp의 기본 형식을 씁니다: 검색/필터로
381
384
  * 줄지 않았으면 "N rows"(단수는 "row"), 줄었으면 "N of M".
382
385
  */
383
- countLabel?: (shown: number, total: number) => React.ReactNode;
386
+ countLabel?: (shown: number, total: number) => react.ReactNode;
384
387
  /**
385
388
  * 툴바 오른쪽, 행 개수 옆에 렌더링할 커스텀 액션입니다(예: Download/내보내기 버튼).
386
389
  * 이전에는 이 자리에 넣을 공식 prop이 없어서 소비자가 `.seed-data-table__toolbar`
@@ -389,7 +392,7 @@ interface DataTableProps<TRow extends {
389
392
  * 렌더링됩니다). 검색/정렬/필터보다 뒤, 카운트보다 앞(같은 그룹 안에서 카운트
390
393
  * 왼쪽)에 옵니다.
391
394
  */
392
- toolbarActions?: React.ReactNode;
395
+ toolbarActions?: react.ReactNode;
393
396
  /** 헤더의 열별 메뉴(이름 변경 / 숨기기 / 왼쪽·오른쪽으로 이동)를 켭니다, seed `Menu`. */
394
397
  columnMenu?: boolean;
395
398
  /** 헤더를 드래그해 열 순서를 바꿀 수 있게 합니다. @default true */
@@ -470,7 +473,7 @@ interface DataTableProps<TRow extends {
470
473
  * consumer's own "Assign to…") -- rendered inside the SAME `MenuScrollArea` the
471
474
  * built-in items are, so a `<MenuItem>` from this package's own `Menu` export
472
475
  * composes the way `column-menu.tsx`'s own items do. */
473
- cellContextMenuItems?: (ctx: DataTableCellContextMenuContext<TRow>) => React.ReactNode;
476
+ cellContextMenuItems?: (ctx: DataTableCellContextMenuContext<TRow>) => react.ReactNode;
474
477
  /** 행을 클릭할 수 있게 하는 호버 표시입니다 (crisp의 3%/4% 포그라운드 틴트). 커서
475
478
  * 모양은 `onRowClick`이 있는지에 따로 달려 있습니다 -- 이 값과 독립적으로 켤 수
476
479
  * 있습니다(crisp의 두 prop, `interactive`/`onRowClick`도 서로 독립적입니다). */
@@ -481,19 +484,19 @@ interface DataTableProps<TRow extends {
481
484
  /** 주어지면 맨 아래에 crisp가 측정한 "+ New" 추가 행 버튼을 렌더링합니다. */
482
485
  onAddRow?: () => void;
483
486
  /** 추가 행 버튼의 라벨입니다. @default "New record" */
484
- addRowLabel?: React.ReactNode;
487
+ addRowLabel?: react.ReactNode;
485
488
  /** 전체 본문을 이 상태로 대체합니다(로딩 아이콘 + `loadingLabel`) -- 필터/정렬
486
489
  * 재조회나 최초 로드처럼 행 전체가 아직 없을 때를 위한 것입니다. `error`/빈 상태
487
490
  * 보다 우선합니다. 인라인 스크롤 로딩 표시기(`onLoadMore`)와는 별개입니다. */
488
491
  loading?: boolean;
489
492
  /** `loading`일 때 스피너 옆에 보이는 텍스트입니다. @default "Loading..." */
490
- loadingLabel?: React.ReactNode;
493
+ loadingLabel?: react.ReactNode;
491
494
  /** 주어지면 행 대신 이 내용을 본문에 렌더링합니다(crisp의 `.crisp-datatable-empty`
492
495
  * 칸 재사용). `loading`보다는 아래, 빈 상태보다는 위 우선순위입니다. */
493
- error?: React.ReactNode;
496
+ error?: react.ReactNode;
494
497
  /** 검색/필터를 거치고도 행이 하나도 없을 때 본문에 렌더링할 내용입니다.
495
498
  * @default "No results." */
496
- empty?: React.ReactNode;
499
+ empty?: react.ReactNode;
497
500
  /**
498
501
  * Attio 스타일 무한 스크롤: 스크롤 컨테이너 바닥 근처의 sentinel 행이 뷰에 들어오면
499
502
  * 호출됩니다. `Promise`를 반환하면 그동안 자동으로 `loadingMore`가 켜집니다(직접
@@ -517,7 +520,7 @@ interface DataTableProps<TRow extends {
517
520
  * `style={{ height: "100%" }}` 같은 바운드를 주는 것만으로도 "전체 높이" 모드가
518
521
  * 됩니다 -- 새 prop 없이 기존 `style` 전달만으로 두 모드 다 지원됩니다.
519
522
  */
520
- scrollProps?: Omit<React.ComponentPropsWithoutRef<"div">, "ref" | "onScroll">;
523
+ scrollProps?: Omit<react.ComponentPropsWithoutRef<"div">, "ref" | "onScroll">;
521
524
  }
522
525
  /**
523
526
  * @see https://seed-design.io/react/components/data-table
@@ -525,8 +528,8 @@ interface DataTableProps<TRow extends {
525
528
  declare const DataTable: <TRow extends {
526
529
  id: string;
527
530
  }>(props: DataTableProps<TRow> & {
528
- ref?: React.ForwardedRef<HTMLDivElement>;
529
- }) => React.ReactElement;
531
+ ref?: react.ForwardedRef<HTMLDivElement>;
532
+ }) => react.ReactElement;
530
533
 
531
534
  interface DataTableViewsControlProps extends UseDataTableViewsProps {
532
535
  /** The trigger's label when no view is active. @default "Views" */
@@ -536,7 +539,7 @@ interface DataTableViewsControlProps extends UseDataTableViewsProps {
536
539
  * meaningful "no view" state to switch back to. */
537
540
  showClearRow?: boolean;
538
541
  /** The clear row's own label, when `showClearRow` is on. @default "label" */
539
- clearLabel?: React.ReactNode;
542
+ clearLabel?: react.ReactNode;
540
543
  /**
541
544
  * Reads the table's CURRENT column order/visibility/widths/pins + toolbar sort/
542
545
  * filter conditions -- called when the user picks "Save as new view" or "Update
@@ -578,7 +581,7 @@ interface DataTableBulkBarProps {
578
581
  * onClick={() => addToList(ids)}>Add to list</ActionButton>}</DataTableBulkBar>`.
579
582
  * Omit for just the bar's own chrome ("N selected" + clear).
580
583
  */
581
- children?: (selectedIds: string[]) => React.ReactNode;
584
+ children?: (selectedIds: string[]) => react.ReactNode;
582
585
  }
583
586
  /**
584
587
  * The floating bulk-action pill (gap #4) -- renders nothing while no row is selected
@@ -594,7 +597,7 @@ declare function DataTableBulkBar({ clearLabel, children, }: DataTableBulkBarPro
594
597
  type ResizablePanelSide = "left" | "right";
595
598
 
596
599
  type ResizablePanelHandleVariant = "panel" | "column";
597
- type ResizablePanelProps = Omit<React.ComponentPropsWithoutRef<"div">, "children" | "onResize"> & {
600
+ type ResizablePanelProps = Omit<react.ComponentPropsWithoutRef<"div">, "children" | "onResize"> & {
598
601
  /** Which edge of its container the panel is anchored to. @default "left" */
599
602
  side?: ResizablePanelSide;
600
603
  /** Controlled width, in px. */
@@ -683,9 +686,9 @@ type ResizablePanelProps = Omit<React.ComponentPropsWithoutRef<"div">, "children
683
686
  * observe `animationName` changes on the SAME node that carries
684
687
  * `data-state`/the keyframe animation above.
685
688
  */
686
- panelElementRef?: React.Ref<HTMLDivElement>;
689
+ panelElementRef?: react.Ref<HTMLDivElement>;
687
690
  className?: string;
688
- children?: React.ReactNode;
691
+ children?: react.ReactNode;
689
692
  };
690
693
  /**
691
694
  * An adjustable-panel primitive with two layout models (`floating`'s own doc
@@ -701,7 +704,7 @@ type ResizablePanelProps = Omit<React.ComponentPropsWithoutRef<"div">, "children
701
704
  * lives in `useResizablePanel` (`@seed-design/react-resizable-panel`), this
702
705
  * component owns the DOM + recipe classes on top of it.
703
706
  */
704
- declare const ResizablePanel: React.ForwardRefExoticComponent<Omit<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref">, "children" | "onResize"> & {
707
+ declare const ResizablePanel: react.ForwardRefExoticComponent<Omit<Omit<react.DetailedHTMLProps<react.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref">, "children" | "onResize"> & {
705
708
  /** Which edge of its container the panel is anchored to. @default "left" */
706
709
  side?: ResizablePanelSide;
707
710
  /** Controlled width, in px. */
@@ -790,10 +793,10 @@ declare const ResizablePanel: React.ForwardRefExoticComponent<Omit<Omit<React.De
790
793
  * observe `animationName` changes on the SAME node that carries
791
794
  * `data-state`/the keyframe animation above.
792
795
  */
793
- panelElementRef?: React.Ref<HTMLDivElement>;
796
+ panelElementRef?: react.Ref<HTMLDivElement>;
794
797
  className?: string;
795
- children?: React.ReactNode;
796
- } & React.RefAttributes<HTMLDivElement>>;
798
+ children?: react.ReactNode;
799
+ } & react.RefAttributes<HTMLDivElement>>;
797
800
 
798
801
  /** One Highlights card -- label, value and an optional icon, laid out the way
799
802
  * Attio's own peek panel lays its own out (your-moon/crisp#448, measured
@@ -802,8 +805,8 @@ declare const ResizablePanel: React.ForwardRefExoticComponent<Omit<Omit<React.De
802
805
  type RecordPreviewHighlight = {
803
806
  key: string;
804
807
  label: string;
805
- value: React.ReactNode;
806
- icon?: React.ComponentType<{
808
+ value: react.ReactNode;
809
+ icon?: react.ComponentType<{
807
810
  size?: number;
808
811
  }>;
809
812
  };
@@ -816,7 +819,7 @@ type RecordPreviewSection = {
816
819
  key: string;
817
820
  label: string;
818
821
  count: number;
819
- content?: React.ReactNode;
822
+ content?: react.ReactNode;
820
823
  };
821
824
  /** One tab of the panel's own mini detail view (a "tabs" capability with no
822
825
  * your-moon/crisp equivalent -- crisp's own peek only ever shows Highlights
@@ -828,12 +831,12 @@ type RecordPreviewSection = {
828
831
  * tab has been visited) -- see `Tabs.tsx`. */
829
832
  type RecordPreviewTab = {
830
833
  key: string;
831
- label: React.ReactNode;
832
- content: React.ReactNode;
834
+ label: react.ReactNode;
835
+ content: react.ReactNode;
833
836
  /** Rendered right after `label` inside the tab trigger, e.g. an unread
834
837
  * count -- purely presentational, this component attaches no meaning to
835
838
  * it. */
836
- badge?: React.ReactNode;
839
+ badge?: react.ReactNode;
837
840
  };
838
841
  /** The record this panel previews. Only `name` is read directly here --
839
842
  * everything else the panel shows comes from `highlights`/`activity`/
@@ -845,7 +848,7 @@ type RecordPreviewRecord = {
845
848
  name?: string;
846
849
  [key: string]: unknown;
847
850
  };
848
- type RecordPreviewProps = Omit<React.ComponentPropsWithoutRef<"div">, "children" | "title" | "onResize"> & {
851
+ type RecordPreviewProps = Omit<react.ComponentPropsWithoutRef<"div">, "children" | "title" | "onResize"> & {
849
852
  /** Whether the panel is mounted/visible. */
850
853
  open: boolean;
851
854
  onClose: () => void;
@@ -866,7 +869,7 @@ type RecordPreviewProps = Omit<React.ComponentPropsWithoutRef<"div">, "children"
866
869
  nextRecordLabel?: string;
867
870
  /** "2 of 37 in All Companies" (your-moon/crisp#413) -- a single
868
871
  * pre-formatted string; omit for no counter at all. */
869
- counterLabel?: React.ReactNode;
872
+ counterLabel?: react.ReactNode;
870
873
  /**
871
874
  * Lets a person drag the panel wider or narrower from its own left edge --
872
875
  * this port reuses `ResizablePanel`'s own handle (`side="right"`, whose
@@ -882,7 +885,7 @@ type RecordPreviewProps = Omit<React.ComponentPropsWithoutRef<"div">, "children"
882
885
  minWidth?: number;
883
886
  maxWidth?: number;
884
887
  /** The panel's heading, and so its accessible name. Defaults to `record.name`. */
885
- title?: React.ReactNode;
888
+ title?: react.ReactNode;
886
889
  /** Avatar image URL beside the title. Omit for an initials tile built from
887
890
  * `record.name`'s first character. */
888
891
  avatar?: string;
@@ -891,13 +894,13 @@ type RecordPreviewProps = Omit<React.ComponentPropsWithoutRef<"div">, "children"
891
894
  record: RecordPreviewRecord | null;
892
895
  /** Heading above the highlight cards (default "Highlights"). Omit the
893
896
  * whole section by omitting `highlights`. */
894
- highlightsLabel?: React.ReactNode;
897
+ highlightsLabel?: react.ReactNode;
895
898
  highlights?: RecordPreviewHighlight[];
896
899
  /** Heading above the activity feed (default "Activity"). */
897
- activityLabel?: React.ReactNode;
898
- viewAllActivityLabel?: React.ReactNode;
900
+ activityLabel?: react.ReactNode;
901
+ viewAllActivityLabel?: react.ReactNode;
899
902
  onViewAllActivity?: () => void;
900
- activity?: React.ReactNode;
903
+ activity?: react.ReactNode;
901
904
  /** Collapsed sections below Activity (your-moon/crisp#413). Omit for no
902
905
  * sections at all. */
903
906
  sections?: RecordPreviewSection[];
@@ -923,9 +926,9 @@ type RecordPreviewProps = Omit<React.ComponentPropsWithoutRef<"div">, "children"
923
926
  onTabChange?: (key: string) => void;
924
927
  /** Label for the synthesized overview tab -- see `tabs`'s own doc
925
928
  * comment. @default "Overview" */
926
- overviewTabLabel?: React.ReactNode;
929
+ overviewTabLabel?: react.ReactNode;
927
930
  /** Rendered below the panel's content, e.g. an Edit button. */
928
- footer?: React.ReactNode;
931
+ footer?: react.ReactNode;
929
932
  /**
930
933
  * An optional dimming backdrop behind the panel -- OFF by default (this
931
934
  * is a peek, not a modal: Attio's own peek never dims the table it
package/dist/index.js CHANGED
@@ -968,15 +968,33 @@ function sameCell$1(a, id, key) {
968
968
  * first captured) nor keeps a ghost entry for one that was removed. Known keys keep
969
969
  * their position from `order`; any key in `keys` missing from `order` is appended, in
970
970
  * `keys`' own order; any entry in `order` no longer in `keys` is dropped.
971
- */ function reconcileColumnOrder(order, keys) {
971
+ *
972
+ * `actionsKey` (the styled layer's `actions`-flagged column, e.g. the row-menu `⋯`
973
+ * column) is kept LAST no matter what -- without this, a brand-new column added to
974
+ * `columns` after the table first mounted got appended past it (the "any key in `keys`
975
+ * missing from `order` is appended" rule above has no notion of "past the end" vs. "at
976
+ * the very end"), stranding it in the middle of the table since `stickyLast`/`actions`
977
+ * only sticks the actual trailing column. A real consumer hit this: their newly-added
978
+ * column landed to the right of the already-pinned-sticky `actions` column, covered by
979
+ * it under horizontal scroll. Column-layout's own `pinnedCellProps` independently
980
+ * targets the `actions` column by FLAG rather than by position (belt and suspenders --
981
+ * see that file's own doc comment), but this still keeps the VISUAL order sane so nothing
982
+ * renders hidden behind the sticky cell in the first place.
983
+ */ function reconcileColumnOrder(order, keys, actionsKey) {
972
984
  const keySet = new Set(keys);
973
985
  const known = order.filter((key)=>keySet.has(key));
974
986
  const knownSet = new Set(known);
975
987
  const added = keys.filter((key)=>!knownSet.has(key));
976
- return [
988
+ const merged = [
977
989
  ...known,
978
990
  ...added
979
991
  ];
992
+ if (actionsKey === undefined || !keySet.has(actionsKey)) return merged;
993
+ const withoutActions = merged.filter((key)=>key !== actionsKey);
994
+ return [
995
+ ...withoutActions,
996
+ actionsKey
997
+ ];
980
998
  }
981
999
  /**
982
1000
  * Moves `key` one step toward `direction` in `order` -- a no-op (same array, well,
@@ -1155,7 +1173,7 @@ const EMPTY_RECORD = {};
1155
1173
  * state this hook already owns, not independent state
1156
1174
  * of their own.
1157
1175
  */ function useDataTableColumns(props) {
1158
- const { columnKeys = EMPTY_STRINGS, columnOrder: columnOrderProp, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys: hiddenProp, defaultHiddenColumnKeys = EMPTY_STRINGS, onHiddenColumnKeysChange, columnWidths: widthsProp, defaultColumnWidths = EMPTY_RECORD, onColumnWidthsChange, columnLabels: labelsProp, defaultColumnLabels = EMPTY_RECORD, onColumnLabelsChange, pinnedColumnKeys: pinnedProp, defaultPinnedColumnKeys = EMPTY_STRINGS, onPinnedColumnKeysChange } = props;
1176
+ const { columnKeys = EMPTY_STRINGS, actionsColumnKey, columnOrder: columnOrderProp, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys: hiddenProp, defaultHiddenColumnKeys = EMPTY_STRINGS, onHiddenColumnKeysChange, columnWidths: widthsProp, defaultColumnWidths = EMPTY_RECORD, onColumnWidthsChange, columnLabels: labelsProp, defaultColumnLabels = EMPTY_RECORD, onColumnLabelsChange, pinnedColumnKeys: pinnedProp, defaultPinnedColumnKeys = EMPTY_STRINGS, onPinnedColumnKeysChange } = props;
1159
1177
  const [rawOrder, setRawOrder] = useControllableState$1({
1160
1178
  prop: columnOrderProp,
1161
1179
  defaultProp: defaultColumnOrder ?? columnKeys,
@@ -1166,9 +1184,10 @@ const EMPTY_RECORD = {};
1166
1184
  // consumer persisted before a new column existed must still show that new column
1167
1185
  // (appended), and one holding a column that got removed from `columns` must not
1168
1186
  // render a ghost header for it.
1169
- const columnOrder = useMemo(()=>reconcileColumnOrder(rawOrder ?? columnKeys, columnKeys), [
1187
+ const columnOrder = useMemo(()=>reconcileColumnOrder(rawOrder ?? columnKeys, columnKeys, actionsColumnKey), [
1170
1188
  rawOrder,
1171
- columnKeys
1189
+ columnKeys,
1190
+ actionsColumnKey
1172
1191
  ]);
1173
1192
  const [hiddenColumnKeys = EMPTY_STRINGS, setHiddenColumnKeys] = useControllableState$1({
1174
1193
  prop: hiddenProp,
@@ -2150,7 +2169,7 @@ function useDataTableRowContext({ strict = true } = {}) {
2150
2169
  return context;
2151
2170
  }
2152
2171
  const DataTableRoot = /*#__PURE__*/ forwardRef((props, ref)=>{
2153
- const { sort, defaultSort, onSortChange, rowIds, selectable, selectedIds, defaultSelectedIds, onSelectionChange, query, defaultQuery, onQueryChange, toolbarSorts, defaultToolbarSorts, onToolbarSortsChange, toolbarFilters, defaultToolbarFilters, onToolbarFiltersChange, columnKeys, columnOrder, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys, defaultHiddenColumnKeys, onHiddenColumnKeysChange, columnWidths, defaultColumnWidths, onColumnWidthsChange, columnLabels, defaultColumnLabels, onColumnLabelsChange, pinnedColumnKeys, defaultPinnedColumnKeys, onPinnedColumnKeysChange, columnCalcOps, defaultColumnCalcOps, onColumnCalcOpsChange, onEdit, onLoadMore, hasMore, loadingMore, ...otherProps } = props;
2172
+ const { sort, defaultSort, onSortChange, rowIds, selectable, selectedIds, defaultSelectedIds, onSelectionChange, query, defaultQuery, onQueryChange, toolbarSorts, defaultToolbarSorts, onToolbarSortsChange, toolbarFilters, defaultToolbarFilters, onToolbarFiltersChange, columnKeys, actionsColumnKey, columnOrder, defaultColumnOrder, onColumnOrderChange, hiddenColumnKeys, defaultHiddenColumnKeys, onHiddenColumnKeysChange, columnWidths, defaultColumnWidths, onColumnWidthsChange, columnLabels, defaultColumnLabels, onColumnLabelsChange, pinnedColumnKeys, defaultPinnedColumnKeys, onPinnedColumnKeysChange, columnCalcOps, defaultColumnCalcOps, onColumnCalcOpsChange, onEdit, onLoadMore, hasMore, loadingMore, ...otherProps } = props;
2154
2173
  const api = useDataTable({
2155
2174
  sort,
2156
2175
  defaultSort,
@@ -2170,6 +2189,7 @@ const DataTableRoot = /*#__PURE__*/ forwardRef((props, ref)=>{
2170
2189
  defaultToolbarFilters,
2171
2190
  onToolbarFiltersChange,
2172
2191
  columnKeys,
2192
+ actionsColumnKey,
2173
2193
  columnOrder,
2174
2194
  defaultColumnOrder,
2175
2195
  onColumnOrderChange,
@@ -3514,10 +3534,25 @@ function RatingCell({ value, maxRating = 5 }) {
3514
3534
  width,
3515
3535
  pinned,
3516
3536
  pinOffset,
3517
- pinnedEdge: false
3537
+ pinnedEdge: false,
3538
+ stickyLastTarget: false
3518
3539
  };
3519
3540
  });
3520
3541
  if (pinnedCount > 0) entries[pinnedCount - 1].pinnedEdge = true;
3542
+ // `stickyLast` sticks the `actions`-flagged column specifically -- not whichever
3543
+ // column merely landed last in render order. Without this, `reconcileColumnOrder`
3544
+ // appending a brand-new column past `actions` (this file's own `<colgroup>` doc
3545
+ // comment on why order must stay in lockstep) silently moved the sticky-right
3546
+ // treatment onto that new column instead, stranding `actions` in the middle of the
3547
+ // table -- a real consumer hit this. Falls back to the genuine last entry when no
3548
+ // column is flagged `actions` at all, matching `stickyLast`'s original single-
3549
+ // column behavior.
3550
+ if (entries.length > 0) {
3551
+ const actionsColumn = columns.find((column)=>column.actions);
3552
+ const actionsIndex = actionsColumn ? entries.findIndex((entry)=>entry.column.key === actionsColumn.key) : -1;
3553
+ const stickyLastIndex = actionsIndex !== -1 ? actionsIndex : entries.length - 1;
3554
+ entries[stickyLastIndex].stickyLastTarget = true;
3555
+ }
3521
3556
  return entries;
3522
3557
  }, [
3523
3558
  columns,
@@ -3557,9 +3592,12 @@ function RatingCell({ value, maxRating = 5 }) {
3557
3592
  }
3558
3593
  /** Sticky positioning for one DATA cell (header, body, or ColumnCalc footer) -- pinned-
3559
3594
  * left (this column's own `pinOffset`, `data-sticky-edge` only on the group's
3560
- * trailing-most member) takes priority over sticky-right (`stickyLast`, always just
3561
- * the last VISIBLE column, e.g. an `actions` column) since a column can only stick to
3562
- * one edge. */ function pinnedCellProps(entry, stickyLast, isLast) {
3595
+ * trailing-most member) takes priority over sticky-right (`stickyLast`, the
3596
+ * `actions`-flagged column when one exists, else the genuine last VISIBLE column)
3597
+ * since a column can only stick to one edge. Reads `entry.stickyLastTarget`
3598
+ * (`useEffectiveColumns`) rather than an `isLast` index the caller computes itself --
3599
+ * that index is just "last in render order", which is exactly what stranded a
3600
+ * reordered-past `actions` column in the first place. */ function pinnedCellProps(entry, stickyLast) {
3563
3601
  if (entry.pinned) {
3564
3602
  return {
3565
3603
  "data-sticky": "",
@@ -3571,7 +3609,7 @@ function RatingCell({ value, maxRating = 5 }) {
3571
3609
  }
3572
3610
  };
3573
3611
  }
3574
- if (stickyLast && isLast) {
3612
+ if (stickyLast && entry.stickyLastTarget) {
3575
3613
  return {
3576
3614
  "data-sticky": "",
3577
3615
  "data-sticky-edge-end": "",
@@ -4730,26 +4768,87 @@ function ChipListEditor({ column, value, pending, onCommit, classNames }) {
4730
4768
  }));
4731
4769
  }
4732
4770
  ////////////////////////////////////////////////////////////////////////////////////
4733
- // -- Feature C (#12): cell-expand "show full content" overlay -- an anchored box
4734
- // (position:absolute over the cell it belongs to, grows in HEIGHT, elevated)
4735
- // that wraps whatever a truncated cell would otherwise show inline: an editable
4736
- // column's OWN editor node (unchanged, just relocated into this box instead of
4737
- // rendered flush in the 36px row) for the editable case, or the plain full value
4738
- // for a read-only one. `body-cells.tsx` decides WHEN to render this (a click on a
4739
- // cell whose content is actually truncated, `truncate.ts`'s `isCellTruncated`);
4740
- // this component only supplies the box.
4741
- function CellExpandOverlay({ className, children }) {
4742
- return(// `stopPropagation` -- an overlay sits on top of (and past) its own cell/row;
4771
+ // -- Feature C (#12): cell-expand "show full content" overlay -- a box that wraps
4772
+ // whatever a truncated cell would otherwise show inline: an editable column's OWN
4773
+ // editor node (unchanged, just relocated into this box instead of rendered flush in
4774
+ // the 36px row) for the editable case, or the plain full value for a read-only one.
4775
+ // `body-cells.tsx` decides WHEN to render this (a click on a cell whose content is
4776
+ // actually truncated, `truncate.ts`'s `isCellTruncated`); this component only
4777
+ // supplies the box.
4778
+ //
4779
+ // Portals to `document.body` (crispy-seed#1094's own pinned-column follow-up,
4780
+ // ported from the chip-list editor's popover, `body-cells.tsx`'s
4781
+ // `isChipListEditable` -- that one shipped this exact fix in 0.10.3) rather than
4782
+ // positioning `absolute` against its own cell: a `position: sticky` `<td>` clips ANY
4783
+ // descendant that grows past its own box, in every browser tested, regardless of
4784
+ // `overflow`/`z-index` on anything in between, so a pinned column's own truncated
4785
+ // cell used to clip this overlay's grown content instead of showing it in full. A
4786
+ // portal sidesteps that by not being a descendant of the sticky `<td>` at all once
4787
+ // mounted -- position is instead measured live off `anchorRef`'s own
4788
+ // `getBoundingClientRect()`, the same technique the chip-list popover already uses.
4789
+ function CellExpandOverlay({ className, anchorRef, contentRef, children }) {
4790
+ const [style, setStyle] = React.useState(null);
4791
+ // Grows 1px past the cell's own edges in every direction, matching this box's
4792
+ // pre-portal `top: -1px; insetInlineStart: -1px; insetInlineEnd: -1px` (the recipe's
4793
+ // `expandOverlay` class, `data-table.ts`) -- hides the 1px border seam against the
4794
+ // cell it's expanding out of. `minHeight`/`insetInlineEnd` from that same class no
4795
+ // longer apply once portaled (they resolved against the `<td>` as this box's own
4796
+ // containing block, meaningless once it's a `position: fixed` child of `<body>`
4797
+ // instead -- the same override the chip-list popover's own `measurePopoverPosition`,
4798
+ // `body-cells.tsx`, already makes), so this overrides both explicitly.
4799
+ const measure = React.useCallback(()=>{
4800
+ const cell = anchorRef.current;
4801
+ if (!cell) return;
4802
+ const rect = cell.getBoundingClientRect();
4803
+ setStyle({
4804
+ position: "fixed",
4805
+ top: rect.top - 1,
4806
+ insetInlineStart: rect.left - 1,
4807
+ insetInlineEnd: "auto",
4808
+ width: rect.width + 2,
4809
+ minHeight: "auto",
4810
+ zIndex: 1000
4811
+ });
4812
+ }, [
4813
+ anchorRef
4814
+ ]);
4815
+ // Measured before paint, on mount -- see this component's own prop doc comment for
4816
+ // why it can't just trust a caller to have measured first.
4817
+ React.useLayoutEffect(()=>{
4818
+ measure();
4819
+ }, [
4820
+ measure
4821
+ ]);
4822
+ // Same capture-phase listeners as the chip-list popover's own reposition effect
4823
+ // (`body-cells.tsx`) -- `scroll` doesn't bubble, so the table's own horizontal/
4824
+ // vertical scroll container (an arbitrary ancestor, not necessarily `window` itself)
4825
+ // is only observable this way.
4826
+ React.useEffect(()=>{
4827
+ window.addEventListener("scroll", measure, true);
4828
+ window.addEventListener("resize", measure);
4829
+ return ()=>{
4830
+ window.removeEventListener("scroll", measure, true);
4831
+ window.removeEventListener("resize", measure);
4832
+ };
4833
+ }, [
4834
+ measure
4835
+ ]);
4836
+ // Nothing to portal until the first measurement lands (avoids a one-frame flash at
4837
+ // the origin corner before `useLayoutEffect` runs).
4838
+ if (!style) return null;
4839
+ return /*#__PURE__*/ createPortal(// `stopPropagation` -- an overlay sits on top of (and past) its own cell/row;
4743
4840
  // a click anywhere inside it must never ALSO fire the row's own `onRowClick`,
4744
4841
  // the same guard every other interactive cell affordance in this directory
4745
4842
  // already applies (`LinkCell`, `EntityCell`'s preview button, editors.tsx's
4746
4843
  // `TextEditor` root).
4747
4844
  // biome-ignore lint/a11y/noStaticElementInteractions: not a widget -- this div never responds to interaction on ITSELF, only stops propagation of events that already occurred on a real interactive descendant (the editor/text it wraps). No role fits it because it isn't a control.
4748
4845
  /*#__PURE__*/ jsx("div", {
4846
+ ref: contentRef,
4749
4847
  className: className,
4848
+ style: style,
4750
4849
  onClick: (event)=>event.stopPropagation(),
4751
4850
  children: children
4752
- }));
4851
+ }), document.body);
4753
4852
  }
4754
4853
 
4755
4854
  // Row-narrowing for the toolbar's search box -- split out of DataTable.tsx the way
@@ -4845,21 +4944,17 @@ function isCellTruncated(cell) {
4845
4944
  * close itself after the first tag commits, see `ChipListEditor`'s own doc
4846
4945
  * comment).
4847
4946
  *
4848
- * Unlike every OTHER open editor in this file, this one does NOT render inside
4849
- * Feature C's `CellExpandOverlay` (an anchored box positioned relative to the
4850
- * `<td>`, `position: absolute` against it) -- it renders through a REAL React
4851
- * portal to `document.body` instead (`DataTableBodyCell`'s own `popoverStyle`).
4852
- * Confirmed live, crispy-seed#1094's pinned-column follow-up: a `position: sticky`
4853
- * `<td>` clips ANY descendant content that grows past its own box to the table's
4854
- * scrollport, in EVERY browser tested, regardless of `overflow: visible` on every
4855
- * element in between and regardless of how high its `z-index` is raised -- neither
4856
- * of which is a stacking-context or overflow bug this file can fix from the
4857
- * INSIDE. A portal sidesteps it entirely by not being a descendant of the sticky
4858
- * `<td>` at all once mounted. The same sticky-clip almost certainly affects the
4859
- * OTHER gated editors' `CellExpandOverlay` too when their own column is pinned,
4860
- * but porting all of Feature C to this same portal model is its own change,
4861
- * out of scope here -- this fix is scoped to the one editor crispy-seed#1094
4862
- * actually reported against a pinned column. */ function isChipListEditable(column) {
4947
+ * This one renders through its OWN REAL React portal to `document.body`
4948
+ * (`DataTableBodyCell`'s own `popoverStyle`) rather than `editors.tsx`'s shared
4949
+ * `CellExpandOverlay`, since its shape is different (Attio's own click-anywhere-
4950
+ * opens gesture, no truncation gate). `CellExpandOverlay` itself ALSO portals to
4951
+ * `document.body` now (ported from this exact fix) -- both exist because a
4952
+ * `position: sticky` `<td>` clips ANY descendant content that grows past its own
4953
+ * box to the table's scrollport, in EVERY browser tested, regardless of
4954
+ * `overflow: visible` on every element in between and regardless of how high its
4955
+ * `z-index` is raised -- neither of which is a stacking-context or overflow bug
4956
+ * fixable from the INSIDE. A portal sidesteps it entirely by not being a
4957
+ * descendant of the sticky `<td>` at all once mounted. */ function isChipListEditable(column) {
4863
4958
  return !!column.editable && column.type === "chip-list";
4864
4959
  }
4865
4960
  function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyFirst, stickyLast, cellContextMenu, onViewEditHistory, cellContextMenuItems }) {
@@ -4868,10 +4963,9 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
4868
4963
  stickyFirst
4869
4964
  });
4870
4965
  return /*#__PURE__*/ jsx(Fragment, {
4871
- children: effective.map((entry, index)=>{
4966
+ children: effective.map((entry)=>{
4872
4967
  const { column } = entry;
4873
- const isLast = index === effective.length - 1;
4874
- const sticky = pinnedCellProps(entry, stickyLast, isLast);
4968
+ const sticky = pinnedCellProps(entry, stickyLast);
4875
4969
  return /*#__PURE__*/ jsx(DataTableBodyCell, {
4876
4970
  column: column,
4877
4971
  row: row,
@@ -5019,6 +5113,16 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
5019
5113
  // mounted, in `document.body` -- see `isChipListEditable`'s doc comment) and by
5020
5114
  // the reposition effect further down.
5021
5115
  const popoverRef = React.useRef(null);
5116
+ // Feature C's own portaled overlay (`CellExpandOverlay`, editors.tsx) -- its own DOM
5117
+ // node, once mounted, same reasoning as `popoverRef` above: it lives outside
5118
+ // `cellRef`'s subtree (a sibling of the table in `document.body`, not a descendant of
5119
+ // the `<td>`), so the outside-click detector below needs a ref to it too. Without
5120
+ // this, `isInside` saw every click/keystroke INSIDE the portaled editor as "outside"
5121
+ // once the overlay actually escaped the `<td>` (this fix's whole point) -- e.g.
5122
+ // typing into the overlay's own `TextEditor` fires a pointerdown on an input that is
5123
+ // no longer `cellRef.current`'s descendant, so every keystroke closed the overlay and
5124
+ // cancelled the edit before the user finished typing.
5125
+ const expandOverlayRef = React.useRef(null);
5022
5126
  // Closes the overlay on Escape or an outside click/pointerdown -- a gated
5023
5127
  // editor's OWN Escape/blur handling already ends `isEditing` on its own (see
5024
5128
  // `TextEditor`/`SelectEditor`'s existing onCancel/onOpenChange wiring), which
@@ -5031,7 +5135,7 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
5031
5135
  React.useEffect(()=>{
5032
5136
  if (!overlayOpen) return;
5033
5137
  function isInside(target) {
5034
- return (cellRef.current?.contains(target) ?? false) || (popoverRef.current?.contains(target) ?? false);
5138
+ return (cellRef.current?.contains(target) ?? false) || (popoverRef.current?.contains(target) ?? false) || (expandOverlayRef.current?.contains(target) ?? false);
5035
5139
  }
5036
5140
  function handlePointerDown(event) {
5037
5141
  if (!isInside(event.target)) {
@@ -5104,7 +5208,9 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
5104
5208
  rowId: row.id,
5105
5209
  classNames: classNames,
5106
5210
  locale: locale,
5107
- overlayActive: overlayActive
5211
+ overlayActive: overlayActive,
5212
+ cellRef: cellRef,
5213
+ expandOverlayRef: expandOverlayRef
5108
5214
  }),
5109
5215
  cellContextMenu && contextMenuOpen && contextMenuPoint && // A `<td>` (unlike its own parent `<tr>`) can validly hold any flow
5110
5216
  // content, so `CellContextMenu`'s own 0x0 `MenuAnchor` -- itself never
@@ -5149,7 +5255,7 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
5149
5255
  * display (a custom `render` wins there, same as before `editable` existed; it never
5150
5256
  * overrides an actual editing/always-live state, matching crisp's own split between
5151
5257
  * `renderTyped`'s rest-state branch and the editing branches above it).
5152
- */ function CellContent({ column, row, rowId, classNames, locale, overlayActive }) {
5258
+ */ function CellContent({ column, row, rowId, classNames, locale, overlayActive, cellRef, expandOverlayRef }) {
5153
5259
  const { isEditing, isPending, getEditError, commitEdit, cancelEdit } = useDataTableContext();
5154
5260
  const rawValue = getColumnValue(column, row);
5155
5261
  const pending = isPending(rowId, column.key);
@@ -5243,6 +5349,8 @@ function ColumnBodyCells({ columns, row, classNames, locale, selectable, stickyF
5243
5349
  children: [
5244
5350
  overlayActive ? /*#__PURE__*/ jsx(CellExpandOverlay, {
5245
5351
  className: classNames.expandOverlay,
5352
+ anchorRef: cellRef,
5353
+ contentRef: expandOverlayRef,
5246
5354
  children: column.editable ? node : /*#__PURE__*/ jsx("span", {
5247
5355
  className: classNames.expandOverlayText,
5248
5356
  children: node
@@ -5406,10 +5514,9 @@ function ColumnCalcCells({ columns, rows, selectable, stickyFirst, stickyLast })
5406
5514
  stickyFirst
5407
5515
  });
5408
5516
  return /*#__PURE__*/ jsx(Fragment, {
5409
- children: effective.map((entry, index)=>{
5517
+ children: effective.map((entry)=>{
5410
5518
  const { column } = entry;
5411
- const isLast = index === effective.length - 1;
5412
- const sticky = pinnedCellProps(entry, stickyLast, isLast);
5519
+ const sticky = pinnedCellProps(entry, stickyLast);
5413
5520
  return /*#__PURE__*/ jsx(DataTableFooterCell, {
5414
5521
  "data-sticky": sticky["data-sticky"],
5415
5522
  "data-sticky-edge": sticky["data-sticky-edge"],
@@ -5632,10 +5739,9 @@ const DEFAULT_SORT_ICON = {
5632
5739
  });
5633
5740
  const [renamingKey, setRenamingKey] = React.useState(null);
5634
5741
  return /*#__PURE__*/ jsx(Fragment, {
5635
- children: effective.map((entry, index)=>{
5742
+ children: effective.map((entry)=>{
5636
5743
  const { column, label, width } = entry;
5637
- const isLast = index === effective.length - 1;
5638
- const sticky = pinnedCellProps(entry, stickyLast, isLast);
5744
+ const sticky = pinnedCellProps(entry, stickyLast);
5639
5745
  return /*#__PURE__*/ jsxs(DataTableHeaderCell, {
5640
5746
  columnKey: column.key,
5641
5747
  sortable: column.sortable,
@@ -11645,6 +11751,12 @@ function DataTableInner(props, ref) {
11645
11751
  const columnKeys = React.useMemo(()=>columns.map((column)=>column.key), [
11646
11752
  columns
11647
11753
  ]);
11754
+ // Threaded down to `reconcileColumnOrder` (react-headless's `dom.ts`) so a brand-new
11755
+ // column added to `columns` after mount gets appended BEFORE this one, not past it --
11756
+ // see that function's own doc comment for the bug this prevents.
11757
+ const actionsColumnKey = React.useMemo(()=>columns.find((column)=>column.actions)?.key, [
11758
+ columns
11759
+ ]);
11648
11760
  const searchable = React.useMemo(()=>columns.some((column)=>column.searchable), [
11649
11761
  columns
11650
11762
  ]);
@@ -11687,6 +11799,7 @@ function DataTableInner(props, ref) {
11687
11799
  toolbarFilters: resolvedToolbarFilters,
11688
11800
  onToolbarFiltersChange: handleToolbarFiltersChange,
11689
11801
  columnKeys: columnKeys,
11802
+ actionsColumnKey: actionsColumnKey,
11690
11803
  columnOrder: columnOrder,
11691
11804
  defaultColumnOrder: defaultColumnOrder,
11692
11805
  onColumnOrderChange: onColumnOrderChange,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crispy-seed/data-table",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "description": "Self-contained, publishable build of seed-design's Attio-measured DataTable, ResizablePanel and RecordPreview — one install, one CSS import.",
5
5
  "repository": {
6
6
  "type": "git",