Skip to content

Commit c76509b

Browse files
author
openclaw
committed
feat: dock and highlight pomodoro widget
1 parent 2bc8c0d commit c76509b

1 file changed

Lines changed: 58 additions & 6 deletions

File tree

src/app/components/PomodoroFloatingWidget.tsx

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type WidgetPosition = {
2525
const POSITION_KEY = 'recall_pomodoro_widget_position';
2626
const COLLAPSED_KEY = 'recall_pomodoro_widget_collapsed';
2727
const DEFAULT_OFFSET = 16;
28+
const EDGE_PEEK = 18;
2829

2930
const readStoredPosition = (): WidgetPosition | null => {
3031
if (typeof window === 'undefined') return null;
@@ -49,14 +50,24 @@ const clampPosition = (position: WidgetPosition, width: number, height: number)
4950
};
5051
};
5152

53+
const getDockedX = (x: number, width: number) => {
54+
if (typeof window === 'undefined') return x;
55+
const leftDistance = x;
56+
const rightDistance = window.innerWidth - (x + width);
57+
return leftDistance <= rightDistance ? DEFAULT_OFFSET : Math.max(DEFAULT_OFFSET, window.innerWidth - width - DEFAULT_OFFSET);
58+
};
59+
5260
export default function PomodoroFloatingWidget({ onOpenPomodoro }: PomodoroFloatingWidgetProps) {
5361
const [tick, setTick] = useState(Date.now());
5462
const [dismissed, setDismissed] = useState(false);
5563
const [collapsed, setCollapsed] = useState(false);
5664
const [position, setPosition] = useState<WidgetPosition | null>(null);
5765
const [isDragging, setIsDragging] = useState(false);
66+
const [isEdgeDocked, setIsEdgeDocked] = useState(false);
67+
const [flashActive, setFlashActive] = useState(false);
5868
const widgetRef = useRef<HTMLDivElement | null>(null);
5969
const dragRef = useRef({ startX: 0, startY: 0, originX: 0, originY: 0 });
70+
const previousRemainingRef = useRef<number | null>(null);
6071

6172
useEffect(() => {
6273
if (typeof window === 'undefined') return;
@@ -125,13 +136,16 @@ export default function PomodoroFloatingWidget({ onOpenPomodoro }: PomodoroFloat
125136
x: window.innerWidth - currentRect.width - DEFAULT_OFFSET,
126137
y: window.innerHeight - currentRect.height - DEFAULT_OFFSET,
127138
};
128-
return clampPosition(fallback, currentRect.width, currentRect.height);
139+
const clamped = clampPosition(fallback, currentRect.width, currentRect.height);
140+
return isEdgeDocked
141+
? { ...clamped, x: getDockedX(clamped.x, currentRect.width) }
142+
: clamped;
129143
});
130144
};
131145

132146
window.addEventListener('resize', handleResize);
133147
return () => window.removeEventListener('resize', handleResize);
134-
}, [position, collapsed, tick]);
148+
}, [position, collapsed, tick, isEdgeDocked]);
135149

