feat(focus-timer): integrate with float window + add pop-out button

Jake's feedback on Phase 1: make the timer pinnable / always-on-top,
combined with the existing Now-list pop-out. Two changes:

1. Mount <FocusTimer /> in src/routes/float/+layout@.svelte so the
   running countdown stays visible in the always-on-top float window
   alongside the WIP task list. No content change to the float page
   itself — the timer is a global overlay.

2. Add a pop-out icon to the main-window focus timer that opens the
   existing /float route via window.open. One click → timer + Now
   list pinned on top without touching main window focus. Hidden
   inside the float window itself (detected via URL) so you cannot
   recursively pop out.

Result matches the Todo float-out UX the user already knows:
click ExternalLink, you get a small always-on-top window with
tasks + a live countdown ring in the top-right.
This commit is contained in:
2026-04-24 12:06:37 +01:00
parent bbc7c217be
commit f25f8db818
2 changed files with 36 additions and 1 deletions

View File

@@ -15,8 +15,26 @@
// follows the sensory-zone theme switcher in Settings.
import { onMount, onDestroy } from "svelte";
import { X, Plus } from "lucide-svelte";
import { X, Plus, ExternalLink } from "lucide-svelte";
import { focusTimer } from "$lib/stores/focusTimer.svelte.js";
import { hasTauriRuntime } from "$lib/utils/runtime.js";
// Hide the "pop out" button inside the float window itself — opening
// a second float from a float would be silly and would re-mount the
// same component. Detect via URL rather than a prop so we do not
// have to thread context through every mount site.
let isSecondaryWindow = $state(false);
if (typeof window !== "undefined") {
isSecondaryWindow = window.location.pathname.startsWith("/float")
|| window.location.pathname.startsWith("/viewer");
}
function handlePopOut() {
// Mirror the button in TasksPage.svelte — opens the always-on-top
// Now list + pinned timer in one floating window.
if (!hasTauriRuntime()) return;
window.open("/float", "_blank", "width=380,height=520");
}
const RING_SIZE = 64;
const RING_STROKE = 5;
@@ -148,6 +166,16 @@
>
<Plus size={14} aria-hidden="true" />
</button>
{#if !isSecondaryWindow}
<button
class="icon-btn"
onclick={handlePopOut}
aria-label="Pop out timer + Now list into floating window"
title="Pop out (keeps timer + tasks on top)"
>
<ExternalLink size={14} aria-hidden="true" />
</button>
{/if}
<button
class="icon-btn"
onclick={handleCancel}

View File

@@ -12,6 +12,7 @@
PREFERENCES_CHANGED_EVENT,
} from "$lib/stores/preferences.svelte.js";
import Titlebar from "$lib/components/Titlebar.svelte";
import FocusTimer from "$lib/components/FocusTimer.svelte";
import { loadOsInfo, isLinux } from "$lib/utils/osInfo.js";
let { children } = $props();
@@ -90,3 +91,9 @@
{@render children()}
</div>
</div>
<!-- Focus timer also visible in the always-on-top float window so a
running countdown stays with the Now list. The component is a
global overlay (position: fixed) so it pins to the top-right of
this window independent of the Tasks content below. -->
<FocusTimer />