Files
tinker_tickets/views/layout_header.php
T
jared ec92445a0f Force header clearance via inline style on main element
CSS cascade fixes were correct but browser was serving cached base.css.
Inline style cannot be cached separately and bypasses all cascade issues.
CSS variables still respect media query :root overrides so --header-height
resolves to the correct value (50px SM, 46px XS) at each breakpoint.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 17:52:22 -04:00

185 lines
11 KiB
PHP

<?php
/**
* layout_header.php — Shared top-of-page partial for all views.
*
* Expected variables set by the including view before require:
* string $pageTitle Page title suffix (e.g. "Dashboard", "Ticket #42")
* string $activeNav Active nav key: 'dashboard', 'tickets', 'admin-*'
* array|null $pageStyles Optional extra CSS hrefs to load
* string $nonce CSP nonce from SecurityHeadersMiddleware::getNonce()
*
* Globals used:
* $GLOBALS['currentUser'] — user array (username, display_name, is_admin, groups)
* $GLOBALS['config'] — app config array
* CsrfMiddleware::getToken() — returns current CSRF token string
*/
$_lt_user = $GLOBALS['currentUser'] ?? [];
$_lt_isAdmin = !empty($_lt_user['is_admin']);
$_lt_navActive = $activeNav ?? 'dashboard';
$_lt_appName = $GLOBALS['config']['APP_NAME'] ?? 'TINKER TICKETS';
$_lt_subtitle = $GLOBALS['config']['APP_SUBTITLE'] ?? 'LotusGuild Infrastructure';
$_lt_assetVer = $GLOBALS['config']['ASSET_VERSION'] ?? '20260329';
?>
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="theme-color" content="#030508">
<title><?= htmlspecialchars($pageTitle ?? 'Dashboard', ENT_QUOTES, 'UTF-8') ?> &mdash; <?= htmlspecialchars($_lt_appName, ENT_QUOTES, 'UTF-8') ?></title>
<meta name="robots" content="noindex, nofollow">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,600;0,700;1,400&family=VT323&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/assets/css/base.css?v=<?= $_lt_assetVer ?>">
<?php if (!empty($pageStyles)): ?>
<?php foreach ($pageStyles as $_lt_css): ?>
<link rel="stylesheet" href="<?= htmlspecialchars($_lt_css, ENT_QUOTES, 'UTF-8') ?>">
<?php endforeach; ?>
<?php endif; ?>
<link rel="icon" href="/assets/images/favicon.png" type="image/png">
<!-- Base JS loaded in head so lt.* is available for inline view scripts -->
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>" src="/assets/js/base.js?v=<?= $_lt_assetVer ?>"></script>
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>" src="/assets/js/utils.js?v=<?= $_lt_assetVer ?>"></script>
<!-- Inline JS globals (CSRF, timezone, user) available immediately -->
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>">
window.CSRF_TOKEN = <?= json_encode(CsrfMiddleware::getToken(), JSON_UNESCAPED_UNICODE | JSON_HEX_TAG) ?>;
window.APP_TIMEZONE = <?= json_encode($GLOBALS['config']['TIMEZONE'] ?? 'UTC', JSON_UNESCAPED_UNICODE | JSON_HEX_TAG) ?>;
window.APP_TIMEZONE_ABBREV = <?= json_encode($GLOBALS['config']['TIMEZONE_ABBREV'] ?? 'UTC', JSON_UNESCAPED_UNICODE | JSON_HEX_TAG) ?>;
window.APP_TIMEZONE_OFFSET = <?= (int)($GLOBALS['config']['TIMEZONE_OFFSET'] ?? 0) ?>;
window.CURRENT_USER = <?= json_encode([
'id' => (int)($GLOBALS['currentUser']['user_id'] ?? 0),
'username'=> $GLOBALS['currentUser']['username'] ?? '',
'isAdmin' => !empty($GLOBALS['currentUser']['is_admin']),
], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG) ?>;
</script>
</head>
<body>
<!-- SKIP LINK -->
<a class="lt-skip-link" href="#main-content">Skip to main content</a>
<!-- BOOT OVERLAY — controlled by lt.boot() in base.js; shown once per session -->
<div id="lt-boot" class="lt-boot-overlay" data-app-name="<?= htmlspecialchars($_lt_appName, ENT_QUOTES, 'UTF-8') ?>" style="display:none" aria-hidden="true">
<pre id="lt-boot-text" class="lt-boot-text"></pre>
</div>
<!-- MOBILE NAV DRAWER — matches web_template structure exactly -->
<div id="lt-nav-drawer" class="lt-nav-drawer" aria-hidden="true" role="dialog" aria-modal="true" aria-label="Navigation menu">
<div class="lt-nav-drawer-header">
<span class="lt-brand-title"><?= htmlspecialchars($_lt_appName, ENT_QUOTES, 'UTF-8') ?></span>
<button type="button" class="lt-nav-drawer-close" id="lt-nav-drawer-close" aria-label="Close navigation">&#x2715;</button>
</div>
<nav class="lt-nav-drawer-links" aria-label="Mobile navigation">
<a href="/"
class="lt-nav-drawer-link<?= $_lt_navActive === 'dashboard' ? ' active' : '' ?>"
<?= $_lt_navActive === 'dashboard' ? 'aria-current="page"' : '' ?>>Dashboard</a>
<?php if ($_lt_isAdmin): ?>
<div class="lt-nav-drawer-section">Admin</div>
<a href="/admin/templates" class="lt-nav-drawer-link lt-nav-drawer-link--indent<?= $_lt_navActive === 'admin-templates' ? ' active' : '' ?>">Templates</a>
<a href="/admin/workflow" class="lt-nav-drawer-link lt-nav-drawer-link--indent<?= $_lt_navActive === 'admin-workflow' ? ' active' : '' ?>">Workflow</a>
<a href="/admin/recurring-tickets" class="lt-nav-drawer-link lt-nav-drawer-link--indent<?= $_lt_navActive === 'admin-recurring' ? ' active' : '' ?>">Recurring</a>
<a href="/admin/custom-fields" class="lt-nav-drawer-link lt-nav-drawer-link--indent<?= $_lt_navActive === 'admin-custom-fields' ? ' active' : '' ?>">Custom Fields</a>
<a href="/admin/user-activity" class="lt-nav-drawer-link lt-nav-drawer-link--indent<?= $_lt_navActive === 'admin-user-activity' ? ' active' : '' ?>">User Activity</a>
<a href="/admin/audit-log" class="lt-nav-drawer-link lt-nav-drawer-link--indent<?= $_lt_navActive === 'admin-audit-log' ? ' active' : '' ?>">Audit Log</a>
<a href="/admin/api-keys" class="lt-nav-drawer-link lt-nav-drawer-link--indent<?= $_lt_navActive === 'admin-api-keys' ? ' active' : '' ?>">API Keys</a>
<?php endif; ?>
</nav>
</div><!-- /.lt-nav-drawer -->
<!-- Overlay: outside drawer, full-screen; JS toggles .open class -->
<div id="lt-nav-overlay" class="lt-nav-drawer-overlay"></div>
<!-- PRIMARY HEADER -->
<header class="lt-header" role="banner">
<div class="lt-header-left">
<!-- Hamburger — opens mobile nav drawer -->
<button type="button"
class="lt-menu-btn"
id="lt-menu-btn"
data-action="open-nav-drawer"
aria-label="Open navigation menu"
aria-expanded="false"
aria-controls="lt-nav-drawer">
<span class="lt-menu-btn-bar"></span>
<span class="lt-menu-btn-bar"></span>
<span class="lt-menu-btn-bar"></span>
</button>
<!-- Brand -->
<div class="lt-brand">
<a href="/"
class="lt-brand-title lt-glitch"
data-text="<?= htmlspecialchars($_lt_appName, ENT_QUOTES, 'UTF-8') ?>"
style="text-decoration:none"
aria-label="<?= htmlspecialchars($_lt_appName, ENT_QUOTES, 'UTF-8') ?> home"><?= htmlspecialchars($_lt_appName, ENT_QUOTES, 'UTF-8') ?></a>
<span class="lt-brand-subtitle"><?= htmlspecialchars($_lt_subtitle, ENT_QUOTES, 'UTF-8') ?></span>
</div>
<!-- Desktop navigation -->
<nav class="lt-nav" aria-label="Main navigation">
<a href="/"
class="lt-nav-link<?= $_lt_navActive === 'dashboard' ? ' active' : '' ?>"
<?= $_lt_navActive === 'dashboard' ? 'aria-current="page"' : '' ?>>
Dashboard
</a>
<?php if ($_lt_isAdmin): ?>
<div class="lt-nav-dropdown" data-action="toggle-nav-dropdown">
<a href="#"
class="lt-nav-link<?= str_starts_with($_lt_navActive, 'admin') ? ' active' : '' ?>"
role="button"
aria-haspopup="true"
aria-expanded="false"
aria-controls="lt-admin-dropdown-menu">
Admin &#x25BE;
</a>
<ul class="lt-nav-dropdown-menu"
id="lt-admin-dropdown-menu"
role="menu"
aria-label="Admin menu">
<li role="none"><a href="/admin/templates" role="menuitem" class="<?= $_lt_navActive === 'admin-templates' ? 'active' : '' ?>">Templates</a></li>
<li role="none"><a href="/admin/workflow" role="menuitem" class="<?= $_lt_navActive === 'admin-workflow' ? 'active' : '' ?>">Workflow</a></li>
<li role="none"><a href="/admin/recurring-tickets" role="menuitem" class="<?= $_lt_navActive === 'admin-recurring' ? 'active' : '' ?>">Recurring</a></li>
<li role="none"><a href="/admin/custom-fields" role="menuitem" class="<?= $_lt_navActive === 'admin-custom-fields' ? 'active' : '' ?>">Custom Fields</a></li>
<li role="none"><a href="/admin/user-activity" role="menuitem" class="<?= $_lt_navActive === 'admin-user-activity' ? 'active' : '' ?>">User Activity</a></li>
<li role="none"><a href="/admin/audit-log" role="menuitem" class="<?= $_lt_navActive === 'admin-audit-log' ? 'active' : '' ?>">Audit Log</a></li>
<li role="none"><a href="/admin/api-keys" role="menuitem" class="<?= $_lt_navActive === 'admin-api-keys' ? 'active' : '' ?>">API Keys</a></li>
</ul>
</div>
<?php endif; ?>
</nav><!-- /.lt-nav -->
</div><!-- /.lt-header-left -->
<div class="lt-header-right">
<?php if (!empty($_lt_user)): ?>
<?php
$_lt_displayName = $_lt_user['display_name'] ?? $_lt_user['username'] ?? '';
$_lt_words = array_filter(explode(' ', $_lt_displayName));
$_lt_initials = strtoupper(implode('', array_map(fn($w) => $w[0], array_slice($_lt_words, 0, 2))));
$_lt_userId = (int)($_lt_user['user_id'] ?? 0);
$_lt_avatarColors = ['lt-avatar--orange', 'lt-avatar--green', 'lt-avatar--purple', ''];
$_lt_avatarColor = $_lt_avatarColors[abs(crc32($_lt_displayName)) % count($_lt_avatarColors)];
?>
<div class="lt-avatar lt-avatar--sm <?= $_lt_avatarColor ?>" aria-hidden="true" title="<?= htmlspecialchars($_lt_displayName, ENT_QUOTES, 'UTF-8') ?>">
<?php if ($_lt_userId > 0): ?>
<img src="/api/user_avatar.php?user_id=<?= $_lt_userId ?>"
alt=""
class="lt-avatar-img"
onerror="this.style.display='none'">
<?php endif ?>
<span class="lt-avatar-initials"><?= htmlspecialchars($_lt_initials) ?></span>
</div>
<span class="lt-header-user"><?= htmlspecialchars($_lt_displayName, ENT_QUOTES, 'UTF-8') ?></span>
<?php if ($_lt_isAdmin): ?>
<span class="lt-badge lt-badge-admin" aria-label="Administrator">ADMIN</span>
<?php endif; ?>
<?php endif; ?>
<button type="button" class="lt-theme-btn" id="lt-theme-btn"
aria-label="Switch to light mode" title="Switch to light mode">&#x2600;</button>
</div><!-- /.lt-header-right -->
</header><!-- /.lt-header -->
<main class="lt-main lt-container" id="main-content" style="padding-top: calc(var(--header-height, 56px) + var(--space-lg, 1.5rem))">