Skip to content

feat: content metadata via provider config and derived store state - #1886

Draft
decepulis wants to merge 8 commits into
mainfrom
feat/content-metadata-poc
Draft

feat: content metadata via provider config and derived store state#1886
decepulis wants to merge 8 commits into
mainfrom
feat/content-metadata-poc

Conversation

@decepulis

@decepulis decepulis commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Warning

Draft. AI-generated in one shot, for reference — not for review.

Implements the metadata design end to end: developers provide metadata through
the provider, media can provide it too, and the store reconciles them by
precedence. All three fields — contentTitle, contentPoster, contentPosterAlt
— across store, media, core, React, and HTML. Orientation lock is migrated onto
the same mechanism and the old configurable-feature path is deleted.

<video-player content-title="From my CMS" default-content-poster="fallback.jpg">
<Player.Provider contentTitle="From my CMS" defaultContentPoster="fallback.jpg">

Layers

Seven commits, one per layer, in the order the design notes suggested.

Commit What
feat(store) derived primitive; symbol-aware, lazily-copying patch
feat(media) three capabilities, events, predicates, host accessors
feat(core) provider-prop declarations, contentMetadataFeature, presets, orientation lock
feat(react) provider props, store-reading <Poster>
feat(html) provider attributes, <media-poster> filling the author's image
chore(sandbox) templates and the e2e generator moved to the provider
docs(design) two new records, three amendments

Checks

pnpm typecheck clean. pnpm lint clean on all 68 changed source files.
2,582 tests pass across the five packages (139 store / 427 media / 1362 core /
374 react / 280 html), including ~90 new ones.

pnpm check:workspace passes 9 of 10 — the failure is .claude/plans: must resolve to .agents/plans, which is pre-existing local environment state
(confirmed by running it on a clean tree) and unrelated to this branch.

The gate checklist from the design notes holds: derived values recompute on tier
writes; symbol tiers stay absent from store.state, selector output, and
combine's duplicate-key warning; detach-then-reattach preserves developer tiers,
clears the media tier, and leaves the resolved value correct; provider props are
composition-gated, proven with @ts-expect-error; the seeding path survives the
development double render; and three fields share the machinery without the
authoring cost feeling silly.

The one thing the design left open, and what I decided

The HTML write path was explicitly "early ideas only" and was the largest gap.
I decided it as follows, and this is the part most worth a second opinion:

  • Declared fields become ordinary reactive properties, so the attribute engine the
    element already has does the work. No third attribute system.
  • The write happens in two places on purpose. An explicit sync in
    connectedCallback before #tryAttach(), because attributes are parsed
    during upgrade but the first update() only runs a microtask after connect — so
    without it the store would attach, and the media would report its own metadata,
    before the developer's values landed. Later changes take the ordinary Lit path
    via willUpdate.
  • The Q12 trap resolved itself: ReactiveElement's generated accessor already
    keeps its own symbol-keyed backing value, so the property keeps reporting what
    was assigned rather than the store's resolved value. No custom accessor needed.

Two smaller open items I also had to decide:

  • The declaration field is named providerProps, not config. "Config" is
    already taken three times over — store lifecycle callbacks, the media host's
    config bag, and the path this replaces.
  • The media tier may supply poster alt text. The design leaned yes; the
    three-tier shape was free once the machinery existed.

Two bugs the design would have shipped

Both found by writing the tests, and both fixed here:

  1. createScreenOrientationLock destructured type eagerly, so "read the
    value at lock time" silently did not work — the store half looks finished while
    the lock still uses whatever was set at attach. Now read from config.type
    inside lock().
  2. <media-poster> deciding alt/src ownership by presence on every pass
    would freeze a playlist on the first video's poster
    , because after the first
    fill the attribute is present either way. Ownership is now snapshotted once.

Known gaps

  • Prose docs are not updated. The skins' poster prop is removed, and
    site/src/content/docs/ still documents it (along with the packages/cli/docs/
    bundle generated from it). Deliberately out of scope for a reference PR —
    content writing deserves the write-docs workflow, not a one-shot.
  • AttachContext.get() now returns source and derived state. A small
    widening the notes did not anticipate, needed because orientation lock reads its
    own resolved value at lock time. Writes stay source-only.
  • EmptyObject was added to @videojs/utils/types so the new generic
    defaults could avoid a bare {}. The only in-repo precedent for that lint was
    in packages/spf, which I was told to treat as a separate project and did not
    read.
  • Not addressed, consistent with the notes: analytics and Google Cast still
    cannot reach resolved values — they sit below the store. google-cast-provider.ts
    still builds Cast metadata from the raw element poster and sets no title.
  • contentPlaceholder is not built. Still a genuinely different concept.

