Skip to content

feat(packages): resolve the poster from the store, not from markup - #2010

Closed
decepulis wants to merge 2 commits into
claude/poster-wrapper-convergefrom
claude/poster-store-config-c31gf2
Closed

feat(packages): resolve the poster from the store, not from markup#2010
decepulis wants to merge 2 commits into
claude/poster-wrapper-convergefrom
claude/poster-store-config-c31gf2

Conversation

@decepulis

@decepulis decepulis commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #2009. Closes #1798.

The problem

The poster was configured in markup: an img carrying slot="poster" in HTML, a poster prop on the React skin. That put content configuration in the skin's hands, made it unreachable from a hand-authored layout, and left the store's resolved poster — user, media, user default — with no reader.

The change

Before, in HTML:

<video-skin>
  <video src="video.mp4" playsinline></video>
  <img slot="poster" src="poster.jpg" alt="Video poster" />
</video-skin>

After:

<video-player poster="poster.jpg">
  <video-skin>
    <video src="video.mp4" playsinline></video>
  </video-skin>
</video-player>

Before, in React:

<Player.Provider>
  <VideoSkin poster="poster.jpg">
    <Video src="video.mp4" playsInline />
  </VideoSkin>
</Player.Provider>

After:

<Player.Provider poster="poster.jpg">
  <VideoSkin>
    <Video src="video.mp4" playsInline />
  </VideoSkin>
</Player.Provider>

PosterCore composes playback and metadata, so the chain is written and tested once and every consumer inherits it: a packaged skin, an ejected one, a layout you wrote yourself. BaseVideoSkinProps is gone — the video skins take the same three props the audio ones always did.

Rendering an image without taking it away

internal/design/ui/poster.md treats author control of the image as the point of the component, and that still holds. The store supplies a default, it doesn't take over.

React's Poster stays the img it was. src fills in from the store when none is passed; every other image attribute is untouched.

<Poster />
<Poster srcSet="poster-480.jpg 480w, poster-1080.jpg 1080w" sizes="100vw" />
<Poster render={(props) => <Image {...props} alt="" fill />} />

It also gains alt="" — it had no alt at all, so screen readers announced the URL.

HTML is the harder half, because media-poster never rendered an image; it positioned one you supplied. It now owns an img carrying part="img" in a shadow root, beside a slot, the same shape media-thumbnail already uses. Fill the slot and the owned image steps aside:

<media-poster>
  <img src="poster.jpg" srcset="poster-480.jpg 480w" alt="Keynote speaker" />
</media-poster>

Hiding it isn't enough on its own: an image that isn't rendered still downloads, which is not hypothetical now that Mux and Vimeo donate posters. The src is withheld too.

Why assignedElements({ flatten: true })

A skin forwards its own slot carrying name="poster" into the element, and that empty slot is itself an assigned node — so counting assigned nodes finds one whether or not you supplied anything. Verified in Chromium:

