ERP Sidebar Architecture Plan
Related: ADR-0008: Frontend Plugin Architecture - Defines plugin menu contract and PluginRegistry integration
Current State Analysis
Existing Implementation (frontend/src/layout/Sidebar.tsx)
| Feature | Status | Notes |
|---|---|---|
| Groups by module | ✅ | Parses module.DocType from name |
| Collapsible sections | ✅ | Toggle per group |
| Search/filter | ✅ | Filters by label |
| Plugin slots | ✅ | sidebar-top, sidebar-bottom |
| Collapse toggle | ✅ | 60px vs 240px width |
| Active item highlight | ✅ | Border + background |
Current Limitations
- Flat grouping - Only groups by first segment (e.g., "core.User" → "Core")
- No nested hierarchy - Can't have multi-level menus (Module → Category → DocType)
- No icons - Text only, no visual differentiation
- No pinned/favorites - Can't pin frequently used items
- No recent items - No quick access to recently viewed
- Static structure - Menu comes only from DocTypes, no custom links
Proposed Architecture
Sidebar Navigation Hierarchy
The sidebar displays a hierarchical menu structure with user-specific enhancements:
┌─────────────────────────────────────┐
│ 🏠 Home │ ← Static entry
├─────────────────────────────────────┤
│ ⭐ Favorites │ ← User-specific
│ └── Sales Invoice │
│ └── Customer │
├─────────────────────────────────────┤
│ 🕐 Recent │ ← Last 5-10 visited
│ └── INV-00123 │
│ └── CUST-00045 │
├─────────────────────────────────────┤
│ 📦 Sales │ ← Module (from plugins)
│ ├── Transactions │ ← Category
│ │ ├── Sales Invoice │
│ │ ├── Sales Order │
│ │ └── Quotation │
│ └── Masters │
│ ├── Customer │
│ └── Price List │
├─────────────────────────────────────┤
│ 🏭 Inventory │
│ ├── Stock Entry │
│ └── Item │
├─────────────────────────────────────┤
│ ⚙️ Settings │ ← Always at bottom
│ └── User │
│ └── Company │
└─────────────────────────────────────┘
Note: Module and category structure comes from
PluginRegistry.getMenu()(see ADR-0008). Sidebar adds Favorites and Recent sections on top.
Menu Source Priority
- Backend metadata - DocTypes with
show_in_menu: true - Plugin contributions - See ADR-0008 Plugin Menu Contract
- User preferences - Favorites, recent, collapsed state (sidebar-specific)
- Static entries - Home, Settings (always present)
Note: Menu items from plugins are registered via
PluginRegistry.getMenu()as defined in ADR-0008. The sidebar consumes this merged menu tree and adds user-specific features (favorites, recent, search).
Sidebar-Specific Features
These features extend the base menu system defined in ADR-0008:
Favorites
- User can pin frequently used items
- Stored in localStorage or user preferences API
- Appears as top-level "⭐ Favorites" section
- Right-click or star icon to pin/unpin
Recent Items
- Track last 10 viewed DocTypes/records
- Stored in localStorage
- Appears as "🕐 Recent" section below Favorites
- Auto-updates on navigation
Search & Filter
- Fuzzy search across all menu items
- Shows matched path (Sales > Invoice > ...)
- Highlights matches
- Filters entire tree, preserving hierarchy
Responsive Behavior
- Mobile: Slide-out drawer (overlay)
- Tablet: Collapsible sidebar
- Desktop: Always visible, collapsible to 60px icons-only
Data Model Extensions
The sidebar extends the base MenuItem interface from ADR-0008:
// Base MenuItem from ADR-0008 plugin contract
import { MenuItem } from "@framework-m/plugin-sdk";
// Sidebar-specific extensions
interface SidebarMenuItem extends MenuItem {
// Runtime state (not persisted)
isExpanded?: boolean;
isFavorite?: boolean;
lastVisited?: Date;
// UI-specific
depth?: number; // Nesting level for indentation
parentId?: string; // For breadcrumb/path display
}
// User preferences
interface SidebarPreferences {
favorites: string[]; // MenuItem IDs
recent: Array<{
id: string;
timestamp: Date;
}>;
collapsed: string[]; // Collapsed section IDs
customOrder?: Record<string, number>; // Custom sort order
}
Integration with PluginRegistry
The sidebar integrates with the plugin system defined in ADR-0008:
// Sidebar.tsx
import { usePluginMenu } from '@framework-m/plugin-sdk';
function Sidebar() {
// Get merged menu from all registered plugins
const pluginMenu = usePluginMenu();
// Load user preferences
const [preferences, setPreferences] = useSidebarPreferences();
// Enhance with sidebar-specific features
const menuTree = useMemo(() => {
return enhanceMenu(pluginMenu, preferences);
}, [pluginMenu, preferences]);
return (
<nav>
<FavoritesSection items={menuTree.favorites} />
<RecentSection items={menuTree.recent} />
<MenuTree items={menuTree.modules} />
</nav>
);
}
function enhanceMenu(
pluginMenu: MenuItem[],
preferences: SidebarPreferences
): EnhancedMenuTree {
// Add favorites section
// Add recent section
// Apply custom ordering
// Set expanded states
// Calculate depths for indentation
}
Scalability Patterns
Pattern 1: Lazy Loading Large Modules
// Load children on expand (for modules with 100+ items)
async function loadMenuChildren(moduleId: string) {
const response = await fetch(`/api/meta/menu/${moduleId}`);
return response.json();
}
Pattern 2: Virtual Scrolling
For 500+ menu items, use virtual scrolling:
import { FixedSizeList } from 'react-window';
<FixedSizeList height={400} itemCount={items.length} itemSize={36}>
{({ index, style }) => <MenuItem item={items[index]} style={style} />}
</FixedSizeList>
Implementation Checklist
Phase 1: Core Improvements ✅ COMPLETE
-
Add icons support ✅
- ✅
Iconcomponent implemented (frontend/src/components/Icon.tsx) - ✅
getModuleIcon()function infrontend/src/types/menu.ts - ✅ Icons used throughout
Sidebar.tsx
- ✅
-
Multi-level nesting ✅
- ✅
MenuIteminterface supportschildrenarray - ✅ Backend API returns nested structure
- ✅ Frontend groups by module (flat, not recursive yet)
- ✅
-
Improve grouping logic ✅
- ✅ Backend: Parses
doctype_name.split(".")for module extraction - ✅ Module icons mapped in both backend and frontend
- ✅ DocTypes grouped by module in sidebar
- ✅ Backend: Parses
Phase 2: User Experience ✅ COMPLETE
-
Favorites section ✅
- ✅
useFavoriteshook (frontend/src/hooks/useFavorites.ts) - ✅ localStorage persistence with
addFavorite,removeFavorite,toggleFavorite - ✅ Star icon on each menu item for toggling
- ✅ Collapsible "⭐ Favorites" section at top of sidebar
- ✅ Drag-to-reorder within favorites (via
useSortable)
- ✅
-
Recent items ✅
- ✅
useRecenthook (frontend/src/hooks/useRecent.ts) - ✅ localStorage persistence with
addRecentItem, max 10 items - ✅ Tracks both DocType views and specific records
- ✅ Collapsible "🕐 Recent" section in sidebar
- ✅ Wired to FormView and ListView with automatic tracking
- ✅
-
Keyboard navigation ✅
- ✅ Arrow key navigation implemented in
Sidebar.tsx - ✅ Focus state tracked with
focusedIndex - ✅ Enter key to navigate to focused item
- ✅ Escape key to clear focus
- ✅ Visual focus indicator (2px outline) on all menu items
- ✅ Focus wrapping at list boundaries
- ✅ Arrow key navigation implemented in
Phase 3: Scalability ✅ MOSTLY COMPLETE
-
Backend menu API ✅
- ✅ Endpoint:
GET /api/meta/menu(menu_routes.py) - ✅ Returns structured menu tree with modules and children
- ✅ Module icons, ordering, and metadata
- ✅ Filters by
api_resource=True - ⚠️ TODO: Wire up router in
app.pyroute handlers (currently NOT registered)
- ✅ Endpoint:
-
Plugin menu integration ❌ NOT STARTED
- ❌ No
@framework-m/plugin-sdkpackage exists yet (see ADR-0008) - ❌ No
PluginRegistry.getMenu()implementation - ❌ No plugin menu merging logic
- ✅ Hook placeholder exists:
useMenuDatacan be extended - 📝 Status: Blocked by ADR-0008 implementation
- ❌ No
-
Badge counts ⏸️ DEFERRED
- ⏸️ Not started (as planned)
- 📝 Future enhancement
Phase 4: Polish ✅ COMPLETE
-
Drag-to-reorder ✅
- ✅
useSortablehook (frontend/src/hooks/useSortable.ts) - ✅ localStorage persistence of custom order
- ✅ Drag handlers with visual feedback
- ✅ Applied to favorites list
- ⏸️ Module groups reordering deferred (optional enhancement)
- ✅
-
Search improvements ✅
- ✅
fuzzySearch.tsutility with scoring and highlighting - ✅
fuzzyMatch()andgetHighlightedSegments()functions - ✅ Filters entire menu tree
- ✅ Visual highlighting of matched text with background color
- ✅ Breadcrumb display showing module path for search results
- ✅
-
Responsive behavior ✅
- ✅ Collapse toggle button implemented (60px ↔ 240px)
- ✅ Icons-only mode when collapsed
- ✅ Mobile slide-out drawer with overlay
- ✅ Backdrop overlay that closes on click
- ✅ Touch gestures for swipe-to-close
- ✅ Mobile menu button in Navbar
- ✅ Media queries for responsive behavior
Implementation Status Summary
✅ Fully Complete (80%)
- Icons system with module mapping
- Favorites with localStorage + drag-to-reorder
- Recent items tracking
- Fuzzy search algorithm
- Backend menu API structure
- Type definitions and data models
- Core sidebar layout and grouping
- Keyboard navigation - All features complete (Arrow keys, Enter, Escape, visual focus, wrapping)
⚠️ Partially Complete (12%)
- Search highlighting - Scoring works, needs visual highlights in UI
- Responsive - Desktop works, needs mobile drawer
- Backend integration - API exists but not wired to app router
❌ Not Started (5%)
- Plugin menu integration - Blocked by ADR-0008 (plugin SDK doesn't exist yet)
- Badge counts - Intentionally deferred
Critical Issues to Fix
🔴 HIGH PRIORITY
-
Menu API not registered in app 🔴
- File:
libs/framework-m-standard/src/framework_m_standard/adapters/web/app.py - Issue:
menu_routes_routercreated but never added toroute_handlers - Fix: Add import and register router
from framework_m_standard.adapters.web.menu_routes import menu_routes_router
route_handlers: list[Any] = [
# ... existing handlers
menu_routes_router, # ADD THIS
] - File:
-
Recent items not tracked on navigation 🟡
- Files: Need to hook into navigation events
- Issue:
useRecenthook exists butaddRecentItem()never called - Fix: Add effect in
FormView.tsxandListView.tsx:
const { addRecentItem } = useRecent();
useEffect(() => {
if (doctype) {
addRecentItem({
name: doctype,
label: doctype,
route: location.pathname,
});
}
}, [doctype, location.pathname]);
🟡 MEDIUM PRIORITY
-
Search highlighting not visible 🟡
- Issue:
fuzzyMatch()returns highlights but UI doesn't show them - Fix: Use
getHighlightedSegments()in menu item rendering
- Issue:
-
Mobile responsive drawer 🟡
- Issue: Sidebar just shrinks on mobile, no overlay drawer
- Fix: Add media queries + overlay backdrop
-
Plugin integration blocked 🔵
- Blocked by: ADR-0008 implementation (plugin SDK doesn't exist)
- Status: Cannot proceed until
@framework-m/plugin-sdkpackage is created
Files Inventory
✅ Implemented and Working
| File | Purpose | Status |
|---|---|---|
frontend/src/layout/Sidebar.tsx | Main sidebar component | ✅ Core complete |
frontend/src/hooks/useFavorites.ts | Favorites management | ✅ Fully functional |
frontend/src/hooks/useRecent.ts | Recent items tracking | ✅ Hook ready, needs wiring |
frontend/src/hooks/useSortable.ts | Drag-to-reorder | ✅ Fully functional |
frontend/src/hooks/useMenuData.ts | Backend menu API client | ✅ Ready (API not wired) |
frontend/src/utils/fuzzySearch.ts | Search algorithm | ✅ Fully functional |
frontend/src/types/menu.ts | TypeScript types | ✅ Complete |
frontend/src/components/Icon.tsx | Icon component | ✅ Fully functional |
libs/.../menu_routes.py | Backend menu API | ✅ Implemented (not registered) |
⚠️ Needs Attention
| File | Issue | Priority |
|---|---|---|
frontend/src/layout/Sidebar.tsx | Add Enter key handler, focus styles | 🟡 Medium |
frontend/src/layout/Sidebar.tsx | Mobile drawer + overlay | 🟡 Medium |
frontend/src/layout/Sidebar.tsx | Render search highlights | 🟡 Medium |
frontend/src/pages/FormView.tsx | Call addRecentItem() on load | 🟡 Medium |
frontend/src/pages/ListView.tsx | Call addRecentItem() on load | 🟡 Medium |
.../adapters/web/app.py | Register menu_routes_router | 🔴 High |
❌ Missing (Per RFC Spec)
| File | Purpose | Status |
|---|---|---|
frontend/src/hooks/useSidebarPreferences.ts | User preferences | ⚠️ Not needed (localStorage in each hook) |
@framework-m/plugin-sdk | Plugin system | ❌ Blocked by ADR-0008 |
Files to Modify
| File | Changes |
|---|---|
frontend/src/layout/Sidebar.tsx | Main implementation |
frontend/src/hooks/useMenu.ts | Integrates with PluginRegistry (new) |
frontend/src/hooks/useFavorites.ts | Favorites management (new) |
frontend/src/hooks/useRecent.ts | Recent items tracking (new) |
frontend/src/hooks/useSidebarPreferences.ts | User preferences (new) |
frontend/src/types/sidebar.ts | TypeScript interfaces (new) |
libs/framework-m-standard/.../routes/meta.py | Backend menu API (new endpoint) |
Note: Plugin menu registration handled by
@framework-m/plugin-sdk(see ADR-0008)
Architecture Alignment
This RFC implements the sidebar consumer of the menu system defined in ADR-0008:
┌─────────────────────────────────────────────────────────────┐
│ ADR-0008: Plugin Architecture │
│ - PluginRegistry.getMenu() → Merged menu tree │
│ - MenuItem interface (base contract) │
│ - Plugin contributions via plugin.config.ts │
└────────────────────┬────────────────────────────────────────┘
│ provides menu data
▼
┌─────────────────────────────────────────────────────────────┐
│ This RFC: Sidebar Implementation │
│ - Consumes PluginRegistry.getMenu() │
│ - Adds favorites, recent, search (UI features) │
│ - Manages user preferences (collapsed, custom order) │
│ - Renders responsive navigation │
└─────────────────────────────────────────────────────────────┘
Separation of Concerns:
- ADR-0008: Defines what menus exist (data model, plugin contract)
- This RFC: Defines how menus are displayed (UI, UX, interactions)
Quick Start: Minimal Changes
For immediate improvement without breaking changes:
// 1. Add icons to groupMenuItems()
const MODULE_ICONS: Record<string, string> = {
Core: '⚙️',
Sales: '💰',
Inventory: '📦',
HR: '👥',
Other: '📄',
};
// 2. Use icon in group header
<span>{MODULE_ICONS[groupName] || '📁'} {groupName}</span>
This gives visual differentiation immediately while planning the full refactor.