Files
mathew/frontend/app/components/ArticlePane.vue
Aran Roig e36fef7290
All checks were successful
Build and Deploy Nuxt / build (push) Successful in 29s
Calc2
2026-10-02 02:14:29 +02:00

160 lines
5.7 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
/* One page of the open book: one article, in whichever language it is turned
to. Its left edge is a spine — the title, set vertically, with the × that
closes the page — and the article itself scrolls on its own inside the page.
A newer page slides over this one and leaves only the spine showing, the way
a stack of read pages does; click anywhere on the page — or its spine — to
turn back to it. Turning the article to another language changes the text on
this page and nothing else: it is the same page of the book, read again.
Everything but the spine of a page that isn't in view is `inert`: it can
still be seen while you slide the stack sideways, but it doesn't take
focus or clicks — the pane's own click handler catches those instead, so
one click turns to the page before its links do anything. */
const props = defineProps<{
/** the article this page holds — its canonical slug */
identity: string
/** the version in view, as the library files it */
slug: string
index: number
active: boolean
}>()
const emit = defineEmits<{
activate: []
close: []
remove: []
}>()
const { get, unreadable } = useArticleLibrary()
const { t, locale } = useI18n()
const { th } = useAppLocale()
const article = computed(() => get(props.slug))
const failed = computed(() => props.slug in unreadable.value)
const title = computed(() => article.value?.title ?? props.slug)
const language = computed(() => articleLanguage(article.value ?? {}))
const languageLabel = computed(() => languageName(language.value))
/* every language this article exists in, the one in view included */
const versions = computed(() => article.value?.versions ?? [])
/* the slug the editor answers to: the identity when the canonical version is
still around, this document's own slug when only translations of it remain */
const editorSlug = computed(() =>
versions.value.some((v) => v.slug === props.identity) ? props.identity : props.slug
)
/* turned to another language, the page begins again at the top of the text
it now holds — the article is the same, its words are not */
const scrollEl = ref<HTMLElement | null>(null)
watch(
() => props.slug,
(to, from) => {
if (from) scrollEl.value?.scrollTo({ top: 0 })
}
)
const updated = computed(() => {
const d = article.value?.updatedAt ? new Date(article.value.updatedAt) : null
if (!d || Number.isNaN(d.getTime())) return ''
return d.toLocaleDateString(locale.value, { dateStyle: 'medium' })
})
function turnToThisPage() {
if (!props.active) emit('activate')
}
/* a tag read at the foot of the article: “probability” → “Probability” */
function topicLabel(tag: string) {
const t = tag.trim()
return t.charAt(0).toUpperCase() + t.slice(1)
}
</script>
<template>
<section
class="book-pane"
:class="{ active }"
:data-tab="identity"
:data-index="index"
:style="{ '--i': index }"
:aria-label="t('pane.aria', { title })"
@click="turnToThisPage"
>
<div
class="pane-spine"
role="button"
tabindex="0"
:aria-current="active ? 'page' : undefined"
:aria-disabled="active"
:title="t('pane.spineHint', { title })"
@click.stop="turnToThisPage"
@keydown.enter.prevent="turnToThisPage"
>
<button
type="button"
class="spine-close"
:aria-label="t('common.closeTab')"
@click.stop="emit('close')"
>
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" aria-hidden="true">
<path d="M6 6l12 12M18 6L6 18" />
</svg>
</button>
<span class="spine-title">{{ title }}</span>
</div>
<div class="pane-body" :inert="active ? null : ''">
<div ref="scrollEl" class="pane-scroll">
<div v-if="failed && !article" class="empty">
<div class="glyph">!</div>
<h3>{{ t('pane.readFailTitle') }}</h3>
<p v-html="th('pane.readFailText', { path: `/wiki/${slug}` })"></p>
</div>
<div v-else-if="!article" class="pane-skeleton">
<div class="skeleton" style="height: 36px; width: 52%; margin-bottom: 14px" />
<div class="skeleton" style="height: 18px; width: 78%; margin-bottom: 36px; border-radius: 8px" />
<div class="skeleton" style="height: 220px" />
</div>
<article v-else class="article-page">
<header class="article-head">
<div class="article-eyebrow">
<span v-if="updated">{{ t('pane.lastEdited', { date: updated }) }}</span>
</div>
<div class="article-title-row">
<h1 class="article-title" :lang="language">{{ article.title }}</h1>
<div v-if="active" class="article-actions">
<ArticleActionsMenu
:identity="identity"
:versions="versions"
:current-slug="slug"
:language="language"
:editor-slug="editorSlug"
@remove="emit('remove')"
/>
</div>
</div>
</header>
<div class="article-body">
<MarkdownView :content="article.content ?? ''" :namespace="slug" :article-lang="language" intercept-links />
</div>
<footer v-if="article.tags?.length" class="article-topics">
<span class="article-topics-label">{{ t('pane.topicsLabel') }}</span>
<NuxtLink
v-for="t in article.tags"
:key="t"
:to="{ path: '/articles/', query: { tag: t } }"
class="topic-link"
>
{{ topicLabel(t) }}
</NuxtLink>
</footer>
</article>
</div>
</div>
</section>
</template>