feat(embeds): detailed Steam store / news / app-widget embeds

Recognize store.steampowered.com content URLs and render each richly, within
the existing privacy-first facade. 2-agent reviewed (both SHIP).

- getSteamTarget / steamWidgetEmbedUrl (videoEmbed.ts, +tests): classify
  /app/{id}, /news/app/{id}/view/{gid}, /(bundle|sub|dlc)/{id}; non-content
  pages (home/search/wishlist) and other hosts fall through to the generic card.
- SteamCard now dispatches:
  - app → OG capsule header + click-to-play facade → Steam's OFFICIAL store
    widget iframe (store.steampowered.com/widget/{id}): live region-aware price,
    discount %, Buy on Steam. Nothing loads from Steam until "Show price &
    store" is pressed; gated by the inlineMediaEmbeds setting. App pages use the
    wide card so the ~646px widget has room.
  - news → rich announcement card (banner + headline + body preview + link) —
    your example URL previously fell through to the plain generic card.
  - bundle/sub/dlc → the OG store card.

Grounded in our CSP: the widget works via frame-src https: (no infra change),
images route through the homeserver (img-src excludes Steam), and there is NO
client-side Steam API call (connect-src + Steam CORS both block it) — which is
also the honest ceiling: no review scores/genres client-side, price/buy come
from the official widget.

Runtime QA still needed: the live widget iframe rendering (height/fit) can't be
verified headlessly.

Gates: tsc 0, eslint 0, prettier clean, 912 tests, build ok.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 21:41:06 -04:00
co-authored by Claude Opus 4.8
parent 8e02cef658
commit ef82650cf7
4 changed files with 293 additions and 15 deletions
@@ -419,6 +419,56 @@ export const BadgeSteam = style({
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,