diff options
| author | Leitwolf11 | 2026-10-18 09:00:14 +0300 |
|---|---|---|
| committer | Leitwolf11 | 2026-10-18 09:00:14 +0300 |
| commit | 47ffd40c8b61aa9dd131e567bfb2ce78ec7f354c (patch) | |
| tree | 4760813c7550a29ee25160f70cf91b2f5bd552cd /frontend/src/components/DocShell.vue | |
| parent | c2eb3d208b37826167496cfdf657ffca33fef703 (diff) | |
| download | website-design/docs-layout.tar.gz | |
Lay out the documentation like kotlinlang.org/docsdesign/docs-layout
The three documentation pages each drew their own sidebar. The two hand-built
pages listed different topics: Getting started had no link to Syntax. Each page
also opened with a capitalized section kicker.
DocShell now frames every documentation page:
- On the left, the topics of the site, with the current one marked. On a
narrow screen they fold into a Contents button above the page.
- Above the page, a breadcrumb: site / Documentation / topic.
- The page in a column of at most 720 px.
- On the right, "On this page". It is read from the h2 headings of the
rendered page, and each heading gets an id from its words in any script.
It is hidden below 1100 px.
- Under the page, previous and next topic cards and an "Edit this page"
link to the file the page is written in.
DocPage, OverviewPage and GettingStartedPage use it and drop their own
sidebars and kickers. The styles of the old layout are removed. The labels
are in all four languages.
Diffstat (limited to 'frontend/src/components/DocShell.vue')
| -rw-r--r-- | frontend/src/components/DocShell.vue | 161 |
1 files changed, 161 insertions, 0 deletions
diff --git a/frontend/src/components/DocShell.vue b/frontend/src/components/DocShell.vue new file mode 100644 index 0000000..ac9ef67 --- /dev/null +++ b/frontend/src/components/DocShell.vue @@ -0,0 +1,161 @@ +<!-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> --> +<!-- SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 --> + +<script setup lang="ts"> +import { computed, nextTick, onMounted, ref, watch } from "vue"; +import { RouterLink } from "vue-router"; +import { sidebarFor } from "../content/routes"; +import { locale } from "../preferences"; +import { site } from "../site"; + +/** + * The frame of every documentation page: the topics of the site on the left, the page in a readable column, the + * headings of the page on the right, and the previous and next topics under it. The headings are read from the + * rendered page, so a page only has to write its h2 elements. + */ +const props = defineProps<{ path: string; editPath: string }>(); + +const labels = { + "en-US": { + contents: "Contents", + onPage: "On this page", + previous: "Previous", + next: "Next", + edit: "Edit this page", + docs: "Documentation", + }, + "de-DE": { + contents: "Inhalt", + onPage: "Auf dieser Seite", + previous: "Zurück", + next: "Weiter", + edit: "Diese Seite bearbeiten", + docs: "Dokumentation", + }, + "ru-RU": { + contents: "Содержание", + onPage: "На этой странице", + previous: "Назад", + next: "Далее", + edit: "Изменить страницу", + docs: "Документация", + }, + "he-IL": { + contents: "תוכן", + onPage: "בעמוד הזה", + previous: "הקודם", + next: "הבא", + edit: "עריכת העמוד", + docs: "תיעוד", + }, +} as const; +const copy = computed(() => labels[locale.value]); +const brand = { language: "Tala", talac: "Talac", tazel: "Tazel" }[site]; + +const sidebar = computed(() => sidebarFor(site, locale.value)); +const position = computed(() => sidebar.value.findIndex((entry) => entry.path === props.path)); +const current = computed(() => sidebar.value[position.value]); +const previous = computed(() => + position.value > 0 ? sidebar.value[position.value - 1] : undefined, +); +const next = computed(() => + position.value >= 0 && position.value < sidebar.value.length - 1 + ? sidebar.value[position.value + 1] + : undefined, +); +const editUrl = computed( + () => `https://github.com/Progmasoft/website/blob/main/frontend/${props.editPath}`, +); + +const article = ref<HTMLElement>(); +const contents = ref<HTMLDetailsElement>(); +const headings = ref<{ id: string; text: string }[]>([]); + +/** A fragment for a heading: its words, in any script, joined by hyphens. */ +function slug(text: string): string { + return text + .toLowerCase() + .replace(/[^\p{L}\p{N}]+/gu, "-") + .replace(/^-+|-+$/g, ""); +} + +async function collectHeadings() { + await nextTick(); + const found: { id: string; text: string }[] = []; + for (const heading of article.value?.querySelectorAll("h2") ?? []) { + const text = heading.textContent?.trim() ?? ""; + if (!text) continue; + let id = slug(text) || `section-${found.length + 1}`; + while (found.some((entry) => entry.id === id)) id = `${id}-${found.length + 1}`; + heading.id = id; + found.push({ id, text }); + } + headings.value = found; +} + +onMounted(() => { + // On a narrow screen the topics fold away above the page; on a wide one they stay open beside it. + if (contents.value && window.matchMedia("(max-width: 760px)").matches) + contents.value.open = false; + collectHeadings(); +}); +watch([locale, () => props.path], collectHeadings); +</script> + +<template> + <div class="docs"> + <aside class="docs-nav"> + <details ref="contents" class="docs-nav-toggle" open> + <summary>{{ copy.contents }}</summary> + <nav :aria-label="copy.docs"> + <p class="docs-nav-site">{{ brand }}</p> + <RouterLink + v-for="entry in sidebar" + :key="entry.path" + :to="entry.path" + :aria-current="entry.path === path ? 'page' : undefined" + >{{ entry.label }}</RouterLink + > + <a v-if="site === 'language'" href="https://github.com/Progmasoft/tala/tree/main/Spec" + >Specification ↗</a + > + </nav> + </details> + </aside> + + <div class="docs-main"> + <nav class="docs-crumbs" aria-label="Breadcrumb"> + <RouterLink to="/">{{ brand }}</RouterLink> + <span aria-hidden="true">/</span> + <span>{{ copy.docs }}</span> + <template v-if="current"> + <span aria-hidden="true">/</span> + <span aria-current="page">{{ current.label }}</span> + </template> + </nav> + <article ref="article" class="doc-content"> + <slot /> + </article> + <nav class="docs-pager" :aria-label="`${copy.previous} / ${copy.next}`"> + <RouterLink v-if="previous" class="docs-pager-previous" :to="previous.path"> + <span>{{ copy.previous }}</span> + <strong>{{ previous.label }}</strong> + </RouterLink> + <RouterLink v-if="next" class="docs-pager-next" :to="next.path"> + <span>{{ copy.next }}</span> + <strong>{{ next.label }}</strong> + </RouterLink> + </nav> + <p class="docs-edit"> + <a :href="editUrl" target="_blank" rel="noopener noreferrer">{{ copy.edit }} ↗</a> + </p> + </div> + + <aside v-if="headings.length > 1" class="docs-toc"> + <p>{{ copy.onPage }}</p> + <a v-for="heading in headings" :key="heading.id" :href="`#${heading.id}`">{{ + heading.text + }}</a> + </aside> + </div> +</template> |