@@ -92,25 +92,18 @@ function rowSelectionCoversAll(sel: RowSelection, rows: TableRowType[]): boolean
9292
9393const COL_WIDTH_MIN = 80
9494const COL_WIDTH_AUTO_FIT_MAX = 1000
95- // Wide enough to host the row-number + per-row run button side by side.
96- // Single-digit row numbers (rows 1–9) and multi-digit need to render with
97- // the play button at the same x-position so the column doesn't reflow
98- // row-by-row.
99- //
100- // Bucketed by the table's plan-derived `maxRows`, not the live count: a small
101- // table sized for ≤9,999 always renders the narrow gutter; an enterprise
102- // table sized up to 9,999,999 always renders the wide one. The gutter never
103- // changes width as rows are added.
104- //
105- // Tables without workflow columns drop the per-row run button (~28px), so
106- // the gutter shrinks accordingly.
107- const CHECKBOX_COL_WIDTH_SMALL_WITH_RUN = 48
108- const CHECKBOX_COL_WIDTH_SMALL_NUMBER_ONLY = 32
109- const CHECKBOX_COL_WIDTH_LARGE_WITH_RUN = 68
110- const CHECKBOX_COL_WIDTH_LARGE_NUMBER_ONLY = 52
111- /** Bucket boundary: tables sized for >9,999 rows get the wide gutter. */
112- const LARGE_ROW_NUMBER_THRESHOLD = 10000
11395const ADD_COL_WIDTH = 120
96+
97+ /** Returns sticky row-number column dimensions sized to the digit count of `maxRows`. */
98+ function checkboxColLayout (
99+ maxRows : number ,
100+ hasWorkflowCols : boolean
101+ ) : { colWidth : number ; numDivWidth : number } {
102+ const digits = maxRows > 0 ? Math . floor ( Math . log10 ( maxRows ) ) + 1 : 1
103+ const numDivWidth = Math . max ( 20 , digits * 8 + 4 )
104+ const colWidth = Math . max ( 32 , numDivWidth + 8 ) + ( hasWorkflowCols ? 16 : 0 )
105+ return { colWidth, numDivWidth }
106+ }
114107const SKELETON_COL_COUNT = 4
115108const SKELETON_ROW_COUNT = 10
116109const ROW_HEIGHT_ESTIMATE = 35
@@ -128,7 +121,7 @@ const CELL_HEADER_CHECKBOX =
128121const CELL_CONTENT =
129122 'relative flex h-[22px] min-w-0 items-center overflow-clip text-ellipsis whitespace-nowrap text-small'
130123const SELECTION_OVERLAY =
131- 'pointer-events-none absolute -top-px -right-px -bottom-px -left-px z-[5] border-[2px] border-[var(--selection)]'
124+ 'pointer-events-none absolute -top-px -right-px -bottom-px z-[5] border-[2px] border-[var(--selection)]'
132125
133126/**
134127 * Snapshot of grid selection state the wrapper needs to render `<TableActionBar>`.
@@ -452,22 +445,10 @@ export function TableGrid({
452445 } , [ columns , columnOrder , tableWorkflowGroups ] )
453446
454447 const hasWorkflowColumns = columns . some ( ( c ) => ! ! c . workflowGroupId )
455- /**
456- * The sticky left column hosts the row number / checkbox always, plus a
457- * per-row run button only when the table has workflow columns. Width is
458- * picked from the table's plan-derived `maxRows` so a free-tier table
459- * (≤9,999) gets the narrow gutter and an enterprise table (up to
460- * 9,999,999) gets the wide one. Bucketed, not continuous, so the gutter
461- * never reflows as rows are added.
462- */
463- const isLargeRowCountTable = ( tableData ?. maxRows ?? 0 ) >= LARGE_ROW_NUMBER_THRESHOLD
464- const checkboxColWidth = isLargeRowCountTable
465- ? hasWorkflowColumns
466- ? CHECKBOX_COL_WIDTH_LARGE_WITH_RUN
467- : CHECKBOX_COL_WIDTH_LARGE_NUMBER_ONLY
468- : hasWorkflowColumns
469- ? CHECKBOX_COL_WIDTH_SMALL_WITH_RUN
470- : CHECKBOX_COL_WIDTH_SMALL_NUMBER_ONLY
448+ const { colWidth : checkboxColWidth , numDivWidth } = checkboxColLayout (
449+ tableData ?. maxRows ?? 0 ,
450+ hasWorkflowColumns
451+ )
471452
472453 const headerGroups = useMemo (
473454 ( ) => buildHeaderGroups ( displayColumns , tableWorkflowGroups ) ,
@@ -3053,7 +3034,7 @@ export function TableGrid({
30533034 onRowToggle = { handleRowToggle }
30543035 runningCount = { runningByRowId . get ( row . id ) ?? 0 }
30553036 hasWorkflowColumns = { hasWorkflowColumns }
3056- isLargeRowCountTable = { isLargeRowCountTable }
3037+ numDivWidth = { numDivWidth }
30573038 onStopRow = { onStopRow }
30583039 onRunRow = { handleRunRow }
30593040 workflowGroups = { tableWorkflowGroups }
@@ -3178,8 +3159,8 @@ interface DataRowProps {
31783159 runningCount : number
31793160 /** Whether the table has at least one workflow column — controls whether a run/stop icon is rendered. */
31803161 hasWorkflowColumns : boolean
3181- /** True for tables sized for >9,999 rows; widens the row-number slot to fit 5–7 digit numbers . */
3182- isLargeRowCountTable : boolean
3162+ /** Width of the row-number inner div in px, derived from the table's maxRows digit count . */
3163+ numDivWidth : number
31833164 onStopRow : ( rowId : string ) => void
31843165 onRunRow : ( rowId : string ) => void
31853166 /**
@@ -3239,7 +3220,7 @@ function dataRowPropsAreEqual(prev: DataRowProps, next: DataRowProps): boolean {
32393220 prev . onRowToggle !== next . onRowToggle ||
32403221 prev . runningCount !== next . runningCount ||
32413222 prev . hasWorkflowColumns !== next . hasWorkflowColumns ||
3242- prev . isLargeRowCountTable !== next . isLargeRowCountTable ||
3223+ prev . numDivWidth !== next . numDivWidth ||
32433224 prev . onStopRow !== next . onStopRow ||
32443225 prev . onRunRow !== next . onRunRow ||
32453226 prev . workflowGroups !== next . workflowGroups
@@ -3281,7 +3262,7 @@ const DataRow = React.memo(function DataRow({
32813262 onRowToggle,
32823263 runningCount,
32833264 hasWorkflowColumns,
3284- isLargeRowCountTable ,
3265+ numDivWidth ,
32853266 onStopRow,
32863267 onRunRow,
32873268 workflowGroups,
@@ -3313,20 +3294,23 @@ const DataRow = React.memo(function DataRow({
33133294 return (
33143295 < tr onContextMenu = { ( e ) => onContextMenu ( e , row ) } >
33153296 < td className = { cn ( CELL_CHECKBOX , 'cursor-pointer' ) } >
3316- < div className = 'flex items-center justify-between gap-1' >
3297+ < div
3298+ className = { cn (
3299+ 'flex items-center gap-1' ,
3300+ hasWorkflowColumns ? 'justify-between' : 'justify-center'
3301+ ) }
3302+ >
33173303 < div
3318- className = { cn (
3319- 'group/checkbox flex h-[20px] shrink-0 items-center justify-end' ,
3320- isLargeRowCountTable ? 'w-[40px]' : 'w-[20px]'
3321- ) }
3304+ className = 'group/checkbox flex h-[20px] shrink-0 items-center justify-center'
3305+ style = { { width : numDivWidth } }
33223306 onMouseDown = { ( e ) => {
33233307 if ( e . button !== 0 ) return
33243308 onRowToggle ( rowIndex , e . shiftKey )
33253309 } }
33263310 >
33273311 < span
33283312 className = { cn (
3329- 'text-right text-[var(--text-tertiary)] text-xs tabular-nums' ,
3313+ 'text-center text-[var(--text-tertiary)] text-xs tabular-nums' ,
33303314 isRowSelected ? 'hidden' : 'block group-hover/checkbox:hidden'
33313315 ) }
33323316 >
@@ -3409,7 +3393,8 @@ const DataRow = React.memo(function DataRow({
34093393 { isHighlighted && ( isMultiCell || isRowChecked ) && (
34103394 < div
34113395 className = { cn (
3412- '-top-px -right-px -bottom-px -left-px pointer-events-none absolute z-[4]' ,
3396+ '-top-px -right-px -bottom-px pointer-events-none absolute z-[4]' ,
3397+ colIndex === 0 ? 'left-0' : '-left-px' ,
34133398 SELECTION_TINT_BG ,
34143399 isFirstRow && isTopEdge && 'top-0' ,
34153400 isTopEdge && 'border-t border-t-[var(--selection)]' ,
@@ -3419,7 +3404,15 @@ const DataRow = React.memo(function DataRow({
34193404 ) }
34203405 />
34213406 ) }
3422- { isAnchor && < div className = { cn ( SELECTION_OVERLAY , isFirstRow && 'top-0' ) } /> }
3407+ { isAnchor && (
3408+ < div
3409+ className = { cn (
3410+ SELECTION_OVERLAY ,
3411+ colIndex === 0 ? 'left-0' : '-left-px' ,
3412+ isFirstRow && 'top-0'
3413+ ) }
3414+ />
3415+ ) }
34233416 < div className = { CELL_CONTENT } >
34243417 < CellContent
34253418 value = {
0 commit comments