Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 9 additions & 13 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
# Default repository owners.
* @m1ngsama @Orangedog433
# Repository owner — tooling, CI and governance files.
* @m1ngsama

# Documentation governance and review flow.
/CONTRIBUTING.md @m1ngsama @Orangedog433
/CONTEXT.md @m1ngsama @Orangedog433
/docs/adr/ @m1ngsama @Orangedog433
/.github/ @m1ngsama @Orangedog433

# Active documentation areas.
/tutorial/ @m1ngsama @Yuna-Celisse
/process/ @m1ngsama @Yuna-Celisse
/repair/ @m1ngsama @Orangedog433
/archived/ @m1ngsama @Yuna-Celisse
# Documentation content — reviewed by the president and the documents maintainers.
/about/ @Sheepkinn @Yuna-Celisse @wen-templari
/concepts/ @Sheepkinn @Yuna-Celisse @wen-templari
/tutorial/ @Sheepkinn @Yuna-Celisse @wen-templari
/process/ @Sheepkinn @Yuna-Celisse @wen-templari
/repair/ @Sheepkinn @Yuna-Celisse @wen-templari
/archived/ @Sheepkinn @Yuna-Celisse @wen-templari
21 changes: 10 additions & 11 deletions .vitepress/config.mts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { withMermaid } from 'vitepress-plugin-mermaid'
import { sidebar as sidebarAbout } from '../about/sidebar'
import { sidebar as sidebarArchived } from '../archived/sidebar'
import { sidebar as sidebarProcess } from '../process/sidebar'
import { sidebar as sidebarRepair } from '../repair/sidebar'
import { sidebar as sidebarTutorial } from '../tutorial/sidebar'
import { sidebar as sidebarGuide } from '../tutorial/sidebar'

