feat: Add Pomodoro stats view and enhance recurring task UI

• Introduce a new PomodoroStatsView to display detailed session statistics.
  - Created a new file (src/views/PomodoroStatsView.ts) with UI components that render today’s progress, weekly summaries, overall statistics, and a list of recent work sessions.
  - Integrated refresh functionality and data calculations (e.g., current streak, average session length, completion rate) using data from the pomodoroService.

• Update the main plugin registration (src/main.ts):
  - Import the new PomodoroStatsView.
  - Register the POMODORO_STATS_VIEW_TYPE.
  - Add a command (‘open-pomodoro-stats’) to allow users to open the Pomodoro statistics view.
  - Add an activatePomodoroStatsView method to switch to the new view.
  - Ensure that when workspace leaves are detached, leaves of type POMODORO_STATS_VIEW_TYPE are also removed.

• Update task filtering and status management:
  - In FilterService.ts, update the “No Due Date” string to “No due date” for consistency.
  - In StatusManager.ts, add a new getNonCompletionStatuses method to retrieve status configurations excluding completed statuses—this is used to manage recurring tasks.

• Enhance recurring task UI and behavior in TaskCard component (src/ui/TaskCard.ts):
  - Modify task cards to detect recurring tasks and conditionally append a “recurring-task” CSS class.
  - Insert a recurring indicator icon in the task card for recurring tasks.
  - Update context menu logic to show only non-completion statuses (via getNonCompletionStatuses) for recurring tasks.
  - Add a menu item that toggles the completion state for recurring tasks for the current date.
  - Update the task card DOM structure (both initial creation and update) to add or remove the recurring indicator as needed.
  - Update the import statements to include new helper functions (shouldUseRecurringTaskUI and getRecurringTaskCompletionText).

• Extend helper functions (src/utils/helpers.ts):
  - Introduce shouldShowRecurringTaskOnDate to determine if a recurring task is due on a target date.
  - Add getRecurringTaskCompletionText to generate completion state text for a recurring task on a specific date.
  - Add shouldUseRecurringTaskUI to decide if recurring task UI behavior should be applied.

• Update PomodoroView (src/views/PomodoroView.ts):
  - Add a “View All Stats” button to the stats section that opens the PomodoroStatsView when clicked.

• Define new view type (src/types.ts):
  - Add constant POMODORO_STATS_VIEW_TYPE for consistent usage when registering and activating the stats view.

• Introduce new CSS styles (styles.css):
  - Add styling for recurring tasks (e.g., a colored left border and gradient background for cards marked as recurring).
  - Define styles for the recurring indicator element.
  - Implement comprehensive styles for the Pomodoro Stats view, including layout grids, header styling, cards for different statistics (pomodoros, streak, minutes, average session, completion rate), and styling for recent sessions.

This commit provides substantial enhancements by adding a focused Pomodoro statistics interface and by improving the handling and visual feedback for recurring tasks throughout the application.
This commit is contained in:
Callum Alpass 2025-06-08 11:52:32 +10:00
parent 41b9ac8808
commit deb1afa896
9 changed files with 579 additions and 6 deletions

View file

