use crate::blocks::dummy_assets;
use fandhe_frontend_core::{div, el, p, span, text, Node};
use fandhe_frontend_pre_styled_ui::avatar::{self, AvatarProps, ImageStatus};
use fandhe_frontend_pre_styled_ui::badge::{self, BadgeProps};
use fandhe_frontend_pre_styled_ui::blockquote::{self, BlockquoteVariant};
use fandhe_frontend_pre_styled_ui::heading::{self, HeadingLevel, HeadingProps, HeadingSize};
use fandhe_frontend_pre_styled_ui::image::{self, AspectRatio, ImageProps};
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::ColorPalette;
fn meta_row(category: &str, date: &str, read_minutes: u32) -> Node {
div(
vec![("class", "blocks-content-article-meta")],
vec![
badge::badge(
&BadgeProps::default(),
vec![("data-blocks-content-article-category", "")],
vec![text(category)],
),
styled_text::text(
&TextProps {
size: TextSize::Sm,
variant: TextVariant::Muted,
..TextProps::default()
},
vec![],
vec![text(date)],
),
span(vec![("aria-hidden", "true")], vec![text("\u{00B7}")]),
styled_text::text(
&TextProps {
size: TextSize::Sm,
variant: TextVariant::Muted,
..TextProps::default()
},
vec![],
vec![text(format!("{read_minutes} 分で読了"))],
),
],
)
}
fn byline(name: &str, role: &str) -> Node {
let initials: String = name
.split_whitespace()
.filter_map(|part| part.chars().next())
.collect();
div(
vec![("class", "blocks-content-article-byline")],
vec![
avatar::root(
&AvatarProps::default(),
vec![("data-blocks-content-article-avatar", "")],
vec![avatar::fallback(
ImageStatus::Error,
vec![],
vec![text(initials)],
)],
),
div(
vec![],
vec![
styled_text::text(
&TextProps {
size: TextSize::Sm,
..TextProps::default()
},
vec![],
vec![text(name)],
),
styled_text::text(
&TextProps {
size: TextSize::Sm,
variant: TextVariant::Muted,
..TextProps::default()
},
vec![],
vec![text(role)],
),
],
),
],
)
}
fn cover(alt: &str) -> Node {
image::image(
&ImageProps {
aspect_ratio: AspectRatio::Video,
..ImageProps::new(dummy_assets::SCREENSHOT_SRC, alt)
},
vec![("data-blocks-content-article-cover", "")],
)
}
fn pull_quote() -> Node {
blockquote::root(
BlockquoteVariant::default(),
ColorPalette::default(),
vec![("data-blocks-content-article-quote", "")],
vec![
blockquote::content(
vec![],
vec![p(
vec![],
vec![text(
"小さく試して確かめてから広げる、という順番だけは崩さない。\
それが結局いちばん早い。",
)],
)],
),
blockquote::caption(
vec![],
vec![text("— Ola Bergström、Operations Coordinator")],
),
],
)
}
fn figure(alt: &str, caption: &str) -> Node {
el(
"figure",
vec![("class", "blocks-content-article-figure")],
vec![
image::image(&ImageProps::new(dummy_assets::PRODUCT_SRC, alt), vec![]),
el(
"figcaption",
vec![("class", "blocks-content-article-figcaption")],
vec![text(caption)],
),
],
)
}
fn article_header() -> Node {
div(
vec![("class", "blocks-content-article-header")],
vec![
meta_row("運用", "2026-09-18", 7),
heading::heading(
HeadingLevel::H3,
&HeadingProps {
size: HeadingSize::Xl3,
..HeadingProps::default()
},
vec![],
vec![text("小さく試して確かめる、という順番を崩さない理由")],
),
styled_text::text(
&TextProps::default(),
vec![],
vec![text(
"大きな変更ほど、いきなり全体へ適用したくなる。\
それでも一度立ち止まり、小さな範囲で確かめてから広げる \
運用に切り替えた経緯をまとめました。",
)],
),
byline("Ola Bergström", "Operations Coordinator"),
],
)
}
fn article_body() -> Node {
div(
vec![("class", "blocks-content-article-body")],
vec![
styled_text::text(
&TextProps::default(),
vec![],
vec![text(
"以前は変更のたびに全チームへ一斉に展開していましたが、\
手戻りが発生すると影響範囲が大きくなりすぎるという \
課題がありました。",
)],
),
styled_text::text(
&TextProps::default(),
vec![],
vec![text(
"そこで、まず 1 チームだけで 2 週間試し、問題が無ければ \
翌週に対象を広げるという段階的な進め方へ変更しました。",
)],
),
heading::heading(
HeadingLevel::H4,
&HeadingProps::default(),
vec![],
vec![text("小さく始めたことで見えたもの")],
),
styled_text::text(
&TextProps::default(),
vec![],
vec![text(
"小さな範囲で試すと、想定していなかった手順の抜けや \
説明不足に早い段階で気付けます。広げる前に直せるため、\
結果として全体への展開が速く終わるようになりました。",
)],
),
pull_quote(),
figure(
"段階展開の進み方を示すプレースホルダー画像",
"1 チーム → 対象チーム拡大 → 全体展開、の 3 段階で進める。",
),
separator::separator(
&SeparatorProps::default(),
vec![("data-blocks-content-article-separator", "")],
),
styled_text::text(
&TextProps {
size: TextSize::Sm,
variant: TextVariant::Muted,
..TextProps::default()
},
vec![],
vec![text(
"次回は、段階展開のチェックリストをどう運用しているかを \
紹介します。",
)],
),
],
)
}
fn instance_centered() -> Node {
div(
vec![("class", "blocks-content-article-centered")],
vec![
article_header(),
cover("記事カバー画像のプレースホルダー"),
article_body(),
],
)
}
fn instance_full_bleed() -> Node {
div(
vec![("class", "blocks-content-article-full-bleed")],
vec![
div(
vec![("class", "blocks-content-article-full-bleed-header")],
vec![article_header()],
),
cover("記事カバー画像のプレースホルダー(全幅)"),
div(
vec![("class", "blocks-content-article-full-bleed-body")],
vec![article_body()],
),
],
)
}
pub fn demo() -> Node {
div(
vec![("class", "blocks-content-article")],
vec![
styled_text::text(
&TextProps {
size: TextSize::Sm,
variant: TextVariant::Muted,
..TextProps::default()
},
vec![],
vec![text("Centered article")],
),
instance_centered(),
styled_text::text(
&TextProps {
size: TextSize::Sm,
variant: TextVariant::Muted,
..TextProps::default()
},
vec![],
vec![text("Full-bleed cover, left-aligned body")],
),
instance_full_bleed(),
],
)
}
参照は対応表 ID R0631・R0632・R0873 の 3 件を集約元とします。参照元の文言・配色・装飾・アイコンは持ち込まず、構造(領域配置・部品構成)のみを取り、デモ文言は独自の日本語で書いています。