react-native-ruler-date-picker 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +186 -60
- package/lib/commonjs/LensTick.js +83 -0
- package/lib/commonjs/LensTick.js.map +1 -0
- package/lib/commonjs/Ruler.js +38 -0
- package/lib/commonjs/Ruler.js.map +1 -0
- package/lib/commonjs/RulerContext.js +17 -0
- package/lib/commonjs/RulerContext.js.map +1 -0
- package/lib/commonjs/RulerDatePicker.js +38 -351
- package/lib/commonjs/RulerDatePicker.js.map +1 -1
- package/lib/commonjs/RulerDateRangePicker.js +56 -0
- package/lib/commonjs/RulerDateRangePicker.js.map +1 -0
- package/lib/commonjs/RulerHeader.js +106 -0
- package/lib/commonjs/RulerHeader.js.map +1 -0
- package/lib/commonjs/RulerIndicator.js +67 -0
- package/lib/commonjs/RulerIndicator.js.map +1 -0
- package/lib/commonjs/RulerLensTicks.js +55 -0
- package/lib/commonjs/RulerLensTicks.js.map +1 -0
- package/lib/commonjs/RulerLensTrack.js +52 -0
- package/lib/commonjs/RulerLensTrack.js.map +1 -0
- package/lib/commonjs/RulerRange.js +39 -0
- package/lib/commonjs/RulerRange.js.map +1 -0
- package/lib/commonjs/RulerRangeContext.js +17 -0
- package/lib/commonjs/RulerRangeContext.js.map +1 -0
- package/lib/commonjs/RulerRangeHeader.js +154 -0
- package/lib/commonjs/RulerRangeHeader.js.map +1 -0
- package/lib/commonjs/RulerRangeHighlight.js +50 -0
- package/lib/commonjs/RulerRangeHighlight.js.map +1 -0
- package/lib/commonjs/RulerRangeIndicator.js +71 -0
- package/lib/commonjs/RulerRangeIndicator.js.map +1 -0
- package/lib/commonjs/RulerRangeRoot.js +80 -0
- package/lib/commonjs/RulerRangeRoot.js.map +1 -0
- package/lib/commonjs/RulerRangeTicks.js +57 -0
- package/lib/commonjs/RulerRangeTicks.js.map +1 -0
- package/lib/commonjs/RulerRangeTrack.js +46 -0
- package/lib/commonjs/RulerRangeTrack.js.map +1 -0
- package/lib/commonjs/RulerResetButton.js +46 -0
- package/lib/commonjs/RulerResetButton.js.map +1 -0
- package/lib/commonjs/RulerRoot.js +87 -0
- package/lib/commonjs/RulerRoot.js.map +1 -0
- package/lib/commonjs/RulerTicks.js +51 -0
- package/lib/commonjs/RulerTicks.js.map +1 -0
- package/lib/commonjs/RulerTrack.js +46 -0
- package/lib/commonjs/RulerTrack.js.map +1 -0
- package/lib/commonjs/Tick.js +41 -36
- package/lib/commonjs/Tick.js.map +1 -1
- package/lib/commonjs/defaults.js +20 -19
- package/lib/commonjs/defaults.js.map +1 -1
- package/lib/commonjs/index.js +287 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/presets.js +108 -0
- package/lib/commonjs/presets.js.map +1 -0
- package/lib/commonjs/tickResolver.js +101 -0
- package/lib/commonjs/tickResolver.js.map +1 -0
- package/lib/commonjs/useHaptics.js +1 -1
- package/lib/commonjs/useHaptics.web.js +43 -0
- package/lib/commonjs/useHaptics.web.js.map +1 -0
- package/lib/commonjs/useRulerDatePicker.js +264 -0
- package/lib/commonjs/useRulerDatePicker.js.map +1 -0
- package/lib/commonjs/useRulerDateRangePicker.js +313 -0
- package/lib/commonjs/useRulerDateRangePicker.js.map +1 -0
- package/lib/commonjs/utils.js +61 -47
- package/lib/commonjs/utils.js.map +1 -1
- package/lib/module/LensTick.js +77 -0
- package/lib/module/LensTick.js.map +1 -0
- package/lib/module/Ruler.js +35 -0
- package/lib/module/Ruler.js.map +1 -0
- package/lib/module/RulerContext.js +12 -0
- package/lib/module/RulerContext.js.map +1 -0
- package/lib/module/RulerDatePicker.js +38 -350
- package/lib/module/RulerDatePicker.js.map +1 -1
- package/lib/module/RulerDateRangePicker.js +52 -0
- package/lib/module/RulerDateRangePicker.js.map +1 -0
- package/lib/module/RulerHeader.js +101 -0
- package/lib/module/RulerHeader.js.map +1 -0
- package/lib/module/RulerIndicator.js +62 -0
- package/lib/module/RulerIndicator.js.map +1 -0
- package/lib/module/RulerLensTicks.js +50 -0
- package/lib/module/RulerLensTicks.js.map +1 -0
- package/lib/module/RulerLensTrack.js +47 -0
- package/lib/module/RulerLensTrack.js.map +1 -0
- package/lib/module/RulerRange.js +36 -0
- package/lib/module/RulerRange.js.map +1 -0
- package/lib/module/RulerRangeContext.js +12 -0
- package/lib/module/RulerRangeContext.js.map +1 -0
- package/lib/module/RulerRangeHeader.js +149 -0
- package/lib/module/RulerRangeHeader.js.map +1 -0
- package/lib/module/RulerRangeHighlight.js +45 -0
- package/lib/module/RulerRangeHighlight.js.map +1 -0
- package/lib/module/RulerRangeIndicator.js +66 -0
- package/lib/module/RulerRangeIndicator.js.map +1 -0
- package/lib/module/RulerRangeRoot.js +75 -0
- package/lib/module/RulerRangeRoot.js.map +1 -0
- package/lib/module/RulerRangeTicks.js +51 -0
- package/lib/module/RulerRangeTicks.js.map +1 -0
- package/lib/module/RulerRangeTrack.js +41 -0
- package/lib/module/RulerRangeTrack.js.map +1 -0
- package/lib/module/RulerResetButton.js +41 -0
- package/lib/module/RulerResetButton.js.map +1 -0
- package/lib/module/RulerRoot.js +82 -0
- package/lib/module/RulerRoot.js.map +1 -0
- package/lib/module/RulerTicks.js +45 -0
- package/lib/module/RulerTicks.js.map +1 -0
- package/lib/module/RulerTrack.js +41 -0
- package/lib/module/RulerTrack.js.map +1 -0
- package/lib/module/Tick.js +41 -36
- package/lib/module/Tick.js.map +1 -1
- package/lib/module/defaults.js +19 -18
- package/lib/module/defaults.js.map +1 -1
- package/lib/module/index.js +46 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/presets.js +103 -0
- package/lib/module/presets.js.map +1 -0
- package/lib/module/tickResolver.js +97 -0
- package/lib/module/tickResolver.js.map +1 -0
- package/lib/module/useHaptics.js +1 -1
- package/lib/module/useHaptics.js.map +1 -1
- package/lib/module/useHaptics.web.js +39 -0
- package/lib/module/useHaptics.web.js.map +1 -0
- package/lib/module/useRulerDatePicker.js +260 -0
- package/lib/module/useRulerDatePicker.js.map +1 -0
- package/lib/module/useRulerDateRangePicker.js +310 -0
- package/lib/module/useRulerDateRangePicker.js.map +1 -0
- package/lib/module/utils.js +59 -45
- package/lib/module/utils.js.map +1 -1
- package/lib/typescript/LensTick.d.ts +20 -0
- package/lib/typescript/LensTick.d.ts.map +1 -0
- package/lib/typescript/Ruler.d.ts +31 -0
- package/lib/typescript/Ruler.d.ts.map +1 -0
- package/lib/typescript/RulerContext.d.ts +11 -0
- package/lib/typescript/RulerContext.d.ts.map +1 -0
- package/lib/typescript/RulerDatePicker.d.ts +10 -7
- package/lib/typescript/RulerDatePicker.d.ts.map +1 -1
- package/lib/typescript/RulerDateRangePicker.d.ts +26 -0
- package/lib/typescript/RulerDateRangePicker.d.ts.map +1 -0
- package/lib/typescript/RulerHeader.d.ts +14 -0
- package/lib/typescript/RulerHeader.d.ts.map +1 -0
- package/lib/typescript/RulerIndicator.d.ts +17 -0
- package/lib/typescript/RulerIndicator.d.ts.map +1 -0
- package/lib/typescript/RulerLensTicks.d.ts +12 -0
- package/lib/typescript/RulerLensTicks.d.ts.map +1 -0
- package/lib/typescript/RulerLensTrack.d.ts +13 -0
- package/lib/typescript/RulerLensTrack.d.ts.map +1 -0
- package/lib/typescript/RulerRange.d.ts +32 -0
- package/lib/typescript/RulerRange.d.ts.map +1 -0
- package/lib/typescript/RulerRangeContext.d.ts +11 -0
- package/lib/typescript/RulerRangeContext.d.ts.map +1 -0
- package/lib/typescript/RulerRangeHeader.d.ts +12 -0
- package/lib/typescript/RulerRangeHeader.d.ts.map +1 -0
- package/lib/typescript/RulerRangeHighlight.d.ts +8 -0
- package/lib/typescript/RulerRangeHighlight.d.ts.map +1 -0
- package/lib/typescript/RulerRangeIndicator.d.ts +17 -0
- package/lib/typescript/RulerRangeIndicator.d.ts.map +1 -0
- package/lib/typescript/RulerRangeRoot.d.ts +7 -0
- package/lib/typescript/RulerRangeRoot.d.ts.map +1 -0
- package/lib/typescript/RulerRangeTicks.d.ts +14 -0
- package/lib/typescript/RulerRangeTicks.d.ts.map +1 -0
- package/lib/typescript/RulerRangeTrack.d.ts +8 -0
- package/lib/typescript/RulerRangeTrack.d.ts.map +1 -0
- package/lib/typescript/RulerResetButton.d.ts +10 -0
- package/lib/typescript/RulerResetButton.d.ts.map +1 -0
- package/lib/typescript/RulerRoot.d.ts +7 -0
- package/lib/typescript/RulerRoot.d.ts.map +1 -0
- package/lib/typescript/RulerTicks.d.ts +9 -0
- package/lib/typescript/RulerTicks.d.ts.map +1 -0
- package/lib/typescript/RulerTrack.d.ts +8 -0
- package/lib/typescript/RulerTrack.d.ts.map +1 -0
- package/lib/typescript/Tick.d.ts +3 -16
- package/lib/typescript/Tick.d.ts.map +1 -1
- package/lib/typescript/defaults.d.ts +6 -9
- package/lib/typescript/defaults.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +37 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/presets.d.ts +27 -0
- package/lib/typescript/presets.d.ts.map +1 -0
- package/lib/typescript/tickResolver.d.ts +10 -0
- package/lib/typescript/tickResolver.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +302 -85
- package/lib/typescript/types.d.ts.map +1 -1
- package/lib/typescript/useHaptics.web.d.ts +17 -0
- package/lib/typescript/useHaptics.web.d.ts.map +1 -0
- package/lib/typescript/useRulerDatePicker.d.ts +26 -0
- package/lib/typescript/useRulerDatePicker.d.ts.map +1 -0
- package/lib/typescript/useRulerDateRangePicker.d.ts +40 -0
- package/lib/typescript/useRulerDateRangePicker.d.ts.map +1 -0
- package/lib/typescript/utils.d.ts +14 -12
- package/lib/typescript/utils.d.ts.map +1 -1
- package/package.json +25 -7
- package/src/LensTick.tsx +90 -0
- package/src/Ruler.ts +32 -0
- package/src/RulerContext.tsx +20 -0
- package/src/RulerDatePicker.tsx +41 -439
- package/src/RulerDateRangePicker.tsx +53 -0
- package/src/RulerHeader.tsx +125 -0
- package/src/RulerIndicator.tsx +84 -0
- package/src/RulerLensTicks.tsx +50 -0
- package/src/RulerLensTrack.tsx +45 -0
- package/src/RulerRange.ts +33 -0
- package/src/RulerRangeContext.tsx +20 -0
- package/src/RulerRangeHeader.tsx +180 -0
- package/src/RulerRangeHighlight.tsx +47 -0
- package/src/RulerRangeIndicator.tsx +85 -0
- package/src/RulerRangeRoot.tsx +87 -0
- package/src/RulerRangeTicks.tsx +68 -0
- package/src/RulerRangeTrack.tsx +39 -0
- package/src/RulerResetButton.tsx +59 -0
- package/src/RulerRoot.tsx +93 -0
- package/src/RulerTicks.tsx +57 -0
- package/src/RulerTrack.tsx +39 -0
- package/src/Tick.tsx +60 -49
- package/src/defaults.ts +21 -19
- package/src/index.ts +83 -1
- package/src/presets.ts +108 -0
- package/src/tickResolver.ts +123 -0
- package/src/types.ts +354 -102
- package/src/useHaptics.web.ts +55 -0
- package/src/useRulerDatePicker.ts +373 -0
- package/src/useRulerDateRangePicker.ts +468 -0
- package/src/utils.ts +67 -68
- package/lib/commonjs/package.json +0 -1
- package/lib/module/package.json +0 -1
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import { useState, useRef, useCallback, useMemo, useEffect } from 'react';
|
|
2
|
+
import { useWindowDimensions } from 'react-native';
|
|
3
|
+
import {
|
|
4
|
+
useSharedValue,
|
|
5
|
+
withDecay,
|
|
6
|
+
withTiming,
|
|
7
|
+
useAnimatedReaction,
|
|
8
|
+
runOnJS,
|
|
9
|
+
type SharedValue,
|
|
10
|
+
} from 'react-native-reanimated';
|
|
11
|
+
import { Gesture, type PanGesture } from 'react-native-gesture-handler';
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
RulerDateRangePickerProps,
|
|
15
|
+
ResolvedTick,
|
|
16
|
+
ResolvedRulerTheme,
|
|
17
|
+
DateRangeChangePayload,
|
|
18
|
+
} from './types';
|
|
19
|
+
import { DEFAULTS, MS_PER_DAY } from './defaults';
|
|
20
|
+
import { resolveTickRules } from './presets';
|
|
21
|
+
import { resolveTick } from './tickResolver';
|
|
22
|
+
import {
|
|
23
|
+
formatDateString,
|
|
24
|
+
normalizeDate,
|
|
25
|
+
dateToDayOffset,
|
|
26
|
+
dayOffsetToDate,
|
|
27
|
+
computeDayRange,
|
|
28
|
+
useResolvedTheme,
|
|
29
|
+
} from './utils';
|
|
30
|
+
import { useHaptics } from './useHaptics';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Return interface for the `useRulerDateRangePicker` headless hook.
|
|
34
|
+
*
|
|
35
|
+
* @experimental
|
|
36
|
+
*/
|
|
37
|
+
export interface UseRulerDateRangePickerReturn {
|
|
38
|
+
translateX: SharedValue<number>;
|
|
39
|
+
panGesture: PanGesture;
|
|
40
|
+
|
|
41
|
+
// Range State
|
|
42
|
+
startDate: Date;
|
|
43
|
+
endDate: Date;
|
|
44
|
+
startDateStr: string;
|
|
45
|
+
endDateStr: string;
|
|
46
|
+
startOffset: number;
|
|
47
|
+
endOffset: number;
|
|
48
|
+
spanDays: number;
|
|
49
|
+
activeHandle: 'start' | 'end';
|
|
50
|
+
setActiveHandle: (handle: 'start' | 'end') => void;
|
|
51
|
+
|
|
52
|
+
// Resolved ticks & layout
|
|
53
|
+
visibleTicks: ResolvedTick[];
|
|
54
|
+
theme: ResolvedRulerTheme;
|
|
55
|
+
tickSpacing: number;
|
|
56
|
+
rulerHeight: number;
|
|
57
|
+
height: number;
|
|
58
|
+
width: number;
|
|
59
|
+
minOffset: number;
|
|
60
|
+
maxOffset: number;
|
|
61
|
+
|
|
62
|
+
// Actions
|
|
63
|
+
scrollToHandle: (handle: 'start' | 'end', animated?: boolean) => void;
|
|
64
|
+
setRange: (start: Date | string, end: Date | string) => void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Headless Hook for managing Date Range selection with dual pins,
|
|
69
|
+
* range highlight calculations, and Reanimated 3 physics.
|
|
70
|
+
*
|
|
71
|
+
* @experimental This hook is experimental and subject to breaking changes
|
|
72
|
+
* in future minor versions.
|
|
73
|
+
*/
|
|
74
|
+
export function useRulerDateRangePicker(
|
|
75
|
+
props: RulerDateRangePickerProps = {}
|
|
76
|
+
): UseRulerDateRangePickerReturn {
|
|
77
|
+
const {
|
|
78
|
+
value,
|
|
79
|
+
defaultValue,
|
|
80
|
+
onChange,
|
|
81
|
+
minDate,
|
|
82
|
+
maxDate,
|
|
83
|
+
ticks = 'calendar',
|
|
84
|
+
tickSpacing = DEFAULTS.tickSpacing,
|
|
85
|
+
tickWidth = DEFAULTS.tickWidth,
|
|
86
|
+
rulerHeight = DEFAULTS.rulerHeight,
|
|
87
|
+
theme: propTheme,
|
|
88
|
+
debounceMs = DEFAULTS.debounceMs,
|
|
89
|
+
haptics = true,
|
|
90
|
+
minSpanDays = 1,
|
|
91
|
+
maxSpanDays,
|
|
92
|
+
style,
|
|
93
|
+
} = props;
|
|
94
|
+
|
|
95
|
+
const bufferTicks = DEFAULTS.bufferTicks;
|
|
96
|
+
const isHapticsEnabled = typeof haptics === 'boolean' ? haptics : (haptics?.enabled ?? true);
|
|
97
|
+
const resolvedHapticTrigger = typeof haptics === 'object' ? haptics.onTrigger : undefined;
|
|
98
|
+
const fastSwipeTickThreshold = (typeof haptics === 'object' ? haptics.fastSwipeTickThreshold : undefined) ?? DEFAULTS.fastSwipeTickThreshold;
|
|
99
|
+
const continuousHapticIntervalMs = (typeof haptics === 'object' ? haptics.continuousHapticIntervalMs : undefined) ?? DEFAULTS.continuousHapticIntervalMs;
|
|
100
|
+
const height = (style as any)?.height ?? DEFAULTS.height;
|
|
101
|
+
|
|
102
|
+
const theme = useResolvedTheme(propTheme);
|
|
103
|
+
const rules = useMemo(() => resolveTickRules(ticks), [ticks]);
|
|
104
|
+
|
|
105
|
+
const { fireDiscreteHaptic, startContinuousHaptic, stopContinuousHaptic } =
|
|
106
|
+
useHaptics({
|
|
107
|
+
enableHaptics: isHapticsEnabled,
|
|
108
|
+
onHapticTrigger: resolvedHapticTrigger,
|
|
109
|
+
continuousHapticIntervalMs,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const todayTime = useMemo(() => {
|
|
113
|
+
const d = new Date();
|
|
114
|
+
d.setHours(0, 0, 0, 0);
|
|
115
|
+
return d.getTime();
|
|
116
|
+
}, []);
|
|
117
|
+
|
|
118
|
+
const { minOffset, maxOffset } = useMemo(
|
|
119
|
+
() => computeDayRange(todayTime, minDate, maxDate),
|
|
120
|
+
[todayTime, minDate, maxDate]
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// ── Initial Dates ─────────────────────────────────────────────────────────
|
|
124
|
+
const initialStartDate = useMemo(() => {
|
|
125
|
+
if (value?.start) return normalizeDate(value.start);
|
|
126
|
+
if (defaultValue?.start) return normalizeDate(defaultValue.start);
|
|
127
|
+
return new Date(todayTime);
|
|
128
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
129
|
+
}, []);
|
|
130
|
+
|
|
131
|
+
const initialEndDate = useMemo(() => {
|
|
132
|
+
if (value?.end) return normalizeDate(value.end);
|
|
133
|
+
if (defaultValue?.end) return normalizeDate(defaultValue.end);
|
|
134
|
+
const end = new Date(todayTime);
|
|
135
|
+
end.setDate(end.getDate() + 7);
|
|
136
|
+
return end;
|
|
137
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
138
|
+
}, []);
|
|
139
|
+
|
|
140
|
+
const [activeHandle, setActiveHandle] = useState<'start' | 'end'>('start');
|
|
141
|
+
const [startDate, setStartDate] = useState<Date>(initialStartDate);
|
|
142
|
+
const [endDate, setEndDate] = useState<Date>(initialEndDate);
|
|
143
|
+
|
|
144
|
+
const startOffset = useMemo(
|
|
145
|
+
() => dateToDayOffset(startDate, todayTime),
|
|
146
|
+
[startDate, todayTime]
|
|
147
|
+
);
|
|
148
|
+
const endOffset = useMemo(
|
|
149
|
+
() => dateToDayOffset(endDate, todayTime),
|
|
150
|
+
[endDate, todayTime]
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const activeOffset = activeHandle === 'start' ? startOffset : endOffset;
|
|
154
|
+
|
|
155
|
+
// ── Shared Values ─────────────────────────────────────────────────────────
|
|
156
|
+
const translateX = useSharedValue(-activeOffset * tickSpacing);
|
|
157
|
+
const contextX = useSharedValue(0);
|
|
158
|
+
const previousIndex = useSharedValue(-activeOffset);
|
|
159
|
+
const isFastMode = useSharedValue(false);
|
|
160
|
+
|
|
161
|
+
const [windowCenter, setWindowCenter] = useState<number>(-activeOffset);
|
|
162
|
+
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
163
|
+
|
|
164
|
+
const windowDimensions = useWindowDimensions();
|
|
165
|
+
const width = props.width ?? windowDimensions.width;
|
|
166
|
+
|
|
167
|
+
const halfVisible = Math.ceil(width / 2 / tickSpacing);
|
|
168
|
+
const windowSize = halfVisible + bufferTicks;
|
|
169
|
+
|
|
170
|
+
// ── Visible Ticks Resolution ──────────────────────────────────────────────
|
|
171
|
+
const visibleTicks = useMemo(() => {
|
|
172
|
+
const resolved: ResolvedTick[] = [];
|
|
173
|
+
const start = Math.max(windowCenter - windowSize, minOffset);
|
|
174
|
+
const end = Math.min(windowCenter + windowSize, maxOffset);
|
|
175
|
+
|
|
176
|
+
for (let i = start; i <= end; i++) {
|
|
177
|
+
const d = dayOffsetToDate(i, todayTime);
|
|
178
|
+
const isToday = i === 0;
|
|
179
|
+
const isSelected = i === startOffset || i === endOffset;
|
|
180
|
+
const isInRange = i >= startOffset && i <= endOffset;
|
|
181
|
+
|
|
182
|
+
resolved.push(
|
|
183
|
+
resolveTick(
|
|
184
|
+
{
|
|
185
|
+
date: d,
|
|
186
|
+
offset: i,
|
|
187
|
+
isToday,
|
|
188
|
+
isSelected,
|
|
189
|
+
isInRange,
|
|
190
|
+
},
|
|
191
|
+
rules,
|
|
192
|
+
theme,
|
|
193
|
+
tickWidth
|
|
194
|
+
)
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
return resolved;
|
|
198
|
+
}, [
|
|
199
|
+
windowCenter,
|
|
200
|
+
windowSize,
|
|
201
|
+
minOffset,
|
|
202
|
+
maxOffset,
|
|
203
|
+
todayTime,
|
|
204
|
+
startOffset,
|
|
205
|
+
endOffset,
|
|
206
|
+
rules,
|
|
207
|
+
theme,
|
|
208
|
+
tickWidth,
|
|
209
|
+
]);
|
|
210
|
+
|
|
211
|
+
// ── Notify Change ─────────────────────────────────────────────────────────
|
|
212
|
+
const emitChange = useCallback(
|
|
213
|
+
(newStart: Date, newEnd: Date) => {
|
|
214
|
+
const spanDays = Math.round(
|
|
215
|
+
(newEnd.getTime() - newStart.getTime()) / (1000 * 60 * 60 * 24)
|
|
216
|
+
);
|
|
217
|
+
const payload: DateRangeChangePayload = {
|
|
218
|
+
start: newStart,
|
|
219
|
+
end: newEnd,
|
|
220
|
+
startDate: newStart,
|
|
221
|
+
endDate: newEnd,
|
|
222
|
+
startString: formatDateString(newStart),
|
|
223
|
+
endString: formatDateString(newEnd),
|
|
224
|
+
spanDays,
|
|
225
|
+
};
|
|
226
|
+
onChange?.(payload);
|
|
227
|
+
},
|
|
228
|
+
[onChange]
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// ── Tick Changed Callback ─────────────────────────────────────────────────
|
|
232
|
+
const onTickChanged = useCallback(
|
|
233
|
+
(index: number, enteringFastMode: boolean) => {
|
|
234
|
+
if (enteringFastMode) {
|
|
235
|
+
startContinuousHaptic();
|
|
236
|
+
} else {
|
|
237
|
+
stopContinuousHaptic();
|
|
238
|
+
fireDiscreteHaptic();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
setWindowCenter((prev) => {
|
|
242
|
+
if (Math.abs(index - prev) >= 5) {
|
|
243
|
+
return index;
|
|
244
|
+
}
|
|
245
|
+
return prev;
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const targetDate = dayOffsetToDate(index, todayTime);
|
|
249
|
+
|
|
250
|
+
if (activeHandle === 'start') {
|
|
251
|
+
const clampedIndex = Math.min(index, endOffset - minSpanDays);
|
|
252
|
+
const validStart = dayOffsetToDate(clampedIndex, todayTime);
|
|
253
|
+
setStartDate(validStart);
|
|
254
|
+
|
|
255
|
+
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
|
|
256
|
+
debounceTimerRef.current = setTimeout(() => {
|
|
257
|
+
emitChange(validStart, endDate);
|
|
258
|
+
}, debounceMs);
|
|
259
|
+
} else {
|
|
260
|
+
const clampedIndex = Math.max(index, startOffset + minSpanDays);
|
|
261
|
+
const validEnd = dayOffsetToDate(clampedIndex, todayTime);
|
|
262
|
+
setEndDate(validEnd);
|
|
263
|
+
|
|
264
|
+
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
|
|
265
|
+
debounceTimerRef.current = setTimeout(() => {
|
|
266
|
+
emitChange(startDate, validEnd);
|
|
267
|
+
}, debounceMs);
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
[
|
|
271
|
+
activeHandle,
|
|
272
|
+
endOffset,
|
|
273
|
+
startOffset,
|
|
274
|
+
minSpanDays,
|
|
275
|
+
todayTime,
|
|
276
|
+
endDate,
|
|
277
|
+
startDate,
|
|
278
|
+
emitChange,
|
|
279
|
+
debounceMs,
|
|
280
|
+
startContinuousHaptic,
|
|
281
|
+
stopContinuousHaptic,
|
|
282
|
+
fireDiscreteHaptic,
|
|
283
|
+
]
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
// ── Animation Settled Callback ────────────────────────────────────────────
|
|
287
|
+
const onAnimationSettled = useCallback(
|
|
288
|
+
(index: number) => {
|
|
289
|
+
stopContinuousHaptic();
|
|
290
|
+
fireDiscreteHaptic();
|
|
291
|
+
setWindowCenter(index);
|
|
292
|
+
|
|
293
|
+
if (activeHandle === 'start') {
|
|
294
|
+
const clampedIndex = Math.min(index, endOffset - minSpanDays);
|
|
295
|
+
const validStart = dayOffsetToDate(clampedIndex, todayTime);
|
|
296
|
+
setStartDate(validStart);
|
|
297
|
+
emitChange(validStart, endDate);
|
|
298
|
+
} else {
|
|
299
|
+
const clampedIndex = Math.max(index, startOffset + minSpanDays);
|
|
300
|
+
const validEnd = dayOffsetToDate(clampedIndex, todayTime);
|
|
301
|
+
setEndDate(validEnd);
|
|
302
|
+
emitChange(startDate, validEnd);
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
[
|
|
306
|
+
activeHandle,
|
|
307
|
+
endOffset,
|
|
308
|
+
startOffset,
|
|
309
|
+
minSpanDays,
|
|
310
|
+
todayTime,
|
|
311
|
+
endDate,
|
|
312
|
+
startDate,
|
|
313
|
+
emitChange,
|
|
314
|
+
stopContinuousHaptic,
|
|
315
|
+
fireDiscreteHaptic,
|
|
316
|
+
]
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
// ── Animated Reaction ─────────────────────────────────────────────────────
|
|
320
|
+
useAnimatedReaction(
|
|
321
|
+
() => Math.round(-translateX.value / tickSpacing),
|
|
322
|
+
(index, prev) => {
|
|
323
|
+
if (index !== prev && index >= minOffset && index <= maxOffset) {
|
|
324
|
+
previousIndex.value = index;
|
|
325
|
+
const ticksSkipped = prev !== null ? Math.abs(index - prev) : 1;
|
|
326
|
+
const shouldBeFast = ticksSkipped > fastSwipeTickThreshold;
|
|
327
|
+
|
|
328
|
+
if (shouldBeFast && !isFastMode.value) {
|
|
329
|
+
isFastMode.value = true;
|
|
330
|
+
runOnJS(onTickChanged)(index, true);
|
|
331
|
+
} else if (!shouldBeFast && isFastMode.value) {
|
|
332
|
+
isFastMode.value = false;
|
|
333
|
+
runOnJS(onTickChanged)(index, false);
|
|
334
|
+
} else if (!shouldBeFast) {
|
|
335
|
+
runOnJS(onTickChanged)(index, false);
|
|
336
|
+
}
|
|
337
|
+
if (shouldBeFast && isFastMode.value) {
|
|
338
|
+
runOnJS(onTickChanged)(index, true);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
[tickSpacing, minOffset, maxOffset, fastSwipeTickThreshold]
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
// ── Pan Gesture ───────────────────────────────────────────────────────────
|
|
346
|
+
const panGesture = useMemo(() => {
|
|
347
|
+
return Gesture.Pan()
|
|
348
|
+
.onStart(() => {
|
|
349
|
+
contextX.value = translateX.value;
|
|
350
|
+
})
|
|
351
|
+
.onUpdate((event) => {
|
|
352
|
+
translateX.value = contextX.value + event.translationX;
|
|
353
|
+
})
|
|
354
|
+
.onEnd((event) => {
|
|
355
|
+
translateX.value = withDecay(
|
|
356
|
+
{
|
|
357
|
+
velocity: event.velocityX,
|
|
358
|
+
deceleration: 0.99,
|
|
359
|
+
},
|
|
360
|
+
(finished) => {
|
|
361
|
+
if (finished) {
|
|
362
|
+
const maxTranslateX = -minOffset * tickSpacing;
|
|
363
|
+
const minTranslateX = -maxOffset * tickSpacing;
|
|
364
|
+
const clampedX = Math.min(
|
|
365
|
+
Math.max(translateX.value, minTranslateX),
|
|
366
|
+
maxTranslateX
|
|
367
|
+
);
|
|
368
|
+
const snappedIndex = Math.round(clampedX / tickSpacing);
|
|
369
|
+
const snappedX = snappedIndex * tickSpacing;
|
|
370
|
+
|
|
371
|
+
translateX.value = withTiming(
|
|
372
|
+
snappedX,
|
|
373
|
+
{ duration: 150 },
|
|
374
|
+
(done) => {
|
|
375
|
+
if (done) {
|
|
376
|
+
isFastMode.value = false;
|
|
377
|
+
const index = Math.round(-snappedX / tickSpacing);
|
|
378
|
+
runOnJS(onAnimationSettled)(index);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
);
|
|
385
|
+
});
|
|
386
|
+
}, [contextX, translateX, minOffset, maxOffset, tickSpacing, onAnimationSettled, isFastMode]);
|
|
387
|
+
|
|
388
|
+
// ── Switch Active Handle ──────────────────────────────────────────────────
|
|
389
|
+
const scrollToHandle = useCallback(
|
|
390
|
+
(handle: 'start' | 'end', animated: boolean = true) => {
|
|
391
|
+
setActiveHandle(handle);
|
|
392
|
+
const targetOffset = handle === 'start' ? startOffset : endOffset;
|
|
393
|
+
const targetX = -targetOffset * tickSpacing;
|
|
394
|
+
|
|
395
|
+
stopContinuousHaptic();
|
|
396
|
+
if (animated) {
|
|
397
|
+
translateX.value = withTiming(targetX, { duration: 250 }, (done) => {
|
|
398
|
+
if (done) {
|
|
399
|
+
runOnJS(setWindowCenter)(targetOffset);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
} else {
|
|
403
|
+
translateX.value = targetX;
|
|
404
|
+
setWindowCenter(targetOffset);
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
[startOffset, endOffset, tickSpacing, translateX, stopContinuousHaptic]
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
const setRange = useCallback(
|
|
411
|
+
(startInput: Date | string, endInput: Date | string) => {
|
|
412
|
+
const s = normalizeDate(startInput);
|
|
413
|
+
const e = normalizeDate(endInput);
|
|
414
|
+
setStartDate(s);
|
|
415
|
+
setEndDate(e);
|
|
416
|
+
emitChange(s, e);
|
|
417
|
+
},
|
|
418
|
+
[emitChange]
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
// Sync external controlled range
|
|
422
|
+
const prevRangePropRef = useRef(value);
|
|
423
|
+
useEffect(() => {
|
|
424
|
+
if (
|
|
425
|
+
value?.start &&
|
|
426
|
+
value?.end &&
|
|
427
|
+
(value.start !== prevRangePropRef.current?.start ||
|
|
428
|
+
value.end !== prevRangePropRef.current?.end)
|
|
429
|
+
) {
|
|
430
|
+
prevRangePropRef.current = value;
|
|
431
|
+
const s = normalizeDate(value.start);
|
|
432
|
+
const e = normalizeDate(value.end);
|
|
433
|
+
if (
|
|
434
|
+
formatDateString(s) !== formatDateString(startDate) ||
|
|
435
|
+
formatDateString(e) !== formatDateString(endDate)
|
|
436
|
+
) {
|
|
437
|
+
setStartDate(s);
|
|
438
|
+
setEndDate(e);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}, [value, startDate, endDate]);
|
|
442
|
+
|
|
443
|
+
const spanDays = Math.max(0, endOffset - startOffset);
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
translateX,
|
|
447
|
+
panGesture,
|
|
448
|
+
startDate,
|
|
449
|
+
endDate,
|
|
450
|
+
startDateStr: formatDateString(startDate),
|
|
451
|
+
endDateStr: formatDateString(endDate),
|
|
452
|
+
startOffset,
|
|
453
|
+
endOffset,
|
|
454
|
+
spanDays,
|
|
455
|
+
activeHandle,
|
|
456
|
+
setActiveHandle,
|
|
457
|
+
visibleTicks,
|
|
458
|
+
theme,
|
|
459
|
+
tickSpacing,
|
|
460
|
+
rulerHeight,
|
|
461
|
+
height,
|
|
462
|
+
width,
|
|
463
|
+
minOffset,
|
|
464
|
+
maxOffset,
|
|
465
|
+
scrollToHandle,
|
|
466
|
+
setRange,
|
|
467
|
+
};
|
|
468
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useColorScheme } from 'react-native';
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
2
|
+
import { LIGHT_THEME, DARK_THEME, MS_PER_DAY, DEFAULT_DAY_RANGE } from './defaults';
|
|
3
|
+
import type { RulerTheme, ResolvedRulerTheme } from './types';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Formats a Date object as a local YYYY-MM-DD string,
|
|
@@ -13,6 +13,37 @@ export function formatDateString(date: Date = new Date()): string {
|
|
|
13
13
|
return `${year}-${month}-${day}`;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Normalizes a Date or string to a local midnight Date object.
|
|
18
|
+
* Parses YYYY-MM-DD explicitly as local midnight to avoid UTC timezone shifts,
|
|
19
|
+
* and guards against Invalid Dates.
|
|
20
|
+
*/
|
|
21
|
+
export function normalizeDate(input?: Date | string): Date {
|
|
22
|
+
let d: Date;
|
|
23
|
+
if (!input) {
|
|
24
|
+
d = new Date();
|
|
25
|
+
} else if (typeof input === 'string') {
|
|
26
|
+
const parts = input.trim().split('-');
|
|
27
|
+
if (parts.length === 3) {
|
|
28
|
+
const year = parseInt(parts[0], 10);
|
|
29
|
+
const month = parseInt(parts[1], 10) - 1;
|
|
30
|
+
const day = parseInt(parts[2], 10);
|
|
31
|
+
d = new Date(year, month, day);
|
|
32
|
+
} else {
|
|
33
|
+
d = new Date(input);
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
d = new Date(input.getTime());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (isNaN(d.getTime())) {
|
|
40
|
+
d = new Date();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
d.setHours(0, 0, 0, 0);
|
|
44
|
+
return d;
|
|
45
|
+
}
|
|
46
|
+
|
|
16
47
|
/**
|
|
17
48
|
* Converts a Date or YYYY-MM-DD string to a day offset relative to a reference time.
|
|
18
49
|
*/
|
|
@@ -20,72 +51,19 @@ export function dateToDayOffset(
|
|
|
20
51
|
date: Date | string,
|
|
21
52
|
referenceTime: number
|
|
22
53
|
): number {
|
|
23
|
-
const d =
|
|
24
|
-
d.setHours(0, 0, 0, 0);
|
|
54
|
+
const d = normalizeDate(date);
|
|
25
55
|
return Math.round((d.getTime() - referenceTime) / MS_PER_DAY);
|
|
26
56
|
}
|
|
27
57
|
|
|
28
58
|
/**
|
|
29
|
-
* Converts a day offset back to a Date object.
|
|
59
|
+
* Converts a day offset back to a Date object at local midnight.
|
|
60
|
+
* Uses native calendar math (setDate) to avoid Daylight Saving Time (DST) 23h/25h hour drift.
|
|
30
61
|
*/
|
|
31
62
|
export function dayOffsetToDate(offset: number, referenceTime: number): Date {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
* Resolves user-provided color props against the system color scheme defaults.
|
|
37
|
-
* User-provided values always take precedence.
|
|
38
|
-
*/
|
|
39
|
-
export function useResolvedColors(
|
|
40
|
-
props: Pick<
|
|
41
|
-
RulerDatePickerProps,
|
|
42
|
-
| 'tickColor'
|
|
43
|
-
| 'tickTallColor'
|
|
44
|
-
| 'labelColor'
|
|
45
|
-
| 'accentColor'
|
|
46
|
-
| 'indicatorColor'
|
|
47
|
-
| 'backgroundColor'
|
|
48
|
-
>
|
|
49
|
-
): ResolvedColors {
|
|
50
|
-
const scheme = useColorScheme();
|
|
51
|
-
const palette = scheme === 'dark' ? DARK_COLORS : LIGHT_COLORS;
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
tickColor: props.tickColor ?? palette.tickColor,
|
|
55
|
-
tickTallColor: props.tickTallColor ?? props.tickColor ?? palette.tickTallColor,
|
|
56
|
-
labelColor: props.labelColor ?? palette.labelColor,
|
|
57
|
-
accentColor: props.accentColor ?? palette.accentColor,
|
|
58
|
-
indicatorColor: props.indicatorColor ?? palette.indicatorColor,
|
|
59
|
-
backgroundColor: props.backgroundColor ?? palette.backgroundColor,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Determines whether a date should display a label based on the labelInterval setting.
|
|
65
|
-
*/
|
|
66
|
-
export function shouldShowLabel(
|
|
67
|
-
date: Date,
|
|
68
|
-
labelInterval: RulerDatePickerProps['labelInterval'],
|
|
69
|
-
todayTime: number
|
|
70
|
-
): boolean {
|
|
71
|
-
if (!labelInterval || labelInterval === 'weekly') {
|
|
72
|
-
// Default: every 7th day relative to today (matches original Friday-style spacing)
|
|
73
|
-
const dayOffset = Math.round(
|
|
74
|
-
(date.getTime() - todayTime) / MS_PER_DAY
|
|
75
|
-
);
|
|
76
|
-
return dayOffset % 7 === 0;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (labelInterval === 'daily') {
|
|
80
|
-
return true;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (labelInterval === 'monthly') {
|
|
84
|
-
return date.getDate() === 1;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Custom callback
|
|
88
|
-
return labelInterval(date);
|
|
63
|
+
const d = new Date(referenceTime);
|
|
64
|
+
d.setDate(d.getDate() + offset);
|
|
65
|
+
d.setHours(0, 0, 0, 0);
|
|
66
|
+
return d;
|
|
89
67
|
}
|
|
90
68
|
|
|
91
69
|
/**
|
|
@@ -93,23 +71,44 @@ export function shouldShowLabel(
|
|
|
93
71
|
*/
|
|
94
72
|
export function computeDayRange(
|
|
95
73
|
todayTime: number,
|
|
96
|
-
minDate?: Date,
|
|
97
|
-
maxDate?: Date
|
|
74
|
+
minDate?: Date | string,
|
|
75
|
+
maxDate?: Date | string
|
|
98
76
|
): { minOffset: number; maxOffset: number } {
|
|
99
77
|
let minOffset = -DEFAULT_DAY_RANGE;
|
|
100
78
|
let maxOffset = DEFAULT_DAY_RANGE;
|
|
101
79
|
|
|
102
80
|
if (minDate) {
|
|
103
|
-
const d =
|
|
104
|
-
d.setHours(0, 0, 0, 0);
|
|
81
|
+
const d = normalizeDate(minDate);
|
|
105
82
|
minOffset = Math.round((d.getTime() - todayTime) / MS_PER_DAY);
|
|
106
83
|
}
|
|
107
84
|
|
|
108
85
|
if (maxDate) {
|
|
109
|
-
const d =
|
|
110
|
-
d.setHours(0, 0, 0, 0);
|
|
86
|
+
const d = normalizeDate(maxDate);
|
|
111
87
|
maxOffset = Math.round((d.getTime() - todayTime) / MS_PER_DAY);
|
|
112
88
|
}
|
|
113
89
|
|
|
90
|
+
if (minOffset > maxOffset) {
|
|
91
|
+
const temp = minOffset;
|
|
92
|
+
minOffset = maxOffset;
|
|
93
|
+
maxOffset = temp;
|
|
94
|
+
}
|
|
95
|
+
|
|
114
96
|
return { minOffset, maxOffset };
|
|
115
97
|
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Resolves user-provided theme against the system color scheme defaults (Light/Dark).
|
|
101
|
+
*/
|
|
102
|
+
export function useResolvedTheme(theme?: RulerTheme): ResolvedRulerTheme {
|
|
103
|
+
const scheme = useColorScheme();
|
|
104
|
+
const palette = scheme === 'dark' ? DARK_THEME : LIGHT_THEME;
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
background: theme?.background ?? palette.background,
|
|
108
|
+
indicatorColor: theme?.indicatorColor ?? palette.indicatorColor,
|
|
109
|
+
accentColor: theme?.accentColor ?? palette.accentColor,
|
|
110
|
+
labelColor: theme?.labelColor ?? palette.labelColor,
|
|
111
|
+
defaultTickColor: theme?.defaultTickColor ?? palette.defaultTickColor,
|
|
112
|
+
rangeHighlightColor: theme?.rangeHighlightColor ?? palette.rangeHighlightColor,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"type":"commonjs"}
|
package/lib/module/package.json
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"type":"module"}
|