Skip to content

feat(packages)!: configure orientation lock through providers - #1999

Open
decepulis wants to merge 11 commits into
mainfrom
claude/videojs-v10-issue-1942-fi9rh9
Open

feat(packages)!: configure orientation lock through providers#1999
decepulis wants to merge 11 commits into
mainfrom
claude/videojs-v10-issue-1942-fi9rh9

Conversation

@decepulis

@decepulis decepulis commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Closes #1942.

Stacked on #2000 — this branch contains its commits, so merge that one first. Its feature-reference generator is what gives this page a Configuration table, and reading orientation lock's config needed a change to it (below).

Moves the orientation lock type from a feature-factory argument to declared provider configuration, and deletes the factory-configured feature form now that its only caller is gone.

Docs preview

What is this?

  1. orientationLockType becomes provider configuration — an attribute in HTML, a prop in React — and can change while the player runs
  2. features.orientationLock({ type }) is gone, along with ConfigurablePlayerFeature, ConfigurablePlayerFeatureConfig, OrientationLockFeatureConfig, forConfig, definePlayerSlice, and definePlayerFeature's two-argument overload
  3. selectOrientationLock joins the other feature selectors, declared beside its feature so it stays out of bundles that never select it
  4. createScreenOrientationLock takes the type per lock() call instead of capturing it at construction, and serializes platform requests so the screen cannot desync from the store
  5. ScreenOrientationLockType is exported, having previously been only structurally reachable
  6. The generator learns the second config convention, so the reference page gets its Configuration, State, and Actions tables

The API

Select the feature as a value, not a call:

- features: [...videoFeatures, features.orientationLock({ type: 'portrait' })]
+ features: [...videoFeatures, features.orientationLock]

Then set the type on the provider — orientationLockType="portrait" as a prop on Player.Provider in React, orientation-lock-type="portrait" as an attribute on the player element in HTML. Clearing the value restores landscape. Read it with selectOrientationLock, or set it imperatively with setOrientationLockType.

The PR title carries the ! so the squash-merged commit marks the break for release-please.

What this isn't

  • No feature-owned HTML conversion metadata. The issue sketched String / Boolean / Number sentinels and a non-string fixture. Orientation lock is string-valued and needs none of it; Boolean is unsound under the current model, because a removed boolean attribute converts to false rather than absent (reactive-element.ts:186), so a Boolean key defaulting to true could never be cleared through markup; and once the sentinel is checked against the action's parameter type it only restates what the compiler already knows. Worth designing against a feature that actually has a non-string value.
  • No validation. A typo'd orientation-lock-type="banana" reaches screen.orientation.lock(), which rejects, and the existing catch swallows it — so it silently does not lock rather than falling back to the default. Left out to keep the config API unchanged. The empty string is the exception, since a valueless attribute is ordinary markup rather than a typo; it restores the default.
  • No per-adapter attribute, property, and prop coverage. contentTitle already proves that plumbing, so each adapter gets one wiring test.

Funny business

  • The reference generator knew one config convention, and orientation lock uses the other. feat(site): document metadata feature #2000 reads a config entry's action and state as identifiers bound to private symbols, which is right for metadata — three owners compete for contentTitle, so its state is symbol-keyed and private. Orientation lock publishes its state and names the same members with strings, so its input was dropped as malformed. Dropping it then cost the whole page: a feature's own file joins the TypeScript program only when it has config or derived keys, so losing the config also lost the lookup of OrientationLockState, and the feature generated no reference at all. On main today that is a silent skip, and this is the only feature page whose FeatureReference renders nothing.

    A state member is now matched by the identifier inside a computed key or by its own name, and initial values are read under both spellings so the input keeps its landscape default. A value that names no member is still dropped and still warns. The feature file itself only gains a JSDoc line for the input's description. A poster fixture covers the published-key convention end to end; captionStyle still pins the degrade paths.

  • selectOrientationLock is declared beside its feature instead of in selectors.ts. createSelector evaluates its slice at module load and every selector in that module shares one evaluation, so an entry there retains this feature in every bundle importing any selector. The other features ship in presets, so their cost is already paid; this one is in no preset, and an entry there measured +290 B per @videojs/html UI component and +298 B per React one. Declared beside the feature, 9 of 291 entries move, all inside compressor noise except @videojs/core/dom — the aggregate entry that includes everything — at +34 B. A /* @__PURE__ */ annotation on the selectors.ts form was measured too and does not fix it: 76 entries still grow, by up to 116 B.

    The consequence is that api-docs-builder scans selectors.ts as a leaf entry point, so a selector declared outside it silently gets no reference page. The feature module is added to UTIL_ENTRY_POINTS, which yields select-orientation-lock.json and no stray pages. The placement rationale is a plain comment rather than JSDoc, since the extractor publishes JSDoc verbatim to the reference page.

  • Making the type responsive exposed three ways the lock could desync from the screen, all from one variable standing for both "last requested" and "type we hold" (fixed in d945a1f):

    • A rejected re-lock was recorded as held. After landscape succeeded and a portrait request rejected, the guard suppressed every later portrait request — the screen stayed landscape permanently while the store and the lock both reported portrait.
    • Overlapping requests hit the same problem from the other side. The platform does not guarantee settle order, so whichever request landed last won the screen while the bookkeeping described the other.
    • Where the platform rejects — desktop Chrome, outside mobile form factors — nothing ever converged, so every published store change re-issued a rejected screen.orientation.lock(). The store publishes at several hertz during playback.

    The primitive now tracks desired and held separately and reconciles between them one request at a time. A re-entrant call records the new type and returns; the running pass re-reads desired before it exits, so the last request wins and platform requests never overlap. A rejection leaves held alone, which keeps it describing the platform and lets a later request for the same type through.

  • attach compares a resolved value rather than detecting a fullscreen edge. Edge detection would not have fired on a config change during steady-state fullscreen, so one sync now serves fullscreen events and config changes alike. Comparing the resolved (fullscreen, type) value is also what keeps the whole-store subscription from re-requesting a rejected lock.

  • A single-owner value does not need a private symbol. Nothing in media donates an orientation preference, so there is nothing to resolve: orientationLockType is a published source key defaulting to landscape, with setOrientationLockType restoring that default for absent input — no symbol and no derived formula, unlike contentTitle, which needs both because three owners compete for it. Publishing the key is also what makes it observable, since publish copies string keys only (store.ts:132) and a symbol-keyed change notifies nobody — and this behavior reacts to its own configuration. internal/design/store/resolved-feature-state.md previously stated config inputs are always symbol-keyed; it now records this case.

  • ConfigActionKey required actions to be mutually assignable with string | null | undefined, which rejects ScreenOrientationLockType | null | undefined. The reverse clause is dropped so a narrower union keeps its own type on the provider input. Actions must still accept null | undefined so providers can clear.

  • The HTML docs example was inert. It set orientation-lock-type on a player whose videoFeatures preset does not include orientation lock, contradicting the same page's note that the feature is not in the default presets. It now defines an element that selects the feature. The fullscreen page's orientation section keeps a code example, updated from the deleted factory call to the provider input, and framework-gated — the sample it replaced was React-only code shown to HTML readers too.

