Skip to content

Commit fbdd455

Browse files
committed
refactor: reuse formatter and move withAlpha to utils
1 parent d9c6631 commit fbdd455

File tree

6 files changed

+67
-70
lines changed

6 files changed

+67
-70
lines changed

app/components/OgImage/BlogPost.vue

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,7 @@ const props = withDefaults(
1515
},
1616
)
1717
18-
const formattedDate = computed(() => {
19-
if (!props.date) return ''
20-
try {
21-
return new Date(props.date).toLocaleDateString('en-US', {
22-
year: 'numeric',
23-
month: 'short',
24-
day: 'numeric',
25-
})
26-
} catch {
27-
return props.date
28-
}
29-
})
18+
const formattedDate = computed(() => formatDate(props.date))
3019
3120
const MAX_VISIBLE_AUTHORS = 2
3221

app/components/OgImage/ShareCard.vue

Lines changed: 12 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const props = withDefaults(
88
theme?: 'light' | 'dark'
99
color?: string
1010
}>(),
11-
{ theme: 'dark' },
11+
{ theme: 'light' },
1212
)
1313
1414
const t = computed(() => SHARE_CARD_THEMES[props.theme])
@@ -20,41 +20,8 @@ const primaryColor = computed(() => {
2020
return ACCENT_COLOR_TOKENS.sky[props.theme].hex
2121
})
2222
23-
function withAlpha(color: string, alpha: number): string {
24-
if (color.startsWith('oklch(')) return color.replace(')', ` / ${alpha})`)
25-
if (color.startsWith('#'))
26-
return (
27-
color +
28-
Math.round(alpha * 255)
29-
.toString(16)
30-
.padStart(2, '0')
31-
)
32-
return color
33-
}
34-
35-
function formatNum(n: number) {
36-
return Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 1 }).format(n)
37-
}
38-
39-
function formatBytes(bytes: number) {
40-
if (!+bytes) return ''
41-
const k = 1024
42-
const sizes = ['B', 'KB', 'MB', 'GB'] as const
43-
const i = Math.floor(Math.log(bytes) / Math.log(k))
44-
return `${parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`
45-
}
46-
47-
function formatDate(iso: string) {
48-
return new Date(iso).toLocaleDateString('en-US', {
49-
month: 'short',
50-
day: 'numeric',
51-
year: 'numeric',
52-
})
53-
}
54-
55-
function truncate(s: string, n: number) {
56-
return s.length > n ? s.slice(0, n - 1) + '' : s
57-
}
23+
const compactFormatter = useCompactNumberFormatter()
24+
const bytesFormatter = useBytesFormatter()
5825
5926
const { data: resolvedVersion } = await useResolvedVersion(
6027
computed(() => props.name),
@@ -151,7 +118,7 @@ const fontMono = "'Geist Mono', ui-monospace, monospace"
151118
class="text-[40px] font-medium leading-none tracking-[-1.5px]"
152119
:style="{ color: t.text, fontFamily: fontMono }"
153120
>
154-
{{ formatNum(weeklyDownloads) }}
121+
{{ compactFormatter.format(weeklyDownloads) }}
155122
</span>
156123
<span class="text-[22px] font-light" :style="{ color: t.textMuted }">weekly</span>
157124
</div>
@@ -171,15 +138,15 @@ const fontMono = "'Geist Mono', ui-monospace, monospace"
171138
v-if="hasTypes"
172139
class="flex items-center text-[20px] font-light py-1 px-[14px] rounded-[6px] leading-[1.6]"
173140
:style="{
174-
border: `1px solid ${withAlpha(t.border, 0.6)}`,
141+
border: `1px solid ${t.borderMuted}`,
175142
color: t.textSubtle,
176143
}"
177144
>Types</span
178145
>
179146
<span
180147
class="flex items-center text-[20px] font-light py-1 px-[14px] rounded-[6px] leading-[1.6]"
181148
:style="{
182-
border: `1px solid ${withAlpha(t.border, 0.6)}`,
149+
border: `1px solid ${t.borderMuted}`,
183150
color: t.textSubtle,
184151
}"
185152
>{{ moduleFormat }}</span
@@ -188,7 +155,7 @@ const fontMono = "'Geist Mono', ui-monospace, monospace"
188155
v-if="license"
189156
class="flex items-center text-[20px] font-light py-1 px-[14px] rounded-[6px] leading-[1.6]"
190157
:style="{
191-
border: `1px solid ${withAlpha(t.border, 0.6)}`,
158+
border: `1px solid ${t.borderMuted}`,
192159
color: t.textSubtle,
193160
}"
194161
>{{ license }}</span
@@ -197,8 +164,8 @@ const fontMono = "'Geist Mono', ui-monospace, monospace"
197164
v-if="repoSlug"
198165
class="flex items-center text-[20px] font-light py-1 px-[14px] rounded-[6px] leading-[1.6]"
199166
:style="{
200-
border: `1px solid ${withAlpha(t.border, 0.5)}`,
201-
color: withAlpha(t.textSubtle, 0.8),
167+
border: `1px solid ${t.borderFaint}`,
168+
color: t.textFaint,
202169
fontFamily: fontMono,
203170
}"
204171
>{{ repoSlug }}</span
@@ -228,7 +195,7 @@ const fontMono = "'Geist Mono', ui-monospace, monospace"
228195
<span
229196
class="text-[24px] font-normal leading-none tracking-[-0.3px]"
230197
:style="{ color: t.textMuted }"
231-
>{{ formatNum(stars) }}</span
198+
>{{ compactFormatter.format(stars) }}</span
232199
>
233200
</div>
234201

@@ -252,7 +219,7 @@ const fontMono = "'Geist Mono', ui-monospace, monospace"
252219
<span
253220
class="text-[24px] font-normal leading-none tracking-[-0.3px]"
254221
:style="{ color: t.textMuted }"
255-
>{{ formatNum(forks) }}</span
222+
>{{ compactFormatter.format(forks) }}</span
256223
>
257224
</div>
258225

@@ -276,7 +243,7 @@ const fontMono = "'Geist Mono', ui-monospace, monospace"
276243
<span
277244
class="text-[24px] font-normal leading-none tracking-[-0.3px]"
278245
:style="{ color: t.textMuted }"
279-
>{{ formatBytes(unpackedSize) }}</span
246+
>{{ bytesFormatter.format(unpackedSize) }}</span
280247
>
281248
</div>
282249

app/utils/colors.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
// Vue Data UI does not support CSS vars nor OKLCH for now
22

3+
/**
4+
* Appends an alpha value to a hex or oklch color string.
5+
* Needed because OG image renderers (satori) don't support CSS variables or
6+
* opacity utilities — colors must be fully resolved values.
7+
*/
8+
export function withAlpha(color: string, alpha: number): string {
9+
if (color.startsWith('oklch(')) return color.replace(')', ` / ${alpha})`)
10+
if (color.startsWith('#'))
11+
return color + Math.round(alpha * 255).toString(16).padStart(2, '0')
12+
return color
13+
}
14+
315
/**
416
* Default neutral OKLCH color used as fallback when CSS variables are unavailable (e.g., during SSR).
517
* This matches the dark mode value of --fg-subtle defined in main.css.

app/utils/formatters.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,20 @@ export function toIsoDateString(date: Date): string {
44
const day = String(date.getUTCDate()).padStart(2, '0')
55
return `${year}-${month}-${day}`
66
}
7+
8+
/**
9+
* Format an ISO date string to a human-readable date (e.g. "Jan 1, 2024").
10+
* Returns the original string if parsing fails, or an empty string if no date is provided.
11+
*/
12+
export function formatDate(date: string | undefined): string {
13+
if (!date) return ''
14+
try {
15+
return new Date(date).toLocaleDateString('en-US', {
16+
year: 'numeric',
17+
month: 'short',
18+
day: 'numeric',
19+
})
20+
} catch {
21+
return date
22+
}
23+
}

app/utils/string.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function truncate(s: string, n: number): string {
2+
return s.length > n ? s.slice(0, n - 1) + '…' : s
3+
}

shared/utils/constants.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -156,28 +156,37 @@ export const BACKGROUND_THEME_TOKENS = {
156156
* Must use hex/rgb — satori (the OG image renderer) does not support oklch.
157157
* Background is not included here — use BACKGROUND_THEME_TOKENS for the card bg.
158158
* Values are hex equivalents of the corresponding CSS custom properties:
159-
* border → --border
160-
* divider → --border-subtle
161-
* text → --fg
162-
* textMuted → --fg-muted
163-
* textSubtle → --fg-subtle
159+
* border → --border
160+
* borderMuted → --border at 60% opacity
161+
* borderFaint → --border at 50% opacity
162+
* divider → --border-subtle
163+
* text → --fg
164+
* textMuted → --fg-muted
165+
* textSubtle → --fg-subtle
166+
* textFaint → --fg-subtle at 80% opacity
164167
*/
165168
export const SHARE_CARD_THEMES = {
166169
dark: {
167-
bg: '#101010', // --bg: oklch(0.171 0 0)
168-
border: '#262626', // --border: oklch(0.269 0 0)
170+
bg: '#101010', // --bg: oklch(0.171 0 0)
171+
border: '#262626', // --border: oklch(0.269 0 0)
172+
borderMuted: '#26262699', // --border @ 60%
173+
borderFaint: '#26262680', // --border @ 50%
169174
divider: '#1f1f1f', // --border-subtle: oklch(0.239 0 0)
170-
text: '#f9f9f9', // --fg: oklch(0.982 0 0)
175+
text: '#f9f9f9', // --fg: oklch(0.982 0 0)
171176
textMuted: '#adadad', // --fg-muted: oklch(0.749 0 0)
172-
textSubtle: '#969696', // --fg-subtle: oklch(0.673 0 0)
177+
textSubtle: '#969696', // --fg-subtle: oklch(0.673 0 0)
178+
textFaint: '#969696cc', // --fg-subtle @ 80%
173179
},
174180
light: {
175-
bg: '#ffffff', // --bg: oklch(1 0 0)
176-
border: '#cecece', // --border: oklch(0.8514 0 0)
181+
bg: '#ffffff', // --bg: oklch(1 0 0)
182+
border: '#cecece', // --border: oklch(0.8514 0 0)
183+
borderMuted: '#cecece99', // --border @ 60%
184+
borderFaint: '#cecece80', // --border @ 50%
177185
divider: '#e5e5e5', // --border-subtle: oklch(0.922 0 0)
178-
text: '#0a0a0a', // --fg: oklch(0.146 0 0)
186+
text: '#0a0a0a', // --fg: oklch(0.146 0 0)
179187
textMuted: '#474747', // --fg-muted: oklch(0.398 0 0)
180-
textSubtle: '#5d5d5d', // --fg-subtle: oklch(0.48 0 0)
188+
textSubtle: '#5d5d5d', // --fg-subtle: oklch(0.48 0 0)
189+
textFaint: '#5d5d5dcc', // --fg-subtle @ 80%
181190
},
182191
} as const satisfies Record<'light' | 'dark', Record<string, string>>
183192

0 commit comments

Comments
 (0)