Skip to content

Commit 13c12b1

Browse files
committed
fix: simplify to HorizontalThresholdSlider only
- Remove TooltipWrapper (was breaking absolute positioning) - Use native title attribute for tooltip - Export only HorizontalThresholdSlider for now - Vertical slider disabled until horizontal is confirmed working
1 parent a759e86 commit 13c12b1

File tree

3 files changed

+107
-235
lines changed

3 files changed

+107
-235
lines changed

src/browser/components/RightSidebar/CostsTab.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { useProviderOptions } from "@/browser/hooks/useProviderOptions";
88
import { supports1MContext } from "@/common/utils/ai/models";
99
import { TOKEN_COMPONENT_COLORS } from "@/common/utils/tokens/tokenMeterUtils";
1010
import { ConsumerBreakdown } from "./ConsumerBreakdown";
11-
import { ThresholdSlider } from "./ThresholdSlider";
11+
import { HorizontalThresholdSlider } from "./ThresholdSlider";
1212
import { useAutoCompactionSettings } from "@/browser/hooks/useAutoCompactionSettings";
1313

1414
// Format token display - show k for thousands with 1 decimal
@@ -234,14 +234,13 @@ const CostsTabComponent: React.FC<CostsTabProps> = ({ workspaceId }) => {
234234
)}
235235
{/* Threshold slider overlay - inside bar for proper positioning */}
236236
{maxTokens && (
237-
<ThresholdSlider
237+
<HorizontalThresholdSlider
238238
config={{
239239
enabled: autoCompactEnabled,
240240
threshold: autoCompactThreshold,
241241
setEnabled: setAutoCompactEnabled,
242242
setThreshold: setAutoCompactThreshold,
243243
}}
244-
orientation="horizontal"
245244
/>
246245
)}
247246
</div>

src/browser/components/RightSidebar/ThresholdSlider.tsx

Lines changed: 68 additions & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
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) */
2723
const 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

Comments
 (0)