diff --git a/apps/calendar/apps/web/src/lib/components/calendar/DayView.svelte b/apps/calendar/apps/web/src/lib/components/calendar/DayView.svelte
index 5a501906a..83aef58d6 100644
--- a/apps/calendar/apps/web/src/lib/components/calendar/DayView.svelte
+++ b/apps/calendar/apps/web/src/lib/components/calendar/DayView.svelte
@@ -17,6 +17,7 @@
getVisibleOverflowEvents,
type OverflowEvents,
} from '$lib/utils/eventFiltering';
+ import EventCard from './EventCard.svelte';
import TaskBlock from './TaskBlock.svelte';
import EventContextMenu from '$lib/components/event/EventContextMenu.svelte';
import { goto } from '$app/navigation';
@@ -625,9 +626,11 @@
const top = minutesToPercent(startMinutes);
const height = Math.max((duration / (totalVisibleHours * 60)) * 100, 1.5); // minimum ~20px at 60px/hour
- const color = calendarsStore.getColor(event.calendarId);
+ return `top: ${top}%; height: ${height}%;`;
+ }
- return `top: ${top}%; height: ${height}%; background-color: ${color};`;
+ function formatEventTime(event: CalendarEvent): string {
+ return `${format(toDate(event.startTime), 'HH:mm')} - ${format(toDate(event.endTime), 'HH:mm')}`;
}
/**
@@ -782,63 +785,27 @@
{/each}
- {#each timedEvents as event}
+ {#each timedEvents as event (event.id)}
{@const isBeingDragged = isDragging && draggedEvent?.id === event.id}
{@const isBeingResized = isResizing && resizeEvent?.id === event.id}
- {@const isDraft = eventsStore.isDraftEvent(event.id)}
- {@const isSearchHighlighted = searchStore.isEventHighlighted(event.id)}
- {@const isSearchDimmed = searchStore.isEventDimmed(event.id)}
-
-
startDrag(event, e)}
- onclick={(e) => !isDraft && handleEventClick(event, e)}
- oncontextmenu={(e) => handleEventContextMenu(event, e)}
- role="button"
- tabindex="0"
- >
-
-
startResize(event, 'top', e)}
- role="separator"
- aria-orientation="horizontal"
- aria-label="Startzeit ändern"
- aria-valuenow={0}
- tabindex="-1"
- >
-
-
- {format(toDate(event.startTime), 'HH:mm')} -
- {format(toDate(event.endTime), 'HH:mm')}
-
-
{event.title || (isDraft ? '(Neuer Termin)' : '')}
- {#if event.location}
-
{event.location}
- {/if}
-
-
-
startResize(event, 'bottom', e)}
- role="separator"
- aria-orientation="horizontal"
- aria-label="Endzeit ändern"
- aria-valuenow={0}
- tabindex="-1"
- >
-
+ color={calendarsStore.getColor(event.calendarId)}
+ isDragging={isBeingDragged}
+ isResizing={isBeingResized}
+ isSearchHighlighted={searchStore.isEventHighlighted(event.id)}
+ isSearchDimmed={searchStore.isEventDimmed(event.id)}
+ formattedTime={formatEventTime(event)}
+ onClick={handleEventClick}
+ onPointerDown={startDrag}
+ onContextMenu={handleEventContextMenu}
+ onResizeStart={startResize}
+ />
{/each}
@@ -1046,127 +1013,6 @@
outline-offset: -2px;
}
- .event-card {
- position: absolute;
- left: 8px;
- width: calc(100% - 16px);
- max-width: 400px;
- color: white;
- border: none;
- text-align: left;
- cursor: grab;
- z-index: 1;
- display: flex;
- flex-direction: column;
- gap: 2px;
- padding: 6px 10px;
- border-radius: var(--radius-md);
- overflow: hidden;
- touch-action: none;
- user-select: none;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
- transition:
- box-shadow 150ms ease,
- opacity 150ms ease;
- }
-
- .event-card:hover {
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
- }
-
- .event-card.dragging {
- cursor: grabbing;
- opacity: 0.9;
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
- z-index: 100;
- }
-
- .event-card.resizing {
- cursor: ns-resize;
- opacity: 0.85;
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
- z-index: 100;
- outline: 2px dashed rgba(255, 255, 255, 0.6);
- outline-offset: -2px;
- }
-
- .event-card.draft {
- outline: 2px solid hsl(var(--color-primary));
- outline-offset: -1px;
- animation: pulse-outline 1.5s ease-in-out infinite;
- }
-
- /* Search highlighting */
- .event-card.search-highlighted {
- outline: 2px solid hsl(var(--color-primary));
- outline-offset: 1px;
- box-shadow:
- 0 0 0 4px hsl(var(--color-primary) / 0.3),
- 0 4px 12px rgba(0, 0, 0, 0.25);
- z-index: 10;
- }
-
- .event-card.search-dimmed {
- opacity: 0.35;
- filter: grayscale(0.3);
- }
-
- @keyframes pulse-outline {
- 0%,
- 100% {
- outline-color: hsl(var(--color-primary));
- }
- 50% {
- outline-color: hsl(var(--color-primary) / 0.5);
- }
- }
-
- /* Resize Handles */
- .resize-handle {
- position: absolute;
- left: 0;
- right: 0;
- height: 8px;
- cursor: ns-resize;
- opacity: 0;
- transition: opacity 150ms ease;
- z-index: 10;
- }
-
- .resize-handle.top {
- top: 0;
- border-radius: var(--radius-sm) var(--radius-sm) 0 0;
- }
-
- .resize-handle.bottom {
- bottom: 0;
- border-radius: 0 0 var(--radius-sm) var(--radius-sm);
- }
-
- .event-card:hover .resize-handle {
- opacity: 1;
- background: rgba(255, 255, 255, 0.3);
- }
-
- .resize-handle:hover {
- background: rgba(255, 255, 255, 0.5) !important;
- }
-
- .event-time {
- font-size: 0.75rem;
- opacity: 0.9;
- }
-
- .event-title {
- font-size: 0.875rem;
- font-weight: 500;
- }
-
- .event-location {
- font-size: 0.75rem;
- opacity: 0.8;
- }
-
/* Time indicator */
.time-indicator {
position: absolute;
diff --git a/apps/calendar/apps/web/src/lib/components/calendar/EventCard.svelte b/apps/calendar/apps/web/src/lib/components/calendar/EventCard.svelte
new file mode 100644
index 000000000..a106e4cea
--- /dev/null
+++ b/apps/calendar/apps/web/src/lib/components/calendar/EventCard.svelte
@@ -0,0 +1,251 @@
+
+
+
+
+ {#if onResizeStart}
+
+ {/if}
+
+
{formattedTime}
+
{event.title || (isDraft ? $_('calendar.draftEvent') : '')}
+ {#if event.location}
+
{event.location}
+ {/if}
+
+
+ {#if onResizeStart}
+
+ {/if}
+
+
+
diff --git a/apps/calendar/apps/web/src/lib/components/calendar/OverflowIndicator.svelte b/apps/calendar/apps/web/src/lib/components/calendar/OverflowIndicator.svelte
new file mode 100644
index 000000000..bbb58a343
--- /dev/null
+++ b/apps/calendar/apps/web/src/lib/components/calendar/OverflowIndicator.svelte
@@ -0,0 +1,57 @@
+
+
+{#if events.length > 0}
+
+ {#each events as { color, tooltip }}
+
+ {/each}
+
+{/if}
+
+
diff --git a/apps/calendar/apps/web/src/lib/components/calendar/TimeColumn.svelte b/apps/calendar/apps/web/src/lib/components/calendar/TimeColumn.svelte
new file mode 100644
index 000000000..04e68b959
--- /dev/null
+++ b/apps/calendar/apps/web/src/lib/components/calendar/TimeColumn.svelte
@@ -0,0 +1,42 @@
+
+
+
+ {#each hours as hour}
+
+ {formatHour(hour)}
+
+ {/each}
+
+
+
diff --git a/apps/calendar/apps/web/src/lib/components/calendar/WeekView.svelte b/apps/calendar/apps/web/src/lib/components/calendar/WeekView.svelte
index ff30f1e55..421291570 100644
--- a/apps/calendar/apps/web/src/lib/components/calendar/WeekView.svelte
+++ b/apps/calendar/apps/web/src/lib/components/calendar/WeekView.svelte
@@ -17,6 +17,7 @@
getVisibleOverflowEvents,
type OverflowEvents,
} from '$lib/utils/eventFiltering';
+ import EventCard from './EventCard.svelte';
import TaskBlock from './TaskBlock.svelte';
import EventContextMenu from '$lib/components/event/EventContextMenu.svelte';
import { goto } from '$app/navigation';
@@ -201,9 +202,11 @@
const top = minutesToPercent(startMinutes);
const height = Math.max((duration / (totalVisibleHours * 60)) * 100, 2); // Min 2% height
- const color = calendarsStore.getColor(event.calendarId);
+ return `top: ${top}%; height: ${height}%;`;
+ }
- return `top: ${top}%; height: ${height}%; background-color: ${color};`;
+ function formatEventTimeRange(event: CalendarEvent): string {
+ return `${formatEventTime(event.startTime)} - ${formatEventTime(event.endTime)}`;
}
/**
@@ -955,60 +958,27 @@
{#each getEventsForDay(day) as event (event.id)}
{@const isBeingDragged = isDragging && draggedEvent?.id === event.id}
{@const isBeingResized = isResizing && resizeEvent?.id === event.id}
- {@const isDraft = eventsStore.isDraftEvent(event.id)}
{@const isCrossDayDrag =
- isBeingDragged && dragTargetDay && !isSameDay(day, dragTargetDay)}
- {@const isSearchHighlighted = searchStore.isEventHighlighted(event.id)}
- {@const isSearchDimmed = searchStore.isEventDimmed(event.id)}
- startDrag(event, e)}
- onclick={(e) => !isDraft && handleEventClick(event, e)}
- onkeydown={(e) => !isDraft && e.key === 'Enter' && goto(`/?event=${event.id}`)}
- oncontextmenu={(e) => handleEventContextMenu(event, e)}
- >
-
-
startResize(event, 'top', e)}
- role="slider"
- aria-label={$_('event.changeStartTime')}
- aria-valuenow={0}
- tabindex="-1"
- >
-
-
- {formatEventTime(event.startTime)} - {formatEventTime(event.endTime)}
-
-
{event.title || (isDraft ? $_('calendar.draftEvent') : '')}
-
-
-
startResize(event, 'bottom', e)}
- role="slider"
- aria-label={$_('event.changeEndTime')}
- aria-valuenow={0}
- tabindex="-1"
- >
-
+ color={calendarsStore.getColor(event.calendarId)}
+ isDragging={isBeingDragged && !isCrossDayDrag}
+ isDraggingSource={isCrossDayDrag}
+ isResizing={isBeingResized}
+ isSearchHighlighted={searchStore.isEventHighlighted(event.id)}
+ isSearchDimmed={searchStore.isEventDimmed(event.id)}
+ formattedTime={formatEventTimeRange(event)}
+ onClick={handleEventClick}
+ onPointerDown={startDrag}
+ onContextMenu={handleEventContextMenu}
+ onResizeStart={startResize}
+ />
{/each}
@@ -1046,15 +1016,13 @@
{#if isDragging && draggedEvent && dragTargetDay && isSameDay(day, dragTargetDay) && !getEventsForDay(day).some((e) => e.id === draggedEvent!.id)}
-
- {formatEventTime(draggedEvent.startTime)}
- {draggedEvent.title}
-
+
{/if}
@@ -1329,140 +1297,6 @@
background: hsl(var(--color-muted) / 0.3);
}
- .event-card {
- position: absolute;
- left: 2px;
- right: 2px;
- padding: 2px 4px;
- color: white;
- border: none;
- border-radius: var(--radius-sm);
- text-align: left;
- cursor: grab;
- z-index: 1;
- overflow: hidden;
- transition:
- box-shadow 0.15s ease,
- opacity 0.15s ease;
- touch-action: none;
- user-select: none;
- }
-
- .event-card:hover {
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
- }
-
- .event-card.dragging {
- cursor: grabbing;
- opacity: 0.9;
- box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
- z-index: 100;
- }
-
- .event-card.resizing {
- opacity: 0.85;
- z-index: 100;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
- outline: 2px dashed rgba(255, 255, 255, 0.6);
- outline-offset: -2px;
- }
-
- /* Ghost style for source position during cross-day drag */
- .event-card.dragging-source {
- opacity: 0.4;
- background: transparent !important;
- border: 2px dashed hsl(var(--color-border));
- pointer-events: none;
- }
-
- .event-card.dragging-source .event-title,
- .event-card.dragging-source .event-time {
- opacity: 0.5;
- }
-
- /* Solid preview at target position during cross-day drag */
- .event-card.drag-preview {
- pointer-events: none;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
- }
-
- /* Search highlighting */
- .event-card.search-highlighted {
- outline: 2px solid hsl(var(--color-primary));
- outline-offset: 1px;
- box-shadow:
- 0 0 0 4px hsl(var(--color-primary) / 0.3),
- 0 4px 12px rgba(0, 0, 0, 0.25);
- z-index: 10;
- }
-
- .event-card.search-dimmed {
- opacity: 0.35;
- filter: grayscale(0.3);
- }
-
- .event-card.draft {
- outline: 2px solid hsl(var(--color-primary));
- outline-offset: -1px;
- animation: pulse-outline 1.5s ease-in-out infinite;
- }
-
- @keyframes pulse-outline {
- 0%,
- 100% {
- outline-color: hsl(var(--color-primary));
- }
- 50% {
- outline-color: hsl(var(--color-primary) / 0.5);
- }
- }
-
- .event-time {
- font-size: 0.65rem;
- opacity: 0.9;
- display: block;
- }
-
- .event-title {
- display: block;
- font-size: 0.75rem;
- font-weight: 500;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- /* Resize handles */
- .resize-handle {
- position: absolute;
- left: 0;
- right: 0;
- height: 8px;
- cursor: ns-resize;
- opacity: 0;
- transition: opacity 0.15s ease;
- z-index: 2;
- }
-
- .resize-handle.top {
- top: 0;
- border-radius: var(--radius-sm) var(--radius-sm) 0 0;
- }
-
- .resize-handle.bottom {
- bottom: 0;
- border-radius: 0 0 var(--radius-sm) var(--radius-sm);
- }
-
- .event-card:hover .resize-handle {
- opacity: 1;
- background: rgba(255, 255, 255, 0.3);
- }
-
- .resize-handle:hover {
- background: rgba(255, 255, 255, 0.5) !important;
- }
-
.time-indicator {
position: absolute;
left: 0;