basalt-ui 2.6.1 → 2.7.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.
@@ -15,7 +15,56 @@
15
15
  * which was the whole reason for the rename in the first place. Write
16
16
  * `key={fieldKey(form, path)} {...inputProps(form, path)}`.
17
17
  */
18
- import type { LooseKeys, UseFormReturnType } from '@mantine/form'
18
+ import type { GetInputPropsReturnType, LooseKeys, UseFormReturnType } from '@mantine/form'
19
+ import type { ReactNode } from 'react'
20
+
21
+ /**
22
+ * What `inputProps` returns: Mantine's shape with `error` typed (`any` upstream). `error` is always
23
+ * present — Mantine sets the key on every call, `undefined` on a clean field — so it is `ReactNode`,
24
+ * not optional. It is the one typed way to read a field error: `form.errors` is an index signature
25
+ * (dot access is TS4111), and a raw control inside `FormRow` takes
26
+ * `<FormRow error={inputProps(form, path).error}>`. A Mantine input spread with `inputProps` already
27
+ * renders it; passing it to `FormRow` as well renders it twice.
28
+ */
29
+ export type InputProps = Omit<GetInputPropsReturnType, 'error'> & { error: ReactNode }
30
+
31
+ type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
32
+
33
+ type AllDigits<S extends string> = S extends `${Digit}${infer Rest}`
34
+ ? Rest extends ''
35
+ ? true
36
+ : AllDigits<Rest>
37
+ : false
38
+
39
+ /** A canonical array index: digits only, no sign, no decimal point, no exponent, no leading zero. */
40
+ type IsIndex<S extends string> = S extends '0'
41
+ ? true
42
+ : S extends `0${string}`
43
+ ? false
44
+ : AllDigits<S>
45
+
46
+ /** One path segment applied to a value; `never` when the segment does not exist there. */
47
+ type Child<T, K extends string> =
48
+ NonNullable<T> extends infer N
49
+ ? N extends readonly (infer V)[]
50
+ ? IsIndex<K> extends true
51
+ ? V
52
+ : never
53
+ : K extends keyof N
54
+ ? N[K]
55
+ : never
56
+ : never
57
+
58
+ type ValueAt<T, P extends string> = P extends `${infer Head}.${infer Rest}`
59
+ ? ValueAt<Child<T, Head>, Rest>
60
+ : Child<T, P>
61
+
62
+ /** `Path` if the value at that dot path is a (possibly optional/nullable) boolean, else `never`. */
63
+ type BooleanAt<Values, Path extends string> = [ValueAt<Values, Path>] extends [never]
64
+ ? never
65
+ : [NonNullable<ValueAt<Values, Path>>] extends [boolean]
66
+ ? Path
67
+ : never
19
68
 
20
69
  // ── inputProps ────────────────────────────────────────────────────────────────
21
70
 
