Initial commit
This commit is contained in:
223
.vitepress/config/en.ts
Normal file
223
.vitepress/config/en.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import type { DefaultTheme, LocaleConfig } from 'vitepress'
|
||||
import type { Theme } from '../theme/types'
|
||||
import { components } from '../theme/plugins/component'
|
||||
import 'dotenv/config'
|
||||
|
||||
export const SITE_NAME = 'Kotatsu Website'
|
||||
export const META_DESCRIPTION = 'A simple and convenient open source manga reader from and for the community, where you can find and read your favorite manga easier than ever.'
|
||||
|
||||
export const searchLocale: Record<string, Partial<Omit<DefaultTheme.LocalSearchOptions, 'locales'>>> = {
|
||||
root: {
|
||||
translations: {
|
||||
button: {
|
||||
buttonText: 'Search',
|
||||
buttonAriaLabel: 'Search'
|
||||
},
|
||||
modal: {
|
||||
resetButtonTitle: 'Reset search',
|
||||
backButtonTitle: 'Close search',
|
||||
noResultsText: 'No results for',
|
||||
footer: {
|
||||
selectText: 'to select',
|
||||
selectKeyAriaLabel: 'enter',
|
||||
navigateText: 'to navigate',
|
||||
navigateUpKeyAriaLabel: 'up arrow',
|
||||
navigateDownKeyAriaLabel: 'down arrow',
|
||||
closeText: 'to close',
|
||||
closeKeyAriaLabel: 'escape'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const config: LocaleConfig<Theme.Config> = {
|
||||
root: {
|
||||
label: 'English',
|
||||
lang: 'en',
|
||||
description: META_DESCRIPTION,
|
||||
|
||||
head: [
|
||||
['meta', { property: 'og:site_name', content: SITE_NAME }],
|
||||
],
|
||||
|
||||
themeConfig: {
|
||||
nav: getNav(),
|
||||
|
||||
sidebar: {
|
||||
"/": defaultSidebar(),
|
||||
},
|
||||
|
||||
outline: {
|
||||
label: 'On this page',
|
||||
level: 'deep',
|
||||
},
|
||||
|
||||
docFooter: {
|
||||
prev: 'Previous page',
|
||||
next: 'Next page',
|
||||
},
|
||||
editLink: {
|
||||
pattern: 'https://github.com/KotatsuApp/kotatsuapp.github.io/edit/master/website/:path',
|
||||
text: 'Suggest changes to this page',
|
||||
},
|
||||
footer: {
|
||||
message: "<a href=\"https://www.gnu.org/licenses/gpl-3.0.en.html\" target=\"_blank\">GPL-3.0 Licensed</a> <span class=\"divider\">|</span> <a href=\"/privacy/\">Privacy policy</a>",
|
||||
copyright: `Copyright © 2020 - ${new Date().getFullYear()} Kotatsu Developers`,
|
||||
},
|
||||
|
||||
components: components.filter(component => component.path.startsWith('components/')),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function defaultSidebar(): DefaultTheme.SidebarItem[] {
|
||||
return [
|
||||
{
|
||||
text: "General sections",
|
||||
items: [
|
||||
{
|
||||
text: "Download",
|
||||
link: "/download/",
|
||||
},
|
||||
{
|
||||
text: "Parsers",
|
||||
link: "/parsers/",
|
||||
},
|
||||
{
|
||||
text: "Changelogs",
|
||||
link: "/changelogs/",
|
||||
},
|
||||
{
|
||||
text: "Contribute",
|
||||
link: "/contribute/",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Frequently Asked Questions",
|
||||
items: [
|
||||
{ text: "General", link: "/manuals/faq/general" },
|
||||
{
|
||||
text: "Feed",
|
||||
link: "/manuals/faq/feed",
|
||||
},
|
||||
{
|
||||
text: "Explore",
|
||||
link: "/manuals/faq/explore/",
|
||||
collapsed: true,
|
||||
items: [
|
||||
{
|
||||
text: "Sources",
|
||||
link: "/manuals/faq/explore/sources"
|
||||
},
|
||||
{
|
||||
text: "Local storage",
|
||||
link: "/manuals/faq/explore/local-storage",
|
||||
},
|
||||
{
|
||||
text: "Bookmarks",
|
||||
link: "/manuals/faq/explore/bookmarks",
|
||||
},
|
||||
{
|
||||
text: "Random",
|
||||
link: "/manuals/faq/explore/random",
|
||||
},
|
||||
{
|
||||
text: "Downloads",
|
||||
link: "/manuals/faq/explore/downloads",
|
||||
},
|
||||
{
|
||||
text: "Suggestions",
|
||||
link: "/manuals/faq/explore/suggestions",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Reader",
|
||||
link: "/manuals/faq/reader",
|
||||
},
|
||||
{
|
||||
text: "Settings",
|
||||
link: "/manuals/faq/settings",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Guides",
|
||||
items: [
|
||||
{
|
||||
text: "Getting started",
|
||||
link: "/manuals/guides/getting-started",
|
||||
},
|
||||
{
|
||||
text: "Troubleshooting",
|
||||
link: "/manuals/guides/troubleshooting/",
|
||||
collapsed: true,
|
||||
items: [
|
||||
{
|
||||
text: "Common issues",
|
||||
link: "/manuals/guides/troubleshooting/common-issues",
|
||||
},
|
||||
{
|
||||
text: "Diagnosis",
|
||||
link: "/manuals/guides/troubleshooting/diagnosis",
|
||||
},
|
||||
],
|
||||
},
|
||||
{ text: "Backups", link: "/manuals/guides/backups" },
|
||||
{ text: "Tracking", link: "/manuals/guides/tracking" },
|
||||
{ text: "Categories", link: "/manuals/guides/categories" },
|
||||
{
|
||||
text: "Local storage",
|
||||
link: "/manuals/guides/local-storage/",
|
||||
collapsed: true,
|
||||
items: [
|
||||
{
|
||||
text: "Advanced editing",
|
||||
link: "/manuals/guides/local-source/advanced",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Reader settings",
|
||||
link: "/manuals/guides/reader-settings",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function getNav(): DefaultTheme.NavItem[] {
|
||||
return [
|
||||
{
|
||||
text: 'Get v{app_version}',
|
||||
activeMatch: "^/*?(download|changelogs)/*?$",
|
||||
items: [
|
||||
{
|
||||
text: "Download",
|
||||
link: "/download/",
|
||||
},
|
||||
{
|
||||
text: "Changelogs",
|
||||
link: "/changelogs/",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Manual",
|
||||
link: "/manuals/guides/getting-started",
|
||||
activeMatch: "/manuals/",
|
||||
},
|
||||
{
|
||||
text: "News",
|
||||
link: "/news/",
|
||||
activeMatch: "/news/",
|
||||
},
|
||||
{
|
||||
text: "Account",
|
||||
link: "/account/",
|
||||
activeMatch: "/account/"
|
||||
}
|
||||
]
|
||||
}
|
||||
111
.vitepress/config/hooks/generateMeta.ts
Normal file
111
.vitepress/config/hooks/generateMeta.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import type { HeadConfig, TransformContext } from "vitepress"
|
||||
import type { DocsPageData } from '../../theme/plugins/component'
|
||||
|
||||
function generateMeta(context: TransformContext, hostname: string) {
|
||||
const head: HeadConfig[] = []
|
||||
const { pageData }: { pageData: DocsPageData } = context
|
||||
|
||||
const url = `${hostname}/${pageData.relativePath.replace(/((^|\/)index)?\.md$/, "$2")}`
|
||||
|
||||
head.push(["link", { rel: "canonical", href: url }])
|
||||
head.push(["meta", { property: "og:url", content: url }])
|
||||
head.push(["meta", { name: "twitter:url", content: url }])
|
||||
head.push(["meta", { name: "twitter:card", content: "summary_large_image" }])
|
||||
|
||||
if (pageData.frontmatter.theme) {
|
||||
head.push(["meta", { name: "theme-color", content: pageData.frontmatter.theme }])
|
||||
}
|
||||
if (pageData.frontmatter.type) {
|
||||
head.push(["meta", { property: "og:type", content: pageData.frontmatter.type }])
|
||||
}
|
||||
if (pageData.frontmatter.customMetaTitle) {
|
||||
head.push([
|
||||
"meta",
|
||||
{
|
||||
property: "og:title",
|
||||
content: pageData.frontmatter.customMetaTitle,
|
||||
},
|
||||
])
|
||||
head.push([
|
||||
"meta",
|
||||
{
|
||||
name: "twitter:title",
|
||||
content: pageData.frontmatter.customMetaTitle,
|
||||
},
|
||||
])
|
||||
head.push(["meta", { property: "og:site_name", content: "" }])
|
||||
} else {
|
||||
head.push(["meta", { property: "og:title", content: pageData.frontmatter.title }])
|
||||
head.push(["meta", { name: "twitter:title", content: pageData.frontmatter.title }])
|
||||
}
|
||||
if (pageData.frontmatter.description) {
|
||||
head.push([
|
||||
"meta",
|
||||
{
|
||||
property: "og:description",
|
||||
content: pageData.frontmatter.description,
|
||||
},
|
||||
])
|
||||
head.push([
|
||||
"meta",
|
||||
{
|
||||
name: "twitter:description",
|
||||
content: pageData.frontmatter.description,
|
||||
},
|
||||
])
|
||||
}
|
||||
if (pageData.frontmatter.image) {
|
||||
head.push([
|
||||
"meta",
|
||||
{
|
||||
property: "og:image",
|
||||
content: `${hostname}/${pageData.frontmatter.image.replace(/^\//, "")}`,
|
||||
},
|
||||
])
|
||||
head.push([
|
||||
"meta",
|
||||
{
|
||||
name: "twitter:image",
|
||||
content: `${hostname}/${pageData.frontmatter.image.replace(/^\//, "")}`,
|
||||
},
|
||||
])
|
||||
} else {
|
||||
const url = pageData.filePath.replace("index.md", "").replace(".md", "")
|
||||
const imageUrl = `${url}/__og_image__/og.png`.replace(/\/\//g, "/").replace(/^\//, "")
|
||||
|
||||
head.push(["meta", { property: "og:image", content: `${hostname}/${imageUrl}` }])
|
||||
head.push(["meta", { property: "og:image:width", content: "1200" }])
|
||||
head.push(["meta", { property: "og:image:height", content: "628" }])
|
||||
head.push(["meta", { property: "og:image:type", content: "image/png" }])
|
||||
head.push(["meta", { property: "og:image:alt", content: pageData.frontmatter.title }])
|
||||
head.push(["meta", { name: "twitter:image", content: `${hostname}/${imageUrl}` }])
|
||||
head.push(["meta", { name: "twitter:image:width", content: "1200" }])
|
||||
head.push(["meta", { name: "twitter:image:height", content: "628" }])
|
||||
head.push(["meta", { name: "twitter:image:alt", content: pageData.frontmatter.title }])
|
||||
}
|
||||
if (pageData.frontmatter.tag) {
|
||||
head.push(["meta", { property: "article:tag", content: pageData.frontmatter.tag }])
|
||||
}
|
||||
if (pageData.frontmatter.date) {
|
||||
head.push([
|
||||
"meta",
|
||||
{
|
||||
property: "article:published_time",
|
||||
content: pageData.frontmatter.date,
|
||||
},
|
||||
])
|
||||
}
|
||||
if (pageData.lastUpdated && pageData.frontmatter.lastUpdated !== false) {
|
||||
head.push([
|
||||
"meta",
|
||||
{
|
||||
property: "article:modified_time",
|
||||
content: new Date(pageData.lastUpdated).toISOString(),
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
return head
|
||||
}
|
||||
|
||||
export default generateMeta
|
||||
109
.vitepress/config/hooks/generateOgImages.ts
Normal file
109
.vitepress/config/hooks/generateOgImages.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises"
|
||||
import { dirname, resolve } from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { createContentLoader } from "vitepress"
|
||||
import type { ContentData, SiteConfig } from "vitepress"
|
||||
import { type SatoriOptions, satoriVue } from "x-satori/vue"
|
||||
import { renderAsync } from "@resvg/resvg-js"
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const __fonts = resolve(__dirname, "../../fonts")
|
||||
|
||||
async function generateOgImages(config: SiteConfig) {
|
||||
const pages = await createContentLoader("**/*.md", { excerpt: true }).load()
|
||||
const template = await readFile(resolve(__dirname, "../../theme/components/OgImageTemplate.vue"), "utf-8")
|
||||
|
||||
const fonts: SatoriOptions["fonts"] = [
|
||||
{
|
||||
name: "Roboto",
|
||||
data: await readFile(resolve(__fonts, "Roboto-Regular.ttf")),
|
||||
weight: 400,
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
name: "Roboto",
|
||||
data: await readFile(resolve(__fonts, "Roboto-Medium.ttf")),
|
||||
weight: 500,
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
name: "Roboto",
|
||||
data: await readFile(resolve(__fonts, "Roboto-Bold.ttf")),
|
||||
weight: 700,
|
||||
style: "normal",
|
||||
},
|
||||
]
|
||||
|
||||
const filteredPages = pages.filter((p) => p.frontmatter.image === undefined)
|
||||
|
||||
for (const page of filteredPages) {
|
||||
await generateImage({
|
||||
page,
|
||||
template,
|
||||
outDir: config.outDir,
|
||||
fonts,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default generateOgImages
|
||||
|
||||
interface GenerateImagesOptions {
|
||||
page: ContentData
|
||||
template: string
|
||||
outDir: string
|
||||
fonts: SatoriOptions["fonts"]
|
||||
}
|
||||
|
||||
function getDir(url: string) {
|
||||
if (url.startsWith("/manuals/faq/")) {
|
||||
return "FAQ"
|
||||
} else if (url.startsWith("/manuals/guides/")) {
|
||||
return "Guide"
|
||||
} else if (url.startsWith("/news/") && url !== "/news/") {
|
||||
return "News"
|
||||
} else if (url.startsWith("/sandbox/")) {
|
||||
return "Sandbox"
|
||||
} else if (url !== "/manuals/faq/" && url !== "/manuals/guides/" && url !== "/news/" && url !== "/sandbox/") {
|
||||
return "Other"
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
async function generateImage({ page, template, outDir, fonts }: GenerateImagesOptions) {
|
||||
const { frontmatter, url } = page
|
||||
|
||||
const options: SatoriOptions = {
|
||||
width: 1200,
|
||||
height: 628,
|
||||
fonts,
|
||||
props: {
|
||||
title:
|
||||
frontmatter.layout === "home"
|
||||
? frontmatter.main.name ?? frontmatter.title
|
||||
: frontmatter.customMetaTitle ?? frontmatter.title,
|
||||
description:
|
||||
frontmatter.layout === "home"
|
||||
? frontmatter.main.tagline ?? frontmatter.description
|
||||
: frontmatter.description,
|
||||
dir: getDir(url),
|
||||
},
|
||||
}
|
||||
|
||||
const svg = await satoriVue(options, template)
|
||||
|
||||
const render = await renderAsync(svg, {
|
||||
fitTo: {
|
||||
mode: "width",
|
||||
value: 1200,
|
||||
},
|
||||
})
|
||||
|
||||
const outputFolder = resolve(outDir, url.substring(1), "__og_image__")
|
||||
const outputFile = resolve(outputFolder, "og.png")
|
||||
|
||||
await mkdir(outputFolder, { recursive: true })
|
||||
|
||||
return await writeFile(outputFile, render.asPng())
|
||||
}
|
||||
130
.vitepress/config/index.ts
Normal file
130
.vitepress/config/index.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import type { Theme } from '../theme/types'
|
||||
import type { DocsPageData } from '../theme/plugins/component'
|
||||
import { type HeadConfig, defineConfigWithTheme } from 'vitepress'
|
||||
import { config as ru, searchLocale as searchLocaleRu } from './ru'
|
||||
import { config as root, searchLocale as searchLocaleEn } from './en'
|
||||
import { config as ua, searchLocale as searchLocaleUa } from './ua'
|
||||
import { addPlugins } from '../theme/plugins/markdown'
|
||||
import { components, prepareData } from '../theme/plugins/component'
|
||||
import { slugify } from 'transliteration'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
import { telegram } from '../../website/icons'
|
||||
import { normalize } from 'vitepress/dist/client/shared'
|
||||
import { tabsMarkdownPlugin } from "vitepress-plugin-tabs"
|
||||
|
||||
import shortcode_plugin from "markdown-it-shortcode-tag"
|
||||
import shortcodes from "./shortcodes"
|
||||
|
||||
import generateOgImages from "./hooks/generateOgImages"
|
||||
import generateMeta from "./hooks/generateMeta"
|
||||
|
||||
const SITE_HOST = 'http://86.57.183.214:4173'
|
||||
const SITE_TITLE = 'kotatsu.app'
|
||||
const SITE_TITLE_SEPARATOR = ' / '
|
||||
|
||||
export default defineConfigWithTheme<Theme.Config>({
|
||||
lastUpdated: true,
|
||||
cleanUrls: true,
|
||||
|
||||
title: SITE_TITLE,
|
||||
titleTemplate: ':title' + SITE_TITLE_SEPARATOR + SITE_TITLE,
|
||||
srcDir: './website',
|
||||
|
||||
markdown: {
|
||||
theme: {
|
||||
light: 'github-light',
|
||||
dark: 'one-dark-pro',
|
||||
},
|
||||
|
||||
anchor: {
|
||||
slugify(str) {
|
||||
str = str.trim()
|
||||
.replace(/^\d*/g, '') // Удаление чисел из начала строки
|
||||
.replace(/[^a-zA-Zа-яА-ЯЁё0-9\-\s]/g, '') // Удаление ненужных символов
|
||||
.replace(/\s\-\s/, '-').replace(/\-+/g, '-') // Избавление от повторяющихся символов
|
||||
.replace(/^(.{25}[^\s]*).*/, '$1') // Ограничение количества символов
|
||||
|
||||
return encodeURIComponent(slugify(str, { lowercase: true }))
|
||||
}
|
||||
},
|
||||
|
||||
config (md) {
|
||||
addPlugins(md)
|
||||
md.use(tabsMarkdownPlugin)
|
||||
md.use(shortcode_plugin, shortcodes)
|
||||
},
|
||||
},
|
||||
|
||||
head: [
|
||||
['link', { rel: 'preconnect', href: 'https://fonts.googleapis.com' }],
|
||||
['link', { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' }],
|
||||
['link', { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap' }],
|
||||
|
||||
['link', { rel: 'icon', href: '/favicon.ico?v=2', sizes: 'any' }],
|
||||
['link', { rel: 'icon', href: '/icon.svg?v=2', type: 'image/svg+xml' }],
|
||||
['link', { rel: 'apple-touch-icon', href: '/apple-touch-icon.png?v=2' }],
|
||||
['link', { rel: 'manifest', href: '/site.webmanifest' }],
|
||||
],
|
||||
|
||||
themeConfig: {
|
||||
titleSeparator: SITE_TITLE_SEPARATOR,
|
||||
i18nRouting: true,
|
||||
|
||||
logo: {
|
||||
light: '/logo.svg',
|
||||
dark: '/logo-dark.svg',
|
||||
},
|
||||
|
||||
socialLinks: [
|
||||
{
|
||||
icon: { svg: telegram },
|
||||
link: 'https://t.me/kotatsuapp',
|
||||
},
|
||||
{ icon: 'github', link: 'https://github.com/KotatsuApp/Kotatsu' },
|
||||
],
|
||||
|
||||
search: {
|
||||
provider: 'local',
|
||||
},
|
||||
|
||||
components,
|
||||
},
|
||||
|
||||
locales: {
|
||||
...root,
|
||||
...ru,
|
||||
...ua,
|
||||
},
|
||||
|
||||
transformPageData(pageData, { siteConfig }) {
|
||||
return prepareData(pageData, siteConfig)
|
||||
},
|
||||
|
||||
transformHead: async (context) => generateMeta(context, SITE_HOST),
|
||||
|
||||
vite: {
|
||||
resolve: {
|
||||
alias: [
|
||||
'VPSidebar',
|
||||
'VPNavBarTranslations',
|
||||
'VPNavScreenTranslations',
|
||||
'VPNavBar',
|
||||
'VPNavBarMenu',
|
||||
'VPNavScreenMenu'
|
||||
].map(componentName => ({
|
||||
find: new RegExp(`^.*\/${componentName}\.vue$`),
|
||||
replacement: fileURLToPath(
|
||||
new URL(`../theme/components/${componentName.replace(/^VP/, '')}.vue`, import.meta.url)
|
||||
)
|
||||
})),
|
||||
},
|
||||
},
|
||||
|
||||
sitemap: {
|
||||
hostname: SITE_HOST,
|
||||
},
|
||||
|
||||
buildEnd: async (context) => {
|
||||
generateOgImages(context)
|
||||
}
|
||||
})
|
||||
111
.vitepress/config/ru.ts
Normal file
111
.vitepress/config/ru.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import type { DefaultTheme, LocaleConfig } from 'vitepress'
|
||||
import type { Theme } from '../theme/types'
|
||||
import { components } from '../theme/plugins/component'
|
||||
import 'dotenv/config'
|
||||
|
||||
export const SITE_NAME = 'Kotatsu Website'
|
||||
export const META_DESCRIPTION = 'Простая и удобная читалка манги с открытым исходным кодом от сообщества и для сообщества, где вы можете найти и читать вашу любимую мангу проще как никогда.'
|
||||
|
||||
export const searchLocale: Record<string, Partial<Omit<DefaultTheme.LocalSearchOptions, 'locales'>>> = {
|
||||
ru: {
|
||||
translations: {
|
||||
button: {
|
||||
buttonText: 'Поиск',
|
||||
buttonAriaLabel: 'Поиск'
|
||||
},
|
||||
modal: {
|
||||
resetButtonTitle: 'Сбросить',
|
||||
backButtonTitle: 'Закрыть',
|
||||
noResultsText: 'Нет результатов по запросу',
|
||||
footer: {
|
||||
selectText: 'для выбора',
|
||||
selectKeyAriaLabel: 'enter',
|
||||
navigateText: 'для навигации',
|
||||
navigateUpKeyAriaLabel: 'стрелка вверх',
|
||||
navigateDownKeyAriaLabel: 'стрелка вниз',
|
||||
closeText: 'закрыть',
|
||||
closeKeyAriaLabel: 'escape'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const config: LocaleConfig<Theme.Config> = {
|
||||
ru: {
|
||||
label: 'Русский',
|
||||
lang: 'ru',
|
||||
description: META_DESCRIPTION,
|
||||
|
||||
head: [
|
||||
['meta', { property: 'og:site_name', content: SITE_NAME }],
|
||||
],
|
||||
|
||||
themeConfig: {
|
||||
nav: getNav(),
|
||||
|
||||
sidebar: {
|
||||
|
||||
},
|
||||
|
||||
outline: {
|
||||
label: 'На этой странице',
|
||||
level: 'deep',
|
||||
},
|
||||
|
||||
returnToTopLabel: 'Наверх',
|
||||
sidebarMenuLabel: 'Меню',
|
||||
darkModeSwitchLabel: 'Тема',
|
||||
langMenuLabel: 'Изменить язык',
|
||||
teamSectionTitle: 'Команда',
|
||||
|
||||
lastUpdated: {
|
||||
text: 'Последнее обновление',
|
||||
},
|
||||
|
||||
docFooter: {
|
||||
prev: 'Предыдущая страница',
|
||||
next: 'Следующая страница',
|
||||
},
|
||||
editLink: {
|
||||
pattern: 'https://github.com/KotatsuApp/kotatsuapp.github.io/edit/master/website/:path',
|
||||
text: 'Предложить изменения на этой странице',
|
||||
},
|
||||
footer: {
|
||||
message: "<a href=\"https://www.gnu.org/licenses/gpl-3.0.en.html\" target=\"_blank\">Лицензия GPL-3.0</a> <span class=\"divider\">|</span> <a href=\"/privacy/\">Политика конфиденциальности</a>",
|
||||
copyright: `Copyright © 2020 - ${new Date().getFullYear()} Kotatsu Developers`,
|
||||
},
|
||||
|
||||
components: components.filter(component => component.path.startsWith('ru/components/')),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function getNav(): DefaultTheme.NavItem[] {
|
||||
return [
|
||||
{
|
||||
text: 'v{app_version}',
|
||||
activeMatch: "^/*?(download|changelogs)/*?$",
|
||||
items: [
|
||||
{
|
||||
text: "Скачать",
|
||||
link: "/ru/download/",
|
||||
},
|
||||
{
|
||||
text: "Изменения в версиях",
|
||||
link: "/ru/changelogs/",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Инструкции",
|
||||
link: "/ru/manuals/guides/getting-started",
|
||||
activeMatch: "/ru/manuals/",
|
||||
},
|
||||
{
|
||||
text: "Новости",
|
||||
link: "/ru/news/",
|
||||
activeMatch: "/ru/news/",
|
||||
},
|
||||
]
|
||||
}
|
||||
89
.vitepress/config/shortcodes.ts
Normal file
89
.vitepress/config/shortcodes.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
const iconMappings = {
|
||||
about: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>information-outline</title><path d="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z" /></svg>',
|
||||
bookmark: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>bookmark-outline</title><path d="M17,18L12,15.82L7,18V5H17M17,3H7A2,2 0 0,0 5,5V21L12,18L19,21V5C19,3.89 18.1,3 17,3Z" /></svg>',
|
||||
bookPage: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 1l-5 5v11l5-4.5V1m2 4v13.5c-1.1-0.35-2.3-0.5-3.5-0.5-1.7 0-4.15 0.65-5.5 1.5V6c-1.45-1.1-3.55-1.5-5.5-1.5C4.55 4.5 2.45 4.9 1 6v14.65c0 0.25 0.25 0.5 0.5 0.5 0.1 0 0.15-0.05 0.25-0.05C3.1 20.45 5.05 20 6.5 20c1.95 0 4.05 0.4 5.5 1.5 1.35-0.85 3.8-1.5 5.5-1.5 1.65 0 3.35 0.3 4.75 1.05 0.1 0.05 0.15 0.05 0.25 0.05 0.25 0 0.5-0.25 0.5-0.5V6c-0.6-0.45-1.25-0.75-2-1M10 18.41C8.75 18.09 7.5 18 6.5 18c-1.06 0-2.32 0.19-3.5 0.5V7.13C3.91 6.73 5.14 6.5 6.5 6.5c1.36 0 2.59 0.23 3.5 0.63v11.28z" /></svg>',
|
||||
compassOutline: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7,17L10.2,10.2L17,7L13.8,13.8L7,17M12,11.1A0.9,0.9 0 0,0 11.1,12A0.9,0.9 0 0,0 12,12.9A0.9,0.9 0 0,0 12.9,12A0.9,0.9 0 0,0 12,11.1M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z" /></svg>',
|
||||
dataPrivacy: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17 14.4C17.6 14.4 18.1 14.9 18.1 15.5S17.6 16.6 17 16.6 15.9 16.1 15.9 15.5 16.4 14.4 17 14.4M17 17.5C16.3 17.5 14.8 17.9 14.8 18.6C15.3 19.3 16.1 19.8 17 19.8S18.7 19.3 19.2 18.6C19.2 17.9 17.7 17.5 17 17.5M18 11.1V6.3L10.5 3L3 6.3V11.2C3 15.7 6.2 20 10.5 21C11.1 20.9 11.6 20.7 12.1 20.5C13.2 22 15 23 17 23C20.3 23 23 20.3 23 17C23 14 20.8 11.6 18 11.1M11 17C11 17.6 11.1 18.1 11.2 18.6C11 18.7 10.7 18.8 10.5 18.9C7.3 17.9 5 14.7 5 11.2V7.6L10.5 5.2L16 7.6V11.1C13.2 11.6 11 14 11 17M17 21C14.8 21 13 19.2 13 17S14.8 13 17 13 21 14.8 21 17 19.2 21 17 21Z" /></svg>',
|
||||
dice: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>dice-5-outline</title><path d="M19 5V19H5V5H19M19 3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3M7.5 6C6.7 6 6 6.7 6 7.5S6.7 9 7.5 9 9 8.3 9 7.5 8.3 6 7.5 6M16.5 15C15.7 15 15 15.7 15 16.5C15 17.3 15.7 18 16.5 18C17.3 18 18 17.3 18 16.5C18 15.7 17.3 15 16.5 15M16.5 6C15.7 6 15 6.7 15 7.5S15.7 9 16.5 9C17.3 9 18 8.3 18 7.5S17.3 6 16.5 6M12 10.5C11.2 10.5 10.5 11.2 10.5 12S11.2 13.5 12 13.5 13.5 12.8 13.5 12 12.8 10.5 12 10.5M7.5 15C6.7 15 6 15.7 6 16.5C6 17.3 6.7 18 7.5 18S9 17.3 9 16.5C9 15.7 8.3 15 7.5 15Z" /></svg>',
|
||||
dotsHorizontal: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z" /></svg>',
|
||||
downloadOutline: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13,5V11H14.17L12,13.17L9.83,11H11V5H13M15,3H9V9H5L12,16L19,9H15V3M19,18H5V20H19V18Z" /></svg>',
|
||||
favourite: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>heart-outline</title><path d="M12.1,18.55L12,18.65L11.89,18.55C7.14,14.24 4,11.39 4,8.5C4,6.5 5.5,5 7.5,5C9.04,5 10.54,6 11.07,7.36H12.93C13.46,6 14.96,5 16.5,5C18.5,5 20,6.5 20,8.5C20,11.39 16.86,14.24 12.1,18.55M16.5,3C14.76,3 13.09,3.81 12,5.08C10.91,3.81 9.24,3 7.5,3C4.42,3 2,5.41 2,8.5C2,12.27 5.4,15.36 10.55,20.03L12,21.35L13.45,20.03C18.6,15.36 22,12.27 22,8.5C22,5.41 19.58,3 16.5,3Z" /></svg>',
|
||||
feed: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>rss</title><path d="M6.18,15.64A2.18,2.18 0 0,1 8.36,17.82C8.36,19 7.38,20 6.18,20C5,20 4,19 4,17.82A2.18,2.18 0 0,1 6.18,15.64M4,4.44A15.56,15.56 0 0,1 19.56,20H16.73A12.73,12.73 0 0,0 4,7.27V4.44M4,10.1A9.9,9.9 0 0,1 13.9,20H11.07A7.07,7.07 0 0,0 4,12.93V10.1Z" /></svg>',
|
||||
history: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13.5,8H12V13L16.28,15.54L17,14.33L13.5,12.25V8M13,3A9,9 0 0,0 4,12H1L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3" /></svg>',
|
||||
local: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18 4v16H6V8.83L10.83 4H18m0-2h-8L4 8v12c0 1.1 0.9 2 2 2h12c1.1 0 2-0.9 2-2V4c0-1.1-0.9-2-2-2zM9 7h2v4H9zm3 0h2v4h-2zm3 0h2v4h-2z" /></svg>',
|
||||
paletteOutline: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2C17.5,2 22,6 22,11A6,6 0 0,1 16,17H14.2C13.9,17 13.7,17.2 13.7,17.5C13.7,17.6 13.8,17.7 13.8,17.8C14.2,18.3 14.4,18.9 14.4,19.5C14.5,20.9 13.4,22 12,22M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C12.3,20 12.5,19.8 12.5,19.5C12.5,19.3 12.4,19.2 12.4,19.1C12,18.6 11.8,18.1 11.8,17.5C11.8,16.1 12.9,15 14.3,15H16A4,4 0 0,0 20,11C20,7.1 16.4,4 12,4M6.5,10C7.3,10 8,10.7 8,11.5C8,12.3 7.3,13 6.5,13C5.7,13 5,12.3 5,11.5C5,10.7 5.7,10 6.5,10M9.5,6C10.3,6 11,6.7 11,7.5C11,8.3 10.3,9 9.5,9C8.7,9 8,8.3 8,7.5C8,6.7 8.7,6 9.5,6M14.5,6C15.3,6 16,6.7 16,7.5C16,8.3 15.3,9 14.5,9C13.7,9 13,8.3 13,7.5C13,6.7 13.7,6 14.5,6M17.5,10C18.3,10 19,10.7 19,11.5C19,12.3 18.3,13 17.5,13C16.7,13 16,12.3 16,11.5C16,10.7 16.7,10 17.5,10Z" /></svg>',
|
||||
sources: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 20h14v2H4c-1.1 0-2-0.9-2-2V6h2v14M22 4v12c0 1.1-0.9 2-2 2H8c-1.1 0-2-0.9-2-2V4c0-1.1 0.9-2 2-2h12c1.1 0 2 0.9 2 2m-2 0H8v12h12V4m-2 2h-5v7l2.5-1.5L18 13V6z" /></svg>',
|
||||
services: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M22,13.5C22,15.26 20.7,16.72 19,16.96V20A2,2 0 0,1 17,22H13.2V21.7A2.7,2.7 0 0,0 10.5,19C9,19 7.8,20.21 7.8,21.7V22H4A2,2 0 0,1 2,20V16.2H2.3C3.79,16.2 5,15 5,13.5C5,12 3.79,10.8 2.3,10.8H2V7A2,2 0 0,1 4,5H7.04C7.28,3.3 8.74,2 10.5,2C12.26,2 13.72,3.3 13.96,5H17A2,2 0 0,1 19,7V10.04C20.7,10.28 22,11.74 22,13.5M17,15H18.5A1.5,1.5 0 0,0 20,13.5A1.5,1.5 0 0,0 18.5,12H17V7H12V5.5A1.5,1.5 0 0,0 10.5,4A1.5,1.5 0 0,0 9,5.5V7H4V9.12C5.76,9.8 7,11.5 7,13.5C7,15.5 5.75,17.2 4,17.88V20H6.12C6.8,18.25 8.5,17 10.5,17C12.5,17 14.2,18.25 14.88,20H17V15Z" /></svg>',
|
||||
threeDots: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>dots-vertical</title><path d="M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z" /></svg>',
|
||||
web: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm6.93 6h-2.95c-0.32-1.25-0.78-2.45-1.38-3.56 1.84 0.63 3.37 1.91 4.33 3.56zM12 4.04c0.83 1.2 1.48 2.53 1.91 3.96h-3.82c0.43-1.43 1.08-2.76 1.91-3.96zM4.26 14C4.1 13.36 4 12.69 4 12s0.1-1.36 0.26-2h3.38c-0.08 0.66-0.14 1.32-0.14 2 0 0.68 0.06 1.34 0.14 2H4.26zm0.82 2h2.95c0.32 1.25 0.78 2.45 1.38 3.56-1.84-0.63-3.37-1.9-4.33-3.56zm2.95-8H5.08c0.96-1.66 2.49-2.93 4.33-3.56C8.81 5.55 8.35 6.75 8.03 8zM12 19.96c-0.83-1.2-1.48-2.53-1.91-3.96h3.82c-0.43 1.43-1.08 2.76-1.91 3.96zM14.34 14H9.66c-0.09-0.66-0.16-1.32-0.16-2 0-0.68 0.07-1.35 0.16-2h4.68c0.09 0.65 0.16 1.32 0.16 2 0 0.68-0.07 1.34-0.16 2zm0.25 5.56c0.6-1.11 1.06-2.31 1.38-3.56h2.95c-0.96 1.65-2.49 2.93-4.33 3.56zM16.36 14c0.08-0.66 0.14-1.32 0.14-2 0-0.68-0.06-1.34-0.14-2h3.38C19.9 10.64 20 11.31 20 12s-0.1 1.36-0.26 2h-3.38z" /></svg>'
|
||||
}
|
||||
|
||||
interface Navigation {
|
||||
name: string
|
||||
icon?: string
|
||||
dependsOn?: string
|
||||
}
|
||||
|
||||
const navigationMappings: Record<string, Navigation> = {
|
||||
// Main menus
|
||||
"main_three_dots": { name: "Three dots", icon: iconMappings.threeDots },
|
||||
"main_history": { name: "History", icon: iconMappings.history },
|
||||
"main_favourites": { name: "Favourites", icon: iconMappings.favourite },
|
||||
"main_explore": { name: "Explore", icon: iconMappings.compassOutline },
|
||||
"main_feed": { name: "Feed", icon: iconMappings.feed },
|
||||
"main_settings": { name: "Settings", dependsOn: "main_three_dots" },
|
||||
|
||||
// Explore
|
||||
"explore_local_storage" : { name: "Local storage", icon: iconMappings.local, dependsOn: "main_explore" },
|
||||
"explore_bookmarks" : { name: "Bookmarks", icon: iconMappings.bookmark, dependsOn: "main_explore" },
|
||||
"explore_random" : { name: "Random", icon: iconMappings.dice, dependsOn: "main_explore" },
|
||||
"explore_downloads" : { name: "Downloads", icon: iconMappings.downloadOutline, dependsOn: "main_explore" },
|
||||
"explore_manage_sources" : { name: "Manage", dependsOn: "main_explore" },
|
||||
|
||||
// Details
|
||||
"details_tracking" : { name: "Tracking", dependsOn: "main_three_dots" },
|
||||
|
||||
// Favourites
|
||||
"favourites_three_dots": { name: "Three dots", icon: iconMappings.threeDots, dependsOn: "main_favourites" },
|
||||
"favourites_manage_categories" : { name: "Manage categories", dependsOn: "favourites_three_dots" },
|
||||
|
||||
// Settings submenu
|
||||
"appearance": { name: "Appearance", icon: iconMappings.paletteOutline, dependsOn: "main_settings" },
|
||||
"sources": { name: "Manga sources", icon: iconMappings.sources, dependsOn: "main_settings" },
|
||||
"reader": { name: "Reader settings", icon: iconMappings.bookPage, dependsOn: "main_settings" },
|
||||
"network": { name: "Network", icon: iconMappings.web, dependsOn: "main_settings" },
|
||||
"data": { name: "Data and privacy", icon: iconMappings.dataPrivacy, dependsOn: "main_settings" },
|
||||
"downloads": { name: "Downloads", icon: iconMappings.downloadOutline, dependsOn: "main_settings" },
|
||||
"checking": { name: "Check for new chapters", icon: iconMappings.feed, dependsOn: "main_settings" },
|
||||
"services": { name: "Services", icon: iconMappings.services, dependsOn: "main_settings" },
|
||||
"about": { name: "About", icon: iconMappings.about, dependsOn: "main_settings" },
|
||||
}
|
||||
|
||||
function generateNavigationHtml(navKey: string) {
|
||||
const navData = navigationMappings[navKey]
|
||||
|
||||
if (!navData) {
|
||||
return "<strong style='color:var(--vp-c-danger-1)'>Unsupported Navigation!</strong>"
|
||||
}
|
||||
|
||||
const { name, icon, dependsOn } = navData
|
||||
|
||||
const iconHtml = icon ?? ""
|
||||
let html = `<span class='shortcode navigation ${navKey}'>${iconHtml}<span class="name">${name}</span></span>`
|
||||
|
||||
if (dependsOn) {
|
||||
html = `${generateNavigationHtml(dependsOn)} <span class='shortcode navigation direction'><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>arrow-right-thin</title><path d="M14 16.94V12.94H5.08L5.05 10.93H14V6.94L19 11.94Z" /></svg></span> ${html}`
|
||||
}
|
||||
|
||||
return html
|
||||
}
|
||||
|
||||
const shortcodes = {
|
||||
nav: {
|
||||
render({ to }) {
|
||||
return generateNavigationHtml(to)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default shortcodes
|
||||
109
.vitepress/config/ua.ts
Normal file
109
.vitepress/config/ua.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import type { DefaultTheme, LocaleConfig } from 'vitepress'
|
||||
import type { Theme } from '../theme/types'
|
||||
import { components } from '../theme/plugins/component'
|
||||
|
||||
export const SITE_NAME = 'Kotatsu Website'
|
||||
export const META_DESCRIPTION = 'Проста і зручна читалка манги з відкритим вихідним кодом від спільноти і для спільноти, де ви можете знайти і читати вашу улюблену мангу простіше як ніколи.'
|
||||
|
||||
export const searchLocale: Record<string, Partial<Omit<DefaultTheme.LocalSearchOptions, 'locales'>>> = {
|
||||
ru: {
|
||||
translations: {
|
||||
button: {
|
||||
buttonText: 'Пошук',
|
||||
buttonAriaLabel: 'Пошук'
|
||||
},
|
||||
modal: {
|
||||
resetButtonTitle: 'Скинути',
|
||||
backButtonTitle: 'Закрити',
|
||||
noResultsText: 'Немає результатів за запитом',
|
||||
footer: {
|
||||
selectText: 'для вибору',
|
||||
selectKeyAriaLabel: 'enter',
|
||||
navigateText: 'для навігації',
|
||||
navigateUpKeyAriaLabel: 'стрілка вгору',
|
||||
navigateDownKeyAriaLabel: 'стрілка донизу',
|
||||
closeText: 'закрити',
|
||||
closeKeyAriaLabel: 'escape'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const config: LocaleConfig<Theme.Config> = {
|
||||
ua: {
|
||||
label: 'Українська',
|
||||
lang: 'ua',
|
||||
description: META_DESCRIPTION,
|
||||
|
||||
head: [
|
||||
['meta', { property: 'og:site_name', content: SITE_NAME }],
|
||||
],
|
||||
|
||||
themeConfig: {
|
||||
nav: getNav(),
|
||||
|
||||
sidebar: {
|
||||
|
||||
},
|
||||
|
||||
outline: {
|
||||
label: 'На цій сторінці',
|
||||
level: 'deep',
|
||||
},
|
||||
|
||||
returnToTopLabel: 'Наверх',
|
||||
sidebarMenuLabel: 'Меню',
|
||||
darkModeSwitchLabel: 'Тема',
|
||||
langMenuLabel: 'Змінити мову',
|
||||
|
||||
lastUpdated: {
|
||||
text: 'Останнє оновлення',
|
||||
},
|
||||
|
||||
docFooter: {
|
||||
prev: 'Попередня сторінка',
|
||||
next: 'Наступна сторінка',
|
||||
},
|
||||
editLink: {
|
||||
pattern: 'https://github.com/KotatsuApp/kotatsuapp.github.io/edit/master/website/:path',
|
||||
text: 'Запропонувати зміни на цій сторінці',
|
||||
},
|
||||
footer: {
|
||||
message: "<a href=\"https://www.gnu.org/licenses/gpl-3.0.en.html\" target=\"_blank\">Ліцензія GPL-3.0</a> <span class=\"divider\">|</span> <a href=\"/privacy/\">Політика конфіденційності</a>",
|
||||
copyright: `Copyright © 2020 - ${new Date().getFullYear()} Kotatsu Developers`,
|
||||
},
|
||||
|
||||
components: components.filter(component => component.path.startsWith('ru/components/')),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getNav(): DefaultTheme.NavItem[] {
|
||||
return [
|
||||
{
|
||||
text: 'v{app_version}',
|
||||
activeMatch: "^/*?(download|changelogs)/*?$",
|
||||
items: [
|
||||
{
|
||||
text: "Завантажити",
|
||||
link: "/ua/download/",
|
||||
},
|
||||
{
|
||||
text: "Зміни у версіях",
|
||||
link: "/ua/changelogs/",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Інструкції",
|
||||
link: "/ua/manuals/guides/getting-started",
|
||||
activeMatch: "/ua/manuals/",
|
||||
},
|
||||
{
|
||||
text: "Новини",
|
||||
link: "/ua/news/",
|
||||
activeMatch: "/ua/news/",
|
||||
},
|
||||
]
|
||||
}
|
||||
BIN
.vitepress/fonts/Montserrat-Bold.ttf
Normal file
BIN
.vitepress/fonts/Montserrat-Bold.ttf
Normal file
Binary file not shown.
BIN
.vitepress/fonts/Montserrat-Medium.ttf
Normal file
BIN
.vitepress/fonts/Montserrat-Medium.ttf
Normal file
Binary file not shown.
BIN
.vitepress/fonts/Montserrat-Regular.ttf
Normal file
BIN
.vitepress/fonts/Montserrat-Regular.ttf
Normal file
Binary file not shown.
BIN
.vitepress/fonts/Montserrat-SemiBold.ttf
Normal file
BIN
.vitepress/fonts/Montserrat-SemiBold.ttf
Normal file
Binary file not shown.
BIN
.vitepress/fonts/Roboto-Bold.ttf
Normal file
BIN
.vitepress/fonts/Roboto-Bold.ttf
Normal file
Binary file not shown.
BIN
.vitepress/fonts/Roboto-Medium.ttf
Normal file
BIN
.vitepress/fonts/Roboto-Medium.ttf
Normal file
Binary file not shown.
BIN
.vitepress/fonts/Roboto-Regular.ttf
Normal file
BIN
.vitepress/fonts/Roboto-Regular.ttf
Normal file
Binary file not shown.
106
.vitepress/theme/components/Breadcrumbs.vue
Normal file
106
.vitepress/theme/components/Breadcrumbs.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<script setup lang="ts">
|
||||
import { Ref } from 'vue'
|
||||
import { useData, withBase, useRoute } from 'vitepress'
|
||||
import type { DocsPageData } from '../plugins/component'
|
||||
|
||||
import VPLink from 'vitepress/dist/client/theme-default/components/VPLink.vue'
|
||||
|
||||
const { page }: { page: Ref<DocsPageData> } = useData()
|
||||
const route = useRoute()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ol
|
||||
v-if="page.breadcrumbs.length"
|
||||
class="Breadcrumbs"
|
||||
itemscope itemtype="https://schema.org/BreadcrumbList"
|
||||
>
|
||||
<li
|
||||
v-for="(item, idx) in page.breadcrumbs"
|
||||
:key="idx"
|
||||
itemscope itemprop="itemListElement" itemtype="https://schema.org/ListItem"
|
||||
class="item"
|
||||
:class="{
|
||||
active: item.link && withBase(item.link) === route.path,
|
||||
}"
|
||||
>
|
||||
<VPLink
|
||||
v-if="item.link && idx < page.breadcrumbs.length - 1"
|
||||
:href="item.link"
|
||||
:title="item.text"
|
||||
itemprop="item"
|
||||
class="link"
|
||||
>
|
||||
<span
|
||||
itemprop="name"
|
||||
class="name"
|
||||
v-text="item.text"
|
||||
/>
|
||||
</VPLink>
|
||||
<template v-else>
|
||||
<span
|
||||
:title="item.text"
|
||||
itemprop="name"
|
||||
class="name"
|
||||
v-text="item.text"
|
||||
/>
|
||||
<link
|
||||
v-if="item.link"
|
||||
:href="item.link"
|
||||
itemprop="item"
|
||||
>
|
||||
</template>
|
||||
<meta
|
||||
itemprop="position"
|
||||
:content="idx.toString()"
|
||||
>
|
||||
</li>
|
||||
</ol>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.Breadcrumbs {
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
column-gap: 10px;
|
||||
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 10px;
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.item {
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.item:last-child {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.item + .item::before {
|
||||
content: '/';
|
||||
font-weight: normal;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.link {
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-1);
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.name {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
136
.vitepress/theme/components/Button.vue
Normal file
136
.vitepress/theme/components/Button.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { normalizeLink } from 'vitepress/dist/client/theme-default/support/utils'
|
||||
import { EXTERNAL_URL_RE } from 'vitepress/dist/client/shared'
|
||||
|
||||
const props = defineProps<{
|
||||
tag?: string
|
||||
theme?: 'brand' | 'alt' | 'sponsor'
|
||||
text: string
|
||||
href?: string
|
||||
target?: string
|
||||
}>()
|
||||
|
||||
const classes = computed(() => [
|
||||
props.theme ?? 'brand'
|
||||
])
|
||||
|
||||
const isExternal = computed(() => props.href && EXTERNAL_URL_RE.test(props.href))
|
||||
|
||||
const component = computed(() => {
|
||||
if (props.tag) {
|
||||
return props.tag
|
||||
}
|
||||
|
||||
return props.href ? 'a' : 'button'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="component"
|
||||
class="Button"
|
||||
:class="classes"
|
||||
:href="href ? normalizeLink(href) : undefined"
|
||||
:target="target ?? (isExternal ? '_blank' : undefined)"
|
||||
:rel="isExternal ? 'noreferrer' : undefined"
|
||||
:aria-label="text"
|
||||
>
|
||||
{{ text }}
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.Button {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
border: 4px solid transparent;
|
||||
border-radius: 8px;
|
||||
padding: 8px 18px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
transition: all 0.25s, color 0.25s;
|
||||
}
|
||||
|
||||
.Button.alt {
|
||||
border: 4px solid var(--vp-c-gray-1);
|
||||
}
|
||||
|
||||
.Button.alt:hover {
|
||||
background-color: var(--vp-c-gray-1);
|
||||
color: var(--vp-c-black);
|
||||
}
|
||||
|
||||
.Button.alt:active {
|
||||
border-color: var(--vp-c-black);
|
||||
background-color: var(--vp-c-black);
|
||||
color: var(--vp-c-white);
|
||||
}
|
||||
|
||||
.Button.cta:hover {
|
||||
background-color: var(--vp-c-gray-1);
|
||||
color: var(--vp-c-black);
|
||||
}
|
||||
|
||||
.Button.cta:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
top: calc(50% - 12.5px);
|
||||
left: 12px;
|
||||
}
|
||||
|
||||
.Button.cta.question {
|
||||
padding-left: 45px;
|
||||
}
|
||||
|
||||
.Button.cta.question::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="dodgerblue" class="w-6 h-6"><path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm11.378-3.917c-.89-.777-2.366-.777-3.255 0a.75.75 0 01-.988-1.129c1.454-1.272 3.776-1.272 5.23 0 1.513 1.324 1.513 3.518 0 4.842a3.75 3.75 0 01-.837.552c-.676.328-1.028.774-1.028 1.152v.75a.75.75 0 01-1.5 0v-.75c0-1.279 1.06-2.107 1.875-2.502.182-.088.351-.199.503-.331.83-.727.83-1.857 0-2.584zM12 18a.75.75 0 100-1.5.75.75 0 000 1.5z" clip-rule="evenodd" /></svg>');
|
||||
background-size: 25px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: bottom;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.Button.brand {
|
||||
border-color: var(--vp-button-brand-bg);
|
||||
color: var(--vp-button-brand-active-text);
|
||||
background-color: var(--vp-button-brand-bg);
|
||||
}
|
||||
|
||||
.Button.brand:hover {
|
||||
border-color: var(--vp-button-brand-hover-bg);
|
||||
color: var(--vp-button-brand-active-text);
|
||||
background-color: var(--vp-button-brand-hover-bg);
|
||||
}
|
||||
|
||||
.Button.brand:active {
|
||||
border-color: var(--vp-button-brand-active-bg);
|
||||
color: var(--vp-button-brand-active-text);
|
||||
background-color: var(--vp-button-brand-active-bg);
|
||||
}
|
||||
|
||||
.Button.sponsor {
|
||||
border-color: var(--vp-button-sponsor-border);
|
||||
color: var(--vp-button-sponsor-text);
|
||||
background-color: var(--vp-button-sponsor-bg);
|
||||
}
|
||||
|
||||
.Button.sponsor:hover {
|
||||
border-color: var(--vp-button-sponsor-hover-border);
|
||||
color: var(--vp-button-sponsor-hover-text);
|
||||
background-color: var(--vp-button-sponsor-hover-bg);
|
||||
}
|
||||
|
||||
.Button.sponsor:active {
|
||||
border-color: var(--vp-button-sponsor-active-border);
|
||||
color: var(--vp-button-sponsor-active-text);
|
||||
background-color: var(--vp-button-sponsor-active-bg);
|
||||
}
|
||||
</style>
|
||||
110
.vitepress/theme/components/Changelog.vue
Normal file
110
.vitepress/theme/components/Changelog.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, toRefs } from "vue"
|
||||
import MarkdownIt from "markdown-it"
|
||||
import { type AppRelease, data as release } from "../data/release.data"
|
||||
|
||||
const props = defineProps<{ type: keyof AppRelease }>()
|
||||
const { type } = toRefs(props)
|
||||
|
||||
const md = new MarkdownIt()
|
||||
|
||||
const changelog = computed(() => {
|
||||
const flavoredString = (release[type.value].body ?? "")
|
||||
.replace(/(?<=\(|(, ))@(.*?)(?=\)|(, ))/g, "[@$2](https://github.com/$2)")
|
||||
.replace("https://github.com/KotatsuApp/Kotatsu/releases", "/changelogs/")
|
||||
.replace("##", "-")
|
||||
|
||||
return md.render(flavoredString)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="changelog">
|
||||
<header>
|
||||
<h2>Changelog</h2>
|
||||
</header>
|
||||
<div v-html="changelog" />
|
||||
</div>
|
||||
<div class="fullChangelog">
|
||||
<p>
|
||||
View the full release
|
||||
<a href="https://github.com/KotatsuApp/Kotatsu/releases/latest" target="_blank" rel="noopener">
|
||||
here
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="stylus">
|
||||
.changelog {
|
||||
display: block
|
||||
border: 1px solid var(--vp-c-bg-soft)
|
||||
border-radius: 12px
|
||||
background-color: var(--vp-c-bg-soft)
|
||||
transition: border-color 0.25s, background-color 0.25s
|
||||
padding: 24px
|
||||
height: 100%
|
||||
margin: 1.5em auto 0.5em
|
||||
|
||||
header {
|
||||
display: flex
|
||||
justify-content: center
|
||||
align-items: baseline
|
||||
margin: 0 0 1rem
|
||||
}
|
||||
|
||||
svg {
|
||||
font-size: 1.2em
|
||||
margin-right: 0.5rem
|
||||
vertical-align: middle
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem
|
||||
margin: 0
|
||||
padding: 0
|
||||
color: var(--vp-c-text-1)
|
||||
border: none
|
||||
}
|
||||
|
||||
div > p {
|
||||
margin: 0 0 1rem
|
||||
color: var(--vp-c-text-2)
|
||||
font-size: 0.9rem
|
||||
}
|
||||
|
||||
table {
|
||||
border-radius: 8px
|
||||
border-collapse: collapse
|
||||
border: 1px solid var(--vp-c-divider)
|
||||
|
||||
tr,
|
||||
th,
|
||||
td {
|
||||
border: none
|
||||
width: 100%
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
border-top: 1px solid var(--vp-c-divider)
|
||||
}
|
||||
|
||||
tr > td {
|
||||
&:first-child {
|
||||
color: var(--vp-c-text-2)
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
font-family: var(--vp-font-family-mono)
|
||||
font-size: var(--vp-code-font-size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fullChangelog {
|
||||
margin: 0 0 1rem
|
||||
color: var(--vp-c-text-2)
|
||||
font-size: 0.9rem
|
||||
}
|
||||
</style>
|
||||
64
.vitepress/theme/components/ChangelogsList.vue
Normal file
64
.vitepress/theme/components/ChangelogsList.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import MarkdownIt from "markdown-it"
|
||||
import { data as changelogs } from "../data/changelogs.data"
|
||||
import Contributors from "./Contributors.vue"
|
||||
|
||||
const md = new MarkdownIt()
|
||||
|
||||
function renderMarkdown(string: string | null | undefined) {
|
||||
const body = string ?? "No changelog provided."
|
||||
const flavoredString = body
|
||||
.split(/---\r\n\r\n### Checksums|---\r\n\r\nMD5/)[0]
|
||||
.replace(/(?<=\(|(, ))@(.*?)(?=\)|(, ))/g, "[@$2](https://github.com/$2)")
|
||||
.replace(/#(\d+)/g, "[#$1](https://github.com/KotatsuApp/Kotatsu/issues/$1)")
|
||||
.replace(/^Check out the .*past release notes.* if you're.*$/m, "")
|
||||
.replace(/https\:\/\/github.com\/KotatsuApp\/Kotatsu\/releases\/tag\/(.*?)/g, "#$1")
|
||||
.trim()
|
||||
|
||||
return md.render(flavoredString)
|
||||
}
|
||||
|
||||
const dateFormatter = new Intl.DateTimeFormat("en", {
|
||||
dateStyle: "medium",
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-for="(release, index) of changelogs"
|
||||
:key="release.tag_name"
|
||||
>
|
||||
<h2 :id="index === 0 ? 'latest' : release.tag_name">
|
||||
<a
|
||||
:href="release.html_url"
|
||||
target="_blank"
|
||||
>
|
||||
{{ release.tag_name.substring(1) }}
|
||||
</a>
|
||||
<Badge v-if="index === 0" type="tip" text="Latest" />
|
||||
<a
|
||||
class="header-anchor"
|
||||
:href="index === 0 ? '#latest' : `#${release.tag_name}`"
|
||||
:aria-label="`Permalink to "${release.tag_name}"`"
|
||||
/>
|
||||
</h2>
|
||||
<time :datetime="release.published_at!">
|
||||
{{ dateFormatter.format(new Date(release.published_at!)) }}
|
||||
</time>
|
||||
<div v-html="renderMarkdown(release.body)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
h2 {
|
||||
margin-bottom: 0
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 0.5rem
|
||||
}
|
||||
|
||||
time {
|
||||
font-size: 0.875rem
|
||||
color: var(--vp-c-text-2)
|
||||
}
|
||||
</style>
|
||||
108
.vitepress/theme/components/Contributors.vue
Normal file
108
.vitepress/theme/components/Contributors.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, toRefs } from "vue"
|
||||
|
||||
const props = defineProps<{ body: string; author: string; tag: string }>()
|
||||
const { body, author, tag } = toRefs(props)
|
||||
|
||||
function isHigherThan(tagName: string, reference: string) {
|
||||
return reference.localeCompare(tagName, undefined, { numeric: true, sensitivity: "base" }) >= 0
|
||||
}
|
||||
|
||||
const notMentioned = computed(() => {
|
||||
return isHigherThan("v0.1", tag.value) ? ["Koitharu"] : []
|
||||
})
|
||||
|
||||
const nonExistent = ref<string[]>([])
|
||||
|
||||
const contributors = computed(() => {
|
||||
const list = [...body.value.matchAll(/(?<=\(|(, ))@(.*?)(?=\)|(, ))/g)]
|
||||
.map((match) => match[2])
|
||||
const uncredited = author.value.includes("[bot]")
|
||||
? notMentioned.value
|
||||
: [author.value, ...notMentioned.value]
|
||||
|
||||
return [...new Set([...uncredited, ...list])].filter((user) => !nonExistent.value.includes(user))
|
||||
})
|
||||
|
||||
const listFormatter = new Intl.ListFormat("en", {
|
||||
style: "long",
|
||||
type: "conjunction",
|
||||
})
|
||||
|
||||
const contributorsText = computed(() => {
|
||||
if (contributors.value.length <= 3) {
|
||||
return listFormatter.format(contributors.value)
|
||||
}
|
||||
|
||||
return listFormatter.format([
|
||||
...contributors.value.slice(0, 2),
|
||||
`${contributors.value.length - 2} other contributors`,
|
||||
])
|
||||
})
|
||||
|
||||
function addToNonExistent(user: string) {
|
||||
if (!nonExistent.value.includes(user)) {
|
||||
nonExistent.value.push(user)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="contributors.length > 0" class="contributors">
|
||||
<h3>Contributors</h3>
|
||||
<ul>
|
||||
<li
|
||||
v-for="contributor of contributors"
|
||||
:key="contributor"
|
||||
>
|
||||
<a
|
||||
:href="`https://github.com/${contributor}`"
|
||||
target="_blank"
|
||||
:title="`${contributor} profile on GitHub`"
|
||||
:aria-label="`${contributor} profile on GitHub`"
|
||||
>
|
||||
<img
|
||||
:src="`https://github.com/${contributor}.png?size=32`"
|
||||
:alt="`@${contributor} profile picture`"
|
||||
loading="lazy"
|
||||
class="avatar"
|
||||
@error="addToNonExistent(contributor)"
|
||||
>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="names">
|
||||
{{ contributorsText }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.contributors {
|
||||
ul {
|
||||
display: flex
|
||||
align-items: center
|
||||
flex-wrap: wrap
|
||||
gap: 0.5rem
|
||||
list-style-type: none
|
||||
padding-left: 0
|
||||
|
||||
li + li {
|
||||
margin-top: 0
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 32px
|
||||
height: 32px
|
||||
border-radius: 50%
|
||||
box-shadow: var(--vp-shadow-1)
|
||||
border: 1px solid var(--vp-c-divider)
|
||||
}
|
||||
|
||||
.names {
|
||||
font-size: 0.875rem
|
||||
color: var(--vp-c-text-2)
|
||||
}
|
||||
}
|
||||
</style>
|
||||
159
.vitepress/theme/components/DownloadButtons.vue
Normal file
159
.vitepress/theme/components/DownloadButtons.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<script setup lang="ts">
|
||||
/// <reference types="@types/gtag.js" />
|
||||
|
||||
import { computed, onMounted, ref } from "vue"
|
||||
import { data as release } from "../data/release.data"
|
||||
|
||||
const downloadInformation = computed(() => ({
|
||||
stable: {
|
||||
tagName: release.stable.tag_name ?? "v0.0.0",
|
||||
asset: (release.stable.assets ?? [])
|
||||
.find((a) => /^kotatsu-\d+\.\d+\.\d+-release.apk/.test(a.name)),
|
||||
},
|
||||
}))
|
||||
|
||||
const isAndroid = ref(true)
|
||||
|
||||
onMounted(() => {
|
||||
isAndroid.value = !!navigator.userAgent.match(/android/i)
|
||||
})
|
||||
|
||||
function handleAnalytics() {
|
||||
window.gtag?.("event", "Download", {
|
||||
event_category: "App",
|
||||
event_label: "Stable",
|
||||
version: release.stable.tag_name
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="!isAndroid" class="custom-block danger">
|
||||
<p class="custom-block-title">
|
||||
Unsupported operating system
|
||||
</p>
|
||||
<p>
|
||||
<strong>Kotatsu</strong> is an <strong>Android app</strong> only.
|
||||
Use an <strong>Android device</strong> to download and install the app.
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="!isAndroid" class="custom-block warning">
|
||||
<p class="custom-block-title">
|
||||
Caution
|
||||
</p>
|
||||
<p>
|
||||
Any app for any operating systems other than Android called
|
||||
<strong>Kotatsu</strong> (except kotatsu-dl) is not affiliated with this project.
|
||||
</p>
|
||||
<blockquote>
|
||||
For more information, read the
|
||||
<a href="/manuals/faq/general">General FAQ</a>.
|
||||
</blockquote>
|
||||
</div>
|
||||
<div class="download-buttons">
|
||||
<a
|
||||
class="download-button primary"
|
||||
:download="downloadInformation.stable.asset?.name"
|
||||
:href="downloadInformation.stable.asset?.browser_download_url"
|
||||
@click="handleAnalytics()"
|
||||
>
|
||||
<span class="text">Stable</span>
|
||||
<span class="version">{{ downloadInformation.stable.tagName }}</span>
|
||||
</a>
|
||||
<a
|
||||
class="download-button secondary"
|
||||
href="https://f-droid.org/packages/org.koitharu.kotatsu/"
|
||||
>
|
||||
<span class="text">Get it on F-Droid</span>
|
||||
</a>
|
||||
</div>
|
||||
<span class="version-disclaimer">
|
||||
Requires <strong>Android 5.0</strong> or higher.
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.download-buttons {
|
||||
display: flex;
|
||||
gap: 0.75em;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0.75em auto;
|
||||
}
|
||||
|
||||
.download-button {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
border: 4px solid transparent;
|
||||
border-radius: 8px;
|
||||
justify-content: center;
|
||||
padding: 8px 18px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
transition: all 0.25s, color 0.25s;
|
||||
}
|
||||
|
||||
.download-button:hover {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.download-button.primary {
|
||||
border-color: var(--vp-button-brand-border);
|
||||
color: var(--vp-button-brand-text);
|
||||
background-color: var(--vp-button-brand-bg);
|
||||
}
|
||||
|
||||
.download-button.primary:hover {
|
||||
border-color: var(--vp-button-brand-hover-border);
|
||||
color: var(--vp-button-brand-hover-text);
|
||||
background-color: var(--vp-button-brand-hover-bg);
|
||||
}
|
||||
|
||||
.download-button.primary:active {
|
||||
border-color: var(--vp-button-brand-active-border);
|
||||
color: var(--vp-button-brand-active-text);
|
||||
background-color: var(--vp-button-brand-active-bg);
|
||||
}
|
||||
|
||||
.download-button.secondary {
|
||||
border-color: var(--vp-button-alt-border);
|
||||
color: var(--vp-button-alt-text);
|
||||
background-color: var(--vp-button-alt-bg);
|
||||
}
|
||||
|
||||
.download-button.secondary:hover {
|
||||
border-color: var(--vp-button-alt-hover-border);
|
||||
color: var(--vp-button-alt-hover-text);
|
||||
background-color: var(--vp-button-alt-hover-bg);
|
||||
}
|
||||
|
||||
.download-button.secondary:active {
|
||||
border-color: var(--vp-button-alt-active-border);
|
||||
color: var(--vp-button-alt-active-text);
|
||||
background-color: var(--vp-button-alt-active-bg);
|
||||
}
|
||||
|
||||
svg {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: 0.5em;
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.version {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.version-disclaimer {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin: 0.75em auto;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
60
.vitepress/theme/components/Feature.vue
Normal file
60
.vitepress/theme/components/Feature.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
icon?: string
|
||||
title: string
|
||||
details?: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="Feature">
|
||||
<article class="box">
|
||||
<article class="iconBox">
|
||||
<h3 class="icon" v-html="icon"></h3>
|
||||
</article>
|
||||
<h2 class="title" v-html="title"></h2>
|
||||
<p v-if="details" class="details" v-html="details"></p>
|
||||
</article>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.Feature {
|
||||
display: block;
|
||||
border: 1px solid var(--vp-c-bg-soft);
|
||||
border-radius: 12px;
|
||||
height: 100%;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
transition: border-color 0.25s, background-color 0.25s;
|
||||
}
|
||||
|
||||
.box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 24px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
line-height: 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.details {
|
||||
flex-grow: 1;
|
||||
padding-top: 8px;
|
||||
line-height: 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.details:deep(a) {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.details:deep(a:hover) {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
112
.vitepress/theme/components/Features.vue
Normal file
112
.vitepress/theme/components/Features.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { DefaultTheme } from 'vitepress/theme-without-fonts'
|
||||
|
||||
import Feature from './Feature.vue'
|
||||
|
||||
export interface Feature {
|
||||
icon?: DefaultTheme.FeatureIcon
|
||||
title: string
|
||||
details: string
|
||||
link?: string
|
||||
linkText?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
features: Feature[]
|
||||
}>()
|
||||
|
||||
const grid = computed(() => {
|
||||
const length = props.features.length
|
||||
|
||||
if (!length) {
|
||||
return
|
||||
} else if (length === 2) {
|
||||
return 'grid-2'
|
||||
} else if (length === 3) {
|
||||
return 'grid-3'
|
||||
} else if (length % 3 === 0) {
|
||||
return 'grid-6'
|
||||
} else if (length > 3) {
|
||||
return 'grid-4'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="features" class="Features">
|
||||
<div class="container">
|
||||
<div class="items">
|
||||
<div
|
||||
v-for="feature in features"
|
||||
:key="feature.title"
|
||||
class="item"
|
||||
:class="[grid]"
|
||||
>
|
||||
<Feature
|
||||
:icon="feature.icon"
|
||||
:title="feature.title"
|
||||
:details="feature.details"
|
||||
:link="feature.link"
|
||||
:link-text="feature.linkText"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.Features {
|
||||
position: relative;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
max-width: 1152px;
|
||||
}
|
||||
|
||||
.items {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: -12px;
|
||||
}
|
||||
|
||||
.item {
|
||||
padding: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.item.grid-2,
|
||||
.item.grid-4,
|
||||
.item.grid-6 {
|
||||
width: calc(100% / 2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.item.grid-2,
|
||||
.item.grid-4 {
|
||||
width: calc(100% / 2);
|
||||
}
|
||||
|
||||
.item.grid-3,
|
||||
.item.grid-6 {
|
||||
width: calc(100% / 3);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.item.grid-4 {
|
||||
width: calc(100% / 4);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.Features {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
24
.vitepress/theme/components/Home.vue
Normal file
24
.vitepress/theme/components/Home.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { useData } from 'vitepress'
|
||||
|
||||
import HomeHero from './HomeHero.vue'
|
||||
|
||||
const { frontmatter: fm } = useData()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="fm.main">
|
||||
<div class="container">
|
||||
<HomeHero :data="fm.main" />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 auto;
|
||||
max-width: 1340px;
|
||||
}
|
||||
</style>
|
||||
143
.vitepress/theme/components/HomeHero.vue
Normal file
143
.vitepress/theme/components/HomeHero.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<script setup lang="ts">
|
||||
import Button from './Button.vue'
|
||||
|
||||
export interface HeroAction {
|
||||
theme?: 'brand' | 'alt'
|
||||
text: string
|
||||
link: string
|
||||
}
|
||||
|
||||
export interface Data {
|
||||
title: string
|
||||
text: string
|
||||
tagline: string
|
||||
actions: HeroAction[]
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
data: Data
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section id="hero">
|
||||
<h1 class="title">
|
||||
<span class="accent">{{ data.title }}</span>
|
||||
</h1>
|
||||
<h2 class="text">
|
||||
{{ data.text }}
|
||||
</h2>
|
||||
<p class="description">
|
||||
{{ data.tagline }}
|
||||
</p>
|
||||
<p class="actions">
|
||||
<p v-for="action in data.actions" :key="action.link" class="action">
|
||||
<Button
|
||||
tag="a"
|
||||
:theme="action.theme"
|
||||
:text="action.text"
|
||||
:href="action.link"
|
||||
/>
|
||||
</p>
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
section {
|
||||
padding: 42px 32px;
|
||||
}
|
||||
|
||||
#hero {
|
||||
padding: 96px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 76px;
|
||||
line-height: 1.25;
|
||||
font-weight: 900;
|
||||
letter-spacing: -1.5px;
|
||||
max-width: 1340px;
|
||||
margin: 0px auto;
|
||||
}
|
||||
|
||||
html:not(.dark) .accent,
|
||||
.dark .title {
|
||||
background: -webkit-linear-gradient(315deg, var(--vp-c-brand-1) 10%, var(--vp-c-brand-3) 50%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 54px;
|
||||
line-height: 1.25;
|
||||
font-weight: 900;
|
||||
letter-spacing: -1.5px;
|
||||
max-width: 1340px;
|
||||
margin: 0px auto;
|
||||
}
|
||||
|
||||
.description {
|
||||
max-width: 1340px;
|
||||
line-height: 1.5;
|
||||
color: var(--vt-c-text-2);
|
||||
transition: color 0.5s;
|
||||
font-size: 22px;
|
||||
margin: 24px auto 40px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: left;
|
||||
margin: -6px;
|
||||
}
|
||||
|
||||
.action {
|
||||
flex-shrink: 0;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
@media (max-width: 1340px) {
|
||||
.tagline {
|
||||
font-size: 64px;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
.description {
|
||||
font-size: 18px;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.tagline {
|
||||
font-size: 48px;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
#hero {
|
||||
padding: 56px 32px;
|
||||
}
|
||||
.description {
|
||||
font-size: 16px;
|
||||
margin: 18px 0 30px;
|
||||
}
|
||||
#highlights h3 {
|
||||
margin-bottom: 0.6em;
|
||||
}
|
||||
#highlights .vt-box {
|
||||
padding: 20px 36px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 370px) {
|
||||
.tagline {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
31
.vitepress/theme/components/Layout.vue
Normal file
31
.vitepress/theme/components/Layout.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import DefaultTheme from 'vitepress/theme-without-fonts'
|
||||
|
||||
import Home from './Home.vue'
|
||||
import TranslatorWidget from './TranslatorWidget.vue'
|
||||
import ScreenAuthorWidget from './ScreenAuthorWidget.vue'
|
||||
import Breadcrumbs from './Breadcrumbs.vue'
|
||||
import NotFound from './NotFound.vue'
|
||||
|
||||
const { Layout } = DefaultTheme
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Layout>
|
||||
<template #home-hero-before>
|
||||
<Home />
|
||||
</template>
|
||||
<template #aside-outline-after>
|
||||
<TranslatorWidget />
|
||||
</template>
|
||||
<template #doc-before>
|
||||
<Breadcrumbs />
|
||||
</template>
|
||||
<template #doc-footer-before>
|
||||
<ScreenAuthorWidget />
|
||||
</template>
|
||||
<template #not-found>
|
||||
<NotFound />
|
||||
</template>
|
||||
</Layout>
|
||||
</template>
|
||||
45
.vitepress/theme/components/List.vue
Normal file
45
.vitepress/theme/components/List.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { DefaultTheme, useRoute, withBase } from 'vitepress'
|
||||
|
||||
import List from './List.vue'
|
||||
import VPLink from 'vitepress/dist/client/theme-default/components/VPLink.vue'
|
||||
|
||||
const route = useRoute()
|
||||
defineProps<{
|
||||
items: DefaultTheme.SidebarItem[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul>
|
||||
<li
|
||||
v-for="(item, key) in items"
|
||||
:key="key"
|
||||
class="item"
|
||||
:class="{ 'has-link': item.link }"
|
||||
>
|
||||
<VPLink
|
||||
v-if="item.link"
|
||||
:href="item.link"
|
||||
:class="{ active: withBase(item.link) === route.path }"
|
||||
>
|
||||
{{ item.text }}
|
||||
</VPLink>
|
||||
<span v-else>
|
||||
{{ item.text }}
|
||||
</span>
|
||||
<List v-if="item.items" :items="item.items" />
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.link {
|
||||
color: var(--vp-c-text-1);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.active {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
</style>
|
||||
232
.vitepress/theme/components/NavBar.vue
Normal file
232
.vitepress/theme/components/NavBar.vue
Normal file
@@ -0,0 +1,232 @@
|
||||
<script lang="ts" setup>
|
||||
import { useWindowScroll } from '@vueuse/core'
|
||||
import { ref, watchPostEffect } from 'vue'
|
||||
import { useData, inBrowser } from 'vitepress'
|
||||
import { useSidebar } from 'vitepress/dist/client/theme-default/composables/sidebar'
|
||||
import VPNavBarTitle from 'vitepress/dist/client/theme-default/components/VPNavBarTitle.vue'
|
||||
import VPNavBarSearch from 'vitepress/dist/client/theme-default/components/VPNavBarSearch.vue'
|
||||
import VPNavBarMenu from 'vitepress/dist/client/theme-default/components/VPNavBarMenu.vue'
|
||||
import VPNavBarTranslations from 'vitepress/dist/client/theme-default/components/VPNavBarTranslations.vue'
|
||||
import VPNavBarAppearance from 'vitepress/dist/client/theme-default/components/VPNavBarAppearance.vue'
|
||||
import VPNavBarSocialLinks from 'vitepress/dist/client/theme-default/components/VPNavBarSocialLinks.vue'
|
||||
import VPNavBarExtra from 'vitepress/dist/client/theme-default/components/VPNavBarExtra.vue'
|
||||
import VPNavBarHamburger from 'vitepress/dist/client/theme-default/components/VPNavBarHamburger.vue'
|
||||
|
||||
defineProps<{
|
||||
isScreenOpen: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'toggle-screen'): void
|
||||
}>()
|
||||
|
||||
const { y } = useWindowScroll({ window: inBrowser && window })
|
||||
const { hasSidebar } = useSidebar()
|
||||
const { frontmatter } = useData()
|
||||
|
||||
const classes = ref<Record<string, boolean>>({})
|
||||
|
||||
watchPostEffect(() => {
|
||||
classes.value = {
|
||||
'has-sidebar': hasSidebar.value,
|
||||
top: frontmatter.value?.layout === 'home' && y.value === 0,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="VPNavBar" :class="classes">
|
||||
<div class="container">
|
||||
<div class="title">
|
||||
<VPNavBarTitle>
|
||||
<template #nav-bar-title-before><slot name="nav-bar-title-before" /></template>
|
||||
<template #nav-bar-title-after><slot name="nav-bar-title-after" /></template>
|
||||
</VPNavBarTitle>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="curtain" />
|
||||
<div class="content-body">
|
||||
<slot name="nav-bar-content-before" />
|
||||
<VPNavBarSearch class="search" />
|
||||
<VPNavBarMenu class="menu" />
|
||||
<VPNavBarTranslations class="translations" />
|
||||
<VPNavBarAppearance class="appearance" />
|
||||
<VPNavBarSocialLinks class="social-links" />
|
||||
<VPNavBarExtra class="extra" />
|
||||
<slot name="nav-bar-content-after" />
|
||||
<VPNavBarHamburger class="hamburger" :active="isScreenOpen" @click="$emit('toggle-screen')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPNavBar {
|
||||
position: relative;
|
||||
border-bottom: 1px solid transparent;
|
||||
padding: 0 8px 0 24px;
|
||||
height: var(--vp-nav-height);
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.VPNavBar {
|
||||
padding: 0 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
/* .VPNavBar.has-sidebar {
|
||||
padding: 0;
|
||||
} */
|
||||
|
||||
.VPNavBar:not(.has-sidebar):not(.top) {
|
||||
border-bottom-color: var(--vp-c-gutter);
|
||||
background-color: var(--vp-nav-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 0 auto;
|
||||
max-width: calc(var(--vp-layout-max-width) - 64px);
|
||||
height: var(--vp-nav-height);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.container > .title,
|
||||
.container > .content {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.container :deep(*) {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* @media (min-width: 960px) {
|
||||
.VPNavBar.has-sidebar .container {
|
||||
max-width: 100%;
|
||||
}
|
||||
} */
|
||||
|
||||
.title {
|
||||
flex-shrink: 0;
|
||||
height: calc(var(--vp-nav-height) - 1px);
|
||||
transition: background-color 0.5s;
|
||||
}
|
||||
|
||||
/* @media (min-width: 960px) {
|
||||
.VPNavBar.has-sidebar .title {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
padding: 0 32px;
|
||||
width: var(--vp-sidebar-width);
|
||||
height: var(--vp-nav-height);
|
||||
background-color: transparent;
|
||||
}
|
||||
} */
|
||||
|
||||
/* @media (min-width: 1440px) {
|
||||
.VPNavBar.has-sidebar .title {
|
||||
padding-left: max(32px, calc((100% - (var(--vp-layout-max-width) - 64px)) / 2));
|
||||
width: calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px);
|
||||
}
|
||||
} */
|
||||
|
||||
.content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/* @media (min-width: 960px) {
|
||||
.VPNavBar.has-sidebar .content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding-right: 32px;
|
||||
padding-left: var(--vp-sidebar-width);
|
||||
}
|
||||
} */
|
||||
|
||||
/* @media (min-width: 1440px) {
|
||||
.VPNavBar.has-sidebar .content {
|
||||
padding-right: calc((100vw - var(--vp-layout-max-width)) / 2 + 32px);
|
||||
padding-left: calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width));
|
||||
}
|
||||
} */
|
||||
|
||||
.content-body {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
height: calc(var(--vp-nav-height) - 1px);
|
||||
transition: background-color 0.5s;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPNavBar:not(.top) .content-body {
|
||||
position: relative;
|
||||
background-color: var(--vp-nav-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.content-body {
|
||||
column-gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.menu + .translations::before,
|
||||
.menu + .appearance::before,
|
||||
.menu + .social-links::before,
|
||||
.translations + .appearance::before,
|
||||
.appearance + .social-links::before {
|
||||
margin-right: 8px;
|
||||
margin-left: 8px;
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background-color: var(--vp-c-divider);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.menu + .appearance::before,
|
||||
.translations + .appearance::before {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.appearance + .social-links::before {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.social-links {
|
||||
margin-right: -8px;
|
||||
}
|
||||
|
||||
/* @media (min-width: 960px) {
|
||||
.VPNavBar.has-sidebar .curtain {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: -31px;
|
||||
width: calc(100% - var(--vp-sidebar-width));
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.VPNavBar.has-sidebar .curtain::before {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
background: linear-gradient(var(--vp-c-bg), transparent 70%);
|
||||
content: "";
|
||||
}
|
||||
} */
|
||||
|
||||
/* @media (min-width: 1440px) {
|
||||
.VPNavBar.has-sidebar .curtain {
|
||||
width: calc(100% - ((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width)));
|
||||
}
|
||||
} */
|
||||
</style>
|
||||
64
.vitepress/theme/components/NavBarMenu.vue
Normal file
64
.vitepress/theme/components/NavBarMenu.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from "vue"
|
||||
import { useData } from "vitepress"
|
||||
import type { DefaultTheme } from 'vitepress'
|
||||
|
||||
import VPNavBarMenuLink from "vitepress/dist/client/theme-default/components/VPNavBarMenuLink.vue"
|
||||
import VPNavBarMenuGroup from "vitepress/dist/client/theme-default/components/VPNavBarMenuGroup.vue"
|
||||
|
||||
import { data as release } from "../data/release.data"
|
||||
|
||||
const { theme } = useData<DefaultTheme.Config>()
|
||||
|
||||
// Used to avoid hydration issues.
|
||||
const replace = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
replace.value = true
|
||||
})
|
||||
|
||||
/**
|
||||
* Workaround to use the release data directly while the sidebar
|
||||
* and navbar doesn't support using the VitePress data loading.
|
||||
*/
|
||||
const nav = computed(() => {
|
||||
if (!replace.value) {
|
||||
return theme.value.nav
|
||||
}
|
||||
|
||||
return theme.value.nav?.map((item) => {
|
||||
if (!item.text.includes("{app_version}")) {
|
||||
return item
|
||||
}
|
||||
|
||||
const appVersion = release.stable.tag_name.substring(1)
|
||||
|
||||
return {
|
||||
...item,
|
||||
text: item.text.replace("{app_version}", appVersion),
|
||||
} satisfies DefaultTheme.NavItem
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav v-if="nav" aria-labelledby="main-nav-aria-label" class="VPNavBarMenu">
|
||||
<span id="main-nav-aria-label" class="visually-hidden">Main navigation</span>
|
||||
<template v-for="item in nav" :key="item.text">
|
||||
<VPNavBarMenuLink v-if="'link' in item" :item="item" />
|
||||
<VPNavBarMenuGroup v-else :item="item" />
|
||||
</template>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPNavBarMenu {
|
||||
display: none
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.VPNavBarMenu {
|
||||
display: flex
|
||||
}
|
||||
}
|
||||
</style>
|
||||
48
.vitepress/theme/components/NavBarTranslations.vue
Normal file
48
.vitepress/theme/components/NavBarTranslations.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import VPIconLanguages from 'vitepress/dist/client/theme-default/components/icons/VPIconLanguages.vue'
|
||||
import VPFlyout from 'vitepress/dist/client/theme-default/components/VPFlyout.vue'
|
||||
import VPMenuLink from 'vitepress/dist/client/theme-default/components/VPMenuLink.vue'
|
||||
import { useData } from 'vitepress'
|
||||
import { useLangs } from '../composables/langs'
|
||||
|
||||
const { theme } = useData()
|
||||
const { localeLinks, currentLang } = useLangs({ correspondingLink: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VPFlyout
|
||||
v-if="localeLinks.length && currentLang.label"
|
||||
class="VPNavBarTranslations"
|
||||
:icon="VPIconLanguages"
|
||||
:label="theme.langMenuLabel || 'Change language'"
|
||||
>
|
||||
<div class="items">
|
||||
<p class="title">{{ currentLang.label }}</p>
|
||||
|
||||
<template v-for="locale in localeLinks" :key="locale.link">
|
||||
<VPMenuLink :item="locale" />
|
||||
</template>
|
||||
</div>
|
||||
</VPFlyout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPNavBarTranslations {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.VPNavBarTranslations {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 0 24px 0 12px;
|
||||
line-height: 32px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
</style>
|
||||
57
.vitepress/theme/components/NavScreenMenu.vue
Normal file
57
.vitepress/theme/components/NavScreenMenu.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from "vue"
|
||||
import { type DefaultTheme, useData } from "vitepress"
|
||||
|
||||
import VPNavScreenMenuLink from "vitepress/dist/client/theme-default/components/VPNavScreenMenuLink.vue"
|
||||
import VPNavScreenMenuGroup from "vitepress/dist/client/theme-default/components/VPNavScreenMenuGroup.vue"
|
||||
|
||||
import { data as release } from "../data/release.data"
|
||||
|
||||
const { theme } = useData<DefaultTheme.Config>()
|
||||
|
||||
// Used to avoid hydration issues.
|
||||
const replace = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
replace.value = true
|
||||
})
|
||||
|
||||
/**
|
||||
* Workaround to use the release data directly while the sidebar
|
||||
* and navbar doesn't support using the VitePress data loading.
|
||||
*/
|
||||
const nav = computed(() => {
|
||||
if (!replace.value) {
|
||||
return theme.value.nav
|
||||
}
|
||||
|
||||
return theme.value.nav?.map((item) => {
|
||||
if (!item.text.includes("{app_version}")) {
|
||||
return item
|
||||
}
|
||||
|
||||
const appVersion = release.stable.tag_name.substring(1)
|
||||
|
||||
return {
|
||||
...item,
|
||||
text: item.text.replace("{app_version}", appVersion),
|
||||
} satisfies DefaultTheme.NavItem
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav v-if="nav" class="VPNavScreenMenu">
|
||||
<template v-for="item in nav" :key="item.text">
|
||||
<VPNavScreenMenuLink
|
||||
v-if="'link' in item"
|
||||
:item="item"
|
||||
/>
|
||||
<VPNavScreenMenuGroup
|
||||
v-else
|
||||
:text="item.text || ''"
|
||||
:items="item.items"
|
||||
/>
|
||||
</template>
|
||||
</nav>
|
||||
</template>
|
||||
77
.vitepress/theme/components/NavScreenTranslations.vue
Normal file
77
.vitepress/theme/components/NavScreenTranslations.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import VPIconChevronDown from 'vitepress/dist/client/theme-default/components/icons/VPIconChevronDown.vue'
|
||||
import VPIconLanguages from 'vitepress/dist/client/theme-default/components/icons/VPIconLanguages.vue'
|
||||
import { useLangs } from '../composables/langs'
|
||||
import VPLink from 'vitepress/dist/client/theme-default/components/VPLink.vue'
|
||||
|
||||
const { localeLinks, currentLang } = useLangs({ correspondingLink: true })
|
||||
const isOpen = ref(false)
|
||||
|
||||
function toggle() {
|
||||
isOpen.value = !isOpen.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="localeLinks.length && currentLang.label"
|
||||
class="VPNavScreenTranslations"
|
||||
:class="{ open: isOpen }"
|
||||
>
|
||||
<button class="title" @click="toggle">
|
||||
<VPIconLanguages class="icon lang" />
|
||||
{{ currentLang.label }}
|
||||
<VPIconChevronDown class="icon chevron" />
|
||||
</button>
|
||||
|
||||
<ul class="list">
|
||||
<li v-for="locale in localeLinks" :key="locale.link" class="item">
|
||||
<VPLink class="link" :href="locale.link">{{ locale.text }}</VPLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPNavScreenTranslations {
|
||||
height: 24px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.VPNavScreenTranslations.open {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.icon.lang {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.icon.chevron {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.list {
|
||||
padding: 4px 0 0 24px;
|
||||
}
|
||||
|
||||
.link {
|
||||
line-height: 32px;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
</style>
|
||||
84
.vitepress/theme/components/NotFound.vue
Normal file
84
.vitepress/theme/components/NotFound.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useData } from 'vitepress'
|
||||
import { useLangs } from 'vitepress/dist/client/theme-default/composables/langs'
|
||||
|
||||
import Button from './Button.vue'
|
||||
|
||||
const { site, theme } = useData()
|
||||
const { localeLinks } = useLangs({ removeCurrent: false })
|
||||
|
||||
const root = ref<string>('/')
|
||||
onMounted(() => {
|
||||
const path = window.location.pathname
|
||||
.replace(site.value.base, '')
|
||||
.replace(/(^.*?\/).*$/, '/$1')
|
||||
|
||||
if (localeLinks.value.length) {
|
||||
root.value =
|
||||
localeLinks.value.find(({ link }) => link.startsWith(path))?.link ||
|
||||
localeLinks.value[0].link
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="NotFound">
|
||||
<p class="code">{{ theme.notFound?.code ?? 'Four-Oh-Four' }}</p>
|
||||
<h1 class="title">{{ theme.notFound?.title ?? 'Page not found' }}</h1>
|
||||
<div class="divider" />
|
||||
<blockquote class="quote">
|
||||
<template v-if="theme.notFound?.quote">
|
||||
{{ theme.notFound?.quote }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>Похоже, что вы перешли по неверной или устаревшей ссылке.</p>
|
||||
<p>Информация, которую вы искали, где-то здесь. Вы можете воспользоваться поиском.</p>
|
||||
<br>
|
||||
<p>Этот сайт автоматически генерируется из файлов, расположенных на GitHub, поэтому адреса могут иногда меняться.</p>
|
||||
</template>
|
||||
</blockquote>
|
||||
<div class="action">
|
||||
<Button
|
||||
class="link"
|
||||
:href="root"
|
||||
theme="alt"
|
||||
:text="theme.notFound?.linkText ?? 'Вернуться на главную'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.NotFound {
|
||||
padding: 64px 24px 96px;
|
||||
text-align: center;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.NotFound {
|
||||
padding: 96px 32px 168px;
|
||||
}
|
||||
}
|
||||
.code {
|
||||
line-height: 64px;
|
||||
font-size: 64px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.title {
|
||||
padding-top: 12px;
|
||||
letter-spacing: 2px;
|
||||
line-height: 20px;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.quote {
|
||||
margin: 0 auto;
|
||||
max-width: 512px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
.action {
|
||||
padding-top: 20px;
|
||||
}
|
||||
</style>
|
||||
42
.vitepress/theme/components/OgImageTemplate.vue
Normal file
42
.vitepress/theme/components/OgImageTemplate.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{ title: string; description?: string; dir?: string }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
tw="w-full h-full bg-white flex"
|
||||
style="background-color: #0058c9;"
|
||||
>
|
||||
<div tw="p-14 w-full min-h-0 grow flex flex-1 flex-col justify-between">
|
||||
<div tw="w-full flex justify-between items-center text-4xl font-medium">
|
||||
<div tw="flex items-center">
|
||||
<svg width="64" height="64" viewBox="0 0 512 512" preserveAspectRatio="xMidYMid meet" fill="#FFFFFF"><path d="M 145.3 101.9 C 127 108.5 111.1 133.3 103.4 167.2 C 100.6 179.6 98 200.4 98 210.5 L 98 217.5 L 89.7 230.5 C 66.6 266.5 45.4 304 25.5 344 C 7.8 379.4 6 383.7 6 390 C 6 392.8 6.7 396.4 7.4 397.9 C 9.7 402.2 15.3 406.6 21.5 408.9 L 27.2 411.1 L 258.7 411 C 483.8 411 490.2 410.9 493.8 409.1 C 505.4 403.1 509.1 390.7 503.1 377.8 C 480.4 328.4 449.6 272.3 421.5 229 L 414.7 218.5 L 413.9 206.5 C 410.8 160.5 401.1 132.3 381.2 111.7 C 372.6 102.9 369.3 100.9 363.2 100.8 C 354.7 100.8 349 104.7 336.3 119.3 C 323.8 133.7 321.2 137.1 312.5 150 C 308.2 156.3 302.9 163.6 300.7 166.2 L 296.7 171 L 255.1 171 L 213.5 170.9 L 205.1 158 C 189 133.2 162.6 103.1 155.1 101 C 150.7 99.8 151.5 99.7 145.3 101.9 Z M 171.8 242.6 C 175 244.8 175.8 246.8 176.9 256 C 178.3 266.9 179.6 268.4 186.7 267.5 C 193.1 266.7 198.3 268.8 201.2 273.4 C 206.9 282.6 197.1 301.9 183.2 308.7 C 178.2 311.2 176.4 311.5 168 311.5 C 159.9 311.5 157.8 311.1 154 309.1 C 142.9 303.3 136.1 294.8 133.3 283.6 C 131.9 277.7 131.8 275.9 132.9 270.7 C 134.4 263.2 137 258.2 141.7 252.9 C 150 243.7 165.7 238.3 171.8 242.6 Z M 348.8 243.5 C 351.9 246.1 352.1 246.9 353.6 258.9 C 354.5 266.2 356.7 268.4 362.6 267.5 C 368.3 266.7 373.2 268.3 376.5 272.1 C 381.9 278.6 378.7 291.1 369.1 301.3 C 361.4 309.5 356.6 311.4 344.5 311.5 C 335.3 311.5 334.1 311.3 329.3 308.6 C 322.1 304.6 315.6 298 312.3 291.2 C 310.1 286.7 309.4 283.8 309.1 277.3 C 308.8 269.8 309.1 268.4 311.8 262.7 C 316.8 251.9 324.4 245.2 334.9 242.4 C 342.1 240.4 345.5 240.7 348.8 243.5 Z" /></svg>
|
||||
<div tw="text-white ml-8 font-bold">
|
||||
Kotatsu
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div tw="text-7xl font-bold text-white" v-html="title" />
|
||||
<div tw="w-full flex flex-col items-start justify-end">
|
||||
<div v-if="description" tw="mt-8 text-3xl text-white" v-html="description" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="dir" tw="w-98 h-full flex flex-none">
|
||||
<svg v-if="dir === 'FAQ'" height="600" width="600" viewBox="0 0 960 960" fill="#0b37aa" xmlns="http://www.w3.org/2000/svg"><path d="M 700 580 L 220 580 L 60 740 L 60 100 C 60 77.91 77.91 60 100 60 L 700 60 C 722.09 60 740 77.91 740 100 L 740 540 C 740 562.09 722.09 580 700 580 M 900 340 L 900 900 L 740 740 L 300 740 C 277.91 740 260 722.09 260 700 L 260 660 L 820 660 L 820 300 L 860 300 C 882.09 300 900 317.91 900 340 M 307.6 140 C 272.8 140 244.8 148 223.2 163.6 C 202.4 180 192 202.8 192.4 234.4 L 192.8 235.6 L 270 235.6 C 270.4 223.6 274 214.4 281.2 208 C 288.4 202 297.2 198.8 307.6 198.8 C 320 198.8 330.4 202.8 337.6 210 C 344.8 217.6 348 228 348 240 C 348 252.8 345.2 263.6 338.8 272.8 C 333.2 282 324.8 290 314.4 296.4 C 294 310 280 322 272.4 332.8 C 264.4 343.2 260 360 260 380 L 340 380 C 340 367.6 341.6 357.6 345.2 350.4 C 348.8 343.2 355.6 336 365.6 329.6 C 383.6 320 398.4 308.4 410 292.4 C 421.6 276.4 427.6 260 427.6 240 C 427.6 209.6 416.8 185.2 395.2 167.2 C 374 149.2 344.8 140 307.6 140 M 260 420 L 260 500 L 340 500 L 340 420 L 260 420 M 500 500 L 580 500 L 580 420 L 500 420 L 500 500 M 500 140 L 500 380 L 580 380 L 580 140 L 500 140 Z" /></svg>
|
||||
<svg v-else-if="dir === 'Guide'" height="600" width="600" viewBox="0 0 960 960" fill="#0b37aa" xmlns="http://www.w3.org/2000/svg"><path d="M 480 261.82 C 600.5 261.82 698.18 359.5 698.18 480 C 698.18 560.73 654.18 631.27 589.09 669.09 L 589.09 734.54 C 589.09 754.62 572.81 770.9 552.73 770.9 L 407.27 770.9 C 387.19 770.9 370.91 754.62 370.91 734.54 L 370.91 669.09 C 305.82 631.27 261.82 560.73 261.82 480 C 261.82 359.5 359.5 261.82 480 261.82 M 552.73 807.27 L 552.73 843.63 C 552.73 863.71 536.45 879.99 516.37 879.99 L 443.64 879.99 C 423.56 879.99 407.28 863.71 407.28 843.63 L 407.28 807.27 L 552.73 807.27 M 770.91 443.64 L 880 443.64 L 880 516.37 L 770.91 516.37 L 770.91 443.64 M 80 443.64 L 189.09 443.64 L 189.09 516.37 L 80 516.37 L 80 443.64 M 516.36 80 L 516.36 189.09 L 443.63 189.09 L 443.63 80 L 516.36 80 M 222.55 170.91 L 300 248.73 L 248.36 300 L 170.91 222.91 L 222.55 170.91 M 660 248.36 L 737.09 170.91 L 789.09 222.91 L 711.64 300 L 660 248.36 Z" /></svg>
|
||||
<svg v-else-if="dir === 'News'" height="600" width="600" viewBox="0 0 960 960" fill="#0b37aa" xmlns="http://www.w3.org/2000/svg"><path d="M157.5-122.5q-31 0-53-22t-22-53v-614q0-6.5 5.5-9t10.5 2.5l35.5 35.5q5.5 5.5 13 5.25t13-5.75l40-40q5.5-5.5 13-5.75t13 5.25l41 41q5.5 5.5 13 5.5t13-5.5l41-41q5.5-5.5 13-5.25t13 5.75l40 40q5.5 5.5 13 5.75t13-5.25l41-41q5.5-5.5 13-5.5t13 5.5l41 41q5.5 5.5 13 5.25t13-5.75l40-40q5.5-5.5 13-5.75t13 5.25l41 41q5.5 5.5 13 5.5t13-5.5l41-41q5.5-5.5 13-5.25t13 5.75l40 40q5.5 5.5 13 5.75t13-5.25l35.5-35.5q5-5 10.5-2.5t5.5 9v614q0 31-22 53t-53 22h-645Zm0-75h285v-245h-285v245Zm360 0h285v-85h-285v85Zm0-160h285v-85h-285v85Zm-360-160h645v-125h-645v125Z" /></svg>
|
||||
<svg v-else-if="dir === 'Sandbox'" height="600" width="600" viewBox="0 0 960 960" fill="#0b37aa" xmlns="http://www.w3.org/2000/svg"><path d="M 220 80 C 142.4 80 80 142.4 80 220 L 80 740 C 80 817.6 142.4 880 220 880 L 640 880 L 880 640 L 880 220 C 880 142.4 817.6 80 740 80 L 220 80 M 230 160 L 730 160 C 768.66 160 800 191.34 800 230 L 800 600 L 740 600 C 662.4 600 600 662.4 600 740 L 600 800 L 230 800 C 191.34 800 160 768.66 160 730 L 160 230 C 160 191.34 191.34 160 230 160 M 577.6 270.8 C 571.2 270.8 564.8 271.6 558.8 273.2 C 521.2 283.6 500 322 509.6 360 C 511.6 366 514.4 372 518 377.6 L 647.2 342.4 C 647.2 335.6 646.4 328.8 644.8 322 C 636.4 292 608.8 270.8 577.6 270.8 M 326.8 340 C 320 340 314 340 308 342 C 270.8 352.4 248.8 390.8 258.8 428 C 260 434.4 263.6 440 267.2 446.4 L 396.4 411.2 C 396.4 404.4 395.6 397.6 394 391.2 C 385.6 360 358 340 326.8 340 M 668.8 450.4 L 303.6 550.8 C 356.4 612 440 637.6 518 616.4 C 596 594.8 654.4 530 668.8 450.4 Z" /></svg>
|
||||
<svg v-else-if="dir === 'Other'" height="600" width="600" viewBox="0 0 960 960" fill="#0b37aa" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M 500.37 828.06 C 551.94 827.86 603.32 821.92 653.57 810.35 L 663.79 807.89 C 662.43 812.48 661.75 817.23 661.76 822.02 C 662.06 823.99 662.8 825.88 663.92 827.54 C 665.04 829.2 666.52 830.58 668.24 831.6 C 670.92 832.4 675.62 829.09 678.35 826.25 C 680.68 823.16 682.67 819.82 684.29 816.3 C 686.16 813.41 688.41 810.95 690.93 807.52 C 695.19 813.26 699.74 818.78 704.58 824.05 C 705.78 825.13 707.23 825.9 708.79 826.3 C 710.36 826.7 712 826.72 713.57 826.35 C 715.6 825.28 715.92 820.62 716.41 817.41 C 716.62 815.65 716.18 813.89 715.18 812.43 C 703.46 795.63 711.43 785.67 725.19 774.28 C 771.64 717.99 781.75 680.21 779.34 617.87 C 776.5 543.54 777.36 470.39 745.74 401.85 C 735.68 380.07 731.99 355.24 726.26 331.59 C 725.31 324.71 725.45 317.72 726.69 310.88 C 731.4 262.72 729.26 216.17 731.51 167.79 C 731.41 144.75 727.39 121.9 719.63 100.21 C 711.07 75.06 665.74 52.48 646.86 71.26 C 626.63 91.43 620.69 131.35 609.4 157.52 C 595.17 189.63 592.28 222.16 574.73 255.12 C 551.45 254.09 528.13 255.54 505.17 259.45 C 504.39 233.35 501.35 207.37 496.07 181.81 C 487.56 141.94 476.7 102.61 449.25 70.56 C 447.27 68.21 445.4 65.74 443.26 63.6 C 423.35 43.48 403.02 45.3 393.01 71.57 C 381.41 101.72 372.73 132.92 367.11 164.73 C 360.15 205.08 357.85 246.28 353.3 287.06 C 352.99 291.79 351.74 296.41 349.61 300.65 C 331.43 333.75 320.9 370.5 318.79 408.21 C 317.09 436.1 312.41 463.73 304.82 490.62 C 288.77 544.13 297.11 597.32 311.29 649.81 C 322.07 692.78 340.72 733.39 366.3 769.57 C 375.97 784.41 387.63 797.84 400.97 809.49 C 408.3 815.27 419.91 815.54 419.91 817.3 C 419.91 819.06 414.93 820.67 402.09 828 C 394.92 832.12 392.73 833.94 392.67 836.29 C 392.62 838.64 397.54 839.77 400.11 841.16 C 400.55 841.4 401.05 841.52 401.55 841.52 C 402.05 841.52 402.55 841.4 402.99 841.16 C 409.02 838.77 414.58 835.35 419.42 831.05 C 423.34 828.33 426.67 824.86 429.21 820.83 C 425.65 827.97 422.97 835.51 421.24 843.3 C 420.7 846.4 430.93 847.58 433.17 843.3 C 436.11 837.95 433.6 829.44 442.37 825.05 C 442.37 825.05 434.5 843.4 440.23 848.54 C 441.68 849.58 443.42 850.15 445.21 850.15 C 447 850.15 448.74 849.59 450.19 848.54 C 455.97 843.99 450.19 833.4 453.94 827.14 C 457.69 820.88 468.48 823.94 500.37 828.06 Z M 586.36 276.15 C 588.55 271.98 590.75 268.28 592.51 264.38 C 605.41 235.86 608.03 207.66 620.28 175.66 C 634.78 137.93 636.33 107.22 657.74 83.51 C 669.19 70.93 703.6 92.29 707.88 108.45 C 713.21 128.9 715.61 149.99 715 171.11 C 713.5 211.19 720.35 251.38 714.04 290.97 C 707.14 330.31 713.09 370.85 731 406.55 C 745.53 434.17 755.07 464.13 759.2 495.06 C 767.71 561.09 774.77 621.13 761.61 684.49 C 748.02 727.83 734.32 764.76 673.96 787.23 C 603.33 813.56 529.16 819.02 454.19 809.33 C 440.23 807.86 426.74 803.47 414.59 796.44 C 402.44 789.41 391.91 779.91 383.67 768.55 C 353.75 727.42 332.6 680.57 321.54 630.92 C 310.2 584.1 306.61 537.06 319.61 489.54 C 325.58 469.06 329.62 448.06 331.65 426.83 C 335.13 383.17 342.35 340.95 365.2 302.74 C 367.41 298.45 368.45 293.65 368.2 288.83 C 368.31 241.14 374.08 193.63 385.38 147.29 C 390.71 123.88 397.59 100.86 405.98 78.37 C 412.4 61.46 422.03 60.12 432.74 74.36 C 444.95 89.67 455.18 106.47 463.19 124.34 C 481.01 167.3 489.99 213.42 489.57 259.94 C 489.57 267.54 491.18 273.42 500.7 271.28 C 529.13 264.76 558.85 266.44 586.36 276.15 Z"/>
|
||||
<path d="M 617.29 348.5 C 600.41 351.64 585.44 361.35 575.7 375.5 C 565.96 389.65 562.23 407.1 565.33 423.99 C 568.43 440.88 578.12 455.87 592.25 465.63 C 606.38 475.39 623.82 479.15 640.72 476.08 C 657.61 473.01 672.62 463.34 682.4 449.23 C 692.19 435.12 695.98 417.69 692.94 400.78 C 690.9 389.55 685.93 379.04 678.54 370.34 C 671.15 361.63 661.59 355.03 650.84 351.2 C 640.1 347.36 628.52 346.43 617.29 348.5 Z M 620.82 365.03 C 633.27 363.35 645.91 366.48 656.12 373.79 C 666.34 381.09 673.39 392.04 675.83 404.36 C 679.95 428.76 661.76 455.03 637.89 459.1 C 627.94 460.94 617.67 459.57 608.54 455.2 C 599.42 450.83 591.92 443.67 587.12 434.77 C 582.32 425.87 580.47 415.67 581.83 405.64 C 583.19 395.62 587.71 386.29 594.71 378.99 C 601.72 371.69 610.86 366.81 620.82 365.03 Z M 427.54 450.01 C 443.01 447.11 456.71 438.19 465.62 425.22 C 474.53 412.25 477.94 396.25 475.1 380.77 C 472.26 365.29 463.39 351.56 450.45 342.6 C 437.51 333.64 421.53 330.17 406.04 332.96 C 390.55 335.75 376.78 344.57 367.78 357.48 C 358.77 370.39 355.25 386.35 357.98 401.85 C 359.82 412.2 364.37 421.88 371.16 429.91 C 377.95 437.93 386.74 444.02 396.65 447.55 C 406.54 451.08 417.21 451.92 427.54 450.01 Z M 375.42 398.8 C 373.81 387.89 376.42 376.76 382.69 367.7 C 388.96 358.63 398.46 352.28 409.24 349.95 C 418.13 348.06 427.41 349.09 435.66 352.9 C 443.92 356.7 450.72 363.09 455.05 371.09 C 459.37 379.08 461 388.27 459.68 397.27 C 458.36 406.27 454.15 414.59 447.7 421 C 441.26 427.41 432.9 431.56 423.9 432.84 C 412.99 434.51 401.85 431.9 392.82 425.56 C 383.8 419.21 377.56 409.62 375.42 398.8 Z"/>
|
||||
<path d="M 630.4 445.78 C 636.82 444.23 643.19 442.3 643.19 430.64 C 643.42 426.72 642.47 422.82 640.46 419.44 C 638.45 416.06 635.47 413.37 631.91 411.7 C 628.35 410.04 624.38 409.47 620.5 410.09 C 610.07 411.8 603.48 426.14 605.73 435.99 C 607.71 444.6 615.2 448.56 630.4 445.78 Z M 415.77 420.36 C 420.4 419.17 424.4 416.23 426.91 412.16 C 429.43 408.09 430.27 403.21 429.26 398.53 C 427.92 389.7 417.54 383.33 405.72 385.9 C 402.53 386.65 399.61 388.27 397.28 390.57 C 394.95 392.87 393.3 395.77 392.5 398.94 C 391.7 402.12 391.8 405.45 392.77 408.58 C 394.85 417.74 401.8 423.09 415.77 420.36 Z"/>
|
||||
<path d="M 44.08 395.21 L 44.08 721.2 C 43.72 725.38 44.46 729.59 46.22 733.4 C 48.36 736.42 51.13 738.92 54.35 740.73 C 120.86 784.13 148.74 809.97 215.26 853.37 C 222.11 857.81 239.29 873.44 247.37 874.77 C 279.48 880.12 290.66 886.49 298.58 887.51 C 386.98 898.96 473.88 901.26 563.08 901.53 C 563.08 783.81 565.43 669.29 565.49 551.41 C 565.69 549.56 565.32 547.69 564.42 546.06 C 563.44 545.03 562.25 544.24 560.93 543.73 C 559.61 543.22 558.18 543.01 556.77 543.12 L 314.63 528.24 C 297.74 527.88 280.94 525.52 264.6 521.23 C 251.56 516.83 239.01 511.11 227.14 504.16 C 165.38 470.21 106.05 432 49.59 389.81" fill="#0058c9"/>
|
||||
<path d="M 34.72 395.21 L 34.72 679.14 C 34.72 695.89 30.87 717.08 36.27 732.92 C 39.96 743.62 49.06 748.28 58.1 754.32 C 88.92 774.98 118.73 797.13 148.75 818.53 C 176.36 838.6 204.45 861.61 233.94 878.57 C 250.64 888.2 276.75 892.43 296.07 896.34 C 382.01 913.68 475.87 910.31 563.09 910.68 C 565.56 910.65 567.93 909.66 569.68 907.91 C 571.43 906.16 572.43 903.79 572.45 901.32 C 572.45 819.02 573.68 736.77 574.38 654.47 C 574.38 633.87 574.7 613.27 574.75 592.67 C 574.75 578.86 579.08 559.12 574 546.01 C 565.81 524.98 527.39 531.78 509.41 530.65 L 388.32 523.21 C 346.96 520.7 298.48 524.07 259.36 509.3 C 222.38 495.33 187.33 470.88 154.05 449.9 C 119.95 428.5 86.77 405.68 54.52 381.46 C 44.83 374.29 35.47 390.56 45.05 397.51 C 103.46 441.72 165.26 481.26 229.88 515.77 C 290.88 547.88 368.69 540.55 435.68 544.67 L 493.63 548.26 C 508.93 549.17 540.77 545.64 551.21 556.45 C 561.64 567.26 556.03 597.87 555.92 613.17 C 555.92 632.01 555.92 650.63 555.49 669.73 C 554.79 746.89 553.78 824 553.72 901.16 L 563.08 891.8 C 481.58 891.48 399.93 889.71 318.96 880.24 C 296.11 877.62 263.47 872.86 243.4 862.26 C 217.18 848.45 191.92 826.46 167.84 809.18 C 142.05 790.72 116.79 771.72 90.68 753.53 C 71.04 739.88 56.38 735.34 53.22 710.35 C 50.81 690.02 53.22 667.54 53.22 647.26 L 53.22 394.84 C 53.22 392.36 52.23 389.98 50.47 388.22 C 48.71 386.46 46.33 385.47 43.85 385.47 C 41.37 385.47 38.99 386.46 37.23 388.22 C 35.47 389.98 34.48 392.36 34.48 394.84 L 34.72 395.21 Z"/>
|
||||
<path d="M 248.55 548.36 C 248.12 643.02 237.85 737.31 237.85 831.97 C 237.85 834.45 238.84 836.83 240.6 838.59 C 242.36 840.35 244.74 841.34 247.22 841.34 C 249.7 841.34 252.08 840.35 253.84 838.59 C 255.6 836.83 256.59 834.45 256.59 831.97 C 256.59 737.31 266.86 643.02 267.29 548.36 C 267.29 545.88 266.3 543.5 264.54 541.74 C 262.78 539.98 260.4 538.99 257.92 538.99 C 255.44 538.99 253.06 539.98 251.3 541.74 C 249.54 543.5 248.55 545.88 248.55 548.36 Z M 295.48 776.96 L 343.27 779.37 L 338.72 773.43 C 336.83 781.98 333.14 790.04 327.92 797.07 C 322.69 804.1 316.03 809.94 308.38 814.21 C 303.03 817.1 307.79 824.91 313.09 822.34 C 321.82 817.45 329.42 810.78 335.39 802.76 C 341.37 794.74 345.58 785.55 347.76 775.79 C 348.01 774.86 347.97 773.89 347.65 772.98 C 347.32 772.08 346.72 771.3 345.94 770.75 C 345.15 770.2 344.22 769.9 343.26 769.9 L 295.47 767.49 C 289.42 767.22 289.48 776.59 295.47 776.85 L 295.47 776.96 Z"/>
|
||||
<path d="M 290.34 807.03 C 303.5 816.93 316.19 827.26 324.11 842.03 C 326.95 847.38 334.81 842.62 332.19 837.32 C 323.52 821.27 309.5 809.82 295.05 798.95 C 290.23 795.36 285.58 803.45 290.34 807.03 Z M 368.95 779.15 C 375.46 786.2 383.3 791.89 392.01 795.9 C 397.36 798.2 402.28 790.12 396.72 787.77 C 388.62 784.19 381.34 779 375.32 772.52 C 371.09 768.24 364.62 774.82 368.68 779.16 L 368.95 779.16 Z M 421.76 775.78 C 421.96 788.76 419.97 801.67 415.87 813.99 C 408.54 833.74 391.84 835.88 373.06 835.07 C 367.01 835.07 367.07 844.22 373.06 844.43 C 389.43 845.13 409.61 844.11 419.4 828.86 C 429.19 813.61 430.9 793.38 431.28 775.78 C 431.27 774.53 430.78 773.34 429.89 772.46 C 429.01 771.58 427.81 771.08 426.57 771.08 C 425.32 771.08 424.13 771.57 423.25 772.46 C 422.37 773.34 421.87 774.53 421.86 775.78 L 421.76 775.78 Z M 469.92 770.43 C 469.92 783.7 472.81 796.92 472.38 810.24 C 471.79 826.83 461.68 835.87 446.64 840.85 C 440.97 842.72 443.38 851.55 449.1 849.84 C 463.17 845.13 475.27 837.59 480.3 822.55 C 485.65 806.5 479.55 786.91 479.34 770.43 C 479.34 769.18 478.84 767.98 477.96 767.1 C 477.08 766.22 475.88 765.72 474.63 765.72 C 473.38 765.72 472.18 766.22 471.3 767.1 C 470.42 767.98 469.92 769.18 469.92 770.43 Z"/>
|
||||
<path d="M 447.87 796.54 L 498.44 796.54 C 502.67 796.54 505.29 795.42 507.86 799.38 C 509.41 801.84 507.86 809.6 507.86 812.33 C 507.86 820.84 511.18 846.42 495.07 841.28 C 489.34 839.41 486.88 848.45 492.61 850.32 C 522.15 859.74 517.44 815.16 517.12 799.38 C 517.12 786.75 513 787.18 500.75 787.18 L 447.88 787.18 C 446.63 787.18 445.43 787.68 444.55 788.56 C 443.67 789.44 443.17 790.64 443.17 791.89 C 443.17 793.14 443.67 794.34 444.55 795.22 C 445.43 796.1 446.63 796.6 447.88 796.6 L 447.88 796.54 Z M 511.5 778.88 C 514.62 780.83 517.47 783.18 519.95 785.89 C 524.07 790.33 530.65 783.7 526.59 779.25 C 523.53 775.99 520.06 773.14 516.26 770.8 C 515.2 770.3 513.99 770.21 512.86 770.55 C 511.74 770.9 510.78 771.65 510.19 772.66 C 509.59 773.67 509.39 774.87 509.64 776.02 C 509.88 777.18 510.55 778.2 511.5 778.88 Z M 521.02 769.84 C 524.64 772 527.55 775.16 529.42 778.94 C 532.26 784.29 540.12 779.53 537.55 774.18 C 534.85 769.03 530.78 764.73 525.78 761.77 C 524.72 761.27 523.51 761.18 522.38 761.52 C 521.26 761.87 520.3 762.62 519.71 763.63 C 519.11 764.64 518.91 765.84 519.16 766.99 C 519.41 768.14 520.07 769.16 521.02 769.84 Z M 501.39 647.35 C 503.42 646.39 501.82 641.3 501.82 639.06 C 501.82 603.53 501.23 565.16 501.39 529.63 L 442.53 524.28 C 443.94 564.17 442.35 604.12 437.77 643.77 C 448.29 636.75 458.36 629.06 467.9 620.76 C 469.68 619.29 471.98 618.58 474.28 618.79 C 476.59 619 478.72 620.12 480.2 621.89 L 501.39 647.35 Z"/>
|
||||
<path d="M 505.88 648.58 C 509.95 633.38 506.52 613.91 506.42 598.17 C 506.42 575.32 505.99 552.47 506.1 529.62 C 506.05 528.39 505.53 527.22 504.66 526.35 C 503.79 525.48 502.62 524.96 501.39 524.91 L 442.53 519.56 C 441.29 519.58 440.1 520.07 439.22 520.96 C 438.34 521.84 437.84 523.03 437.82 524.27 C 439.09 564.16 437.51 604.09 433.11 643.76 C 433.13 644.86 433.52 645.92 434.24 646.76 C 434.96 647.59 435.94 648.16 437.02 648.34 C 438.11 648.53 439.22 648.33 440.17 647.78 C 452.1 639.75 467.73 621.02 480.78 629.85 C 487.36 634.18 492.98 644.51 498.06 650.67 C 501.91 655.33 508.76 648.64 504.7 644.03 C 497.96 635.95 491.22 623.75 482.55 617.86 C 466.02 606.46 447.61 631.51 435.41 639.69 L 442.47 643.76 C 446.43 611.65 452.1 574.2 447.45 542.41 C 447.45 541.02 447.08 525.71 443.65 529.03 C 444.35 528.39 448.14 529.73 449.27 529.57 C 456.88 529.41 464.5 530.12 471.96 531.66 C 479.01 531.72 486.04 532.34 492.99 533.53 C 503.69 536.37 498.34 533.53 496.68 547.6 C 495.96 562.91 496.06 578.26 497 593.57 C 497 609.62 500.96 631.03 497 646.12 C 496.86 647.23 497.12 648.36 497.74 649.29 C 498.36 650.22 499.3 650.9 500.37 651.2 C 501.45 651.5 502.61 651.4 503.62 650.92 C 504.63 650.44 505.43 649.61 505.88 648.58 Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
30
.vitepress/theme/components/ReleaseDate.vue
Normal file
30
.vitepress/theme/components/ReleaseDate.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, toRefs } from "vue"
|
||||
import moment from "moment"
|
||||
import { type AppRelease, data as release } from "../data/release.data"
|
||||
|
||||
const props = defineProps<{ type: keyof AppRelease }>()
|
||||
const { type } = toRefs(props)
|
||||
|
||||
const momentInfo = computed(() => ({
|
||||
relative: moment(release[type.value].published_at).fromNow(),
|
||||
exact: moment(release[type.value].published_at).format("dddd, MMMM Do YYYY [at] HH:mm"),
|
||||
iso: release[type.value].published_at ?? undefined,
|
||||
}))
|
||||
|
||||
// Mimic the <ClientOnly /> behavior to show custom text while rendering.
|
||||
const show = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
show.value = true
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<time v-if="show" :datetime="momentInfo.iso" :title="momentInfo.exact">
|
||||
{{ momentInfo.relative }}
|
||||
</time>
|
||||
<time v-else :datetime="momentInfo.iso">
|
||||
{{ momentInfo.exact }}
|
||||
</time>
|
||||
</template>
|
||||
102
.vitepress/theme/components/ScreenAuthorWidget.vue
Normal file
102
.vitepress/theme/components/ScreenAuthorWidget.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
import { type Ref, computed } from 'vue'
|
||||
import { type PageData, useData } from 'vitepress'
|
||||
import type { DocsPageData } from '../plugins/component'
|
||||
import { type Translator, translators } from '../../../website/translators'
|
||||
|
||||
import { VPImage } from 'vitepress/theme-without-fonts'
|
||||
import VPLink from 'vitepress/dist/client/theme-default/components/VPLink.vue'
|
||||
|
||||
export interface Data {
|
||||
page: Ref<DocsPageData>
|
||||
frontmatter: Ref<PageData['frontmatter']>
|
||||
lang: Ref<string>
|
||||
}
|
||||
|
||||
const { page, frontmatter, lang }: Data = useData()
|
||||
|
||||
const translatorLabel = computed<string>(() => lang.value === 'ru' ? 'Переводчик' : 'Translator')
|
||||
const translator = computed<Translator>(() => {
|
||||
const data = page.value?.component?.translator || translators[frontmatter.value.translator]
|
||||
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
|
||||
return {
|
||||
...data,
|
||||
name: typeof data.name === 'string' ? data.name : data.name[lang.value]
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article
|
||||
v-if="translator"
|
||||
class="ScreenAuthorWidget"
|
||||
>
|
||||
<VPLink
|
||||
:href="translator.github"
|
||||
:no-icon="true"
|
||||
class="body"
|
||||
>
|
||||
<VPImage
|
||||
v-if="translator.avatar"
|
||||
class="avatar"
|
||||
:image="translator.avatar"
|
||||
/>
|
||||
<div class="info">
|
||||
<span class="label">{{ translatorLabel }}:</span>
|
||||
<span class="name">{{ translator.name }}</span>
|
||||
</div>
|
||||
</VPLink>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.ScreenAuthorWidget {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.ScreenAuthorWidget {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
font-size: 13px;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
:deep(.avatar) {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
:deep(.name) {
|
||||
font-weight: bold;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
:deep(.label) {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
:deep(.link) {
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
:deep(.link):hover {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
</style>
|
||||
178
.vitepress/theme/components/SearchBar.vue
Normal file
178
.vitepress/theme/components/SearchBar.vue
Normal file
@@ -0,0 +1,178 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useData } from 'vitepress'
|
||||
import { createTranslate } from 'vitepress/dist/client/theme-default/support/translation'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
const uid = uuidv4()
|
||||
const { theme } = useData()
|
||||
const $t = createTranslate(theme.value.search?.options)
|
||||
withDefaults(defineProps<{
|
||||
modelValue: string
|
||||
placeholder: string
|
||||
backButton?: boolean
|
||||
}>(), {
|
||||
backButton: true
|
||||
})
|
||||
|
||||
/* Search input focus */
|
||||
|
||||
const searchInput = ref<HTMLInputElement>()
|
||||
|
||||
function focusSearchInput() {
|
||||
searchInput.value?.focus()
|
||||
// searchInput.value?.select()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
focusSearchInput()
|
||||
})
|
||||
|
||||
function onSearchBarClick(event: PointerEvent) {
|
||||
if (event.pointerType === 'mouse') {
|
||||
focusSearchInput()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form class="search-bar" @pointerup="onSearchBarClick($event)" @submit.prevent="">
|
||||
<label :title="placeholder" id="localsearch-label" :for="uid">
|
||||
<svg
|
||||
class="search-icon"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<path d="m21 21l-4.35-4.35" />
|
||||
</g>
|
||||
</svg>
|
||||
</label>
|
||||
<div
|
||||
v-if="backButton"
|
||||
class="search-actions before"
|
||||
>
|
||||
<button
|
||||
class="back-button"
|
||||
:title="$t('modal.backButtonTitle')"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 12H5m7 7l-7-7l7-7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
ref="searchInput"
|
||||
:id="uid"
|
||||
:value="modelValue"
|
||||
:placeholder="placeholder"
|
||||
class="search-input"
|
||||
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<div class="search-actions">
|
||||
<button
|
||||
v-if="modelValue"
|
||||
class="clear-button"
|
||||
:title="$t('modal.resetButtonTitle')"
|
||||
@click="$emit('update:modelValue', '')"
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 20 20">
|
||||
<path d="M10 10l5.09-5.09L10 10l5.09 5.09L10 10zm0 0L4.91 4.91 10 10l-5.09 5.09L10 10z" stroke="currentColor" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.search-bar {
|
||||
border: 1px solid var(--vp-c-gray-soft);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 8px;
|
||||
height: 56px;
|
||||
cursor: text;
|
||||
background-color: var(--vp-c-bg-elv);
|
||||
box-shadow: var(--vp-shadow-1);
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.search-bar {
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-bar:focus-within {
|
||||
border-color: var(--vp-c-border);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.search-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.search-input {
|
||||
padding: 6px 12px;
|
||||
font-size: inherit;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.search-input {
|
||||
padding: 6px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
@media (any-pointer: coarse) {
|
||||
.search-actions {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
.search-actions.before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.search-actions button {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.search-actions button:hover {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
</style>
|
||||
186
.vitepress/theme/components/Sidebar.vue
Normal file
186
.vitepress/theme/components/Sidebar.vue
Normal file
@@ -0,0 +1,186 @@
|
||||
<script lang="ts" setup>
|
||||
import { useScrollLock } from '@vueuse/core'
|
||||
import { ref, watchPostEffect, watch, computed } from 'vue'
|
||||
import { DefaultTheme, useRoute, useData, inBrowser } from 'vitepress'
|
||||
import { useSidebar } from 'vitepress/dist/client/theme-default/composables/sidebar'
|
||||
import VPSidebarItem from 'vitepress/dist/client/theme-default/components/VPSidebarItem.vue'
|
||||
|
||||
const { sidebar: flatSidebar, sidebarGroups, hasSidebar } = useSidebar()
|
||||
const route = useRoute()
|
||||
|
||||
const sidebar = computed<DefaultTheme.SidebarItem[]>(() => {
|
||||
return route.path.includes('/components/') ? flatSidebar.value : sidebarGroups.value
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
}>()
|
||||
|
||||
// a11y: focus Nav element when menu has opened
|
||||
const navEl = ref<HTMLElement | null>(null)
|
||||
const isLocked = useScrollLock(inBrowser ? document.body : null)
|
||||
|
||||
watch(
|
||||
[props, navEl],
|
||||
() => {
|
||||
if (props.open) {
|
||||
isLocked.value = true
|
||||
navEl.value?.focus()
|
||||
} else isLocked.value = false
|
||||
},
|
||||
{ immediate: true, flush: 'post' }
|
||||
)
|
||||
|
||||
const { lang } = useData()
|
||||
const activeLinkEl = ref<HTMLElement | null>(null)
|
||||
const activeGroupEl = ref<HTMLElement | null>(null)
|
||||
|
||||
watch(() => route.path, () => {
|
||||
if (!route.path.includes('/components/')) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!navEl.value) {
|
||||
return
|
||||
}
|
||||
|
||||
activeLinkEl.value = navEl.value.querySelector<HTMLElement>(`a[href="${route.path}"]`)
|
||||
activeGroupEl.value = activeLinkEl.value.closest<HTMLElement>('.VPSidebarItem.level-0')
|
||||
|
||||
const offset = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--vp-nav-height')) * 2
|
||||
|
||||
if (!isInViewport(activeGroupEl.value, offset)) {
|
||||
navEl.value.scrollTo({
|
||||
top: activeGroupEl.value.offsetTop - offset
|
||||
})
|
||||
}
|
||||
}, { flush: 'post' })
|
||||
|
||||
// tmp fix vitepress bug
|
||||
watch(lang, () => activeGroupEl.value && activeGroupEl.value.classList.remove('collapsed'), { flush: 'post' })
|
||||
|
||||
function isInViewport(el: HTMLElement, offset: number) {
|
||||
const { top, bottom } = el.getBoundingClientRect()
|
||||
const { innerHeight } = window
|
||||
return top >= offset && bottom <= offset + innerHeight
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside
|
||||
v-if="hasSidebar"
|
||||
class="VPSidebar"
|
||||
:class="{ open }"
|
||||
ref="navEl"
|
||||
@click.stop
|
||||
>
|
||||
<div class="curtain" />
|
||||
|
||||
<nav class="nav" id="VPSidebarNav" aria-labelledby="sidebar-aria-label" tabindex="-1">
|
||||
<span class="visually-hidden" id="sidebar-aria-label">
|
||||
Sidebar Navigation
|
||||
</span>
|
||||
|
||||
<slot name="sidebar-nav-before" />
|
||||
|
||||
<div
|
||||
v-for="item in sidebar"
|
||||
:key="item.text"
|
||||
class="group"
|
||||
>
|
||||
<VPSidebarItem :item="item" :depth="0" :class="{ landing: !item.items }" />
|
||||
</div>
|
||||
|
||||
<slot name="sidebar-nav-after" />
|
||||
</nav>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.VPSidebar {
|
||||
position: fixed;
|
||||
top: var(--vp-layout-top-height, 0px);
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: var(--vp-z-index-sidebar);
|
||||
padding: 32px 32px 96px;
|
||||
width: calc(100vw - 64px);
|
||||
max-width: 320px;
|
||||
background-color: var(--vp-sidebar-bg-color);
|
||||
opacity: 0;
|
||||
box-shadow: var(--vp-shadow-3);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
transform: translateX(-100%);
|
||||
transition: opacity 0.5s, transform 0.25s ease;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.VPSidebar.open {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateX(0);
|
||||
transition: opacity 0.25s,
|
||||
transform 0.5s cubic-bezier(0.19, 1, 0.22, 1);
|
||||
}
|
||||
|
||||
.dark .VPSidebar {
|
||||
box-shadow: var(--vp-shadow-1);
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.VPSidebar {
|
||||
z-index: 1;
|
||||
padding-top: var(--vp-nav-height);
|
||||
padding-bottom: 128px;
|
||||
width: var(--vp-sidebar-width);
|
||||
max-width: 100%;
|
||||
background-color: var(--vp-sidebar-bg-color);
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
box-shadow: none;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.VPSidebar {
|
||||
padding-left: max(32px, calc((100% - (var(--vp-layout-max-width) - 64px)) / 2));
|
||||
width: calc((100% - (var(--vp-layout-max-width) - 64px)) / 2 + var(--vp-sidebar-width) - 32px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.curtain {
|
||||
position: sticky;
|
||||
top: -64px;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
margin-top: calc(var(--vp-nav-height) * -1);
|
||||
margin-right: -32px;
|
||||
margin-left: -32px;
|
||||
height: var(--vp-nav-height);
|
||||
background-color: var(--vp-sidebar-bg-color);
|
||||
}
|
||||
}
|
||||
|
||||
.nav {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.group + .group {
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.group {
|
||||
padding-top: 10px;
|
||||
width: calc(var(--vp-sidebar-width) - 64px);
|
||||
}
|
||||
}
|
||||
|
||||
.landing :deep(.VPLink .text) {
|
||||
font-weight: normal;
|
||||
}
|
||||
</style>
|
||||
95
.vitepress/theme/components/TranslatorWidget.vue
Normal file
95
.vitepress/theme/components/TranslatorWidget.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<script setup lang="ts">
|
||||
import { type Ref, computed } from 'vue'
|
||||
import { type PageData, useData } from 'vitepress'
|
||||
import type { DocsPageData } from '../plugins/component'
|
||||
import { type Translator, translators } from '../../../website/translators'
|
||||
|
||||
import { VPImage } from 'vitepress/theme-without-fonts'
|
||||
import VPLink from 'vitepress/dist/client/theme-default/components/VPLink.vue'
|
||||
|
||||
export interface Data {
|
||||
page: Ref<DocsPageData>
|
||||
frontmatter: Ref<PageData['frontmatter']>
|
||||
lang: Ref<string>
|
||||
}
|
||||
|
||||
const { page, frontmatter, lang }: Data = useData()
|
||||
|
||||
const translatorLabel = computed<string>(() => lang.value === 'en' ? 'Translator' : 'Переводчик')
|
||||
const translator = computed<Translator>(() => {
|
||||
const data = page.value?.component?.translator || translators[frontmatter.value.author]
|
||||
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
|
||||
return {
|
||||
...data,
|
||||
name: typeof data.name === 'string' ? data.name : data.name[lang.value]
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article
|
||||
v-if="translator"
|
||||
class="TranslatorWidget"
|
||||
>
|
||||
<VPLink
|
||||
:href="translator.github"
|
||||
:no-icon="true"
|
||||
class="body"
|
||||
>
|
||||
<VPImage
|
||||
v-if="translator.avatar"
|
||||
class="avatar"
|
||||
:image="translator.avatar"
|
||||
/>
|
||||
<div class="info">
|
||||
<div class="name">{{ translator.name }}</div>
|
||||
<div class="label">{{ translatorLabel }}</div>
|
||||
</div>
|
||||
</VPLink>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.TranslatorWidget {
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
border-radius: var(--vp-border-radius);
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.avatar) {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
:deep(.name) {
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
:deep(.label) {
|
||||
font-size: 14px;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
:deep(.link) {
|
||||
transition: color 0.25s;
|
||||
}
|
||||
|
||||
:deep(.link):hover {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
</style>
|
||||
26
.vitepress/theme/components/Tree.vue
Normal file
26
.vitepress/theme/components/Tree.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import type { DefaultTheme } from 'vitepress'
|
||||
|
||||
import TreeItem from './TreeItem.vue'
|
||||
|
||||
defineProps<{
|
||||
items: DefaultTheme.SidebarItem[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul v-if="items" class="list">
|
||||
<TreeItem
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
:item="item"
|
||||
/>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.list {
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
}
|
||||
</style>
|
||||
130
.vitepress/theme/components/TreeItem.vue
Normal file
130
.vitepress/theme/components/TreeItem.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import type { DefaultTheme } from 'vitepress'
|
||||
|
||||
import Tree from './Tree.vue'
|
||||
import VPLink from 'vitepress/dist/client/theme-default/components/VPLink.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
item: DefaultTheme.SidebarItem
|
||||
}>()
|
||||
const isOpen = ref<boolean>(false)
|
||||
const isFolder = computed<boolean>(() => {
|
||||
return !!(props.item.items && props.item.items.length)
|
||||
})
|
||||
|
||||
const toggle = function () {
|
||||
if (isFolder.value) {
|
||||
isOpen.value = !isOpen.value
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li
|
||||
class="item"
|
||||
:class="{
|
||||
folder: isFolder,
|
||||
opened: isOpen,
|
||||
'is-link': !!props.item.link
|
||||
}"
|
||||
>
|
||||
<span
|
||||
class="title"
|
||||
@click="toggle"
|
||||
>
|
||||
<div
|
||||
v-if="isFolder"
|
||||
class="icon"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 -0.5 17 17"
|
||||
version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<path d="M6.113,15.495 C5.531,16.076 4.01,16.395 4.01,14.494 L4.01,1.506 C4.01,-0.333 5.531,-0.076 6.113,0.506 L12.557,6.948 C13.137,7.529 13.137,8.47 12.557,9.052 L6.113,15.495 L6.113,15.495 Z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<VPLink :href="props.item.link">
|
||||
{{ props.item.text }}
|
||||
</VPLink>
|
||||
</span>
|
||||
<Tree
|
||||
v-show="isOpen"
|
||||
v-if="isFolder"
|
||||
:items="props.item.items"
|
||||
class="list"
|
||||
/>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.item {
|
||||
position: relative;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.item:not(.folder) {
|
||||
border-left: 1px dashed var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.item:not(.folder)::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: calc(50% - .5px);
|
||||
left: 3px;
|
||||
width: 12px;
|
||||
height: 0;
|
||||
border-top: 1px dashed var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.title {
|
||||
position: relative;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.folder .title {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 20px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.icon svg {
|
||||
position: relative;
|
||||
margin-left: -3px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
fill: var(--vp-c-text-3);
|
||||
transition: fill .25s;
|
||||
}
|
||||
|
||||
.item:not(.is-link) > .title:hover > .icon svg,
|
||||
.icon:hover svg {
|
||||
fill: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.opened > .title > .icon svg {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
:deep(.link) {
|
||||
color: var(--vp-c-text-1);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
:deep(.link:hover) {
|
||||
color: var(--vp-c-brand-1);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.item :deep(.list) {
|
||||
padding-left: 20px;
|
||||
}
|
||||
</style>
|
||||
114
.vitepress/theme/composables/langs.ts
Normal file
114
.vitepress/theme/composables/langs.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { Ref, computed } from 'vue'
|
||||
import type { DefaultTheme, SiteData } from 'vitepress'
|
||||
import type { DocsPageData } from '../plugins/component'
|
||||
import { useData } from 'vitepress'
|
||||
import { ensureStartingSlash } from '../utils'
|
||||
import { getFlatSideBarLinks } from 'vitepress/dist/client/theme-default/support/sidebar'
|
||||
|
||||
export function useLangs({
|
||||
removeCurrent = true,
|
||||
correspondingLink = false
|
||||
} = {}) {
|
||||
interface Data {
|
||||
site: Ref<SiteData>
|
||||
localeIndex: Ref<string>
|
||||
page: Ref<DocsPageData>
|
||||
theme: Ref<DefaultTheme.Config>
|
||||
}
|
||||
|
||||
const { site, localeIndex, page, theme }: Data = useData()
|
||||
const currentLang = computed(() => ({
|
||||
label: site.value.locales[localeIndex.value]?.label,
|
||||
link:
|
||||
site.value.locales[localeIndex.value]?.link ||
|
||||
(localeIndex.value === 'root' ? '/' : `/${localeIndex.value}/`)
|
||||
}))
|
||||
|
||||
const localeLinks = computed(() =>
|
||||
Object.entries(site.value.locales).flatMap(([key, value]) => {
|
||||
if (removeCurrent && currentLang.value.label === value.label) {
|
||||
return []
|
||||
}
|
||||
|
||||
const text = value.label
|
||||
const rootLink = value.link || (key === 'root' ? '/' : `/${key}/`)
|
||||
let link = normalizeLink(
|
||||
rootLink,
|
||||
theme.value.i18nRouting !== false && correspondingLink,
|
||||
page.value.relativePath.slice(currentLang.value.link.length - 1),
|
||||
!site.value.cleanUrls
|
||||
)
|
||||
|
||||
if (
|
||||
link === '/' ||
|
||||
Object.entries(site.value.locales).some(([key]) => link.replace(/^\//, '').replace(/\/$/, '') === key)
|
||||
) {
|
||||
return {
|
||||
text,
|
||||
link
|
||||
}
|
||||
}
|
||||
|
||||
const { themeConfig } = site.value.locales[key]
|
||||
const { nav, sidebar } = themeConfig
|
||||
|
||||
for (const item of nav) {
|
||||
if (Object.prototype.hasOwnProperty.call(item, 'link') && item.link === link) {
|
||||
return {
|
||||
text,
|
||||
link
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const data of Object.values(sidebar)) {
|
||||
const flatSidebar = getFlatSideBarLinks(data)
|
||||
|
||||
for (const item of flatSidebar) {
|
||||
if (item.link === link) {
|
||||
return {
|
||||
text,
|
||||
link
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!page.value.component) {
|
||||
return {
|
||||
text: value.label,
|
||||
link: rootLink,
|
||||
}
|
||||
}
|
||||
|
||||
const component = themeConfig.components.find(component => component.title === page.value.component.title)
|
||||
if (!component) {
|
||||
return {
|
||||
text: value.label,
|
||||
link: rootLink,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
text,
|
||||
link: component.link
|
||||
}
|
||||
}
|
||||
))
|
||||
|
||||
return { localeLinks, currentLang }
|
||||
}
|
||||
|
||||
function normalizeLink(
|
||||
link: string,
|
||||
addPath: boolean,
|
||||
path: string,
|
||||
addExt: boolean
|
||||
): string {
|
||||
return addPath
|
||||
? link.replace(/\/$/, '') +
|
||||
ensureStartingSlash(path
|
||||
.replace(/(^|\/)?index.md$/, '$1')
|
||||
.replace(/\.md$/, addExt ? '.html' : ''))
|
||||
: link
|
||||
}
|
||||
66
.vitepress/theme/composables/prev-next.ts
Normal file
66
.vitepress/theme/composables/prev-next.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { computed } from 'vue'
|
||||
import type { ComputedRef, Ref } from 'vue'
|
||||
import type { DocsPageData } from '../plugins/component'
|
||||
import { useData } from 'vitepress'
|
||||
import { isActive } from 'vitepress/dist/client/shared'
|
||||
import { getFlatSideBarLinks } from 'vitepress/dist/client/theme-default/support/sidebar'
|
||||
|
||||
export function usePrevNext(): ComputedRef {
|
||||
interface Data {
|
||||
theme: Ref<any>
|
||||
page: Ref<DocsPageData>
|
||||
frontmatter: Ref<DocsPageData['frontmatter']>
|
||||
}
|
||||
|
||||
const { page, theme, frontmatter }: Data = useData()
|
||||
|
||||
return computed(() => {
|
||||
const component = page.value.component
|
||||
|
||||
if (!component) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const candidates = getFlatSideBarLinks([component])
|
||||
const index = candidates.findIndex((link) => isActive(page.value.relativePath, link.link))
|
||||
|
||||
const hidePrev =
|
||||
(theme.value.docFooter?.prev === false && !frontmatter.value.prev) ||
|
||||
frontmatter.value.prev === false
|
||||
|
||||
const hideNext =
|
||||
(theme.value.docFooter?.next === false && !frontmatter.value.next) ||
|
||||
frontmatter.value.next === false
|
||||
|
||||
return {
|
||||
prev: hidePrev
|
||||
? undefined
|
||||
: {
|
||||
text: (typeof frontmatter.value.prev === 'string'
|
||||
? frontmatter.value.prev
|
||||
: typeof frontmatter.value.prev === 'object'
|
||||
? frontmatter.value.prev.text
|
||||
: undefined) ??
|
||||
candidates[index - 1]?.docFooterText ??
|
||||
candidates[index - 1]?.text,
|
||||
link: (typeof frontmatter.value.prev === 'object'
|
||||
? frontmatter.value.prev.link
|
||||
: undefined) ?? candidates[index - 1]?.link
|
||||
},
|
||||
next: hideNext
|
||||
? undefined
|
||||
: {
|
||||
text: (typeof frontmatter.value.next === 'string'
|
||||
? frontmatter.value.next
|
||||
: typeof frontmatter.value.next === 'object'
|
||||
? frontmatter.value.next.text
|
||||
: undefined) ??
|
||||
candidates[index + 1]?.docFooterText ??
|
||||
candidates[index + 1]?.text,
|
||||
link: (typeof frontmatter.value.next === 'object'
|
||||
? frontmatter.value.next.link
|
||||
: undefined) ?? candidates[index + 1]?.link
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
71
.vitepress/theme/composables/zoom.ts
Normal file
71
.vitepress/theme/composables/zoom.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import 'glightbox/dist/css/glightbox.css'
|
||||
import { scrollbarWidth } from '@xobotyi/scrollbar-width'
|
||||
|
||||
import {
|
||||
watch,
|
||||
type App,
|
||||
} from 'vue'
|
||||
import type { Router } from 'vitepress'
|
||||
|
||||
export const glightboxSymbol = Symbol('glightbox')
|
||||
|
||||
export async function createZoom(app: App, router: Router) {
|
||||
if (import.meta.env.SSR) {
|
||||
return
|
||||
}
|
||||
|
||||
const GLightbox = await import('glightbox')
|
||||
|
||||
const lightbox = GLightbox.default({
|
||||
openEffect: 'fade',
|
||||
closeEffect: 'fade',
|
||||
zoomable: true,
|
||||
skin: 'white',
|
||||
svg: {
|
||||
close: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M17,17.8c-0.2,0-0.4-0.1-0.6-0.2L12,13.1l-4.4,4.4c-0.3,0.3-0.8,0.3-1.1,0c-0.3-0.3-0.3-0.8,0-1.1l4.4-4.4L6.4,7.6 c-0.3-0.3-0.3-0.8,0-1.1c0.3-0.3,0.8-0.3,1.1,0l4.4,4.4l4.4-4.4c0.3-0.3,0.8-0.3,1.1,0c0.2,0.2,0.2,0.4,0.2,0.6s-0.1,0.4-0.2,0.6 L13.1,12l4.4,4.4c0.3,0.3,0.3,0.8,0,1.1C17.4,17.7,17.2,17.8,17,17.8z"/></svg>',
|
||||
next: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9,18.8c-0.2,0-0.4-0.1-0.5-0.2c-0.3-0.3-0.3-0.8,0-1.1l5.5-5.5L8.5,6.5c-0.3-0.3-0.3-0.8,0-1.1s0.8-0.3,1.1,0l6,6 c0.3,0.3,0.3,0.8,0,1.1l-6,6C9.4,18.7,9.2,18.8,9,18.8z"/></svg>',
|
||||
prev: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M15,18.8c-0.2,0-0.4-0.1-0.5-0.2l-6-6c-0.3-0.3-0.3-0.8,0-1.1l6-6c0.3-0.3,0.8-0.3,1.1,0s0.3,0.8,0,1.1L10.1,12l5.5,5.5 c0.3,0.3,0.3,0.8,0,1.1C15.4,18.7,15.2,18.8,15,18.8z"/></svg>',
|
||||
},
|
||||
})
|
||||
|
||||
lightbox.reload = () => {
|
||||
const elements = Array.from(document.querySelectorAll('.vp-doc img'))
|
||||
.map((element, index) => {
|
||||
const target = element.parentElement instanceof HTMLAnchorElement ? element.parentElement : element
|
||||
target.addEventListener('click', (e) => {
|
||||
e.preventDefault()
|
||||
lightbox.openAt(index)
|
||||
})
|
||||
|
||||
const href = target.getAttribute(target instanceof HTMLAnchorElement ? 'href' : 'src')
|
||||
const title = element.getAttribute('alt')
|
||||
|
||||
return {
|
||||
href,
|
||||
title,
|
||||
type: 'image',
|
||||
}
|
||||
})
|
||||
|
||||
lightbox.setElements(elements)
|
||||
}
|
||||
|
||||
const scrollWidth = scrollbarWidth()
|
||||
|
||||
lightbox.on('open', () => updateFixedElements(scrollWidth))
|
||||
lightbox.on('close', () => updateFixedElements())
|
||||
|
||||
app.provide(glightboxSymbol, lightbox)
|
||||
|
||||
watch(
|
||||
() => router.route.data.relativePath,
|
||||
() => setTimeout(() => {
|
||||
lightbox.reload()
|
||||
}),
|
||||
{ immediate: true }
|
||||
)
|
||||
}
|
||||
|
||||
function updateFixedElements(scrollWidth?: number) {
|
||||
document.documentElement.style.setProperty('--scrollbar-width', scrollWidth ? scrollWidth + 'px' : '')
|
||||
}
|
||||
22
.vitepress/theme/data/changelogs.data.ts
Normal file
22
.vitepress/theme/data/changelogs.data.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { defineLoader } from "vitepress"
|
||||
import { Octokit } from "@octokit/rest"
|
||||
import type { GetResponseDataTypeFromEndpointMethod } from "@octokit/types"
|
||||
|
||||
const octokit = new Octokit()
|
||||
|
||||
type GitHubReleaseList = GetResponseDataTypeFromEndpointMethod<typeof octokit.repos.listReleases>
|
||||
|
||||
declare const data: GitHubReleaseList
|
||||
export { data }
|
||||
|
||||
export default defineLoader({
|
||||
async load(): Promise<GitHubReleaseList> {
|
||||
const releases = await octokit.paginate(octokit.repos.listReleases, {
|
||||
owner: "KotatsuApp",
|
||||
repo: "Kotatsu",
|
||||
per_page: 100,
|
||||
})
|
||||
|
||||
return releases
|
||||
},
|
||||
})
|
||||
25
.vitepress/theme/data/release.data.ts
Normal file
25
.vitepress/theme/data/release.data.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineLoader } from "vitepress"
|
||||
import { Octokit } from "@octokit/rest"
|
||||
import type { GetResponseDataTypeFromEndpointMethod } from "@octokit/types"
|
||||
|
||||
const octokit = new Octokit()
|
||||
|
||||
type GitHubRelease = GetResponseDataTypeFromEndpointMethod<typeof octokit.repos.getLatestRelease>
|
||||
|
||||
export interface AppRelease {
|
||||
stable: GitHubRelease
|
||||
}
|
||||
|
||||
declare const data: AppRelease
|
||||
export { data }
|
||||
|
||||
export default defineLoader({
|
||||
async load(): Promise<AppRelease> {
|
||||
const { data: stable } = await octokit.repos.getLatestRelease({
|
||||
owner: "KotatsuApp",
|
||||
repo: "Kotatsu",
|
||||
})
|
||||
|
||||
return { stable }
|
||||
},
|
||||
})
|
||||
33
.vitepress/theme/index.ts
Normal file
33
.vitepress/theme/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { type Router, inBrowser } from 'vitepress'
|
||||
import { type App, watch, defineComponent } from 'vue'
|
||||
import { createZoom } from './composables/zoom'
|
||||
import { enhanceAppWithTabs } from "vitepress-plugin-tabs/client"
|
||||
import analytics from "./plugins/analytics"
|
||||
import DefaultTheme from 'vitepress/theme-without-fonts'
|
||||
import Layout from './components/Layout.vue'
|
||||
import './styles/global.css'
|
||||
import './styles/glightbox.css'
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
Layout: Layout,
|
||||
|
||||
enhanceApp({ app, router }: { app: App, router: Router }) {
|
||||
enhanceAppWithTabs(app)
|
||||
createZoom(app, router)
|
||||
analytics({ id: "G-X37JGMJE4R" })
|
||||
if (
|
||||
import.meta.env.PROD &&
|
||||
inBrowser
|
||||
) {
|
||||
watch(
|
||||
() => router.route.data.relativePath,
|
||||
(_path, oldPath) => {
|
||||
if (!oldPath) { // Skip initial change
|
||||
return
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
37
.vitepress/theme/plugins/analytics.ts
Normal file
37
.vitepress/theme/plugins/analytics.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// Code based on vitepress-plugin-google-analytics.
|
||||
// Customized as the plugin did not consider the script loading time.
|
||||
// https://github.com/ZhongxuYang/vitepress-plugin-google-analytics
|
||||
|
||||
function mountGoogleAnalytics(id: string) {
|
||||
if (("dataLayer" in window && window.gtag) || window.location.hostname === "localhost") {
|
||||
return
|
||||
}
|
||||
|
||||
const analyticsScript = document.createElement("script")
|
||||
|
||||
analyticsScript.addEventListener("load", () => {
|
||||
// @ts-expect-error Missing types
|
||||
window.dataLayer = window.dataLayer || []
|
||||
function gtag(..._args: any[]) {
|
||||
// @ts-expect-error Missing types
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
window.dataLayer.push(arguments)
|
||||
}
|
||||
|
||||
gtag("js", new Date())
|
||||
gtag("config", id)
|
||||
|
||||
window.gtag = gtag
|
||||
})
|
||||
|
||||
analyticsScript.src = `https://www.googletagmanager.com/gtag/js?id=${id}`
|
||||
|
||||
document.body.appendChild(analyticsScript)
|
||||
}
|
||||
|
||||
export default function ({ id }: { id: string }) {
|
||||
// eslint-disable-next-line n/prefer-global/process
|
||||
if (process.env.NODE_ENV === "production" && id && typeof window !== "undefined") {
|
||||
mountGoogleAnalytics(id)
|
||||
}
|
||||
}
|
||||
109
.vitepress/theme/plugins/component.ts
Normal file
109
.vitepress/theme/plugins/component.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import type { DefaultTheme, PageData, SiteConfig } from 'vitepress'
|
||||
import { normalize } from 'vitepress/dist/client/shared'
|
||||
import { ensureStartingSlash, getTranslator } from '../utils'
|
||||
|
||||
import { readFileSync } from 'fs'
|
||||
import { basename } from 'path'
|
||||
import fg from 'fast-glob'
|
||||
import matter from 'gray-matter'
|
||||
|
||||
import { generateSidebarItem, getTitleFromContent } from './sidebar'
|
||||
|
||||
import type { Translator } from '../../../website/translators'
|
||||
import { findPath } from '../utils'
|
||||
|
||||
export interface ComponentData {
|
||||
path: string
|
||||
link: string
|
||||
title: string
|
||||
titleLower: string
|
||||
description?: string
|
||||
|
||||
text?: string
|
||||
translator?: Translator
|
||||
logo?: string
|
||||
dependencies?: Array<string>
|
||||
categories?: Array<string>
|
||||
|
||||
repository?: string
|
||||
|
||||
items?: DefaultTheme.SidebarItem[]
|
||||
}
|
||||
|
||||
export interface DocsPageData extends PageData {
|
||||
component?: ComponentData
|
||||
breadcrumbs?: DefaultTheme.SidebarItem[]
|
||||
}
|
||||
|
||||
export const components: ComponentData[] = fg
|
||||
.sync([
|
||||
'website/manuals/faq/explore/*.md',
|
||||
'!website/manuals/faq/explore/index.md',
|
||||
])
|
||||
.map(file => {
|
||||
const content = readFileSync(file, 'utf-8')
|
||||
const { data } = matter(content)
|
||||
const {
|
||||
title = getTitleFromContent(content) || basename(file),
|
||||
translator,
|
||||
logo,
|
||||
categories = [],
|
||||
dependencies = [],
|
||||
items,
|
||||
repository,
|
||||
description,
|
||||
} = data
|
||||
|
||||
const filePath = file.substring(file.indexOf('/') + 1)
|
||||
const component: ComponentData = {
|
||||
path: filePath,
|
||||
link: ensureStartingSlash(normalize(filePath)),
|
||||
repository,
|
||||
title,
|
||||
titleLower: title.toLowerCase(),
|
||||
text: title,
|
||||
description,
|
||||
logo,
|
||||
dependencies: Array.isArray(dependencies) ? dependencies : Array(dependencies),
|
||||
categories: Array.isArray(categories) ? categories : Array(categories),
|
||||
}
|
||||
|
||||
component.translator = getTranslator(translator)
|
||||
|
||||
if (items) {
|
||||
component.items = generateSidebarItem(items, component.link)
|
||||
}
|
||||
|
||||
return component
|
||||
})
|
||||
.sort((a, b) => (a.text && b.text) ? a.text.localeCompare(b.text) : 0)
|
||||
|
||||
export default class DocsComponent {
|
||||
static prepareData(
|
||||
pageData: DocsPageData,
|
||||
siteConfig: SiteConfig,
|
||||
): DocsPageData {
|
||||
const component = components.find(component => pageData.relativePath.startsWith(component.path.replace(/index\.md$/, '')))
|
||||
|
||||
pageData.component = component
|
||||
pageData.breadcrumbs = findPath(pageData, siteConfig.userConfig)
|
||||
|
||||
pageData.title = !pageData.frontmatter.title && pageData.breadcrumbs.length
|
||||
? pageData.breadcrumbs.map(item => item.text).reverse().join(siteConfig.userConfig.themeConfig.titleSeparator)
|
||||
: pageData.title
|
||||
|
||||
if (
|
||||
component
|
||||
&& !pageData.description
|
||||
&& pageData.component.description
|
||||
) {
|
||||
pageData.description = pageData.component.description
|
||||
}
|
||||
|
||||
return pageData
|
||||
}
|
||||
}
|
||||
|
||||
export { DocsComponent }
|
||||
|
||||
export const { prepareData } = DocsComponent
|
||||
280
.vitepress/theme/plugins/markdown.ts
Normal file
280
.vitepress/theme/plugins/markdown.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
import type MarkdownIt from 'markdown-it'
|
||||
import type { RenderRule } from 'markdown-it/lib/renderer'
|
||||
import type StateBlock from 'markdown-it/lib/rules_block/state_block'
|
||||
import { isSpace } from 'markdown-it/lib/common/utils'
|
||||
import container from 'markdown-it-container'
|
||||
import kbd from 'markdown-it-kbd'
|
||||
|
||||
export const addPlugins = (md: MarkdownIt) => {
|
||||
md.use(...createContainer('info', 'Информация', md))
|
||||
.use(...createContainer('tip', 'Подсказка', md))
|
||||
.use(...createContainer('warning', 'Внимание', md))
|
||||
.use(...createContainer('danger', 'Осторожно', md))
|
||||
.use(...createContainer('details', 'Подробнее', md))
|
||||
|
||||
.use(kbd)
|
||||
|
||||
md.block.ruler.at('table', table)
|
||||
}
|
||||
|
||||
type ContainerArgs = [typeof container, string, { render: RenderRule }]
|
||||
|
||||
function createContainer(
|
||||
klass: string,
|
||||
defaultTitle: string,
|
||||
md: MarkdownIt
|
||||
): ContainerArgs {
|
||||
return [
|
||||
container,
|
||||
klass,
|
||||
{
|
||||
render(tokens, idx, _options, env) {
|
||||
const token = tokens[idx]
|
||||
const info = token.info.trim().slice(klass.length).trim()
|
||||
const attrs = md.renderer.renderAttrs(token)
|
||||
if (token.nesting === 1) {
|
||||
const title = md.renderInline(info || defaultTitle, {
|
||||
references: env.references
|
||||
})
|
||||
if (klass === 'details')
|
||||
return `<details class="${klass} custom-block"${attrs}><summary>${title}</summary>\n`
|
||||
return `<div class="${klass} custom-block"${attrs}><p class="custom-block-title">${title}</p>\n`
|
||||
} else return klass === 'details' ? `</details>\n` : `</div>\n`
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// from https://github.com/markdown-it/markdown-it/blob/2b6cac25823af011ff3bc7628bc9b06e483c5a08/lib/rules_block/table.js
|
||||
// GFM table, non-standard
|
||||
|
||||
function table(
|
||||
state: StateBlock,
|
||||
startLine: number,
|
||||
endLine: number,
|
||||
silent: any
|
||||
) {
|
||||
var ch, lineText, pos, i, l, nextLine, headers, columns, columnCount, token,
|
||||
aligns, t, tableLines, tbodyLines, oldParentType, terminate,
|
||||
terminatorRules, firstCh, secondCh;
|
||||
|
||||
// should have at least two lines
|
||||
if (startLine + 2 > endLine) { return false; }
|
||||
|
||||
nextLine = startLine + 1;
|
||||
|
||||
if (state.sCount[nextLine] < state.blkIndent) { return false; }
|
||||
|
||||
// if it's indented more than 3 spaces, it should be a code block
|
||||
if (state.sCount[nextLine] - state.blkIndent >= 4) { return false; }
|
||||
|
||||
// first character of the second line should be '|', '-', ':',
|
||||
// and no other characters are allowed but spaces;
|
||||
// basically, this is the equivalent of /^[-:|][-:|\s]*$/ regexp
|
||||
|
||||
pos = state.bMarks[nextLine] + state.tShift[nextLine];
|
||||
if (pos >= state.eMarks[nextLine]) { return false; }
|
||||
|
||||
firstCh = state.src.charCodeAt(pos++);
|
||||
if (firstCh !== 0x7C/* | */ && firstCh !== 0x2D/* - */ && firstCh !== 0x3A/* : */) { return false; }
|
||||
|
||||
if (pos >= state.eMarks[nextLine]) { return false; }
|
||||
|
||||
secondCh = state.src.charCodeAt(pos++);
|
||||
if (secondCh !== 0x7C/* | */ && secondCh !== 0x2D/* - */ && secondCh !== 0x3A/* : */ && !isSpace(secondCh)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if first character is '-', then second character must not be a space
|
||||
// (due to parsing ambiguity with list)
|
||||
if (firstCh === 0x2D/* - */ && isSpace(secondCh)) { return false; }
|
||||
|
||||
while (pos < state.eMarks[nextLine]) {
|
||||
ch = state.src.charCodeAt(pos);
|
||||
|
||||
if (ch !== 0x7C/* | */ && ch !== 0x2D/* - */ && ch !== 0x3A/* : */ && !isSpace(ch)) { return false; }
|
||||
|
||||
pos++;
|
||||
}
|
||||
|
||||
lineText = getLine(state, startLine + 1);
|
||||
|
||||
columns = lineText.split('|');
|
||||
aligns = [];
|
||||
for (i = 0; i < columns.length; i++) {
|
||||
t = columns[i].trim();
|
||||
if (!t) {
|
||||
// allow empty columns before and after table, but not in between columns;
|
||||
// e.g. allow ` |---| `, disallow ` ---||--- `
|
||||
if (i === 0 || i === columns.length - 1) {
|
||||
continue;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!/^:?-+:?$/.test(t)) { return false; }
|
||||
if (t.charCodeAt(t.length - 1) === 0x3A/* : */) {
|
||||
aligns.push(t.charCodeAt(0) === 0x3A/* : */ ? 'center' : 'right');
|
||||
} else if (t.charCodeAt(0) === 0x3A/* : */) {
|
||||
aligns.push('left');
|
||||
} else {
|
||||
aligns.push('');
|
||||
}
|
||||
}
|
||||
|
||||
lineText = getLine(state, startLine).trim();
|
||||
if (lineText.indexOf('|') === -1) { return false; }
|
||||
if (state.sCount[startLine] - state.blkIndent >= 4) { return false; }
|
||||
columns = escapedSplit(lineText);
|
||||
if (columns.length && columns[0] === '') columns.shift();
|
||||
if (columns.length && columns[columns.length - 1] === '') columns.pop();
|
||||
|
||||
// header row will define an amount of columns in the entire table,
|
||||
// and align row should be exactly the same (the rest of the rows can differ)
|
||||
columnCount = columns.length;
|
||||
headers = [...columns];
|
||||
if (columnCount === 0 || columnCount !== aligns.length) { return false; }
|
||||
|
||||
if (silent) { return true; }
|
||||
|
||||
oldParentType = state.parentType;
|
||||
// @ts-expect-error
|
||||
state.parentType = 'table';
|
||||
|
||||
// use 'blockquote' lists for termination because it's
|
||||
// the most similar to tables
|
||||
terminatorRules = state.md.block.ruler.getRules('blockquote');
|
||||
|
||||
token = state.push('table_open', 'table', 1);
|
||||
token.map = tableLines = [ startLine, 0 ];
|
||||
|
||||
token = state.push('thead_open', 'thead', 1);
|
||||
token.map = [ startLine, startLine + 1 ];
|
||||
|
||||
token = state.push('tr_open', 'tr', 1);
|
||||
token.map = [ startLine, startLine + 1 ];
|
||||
|
||||
for (i = 0; i < columns.length; i++) {
|
||||
token = state.push('th_open', 'th', 1);
|
||||
if (aligns[i]) {
|
||||
token.attrs = [ [ 'style', 'text-align:' + aligns[i] ] ];
|
||||
}
|
||||
|
||||
token = state.push('inline', '', 0);
|
||||
token.content = columns[i].trim();
|
||||
token.children = [];
|
||||
|
||||
token = state.push('th_close', 'th', -1);
|
||||
}
|
||||
|
||||
token = state.push('tr_close', 'tr', -1);
|
||||
token = state.push('thead_close', 'thead', -1);
|
||||
|
||||
for (nextLine = startLine + 2; nextLine < endLine; nextLine++) {
|
||||
if (state.sCount[nextLine] < state.blkIndent) { break; }
|
||||
|
||||
terminate = false;
|
||||
for (i = 0, l = terminatorRules.length; i < l; i++) {
|
||||
if (terminatorRules[i](state, nextLine, endLine, true)) {
|
||||
terminate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (terminate) { break; }
|
||||
lineText = getLine(state, nextLine).trim();
|
||||
if (!lineText) { break; }
|
||||
if (state.sCount[nextLine] - state.blkIndent >= 4) { break; }
|
||||
columns = escapedSplit(lineText);
|
||||
if (columns.length && columns[0] === '') columns.shift();
|
||||
if (columns.length && columns[columns.length - 1] === '') columns.pop();
|
||||
|
||||
if (nextLine === startLine + 2) {
|
||||
token = state.push('tbody_open', 'tbody', 1);
|
||||
token.map = tbodyLines = [ startLine + 2, 0 ];
|
||||
}
|
||||
|
||||
token = state.push('tr_open', 'tr', 1);
|
||||
token.map = [ nextLine, nextLine + 1 ];
|
||||
|
||||
for (i = 0; i < columnCount; i++) {
|
||||
token = state.push('td_open', 'td', 1);
|
||||
const attrs = [];
|
||||
if (aligns[i]) {
|
||||
token.attrs = attrs.push([ 'style', 'text-align:' + aligns[i] ]);
|
||||
}
|
||||
|
||||
attrs.push(['data-label', headers[i].trim()]);
|
||||
|
||||
if (attrs.length) {
|
||||
token.attrs = attrs;
|
||||
}
|
||||
|
||||
token = state.push('inline', '', 0);
|
||||
token.content = columns[i] ? columns[i].trim() : '';
|
||||
token.children = [];
|
||||
|
||||
token = state.push('td_close', 'td', -1);
|
||||
}
|
||||
token = state.push('tr_close', 'tr', -1);
|
||||
}
|
||||
|
||||
if (tbodyLines) {
|
||||
token = state.push('tbody_close', 'tbody', -1);
|
||||
tbodyLines[1] = nextLine;
|
||||
}
|
||||
|
||||
token = state.push('table_close', 'table', -1);
|
||||
tableLines[1] = nextLine;
|
||||
|
||||
state.parentType = oldParentType;
|
||||
state.line = nextLine;
|
||||
return true;
|
||||
}
|
||||
|
||||
function getLine(
|
||||
state: StateBlock,
|
||||
line: number
|
||||
): string {
|
||||
var pos = state.bMarks[line] + state.tShift[line],
|
||||
max = state.eMarks[line];
|
||||
|
||||
return state.src.slice(pos, max)
|
||||
}
|
||||
|
||||
function escapedSplit(str: string): string[] {
|
||||
var result = [],
|
||||
pos = 0,
|
||||
max = str.length,
|
||||
ch,
|
||||
isEscaped = false,
|
||||
lastPos = 0,
|
||||
current = '';
|
||||
|
||||
ch = str.charCodeAt(pos);
|
||||
|
||||
while (pos < max) {
|
||||
if (ch === 0x7c/* | */) {
|
||||
if (!isEscaped) {
|
||||
// pipe separating cells, '|'
|
||||
result.push(current + str.substring(lastPos, pos));
|
||||
current = '';
|
||||
lastPos = pos + 1;
|
||||
} else {
|
||||
// escaped pipe, '\|'
|
||||
current += str.substring(lastPos, pos - 1);
|
||||
lastPos = pos;
|
||||
}
|
||||
}
|
||||
|
||||
isEscaped = (ch === 0x5c/* \ */);
|
||||
pos++;
|
||||
|
||||
ch = str.charCodeAt(pos);
|
||||
}
|
||||
|
||||
result.push(current + str.substring(lastPos));
|
||||
|
||||
return result;
|
||||
}
|
||||
97
.vitepress/theme/plugins/sidebar.ts
Normal file
97
.vitepress/theme/plugins/sidebar.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import type { DefaultTheme } from 'vitepress'
|
||||
import { normalize } from 'vitepress/dist/client/shared'
|
||||
import faqCategories from '../../../website/faq/categories.json'
|
||||
|
||||
import { readFileSync } from 'fs'
|
||||
import { join, basename } from 'path'
|
||||
import fg from 'fast-glob'
|
||||
import matter from 'gray-matter'
|
||||
|
||||
declare interface Options {
|
||||
root: string | Array<string>
|
||||
ignore?: Array<string>
|
||||
collapsed?: boolean
|
||||
}
|
||||
|
||||
export default class Sidebar {
|
||||
static generateSidebar(
|
||||
options: Options
|
||||
): DefaultTheme.SidebarItem[] {
|
||||
const entries = fg.sync(options.root, options)
|
||||
|
||||
return entries
|
||||
.map(path => Sidebar.getData(path, options))
|
||||
.sort((a, b) => (a.text && b.text) ? a.text.localeCompare(b.text) : 0)
|
||||
}
|
||||
|
||||
static generateSidebarItem(
|
||||
items: DefaultTheme.SidebarItem[],
|
||||
path: string,
|
||||
): DefaultTheme.SidebarItem[] {
|
||||
items = items.map(({ text, link, items }) => {
|
||||
const item: DefaultTheme.SidebarItem = { text }
|
||||
|
||||
if (link) {
|
||||
item.link = join(path, link).replace(/\\/g, '/')
|
||||
}
|
||||
|
||||
if (items) {
|
||||
item.collapsed = true
|
||||
item.items = Sidebar.generateSidebarItem(items, path)
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
static getData(
|
||||
path: string,
|
||||
options?: Partial<Options>
|
||||
): DefaultTheme.SidebarItem {
|
||||
const src = readFileSync(path, 'utf-8')
|
||||
const { data } = matter(src)
|
||||
const {
|
||||
title = Sidebar.getTitleFromContent(src) || basename(path),
|
||||
items,
|
||||
} = data
|
||||
|
||||
const link = normalize(path.replace(/^website/, ''))
|
||||
|
||||
const output: DefaultTheme.SidebarItem = {
|
||||
text: title,
|
||||
link,
|
||||
}
|
||||
|
||||
if (items) {
|
||||
output.collapsed = options.collapsed === null || options.collapsed === undefined || options.collapsed
|
||||
output.items = Sidebar.generateSidebarItem(items, link)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
static getTitleFromContent(
|
||||
content: string,
|
||||
): string | undefined {
|
||||
const lines = content.split('\n')
|
||||
for (let i = 0, len = lines.length; i < len; i += 1) {
|
||||
let str = lines[i].toString().replace('\r', '')
|
||||
if (str.indexOf('# ') !== -1) {
|
||||
str = str.replace('# ', '')
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
export { Sidebar }
|
||||
|
||||
export const {
|
||||
generateSidebar,
|
||||
generateSidebarItem,
|
||||
getTitleFromContent
|
||||
} = Sidebar
|
||||
93
.vitepress/theme/styles/glightbox.css
Normal file
93
.vitepress/theme/styles/glightbox.css
Normal file
@@ -0,0 +1,93 @@
|
||||
:root {
|
||||
--vp-c-overlay: rgba(255 255 255 / .8);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--vp-c-overlay: rgba(0 0 0 / .8);
|
||||
}
|
||||
|
||||
.vp-doc img {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.glightbox-open.glightbox-mobile .VPLocalNav {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.glightbox-white {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.glightbox-white .goverlay {
|
||||
background: var(--vp-c-overlay);
|
||||
}
|
||||
|
||||
.glightbox-white .gloader {
|
||||
border: 2px solid var(--vp-c-text-1);
|
||||
border-right-color: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.glightbox-white .gslide-description {
|
||||
background: none;
|
||||
padding-block: 20px;
|
||||
}
|
||||
|
||||
.glightbox-white .gclose {
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.glightbox-white .gbtn:not(.disabled):hover {
|
||||
background-color: var(--vp-c-gray-3);
|
||||
}
|
||||
|
||||
.glightbox-white .gnext,
|
||||
.glightbox-white .gprev {
|
||||
position: absolute;
|
||||
top: -100%;
|
||||
}
|
||||
|
||||
.glightbox-white .gprev {
|
||||
left: 30px;
|
||||
}
|
||||
|
||||
.glightbox-white .gnext {
|
||||
right: 30px;
|
||||
}
|
||||
|
||||
.glightbox-white .gbtn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 1px solid var(--vp-c-gray-soft);
|
||||
border-radius: 50%;
|
||||
box-shadow: var(--vp-shadow-1);
|
||||
background-color: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.glightbox-white .gbtn svg {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
@media (min-width: 767px) {
|
||||
.glightbox-white .gclose {
|
||||
top: 30px;
|
||||
right: 30px;
|
||||
}
|
||||
|
||||
.glightbox-white .gbtn {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.glightbox-white .gprev,
|
||||
.glightbox-white .gnext {
|
||||
top: 45%;
|
||||
}
|
||||
}
|
||||
|
||||
.glightbox-mobile .glightbox-container .gslide-title,
|
||||
.glightbox-mobile .glightbox-container .gslide-description {
|
||||
background: none;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
468
.vitepress/theme/styles/global.css
Normal file
468
.vitepress/theme/styles/global.css
Normal file
@@ -0,0 +1,468 @@
|
||||
:root {
|
||||
--vp-font-family-base: 'Montserrat', sans-serif;
|
||||
--vp-shadow-6: 0px 4px 15px rgba(47, 63, 147, 0.08);
|
||||
--vp-border: var(--vp-border-width) solid var(--vp-c-bg-soft);
|
||||
--vp-border-width: 4px;
|
||||
--vp-border-radius: 20px;
|
||||
|
||||
--vp-c-gray-1: #f7f7f7;
|
||||
|
||||
--vp-c-blue-1: #0059C8;
|
||||
--vp-c-blue-2: #0152b6;
|
||||
--vp-c-blue-3: #0764d4;
|
||||
--vp-c-blue-soft: #005ac80e;
|
||||
|
||||
--vp-c-brand-1: var(--vp-c-blue-1);
|
||||
--vp-c-brand-2: var(--vp-c-blue-2);
|
||||
--vp-c-brand-3: var(--vp-c-blue-3);
|
||||
--vp-c-brand-soft: var(--vp-c-blue-soft);
|
||||
|
||||
--vp-c-sponsor-1: #eab11f;
|
||||
--vp-c-sponsor-2: #e0a615;
|
||||
--vp-c-sponsor-3: #ecb732;
|
||||
--vp-c-sponsor-soft: rgba(236, 183, 50, 0.14);
|
||||
|
||||
--vp-button-sponsor-border: var(--vp-c-sponsor-3);
|
||||
--vp-button-sponsor-text: var(--vp-c-black);
|
||||
--vp-button-sponsor-hover-border: var(--vp-c-sponsor-3);
|
||||
--vp-button-sponsor-hover-text: var(--vp-c-black);
|
||||
--vp-button-sponsor-hover-bg: var(--vp-c-sponsor-3);
|
||||
--vp-button-sponsor-active-border: var(--vp-c-sponsor-2);
|
||||
--vp-button-sponsor-active-text: var(--vp-c-black);
|
||||
--vp-button-sponsor-active-bg: var(--vp-c-sponsor-2);
|
||||
|
||||
--vp-code-color: var(--vp-c-blue-3);
|
||||
|
||||
--vp-custom-block-info-border: var(--vp-c-blue-soft);
|
||||
--vp-custom-block-info-bg: var(--vp-c-blue-soft);
|
||||
--vp-custom-block-info-text: var(--vp-c-neutral);
|
||||
|
||||
--vp-custom-block-tip-border: var(--vp-c-blue-soft);
|
||||
--vp-custom-block-tip-bg: var(--vp-c-blue-soft);
|
||||
--vp-custom-block-tip-text: var(--vp-c-neutral);
|
||||
|
||||
--vp-custom-block-warning-border: var(--vp-c-yellow-soft);
|
||||
--vp-custom-block-warning-bg: var(--vp-c-yellow-soft);
|
||||
--vp-custom-block-warning-text: var(--vp-c-neutral);
|
||||
|
||||
--vp-custom-block-danger-border: var(--vp-c-danger-soft);
|
||||
--vp-custom-block-danger-bg: var(--vp-c-danger-soft);
|
||||
--vp-custom-block-danger-text: var(--vp-c-neutral);
|
||||
|
||||
--vp-custom-block-details-border: var(--vp-c-divider);
|
||||
--vp-custom-block-details-bg: var(--vp-c-bg-soft);
|
||||
|
||||
--vp-local-search-highlight-bg: transparent;
|
||||
--vp-local-search-highlight-text: var(--vp-c-blue-1);
|
||||
--vp-local-search-bg: var(--vp-c-bg-soft);
|
||||
--vp-local-search-result-bg: var(--vp-c-white);
|
||||
--vp-local-search-result-shadow: 0 1px 3px 0 #d4d9e1;
|
||||
--vp-local-search-footer-shadow: 0 -1px 0 0 #e0e3e8, 0 -3px 6px 0 rgba(69, 98, 155, .12);
|
||||
|
||||
--vp-home-hero-name-color: transparent;
|
||||
--vp-home-hero-name-background: -webkit-linear-gradient(120deg, var(--vp-c-brand) 30%, var(--vp-c-brand-dark));
|
||||
--vp-home-hero-image-background-image: linear-gradient(-45deg, var(--vp-c-brand-light) 50%, var(--vp-c-brand-lighter) 50%);
|
||||
--vp-home-hero-image-filter: blur(40px);
|
||||
}
|
||||
|
||||
[lang|="ru"] {
|
||||
--vp-code-copy-copied-text-content: 'Скопировано';
|
||||
}
|
||||
|
||||
.dark {
|
||||
--vp-c-blue-1: #7da1e9;
|
||||
--vp-c-blue-2: #6382c2;
|
||||
--vp-c-blue-3: #86a2da;
|
||||
--vp-local-search-result-bg: var(--vp-c-bg-soft-mute);
|
||||
--vp-local-search-result-shadow: none;
|
||||
--vp-local-search-footer-shadow: none;
|
||||
--vp-home-hero-image-background-image: linear-gradient(-45deg, var(--vp-c-brand-darker) 25%, var(--vp-c-brand-darkest) 25%);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
:root {
|
||||
--vp-home-hero-image-filter: blur(56px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
:root {
|
||||
--vp-home-hero-image-filter: blur(72px);
|
||||
}
|
||||
}
|
||||
|
||||
main figure {
|
||||
margin: 2rem 0;
|
||||
transition: transform var(--vp-tt);
|
||||
}
|
||||
|
||||
main img {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 24px;
|
||||
border-radius: var(--vp-border-radius);
|
||||
box-shadow: 2px 2px 8px 4px var(--vp-c-bg-alt);
|
||||
}
|
||||
|
||||
figcaption {
|
||||
text-align: center;
|
||||
margin-top: 1.25rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
main figure > a .external-link-icon {
|
||||
display: none
|
||||
}
|
||||
|
||||
figure figcaption {
|
||||
color: inherit
|
||||
}
|
||||
|
||||
.custom-block.tip figure img {
|
||||
--vp-c-bg-alt: var(--vp-custom-block-tip-bg)
|
||||
}
|
||||
|
||||
main :where(h1, h2, h3, h4, h5, h6) + figure {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.Layout {
|
||||
margin-right: var(--scrollbar-width);
|
||||
}
|
||||
|
||||
.VPNav {
|
||||
background-color: var(--vp-c-bg);
|
||||
border-radius: 0 0 var(--vp-border-radius) var(--vp-border-radius);
|
||||
box-shadow: var(--vp-shadow-6);
|
||||
padding-right: var(--scrollbar-width);
|
||||
}
|
||||
|
||||
.VPLocalNav {
|
||||
border-top: none !important;
|
||||
}
|
||||
|
||||
.VPNavBar.has-sidebar {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.VPNavBarTitle .title {
|
||||
font-size: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.VPSidebar {
|
||||
left: calc((var(--scrollbar-width) / 2) * -1) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.VPSidebarItem.level-0 {
|
||||
padding-bottom: 10px !important;
|
||||
}
|
||||
|
||||
.VPDocAsideOutline {
|
||||
border: var(--vp-border);
|
||||
border-radius: var(--vp-border-radius);
|
||||
padding: 20px 20px 20px 0;
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .content {
|
||||
padding-left: 20px;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.VPDocAsideOutline .outline-marker {
|
||||
width: 4px;
|
||||
left: -4px;
|
||||
}
|
||||
|
||||
.VPDoc .content-container {
|
||||
max-width: unset !important;
|
||||
}
|
||||
|
||||
.pager .pager-link {
|
||||
border: 1px solid var(--vp-c-gray-soft);
|
||||
box-shadow: var(--vp-shadow-1);
|
||||
}
|
||||
|
||||
.pager .pager-link .title {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.curtain {
|
||||
display: none;
|
||||
}
|
||||
|
||||
kbd {
|
||||
border: 1px solid var(--vp-c-border);
|
||||
box-shadow: var(--vp-c-gray-soft) 0px 1px 1px 0px, var(--vp-c-neutral-inverse) 0px 1px 0px 0px inset;
|
||||
background-color: var(--vp-c-gray-soft);
|
||||
border-radius: 3px;
|
||||
display: inline-block;
|
||||
margin: 0 .1em;
|
||||
overflow-wrap: break-word;
|
||||
padding: .1em .6em;
|
||||
text-shadow: 0 1px 0 var(--vp-c-white);
|
||||
}
|
||||
|
||||
.vp-doc p {
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.vp-doc li + li {
|
||||
margin-top: .25em;
|
||||
}
|
||||
|
||||
.vp-doc table {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.vp-doc ol {
|
||||
counter-reset: li;
|
||||
list-style: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.vp-doc ol > li {
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
.vp-doc ol > li::before {
|
||||
width: calc(1.5rem - 2px);
|
||||
height: calc(1.5rem - 2px);
|
||||
font-size: calc(1rem - 2px);
|
||||
margin: 1px 0 0 .5rem;
|
||||
content: counter(li);
|
||||
counter-increment: li;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--vp-c-text-1);
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
background-color: var(--vp-sidebar-bg-color);
|
||||
}
|
||||
|
||||
.custom-block {
|
||||
border-radius: var(--vp-border-radius);
|
||||
border-width: var(--vp-border-width);
|
||||
}
|
||||
|
||||
.custom-block.info a,
|
||||
.custom-block.info code {
|
||||
color: var(--vp-c-blue-1);
|
||||
}
|
||||
|
||||
.custom-block.info a:hover {
|
||||
color: var(--vp-c-blue-2);
|
||||
}
|
||||
|
||||
.navigation {
|
||||
color: var(--vp-c-brand-2);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.navigation:hover {
|
||||
color: var(--vp-c-brand-1);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.navigation svg,
|
||||
span.name {
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
bottom: 1px;
|
||||
}
|
||||
|
||||
.navigation svg {
|
||||
fill: currentColor;
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
display: inline-block;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.navigation.direction {
|
||||
fill: currentColor;
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.plugin-tabs {
|
||||
border-radius: var(--vp-border-radius);
|
||||
}
|
||||
|
||||
.plugin-tabs--content {
|
||||
padding: 2rem !important;
|
||||
}
|
||||
|
||||
.plugin-tabs--tab {
|
||||
position: relative;
|
||||
padding: 0 12px;
|
||||
line-height: 54px;
|
||||
border-bottom: 2px solid transparent;
|
||||
color: var(--vp-plugin-tabs-tab-text-color);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
transition: color 0.50s;
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
.vp-doc table {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.vp-doc table tbody {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.vp-doc table thead {
|
||||
border: none;
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.vp-doc table tr {
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
display: block;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.vp-doc table tr:nth-child(2n) {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.vp-doc table td {
|
||||
border-top: none;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.vp-doc table td::before {
|
||||
display: block;
|
||||
content: attr(data-label) ':';
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.vp-doc table td:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vp-doc table td:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.DocSearch-Button {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background-color: var(--vp-c-bg-elv);
|
||||
border: 1px solid var(--vp-c-gray-soft);
|
||||
box-shadow: var(--vp-shadow-1);
|
||||
}
|
||||
|
||||
.DocSearch-Button:hover {
|
||||
background-color: var(--vp-c-bg-elv);
|
||||
border: 1px solid var(--vp-c-gray-2);
|
||||
box-shadow: var(--vp-shadow-1);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.DocSearch-Button {
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.screenshots {
|
||||
display: flex;
|
||||
margin: 0 auto 0;
|
||||
max-width: 1152px;
|
||||
}
|
||||
|
||||
.shot {
|
||||
flex: 33.33%;
|
||||
padding: 12px;
|
||||
margin: 12px auto 0;
|
||||
}
|
||||
|
||||
/* Parsers lib layout */
|
||||
.dl {
|
||||
color: var(--vp-c-text-1);
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
gap: 1rem;
|
||||
max-width: 1152px;
|
||||
margin: 0 auto 0;
|
||||
}
|
||||
|
||||
.dl h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 600;
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
.dl .btn {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
border: 4px solid transparent;
|
||||
border-radius: 8px;
|
||||
padding: 8px 18px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
transition: all 0.25s, color 0.25s;
|
||||
}
|
||||
|
||||
/* Parsers lib layout */
|
||||
.parsers {
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-1);
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
gap: 1rem;
|
||||
max-width: 1152px;
|
||||
margin: 0 auto 0;
|
||||
border-radius: var(--vp-border-radius);
|
||||
}
|
||||
|
||||
.parsers h1 {
|
||||
font-size: 2.2rem;
|
||||
font-weight: 600;
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
.parsers .btn {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
border: 4px solid transparent;
|
||||
border-radius: 8px;
|
||||
padding: 8px 18px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
transition: all 0.25s, color 0.25s;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 24px auto 18px;
|
||||
width: 64px;
|
||||
height: 1px;
|
||||
background-color: var(--vp-c-divider);
|
||||
}
|
||||
23
.vitepress/theme/types/index.ts
Normal file
23
.vitepress/theme/types/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { DefaultTheme } from 'vitepress'
|
||||
import type { ComponentData } from '../plugins/component'
|
||||
|
||||
export namespace Theme {
|
||||
export interface Sponsor {
|
||||
message?: string
|
||||
linkText?: string
|
||||
link?: string
|
||||
}
|
||||
|
||||
export interface Config extends DefaultTheme.Config {
|
||||
titleSeparator?: string
|
||||
components?: Array<ComponentData>
|
||||
teamSectionTitle?: string
|
||||
|
||||
sponsorLink?: string
|
||||
sponsor?: Sponsor
|
||||
}
|
||||
|
||||
export interface TeamMember extends Omit<DefaultTheme.TeamMember, 'name'> {
|
||||
name: Record<string, string>
|
||||
}
|
||||
}
|
||||
111
.vitepress/theme/utils.ts
Normal file
111
.vitepress/theme/utils.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import type { DefaultTheme, UserConfig } from 'vitepress'
|
||||
import type { ComponentData, DocsPageData } from './plugins/component'
|
||||
import { normalize } from 'vitepress/dist/client/shared'
|
||||
|
||||
import { type Translator, translators } from '../../website/translators'
|
||||
import { Theme } from './types'
|
||||
|
||||
export function findPath(
|
||||
pageData: DocsPageData,
|
||||
config: UserConfig,
|
||||
): DefaultTheme.SidebarItem[] {
|
||||
let searchable = normalize(pageData.relativePath)
|
||||
|
||||
const localeLinks = Object.entries(config.locales).flatMap(([key]) => ({ key, link: key === 'root' ? '/' : `${key}/` }))
|
||||
if (localeLinks.some(({ link }) => link === searchable)) return []
|
||||
|
||||
const locale = localeLinks.find(locale => locale.link.startsWith(searchable.replace(/(^.*?\/).*$/, '$1'))) || localeLinks[0]
|
||||
searchable = ensureStartingSlash(searchable)
|
||||
const localeConfig: Theme.Config = config.locales[locale.key].themeConfig
|
||||
const root = localeConfig.nav.find(item => {
|
||||
if (!('link' in item)) {
|
||||
return false
|
||||
}
|
||||
const tmp = locale.link === '/' ? item.link.replace(/(^\/.*?\/).*$/, '$1') : item.link.replace(/(^\/.*?\/)(.*?\/).*$/, '$1$2')
|
||||
return searchable.startsWith(tmp)
|
||||
})
|
||||
const path: DefaultTheme.SidebarItem[] = []
|
||||
if (root && ('link' in root)) {
|
||||
path.push({ text: root.text, link: root.link })
|
||||
}
|
||||
|
||||
let tree: DefaultTheme.SidebarItem | ComponentData
|
||||
|
||||
if (pageData.component) {
|
||||
const { title, link, items } = pageData.component
|
||||
tree = pageData.component
|
||||
path.push({ text: title, link, items })
|
||||
} else {
|
||||
const sidebar = Object.entries(localeConfig.sidebar).find(([link]) => searchable.startsWith(link))
|
||||
if (sidebar) tree = sidebar[1]
|
||||
}
|
||||
|
||||
if (!tree) return path
|
||||
|
||||
const keyExists = (tree) => {
|
||||
if (!tree || (typeof tree !== 'object' && !Array.isArray(tree.items) && !Array.isArray(tree))) {
|
||||
return false
|
||||
}
|
||||
else if (tree.hasOwnProperty('link') && ensureStartingSlash(tree.link) === searchable) {
|
||||
return true
|
||||
}
|
||||
else if (Array.isArray(tree)) {
|
||||
for (let i = 0; i < tree.length; i++) {
|
||||
const item = tree[i]
|
||||
path.push(item)
|
||||
const result = keyExists(item)
|
||||
if (result) {
|
||||
return result
|
||||
}
|
||||
|
||||
path.pop()
|
||||
}
|
||||
}
|
||||
else if (Array.isArray(tree.items)) {
|
||||
for (let i = 0; i < tree.items.length; i++) {
|
||||
const item = tree.items[i]
|
||||
path.push(item)
|
||||
const result = keyExists(item)
|
||||
if (result) {
|
||||
return result
|
||||
}
|
||||
|
||||
path.pop()
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
keyExists(tree)
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
export function ellipsis(
|
||||
string: string = '',
|
||||
length: number = 0,
|
||||
etc: string = '...'
|
||||
): string {
|
||||
if (string.length <= length) {
|
||||
return string
|
||||
}
|
||||
|
||||
return string.substring(0, length) + (string.length > length ? etc : '')
|
||||
}
|
||||
|
||||
export function ensureStartingSlash(path: string): string {
|
||||
return /^\//.test(path) ? path : `/${path}`
|
||||
}
|
||||
|
||||
export function getTranslator(translator: string): Translator | undefined {
|
||||
if (!translator) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(translators, translator)) {
|
||||
return
|
||||
}
|
||||
|
||||
return translators[translator]
|
||||
}
|
||||
Reference in New Issue
Block a user