-
-
+
+
-
-
-
-
@@ -529,11 +719,6 @@ header h1 {
grid-template-columns: 1fr var(--month-w);
}
-.main-calendar-area {
- position: relative;
- overflow: hidden;
-}
-
.calendar-content {
position: relative;
width: 100%;
@@ -552,7 +737,7 @@ header h1 {
.month-label {
position: absolute;
- left: 0;
+ inset-inline-start: 0;
width: 100%;
background-image: linear-gradient(to bottom, rgba(186, 186, 186, 0.3), rgba(186, 186, 186, 0.2));
font-size: 2em;
@@ -587,4 +772,93 @@ header h1 {
height: var(--row-h);
pointer-events: none;
}
+
+/* Search overlay */
+.event-search {
+ position: fixed;
+ top: 0.75rem;
+ inset-inline-end: 0.75rem;
+ z-index: 1200;
+ background: color-mix(in srgb, var(--panel) 90%, transparent);
+ backdrop-filter: blur(0.75em);
+ -webkit-backdrop-filter: blur(0.75em);
+ color: var(--ink);
+ padding: 0.75rem 0.75rem 0.6rem 0.75rem;
+ border-radius: 0.6rem;
+ width: min(28rem, 80vw);
+ box-shadow: 0 0.5em 1.25em rgba(0, 0, 0, 0.35);
+ border: 1px solid color-mix(in srgb, var(--muted) 40%, transparent);
+ font-size: 0.9rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.4rem;
+}
+.event-search .search-row {
+ display: flex;
+ gap: 0.4rem;
+}
+.event-search input[type='text'] {
+ flex: 1;
+ padding: 0.45rem 0.6rem;
+ border-radius: 0.4rem;
+ border: 1px solid color-mix(in srgb, var(--muted) 40%, transparent);
+ background: color-mix(in srgb, var(--panel) 85%, transparent);
+ color: inherit;
+}
+.event-search button {
+ background: color-mix(in srgb, var(--accent, #4b7) 70%, transparent);
+ color: var(--ink, #111);
+ border: 0;
+ border-radius: 0.4rem;
+ padding: 0.45rem 0.6rem;
+ cursor: pointer;
+}
+.event-search button:disabled {
+ opacity: 0.4;
+ cursor: default;
+}
+.event-search .results {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ max-height: 14rem;
+ overflow: auto;
+ border: 1px solid color-mix(in srgb, var(--muted) 30%, transparent);
+ border-radius: 0.4rem;
+}
+.event-search .results li {
+ display: flex;
+ justify-content: space-between;
+ gap: 0.75rem;
+ padding: 0.4rem 0.55rem;
+ cursor: pointer;
+ font-size: 0.85rem;
+ line-height: 1.2;
+}
+.event-search .results li.active {
+ background: color-mix(in srgb, var(--accent, #4b7) 45%, transparent);
+ color: var(--ink, #111);
+ font-weight: 600;
+}
+.event-search .results li:hover:not(.active) {
+ background: color-mix(in srgb, var(--panel) 70%, transparent);
+}
+.event-search .results .title {
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.event-search .results .date {
+ opacity: 0.6;
+ font-family: monospace;
+}
+.event-search .no-results {
+ padding: 0.25rem 0.1rem;
+ opacity: 0.7;
+}
+.event-search .hint {
+ opacity: 0.55;
+ font-size: 0.7rem;
+}
diff --git a/src/components/EventDialog.vue b/src/components/EventDialog.vue
index 1f2e569..2d04cce 100644
--- a/src/components/EventDialog.vue
+++ b/src/components/EventDialog.vue
@@ -12,6 +12,7 @@ import {
formatDateLong,
DEFAULT_TZ,
} from '@/utils/date'
+import { getDate as getOccurrenceDate } from '@/utils/events'
import { addDays, addMonths } from 'date-fns'
const props = defineProps({
@@ -301,48 +302,18 @@ function openEditDialog(payload) {
if (!payload) return
const baseId = payload.id
- let occurrenceIndex = payload.occurrenceIndex || 0
+ let n = payload.n || 0
let weekday = null
let occurrenceDate = null
const event = calendarStore.getEventById(baseId)
if (!event) return
- if (event.recur) {
- if (event.recur.freq === 'weeks' && occurrenceIndex >= 0) {
- const pattern = event.recur.weekdays || []
- const baseStart = fromLocalString(event.startDate, DEFAULT_TZ)
- const baseEnd = new Date(fromLocalString(event.startDate, DEFAULT_TZ))
- baseEnd.setDate(baseEnd.getDate() + (event.days || 1) - 1)
- if (occurrenceIndex === 0) {
- occurrenceDate = baseStart
- weekday = baseStart.getDay()
- } else {
- const interval = event.recur.interval || 1
- const WEEK_MS = 7 * 86400000
- const baseBlockStart = getMondayOfISOWeek(baseStart)
- function isAligned(d) {
- const blk = getMondayOfISOWeek(d)
- const diff = Math.floor((blk - baseBlockStart) / WEEK_MS)
- return diff % interval === 0
- }
- let cur = addDays(baseEnd, 1)
- let found = 0
- let safety = 0
- while (found < occurrenceIndex && safety < 20000) {
- if (pattern[cur.getDay()] && isAligned(cur)) {
- found++
- if (found === occurrenceIndex) break
- }
- cur = addDays(cur, 1)
- safety++
- }
- occurrenceDate = cur
- weekday = cur.getDay()
- }
- } else if (event.recur.freq === 'months' && occurrenceIndex >= 0) {
- const baseDate = fromLocalString(event.startDate, DEFAULT_TZ)
- occurrenceDate = addMonths(baseDate, occurrenceIndex)
+ if (event.recur && n >= 0) {
+ const occStr = getOccurrenceDate(event, n, DEFAULT_TZ)
+ if (occStr) {
+ occurrenceDate = fromLocalString(occStr, DEFAULT_TZ)
+ weekday = occurrenceDate.getDay()
}
}
dialogMode.value = 'edit'
@@ -372,10 +343,10 @@ function openEditDialog(payload) {
eventSaved.value = false
if (event.recur) {
- if (event.recur.freq === 'weeks' && occurrenceIndex >= 0) {
- occurrenceContext.value = { baseId, occurrenceIndex, weekday, occurrenceDate }
- } else if (event.recur.freq === 'months' && occurrenceIndex > 0) {
- occurrenceContext.value = { baseId, occurrenceIndex, weekday: null, occurrenceDate }
+ if (event.recur.freq === 'weeks' && n >= 0) {
+ occurrenceContext.value = { baseId, occurrenceIndex: n, weekday, occurrenceDate }
+ } else if (event.recur.freq === 'months' && n > 0) {
+ occurrenceContext.value = { baseId, occurrenceIndex: n, weekday: null, occurrenceDate }
}
}
// anchor to base event start date
@@ -594,8 +565,10 @@ const recurrenceSummary = computed(() => {
+
+
+
+
+ {
+
+
+
+
+ -
+
- + {{ r.title }} + {{ r.startDate }} + +
{{ searchQuery ? 'No matches' : 'Type to search' }}
+ Enter to go, Esc to close, ↑/↓ to browse
+
+ {{ dialogMode === 'create' ? 'Create Event' : 'Edit Event' }}
+ · {{ headerDateShort }}
+
{{ recurrenceSummary }}
-
- until {{ formattedFinalOccurrence }}
+ until {{ formattedFinalOccurrence }}
Does not recur