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/feature-highlights-gallery

v0.1.0

Published

Anvilkit Puck-native feature highlights gallery — a horizontally scrolling slide carousel with a title, square image cards, prev/next arrows, and a per-card full-screen popup; every card's and every button's colours, sizes, padding, radius and shape are P

Readme

@pagix/feature-highlights-gallery

Puck-native feature highlights gallery — a replica of the Huawei consumer-site "为什么选择华为手机?" module. A centered headline over a horizontally scrolling CSS scroll-snap track of square image cards, each with a kicker, a headline and a round ⊕ button that opens a full-width popup of sub-cards. Desktop gets a pair of prev/next arrows under the track.

Its reason for existing versus its three siblings (plp-product-gallery, focused-product, feature-image-text): every visual property of every card and every button is Puck-configurable — globally, and overridden per card.

RSC-safe by design: no "use client", no React hooks in the root component. The prev/next arrows are wired by a per-instance inline <script> that calls scrollBy and toggles disabled at the scroll ends (the plp-product-gallery / variant-banner pattern). The ⊕ popup is the one interactive piece that needs state, so it lives in a sibling "use client" file and reuses @anvilkit/ui's dialog.

Install

pnpm add @pagix/feature-highlights-gallery @anvilkit/ui @puckeditor/core

The package entry imports its own CSS side-effect (styles.css).

Usage

import {
	FeatureHighlightsGallery,
	type FeatureHighlightsGalleryProps,
} from "@pagix/feature-highlights-gallery";

const props: FeatureHighlightsGalleryProps = {
	title: "为什么选择华为手机?",
	cards: [
		{
			image: "/img/camera.png",
			subtitle: "影像",
			headline: "全焦段,出片真实有质感",
			popup: {
				subtitle: "影像",
				headline: "全焦段,出片真实有质感",
				subCards: [
					{
						image: "/img/macro.png",
						title: "长焦微距",
						description: "远距离也能拍出细节。",
						layout: "half",
					},
				],
			},
		},
	],
	// Global defaults; every one is overridable per card.
	cardBackground: "",
	cardWidth: 0,
	cardHeight: 0,
	cardPadding: 40,
	cardRadius: 8,
	buttonBackground: "",
	buttonIconColor: "",
	buttonIconSize: 16,
	buttonWidth: 36,
	buttonHeight: 36,
	buttonShape: "circle",
};

<FeatureHighlightsGallery {...props} />;

In Puck, register the config factory:

import { createFeatureHighlightsGalleryConfig } from "@pagix/feature-highlights-gallery";

const config = {
	components: {
		FeatureHighlightsGallery: createFeatureHighlightsGalleryConfig({ locale: "zh" }),
	},
};

The factory accepts { locale?: "en" | "zh" | "ja" | "ko", messages? } and translates the editor chrome (field labels, style-target labels, item summaries, a11y labels). Per-key fallback order: messages → locale pack → en.

Props

| Prop | Type | Default | Description | | --- | --- | --- | --- | | title | string | "Why choose a Huawei phone?" | Centered section headline; empty → the catalog's defaultTitle | | cards | HighlightCard[] | 4 sample cards | The slides | | cardBackground | string | "" | Global card background; empty → bg-card | | cardWidth | number | 0 | Global card width (px); 0 → the five-step responsive width | | cardHeight | number | 0 | Global card height (px); 0 → square (1:1) | | cardPadding | number | 40 | Global content-area padding (px) | | cardRadius | number | 8 | Global card corner radius (px) | | buttonBackground | string | "" | Global ⊕ background; empty → a muted foreground tint | | buttonIconColor | string | "" | Global ⊕ icon color; empty → text-foreground | | buttonIconSize | number | 16 | Global ⊕ glyph size (px) | | buttonWidth | number | 36 | Global ⊕ width (px) | | buttonHeight | number | 36 | Global ⊕ height (px) | | buttonShape | "circle" \| "rounded" \| "square" | "circle" | Global ⊕ shape | | classNames | Record<string, string> | — | Tailwind passthrough per style target (see below) | | animation | AnimationProps | — | Entrance animation (none / fade-in / slide-up / slide-down / zoom-in) |

