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:
Dorian
2026-03-03 23:48:00 +00:00
co-authored by Claude Opus 4.6
parent aeb184b930
commit edb4bbadcd
5 changed files with 297 additions and 12 deletions
@@ -277,6 +277,13 @@
>
View brief
</button>
<button
v-if="isLongForm"
class="text-[10px] text-accent/70 hover:text-accent transition-colors"
@click.stop="openLongFormArticle(message.content)"
>
Read as article
</button>
</div>
</div>
</div>
@@ -364,7 +371,7 @@ function submitEdit() {
isEditing.value = false
editContent.value = ''
}
const { getContextualInlineContent, stripContentTags, stripMarkdownLinks, updatePanelFromText, panelOpen, availableTabs, setActiveTab, openFilmDetail, openBookDetail, openTVSeriesDetail, openImageDetail, openPlaceDetail, openSongDetail, openPodcastDetail, openArticleDetail, openWebsiteDetail, closeFilmDetail, closeBookDetail, closeTVSeriesDetail, closeImageDetail, closePlaceDetail, closeSongDetail, closePodcastDetail } = useContentPanel()
const { getContextualInlineContent, stripContentTags, stripMarkdownLinks, updatePanelFromText, panelOpen, availableTabs, setActiveTab, openFilmDetail, openBookDetail, openTVSeriesDetail, openImageDetail, openPlaceDetail, openSongDetail, openPodcastDetail, openArticleDetail, openWebsiteDetail, openLongFormArticle, closeFilmDetail, closeBookDetail, closeTVSeriesDetail, closeImageDetail, closePlaceDetail, closeSongDetail, closePodcastDetail } = useContentPanel()
const codeContext = useCodeContext()
const isUser = computed(() => props.message.role === 'user')
@@ -459,6 +466,15 @@ const formattedTime = computed(() => {
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
})
// Long-form article detection (>800 words + has headings)
const isLongForm = computed(() => {
if (isUser.value) return false
const text = props.message.content
const words = text.split(/\s+/).length
const hasHeadings = /^#{2,3}\s+.+$/m.test(text)
return words > 800 && hasHeadings
})
// Token estimate (~4 chars per token)
const estimatedTokens = computed(() => Math.ceil(props.message.content.length / 4))
const showTokenCount = computed(() => props.message.content.length > 20)