feat: US-002 - Frontend: Compact habit cards (~100px height)

This commit is contained in:
Echo
2026-02-10 18:35:07 +00:00
parent 6d40d7e24b
commit 4d50965bac
2 changed files with 294 additions and 182 deletions

View File

@@ -93,14 +93,19 @@
min-height: 44px;
}
.habit-card-check-btn {
min-height: 48px;
font-size: var(--text-lg);
.habit-card-check-btn-compact {
min-width: 44px;
min-height: 44px;
}
.habit-card-skip-btn {
min-height: 44px;
padding: var(--space-2) var(--space-3);
/* Compact cards stay compact on mobile */
.habit-card {
min-height: 90px;
max-height: 120px;
}
.habit-card-name {
font-size: var(--text-xs);
}
.modal-close {
@@ -233,11 +238,13 @@
border: 1px solid var(--border);
border-radius: var(--radius-lg);
border-left: 4px solid var(--accent);
padding: var(--space-4);
padding: var(--space-3);
transition: all var(--transition-base);
display: flex;
flex-direction: column;
gap: var(--space-3);
gap: var(--space-2);
min-height: 90px;
max-height: 110px;
}
.habit-card:hover {
@@ -246,29 +253,73 @@
box-shadow: var(--shadow-md);
}
.habit-card-header {
/* Compact single-row layout */
.habit-card-row {
display: flex;
align-items: center;
gap: var(--space-2);
}
.habit-card-icon {
width: 20px;
height: 20px;
width: 18px;
height: 18px;
color: var(--text-primary);
flex-shrink: 0;
}
.habit-card-name {
flex: 1;
font-size: var(--text-base);
font-size: var(--text-sm);
font-weight: 600;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.habit-card-streak {
font-size: var(--text-xs);
color: var(--text-muted);
white-space: nowrap;
flex-shrink: 0;
}
/* Compact check button */
.habit-card-check-btn-compact {
width: 32px;
height: 32px;
border: 2px solid var(--accent);
background: transparent;
color: var(--accent);
border-radius: 50%;
font-size: var(--text-lg);
font-weight: 600;
cursor: pointer;
transition: all var(--transition-base);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.habit-card-check-btn-compact:hover:not(:disabled) {
background: var(--accent);
color: white;
transform: scale(1.1);
}
.habit-card-check-btn-compact:disabled {
opacity: 0.6;
cursor: not-allowed;
background: var(--accent);
border-color: var(--accent);
color: white;
}
.habit-card-actions {
display: flex;
gap: var(--space-2);
gap: var(--space-1);
flex-shrink: 0;
}
.habit-card-action-btn {
@@ -290,123 +341,48 @@
}
.habit-card-action-btn svg {
width: 16px;
height: 16px;
width: 14px;
height: 14px;
}
.habit-card-streaks {
display: flex;
gap: var(--space-4);
font-size: var(--text-sm);
color: var(--text-muted);
}
.habit-card-streak {
/* Progress bar row */
.habit-card-progress-row {
display: flex;
align-items: center;
gap: var(--space-1);
gap: var(--space-2);
}
.habit-card-check-btn {
width: 100%;
padding: var(--space-3);
border: 2px solid var(--accent);
background: var(--accent);
color: white;
border-radius: var(--radius-md);
font-size: var(--text-base);
.habit-card-progress-bar {
flex: 1;
height: 6px;
background: var(--bg-muted);
border-radius: var(--radius-sm);
overflow: hidden;
}
.habit-card-progress-fill {
height: 100%;
transition: width var(--transition-base);
border-radius: var(--radius-sm);
}
.habit-card-progress-text {
font-size: var(--text-xs);
color: var(--text-muted);
font-weight: 600;
cursor: pointer;
transition: all var(--transition-base);
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
min-width: 32px;
text-align: right;
flex-shrink: 0;
}
.habit-card-check-btn:hover:not(:disabled) {
background: var(--accent-hover);
transform: scale(1.02);
}
.habit-card-check-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
background: var(--bg-muted);
border-color: var(--border);
color: var(--text-muted);
}
.habit-card-last-check {
font-size: var(--text-sm);
color: var(--text-muted);
text-align: center;
}
.habit-card-lives {
display: flex;
justify-content: center;
align-items: center;
gap: var(--space-2);
font-size: var(--text-lg);
}
.habit-card-lives-hearts {
display: flex;
gap: var(--space-1);
}
.habit-card-skip-btn {
background: none;
border: none;
color: var(--text-muted);
font-size: var(--text-xs);
cursor: pointer;
padding: var(--space-1) var(--space-2);
border-radius: var(--radius-sm);
transition: all var(--transition-base);
}
.habit-card-skip-btn:hover:not(:disabled) {
background: var(--bg-hover);
color: var(--text-primary);
}
.habit-card-skip-btn:disabled {
cursor: not-allowed;
opacity: 0.3;
}
.habit-card-completion {
font-size: var(--text-sm);
color: var(--text-muted);
text-align: center;
}
.habit-card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: var(--space-2);
border-top: 1px solid var(--border);
}
.habit-card-category {
font-size: var(--text-xs);
padding: var(--space-1) var(--space-2);
border-radius: var(--radius-sm);
background: var(--bg-muted);
color: var(--text-muted);
}
.habit-card-priority {
/* Next date row */
.habit-card-next-date {
font-size: var(--text-xs);
color: var(--text-muted);
display: flex;
align-items: center;
gap: var(--space-1);
text-align: left;
}
/* Keep priority indicator styles for future use */
.priority-indicator {
width: 8px;
height: 8px;
@@ -1344,15 +1320,23 @@
// Render single habit card
function renderHabitCard(habit) {
const isDoneToday = isCheckedToday(habit);
const lastCheckInfo = getLastCheckInfo(habit);
const livesHtml = renderLives(habit.lives || 3);
const completionRate = habit.completion_rate_30d || 0;
const completionRate = Math.round(habit.completion_rate_30d || 0);
const nextCheckDate = getNextCheckDate(habit);
return `
<div class="habit-card" style="border-left-color: ${habit.color}">
<div class="habit-card-header">
<div class="habit-card-row">
<i data-lucide="${habit.icon || 'circle'}" class="habit-card-icon"></i>
<span class="habit-card-name">${escapeHtml(habit.name)}</span>
<span class="habit-card-streak">🔥 ${habit.streak?.current || 0}</span>
<button
class="habit-card-check-btn-compact"
id="checkin-btn-${habit.id}"
${isDoneToday ? 'disabled' : ''}
title="${isDoneToday ? 'Completed today' : 'Check in'}"
>
${isDoneToday ? '✓' : '○'}
</button>
<div class="habit-card-actions">
<button class="habit-card-action-btn" onclick="showEditHabitModal('${habit.id}')" title="Edit">
<i data-lucide="settings"></i>
@@ -1363,46 +1347,14 @@
</div>
</div>
<div class="habit-card-streaks">
<div class="habit-card-streak">
🔥 ${habit.streak?.current || 0}
</div>
<div class="habit-card-streak">
🏆 ${habit.streak?.best || 0}
<div class="habit-card-progress-row">
<div class="habit-card-progress-bar">
<div class="habit-card-progress-fill" style="width: ${completionRate}%; background-color: ${habit.color}"></div>
</div>
<span class="habit-card-progress-text">${completionRate}%</span>
</div>
<button
class="habit-card-check-btn"
id="checkin-btn-${habit.id}"
${isDoneToday ? 'disabled' : ''}
>
${isDoneToday ? '✓ Done today' : 'Check In'}
</button>
<div class="habit-card-last-check">${lastCheckInfo}</div>
<div class="habit-card-lives">
<div class="habit-card-lives-hearts">${livesHtml}</div>
<button
class="habit-card-skip-btn"
onclick="skipHabitDay('${habit.id}', '${escapeHtml(habit.name)}')"
${habit.lives === 0 ? 'disabled' : ''}
title="${habit.lives === 0 ? 'No lives left' : 'Skip today and use 1 life'}"
>
Skip day
</button>
</div>
<div class="habit-card-completion">${completionRate}% (30d)</div>
<div class="habit-card-footer">
<span class="habit-card-category">${escapeHtml(habit.category || 'General')}</span>
<span class="habit-card-priority">
<span class="priority-indicator priority-${getPriorityLevel(habit.priority || 3)}"></span>
P${habit.priority || 3}
</span>
</div>
<div class="habit-card-next-date">${nextCheckDate}</div>
</div>
`;
}
@@ -1456,6 +1408,18 @@
return 'low';
}
// Get next check date text
function getNextCheckDate(habit) {
if (isCheckedToday(habit)) {
return 'Next: Tomorrow';
}
if (habit.should_check_today) {
return 'Due: Today';
}
// For habits not due today, show generic "upcoming"
return 'Next: Upcoming';
}
// Stats calculation and rendering
function renderStats() {
const statsSection = document.getElementById('statsSection');