header-flyout-menu
fandhe-frontend-pre-styled-ui の navigation-menu / button / icon / link 部品を合成した、ドロップダウン付きヘッダーです。Blocks セクションは新規部品を追加するものではなく、既存の Themes/Primitives 部品を組み合わせた実例集であることに注意してください(主参照は対応表 ID R0981。出典の固有名・ファイル名は記載しません)。
ロゴ・ナビ・アクションを横並びに配置し、48rem 未満の狭い幅ではナビとアクションを隠してハンバーガーボタンのみを表示します。Demo は集約元のバリエーション(対応表 ID R0575/R0576/R0577/R0987/R0988 を集約)を 3 形並記しています。標準形は「製品」フライアウトを開いた状態(末尾に補助 CTA 行つき)・「リソース」は閉じた状態のまま固定表示し、各項目はアイコン・ラベル・説明の 3 要素で構成しています。テキストのみ形はラベルだけのドロップダウン + ボタン 2 個、中央寄せ形は 48rem 以上でナビを中央へ寄せ、フライアウト項目をアイコン付き 2 列グリッドで並べます。
本 Demo は静的な表示例であり、docs サイトは JS ハイドレーションを行わないため、押しても何も起きないトリガー(ナビ項目のトリガー・ハンバーガーボタン・主 CTA ボタン)はすべて disabled にして操作不能であることを明示しています。<form> 要素は一切持たず、データの取得・送信・状態管理を行いません。アクション行のボタンは type="button" のまま送信先を持ちません。文言はすべて独自に書いた架空のものであり、実企業名・実クレデンシャル・PII を含みません。実際に使うときはリンク先を差し替えてください。
Demo
使用部品
Rust コード
use fandhe_frontend_core::{div, el, header, p, span, text, Node};
use fandhe_frontend_pre_styled_ui::button::{self, ButtonProps, ButtonVariant};
use fandhe_frontend_pre_styled_ui::icon::{icon, IconProps};
use fandhe_frontend_pre_styled_ui::link::{self, LinkProps};
use fandhe_frontend_pre_styled_ui::navigation_menu::{self, NavigationMenuProps, OpenState};
use fandhe_frontend_pre_styled_ui::Size;
/// 実在の自リポジトリ URL(`href` の方針、モジュール doc参照)。
const REPO: &str = "https://github.com/Fandhe-AI/fandhe-frontend";
/// 実在の自組織 URL(Company リンク用)。
const ORG: &str = "https://github.com/Fandhe-AI";
/// 「Product」フライアウトの項目一覧(アイコン path・ラベル・説明・
/// href の組。件数を単体テストから参照するため `const` 配列にする)。
const PRODUCT_FLYOUT_ITEMS: &[(&str, &str, &str, &str)] = &[
(
"M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z",
"ダッシュボード",
"主要な指標をひと目で確認できる管理画面",
REPO,
),
(
"M12 2l2.4 7.2H22l-6 4.6 2.3 7.2L12 16.4 5.7 21l2.3-7.2-6-4.6h7.6z",
"自動化",
"繰り返し作業をワークフローとして自動化",
REPO,
),
(
"M4 12a8 8 0 1116 0 8 8 0 01-16 0zm8-5v5l3 3",
"レポート",
"チームの成果を定期レポートとして共有",
REPO,
),
(
"M12 2l9 4.9v9.6L12 22l-9-4.9V6.9z",
"連携",
"他ツールとの連携でデータを一元管理",
REPO,
),
(
"M12 2l7 3v6c0 4.9-3 8.7-7 11-4-2.3-7-6.1-7-11V5z",
"セキュリティ",
"アクセス権限と監査ログを一元管理",
REPO,
),
];
/// 「Resources」フライアウトの項目一覧。
const RESOURCES_FLYOUT_ITEMS: &[(&str, &str, &str, &str)] = &[
(
"M6 2h9l3 3v17H6zM15 2v3h3",
"ドキュメント",
"導入手順と API リファレンス",
REPO,
),
(
"M12 2a5 5 0 015 5 5 5 0 01-10 0 5 5 0 015-5zM4 22a8 8 0 0116 0",
"コミュニティ",
"利用者同士で情報交換できる場",
REPO,
),
];
/// centered 形フライアウトの項目一覧(2 列グリッドで並べる、R0577)。
const PLATFORM_FLYOUT_ITEMS: &[(&str, &str, &str, &str)] = &[
(
"M4 4h16v4H4zM4 12h16v8H4z",
"ワークスペース",
"チームで共有する作業スペース",
REPO,
),
(
"M12 2l9 4.9v9.6L12 22l-9-4.9V6.9z",
"API",
"外部サービスと連携する REST/gRPC API",
REPO,
),
(
"M4 12a8 8 0 1116 0 8 8 0 01-16 0zm8-5v5l3 3",
"モニタリング",
"稼働状況をリアルタイムに可視化",
REPO,
),
(
"M12 2l7 3v6c0 4.9-3 8.7-7 11-4-2.3-7-6.1-7-11V5z",
"アクセス制御",
"ロールベースの権限管理",
REPO,
),
];
/// simple 形フライアウトの項目一覧(テキストのみ、R0575)。
const SERVICES_TEXT_ITEMS: &[(&str, &str)] = &[
("導入支援", REPO),
("運用代行", REPO),
("トレーニング", REPO),
];
/// 装飾用の幾何図形アイコン(`label: None`)を組み立てる(自作 SVG、実在
/// ブランドのロゴを模さない)。
fn geo_icon(size: Size, d: &str) -> Node {
icon(
&IconProps {
size,
label: None,
..IconProps::default()
},
vec![],
vec![el("path", vec![("d", d)], vec![])],
)
}
/// シェブロン(開閉インジケータ)用の装飾アイコン。
fn chevron_icon() -> Node {
geo_icon(Size::Sm, "M6 9l6 6 6-6")
}
/// ハンバーガー(3 本線)アイコン。
fn hamburger_icon() -> Node {
geo_icon(Size::Md, "M3 6h18v2H3zM3 11h18v2H3zM3 16h18v2H3z")
}
/// ロゴ(幾何図形アイコン + ブランド名テキスト)。実在ブランドのロゴを
/// 模さない自作の SVG。
fn logo() -> Node {
link::root(
REPO,
&LinkProps::default(),
vec![("data-blocks-header-flyout-menu-logo", "")],
vec![
geo_icon(Size::Md, "M4 4h16v6H4zM4 14h16v6H4z"),
span(
vec![("class", "blocks-header-flyout-menu-brand")],
vec![text("Fandhe Frontend")],
),
],
)
}
/// フライアウト 1 件分の項目(アイコン + ラベル + 説明の 3 要素)。
fn flyout_item(icon_path: &str, label: &str, description: &str, href: &str) -> Node {
navigation_menu::link(
href,
false,
vec![("data-blocks-header-flyout-menu-item", "")],
vec![
geo_icon(Size::Md, icon_path),
div(
vec![("class", "blocks-header-flyout-menu-item-text")],
vec![
span(
vec![("class", "blocks-header-flyout-menu-item-label")],
vec![text(label)],
),
p(
vec![("class", "blocks-header-flyout-menu-item-description")],
vec![text(description)],
),
],
),
],
)
}
/// アイコン + ラベル + 説明の項目一覧を [`flyout_item`] へ写像する
/// (`nav_item_with_flyout` へ渡す `items` の組み立て用)。
fn flyout_items(items: &[(&str, &str, &str, &str)]) -> Vec<Node> {
items
.iter()
.map(|(icon_path, label, description, href)| {
flyout_item(icon_path, label, description, href)
})
.collect()
}
/// テキストのみの簡易フライアウト項目(R0575。アイコン・説明を持たない)。
fn flyout_text_item(label: &str, href: &str) -> Node {
navigation_menu::link(
href,
false,
vec![("data-blocks-header-flyout-menu-text-item", "")],
vec![text(label)],
)
}
/// フライアウト下部の補助 CTA 行 1 本(アイコン付き `link::root`、
/// モジュール doc「補助 CTA 行」節)。
fn footer_link(icon_path: &str, label: &str, href: &str) -> Node {
link::root(
href,
&LinkProps::default(),
vec![("data-blocks-header-flyout-menu-panel-footer-link", "")],
vec![geo_icon(Size::Sm, icon_path), text(label)],
)
}
/// 補助 CTA 行のコンテナ(2 本のリンクを横並びで持つ)。
fn panel_footer() -> Node {
div(
vec![("data-blocks-header-flyout-menu-panel-footer", "")],
vec![
footer_link("M5 12h14M13 6l6 6-6 6", "デモを見る", REPO),
footer_link("M2 12h20M12 2v20", "営業に相談する", REPO),
],
)
}
/// caption(並記された各形の見出し、`footer_inline_nav` 前例と同型)。
fn caption(label: &str) -> Node {
p(
vec![("class", "blocks-header-flyout-menu-caption")],
vec![text(label)],
)
}
/// トリガー付きナビ項目(フライアウトを持つ)。`disabled: true` 固定
/// (モジュール doc「静的表示」節)。`footer`(補助 CTA 行)・
/// `columns`(パネルの列数、centered 形の 2 列グリッド用)は任意。
fn nav_item_with_flyout(
props: &NavigationMenuProps,
value: &str,
label: &str,
state: OpenState,
items: Vec<Node>,
footer: Option<Node>,
columns: Option<&'static str>,
) -> Node {
let trigger_id = format!("blocks-header-flyout-menu-{value}-trigger");
let content_id = format!("blocks-header-flyout-menu-{value}-content");
let mut panel_attrs = vec![("data-blocks-header-flyout-menu-panel", "")];
if let Some(cols) = columns {
panel_attrs.push(("data-blocks-header-flyout-menu-columns", cols));
}
let mut panel_children = items;
if let Some(footer_node) = footer {
panel_children.push(footer_node);
}
navigation_menu::item(
state,
false,
props,
value,
vec![],
vec![
navigation_menu::trigger(
state,
true,
value,
Some(trigger_id.as_str()),
Some(content_id.as_str()),
vec![],
vec![
span(vec![], vec![text(label)]),
navigation_menu::item_indicator(
state,
props,
value,
vec![],
vec![chevron_icon()],
),
],
),
navigation_menu::content(
state,
props,
value,
Some(content_id.as_str()),
Some(trigger_id.as_str()),
panel_attrs,
panel_children,
),
],
)
}
/// トリガーを持たない素のリンク項目(Pricing/Company)。
fn nav_link_item(props: &NavigationMenuProps, value: &str, label: &str, href: &str) -> Node {
navigation_menu::item(
OpenState::Closed,
false,
props,
value,
vec![],
vec![navigation_menu::link(
href,
false,
vec![],
vec![text(label)],
)],
)
}
/// 横並びナビ本体(standard 形、R0981)。
fn main_nav() -> Node {
let props = NavigationMenuProps::default();
navigation_menu::root(
&props,
"Main",
vec![("data-blocks-header-flyout-menu-nav", "")],
vec![navigation_menu::list(
&props,
vec![],
vec![
nav_item_with_flyout(
&props,
"product",
"製品",
OpenState::Open,
flyout_items(PRODUCT_FLYOUT_ITEMS),
Some(panel_footer()),
None,
),
nav_item_with_flyout(
&props,
"resources",
"リソース",
OpenState::Closed,
flyout_items(RESOURCES_FLYOUT_ITEMS),
None,
None,
),
nav_link_item(&props, "pricing", "料金", REPO),
nav_link_item(&props, "company", "会社概要", ORG),
],
)],
)
}
/// アクション行(ログイン + 主 CTA)。無 JS デモのため主 CTA も
/// ハンバーガーと同じ理由(モジュール doc「静的表示」節)で
/// `disabled: true` 固定にし、フォーカス・クリック不能を明示する。
fn actions() -> Node {
div(
vec![("data-blocks-header-flyout-menu-actions", "")],
vec![
link::root(REPO, &LinkProps::default(), vec![], vec![text("ログイン")]),
button::button(
&ButtonProps {
disabled: true,
..ButtonProps::default()
},
vec![("data-blocks-header-flyout-menu-cta", "")],
vec![text("使ってみる")],
),
],
)
}
/// simple 形のアクション行(R0575。ボタン 2 個、どちらも `disabled: true`)。
fn actions_simple() -> Node {
div(
vec![("data-blocks-header-flyout-menu-actions", "")],
vec![
button::button(
&ButtonProps {
variant: ButtonVariant::Outline,
disabled: true,
..ButtonProps::default()
},
vec![("data-blocks-header-flyout-menu-cta-secondary", "")],
vec![text("ログイン")],
),
button::button(
&ButtonProps {
disabled: true,
..ButtonProps::default()
},
vec![("data-blocks-header-flyout-menu-cta", "")],
vec![text("使ってみる")],
),
],
)
}
/// ハンバーガーボタン(狭い幅専用、押しても何も起きないため
/// `disabled: true` 固定。`aria-controls` は開く先を描画しないため付けない)。
fn hamburger() -> Node {
button::icon_button(
&ButtonProps {
variant: ButtonVariant::Ghost,
disabled: true,
..ButtonProps::default()
},
"Open main menu",
vec![("data-blocks-header-flyout-menu-toggle", "")],
vec![hamburger_icon()],
)
}
/// standard 形(R0981 主参照)。
fn standard_header() -> Node {
header(
vec![
("class", "blocks-header-flyout-menu-layout"),
("data-blocks-header-flyout-menu-root", ""),
("data-blocks-header-flyout-menu-variant", "standard"),
],
vec![logo(), main_nav(), actions(), hamburger()],
)
}
/// simple 形(R0575。テキストのみのドロップダウン + ボタン 2 個)。
fn simple_header() -> Node {
let props = NavigationMenuProps::default();
let nav = navigation_menu::root(
&props,
"Main (text only)",
vec![("data-blocks-header-flyout-menu-nav", "")],
vec![navigation_menu::list(
&props,
vec![],
vec![nav_item_with_flyout(
&props,
"services",
"サービス",
OpenState::Open,
SERVICES_TEXT_ITEMS
.iter()
.map(|(label, href)| flyout_text_item(label, href))
.collect(),
None,
None,
)],
)],
);
header(
vec![
("class", "blocks-header-flyout-menu-layout"),
("data-blocks-header-flyout-menu-root", ""),
("data-blocks-header-flyout-menu-variant", "simple"),
],
vec![logo(), nav, actions_simple(), hamburger()],
)
}
/// centered 形(R0577。48rem 以上でナビを中央寄せ、フライアウトは
/// アイコン付き項目を 2 列グリッドで並べ末尾に補助 CTA を持つ)。
fn centered_header() -> Node {
let props = NavigationMenuProps::default();
let nav = navigation_menu::root(
&props,
"Main (centered)",
vec![("data-blocks-header-flyout-menu-nav", "")],
vec![navigation_menu::list(
&props,
vec![],
vec![
nav_item_with_flyout(
&props,
"platform",
"プラットフォーム",
OpenState::Open,
flyout_items(PLATFORM_FLYOUT_ITEMS),
Some(panel_footer()),
Some("2"),
),
nav_link_item(&props, "docs", "ドキュメント", REPO),
nav_link_item(&props, "about", "会社概要", ORG),
],
)],
);
header(
vec![
("class", "blocks-header-flyout-menu-layout"),
("data-blocks-header-flyout-menu-root", ""),
("data-blocks-header-flyout-menu-variant", "centered"),
],
vec![logo(), nav, actions(), hamburger()],
)
}
/// `header-flyout-menu` の Demo 本体。集約元のバリエーション(standard/
/// simple/centered)を縦に並記する(モジュール doc「3 形の並記」節、
/// `footer_inline_nav` 前例と同型)。呼び出しごとに同一の `Node` を返す
/// 純関数。
pub fn demo() -> Node {
div(
vec![("class", "blocks-header-flyout-menu-stack")],
vec![
caption("標準形"),
standard_header(),
caption("テキストのみ"),
simple_header(),
caption("中央寄せ"),
centered_header(),
],
)
}原案差分メモ
- 集約元のバリエーションは 3 形(標準形/テキストのみ形/中央寄せ形)で並記しました。標準形が R0981(主参照)、テキストのみ形が R0575、中央寄せ形が R0577 に対応します。
- R0576/R0988(モバイル drawer トリガー)は Demo に描画していません。狭幅でのハンバーガーボタンがトリガー相当ですが、docs サイトは無 JS のため開いた先の drawer 自体を表現できないためです。
- R0987(2 項目のフライアウト)は、標準形の「リソース」(2 項目・閉状態)と同じ構造であるため、独立したインスタンスにはしていません。
- 押しても何も起きないトリガー・ボタン(各形のフライアウトトリガー・ハンバーガー・アクション行のボタン)はすべて
disabledにして固定し、フォーカス・操作不能であることを明示しています。 - 文言・アイコンはすべて独自に書いた架空のものです。配色・余白・角丸は既存のテーマトークンに従っています。
関連情報: Navigation Menu / Button / Icon / Link