Skip to content

Commit 32421a5

Browse files
committed
feat: enable auto-blur feature for widgets and enhance widget window styling
1 parent 25b22b4 commit 32421a5

6 files changed

Lines changed: 74 additions & 1 deletion

File tree

src-tauri/src/commands/widget_cmd.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,7 @@ pub fn build_widget_window_sync(app: &AppHandle, config: &WidgetConfig) -> Resul
460460
.inner_size(width, height)
461461
.position(x, y)
462462
.decorations(false)
463+
.transparent(true)
463464
.always_on_top(config.always_on_top_mode == "always")
464465
.skip_taskbar(false)
465466
.resizable(true)

src/styles/globals.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ body {
7979
overflow: hidden;
8080
}
8181

82+
html.widget-window,
83+
html.widget-window body {
84+
background: transparent;
85+
background-image: none;
86+
}
87+
8288
.widget-root > * {
8389
background-image:
8490
linear-gradient(rgba(30, 36, 51, var(--timelens-widget-overlay, 0.62)), rgba(30, 36, 51, var(--timelens-widget-overlay, 0.62))),

src/widgets/NoteWidget.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,9 @@ export default function NoteWidget({ widgetId }: Props) {
142142
} catch {
143143
// ignore
144144
}
145+
window.dispatchEvent(new CustomEvent("timelens-widget-auto-blur-changed", {
146+
detail: { widgetId, enabled: next },
147+
}));
145148
};
146149

147150
const selectedNote = useMemo(

src/widgets/TodoWidget.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,9 @@ export default function TodoWidget({ widgetId }: Props) {
199199
} catch {
200200
// ignore
201201
}
202+
window.dispatchEvent(new CustomEvent("timelens-widget-auto-blur-changed", {
203+
detail: { widgetId, enabled: next },
204+
}));
202205
};
203206

204207
const sensors = useSensors(

src/widgets/WidgetWindow.tsx

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,19 @@ export default function WidgetWindow({ widgetId }: Props) {
3434
const [isBlurred, setIsBlurred] = useState(false);
3535
const [idle, setIdle] = useState(false);
3636
const idleTimer = useRef<ReturnType<typeof setTimeout>>();
37+
const autoBlurEnabled = useRef(false);
38+
const rootRef = useRef<HTMLDivElement>(null);
3739
const [widgetType, setWidgetType] = useState<string>(
3840
widgetId.includes("-") ? widgetId.substring(0, widgetId.lastIndexOf("-")) : ""
3941
);
4042
const [refreshKey, setRefreshKey] = useState(0);
4143
const [paused, setPaused] = useState(false);
4244

45+
useEffect(() => {
46+
document.documentElement.classList.add("widget-window");
47+
return () => document.documentElement.classList.remove("widget-window");
48+
}, []);
49+
4350
const getMonitorIndexForRect = async (x: number, y: number, width: number, height: number) => {
4451
try {
4552
const target = await monitorFromPoint(x + width / 2, y + height / 2);
@@ -57,6 +64,20 @@ export default function WidgetWindow({ widgetId }: Props) {
5764
};
5865

5966
useEffect(() => {
67+
const autoBlurKey = `${widgetId}-auto-blur`;
68+
autoBlurEnabled.current = localStorage.getItem(autoBlurKey) === "1";
69+
const onAutoBlurChanged = (event: Event) => {
70+
const { widgetId: changedWidgetId, enabled } = (event as CustomEvent<{
71+
widgetId?: string;
72+
enabled?: boolean;
73+
}>).detail ?? {};
74+
if (changedWidgetId !== widgetId) return;
75+
autoBlurEnabled.current = enabled === true;
76+
clearTimeout(idleTimer.current);
77+
if (!autoBlurEnabled.current) setIdle(false);
78+
};
79+
window.addEventListener("timelens-widget-auto-blur-changed", onAutoBlurChanged);
80+
6081
const heartbeatKey = `timelens-widget-heartbeat:${widgetId}`;
6182
const recordHeartbeat = (event: string) => {
6283
try {
@@ -140,6 +161,7 @@ export default function WidgetWindow({ widgetId }: Props) {
140161
return () => {
141162
unlistenFocus.then((u) => u());
142163
unlistenMove.then((u) => u());
164+
window.removeEventListener("timelens-widget-auto-blur-changed", onAutoBlurChanged);
143165
window.removeEventListener("mousedown", restoreOnMouseDown);
144166
if (positionSaveTimer.current) clearTimeout(positionSaveTimer.current);
145167
};
@@ -211,7 +233,27 @@ export default function WidgetWindow({ widgetId }: Props) {
211233
};
212234

213235
const handleMouseLeave = () => {
214-
idleTimer.current = setTimeout(() => setIdle(true), 2000);
236+
if (!autoBlurEnabled.current) return;
237+
idleTimer.current = setTimeout(() => {
238+
const activeElement = document.activeElement;
239+
const isEditing = activeElement instanceof HTMLTextAreaElement
240+
|| activeElement instanceof HTMLInputElement
241+
|| activeElement instanceof HTMLElement && activeElement.isContentEditable;
242+
if (!isEditing || !rootRef.current?.contains(activeElement)) {
243+
setIdle(true);
244+
}
245+
}, 2000);
246+
};
247+
248+
const handleFocusCapture = (event: React.FocusEvent<HTMLDivElement>) => {
249+
const target = event.target;
250+
const isEditing = target instanceof HTMLTextAreaElement
251+
|| target instanceof HTMLInputElement
252+
|| target instanceof HTMLElement && target.isContentEditable;
253+
if (isEditing) {
254+
clearTimeout(idleTimer.current);
255+
setIdle(false);
256+
}
215257
};
216258

217259
// Cleanup idle timer on unmount
@@ -221,9 +263,11 @@ export default function WidgetWindow({ widgetId }: Props) {
221263

222264
return (
223265
<div
266+
ref={rootRef}
224267
className={`widget-root ${isBlurred && widgetType !== "clock" ? "widget-root--faded" : ""} ${idle ? "widget-idle" : ""}`}
225268
onMouseEnter={handleMouseEnter}
226269
onMouseLeave={handleMouseLeave}
270+
onFocusCapture={handleFocusCapture}
227271
>
228272
{widgetType === "clock" && <ClockWidget key={refreshKey} widgetId={widgetId} isBlurred={isBlurred} />}
229273
{widgetType === "todo" && <TodoWidget key={refreshKey} widgetId={widgetId} />}

src/widgets/__tests__/NoteWidget.test.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,22 @@ describe("NoteWidget", () => {
5555
expect(writeCalls.length).toBeGreaterThan(0);
5656
});
5757

58+
it("enables auto blur for the current widget", async () => {
59+
const onAutoBlurChanged = vi.fn();
60+
window.addEventListener("timelens-widget-auto-blur-changed", onAutoBlurChanged);
61+
mockGatewayState({ notes: null, notes_backup: null });
62+
renderWithProviders(<NoteWidget widgetId="note-test" />);
63+
64+
await screen.findByText("No notes yet. Create your first one.");
65+
await userEvent.click(screen.getByRole("button", { name: "Auto-blur when unfocused" }));
66+
67+
expect(localStorage.getItem("note-test-auto-blur")).toBe("1");
68+
expect(onAutoBlurChanged).toHaveBeenCalledWith(expect.objectContaining({
69+
detail: { widgetId: "note-test", enabled: true },
70+
}));
71+
window.removeEventListener("timelens-widget-auto-blur-changed", onAutoBlurChanged);
72+
});
73+
5874
it("deletes the current note", async () => {
5975
const notes = JSON.stringify([
6076
{ id: "n1", content: "First note", updatedAt: new Date().toISOString() },

0 commit comments

Comments
 (0)