Files
tinker_tickets/views/layout_footer.php
T
jared 2e450dc01d Apply web_template gap analysis improvements (P1-P3)
P1-A: Fix CSP - add fonts.googleapis.com to style-src, fonts.gstatic.com to font-src
P1-B: CSRF token rotation - add rotateToken() to CsrfMiddleware; bootstrap.php rotates
      after successful validation and stores in $GLOBALS['_new_csrf_token']; add
      apiRespond() helper to append token to responses; lt.api interceptor in
      layout_footer.php auto-updates window.CSRF_TOKEN from responses
P1-C: Styled 403/404 error views with TDS layout instead of raw text; index.php now
      uses requireAdmin() helper eliminating 7 duplicated guard blocks (P3-D)
P2-A: Remove duplicate JS-generated keyboard help modal from keyboard-shortcuts.js;
      '?' key now routes to static #lt-keys-help modal in footer
P2-B: Asset versioning driven by config ASSET_VERSION key; base.css and base.js get
      ?v= cache-busting in layout_header.php
P2-C: Add data-theme="dark" to <html> tag to prevent FOUC on light-mode users
P2-E: Escape status value in dashboard.js hover preview class attribute via lt.escHtml()
P2-F: Replace bespoke showLoadingOverlay() with lt-spinner / lt-loading-text from
      base.css; add .lt-loading-overlay wrapper CSS to dashboard.css
P2-G: Add keyboard-shortcuts.js to all 7 admin views so J/K nav and ? help work
P3-A: APP_NAME, APP_SUBTITLE, APP_VERSION driven from config.php; layout header/footer
      use config values instead of hardcoded strings
P3-G: Replace custom initTableSorting() with lt.sortTable.init() which manages aria-sort

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 17:02:40 -04:00