@ -12,6 +12,7 @@ import {
TASK_LIST_VIEW_TYPE,
AGENDA_VIEW_TYPE,
POMODORO_VIEW_TYPE,
POMODORO_STATS_VIEW_TYPE,
KANBAN_VIEW_TYPE,
TimeInfo,
TaskInfo,
@ -26,6 +27,7 @@ import { TaskListView } from './views/TaskListView';
import { NotesView } from './views/NotesView';
import { AgendaView } from './views/AgendaView';
import { PomodoroView } from './views/PomodoroView';
import { PomodoroStatsView } from './views/PomodoroStatsView';
import { KanbanView } from './views/KanbanView';
import { TaskCreationModal } from './modals/TaskCreationModal';
import { TaskEditModal } from './modals/TaskEditModal';
@ -173,6 +175,10 @@ export default class TaskNotesPlugin extends Plugin {
POMODORO_VIEW_TYPE,
(leaf) => new PomodoroView(leaf, this)
);
this.registerView(
POMODORO_STATS_VIEW_TYPE,
(leaf) => new PomodoroStatsView(leaf, this)
);
this.registerView(
KANBAN_VIEW_TYPE,
(leaf) => new KanbanView(leaf, this)
@ -430,6 +436,14 @@ export default class TaskNotesPlugin extends Plugin {
}
});
this.addCommand({
id: 'open-pomodoro-stats',
name: 'Open pomodoro statistics',
callback: async () => {
await this.activatePomodoroStatsView();
}
});
this.addCommand({
id: 'open-kanban-popout',
name: 'Open kanban board in new window',
@ -535,6 +549,10 @@ export default class TaskNotesPlugin extends Plugin {
return this.activateView(POMODORO_VIEW_TYPE);
}
async activatePomodoroStatsView() {
return this.activateView(POMODORO_STATS_VIEW_TYPE);
}
async activateKanbanView() {
return this.activateView(KANBAN_VIEW_TYPE);
}
@ -578,6 +596,7 @@ export default class TaskNotesPlugin extends Plugin {
workspace.detachLeavesOfType(NOTES_VIEW_TYPE);
workspace.detachLeavesOfType(AGENDA_VIEW_TYPE);
workspace.detachLeavesOfType(POMODORO_VIEW_TYPE);
workspace.detachLeavesOfType(POMODORO_STATS_VIEW_TYPE);
workspace.detachLeavesOfType(KANBAN_VIEW_TYPE);
// Create a calendar view

View file

@ -301,7 +301,7 @@ export class FilterService extends EventEmitter {
* Get due date group for task (Today, Tomorrow, This Week, etc.)
*/
private getDueDateGroup(dueDate?: string): string {
if (!dueDate) return 'No Due Date';
if (!dueDate) return 'No due date';
const due = new Date(dueDate);
const today = new Date();

View file

@ -92,6 +92,13 @@ export class StatusManager {
return [...this.statuses];
}
/**
* Get non-completion status configurations (for recurring tasks)
*/
getNonCompletionStatuses(): StatusConfig[] {
return this.statuses.filter(s => !s.isCompleted);
}
/**
* Update status configurations
*/

View file

@ -4,6 +4,7 @@ export const TASK_LIST_VIEW_TYPE = 'tasknotes-task-list-view';
export const NOTES_VIEW_TYPE = 'tasknotes-notes-view';
export const AGENDA_VIEW_TYPE = 'tasknotes-agenda-view';
export const POMODORO_VIEW_TYPE = 'tasknotes-pomodoro-view';
export const POMODORO_STATS_VIEW_TYPE = 'tasknotes-pomodoro-stats-view';
export const KANBAN_VIEW_TYPE = 'tasknotes-kanban-view';
// Event types

View file

@ -2,7 +2,7 @@ import { format } from 'date-fns';
import { TFile, Menu } from 'obsidian';
import { TaskInfo } from '../types';
import TaskNotesPlugin from '../main';
import { calculateTotalTimeSpent, isRecurringTaskDueOn, getEffectiveTaskStatus } from '../utils/helpers';
import { calculateTotalTimeSpent, isRecurringTaskDueOn, getEffectiveTaskStatus, shouldUseRecurringTaskUI, getRecurringTaskCompletionText } from '../utils/helpers';
export interface TaskCardOptions {
showDueDate: boolean;
@ -39,7 +39,8 @@ export function createTaskCard(task: TaskInfo, plugin: TaskNotesPlugin, options:
const card = document.createElement('div');
const isActivelyTracked = plugin.getActiveTimeSession(task) !== null;
const isCompleted = plugin.statusManager.isCompletedStatus(effectiveStatus);
card.className = `tasknotes-card tasknotes-card--normal tasknotes-card--flex task-card ${effectiveStatus} ${task.archived ? 'archived' : ''} ${isActivelyTracked ? 'actively-tracked' : ''} ${isCompleted ? 'task-completed' : ''}`;
const isRecurring = !!task.recurrence;
card.className = `tasknotes-card tasknotes-card--normal tasknotes-card--flex task-card ${effectiveStatus} ${task.archived ? 'archived' : ''} ${isActivelyTracked ? 'actively-tracked' : ''} ${isCompleted ? 'task-completed' : ''} ${isRecurring ? 'recurring-task' : ''}`;
card.dataset.taskPath = task.path;
// Apply priority as left border color
@ -79,6 +80,14 @@ export function createTaskCard(task: TaskInfo, plugin: TaskNotesPlugin, options:
statusDot.style.backgroundColor = statusConfig.color;
}
// Recurring task indicator
if (task.recurrence) {
const recurringIndicator = card.createEl('span', {
cls: 'recurring-indicator',
attr: { 'aria-label': `Recurring: ${task.recurrence.frequency}` }
});
}
// Main content container
const contentContainer = card.createEl('div', { cls: 'task-content' });
@ -202,8 +211,14 @@ async function showTaskContextMenu(event: MouseEvent, taskPath: string, plugin:
const menu = new Menu();
// For recurring tasks, only show non-completion statuses
// For regular tasks, show all statuses
const availableStatuses = task.recurrence
? plugin.statusManager.getNonCompletionStatuses()
: plugin.statusManager.getAllStatuses();
// Direct status options (no submenu to avoid issues)
plugin.statusManager.getAllStatuses().forEach(statusConfig => {
availableStatuses.forEach(statusConfig => {
menu.addItem((item) => {
const isSelected = task.status === statusConfig.value;
item.setTitle(`${isSelected ? '✓ ' : ''}${statusConfig.label}`);
@ -223,6 +238,27 @@ async function showTaskContextMenu(event: MouseEvent, taskPath: string, plugin:
});
});
// Add completion toggle for recurring tasks
if (task.recurrence) {
menu.addSeparator();
// Check current completion status for this date
const dateStr = format(targetDate, 'yyyy-MM-dd');
const isCompletedForDate = task.complete_instances?.includes(dateStr) || false;
menu.addItem((item) => {
item.setTitle(isCompletedForDate ? 'Mark incomplete for this date' : 'Mark complete for this date');
item.setIcon(isCompletedForDate ? 'x' : 'check');
item.onClick(async () => {
try {
await plugin.toggleRecurringTaskComplete(task, targetDate);
} catch (error) {
console.error('Error toggling recurring task completion:', error);
}
});
});
}
menu.addSeparator();
// Direct priority options (no submenu to avoid issues)
@ -336,7 +372,8 @@ export function updateTaskCard(element: HTMLElement, task: TaskInfo, plugin: Tas
// Update main element classes
const isActivelyTracked = plugin.getActiveTimeSession(task) !== null;
const isCompleted = plugin.statusManager.isCompletedStatus(effectiveStatus);
element.className = `tasknotes-card tasknotes-card--normal tasknotes-card--flex task-card ${effectiveStatus} ${task.archived ? 'archived' : ''} ${isActivelyTracked ? 'actively-tracked' : ''} ${isCompleted ? 'task-completed' : ''}`;
const isRecurring = !!task.recurrence;
element.className = `tasknotes-card tasknotes-card--normal tasknotes-card--flex task-card ${effectiveStatus} ${task.archived ? 'archived' : ''} ${isActivelyTracked ? 'actively-tracked' : ''} ${isCompleted ? 'task-completed' : ''} ${isRecurring ? 'recurring-task' : ''}`;
// Update priority border color
const priorityConfig = plugin.priorityManager.getPriorityConfig(task.priority);
@ -360,6 +397,23 @@ export function updateTaskCard(element: HTMLElement, task: TaskInfo, plugin: Tas
}
}
// Update recurring indicator
const existingRecurringIndicator = element.querySelector('.recurring-indicator');
if (task.recurrence && !existingRecurringIndicator) {
// Add recurring indicator if task is now recurring but didn't have one
const recurringIndicator = element.createEl('span', {
cls: 'recurring-indicator',
attr: { 'aria-label': `Recurring: ${task.recurrence.frequency}` }
});
statusDot.insertAdjacentElement('afterend', recurringIndicator);
} else if (!task.recurrence && existingRecurringIndicator) {
// Remove recurring indicator if task is no longer recurring
existingRecurringIndicator.remove();
} else if (task.recurrence && existingRecurringIndicator) {
// Update existing recurring indicator
existingRecurringIndicator.setAttribute('aria-label', `Recurring: ${task.recurrence.frequency}`);
}
// Update title
const titleEl = element.querySelector('.task-title') as HTMLElement;
if (titleEl) {

View file

@ -537,6 +537,34 @@ export function getEffectiveTaskStatus(task: any, date: Date): string {
return completedDates.includes(dateStr) ? 'done' : 'open';
}
/**
* Checks if a recurring task should be due on the current target date
*/
export function shouldShowRecurringTaskOnDate(task: TaskInfo, targetDate: Date): boolean {
if (!task.recurrence) return true; // Non-recurring tasks are always shown
return isRecurringTaskDueOn(task, targetDate);
}
/**
* Gets the completion state text for a recurring task on a specific date
*/
export function getRecurringTaskCompletionText(task: TaskInfo, targetDate: Date): string {
if (!task.recurrence) return '';
const dateStr = format(targetDate, 'yyyy-MM-dd');
const isCompleted = task.complete_instances?.includes(dateStr) || false;
return isCompleted ? 'Completed for this date' : 'Not completed for this date';
}
/**
* Checks if a task should use recurring task UI behavior
*/
export function shouldUseRecurringTaskUI(task: TaskInfo): boolean {
return !!task.recurrence;
}
/**
* Extracts note information from a note file's content
*/

View file

@ -0,0 +1,250 @@
import { ItemView, WorkspaceLeaf, Setting } from 'obsidian';
import { format, subDays, startOfWeek, endOfWeek } from 'date-fns';
import TaskNotesPlugin from '../main';
import {
POMODORO_STATS_VIEW_TYPE,
PomodoroHistoryStats,
PomodoroSessionHistory
} from '../types';
export class PomodoroStatsView extends ItemView {
plugin: TaskNotesPlugin;
// UI elements
private todayStatsEl: HTMLElement | null = null;
private weekStatsEl: HTMLElement | null = null;
private recentSessionsEl: HTMLElement | null = null;
private overallStatsEl: HTMLElement | null = null;
constructor(leaf: WorkspaceLeaf, plugin: TaskNotesPlugin) {
super(leaf);
this.plugin = plugin;
}
getViewType(): string {
return POMODORO_STATS_VIEW_TYPE;
}
getDisplayText(): string {
return 'Pomodoro Stats';
}
getIcon(): string {
return 'bar-chart';
}
async onOpen() {
await this.plugin.onReady();
await this.render();
}
async onClose() {
this.contentEl.empty();
}
async render() {
const container = this.contentEl.createDiv({ cls: 'tasknotes-container pomodoro-stats-container' });
// Header
const header = container.createDiv({ cls: 'pomodoro-stats-header' });
new Setting(header)
.setName('Pomodoro Statistics')
.setHeading();
// Refresh button
const refreshButton = header.createEl('button', {
cls: 'pomodoro-stats-refresh-button',
text: 'Refresh'
});
this.registerDomEvent(refreshButton, 'click', () => {
this.refreshStats();
});
// Today's stats
const todaySection = container.createDiv({ cls: 'pomodoro-stats-section' });
new Setting(todaySection)
.setName('Today')
.setHeading();
this.todayStatsEl = todaySection.createDiv({ cls: 'pomodoro-stats-grid' });
// This week's stats
const weekSection = container.createDiv({ cls: 'pomodoro-stats-section' });
new Setting(weekSection)
.setName('This Week')
.setHeading();
this.weekStatsEl = weekSection.createDiv({ cls: 'pomodoro-stats-grid' });
// Overall stats
const overallSection = container.createDiv({ cls: 'pomodoro-stats-section' });
new Setting(overallSection)
.setName('All Time')
.setHeading();
this.overallStatsEl = overallSection.createDiv({ cls: 'pomodoro-stats-grid' });
// Recent sessions
const recentSection = container.createDiv({ cls: 'pomodoro-stats-section' });
new Setting(recentSection)
.setName('Recent Sessions')
.setHeading();
this.recentSessionsEl = recentSection.createDiv({ cls: 'pomodoro-recent-sessions' });
// Initial load
await this.refreshStats();
}
private async refreshStats() {
try {
await Promise.all([
this.updateTodayStats(),
this.updateWeekStats(),
this.updateOverallStats(),
this.updateRecentSessions()
]);
} catch (error) {
console.error('Failed to refresh stats:', error);
}
}
private async updateTodayStats() {
if (!this.todayStatsEl) return;
const stats = await this.plugin.pomodoroService.getTodayStats();
this.renderStatsGrid(this.todayStatsEl, stats);
}
private async updateWeekStats() {
if (!this.weekStatsEl) return;
const today = new Date();
const weekStart = startOfWeek(today, { weekStartsOn: 1 }); // Monday
const weekEnd = endOfWeek(today, { weekStartsOn: 1 });
const stats = await this.calculateStatsForRange(weekStart, weekEnd);
this.renderStatsGrid(this.weekStatsEl, stats);
}
private async updateOverallStats() {
if (!this.overallStatsEl) return;
const history = await this.plugin.pomodoroService.getSessionHistory();
const stats = this.calculateOverallStats(history);
this.renderStatsGrid(this.overallStatsEl, stats);
}
private async updateRecentSessions() {
if (!this.recentSessionsEl) return;
const history = await this.plugin.pomodoroService.getSessionHistory();
const recentSessions = history
.filter(session => session.type === 'work')
.slice(-10)
.reverse();
this.recentSessionsEl.empty();
if (recentSessions.length === 0) {
this.recentSessionsEl.createDiv({
cls: 'pomodoro-no-sessions',
text: 'No sessions recorded yet'
});
return;
}
for (const session of recentSessions) {
const sessionEl = this.recentSessionsEl.createDiv({ cls: 'pomodoro-session-item' });
const dateEl = sessionEl.createSpan({ cls: 'session-date' });
dateEl.textContent = format(new Date(session.startTime), 'MMM d, HH:mm');
const durationEl = sessionEl.createSpan({ cls: 'session-duration' });
durationEl.textContent = `${session.duration}min`;
const statusEl = sessionEl.createSpan({ cls: 'session-status' });
statusEl.textContent = session.completed ? 'Completed' : 'Interrupted';
statusEl.addClass(session.completed ? 'status-completed' : 'status-interrupted');
if (session.taskPath) {
const taskEl = sessionEl.createSpan({ cls: 'session-task' });
const taskName = session.taskPath.split('/').pop()?.replace('.md', '') || '';
taskEl.textContent = taskName;
}
}
}
private renderStatsGrid(container: HTMLElement, stats: PomodoroHistoryStats) {
container.empty();
// Completed pomodoros
const pomodorosCard = container.createDiv({ cls: 'pomodoro-stat-card' });
pomodorosCard.createDiv({ cls: 'stat-value', text: stats.pomodorosCompleted.toString() });
pomodorosCard.createDiv({ cls: 'stat-label', text: 'Pomodoros' });
// Current streak
const streakCard = container.createDiv({ cls: 'pomodoro-stat-card' });
streakCard.createDiv({ cls: 'stat-value', text: stats.currentStreak.toString() });
streakCard.createDiv({ cls: 'stat-label', text: 'Streak' });
// Total minutes
const minutesCard = container.createDiv({ cls: 'pomodoro-stat-card' });
minutesCard.createDiv({ cls: 'stat-value', text: stats.totalMinutes.toString() });
minutesCard.createDiv({ cls: 'stat-label', text: 'Minutes' });
// Average session length
const avgCard = container.createDiv({ cls: 'pomodoro-stat-card' });
avgCard.createDiv({ cls: 'stat-value', text: stats.averageSessionLength.toString() });
avgCard.createDiv({ cls: 'stat-label', text: 'Avg Length' });
// Completion rate
const rateCard = container.createDiv({ cls: 'pomodoro-stat-card' });
rateCard.createDiv({ cls: 'stat-value', text: `${stats.completionRate}%` });
rateCard.createDiv({ cls: 'stat-label', text: 'Completion' });
}
private async calculateStatsForRange(startDate: Date, endDate: Date): Promise<PomodoroHistoryStats> {
const history = await this.plugin.pomodoroService.getSessionHistory();
// Filter sessions within date range
const rangeSessions = history.filter(session => {
const sessionDate = new Date(session.startTime);
return sessionDate >= startDate && sessionDate <= endDate;
});
return this.calculateStatsFromSessions(rangeSessions);
}
private calculateOverallStats(history: PomodoroSessionHistory[]): PomodoroHistoryStats {
return this.calculateStatsFromSessions(history);
}
private calculateStatsFromSessions(sessions: PomodoroSessionHistory[]): PomodoroHistoryStats {
// Filter work sessions only
const workSessions = sessions.filter(session => session.type === 'work');
const completedWork = workSessions.filter(session => session.completed);
// Calculate streak from most recent sessions
let currentStreak = 0;
for (let i = workSessions.length - 1; i >= 0; i--) {
if (workSessions[i].completed) {
currentStreak++;
} else {
break;
}
}
const totalMinutes = completedWork.reduce((sum, session) => sum + session.duration, 0);
const averageSessionLength = completedWork.length > 0
? totalMinutes / completedWork.length
: 0;
const completionRate = workSessions.length > 0
? (completedWork.length / workSessions.length) * 100
: 0;
return {
pomodorosCompleted: completedWork.length,
currentStreak,
totalMinutes,
averageSessionLength: Math.round(averageSessionLength),
completionRate: Math.round(completionRate)
};
}
}

View file

@ -241,9 +241,20 @@ export class PomodoroView extends ItemView {
// Statistics
const statsSection = container.createDiv({ cls: 'pomodoro-stats-section' });
new Setting(statsSection)
const statsHeader = statsSection.createDiv({ cls: 'pomodoro-stats-header' });
new Setting(statsHeader)
.setName('Today\'s progress')
.setHeading();
const viewStatsButton = statsHeader.createEl('button', {
cls: 'pomodoro-view-stats-button',
text: 'View All Stats'
});
this.registerDomEvent(viewStatsButton, 'click', async () => {
await this.plugin.activatePomodoroStatsView();
});
this.statsDisplay = statsSection.createDiv({ cls: 'pomodoro-stats' });
// Create stat elements and cache references

View file

@ -4299,6 +4299,40 @@
line-height: 1.2;
}
/* Recurring task styles */
.task-card.recurring-task {
border-left: 3px solid var(--priority-color, var(--interactive-accent));
background: linear-gradient(135deg, var(--background-primary) 95%, var(--interactive-accent-hover) 100%);
}
.task-card.recurring-task:hover {
background: linear-gradient(135deg, var(--background-modifier-hover) 95%, var(--interactive-accent-hover) 100%);
}
/* Recurring indicator */
.task-card .recurring-indicator {
flex-shrink: 0;
width: var(--cs-indicator-md);
height: var(--cs-indicator-md);
border-radius: 50%;
background: var(--interactive-accent);
border: 2px solid var(--background-primary);
margin-top: 2px;
position: relative;
}
.task-card .recurring-indicator::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 2px;
height: 2px;
background: var(--background-primary);
border-radius: 50%;
}
/* Update animation */
.task-card.task-updated {
background: var(--interactive-accent);
@ -5889,3 +5923,172 @@
}
}
/* Pomodoro Stats View Styles */
.pomodoro-stats-container {
padding: var(--cs-spacing-lg);
max-width: 1200px;
margin: 0 auto;
overflow-y: auto;
}
.pomodoro-stats-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--cs-spacing-xl);
}
.pomodoro-stats-refresh-button {
padding: var(--cs-spacing-xs) var(--cs-spacing-md);
border: 1px solid var(--background-modifier-border);
border-radius: var(--cs-radius-md);
background: var(--background-primary);
color: var(--text-normal);
font-size: var(--cs-text-sm);
cursor: pointer;
transition: all var(--cs-transition-fast);
}
.pomodoro-stats-refresh-button:hover {
background: var(--background-modifier-hover);
}
.pomodoro-stats-section {
margin-bottom: var(--cs-spacing-xl);
}
.pomodoro-stats-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--cs-spacing-md);
}
.pomodoro-view-stats-button {
padding: var(--cs-spacing-xs) var(--cs-spacing-sm);
border: 1px solid var(--background-modifier-border);
border-radius: var(--cs-radius-md);
background: var(--background-primary);
color: var(--text-muted);
font-size: var(--cs-text-xs);
cursor: pointer;
transition: all var(--cs-transition-fast);
}
.pomodoro-view-stats-button:hover {
background: var(--background-modifier-hover);
color: var(--text-normal);
}
.pomodoro-stats-section h3 {
font-size: var(--cs-text-lg);
font-weight: 600;
color: var(--text-normal);
margin: 0 0 var(--cs-spacing-md) 0;
border-bottom: 1px solid var(--background-modifier-border);
padding-bottom: var(--cs-spacing-xs);
}
.pomodoro-stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: var(--cs-spacing-md);
margin-bottom: var(--cs-spacing-lg);
}
.pomodoro-stat-card {
background: var(--background-secondary);
border: 1px solid var(--background-modifier-border);
border-radius: var(--cs-radius-md);
padding: var(--cs-spacing-md);
text-align: center;
transition: all var(--cs-transition-fast);
}
.pomodoro-stat-card:hover {
background: var(--background-modifier-hover);
transform: translateY(-1px);
}
.pomodoro-stat-card .stat-value {
font-size: 2rem;
font-weight: 600;
color: var(--text-accent);
line-height: 1;
margin-bottom: var(--cs-spacing-xs);
}
.pomodoro-stat-card .stat-label {
font-size: var(--cs-text-sm);
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.pomodoro-recent-sessions {
background: var(--background-secondary);
border: 1px solid var(--background-modifier-border);
border-radius: var(--cs-radius-md);
padding: var(--cs-spacing-md);
}
.pomodoro-no-sessions {
text-align: center;
color: var(--text-muted);
font-style: italic;
padding: var(--cs-spacing-lg);
}
.pomodoro-session-item {
display: flex;
align-items: center;
gap: var(--cs-spacing-md);
padding: var(--cs-spacing-sm) 0;
border-bottom: 1px solid var(--background-modifier-border);
}
.pomodoro-session-item:last-child {
border-bottom: none;
}
.session-date {
font-size: var(--cs-text-sm);
color: var(--text-muted);
min-width: 80px;
}
.session-duration {
font-size: var(--cs-text-sm);
font-weight: 500;
color: var(--text-normal);
min-width: 50px;
}
.session-status {
font-size: var(--cs-text-xs);
padding: 2px var(--cs-spacing-xs);
border-radius: var(--cs-radius-sm);
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 500;
}
.status-completed {
background: rgba(46, 204, 113, 0.1);
color: #2ecc71;
}
.status-interrupted {
background: rgba(231, 76, 60, 0.1);
color: #e74c3c;
}
.session-task {
font-size: var(--cs-text-sm);
color: var(--text-normal);
flex-grow: 1;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}