@pagix/main-product-hero
v0.1.0
Published
Anvilkit Puck-native PLP main product hero — single product showcase with title, price, learn-more and buy buttons, plus a large product image below. Supports white/black background variants.
Downloads
33
Readme
@pagix/main-product-hero
Puck-native PLP main product hero — one product, centred title / price / two CTA pills, with a large media frame below. Single column by design; there is no layout variant.
Usage
import { createMainProductHeroConfig } from "@pagix/main-product-hero";
const config = createMainProductHeroConfig({ locale: "zh" });| Export | Purpose |
| --- | --- |
| componentConfig | The Puck ComponentConfig (English default) |
| mainProductHeroConfig | Convenience alias of componentConfig |
| createMainProductHeroConfig(options?) | Locale-aware factory ({ locale?, messages? }) |
| createComponentConfig(options?) | Alias of the factory |
| defaultProps / fields / metadata | Package contract |
| MainProductHero | The RSC-safe component |
| colorField / imageUrlField / videoUrlField | Package-private Puck field factories |
Props
| Prop | Type | Notes |
| --- | --- | --- |
| title | string | Centred heading. 5.33vw on desktop, 24–32px on mobile |
| price | number? | Omitted from the DOM when undefined |
| currency | string? | Default ¥ |
| priceSuffix | string? | Default and up — the original site's 「起」 |
| learnMoreText / learnMoreLink | string? | Primary (filled) pill |
| buyText / buyLink | string? | Secondary (outline) pill, carries the arrow icon |
| mediaType | "image" \| "video" \| "color" | Which payload fills the frame — a three-way exclusive choice |
| image | { src, alt } | One image file |
| video | { videoUrl, autoplay, muted, loop } | One video file; flags default to true |
| mediaColor | string | Flat fill for mediaType: "color"; "" shows the placeholder |
| mediaFit | "cover" \| "contain" | Fills the frame. Ignored by the colour block |
| mediaWidth | number? | Picture-area width in px. 0 keeps the built-in responsive width |
| mediaHeight | number? | Picture-area height in px. 0 keeps the built-in responsive height |
| cardWidth | number? | Card (whole component) width in px. 0 keeps the built-in w-full |
| cardHeight | number? | Card height in px. 0 leaves it content-sized |
| cardRadius | number? | Card corner radius in px. 0 leaves it square |
| cardAlign | "left" \| "center" \| "right" | Card placement; default center |
| bgVariant | "white" \| "black" | Swaps the whole palette, buttons included |
| background / foreground | string | Required; "" keeps the variant's colour |
Media type
mediaType picks exactly one of three payloads for the frame:
| Value | Renders |
| --- | --- |
| image | <img> — one source |
| video | <video> — one source, playsInline, preload="metadata" |
| color | A flat colour block, aria-hidden, no content inside |
Each option carries exactly one upload slot — one image, one video, no
poster and no per-band source set. A regression test
(gives each of the three media options exactly one upload slot) pins that.
All three groups stay declared in fields; resolveFields only changes
which one the inspector shows, so switching type and back keeps whatever
was authored under the others. Documents saved before mediaType existed
fall back to the image group.
An empty payload in any of the three renders a placeholder block rather
than nothing, so a newly dropped node stays selectable and keeps its
media style target.
learnMoreLink wraps the image and the video; the colour block is
decoration, not a product link target, and stays bare.
Sizing: two independent levels
There are two boxes, and they are sized separately on purpose:
| Level | Props | What it is |
| --- | --- | --- |
| Card | cardWidth · cardHeight · cardRadius · cardAlign | The whole component — the outer <section> (the root style target). Sizes the block: title, CTAs and picture area together |
| Picture area | mediaWidth · mediaHeight | The box the uploaded image or video is shown in — the media style target inside the card |
A 900px card with a 480×270 picture renders as exactly that; neither takes
the other's values. A narrower picture area stays centred, because the frame
sits in the column's flex-col items-center — there is deliberately no
alignment prop at this level.
The uploaded <img> / <video> stay h-full w-full inside the frame, so
resizing the picture area resizes the picture rather than leaving a gap
inside a fixed frame. mediaFit still decides whether the file is cropped
(cover) or letterboxed (contain) in whatever box it ends up with.
Card geometry
The card is the whole component — the outer <section>, i.e. the root
style target. cardWidth / cardHeight / cardRadius therefore size the
entire block, title, CTA pills and media frame included; the media frame
inside is sized separately by mediaWidth / mediaHeight.
They are emitted as inline styles on the root, so an authored value wins over
both the built-in classes and any root rule the appearance compiler emits
(those are class-level).
0 is the "keep the built-in value" sentinel for the three numeric props,
matching plp-product-gallery's slideWidth / slideHeight /
slideRadius. Nothing authored means nothing is emitted, so an untouched
node renders exactly as before.
Two consequences of the card being a real box:
- The root is already
overflow-hidden, socardRadiusclips the whole subtree. AcardHeightshorter than the content therefore clips it rather than growing — the height is a box, not a minimum. cardWidthcarriesmax-width: 100%.w-fullis already container-bounded, an authored px width is not, and the root isoverflow-hidden— without the cap a wide card would be silently clipped on a phone instead of shrinking.
cardAlign is a free-side margin: auto (mr-auto / mx-auto /
ml-auto). It only has an effect once cardWidth narrows the card below its
container; with no authored width the card still fills the container and the
class is inert rather than wrong.
These four are the only card geometry exposed as props. The
rootstyle target still carries the rest of the §6.1 vocabulary (background, border, margin, padding, …); alignment is not expressible there.
Style targets
root · content · media
Image
A single <img>. The frame is fluid (100% wide on mobile, 1000×500 on
desktop) and mediaFit decides whether the file is cropped (cover) or
letterboxed (contain) inside it, so one source covers every band.
SPEC deviation: the original spec called for three responsive sources (
PC/Pad/Mobile). The author asked for one upload per media option, so the<picture>set was dropped in favour of a single fluid<img>.
Video upload caveat
videoUrl 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, since video bytes are far too large to inline as a data URL
the way image does. Paste a hosted URL for anything that must persist.
Same trade-off as @pagix/video-banner.
Styling
styles.css imports @anvilkit/tailwind-config/component and scans the
@anvilkit/ui Button source (the CTA pills render through it). The
@source path is five levels up — packages/runtime/ui/src/button.tsx.
License
MIT
