@@ -36,63 +36,78 @@ const sliderIcons = [
36
36
37
37
/* ──────── Slider ──────── */
38
38
function 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
41
43
42
- /* -------- pointer handlers -------- */
43
44
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
+ }
53
53
} ;
54
54
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
59
62
} ;
60
63
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
+ } ;
62
77
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 ) ;
65
83
66
84
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 ) ;
69
90
} ;
70
- } , [ dragging , min , max , step , onChange , value ] ) ;
91
+ } , [ isDragging , min , max , step , value , onChange ] ) ;
71
92
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
75
95
76
96
return (
77
97
< 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
+ } }
79
103
>
80
- { /* TRACK */ }
81
104
< div
82
- ref = { trackRef }
105
+ ref = { sliderRef }
83
106
style = { {
84
107
position : 'relative' ,
85
108
width : '100%' ,
86
109
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
96
111
} }
97
112
>
98
113
< div
@@ -120,6 +135,11 @@ function FancySlider({ min, max, step, value, onChange, icons }) {
120
135
} }
121
136
/>
122
137
< div
138
+ onMouseDown = { ( e ) => {
139
+ e . preventDefault ( ) ;
140
+ setIsDragging ( true ) ;
141
+ } }
142
+ onTouchStart = { ( ) => setIsDragging ( true ) }
123
143
style = { {
124
144
position : 'absolute' ,
125
145
top : '50%' ,
@@ -135,40 +155,38 @@ function FancySlider({ min, max, step, value, onChange, icons }) {
135
155
/>
136
156
</ div >
137
157
138
- { /* ICONS */ }
158
+ { /* icon row – absolutely identical coordinates */ }
139
159
< div
140
160
style = { {
141
161
display : 'flex' ,
142
162
justifyContent : 'space-between' ,
143
163
width : '100%' ,
144
164
position : 'absolute' ,
145
165
top : 0 ,
146
- pointerEvents : 'none' , // icons themselves don’t intercept drag
147
166
} }
148
167
>
149
- { icons . map ( ( { icon : Icon , value : v , key } ) => {
168
+ { icons . map ( ( { icon : IconComponent , value : v , key } ) => {
150
169
const iconRatio = ( v - min ) / ( max - min ) ;
151
- const active = v === value ;
170
+ const isActive = v === value ;
152
171
return (
153
172
< div
154
173
key = { key }
155
- onPointerDown = { ( e ) => { // allow tap-to-jump
156
- e . preventDefault ( ) ;
157
- onChange ( v ) ;
158
- } }
174
+ onClick = { ( ) => onChange ( v ) }
159
175
style = { {
160
176
position : 'absolute' ,
161
177
left : `calc(${ iconRatio * 100 } % - ${ iconSize / 2 } px)` ,
162
178
top : '50%' ,
163
179
transform : 'translateY(-50%)' ,
164
- fontSize : iconSize ,
165
- color : active ? '#d6ceba' : 'rgba(214,206,186,.5)' ,
166
180
cursor : 'pointer' ,
167
- pointerEvents : 'auto' ,
181
+ zIndex : 1 ,
182
+ color : isActive
183
+ ? '#d6ceba'
184
+ : 'rgba(214,206,186,.5)' ,
185
+ fontSize : `${ iconSize } px` ,
168
186
} }
169
187
title = { key . charAt ( 0 ) . toUpperCase ( ) + key . slice ( 1 ) }
170
188
>
171
- < Icon />
189
+ < IconComponent />
172
190
</ div >
173
191
) ;
174
192
} ) }
@@ -179,7 +197,7 @@ function FancySlider({ min, max, step, value, onChange, icons }) {
179
197
180
198
/* ──────── Left Pane ──────── */
181
199
const LeftPane = ( { selectedHour, onTimeChange, activity, gif } ) => {
182
- const gifSrc = gifMap [ gif ] || inboxclipGif ; // fallback
200
+ const gifSrc = gifMap [ gif ] || inboxclipGif ; // fallback gif
183
201
184
202
return (
185
203
< div className = "leftpane-container" >
@@ -203,14 +221,16 @@ const LeftPane = ({ selectedHour, onTimeChange, activity, gif }) => {
203
221
204
222
{ /* Hour selector */ }
205
223
< 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 >
214
234
</ div >
215
235
</ div >
216
236
) ;
0 commit comments