npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pagix/pdp-feature-video

v0.1.0

Published

Anvilkit Puck-native PDP feature video card — a large rounded media card (video or image, or neither) with a 'Watch full video' trigger that opens the full-length video in a player window of its own, plus a disclaimer run. The card carries six Puck-config

Downloads

31

Readme

@pagix/pdp-feature-video

Puck-native PDP feature video — a large rounded media card that loops a decorative clip, with a "watch the full video" trigger on its corner that opens the full-length video in a window of its own, and a disclaimer run under or over it. Replicates Huawei's section-feature-video / .design-feature-* module on a product detail page, with three axes the original does not have: the card carries six authored dimensions (background, width, height, padding, radius, position), the media is a tri-state (video / image / none), and both text runs are fully authorable (colour, size, weight, line height, align, box).

Usage

import { createPdpFeatureVideoConfig } from "@pagix/pdp-feature-video";

const config = createPdpFeatureVideoConfig({ locale: "zh" });

| Export | Purpose | | --- | --- | | componentConfig | The Puck ComponentConfig (English default) | | pdpFeatureVideoConfig | Convenience alias of componentConfig | | createPdpFeatureVideoConfig(options?) | Locale-aware factory ({ locale?, messages? }) | | createComponentConfig(options?) | Alias of the factory | | defaultProps / fields / metadata | Package contract | | PdpFeatureVideoView | The RSC-safe component | | PdpFeatureVideoWatchButton | The trigger — a new-tab link that opens a player window (client component) | | colorField / imageUrlField / videoUrlField / dimensionField / textStyleField | Puck field factories, re-exported so hosts can build matching fields |

The media tri-state

mediaType is a top-level radio field with three values, and each one renders a different payload in the same media layer:

| mediaType | Renders | Watch trigger | | --- | --- | --- | | video | A looped, muted, art-directed <video> | Yes, when watchVideoUrl is authored | | image | An art-directed <picture> | No | | none | The card's own bgColor and nothing else | No |

mediaType is top-level, not nested in a media object, because Puck's resolveFields inspects data.props — top-level entries only — so a type nested one level down could not drive the inspector panel. Same placement, and the same reason, as pdp-product-hero.backgroundType.

The image modes having no trigger is not a missing feature: a still or an empty card has no full-length video to open, and SPEC §9.4 records the absence as expected behaviour ("图片 / 无媒体模式下,看不到观看按钮"). That is also why resolveFields hides the full-video link along with the video sources under image and none — it would be a control with no effect.

resolveFields only ever changes what the panel renders, never what the node stores, so switching type and back restores the previous authoring rather than discarding it. The none state is a deliberate mode rather than a degenerate one: it is how the card's own six dimensions get authored before any asset exists, and the editor paints a "No media selected" hint over it so an empty frame does not read as a broken card.

Layout

In DOM order:

| Region | Note | Style target | | --- | --- | --- | | Section | Full-bleed band, entrance animation | root | | Card frame | The rounded surface cardStyle lands on; overlays are its siblings | card | | Media layer | The <video>, the <picture>, or the muted placeholder | media | | Watch trigger | Overlaid on the picture, not in the card's flow | watchButton | | Disclaimer | Over the picture at lg, beneath the card below it | disclaimer |

The trigger and the disclaimer are siblings of the media layer rather than children of it, so an authored cardStyle.padding insets the picture without also dragging the button off the frame's corner and the notice off its edge.

The card's six dimensions

cardStyle is one object field:

| Knob | Default | Note | | --- | --- | --- | | bgColor | #0A0A0A | The original's near-black card | | width | 0 | 0 → the original's per-breakpoint widths — see "Responsive" | | height | 0 | 0 → the media's own 16 / 9 frame | | padding | 0 | Insets the media inside the frame | | borderRadius | 0 | 0 → the original's per-breakpoint radii | | position | center | left / center / right / full-width |

