Skip to content

feat(packages): add title component - #1997

Draft
decepulis wants to merge 5 commits into
mainfrom
claude/title-state-store-yzkrvh
Draft

feat(packages): add title component#1997
decepulis wants to merge 5 commits into
mainfrom
claude/title-state-store-yzkrvh

Conversation

@decepulis

@decepulis decepulis commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Refs #1123
Refs #1798

Summary

Adds <media-title> and <Title>, the UI for the content title that #1946 taught the store to resolve. This is the component layer only — no skin templates or skin styles, which are their own can of worms. #2036 stacks on this and puts the title in the video skins.

Revives the salvageable half of the closed #1603. That PR shipped its own metadata state, which #1875 and #1946 have since replaced with a different design, so only its UI survives — and rewritten, not ported.

Changes

  • TitleCore projects metadata, playback, and controls state into title, hasTitle, and visible.
  • The title shows when a title exists, controls are visible, and playback is paused — Sam's call from the feat: add title display and media metadata state #1603 review.
  • Two data attributes: data-has-title to remove the element when there is nothing to show, data-visible for the transition.
  • metadataFeature is the only feature the component needs. playbackFeature and controlsFeature are each used when present; a player without them has nothing that hides the title, so it stays up for as long as one resolves.
  • Granular registration only: @videojs/html/ui/title. No stock skin renders it yet, so no skin entry pays for it.
  • API reference page with HTML and React demos.
Notable decisions

The core owns the visibility policy, not the skin CSS. #1603 decided visibility in a stylesheet, reaching sideways into the controls' internals: .media-controls[data-visible]:has([data-paused]) ~ .media-title. That [data-paused] comes from media-play-button, so the title silently stopped working in any layout without a play button nested inside its controls. PosterCore and BufferingIndicatorCore both compute their own visible and expose data-visible for CSS to react to; this follows them.

Optional features degrade in the adapters, not in the core. TitleMediaState requires all three fields, and the HTML and React adapters substitute paused: true / controlsVisible: true when a slice is absent. That is the shape TimeSliderElement already uses for its optional buffer slice (...(buffer ?? { buffered: [], seekable: [] })), and it keeps the branching out of the core.

No ARIA. #1603 put aria-label (the title text) and a conditional aria-hidden on a plain <span>. aria-label on a role-less element is invalid and ignored by most AT, and it duplicated the visible text. The text node is already in the accessibility tree. The label prop existed only to feed that attribute and is gone with it.

The component owns its text; no slot. Authors override through contentTitle / content-title / setContentTitle. A DOM-content override would be a second override mechanism competing with the precedence chain internal/design/store/resolved-feature-state.md defines, and primitives in this codebase take no slots.

