valaxy-theme-press 0.28.11 → 1.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -6
- package/components/PressCategory.vue +84 -34
- package/components/PressNotFoundVisual.vue +177 -0
- package/components/PressSidebar.vue +104 -22
- package/components/PressSidebarItem.vue +52 -35
- package/composables/prev-next.ts +11 -86
- package/composables/sidebar.ts +1 -1
- package/layouts/404.vue +269 -23
- package/package.json +3 -3
- package/types/index.d.ts +3 -4
- package/utils/sidebar.ts +89 -0
package/README.md
CHANGED
|
@@ -1,15 +1,35 @@
|
|
|
1
1
|
# valaxy-theme-press
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Documentation theme for [Valaxy](https://valaxy.site).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
It is inspired by [VitePress](https://vitepress.dev), and it powers the Valaxy documentation site.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
它增加了 VitePress 此前默认所没有的一些功能,KaTeX、文章分类、单页 CSS i18n + Vue i18n、Vue Router、Pinia 状态管理,组件自动注册等。
|
|
7
|
+
## Install
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add valaxy-theme-press
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import type { PressTheme } from 'valaxy-theme-press'
|
|
15
|
+
import { defineValaxyConfig } from 'valaxy'
|
|
16
|
+
|
|
17
|
+
export default defineValaxyConfig<PressTheme.Config>({
|
|
18
|
+
theme: 'press',
|
|
19
|
+
themeConfig: {
|
|
20
|
+
logo: '/favicon.svg',
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Links
|
|
26
|
+
|
|
27
|
+
- [Documentation](https://valaxy.site/themes/press)
|
|
28
|
+
- [中文文档](https://valaxy.site/zh/themes/press)
|
|
29
|
+
- [Preview](https://press.valaxy.site)
|
|
30
|
+
- [Source](https://github.com/YunYouJun/valaxy/tree/main/packages/valaxy-theme-press)
|
|
11
31
|
|
|
12
32
|
## Reference
|
|
13
33
|
|
|
14
|
-
- [vitepress](https://vitepress.
|
|
34
|
+
- [vitepress](https://vitepress.dev/)
|
|
15
35
|
- [nextjs](https://nextjs.org/)
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import type { Category } from 'valaxy'
|
|
2
|
+
import type { Category, CategoryList } from 'valaxy'
|
|
3
3
|
import { isCategoryList, useValaxyI18n } from 'valaxy'
|
|
4
|
-
import { ref } from 'vue'
|
|
4
|
+
import { computed, ref } from 'vue'
|
|
5
5
|
import { useI18n } from 'vue-i18n'
|
|
6
6
|
|
|
7
|
+
type CategoryChild = CategoryList['children'] extends Map<string, infer T> ? T : never
|
|
8
|
+
|
|
7
9
|
const props = withDefaults(defineProps<{
|
|
8
10
|
// to eliminate the warning
|
|
9
11
|
category: Category
|
|
@@ -16,54 +18,102 @@ const props = withDefaults(defineProps<{
|
|
|
16
18
|
collapsable?: boolean
|
|
17
19
|
}>(), {
|
|
18
20
|
collapsable: true,
|
|
21
|
+
level: 0,
|
|
19
22
|
})
|
|
20
23
|
|
|
21
|
-
const
|
|
24
|
+
const collapsed = ref(props.collapsable)
|
|
25
|
+
const hasChildren = computed(() => props.category.children.size > 0)
|
|
22
26
|
const { t } = useI18n()
|
|
23
27
|
const { $tO } = useValaxyI18n()
|
|
28
|
+
|
|
29
|
+
function toggle() {
|
|
30
|
+
collapsed.value = !collapsed.value
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getCategoryItemKey(categoryItem: CategoryChild, index: number): string | number {
|
|
34
|
+
return isCategoryList(categoryItem)
|
|
35
|
+
? categoryItem.name
|
|
36
|
+
: categoryItem.path || index
|
|
37
|
+
}
|
|
24
38
|
</script>
|
|
25
39
|
|
|
26
40
|
<template>
|
|
27
41
|
<li
|
|
28
42
|
v-if="category.total"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class="press-sidebar-item category-list-item inline-flex items-center justify-between"
|
|
32
|
-
text-14px
|
|
33
|
-
tabindex="0"
|
|
43
|
+
class="press-category-item"
|
|
44
|
+
:class="[`level-${props.level}`, { collapsed }]"
|
|
34
45
|
>
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
tabindex="0" role="button" aria-label="toggle section"
|
|
41
|
-
class="caret folder-action inline-flex cursor-pointer"
|
|
42
|
-
text-base
|
|
43
|
-
@click="collapsable = !collapsable"
|
|
46
|
+
<div
|
|
47
|
+
p="t-2"
|
|
48
|
+
w="full" border="t t-$pr-c-divider-light"
|
|
49
|
+
class="press-sidebar-item category-list-item inline-flex items-center justify-between"
|
|
50
|
+
text-14px
|
|
44
51
|
>
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
<span class="category-name" font="bold" m="l-1" @click="displayCategory ? displayCategory(category.name) : null">
|
|
53
|
+
{{ category.name === 'Uncategorized' ? t('category.uncategorized') : t(`category.${category.name}`) }}
|
|
54
|
+
<!-- <sup font="normal">[{{ category.total }}]</sup> -->
|
|
55
|
+
</span>
|
|
56
|
+
<button
|
|
57
|
+
v-if="hasChildren"
|
|
58
|
+
type="button"
|
|
59
|
+
aria-label="toggle section"
|
|
60
|
+
:aria-expanded="!collapsed"
|
|
61
|
+
class="caret"
|
|
62
|
+
@click.stop="toggle"
|
|
63
|
+
>
|
|
64
|
+
<span class="caret-icon" :class="{ open: !collapsed }" i-ri-arrow-right-s-line aria-hidden="true" />
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
49
67
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
<ul v-if="hasChildren && !collapsed" class="items press-category-list press-sidebar-category-list">
|
|
69
|
+
<template v-for="(categoryItem, i) in category.children.values()" :key="getCategoryItemKey(categoryItem, i)">
|
|
70
|
+
<li v-if="!isCategoryList(categoryItem)" class="post-list-item">
|
|
71
|
+
<RouterLink
|
|
72
|
+
v-if="categoryItem.title" :to="categoryItem.path || ''"
|
|
73
|
+
class="inline-flex items-center"
|
|
74
|
+
>
|
|
75
|
+
<span class="text ml-1" text="sm">{{ $tO(categoryItem.title) }}</span>
|
|
76
|
+
</RouterLink>
|
|
77
|
+
</li>
|
|
60
78
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
79
|
+
<PressCategory
|
|
80
|
+
v-else
|
|
81
|
+
:category="categoryItem"
|
|
82
|
+
:display-category="displayCategory"
|
|
83
|
+
:collapsable="collapsed"
|
|
84
|
+
:level="props.level + 1"
|
|
85
|
+
/>
|
|
86
|
+
</template>
|
|
87
|
+
</ul>
|
|
88
|
+
</li>
|
|
64
89
|
</template>
|
|
65
90
|
|
|
66
91
|
<style lang="scss">
|
|
92
|
+
.press-category-item,
|
|
93
|
+
.press-category-list,
|
|
94
|
+
.post-list-item {
|
|
95
|
+
list-style: none;
|
|
96
|
+
margin: 0;
|
|
97
|
+
padding: 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.press-category-item.level-0 {
|
|
101
|
+
padding-bottom: 24px;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.press-category-item.collapsed.level-0 {
|
|
105
|
+
padding-bottom: 10px;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.press-category-item.level-1 > .press-category-list,
|
|
109
|
+
.press-category-item.level-2 > .press-category-list,
|
|
110
|
+
.press-category-item.level-3 > .press-category-list,
|
|
111
|
+
.press-category-item.level-4 > .press-category-list,
|
|
112
|
+
.press-category-item.level-5 > .press-category-list {
|
|
113
|
+
border-left: 1px solid var(--vp-c-divider);
|
|
114
|
+
padding-left: 16px;
|
|
115
|
+
}
|
|
116
|
+
|
|
67
117
|
.post-list-item {
|
|
68
118
|
// align with vitepress
|
|
69
119
|
a {
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
withDefaults(defineProps<{
|
|
3
|
+
code?: string
|
|
4
|
+
label: string
|
|
5
|
+
}>(), {
|
|
6
|
+
code: '404',
|
|
7
|
+
})
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<template>
|
|
11
|
+
<div class="not-found-visual" aria-hidden="true">
|
|
12
|
+
<div class="not-found-orbit">
|
|
13
|
+
<span class="not-found-satellite not-found-satellite-a" />
|
|
14
|
+
<span class="not-found-satellite not-found-satellite-b" />
|
|
15
|
+
<span class="not-found-satellite not-found-satellite-c" />
|
|
16
|
+
<span class="not-found-signal">{{ label }}</span>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="not-found-code" :title="code" font="mono">
|
|
20
|
+
{{ code }}
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<style lang="scss" scoped>
|
|
26
|
+
.not-found-visual {
|
|
27
|
+
position: relative;
|
|
28
|
+
display: grid;
|
|
29
|
+
place-items: center;
|
|
30
|
+
min-height: clamp(280px, 42vw, 420px);
|
|
31
|
+
isolation: isolate;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.not-found-orbit {
|
|
35
|
+
position: absolute;
|
|
36
|
+
width: min(100%, 360px);
|
|
37
|
+
aspect-ratio: 1;
|
|
38
|
+
border: 1px solid color-mix(in srgb, var(--vp-c-brand-1) 26%, transparent);
|
|
39
|
+
border-radius: 50%;
|
|
40
|
+
background:
|
|
41
|
+
radial-gradient(circle at 50% 50%, color-mix(in srgb, var(--vp-c-brand-1) 18%, transparent) 0 2px, transparent 3px),
|
|
42
|
+
radial-gradient(circle at 18% 24%, color-mix(in srgb, #f59e0b 32%, transparent) 0 3px, transparent 4px),
|
|
43
|
+
radial-gradient(circle at 78% 30%, color-mix(in srgb, #14b8a6 30%, transparent) 0 2px, transparent 3px),
|
|
44
|
+
radial-gradient(circle at 72% 76%, color-mix(in srgb, #ef4444 28%, transparent) 0 3px, transparent 4px);
|
|
45
|
+
box-shadow:
|
|
46
|
+
inset 0 0 56px color-mix(in srgb, var(--vp-c-brand-1) 9%, transparent),
|
|
47
|
+
0 28px 80px color-mix(in srgb, var(--vp-c-brand-1) 12%, transparent);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.not-found-orbit::before,
|
|
51
|
+
.not-found-orbit::after {
|
|
52
|
+
content: "";
|
|
53
|
+
position: absolute;
|
|
54
|
+
inset: 12%;
|
|
55
|
+
border: 1px dashed color-mix(in srgb, var(--vp-c-text-2) 22%, transparent);
|
|
56
|
+
border-radius: 50%;
|
|
57
|
+
transform: rotate(-22deg) scaleX(1.18);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.not-found-orbit::after {
|
|
61
|
+
inset: 25%;
|
|
62
|
+
transform: rotate(34deg) scaleX(1.35);
|
|
63
|
+
border-style: solid;
|
|
64
|
+
opacity: .55;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.not-found-satellite {
|
|
68
|
+
position: absolute;
|
|
69
|
+
width: 14px;
|
|
70
|
+
height: 14px;
|
|
71
|
+
border-radius: 50%;
|
|
72
|
+
box-shadow: 0 0 0 6px color-mix(in srgb, currentcolor 12%, transparent);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.not-found-satellite-a {
|
|
76
|
+
top: 18%;
|
|
77
|
+
left: 67%;
|
|
78
|
+
color: #f59e0b;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.not-found-satellite-b {
|
|
82
|
+
right: 15%;
|
|
83
|
+
bottom: 24%;
|
|
84
|
+
color: #14b8a6;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.not-found-satellite-c {
|
|
88
|
+
bottom: 18%;
|
|
89
|
+
left: 24%;
|
|
90
|
+
color: #ef4444;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.not-found-signal {
|
|
94
|
+
position: absolute;
|
|
95
|
+
right: 2%;
|
|
96
|
+
bottom: 10%;
|
|
97
|
+
max-width: 9rem;
|
|
98
|
+
padding: 6px 10px;
|
|
99
|
+
border: 1px solid var(--vp-c-divider);
|
|
100
|
+
border-radius: 8px;
|
|
101
|
+
background: var(--vp-c-bg);
|
|
102
|
+
color: var(--vp-c-text-2);
|
|
103
|
+
font-size: 12px;
|
|
104
|
+
line-height: 1.4;
|
|
105
|
+
box-shadow: 0 12px 32px rgb(0 0 0 / .08);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.not-found-code {
|
|
109
|
+
position: relative;
|
|
110
|
+
z-index: 1;
|
|
111
|
+
color: var(--vp-c-text-1);
|
|
112
|
+
font-size: clamp(6.5rem, 18vw, 13rem);
|
|
113
|
+
font-weight: 900;
|
|
114
|
+
line-height: .82;
|
|
115
|
+
letter-spacing: 0;
|
|
116
|
+
text-shadow:
|
|
117
|
+
0 1px 0 var(--vp-c-bg),
|
|
118
|
+
0 14px 34px rgb(0 0 0 / .18);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@media (max-width: 767px) {
|
|
122
|
+
.not-found-visual {
|
|
123
|
+
min-height: clamp(132px, 24svh, 205px);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.not-found-orbit {
|
|
127
|
+
width: min(56vw, 200px, 28svh);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.not-found-code {
|
|
131
|
+
font-size: clamp(4.5rem, 22vw, 6.25rem);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.not-found-signal {
|
|
135
|
+
right: 0;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@media (max-width: 767px) and (max-height: 700px) {
|
|
140
|
+
.not-found-visual {
|
|
141
|
+
min-height: clamp(104px, 21svh, 150px);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.not-found-orbit {
|
|
145
|
+
width: min(46vw, 160px, 24svh);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.not-found-signal {
|
|
149
|
+
display: none;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
154
|
+
.not-found-satellite-a {
|
|
155
|
+
animation: not-found-float 5s ease-in-out infinite;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.not-found-satellite-b {
|
|
159
|
+
animation: not-found-float 6s ease-in-out .8s infinite;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.not-found-satellite-c {
|
|
163
|
+
animation: not-found-float 5.5s ease-in-out .35s infinite;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@keyframes not-found-float {
|
|
168
|
+
0%,
|
|
169
|
+
100% {
|
|
170
|
+
transform: translate3d(0, 0, 0);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
50% {
|
|
174
|
+
transform: translate3d(0, -10px, 0);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
</style>
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
2
|
import type { CategoryList, Post } from 'valaxy'
|
|
3
|
+
import type { PressTheme } from '../types'
|
|
3
4
|
import { removeItemFromCategory, usePageList, useSidebar } from 'valaxy'
|
|
4
5
|
import { computed } from 'vue'
|
|
6
|
+
import { useRoute } from 'vue-router'
|
|
5
7
|
import { useLocaleConfig } from '../composables'
|
|
8
|
+
import { getSidebar, getSidebarGroups, isSidebarItem } from '../utils/sidebar'
|
|
6
9
|
|
|
7
10
|
defineProps<{
|
|
8
11
|
open: boolean
|
|
9
12
|
}>()
|
|
10
13
|
|
|
11
14
|
const pages = usePageList()
|
|
15
|
+
const route = useRoute()
|
|
12
16
|
const { localeConfig, currentLocaleKey, hasLocales, currentLocale } = useLocaleConfig()
|
|
13
17
|
|
|
14
18
|
/**
|
|
@@ -34,6 +38,29 @@ const localePages = computed(() => {
|
|
|
34
38
|
|
|
35
39
|
const sidebar = computed(() => localeConfig.value.sidebar)
|
|
36
40
|
|
|
41
|
+
const sidebarItems = computed(() => getSidebar(sidebar.value, route.path))
|
|
42
|
+
|
|
43
|
+
const sidebarGroups = computed(() => {
|
|
44
|
+
return getSidebarGroups(sidebarItems.value)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const renderGroups = computed(() => {
|
|
48
|
+
return sidebarGroups.value.map((group, index) => {
|
|
49
|
+
const groupItem = group.text || group.link
|
|
50
|
+
? { ...group, items: group.items.filter(isSidebarItem) }
|
|
51
|
+
: undefined
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
key: group.text || group.link || index,
|
|
55
|
+
groupItem,
|
|
56
|
+
items: groupItem ? [] : group.items,
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// Explicit groups and generated categories share the same root visual depth.
|
|
62
|
+
const sidebarGroupDepth = 0
|
|
63
|
+
|
|
37
64
|
/**
|
|
38
65
|
* Build categories from locale-filtered pages.
|
|
39
66
|
* Fully reactive: recomputes when route/locale changes.
|
|
@@ -103,9 +130,8 @@ const categories = computed(() => {
|
|
|
103
130
|
|
|
104
131
|
// Remove categories not listed in sidebar config
|
|
105
132
|
removeItemFromCategory(categoryList, 'Uncategorized')
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const sidebarNames = sidebarVal.filter((item): item is string => typeof item === 'string')
|
|
133
|
+
if (sidebarItems.value.length) {
|
|
134
|
+
const sidebarNames = sidebarItems.value.filter((item): item is string => typeof item === 'string')
|
|
109
135
|
categoryList.children.forEach((_val, key) => {
|
|
110
136
|
if (!sidebarNames.includes(key))
|
|
111
137
|
removeItemFromCategory(categoryList, key)
|
|
@@ -116,31 +142,63 @@ const categories = computed(() => {
|
|
|
116
142
|
})
|
|
117
143
|
|
|
118
144
|
const { hasSidebar } = useSidebar()
|
|
145
|
+
|
|
146
|
+
const shouldShowSidebar = computed(() => {
|
|
147
|
+
return hasSidebar.value && sidebarGroups.value.length > 0
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
function getSidebarItemKey(item: PressTheme.SidebarEntry, index: number): string | number {
|
|
151
|
+
if (typeof item === 'string')
|
|
152
|
+
return item
|
|
153
|
+
return item.text || item.link || index
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getSidebarRootItemClasses(groupIndex: number, itemIndex = 0) {
|
|
157
|
+
return [
|
|
158
|
+
'press-sidebar-root-item',
|
|
159
|
+
{ 'is-separated': groupIndex > 0 || itemIndex > 0 },
|
|
160
|
+
]
|
|
161
|
+
}
|
|
119
162
|
</script>
|
|
120
163
|
|
|
121
164
|
<template>
|
|
122
165
|
<aside
|
|
123
|
-
v-if="
|
|
166
|
+
v-if="shouldShowSidebar"
|
|
124
167
|
class="press-sidebar shadow-lg" :class="{ open }"
|
|
125
168
|
@click.stop
|
|
126
169
|
>
|
|
127
|
-
<
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
170
|
+
<nav
|
|
171
|
+
id="pr-sidebar-nav"
|
|
172
|
+
class="press-sidebar-nav"
|
|
173
|
+
aria-label="Sidebar Navigation"
|
|
174
|
+
text="left"
|
|
175
|
+
m="2"
|
|
176
|
+
>
|
|
177
|
+
<ul v-for="(group, groupIndex) in renderGroups" :key="group.key" class="press-sidebar-list category-list">
|
|
135
178
|
<PressSidebarItem
|
|
136
|
-
v-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
:
|
|
140
|
-
:depth="0"
|
|
179
|
+
v-if="group.groupItem"
|
|
180
|
+
:class="getSidebarRootItemClasses(groupIndex)"
|
|
181
|
+
:item="group.groupItem"
|
|
182
|
+
:depth="sidebarGroupDepth"
|
|
141
183
|
/>
|
|
184
|
+
<template v-else>
|
|
185
|
+
<template v-for="(item, index) in group.items" :key="getSidebarItemKey(item, index)">
|
|
186
|
+
<PressCategoryByName
|
|
187
|
+
v-if="typeof item === 'string'"
|
|
188
|
+
:class="getSidebarRootItemClasses(groupIndex, index)"
|
|
189
|
+
:categories="categories"
|
|
190
|
+
:item="item"
|
|
191
|
+
/>
|
|
192
|
+
<PressSidebarItem
|
|
193
|
+
v-else
|
|
194
|
+
:class="getSidebarRootItemClasses(groupIndex, index)"
|
|
195
|
+
:item="item"
|
|
196
|
+
:depth="0"
|
|
197
|
+
/>
|
|
198
|
+
</template>
|
|
199
|
+
</template>
|
|
142
200
|
</ul>
|
|
143
|
-
</
|
|
201
|
+
</nav>
|
|
144
202
|
</aside>
|
|
145
203
|
</template>
|
|
146
204
|
|
|
@@ -189,6 +247,24 @@ const { hasSidebar } = useSidebar()
|
|
|
189
247
|
}
|
|
190
248
|
}
|
|
191
249
|
|
|
250
|
+
.press-sidebar-list {
|
|
251
|
+
list-style: none;
|
|
252
|
+
margin: 0;
|
|
253
|
+
padding: 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.press-sidebar-root-item.is-separated {
|
|
257
|
+
border-top: 1px solid var(--pr-c-divider-light);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.press-sidebar-root-item > .press-sidebar-item {
|
|
261
|
+
padding-top: 0.5rem;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.press-sidebar-root-item > .category-list-item {
|
|
265
|
+
border-top: 0;
|
|
266
|
+
}
|
|
267
|
+
|
|
192
268
|
.category-list {
|
|
193
269
|
&:first-child {
|
|
194
270
|
.category-list-item {
|
|
@@ -211,6 +287,16 @@ const { hasSidebar } = useSidebar()
|
|
|
211
287
|
flex-shrink: 0;
|
|
212
288
|
}
|
|
213
289
|
|
|
290
|
+
.caret-icon {
|
|
291
|
+
width: 18px;
|
|
292
|
+
height: 18px;
|
|
293
|
+
transition: transform var(--va-transition-duration);
|
|
294
|
+
|
|
295
|
+
&.open {
|
|
296
|
+
transform: rotate(90deg);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
214
300
|
&:hover .caret {
|
|
215
301
|
color: var(--vp-c-text-2);
|
|
216
302
|
}
|
|
@@ -219,8 +305,4 @@ const { hasSidebar } = useSidebar()
|
|
|
219
305
|
color: var(--vp-c-text-1);
|
|
220
306
|
}
|
|
221
307
|
}
|
|
222
|
-
|
|
223
|
-
.category-list+.category-list {
|
|
224
|
-
margin-top: 1rem;
|
|
225
|
-
}
|
|
226
308
|
</style>
|
|
@@ -19,8 +19,6 @@ const {
|
|
|
19
19
|
toggle,
|
|
20
20
|
} = useSidebarControl(computed(() => props.item))
|
|
21
21
|
|
|
22
|
-
const sectionTag = computed(() => (hasChildren.value ? 'section' : `div`))
|
|
23
|
-
|
|
24
22
|
const linkTag = computed(() => (isLink.value ? 'a' : 'div'))
|
|
25
23
|
|
|
26
24
|
const textTag = computed(() => {
|
|
@@ -31,7 +29,13 @@ const textTag = computed(() => {
|
|
|
31
29
|
: `h${props.depth + 2}`
|
|
32
30
|
})
|
|
33
31
|
|
|
34
|
-
const
|
|
32
|
+
const rawText = computed(() => props.item.text || '')
|
|
33
|
+
|
|
34
|
+
const childItems = computed(() => props.item.items || [])
|
|
35
|
+
|
|
36
|
+
const hasChildItems = computed(() => childItems.value.length > 0)
|
|
37
|
+
|
|
38
|
+
const hasCaret = computed(() => collapsible.value && hasChildItems.value)
|
|
35
39
|
|
|
36
40
|
const classes = computed(() => [
|
|
37
41
|
[`level-${props.depth}`],
|
|
@@ -42,6 +46,8 @@ const classes = computed(() => [
|
|
|
42
46
|
{ 'has-active': hasActiveLink.value },
|
|
43
47
|
])
|
|
44
48
|
|
|
49
|
+
const itemRole = computed(() => (hasCaret.value && !isLink.value ? 'button' : undefined))
|
|
50
|
+
|
|
45
51
|
function onItemInteraction(e: MouseEvent | Event) {
|
|
46
52
|
if ('key' in e && e.key !== 'Enter')
|
|
47
53
|
return
|
|
@@ -49,27 +55,28 @@ function onItemInteraction(e: MouseEvent | Event) {
|
|
|
49
55
|
!props.item.link && toggle()
|
|
50
56
|
}
|
|
51
57
|
|
|
52
|
-
function onCaretClick() {
|
|
53
|
-
props.item.link && toggle()
|
|
54
|
-
}
|
|
55
|
-
|
|
56
58
|
const { t } = useI18n()
|
|
57
59
|
|
|
58
|
-
const htmlText = computed(() =>
|
|
60
|
+
const htmlText = computed(() => {
|
|
61
|
+
return t(rawText.value) || rawText.value
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
function getChildItemKey(item: DefaultTheme.SidebarItem, index: number): string | number {
|
|
65
|
+
return item.text || item.link || index
|
|
66
|
+
}
|
|
59
67
|
</script>
|
|
60
68
|
|
|
61
69
|
<template>
|
|
62
|
-
<
|
|
63
|
-
:
|
|
64
|
-
class="VPSidebarItem" :class="classes"
|
|
70
|
+
<li
|
|
71
|
+
class="VPSidebarItem press-sidebar-item-node" :class="classes"
|
|
65
72
|
>
|
|
66
73
|
<div
|
|
67
|
-
v-if="
|
|
74
|
+
v-if="rawText"
|
|
68
75
|
class="press-sidebar-item item"
|
|
69
76
|
:role="itemRole"
|
|
70
|
-
:tabindex="item.
|
|
77
|
+
:tabindex="hasCaret && !props.item.link ? 0 : undefined"
|
|
71
78
|
v-on="
|
|
72
|
-
item.
|
|
79
|
+
hasCaret && !props.item.link
|
|
73
80
|
? { click: onItemInteraction, keydown: onItemInteraction }
|
|
74
81
|
: {}
|
|
75
82
|
"
|
|
@@ -77,47 +84,57 @@ const htmlText = computed(() => t(props.item.text || ''))
|
|
|
77
84
|
<div class="indicator" />
|
|
78
85
|
|
|
79
86
|
<AppLink
|
|
80
|
-
v-if="item.link"
|
|
87
|
+
v-if="props.item.link"
|
|
81
88
|
:tag="linkTag"
|
|
82
89
|
class="link"
|
|
83
|
-
:href="item.link"
|
|
84
|
-
:rel="item.rel"
|
|
85
|
-
:target="item.target"
|
|
90
|
+
:href="props.item.link"
|
|
91
|
+
:rel="props.item.rel"
|
|
92
|
+
:target="props.item.target"
|
|
86
93
|
>
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
<component :is="textTag" class="text ml-1">
|
|
95
|
+
<span v-html="htmlText" />
|
|
96
|
+
</component>
|
|
89
97
|
</AppLink>
|
|
90
|
-
|
|
91
|
-
|
|
98
|
+
<component :is="textTag" v-else class="text ml-1">
|
|
99
|
+
<span v-html="htmlText" />
|
|
100
|
+
</component>
|
|
92
101
|
|
|
93
102
|
<button
|
|
94
|
-
v-if="
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
v-if="hasCaret"
|
|
104
|
+
type="button"
|
|
105
|
+
aria-label="toggle section"
|
|
106
|
+
:aria-expanded="!collapsed"
|
|
107
|
+
class="caret"
|
|
108
|
+
@click.stop="toggle"
|
|
98
109
|
>
|
|
99
|
-
<
|
|
100
|
-
<div v-else i-ri-folder-reduce-line />
|
|
110
|
+
<span class="caret-icon" :class="{ open: !collapsed }" i-ri-arrow-right-s-line aria-hidden="true" />
|
|
101
111
|
</button>
|
|
102
112
|
</div>
|
|
103
113
|
|
|
104
|
-
<
|
|
114
|
+
<ul v-if="hasChildItems" class="items press-sidebar-item-list">
|
|
105
115
|
<template v-if="depth < 5">
|
|
106
116
|
<PressSidebarItem
|
|
107
|
-
v-for="i in
|
|
108
|
-
:key="i
|
|
117
|
+
v-for="(i, index) in childItems"
|
|
118
|
+
:key="getChildItemKey(i, index)"
|
|
109
119
|
:item="i"
|
|
110
120
|
:depth="depth + 1"
|
|
111
121
|
/>
|
|
112
122
|
</template>
|
|
113
|
-
</
|
|
114
|
-
</
|
|
123
|
+
</ul>
|
|
124
|
+
</li>
|
|
115
125
|
</template>
|
|
116
126
|
|
|
117
127
|
<style scoped>
|
|
118
|
-
|
|
128
|
+
.press-sidebar-item-node,
|
|
129
|
+
.press-sidebar-item-list {
|
|
130
|
+
list-style: none;
|
|
131
|
+
margin: 0;
|
|
132
|
+
padding: 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.VPSidebarItem.level-0 {
|
|
119
136
|
padding-bottom: 24px;
|
|
120
|
-
}
|
|
137
|
+
}
|
|
121
138
|
|
|
122
139
|
.VPSidebarItem.collapsed.level-0 {
|
|
123
140
|
padding-bottom: 10px;
|
package/composables/prev-next.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { isCategoryList, removeItemFromCategory, useFrontmatter, usePageList, us
|
|
|
4
4
|
import { computed } from 'vue'
|
|
5
5
|
import { useI18n } from 'vue-i18n'
|
|
6
6
|
import { useRoute } from 'vue-router'
|
|
7
|
+
import { getSidebar } from '../utils/sidebar'
|
|
7
8
|
import { useLocaleConfig } from './locale'
|
|
8
9
|
import { normalize } from './sidebar'
|
|
9
10
|
|
|
@@ -109,70 +110,6 @@ function getCategoryLinks(categoryTree: CategoryList, categoryName: string, reso
|
|
|
109
110
|
return links
|
|
110
111
|
}
|
|
111
112
|
|
|
112
|
-
/**
|
|
113
|
-
* Resolve sidebar items for the current route path from the theme config.
|
|
114
|
-
*/
|
|
115
|
-
function getSidebarItems(
|
|
116
|
-
sidebar: PressTheme.Sidebar | undefined,
|
|
117
|
-
path: string,
|
|
118
|
-
): PressTheme.SidebarItem[] {
|
|
119
|
-
if (!sidebar)
|
|
120
|
-
return []
|
|
121
|
-
|
|
122
|
-
if (Array.isArray(sidebar)) {
|
|
123
|
-
// Sidebar arrays may contain strings (category names) — filter to objects only
|
|
124
|
-
const items = sidebar.filter((item): item is PressTheme.SidebarItem => typeof item !== 'string')
|
|
125
|
-
return addBase(items)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// SidebarMulti: find the longest matching prefix
|
|
129
|
-
path = ensureStartingSlash(path)
|
|
130
|
-
const dir = Object.keys(sidebar)
|
|
131
|
-
.sort((a, b) => b.split('/').length - a.split('/').length)
|
|
132
|
-
.find((dir) => {
|
|
133
|
-
const normalizedDir = ensureStartingSlash(dir)
|
|
134
|
-
// Ensure boundary match: dir must end with '/' or path must have '/' after dir
|
|
135
|
-
const dirWithSlash = normalizedDir.endsWith('/') ? normalizedDir : `${normalizedDir}/`
|
|
136
|
-
return path.startsWith(dirWithSlash) || path === normalizedDir
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
if (!dir)
|
|
140
|
-
return []
|
|
141
|
-
|
|
142
|
-
const matched = sidebar[dir]
|
|
143
|
-
if (Array.isArray(matched)) {
|
|
144
|
-
const items = matched.filter((item): item is PressTheme.SidebarItem => typeof item !== 'string')
|
|
145
|
-
return addBase(items)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return addBase(matched.items, matched.base)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function ensureStartingSlash(path: string): string {
|
|
152
|
-
return path.startsWith('/') ? path : `/${path}`
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Join base and link with exactly one `/` separator.
|
|
157
|
-
*/
|
|
158
|
-
function joinPath(base: string, link: string): string {
|
|
159
|
-
const normalizedBase = base.endsWith('/') ? base.slice(0, -1) : base
|
|
160
|
-
const normalizedLink = link.startsWith('/') ? link : `/${link}`
|
|
161
|
-
return normalizedBase + normalizedLink
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function addBase(items: PressTheme.SidebarItem[], base?: string): PressTheme.SidebarItem[] {
|
|
165
|
-
return items.map((item) => {
|
|
166
|
-
const result = { ...item }
|
|
167
|
-
const itemBase = result.base || base
|
|
168
|
-
if (itemBase && result.link)
|
|
169
|
-
result.link = joinPath(itemBase, result.link)
|
|
170
|
-
if (result.items)
|
|
171
|
-
result.items = addBase(result.items, itemBase)
|
|
172
|
-
return result
|
|
173
|
-
})
|
|
174
|
-
}
|
|
175
|
-
|
|
176
113
|
interface FlatLink {
|
|
177
114
|
text: string
|
|
178
115
|
link: string
|
|
@@ -251,29 +188,17 @@ export function usePrevNext() {
|
|
|
251
188
|
})
|
|
252
189
|
|
|
253
190
|
return computed(() => {
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
candidates = []
|
|
263
|
-
for (const item of sidebar) {
|
|
264
|
-
if (typeof item === 'string') {
|
|
265
|
-
// Expand category name into its page links
|
|
266
|
-
candidates.push(...getCategoryLinks(categoryTree, item, resolveTitle))
|
|
267
|
-
}
|
|
268
|
-
else {
|
|
269
|
-
// Object sidebar item — flatten as before
|
|
270
|
-
candidates.push(...getFlatLinks(addBase([item]), t))
|
|
271
|
-
}
|
|
191
|
+
const categoryTree = buildCategoryTree(localePages.value)
|
|
192
|
+
const sidebarItems = getSidebar(localeConfig.value.sidebar, route.path)
|
|
193
|
+
const candidates: FlatLink[] = []
|
|
194
|
+
|
|
195
|
+
for (const item of sidebarItems) {
|
|
196
|
+
if (typeof item === 'string') {
|
|
197
|
+
candidates.push(...getCategoryLinks(categoryTree, item, resolveTitle))
|
|
198
|
+
continue
|
|
272
199
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
// SidebarMulti — only object items, use original logic
|
|
276
|
-
candidates = getFlatLinks(getSidebarItems(sidebar, route.path), t)
|
|
200
|
+
|
|
201
|
+
candidates.push(...getFlatLinks([item], t))
|
|
277
202
|
}
|
|
278
203
|
|
|
279
204
|
const index = candidates.findIndex(link =>
|
package/composables/sidebar.ts
CHANGED
|
@@ -38,7 +38,7 @@ export function isActive(
|
|
|
38
38
|
if (matchPath === undefined)
|
|
39
39
|
return false
|
|
40
40
|
|
|
41
|
-
currentPath = normalize(`/${currentPath}`)
|
|
41
|
+
currentPath = normalize(currentPath.startsWith('/') ? currentPath : `/${currentPath}`)
|
|
42
42
|
|
|
43
43
|
if (asRegex)
|
|
44
44
|
return new RegExp(matchPath).test(currentPath)
|
package/layouts/404.vue
CHANGED
|
@@ -1,34 +1,280 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
+
import { useBack } from 'valaxy'
|
|
3
|
+
import { computed } from 'vue'
|
|
2
4
|
import { useI18n } from 'vue-i18n'
|
|
3
|
-
import {
|
|
5
|
+
import { useRoute } from 'vue-router'
|
|
6
|
+
import PressNotFoundVisual from '../components/PressNotFoundVisual.vue'
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
|
|
8
|
+
type NotFoundLocale = 'en' | 'zh'
|
|
9
|
+
|
|
10
|
+
const route = useRoute()
|
|
11
|
+
const { locale } = useI18n()
|
|
12
|
+
const { back } = useBack()
|
|
13
|
+
|
|
14
|
+
const messages = {
|
|
15
|
+
en: {
|
|
16
|
+
eyebrow: 'Lost route',
|
|
17
|
+
title: 'This page drifted out of orbit.',
|
|
18
|
+
description: 'The address you opened does not match a page in this site. Check the path, return to the last page, or restart from the home page.',
|
|
19
|
+
currentPath: 'Current path',
|
|
20
|
+
back: 'Go back',
|
|
21
|
+
home: 'Home',
|
|
22
|
+
orbit: 'Route signal',
|
|
23
|
+
},
|
|
24
|
+
zh: {
|
|
25
|
+
eyebrow: '路线迷航',
|
|
26
|
+
title: '这个页面暂时偏离了轨道。',
|
|
27
|
+
description: '当前地址没有匹配到站点页面。你可以检查路径,返回上一页,或从首页重新出发。',
|
|
28
|
+
currentPath: '当前路径',
|
|
29
|
+
back: '返回上一页',
|
|
30
|
+
home: '回到首页',
|
|
31
|
+
orbit: '路由信号',
|
|
32
|
+
},
|
|
33
|
+
} as const
|
|
34
|
+
|
|
35
|
+
const currentLocale = computed<NotFoundLocale>(
|
|
36
|
+
() => locale.value.startsWith('zh') || route.path.startsWith('/zh') ? 'zh' : 'en',
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const content = computed(() => messages[currentLocale.value])
|
|
40
|
+
const currentPath = computed(() => route.fullPath || route.path)
|
|
41
|
+
const homeLink = computed(() => currentLocale.value === 'zh' ? '/zh/' : '/')
|
|
7
42
|
</script>
|
|
8
43
|
|
|
9
44
|
<template>
|
|
10
|
-
<
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
45
|
+
<div class="layout antialiased not-found-layout">
|
|
46
|
+
<PressNav />
|
|
47
|
+
<main class="not-found-shell">
|
|
48
|
+
<section class="not-found-panel" aria-labelledby="not-found-title">
|
|
49
|
+
<PressNotFoundVisual :label="content.orbit" />
|
|
50
|
+
|
|
51
|
+
<div class="not-found-copy">
|
|
52
|
+
<div class="not-found-toolbar">
|
|
53
|
+
<p class="not-found-eyebrow">
|
|
54
|
+
<span i-ri-compass-3-line aria-hidden="true" />
|
|
55
|
+
{{ content.eyebrow }}
|
|
56
|
+
</p>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<h1 id="not-found-title" class="not-found-title">
|
|
60
|
+
{{ content.title }}
|
|
61
|
+
</h1>
|
|
62
|
+
|
|
63
|
+
<p class="not-found-description">
|
|
64
|
+
{{ content.description }}
|
|
65
|
+
</p>
|
|
66
|
+
|
|
67
|
+
<p class="not-found-path">
|
|
68
|
+
<span>{{ content.currentPath }}</span>
|
|
69
|
+
<code>{{ currentPath }}</code>
|
|
70
|
+
</p>
|
|
71
|
+
|
|
72
|
+
<div class="not-found-actions">
|
|
73
|
+
<button type="button" class="not-found-action primary" @click="back">
|
|
74
|
+
<span i-ri-arrow-left-line aria-hidden="true" />
|
|
75
|
+
{{ content.back }}
|
|
76
|
+
</button>
|
|
77
|
+
|
|
78
|
+
<AppLink class="not-found-action secondary" :href="homeLink">
|
|
79
|
+
<span i-ri-home-4-line aria-hidden="true" />
|
|
80
|
+
{{ content.home }}
|
|
81
|
+
</AppLink>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</section>
|
|
85
|
+
</main>
|
|
86
|
+
</div>
|
|
27
87
|
</template>
|
|
28
88
|
|
|
29
89
|
<style lang="scss" scoped>
|
|
30
|
-
.not-found {
|
|
31
|
-
|
|
32
|
-
|
|
90
|
+
.not-found-layout {
|
|
91
|
+
min-height: 100svh;
|
|
92
|
+
overflow: hidden;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.not-found-shell {
|
|
96
|
+
min-height: 100svh;
|
|
97
|
+
box-sizing: border-box;
|
|
98
|
+
display: grid;
|
|
99
|
+
place-items: center;
|
|
100
|
+
overflow: hidden;
|
|
101
|
+
padding: calc(var(--pr-nav-height) + clamp(16px, 3vw, 28px)) clamp(18px, 5vw, 72px) clamp(20px, 4vw, 40px);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.not-found-panel {
|
|
105
|
+
width: min(100%, 1040px);
|
|
106
|
+
max-height: calc(100svh - var(--pr-nav-height) - clamp(36px, 7vw, 68px));
|
|
107
|
+
display: grid;
|
|
108
|
+
grid-template-columns: minmax(280px, .9fr) minmax(0, 1fr);
|
|
109
|
+
align-items: center;
|
|
110
|
+
gap: clamp(28px, 6vw, 72px);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.not-found-copy {
|
|
114
|
+
min-width: 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.not-found-toolbar {
|
|
118
|
+
display: flex;
|
|
119
|
+
align-items: center;
|
|
120
|
+
margin-bottom: 18px;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.not-found-eyebrow {
|
|
124
|
+
display: inline-flex;
|
|
125
|
+
align-items: center;
|
|
126
|
+
gap: 8px;
|
|
127
|
+
margin: 0;
|
|
128
|
+
color: var(--vp-c-brand-1);
|
|
129
|
+
font-size: 14px;
|
|
130
|
+
font-weight: 700;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.not-found-title {
|
|
134
|
+
max-width: 12ch;
|
|
135
|
+
margin: 0;
|
|
136
|
+
color: var(--vp-c-text-1);
|
|
137
|
+
font-size: clamp(2.25rem, 6vw, 4.75rem);
|
|
138
|
+
line-height: .98;
|
|
139
|
+
letter-spacing: 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.not-found-description {
|
|
143
|
+
max-width: 42rem;
|
|
144
|
+
margin: 18px 0 0;
|
|
145
|
+
color: var(--vp-c-text-2);
|
|
146
|
+
font-size: clamp(1rem, 2vw, 1.125rem);
|
|
147
|
+
line-height: 1.7;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.not-found-path {
|
|
151
|
+
display: grid;
|
|
152
|
+
gap: 8px;
|
|
153
|
+
max-width: 100%;
|
|
154
|
+
margin: 20px 0 0;
|
|
155
|
+
padding: 12px 16px;
|
|
156
|
+
border: 1px solid var(--vp-c-divider);
|
|
157
|
+
border-radius: 8px;
|
|
158
|
+
background: color-mix(in srgb, var(--vp-c-bg-soft) 72%, transparent);
|
|
159
|
+
color: var(--vp-c-text-2);
|
|
160
|
+
font-size: 13px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.not-found-path code {
|
|
164
|
+
display: block;
|
|
165
|
+
max-width: 100%;
|
|
166
|
+
overflow-wrap: anywhere;
|
|
167
|
+
color: var(--vp-c-text-1);
|
|
168
|
+
font-size: 14px;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.not-found-actions {
|
|
172
|
+
display: flex;
|
|
173
|
+
flex-wrap: wrap;
|
|
174
|
+
gap: 12px;
|
|
175
|
+
margin-top: 22px;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.not-found-action {
|
|
179
|
+
display: inline-flex;
|
|
180
|
+
align-items: center;
|
|
181
|
+
justify-content: center;
|
|
182
|
+
gap: 8px;
|
|
183
|
+
min-height: 42px;
|
|
184
|
+
padding: 0 18px;
|
|
185
|
+
border: 1px solid transparent;
|
|
186
|
+
border-radius: 999px;
|
|
187
|
+
font-size: 14px;
|
|
188
|
+
font-weight: 700;
|
|
189
|
+
text-decoration: none;
|
|
190
|
+
cursor: pointer;
|
|
191
|
+
transition: transform var(--va-transition-duration), border-color var(--va-transition-duration), background-color var(--va-transition-duration), color var(--va-transition-duration), box-shadow var(--va-transition-duration);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.not-found-action:hover {
|
|
195
|
+
transform: translateY(-1px);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.not-found-action.primary {
|
|
199
|
+
background: var(--vp-c-brand-1);
|
|
200
|
+
color: var(--vp-c-white);
|
|
201
|
+
box-shadow: 0 12px 28px color-mix(in srgb, var(--vp-c-brand-1) 24%, transparent);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.not-found-action.secondary {
|
|
205
|
+
border-color: var(--vp-c-divider);
|
|
206
|
+
background: var(--vp-c-bg);
|
|
207
|
+
color: var(--vp-c-text-1);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
@media (max-width: 767px) {
|
|
211
|
+
.not-found-layout {
|
|
212
|
+
height: 100svh;
|
|
213
|
+
display: flex;
|
|
214
|
+
flex-direction: column;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.not-found-shell {
|
|
218
|
+
flex: 1;
|
|
219
|
+
min-height: 0;
|
|
220
|
+
padding: clamp(8px, 2.4svh, 16px) clamp(18px, 5vw, 24px) clamp(12px, 3svh, 20px);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.not-found-panel {
|
|
224
|
+
max-height: 100%;
|
|
225
|
+
grid-template-columns: 1fr;
|
|
226
|
+
align-content: center;
|
|
227
|
+
gap: clamp(8px, 1.6svh, 14px);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.not-found-toolbar {
|
|
231
|
+
margin-bottom: clamp(8px, 1.8svh, 14px);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.not-found-title {
|
|
235
|
+
max-width: 13ch;
|
|
236
|
+
font-size: clamp(1.75rem, 8.5vw, 2.75rem);
|
|
237
|
+
line-height: 1;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.not-found-description {
|
|
241
|
+
margin-top: clamp(8px, 1.6svh, 12px);
|
|
242
|
+
font-size: clamp(.88rem, 3.8vw, 1rem);
|
|
243
|
+
line-height: 1.55;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.not-found-path {
|
|
247
|
+
gap: 6px;
|
|
248
|
+
margin-top: clamp(10px, 1.8svh, 14px);
|
|
249
|
+
padding: 10px 14px;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.not-found-path code {
|
|
253
|
+
font-size: 13px;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.not-found-actions,
|
|
257
|
+
.not-found-action {
|
|
258
|
+
width: 100%;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.not-found-actions {
|
|
262
|
+
gap: 8px;
|
|
263
|
+
margin-top: clamp(10px, 2svh, 16px);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.not-found-action {
|
|
267
|
+
min-height: 38px;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
@media (max-width: 767px) and (max-height: 700px) {
|
|
272
|
+
.not-found-path {
|
|
273
|
+
display: none;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.not-found-description {
|
|
277
|
+
max-width: 34rem;
|
|
278
|
+
}
|
|
33
279
|
}
|
|
34
280
|
</style>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "valaxy-theme-press",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-beta.2",
|
|
4
4
|
"description": "Docs (VitePress-like) theme for Valaxy",
|
|
5
5
|
"author": {
|
|
6
6
|
"email": "me@yunyoujun.cn",
|
|
@@ -33,10 +33,10 @@
|
|
|
33
33
|
"@docsearch/css": "^4.6.3",
|
|
34
34
|
"@docsearch/js": "^4.6.3",
|
|
35
35
|
"@docsearch/sidepanel-js": "^4.6.3",
|
|
36
|
-
"reka-ui": "^2.
|
|
36
|
+
"reka-ui": "^2.10.1",
|
|
37
37
|
"vitepress": "^2.0.0-alpha.17"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"valaxy": "0.
|
|
40
|
+
"valaxy": "1.0.0-beta.2"
|
|
41
41
|
}
|
|
42
42
|
}
|
package/types/index.d.ts
CHANGED
|
@@ -4,8 +4,6 @@ export * from '../composables'
|
|
|
4
4
|
export * from './home.d'
|
|
5
5
|
|
|
6
6
|
export namespace PressTheme {
|
|
7
|
-
export type Sidebar = any
|
|
8
|
-
|
|
9
7
|
export interface Footer {
|
|
10
8
|
message?: string
|
|
11
9
|
copyright?: string
|
|
@@ -32,9 +30,10 @@ export namespace PressTheme {
|
|
|
32
30
|
text?: string
|
|
33
31
|
}
|
|
34
32
|
|
|
35
|
-
export type
|
|
33
|
+
export type SidebarEntry = string | SidebarItem
|
|
34
|
+
export type Sidebar = SidebarEntry[] | SidebarMulti
|
|
36
35
|
export interface SidebarMulti {
|
|
37
|
-
[path: string]:
|
|
36
|
+
[path: string]: SidebarEntry[] | { items: SidebarEntry[], base?: string }
|
|
38
37
|
}
|
|
39
38
|
export interface SidebarItem {
|
|
40
39
|
/**
|
package/utils/sidebar.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { PressTheme } from '../types'
|
|
2
|
+
|
|
3
|
+
export interface SidebarGroup extends Omit<PressTheme.SidebarItem, 'items'> {
|
|
4
|
+
items: PressTheme.SidebarEntry[]
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the sidebar for the current path.
|
|
9
|
+
*
|
|
10
|
+
* This follows VitePress' default theme behavior: arrays are used as-is,
|
|
11
|
+
* multi-sidebars use the longest matching path prefix, and object entries can
|
|
12
|
+
* provide a shared `base` for child links.
|
|
13
|
+
*/
|
|
14
|
+
export function getSidebar(sidebar: PressTheme.Sidebar | undefined, path: string): PressTheme.SidebarEntry[] {
|
|
15
|
+
if (Array.isArray(sidebar))
|
|
16
|
+
return addBase(sidebar)
|
|
17
|
+
|
|
18
|
+
if (!sidebar)
|
|
19
|
+
return []
|
|
20
|
+
|
|
21
|
+
const normalizedPath = ensureStartingSlash(path)
|
|
22
|
+
const dir = Object.keys(sidebar)
|
|
23
|
+
.sort((a, b) => b.split('/').length - a.split('/').length)
|
|
24
|
+
.find(dir => normalizedPath.startsWith(ensureStartingSlash(dir)))
|
|
25
|
+
|
|
26
|
+
const matched = dir ? sidebar[dir] : []
|
|
27
|
+
|
|
28
|
+
return Array.isArray(matched)
|
|
29
|
+
? addBase(matched)
|
|
30
|
+
: addBase(matched.items, matched.base)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Convert resolved sidebar entries into renderable groups.
|
|
35
|
+
*
|
|
36
|
+
* Explicit groups with `items` are kept as groups. Consecutive top-level links
|
|
37
|
+
* or category names are grouped into an anonymous group, matching VitePress'
|
|
38
|
+
* sidebar rendering model.
|
|
39
|
+
*/
|
|
40
|
+
export function getSidebarGroups(sidebar: PressTheme.SidebarEntry[]): SidebarGroup[] {
|
|
41
|
+
const groups: SidebarGroup[] = []
|
|
42
|
+
let lastGroupIndex = 0
|
|
43
|
+
|
|
44
|
+
for (const item of sidebar) {
|
|
45
|
+
if (isSidebarItem(item) && item.items) {
|
|
46
|
+
groups.push({
|
|
47
|
+
...item,
|
|
48
|
+
items: item.items,
|
|
49
|
+
})
|
|
50
|
+
lastGroupIndex = groups.length
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!groups[lastGroupIndex])
|
|
55
|
+
groups.push({ items: [] })
|
|
56
|
+
|
|
57
|
+
groups[lastGroupIndex].items.push(item)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return groups
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function isSidebarItem(item: PressTheme.SidebarEntry): item is PressTheme.SidebarItem {
|
|
64
|
+
return typeof item !== 'string'
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function ensureStartingSlash(path: string): string {
|
|
68
|
+
return path.startsWith('/') ? path : `/${path}`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function addBase(items: PressTheme.SidebarEntry[], base?: string): PressTheme.SidebarEntry[] {
|
|
72
|
+
return items.map(item => addBaseToItem(item, base))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function addBaseToItem(item: PressTheme.SidebarEntry, base?: string): PressTheme.SidebarEntry {
|
|
76
|
+
if (!isSidebarItem(item))
|
|
77
|
+
return item
|
|
78
|
+
|
|
79
|
+
const result = { ...item }
|
|
80
|
+
const itemBase = result.base || base
|
|
81
|
+
|
|
82
|
+
if (itemBase && result.link)
|
|
83
|
+
result.link = itemBase + result.link.replace(/^\//, itemBase.endsWith('/') ? '' : '/')
|
|
84
|
+
|
|
85
|
+
if (result.items)
|
|
86
|
+
result.items = addBase(result.items, itemBase).filter(isSidebarItem)
|
|
87
|
+
|
|
88
|
+
return result
|
|
89
|
+
}
|