@@ -37,8 +86,34 @@ import type { LooseKeys, UseFormReturnType } from '@mantine/form'
37
86
  export function inputProps<Values extends Record<string, unknown>, Path extends LooseKeys<Values>>(
38
87
  form: UseFormReturnType<Values>,
39
88
  path: Path,
40
- ): ReturnType<UseFormReturnType<Values>['getInputProps']> {
41
- return form.getInputProps(path)
89
+ ): InputProps
90
+ /**
91
+ * The boolean overload: `{ type: 'checkbox' }` binds a boolean field to Mantine `Checkbox` and
92
+ * `Switch`. It returns `onChange` plus `defaultChecked` in `useBasaltForm`'s default `uncontrolled`
93
+ * mode, or `checked` under `mode: 'controlled'` (Mantine decides; there is no `value`).
94
+ *
95
+ * Only a path whose value is a boolean (optional/nullable included) type-checks; a string path is a
96
+ * type error, not a silently wrong binding. Two limits of that check:
97
+ * - The path must be a literal. A `string`-typed path is rejected here (use a literal, or the
98
+ * one-argument form), so the path argument does not autocomplete.
99
+ * - On a union parent, a key present on several members is accepted only when every member types it
100
+ * as boolean; array segments must be canonical indices (`flags.0`, never `flags.1.5` or `flags.01`).
101
+ *
102
+ * @example
103
+ * <Switch key={fieldKey(form, 'active')} {...inputProps(form, 'active', { type: 'checkbox' })} />
104
+ */
105
+ export function inputProps<Values extends Record<string, unknown>, Path extends string>(
106
+ form: UseFormReturnType<Values>,
107
+ path: BooleanAt<Values, Path>,
108
+ options: { type: 'checkbox' },
109
+ ): InputProps
110
+ export function inputProps<Values extends Record<string, unknown>>(
111
+ form: UseFormReturnType<Values>,
112
+ path: string,
113
+ options?: { type: 'checkbox' },
114
+ ): InputProps {
115
+ // Mantine types `error` optional but sets the key on every call (`withError` defaults to true).
116
+ return form.getInputProps(path, options) as InputProps
42
117
  }
43
118
 
44
119
  /**
@@ -6,14 +6,13 @@
6
6
  * `basalt-ui`. Same reason `./charts` and `./tokens` stay off the root barrel, one optional peer
7
7
  * over. Import everything below from `basalt-ui/forms`.
8
8
  *
9
- * Four pieces: the form (`useBasaltForm` + `inputProps`/`fieldKey`), the submit lifecycle
10
- * (`useFormSubmit` + `FormStateProvider`), the list field (`useFieldArray`) and the draft
11
- * (`useFormDraft`). The layout (`FormSection` / `FormRow` / `FormGroup` / `FormActions`) lives in
12
- * `basalt-ui/controls` (it needs no `@mantine/form`); `useForm`/`schemaResolver` come from
13
- * `@mantine/form` directly. Both stopped being re-exported here in 1.39.0.
9
+ * One piece: the form (`useBasaltForm` + `inputProps`/`fieldKey`). The layout (FormRow /
10
+ * FormGroup) lives in `basalt-ui/controls` (it needs no `@mantine/form`); the form hook and
11
+ * schema resolvers come from `@mantine/form` directly.
14
12
  *
15
13
  * Optional peer: @mantine/form ^9.3.0 — install with: bun add @mantine/form
16
14
  */
17
15
 
18
16
  export { useBasaltForm } from './create-form'
19
17
  export { inputProps, fieldKey } from './field'
18
+ export type { InputProps } from './field'
package/src/surfaces.ts CHANGED
@@ -502,7 +502,7 @@ export const SURFACES = {
502
502
  'C12 — one shape for refresh/sync (SyncButton); only the alias table sees a renamed copy',
503
503
  ],
504
504
  description:
