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-color-selector

v0.1.0

Published

Anvilkit Puck-native PDP color selector — large rounded product image card with circular color swatches (clickable to switch the card image and its name/description strip) and a fully Puck-configurable look: card colour, size, padding and radius, text col

Readme

@pagix/pdp-color-selector

Puck-native PDP colour selector — a large rounded card holding the product image, a row of circular colour swatches beneath it, and a name / divider / description strip under those. Replicates Huawei's section-design-color / .design-color-* module on a product detail page, with two axes the original does not have: every style is authorable (card colour, size, padding and radius; each text run's colour, size and box; swatch size, gap and ring) and the colourways are a list — add, remove and reorder them at will.

Usage

import { createPdpColorSelectorConfig } from "@pagix/pdp-color-selector";

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

| Export | Purpose | | --- | --- | | componentConfig | The Puck ComponentConfig (English default) | | pdpColorSelectorConfig | Convenience alias of componentConfig | | createPdpColorSelectorConfig(options?) | Locale-aware factory ({ locale?, messages? }) | | createComponentConfig(options?) | Alias of the factory | | defaultProps / fields / metadata | Package contract | | PdpColorSelectorView | The RSC-safe component | | PdpColorSelectorInitScript | The colour-switch controller, as source text | | colorField / gradientField / imageUrlField / dimensionField / textStyleField | Puck field factories. All five stay exported; gradientField (raw CSS linear-gradient text) is no longer used by this package's own fields — see "Swatch colours" |

Layout

Four regions, in this DOM order:

| Region | Note | Style target | | --- | --- | --- | | Card frame | One frame per section — cardStyle is section-level, not per colourway | card | | Image box | One per colourway, sharing a single grid cell | image | | Swatch row | role="group", one control per colourway | swatchesswatch | | Caption row | Name · divider · description, one caption per colourway | textStripname · line · detail |

The image boxes share one grid cell (grid-cols-1 grid-rows-1 + col-start-1 row-start-1) rather than being absolutely positioned, so the authored cardStyle.padding actually insets them — an inset-0 child fills the padding box and would silently ignore it.

Clicking a swatch switches the image and the text

The section is a Server Component: its config flows into the Studio demo config, which reaches Puck's RSC <Render>, and that injects non-serializable values (puck.renderDropZone, dragRef, …) into every component's props. Crossing a "use client" boundary there is a hard RSC violation, so the view holds no state and mounts no hooks.

Switching is therefore progressive enhancement, not React state:

  • The server renders every colourway's image and caption, with hidden on all but the active one, and data-pcs-active on the matching swatch. The no-JS paint is already correct.
  • After hydration a small vanilla-JS controller (PdpColorSelectorScript) appends a <script> to its own section and re-points those two attributes on click. It never invents state — activeIndex stays the declared Puck field, and the server-rendered DOM is the truth it mutates.
  • activeIndex is the initial colour: swap it in the inspector and the server-rendered default changes with it.

The controller finds its own section by walking up from its <script> to the nearest [data-pcs-color-selector], so one constant script body drives every instance on the page and no per-render instance id is needed. A random id would differ between the server and client renders and cause a hydration mismatch.

In editMode the swatches render as inert <span>s and no controller is mounted — a click in the canvas selects the node instead of changing colour, and activeIndex is how another colourway is previewed. The target stamps are identical in both modes; only the affordance differs.

Image art direction

Each colourway carries three sources, rendered as a <picture>:

| Field | Served when | | --- | --- | | pcSrc | Default / widest band | | padSrc | (max-aspect-ratio: 6/5) | | mobileSrc | (max-aspect-ratio: 7/10) |

The <source> list is ordered narrowest first and the media-less desktop entry comes last, because a <picture> picks the first matching source — put the desktop entry earlier and it would shadow both narrower bands. An empty band emits no <source> at all, and the <img> falls back to the widest authored band.

The product image can carry the original's edge fade (radial-gradient(farthest-side, #FFFFFF 80%, #FFFFFF00 100%)) with its -webkit-mask-image twin, since older Safari honours only the prefixed form. It ships off (cardStyle.imageFade) because the mask is an ellipse: how much of the picture it dissolves depends on the frame's aspect ratio rather than on the authored stop, so at the default 67.5vw × 35vw it cuts the sides and the image reads as an oval crop rather than a vignette.

pcSrc accepts a pasted URL or a file upload; an upload becomes a session-scoped URL.createObjectURL — it previews immediately but is gone after a reload. Paste a hosted URL for anything that must persist.

Props

| Prop | Type | Notes | | --- | --- | --- | | colors | ColorItem[] | { name, description, image, swatchColorFrom, swatchColorTo }; see below | | cardStyle | { bgColor, width, height, padding, borderRadius, align, imageFade } | Card frame; value <= 0 means "not authored" | | nameTextStyle | NameTextStyle | Name run; a TextStyle without color — see below | | lineTextStyle | TextStyle | Divider; color/width/height are the ones that draw it | | detailTextStyle | TextStyle | Description run | | swatchStyle | { size, gap, borderWidth, borderColor } | Disc geometry and ring | | activeIndex | number | Colour shown on first paint; clamped to the list | | classNames | Record<string, string> | Tailwind passthrough, one key per style target | | animation | AnimationProps | Entrance animation, applied to the root |

A Dimension is { value, unit } with unit one of px / vw / % / rem (em is deliberately absent — it resolves against the parent's font size, and the parent is itself authorable here, so an em text size would resolve against a moving target). value <= 0 on a box dimension means "size to content" and emits no property at all.

A TextStyle is { fontSize, color, fontWeight, lineHeight, textAlign, borderRadius, width, height }, edited as a single inspector control. width / height size that run's own box, which is what makes "set the divider's width" a meaningful control; 0 leaves the run sized to its content.

nameTextStyle is a NameTextStyle — the same shape with no color member. The name is the text that identifies the swatch, so its colour is the colourway's colour (see "Swatch colours"); an authored one would have nothing left to mean, so the member, the inspector row and the view all go without it rather than leaving a control the render happens to ignore. A stored document that still carries nameTextStyle.color renders the swatch colour anyway, because the view sets color after spreading the style. lineTextStyle / detailTextStyle keep color with the ordinary "empty means unset" reading.

Card defaults

| Knob | Default | Note | | --- | --- | --- | | bgColor | transparent | Keeps the page background | | width | 67.5vw | The original's PC width | | height | 35vw | 0 falls back to a 16 / 9 frame | | padding | 0 | Insets the image — see the grid-cell note above | | borderRadius | 1.66667vw | The original's PC radius | | align | center | Where the card sits across the section: left / center / rightmr-auto / mx-auto / ml-auto. Only horizontal — the card's vertical position is its flow position | | imageFade | off | off / on. on masks the hero image's outer edge; off keeps the whole picture — see "Image art direction" |

A stored document from before align (or imageFade) existed renders centred (unfaded): the adapter merges defaultProps.cardStyle under whatever the node stored, so a missing key falls back rather than crashing (Puck does not re-apply defaultProps at render time — it passes the node's stored props through verbatim).

Swatch defaults are 2.8125vw discs, 1.25vw apart, with a 0.10416666667vw white ring — all three from the original.

Swatch colours

A colourway authors two colours, swatchColorFrom and swatchColorTo, each a picker + hex input. Equal values paint a flat disc; different values paint a top-to-bottom linear-gradient.

Two authored stops rather than one CSS string, because a raw linear-gradient text field is a textarea over CSS syntax — it cannot offer a picker and it cannot validate. The trade-off is that the original's 3–5 stop gradients collapse to their two endpoints; the middle stops are listed under "Default content" for anyone restoring them.

swatchColorFrom (falling back to swatchColorTo) also doubles as the colour of the colourway: it is what the caption's name run uses as its colour, via colorwayColor. Both empty paints nothing — the fleet-wide "not authored" convention — and leaves the name inheriting the page colour rather than painting black.

Style targets

root · card · image · swatches · swatch · textStrip · name · line · detail

All nine are declared in metadata.anvilkit.editor.styleTargets and stamped in every render mode, including edit mode. image, swatch, name, line and detail repeat per colourway — a target is a class of element, so one authored rule reaches all of them.

image stamps the box around the <img>, not the <img> itself, so the target survives a colourway with no authored source (the box renders a muted placeholder) and a section with no colourways at all. swatch stamps the live <button> in production and the inert <span> in edit mode.

Default content and the original's assets

defaultProps ships five colourways with English names. The Huawei original's copy, for anyone restoring it:

| Default name | Original name | Original description | | --- | --- | --- | | Orange Ocean | 橘子海 | 橘光跃动海面,假期正式加载。 | | Glimmer Purple | 霞光紫 | 霓虹与晚霞相拥,谁在悄悄心动。 | | Emerald Lake | 翡翠湖 | 湖面清透如镜,此刻只想放空。 | | Morning Gold | 晨曦金 | 阳光照进窗台,今日元气满满。 | | Obsidian Black | 曜石黑 | 繁星划开夜色,世界安静下来。 |

The swatch colours are the original gradients' two endpoints, not the gradients themselves. The originals carry three to five stops; the editor authors two, so the defaults are #…From / #…To taken from the first and last stop. For Orange Ocean that means the midpoint is a straight #5DC6E5 → #F96517 interpolation instead of the original's pale #FCE1C7 — a visible, deliberate trade-off (see "Swatch colours" above).

The full originals, verbatim, for anyone who needs them:

| Default name | Original linear-gradient | | --- | --- | | Orange Ocean | linear-gradient(to bottom, #5DC6E5 0%, #B6E7F2 30%, #FCE1C7 50%, #FF9D4E 70%, #F96517 100%) | | Glimmer Purple | linear-gradient(to bottom, #7E78C0 0%, #9B86B3 30%, #C6999B 50%, #F4C6A1 70%, #FDE9D1 100%) | | Emerald Lake | linear-gradient(to bottom, #49582B 0%, #5C6549 30%, #858B7A 50%, #B8BAB2 70%, #E9E9EA 100%) | | Morning Gold | linear-gradient(to bottom, #F4F4F4 0%, #FFFFFF 45%, #F2E5CE 100%) | | Obsidian Black | linear-gradient(to bottom, #A9A7A3 0%, #403E3A 50%, #191713 100%) |

gradientField (raw CSS text) is still exported and is what you'd use to restore them exactly; no field of this package uses it any more.

The image sources ship empty on purpose — this repo carries no content/dam assets, and an empty source is the fleet-wide "not authored" convention. The originals were /content/dam/.../design-color-{1..5}.webp with alt text HUAWEI Pura 90 Pro Max <name>.

i18n

Editor chrome is translated (en / zh / ja / ko, 72 keys per locale): field and row labels, style-target labels, the swatch group's aria label, the per-swatch "Show {name}" template, and the empty-state hint. Default page content stays English, per the workspace i18n scope. The factory injects the localized strings; 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. The active swatch's scale is driven by Tailwind data variants (data-[pcs-active=true]:scale-110) rather than a conditional class list, so the controller's attribute write and a fresh server render produce byte-identical styling.

License

MIT