@@ -11,13 +11,14 @@ export interface AutoCompactionConfig {
1111 setThreshold : ( threshold : number ) => void ;
1212}
1313
14- interface HorizontalThresholdSliderProps {
14+ interface ThresholdSliderProps {
1515 config : AutoCompactionConfig ;
16+ orientation : "horizontal" | "vertical" ;
1617}
1718
1819// ----- Constants -----
1920
20- /** Threshold at which we consider auto-compaction disabled (dragged all the way right ) */
21+ /** Threshold at which we consider auto-compaction disabled (dragged all the way to end ) */
2122const DISABLE_THRESHOLD = 100 ;
2223
2324/** Size of the triangle markers in pixels */
@@ -26,27 +27,65 @@ const TRIANGLE_SIZE = 4;
2627// ----- Subcomponents -----
2728
2829/** CSS triangle pointing in specified direction */
29- const Triangle : React . FC < { direction : "up" | "down" ; color : string } > = ( { direction, color } ) => (
30- < div
31- style = { {
32- width : 0 ,
33- height : 0 ,
34- borderLeft : `${ TRIANGLE_SIZE } px solid transparent` ,
35- borderRight : `${ TRIANGLE_SIZE } px solid transparent` ,
36- ...( direction === "down"
37- ? { borderTop : `${ TRIANGLE_SIZE } px solid ${ color } ` }
38- : { borderBottom : `${ TRIANGLE_SIZE } px solid ${ color } ` } ) ,
39- } }
40- />
41- ) ;
30+ const Triangle : React . FC < { direction : "up" | "down" | "left" | "right" ; color : string } > = ( {
31+ direction,
32+ color,
33+ } ) => {
34+ const styles : React . CSSProperties = { width : 0 , height : 0 } ;
35+
36+ if ( direction === "up" || direction === "down" ) {
37+ styles . borderLeft = `${ TRIANGLE_SIZE } px solid transparent` ;
38+ styles . borderRight = `${ TRIANGLE_SIZE } px solid transparent` ;
39+ if ( direction === "down" ) {
40+ styles . borderTop = `${ TRIANGLE_SIZE } px solid ${ color } ` ;
41+ } else {
42+ styles . borderBottom = `${ TRIANGLE_SIZE } px solid ${ color } ` ;
43+ }
44+ } else {
45+ styles . borderTop = `${ TRIANGLE_SIZE } px solid transparent` ;
46+ styles . borderBottom = `${ TRIANGLE_SIZE } px solid transparent` ;
47+ if ( direction === "right" ) {
48+ styles . borderLeft = `${ TRIANGLE_SIZE } px solid ${ color } ` ;
49+ } else {
50+ styles . borderRight = `${ TRIANGLE_SIZE } px solid ${ color } ` ;
51+ }
52+ }
53+
54+ return < div style = { styles } /> ;
55+ } ;
56+
57+ // ----- Shared utilities -----
4258
43- // ----- Main component: HorizontalThresholdSlider -----
59+ /** Clamp and snap percentage to valid threshold values */
60+ const snapPercent = ( raw : number ) : number => {
61+ const clamped = Math . max ( AUTO_COMPACTION_THRESHOLD_MIN , Math . min ( 100 , raw ) ) ;
62+ return Math . round ( clamped / 5 ) * 5 ;
63+ } ;
64+
65+ /** Apply threshold, handling the disable case */
66+ const applyThreshold = ( pct : number , setThreshold : ( v : number ) => void ) : void => {
67+ setThreshold ( pct >= DISABLE_THRESHOLD ? 100 : Math . min ( pct , AUTO_COMPACTION_THRESHOLD_MAX ) ) ;
68+ } ;
69+
70+ /** Get tooltip text based on threshold */
71+ const getTooltip = ( threshold : number , orientation : "horizontal" | "vertical" ) : string => {
72+ const isEnabled = threshold < DISABLE_THRESHOLD ;
73+ const direction = orientation === "horizontal" ? "left" : "up" ;
74+ return isEnabled
75+ ? `Auto-compact at ${ threshold } % · Drag to adjust (per-model)`
76+ : `Auto-compact disabled · Drag ${ direction } to enable (per-model)` ;
77+ } ;
78+
79+ // ----- Main component: ThresholdSlider -----
4480
4581/**
46- * A draggable threshold indicator for horizontal progress bars.
82+ * A draggable threshold indicator for progress bars (horizontal or vertical) .
4783 *
48- * Renders as a vertical line with triangle handles at the threshold position.
49- * Drag left/right to adjust threshold. Drag to 100% to disable.
84+ * - Horizontal: Renders as a vertical line with up/down triangle handles.
85+ * Drag left/right to adjust threshold. Drag to 100% (right) to disable.
86+ *
87+ * - Vertical: Renders as a horizontal line with left/right triangle handles.
88+ * Drag up/down to adjust threshold. Drag to 100% (bottom) to disable.
5089 *
5190 * USAGE: Place as a sibling AFTER the progress bar, both inside a relative container.
5291 *
@@ -57,30 +96,30 @@ const Triangle: React.FC<{ direction: "up" | "down"; color: string }> = ({ direc
5796 * how Tailwind's JIT compiler or class application interacts with dynamically
5897 * rendered components in this context. Inline styles work reliably.
5998 */
60- export const HorizontalThresholdSlider : React . FC < HorizontalThresholdSliderProps > = ( { config } ) => {
99+ export const ThresholdSlider : React . FC < ThresholdSliderProps > = ( { config, orientation } ) => {
61100 const containerRef = useRef < HTMLDivElement > ( null ) ;
101+ const isHorizontal = orientation === "horizontal" ;
62102
63103 const handleMouseDown = ( e : React . MouseEvent ) => {
64104 e . preventDefault ( ) ;
65105
66106 const rect = containerRef . current ?. getBoundingClientRect ( ) ;
67107 if ( ! rect ) return ;
68108
69- const calcPercent = ( clientX : number ) => {
70- const raw = ( ( clientX - rect . left ) / rect . width ) * 100 ;
71- const clamped = Math . max ( AUTO_COMPACTION_THRESHOLD_MIN , Math . min ( 100 , raw ) ) ;
72- return Math . round ( clamped / 5 ) * 5 ;
109+ const calcPercent = ( clientX : number , clientY : number ) => {
110+ if ( isHorizontal ) {
111+ return snapPercent ( ( ( clientX - rect . left ) / rect . width ) * 100 ) ;
112+ } else {
113+ // Vertical: top = low %, bottom = high %
114+ return snapPercent ( ( ( clientY - rect . top ) / rect . height ) * 100 ) ;
115+ }
73116 } ;
74117
75- const applyThreshold = ( pct : number ) => {
76- config . setThreshold (
77- pct >= DISABLE_THRESHOLD ? 100 : Math . min ( pct , AUTO_COMPACTION_THRESHOLD_MAX )
78- ) ;
79- } ;
118+ const apply = ( pct : number ) => applyThreshold ( pct , config . setThreshold ) ;
80119
81- applyThreshold ( calcPercent ( e . clientX ) ) ;
120+ apply ( calcPercent ( e . clientX , e . clientY ) ) ;
82121
83- const onMove = ( ev : MouseEvent ) => applyThreshold ( calcPercent ( ev . clientX ) ) ;
122+ const onMove = ( ev : MouseEvent ) => apply ( calcPercent ( ev . clientX , ev . clientY ) ) ;
84123 const onUp = ( ) => {
85124 document . removeEventListener ( "mousemove" , onMove ) ;
86125 document . removeEventListener ( "mouseup" , onUp ) ;
@@ -91,42 +130,64 @@ export const HorizontalThresholdSlider: React.FC<HorizontalThresholdSliderProps>
91130
92131 const isEnabled = config . threshold < DISABLE_THRESHOLD ;
93132 const color = isEnabled ? "var(--color-plan-mode)" : "var(--color-muted)" ;
94- const title = isEnabled
95- ? `Auto-compact at ${ config . threshold } % · Drag to adjust (per-model)`
96- : "Auto-compact disabled · Drag left to enable (per-model)" ;
133+ const title = getTooltip ( config . threshold , orientation ) ;
134+
135+ // Container styles
136+ const containerStyle : React . CSSProperties = {
137+ position : "absolute" ,
138+ cursor : isHorizontal ? "ew-resize" : "ns-resize" ,
139+ top : 0 ,
140+ bottom : 0 ,
141+ left : 0 ,
142+ right : 0 ,
143+ zIndex : 50 ,
144+ } ;
97145
98- return (
99- < div
100- ref = { containerRef }
101- style = { {
102- position : "absolute" ,
103- cursor : "ew-resize" ,
104- top : 0 ,
105- bottom : 0 ,
106- left : 0 ,
107- right : 0 ,
108- zIndex : 50 ,
109- } }
110- onMouseDown = { handleMouseDown }
111- title = { title }
112- >
113- { /* Indicator: top triangle + line + bottom triangle, centered on threshold */ }
114- < div
115- style = { {
116- position : "absolute" ,
146+ // Indicator positioning - use transform for centering on both axes
147+ const indicatorStyle : React . CSSProperties = {
148+ position : "absolute" ,
149+ pointerEvents : "none" ,
150+ display : "flex" ,
151+ alignItems : "center" ,
152+ ...( isHorizontal
153+ ? {
117154 left : `${ config . threshold } %` ,
118- top : `calc(50% - ${ TRIANGLE_SIZE + 3 } px)` ,
119- transform : "translateX(-50%)" ,
120- pointerEvents : "none" ,
121- display : "flex" ,
155+ top : "50%" ,
156+ transform : "translate(-50%, -50%)" ,
122157 flexDirection : "column" ,
123- alignItems : "center" ,
124- } }
125- >
126- < Triangle direction = "down" color = { color } />
127- < div style = { { width : 1 , height : 6 , background : color } } />
128- < Triangle direction = "up" color = { color } />
158+ }
159+ : {
160+ top : `${ config . threshold } %` ,
161+ left : "50%" ,
162+ transform : "translate(-50%, -50%)" ,
163+ flexDirection : "row" ,
164+ } ) ,
165+ } ;
166+
167+ // Line between triangles
168+ const lineStyle : React . CSSProperties = isHorizontal
169+ ? { width : 1 , height : 6 , background : color }
170+ : { width : 6 , height : 1 , background : color } ;
171+
172+ return (
173+ < div ref = { containerRef } style = { containerStyle } onMouseDown = { handleMouseDown } title = { title } >
174+ < div style = { indicatorStyle } >
175+ < Triangle direction = { isHorizontal ? "down" : "right" } color = { color } />
176+ < div style = { lineStyle } />
177+ < Triangle direction = { isHorizontal ? "up" : "left" } color = { color } />
129178 </ div >
130179 </ div >
131180 ) ;
132181} ;
182+
183+ // ----- Convenience exports -----
184+
185+ /** Horizontal threshold slider (alias for backwards compatibility) */
186+ export const HorizontalThresholdSlider : React . FC < { config : AutoCompactionConfig } > = ( {
187+ config,
188+ } ) => < ThresholdSlider config = { config } orientation = "horizontal" /> ;
189+
190+ /** Vertical threshold slider */
191+ export const VerticalThresholdSlider : React . FC < { config : AutoCompactionConfig } > = ( { config } ) => (
192+ < ThresholdSlider config = { config } orientation = "vertical" />
193+ ) ;
0 commit comments