mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:01:08 +02:00
feat(calendar): add number labels to ViewSwitcher and extended day views
- Replace icons with number labels (1, 3, 5, 7, 10, 14, 30, 60, 90, 365, M, Y, L) - Add new standard view types: 30day, 60day, 90day, 365day - Add 3day view as standard option - Add custom day range input (1-365 days) in context menu - Update PillTabGroup to show labels when no icon is provided - Change MultiDayView dayCount prop from union to number type - Add ultra-compact class for views with 14+ days 🤖 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
1395291b49
commit
3edb65c2c3
9 changed files with 627 additions and 63 deletions
|
|
@ -42,7 +42,7 @@
|
|||
|
||||
// Props
|
||||
interface Props {
|
||||
dayCount: 5 | 10 | 14;
|
||||
dayCount: number;
|
||||
/** Optional date override for carousel navigation (uses viewStore.currentDate if not provided) */
|
||||
date?: Date;
|
||||
onQuickCreate?: (date: Date, position: { x: number; y: number }) => void;
|
||||
|
|
@ -105,7 +105,8 @@
|
|||
let columnClass = $derived.by(() => {
|
||||
if (days.length <= 5) return 'normal';
|
||||
if (days.length <= 10) return 'compact';
|
||||
return 'very-compact';
|
||||
if (days.length <= 14) return 'very-compact';
|
||||
return 'ultra-compact';
|
||||
});
|
||||
|
||||
// ========== Drag & Drop State ==========
|
||||
|
|
@ -823,6 +824,7 @@
|
|||
class="multi-day-view"
|
||||
class:compact={columnClass === 'compact'}
|
||||
class:very-compact={columnClass === 'very-compact'}
|
||||
class:ultra-compact={columnClass === 'ultra-compact'}
|
||||
>
|
||||
<!-- Sticky header container -->
|
||||
<div class="sticky-header">
|
||||
|
|
@ -1527,4 +1529,61 @@
|
|||
.very-compact .overflow-line:hover {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
/* Ultra-compact mode for 14+ days */
|
||||
.ultra-compact .day-header {
|
||||
padding: 0.0625rem;
|
||||
}
|
||||
|
||||
.ultra-compact .day-name {
|
||||
font-size: 0.55rem;
|
||||
}
|
||||
|
||||
.ultra-compact .day-number {
|
||||
font-size: 0.75rem;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.ultra-compact .time-label {
|
||||
font-size: 0.55rem;
|
||||
padding-right: 0.125rem;
|
||||
}
|
||||
|
||||
.ultra-compact .event-card {
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 0 1px;
|
||||
}
|
||||
|
||||
.ultra-compact .event-title {
|
||||
font-size: 0.5rem;
|
||||
}
|
||||
|
||||
.ultra-compact .all-day-event {
|
||||
padding: 1px 2px;
|
||||
font-size: 0.55rem;
|
||||
}
|
||||
|
||||
.ultra-compact .all-day-block-event {
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 1px 2px;
|
||||
}
|
||||
|
||||
.ultra-compact .all-day-block-event .event-title {
|
||||
font-size: 0.5rem;
|
||||
}
|
||||
|
||||
.ultra-compact .resize-handle {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.ultra-compact .overflow-line {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.ultra-compact .overflow-line:hover {
|
||||
height: 3px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -311,6 +311,14 @@
|
|||
<MultiDayView dayCount={10} date={prevDate} />
|
||||
{:else if viewStore.viewType === '14day'}
|
||||
<MultiDayView dayCount={14} date={prevDate} />
|
||||
{:else if viewStore.viewType === '30day'}
|
||||
<MultiDayView dayCount={30} date={prevDate} />
|
||||
{:else if viewStore.viewType === '60day'}
|
||||
<MultiDayView dayCount={60} date={prevDate} />
|
||||
{:else if viewStore.viewType === '90day'}
|
||||
<MultiDayView dayCount={90} date={prevDate} />
|
||||
{:else if viewStore.viewType === '365day'}
|
||||
<MultiDayView dayCount={365} date={prevDate} />
|
||||
{:else if viewStore.viewType === 'custom'}
|
||||
<MultiDayView dayCount={settingsStore.customDayCount} date={prevDate} />
|
||||
{:else if viewStore.viewType === 'month'}
|
||||
|
|
@ -336,6 +344,14 @@
|
|||
<MultiDayView dayCount={10} {onQuickCreate} {onEventClick} />
|
||||
{:else if viewStore.viewType === '14day'}
|
||||
<MultiDayView dayCount={14} {onQuickCreate} {onEventClick} />
|
||||
{:else if viewStore.viewType === '30day'}
|
||||
<MultiDayView dayCount={30} {onQuickCreate} {onEventClick} />
|
||||
{:else if viewStore.viewType === '60day'}
|
||||
<MultiDayView dayCount={60} {onQuickCreate} {onEventClick} />
|
||||
{:else if viewStore.viewType === '90day'}
|
||||
<MultiDayView dayCount={90} {onQuickCreate} {onEventClick} />
|
||||
{:else if viewStore.viewType === '365day'}
|
||||
<MultiDayView dayCount={365} {onQuickCreate} {onEventClick} />
|
||||
{:else if viewStore.viewType === 'custom'}
|
||||
<MultiDayView dayCount={settingsStore.customDayCount} {onQuickCreate} {onEventClick} />
|
||||
{:else if viewStore.viewType === 'month'}
|
||||
|
|
@ -363,6 +379,14 @@
|
|||
<MultiDayView dayCount={10} date={nextDate} />
|
||||
{:else if viewStore.viewType === '14day'}
|
||||
<MultiDayView dayCount={14} date={nextDate} />
|
||||
{:else if viewStore.viewType === '30day'}
|
||||
<MultiDayView dayCount={30} date={nextDate} />
|
||||
{:else if viewStore.viewType === '60day'}
|
||||
<MultiDayView dayCount={60} date={nextDate} />
|
||||
{:else if viewStore.viewType === '90day'}
|
||||
<MultiDayView dayCount={90} date={nextDate} />
|
||||
{:else if viewStore.viewType === '365day'}
|
||||
<MultiDayView dayCount={365} date={nextDate} />
|
||||
{:else if viewStore.viewType === 'custom'}
|
||||
<MultiDayView dayCount={settingsStore.customDayCount} date={nextDate} />
|
||||
{:else if viewStore.viewType === 'month'}
|
||||
|
|
|
|||
|
|
@ -1,38 +1,85 @@
|
|||
<script lang="ts">
|
||||
import { ContextMenu, type ContextMenuItem } from '@manacore/shared-ui';
|
||||
import { Calendar, ListBullets, GridFour, CalendarBlank } from '@manacore/shared-icons';
|
||||
import { onMount } from 'svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { viewStore } from '$lib/stores/view.svelte';
|
||||
import type { CalendarViewType } from '@calendar/shared';
|
||||
|
||||
// Context menu state
|
||||
let visible = $state(false);
|
||||
let x = $state(0);
|
||||
let y = $state(0);
|
||||
let menuElement = $state<HTMLElement | null>(null);
|
||||
let adjustedX = $state(0);
|
||||
let adjustedY = $state(0);
|
||||
|
||||
// Custom day count input state
|
||||
let customDayInput = $state(String(settingsStore.customDayCount));
|
||||
|
||||
// View labels
|
||||
const viewLabels: Record<CalendarViewType, string> = {
|
||||
day: 'Tag',
|
||||
day: 'Tag (1)',
|
||||
'3day': '3 Tage',
|
||||
'5day': '5 Tage',
|
||||
week: 'Woche',
|
||||
week: 'Woche (7)',
|
||||
'10day': '10 Tage',
|
||||
'14day': '14 Tage',
|
||||
'30day': '30 Tage',
|
||||
'60day': '60 Tage',
|
||||
'90day': '90 Tage',
|
||||
'365day': '365 Tage',
|
||||
month: 'Monat',
|
||||
year: 'Jahr',
|
||||
agenda: 'Agenda',
|
||||
custom: 'Benutzerdefiniert',
|
||||
};
|
||||
|
||||
// All available views
|
||||
// All available views (ordered)
|
||||
const allViews: CalendarViewType[] = [
|
||||
'day',
|
||||
'3day',
|
||||
'5day',
|
||||
'week',
|
||||
'10day',
|
||||
'14day',
|
||||
'30day',
|
||||
'60day',
|
||||
'90day',
|
||||
'365day',
|
||||
'month',
|
||||
'year',
|
||||
'agenda',
|
||||
'custom',
|
||||
];
|
||||
|
||||
// Adjust position to keep menu within viewport
|
||||
$effect(() => {
|
||||
if (visible && menuElement) {
|
||||
const rect = menuElement.getBoundingClientRect();
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
// Adjust X if menu would overflow right
|
||||
if (x + rect.width > viewportWidth - 10) {
|
||||
adjustedX = x - rect.width;
|
||||
} else {
|
||||
adjustedX = x;
|
||||
}
|
||||
|
||||
// Adjust Y if menu would overflow bottom
|
||||
if (y + rect.height > viewportHeight - 10) {
|
||||
adjustedY = y - rect.height;
|
||||
} else {
|
||||
adjustedY = y;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Sync custom day input when settings change
|
||||
$effect(() => {
|
||||
customDayInput = String(settingsStore.customDayCount);
|
||||
});
|
||||
|
||||
function isViewEnabled(view: CalendarViewType): boolean {
|
||||
return settingsStore.quickViewPillViews.includes(view);
|
||||
}
|
||||
|
|
@ -53,42 +100,75 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Build menu items
|
||||
let menuItems = $derived.by((): ContextMenuItem[] => {
|
||||
return allViews.map((view) => ({
|
||||
id: view,
|
||||
label: viewLabels[view],
|
||||
icon: getViewIcon(view),
|
||||
toggle: true,
|
||||
checked: isViewEnabled(view),
|
||||
action: () => toggleView(view),
|
||||
}));
|
||||
});
|
||||
function handleCustomDayInputChange(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
customDayInput = target.value;
|
||||
}
|
||||
|
||||
// Get appropriate icon for view type
|
||||
function getViewIcon(view: CalendarViewType) {
|
||||
switch (view) {
|
||||
case 'day':
|
||||
case '5day':
|
||||
case '10day':
|
||||
case '14day':
|
||||
return CalendarBlank;
|
||||
case 'week':
|
||||
return Calendar;
|
||||
case 'month':
|
||||
case 'year':
|
||||
return GridFour;
|
||||
case 'agenda':
|
||||
return ListBullets;
|
||||
default:
|
||||
return Calendar;
|
||||
function applyCustomDays() {
|
||||
const value = parseInt(customDayInput, 10);
|
||||
if (isNaN(value) || value < 1 || value > 365) {
|
||||
// Reset to current value if invalid
|
||||
customDayInput = String(settingsStore.customDayCount);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set custom day count
|
||||
settingsStore.set('customDayCount', value);
|
||||
customDayInput = String(value);
|
||||
|
||||
// Auto-enable 'custom' view if not already
|
||||
const current = settingsStore.quickViewPillViews;
|
||||
if (!current.includes('custom')) {
|
||||
settingsStore.set('quickViewPillViews', [...current, 'custom']);
|
||||
}
|
||||
|
||||
// Switch to custom view
|
||||
viewStore.setViewType('custom');
|
||||
|
||||
// Close the menu
|
||||
visible = false;
|
||||
}
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') {
|
||||
visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
visible = false;
|
||||
function handleInputKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
applyCustomDays();
|
||||
}
|
||||
// Stop propagation to prevent menu from closing
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Close on click outside
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (menuElement && !menuElement.contains(e.target as Node)) {
|
||||
visible = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Close on scroll
|
||||
const handleScroll = () => {
|
||||
visible = false;
|
||||
};
|
||||
|
||||
window.addEventListener('click', handleClickOutside);
|
||||
window.addEventListener('scroll', handleScroll, true);
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('click', handleClickOutside);
|
||||
window.removeEventListener('scroll', handleScroll, true);
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
});
|
||||
|
||||
// Export show function to be called from parent
|
||||
export function show(clientX: number, clientY: number) {
|
||||
x = clientX;
|
||||
|
|
@ -101,4 +181,230 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<ContextMenu {visible} {x} {y} items={menuItems} onClose={handleClose} />
|
||||
{#if visible}
|
||||
<!-- Backdrop to block clicks on elements behind -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="context-menu-backdrop"
|
||||
onpointerdown={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
visible = false;
|
||||
}}
|
||||
onclick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
visible = false;
|
||||
}}
|
||||
oncontextmenu={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
visible = false;
|
||||
}}
|
||||
></div>
|
||||
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<div
|
||||
bind:this={menuElement}
|
||||
class="context-menu"
|
||||
style="left: {adjustedX}px; top: {adjustedY}px;"
|
||||
role="menu"
|
||||
tabindex="-1"
|
||||
transition:fly={{ duration: 150, y: -8 }}
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
oncontextmenu={(e) => e.preventDefault()}
|
||||
onkeydown={handleKeyDown}
|
||||
>
|
||||
<!-- Standard view toggles -->
|
||||
{#each allViews as view}
|
||||
<button class="menu-item has-toggle" onclick={() => toggleView(view)} role="menuitem">
|
||||
<span class="item-toggle" class:checked={isViewEnabled(view)}>
|
||||
<span class="toggle-track">
|
||||
<span class="toggle-thumb"></span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="item-label">{viewLabels[view]}</span>
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- Custom day count section -->
|
||||
<div class="custom-section">
|
||||
<span class="custom-label">Benutzerdefiniert (1-365)</span>
|
||||
<div class="custom-input-row">
|
||||
<input
|
||||
type="number"
|
||||
class="custom-input"
|
||||
min="1"
|
||||
max="365"
|
||||
value={customDayInput}
|
||||
oninput={handleCustomDayInputChange}
|
||||
onkeydown={handleInputKeyDown}
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<span class="custom-unit">Tage</span>
|
||||
<button class="custom-apply-btn" onclick={applyCustomDays}> Setzen </button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.context-menu-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9998;
|
||||
background: transparent;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
min-width: 200px;
|
||||
max-width: 280px;
|
||||
padding: 0.375rem;
|
||||
background: var(--color-surface-elevated-3);
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
border-radius: var(--radius-lg);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.625rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
font-size: 0.8125rem;
|
||||
color: hsl(var(--color-foreground));
|
||||
text-align: left;
|
||||
transition: background-color 100ms ease;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background: hsl(var(--color-muted));
|
||||
}
|
||||
|
||||
.item-label {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
margin: 0.375rem 0.5rem;
|
||||
background: hsl(var(--color-border));
|
||||
}
|
||||
|
||||
/* Toggle switch styles */
|
||||
.item-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toggle-track {
|
||||
position: relative;
|
||||
width: 28px;
|
||||
height: 16px;
|
||||
background: hsl(var(--color-muted));
|
||||
border-radius: 8px;
|
||||
transition: background-color 150ms ease;
|
||||
}
|
||||
|
||||
.toggle-thumb {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: hsl(var(--color-background));
|
||||
border-radius: 50%;
|
||||
transition: transform 150ms ease;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.item-toggle.checked .toggle-track {
|
||||
background: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
.item-toggle.checked .toggle-thumb {
|
||||
transform: translateX(12px);
|
||||
}
|
||||
|
||||
/* Custom section styles */
|
||||
.custom-section {
|
||||
padding: 0.5rem 0.625rem;
|
||||
}
|
||||
|
||||
.custom-label {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.custom-input-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.custom-input {
|
||||
width: 60px;
|
||||
padding: 0.375rem 0.5rem;
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
border-radius: var(--radius-md);
|
||||
background: hsl(var(--color-background));
|
||||
color: hsl(var(--color-foreground));
|
||||
font-size: 0.8125rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.custom-input:focus {
|
||||
outline: none;
|
||||
border-color: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
/* Hide number input spinners */
|
||||
.custom-input::-webkit-outer-spin-button,
|
||||
.custom-input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
.custom-input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
.custom-unit {
|
||||
font-size: 0.8125rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.custom-apply-btn {
|
||||
margin-left: auto;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
background: hsl(var(--color-primary));
|
||||
color: hsl(var(--color-primary-foreground));
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: opacity 150ms ease;
|
||||
}
|
||||
|
||||
.custom-apply-btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@ export interface CalendarAppSettings {
|
|||
dateStripShowWeekNumbers: boolean; // Show week numbers at start of week
|
||||
dateStripCollapsed: boolean; // Whether DateStrip is minimized to FAB
|
||||
|
||||
// TagStrip settings
|
||||
tagStripCollapsed: boolean; // Whether TagStrip is hidden
|
||||
|
||||
// Birthday settings (cross-app integration with Contacts)
|
||||
showBirthdays: boolean; // Show contact birthdays in calendar
|
||||
showBirthdayAge: boolean; // Show age in birthday events
|
||||
|
|
@ -52,6 +55,7 @@ export interface CalendarAppSettings {
|
|||
|
||||
// Quick View Pill settings
|
||||
quickViewPillViews: CalendarViewType[]; // Views shown in quick switcher
|
||||
customDayCount: number; // Custom day count for 'custom' view type (1-365)
|
||||
|
||||
// Event defaults
|
||||
defaultEventDuration: number; // in minutes
|
||||
|
|
@ -82,6 +86,8 @@ const DEFAULT_SETTINGS: CalendarAppSettings = {
|
|||
dateStripCompact: false,
|
||||
dateStripShowWeekNumbers: false,
|
||||
dateStripCollapsed: false,
|
||||
// TagStrip defaults
|
||||
tagStripCollapsed: true, // Hidden by default
|
||||
// Birthday defaults
|
||||
showBirthdays: true,
|
||||
showBirthdayAge: true,
|
||||
|
|
@ -89,6 +95,7 @@ const DEFAULT_SETTINGS: CalendarAppSettings = {
|
|||
sidebarCollapsed: false,
|
||||
// Quick View Pill defaults
|
||||
quickViewPillViews: ['week', 'month', 'agenda'],
|
||||
customDayCount: 30, // Default: 30 days (1 month)
|
||||
// Event defaults
|
||||
defaultEventDuration: 60,
|
||||
defaultReminder: 15,
|
||||
|
|
@ -225,6 +232,10 @@ export const settingsStore = {
|
|||
get dateStripCollapsed() {
|
||||
return settings.dateStripCollapsed;
|
||||
},
|
||||
// TagStrip settings
|
||||
get tagStripCollapsed() {
|
||||
return settings.tagStripCollapsed;
|
||||
},
|
||||
// Birthday settings
|
||||
get showBirthdays() {
|
||||
return settings.showBirthdays;
|
||||
|
|
@ -244,6 +255,9 @@ export const settingsStore = {
|
|||
get quickViewPillViews() {
|
||||
return settings.quickViewPillViews;
|
||||
},
|
||||
get customDayCount() {
|
||||
return settings.customDayCount;
|
||||
},
|
||||
get cloudSyncEnabled() {
|
||||
return cloudSyncEnabled;
|
||||
},
|
||||
|
|
@ -284,6 +298,15 @@ export const settingsStore = {
|
|||
syncToCloud();
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle TagStrip visibility
|
||||
*/
|
||||
toggleTagStrip() {
|
||||
settings = { ...settings, tagStripCollapsed: !settings.tagStripCollapsed };
|
||||
saveSettings(settings);
|
||||
syncToCloud();
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize settings from localStorage
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -38,6 +38,11 @@ const viewRange = $derived.by(() => {
|
|||
start: startOfDay(currentDate),
|
||||
end: endOfDay(currentDate),
|
||||
};
|
||||
case '3day':
|
||||
return {
|
||||
start: startOfDay(currentDate),
|
||||
end: endOfDay(addDays(currentDate, 2)),
|
||||
};
|
||||
case '5day':
|
||||
return {
|
||||
start: startOfDay(currentDate),
|
||||
|
|
@ -58,6 +63,33 @@ const viewRange = $derived.by(() => {
|
|||
start: startOfDay(currentDate),
|
||||
end: endOfDay(addDays(currentDate, 13)),
|
||||
};
|
||||
case '30day':
|
||||
return {
|
||||
start: startOfDay(currentDate),
|
||||
end: endOfDay(addDays(currentDate, 29)),
|
||||
};
|
||||
case '60day':
|
||||
return {
|
||||
start: startOfDay(currentDate),
|
||||
end: endOfDay(addDays(currentDate, 59)),
|
||||
};
|
||||
case '90day':
|
||||
return {
|
||||
start: startOfDay(currentDate),
|
||||
end: endOfDay(addDays(currentDate, 89)),
|
||||
};
|
||||
case '365day':
|
||||
return {
|
||||
start: startOfDay(currentDate),
|
||||
end: endOfDay(addDays(currentDate, 364)),
|
||||
};
|
||||
case 'custom': {
|
||||
const customDays = settingsStore.customDayCount;
|
||||
return {
|
||||
start: startOfDay(currentDate),
|
||||
end: endOfDay(addDays(currentDate, customDays - 1)),
|
||||
};
|
||||
}
|
||||
case 'month':
|
||||
return {
|
||||
start: startOfMonth(currentDate),
|
||||
|
|
@ -108,7 +140,22 @@ export const viewStore = {
|
|||
const savedView = localStorage.getItem('calendar-view-type');
|
||||
if (
|
||||
savedView &&
|
||||
['day', '5day', 'week', '10day', '14day', 'month', 'year', 'agenda'].includes(savedView)
|
||||
[
|
||||
'day',
|
||||
'3day',
|
||||
'5day',
|
||||
'week',
|
||||
'10day',
|
||||
'14day',
|
||||
'30day',
|
||||
'60day',
|
||||
'90day',
|
||||
'365day',
|
||||
'month',
|
||||
'year',
|
||||
'agenda',
|
||||
'custom',
|
||||
].includes(savedView)
|
||||
) {
|
||||
viewType = savedView as CalendarViewType;
|
||||
} else {
|
||||
|
|
@ -149,6 +196,9 @@ export const viewStore = {
|
|||
case 'day':
|
||||
currentDate = subDays(currentDate, 1);
|
||||
break;
|
||||
case '3day':
|
||||
currentDate = subDays(currentDate, 3);
|
||||
break;
|
||||
case '5day':
|
||||
currentDate = subDays(currentDate, 5);
|
||||
break;
|
||||
|
|
@ -161,6 +211,21 @@ export const viewStore = {
|
|||
case '14day':
|
||||
currentDate = subDays(currentDate, 14);
|
||||
break;
|
||||
case '30day':
|
||||
currentDate = subDays(currentDate, 30);
|
||||
break;
|
||||
case '60day':
|
||||
currentDate = subDays(currentDate, 60);
|
||||
break;
|
||||
case '90day':
|
||||
currentDate = subDays(currentDate, 90);
|
||||
break;
|
||||
case '365day':
|
||||
currentDate = subDays(currentDate, 365);
|
||||
break;
|
||||
case 'custom':
|
||||
currentDate = subDays(currentDate, settingsStore.customDayCount);
|
||||
break;
|
||||
case 'month':
|
||||
currentDate = subMonths(currentDate, 1);
|
||||
break;
|
||||
|
|
@ -181,6 +246,9 @@ export const viewStore = {
|
|||
case 'day':
|
||||
currentDate = addDays(currentDate, 1);
|
||||
break;
|
||||
case '3day':
|
||||
currentDate = addDays(currentDate, 3);
|
||||
break;
|
||||
case '5day':
|
||||
currentDate = addDays(currentDate, 5);
|
||||
break;
|
||||
|
|
@ -193,6 +261,21 @@ export const viewStore = {
|
|||
case '14day':
|
||||
currentDate = addDays(currentDate, 14);
|
||||
break;
|
||||
case '30day':
|
||||
currentDate = addDays(currentDate, 30);
|
||||
break;
|
||||
case '60day':
|
||||
currentDate = addDays(currentDate, 60);
|
||||
break;
|
||||
case '90day':
|
||||
currentDate = addDays(currentDate, 90);
|
||||
break;
|
||||
case '365day':
|
||||
currentDate = addDays(currentDate, 365);
|
||||
break;
|
||||
case 'custom':
|
||||
currentDate = addDays(currentDate, settingsStore.customDayCount);
|
||||
break;
|
||||
case 'month':
|
||||
currentDate = addMonths(currentDate, 1);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
subMonths,
|
||||
subYears,
|
||||
} from 'date-fns';
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
|
||||
/**
|
||||
* Calculate a date offset based on the current view type
|
||||
|
|
@ -35,6 +36,9 @@ export function getOffsetDate(date: Date, viewType: CalendarViewType, offset: nu
|
|||
case 'day':
|
||||
return offset > 0 ? addDays(date, offset) : subDays(date, Math.abs(offset));
|
||||
|
||||
case '3day':
|
||||
return offset > 0 ? addDays(date, offset * 3) : subDays(date, Math.abs(offset) * 3);
|
||||
|
||||
case '5day':
|
||||
return offset > 0 ? addDays(date, offset * 5) : subDays(date, Math.abs(offset) * 5);
|
||||
|
||||
|
|
@ -47,6 +51,23 @@ export function getOffsetDate(date: Date, viewType: CalendarViewType, offset: nu
|
|||
case '14day':
|
||||
return offset > 0 ? addDays(date, offset * 14) : subDays(date, Math.abs(offset) * 14);
|
||||
|
||||
case '30day':
|
||||
return offset > 0 ? addDays(date, offset * 30) : subDays(date, Math.abs(offset) * 30);
|
||||
|
||||
case '60day':
|
||||
return offset > 0 ? addDays(date, offset * 60) : subDays(date, Math.abs(offset) * 60);
|
||||
|
||||
case '90day':
|
||||
return offset > 0 ? addDays(date, offset * 90) : subDays(date, Math.abs(offset) * 90);
|
||||
|
||||
case '365day':
|
||||
return offset > 0 ? addDays(date, offset * 365) : subDays(date, Math.abs(offset) * 365);
|
||||
|
||||
case 'custom': {
|
||||
const days = settingsStore.customDayCount;
|
||||
return offset > 0 ? addDays(date, offset * days) : subDays(date, Math.abs(offset) * days);
|
||||
}
|
||||
|
||||
case 'month':
|
||||
return offset > 0 ? addMonths(date, offset) : subMonths(date, Math.abs(offset));
|
||||
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@
|
|||
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 TagStrip from '$lib/components/calendar/TagStrip.svelte';
|
||||
import EventContextMenu from '$lib/components/event/EventContextMenu.svelte';
|
||||
import ViewModePillContextMenu from '$lib/components/calendar/ViewModePillContextMenu.svelte';
|
||||
import { eventContextMenuStore } from '$lib/stores/eventContextMenu.svelte';
|
||||
|
|
@ -249,14 +250,32 @@
|
|||
// User email for user dropdown
|
||||
let userEmail = $derived(authStore.user?.email || 'Menü');
|
||||
|
||||
// Toggle TagStrip visibility
|
||||
function handleTagsToggle() {
|
||||
settingsStore.toggleTagStrip();
|
||||
}
|
||||
|
||||
// Tags button active state (show as active when TagStrip is visible)
|
||||
let isTagStripVisible = $derived(!settingsStore.tagStripCollapsed);
|
||||
|
||||
// Offset for elements above TagStrip (70px when visible)
|
||||
let tagStripOffset = $derived(showCalendarToolbar && !settingsStore.tagStripCollapsed ? 70 : 0);
|
||||
|
||||
// Base navigation items for Calendar (without Kalender/Aufgaben - handled by tab group)
|
||||
const baseNavItems: PillNavItem[] = [
|
||||
{ href: '/tags', label: 'Tags', icon: 'tag' },
|
||||
// Note: Tags uses onClick to toggle TagStrip visibility instead of navigating
|
||||
let baseNavItems = $derived<PillNavItem[]>([
|
||||
{
|
||||
href: '/tags',
|
||||
label: 'Tags',
|
||||
icon: 'tag',
|
||||
onClick: handleTagsToggle,
|
||||
active: isTagStripVisible,
|
||||
},
|
||||
{ href: '/statistics', label: 'Statistiken', icon: 'bar-chart-3' },
|
||||
{ href: '/network', label: 'Netzwerk', icon: 'share-2' },
|
||||
{ href: '/settings', label: 'Einstellungen', icon: 'settings' },
|
||||
{ href: '/feedback', label: 'Feedback', icon: 'chat' },
|
||||
];
|
||||
]);
|
||||
|
||||
// Navigation items filtered by visibility settings
|
||||
const navItems = $derived(
|
||||
|
|
@ -292,6 +311,10 @@
|
|||
week: '7',
|
||||
'10day': '10',
|
||||
'14day': '14',
|
||||
'30day': '30',
|
||||
'60day': '60',
|
||||
'90day': '90',
|
||||
'365day': '365',
|
||||
month: 'M',
|
||||
year: 'Y',
|
||||
agenda: 'L',
|
||||
|
|
@ -306,6 +329,10 @@
|
|||
week: 'Wochenansicht',
|
||||
'10day': '10-Tage-Ansicht',
|
||||
'14day': '14-Tage-Ansicht',
|
||||
'30day': '30-Tage-Ansicht',
|
||||
'60day': '60-Tage-Ansicht',
|
||||
'90day': '90-Tage-Ansicht',
|
||||
'365day': '365-Tage-Ansicht',
|
||||
month: 'Monatsansicht',
|
||||
year: 'Jahresansicht',
|
||||
agenda: 'Agenda',
|
||||
|
|
@ -543,18 +570,33 @@
|
|||
<!-- Date strip (only on main calendar page) -->
|
||||
{#if showCalendarToolbar}
|
||||
{#if settingsStore.dateStripCollapsed}
|
||||
<DateStripFab {isSidebarMode} isToolbarExpanded={!isToolbarCollapsed} {isMobile} />
|
||||
<DateStripFab
|
||||
{isSidebarMode}
|
||||
isToolbarExpanded={!isToolbarCollapsed}
|
||||
{isMobile}
|
||||
hasTagStrip={!settingsStore.tagStripCollapsed}
|
||||
/>
|
||||
{:else}
|
||||
<DateStrip {isSidebarMode} isToolbarExpanded={!isToolbarCollapsed} />
|
||||
<DateStrip
|
||||
{isSidebarMode}
|
||||
isToolbarExpanded={!isToolbarCollapsed}
|
||||
hasTagStrip={!settingsStore.tagStripCollapsed}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Tag strip (only on main calendar page, when not collapsed) - directly above PillNav -->
|
||||
{#if showCalendarToolbar && !settingsStore.tagStripCollapsed}
|
||||
<TagStrip {isSidebarMode} />
|
||||
{/if}
|
||||
|
||||
<!-- Calendar toolbar (only on main calendar page, not in sidebar mode) -->
|
||||
{#if showCalendarToolbar && !isSidebarMode}
|
||||
<CalendarToolbar
|
||||
{isSidebarMode}
|
||||
isCollapsed={isToolbarCollapsed}
|
||||
{isMobile}
|
||||
bottomOffset={settingsStore.tagStripCollapsed ? '70px' : '140px'}
|
||||
onModeChange={handleToolbarModeChange}
|
||||
onCollapsedChange={handleToolbarCollapsedChange}
|
||||
/>
|
||||
|
|
@ -587,12 +629,12 @@
|
|||
createText="Erstellen"
|
||||
appIcon="calendar"
|
||||
bottomOffset={isMobile
|
||||
? '70px'
|
||||
? `${70 + tagStripOffset}px`
|
||||
: isSidebarMode
|
||||
? '0px'
|
||||
? `${tagStripOffset}px`
|
||||
: showCalendarToolbar && !isToolbarCollapsed
|
||||
? '140px'
|
||||
: '70px'}
|
||||
? `${140 + tagStripOffset}px`
|
||||
: `${70 + tagStripOffset}px`}
|
||||
hasFabRight={showCalendarToolbar && !isSidebarMode}
|
||||
hasFabLeft={!isMobile &&
|
||||
showCalendarToolbar &&
|
||||
|
|
|
|||
|
|
@ -1,9 +1,28 @@
|
|||
/**
|
||||
* Calendar view types
|
||||
*/
|
||||
export type CalendarViewType =
|
||||
| 'day'
|
||||
| '3day'
|
||||
| '5day'
|
||||
| 'week'
|
||||
| '10day'
|
||||
| '14day'
|
||||
| '30day'
|
||||
| '60day'
|
||||
| '90day'
|
||||
| '365day'
|
||||
| 'month'
|
||||
| 'year'
|
||||
| 'agenda'
|
||||
| 'custom';
|
||||
|
||||
/**
|
||||
* Calendar settings stored in JSONB
|
||||
*/
|
||||
export interface CalendarSettings {
|
||||
/** Default view when opening the calendar */
|
||||
defaultView?: 'day' | '5day' | 'week' | '10day' | '14day' | 'month' | 'year' | 'agenda';
|
||||
defaultView?: CalendarViewType;
|
||||
/** 0 = Sunday, 1 = Monday */
|
||||
weekStartsOn?: 0 | 1;
|
||||
/** Show week numbers in calendar views */
|
||||
|
|
@ -57,19 +76,6 @@ export interface UpdateCalendarInput {
|
|||
settings?: CalendarSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calendar view types
|
||||
*/
|
||||
export type CalendarViewType =
|
||||
| 'day'
|
||||
| '5day'
|
||||
| 'week'
|
||||
| '10day'
|
||||
| '14day'
|
||||
| 'month'
|
||||
| 'year'
|
||||
| 'agenda';
|
||||
|
||||
/**
|
||||
* Default calendar colors
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@
|
|||
</svg>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if option.label && isSidebarMode}
|
||||
{#if option.label}
|
||||
<span class="tab-label">{option.label}</span>
|
||||
{/if}
|
||||
</button>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue