auth-otp-verify
fandhe-frontend-pre-styled-ui の pin-input / field / button / link / heading / text 部品を合成した、ワンタイムコード(OTP)確認画面の合成例です。Blocks セクションは新規部品を追加するものではなく、既存の Themes/Primitives 部品を組み合わせた実例集であることに注意してください。
本 Demo は静的な表示例であり、<form> 要素を持たず、入力値の送信・検証・認証処理を一切行いません(確認ボタンは type="button" のまま、「Resend code」はページ遷移しないリンク風ボタン、6 桁の入力欄はいずれも name を持たず送信経路を作りません)。実際の OTP 確認処理を実装する場合は、送信処理・検証ロジックを利用者自身の Rust コードで書いてください(docs/policy/intentional-non-adoption.md §3.25 の責務境界: UI コンポーネント層はアプリケーションロジックを内包しません)。
Demo
Check your inbox
Enter the 6-digit code we sent to m•••@example.com.
digit 1 of 6digit 2 of 6digit 3 of 6digit 4 of 6digit 5 of 6digit 6 of 6
使用部品
Rust コード
Rust
use fandhe_frontend_core::{div, text, Node};
use fandhe_frontend_pre_styled_ui::button::{self, ButtonProps, ButtonVariant};
use fandhe_frontend_pre_styled_ui::field::{self, FieldOrientation, FieldRootProps};
use fandhe_frontend_pre_styled_ui::heading::{
heading, HeadingLevel, HeadingProps, HeadingSize, HeadingWeight,
};
use fandhe_frontend_pre_styled_ui::input::{FieldIds, FieldProps};
use fandhe_frontend_pre_styled_ui::link::{self, LinkProps};
use fandhe_frontend_pre_styled_ui::pin_input::{self, PinInputKind, PinInputProps};
use fandhe_frontend_pre_styled_ui::text::{self as styled_text, TextProps, TextSize, TextVariant};
use fandhe_frontend_pre_styled_ui::visually_hidden;
use fandhe_frontend_pre_styled_ui::Size;
/// `field` の `id` 派生元(`"{FIELD_ID}-control"`/`"{FIELD_ID}-helper-text"`
/// を [`field::label`]/[`field::helper_text`] が自動導出し、桁入力側は
/// リテラルで同じ値を明示配線する、モジュール doc「`field` との配線」節参照)。
const FIELD_ID: &str = "blocks-auth-otp-verify-code";
const CONTROL_ID: &str = "blocks-auth-otp-verify-code-control";
const HELPER_TEXT_ID: &str = "blocks-auth-otp-verify-code-helper-text";
/// `field::label` の既定 `id` 導出(`"{FIELD_ID}-label"`)と同じ値
/// (モジュール doc「各桁の `aria-labelledby`」節参照)。
const LABEL_ID: &str = "blocks-auth-otp-verify-code-label";
/// 部分入力状態(先頭 3 桁のみ値を持つ)で固定した 6 桁の pin-input 入力群。
/// 各桁 `input` の直後に、桁位置を読み上げる visually-hidden テキストを
/// 並べて出力する(`aria-labelledby` の参照先、モジュール doc 参照)。
fn pin_digits(props: &PinInputProps) -> Vec<Node> {
let values = ["4", "2", "7", "", "", ""];
values
.into_iter()
.enumerate()
.flat_map(|(index, value)| {
let digit_id = format!("{FIELD_ID}-digit-{}", index + 1);
let labelledby = format!("{LABEL_ID} {digit_id}");
let mut attrs: Vec<(&str, &str)> = vec![
("aria-describedby", HELPER_TEXT_ID),
("aria-labelledby", labelledby.as_str()),
];
if index == 0 {
attrs.push(("id", CONTROL_ID));
}
let input = pin_input::input(
index,
values.len(),
value,
PinInputKind::Numeric,
false,
true,
props,
false,
attrs,
);
let digit_text = visually_hidden::root(
vec![("id", digit_id.as_str())],
vec![text(format!("digit {} of {}", index + 1, values.len()))],
);
vec![input, digit_text]
})
.collect()
}
/// `auth-otp-verify` の Demo 本体。呼び出しごとに同一の `Node` を返す純関数。
pub fn demo() -> Node {
let field_props = FieldProps {
id: FIELD_ID,
ids: FieldIds::default(),
disabled: false,
invalid: false,
required: true,
readonly: false,
has_helper_text: true,
};
let orientation = FieldRootProps {
orientation: FieldOrientation::Vertical,
};
let pin_props = PinInputProps {
required: true,
..PinInputProps::default()
};
let link_button = ButtonProps {
variant: ButtonVariant::Link,
..ButtonProps::default()
};
let header = div(
vec![("class", "blocks-auth-otp-verify-header")],
vec![
heading(
HeadingLevel::H3,
&HeadingProps {
size: HeadingSize::Xl,
weight: HeadingWeight::Bold,
},
vec![],
vec![text("Check your inbox")],
),
styled_text::text(
&TextProps {
size: TextSize::Sm,
variant: TextVariant::Muted,
..TextProps::default()
},
vec![("data-blocks-auth-otp-verify-lead", "")],
vec![text("Enter the 6-digit code we sent to m•••@example.com.")],
),
],
);
let code_field = field::root(
&orientation,
&field_props,
vec![],
vec![
field::label(&field_props, vec![], vec![text("Verification code")]),
pin_input::root(
Size::Lg,
false,
false,
vec![("data-blocks-auth-otp-verify-pin", "")],
vec![pin_input::control(vec![], pin_digits(&pin_props))],
),
field::helper_text(
&field_props,
vec![],
vec![text("The code expires in 10 minutes.")],
),
],
);
let submit = button::button(
&ButtonProps::default(),
vec![("data-blocks-auth-otp-verify-submit", "")],
vec![text("Verify code")],
);
let footer = div(
vec![("class", "blocks-auth-otp-verify-footer")],
vec![
div(
vec![("class", "blocks-auth-otp-verify-resend")],
vec![
text("Didn't get the code? "),
button::button(&link_button, vec![], vec![text("Resend code")]),
],
),
link::root(
"../login-01/",
&LinkProps::default(),
vec![("data-blocks-auth-otp-verify-back", "")],
vec![text("Back to sign in")],
),
],
);
div(
vec![("data-blocks-auth-otp-verify-stack", "")],
vec![header, code_field, submit, footer],
)
}原案差分メモ
主参照 R0144 のみを使用(集約元も同一 1 件のため差分なし)。イシュー本文の仕様記述から次の判断で構成しています。
- 見出しは
heading::heading(H3): Demo 内にh1を置くとページ本体の H1 と重複するためHeadingLevel::H3を使っています(signup-05と同じ判断)。headingはdata-scope="heading"を持つため目次(TOC)の収集対象外です。 - 送信先はマスク表示: 説明文の送信先は
m•••@example.com(予約ドメイン + マスク)とし、実サービス名・実 PII を含めません。 - 6 桁は部分入力状態で固定: 先頭 3 桁のみ値を持つ状態(
4/2/7)で固定し、data-filledの見え方を静的に示します。値そのものは意味を持たないダミーです。 field::label/field::helper_textの配線:fieldのidから自動導出されるfor/aria-describedbyの対象値("{id}-control"/"{id}-helper-text")を、先頭桁のinputのid属性・全桁のinputのaria-describedby属性へ明示的に配線しています。- 「戻る」導線は
link: 実在する兄弟ページ../login-01/への遷移のためlink::rootを使っています(href="#"を使わない方針、newsletter-stackedと同型の判断)。 - 「再送信」は
ButtonVariant::Link: ページ遷移を伴わない状態変化アクションのため、login-01/signup-05と同じ判断でリンク風ボタン(<button type="button">のまま)を使っています。