promo-signup-offer
角丸カードを画像列(片側)と登録フォーム(反対側)の 2 列へ分けた、画像付きの登録特典カードです。card / heading / text / field / input / button / image / radio-group / fieldset / link の 10 部品を合成します。Blocks は既存部品の合成例であり、新しい UI 部品は追加しません。
主参照は対応表 ID R0346 です。
メール登録フォームはダミー表示のみであり、<form> を含まず送信処理・データ取得は一切行いません。文言はすべて架空のデータであり、実在の企業・ブランド・PII・実クレデンシャルは含みません。画像はビルド時生成の同梱プレースホルダー SVG です。
Demo
基準形
先行登録で特典をゲット
メールアドレスをご登録いただくと、次回のお買い物でお使いいただける特典をお送りします。
カテゴリ選択
先行登録で特典をゲット
メールアドレスをご登録いただくと、次回のお買い物でお使いいただける特典をお送りします。
左右反転 + 注記
先行登録で特典をゲット
メールアドレスをご登録いただくと、次回のお買い物でお使いいただける特典をお送りします。
このカードはレイアウト例です。リンクは本フレームワークのGitHub リポジトリを指します。
中央寄せ
先行登録で特典をゲット
メールアドレスをご登録いただくと、次回のお買い物でお使いいただける特典をお送りします。
使用部品
Rust コード
Rust
use crate::blocks::dummy_assets;
use fandhe_frontend_core::{div, text, Node};
use fandhe_frontend_pre_styled_ui::button::{self, ButtonProps};
use fandhe_frontend_pre_styled_ui::card::{self, CardProps};
use fandhe_frontend_pre_styled_ui::field::{
self, FieldIds, FieldOrientation, FieldProps, FieldRootProps,
};
use fandhe_frontend_pre_styled_ui::fieldset::{self, FieldsetProps, FieldsetRootProps};
use fandhe_frontend_pre_styled_ui::heading::{self, HeadingLevel, HeadingProps, HeadingSize};
use fandhe_frontend_pre_styled_ui::image::{self, ImageFit, ImageProps};
use fandhe_frontend_pre_styled_ui::input::{self, InputProps};
use fandhe_frontend_pre_styled_ui::link::{self, LinkProps};
use fandhe_frontend_pre_styled_ui::radio_group::{self, RadioGroupProps};
use fandhe_frontend_pre_styled_ui::text::{self as styled_text, TextProps, TextSize, TextVariant};
use fandhe_frontend_pre_styled_ui::{ColorPalette, Size};
/// リンク先の固定外部 URL(注記リンク、モジュール doc「見出しレベル」節に
/// 準じ死リンク `href="#"` は使わない既存方針)。実体は本リポジトリであり
/// 実在の利用規約ページではないため、リンク文言・周辺の注記文は遷移先
/// (本リポジトリ)を正直に説明する語にする(モジュール doc「注記リンクは
/// 同意を求めない・遷移先と一致する文言にする」節参照)。
const REPO: &str = "https://github.com/Fandhe-AI/fandhe-frontend";
/// 興味カテゴリ fieldset の `id`。legend の id は headless
/// [`fieldset::legend`] の導出規則(`"{id}-legend"`)に一致させてリテラルで
/// 直書きする(`format!` は使わない)。
const INTEREST_FIELDSET_ID: &str = "blocks-promo-signup-offer-interest";
const INTEREST_LEGEND_ID: &str = "blocks-promo-signup-offer-interest-legend";
/// 興味カテゴリ radio group のネイティブ `<input>` の共通 `name`。
const INTEREST_NAME: &str = "blocks-promo-signup-offer-interest";
/// 各形の直前に置く短い形ラベル(`styled_text::text` の `Sm`/`Muted`)。
fn variant_label(label: &'static str) -> Node {
styled_text::text(
&TextProps {
size: TextSize::Sm,
variant: TextVariant::Muted,
..TextProps::default()
},
vec![],
vec![text(label)],
)
}
/// 興味カテゴリ選択肢 1 件(`radio_group::item` 3 パーツの組み立て)。
fn interest_item(
checked: bool,
props: &RadioGroupProps,
value: &'static str,
label: &'static str,
) -> Node {
radio_group::item(
checked,
props,
value,
vec![],
vec![
radio_group::item_hidden_input(checked, props, Some(INTEREST_NAME), value, vec![]),
radio_group::item_control(checked, props, vec![]),
radio_group::item_text(checked, props, vec![], vec![text(label)]),
],
)
}
/// 形 B(R0345)が追加する興味カテゴリ fieldset + radio group(ネイティブ
/// 操作不能にする理由はモジュール doc「興味カテゴリ radio group をネイティブ
/// disabled にする理由」節)。
fn interest_fieldset() -> Node {
let fieldset_props = FieldsetProps {
id: INTEREST_FIELDSET_ID,
disabled: false,
invalid: false,
has_helper_text: false,
};
let radio_props = RadioGroupProps {
disabled: true,
..RadioGroupProps::default()
};
fieldset::root(
&FieldsetRootProps::default(),
&fieldset_props,
vec![("data-blocks-promo-signup-offer-interest-fieldset", "")],
vec![
fieldset::legend(&fieldset_props, vec![], vec![text("興味のあるカテゴリ")]),
radio_group::root(
Size::Md,
ColorPalette::Accent,
true,
None,
Some(INTEREST_LEGEND_ID),
vec![("data-blocks-promo-signup-offer-interest-group", "")],
vec![
interest_item(true, &radio_props, "apparel", "アパレル"),
interest_item(false, &radio_props, "home", "生活雑貨"),
interest_item(false, &radio_props, "food", "食品"),
],
),
],
)
}
/// 形 C(R0347)が末尾に添える注記(text + link)。登録への同意を求める
/// 文言は使わず、リンク先([`REPO`])をそのまま説明する(モジュール doc
/// 「注記リンクは同意を求めない・遷移先と一致する文言にする」節)。
fn consent_text() -> Node {
styled_text::text(
&TextProps {
size: TextSize::Sm,
variant: TextVariant::Muted,
..TextProps::default()
},
vec![("data-blocks-promo-signup-offer-consent", "")],
vec![
text("このカードはレイアウト例です。リンクは本フレームワークの"),
link::root(
REPO,
&LinkProps {
external: true,
..LinkProps::default()
},
vec![],
vec![text("GitHub リポジトリ")],
),
text("を指します。"),
],
)
}
/// カード 1 枚分(画像列 + 本文列)を組み立てる。`layout` は
/// `data-blocks-promo-signup-offer-layout` へ渡す属性値
/// (`"default"`/`"reverse"`/`"centered"`)で、[`LAYOUT_CSS`] が CSS フックに
/// 使う。`email_id` は variant ごとに一意なリテラルを呼び出し側から渡す
/// (モジュール doc「id / ARIA の方針」節)。`extra` はボタンの**上**(興味
/// カテゴリ fieldset 用)、`trailing` はボタンの**下**(注記用)に差し込む
/// (モジュール doc「注記はボタンの下に置く」節)。
fn offer_card(
layout: &'static str,
email_id: &'static str,
extra: Vec<Node>,
trailing: Vec<Node>,
) -> Node {
let email = FieldProps {
id: email_id,
ids: FieldIds::default(),
disabled: false,
invalid: false,
required: true,
readonly: false,
has_helper_text: false,
};
let mut body_children = vec![
image::image(
&ImageProps::new(dummy_assets::LOGO_SRC, ""),
vec![("data-blocks-promo-signup-offer-logo", "")],
),
heading::heading(
HeadingLevel::H3,
&HeadingProps {
size: HeadingSize::Xl2,
..HeadingProps::default()
},
vec![],
vec![text("先行登録で特典をゲット")],
),
styled_text::text(
&TextProps {
variant: TextVariant::Muted,
..TextProps::default()
},
vec![],
vec![text(
"メールアドレスをご登録いただくと、次回のお買い物でお使いいただける特典をお送りします。",
)],
),
field::root(
&FieldRootProps {
orientation: FieldOrientation::Vertical,
},
&email,
vec![],
vec![
field::label(&email, vec![], vec![text("メールアドレス")]),
input::input(
&InputProps::default(),
&email,
vec![
("type", "email"),
("placeholder", "you@example.com"),
("autocomplete", "email"),
],
),
],
),
];
body_children.extend(extra);
body_children.push(button::button(
&ButtonProps::default(),
vec![("data-blocks-promo-signup-offer-submit", "")],
vec![text("今すぐ登録する")],
));
body_children.extend(trailing);
let media = div(
vec![("class", "blocks-promo-signup-offer-media")],
vec![image::image(
&ImageProps {
fit: ImageFit::Cover,
..ImageProps::new(dummy_assets::PRODUCT_SRC, "")
},
vec![("data-blocks-promo-signup-offer-image", "")],
)],
);
let body = card::body(
vec![("data-blocks-promo-signup-offer-body", "")],
body_children,
);
card::root(
CardProps::default(),
vec![
("data-blocks-promo-signup-offer-card", ""),
("data-blocks-promo-signup-offer-layout", layout),
],
vec![media, body],
)
}
/// `promo-signup-offer` の Demo 本体。呼び出しごとに同一の `Node` を返す
/// 純関数(他 block と同じ契約)。4 形を縦に並べる(モジュール doc「4 形を
/// 1 つの Demo に並記する」節)。
pub fn demo() -> Node {
div(
vec![("class", "blocks-promo-signup-offer-layout")],
vec![
variant_label("基準形"),
offer_card(
"default",
"blocks-promo-signup-offer-email-a",
vec![],
vec![],
),
variant_label("カテゴリ選択"),
offer_card(
"default",
"blocks-promo-signup-offer-email-b",
vec![interest_fieldset()],
vec![],
),
variant_label("左右反転 + 注記"),
offer_card(
"reverse",
"blocks-promo-signup-offer-email-c",
vec![],
vec![consent_text()],
),
variant_label("中央寄せ"),
offer_card(
"centered",
"blocks-promo-signup-offer-email-d",
vec![],
vec![],
),
],
)
}差分メモ
主参照は R0346(基準形)で、画像列(片側)と登録フォーム(反対側)の 2 列構成をそのまま採用しています。
- R0345(カテゴリ選択): 基準形のフォーム列へ、興味のあるカテゴリを選ぶ fieldset + radio group を追加した形として並記しています。radio は先頭 1 件が選択済みの静的な初期状態で、ネイティブ操作不能にする
disabled: trueで固定しています(他 block と同じ判断)。 - R0347(左右反転 + リポジトリ案内注記):
48rem以上で画像列へorder: 2を与えて左右を入れ替え、登録ボタンの下にリンク先(本リポジトリ)を説明する注記を添えた形として並記しています。同意を求める文言は使いません(consent_text()のコメント参照)。 - R0348(中央寄せ): フォーム列の内容を中央寄せにした形として並記しています。
- 状態表示: 無 JS のため本 block はトグル系の状態(開閉・選択等)を持たず、4 形は差分がある部分のみを静的に並記しています。
- md 未満の画像非表示:
48remはBreakpoint::Md(fandhe_frontend_pre_styled_ui::recipe::Breakpoint::Md)と一致するリテラル値です。
関連情報: Card / Heading / Text / Field / Input / Button / Image / Radio Group / Fieldset / Link