# RFCP - Iteration 9: UX Polish & Workflow Improvements
## Overview
Polish existing features for better workflow without breaking changes.
**Focus:** Keyboard shortcuts, batch operations, number inputs, visual improvements.
---
## Feature 1: Better Keyboard Shortcuts
**Problems:**
1. Ctrl+N conflicts with browser (New Window)
2. Missing useful shortcuts for common actions
3. No sector-specific shortcuts
### New Hotkey Map
**File:** `frontend/src/hooks/useHotkeys.ts` (update)
```typescript
const HOTKEYS = {
// Coverage
'ctrl+enter': 'Calculate Coverage',
'shift+c': 'Clear Coverage',
// Sites
'shift+s': 'New Site (Place Mode)', // was Ctrl+N
'm': 'Manual Site Entry',
// View
'h': 'Toggle Heatmap',
'g': 'Toggle Grid',
't': 'Toggle Terrain',
'r': 'Toggle Ruler',
// Navigation
'f': 'Fit to Coverage',
'esc': 'Cancel/Close',
// Selection
'ctrl+a': 'Select All Sites',
'ctrl+d': 'Deselect All',
'delete': 'Delete Selected',
// Edit
'e': 'Edit Selected Site',
'shift+e': 'Batch Edit Selected',
// Sector operations
'ctrl+shift+s': 'Add Sector to Selected',
// Help
'?': 'Show Keyboard Shortcuts',
};
```
**No conflicts:**
- ✅ Avoid: Ctrl+N, Ctrl+T, Ctrl+W, Ctrl+R, Ctrl+S
- ✅ Use: Shift+, Alt+, single letters (when not in input)
- ✅ Ctrl+ only for safe combos (Ctrl+Enter, Ctrl+A, Ctrl+D)
### Implementation
```typescript
// Check if in input field
const isInputActive = () => {
const active = document.activeElement;
return active?.tagName === 'INPUT' ||
active?.tagName === 'TEXTAREA' ||
active?.tagName === 'SELECT';
};
// Only trigger shortcuts if NOT in input
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (isInputActive()) return;
// Handle shortcuts...
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, []);
```
---
## Feature 2: Batch Edit Azimuth
**Problem:** Batch Edit can change height, power, frequency - but not azimuth!
### Solution
**File:** `frontend/src/components/panels/SiteList.tsx`
```typescript
// Add to batch edit UI
{selectedSiteIds.length > 0 && (
Batch Edit ({selectedSiteIds.length} selected)
{/* Existing: Adjust Height, Set Height */}
{/* NEW: Adjust Azimuth */}
{/* NEW: Set Azimuth */}
{
if (e.key === 'Enter') {
setAzimuthBatch(Number(e.currentTarget.value));
}
}}
/>
)}
```
**Store methods:**
```typescript
// In sites.ts
const adjustAzimuthBatch = (delta: number) => {
const { sites, selectedSiteIds } = get();
selectedSiteIds.forEach(id => {
const site = sites.find(s => s.id === id);
if (site) {
const newAzimuth = (site.azimuth + delta + 360) % 360;
updateSite(id, { azimuth: newAzimuth });
}
});
toast.success(`Adjusted azimuth by ${delta}° for ${selectedSiteIds.length} sites`);
useCoverageStore.getState().clearCoverage();
};
const setAzimuthBatch = (azimuth: number) => {
const { selectedSiteIds } = get();
selectedSiteIds.forEach(id => {
updateSite(id, { azimuth });
});
toast.success(`Set azimuth to ${azimuth}° for ${selectedSiteIds.length} sites`);
useCoverageStore.getState().clearCoverage();
};
```
---
## Feature 3: Number Inputs with Arrow Controls
**Problem:** Sliders are imprecise. Need exact values + fine adjustments.
### Enhanced Input Component
**File:** `frontend/src/components/common/NumberInput.tsx` (new)
```typescript
interface NumberInputProps {
label: string;
value: number;
onChange: (value: number) => void;
min?: number;
max?: number;
step?: number;
unit?: string;
showSlider?: boolean;
}
export function NumberInput({
label,
value,
onChange,
min = 0,
max = 100,
step = 1,
unit = '',
showSlider = true
}: NumberInputProps) {
return (
{/* Text input for exact value */}
onChange(Number(e.target.value))}
min={min}
max={max}
step={step}
className="number-field"
/>
{/* Arrow buttons */}
{unit &&
{unit}}
{/* Optional slider for visual feedback */}
{showSlider && (
onChange(Number(e.target.value))}
min={min}
max={max}
step={step}
className="slider"
/>
)}
);
}
```
**CSS:**
```css
.number-input-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.input-controls {
display: flex;
align-items: center;
gap: 4px;
}
.number-field {
width: 80px;
padding: 6px;
border: 1px solid #ccc;
border-radius: 4px;
text-align: center;
font-size: 14px;
}
.arrow-controls {
display: flex;
flex-direction: column;
gap: 2px;
}
.arrow-up, .arrow-down {
width: 24px;
height: 16px;
padding: 0;
border: 1px solid #ccc;
background: #fff;
cursor: pointer;
font-size: 8px;
line-height: 1;
}
.arrow-up:hover, .arrow-down:hover {
background: #f0f0f0;
}
.unit {
font-size: 12px;
color: #666;
margin-left: 4px;
}
.slider {
width: 100%;
}
```
**Usage in SiteForm:**
```typescript
import { NumberInput } from '@/components/common/NumberInput';
// Replace slider inputs:
```
---
## Feature 4: Visual Sector Grouping
**Problem:** UI shows "Sites (2)" but they're actually sectors of same location.
**Solution:** Visual grouping WITHOUT data model change.
### Approach A: Color-coded Names
Auto-detect sites at same location and style them:
```typescript
// In SiteList.tsx
const getSiteGroups = (sites: Site[]) => {
const groups = new Map();
sites.forEach(site => {
// Group by lat/lon (within 10m)
const key = `${site.lat.toFixed(4)},${site.lon.toFixed(4)}`;
if (!groups.has(key)) {
groups.set(key, []);
}
groups.get(key)!.push(site);
});
return groups;
};
// In render:
{siteGroups.map((group, idx) => {
if (group.length === 1) {
// Single site - render normally
return ;
} else {
// Multi-sector group
return (
📡 {group[0].name.replace(/-Alpha|-Beta|-Gamma|-clone/g, '')} ({group.length} sectors)
{group.map(site => (
))}
);
}
})}
```
### Approach B: Compact Display
```typescript
// Show azimuth as badge
{site.name}
{isGrouped && (
{site.azimuth}°
)}
```
---
## Feature 5: Quick Actions Menu
**Add right-click context menu for sites:**
```typescript
// On site item
{
e.preventDefault();
showContextMenu(e, site.id);
}}
>
{/* site content */}
// Context menu
{contextMenu && (
editSite(siteId) },
{ label: 'Add Sector', action: () => cloneSector(siteId) },
{ label: 'Clone Site', action: () => cloneSite(siteId) },
{ label: 'Zoom to Site', action: () => zoomToSite(siteId) },
{ separator: true },
{ label: 'Delete', action: () => deleteSite(siteId), danger: true },
]}
onClose={() => setContextMenu(null)}
/>
)}
```
---
## Feature 6: Undo/Redo System
**Add undo for destructive operations:**
```typescript
// Simple undo stack
const undoStack: Array<{ action: string; data: any }> = [];
const deleteSite = (id: string) => {
const site = sites.find(s => s.id === id);
// Push to undo stack
undoStack.push({ action: 'delete', data: site });
// Perform delete
setSites(sites.filter(s => s.id !== id));
// Show undo toast
toast.success('Site deleted', {
action: {
label: 'Undo',
onClick: () => {
const lastAction = undoStack.pop();
if (lastAction?.action === 'delete') {
setSites([...sites, lastAction.data]);
}
}
}
});
};
```
---
## Recommended Implementation Order
**Priority 1 (Critical UX):**
1. ✅ Fix Ctrl+N hotkey → Shift+S
2. ✅ Add batch azimuth operations
3. ✅ Number inputs with arrows
**Priority 2 (Nice to have):**
4. ⭐ Visual sector grouping (color-coded)
5. ⭐ Additional hotkeys (Shift+C, G, T, R)
6. ⭐ Context menu for sites
**Priority 3 (Future):**
7. 🔮 Undo/Redo system
8. 🔮 Full data model refactor (Site → Sectors)
---
## Testing
### Hotkeys:
- [ ] Shift+S creates new site (Ctrl+N doesn't interfere)
- [ ] H toggles heatmap
- [ ] G toggles grid
- [ ] ? shows shortcuts modal
### Batch Azimuth:
- [ ] Select 3 sites
- [ ] Adjust +45°
- [ ] All sites rotate together
### Number Inputs:
- [ ] Type "135" in azimuth field
- [ ] Click ▲ → 136°
- [ ] Click ▼ → 135°
- [ ] Slider still works
### Visual Grouping:
- [ ] 3 sectors at same location show grouped
- [ ] Badge shows azimuth (0°, 120°, 240°)
- [ ] Single sites show normally
---
## Build & Deploy
```bash
cd /opt/rfcp/frontend
npm run build
sudo systemctl reload caddy
```
---
## Commit Message
```
feat(ux): keyboard shortcuts, batch azimuth, number inputs
- Changed new site hotkey: Ctrl+N → Shift+S (avoid browser conflict)
- Added batch azimuth operations (adjust ±10/45/90°, set absolute)
- Implemented NumberInput component with arrow controls (+/- buttons)
- Added visual sector grouping (detect co-located sites)
- Enhanced hotkey system (G=grid, T=terrain, R=ruler, ?=help)
- Site form now has precise numeric entry + sliders
Improved workflow for multi-sector site planning.
```
🚀 Ready for Iteration 9!