Skip to content

Commit 8101797

Browse files
committed
feat: create parameter badge generator
1 parent 3907e95 commit 8101797

File tree

3 files changed

+194
-6
lines changed

3 files changed

+194
-6
lines changed

docs/app/components/BadgeGenerator.vue

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const copyToClipboard = async () => {
4545

4646
<template>
4747
<div
48-
class="my-8 p-5 rounded-xl border border-gray-200/60 dark:border-white/5 bg-gray-50/50 dark:bg-white/[0.02] flex flex-col sm:flex-row items-end gap-4"
48+
class="my-8 p-5 rounded-xl border border-gray-200/60 dark:border-white/5 bg-gray-50/50 dark:bg-white/2 flex flex-col sm:flex-row items-end gap-4"
4949
>
5050
<div class="flex flex-col gap-1.5 flex-1 w-full">
5151
<label class="text-[11px] font-bold uppercase tracking-wider text-gray-400 ml-1"
@@ -55,7 +55,7 @@ const copyToClipboard = async () => {
5555
v-model="pkg"
5656
type="text"
5757
spellcheck="false"
58-
class="w-full h-[42px] px-4 py-2 rounded-lg border border-gray-200 dark:border-white/10 bg-white dark:bg-black/20 focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500 outline-none text-sm transition-all"
58+
class="w-full h-10.5 px-4 py-2 rounded-lg border border-gray-200 dark:border-white/10 bg-white dark:bg-black/20 focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500 outline-none text-sm transition-all"
5959
:class="{ 'border-red-500/50 focus:ring-red-500/10 focus:border-red-500': !isValid }"
6060
placeholder="e.g. nuxt"
6161
/>
@@ -68,7 +68,7 @@ const copyToClipboard = async () => {
6868
<div class="relative">
6969
<select
7070
v-model="type"
71-
class="w-full h-[42px] px-4 py-2 rounded-lg border border-gray-200 dark:border-white/10 bg-white dark:bg-black/20 focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500 outline-none text-sm transition-all appearance-none cursor-pointer"
71+
class="w-full h-10.5 px-4 py-2 rounded-lg border border-gray-200 dark:border-white/10 bg-white dark:bg-black/20 focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500 outline-none text-sm transition-all appearance-none cursor-pointer"
7272
>
7373
<option v-for="t in types" :key="t" :value="t" class="dark:bg-gray-900">{{ t }}</option>
7474
</select>
@@ -83,15 +83,15 @@ const copyToClipboard = async () => {
8383
>Preview & Action</label
8484
>
8585
<div
86-
class="flex items-center bg-white dark:bg-black/20 border border-gray-200 dark:border-white/10 rounded-lg h-[42px] overflow-hidden"
86+
class="flex items-center bg-white dark:bg-black/20 border border-gray-200 dark:border-white/10 rounded-lg h-10.5 overflow-hidden"
8787
>
8888
<div
8989
class="flex-1 flex items-center justify-center px-3 border-r border-gray-200 dark:border-white/10 h-full bg-gray-50/50 dark:bg-transparent"
9090
>
9191
<img
9292
v-if="isValid"
9393
:src="`https://npmx.dev/api/registry/badge/${type}/${pkg}`"
94-
class="h-[20px]"
94+
class="h-5"
9595
alt="Badge Preview"
9696
@error="isValid = false"
9797
/>
@@ -103,7 +103,7 @@ const copyToClipboard = async () => {
103103
<button
104104
@click="copyToClipboard"
105105
:disabled="!isValid"
106-
class="px-4 h-full text-[11px] font-bold uppercase tracking-widest transition-all disabled:opacity-20 disabled:cursor-not-allowed min-w-[85px] hover:bg-gray-50 dark:hover:bg-white/5"
106+
class="px-4 h-full text-[11px] font-bold uppercase tracking-widest transition-all disabled:opacity-20 disabled:cursor-not-allowed min-w-21.25 hover:bg-gray-50 dark:hover:bg-white/5"
107107
:class="
108108
copied
109109
? 'text-emerald-500 bg-emerald-50/50 dark:bg-emerald-500/10'
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
<script setup>
2+
const pkg = useState('badge-pkg', () => 'nuxt')
3+
const type = useState('badge-type', () => 'version')
4+
const isValid = ref(true)
5+
const copied = ref(false)
6+
7+
const labelColor = useState('badge-label-color', () => '')
8+
const labelText = useState('badge-label-text', () => '')
9+
const badgeColor = useState('badge-color', () => '')
10+
const usePkgName = useState('badge-use-name', () => false)
11+
const badgeStyle = useState('badge-style', () => 'default')
12+
13+
const types = [
14+
'version', 'license', 'size', 'downloads', 'downloads-day',
15+
'downloads-week', 'downloads-month', 'downloads-year',
16+
'vulnerabilities', 'dependencies', 'created', 'updated',
17+
'engines', 'types', 'maintainers', 'deprecated',
18+
'quality', 'popularity', 'maintenance', 'score', 'name',
19+
]
20+
21+
const styles = ['default', 'shieldsio']
22+
23+
const validateHex = (hex) => {
24+
if (!hex) return true
25+
const clean = hex.replace('#', '')
26+
return /^[0-9A-F]{3}$/i.test(clean) || /^[0-9A-F]{6}$/i.test(clean)
27+
}
28+
29+
const isLabelHexValid = computed(() => validateHex(labelColor.value))
30+
const isBadgeHexValid = computed(() => validateHex(badgeColor.value))
31+
const isInputValid = computed(() => isLabelHexValid.value && isBadgeHexValid.value && pkg.value.length > 0)
32+
33+
const cleanHex = (hex) => hex?.replace('#', '') || ''
34+
35+
const queryParams = computed(() => {
36+
if (!isInputValid.value) return ''
37+
const params = new URLSearchParams()
38+
39+
if (labelColor.value) params.append('labelColor', cleanHex(labelColor.value))
40+
if (badgeColor.value) params.append('color', cleanHex(badgeColor.value))
41+
if (badgeStyle.value !== 'default') params.append('style', badgeStyle.value)
42+
43+
if (usePkgName.value) {
44+
params.append('name', 'true')
45+
} else if (labelText.value) {
46+
params.append('label', labelText.value)
47+
}
48+
49+
const queryString = params.toString()
50+
return queryString ? `?${queryString}` : ''
51+
})
52+
53+
const badgeUrl = computed(() => {
54+
if (!isInputValid.value) return ''
55+
return `https://npmx.dev/api/registry/badge/${type.value}/${pkg.value}${queryParams.value}`
56+
})
57+
58+
watch([pkg, type, queryParams], () => {
59+
isValid.value = true
60+
})
61+
62+
const copyToClipboard = async () => {
63+
const markdown = `[![Open on npmx.dev](${badgeUrl.value})](https://npmx.dev/package/${pkg.value})`
64+
await navigator.clipboard.writeText(markdown)
65+
66+
copied.value = true
67+
setTimeout(() => {
68+
copied.value = false
69+
}, 2000)
70+
}
71+
</script>
72+
73+
<template>
74+
<div class="my-8 p-5 rounded-xl border border-gray-200/60 dark:border-white/5 bg-gray-50/50 dark:bg-white/2 flex flex-col gap-6">
75+
76+
<div class="flex flex-col lg:flex-row items-end gap-4">
77+
<div class="flex flex-col gap-1.5 flex-1 w-full">
78+
<label class="text-[11px] font-bold uppercase tracking-wider text-gray-400 ml-1">Package Name</label>
79+
<input
80+
v-model="pkg"
81+
type="text"
82+
placeholder="e.g. nuxt"
83+
class="w-full h-10.5 px-4 py-2 rounded-lg border border-gray-200 dark:border-white/10 bg-white dark:bg-black/20 focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500 outline-none text-sm transition-all"
84+
/>
85+
</div>
86+
87+
<div class="flex flex-col gap-1.5 flex-1 w-full">
88+
<label class="text-[11px] font-bold uppercase tracking-wider text-gray-400 ml-1">Badge Type</label>
89+
<div class="relative">
90+
<select v-model="type" class="w-full h-10.5 px-4 py-2 rounded-lg border border-gray-200 dark:border-white/10 bg-white dark:bg-black/20 focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500 outline-none text-sm transition-all appearance-none cursor-pointer">
91+
<option v-for="t in types" :key="t" :value="t" class="dark:bg-gray-900">{{ t }}</option>
92+
</select>
93+
<span class="absolute right-3 top-1/2 -translate-y-1/2 i-lucide-chevron-down w-4 h-4 text-gray-400 pointer-events-none" />
94+
</div>
95+
</div>
96+
97+
<div class="flex flex-col gap-1.5 flex-2 w-full">
98+
<label class="text-[11px] font-bold uppercase tracking-wider text-gray-400 ml-1">Preview & Action</label>
99+
<div class="flex items-center bg-white dark:bg-black/20 border border-gray-200 dark:border-white/10 rounded-lg h-10.5 overflow-hidden">
100+
<div class="flex-1 flex items-center justify-center px-3 border-r border-gray-200 dark:border-white/10 h-full bg-gray-50/10 dark:bg-transparent">
101+
<img
102+
v-if="isValid && isInputValid"
103+
:src="badgeUrl"
104+
class="h-5"
105+
alt="Badge Preview"
106+
@error="isValid = false"
107+
/>
108+
<span v-else class="text-[10px] font-bold text-red-500 uppercase tracking-tighter">
109+
{{ !isInputValid ? 'Invalid Parameters' : 'Not Found' }}
110+
</span>
111+
</div>
112+
<button
113+
@click="copyToClipboard"
114+
:disabled="!isValid || !isInputValid"
115+
class="px-6 h-full text-[11px] font-bold uppercase tracking-widest transition-all disabled:opacity-20 disabled:cursor-not-allowed min-w-24 hover:bg-gray-50 dark:hover:bg-white/5"
116+
:class="copied ? 'text-emerald-500 bg-emerald-50/50 dark:bg-emerald-500/10' : 'text-gray-500 dark:text-gray-400'"
117+
>
118+
{{ copied ? 'Done!' : 'Copy' }}
119+
</button>
120+
</div>
121+
</div>
122+
</div>
123+
124+
<div class="h-px bg-gray-200 dark:bg-white/5 w-full" />
125+
126+
<div class="grid grid-cols-1 sm:grid-cols-3 gap-6">
127+
128+
<div class="flex flex-col gap-1.5">
129+
<div class="flex items-center justify-between min-h-3.75">
130+
<label class="text-[10px] font-bold uppercase text-gray-400 ml-1">Label Text</label>
131+
<span v-if="usePkgName" class="text-[9px] text-emerald-500 font-medium transition-opacity">Auto-Name Enabled</span>
132+
</div>
133+
<input
134+
v-model="labelText"
135+
:disabled="usePkgName"
136+
type="text"
137+
placeholder="Custom Label"
138+
class="px-3 py-2 h-9 rounded-lg border border-gray-200 dark:border-white/10 bg-white dark:bg-black/20 text-xs outline-none focus:border-emerald-500 disabled:cursor-not-allowed transition-all"
139+
:class="{ 'opacity-30 grayscale': usePkgName }"
140+
/>
141+
</div>
142+
143+
<div class="flex flex-col gap-1.5">
144+
<label class="text-[10px] font-bold uppercase text-gray-400 ml-1">Label Color</label>
145+
<div
146+
class="flex items-center px-3 rounded-lg border bg-white dark:bg-black/20 transition-all"
147+
:class="isLabelHexValid ? 'border-gray-200 dark:border-white/10 focus-within:border-emerald-500' : 'border-red-500 ring-1 ring-red-500/20'"
148+
>
149+
<span class="text-gray-400 text-xs font-mono mr-1">#</span>
150+
<input v-model="labelColor" type="text" placeholder="0a0a0a" class="w-full py-2 bg-transparent text-xs outline-none" />
151+
<span v-if="!isLabelHexValid" class="i-lucide-alert-circle w-3.5 h-3.5 text-red-500 ml-1" />
152+
</div>
153+
</div>
154+
155+
<div class="flex flex-col gap-1.5">
156+
<label class="text-[10px] font-bold uppercase text-gray-400 ml-1">Badge Color</label>
157+
<div
158+
class="flex items-center px-3 rounded-lg border bg-white dark:bg-black/20 transition-all"
159+
:class="isBadgeHexValid ? 'border-gray-200 dark:border-white/10 focus-within:border-emerald-500' : 'border-red-500 ring-1 ring-red-500/20'"
160+
>
161+
<span class="text-gray-400 text-xs font-mono mr-1">#</span>
162+
<input v-model="badgeColor" type="text" placeholder="ff69b4" class="w-full py-2 bg-transparent text-xs outline-none" />
163+
<span v-if="!isBadgeHexValid" class="i-lucide-alert-circle w-3.5 h-3.5 text-red-500 ml-1" />
164+
</div>
165+
</div>
166+
</div>
167+
168+
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 pt-4 border-t border-gray-200/50 dark:border-white/5">
169+
<label class="relative inline-flex items-center cursor-pointer group">
170+
<input v-model="usePkgName" type="checkbox" class="sr-only peer">
171+
<div class="w-9 h-5 bg-gray-200 peer-focus:outline-none dark:bg-white/10 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-0.5 after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-emerald-500 rounded-full"></div>
172+
<span class="ml-3 text-[10px] font-bold uppercase text-gray-400 group-hover:text-gray-600 dark:group-hover:text-gray-300 transition-colors">Use package name as label</span>
173+
</label>
174+
175+
<div class="flex items-center gap-3">
176+
<label class="text-[10px] font-bold uppercase text-gray-400 whitespace-nowrap">Badge Style</label>
177+
<select v-model="badgeStyle" class="min-w-30 px-3 py-1.5 rounded-lg border border-gray-200 dark:border-white/10 bg-white dark:bg-black/20 text-xs outline-none cursor-pointer hover:border-emerald-500 transition-colors">
178+
<option v-for="s in styles" :key="s" :value="s">{{ s }}</option>
179+
</select>
180+
</div>
181+
</div>
182+
183+
</div>
184+
</template>

docs/content/2.guide/6.badges.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ npmx.dev offers many different SVG badges with stats about any package via its A
6565

6666
You can further customize your badges by appending query parameters to the badge URL.
6767

68+
Use this generator to get the Markdown code you desire:
69+
70+
:badge-generator-parameters
71+
6872
### `labelColor`
6973

7074
Overrides the default label color. You can pass a standard hex code (with or without the `#` prefix). The label text color is automatically chosen (black or white) based on WCAG contrast ratio, so the badge remains readable.

0 commit comments

Comments
 (0)