form-layout-property-panel
fandhe-frontend-pre-styled-ui の fieldset / field / number-input / native-select / select / color-picker / color-swatch / segment-group / collapsible / tooltip / card / breadcrumb / clipboard / separator の 14 部品を合成した、デザインツール風プロパティパネルの実例です。Blocks セクションは新規部品を追加するものではなく、既存の Themes 部品を組み合わせた実例集であることに注意してください(集約元は対応表 ID R0225〜R0228 の 4 件。出典の固有名・ファイル名は記載しません)。
4 つの版(A〜D)を並記しています。
- A(基本): 幅の狭い縦長パネルに「位置」「レイアウト」「文字」の 3 節を区切り線(
separator)で分けて縦に積みます。各節は 2 列グリッドの行にラベル付き数値入力・選択・切替を詰めて配置し、狭い画面幅でもパネル幅を保ちます。 - B(テーマ選択 + 開閉式の節): 先頭にテーマ選択、続けて開閉式の節(
collapsible)を 2 つ持ちます。「塗り」は開いた状態(color_pickerの閉じた trigger + 不透明度)、「枠線」は閉じた状態(太さ + 色見本)で、開閉 2 状態を並べて示します。 - C(カード枠 + パンくずのヘッダー):
cardで包み、ヘッダーにbreadcrumbとタイトルを、本文にフォント設定を、フッターに CSS 宣言をコピーするclipboardを置きます。行間ラベルの横には常時表示のtooltipを添えます。 - D(極小サイズの狭幅パネル): 入力に
Size::Xsを使う、より狭いパネルです。位置節の X/Y/幅/高さと単位選択のみを持ちます。
本 Demo は静的な表示例であり、<form> 要素は一切持たず、データの取得・送信・状態管理を行いません。number_input の増減ボタンと入力本体は静的固定のため readonly(aria-valuenow は初期値のまま更新されないため、表示値とのずれを防ぐ目的です)、segment_group/collapsible の trigger は開閉/切替が JS ハイドレーション前提のためネイティブ disabled で操作不能にし、現在値は固定表示にしています(単位・配置・太さ・テーマ・フォントのネイティブ <select> は JS なしでも実際に動作するため disabled を付けず、可視ラベル付きにしています)。tooltip/clipboard の trigger は開閉・コピー操作が JS ハイドレーション前提のため(無 JS の docs サイトでは押しても機能しません)ネイティブ disabled で操作不能にし、tooltip は説明を常時表示、clipboard は idle 表示のまま固定しています。パンくずの途中項目は遷移先を持ちません(末尾のみ現在ページを示します)。文言はすべて独自に書いた架空のものであり、実企業名・実クレデンシャル・PII を含みません。
Demo
基本(位置・レイアウト・文字)
テーマ選択 + 開閉式の節(R0226)
カード枠 + パンくず(R0227)
フォント設定
極小サイズの狭幅パネル(R0228)
使用部品
- Field
- Fieldset
- Number Input
- Native Select
- Select
- Color Picker
- Color Swatch
- Segment Group
- Collapsible
- Tooltip
- Card
- Breadcrumb
- Clipboard
- Separator
Rust コード
use fandhe_frontend_core::{div, text, Node};
use fandhe_frontend_pre_styled_ui::breadcrumb::{self, BreadcrumbVariant};
use fandhe_frontend_pre_styled_ui::card::{self, CardProps};
use fandhe_frontend_pre_styled_ui::clipboard;
use fandhe_frontend_pre_styled_ui::collapsible;
use fandhe_frontend_pre_styled_ui::color_picker::{self, ColorPickerProps};
use fandhe_frontend_pre_styled_ui::color_swatch::{self, Color, ColorSwatchProps, Rgb};
use fandhe_frontend_pre_styled_ui::fandhe_frontend_headless_ui::color_picker::ColorPicker;
use fandhe_frontend_pre_styled_ui::field::{self, FieldIds, FieldProps, FieldRootProps};
use fandhe_frontend_pre_styled_ui::fieldset::{self, FieldsetProps, FieldsetRootProps};
use fandhe_frontend_pre_styled_ui::input::{self, InputProps};
use fandhe_frontend_pre_styled_ui::native_select::{self, NativeSelectProps};
use fandhe_frontend_pre_styled_ui::number_input::{self, NumberInputFlags};
use fandhe_frontend_pre_styled_ui::segment_group::{self, SegmentGroupProps};
use fandhe_frontend_pre_styled_ui::select::{self, OpenState, SelectProps};
use fandhe_frontend_pre_styled_ui::separator::{self, SeparatorProps};
use fandhe_frontend_pre_styled_ui::text::{self as styled_text, TextProps, TextSize, TextVariant};
use fandhe_frontend_pre_styled_ui::tooltip;
use fandhe_frontend_pre_styled_ui::visually_hidden;
use fandhe_frontend_pre_styled_ui::Size;
const FONT_LABEL_ID: &str = "blocks-form-layout-property-panel-font-label";
const FONT_CONTENT_ID: &str = "blocks-form-layout-property-panel-font-content";
const COLOR_HEX_ID: &str = "blocks-form-layout-property-panel-color-hex";
const DIRECTION_LABEL_ID: &str = "blocks-form-layout-property-panel-direction-label";
const TEXT_ALIGN_LABEL_ID: &str = "blocks-form-layout-property-panel-text-align-label";
// --- B(R0226: テーマ選択 + 開閉式の節 2 つ)で使う id 群。他版と衝突
// しない `-b-` 接頭辞で分離する(`blocks_contract` の id 重複検査対応)。
const B_FILL_CONTENT_ID: &str = "blocks-form-layout-property-panel-b-fill-content";
const B_BORDER_CONTENT_ID: &str = "blocks-form-layout-property-panel-b-border-content";
const B_COLOR_LABEL_ID: &str = "blocks-form-layout-property-panel-b-color-label";
// --- C(R0227: カード枠 + パンくずのヘッダー)で使う id 群。
const C_TOOLTIP_CONTENT_ID: &str = "blocks-form-layout-property-panel-c-tooltip-content";
const C_CLIP_LABEL_ID: &str = "blocks-form-layout-property-panel-c-clip-label";
const C_CLIP_INPUT_ID: &str = "blocks-form-layout-property-panel-c-clip-input";
/// コピー対象の CSS 宣言(C 版の `card::footer` clipboard)。無害な
/// フォント指定 1 行のみで、実在のトークン・秘密情報を含まない。
const C_FONT_CSS_DECLARATION: &str = "font: 700 20px/1.4 \"Noto Sans JP\", sans-serif;";
/// 節見出し 1 節分の `FieldsetProps`(`legend`/`helper-text` 等の id 生成に
/// 使う base id のみ渡す。本 Demo は helper-text/error-text を出さない)。
fn fieldset_props(id: &'static str) -> FieldsetProps<'static> {
FieldsetProps {
id,
disabled: false,
invalid: false,
has_helper_text: false,
}
}
/// 数値入力 1 行分(`number_input`。増減ボタンは静的固定のため
/// `disabled: true`、モジュール doc「静的表示」節)。`id` はラベルの
/// `for`/コントロールの紐付けに使う。`min`/`max` は欄ごとの実際の許容範囲
/// を `aria-valuemin`/`aria-valuemax` へそのまま反映する(X/Y 座標・回転角
/// のように負値を取り得る欄と、幅・高さ・間隔のように非負のみの欄とで
/// 支援技術へ伝える範囲を区別する)。`size` は D 版(狭幅パネル)が
/// `Size::Xs` を渡せるよう #2914 で引数化した(A〜C は従来どおり
/// `Size::Sm`、イシュー #2914)。
fn number_field(
id: &'static str,
label_text: &'static str,
value: &'static str,
min: &'static str,
max: &'static str,
size: Size,
) -> Node {
let flags = NumberInputFlags {
readonly: true,
..NumberInputFlags::default()
};
number_input::root(
size,
false,
false,
true,
vec![],
vec![
number_input::label(flags, Some(id), vec![], vec![text(label_text)]),
number_input::control(
flags,
vec![],
vec![
number_input::decrement_trigger(Some(id), true, vec![], vec![text("-")]),
number_input::input(id, Some(id), Some(value), min, max, flags, vec![]),
number_input::increment_trigger(Some(id), true, vec![], vec![text("+")]),
],
),
],
)
}
/// 単位・その他の選択欄(ネイティブ `<select>`。JS なしでも実際に動作
/// するため静的固定は行わない、モジュール doc「静的表示」節)。
/// `field::root` + `field::label` + [`native_select::native_select`] の
/// 構成で可視ラベルを持たせる(`label_text` がそのままラベル文言になる。
/// `aria-label` だけだと隣のサイズ欄〔ラベルが上にある縦並び〕とグリッドの
/// 行内で縦位置がずれるため、`form_layout_two_column` の「国・地域」欄と
/// 同型の構成へ揃えた、イシュー #2913 コードレビューで是正)。
///
/// `note`(`Some` のとき `field::helper_text` を添える)は、選択操作自体が
/// 実際に効く(`native_select` は無 JS でも開閉・選択できる)が選択結果を
/// 反映する対象がこの静的デモには無い欄向け。B 版のテーマ選択がこれに
/// 該当し、選んでも配下のパネル配色は変わらない(無 JS デモのため)ことを
/// 明示して誤解を防ぐ(イシュー #2914 コードレビュー是正、P2)。他の呼び
/// 出し(単位・配置・フォント)は選択自体が示す意味が完結するため `None`。
fn unit_select(
id: &'static str,
label_text: &'static str,
options: &[(&'static str, &'static str)],
size: Size,
note: Option<&'static str>,
) -> Node {
let field_props = FieldProps {
id,
ids: FieldIds::default(),
disabled: false,
invalid: false,
required: false,
readonly: false,
has_helper_text: note.is_some(),
};
let option_nodes: Vec<Node> = options
.iter()
.map(|(value, label)| {
fandhe_frontend_core::el("option", vec![("value", value)], vec![text(*label)])
})
.collect();
let mut children = vec![
field::label(&field_props, vec![], vec![text(label_text)]),
native_select::native_select(
&NativeSelectProps {
size,
..NativeSelectProps::default()
},
&field_props,
vec![],
option_nodes,
),
];
if let Some(note_text) = note {
children.push(field::helper_text(
&field_props,
vec![],
vec![text(note_text)],
));
}
field::root(&FieldRootProps::default(), &field_props, vec![], children)
}
/// 「位置」節。X/Y/幅/高さ(数値 4 件・単位共有)+ 回転(数値 + `deg` 平文)。
fn position_section() -> Node {
let props = fieldset_props("blocks-form-layout-property-panel-position");
fieldset::root(
&FieldsetRootProps { size: Size::Sm },
&props,
vec![],
vec![
fieldset::legend(&props, vec![], vec![text("位置")]),
div(
vec![("class", "blocks-form-layout-property-panel-grid")],
vec![
number_field(
"blocks-form-layout-property-panel-pos-x",
"X",
"120",
"-9999",
"9999",
Size::Sm,
),
number_field(
"blocks-form-layout-property-panel-pos-y",
"Y",
"48",
"-9999",
"9999",
Size::Sm,
),
number_field(
"blocks-form-layout-property-panel-pos-w",
"幅",
"320",
"0",
"9999",
Size::Sm,
),
number_field(
"blocks-form-layout-property-panel-pos-h",
"高さ",
"180",
"0",
"9999",
Size::Sm,
),
div(
vec![("class", "blocks-form-layout-property-panel-grid-span-2")],
vec![unit_select(
"blocks-form-layout-property-panel-pos-unit",
"位置・サイズの単位",
&[("px", "px"), ("percent", "%"), ("rem", "rem")],
Size::Sm,
None,
)],
),
div(
vec![("class", "blocks-form-layout-property-panel-grid-span-2")],
vec![div(
vec![("class", "blocks-form-layout-property-panel-rotation")],
vec![
number_field(
"blocks-form-layout-property-panel-rotation",
"回転",
"0",
"-360",
"360",
Size::Sm,
),
styled_text::text(
&TextProps {
size: TextSize::Xs,
variant: TextVariant::Muted,
..TextProps::default()
},
vec![],
vec![text("deg")],
),
],
)],
),
],
),
],
)
}
/// 「レイアウト」節。方向(segment group、横を選択して固定)+ 間隔(数値)
/// + 配置(ネイティブ select)。
fn layout_section() -> Node {
let props = fieldset_props("blocks-form-layout-property-panel-layout-fieldset");
let direction_props = SegmentGroupProps::default();
// ネイティブ操作を構造的に禁止するため item 系パーツ全体を disabled
// 扱いにする(モジュール doc「静的表示」節)。`item`/`item_control`/
// `item_text` も `item_hidden_input` と揃えて disabled: true を渡し、
// 3 パーツすべてに `data-disabled` を反映させる(disabled_direction_props
// を経由しないと data-disabled が `item_hidden_input` にしか付かず、
// LAYOUT_CSS の中和セレクタが空振りするため)。
let disabled_direction_props = SegmentGroupProps {
disabled: true,
..direction_props
};
fieldset::root(
&FieldsetRootProps { size: Size::Sm },
&props,
vec![],
vec![
fieldset::legend(&props, vec![], vec![text("レイアウト")]),
div(
vec![("class", "blocks-form-layout-property-panel-grid")],
vec![
div(
vec![("class", "blocks-form-layout-property-panel-grid-span-2")],
vec![
styled_text::text(
&TextProps {
size: TextSize::Sm,
..TextProps::default()
},
vec![("id", DIRECTION_LABEL_ID)],
vec![text("方向")],
),
segment_group::root_with_props(
Size::Sm,
&direction_props,
None,
Some(DIRECTION_LABEL_ID),
vec![],
vec![
segment_group::indicator(
Some((0, 2)),
&disabled_direction_props,
None,
vec![],
),
segment_group::item(
true,
&disabled_direction_props,
"row",
vec![],
vec![
segment_group::item_hidden_input(
true,
&disabled_direction_props,
Some("blocks-form-layout-property-panel-direction"),
"row",
vec![],
),
segment_group::item_control(
true,
&disabled_direction_props,
vec![],
),
segment_group::item_text(
true,
&disabled_direction_props,
vec![],
vec![text("横")],
),
],
),
segment_group::item(
false,
&disabled_direction_props,
"column",
vec![],
vec![
segment_group::item_hidden_input(
false,
&disabled_direction_props,
Some("blocks-form-layout-property-panel-direction"),
"column",
vec![],
),
segment_group::item_control(
false,
&disabled_direction_props,
vec![],
),
segment_group::item_text(
false,
&disabled_direction_props,
vec![],
vec![text("縦")],
),
],
),
],
),
],
),
number_field(
"blocks-form-layout-property-panel-gap",
"間隔",
"16",
"0",
"9999",
Size::Sm,
),
div(
vec![("class", "blocks-form-layout-property-panel-grid-span-2")],
vec![unit_select(
"blocks-form-layout-property-panel-align",
"配置",
&[
("start", "先頭揃え"),
("center", "中央揃え"),
("end", "末尾揃え"),
("stretch", "引き伸ばし"),
],
Size::Sm,
None,
)],
),
],
),
],
)
}
/// 「文字」節。フォント(styled select・閉じた固定表示)+ サイズ(数値)+
/// 太さ(ネイティブ select)+ 揃え(segment group)+ 色(swatch + hex 表示)。
fn text_section() -> Node {
let props = fieldset_props("blocks-form-layout-property-panel-text-fieldset");
let align_props = SegmentGroupProps::default();
// 隣接する color_swatch は固定値 #1a1a1a のまま更新されない静的 Demo
// のため、input を編集可能に見せると値と色見本がずれる(イシュー #3355
// コードレビューで是正、モジュール doc「静的表示」節と同じ判断)。
let color_id_props = FieldProps {
id: COLOR_HEX_ID,
ids: FieldIds::default(),
disabled: false,
invalid: false,
required: false,
readonly: true,
has_helper_text: false,
};
fieldset::root(
&FieldsetRootProps { size: Size::Sm },
&props,
vec![],
vec![
fieldset::legend(&props, vec![], vec![text("文字")]),
div(
vec![("class", "blocks-form-layout-property-panel-grid")],
vec![
div(
vec![("class", "blocks-form-layout-property-panel-grid-span-2")],
vec![closed_font_select()],
),
number_field(
"blocks-form-layout-property-panel-font-size",
"サイズ",
"16",
"0",
"9999",
Size::Sm,
),
unit_select(
"blocks-form-layout-property-panel-font-weight",
"太さ",
&[("400", "標準"), ("700", "太字"), ("300", "細字")],
Size::Sm,
None,
),
div(
vec![("class", "blocks-form-layout-property-panel-grid-span-2")],
vec![
styled_text::text(
&TextProps {
size: TextSize::Sm,
..TextProps::default()
},
vec![("id", TEXT_ALIGN_LABEL_ID)],
vec![text("揃え")],
),
segment_group::root_with_props(
Size::Sm,
&align_props,
None,
Some(TEXT_ALIGN_LABEL_ID),
vec![],
vec![
segment_group::indicator(
Some((0, 3)),
&SegmentGroupProps {
disabled: true,
..SegmentGroupProps::default()
},
None,
vec![],
),
align_item(true, "left", "左"),
align_item(false, "center", "中央"),
align_item(false, "right", "右"),
],
),
],
),
div(
vec![("class", "blocks-form-layout-property-panel-grid-span-2")],
vec![div(
vec![("class", "blocks-form-layout-property-panel-color")],
vec![
color_swatch::color_swatch(
&ColorSwatchProps {
value: Color::from_rgb(Rgb::new(0x1a, 0x1a, 0x1a)),
size: Size::Sm,
..ColorSwatchProps::default()
},
vec![("aria-hidden", "true")],
vec![],
),
field::root(
&FieldRootProps::default(),
&color_id_props,
vec![("data-blocks-form-layout-property-panel-color", "")],
vec![
field::label(&color_id_props, vec![], vec![text("文字色")]),
input::input(
&InputProps {
size: Size::Sm,
..InputProps::default()
},
&color_id_props,
vec![
("type", "text"),
("value", "#1a1a1a"),
("inputmode", "text"),
],
),
],
),
],
)],
),
],
),
],
)
}
/// 「揃え」segment group の項目 1 件。
fn align_item(checked: bool, value: &'static str, label_text: &'static str) -> Node {
// レイアウト節の direction 同様、item/item_control/item_text も
// disabled_props で揃え、3 パーツすべてに `data-disabled` を反映させる
// (item_hidden_input のみ disabled だと LAYOUT_CSS の中和セレクタが
// 空振りする、モジュール doc「静的表示」節参照)。
let disabled_props = SegmentGroupProps {
disabled: true,
..SegmentGroupProps::default()
};
segment_group::item(
checked,
&disabled_props,
value,
vec![],
vec![
segment_group::item_hidden_input(
checked,
&disabled_props,
Some("blocks-form-layout-property-panel-text-align"),
value,
vec![],
),
segment_group::item_control(checked, &disabled_props, vec![]),
segment_group::item_text(checked, &disabled_props, vec![], vec![text(label_text)]),
],
)
}
/// 閉じた状態で固定した styled select 1 件(フォント欄。`card_form_footer`
/// の `closed_select` と同一方針、モジュール doc「静的表示」節参照)。
fn closed_font_select() -> Node {
let select_props = SelectProps {
disabled: true,
..SelectProps::default()
};
let option_label = "Noto Sans JP";
div(
vec![("class", "blocks-form-layout-property-panel-select")],
vec![
select::label(
&select_props,
Some(FONT_LABEL_ID),
vec![],
vec![text("フォント")],
),
select::root(
Size::Sm,
OpenState::Closed,
&select_props,
vec![],
vec![
select::control(
OpenState::Closed,
&select_props,
vec![],
vec![select::trigger(
OpenState::Closed,
&select_props,
false,
Some(FONT_CONTENT_ID),
Some(FONT_LABEL_ID),
vec![],
vec![
select::value_text(
false,
&select_props,
vec![],
vec![text(option_label)],
),
select::indicator(OpenState::Closed, &select_props, vec![], vec![]),
],
)],
),
select::positioner(
OpenState::Closed,
vec![],
vec![select::content(
OpenState::Closed,
Some(FONT_CONTENT_ID),
Some(FONT_LABEL_ID),
None,
vec![],
vec![select::item(
OpenState::Open,
&select_props,
false,
false,
"noto-sans-jp",
None,
vec![],
vec![select::item_text(
OpenState::Open,
&select_props,
false,
false,
None,
vec![],
vec![text(option_label)],
)],
)],
)],
),
],
),
],
)
}
/// 版見出し(A〜D の先頭に置く短いキャプション)。
fn version_caption(label_text: &'static str) -> Node {
styled_text::text(
&TextProps {
size: TextSize::Xs,
variant: TextVariant::Muted,
..TextProps::default()
},
vec![],
vec![text(label_text)],
)
}
/// A(R0225 基準形): 骨格(狭い縦長パネル・節の区切り・行の 2 列配置)と
/// 位置/レイアウト/文字の主要 3 節。#2913 で実装済みの中身は変えず、
/// `.blocks-form-layout-property-panel-panel` へ外枠クラスを移した
/// (#2914、モジュール doc「集約元と版の対応」節参照)。
fn panel_basic() -> Node {
div(
vec![("class", "blocks-form-layout-property-panel-panel")],
vec![
version_caption("基本(位置・レイアウト・文字)"),
position_section(),
separator::separator(&SeparatorProps::default(), vec![]),
layout_section(),
separator::separator(&SeparatorProps::default(), vec![]),
text_section(),
],
)
}
/// B(R0226): テーマ選択(先頭)+ 開閉式の節 2 つ(「塗り」= open・
/// 「枠線」= closed)。`collapsible::trigger`/`content` は
/// `app_shell_stacked_overlap::hamburger`/`mobile_panel` と同じ判断で
/// `disabled: true` 固定(無 JS の docs サイトでは押しても何も起きない
/// ため、モジュール doc「静的表示(無 JS)の扱い」節参照)。
fn panel_theme_collapsible() -> Node {
let fill_color = ColorPicker::from_color(Color::from_rgb(Rgb::new(0x22, 0x63, 0xeb)));
let readonly_color_props = ColorPickerProps {
readonly: true,
..ColorPickerProps::default()
};
// `trigger` のみ `disabled: true` も重ねる(`readonly` は
// ネイティブ `disabled` 属性を出さず操作可能に見えてしまうため、
// `number_field` の増減ボタンと同じ判断。イシュー #2914 コード
// レビュー是正、P1)。
let disabled_trigger_color_props = ColorPickerProps {
readonly: true,
disabled: true,
..ColorPickerProps::default()
};
let fill_section = collapsible::content(
collapsible::OpenState::Open,
true,
Some(B_FILL_CONTENT_ID),
vec![("data-blocks-form-layout-property-panel-b-section", "")],
vec![
color_picker::root(
&fill_color,
&readonly_color_props,
vec![],
vec![
color_picker::label(
&readonly_color_props,
vec![("id", B_COLOR_LABEL_ID)],
vec![text("塗り色")],
),
color_picker::control(
OpenState::Closed,
&readonly_color_props,
vec![],
vec![
color_picker::channel_input(
fill_color.hex().as_str(),
&readonly_color_props,
vec![("aria-labelledby", B_COLOR_LABEL_ID)],
),
color_picker::trigger(
&fill_color,
&disabled_trigger_color_props,
None,
vec![("aria-label", "塗り色を選ぶ")],
vec![],
),
],
),
],
),
number_field(
"blocks-form-layout-property-panel-b-opacity",
"不透明度",
"100",
"0",
"100",
Size::Sm,
),
],
);
let border_section = collapsible::content(
collapsible::OpenState::Closed,
true,
Some(B_BORDER_CONTENT_ID),
vec![("data-blocks-form-layout-property-panel-b-section", "")],
vec![
number_field(
"blocks-form-layout-property-panel-b-border-width",
"太さ",
"1",
"0",
"9999",
Size::Sm,
),
color_swatch::color_swatch(
&ColorSwatchProps {
value: Color::from_rgb(Rgb::new(0xd1, 0xd5, 0xdb)),
size: Size::Sm,
..ColorSwatchProps::default()
},
vec![("aria-label", "枠線の色")],
vec![],
),
],
);
div(
vec![("class", "blocks-form-layout-property-panel-panel")],
vec![
version_caption("テーマ選択 + 開閉式の節(R0226)"),
unit_select(
"blocks-form-layout-property-panel-b-theme",
"テーマ",
&[
("standard", "標準"),
("dark", "ダーク"),
("high-contrast", "ハイコントラスト"),
],
Size::Sm,
Some("選択操作自体は無 JS でも行えますが、この静的デモでは配下パネルの配色は変わりません。"),
),
separator::separator(&SeparatorProps::default(), vec![]),
collapsible::root(
collapsible::OpenState::Open,
true,
vec![],
vec![
collapsible::trigger(
collapsible::OpenState::Open,
true,
Some(B_FILL_CONTENT_ID),
vec![],
vec![
text("塗り"),
collapsible::indicator(
collapsible::OpenState::Open,
true,
vec![],
vec![],
),
],
),
fill_section,
],
),
collapsible::root(
collapsible::OpenState::Closed,
true,
vec![],
vec![
collapsible::trigger(
collapsible::OpenState::Closed,
true,
Some(B_BORDER_CONTENT_ID),
vec![],
vec![
text("枠線"),
collapsible::indicator(
collapsible::OpenState::Closed,
true,
vec![],
vec![],
),
],
),
border_section,
],
),
],
)
}
/// C(R0227): カード枠 + パンくずのヘッダーで包んだフォント設定。
fn panel_card_font() -> Node {
let breadcrumb_nav = breadcrumb::root(
Size::Sm,
BreadcrumbVariant::Plain,
Some("プロパティの階層"),
vec![],
vec![breadcrumb::list(
vec![],
vec![
breadcrumb::item(vec![], vec![text("レイヤー")]),
breadcrumb::separator(vec![], vec![text("/")]),
breadcrumb::item(vec![], vec![text("テキスト")]),
breadcrumb::separator(vec![], vec![text("/")]),
breadcrumb::item(
vec![],
vec![breadcrumb::current_link(vec![], vec![text("フォント")])],
),
],
)],
);
// `number_field` ヘルパーは呼び出し側で属性を追加できないため、行間欄
// だけは `number_input` パーツを直接組み立てる。ヘルパーへ describedby
// 引数を足して全 15 呼び出し箇所を書き換えるより、この 1 箇所だけ
// 展開するほうが差分が小さい。tooltip の説明文(`C_TOOLTIP_CONTENT_ID`)
// を input へ `aria-describedby` で関連付ける(disabled な
// `tooltip::trigger` にしか説明が結び付いておらず、行間欄へフォーカス
// 移動した支援技術利用者に説明が伝わらなかったため、イシュー #2914
// コードレビュー是正、P2)。
let line_height_id = "blocks-form-layout-property-panel-c-line-height";
let line_height_flags = NumberInputFlags {
readonly: true,
..NumberInputFlags::default()
};
let line_height_field = number_input::root(
Size::Sm,
false,
false,
true,
vec![],
vec![
number_input::label(
line_height_flags,
Some(line_height_id),
vec![],
vec![text("行間")],
),
number_input::control(
line_height_flags,
vec![],
vec![
number_input::decrement_trigger(
Some(line_height_id),
true,
vec![],
vec![text("-")],
),
number_input::input(
line_height_id,
Some(line_height_id),
Some("1.4"),
"0",
"9999",
line_height_flags,
vec![("aria-describedby", C_TOOLTIP_CONTENT_ID)],
),
number_input::increment_trigger(
Some(line_height_id),
true,
vec![],
vec![text("+")],
),
],
),
],
);
let line_height_row = div(
vec![("class", "blocks-form-layout-property-panel-c-tooltip-row")],
vec![
line_height_field,
tooltip::root(
OpenState::Open,
vec![],
vec![
// trigger は `disabled: true` 固定 + 常時 open で出す。無 JS の
// docs サイトでは押しても開閉できず、closed のまま出すと
// 操作可能なボタンに見えて実際には機能しない見た目になるため
// (イシュー #2914 コードレビュー是正、P2)。説明を常時表示
// することで代替する(レビュー指摘の代替案)。
tooltip::trigger(
OpenState::Open,
true,
Some(C_TOOLTIP_CONTENT_ID),
vec![("aria-label", "行間についてのヘルプ")],
vec![text("?")],
),
tooltip::positioner(
OpenState::Open,
vec![],
vec![tooltip::content(
OpenState::Open,
Some(C_TOOLTIP_CONTENT_ID),
vec![],
vec![text("フォントサイズに対する行の高さの倍率です。")],
)],
),
],
),
],
);
div(
vec![("class", "blocks-form-layout-property-panel-panel")],
vec![
version_caption("カード枠 + パンくず(R0227)"),
card::root(
CardProps {
size: Size::Sm,
..CardProps::default()
},
vec![],
vec![
card::header(
vec![],
vec![
breadcrumb_nav,
card::title(vec![], vec![text("フォント設定")]),
],
),
card::body(
vec![],
vecと
// 一致する 1 件のみにする。無 JS の docs サイトでは選択を
// 変えても footer の clipboard コピー内容は更新されないため、
// 選べる値を 1 つに固定して表示/コピー内容の不一致を構造的に
// 防ぐ(イシュー #2914 コードレビュー是正、P1)。
unit_select(
"blocks-form-layout-property-panel-c-font",
"フォント",
&[("noto-sans-jp", "Noto Sans JP")],
Size::Sm,
None,
),
number_field(
"blocks-form-layout-property-panel-c-size",
"サイズ",
"20",
"0",
"9999",
Size::Sm,
),
line_height_row,
],
),
card::footer(
vec![],
vec![clipboard::root(
C_FONT_CSS_DECLARATION,
false,
vec![],
vec![
visually_hidden::root(
vec![],
vec![clipboard::label(
false,
Some(C_CLIP_INPUT_ID),
vec![("id", C_CLIP_LABEL_ID)],
vec![text("CSS 宣言")],
)],
),
clipboard::control(
false,
vec![],
vec![
// `value_text`(span)ではなく `input` を使う。前者は
// `id` を持てても `label::for` の関連付け先になれない
// (`for` はフォームコントロールのみを指す)ため、
// 隣接する `label` の `for` が宙に浮いていた
// (イシュー #2914 コードレビュー是正、Bugbot 指摘)。
// A 版(`hero_install_command`)と同じ
// `clipboard::input` パターンへ揃える。
clipboard::input(
C_FONT_CSS_DECLARATION,
false,
vec![("id", C_CLIP_INPUT_ID)],
),
// headless `clipboard::trigger` は disabled 引数を
// 持たないため、ネイティブ `disabled` 属性を
// `attrs` 経由で直接付与する(`gallery_carousel`
// の `indicator` と同じ手段。減光 CSS は
// [`LAYOUT_CSS`] のブロック固有セレクタが担う、
// イシュー #2914 コードレビュー是正、P1)。
clipboard::trigger(
false,
vec![
("disabled", ""),
("data-blocks-form-layout-property-panel-c-clipboard-trigger", ""),
],
vec![
clipboard::indicator(
false,
false,
vec![],
vec![text("Copy")],
),
clipboard::indicator(
true,
false,
vec![],
vec![text("Copied!")],
),
],
),
],
),
],
)],
),
],
),
],
)
}
/// D(R0228): 極小サイズの入力を並べた狭幅パネル。「位置」の X/Y/幅/高さ +
/// 単位選択のみを持つ(`fieldset` は Xs 段を登録していないため
/// `Size::Sm` のまま、モジュール doc「使用部品」節参照)。
fn panel_compact() -> Node {
let props = fieldset_props("blocks-form-layout-property-panel-d-position");
div(
vec![(
"class",
"blocks-form-layout-property-panel-panel blocks-form-layout-property-panel-panel--compact",
)],
vec![
version_caption("極小サイズの狭幅パネル(R0228)"),
fieldset::root(
&FieldsetRootProps { size: Size::Sm },
&props,
vec![],
vec![
fieldset::legend(&props, vec![], vec![text("位置")]),
div(
vec![("class", "blocks-form-layout-property-panel-grid")],
vec![
number_field(
"blocks-form-layout-property-panel-d-pos-x",
"X",
"120",
"-9999",
"9999",
Size::Xs,
),
number_field(
"blocks-form-layout-property-panel-d-pos-y",
"Y",
"48",
"-9999",
"9999",
Size::Xs,
),
number_field(
"blocks-form-layout-property-panel-d-pos-w",
"幅",
"320",
"0",
"9999",
Size::Xs,
),
number_field(
"blocks-form-layout-property-panel-d-pos-h",
"高さ",
"180",
"0",
"9999",
Size::Xs,
),
div(
vec![("class", "blocks-form-layout-property-panel-grid-span-2")],
vec![unit_select(
"blocks-form-layout-property-panel-d-unit",
"単位",
&[("px", "px"), ("percent", "%"), ("rem", "rem")],
Size::Xs,
None,
)],
),
],
),
],
),
],
)
}
/// `form-layout-property-panel` の Demo 本体。集約元 4 件(R0225〜R0228、
/// A〜D)を並記する(#2914。モジュール doc「集約元と版の対応」節参照)。
/// 呼び出しごとに同一の `Node` を返す純関数。
pub fn demo() -> Node {
div(
vec![("class", "blocks-form-layout-property-panel-layout")],
vec![
panel_basic(),
panel_theme_collapsible(),
panel_card_font(),
panel_compact(),
],
)
}原案差分メモ
- R0225(A・主参照): 位置/レイアウト/文字の 3 節を持つプロパティパネルです(前半 #2913 で実装済み)。単位選択(px/%/rem)は X/Y/幅/高さの 4 欄で 1 個の
native_selectを共有する簡略化をしています(デザインツールの慣行に合わせた判断で、欄ごとに 4 個の selector を並べていません)。回転は単位がdeg固定のため selector を持たず、平文で添えています。segment_group(方向・揃え)はネイティブdisabledの radio で選択状態を固定表示にしています。色欄はcolor_swatch(装飾、aria-hidden)+ 16 進値のテキスト入力(field/input)の組み合わせで表現しています。 - R0226(B): テーマ選択を先頭に、開閉式の節を 2 つ持つ版です。「塗り」は開いた状態、「枠線」は閉じた状態で並記し、状態の違いを示します。
collapsibleの trigger/content は無 JS の docs サイトでは開閉できないためdisabled: trueで固定し、開いた節・閉じた節の両方を SSR へ含めることで表現しています。color_pickerはColorPickerProps { readonly: true, .. }を全パーツへ一律に渡し、表示値と見本がずれないようにしています。 - R0227(C): カード枠 + パンくずのヘッダーで包んだフォント設定の版です。パンくずの途中項目は遷移先を持たない平文表示(末尾のみ現在ページを示す
current-link)にしています。行間ラベルの横に添えたtooltipは開いた状態(OpenState::Open)で常時表示です。フッターのclipboardはコピー対象を無害な CSS 宣言 1 行にしています。 - R0228(D): 極小サイズ(
Size::Xs)の入力を並べた狭幅パネルです。fieldsetは Xs 段のサイズ variant を持たないためSize::Smのままにしています。 tooltip/clipboardの trigger は開閉・コピー操作が JS ハイドレーション前提のため(無 JS の docs サイトでは押しても機能しません)ネイティブdisabledで操作不能にしています(segment_group/collapsible/フォント欄のselectも同様です)。- 文言・配色・数値はすべて架空のものです。
関連情報: Field / Fieldset / Number Input / Native Select / Select / Color Picker / Color Swatch / Segment Group / Collapsible / Tooltip / Card / Breadcrumb / Clipboard / Separator