monorepofiy

This commit is contained in:
taskylizard
2024-02-25 01:31:53 +00:00
parent b693586301
commit 9065a0aeb7
42 changed files with 14 additions and 6 deletions

109
docs/.vitepress/config.mts Normal file
View File

@@ -0,0 +1,109 @@
import { defineConfig } from 'vitepress'
import UnoCSS from 'unocss/vite'
import consola from 'consola'
import {
commitRef,
feedback,
meta,
search,
sidebar,
socialLinks
} from './constants'
import { generateImages, generateMeta, generateFeed } from './hooks'
import { toggleStarredPlugin } from './markdown/toggleStarred'
import { movePlugin, emojiRender, defs } from './markdown/emoji'
// @unocss-include
const baseUrl = process.env.GITHUB_ACTIONS ? '/FMHYedit' : '/'
export default defineConfig({
title: 'FMHY',
description: meta.description,
titleTemplate: ':title • freemediaheckyeah',
lang: 'en-US',
lastUpdated: false,
cleanUrls: true,
appearance: 'dark',
base: baseUrl,
srcExclude: ['readme.md', 'single-page'],
ignoreDeadLinks: true,
sitemap: {
hostname: meta.hostname
},
head: [
['meta', { name: 'theme-color', content: '#7bc5e4' }],
['meta', { name: 'og:type', content: 'website' }],
['meta', { name: 'og:locale', content: 'en' }],
['link', { rel: 'icon', href: '/test.png' }],
// PWA
['link', { rel: 'icon', href: '/test.png', type: 'image/svg+xml' }],
['link', { rel: 'alternate icon', href: '/test.png' }],
['link', { rel: 'mask-icon', href: '/test.png', color: '#7bc5e4' }],
// prettier-ignore
["meta", { name: "keywords", content: meta.keywords.join(" ") }],
['link', { rel: 'apple-touch-icon', href: '/test.png', sizes: '192x192' }]
],
transformHead: async (context) => generateMeta(context, meta.hostname),
buildEnd: async (context) => {
generateImages(context)
.then(() => generateFeed(context))
.finally(() => consola.success('Success!'))
},
vite: {
optimizeDeps: { exclude: ['workbox-window'] },
plugins: [
UnoCSS({
configFile: '../unocss.config.ts'
}),
{
name: 'custom:adjust-order',
configResolved(c) {
movePlugin(
c.plugins as any,
'vitepress',
'before',
'unocss:transformers:pre'
)
}
}
],
build: {
// Shut the fuck up
chunkSizeWarningLimit: Number.POSITIVE_INFINITY
}
},
markdown: {
emoji: { defs },
config(md) {
md.use(emojiRender)
md.use(toggleStarredPlugin)
}
},
themeConfig: {
search,
footer: {
message: `${feedback} (rev: ${commitRef})`
},
outline: 'deep',
logo: '/fmhy.ico',
nav: [
{ text: 'Beginners Guide', link: '/beginners-guide' },
{ text: 'Glossary', link: 'https://rentry.org/The-Piracy-Glossary' },
{ text: 'Guides', link: 'https://rentry.co/fmhy-guides' },
{
text: 'Backups',
link: 'https://github.com/fmhy/FMHY/wiki/Backups'
},
{
text: 'Ecosystem',
items: [
{ text: 'Posts', link: '/posts' },
{ text: 'Feedback', link: '/feedback' },
{ text: 'snowbin', link: 'https://pastes.fmhy.net' }
]
}
],
sidebar,
socialLinks
}
})

View File