🤖 Generated with Claude Code

decepulis and others added 7 commits July 29, 2026 16:53
A slice may declare `derived` formulas alongside its state. The store computes
them eagerly inside `patch` and folds the answers into the same frozen snapshot,
before it notifies — so a derived key stays an ordinary value and every existing
read path (store getters, selectors, `store.state`, `shallowEqual`) keeps working
unchanged.

Three rules keep it small:

- Every formula reruns on every patch that changed something. No dependency
  tracking, so formulas must be cheap.
- A formula reads source keys only. Reading another formula's output is a compile
  error, which removes run ordering and cycle detection.
- The formula is the only writer of its key. `patch` strips derived keys from
  incoming partials and warns in `__DEV__`.

That last rule is what makes `detach` order-independent: whether cleanup or the
reset runs first, the source keys end up the same and the formula produces the
same answer.

`patch` also now iterates with `Reflect.ownKeys` rather than `for...in`, so
symbol-keyed entries participate — previously a symbol patch was a silent no-op.
It copies the state object lazily, only once a real change is found, which drops
the separate `changed` flag and the `hasOwnProperty` guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three per-field capabilities — `contentTitle`, `contentPoster`, and
`contentPosterAlt` — each with its own change event and predicate, plus host
getters and setters following the `streamType` pattern.

These are deliberately separate from `title` and `poster` and are not reconciled
with them. Those are the developer's settings on the element — the native tooltip,
the `<video poster>` frame. The `content*` properties are facts about the content,
usually from a backend.

Absence is `null` or `undefined`; an empty string is a real value meaning
deliberately blank, and a media must never manufacture one. The host's backing
fields start as `null` rather than `undefined` so a declaring host stays capable
even before a value arrives — otherwise capability would flicker, and since a
feature checks it once at attach, a value arriving later would never reach the
store. `CustomMediaElement` declarations use `empty: null` for the same reason,
following `preload` rather than the neighbouring `poster` and `src`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… feature

A feature may declare fields the developer can set on the player provider, as
React props and HTML attributes. Each declaration names one of the feature's own
actions as the write target, because tier slots are private to a feature and the
provider is generic — naming an action is how a generic provider hands a value to
a feature that knows what to do with it. It also means the declarative and
imperative paths run through one function and cannot drift.

`createPlayer` collects declarations once, so a prop exists only when its feature
is composed. A `__DEV__` check catches duplicate names and names that already
exist on the provider element's prototype chain — tested with `in` rather than
`getOwnPropertyDescriptor`, which sees only own properties and would miss an
inherited DOM property like `title`.

The new `contentMetadataFeature` resolves each field from three symbol-keyed
slots: `user ?? media ?? developerDefault ?? ''`. Separate slots rather than one
slot with write-time precedence, so they resolve identically however the writes
arrive. Media slots are seeded in `state()` so `detach` clears them; developer
slots are left out so the reset cannot reach them. It joins all four real presets.

Orientation lock moves onto the same mechanism and the configurable-feature path
in `feature.ts` is deleted. Its value is now read at lock time rather than
captured at attach, which is the actual behavioural change. It declares one prop,
not two — nothing donates an orientation preference, so a `default*` tier would
just be a second developer value with lower precedence.

BREAKING CHANGE: `definePlayerFeature` no longer takes a second config argument.
`orientationLockFeature({ type })` becomes the `orientation-lock` provider
attribute or `store.setOrientationLock()`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Provider props are seeded inside the `useState` initializer that builds the
store, and later changes go through a layout effect. Not the render body: a store
built during render has no subscribers yet, so a render React throws away
discards the seeded values with it, but writing to an already-subscribed store
during render can leave a value behind from a render that never committed.

Seeding still happens during render, including on the server, so server HTML
carries the right values and there is no hydration flash. The accepted cost is
that on the pass where a prop changes, children render the stale value first —
children render before the parent's layout effect. Nothing flashes, because the
notification's microtask drains before paint. A passive effect would be worse: it
permits a paint in between.

`<Poster>` with no `src` now reads the resolved value from the store, and fills
`alt` from `contentPosterAlt`. A local `src` short-circuits that and the store is
not consulted at all, so this is not a fourth precedence tier — the component
only decides whether to ask. `srcSet`, `loading`, and the render prop are
untouched. `alt` is decided by presence, never emptiness, since `alt=""` is the
platform's marker for a decorative image.

