diff --git a/dashboard/habits.html b/dashboard/habits.html
index e540835..1a236dd 100644
--- a/dashboard/habits.html
+++ b/dashboard/habits.html
@@ -86,6 +86,96 @@
.filter-group {
width: 100%;
}
+
+ /* Mobile touch targets - minimum 44px */
+ .habit-card-action-btn {
+ min-width: 44px;
+ min-height: 44px;
+ }
+
+ .habit-card-check-btn {
+ min-height: 48px;
+ font-size: var(--text-lg);
+ }
+
+ .habit-card-skip-btn {
+ min-height: 44px;
+ padding: var(--space-2) var(--space-3);
+ }
+
+ .modal-close {
+ min-width: 44px;
+ min-height: 44px;
+ }
+
+ /* Stats row 2x2 on mobile */
+ .stats-row {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ /* Icon and color pickers wrap properly */
+ .color-picker-swatches {
+ grid-template-columns: repeat(4, 1fr);
+ }
+
+ .icon-picker-grid {
+ grid-template-columns: repeat(4, 1fr);
+ max-height: 300px;
+ }
+
+ /* Day checkboxes wrap on small screens */
+ .day-checkboxes {
+ grid-template-columns: repeat(4, 1fr);
+ }
+
+ /* Modal padding adjustment */
+ .modal {
+ margin: var(--space-2);
+ }
+
+ .modal-body,
+ .modal-header,
+ .modal-footer {
+ padding: var(--space-3);
+ }
+
+ /* Touch-friendly form elements */
+ .form-input,
+ .form-select,
+ .form-textarea {
+ min-height: 44px;
+ font-size: var(--text-base);
+ }
+
+ /* Larger touch targets for pickers */
+ .color-swatch {
+ min-height: 44px;
+ }
+
+ .icon-option {
+ min-height: 44px;
+ }
+
+ .icon-option svg {
+ width: 24px;
+ height: 24px;
+ }
+
+ .day-checkbox-label {
+ min-height: 44px;
+ padding: var(--space-1);
+ }
+
+ /* Mood and rating buttons */
+ .mood-btn {
+ min-width: 44px;
+ min-height: 44px;
+ font-size: 36px;
+ }
+
+ .rating-star {
+ font-size: 36px;
+ }
}
/* Habits grid */
diff --git a/dashboard/swipe-nav.js b/dashboard/swipe-nav.js
index 728016b..5f0322a 100644
--- a/dashboard/swipe-nav.js
+++ b/dashboard/swipe-nav.js
@@ -3,7 +3,7 @@
* Swipe left/right to navigate between pages
*/
(function() {
- const pages = ['index.html', 'notes.html', 'files.html'];
+ const pages = ['index.html', 'notes.html', 'habits.html', 'files.html', 'workspace.html'];
// Get current page index
function getCurrentIndex() {
diff --git a/dashboard/tests/test_habits_frontend.py b/dashboard/tests/test_habits_frontend.py
index 68fc7e2..b304776 100644
--- a/dashboard/tests/test_habits_frontend.py
+++ b/dashboard/tests/test_habits_frontend.py
@@ -8,6 +8,7 @@ Story US-010: Frontend - Check-in interaction (click and long-press)
Story US-011: Frontend - Skip, lives display, and delete confirmation
Story US-012: Frontend - Filter and sort controls
Story US-013: Frontend - Stats section and weekly summary
+Story US-014: Frontend - Mobile responsive and touch optimization
"""
import sys
@@ -1466,6 +1467,138 @@ def test_typecheck_us013():
assert result == 0, "api.py should pass typecheck (syntax check)"
print("✓ Test 89: Typecheck passes")
+def test_mobile_grid_responsive():
+ """Test 90: Grid shows 1 column on screens below 768px"""
+ habits_path = Path(__file__).parent.parent / 'habits.html'
+ content = habits_path.read_text()
+
+ # Check for mobile breakpoint
+ assert '@media (max-width: 768px)' in content, "Should have mobile breakpoint"
+ assert 'grid-template-columns: 1fr' in content, "Should use 1 column on mobile"
+ print("✓ Test 90: Grid is responsive for mobile (1 column)")
+
+def test_tablet_grid_responsive():
+ """Test 91: Grid shows 2 columns between 768px and 1200px"""
+ habits_path = Path(__file__).parent.parent / 'habits.html'
+ content = habits_path.read_text()
+
+ # Check for tablet breakpoint
+ assert '@media (min-width: 769px) and (max-width: 1200px)' in content, "Should have tablet breakpoint"
+ assert 'repeat(2, 1fr)' in content, "Should use 2 columns on tablet"
+ print("✓ Test 91: Grid shows 2 columns on tablet screens")
+
+def test_touch_targets_44px():
+ """Test 92: All buttons and interactive elements have minimum 44px touch target"""
+ habits_path = Path(__file__).parent.parent / 'habits.html'
+ content = habits_path.read_text()
+
+ # Check for minimum touch target sizes in mobile styles
+ assert 'min-width: 44px' in content, "Should have min-width 44px for touch targets"
+ assert 'min-height: 44px' in content, "Should have min-height 44px for touch targets"
+ assert 'min-height: 48px' in content, "Check-in button should have min-height 48px"
+ print("✓ Test 92: Touch targets meet 44px minimum on mobile")
+
+def test_modals_scrollable_mobile():
+ """Test 93: Modals are scrollable with max-height 90vh on small screens"""
+ habits_path = Path(__file__).parent.parent / 'habits.html'
+ content = habits_path.read_text()
+
+ # Check modal has max-height and overflow
+ assert 'max-height: 90vh' in content, "Modal should have max-height 90vh"
+ assert 'overflow-y: auto' in content, "Modal should have overflow-y auto for scrolling"
+ print("✓ Test 93: Modals are scrollable on small screens")
+
+def test_pickers_wrap_mobile():
+ """Test 94: Icon and color pickers wrap properly on mobile"""
+ habits_path = Path(__file__).parent.parent / 'habits.html'
+ content = habits_path.read_text()
+
+ # Check for mobile grid adjustments in @media query
+ mobile_section = content[content.find('@media (max-width: 768px)'):content.find('@media (max-width: 768px)') + 3000]
+
+ assert 'grid-template-columns: repeat(4, 1fr)' in mobile_section, "Pickers should use 4 columns on mobile"
+ print("✓ Test 94: Icon and color pickers wrap properly on mobile")
+
+def test_filter_bar_stacks_mobile():
+ """Test 95: Filter bar stacks vertically on mobile"""
+ habits_path = Path(__file__).parent.parent / 'habits.html'
+ content = habits_path.read_text()
+
+ # Check for filter bar mobile styles
+ mobile_section = content[content.find('@media (max-width: 768px)'):content.find('@media (max-width: 768px)') + 3000]
+
+ assert '.filter-bar' in mobile_section, "Should have filter-bar mobile styles"
+ assert 'flex-direction: column' in mobile_section, "Filter bar should stack vertically"
+ print("✓ Test 95: Filter bar stacks vertically on mobile")
+
+def test_stats_2x2_mobile():
+ """Test 96: Stats row shows 2x2 grid on mobile"""
+ habits_path = Path(__file__).parent.parent / 'habits.html'
+ content = habits_path.read_text()
+
+ # Check for stats row mobile layout
+ mobile_section = content[content.find('@media (max-width: 768px)'):content.find('@media (max-width: 768px)') + 3000]
+
+ assert '.stats-row' in mobile_section, "Should have stats-row mobile styles"
+ assert 'grid-template-columns: repeat(2, 1fr)' in mobile_section, "Stats should use 2x2 grid on mobile"
+ print("✓ Test 96: Stats row shows 2x2 grid on mobile")
+
+def test_swipe_nav_integration():
+ """Test 97: swipe-nav.js is integrated for page navigation"""
+ habits_path = Path(__file__).parent.parent / 'habits.html'
+ content = habits_path.read_text()
+
+ # Check swipe-nav.js is included
+ assert 'src="/echo/swipe-nav.js"' in content, "Should include swipe-nav.js"
+
+ # Check swipe-nav.js has habits.html in pages array
+ swipe_nav_path = Path(__file__).parent.parent / 'swipe-nav.js'
+ if swipe_nav_path.exists():
+ swipe_content = swipe_nav_path.read_text()
+ assert 'habits.html' in swipe_content, "swipe-nav.js should include habits.html in pages array"
+
+ print("✓ Test 97: swipe-nav.js is integrated")
+
+def test_mobile_form_inputs():
+ """Test 98: Form inputs are touch-friendly on mobile"""
+ habits_path = Path(__file__).parent.parent / 'habits.html'
+ content = habits_path.read_text()
+
+ # Check for form input mobile styles
+ mobile_section = content[content.find('@media (max-width: 768px)'):content.find('@media (max-width: 768px)') + 3000]
+
+ assert '.form-input' in mobile_section or 'min-height: 44px' in mobile_section, "Form inputs should have mobile styles"
+ print("✓ Test 98: Form inputs are touch-friendly on mobile")
+
+def test_mobile_no_console_errors():
+ """Test 99: No obvious console error sources in mobile-specific code"""
+ habits_path = Path(__file__).parent.parent / 'habits.html'
+ content = habits_path.read_text()
+
+ # Basic sanity checks
+ assert '' in content, "Style tags should be closed"
+ print("✓ Test 99: No obvious console error sources")
+
+def test_typecheck_us014():
+ """Test 100: Typecheck passes after US-014 changes"""
+ api_path = Path(__file__).parent.parent / 'api.py'
+ result = os.system(f'python3 -m py_compile {api_path}')
+ assert result == 0, "api.py should pass typecheck"
+ print("✓ Test 100: Typecheck passes")
+
+def test_mobile_day_checkboxes_wrap():
+ """Test 101: Day checkboxes wrap properly on mobile"""
+ habits_path = Path(__file__).parent.parent / 'habits.html'
+ content = habits_path.read_text()
+
+ # Check for day checkboxes mobile styles
+ mobile_section = content[content.find('@media (max-width: 768px)'):content.find('@media (max-width: 768px)') + 3000]
+
+ assert '.day-checkboxes' in mobile_section, "Should have day-checkboxes mobile styles"
+ print("✓ Test 101: Day checkboxes wrap properly on mobile")
+
def run_all_tests():
"""Run all tests in sequence"""
tests = [
@@ -1566,9 +1699,22 @@ def run_all_tests():
test_stats_css_styling,
test_stats_no_console_errors,
test_typecheck_us013,
+ # US-014 tests
+ test_mobile_grid_responsive,
+ test_tablet_grid_responsive,
+ test_touch_targets_44px,
+ test_modals_scrollable_mobile,
+ test_pickers_wrap_mobile,
+ test_filter_bar_stacks_mobile,
+ test_stats_2x2_mobile,
+ test_swipe_nav_integration,
+ test_mobile_form_inputs,
+ test_mobile_no_console_errors,
+ test_typecheck_us014,
+ test_mobile_day_checkboxes_wrap,
]
- print(f"\nRunning {len(tests)} frontend tests for US-006 through US-013...\n")
+ print(f"\nRunning {len(tests)} frontend tests for US-006 through US-014...\n")
failed = []
for test in tests: