Description
Add a master checkbox (tri-state: checked/unchecked/indeterminate) to the header of the DuplicateGroupsPreview component to improve bulk selection UX in manual cleanup mode. This checkbox will provide a standard table-like pattern for selecting all duplicate groups at once, supplementing or replacing the existing "Select All"/"Deselect All" text buttons.
Context
Type: enhancement
Scope: small
Complexity: low
Priority: medium
Current Implementation
The DuplicateGroupsPreview component already has functional bulk selection via text buttons (lines 93-108):
- "Select All" button calls
selectAll() (line 51-53)
- "Deselect All" button calls
deselectAll() (line 55-57)
- Buttons only visible when
mode === 'manual' && !dryRun
While functional, text buttons are less discoverable and don't provide visual state indication.
Relevant Files
Files identified by multi-agent investigation (ranked by relevance):
| File |
Lines |
Relevance |
Notes |
app/components/migration/DuplicateGroupsPreview.tsx |
51-57, 93-108, 139-147 |
HIGH |
Primary file - contains selection state, bulk selection functions, and UI |
app/components/migration/CleanupPanel.tsx |
185-198 |
MEDIUM |
Parent component - provides mode and dryRun props |
app/hooks/useCleanup.ts |
full file |
MEDIUM |
State management hook - no changes needed |
lib/migration/cleanup/types.ts |
full file |
LOW |
Type definitions - reference only |
Analysis
Feature Rationale
UX Benefits of Master Checkbox:
- Discoverability: Checkbox aligned with column header follows universal table patterns (Gmail, file managers, data grids)
- Visual State Indication: Tri-state checkbox immediately communicates selection status:
- Checked: all groups selected
- Unchecked: no groups selected
- Indeterminate: partial selection
- Muscle Memory: Users expect master checkbox pattern in table-like interfaces
- Consistency: Aligns with existing individual checkboxes per group (lines 139-147)
- Reduced Interaction Cost: Single click to toggle all vs. scanning for text buttons
Impact Assessment
Minimal Impact - Supplements Existing Logic:
- Existing
selectAll() and deselectAll() functions will be reused
selectedGroups state (Set) remains unchanged
- Individual checkboxes continue to work as-is
- No changes needed to useCleanup hook or parent components
- No API or backend changes required
Edge Cases
| Scenario |
Groups Count |
Selected Count |
Master State |
Behavior |
| Empty list |
0 |
0 |
Hidden/disabled |
No interaction |
| Single group, unselected |
1 |
0 |
Unchecked |
Click → selectAll() |
| Single group, selected |
1 |
1 |
Checked |
Click → deselectAll() |
| Multiple, all selected |
N |
N |
Checked |
Click → deselectAll() |
| Multiple, partial |
N |
K (0<K<N) |
Indeterminate |
Click → selectAll() |
Accessibility
Requirements (WAI-ARIA 1.2):
aria-checked attribute: 'true', 'false', or 'mixed'
aria-label or associated label for screen readers
- Keyboard navigation support (Tab to focus, Space to toggle)
- Focus indicator visible in light and dark modes
Implementation Pattern:
const masterCheckboxRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (masterCheckboxRef.current) {
masterCheckboxRef.current.indeterminate = isIndeterminate;
}
}, [isIndeterminate]);
<input
ref={masterCheckboxRef}
type="checkbox"
checked={allSelected}
onChange={handleMasterToggle}
aria-label="Select all duplicate groups"
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
/>
Performance
Optimizations Required:
- Use
useMemo for state calculations (allSelected, isIndeterminate)
- Use
useRef + useEffect for indeterminate property (DOM-only, not attribute)
Impact: Negligible - O(1) additional state calculations per render
Technical Approach
Implementation Steps
- Add Derived State and Ref (after line 29):
const masterCheckboxRef = useRef<HTMLInputElement>(null);
const allSelected = groups.length > 0 && selectedGroups.size === groups.length;
const isIndeterminate = selectedGroups.size > 0 && selectedGroups.size < groups.length;
- Add Indeterminate Effect (after line 57):
useEffect(() => {
if (masterCheckboxRef.current) {
masterCheckboxRef.current.indeterminate = isIndeterminate;
}
}, [isIndeterminate]);
- Add Master Toggle Handler (after useEffect):
const handleMasterToggle = () => {
if (allSelected) {
deselectAll();
} else {
selectAll();
}
};
- Replace Text Buttons with Master Checkbox (lines 93-108):
{mode === 'manual' && !dryRun && (
<div className="flex items-center gap-2">
<input
ref={masterCheckboxRef}
type="checkbox"
checked={allSelected}
onChange={handleMasterToggle}
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
aria-label="Select all duplicate groups"
/>
<label className="text-sm text-gray-600 dark:text-gray-400 select-none">
Select All
</label>
</div>
)}
Alternative Approaches
Option A: Keep Text Buttons + Add Master Checkbox (Redundancy)
- Pros: No UX regression, provides both patterns
- Cons: Cluttered UI, duplicate functionality
Option B: Replace Text Buttons with Master Checkbox (Recommended)
- Pros: Cleaner UI, follows standard patterns
- Cons: None (existing functionality fully preserved)
Acceptance Criteria
Testing Requirements
Tri-State Behavior
Interaction
Accessibility
Edge Cases
Additional Considerations
Security
No security implications - purely client-side UI enhancement.
Performance
- Use
useMemo to prevent unnecessary state recalculations
useEffect with isIndeterminate dependency only runs when state changes
- No performance regression expected
Documentation
Investigation Summary:
- Agents used: 9 (Gemini Flash, Gemini Lite, Codex, OpenCode BigPickle, OpenCode GLM 4.7, OpenCode Grok Code, Codex Scope, Gemini Implementation, Opus Deep Analysis)
- Files identified: 4 (1 HIGH, 2 MEDIUM, 1 LOW relevance)
- Confidence: HIGH
Issue created with multi-agent investigation using Claude Code
Description
Add a master checkbox (tri-state: checked/unchecked/indeterminate) to the header of the DuplicateGroupsPreview component to improve bulk selection UX in manual cleanup mode. This checkbox will provide a standard table-like pattern for selecting all duplicate groups at once, supplementing or replacing the existing "Select All"/"Deselect All" text buttons.
Context
Type: enhancement
Scope: small
Complexity: low
Priority: medium
Current Implementation
The DuplicateGroupsPreview component already has functional bulk selection via text buttons (lines 93-108):
selectAll()(line 51-53)deselectAll()(line 55-57)mode === 'manual' && !dryRunWhile functional, text buttons are less discoverable and don't provide visual state indication.
Relevant Files
Files identified by multi-agent investigation (ranked by relevance):
app/components/migration/DuplicateGroupsPreview.tsxapp/components/migration/CleanupPanel.tsxapp/hooks/useCleanup.tslib/migration/cleanup/types.tsAnalysis
Feature Rationale
UX Benefits of Master Checkbox:
Impact Assessment
Minimal Impact - Supplements Existing Logic:
selectAll()anddeselectAll()functions will be reusedselectedGroupsstate (Set) remains unchangedEdge Cases
Accessibility
Requirements (WAI-ARIA 1.2):
aria-checkedattribute:'true','false', or'mixed'aria-labelor associated label for screen readersImplementation Pattern:
Performance
Optimizations Required:
useMemofor state calculations (allSelected, isIndeterminate)useRef+useEffectfor indeterminate property (DOM-only, not attribute)Impact: Negligible - O(1) additional state calculations per render
Technical Approach
Implementation Steps
Alternative Approaches
Option A: Keep Text Buttons + Add Master Checkbox (Redundancy)
Option B: Replace Text Buttons with Master Checkbox (Recommended)
Acceptance Criteria
mode === 'manual' && !dryRun && groups.length > 0Testing Requirements
Tri-State Behavior
Interaction
Accessibility
Edge Cases
Additional Considerations
Security
No security implications - purely client-side UI enhancement.
Performance
useMemoto prevent unnecessary state recalculationsuseEffectwithisIndeterminatedependency only runs when state changesDocumentation
Investigation Summary:
Issue created with multi-agent investigation using Claude Code