505
- 'The control tier (docs/CONTROLS-SPEC.md §3): FilterSet (nowrap row + measured +N fold + the mobile Filters (n) sheet), RangeFilter/CompareFilter/SelectFilter/MultiSelectFilter/NumberFilter/SearchFilter/ToggleFilter (each bound to a FieldHandle — no value/onChange/size, law C2/C5; NumberFilter is the field.number lane, a radio list over `options` or a stepper without), ViewTabs, the action/sync family (ActionGroup, OverflowMenu, SyncButton, BarAction/GlobalAction), and the form layout FormSection/FormRow/FormGroup/FormActions (moved here from ./forms in 1.36.0 — FormRow is law C1’s form row and needs no @mantine/form). THREE surfaces, picked by the home and never by a prop (useFilterSurface): pill in a PageBar row, sheet in the mobile Filters (n) drawer, panel in a PageAside body — where every control renders an inspector/facet ROW instead of a chip (docs/ASIDE-SPEC.md §3). PanelRow is that row primitive (label above, hint, mono readout, an `end` control on the label line) and SliderControl is the one bound control with no pill form at all; the counts/max props on MultiSelectFilter render the Foundry facet list there. Every control owns its own desktop/mobile swap in CSS (C9) and renders size="ctl" internally. Resolves and renders with NO @mantine/dates installed — the custom date picker is injected through RangeFilter.customPicker from ./controls-dates.',
505
+ 'The control tier (docs/CONTROLS-SPEC.md §3). Exports: `FilterSet` (nowrap row + measured +N fold + the mobile Filters (n) sheet), `RangeFilter`/`SelectFilter`/`MultiSelectFilter`/`NumberFilter`/`SearchFilter`/`ToggleFilter` (each bound to a FieldHandle — no value/onChange/size, law C2/C5; `NumberFilter` is the field.number lane, a radio list over `options` or a stepper without), `ViewTabs`, `SyncButton` with the `BarAction`/`GlobalAction` types, and the form layout `FormRow`/`FormGroup` (moved here from ./forms in 1.36.0 — `FormRow` is law C1’s form row and needs no @mantine/form). THREE surfaces, picked by the home and never by a prop: pill in a PageBar row, sheet in the mobile Filters (n) drawer, panel in a PageAside body — where every control renders an inspector/facet ROW instead of a chip (docs/ASIDE-SPEC.md §3); the counts/max props on `MultiSelectFilter` render the Foundry facet list there. Every control owns its own desktop/mobile swap in CSS (C9) and renders size="ctl" internally. Resolves and renders with NO @mantine/dates installed — the custom date picker is injected through RangeFilter.customPicker from ./controls-dates.',
506
506
  optionalPeers: [],
507
507
  // `@mantine/dates` is an optional peer basaltViteConfig pre-bundles the whole @mantine scope
508
508
  // for — a consumer without the peer (linewatch) must never resolve it through this surface.
@@ -32,6 +32,7 @@
32
32
  * />
33
33
  */
34
34
  import {
35
+ ActionIcon,
35
36
  Box,
36
37
  Checkbox,
37
38
  Group,
@@ -46,6 +47,7 @@ import {
46
47
  } from '@mantine/core'
47
48
  import type { MantineSpacing } from '@mantine/core'
48
49
  import type {
50
+ Cell,
49
51
  Column,
50
52
  ColumnDef,
51
53
  ColumnFiltersState,
@@ -69,8 +71,8 @@ import { HIT_COARSE } from '../common/hit-floor'
69
71
  import { useMeasuredWidths } from '../common/use-measured-widths'
70
72
  import { assertRequiredProps, useValidateProps } from '../common/validate'
71
73
  import { FilterSet } from '../controls'
72
- import { ActionGroup, BarActionSlot } from '../controls/actions'
73
- import type { BarAction, SlotActions } from '../controls/actions'
74
+ import { ActionGroup, BarActionSlot, OverflowMenu } from '../controls/actions'
75
+ import type { BarAction, BarActionItem, SlotActions } from '../controls/actions'
74
76
  // `EnumFilter` is deliberately NOT on the `./controls` barrel (`docs/CONTROLS-SPEC.md` §3 —
75
77
  // reaching for it directly is hand-rolling a filter, `basalt/hand-rolled-filter`); a facet column
76
78
  // is the one place inside the framework itself that legitimately builds a `FieldHandle` over
@@ -78,6 +80,7 @@ import type { BarAction, SlotActions } from '../controls/actions'
78
80
  import { EnumFilter } from '../controls/enum-filter'
79
81
  import { MultiSelectFilter } from '../controls/multi-select-filter'
80
82
  import { CtlSlot } from '../theme'
83
+ import { IconSlot } from '../theme/icon-slot'
81
84
  import { alpha, VX } from '../tokens'
82
85
  import type { EnumField, FieldHandle, MultiField } from '../state/fields'
83
86
  import { WidgetHeader } from '../dashboard'
@@ -358,6 +361,113 @@ function selectionColumn<T extends RowData>(): ColumnDef<BasaltTableFeatures, T,
358
361
  }
359
362
  }
