06/04 Optimize app

This commit is contained in:
2026-06-04 15:00:09 -04:00
parent 3f2e83ce2d
commit e03eb26417
8 changed files with 125 additions and 23 deletions
+61 -1
View File
@@ -32,13 +32,30 @@ document.addEventListener('click', (e) => {
}
});
// Close modal on Escape key
// Close modal on Escape; trap Tab focus inside open modals
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
document.querySelectorAll('.modal-overlay.open').forEach(m => {
m.classList.remove('open');
document.body.style.overflow = '';
});
return;
}
if (e.key === 'Tab') {
const openModal = document.querySelector('.modal-overlay.open');
if (!openModal) return;
const focusable = Array.from(openModal.querySelectorAll(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)).filter(el => el.offsetParent !== null);
if (focusable.length === 0) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey) {
if (document.activeElement === first) { e.preventDefault(); last.focus(); }
} else {
if (document.activeElement === last) { e.preventDefault(); first.focus(); }
}
}
});
@@ -255,11 +272,15 @@ function getCsrfToken() {
sidebar.classList.add('open');
backdrop.classList.add('open');
document.body.style.overflow = 'hidden';
toggle.setAttribute('aria-expanded', 'true');
toggle.setAttribute('aria-label', 'Close navigation');
}
function closeSidebar() {
sidebar.classList.remove('open');
backdrop.classList.remove('open');
document.body.style.overflow = '';
toggle.setAttribute('aria-expanded', 'false');
toggle.setAttribute('aria-label', 'Open navigation');
}
toggle.addEventListener('click', openSidebar);
@@ -290,6 +311,45 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
/* ── Password strength meter ───────────────────────────────── */
function _pwStrengthLevel(pw) {
var score = 0;
if (pw.length >= 8) score++;
if (pw.length >= 12) score++;
if (/[A-Z]/.test(pw)) score++;
if (/[0-9]/.test(pw)) score++;
if (/[^A-Za-z0-9]/.test(pw)) score++;
var map = ['pw-weak','pw-weak','pw-fair','pw-strong','pw-great','pw-great'];
var lbl = ['Weak','Weak','Fair','Strong','Very strong','Very strong'];
return { cls: map[score], label: lbl[score] };
}
function attachPasswordStrength(inputId, fillId, labelId) {
var input = document.getElementById(inputId);
var fill = document.getElementById(fillId);
var lbl = document.getElementById(labelId);
if (!input || !fill || !lbl) return;
input.addEventListener('input', function() {
if (!this.value) {
fill.className = 'pw-strength-fill';
lbl.className = 'pw-strength-label';
lbl.textContent = '';
return;
}
var r = _pwStrengthLevel(this.value);
fill.className = 'pw-strength-fill ' + r.cls;
lbl.className = 'pw-strength-label ' + r.cls;
lbl.textContent = r.label;
});
}
/* ── HTML escape (safe for attributes and text nodes) ──────── */
function esc(str) {
return String(str || '').replace(/[&<>"']/g, function(c) {
return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c];
});
}
/* ── Generic fetch-based form submit (JSON response) ───────── */
async function submitJson(url, data, method = 'POST') {
const res = await fetch(url, {