ViewProductブロック
ViewProduct は、MUI 上に構築された商品詳細ビューブロックで、ヘッダー(組織、画像、チップ、タイトル)、タグ、プライマリおよび任意のセカンダリ説明セクション、任意のフッター、アクションボタンを備えています。
インストール
- npm
- yarn
- pnpm
- bun
npm install @nodeblocks/frontend-view-product-block
yarn add @nodeblocks/frontend-view-product-block
pnpm add @nodeblocks/frontend-view-product-block
bun add @nodeblocks/frontend-view-product-block
必要なもの
| 項目 | 用途 |
|---|---|
imageUrl | 商品画像のソース URL |
headerTitle | 商品ビューのタイトル |
chips | 画像下のステータスまたはカテゴリチップ(各チップに label が必須) |
tags | コンテンツエリアのタグチップ(label はテキストまたはパンくず形式の string[]、任意の icon は ReactElement) |
sections | プライマリ説明ブロック(label、value、任意の icon) |
actions(任意) | フッターボタン。onClick をカート、チェックアウト、ナビゲーションに接続 |
ViewProduct はプレゼンテーション用です — 商品フィールドをプロパティとして渡し、親で actions[].onClick を実装してください。任意の secondarySections、footerLogo、footerText を指定すると、デフォルトレイアウトを拡張できます。
コード例
- クイックスタート
- セカンダリセクション
- 複合コンポーネント
- ブロックのオーバーライド
function Example() { const [lastAction, setLastAction] = React.useState(''); const sellerName = 'TechStore'; const chips = [ { label: 'スマートフォン' }, { label: '5G' }, { label: 'SIMフリー' }, { label: '本日発送', variant: 'filled' }, ]; const tags = [ { label: 'プレミアム' }, { label: ['電子機器', '携帯電話', 'スマートフォン', 'Apple'] }, { label: '在庫あり' }, ]; const sections = [ { label: '商品説明', value: 'iPhone 15 Pro はチタニウムデザイン、A17 Pro チップ、48MP メインセンサーを備えた高度な Pro カメラシステムを特徴としています。', }, { label: '配送と保証', value: '5〜7 営業日で標準配送無料。製造上の欠陥に対する1年間の限定保証付き。', }, ]; const actions = [ { label: 'カートに追加', onClick: () => setLastAction('カートに追加') }, { label: '今すぐ購入', onClick: () => setLastAction('今すぐ購入') }, ]; return ( <> {lastAction ? ( <Typography variant="body2" sx={{ mb: 2, textAlign: 'center' }}> 最後のアクション: <strong>{lastAction}</strong> </Typography> ) : null} <ViewProduct organizationName={sellerName} headerLogo="https://docs.nodeblocks.dev/img/icon.png" imageUrl="https://docs.nodeblocks.dev/img/undraw_docusaurus_react.svg" imageAlt="iPhone 15 Pro" chips={chips} headerTitle="iPhone 15 Pro - 最新モデル" tags={tags} sections={sections} footerLogo="https://docs.nodeblocks.dev/img/icon.png" footerText={`${sellerName} · この販売者から127商品`} actions={actions} /> </> ); }
secondarySections を渡すと、プライマリの sections の下に、控えめなカードスタイルの追加説明ブロックがレンダリングされます。
function Example() { const [lastAction, setLastAction] = React.useState(''); const sellerName = 'TechStore'; const chips = [ { label: 'スマートフォン' }, { label: '5G' }, { label: 'SIMフリー' }, ]; const tags = [ { label: 'プレミアム' }, { label: ['電子機器', '携帯電話', 'スマートフォン'] }, { label: '在庫あり' }, ]; const sections = [ { label: '商品説明', value: 'iPhone 15 Pro はチタニウムデザイン、A17 Pro チップ、48MP メインセンサーを備えた高度な Pro カメラシステムを特徴としています。', }, { label: '配送と保証', value: '5〜7 営業日で標準配送無料。製造上の欠陥に対する1年間の限定保証付き。', }, ]; const secondarySections = [ { label: 'お支払い方法', value: '主要クレジットカード、Apple Pay、または承認対象の分割払いプランで安全にお支払いいただけます。すべての取引は暗号化されています。', }, { label: '返品・交換', value: '対象商品は未使用の状態で30日以内に返品可能です。開封済み商品には再入荷手数料が発生する場合があります。返品を開始するにはサポートにお問い合わせください。', }, ]; const actions = [ { label: 'カートに追加', onClick: () => setLastAction('カートに追加') }, { label: '今すぐ購入', onClick: () => setLastAction('今すぐ購入') }, ]; return ( <> {lastAction ? ( <Typography variant="body2" sx={{ mb: 2, textAlign: 'center' }}> 最後のアクション: <strong>{lastAction}</strong> </Typography> ) : null} <ViewProduct organizationName={sellerName} headerLogo="https://docs.nodeblocks.dev/img/icon.png" imageUrl="https://docs.nodeblocks.dev/img/undraw_docusaurus_react.svg" imageAlt="iPhone 15 Pro" chips={chips} headerTitle="iPhone 15 Pro - 最新モデル" tags={tags} sections={sections} secondarySections={secondarySections} footerLogo="https://docs.nodeblocks.dev/img/icon.png" footerText={`${sellerName} · この販売者から127商品`} actions={actions} /> </> ); }
各 secondarySections エントリには、プライマリの sections と同様に、MUI の SvgIconComponent として icon(例: SupportAgentOutlined)を含めることができます。
子ブロックを使ってレイアウトとスタイルをカスタマイズします。ルートだけでなく、サブコンポーネントにセクション固有のプロパティを渡します。
function Example() { const [lastAction, setLastAction] = React.useState(''); const chips = [{ label: '新着' }, { label: '注目', color: 'success' }]; const tags = [{ label: 'プレミアム' }, { label: '送料無料' }]; const sections = [ { label: '概要', value: '軽量チタニウムデザインで、終日バッテリーを実現。' }, ]; const actions = [ { label: 'カートに追加', onClick: () => setLastAction('カートに追加') }, { label: '今すぐ購入', onClick: () => setLastAction('今すぐ購入') }, ]; return ( <> {lastAction ? ( <Typography variant="body2" sx={{ mb: 2, textAlign: 'center' }}> 最後のアクション: <strong>{lastAction}</strong> </Typography> ) : null} <ViewProduct imageUrl="https://docs.nodeblocks.dev/img/undraw_docusaurus_react.svg" chips={chips} headerTitle="iPhone 15 Pro" tags={tags} sections={sections} > <ViewProduct.Header> <ViewProduct.CompanyName organizationName="TechStore" headerLogo="https://docs.nodeblocks.dev/img/icon.png" sx={{ bgcolor: 'grey.50', borderRadius: 2, px: 2 }} /> <ViewProduct.Image imageAlt="商品メイン画像" sx={{ mb: 2 }} /> <ViewProduct.Chips chips={chips} sx={{ px: 2 }} /> <ViewProduct.Title headerTitle="iPhone 15 Pro" sx={{ color: 'primary.main', px: 2 }} /> </ViewProduct.Header> <ViewProduct.Content> <ViewProduct.Tags tags={tags} sx={{ columnGap: 2 }} /> <ViewProduct.Sections sections={sections} /> </ViewProduct.Content> <ViewProduct.Actions actions={actions} sx={{ py: 2, bgcolor: 'grey.50', borderRadius: 2 }} /> </ViewProduct> </> ); }
関数子要素と defaultBlocks、defaultBlockOrder を使って、ブロックの注入、デフォルトの差し替え、順序の制御を行います。
function Example() { const [promoVisible, setPromoVisible] = React.useState(true); const [lastAction, setLastAction] = React.useState(''); const sellerName = 'TechStore'; const chips = [ { label: '20% OFF', variant: 'filled', color: 'error', size: 'small' }, { label: '速達配送', variant: 'filled', color: 'primary' }, { label: '正規代理店', variant: 'outlined' }, ]; const tags = [ { label: 'プレミアム' }, { label: ['電子機器', 'スマートフォン', 'iPhone 15 Pro'] }, { label: '在庫あり · 本日発送' }, ]; const sections = [ { label: 'お客様が選ぶ理由', value: 'チタニウムデザイン、終日バッテリー、Pro カメラシステム — 外出先でも確実な 4K 撮影が必要なクリエイター向けに最適化されています。', }, { label: '配送', value: 'あと2時間14分以内に注文すると当日発送。30日以内の無料返品。', }, ]; const actions = [ { label: 'カートに追加', onClick: () => setLastAction('カートに追加') }, { label: '今すぐ購入', onClick: () => setLastAction('今すぐ購入') }, ]; const trustSignals = [ { title: '無料返品', detail: '30日間' }, { title: '安全なチェックアウト', detail: '256ビット暗号化' }, { title: '24時間サポート', detail: 'ライブチャットと電話' }, ]; return ( <> {lastAction ? ( <Typography variant="body2" sx={{ mb: 2, textAlign: 'center' }}> 最後のアクション: <strong>{lastAction}</strong> </Typography> ) : null} <ViewProduct organizationName={sellerName} headerLogo="https://docs.nodeblocks.dev/img/icon.png" imageUrl="https://docs.nodeblocks.dev/img/undraw_docusaurus_react.svg" imageAlt="iPhone 15 Pro" chips={chips} headerTitle="iPhone 15 Pro — 週末限定" tags={tags} sections={sections} footerLogo="https://docs.nodeblocks.dev/img/icon.png" footerText={`${sellerName} · この販売者から127商品`} actions={actions} > {({ defaultBlocks }) => ({ blocks: { ...defaultBlocks, promoRibbon: promoVisible ? ( <Box sx={{ mx: { xs: 2, sm: 0 }, mb: 2, px: 2, py: 1.5, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 2, borderRadius: 2, color: 'common.white', background: (theme) => `linear-gradient(135deg, ${theme.palette.primary.main} 0%, ${theme.palette.primary.dark} 55%, #0f172a 100%)`, boxShadow: (theme) => `0 12px 32px ${theme.palette.primary.main}33`, }} > <Box sx={{ minWidth: 0 }}> <Typography variant="overline" sx={{ display: 'block', lineHeight: 1.2, opacity: 0.9, letterSpacing: 0.8 }} > 期間限定 </Typography> <Typography variant="subtitle2" sx={{ fontWeight: 700 }}> 速達配送無料 + チェックアウト時20% OFF </Typography> </Box> <Box component="button" type="button" onClick={() => setPromoVisible(false)} aria-label="プロモーションを閉じる" sx={{ flexShrink: 0, border: '1px solid rgba(255,255,255,0.35)', bgcolor: 'rgba(255,255,255,0.12)', color: 'inherit', borderRadius: '50%', width: 32, height: 32, cursor: 'pointer', fontSize: 18, lineHeight: 1, }} > × </Box> </Box> ) : null, content: ( <ViewProduct.Content> <Box sx={{ mx: { xs: 2, sm: 0 }, mb: 3, p: 2, borderRadius: 2, bgcolor: 'grey.50', border: '1px solid', borderColor: 'divider', }} > <ViewProduct.Tags tags={tags} sx={{ columnGap: 1, rowGap: 1, '& .MuiChip-root': { height: 'auto', py: 0.75, bgcolor: 'background.paper', border: '1px solid', borderColor: 'grey.200', borderRadius: 999, transition: 'border-color 0.2s, box-shadow 0.2s', '&:hover': { borderColor: 'primary.light', boxShadow: (theme) => `0 4px 12px ${theme.palette.primary.main}22`, }, }, '& .MuiChip-label': { px: 1.25, fontWeight: 600, color: 'text.primary', }, '& .MuiChip-root:nth-of-type(1) .MuiChip-label': { color: 'primary.main', }, '& .MuiChip-root:nth-of-type(3) .MuiChip-label': { color: 'success.main', }, }} /> </Box> <ViewProduct.Sections sections={sections} /> </ViewProduct.Content> ), trustBar: ( <Box sx={{ mx: { xs: 2, sm: 0 }, mb: { xs: 2, sm: 3 }, display: 'grid', gridTemplateColumns: { xs: '1fr', sm: 'repeat(3, 1fr)' }, gap: 1.5, }} > {trustSignals.map((item) => ( <Box key={item.title} sx={{ p: 1.75, borderRadius: 2, border: '1px solid', borderColor: 'divider', bgcolor: 'background.paper', boxShadow: '0 1px 2px rgba(15, 23, 42, 0.06)', }} > <Typography variant="subtitle2" sx={{ fontWeight: 700 }}> {item.title} </Typography> <Typography variant="caption" color="text.secondary"> {item.detail} </Typography> </Box> ))} </Box> ), actions: ( <ViewProduct.Actions actions={actions} sx={{ py: 0, px: 0, '& .MuiButton-root:first-of-type': { bgcolor: 'grey.100', color: 'text.primary', boxShadow: 'none', '&:hover': { bgcolor: 'grey.200', boxShadow: 'none' }, }, '& .MuiButton-root:last-of-type': { boxShadow: (theme) => `0 8px 20px ${theme.palette.primary.main}40`, }, }} /> ), }, blockOrder: [ ...(promoVisible ? ['promoRibbon'] : []), 'header', 'content', 'trustBar', 'footer', 'actions', ], })} </ViewProduct> </> ); }
ブロックのオーバーライドを使うタイミング
デフォルトの blockOrder は header、content、footer、actions です。この例では4つの一般的なパターンを組み合わせています: 閉じられるプロモブロック(promoRibbon)、ViewProduct.Tags のスタイル変更のための content の差し替え(ピル型チップ、ハイライトパネル、アクセントラベル色)、コンテンツとフッターの間のカスタムブロック(trustBar)、ガラス風の固定 CTA バーへの actions の差し替え。閉じた後は blockOrder から promoRibbon を外し、レイアウトが空きスペースを確保しないようにします。
主要プロパティ
コアプロパティ
| プロパティ | 型 | 必須 | デフォルト | 説明 |
|---|---|---|---|---|
imageUrl | string | はい | — | 商品画像 URL。設定時のみ ViewProduct.Image がレンダリングされる |
headerTitle | string | はい | — | ヘッダー内の商品タイトル |
chips | (ChipProps & { label: string })[] | はい | — | ヘッダーチップ。各項目に label が必須(variant、color、size など MUI Chip プロパティも可) |
tags | { label: string | string[]; icon?: ReactElement }[] | はい | — | コンテンツ内のタグ行。label が string[] の場合は / 区切りのパンくず形式でレンダリング |
sections | { icon?: SvgIconComponent; label: string; value: string }[] | はい | — | 任意の MUI アイコンコンポーネント付きプライマリコンテンツセクション |
コンテンツプロパティ
| プロパティ | 型 | 必須 | デフォルト | 説明 |
|---|---|---|---|---|
organizationName | string | いいえ | undefined | ヘッダー内の販売者または組織名 |
headerLogo | string | いいえ | undefined | 組織名横の円形ロゴの画像 URL |
imageAlt | string | いいえ | undefined | 商品画像の alt テキスト |
secondarySections | { icon?: SvgIconComponent; label: string; value: string }[] | いいえ | undefined | プライマリセクション下の控えめなカードスタイルの追加セクション |
footerLogo | string | いいえ | undefined | フッター画像 URL(または data URI) |
footerText | string | いいえ | undefined | フッターキャプション(例: 販売者の商品数) |
actions | { label: ReactNode; onClick: () => void }[] | いいえ | undefined | 下部アクション行の等幅アクションボタン |
ルートに labels プロパティはありません。headerTitle、チップの label、セクションの label/value、footerText、actions[].label でコピーを上書きしてください。
レイアウトと構成プロパティ
| プロパティ | 型 | 必須 | デフォルト | 説明 |
|---|---|---|---|---|
children | BlocksOverride | ReactNode | いいえ | undefined | 複合 JSX 子要素、または blocks と blockOrder を返す関数オーバーライド |
className | string | いいえ | undefined | ルートスタックのクラス名(nbb-view-product) |
sx | SxProps | いいえ | undefined | ルートスタック用の MUI システムスタイル |
ViewProduct は StackProps(children を除く)を継承します。オーバーライドなしのデフォルトブロック順: header、content、footer、actions(defaultBlockOrder)。
サブコンポーネントのプロパティ
サブコンポーネントはコンテキストから読み取り、同じコンテンツキーをプロパティとして渡すとローカルで上書きできます。
| サブコンポーネント | 主要プロパティ | 継承 | ベース |
|---|---|---|---|
ViewProduct.CompanyName | organizationName、headerLogo、children、className、sx | StackProps | Stack + Box(img)+ Typography |
ViewProduct.Image | imageUrl、imageAlt、children、className | BoxProps | Box(img、アスペクト比 1368/540) |
ViewProduct.Chips | chips、children、className、sx | StackProps | Stack + Chip(variant="outlined"、color="primary") |
ViewProduct.Title | headerTitle、children、className、sx | TypographyProps | Typography(component="h4") |
ViewProduct.Header | organizationName、headerLogo、imageUrl、imageAlt、chips、headerTitle、children、className、sx | StackProps | Stack + Divider |
ViewProduct.Tags | tags、children、className、sx | StackProps | Stack + Chip(透明背景) |
ViewProduct.Sections | sections、children、className | StackProps | Stack + Typography |
ViewProduct.SecondarySections | secondarySections、children、className | StackProps | Stack + カードスタイルのセクションブロック |
ViewProduct.Content | tags、sections、secondarySections、children、className、sx | StackProps | Tags、Sections、SecondarySections を構成する Stack |
ViewProduct.Footer | footerLogo、footerText、children、className、sx | BoxProps | Box + Stack |
ViewProduct.Actions | actions、children、className、sx | StackProps | Stack + Button(variant="contained"、size="medium") |
デフォルト UI ブロック
| ブロック | ベース | 備考 |
|---|---|---|
ViewProduct(ルート) | Stack | 中央揃えの縦レイアウト(nbb-view-product) |
header(ViewProduct.Header) | Stack | 会社名、画像、チップ、タイトル、区切り線 |
content(ViewProduct.Content) | Stack | タグ、セクション、任意のセカンダリセクション |
footer(ViewProduct.Footer) | Box | footerLogo、footerText、またはカスタム children が設定されている場合のみレンダリング |
actions(ViewProduct.Actions) | Stack + Button | actions が指定されている場合の等幅ボタン |
デフォルトのルートレンダリング順序: header → content → footer → actions。
会社ロゴの alt のデフォルトは 会社ロゴ です。フッターロゴの alt のデフォルトは フッターロゴ です。
TypeScript
import * as React from 'react';
import { ViewProduct } from '@nodeblocks/frontend-view-product-block';
import type { ChipProps } from '@mui/material';
import type { SvgIconComponent } from '@mui/icons-material';
import type { ReactElement, ReactNode } from 'react';
type ProductChip = ChipProps & { label: string };
type ProductTag = {
label: string | string[];
icon?: ReactElement;
};
type ProductSection = {
icon?: SvgIconComponent;
label: string;
value: string;
};
type ProductAction = {
label: ReactNode;
onClick: () => void;
};
export function ProductDetailView() {
const chips: ProductChip[] = [{ label: '新着' }, { label: '在庫あり', variant: 'filled' }];
const tags: ProductTag[] = [{ label: 'プレミアム' }];
const sections: ProductSection[] = [
{ label: '説明', value: '商品の詳細はこちら。' },
];
const actions: ProductAction[] = [
{ label: 'カートに追加', onClick: () => undefined },
];
return (
<ViewProduct
organizationName="TechStore"
imageUrl="https://example.com/product.jpg"
imageAlt="商品"
chips={chips}
headerTitle="サンプル商品"
tags={tags}
sections={sections}
actions={actions}
/>
);
}