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>
This commit is contained in:
@@ -323,3 +323,24 @@ kbd {
|
||||
@media (max-width: 480px) {
|
||||
.lt-stats-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* Loading overlay — wraps lt-spinner for element-level loading states */
|
||||
.has-lt-overlay { position: relative; }
|
||||
.lt-loading-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
background: rgba(var(--bg-primary-rgb, 10,10,10), 0.75);
|
||||
z-index: 10;
|
||||
border-radius: inherit;
|
||||
}
|
||||
.lt-loading-text {
|
||||
font-size: 0.72rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
+25
-24
@@ -288,13 +288,11 @@ function clearAllFilters() {
|
||||
}
|
||||
|
||||
function initTableSorting() {
|
||||
const tableHeaders = document.querySelectorAll('th');
|
||||
tableHeaders.forEach((header, index) => {
|
||||
header.addEventListener('click', () => {
|
||||
const table = header.closest('table');
|
||||
sortTable(table, index);
|
||||
});
|
||||
});
|
||||
// Use the TDS lt.sortTable helper which manages aria-sort attributes correctly.
|
||||
// Falls back to no-op if the table isn't present on this page.
|
||||
if (window.lt && lt.sortTable && document.getElementById('tickets-table')) {
|
||||
lt.sortTable.init('tickets-table');
|
||||
}
|
||||
}
|
||||
|
||||
function initSidebarFilters() {
|
||||
@@ -1230,7 +1228,7 @@ function showTicketPreview(event) {
|
||||
currentPreview.innerHTML = `
|
||||
<div class="preview-header">
|
||||
<span class="preview-id">#${lt.escHtml(ticketId)}</span>
|
||||
<span class="preview-status status-${status.replace(/\s+/g, '-')}">${lt.escHtml(status)}</span>
|
||||
<span class="preview-status status-${lt.escHtml(status.replace(/\s+/g, '-'))}">${lt.escHtml(status)}</span>
|
||||
</div>
|
||||
<div class="preview-title">${lt.escHtml(title)}</div>
|
||||
<div class="preview-meta">
|
||||
@@ -1330,34 +1328,37 @@ function exportSelectedTickets(format) {
|
||||
|
||||
|
||||
/**
|
||||
* Show loading overlay on element
|
||||
* Show TDS spinner overlay on an element.
|
||||
* Uses lt-spinner + lt-loading-text from base.css.
|
||||
*/
|
||||
function showLoadingOverlay(element, message = 'Loading...') {
|
||||
// Remove existing overlay
|
||||
const existing = element.querySelector('.loading-overlay');
|
||||
const existing = element.querySelector('.lt-loading-overlay');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'loading-overlay';
|
||||
overlay.innerHTML = `
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-text">${message}</div>
|
||||
`;
|
||||
element.classList.add('has-overlay');
|
||||
overlay.className = 'lt-loading-overlay';
|
||||
|
||||
const spinner = document.createElement('div');
|
||||
spinner.className = 'lt-spinner';
|
||||
|
||||
const text = document.createElement('div');
|
||||
text.className = 'lt-loading-text';
|
||||
text.textContent = message;
|
||||
|
||||
overlay.appendChild(spinner);
|
||||
overlay.appendChild(text);
|
||||
element.classList.add('has-lt-overlay');
|
||||
element.appendChild(overlay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide loading overlay
|
||||
* Hide TDS spinner overlay
|
||||
*/
|
||||
function hideLoadingOverlay(element) {
|
||||
const overlay = element.querySelector('.loading-overlay');
|
||||
const overlay = element.querySelector('.lt-loading-overlay');
|
||||
if (overlay) {
|
||||
overlay.classList.add('loading-overlay--hiding');
|
||||
setTimeout(() => {
|
||||
overlay.remove();
|
||||
element.classList.remove('has-overlay');
|
||||
}, 300);
|
||||
overlay.remove();
|
||||
element.classList.remove('has-lt-overlay');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,60 +26,6 @@ function navigateTableRow(direction) {
|
||||
}
|
||||
}
|
||||
|
||||
function showKeyboardHelp() {
|
||||
if (document.getElementById('keyboardHelpModal')) return;
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'keyboardHelpModal';
|
||||
modal.className = 'lt-modal-overlay';
|
||||
modal.setAttribute('aria-hidden', 'true');
|
||||
modal.setAttribute('role', 'dialog');
|
||||
modal.setAttribute('aria-modal', 'true');
|
||||
modal.setAttribute('aria-labelledby', 'keyboardHelpModalTitle');
|
||||
modal.innerHTML = `
|
||||
<div class="lt-modal lt-modal-sm">
|
||||
<div class="lt-modal-header">
|
||||
<span class="lt-modal-title" id="keyboardHelpModalTitle">KEYBOARD SHORTCUTS</span>
|
||||
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||
</div>
|
||||
<div class="lt-modal-body">
|
||||
<h4 class="kb-section-heading">Navigation</h4>
|
||||
<table class="kb-shortcuts-table">
|
||||
<tr><td><kbd>J</kbd></td><td>Next ticket in list</td></tr>
|
||||
<tr><td><kbd>K</kbd></td><td>Previous ticket in list</td></tr>
|
||||
<tr><td><kbd>Enter</kbd></td><td>Open selected ticket</td></tr>
|
||||
<tr><td><kbd>G</kbd> then <kbd>D</kbd></td><td>Go to Dashboard</td></tr>
|
||||
</table>
|
||||
<h4 class="kb-section-heading">Actions</h4>
|
||||
<table class="kb-shortcuts-table">
|
||||
<tr><td><kbd>N</kbd></td><td>New ticket</td></tr>
|
||||
<tr><td><kbd>C</kbd></td><td>Focus comment box</td></tr>
|
||||
<tr><td><kbd>Ctrl/Cmd+E</kbd></td><td>Toggle Edit Mode</td></tr>
|
||||
<tr><td><kbd>Ctrl/Cmd+S</kbd></td><td>Save Changes</td></tr>
|
||||
</table>
|
||||
<h4 class="kb-section-heading">Quick Status (Ticket Page)</h4>
|
||||
<table class="kb-shortcuts-table">
|
||||
<tr><td><kbd>1</kbd></td><td>Set Open</td></tr>
|
||||
<tr><td><kbd>2</kbd></td><td>Set Pending</td></tr>
|
||||
<tr><td><kbd>3</kbd></td><td>Set In Progress</td></tr>
|
||||
<tr><td><kbd>4</kbd></td><td>Set Closed</td></tr>
|
||||
</table>
|
||||
<h4 class="kb-section-heading">Other</h4>
|
||||
<table class="kb-shortcuts-table no-margin">
|
||||
<tr><td><kbd>Ctrl/Cmd+K</kbd></td><td>Focus Search</td></tr>
|
||||
<tr><td><kbd>ESC</kbd></td><td>Close Modal / Cancel</td></tr>
|
||||
<tr><td><kbd>?</kbd></td><td>Show This Help</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="lt-modal-footer">
|
||||
<button class="lt-btn lt-btn-ghost" data-modal-close>CLOSE</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
lt.modal.open('keyboardHelpModal');
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!window.lt) return;
|
||||
|
||||
@@ -101,9 +47,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
|
||||
// ?: Show keyboard shortcuts help (lt.keys.initDefaults also handles this, but we override to show our modal)
|
||||
// ?: Show keyboard shortcuts help — use the static #lt-keys-help modal in the footer
|
||||
lt.keys.on('?', function() {
|
||||
showKeyboardHelp();
|
||||
if (window.lt) lt.modal.open('lt-keys-help');
|
||||
});
|
||||
|
||||
// J: Next row
|
||||
|
||||
Reference in New Issue
Block a user