diff --git a/src/package/rangeflow/components/DateSlider/DateLabelsTrack.tsx b/src/package/rangeflow/components/DateSlider/DateLabelsTrack.tsx
index 0a2b18a..c5dda46 100644
--- a/src/package/rangeflow/components/DateSlider/DateLabelsTrack.tsx
+++ b/src/package/rangeflow/components/DateSlider/DateLabelsTrack.tsx
@@ -1,11 +1,11 @@
import clsx from 'clsx'
-import dayjs from 'dayjs'
import { createElement, memo, useMemo } from 'react'
import { OdometerText } from '../../animations/OdometerText'
import { useDaysInRange } from '../../hooks/use-days-in-range'
import { useRangeFlowSlots } from '../../hooks/use-rangeflow-slots'
import { useRangeFlowStore } from '../../hooks/use-rangeflow-store'
+import { normalizeDateRange } from '../../utils/normalize-date-range'
function getLabelFormat(daysInRange: number): string {
if (daysInRange > 120) {
@@ -42,12 +42,13 @@ export const DateLabelsTrack = memo(() => {
const labels = useMemo(() => {
const format = getLabelFormat(daysInRange)
- const count = getLabelCount(daysInRange)
- const start = dayjs(range.from)
- const totalMs = dayjs(range.to).diff(start)
+ const count = Math.max(getLabelCount(daysInRange), 1)
+ const { start, end } = normalizeDateRange(range)
+ const totalMs = end.diff(start)
+ const steps = Math.max(count - 1, 1)
return Array.from({ length: count }, (_, i) => {
- const ratio = i / (count - 1)
+ const ratio = i / steps
return start.add(totalMs * ratio, 'ms').format(format)
})
diff --git a/src/package/rangeflow/components/DateSlider/SliderValue.tsx b/src/package/rangeflow/components/DateSlider/SliderValue.tsx
index 9a268f4..d4fceca 100644
--- a/src/package/rangeflow/components/DateSlider/SliderValue.tsx
+++ b/src/package/rangeflow/components/DateSlider/SliderValue.tsx
@@ -11,7 +11,13 @@ export const SliderValue = memo(() => {
const selected = useRangeFlowStore(state => state.selected_date)
const label = useMemo(() => {
- const days = dayjs(selected.to).diff(selected.from, 'day') + 1
+ const from = dayjs(selected.from)
+ const to = dayjs(selected.to)
+
+ const safeFrom = from.isValid() ? from : to
+ const safeTo = to.isValid() ? to : from
+ const rawDays = safeTo.isValid() && safeFrom.isValid() ? safeTo.diff(safeFrom, 'day') + 1 : 1
+ const days = Number.isFinite(rawDays) ? Math.max(rawDays, 1) : 1
if (size < 10) {
return `${days}D`
diff --git a/src/package/rangeflow/components/PickerBar/CalendarPopover.tsx b/src/package/rangeflow/components/PickerBar/CalendarPopover.tsx
index 755e623..2e2ef96 100644
--- a/src/package/rangeflow/components/PickerBar/CalendarPopover.tsx
+++ b/src/package/rangeflow/components/PickerBar/CalendarPopover.tsx
@@ -25,15 +25,28 @@ export function CalendarPopover({ children }: Props) {
} = useRangeFlowRefs()
const extendRange = () => {
- let rangeFrom = dayjs(range.from)
- let rangeTo = dayjs(range.to)
+ const rangeFromDate = dayjs(range.from)
+ const rangeToDate = dayjs(range.to)
+ const selectedFrom = dayjs(date.from)
+ const selectedTo = dayjs(date.to)
- if (dayjs(date.from).isBefore(rangeFrom)) {
- rangeFrom = dayjs(date.from).subtract(10, 'day')
+ let rangeFrom = rangeFromDate.isValid() ? rangeFromDate : dayjs()
+ let rangeTo = rangeToDate.isValid() ? rangeToDate : rangeFrom
+
+ if (rangeTo.isBefore(rangeFrom)) {
+ rangeTo = rangeFrom
+ }
+
+ if (selectedFrom.isValid() && selectedFrom.isBefore(rangeFrom)) {
+ rangeFrom = selectedFrom.subtract(10, 'day')
}
- if (dayjs(date.to).isAfter(rangeTo)) {
- rangeTo = dayjs(date.to).add(10, 'day')
+ if (selectedTo.isValid() && selectedTo.isAfter(rangeTo)) {
+ rangeTo = selectedTo.add(10, 'day')
+ }
+
+ if (rangeTo.isBefore(rangeFrom)) {
+ rangeTo = rangeFrom.add(1, 'day')
}
return {
@@ -67,7 +80,7 @@ export function CalendarPopover({ children }: Props) {
{children}
diff --git a/src/package/rangeflow/components/PickerBar/SelectedDate.tsx b/src/package/rangeflow/components/PickerBar/SelectedDate.tsx
index 311adc9..ca1090d 100644
--- a/src/package/rangeflow/components/PickerBar/SelectedDate.tsx
+++ b/src/package/rangeflow/components/PickerBar/SelectedDate.tsx
@@ -14,19 +14,18 @@ export function SelectedDate() {
const start = dayjs(date.from)
const end = dayjs(date.to)
-
const formatter = start.isSame(end, 'year') ? 'DD MMM' : 'DD MMM YYYY'
const labels = {
- from: start.format(formatter),
- to: end.format(formatter)
+ from: start.isValid() ? start.format(formatter) : 'Invalid Date',
+ to: end.isValid() ? end.format(formatter) : 'Invalid Date'
}
- if (today.isSame(start, 'day')) {
+ if (start.isValid() && today.isSame(start, 'day')) {
labels.from = 'Today'
}
- if (today.isSame(end, 'day')) {
+ if (end.isValid() && today.isSame(end, 'day')) {
labels.to = 'Today'
}
diff --git a/src/package/rangeflow/hooks/use-days-in-range.ts b/src/package/rangeflow/hooks/use-days-in-range.ts
index 79b1e91..4b892b0 100644
--- a/src/package/rangeflow/hooks/use-days-in-range.ts
+++ b/src/package/rangeflow/hooks/use-days-in-range.ts
@@ -1,8 +1,12 @@
-import dayjs from 'dayjs'
import { useMemo } from 'react'
import type { DateRange } from '../types'
+import { normalizeDateRange } from '../utils/normalize-date-range'
export function useDaysInRange(range: DateRange) {
- return useMemo(() => dayjs(range.to).diff(dayjs(range.from), 'day'), [range.from, range.to])
+ return useMemo(() => {
+ const { start, end } = normalizeDateRange(range)
+ const rawDiff = end.diff(start, 'day')
+ return Number.isFinite(rawDiff) ? Math.max(rawDiff + 1, 1) : 1
+ }, [range.from, range.to])
}
diff --git a/src/package/rangeflow/utils/create-slider-values.ts b/src/package/rangeflow/utils/create-slider-values.ts
index 15766d6..bc6a1b1 100644
--- a/src/package/rangeflow/utils/create-slider-values.ts
+++ b/src/package/rangeflow/utils/create-slider-values.ts
@@ -4,6 +4,7 @@ import { SLIDER_THUMB_MIN_SIZE } from '../constants/slider'
import type { DateRange } from '../types'
import { clamp } from './clamp'
import { interpolate } from './interpolate'
+import { normalizeDateRange } from './normalize-date-range'
const toVisual = interpolate([1, 100], [SLIDER_THUMB_MIN_SIZE, 100])
@@ -14,14 +15,31 @@ export function createSliderValues(
to: Date
}
) {
- const rangeStart = dayjs(range.from).startOf('day')
- const daysInRange = dayjs(range.to).startOf('day').diff(rangeStart, 'day')
+ const { start: rangeStart, end: rangeEnd } = normalizeDateRange(range)
- const fromDay = dayjs(selected.from).startOf('day')
- const toDay = dayjs(selected.to).startOf('day')
+ const rawRangeDays = rangeEnd.diff(rangeStart, 'day')
+ const daysInRange = Number.isFinite(rawRangeDays)
+ ? Math.max(rawRangeDays + 1, 1)
+ : 1
- const pastDays = Math.max(fromDay.diff(rangeStart, 'day'), 0)
- const selectedDays = toDay.diff(fromDay, 'day') + 1
+ const fromDay = dayjs(selected.from)
+ const toDay = dayjs(selected.to)
+ const safeFrom = fromDay.isValid() ? fromDay.startOf('day') : rangeStart
+ let safeTo = toDay.isValid() ? toDay.startOf('day') : safeFrom
+
+ if (safeTo.isBefore(safeFrom)) {
+ safeTo = safeFrom
+ }
+
+ const rawPastDays = safeFrom.diff(rangeStart, 'day')
+ const pastDays = Number.isFinite(rawPastDays)
+ ? Math.max(Math.min(rawPastDays, daysInRange - 1), 0)
+ : 0
+
+ const rawSelectedDays = safeTo.diff(safeFrom, 'day') + 1
+ const selectedDays = Number.isFinite(rawSelectedDays)
+ ? Math.max(Math.min(rawSelectedDays, daysInRange), 1)
+ : 1
const rawSize = (selectedDays * 100) / daysInRange
const rawLeft = (pastDays * 100) / daysInRange
diff --git a/src/package/rangeflow/utils/derive-selection-from-layout.ts b/src/package/rangeflow/utils/derive-selection-from-layout.ts
index f14ea8b..9c7dbf1 100644
--- a/src/package/rangeflow/utils/derive-selection-from-layout.ts
+++ b/src/package/rangeflow/utils/derive-selection-from-layout.ts
@@ -10,6 +10,7 @@ import {
import type { DateRange } from '../types'
import { clamp } from './clamp'
import { interpolate } from './interpolate'
+import { normalizeDateRange } from './normalize-date-range'
// Inverse of the `toVisual` mapping in createSliderValues: [MIN, 100] → [1, 100].
const fromVisual = interpolate([SLIDER_THUMB_MIN_SIZE, 100], [1, 100])
@@ -21,20 +22,29 @@ export function deriveSelectionFromLayout(layout: Layout, range: DateRange) {
const left = layout[SLIDER_LEFT_SPACER]
const right = layout[SLIDER_RIGHT_SPACER]
- const start = dayjs(range.from).startOf('day')
- const daysInRange = dayjs(range.to).startOf('day').diff(start, 'day')
+ const { start, end } = normalizeDateRange(range)
+ const rawRangeDays = end.diff(start, 'day')
+ const daysInRange = Number.isFinite(rawRangeDays)
+ ? Math.max(rawRangeDays + 1, 1)
+ : 1
+
+ const safeSize = Number.isFinite(size) ? size : SLIDER_THUMB_MIN_SIZE
+ const safeLeft = Number.isFinite(left) ? left : 0
+ const safeRight = Number.isFinite(right)
+ ? right
+ : Math.max(100 - safeLeft - safeSize, 0)
+ const actualSize = fromVisual(safeSize)
+ const actualLeft = safeLeft + (safeSize - actualSize)
// Undo the inflation createSliderValues applied to size (absorbed by the
// left spacer). The right spacer is already unscaled, so no inversion there.
- const actualSize = fromVisual(size)
- const actualLeft = left + (size - actualSize)
// Derive totalDays from the thumb width (the authoritative value), not from
// the leftover between startDay and trailingDays — otherwise independent
// rounding of the two spacers can flip the selection length by ±1 day
// while the user is only translating the thumb across the track.
const totalDays =
- Math.round(size) <= SLIDER_THUMB_MIN_SIZE
+ Math.round(safeSize) <= SLIDER_THUMB_MIN_SIZE
? 1
: Math.max(Math.round((actualSize * daysInRange) / 100), 1)
diff --git a/src/package/rangeflow/utils/normalize-date-range.ts b/src/package/rangeflow/utils/normalize-date-range.ts
new file mode 100644
index 0000000..f837f89
--- /dev/null
+++ b/src/package/rangeflow/utils/normalize-date-range.ts
@@ -0,0 +1,17 @@
+import dayjs from 'dayjs'
+import type { DateRange } from '../types'
+
+export function normalizeDateRange(range: DateRange) {
+ const now = dayjs().startOf('day')
+ const startDate = dayjs(range.from)
+ const endDate = dayjs(range.to)
+
+ const start = startDate.isValid() ? startDate.startOf('day') : now
+ let end = endDate.isValid() ? endDate.startOf('day') : start
+
+ if (end.isBefore(start)) {
+ end = start
+ }
+
+ return { start, end }
+}