@@ -0,0 +1,180 @@
import type { DefaultTheme } from 'vitepress'
// @unocss-include
export const meta = {
name: 'freemediaheckyeah',
description: 'The largest collection of free stuff on the internet!',
hostname: 'https://fmhy.net',
keywords: ['stream', 'movies', 'gaming', 'reading', 'anime']
}
export const commitRef = process.env.CF_PAGES
? `<a href="https://github.com/fmhy/FMHYEdit/commit/${
process.env.CF_PAGES_COMMIT_SHA
}">${process.env.CF_PAGES_COMMIT_SHA.slice(0, 8)}</a>`
: 'dev'
export const feedback = `<a href="/feedback" class="feedback-footer">Made with ❤️</a>`
export const search: DefaultTheme.Config['search'] = {
options: {
miniSearch: {
searchOptions: {
combineWith: 'AND',
fuzzy: false,
// @ts-ignore
boostDocument: (
_,
term,
storedFields: Record<string, string | string[]>
) => {
const titles = (storedFields?.titles as string[])
.filter((t) => Boolean(t))
.map((t) => t.toLowerCase())
// Uprate if term appears in titles. Add bonus for higher levels (i.e. lower index)
const titleIndex =
titles
.map((t, i) => (t?.includes(term) ? i : -1))
.find((i) => i >= 0) ?? -1
if (titleIndex >= 0) return 10000 - titleIndex
return 1
}
}
},
detailedView: true
},
provider: 'local'
}
export const socialLinks: DefaultTheme.SocialLink[] = [
{ icon: 'github', link: 'https://github.com/fmhy/FMHYEdit' },
{ icon: 'discord', link: 'https://discord.gg/Stz6y6NgNg' },
{
icon: 'reddit',
link: 'https://reddit.com/r/FREEMEDIAHECKYEAH'
}
]
export const sidebar: DefaultTheme.Sidebar | DefaultTheme.NavItemWithLink[] = [
{
text: '<span class="i-twemoji:name-badge"></span> Adblocking / Privacy',
link: '/adblockvpnguide'
},
{
text: '<span class="i-twemoji:robot"></span> Artificial Intelligence',
link: '/ai'
},
{
text: '<span class="i-twemoji:television"></span> Movies / TV / Anime',
link: '/videopiracyguide'
},
{
text: '<span class="i-twemoji:musical-note"></span> Music / Podcasts / Radio',
link: '/audiopiracyguide'
},
{
text: '<span class="i-twemoji:video-game"></span> Gaming / Emulation',
link: '/gamingpiracyguide'
},
{
text: '<span class="i-twemoji:green-book"></span> Books / Comics / Manga',
link: '/readingpiracyguide'
},
{
text: '<span class="i-twemoji:floppy-disk"></span> Downloading',
link: '/downloadpiracyguide'
},
{
text: '<span class="i-twemoji:cyclone"></span> Torrenting',
link: '/torrentpiracyguide'
},
{
text: '<span class="i-twemoji:brain"></span> Educational',
link: '/edupiracyguide'
},
{
text: '<span class="i-twemoji:mobile-phone"></span> Android / iOS',
link: '/android-iosguide'
},
{
text: '<span class="i-twemoji:penguin"></span> Linux / MacOS',
link: '/linuxguide'
},
{
text: '<span class="i-twemoji:globe-showing-asia-australia"></span> Non-English',
link: '/non-english'
},
{
text: '<span class="i-twemoji:file-folder"></span> Miscellaneous',
link: '/miscguide'
},
{
text: '<span class="i-twemoji:wrench"></span> Tools',
collapsed: false,
items: [
{
text: '<span class="i-twemoji:laptop"></span> System Tools',
link: '/system-tools'
},
{
text: '<span class="i-twemoji:card-file-box"></span> File Tools',
link: '/file-tools'
},
{
text: '<span class="i-twemoji:paperclip"></span> Internet Tools',
link: '/internet-tools'
},
{
text: '<span class="i-twemoji:left-speech-bubble"></span> Social Media Tools',
link: '/social-media-tools'
},
{
text: '<span class="i-twemoji:memo"></span> Text Tools',
link: '/text-tools'
},
{
text: '<span class="i-twemoji:alien-monster"></span> Gaming Tools',
link: '/gamingpiracyguide#gaming-tools'
},
{
text: '<span class="i-twemoji:camera"></span> Image Tools',
link: '/img-tools'
},
{
text: '<span class="i-twemoji:videocassette"></span> Video Tools',
link: '/video-tools'
},
{
text: '<span class="i-twemoji:speaker-high-volume"></span> Audio Tools',
link: '/audiopiracyguide#audio-tools'
},
{
text: '<span class="i-twemoji:red-apple"></span> Educational Tools',
link: '/edupiracyguide#educational-tools'
},
{
text: '<span class="i-twemoji:man-technologist"></span> Developer Tools',
link: '/devtools'
}
]
},
{
text: '<span class="i-twemoji:plus"></span> More',
collapsed: true,
items: [
{
text: '<span class="i-twemoji:no-one-under-eighteen"></span> NSFW',
link: '/nsfwpiracy'
},
{
text: '<span class="i-twemoji:warning"></span> Unsafe Sites',
link: '/unsafesites'
},
{
text: '<span class="i-twemoji:package"></span> Storage',
link: '/storage'
}
]
}
]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,27 @@
<script setup lang="ts">
defineProps<{ title: string; description?: string }>()
</script>
<template>
<div
tw="w-full h-full bg-black flex flex-col"
style="background-image: url(https://fmhy.pages.dev/og.png)"
>
<div
tw="p-10 w-full min-h-0 grow flex flex-col items-center justify-between"
>
<div tw="w-full flex justify-between items-center text-5xl font-medium">
<div tw="flex items-center">
<div tw="text-zinc-100 ml-2 mt-1 font-semibold">
freemediaheckyeah
</div>
</div>
</div>
<div tw="w-full pr-56 flex flex-col items-start justify-end">
<div style="color: #f3f4f6" tw="text-6xl font-bold" v-html="title" />
<div style="color: #c0caf5" tw="mt-2 text-4xl" v-html="description" />
</div>
</div>
<div tw="shrink-0 h-2 w-full flex" style="background-color: #c4b5fd" />
</div>
</template>

View File

@@ -0,0 +1,8 @@
/**
* Barrel generated using @taskylizard/tasker.
*/
export * from './meta'
export * from './opengraph'
export * from './rss'
export * from './satoriConfig'

View File

@@ -0,0 +1,100 @@
import type { HeadConfig, TransformContext } from 'vitepress'
export function generateMeta(context: TransformContext, hostname: string) {
const head: HeadConfig[] = []
const { pageData } = context
const url = `${hostname}/${pageData.relativePath.replace(/((^|\/)index)?\.md$/, '$2')}`
head.push(
['link', { rel: 'canonical', href: url }],
['meta', { property: 'og:url', content: url }],
['meta', { name: 'twitter:url', content: url }],
['meta', { name: 'twitter:card', content: 'summary_large_image' }],
['meta', { property: 'og:title', content: pageData.frontmatter.title }],
['meta', { name: 'twitter:title', content: pageData.frontmatter.title }]
)
if (pageData.frontmatter.description) {
head.push(
[
'meta',
{
property: 'og:description',
content: pageData.frontmatter.description
}
],
[
'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`
.replaceAll('//', '/')
.replace(/^\//, '')
head.push(
['meta', { property: 'og:image', content: `${hostname}/${imageUrl}` }],
['meta', { property: 'og:image:width', content: '1200' }],
['meta', { property: 'og:image:height', content: '628' }],
['meta', { property: 'og:image:type', content: 'image/png' }],
[
'meta',
{ property: 'og:image:alt', content: pageData.frontmatter.title }
],
['meta', { name: 'twitter:image', content: `${hostname}/${imageUrl}` }],
['meta', { name: 'twitter:image:width', content: '1200' }],
['meta', { name: 'twitter:image:height', content: '628' }],
[
'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
}

View File

@@ -0,0 +1,96 @@
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'
import consola from 'consola'
const __dirname = dirname(fileURLToPath(import.meta.url))
const __fonts = resolve(__dirname, '../fonts')
export async function generateImages(config: SiteConfig): Promise<void> {
const pages = await createContentLoader('**/*.md', { excerpt: true }).load()
const template = await readFile(resolve(__dirname, './Template.vue'), 'utf-8')
const fonts: SatoriOptions['fonts'] = [
{
name: 'Inter',
data: await readFile(resolve(__fonts, 'Inter-Regular.otf')),
weight: 400,
style: 'normal'
},
{
name: 'Inter',
data: await readFile(resolve(__fonts, 'Inter-Medium.otf')),
weight: 500,
style: 'normal'
},
{
name: 'Inter',
data: await readFile(resolve(__fonts, 'Inter-SemiBold.otf')),
weight: 600,
style: 'normal'
},
{
name: 'Inter',
data: await readFile(resolve(__fonts, 'Inter-Bold.otf')),
weight: 700,
style: 'normal'
}
]
for (const page of pages) {
await generateImage({
page,
template,
outDir: config.outDir,
fonts
})
}
return consola.info('Generated opengraph images.')
}
interface GenerateImagesOptions {
page: ContentData
template: string
outDir: string
fonts: SatoriOptions['fonts']
}
async function generateImage({
page,
template,
outDir,
fonts
}: GenerateImagesOptions): Promise<void> {
const { frontmatter, url } = page
const options: SatoriOptions = {
width: 1200,
height: 628,
fonts,
props: {
title:
frontmatter.layout === 'home'
? frontmatter.hero.name ?? frontmatter.title
: frontmatter.title,
description:
frontmatter.layout === 'home'
? frontmatter.hero.tagline ?? frontmatter.description
: frontmatter.description
}
}
const svg = await satoriVue(options, template)
const render = await renderAsync(svg)
const outputFolder = resolve(outDir, url.slice(1), '__og_image__')
const outputFile = resolve(outputFolder, 'og.png')
await mkdir(outputFolder, { recursive: true })
await writeFile(outputFile, render.asPng())
}

View File

@@ -0,0 +1,49 @@
import path from 'node:path'
import { writeFileSync } from 'node:fs'
import { Feed } from 'feed'
import {
createContentLoader,
type ContentData,
type SiteConfig
} from 'vitepress'
import consola from 'consola'
import { meta } from '../constants'
export async function generateFeed(config: SiteConfig): Promise<void> {
const feed: Feed = new Feed({
id: meta.hostname,
link: meta.hostname,
title: `FMHY blog`,
description: meta.description,
language: 'en-US',
image: 'https://github.com/fmhy.png',
favicon: `${meta.hostname}/favicon.ico`,
copyright: `Copyright (c) 2023-present FMHY`
})
const posts: ContentData[] = await createContentLoader('posts/*.md', {
excerpt: true,
render: true,
transform: (rawData) => {
return rawData.sort((a, b) => {
return (
Number(new Date(b.frontmatter.date)) -
Number(new Date(a.frontmatter.date))
)
})
}
}).load()
for (const { url, frontmatter, html } of posts) {
feed.addItem({
title: frontmatter.title as string,
id: `${meta.hostname}${url.replace(/\/\d+\./, '/')}`,
link: `${meta.hostname}${url.replace(/\/\d+\./, '/')}`,
date: frontmatter.date,
content: html!
})
}
writeFileSync(path.join(config.outDir, 'feed.rss'), feed.rss2())
return consola.info('Generated rss feed.')
}

View File

@@ -0,0 +1,47 @@
import { readFile } from 'node:fs/promises'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import type { SatoriOptions } from 'x-satori/vue'
import { defineSatoriConfig } from 'x-satori/vue'
const __dirname = dirname(fileURLToPath(import.meta.url))
const __fonts = resolve(__dirname, '../fonts')
const fonts: SatoriOptions['fonts'] = [
{
name: 'Inter',
data: await readFile(resolve(__fonts, 'Inter-Regular.otf')),
weight: 400,
style: 'normal'
},
{
name: 'Inter',
data: await readFile(resolve(__fonts, 'Inter-Medium.otf')),
weight: 500,
style: 'normal'
},
{
name: 'Inter',
data: await readFile(resolve(__fonts, 'Inter-SemiBold.otf')),
weight: 600,
style: 'normal'
},
{
name: 'Inter',
data: await readFile(resolve(__fonts, 'Inter-Bold.otf')),
weight: 700,
style: 'normal'
}
]
export default defineSatoriConfig({
width: 1200,
height: 628,
fonts,
props: {
title: 'Title',
description:
'Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat.',
dir: '/j'
}
})

View File

@@ -0,0 +1,29 @@
import { type MarkdownRenderer } from 'vitepress'
// FIXME: tasky: possibly write less horror jank?
export function base64DecodePlugin(md: MarkdownRenderer) {
const decode = (str: string): string =>
Buffer.from(str, 'base64').toString('binary')
// Save the original rule for backticks
const defaultRender =
md.renderer.rules.code_inline ||
function (tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options)
}
md.renderer.rules.code_inline = function (tokens, idx, options, env, self) {
// @ts-expect-error shut the fuck up already I HATE THIS
if (
!env.frontmatter.title ||
(env.frontmatter.title && !env.frontmatter.title === 'base64')
) {
return defaultRender(tokens, idx, options, env, self)
}
const token = tokens[idx]
const content = token.content
return `<button class='base64' onclick="(function(btn){ const codeEl = btn.querySelector('code'); navigator.clipboard.writeText('${decode(
content
)}').then(() => { const originalText = codeEl.textContent; codeEl.textContent = 'Copied'; setTimeout(() => codeEl.textContent = originalText, 3000); }).catch(console.error); })(this)"><code>${content}</code></button>`
}
}

View File

@@ -0,0 +1,42 @@
import { icons as twemoji } from '@iconify-json/twemoji'
import type { MarkdownRenderer } from 'vitepress'
export const defs = {
...Object.fromEntries(
Object.entries(twemoji.icons).map(([key]) => {
return [key, '']
})
)
}
export function emojiRender(md: MarkdownRenderer) {
md.renderer.rules.emoji = (tokens, idx) => {
if (tokens[idx].markup.startsWith('star')) {
return `<span class="i-twemoji-${tokens[idx].markup} starred"></span>`
}
return `<span class="i-twemoji-${tokens[idx].markup}"></span>`
}
}
export function movePlugin(
plugins: { name: string }[],
pluginAName: string,
order: 'before' | 'after',
pluginBName: string
) {
const pluginBIndex = plugins.findIndex((p) => p.name === pluginBName)
if (pluginBIndex === -1) return
const pluginAIndex = plugins.findIndex((p) => p.name === pluginAName)
if (pluginAIndex === -1) return
if (order === 'before' && pluginAIndex > pluginBIndex) {
const pluginA = plugins.splice(pluginAIndex, 1)[0]
plugins.splice(pluginBIndex, 0, pluginA)
}
if (order === 'after' && pluginAIndex < pluginBIndex) {
const pluginA = plugins.splice(pluginAIndex, 1)[0]
plugins.splice(pluginBIndex, 0, pluginA)
}
}

View File

@@ -0,0 +1,18 @@
import type { MarkdownRenderer } from 'vitepress'
const excluded = ['Beginners Guide']
export function toggleStarredPlugin(md: MarkdownRenderer) {
md.renderer.rules.list_item_open = (tokens, index, options, env, self) => {
const contentToken = tokens[index + 2]
if (
!excluded.includes(env.frontmatter.title) &&
contentToken &&
contentToken.content.startsWith(':star:')
) {
return `<li class="starred">`
} else {
return self.renderToken(tokens, index, options)
}
}
}

View File

@@ -0,0 +1,83 @@
<script setup lang="ts">
import DefaultTheme from 'vitepress/theme'
import { useData } from 'vitepress'
import { nextTick, provide } from 'vue'
import Sidebar from './components/SidebarCard.vue'
import Announcement from './components/Announcement.vue'
const { isDark } = useData()
const enableTransitions = () =>
'startViewTransition' in document &&
window.matchMedia('(prefers-reduced-motion: no-preference)').matches
provide('toggle-appearance', async ({ clientX: x, clientY: y }: MouseEvent) => {
if (!enableTransitions()) {
isDark.value = !isDark.value
return
}
const clipPath = [
`circle(0px at ${x}px ${y}px)`,
`circle(${Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y)
)}px at ${x}px ${y}px)`
]
// @ts-expect-error
await document.startViewTransition(async () => {
isDark.value = !isDark.value
await nextTick()
}).ready
document.documentElement.animate(
{ clipPath: isDark.value ? clipPath.reverse() : clipPath },
{
duration: 300,
easing: 'ease-in',
pseudoElement: `::view-transition-${isDark.value ? 'old' : 'new'}(root)`
}
)
})
const { Layout } = DefaultTheme
</script>
<template>
<Layout>
<template #sidebar-nav-after>
<Sidebar />
</template>
<template #home-hero-prelink>
<Announcement />
</template>
<Content />
</Layout>
</template>
<style>
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
::view-transition-old(root),
.dark::view-transition-new(root) {
z-index: 1;
}
::view-transition-new(root),
.dark::view-transition-old(root) {
z-index: 9999;
}
.VPSwitchAppearance {
width: 22px !important;
}
.VPSwitchAppearance .check {
transform: none !important;
}
</style>

View File

@@ -0,0 +1,24 @@
<script setup lang="ts">
import { useData } from 'vitepress'
import Authors from './components/Authors.vue'
const props = defineProps<{
authors: string[]
}>()
const formatDate = (raw: string): string => {
const date = new Date(raw)
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric'
})
}
const { frontmatter } = useData()
</script>
<template>
<h1>{{ frontmatter.title }}</h1>
<div>{{ frontmatter.description }} {{ formatDate(frontmatter.date) }}</div>
<Authors :authors="props.authors" />
</template>

View File

@@ -0,0 +1,67 @@
<!-- eslint-disable vue/require-v-for-key -->
<script setup lang="ts">
import { data as posts } from './posts.data'
const formatDate = (raw: string): string => {
const date = new Date(raw)
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric'
})
}
</script>
<template>
<div>
<section>
<h1 class="flex items-center gap-2">Posts</h1>
<p>Everything from Monthly Updates to fmhy updates.</p>
We also have a
<a href="/feed.rss" target="_blank" title="RSS feed">
<div class="i-carbon-rss vertical-top" />
RSS feed.
</a>
</section>
<template v-for="year in Object.keys(posts).reverse()" :key="year">
<h2>{{ year }}</h2>
<ul>
<li v-for="post of posts[year]" :key="post.url">
<article>
<a :href="post.url" class="border-none">{{ post.title }}</a>
-
<dl class="m-0 inline">
<dt class="sr-only">Published on</dt>
<dd class="m-0 inline">
<time :datetime="post.date" class="font-bold">
{{ formatDate(post.date) }}
</time>
</dd>
</dl>
</article>
</li>
</ul>
</template>
</div>
</template>
<style scoped>
.VPBadge {
border: 1px solid transparent;
border-radius: 8px;
display: inline-flex;
margin-left: 2px;
padding: 0 10px;
line-height: 22px;
font-size: 12px;
font-weight: 500;
transform: translateY(-2px);
align-items: center;
gap: 0.2rem;
padding-right: 10px;
vertical-align: middle;
color: var(--vp-badge-tip-text);
background-color: transparent;
border-color: var(--vp-custom-block-tip-outline);
}
</style>

View File

@@ -0,0 +1,15 @@
<script setup lang="ts">
import { useData } from 'vitepress'
const { frontmatter } = useData()
</script>
<template>
<a
v-if="frontmatter.hero.prelink"
:href="frontmatter.hero.prelink.link"
class="inline-flex items-center rounded-lg bg-[var(--vp-c-default-soft)] px-4 py-1 text-sm font-semibold mb-3"
>
{{ frontmatter.hero.prelink.title }}
</a>
</template>

View File

@@ -0,0 +1,49 @@
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
authors: string[]
}>()
interface Author {
name: string
github: string
}
const data = [
{
name: 'nbats',
github: 'https://github.com/nbats'
},
{
name: 'Kai',
github: 'https://github.com/Kai-FMHY'
},
{
name: 'taskylizard',
github: 'https://github.com/taskylizard'
},
{
name: 'zinklog',
github: 'https://github.com/zinklog2'
},
{
name: 'Q',
github: 'https://github.com/qiracy'
}
] satisfies Author[]
const authors = computed(() =>
data.filter((author) => props.authors.includes(author.name))
)
</script>
<template>
<div class="flex flex-wrap gap-4 pt-2">
<div v-for="(c, index) of authors" class="flex gap-2 items-center">
<img :src="`${c.github}.png`" class="w-8 h-8 rounded-full" />
<a :href="c.github">{{ c.name }}</a>
<span v-if="index < authors.length - 1"></span>
</div>
</div>
</template>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
defineProps<{
icon: string
}>()
</script>
<template>
<div class="flex items-center mb-[8px] g-[12px]">
<span class="flex items-center">
<div class="text-2xl" :class="icon" />
<div class="ml-2 text-sm text-[var(--vp-c-text-2)]">
<slot />
</div>
</span>
</div>
</template>

