diff --git a/theme/src/client/features/components/NpmBadge.vue b/theme/src/client/features/components/NpmBadge.vue
new file mode 100644
index 00000000..98fcd98c
--- /dev/null
+++ b/theme/src/client/features/components/NpmBadge.vue
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/theme/src/client/features/components/NpmBadgeGroup.vue b/theme/src/client/features/components/NpmBadgeGroup.vue
new file mode 100644
index 00000000..ea684901
--- /dev/null
+++ b/theme/src/client/features/components/NpmBadgeGroup.vue
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/theme/src/client/features/composables/npm-badge.ts b/theme/src/client/features/composables/npm-badge.ts
new file mode 100644
index 00000000..919ed869
--- /dev/null
+++ b/theme/src/client/features/composables/npm-badge.ts
@@ -0,0 +1,175 @@
+import { computed, inject, provide, ref, toValue } from 'vue'
+import type { ComputedRef, InjectionKey, Ref } from 'vue'
+
+const DEFAULT_COLOR = '#32A9C3'
+const DEFAULT_LABEL_COLOR = '#1B3C4A'
+const BADGE_URL = 'https://img.shields.io'
+const GITHUB_URL = 'https://github.com'
+const NPM_URL = 'https://www.npmjs.com/package'
+
+export type NpmBadgeType =
+ // github
+ | 'source' // github source
+ | 'stars' // github stars
+ | 'forks' // github forks
+ | 'license' // github license
+ // npm
+ | 'version' // npm version
+ | 'dt' // alias d18m
+ | 'd18m' // npm downloads last 18 months
+ | 'dw' // npm downloads weekly
+ | 'dm' // npm downloads monthly
+ | 'dy' // npm downloads yearly
+
+export type NpmBadgeTheme = 'flat' | 'flat-square' | 'plastic' | 'for-the-badge' | 'social'
+
+export interface NpmBadgeBaseOptions {
+ // npm package name
+ name?: string
+ // github repo
+ repo?: string
+ // github branch
+ branch?: string
+ // github directory
+ dir?: string
+ // text color
+ color?: string
+ // label color
+ labelColor?: string
+ // badge style
+ theme?: NpmBadgeTheme
+}
+
+export interface NpmBadgeOptions extends NpmBadgeBaseOptions {
+ type: NpmBadgeType
+ // label text
+ label?: string
+}
+
+export interface NpmBadgeGroupOptions extends Omit {
+ items?: string | NpmBadgeType | NpmBadgeType[]
+}
+
+export interface NpmBadgeInfo {
+ badgeUrl: string
+ link?: string
+ alt?: string
+}
+
+type NpmBadgeBaseOptionsRef = Ref
+
+const NpmBadgeSymbol: InjectionKey = Symbol(__VUEPRESS_DEV__ ? 'NpmBadge' : '')
+
+export function useNpmBadge(opt: Ref): ComputedRef {
+ const parentOpt = inject(NpmBadgeSymbol, ref({}) as NpmBadgeBaseOptionsRef)
+
+ return computed(() => {
+ const po = toValue(parentOpt)
+ const o = toValue(opt)
+ const res: NpmBadgeOptions = {
+ name: o.name || po.name,
+ repo: o.repo || po.repo,
+ branch: o.branch || po.branch,
+ dir: o.dir || po.dir,
+ type: o.type,
+ color: o.color || po.color,
+ label: o.label,
+ labelColor: o.labelColor || po.labelColor,
+ theme: o.theme || po.theme,
+ }
+ return resolveNpmBadgeOptions(res)
+ })
+}
+
+export function useNpmBadgeGroup(opt: Ref) {
+ const baseOptions = computed(() => {
+ const o = toValue(opt)
+ return {
+ name: o.name,
+ repo: o.repo,
+ branch: o.branch,
+ dir: o.dir,
+ color: o.color,
+ labelColor: o.labelColor,
+ theme: o.theme,
+ }
+ })
+
+ provide(NpmBadgeSymbol, baseOptions)
+}
+
+function resolveNpmBadgeOptions(options: NpmBadgeOptions): NpmBadgeInfo {
+ let { name = '', repo = '', branch = 'main', dir = '', type, color, label, labelColor, theme = '' } = options
+ name = name || repo.split('/')?.[1] || ''
+ const normalizeName = encodeURIComponent(name)
+
+ const githubLink = repo ? `${GITHUB_URL}/${repo}${dir ? `/tree/${branch}/${dir}` : ''}` : ''
+ const npmLink = `${NPM_URL}/${name}`
+
+ const params = new URLSearchParams()
+
+ if (type !== 'source' && type !== 'stars' && type !== 'forks') {
+ params.append('style', theme || 'flat')
+ params.append('color', color || DEFAULT_COLOR)
+ params.append('labelColor', labelColor || DEFAULT_LABEL_COLOR)
+ }
+
+ switch (type) {
+ case 'source': {
+ params.append('logo', 'github')
+ params.append('color', labelColor || DEFAULT_LABEL_COLOR)
+ return {
+ badgeUrl: `${BADGE_URL}/badge/source-a?${params.toString()}`,
+ link: githubLink,
+ alt: 'github source',
+ }
+ }
+ case 'stars':
+ case 'forks': {
+ params.append('style', theme || 'social')
+ return {
+ badgeUrl: `${BADGE_URL}/github/${type}/${repo}?${params.toString()}`,
+ link: githubLink,
+ alt: `github ${type}`,
+ }
+ }
+ case 'license':
+ return {
+ badgeUrl: `${BADGE_URL}/github/license/${repo}?${params.toString()}`,
+ link: githubLink,
+ alt: 'license',
+ }
+ case 'version': {
+ params.append('label', label || name || 'npm')
+ return {
+ badgeUrl: `${BADGE_URL}/npm/v/${normalizeName}?${params.toString()}`,
+ link: npmLink,
+ alt: 'npm version',
+ }
+ }
+ case 'dt':
+ case 'd18m': {
+ params.append('label', label || 'downloads')
+ return {
+ badgeUrl: `${BADGE_URL}/npm/d18m/${normalizeName}?${params.toString()}`,
+ link: npmLink,
+ alt: 'npm downloads',
+ }
+ }
+ case 'dm':
+ case 'dy':
+ case 'dw': {
+ params.append('label', label || 'downloads')
+ return {
+ badgeUrl: `${BADGE_URL}/npm/${type}/${normalizeName}?${params.toString()}`,
+ link: npmLink,
+ alt: 'npm downloads',
+ }
+ }
+ default:
+ return {
+ badgeUrl: `${BADGE_URL}/badge/unknown?${params.toString()}`,
+ alt: 'unknown',
+ }
+ }
+}