From 3a77dae6e6fe718544c56e99a4c59a745abe6367 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 20 Sep 2026 19:32:09 +0000 Subject: [PATCH 1/2] feat(app): reveal search row-selection checkboxes on hover The checkbox column was permanently visible on every search row, spending attention and a glyph per row on a feature most users will not reach for. Fade it in on row hover or checkbox focus instead; any selection puts the table in selection mode and reveals all of them, so shift-click ranges stay aimable. The cell keeps its width in every state so the data columns never reflow, and touch devices keep the checkbox visible. Co-authored-by: Mike Shi --- .changeset/search-row-select-hover-reveal.md | 11 +++++++ packages/app/src/components/DBRowTable.tsx | 4 ++- .../components/DBTable/RowSelectionCells.tsx | 7 +++- packages/app/styles/LogTable.module.scss | 27 ++++++++++++++++ .../tests/e2e/components/TableComponent.ts | 13 ++++++++ .../features/search/row-multi-select.spec.ts | 32 +++++++++++++++++++ 6 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 .changeset/search-row-select-hover-reveal.md diff --git a/.changeset/search-row-select-hover-reveal.md b/.changeset/search-row-select-hover-reveal.md new file mode 100644 index 0000000000..dca0f31ad7 --- /dev/null +++ b/.changeset/search-row-select-hover-reveal.md @@ -0,0 +1,11 @@ +--- +'@hyperdx/app': patch +--- + +feat: reveal search row-selection checkboxes on hover + +The multi-select checkbox now fades in when a row is hovered or the checkbox +takes keyboard focus, instead of sitting on every row all the time. Selecting +any row reveals every checkbox so shift-click ranges stay aimable, and touch +devices (no hover) keep them visible. The cell keeps its width in every state, +so nothing reflows on hover. diff --git a/packages/app/src/components/DBRowTable.tsx b/packages/app/src/components/DBRowTable.tsx index 34033eface..f507a35e62 100644 --- a/packages/app/src/components/DBRowTable.tsx +++ b/packages/app/src/components/DBRowTable.tsx @@ -1009,7 +1009,9 @@ export const RawLogTable = memo( /> )} 0, + })} style={{ minWidth: tableMinWidth }} id={tableId} > diff --git a/packages/app/src/components/DBTable/RowSelectionCells.tsx b/packages/app/src/components/DBTable/RowSelectionCells.tsx index 8449d32623..aa73ba07b8 100644 --- a/packages/app/src/components/DBTable/RowSelectionCells.tsx +++ b/packages/app/src/components/DBTable/RowSelectionCells.tsx @@ -23,12 +23,17 @@ export const RowSelectionCell = memo( isSelected: boolean; onToggle: (rowId: string, options: { extendRange: boolean }) => void; }) => ( -
+ e.stopPropagation()} diff --git a/packages/app/styles/LogTable.module.scss b/packages/app/styles/LogTable.module.scss index 29cea10431..7c12ca4d33 100644 --- a/packages/app/styles/LogTable.module.scss +++ b/packages/app/styles/LogTable.module.scss @@ -50,6 +50,10 @@ $button-height: 18px; opacity: 1; } + &:hover .selectCell { + opacity: 1; + } + &:hover .rowContentButton { background-color: var(--log-table-muted-bg); } @@ -63,9 +67,32 @@ $button-height: 18px; } } +// Search is a read-and-navigate surface, so the checkbox stays out of the way +// until the row is hovered. The cell keeps its width whatever the state — +// revealing the control must never reflow the data columns. .selectCell { vertical-align: middle; user-select: none; + opacity: 0; + transition: opacity 0.15s ease-in-out; + + // Tab still reaches the checkbox while it is transparent. + &:focus-within { + opacity: 1; + } +} + +// One checked row puts the table in selection mode and reveals every checkbox, +// so a shift-click range has visible endpoints and checked rows stay marked. +.selectionActive .selectCell { + opacity: 1; +} + +// Touch has no hover to reveal with. +@media (hover: none) { + .selectCell { + opacity: 1; + } } .selectCheckbox { diff --git a/packages/app/tests/e2e/components/TableComponent.ts b/packages/app/tests/e2e/components/TableComponent.ts index 05f8df27e1..c3e9e4cd13 100644 --- a/packages/app/tests/e2e/components/TableComponent.ts +++ b/packages/app/tests/e2e/components/TableComponent.ts @@ -123,6 +123,19 @@ export class TableComponent { return this.getRow(index).getByTestId('row-select-checkbox'); } + /** + * The cell holding a row's multi-select checkbox. It is the element that + * fades the checkbox in and out, so assert visibility on it rather than on + * the checkbox itself. + */ + getRowCheckboxCell(index: number) { + return this.getRow(index).getByTestId('row-select-cell'); + } + + async hoverRow(index: number) { + await this.getRow(index).hover(); + } + /** * Select multiple rows by indices */ diff --git a/packages/app/tests/e2e/features/search/row-multi-select.spec.ts b/packages/app/tests/e2e/features/search/row-multi-select.spec.ts index aeb050d953..5052347168 100644 --- a/packages/app/tests/e2e/features/search/row-multi-select.spec.ts +++ b/packages/app/tests/e2e/features/search/row-multi-select.spec.ts @@ -9,6 +9,38 @@ test.describe('Search row multi-select', { tag: '@search' }, () => { await searchPage.goto(); }); + test( + 'reveals a row checkbox only while the row is hovered', + { tag: ['@local-mode'] }, + async () => { + const checkbox = searchPage.table.getRowCheckboxCell(0); + await expect(checkbox).toHaveCSS('opacity', '0'); + + await searchPage.table.hoverRow(0); + await expect(checkbox).toHaveCSS('opacity', '1'); + + await searchPage.table.hoverRow(3); + await expect(checkbox).toHaveCSS('opacity', '0'); + }, + ); + + test( + 'shows every checkbox while a selection exists', + { tag: ['@local-mode'] }, + async () => { + // Row 3 is never hovered here, so it can only be visible because the + // table is in selection mode — which is what keeps shift-click aimable. + const unhovered = searchPage.table.getRowCheckboxCell(3); + await expect(unhovered).toHaveCSS('opacity', '0'); + + await searchPage.table.selectRows([0]); + await expect(unhovered).toHaveCSS('opacity', '1'); + + await searchPage.clearRowSelection(); + await expect(unhovered).toHaveCSS('opacity', '0'); + }, + ); + test( 'selects rows and reports the count', { tag: ['@local-mode'] }, From 31cf6dfde72a6c0eec2eea9b424234db77e6ddac Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 21 Sep 2026 01:23:25 +0000 Subject: [PATCH 2/2] fix(app): stop a clicked row checkbox from staying visible, and slim it Clicking a checkbox leaves DOM focus on it, so the :focus-within reveal rule pinned it visible on the row the user had just unchecked. Match :focus-visible instead, which a mouse click does not set. Also drop the glyph from 16px to 14px, trimming the column from 24px to 18px. Co-authored-by: Mike Shi --- .changeset/search-row-select-hover-reveal.md | 3 ++- .../components/DBTable/RowSelectionCells.tsx | 9 +++++-- packages/app/styles/LogTable.module.scss | 6 +++-- .../features/search/row-multi-select.spec.ts | 24 +++++++++++++++++++ 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/.changeset/search-row-select-hover-reveal.md b/.changeset/search-row-select-hover-reveal.md index dca0f31ad7..dc1b197646 100644 --- a/.changeset/search-row-select-hover-reveal.md +++ b/.changeset/search-row-select-hover-reveal.md @@ -5,7 +5,8 @@ feat: reveal search row-selection checkboxes on hover The multi-select checkbox now fades in when a row is hovered or the checkbox -takes keyboard focus, instead of sitting on every row all the time. Selecting +takes keyboard focus, instead of sitting on every row all the time, and is a +little smaller so its column costs less horizontal room. Selecting any row reveals every checkbox so shift-click ranges stay aimable, and touch devices (no hover) keep them visible. The cell keeps its width in every state, so nothing reflows on hover. diff --git a/packages/app/src/components/DBTable/RowSelectionCells.tsx b/packages/app/src/components/DBTable/RowSelectionCells.tsx index aa73ba07b8..322ae2dd69 100644 --- a/packages/app/src/components/DBTable/RowSelectionCells.tsx +++ b/packages/app/src/components/DBTable/RowSelectionCells.tsx @@ -3,7 +3,12 @@ import { Checkbox } from '@mantine/core'; import styles from '@styles/LogTable.module.scss'; -export const ROW_SELECTION_COLUMN_WIDTH = 24; +// Slimmer than Mantine's xs (16px): the column is pure overhead on a table +// that is already short of horizontal room. Plus the 4px edge gap the +// checkbox carries in `.selectCheckbox`. +const CHECKBOX_SIZE = 14; + +export const ROW_SELECTION_COLUMN_WIDTH = CHECKBOX_SIZE + 4; const CELL_WIDTH_STYLE = { width: ROW_SELECTION_COLUMN_WIDTH, @@ -30,7 +35,7 @@ export const RowSelectionCell = memo( > { }, ); + test( + 'hides the checkbox again once the row is unchecked', + { tag: ['@local-mode'] }, + async ({ page }) => { + const checkbox = searchPage.table.getRowCheckboxCell(0); + + await searchPage.table.selectRows([0]); + await expect(searchPage.selectionCount).toBeVisible(); + // Clicking again unchecks it, and leaves focus on the checkbox. + await searchPage.table.selectRows([0]); + await expect(searchPage.selectionCount).toBeHidden(); + + await searchPage.table.hoverRow(5); + await expect(checkbox).toHaveCSS('opacity', '0'); + + // That focus must still reveal the checkbox once it is keyboard focus: + // shift-tab walks back from the row's expand button to its checkbox. + await searchPage.table.expandRow(0); + await searchPage.table.hoverRow(5); + await page.keyboard.press('Shift+Tab'); + await expect(checkbox).toHaveCSS('opacity', '1'); + }, + ); + test( 'shows every checkbox while a selection exists', { tag: ['@local-mode'] },