feat(fc2a): add TimelineSidebar and assemble GalleryView

GalleryView wires the grid + timeline + modal together. The URL is the
source of truth for which image (if any) is open — clicking an item pushes
?image=N; closing the modal pops it. This makes deep links shareable and
the back button work intuitively.

Below 900px viewport, the timeline drops below the grid as a horizontal
strip so it doesn't compete with thumbnail space on mobile.

Modal store + ImageViewer are placeholders here; real implementations land
in Batch 5 (Tasks 19–22).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-14 12:32:50 -04:00
parent 708c033627
commit 899b193085
5 changed files with 186 additions and 1 deletions
+77
View File
@@ -0,0 +1,77 @@
<template>
<v-container fluid class="py-6">
<h1 class="fc-h1 mb-4">Gallery</h1>
<div class="fc-gallery-layout">
<div class="fc-gallery-layout__main">
<EmptyState v-if="store.isEmpty" />
<GalleryGrid v-else @open="openImage" />
</div>
<TimelineSidebar v-if="store.images.length > 0" class="fc-gallery-layout__sidebar" />
</div>
<ImageViewer
v-if="modal.currentImageId !== null"
@close="closeImage"
/>
</v-container>
</template>
<script setup>
import { onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useGalleryStore } from '../stores/gallery.js'
import { useModalStore } from '../stores/modal.js'
import GalleryGrid from '../components/gallery/GalleryGrid.vue'
import TimelineSidebar from '../components/gallery/TimelineSidebar.vue'
import EmptyState from '../components/gallery/EmptyState.vue'
import ImageViewer from '../components/modal/ImageViewer.vue'
const store = useGalleryStore()
const modal = useModalStore()
const router = useRouter()
const route = useRoute()
onMounted(async () => {
await store.loadInitial()
await store.loadTimeline()
// Open modal if URL has ?image=N
const initial = parseInt(route.query.image, 10)
if (!isNaN(initial)) modal.open(initial)
})
watch(() => route.query.image, (q) => {
const id = parseInt(q, 10)
if (!isNaN(id) && id !== modal.currentImageId) modal.open(id)
else if (isNaN(id) && modal.currentImageId !== null) modal.close()
})
function openImage(id) {
router.push({ query: { ...route.query, image: id } })
}
function closeImage() {
const q = { ...route.query }
delete q.image
router.push({ query: q })
}
</script>
<style scoped>
.fc-h1 {
font-family: 'Fraunces', Georgia, serif;
font-size: 32px;
font-weight: 500;
color: rgb(var(--v-theme-on-surface));
}
.fc-gallery-layout {
display: flex;
gap: 16px;
align-items: flex-start;
}
.fc-gallery-layout__main { flex: 1; min-width: 0; }
.fc-gallery-layout__sidebar { flex-shrink: 0; }
@media (max-width: 900px) {
.fc-gallery-layout { flex-direction: column-reverse; }
.fc-gallery-layout__sidebar { width: 100%; max-height: 200px; }
}
</style>