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,233 +1,128 @@
{# app/templates/admin/partials/sidebar.html #} {# app/templates/admin/partials/sidebar.html #}
<!-- Desktop sidebar --> {# Collapsible sidebar sections with localStorage persistence #}
<aside class="z-20 hidden w-64 overflow-y-auto bg-white dark:bg-gray-800 md:block flex-shrink-0">
<div class="py-4 text-gray-500 dark:text-gray-400">
<a class="ml-6 text-lg font-bold text-gray-800 dark:text-gray-200" href="/admin/dashboard">
Admin Portal
</a>
<ul class="mt-6">
<!-- Dashboard -->
<li class="relative px-6 py-3">
<span x-show="currentPage === 'dashboard'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
:class="currentPage === 'dashboard' ? 'text-gray-800 dark:text-gray-100' : ''"
href="/admin/dashboard">
<span x-html="$icon('home')"></span>
<span class="ml-4">Dashboard</span>
</a>
</li>
</ul>
<!-- Platform Administration Section --> {# ============================================================================
<div class="px-6 my-6"> REUSABLE MACROS FOR SIDEBAR ITEMS
<hr class="border-gray-200 dark:border-gray-700" /> ============================================================================ #}
</div>
<p class="px-6 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider">
Platform Administration
</p>
<ul class="mt-3">
<!-- Companies -->
<li class="relative px-6 py-3">
<span x-show="currentPage === 'companies'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
:class="currentPage === 'companies' ? 'text-gray-800 dark:text-gray-100' : ''"
href="/admin/companies">
<span x-html="$icon('office-building')"></span>
<span class="ml-4">Companies</span>
</a>
</li>
<!-- Vendors --> {# Macro for collapsible section header #}
<li class="relative px-6 py-3"> {% macro section_header(title, section_key) %}
<span x-show="currentPage === 'vendors'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span> <div class="px-6 my-4">
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200" <hr class="border-gray-200 dark:border-gray-700" />
:class="currentPage === 'vendors' ? 'text-gray-800 dark:text-gray-100' : ''" </div>
href="/admin/vendors"> <button
<span x-html="$icon('shopping-bag')"></span> @click="toggleSection('{{ section_key }}')"
<span class="ml-4">Vendors</span> class="flex items-center justify-between w-full px-6 py-2 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
</a> >
</li> <span>{{ title }}</span>
<span
x-html="$icon('chevron-down', 'w-4 h-4 transition-transform duration-200')"
:class="{ 'rotate-180': openSections.{{ section_key }} }"
></span>
</button>
{% endmacro %}
<!-- Users --> {# Macro for collapsible section content wrapper #}
<li class="relative px-6 py-3"> {% macro section_content(section_key) %}
<span x-show="currentPage === 'users'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span> <ul
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200" x-show="openSections.{{ section_key }}"
:class="currentPage === 'users' ? 'text-gray-800 dark:text-gray-100' : ''" x-transition:enter="transition-all duration-200 ease-out"
href="/admin/users"> x-transition:enter-start="opacity-0 -translate-y-2"
<span x-html="$icon('users')"></span> x-transition:enter-end="opacity-100 translate-y-0"
<span class="ml-4">Users</span> x-transition:leave="transition-all duration-150 ease-in"
</a> x-transition:leave-start="opacity-100 translate-y-0"
</li> x-transition:leave-end="opacity-0 -translate-y-2"
class="mt-1 overflow-hidden"
>
{{ caller() }}
</ul>
{% endmacro %}
<!-- Customers --> {# Macro for menu item #}
<li class="relative px-6 py-3"> {% macro menu_item(page_id, url, icon, label) %}
<span x-show="currentPage === 'customers'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span> <li class="relative px-6 py-3">
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200" <span x-show="currentPage === '{{ page_id }}'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
:class="currentPage === 'customers' ? 'text-gray-800 dark:text-gray-100' : ''" <a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
href="/admin/customers"> :class="currentPage === '{{ page_id }}' ? 'text-gray-800 dark:text-gray-100' : ''"
<span x-html="$icon('user-group')"></span> href="{{ url }}">
<span class="ml-4">Customers</span> <span x-html="$icon('{{ icon }}')"></span>
</a> <span class="ml-4">{{ label }}</span>
</li> </a>
</li>
{% endmacro %}
<!-- Marketplace --> {# ============================================================================
<li class="relative px-6 py-3"> SIDEBAR CONTENT (shared between desktop and mobile)
<span x-show="currentPage === 'marketplace'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span> ============================================================================ #}
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
:class="currentPage === 'marketplace' ? 'text-gray-800 dark:text-gray-100' : ''"
href="/admin/marketplace">
<span x-html="$icon('globe')"></span>
<span class="ml-4">Marketplace</span>
</a>
</li>
</ul>
<!-- Content Management Section --> {% macro sidebar_content() %}
<div class="px-6 my-6"> <div class="py-4 text-gray-500 dark:text-gray-400">
<hr class="border-gray-200 dark:border-gray-700" /> <a class="ml-6 text-lg font-bold text-gray-800 dark:text-gray-200" href="/admin/dashboard">
</div> Admin Portal
<p class="px-6 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider"> </a>
Content Management
</p>
<ul class="mt-3">
<!-- Platform Homepage -->
<li class="relative px-6 py-3">
<span x-show="currentPage === 'platform-homepage'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
:class="currentPage === 'platform-homepage' ? 'text-gray-800 dark:text-gray-100' : ''"
href="/admin/platform-homepage">
<span x-html="$icon('home')"></span>
<span class="ml-4">Platform Homepage</span>
</a>
</li>
<!-- Content Pages --> <!-- Dashboard (always visible) -->
<li class="relative px-6 py-3"> <ul class="mt-6">
<span x-show="currentPage === 'content-pages'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span> {{ menu_item('dashboard', '/admin/dashboard', 'home', 'Dashboard') }}
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200" </ul>
:class="currentPage === 'content-pages' ? 'text-gray-800 dark:text-gray-100' : ''"
href="/admin/content-pages">
<span x-html="$icon('document-text')"></span>
<span class="ml-4">Content Pages</span>
</a>
</li>
<!-- Vendor Themes --> <!-- Platform Administration Section -->
<li class="relative px-6 py-3"> {{ section_header('Platform Administration', 'platformAdmin') }}
<span x-show="currentPage === 'vendor-theme'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span> {% call section_content('platformAdmin') %}
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200" {{ menu_item('companies', '/admin/companies', 'office-building', 'Companies') }}
:class="currentPage === 'vendor-theme' ? 'text-gray-800 dark:text-gray-100' : ''" {{ menu_item('vendors', '/admin/vendors', 'shopping-bag', 'Vendors') }}
href="/admin/vendor-themes"> {{ menu_item('users', '/admin/users', 'users', 'Users') }}
<span x-html="$icon('color-swatch')"></span> {{ menu_item('customers', '/admin/customers', 'user-group', 'Customers') }}
<span class="ml-4">Vendor Themes</span> {{ menu_item('marketplace', '/admin/marketplace', 'globe', 'Marketplace') }}
</a> {% endcall %}
</li>
</ul>
<!-- Developer Tools Section --> <!-- Content Management Section -->
<div class="px-6 my-6"> {{ section_header('Content Management', 'contentMgmt') }}
<hr class="border-gray-200 dark:border-gray-700" /> {% call section_content('contentMgmt') %}
</div> {{ menu_item('platform-homepage', '/admin/platform-homepage', 'home', 'Platform Homepage') }}
<p class="px-6 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider"> {{ menu_item('content-pages', '/admin/content-pages', 'document-text', 'Content Pages') }}
Developer Tools {{ menu_item('vendor-theme', '/admin/vendor-themes', 'color-swatch', 'Vendor Themes') }}
</p> {% endcall %}
<ul class="mt-3">
<!-- Components -->
<li class="relative px-6 py-3">
<span x-show="currentPage === 'components'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
:class="currentPage === 'components' ? 'text-gray-800 dark:text-gray-100' : ''"
href="/admin/components">
<span x-html="$icon('view-grid')"></span>
<span class="ml-4">Components</span>
</a>
</li>
<!-- Icons --> <!-- Developer Tools Section -->
<li class="relative px-6 py-3"> {{ section_header('Developer Tools', 'devTools') }}
<span x-show="currentPage === 'icons'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span> {% call section_content('devTools') %}
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200" {{ menu_item('components', '/admin/components', 'view-grid', 'Components') }}
:class="currentPage === 'icons' ? 'text-gray-800 dark:text-gray-100' : ''" {{ menu_item('icons', '/admin/icons', 'photograph', 'Icons') }}
href="/admin/icons"> {{ menu_item('testing', '/admin/testing', 'beaker', 'Testing Hub') }}
<span x-html="$icon('photograph')"></span> {{ menu_item('code-quality', '/admin/code-quality', 'shield-check', 'Code Quality') }}
<span class="ml-4">Icons</span> {% endcall %}
</a>
</li>
<!-- Testing Hub --> <!-- Platform Monitoring Section -->
<li class="relative px-6 py-3"> {{ section_header('Platform Monitoring', 'monitoring') }}
<span x-show="currentPage === 'testing'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span> {% call section_content('monitoring') %}
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200" {{ menu_item('imports', '/admin/imports', 'cube', 'Import Jobs') }}
:class="currentPage === 'testing' ? 'text-gray-800 dark:text-gray-100' : ''" {{ menu_item('logs', '/admin/logs', 'document-text', 'Application Logs') }}
href="/admin/testing"> {% endcall %}
<span x-html="$icon('beaker')"></span>
<span class="ml-4">Testing Hub</span>
</a>
</li>
<!-- Code Quality --> <!-- Settings (always visible) -->
<li class="relative px-6 py-3"> <div class="px-6 my-4">
<span x-show="currentPage === 'code-quality'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span> <hr class="border-gray-200 dark:border-gray-700" />
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
:class="currentPage === 'code-quality' ? 'text-gray-800 dark:text-gray-100' : ''"
href="/admin/code-quality">
<span x-html="$icon('shield-check')"></span>
<span class="ml-4">Code Quality</span>
</a>
</li>
</ul>
<!-- Platform Monitoring Section -->
<div class="px-6 my-6">
<hr class="border-gray-200 dark:border-gray-700" />
</div>
<p class="px-6 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider">
Platform Monitoring
</p>
<ul class="mt-3">
<!-- Import Jobs -->
<li class="relative px-6 py-3">
<span x-show="currentPage === 'imports'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
:class="currentPage === 'imports' ? 'text-gray-800 dark:text-gray-100' : ''"
href="/admin/imports">
<span x-html="$icon('cube')"></span>
<span class="ml-4">Import Jobs</span>
</a>
</li>
<!-- Application Logs -->
<li class="relative px-6 py-3">
<span x-show="currentPage === 'logs'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
:class="currentPage === 'logs' ? 'text-gray-800 dark:text-gray-100' : ''"
href="/admin/logs">
<span x-html="$icon('document-text')"></span>
<span class="ml-4">Application Logs</span>
</a>
</li>
</ul>
<!-- Settings Section -->
<div class="px-6 my-6">
<hr class="border-gray-200 dark:border-gray-700" />
</div>
<ul>
<!-- Settings -->
<li class="relative px-6 py-3">
<span x-show="currentPage === 'settings'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
:class="currentPage === 'settings' ? 'text-gray-800 dark:text-gray-100' : ''"
href="/admin/settings">
<span x-html="$icon('cog')"></span>
<span class="ml-4">Settings</span>
</a>
</li>
</ul>
</div> </div>
<ul>
{{ menu_item('settings', '/admin/settings', 'cog', 'Settings') }}
</ul>
</div>
{% endmacro %}
{# ============================================================================
DESKTOP SIDEBAR
============================================================================ #}
<aside class="z-20 hidden w-64 overflow-y-auto bg-white dark:bg-gray-800 md:block flex-shrink-0">
{{ sidebar_content() }}
</aside> </aside>
<!-- Mobile sidebar --> {# ============================================================================
MOBILE SIDEBAR
============================================================================ #}
<!-- Mobile sidebar backdrop -->
<div x-show="isSideMenuOpen" <div x-show="isSideMenuOpen"
x-transition:enter="transition ease-in-out duration-150" x-transition:enter="transition ease-in-out duration-150"
x-transition:enter-start="opacity-0" x-transition:enter-start="opacity-0"
@@ -237,6 +132,7 @@
x-transition:leave-end="opacity-0" x-transition:leave-end="opacity-0"
class="fixed inset-0 z-10 flex items-end bg-black bg-opacity-50 sm:items-center sm:justify-center"></div> class="fixed inset-0 z-10 flex items-end bg-black bg-opacity-50 sm:items-center sm:justify-center"></div>
<!-- Mobile sidebar panel -->
<aside class="fixed inset-y-0 z-20 flex-shrink-0 w-64 mt-16 overflow-y-auto bg-white dark:bg-gray-800 md:hidden" <aside class="fixed inset-y-0 z-20 flex-shrink-0 w-64 mt-16 overflow-y-auto bg-white dark:bg-gray-800 md:hidden"
x-show="isSideMenuOpen" x-show="isSideMenuOpen"
x-transition:enter="transition ease-in-out duration-150" x-transition:enter="transition ease-in-out duration-150"
@@ -247,228 +143,5 @@
x-transition:leave-end="opacity-0 transform -translate-x-20" x-transition:leave-end="opacity-0 transform -translate-x-20"
@click.away="closeSideMenu" @click.away="closeSideMenu"
@keydown.escape="closeSideMenu"> @keydown.escape="closeSideMenu">
<div class="py-4 text-gray-500 dark:text-gray-400"> {{ sidebar_content() }}
<a class="ml-6 text-lg font-bold text-gray-800 dark:text-gray-200" href="/admin/dashboard">
Admin Portal
</a>
<ul class="mt-6">
<!-- Dashboard -->
<li class="relative px-6 py-3">
<span x-show="currentPage === 'dashboard'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
:class="currentPage === 'dashboard' ? 'text-gray-800 dark:text-gray-100' : ''"
href="/admin/dashboard">
<span x-html="$icon('home')"></span>
<span class="ml-4">Dashboard</span>
</a>
</li>
</ul>
<!-- Platform Administration Section -->
<div class="px-6 my-6">
<hr class="border-gray-200 dark:border-gray-700" />
</div>
<p class="px-6 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider">
Platform Administration
</p>
<ul class="mt-3">
<!-- Companies -->
<li class="relative px-6 py-3">
<span x-show="currentPage === 'companies'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
:class="currentPage === 'companies' ? 'text-gray-800 dark:text-gray-100' : ''"
href="/admin/companies">
<span x-html="$icon('office-building')"></span>
<span class="ml-4">Companies</span>
</a>
</li>
<!-- Vendors -->
<li class="relative px-6 py-3">
<span x-show="currentPage === 'vendors'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
:class="currentPage === 'vendors' ? 'text-gray-800 dark:text-gray-100' : ''"
href="/admin/vendors">
<span x-html="$icon('shopping-bag')"></span>
<span class="ml-4">Vendors</span>
</a>
</li>
<!-- Users -->
<li class="relative px-6 py-3">
<span x-show="currentPage === 'users'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
:class="currentPage === 'users' ? 'text-gray-800 dark:text-gray-100' : ''"
href="/admin/users">
<span x-html="$icon('users')"></span>
<span class="ml-4">Users</span>
</a>
</li>
<!-- Customers -->
<li class="relative px-6 py-3">
<span x-show="currentPage === 'customers'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
:class="currentPage === 'customers' ? 'text-gray-800 dark:text-gray-100' : ''"
href="/admin/customers">
<span x-html="$icon('user-group')"></span>
<span class="ml-4">Customers</span>
</a>
</li>
<!-- Marketplace -->
<li class="relative px-6 py-3">
<span x-show="currentPage === 'marketplace'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
:class="currentPage === 'marketplace' ? 'text-gray-800 dark:text-gray-100' : ''"
href="/admin/marketplace">
<span x-html="$icon('globe')"></span>
<span class="ml-4">Marketplace</span>
</a>
</li>
</ul>
<!-- Content Management Section -->
<div class="px-6 my-6">
<hr class="border-gray-200 dark:border-gray-700" />
</div>
<p class="px-6 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider">
Content Management
</p>
<ul class="mt-3">
<!-- Platform Homepage -->
<li class="relative px-6 py-3">
<span x-show="currentPage === 'platform-homepage'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
:class="currentPage === 'platform-homepage' ? 'text-gray-800 dark:text-gray-100' : ''"
href="/admin/platform-homepage">
<span x-html="$icon('home')"></span>
<span class="ml-4">Platform Homepage</span>
</a>
</li>
<!-- Content Pages -->
<li class="relative px-6 py-3">
<span x-show="currentPage === 'content-pages'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
:class="currentPage === 'content-pages' ? 'text-gray-800 dark:text-gray-100' : ''"
href="/admin/content-pages">
<span x-html="$icon('document-text')"></span>
<span class="ml-4">Content Pages</span>
</a>
</li>
<!-- Vendor Themes -->
<li class="relative px-6 py-3">
<span x-show="currentPage === 'vendor-theme'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
:class="currentPage === 'vendor-theme' ? 'text-gray-800 dark:text-gray-100' : ''"
href="/admin/vendor-themes">
<span x-html="$icon('color-swatch')"></span>
<span class="ml-4">Vendor Themes</span>
</a>
</li>
</ul>
<!-- Developer Tools Section -->
<div class="px-6 my-6">
<hr class="border-gray-200 dark:border-gray-700" />
</div>
<p class="px-6 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider">
Developer Tools
</p>
<ul class="mt-3">
<!-- Components -->
<li class="relative px-6 py-3">
<span x-show="currentPage === 'components'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
:class="currentPage === 'components' ? 'text-gray-800 dark:text-gray-100' : ''"
href="/admin/components">
<span x-html="$icon('view-grid')"></span>
<span class="ml-4">Components</span>
</a>
</li>
<!-- Icons -->
<li class="relative px-6 py-3">
<span x-show="currentPage === 'icons'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
:class="currentPage === 'icons' ? 'text-gray-800 dark:text-gray-100' : ''"
href="/admin/icons">
<span x-html="$icon('photograph')"></span>
<span class="ml-4">Icons</span>
</a>
</li>
<!-- Testing Hub -->
<li class="relative px-6 py-3">
<span x-show="currentPage === 'testing'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
:class="currentPage === 'testing' ? 'text-gray-800 dark:text-gray-100' : ''"
href="/admin/testing">
<span x-html="$icon('beaker')"></span>
<span class="ml-4">Testing Hub</span>
</a>
</li>
<!-- Code Quality -->
<li class="relative px-6 py-3">
<span x-show="currentPage === 'code-quality'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
:class="currentPage === 'code-quality' ? 'text-gray-800 dark:text-gray-100' : ''"
href="/admin/code-quality">
<span x-html="$icon('shield-check')"></span>
<span class="ml-4">Code Quality</span>
</a>
</li>
</ul>
<!-- Platform Monitoring Section -->
<div class="px-6 my-6">
<hr class="border-gray-200 dark:border-gray-700" />
</div>
<p class="px-6 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider">
Platform Monitoring
</p>
<ul class="mt-3">
<!-- Import Jobs -->
<li class="relative px-6 py-3">
<span x-show="currentPage === 'imports'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
:class="currentPage === 'imports' ? 'text-gray-800 dark:text-gray-100' : ''"
href="/admin/imports">
<span x-html="$icon('cube')"></span>
<span class="ml-4">Import Jobs</span>
</a>
</li>
<!-- Application Logs -->
<li class="relative px-6 py-3">
<span x-show="currentPage === 'logs'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
:class="currentPage === 'logs' ? 'text-gray-800 dark:text-gray-100' : ''"
href="/admin/logs">
<span x-html="$icon('document-text')"></span>
<span class="ml-4">Application Logs</span>
</a>
</li>
</ul>
<!-- Settings Section -->
<div class="px-6 my-6">
<hr class="border-gray-200 dark:border-gray-700" />
</div>
<ul>
<!-- Settings -->
<li class="relative px-6 py-3">
<span x-show="currentPage === 'settings'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
:class="currentPage === 'settings' ? 'text-gray-800 dark:text-gray-100' : ''"
href="/admin/settings">
<span x-html="$icon('cog')"></span>
<span class="ml-4">Settings</span>
</a>
</li>
</ul>
</div>
</aside> </aside>

View File

@@ -1,15 +1,15 @@
/** /**
* Alpine.js v3 global data initialization * 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() { function data() {
// ─────────────────────────────────────────────────────────────────
// Theme (dark mode) persistence
// ─────────────────────────────────────────────────────────────────
function getThemeFromLocalStorage() { function getThemeFromLocalStorage() {
// if user already changed the theme, use it
if (window.localStorage.getItem('dark')) { if (window.localStorage.getItem('dark')) {
return JSON.parse(window.localStorage.getItem('dark')) return JSON.parse(window.localStorage.getItem('dark'))
} }
// else return their preferences
return ( return (
!!window.matchMedia && !!window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches window.matchMedia('(prefers-color-scheme: dark)').matches
@@ -20,12 +20,74 @@ function data() {
window.localStorage.setItem('dark', value) 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 { return {
// ─────────────────────────────────────────────────────────────────
// Theme
// ─────────────────────────────────────────────────────────────────
dark: getThemeFromLocalStorage(), dark: getThemeFromLocalStorage(),
toggleTheme() { toggleTheme() {
this.dark = !this.dark this.dark = !this.dark
setThemeToLocalStorage(this.dark) setThemeToLocalStorage(this.dark)
}, },
// ─────────────────────────────────────────────────────────────────
// Mobile side menu
// ─────────────────────────────────────────────────────────────────
isSideMenuOpen: false, isSideMenuOpen: false,
toggleSideMenu() { toggleSideMenu() {
this.isSideMenuOpen = !this.isSideMenuOpen this.isSideMenuOpen = !this.isSideMenuOpen
@@ -33,6 +95,10 @@ function data() {
closeSideMenu() { closeSideMenu() {
this.isSideMenuOpen = false this.isSideMenuOpen = false
}, },
// ─────────────────────────────────────────────────────────────────
// Notifications menu
// ─────────────────────────────────────────────────────────────────
isNotificationsMenuOpen: false, isNotificationsMenuOpen: false,
toggleNotificationsMenu() { toggleNotificationsMenu() {
this.isNotificationsMenuOpen = !this.isNotificationsMenuOpen this.isNotificationsMenuOpen = !this.isNotificationsMenuOpen
@@ -40,6 +106,10 @@ function data() {
closeNotificationsMenu() { closeNotificationsMenu() {
this.isNotificationsMenuOpen = false this.isNotificationsMenuOpen = false
}, },
// ─────────────────────────────────────────────────────────────────
// Profile menu
// ─────────────────────────────────────────────────────────────────
isProfileMenuOpen: false, isProfileMenuOpen: false,
toggleProfileMenu() { toggleProfileMenu() {
this.isProfileMenuOpen = !this.isProfileMenuOpen this.isProfileMenuOpen = !this.isProfileMenuOpen
@@ -47,11 +117,37 @@ function data() {
closeProfileMenu() { closeProfileMenu() {
this.isProfileMenuOpen = false this.isProfileMenuOpen = false
}, },
// ─────────────────────────────────────────────────────────────────
// Pages menu (legacy)
// ─────────────────────────────────────────────────────────────────
isPagesMenuOpen: false, isPagesMenuOpen: false,
togglePagesMenu() { togglePagesMenu() {
this.isPagesMenuOpen = !this.isPagesMenuOpen 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 // Page identifier - will be set by individual pages
// ─────────────────────────────────────────────────────────────────
currentPage: '' currentPage: ''
} }
} }