Testing

Verified after merging origin/main and #2000's branch. pnpm typecheck, pnpm lint, pnpm check:workspace (10/10), and pnpm -F site astro check (0 errors, 0 warnings) pass. Package suites pass individually — core 1417, html 307, react 395, store 124, and site 548, which includes the api-docs-builder suite at 221.

Coverage added for initial config, updates, clearing to the default, detach preservation, post-detach quiescence, re-lock during fullscreen, selector identity and shape, and one wiring test per adapter. Four builder tests cover the published-key config convention: input type, default, JSDoc description, and the state interface read from the feature's own file.

The lock state machine is pinned by exact platform call counts rather than by observing a later unlock() — the earlier supersede test passed under a mutation of the branch it claimed to cover. Each new guard was mutation-checked: dropping serialization, recording a rejection as held, ?? for ||, and dropping the sync comparison each fail at least one test.

Also fixes cross-test leakage in orientation-lock.test.ts, where stores were never destroyed, so their document fullscreen listeners stayed live and reacted to later tests' dispatches.

https://claude.ai/code/session_01DdtP5uKxwgyhrDLEg5T6in

Move the orientation lock type from a feature-factory argument to declared
provider configuration, so React props and HTML attributes can set it and
change it during the player's lifetime.

The type is a single-owner value: nothing in media donates an orientation
preference. It is therefore a published source key defaulting to landscape,
with `setOrientationLockType` applying that default on nullish input, rather
than the private symbol plus `derived` formula that a multi-owner value like
`contentTitle` needs. Publishing it is also what makes it observable, since
`publish` copies string keys only and a symbol-keyed change notifies nobody.

`createScreenOrientationLock` now takes the type per `lock()` call instead of
capturing it at construction, which was the latent bug behind a responsive
config: the store half would look correct while the lock kept using whatever
value was set at attach. `desired` widens from a boolean to the requested type
so it doubles as the re-lock comparison and discards a superseded in-flight
request. `attach` drops its `wasFullscreen` edge detection for a value-based
`sync`, letting one handler serve fullscreen events and config changes alike.

`ConfigActionKey` no longer requires an action to be mutually assignable with
`string | null | undefined`; a narrower union such as this string enum keeps
its own type on the provider input. Actions must still accept `null` and
`undefined` so providers can clear an absent input.

Removes the legacy factory-configured `definePlayerFeature` overload,
`ConfigurablePlayerFeature`, and `ConfigurablePlayerFeatureConfig`. Orientation
lock was the only caller.

Closes #1942

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

netlify Bot commented Aug 6, 2026

Copy link
Copy Markdown

Deploy Preview for vjs10-site ready!

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

@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 12, 2026 12:10am

Request Review

claude added 2 commits August 6, 2026 23:00
Adds `reference/feature-metadata`, the last preset feature without a
reference page, and registers it in the Features sidebar. The page
covers title precedence, the media `contentData.title` tier, and the
`contentTitle` / `defaultContentTitle` provider inputs for both React
props and HTML attributes.

The feature keeps its per-owner title inputs in a private, symbol-keyed
`MetadataSourceState` and publishes the resolved shape through `derived`,
so the builder found no interface behind the `state()` annotation and
skipped the feature entirely — no `metadata.json`, no generated State or
Actions tables. Features in that shape now name their published interface
with `@state <InterfaceName>` on the feature export, which takes
precedence over the `state()` annotation. `metadataFeature` points at the
existing `MediaMetadataState`.

This is only the state and actions gap. Generated configuration sections
for feature references remain open in #1940.

Refs #1940

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RwRUKpEp5dgz9bQwmd8kZo
Feature references documented state and actions but said nothing about
the provider inputs a selected feature adds. The builder now reads each
feature's `config` map and emits a `config` record alongside state and
actions; feature pages render it as a Configuration section ahead of
State, mirroring props-before-state in component references.

Each input is typed from the private action it forwards to, defaulted
from the initializer of the state key it writes, and described by the
JSDoc on its `config` entry — so the source declaration stays the only
inventory. React shows the prop name; HTML adds the kebab-cased
attribute, derived the same way `createProviderMixin` derives it. Both
variants state that the surface exists only while the feature is
selected.

Unconfigured features emit `config: {}` and render no section, so
existing reference output is unchanged.

The metadata fixture now mirrors the real feature — symbol-keyed private
state, actions behind those symbols, a `satisfies`-wrapped config map,
and `derived` — so the E2E suite exercises the shape the builder meets
in the repo.

Closes #1940

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RwRUKpEp5dgz9bQwmd8kZo
@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
/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-data 154.03 kB 26.69 kB -127.33 kB (-82.7%)
/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.95 kB +1.93 kB (+1.0%) 0 B
/video (minimal + hls) 191.44 kB 193.55 kB +2.12 kB (+1.1%) 0 B
Small changes (64, ≤ 300 B)
Path Base initial PR initial Initial diff Lazy diff
/video/minimal-skin 55.36 kB 55.38 kB +28 B (+0.0%) 0 B
/video/minimal-skin.tailwind 55.97 kB 55.99 kB +23 B (+0.0%) 0 B
/video/player 10.01 kB 9.89 kB -124 B (-1.2%) 0 B
/video/skin 55.74 kB 55.77 kB +32 B (+0.1%) 0 B
/video/skin.tailwind 56.44 kB 56.48 kB +42 B (+0.1%) 0 B
/audio/minimal-skin 44.07 kB 44.19 kB +124 B (+0.3%) 0 B
/audio/minimal-skin.tailwind 44.66 kB 44.71 kB +60 B (+0.1%) 0 B
/audio/player 6.99 kB 6.90 kB -99 B (-1.4%) 0 B
/audio/skin 47.92 kB 48.00 kB +81 B (+0.2%) 0 B
/audio/skin.tailwind 48.47 kB 48.51 kB +42 B (+0.1%) 0 B
/background/player 5.32 kB 5.31 kB -13 B (-0.2%) 0 B
/live-video/minimal-skin 51.67 kB 51.71 kB +45 B (+0.1%) 0 B
/live-video/minimal-skin.tailwind 52.26 kB 52.31 kB +54 B (+0.1%) 0 B
/live-video/player 9.42 kB 9.31 kB -117 B (-1.2%) 0 B
/live-video/skin 54.20 kB 54.25 kB +49 B (+0.1%) 0 B
/live-video/skin.tailwind 54.74 kB 54.85 kB +109 B (+0.2%) 0 B
/live-audio/minimal-skin 36.44 kB 36.49 kB +51 B (+0.1%) 0 B
/live-audio/minimal-skin.tailwind 35.83 kB 35.91 kB +73 B (+0.2%) 0 B
/live-audio/player 7.01 kB 6.90 kB -113 B (-1.6%) 0 B
/live-audio/skin 40.38 kB 40.43 kB +51 B (+0.1%) 0 B
/live-audio/skin.tailwind 39.88 kB 39.97 kB +93 B (+0.2%) 0 B
/ui/airplay-button 10.79 kB 10.68 kB -113 B (-1.0%) 0 B
/ui/alert-dialog 4.14 kB 4.13 kB -9 B (-0.2%)
/ui/alert-dialog-close 1.94 kB 1.93 kB -9 B (-0.5%)
/ui/audio-track-radio-group 10.56 kB 10.42 kB -144 B (-1.3%) 0 B
/ui/buffering-indicator 8.04 kB 7.91 kB -134 B (-1.6%)
/ui/captions-button 11.06 kB 10.93 kB -130 B (-1.1%) 0 B
/ui/captions-radio-group 10.62 kB 10.49 kB -132 B (-1.2%) 0 B
/ui/cast-button 10.77 kB 10.64 kB -128 B (-1.2%) 0 B
/ui/compounds 35.98 kB 36.06 kB +88 B (+0.2%) 0 B
/ui/controls 8.21 kB 8.07 kB -147 B (-1.7%) 0 B
/ui/error-dialog 11.20 kB 11.08 kB -121 B (-1.1%) 0 B
/ui/fullscreen-button 10.74 kB 10.63 kB -114 B (-1.0%) 0 B
/ui/hotkey 9.32 kB 9.18 kB -142 B (-1.5%)
/ui/menu 20.50 kB 20.54 kB +38 B (+0.2%) 0 B
/ui/mute-button 10.76 kB 10.64 kB -124 B (-1.1%) 0 B
/ui/pip-button 10.74 kB 10.64 kB -102 B (-0.9%) 0 B
/ui/play-button 10.73 kB 10.61 kB -125 B (-1.1%) 0 B
/ui/playback-rate-button 10.93 kB 10.81 kB -126 B (-1.1%) 0 B
/ui/playback-rate-radio-group 10.33 kB 10.22 kB -119 B (-1.1%) 0 B
/ui/popover 7.61 kB 7.78 kB +172 B (+2.2%)
/ui/poster 8.40 kB 8.27 kB -134 B (-1.6%) 0 B
/ui/quality-radio-group 11.12 kB 10.99 kB -138 B (-1.2%) 0 B
/ui/seek-button 10.79 kB 10.66 kB -134 B (-1.2%) 0 B
/ui/seek-indicator 12.51 kB 12.40 kB -121 B (-0.9%)
/ui/slider 11.60 kB 11.49 kB -115 B (-1.0%) 0 B
/ui/status-announcer 10.14 kB 10.05 kB -98 B (-0.9%) 0 B
/ui/status-indicator 13.02 kB 12.90 kB -120 B (-0.9%) 0 B
/ui/thumbnail 9.17 kB 9.04 kB -137 B (-1.5%)
/ui/time 10.53 kB 10.39 kB -144 B (-1.3%) 0 B
/ui/time-slider 14.51 kB 14.40 kB -114 B (-0.8%) 0 B
/ui/tooltip 8.89 kB 9.07 kB +185 B (+2.0%) 0 B
/ui/volume-indicator 13.04 kB 12.92 kB -121 B (-0.9%) 0 B
/ui/volume-slider 12.22 kB 12.10 kB -121 B (-1.0%) 0 B
/media/dash-video 211.28 kB 211.36 kB +81 B (+0.0%)
/media/simple-hls-audio-only 21.62 kB 21.59 kB -23 B (-0.1%)
/media/simple-hls-video 27.04 kB 27.08 kB +33 B (+0.1%)
/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%)
/video (default) 55.76 kB 55.76 kB +7 B (+0.0%) 0 B
/video (minimal) 55.42 kB 55.41 kB -7 B (-0.0%) 0 B
/audio (default) 47.93 kB 48.07 kB +140 B (+0.3%) 0 B
/audio (minimal) 44.08 kB 44.21 kB +130 B (+0.3%) 0 B
/background 5.59 kB 5.59 kB -9 B (-0.2%) 0 B
Presets (7)
Entry Initial Lazy
/video (default) 55.76 kB 56.93 kB
/video (default + hls) 193.95 kB 56.93 kB
/video (minimal) 55.41 kB 56.93 kB
/video (minimal + hls) 193.55 kB 56.93 kB
/audio (default) 48.07 kB 56.93 kB
/audio (minimal) 44.21 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.36 kB
/media/google-cast 6.44 kB
/media/hlsjs-video 140.20 kB
/media/mux-audio 140.92 kB
/media/mux-data 26.69 kB
/media/mux-video 141.13 kB
/media/native-hls-video 7.51 kB
/media/simple-hls-audio-only 21.59 kB
/media/simple-hls-video 27.08 kB
/media/vimeo-video 12.58 kB
/media/youtube-video 6.67 kB
Players (5)
Entry Initial Lazy
/video/player 9.89 kB 56.93 kB
/audio/player 6.90 kB 56.93 kB
/background/player 5.31 kB 56.93 kB
/live-video/player 9.31 kB 56.93 kB
/live-audio/player 6.90 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.38 kB 56.93 kB
/video/minimal-skin.tailwind js 55.99 kB 56.93 kB
/video/skin js 55.77 kB 56.93 kB
/video/skin.tailwind js 56.48 kB 56.93 kB
/audio/minimal-skin.css css 4.11 kB
/audio/skin.css css 4.05 kB
/audio/minimal-skin js 44.19 kB 56.93 kB
/audio/minimal-skin.tailwind js 44.71 kB 56.93 kB
/audio/skin js 48.00 kB 56.93 kB
/audio/skin.tailwind js 48.51 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.71 kB 56.93 kB
/live-video/minimal-skin.tailwind js 52.31 kB 56.93 kB
/live-video/skin js 54.25 kB 56.93 kB
/live-video/skin.tailwind js 54.85 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.49 kB 56.93 kB
/live-audio/minimal-skin.tailwind js 35.91 kB 56.93 kB
/live-audio/skin js 40.43 kB 56.93 kB
/live-audio/skin.tailwind js 39.97 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 (39)
Entry Initial Lazy
/ui/airplay-button 10.68 kB 56.93 kB
/ui/alert-dialog 4.13 kB
/ui/alert-dialog-close 1.93 kB
/ui/alert-dialog-description 1.61 kB
/ui/alert-dialog-title 1.61 kB
/ui/audio-track-radio-group 10.42 kB 56.93 kB
/ui/buffering-indicator 7.91 kB
/ui/captions-button 10.93 kB 56.93 kB
/ui/captions-radio-group 10.49 kB 56.93 kB
/ui/cast-button 10.64 kB 56.93 kB
/ui/compounds 36.06 kB 56.93 kB
/ui/controls 8.07 kB 56.93 kB
/ui/error-dialog 11.08 kB 56.93 kB
/ui/fullscreen-button 10.63 kB 56.93 kB
/ui/hotkey 9.18 kB
/ui/menu 20.54 kB 56.93 kB
/ui/mute-button 10.64 kB 56.93 kB
/ui/pip-button 10.64 kB 56.93 kB
/ui/play-button 10.61 kB 56.93 kB
/ui/playback-rate-button 10.81 kB 56.93 kB
/ui/playback-rate-radio-group 10.22 kB 56.93 kB
/ui/popover 7.78 kB
/ui/poster 8.27 kB 56.93 kB
/ui/quality-radio-group 10.99 kB 56.93 kB
/ui/seek-button 10.66 kB 56.93 kB
/ui/seek-indicator 12.40 kB
/ui/seek-indicator-value 1.02 kB
/ui/slider 11.49 kB 56.93 kB
/ui/status-announcer 10.05 kB 56.93 kB
/ui/status-indicator 12.90 kB 56.93 kB
/ui/status-indicator-value 1.02 kB
/ui/thumbnail 9.04 kB
/ui/time 10.39 kB 56.93 kB
/ui/time-slider 14.40 kB 56.93 kB
/ui/tooltip 9.07 kB 56.93 kB
/ui/volume-indicator 12.92 kB 56.93 kB
/ui/volume-indicator-fill 1.03 kB
/ui/volume-indicator-value 1.02 kB
/ui/volume-slider 12.10 kB 56.93 kB

⚛️ @videojs/react

Path Base initial PR initial Initial diff Lazy diff
/media/hlsjs-video 136.61 kB 138.60 kB +2.00 kB (+1.5%)
/media/mux-audio 137.53 kB 139.38 kB +1.84 kB (+1.3%)
/media/mux-data 152.88 kB 25.47 kB -127.41 kB (-83.3%)
/media/mux-video 137.58 kB 139.43 kB +1.85 kB (+1.3%)
/media/native-hls-video 3.53 kB 5.75 kB +2.22 kB (+62.9%)
/video (default + hls) 178.49 kB 180.36 kB +1.87 kB (+1.0%) 0 B
/video (minimal + hls) 178.45 kB 180.27 kB +1.82 kB (+1.0%) 0 B
Small changes (59, ≤ 300 B)
Path Base initial PR initial Initial diff Lazy diff
/media/dash-video 209.69 kB 209.68 kB -14 B (-0.0%)
/media/simple-hls-audio-only 19.88 kB 19.89 kB +15 B (+0.1%)
/media/simple-hls-video 25.34 kB 25.41 kB +64 B (+0.2%)
/media/vimeo-video 10.76 kB 10.75 kB -1 B (-0.0%)
/media/youtube-video 4.78 kB 4.78 kB +7 B (+0.1%)
/video/minimal-skin 43.50 kB 43.44 kB -64 B (-0.1%) 0 B
/video/minimal-skin.tailwind 49.73 kB 49.60 kB -131 B (-0.3%) 0 B
/video/skin 43.42 kB 43.36 kB -54 B (-0.1%) 0 B
/video/skin.tailwind 49.66 kB 49.58 kB -86 B (-0.2%) 0 B
/audio/minimal-skin 35.80 kB 35.75 kB -52 B (-0.1%) 0 B
/audio/minimal-skin.tailwind 38.39 kB 38.27 kB -124 B (-0.3%) 0 B
/audio/skin 35.80 kB 35.70 kB -97 B (-0.3%) 0 B
/audio/skin.tailwind 40.16 kB 40.04 kB -120 B (-0.3%) 0 B
/background/skin 272 B 273 B +1 B (+0.4%)
/live-video/minimal-skin 38.50 kB 38.40 kB -96 B (-0.2%) 0 B
/live-video/minimal-skin.tailwind 44.74 kB 44.64 kB -104 B (-0.2%) 0 B
/live-video/skin 38.51 kB 38.41 kB -107 B (-0.3%) 0 B
/live-video/skin.tailwind 44.71 kB 44.69 kB -19 B (-0.0%) 0 B
/live-audio/minimal-skin 26.41 kB 26.29 kB -125 B (-0.5%) 0 B
/live-audio/minimal-skin.tailwind 29.71 kB 29.57 kB -137 B (-0.5%) 0 B
/live-audio/skin 26.44 kB 26.40 kB -47 B (-0.2%) 0 B
/live-audio/skin.tailwind 29.85 kB 29.73 kB -117 B (-0.4%) 0 B
/ui/airplay-button 10.04 kB 9.91 kB -130 B (-1.3%) 0 B
/ui/alert-dialog 3.25 kB 3.25 kB -3 B (-0.1%)
/ui/audio-track 7.49 kB 7.37 kB -125 B (-1.6%) 0 B
/ui/buffering-indicator 7.26 kB 7.14 kB -124 B (-1.7%)
/ui/captions-button 10.01 kB 9.88 kB -127 B (-1.2%) 0 B
/ui/captions-radio-group 7.62 kB 7.51 kB -107 B (-1.4%) 0 B
/ui/cast-button 10.02 kB 9.88 kB -141 B (-1.4%) 0 B
/ui/controls 6.98 kB 6.86 kB -122 B (-1.7%)
/ui/error-dialog 9.99 kB 9.86 kB -130 B (-1.3%) 0 B
/ui/fullscreen-button 9.98 kB 9.86 kB -121 B (-1.2%) 0 B
/ui/gesture 7.37 kB 7.24 kB -134 B (-1.8%)
/ui/hotkey 7.82 kB 7.68 kB -135 B (-1.7%)
/ui/live-button 8.42 kB 8.31 kB -114 B (-1.3%) 0 B
/ui/menu 20.61 kB 20.52 kB -89 B (-0.4%) 0 B
/ui/mute-button 9.97 kB 9.85 kB -122 B (-1.2%) 0 B
/ui/pip-button 9.97 kB 9.86 kB -122 B (-1.2%) 0 B
/ui/play-button 9.96 kB 9.83 kB -134 B (-1.3%) 0 B
/ui/playback-rate 6.79 kB 6.67 kB -123 B (-1.8%)
/ui/playback-rate-button 9.96 kB 9.83 kB -130 B (-1.3%) 0 B
/ui/popover 7.47 kB 7.47 kB +2 B (+0.0%)
/ui/poster 6.86 kB 6.74 kB -117 B (-1.7%)
/ui/quality 8.01 kB 7.89 kB -120 B (-1.5%) 0 B
/ui/seek-button 9.99 kB 9.87 kB -129 B (-1.3%) 0 B
/ui/seek-indicator 11.80 kB 11.69 kB -112 B (-0.9%)
/ui/slider 11.96 kB 11.89 kB -76 B (-0.6%) 0 B
/ui/status-announcer 9.68 kB 9.57 kB -111 B (-1.1%) 0 B
/ui/status-indicator 12.26 kB 12.17 kB -99 B (-0.8%) 0 B
/ui/thumbnail 8.07 kB 7.95 kB -123 B (-1.5%)
/ui/time 9.48 kB 9.34 kB -137 B (-1.4%) 0 B
/ui/time-slider 12.06 kB 11.98 kB -86 B (-0.7%) 0 B
/ui/tooltip 8.22 kB 8.22 kB -3 B (-0.0%)
/ui/volume-indicator 12.37 kB 12.24 kB -129 B (-1.0%) 0 B
/ui/volume-slider 11.40 kB 11.29 kB -115 B (-1.0%) 0 B
/video (default) 43.48 kB 43.44 kB -37 B (-0.1%) 0 B
/video (minimal) 43.55 kB 43.44 kB -114 B (-0.3%) 0 B
/audio (default) 35.94 kB 35.77 kB -176 B (-0.5%) 0 B
/audio (minimal) 35.96 kB 35.86 kB -107 B (-0.3%) 0 B
Presets (7)
Entry Initial Lazy
/video (default) 43.44 kB 56.93 kB
/video (default + hls) 180.36 kB 56.93 kB
/video (minimal) 43.44 kB 56.93 kB
/video (minimal + hls) 180.27 kB 56.93 kB
/audio (default) 35.77 kB 56.93 kB
/audio (minimal) 35.86 kB 56.93 kB
/background 581 B
Media (12)
Entry Initial
/media/background-video 394 B
/media/dash-video 209.68 kB
/media/google-cast 5.34 kB
/media/hlsjs-video 138.60 kB
/media/mux-audio 139.38 kB
/media/mux-data 25.47 kB
/media/mux-video 139.43 kB
/media/native-hls-video 5.75 kB
/media/simple-hls-audio-only 19.89 kB
/media/simple-hls-video 25.41 kB
/media/vimeo-video 10.75 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.44 kB 56.93 kB
/video/minimal-skin.tailwind js 49.60 kB 56.93 kB
/video/skin js 43.36 kB 56.93 kB
/video/skin.tailwind js 49.58 kB 56.93 kB
/audio/minimal-skin.css css 3.97 kB
/audio/skin.css css 3.90 kB
/audio/minimal-skin js 35.75 kB 56.93 kB
/audio/minimal-skin.tailwind js 38.27 kB 56.93 kB
/audio/skin js 35.70 kB 56.93 kB
/audio/skin.tailwind js 40.04 kB 56.93 kB
/background/skin.css css 90 B
/background/skin js 273 B
/live-video/minimal-skin.css css 5.81 kB
/live-video/skin.css css 5.81 kB
/live-video/minimal-skin js 38.40 kB 56.93 kB
/live-video/minimal-skin.tailwind js 44.64 kB 56.93 kB
/live-video/skin js 38.41 kB 56.93 kB
/live-video/skin.tailwind js 44.69 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.29 kB 56.93 kB
/live-audio/minimal-skin.tailwind js 29.57 kB 56.93 kB
/live-audio/skin js 26.40 kB 56.93 kB
/live-audio/skin.tailwind js 29.73 kB 56.93 kB
UI Components (33)
Entry Initial Lazy
/ui/airplay-button 9.91 kB 56.93 kB
/ui/alert-dialog 3.25 kB
/ui/audio-track 7.37 kB 56.93 kB
/ui/buffering-indicator 7.14 kB
/ui/captions-button 9.88 kB 56.93 kB
/ui/captions-radio-group 7.51 kB 56.93 kB
/ui/cast-button 9.88 kB 56.93 kB
/ui/controls 6.86 kB
/ui/error-dialog 9.86 kB 56.93 kB
/ui/fullscreen-button 9.86 kB 56.93 kB
/ui/gesture 7.24 kB
/ui/hotkey 7.68 kB
/ui/live-button 8.31 kB 56.93 kB
/ui/menu 20.52 kB 56.93 kB
/ui/mute-button 9.85 kB 56.93 kB
/ui/pip-button 9.86 kB 56.93 kB
/ui/play-button 9.83 kB 56.93 kB
/ui/playback-rate 6.67 kB
/ui/playback-rate-button 9.83 kB 56.93 kB
/ui/popover 7.47 kB
/ui/poster 6.74 kB
/ui/quality 7.89 kB 56.93 kB
/ui/seek-button 9.87 kB 56.93 kB
/ui/seek-indicator 11.69 kB
/ui/slider 11.89 kB 56.93 kB
/ui/status-announcer 9.57 kB 56.93 kB
/ui/status-indicator 12.17 kB 56.93 kB
/ui/thumbnail 7.95 kB
/ui/time 9.34 kB 56.93 kB
/ui/time-slider 11.98 kB 56.93 kB
/ui/tooltip 8.22 kB
/ui/volume-indicator 12.24 kB 56.93 kB
/ui/volume-slider 11.29 kB 56.93 kB
🧩 @videojs/core — 3 small size changes
Path Base initial PR initial Initial diff Lazy diff
. 11.78 kB 11.77 kB -7 B (-0.1%)
/dom 20.00 kB 20.02 kB +22 B (+0.1%)
/i18n 3.13 kB 3.11 kB -21 B (-0.7%) 0 B
Entries (76)
Entry Initial Lazy
. 11.77 kB
/dom 20.02 kB
/components 900 B
/i18n 3.11 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 — 1 small size change
Path Base initial PR initial Initial diff Lazy diff
. 1.72 kB 1.71 kB -1 B (-0.1%)
Entries (3)
Entry Initial
. 1.71 kB
/html 703 B
/react 366 B
🔧 @videojs/utils — 3 small size changes
Path Base initial PR initial Initial diff Lazy diff
/events 319 B 320 B +1 B (+0.3%)
/predicate 265 B 297 B +32 B (+12.1%)
/style 190 B 188 B -2 B (-1.1%)
Entries (13)
Entry Initial
/array 104 B
/dom 3.29 kB
/events 320 B
/function 369 B
/i18n 44 B
/jwt 176 B
/object 508 B
/predicate 297 B
/percent 281 B
/string 239 B
/style 188 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.61 kB +2.10 kB (+1.3%)
/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.38 kB +216 B (+0.1%)
/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.38 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.61 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 — 3 small size changes
Path Base initial PR initial Initial diff Lazy diff
/hls 20.86 kB 20.86 kB -5 B (-0.0%)
/background-video 14.76 kB 14.77 kB +3 B (+0.0%)
/simple-hls-audio-only 19.44 kB 19.39 kB -53 B (-0.3%)
Entries (7)
Entry Initial
. 4.53 kB
/dom 6.57 kB
/hls 20.86 kB
/media-tracks 504 B
/background-video 14.77 kB
/simple-hls 24.92 kB
/simple-hls-audio-only 19.39 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.

claude added 2 commits August 7, 2026 16:29
Replaces the `@state` tag from the previous commit. That tag was a new
convention every feature author would have to know, and it failed
silently: misspelling the interface name made the generator skip the
feature and exit zero, publishing a page whose API tables had quietly
vanished.

Nothing needs to mark the published shape, because it is already
derivable. Every non-symbol property of the source state is public —
inherited members included, carrying their JSDoc across files — and
`derived` keys are published on top, typed from what they return.
Symbol-keyed members are private by construction, and TypeScript names
them `__@SYMBOL@id`, which is how they are recognized. Features whose
annotation names a media/state.ts interface keep the existing lookup, so
their output is unchanged; only the fallback is new. A feature file joins
the program solely when the checker must read it.

A feature that derives its shape now describes itself from its own export
JSDoc rather than borrowing an interface description.

Also hardens the parts that could ship wrong output quietly:

- Config inputs warn instead of degrading in silence — when an action has
  no matching source-state member (type falls back to "unknown") and when
  an entry's action or state is not a plain identifier (input dropped).
- A named-constant initializer resolves to its literal, so a default reads
  as `'Untitled'` rather than leaking a private identifier into the docs.
- The fixture `PlayerFeatureConfig` mirrors the real constraint rather than
  loosening it to `PropertyKey`. Fixtures are inside site/tsconfig.json, so
  `astro check` now rejects a fixture config that production would reject;
  this immediately caught the mock passing `any` where the real overload
  takes a constrained generic.
- A new caption-style fixture covers both degrade paths, asserting the
  warnings alongside the output.

Verified all 17 other generated feature files are byte-identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RwRUKpEp5dgz9bQwmd8kZo
Review found three ways the responsive lock could desync from the screen,
all from `desired` doubling as "last requested" and "type we hold".

A rejected re-lock was recorded as held: after landscape succeeded and a
portrait request rejected, `locked` stayed true while `desired` moved to
portrait, so the `locked && desired === type` guard suppressed every later
portrait request. The screen stayed landscape permanently while the store
and the lock both reported portrait. Overlapping requests had the same
problem from the other side — the platform does not guarantee settle order,
so whichever landed last won the screen while `desired` described the other.

`createScreenOrientationLock` now tracks `desired` and `held` separately and
reconciles between them one request at a time. A re-entrant call records the
new type and returns; the running pass re-reads `desired` before it exits, so
the last request wins and platform requests never overlap. A rejection leaves
`held` alone, which keeps it describing the platform and lets a later request
for the same type through.

`attach` compares the resolved `(fullscreen, type)` value before acting. The
feature subscribes to the whole store, which publishes on every state change
— `currentTime` alone runs at several hertz during playback. Where the
platform rejects, as desktop Chrome does outside mobile form factors, `held`
never catches up, so without this comparison every publish re-issued a
rejected `screen.orientation.lock()`. The deleted `wasFullscreen` edge
detection had made exactly one request per fullscreen entry.

`setOrientationLockType` treats the empty string as absent input alongside
nullish. A valueless `orientation-lock-type` attribute arrives as `''`, which
had disabled the lock silently rather than restoring the documented default.

Adds `selectOrientationLock`, which the feature needs now that it publishes
state that the reference pages tell consumers to read.

The HTML configuration example used `<video-player>`, whose `videoFeatures`
preset does not include orientation lock, so the attribute it demonstrated
was inert. It now defines an element that selects the feature. The fullscreen
page links to that setup instead of repeating an ungated React snippet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdtP5uKxwgyhrDLEg5T6in
@decepulis decepulis changed the title feat(packages): configure orientation lock through providers feat(packages)!: configure orientation lock through providers Aug 10, 2026
`createSelector` evaluates its slice when the selector module loads
(`selector.ts:30`), so an exported selector retains its feature in any bundle
that imports anything from `selectors.ts`. Every other feature with a selector
ships in a preset, so its cost is already paid. Orientation lock is opt-in and
was in no preset, which made a selector for it new weight for everyone.

Measured against the merge base: `selectOrientationLock` added 290 B to every
`@videojs/html` UI component and 298 B to every `@videojs/react` one — enough
to turn this PR's legacy-removal savings into a net increase. Without it, a
play-button entry is 120 B smaller than on main rather than 170 B larger.

The state is still public and still readable; only the pre-built selector is
gone. The reference page shows selecting the key directly and says why there
is no named selector, so the omission does not read as an oversight.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdtP5uKxwgyhrDLEg5T6in
Consumers of the orientation lock feature need a named selector like every
other feature has. Declaring it in `selectors.ts` costs every bundle that
imports any selector, because `createSelector` evaluates its slice at module
load and all selectors there share one evaluation. The other features ship in
presets, so their cost is already paid; orientation lock is in no preset, so
an entry there measured +290 B per `@videojs/html` UI component and +298 B
per React one.

Declaring it beside the feature keeps it out of those bundles: 9 of 291
entries move, all within compressor noise except `@videojs/core/dom`, the
aggregate entry, at +34 B. A `/* @__PURE__ */` annotation on the
`selectors.ts` form was measured too and does not fix it — 76 entries still
grow, by up to 116 B.

The api-docs-builder scans `selectors.ts` as a leaf entry point, so a
selector outside it gets no reference page. Add the feature module as an
entry point; it yields `select-orientation-lock.json` and no stray pages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdtP5uKxwgyhrDLEg5T6in
@decepulis
decepulis marked this pull request as ready for review August 11, 2026 23:56
The removed example called `features.orientationLock({ type })`, which this
branch replaces with provider configuration. Show the provider input instead
of leaving the section with only a link.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdtP5uKxwgyhrDLEg5T6in
claude added 2 commits August 12, 2026 00:04
…s-v10-issue-1942-fi9rh9

Stack on #2000 for the feature reference generator, which grows a
Configuration table and learns to read a feature's own state interface.

Conflict: both branches add a reference page to the sidebar. #2000's copy of
the section predates the restructure on main, so its sidebar entry moves into
the current Player Features list.
The feature reference generator assumed one config convention: a feature that
keeps its state private, naming its action and state through symbol constants.
Orientation lock publishes its state and names the same members with strings,
so its input was dropped, and dropping it kept the feature file out of the
program that resolves its state interface — no reference at all.

Match a state member by the identifier inside a computed key or by its own
name, and read initial values under both spellings so the input keeps its
default. A value that names no member is still dropped and still warns.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdtP5uKxwgyhrDLEg5T6in
@decepulis
decepulis changed the base branch from main to claude/metadata-feature-docs-sqmcgl August 12, 2026 00:09
@decepulis
decepulis changed the base branch from claude/metadata-feature-docs-sqmcgl to main August 12, 2026 00:10
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.

Move Orientation Lock to Provider Configuration

2 participants