BREAKING CHANGE: the skins' `poster` prop is removed. Set `content-poster` on the
provider instead, or pass `src` to `<Poster>` directly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e store

Declared fields become ordinary reactive properties on the provider element, so
`<video-player content-title="…">` runs through the attribute engine the element
already has rather than a third mechanism. `ReactiveElement`'s generated accessor
keeps its own symbol-keyed backing value, which matters: after
`el.contentTitle = undefined` hands control back to the media, reading the
property must still report what was assigned, not the media's title.

The write happens in two places, deliberately. An explicit sync in
`connectedCallback` before `#tryAttach()` — attributes are parsed during upgrade,
but the first `update()` only runs a microtask after connect, so without it the
store would attach and the media would report its own metadata before the
developer's values landed. Later changes take the ordinary Lit path via
`willUpdate`.

`<media-poster>` fills an empty `src` and a missing `alt` on the image the author
supplied, and never creates one — a bare `<media-poster>` with no image renders
nothing, by design. Default skins get zero-markup behaviour from a fallback `<img>`
inside their own poster slot; an author-supplied `slot="poster"` image still wins.
No shadow root is added.

Ownership of each attribute is snapshotted the first time an image is seen rather
than re-derived from presence, because after the first fill the attribute is
present either way — re-deriving would freeze a playlist on the first video's
poster.

The platform asymmetry with React's `<Poster>` is deliberate: React's component
owns its image, HTML's element wraps yours. Behaviour is identical either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The React skins no longer take a `poster` prop, so the sandbox templates and the
e2e page generator set `content-poster` on the provider instead. The HTML
templates are unchanged — an author-supplied `slot="poster"` image still wins over
the skin's fallback.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nfig

Two new records and three amendments, per the design notes' paperwork list:

- `store/derived-state.md` — the primitive, and why tracking, compute-on-read, a
  widened state type, and visible string keys for tier slots were all rejected.
- `media/content-metadata.md` — the capability contract, including the half that
  is not structurally enforced: dispatching on change is, clearing on source
  change is convention with nothing checking it.
- `store/reactive-state.md` — marked partly superseded. Derivation is now eager,
  and a `derived` map means a patch can change keys the caller did not name, which
  is a real reduction in the explicitness that record valued. Also notes that its
  "computed values" most likely describes selectors, so this work should not be
  framed as finishing a ratified design.
- `rfc/player-api.md` — amended for the provider-config surface, since it changes
  a public API that RFC settled. Reconciles the deferred symbol-feature-keys
  alternative and the provider-complexity concerns from the player records.
- The two poster design records — reconciled. The poster fills in rather than
  takes over, so `poster.md`'s rejection of a component-managed `src` still holds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 29, 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 Jul 30, 2026 1:18am

Request Review

@netlify

netlify Bot commented Jul 29, 2026

Copy link
Copy Markdown

Deploy Preview for vjs10-site ready!

Name Link
🔨 Latest commit 7a7802b
🔍 Latest deploy log https://app.netlify.com/projects/vjs10-site/deploys/6a6aa60a157eb80008efe26e
😎 Deploy Preview https://deploy-preview-1886--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.

@decepulis decepulis changed the title feat: content metadata via provider config and derived store state (AI one-shot, reference only) feat: content metadata via provider config and derived store state Jul 29, 2026
@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle Size Report

🎨 @videojs/html