Every measurement is a Dimension{ value, unit } with unit one of px / vw / % / rem — so the inspector offers a unit alongside each number rather than pinning the card to pixels. value <= 0 means "not authored" and emits no property at all, which is what lets the same field satisfy two SPEC items at once: 0 keeps the original's responsive dimensions (§9.6), and any authored value overrides them (§9.2).

position is an auto-margin on the free side (mr-auto / mx-auto / ml-auto) — the card is a block with an authored width, so there is no vertical axis to it. full-width is the exception: it overrides the authored width outright and drops the responsive width classes, exactly as SPEC §8.2 item 10 describes.

A stored document that predates a knob renders its declared default rather than crashing. Puck does not re-apply defaultProps at render time — it passes the node's stored props through verbatim — so the adapter merges each block over its declared default (styleBlock). Without that, a document written before cardStyle.width existed reaches the view as undefined and 500s the published page for a document the editor never produced.

The watch trigger

The card's loop is decorative; the full video is a separate asset. Clicking the trigger opens that asset in a new window whose whole viewport is the video.

Why the window is written rather than merely navigated to: handed a media URL, the browser renders its own media page, and that page sizes the video to its own box. Measured in Chromium, flower.mp4 opened in a 1280x720 window renders a 960x540 element floating in black. Writing the document is what makes the video fill the window, and it is the reason the trigger is the package's one "use client" module; the view stays a server component.

What the new window cannot do — measured, not assumed. It cannot put itself into browser fullscreen. User activation does not survive window.open: a document calling requestFullscreen() on load is refused with TypeError: Permissions check failed, the same refusal a plain navigation gets, while document.fullscreenEnabled still reads true. No window feature changes this — width / height are ignored too, so the popup is whatever window or tab the visitor's browser prefers. The visitor's own F11 / ⛶ remains the only route to real fullscreen in that window; filling it is the closest thing this side of the gesture can honestly deliver.

The trigger is still an <a href target="_blank" rel="noopener noreferrer">, and the handler is purely additive — it calls preventDefault() only once a player window actually exists, so the trigger degrades to exactly the new-tab link it used to be. All of these cases take that path, and all are real:

  • A blocked popup, and a cross-origin-isolated document (COOP same-origin makes window.open return null).
  • A modified click — ⌘/ctrl/shift/alt asks for the browser's own new tab, which is what the visitor wanted.
  • A window we cannot script: it is closed again and the link does its original job, rather than stranding a blank tab.

Right-click, middle-click, "open in new tab" and JavaScript-disabled all keep working on that same path.

rel="noopener noreferrer" is not optional on an author-supplied URL, and it guards exactly the path above — the one that really navigates. On the player path the same protection is applied directly: opener is set to null before the new document exists, so it never gets a handle back into the PDP.

The player document is built node by node — document.createElement, then video.src = href — never from an HTML string. watchVideoUrl is author-supplied, and a URL round-tripped through src="..." markup would be an injection point. Sizing is written as CSSOM properties for a related reason: a host's style-src blocks <style> and the style attribute but not property assignment. The address bar shows about:blank, which is the honest cost of a library that cannot add a route to its host.

If the media cannot load in that document — a host CSP whose media-src excludes the URL, or a dead link — the window is sent to the raw URL instead of leaving the visitor on a black screen. A top-level navigation answers to a different rule set, and it is the surface this trigger had before it could build its own.

The player starts paused, with the native controls on screen. Sound is the reason: a window opened this way has no user activation, so the browser refuses autoplay-with-sound and the first frame simply stands there until the visitor presses play. Measured — readyState reaches 4 with paused: true. Muting it to win the autoplay would trade a normal player for a silent one, which is the worse of the two.

Nothing is fetched until the trigger is used: the card's decorative loop is the only <video> in the page's markup, and the full-length URL appears as the anchor's href and nowhere else.