360
363
 
364
+ // ── Row actions ───────────────────────────────────────────────────────────────
365
+
366
+ /** One per-row action: `BarActionItem`'s vocabulary with the row handed to the handler. */
367
+ export type DataTableRowAction<T = unknown> = {
368
+ key: string
369
+ label: string
370
+ /** A lone action with an icon renders as that icon button; anything else folds into the menu. */
371
+ icon?: ReactNode
372
+ onClick: (row: T) => void
373
+ danger?: boolean
374
+ /** A disabled action renders inert and never fires. */
375
+ disabled?: (row: T) => boolean
376
+ /** A loading action shows the busy state (icon button) or sits inert (menu) and never fires. */
377
+ loading?: (row: T) => boolean
378
+ }
379
+
380
+ /** The synthetic trailing column `rowActions` appends — namespaced like {@link SELECT_COLUMN_ID}. */
381
+ const ACTIONS_COLUMN_ID = '__basalt_actions__'
382
+
383
+ /** Wide enough for one 30px control plus the cell's horizontal padding. */
384
+ const ACTIONS_COLUMN_SIZE = 56
385
+
386
+ /**
387
+ * The cells whose own controls must not read as "activate the row": a tickable selection cell (a
388
+ * disabled row's tap IS a row tap) and every actions cell.
389
+ */
390
+ function stopsRowActivation<T extends RowData>(
391
+ cell: Cell<BasaltTableFeatures, T, unknown>,
392
+ ): boolean {
393
+ if (cell.column.id === ACTIONS_COLUMN_ID) return true
394
+ return cell.column.id === SELECT_COLUMN_ID && cell.row.getCanSelect()
395
+ }
396
+
397
+ function RowActionCell<T>({
398
+ actions,
399
+ row,
400
+ rowIndex,
401
+ rowLabel,
402
+ }: {
403
+ actions: readonly DataTableRowAction<T>[]
404
+ row: T
405
+ rowIndex: number
406
+ rowLabel: ((row: T) => string) | undefined
407
+ }): ReactNode {
408
+ const named = rowLabel?.(row)
409
+ const [only] = actions
410
+ if (actions.length === 1 && only?.icon !== undefined) {
411
+ const inert = only.disabled?.(row) === true || only.loading?.(row) === true
412
+ return (
413
+ <ActionIcon
414
+ variant="subtle"
415
+ size="ctl"
416
+ aria-label={named === undefined ? only.label : `${only.label}: ${named}`}
417
+ disabled={only.disabled?.(row) === true}
418
+ loading={only.loading?.(row) === true}
419
+ {...(only.danger === true && { color: 'red' })}
420
+ onClick={() => {
421
+ if (!inert) only.onClick(row)
422
+ }}
423
+ >
424
+ <IconSlot>{only.icon}</IconSlot>
425
+ </ActionIcon>
426
+ )
427
+ }
428
+ const items: BarActionItem[] = actions.map((action) => ({
429
+ key: action.key,
430
+ label: action.label,
431
+ ...(action.icon !== undefined && { icon: action.icon }),
432
+ ...(action.danger === true && { danger: true }),
433
+ // The menu has no busy state of its own, so loading reads as inert there.
434
+ disabled: action.disabled?.(row) === true || action.loading?.(row) === true,
435
+ onClick: () => action.onClick(row),
436
+ }))
437
+ return (
438
+ <OverflowMenu
439
+ actions={items}
440
+ trigger="kebab"
441
+ label={named === undefined ? `Row ${rowIndex + 1} actions` : `Actions for ${named}`}
442
+ />
443
+ )
444
+ }
445
+
446
+ /**
447
+ * Header-less: the `<th>` carries a screen-reader-only name and nothing sorts across it. Built per
448
+ * render for the same reason as {@link selectionColumn} (`ColumnDef` is generic in `T`).
449
+ */
450
+ function rowActionsColumn<T extends RowData>(
451
+ actions: readonly DataTableRowAction<T>[],
452
+ rowLabel: ((row: T) => string) | undefined,
453
+ ): ColumnDef<BasaltTableFeatures, T, unknown> {
454
+ return {
455
+ id: ACTIONS_COLUMN_ID,
456
+ size: ACTIONS_COLUMN_SIZE,
457
+ enableSorting: false,
458
+ enableHiding: false,
459
+ header: () => <span className={classes.srOnly}>Actions</span>,
460
+ cell: ({ row }) => (
461
+ <RowActionCell
462
+ actions={actions}
463
+ row={row.original}
464
+ rowIndex={row.index}
465
+ rowLabel={rowLabel}
466
+ />
467
+ ),
468
+ }
469
+ }
470
+
361
471
  /**
362
472
  * The bar a selection raises, above the table and below its header — the count, then the actions,
363
473
  * through the SAME `BarAction[]` vocabulary `PageBar` uses, so the ≤3-inline fold and the mobile
@@ -661,6 +771,27 @@ export type BasaltDataTableProps<T extends RowData> = BasaltProps &
661
771
  */
