feat(layout): migrate to full-width topbar with mobile drawer navigation
Move topbar outside the grid container as a standalone sticky header (Shopify/Stripe pattern). Sidebar and icon bar use position:sticky relative to topbar height. Mobile uses drawer pattern with backdrop, focus trap, ESC close, and scroll lock. Tenant branding moved from sidebar header into the topbar. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -320,12 +320,12 @@ $componentPageConfig['moduleRuntimeComponents'] = $moduleRuntimeComponents;
|
||||
|
||||
<body>
|
||||
<a class="app-skip-link" href="#app-main-content"><?php e(t('Skip to main content')); ?></a>
|
||||
<?php require __DIR__ . '/partials/app-topbar.phtml'; ?>
|
||||
<div class="app-container">
|
||||
<?php require __DIR__ . '/partials/app-main-aside-icon-bar.phtml'; ?>
|
||||
<?php require __DIR__ . '/partials/app-main-aside.phtml'; ?>
|
||||
<div class="app-main">
|
||||
<main>
|
||||
<?php require __DIR__ . '/partials/app-topbar.phtml'; ?>
|
||||
<div id="app-main-content" class="app-main-content" tabindex="-1">
|
||||
<?php Buffer::get('html'); ?>
|
||||
</div>
|
||||
@@ -333,6 +333,7 @@ $componentPageConfig['moduleRuntimeComponents'] = $moduleRuntimeComponents;
|
||||
<?php require __DIR__ . '/partials/app-footer.phtml'; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="app-drawer-backdrop" aria-hidden="true"></div>
|
||||
|
||||
<script src="<?php e(assetVersion('vendor/fslightbox/fslightbox.js')); ?>"></script>
|
||||
<?php require __DIR__ . '/partials/app-confirm-dialog.phtml'; ?>
|
||||
|
||||
@@ -25,9 +25,8 @@ $docsDefaultPath = 'admin/docs/' . $docsDefaultSlug;
|
||||
$hasOrganization = $canViewTenants || $canViewDepartments || $canViewUsers;
|
||||
$hasUsersSection = $canViewRoles || $canViewPermissions;
|
||||
$hasAutomationSection = $canViewImports || $canViewJobs;
|
||||
$hasMonitoringSection = $canViewStats;
|
||||
$hasLogsSection = $canViewMailLog;
|
||||
$hasSystemSection = $canViewSettings;
|
||||
$hasSystemSection = $canViewSettings || $canViewStats;
|
||||
$hasAdminPanel = layoutHasAdminPanel($layoutAuth);
|
||||
|
||||
// Declarative nav config for admin panel groups
|
||||
@@ -105,21 +104,6 @@ $adminNavGroups = [
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'admin-monitoring',
|
||||
'label' => t('Monitoring'),
|
||||
'icon' => 'bi-graph-up',
|
||||
'visible' => $hasMonitoringSection,
|
||||
'items' => [
|
||||
[
|
||||
'label' => t('Statistics'),
|
||||
'path' => 'admin/stats',
|
||||
'active' => navActive('admin/stats', true),
|
||||
'visible' => $canViewStats,
|
||||
'withTenant' => false,
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'admin-logs',
|
||||
'label' => t('Logs'),
|
||||
@@ -148,6 +132,13 @@ $adminNavGroups = [
|
||||
'visible' => $canViewSettings,
|
||||
'withTenant' => false,
|
||||
],
|
||||
[
|
||||
'label' => t('Statistics'),
|
||||
'path' => 'admin/stats',
|
||||
'active' => navActive('admin/stats', true),
|
||||
'visible' => $canViewStats,
|
||||
'withTenant' => false,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -281,24 +272,6 @@ $moduleSlots = is_array($layoutNav['moduleSlots'] ?? null) ? $layoutNav['moduleS
|
||||
$modulePanelSlots = is_array($moduleSlots['aside.tab_panel'] ?? null) ? $moduleSlots['aside.tab_panel'] : [];
|
||||
?>
|
||||
<aside class="app-sidebar" id="app-sidebar" data-app-component="sidebar-toggle">
|
||||
<?php if ($currentTenant): ?>
|
||||
<div class="app-sidebar-tenant-logo">
|
||||
<a href="<?php e(lurl('')); ?>">
|
||||
<?php if ($hasTenantAvatar): ?>
|
||||
<img src="auth/tenant-avatar-file?uuid=<?php e($tenantUuid); ?>&size=256" alt="<?php e($tenantName); ?>"
|
||||
title="<?php e($tenantName); ?>">
|
||||
<?php else: ?>
|
||||
<div class="app-sidebar-tenant-name">
|
||||
<?php e($tenantName); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
<button type="button" class="app-sidebar-tenant-toggle" data-sidebar-toggle
|
||||
aria-label="<?php e(t('Toggle Sidebar')); ?>">
|
||||
<i class="bi bi-list"></i>
|
||||
</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div id="app-sidebar-panels" class="app-sidebar-panels">
|
||||
<nav id="aside-panel-explorer" class="app-sidebar-panel" data-aside-panel="explorer"
|
||||
data-aside-title="<?php e(t('Explorer')); ?>" role="tabpanel" aria-labelledby="aside-tab-explorer"
|
||||
|
||||
@@ -11,6 +11,13 @@ $theme = currentTheme();
|
||||
$csrfKey = Session::$csrfSessionKey;
|
||||
$csrfToken = $_SESSION[$csrfKey] ?? '';
|
||||
|
||||
// Tenant branding data (from $layoutNav, same source as app-main-aside.phtml)
|
||||
$layoutNav = is_array($layoutNav ?? null) ? $layoutNav : [];
|
||||
$tenantAvatar = is_array($layoutNav['tenantAvatar'] ?? null) ? $layoutNav['tenantAvatar'] : [];
|
||||
$brandTenantUuid = trim((string) ($tenantAvatar['uuid'] ?? ''));
|
||||
$brandTenantName = trim((string) ($tenantAvatar['name'] ?? ''));
|
||||
$brandHasTenantAvatar = !empty($tenantAvatar['hasAvatar']);
|
||||
|
||||
// Tenant switcher data
|
||||
$currentTenant = $_SESSION['current_tenant'] ?? null;
|
||||
$currentTenantId = (int) ($currentTenant['id'] ?? 0);
|
||||
@@ -30,6 +37,22 @@ $moduleTopbarSlots = is_array($moduleSlots['topbar.right_item'] ?? null) ? $modu
|
||||
<header class="app-header">
|
||||
<nav class="app-topbar">
|
||||
<div class="app-topbar-left">
|
||||
<button type="button" class="app-topbar-hamburger" data-sidebar-toggle
|
||||
aria-expanded="false"
|
||||
aria-label="<?php e(t('Open navigation')); ?>"
|
||||
data-open-label="<?php e(t('Open navigation')); ?>"
|
||||
data-close-label="<?php e(t('Close navigation')); ?>">
|
||||
<i class="bi bi-list" aria-hidden="true"></i>
|
||||
</button>
|
||||
<a href="<?php e(lurl('')); ?>" class="app-topbar-brand">
|
||||
<?php if ($brandHasTenantAvatar): ?>
|
||||
<img src="auth/tenant-avatar-file?uuid=<?php e($brandTenantUuid); ?>&size=256" alt="<?php e($brandTenantName); ?>">
|
||||
<?php elseif ($brandTenantName !== ''): ?>
|
||||
<span class="app-topbar-brand-name"><?php e($brandTenantName); ?></span>
|
||||
<?php else: ?>
|
||||
<span class="app-topbar-brand-name"><?php e(appTitle()); ?></span>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
<?php
|
||||
if (is_array($breadcrumbs ?? null) && $breadcrumbs !== []) {
|
||||
require __DIR__ . '/app-breadcrumb.phtml';
|
||||
@@ -45,11 +68,27 @@ $moduleTopbarSlots = is_array($moduleSlots['topbar.right_item'] ?? null) ? $modu
|
||||
</button>
|
||||
</div>
|
||||
<ul class="app-topbar-right">
|
||||
<?php foreach ($moduleTopbarSlots as $slot): ?>
|
||||
<?php
|
||||
if (!is_array($slot)) {
|
||||
continue;
|
||||
}
|
||||
$slotPermission = trim((string) ($slot['permission'] ?? ''));
|
||||
if ($slotPermission !== '' && empty($layoutAuth[$slotPermission])) {
|
||||
continue;
|
||||
}
|
||||
$slotTemplate = trim((string) ($slot['template'] ?? ''));
|
||||
if ($slotTemplate === '' || !is_file($slotTemplate)) {
|
||||
continue;
|
||||
}
|
||||
include $slotTemplate;
|
||||
?>
|
||||
<?php endforeach; ?>
|
||||
<?php if ($canSwitchTenant): ?>
|
||||
<li class="app-topbar-tenant-slot" data-app-component="tenant-switcher" data-tenant-switcher data-tooltip="<?php e(t('Tenant')); ?>" data-tooltip-pos="bottom">
|
||||
<details class="dropdown app-topbar-tenant-menu">
|
||||
<summary class="app-topbar-tenant-chip" aria-label="<?php e(t('Switch tenant')); ?>" title="<?php e($tenantLabel); ?>">
|
||||
<span class="app-topbar-tenant-name"><?php e($tenantLabel); ?></span>
|
||||
<li data-app-component="tenant-switcher" data-tenant-switcher data-tooltip="<?php e(t('Tenant')); ?>" data-tooltip-pos="bottom">
|
||||
<details class="dropdown app-topbar-tenant-menu" data-summary-chevron="none">
|
||||
<summary aria-label="<?php e(t('Switch tenant')); ?>" title="<?php e($tenantLabel); ?>">
|
||||
<i class="bi bi-building" aria-hidden="true"></i>
|
||||
</summary>
|
||||
<ul class="app-topbar-menu-list app-topbar-menu-list--right">
|
||||
<?php foreach ($availableTenants as $tenant): ?>
|
||||
@@ -84,24 +123,8 @@ $moduleTopbarSlots = is_array($moduleSlots['topbar.right_item'] ?? null) ? $modu
|
||||
</details>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php foreach ($moduleTopbarSlots as $slot): ?>
|
||||
<?php
|
||||
if (!is_array($slot)) {
|
||||
continue;
|
||||
}
|
||||
$slotPermission = trim((string) ($slot['permission'] ?? ''));
|
||||
if ($slotPermission !== '' && empty($layoutAuth[$slotPermission])) {
|
||||
continue;
|
||||
}
|
||||
$slotTemplate = trim((string) ($slot['template'] ?? ''));
|
||||
if ($slotTemplate === '' || !is_file($slotTemplate)) {
|
||||
continue;
|
||||
}
|
||||
include $slotTemplate;
|
||||
?>
|
||||
<?php endforeach; ?>
|
||||
<li>
|
||||
<details class="dropdown app-topbar-user-menu" data-app-component="theme-controls"
|
||||
<details class="dropdown app-topbar-user-menu" data-summary-chevron="none" data-app-component="theme-controls"
|
||||
<?php if ($allowUserTheme): ?>
|
||||
data-theme-menu
|
||||
data-theme-url="admin/users/theme"
|
||||
@@ -109,7 +132,7 @@ $moduleTopbarSlots = is_array($moduleSlots['topbar.right_item'] ?? null) ? $modu
|
||||
data-csrf-token="<?php e($csrfToken); ?>"
|
||||
<?php endif; ?>>
|
||||
<summary aria-label="<?php e(t('Account')); ?>">
|
||||
<i class="bi bi-person-fill"></i>
|
||||
<i class="bi bi-person"></i>
|
||||
</summary>
|
||||
<ul class="app-topbar-menu-list app-topbar-menu-list--right">
|
||||
<li class="app-topbar-menu-heading" role="presentation"><small><?php e(t('Navigation')); ?></small></li>
|
||||
|
||||
Reference in New Issue
Block a user