// https://vitepress.dev/reference/site-config
export default withMermaid({
Expand All @@ -15,18 +14,19 @@ export default withMermaid({
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
nav: [
{ text: '教程', link: '/tutorial/' },
{ text: '流程', link: '/process/' },
{ text: '维修', link: '/repair/guide' },
{ text: '存档', link: '/archived' },
{ text: '关于', link: '/about/' },
{ text: '指南', link: '/tutorial/' },
{ text: '维修', link: '/repair/' },
],
search: {
provider: 'local',
},
sidebar: {
'/tutorial/': sidebarTutorial,
'/process/': sidebarProcess,
'/repair/': sidebarRepair,
'/about/': sidebarAbout,
// Guide = tutorial + process; one shared sidebar mounted on both paths.
'/tutorial/': sidebarGuide,
'/process/': sidebarGuide,
// repair and concepts use a hub + inline-link + search model; no full sidebar.
'/archived': sidebarArchived,
},
editLink: {
Expand All @@ -46,7 +46,6 @@ export default withMermaid({
srcExclude: [
'README.md',
'CONTRIBUTING.md',
'CONTEXT.md',
'docs/**',
],
lastUpdated: true,
Expand Down
166 changes: 166 additions & 0 deletions .vitepress/theme/ConceptPreview.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<script setup lang="ts">
import type { PagePreview } from './concepts.data'
import { onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { data as pages } from './concepts.data'

// Build-time map: page URL path -> its preview. Every internal link previews.
const map = new Map(pages.map(p => [p.path, p]))

const card = ref<HTMLElement | null>(null)
const state = reactive({
visible: false,
title: '',
summary: '',
x: 0,
y: 0,
placement: 'below' as 'below' | 'above',
})

let showTimer: ReturnType<typeof setTimeout> | undefined
let hideTimer: ReturnType<typeof setTimeout> | undefined
let current: HTMLAnchorElement | null = null

function pathFromHref(href: string): string | null {
try {
const url = new URL(href, location.origin)
if (url.origin !== location.origin)
return null
let p = url.pathname.replace(/\.html$/, '')
if (p.endsWith('/index'))
p = p.slice(0, -'index'.length)
return p
}
catch {
return null
}
}

function anchorFrom(target: EventTarget | null): HTMLAnchorElement | null {
let el = target as HTMLElement | null
while (el && el !== document.body) {
if (
el.tagName === 'A'
&& el.closest('.vp-doc')
&& !el.classList.contains('header-anchor')
) {
return el as HTMLAnchorElement
}
el = el.parentElement
}
return null
}

function place(a: HTMLAnchorElement, preview: PagePreview) {
const r = a.getBoundingClientRect()
state.title = preview.title
state.summary = preview.summary
state.placement = window.innerHeight - r.bottom < 200 ? 'above' : 'below'
state.x = Math.min(Math.max(12, r.left), window.innerWidth - 332)
state.y = state.placement === 'below' ? r.bottom + 10 : r.top - 10
state.visible = true
}

function scheduleShow(a: HTMLAnchorElement) {
const p = pathFromHref(a.href)
const preview = p ? map.get(p) : undefined
if (!preview)
return
clearTimeout(hideTimer)
clearTimeout(showTimer)
current = a
showTimer = setTimeout(() => place(a, preview), 220)
}

function scheduleHide() {
clearTimeout(showTimer)
clearTimeout(hideTimer)
hideTimer = setTimeout(() => {
state.visible = false
current = null
}, 160)
}

function onOver(e: MouseEvent) {
const a = anchorFrom(e.target)
if (a && a !== current)
scheduleShow(a)
}

function onOut(e: MouseEvent) {
const a = anchorFrom(e.target)
if (!a || a !== current)
return
const to = e.relatedTarget as Node | null
if (card.value && to && card.value.contains(to))
return
scheduleHide()
}

function onFocusIn(e: FocusEvent) {
const a = anchorFrom(e.target)
if (a)
scheduleShow(a)
}

function onFocusOut() {
scheduleHide()
}

function onKeydown(e: KeyboardEvent) {
if (e.key === 'Escape')
state.visible = false
}

function keepOpen() {
clearTimeout(hideTimer)
}

onMounted(() => {
// Progressive enhancement: only devices that actually hover get the card.
// Touch / no-hover users simply follow the link to the full concept page.
if (window.matchMedia('(hover: hover)').matches) {
document.addEventListener('mouseover', onOver)
document.addEventListener('mouseout', onOut)
}
document.addEventListener('focusin', onFocusIn)
document.addEventListener('focusout', onFocusOut)
document.addEventListener('keydown', onKeydown)
})

onBeforeUnmount(() => {
document.removeEventListener('mouseover', onOver)
document.removeEventListener('mouseout', onOut)
document.removeEventListener('focusin', onFocusIn)
document.removeEventListener('focusout', onFocusOut)
document.removeEventListener('keydown', onKeydown)
})
</script>

<template>
<ClientOnly>
<Teleport to="body">
<Transition name="nb-cp">
<div
v-if="state.visible"
ref="card"
class="nb-concept-card"
:class="`is-${state.placement}`"
:style="{ left: `${state.x}px`, top: `${state.y}px` }"
role="tooltip"
@mouseenter="keepOpen"
@mouseleave="scheduleHide"
>
<div class="nb-cp-kicker">
词条
</div>
<div class="nb-cp-title">
{{ state.title }}
</div>
<p v-if="state.summary" class="nb-cp-summary">
{{ state.summary }}
</p>
</div>
</Transition>
</Teleport>
</ClientOnly>
</template>
30 changes: 30 additions & 0 deletions .vitepress/theme/HubBackLink.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script setup lang="ts">
import { useRoute } from 'vitepress'
import { computed } from 'vue'

// On the no-sidebar hub sections, a deep page has no sibling nav — offer one
// light "back to hub" anchor. The hub pages themselves show nothing.
const hubs: Array<{ prefix: string, text: string, link: string }> = [
{ prefix: '/repair/', text: '维修', link: '/repair/' },
{ prefix: '/concepts/', text: '概念库', link: '/concepts/' },
]

const route = useRoute()

const hub = computed(() => {
const path = route.path
for (const h of hubs) {
if (!path.startsWith(h.prefix))
continue
const rest = path.slice(h.prefix.length).replace(/\.html$/, '')
if (rest === '' || rest === 'index')
return null
return h
}
return null
})
</script>

<template>
<a v-if="hub" class="nb-hub-back" :href="hub.link">↑ 回{{ hub.text }}</a>
</template>
18 changes: 18 additions & 0 deletions .vitepress/theme/Layout.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script setup lang="ts">
import DefaultTheme from 'vitepress/theme'
import ConceptPreview from './ConceptPreview.vue'
import HubBackLink from './HubBackLink.vue'

const { Layout } = DefaultTheme
</script>

<template>
<Layout>
<template #doc-before>
<HubBackLink />
</template>
<template #layout-bottom>
<ConceptPreview />
</template>
</Layout>
</template>
Loading
Loading