Skip to content
fandhe-frontendcore v0.4.3
GitHub

card-meta-cta

card / badge / heading / text / list / icon / button / rating-group / number-input / separator の 10 部品を合成した、題名・分類・説明・要点を縦に並べ下端に主操作ボタンを置く「情報+CTA」カードの合成例です。求人・料金プラン・商品の 3 例を同じ骨格で横並びに表示し、狭い幅では 1 列に積みます。

いずれも <form> を持たず、送信処理・状態機械のない静的な表示例です。商品版の数量入力・評価は readonly の固定値表示で、実行時の入力連動は行いません(docs サイトは JS ハイドレーションを一切行わないため)。

Demo

エンジニアリング

フロントエンドエンジニア

描画コアと UI コンポーネント層の設計・実装を担当します。

  • 勤務地:リモート
  • 雇用形態:正社員
  • 募集期間:通年
おすすめ

Growth

チームでの本格運用に必要な機能をまとめたプランです。

$29 / 月

  • エディタ統合
  • 無制限プロジェクト
  • 優先サポート
新着

デスクマット Pro

手首の負担を抑える傾斜構造の作業用デスクマットです。

$48

  • サイズ:90 × 40 cm
  • 素材:撥水コーティング表面
評価 4.0(128 件)

使用部品

Rust コード

Rust
use fandhe_frontend_core::{div, el, text, Node};
use fandhe_frontend_pre_styled_ui::badge::{self, BadgeProps};
use fandhe_frontend_pre_styled_ui::button::{button, ButtonProps};
use fandhe_frontend_pre_styled_ui::card::{self, CardProps};
use fandhe_frontend_pre_styled_ui::heading::{
    heading, HeadingLevel, HeadingProps, HeadingSize, HeadingWeight,
};
use fandhe_frontend_pre_styled_ui::icon::{icon, IconProps};
use fandhe_frontend_pre_styled_ui::list::{self, ListType, ListVariant};
use fandhe_frontend_pre_styled_ui::number_input::{self, NumberInputFlags};
use fandhe_frontend_pre_styled_ui::rating_group::{
    self, RatingGroup, RatingGroupProps, RatingItemFlags,
};
use fandhe_frontend_pre_styled_ui::recipe::ColorPalette;
use fandhe_frontend_pre_styled_ui::separator::{self, SeparatorProps};
use fandhe_frontend_pre_styled_ui::text::{
    self as styled_text, TextProps, TextSize, TextVariant, TextWeight,
};
use fandhe_frontend_pre_styled_ui::Size;

/// 評価ラベル(`rating_group::label` の `id`)。3 枚のカードを同一ページへ
/// 並記するため固定文字列にする(block 内では商品カード 1 枚のみが評価を
/// 持つため重複しない)。
const RATING_LABEL_ID: &str = "blocks-card-meta-cta-rating-label";
/// 数量入力の `id`(`number_input::label`/`input` が共有)。
const QUANTITY_INPUT_ID: &str = "blocks-card-meta-cta-quantity";

/// 装飾用の自作幾何アイコン(lucide 等の既存アイコンセットの path を
/// 複製しないための単純図形、`careers_card_grid::geo_icon` と同型)。
fn geo_icon(path_d: &'static str) -> Node {
    icon(
        &IconProps {
            size: Size::Sm,
            ..IconProps::default()
        },
        vec![],
        vec![el(
            "path",
            vec![
                ("d", path_d),
                ("fill", "none"),
                ("stroke", "currentColor"),
                ("stroke-width", "2"),
                ("stroke-linecap", "round"),
                ("stroke-linejoin", "round"),
            ],
            vec![],
        )],
    )
}

/// 位置ピンの幾何アイコン(求人カードの勤務地行)。
fn location_icon() -> Node {
    geo_icon("M12 21s-7-6.5-7-11a7 7 0 0114 0c0 4.5-7 11-7 11z M12 12a2 2 0 100-4 2 2 0 000 4z")
}

/// 時計の幾何アイコン(求人カードの雇用形態行)。
fn clock_icon() -> Node {
    geo_icon("M12 3a9 9 0 100 18 9 9 0 000-18z M12 7v5l4 2")
}

