mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:01:09 +02:00
feat(calendar): integrate todo tasks into calendar views
Add todo items display in Day/Week/Month views and sidebar section. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a898160423
commit
f3c567f56e
7 changed files with 640 additions and 0 deletions
|
|
@ -3,6 +3,8 @@
|
|||
import { eventsStore } from '$lib/stores/events.svelte';
|
||||
import { calendarsStore } from '$lib/stores/calendars.svelte';
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { todosStore } from '$lib/stores/todos.svelte';
|
||||
import TodoRow from './TodoRow.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import {
|
||||
format,
|
||||
|
|
@ -405,6 +407,16 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Todos section -->
|
||||
{#if todosStore.serviceAvailable && todosStore.getTodosForDay(viewStore.currentDate).length > 0}
|
||||
<div class="todos-section">
|
||||
<div class="time-gutter"></div>
|
||||
<div class="todos-content">
|
||||
<TodoRow date={viewStore.currentDate} maxVisible={4} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Time grid -->
|
||||
<div class="time-grid scrollbar-thin">
|
||||
<div class="time-column">
|
||||
|
|
@ -533,6 +545,16 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Todos section */
|
||||
.todos-section {
|
||||
display: flex;
|
||||
border-bottom: 1px solid hsl(var(--color-border) / 0.5);
|
||||
}
|
||||
|
||||
.todos-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Block-style all-day events (displayed as full-day blocks in the grid) */
|
||||
.all-day-block-event {
|
||||
position: absolute;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
import { eventsStore } from '$lib/stores/events.svelte';
|
||||
import { calendarsStore } from '$lib/stores/calendars.svelte';
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { todosStore } from '$lib/stores/todos.svelte';
|
||||
import TodoDayCell from './TodoDayCell.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import {
|
||||
format,
|
||||
|
|
@ -265,6 +267,11 @@
|
|||
{format(day, 'd')}
|
||||
</span>
|
||||
|
||||
<!-- Todos for this day -->
|
||||
{#if todosStore.serviceAvailable}
|
||||
<TodoDayCell date={day} maxVisible={2} />
|
||||
{/if}
|
||||
|
||||
<div class="day-events">
|
||||
{#each getEventsForDay(day) as event}
|
||||
{@const isBeingDragged = isDragging && draggedEvent?.id === event.id}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
<script lang="ts">
|
||||
import { todosStore } from '$lib/stores/todos.svelte';
|
||||
import type { Task } from '$lib/api/todos';
|
||||
import { PRIORITY_COLORS } from '$lib/api/todos';
|
||||
import TodoCheckbox from '$lib/components/todo/TodoCheckbox.svelte';
|
||||
import TodoDetailModal from '$lib/components/todo/TodoDetailModal.svelte';
|
||||
|
||||
interface Props {
|
||||
date: Date;
|
||||
maxVisible?: number;
|
||||
}
|
||||
|
||||
let { date, maxVisible = 2 }: Props = $props();
|
||||
|
||||
let selectedTask = $state<Task | null>(null);
|
||||
let togglingIds = $state<Set<string>>(new Set());
|
||||
|
||||
const todosForDay = $derived(todosStore.getTodosForDay(date));
|
||||
const visibleTodos = $derived(todosForDay.slice(0, maxVisible));
|
||||
const overflowCount = $derived(Math.max(0, todosForDay.length - maxVisible));
|
||||
|
||||
async function handleToggle(task: Task, e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
togglingIds = new Set([...togglingIds, task.id]);
|
||||
await todosStore.toggleComplete(task.id);
|
||||
togglingIds = new Set([...togglingIds].filter((id) => id !== task.id));
|
||||
}
|
||||
|
||||
function handleTaskClick(task: Task) {
|
||||
selectedTask = task;
|
||||
}
|
||||
|
||||
function handleModalClose() {
|
||||
selectedTask = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if todosForDay.length > 0}
|
||||
<div class="todo-day-cell">
|
||||
{#each visibleTodos as task (task.id)}
|
||||
<button
|
||||
type="button"
|
||||
class="todo-cell-item"
|
||||
class:completed={task.isCompleted}
|
||||
style="--priority-color: {PRIORITY_COLORS[task.priority]};"
|
||||
onclick={() => handleTaskClick(task)}
|
||||
>
|
||||
<span class="priority-dot"></span>
|
||||
<span class="todo-cell-title">{task.title}</span>
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
{#if overflowCount > 0}
|
||||
<span class="overflow-text">+{overflowCount} Aufgaben</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Detail Modal -->
|
||||
{#if selectedTask}
|
||||
<TodoDetailModal task={selectedTask} onClose={handleModalClose} />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.todo-day-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.todo-cell-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
border: none;
|
||||
background: hsl(var(--color-muted) / 0.3);
|
||||
cursor: pointer;
|
||||
transition: background 150ms ease;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.todo-cell-item:hover {
|
||||
background: hsl(var(--color-muted) / 0.5);
|
||||
}
|
||||
|
||||
.todo-cell-item.completed {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.todo-cell-item.completed .todo-cell-title {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.priority-dot {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: var(--priority-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.todo-cell-title {
|
||||
font-size: 0.625rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--color-foreground));
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.overflow-text {
|
||||
font-size: 0.5625rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
padding: 0 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
<script lang="ts">
|
||||
import { todosStore } from '$lib/stores/todos.svelte';
|
||||
import type { Task } from '$lib/api/todos';
|
||||
import { PRIORITY_COLORS } from '$lib/api/todos';
|
||||
import TodoCheckbox from '$lib/components/todo/TodoCheckbox.svelte';
|
||||
import TodoDetailModal from '$lib/components/todo/TodoDetailModal.svelte';
|
||||
import { Check } from 'lucide-svelte';
|
||||
|
||||
interface Props {
|
||||
date: Date;
|
||||
maxVisible?: number;
|
||||
}
|
||||
|
||||
let { date, maxVisible = 3 }: Props = $props();
|
||||
|
||||
let selectedTask = $state<Task | null>(null);
|
||||
let togglingIds = $state<Set<string>>(new Set());
|
||||
|
||||
const todosForDay = $derived(todosStore.getTodosForDay(date));
|
||||
const visibleTodos = $derived(todosForDay.slice(0, maxVisible));
|
||||
const overflowCount = $derived(Math.max(0, todosForDay.length - maxVisible));
|
||||
|
||||
async function handleToggle(task: Task) {
|
||||
togglingIds = new Set([...togglingIds, task.id]);
|
||||
await todosStore.toggleComplete(task.id);
|
||||
togglingIds = new Set([...togglingIds].filter((id) => id !== task.id));
|
||||
}
|
||||
|
||||
function handleTaskClick(task: Task, e: MouseEvent) {
|
||||
// Don't open modal if clicking checkbox
|
||||
if ((e.target as HTMLElement).closest('.todo-checkbox')) return;
|
||||
selectedTask = task;
|
||||
}
|
||||
|
||||
function handleModalClose() {
|
||||
selectedTask = null;
|
||||
}
|
||||
|
||||
function handleShowAll() {
|
||||
// Show first todo's modal, or navigate to tasks page
|
||||
if (todosForDay.length > 0) {
|
||||
selectedTask = todosForDay[0];
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if todosForDay.length > 0}
|
||||
<div class="todo-row">
|
||||
<span class="todo-row-label">Aufgaben:</span>
|
||||
<div class="todo-pills">
|
||||
{#each visibleTodos as task (task.id)}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
||||
<button
|
||||
type="button"
|
||||
class="todo-pill"
|
||||
class:completed={task.isCompleted}
|
||||
style="--priority-color: {PRIORITY_COLORS[task.priority]};"
|
||||
onclick={(e) => handleTaskClick(task, e)}
|
||||
>
|
||||
<TodoCheckbox
|
||||
checked={task.isCompleted}
|
||||
loading={togglingIds.has(task.id)}
|
||||
size="sm"
|
||||
onchange={() => handleToggle(task)}
|
||||
/>
|
||||
<span class="todo-pill-title">{task.title}</span>
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
{#if overflowCount > 0}
|
||||
<button type="button" class="overflow-badge" onclick={handleShowAll}>
|
||||
+{overflowCount} mehr
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Detail Modal -->
|
||||
{#if selectedTask}
|
||||
<TodoDetailModal task={selectedTask} onClose={handleModalClose} />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.todo-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.375rem 0.5rem;
|
||||
background: hsl(var(--color-muted) / 0.2);
|
||||
border-bottom: 1px solid hsl(var(--color-border) / 0.5);
|
||||
}
|
||||
|
||||
.todo-row-label {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.todo-pills {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.todo-pills::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.todo-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: var(--radius-md);
|
||||
border: none;
|
||||
background: hsl(var(--color-surface));
|
||||
border-left: 2px solid var(--priority-color);
|
||||
cursor: pointer;
|
||||
transition: all 150ms ease;
|
||||
flex-shrink: 0;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.todo-pill:hover {
|
||||
background: hsl(var(--color-muted) / 0.5);
|
||||
}
|
||||
|
||||
.todo-pill.completed {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.todo-pill.completed .todo-pill-title {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.todo-pill-title {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--color-foreground));
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.overflow-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: var(--radius-md);
|
||||
border: none;
|
||||
background: hsl(var(--color-primary) / 0.1);
|
||||
color: hsl(var(--color-primary));
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: background 150ms ease;
|
||||
}
|
||||
|
||||
.overflow-badge:hover {
|
||||
background: hsl(var(--color-primary) / 0.2);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,292 @@
|
|||
<script lang="ts">
|
||||
import { todosStore } from '$lib/stores/todos.svelte';
|
||||
import type { Task } from '$lib/api/todos';
|
||||
import TodoItem from '$lib/components/todo/TodoItem.svelte';
|
||||
import TodoDetailModal from '$lib/components/todo/TodoDetailModal.svelte';
|
||||
import QuickAddTodo from '$lib/components/todo/QuickAddTodo.svelte';
|
||||
import { ChevronDown, ChevronRight, Plus, CheckSquare, AlertTriangle } from 'lucide-svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
maxItems?: number;
|
||||
}
|
||||
|
||||
let { maxItems = 5 }: Props = $props();
|
||||
|
||||
let isExpanded = $state(true);
|
||||
let showQuickAdd = $state(false);
|
||||
let selectedTask = $state<Task | null>(null);
|
||||
|
||||
// Derived: combined overdue + today todos
|
||||
const displayTodos = $derived(todosStore.getSidebarTodos(maxItems));
|
||||
const overdueCount = $derived(todosStore.overdueTodos.length);
|
||||
const totalActiveCount = $derived(todosStore.activeTodosCount);
|
||||
|
||||
onMount(async () => {
|
||||
// Fetch todos on mount
|
||||
await todosStore.fetchTodayTodos();
|
||||
await todosStore.fetchUpcomingTodos();
|
||||
});
|
||||
|
||||
function toggleExpanded() {
|
||||
isExpanded = !isExpanded;
|
||||
}
|
||||
|
||||
function handleAddClick(e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
showQuickAdd = true;
|
||||
}
|
||||
|
||||
function handleTaskClick(task: Task) {
|
||||
selectedTask = task;
|
||||
}
|
||||
|
||||
function handleModalClose() {
|
||||
selectedTask = null;
|
||||
}
|
||||
|
||||
function handleQuickAddSubmit() {
|
||||
// Keep quick add open for successive adds
|
||||
}
|
||||
|
||||
function handleQuickAddCancel() {
|
||||
showQuickAdd = false;
|
||||
}
|
||||
|
||||
function goToAllTasks() {
|
||||
goto('/tasks');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="todo-sidebar-section">
|
||||
<!-- Header -->
|
||||
<button type="button" class="section-header" onclick={toggleExpanded}>
|
||||
<div class="header-left">
|
||||
{#if isExpanded}
|
||||
<ChevronDown size={16} />
|
||||
{:else}
|
||||
<ChevronRight size={16} />
|
||||
{/if}
|
||||
<CheckSquare size={16} class="section-icon" />
|
||||
<span class="section-title">Aufgaben</span>
|
||||
{#if totalActiveCount > 0}
|
||||
<span class="count-badge">{totalActiveCount}</span>
|
||||
{/if}
|
||||
{#if overdueCount > 0}
|
||||
<span class="overdue-badge" title="{overdueCount} überfällig">
|
||||
<AlertTriangle size={12} />
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="add-button"
|
||||
onclick={handleAddClick}
|
||||
aria-label="Aufgabe hinzufügen"
|
||||
>
|
||||
<Plus size={16} />
|
||||
</button>
|
||||
</button>
|
||||
|
||||
<!-- Content -->
|
||||
{#if isExpanded}
|
||||
<div class="section-content">
|
||||
{#if !todosStore.serviceAvailable}
|
||||
<div class="service-unavailable">
|
||||
<AlertTriangle size={16} />
|
||||
<span>Todo-Service nicht erreichbar</span>
|
||||
</div>
|
||||
{:else if todosStore.loading}
|
||||
<div class="loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<span>Laden...</span>
|
||||
</div>
|
||||
{:else if displayTodos.length === 0}
|
||||
<div class="empty-state">
|
||||
<CheckSquare size={20} />
|
||||
<span>Keine offenen Aufgaben</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="todo-list">
|
||||
{#each displayTodos as task (task.id)}
|
||||
<TodoItem
|
||||
{task}
|
||||
variant="compact"
|
||||
showProject={false}
|
||||
onclick={() => handleTaskClick(task)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if totalActiveCount > maxItems}
|
||||
<button type="button" class="show-all-button" onclick={goToAllTasks}>
|
||||
Alle {totalActiveCount} anzeigen
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Quick Add -->
|
||||
{#if showQuickAdd}
|
||||
<div class="quick-add-wrapper">
|
||||
<QuickAddTodo
|
||||
placeholder="Neue Aufgabe..."
|
||||
autofocus
|
||||
showButton={false}
|
||||
onsubmit={handleQuickAddSubmit}
|
||||
oncancel={handleQuickAddCancel}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Detail Modal -->
|
||||
{#if selectedTask}
|
||||
<TodoDetailModal task={selectedTask} onClose={handleModalClose} />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.todo-sidebar-section {
|
||||
background: hsl(var(--color-surface));
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition: background 150ms ease;
|
||||
}
|
||||
|
||||
.section-header:hover {
|
||||
background: hsl(var(--color-muted) / 0.3);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
.header-left :global(svg) {
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.header-left :global(.section-icon) {
|
||||
color: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.count-badge {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
background: hsl(var(--color-primary) / 0.15);
|
||||
color: hsl(var(--color-primary));
|
||||
padding: 1px 6px;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.overdue-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: hsl(var(--color-danger));
|
||||
}
|
||||
|
||||
.add-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: var(--radius-md);
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
cursor: pointer;
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
|
||||
.add-button:hover {
|
||||
background: hsl(var(--color-primary) / 0.15);
|
||||
color: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
.section-content {
|
||||
padding: 0 0.5rem 0.5rem;
|
||||
}
|
||||
|
||||
.todo-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.service-unavailable,
|
||||
.loading,
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1.5rem 1rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.service-unavailable {
|
||||
color: hsl(var(--color-danger));
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid hsl(var(--color-muted));
|
||||
border-top-color: hsl(var(--color-primary));
|
||||
border-radius: 50%;
|
||||
animation: spin 600ms linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.show-all-button {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: hsl(var(--color-primary));
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-md);
|
||||
transition: background 150ms ease;
|
||||
}
|
||||
|
||||
.show-all-button:hover {
|
||||
background: hsl(var(--color-primary) / 0.1);
|
||||
}
|
||||
|
||||
.quick-add-wrapper {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0 0.25rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
import { eventsStore } from '$lib/stores/events.svelte';
|
||||
import { calendarsStore } from '$lib/stores/calendars.svelte';
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { todosStore } from '$lib/stores/todos.svelte';
|
||||
import TodoRow from './TodoRow.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import {
|
||||
format,
|
||||
|
|
@ -499,6 +501,18 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Todos row (shown per day, below all-day events) -->
|
||||
{#if todosStore.serviceAvailable}
|
||||
<div class="todos-row">
|
||||
<div class="time-gutter"></div>
|
||||
{#each days as day}
|
||||
<div class="todos-cell">
|
||||
<TodoRow date={day} maxVisible={2} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Day headers -->
|
||||
<div class="day-headers">
|
||||
<div class="time-gutter"></div>
|
||||
|
|
@ -651,6 +665,18 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Todos row */
|
||||
.todos-row {
|
||||
display: flex;
|
||||
border-bottom: 1px solid hsl(var(--color-border) / 0.5);
|
||||
}
|
||||
|
||||
.todos-cell {
|
||||
flex: 1;
|
||||
border-left: 1px solid hsl(var(--color-border));
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Block-style all-day events (displayed as full-day blocks in the grid) */
|
||||
.all-day-block-event {
|
||||
position: absolute;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
import YearView from '$lib/components/calendar/YearView.svelte';
|
||||
import MiniCalendar from '$lib/components/calendar/MiniCalendar.svelte';
|
||||
import CalendarSidebar from '$lib/components/calendar/CalendarSidebar.svelte';
|
||||
import TodoSidebarSection from '$lib/components/calendar/TodoSidebarSection.svelte';
|
||||
import QuickEventOverlay from '$lib/components/event/QuickEventOverlay.svelte';
|
||||
import EventDetailModal from '$lib/components/event/EventDetailModal.svelte';
|
||||
import { CalendarViewSkeleton } from '$lib/components/skeletons';
|
||||
|
|
@ -130,6 +131,8 @@
|
|||
<MiniCalendar selectedDate={viewStore.currentDate} onDateSelect={handleDateSelect} />
|
||||
|
||||
<CalendarSidebar />
|
||||
|
||||
<TodoSidebarSection maxItems={5} />
|
||||
</aside>
|
||||
|
||||
<!-- FAB when sidebar is collapsed -->
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue