Compare commits
	
		
			7 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | a0b140d54b | ||
|   | 365d9e1be2 | ||
|   | 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 }, | ||||
| ) | ||||
|   | ||||
| @@ -1,15 +1,61 @@ | ||||
| <script setup> | ||||
| import { computed } from 'vue' | ||||
| import { formatDateCompact, fromLocalString } from '@/utils/date' | ||||
| 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) | ||||
|   return formatDateCompact(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> | ||||
|  | ||||
| @@ -17,16 +63,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 +80,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 +124,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); | ||||
| @@ -127,6 +182,7 @@ const formattedDate = computed(() => { | ||||
|   color: var(--ink); | ||||
|   line-height: 1; | ||||
|   pointer-events: none; | ||||
|   white-space: pre-wrap; | ||||
| } | ||||
|  | ||||
| .cell.weekend .compact-date { | ||||
|   | ||||
| @@ -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; | ||||
|   } | ||||
|   | ||||
| @@ -52,17 +52,17 @@ function clampScroll(x) { | ||||
| function animateTo(target){target=clampScroll(target);const now=performance.now();if(animActive){const p=Math.min(1,(now-animStart)/ANIM_DURATION);animFrom=animFrom+(animTo-animFrom)*easeOutCubic(p);animTo=target;animStart=now;}else{animFrom=props.scrollTop;animTo=target;animStart=now;animActive=true;animFrame=requestAnimationFrame(stepAnim);return}if(!animFrame)animFrame=requestAnimationFrame(stepAnim)} | ||||
| function stepAnim(){if(!animActive)return;const t=Math.min(1,(performance.now()-animStart)/ANIM_DURATION);const val=animFrom+(animTo-animFrom)*easeOutCubic(t);emit('scroll-to',clampScroll(val));if(t>=1){animActive=false;animFrame=null;return}animFrame=requestAnimationFrame(stepAnim)} | ||||
|  | ||||
| function onDragMouseDown(e){if(e.button!==0)return;if(animActive){const now=performance.now();const p=Math.min(1,(now-animStart)/ANIM_DURATION);const cur=animFrom+(animTo-animFrom)*easeOutCubic(p);animActive=false;animFrame&&cancelAnimationFrame(animFrame);animFrame=null;emit('scroll-to',clampScroll(cur));}cancelDragMomentum();isDragging.value=true;mainStartScroll=props.scrollTop;accumDelta=0;lastClientY=e.clientY;dragSamples=[{t:performance.now(),s:mainStartScroll}];if(jogwheelViewport.value&&jogwheelViewport.value.requestPointerLock)jogwheelViewport.value.requestPointerLock();window.addEventListener('mousemove',onDragMouseMove,{passive:false});window.addEventListener('mouseup',onDragMouseUp,{passive:false});e.preventDefault()} | ||||
| function onDragPointerDown(e){if(e.button!==0)return;if(animActive){const now=performance.now();const p=Math.min(1,(now-animStart)/ANIM_DURATION);const cur=animFrom+(animTo-animFrom)*easeOutCubic(p);animActive=false;animFrame&&cancelAnimationFrame(animFrame);animFrame=null;emit('scroll-to',clampScroll(cur));}cancelDragMomentum();isDragging.value=true;mainStartScroll=props.scrollTop;accumDelta=0;lastClientY=e.clientY;dragSamples=[{t:performance.now(),s:mainStartScroll}];if(jogwheelViewport.value&&jogwheelViewport.value.requestPointerLock)jogwheelViewport.value.requestPointerLock();window.addEventListener('pointermove',onDragPointerMove,{passive:false});window.addEventListener('pointerup',onDragPointerUp,{passive:false});window.addEventListener('pointercancel',onDragPointerUp,{passive:false});e.preventDefault()} | ||||
|  | ||||
| function onDragMouseMove(e){if(!isDragging.value) return;let dy=typeof e.movementY==='number'?e.movementY:0;if(!pointerLocked){if(lastClientY!=null)dy=e.clientY-lastClientY;lastClientY=e.clientY;}accumDelta+=dy;let desired=mainStartScroll-accumDelta*SPEED_DRAG;if(desired<0)desired=0;const maxScroll=Math.max(0,props.totalVirtualWeeks*props.rowHeight-props.viewportHeight);if(desired>maxScroll)desired=maxScroll;emit('scroll-to',desired);dragSamples.push({t:performance.now(),s:desired});e.preventDefault()} | ||||
| function onDragPointerMove(e){if(!isDragging.value) return;let dy=typeof e.movementY==='number'?e.movementY:0;if(!pointerLocked){if(lastClientY!=null)dy=e.clientY-lastClientY;lastClientY=e.clientY;}accumDelta+=dy;let desired=mainStartScroll-accumDelta*SPEED_DRAG;if(desired<0)desired=0;const maxScroll=Math.max(0,props.totalVirtualWeeks*props.rowHeight-props.viewportHeight);if(desired>maxScroll)desired=maxScroll;emit('scroll-to',desired);dragSamples.push({t:performance.now(),s:desired});e.preventDefault()} | ||||
|  | ||||
| function onDragMouseUp(e){if(!isDragging.value)return;isDragging.value=false;lastClientY=null;window.removeEventListener('mousemove',onDragMouseMove);window.removeEventListener('mouseup',onDragMouseUp);if(pointerLocked&&document.exitPointerLock)document.exitPointerLock();const v=computeDragVelocity();dragSamples=[];if(Math.abs(v)>=DRAG_MIN_V)startDragMomentum(v);e.preventDefault()} | ||||
| function onDragPointerUp(e){if(!isDragging.value)return;isDragging.value=false;lastClientY=null;window.removeEventListener('pointermove',onDragPointerMove);window.removeEventListener('pointerup',onDragPointerUp);window.removeEventListener('pointercancel',onDragPointerUp);if(pointerLocked&&document.exitPointerLock)document.exitPointerLock();const v=computeDragVelocity();dragSamples=[];if(Math.abs(v)>=DRAG_MIN_V)startDragMomentum(v);e.preventDefault()} | ||||
|  | ||||
| function handlePointerLockChange(){pointerLocked=document.pointerLockElement===jogwheelViewport.value;if(!pointerLocked&&isDragging.value)onDragMouseUp(new MouseEvent('mouseup'))} | ||||
| function handlePointerLockChange(){pointerLocked=document.pointerLockElement===jogwheelViewport.value;if(!pointerLocked&&isDragging.value)onDragPointerUp(new PointerEvent('pointerup'))} | ||||
|  | ||||
| onMounted(()=>{if(jogwheelViewport.value){jogwheelViewport.value.addEventListener('mousedown',onDragMouseDown);jogwheelViewport.value.addEventListener('wheel',onWheel,{passive:false,capture:true});}document.addEventListener('pointerlockchange',handlePointerLockChange)}) | ||||
| onMounted(()=>{if(jogwheelViewport.value){jogwheelViewport.value.addEventListener('pointerdown',onDragPointerDown);jogwheelViewport.value.addEventListener('wheel',onWheel,{passive:false,capture:true});}document.addEventListener('pointerlockchange',handlePointerLockChange)}) | ||||
|  | ||||
| onBeforeUnmount(()=>{if(jogwheelViewport.value){jogwheelViewport.value.removeEventListener('mousedown',onDragMouseDown);jogwheelViewport.value.removeEventListener('wheel',onWheel);}window.removeEventListener('mousemove',onDragMouseMove);window.removeEventListener('mouseup',onDragMouseUp);document.removeEventListener('pointerlockchange',handlePointerLockChange)}) | ||||
| onBeforeUnmount(()=>{if(jogwheelViewport.value){jogwheelViewport.value.removeEventListener('pointerdown',onDragPointerDown);jogwheelViewport.value.removeEventListener('wheel',onWheel);}window.removeEventListener('pointermove',onDragPointerMove);window.removeEventListener('pointerup',onDragPointerUp);window.removeEventListener('pointercancel',onDragPointerUp);document.removeEventListener('pointerlockchange',handlePointerLockChange)}) | ||||
|  | ||||
| function onWheel(e){if(e.ctrlKey)return;e.preventDefault();e.stopPropagation();cancelDragMomentum();const dy=e.deltaY;if(Math.abs(dy)<MIN_WHEEL_ABS)return;const dir=dy>0?1:-1;const base=animActive?animTo:props.scrollTop;animateTo(base+dir*MONTH_SCROLL())} | ||||
|  | ||||
| @@ -87,6 +87,7 @@ function startDragMomentum(v){cancelDragMomentum();dragMomentumVelocity=v;dragMo | ||||
|   z-index: 20; | ||||
|   cursor: ns-resize; | ||||
|   overscroll-behavior: contain; | ||||
|   touch-action: none; | ||||
| } | ||||
|  | ||||
| .jogwheel-viewport::-webkit-scrollbar { 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,24 +185,13 @@ 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) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Format date as compact string for day cell corner (e.g., "Mon 15 Jan") | ||||
|  */ | ||||
| function formatDateCompact(date) { | ||||
|   return date.toLocaleDateString(undefined, {  | ||||
|     weekday: 'short',  | ||||
|     day: 'numeric',  | ||||
|     month: 'short' | ||||
|   }) | ||||
| } | ||||
|  | ||||
| export { | ||||
|   // constants | ||||
|   monthAbbr, | ||||
| @@ -229,7 +218,6 @@ export { | ||||
|   formatDateRange, | ||||
|   formatDateShort, | ||||
|   formatDateLong, | ||||
|   formatDateCompact, | ||||
|   formatTodayString, | ||||
|   lunarPhaseSymbol, | ||||
|   // iso helpers re-export | ||||
|   | ||||
		Reference in New Issue
	
	Block a user