author slots an image  →  assigned: 1 (the skin's slot), flattened: [IMG]
author slots nothing   →  assigned: 1 (the skin's slot), flattened: []

Same reason the owned image sits beside the slot rather than inside it as fallback content: fallback renders only when nothing is assigned, and inside a skin something always is.

Why data-loaded moved to the host

A selector can't reach past ::part() to read an attribute. media-poster:not([data-loaded])::part(img) is expressible; ::part(img):not([data-loaded]) is not. Skins reach the owned image as a part and leave yours alone, which is what held the blur-up sequence together before.

Fixes that fall out

  • Ejected Tailwind HTML skins rendered an unpositioned poster. The shadow branch styled only ::slotted(img), but site/scripts/ejected-skins/html.ts inlines a literal img where the slot used to be, which ::slotted never matches. All three variants — part, slotted, descendant — are emitted now.
  • The React skins' poster was styled through .media-default-skin > img. It's the explicit .media-poster now, matching the .media-poster-placeholder its sibling uses.

Testing

Suites green per package: core 1411, react 406, html 311, media 539, utils 419, skins 4, site 530. pnpm typecheck reports only the 24 pre-existing packages/store test errors; check:workspace and astro check clean.

New coverage: PosterCore resolving the URL; the element rendering it, stepping aside for a slotted image without fetching its own, taking back over when that image is removed, and reporting load on the host; and the React side filling in src, preferring one passed to it, defaulting alt, and handing the resolved src to a render override.

Verified in Chromium across both skins and both stylings: the store-resolved poster paints over the poster placeholder, a slotted image replaces it with the owned one hidden and unfetched, and Tailwind compiles the [&::part(img)] variants that reach it.

🤖 Generated with Claude Code

https://claude.ai/code/session_019fBNWkGLWPzsP4QK6w7u2L

@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
v10-sandbox Ready Ready Preview Aug 10, 2026 8:58pm

Request Review

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle Size Report

🎨 @videojs/html

Path Base initial PR initial Initial diff Lazy diff
/video/minimal-skin.tailwind 56.19 kB 56.53 kB +348 B (+0.6%) 0 B
/ui/poster 8.45 kB 8.07 kB -391 B (-4.5%) -54.60 kB
Small changes (15, ≤ 300 B)
Path Base initial PR initial Initial diff Lazy diff
/video/minimal-skin.css 5.91 kB 5.94 kB +26 B (+0.4%)
/video/skin.css 5.92 kB 5.94 kB +27 B (+0.4%)
/video/minimal-skin 55.59 kB 55.73 kB +142 B (+0.2%) 0 B
/video/skin 55.74 kB 56.00 kB +264 B (+0.5%) 0 B
/video/skin.tailwind 56.43 kB 56.62 kB +190 B (+0.3%) 0 B
/live-video/minimal-skin.css 5.91 kB 5.94 kB +26 B (+0.4%)
/live-video/skin.css 5.92 kB 5.94 kB +27 B (+0.4%)
/live-video/minimal-skin 52.03 kB 52.08 kB +48 B (+0.1%) 0 B
/live-video/minimal-skin.tailwind 52.47 kB 52.71 kB +238 B (+0.4%) 0 B
/live-video/skin 54.28 kB 54.40 kB +125 B (+0.2%) 0 B
/live-video/skin.tailwind 54.85 kB 54.96 kB +111 B (+0.2%) 0 B
/video (default) 55.73 kB 55.88 kB +146 B (+0.3%) 0 B
/video (default + hls) 191.94 kB 192.11 kB +171 B (+0.1%) 0 B
/video (minimal) 55.63 kB 55.76 kB +139 B (+0.2%) 0 B
/video (minimal + hls) 191.90 kB 191.95 kB +55 B (+0.0%) 0 B
Presets (7)
Entry Initial Lazy
/video (default) 55.88 kB 54.60 kB
/video (default + hls) 192.11 kB 54.60 kB
/video (minimal) 55.76 kB 54.60 kB
/video (minimal + hls) 191.95 kB 54.60 kB
/audio (default) 47.83 kB 54.60 kB
/audio (minimal) 44.32 kB 54.60 kB
/background 5.59 kB 54.60 kB
Media (13)
Entry Initial Lazy
/media/background-video 1.14 kB
/media/container 2.58 kB 54.60 kB
/media/dash-video 211.37 kB
/media/google-cast 6.44 kB
/media/hlsjs-video 138.24 kB
/media/mux-audio 139.20 kB
/media/mux-data 154.03 kB
/media/mux-video 139.63 kB
/media/native-hls-video 5.32 kB
/media/simple-hls-audio-only 21.62 kB
/media/simple-hls-video 27.02 kB
/media/vimeo-video 12.60 kB
/media/youtube-video 6.67 kB
Players (5)
Entry Initial Lazy
/video/player 10.13 kB 54.60 kB
/audio/player 7.13 kB 54.60 kB
/background/player 5.32 kB 54.60 kB
/live-video/player 9.53 kB 54.60 kB
/live-audio/player 7.15 kB 54.60 kB
Skins (30)
Entry Type Initial Lazy
/video/minimal-skin.css css 5.94 kB
/video/skin.css css 5.94 kB
/video/minimal-skin js 55.73 kB 54.60 kB
/video/minimal-skin.tailwind js 56.53 kB 54.60 kB
/video/skin js 56.00 kB 54.60 kB
/video/skin.tailwind js 56.62 kB 54.60 kB
/audio/minimal-skin.css css 4.11 kB
/audio/skin.css css 4.05 kB
/audio/minimal-skin js 44.28 kB 54.60 kB
/audio/minimal-skin.tailwind js 44.79 kB 54.60 kB
/audio/skin js 47.82 kB 54.60 kB
/audio/skin.tailwind js 48.33 kB 54.60 kB
/background/skin.css css 133 B
/background/skin js 1.14 kB
/live-video/minimal-skin.css css 5.94 kB
/live-video/skin.css css 5.94 kB
/live-video/minimal-skin js 52.08 kB 54.60 kB
/live-video/minimal-skin.tailwind js 52.71 kB 54.60 kB
/live-video/skin js 54.40 kB 54.60 kB
/live-video/skin.tailwind js 54.96 kB 54.60 kB
/live-audio/minimal-skin.css css 4.11 kB
/live-audio/skin.css css 4.05 kB
/live-audio/minimal-skin js 36.51 kB 54.60 kB
/live-audio/minimal-skin.tailwind js 36.04 kB 54.60 kB
/live-audio/skin js 40.31 kB 54.60 kB
/live-audio/skin.tailwind js 39.79 kB 54.60 kB
/global.css css 183 B
/shared.css css 104 B
/tailwind.css css 161 B
/skin-element js 1.39 kB
UI Components (40)
Entry Initial Lazy
/ui/airplay-button 10.90 kB 54.60 kB
/ui/alert-dialog 4.14 kB
/ui/alert-dialog-close 1.94 kB
/ui/alert-dialog-description 1.61 kB
/ui/alert-dialog-title 1.61 kB
/ui/audio-track-radio-group 10.69 kB 54.60 kB
/ui/buffering-indicator 8.17 kB
/ui/captions-button 11.19 kB 54.60 kB
/ui/captions-radio-group 10.72 kB 54.60 kB
/ui/cast-button 10.90 kB 54.60 kB
/ui/compounds 35.92 kB 54.60 kB
/ui/controls 8.33 kB 54.60 kB
/ui/error-dialog 11.30 kB 54.60 kB
/ui/fullscreen-button 10.87 kB 54.60 kB
/ui/hotkey 9.45 kB
/ui/menu 20.62 kB 54.60 kB
/ui/mute-button 10.86 kB 54.60 kB
/ui/pip-button 10.86 kB 54.60 kB
/ui/placeholder 7.83 kB
/ui/play-button 10.88 kB 54.60 kB
/ui/playback-rate-button 11.05 kB 54.60 kB
/ui/playback-rate-radio-group 10.46 kB 54.60 kB
/ui/popover 7.61 kB
/ui/poster 8.07 kB
/ui/quality-radio-group 11.23 kB 54.60 kB
/ui/seek-button 10.90 kB 54.60 kB
/ui/seek-indicator 12.61 kB
/ui/seek-indicator-value 1.02 kB
/ui/slider 11.74 kB 54.60 kB
/ui/status-announcer 10.25 kB 54.60 kB
/ui/status-indicator 13.10 kB 54.60 kB
/ui/status-indicator-value 1.02 kB
/ui/thumbnail 9.29 kB
/ui/time 10.44 kB 54.60 kB
/ui/time-slider 14.56 kB 54.60 kB
/ui/tooltip 8.89 kB 54.60 kB
/ui/volume-indicator 13.14 kB 54.60 kB
/ui/volume-indicator-fill 1.03 kB
/ui/volume-indicator-value 1.02 kB
/ui/volume-slider 12.33 kB 54.60 kB
⚛️ @videojs/react — 24 small size changes
Path Base initial PR initial Initial diff Lazy diff
/video/minimal-skin.css 5.81 kB 5.83 kB +22 B (+0.4%)
/video/skin.css 5.81 kB 5.83 kB +19 B (+0.3%)
/video/minimal-skin 43.91 kB 43.89 kB -11 B (-0.0%) 0 B
/video/minimal-skin.tailwind 50.01 kB 50.11 kB +100 B (+0.2%) 0 B
/video/skin 43.59 kB 43.55 kB -39 B (-0.1%) 0 B
/video/skin.tailwind 49.77 kB 49.82 kB +57 B (+0.1%) 0 B
/live-video/minimal-skin.css 5.81 kB 5.83 kB +22 B (+0.4%)
/live-video/skin.css 5.81 kB 5.83 kB +19 B (+0.3%)
/live-video/minimal-skin 38.81 kB 38.81 kB -6 B (-0.0%) 0 B
/live-video/minimal-skin.tailwind 44.95 kB 45.07 kB +130 B (+0.3%) 0 B
/live-video/skin 38.82 kB 38.82 kB -7 B (-0.0%) 0 B
/live-video/skin.tailwind 45.08 kB 45.10 kB +25 B (+0.1%) 0 B
/live-audio/minimal-skin 26.50 kB 26.50 kB -2 B (-0.0%) 0 B
/live-audio/skin 26.58 kB 26.57 kB -11 B (-0.0%) 0 B
/ui/cast-button 10.14 kB 10.13 kB -5 B (-0.0%) 0 B
/ui/popover 7.47 kB 7.47 kB +1 B (+0.0%)
/ui/poster 7.00 kB 7.06 kB +65 B (+0.9%)
/ui/status-announcer 9.77 kB 9.77 kB -4 B (-0.0%) 0 B
/ui/tooltip 8.21 kB 8.21 kB +1 B (+0.0%)
/ui/volume-indicator 12.46 kB 12.45 kB -10 B (-0.1%) 0 B
/video (default) 43.70 kB 43.68 kB -19 B (-0.0%) 0 B
/video (default + hls) 178.67 kB 178.59 kB -78 B (-0.0%) 0 B
/video (minimal) 43.98 kB 43.95 kB -27 B (-0.1%) 0 B
/video (minimal + hls) 178.96 kB 178.90 kB -67 B (-0.0%) 0 B
Presets (7)
Entry Initial Lazy
/video (default) 43.68 kB 54.60 kB
/video (default + hls) 178.59 kB 54.60 kB
/video (minimal) 43.95 kB 54.60 kB
/video (minimal + hls) 178.90 kB 54.60 kB
/audio (default) 35.86 kB 54.60 kB
/audio (minimal) 35.85 kB 54.60 kB
/background 581 B
Media (12)
Entry Initial
/media/background-video 394 B
/media/dash-video 209.61 kB
/media/google-cast 5.34 kB
/media/hlsjs-video 136.61 kB
/media/mux-audio 137.49 kB
/media/mux-data 152.88 kB
/media/mux-video 137.71 kB
/media/native-hls-video 3.53 kB
/media/simple-hls-audio-only 19.88 kB
/media/simple-hls-video 25.36 kB
/media/vimeo-video 10.77 kB
/media/youtube-video 4.78 kB
Skins (27)
Entry Type Initial Lazy
/tailwind.css css 161 B
/video/minimal-skin.css css 5.83 kB
/video/skin.css css 5.83 kB
/video/minimal-skin js 43.89 kB 54.60 kB
/video/minimal-skin.tailwind js 50.11 kB 54.60 kB
/video/skin js 43.55 kB 54.60 kB
/video/skin.tailwind js 49.82 kB 54.60 kB
/audio/minimal-skin.css css 3.97 kB
/audio/skin.css css 3.90 kB
/audio/minimal-skin js 35.77 kB 54.60 kB
/audio/minimal-skin.tailwind js 38.28 kB 54.60 kB
/audio/skin js 35.77 kB 54.60 kB
/audio/skin.tailwind js 40.10 kB 54.60 kB
/background/skin.css css 90 B
/background/skin js 273 B
/live-video/minimal-skin.css css 5.83 kB
/live-video/skin.css css 5.83 kB
/live-video/minimal-skin js 38.81 kB 54.60 kB
/live-video/minimal-skin.tailwind js 45.07 kB 54.60 kB
/live-video/skin js 38.82 kB 54.60 kB
/live-video/skin.tailwind js 45.10 kB 54.60 kB
/live-audio/minimal-skin.css css 3.97 kB
/live-audio/skin.css css 3.90 kB
/live-audio/minimal-skin js 26.50 kB 54.60 kB
/live-audio/minimal-skin.tailwind js 29.78 kB 54.60 kB
/live-audio/skin js 26.57 kB 54.60 kB
/live-audio/skin.tailwind js 29.96 kB 54.60 kB
UI Components (34)
Entry Initial Lazy
/ui/airplay-button 10.16 kB 54.60 kB
/ui/alert-dialog 3.25 kB
/ui/audio-track 7.62 kB 54.60 kB
/ui/buffering-indicator 7.39 kB
/ui/captions-button 10.14 kB 54.60 kB
/ui/captions-radio-group 7.75 kB 54.60 kB
/ui/cast-button 10.13 kB 54.60 kB
/ui/controls 7.12 kB
/ui/error-dialog 10.08 kB 54.60 kB
/ui/fullscreen-button 10.10 kB 54.60 kB
/ui/gesture 7.51 kB
/ui/hotkey 7.94 kB
/ui/live-button 8.54 kB 54.60 kB
/ui/menu 20.73 kB 54.60 kB
/ui/mute-button 10.09 kB 54.60 kB
/ui/pip-button 10.11 kB 54.60 kB
/ui/placeholder 6.96 kB
/ui/play-button 10.08 kB 54.60 kB
/ui/playback-rate 6.93 kB
/ui/playback-rate-button 10.09 kB 54.60 kB
/ui/popover 7.47 kB
/ui/poster 7.06 kB
/ui/quality 8.14 kB 54.60 kB
/ui/seek-button 10.12 kB 54.60 kB
/ui/seek-indicator 11.89 kB
/ui/slider 12.09 kB 54.60 kB
/ui/status-announcer 9.77 kB 54.60 kB
/ui/status-indicator 12.37 kB 54.60 kB
/ui/thumbnail 8.21 kB
/ui/time 9.42 kB 54.60 kB
/ui/time-slider 12.13 kB 54.60 kB
/ui/tooltip 8.21 kB
/ui/volume-indicator 12.45 kB 54.60 kB
/ui/volume-slider 11.52 kB 54.60 kB
🧩 @videojs/core — 1 small size change
Path Base initial PR initial Initial diff Lazy diff
. 11.66 kB 11.68 kB +29 B (+0.2%)
Entries (76)
Entry Initial Lazy
. 11.68 kB
/dom 20.09 kB
/components 900 B
/i18n 3.06 kB 54.60 kB
/i18n/locales/all 33.25 kB
/i18n/locales/ar 1.17 kB
/i18n/locales/az 1.07 kB
/i18n/locales/bg 1.24 kB
/i18n/locales/bn 1.26 kB
/i18n/locales/bs 971 B
/i18n/locales/ca 1.03 kB
/i18n/locales/cs 1.01 kB
/i18n/locales/cy 1005 B
/i18n/locales/da 964 B
/i18n/locales/de 1.05 kB
/i18n/locales/el 1.42 kB
/i18n/locales/en 753 B
/i18n/locales/es 981 B
/i18n/locales/et 1.01 kB
/i18n/locales/eu 1001 B
/i18n/locales/fa 1.17 kB
/i18n/locales/fi 1015 B
/i18n/locales/fr 1.03 kB
/i18n/locales/gd 1.05 kB
/i18n/locales/gl 978 B
/i18n/locales/he 1.09 kB
/i18n/locales/hi 1.26 kB
/i18n/locales/hr 998 B
/i18n/locales/hu 1.04 kB
/i18n/locales/id 879 B
/i18n/locales/it 995 B
/i18n/locales/ja 1.15 kB
/i18n/locales/ko 1.10 kB
/i18n/locales/lt 997 B
/i18n/locales/lv 1.04 kB
/i18n/locales/mr 1.27 kB
/i18n/locales/nb 948 B
/i18n/locales/ne 1.25 kB
/i18n/locales/nl 986 B
/i18n/locales/nn 956 B
/i18n/locales/oc 1.04 kB
/i18n/locales/pl 1.08 kB
/i18n/locales/pt 1008 B
/i18n/locales/pt-BR 1008 B
/i18n/locales/pt-PT 993 B
/i18n/locales/ro 1.02 kB
/i18n/locales/ru 1.33 kB
/i18n/locales/sk 1.05 kB
/i18n/locales/sl 1008 B
/i18n/locales/sr 1.02 kB
/i18n/locales/sv 986 B
/i18n/locales/te 1.28 kB
/i18n/locales/th 1.24 kB
/i18n/locales/tr 1.03 kB
/i18n/locales/uk 1.34 kB
/i18n/locales/vi 1.05 kB
/i18n/locales/zh 987 B
/i18n/locales/zh-CN 987 B
/i18n/locales/zh-TW 987 B
/i18n/text/airplay 101 B
/i18n/text/buttons 137 B
/i18n/text/captions 93 B
/i18n/text/cast 114 B
/i18n/text/common 90 B
/i18n/text/container 75 B
/i18n/text/errors 285 B
/i18n/text/fullscreen 98 B
/i18n/text/live 126 B
/i18n/text/menu 249 B
/i18n/text/pip 101 B
/i18n/text/playback 80 B
/i18n/text/seek 105 B
/i18n/text/slider 65 B
/i18n/text/status 222 B
/i18n/text/time 214 B
/i18n/text/volume 133 B
🏷️ @videojs/element — no changes
Entries (2)
Entry Initial
. 996 B
/context 943 B
📦 @videojs/store — no changes
Entries (3)
Entry Initial
. 1.72 kB
/html 703 B
/react 366 B
🔧 @videojs/utils — no changes
Entries (13)
Entry Initial
/array 104 B
/dom 3.29 kB
/events 319 B
/function 369 B
/i18n 44 B
/jwt 176 B
/object 508 B
/predicate 265 B
/percent 281 B
/string 239 B
/style 224 B
/time 1013 B
/number 158 B
📦 @videojs/jsx — no changes
Entries (3)
Entry Initial
. 393 B
/jsx-runtime 371 B
/jsx-dev-runtime 375 B
📦 @videojs/media — no changes
Entries (15)
Entry Initial
. 993 B
/dom/audio-host 1.10 kB
/dom/custom-media-element 2.09 kB
/dom/dash 209.25 kB
/dom/google-cast 4.06 kB
/dom/hls-js 136.18 kB
/dom/media-host 1.20 kB
/dom/media-played-ranges 576 B
/dom/mux 159.62 kB
/dom/native-hls 3.01 kB
/dom/simple-hls 24.87 kB
/dom/simple-hls-audio-only 19.37 kB
/dom/video-host 1.39 kB
/dom/vimeo 10.22 kB
/dom/youtube 4.16 kB
📦 @videojs/spf — no changes
Entries (5)
Entry Initial
. 4.53 kB
/dom 6.57 kB
/hls 21.67 kB
/media-tracks 504 B
/background-video 14.79 kB

ℹ️ How to interpret

Each entry is independently bundled, minified, and brotli-compressed. Initial size includes its static import graph; lazy dynamic chunks are reported separately.

Entries are not additive because their dependency graphs overlap. Preset rows represent realistic combined bundles.

Changes of 300 B or less across initial, lazy, and total size are collapsed, not discarded.

Run pnpm size locally to check current initial sizes.

@decepulis
decepulis force-pushed the claude/poster-wrapper-converge branch from ea57f4e to 1211fed Compare August 8, 2026 00:31
@decepulis
decepulis force-pushed the claude/poster-store-config-c31gf2 branch from b875d68 to 0c81705 Compare August 8, 2026 00:54
@decepulis decepulis changed the title feat(packages): resolve the poster from the store, not the skin feat(packages): resolve the poster from the store, not from markup Aug 8, 2026
@decepulis
decepulis force-pushed the claude/poster-wrapper-converge branch from 1211fed to a7e5054 Compare August 10, 2026 20:53
@decepulis
decepulis force-pushed the claude/poster-store-config-c31gf2 branch from 0c81705 to e807ada Compare August 10, 2026 20:53
claude added 2 commits August 10, 2026 20:54
The poster URL was configured in markup — an `<img slot="poster">` in
HTML, a `poster` prop on the React skin. That left content configuration
in the skin's hands, made it unreachable from a hand-authored layout, and
left the store's resolved `poster` with no reader at all.

The URL now comes from the metadata feature, so a packaged skin, an
ejected one, and a layout you wrote yourself all inherit the same
resolution chain. Rendering an image is what that requires, so each
binding renders one — as a default, not a takeover:

- React renders the `<img>` it always did. `src` fills in from the store
  when you do not pass one; every other image attribute is untouched, and
  `render` receives the resolved `src` so it can reach `next/image`.
- HTML owns an `<img part="img">` in a shadow root, beside the slot.
  Supply an image and the owned one is hidden and its `src` removed —
  an image that is not rendered still downloads.

`data-loaded` moves to the host, because a selector cannot reach past
`::part()` to read an attribute. The skins style three images: the owned
part, one slotted through a skin, and one inlined by an ejected skin.
That third selector fixes a pre-existing bug where an ejected Tailwind
skin rendered the poster unpositioned.

Refs #1798
Rewrite the poster reference around the image the component renders: how
to supply your own, how to reach the owned one as a part, and what
`data-loaded` is for. Move the poster URL onto the provider in the demos,
the home page, and the skin examples.

Refs #1798

Copy link
Copy Markdown
Collaborator Author

Closing in favour of #2039, which takes the compound-image route: <media-poster> coordinates a child image the way <picture> does, instead of owning one in a shadow root beside a slot.

What carried over from here: the store-resolved poster on the metadata feature, BaseVideoSkinProps going away, poster moving to the provider, the React skins styled through an explicit .media-poster, and the ejected-skin fix for an inlined <img> that ::slotted never matched.

What #2039 does differently: no owned image, so no part="img" and no second image in the tree — which also means no ::part() styling contract and no withheld src on a hidden download. The data-loaded-on-the-host reasoning from here holds either way and stayed.

The alt="" default from this PR is in #2039 too. Resolving alt through the store was considered and dropped: filling in a src has <picture> as precedent, describing an image does not.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Communicate Media Metadata to the UI

2 participants