` inside each button, creating/destroying the indicator. Change this to a single absolutely-positioned indicator div that slides to the active item using CSS transitions.
+
+**Step 1: Rewrite NavRail template**
+
+Replace the entire template in `src/components/NavRail.vue` with:
+
+```html
+
+
+
+```
+
+**Step 2: Add activeIndex computed**
+
+In the `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/AppDatePicker.vue b/src/components/AppDatePicker.vue
index 53dad42..68629c4 100644
--- a/src/components/AppDatePicker.vue
+++ b/src/components/AppDatePicker.vue
@@ -51,28 +51,105 @@ const displayText = computed(() => {
return datePart
})
-// ── Time helpers ──────────────────────────────────────────────────────
+// ── Time wheel ──────────────────────────────────────────────────────
+const WHEEL_ITEM_H = 36
+const WHEEL_VISIBLE = 5
+const WHEEL_HEIGHT = WHEEL_ITEM_H * WHEEL_VISIBLE // 180px
+const WHEEL_PAD = WHEEL_ITEM_H * 2 // 72px spacer (2 items above/below center)
+
const internalHour = ref(props.hour)
const internalMinute = ref(props.minute)
+const hourWheelRef = ref(null)
+const minuteWheelRef = ref(null)
watch(() => props.hour, (v) => { internalHour.value = v })
watch(() => props.minute, (v) => { internalMinute.value = v })
-function onHourInput(e: Event) {
- const val = parseInt((e.target as HTMLInputElement).value)
- if (!isNaN(val)) {
- const clamped = Math.min(23, Math.max(0, val))
- internalHour.value = clamped
- emit('update:hour', clamped)
- }
+// Debounced scroll handler to read the current value
+let hourScrollTimer: ReturnType | null = null
+let minuteScrollTimer: ReturnType | null = null
+
+function onHourScroll() {
+ if (hourScrollTimer) clearTimeout(hourScrollTimer)
+ hourScrollTimer = setTimeout(() => {
+ if (!hourWheelRef.value) return
+ const index = Math.round(hourWheelRef.value.scrollTop / WHEEL_ITEM_H)
+ const clamped = Math.min(23, Math.max(0, index))
+ if (internalHour.value !== clamped) {
+ internalHour.value = clamped
+ emit('update:hour', clamped)
+ }
+ }, 60)
}
-function onMinuteInput(e: Event) {
- const val = parseInt((e.target as HTMLInputElement).value)
- if (!isNaN(val)) {
- const clamped = Math.min(59, Math.max(0, val))
- internalMinute.value = clamped
- emit('update:minute', clamped)
+function onMinuteScroll() {
+ if (minuteScrollTimer) clearTimeout(minuteScrollTimer)
+ minuteScrollTimer = setTimeout(() => {
+ if (!minuteWheelRef.value) return
+ const index = Math.round(minuteWheelRef.value.scrollTop / WHEEL_ITEM_H)
+ const clamped = Math.min(59, Math.max(0, index))
+ if (internalMinute.value !== clamped) {
+ internalMinute.value = clamped
+ emit('update:minute', clamped)
+ }
+ }, 60)
+}
+
+// Mouse wheel: one item per tick
+function onHourWheel(e: WheelEvent) {
+ e.preventDefault()
+ if (!hourWheelRef.value) return
+ const dir = e.deltaY > 0 ? 1 : -1
+ const cur = Math.round(hourWheelRef.value.scrollTop / WHEEL_ITEM_H)
+ const next = Math.min(23, Math.max(0, cur + dir))
+ hourWheelRef.value.scrollTo({ top: next * WHEEL_ITEM_H, behavior: 'smooth' })
+}
+
+function onMinuteWheel(e: WheelEvent) {
+ e.preventDefault()
+ if (!minuteWheelRef.value) return
+ const dir = e.deltaY > 0 ? 1 : -1
+ const cur = Math.round(minuteWheelRef.value.scrollTop / WHEEL_ITEM_H)
+ const next = Math.min(59, Math.max(0, cur + dir))
+ minuteWheelRef.value.scrollTo({ top: next * WHEEL_ITEM_H, behavior: 'smooth' })
+}
+
+// Click-and-drag support
+let dragEl: HTMLElement | null = null
+let dragStartY = 0
+let dragStartScrollTop = 0
+
+function onWheelPointerDown(e: PointerEvent) {
+ const el = e.currentTarget as HTMLElement
+ dragEl = el
+ dragStartY = e.clientY
+ dragStartScrollTop = el.scrollTop
+ el.setPointerCapture(e.pointerId)
+}
+
+function onWheelPointerMove(e: PointerEvent) {
+ if (!dragEl) return
+ e.preventDefault()
+ const delta = dragStartY - e.clientY
+ dragEl.scrollTop = dragStartScrollTop + delta
+}
+
+function onWheelPointerUp(e: PointerEvent) {
+ if (!dragEl) return
+ const el = dragEl
+ dragEl = null
+ el.releasePointerCapture(e.pointerId)
+ // Snap to nearest item
+ const index = Math.round(el.scrollTop / WHEEL_ITEM_H)
+ el.scrollTo({ top: index * WHEEL_ITEM_H, behavior: 'smooth' })
+}
+
+function scrollWheelsToTime() {
+ if (hourWheelRef.value) {
+ hourWheelRef.value.scrollTop = internalHour.value * WHEEL_ITEM_H
+ }
+ if (minuteWheelRef.value) {
+ minuteWheelRef.value.scrollTop = internalMinute.value * WHEEL_ITEM_H
}
}
@@ -106,9 +183,7 @@ const dayCells = computed(() => {
const y = viewYear.value
const m = viewMonth.value
- // First day of the month (0=Sun, 1=Mon, ...)
const firstDayOfWeek = new Date(y, m, 1).getDay()
- // Shift so Monday=0
const startOffset = (firstDayOfWeek + 6) % 7
const daysInMonth = new Date(y, m + 1, 0).getDate()
@@ -116,7 +191,6 @@ const dayCells = computed(() => {
const cells: DayCell[] = []
- // Previous month padding
const prevMonth = m === 0 ? 11 : m - 1
const prevYear = m === 0 ? y - 1 : y
for (let i = startOffset - 1; i >= 0; i--) {
@@ -130,7 +204,6 @@ const dayCells = computed(() => {
})
}
- // Current month days
for (let d = 1; d <= daysInMonth; d++) {
cells.push({
date: d,
@@ -141,7 +214,6 @@ const dayCells = computed(() => {
})
}
- // Next month padding (fill up to 42 cells = 6 rows)
const nextMonth = m === 11 ? 0 : m + 1
const nextYear = m === 11 ? y + 1 : y
let nextDay = 1
@@ -173,10 +245,9 @@ function updatePosition() {
if (!triggerRef.value) return
const rect = triggerRef.value.getBoundingClientRect()
const zoom = getZoomFactor()
- const panelWidth = 280
+ const panelWidth = props.showTime ? 390 : 280
const renderedWidth = panelWidth * zoom
- // Compute left in viewport space, then convert to panel's zoomed coordinate space
let leftViewport = rect.left
if (leftViewport + renderedWidth > window.innerWidth) {
leftViewport = window.innerWidth - renderedWidth - 8
@@ -203,7 +274,6 @@ function toggle() {
}
function open() {
- // Sync view to modelValue or today
if (props.modelValue) {
const [y, m] = props.modelValue.split('-').map(Number)
viewYear.value = y
@@ -221,6 +291,10 @@ function open() {
document.addEventListener('click', onClickOutside, true)
document.addEventListener('scroll', onScrollOrResize, true)
window.addEventListener('resize', onScrollOrResize)
+
+ if (props.showTime) {
+ scrollWheelsToTime()
+ }
})
}
@@ -254,12 +328,16 @@ function nextMonthNav() {
function selectDay(cell: DayCell) {
if (!cell.isCurrentMonth) return
emit('update:modelValue', cell.dateString)
- close()
+ if (!props.showTime) {
+ close()
+ }
}
function selectToday() {
emit('update:modelValue', todayString())
- close()
+ if (!props.showTime) {
+ close()
+ }
}
// ── Event handlers ──────────────────────────────────────────────────
@@ -332,11 +410,12 @@ onBeforeUnmount(() => {
+