191 lines
9.3 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* layout_footer.php — Shared bottom-of-page partial for all views.
*
* Expected variables available from the including view (set before require):
* string $nonce CSP nonce from SecurityHeadersMiddleware::getNonce()
* array|null $pageScripts Optional array of extra JS paths to load after base.js
* string|null $pageInlineScript Optional raw JS string to run after all scripts load
*
* Globals used:
* $GLOBALS['currentUser'] — user array (user_id, username, is_admin)
* $GLOBALS['config'] — app config array (TIMEZONE, TIMEZONE_ABBREV)
* CsrfMiddleware::getToken() — returns current CSRF token string
*/
// layout_footer.php — JS globals + runtime scripts are loaded here
?>
</main><!-- /#main-content / .lt-main -->
<!-- ================================================================
FOOTER — keyboard hint bar + version
================================================================ -->
<?php
// Context-sensitive keyboard hints based on active nav
$_ltf_nav = $activeNav ?? 'dashboard';
$_ltf_isTicket = str_starts_with($pageTitle ?? '', 'Ticket #');
?>
<footer class="lt-footer" role="contentinfo" aria-label="Keyboard shortcuts and app info">
<nav class="lt-footer-hints" aria-label="Keyboard shortcuts">
<?php if ($_ltf_isTicket): ?>
<a href="/" class="lt-footer-hint" title="Go to dashboard"><span class="lt-footer-key">[ ← ]</span> BACK</a>
<span class="lt-footer-sep">|</span>
<span class="lt-footer-hint" title="Press 14 to change status"><span class="lt-footer-key">[ 1-4 ]</span> STATUS</span>
<span class="lt-footer-sep">|</span>
<span class="lt-footer-hint" title="Press C to jump to comment box"><span class="lt-footer-key">[ C ]</span> COMMENT</span>
<span class="lt-footer-sep">|</span>
<button type="button" class="lt-footer-hint" data-action="open-settings" title="Open settings"><span class="lt-footer-key">[ * ]</span> CFG</button>
<?php elseif (str_starts_with($_ltf_nav, 'admin')): ?>
<a href="/" class="lt-footer-hint" title="Go to dashboard"><span class="lt-footer-key">[ ~ ]</span> HOME</a>
<span class="lt-footer-sep">|</span>
<button type="button" class="lt-footer-hint" data-action="open-settings" title="Open settings"><span class="lt-footer-key">[ * ]</span> CFG</button>
<?php else: ?>
<a href="/" class="lt-footer-hint" title="Go to dashboard (G then D)"><span class="lt-footer-key">[ ~ ]</span> HOME</a>
<span class="lt-footer-sep">|</span>
<span class="lt-footer-hint" title="Press / or Ctrl+K to search"><span class="lt-footer-key">[ / ]</span> SEARCH</span>
<span class="lt-footer-sep">|</span>
<a href="/ticket/create" class="lt-footer-hint" title="Create new ticket (N)"><span class="lt-footer-key">[ + ]</span> NEW</a>
<span class="lt-footer-sep">|</span>
<button type="button" class="lt-footer-hint" data-action="open-settings" title="Open settings"><span class="lt-footer-key">[ * ]</span> CFG</button>
<?php endif ?>
<span class="lt-footer-sep">|</span>
<button type="button" class="lt-footer-hint" data-action="show-keyboard-help" title="Show keyboard shortcuts (?)"><span class="lt-footer-key">[ ? ]</span> HELP</button>
</nav>
<span aria-label="Application version"><?= htmlspecialchars($GLOBALS['config']['APP_NAME'] ?? 'TINKER TICKETS', ENT_QUOTES, 'UTF-8') ?> &mdash; TDS v<?= htmlspecialchars($GLOBALS['config']['APP_VERSION'] ?? '1.2', ENT_QUOTES, 'UTF-8') ?></span>
</footer>
<!-- ================================================================
KEYBOARD SHORTCUTS HELP MODAL — opened by ? key or footer [?] hint
================================================================ -->
<div id="lt-keys-help" class="lt-modal-overlay" aria-hidden="true">
<div class="lt-modal" role="dialog" aria-modal="true" aria-labelledby="keys-help-title">
<div class="lt-modal-header">
<span class="lt-modal-title" id="keys-help-title">Keyboard Shortcuts</span>
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close">&#x2715;</button>
</div>
<div class="lt-modal-body">
<table class="lt-data-table" style="width:100%">
<thead>
<tr><th scope="col">Shortcut</th><th scope="col">Action</th></tr>
</thead>
<tbody>
<tr><td>Ctrl / &#x2318; + K</td><td>Focus search box</td></tr>
<tr><td>Ctrl / &#x2318; + E</td><td>Toggle edit mode (ticket page)</td></tr>
<tr><td>Ctrl / &#x2318; + S</td><td>Save changes (ticket page)</td></tr>
<tr><td>j / &#x2193;</td><td>Select next row</td></tr>
<tr><td>k / &#x2191;</td><td>Select previous row</td></tr>
<tr><td>Enter</td><td>Open selected ticket</td></tr>
<tr><td>n</td><td>New ticket</td></tr>
<tr><td>1&ndash;4</td><td>Change ticket status (ticket page)</td></tr>
<tr><td>c</td><td>Jump to comment box (ticket page)</td></tr>
<tr><td>?</td><td>Show this help</td></tr>
<tr><td>ESC</td><td>Close modal / cancel</td></tr>
</tbody>
</table>
</div>
<div class="lt-modal-footer">
<button type="button" class="lt-btn" data-modal-close>Close</button>
</div>
</div>
</div>
<!-- ================================================================
COMMAND PALETTE — Ctrl+K opens when no search input focused
================================================================ -->
<div id="lt-cmd-overlay" class="lt-cmd-overlay" role="dialog" aria-modal="true" aria-label="Command palette" aria-hidden="true">
<div class="lt-cmd-palette" id="lt-cmd-palette">
<div class="lt-cmd-input-wrap">
<span class="lt-cmd-prompt">&gt;</span>
<input id="lt-cmd-input" class="lt-cmd-input" type="text"
placeholder="Search commands&hellip;" autocomplete="off"
spellcheck="false" aria-label="Search commands">
</div>
<div class="lt-cmd-results" id="lt-cmd-results">
<div class="lt-cmd-empty">Start typing to search&hellip;</div>
</div>
<div class="lt-cmd-footer">
<span><kbd>&#x2191;</kbd><kbd>&#x2193;</kbd> Navigate</span>
<span><kbd>Enter</kbd> Select</span>
<span><kbd>Esc</kbd> Close</span>
</div>
</div>
</div>
<!-- base.js + utils.js + globals already loaded in <head> via layout_header.php -->
<?php if (!empty($pageScripts)): ?>
<!-- PAGE-SPECIFIC SCRIPTS -->
<?php foreach ($pageScripts as $_ltf_script): ?>
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>" src="<?= htmlspecialchars($_ltf_script, ENT_QUOTES, 'UTF-8') ?>"></script>
<?php endforeach; ?>
<?php endif; ?>
<?php if (!empty($pageInlineScript)): ?>
<!-- PAGE INLINE SCRIPT -->
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>">
<?= $pageInlineScript ?>
</script>
<?php endif; ?>
<!-- LT INIT — boot animation + global UI init (base.js handles keys/nav automatically) -->
<script nonce="<?= htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') ?>">
if (window.lt) {
lt.init({ bootName: <?= json_encode($GLOBALS['config']['APP_NAME'] ?? 'TINKER TICKETS', JSON_HEX_TAG) ?> });
// Theme toggle button
var themeBtn = document.getElementById('lt-theme-btn');
if (themeBtn) themeBtn.addEventListener('click', function() { lt.theme.toggle(); });
// Command palette — global navigation commands available on all pages
lt.cmdPalette.init([
{
group: 'Navigation',
items: [
{ icon: '~', label: 'Dashboard', kbd: 'G D', action: function() { window.location.href = '/'; } },
{ icon: '+', label: 'New Ticket', kbd: 'N', action: function() { window.location.href = '/ticket/create'; } },
]
},
{
group: 'Help',
items: [
{ icon: '?', label: 'Keyboard Shortcuts', kbd: '?', action: function() { lt.modal.open('lt-keys-help'); } },
]
},
]);
}
// Patch lt.api mutating methods to auto-rotate CSRF token when server returns a new one
if (window.lt && lt.api) {
['post', 'put', 'patch', 'delete'].forEach(function(method) {
if (typeof lt.api[method] !== 'function') return;
var _orig = lt.api[method];
lt.api[method] = function(url, body) {
return _orig.call(lt.api, url, body).then(function(data) {
if (data && data.csrf_token) window.CSRF_TOKEN = data.csrf_token;
return data;
});
};
});
}
// Footer hint bar actions (keyboard help + settings — work on all pages)
document.addEventListener('click', function(e) {
var btn = e.target.closest('[data-action]');
if (!btn) return;
var action = btn.getAttribute('data-action');
if (action === 'show-keyboard-help') {
if (window.lt) lt.modal.open('lt-keys-help');
} else if (action === 'open-settings' || action === 'open-settings-modal') {
if (typeof openSettingsModal === 'function') {
openSettingsModal();
} else if (window.lt) {
lt.toast.info('Settings available on the Dashboard');
}
}
});
</script>
</body>
</html>