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:
Dorian
2026-03-03 13:08:32 +00:00
co-authored by Claude Opus 4.6
parent e8fc54cade
commit 00bdc055ba
31 changed files with 3175 additions and 1555 deletions
+32 -9
View File
@@ -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 {