Files
calendar/src/components/CalendarDay.vue

215 lines
5.5 KiB
Vue

<script setup>
import { computed, ref, onMounted, onBeforeUnmount } from 'vue'
import { fromLocalString } from '@/utils/date'
const props = defineProps({
day: Object,
dragging: { type: Boolean, default: false },
})
// Reactive viewport width detection
const isNarrowView = ref(false)
const isVeryNarrowView = ref(false)
const isSmallView = ref(false)
function checkViewportWidth() {
const width = window.innerWidth
isSmallView.value = width < 800
isNarrowView.value = width < 600
isVeryNarrowView.value = width < 400
}
onMounted(() => {
checkViewportWidth()
window.addEventListener('resize', checkViewportWidth)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', checkViewportWidth)
})
const formattedDate = computed(() => {
const date = fromLocalString(props.day.date)
let options = { day: 'numeric', month: 'short' }
if (isVeryNarrowView.value) {
// Very narrow: show only day number
options = { day: 'numeric' }
} else if (isNarrowView.value) {
// Narrow: show day and month, no weekday
options = { day: 'numeric', month: 'short' }
} else {
// Wide: show weekday, day, and month
options = { weekday: 'short', day: 'numeric', month: 'short' }
}
let formatted = date.toLocaleDateString(undefined, options)
// Below 700px, replace first space with newline to force weekday on separate line
if (isSmallView.value && !isNarrowView.value && !isVeryNarrowView.value) {
formatted = formatted.replace(/\s/, '\n')
}
// Replace the last space (between month and day) with nbsp to prevent breaking there
// but keep the space after weekday (if present) as regular space to allow wrapping
formatted = formatted.replace(/\s+(?=\S+$)/, '\u00A0')
return formatted
})
</script>
<template>
<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 }]"
:data-date="props.day.date"
>
<span class="compact-date">{{ formattedDate }}</span>
<h1 class="day-number">{{ props.day.displayText }}</h1>
<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">
{{ props.day.holiday.name }}
</div>
</div>
</template>
<style scoped>
.cell {
position: relative;
user-select: none;
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr auto;
grid-template-areas:
'day-number'
'holiday-info';
padding: 0.25em;
overflow: visible;
width: 100%;
height: var(--row-h);
font-weight: 700;
transition: background-color 0.15s ease;
align-items: center;
justify-items: center;
}
.cell h1.day-number {
position: absolute;
font-size: 5vmin;
font-weight: 800;
color: var(--ink);
transition: all 0.15s ease;
}
.cell.firstday h1.day-number {
font-weight: 400;
}
.cell.weekend h1.day-number {
color: var(--weekend);
}
.cell.firstday h1.day-number {
color: var(--firstday);
}
.cell.today::before {
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(--today);
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);
}
.cell {
background-image: linear-gradient(
135deg,
var(--holiday-grad-start, rgba(255, 255, 255, 0.3)) 0%,
var(--holiday-grad-end, rgba(255, 255, 255, 0)) 70%
);
}
@media (prefers-color-scheme: dark) {
.cell {
background-image: linear-gradient(
135deg,
var(--holiday-grad-start, rgba(255, 255, 255, 0.05)) 0%,
var(--holiday-grad-end, rgba(255, 255, 255, 0)) 70%
);
}
}
.lunar-phase {
grid-area: lunar-phase;
position: absolute;
inset-block-start: 0.5em;
inset-inline-end: 0.2em;
font-size: 0.8em;
opacity: 0.7;
}
.compact-date {
position: absolute;
top: 0.25em;
left: 0.25em;
inset-inline-end: 1rem; /* Space for lunar phase */
font-weight: 400;
color: var(--ink);
line-height: 1;
pointer-events: none;
white-space: pre-wrap;
}
.cell.weekend .compact-date {
color: var(--weekend);
}
.cell.firstday .compact-date {
color: var(--firstday);
}
.cell.today .compact-date {
color: var(--strong);
}
.cell.selected .compact-date {
color: var(--strong);
}
.holiday-info {
grid-area: holiday-info;
align-self: end;
overflow: hidden;
max-width: 100%;
color: var(--holiday);
font-size: 1em;
font-weight: 400;
line-height: 1.0;
padding-inline: 0.15em;
padding-block: 0;
pointer-events: auto;
}
</style>