@voidzero-dev/vitepress-theme 4.5.0 → 4.6.0
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/package.json +3 -2
- package/src/components/shared/CodeGroup.vue +3 -2
- package/src/components/vitepress-default/VPFlyout.vue +13 -4
- package/src/components/vitepress-default/VPMenuLink.vue +5 -0
- package/src/components/vitepress-default/VPNavBarMenuGroup.vue +1 -0
- package/src/components/vitepress-default/VPNavBarMenuLink.vue +6 -1
- package/src/components/vitepress-default/VPNavScreenMenu.vue +1 -0
- package/src/components/vitepress-default/VPNavScreenMenuGroup.vue +10 -1
- package/src/components/vitepress-default/VPNavScreenMenuGroupLink.vue +7 -1
- package/src/components/vitepress-default/VPNavScreenMenuLink.vue +7 -1
- package/src/components/vitepress-default/VPSidebarItem.vue +32 -2
- package/src/composables/vitepress-default/icon.ts +13 -0
- package/src/index.ts +1 -0
- package/src/types/theme-config.ts +18 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voidzero-dev/vitepress-theme",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.6.0",
|
|
4
4
|
"description": "Shared VitePress theme for VoidZero projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"mark.js": "8.11.1",
|
|
33
33
|
"minisearch": "^7.2.0",
|
|
34
34
|
"reka-ui": "^2.5.1",
|
|
35
|
-
"tailwindcss": "^4.1.18"
|
|
35
|
+
"tailwindcss": "^4.1.18",
|
|
36
|
+
"@iconify/vue": "^5.0.0"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
38
39
|
"@types/mark.js": "^8.11.12",
|
|
@@ -3,6 +3,7 @@ import { ref } from 'vue'
|
|
|
3
3
|
|
|
4
4
|
interface CodeTab {
|
|
5
5
|
label: string
|
|
6
|
+
prefix?: string
|
|
6
7
|
code: string
|
|
7
8
|
language?: string
|
|
8
9
|
}
|
|
@@ -37,7 +38,7 @@ const groupId = Math.random().toString(36).substring(7)
|
|
|
37
38
|
>
|
|
38
39
|
<button class="copy"></button>
|
|
39
40
|
<span class="lang">{{ tab.language || 'bash' }}</span>
|
|
40
|
-
<pre><code>{{ tab.code }}</code></pre>
|
|
41
|
+
<pre><code><span v-if="tab.prefix" style="user-select:none;-webkit-user-select:none;">{{ tab.prefix }}</span><span>{{ tab.code }}</span></code></pre>
|
|
41
42
|
</div>
|
|
42
43
|
</div>
|
|
43
44
|
</div>
|
|
@@ -49,4 +50,4 @@ code {
|
|
|
49
50
|
outline: none !important;
|
|
50
51
|
color: var(--vp-c-brand-1) !important;
|
|
51
52
|
}
|
|
52
|
-
</style>
|
|
53
|
+
</style>
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
<script lang="ts" setup generic="T extends DefaultTheme.NavItem">
|
|
2
2
|
import type { DefaultTheme } from 'vitepress/theme'
|
|
3
|
+
import type { IconOption } from '../../types/theme-config'
|
|
3
4
|
import { ref } from 'vue'
|
|
4
5
|
import { useFlyout } from '@vp-composables/flyout'
|
|
6
|
+
import { useIcon } from '@vp-composables/icon'
|
|
7
|
+
import { Icon } from '@iconify/vue'
|
|
5
8
|
import VPMenu from './VPMenu.vue'
|
|
6
9
|
|
|
7
|
-
defineProps<{
|
|
8
|
-
icon?:
|
|
10
|
+
const props = defineProps<{
|
|
11
|
+
icon?: IconOption
|
|
9
12
|
button?: string
|
|
10
13
|
label?: string
|
|
11
14
|
items?: T[]
|
|
12
15
|
}>()
|
|
13
16
|
|
|
17
|
+
const resolvedIcon = useIcon(() => props.icon)
|
|
18
|
+
|
|
14
19
|
const open = ref(false)
|
|
15
20
|
const el = ref<HTMLElement>()
|
|
16
21
|
|
|
@@ -36,8 +41,8 @@ function onBlur() {
|
|
|
36
41
|
:aria-label="label"
|
|
37
42
|
@click="open = !open"
|
|
38
43
|
>
|
|
39
|
-
<span v-if="button ||
|
|
40
|
-
<
|
|
44
|
+
<span v-if="button || resolvedIcon" class="flex items-center gap-1 leading-normal overflow-hidden">
|
|
45
|
+
<Icon v-if="resolvedIcon" :icon="resolvedIcon" class="shrink-0 mr-0.5" />
|
|
41
46
|
<span v-if="button" v-html="button" class="overflow-hidden text-ellipsis"></span>
|
|
42
47
|
<span class="vpi-chevron-down text-sm shrink-0" />
|
|
43
48
|
</span>
|
|
@@ -76,4 +81,8 @@ button[aria-expanded="true"] + .menu,
|
|
|
76
81
|
.VPFlyout:hover .menu {
|
|
77
82
|
display: block;
|
|
78
83
|
}
|
|
84
|
+
|
|
85
|
+
.VPFlyout.active button {
|
|
86
|
+
color: var(--vp-c-brand-1);
|
|
87
|
+
}
|
|
79
88
|
</style>
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import type { DefaultTheme } from 'vitepress/theme'
|
|
3
3
|
import { computed } from 'vue'
|
|
4
4
|
import { useData } from '@vp-composables/data'
|
|
5
|
+
import { useIcon } from '@vp-composables/icon'
|
|
5
6
|
import { isActive } from '../../support/vitepress-default/shared-utils'
|
|
7
|
+
import { Icon } from '@iconify/vue'
|
|
6
8
|
import VPLink from './VPLink.vue'
|
|
7
9
|
|
|
8
10
|
const props = defineProps<{
|
|
@@ -12,6 +14,8 @@ const props = defineProps<{
|
|
|
12
14
|
|
|
13
15
|
const { page } = useData()
|
|
14
16
|
|
|
17
|
+
const icon = useIcon(() => props.item.icon)
|
|
18
|
+
|
|
15
19
|
const href = computed(() =>
|
|
16
20
|
typeof props.item.link === 'function'
|
|
17
21
|
? props.item.link(page.value)
|
|
@@ -38,6 +42,7 @@ defineOptions({ inheritAttrs: false })
|
|
|
38
42
|
:rel="props.rel ?? item.rel"
|
|
39
43
|
:no-icon="item.noIcon"
|
|
40
44
|
>
|
|
45
|
+
<Icon v-if="icon" :icon="icon" class="shrink-0 mr-0.5" />
|
|
41
46
|
<span v-html="item.text"></span>
|
|
42
47
|
</VPLink>
|
|
43
48
|
</div>
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import type { DefaultTheme } from 'vitepress/theme'
|
|
3
3
|
import { computed } from 'vue'
|
|
4
4
|
import { useData } from '@vp-composables/data'
|
|
5
|
+
import { useIcon } from '@vp-composables/icon'
|
|
5
6
|
import { isActive } from '../../support/vitepress-default/shared-utils'
|
|
7
|
+
import { Icon } from '@iconify/vue'
|
|
6
8
|
import VPLink from './VPLink.vue'
|
|
7
9
|
|
|
8
10
|
const props = defineProps<{
|
|
@@ -11,6 +13,8 @@ const props = defineProps<{
|
|
|
11
13
|
|
|
12
14
|
const { page } = useData()
|
|
13
15
|
|
|
16
|
+
const icon = useIcon(() => props.item.icon)
|
|
17
|
+
|
|
14
18
|
const href = computed(() =>
|
|
15
19
|
typeof props.item.link === 'function'
|
|
16
20
|
? props.item.link(page.value)
|
|
@@ -20,7 +24,7 @@ const href = computed(() =>
|
|
|
20
24
|
|
|
21
25
|
<template>
|
|
22
26
|
<VPLink
|
|
23
|
-
class="flex items-center px-3 py-2 text-base font-heading text-primary
|
|
27
|
+
class="flex items-center gap-1 px-3 py-2 text-base font-heading text-primary
|
|
24
28
|
dark:text-white hover:opacity-85 transition-opacity whitespace-nowrap overflow-hidden text-ellipsis"
|
|
25
29
|
:class="{
|
|
26
30
|
active: isActive(
|
|
@@ -29,6 +33,7 @@ const href = computed(() =>
|
|
|
29
33
|
!!item.activeMatch
|
|
30
34
|
)
|
|
31
35
|
}" :href :target="item.target" :rel="item.rel" :no-icon="item.noIcon" tabindex="0">
|
|
36
|
+
<Icon v-if="icon" :icon="icon" class="shrink-0 mr-0.5" />
|
|
32
37
|
<span v-html="item.text"></span>
|
|
33
38
|
</VPLink>
|
|
34
39
|
</template>
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
+
import type { IconOption } from '../../types/theme-config'
|
|
2
3
|
import { computed, ref } from 'vue'
|
|
4
|
+
import { useIcon } from '@vp-composables/icon'
|
|
5
|
+
import { Icon } from '@iconify/vue'
|
|
3
6
|
import VPNavScreenMenuGroupLink from './VPNavScreenMenuGroupLink.vue'
|
|
4
7
|
import VPNavScreenMenuGroupSection from './VPNavScreenMenuGroupSection.vue'
|
|
5
8
|
|
|
6
9
|
const props = defineProps<{
|
|
7
10
|
text: string
|
|
11
|
+
icon?: IconOption
|
|
8
12
|
items: any[]
|
|
9
13
|
}>()
|
|
10
14
|
|
|
15
|
+
const resolvedIcon = useIcon(() => props.icon)
|
|
16
|
+
|
|
11
17
|
const isOpen = ref(false)
|
|
12
18
|
|
|
13
19
|
const groupId = computed(
|
|
@@ -27,7 +33,10 @@ function toggle() {
|
|
|
27
33
|
:aria-expanded="isOpen"
|
|
28
34
|
@click="toggle"
|
|
29
35
|
>
|
|
30
|
-
<span class="
|
|
36
|
+
<span class="flex items-center">
|
|
37
|
+
<Icon v-if="resolvedIcon" :icon="resolvedIcon" class="shrink-0 mr-0.5" />
|
|
38
|
+
<span class="button-text" v-html="text"></span>
|
|
39
|
+
</span>
|
|
31
40
|
<span class="vpi-plus button-icon" />
|
|
32
41
|
</button>
|
|
33
42
|
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import type { DefaultTheme } from 'vitepress/theme'
|
|
3
3
|
import { computed, inject } from 'vue'
|
|
4
4
|
import { useData } from '@vp-composables/data'
|
|
5
|
+
import { useIcon } from '@vp-composables/icon'
|
|
5
6
|
import { navInjectionKey } from '@vp-composables/nav'
|
|
7
|
+
import { Icon } from '@iconify/vue'
|
|
6
8
|
import VPLink from './VPLink.vue'
|
|
7
9
|
|
|
8
10
|
const props = defineProps<{
|
|
@@ -11,6 +13,8 @@ const props = defineProps<{
|
|
|
11
13
|
|
|
12
14
|
const { page } = useData()
|
|
13
15
|
|
|
16
|
+
const icon = useIcon(() => props.item.icon)
|
|
17
|
+
|
|
14
18
|
const href = computed(() =>
|
|
15
19
|
typeof props.item.link === 'function'
|
|
16
20
|
? props.item.link(page.value)
|
|
@@ -29,13 +33,15 @@ const { closeScreen } = inject(navInjectionKey)!
|
|
|
29
33
|
:no-icon="item.noIcon"
|
|
30
34
|
@click="closeScreen"
|
|
31
35
|
>
|
|
36
|
+
<Icon v-if="icon" :icon="icon" class="shrink-0 mr-0.5" />
|
|
32
37
|
<span v-html="item.text"></span>
|
|
33
38
|
</VPLink>
|
|
34
39
|
</template>
|
|
35
40
|
|
|
36
41
|
<style scoped>
|
|
37
42
|
.VPNavScreenMenuGroupLink {
|
|
38
|
-
display:
|
|
43
|
+
display: flex;
|
|
44
|
+
align-items: center;
|
|
39
45
|
margin-left: 12px;
|
|
40
46
|
line-height: 32px;
|
|
41
47
|
font-size: 14px;
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import type { DefaultTheme } from 'vitepress/theme'
|
|
3
3
|
import { computed, inject } from 'vue'
|
|
4
4
|
import { useData } from '@vp-composables/data'
|
|
5
|
+
import { useIcon } from '@vp-composables/icon'
|
|
5
6
|
import { navInjectionKey } from '@vp-composables/nav'
|
|
7
|
+
import { Icon } from '@iconify/vue'
|
|
6
8
|
import VPLink from './VPLink.vue'
|
|
7
9
|
|
|
8
10
|
const props = defineProps<{
|
|
@@ -11,6 +13,8 @@ const props = defineProps<{
|
|
|
11
13
|
|
|
12
14
|
const { page } = useData()
|
|
13
15
|
|
|
16
|
+
const icon = useIcon(() => props.item.icon)
|
|
17
|
+
|
|
14
18
|
const href = computed(() =>
|
|
15
19
|
typeof props.item.link === 'function'
|
|
16
20
|
? props.item.link(page.value)
|
|
@@ -29,13 +33,15 @@ const { closeScreen } = inject(navInjectionKey)!
|
|
|
29
33
|
:no-icon="item.noIcon"
|
|
30
34
|
@click="closeScreen"
|
|
31
35
|
>
|
|
36
|
+
<Icon v-if="icon" :icon="icon" class="shrink-0 mr-0.5" />
|
|
32
37
|
<span v-html="item.text"></span>
|
|
33
38
|
</VPLink>
|
|
34
39
|
</template>
|
|
35
40
|
|
|
36
41
|
<style scoped>
|
|
37
42
|
.VPNavScreenMenuLink {
|
|
38
|
-
display:
|
|
43
|
+
display: flex;
|
|
44
|
+
align-items: center;
|
|
39
45
|
border-bottom: 1px solid var(--vp-c-divider);
|
|
40
46
|
padding: 12px 0 11px;
|
|
41
47
|
line-height: 24px;
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import type { DefaultTheme } from 'vitepress/theme'
|
|
3
3
|
import { computed } from 'vue'
|
|
4
4
|
import { useSidebarItemControl } from '@vp-composables/sidebar'
|
|
5
|
+
import { useIcon } from '@vp-composables/icon'
|
|
6
|
+
import { Icon } from '@iconify/vue'
|
|
5
7
|
import VPLink from './VPLink.vue'
|
|
6
8
|
|
|
7
9
|
const props = defineProps<{
|
|
@@ -52,10 +54,12 @@ function onItemInteraction(e: MouseEvent | Event) {
|
|
|
52
54
|
function onCaretClick() {
|
|
53
55
|
props.item.link && toggle()
|
|
54
56
|
}
|
|
57
|
+
|
|
58
|
+
const itemIcon = useIcon(() => (props.item as any).icon)
|
|
55
59
|
</script>
|
|
56
60
|
|
|
57
61
|
<template>
|
|
58
|
-
<component :is="sectionTag" class="VPSidebarItem" :class="classes">
|
|
62
|
+
<component :is="sectionTag" class="VPSidebarItem" :class="[classes, { 'has-icon': itemIcon }]">
|
|
59
63
|
<div
|
|
60
64
|
v-if="item.text"
|
|
61
65
|
class="item"
|
|
@@ -77,9 +81,13 @@ function onCaretClick() {
|
|
|
77
81
|
:rel="item.rel"
|
|
78
82
|
:target="item.target"
|
|
79
83
|
>
|
|
84
|
+
<Icon v-if="itemIcon" :icon="itemIcon" class="sidebar-icon shrink-0 mr-0.5" />
|
|
80
85
|
<component :is="textTag" class="text" v-html="item.text" />
|
|
81
86
|
</VPLink>
|
|
82
|
-
<
|
|
87
|
+
<template v-else>
|
|
88
|
+
<Icon v-if="itemIcon" :icon="itemIcon" class="sidebar-icon shrink-0 mr-0.5" />
|
|
89
|
+
<component :is="textTag" class="text" v-html="item.text" />
|
|
90
|
+
</template>
|
|
83
91
|
|
|
84
92
|
<div
|
|
85
93
|
v-if="item.collapsed != null && item.items && item.items.length"
|
|
@@ -112,6 +120,8 @@ function onCaretClick() {
|
|
|
112
120
|
.item {
|
|
113
121
|
position: relative;
|
|
114
122
|
display: flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
gap: 6px;
|
|
115
125
|
width: 100%;
|
|
116
126
|
}
|
|
117
127
|
|
|
@@ -139,6 +149,7 @@ function onCaretClick() {
|
|
|
139
149
|
.link {
|
|
140
150
|
display: flex;
|
|
141
151
|
align-items: center;
|
|
152
|
+
gap: 6px;
|
|
142
153
|
flex-grow: 1;
|
|
143
154
|
}
|
|
144
155
|
|
|
@@ -229,6 +240,7 @@ function onCaretClick() {
|
|
|
229
240
|
transform: rotate(0)/*rtl:rotate(180deg)*/;
|
|
230
241
|
}
|
|
231
242
|
|
|
243
|
+
.VPSidebarItem.level-0.has-icon > .items,
|
|
232
244
|
.VPSidebarItem.level-1 .items,
|
|
233
245
|
.VPSidebarItem.level-2 .items,
|
|
234
246
|
.VPSidebarItem.level-3 .items,
|
|
@@ -241,4 +253,22 @@ function onCaretClick() {
|
|
|
241
253
|
.VPSidebarItem.collapsed .items {
|
|
242
254
|
display: none;
|
|
243
255
|
}
|
|
256
|
+
|
|
257
|
+
.sidebar-icon {
|
|
258
|
+
color: var(--vp-c-brand-1);
|
|
259
|
+
height: 1.2em;
|
|
260
|
+
width: 1.2em;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* Child-level icons (framework logos etc.) use text color instead of brand.
|
|
264
|
+
Multi-color icons have explicit SVG fills and are unaffected. */
|
|
265
|
+
.VPSidebarItem:not(.level-0) > .item .sidebar-icon {
|
|
266
|
+
color: var(--vp-c-text-1);
|
|
267
|
+
height: 1em;
|
|
268
|
+
width: 1em;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.VPSidebarItem.level-0.has-icon > .items {
|
|
272
|
+
margin-left: 6px;
|
|
273
|
+
}
|
|
244
274
|
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { computed, type MaybeRefOrGetter, toValue } from 'vue'
|
|
2
|
+
import { useData } from './data'
|
|
3
|
+
import type { IconOption } from '../../types/theme-config'
|
|
4
|
+
|
|
5
|
+
export function useIcon(icon: MaybeRefOrGetter<IconOption | undefined>) {
|
|
6
|
+
const { isDark } = useData()
|
|
7
|
+
return computed(() => {
|
|
8
|
+
const val = toValue(icon)
|
|
9
|
+
if (!val) return undefined
|
|
10
|
+
if (typeof val === 'string') return val
|
|
11
|
+
return isDark.value ? val.dark : val.light
|
|
12
|
+
})
|
|
13
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
import type { DefaultTheme } from 'vitepress/theme'
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Icon option — a single string for both modes, or per-mode variants
|
|
9
|
+
*/
|
|
10
|
+
export type IconOption = string | { light: string; dark: string }
|
|
11
|
+
|
|
7
12
|
/**
|
|
8
13
|
* Grid size options for sponsor display
|
|
9
14
|
*/
|
|
@@ -98,5 +103,18 @@ declare module 'vitepress' {
|
|
|
98
103
|
variant?: 'voidzero' | 'viteplus' | 'vite' | 'vitest' | 'rolldown' | 'oxc'
|
|
99
104
|
banner?: BannerConfig
|
|
100
105
|
}
|
|
106
|
+
|
|
107
|
+
// Add icon support to nav and sidebar items
|
|
108
|
+
interface NavItemWithLink {
|
|
109
|
+
icon?: IconOption
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface NavItemWithChildren {
|
|
113
|
+
icon?: IconOption
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Note: SidebarItem is a type alias (not interface) so it can't be
|
|
117
|
+
// augmented. Sidebar icon support works via runtime casting in
|
|
118
|
+
// VPSidebarItem.vue — the icon property is passed through config objects.
|
|
101
119
|
}
|
|
102
120
|
}
|