GIF previews rendered but never played: Synapse's /thumbnail endpoint flattens animated GIFs to a still first frame. GifCard and the generic OG card now request the original via /download (no width/height) for GIFs, so they animate. Guarded with shouldServeGifOriginal(): a matrix:image:size cap (10 MB) keeps a huge self-hosted GIF on the frozen thumbnail, and the generic card's eager <img> gains loading="lazy" (it was the one preview image missing it) so originals stay off the wire until near the viewport. Also adds Mixcloud + Deezer inline media embeds (iframe widgets via parseMediaEmbed/MediaEmbedCard, matching the existing click-to-play pattern), and fixes Deezer podcast links: they live at /show/<id>, not /podcast/<id> (the latter 404s on Deezer's own oEmbed) — verified against the live API. Reviewed by two agents; both findings (Deezer /show, GIF eager-load) fixed and covered by tests. Desktop Tauri frame-src CSP updated separately. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
987 lines
22 KiB
TypeScript
987 lines
22 KiB
TypeScript
import { keyframes, style } from '@vanilla-extract/css';
|
||
import { DefaultReset, color, config, toRem } from 'folds';
|
||
|
||
export const UrlPreview = style([
|
||
DefaultReset,
|
||
{
|
||
// 25rem (=400px) on desktop, but shrink to fit narrow phones so a single
|
||
// card doesn't exceed the viewport (mirrors UrlPreviewWide's min()).
|
||
width: 'min(25rem, 92vw)',
|
||
minHeight: toRem(102),
|
||
backgroundColor: color.SurfaceVariant.Container,
|
||
color: color.SurfaceVariant.OnContainer,
|
||
border: `${config.borderWidth.B300} solid ${color.SurfaceVariant.ContainerLine}`,
|
||
borderRadius: config.radii.R300,
|
||
overflow: 'hidden',
|
||
},
|
||
]);
|
||
|
||
// Wider, responsive card for interactive embeds (video players, tweets, posts)
|
||
// so the player chrome / tweet content isn't cramped or clipped. Defined after
|
||
// UrlPreview so its width wins the cascade.
|
||
export const UrlPreviewWide = style({
|
||
width: 'min(38rem, 94vw)',
|
||
});
|
||
|
||
// The Twitch/Twitter/TikTok-fallback cards lay their header/thumbnail out BESIDE
|
||
// the content as direct children of the UrlPreview flex row; on a phone that
|
||
// squeezes both. Stack them vertically on narrow viewports only. `UrlPreview`'s
|
||
// Box has no explicit direction (browser default row), so this override wins
|
||
// with nothing to compete against, and desktop (>750px) is unchanged.
|
||
export const StackOnMobile = style({
|
||
'@media': {
|
||
'(max-width: 750px)': {
|
||
flexDirection: 'column',
|
||
},
|
||
},
|
||
});
|
||
|
||
export const UrlPreviewImg = style([
|
||
DefaultReset,
|
||
{
|
||
width: toRem(100),
|
||
height: toRem(100),
|
||
objectFit: 'cover',
|
||
objectPosition: 'center',
|
||
flexShrink: 0,
|
||
overflow: 'hidden',
|
||
cursor: 'pointer',
|
||
|
||
':hover': {
|
||
filter: 'brightness(0.8)',
|
||
},
|
||
},
|
||
]);
|
||
|
||
export const UrlPreviewContent = style([
|
||
DefaultReset,
|
||
{
|
||
padding: config.space.S200,
|
||
},
|
||
]);
|
||
|
||
export const UrlPreviewDescription = style([
|
||
DefaultReset,
|
||
{
|
||
display: '-webkit-box',
|
||
WebkitLineClamp: 2,
|
||
WebkitBoxOrient: 'vertical',
|
||
overflow: 'hidden',
|
||
},
|
||
]);
|
||
|
||
// YouTube card styles
|
||
export const YouTubeThumbnailWrapper = 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 YouTubeThumbnailImg = style([
|
||
DefaultReset,
|
||
{
|
||
width: '100%',
|
||
height: '100%',
|
||
objectFit: 'cover',
|
||
objectPosition: 'center',
|
||
display: 'block',
|
||
},
|
||
]);
|
||
|
||
export const YouTubePlayOverlay = style([
|
||
DefaultReset,
|
||
{
|
||
position: 'absolute',
|
||
inset: 0,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
pointerEvents: 'none',
|
||
},
|
||
]);
|
||
|
||
export const YouTubePlayButton = 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',
|
||
},
|
||
]);
|
||
|
||
// GitHub card styles
|
||
export const GitHubIconWrapper = style([
|
||
DefaultReset,
|
||
{
|
||
flexShrink: 0,
|
||
width: toRem(72),
|
||
minHeight: toRem(72),
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
color: color.SurfaceVariant.OnContainer,
|
||
opacity: 0.8,
|
||
backgroundColor: color.Surface.ContainerHover,
|
||
},
|
||
]);
|
||
|
||
// Generic card favicon styles
|
||
export const GenericFaviconWrapper = style([
|
||
DefaultReset,
|
||
{
|
||
display: 'inline-flex',
|
||
alignItems: 'center',
|
||
gap: config.space.S100,
|
||
verticalAlign: 'middle',
|
||
},
|
||
]);
|
||
|
||
export const GenericFaviconImg = style([
|
||
DefaultReset,
|
||
{
|
||
width: toRem(16),
|
||
height: toRem(16),
|
||
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',
|
||
},
|
||
]);
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Inline video embed (YouTube / Vimeo) — media-forward click-to-play facade
|
||
// ---------------------------------------------------------------------------
|
||
|
||
export const EmbedMediaLandscape = style([
|
||
DefaultReset,
|
||
{
|
||
position: 'relative',
|
||
width: '100%',
|
||
// 16:9 via the padding-top hack (56.25%), NOT `aspect-ratio`. An absolutely-
|
||
// positioned *replaced* element (the player <iframe>) collapses to its ~200px
|
||
// intrinsic size inside an `aspect-ratio` box; padding derives a definite height
|
||
// from the definite width so the iframe (inset:0) reliably fills it.
|
||
height: 0,
|
||
paddingTop: '56.25%',
|
||
overflow: 'hidden',
|
||
backgroundColor: color.Surface.Container,
|
||
selectors: {
|
||
'&:fullscreen': { width: '100vw', height: '100vh', paddingTop: 0 },
|
||
},
|
||
},
|
||
]);
|
||
|
||
export const EmbedMediaPortrait = style([
|
||
DefaultReset,
|
||
{
|
||
position: 'relative',
|
||
width: toRem(300),
|
||
maxWidth: '100%',
|
||
// 9:16 via the padding-top hack (177.78%) — see EmbedMediaLandscape.
|
||
height: 0,
|
||
paddingTop: '177.78%',
|
||
margin: '0 auto',
|
||
overflow: 'hidden',
|
||
backgroundColor: color.Surface.Container,
|
||
selectors: {
|
||
'&:fullscreen': { width: '100vw', height: '100vh', paddingTop: 0, margin: 0 },
|
||
},
|
||
},
|
||
]);
|
||
|
||
// Fills the aspect box; used for both the <button> facade and the <a> fallback.
|
||
export const EmbedFacade = style([
|
||
DefaultReset,
|
||
{
|
||
position: 'absolute',
|
||
inset: 0,
|
||
width: '100%',
|
||
height: '100%',
|
||
display: 'block',
|
||
padding: 0,
|
||
border: 'none',
|
||
background: 'none',
|
||
cursor: 'pointer',
|
||
|
||
':hover': {
|
||
filter: 'brightness(0.85)',
|
||
},
|
||
selectors: {
|
||
'&:focus-visible': {
|
||
outline: `2px solid ${color.Primary.Main}`,
|
||
outlineOffset: '-2px',
|
||
},
|
||
},
|
||
},
|
||
]);
|
||
|
||
export const EmbedIframe = style([
|
||
DefaultReset,
|
||
{
|
||
position: 'absolute',
|
||
inset: 0,
|
||
width: '100%',
|
||
height: '100%',
|
||
border: 0,
|
||
display: 'block',
|
||
},
|
||
]);
|
||
|
||
// Variable-height iframe that sizes itself (X/Twitter, Instagram, Reddit posts —
|
||
// height set inline). Capped ~550px (X/IG native max) + centered in the wide card.
|
||
export const EmbedIframeStatic = style([
|
||
DefaultReset,
|
||
{
|
||
width: '100%',
|
||
maxWidth: toRem(550),
|
||
margin: '0 auto',
|
||
border: 0,
|
||
display: 'block',
|
||
},
|
||
]);
|
||
|
||
export const EmbedPlaceholder = style([
|
||
DefaultReset,
|
||
{
|
||
position: 'absolute',
|
||
inset: 0,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
backgroundColor: '#111111',
|
||
color: '#ffffff',
|
||
fontSize: toRem(28),
|
||
},
|
||
]);
|
||
|
||
// Audio embeds (Spotify / SoundCloud) — full-width, short fixed height (set inline).
|
||
export const EmbedMediaAudio = style([
|
||
DefaultReset,
|
||
{
|
||
position: 'relative',
|
||
width: '100%',
|
||
overflow: 'hidden',
|
||
backgroundColor: color.Surface.Container,
|
||
},
|
||
]);
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 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',
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Steam card — full-width header/banner image + official store widget iframe
|
||
// ---------------------------------------------------------------------------
|
||
|
||
export const SteamBannerWrapper = style([
|
||
DefaultReset,
|
||
{
|
||
position: 'relative',
|
||
display: 'block',
|
||
width: '100%',
|
||
// Steam header capsules are 460×215 (~2.14:1); news banners vary but crop
|
||
// fine to the same ratio.
|
||
aspectRatio: '460 / 215',
|
||
overflow: 'hidden',
|
||
flexShrink: 0,
|
||
backgroundColor: '#0e1520',
|
||
cursor: 'pointer',
|
||
|
||
':hover': {
|
||
filter: 'brightness(0.9)',
|
||
},
|
||
},
|
||
]);
|
||
|
||
export const SteamBannerImg = style([
|
||
DefaultReset,
|
||
{
|
||
width: '100%',
|
||
height: '100%',
|
||
objectFit: 'cover',
|
||
objectPosition: 'center',
|
||
display: 'block',
|
||
},
|
||
]);
|
||
|
||
// The official Steam store widget is a compact banner (~646×190). Full-width,
|
||
// fixed height so the iframe doesn't collapse to its intrinsic size.
|
||
export const SteamWidget = style([
|
||
DefaultReset,
|
||
{
|
||
width: '100%',
|
||
height: toRem(190),
|
||
border: 0,
|
||
display: 'block',
|
||
borderRadius: config.radii.R300,
|
||
backgroundColor: '#1b2838',
|
||
marginTop: config.space.S100,
|
||
},
|
||
]);
|
||
|
||
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',
|
||
});
|
||
|
||
export const BadgeYouTubeShorts = style({
|
||
backgroundColor: '#FF0000',
|
||
color: '#ffffff',
|
||
});
|
||
|
||
export const BadgeTikTok = style({
|
||
backgroundColor: '#000000',
|
||
color: '#ffffff',
|
||
});
|
||
|
||
export const BadgeSoundCloud = style({
|
||
backgroundColor: '#ff5500',
|
||
color: '#ffffff',
|
||
});
|
||
|
||
export const BadgeDailymotion = style({
|
||
backgroundColor: '#0066dc',
|
||
color: '#ffffff',
|
||
});
|
||
|
||
export const BadgeStreamable = style({
|
||
backgroundColor: '#0f90fa',
|
||
color: '#ffffff',
|
||
});
|
||
|
||
export const BadgeAppleMusic = style({
|
||
backgroundColor: '#fa243c',
|
||
color: '#ffffff',
|
||
});
|
||
|
||
export const BadgeInstagram = style({
|
||
backgroundColor: '#e1306c',
|
||
color: '#ffffff',
|
||
});
|
||
|
||
export const BadgeBluesky = style({
|
||
backgroundColor: '#0085ff',
|
||
color: '#ffffff',
|
||
});
|
||
|
||
export const BadgeLoom = style({
|
||
backgroundColor: '#625df5',
|
||
color: '#ffffff',
|
||
});
|
||
|
||
export const BadgeKick = style({
|
||
backgroundColor: '#53fc18',
|
||
color: '#000000',
|
||
});
|
||
|
||
export const BadgeTidal = style({
|
||
backgroundColor: '#000000',
|
||
color: '#ffffff',
|
||
});
|
||
|
||
export const BadgeMixcloud = style({
|
||
backgroundColor: '#52aad8',
|
||
color: '#ffffff',
|
||
});
|
||
|
||
export const BadgeDeezer = style({
|
||
backgroundColor: '#a238ff',
|
||
color: '#ffffff',
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 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,
|
||
},
|
||
]);
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 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',
|
||
},
|
||
]);
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// GIF card (Giphy / Tenor)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
export const GifThumbnailWrapper = style([
|
||
DefaultReset,
|
||
{
|
||
position: 'relative',
|
||
width: '100%',
|
||
maxHeight: toRem(200),
|
||
overflow: 'hidden',
|
||
flexShrink: 0,
|
||
backgroundColor: color.Surface.Container,
|
||
cursor: 'pointer',
|
||
|
||
':hover': {
|
||
filter: 'brightness(0.9)',
|
||
},
|
||
},
|
||
]);
|
||
|
||
export const GifThumbnailImg = style([
|
||
DefaultReset,
|
||
{
|
||
width: '100%',
|
||
maxHeight: toRem(200),
|
||
objectFit: 'cover',
|
||
objectPosition: 'center',
|
||
display: 'block',
|
||
},
|
||
]);
|
||
|
||
export const GifBadge = style([
|
||
DefaultReset,
|
||
{
|
||
position: 'absolute',
|
||
top: config.space.S100,
|
||
right: config.space.S100,
|
||
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',
|
||
letterSpacing: '0.05em',
|
||
backgroundColor: 'rgba(0, 0, 0, 0.65)',
|
||
color: '#ffffff',
|
||
pointerEvents: 'none',
|
||
},
|
||
]);
|
||
|
||
export const BadgeGiphy = style({
|
||
backgroundColor: '#00c0c0',
|
||
color: '#ffffff',
|
||
});
|
||
|
||
export const BadgeTenor = style({
|
||
backgroundColor: '#0078d4',
|
||
color: '#ffffff',
|
||
});
|