HighlightCard

| Field | Type | Description | | --- | --- | --- | | image | string | Image URL or data URL (the editor field takes a link or an upload); empty → a muted placeholder | | imageAlt | string | Alt text; defaults to headline | | subtitle | string | Kicker above the headline, e.g. 影像 | | headline | string | Card headline | | style | HighlightCardStyle? | Per-card overrides of the five global card properties | | buttonStyle | HighlightCardButtonStyle? | Per-card overrides of the six global ⊕ properties | | popup | HighlightPopup | Content of the popup this card's ⊕ opens |

Override semantics — "" and 0 mean "not authored"

HighlightCardStyle and HighlightCardButtonStyle declare every key as required rather than optional. "" (colors, shape) and 0 (sizes) are the "inherit" markers, so a card falls through to the matching global prop, which in turn falls through to the design system.

Required-not-optional is also a type constraint: an optional key types as Field<T | undefined>, which the shared colorField factory deliberately does not satisfy — the same reason plp-product-gallery declares its per-card colors as required strings.

buttonStyle.shape additionally offers an explicit Inherit global option (value ""). Without it, defaultItemProps would pin "circle" on every new card and the global buttonShape field would have no visible effect.

HighlightPopup / HighlightPopupSubCard

| Field | Type | Description | | --- | --- | --- | | subtitle | string | Popup kicker | | headline | string | Popup headline (rendered as the dialog title) | | subCards | HighlightPopupSubCard[] | Sub-cards; empty → the ⊕ renders but opens nothing | | subCards[].layout | "full" \| "half" | full spans the popup width, half sits two-up on desktop | | subCards[].background | string | Panel background; empty → bg-muted/40 | | subCards[].textColor | string | Panel text color; empty → text-muted-foreground |

Style targets

Eight targets, each stamped with data-ak-style-node / data-ak-style-target via anvilTargetAttrs in every render branch and every mode:

| Target | Element | | --- | --- | | root | The <section> (stamped by anvilRootAttrs) | | title | The <h2> | | track | The scroll container | | card | Each <article> | | cardImage | The image wrapper inside a card | | cardContent | The kicker + headline block inside a card | | cardBtn | The ⊕ trigger (a <button> live, an inert <span> in editMode) | | nav | The arrow group |

card, cardImage, cardContent and cardBtn repeat per card. A target is a class of element, so one generated rule styles every occurrence — the same treatment plp-product-gallery gives item.

The ⊕ popup panel is deliberately not a target: @anvilkit/ui's DialogContent renders its panel behind an open dialog, so it is absent from the server-rendered markup that the authoring-parity suite inspects, and the editMode branch renders an inert <span> rather than a dialog — no declaration could hold in both branches. tooltip and select make the same call for the same reason. Popup paint is authored through the subCards[].background and subCards[].textColor props instead (see below).

Huawei original — exact parameters

Measured from the source module; this is the fidelity target the responsive classes implement.

| Property | Original value | | --- | --- | | Headline | Centered, bold, three tiers: 48 / 32 / 24px | | Card background | White | | Card width | 428 / 374 / 340 / 272 / 240px (five tiers, see below) | | Card aspect | Square, 1:1 (width = height) | | Card radius | 8px | | Card padding | 40px 52px 60px 40px (top right bottom left) | | Card kicker | 16px, gray #666 | | Card headline | 28px, bold, black | | ⊕ button | 36 × 36px, rounded-full, bottom-right of the card | | Arrows | Right-aligned, desktop only, gap: 12px | | Popup backdrop | Black translucent + backdrop-filter: blur(30px) | | Popup container | Centered, white, max-width: 1560px, padding: 32px 80px 80px | | Popup close | 36 × 36px circle, top-right |

Responsive breakpoints

| Breakpoint | Card | Card headline | Kicker | Container padding | | --- | --- | --- | --- | --- | | ≥ 1441 (pc-big) | 428px | 28px | 16px | 0 80px | | 1440–1080 (pc) | 374px | 24px | 16px | 0 80px | | 1079–768 (tablet) | 340px | 24px | 16px | 0 40px | | 767–480 (mobile) | 272px | 20px | 14px | 0 16px | | < 480 (xs) | 240px | 18px | 12px | 0 16px |

Known deviations from the original

Each is a deliberate trade, not an oversight:

  1. Card padding is a single px value, not a CSS shorthand. The original’s 40px 52px 60px 40px would need a text field or a four-field group. This package uses a plain { type: "number" } per the series convention (plp-product-gallery), which cannot express per-side values. Set the card padding to 40 and use the cardContent style target or classNames.cardContent for asymmetric padding.
  2. Card radius is one value for all four corners. The original only uses a uniform 8px, so a single field covers the real case; per-corner radii are not offered.
  3. The popup backdrop is the design-system one, not blur(30px) over black. DialogContent renders its own DialogOverlay internally and exposes no prop to reach it, and the repo forbids patching imported base components. The default is bg-black/10 + backdrop-blur-xs.
  4. The popup close button is the design-system one (absolute top-2 right-2), not a 36 × 36 circle — same reason as (3).
  5. cardWidth is a single px value, so an authored width replaces the whole five-step responsive ladder. A fixed pixel width cannot also be responsive; leaving it 0 keeps the ladder.
  6. Arrows are hidden below md, matching the original, so the mobile experience is touch/trackpad scroll only.
  7. Fidelity is limited by the utility scale for two tiers: the 32px headline becomes md:text-[32px] and 48px becomes xl:text-[48px] — arbitrary values, so exact — but md:/xl: are used as the nearest tier boundaries rather than the original’s own breakpoints.
  8. The popup panel carries no style target, so it is not restyleable from the style panel. It renders only behind an open dialog, and the §6 contract requires every declared target to be stampable in both the production and edit-mode DOM. Use the subCards[].background / subCards[].textColor props instead.

Accessibility

  • Card images carry alt; the ⊕ trigger has a localized aria-label.
  • The popup is a real dialog from @anvilkit/ui: focus trap, Escape and outside-click dismissal, scroll lock and aria-modal come from Base UI.
  • The track is a native scroll container — touch swipe, trackpad scroll and scrollbar-free keyboard scrolling work without JavaScript. The arrow script only adds stepping.
  • Prev/next buttons expose a real disabled state at the scroll ends.
  • Entrance animations respect prefers-reduced-motion.

editMode

The ⊕ renders as an inert aria-hidden <span> and the arrows' script is not bound, so the editor canvas stays click-safe. The style-target stamps are identical in both modes — edit mode may change affordances, never target structure.

Authoring surface (PLAN-0025 §6 / PLAN-0027)

src/config.ts declares metadata.anvilkit.editor.styleTargets (no version field — the contract is structural). Every declared target id is stamped on a real element in every render branch, and every stamped element appears in the map — the workspace authoring-parity suite enforces both directions, which is why a portal'd panel cannot be declared. Extend the map only together with the matching stamp and the feature-highlights-gallery.targets.<targetId> label in all four catalogs.

Two authored props ride the same targets:

  • classNames?: Record<string, string> — Tailwind passthrough per target id, merged after the base classes so authored classes win. A class only takes effect if the host page's compiled CSS contains it.
  • animation?: { preset, durationMs?, delayMs?, easing? } — entrance preset applied to the root via animationAttrs(); the keyframes live in src/styles.css behind @media (prefers-reduced-motion: no-preference).

Neither is stamped into defaultProps, and neither are the hidden appearance / interactions / bindings carriers from ...authoringFields. src/authoring.ts is a verbatim per-package copy locked by the workspace authoring-contract suite.

i18n

Four locale catalogs (en / zh / ja / ko) with identical key sets, enforced by the workspace test suite. Keys are flat and slug-namespaced: feature-highlights-gallery.label, .defaultTitle, .a11y.*, .fields.<path>.label, .fields.<path>.placeholder, .fields.<path>.itemSummary ({index} placeholder), .targets.<id>.

defaultProps page content stays English by convention; the Chinese copy from the original module lives in the zh catalog and in the table above.