mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
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:
parent
21f2b28bf0
commit
b42db508a3
7 changed files with 495 additions and 510 deletions
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue