feat(sidebar): add collapsible sections with localStorage persistence

Sidebar sections can now be collapsed/expanded by clicking the header.
State is persisted to localStorage so sections remain open/closed
across page navigation and browser sessions.

Changes:
- init-alpine.js: Added openSections state, toggleSection(),
  expandSectionForCurrentPage(), and localStorage helpers
- sidebar.html: Refactored with Jinja2 macros for DRY code,
  added collapsible sections with CSS transitions and rotating
  chevron icons

Features:
- Click section header to toggle expand/collapse
- Chevron rotates 180 degrees when expanded
- Smooth CSS transitions (no extra Alpine plugins needed)
- State persists in localStorage (admin_sidebar_sections key)
- Default: Platform Administration open, others closed
- Dashboard and Settings always visible (not collapsible)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-03 23:13:59 +01:00
parent 7aaa262d34
commit 76f8a59954
2 changed files with 212 additions and 443 deletions

View File

@@ -1,15 +1,15 @@
/**
* Alpine.js v3 global data initialization
* Provides theme toggle, menu controls, and page state
* Provides theme toggle, menu controls, sidebar sections, and page state
*/
function data() {
// ─────────────────────────────────────────────────────────────────
// Theme (dark mode) persistence
// ─────────────────────────────────────────────────────────────────
function getThemeFromLocalStorage() {
// if user already changed the theme, use it
if (window.localStorage.getItem('dark')) {
return JSON.parse(window.localStorage.getItem('dark'))
}
// else return their preferences
return (
!!window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches
@@ -20,12 +20,74 @@ function data() {
window.localStorage.setItem('dark', value)
}
// ─────────────────────────────────────────────────────────────────
// Sidebar sections persistence
// ─────────────────────────────────────────────────────────────────
const SIDEBAR_STORAGE_KEY = 'admin_sidebar_sections';
// Default state: Platform Administration open, others closed
const defaultSections = {
platformAdmin: true,
contentMgmt: false,
devTools: false,
monitoring: false
};
function getSidebarSectionsFromStorage() {
try {
const stored = window.localStorage.getItem(SIDEBAR_STORAGE_KEY);
if (stored) {
return { ...defaultSections, ...JSON.parse(stored) };
}
} catch (e) {
console.warn('Failed to parse sidebar sections from localStorage:', e);
}
return { ...defaultSections };
}
function saveSidebarSectionsToStorage(sections) {
try {
window.localStorage.setItem(SIDEBAR_STORAGE_KEY, JSON.stringify(sections));
} catch (e) {
console.warn('Failed to save sidebar sections to localStorage:', e);
}
}
// Map pages to their parent sections
const pageSectionMap = {
// Platform Administration
companies: 'platformAdmin',
vendors: 'platformAdmin',
users: 'platformAdmin',
customers: 'platformAdmin',
marketplace: 'platformAdmin',
// Content Management
'platform-homepage': 'contentMgmt',
'content-pages': 'contentMgmt',
'vendor-theme': 'contentMgmt',
// Developer Tools
components: 'devTools',
icons: 'devTools',
testing: 'devTools',
'code-quality': 'devTools',
// Platform Monitoring
imports: 'monitoring',
logs: 'monitoring'
};
return {
// ─────────────────────────────────────────────────────────────────
// Theme
// ─────────────────────────────────────────────────────────────────
dark: getThemeFromLocalStorage(),
toggleTheme() {
this.dark = !this.dark
setThemeToLocalStorage(this.dark)
},
// ─────────────────────────────────────────────────────────────────
// Mobile side menu
// ─────────────────────────────────────────────────────────────────
isSideMenuOpen: false,
toggleSideMenu() {
this.isSideMenuOpen = !this.isSideMenuOpen
@@ -33,6 +95,10 @@ function data() {
closeSideMenu() {
this.isSideMenuOpen = false
},
// ─────────────────────────────────────────────────────────────────
// Notifications menu
// ─────────────────────────────────────────────────────────────────
isNotificationsMenuOpen: false,
toggleNotificationsMenu() {
this.isNotificationsMenuOpen = !this.isNotificationsMenuOpen
@@ -40,6 +106,10 @@ function data() {
closeNotificationsMenu() {
this.isNotificationsMenuOpen = false
},
// ─────────────────────────────────────────────────────────────────
// Profile menu
// ─────────────────────────────────────────────────────────────────
isProfileMenuOpen: false,
toggleProfileMenu() {
this.isProfileMenuOpen = !this.isProfileMenuOpen
@@ -47,11 +117,37 @@ function data() {
closeProfileMenu() {
this.isProfileMenuOpen = false
},
// ─────────────────────────────────────────────────────────────────
// Pages menu (legacy)
// ─────────────────────────────────────────────────────────────────
isPagesMenuOpen: false,
togglePagesMenu() {
this.isPagesMenuOpen = !this.isPagesMenuOpen
},
// ─────────────────────────────────────────────────────────────────
// Collapsible sidebar sections
// ─────────────────────────────────────────────────────────────────
openSections: getSidebarSectionsFromStorage(),
toggleSection(section) {
this.openSections[section] = !this.openSections[section];
saveSidebarSectionsToStorage(this.openSections);
},
// Auto-expand section containing current page
expandSectionForCurrentPage() {
const section = pageSectionMap[this.currentPage];
if (section && !this.openSections[section]) {
this.openSections[section] = true;
saveSidebarSectionsToStorage(this.openSections);
}
},
// ─────────────────────────────────────────────────────────────────
// Page identifier - will be set by individual pages
// ─────────────────────────────────────────────────────────────────
currentPage: ''
}
}