mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:21:09 +02:00
feat(calendar): type-specific styling, filter UI, cross-module navigation
- EventCard: visual differentiation per blockType (task=checkbox, habit=icon, timeEntry=striped+clock, focus=dashed, live=pulse animation) - CalendarHeader: filter toggle with chips for event/task/habit/timeEntry types - Calendar page: clicking external items navigates to source module Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8c8b3f80da
commit
2502d6241d
5 changed files with 205 additions and 3 deletions
|
|
@ -1,7 +1,17 @@
|
|||
<script lang="ts">
|
||||
import { calendarViewStore } from '../stores/view.svelte';
|
||||
import type { CalendarViewType } from '../types';
|
||||
import { CaretLeft, CaretRight, Plus } from '@manacore/shared-icons';
|
||||
import type { TimeBlockType } from '$lib/data/time-blocks/types';
|
||||
import {
|
||||
CaretLeft,
|
||||
CaretRight,
|
||||
Plus,
|
||||
CalendarBlank,
|
||||
CheckSquare,
|
||||
Timer,
|
||||
Heart,
|
||||
Funnel,
|
||||
} from '@manacore/shared-icons';
|
||||
import { format } from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
|
||||
|
|
@ -11,6 +21,19 @@
|
|||
|
||||
let { onNewEvent }: Props = $props();
|
||||
|
||||
let showFilters = $state(false);
|
||||
|
||||
const blockTypeConfig: { type: TimeBlockType; label: string; icon: typeof CalendarBlank }[] = [
|
||||
{ type: 'event', label: 'Termine', icon: CalendarBlank },
|
||||
{ type: 'task', label: 'Aufgaben', icon: CheckSquare },
|
||||
{ type: 'timeEntry', label: 'Zeiten', icon: Timer },
|
||||
{ type: 'habit', label: 'Habits', icon: Heart },
|
||||
];
|
||||
|
||||
let allActive = $derived(
|
||||
blockTypeConfig.every((c) => calendarViewStore.visibleBlockTypes.has(c.type))
|
||||
);
|
||||
|
||||
let headerLabel = $derived.by(() => {
|
||||
if (calendarViewStore.viewType === 'month') {
|
||||
return format(calendarViewStore.currentDate, 'MMMM yyyy', { locale: de });
|
||||
|
|
@ -52,11 +75,36 @@
|
|||
{/each}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onclick={() => (showFilters = !showFilters)}
|
||||
class="filter-btn"
|
||||
class:active={!allActive}
|
||||
aria-label="Filter"
|
||||
>
|
||||
<Funnel size={16} />
|
||||
</button>
|
||||
|
||||
<button onclick={onNewEvent} class="new-event-btn">
|
||||
<Plus size={16} />
|
||||
Termin
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if showFilters}
|
||||
<div class="filter-bar">
|
||||
{#each blockTypeConfig as cfg}
|
||||
{@const isActive = calendarViewStore.visibleBlockTypes.has(cfg.type)}
|
||||
<button
|
||||
class="filter-chip"
|
||||
class:active={isActive}
|
||||
onclick={() => calendarViewStore.toggleBlockType(cfg.type)}
|
||||
>
|
||||
<svelte:component this={cfg.icon} size={14} />
|
||||
{cfg.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
<style>
|
||||
|
|
@ -156,6 +204,60 @@
|
|||
color: hsl(var(--color-primary-foreground));
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
padding: 0.375rem;
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
background: transparent;
|
||||
border-radius: var(--radius-md, 8px);
|
||||
cursor: pointer;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.filter-btn:hover {
|
||||
background: hsl(var(--color-muted));
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
.filter-btn.active {
|
||||
background: hsl(var(--color-primary) / 0.1);
|
||||
border-color: hsl(var(--color-primary) / 0.3);
|
||||
color: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
gap: 0.375rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-top: 1px solid hsl(var(--color-border));
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.filter-chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0.625rem;
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
border-radius: 9999px;
|
||||
background: transparent;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.filter-chip:hover {
|
||||
background: hsl(var(--color-muted));
|
||||
}
|
||||
|
||||
.filter-chip.active {
|
||||
background: hsl(var(--color-primary) / 0.1);
|
||||
border-color: hsl(var(--color-primary) / 0.3);
|
||||
color: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
.new-event-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
<script lang="ts">
|
||||
import type { CalendarEvent } from '../types';
|
||||
import { eventsStore } from '../stores/events.svelte';
|
||||
import { CheckSquare, Clock, Timer, Lightning } from '@manacore/shared-icons';
|
||||
import { getIconComponent } from '@manacore/shared-icons';
|
||||
|
||||
interface Props {
|
||||
event: CalendarEvent;
|
||||
|
|
@ -32,6 +34,11 @@
|
|||
|
||||
let isDraft = $derived(eventsStore.isDraftEvent(event.id));
|
||||
|
||||
/** Resolve the Phosphor icon component for habit blocks. */
|
||||
let habitIconComponent = $derived(
|
||||
event.blockType === 'habit' && event.icon ? getIconComponent(event.icon) : null
|
||||
);
|
||||
|
||||
function handleClick(e: MouseEvent) {
|
||||
if (isDragging || isResizing || isDraft) {
|
||||
e.preventDefault();
|
||||
|
|
@ -71,11 +78,12 @@
|
|||
</script>
|
||||
|
||||
<div
|
||||
class="event-card"
|
||||
class="event-card block-type-{event.blockType}"
|
||||
class:dragging={isDragging && !isDraggingSource}
|
||||
class:dragging-source={isDraggingSource}
|
||||
class:resizing={isResizing}
|
||||
class:draft={isDraft}
|
||||
class:live={event.isLive}
|
||||
data-event-id={event.id}
|
||||
{style}
|
||||
style:background-color={color}
|
||||
|
|
@ -98,7 +106,22 @@
|
|||
></div>
|
||||
{/if}
|
||||
|
||||
<span class="event-time">{formattedTime}</span>
|
||||
<div class="event-header-row">
|
||||
<!-- Type icon -->
|
||||
{#if event.blockType === 'task'}
|
||||
<span class="type-icon"><CheckSquare size={10} weight="bold" /></span>
|
||||
{:else if event.blockType === 'timeEntry'}
|
||||
<span class="type-icon"><Timer size={10} weight="bold" /></span>
|
||||
{:else if event.blockType === 'habit' && habitIconComponent}
|
||||
<span class="type-icon">
|
||||
<svelte:component this={habitIconComponent} size={10} weight="bold" />
|
||||
</span>
|
||||
{:else if event.blockType === 'focus'}
|
||||
<span class="type-icon"><Lightning size={10} weight="bold" /></span>
|
||||
{/if}
|
||||
|
||||
<span class="event-time">{formattedTime}</span>
|
||||
</div>
|
||||
<span class="event-title">{event.title || (isDraft ? 'Neuer Termin' : '')}</span>
|
||||
{#if event.location}
|
||||
<span class="event-location">{event.location}</span>
|
||||
|
|
@ -144,6 +167,50 @@
|
|||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
/* ─── Block-type visual differentiation ─── */
|
||||
|
||||
.event-card.block-type-task {
|
||||
border-left: 3px solid rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.event-card.block-type-habit {
|
||||
border-radius: var(--radius-sm, 4px) 8px 8px var(--radius-sm, 4px);
|
||||
}
|
||||
|
||||
.event-card.block-type-timeEntry {
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
background-image: repeating-linear-gradient(
|
||||
-45deg,
|
||||
transparent,
|
||||
transparent 4px,
|
||||
rgba(255, 255, 255, 0.05) 4px,
|
||||
rgba(255, 255, 255, 0.05) 8px
|
||||
);
|
||||
}
|
||||
|
||||
.event-card.block-type-focus {
|
||||
border: 2px dashed rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
/* Live/running indicator */
|
||||
.event-card.live {
|
||||
animation: pulse-border 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-border {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Drag/resize states ─── */
|
||||
|
||||
.event-card.dragging {
|
||||
cursor: grabbing;
|
||||
opacity: 0.9;
|
||||
|
|
@ -177,6 +244,21 @@
|
|||
background-color: hsl(var(--color-primary) / 0.3) !important;
|
||||
}
|
||||
|
||||
/* ─── Content ─── */
|
||||
|
||||
.event-header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.type-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 0.85;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.event-time {
|
||||
display: block;
|
||||
font-size: 0.6rem;
|
||||
|
|
@ -201,6 +283,8 @@
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* ─── Resize handles ─── */
|
||||
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
getCalendarColor,
|
||||
} from '$lib/modules/calendar/queries';
|
||||
import type { Calendar, CalendarEvent } from '$lib/modules/calendar/types';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import CalendarHeader from '$lib/modules/calendar/components/CalendarHeader.svelte';
|
||||
import DateStrip from '$lib/modules/calendar/components/DateStrip.svelte';
|
||||
|
|
@ -78,6 +79,21 @@
|
|||
let quickCreatePosition = $state({ x: 0, y: 0 });
|
||||
|
||||
function handleEventClick(event: CalendarEvent) {
|
||||
// Cross-module navigation: external items open in their source module
|
||||
if (event.calendarId === '__external__') {
|
||||
const routeMap: Record<string, string> = {
|
||||
todo: '/todo',
|
||||
times: '/times',
|
||||
habits: '/habits',
|
||||
};
|
||||
const route = routeMap[event.sourceModule];
|
||||
if (route) {
|
||||
goto(route);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Native calendar events: open detail modal
|
||||
selectedEvent = event;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue