mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:41:09 +02:00
feat(calendar): add birthday calendar and collapsible date strip
- Add virtual birthday calendar to calendars store - Create DateStripFab component for collapsed date strip mode - Add dateStripCollapsed setting support in layout - Adjust InputBar positioning for FAB visibility 🤖 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
0badca21ae
commit
cc28cc739e
3 changed files with 197 additions and 1 deletions
|
|
@ -0,0 +1,113 @@
|
|||
<script lang="ts">
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { format } from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
import DateStripContextMenu from './DateStripContextMenu.svelte';
|
||||
|
||||
interface Props {
|
||||
isSidebarMode?: boolean;
|
||||
isToolbarExpanded?: boolean;
|
||||
}
|
||||
|
||||
let { isSidebarMode = false, isToolbarExpanded = false }: Props = $props();
|
||||
|
||||
let contextMenu: DateStripContextMenu;
|
||||
|
||||
function handleClick() {
|
||||
settingsStore.set('dateStripCollapsed', false);
|
||||
}
|
||||
|
||||
function handleContextMenu(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
contextMenu?.show(e.clientX, e.clientY);
|
||||
}
|
||||
|
||||
// Format current date for FAB display: "Dez 14"
|
||||
let fabLabel = $derived(format(new Date(), 'MMM d', { locale: de }));
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="datestrip-fab-container"
|
||||
class:sidebar-mode={isSidebarMode}
|
||||
class:toolbar-expanded={isToolbarExpanded}
|
||||
>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<button
|
||||
onclick={handleClick}
|
||||
oncontextmenu={handleContextMenu}
|
||||
class="datestrip-fab"
|
||||
title="Datumsleiste erweitern (Rechtsklick für Optionen)"
|
||||
>
|
||||
<span class="fab-label">{fabLabel}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<DateStripContextMenu bind:this={contextMenu} />
|
||||
|
||||
<style>
|
||||
.datestrip-fab-container {
|
||||
position: fixed;
|
||||
bottom: calc(70px + 9px + env(safe-area-inset-bottom, 0px));
|
||||
/* Position left of InputBar: center (50%) minus half of InputBar (225px) minus gap (8px) minus fab width (54px) */
|
||||
left: calc(50% - 225px - 8px - 54px);
|
||||
z-index: 49;
|
||||
pointer-events: none;
|
||||
transition:
|
||||
bottom 0.2s ease,
|
||||
left 0.2s ease;
|
||||
}
|
||||
|
||||
.datestrip-fab-container.sidebar-mode {
|
||||
bottom: calc(9px + env(safe-area-inset-bottom, 0px));
|
||||
/* In sidebar mode, InputBar is 700px wide, so position accordingly */
|
||||
left: calc(50% - 350px - 8px - 54px);
|
||||
}
|
||||
|
||||
.datestrip-fab-container.toolbar-expanded {
|
||||
bottom: calc(140px + 9px + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.datestrip-fab-container.sidebar-mode.toolbar-expanded {
|
||||
bottom: calc(70px + 9px + env(safe-area-inset-bottom, 0px));
|
||||
/* In sidebar mode, InputBar is 700px wide */
|
||||
left: calc(50% - 350px - 8px - 54px);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.datestrip-fab-container {
|
||||
left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.datestrip-fab {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 54px;
|
||||
height: 54px;
|
||||
padding: 0 0.75rem;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all 0.2s ease;
|
||||
pointer-events: auto;
|
||||
background: hsl(var(--color-surface) / 0.85);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
box-shadow: 0 2px 8px hsl(var(--color-foreground) / 0.08);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.datestrip-fab:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 12px hsl(var(--color-foreground) / 0.15);
|
||||
}
|
||||
|
||||
.fab-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--color-foreground));
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -4,18 +4,42 @@
|
|||
|
||||
import type { Calendar, CreateCalendarInput, UpdateCalendarInput } from '@calendar/shared';
|
||||
import * as api from '$lib/api/calendars';
|
||||
import { BIRTHDAY_CALENDAR } from '$lib/api/birthdays';
|
||||
import { settingsStore } from './settings.svelte';
|
||||
|
||||
// State
|
||||
let calendars = $state<Calendar[]>([]);
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Virtual birthday calendar (created dynamically based on settings)
|
||||
const birthdayCalendar: Calendar = {
|
||||
id: BIRTHDAY_CALENDAR.id,
|
||||
userId: '',
|
||||
name: BIRTHDAY_CALENDAR.name,
|
||||
color: BIRTHDAY_CALENDAR.color,
|
||||
isDefault: false,
|
||||
isVisible: true, // Visibility controlled by settingsStore.showBirthdays
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Helper to safely get calendars array (Svelte 5 runes safety)
|
||||
function getCalendarsArray(): Calendar[] {
|
||||
const arr = calendars ?? [];
|
||||
return Array.isArray(arr) ? arr : [];
|
||||
}
|
||||
|
||||
// Derived: all calendars including virtual birthday calendar
|
||||
const allCalendars = $derived.by(() => {
|
||||
const userCalendars = getCalendarsArray();
|
||||
// Add virtual birthday calendar if birthdays are enabled in settings
|
||||
if (settingsStore.showBirthdays) {
|
||||
return [...userCalendars, { ...birthdayCalendar, isVisible: true }];
|
||||
}
|
||||
return userCalendars;
|
||||
});
|
||||
|
||||
// Derived: visible calendars
|
||||
const visibleCalendars = $derived(getCalendarsArray().filter((c) => c.isVisible));
|
||||
|
||||
|
|
@ -30,6 +54,9 @@ export const calendarsStore = {
|
|||
get calendars() {
|
||||
return calendars;
|
||||
},
|
||||
get allCalendars() {
|
||||
return allCalendars;
|
||||
},
|
||||
get visibleCalendars() {
|
||||
return visibleCalendars;
|
||||
},
|
||||
|
|
@ -42,6 +69,9 @@ export const calendarsStore = {
|
|||
get error() {
|
||||
return error;
|
||||
},
|
||||
get birthdayCalendarId() {
|
||||
return BIRTHDAY_CALENDAR.id;
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch all calendars
|
||||
|
|
@ -143,7 +173,26 @@ export const calendarsStore = {
|
|||
* Get calendar color by ID (with fallback)
|
||||
*/
|
||||
getColor(id: string) {
|
||||
// Handle virtual birthday calendar
|
||||
if (id === BIRTHDAY_CALENDAR.id) {
|
||||
return BIRTHDAY_CALENDAR.color;
|
||||
}
|
||||
const calendar = getCalendarsArray().find((c) => c.id === id);
|
||||
return calendar?.color || '#3b82f6';
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle birthday calendar visibility
|
||||
* (This updates the settings store, not the calendar itself)
|
||||
*/
|
||||
toggleBirthdaysVisibility() {
|
||||
settingsStore.set('showBirthdays', !settingsStore.showBirthdays);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if a calendar ID is the virtual birthday calendar
|
||||
*/
|
||||
isBirthdayCalendar(id: string) {
|
||||
return id === BIRTHDAY_CALENDAR.id;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@
|
|||
import CalendarToolbar from '$lib/components/calendar/CalendarToolbar.svelte';
|
||||
import CalendarToolbarContent from '$lib/components/calendar/CalendarToolbarContent.svelte';
|
||||
import DateStrip from '$lib/components/calendar/DateStrip.svelte';
|
||||
import DateStripFab from '$lib/components/calendar/DateStripFab.svelte';
|
||||
import EventContextMenu from '$lib/components/event/EventContextMenu.svelte';
|
||||
import { eventContextMenuStore } from '$lib/stores/eventContextMenu.svelte';
|
||||
|
||||
|
|
@ -387,8 +388,12 @@
|
|||
|
||||
<!-- Date strip (only on main calendar page) -->
|
||||
{#if showCalendarToolbar}
|
||||
{#if settingsStore.dateStripCollapsed}
|
||||
<DateStripFab {isSidebarMode} isToolbarExpanded={!isToolbarCollapsed} />
|
||||
{:else}
|
||||
<DateStrip {isSidebarMode} isToolbarExpanded={!isToolbarCollapsed} />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Calendar toolbar (only on main calendar page, not in sidebar mode) -->
|
||||
{#if showCalendarToolbar && !isSidebarMode}
|
||||
|
|
@ -433,6 +438,7 @@
|
|||
? '140px'
|
||||
: '70px'}
|
||||
hasFabRight={showCalendarToolbar && !isSidebarMode}
|
||||
hasFabLeft={showCalendarToolbar && !isSidebarMode && settingsStore.dateStripCollapsed}
|
||||
/>
|
||||
</div>
|
||||
</SplitPaneContainer>
|
||||
|
|
@ -517,4 +523,32 @@
|
|||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Adjust InputBar when FABs are visible (toolbar FAB on right, DateStripFab on left) */
|
||||
/* For a centered InputBar with max-width 450px, left edge is at 50% - 225px */
|
||||
/* DateStripFab is positioned at: 50% - 225px - 8px gap - 54px fab width */
|
||||
/* Note: In sidebar mode, InputBar uses default 700px max-width */
|
||||
:global(.quick-input-bar.has-fab-right .input-container),
|
||||
:global(.quick-input-bar.has-fab-left .input-container) {
|
||||
max-width: 450px;
|
||||
}
|
||||
|
||||
/* On smaller screens (<900px), FABs move to fixed positions (left: 1rem, right: 1rem) */
|
||||
@media (max-width: 900px) {
|
||||
:global(.quick-input-bar.has-fab-right .input-container) {
|
||||
max-width: calc(100% - 140px); /* 54px FAB + padding */
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
}
|
||||
:global(.quick-input-bar.has-fab-left .input-container) {
|
||||
max-width: calc(100% - 140px); /* 54px FAB + padding */
|
||||
margin-left: 0;
|
||||
margin-right: auto;
|
||||
}
|
||||
:global(.quick-input-bar.has-fab-right.has-fab-left .input-container) {
|
||||
max-width: calc(100% - 200px); /* Both FABs */
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue