33 AUTO_COMPACTION_THRESHOLD_MIN ,
44 AUTO_COMPACTION_THRESHOLD_MAX ,
55} from "@/common/constants/ui" ;
6- import { TooltipWrapper , Tooltip } from "../Tooltip" ;
76
87// ----- Types -----
98
@@ -14,51 +13,44 @@ export interface AutoCompactionConfig {
1413 setThreshold : ( threshold : number ) => void ;
1514}
1615
17- type Orientation = "horizontal" | "vertical" ;
18-
19- interface ThresholdSliderProps {
16+ interface HorizontalThresholdSliderProps {
2017 config : AutoCompactionConfig ;
21- orientation : Orientation ;
2218}
2319
2420// ----- Constants -----
2521
26- /** Threshold at which we consider auto-compaction disabled (dragged all the way right/down ) */
22+ /** Threshold at which we consider auto-compaction disabled (dragged all the way right) */
2723const DISABLE_THRESHOLD = 100 ;
2824
29- // ----- Hook: useDraggableThreshold -----
25+ // ----- Main component: HorizontalThresholdSlider -----
3026
31- interface DragState {
32- isDragging : boolean ;
33- dragValue : number | null ;
34- }
27+ /**
28+ * A draggable threshold indicator for horizontal progress bars.
29+ *
30+ * Renders as a vertical line with triangle handles at the threshold position.
31+ * Drag left/right to adjust threshold. Drag to 100% to disable.
32+ *
33+ * USAGE: Place as a sibling AFTER the progress bar, both inside a relative container.
34+ */
35+ export const HorizontalThresholdSlider : React . FC < HorizontalThresholdSliderProps > = ( { config } ) => {
36+ const containerRef = useRef < HTMLDivElement > ( null ) ;
37+ const [ isDragging , setIsDragging ] = useState ( false ) ;
38+ const [ dragValue , setDragValue ] = useState < number | null > ( null ) ;
3539
36- function useDraggableThreshold (
37- containerRef : React . RefObject < HTMLDivElement > ,
38- config : AutoCompactionConfig ,
39- orientation : Orientation
40- ) {
41- const [ dragState , setDragState ] = useState < DragState > ( {
42- isDragging : false ,
43- dragValue : null ,
44- } ) ;
40+ // Current display position
41+ const position = dragValue ?? ( config . enabled ? config . threshold : DISABLE_THRESHOLD ) ;
4542
4643 const calculatePercentage = useCallback (
47- ( clientX : number , clientY : number ) : number => {
44+ ( clientX : number ) : number => {
4845 const container = containerRef . current ;
4946 if ( ! container ) return config . threshold ;
5047
5148 const rect = container . getBoundingClientRect ( ) ;
52- const raw =
53- orientation === "horizontal"
54- ? ( ( clientX - rect . left ) / rect . width ) * 100
55- : ( ( clientY - rect . top ) / rect . height ) * 100 ;
56-
57- // Clamp and round to nearest 5
49+ const raw = ( ( clientX - rect . left ) / rect . width ) * 100 ;
5850 const clamped = Math . max ( AUTO_COMPACTION_THRESHOLD_MIN , Math . min ( 100 , raw ) ) ;
5951 return Math . round ( clamped / 5 ) * 5 ;
6052 } ,
61- [ containerRef , orientation , config . threshold ]
53+ [ config . threshold ]
6254 ) ;
6355
6456 const applyThreshold = useCallback (
@@ -78,18 +70,20 @@ function useDraggableThreshold(
7870 e . preventDefault ( ) ;
7971 e . stopPropagation ( ) ;
8072
81- const percentage = calculatePercentage ( e . clientX , e . clientY ) ;
82- setDragState ( { isDragging : true , dragValue : percentage } ) ;
73+ const percentage = calculatePercentage ( e . clientX ) ;
74+ setIsDragging ( true ) ;
75+ setDragValue ( percentage ) ;
8376 applyThreshold ( percentage ) ;
8477
8578 const handleMouseMove = ( moveEvent : MouseEvent ) => {
86- const newPercentage = calculatePercentage ( moveEvent . clientX , moveEvent . clientY ) ;
87- setDragState ( { isDragging : true , dragValue : newPercentage } ) ;
79+ const newPercentage = calculatePercentage ( moveEvent . clientX ) ;
80+ setDragValue ( newPercentage ) ;
8881 applyThreshold ( newPercentage ) ;
8982 } ;
9083
9184 const handleMouseUp = ( ) => {
92- setDragState ( { isDragging : false , dragValue : null } ) ;
85+ setIsDragging ( false ) ;
86+ setDragValue ( null ) ;
9387 document . removeEventListener ( "mousemove" , handleMouseMove ) ;
9488 document . removeEventListener ( "mouseup" , handleMouseUp ) ;
9589 } ;
@@ -100,90 +94,26 @@ function useDraggableThreshold(
10094 [ calculatePercentage , applyThreshold ]
10195 ) ;
10296
103- return { ...dragState , handleMouseDown } ;
104- }
105-
106- // ----- Helper: compute display position -----
107-
108- function computePosition ( config : AutoCompactionConfig , dragValue : number | null ) : number {
109- if ( dragValue !== null ) return dragValue ;
110- return config . enabled ? config . threshold : DISABLE_THRESHOLD ;
111- }
112-
113- // ----- Helper: tooltip text -----
114-
115- function getTooltipText (
116- config : AutoCompactionConfig ,
117- isDragging : boolean ,
118- dragValue : number | null ,
119- orientation : Orientation
120- ) : string {
121- if ( isDragging && dragValue !== null ) {
122- return dragValue >= DISABLE_THRESHOLD
97+ // Tooltip text
98+ const title = isDragging
99+ ? dragValue !== null && dragValue >= DISABLE_THRESHOLD
123100 ? "Release to disable auto-compact"
124- : `Auto-compact at ${ dragValue } %` ;
125- }
126- const direction = orientation === "horizontal" ? "left" : "up" ;
127- return config . enabled
128- ? `Auto-compact at ${ config . threshold } % · Drag to adjust`
129- : `Auto-compact disabled · Drag ${ direction } to enable` ;
130- }
101+ : `Auto-compact at ${ dragValue } %`
102+ : config . enabled
103+ ? `Auto-compact at ${ config . threshold } % · Drag to adjust`
104+ : "Auto-compact disabled · Drag left to enable" ;
131105
132- // ----- Sub-components: Triangle indicators -----
133-
134- interface TriangleProps {
135- direction : "up" | "down" | "left" | "right" ;
136- color : string ;
137- opacity : number ;
138- }
139-
140- const Triangle : React . FC < TriangleProps > = ( { direction, color, opacity } ) => {
141- const size = 4 ;
142- const tipSize = 5 ;
143-
144- const styles : Record < TriangleProps [ "direction" ] , React . CSSProperties > = {
145- up : {
146- borderLeft : `${ size } px solid transparent` ,
147- borderRight : `${ size } px solid transparent` ,
148- borderBottom : `${ tipSize } px solid ${ color } ` ,
149- } ,
150- down : {
151- borderLeft : `${ size } px solid transparent` ,
152- borderRight : `${ size } px solid transparent` ,
153- borderTop : `${ tipSize } px solid ${ color } ` ,
154- } ,
155- left : {
156- borderTop : `${ size } px solid transparent` ,
157- borderBottom : `${ size } px solid transparent` ,
158- borderRight : `${ tipSize } px solid ${ color } ` ,
159- } ,
160- right : {
161- borderTop : `${ size } px solid transparent` ,
162- borderBottom : `${ size } px solid transparent` ,
163- borderLeft : `${ tipSize } px solid ${ color } ` ,
164- } ,
165- } ;
166-
167- return < div style = { { width : 0 , height : 0 , opacity, ...styles [ direction ] } } /> ;
168- } ;
169-
170- // ----- Sub-component: ThresholdIndicator -----
171-
172- interface ThresholdIndicatorProps {
173- position : number ;
174- color : string ;
175- opacity : number ;
176- orientation : Orientation ;
177- }
106+ const lineColor = config . enabled ? "var(--color-plan-mode)" : "var(--color-muted)" ;
107+ const opacity = isDragging ? 1 : 0.8 ;
178108
179- const ThresholdIndicator : React . FC < ThresholdIndicatorProps > = ( {
180- position ,
181- color ,
182- opacity ,
183- orientation ,
184- } ) => {
185- if ( orientation === "horizontal" ) {
186- return (
109+ return (
110+ < div
111+ ref = { containerRef }
112+ className = "absolute inset-0 z-10 cursor-ew-resize"
113+ onMouseDown = { handleMouseDown }
114+ title = { title }
115+ >
116+ { /* Vertical line with triangle handles */ }
187117 < div
188118 className = "pointer-events-none absolute flex flex-col items-center"
189119 style = { {
@@ -193,74 +123,31 @@ const ThresholdIndicator: React.FC<ThresholdIndicatorProps> = ({
193123 transform : "translateX(-50%)" ,
194124 } }
195125 >
196- < Triangle direction = "down" color = { color } opacity = { opacity } />
197- < div className = "flex-1" style = { { width : 2 , background : color , opacity } } />
198- < Triangle direction = "up" color = { color } opacity = { opacity } />
199- </ div >
200- ) ;
201- }
202-
203- // Vertical
204- return (
205- < div
206- className = "pointer-events-none absolute flex items-center"
207- style = { {
208- top : `${ position } %` ,
209- left : - 4 ,
210- right : - 4 ,
211- transform : "translateY(-50%)" ,
212- } }
213- >
214- < Triangle direction = "right" color = { color } opacity = { opacity } />
215- < div className = "flex-1" style = { { height : 2 , background : color , opacity } } />
216- < Triangle direction = "left" color = { color } opacity = { opacity } />
217- </ div >
218- ) ;
219- } ;
220-
221- // ----- Main component -----
222-
223- /**
224- * ThresholdSlider renders an interactive threshold indicator overlay.
225- *
226- * IMPORTANT: This component must be placed inside a container with:
227- * - `position: relative` (for absolute positioning)
228- * - `overflow: visible` (so triangles can extend beyond bounds)
229- *
230- * The slider fills its container via `inset-0` and positions the indicator
231- * line at the threshold percentage.
232- */
233- export const ThresholdSlider : React . FC < ThresholdSliderProps > = ( { config, orientation } ) => {
234- const containerRef = useRef < HTMLDivElement > ( null ) ;
235- const { isDragging, dragValue, handleMouseDown } = useDraggableThreshold (
236- containerRef ,
237- config ,
238- orientation
239- ) ;
240-
241- const position = computePosition ( config , dragValue ) ;
242- const lineColor = config . enabled ? "var(--color-plan-mode)" : "var(--color-muted)" ;
243- const opacity = isDragging ? 1 : 0.8 ;
244- const tooltipText = getTooltipText ( config , isDragging , dragValue , orientation ) ;
245- const cursor = orientation === "horizontal" ? "cursor-ew-resize" : "cursor-ns-resize" ;
246-
247- return (
248- < TooltipWrapper >
249- < div
250- ref = { containerRef }
251- className = { `absolute inset-0 z-10 ${ cursor } ` }
252- onMouseDown = { handleMouseDown }
253- >
254- < ThresholdIndicator
255- position = { position }
256- color = { lineColor }
257- opacity = { opacity }
258- orientation = { orientation }
126+ { /* Top triangle (pointing down) */ }
127+ < div
128+ style = { {
129+ width : 0 ,
130+ height : 0 ,
131+ borderLeft : "4px solid transparent" ,
132+ borderRight : "4px solid transparent" ,
133+ borderTop : `5px solid ${ lineColor } ` ,
134+ opacity,
135+ } }
136+ />
137+ { /* Line */ }
138+ < div style = { { flex : 1 , width : 2 , background : lineColor , opacity } } />
139+ { /* Bottom triangle (pointing up) */ }
140+ < div
141+ style = { {
142+ width : 0 ,
143+ height : 0 ,
144+ borderLeft : "4px solid transparent" ,
145+ borderRight : "4px solid transparent" ,
146+ borderBottom : `5px solid ${ lineColor } ` ,
147+ opacity,
148+ } }
259149 />
260150 </ div >
261- < Tooltip align = "center" width = "auto" >
262- { tooltipText }
263- </ Tooltip >
264- </ TooltipWrapper >
151+ </ div >
265152 ) ;
266153} ;
0 commit comments