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: