Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
37f4483
Give a watchface its own page, and install it from there
freethinkel Aug 9, 2026
158ab17
Read body text in Inter
freethinkel Aug 9, 2026
0d690db
Add a bottom sheet that dismisses by scrolling
freethinkel Aug 9, 2026
8d9bb85
Show the watch panel as a sheet
freethinkel Aug 9, 2026
5a16295
Slide the sheet up instead of appearing at the bottom
freethinkel Aug 9, 2026
9b59cbc
Slide the sheet in on its own, and give it a tail to stretch
freethinkel Aug 9, 2026
bbba41d
Make the sheet headless, and publish how far it has travelled
freethinkel Aug 9, 2026
f9d1de4
Keep the sheet's stretch to a stretch, and give it its own curve
freethinkel Aug 9, 2026
b5b46b5
Give the sheet the scroll range Silk gives it: its own height
freethinkel Aug 9, 2026
c16dbe6
Paint the sheet's surface as a layer that bleeds past its edge
freethinkel Aug 9, 2026
58c4d5a
Park the sheet on its snap point, not at the end of the scroll
freethinkel Aug 9, 2026
8d32e36
Paint the surface below the sheet with a shadow, ending the scroll at…
freethinkel Aug 9, 2026
559f8fa
Give the surface back its own layer, shadow still painting the bleed
freethinkel Aug 9, 2026
90ef3c3
Let the sheet take its time coming in
freethinkel Aug 9, 2026
5eec640
Revert "Let the sheet take its time coming in"
freethinkel Aug 9, 2026
ededcfb
Give the sheet a floor of 40svh
freethinkel Aug 9, 2026
e617814
Raise the sheet's floor to 60svh
freethinkel Aug 9, 2026
6cdc3ad
Clip the scrollport, not the bleed: Silk's real geometry
freethinkel Aug 10, 2026
87678af
Dock the sheet and stop the browser scrolling the dialog itself
freethinkel Aug 11, 2026
ce675a9
Show the watch panel as an anchored popover on desktop
freethinkel Aug 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Instrument+Serif&display=swap"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400..600&family=Instrument+Serif&display=swap"
rel="stylesheet"
/>
%sveltekit.head%
Expand Down
201 changes: 0 additions & 201 deletions src/lib/modules/device/components/watch-popover.svelte

This file was deleted.

161 changes: 161 additions & 0 deletions src/lib/modules/device/components/watch-sheet.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
<script lang="ts">
// The watch panel: connect, what the watch reports about itself, and what is installed on it.
// The shell is adaptive — an anchored popover on desktop, the swipeable sheet on the phone —
// since it is the same panel from the header and from the phone's tab bar.
import type { Snippet } from "svelte";
import { AdaptivePopover } from "$lib/shared/components/adaptive-popover";
import { Button } from "$lib/shared/components/button";
import { Icon } from "$lib/shared/components/icon";
import { bleModel } from "../model";
import { dialLabel, dialPreview, dialTitle } from "../lib/catalog-names";
import { WF_CAPACITY } from "../lib/ble";
import WatchSelector from "./watch-selector.svelte";

interface Props {
trigger: Snippet<[{ open: boolean; toggle: () => void; connected: boolean }]>;
}
const { trigger }: Props = $props();

let open = $state(false);
// The popover light-dismisses on pointerdown; without this guard the click that follows
// would toggle it straight back open, so a toggle right after a close means "close".
let closedAt = 0;
function toggle() {
if (open) open = false;
else if (Date.now() - closedAt > 250) open = true;
}

Check warning on line 26 in src/lib/modules/device/components/watch-sheet.svelte

View workflow job for this annotation

GitHub Actions / check

style(blank-line-after-vars)

Expected a blank line after variable declarations
function onClose() {
open = false;
closedAt = Date.now();
}

const {
$bleStatus: bleStatus,
$bleInfo: bleInfo,
$dials: dials,
connectRequested,
forgetRequested,
$connecting: connecting,
$forgetting: forgetting,
} = bleModel;

const muted = "oklch(from var(--color-text) l c h / 55%)";
</script>

{@render trigger({ open, toggle, connected: !!$bleInfo })}

<AdaptivePopover
{open}
title={$bleInfo ? "CMF Watch Pro 2" : "Connect your watch"}
anchor="--watch-trigger"
{onClose}
>
<!-- ponytail: don't gate on navigator.bluetooth at render time — the Safari polyfill injects later; the real check is in ble.ts on click -->
{#if !$bleInfo}
<div class="section">
<p class="desc">CMF Watch Pro 2 over Web Bluetooth.</p>
<div class="row">
<Button onClick={() => connectRequested()} disabled={$connecting}>
<Icon name="bluetooth" size={16} />
{$connecting ? "Connecting…" : "Connect"}
</Button>
<span
title="Clear Chrome's device permission and the saved auth key — does not affect pairing state on the watch itself"
>
<Button kind="ghost" onClick={() => forgetRequested()} disabled={$forgetting}>
<Icon name="eraser" size={16} />
{$forgetting ? "Forgetting…" : "Forget device"}
</Button>
</span>
{#if $bleStatus}<span class="status">{$bleStatus}</span>{/if}
</div>
</div>
{:else}
<div class="section">
<p class="desc">{$bleStatus}</p>
<div class="info">
<p>
<Icon name="battery-full" size={16} color="var(--color-accent)" />
Battery: {$bleInfo.battery ?? "?"}%
</p>
<p>
<Icon name="cpu" size={16} color={muted} /> Firmware: {$bleInfo.firmware ?? "?"}
</p>
<p>
<Icon name="hash" size={16} color={muted} /> Serial: {$bleInfo.serial ?? "?"}
</p>
</div>
</div>

{#if $dials}
<div class="section">
<h2 class="title">
Watchfaces on the watch ({$dials.length}/{WF_CAPACITY})
</h2>
<div class="badges">
{#each $dials as id, i (id)}
<WatchSelector name={dialLabel(id, i)} preview={dialPreview(id)} title={dialTitle(id)} />
{:else}
<span class="none">the watch reported none</span>
{/each}
</div>
</div>
{/if}
{/if}
</AdaptivePopover>

<style>
.section {
display: flex;
flex-direction: column;
gap: 0.75rem;

& + .section {
margin-top: 1.25rem;
}
}
.title {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
}
.desc {
margin: 0;
font-size: 0.75rem;
color: oklch(from var(--color-text) l c h / 55%);
}
.row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
}
.status {
font-size: 0.625rem;
color: oklch(from var(--color-text) l c h / 55%);
}
.info {
display: flex;
flex-direction: column;
gap: 0.5rem;
font-size: 0.75rem;

p {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0;
}
}
.badges {
display: flex;
gap: 0.75rem;
/* six 64px dials never fit the sheet's width — scroll them sideways rather than wrap */
overflow-x: auto;
padding-bottom: 0.25rem;
}
.none {
font-size: 0.625rem;
color: oklch(from var(--color-text) l c h / 55%);
}
</style>
Loading
Loading