mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:21:10 +02:00
refactor(calendar): extract EventCard component from views
- Create EventCard component encapsulating event block with resize handles - Create TimeColumn component for hour labels (not yet integrated) - Create OverflowIndicator component for overflow events (not yet integrated) - Integrate EventCard into DayView (154 lines reduced) - Integrate EventCard into WeekView (166 lines reduced) - Remove duplicate event-card styles from both views 🤖 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
668957a30b
commit
b966c91c32
5 changed files with 400 additions and 370 deletions
|
|
@ -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}
|
||||
|
||||
<!-- Timed events -->
|
||||
{#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)}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
class="event-card"
|
||||
class:dragging={isBeingDragged}
|
||||
class:resizing={isBeingResized}
|
||||
class:draft={isDraft}
|
||||
class:search-highlighted={isSearchHighlighted}
|
||||
class:search-dimmed={isSearchDimmed}
|
||||
data-event-id={event.id}
|
||||
<EventCard
|
||||
{event}
|
||||
style={isBeingDragged
|
||||
? `top: ${dragPreviewTop}%; height: ${dragPreviewHeight}%; background-color: ${calendarsStore.getColor(event.calendarId)};`
|
||||
? `top: ${dragPreviewTop}%; height: ${dragPreviewHeight}%;`
|
||||
: isBeingResized
|
||||
? `top: ${resizePreviewTop}%; height: ${resizePreviewHeight}%; background-color: ${calendarsStore.getColor(event.calendarId)};`
|
||||
? `top: ${resizePreviewTop}%; height: ${resizePreviewHeight}%;`
|
||||
: getEventStyle(event)}
|
||||
onpointerdown={(e) => startDrag(event, e)}
|
||||
onclick={(e) => !isDraft && handleEventClick(event, e)}
|
||||
oncontextmenu={(e) => handleEventContextMenu(event, e)}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<!-- Top resize handle -->
|
||||
<div
|
||||
class="resize-handle top"
|
||||
onpointerdown={(e) => startResize(event, 'top', e)}
|
||||
role="separator"
|
||||
aria-orientation="horizontal"
|
||||
aria-label="Startzeit ändern"
|
||||
aria-valuenow={0}
|
||||
tabindex="-1"
|
||||
></div>
|
||||
|
||||
<span class="event-time">
|
||||
{format(toDate(event.startTime), 'HH:mm')} -
|
||||
{format(toDate(event.endTime), 'HH:mm')}
|
||||
</span>
|
||||
<span class="event-title">{event.title || (isDraft ? '(Neuer Termin)' : '')}</span>
|
||||
{#if event.location}
|
||||
<span class="event-location">{event.location}</span>
|
||||
{/if}
|
||||
|
||||
<!-- Bottom resize handle -->
|
||||
<div
|
||||
class="resize-handle bottom"
|
||||
onpointerdown={(e) => startResize(event, 'bottom', e)}
|
||||
role="separator"
|
||||
aria-orientation="horizontal"
|
||||
aria-label="Endzeit ändern"
|
||||
aria-valuenow={0}
|
||||
tabindex="-1"
|
||||
></div>
|
||||
</div>
|
||||
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}
|
||||
|
||||
<!-- Scheduled Tasks (Time-Blocking) -->
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,251 @@
|
|||
<script lang="ts">
|
||||
import type { CalendarEvent } from '@calendar/shared';
|
||||
import { eventsStore } from '$lib/stores/events.svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
event: CalendarEvent;
|
||||
style: string;
|
||||
color: string;
|
||||
isDragging?: boolean;
|
||||
isResizing?: boolean;
|
||||
isDraggingSource?: boolean;
|
||||
isSearchHighlighted?: boolean;
|
||||
isSearchDimmed?: boolean;
|
||||
formattedTime: string;
|
||||
onClick?: (event: CalendarEvent, e: MouseEvent) => void;
|
||||
onPointerDown?: (event: CalendarEvent, e: PointerEvent) => void;
|
||||
onContextMenu?: (event: CalendarEvent, e: MouseEvent) => void;
|
||||
onResizeStart?: (event: CalendarEvent, edge: 'top' | 'bottom', e: PointerEvent) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
event,
|
||||
style,
|
||||
color,
|
||||
isDragging = false,
|
||||
isResizing = false,
|
||||
isDraggingSource = false,
|
||||
isSearchHighlighted = false,
|
||||
isSearchDimmed = false,
|
||||
formattedTime,
|
||||
onClick,
|
||||
onPointerDown,
|
||||
onContextMenu,
|
||||
onResizeStart,
|
||||
}: Props = $props();
|
||||
|
||||
let isDraft = $derived(eventsStore.isDraftEvent(event.id));
|
||||
|
||||
function handleClick(e: MouseEvent) {
|
||||
if (isDragging || isResizing || isDraft) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
onClick?.(event, e);
|
||||
}
|
||||
|
||||
function handlePointerDown(e: PointerEvent) {
|
||||
onPointerDown?.(event, e);
|
||||
}
|
||||
|
||||
function handleContextMenu(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (isDraft) return;
|
||||
onContextMenu?.(event, e);
|
||||
}
|
||||
|
||||
function handleResizeTop(e: PointerEvent) {
|
||||
e.stopPropagation();
|
||||
onResizeStart?.(event, 'top', e);
|
||||
}
|
||||
|
||||
function handleResizeBottom(e: PointerEvent) {
|
||||
e.stopPropagation();
|
||||
onResizeStart?.(event, 'bottom', e);
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if ((e.key === 'Enter' || e.key === ' ') && !isDraft) {
|
||||
e.preventDefault();
|
||||
onClick?.(event, e as unknown as MouseEvent);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="event-card"
|
||||
class:dragging={isDragging && !isDraggingSource}
|
||||
class:dragging-source={isDraggingSource}
|
||||
class:resizing={isResizing}
|
||||
class:draft={isDraft}
|
||||
class:search-highlighted={isSearchHighlighted}
|
||||
class:search-dimmed={isSearchDimmed}
|
||||
data-event-id={event.id}
|
||||
{style}
|
||||
style:background-color={color}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label={event.title || $_('calendar.draftEvent')}
|
||||
onpointerdown={handlePointerDown}
|
||||
onclick={handleClick}
|
||||
onkeydown={handleKeydown}
|
||||
oncontextmenu={handleContextMenu}
|
||||
>
|
||||
<!-- Top resize handle -->
|
||||
{#if onResizeStart}
|
||||
<div
|
||||
class="resize-handle top"
|
||||
onpointerdown={handleResizeTop}
|
||||
role="slider"
|
||||
aria-label={$_('event.changeStartTime')}
|
||||
aria-valuenow={0}
|
||||
tabindex="-1"
|
||||
></div>
|
||||
{/if}
|
||||
|
||||
<span class="event-time">{formattedTime}</span>
|
||||
<span class="event-title">{event.title || (isDraft ? $_('calendar.draftEvent') : '')}</span>
|
||||
{#if event.location}
|
||||
<span class="event-location">{event.location}</span>
|
||||
{/if}
|
||||
|
||||
<!-- Bottom resize handle -->
|
||||
{#if onResizeStart}
|
||||
<div
|
||||
class="resize-handle bottom"
|
||||
onpointerdown={handleResizeBottom}
|
||||
role="slider"
|
||||
aria-label={$_('event.changeEndTime')}
|
||||
aria-valuenow={0}
|
||||
tabindex="-1"
|
||||
></div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.event-card {
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
right: 2px;
|
||||
padding: 2px 4px;
|
||||
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;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.event-card:hover {
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.event-card:focus-visible {
|
||||
outline: 2px solid hsl(var(--color-primary));
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.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 hsl(var(--color-primary) / 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,
|
||||
.event-card.dragging-source .event-location {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.event-card.draft {
|
||||
border: 2px dashed hsl(var(--color-primary) / 0.6);
|
||||
background-color: hsl(var(--color-primary) / 0.3) !important;
|
||||
}
|
||||
|
||||
.event-card.search-highlighted {
|
||||
outline: 2px solid hsl(var(--color-primary));
|
||||
outline-offset: 1px;
|
||||
box-shadow: 0 0 0 3px hsl(var(--color-primary) / 0.3);
|
||||
}
|
||||
|
||||
.event-card.search-dimmed {
|
||||
opacity: 0.35;
|
||||
filter: grayscale(0.3);
|
||||
}
|
||||
|
||||
.event-time {
|
||||
display: block;
|
||||
font-size: 0.6rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.event-title {
|
||||
display: block;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.event-location {
|
||||
display: block;
|
||||
font-size: 0.6rem;
|
||||
opacity: 0.85;
|
||||
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);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* OverflowIndicator Component
|
||||
* Shows colored lines indicating events outside the visible time range
|
||||
*/
|
||||
|
||||
import type { CalendarEvent } from '@calendar/shared';
|
||||
|
||||
interface OverflowEvent {
|
||||
event: CalendarEvent;
|
||||
color: string;
|
||||
tooltip: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
events: OverflowEvent[];
|
||||
position: 'top' | 'bottom';
|
||||
label?: string;
|
||||
}
|
||||
|
||||
let { events, position, label }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if events.length > 0}
|
||||
<div class="overflow-indicator {position}" title={label}>
|
||||
{#each events as { color, tooltip }}
|
||||
<div class="overflow-line" style="background-color: {color}" title={tooltip}></div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.overflow-indicator {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
padding: 2px 4px;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.overflow-indicator.top {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.overflow-indicator.bottom {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.overflow-line {
|
||||
flex: 1;
|
||||
height: 3px;
|
||||
border-radius: 1px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* TimeColumn Component
|
||||
* Renders the time labels column for calendar views (WeekView, DayView, MultiDayView)
|
||||
*/
|
||||
|
||||
interface Props {
|
||||
hours: number[];
|
||||
formatHour: (hour: number) => string;
|
||||
}
|
||||
|
||||
let { hours, formatHour }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="time-column">
|
||||
{#each hours as hour}
|
||||
<div class="time-label">
|
||||
{formatHour(hour)}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.time-column {
|
||||
width: 48px;
|
||||
flex-shrink: 0;
|
||||
border-right: 1px solid hsl(var(--color-border));
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.time-label {
|
||||
height: var(--hour-height, 60px);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
padding-right: 8px;
|
||||
padding-top: 0;
|
||||
font-size: 0.7rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
transform: translateY(-0.4em);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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)}
|
||||
<div
|
||||
class="event-card"
|
||||
class:dragging={isBeingDragged && !isCrossDayDrag}
|
||||
class:dragging-source={isCrossDayDrag}
|
||||
class:resizing={isBeingResized}
|
||||
class:draft={isDraft}
|
||||
class:search-highlighted={isSearchHighlighted}
|
||||
class:search-dimmed={isSearchDimmed}
|
||||
data-event-id={event.id}
|
||||
isBeingDragged && dragTargetDay !== null && !isSameDay(day, dragTargetDay)}
|
||||
<EventCard
|
||||
{event}
|
||||
style={isBeingDragged && !isCrossDayDrag
|
||||
? `top: ${dragPreviewTop}%; height: ${dragPreviewHeight}%; background-color: ${calendarsStore.getColor(event.calendarId)};`
|
||||
? `top: ${dragPreviewTop}%; height: ${dragPreviewHeight}%;`
|
||||
: isBeingResized
|
||||
? `top: ${resizePreviewTop}%; height: ${resizePreviewHeight}%; background-color: ${calendarsStore.getColor(event.calendarId)};`
|
||||
? `top: ${resizePreviewTop}%; height: ${resizePreviewHeight}%;`
|
||||
: getEventStyle(event)}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label={event.title || $_('calendar.draftEvent')}
|
||||
onpointerdown={(e) => startDrag(event, e)}
|
||||
onclick={(e) => !isDraft && handleEventClick(event, e)}
|
||||
onkeydown={(e) => !isDraft && e.key === 'Enter' && goto(`/?event=${event.id}`)}
|
||||
oncontextmenu={(e) => handleEventContextMenu(event, e)}
|
||||
>
|
||||
<!-- Top resize handle -->
|
||||
<div
|
||||
class="resize-handle top"
|
||||
onpointerdown={(e) => startResize(event, 'top', e)}
|
||||
role="slider"
|
||||
aria-label={$_('event.changeStartTime')}
|
||||
aria-valuenow={0}
|
||||
tabindex="-1"
|
||||
></div>
|
||||
|
||||
<span class="event-time">
|
||||
{formatEventTime(event.startTime)} - {formatEventTime(event.endTime)}
|
||||
</span>
|
||||
<span class="event-title"
|
||||
>{event.title || (isDraft ? $_('calendar.draftEvent') : '')}</span
|
||||
>
|
||||
|
||||
<!-- Bottom resize handle -->
|
||||
<div
|
||||
class="resize-handle bottom"
|
||||
onpointerdown={(e) => startResize(event, 'bottom', e)}
|
||||
role="slider"
|
||||
aria-label={$_('event.changeEndTime')}
|
||||
aria-valuenow={0}
|
||||
tabindex="-1"
|
||||
></div>
|
||||
</div>
|
||||
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}
|
||||
|
||||
<!-- Scheduled Tasks (Time-Blocking) -->
|
||||
|
|
@ -1046,15 +1016,13 @@
|
|||
|
||||
<!-- Drag preview (solid) for cross-day dragging - shows where event will be -->
|
||||
{#if isDragging && draggedEvent && dragTargetDay && isSameDay(day, dragTargetDay) && !getEventsForDay(day).some((e) => e.id === draggedEvent!.id)}
|
||||
<div
|
||||
class="event-card drag-preview"
|
||||
style="top: {dragPreviewTop}%; height: {dragPreviewHeight}%; background-color: {calendarsStore.getColor(
|
||||
draggedEvent.calendarId
|
||||
)};"
|
||||
>
|
||||
<span class="event-time">{formatEventTime(draggedEvent.startTime)}</span>
|
||||
<span class="event-title">{draggedEvent.title}</span>
|
||||
</div>
|
||||
<EventCard
|
||||
event={draggedEvent}
|
||||
style="top: {dragPreviewTop}%; height: {dragPreviewHeight}%;"
|
||||
color={calendarsStore.getColor(draggedEvent.calendarId)}
|
||||
isDragging={true}
|
||||
formattedTime={formatEventTimeRange(draggedEvent)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Overflow indicators for events outside visible time range -->
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue