First commit

This commit is contained in:
2026-09-30 01:49:35 +02:00
commit 6edd03aace
109 changed files with 27020 additions and 0 deletions

View File

@@ -0,0 +1,146 @@
<script setup lang="ts">
/* One page of the open book. 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.
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<{
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 article behind this document, and the slug its editor answers to:
the identity when the canonical version is still around, this document's
own slug when only translations of it remain */
const identity = computed(() => identityOf(article.value ?? { slug: props.slug }))
const editorSlug = computed(() =>
versions.value.some((v) => v.slug === identity.value) ? identity.value : props.slug
)
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-slug="slug"
: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 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" 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>