feat(P2-7): expand link previews to 13 domain-specific card types

YouTube, Vimeo (shared VideoCard with thumbnail + play overlay),
GitHub (repo name/description parse), Twitter/X (tweet text parse),
Reddit (subreddit + post title), Spotify (artwork + track/artist),
Twitch (thumbnail + LIVE badge), Steam (game header image),
Wikipedia (clean text card), Discord (server icon + member count),
npm (package name/description), Stack Overflow (question excerpt),
IMDb (portrait poster + title/rating)

Generic fallback gains favicon from google/s2/favicons; empty cards
(no title or description) are suppressed. Shared SiteBadge component
with brand-colour CSS classes per domain in UrlPreview.css.tsx.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 23:34:23 -04:00
co-authored by Claude Sonnet 4.6
parent 01ba24df12
commit 73921cb2a1
2 changed files with 1060 additions and 47 deletions
@@ -138,3 +138,209 @@ export const GenericFaviconImg = style([
flexShrink: 0,
},
]);
// ---------------------------------------------------------------------------
// Shared media-card thumbnail (VideoCard, Steam, etc.)
// ---------------------------------------------------------------------------
export const MediaThumbnailWrapper = style([
DefaultReset,
{
position: 'relative',
flexShrink: 0,
width: toRem(160),
height: toRem(90),
overflow: 'hidden',
cursor: 'pointer',
backgroundColor: color.Surface.Container,
':hover': {
filter: 'brightness(0.85)',
},
},
]);
export const MediaThumbnailImg = style([
DefaultReset,
{
width: '100%',
height: '100%',
objectFit: 'cover',
objectPosition: 'center',
display: 'block',
},
]);
export const MediaPlayOverlay = style([
DefaultReset,
{
position: 'absolute',
inset: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
pointerEvents: 'none',
},
]);
export const MediaPlayButton = style([
DefaultReset,
{
width: toRem(44),
height: toRem(44),
borderRadius: '50%',
backgroundColor: 'rgba(0, 0, 0, 0.75)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#ffffff',
},
]);
// ---------------------------------------------------------------------------
// Shared square artwork thumbnail (Spotify, IMDb poster, Discord icon)
// ---------------------------------------------------------------------------
export const ArtworkThumbnail = style([
DefaultReset,
{
flexShrink: 0,
width: toRem(72),
height: toRem(72),
objectFit: 'cover',
objectPosition: 'center',
display: 'block',
},
]);
// IMDb portrait poster (60 × 90)
export const PosterThumbnail = style([
DefaultReset,
{
flexShrink: 0,
width: toRem(60),
height: toRem(90),
objectFit: 'cover',
objectPosition: 'center',
display: 'block',
},
]);
// ---------------------------------------------------------------------------
// Site-badge shared base
// ---------------------------------------------------------------------------
export const SiteBadge = style([
DefaultReset,
{
display: 'inline-flex',
alignItems: 'center',
gap: config.space.S100,
paddingLeft: config.space.S100,
paddingRight: config.space.S100,
paddingTop: '2px',
paddingBottom: '2px',
borderRadius: config.radii.R300,
fontSize: toRem(11),
fontWeight: 700,
lineHeight: '1.4',
letterSpacing: '0.03em',
flexShrink: 0,
},
]);
// Individual badge colour overrides (background / foreground)
export const BadgeVimeo = style({
backgroundColor: '#1ab7ea',
color: '#ffffff',
});
export const BadgeTwitter = style({
backgroundColor: '#000000',
color: '#ffffff',
});
export const BadgeReddit = style({
backgroundColor: '#ff4500',
color: '#ffffff',
});
export const BadgeSpotify = style({
backgroundColor: '#1db954',
color: '#ffffff',
});
export const BadgeTwitch = style({
backgroundColor: '#9146ff',
color: '#ffffff',
});
export const BadgeSteam = style({
backgroundColor: '#1b2838',
color: '#c7d5e0',
});
export const BadgeWikipedia = style({
backgroundColor: color.SurfaceVariant.ContainerLine,
color: color.SurfaceVariant.OnContainer,
});
export const BadgeDiscord = style({
backgroundColor: '#5865f2',
color: '#ffffff',
});
export const BadgeNpm = style({
backgroundColor: '#cb3837',
color: '#ffffff',
});
export const BadgeStackOverflow = style({
backgroundColor: '#f48024',
color: '#ffffff',
});
export const BadgeImdb = style({
backgroundColor: '#f5c518',
color: '#000000',
});
// ---------------------------------------------------------------------------
// Twitch LIVE badge
// ---------------------------------------------------------------------------
export const TwitchLiveBadge = style([
DefaultReset,
{
display: 'inline-flex',
alignItems: 'center',
paddingLeft: config.space.S100,
paddingRight: config.space.S100,
paddingTop: '2px',
paddingBottom: '2px',
borderRadius: config.radii.R300,
fontSize: toRem(11),
fontWeight: 700,
lineHeight: '1.4',
backgroundColor: '#e91916',
color: '#ffffff',
letterSpacing: '0.04em',
},
]);
// ---------------------------------------------------------------------------
// Icon-wrapper (generic left-side icon block — Twitter, Reddit, Wikipedia…)
// ---------------------------------------------------------------------------
export const IconWrapper = style([
DefaultReset,
{
flexShrink: 0,
width: toRem(72),
minHeight: toRem(72),
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: color.Surface.ContainerHover,
},
]);