feat: pull-to-refresh on Home and Library
CI / test (push) Has been cancelled

iOS standalone PWAs don't get Safari's native pull-to-refresh, so add
our own. New <PullToRefresh> component handles the gesture: dampened
drag past an 80px threshold triggers an async onRefresh; below that it
springs back. Swipe direction is locked to the first 6px of movement,
so horizontal carousel swipes (landscape Home) don't accidentally fire
PTR. The arrow icon rotates from 0° to 180° as the pull approaches the
threshold and turns primary-color when ready; during refresh a CSS
spinner replaces it.

- HomeView refreshes the device list (and sync status with it)
- LibraryView refreshes images, pending-share count, devices, and the
  active shared sub-tab page when it's the one in view

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-06 19:09:52 -04:00
parent ca4595873d
commit 328ad632d3
24 changed files with 419 additions and 83 deletions
+18
View File
@@ -1,5 +1,6 @@
<template>
<main class="library">
<PullToRefresh :is-at-top="isAtTop" :on-refresh="refreshLibrary">
<!-- Tabs -->
<div class="library__tabs" role="tablist">
<button
@@ -184,6 +185,7 @@
>Next </button>
</div>
</template>
</PullToRefresh>
<!-- Share sheet -->
<ShareSheet v-if="shareImageId !== null" v-model="shareSheetOpen" :image-id="shareImageId!" />
@@ -213,6 +215,7 @@ import BaseBottomSheet from '@/components/BaseBottomSheet.vue'
import BaseButton from '@/components/BaseButton.vue'
import ApproveCard from '@/components/ApproveCard.vue'
import ShareSheet from '@/components/ShareSheet.vue'
import PullToRefresh from '@/components/PullToRefresh.vue'
import type { Device, Image, SharedImage } from '@/types'
const router = useRouter()
@@ -288,6 +291,21 @@ onMounted(() => {
if (activeTab.value === 'shared') loadShared(sharedTab.value)
})
// ── Pull-to-refresh ───────────────────────────────────────────────────────────
function isAtTop(): boolean {
return window.scrollY === 0
}
async function refreshLibrary() {
await Promise.all([
imagesStore.fetchImages(),
imagesStore.fetchPendingCount(),
devicesStore.fetchDevices(),
activeTab.value === 'shared' ? loadShared(sharedTab.value, sharedPage.value) : Promise.resolve(),
])
}
// For now "mine" and "all" show the same list; shared is a placeholder
const visibleImages = computed(() => imagesStore.images)