136150
useEffect(() => {
137151
if (typeof window === 'undefined' || !position) return;
@@ -148,6 +162,16 @@ export default function PomodoroFloatingWidget({ onOpenPomodoro }: PomodoroFloat
148162
return getResolvedPomodoroState(base, tick);
149163
}, [tick]);
150164

165+
useEffect(() => {
166+
const previousRemaining = previousRemainingRef.current;
167+
if (previousRemaining !== null && previousRemaining > 0 && state.remaining === 0) {
168+
setFlashActive(true);
169+
const timer = window.setTimeout(() => setFlashActive(false), 2200);
170+
return () => window.clearTimeout(timer);
171+
}
172+
previousRemainingRef.current = state.remaining;
173+
}, [state.remaining]);
174+
151175
useEffect(() => {
152176
if (!isDragging) return;
153177

@@ -163,13 +187,23 @@ export default function PomodoroFloatingWidget({ onOpenPomodoro }: PomodoroFloat
163187
rect.height,
164188
);
165189
setPosition(next);
190+
setIsEdgeDocked(false);
191+
};
192+
193+
const stopDragging = () => {
194+
if (widgetRef.current && position) {
195+
const rect = widgetRef.current.getBoundingClientRect();
196+
const dockedX = getDockedX(position.x, rect.width);
197+
setPosition((prev) => (prev ? { ...prev, x: dockedX } : prev));
198+
setIsEdgeDocked(true);
199+
}
200+
setIsDragging(false);
166201
};
167202

168203
const onMouseMove = (event: MouseEvent) => handleMove(event.clientX, event.clientY);
169204
const onTouchMove = (event: TouchEvent) => {
170205
if (event.touches[0]) handleMove(event.touches[0].clientX, event.touches[0].clientY);
171206
};
172-
const stopDragging = () => setIsDragging(false);
173207

174208
window.addEventListener('mousemove', onMouseMove);
175209
window.addEventListener('mouseup', stopDragging);
@@ -181,7 +215,7 @@ export default function PomodoroFloatingWidget({ onOpenPomodoro }: PomodoroFloat
181215
window.removeEventListener('touchmove', onTouchMove);
182216
window.removeEventListener('touchend', stopDragging);
183217
};
184-
}, [isDragging]);
218+
}, [isDragging, position]);
185219

186220
if (!state.hasActiveSession || dismissed || !position) return null;
187221

@@ -224,11 +258,29 @@ export default function PomodoroFloatingWidget({ onOpenPomodoro }: PomodoroFloat
224258
setIsDragging(true);
225259
};
226260

261+
const collapsedEdgeStyle = collapsed && isEdgeDocked && widgetRef.current
262+
? position.x <= window.innerWidth / 2
263+
? { transform: `translateX(-${Math.max(0, widgetRef.current.getBoundingClientRect().width - EDGE_PEEK)}px)` }
264+
: { transform: `translateX(${Math.max(0, widgetRef.current.getBoundingClientRect().width - EDGE_PEEK)}px)` }
265+
: undefined;
266+
227267
return (
228268
<div
229269
ref={widgetRef}
230-
className={`fixed z-[65] select-none rounded-2xl border border-[rgba(var(--theme-accent),0.26)] bg-[rgba(19,22,28,0.94)] shadow-[0_18px_40px_rgba(0,0,0,0.32)] backdrop-blur-xl ${collapsed ? 'w-auto p-2.5' : 'w-[min(88vw,320px)] p-3'}`}
231-
style={{ left: position.x, top: position.y }}
270+
className={`fixed z-[65] select-none rounded-2xl border backdrop-blur-xl transition-[transform,opacity,box-shadow,border-color,background-color] duration-300 ${
271+
flashActive
272+
? 'border-amber-300/80 bg-[rgba(120,80,10,0.92)] shadow-[0_0_0_1px_rgba(251,191,36,0.35),0_0_32px_rgba(251,191,36,0.35)]'
273+
: collapsed && isEdgeDocked
274+
? 'border-[rgba(var(--theme-accent),0.18)] bg-[rgba(19,22,28,0.52)] shadow-[0_12px_28px_rgba(0,0,0,0.22)] hover:bg-[rgba(19,22,28,0.82)] hover:border-[rgba(var(--theme-accent),0.28)] hover:translate-x-0'
275+
: 'border-[rgba(var(--theme-accent),0.26)] bg-[rgba(19,22,28,0.94)] shadow-[0_18px_40px_rgba(0,0,0,0.32)]'
276+
} ${collapsed ? 'w-auto p-2.5' : 'w-[min(88vw,320px)] p-3'}`}
277+
style={{ left: position.x, top: position.y, ...(collapsedEdgeStyle ?? {}) }}
278+
onMouseEnter={() => {
279+
if (collapsed && isEdgeDocked) setIsEdgeDocked(false);
280+
}}
281+
onMouseLeave={() => {
282+
if (collapsed) setIsEdgeDocked(true);
283+
}}
232284
>
233285
<div className="flex items-start justify-between gap-3">
234286
<button

0 commit comments

Comments
 (0)