feat(P2-7): deep link preview cards for TikTok, X, Twitch, Reddit, YT Shorts
YouTube Shorts: portrait 9:16 thumbnail, red Shorts badge, channel parse TikTok: portrait thumbnail, @user extract, caption parse (3 OG formats), hashtag chips, dark ♫ placeholder fallback Twitter/X: tweet text parse from all og:title formats, media image when og:image:width>=300, profile vs tweet URL distinction, 𝕏 SVG badge Twitch: live/clip/VOD detection, pulsing LIVE badge with CSS keyframes, game extraction from og:description, channel from URL Reddit: r/subreddit badge, u/author + upvote + comment count parsed from og:description, post thumbnail 80x60, redd.it short URL support Shared PortraitThumbnail (80x142) reused by TikTok + Shorts. All brand hex colors in CSS file only, never in TSX. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
import { keyframes, style } from '@vanilla-extract/css';
|
||||
import { DefaultReset, color, config, toRem } from 'folds';
|
||||
|
||||
export const UrlPreview = style([
|
||||
@@ -305,6 +305,16 @@ export const BadgeImdb = style({
|
||||
color: '#000000',
|
||||
});
|
||||
|
||||
export const BadgeYouTubeShorts = style({
|
||||
backgroundColor: '#FF0000',
|
||||
color: '#ffffff',
|
||||
});
|
||||
|
||||
export const BadgeTikTok = style({
|
||||
backgroundColor: '#000000',
|
||||
color: '#ffffff',
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Twitch LIVE badge
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -344,3 +354,324 @@ export const IconWrapper = style([
|
||||
backgroundColor: color.Surface.ContainerHover,
|
||||
},
|
||||
]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Portrait thumbnail — 9:16 for Shorts & TikTok
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const PortraitThumbnail = style([
|
||||
DefaultReset,
|
||||
{
|
||||
position: 'relative',
|
||||
flexShrink: 0,
|
||||
width: toRem(80),
|
||||
height: toRem(142),
|
||||
borderRadius: config.radii.R300,
|
||||
overflow: 'hidden',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: color.Surface.Container,
|
||||
|
||||
':hover': {
|
||||
filter: 'brightness(0.85)',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
export const PortraitThumbnailImg = style([
|
||||
DefaultReset,
|
||||
{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
objectPosition: 'center',
|
||||
display: 'block',
|
||||
},
|
||||
]);
|
||||
|
||||
export const PortraitPlayOverlay = style([
|
||||
DefaultReset,
|
||||
{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
]);
|
||||
|
||||
export const PortraitPlayButton = style([
|
||||
DefaultReset,
|
||||
{
|
||||
width: toRem(36),
|
||||
height: toRem(36),
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.72)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#ffffff',
|
||||
fontSize: toRem(16),
|
||||
},
|
||||
]);
|
||||
|
||||
export const PortraitPlaceholder = style([
|
||||
DefaultReset,
|
||||
{
|
||||
flexShrink: 0,
|
||||
width: toRem(80),
|
||||
height: toRem(142),
|
||||
borderRadius: config.radii.R300,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: '#111111',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: toRem(32),
|
||||
},
|
||||
]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Twitter / X card
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const TweetBlock = style([
|
||||
DefaultReset,
|
||||
{
|
||||
backgroundColor: 'rgba(0,0,0,0.15)',
|
||||
borderRadius: config.radii.R300,
|
||||
padding: config.space.S200,
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 4,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
fontSize: toRem(13),
|
||||
lineHeight: '1.5',
|
||||
},
|
||||
]);
|
||||
|
||||
export const TweetMediaImg = style([
|
||||
DefaultReset,
|
||||
{
|
||||
width: '100%',
|
||||
maxHeight: toRem(200),
|
||||
objectFit: 'cover',
|
||||
objectPosition: 'center',
|
||||
display: 'block',
|
||||
borderRadius: config.radii.R300,
|
||||
marginTop: config.space.S100,
|
||||
},
|
||||
]);
|
||||
|
||||
export const TwitterHeader = style([
|
||||
DefaultReset,
|
||||
{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: config.space.S100,
|
||||
padding: `${config.space.S100} ${config.space.S200}`,
|
||||
paddingTop: config.space.S200,
|
||||
},
|
||||
]);
|
||||
|
||||
export const ProfileAvatarImg = style([
|
||||
DefaultReset,
|
||||
{
|
||||
width: toRem(36),
|
||||
height: toRem(36),
|
||||
borderRadius: '50%',
|
||||
objectFit: 'cover',
|
||||
objectPosition: 'center',
|
||||
flexShrink: 0,
|
||||
display: 'block',
|
||||
},
|
||||
]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Twitch card
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const TwitchThumbnailWrapper = style([
|
||||
DefaultReset,
|
||||
{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
maxWidth: toRem(240),
|
||||
aspectRatio: '16 / 9',
|
||||
overflow: 'hidden',
|
||||
flexShrink: 0,
|
||||
backgroundColor: '#0e0e10',
|
||||
cursor: 'pointer',
|
||||
|
||||
':hover': {
|
||||
filter: 'brightness(0.85)',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
export const TwitchThumbnailImg = style([
|
||||
DefaultReset,
|
||||
{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
objectPosition: 'center',
|
||||
display: 'block',
|
||||
},
|
||||
]);
|
||||
|
||||
export const TwitchLiveOverlay = style([
|
||||
DefaultReset,
|
||||
{
|
||||
position: 'absolute',
|
||||
top: config.space.S100,
|
||||
right: config.space.S100,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
backgroundColor: '#e91916',
|
||||
color: '#ffffff',
|
||||
borderRadius: config.radii.R300,
|
||||
paddingLeft: config.space.S100,
|
||||
paddingRight: config.space.S100,
|
||||
paddingTop: '2px',
|
||||
paddingBottom: '2px',
|
||||
fontSize: toRem(11),
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.04em',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
]);
|
||||
|
||||
const livePulseKeyframes = keyframes({
|
||||
'0%': { opacity: 1 },
|
||||
'100%': { opacity: 0.4 },
|
||||
});
|
||||
|
||||
export const LiveDot = style([
|
||||
DefaultReset,
|
||||
{
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#ffffff',
|
||||
animation: `${livePulseKeyframes} 1s ease-in-out infinite alternate`,
|
||||
},
|
||||
]);
|
||||
|
||||
export const BadgeTwitchPurple = style({
|
||||
backgroundColor: '#9146FF',
|
||||
color: '#ffffff',
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Reddit card
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const RedditPostLayout = style([
|
||||
DefaultReset,
|
||||
{
|
||||
display: 'flex',
|
||||
gap: config.space.S200,
|
||||
padding: config.space.S200,
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
]);
|
||||
|
||||
export const RedditThumb = style([
|
||||
DefaultReset,
|
||||
{
|
||||
flexShrink: 0,
|
||||
width: toRem(80),
|
||||
height: toRem(60),
|
||||
borderRadius: config.radii.R300,
|
||||
objectFit: 'cover',
|
||||
objectPosition: 'center',
|
||||
display: 'block',
|
||||
},
|
||||
]);
|
||||
|
||||
export const RedditSubBadge = 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: '#FF4500',
|
||||
color: '#ffffff',
|
||||
letterSpacing: '0.02em',
|
||||
},
|
||||
]);
|
||||
|
||||
export const RedditUpvote = style([
|
||||
DefaultReset,
|
||||
{
|
||||
color: '#FF4500',
|
||||
fontWeight: 700,
|
||||
fontSize: toRem(12),
|
||||
},
|
||||
]);
|
||||
|
||||
export const RedditMeta = style([
|
||||
DefaultReset,
|
||||
{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: config.space.S200,
|
||||
marginTop: config.space.S100,
|
||||
fontSize: toRem(12),
|
||||
opacity: 0.75,
|
||||
},
|
||||
]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TikTok hashtag chip
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const HashtagChip = style([
|
||||
DefaultReset,
|
||||
{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
fontSize: toRem(11),
|
||||
opacity: 0.7,
|
||||
marginRight: '4px',
|
||||
},
|
||||
]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shorts card header bar
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const ShortsHeader = style([
|
||||
DefaultReset,
|
||||
{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: config.space.S100,
|
||||
padding: `${config.space.S100} ${config.space.S200}`,
|
||||
backgroundColor: color.Surface.ContainerHover,
|
||||
borderBottom: `1px solid ${color.SurfaceVariant.ContainerLine}`,
|
||||
},
|
||||
]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Side-by-side portrait layout (Shorts, TikTok)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const PortraitSideLayout = style([
|
||||
DefaultReset,
|
||||
{
|
||||
display: 'flex',
|
||||
gap: config.space.S200,
|
||||
padding: config.space.S200,
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
]);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user