feat(layout): responsive layout with side-by-side detail and mobile tabs
Desktop (>= 1024px): - Content grid and detail pane shown side-by-side when a detail is selected - Three-column layout: chat | grid | detail - Grid takes 45% width, detail fills remaining space Mobile (< 1024px): - Bottom tab bar with "Chat" and "Content" tabs - Only one view visible at a time - Auto-switches to Content tab when panel opens - Orange dot indicator on Content tab when content is available Extracted reusable components: - ContentGridView: all grid renderers in one component - DetailView: all detail views in one component - CloseButton: reusable close/dismiss button Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
7f8dc72dd7
commit
48dd7a9c68
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<button
|
||||
class="absolute top-3 right-3 z-10 p-2 rounded-lg path-glass-icon transition-colors"
|
||||
:class="isDark
|
||||
? 'text-white/70 hover:bg-white/10'
|
||||
: 'text-gray-500 hover:bg-black/5 hover:text-gray-800'"
|
||||
title="Close"
|
||||
aria-label="Close"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
|
||||
const { isDark } = useTheme()
|
||||
defineEmits<{ click: [] }>()
|
||||
</script>
|
||||
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<div class="flex-1 min-h-0 flex flex-col">
|
||||
<FilmGrid
|
||||
v-if="activeTab === 'film'"
|
||||
:films="panelFilms"
|
||||
:title="panelTitle"
|
||||
@select-film="openFilmDetail"
|
||||
>
|
||||
<template #header-actions>
|
||||
<slot name="header-actions">
|
||||
<CloseButton @click="$emit('close')" />
|
||||
</slot>
|
||||
</template>
|
||||
</FilmGrid>
|
||||
<BookGrid
|
||||
v-else-if="activeTab === 'book'"
|
||||
:books="panelBooks"
|
||||
:title="panelTitle"
|
||||
@select-book="openBookDetail"
|
||||
>
|
||||
<template #header-actions>
|
||||
<slot name="header-actions">
|
||||
<CloseButton @click="$emit('close')" />
|
||||
</slot>
|
||||
</template>
|
||||
</BookGrid>
|
||||
<TVSeriesGrid
|
||||
v-else-if="activeTab === 'tvshow'"
|
||||
:series="panelTVSeries"
|
||||
:title="panelTitle"
|
||||
@select-series="openTVSeriesDetail"
|
||||
>
|
||||
<template #header-actions>
|
||||
<slot name="header-actions">
|
||||
<CloseButton @click="$emit('close')" />
|
||||
</slot>
|
||||
</template>
|
||||
</TVSeriesGrid>
|
||||
<ImageGrid
|
||||
v-else-if="activeTab === 'image'"
|
||||
:images="panelImages"
|
||||
:title="panelTitle"
|
||||
@select-image="openImageDetail"
|
||||
>
|
||||
<template #header-actions>
|
||||
<slot name="header-actions">
|
||||
<CloseButton @click="$emit('close')" />
|
||||
</slot>
|
||||
</template>
|
||||
</ImageGrid>
|
||||
<SongGrid
|
||||
v-else-if="activeTab === 'song'"
|
||||
:songs="panelSongs"
|
||||
:title="panelTitle"
|
||||
@select-song="openSongDetail"
|
||||
>
|
||||
<template #header-actions>
|
||||
<slot name="header-actions">
|
||||
<CloseButton @click="$emit('close')" />
|
||||
</slot>
|
||||
</template>
|
||||
</SongGrid>
|
||||
<MagazineGrid
|
||||
v-else-if="activeTab === 'magazine'"
|
||||
:sections="panelMagazineSections"
|
||||
:hero-image-url="panelMagazineHeroImage"
|
||||
:title="panelTitle"
|
||||
:query="panelQuery"
|
||||
>
|
||||
<template #header-actions>
|
||||
<slot name="header-actions">
|
||||
<CloseButton @click="$emit('close')" />
|
||||
</slot>
|
||||
</template>
|
||||
</MagazineGrid>
|
||||
<NewsGrid
|
||||
v-else-if="activeTab === 'news'"
|
||||
:articles="panelWebResults"
|
||||
:title="panelTitle"
|
||||
:query="panelQuery"
|
||||
>
|
||||
<template #header-actions>
|
||||
<slot name="header-actions">
|
||||
<CloseButton @click="$emit('close')" />
|
||||
</slot>
|
||||
</template>
|
||||
</NewsGrid>
|
||||
<NewsGrid
|
||||
v-else-if="activeTab === 'websites'"
|
||||
:articles="panelWebsites"
|
||||
:title="panelTitle"
|
||||
variant="websites"
|
||||
>
|
||||
<template #header-actions>
|
||||
<slot name="header-actions">
|
||||
<CloseButton @click="$emit('close')" />
|
||||
</slot>
|
||||
</template>
|
||||
</NewsGrid>
|
||||
<PodcastGrid
|
||||
v-else-if="activeTab === 'podcast'"
|
||||
:podcasts="panelPodcasts"
|
||||
:title="panelTitle"
|
||||
@select-podcast="openPodcastDetail"
|
||||
>
|
||||
<template #header-actions>
|
||||
<slot name="header-actions">
|
||||
<CloseButton @click="$emit('close')" />
|
||||
</slot>
|
||||
</template>
|
||||
</PodcastGrid>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Film, Song, Podcast, Book, TVSeries, ImageItem } from '@aiui/core/types/content'
|
||||
import type { WebSearchResult } from '@aiui/core/types/message'
|
||||
import type { ContentTab, MagazineSection } from '@/composables/useContentPanel'
|
||||
import { useContentPanel } from '@/composables/useContentPanel'
|
||||
import FilmGrid from './FilmGrid.vue'
|
||||
import BookGrid from './BookGrid.vue'
|
||||
import TVSeriesGrid from './TVSeriesGrid.vue'
|
||||
import ImageGrid from './ImageGrid.vue'
|
||||
import SongGrid from './SongGrid.vue'
|
||||
import PodcastGrid from './PodcastGrid.vue'
|
||||
import MagazineGrid from './MagazineGrid.vue'
|
||||
import NewsGrid from './NewsGrid.vue'
|
||||
import CloseButton from './CloseButton.vue'
|
||||
|
||||
defineProps<{
|
||||
activeTab: ContentTab
|
||||
panelFilms: Film[]
|
||||
panelBooks: Book[]
|
||||
panelTVSeries: TVSeries[]
|
||||
panelImages: ImageItem[]
|
||||
panelSongs: Song[]
|
||||
panelPodcasts: Podcast[]
|
||||
panelWebResults: WebSearchResult[]
|
||||
panelWebsites: WebSearchResult[]
|
||||
panelMagazineSections: MagazineSection[]
|
||||
panelMagazineHeroImage: string | null
|
||||
panelTitle: string
|
||||
panelQuery: string
|
||||
}>()
|
||||
|
||||
defineEmits<{ close: [] }>()
|
||||
|
||||
const {
|
||||
openFilmDetail,
|
||||
openBookDetail,
|
||||
openTVSeriesDetail,
|
||||
openImageDetail,
|
||||
openSongDetail,
|
||||
openPodcastDetail,
|
||||
} = useContentPanel()
|
||||
</script>
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<FilmDetail
|
||||
v-if="selectedFilm"
|
||||
:film="selectedFilm"
|
||||
@back="closeFilmDetail"
|
||||
/>
|
||||
<SongDetail
|
||||
v-else-if="selectedSong"
|
||||
:song="selectedSong"
|
||||
@back="closeSongDetail"
|
||||
/>
|
||||
<PodcastDetail
|
||||
v-else-if="selectedPodcast"
|
||||
:podcast="selectedPodcast"
|
||||
@back="closePodcastDetail"
|
||||
/>
|
||||
<BookDetail
|
||||
v-else-if="selectedBook"
|
||||
:book="selectedBook"
|
||||
@back="closeBookDetail"
|
||||
/>
|
||||
<TVSeriesDetail
|
||||
v-else-if="selectedTVSeries"
|
||||
:series="selectedTVSeries"
|
||||
@back="closeTVSeriesDetail"
|
||||
/>
|
||||
<ImageDetail
|
||||
v-else-if="selectedImage"
|
||||
:image="selectedImage"
|
||||
@back="closeImageDetail"
|
||||
/>
|
||||
<ArticleDetail
|
||||
v-else-if="selectedArticle"
|
||||
:article="selectedArticle"
|
||||
@back="closeArticleDetail"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useContentPanel } from '@/composables/useContentPanel'
|
||||
import FilmDetail from './FilmDetail.vue'
|
||||
import BookDetail from './BookDetail.vue'
|
||||
import TVSeriesDetail from './TVSeriesDetail.vue'
|
||||
import SongDetail from './SongDetail.vue'
|
||||
import PodcastDetail from './PodcastDetail.vue'
|
||||
import ImageDetail from './ImageDetail.vue'
|
||||
import ArticleDetail from './ArticleDetail.vue'
|
||||
|
||||
const {
|
||||
selectedFilm,
|
||||
selectedBook,
|
||||
selectedTVSeries,
|
||||
selectedImage,
|
||||
selectedSong,
|
||||
selectedPodcast,
|
||||
selectedArticle,
|
||||
closeFilmDetail,
|
||||
closeBookDetail,
|
||||
closeTVSeriesDetail,
|
||||
closeImageDetail,
|
||||
closeSongDetail,
|
||||
closePodcastDetail,
|
||||
closeArticleDetail,
|
||||
} = useContentPanel()
|
||||
</script>
|
||||
Reference in New Issue
Block a user