sidebar-03
fandhe-frontend-pre-styled-ui の sidebar(size="lg" のブランド header・menu-sub を入れ子にしたナビゲーション・data-active の現在項目・rail)/ breadcrumb(inset ヘッダー)/ separator / icon を合成した、shadcn/ui Blocks の sidebar-03 に相当する submenu 付きサイドバーの合成例です。Blocks セクションは新規部品を追加するものではなく、既存の Themes/Primitives 部品を組み合わせた実例集であることに注意してください。
docs サイトは JS ハイドレーションを行わないため、開閉操作(トリガーボタン・Cmd/Ctrl+B)は wasm-full 側の実行時責務であり本 SSR 合成例では動作しません。sidebar-03 は既定 collapsible="offcanvas"(icon 折りたたみを持たない)のため、sidebar-07 と異なり expanded インスタンス 1 つのみを掲示します。
本 Demo は静的な表示例であり、<form> 要素を持たず、値の送信・検証・認証処理・データ取得を一切行いません。ナビゲーション項目はすべて href: None(<button type="button">)として組み、死リンク(href="#")は出しません。パンくず・ナビゲーションラベルはすべて一般名詞または架空のものであり、実企業名・実在人物・実クレデンシャル・PII を含みません(docs/policy/ intentional-non-adoption.md §3.25 の責務境界: UI コンポーネント層はアプリケーションロジックを内包しません)。アイコンは lucide 等の著作物ではなく自作の単純な矩形図形です。
Demo
使用部品
Rust コード
use fandhe_frontend_core::{div, el, span, text, Node};
use fandhe_frontend_pre_styled_ui::breadcrumb::{self, BreadcrumbVariant};
use fandhe_frontend_pre_styled_ui::icon::{icon, IconProps};
use fandhe_frontend_pre_styled_ui::separator::{self, SeparatorProps, SeparatorVariant};
use fandhe_frontend_pre_styled_ui::sidebar;
use fandhe_frontend_pre_styled_ui::sidebar::{
Sidebar, SidebarMenuButtonProps, SidebarMenuButtonSize, SidebarMenuSubButtonProps,
SidebarProps, SidebarState,
};
use fandhe_frontend_pre_styled_ui::{Orientation, Size};
/// 自作の単純な矩形アイコン(`d` は呼び出し側が座標を選ぶ、モジュール doc
/// 「アイコンは自作の単純幾何図形」参照)。
fn geo_icon(path_d: &'static str) -> Node {
icon(
&IconProps::default(),
vec![],
vec![el("path", vec![("d", path_d)], vec![])],
)
}
/// header のブランド行(`size="lg"` の `menu_button` 1 個。角丸の濃色
/// アイコン枠 + 「Documentation」/「v1.0.0」の 2 行ラベル。モジュール doc
/// 「Issue 見立てとの差異」参照)。
fn brand_header() -> Node {
let icon_box = div(
vec![("data-blocks-sidebar-03-brand-icon", "")],
vec![geo_icon("M4 4h16v16H4z")],
);
let button = sidebar::menu_button(
&SidebarMenuButtonProps {
href: None,
size: SidebarMenuButtonSize::Lg,
..Default::default()
},
Some(icon_box),
vec![("data-blocks-sidebar-03-brand", "")],
vec![
span(vec![], vec![text("Documentation")]),
span(vec![], vec![text("v1.0.0")]),
],
);
sidebar::header(
vec![],
vec![sidebar::menu(
vec![],
vec![sidebar::menu_item(vec![], vec![button])],
)],
)
}
/// nav の 1 グループ(親見出しの `menu_button` + `menu-sub` の子項目群)。
/// `active` に一致する子項目のみ `data-active` を付与する
/// (モジュール doc「`asChild` 相当が無い点と `href` の扱い」参照)。
fn nav_group(title: &'static str, active: Option<&'static str>, subs: &[&'static str]) -> Node {
let parent = sidebar::menu_button(
&SidebarMenuButtonProps {
href: None,
..Default::default()
},
None,
vec![("data-blocks-sidebar-03-parent", "")],
vec![text(title)],
);
let sub_items: Vec<Node> = subs
.iter()
.map(|label| {
sidebar::menu_sub_item(
vec![],
vec![sidebar::menu_sub_button(
&SidebarMenuSubButtonProps {
href: None,
active: active == Some(*label),
..Default::default()
},
vec![],
vec![text(*label)],
)],
)
})
.collect();
sidebar::menu_item(vec![], vec![parent, sidebar::menu_sub(vec![], sub_items)])
}
/// nav データ(参照実物の 5 グループ・26 項目。他社製品固有名は一般名へ
/// 置換、モジュール doc「ラベルは一般名詞へ置換」参照)。
fn nav_content() -> Node {
let groups = vec![
nav_group(
"Getting Started",
None,
&["Installation", "Project Structure"],
),
nav_group(
"Build Your Application",
Some("Data Fetching"),
&[
"Routing",
"Data Fetching",
"Rendering",
"Caching",
"Styling",
"Optimizing",
"Configuring",
"Testing",
"Authentication",
"Deploying",
"Upgrading",
"Examples",
],
),
nav_group(
"API Reference",
None,
&[
"Components",
"File Conventions",
"Functions",
"Config Options",
"CLI",
"Edge Runtime",
],
),
nav_group(
"Architecture",
None,
&[
"Accessibility",
"Hot Reload",
"Compiler",
"Supported Browsers",
"Bundler",
],
),
nav_group("Community", None, &["Contribution Guide"]),
];
sidebar::content(
vec![],
vec![sidebar::group(
None,
vec![],
vec![sidebar::group_content(
vec![],
vec![sidebar::menu(vec![], groups)],
)],
)],
)
}
/// 左サイドバーの `root`(`sidebar_07::app_sidebar` と同型、`provider` の
/// 直接の子として置く)。
fn app_sidebar(state: &Sidebar, props: &SidebarProps, root_id: &str) -> Node {
sidebar::root(
state,
props,
"Main navigation",
Some(root_id),
vec![],
vec![
brand_header(),
nav_content(),
sidebar::rail(state, "Toggle sidebar rail", vec![], vec![]),
],
)
}
/// inset 側(トリガー + 縦 separator + breadcrumb のヘッダー、3 枚の
/// プレースホルダーカード、大きなプレースホルダー 1 枚。
/// `sidebar_07::inset_area` と同型)。
fn inset_area(state: &Sidebar, root_id: &str) -> Node {
let header = div(
vec![("data-blocks-sidebar-03-header", "")],
vec![
sidebar::trigger(state, "Toggle sidebar", Some(root_id), vec![], vec![]),
separator::separator(
&SeparatorProps {
orientation: Orientation::Vertical,
variant: SeparatorVariant::Solid,
},
vec![],
),
breadcrumb::root(
Size::Md,
BreadcrumbVariant::default(),
Some("Breadcrumb"),
vec![],
vec![breadcrumb::list(
vec![],
vec![
breadcrumb::item(
vec![],
vec![breadcrumb::link(
"../",
vec![],
vec![text("Build Your Application")],
)],
),
breadcrumb::separator(vec![], vec![text("/")]),
breadcrumb::item(
vec![],
vec![breadcrumb::current_link(
vec![],
vec![text("Data Fetching")],
)],
),
],
)],
),
],
);
let grid = div(
vec![("data-blocks-sidebar-03-grid", "")],
vec![
div(vec![("data-blocks-sidebar-03-placeholder", "")], vec![]),
div(vec![("data-blocks-sidebar-03-placeholder", "")], vec![]),
div(vec![("data-blocks-sidebar-03-placeholder", "")], vec![]),
],
);
let large_placeholder = div(vec![("data-blocks-sidebar-03-placeholder-lg", "")], vec![]);
sidebar::inset(vec![], vec![header, grid, large_placeholder])
}
/// `sidebar-03` の Demo 本体。呼び出しごとに同一の `Node` を返す純関数
/// (モジュール doc「無 JS のため静的 1 インスタンス」参照。既定
/// `collapsible: Offcanvas` のまま `Expanded` の単一インスタンスを描画)。
pub fn demo() -> Node {
let state = Sidebar::new(SidebarState::Expanded);
let props = SidebarProps::default();
let root_id = "blocks-sidebar-03-root";
sidebar::provider(
&state,
&props,
vec![("data-blocks-sidebar-03-instance", "")],
vec![
app_sidebar(&state, &props, root_id),
inset_area(&state, root_id),
],
)
}shadcn 側との差分メモ
- Issue 見立てでは header に検索
inputを含む構成が想定されていましたが、参照実物(shadcnsidebar-03のapp-sidebar.tsx/page.tsx)・スクリーンショットのいずれにも検索inputや version-switcher のドロップダウンは存在せず、header はsize="lg"のmenu_button1 個(アイコン + 「Documentation」/「v1.0.0」の 2 行ラベル)のみです。受け入れ条件がスクショ比較であるため、本実装は参照実物に忠実な構成(input無し)を採りました。sidebar::input自体は/themes/sidebar/ページで別途掲示済みのため、部品の不足を意味しません。 - ナビゲーション項目(親見出し・submenu 項目)はすべて
href: None(<button type="button">)で組んでいます。href="#"の死リンクはcrates/docs-site/tests/blocks_contract.rsが禁止しており、<a>にするとaria-current="page"が付与されますが<button>では付与されません。現在項目の視覚状態はhrefに依らないdata-activeで表現しています。 - 参照実物のナビゲーションラベルに含まれる他社製品固有の名称(「next.config.js Options」「Next.js Compiler」「Turbopack」「Fast Refresh」)は、他社製品名を持ち込まない方針に従いそれぞれ「Config Options」「Compiler」「Bundler」「Hot Reload」の一般名へ置換しています。
collapsible/menu/avatarは使いません(sidebar-07との違い)。sidebar-03は team switcher・user footer のドロップダウンや icon 折りたたみを持たない、より単純な構成です。- アイコンは lucide ではなく自作の単純な矩形 SVG path です(著作物を複製しないため)。
- inset 側のプレースホルダー領域は素の
divです(実データ取得は行いません)。
関連情報: Sidebar / Breadcrumb / Separator / Icon