View File

@@ -0,0 +1,255 @@
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { useRouter } from 'vitepress'
import {
type FeedbackType,
getFeedbackOption,
feedbackOptions
} from '../../../../api/types/Feedback'
const loading = ref<boolean>(false)
const error = ref<unknown>(null)
const success = ref<boolean>(false)
const router = useRouter()
const feedback = reactive<FeedbackType>({ message: '' })
async function handleSubmit(type?: FeedbackType['type']) {
if (type) feedback.type = type
loading.value = true
const body: FeedbackType = {
message: feedback.message,
type: feedback.type,
page: router.route.path
}
try {
const response = await fetch('https://feedback.tasky.workers.dev', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
})
const data = await response.json()
if (data.error) {
error.value = data.error
return
}
if (data.status === 'ok') {
success.value = true
}
} catch (error) {
error.value = error
} finally {
loading.value = false
}
}
</script>
<template>
<div class="wrapper">
<Transition name="fade" mode="out-in">
<div v-if="!feedback.type" class="step">
<div>
<div>
<p class="heading">Feedback</p>
</div>
</div>
<div class="button-container">
<button
v-for="item in feedbackOptions"
:key="item.value"
class="btn"
@click="handleSubmit(item.value as FeedbackType['type'])"
>
<span>{{ item.label }}</span>
</button>
</div>
</div>
<div v-else-if="feedback.type && !success" class="step">
<div>
<p class="desc">Page: {{ router.route.path }}</p>
<div>
<span>{{ getFeedbackOption(feedback.type)?.label }}</span>
<button
style="margin-left: 0.5rem"
class="btn"
@click="feedback.type = undefined"
>
<span class="i-carbon-close-large">close</span>
</button>
</div>
</div>
<div v-if="feedback.type === 'suggestion'" class="text-sm mb-2">
<strong>🕹 Emulators</strong>
<p class="desc">
They're already on the
<a
class="text-primary font-bold text-underline"
href="https://emulation.gametechwiki.com/index.php/Main_Page"
>
Game Tech Wiki.
</a>
</p>
<strong>🔻 Leeches</strong>
<p class="desc">
They're already on the
<a
class="text-primary font-bold text-underline"
href="https://filehostlist.miraheze.org/wiki/Free_Premium_Leeches"
>
File Hosting Wiki.
</a>
</p>
<strong>🐧 Distros</strong>
<p class="desc">
They're already on
<a
class="text-primary font-bold text-underline"
href="https://distrowatch.com/"
>
DistroWatch.
</a>
</p>
<strong>🎲 Mining / Betting Sites</strong>
<p class="desc">
Don't post anything related to betting, mining, BINs, CCs, etc.
</p>
<strong>🎮 Multiplayer Game Hacks</strong>
<p class="desc">
Don't post any hacks/exploits that give unfair advantages in
multiplayer games.
</p>
</div>
<textarea
v-model="feedback.message"
autofocus
class="input"
placeholder="What a lovely wiki!"
/>
<p class="desc mb-2">
If you want a reply to your feedback, feel free to mention a contact
in the message or join our
<a
class="text-primary font-semibold text-underline"
href="https://discord.gg/Stz6y6NgNg"
>
Discord.
</a>
</p>
<button
type="submit"
class="btn btn-primary"
:disabled="
feedback.message.length < 5 || feedback.message.length > 1000
"
@click="handleSubmit()"
>
Submit
</button>
</div>
<div v-else class="step">
<p class="heading">Thanks for your feedback!</p>
</div>
</Transition>
</div>
</template>
<style scoped>
.step > * + * {
margin-top: 1rem;
}
.btn {
border: 1px solid var(--vp-c-divider);
background-color: var(--vp-c-bg);
border-radius: 8px;
transition:
border-color 0.25s,
background-color 0.25s;
display: inline-block;
font-size: 14px;
font-weight: 500;
line-height: 1.5;
margin: 0;
padding: 0.375rem 0.75rem;
text-align: center;
vertical-align: middle;
white-space: nowrap;
}
.btn:disabled {
opacity: 0.5;
}
.btn:hover {
border-color: var(--vp-c-brand);
}
.btn-primary {
color: #fff;
background-color: var(--vp-c-brand);
border-color: var(--vp-c-brand);
}
.btn-primary:hover {
background-color: var(--vp-c-brand-darker);
border-color: var(--vp-c-brand-darker);
}
.heading {
font-size: 1.2rem;
font-weight: 700;
}
.button-container {
display: grid;
grid-gap: 0.5rem;
}
.wrapper {
margin: 2rem 0;
padding: 1.5rem;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-alt);
}
.input {
width: 100%;
height: 100px;
border: 1px solid #ccc;
border-radius: 4px;
padding: 0.375rem 0.75rem;
}
.contact-input {
height: 50px;
border: 1px solid #ccc;
border-radius: 4px;
padding: 0.375rem 0.75rem;
}
.desc {
display: block;
line-height: 20px;
font-size: 12px;
font-weight: 500;
color: var(--vp-c-text-2);
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.25s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@@ -0,0 +1,157 @@
<script setup lang="ts">
import { ref } from 'vue'
import {
TransitionRoot,
TransitionChild,
Dialog,
DialogPanel,
DialogTitle,
DialogDescription
} from '@headlessui/vue'
const isOpen = ref(true)
const feedbackOptions = [
{
label: '💡 Suggestion',
value: 'suggestion'
},
{
label: '❤️ Appreciation',
value: 'appreciate'
},
{ label: '🐞 Bug', value: 'bug' },
{ label: '📂 Other', value: 'other' }
]
function closeModal() {
isOpen.value = false
}
function openModal() {
isOpen.value = true
}
</script>
<template>
<button
type="button"
class="p-[4px 8px] text-xl i-carbon:user-favorite-alt-filled"
@click="openModal"
/>
<TransitionRoot appear :show="isOpen" as="template">
<Dialog as="div" class="relative z-10" @close="closeModal">
<TransitionChild
as="template"
enter="duration-300 ease-out"
enter-from="opacity-0"
enter-to="opacity-100"
leave="duration-200 ease-in"
leave-from="opacity-100"
leave-to="opacity-0"
>
<div class="fixed inset-0 bg-black/25" />
</TransitionChild>
<div class="fixed inset-0 overflow-y-auto">
<div
class="flex min-h-full items-center justify-center p-4 text-center"
>
<TransitionChild
as="template"
enter="duration-300 ease-out"
enter-from="opacity-0 scale-95"
enter-to="opacity-100 scale-100"
leave="duration-200 ease-in"
leave-from="opacity-100 scale-100"
leave-to="opacity-0 scale-95"
>
<DialogPanel
class="w-full max-w-md transform overflow-hidden rounded-2xl bg-bg p-6 text-left align-middle shadow-xl transition-all"
>
<DialogTitle
as="h3"
class="text-lg font-medium leading-6 text-text"
>
Feedback
</DialogTitle>
<div class="mt-2">
<div class="grid gap-[0.5rem]">
<button
v-for="item in feedbackOptions"
:key="item.value"
class="inline-flex justify-center rounded-md border border-transparent bg-bg-alt px-4 py-2 text-sm font-medium text-text hover:border-primary focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2"
>
<span>{{ item.label }}</span>
</button>
</div>
</div>
<div class="mt-2">
<div>
<label class="field-label">Feedback*</label>
<textarea placeholder="meow" rows="5" />
</div>
</div>
<div class="mt-4">
<button
type="button"
class="inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
@click="closeModal"
>
Close
</button>
</div>
</DialogPanel>
</TransitionChild>
</div>
</div>
</Dialog>
</TransitionRoot>
</template>
<style scoped>
textarea,
input {
font-family: var(--vp-font-family-base);
background: var(--vp-c-bg-soft);
font-size: 14px;
border-radius: 4px;
padding: 16px;
width: 100%;
&::placeholder {
color: var(--vp-c-text-2) !important;
opacity: 1;
}
}
.btn {
border: 1px solid var(--vp-c-divider);
background-color: var(--vp-c-bg);
border-radius: 8px;
transition:
border-color 0.25s,
background-color 0.25s;
display: inline-block;
font-size: 14px;
font-weight: 500;
line-height: 1.5;
margin: 0;
padding: 0.375rem 0.75rem;
text-align: center;
vertical-align: middle;
white-space: nowrap;
}
.btn:disabled {
opacity: 0.5;
}
.btn:hover {
border-color: var(--vp-c-brand);
}
</style>

View File

@@ -0,0 +1,47 @@
<script setup lang="ts">
defineProps<{
label: string
id: string
}>()
</script>
<template>
<div class="input-field">
<div v-if="label" class="input-label">
<label :for="id" class="pane-label">
{{ label }}
</label>
<div class="display-value">
<slot name="display" />
</div>
</div>
<slot />
</div>
</template>
<style scoped>
.pane-label {
line-height: 20px;
font-size: 13px;
font-weight: 600;
color: var(--vt-c-text-1);
display: block;
}
.input-field:not(:last-child) {
margin-bottom: 16px;
}
.display-value {
font-size: 13px;
color: var(--vp-c-text-2);
}
.input-label {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
gap: 12px;
}
</style>

View File

@@ -0,0 +1,83 @@
<script setup lang="ts">
import { ref } from 'vue'
import Feedback from './Feedback.vue'
const showModal = ref(false)
</script>
<template>
<button
class="p-[4px 8px] text-xl i-carbon:user-favorite-alt-filled"
@click="showModal = true"
/>
<Teleport to="body">
<Transition name="modal">
<div v-show="showModal" class="modal-mask">
<div class="modal-container">
<Feedback />
<div class="model-footer">
<button class="modal-button" @click="showModal = false">
Close
</button>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<style scoped>
.modal-mask {
position: fixed;
z-index: 200;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.3s ease;
}
.modal-container {
width: 300px;
margin: auto;
padding: 20px 30px;
background-color: var(--vp-c-bg);
border-radius: 2px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
transition: all 0.3s ease;
}
.model-footer {
margin-top: 8px;
text-align: right;
}
.modal-button {
padding: 4px 8px;
border-radius: 4px;
border-color: var(--vp-button-alt-border);
color: var(--vp-button-alt-text);
background-color: var(--vp-button-alt-bg);
}
.modal-button:hover {
border-color: var(--vp-button-alt-hover-border);
color: var(--vp-button-alt-hover-text);
background-color: var(--vp-button-alt-hover-bg);
}
.modal-enter-from,
.modal-leave-to {
opacity: 0;
}
.modal-enter-from .modal-container,
.modal-leave-to .modal-container {
transform: scale(1.1);
}
</style>

View File

@@ -0,0 +1,60 @@
<script setup lang="ts">
import Field from './CardField.vue'
import Modal from './Modal.vue'
import InputField from './InputField.vue'
import ToggleStarred from './ToggleStarred.vue'
</script>
<template>
<div class="card">
<div class="card-header">
<div class="card-title">Emoji Legend</div>
</div>
<Field icon="i-twemoji-star">Recommendations</Field>
<Field icon="i-twemoji-globe-with-meridians">Indexes</Field>
<Field icon="i-twemoji-repeat-button">Storage Links</Field>
<div class="card-header">
<div class="card-title">Options</div>
</div>
<InputField id="feedback" label="Feedback">
<template #display>
<Modal />
</template>
</InputField>
<InputField id="toggle-starred" label="Toggle Starred">
<template #display>
<ToggleStarred />
</template>
</InputField>
</div>
</template>
<style scoped>
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.card-title {
font-weight: 700;
color: var(--vp-c-text-1);
line-height: 32px;
font-size: 16px;
}
.card {
background: var(--vp-c-bg);
padding: 12px 24px 24px;
border-radius: 12px;
position: relative;
z-index: 0;
border: 1px solid transparent;
transition: border-color 0.4s ease-in-out;
}
.card:hover {
border-color: var(--vp-c-brand-1);
}
</style>

View File

@@ -0,0 +1,52 @@
<script setup>
import { ref } from 'vue'
import { Switch } from '@headlessui/vue'
const enabled = ref(false)
</script>
<template>
<Switch v-model="enabled" class="switch" :class="{ enabled }">
<span class="thumb" />
</Switch>
</template>
<style>
.switch {
display: inline-flex;
position: relative;
width: 40px;
height: 22px;
flex-shrink: 0;
border: 1px solid var(--vp-input-border-color);
background-color: var(--vp-input-switch-bg-color);
transition:
border-color 0.25s,
background-color 0.4s ease;
border-radius: 11px;
}
.switch.enabled {
background-color: var(--vp-c-brand);
}
</style>
<style scoped>
.switch:hover {
border-color: var(--vp-input-hover-border-color);
}
.thumb {
display: inline-block;
background-color: #fff;
transition: transform 0.25s;
width: 20px;
height: 20px;
border-radius: 50%;
box-shadow: var(--vp-shadow-1);
}
.switch.enabled .thumb {
transform: translateX(18px);
}
</style>

View File

@@ -0,0 +1,16 @@
<script setup lang="ts">
import Switch from './Switch.vue'
const toggleStarred = () =>
document.documentElement.classList.toggle('starred-only')
</script>
<template>
<Switch @click="toggleStarred()" />
</template>
<style>
.starred-only li:not(.starred) {
display: none;
}
</style>

View File

@@ -0,0 +1,23 @@
import nprogress, { type NProgress } from 'nprogress'
import type { EnhanceAppContext } from 'vitepress'
export function loadProgress(router: EnhanceAppContext['router']): NProgress {
if (typeof window === 'undefined') return
setTimeout(() => {
nprogress.configure({ showSpinner: false })
const cacheBeforeRouteChange = router.onBeforeRouteChange
const cacheAfterRouteChange = router.onAfterRouteChanged
router.onBeforeRouteChange = (to) => {
nprogress.start()
cacheBeforeRouteChange?.(to)
}
router.onAfterRouteChanged = (to) => {
nprogress.done()
cacheAfterRouteChange?.(to)
}
})
return nprogress
}

View File

@@ -0,0 +1,16 @@
import { type Theme } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
import Layout from './Layout.vue'
import Post from './PostLayout.vue'
import { loadProgress } from './composables/nprogress'
import './style.css'
import 'uno.css'
export default {
extends: DefaultTheme,
Layout,
enhanceApp({ router, app }) {
app.component('Post', Post)
loadProgress(router)
}
} satisfies Theme

View File

@@ -0,0 +1,30 @@
import { createContentLoader, type ContentData } from 'vitepress'
import { groupBy } from '../utils'
interface Post {
title: string
url: string
date: string
}
type Dictionary = ReturnType<typeof transformRawPosts>
declare const data: Dictionary
export { data }
function transformRawPosts(rawPosts: ContentData[]): Record<string, Post[]> {
const posts: Post[] = rawPosts
.map(({ url, frontmatter }) => ({
title: frontmatter.title,
url,
date: (frontmatter.date as Date).toISOString().slice(0, 10)
}))
.sort((a, b) => b.date.localeCompare(a.date))
return groupBy(posts, (post) => post.date.slice(0, 4))
}
export default createContentLoader('posts/*.md', {
includeSrc: true,
transform: (raw) => transformRawPosts(raw)
})

View File

@@ -0,0 +1,279 @@
:root {
/* Colors: Brand */
--vp-c-brand-1: #7bc5e4;
--vp-c-brand-2: #c4e2f2;
--vp-c-brand-3: #4882a7;
--vp-c-brand-soft: #a4d5ec;
/* Colors: Button */
--vp-button-brand-bg: var(--vp-c-brand-1);
--vp-button-brand-border: var(--vp-c-brand-soft);
--vp-button-brand-text: rgba(42, 40, 47);
--vp-button-brand-hover-border: var(--vp-c-brand-soft);
--vp-button-brand-hover-text: rgba(42, 40, 47);
--vp-button-brand-hover-bg: var(--vp-c-brand-soft);
--vp-button-brand-active-border: var(--vp-c-brand-soft);
--vp-button-brand-active-text: rgba(42, 40, 47);
--vp-button-brand-active-bg: var(--vp-button-brand-bg);
--vp-button-alt-bg: #484848;
--vp-button-alt-text: #f0eeee;
--vp-button-alt-hover-bg: #484848;
--vp-button-alt-hover-text: #f0eeee;
--vp-c-bg-elv: rgba(255, 255, 255, 0.7);
--vp-c-bg-mark: rgb(232, 232, 232);
/* Colors: Custom Block */
--vp-custom-block-info-bg: rgba(171, 210, 244, 0.05);
--vp-custom-block-info-border: #60c4fa;
--vp-custom-block-info-text: rgb(39, 115, 145);
--vp-custom-block-info-text-deep: rgb(18, 121, 162);
--vp-custom-block-tip-bg: rgba(137, 202, 176, 0.05);
--vp-custom-block-tip-border: rgba(34, 197, 94, 1);
--vp-custom-block-tip-text: rgb(10, 128, 90);
--vp-custom-block-tip-text-deep: rgb(11, 133, 94);
--vp-custom-block-warning-bg: rgba(250, 204, 21, 0.05);
--vp-custom-block-warning-border: rgba(245, 158, 11, 1);
--vp-custom-block-warning-text: rgb(166, 114, 35);
--vp-custom-block-warning-text-deep: rgb(199, 109, 6);
--vp-custom-block-danger-bg: rgba(220, 38, 38, 0.05);
--vp-custom-block-danger-border: rgba(248, 113, 113, 1);
--vp-custom-block-danger-text: rgb(196, 46, 46);
--vp-custom-block-danger-text-deep: rgba(220, 38, 38, 1);
/* Scrollbar */
scroll-behavior: smooth;
scrollbar-width: 4px;
}
.dark {
/* Colors: Background */
--vp-c-bg: rgb(26, 26, 26);
--vp-c-bg-alt: rgb(23, 23, 23);
--vp-c-bg-elv: rgba(23, 23, 23, 0.8);
/* Colors: Custom Block */
--vp-custom-block-info-bg: rgba(84, 110, 155, 0.1);
--vp-custom-block-info-border: #3686b1;
--vp-custom-block-info-text: #52b0e3;
--vp-custom-block-info-text-deep: #00b7ff;
--vp-custom-block-tip-bg: rgba(51, 130, 118, 0.1);
--vp-custom-block-tip-border: rgba(4, 120, 87, 1);
--vp-custom-block-tip-text: rgb(25, 190, 129);
--vp-custom-block-tip-text-deep: rgba(52, 211, 153, 1);
--vp-custom-block-warning-bg: rgba(253, 224, 71, 0.1);
--vp-custom-block-warning-border: rgba(202, 138, 4, 1);
--vp-custom-block-warning-text: rgba(234, 179, 8, 1);
--vp-custom-block-warning-text-deep: rgba(250, 204, 21, 1);
--vp-custom-block-danger-bg: rgba(239, 68, 68, 0.1);
--vp-custom-block-danger-border: rgba(127, 29, 29, 1);
--vp-custom-block-danger-text: rgba(248, 113, 113, 1);
--vp-custom-block-danger-text-deep: rgba(248, 113, 113, 1);
}
.vp-doc a {
color: var(--vp-c-brand-1);
text-decoration: underline;
text-underline-offset: 4px;
text-decoration-style: solid;
text-decoration-color: transparent;
-webkit-text-decoration-color: transparent;
transition: text-decoration-color 0.25s;
}
.vp-doc a:hover {
color: var(--vp-c-brand-1);
text-decoration-color: var(--vp-c-brand-1);
-webkit-text-decoration-color: var(--vp-c-brand-1);
}
.vp-doc .custom-block a {
text-decoration: underline;
text-underline-offset: 4px;
text-decoration-style: solid;
}
::selection {
background-color: var(--vp-button-brand-bg);
}
.VPFooter a {
text-decoration-line: underline;
text-decoration-style: dashed;
text-underline-offset: 5px;
transition: 0.3s;
}
.VPFooter a:hover {
color: var(--vp-c-text-1);
text-decoration-line: underline;
text-decoration-style: dashed;
text-underline-offset: 5px;
}
/* Custom scrollbar */
.VPSidebar::-webkit-scrollbar {
block-size: 4px;
border-end-end-radius: 14px;
border-start-end-radius: 14px;
inline-size: 4px;
}
/**
* Component: Home
*/
:root {
--vp-home-hero-name-color: transparent;
--vp-home-hero-name-background: -webkit-linear-gradient(
120deg,
#c4b5fd 30%,
#7bc5e4
);
--vp-home-hero-image-background-image: linear-gradient(
-45deg,
#c4b5fd 50%,
#47caff 50%
);
--vp-home-hero-image-filter: blur(44px);
}
@media (min-width: 640px) {
:root {
--vp-home-hero-image-filter: blur(56px);
}
}
@media (min-width: 960px) {
:root {
--vp-home-hero-image-filter: blur(68px);
}
}
.base64 {
min-width: 100%;
width: 0px;
white-space: pre-wrap;
}
/* Make clicks pass-through */
#nprogress {
pointer-events: none;
}
#nprogress .bar {
background: var(--vp-c-brand-1);
position: fixed;
z-index: 1031;
top: 0;
left: 0;
width: 100%;
height: 2px;
}
/* Fancy blur effect */
#nprogress .peg {
display: block;
position: absolute;
right: 0px;
width: 100px;
height: 100%;
box-shadow:
0 0 10px var(--vp-c-brand-1),
0 0 5px var(--vp-c-brand-1);
opacity: 1;
-webkit-transform: rotate(3deg) translate(0px, -4px);
-ms-transform: rotate(3deg) translate(0px, -4px);
transform: rotate(3deg) translate(0px, -4px);
}
#nprogress .spinner {
display: block;
position: fixed;
z-index: 1031;
top: 15px;
right: 15px;
}
#nprogress .spinner-icon {
width: 18px;
height: 18px;
box-sizing: border-box;
border: solid 2px transparent;
border-top-color: var(--vp-c-brand);
border-left-color: var(--vp-c-brand);
border-radius: 50%;
-webkit-animation: nprogress-spinner 400ms linear infinite;
animation: nprogress-spinner 400ms linear infinite;
}
.nprogress-custom-parent {
overflow: hidden;
position: relative;
}
.nprogress-custom-parent #nprogress .spinner,
.nprogress-custom-parent #nprogress .bar {
position: absolute;
}
@-webkit-keyframes nprogress-spinner {
0% {
-webkit-transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
}
}
@keyframes nprogress-spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
#VPContent strong > a {
font-weight: bold;
}
.info.custom-block {
--icon: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWluZm8iPjxjaXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjEwIi8+PHBhdGggZD0iTTEyIDE2di00Ii8+PHBhdGggZD0iTTEyIDhoLjAxIi8+PC9zdmc+');
}
.tip.custom-block {
--icon: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWxpZ2h0YnVsYiI+PHBhdGggZD0iTTE1IDE0Yy4yLTEgLjctMS43IDEuNS0yLjUgMS0uOSAxLjUtMi4yIDEuNS0zLjVBNiA2IDAgMCAwIDYgOGMwIDEgLjIgMi4yIDEuNSAzLjUuNy43IDEuMyAxLjUgMS41IDIuNSIvPjxwYXRoIGQ9Ik05IDE4aDYiLz48cGF0aCBkPSJNMTAgMjJoNCIvPjwvc3ZnPg==');
}
.warning.custom-block {
--icon: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLWFsZXJ0LXRyaWFuZ2xlIj48cGF0aCBkPSJtMjEuNzMgMTgtOC0xNGEyIDIgMCAwIDAtMy40OCAwbC04IDE0QTIgMiAwIDAgMCA0IDIxaDE2YTIgMiAwIDAgMCAxLjczLTNaIi8+PHBhdGggZD0iTTEyIDl2NCIvPjxwYXRoIGQ9Ik0xMiAxN2guMDEiLz48L3N2Zz4=');
}
.danger.custom-block {
--icon: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIGNsYXNzPSJsdWNpZGUgbHVjaWRlLXNrdWxsIj48Y2lyY2xlIGN4PSI5IiBjeT0iMTIiIHI9IjEiLz48Y2lyY2xlIGN4PSIxNSIgY3k9IjEyIiByPSIxIi8+PHBhdGggZD0iTTggMjB2Mmg4di0yIi8+PHBhdGggZD0ibTEyLjUgMTctLjUtMS0uNSAxaDF6Ii8+PHBhdGggZD0iTTE2IDIwYTIgMiAwIDAgMCAxLjU2LTMuMjUgOCA4IDAgMSAwLTExLjEyIDBBMiAyIDAgMCAwIDggMjAiLz48L3N2Zz4=');
}
.custom-block-title {
display: inline-flex;
align-items: center;
gap: 8px;
}
.custom-block-title::before {
content: '';
width: 16px;
height: 16px;
-webkit-mask: var(--icon) no-repeat;
mask: var(--icon) no-repeat;
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
background-color: currentColor;
color: inherit;
}

12
docs/.vitepress/utils.ts Normal file
View File

@@ -0,0 +1,12 @@
export function groupBy<T, K extends keyof any>(
arr: T[],
key: (i: T) => K
): Record<K, T[]> {
return arr.reduce(
(groups, item) => {
;(groups[key(item)] ||= []).push(item)
return groups
},
{} as Record<K, T[]>
)
}

5
docs/.vitepress/vue-shim.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
/* eslint-disable ts/consistent-type-imports */
declare module '*.vue' {
const component: import('vue').Component
export default component
}

View File

@@ -6,7 +6,8 @@
"dev": "vitepress dev",
"build": "vitepress build",
"preview": "vitepress preview",
"lint": "eslint --cache --fix ."
"lint": "eslint --cache --fix .",
"og:dev": "x-satori -t ./.vitepress/hooks/Template.vue -c ./.vitepress/hooks/satoriConfig.ts --dev"
},
"dependencies": {
"@headlessui/vue": "^1.7.17",