diff --git a/src/components/CalendarDay.vue b/src/components/CalendarDay.vue
index fde7669..0b93324 100644
--- a/src/components/CalendarDay.vue
+++ b/src/components/CalendarDay.vue
@@ -21,13 +21,10 @@ const props = defineProps({
]"
:data-date="props.day.date"
>
-
{{ props.day.displayText }}
+ {{ props.day.displayText }}
{{ props.day.lunarPhase }}
-
-
-
- {{ props.day.holiday.name }}
-
+
+ {{ props.day.holiday.name }}
@@ -38,19 +35,27 @@ const props = defineProps({
border-right: 1px solid var(--border-color);
border-bottom: 1px solid var(--border-color);
user-select: none;
- display: flex;
- flex-direction: row;
- align-items: flex-start;
- justify-content: flex-start;
+ display: grid;
+ /* 3 columns: day number, flexible space, lunar phase */
+ grid-template-columns: min-content 1fr min-content;
+ /* 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;
overflow: hidden;
width: 100%;
height: var(--row-h);
font-weight: 700;
transition: background-color 0.15s ease;
+ align-items: start;
}
-
-.cell h1 {
+.cell h1.day-number {
margin: 0;
padding: 0;
min-width: 1.5em;
@@ -58,15 +63,16 @@ const props = defineProps({
font-weight: 700;
color: var(--ink);
transition: background-color 0.15s ease;
+ grid-area: day-number;
}
-.cell.weekend h1 {
+.cell.weekend h1.day-number {
color: var(--weekend);
}
-.cell.firstday h1 {
+.cell.firstday h1.day-number {
color: var(--firstday);
text-shadow: 0 0 0.1em var(--strong);
}
-.cell.today h1 {
+.cell.today h1.day-number {
border-radius: 2em;
background: var(--today);
border: 0.2em solid var(--today);
@@ -77,16 +83,9 @@ const props = defineProps({
.cell.selected {
filter: hue-rotate(180deg);
}
-.cell.selected h1 {
+.cell.selected h1.day-number {
color: var(--strong);
}
-.lunar-phase {
- position: absolute;
- top: 0.5em;
- right: 0.2em;
- font-size: 0.8em;
- opacity: 0.7;
-}
.cell.holiday {
background-image: linear-gradient(
135deg,
@@ -103,27 +102,32 @@ const props = defineProps({
);
}
}
-.cell.holiday h1 {
+.cell.holiday h1.day-number {
/* Slight emphasis without forcing a specific hue */
color: var(--holiday);
text-shadow: 0 0 0.3em rgba(255, 255, 255, 0.4);
}
-.holiday-info {
- position: absolute;
- bottom: 0.1em;
- left: 0.1em;
- right: 0.1em;
- line-height: 1;
- overflow: hidden;
- font-size: clamp(1.2vw, 0.6em, 1em);
+.lunar-phase {
+ grid-area: lunar-phase;
+ align-self: start;
+ justify-self: end;
+ margin-top: 0.5em;
+ margin-inline-end: 0.2em;
+ font-size: 0.8em;
+ opacity: 0.7;
}
-.holiday-name {
- display: block;
- color: var(--holiday-label);
- padding: 0.15em 0.35em 0.15em 0.25em;
+.holiday-info {
+ grid-area: holiday-info;
+ align-self: end;
+ overflow: hidden;
text-overflow: ellipsis;
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;
}
diff --git a/src/components/CalendarView.vue b/src/components/CalendarView.vue
index b2912e6..387f5d7 100644
--- a/src/components/CalendarView.vue
+++ b/src/components/CalendarView.vue
@@ -87,7 +87,7 @@ const vwm = createVirtualWeekManager({
contentHeight,
})
const visibleWeeks = vwm.visibleWeeks
-const { scheduleWindowUpdate, resetWeeks, refreshEvents } = vwm
+const { scheduleWindowUpdate, resetWeeks, refreshEvents, refreshHolidays } = vwm
// Scroll managers (after scheduleWindowUpdate available)
const scrollManager = createScrollManager({ viewport, scheduleRebuild: scheduleWindowUpdate })
@@ -98,8 +98,7 @@ const weekColumnScrollManager = createWeekColumnScrollManager({
contentHeight,
setScrollTop,
})
-const { isWeekColDragging, handleWeekColMouseDown, handlePointerLockChange } =
- weekColumnScrollManager
+const { handleWeekColMouseDown, handlePointerLockChange } = weekColumnScrollManager
const monthScrollManager = createMonthScrollManager({
viewport,
viewportHeight,
@@ -160,6 +159,25 @@ function clearSelection() {
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) {
dateStr = normalizeDate(dateStr)
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(() => {
computeRowHeight()
calendarStore.updateCurrentDate()
@@ -376,8 +385,6 @@ const handleEventClick = (payload) => {
emit('edit-event', payload)
}
-// header year change delegated to manager
-
// Heuristic: rotate month label (180deg) only for predominantly Latin text.
// We explicitly avoid locale detection; rely solely on characters present.
// 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(
() => calendarStore.events,
() => {
diff --git a/src/components/HeaderControls.vue b/src/components/HeaderControls.vue
index e61a373..e1ca08a 100644
--- a/src/components/HeaderControls.vue
+++ b/src/components/HeaderControls.vue
@@ -101,12 +101,12 @@ onBeforeUnmount(() => {
display: flex;
justify-content: end;
align-items: center;
- margin-right: 1.5rem;
+ margin-inline-end: 2rem;
}
.toggle-btn {
position: fixed;
top: 0;
- right: 0;
+ inset-inline-end: 0;
background: transparent;
border: none;
color: var(--muted);
@@ -157,7 +157,6 @@ onBeforeUnmount(() => {
color: var(--muted);
padding: 0;
margin: 0;
- margin-right: 0.6rem;
cursor: pointer;
font-size: 1.5rem;
line-height: 1;
diff --git a/src/plugins/virtualWeeks.js b/src/plugins/virtualWeeks.js
index 6d73466..817228f 100644
--- a/src/plugins/virtualWeeks.js
+++ b/src/plugins/virtualWeeks.js
@@ -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() {
const top = addDays(new Date(calendarStore.now), -21)
const targetWeekIndex = getWeekIndex(top)
@@ -391,6 +413,7 @@ export function createVirtualWeekManager({
resetWeeks,
updateVisibleWeeks,
refreshEvents,
+ refreshHolidays,
getWeekIndex,
getFirstDayForVirtualWeek,
goToToday,