From cc28cc739e85ae404224147f3240471481a5acf2 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Sun, 14 Dec 2025 21:29:58 +0100 Subject: [PATCH] feat(calendar): add birthday calendar and collapsible date strip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../components/calendar/DateStripFab.svelte | 113 ++++++++++++++++++ .../web/src/lib/stores/calendars.svelte.ts | 49 ++++++++ .../apps/web/src/routes/(app)/+layout.svelte | 36 +++++- 3 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 apps/calendar/apps/web/src/lib/components/calendar/DateStripFab.svelte diff --git a/apps/calendar/apps/web/src/lib/components/calendar/DateStripFab.svelte b/apps/calendar/apps/web/src/lib/components/calendar/DateStripFab.svelte new file mode 100644 index 000000000..94eedf059 --- /dev/null +++ b/apps/calendar/apps/web/src/lib/components/calendar/DateStripFab.svelte @@ -0,0 +1,113 @@ + + +
+ + +
+ + + + diff --git a/apps/calendar/apps/web/src/lib/stores/calendars.svelte.ts b/apps/calendar/apps/web/src/lib/stores/calendars.svelte.ts index 1f8fc7515..bf9489a31 100644 --- a/apps/calendar/apps/web/src/lib/stores/calendars.svelte.ts +++ b/apps/calendar/apps/web/src/lib/stores/calendars.svelte.ts @@ -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([]); let loading = $state(false); let error = $state(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; + }, }; diff --git a/apps/calendar/apps/web/src/routes/(app)/+layout.svelte b/apps/calendar/apps/web/src/routes/(app)/+layout.svelte index fcc0688c6..52066a92b 100644 --- a/apps/calendar/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/calendar/apps/web/src/routes/(app)/+layout.svelte @@ -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,7 +388,11 @@ {#if showCalendarToolbar} - + {#if settingsStore.dateStripCollapsed} + + {:else} + + {/if} {/if} @@ -433,6 +438,7 @@ ? '140px' : '70px'} hasFabRight={showCalendarToolbar && !isSidebarMode} + hasFabLeft={showCalendarToolbar && !isSidebarMode && settingsStore.dateStripCollapsed} /> @@ -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; + } + }