/// カレンダーの幾何アイコン(求人カードの募集期間行)。
fn calendar_icon() -> Node {
    geo_icon("M4 5h16v15H4z M4 9h16 M8 3v4 M16 3v4")
}

/// チェックの幾何アイコン(料金プランカードの機能一覧、装飾用)。
fn check_icon() -> Node {
    geo_icon("M4 12l5 5L20 6")
}

/// 仕様タグの幾何アイコン(商品カードの仕様一覧、装飾用)。
fn spec_icon() -> Node {
    geo_icon("M4 7h16 M4 12h10 M4 17h13")
}

/// 要点リストの 1 行(アイコン + 本文テキスト。3 例で共用する)。
fn meta_item(icon_node: Node, label: &str) -> Node {
    list::item(
        vec![],
        vec![list::indicator(vec![], vec![icon_node]), text(label)],
    )
}

/// 3 例が共有するカード骨格(分類 badge + 題名 + 説明 + 要点リスト +
/// 全幅 CTA ボタン)。`pre_list_extras` は説明と要点リストの間に挿入する
/// 追加ノード(料金プラン・商品カードの価格行)。`extras` は要点リストと
/// CTA の間に挿入する追加ノード(商品カードの評価・数量入力)。
fn meta_cta_card(
    badge_label: &str,
    title: &str,
    description: &str,
    pre_list_extras: Vec<Node>,
    items: Vec<(Node, &str)>,
    extras: Vec<Node>,
    cta_label: &str,
) -> Node {
    let list_items: Vec<Node> = items
        .into_iter()
        .map(|(icon_node, label)| meta_item(icon_node, label))
        .collect();

    let mut body_children = vec![styled_text::text(
        &TextProps {
            variant: TextVariant::Muted,
            ..TextProps::default()
        },
        vec![],
        vec![text(description)],
    )];
    body_children.extend(pre_list_extras);
    body_children.push(separator::separator(&SeparatorProps::default(), vec![]));
    body_children.push(list::root(
        ListType::default(),
        ListVariant::Plain,
        vec![],
        list_items,
    ));
    body_children.extend(extras);

    card::root(
        CardProps::default(),
        vec![("data-blocks-card-meta-cta-card", "")],
        vec![
            card::header(
                vec![],
                vec![
                    badge::badge(&BadgeProps::default(), vec![], vec![text(badge_label)]),
                    heading(
                        HeadingLevel::H3,
                        &HeadingProps {
                            size: HeadingSize::Lg,
                            weight: HeadingWeight::Semibold,
                        },
                        vec![],
                        vec![text(title)],
                    ),
                ],
            ),
            card::body(vec![("class", "blocks-card-meta-cta-body")], body_children),
            card::footer(
                vec![("class", "blocks-card-meta-cta-footer")],
                vec![button(
                    &ButtonProps::default(),
                    vec![("data-blocks-card-meta-cta-cta", "")],
                    vec![text(cta_label)],
                )],
            ),
        ],
    )
}

/// 求人カード(代表構成、対応表 ID R0024)。
fn job_card() -> Node {
    meta_cta_card(
        "エンジニアリング",
        "フロントエンドエンジニア",
        "描画コアと UI コンポーネント層の設計・実装を担当します。",
        vec![],
        vec![
            (location_icon(), "勤務地:リモート"),
            (clock_icon(), "雇用形態:正社員"),
            (calendar_icon(), "募集期間:通年"),
        ],
        vec![],
        "応募する",
    )
}

/// 料金プランカード(集約元 R0028: 機能一覧)。プラン名・価格は
/// `crate::blocks::dummy_assets::SAMPLE_PRICE_TIERS` の 2 番目
/// ("Growth"/"$29")を使う。
fn plan_card() -> Node {
    let price_row = styled_text::text(
        &TextProps {
            size: TextSize::Xl2,
            weight: TextWeight::Bold,
            ..TextProps::default()
        },
        vec![],
        vec![
            text(crate::blocks::dummy_assets::SAMPLE_PRICE_TIERS[1].1),
            text(" / 月"),
        ],
    );
    // 価格行は要点リストの前(description の直後)に差し込むため、
    // meta_cta_card の pre_list_extras 経由で渡す(要点リストは機能一覧の
    // みに限定するため items には含めない)。
    let items = vec![
        (check_icon(), "エディタ統合"),
        (check_icon(), "無制限プロジェクト"),
        (check_icon(), "優先サポート"),
    ];
    meta_cta_card(
        "おすすめ",
        crate::blocks::dummy_assets::SAMPLE_PRICE_TIERS[1].0,
        "チームでの本格運用に必要な機能をまとめたプランです。",
        vec![price_row],
        items,
        vec![],
        "このプランを選ぶ",
    )
}

/// 商品カード(集約元 R0025: 数量入力+評価。readonly の静的表示、
/// モジュール doc「readonly 静的表示」節参照)。
fn product_card() -> Node {
    let price_row = styled_text::text(
        &TextProps {
            size: TextSize::Xl2,
            weight: TextWeight::Bold,
            ..TextProps::default()
        },
        vec![],
        vec![text("$48")],
    );

    let rating_props = RatingGroupProps {
        disabled: false,
        readonly: true,
        required: false,
    };
    let rating_group_state = RatingGroup::new(5, Some(4), true);
    let rating_label = rating_group::label(
        &rating_props,
        Some(RATING_LABEL_ID),
        vec![],
        vec![text("評価 4.0(128 件)")],
    );
    let rating_items: Vec<Node> = (1..=rating_group_state.count())
        .map(|i| {
            rating_group::item(
                i,
                RatingItemFlags {
                    checked: rating_group_state.is_checked(i),
                    highlighted: rating_group_state.is_highlighted(i),
                    disabled: false,
                    readonly: true,
                },
                &format!("{i} star{}", if i == 1 { "" } else { "s" }),
                vec![],
                vec![],
            )
        })
        .collect();
    let rating_control =
        rating_group::control(&rating_props, Some(RATING_LABEL_ID), vec![], rating_items);
    let rating = rating_group::root(
        Size::Sm,
        ColorPalette::Accent,
        &rating_props,
        vec![("data-blocks-card-meta-cta-rating", "")],
        vec![rating_label, rating_control],
    );

    let quantity_flags = NumberInputFlags {
        readonly: true,
        ..NumberInputFlags::default()
    };
    let quantity_input = number_input::root(
        Size::Md,
        false,
        false,
        true,
        vec![("data-blocks-card-meta-cta-quantity", "")],
        vec![
            number_input::label(
                quantity_flags,
                Some(QUANTITY_INPUT_ID),
                vec![],
                vec![text("数量")],
            ),
            number_input::control(
                quantity_flags,
                vec![],
                vec![
                    number_input::decrement_trigger(
                        Some(QUANTITY_INPUT_ID),
                        true,
                        vec![],
                        vec![text("-")],
                    ),
                    number_input::input(
                        "quantity",
                        Some(QUANTITY_INPUT_ID),
                        Some("1"),
                        "1",
                        "10",
                        quantity_flags,
                        vec![],
                    ),
                    number_input::increment_trigger(
                        Some(QUANTITY_INPUT_ID),
                        true,
                        vec![],
                        vec![text("+")],
                    ),
                ],
            ),
        ],
    );

    meta_cta_card(
        "新着",
        "デスクマット Pro",
        "手首の負担を抑える傾斜構造の作業用デスクマットです。",
        vec![price_row],
        vec![
            (spec_icon(), "サイズ:90 × 40 cm"),
            (spec_icon(), "素材:撥水コーティング表面"),
        ],
        vec![rating, quantity_input],
        "カートに追加",
    )
}

/// `card-meta-cta` の Demo 本体(求人・料金プラン・商品の 3 例を横並びに
/// する。狭い幅では 1 列へ積む、[`LAYOUT_CSS`] 参照)。
pub fn demo() -> Node {
    div(
        vec![("class", "blocks-card-meta-cta-layout")],
        vec![job_card(), plan_card(), product_card()],
    )
}

原案差分メモ

  • 代表構成(求人)・商品版(数量入力+評価)・料金プラン版(機能一覧)の 3 バリエーションを 1 つの共有骨格へまとめ、3 枚並記で示しました。
  • 数量入力と評価はいずれも readonly の静的表示にしています。
  • 参照元のアイコン・配色は持ち込まず、線画の自作幾何アイコンにしています。