From c1d4ed1b032038459e5698dac47d769b1c7ab148 Mon Sep 17 00:00:00 2001 From: Echo Date: Tue, 10 Feb 2026 12:49:11 +0000 Subject: [PATCH] feat: 12.0 - Frontend - Habit card styling --- dashboard/habits.html | 72 +++++++- dashboard/test_habits_card_styling.py | 256 ++++++++++++++++++++++++++ 2 files changed, 323 insertions(+), 5 deletions(-) create mode 100644 dashboard/test_habits_card_styling.py diff --git a/dashboard/habits.html b/dashboard/habits.html index 4b5289e..65e93ef 100644 --- a/dashboard/habits.html +++ b/dashboard/habits.html @@ -87,7 +87,7 @@ .habit-card { background: var(--bg-surface); border: 1px solid var(--border); - border-radius: var(--radius-md); + border-radius: var(--radius-lg); padding: var(--space-4); display: flex; align-items: center; @@ -100,6 +100,10 @@ transform: translateY(-1px); } + .habit-card.checked { + background: rgba(34, 197, 94, 0.1); + } + .habit-icon { width: 40px; height: 40px; @@ -129,8 +133,13 @@ } .habit-frequency { + display: inline-block; font-size: var(--text-xs); - color: var(--text-muted); + color: var(--text-secondary); + background: var(--bg-elevated); + border: 1px solid var(--border); + padding: 2px 8px; + border-radius: var(--radius-sm); text-transform: capitalize; } @@ -138,7 +147,7 @@ display: flex; align-items: center; gap: var(--space-1); - font-size: var(--text-lg); + font-size: var(--text-xl); font-weight: 600; color: var(--accent); flex-shrink: 0; @@ -162,6 +171,12 @@ .habit-checkbox:hover:not(.disabled) { border-color: var(--accent); background: var(--accent-light, rgba(99, 102, 241, 0.1)); + animation: pulse 1.5s ease-in-out infinite; + } + + @keyframes pulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.05); } } .habit-checkbox.checked { @@ -343,6 +358,41 @@ transform: translateX(-50%) translateY(0); opacity: 1; } + + /* Mobile responsiveness */ + @media (max-width: 768px) { + .main { + padding: var(--space-3); + } + + .habit-card { + width: 100%; + } + + .habits-list { + width: 100%; + } + + .habit-icon { + width: 36px; + height: 36px; + } + + .habit-icon svg { + width: 18px; + height: 18px; + } + + .habit-checkbox { + width: 28px; + height: 28px; + } + + .habit-checkbox svg { + width: 16px; + height: 16px; + } + } @@ -627,13 +677,13 @@ // Create habit card element function createHabitCard(habit) { const card = document.createElement('div'); - card.className = 'habit-card'; + const isChecked = habit.checkedToday || false; + card.className = isChecked ? 'habit-card checked' : 'habit-card'; // Determine icon based on frequency const iconName = habit.frequency === 'daily' ? 'calendar' : 'clock'; // Checkbox state - const isChecked = habit.checkedToday || false; const checkboxClass = isChecked ? 'habit-checkbox checked disabled' : 'habit-checkbox'; const checkIcon = isChecked ? '' : ''; @@ -677,6 +727,12 @@ checkboxElement.innerHTML = ''; lucide.createIcons(); + // Add 'checked' class to parent card for green background + const card = checkboxElement.closest('.habit-card'); + if (card) { + card.classList.add('checked'); + } + // Store original state for rollback const originalCheckbox = checkboxElement.cloneNode(true); const streakElement = document.getElementById(`streak-${habitId}`); @@ -708,6 +764,12 @@ checkboxElement.classList.remove('checked', 'disabled'); checkboxElement.innerHTML = ''; + // Revert card background + const card = checkboxElement.closest('.habit-card'); + if (card) { + card.classList.remove('checked'); + } + // Revert streak if (streakElement) { streakElement.textContent = originalStreak; diff --git a/dashboard/test_habits_card_styling.py b/dashboard/test_habits_card_styling.py new file mode 100644 index 0000000..6e463a3 --- /dev/null +++ b/dashboard/test_habits_card_styling.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +""" +Tests for Story 12.0: Frontend - Habit card styling +Tests styling enhancements for habit cards +""" + +def test_file_exists(): + """Test that habits.html exists""" + import os + assert os.path.exists('dashboard/habits.html'), "habits.html file should exist" + print("✓ habits.html exists") + +def test_card_border_radius(): + """Test that cards use --radius-lg border radius""" + with open('dashboard/habits.html', 'r') as f: + content = f.read() + + # Check that habit-card has border-radius: var(--radius-lg) + assert 'border-radius: var(--radius-lg);' in content, "habit-card should use --radius-lg border radius" + + # Check it's in the .habit-card CSS rule + habit_card_start = content.find('.habit-card {') + habit_card_end = content.find('}', habit_card_start) + habit_card_css = content[habit_card_start:habit_card_end] + assert 'border-radius: var(--radius-lg)' in habit_card_css, "habit-card should have --radius-lg in its CSS" + + print("✓ Cards use --radius-lg border radius") + +def test_streak_font_size(): + """Test that streak uses --text-xl font size""" + with open('dashboard/habits.html', 'r') as f: + content = f.read() + + # Find .habit-streak CSS rule + streak_start = content.find('.habit-streak {') + assert streak_start > 0, ".habit-streak CSS rule should exist" + + streak_end = content.find('}', streak_start) + streak_css = content[streak_start:streak_end] + + # Check for font-size: var(--text-xl) + assert 'font-size: var(--text-xl)' in streak_css, "Streak should use --text-xl font size" + + print("✓ Streak displayed prominently with --text-xl font size") + +def test_checked_habit_background(): + """Test that checked habits have subtle green background tint""" + with open('dashboard/habits.html', 'r') as f: + content = f.read() + + # Check for .habit-card.checked CSS rule + assert '.habit-card.checked' in content, "Should have .habit-card.checked CSS rule" + + # Find the CSS rule + checked_start = content.find('.habit-card.checked {') + assert checked_start > 0, ".habit-card.checked CSS rule should exist" + + checked_end = content.find('}', checked_start) + checked_css = content[checked_start:checked_end] + + # Check for green background (using rgba with green color and low opacity) + assert 'background: rgba(34, 197, 94, 0.1)' in checked_css, "Checked cards should have green background tint" + + # Check JavaScript adds 'checked' class to card + assert "card.classList.add('checked')" in content, "JavaScript should add 'checked' class to card" + + print("✓ Checked habits have subtle green background tint") + +def test_checkbox_pulse_animation(): + """Test that unchecked habits have subtle pulse animation on checkbox hover""" + with open('dashboard/habits.html', 'r') as f: + content = f.read() + + # Check for animation on hover (not disabled) + hover_start = content.find('.habit-checkbox:hover:not(.disabled) {') + assert hover_start > 0, "Should have hover rule for unchecked checkboxes" + + hover_end = content.find('}', hover_start) + hover_css = content[hover_start:hover_end] + + # Check for pulse animation + assert 'animation: pulse' in hover_css, "Unchecked checkboxes should have pulse animation on hover" + + # Check for @keyframes pulse definition + assert '@keyframes pulse' in content, "Should have pulse keyframes definition" + + # Verify pulse animation scales element + keyframes_start = content.find('@keyframes pulse {') + keyframes_end = content.find('}', keyframes_start) + keyframes_css = content[keyframes_start:keyframes_end] + assert 'scale(' in keyframes_css, "Pulse animation should scale the element" + + print("✓ Unchecked habits have subtle pulse animation on checkbox hover") + +def test_frequency_badge_styling(): + """Test that frequency badge uses dashboard tag styling""" + with open('dashboard/habits.html', 'r') as f: + content = f.read() + + # Find .habit-frequency CSS rule + freq_start = content.find('.habit-frequency {') + assert freq_start > 0, ".habit-frequency CSS rule should exist" + + freq_end = content.find('}', freq_start) + freq_css = content[freq_start:freq_end] + + # Check for tag-like styling + assert 'display: inline-block' in freq_css, "Frequency should be inline-block" + assert 'background: var(--bg-elevated)' in freq_css, "Frequency should use --bg-elevated" + assert 'border: 1px solid var(--border)' in freq_css, "Frequency should have border" + assert 'padding:' in freq_css, "Frequency should have padding" + assert 'border-radius:' in freq_css, "Frequency should have border-radius" + + print("✓ Frequency badge uses dashboard tag styling") + +def test_card_uses_css_variables(): + """Test that cards use --bg-surface with --border""" + with open('dashboard/habits.html', 'r') as f: + content = f.read() + + # Find .habit-card CSS rule + card_start = content.find('.habit-card {') + assert card_start > 0, ".habit-card CSS rule should exist" + + card_end = content.find('}', card_start) + card_css = content[card_start:card_end] + + # Check for CSS variables + assert 'background: var(--bg-surface)' in card_css, "Cards should use --bg-surface" + assert 'border: 1px solid var(--border)' in card_css, "Cards should use --border" + + print("✓ Cards use --bg-surface with --border") + +def test_mobile_responsiveness(): + """Test that cards are responsive on mobile (full width < 768px)""" + with open('dashboard/habits.html', 'r') as f: + content = f.read() + + # Check for media query + assert '@media (max-width: 768px)' in content, "Should have mobile media query" + + # Find mobile media query + mobile_start = content.find('@media (max-width: 768px)') + assert mobile_start > 0, "Mobile media query should exist" + + mobile_end = content.find('}', content.find('}', content.find('}', mobile_start) + 1) + 1) + mobile_css = content[mobile_start:mobile_end] + + # Check for habit-card width + assert '.habit-card {' in mobile_css or 'habit-card' in mobile_css, "Mobile styles should target habit-card" + assert 'width: 100%' in mobile_css, "Cards should be full width on mobile" + + # Check for reduced spacing + assert '.main {' in mobile_css, "Main container should have mobile styling" + + print("✓ Responsive on mobile (full width < 768px)") + +def test_checked_class_in_createHabitCard(): + """Test that createHabitCard adds 'checked' class to card""" + with open('dashboard/habits.html', 'r') as f: + content = f.read() + + # Find createHabitCard function + func_start = content.find('function createHabitCard(habit) {') + assert func_start > 0, "createHabitCard function should exist" + + func_end = content.find('return card;', func_start) + func_code = content[func_start:func_end] + + # Check for checked class logic + assert "isChecked ? 'habit-card checked' : 'habit-card'" in func_code, "Should add 'checked' class to card when habit is checked" + + print("✓ createHabitCard adds 'checked' class when appropriate") + +def test_all_acceptance_criteria(): + """Summary test: verify all 7 acceptance criteria are met""" + with open('dashboard/habits.html', 'r') as f: + content = f.read() + + criteria = [] + + # 1. Cards use --bg-surface with --border + card_start = content.find('.habit-card {') + card_end = content.find('}', card_start) + card_css = content[card_start:card_end] + if 'background: var(--bg-surface)' in card_css and 'border: 1px solid var(--border)' in card_css: + criteria.append("✓ Cards use --bg-surface with --border") + + # 2. Streak displayed prominently with --text-xl + streak_start = content.find('.habit-streak {') + streak_end = content.find('}', streak_start) + streak_css = content[streak_start:streak_end] + if 'font-size: var(--text-xl)' in streak_css: + criteria.append("✓ Streak displayed prominently with --text-xl") + + # 3. Checked habits have subtle green background tint + if '.habit-card.checked' in content and 'rgba(34, 197, 94, 0.1)' in content: + criteria.append("✓ Checked habits have subtle green background tint") + + # 4. Unchecked habits have subtle pulse animation on checkbox hover + if 'animation: pulse' in content and '@keyframes pulse' in content: + criteria.append("✓ Unchecked habits have pulse animation on hover") + + # 5. Frequency badge uses dashboard tag styling + freq_start = content.find('.habit-frequency {') + freq_end = content.find('}', freq_start) + freq_css = content[freq_start:freq_end] + if 'display: inline-block' in freq_css and 'background: var(--bg-elevated)' in freq_css: + criteria.append("✓ Frequency badge uses dashboard tag styling") + + # 6. Cards have --radius-lg border radius + if 'border-radius: var(--radius-lg)' in card_css: + criteria.append("✓ Cards have --radius-lg border radius") + + # 7. Responsive on mobile (full width < 768px) + if '@media (max-width: 768px)' in content and 'width: 100%' in content[content.find('@media (max-width: 768px)'):]: + criteria.append("✓ Responsive on mobile (full width < 768px)") + + for criterion in criteria: + print(criterion) + + assert len(criteria) == 7, f"Should meet all 7 acceptance criteria, met {len(criteria)}" + print(f"\n✓ All 7 acceptance criteria met!") + +if __name__ == '__main__': + import os + os.chdir('/home/moltbot/clawd') + + tests = [ + test_file_exists, + test_card_uses_css_variables, + test_card_border_radius, + test_streak_font_size, + test_checked_habit_background, + test_checkbox_pulse_animation, + test_frequency_badge_styling, + test_mobile_responsiveness, + test_checked_class_in_createHabitCard, + test_all_acceptance_criteria + ] + + print("Running tests for Story 12.0: Frontend - Habit card styling\n") + + for test in tests: + try: + test() + except AssertionError as e: + print(f"✗ {test.__name__} failed: {e}") + exit(1) + except Exception as e: + print(f"✗ {test.__name__} error: {e}") + exit(1) + + print(f"\n{'='*60}") + print(f"All {len(tests)} tests passed! ✓") + print(f"{'='*60}")