diff --git a/.changeset/search-row-select-hover-reveal.md b/.changeset/search-row-select-hover-reveal.md
new file mode 100644
index 0000000000..dc1b197646
--- /dev/null
+++ b/.changeset/search-row-select-hover-reveal.md
@@ -0,0 +1,12 @@
+---
+'@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, 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/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..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,
@@ -23,12 +28,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..bec56a301b 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,34 @@ $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. Deliberately not
+ // :focus-within — a mouse click leaves focus on the checkbox, which would
+ // strand it on screen after the row is unchecked.
+ &:has(:focus-visible) {
+ 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..451446253a 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,62 @@ 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(
+ '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'] },
+ 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'] },
|