fix(home): shrink frame card, three-state status, draggable sheet, label overlap
CI / test (push) Has been cancelled
CI / test (push) Has been cancelled
- HomeView clears the bottom nav so + Add Photo isn't covered.
- Cap large frame-card preview to min(240px, 30dvh) so portrait frames
no longer dominate the screen at full mobile width.
- Three-state device status — green/Online (recent sync), yellow/Sync
issue (one window missed), red/Offline (two+ windows missed). Window
is rotationIntervalMinutes for interval-mode devices, 24h for daily
wakeHour-mode devices.
- Show last-sync ("synced 2h ago") and next-expected-sync line on the
large card. wakeHour devices show local-hour ("next sync ~4 AM
tomorrow") in the device's configured timezone.
- BaseBottomSheet drag-to-dismiss on the handle. Touch and pointer
events; releases past 80px close the sheet. Snaps back below.
- BaseInput floating label rewrite — taller field, label re-anchors
to top: 8px when filled/focused so it sits cleanly above the value
instead of overlapping it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,8 @@
|
||||
:status="deviceStatus(devicesStore.devices[0])"
|
||||
:orientation="devicesStore.devices[0].orientation"
|
||||
:thumbnailUrl="previewUrl(devicesStore.devices[0])"
|
||||
:lastSync="lastSyncLabel(devicesStore.devices[0])"
|
||||
:nextSync="nextSyncLabel(devicesStore.devices[0])"
|
||||
@add-photo="onAddPhoto"
|
||||
@edit="onEdit"
|
||||
/>
|
||||
@@ -46,6 +48,8 @@
|
||||
:status="deviceStatus(device)"
|
||||
:orientation="device.orientation"
|
||||
:thumbnailUrl="previewUrl(device)"
|
||||
:lastSync="lastSyncLabel(device)"
|
||||
:nextSync="nextSyncLabel(device)"
|
||||
@add-photo="onAddPhoto"
|
||||
@edit="onEdit"
|
||||
/>
|
||||
@@ -105,11 +109,58 @@ import { useDevicesStore } from '@/stores/devices'
|
||||
import { useUploadStore } from '@/stores/upload'
|
||||
import type { Device } from '@/types'
|
||||
|
||||
function deviceStatus(device: Device): 'ok' | 'offline' {
|
||||
// Sync interval for status comparisons. Devices configured with a daily wake
|
||||
// hour use a 24h window; otherwise the rotation interval drives it.
|
||||
function syncIntervalMs(device: Device): number {
|
||||
if (device.wakeHour !== null) return 24 * 60 * 60 * 1000
|
||||
return device.rotationIntervalMinutes * 60_000
|
||||
}
|
||||
|
||||
function deviceStatus(device: Device): 'ok' | 'sync-fail' | 'offline' {
|
||||
if (!device.lastSeenAt) return 'offline'
|
||||
const seenMs = Date.now() - new Date(device.lastSeenAt).getTime()
|
||||
const windowMs = Math.max(device.rotationIntervalMinutes * 2 * 60_000, 30 * 60_000)
|
||||
return seenMs <= windowMs ? 'ok' : 'offline'
|
||||
const elapsed = Date.now() - new Date(device.lastSeenAt).getTime()
|
||||
const interval = syncIntervalMs(device)
|
||||
if (elapsed <= interval) return 'ok'
|
||||
if (elapsed <= 2 * interval) return 'sync-fail'
|
||||
return 'offline'
|
||||
}
|
||||
|
||||
function lastSyncLabel(device: Device): string | null {
|
||||
if (!device.lastSeenAt) return null
|
||||
const ago = Date.now() - new Date(device.lastSeenAt).getTime()
|
||||
if (ago < 60_000) return 'just now'
|
||||
if (ago < 3_600_000) return `${Math.round(ago / 60_000)}m ago`
|
||||
if (ago < 86_400_000) return `${Math.round(ago / 3_600_000)}h ago`
|
||||
const days = Math.round(ago / 86_400_000)
|
||||
if (days === 1) return 'yesterday'
|
||||
return `${days} days ago`
|
||||
}
|
||||
|
||||
function formatHour(h: number): string {
|
||||
if (h === 0) return '12 AM'
|
||||
if (h < 12) return `${h} AM`
|
||||
if (h === 12) return '12 PM'
|
||||
return `${h - 12} PM`
|
||||
}
|
||||
|
||||
function nextSyncLabel(device: Device): string | null {
|
||||
if (device.wakeHour !== null) {
|
||||
const fmt = new Intl.DateTimeFormat('en-US', {
|
||||
timeZone: device.timezone || 'UTC',
|
||||
hour: 'numeric',
|
||||
hour12: false,
|
||||
})
|
||||
const currentHour = parseInt(fmt.format(new Date()), 10)
|
||||
const tag = currentHour < device.wakeHour ? 'today' : 'tomorrow'
|
||||
return `next sync ~${formatHour(device.wakeHour)} ${tag}`
|
||||
}
|
||||
if (!device.lastSeenAt) return null
|
||||
const next = new Date(device.lastSeenAt).getTime() + device.rotationIntervalMinutes * 60_000
|
||||
const fromNow = next - Date.now()
|
||||
if (fromNow <= 0) return null
|
||||
if (fromNow < 60_000) return 'next sync in <1m'
|
||||
if (fromNow < 3_600_000) return `next sync in ${Math.round(fromNow / 60_000)}m`
|
||||
return `next sync in ${Math.round(fromNow / 3_600_000)}h`
|
||||
}
|
||||
|
||||
function previewUrl(device: Device): string | undefined {
|
||||
@@ -250,7 +301,7 @@ async function saveSettings() {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.home-view {
|
||||
padding: var(--space-4);
|
||||
padding: var(--space-4) var(--space-4) calc(var(--bottom-nav-height) + var(--space-4));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
|
||||
Reference in New Issue
Block a user