662
772
  bulkActions?: (rows: T[]) => BarAction[]
663
773
 
774
+ /**
775
+ * Per-row actions as typed data. The table renders them itself in a trailing, pinned,
776
+ * header-less column: an icon button for one action that carries an `icon`, an overflow menu
777
+ * for anything else. Clicking either never fires `onRowActivate`. Never put a `Menu` or
778
+ * `ActionIcon` in a `display` / `cell` renderer (`basalt/control-outside-home`).
779
+ *
780
+ * Pass a STABLE reference — a module constant or a `useMemo` — because the column is rebuilt
781
+ * whenever the array identity changes. A `renderCard` projection owns its own card, so the
782
+ * column renders in the table body only.
783
+ *
784
+ * @example
785
+ * const ROW_ACTIONS: DataTableRowAction<Row>[] = [
786
+ * { key: 'edit', label: 'Edit', icon: <IconEdit />, onClick: (row) => edit(row) },
787
+ * { key: 'delete', label: 'Delete', danger: true, onClick: (row) => remove(row) },
788
+ * ]
789
+ * <BasaltDataTable rowActions={ROW_ACTIONS} rowLabel={(row) => row.name} … />
790
+ */
791
+ rowActions?: DataTableRowAction<T>[]
792
+ /** Names a row for assistive tech in its action controls; default `Row N actions`. */
793
+ rowLabel?: (row: T) => string
794
+
664
795
  // ── Pagination ────────────────────────────────────────────────────────────────
665
796
 
