feat: 12.0 - Frontend - Habit card styling
This commit is contained in:
@@ -87,7 +87,7 @@
|
|||||||
.habit-card {
|
.habit-card {
|
||||||
background: var(--bg-surface);
|
background: var(--bg-surface);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-lg);
|
||||||
padding: var(--space-4);
|
padding: var(--space-4);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -100,6 +100,10 @@
|
|||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.habit-card.checked {
|
||||||
|
background: rgba(34, 197, 94, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
.habit-icon {
|
.habit-icon {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
@@ -129,8 +133,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.habit-frequency {
|
.habit-frequency {
|
||||||
|
display: inline-block;
|
||||||
font-size: var(--text-xs);
|
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;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +147,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-1);
|
gap: var(--space-1);
|
||||||
font-size: var(--text-lg);
|
font-size: var(--text-xl);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -162,6 +171,12 @@
|
|||||||
.habit-checkbox:hover:not(.disabled) {
|
.habit-checkbox:hover:not(.disabled) {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
background: var(--accent-light, rgba(99, 102, 241, 0.1));
|
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 {
|
.habit-checkbox.checked {
|
||||||
@@ -343,6 +358,41 @@
|
|||||||
transform: translateX(-50%) translateY(0);
|
transform: translateX(-50%) translateY(0);
|
||||||
opacity: 1;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -627,13 +677,13 @@
|
|||||||
// Create habit card element
|
// Create habit card element
|
||||||
function createHabitCard(habit) {
|
function createHabitCard(habit) {
|
||||||
const card = document.createElement('div');
|
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
|
// Determine icon based on frequency
|
||||||
const iconName = habit.frequency === 'daily' ? 'calendar' : 'clock';
|
const iconName = habit.frequency === 'daily' ? 'calendar' : 'clock';
|
||||||
|
|
||||||
// Checkbox state
|
// Checkbox state
|
||||||
const isChecked = habit.checkedToday || false;
|
|
||||||
const checkboxClass = isChecked ? 'habit-checkbox checked disabled' : 'habit-checkbox';
|
const checkboxClass = isChecked ? 'habit-checkbox checked disabled' : 'habit-checkbox';
|
||||||
const checkIcon = isChecked ? '<i data-lucide="check"></i>' : '';
|
const checkIcon = isChecked ? '<i data-lucide="check"></i>' : '';
|
||||||
|
|
||||||
@@ -677,6 +727,12 @@
|
|||||||
checkboxElement.innerHTML = '<i data-lucide="check"></i>';
|
checkboxElement.innerHTML = '<i data-lucide="check"></i>';
|
||||||
lucide.createIcons();
|
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
|
// Store original state for rollback
|
||||||
const originalCheckbox = checkboxElement.cloneNode(true);
|
const originalCheckbox = checkboxElement.cloneNode(true);
|
||||||
const streakElement = document.getElementById(`streak-${habitId}`);
|
const streakElement = document.getElementById(`streak-${habitId}`);
|
||||||
@@ -708,6 +764,12 @@
|
|||||||
checkboxElement.classList.remove('checked', 'disabled');
|
checkboxElement.classList.remove('checked', 'disabled');
|
||||||
checkboxElement.innerHTML = '';
|
checkboxElement.innerHTML = '';
|
||||||
|
|
||||||
|
// Revert card background
|
||||||
|
const card = checkboxElement.closest('.habit-card');
|
||||||
|
if (card) {
|
||||||
|
card.classList.remove('checked');
|
||||||
|
}
|
||||||
|
|
||||||
// Revert streak
|
// Revert streak
|
||||||
if (streakElement) {
|
if (streakElement) {
|
||||||
streakElement.textContent = originalStreak;
|
streakElement.textContent = originalStreak;
|
||||||
|
|||||||
256
dashboard/test_habits_card_styling.py
Normal file
256
dashboard/test_habits_card_styling.py
Normal file
@@ -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}")
|
||||||
Reference in New Issue
Block a user