@voidzero-dev/vitepress-theme 4.5.1 → 4.7.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/README.md CHANGED
@@ -4,14 +4,11 @@ Shared VitePress theme for VoidZero projects, including Vite+, Vite, Vitest, Rol
4
4
 
5
5
  ## Publishing package
6
6
 
7
- Make to sure to in latest commit
8
-
9
- 1. Update version in `package.json` and commit it (don't push yet)
10
- 2. Add tag locally `git tag vx.y.z` (matching version in package.json)
11
- 3. Push changes `git push origin`
12
- 4. Push to remote `git push origin vx.y.z`
13
- 5. Go to GitHub UI > "Releases" > "Tags" > Click the latest tag > "Create release from tag"
14
- 6. Click "Publish release"
7
+ ```bash
8
+ pnpm release <version> # e.g. pnpm release 4.6.1
9
+ ```
10
+
11
+ This updates `package.json`, commits, tags, and pushes. CI publishes to npm and creates the GitHub release.
15
12
 
16
13
  ## Developing locally
17
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voidzero-dev/vitepress-theme",
3
- "version": "4.5.1",
3
+ "version": "4.7.0",
4
4
  "description": "Shared VitePress theme for VoidZero projects",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -32,10 +32,14 @@
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",
39
40
  "typescript": "^5.9.3"
41
+ },
42
+ "scripts": {
43
+ "release": "node scripts/release.ts"
40
44
  }
41
45
  }
@@ -1,4 +1,5 @@
1
1
  <script setup>
2
+ import { Icon } from '@iconify/vue'
2
3
  import { useData } from 'vitepress'
3
4
 
4
5
  const { frontmatter } = useData()
@@ -7,11 +8,12 @@ const { frontmatter } = useData()
7
8
  <template>
8
9
  <section v-if="frontmatter.features"
9
10
  class="wrapper wrapper--ticks border-t grid md:grid-cols-2 divide-x divide-y divide-nickel">
10
- <div v-for="feature in frontmatter.features" :key="feature.icon"
11
+ <div v-for="feature in frontmatter.features" :key="feature.icon || feature.iconify"
11
12
  class="flex flex-col gap-3 justify-between border-r-0 md:odd:border-r md:nth-last-2:border-b-0">
12
13
  <div class="px-10 py-8 sm:px-15 sm:py-12 flex flex-col gap-3">
13
14
  <p class="text-2xl">
14
- {{ feature.icon }}
15
+ <Icon v-if="feature.iconify" :icon="feature.iconify" width="1em" height="1em" />
16
+ <template v-else>{{ feature.icon }}</template>
15
17
  </p>
16
18
  <h5 class="text-balance sm:text-pretty text-white">
17
19
  {{ feature.title }}
@@ -1,6 +1,10 @@
1
- <script setup>
1
+ <script setup lang="ts">
2
2
  import { useData } from 'vitepress'
3
3
 
4
+ const props = defineProps<{
5
+ background?: string
6
+ }>()
7
+
4
8
  const { frontmatter } = useData()
5
9
  </script>
6
10
 
@@ -27,9 +31,11 @@ const { frontmatter } = useData()
27
31
  </div>
28
32
  </div>
29
33
  </div>
30
- <div class="flex flex-col">
34
+ <div class="flex flex-col bg-cover bg-center" :style="props.background ? { backgroundImage: `url(${props.background})` } : undefined">
31
35
  <div class="relative px-10 pb-10 md:pt-10 h-full flex flex-col justify-center overflow-clip">
32
- <img :src="frontmatter.hero.image.src" :alt="frontmatter.hero.image.alt" class="max-h-[20rem]">
36
+ <slot name="hero-image">
37
+ <img v-if="frontmatter.hero.image?.src" :src="frontmatter.hero.image.src" :alt="frontmatter.hero.image.alt" class="max-h-[20rem]">
38
+ </slot>
33
39
  </div>
34
40
  </div>
35
41
  </div>
@@ -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?: string
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 || icon" class="flex items-center gap-1 leading-normal overflow-hidden">
40
- <span v-if="icon" :class="icon" class="shrink-0" />
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>
@@ -36,6 +36,7 @@ const childrenActive = computed(() => isChildActive(props.item))
36
36
  isActive(page.relativePath, item.activeMatch, !!item.activeMatch) ||
37
37
  childrenActive
38
38
  }"
39
+ :icon="item.icon"
39
40
  :button="item.text"
40
41
  :items="item.items"
41
42
  />
@@ -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>
@@ -19,6 +19,7 @@ const { theme } = useData()
19
19
  <VPNavScreenMenuGroup
20
20
  v-else
21
21
  :text="item.text || ''"
22
+ :icon="item.icon"
22
23
  :items="item.items"
23
24
  />
24
25
  </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="button-text" v-html="text"></span>
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: block;
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: block;
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
- <component v-else :is="textTag" class="text" v-html="item.text" />
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
@@ -94,6 +94,7 @@ export type {
94
94
  FooterLink,
95
95
  FooterSocialLink,
96
96
  GridSize,
97
+ IconOption,
97
98
  Sponsor,
98
99
  SponsorTier,
99
100
  VoidZeroThemeConfig,
@@ -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
  }