feat(app): add design system viewer, nostr feed, stop generation, and content refactor
- Design system browser with grid/detail views for tokens and components - Nostr feed tab with note/article/zap filtering and relay status - Stop generation button to abort AI streaming mid-response - Paste & extract content without sending to AI - Refactor useContentPanel into contentExtraction.ts and contentFiltering.ts - Banner fallback composable for 3-stage image loading - Wikipedia and Google Books as fallback image sources - Loading skeletons with variant-specific shapes - Mobile UX: auto-switch to content, back button, detail flow - Project grid with breadcrumb nav and inline creation - Filesystem Vite plugin for local project browsing - Magazine text cleanup and song grid polish Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
e8fc54cade
commit
00bdc055ba
@@ -26,12 +26,11 @@
|
||||
<!-- Grid/list panel (on wide desktop: always show grid; on regular: show when no detail) -->
|
||||
<div
|
||||
v-if="panelOpen && hasGridContent && (isWideDesktop || !hasDetailOpen)"
|
||||
class="flex-1 min-w-0 flex flex-col"
|
||||
class="flex-1 min-w-0 flex flex-col relative"
|
||||
>
|
||||
<CloseButton @click="closePanel" />
|
||||
<div
|
||||
v-if="availableTabs.length > 1"
|
||||
class="shrink-0 flex items-center justify-between gap-2 px-4 py-2"
|
||||
class="shrink-0 flex items-center gap-2 px-4 pr-12 py-3"
|
||||
:style="isDark
|
||||
? 'border-bottom: 1px solid rgba(255, 255, 255, 0.08)'
|
||||
: 'border-bottom: 1px solid rgba(0, 0, 0, 0.06)'"
|
||||
@@ -54,6 +53,8 @@
|
||||
</div>
|
||||
<ContentGridView
|
||||
:active-tab="activeTab"
|
||||
:is-wide-desktop="isWideDesktop"
|
||||
:is-mobile="isMobile"
|
||||
:panel-films="panelFilms"
|
||||
:panel-books="panelBooks"
|
||||
:panelTVSeries="panelTVSeries"
|
||||
@@ -164,10 +165,18 @@
|
||||
<div v-show="mobileTab === 'content'" class="flex-1 min-h-0 flex flex-col p-2 pb-0">
|
||||
<div class="flex-1 min-h-0 path-glass-card flex flex-col rounded-2xl overflow-hidden">
|
||||
<template v-if="panelOpen">
|
||||
<div
|
||||
v-if="availableTabs.length > 1"
|
||||
class="shrink-0 flex items-center gap-2 px-3 pt-2 pb-1 overflow-x-auto scrollbar-hide"
|
||||
>
|
||||
<div class="shrink-0 flex items-center gap-2 px-3 pt-2 pb-1 overflow-x-auto scrollbar-hide">
|
||||
<button
|
||||
class="p-1.5 rounded-lg transition-colors shrink-0"
|
||||
:class="isDark
|
||||
? 'text-white/50 hover:text-white/80 hover:bg-white/5'
|
||||
: 'text-gray-400 hover:text-gray-700 hover:bg-black/5'"
|
||||
@click="mobileTab = 'chat'"
|
||||
>
|
||||
<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="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
v-for="tab in availableTabs"
|
||||
:key="tab"
|
||||
@@ -184,6 +193,8 @@
|
||||
</div>
|
||||
<ContentGridView
|
||||
:active-tab="activeTab"
|
||||
:is-wide-desktop="isWideDesktop"
|
||||
:is-mobile="isMobile"
|
||||
:panel-films="panelFilms"
|
||||
:panel-books="panelBooks"
|
||||
:panelTVSeries="panelTVSeries"
|
||||
@@ -349,7 +360,7 @@ import { usePlayer } from '@/composables/usePlayer'
|
||||
import { useCodeContext } from '@/composables/useCodeContext'
|
||||
|
||||
const chatStore = useChatStore()
|
||||
const { activeFile: codeActiveFile, isCodeMode } = useCodeContext()
|
||||
const { activeFile: codeActiveFile, isCodeMode, clearActiveFile: clearCodeFile } = useCodeContext()
|
||||
const { hasTrack } = usePlayer()
|
||||
const { isDark } = useTheme()
|
||||
|
||||
@@ -384,6 +395,7 @@ const {
|
||||
selectedArticle,
|
||||
selectedWebsite,
|
||||
selectedMagazineSection,
|
||||
selectedDesignSystemItem,
|
||||
closeFilmDetail,
|
||||
closeBookDetail,
|
||||
closeTVSeriesDetail,
|
||||
@@ -417,19 +429,27 @@ function onResize() { windowWidth.value = window.innerWidth }
|
||||
onMounted(() => window.addEventListener('resize', onResize))
|
||||
onUnmounted(() => window.removeEventListener('resize', onResize))
|
||||
|
||||
// Auto-switch to content tab on mobile when panel opens or detail is selected
|
||||
// Auto-switch to content tab on mobile when panel opens or content changes
|
||||
watch(panelOpen, (open) => {
|
||||
if (open && isMobile.value) mobileTab.value = 'content'
|
||||
})
|
||||
watch(panelTitle, () => {
|
||||
if (panelOpen.value && isMobile.value && mobileTab.value === 'chat') mobileTab.value = 'content'
|
||||
})
|
||||
watch(activeTab, () => {
|
||||
if (panelOpen.value && isMobile.value && mobileTab.value === 'chat') mobileTab.value = 'content'
|
||||
})
|
||||
|
||||
const hasDetailOpen = computed(() =>
|
||||
!!(selectedFilm.value || selectedBook.value || selectedTVSeries.value ||
|
||||
selectedImage.value || selectedPlace.value || selectedSong.value || selectedPodcast.value || selectedArticle.value || selectedWebsite.value || selectedMagazineSection.value ||
|
||||
selectedDesignSystemItem.value ||
|
||||
(isCodeMode.value && codeActiveFile.value))
|
||||
)
|
||||
|
||||
watch(hasDetailOpen, (open) => {
|
||||
if (open && isMobile.value) mobileTab.value = 'context'
|
||||
if (!open && isMobile.value && mobileTab.value === 'context') mobileTab.value = 'content'
|
||||
})
|
||||
|
||||
const hasGridContent = computed(() =>
|
||||
@@ -447,6 +467,7 @@ function closeAllDetails() {
|
||||
closeArticleDetail()
|
||||
closeWebsiteDetail()
|
||||
closeMagazineSectionDetail()
|
||||
clearCodeFile()
|
||||
}
|
||||
|
||||
// Context navigation — flat list of all detail-openable items
|
||||
@@ -532,6 +553,8 @@ const TAB_LABELS: Record<ContentTab, string> = {
|
||||
websites: 'Websites',
|
||||
magazine: 'Brief',
|
||||
code: 'Code',
|
||||
'design-system': 'Design',
|
||||
nostr: 'Nostr',
|
||||
}
|
||||
|
||||
function tabLabel(tab: ContentTab): string {
|
||||
|
||||
Reference in New Issue
Block a user