feat(calendar): add tasks page for todo management

Add dedicated tasks route with navigation entry and todo detail modal.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-12-10 15:55:18 +01:00
parent aa778bc0de
commit 3fc89f565d
3 changed files with 1113 additions and 0 deletions

View file

@ -0,0 +1,625 @@
<script lang="ts">
import { todosStore } from '$lib/stores/todos.svelte';
import type { Task, UpdateTaskInput, TaskPriority } from '$lib/api/todos';
import { PRIORITY_LABELS, PRIORITY_COLORS } from '$lib/api/todos';
import { toast } from '$lib/stores/toast';
import TodoCheckbox from './TodoCheckbox.svelte';
import PriorityBadge from './PriorityBadge.svelte';
import { X, Calendar, Clock, Folder, Tag, Trash2, CheckSquare, AlertCircle } from 'lucide-svelte';
import { format, parseISO } from 'date-fns';
import { de } from 'date-fns/locale';
interface Props {
task: Task;
onClose: () => void;
}
let { task: initialTask, onClose }: Props = $props();
// Local editable state
let task = $state<Task>({ ...initialTask });
let isEditing = $state(false);
let isSaving = $state(false);
let isDeleting = $state(false);
let isToggling = $state(false);
// Form state
let title = $state(task.title);
let description = $state(task.description || '');
let dueDate = $state(task.dueDate ? formatDateForInput(task.dueDate) : '');
let dueTime = $state(task.dueTime || '');
let priority = $state<TaskPriority>(task.priority);
function formatDateForInput(date: string | Date | null | undefined): string {
if (!date) return '';
const d = typeof date === 'string' ? parseISO(date) : date;
return format(d, 'yyyy-MM-dd');
}
function formatDisplayDate(date: string | Date | null | undefined): string {
if (!date) return 'Kein Datum';
const d = typeof date === 'string' ? parseISO(date) : date;
return format(d, 'EEEE, d. MMMM yyyy', { locale: de });
}
async function handleToggleComplete() {
isToggling = true;
const result = await todosStore.toggleComplete(task.id);
if (result.data) {
task = result.data;
} else if (result.error) {
toast.error(`Fehler: ${result.error.message}`);
}
isToggling = false;
}
async function handleSave() {
if (!title.trim()) {
toast.error('Titel darf nicht leer sein');
return;
}
isSaving = true;
const updateData: UpdateTaskInput = {
title: title.trim(),
description: description.trim() || null,
dueDate: dueDate || null,
dueTime: dueTime || null,
priority,
};
const result = await todosStore.updateTodo(task.id, updateData);
if (result.error) {
toast.error(`Fehler beim Speichern: ${result.error.message}`);
} else if (result.data) {
task = result.data;
toast.success('Aufgabe aktualisiert');
isEditing = false;
}
isSaving = false;
}
async function handleDelete() {
if (!confirm('Möchten Sie diese Aufgabe wirklich löschen?')) {
return;
}
isDeleting = true;
const result = await todosStore.deleteTodo(task.id);
if (result.error) {
toast.error(`Fehler beim Löschen: ${result.error.message}`);
isDeleting = false;
} else {
toast.success('Aufgabe gelöscht');
onClose();
}
}
function startEditing() {
// Reset form state to current task values
title = task.title;
description = task.description || '';
dueDate = task.dueDate ? formatDateForInput(task.dueDate) : '';
dueTime = task.dueTime || '';
priority = task.priority;
isEditing = true;
}
function cancelEditing() {
isEditing = false;
}
function handleBackdropClick(e: MouseEvent) {
if (e.target === e.currentTarget) {
onClose();
}
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
if (isEditing) {
cancelEditing();
} else {
onClose();
}
}
}
</script>
<svelte:window onkeydown={handleKeydown} />
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<div class="modal-backdrop" onclick={handleBackdropClick}>
<div class="modal" role="dialog" aria-labelledby="modal-title" aria-modal="true">
<!-- Header -->
<div class="modal-header">
<div class="header-left">
<TodoCheckbox
checked={task.isCompleted}
loading={isToggling}
size="lg"
onchange={handleToggleComplete}
/>
{#if !isEditing}
<h2 id="modal-title" class="modal-title" class:completed={task.isCompleted}>
{task.title}
</h2>
{/if}
</div>
<button type="button" class="close-button" onclick={onClose} aria-label="Schließen">
<X size={20} />
</button>
</div>
<!-- Content -->
<div class="modal-content">
{#if isEditing}
<!-- Edit Mode -->
<form
class="edit-form"
onsubmit={(e) => {
e.preventDefault();
handleSave();
}}
>
<div class="form-group">
<label for="title">Titel</label>
<input
id="title"
type="text"
bind:value={title}
placeholder="Aufgabentitel"
required
autofocus
/>
</div>
<div class="form-group">
<label for="description">Beschreibung</label>
<textarea
id="description"
bind:value={description}
placeholder="Beschreibung hinzufügen..."
rows="3"
></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="dueDate">Fälligkeitsdatum</label>
<input id="dueDate" type="date" bind:value={dueDate} />
</div>
<div class="form-group">
<label for="dueTime">Uhrzeit</label>
<input id="dueTime" type="time" bind:value={dueTime} />
</div>
</div>
<div class="form-group">
<label>Priorität</label>
<div class="priority-options">
{#each Object.entries(PRIORITY_LABELS) as [key, label]}
<button
type="button"
class="priority-option"
class:selected={priority === key}
style="--priority-color: {PRIORITY_COLORS[key as TaskPriority]};"
onclick={() => (priority = key as TaskPriority)}
>
<span class="priority-dot"></span>
{label}
</button>
{/each}
</div>
</div>
</form>
{:else}
<!-- View Mode -->
<div class="detail-section">
{#if task.description}
<p class="description">{task.description}</p>
{/if}
<div class="detail-list">
<div class="detail-item">
<Calendar size={16} />
<span>{formatDisplayDate(task.dueDate)}</span>
</div>
{#if task.dueTime}
<div class="detail-item">
<Clock size={16} />
<span>{task.dueTime} Uhr</span>
</div>
{/if}
<div class="detail-item">
<AlertCircle size={16} />
<PriorityBadge {priority} variant="pill" showLabel />
</div>
{#if task.project}
<div class="detail-item">
<Folder size={16} />
<span class="project-name" style="color: {task.project.color};">
{task.project.name}
</span>
</div>
{/if}
{#if task.labels && task.labels.length > 0}
<div class="detail-item labels-row">
<Tag size={16} />
<div class="labels">
{#each task.labels as label}
<span class="label-tag" style="--label-color: {label.color};">
{label.name}
</span>
{/each}
</div>
</div>
{/if}
</div>
{#if task.subtasks && task.subtasks.length > 0}
<div class="subtasks-section">
<h3>
<CheckSquare size={16} />
Unteraufgaben ({task.subtasks.filter((s) => s.isCompleted).length}/{task.subtasks
.length})
</h3>
<ul class="subtask-list">
{#each task.subtasks as subtask}
<li class:completed={subtask.isCompleted}>
<span class="subtask-check">{subtask.isCompleted ? '☑' : '☐'}</span>
{subtask.title}
</li>
{/each}
</ul>
</div>
{/if}
</div>
{/if}
</div>
<!-- Footer -->
<div class="modal-footer">
{#if isEditing}
<button type="button" class="btn btn-secondary" onclick={cancelEditing} disabled={isSaving}>
Abbrechen
</button>
<button type="button" class="btn btn-primary" onclick={handleSave} disabled={isSaving}>
{#if isSaving}
Speichern...
{:else}
Speichern
{/if}
</button>
{:else}
<button type="button" class="btn btn-danger" onclick={handleDelete} disabled={isDeleting}>
<Trash2 size={16} />
{#if isDeleting}
Löschen...
{:else}
Löschen
{/if}
</button>
<button type="button" class="btn btn-primary" onclick={startEditing}> Bearbeiten </button>
{/if}
</div>
</div>
</div>
<style>
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
padding: 1rem;
}
.modal {
width: 100%;
max-width: 500px;
max-height: 90vh;
background: hsl(var(--color-background));
border-radius: var(--radius-lg);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
display: flex;
flex-direction: column;
overflow: hidden;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.25rem;
border-bottom: 1px solid hsl(var(--color-border));
}
.header-left {
display: flex;
align-items: center;
gap: 0.75rem;
flex: 1;
min-width: 0;
}
.modal-title {
font-size: 1.125rem;
font-weight: 600;
color: hsl(var(--color-foreground));
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.modal-title.completed {
text-decoration: line-through;
color: hsl(var(--color-muted-foreground));
}
.close-button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: var(--radius-md);
border: none;
background: transparent;
color: hsl(var(--color-muted-foreground));
cursor: pointer;
transition: all 150ms ease;
}
.close-button:hover {
background: hsl(var(--color-muted));
color: hsl(var(--color-foreground));
}
.modal-content {
flex: 1;
overflow-y: auto;
padding: 1.25rem;
}
/* View Mode */
.description {
font-size: 0.875rem;
color: hsl(var(--color-muted-foreground));
line-height: 1.6;
margin: 0 0 1rem;
white-space: pre-wrap;
}
.detail-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.detail-item {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 0.875rem;
color: hsl(var(--color-foreground));
}
.detail-item :global(svg) {
color: hsl(var(--color-muted-foreground));
flex-shrink: 0;
}
.labels-row {
align-items: flex-start;
}
.labels {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
}
.label-tag {
font-size: 0.75rem;
color: var(--label-color);
background: color-mix(in srgb, var(--label-color) 15%, transparent);
padding: 2px 8px;
border-radius: 9999px;
}
.subtasks-section {
margin-top: 1.25rem;
padding-top: 1rem;
border-top: 1px solid hsl(var(--color-border));
}
.subtasks-section h3 {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
color: hsl(var(--color-foreground));
margin: 0 0 0.75rem;
}
.subtask-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.subtask-list li {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.8125rem;
color: hsl(var(--color-foreground));
}
.subtask-list li.completed {
color: hsl(var(--color-muted-foreground));
text-decoration: line-through;
}
.subtask-check {
font-size: 0.875rem;
}
/* Edit Mode */
.edit-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.form-group label {
font-size: 0.8125rem;
font-weight: 500;
color: hsl(var(--color-foreground));
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
input[type='text'],
input[type='date'],
input[type='time'],
textarea {
padding: 0.5rem 0.75rem;
border: 1px solid hsl(var(--color-border));
border-radius: var(--radius-md);
background: hsl(var(--color-surface));
color: hsl(var(--color-foreground));
font-size: 0.875rem;
transition: border-color 150ms ease;
}
input:focus,
textarea:focus {
outline: none;
border-color: hsl(var(--color-primary));
}
textarea {
resize: vertical;
min-height: 80px;
}
.priority-options {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.priority-option {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.75rem;
border-radius: var(--radius-md);
border: 1px solid hsl(var(--color-border));
background: transparent;
color: hsl(var(--color-foreground));
font-size: 0.8125rem;
cursor: pointer;
transition: all 150ms ease;
}
.priority-option:hover {
border-color: var(--priority-color);
}
.priority-option.selected {
border-color: var(--priority-color);
background: color-mix(in srgb, var(--priority-color) 15%, transparent);
}
.priority-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--priority-color);
}
/* Footer */
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
padding: 1rem 1.25rem;
border-top: 1px solid hsl(var(--color-border));
}
.btn {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 1rem;
border-radius: var(--radius-md);
border: none;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 150ms ease;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-primary {
background: hsl(var(--color-primary));
color: hsl(var(--color-primary-foreground));
}
.btn-primary:hover:not(:disabled) {
background: hsl(var(--color-primary) / 0.9);
}
.btn-secondary {
background: hsl(var(--color-muted));
color: hsl(var(--color-foreground));
}
.btn-secondary:hover:not(:disabled) {
background: hsl(var(--color-muted) / 0.8);
}
.btn-danger {
background: hsl(var(--color-danger) / 0.1);
color: hsl(var(--color-danger));
}
.btn-danger:hover:not(:disabled) {
background: hsl(var(--color-danger));
color: white;
}
</style>

View file

@ -60,6 +60,7 @@
onclick: () => viewStore.goToToday(),
},
{ id: 'agenda', label: 'Agenda anzeigen', icon: 'list', href: '/agenda' },
{ id: 'tasks', label: 'Aufgaben anzeigen', icon: 'check-square', href: '/tasks' },
{ id: 'settings', label: 'Einstellungen', icon: 'settings', href: '/settings' },
];
@ -183,6 +184,7 @@
const baseNavItems: PillNavItem[] = [
{ href: '/', label: 'Kalender', icon: 'calendar' },
{ href: '/agenda', label: 'Agenda', icon: 'list' },
{ href: '/tasks', label: 'Aufgaben', icon: 'check-square' },
{ href: '/tags', label: 'Tags', icon: 'tag' },
{ href: '/statistics', label: 'Statistiken', icon: 'bar-chart-3' },
{ href: '/network', label: 'Netzwerk', icon: 'share-2' },

View file

@ -0,0 +1,486 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { authStore } from '$lib/stores/auth.svelte';
import { eventsStore } from '$lib/stores/events.svelte';
import { todosStore } from '$lib/stores/todos.svelte';
import type { Task } from '$lib/api/todos';
import type { CalendarEvent } from '@calendar/shared';
import AgendaItem from '$lib/components/agenda/AgendaItem.svelte';
import AgendaFilters from '$lib/components/agenda/AgendaFilters.svelte';
import TodoDetailModal from '$lib/components/todo/TodoDetailModal.svelte';
import QuickAddTodo from '$lib/components/todo/QuickAddTodo.svelte';
import { AgendaSkeleton } from '$lib/components/skeletons';
import {
format,
parseISO,
isToday,
isTomorrow,
addDays,
startOfDay,
endOfDay,
isBefore,
} from 'date-fns';
import { de } from 'date-fns/locale';
import { CheckSquare, AlertTriangle, Plus } from 'lucide-svelte';
// State
let loading = $state(true);
let showEvents = $state(true);
let showTodos = $state(true);
let timeRange = $state<'7' | '30' | 'all'>('30');
let selectedTask = $state<Task | null>(null);
let showQuickAdd = $state(false);
// Combined and grouped items
type AgendaGroup = {
date: Date;
items: Array<{ type: 'event' | 'todo'; event?: CalendarEvent; todo?: Task }>;
};
let groupedItems = $derived.by(() => {
const groups = new Map<string, AgendaGroup['items']>();
const today = startOfDay(new Date());
// Add events
if (showEvents) {
const currentEvents = eventsStore.events ?? [];
if (Array.isArray(currentEvents)) {
for (const event of currentEvents) {
const start =
typeof event.startTime === 'string' ? parseISO(event.startTime) : event.startTime;
const dateKey = format(start, 'yyyy-MM-dd');
if (!groups.has(dateKey)) {
groups.set(dateKey, []);
}
groups.get(dateKey)!.push({ type: 'event', event });
}
}
}
// Add todos
if (showTodos) {
const currentTodos = todosStore.todos ?? [];
if (Array.isArray(currentTodos)) {
for (const todo of currentTodos) {
if (todo.isCompleted) continue; // Skip completed todos
let dateKey: string;
if (todo.dueDate) {
const dueDate =
typeof todo.dueDate === 'string' ? parseISO(todo.dueDate) : todo.dueDate;
// Group overdue todos under today
if (isBefore(startOfDay(dueDate), today)) {
dateKey = format(today, 'yyyy-MM-dd');
} else {
dateKey = format(dueDate, 'yyyy-MM-dd');
}
} else {
// Todos without due date go under today
dateKey = format(today, 'yyyy-MM-dd');
}
if (!groups.has(dateKey)) {
groups.set(dateKey, []);
}
groups.get(dateKey)!.push({ type: 'todo', todo });
}
}
}
// Sort groups by date and items within each group
return Array.from(groups.entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([dateKey, items]) => ({
date: parseISO(dateKey),
items: items.sort((a, b) => {
// Todos before events
if (a.type !== b.type) return a.type === 'todo' ? -1 : 1;
// Sort events by time
if (a.type === 'event' && b.type === 'event' && a.event && b.event) {
const aStart =
typeof a.event.startTime === 'string'
? parseISO(a.event.startTime)
: a.event.startTime;
const bStart =
typeof b.event.startTime === 'string'
? parseISO(b.event.startTime)
: b.event.startTime;
return aStart.getTime() - bStart.getTime();
}
// Sort todos by priority
if (a.type === 'todo' && b.type === 'todo' && a.todo && b.todo) {
const priorityOrder = { urgent: 0, high: 1, medium: 2, low: 3 };
return priorityOrder[a.todo.priority] - priorityOrder[b.todo.priority];
}
return 0;
}),
}));
});
// Stats
const overdueCount = $derived(todosStore.overdueTodos.length);
const todayCount = $derived(todosStore.todaysTodos.length);
const totalActiveCount = $derived(todosStore.activeTodosCount);
onMount(async () => {
if (!authStore.isAuthenticated) {
goto('/login');
return;
}
// Fetch data based on time range
await fetchData();
loading = false;
});
async function fetchData() {
const start = startOfDay(new Date());
const days = timeRange === '7' ? 7 : timeRange === '30' ? 30 : 90;
const end = endOfDay(addDays(start, days));
await Promise.all([
eventsStore.fetchEvents(start, end),
todosStore.fetchTodos(start, end),
todosStore.fetchTodayTodos(),
]);
}
function formatDateHeader(date: Date) {
if (isToday(date)) {
return 'Heute';
}
if (isTomorrow(date)) {
return 'Morgen';
}
return format(date, 'EEEE, d. MMMM', { locale: de });
}
function handleEventClick(eventId: string) {
goto(`/?event=${eventId}`);
}
function handleTodoClick(task: Task) {
selectedTask = task;
}
function handleModalClose() {
selectedTask = null;
}
function toggleEvents() {
showEvents = !showEvents;
}
function toggleTodos() {
showTodos = !showTodos;
}
function handleRangeChange(range: '7' | '30' | 'all') {
timeRange = range;
loading = true;
fetchData().then(() => (loading = false));
}
</script>
<svelte:head>
<title>Aufgaben | Kalender</title>
</svelte:head>
<div class="tasks-page">
<header class="page-header">
<div class="header-content">
<div class="header-icon">
<CheckSquare size={24} />
</div>
<div>
<h1>Aufgaben</h1>
<p class="subtitle">Ihre Termine und Aufgaben auf einen Blick</p>
</div>
</div>
<!-- Stats -->
<div class="stats">
{#if overdueCount > 0}
<span class="stat overdue">
<AlertTriangle size={14} />
{overdueCount} überfällig
</span>
{/if}
<span class="stat">{todayCount} heute</span>
<span class="stat">{totalActiveCount} gesamt</span>
</div>
</header>
<!-- Filters -->
<AgendaFilters
{showEvents}
{showTodos}
{timeRange}
onToggleEvents={toggleEvents}
onToggleTodos={toggleTodos}
onRangeChange={handleRangeChange}
/>
<!-- Quick Add -->
<div class="quick-add-section">
{#if showQuickAdd}
<QuickAddTodo
placeholder="Neue Aufgabe hinzufügen..."
autofocus
showButton={false}
onsubmit={() => (showQuickAdd = false)}
oncancel={() => (showQuickAdd = false)}
/>
{:else}
<button type="button" class="quick-add-button" onclick={() => (showQuickAdd = true)}>
<Plus size={16} />
<span>Neue Aufgabe</span>
</button>
{/if}
</div>
<!-- Content -->
{#if loading}
<AgendaSkeleton />
{:else if !todosStore.serviceAvailable}
<div class="error-state card">
<AlertTriangle size={24} />
<p>Todo-Service ist nicht erreichbar</p>
<p class="hint">Bitte versuchen Sie es später erneut</p>
</div>
{:else if groupedItems.length === 0}
<div class="empty-state card">
<CheckSquare size={32} />
<p>Keine Einträge gefunden</p>
<p class="hint">
{#if !showEvents && !showTodos}
Aktivieren Sie mindestens einen Filter
{:else}
Erstellen Sie eine neue Aufgabe oder ändern Sie den Zeitraum
{/if}
</p>
</div>
{:else}
<div class="item-list">
{#each groupedItems as group}
<div class="date-group">
<h2 class="date-header" class:today={isToday(group.date)}>
{formatDateHeader(group.date)}
<span class="item-count">({group.items.length})</span>
</h2>
<div class="items">
{#each group.items as item}
{#if item.type === 'event' && item.event}
<AgendaItem
type="event"
event={item.event}
onclick={() => handleEventClick(item.event!.id)}
/>
{:else if item.type === 'todo' && item.todo}
<AgendaItem
type="todo"
todo={item.todo}
onclick={() => handleTodoClick(item.todo!)}
/>
{/if}
{/each}
</div>
</div>
{/each}
</div>
{/if}
</div>
<!-- Detail Modal -->
{#if selectedTask}
<TodoDetailModal task={selectedTask} onClose={handleModalClose} />
{/if}
<style>
.tasks-page {
max-width: 700px;
margin: 0 auto;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.page-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
margin-bottom: 0.5rem;
}
.header-content {
display: flex;
align-items: center;
gap: 0.75rem;
}
.header-icon {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
border-radius: var(--radius-lg);
background: hsl(var(--color-primary) / 0.1);
color: hsl(var(--color-primary));
}
h1 {
font-size: 1.5rem;
font-weight: 600;
color: hsl(var(--color-foreground));
margin: 0;
}
.subtitle {
font-size: 0.875rem;
color: hsl(var(--color-muted-foreground));
margin: 0.25rem 0 0;
}
.stats {
display: flex;
align-items: center;
gap: 0.75rem;
}
.stat {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.75rem;
color: hsl(var(--color-muted-foreground));
padding: 0.25rem 0.5rem;
background: hsl(var(--color-muted) / 0.5);
border-radius: var(--radius-sm);
}
.stat.overdue {
color: hsl(var(--color-danger));
background: hsl(var(--color-danger) / 0.1);
}
.quick-add-section {
margin-bottom: 0.5rem;
}
.quick-add-button {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
width: 100%;
padding: 0.75rem;
border-radius: var(--radius-lg);
border: 1px dashed hsl(var(--color-border));
background: transparent;
color: hsl(var(--color-muted-foreground));
font-size: 0.875rem;
cursor: pointer;
transition: all 150ms ease;
}
.quick-add-button:hover {
border-color: hsl(var(--color-primary));
color: hsl(var(--color-primary));
background: hsl(var(--color-primary) / 0.05);
}
.item-list {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.date-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.date-header {
font-size: 0.8125rem;
font-weight: 600;
color: hsl(var(--color-muted-foreground));
text-transform: uppercase;
letter-spacing: 0.05em;
display: flex;
align-items: center;
gap: 0.5rem;
}
.date-header.today {
color: hsl(var(--color-primary));
}
.item-count {
font-weight: 400;
font-size: 0.75rem;
}
.items {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.empty-state,
.error-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.75rem;
padding: 3rem 1.5rem;
text-align: center;
color: hsl(var(--color-muted-foreground));
}
.empty-state :global(svg),
.error-state :global(svg) {
opacity: 0.5;
}
.error-state {
color: hsl(var(--color-danger));
}
.hint {
font-size: 0.8125rem;
opacity: 0.7;
margin: 0;
}
.card {
background: hsl(var(--color-surface));
border-radius: var(--radius-lg);
border: 1px solid hsl(var(--color-border));
}
@media (max-width: 640px) {
.tasks-page {
padding: 1rem;
}
.page-header {
flex-direction: column;
align-items: stretch;
}
.stats {
justify-content: flex-start;
}
}
</style>