11<script setup lang="ts">
2- withDefaults (
3- defineProps <{
4- name: string
5- version: string
6- downloads? : string
7- license? : string
8- primaryColor? : string
9- }>(),
10- {
11- downloads: ' ' ,
12- license: ' ' ,
13- primaryColor: ' #60a5fa' ,
14- },
15- )
2+ interface Props {
3+ name: string
4+ version: string
5+ downloads? : string
6+ license? : string
7+ primaryColor? : string
8+ description? : string
9+ repoOwner? : string
10+ repoName? : string
11+ stars? : number
12+ forks? : number
13+ directDepsCount? : number
14+ updatedAt? : string
15+ hasTypes? : boolean
16+ hasESM? : boolean
17+ hasCJS? : boolean
18+ }
19+
20+ const props = withDefaults (defineProps <Props >(), {
21+ downloads: ' ' ,
22+ license: ' ' ,
23+ primaryColor: ' #60a5fa' ,
24+ description: ' ' ,
25+ repoOwner: ' ' ,
26+ repoName: ' ' ,
27+ stars: 0 ,
28+ forks: 0 ,
29+ directDepsCount: 0 ,
30+ updatedAt: ' ' ,
31+ hasTypes: false ,
32+ hasESM: false ,
33+ hasCJS: false ,
34+ })
35+
36+ // Dynamic font sizing based on name length
37+ // OG images are 1200px wide, with 64px padding on each side = 1072px content width
38+ // Icon is 80px + 24px gap = 104px, leaving ~968px for name + version
39+ const titleFontSize = computed (() => {
40+ const len = props .name .length
41+ if (len <= 12 ) return 96 // text-8xl
42+ if (len <= 18 ) return 80
43+ if (len <= 24 ) return 64
44+ if (len <= 32 ) return 52
45+ if (len <= 40 ) return 44
46+ return 36 // very long names
47+ })
48+
49+ const versionFontSize = computed (() => {
50+ // Version scales proportionally but stays readable
51+ const base = titleFontSize .value
52+ return Math .max (42 , Math .round (base * 0.5 ))
53+ })
54+
55+ const truncatedVersion = computed (() => {
56+ const v = props .version
57+ if (v .length <= 12 ) return v
58+ return v .slice (0 , 11 ) + ' …'
59+ })
1660 </script >
1761
1862<template >
1963 <div
20- class =" h-full w-full flex flex-col justify-center px-20 bg-[#050505] text-[#fafafa] relative overflow-hidden"
64+ class =" h-full w-full flex flex-col justify-between px-16 py-14 bg-[#050505] text-[#fafafa] relative overflow-hidden"
65+ style =" font-family : ' Geist' , sans-serif "
2166 >
22- <div class =" relative z-10 flex flex-col gap-6" >
23- <div class =" flex items-start gap-4" >
67+ <!-- Top section -->
68+ <div class =" relative z-10 flex flex-col gap-4" >
69+ <!-- Short name layout: icon + ./name + version in one row -->
70+ <div v-if =" props.name.length < 24" class =" flex flex-row items-end gap-6 align-baseline mb-6" >
2471 <div
25- class =" flex items-start justify-center w-16 h-16 rounded-xl shadow-lg bg-gradient-to-tr from-[#3b82f6] "
26- :style =" { backgroundColor: primaryColor }"
72+ class =" w-20 h-20 rounded-xl shadow-lg flex items-center justify-center "
73+ :style =" { backgroundColor: props. primaryColor }"
2774 >
2875 <svg
29- width =" 36 "
30- height =" 36 "
76+ width =" 42 "
77+ height =" 42 "
3178 viewBox =" 0 0 24 24"
3279 fill =" none"
3380 stroke =" white"
3481 stroke-width =" 2.5"
3582 stroke-linecap =" round"
3683 stroke-linejoin =" round"
84+ :style =" { marginTop: '36px' }"
3785 >
3886 <path d =" m7.5 4.27 9 5.15" />
3987 <path
@@ -45,55 +93,236 @@ withDefaults(
4593 </div >
4694
4795 <h1
48- class =" text-8xl font-bold tracking-tighter "
49- style =" font-family : ' Geist Sans ' , sans-serif "
96+ class =" font-bold tracking-tight leading-none mt-0 "
97+ : style =" { fontSize: `${titleFontSize}px`, marginBottom: '-8px' } "
5098 >
51- <span :style =" { color: primaryColor }" class = " opacity-80 " >./</span >{{ name }}
99+ <span class = " opacity-80 " :style =" { color: props. primaryColor }" >./</span >{{ props. name }}
52100 </h1 >
53- </div >
54101
55- <div
56- class =" flex items-center gap-3 text-4xl font-light text-[#a3a3a3]"
57- style =" font-family : ' Geist Sans' , sans-serif "
58- >
59- <span
60- class =" px-3 py-1 rounded-lg border"
61- :style =" {
62- color: primaryColor,
63- backgroundColor: primaryColor + '10',
64- borderColor: primaryColor + '30',
65- boxShadow: `0 0 20px ${primaryColor}25`,
66- }"
67- >
68- {{ version }}
102+ <span class =" pb-1" :style =" { fontSize: `${versionFontSize}px`, color: props.primaryColor }" >
103+ v{{ truncatedVersion }}
69104 </span >
70- <span v-if =" downloads" >
71- <span >• {{ downloads }} </span >
72- <span class =" flex items-center gap-0" >
105+ </div >
106+
107+ <!-- Long name layout: icon + name on first row, version below -->
108+ <template v-else >
109+ <div class =" flex flex-row items-center gap-6" >
110+ <div
111+ class =" w-20 h-20 rounded-xl shadow-lg flex items-center justify-center"
112+ :style =" { backgroundColor: props.primaryColor }"
113+ >
73114 <svg
74- width =" 30 "
75- height =" 30 "
115+ width =" 42 "
116+ height =" 42 "
76117 viewBox =" 0 0 24 24"
77118 fill =" none"
78- stroke =" currentColor "
79- stroke-width =" 2"
119+ stroke =" white "
120+ stroke-width =" 2.5 "
80121 stroke-linecap =" round"
81122 stroke-linejoin =" round"
82- class = " text-white/70 "
123+ :style = " { marginTop: '36px' } "
83124 >
84- <circle cx =" 12" cy =" 12" r =" 10" class =" opacity-40" />
85- <path d =" M12 8v8m-3-3l3 3 3-3" />
125+ <path d =" m7.5 4.27 9 5.15" />
126+ <path
127+ d =" M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"
128+ />
129+ <path d =" m3.3 7 8.7 5 8.7-5" />
130+ <path d =" M12 22V12" />
86131 </svg >
87- <span >/wk</span >
132+ </div >
133+
134+ <h1
135+ class =" font-bold tracking-tight leading-none"
136+ :style =" { fontSize: `${titleFontSize}px` }"
137+ >
138+ {{ props.name }}
139+ </h1 >
140+ </div >
141+
142+ <!-- ./ and version on second row, under the icon -->
143+ <div class =" flex flex-row items-baseline gap-6" >
144+ <div
145+ class =" w-20 text-right opacity-80"
146+ :style =" {
147+ fontSize: `${versionFontSize}px`,
148+ color: props.primaryColor,
149+ paddingLeft: '22px',
150+ }"
151+ >
152+ ./
153+ </div >
154+ <span :style =" { fontSize: `${versionFontSize}px`, color: props.primaryColor }" >
155+ v{{ truncatedVersion }}
88156 </span >
157+ </div >
158+ </template >
159+
160+ <!-- Description (can extend under stats, will be faded out) -->
161+ <div v-if =" props.description" class =" text-4xl text-[#a3a3a3] max-w-[1000px]" >
162+ {{ props.description }}
163+ </div >
164+ </div >
165+
166+ <!-- Module format badges - top right (unchanged size) -->
167+ <div class =" absolute top-8 right-10 flex flex-row items-center gap-2 text-xl" >
168+ <span
169+ v-if =" props.hasTypes"
170+ class =" px-3 py-1 rounded-md bg-[#1a1a1a] border border-[#333] text-[#a3a3a3] font-medium"
171+ >
172+ Types
173+ </span >
174+ <span
175+ v-if =" props.hasESM"
176+ class =" px-3 py-1 rounded-md bg-[#1a1a1a] border border-[#333] text-[#a3a3a3] font-medium"
177+ >
178+ ESM
179+ </span >
180+ <span
181+ v-if =" props.hasCJS"
182+ class =" px-3 py-1 rounded-md bg-[#1a1a1a] border border-[#333] text-[#a3a3a3] font-medium"
183+ >
184+ CJS
185+ </span >
186+ </div >
187+
188+ <!-- Gradient fade above stats -->
189+ <div
190+ class =" absolute left-0 right-0 h-24 z-10"
191+ :style =" { bottom: '140px', background: 'linear-gradient(to bottom, transparent, #050505)' }"
192+ />
193+
194+ <!-- Bottom stats (fixed at bottom) -->
195+ <div class =" absolute bottom-14 left-16 right-16 z-20 flex flex-col gap-4 bg-[#050505]" >
196+ <!-- GitHub stats row -->
197+ <div
198+ v-if =" props.repoOwner && props.repoName"
199+ class =" flex flex-row items-center gap-12 text-3xl text-[#737373]"
200+ >
201+ <span class =" flex flex-row items-center gap-3" >
202+ <svg width =" 28" height =" 28" viewBox =" 0 0 24 24" fill =" currentColor" >
203+ <path
204+ d =" M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"
205+ />
206+ </svg >
207+ {{ props.repoOwner }}/{{ props.repoName }}
208+ </span >
209+ <span v-if =" props.stars" class =" flex flex-row items-center gap-3" >
210+ <svg
211+ width =" 28"
212+ height =" 28"
213+ viewBox =" 0 0 24 24"
214+ fill =" none"
215+ stroke =" currentColor"
216+ stroke-width =" 2"
217+ stroke-linecap =" round"
218+ stroke-linejoin =" round"
219+ >
220+ <polygon
221+ points =" 12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"
222+ />
223+ </svg >
224+ {{ props.stars.toLocaleString() }}
225+ </span >
226+ <span v-if =" props.forks" class =" flex flex-row items-center gap-3" >
227+ <svg
228+ width =" 28"
229+ height =" 28"
230+ viewBox =" 0 0 24 24"
231+ fill =" none"
232+ stroke =" currentColor"
233+ stroke-width =" 2"
234+ stroke-linecap =" round"
235+ stroke-linejoin =" round"
236+ >
237+ <circle cx =" 12" cy =" 18" r =" 3" />
238+ <circle cx =" 6" cy =" 6" r =" 3" />
239+ <circle cx =" 18" cy =" 6" r =" 3" />
240+ <path d =" M18 9v2c0 .6-.4 1-1 1H7c-.6 0-1-.4-1-1V9" />
241+ <path d =" M12 12v3" />
242+ </svg >
243+ {{ props.forks.toLocaleString() }}
244+ </span >
245+ </div >
246+
247+ <!-- npm stats row -->
248+ <div class =" flex flex-row items-center gap-12 text-3xl text-[#a3a3a3]" >
249+ <span v-if =" props.downloads" class =" flex flex-row items-center gap-3" >
250+ <svg
251+ width =" 28"
252+ height =" 28"
253+ viewBox =" 0 0 24 24"
254+ fill =" none"
255+ stroke =" currentColor"
256+ stroke-width =" 2"
257+ stroke-linecap =" round"
258+ stroke-linejoin =" round"
259+ >
260+ <path d =" M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
261+ <polyline points =" 7 10 12 15 17 10" />
262+ <line x1 =" 12" x2 =" 12" y1 =" 15" y2 =" 3" />
263+ </svg >
264+ {{ props.downloads }}/wk
265+ </span >
266+ <span v-if =" props.directDepsCount !== undefined" class =" flex flex-row items-center gap-3" >
267+ <svg
268+ width =" 28"
269+ height =" 28"
270+ viewBox =" 0 0 24 24"
271+ fill =" none"
272+ stroke =" currentColor"
273+ stroke-width =" 2"
274+ stroke-linecap =" round"
275+ stroke-linejoin =" round"
276+ >
277+ <path
278+ d =" M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"
279+ />
280+ </svg >
281+ {{ props.directDepsCount }} deps
282+ </span >
283+ <span v-if =" props.license" class =" flex flex-row items-center gap-3" >
284+ <svg
285+ width =" 28"
286+ height =" 28"
287+ viewBox =" 0 0 24 24"
288+ fill =" none"
289+ stroke =" currentColor"
290+ stroke-width =" 2"
291+ stroke-linecap =" round"
292+ stroke-linejoin =" round"
293+ >
294+ <circle cx =" 12" cy =" 12" r =" 10" />
295+ <path d =" m15 9-6 6" />
296+ <path d =" M9 9h.01" />
297+ <path d =" M15 15h.01" />
298+ </svg >
299+ {{ props.license }}
300+ </span >
301+ <span v-if =" props.updatedAt" class =" flex flex-row items-center gap-3" >
302+ <svg
303+ width =" 28"
304+ height =" 28"
305+ viewBox =" 0 0 24 24"
306+ fill =" none"
307+ stroke =" currentColor"
308+ stroke-width =" 2"
309+ stroke-linecap =" round"
310+ stroke-linejoin =" round"
311+ >
312+ <rect width =" 18" height =" 18" x =" 3" y =" 4" rx =" 2" ry =" 2" />
313+ <line x1 =" 16" x2 =" 16" y1 =" 2" y2 =" 6" />
314+ <line x1 =" 8" x2 =" 8" y1 =" 2" y2 =" 6" />
315+ <line x1 =" 3" x2 =" 21" y1 =" 10" y2 =" 10" />
316+ </svg >
317+ {{ props.updatedAt }}
89318 </span >
90- <span v-if =" license" > • {{ license }}</span >
91319 </div >
92320 </div >
93321
322+ <!-- Background glow -->
94323 <div
95324 class =" absolute -top-32 -right-32 w-[550px] h-[550px] rounded-full blur-3xl"
96- :style =" { backgroundColor: primaryColor + '10' }"
325+ :style =" { backgroundColor: props. primaryColor + '10' }"
97326 />
98327 </div >
99328</template >
0 commit comments