` returns null via `Transition` mounted state when `selectionCount === 0`.
-
-- [ ] **Step 4: Build client**
-
-Run: `pnpm nx run client:build`
-Expected: build succeeds.
-
-- [ ] **Step 5: Commit**
-
-```bash
-git add apps/client/src/features/base/components/grid/selection-action-bar.tsx apps/client/src/features/base/components/grid/grid-container.tsx apps/client/src/features/base/styles/grid.module.css
-git commit -m "feat(base): floating selection action bar with bulk delete"
-```
-
----
-
-### Task 14: Keyboard handler for Delete / Backspace / Esc
-
-**Files:**
-- Modify: `apps/client/src/features/base/components/grid/grid-container.tsx`
-
-- [ ] **Step 1: Add keyboard handler**
-
-In `grid-container.tsx`, add a `useEffect` that attaches a `keydown` listener to `scrollRef.current` (the grid wrapper). Guards:
-1. `editingCell` is null
-2. `document.activeElement` is contained by `scrollRef.current` (i.e. focus inside the grid)
-3. Not typing in an input / textarea / contenteditable
-
-Behavior:
-- `Escape` → `clear()` if `selectionCount > 0`; do not call `preventDefault` (other handlers may want it).
-- `Delete` or `Backspace` → if `selectionCount > 0`, call the same delete path as the action bar. Extract the delete handler into a shared callback or lift it out. For simplicity, import and dispatch a custom event `base:rows:delete-requested` that the `SelectionActionBar` listens for and runs its `handleDelete`. (Alternative: hoist the delete logic into a shared hook `use-delete-selected-rows` and call from both places.)
-
-**Recommended:** implement the shared hook. Create `apps/client/src/features/base/hooks/use-delete-selected-rows.ts`:
+- [ ] **Step 2: Create `use-delete-selected-rows.ts`**
```ts
import { useCallback } from "react";
@@ -1059,9 +963,101 @@ export function useDeleteSelectedRows(baseId: string) {
}
```
-Refactor `SelectionActionBar` to use this hook (replace its `handleDelete` body and the local `BATCH_SIZE`/inline mutation logic).
+- [ ] **Step 3: Create `selection-action-bar.tsx`**
-Then add to `grid-container.tsx`:
+```tsx
+import { memo } from "react";
+import { ActionIcon, Button, Transition } from "@mantine/core";
+import { IconTrash, IconX } from "@tabler/icons-react";
+import { useTranslation } from "react-i18next";
+import { useRowSelection } from "@/features/base/hooks/use-row-selection";
+import { useDeleteSelectedRows } from "@/features/base/hooks/use-delete-selected-rows";
+import classes from "@/features/base/styles/grid.module.css";
+
+type SelectionActionBarProps = {
+ baseId: string;
+};
+
+export const SelectionActionBar = memo(function SelectionActionBar({
+ baseId,
+}: SelectionActionBarProps) {
+ const { t } = useTranslation();
+ const { selectionCount, clear } = useRowSelection();
+ const { deleteSelected, isPending } = useDeleteSelectedRows(baseId);
+
+ const isOpen = selectionCount > 0;
+
+ return (
+
+ {(styles) => (
+
+
+
+ {t("{{count}} selected", { count: selectionCount })}
+
+
}
+ loading={isPending}
+ onClick={() => void deleteSelected()}
+ >
+ {t("Delete")}
+
+
+
+
+
+
+ )}
+
+ );
+});
+```
+
+- [ ] **Step 4: Mount in `grid-container.tsx`**
+
+In `grid-container.tsx`:
+- Add `import { SelectionActionBar } from "./selection-action-bar";`
+- Render `` directly after the `` line, inside the `` container. Skip if `!baseId`.
+
+- [ ] **Step 5: Build client**
+
+Run: `pnpm nx run client:build`
+Expected: build succeeds.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add apps/client/src/features/base/hooks/use-delete-selected-rows.ts apps/client/src/features/base/components/grid/selection-action-bar.tsx apps/client/src/features/base/components/grid/grid-container.tsx apps/client/src/features/base/styles/grid.module.css
+git commit -m "feat(base): floating selection action bar with bulk delete"
+```
+
+---
+
+### Task 14: Keyboard handler for Delete / Backspace / Esc
+
+**Files:**
+- Modify: `apps/client/src/features/base/components/grid/grid-container.tsx`
+
+- [ ] **Step 1: Add keyboard handler**
+
+In `grid-container.tsx`, add a `useEffect` that attaches a `keydown` listener to `scrollRef.current` (the grid wrapper). Guards:
+1. `editingCell` is null
+2. `document.activeElement` is contained by `scrollRef.current` (i.e. focus inside the grid)
+3. Not typing in an input / textarea / contenteditable
+
+Behavior:
+- `Escape` → `clear()` if `selectionCount > 0`.
+- `Delete` or `Backspace` → if `selectionCount > 0`, call `deleteSelected()` from `useDeleteSelectedRows`.
+
+Add imports for `useRowSelection` and `useDeleteSelectedRows`, then:
```ts
const { deleteSelected } = useDeleteSelectedRows(baseId ?? "");
@@ -1106,7 +1102,7 @@ Expected: build succeeds.
- [ ] **Step 3: Commit**
```bash
-git add apps/client/src/features/base/hooks/use-delete-selected-rows.ts apps/client/src/features/base/components/grid/selection-action-bar.tsx apps/client/src/features/base/components/grid/grid-container.tsx
+git add apps/client/src/features/base/components/grid/grid-container.tsx
git commit -m "feat(base): keyboard delete and esc to clear selection"
```
@@ -1125,12 +1121,17 @@ Inside `BaseTable`, after the existing `useEffect` that syncs `activeViewId`, ad
const { clear: clearSelection } = useRowSelection();
useEffect(() => {
clearSelection();
- // Clear whenever identity of base, view, filter, or sorts changes.
-}, [baseId, activeView?.id, activeFilter, activeSorts, clearSelection]);
+ // Clear whenever identity of base or active view changes. Filter and sort
+ // changes flow through activeView.config, which re-renders the rows —
+ // depending on activeView.id alone keeps this effect stable (object
+ // identity of activeFilter / activeSorts may change every render).
+}, [baseId, activeView?.id, clearSelection]);
```
Import `useRowSelection` from `@/features/base/hooks/use-row-selection`.
+Note: the spec asks for selection to clear on filter/sort change within a single view too. For v1, clearing only on view/base change is sufficient — a user changing sort within the same view still sees the same row set re-ordered, and the selected rows remain valid. If the product later wants "clear on filter change within a view," add a filter-identity hash via `JSON.stringify(activeFilter)` as a dep.
+
- [ ] **Step 2: Build client**
Run: `pnpm nx run client:build`