code-block-header
fandhe-frontend-pre-styled-ui の code / clipboard / button / badge / text の 5 部品を合成した、角丸枠の上端にヘッダー帯を持つコードブロックの実例です。Blocks セクションは新規部品を追加するものではなく、既存の Themes/Primitives 部品を組み合わせた実例集であることに注意してください(主参照は対応表 ID R0054、集約元は R0055・R0057。出典の固有名・ファイル名は記載しません)。
ヘッダー帯にファイル名タイトルとコピー操作だけを置いた最小構成(例 A)に加え、折り返し・ダウンロードの補助ボタンを追加した形(例 B)、状態バッジと本文の行番号を追加した形(例 C)の 3 通りを並べています。長い行はページ全体ではなく枠の中で横スクロールします。
本 Demo は静的な表示例であり、<form> 要素は一切持ちません。クリップボードは未コピー(idle)状態の固定表示で、B の補助ボタンは遷移先・クリック処理を持たないため押下不能(disabled)です(実際のコピー動作には fandhe-frontend-wasm-full の JS 配線が必要で、docs サイトは JS ハイドレーションを行いません)。C の行番号は CSS カウンターで振るため DOM にテキストを持たず、選択範囲に含めてもコピーされません。文言はすべて独自に書いた架空のものであり、実企業名・実クレデンシャル・PII を含みません。
Demo
use fandhe_frontend_core::{div, text};
fn demo() -> fandhe_frontend_core::Node {
div(vec![], vec![text("こんにちは")])
}
// この行はとても長いコメントで、枠の幅を越えて横スクロールが発生することを示すためにわざと伸ばしてあります
use fandhe_frontend_core::{div, render, text};
fn demo() -> fandhe_frontend_core::Node {
div(vec![], vec![text("こんにちは")])
}
fn main() {
println!("{}", render(&demo()));
}
use fandhe_frontend_core::{render, text};
fn check() -> bool {
// render() が空文字列を返さなければ true
!render(&text("ok")).is_empty()
}
使用部品
Rust コード
Rust
use fandhe_frontend_core::{div, pre, span, 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::clipboard;
use fandhe_frontend_pre_styled_ui::code::{self, CodeProps};
use fandhe_frontend_pre_styled_ui::recipe::ColorPalette;
use fandhe_frontend_pre_styled_ui::text::{self as styled_text, TextProps, TextSize};
use fandhe_frontend_pre_styled_ui::Size;
/// A(R0054・主参照)で表示するコード片。1 行だけ意図的に長くし、枠内
/// 横スクロールが起きることを示す(モジュール doc「長い行は枠内で横
/// スクロールする」節)。
const SNIPPET_A: &str = "use fandhe_frontend_core::{div, text};\n\nfn demo() -> fandhe_frontend_core::Node {\n div(vec![], vec![text(\"こんにちは\")])\n}\n\n// この行はとても長いコメントで、枠の幅を越えて横スクロールが発生することを示すためにわざと伸ばしてあります\n";
/// B(R0055)で表示するコード片。`render(&demo())` が呼ぶ `demo` 自体を
/// 片内に定義し、コピーしたコード片単体で実行できる自己完結の例にする
/// (Codex レビュー指摘、イシュー #3104 PR #3545)。
const SNIPPET_B: &str = "use fandhe_frontend_core::{div, render, text};\n\nfn demo() -> fandhe_frontend_core::Node {\n div(vec![], vec![text(\"こんにちは\")])\n}\n\nfn main() {\n println!(\"{}\", render(&demo()));\n}\n";
/// C(R0057)で表示するコード片。行番号を 1 行ずつ振るため
/// `.lines()` で分割する。`fandhe_frontend_cli`(`[[bin]]` のみでライブラリ
/// ターゲットを持たない)の `use ... as _;` では import が解決できず、
/// コピーしたコード片単体でコンパイルできなかったため、実在するライブラリ
/// クレート `fandhe_frontend_core` を使った自己完結の例へ置き換える
/// (Codex レビュー指摘、イシュー #3104 PR #3545)。
const SNIPPET_C: &str = "use fandhe_frontend_core::{render, text};\n\nfn check() -> bool {\n // render() が空文字列を返さなければ true\n !render(&text(\"ok\")).is_empty()\n}\n";
/// A(R0054・主参照): ファイル名タイトル + コピー操作だけの最小構成。
fn instance_a() -> Node {
header_instance(
"blocks-code-block-header-a",
SNIPPET_A,
"demo.rs",
None,
false,
false,
)
}
/// B(R0055): A に無効化済みの補助ボタン(折り返し・ダウンロード)を
/// 追加した構成。
fn instance_b() -> Node {
header_instance(
"blocks-code-block-header-b",
SNIPPET_B,
"main.rs",
None,
true,
false,
)
}
/// C(R0057): ヘッダーに状態バッジを追加し、本文の左端へ行番号を振った
/// 構成。
fn instance_c() -> Node {
header_instance(
"blocks-code-block-header-c",
SNIPPET_C,
"check.rs",
Some("成功"),
false,
true,
)
}
/// A/B/C 共通のヘッダー付きコードブロックを組み立てる。
///
/// - `root_id`: 独立マウントルート識別子(モジュール doc「A/B/C を独立
/// マウントルートへ分離する理由」節参照)。
/// - `snippet`: 本文に表示するコード文字列。
/// - `title`: ヘッダー帯に表示するファイル名タイトル。
/// - `status_badge`: `Some(文言)` のときヘッダー帯の左端へ
/// `ColorPalette::Success` の状態バッジを追加する(C 専用)。
/// - `with_aux_buttons`: `true` のときコピー操作の隣へ無効化済みの補助
/// ボタン(折り返し・ダウンロード)を 2 個追加する(B 専用)。
/// - `numbered`: `true` のとき本文の各行を `span` で包み、CSS カウンター
/// で行番号を振る(C 専用、モジュール doc「行番号は CSS カウンターで
/// 振る」節参照)。
fn header_instance(
root_id: &'static str,
snippet: &'static str,
title: &'static str,
status_badge: Option<&'static str>,
with_aux_buttons: bool,
numbered: bool,
) -> Node {
let mut bar_left: Vec<Node> = Vec::new();
if let Some(label) = status_badge {
bar_left.push(badge::badge(
&BadgeProps {
palette: ColorPalette::Success,
..BadgeProps::default()
},
vec![],
vec![text(label)],
));
}
bar_left.push(styled_text::text(
&TextProps {
size: TextSize::Sm,
..TextProps::default()
},
vec![],
vec![text(title)],
));
let mut bar_right: Vec<Node> = vec![clipboard::root(
snippet,
false,
vec![("data-blocks-code-block-header-copy", "")],
vec![clipboard::control(
false,
vec![],
vec![clipboard::trigger(
false,
vec![],
vec![
clipboard::indicator(false, false, vec![], vec![text("コピー")]),
clipboard::indicator(true, false, vec![], vec![text("コピー済み")]),
],
)],
)],
)];
if with_aux_buttons {
bar_right.push(button::button(
&ButtonProps {
variant: ButtonVariant::Ghost,
size: Size::Sm,
// 遷移先・クリック処理を持たない合成例のボタンのため
// `disabled: true` にして「押しても何も起きない」ことを
// 明示する(モジュール doc「静的表示の制約」節、
// `hero_install_command` と同型の判断)。
disabled: true,
..ButtonProps::default()
},
vec![],
vec![text("折り返し")],
));
bar_right.push(button::button(
&ButtonProps {
variant: ButtonVariant::Ghost,
size: Size::Sm,
disabled: true,
..ButtonProps::default()
},
vec![],
vec![text("ダウンロード")],
));
}
// 各行 span は `display: block` を持たず、span 間に挟む改行テキスト
// (`\n`)のみが視覚上の改行源になる(`white-space: pre` が解釈)。
// `display: block` も併用すると改行が二重になり行間が間延びする
// (Codex レビュー・Cursor Bugbot 指摘、イシュー #3104 PR #3545)。
// `.lines()` は末尾の改行を 1 行として数えないため、元のスニペットが
// 改行で終わる場合は最終行の後ろにも改行テキストを 1 つ補い、選択範囲
// コピー結果が `clipboard::root` へ渡す元のスニペット(末尾改行あり)
// と一致するようにする(Codex レビュー指摘、イシュー #3104 PR #3545)。
let body_children: Vec<Node> = if numbered {
let lines: Vec<&str> = snippet.lines().collect();
let line_count = lines.len();
let ends_with_newline = snippet.ends_with('\n');
lines
.into_iter()
.enumerate()
.flat_map(|(i, line)| {
let mut nodes = vec![span(
vec![("data-blocks-code-block-header-line", "")],
vec![text(line)],
)];
if i + 1 < line_count || ends_with_newline {
nodes.push(text("\n"));
}
nodes
})
.collect()
} else {
vec![text(snippet)]
};
let pre_label = format!("{title} のコード");
div(
vec![("id", root_id), ("class", "blocks-code-block-header-frame")],
vec![
div(
vec![("class", "blocks-code-block-header-bar")],
vec![
div(
vec![("data-blocks-code-block-header-bar-start", "")],
bar_left,
),
div(
vec![("data-blocks-code-block-header-bar-end", "")],
bar_right,
),
],
),
pre(
vec![
("class", "blocks-code-block-header-pre"),
("tabindex", "0"),
// `tabindex="0"` でキーボード操作可能にした横スクロール
// 領域には、スクリーンリーダーが読み上げられるアクセ
// シブルネームを明示する(Cursor Bugbot 指摘、イシュー
// #3104 PR #3545)。
("aria-label", pre_label.as_str()),
],
vec![code::code(
&CodeProps::default(),
vec![("data-blocks-code-block-header-code", "")],
body_children,
)],
),
],
)
}
pub fn demo() -> Node {
div(
vec![("class", "blocks-code-block-header-layout")],
vec![instance_a(), instance_b(), instance_c()],
)
}原案差分メモ
- 例 A(主参照 R0054)はファイル名タイトル + コピー操作だけの最小構成を表します。
- 例 B(R0055)は A に補助ボタン(折り返し・ダウンロード)を追加した形で、いずれも実処理を持たない合成例のボタンのため
disabledで押下不能を明示します。 - 例 C(R0057)はヘッダーへ状態バッジを追加し、本文の左端へ行番号を振った形です。行番号は DOM にテキストを置かず CSS の
counter-increment/::before { content: counter(...) }で視覚的に振ることで、選択範囲へ行番号が混ざらないようにしています。 - 参照元の文言・配色・アイコンは持ち込まず、コード片・ファイル名等はすべて独自に作成した架空の値です。