Deliberately not here: skin templates and styles (#2036), and naming the player region from the title (the container has its own don't-clobber-the-author logic in both platforms; worth its own change).

Testing

pnpm -F @videojs/core test src/core/ui/title · pnpm -F @videojs/html test src/ui/title · pnpm -F @videojs/react test src/ui/title — 24 tests.

Full suites green: core 1414, html 314, react 403. pnpm build, pnpm typecheck, pnpm lint, pnpm check:workspace (10/10), and pnpm -F site astro check (0 errors) all pass.

The HTML element test attaches a fake media and drives paused through real play/pause events rather than poking store internals. It also covers the two reduced compositions — no controlsFeature (the audio presets) and metadataFeature alone (the metadata sandbox template) — because both are real consumers, not hypotheticals.

Merged with main rather than rebased; the only conflict was site/src/docs.config.ts, where the API reference grew nested groups. Title sits alphabetically under UI Components.


Generated by Claude Code

claude added 2 commits August 6, 2026 22:48
Add `<media-title>` and `<Title />`, which display the content title the
metadata feature resolves.

`TitleCore` reads metadata, controls, and playback state and exposes
`hasTitle` and `visible` as data attributes, so skins react to
`[data-visible]` rather than reaching across the DOM for playback state.
The title shows when a title exists, controls are visible, and playback is
paused. Controls and playback are optional: a player composed of
`metadataFeature` alone has no show-and-hide choreography to join, so the
title stays visible for as long as one resolves.

The component owns its text content. Authors set the title through
`content-title` / `contentTitle` or `setContentTitle`, keeping the
resolution order defined in the resolved-feature-state record as the only
way to supply one.

Refs #1123

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118ZxrT7hBHhZ57GdgCTvUy
Document the component, the two data attributes skins style against, and
how a title reaches the player. Include HTML and React demos that key
their transition off `data-visible` so the styling contract is exercised
before a skin depends on it.

Refs #1123

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118ZxrT7hBHhZ57GdgCTvUy
@vercel

vercel Bot commented Aug 6, 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 10:44pm

Request Review

@netlify

netlify Bot commented Aug 6, 2026

Copy link
Copy Markdown

Deploy Preview for vjs10-site ready!

Name Link
🔨 Latest commit 560b7c3
🔍 Latest deploy log https://app.netlify.com/projects/vjs10-site/deploys/6a7a542a3648850008efc7f6
😎 Deploy Preview https://deploy-preview-1997--vjs10-site.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle Size Report

🎨 @videojs/html

Path Base initial PR initial Initial diff Lazy diff
/ui/title 7.72 kB new
/media/hlsjs-video 138.24 kB 140.20 kB +1.96 kB (+1.4%)
/media/mux-audio 139.07 kB 140.92 kB +1.86 kB (+1.3%)
/media/mux-video 139.27 kB 141.13 kB +1.86 kB (+1.3%)
/media/native-hls-video 5.32 kB 7.51 kB +2.19 kB (+41.2%)
/video (default + hls) 192.01 kB 193.91 kB +1.89 kB (+1.0%) 0 B
/video (minimal + hls) 191.44 kB 193.44 kB +2.00 kB (+1.0%) 0 B
Small changes (9, ≤ 300 B)
Path Base initial PR initial Initial diff Lazy diff
/video/player 10.01 kB 10.02 kB +6 B (+0.1%) 0 B
/audio/minimal-skin 44.07 kB 44.16 kB +87 B (+0.2%) 0 B
/ui/fullscreen-button 10.74 kB 10.74 kB -3 B (-0.0%) 0 B
/ui/pip-button 10.74 kB 10.73 kB -8 B (-0.1%) 0 B
/ui/playback-rate-button 10.93 kB 10.92 kB -11 B (-0.1%) 0 B
/media/dash-video 211.28 kB 211.37 kB +87 B (+0.0%)
/media/vimeo-video 12.57 kB 12.58 kB +6 B (+0.0%)
/media/youtube-video 6.67 kB 6.67 kB +4 B (+0.1%)
/audio (minimal) 44.08 kB 44.18 kB +97 B (+0.2%) 0 B
Presets (7)
Entry Initial Lazy
/video (default) 55.76 kB 56.93 kB
/video (default + hls) 193.91 kB 56.93 kB
/video (minimal) 55.42 kB 56.93 kB
/video (minimal + hls) 193.44 kB 56.93 kB
/audio (default) 47.93 kB 56.93 kB
/audio (minimal) 44.18 kB 56.93 kB
/background 5.59 kB 56.93 kB
Media (13)
Entry Initial Lazy
/media/background-video 1.14 kB
/media/container 2.58 kB 56.93 kB
/media/dash-video 211.37 kB
/media/google-cast 6.44 kB
/media/hlsjs-video 140.20 kB
/media/mux-audio 140.92 kB
/media/mux-data 154.03 kB
/media/mux-video 141.13 kB
/media/native-hls-video 7.51 kB
/media/simple-hls-audio-only 21.62 kB
/media/simple-hls-video 27.04 kB
/media/vimeo-video 12.58 kB
/media/youtube-video 6.67 kB
Players (5)
Entry Initial Lazy
/video/player 10.02 kB 56.93 kB
/audio/player 6.99 kB 56.93 kB
/background/player 5.32 kB 56.93 kB
/live-video/player 9.42 kB 56.93 kB
/live-audio/player 7.01 kB 56.93 kB
Skins (30)
Entry Type Initial Lazy
/video/minimal-skin.css css 5.92 kB
/video/skin.css css 5.92 kB
/video/minimal-skin js 55.36 kB 56.93 kB
/video/minimal-skin.tailwind js 55.97 kB 56.93 kB
/video/skin js 55.74 kB 56.93 kB
/video/skin.tailwind js 56.44 kB 56.93 kB
/audio/minimal-skin.css css 4.11 kB
/audio/skin.css css 4.05 kB
/audio/minimal-skin js 44.16 kB 56.93 kB
/audio/minimal-skin.tailwind js 44.66 kB 56.93 kB
/audio/skin js 47.92 kB 56.93 kB
/audio/skin.tailwind js 48.47 kB 56.93 kB
/background/skin.css css 133 B
/background/skin js 1.14 kB
/live-video/minimal-skin.css css 5.92 kB
/live-video/skin.css css 5.92 kB
/live-video/minimal-skin js 51.67 kB 56.93 kB
/live-video/minimal-skin.tailwind js 52.26 kB 56.93 kB
/live-video/skin js 54.20 kB 56.93 kB
/live-video/skin.tailwind js 54.74 kB 56.93 kB
/live-audio/minimal-skin.css css 4.11 kB
/live-audio/skin.css css 4.05 kB
/live-audio/minimal-skin js 36.44 kB 56.93 kB
/live-audio/minimal-skin.tailwind js 35.83 kB 56.93 kB
/live-audio/skin js 40.38 kB 56.93 kB
/live-audio/skin.tailwind js 39.88 kB 56.93 kB
/global.css css 183 B
/shared.css css 104 B
/tailwind.css css 161 B
/skin-element js 1.45 kB
UI Components (40)
Entry Initial Lazy
/ui/airplay-button 10.79 kB 56.93 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.56 kB 56.93 kB
/ui/buffering-indicator 8.04 kB
/ui/captions-button 11.06 kB 56.93 kB
/ui/captions-radio-group 10.62 kB 56.93 kB
/ui/cast-button 10.77 kB 56.93 kB
/ui/compounds 35.98 kB 56.93 kB
/ui/controls 8.21 kB 56.93 kB
/ui/error-dialog 11.20 kB 56.93 kB
/ui/fullscreen-button 10.74 kB 56.93 kB
/ui/hotkey 9.32 kB
/ui/menu 20.50 kB 56.93 kB
/ui/mute-button 10.76 kB 56.93 kB
/ui/pip-button 10.73 kB 56.93 kB
/ui/play-button 10.73 kB 56.93 kB
/ui/playback-rate-button 10.92 kB 56.93 kB
/ui/playback-rate-radio-group 10.33 kB 56.93 kB
/ui/popover 7.61 kB
/ui/poster 8.40 kB 56.93 kB
/ui/quality-radio-group 11.12 kB 56.93 kB
/ui/seek-button 10.79 kB 56.93 kB
/ui/seek-indicator 12.51 kB
/ui/seek-indicator-value 1.02 kB
/ui/slider 11.60 kB 56.93 kB
/ui/status-announcer 10.14 kB 56.93 kB
/ui/status-indicator 13.02 kB 56.93 kB
/ui/status-indicator-value 1.02 kB
/ui/thumbnail 9.17 kB
/ui/time 10.53 kB 56.93 kB
/ui/time-slider 14.51 kB 56.93 kB
/ui/title 7.72 kB
/ui/tooltip 8.89 kB 56.93 kB
/ui/volume-indicator 13.04 kB 56.93 kB
/ui/volume-indicator-fill 1.03 kB
/ui/volume-indicator-value 1.02 kB
/ui/volume-slider 12.22 kB 56.93 kB

⚛️ @videojs/react

Path Base initial PR initial Initial diff Lazy diff
/media/hlsjs-video 136.61 kB 138.48 kB +1.87 kB (+1.4%)
/media/mux-audio 137.53 kB 139.29 kB +1.76 kB (+1.3%)
/media/mux-video 137.58 kB 139.54 kB +1.96 kB (+1.4%)
/media/native-hls-video 3.53 kB 5.75 kB +2.22 kB (+62.9%)
/ui/title 6.81 kB new
/video (default + hls) 178.49 kB 180.36 kB +1.87 kB (+1.0%) 0 B
/video (minimal + hls) 178.45 kB 180.38 kB +1.92 kB (+1.1%) 0 B
Small changes (20, ≤ 300 B)
Path Base initial PR initial Initial diff Lazy diff
/media/dash-video 209.69 kB 209.64 kB -49 B (-0.0%)
/media/vimeo-video 10.76 kB 10.79 kB +32 B (+0.3%)
/media/youtube-video 4.78 kB 4.78 kB +6 B (+0.1%)
/audio/minimal-skin 35.80 kB 35.83 kB +36 B (+0.1%) 0 B
/live-audio/minimal-skin 26.41 kB 26.40 kB -6 B (-0.0%) 0 B
/live-audio/minimal-skin.tailwind 29.71 kB 29.71 kB +1 B (+0.0%) 0 B
/live-audio/skin 26.44 kB 26.45 kB +1 B (+0.0%) 0 B
/ui/alert-dialog 3.25 kB 3.26 kB +1 B (+0.0%)
/ui/captions-button 10.01 kB 10.01 kB -1 B (-0.0%) 0 B
/ui/error-dialog 9.99 kB 9.98 kB -5 B (-0.0%) 0 B
/ui/fullscreen-button 9.98 kB 9.98 kB +4 B (+0.0%) 0 B
/ui/live-button 8.42 kB 8.42 kB -1 B (-0.0%) 0 B
/ui/mute-button 9.97 kB 9.97 kB -3 B (-0.0%) 0 B
/ui/pip-button 9.97 kB 9.98 kB +1 B (+0.0%) 0 B
/ui/playback-rate-button 9.96 kB 9.96 kB +1 B (+0.0%) 0 B
/ui/poster 6.86 kB 6.86 kB +6 B (+0.1%)
/ui/time 9.48 kB 9.48 kB -2 B (-0.0%) 0 B
/ui/time-slider 12.06 kB 12.07 kB +3 B (+0.0%) 0 B
/ui/volume-indicator 12.37 kB 12.37 kB -2 B (-0.0%) 0 B
/audio (default) 35.94 kB 35.93 kB -13 B (-0.0%) 0 B
Presets (7)
Entry Initial Lazy
/video (default) 43.48 kB 56.93 kB
/video (default + hls) 180.36 kB 56.93 kB
/video (minimal) 43.55 kB 56.93 kB
/video (minimal + hls) 180.38 kB 56.93 kB
/audio (default) 35.93 kB 56.93 kB
/audio (minimal) 35.96 kB 56.93 kB
/background 581 B
Media (12)
Entry Initial
/media/background-video 394 B
/media/dash-video 209.64 kB
/media/google-cast 5.34 kB
/media/hlsjs-video 138.48 kB
/media/mux-audio 139.29 kB
/media/mux-data 152.88 kB
/media/mux-video 139.54 kB
/media/native-hls-video 5.75 kB
/media/simple-hls-audio-only 19.88 kB
/media/simple-hls-video 25.34 kB
/media/vimeo-video 10.79 kB
/media/youtube-video 4.78 kB
Skins (27)
Entry Type Initial Lazy
/tailwind.css css 161 B
/video/minimal-skin.css css 5.81 kB
/video/skin.css css 5.81 kB
/video/minimal-skin js 43.50 kB 56.93 kB
/video/minimal-skin.tailwind js 49.73 kB 56.93 kB
/video/skin js 43.42 kB 56.93 kB
/video/skin.tailwind js 49.66 kB 56.93 kB
/audio/minimal-skin.css css 3.97 kB
/audio/skin.css css 3.90 kB
/audio/minimal-skin js 35.83 kB 56.93 kB
/audio/minimal-skin.tailwind js 38.39 kB 56.93 kB
/audio/skin js 35.80 kB 56.93 kB
/audio/skin.tailwind js 40.16 kB 56.93 kB
/background/skin.css css 90 B
/background/skin js 272 B
/live-video/minimal-skin.css css 5.81 kB
/live-video/skin.css css 5.81 kB
/live-video/minimal-skin js 38.50 kB 56.93 kB
/live-video/minimal-skin.tailwind js 44.74 kB 56.93 kB
/live-video/skin js 38.51 kB 56.93 kB
/live-video/skin.tailwind js 44.71 kB 56.93 kB
/live-audio/minimal-skin.css css 3.97 kB
/live-audio/skin.css css 3.90 kB
/live-audio/minimal-skin js 26.40 kB 56.93 kB
/live-audio/minimal-skin.tailwind js 29.71 kB 56.93 kB
/live-audio/skin js 26.45 kB 56.93 kB
/live-audio/skin.tailwind js 29.85 kB 56.93 kB
UI Components (34)
Entry Initial Lazy
/ui/airplay-button 10.04 kB 56.93 kB
/ui/alert-dialog 3.26 kB
/ui/audio-track 7.49 kB 56.93 kB
/ui/buffering-indicator 7.26 kB
/ui/captions-button 10.01 kB 56.93 kB
/ui/captions-radio-group 7.62 kB 56.93 kB
/ui/cast-button 10.02 kB 56.93 kB
/ui/controls 6.98 kB
/ui/error-dialog 9.98 kB 56.93 kB
/ui/fullscreen-button 9.98 kB 56.93 kB
/ui/gesture 7.37 kB
/ui/hotkey 7.82 kB
/ui/live-button 8.42 kB 56.93 kB
/ui/menu 20.61 kB 56.93 kB
/ui/mute-button 9.97 kB 56.93 kB
/ui/pip-button 9.98 kB 56.93 kB
/ui/play-button 9.96 kB 56.93 kB
/ui/playback-rate 6.79 kB
/ui/playback-rate-button 9.96 kB 56.93 kB
/ui/popover 7.47 kB
/ui/poster 6.86 kB
/ui/quality 8.01 kB 56.93 kB
/ui/seek-button 9.99 kB 56.93 kB
/ui/seek-indicator 11.80 kB
/ui/slider 11.96 kB 56.93 kB
/ui/status-announcer 9.68 kB 56.93 kB
/ui/status-indicator 12.26 kB 56.93 kB
/ui/thumbnail 8.07 kB
/ui/time 9.48 kB 56.93 kB
/ui/time-slider 12.07 kB 56.93 kB
/ui/title 6.81 kB
/ui/tooltip 8.22 kB
/ui/volume-indicator 12.37 kB 56.93 kB
/ui/volume-slider 11.40 kB 56.93 kB
🧩 @videojs/core — 1 small size change
Path Base initial PR initial Initial diff Lazy diff
. 11.78 kB 11.83 kB +58 B (+0.5%)
Entries (76)
Entry Initial Lazy
. 11.83 kB
/dom 20.00 kB
/components 900 B
/i18n 3.13 kB 56.93 kB
/i18n/locales/all 35.05 kB
/i18n/locales/ar 1.22 kB
/i18n/locales/az 1.09 kB
/i18n/locales/bg 1.29 kB
/i18n/locales/bn 1.28 kB
/i18n/locales/bs 1.00 kB
/i18n/locales/ca 1.07 kB
/i18n/locales/cs 1.07 kB
/i18n/locales/cy 1.02 kB
/i18n/locales/da 1006 B
/i18n/locales/de 1.09 kB
/i18n/locales/el 1.49 kB
/i18n/locales/en 785 B
/i18n/locales/es 1018 B
/i18n/locales/et 1.05 kB
/i18n/locales/eu 1.03 kB
/i18n/locales/fa 1.22 kB
/i18n/locales/fi 1.04 kB
/i18n/locales/fr 1.07 kB
/i18n/locales/gd 1.10 kB
/i18n/locales/gl 1022 B
/i18n/locales/he 1.14 kB
/i18n/locales/hi 1.30 kB
/i18n/locales/hr 1.02 kB
/i18n/locales/hu 1.10 kB
/i18n/locales/id 919 B
/i18n/locales/it 1.01 kB
/i18n/locales/ja 1.18 kB
/i18n/locales/ko 1.14 kB
/i18n/locales/lt 1.02 kB
/i18n/locales/lv 1.09 kB
/i18n/locales/mr 1.30 kB
/i18n/locales/nb 987 B
/i18n/locales/ne 1.29 kB
/i18n/locales/nl 1.00 kB
/i18n/locales/nn 987 B
/i18n/locales/oc 1.08 kB
/i18n/locales/pl 1.14 kB
/i18n/locales/pt 1.03 kB
/i18n/locales/pt-BR 1.03 kB
/i18n/locales/pt-PT 1.01 kB
/i18n/locales/ro 1.07 kB
/i18n/locales/ru 1.40 kB
/i18n/locales/sk 1.10 kB
/i18n/locales/sl 1.04 kB
/i18n/locales/sr 1.07 kB
/i18n/locales/sv 1.01 kB
/i18n/locales/te 1.32 kB
/i18n/locales/th 1.29 kB
/i18n/locales/tr 1.08 kB
/i18n/locales/uk 1.41 kB
/i18n/locales/vi 1.11 kB
/i18n/locales/zh 1.02 kB
/i18n/locales/zh-CN 1.02 kB
/i18n/locales/zh-TW 1.02 kB
/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 280 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 190 B
/time 1.01 kB
/number 158 B
📦 @videojs/jsx — no changes
Entries (3)
Entry Initial
. 393 B
/jsx-runtime 371 B
/jsx-dev-runtime 375 B

📦 @videojs/media

Path Base initial PR initial Initial diff Lazy diff
/dom 121 B new
/dom/hls-js 136.18 kB 138.13 kB +1.95 kB (+1.4%)
/dom/mux 159.51 kB 161.41 kB +1.90 kB (+1.2%)
/dom/native-hls 3.01 kB 5.26 kB +2.25 kB (+74.7%)
Small changes (4, ≤ 300 B)
Path Base initial PR initial Initial diff Lazy diff
. 993 B 1.04 kB +70 B (+7.0%)
/dom/dash 209.16 kB 209.26 kB +96 B (+0.0%)
/dom/vimeo 10.19 kB 10.19 kB -2 B (-0.0%)
/dom/youtube 4.16 kB 4.17 kB +9 B (+0.2%)
Entries (15)
Entry Initial
. 1.04 kB
/dom 121 B
/dom/audio-host 1.10 kB
/dom/custom-media-element 2.09 kB
/dom/dash 209.26 kB
/dom/google-cast 4.06 kB
/dom/hls-js 138.13 kB
/dom/media-host 1.20 kB
/dom/media-played-ranges 576 B
/dom/mux 161.41 kB
/dom/native-hls 5.26 kB
/dom/video-host 1.39 kB
/dom/vimeo 10.19 kB
/dom/youtube 4.17 kB
/media-tracks 1.98 kB
📦 @videojs/spf — no changes
Entries (7)
Entry Initial
. 4.53 kB
/dom 6.57 kB
/hls 20.86 kB
/media-tracks 504 B
/background-video 14.76 kB
/simple-hls 24.92 kB
/simple-hls-audio-only 19.44 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 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just some comments for claude

import type { MediaControlsState, MediaMetadataState, MediaPlaybackState } from '@videojs/media';

/**
* Media state the title reads. Controls and playback are optional so a player

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these optional? Would this code be more simple they weren't? Is there precedent for this in the codebase?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — one of the two was buying nothing, and the whole type was wider than it needed to be. Fixed in 1c48b8d.

Precedent. No core in the repo takes a Partial<FeatureState>; the only Partial<> uses are label overrides. The two shapes that do exist:

  • LiveButtonCore narrows with Pick: Pick<MediaTimeState, 'currentTime' | 'seek'> & Pick<MediaBufferState, 'seekable'> & MediaLiveState.
  • TimeSliderCore requires MediaTimeState & MediaBufferState, and TimeSliderElement supplies { buffered: [], seekable: [] } when the buffer feature is absent — the fallback lives in the adapter, not the core.

Playback: made required. Every preset includes playbackFeature, so Partial there was dead weight. The adapters now bail and warn like Poster does.

Controls: still optional, but for a concrete reason. audioFeatures and liveAudioFeatures leave out controlsFeature — audio chrome never auto-hides, so there is no visibility to follow. controlsVisible ?? true is what "this player has no hiding controls" means, and the JSDoc now says exactly that instead of hand-waving about features in general.

Net effect on the type:

export type TitleMediaState = Pick<MediaMetadataState, 'contentTitle'> &
  Pick<MediaPlaybackState, 'paused'> & {
    controlsVisible?: MediaControlsState['controlsVisible'] | undefined;
  };

Three fields instead of three whole feature states, one fallback instead of two, and the tests no longer need to mock setContentTitle, requestControlsLock, and friends just to construct a state.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correcting my reply above — I took "make it required" one step too far and broke a real consumer.

createPlayer({ features: [metadataFeature] }) has no playback slice, and that is exactly what apps/sandbox/templates/metadata composes. With playback required the adapters bailed out, so that template rendered nothing. This PR updates that template, so I broke the thing it was meant to demonstrate.

Fixed in 560b7c3 by pushing your question one level further out. TitleMediaState now requires all three fields, and TitleCore has no ?? left in it at all:

export type TitleMediaState = Pick<MediaMetadataState, 'contentTitle'> &
  Pick<MediaPlaybackState, 'paused'> &
  Pick<MediaControlsState, 'controlsVisible'>;

The degradation moved out to the adapters, which is where TimeSliderElement already keeps it for its optional buffer slice (...(buffer ?? { buffered: [], seekable: [] }), time-slider-element.ts:161):

paused: this.#playbackState.value?.paused ?? true,
controlsVisible: this.#controlsState.value?.controlsVisible ?? true,

So the answer to your question is yes, and more than my first pass managed: no optionality in the core, no branch to reason about, and a player missing either slice still works — a player with nothing that hides the title simply doesn't hide it. Both reduced compositions now have tests: no controlsFeature (the audio presets) and metadataFeature alone (the sandbox).


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all of these definitions should be moved to the PR where the title is actually added to the skins, so they wouldn't clutter this PR.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48b8d — removed from all six ui.ts / minimal-ui.ts entries. define/ui/title.ts stays, so <media-title> is still available as a granular import (that is what the docs demo uses).

Worth noting this also fixes a size regression the bundle bot flagged on this PR: every skin entry does import './ui', so registering the element there grew all thirteen skin entries (/video/skin +257 B) for a component none of them render. The stacked skin PR adds the registrations back where they pay for themselves.


Generated by Claude Code

Comment on lines +40 to +41
The component displays the title resolved by the metadata feature. It owns its own
text content — set the title on the player rather than by passing children.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"resolved by the metadata feature" is hella jargony. Heavily consider the guidance in writing-style.md

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rewritten. "Resolved by the metadata feature" is gone, along with "feature", "state", and "precedence" as reader-facing words. The mechanism is now stated as three places a title can come from, in the order the player tries them:

A title can come from three places. The player uses the first one that has a value:

  1. contentTitle, the title you set.
  2. The title the media reports about itself.
  3. defaultContentTitle, the fallback you set for media that does not name itself.

Generated by Claude Code

Comment on lines +55 to +58
The metadata feature resolves one value from several sources, in order: the title you
set, the title the media reports, the fallback you set with `defaultContentTitle`, and
finally an empty string. You can also update it at any time by calling `setContentTitle`
on the store.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This paragraph is so so so important. Make sure it communicates with the utmost clarity and minimal jargon

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — this is the paragraph people will actually need, so it now states the rule as a rule and then says what it buys the viewer:

The title shows when three things are true: a title exists, the controls are on screen, and playback is paused. Press play and it hides, so it stays out of the way of the video.

A player that leaves out a feature simply has less to hide behind. Audio players have no auto-hiding controls, so their title follows playback alone; a player with no playback at all shows its title for as long as one resolves.

The second paragraph is new in 560b7c3 and describes real behavior now, not a hypothetical — both compositions are covered by tests.

One nearby claim was also just wrong: the page said an empty title "renders nothing." It doesn't — the element stays in the page with no text. That is the whole reason data-has-title exists, so the corrected sentence points at it rather than leaving the reader to discover an invisible empty element sitting on their layout.


Generated by Claude Code

Address review feedback on the title component:

- `TitleMediaState` now picks the three fields the core reads instead of
  intersecting three whole feature states. Playback is required — every
  preset includes it. Controls stays optional because the audio presets
  leave out `controlsFeature`; audio chrome never auto-hides.
- Drop the bulk `safeDefine(TitleElement)` calls from the `ui` and
  `minimal-ui` entries. Nothing renders `<media-title>` yet, so they land
  with the skin work instead. The granular `ui/title` entry is unchanged.
- Rewrite the title resolution and visibility prose without jargon.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118ZxrT7hBHhZ57GdgCTvUy
…ore-yzkrvh

# Conflicts:
#	site/src/docs.config.ts
Making playback required in 1c48b8d broke every player composed without
`playbackFeature` — including the metadata sandbox template this PR
updates, which composes `metadataFeature` alone and now rendered nothing.

Take the shape `TimeSliderElement` already uses for its optional buffer
slice: the core type requires every field, and the adapters substitute a
neutral value when a slice is absent. `TitleCore` loses both `??`
branches, `TitleMediaState` loses its optional member, and `paused` and
`controlsVisible` fall back to the value that keeps the title on screen.

Also adds the `ui/title/index.ts` React entry, without which the
component was excluded from the build's entry glob and the bundle-size
report, and corrects the reference page: an empty title renders an empty
element, not nothing.
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.

2 participants