Path Base initial PR initial Diff % Lazy
/video/minimal-skin 52.27 kB 53.04 kB +791 B +1.5% 0 B 🔺
/video/minimal-skin.tailwind 53.02 kB 53.72 kB +723 B +1.3% 0 B 🔺
/video/player 8.22 kB 8.82 kB +608 B +7.2% 🔺
/video/skin 52.37 kB 53.09 kB +730 B +1.4% 0 B 🔺
/video/skin.tailwind 53.16 kB 53.82 kB +675 B +1.2% 0 B 🔺
/audio/minimal-skin 41.99 kB 42.59 kB +620 B +1.4% 0 B 🔺
/audio/minimal-skin.tailwind 42.49 kB 43.15 kB +674 B +1.5% 0 B 🔺
/audio/player 5.37 kB 5.98 kB +624 B +11.3% 🔴
/audio/skin 45.45 kB 45.99 kB +561 B +1.2% 0 B 🔺
/audio/skin.tailwind 45.94 kB 46.57 kB +649 B +1.4% 0 B 🔺
/background/player 3.92 kB 4.29 kB +384 B +9.6% 🔺
/live-video/minimal-skin 48.65 kB 49.34 kB +705 B +1.4% 0 B 🔺
/live-video/minimal-skin.tailwind 49.18 kB 49.96 kB +794 B +1.6% 0 B 🔺
/live-video/player 7.63 kB 8.22 kB +600 B +7.7% 🔺
/live-video/skin 50.86 kB 51.49 kB +646 B +1.2% 0 B 🔺
/live-video/skin.tailwind 51.47 kB 52.22 kB +768 B +1.5% 0 B 🔺
/live-audio/minimal-skin 34.17 kB 34.75 kB +598 B +1.7% 0 B 🔺
/live-audio/minimal-skin.tailwind 33.64 kB 34.16 kB +531 B +1.5% 0 B 🔺
/live-audio/player 5.38 kB 6.00 kB +630 B +11.4% 🔴
/live-audio/skin 37.68 kB 38.31 kB +640 B +1.7% 0 B 🔺
/live-audio/skin.tailwind 37.26 kB 37.81 kB +561 B +1.5% 0 B 🔺
/ui/airplay-button 10.26 kB 10.58 kB +327 B +3.1% 0 B 🔺
/ui/audio-track-radio-group 10.06 kB 10.38 kB +331 B +3.2% 0 B 🔺
/ui/buffering-indicator 7.57 kB 7.89 kB +327 B +4.2% 🔺
/ui/captions-button 10.51 kB 10.84 kB +344 B +3.2% 0 B 🔺
/ui/captions-radio-group 10.14 kB 10.45 kB +314 B +3.0% 0 B 🔺
/ui/cast-button 10.23 kB 10.55 kB +332 B +3.2% 0 B 🔺
/ui/compounds 32.22 kB 32.58 kB +369 B +1.1% 0 B 🔺
/ui/error-dialog 10.68 kB 11.00 kB +322 B +2.9% 0 B 🔺
/ui/fullscreen-button 10.21 kB 10.54 kB +335 B +3.2% 0 B 🔺
/ui/menu 19.24 kB 19.56 kB +327 B +1.7% 0 B 🔺
/ui/mute-button 10.25 kB 10.59 kB +348 B +3.3% 0 B 🔺
/ui/pip-button 10.22 kB 10.53 kB +319 B +3.0% 0 B 🔺
/ui/play-button 10.23 kB 10.58 kB +356 B +3.4% 0 B 🔺
/ui/playback-rate-button 10.42 kB 10.77 kB +349 B +3.3% 0 B 🔺
/ui/playback-rate-radio-group 9.84 kB 10.18 kB +346 B +3.4% 0 B 🔺
/ui/poster 7.93 kB 8.30 kB +384 B +4.7% 0 B 🔺
/ui/quality-radio-group 10.63 kB 10.94 kB +311 B +2.9% 0 B 🔺
/ui/seek-button 10.30 kB 10.61 kB +323 B +3.1% 0 B 🔺
/ui/seek-indicator 11.32 kB 11.65 kB +336 B +2.9% 🔺
/ui/status-announcer 10.86 kB 11.20 kB +348 B +3.1% 0 B 🔺
/ui/status-indicator 11.91 kB 12.22 kB +318 B +2.6% 0 B 🔺
/ui/time-slider 13.80 kB 14.14 kB +348 B +2.5% 0 B 🔺
/ui/volume-indicator 11.93 kB 12.27 kB +346 B +2.8% 0 B 🔺
/ui/volume-slider 11.64 kB 12.00 kB +364 B +3.1% 0 B 🔺
/video (default) 52.41 kB 53.08 kB +689 B +1.3% 0 B 🔺
/video (default + hls) 191.64 kB 192.55 kB +938 B +0.5% 0 B 🔺
/video (minimal) 52.32 kB 53.06 kB +759 B +1.4% 0 B 🔺
/video (minimal + hls) 191.66 kB 192.56 kB +922 B +0.5% 0 B 🔺
/audio (default) 45.42 kB 45.99 kB +585 B +1.3% 0 B 🔺
/audio (minimal) 41.99 kB 42.56 kB +587 B +1.4% 0 B 🔺
/background 4.20 kB 4.57 kB +377 B +8.8% 🔺
Presets (7)
Entry Initial Lazy
/video (default) 53.08 kB 50.16 kB
/video (default + hls) 192.55 kB 50.16 kB
/video (minimal) 53.06 kB 50.16 kB
/video (minimal + hls) 192.56 kB 50.16 kB
/audio (default) 45.99 kB 50.16 kB
/audio (minimal) 42.56 kB 50.16 kB
/background 4.57 kB
Media (10)
Entry Initial
/media/background-video 1.14 kB
/media/container 1.71 kB
/media/dash-video 215.03 kB
/media/hlsjs-video 141.52 kB
/media/mux-audio 164.94 kB
/media/mux-video 165.09 kB
/media/native-hls-video 9.19 kB
/media/simple-hls-audio-only 19.24 kB
/media/simple-hls-video 21.28 kB
/media/vimeo-video 12.35 kB
Players (5)
Entry Initial
/video/player 8.82 kB
/audio/player 5.98 kB
/background/player 4.29 kB
/live-video/player 8.22 kB
/live-audio/player 6.00 kB
Skins (30)
Entry Type Initial Lazy
/video/minimal-skin.css css 5.70 kB
/video/skin.css css 5.66 kB
/video/minimal-skin js 53.04 kB 50.16 kB
/video/minimal-skin.tailwind js 53.72 kB 50.16 kB
/video/skin js 53.09 kB 50.16 kB
/video/skin.tailwind js 53.82 kB 50.16 kB
/audio/minimal-skin.css css 3.97 kB
/audio/skin.css css 3.86 kB
/audio/minimal-skin js 42.59 kB 50.16 kB
/audio/minimal-skin.tailwind js 43.15 kB 50.16 kB
/audio/skin js 45.99 kB 50.16 kB
/audio/skin.tailwind js 46.57 kB 50.16 kB
/background/skin.css css 133 B
/background/skin js 1.14 kB
/live-video/minimal-skin.css css 5.70 kB
/live-video/skin.css css 5.66 kB
/live-video/minimal-skin js 49.34 kB 50.16 kB
/live-video/minimal-skin.tailwind js 49.96 kB 50.16 kB
/live-video/skin js 51.49 kB 50.16 kB
/live-video/skin.tailwind js 52.22 kB 50.16 kB
/live-audio/minimal-skin.css css 3.97 kB
/live-audio/skin.css css 3.86 kB
/live-audio/minimal-skin js 34.75 kB 50.16 kB
/live-audio/minimal-skin.tailwind js 34.16 kB 50.16 kB
/live-audio/skin js 38.31 kB 50.16 kB
/live-audio/skin.tailwind js 37.81 kB 50.16 kB
/global.css css 183 B
/shared.css css 153 B
/tailwind.css css 161 B
/skin-element js 1.51 kB
UI Components (39)
Entry Initial
/ui/airplay-button 2.61 kB
/ui/alert-dialog 2.92 kB
/ui/alert-dialog-close 2.56 kB
/ui/alert-dialog-description 2.54 kB
/ui/alert-dialog-title 2.55 kB
/ui/audio-track-radio-group 3.03 kB
/ui/buffering-indicator 2.72 kB
/ui/captions-button 2.60 kB
/ui/captions-radio-group 2.97 kB
/ui/cast-button 2.53 kB
/ui/compounds 3.27 kB
/ui/controls 2.98 kB
/ui/error-dialog 3.00 kB
/ui/fullscreen-button 2.60 kB
/ui/hotkey 2.66 kB
/ui/menu 3.02 kB
/ui/mute-button 2.67 kB
/ui/pip-button 2.56 kB
/ui/play-button 2.65 kB
/ui/playback-rate-button 2.67 kB
/ui/playback-rate-radio-group 2.90 kB
/ui/popover 3.19 kB
/ui/poster 2.51 kB
/ui/quality-radio-group 2.95 kB
/ui/seek-button 2.61 kB
/ui/seek-indicator 2.85 kB
/ui/seek-indicator-value 471 B
/ui/slider 3.00 kB
/ui/status-announcer 2.52 kB
/ui/status-indicator 2.62 kB
/ui/status-indicator-value 455 B
/ui/thumbnail 2.68 kB
/ui/time 2.98 kB
/ui/time-slider 3.01 kB
/ui/tooltip 2.99 kB
/ui/volume-indicator 2.57 kB
/ui/volume-indicator-fill 389 B
/ui/volume-indicator-value 468 B
/ui/volume-slider 3.00 kB

