@@ -36,63 +36,78 @@ const sliderIcons = [
3636
3737/* ──────── Slider ──────── */
3838function FancySlider ( { min, max, step, value, onChange, icons } ) {
39- const trackRef = useRef ( null ) ;
40- const [ dragging , setDragging ] = useState ( false ) ;
39+ const sliderRef = React . useRef ( null ) ;
40+ const [ isDragging , setIsDragging ] = useState ( false ) ;
41+ const lastUpdateRef = useRef ( 0 ) ; // throttle redraws
42+ const THROTTLE_MS = 120 ; // ~8 fps is plenty
4143
42- /* -------- pointer handlers -------- */
4344 useEffect ( ( ) => {
44- const track = trackRef . current ;
45- if ( ! track ) return ;
46-
47- const move = ( clientX ) => {
48- const { left, width } = track . getBoundingClientRect ( ) ;
49- const clamped = Math . max ( 0 , Math . min ( clientX - left , width ) ) ;
50- const ratio = clamped / width ;
51- const newVal = Math . round ( ( min + ratio * ( max - min ) ) / step ) * step ;
52- if ( newVal !== value ) onChange ( newVal ) ;
45+ /* ─── helper that *may* trigger parent update ─── */
46+ const maybeUpdate = ( newVal , force = false ) => {
47+ if ( newVal === value ) return ; // no change – skip
48+ const now = Date . now ( ) ;
49+ if ( force || now - lastUpdateRef . current > THROTTLE_MS ) {
50+ lastUpdateRef . current = now ;
51+ onChange ( newVal ) ;
52+ }
5353 } ;
5454
55- const handlePointerMove = ( e ) => {
56- if ( ! dragging ) return ;
57- e . preventDefault ( ) ;
58- move ( e . clientX ) ;
55+ const handleMove = ( clientX ) => {
56+ if ( ! isDragging || ! sliderRef . current ) return ;
57+ const { left, width } = sliderRef . current . getBoundingClientRect ( ) ;
58+ const clampedX = Math . max ( 0 , Math . min ( clientX - left , width ) ) ;
59+ const ratio = clampedX / width ;
60+ const newValue = Math . round ( ( min + ratio * ( max - min ) ) / step ) * step ;
61+ maybeUpdate ( newValue ) ; // throttled
5962 } ;
6063
61- const handlePointerUp = ( ) => setDragging ( false ) ;
64+ const mouse = ( e ) => handleMove ( e . clientX ) ;
65+ const touch = ( e ) => {
66+ if ( isDragging ) e . preventDefault ( ) ; // block pull-to-refresh
67+ if ( e . touches [ 0 ] ) handleMove ( e . touches [ 0 ] . clientX ) ;
68+ } ;
69+ const endDrag = ( e ) => {
70+ if ( ! sliderRef . current ) return ;
71+ /* ensure *one* final update with the exact position */
72+ const finalX = e . changedTouches ?. [ 0 ] ?. clientX ?? e . clientX ;
73+ handleMove ( finalX ) ;
74+ maybeUpdate ( value , true /*force*/ ) ;
75+ setIsDragging ( false ) ;
76+ } ;
6277
63- window . addEventListener ( 'pointermove' , handlePointerMove ) ;
64- window . addEventListener ( 'pointerup' , handlePointerUp ) ;
78+ window . addEventListener ( 'mousemove' , mouse ) ;
79+ window . addEventListener ( 'mouseup' , endDrag ) ;
80+ window . addEventListener ( 'touchmove' , touch , { passive : false } ) ;
81+ window . addEventListener ( 'touchend' , endDrag ) ;
82+ window . addEventListener ( 'touchcancel' , endDrag ) ;
6583
6684 return ( ) => {
67- window . removeEventListener ( 'pointermove' , handlePointerMove ) ;
68- window . removeEventListener ( 'pointerup' , handlePointerUp ) ;
85+ window . removeEventListener ( 'mousemove' , mouse ) ;
86+ window . removeEventListener ( 'mouseup' , endDrag ) ;
87+ window . removeEventListener ( 'touchmove' , touch , { passive : false } ) ;
88+ window . removeEventListener ( 'touchend' , endDrag ) ;
89+ window . removeEventListener ( 'touchcancel' , endDrag ) ;
6990 } ;
70- } , [ dragging , min , max , step , onChange , value ] ) ;
91+ } , [ isDragging , min , max , step , value , onChange ] ) ;
7192
72- /* -------- slider visuals -------- */
73- const ratio = ( value - min ) / ( max - min ) ;
74- const iconSize = 20 ;
93+ const ratio = ( value - min ) / ( max - min ) ;
94+ const iconSize = 20 ; // icon sizing unchanged
7595
7696 return (
7797 < div
78- style = { { position : 'relative' , width : '100%' , height : 40 } }
98+ style = { {
99+ position : 'relative' ,
100+ width : '100%' ,
101+ height : 40 , // leave room for icons (unchanged)
102+ } }
79103 >
80- { /* TRACK */ }
81104 < div
82- ref = { trackRef }
105+ ref = { sliderRef }
83106 style = { {
84107 position : 'relative' ,
85108 width : '100%' ,
86109 height : 20 ,
87- marginTop : 15 ,
88- touchAction : 'none' , // disable browser gestures
89- overscrollBehaviorY : 'contain'
90- } }
91- onPointerDown = { ( e ) => {
92- setDragging ( true ) ;
93- e . target . setPointerCapture ( e . pointerId ) ;
94- e . preventDefault ( ) ;
95- move ( e . clientX ) ;
110+ marginTop : 15 , // same 15 px offset as original
96111 } }
97112 >
98113 < div
@@ -120,6 +135,11 @@ function FancySlider({ min, max, step, value, onChange, icons }) {
120135 } }
121136 />
122137 < div
138+ onMouseDown = { ( e ) => {
139+ e . preventDefault ( ) ;
140+ setIsDragging ( true ) ;
141+ } }
142+ onTouchStart = { ( ) => setIsDragging ( true ) }
123143 style = { {
124144 position : 'absolute' ,
125145 top : '50%' ,
@@ -135,40 +155,38 @@ function FancySlider({ min, max, step, value, onChange, icons }) {
135155 />
136156 </ div >
137157
138- { /* ICONS */ }
158+ { /* icon row – absolutely identical coordinates */ }
139159 < div
140160 style = { {
141161 display : 'flex' ,
142162 justifyContent : 'space-between' ,
143163 width : '100%' ,
144164 position : 'absolute' ,
145165 top : 0 ,
146- pointerEvents : 'none' , // icons themselves don’t intercept drag
147166 } }
148167 >
149- { icons . map ( ( { icon : Icon , value : v , key } ) => {
168+ { icons . map ( ( { icon : IconComponent , value : v , key } ) => {
150169 const iconRatio = ( v - min ) / ( max - min ) ;
151- const active = v === value ;
170+ const isActive = v === value ;
152171 return (
153172 < div
154173 key = { key }
155- onPointerDown = { ( e ) => { // allow tap-to-jump
156- e . preventDefault ( ) ;
157- onChange ( v ) ;
158- } }
174+ onClick = { ( ) => onChange ( v ) }
159175 style = { {
160176 position : 'absolute' ,
161177 left : `calc(${ iconRatio * 100 } % - ${ iconSize / 2 } px)` ,
162178 top : '50%' ,
163179 transform : 'translateY(-50%)' ,
164- fontSize : iconSize ,
165- color : active ? '#d6ceba' : 'rgba(214,206,186,.5)' ,
166180 cursor : 'pointer' ,
167- pointerEvents : 'auto' ,
181+ zIndex : 1 ,
182+ color : isActive
183+ ? '#d6ceba'
184+ : 'rgba(214,206,186,.5)' ,
185+ fontSize : `${ iconSize } px` ,
168186 } }
169187 title = { key . charAt ( 0 ) . toUpperCase ( ) + key . slice ( 1 ) }
170188 >
171- < Icon />
189+ < IconComponent />
172190 </ div >
173191 ) ;
174192 } ) }
@@ -179,7 +197,7 @@ function FancySlider({ min, max, step, value, onChange, icons }) {
179197
180198/* ──────── Left Pane ──────── */
181199const LeftPane = ( { selectedHour, onTimeChange, activity, gif } ) => {
182- const gifSrc = gifMap [ gif ] || inboxclipGif ; // fallback
200+ const gifSrc = gifMap [ gif ] || inboxclipGif ; // fallback gif
183201
184202 return (
185203 < div className = "leftpane-container" >
@@ -203,14 +221,16 @@ const LeftPane = ({ selectedHour, onTimeChange, activity, gif }) => {
203221
204222 { /* Hour selector */ }
205223 < div style = { { width : 200 , margin : '0 auto' } } >
206- < FancySlider
207- min = { 1 }
208- max = { 6 }
209- step = { 1 }
210- value = { selectedHour }
211- onChange = { onTimeChange }
212- icons = { sliderIcons }
213- />
224+ < div style = { { display : 'flex' , alignItems : 'center' , gap : 20 } } >
225+ < FancySlider
226+ min = { 1 }
227+ max = { 6 }
228+ step = { 1 }
229+ value = { selectedHour }
230+ onChange = { onTimeChange }
231+ icons = { sliderIcons }
232+ />
233+ </ div >
214234 </ div >
215235 </ div >
216236 ) ;
0 commit comments