feat(docs): authors region, published at, more tweaks on ui, @assets() fix

This commit is contained in:
Neko Ayaka
2026-05-02 22:47:35 +08:00
parent b4a35e4ac0
commit 32087c05de
8 changed files with 198 additions and 12 deletions
+71
View File
@@ -1,8 +1,13 @@
<script setup lang="ts">
import type { DefaultTheme } from 'vitepress/theme'
import type { Author } from '../functions/authors.data'
import { intlFormat } from 'date-fns'
import { AvatarFallback, AvatarImage, AvatarRoot } from 'reka-ui'
import { Content, useData, useRoute } from 'vitepress'
import { computed, toRefs } from 'vue'
import { useI18n } from 'vue-i18n'
// import DocCarbonAds from '../components/DocCarbonAds.vue'
import DocCommunity from '../components/DocCommunity.vue'
@@ -14,6 +19,9 @@ import DocTopbar from '../components/DocTopbar.vue'
import { isBetweenHalloweenAndHalfOfNovember } from '../composables/date'
import { flatten } from '../utils/flatten'
import * as authorsData from '../functions/authors.data'
const { t } = useI18n()
const { theme, frontmatter } = useData()
const { path } = toRefs(useRoute())
@@ -43,6 +51,25 @@ const isCommunityEnabled = computed(() => {
})
const isCharactersPage = computed(() => path.value.includes('characters'))
const publishedAt = computed(() => {
if (frontmatter.value.publishedAtOverride) {
return frontmatter.value.publishedAtOverride
}
if (frontmatter.value.publishedAt) {
return intlFormat(new Date(frontmatter.value.publishedAt), { dateStyle: 'long' })
}
if (frontmatter.value.date) {
return intlFormat(new Date(frontmatter.value.data), { dateStyle: 'long' })
}
return undefined
})
const authors = computed(() => {
const data = (authorsData as unknown as { data: Array<{ url: string, authors: Author[] }> }).data
return data.find(item => item.url === path.value)?.authors || []
})
</script>
<template>
@@ -89,6 +116,50 @@ const isCharactersPage = computed(() => path.value.includes('characters'))
{{ frontmatter.title || '' }}
</h1>
<div v-if="publishedAt || authors && authors.length" class="mb-10 mt-5 flex flex-col gap-3 sm:gap-5">
<div v-if="publishedAt" class="text-neutral-400 dark:text-neutral-500">
<span>
{{ t('docs.theme.doc.published-at', { date: publishedAt }) }}
</span>
</div>
<div class="flex flex-row gap-2 sm:gap-4">
<!-- Authors -->
<div v-for="(author, index) of authors" :key="index" class="flex flex-row items-center gap-2.5">
<AvatarRoot class="size-10 inline-flex select-none items-center justify-center overflow-hidden rounded-full bg-neutral-100 align-middle dark:bg-neutral-800">
<AvatarImage
class="h-full w-full rounded-[inherit] object-cover"
:src="author.avatar || author.avatarFallback"
:alt="`${author.displayName}'s avatar`"
/>
<AvatarFallback
class="h-full w-full flex items-center justify-center bg-white text-sm text-primary font-medium leading-1 dark:bg-neutral-800 dark:text-neutral-300"
:delay-ms="600"
as-child
>
{{
[
author.displayName.charAt(0).toUpperCase(),
author.displayName.charAt(1).toUpperCase(),
].join('')
}}
</AvatarFallback>
</AvatarRoot>
<div class="flex flex-col">
<div>
<span>{{ author.displayName }}</span>
</div>
<div v-if="author.githubUsername">
<a :href="`https://github.com/${author.githubUsername}`" target="_blank" rel="noopener noreferrer" class="text-sm text-primary hover:underline">
<span>{{ author.githubUsername }}</span>
</a>
</div>
</div>
</div>
</div>
</div>
<Content />
</article>
+90
View File
@@ -0,0 +1,90 @@
import { webcrypto } from 'node:crypto'
import { createContentLoader } from 'vitepress'
export interface Author {
role: string
kind: 'person' | 'team'
displayName: string
githubUsername?: string
githubEmail?: string
avatar?: string
avatarFallback: string
}
interface MarkdownAuthor {
name?: string
role?: string
kind?: 'person' | 'team'
avatar?: string
githubUsername?: string
githubEmail?: string
}
/**
* Hashes a string using SHA-256
*
* Official example by MDN: https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
* @param {string} message - The message to be hashed
* @returns {Promise<string>} - The SHA-256 hash of the message
*/
async function digestStringAsSHA256(message: string) {
const msgUint8 = new TextEncoder().encode(message) // encode as (utf-8) Uint8Array
const hashBuffer = await webcrypto.subtle.digest('SHA-256', msgUint8) // hash the message
const hashArray = Array.from(new Uint8Array(hashBuffer)) // convert buffer to byte array
const hashHex = hashArray
.map(b => b.toString(16).padStart(2, '0'))
.join('') // convert bytes to hex string
return hashHex
}
async function newAvatarForAuthor(mappedAuthor?: { overrideAvatar?: string, githubUsername?: string, displayName?: string } | null, email?: string | null): Promise<string> {
if (mappedAuthor) {
if (mappedAuthor.overrideAvatar)
return mappedAuthor.overrideAvatar
if (mappedAuthor.githubUsername)
return `https://github.com/${mappedAuthor.githubUsername}.png`
}
return `https://gravatar.com/avatar/${await digestStringAsSHA256(email || mappedAuthor?.githubUsername || mappedAuthor?.displayName || 'unknown')}?d=retro`
}
export default createContentLoader('**/*.md', {
async transform(raw): Promise<Array<{ url: string, authors: Author[] }>> {
return (await Promise.all(
raw
.map(async ({ url, frontmatter }) => {
const authors: MarkdownAuthor[] = frontmatter.authors
if (!authors || !Array.isArray(authors)) {
return
}
const authorsTransformed = await Promise.all(authors.map(async (author): Promise<Author> => {
const displayName = author.name || author.githubUsername || author.githubEmail || 'Unknown Author'
return {
role: author.role || 'Contributor',
kind: author.kind || 'person',
displayName,
githubUsername: author.githubUsername,
githubEmail: author.githubEmail,
avatar: author.avatar || await newAvatarForAuthor({ githubUsername: author.githubUsername, displayName }, author.githubEmail),
avatarFallback: `https://gravatar.com/avatar/${await digestStringAsSHA256(displayName)}?d=retro`,
}
}))
return {
url,
authors: authorsTransformed,
}
}),
)).filter(item => item != null)
},
})
@@ -10,7 +10,7 @@ import matter from 'gray-matter'
import { glob } from 'tinyglobby'
function fromAtAssets(url: string): string {
const reg = /^@assets\(('\S+')|("\S+")|(\S+)\)$/
const reg = /^@assets\((?:'(\S+)'|"(\S+)"|(\S+))\)$/
if (reg.test(url)) {
const res = url
.trim()
@@ -35,7 +35,7 @@ interface VitePressConfig extends ResolvedConfig {
function recursivelyFindAtAssets(propertyMaybeObjectOrScalar: unknown, fn: (value: string) => string | undefined) {
if (typeof propertyMaybeObjectOrScalar === 'string') {
// eslint-disable-next-line regexp/no-unused-capturing-group
if (/^@assets\(('\S+')|("\S+")|(\S+)\)$/.test(propertyMaybeObjectOrScalar)) {
if (/^@assets\((?:'(\S+)'|"(\S+)"|(\S+))\)$/.test(propertyMaybeObjectOrScalar)) {
// If the string matches the @assets(...) pattern, we replace it with the result of the function
const match = fromAtAssets(propertyMaybeObjectOrScalar)
const modified = fn(match)
+4 -4
View File
@@ -103,7 +103,7 @@
}
&> :not(p:nth-child(1)) {
@apply text-slate-950 dark:text-slate-50 leading-snug;
@apply text-slate-950 dark:text-slate-50 leading-normal;
}
}
@@ -123,7 +123,7 @@
}
&> :not(p:nth-child(1)) {
@apply text-violet-950 dark:text-violet-50 leading-snug;
@apply text-violet-950 dark:text-violet-50 leading-normal;
}
}
@@ -143,7 +143,7 @@
}
&> :not(p:nth-child(1)) {
@apply text-yellow-950 dark:text-yellow-50 leading-snug;
@apply text-yellow-950 dark:text-yellow-50 leading-normal;
}
}
@@ -163,7 +163,7 @@
}
&> :not(p:nth-child(1)) {
@apply text-red-950 dark:text-red-50 leading-snug;
@apply text-red-950 dark:text-red-50 leading-normal;
}
}
@@ -1,6 +1,15 @@
# Project AIRI Manual
Writing time: (UTC+8) April 2, 2026 evening
---
title: Project AIRI Manual
authors:
- name: MuGewRayce
role: Lead writing team
kind: person
- name: JhIcefair
role: Contributing editor (primary)
kind: person
publishedAt: 2026-04-02
publishedAtOverride: April 2, 2026 evening (UTC+8)
---
Corresponding version: AIRI-0.9.0-beta.4-windows-x64
@@ -1,6 +1,18 @@
# Project AIRI 说明书
编写时间:(北京时间)2026 年 4 月 2 日 - 晚上
---
title: Project AIRI 说明书
authors:
- name: 沐玖芸萱
aliases:
- 沐玖芸萱
- MuGewRayce
role: Lead writing team
kind: person
- name: JhIcefair
role: Contributing editor (primary)
kind: person
publishedAt: 2026-04-02
publishedAtOverride: 2026 年 4 月 2 日 - 晚上(北京时间)
---
对应版本:AIRI-0.9.0-beta.4-windows-x64
+3
View File
@@ -7,6 +7,9 @@ export default defineConfig({
presetAttributify(),
presetTypography({
cssExtend: {
'h1': {
'margin-bottom': '1rem',
},
'a': {
'color': '#223f5dff',
'text-decoration': 'underline',
@@ -15,6 +15,7 @@ doc:
title: Next page
previous-page:
title: Previous page
published-at: Published at {date}
home:
subtitle: >-
Re-creating Neuro-sama, a container of souls of AI waifu / virtual