empty-state-starter-grid
候補が何もない状態から、行き先を選んでもらう空状態です。見出しと説明文の下に、候補(ガイド・サンプル・API カタログ等のサイト内索引ページ)を「アイコン + 題名 + 説明 + 開く操作」のタイルとしてグリッドで並べ、末尾に一覧へ戻る導線を置きます。item / icon / heading / text / link の 5 部品を合成します。Blocks は既存部品の合成例であり、新しい UI 部品は追加しません。
主参照は対応表 ID R0463(代表構成)で、R1396(テンプレート選択の 2 列グリッド・全面クリック)を集約しています。タイルの遷移先はいずれもこのサイト内に実在する索引ページ(ガイド・サンプル・Primitives 等)への相対パスです。実際に使う際は自分のページ URL へ差し替えてください。
本 Demo は無 JS の静的表示のみであり、<form> を含みません。タイル全体をクリック可能にするため「開く」リンクを stretched-link(CSS の ::after { inset: 0 })でタイル全面へ拡張しています。button は使用部品に含めていません。「タイル全体をリンク」と「開くボタン」を両立させようとすると <a> に <button> を入れ子にすることになり HTML として不正になるためです。
Demo
代表構成・3 列(R0463)
どこから見ますか
気になる入り口を選ぶと、対応する一覧ページへ移動します。
テンプレート選択・2 列(R1396)
使用部品
Rust コード
Rust
use fandhe_frontend_core::{div, el, text, Node};
use fandhe_frontend_pre_styled_ui::heading::{heading, HeadingLevel, HeadingProps};
use fandhe_frontend_pre_styled_ui::icon::{icon, IconProps};
use fandhe_frontend_pre_styled_ui::item::{self, ItemMediaVariant, ItemRootProps};
use fandhe_frontend_pre_styled_ui::link::{self, LinkProps, LinkVariant};
use fandhe_frontend_pre_styled_ui::text::{self as styled_text, TextProps, TextSize, TextVariant};
const TILE_ATTR: &str = "data-blocks-empty-state-starter-grid-tile";
const MEDIA_ATTR: &str = "data-blocks-empty-state-starter-grid-media";
const OPEN_ATTR: &str = "data-blocks-empty-state-starter-grid-open";
/// タイル 1 件分のデータ(モジュール doc「href の方針」節参照)。
struct Tile {
href: &'static str,
title: &'static str,
description: &'static str,
icon_path_d: &'static str,
}
/// 候補タイル 6 件(サイト内に実在する索引ページのみを指す)。
const TILES: [Tile; 6] = [
Tile {
href: "../../guides/",
title: "ガイド集",
description: "手順に沿って機能の使い方を学べるガイド一覧から始めます。",
icon_path_d: "M4 4h16v16H4zM8 8h8M8 12h8M8 16h5",
},
Tile {
href: "../../examples/",
title: "サンプル集",
description: "動く構成をそのまま確認できるサンプル一覧から始めます。",
icon_path_d: "M4 4h7v7H4zM13 4h7v4h-7zM13 11h7v9h-7zM4 14h7v6H4z",
},
Tile {
href: "../../primitives/",
title: "Primitives 一覧",
description: "アクセシブルな headless UI 部品一覧から始めます。",
icon_path_d: "M4 4h16v6H4zM4 13h16M4 17h10",
},
Tile {
href: "../../themes/",
title: "Themes 一覧",
description: "スタイル済みの UI 部品一覧から始めます。",
icon_path_d: "M4 4h6v16H4zM12 4h8v16h-8zM14 8h4M14 12h4",
},
Tile {
href: "../",
title: "Blocks 一覧",
description: "既存部品を組み合わせた合成例一覧から始めます。",
icon_path_d: "M12 3l1.2 2.4 2.6.4-1.9 1.9.5 2.6-2.4-1.3-2.4 1.3.5-2.6-1.9-1.9 2.6-.4z",
},
Tile {
href: "../../api/",
title: "API カタログ",
description: "エンドポイント一覧を備えた参照ページから始めます。",
icon_path_d: "M4 4h16v16H4zM4 9h16",
},
];
/// 自作の線画(stroke)アイコンを組み立てる(`grid_list_action_tiles::
/// geo_icon` と同型のパターン。装飾用途のため `IconProps::label` は付けない)。
fn geo_icon(path_d: &'static str) -> Node {
icon(
&IconProps::default(),
vec![],
vec![el(
"path",
vec![
("d", path_d),
("fill", "none"),
("stroke", "currentColor"),
("stroke-width", "1.5"),
("stroke-linecap", "round"),
("stroke-linejoin", "round"),
],
vec![],
)],
)
}
/// タイル 1 件分を組み立てる(モジュール doc「構造」節。`item::root` へ
/// `position: relative`([`LAYOUT_CSS`])を持たせ、`open_link` の
/// stretched-link(`::after { inset: 0 }`)でタイル全面をクリック可能に
/// する)。
fn starter_tile(tile: &Tile) -> Node {
item::root(
ItemRootProps::default(),
vec![(TILE_ATTR, "")],
vec![
item::media(
ItemMediaVariant::Icon,
vec![(MEDIA_ATTR, "")],
vec![geo_icon(tile.icon_path_d)],
),
item::content(
vec![],
vec![
heading(
HeadingLevel::H4,
&HeadingProps::default(),
vec![],
vec![text(tile.title)],
),
styled_text::text(
&TextProps {
variant: TextVariant::Muted,
..TextProps::default()
},
vec![],
vec![text(tile.description)],
),
],
),
item::actions(
vec![],
vec![link::root(
tile.href,
&LinkProps::default(),
vec![
(OPEN_ATTR, ""),
("aria-label", &format!("{} を開く", tile.title)),
],
vec![text("開く")],
)],
),
],
)
}
/// 見出し + 説明文 + タイルグリッド + 末尾導線の 1 インスタンス分を
/// 組み立てる。`columns` は `[data-columns]` の値(`@container` 側で
/// 参照、[`LAYOUT_CSS`])。
fn starter_instance(
heading_text: &'static str,
description_text: &'static str,
columns: &'static str,
tiles: &[Tile],
) -> Node {
div(
vec![("class", "blocks-empty-state-starter-grid-instance")],
vec![
div(
vec![("class", "blocks-empty-state-starter-grid-header")],
vec![
heading(
HeadingLevel::H3,
&HeadingProps::default(),
vec![],
vec![text(heading_text)],
),
styled_text::text(
&TextProps {
variant: TextVariant::Muted,
..TextProps::default()
},
vec![],
vec![text(description_text)],
),
],
),
div(
vec![
("class", "blocks-empty-state-starter-grid-grid"),
("data-columns", columns),
],
tiles.iter().map(starter_tile).collect(),
),
div(
vec![("class", "blocks-empty-state-starter-grid-footer")],
vec![
styled_text::text(
&TextProps {
size: TextSize::Sm,
variant: TextVariant::Muted,
..TextProps::default()
},
vec![],
vec![text("候補が合わない場合は")],
),
link::root(
"../",
&LinkProps {
variant: LinkVariant::Underline,
..LinkProps::default()
},
vec![],
vec![text("Blocks 一覧に戻る")],
),
],
),
],
)
}
/// 版ラベル(`empty_state_invite_team::variant_label` と同型)。
fn variant_label(label: &'static str) -> Node {
styled_text::text(
&TextProps {
size: TextSize::Sm,
variant: TextVariant::Muted,
..TextProps::default()
},
vec![],
vec![text(label)],
)
}
/// `empty-state-starter-grid` の Demo 本体。呼び出しごとに同一の `Node`
/// を返す純関数(モジュール doc「2 版と集約元の対応」節)。
pub fn demo() -> Node {
div(
vec![("class", "blocks-empty-state-starter-grid-stack")],
vec![
variant_label("代表構成・3 列(R0463)"),
starter_instance(
"どこから見ますか",
"気になる入り口を選ぶと、対応する一覧ページへ移動します。",
"3",
&TILES,
),
variant_label("テンプレート選択・2 列(R1396)"),
starter_instance(
"調べたい分野を選ぶ",
"気になる分野を選ぶと、そのページへ移動します。",
"2",
&TILES[..4],
),
],
)
}原案差分メモ
- 版 A(代表構成・3 列、R0463): 見出し・説明文の下に、サイト内索引ページへの候補タイル 6 件を 3 列グリッドで並べます。
- 版 B(テンプレート選択・2 列、R1396): 版 A と同じ骨格のまま、候補を先頭 4 件へ絞り 2 列グリッドで並べます。
- 各タイルはアイコン・題名・説明・「開く」リンクで構成し、「開く」リンクを stretched-link(CSS の
::after { inset: 0 })でタイル全面へ拡張して、タイルのどこをクリックしても遷移できるようにしています。 - Issue の部品指示にある
buttonは使用しません。「タイル全体をリンク」と「開くボタン」を両立させようとすると<a>に<button>を入れ子にすることになり HTML として不正になるためです。ボタン風の外見はlink::rootへ枠線・padding を与えて表現しています。 - 狭幅(コンテナ幅 36rem 未満)では常に 1 列、36rem 以上では 2 列、56rem 以上かつ版 A(3 列指定)のみ 3 列へ切り替わります(
@containerによるコンテナクエリ判定)。 - 装飾アイコンは自作の線画(SVG path)で、参照元由来のアイコンセットではありません。