feat(renderer): full article reader with TOC, font controls & print (M10.1)
- ArticleReader.vue: sticky TOC sidebar (desktop), mobile dropdown - Auto-generated heading anchors with IntersectionObserver tracking - Reading time estimate, font size controls (persisted localStorage) - Print mode opens clean window with serif typography - Long-form detection (>800 words + headings) shows "Read as article" - Removed isDark conditionals from ArticleDetail (dark-only app) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
aeb184b930
commit
edb4bbadcd
@@ -32,14 +32,13 @@
|
||||
<div class="p-4 space-y-4">
|
||||
<article
|
||||
v-if="article.content"
|
||||
class="[&_p]:mb-3 [&_ul]:list-disc [&_ol]:list-decimal [&_li]:ml-4 [&_a]:underline [&_a]:underline-offset-2 [&_h1]:text-lg [&_h2]:text-base [&_h3]:text-sm [&_blockquote]:border-l-2 [&_blockquote]:pl-3 [&_blockquote]:italic"
|
||||
:class="isDark ? 'text-white/90' : 'text-gray-900'"
|
||||
class="text-white/90 [&_p]:mb-3 [&_ul]:list-disc [&_ol]:list-decimal [&_li]:ml-4 [&_a]:underline [&_a]:underline-offset-2 [&_h1]:text-lg [&_h2]:text-base [&_h3]:text-sm [&_blockquote]:border-l-2 [&_blockquote]:pl-3 [&_blockquote]:italic"
|
||||
>
|
||||
<div v-html="sanitizedContent" />
|
||||
</article>
|
||||
|
||||
<div v-else class="py-4">
|
||||
<p class="text-sm" :class="isDark ? 'text-white/50' : 'text-gray-500'">
|
||||
<p class="text-sm text-white/50">
|
||||
Full article content is not available. Open the link below to read on the source site.
|
||||
</p>
|
||||
</div>
|
||||
@@ -49,10 +48,7 @@
|
||||
:href="article.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-2 p-3 rounded-xl transition-colors"
|
||||
:class="isDark
|
||||
? 'bg-white/10 hover:bg-white/15 text-white/90'
|
||||
: 'bg-black/5 hover:bg-black/10 text-gray-800'"
|
||||
class="inline-flex items-center gap-2 p-3 rounded-xl transition-colors bg-white/10 hover:bg-white/15 text-white/90"
|
||||
>
|
||||
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
@@ -66,14 +62,11 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { WebSearchResult } from '@aiui/core/types/message'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
import { isSafeImgSrc, sanitizeHtml, escapeHtml } from '@/utils/html'
|
||||
|
||||
const props = defineProps<{ article: WebSearchResult }>()
|
||||
defineEmits<{ back: [] }>()
|
||||
|
||||
const { isDark } = useTheme()
|
||||
|
||||
const articleDomain = computed(() => {
|
||||
const url = props.article?.url
|
||||
if (!url || typeof url !== 'string') return ''
|
||||
|
||||
@@ -87,6 +87,12 @@
|
||||
:article="selectedArticle"
|
||||
@back="closeArticleDetail"
|
||||
/>
|
||||
<ArticleReader
|
||||
v-else-if="longFormArticle"
|
||||
:content="longFormArticle.content"
|
||||
:title="longFormArticle.title"
|
||||
@back="closeLongFormArticle"
|
||||
/>
|
||||
|
||||
<!-- Grid views by active tab -->
|
||||
<component
|
||||
@@ -170,6 +176,7 @@ import PodcastGrid from './PodcastGrid.vue'
|
||||
import PodcastDetail from './PodcastDetail.vue'
|
||||
import NewsGrid from './NewsGrid.vue'
|
||||
import ArticleDetail from './ArticleDetail.vue'
|
||||
import ArticleReader from '@/components/renderers/ArticleReader.vue'
|
||||
import MagazineGrid from './MagazineGrid.vue'
|
||||
import ProjectGrid from './ProjectGrid.vue'
|
||||
import NostrGrid from './NostrGrid.vue'
|
||||
@@ -218,11 +225,13 @@ const {
|
||||
openPodcastDetail,
|
||||
closePodcastDetail,
|
||||
closeArticleDetail,
|
||||
longFormArticle,
|
||||
closeLongFormArticle,
|
||||
closePanel,
|
||||
} = useContentPanel()
|
||||
|
||||
const hasDetailOpen = computed(() =>
|
||||
!!(selectedFilm.value || selectedBook.value || selectedTVSeries.value || selectedSong.value || selectedPodcast.value || selectedArticle.value || selectedDesignSystemItem.value)
|
||||
!!(selectedFilm.value || selectedBook.value || selectedTVSeries.value || selectedSong.value || selectedPodcast.value || selectedArticle.value || selectedDesignSystemItem.value || longFormArticle.value)
|
||||
)
|
||||
|
||||
const windowWidth = ref(window.innerWidth)
|
||||
|
||||
Reference in New Issue
Block a user