Sizes are marginal over the root entry point.

⚛️ @videojs/react

Path Base initial PR initial Diff % Lazy
/video/minimal-skin 41.80 kB 42.13 kB +335 B +0.8% 0 B 🔺
/video/minimal-skin.tailwind 47.83 kB 48.19 kB +370 B +0.8% 0 B 🔺
/video/skin 41.46 kB 41.77 kB +317 B +0.7% 0 B 🔺
/video/skin.tailwind 47.51 kB 47.87 kB +371 B +0.8% 0 B 🔺
/audio/minimal-skin 34.61 kB 34.96 kB +350 B +1.0% 0 B 🔺
/audio/minimal-skin.tailwind 36.63 kB 37.00 kB +383 B +1.0% 0 B 🔺
/audio/skin 34.54 kB 34.89 kB +358 B +1.0% 0 B 🔺
/live-video/minimal-skin 36.14 kB 36.49 kB +353 B +1.0% 0 B 🔺
/live-video/minimal-skin.tailwind 41.99 kB 42.35 kB +373 B +0.9% 0 B 🔺
/live-video/skin 36.14 kB 36.48 kB +352 B +1.0% 0 B 🔺
/live-video/skin.tailwind 42.07 kB 42.40 kB +342 B +0.8% 0 B 🔺
/live-audio/minimal-skin 24.70 kB 25.01 kB +317 B +1.3% 0 B 🔺
/live-audio/minimal-skin.tailwind 27.76 kB 28.15 kB +399 B +1.4% 0 B 🔺
/live-audio/skin 24.73 kB 25.05 kB +330 B +1.3% 0 B 🔺
/live-audio/skin.tailwind 27.86 kB 28.20 kB +349 B +1.2% 0 B 🔺
/ui/airplay-button 9.45 kB 9.78 kB +341 B +3.5% 0 B 🔺
/ui/audio-track 6.98 kB 7.33 kB +355 B +5.0% 0 B 🔺
/ui/buffering-indicator 6.76 kB 7.09 kB +334 B +4.8% 🔺
/ui/captions-button 9.42 kB 9.75 kB +343 B +3.6% 0 B 🔺
/ui/captions-radio-group 7.11 kB 7.46 kB +352 B +4.8% 0 B 🔺
/ui/cast-button 9.43 kB 9.77 kB +344 B +3.6% 0 B 🔺
/ui/error-dialog 9.45 kB 9.79 kB +347 B +3.6% 0 B 🔺
/ui/fullscreen-button 9.40 kB 9.73 kB +338 B +3.5% 0 B 🔺
/ui/live-button 7.94 kB 8.26 kB +329 B +4.0% 0 B 🔺
/ui/menu 19.21 kB 19.58 kB +379 B +1.9% 0 B 🔺
/ui/mute-button 9.44 kB 9.77 kB +337 B +3.5% 0 B 🔺
/ui/pip-button 9.40 kB 9.72 kB +336 B +3.5% 0 B 🔺
/ui/play-button 9.42 kB 9.75 kB +341 B +3.5% 0 B 🔺
/ui/playback-rate 6.30 kB 6.63 kB +339 B +5.3% 🔺
/ui/playback-rate-button 9.43 kB 9.75 kB +334 B +3.5% 0 B 🔺
/ui/poster 6.39 kB 6.69 kB +304 B +4.6% 🔺
/ui/quality 7.50 kB 7.84 kB +345 B +4.5% 0 B 🔺
/ui/seek-button 9.46 kB 9.80 kB +348 B +3.6% 0 B 🔺
/ui/seek-indicator 10.60 kB 10.96 kB +366 B +3.4% 🔺
/ui/slider 11.42 kB 11.79 kB +373 B +3.2% 0 B 🔺
/ui/status-announcer 10.15 kB 10.50 kB +355 B +3.4% 0 B 🔺
/ui/status-indicator 11.16 kB 11.50 kB +341 B +3.0% 0 B 🔺
/ui/time-slider 11.36 kB 11.70 kB +346 B +3.0% 0 B 🔺
/ui/volume-indicator 11.25 kB 11.60 kB +362 B +3.1% 0 B 🔺
/ui/volume-slider 10.81 kB 11.17 kB +363 B +3.3% 0 B 🔺
/video (default) 41.52 kB 41.89 kB +379 B +0.9% 0 B 🔺
/video (default + hls) 179.63 kB 180.13 kB +517 B +0.3% 0 B 🔺
/video (minimal) 41.88 kB 42.27 kB +399 B +0.9% 0 B 🔺
/video (minimal + hls) 180.01 kB 180.54 kB +546 B +0.3% 0 B 🔺
/audio (default) 34.65 kB 35.04 kB +396 B +1.1% 0 B 🔺
/audio (minimal) 34.71 kB 35.04 kB +340 B +1.0% 0 B 🔺
Presets (7)
Entry Initial Lazy
/video (default) 41.89 kB 50.16 kB
/video (default + hls) 180.13 kB 50.16 kB
/video (minimal) 42.27 kB 50.16 kB
/video (minimal + hls) 180.54 kB 50.16 kB
/audio (default) 35.04 kB 50.16 kB
/audio (minimal) 35.04 kB 50.16 kB
/background 581 B
Media (9)
Entry Initial
/media/background-video 394 B
/media/dash-video 213.02 kB
/media/hlsjs-video 139.95 kB
/media/mux-audio 163.31 kB
/media/mux-video 163.47 kB
/media/native-hls-video 7.38 kB
/media/simple-hls-audio-only 17.48 kB
/media/simple-hls-video 19.58 kB
/media/vimeo-video 10.46 kB
Skins (27)
Entry Type Initial Lazy
/tailwind.css css 161 B
/video/minimal-skin.css css 5.56 kB
/video/skin.css css 5.52 kB
/video/minimal-skin js 42.13 kB 50.16 kB
/video/minimal-skin.tailwind js 48.19 kB 50.16 kB
/video/skin js 41.77 kB 50.16 kB
/video/skin.tailwind js 47.87 kB 50.16 kB
/audio/minimal-skin.css css 3.79 kB
/audio/skin.css css 3.68 kB
/audio/minimal-skin js 34.96 kB 50.16 kB
/audio/minimal-skin.tailwind js 37.00 kB 50.16 kB
/audio/skin js 34.89 kB 50.16 kB
/audio/skin.tailwind js 39.04 kB 50.16 kB
/background/skin.css css 90 B
/background/skin js 272 B
/live-video/minimal-skin.css css 5.56 kB
/live-video/skin.css css 5.52 kB
/live-video/minimal-skin js 36.49 kB 50.16 kB
/live-video/minimal-skin.tailwind js 42.35 kB 50.16 kB
/live-video/skin js 36.48 kB 50.16 kB
/live-video/skin.tailwind js 42.40 kB 50.16 kB
/live-audio/minimal-skin.css css 3.79 kB
/live-audio/skin.css css 3.68 kB
/live-audio/minimal-skin js 25.01 kB 50.16 kB
/live-audio/minimal-skin.tailwind js 28.15 kB 50.16 kB
/live-audio/skin js 25.05 kB 50.16 kB
/live-audio/skin.tailwind js 28.20 kB 50.16 kB
UI Components (33)
Entry Initial
/ui/airplay-button 2.48 kB
/ui/alert-dialog 2.70 kB
/ui/audio-track 2.39 kB
/ui/buffering-indicator 2.68 kB
/ui/captions-button 2.46 kB
/ui/captions-radio-group 2.40 kB
/ui/cast-button 2.52 kB
/ui/controls 2.63 kB
/ui/error-dialog 2.53 kB
/ui/fullscreen-button 2.49 kB
/ui/gesture 2.47 kB
/ui/hotkey 2.49 kB
/ui/live-button 2.40 kB
/ui/menu 2.71 kB
/ui/mute-button 2.51 kB
/ui/pip-button 2.49 kB
/ui/play-button 2.50 kB
/ui/playback-rate 2.60 kB
/ui/playback-rate-button 2.54 kB
/ui/popover 3.28 kB
/ui/poster 2.72 kB
/ui/quality 2.45 kB
/ui/seek-button 2.48 kB
/ui/seek-indicator 2.69 kB
/ui/slider 2.62 kB
/ui/status-announcer 2.39 kB
/ui/status-indicator 2.43 kB
/ui/thumbnail 2.56 kB
/ui/time 2.30 kB
/ui/time-slider 2.60 kB
/ui/tooltip 3.19 kB
/ui/volume-indicator 2.48 kB
/ui/volume-slider 2.63 kB

Sizes are marginal over the root entry point.

🧩 @videojs/core

Path Base initial PR initial Diff % Lazy
/dom 17.82 kB 18.36 kB +551 B +3.0% 🔺
Entries (72)
Entry Initial Lazy
. 10.86 kB
/dom 18.36 kB
/i18n 2.99 kB 50.16 kB
/i18n/locales/all 30.51 kB
/i18n/locales/ar 1.11 kB
/i18n/locales/az 1.01 kB
/i18n/locales/bg 1.17 kB
/i18n/locales/bn 1.20 kB
/i18n/locales/bs 926 B
/i18n/locales/ca 988 B
/i18n/locales/cs 978 B
/i18n/locales/cy 948 B
/i18n/locales/da 918 B
/i18n/locales/de 1023 B
/i18n/locales/el 1.37 kB
/i18n/locales/en 707 B
/i18n/locales/es 925 B
/i18n/locales/et 995 B
/i18n/locales/eu 939 B
/i18n/locales/fa 1.11 kB
/i18n/locales/fi 970 B
/i18n/locales/fr 1001 B
/i18n/locales/gd 1.00 kB
/i18n/locales/gl 929 B
/i18n/locales/he 1.03 kB
/i18n/locales/hi 1.21 kB
/i18n/locales/hr 948 B
/i18n/locales/hu 1019 B
/i18n/locales/it 958 B
/i18n/locales/ja 1.10 kB
/i18n/locales/ko 1.04 kB
/i18n/locales/lv 1012 B
/i18n/locales/mr 1.21 kB
/i18n/locales/nb 908 B
/i18n/locales/ne 1.20 kB
/i18n/locales/nl 934 B
/i18n/locales/nn 913 B
/i18n/locales/oc 999 B
/i18n/locales/pl 1.03 kB
/i18n/locales/pt 955 B
/i18n/locales/pt-BR 955 B
/i18n/locales/pt-PT 914 B
/i18n/locales/ro 996 B
/i18n/locales/ru 1.24 kB
/i18n/locales/sk 1018 B
/i18n/locales/sl 960 B
/i18n/locales/sr 989 B
/i18n/locales/sv 935 B
/i18n/locales/te 1.22 kB
/i18n/locales/th 1.19 kB
/i18n/locales/tr 1003 B
/i18n/locales/uk 1.27 kB
/i18n/locales/vi 1006 B
/i18n/locales/zh 947 B
/i18n/locales/zh-CN 947 B
/i18n/locales/zh-TW 949 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/errors 277 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 179 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.60 kB
/html 695 B
/react 361 B
🔧 @videojs/utils — no changes
Entries (12)
Entry Initial
/array 104 B
/dom 2.94 kB
/events 319 B
/function 327 B
/jwt 176 B
/object 505 B
/predicate 265 B
/percent 281 B
/string 239 B
/style 190 B
/time 813 B
/number 158 B

📦 @videojs/media

Path Base initial PR initial Diff % Lazy
/dom/mux 159.27 kB 159.61 kB +350 B +0.2% 🔺
Entries (14)
Entry Initial
. 1006 B
/dom/audio-host 1.24 kB
/dom/custom-media-element 2.13 kB
/dom/dash 209.11 kB
/dom/google-cast 4.04 kB
/dom/hls-js 136.00 kB
/dom/media-host 1.36 kB
/dom/media-played-ranges 576 B
/dom/mux 159.61 kB
/dom/native-hls 3.15 kB
/dom/simple-hls 18.98 kB
/dom/simple-hls-audio-only 16.98 kB
/dom/video-host 1.53 kB
/dom/vimeo 9.88 kB
📦 @videojs/spf — no changes
Entries (4)
Entry Initial
. 4.46 kB
/dom 6.49 kB
/hls 17.85 kB
/background-video 13.30 kB

ℹ️ How to interpret

JS sizes are initial static graph totals (minified + brotli). Lazy dynamic chunks are shown separately when present.

Icon Meaning
No change
🔺 Increased ≤ 10%
🔴 Increased > 10%
🔽 Decreased
🆕 New (no baseline)

Run pnpm size locally to check current initial sizes.

Q30 asked that the derived-map merge in `combine` not repeat the existing
duplicate-state-key warning's habit of dropping the slice name. It did. The
derived warning now names the declaring slice, which matters more for a formula
than for a state key — you cannot grep for it.

Q21's rendering rule is that an empty resolved value means render nothing, "no
poster element", rather than an image with an empty source. `<Poster>` was
returning an `<img>` with no `src` attribute, which avoided the `src=""`
request-the-current-page bug but not the element. Since the skins now render
`<Poster />` unconditionally, that put a stray `<img>` in every player without a
poster — and the blur-up placeholder keys off
`img[data-visible]:not([data-loaded])`, so a skin given a `placeholder` but no
`content-poster` would show it forever, waiting for a load that can never
happen. `<Poster>` now returns null when there is nothing to render, keeping the
`srcSet` and `render` escape hatches, and a local `src=""` takes the same path —
the deliberate local exception Q19 anticipated.

The JSDoc explaining why the `content*` fields exist beside `title` and `poster`
— Q21's open obligation — was attached to `MediaContentTitleEvents` rather than
the capability. The site builder extracts JSDoc for API reference pages, so it
was documenting the wrong export. Moved onto the capability, with the poster and
alt capabilities pointing at it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant