Event search (Ctrl+F), locale/RTL handling, weekday selector workday/weekend, refactored event handling, Firefox compatibility #3

Merged
LeoVasanko merged 17 commits from vol003 into main 2025-08-27 13:41:46 +01:00
4 changed files with 88 additions and 55 deletions
Showing only changes of commit 5752855f52 - Show all commits

View File

@ -21,13 +21,10 @@ const props = defineProps({
]" ]"
:data-date="props.day.date" :data-date="props.day.date"
> >
<h1>{{ props.day.displayText }}</h1> <h1 class="day-number">{{ props.day.displayText }}</h1>
<span v-if="props.day.lunarPhase" class="lunar-phase">{{ props.day.lunarPhase }}</span> <span v-if="props.day.lunarPhase" class="lunar-phase">{{ props.day.lunarPhase }}</span>
<div v-if="props.day.holiday" class="holiday-info" dir="auto" :title="props.day.holiday.name">
<div v-if="props.day.holiday" class="holiday-info"> {{ props.day.holiday.name }}
<span class="holiday-name" :title="props.day.holiday.name">
{{ props.day.holiday.name }}
</span>
</div> </div>
</div> </div>
</template> </template>
@ -38,19 +35,27 @@ const props = defineProps({
border-right: 1px solid var(--border-color); border-right: 1px solid var(--border-color);
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
user-select: none; user-select: none;
display: flex; display: grid;
flex-direction: row; /* 3 columns: day number, flexible space, lunar phase */
align-items: flex-start; grid-template-columns: min-content 1fr min-content;
justify-content: flex-start; /* 3 rows: header, flexible filler, holiday label */
grid-template-rows: auto 1fr auto;
/* Named grid areas (only ones actually used) */
grid-template-areas:
'day-number . lunar-phase'
'day-number . lunar-phase'
'holiday-info holiday-info holiday-info';
/* Explicit areas mainly for clarity */
grid-auto-flow: row;
padding: 0.25em; padding: 0.25em;
overflow: hidden; overflow: hidden;
width: 100%; width: 100%;
height: var(--row-h); height: var(--row-h);
font-weight: 700; font-weight: 700;
transition: background-color 0.15s ease; transition: background-color 0.15s ease;
align-items: start;
} }
.cell h1.day-number {
.cell h1 {
margin: 0; margin: 0;
padding: 0; padding: 0;
min-width: 1.5em; min-width: 1.5em;
@ -58,15 +63,16 @@ const props = defineProps({
font-weight: 700; font-weight: 700;
color: var(--ink); color: var(--ink);
transition: background-color 0.15s ease; transition: background-color 0.15s ease;
grid-area: day-number;
} }
.cell.weekend h1 { .cell.weekend h1.day-number {
color: var(--weekend); color: var(--weekend);
} }
.cell.firstday h1 { .cell.firstday h1.day-number {
color: var(--firstday); color: var(--firstday);
text-shadow: 0 0 0.1em var(--strong); text-shadow: 0 0 0.1em var(--strong);
} }
.cell.today h1 { .cell.today h1.day-number {
border-radius: 2em; border-radius: 2em;
background: var(--today); background: var(--today);
border: 0.2em solid var(--today); border: 0.2em solid var(--today);
@ -77,16 +83,9 @@ const props = defineProps({
.cell.selected { .cell.selected {
filter: hue-rotate(180deg); filter: hue-rotate(180deg);
} }
.cell.selected h1 { .cell.selected h1.day-number {
color: var(--strong); color: var(--strong);
} }
.lunar-phase {
position: absolute;
top: 0.5em;
right: 0.2em;
font-size: 0.8em;
opacity: 0.7;
}
.cell.holiday { .cell.holiday {
background-image: linear-gradient( background-image: linear-gradient(
135deg, 135deg,
@ -103,27 +102,32 @@ const props = defineProps({
); );
} }
} }
.cell.holiday h1 { .cell.holiday h1.day-number {
/* Slight emphasis without forcing a specific hue */ /* Slight emphasis without forcing a specific hue */
color: var(--holiday); color: var(--holiday);
text-shadow: 0 0 0.3em rgba(255, 255, 255, 0.4); text-shadow: 0 0 0.3em rgba(255, 255, 255, 0.4);
} }
.holiday-info { .lunar-phase {
position: absolute; grid-area: lunar-phase;
bottom: 0.1em; align-self: start;
left: 0.1em; justify-self: end;
right: 0.1em; margin-top: 0.5em;
line-height: 1; margin-inline-end: 0.2em;
overflow: hidden; font-size: 0.8em;
font-size: clamp(1.2vw, 0.6em, 1em); opacity: 0.7;
} }
.holiday-name { .holiday-info {
display: block; grid-area: holiday-info;
color: var(--holiday-label); align-self: end;
padding: 0.15em 0.35em 0.15em 0.25em; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
overflow: hidden; color: var(--holiday-label);
font-size: clamp(1.2vw, 0.6em, 1em);
line-height: 1;
padding-inline: 0.15em;
padding-block-end: 0.05em;
pointer-events: auto;
} }
</style> </style>

View File

@ -87,7 +87,7 @@ const vwm = createVirtualWeekManager({
contentHeight, contentHeight,
}) })
const visibleWeeks = vwm.visibleWeeks const visibleWeeks = vwm.visibleWeeks
const { scheduleWindowUpdate, resetWeeks, refreshEvents } = vwm const { scheduleWindowUpdate, resetWeeks, refreshEvents, refreshHolidays } = vwm
// Scroll managers (after scheduleWindowUpdate available) // Scroll managers (after scheduleWindowUpdate available)
const scrollManager = createScrollManager({ viewport, scheduleRebuild: scheduleWindowUpdate }) const scrollManager = createScrollManager({ viewport, scheduleRebuild: scheduleWindowUpdate })
@ -98,8 +98,7 @@ const weekColumnScrollManager = createWeekColumnScrollManager({
contentHeight, contentHeight,
setScrollTop, setScrollTop,
}) })
const { isWeekColDragging, handleWeekColMouseDown, handlePointerLockChange } = const { handleWeekColMouseDown, handlePointerLockChange } = weekColumnScrollManager
weekColumnScrollManager
const monthScrollManager = createMonthScrollManager({ const monthScrollManager = createMonthScrollManager({
viewport, viewport,
viewportHeight, viewportHeight,
@ -160,6 +159,25 @@ function clearSelection() {
selection.value = { startDate: null, dayCount: 0 } selection.value = { startDate: null, dayCount: 0 }
} }
// React to holiday config changes: rebuild or refresh holidays
watch(
() => [
calendarStore.config.holidays.enabled,
calendarStore.config.holidays.country,
calendarStore.config.holidays.state,
calendarStore.config.holidays.region,
],
(_newVals, _oldVals) => {
// If weeks already built, just refresh holiday info
if (visibleWeeks.value.length) {
refreshHolidays('config-change')
} else {
resetWeeks('holiday-config-change')
}
},
{ deep: false },
)
function startDrag(dateStr) { function startDrag(dateStr) {
dateStr = normalizeDate(dateStr) dateStr = normalizeDate(dateStr)
if (calendarStore.config.select_days === 0) return if (calendarStore.config.select_days === 0) return
@ -294,15 +312,6 @@ function calculateSelection(anchorStr, otherStr) {
} }
} }
// ---------------- Week label column drag scrolling ----------------
function getWeekLabelRect() {
// Prefer header year label width as stable reference
const headerYear = document.querySelector('.calendar-header .year-label')
if (headerYear) return headerYear.getBoundingClientRect()
const weekLabel = viewport.value?.querySelector('.week-row .week-label')
return weekLabel ? weekLabel.getBoundingClientRect() : null
}
onMounted(() => { onMounted(() => {
computeRowHeight() computeRowHeight()
calendarStore.updateCurrentDate() calendarStore.updateCurrentDate()
@ -376,8 +385,6 @@ const handleEventClick = (payload) => {
emit('edit-event', payload) emit('edit-event', payload)
} }
// header year change delegated to manager
// Heuristic: rotate month label (180deg) only for predominantly Latin text. // Heuristic: rotate month label (180deg) only for predominantly Latin text.
// We explicitly avoid locale detection; rely solely on characters present. // We explicitly avoid locale detection; rely solely on characters present.
// Disable rotation if any CJK Unified Ideograph or Compatibility Ideograph appears. // Disable rotation if any CJK Unified Ideograph or Compatibility Ideograph appears.
@ -402,7 +409,7 @@ watch(
}, },
) )
// Watch lightweight mutation counter only (not deep events map) and rebuild lazily // Event changes
watch( watch(
() => calendarStore.events, () => calendarStore.events,
() => { () => {

View File

@ -101,12 +101,12 @@ onBeforeUnmount(() => {
display: flex; display: flex;
justify-content: end; justify-content: end;
align-items: center; align-items: center;
margin-right: 1.5rem; margin-inline-end: 2rem;
} }
.toggle-btn { .toggle-btn {
position: fixed; position: fixed;
top: 0; top: 0;
right: 0; inset-inline-end: 0;
background: transparent; background: transparent;
border: none; border: none;
color: var(--muted); color: var(--muted);
@ -157,7 +157,6 @@ onBeforeUnmount(() => {
color: var(--muted); color: var(--muted);
padding: 0; padding: 0;
margin: 0; margin: 0;
margin-right: 0.6rem;
cursor: pointer; cursor: pointer;
font-size: 1.5rem; font-size: 1.5rem;
line-height: 1; line-height: 1;

View File

@ -371,6 +371,28 @@ export function createVirtualWeekManager({
} }
} }
// Refresh holiday data for currently visible weeks without rebuilding structure
function refreshHolidays(reason = 'holidays-refresh') {
if (!visibleWeeks.value.length) return
const enabled = calendarStore.config.holidays.enabled
if (enabled) calendarStore._ensureHolidaysInitialized?.()
for (const week of visibleWeeks.value) {
for (const day of week.days) {
if (enabled) {
const holiday = getHolidayForDate(day.date)
;((day.holiday = holiday), (day.isHoliday = holiday !== null))
} else {
day.holiday = null
day.isHoliday = false
}
}
}
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.debug('[VirtualWeeks] refreshHolidays', reason, { weeks: visibleWeeks.value.length })
}
}
function goToToday() { function goToToday() {
const top = addDays(new Date(calendarStore.now), -21) const top = addDays(new Date(calendarStore.now), -21)
const targetWeekIndex = getWeekIndex(top) const targetWeekIndex = getWeekIndex(top)
@ -391,6 +413,7 @@ export function createVirtualWeekManager({
resetWeeks, resetWeeks,
updateVisibleWeeks, updateVisibleWeeks,
refreshEvents, refreshEvents,
refreshHolidays,
getWeekIndex, getWeekIndex,
getFirstDayForVirtualWeek, getFirstDayForVirtualWeek,
goToToday, goToToday,