Skip to content
fandhe-frontendcore v0.4.3
GitHub

hero-social-proof

badge / heading / text / button / avatar / rating-group の合成例(既存部品のみ、新規部品なし)。タグライン・見出し・リード文・CTA の下に、重なりアバター群 + readonly の星評価 + 利用者数の短文をまとめた社会的証明行を置きます。星評価は他ユーザーの平均評価を表す静的表示(readonly)です。sm(640px)未満では社会的証明行を縦積みにします。<form> は持ちません。

Demo

New: workspace insights

Ship features your team can trust

Ship with confidence, review with ease.

Average rating

Rated 5.0/5 by 12,000+ teams

使用部品

Rust コード

Rust
use fandhe_frontend_core::{div, text, Node};
use fandhe_frontend_pre_styled_ui::avatar::{self, AvatarProps, ImageStatus};
use fandhe_frontend_pre_styled_ui::badge::{self, BadgeProps};
use fandhe_frontend_pre_styled_ui::button::{self, ButtonProps, ButtonVariant};
use fandhe_frontend_pre_styled_ui::heading::{self, HeadingLevel, HeadingProps, HeadingSize};
use fandhe_frontend_pre_styled_ui::rating_group::{
    self, RatingGroup, RatingGroupProps, RatingItemFlags,
};
use fandhe_frontend_pre_styled_ui::text::{self as styled_text, TextProps, TextSize};
use fandhe_frontend_pre_styled_ui::visually_hidden;
use fandhe_frontend_pre_styled_ui::{ColorPalette, Size};

/// [`avatar_stack`] が使う架空イニシャル 4 件(`crate::blocks::dummy_assets::PERSON_NAMES`
/// 先頭 4 名から手書きした定数)。各アバターは同一画像の繰り返しを避けるため
/// 画像を持たせず、`ImageStatus::Error` の fallback(イニシャルのみ)で
/// 個別の利用者を示す(`testimonials_stack` と同じ判断軸)。
const AVATAR_INITIALS: [&str; 4] = ["HF", "EV", "KB", "ML"];

/// [`rating`] の `label` に使う一意 id(全 block Demo 横断で衝突しない
/// よう `blocks-hero-social-proof-` 接頭辞を付ける)。
const RATING_LABEL_ID: &str = "blocks-hero-social-proof-rating-label";

/// 重なりアバター群(社会的証明の装飾。モジュール doc「アバター群を
/// `aria-hidden` にする a11y 判断」参照)。
fn avatar_stack() -> Node {
    let avatars: Vec<Node> = AVATAR_INITIALS
        .iter()
        .map(|initials| {
            avatar::root(
                &AvatarProps {
                    size: Size::Sm,
                    stacked: true,
                    ..AvatarProps::default()
                },
                vec![("data-blocks-hero-social-proof-avatar", "")],
                vec![avatar::fallback(
                    ImageStatus::Error,
                    vec![],
                    vec![text(*initials)],
                )],
            )
        })
        .collect();
    avatar::group(
        vec![
            ("class", "blocks-hero-social-proof-avatars"),
            ("aria-hidden", "true"),
        ],
        avatars,
    )
}

/// 星評価(readonly。他ユーザーの平均評価を表す静的表示。
/// `showcase.rs::rating_group_section` の readonly 構成と同型)。
///
/// `label`("Average rating")は隣接する [`text`]("Rated 5.0/5 by
/// 12,000+ teams" の短文)と情報が重複するうえ、
/// `rating_group::label` の既定の可視表示は `.blocks-hero-social-proof-proof`
/// が横一列に並べる前提のレイアウトを崩す。`visually_hidden::root` で
/// テキストを包み、`aria-labelledby` で `control` と結びつく `id` を持つ
/// `<span>` 自体は DOM に残しつつ視覚的には隠す(`position: absolute` の
/// clip 手法のため他要素のレイアウトへ影響しない)。
fn rating() -> Node {
    let g = RatingGroup::new(5, Some(5), true);
    let props = RatingGroupProps {
        disabled: false,
        readonly: true,
        required: false,
    };
    let label = rating_group::label(
        &props,
        Some(RATING_LABEL_ID),
        vec![],
        vec![visually_hidden::root(vec![], vec![text("Average rating")])],
    );
    let items: Vec<Node> = (1..=g.count())
        .map(|i| {
            rating_group::item(
                i,
                RatingItemFlags {
                    checked: g.is_checked(i),
                    highlighted: g.is_highlighted(i),
                    disabled: false,
                    readonly: true,
                },
                &format!("{i} star{}", if i == 1 { "" } else { "s" }),
                vec![],
                vec![],
            )
        })
        .collect();
    let control = rating_group::control(&props, Some(RATING_LABEL_ID), vec![], items);
    rating_group::root(
        Size::Sm,
        ColorPalette::Accent,
        &props,
        vec![("data-blocks-hero-social-proof-rating", "")],
        vec![label, control],
    )
}

pub fn demo() -> Node {
    let tagline = badge::badge(
        &BadgeProps::default(),
        vec![("data-blocks-hero-social-proof-tagline", "")],
        vec![text("New: workspace insights")],
    );

    let title = heading::heading(
        HeadingLevel::H3,
        &HeadingProps {
            size: HeadingSize::Xl4,
            ..HeadingProps::default()
        },
        vec![("data-blocks-hero-social-proof-heading", "")],
        vec![text("Ship features your team can trust")],
    );

    let lead = styled_text::text(
        &TextProps {
            size: TextSize::Lg,
            ..TextProps::default()
        },
        vec![("data-blocks-hero-social-proof-lead", "")],
        vec![text("Ship with confidence, review with ease.")],
    );

    let actions = div(
        vec![("class", "blocks-hero-social-proof-actions")],
        vec![
            button::button(&ButtonProps::default(), vec![], vec![text("Get started")]),
            button::button(
                &ButtonProps {
                    variant: ButtonVariant::Outline,
                    ..ButtonProps::default()
                },
                vec![],
                vec![text("View docs")],
            ),
        ],
    );

    let proof = div(
        vec![("class", "blocks-hero-social-proof-proof")],
        vec![
            avatar_stack(),
            rating(),
            styled_text::text(
                &TextProps {
                    size: TextSize::Sm,
                    ..TextProps::default()
                },
                vec![("data-blocks-hero-social-proof-proof-text", "")],
                vec![text("Rated 5.0/5 by 12,000+ teams")],
            ),
        ],
    );

    div(
        vec![("class", "blocks-hero-social-proof-inner")],
        vec![tagline, title, lead, actions, proof],
    )
}