@@ -22,17 +22,51 @@ function wrapText(value: string, maxChars: number, maxLines: number) {
2222 const words = value . trim ( ) . split ( / \s + / ) . filter ( Boolean )
2323 const lines : string [ ] = [ ]
2424 let current = ''
25+
26+ function pushLine ( line : string ) {
27+ if ( ! line ) return
28+ lines . push ( line )
29+ }
30+
31+ function splitLongWord ( word : string ) {
32+ if ( word . length <= maxChars ) return [ word ]
33+ const parts : string [ ] = [ ]
34+ let remaining = word
35+ while ( remaining . length > maxChars ) {
36+ parts . push ( `${ remaining . slice ( 0 , maxChars - 1 ) } …` )
37+ remaining = remaining . slice ( maxChars - 1 )
38+ }
39+ if ( remaining ) parts . push ( remaining )
40+ return parts
41+ }
42+
2543 for ( const word of words ) {
44+ if ( word . length > maxChars ) {
45+ if ( current ) {
46+ pushLine ( current )
47+ current = ''
48+ if ( lines . length >= maxLines - 1 ) break
49+ }
50+ const parts = splitLongWord ( word )
51+ for ( const part of parts ) {
52+ pushLine ( part )
53+ if ( lines . length >= maxLines ) break
54+ }
55+ current = ''
56+ if ( lines . length >= maxLines - 1 ) break
57+ continue
58+ }
59+
2660 const next = current ? `${ current } ${ word } ` : word
2761 if ( next . length <= maxChars ) {
2862 current = next
2963 continue
3064 }
31- if ( current ) lines . push ( current )
65+ pushLine ( current )
3266 current = word
3367 if ( lines . length >= maxLines - 1 ) break
3468 }
35- if ( lines . length < maxLines && current ) lines . push ( current )
69+ if ( lines . length < maxLines && current ) pushLine ( current )
3670 if ( lines . length > maxLines ) lines . length = maxLines
3771
3872 const usedWords = lines . join ( ' ' ) . split ( / \s + / ) . filter ( Boolean ) . length
@@ -48,6 +82,12 @@ export function buildSkillOgSvg(params: SkillOgSvgParams) {
4882 const rawTitle = params . title . trim ( ) || 'ClawdHub Skill'
4983 const rawDescription = params . description . trim ( ) || 'Published on ClawdHub.'
5084
85+ const cardX = 72
86+ const cardY = 96
87+ const cardW = 640
88+ const cardH = 456
89+ const cardR = 34
90+
5191 const titleLines = wrapText ( rawTitle , 22 , 2 )
5292 const descLines = wrapText ( rawDescription , 52 , 3 )
5393
@@ -59,6 +99,8 @@ export function buildSkillOgSvg(params: SkillOgSvgParams) {
5999 const descLineHeight = 34
60100
61101 const pillText = `${ params . ownerLabel } • ${ params . versionLabel } `
102+ const underlineY = cardY + cardH - 80
103+ const footerY = cardY + cardH - 18
62104
63105 const titleTspans = titleLines
64106 . map ( ( line , index ) => {
@@ -110,6 +152,10 @@ export function buildSkillOgSvg(params: SkillOgSvgParams) {
110152 <stop stop-color="#FFFFFF" stop-opacity="0.16"/>
111153 <stop offset="1" stop-color="#FFFFFF" stop-opacity="0.06"/>
112154 </linearGradient>
155+
156+ <clipPath id="cardClip">
157+ <rect x="${ cardX } " y="${ cardY } " width="${ cardW } " height="${ cardH } " rx="${ cardR } "/>
158+ </clipPath>
113159 </defs>
114160
115161 <rect width="1200" height="630" fill="url(#bg)"/>
@@ -127,39 +173,41 @@ export function buildSkillOgSvg(params: SkillOgSvgParams) {
127173 </g>
128174
129175 <g filter="url(#cardShadow)">
130- <rect x="72 " y="96 " width="640 " height="438 " rx="34 " fill="#201B18" fill-opacity="0.92" stroke="url(#stroke)"/>
176+ <rect x="${ cardX } " y="${ cardY } " width="${ cardW } " height="${ cardH } " rx="${ cardR } " fill="#201B18" fill-opacity="0.92" stroke="url(#stroke)"/>
131177 </g>
132178
133- <image href="${ params . markDataUrl } " x="108" y="134" width="46" height="46" preserveAspectRatio="xMidYMid meet"/>
179+ <g clip-path="url(#cardClip)">
180+ <image href="${ params . markDataUrl } " x="108" y="134" width="46" height="46" preserveAspectRatio="xMidYMid meet"/>
134181
135- <g>
136- <rect x="166" y="136" width="520" height="42" rx="21" fill="url(#pill)" stroke="#E86A47" stroke-opacity="0.28"/>
137- <text x="186" y="163"
182+ <g>
183+ <rect x="166" y="136" width="520" height="42" rx="21" fill="url(#pill)" stroke="#E86A47" stroke-opacity="0.28"/>
184+ <text x="186" y="163"
185+ fill="#F6EFE4"
186+ font-size="18"
187+ font-weight="600"
188+ font-family="${ FONT_SANS } , sans-serif"
189+ opacity="0.92">${ escapeXml ( pillText ) } </text>
190+ </g>
191+
192+ <text x="114" y="${ titleY } "
193+ fill="#F6EFE4"
194+ font-size="${ titleFontSize } "
195+ font-weight="800"
196+ font-family="${ FONT_SANS } , sans-serif">${ titleTspans } </text>
197+
198+ <text x="114" y="${ descY } "
199+ fill="#C6B8A8"
200+ font-size="26"
201+ font-weight="500"
202+ font-family="${ FONT_SANS } , sans-serif">${ descTspans } </text>
203+
204+ <rect x="114" y="${ underlineY } " width="110" height="6" rx="3" fill="#E86A47"/>
205+ <text x="114" y="${ footerY } "
138206 fill="#F6EFE4"
139- font-size="18 "
140- font-weight="600 "
141- font-family=" ${ FONT_SANS } , sans-serif "
142- opacity="0.92 ">${ escapeXml ( pillText ) } </text>
207+ font-size="20 "
208+ font-weight="500 "
209+ opacity="0.90 "
210+ font-family=" ${ FONT_MONO } , monospace ">${ escapeXml ( params . footer ) } </text>
143211 </g>
144-
145- <text x="114" y="${ titleY } "
146- fill="#F6EFE4"
147- font-size="${ titleFontSize } "
148- font-weight="800"
149- font-family="${ FONT_SANS } , sans-serif">${ titleTspans } </text>
150-
151- <text x="114" y="${ descY } "
152- fill="#C6B8A8"
153- font-size="26"
154- font-weight="500"
155- font-family="${ FONT_SANS } , sans-serif">${ descTspans } </text>
156-
157- <rect x="114" y="472" width="110" height="6" rx="3" fill="#E86A47"/>
158- <text x="114" y="530"
159- fill="#F6EFE4"
160- font-size="20"
161- font-weight="500"
162- opacity="0.90"
163- font-family="${ FONT_MONO } , monospace">${ escapeXml ( params . footer ) } </text>
164212</svg>`
165213}
0 commit comments