watchVideoUrl is separate from videoSource.pcVideoUrl rather than derived from it, because the card's loop and the full video are independent assets in the original. The trigger renders only when mediaType === "video" and a URL is authored — a still or an empty card has nothing to play.

Because a <video> element fetches whatever the URL points at rather than rendering it, the field carries a hint telling the author to use a media-file URL: a page URL there plays nothing at all. The hint is a distinct slot on the shared videoUrlField (VideoUrlFieldLabels.hint) so the other two video URL fields stay uncluttered; a placeholder could not carry it, since a placeholder vanishes as soon as the field has a value — exactly when the constraint starts to matter.

The trigger sits on top of arbitrary media, so it carries its own surface instead of inheriting the page one: a translucent theme-coloured plate with a backdrop blur, which reads over a light or a dark frame in either theme.

In the editor canvas the trigger renders as an inert <span> with the same stamps as production. A live link would navigate the canvas out from under the user on a mis-click and swallow the click Puck uses to select the component; pointer-events-none keeps the canvas select-safe. watchButtonText falls back to the catalog string when blanked — a pill with no label is a broken control.

Text

watchButtonStyle and disclaimerStyle are complete TextStyles: { fontSize, color, fontWeight, lineHeight, textAlign, borderRadius, width, height }, each edited as a single inspector control. width / height size that run's own box; 0 leaves it sized to its content.

borderRadius reads 0 as "not authored" here too, which matters more than it looks: the trigger's pill shape lives in its own rounded-full class, and an inline border-radius: 0px would win over it. An authored non-zero radius overrides the class as SPEC §9.5 asks.

color: "" means unauthored, and for the disclaimer that is the useful default — the view's class pair supplies the original's muted grey under the card and its translucent white once the run sits on the picture, so one unauthored value paints correctly in both positions.

At lg the disclaimer is click-through (lg:pointer-events-none). It overlays the card's bottom strip, sharing it with the trigger, and it is a shrink-to-fit block whose width is a function of the authored font size — so raising disclaimerStyle.fontSize, which is exactly what §9.5 asks an author to do, grows it across the card and would otherwise swallow the click that opens the full video. Below lg it is in-flow text on the page surface with nothing beneath it, so it stays selectable there.

