feature-split-list-image
badge / heading / text / icon / card / image / button / data-list の 8 部品を合成した、左列に見出し・feature 一覧、右列に画像(1 枚または複数枚の組)を置く 2 列 feature セクションです(ただしインスタンス B のみ lg 以上で左右が入れ替わり、画像が左列・feature 一覧が右列になります)。
4 つの静的インスタンスを縦に並べています。インスタンス A はアイコン付きカード一覧(4 件・2 列)+ 縦長画像 1 枚、インスタンス B はアイコン付きの行一覧(3 件)+ 正方形画像 1 枚(この 1 件だけ DOM 順が「画像 → 一覧」で、lg 以上では画像が左列・一覧が右列になります)、インスタンス C は数値を主役にしたカード(2 件)+ ボタン行 + 正方形画像 1 枚、インスタンス D は定義リスト(4 件)+ 画像 4 枚の 2×2 グリッドです。幅 lg(64rem)未満は一覧・画像を 1 列に畳み、lg 以上で 2 列へ切り替わります。文言・データはすべて架空のもので、データ取得・送信は行わない静的な表示例です。<form> は使用せず、ボタンは type="button" のまま送信先を持ちません。
Demo
カード一覧(4 件・2 列)+ 縦長画像 1 枚。一覧 → 画像の順。
カード一覧と画像で特長を紹介する
各カードはアイコン・見出し・説明の順に積み、画面幅に応じて 1〜2 列へ切り替わります。
決定的なビルド
同じ入力からは常に同じ静的ファイルを生成します。
型で表現する構造
スロットと props は Rust の型で表現されます。
外部依存ゼロ
描画コアは外部クレートに依存しません。
単一実行ファイル配布
SSR/SSG のいずれも単一バイナリへまとめられます。
アイコン付き行(3 件)+ 正方形画像 1 枚。画像 → 一覧の順(狭幅で画像が上、lg で画像が左)。
アイコン付きの一覧で要点を伝える
各行はアイコン・短い見出し・説明の 3 要素で揃えています。
既定エスケープ
テキスト補間は既定でエスケープされます。
無 JS の静的表示
JS ハイドレーションを行わない決定的な表示です。
機械検証可能な構成
構造マニフェストが依存関係を機械検証します。
数値カード(2 件)+ ボタン行 + 正方形画像 1 枚。一覧 → 画像の順。
数値で語る特長とボタン導線
数値カードの下に、資料へのボタン導線を並べています。
14
公開クレート
描画コアから CLI までを個別クレートへ分割しています。
60
依存上限
標準構成の依存パッケージ数は 60 件以内に収めます。
定義リスト(4 件)+ 画像 4 枚の 2×2 グリッド。一覧 → 画像の順。
定義リストと画像の組で詳細を示す
用語ごとの短い説明を、画像 4 枚の組と並べています。
既定エスケープ
迂回経路は明示的なオプトイン API に限られます。
決定的な出力
同じ入力からは常に同じ静的ファイルを生成します。
unsafe 境界の限定
描画コア・状態管理コアでは unsafe を使用しません。
単一バイナリ配布
SSR/SSG を単一実行ファイルへまとめて配布できます。
使用部品
Rust コード
use crate::blocks::dummy_assets;
use fandhe_frontend_core::{div, el, text as core_text, Node};
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::card::{self, CardProps};
use fandhe_frontend_pre_styled_ui::data_list::{self, DataListOrientation, DataListProps};
use fandhe_frontend_pre_styled_ui::heading::{
self, HeadingLevel, HeadingProps, HeadingSize, HeadingWeight,
};
use fandhe_frontend_pre_styled_ui::icon::{icon, IconProps};
use fandhe_frontend_pre_styled_ui::image::{self, AspectRatio, ImageFit, ImageProps};
use fandhe_frontend_pre_styled_ui::text::{
self as styled_text, TextProps, TextSize, TextVariant, TextWeight,
};
use fandhe_frontend_pre_styled_ui::Size;
/// 装飾用の自作幾何アイコン(lucide 等の既存アイコンセットの path を
/// 複製しないための単純図形、`careers_card_grid::geo_icon` と同型の判断)。
fn geo_icon(size: Size, path_d: &'static str) -> Node {
icon(
&IconProps {
size,
..IconProps::default()
},
vec![],
vec![el(
"path",
vec![
("d", path_d),
("fill", "none"),
("stroke", "currentColor"),
("stroke-width", "2"),
("stroke-linecap", "round"),
("stroke-linejoin", "round"),
],
vec![],
)],
)
}
/// 円の幾何アイコン(インスタンス A のカード見出しに添える)。
fn circle_icon() -> Node {
geo_icon(Size::Md, "M12 3a9 9 0 100 18 9 9 0 000-18z")
}
/// 四角の幾何アイコン(インスタンス A のカード見出しに添える)。
fn square_icon() -> Node {
geo_icon(Size::Md, "M4 4h16v16H4z")
}
/// 折れ線の幾何アイコン(インスタンス A のカード見出しに添える)。
fn zigzag_icon() -> Node {
geo_icon(Size::Md, "M4 18l5-9 4 6 4-8 3 5")
}
/// 三角の幾何アイコン(インスタンス A のカード見出しに添える)。
fn triangle_icon() -> Node {
geo_icon(Size::Md, "M12 4l8 16H4z")
}
/// 盾の幾何アイコン(インスタンス B のアイコン付き行に添える)。
fn shield_icon() -> Node {
geo_icon(Size::Sm, "M12 3l7 3v5c0 5-3.5 8.5-7 10-3.5-1.5-7-5-7-10V6z")
}
/// 稲妻の幾何アイコン(インスタンス B のアイコン付き行に添える)。
fn bolt_icon() -> Node {
geo_icon(Size::Sm, "M13 3L5 14h5l-1 7 8-11h-5z")
}
/// 歯車の幾何アイコン(インスタンス B のアイコン付き行に添える)。
fn gear_icon() -> Node {
geo_icon(
Size::Sm,
"M12 8a4 4 0 100 8 4 4 0 000-8z M12 2v3 M12 19v3 M4.2 4.2l2.1 2.1 M17.7 17.7l2.1 2.1 M2 12h3 M19 12h3 M4.2 19.8l2.1-2.1 M17.7 6.3l2.1-2.1",
)
}
/// 星の幾何アイコン(インスタンス D の定義リストに添える)。
fn star_icon() -> Node {
geo_icon(
Size::Sm,
"M12 3l2.6 5.6 6.1.6-4.6 4.1 1.3 6-5.4-3.2-5.4 3.2 1.3-6-4.6-4.1 6.1-.6z",
)
}
/// 波の幾何アイコン(インスタンス D の定義リストに添える)。
fn wave_icon() -> Node {
geo_icon(Size::Sm, "M3 12c2-3 4-3 6 0s4 3 6 0 4-3 6 0")
}
/// 錠前の幾何アイコン(インスタンス D の定義リストに添える)。
fn lock_icon() -> Node {
geo_icon(Size::Sm, "M6 11V8a6 6 0 1112 0v3 M5 11h14v9H5z")
}
/// 地球の幾何アイコン(インスタンス D の定義リストに添える)。
fn globe_icon() -> Node {
geo_icon(
Size::Sm,
"M12 3a9 9 0 100 18 9 9 0 000-18z M3 12h18 M12 3c2.5 2.5 2.5 15.5 0 18 M12 3c-2.5 2.5-2.5 15.5 0 18",
)
}
/// feature 一覧カード 1 件分の架空データ(インスタンス A)。
struct FeatureItem {
icon_node: fn() -> Node,
title: &'static str,
body: &'static str,
}
/// インスタンス A のカードデータ(4 件)。
const FEATURES_A: [FeatureItem; 4] = [
FeatureItem {
icon_node: circle_icon,
title: "決定的なビルド",
body: "同じ入力からは常に同じ静的ファイルを生成します。",
},
FeatureItem {
icon_node: square_icon,
title: "型で表現する構造",
body: "スロットと props は Rust の型で表現されます。",
},
FeatureItem {
icon_node: zigzag_icon,
title: "外部依存ゼロ",
body: "描画コアは外部クレートに依存しません。",
},
FeatureItem {
icon_node: triangle_icon,
title: "単一実行ファイル配布",
body: "SSR/SSG のいずれも単一バイナリへまとめられます。",
},
];
/// インスタンス B のアイコン付き行データ(3 件)。
const FEATURES_B: [FeatureItem; 3] = [
FeatureItem {
icon_node: shield_icon,
title: "既定エスケープ",
body: "テキスト補間は既定でエスケープされます。",
},
FeatureItem {
icon_node: bolt_icon,
title: "無 JS の静的表示",
body: "JS ハイドレーションを行わない決定的な表示です。",
},
FeatureItem {
icon_node: gear_icon,
title: "機械検証可能な構成",
body: "構造マニフェストが依存関係を機械検証します。",
},
];
/// インスタンス C の数値カードデータ(2 件)。
struct StatItem {
value: &'static str,
title: &'static str,
body: &'static str,
}
const STATS_C: [StatItem; 2] = [
StatItem {
value: "14",
title: "公開クレート",
body: "描画コアから CLI までを個別クレートへ分割しています。",
},
StatItem {
value: "60",
title: "依存上限",
body: "標準構成の依存パッケージ数は 60 件以内に収めます。",
},
];
/// インスタンス D の定義リストデータ(4 件)。
struct DefinitionItem {
icon_node: fn() -> Node,
term: &'static str,
detail: &'static str,
}
const DEFINITIONS_D: [DefinitionItem; 4] = [
DefinitionItem {
icon_node: star_icon,
term: "既定エスケープ",
detail: "迂回経路は明示的なオプトイン API に限られます。",
},
DefinitionItem {
icon_node: wave_icon,
term: "決定的な出力",
detail: "同じ入力からは常に同じ静的ファイルを生成します。",
},
DefinitionItem {
icon_node: lock_icon,
term: "unsafe 境界の限定",
detail: "描画コア・状態管理コアでは unsafe を使用しません。",
},
DefinitionItem {
icon_node: globe_icon,
term: "単一バイナリ配布",
detail: "SSR/SSG を単一実行ファイルへまとめて配布できます。",
},
];
/// セクション見出し(badge + H3 + リード文)を組み立てる。
fn section_header(eyebrow: &str, title: &str, lead: &str) -> Node {
div(
vec![("class", "blocks-feature-split-list-image-header")],
vec![
badge::badge(
&BadgeProps::default(),
vec![("data-blocks-feature-split-list-image-eyebrow", "")],
vec![core_text(eyebrow)],
),
heading::heading(
HeadingLevel::H3,
&HeadingProps {
size: HeadingSize::Xl2,
weight: HeadingWeight::Bold,
},
vec![],
vec![core_text(title)],
),
styled_text::text(
&TextProps {
variant: TextVariant::Muted,
..TextProps::default()
},
vec![("data-blocks-feature-split-list-image-lead", "")],
vec![core_text(lead)],
),
],
)
}
/// インスタンス A の feature カード 1 枚(アイコン + H4 見出し + 説明)。
fn feature_card(item: &FeatureItem) -> Node {
card::root(
CardProps::default(),
vec![("data-blocks-feature-split-list-image-card", "")],
vec![card::body(
vec![("class", "blocks-feature-split-list-image-card-body")],
vec![
(item.icon_node)(),
heading::heading(
HeadingLevel::H4,
&HeadingProps {
size: HeadingSize::Sm,
weight: HeadingWeight::Semibold,
},
vec![],
vec![core_text(item.title)],
),
styled_text::text(
&TextProps {
size: TextSize::Sm,
variant: TextVariant::Muted,
..TextProps::default()
},
vec![("data-blocks-feature-split-list-image-card-desc", "")],
vec![core_text(item.body)],
),
],
)],
)
}
/// インスタンス B のアイコン付き行 1 件(アイコン列 + (H4 + 説明))。
fn feature_row(item: &FeatureItem) -> Node {
div(
vec![("class", "blocks-feature-split-list-image-row")],
vec![
div(
vec![("data-blocks-feature-split-list-image-row-icon", "")],
vec![(item.icon_node)()],
),
div(
vec![("class", "blocks-feature-split-list-image-row-text")],
vec![
heading::heading(
HeadingLevel::H4,
&HeadingProps {
size: HeadingSize::Sm,
weight: HeadingWeight::Semibold,
},
vec![],
vec![core_text(item.title)],
),
styled_text::text(
&TextProps {
size: TextSize::Sm,
variant: TextVariant::Muted,
..TextProps::default()
},
vec![],
vec![core_text(item.body)],
),
],
),
],
)
}
/// インスタンス C の数値カード 1 枚(大きな数値 + H4 見出し + 説明)。
fn stat_card(item: &StatItem) -> Node {
card::root(
CardProps::default(),
vec![("data-blocks-feature-split-list-image-stat-card", "")],
vec![card::body(
vec![("class", "blocks-feature-split-list-image-card-body")],
vec![
styled_text::text(
&TextProps {
size: TextSize::Xl3,
weight: TextWeight::Bold,
..TextProps::default()
},
vec![("data-blocks-feature-split-list-image-stat-value", "")],
vec![core_text(item.value)],
),
heading::heading(
HeadingLevel::H4,
&HeadingProps {
size: HeadingSize::Sm,
weight: HeadingWeight::Semibold,
},
vec![],
vec![core_text(item.title)],
),
styled_text::text(
&TextProps {
size: TextSize::Sm,
variant: TextVariant::Muted,
..TextProps::default()
},
vec![],
vec![core_text(item.body)],
),
],
)],
)
}
/// インスタンス C 末尾のボタン行(送信先を持たない見た目のみの導線)。
fn stat_button_row() -> Node {
div(
vec![("class", "blocks-feature-split-list-image-button-row")],
vec![
button::button(
&ButtonProps::default(),
vec![],
vec![core_text("詳しく見る")],
),
button::button(
&ButtonProps {
variant: ButtonVariant::Outline,
..ButtonProps::default()
},
vec![],
vec![core_text("資料をダウンロード")],
),
],
)
}
/// インスタンス D の定義リスト 1 項目(アイコン + 短い見出し / 説明)。
fn definition_item(item: &DefinitionItem) -> Node {
data_list::item(
vec![],
vec![
data_list::item_label(
vec![("class", "blocks-feature-split-list-image-dt")],
vec![
(item.icon_node)(),
styled_text::text(
&TextProps {
weight: TextWeight::Bold,
..TextProps::default()
},
vec![],
vec![core_text(item.term)],
),
],
),
data_list::item_value(
vec![],
vec![styled_text::text(
&TextProps {
variant: TextVariant::Muted,
..TextProps::default()
},
vec![],
vec![core_text(item.detail)],
)],
),
],
)
}
/// 画像 1 枚を組み立てる(`alt=""`、装飾扱い)。
fn split_image(src: &str, aspect_ratio: AspectRatio) -> Node {
image::image(
&ImageProps {
fit: ImageFit::Cover,
aspect_ratio,
..ImageProps::new(src, "")
},
vec![("data-blocks-feature-split-list-image-image", "")],
)
}
/// インスタンス A・C・D 共通の「一覧 → 画像」2 列レイアウト。
fn split_list_then_image(list: Node, image_col: Node) -> Node {
div(
vec![("class", "blocks-feature-split-list-image-split")],
vec![list, image_col],
)
}
/// インスタンス B 専用の「画像 → 一覧」2 列レイアウト(本モジュール doc
/// 「DOM 順と視覚順」節)。
fn split_image_then_list(image_col: Node, list: Node) -> Node {
div(
vec![("class", "blocks-feature-split-list-image-split")],
vec![image_col, list],
)
}
/// インスタンス A(カード一覧 + 縦長画像 1 枚)。
fn instance_a() -> Node {
let header = section_header(
"特長",
"カード一覧と画像で特長を紹介する",
"各カードはアイコン・見出し・説明の順に積み、画面幅に応じて 1〜2 列へ切り替わります。",
);
let grid = div(
vec![("class", "blocks-feature-split-list-image-card-grid")],
FEATURES_A.iter().map(feature_card).collect(),
);
let list = div(
vec![("class", "blocks-feature-split-list-image-list")],
vec![header, grid],
);
let image_col = split_image(dummy_assets::PRODUCT_SRC, AspectRatio::Portrait);
div(
vec![("class", "blocks-feature-split-list-image-instance")],
vec![split_list_then_image(list, image_col)],
)
}
/// インスタンス B(アイコン付き行 3 件 + 正方形画像 1 枚、DOM 順は画像が先)。
fn instance_b() -> Node {
let header = section_header(
"できること",
"アイコン付きの一覧で要点を伝える",
"各行はアイコン・短い見出し・説明の 3 要素で揃えています。",
);
let rows = div(
vec![("class", "blocks-feature-split-list-image-row-list")],
FEATURES_B.iter().map(feature_row).collect(),
);
let list = div(
vec![("class", "blocks-feature-split-list-image-list")],
vec![header, rows],
);
let image_col = split_image(dummy_assets::SCREENSHOT_SRC, AspectRatio::Square);
div(
vec![("class", "blocks-feature-split-list-image-instance")],
vec![split_image_then_list(image_col, list)],
)
}
/// インスタンス C(数値カード 2 枚 + ボタン行 + 正方形画像 1 枚)。
fn instance_c() -> Node {
let header = section_header(
"実績",
"数値で語る特長とボタン導線",
"数値カードの下に、資料へのボタン導線を並べています。",
);
let grid = div(
vec![("class", "blocks-feature-split-list-image-card-grid")],
STATS_C.iter().map(stat_card).collect(),
);
let list = div(
vec![("class", "blocks-feature-split-list-image-list")],
vec![header, grid, stat_button_row()],
);
let image_col = split_image(dummy_assets::BACKGROUND_SRC, AspectRatio::Square);
div(
vec![("class", "blocks-feature-split-list-image-instance")],
vec![split_list_then_image(list, image_col)],
)
}
/// インスタンス D(定義リスト 4 項目 + 画像 4 枚の 2×2 グリッド)。
fn instance_d() -> Node {
let header = section_header(
"詳細",
"定義リストと画像の組で詳細を示す",
"用語ごとの短い説明を、画像 4 枚の組と並べています。",
);
let items: Vec<Node> = DEFINITIONS_D.iter().map(definition_item).collect();
let dl = data_list::root(
DataListProps {
orientation: DataListOrientation::Vertical,
..DataListProps::default()
},
vec![],
items,
);
let list = div(
vec![("class", "blocks-feature-split-list-image-list")],
vec![header, dl],
);
let image_grid = div(
vec![("class", "blocks-feature-split-list-image-image-grid")],
vec![
split_image(dummy_assets::PRODUCT_SRC, AspectRatio::Square),
split_image(dummy_assets::SCREENSHOT_SRC, AspectRatio::Square),
split_image(dummy_assets::BACKGROUND_SRC, AspectRatio::Square),
split_image(dummy_assets::LOGO_SRC, AspectRatio::Square),
],
);
div(
vec![("class", "blocks-feature-split-list-image-instance")],
vec![split_list_then_image(list, image_grid)],
)
}
/// 各インスタンスに添える差分注記(`feature_image_cards` の `note_a`/
/// `note_b` と同じ形)。
fn note(body: &str) -> Node {
styled_text::text(
&TextProps {
size: TextSize::Xs,
variant: TextVariant::Muted,
..TextProps::default()
},
vec![],
vec![core_text(body)],
)
}
/// `feature-split-list-image` の Demo 本体。呼び出しごとに同一の `Node` を
/// 返す純関数(他 block と同じ状態を持たない設計)。
#[must_use]
pub fn demo() -> Node {
div(
vec![("class", "blocks-feature-split-list-image-layout")],
vec![
note("カード一覧(4 件・2 列)+ 縦長画像 1 枚。一覧 → 画像の順。"),
instance_a(),
note("アイコン付き行(3 件)+ 正方形画像 1 枚。画像 → 一覧の順(狭幅で画像が上、lg で画像が左)。"),
instance_b(),
note("数値カード(2 件)+ ボタン行 + 正方形画像 1 枚。一覧 → 画像の順。"),
instance_c(),
note("定義リスト(4 件)+ 画像 4 枚の 2×2 グリッド。一覧 → 画像の順。"),
instance_d(),
],
)
}原案差分メモ
参照(対応表 ID R0486・R0105・R0102・R0484・R0477・R0489・R0487・R1155・R1157・R1161。出典の固有名・ファイル名は記載しません)からの意図的な差分は次のとおりです。
- R0486 を基準形(左列一覧 + 右列画像の 2 列 feature セクション)とし、インスタンス A にしました。
- R0105 の「狭幅で画像が上・lg で画像が右」は DOM 順を変えずに済むインスタンス A・C・D の並びで代替し、CTA(ボタン導線)はインスタンス C へ集約しました。
- R0102・R0484・R0477 を統合してインスタンス B(アイコン付き行一覧)にしました。R0477 が持つ縦長画像は、インスタンス A で既に使っているためインスタンス B では正方形画像で代替しています。DOM 順のみ「画像 → 一覧」に入れ替え、狭幅で画像が上・lg で画像が左になるようにしました。
- R0489・R0487 を統合してインスタンス C(数値カード + ボタン行)にしました。
- R1155(6 項目)・R1157(全高画像)・R1161(大小 3 枚の段違い画像)を統合し、インスタンス D では項目数を 4 件へ縮約し、画像は均等な 2×2 グリッド(4 枚)で代替しました。
- 参照元の背景帯・装飾・実際の文言は持ち込まず、文言はすべて独自の架空のもの(日本語)にしました。
- 見出しは
h3/h4に下げました(ページ側が## Demoとしてh2を出すため)。インスタンス D の定義リストの短い見出しは、dtが見出し要素を子孫に持てないため太字テキストで表しています。 - 画像は
dummy_assetsのプレースホルダーとalt=""(装飾扱い)にしました。 - 文言・配色・余白は独自のもの、または既存のテーマトークンにそのまま従います。