mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:41:09 +02:00
✨ feat(calendar): improve header UI and add quick event overlay
- Compact header styling with smaller buttons and reduced padding - Add configurable hour filter display in header - Add QuickEventOverlay component for fast event creation - Add allDayDisplayMode to event metadata types - Extend settings store with filter hours configuration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
482509a574
commit
e56485f21e
5 changed files with 606 additions and 31 deletions
|
|
@ -60,18 +60,18 @@
|
|||
|
||||
<header class="calendar-header">
|
||||
<div class="header-left">
|
||||
<button class="btn btn-ghost" onclick={() => viewStore.goToToday()}>
|
||||
<button class="today-btn" onclick={() => viewStore.goToToday()}>
|
||||
Heute
|
||||
</button>
|
||||
|
||||
<div class="nav-buttons">
|
||||
<button class="btn btn-ghost btn-icon" onclick={() => viewStore.goToPrevious()} aria-label="Zurück">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<button class="nav-btn" onclick={() => viewStore.goToPrevious()} aria-label="Zurück">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-icon" onclick={() => viewStore.goToNext()} aria-label="Weiter">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<button class="nav-btn" onclick={() => viewStore.goToNext()} aria-label="Weiter">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
|
@ -93,14 +93,14 @@
|
|||
Mo-Fr
|
||||
</button>
|
||||
|
||||
<!-- Hide early hours toggle -->
|
||||
<!-- Filter hours toggle -->
|
||||
<button
|
||||
class="filter-toggle"
|
||||
class:active={settingsStore.hideEarlyHours}
|
||||
onclick={() => settingsStore.set('hideEarlyHours', !settingsStore.hideEarlyHours)}
|
||||
title="Frühe Stunden ausblenden (0-6 Uhr)"
|
||||
class:active={settingsStore.filterHoursEnabled}
|
||||
onclick={() => settingsStore.set('filterHoursEnabled', !settingsStore.filterHoursEnabled)}
|
||||
title="Stunden filtern ({settingsStore.dayStartHour}-{settingsStore.dayEndHour} Uhr)"
|
||||
>
|
||||
7-24
|
||||
{settingsStore.dayStartHour}-{settingsStore.dayEndHour}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -123,7 +123,7 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 1.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-bottom: 1px solid hsl(var(--color-border));
|
||||
background: hsl(var(--color-surface));
|
||||
}
|
||||
|
|
@ -131,16 +131,50 @@
|
|||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.today-btn {
|
||||
padding: 0.25rem 0.625rem;
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
background: transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--color-foreground));
|
||||
cursor: pointer;
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
|
||||
.today-btn:hover {
|
||||
background: hsl(var(--color-muted));
|
||||
}
|
||||
|
||||
.nav-buttons {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
padding: 0.25rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
cursor: pointer;
|
||||
transition: all 150ms ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-btn:hover {
|
||||
background: hsl(var(--color-muted));
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 1.25rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--color-foreground));
|
||||
margin: 0;
|
||||
|
|
@ -149,22 +183,22 @@
|
|||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.view-selector {
|
||||
display: flex;
|
||||
background: hsl(var(--color-muted));
|
||||
border-radius: var(--radius-md);
|
||||
padding: 0.25rem;
|
||||
padding: 0.125rem;
|
||||
}
|
||||
|
||||
.view-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
padding: 0.25rem 0.625rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.875rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
cursor: pointer;
|
||||
|
|
@ -183,15 +217,15 @@
|
|||
|
||||
.filter-toggles {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.filter-toggle {
|
||||
padding: 0.5rem 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
background: transparent;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.75rem;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
cursor: pointer;
|
||||
|
|
@ -209,14 +243,11 @@
|
|||
border-color: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.calendar-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
|
|
@ -225,7 +256,7 @@
|
|||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@
|
|||
let location = $state(event?.location || '');
|
||||
let isAllDay = $state(event?.isAllDay || false);
|
||||
let calendarId = $state(event?.calendarId || '');
|
||||
let allDayDisplayMode = $state<'default' | 'header' | 'block'>(
|
||||
event?.metadata?.allDayDisplayMode || 'default'
|
||||
);
|
||||
|
||||
// Set default calendar when calendars are loaded
|
||||
$effect(() => {
|
||||
|
|
@ -73,6 +76,11 @@
|
|||
const startDateTime = new Date(`${startDate}T${isAllDay ? '00:00' : startTime}`);
|
||||
const endDateTime = new Date(`${endDate}T${isAllDay ? '23:59' : endTime}`);
|
||||
|
||||
// Build metadata with display mode if not default
|
||||
const metadata = isAllDay && allDayDisplayMode !== 'default'
|
||||
? { ...(event?.metadata || {}), allDayDisplayMode: allDayDisplayMode as 'header' | 'block' }
|
||||
: event?.metadata;
|
||||
|
||||
const data: CreateEventInput | UpdateEventInput = {
|
||||
title: title.trim(),
|
||||
description: description.trim() || undefined,
|
||||
|
|
@ -81,6 +89,7 @@
|
|||
startTime: startDateTime.toISOString(),
|
||||
endTime: endDateTime.toISOString(),
|
||||
calendarId,
|
||||
metadata,
|
||||
};
|
||||
|
||||
submitting = true;
|
||||
|
|
@ -117,6 +126,21 @@
|
|||
</label>
|
||||
</div>
|
||||
|
||||
{#if isAllDay}
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="displayMode" class="text-sm font-medium text-foreground">Anzeigeart</label>
|
||||
<select
|
||||
id="displayMode"
|
||||
class="w-full px-3 py-2 border-2 border-border rounded-lg bg-background text-foreground focus:outline-none focus:border-primary transition-colors"
|
||||
bind:value={allDayDisplayMode}
|
||||
>
|
||||
<option value="default">Standard (aus Einstellungen)</option>
|
||||
<option value="header">In Kopfzeile</option>
|
||||
<option value="block">Als Tagesblock</option>
|
||||
</select>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-1 flex flex-col gap-2">
|
||||
<label for="startDate" class="text-sm font-medium text-foreground">Beginn</label>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,499 @@
|
|||
<script lang="ts">
|
||||
import { calendarsStore } from '$lib/stores/calendars.svelte';
|
||||
import { eventsStore } from '$lib/stores/events.svelte';
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { format, addMinutes } from 'date-fns';
|
||||
|
||||
interface Props {
|
||||
startTime: Date;
|
||||
position: { x: number; y: number };
|
||||
onClose: () => void;
|
||||
onCreated?: () => void;
|
||||
}
|
||||
|
||||
let { startTime, position, onClose, onCreated }: Props = $props();
|
||||
|
||||
// Form state
|
||||
let title = $state('');
|
||||
let calendarId = $state('');
|
||||
let isExpanded = $state(false);
|
||||
let description = $state('');
|
||||
let location = $state('');
|
||||
let isAllDay = $state(false);
|
||||
let submitting = $state(false);
|
||||
|
||||
// Time fields
|
||||
let endTime = $derived(addMinutes(startTime, settingsStore.defaultEventDuration));
|
||||
let startTimeStr = $derived(format(startTime, 'HH:mm'));
|
||||
let endTimeStr = $state('');
|
||||
let startDateStr = $derived(format(startTime, 'yyyy-MM-dd'));
|
||||
let endDateStr = $state('');
|
||||
|
||||
// Initialize end time string
|
||||
$effect(() => {
|
||||
endTimeStr = format(endTime, 'HH:mm');
|
||||
endDateStr = format(endTime, 'yyyy-MM-dd');
|
||||
});
|
||||
|
||||
// Set default calendar
|
||||
$effect(() => {
|
||||
if (!calendarId && calendarsStore.defaultCalendar?.id) {
|
||||
calendarId = calendarsStore.defaultCalendar.id;
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate overlay position (ensure it stays within viewport)
|
||||
let overlayStyle = $derived.by(() => {
|
||||
const overlayWidth = isExpanded ? 360 : 300;
|
||||
const overlayHeight = isExpanded ? 400 : 180;
|
||||
|
||||
let left = position.x;
|
||||
let top = position.y;
|
||||
|
||||
// Keep within viewport bounds
|
||||
if (typeof window !== 'undefined') {
|
||||
if (left + overlayWidth > window.innerWidth - 20) {
|
||||
left = window.innerWidth - overlayWidth - 20;
|
||||
}
|
||||
if (top + overlayHeight > window.innerHeight - 20) {
|
||||
top = window.innerHeight - overlayHeight - 20;
|
||||
}
|
||||
if (left < 20) left = 20;
|
||||
if (top < 20) top = 20;
|
||||
}
|
||||
|
||||
return `left: ${left}px; top: ${top}px;`;
|
||||
});
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
if (!title.trim() || !calendarId) return;
|
||||
|
||||
submitting = true;
|
||||
|
||||
try {
|
||||
const startDateTime = isAllDay
|
||||
? new Date(`${startDateStr}T00:00:00`)
|
||||
: new Date(`${startDateStr}T${startTimeStr}`);
|
||||
const endDateTime = isAllDay
|
||||
? new Date(`${endDateStr}T23:59:59`)
|
||||
: new Date(`${endDateStr}T${endTimeStr}`);
|
||||
|
||||
await eventsStore.createEvent({
|
||||
title: title.trim(),
|
||||
calendarId,
|
||||
startTime: startDateTime.toISOString(),
|
||||
endTime: endDateTime.toISOString(),
|
||||
isAllDay,
|
||||
description: description.trim() || undefined,
|
||||
location: location.trim() || undefined,
|
||||
});
|
||||
|
||||
onCreated?.();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Failed to create event:', error);
|
||||
} finally {
|
||||
submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleMoreOptions() {
|
||||
const params = new URLSearchParams({
|
||||
start: startTime.toISOString(),
|
||||
title: title.trim(),
|
||||
calendar: calendarId,
|
||||
});
|
||||
if (description) params.set('description', description);
|
||||
if (location) params.set('location', location);
|
||||
goto(`/event/new?${params.toString()}`);
|
||||
onClose();
|
||||
}
|
||||
|
||||
function handleBackdropClick(e: MouseEvent) {
|
||||
if (e.target === e.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<!-- Backdrop -->
|
||||
<div class="overlay-backdrop" onclick={handleBackdropClick} role="presentation">
|
||||
<!-- Overlay -->
|
||||
<div
|
||||
class="quick-event-overlay"
|
||||
class:expanded={isExpanded}
|
||||
style={overlayStyle}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Termin erstellen"
|
||||
>
|
||||
<form onsubmit={handleSubmit}>
|
||||
<!-- Header -->
|
||||
<div class="overlay-header">
|
||||
<span class="time-badge">
|
||||
{format(startTime, 'EEE, d. MMM')} {startTimeStr}
|
||||
</span>
|
||||
<button type="button" class="close-btn" onclick={onClose} aria-label="Schließen">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Title input -->
|
||||
<div class="form-group">
|
||||
<input
|
||||
type="text"
|
||||
class="title-input"
|
||||
bind:value={title}
|
||||
placeholder="Termin hinzufügen"
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Calendar select (compact) -->
|
||||
<div class="form-group compact-row">
|
||||
<div class="calendar-dot" style="background-color: {calendarsStore.getColor(calendarId)}"></div>
|
||||
<select class="compact-select" bind:value={calendarId}>
|
||||
{#each calendarsStore.calendars as cal}
|
||||
<option value={cal.id}>{cal.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Expanded section -->
|
||||
{#if isExpanded}
|
||||
<div class="expanded-section">
|
||||
<!-- All day toggle -->
|
||||
<label class="toggle-row">
|
||||
<input type="checkbox" bind:checked={isAllDay} />
|
||||
<span>Ganztägig</span>
|
||||
</label>
|
||||
|
||||
<!-- Time fields -->
|
||||
{#if !isAllDay}
|
||||
<div class="time-row">
|
||||
<div class="time-field">
|
||||
<label>Von</label>
|
||||
<input type="time" bind:value={startTimeStr} />
|
||||
</div>
|
||||
<span class="time-sep">–</span>
|
||||
<div class="time-field">
|
||||
<label>Bis</label>
|
||||
<input type="time" bind:value={endTimeStr} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Location -->
|
||||
<div class="form-group">
|
||||
<input
|
||||
type="text"
|
||||
class="field-input"
|
||||
bind:value={location}
|
||||
placeholder="Ort hinzufügen"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="form-group">
|
||||
<textarea
|
||||
class="field-input"
|
||||
bind:value={description}
|
||||
placeholder="Beschreibung"
|
||||
rows="2"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="overlay-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="expand-btn"
|
||||
onclick={() => isExpanded = !isExpanded}
|
||||
>
|
||||
{isExpanded ? 'Weniger' : 'Mehr Optionen'}
|
||||
<svg class="w-3 h-3" class:rotated={isExpanded} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button type="button" class="btn-ghost" onclick={handleMoreOptions}>
|
||||
Alle Optionen
|
||||
</button>
|
||||
<button type="submit" class="btn-primary" disabled={submitting || !title.trim()}>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.overlay-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.quick-event-overlay {
|
||||
position: fixed;
|
||||
width: 300px;
|
||||
background: hsl(var(--color-surface));
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15), 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1001;
|
||||
overflow: hidden;
|
||||
animation: slideIn 150ms ease-out;
|
||||
}
|
||||
|
||||
.quick-event-overlay.expanded {
|
||||
width: 360px;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.overlay-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid hsl(var(--color-border));
|
||||
background: hsl(var(--color-muted) / 0.3);
|
||||
}
|
||||
|
||||
.time-badge {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
padding: 0.25rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: all 150ms;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: hsl(var(--color-muted));
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
.form-group {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.title-input {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--color-foreground));
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.title-input::placeholder {
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.compact-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.calendar-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.compact-select {
|
||||
flex: 1;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
border-radius: var(--radius-sm);
|
||||
background: hsl(var(--color-background));
|
||||
color: hsl(var(--color-foreground));
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.expanded-section {
|
||||
border-top: 1px solid hsl(var(--color-border));
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.toggle-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.toggle-row input {
|
||||
accent-color: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
.time-row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.time-field {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.time-field label {
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.time-field input {
|
||||
padding: 0.375rem 0.5rem;
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
border-radius: var(--radius-sm);
|
||||
background: hsl(var(--color-background));
|
||||
color: hsl(var(--color-foreground));
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.time-sep {
|
||||
padding-bottom: 0.375rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.field-input {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
border-radius: var(--radius-sm);
|
||||
background: hsl(var(--color-background));
|
||||
color: hsl(var(--color-foreground));
|
||||
font-size: 0.875rem;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.field-input:focus {
|
||||
outline: none;
|
||||
border-color: hsl(var(--color-primary));
|
||||
}
|
||||
|
||||
.overlay-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
border-top: 1px solid hsl(var(--color-border));
|
||||
background: hsl(var(--color-muted) / 0.2);
|
||||
}
|
||||
|
||||
.expand-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.375rem 0.5rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: all 150ms;
|
||||
}
|
||||
|
||||
.expand-btn:hover {
|
||||
color: hsl(var(--color-foreground));
|
||||
background: hsl(var(--color-muted));
|
||||
}
|
||||
|
||||
.expand-btn svg {
|
||||
transition: transform 150ms;
|
||||
}
|
||||
|
||||
.expand-btn svg.rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
background: transparent;
|
||||
color: hsl(var(--color-foreground));
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: all 150ms;
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
background: hsl(var(--color-muted));
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: none;
|
||||
background: hsl(var(--color-primary));
|
||||
color: hsl(var(--color-primary-foreground));
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: all 150ms;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: hsl(var(--color-primary) / 0.9);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -9,6 +9,7 @@ import type { CalendarViewType } from '@calendar/shared';
|
|||
// Settings types
|
||||
export type WeekStartDay = 0 | 1; // 0 = Sunday, 1 = Monday
|
||||
export type TimeFormat = '24h' | '12h';
|
||||
export type AllDayDisplayMode = 'header' | 'block'; // header = separate row, block = full day block in grid
|
||||
|
||||
export interface CalendarAppSettings {
|
||||
// View settings
|
||||
|
|
@ -20,6 +21,7 @@ export interface CalendarAppSettings {
|
|||
filterHoursEnabled: boolean; // Filter visible hours
|
||||
dayStartHour: number; // First visible hour (0-23)
|
||||
dayEndHour: number; // Last visible hour (0-23)
|
||||
allDayDisplayMode: AllDayDisplayMode; // How to display all-day events
|
||||
|
||||
// UI settings
|
||||
sidebarCollapsed: boolean;
|
||||
|
|
@ -35,7 +37,10 @@ const DEFAULT_SETTINGS: CalendarAppSettings = {
|
|||
showOnlyWeekdays: false,
|
||||
showWeekNumbers: false,
|
||||
timeFormat: '24h',
|
||||
hideEarlyHours: false,
|
||||
filterHoursEnabled: false,
|
||||
dayStartHour: 6,
|
||||
dayEndHour: 20,
|
||||
allDayDisplayMode: 'header',
|
||||
sidebarCollapsed: false,
|
||||
defaultEventDuration: 60,
|
||||
defaultReminder: 15,
|
||||
|
|
@ -94,8 +99,17 @@ export const settingsStore = {
|
|||
get timeFormat() {
|
||||
return settings.timeFormat;
|
||||
},
|
||||
get hideEarlyHours() {
|
||||
return settings.hideEarlyHours;
|
||||
get filterHoursEnabled() {
|
||||
return settings.filterHoursEnabled;
|
||||
},
|
||||
get dayStartHour() {
|
||||
return settings.dayStartHour;
|
||||
},
|
||||
get dayEndHour() {
|
||||
return settings.dayEndHour;
|
||||
},
|
||||
get allDayDisplayMode() {
|
||||
return settings.allDayDisplayMode;
|
||||
},
|
||||
get defaultEventDuration() {
|
||||
return settings.defaultEventDuration;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,11 @@ export interface EventAttendee {
|
|||
status?: 'accepted' | 'declined' | 'tentative' | 'pending';
|
||||
}
|
||||
|
||||
/**
|
||||
* How to display all-day events
|
||||
*/
|
||||
export type AllDayDisplayMode = 'header' | 'block';
|
||||
|
||||
/**
|
||||
* Event metadata stored in JSONB
|
||||
*/
|
||||
|
|
@ -23,6 +28,8 @@ export interface EventMetadata {
|
|||
priority?: 'low' | 'normal' | 'high';
|
||||
/** Tags/labels for the event */
|
||||
tags?: string[];
|
||||
/** Override for all-day display mode (uses global setting if not set) */
|
||||
allDayDisplayMode?: AllDayDisplayMode;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue