194 lines
8.9 KiB
Vue
194 lines
8.9 KiB
Vue
<script setup lang="ts">
|
|
const { collapsed, toggleSidebar, restoreSidebar } = useSidebar()
|
|
const { show: openSearch } = useSearch()
|
|
const { show: openSettings } = useSettings()
|
|
const { tabs, lastSlug } = useArticleTabs()
|
|
const { user, isAdmin, signOut } = useAuth()
|
|
const { show: openSignIn } = useSignInPopup()
|
|
const { show: openProfile } = useProfileMenu()
|
|
const { t } = useI18n()
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
|
|
/* The open articles live in the reading surface: the book ArticleBookStack
|
|
lays their pages side by side, older ones parked with only their spines
|
|
showing. The stack stays open all session, but the view of it is only drawn
|
|
in the reader — so this button is the way back: press it wherever you are
|
|
(the graph, the list, the editor) and every article you have open comes back
|
|
into view, stacked as you left it. */
|
|
const inReader = computed(() => wikiSlugFromPath(route.path) !== null)
|
|
|
|
function showOpenArticles() {
|
|
if (inReader.value) return // the pages are already spread out in view
|
|
/* turn back to the page that was last in view — every tab knows its own
|
|
address, language versions included */
|
|
const target =
|
|
tabs.value.find((t) => t.slug === lastSlug.value) ?? tabs.value[tabs.value.length - 1]
|
|
if (target) void router.push(tabHref(target))
|
|
}
|
|
|
|
/* the shortcut hint beside the search bar — "/" keeps focus on the home
|
|
page's inline box there, so the popup's universal key is Ctrl/⌘+K */
|
|
const kbdHint = ref('/')
|
|
onMounted(() => {
|
|
kbdHint.value = /Mac|iPhone|iPad/i.test(navigator.userAgent) ? '⌘K' : 'Ctrl K'
|
|
restoreSidebar()
|
|
})
|
|
|
|
/* the account row wears an initial in a circle, so it still reads as a person
|
|
once the collapsed rail has dropped the name beside it */
|
|
const initials = computed(() => (user.value?.username.charAt(0) ?? '?').toUpperCase())
|
|
</script>
|
|
|
|
<template>
|
|
<aside class="site-sidebar" :class="{ collapsed }">
|
|
<NuxtLink to="/" class="logo" :aria-label="t('sidebar.home')">
|
|
<span class="logo-mark">∑</span>
|
|
<span class="hide-collapsed">math<span class="logo-accent">ew</span></span>
|
|
</NuxtLink>
|
|
|
|
<nav class="sidebar-nav">
|
|
<button
|
|
type="button"
|
|
class="sidebar-search"
|
|
:aria-label="t('common.searchArticles')"
|
|
@click="openSearch()"
|
|
>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
|
|
<circle cx="11" cy="11" r="7" />
|
|
<path d="m20 20-3.2-3.2" />
|
|
</svg>
|
|
<span class="sidebar-search-label hide-collapsed">{{ t('sidebar.search') }}</span>
|
|
<kbd class="kbd-hint hide-collapsed">{{ kbdHint }}</kbd>
|
|
</button>
|
|
|
|
<button
|
|
v-if="tabs.length"
|
|
type="button"
|
|
class="nav-link sidebar-open"
|
|
:class="{ 'is-current': inReader }"
|
|
:aria-label="t('sidebar.openAria', tabs.length)"
|
|
:title="t('sidebar.openTitle', tabs.length)"
|
|
@click="showOpenArticles()"
|
|
>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
<path d="M12 2 2 7l10 5 10-5-10-5z" />
|
|
<path d="m2 12 10 5 10-5" />
|
|
<path d="m2 17 10 5 10-5" />
|
|
</svg>
|
|
<span class="hide-collapsed">{{ t('sidebar.open') }}</span>
|
|
<span class="tabs-count hide-collapsed">{{ tabs.length }}</span>
|
|
</button>
|
|
|
|
<NuxtLink to="/articles" class="nav-link" :title="t('sidebar.articles')">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
|
|
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" />
|
|
</svg>
|
|
<span class="hide-collapsed">{{ t('sidebar.articles') }}</span>
|
|
</NuxtLink>
|
|
<NuxtLink to="/graph" class="nav-link" :title="t('sidebar.graph')">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
<circle cx="18" cy="5" r="3" />
|
|
<circle cx="6" cy="12" r="3" />
|
|
<circle cx="18" cy="19" r="3" />
|
|
<path d="m8.6 10.5 6.8-4M8.6 13.5l6.8 4" />
|
|
</svg>
|
|
<span class="hide-collapsed">{{ t('sidebar.graph') }}</span>
|
|
</NuxtLink>
|
|
<NuxtLink
|
|
v-if="isAdmin"
|
|
to="/admin"
|
|
class="nav-link"
|
|
:title="t('sidebar.adminHint')"
|
|
>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
<path d="M12 22s8-4.5 8-10V5.5L12 2 4 5.5V12c0 5.5 8 10 8 10z" />
|
|
<path d="m9 12 2 2 4-4" />
|
|
</svg>
|
|
<span class="hide-collapsed">{{ t('sidebar.admin') }}</span>
|
|
</NuxtLink>
|
|
<NuxtLink to="/new" class="btn btn-primary btn-sm" :title="t('sidebar.newArticle')">
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round">
|
|
<path d="M12 5v14M5 12h14" />
|
|
</svg>
|
|
<span class="hide-collapsed">{{ t('sidebar.newArticle') }}</span>
|
|
</NuxtLink>
|
|
</nav>
|
|
|
|
<div class="sidebar-footer">
|
|
<!-- who is using the wiki. Reading needs no account, so a visitor is given
|
|
the door in; a signed-in person sees who writes as themselves and can
|
|
open their profile — the picture, the account, the way out. -->
|
|
<div v-if="user" class="account-row">
|
|
<button
|
|
type="button"
|
|
class="account-open"
|
|
:title="t('sidebar.signedInAs', { name: user.username })"
|
|
@click="openProfile()"
|
|
>
|
|
<span class="account-avatar" aria-hidden="true">
|
|
<img v-if="user.avatar" :src="user.avatar" alt="" />
|
|
<template v-else>{{ initials }}</template>
|
|
</span>
|
|
<span class="hide-collapsed account-name">{{ user.username }}</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="account-exit hide-collapsed"
|
|
:aria-label="t('common.signOut')"
|
|
:title="t('common.signOut')"
|
|
@click="signOut()"
|
|
>
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
|
<path d="m16 17 5-5-5-5" />
|
|
<path d="M21 12H9" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<button
|
|
v-else
|
|
type="button"
|
|
class="icon-btn"
|
|
:title="t('sidebar.signInHint')"
|
|
@click="openSignIn()"
|
|
>
|
|
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
<circle cx="8.5" cy="15.5" r="4" />
|
|
<path d="m11.6 12.4 8.4-8.4 2 2-2 2 1.5 1.5-2 2-1.5-1.5-1.9 1.9" />
|
|
</svg>
|
|
<span class="hide-collapsed">{{ t('auth.signin') }}</span>
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
class="icon-btn"
|
|
:aria-label="t('settings.title')"
|
|
:title="t('settings.title')"
|
|
@click="openSettings()"
|
|
>
|
|
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
<circle cx="12" cy="12" r="3" />
|
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1Z" />
|
|
</svg>
|
|
<span class="hide-collapsed">{{ t('settings.title') }}</span>
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
class="panel-icon-btn sidebar-collapse-btn"
|
|
:aria-label="collapsed ? t('sidebar.expand') : t('sidebar.collapse')"
|
|
:title="collapsed ? t('sidebar.expand') : t('sidebar.collapse')"
|
|
@click="toggleSidebar()"
|
|
>
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
<path d="m15 18-6-6 6-6" />
|
|
</svg>
|
|
<span class="hide-collapsed">{{ t('sidebar.collapseLabel') }}</span>
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
</template>
|