Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e210babe29 | ||
|
|
31c5551535 | ||
|
|
9b2354fd91 | ||
|
|
43aa8db650 | ||
|
|
debeececaf |
@@ -37,8 +37,6 @@ onMounted(() => {
|
||||
document.addEventListener('keydown', handleGlobalKey, { passive: false })
|
||||
// Set document language via shared util
|
||||
if (lang) document.documentElement.setAttribute('lang', lang)
|
||||
// Initialize title
|
||||
document.title = formatTodayString(new Date(calendarStore.now))
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@@ -49,7 +47,7 @@ onBeforeUnmount(() => {
|
||||
watch(
|
||||
() => calendarStore.now,
|
||||
(val) => {
|
||||
document.title = formatTodayString(new Date(val))
|
||||
document.title = formatTodayString(new Date(val), "short", "short")
|
||||
},
|
||||
{ immediate: false },
|
||||
)
|
||||
|
||||
@@ -17,16 +17,7 @@ const formattedDate = computed(() => {
|
||||
<div
|
||||
class="cell"
|
||||
:style="props.dragging ? 'touch-action:none;' : 'touch-action:pan-y;'"
|
||||
:class="[
|
||||
props.day.monthClass,
|
||||
{
|
||||
today: props.day.isToday,
|
||||
weekend: props.day.isWeekend,
|
||||
firstday: props.day.isFirstDay,
|
||||
selected: props.day.isSelected,
|
||||
holiday: props.day.isHoliday,
|
||||
},
|
||||
]"
|
||||
:class="[props.day.monthClass, { today: props.day.isToday, weekend: props.day.isWeekend, firstday: props.day.isFirstDay, selected: props.day.isSelected, holiday: props.day.isHoliday }]"
|
||||
:data-date="props.day.date"
|
||||
>
|
||||
<span class="compact-date">{{ formattedDate }}</span>
|
||||
@@ -43,10 +34,8 @@ const formattedDate = computed(() => {
|
||||
position: relative;
|
||||
user-select: none;
|
||||
display: grid;
|
||||
/* Updated grid for centered day number */
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr auto;
|
||||
/* Named grid areas */
|
||||
grid-template-areas:
|
||||
'day-number'
|
||||
'holiday-info';
|
||||
@@ -89,6 +78,26 @@ const formattedDate = computed(() => {
|
||||
z-index: 15;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Search highlight animation */
|
||||
.cell.search-highlight-flash::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: calc(100% + .2rem);
|
||||
height: calc(100% + .2rem);
|
||||
border-radius: 1rem;
|
||||
background: transparent;
|
||||
border: 0.3em solid var(--strong);
|
||||
z-index: 16;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cell.search-highlight-flash::after { animation: search-highlight-flash 1.5s ease-out forwards; }
|
||||
|
||||
@keyframes search-highlight-flash {0%{opacity:0;transform:translate(-50%,-50%) scale(.8);border-width:.1em}15%{opacity:1;transform:translate(-50%,-50%) scale(1.05);border-width:.4em}30%{opacity:1;transform:translate(-50%,-50%) scale(1);border-width:.3em}100%{opacity:0;transform:translate(-50%,-50%) scale(1);border-width:.3em}}
|
||||
.cell.selected h1.day-number {
|
||||
opacity: 0.3;
|
||||
filter: brightness(1.2);
|
||||
|
||||
@@ -197,11 +197,24 @@ function measureFromProbe() {
|
||||
const {
|
||||
getWeekIndex,
|
||||
getFirstDayForVirtualWeek,
|
||||
goToToday,
|
||||
handleHeaderYearChange,
|
||||
scrollToWeekCentered,
|
||||
} = vwm
|
||||
|
||||
function showDay(input) {
|
||||
const dateStr = input instanceof Date ? toLocalString(input, DEFAULT_TZ) : String(input)
|
||||
const weekIndex = getWeekIndex(fromLocalString(dateStr, DEFAULT_TZ))
|
||||
scrollToWeekCentered(weekIndex, 'nav', true)
|
||||
const diff = Math.abs(weekIndex - centerVisibleWeek.value)
|
||||
const delay = Math.min(800, diff * 40)
|
||||
setTimeout(() => {
|
||||
const el = document.querySelector(`[data-date="${dateStr}"]`)
|
||||
if (!el) return
|
||||
el.classList.add('search-highlight-flash')
|
||||
setTimeout(() => el.classList.remove('search-highlight-flash'), 1500)
|
||||
}, delay)
|
||||
}
|
||||
|
||||
// Reference date for search: center of the current viewport (virtual week at vertical midpoint)
|
||||
const centerVisibleWeek = computed(() => {
|
||||
const midRow = (scrollTop.value + viewportHeight.value / 2) / rowHeight.value
|
||||
@@ -245,7 +258,6 @@ watch(
|
||||
|
||||
function startDrag(dateStr) {
|
||||
dateStr = normalizeDate(dateStr)
|
||||
if (calendarStore.config.select_days === 0) return
|
||||
isDragging.value = true
|
||||
dragAnchor.value = dateStr
|
||||
selection.value = { startDate: dateStr, dayCount: 1 }
|
||||
@@ -358,23 +370,13 @@ function getDateFromCoordinates(clientX, clientY) {
|
||||
}
|
||||
|
||||
function calculateSelection(anchorStr, otherStr) {
|
||||
const limit = calendarStore.config.select_days
|
||||
const anchorDate = fromLocalString(anchorStr, DEFAULT_TZ)
|
||||
const otherDate = fromLocalString(otherStr, DEFAULT_TZ)
|
||||
const forward = otherDate >= anchorDate
|
||||
const span = daysInclusive(anchorStr, otherStr)
|
||||
|
||||
if (span <= limit) {
|
||||
const startDate = forward ? anchorStr : otherStr
|
||||
return { startDate, dayCount: span }
|
||||
}
|
||||
|
||||
if (forward) {
|
||||
return { startDate: anchorStr, dayCount: limit }
|
||||
} else {
|
||||
const startDate = addDaysStr(anchorStr, -(limit - 1), DEFAULT_TZ)
|
||||
return { startDate, dayCount: limit }
|
||||
}
|
||||
const startDate = forward ? anchorStr : otherStr
|
||||
return { startDate, dayCount: span }
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -457,26 +459,15 @@ const handleEventClick = (payload) => {
|
||||
openEditEventDialog(payload)
|
||||
}
|
||||
|
||||
function scrollToEventStart(startDate, smooth = true) {
|
||||
try {
|
||||
const dateObj = fromLocalString(startDate, DEFAULT_TZ)
|
||||
const weekIndex = getWeekIndex(dateObj)
|
||||
scrollToWeekCentered(weekIndex, 'search-jump', smooth)
|
||||
} catch {
|
||||
/* noop */
|
||||
function handleHeaderSearchPreview(r) { if (r) showDay(r.startDate) }
|
||||
function handleHeaderSearchActivate(r) {
|
||||
if (!r) return
|
||||
showDay(r.startDate)
|
||||
if (!r._goto && !r._holiday) {
|
||||
const ev = calendarStore.getEventById(r.id)
|
||||
if (ev) openEditEventDialog({ id: ev.id, event: ev })
|
||||
}
|
||||
}
|
||||
function handleHeaderSearchPreview(result) {
|
||||
if (!result) return
|
||||
scrollToEventStart(result.startDate, true)
|
||||
}
|
||||
function handleHeaderSearchActivate(result) {
|
||||
if (!result) return
|
||||
scrollToEventStart(result.startDate, true)
|
||||
// Open edit dialog for the event
|
||||
const ev = calendarStore.getEventById(result.id)
|
||||
if (ev) openEditEventDialog({ id: ev.id, event: ev })
|
||||
}
|
||||
|
||||
// Heuristic: rotate month label (180deg) only for predominantly Latin text.
|
||||
// We explicitly avoid locale detection; rely solely on characters present.
|
||||
@@ -543,7 +534,7 @@ window.addEventListener('resize', () => {
|
||||
<div class="wrap">
|
||||
<HeaderControls
|
||||
:reference-date="centerVisibleDateStr"
|
||||
@go-to-today="goToToday"
|
||||
@go-to-today="() => showDay(calendarStore.today)"
|
||||
@search-preview="handleHeaderSearchPreview"
|
||||
@search-activate="handleHeaderSearchActivate"
|
||||
/>
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
<div class="header-controls-wrapper">
|
||||
<Transition name="header-controls" appear>
|
||||
<div v-if="isVisible" class="header-controls">
|
||||
<EventSearch
|
||||
ref="eventSearchRef"
|
||||
:reference-date="referenceDate"
|
||||
@activate="handleSearchActivate"
|
||||
@preview="(r) => emit('search-preview', r)"
|
||||
/>
|
||||
<div class="search-with-spacer">
|
||||
<!-- Shrinkable spacer to align search with week label column; smoothly shrinks as needed -->
|
||||
<div class="pre-search-spacer" aria-hidden="true"></div>
|
||||
<EventSearch
|
||||
ref="eventSearchRef"
|
||||
:reference-date="referenceDate"
|
||||
@activate="handleSearchActivate"
|
||||
@preview="(r) => emit('search-preview', r)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="current-time"
|
||||
aria-label="Current time (click to go to today)"
|
||||
@@ -204,7 +208,29 @@ onBeforeUnmount(() => {
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding-inline-end: 2rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* Group search + spacer so outer gap doesn't create unwanted space */
|
||||
.search-with-spacer {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
align-items: stretch;
|
||||
}
|
||||
.search-with-spacer > .search-bar {
|
||||
flex: 1 1 auto;
|
||||
min-width: 6rem; /* allow spacer to give up space first */
|
||||
}
|
||||
|
||||
.pre-search-spacer {
|
||||
flex: 0 1000 var(--week-w);
|
||||
width: var(--week-w);
|
||||
min-width: .5rem;
|
||||
pointer-events: none;
|
||||
transition: flex-basis 0.35s ease, width 0.35s ease;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -307,7 +333,6 @@ onBeforeUnmount(() => {
|
||||
font-size: 1.5em;
|
||||
white-space: pre-line;
|
||||
text-align: center;
|
||||
margin-inline-end: 2rem;
|
||||
}
|
||||
|
||||
.current-time {
|
||||
@@ -323,7 +348,7 @@ onBeforeUnmount(() => {
|
||||
color: var(--strong);
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
@media (max-width: 770px) {
|
||||
.current-time {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
aria-label="Search dates, holidays and events"
|
||||
@keydown="handleSearchKeydown"
|
||||
/>
|
||||
<div v-if="shortcut" class="shortcut-hint">
|
||||
{{ shortcut }}
|
||||
</div>
|
||||
<ul
|
||||
v-if="searchQuery.trim() && searchResults.length"
|
||||
class="search-dropdown"
|
||||
@@ -60,6 +63,10 @@ const searchIndex = ref(0)
|
||||
const searchInputRef = ref(null)
|
||||
let previewTimer = null
|
||||
|
||||
const shortcut = /Mac/.test(navigator.userAgent) ? '⌘F'
|
||||
: /Windows|Linux/.test(navigator.userAgent) ? 'Ctrl+F'
|
||||
: ''
|
||||
|
||||
// Accent-insensitive lowercasing
|
||||
const norm = (s) =>
|
||||
s
|
||||
@@ -469,8 +476,8 @@ function parseGoToDateCandidate(input, refStr) {
|
||||
|
||||
<style scoped>
|
||||
.search-bar {
|
||||
flex: 0 1 20rem;
|
||||
margin-inline: auto; /* center with equal free-space on both sides */
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
}
|
||||
.search-bar input {
|
||||
@@ -509,6 +516,27 @@ function parseGoToDateCandidate(input, refStr) {
|
||||
.search-bar input::-webkit-search-cancel-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.shortcut-hint {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
inset-inline-end: 0.5rem;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.6;
|
||||
color: var(--muted);
|
||||
font-family: ui-monospace, SF Mono, Consolas, Monaco, "Cascadia Mono", "Segoe UI Mono", "Roboto Mono", "Ubuntu Mono", "Source Code Pro", "Fira Mono", "Droid Sans Mono", "Courier New", monospace;
|
||||
background: color-mix(in srgb, var(--panel) 85%, transparent);
|
||||
padding: 0.15rem 0.3rem;
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid color-mix(in srgb, var(--muted) 25%, transparent);
|
||||
}
|
||||
|
||||
.search-bar input:focus + .shortcut-hint,
|
||||
.search-bar input:not(:placeholder-shown) + .shortcut-hint {
|
||||
display: none;
|
||||
}
|
||||
.search-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.25rem);
|
||||
|
||||
@@ -23,7 +23,6 @@ export const useCalendarStore = defineStore('calendar', {
|
||||
_holidayConfigSignature: null,
|
||||
_holidaysInitialized: false,
|
||||
config: {
|
||||
select_days: 14,
|
||||
first_day: 1,
|
||||
holidays: {
|
||||
enabled: true,
|
||||
|
||||
@@ -185,9 +185,9 @@ function formatDateLong(date, includeYear = false) {
|
||||
/**
|
||||
* Format date as today string (e.g., "Monday\nJanuary 15")
|
||||
*/
|
||||
function formatTodayString(date) {
|
||||
function formatTodayString(date, weekday = "long", month = "long") {
|
||||
const formatted = date
|
||||
.toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' })
|
||||
.toLocaleDateString(undefined, { weekday, month, day: 'numeric' })
|
||||
.replace(/,? /, '\n')
|
||||
return formatted.charAt(0).toUpperCase() + formatted.slice(1)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user