666
797
  /**
@@ -1354,6 +1485,8 @@ export function BasaltDataTable<T extends RowData>(props: BasaltDataTableProps<T
1354
1485
  enableRowSelection = false,
1355
1486
  getRowId,
1356
1487
  bulkActions,
1488
+ rowActions,
1489
+ rowLabel,
1357
1490
  enablePagination = false,
1358
1491
  pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,
1359
1492
  manualPagination = false,
@@ -1443,9 +1576,14 @@ export function BasaltDataTable<T extends RowData>(props: BasaltDataTableProps<T
1443
1576
  // The checkbox column is PREPENDED here rather than asked of the caller, so `columns` stays the
1444
1577
  // caller's own data columns and `meta.align` / `numeral` never have to reason about a synthetic
1445
1578
  // one. Everything counting columns downstream reads `columnCount`, below.
1579
+ const hasRowActions = rowActions !== undefined && rowActions.length > 0
1446
1580
  const tableColumns = useMemo(
1447
- () => (enableRowSelection ? [selectionColumn<T>(), ...facetColumns] : facetColumns),
1448
- [facetColumns, enableRowSelection],
1581
+ () => [
1582
+ ...(enableRowSelection ? [selectionColumn<T>()] : []),
1583
+ ...facetColumns,
1584
+ ...(hasRowActions ? [rowActionsColumn<T>(rowActions, rowLabel)] : []),
1585
+ ],
1586
+ [facetColumns, enableRowSelection, hasRowActions, rowActions, rowLabel],
1449
1587
  )
1450
1588
 
1451
1589
  // Enforced before the table is built, so the degraded options below are the ones TanStack gets —
@@ -1519,6 +1657,17 @@ export function BasaltDataTable<T extends RowData>(props: BasaltDataTableProps<T
1519
1657
  const hasFolded = foldedColumns.size > 0
1520
1658
  const foldRows = useRowDisclosure({ data, getRowId })
1521
1659
 
1660
+ // The actions column always sticks to the end edge, behind any end-pinned columns the caller
1661
+ // asked for — so it is pinned through TanStack's own state, which also keeps their offsets right.
1662
+ const pinningActive = enablePinning || hasRowActions
1663
+ const tablePinning = useMemo(
1664
+ () =>
1665
+ hasRowActions
1666
+ ? { ...columnPinning, end: [...columnPinning.end, ACTIONS_COLUMN_ID] }
1667
+ : columnPinning,
1668
+ [columnPinning, hasRowActions],
1669
+ )
1670
+
1522
1671
  const table = useTable({
1523
1672
  features: basaltTableFeatures,
1524
1673
  data,
@@ -1527,7 +1676,7 @@ export function BasaltDataTable<T extends RowData>(props: BasaltDataTableProps<T
1527
1676
  sorting: tableState.sorting,
1528
1677
  globalFilter,
1529
1678
  columnFilters: tableState.columnFilters,
1530
- columnPinning,
1679
+ columnPinning: tablePinning,
1531
1680
  columnVisibility,
1532
1681
  ...(enablePagination && { pagination: tableState.pagination }),
1533
1682
  ...(enableRowSelection && { rowSelection: tableState.rowSelection }),
@@ -1556,7 +1705,7 @@ export function BasaltDataTable<T extends RowData>(props: BasaltDataTableProps<T
1556
1705
  ...(manualPagination && pageCount !== undefined && { pageCount }),
1557
1706
  }),
1558
1707
  enableSorting: sortingEnabled,
1559
- enableColumnPinning: enablePinning,
1708
+ enableColumnPinning: pinningActive,
1560
1709
  })
1561
1710
 
1562
1711
  const showSearch = enableGlobalFilter && filteringEnabled
@@ -1575,7 +1724,11 @@ export function BasaltDataTable<T extends RowData>(props: BasaltDataTableProps<T
1575
1724
  // the prepended checkbox column, one more again for the fold-disclosure toggle once anything has
1576
1725
  // folded — a stale count left the empty state and the error row short of the header.
1577
1726
  const columnCount =
1578
- columns.length + (enableRowSelection ? 1 : 0) + (hasFolded ? 1 : 0) - foldedColumns.size
1727
+ columns.length +
1728
+ (enableRowSelection ? 1 : 0) +
1729
+ (hasRowActions ? 1 : 0) +
1730
+ (hasFolded ? 1 : 0) -
1731
+ foldedColumns.size
1579
1732
 
1580
1733
  // `getSelectedRowModel()` is TanStack's own memo, so its `rows` array keeps its identity until
1581
1734
  // the selection or the row model actually moves — which is what makes both memos below hold.
@@ -1594,7 +1747,7 @@ export function BasaltDataTable<T extends RowData>(props: BasaltDataTableProps<T
1594
1747
  : bulkActions(selectedRows),
1595
1748
  [bulkActions, selectedRows],
1596
1749
  )
1597
- const headerGroups = enablePinning ? getOrderedHeaderGroups(table) : table.getHeaderGroups()
1750
+ const headerGroups = pinningActive ? getOrderedHeaderGroups(table) : table.getHeaderGroups()
1598
1751
  // The rows actually rendered, not `data` — a filter or a page index that matches nothing leaves
1599
1752
  // `data` non-empty while the body has nothing in it, and a blank body reads as a broken table.
1600
1753
  const rows = table.getRowModel().rows
@@ -1672,18 +1825,19 @@ export function BasaltDataTable<T extends RowData>(props: BasaltDataTableProps<T
1672
1825
  const canSort = sortingEnabled && header.column.getCanSort()
1673
1826
  const sorted = header.column.getIsSorted()
1674
1827
  const toggleSorting = header.column.getToggleSortingHandler()
1675
- const pinnedStyle = enablePinning
1828
+ const pinnedStyle = pinningActive
1676
1829
  ? getPinnedCellStyle(table, header.column)
1677
1830
  : undefined
1678
1831
  const align = resolveAlign(header.column)
1679
1832
  const isSelectColumn = header.column.id === SELECT_COLUMN_ID
1833
+ const isFoldCandidate = !isSelectColumn && header.column.id !== ACTIONS_COLUMN_ID
1680
1834
  return (
1681
1835
  <Table.Th
1682
1836
  key={header.id}
1683
1837
  {...(isSelectColumn && { className: classes.selectCell })}
1684
1838
  // The measured fold's own read (`useColumnFold`) — never present on the
1685
1839
  // selection column, which is never a fold candidate.
1686
- {...(!isSelectColumn && { [FOLD_ID_ATTR]: header.column.id })}
1840
+ {...(isFoldCandidate && { [FOLD_ID_ATTR]: header.column.id })}
1687
1841
  onClick={canSort ? toggleSorting : undefined}
1688
1842
  onKeyDown={
1689
1843
  canSort
@@ -1731,7 +1885,7 @@ export function BasaltDataTable<T extends RowData>(props: BasaltDataTableProps<T
1731
1885
  </Table.Tr>
1732
1886
  ) : (
1733
1887
  rows.map((row) => {
1734
- const cells = enablePinning
1888
+ const cells = pinningActive
1735
1889
  ? [
1736
1890
  ...row.getStartVisibleCells(),
1737
1891
  ...row.getCenterVisibleCells(),
@@ -1755,7 +1909,7 @@ export function BasaltDataTable<T extends RowData>(props: BasaltDataTableProps<T
1755
1909
  </Table.Td>
1756
1910
  )}
1757
1911
  {cells.map((cell) => {
1758
- const pinnedStyle = enablePinning
1912
+ const pinnedStyle = pinningActive
1759
1913
  ? getPinnedCellStyle(table, cell.column)
1760
1914
  : undefined
1761
1915
  const align = resolveAlign(cell.column)
@@ -1764,11 +1918,11 @@ export function BasaltDataTable<T extends RowData>(props: BasaltDataTableProps<T
1764
1918
  key={cell.id}
1765
1919
  {...(cell.column.id === SELECT_COLUMN_ID && {
1766
1920
  className: classes.selectCell,
1767
- // The label's ::after fills the cell, so a tap on it bubbles to the row —
1768
- // stop it only where a tick is possible; a disabled row's tap is a row tap.
1769
- ...(cell.row.getCanSelect() && {
1770
- onClick: (event: ReactMouseEvent) => event.stopPropagation(),
1771
- }),
1921
+ })}
1922
+ {...(stopsRowActivation(cell) && {
1923
+ // The label's ::after fills the select cell, so a tap on it bubbles to the
1924
+ // row; the actions menu portals out but React bubbles through the portal.
1925
+ onClick: (event: ReactMouseEvent) => event.stopPropagation(),
1772
1926
  })}
1773
1927
  style={{
1774
1928
  ...(typeof cell.getValue() === 'number' &&