diff --git a/index.html b/index.html index 06cff1c..5686b46 100644 --- a/index.html +++ b/index.html @@ -423,6 +423,35 @@ html, body { fill: currentColor; } +/* Chat safety plan button — always visible, subtle */ +#chat-safety-plan-btn { + flex-shrink: 0; + width: 44px; + height: 44px; + background: transparent; + color: #8b949e; + border: 1px solid #30363d; + border-radius: 12px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s, color 0.2s, border-color 0.2s; + -webkit-appearance: none; +} +#chat-safety-plan-btn:hover, +#chat-safety-plan-btn:focus { + background: #161b22; + color: #58a6ff; + border-color: #58a6ff; + outline: 2px solid #58a6ff; + outline-offset: 2px; +} +#chat-safety-plan-btn svg { + width: 20px; + height: 20px; +} + /* ===== MODALS ===== */ .modal-overlay { position: fixed; @@ -675,6 +704,9 @@ html, body { + @@ -813,6 +845,7 @@ Sovereignty and service always.`; // Safety Plan Elements var safetyPlanBtn = document.getElementById('safety-plan-btn'); + var chatSafetyPlanBtn = document.getElementById('chat-safety-plan-btn'); var crisisSafetyPlanBtn = document.getElementById('crisis-safety-plan-btn'); var safetyPlanModal = document.getElementById('safety-plan-modal'); var closeSafetyPlan = document.getElementById('close-safety-plan'); @@ -1290,6 +1323,15 @@ Sovereignty and service always.`; _activateSafetyPlanFocusTrap(safetyPlanBtn); }); + // Chat input area safety plan button — always visible (#38) + if (chatSafetyPlanBtn) { + chatSafetyPlanBtn.addEventListener('click', function() { + loadSafetyPlan(); + safetyPlanModal.classList.add('active'); + _activateSafetyPlanFocusTrap(chatSafetyPlanBtn); + }); + } + // Crisis panel safety plan button (if crisis panel is visible) if (crisisSafetyPlanBtn) { crisisSafetyPlanBtn.addEventListener('click', function() { diff --git a/tests/test_safety_plan_in_chat.py b/tests/test_safety_plan_in_chat.py new file mode 100644 index 0000000..63211dd --- /dev/null +++ b/tests/test_safety_plan_in_chat.py @@ -0,0 +1,102 @@ +""" +Tests for #38 — Safety plan accessible from chat (not just overlay). + +Verifies: +1. Safety plan button exists in the input area +2. Button has proper ARIA attributes +3. Button is keyboard focusable +4. Button does not require crisis detection to be visible +""" + +import re +import unittest +from pathlib import Path + + +INDEX_HTML = Path(__file__).parent.parent / "index.html" + + +class TestSafetyPlanInChat(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.html = INDEX_HTML.read_text() + + def test_chat_safety_plan_button_exists(self): + """Button #chat-safety-plan-btn exists in the DOM.""" + self.assertIn('id="chat-safety-plan-btn"', self.html) + + def test_button_has_aria_label(self): + """Button has aria-label for screen readers.""" + match = re.search( + r']*id="chat-safety-plan-btn"[^>]*aria-label="([^"]*)"', + self.html + ) + self.assertIsNotNone(match, "chat-safety-plan-btn missing aria-label") + self.assertIn("safety", match.group(1).lower()) + + def test_button_has_title(self): + """Button has title attribute for tooltip.""" + self.assertRegex( + self.html, + r']*id="chat-safety-plan-btn"[^>]*title="[^"]*"[^>]*>' + ) + + def test_button_is_in_input_area(self): + """Button is inside #input-area, not in crisis overlay.""" + input_area = re.search( + r'
(.*?)
\s*', + self.html, re.DOTALL + ) + self.assertIsNotNone(input_area) + self.assertIn('chat-safety-plan-btn', input_area.group(1)) + + def test_button_not_in_crisis_overlay(self): + """Button is NOT inside #crisis-overlay (always visible, no detection).""" + overlay = re.search( + r'
\s*
', + self.html, re.DOTALL + ) + if overlay: + self.assertNotIn('chat-safety-plan-btn', overlay.group(0)) + + def test_button_has_shield_icon(self): + """Button includes a shield SVG icon.""" + btn_match = re.search( + r']*id="chat-safety-plan-btn"[^>]*>(.*?)', + self.html, re.DOTALL + ) + self.assertIsNotNone(btn_match) + self.assertIn('svg', btn_match.group(1).lower()) + # Shield path + self.assertIn('M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z', btn_match.group(1)) + + def test_css_exists_for_button(self): + """CSS rules exist for #chat-safety-plan-btn.""" + self.assertIn('#chat-safety-plan-btn', self.html) + # Check for hover/focus styles + self.assertIn('#chat-safety-plan-btn:hover', self.html) + self.assertIn('#chat-safety-plan-btn:focus', self.html) + + def test_javascript_listener_exists(self): + """JavaScript event listener exists for the button.""" + self.assertIn('chatSafetyPlanBtn', self.html) + self.assertIn("chatSafetyPlanBtn.addEventListener('click'", self.html) + + def test_javascript_calls_load_safety_plan(self): + """Click handler calls loadSafetyPlan() and shows modal.""" + listener = re.search( + r'chatSafetyPlanBtn\.addEventListener.*?\{(.*?)\}', + self.html, re.DOTALL + ) + self.assertIsNotNone(listener) + body = listener.group(1) + self.assertIn('loadSafetyPlan()', body) + self.assertIn("safetyPlanModal.classList.add('active')", body) + + def test_focus_trap_uses_button_as_return_target(self): + """Focus trap returns focus to chatSafetyPlanBtn when modal closes.""" + self.assertIn('_activateSafetyPlanFocusTrap(chatSafetyPlanBtn)', self.html) + + +if __name__ == "__main__": + unittest.main()