Skip to content

Commit 2501b3c

Browse files
trueberrylessFelix Schneiderautofix-ci[bot]CodeRabbitghostdevv
authored
feat(docs): add custom badge generator (#2152)
Co-authored-by: Felix Schneider <Schneider.Felix.SE@fronius.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: CodeRabbit <132028505+coderabbitai@users.noreply.github.com> Co-authored-by: Willow (GHOST) <git@willow.sh>
1 parent 7c1cea8 commit 2501b3c

File tree

9 files changed

+540
-117
lines changed

9 files changed

+540
-117
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<script lang="ts" setup>
2+
import { useClipboard } from '@vueuse/core'
3+
4+
const pkg = useState('badge-pkg', () => 'nuxt')
5+
const type = useState<BadgeType>('badge-type', () => 'version')
6+
const isValid = ref(true)
7+
8+
const { copy, copied } = useClipboard({ copiedDuring: 2000 })
9+
10+
watch([pkg, type], () => {
11+
isValid.value = true
12+
})
13+
14+
const copyToClipboard = async () => {
15+
const markdown = `[![Open on npmx.dev](https://npmx.dev/api/registry/badge/${type.value}/${pkg.value})](https://npmx.dev/package/${pkg.value})`
16+
copy(markdown)
17+
}
18+
</script>
19+
20+
<template>
21+
<div
22+
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"
23+
>
24+
<div class="flex flex-col gap-1.5 flex-1 w-full">
25+
<label class="text-[11px] font-bold uppercase tracking-wider text-gray-400 ml-1"
26+
>Package Name</label
27+
>
28+
<input
29+
v-model="pkg"
30+
type="text"
31+
spellcheck="false"
32+
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"
33+
:class="{ 'border-red-500/50 focus:ring-red-500/10 focus:border-red-500': !isValid }"
34+
placeholder="e.g. nuxt"
35+
/>
36+
</div>
37+
38+
<div class="flex flex-col gap-1.5 flex-1 w-full">
39+
<label class="text-[11px] font-bold uppercase tracking-wider text-gray-400 ml-1"
40+
>Badge Type</label
41+
>
42+
<div class="relative">
43+
<select
44+
v-model="type"
45+
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"
46+
>
47+
<option v-for="t in BADGE_TYPES" :key="t" :value="t" class="dark:bg-gray-900">
48+
{{ titleCase(t) }}
49+
</option>
50+
</select>
51+
<span
52+
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"
53+
/>
54+
</div>
55+
</div>
56+
57+
<div class="flex flex-col gap-1.5 flex-2 w-full">
58+
<label class="text-[11px] font-bold uppercase tracking-wider text-gray-400 ml-1"
59+
>Preview & Action</label
60+
>
61+
<div
62+
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"
63+
>
64+
<div
65+
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"
66+
>
67+
<img
68+
v-if="isValid"
69+
:src="`https://npmx.dev/api/registry/badge/${type}/${pkg}`"
70+
class="h-5"
71+
alt="Badge Preview"
72+
@error="isValid = false"
73+
/>
74+
<span v-else class="text-[10px] font-bold text-red-500 uppercase tracking-tighter"
75+
>Invalid</span
76+
>
77+
</div>
78+
79+
<button
80+
@click="copyToClipboard"
81+
:disabled="!isValid || !pkg"
82+
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"
83+
:class="
84+
copied
85+
? 'text-emerald-500 bg-emerald-50/50 dark:bg-emerald-500/10'
86+
: 'text-gray-500 dark:text-gray-400'
87+
"
88+
>
89+
{{ copied ? 'Done!' : 'Copy' }}
90+
</button>
91+
</div>
92+
</div>
93+
</div>
94+
</template>
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
<script lang="ts" setup>
2+
import { useClipboard } from '@vueuse/core'
3+
4+
const pkg = useState('badge-pkg', () => 'nuxt')
5+
const type = useState<BadgeType>('badge-type', () => 'version')
6+
const isValid = ref(true)
7+
8+
const { copy, copied } = useClipboard({ copiedDuring: 2000 })
9+
10+
const labelColor = useState('badge-label-color', () => '')
11+
const labelText = useState('badge-label-text', () => '')
12+
const badgeValue = useState('badge-value', () => '')
13+
const badgeColor = useState('badge-color', () => '')
14+
const usePkgName = useState('badge-use-name', () => false)
15+
const badgeStyle = useState('badge-style', () => 'default')
16+
17+
const styles = ['default', 'shieldsio']
18+
19+
const validateHex = (hex: string) => {
20+
if (!hex) return true
21+
const clean = hex.replace('#', '')
22+
return /^[0-9A-F]{3}$/i.test(clean) || /^[0-9A-F]{6}$/i.test(clean)
23+
}
24+
25+
const isLabelHexValid = computed(() => validateHex(labelColor.value))
26+
const isBadgeHexValid = computed(() => validateHex(badgeColor.value))
27+
const isInputValid = computed(
28+
() => isLabelHexValid.value && isBadgeHexValid.value && pkg.value.length > 0,
29+
)
30+
31+
const cleanHex = (hex: string) => hex?.replace('#', '') || ''
32+
33+
const queryParams = computed(() => {
34+
if (!isInputValid.value) return ''
35+
const params = new URLSearchParams()
36+
37+
if (labelColor.value) params.append('labelColor', cleanHex(labelColor.value))
38+
if (badgeColor.value) params.append('color', cleanHex(badgeColor.value))
39+
if (badgeStyle.value !== 'default') params.append('style', badgeStyle.value)
40+
if (badgeValue.value) params.append('value', badgeValue.value)
41+
42+
if (usePkgName.value) {
43+
params.append('name', 'true')
44+
} else if (labelText.value) {
45+
params.append('label', labelText.value)
46+
}
47+
48+
const queryString = params.toString()
49+
return queryString ? `?${queryString}` : ''
50+
})
51+
52+
const badgeUrl = computed(() => {
53+
if (!isInputValid.value) return ''
54+
return `https://npmx.dev/api/registry/badge/${type.value}/${pkg.value}${queryParams.value}`
55+
})
56+
57+
watch([pkg, type, queryParams], () => {
58+
isValid.value = true
59+
})
60+
61+
const copyToClipboard = async () => {
62+
const markdown = `[![Open on npmx.dev](${badgeUrl.value})](https://npmx.dev/package/${pkg.value})`
63+
copy(markdown)
64+
}
65+
</script>
66+
67+
<template>
68+
<div
69+
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"
70+
>
71+
<div class="flex flex-col lg:flex-row items-end gap-4">
72+
<div class="flex flex-col gap-1.5 flex-1 w-full">
73+
<label class="text-[11px] font-bold uppercase tracking-wider text-gray-400 ml-1"
74+
>Package Name</label
75+
>
76+
<input
77+
v-model="pkg"
78+
type="text"
79+
placeholder="e.g. nuxt"
80+
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"
81+
/>
82+
</div>
83+
84+
<div class="flex flex-col gap-1.5 flex-1 w-full">
85+
<label class="text-[11px] font-bold uppercase tracking-wider text-gray-400 ml-1"
86+
>Badge Type</label
87+
>
88+
<div class="relative">
89+
<select
90+
v-model="type"
91+
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"
92+
>
93+
<option v-for="t in BADGE_TYPES" :key="t" :value="t" class="dark:bg-gray-900">
94+
{{ titleCase(t) }}
95+
</option>
96+
</select>
97+
<span
98+
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"
99+
/>
100+
</div>
101+
</div>
102+
103+
<div class="flex flex-col gap-1.5 flex-2 w-full">
104+
<label class="text-[11px] font-bold uppercase tracking-wider text-gray-400 ml-1"
105+
>Preview & Action</label
106+
>
107+
<div
108+
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"
109+
>
110+
<div
111+
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"
112+
>
113+
<img
114+
v-if="isValid && isInputValid"
115+
:src="badgeUrl"
116+
class="h-5"
117+
alt="Badge Preview"
118+
@error="isValid = false"
119+
/>
120+
<span v-else class="text-[10px] font-bold text-red-500 uppercase tracking-tighter">
121+
{{ !isInputValid ? 'Invalid Parameters' : 'Not Found' }}
122+
</span>
123+
</div>
124+
<button
125+
@click="copyToClipboard"
126+
:disabled="!isValid || !isInputValid || !pkg"
127+
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"
128+
:class="
129+
copied
130+
? 'text-emerald-500 bg-emerald-50/50 dark:bg-emerald-500/10'
131+
: 'text-gray-500 dark:text-gray-400'
132+
"
133+
>
134+
{{ copied ? 'Done!' : 'Copy' }}
135+
</button>
136+
</div>
137+
</div>
138+
</div>
139+
140+
<div class="h-px bg-gray-200 dark:bg-white/5 w-full" />
141+
142+
<div class="grid grid-cols-1 sm:grid-cols-4 gap-6">
143+
<div class="flex flex-col gap-1.5">
144+
<label class="text-[10px] font-bold uppercase text-gray-400 ml-1">Label Text</label>
145+
<div class="relative group">
146+
<input
147+
v-model="labelText"
148+
:disabled="usePkgName"
149+
type="text"
150+
placeholder="Custom Label"
151+
class="w-full 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"
152+
:class="{ 'opacity-50 grayscale pl-3': usePkgName }"
153+
/>
154+
155+
<transition
156+
enter-active-class="transition duration-200 ease-out"
157+
enter-from-class="opacity-0 scale-95"
158+
enter-to-class="opacity-100 scale-100"
159+
leave-active-class="transition duration-150 ease-in"
160+
leave-from-class="opacity-100 scale-100"
161+
leave-to-class="opacity-0 scale-95"
162+
>
163+
<div
164+
v-if="usePkgName"
165+
class="absolute right-1.5 top-1/2 -translate-y-1/2 pointer-events-none"
166+
>
167+
<span
168+
class="flex items-center gap-1 px-1.5 py-0.5 rounded-md bg-emerald-500/10 dark:bg-emerald-500/20 text-emerald-500 text-[9px] font-bold uppercase tracking-tighter border border-emerald-500/20"
169+
>
170+
Auto
171+
</span>
172+
</div>
173+
</transition>
174+
</div>
175+
</div>
176+
177+
<div class="flex flex-col gap-1.5">
178+
<label class="text-[10px] font-bold uppercase text-gray-400 ml-1">Badge Value</label>
179+
<input
180+
v-model="badgeValue"
181+
type="text"
182+
placeholder="Override Value"
183+
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 transition-all"
184+
/>
185+
</div>
186+
187+
<div class="flex flex-col gap-1.5">
188+
<label class="text-[10px] font-bold uppercase text-gray-400 ml-1">Label Color</label>
189+
<div
190+
class="flex items-center px-3 rounded-lg border bg-white dark:bg-black/20 transition-all"
191+
:class="
192+
isLabelHexValid
193+
? 'border-gray-200 dark:border-white/10 focus-within:border-emerald-500'
194+
: 'border-red-500 ring-1 ring-red-500/20'
195+
"
196+
>
197+
<span class="text-gray-400 text-xs font-mono mr-1">#</span>
198+
<input
199+
v-model="labelColor"
200+
type="text"
201+
placeholder="0a0a0a"
202+
class="w-full py-2 bg-transparent text-xs outline-none"
203+
/>
204+
<span
205+
v-if="!isLabelHexValid"
206+
class="i-lucide-alert-circle w-3.5 h-3.5 text-red-500 ml-1"
207+
/>
208+
</div>
209+
</div>
210+
211+
<div class="flex flex-col gap-1.5">
212+
<label class="text-[10px] font-bold uppercase text-gray-400 ml-1">Badge Color</label>
213+
<div
214+
class="flex items-center px-3 rounded-lg border bg-white dark:bg-black/20 transition-all"
215+
:class="
216+
isBadgeHexValid
217+
? 'border-gray-200 dark:border-white/10 focus-within:border-emerald-500'
218+
: 'border-red-500 ring-1 ring-red-500/20'
219+
"
220+
>
221+
<span class="text-gray-400 text-xs font-mono mr-1">#</span>
222+
<input
223+
v-model="badgeColor"
224+
type="text"
225+
placeholder="ff69b4"
226+
class="w-full py-2 bg-transparent text-xs outline-none"
227+
/>
228+
<span
229+
v-if="!isBadgeHexValid"
230+
class="i-lucide-alert-circle w-3.5 h-3.5 text-red-500 ml-1"
231+
/>
232+
</div>
233+
</div>
234+
</div>
235+
236+
<div
237+
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"
238+
>
239+
<label class="relative inline-flex items-center cursor-pointer group">
240+
<input v-model="usePkgName" type="checkbox" class="sr-only peer" />
241+
<div
242+
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"
243+
></div>
244+
<span
245+
class="ml-3 text-[10px] font-bold uppercase text-gray-400 group-hover:text-gray-600 dark:group-hover:text-gray-300 transition-colors"
246+
>Use package name as label</span
247+
>
248+
</label>
249+
250+
<div class="flex items-center gap-3">
251+
<label class="text-[10px] font-bold uppercase text-gray-400 whitespace-nowrap"
252+
>Badge Style</label
253+
>
254+
<select
255+
v-model="badgeStyle"
256+
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"
257+
>
258+
<option v-for="s in styles" :key="s" :value="s">{{ s }}</option>
259+
</select>
260+
</div>
261+
</div>
262+
</div>
263+
</template>

0 commit comments

Comments
 (0)