Files
tinker_tickets/views/layout_header.php
T
jared 4150e1ced3 Fix lt-scanlines header overlap — move class off body to dedicated div
body::before and body::after are used for background grid/gradient effects.
Adding lt-scanlines to body caused ::after conflict (higher specificity) and
put the scanline overlay at z-index 9998, above the header at z-index 300.

Move lt-scanlines to a dedicated fixed div so pseudo-elements don't conflict
and the header remains fully visible.

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

188 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>
<!-- Scanlines overlay — dedicated div avoids conflict with body::before/::after pseudo-elements -->
<div class="lt-scanlines" aria-hidden="true" style="position:fixed;inset:0;pointer-events:none;z-index:9998"></div>
<!-- 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">