# 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!