diff --git a/index.html b/index.html index 771a22e..234ef73 100644 --- a/index.html +++ b/index.html @@ -613,6 +613,21 @@ html, body { top: 8px; outline: 2px solid #58a6ff; } + /* Safety plan inline status feedback */ + #sp-status { + font-size: 0.85rem; + opacity: 0; + transition: opacity 0.3s ease; + margin-right: auto; + } + #sp-status.success { + color: #3fb950; + opacity: 1; + } + #sp-status.error { + color: #f85149; + opacity: 1; + }
@@ -738,6 +753,7 @@ html, body { @@ -1119,8 +1135,36 @@ Sovereignty and service always.`; overlayDismissBtn.focus(); } - // Register focus trap on document (always listening, gated by class check) + // Crisis overlay Escape key handler + function trapCrisisOverlayEscape(e) { + if (e.key !== 'Escape') return; + if (!crisisOverlay.classList.contains('active')) return; + if (overlayDismissBtn.disabled) return; // Don't escape during countdown + // Dismiss the overlay + crisisOverlay.classList.remove('active'); + if (overlayTimer) { + clearInterval(overlayTimer); + overlayTimer = null; + } + // Re-enable background interaction + var mainApp = document.querySelector('.app'); + if (mainApp) mainApp.removeAttribute('inert'); + var chatSection = document.getElementById('chat'); + if (chatSection) chatSection.removeAttribute('aria-hidden'); + var footerEl = document.querySelector('footer'); + if (footerEl) footerEl.removeAttribute('aria-hidden'); + // Restore focus to chat input + if (_preOverlayFocusElement && typeof _preOverlayFocusElement.focus === 'function') { + _preOverlayFocusElement.focus(); + } else { + msgInput.focus(); + } + _preOverlayFocusElement = null; + } + + // Register focus trap and Escape handler on document (always listening, gated by class check) document.addEventListener('keydown', trapFocusInOverlay); + document.addEventListener('keydown', trapCrisisOverlayEscape); overlayDismissBtn.addEventListener('click', function() { if (!overlayDismissBtn.disabled) { @@ -1270,11 +1314,23 @@ Sovereignty and service always.`; }; try { localStorage.setItem('timmy_safety_plan', JSON.stringify(plan)); - safetyPlanModal.classList.remove('active'); - _restoreSafetyPlanFocus(); - alert('Safety plan saved locally.'); + var spStatus = document.getElementById('sp-status'); + spStatus.textContent = '\u2713 Safety plan saved locally.'; + spStatus.className = 'success'; + setTimeout(function() { + spStatus.className = ''; + spStatus.textContent = ''; + safetyPlanModal.classList.remove('active'); + _restoreSafetyPlanFocus(); + }, 2000); } catch (e) { - alert('Error saving plan.'); + var spStatusErr = document.getElementById('sp-status'); + spStatusErr.textContent = '\u2717 Error saving plan.'; + spStatusErr.className = 'error'; + setTimeout(function() { + spStatusErr.className = ''; + spStatusErr.textContent = ''; + }, 4000); } }); diff --git a/tests/test_safety_plan_inline_feedback.py b/tests/test_safety_plan_inline_feedback.py new file mode 100644 index 0000000..09b4caa --- /dev/null +++ b/tests/test_safety_plan_inline_feedback.py @@ -0,0 +1,74 @@ +import pathlib +import re +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +INDEX_HTML = ROOT / 'index.html' + + +class TestSafetyPlanInlineFeedback(unittest.TestCase): + """Test that safety plan uses inline feedback instead of blocking alert().""" + + @classmethod + def setUpClass(cls): + cls.html = INDEX_HTML.read_text() + + def test_no_alert_calls(self): + """Safety plan save must not use browser alert().""" + alert_matches = re.findall(r'alert\(', self.html) + self.assertEqual( + len(alert_matches), 0, + f'Found {len(alert_matches)} alert() calls - must use inline feedback instead.', + ) + + def test_sp_status_element_exists(self): + """Modal footer must contain #sp-status element for inline feedback.""" + self.assertRegex( + self.html, + r'id=["\']sp-status["\']', + 'Expected #sp-status element in the safety plan modal.', + ) + + def test_sp_status_has_aria_live(self): + """#sp-status must have aria-live for accessible announcements.""" + self.assertRegex( + self.html, + r'aria-live=["\']polite["\']', + 'Expected #sp-status to have aria-live="polite".', + ) + + def test_success_feedback_exists(self): + """Must show success message on save.""" + self.assertIn( + 'Safety plan saved locally.', + self.html, + 'Expected success message for safety plan save.', + ) + + def test_error_feedback_exists(self): + """Must show error message on save failure.""" + self.assertIn( + 'Error saving plan.', + self.html, + 'Expected error message for safety plan save failure.', + ) + + def test_css_success_state(self): + """Must have CSS for .sp-status.success state.""" + self.assertIn( + 'sp-status.success', + self.html, + 'Expected CSS for .sp-status.success state.', + ) + + def test_css_error_state(self): + """Must have CSS for .sp-status.error state.""" + self.assertIn( + 'sp-status.error', + self.html, + 'Expected CSS for .sp-status.error state.', + ) + + +if __name__ == '__main__': + unittest.main()