Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions src/package/rangeflow/components/DateSlider/DateLabelsTrack.tsx
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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)
})
Expand Down
8 changes: 7 additions & 1 deletion src/package/rangeflow/components/DateSlider/SliderValue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
40 changes: 29 additions & 11 deletions src/package/rangeflow/components/PickerBar/CalendarPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -67,7 +80,7 @@ export function CalendarPopover({ children }: Props) {
<PopoverTrigger className="cursor-pointer">{children}</PopoverTrigger>
<PopoverContent align="start" sideOffset={10}>
<Calendar
defaultMonth={date.from}
defaultMonth={dayjs(date.from).isValid() ? date.from : undefined}
numberOfMonths={2}
showOutsideDays={false}
{...CalendarProps}
Expand All @@ -81,13 +94,18 @@ export function CalendarPopover({ children }: Props) {
return
}

const nextSelected = {
from: dayjs(nextDate.from).startOf('day').toDate(),
to: dayjs(nextDate.to).startOf('day').toDate()
const from = dayjs(nextDate.from).startOf('day')
const to = dayjs(nextDate.to).startOf('day')

if (!from.isValid() || !to.isValid()) {
return
}

update({
selected_date: nextSelected
selected_date: {
from: from.toDate(),
to: to.toDate()
}
})
}}
/>
Expand Down
9 changes: 4 additions & 5 deletions src/package/rangeflow/components/PickerBar/SelectedDate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}

Expand Down
8 changes: 6 additions & 2 deletions src/package/rangeflow/hooks/use-days-in-range.ts
Original file line number Diff line number Diff line change
@@ -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])
}
30 changes: 24 additions & 6 deletions src/package/rangeflow/utils/create-slider-values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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])

Expand All @@ -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
Expand Down
20 changes: 15 additions & 5 deletions src/package/rangeflow/utils/derive-selection-from-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand All @@ -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)

Expand Down
17 changes: 17 additions & 0 deletions src/package/rangeflow/utils/normalize-date-range.ts
Original file line number Diff line number Diff line change
@@ -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 }
}