Skip to content
fandhe-frontendcore v0.4.3
GitHub

bento-three-column-tall

両端のセルが縦 2 行にまたがる 3 列 bento グリッドです。見出し帯(eyebrow badge + 見出し + リード文)の右側(1024px 以上)に CTA ボタンを添えたヘッダーの下に、fandhe-frontend-pre-styled-ui の card / image / code / heading / text を合成したセルを並べます。狭い画面(1024px 未満)では 1 列積みへ切り替わり、縦長セルも通常の高さに戻ります。

セルのカバー領域には画像・端末風の枠・コード表示枠の 3 種類のメディアを混在させています。端末風の枠・コード表示枠はいずれも code パートの行を並べただけの静的表示で、@keyframes/animation は使いません。

「両端のセルが縦長(基準形)」「1 枚目のみ縦長」の 2 形態を並べて示しています。いずれも画像はビルド時生成のモノトーン抽象図形(プレースホルダー)で、機能名・説明文・コマンド行・設定値はすべて架空のものです。Blocks セクションは新規部品を追加するものではなく、既存の Themes/Primitives 部品を組み合わせた実例集であることに注意してください。

Demo

プラットフォーム機能

必要な機能をひとつの基盤に

運用・デプロイ・権限管理をまとめて提供する、開発者向けプラットフォームの主要機能です。

両端のセルが縦長(基準形)

統合ダッシュボード

複数サービスの稼働状況を 1 画面へ集約して表示します。

$ fw deploy --target staging
$ pipeline run --target staging
$ pipeline status

自動デプロイ

コミットからビルド・検証・配信までを自動化します。

チーム権限管理

ロールごとに閲覧・操作範囲を細かく制御します。

alerts.toml
[alert.api_requests]
threshold = 10000
window = "1h"
notify = ["email"]

利用量アラート

しきい値を超えた利用量を検知し即座に通知します。

1 枚目のみ縦長

監視ビュー

リクエスト数・エラー率をリアルタイムで確認できます。

ビルドキャッシュ

依存関係の再ビルドを省略し検証時間を短縮します。

監査ログ

操作履歴を改ざん不可能な形式で保存します。

Webhook 連携

外部サービスへイベントを即座に転送します。

API キー管理

発行済みキーの利用範囲・有効期限を一覧管理します。

使用部品

Rust コード

Rust
use crate::blocks::dummy_assets;
use fandhe_frontend_core::{div, p, span, text, Node};
use fandhe_frontend_pre_styled_ui::badge::{self, BadgeProps};
use fandhe_frontend_pre_styled_ui::button::{self, ButtonProps};
use fandhe_frontend_pre_styled_ui::card::{self, CardProps};
use fandhe_frontend_pre_styled_ui::code::{self, CodeProps};
use fandhe_frontend_pre_styled_ui::heading::{self, HeadingLevel, HeadingProps, HeadingSize};
use fandhe_frontend_pre_styled_ui::image::{self, AspectRatio, ImageProps};
use fandhe_frontend_pre_styled_ui::text::{self as styled_text, TextProps, TextSize, TextVariant};

/// 1 セルのカバー領域に置くメディア種別(画像 / 端末風の枠 / コード
/// 表示枠)。[`cell`] が `slot` ごとに 1 種類を選び分ける。
enum Media {
    /// ビルド時生成のモノトーン抽象図形プレースホルダー。
    Image {
        src: &'static str,
        aspect: AspectRatio,
    },
    /// `hero_terminal` と同型の端末風の枠(titlebar + プロンプト行)。
    Terminal { lines: &'static [&'static str] },
    /// ファイル名タブ付きのコード表示枠。
    CodeWindow {
        filename: &'static str,
        lines: &'static [&'static str],
    },
}

/// 1 枚分のセルデータ(架空の開発者向けプラットフォームの機能紹介、
/// 実企業名・実サービス名は含まない)。`slot` はグリッド内の配置を表す
/// 識別子で、[`LAYOUT_CSS`] の `[data-blocks-bento-three-column-tall-cell]`
/// セレクタの値と一致させる。
struct BentoCell {
    slot: &'static str,
    title: &'static str,
    description: &'static str,
    media: Media,
}

/// 基準形(`both-tall`、対応表 ID R0410 + R0768): 両端セルが縦長で、
/// メディアは画像・端末風の枠・コード表示枠を混在させる(R0768 の
/// 「セルごとにメディアが異なる」を取り込む)。
const BOTH_TALL_CELLS: [BentoCell; 4] = [
    BentoCell {
        slot: "start",
        title: "統合ダッシュボード",
        description: "複数サービスの稼働状況を 1 画面へ集約して表示します。",
        media: Media::Image {
            src: dummy_assets::SCREENSHOT_SRC,
            aspect: AspectRatio::Portrait,
        },
    },
    BentoCell {
        slot: "center-top",
        title: "自動デプロイ",
        description: "コミットからビルド・検証・配信までを自動化します。",
        media: Media::Terminal {
            lines: &[
                "fw deploy --target staging",
                "pipeline run --target staging",
                "pipeline status",
            ],
        },
    },
    BentoCell {
        slot: "center-bottom",
        title: "チーム権限管理",
        description: "ロールごとに閲覧・操作範囲を細かく制御します。",
        media: Media::Image {
            src: dummy_assets::LOGO_SRC,
            aspect: AspectRatio::Video,
        },
    },
    BentoCell {
        slot: "end",
        title: "利用量アラート",
        description: "しきい値を超えた利用量を検知し即座に通知します。",
        media: Media::CodeWindow {
            filename: "alerts.toml",
            lines: &[
                "[alert.api_requests]",
                "threshold = 10000",
                "window = \"1h\"",
                "notify = [\"email\"]",
            ],
        },
    },
];

/// 1 枚目のみ縦長(`start-tall`、対応表 ID R0107): 見出し+CTA 行は共有
/// ヘッダーへ畳み込み済みのため、セル自体は基準形と重複しない架空の機能
/// 名を画像メディアで並べる。
const START_TALL_CELLS: [BentoCell; 5] = [
    BentoCell {
        slot: "start",
        title: "監視ビュー",
        description: "リクエスト数・エラー率をリアルタイムで確認できます。",
        media: Media::Image {
            src: dummy_assets::SCREENSHOT_SRC,
            aspect: AspectRatio::Portrait,
        },
    },
    BentoCell {
        slot: "center-top",
        title: "ビルドキャッシュ",
        description: "依存関係の再ビルドを省略し検証時間を短縮します。",
        media: Media::Image {
            src: dummy_assets::PRODUCT_SRC,
            aspect: AspectRatio::Video,
        },
    },
    BentoCell {
        slot: "center-bottom",
        title: "監査ログ",
        description: "操作履歴を改ざん不可能な形式で保存します。",
        media: Media::Image {
            src: dummy_assets::LOGO_SRC,
            aspect: AspectRatio::Video,
        },
    },
    BentoCell {
        slot: "end-top",
        title: "Webhook 連携",
        description: "外部サービスへイベントを即座に転送します。",
        media: Media::Image {
            src: dummy_assets::BACKGROUND_SRC,
            aspect: AspectRatio::Video,
        },
    },
    BentoCell {
        slot: "end-bottom",
        title: "API キー管理",
        description: "発行済みキーの利用範囲・有効期限を一覧管理します。",
        media: Media::Image {
            src: dummy_assets::PRODUCT_SRC,
            aspect: AspectRatio::Video,
        },
    },
];

/// 見出し帯(eyebrow badge + 見出し + リード文)。CTA ボタンは
/// [`header`] 側で右に添える。
fn intro() -> Node {
    div(
        vec![("class", "blocks-bento-three-column-tall-intro")],
        vec![
            badge::badge(
                &BadgeProps::default(),
                vec![],
                vec![text("プラットフォーム機能")],
            ),
            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(
                    "運用・デプロイ・権限管理をまとめて提供する、開発者向けプラットフォームの主要機能です。",
                )],
            ),
        ],
    )
}

/// 見出し帯 + CTA の行(2 形態で共有する 1 個のヘッダー)。
fn header() -> Node {
    div(
        vec![("class", "blocks-bento-three-column-tall-header")],
        vec![
            intro(),
            button::button(
                &ButtonProps::default(),
                vec![("data-blocks-bento-three-column-tall-cta", "")],
                vec![text("機能一覧を見る")],
            ),
        ],
    )
}

/// プロンプト記号(端末風の枠の各行先頭)。
fn prompt() -> Node {
    span(
        vec![("class", "blocks-bento-three-column-tall-prompt")],
        vec![text("$ ")],
    )
}

/// [`Media`] をカバー領域の子ノードへ変換する。画像はそのまま
/// [`image::image`] へ委譲し、端末風の枠・コード表示枠は 1 行ずつ
/// `code::code` で描く静的表示にする(`@keyframes`/`animation` は使わない)。
fn media(item: &Media) -> Node {
    match item {
        Media::Image { src, aspect } => image::image(
            &ImageProps {
                aspect_ratio: *aspect,
                ..ImageProps::new(src, "")
            },
            vec![],
        ),
        Media::Terminal { lines } => {
            let dot = || {
                span(
                    vec![("class", "blocks-bento-three-column-tall-dot")],
                    vec![],
                )
            };
            let line_nodes: Vec<Node> = lines
                .iter()
                .map(|line| {
                    div(
                        vec![("class", "blocks-bento-three-column-tall-line")],
                        vec![
                            prompt(),
                            code::code(&CodeProps::default(), vec![], vec![text(*line)]),
                        ],
                    )
                })
                .collect();
            div(
                vec![("data-blocks-bento-three-column-tall-media", "terminal")],
                vec![
                    div(
                        vec![("class", "blocks-bento-three-column-tall-titlebar")],
                        vec![dot(), dot(), dot()],
                    ),
                    div(
                        vec![("class", "blocks-bento-three-column-tall-lines")],
                        line_nodes,
                    ),
                ],
            )
        }
        Media::CodeWindow { filename, lines } => {
            let line_nodes: Vec<Node> = lines
                .iter()
                .map(|line| {
                    div(
                        vec![("class", "blocks-bento-three-column-tall-line")],
                        vec![code::code(&CodeProps::default(), vec![], vec![text(*line)])],
                    )
                })
                .collect();
            div(
                vec![("data-blocks-bento-three-column-tall-media", "code")],
                vec![
                    div(
                        vec![("class", "blocks-bento-three-column-tall-tabbar")],
                        vec![span(
                            vec![("class", "blocks-bento-three-column-tall-tab")],
                            vec![text(*filename)],
                        )],
                    ),
                    div(
                        vec![("class", "blocks-bento-three-column-tall-lines")],
                        line_nodes,
                    ),
                ],
            )
        }
    }
}

/// 1 枚分の bento セル(`card` + カバーメディア + 見出し/説明)を組み立
/// てる。
fn cell(item: &BentoCell) -> Node {
    card::root(
        CardProps::default(),
        vec![("data-blocks-bento-three-column-tall-cell", item.slot)],
        vec![
            card::cover(
                vec![("data-blocks-bento-three-column-tall-cover", "")],
                vec![media(&item.media)],
            ),
            card::body(
                vec![("class", "blocks-bento-three-column-tall-body")],
                vec![
                    heading::heading(
                        HeadingLevel::H4,
                        &HeadingProps::default(),
                        vec![],
                        vec![text(item.title)],
                    ),
                    styled_text::text(
                        &TextProps {
                            size: TextSize::Sm,
                            variant: TextVariant::Muted,
                            ..TextProps::default()
                        },
                        vec![],
                        vec![text(item.description)],
                    ),
                ],
            ),
        ],
    )
}

/// 1 形態ぶんの並記ブロック(キャプション + グリッド)。`key` は
/// [`LAYOUT_CSS`] の `data-blocks-bento-three-column-tall-variant` 値。
fn variant(key: &'static str, caption: &'static str, cells: &[BentoCell]) -> Node {
    let cell_nodes: Vec<Node> = cells.iter().map(cell).collect();
    div(
        vec![
            ("class", "blocks-bento-three-column-tall-variant"),
            ("data-blocks-bento-three-column-tall-variant", key),
        ],
        vec![
            p(
                vec![("class", "blocks-bento-three-column-tall-caption")],
                vec![text(caption)],
            ),
            div(
                vec![("class", "blocks-bento-three-column-tall-grid")],
                cell_nodes,
            ),
        ],
    )
}

/// `bento-three-column-tall` の Demo 本体。呼び出しごとに同一の `Node` を
/// 返す純関数(見出し帯 + CTA、両端が縦長の基準形、1 枚目のみ縦長の 2
/// 形態を並記する、モジュール doc「レイアウト」節)。
pub fn demo() -> Node {
    div(
        vec![("class", "blocks-bento-three-column-tall")],
        vec![
            header(),
            variant("both-tall", "両端のセルが縦長(基準形)", &BOTH_TALL_CELLS),
            variant("start-tall", "1 枚目のみ縦長", &START_TALL_CELLS),
        ],
    )
}

関連情報: Badge / Heading / Text / Button / Card / Image / Code

差分メモ

集約元 3 件(対応表 ID)からの取り込み方針です。

  • 基準形(「両端のセルが縦長」)は R0410 に対応し、Demo の 1 つ目の形態として示しています。
  • R0107 の「1 枚目のみ縦長」は、2 つ目の形態として並記しました。R0107 が持つ見出し + CTA 行は、2 形態で共有する 1 個のヘッダーへ畳み込み、CTA ボタンは 1 回だけ出力しています。
  • R0768 の「セルごとにメディアが異なる」は、基準形の中に端末風の枠とコード表示枠として取り込みました。参照側にあったスマホ枠のモック表示は、既存部品の組み合わせだけでは素直に再現できないため、画像(スクリーンショット枠)で代替しています。
  • 見出しレベルは H3(見出し帯)/ H4(各セル)に 1 段下げています。ページ側が ## Demo として h2 を出力するためです。
  • 端末風の枠・コード表示枠の内容はすべて架空で、@keyframes/ animation を持たない静的表示にしています。
  • 文言・配色・装飾・アイコンは参照元から持ち込まず、既存のテーマトークンに従っています。
  • id 属性・aria-* 参照は出力していません。