The disclaimer renders unconditionally (pdp-product-hero's does too), so an emptied string paints nothing but keeps the style target alive and selectable. The two strings resolve at render time through the locale resolver: "" is a legal authored value for both and falls back to the catalog, because the original copy is Chinese and belongs in the zh catalog rather than in the payload.

Responsive

The original's per-breakpoint numbers are the class defaults, which is where an unauthored dimension lands:

| Band | Width | Radius | Trigger | | --- | --- | --- | --- | | PC (≥1024px) | 81.25vw | 1.66667vw | Absolute, bottom-right corner | | Tablet (768–1023px) | 90.857vw | 3.42857142vw | Centred bar under the picture | | Mobile (<768px) | 91.11111vw | 4vw | Centred bar under the picture |

The breakpoint radius is a class on the media layer, matching the original, where it belongs to .design-feature-image and not to its container. With the default padding: 0 the media fills the frame, so the rounded rectangle you see is the media layer's. An authored borderRadius goes on both, so raising the padding alone gives a rounded picture in a square frame — the two dimensions are independent, as the inspector presents them.

The disclaimer follows the trigger: in the card's flow and centred below lg, absolutely positioned bottom-left at lg and up.

The trigger is simplified against the original — a centred auto-width pill rather than a full-bleed bar at the narrow bands. Both are "居中横条" per SPEC §9.6; the pill keeps the same markup at every breakpoint and needs no second colour scheme on small screens.

Art direction

Both media kinds carry three sources, one per band, rendered with the same media queries on the <video>'s <source media> and the <picture>'s <source media>:

| Band | Query | | --- | --- | | PC | (min-width: 1024px) | | Tablet | (min-width: 768px) and (max-width: 1023px) | | Mobile | (max-width: 767px) |

The video's <source> list is ordered widest first, unlike the <picture>'s narrowest-first convention: media support on <video> sources is uneven, and a browser that ignores the attribute should land on the desktop file rather than stretch a phone crop across a monitor. An empty band emits no <source> at all.

Props

| Prop | Type | Notes | | --- | --- | --- | | mediaType | "video" \| "image" \| "none" | Which payload renders | | videoSource | VideoSource | { pcVideoUrl, pcPosterUrl, padVideoUrl, padPosterUrl, mobileVideoUrl, mobilePosterUrl } | | mediaImage | MediaImage | { pcSrc, padSrc, mobileSrc, alt } | | watchVideoUrl | string | The full-length video the trigger links to; must be a media file, and separate from videoSource on purpose | | cardStyle | CardStyle | The six card dimensions above | | watchButtonText | string? | Optional with an English literal default so the view renders standalone; the factory injects the localized value | | watchButtonStyle | TextStyle | Trigger run | | disclaimerText | string? | Same optional split; blank blanks it, absent falls back | | disclaimerStyle | TextStyle | Notice run | | classNames | Record<string, string> | Tailwind passthrough, one key per style target | | animation | AnimationProps | Entrance animation on the root |

The link is opened with target="_blank" and rel="noopener noreferrer"; that pair is part of the component's contract rather than an authored choice, because getting it wrong is a security bug rather than a styling preference.

Style targets

root · card · media · watchButton · disclaimer

All five are declared in metadata.anvilkit.editor.styleTargets (with responsive: true and a vocabulary-only property allowlist each) and stamped in every render mode, including edit mode.

media stamps the layer around the payload, not the <video> or <img> inside it, so the target survives a node with no authored source — the layer renders a muted placeholder instead — and a none card. watchButton stamps the anchor in production and the inert <span> in edit mode.

Default content and the original's assets

defaultProps ships two sample assets, so a freshly dropped node renders something real instead of an empty frame (SPEC §9.8). Both follow fleet precedent rather than pointing at Huawei's DAM paths, which this repo cannot serve: video-banner ships the same MDN CC0 clip and carousel / blog-list ship Unsplash stills. watchVideoUrl points at the same file as the card's clip — the repo carries no full-length feature video, and the link's contract is identical whatever it points at.

The originals, for anyone wiring the real files:

| Field | Original path | | --- | --- | | pcVideoUrl / pcPosterUrl | /content/dam/.../design/design-feature.mp4 · .jpg | | padVideoUrl / padPosterUrl | /content/dam/.../design/design-feature-pad.mp4 · .jpg | | mobileVideoUrl / mobilePosterUrl | /content/dam/.../design/design-feature-xs.mp4 · .jpg | | watchVideoUrl | /content/dam/.../design/design-feature-popup.mp4 |

Default copy is English, per the workspace i18n scope. The original's Chinese, which is what the zh catalog ships:

| String | Original | | --- | --- | | Trigger | 观看完整视频 | | Disclaimer | *产品非专业防水手机,画面为视觉创意效果。产品外观仅为示意,请以实物为准。 |

The disclaimer's leading * is the original's footnote marker, not a typo in this catalog.

i18n

Editor chrome is translated (en / zh / ja / ko, 65 keys per locale): field, row and style-target labels, the three option lists, the empty-state hint, the full-video link's authoring hint, and the two fallback runs. Default page content stays English. The factory injects the localized strings into its own defaultProps; the static componentConfig export stays English, and createComponentConfig() deep-equals it.

Styling

styles.css imports @anvilkit/tailwind-config/component and scans this package's sources. Beyond the shared sheet it carries only the ak-anim-* entrance keyframes, which are gated behind prefers-reduced-motion: no-preference — reduced-motion users get no motion at all.

Authored styles reach the DOM as inline style, which is the one place the workspace allows it: they are runtime-computed values (a colour from a picker, a number with a unit select) that no Tailwind class can express.

License

MIT