feat(calendar): improve calendar UI with new components

- Update CalendarHeader with better navigation
- Add PillCalendarSelector for calendar switching
- Improve DateStrip with view range highlighting
- Update CalendarToolbar with refined controls
- Remove separate event/new page (using modal instead)
- Adjust app.css for new layout

🤖 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-12 02:37:43 +01:00
parent 21f2b28bf0
commit b42db508a3
7 changed files with 495 additions and 510 deletions

View file

@ -42,18 +42,18 @@
/* Hour slot in day/week view */
.hour-slot {
height: var(--hour-height);
border-bottom: 1px solid hsl(var(--color-border) / 0.5);
border-bottom: 1px solid color-mix(in srgb, var(--color-border) 50%, transparent);
position: relative;
}
.hour-slot:hover {
background-color: hsl(var(--color-muted) / 0.3);
background-color: color-mix(in srgb, var(--color-muted) 30%, transparent);
}
/* Event card in calendar */
.event-card {
background-color: hsl(var(--color-primary));
color: hsl(var(--color-primary-foreground));
background-color: var(--color-primary);
color: var(--color-primary-foreground);
border-radius: var(--radius-sm);
padding: 2px 6px;
font-size: 0.75rem;
@ -70,17 +70,17 @@
/* Day cell in month view */
.day-cell {
min-height: 100px;
border: 1px solid hsl(var(--color-border));
border: 1px solid var(--color-border);
padding: var(--spacing-xs);
transition: background-color var(--transition-fast);
}
.day-cell:hover {
background-color: hsl(var(--color-muted) / 0.3);
background-color: color-mix(in srgb, var(--color-muted) 30%, transparent);
}
.day-cell.today {
background-color: hsl(var(--color-primary) / 0.1);
background-color: color-mix(in srgb, var(--color-primary) 10%, transparent);
}
.day-cell.other-month {
@ -93,7 +93,7 @@
left: 0;
right: 0;
height: 2px;
background-color: hsl(var(--color-error));
background-color: var(--color-error);
z-index: 10;
}
@ -105,7 +105,7 @@
width: 10px;
height: 10px;
border-radius: 50%;
background-color: hsl(var(--color-error));
background-color: var(--color-error);
}
/* Mini calendar */
@ -125,24 +125,24 @@
}
.mini-calendar .day:hover {
background-color: hsl(var(--color-muted));
background-color: var(--color-muted);
}
.mini-calendar .day.today {
background-color: hsl(var(--color-primary));
color: hsl(var(--color-primary-foreground));
background-color: var(--color-primary);
color: var(--color-primary-foreground);
}
.mini-calendar .day.selected {
border: 2px solid hsl(var(--color-primary));
border: 2px solid var(--color-primary);
}
/* Card styles */
.card {
background-color: hsl(var(--color-surface));
background-color: var(--color-surface);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
border: 1px solid hsl(var(--color-border));
border: 1px solid var(--color-border);
}
/* Button styles */
@ -161,12 +161,12 @@
}
.btn-primary {
background: hsl(var(--color-primary));
color: hsl(var(--color-primary-foreground));
background: var(--color-primary);
color: var(--color-primary-foreground);
}
.btn-primary:hover {
background: hsl(var(--color-primary) / 0.9);
filter: brightness(0.9);
}
.btn-primary:disabled {
@ -175,21 +175,21 @@
}
.btn-secondary {
background: hsl(var(--color-secondary));
color: hsl(var(--color-secondary-foreground));
background: var(--color-secondary);
color: var(--color-secondary-foreground);
}
.btn-secondary:hover {
background: hsl(var(--color-secondary) / 0.8);
filter: brightness(0.9);
}
.btn-ghost {
background: transparent;
color: hsl(var(--color-foreground));
color: var(--color-foreground);
}
.btn-ghost:hover {
background: hsl(var(--color-muted));
background: var(--color-muted);
}
.btn-icon {
@ -206,21 +206,21 @@
display: block;
width: 100%;
padding: 0.5rem 0.75rem;
border: 2px solid hsl(var(--color-border));
border: 2px solid var(--color-border);
border-radius: var(--radius-md);
background-color: hsl(var(--color-background));
color: hsl(var(--color-foreground));
background-color: var(--color-background);
color: var(--color-foreground);
font-size: 0.875rem;
transition: border-color var(--transition-fast);
}
.input:focus {
outline: none;
border-color: hsl(var(--color-primary));
border-color: var(--color-primary);
}
.input::placeholder {
color: hsl(var(--color-muted-foreground));
color: var(--color-muted-foreground);
}
/* Select styling */
@ -235,7 +235,7 @@ select.input {
/* Text colors */
.text-destructive {
color: hsl(var(--color-error));
color: var(--color-error);
}
/* Scrollbar styling */

View file

@ -1,50 +1,7 @@
<script lang="ts">
import { viewStore } from '$lib/stores/view.svelte';
import { settingsStore } from '$lib/stores/settings.svelte';
import { isNavCollapsed } from '$lib/stores/navigation';
import { format } from 'date-fns';
import { de } from 'date-fns/locale';
import type { CalendarViewType } from '@calendar/shared';
import { PillTimeRangeSelector, PillViewSwitcher } from '@manacore/shared-ui';
// View type labels
const viewLabels: Record<CalendarViewType, string> = {
day: 'Tag',
'5day': '5 Tage',
week: 'Woche',
'10day': '10 Tage',
'14day': '14 Tage',
month: 'Monat',
year: 'Jahr',
agenda: 'Agenda',
};
// Views to show in selector
const visibleViews: CalendarViewType[] = [
'day',
'5day',
'week',
'10day',
'14day',
'month',
'year',
];
// Convert to ViewOptions for PillViewSwitcher
const viewOptions = visibleViews.map((type) => ({
id: type,
label: viewLabels[type],
title: viewLabels[type],
}));
// Hours change handlers
function handleStartHourChange(hour: number) {
settingsStore.set('dayStartHour', hour);
}
function handleEndHourChange(hour: number) {
settingsStore.set('dayEndHour', hour);
}
// Format title based on view type
let title = $derived.by(() => {
@ -86,295 +43,28 @@
return format(date, 'MMMM yyyy', { locale: de });
}
});
function handleViewChange(type: string) {
viewStore.setViewType(type as CalendarViewType);
}
</script>
<header class="calendar-header" class:nav-collapsed={$isNavCollapsed}>
<div class="header-left">
<button
class="pill glass-pill today-btn"
onclick={() => viewStore.goToToday()}
title="Zum heutigen Tag springen"
>
Heute
</button>
<div class="nav-buttons glass-pill">
<button class="nav-btn" onclick={() => viewStore.goToPrevious()} aria-label="Zurück">
<svg class="nav-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 19l-7-7 7-7"
/>
</svg>
</button>
<div class="nav-divider"></div>
<button class="nav-btn" onclick={() => viewStore.goToNext()} aria-label="Weiter">
<svg class="nav-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
<h1 class="header-title">{title}</h1>
</div>
<div class="header-right">
<!-- Filter toggles as pills -->
<div class="filter-pills">
<!-- Weekdays only toggle -->
<button
class="pill glass-pill filter-pill"
class:active={settingsStore.showOnlyWeekdays}
onclick={() => settingsStore.set('showOnlyWeekdays', !settingsStore.showOnlyWeekdays)}
title="Nur Wochentage anzeigen (Mo-Fr)"
>
Mo-Fr
</button>
<!-- Hours filter toggle -->
<button
class="pill glass-pill filter-pill"
class:active={settingsStore.filterHoursEnabled}
onclick={() => settingsStore.set('filterHoursEnabled', !settingsStore.filterHoursEnabled)}
title="Stundenfilter ein/aus"
>
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</button>
<!-- Hours time range selector -->
<PillTimeRangeSelector
startHour={settingsStore.dayStartHour}
endHour={settingsStore.dayEndHour}
onStartHourChange={handleStartHourChange}
onEndHourChange={handleEndHourChange}
direction="down"
/>
</div>
<!-- View selector -->
<PillViewSwitcher
options={viewOptions}
value={viewStore.viewType}
onChange={handleViewChange}
primaryColor="#3b82f6"
/>
</div>
<header class="calendar-header">
<h1 class="header-title">{title}</h1>
</header>
<style>
.calendar-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
background: transparent;
transition: padding-left 300ms ease;
gap: 1rem;
flex-wrap: wrap;
}
.calendar-header.nav-collapsed {
padding-left: 4rem;
}
.header-left {
display: flex;
align-items: center;
gap: 0.75rem;
}
.header-title {
font-size: 1.125rem;
font-size: 1.25rem;
font-weight: 600;
color: hsl(var(--color-foreground));
margin: 0;
white-space: nowrap;
}
.header-right {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
}
/* Glass pill base styles */
.pill {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 0.875rem;
border-radius: 9999px;
font-size: 0.875rem;
font-weight: 500;
white-space: nowrap;
text-decoration: none;
transition: all 0.2s;
border: none;
cursor: pointer;
}
.glass-pill {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
color: #374151;
}
:global(.dark) .glass-pill {
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.15);
color: #f3f4f6;
}
.glass-pill:hover {
background: rgba(255, 255, 255, 0.95);
border-color: rgba(0, 0, 0, 0.15);
transform: translateY(-1px);
box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
:global(.dark) .glass-pill:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.25);
}
/* Today button */
.today-btn {
padding: 0.375rem 0.75rem;
font-size: 0.8125rem;
}
/* Navigation buttons group */
.nav-buttons {
display: flex;
align-items: center;
padding: 0;
gap: 0;
}
.nav-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 0.375rem 0.5rem;
background: transparent;
border: none;
cursor: pointer;
color: inherit;
transition: background 0.2s;
}
.nav-btn:first-child {
border-radius: 9999px 0 0 9999px;
}
.nav-btn:last-child {
border-radius: 0 9999px 9999px 0;
}
.nav-btn:hover {
background: rgba(0, 0, 0, 0.05);
}
:global(.dark) .nav-btn:hover {
background: rgba(255, 255, 255, 0.1);
}
.nav-icon {
width: 1rem;
height: 1rem;
}
.nav-divider {
width: 1px;
height: 1rem;
background: rgba(0, 0, 0, 0.15);
}
:global(.dark) .nav-divider {
background: rgba(255, 255, 255, 0.2);
}
/* Filter pills */
.filter-pills {
display: flex;
align-items: center;
gap: 0.5rem;
}
.filter-pill {
padding: 0.375rem 0.625rem;
font-size: 0.75rem;
}
.filter-pill.active {
background: color-mix(in srgb, #3b82f6 20%, white 80%);
border-color: #3b82f6;
color: #3b82f6;
}
:global(.dark) .filter-pill.active {
background: color-mix(in srgb, #3b82f6 30%, transparent 70%);
border-color: #3b82f6;
color: #3b82f6;
}
.pill-icon {
width: 0.875rem;
height: 0.875rem;
}
/* Responsive */
@media (max-width: 900px) {
.calendar-header {
flex-direction: column;
align-items: flex-start;
gap: 0.75rem;
padding: 0.75rem;
}
.header-left {
width: 100%;
justify-content: flex-start;
}
.header-right {
width: 100%;
justify-content: flex-start;
}
.header-title {
font-size: 1rem;
}
}
@media (max-width: 640px) {
.header-title {
font-size: 0.875rem;
}
.filter-pills {
flex-wrap: wrap;
font-size: 1rem;
}
}
</style>

View file

@ -9,6 +9,7 @@
PillTimeRangeSelector,
PillViewSwitcher,
} from '@manacore/shared-ui';
import PillCalendarSelector from './PillCalendarSelector.svelte';
// View type labels
const viewLabels: Record<CalendarViewType, string> = {
@ -55,6 +56,11 @@
</script>
<PillToolbar position="bottom" bottomOffset="70px">
<!-- Calendar selector -->
<PillCalendarSelector direction="up" embedded={true} />
<PillToolbarDivider />
<!-- Today button -->
<PillToolbarButton onclick={() => viewStore.goToToday()} title="Zum heutigen Tag springen">
Heute

View file

@ -120,6 +120,7 @@
if (!scrollContainer || isLoadingMore) return;
checkTodayVisibility();
updateVisibleMonth();
const { scrollLeft, clientWidth } = scrollContainer;
const dayWidth = 54;
@ -135,14 +136,28 @@
}
}
function getMonthLabel(day: Date, index: number): string | null {
if (day.getDate() === 1 || index === 0) {
if (day.getMonth() === 0 && day.getDate() === 1) {
return format(day, 'MMM yyyy', { locale: de });
// Get the month of the center visible day
let visibleMonth = $state(format(new Date(), 'MMMM yyyy', { locale: de }));
function updateVisibleMonth() {
if (!scrollContainer) return;
const containerRect = scrollContainer.getBoundingClientRect();
const centerX = containerRect.left + containerRect.width / 2;
// Find the day element closest to center
const dayElements = scrollContainer.querySelectorAll('.day-item');
for (const el of dayElements) {
const rect = el.getBoundingClientRect();
if (rect.left <= centerX && rect.right >= centerX) {
const dateStr = el.getAttribute('data-date');
if (dateStr) {
const date = new Date(dateStr);
visibleMonth = format(date, 'MMMM yyyy', { locale: de });
}
break;
}
return format(day, 'MMM', { locale: de });
}
return null;
}
onMount(() => {
@ -156,20 +171,20 @@
{/if}
<div class="date-strip-container">
<!-- Month label above the days -->
<div class="month-header">
<span class="month-label">{visibleMonth}</span>
</div>
<!-- Days row -->
<div class="days-scroll" bind:this={scrollContainer} onscroll={handleScroll}>
{#each days as day, index}
{@const monthLabel = getMonthLabel(day, index)}
{#each days as day}
{@const dayIsToday = isToday(day)}
{@const dayIsSelected = isSameDay(day, currentDate)}
{@const dayIsWeekend = day.getDay() === 0 || day.getDay() === 6}
{@const dayInRange = isWithinInterval(day, { start: viewRange.start, end: viewRange.end })}
{@const dayIsRangeStart = isSameDay(day, viewRange.start)}
{@const dayIsRangeEnd = isSameDay(day, viewRange.end)}
{#if monthLabel}
<div class="month-marker">
<span class="month-label">{monthLabel}</span>
</div>
{/if}
<button
class="day-item"
class:weekend={dayIsWeekend}
@ -235,7 +250,7 @@
.date-strip-container {
display: flex;
align-items: center;
flex-direction: column;
background: var(--color-surface, #ffffff);
border-radius: 16px;
margin: 0 1rem;
@ -247,6 +262,19 @@
overflow: hidden;
}
.month-header {
display: flex;
justify-content: center;
padding: 0.25rem 0 0.5rem;
}
.month-label {
font-size: 0.875rem;
font-weight: 600;
color: var(--color-foreground, #1f2937);
white-space: nowrap;
}
.days-scroll {
display: flex;
align-items: center;
@ -262,25 +290,6 @@
display: none;
}
.month-marker {
display: flex;
align-items: center;
padding: 0 0.5rem;
flex-shrink: 0;
}
.month-label {
font-size: 0.6875rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #3b82f6;
white-space: nowrap;
padding: 0.25rem 0.5rem;
background: rgba(59, 130, 246, 0.1);
border-radius: 4px;
}
.day-item {
display: flex;
flex-direction: column;
@ -374,8 +383,7 @@
}
.month-label {
font-size: 0.625rem;
padding: 0.125rem 0.375rem;
font-size: 0.75rem;
}
}
</style>

View file

@ -0,0 +1,412 @@
<script lang="ts">
import { calendarsStore } from '$lib/stores/calendars.svelte';
import { goto } from '$app/navigation';
interface Props {
direction?: 'up' | 'down';
embedded?: boolean;
}
let { direction = 'up', embedded = false }: Props = $props();
let isOpen = $state(false);
let triggerButton: HTMLButtonElement;
let dropdownPosition = $state({ top: 0, left: 0 });
function toggle() {
if (triggerButton) {
const rect = triggerButton.getBoundingClientRect();
if (direction === 'down') {
dropdownPosition = {
top: rect.bottom + 8,
left: rect.left,
};
} else {
dropdownPosition = {
top: rect.top - 8,
left: rect.left,
};
}
}
isOpen = !isOpen;
}
function close() {
isOpen = false;
}
function handleToggle(calendarId: string) {
calendarsStore.toggleVisibility(calendarId);
}
function handleAddCalendar() {
close();
goto('/settings');
}
// Count visible calendars
let visibleCount = $derived(calendarsStore.calendars.filter((c) => c.isVisible).length);
let totalCount = $derived(calendarsStore.calendars.length);
</script>
<div class="pill-calendar-selector">
<!-- Trigger Button -->
<button
bind:this={triggerButton}
onclick={toggle}
class="trigger-button"
class:pill={!embedded}
class:glass-pill={!embedded}
class:embedded-btn={embedded}
>
<svg class="pill-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<span class="pill-label">{visibleCount}/{totalCount}</span>
<svg
class="chevron-icon"
class:rotated={isOpen}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
{#if isOpen}
<!-- Backdrop -->
<button
class="menu-backdrop"
onclick={close}
onkeydown={(e) => e.key === 'Escape' && close()}
aria-label="Close dropdown"
></button>
<!-- Dropdown -->
<div
class="dropdown-container"
class:dropdown-up={direction === 'up'}
class:dropdown-down={direction === 'down'}
style="top: {dropdownPosition.top}px; left: {dropdownPosition.left}px;"
>
<div class="dropdown-header">
<span class="header-title">Kalender</span>
<button class="add-btn" onclick={handleAddCalendar} aria-label="Kalender hinzufügen">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 4v16m8-8H4"
/>
</svg>
</button>
</div>
<div class="calendar-list">
{#each calendarsStore.calendars as calendar}
<label class="calendar-item">
<input
type="checkbox"
checked={calendar.isVisible}
onchange={() => handleToggle(calendar.id)}
style="accent-color: {calendar.color}"
/>
<span class="color-dot" style="background-color: {calendar.color}"></span>
<span class="calendar-name">{calendar.name}</span>
</label>
{/each}
{#if calendarsStore.calendars.length === 0}
<p class="empty-message">Keine Kalender</p>
{/if}
</div>
</div>
{/if}
</div>
<style>
.pill-calendar-selector {
position: relative;
}
.trigger-button {
position: relative;
}
/* Embedded mode - no background/border, for use inside a parent bar */
.embedded-btn {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 0.75rem;
border-radius: 9999px;
font-size: 0.875rem;
font-weight: 500;
white-space: nowrap;
border: none;
cursor: pointer;
transition: all 0.15s ease;
background: transparent;
color: #374151;
}
:global(.dark) .embedded-btn {
color: #f3f4f6;
}
.embedded-btn:hover {
background: rgba(0, 0, 0, 0.05);
}
:global(.dark) .embedded-btn:hover {
background: rgba(255, 255, 255, 0.1);
}
.pill {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 0.875rem;
border-radius: 9999px;
font-size: 0.875rem;
font-weight: 500;
white-space: nowrap;
text-decoration: none;
transition: all 0.2s;
border: none;
cursor: pointer;
}
.glass-pill {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
color: #374151;
}
:global(.dark) .glass-pill {
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.15);
color: #f3f4f6;
}
.glass-pill:hover {
background: rgba(255, 255, 255, 0.95);
border-color: rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
box-shadow:
0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
:global(.dark) .glass-pill:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.25);
}
.pill-icon {
width: 1rem;
height: 1rem;
flex-shrink: 0;
}
.pill-label {
display: inline;
}
.chevron-icon {
width: 0.75rem;
height: 0.75rem;
transition: transform 0.2s;
margin-left: 0.25rem;
}
.chevron-icon.rotated {
transform: rotate(180deg);
}
.menu-backdrop {
position: fixed;
inset: 0;
z-index: 9998;
background: transparent;
border: none;
cursor: default;
}
.dropdown-container {
position: fixed;
z-index: 9999;
min-width: 200px;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 1rem;
box-shadow:
0 10px 25px -5px rgba(0, 0, 0, 0.15),
0 8px 10px -6px rgba(0, 0, 0, 0.1);
padding: 0.75rem;
animation: dropdownIn 0.15s ease-out forwards;
}
:global(.dark) .dropdown-container {
background: rgba(30, 30, 30, 0.95);
border: 1px solid rgba(255, 255, 255, 0.15);
}
.dropdown-up {
transform: translateY(-100%);
}
.dropdown-down {
transform: translateY(0);
}
@keyframes dropdownIn {
from {
opacity: 0;
transform: translateY(-100%) scale(0.95);
}
to {
opacity: 1;
transform: translateY(-100%) scale(1);
}
}
.dropdown-down {
animation-name: dropdownInDown;
}
@keyframes dropdownInDown {
from {
opacity: 0;
transform: translateY(0) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.dropdown-header {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 0.5rem;
margin-bottom: 0.5rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
:global(.dark) .dropdown-header {
border-bottom-color: rgba(255, 255, 255, 0.15);
}
.header-title {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #6b7280;
}
:global(.dark) .header-title {
color: #9ca3af;
}
.add-btn {
padding: 0.25rem;
border: none;
background: transparent;
border-radius: 0.375rem;
cursor: pointer;
color: #6b7280;
transition: all 0.15s;
}
:global(.dark) .add-btn {
color: #9ca3af;
}
.add-btn:hover {
background: rgba(0, 0, 0, 0.05);
color: #374151;
}
:global(.dark) .add-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: #f3f4f6;
}
.calendar-list {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.calendar-item {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
padding: 0.375rem 0.5rem;
border-radius: 0.5rem;
transition: background 0.15s;
}
.calendar-item:hover {
background: rgba(0, 0, 0, 0.05);
}
:global(.dark) .calendar-item:hover {
background: rgba(255, 255, 255, 0.1);
}
.calendar-item input {
width: 16px;
height: 16px;
cursor: pointer;
flex-shrink: 0;
}
.color-dot {
width: 10px;
height: 10px;
border-radius: 9999px;
flex-shrink: 0;
}
.calendar-name {
font-size: 0.875rem;
color: #374151;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
:global(.dark) .calendar-name {
color: #f3f4f6;
}
.empty-message {
font-size: 0.875rem;
color: #6b7280;
text-align: center;
padding: 0.5rem;
}
:global(.dark) .empty-message {
color: #9ca3af;
}
</style>

View file

@ -1,7 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { _ } from 'svelte-i18n';
import { viewStore } from '$lib/stores/view.svelte';
import { eventsStore } from '$lib/stores/events.svelte';
@ -9,14 +8,11 @@
import { authStore } from '$lib/stores/auth.svelte';
import { settingsStore } from '$lib/stores/settings.svelte';
import { isSidebarMode as sidebarModeStore } from '$lib/stores/navigation';
import CalendarHeader from '$lib/components/calendar/CalendarHeader.svelte';
import WeekView from '$lib/components/calendar/WeekView.svelte';
import DayView from '$lib/components/calendar/DayView.svelte';
import MonthView from '$lib/components/calendar/MonthView.svelte';
import MultiDayView from '$lib/components/calendar/MultiDayView.svelte';
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 { CalendarViewSkeleton } from '$lib/components/skeletons';
@ -100,14 +96,6 @@
eventsStore.fetchEvents(viewStore.viewRange.start, viewStore.viewRange.end);
}
});
function handleDateSelect(date: Date) {
viewStore.setDate(date);
}
function handleNewEvent() {
goto('/event/new');
}
</script>
<svelte:head>
@ -133,20 +121,6 @@
</svg>
</button>
<button
class="w-full mb-4 flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg font-medium text-primary-foreground bg-primary hover:bg-primary/90 transition-colors"
onclick={handleNewEvent}
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
{$_('calendar.newEvent')}
</button>
<MiniCalendar selectedDate={viewStore.currentDate} onDateSelect={handleDateSelect} />
<CalendarSidebar />
<TodoSidebarSection maxItems={5} />
</aside>
@ -167,23 +141,11 @@
/>
</svg>
</button>
<button class="fab-new-event" onclick={handleNewEvent} title={$_('calendar.newEvent')}>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 4v16m8-8H4"
/>
</svg>
</button>
</div>
{/if}
<!-- Main Calendar Area -->
<div class="calendar-main" class:expanded={settingsStore.sidebarCollapsed}>
<CalendarHeader />
<div class="calendar-content">
{#if !initialized}
<CalendarViewSkeleton />
@ -317,8 +279,7 @@
}
}
.fab-expand,
.fab-new-event {
.fab-expand {
width: 48px;
height: 48px;
border-radius: var(--radius-full);
@ -329,9 +290,6 @@
cursor: pointer;
transition: all 150ms ease;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.fab-expand {
background: hsl(var(--color-surface));
color: hsl(var(--color-foreground));
border: 1px solid hsl(var(--color-border));
@ -342,16 +300,6 @@
transform: scale(1.05);
}
.fab-new-event {
background: hsl(var(--color-primary));
color: hsl(var(--color-primary-foreground));
}
.fab-new-event:hover {
background: hsl(var(--color-primary) / 0.9);
transform: scale(1.05);
}
.calendar-main {
flex: 1;
display: flex;

View file

@ -1,79 +0,0 @@
<script lang="ts">
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { eventsStore } from '$lib/stores/events.svelte';
import { calendarsStore } from '$lib/stores/calendars.svelte';
import { toast } from '$lib/stores/toast';
import EventForm from '$lib/components/event/EventForm.svelte';
import type { CreateEventInput, UpdateEventInput } from '@calendar/shared';
import { addHours, parseISO } from 'date-fns';
let initialStart = $state<Date | null>(null);
onMount(() => {
if (!authStore.isAuthenticated) {
goto('/login');
return;
}
// Check for start time in URL params
const startParam = $page.url.searchParams.get('start');
if (startParam) {
initialStart = parseISO(startParam);
}
});
async function handleSave(data: CreateEventInput | UpdateEventInput) {
// In create mode, data is always CreateEventInput
const result = await eventsStore.createEvent(data as CreateEventInput);
if (result.error) {
toast.error(`Fehler beim Erstellen: ${result.error.message}`);
return;
}
// Refresh calendars in case a default calendar was created
if (calendarsStore.calendars.length === 0) {
await calendarsStore.fetchCalendars();
}
toast.success('Termin erstellt');
goto('/');
}
function handleCancel() {
goto('/');
}
</script>
<svelte:head>
<title>Neuer Termin | Kalender</title>
</svelte:head>
<div class="page-container">
<div class="card">
<h1 class="page-title">Neuer Termin</h1>
<EventForm
mode="create"
initialStartTime={initialStart}
onSave={handleSave}
onCancel={handleCancel}
/>
</div>
</div>
<style>
.page-container {
max-width: 600px;
margin: 0 auto;
}
.page-title {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 1.5rem;
color: hsl(var(--color-foreground));
}
</style>