mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
feat(calendar): add search highlighting to calendar views
- Add searchStore for managing search state and event highlighting - Integrate InputBar search with calendar view highlighting - Dim non-matching events and highlight matching events during search - Add search-highlighted and search-dimmed CSS classes to all views - Adjust toolbar position for DateStrip component 🤖 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
7ff8213bd6
commit
51912a285d
7 changed files with 221 additions and 18 deletions
|
|
@ -55,7 +55,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<PillToolbar position="bottom" bottomOffset="70px">
|
||||
<PillToolbar position="bottom" bottomOffset="140px">
|
||||
<!-- Calendar selector -->
|
||||
<PillCalendarSelector direction="up" embedded={true} />
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { eventsStore } from '$lib/stores/events.svelte';
|
||||
import { calendarsStore } from '$lib/stores/calendars.svelte';
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { searchStore } from '$lib/stores/search.svelte';
|
||||
import { todosStore, type Task } from '$lib/stores/todos.svelte';
|
||||
import TaskBlock from './TaskBlock.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
|
@ -677,6 +678,8 @@
|
|||
{#each headerAllDayEvents as event}
|
||||
<button
|
||||
class="all-day-event"
|
||||
class:search-highlighted={searchStore.isEventHighlighted(event.id)}
|
||||
class:search-dimmed={searchStore.isEventDimmed(event.id)}
|
||||
style="background-color: {calendarsStore.getColor(event.calendarId)}"
|
||||
onclick={(e) => handleEventClick(event, e)}
|
||||
>
|
||||
|
|
@ -719,6 +722,8 @@
|
|||
{#each blockAllDayEvents as event}
|
||||
<button
|
||||
class="all-day-block-event"
|
||||
class:search-highlighted={searchStore.isEventHighlighted(event.id)}
|
||||
class:search-dimmed={searchStore.isEventDimmed(event.id)}
|
||||
style="background-color: {calendarsStore.getColor(event.calendarId)}"
|
||||
onclick={(e) => handleEventClick(event, e)}
|
||||
>
|
||||
|
|
@ -731,12 +736,16 @@
|
|||
{@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}
|
||||
style={isBeingDragged
|
||||
? `top: ${dragPreviewTop}%; height: ${dragPreviewHeight}%; background-color: ${calendarsStore.getColor(event.calendarId)};`
|
||||
|
|
@ -844,6 +853,18 @@
|
|||
border-radius: var(--radius-sm);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.all-day-event.search-highlighted {
|
||||
outline: 2px solid hsl(var(--color-primary));
|
||||
outline-offset: 1px;
|
||||
box-shadow: 0 0 0 3px hsl(var(--color-primary) / 0.3);
|
||||
}
|
||||
|
||||
.all-day-event.search-dimmed {
|
||||
opacity: 0.35;
|
||||
filter: grayscale(0.3);
|
||||
}
|
||||
|
||||
/* Block-style all-day events (displayed as full-day blocks in the grid) */
|
||||
|
|
@ -870,6 +891,17 @@
|
|||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.all-day-block-event.search-highlighted {
|
||||
opacity: 0.6;
|
||||
outline: 2px solid hsl(var(--color-primary));
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.all-day-block-event.search-dimmed {
|
||||
opacity: 0.15;
|
||||
filter: grayscale(0.5);
|
||||
}
|
||||
|
||||
.all-day-block-event .event-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
|
|
@ -969,6 +1001,21 @@
|
|||
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% {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { eventsStore } from '$lib/stores/events.svelte';
|
||||
import { calendarsStore } from '$lib/stores/calendars.svelte';
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { searchStore } from '$lib/stores/search.svelte';
|
||||
import { todosStore } from '$lib/stores/todos.svelte';
|
||||
import TodoDayCell from './TodoDayCell.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
|
@ -286,11 +287,15 @@
|
|||
{#each getEventsForDay(day) as event}
|
||||
{@const isBeingDragged = isDragging && draggedEvent?.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-pill"
|
||||
class:dragging={isBeingDragged}
|
||||
class:draft={isDraft}
|
||||
class:search-highlighted={isSearchHighlighted}
|
||||
class:search-dimmed={isSearchDimmed}
|
||||
data-event-id={event.id}
|
||||
style="background-color: {calendarsStore.getColor(event.calendarId)}"
|
||||
onpointerdown={(e) => startDrag(event, e)}
|
||||
|
|
@ -458,6 +463,20 @@
|
|||
animation: pulse-outline 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Search highlighting */
|
||||
.event-pill.search-highlighted {
|
||||
outline: 2px solid hsl(var(--color-primary));
|
||||
outline-offset: 1px;
|
||||
box-shadow: 0 0 0 3px hsl(var(--color-primary) / 0.3);
|
||||
z-index: 10;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.event-pill.search-dimmed {
|
||||
opacity: 0.35;
|
||||
filter: grayscale(0.3);
|
||||
}
|
||||
|
||||
@keyframes pulse-outline {
|
||||
0%,
|
||||
100% {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { eventsStore } from '$lib/stores/events.svelte';
|
||||
import { calendarsStore } from '$lib/stores/calendars.svelte';
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { searchStore } from '$lib/stores/search.svelte';
|
||||
import { todosStore, type Task } from '$lib/stores/todos.svelte';
|
||||
import TaskBlock from './TaskBlock.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
|
@ -781,6 +782,8 @@
|
|||
{#each getHeaderAllDayEventsForDay(day) as event}
|
||||
<button
|
||||
class="all-day-event"
|
||||
class:search-highlighted={searchStore.isEventHighlighted(event.id)}
|
||||
class:search-dimmed={searchStore.isEventDimmed(event.id)}
|
||||
style="background-color: {calendarsStore.getColor(event.calendarId)}"
|
||||
onclick={(e) => handleEventClick(event, e)}
|
||||
title={event.title}
|
||||
|
|
@ -841,6 +844,8 @@
|
|||
{#each getBlockAllDayEventsForDay(day) as event (event.id)}
|
||||
<button
|
||||
class="all-day-block-event"
|
||||
class:search-highlighted={searchStore.isEventHighlighted(event.id)}
|
||||
class:search-dimmed={searchStore.isEventDimmed(event.id)}
|
||||
style="background-color: {calendarsStore.getColor(event.calendarId)}"
|
||||
onclick={(e) => handleEventClick(event, e)}
|
||||
title={event.title}
|
||||
|
|
@ -856,12 +861,16 @@
|
|||
{@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}
|
||||
style={isBeingDragged && !isCrossDayDrag
|
||||
? `top: ${dragPreviewTop}%; height: ${dragPreviewHeight}%; background-color: ${calendarsStore.getColor(event.calendarId)};`
|
||||
|
|
@ -995,6 +1004,18 @@
|
|||
border: none;
|
||||
cursor: pointer;
|
||||
max-width: 100%;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.all-day-event.search-highlighted {
|
||||
outline: 2px solid hsl(var(--color-primary));
|
||||
outline-offset: 1px;
|
||||
box-shadow: 0 0 0 3px hsl(var(--color-primary) / 0.3);
|
||||
}
|
||||
|
||||
.all-day-event.search-dimmed {
|
||||
opacity: 0.35;
|
||||
filter: grayscale(0.3);
|
||||
}
|
||||
|
||||
.compact .all-day-event,
|
||||
|
|
@ -1034,6 +1055,17 @@
|
|||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.all-day-block-event.search-highlighted {
|
||||
opacity: 0.6;
|
||||
outline: 2px solid hsl(var(--color-primary));
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.all-day-block-event.search-dimmed {
|
||||
opacity: 0.15;
|
||||
filter: grayscale(0.5);
|
||||
}
|
||||
|
||||
.all-day-block-event .event-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
|
|
@ -1214,6 +1246,21 @@
|
|||
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% {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { eventsStore } from '$lib/stores/events.svelte';
|
||||
import { calendarsStore } from '$lib/stores/calendars.svelte';
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { searchStore } from '$lib/stores/search.svelte';
|
||||
import { todosStore, type Task } from '$lib/stores/todos.svelte';
|
||||
import TaskBlock from './TaskBlock.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
|
@ -818,6 +819,8 @@
|
|||
{#each getHeaderAllDayEventsForDay(day) as event}
|
||||
<button
|
||||
class="all-day-event"
|
||||
class:search-highlighted={searchStore.isEventHighlighted(event.id)}
|
||||
class:search-dimmed={searchStore.isEventDimmed(event.id)}
|
||||
style="background-color: {calendarsStore.getColor(event.calendarId)}"
|
||||
onclick={() => goto(`/?event=${event.id}`)}
|
||||
>
|
||||
|
|
@ -875,6 +878,8 @@
|
|||
{#each getBlockAllDayEventsForDay(day) as event (event.id)}
|
||||
<button
|
||||
class="all-day-block-event"
|
||||
class:search-highlighted={searchStore.isEventHighlighted(event.id)}
|
||||
class:search-dimmed={searchStore.isEventDimmed(event.id)}
|
||||
style="background-color: {calendarsStore.getColor(event.calendarId)}"
|
||||
onclick={() => goto(`/?event=${event.id}`)}
|
||||
>
|
||||
|
|
@ -889,12 +894,16 @@
|
|||
{@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}
|
||||
style={isBeingDragged && !isCrossDayDrag
|
||||
? `top: ${dragPreviewTop}%; height: ${dragPreviewHeight}%; background-color: ${calendarsStore.getColor(event.calendarId)};`
|
||||
|
|
@ -1028,6 +1037,18 @@
|
|||
text-overflow: ellipsis;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.all-day-event.search-highlighted {
|
||||
outline: 2px solid hsl(var(--color-primary));
|
||||
outline-offset: 1px;
|
||||
box-shadow: 0 0 0 3px hsl(var(--color-primary) / 0.3);
|
||||
}
|
||||
|
||||
.all-day-event.search-dimmed {
|
||||
opacity: 0.35;
|
||||
filter: grayscale(0.3);
|
||||
}
|
||||
|
||||
/* Block-style all-day events (displayed as full-day blocks in the grid) */
|
||||
|
|
@ -1054,6 +1075,17 @@
|
|||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.all-day-block-event.search-highlighted {
|
||||
opacity: 0.6;
|
||||
outline: 2px solid hsl(var(--color-primary));
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.all-day-block-event.search-dimmed {
|
||||
opacity: 0.15;
|
||||
filter: grayscale(0.5);
|
||||
}
|
||||
|
||||
.all-day-block-event .event-title {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
|
|
@ -1225,6 +1257,21 @@
|
|||
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);
|
||||
}
|
||||
|
||||
/* Task drag ghost */
|
||||
.task-drag-ghost {
|
||||
position: absolute;
|
||||
|
|
|
|||
45
apps/calendar/apps/web/src/lib/stores/search.svelte.ts
Normal file
45
apps/calendar/apps/web/src/lib/stores/search.svelte.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* Search Store - manages search state for highlighting events in calendar views
|
||||
*/
|
||||
|
||||
interface SearchItem {
|
||||
id: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
class SearchStore {
|
||||
// Current search query
|
||||
query = $state('');
|
||||
|
||||
// Event IDs that match the search
|
||||
matchingEventIds = $state<Set<string>>(new Set());
|
||||
|
||||
// Whether search is active (user is typing in InputBar)
|
||||
isSearching = $state(false);
|
||||
|
||||
// Set search query and matching items (events or any items with an id)
|
||||
setSearch(query: string, matchingItems: SearchItem[]) {
|
||||
this.query = query;
|
||||
this.matchingEventIds = new Set(matchingItems.map((item) => item.id));
|
||||
this.isSearching = query.trim().length > 0;
|
||||
}
|
||||
|
||||
// Clear search
|
||||
clear() {
|
||||
this.query = '';
|
||||
this.matchingEventIds = new Set();
|
||||
this.isSearching = false;
|
||||
}
|
||||
|
||||
// Check if an event matches the search
|
||||
isEventHighlighted(eventId: string): boolean {
|
||||
return this.isSearching && this.matchingEventIds.has(eventId);
|
||||
}
|
||||
|
||||
// Check if an event should be dimmed (search active but event doesn't match)
|
||||
isEventDimmed(eventId: string): boolean {
|
||||
return this.isSearching && !this.matchingEventIds.has(eventId);
|
||||
}
|
||||
}
|
||||
|
||||
export const searchStore = new SearchStore();
|
||||
|
|
@ -8,7 +8,6 @@
|
|||
PillNavItem,
|
||||
PillDropdownItem,
|
||||
QuickInputItem,
|
||||
QuickAction,
|
||||
CreatePreview,
|
||||
} from '@manacore/shared-ui';
|
||||
import { theme } from '$lib/stores/theme';
|
||||
|
|
@ -34,6 +33,7 @@
|
|||
import { getPillAppItems } from '@manacore/shared-branding';
|
||||
import { setLocale, supportedLocales } from '$lib/i18n';
|
||||
import { searchEvents } from '$lib/api/events';
|
||||
import { searchStore } from '$lib/stores/search.svelte';
|
||||
import { format } from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
import {
|
||||
|
|
@ -49,19 +49,7 @@
|
|||
|
||||
let { children } = $props();
|
||||
|
||||
// QuickInputBar quick actions
|
||||
const quickActions: QuickAction[] = [
|
||||
{
|
||||
id: 'today',
|
||||
label: 'Heute',
|
||||
icon: 'calendar',
|
||||
onclick: () => viewStore.goToToday(),
|
||||
},
|
||||
{ id: 'agenda', label: 'Agenda', icon: 'list', href: '/agenda' },
|
||||
{ id: 'settings', label: 'Einstellungen', icon: 'settings', href: '/settings' },
|
||||
];
|
||||
|
||||
// QuickInputBar search - search events
|
||||
// InputBar search - search events
|
||||
async function handleSearch(query: string): Promise<QuickInputItem[]> {
|
||||
if (!query.trim()) return [];
|
||||
|
||||
|
|
@ -76,9 +64,19 @@
|
|||
}
|
||||
|
||||
function handleSelect(item: QuickInputItem) {
|
||||
searchStore.clear();
|
||||
goto(`/event/${item.id}`);
|
||||
}
|
||||
|
||||
// Update search store when search changes (for calendar view highlighting)
|
||||
function handleSearchChange(query: string, results: QuickInputItem[]) {
|
||||
if (!query.trim()) {
|
||||
searchStore.clear();
|
||||
} else {
|
||||
searchStore.setSearch(query, results);
|
||||
}
|
||||
}
|
||||
|
||||
// QuickInputBar Quick-Create handlers
|
||||
function handleParseCreate(query: string): CreatePreview | null {
|
||||
if (!query.trim()) return null;
|
||||
|
|
@ -347,11 +345,11 @@
|
|||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Global Quick Input Bar -->
|
||||
<!-- Global Input Bar -->
|
||||
<QuickInputBar
|
||||
onSearch={handleSearch}
|
||||
onSelect={handleSelect}
|
||||
{quickActions}
|
||||
onSearchChange={handleSearchChange}
|
||||
placeholder="Neuer Termin oder suchen..."
|
||||
emptyText="Keine Termine gefunden"
|
||||
searchingText="Suche..."
|
||||
|
|
@ -360,7 +358,7 @@
|
|||
createText="Erstellen"
|
||||
appIcon="calendar"
|
||||
primaryColor="#3b82f6"
|
||||
autoFocus={false}
|
||||
autoFocus={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue