feat(packages)!: configure orientation lock through providers - #1999
Open
decepulis wants to merge 11 commits into
Open
feat(packages)!: configure orientation lock through providers#1999decepulis wants to merge 11 commits into
decepulis wants to merge 11 commits into
Conversation
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
✅ Deploy Preview for vjs10-site ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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
Contributor
📦 Bundle Size Report🎨 @videojs/html
Small changes (64, ≤ 300 B)
Presets (7)
Media (13)
Players (5)
Skins (30)
UI Components (39)
⚛️ @videojs/react
Small changes (59, ≤ 300 B)
Presets (7)
Media (12)
Skins (27)
UI Components (33)
🧩 @videojs/core — 3 small size changes
Entries (76)
🏷️ @videojs/element — no changesEntries (2)
📦 @videojs/store — 1 small size change
Entries (3)
🔧 @videojs/utils — 3 small size changes
Entries (13)
📦 @videojs/jsx — no changesEntries (3)
📦 @videojs/media
Small changes (4, ≤ 300 B)
Entries (15)
📦 @videojs/spf — 3 small size changes
Entries (7)
ℹ️ How to interpretEach 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 |
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
`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
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
…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
changed the base branch from
main
to
claude/metadata-feature-docs-sqmcgl
August 12, 2026 00:09
decepulis
changed the base branch from
claude/metadata-feature-docs-sqmcgl
to
main
August 12, 2026 00:10
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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?
orientationLockTypebecomes provider configuration — an attribute in HTML, a prop in React — and can change while the player runsfeatures.orientationLock({ type })is gone, along withConfigurablePlayerFeature,ConfigurablePlayerFeatureConfig,OrientationLockFeatureConfig,forConfig,definePlayerSlice, anddefinePlayerFeature's two-argument overloadselectOrientationLockjoins the other feature selectors, declared beside its feature so it stays out of bundles that never select itcreateScreenOrientationLocktakes the type perlock()call instead of capturing it at construction, and serializes platform requests so the screen cannot desync from the storeScreenOrientationLockTypeis exported, having previously been only structurally reachableThe API
Select the feature as a value, not a call:
Then set the type on the provider —
orientationLockType="portrait"as a prop onPlayer.Providerin React,orientation-lock-type="portrait"as an attribute on the player element in HTML. Clearing the value restoreslandscape. Read it withselectOrientationLock, or set it imperatively withsetOrientationLockType.The PR title carries the
!so the squash-merged commit marks the break for release-please.What this isn't
String/Boolean/Numbersentinels and a non-string fixture. Orientation lock is string-valued and needs none of it;Booleanis unsound under the current model, because a removed boolean attribute converts tofalserather than absent (reactive-element.ts:186), so aBooleankey defaulting totruecould 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.orientation-lock-type="banana"reachesscreen.orientation.lock(), which rejects, and the existingcatchswallows 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.contentTitlealready 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
actionandstateas identifiers bound to private symbols, which is right for metadata — three owners compete forcontentTitle, 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 orderivedkeys, so losing the config also lost the lookup ofOrientationLockState, and the feature generated no reference at all. Onmaintoday that is a silent skip, and this is the only feature page whoseFeatureReferencerenders 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
landscapedefault. 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. Aposterfixture covers the published-key convention end to end;captionStylestill pins the degrade paths.selectOrientationLockis declared beside its feature instead of inselectors.ts.createSelectorevaluates 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/htmlUI 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 theselectors.tsform was measured too and does not fix it: 76 entries still grow, by up to 116 B.The consequence is that
api-docs-builderscansselectors.tsas a leaf entry point, so a selector declared outside it silently gets no reference page. The feature module is added toUTIL_ENTRY_POINTS, which yieldsselect-orientation-lock.jsonand 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):
screen.orientation.lock(). The store publishes at several hertz during playback.The primitive now tracks
desiredandheldseparately and reconciles between them one request at a time. A re-entrant call records the new type and returns; the running pass re-readsdesiredbefore it exits, so the last request wins and platform requests never overlap. A rejection leavesheldalone, which keeps it describing the platform and lets a later request for the same type through.attachcompares a resolved value rather than detecting a fullscreen edge. Edge detection would not have fired on a config change during steady-state fullscreen, so onesyncnow 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:
orientationLockTypeis a published source key defaulting tolandscape, withsetOrientationLockTyperestoring that default for absent input — no symbol and noderivedformula, unlikecontentTitle, which needs both because three owners compete for it. Publishing the key is also what makes it observable, sincepublishcopies 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.mdpreviously stated config inputs are always symbol-keyed; it now records this case.ConfigActionKeyrequired actions to be mutually assignable withstring | null | undefined, which rejectsScreenOrientationLockType | null | undefined. The reverse clause is dropped so a narrower union keeps its own type on the provider input. Actions must still acceptnull | undefinedso providers can clear.The HTML docs example was inert. It set
orientation-lock-typeon a player whosevideoFeaturespreset 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/mainand #2000's branch.pnpm typecheck,pnpm lint,pnpm check:workspace(10/10), andpnpm -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 thesynccomparison each fail at least one test.Also fixes cross-test leakage in
orientation-lock.test.ts, where stores were never destroyed, so theirdocumentfullscreen listeners stayed live and reacted to later tests' dispatches.https://claude.ai/code/session_01DdtP5uKxwgyhrDLEg5T6in