feat(story-2.4): home screen device list with FrameCard component

- FrameCard: large (single device, 5:3 preview + Add Photo CTA) and
  compact (52px thumb + name + count + icon pill) variants; WCAG-
  compliant offline/sync-fail status (color + text, never color alone)
- devices Pinia store: fetchDevices() → GET /api/devices
- HomeView: 0 devices → dashed empty-state card; 1 device → large
  FrameCard; 2+ → compact stack; add-photo wired (Epic 3 stub)
- Fix Device type: rotationInterval → rotationIntervalHours to match API

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-28 00:50:46 -04:00
parent fb380c45bd
commit 6c7c7a1a6f
4 changed files with 330 additions and 4 deletions
+120 -3
View File
@@ -1,9 +1,126 @@
<template>
<main class="view">
<h1>Home</h1>
<main class="home-view">
<!-- Loading -->
<div v-if="devicesStore.loading" class="home-view__loading" aria-live="polite">
Loading
</div>
<!-- Empty state -->
<div v-else-if="devicesStore.devices.length === 0" class="home-view__empty">
<div class="home-view__empty-card">
<svg class="home-view__empty-icon" width="48" height="48" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="1.5" aria-hidden="true">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<polyline points="21,15 16,10 5,21"/>
</svg>
<p class="home-view__empty-title">Set up your first frame</p>
<p class="home-view__empty-sub">
Power on your pictureFrame device and scan the QR code it displays to get started.
</p>
</div>
</div>
<!-- Single device large card -->
<div v-else-if="devicesStore.devices.length === 1" class="home-view__single">
<FrameCard
:deviceId="devicesStore.devices[0].id"
:name="devicesStore.devices[0].name"
size="large"
status="ok"
@add-photo="onAddPhoto"
/>
</div>
<!-- Multiple devices compact stack -->
<div v-else class="home-view__list">
<FrameCard
v-for="device in devicesStore.devices"
:key="device.id"
:deviceId="device.id"
:name="device.name"
size="compact"
status="ok"
@add-photo="onAddPhoto"
/>
</div>
</main>
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
import { useDevicesStore } from '@/stores/devices'
import FrameCard from '@/components/FrameCard.vue'
const devicesStore = useDevicesStore()
onMounted(() => devicesStore.fetchDevices())
function onAddPhoto(deviceId: number) {
// Photo upload flow — Epic 3
console.log('add-photo', deviceId)
}
</script>
<style scoped lang="scss">
.view { padding: var(--space-4); }
.home-view {
padding: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-3);
&__loading {
color: var(--color-text-muted);
font-size: var(--text-sm);
padding: var(--space-4) 0;
text-align: center;
}
&__empty {
display: flex;
justify-content: center;
padding-top: var(--space-6);
}
&__empty-card {
background: var(--color-surface);
border: 2px dashed var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-6) var(--space-5);
text-align: center;
max-width: 320px;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-3);
}
&__empty-icon {
color: var(--color-text-muted);
opacity: 0.5;
}
&__empty-title {
font-size: var(--text-md);
font-weight: 700;
}
&__empty-sub {
font-size: var(--text-sm);
color: var(--color-text-muted);
line-height: 1.5;
}
&__single {
display: flex;
flex-direction: column;
}
&__list {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
}
</style>